【Three.js基础学习】22.New project structure

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

前言

这里将使用全新的项目结构,将不同工具分层,区分开使用。


一、结构目录

二、对应文件

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基础学习 新的结构 狐狸


总结

在项目中如何运用,以及对应的结构区分方式方法不止这一种,择优而用!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值