提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
这里将使用全新的项目结构,将不同工具分层,区分开使用。
一、结构目录
二、对应文件
1.script.js
获取画布,引入样式和功能。
/* 课程要点:1. 类和模块import / expore defaultcalss 类名 {constructor(){}}new 类名() // 实例化calss 类 extends 类 { // 继承contructor(){super() // 基于类的引用} } 2.新项目建立一个时间,尺寸类的函数。EventEmitter.js回调如何在Camera.js使用尺寸大小,画布等1.全局变量2.传参3.保存实例化,在引入 在Experience.js保存this ,在Camera中引入Experience.js 再次实例化时判断instance是否已经实例化,若是则不创建新的 (使用这个)*/import './style.css'
import Experience from './Experience/Experience'const experience = new Experience(document.querySelector('canvas.webgl'))
2.Experience.js
2-1 Experience.js
总览功能,初始化创建实例,引入各个类,创建实例,监听页面大小,定时刷新渲染,执行各个类的方法,实现功能
import * as THREE from 'three'
import Sizes from './Utils/Sizes.js'
import Time from './Utils/Time.js'
import Camera from './Camera.js'
import Renderer from './Renderer.js'
import World from './World/World.js'
import Resources from './Utils/Resources.js'
import Debug from './Utils/Debug.js'
import sources from './sources.js' // 资源 s没有大写let instance = null
export default class Experience{constructor(canvas){if(instance){ // 很重要,其他引入时,可以拿到同一个实例内容return instance}instance = this// Global accesswindow.experience = this// Optionsthis.canvas = canvas// Setupthis.debug = new Debug()this.sizes = new Sizes() // 尺寸类this.time = new Time() // 时间类this.scene = new THREE.Scene() // 场景this.resources = new Resources(sources) // 资源类 加载器在这里已经加载完成this.camera = new Camera() // 照相机this.renderer = new Renderer() // 渲染类this.world = new World() // 将展示的3D 在此处 环境贴图,资源用在此处// Sizes resize eventthis.sizes.on('resize',()=>{ // 监听页面大小,更新canvasthis.resize()})// Time tick evnet/* 继承关系: Time 类继承自 EventEmitter,因此 Time 类实例拥有 EventEmitter 中定义的 trigger() 方法。trigger('tick') 的作用:this.trigger('tick') 会在 EventEmitter 中寻找并调用已注册到 tick 事件的所有回调函数。事件注册 :在 Experience 类的构造函数中,this.time.on('tick', () => { this.update() }) 注册了 tick 事件的回调函数(this.update() 方法)。on() 方法会将该回调添加到 EventEmitter 的 callbacks 中。事件触发:当 tick() 方法中的 this.trigger('tick') 被调用时,EventEmitter 的 trigger() 方法会找到所有与tick 事件关联的回调并依次执行它们,因此Experience 中的 update() 方法也会被调用*/this.time.on('tick',()=>{ // 定时更新,刷新帧this.update()})}resize(){this.camera.resize()this.renderer.resize()}update(){this.camera.update()this.world.update()this.renderer.update()}destroy(){ // 销毁this.time.off('tick')this.time.off('resize')// 遍历 scenethis.scene.traverse((child)=>{if(child instanceof THREE.Mesh){child.geometry.dispose()for(const key in child.material){const value = child.material[key]if(value && typeof value.dispose === 'function'){ // 判断某个值有功能 就销毁value.dispose()}}}})this.camera.controls.dispose()this.renderer.instance.dispose()if(this.debug.active)this.debug.ui.destroy()}
}
2-2 Camera.js
创建照相机和轨道控制器,resize更新尺寸,update更新轨道,在experience.js中使用
import * as THREE from 'three'
import { OrbitControls } from "three/examples/jsm/controls/orbitcontrols";
import Experience from "./Experience.js";
export default class Camera {constructor(){this.experience = new Experience()this.sizes = this.experience.sizesthis.scene = this.experience.scenethis.canvas = this.experience.canvasthis.setInstance()this.setObitControls()}setInstance(){ // 创建照相机this.instance = new THREE.PerspectiveCamera(75,this.sizes.width / this.sizes.height,0.1,100)this.instance.position.set(6,4,8)this.scene.add(this.instance)}setObitControls(){ // 轨道控制器this.controls = new OrbitControls(this.instance,this.canvas)this.controls.enableDamping = true}resize(){console.log('update camera for canvas')this.instance.aspect = this.sizes.width/this.sizes.heightthis.instance.updateProjectionMatrix()}update(){this.controls.update()}
}
2-3 Renderer.js
创建渲染,设置对应属性。
import * as THREE from 'three'
import Experience from './Experience.js'export default class Renderer
{constructor(){this.experience = new Experience() // 初始化实例获取数据this.canvas = this.experience.canvasthis.sizes = this.experience.sizesthis.scene = this.experience.scenethis.camera = this.experience.camerathis.setInstance()}setInstance(){this.instance = new THREE.WebGLRenderer({canvas:this.canvas,antialias:true,})this.instance.physicallyCorrectLights = truethis.instance.outputEncoding = THREE.sRGBEncodingthis.instance.toneMapping = THREE.CineonToneMappingthis.instance.toneMappingExposure = 1.75this.instance.shadowMap.enabled = truethis.instance.shadowMap.type = THREE.PCFSoftShadowMapthis.instance.setClearColor('#211d20')this.instance.setSize(this.sizes.width,this.sizes.height)this.instance.setPixelRatio(this.sizes.pixelRatio)}resize(){this.instance.setSize(this.sizes.width,this.sizes.height)this.instance.setPixelRatio(this.sizes.pixelRatio)}update(){this.instance.render(this.scene,this.camera.instance)}
}
2-4 sources.js
资源地址,environmentMapTexture为环境贴图,texture贴图,foxModel模型
export default [{name: 'environmentMapTexture',type: 'cubeTexture',path:['textures/environmentMap/px.jpg','textures/environmentMap/nx.jpg','textures/environmentMap/py.jpg','textures/environmentMap/ny.jpg','textures/environmentMap/pz.jpg','textures/environmentMap/nz.jpg']},{name: 'grassColorTexture',type: 'texture',path: 'textures/dirt/color.jpg'},{name: 'grassNormalTexture',type: 'texture',path: 'textures/dirt/normal.jpg'},{name: 'foxModel',type: 'gltfModel',path: 'models/Fox/glTF/Fox.gltf'}
]
3.Utils
3-1 Sizes.js
监听页面,获取尺寸,暴露执行this.trigger(resize)
import EventEmitter from "./EventEmitter"
export default class Sizes extends EventEmitter{constructor(){super()// setupthis.width = window.innerWidththis.height = window.innerHeightthis.pixelRatio = Math.min(window.devicePixelRatio,2)// Resize eventwindow.addEventListener('resize',()=>{this.width = window.innerWidththis.height = window.innerHeightthis.pixelRatio = Math.min(window.devicePixelRatio,2)this.trigger('resize')})}
}
3-2 Time.js
获取上一秒和这一秒,花了多少时间,帧率
import EventEmitter from "./EventEmitter.js";
export default class Time extends EventEmitter{constructor(){super()// Setupthis.start = Date.now()this.current = this.startthis.elapsed = 0this.delta = 16 // 60FPS下,每帧增量时间16毫秒window.requestAnimationFrame(()=>{ // 这里等上一秒执行 因为当前时间和上次时间减为0 所以第一帧就是0this.tick()})}tick(){const currentTime = Date.now()this.delta = currentTime - this.current // 当前时间 - 上次时间this.current = currentTime // 更新this.elapsed = this.current - this.start // 花了多少时间this.trigger('tick') // 计时器触发 ,回调触发tick回调的所有函数window.requestAnimationFrame(()=>{ // 保留上下文this.tick()})}
}
3-3 EventEmitter.js
提供on,trigger方法 回调函数 on方法添加有对应名称的方法,trigger 会执行所有callbacks中所有的方法
export default class EventEmitter
{constructor(){this.callbacks = {}this.callbacks.base = {}}on(_names, callback){// Errorsif(typeof _names === 'undefined' || _names === ''){console.warn('wrong names')return false}if(typeof callback === 'undefined'){console.warn('wrong callback')return false}// Resolve namesconst names = this.resolveNames(_names)// Each namenames.forEach((_name) =>{// Resolve nameconst name = this.resolveName(_name)// Create namespace if not existif(!(this.callbacks[ name.namespace ] instanceof Object))this.callbacks[ name.namespace ] = {}// Create callback if not existif(!(this.callbacks[ name.namespace ][ name.value ] instanceof Array))this.callbacks[ name.namespace ][ name.value ] = []// Add callbackthis.callbacks[ name.namespace ][ name.value ].push(callback)})// console.log(names,this.callbacks,'eeee')return this}off(_names){// Errorsif(typeof _names === 'undefined' || _names === ''){console.warn('wrong name')return false}// Resolve namesconst names = this.resolveNames(_names)// Each namenames.forEach((_name) =>{// Resolve nameconst name = this.resolveName(_name)// Remove namespaceif(name.namespace !== 'base' && name.value === ''){delete this.callbacks[ name.namespace ]}// Remove specific callback in namespaceelse{// Defaultif(name.namespace === 'base'){// Try to remove from each namespacefor(const namespace in this.callbacks){if(this.callbacks[ namespace ] instanceof Object && this.callbacks[ namespace ][ name.value ] instanceof Array){delete this.callbacks[ namespace ][ name.value ]// Remove namespace if emptyif(Object.keys(this.callbacks[ namespace ]).length === 0)delete this.callbacks[ namespace ]}}}// Specified namespaceelse if(this.callbacks[ name.namespace ] instanceof Object && this.callbacks[ name.namespace ][ name.value ] instanceof Array){delete this.callbacks[ name.namespace ][ name.value ]// Remove namespace if emptyif(Object.keys(this.callbacks[ name.namespace ]).length === 0)delete this.callbacks[ name.namespace ]}}})return this}trigger(_name, _args){// Errorsif(typeof _name === 'undefined' || _name === ''){console.warn('wrong name')return false}let finalResult = nulllet result = null// Default argsconst args = !(_args instanceof Array) ? [] : _args// Resolve names (should on have one event)let name = this.resolveNames(_name)// Resolve namename = this.resolveName(name[ 0 ])// Default namespaceif(name.namespace === 'base'){// Try to find callback in each namespacefor(const namespace in this.callbacks){if(this.callbacks[ namespace ] instanceof Object && this.callbacks[ namespace ][ name.value ] instanceof Array){this.callbacks[ namespace ][ name.value ].forEach(function(callback){result = callback.apply(this, args)if(typeof finalResult === 'undefined'){finalResult = result}})}}}// Specified namespaceelse if(this.callbacks[ name.namespace ] instanceof Object){if(name.value === ''){console.warn('wrong name')return this}this.callbacks[ name.namespace ][ name.value ].forEach(function(callback){result = callback.apply(this, args)if(typeof finalResult === 'undefined')finalResult = result})}return finalResult}resolveNames(_names){let names = _namesnames = names.replace(/[^a-zA-Z0-9 ,/.]/g, '')names = names.replace(/[,/]+/g, ' ')names = names.split(' ')return names}resolveName(name){const newName = {}const parts = name.split('.')newName.original = namenewName.value = parts[ 0 ]newName.namespace = 'base' // Base namespace// Specified namespaceif(parts.length > 1 && parts[ 1 ] !== ''){newName.namespace = parts[ 1 ]}return newName}
}
3-4 Resources.js
加载器初始化gltfLoader,textLoader,cubeTextureLoader
import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/gltfloader";
import EventEmitter from "./EventEmitter";export default class Resources extends EventEmitter{constructor(sources){super()// Optionsthis.sources = sourcesthis.items = {} // 资源this.toLoad = this.sources.length // 全部资源长度this.loaded = 0 // 资源初始this.setLoaders()this.startLoading()}setLoaders(){this.loaders = {} // 加载器this.loaders.gltfLoader = new GLTFLoader()this.loaders.textureLoader = new THREE.TextureLoader()this.loaders.cubeTextureLoader = new THREE.CubeTextureLoader()}startLoading(){ // 要加载的资源// load are sourcefor(const source of this.sources){if(source.type === 'gltfModel'){this.loaders.gltfLoader.load(source.path,(file)=>{this.sourceLoaded(source,file)})}else if(source.type === 'texture'){this.loaders.textureLoader.load(source.path,(file)=>{this.sourceLoaded(source,file)})}else if(source.type === 'cubeTexture'){this.loaders.cubeTextureLoader.load(source.path,(file)=>{this.sourceLoaded(source,file)})}}}sourceLoaded(source,file){this.items[source.name] = file // 存储this.loaded++ // 资源加载if(this.loaded === this.toLoad){ // 判断资源是否加载完成this.trigger('ready')}}
}
3-5 Debug.js
lil-GUI调试
import * as dat from 'lil-gui'
export default class Debug{constructor(){this.active = window.location.hash === '#debug'if(this.active){this.ui = new dat.GUI()}}
}
4.World
4-1 World.js
加载对应的资源,如模型 环境 和贴图等
import Experience from '../Experience.js'
import Environment from './Environment.js'
import Floor from './Floor.js'
import Fox from './Fox.js'export default class World
{constructor(){this.experience = new Experience()this.scene = this.experience.scenethis.resources = this.experience.resources // 资源// const testMesh = new THREE.Mesh( // 创建网格// new THREE.BoxGeometry(1,1,1),// new THREE.MeshStandardMaterial()// )// this.scene.add(testMesh)this.resources.on('ready',()=>{ // 判断是否完成资源加载 ,然后使用this.floor = new Floor() // 先添加底部 ,因为环境贴图若更改强度,地板没加载则无法适应强度this.fox = new Fox() // 狐狸模型this.environment = new Environment() // 环境类})}update(){if(this.fox){this.fox.update()}}
}
4-2 Environment.js
环境类 阳光设置,环境贴图等属性设置
import * as THREE from 'three'
import Experience from '../Experience.js'export default class Environment
{constructor(){this.experience = new Experience()this.scene = this.experience.scenethis.resources = this.experience.resources // 环境类要加载资源 当然要使用this.debug = this.experience.debug// Debugif(this.debug.active){this.debugFolder = this.debug.ui.addFolder('environment')}this.setSunLight() this.setEnvironmentMap() // 设置环境贴图}setSunLight(){ // 设置太阳光 ,光线this.sunLight = new THREE.DirectionalLight(0xffffff,0.5)this.sunLight.castShadow = truethis.sunLight.shadow.camera.far = 15this.sunLight.shadow.mapSize.set(1024,1024) // 数值越大 阴影越清晰this.sunLight.shadow.normalBias = 0.05 // 偏移this.sunLight.position.set(3.5,2,-1.25)this.scene.add(this.sunLight)// Debugif(this.debug.active){this.debugFolder.add(this.sunLight,'intensity').name('sunLightIntensity').min(0).max(10).step(0.001)this.debugFolder.add(this.sunLight.position,'x').name('sunLightX').min(-5).max(5).step(0.001)this.debugFolder.add(this.sunLight.position,'y').name('sunLightY').min(-5).max(5).step(0.001)this.debugFolder.add(this.sunLight.position,'z').name('sunLightZ').min(-5).max(5).step(0.001)}}setEnvironmentMap(){ // 环境贴图this.environmentMap = {}this.environmentMap.intensity = 0.4 // 环境贴图强度this.environmentMap.texture = this.resources.items.environmentMapTexturethis.environmentMap.texture.encoding = THREE.sRGBEncodingthis.scene.environment = this.environmentMap.texture // 环境为场景添加灯光this.environmentMap.updateMaterials = () =>{this.scene.traverse((child)=>{ // 更新环境贴图if(child instanceof THREE.Mesh && child.material instanceof THREE.MeshStandardMaterial){child.material.envmap = this.environmentMap.texturechild.material.envMapIntensity = this.environmentMap.intensitychild.material.needsUpdate = true}})}this.environmentMap.updateMaterials()// Debugif(this.debug.active){this.debugFolder.add(this.environmentMap,'intensity').name('envMapIntensity').min(0).max(4).step(0.001).onChange(this.environmentMap.updateMaterials)}}
}
4-3 Floor.js
地板创建,设置贴图(加载器已经加载完成直接设置),创建材质,创建网格,添加到场景
import * as THREE from 'three'
import Experience from '../Experience.js'export default class Floor
{constructor(){this.experience = new Experience()this.scene = this.experience.scenethis.resources = this.experience.resourcesthis.setGeometry()this.setTextures()this.setMaterial()this.setMesh()}setGeometry(){this.geometry = new THREE.CircleGeometry(5,64)}setTextures(){this.textures = {}this.textures.color = this.resources.items.grassColorTexturethis.textures.color.encoding = THREE.sRGBEncoding // 编码this.textures.color.repeat.set(1.5,1.5)this.textures.color.wrapS = THREE.RepeatWrapping;this.textures.color.wrapT = THREE.RepeatWrapping;this.textures.normal = this.resources.items.grassNormalTexturethis.textures.normal.repeat.set(1.5,1.5)this.textures.normal.wrapS = THREE.RepeatWrapping;this.textures.normal.wrapT = THREE.RepeatWrapping;}setMaterial(){this.material = new THREE.MeshStandardMaterial({map:this.textures.color,normalMap:this.textures.normal,})}setMesh(){this.mesh = new THREE.Mesh(this.geometry,this.material)this.mesh.rotation.x = - Math.PI * 0.5this.mesh.receiveShadow = truethis.scene.add(this.mesh)}
}
4-4 Fox.js
同上,不过是设置模型(模型加载器已经加载)
import * as THREE from 'three'
import Experience from '../Experience.js'export default class Fox
{constructor(){this.experience = new Experience()this.scene = this.experience.scenethis.resources = this.experience.resourcesthis.time = this.experience.timethis.debug = this.experience.debug // 调试面板if(this.debug.active){this.debugFolder = this.debug.ui.addFolder('fox') // addFolder 以层级形式创建新的GUI}// set upthis.resources = this.resources.items.foxModelthis.setModel()this.setAnimation()}setModel(){this.model = this.resources.scene // 整个模型this.model.scale.set(0.02,0.02,0.02) // 设置模型位置this.scene.add(this.model)this.model.traverse((child)=>{ // 遍历if(child instanceof THREE.Mesh){child.castShadow = true}})}setAnimation(){this.animation = {}this.animation.mixer = new THREE.AnimationMixer(this.model)this.animation.actions = {}this.animation.actions.idle = this.animation.mixer.clipAction(this.resources.animations[0])this.animation.actions.walking = this.animation.mixer.clipAction(this.resources.animations[1])this.animation.actions.runing = this.animation.mixer.clipAction(this.resources.animations[2])this.animation.actions.current = this.animation.actions.idlethis.animation.actions.current.play()// 开始 同时需要在更新时间时候 更新关键帧this.animation.play = (name) => { // 如何实现动画之间的平稳过渡 const newAction = this.animation.actions[name] // 新动画const oldAction = this.animation.actions.current // 旧动画newAction.reset()newAction.play()newAction.crossFadeFrom(oldAction, 1)this.animation.actions.current = newAction}// Debugif(this.debug.active){const debugObject = {playIdle:()=>{this.animation.play('idle')},playWalking:()=>{this.animation.play('walking')},playRuning:()=>{this.animation.play('runing')},}this.debugFolder.add(debugObject,'playIdle')this.debugFolder.add(debugObject,'playWalking')this.debugFolder.add(debugObject,'playRuning')}}update(){this.animation.mixer.update(this.time.delta * 0.001) // 提供增量时间,更新}
}
5. new Experience()
其中这段代码可以保证引入Experience.js的文件可以使用它内部的变量。以word.js举例
this.scene = this.experience.scene
this.resources = this.experience.resources // 资源
是可以使用的
let instance = null
export default class Experience{constructor(canvas){if(instance){ // 很重要,其他引入时,可以拿到同一个实例内容return instance}instance = this}
import Experience from '../Experience.js'export default class World
{constructor(){this.experience = new Experience()}
三,效果
three.js基础学习 新的结构 狐狸
总结
在项目中如何运用,以及对应的结构区分方式方法不止这一种,择优而用!