提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
这里将使用全新的项目结构,将不同工具分层,区分开使用。
一、结构目录
二、对应文件
1.script.js
获取画布,引入样式和功能。
/*
课程要点:
1. 类和模块
import / expore default
calss 类名 {
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 access
window.experience = this
// Options
this.canvas = canvas
// Setup
this.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 event
this.sizes.on('resize',()=>{ // 监听页面大小,更新canvas
this.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')
// 遍历 scene
this.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.sizes
this.scene = this.experience.scene
this.canvas = this.experience.canvas
this.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.height
this.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.canvas
this.sizes = this.experience.sizes
this.scene = this.experience.scene
this.camera = this.experience.camera
this.setInstance()
}
setInstance()
{
this.instance = new THREE.WebGLRenderer({
canvas:this.canvas,
antialias:true,
})
this.instance.physicallyCorrectLights = true
this.instance.outputEncoding = THREE.sRGBEncoding
this.instance.toneMapping = THREE.CineonToneMapping
this.instance.toneMappingExposure = 1.75
this.instance.shadowMap.enabled = true
this.instance.shadowMap.type = THREE.PCFSoftShadowMap
this.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()
// setup
this.width = window.innerWidth
this.height = window.innerHeight
this.pixelRatio = Math.min(window.devicePixelRatio,2)
// Resize event
window.addEventListener('resize',()=>{
this.width = window.innerWidth
this.height = window.innerHeight
this.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()
// Setup
this.start = Date.now()
this.current = this.start
this.elapsed = 0
this.delta = 16 // 60FPS下,每帧增量时间16毫秒
window.requestAnimationFrame(()=>{ // 这里等上一秒执行 因为当前时间和上次时间减为0 所以第一帧就是0
this.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)
{
// Errors
if(typeof _names === 'undefined' || _names === '')
{
console.warn('wrong names')
return false
}
if(typeof callback === 'undefined')
{
console.warn('wrong callback')
return false
}
// Resolve names
const names = this.resolveNames(_names)
// Each name
names.forEach((_name) =>
{
// Resolve name
const name = this.resolveName(_name)
// Create namespace if not exist
if(!(this.callbacks[ name.namespace ] instanceof Object))
this.callbacks[ name.namespace ] = {}
// Create callback if not exist
if(!(this.callbacks[ name.namespace ][ name.value ] instanceof Array))
this.callbacks[ name.namespace ][ name.value ] = []
// Add callback
this.callbacks[ name.namespace ][ name.value ].push(callback)
})
// console.log(names,this.callbacks,'eeee')
return this
}
off(_names)
{
// Errors
if(typeof _names === 'undefined' || _names === '')
{
console.warn('wrong name')
return false
}
// Resolve names
const names = this.resolveNames(_names)
// Each name
names.forEach((_name) =>
{
// Resolve name
const name = this.resolveName(_name)
// Remove namespace
if(name.namespace !== 'base' && name.value === '')
{
delete this.callbacks[ name.namespace ]
}
// Remove specific callback in namespace
else
{
// Default
if(name.namespace === 'base')
{
// Try to remove from each namespace
for(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 empty
if(Object.keys(this.callbacks[ namespace ]).length === 0)
delete this.callbacks[ namespace ]
}
}
}
// Specified namespace
else 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 empty
if(Object.keys(this.callbacks[ name.namespace ]).length === 0)
delete this.callbacks[ name.namespace ]
}
}
})
return this
}
trigger(_name, _args)
{
// Errors
if(typeof _name === 'undefined' || _name === '')
{
console.warn('wrong name')
return false
}
let finalResult = null
let result = null
// Default args
const args = !(_args instanceof Array) ? [] : _args
// Resolve names (should on have one event)
let name = this.resolveNames(_name)
// Resolve name
name = this.resolveName(name[ 0 ])
// Default namespace
if(name.namespace === 'base')
{
// Try to find callback in each namespace
for(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 namespace
else 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 = _names
names = 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 = name
newName.value = parts[ 0 ]
newName.namespace = 'base' // Base namespace
// Specified namespace
if(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()
// Options
this.sources = sources
this.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 source
for(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.scene
this.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.scene
this.resources = this.experience.resources // 环境类要加载资源 当然要使用
this.debug = this.experience.debug
// Debug
if(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 = true
this.sunLight.shadow.camera.far = 15
this.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)
// Debug
if(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.environmentMapTexture
this.environmentMap.texture.encoding = THREE.sRGBEncoding
this.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.texture
child.material.envMapIntensity = this.environmentMap.intensity
child.material.needsUpdate = true
}
})
}
this.environmentMap.updateMaterials()
// Debug
if(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.scene
this.resources = this.experience.resources
this.setGeometry()
this.setTextures()
this.setMaterial()
this.setMesh()
}
setGeometry()
{
this.geometry = new THREE.CircleGeometry(5,64)
}
setTextures()
{
this.textures = {}
this.textures.color = this.resources.items.grassColorTexture
this.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.grassNormalTexture
this.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.5
this.mesh.receiveShadow = true
this.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.scene
this.resources = this.experience.resources
this.time = this.experience.time
this.debug = this.experience.debug // 调试面板
if(this.debug.active){
this.debugFolder = this.debug.ui.addFolder('fox') // addFolder 以层级形式创建新的GUI
}
// set up
this.resources = this.resources.items.foxModel
this.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.idle
this.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
}
// Debug
if(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基础学习 新的结构 狐狸
总结
在项目中如何运用,以及对应的结构区分方式方法不止这一种,择优而用!