• threejs官方demo学习(1):animation


    前言

    之前的threejs入门视频教学已经学习完了,下面会陆续学习官方demo。官方网址太卡了建议在本地进行搭建,具体见:threejs视频教程学习(1):本地搭建threeJS官网、渲染第一个场景

    官方的例子都是html格式,后续以vue3的格式进行学习。

    webgl_animation_keyframes

    代码

    <template>
        <div id="keyframes"> </div>
    </template>
    
    <script setup lang="ts">
    import { onMounted, ref } from 'vue';
    import * as THREE from 'three';
    // 引入轨道控制器
    import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
    // 引入模型加载器
    import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
    // 引入模型解压器
    import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
    // 引入房间环境,之前没有用过,应该是创建一个室内环境
    import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment';
    // 引入性能监视器,之前也没用过
    import Stats from 'three/examples/jsm/libs/stats.module';
    
    const mixer = ref();
    
    onMounted(() => {
        // 创建一个clock对象,用于跟踪时间
        const clock = new THREE.Clock();
        // 获取dom容器
        const container = document.getElementById('keyframes');
        // 创建一个性能监听器
        const stats = new Stats();
        // 修改一下位置
        stats.dom.style.position = 'relative';
        // 将性能监听器添加到容器中
        container?.appendChild(stats.dom);
        // 创建一个渲染器
        const renderer = new THREE.WebGLRenderer({
            antialias: true // 设置防锯齿
        });
        // 设置渲染器的像素比例
        renderer.setPixelRatio(window.devicePixelRatio);
        // 设置渲染的尺寸
        renderer.setSize(1000, 800);
        // 设置渲染的输出格式
        renderer.outputEncoding = THREE.sRGBEncoding;
        // 将渲染的内容添加到容器中
        container?.appendChild(renderer.domElement);
    
        // 创建一个PMREMGenerator,从立方体映射环境纹理生成预过滤的 Mipmap 辐射环境贴图
        const pmremGenerator = new THREE.PMREMGenerator(renderer);
        // 创建一个场景
        const scene = new THREE.Scene();
        // 设置背景色
        scene.background = new THREE.Color(0xbfe3dd);
        // 设置场景的纹理,从提供的场景中生成纹理
        scene.environment = pmremGenerator.fromScene(new RoomEnvironment(), 0.04).texture;
    
        // 创建相机并设置位置
        const camera = new THREE.PerspectiveCamera(40, 1.25, 1, 100);
        camera.position.set(5, 2, 8);
    
        // 设置轨道控制器
        const controls = new OrbitControls(camera, renderer.domElement);
        controls.target.set(0, 0.5, 0);
        controls.update();
        controls.enablePan = false; // 当设置为false时,控制器将不会响应用户的操作。默认值为true。
        controls.enableDamping = true; // 开启阻尼
    
        // 创建解压器并设置路径,官方文档中有draco文件夹,直接复制到自己项目里就好,另外gltf后面一定要加斜杠
        const dracoLoader = new DRACOLoader();
        dracoLoader.setDecoderPath('../../../../public/draco/gltf/');
    
        // 创建模型加载器并加载模型
        const loader = new GLTFLoader();
        loader.setDRACOLoader(dracoLoader);
        loader.load('../../../../public/LittlestTokyo.glb', gltf => {
            const model = gltf.scene;
            // 设置模型的位置
            model.position.set(1, 1, 0);
            // 设置视角
            model.scale.set(0.01, 0.01, 0.01);
            // 将模型添加到场景中
            scene.add(model);
    
            // 创建一个动画混合器,动画混合器是用于场景中特定对象的动画的播放器。当场景中的多个对象独立动画时,每个对象都可以使用同一个动画混合器。
            mixer.value = new THREE.AnimationMixer(model);
            // 设置剪辑动画
            mixer.value.clipAction(gltf.animations[0]).play();
            // 执行动画
            animate();
        });
    
        // 动画执行函数
        const animate = () => {
            // 调用动画帧执行动画
            requestAnimationFrame(animate);
            // 获取当前秒数
            const delta = clock.getDelta();
            // 更新动画混合器、轨道控制器、性能监听器
            mixer.value.update(delta);
            controls.update();
            stats.update();
            // 重新渲染
            renderer.render(scene, camera);
        };
    });
    </script>
    
    <style lang="scss" scoped>
    #keyframes {
        background-color: #bfe3dd;
        width: 1000px;
        height: 800px;
    }
    </style>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111

    效果图
    左上角的是性能监听器,点击启动,再次点击关闭
    在这里插入图片描述

    知识点

    最复杂的是模型,不过模型是做好的。主要是如何加载模型,设置动画。

    RoomEnvironment

    import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment';
    
    • 1

    应该是室内环境,但是在官方文档里没有找到。

    PMREMGenerator

    // 创建一个PMREMGenerator,从立方体映射环境纹理生成预过滤的 Mipmap 辐射环境贴图
     const pmremGenerator = new THREE.PMREMGenerator(renderer);
     // 创建一个场景
     const scene = new THREE.Scene();
     // 设置背景色
     scene.background = new THREE.Color(0xbfe3dd);
     // 设置场景的纹理,从提供的场景中生成纹理
     scene.environment = pmremGenerator.fromScene(new RoomEnvironment(), 0.04).texture;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    看官方的demo应该是用来提取场景中的纹理的。

    AnimationMixer

    官方文档的解释是:动画混合器是用于场景中特定对象的动画的播放器。
    当我将相关代码注释后,模型里的动画停止了执行,结合文档的解释,应该就是用于播放模型自身的动画。

    Clock
    该对象用于跟踪时间,结合上面动画混合器的update 方法,这里单纯用于给动画混合器提供参数。
    在这里插入图片描述

    webgl_animation_skinning_blending

    官方的例子有点复杂,看了好几遍才明白,实际上也是基于上面的动画加载,只是将各个动作给拆解开来了。在官方例子的基础上进行了简化,把一些代码进行了删除,这样更适合我这样的初学者。

    代码
    这里是有点问题的,没太搞明白,new THREE.AnimationMixer(model)单独定为一个变量使用时会提示某些方法找不到,因此就没有单独定义

    <template>
        <div id="keyframes"> </div>
    </template>
    
    <script setup lang="ts">
    import { onMounted } from 'vue';
    import * as THREE from 'three';
    // 引入gui
    import * as dat from 'dat.gui';
    // 模型加载器
    import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
    // 引入轨道控制器
    import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
    
    onMounted(() => {
        // 获取dom容器
        const container = document.getElementById('keyframes');
        // 创建一个场景
        const scene = new THREE.Scene();
        // 设置场景的背景色
        scene.background = new THREE.Color(0xa0a0a0);
        // 给场景中添加雾、雾的颜色、最小距离、最大距离。最小距离和最大距离之间是雾的范围
        scene.fog = new THREE.Fog(0xa0a0a0, 10, 50);
    
        // 创建相机
        const camera = new THREE.PerspectiveCamera(75, 1.25, 1, 1000);
        // 设置相机的位置
        camera.position.set(2, 2, -10);
        // 将相机添加到场景里
        scene.add(camera);
    
        // 创建渲染器
        const renderer = new THREE.WebGLRenderer({
            antialias: true // 设置防锯齿
        });
        // 设置渲染器的像素比例
        renderer.setPixelRatio(window.devicePixelRatio);
        // 设置渲染尺寸
        renderer.setSize(1000, 800);
        // 设置渲染输出的编码
        renderer.outputEncoding = THREE.sRGBEncoding;
        // 开启场景阴影渲染
        renderer.shadowMap.enabled = true;
        // 将渲染对象添加到容器
        container?.appendChild(renderer.domElement);
    
        // 添加平行光,平行光可以产生投影
        const dirLight = new THREE.DirectionalLight(0xffffff);
        // 设置平行光的位置
        dirLight.position.set(-3, 10, -10);
        // 设置光照产生阴影
        dirLight.castShadow = true;
    
        // 向场景中添加灯光
        scene.add(dirLight);
        // 添加坐标辅助器
        const axesHelper = new THREE.AxesHelper(10);
        scene.add(axesHelper);
        // 创建轨道控制器
        const controls = new OrbitControls(camera, renderer.domElement);
        // 设置控制器阻尼
        controls.enableDamping = true;
    
        // 渲染函数
        const render = () => {
            controls.update();
            renderer.render(scene, camera);
            requestAnimationFrame(render);
        };
        render();
        // 创建一个具有镜面高光的平面
        const mesh = new THREE.Mesh(new THREE.PlaneGeometry(100, 100), new THREE.MeshPhongMaterial({ color: 0x999999 }));
        // 把平面变成水平的
        mesh.rotation.x = -Math.PI / 2;
        // 设置接收物体投影
        mesh.receiveShadow = true;
        scene.add(mesh);
    
        // 模型
        let model = null;
        // clock对象
        const clock = new THREE.Clock();
        // 用来模拟骨骼 Skeleton 的辅助对象
        let skeleton = null;
        // 放松动作
        let idleAction = null;
        // 步行
        let walkAction = null;
        // 跑
        let runAction = null;
        // 所有的设置
        let settings = {};
        // 所有动作
        let actions = [];
        // 当前激活动作
        let active = 2;
        // 所有的动画
        let animations = [];
    
        // 创建模型加载器,加载模型
        const loader = new GLTFLoader();
        // 应该是模型比较小,这里没有引入模型解压器
        loader.load('../../../../public/Soldier.glb', gltf => {
            model = gltf.scene;
            scene.add(model);
            // 进行深度遍历
            model.traverse(function(object) {
                // 如果是物体的话开启投影
                if (object.isMesh) {
                    object.castShadow = true;
                }
            });
            // 获取模型的骨骼对象
            skeleton = new THREE.SkeletonHelper(model);
            // 将骨骼设置为可见
            skeleton.visible = true;
            // 向场景中添加骨骼辅助对象
            scene.add(skeleton);
    
            // 创建控制面板
            createPanel();
            // 获取模型的动画
            animations = gltf.animations;
            // 这里new THREE.AnimationMixer(model);不统一使用一个变量代替是因为,使用一个变量代替后报错了,不知道因为什么
            // 跑
            runAction = new THREE.AnimationMixer(model);
            runAction.clipAction(animations[1]);
            // 放松动作
            idleAction = new THREE.AnimationMixer(model);
            idleAction.clipAction(animations[2]);
            // 步行
            walkAction = new THREE.AnimationMixer(model);
            walkAction.clipAction(animations[3]).play();
    
            actions = [runAction, idleAction, walkAction];
            animate();
        });
    
        /** **************上面的是模型的加载和渲染,下面这些都与动画有关***************** */
    
        // 执行动画
        const animate = () => {
            // 调用动画帧执行动画
            requestAnimationFrame(animate);
            // 获取当前秒数
            const delta = clock.getDelta();
            // 更新动画混合器、轨道控制器、性能监听器
            actions[active].update(delta);
            controls.update();
            // 重新渲染
            renderer.render(scene, camera);
        };
    
        // 创建GUI面板
        const createPanel = () => {
            const panel = new dat.GUI({ width: 310 });
            settings = {
                visible: true, // 显示模型
                idle: false // 放松
            };
            panel.add(settings, 'visible').name('是否显示').onChange(showModel);
            panel.add(settings, 'idle').name('放松').onChange(runIdleAction);
        };
    
        // 显示模型
        const showModel = (visibility: boolean) => {
            // 隐藏模型
            model.visible = visibility;
            // 隐藏骨骼
            skeleton.visible = visibility;
        };
    
        // 放松
        const runIdleAction = (value:boolean) => {
            if (value) {
                // 将跑、走动画停止,开启放松动画
                actions[0].clipAction(animations[1]).stop();
                actions[2].clipAction(animations[3]).stop();
                actions[1].clipAction(animations[2]).play();
                // 更新激活的动作
                active = 1;
            } else {
            // 变为走
                actions[0].clipAction(animations[1]).stop();
                actions[2].clipAction(animations[3]).play();
                actions[1].clipAction(animations[2]).stop();
                // 更新激活的动作
                active = 2;
            }
        };
    });
    </script>
    
    <style lang="scss" scoped>
    #keyframes {
        width: 1000px;
        height: 800px;
        background-color: #bfe3dd;
    }
    </style>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200

    效果图
    在这里插入图片描述

    知识点

    Fog
    在常见中添加雾,具体内容可以看官方文档

     scene.fog = new THREE.Fog(0xa0a0a0, 10, 50);
    
    • 1

    如何产生投影
    这个之前梳理过,具体见:threejs视频教程学习(4):贴图、材质、光线

    SkeletonHelper
    SkeletonHelper,用来模拟骨骼 Skeleton 的辅助对象.。该辅助对象使用 LineBasicMaterial 材质。
    模型里面必须有骨骼辅助对象,才能够获取到

    // 获取模型的骨骼对象
    skeleton = new THREE.SkeletonHelper(model);
    // 将骨骼设置为可见
    skeleton.visible = true;
    // 向场景中添加骨骼辅助对象
    scene.add(skeleton);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    gui
    gui的基本使用见:threejs视频教程学习(3):应用图形用户界面更改变量 (dat.gui的简单使用)

    webgl_animation_skinning_additive_blending

    做的比较简陋,好多细节都没有实现。
    代码

    <template>
        <div id="keyframes"> </div>
    </template>
    
    <script setup lang="ts">
    import { onMounted } from 'vue';
    import * as THREE from 'three';
    // 引入gui
    import * as dat from 'dat.gui';
    // 模型加载器
    import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
    // 引入轨道控制器
    import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
    
    onMounted(() => {
        // 获取dom
        const container = document.getElementById('keyframes');
        // 走
        let walkAction = null;
        let walk = null;
        // 跑
        let runAction = null;
        let run = null;
        const clock = new THREE.Clock();
    
        // 创建场景
        const scene = new THREE.Scene();
        // 设置背景色
        scene.background = new THREE.Color(0xa0a0a0);
        // 设置雾气
        scene.fog = new THREE.Fog(0xa0a0a0, 10, 50);
        // 设置半球光
        const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444);
        hemiLight.position.set(0, 20, 0);
        scene.add(hemiLight);
        // 设置平行光
        const dirLight = new THREE.DirectionalLight(0xffffff);
        dirLight.position.set(3, 10, 10);
        dirLight.castShadow = true; // 开启阴影
        scene.add(dirLight);
    
        // 创建透视相机
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 100);
        camera.position.set(-1, 2, 3);
        scene.add(camera);
    
        // 创建渲染器
        const renderer = new THREE.WebGLRenderer({
            antialias: true // 抗锯齿
        });
        renderer.setPixelRatio(window.devicePixelRatio); // 像素比
        renderer.setSize(window.innerWidth, window.innerHeight); // 渲染尺寸
        renderer.outputEncoding = THREE.sRGBEncoding;
        renderer.shadowMap.enabled = true; // 开启场景阴影渲染
        container?.appendChild(renderer.domElement);
    
        // 创建轨道控制器
        const control = new OrbitControls(camera, renderer.domElement);
        control.enableZoom = false; // 关闭缩放
        control.target.set(0, 1, 0);
        control.update(); // 更新控制器
    
        // 创建一个平面
        const mesh = new THREE.Mesh(new THREE.PlaneGeometry(100, 100), new THREE.MeshPhongMaterial({ color: 0x999999, depthWrite: false }));
        mesh.rotation.x = -Math.PI / 2;
        mesh.receiveShadow = true; // 设置平面接收阴影
        scene.add(mesh);
    
        // 加载模型
        const loader = new GLTFLoader();
        loader.load('../../../../public/Xbot.glb', gltf => {
            // 模型
            const model = gltf.scene;
            scene.add(model);
            // 递归找到物体开启阴影
            model.traverse(object => {
                if (object.isMesh) object.castShadow = true;
            });
            // 获取模型的动作
            const animations = gltf.animations;
            console.log('动画', animations);
    
            // 跑
            runAction = new THREE.AnimationMixer(model);
            run = runAction.clipAction(animations[3]);
    
            // 走
            walkAction = new THREE.AnimationMixer(model);
            walk = walkAction.clipAction(animations[6]);
    
            animate();
        });
    
        // 执行动画
        const animate = () => {
            // 调用动画帧执行动画
            requestAnimationFrame(animate);
            // 获取当前时间,这个不能直接new ,不然动画不会生效
            const delta = clock.getDelta();
            if (settings.run) {
                run.play();
                runAction.update(delta);
            } else {
                walk.play();
                walkAction.update(delta);
            }
            control.update();
            // 重新渲染
            renderer.render(scene, camera);
        };
    
        // 添加GUI
        let settings = {};
        const panel = new dat.GUI({ width: 310 });
        settings = {
            run: true,
            speed: 1
        };
        panel.add(settings, 'run').name('跑');
        panel.add(settings, 'speed').min(0).max(5)
            .onChange((value) => {
                // 通过修改AnimationAction对象的timeScale(时间比例因子)来调节动画的速度
                if (settings.run) {
                    runAction.timeScale = value;
                } else {
                    walkAction.timeScale = value;
                }
            });
    });
    </script>
    
    <style lang="scss" scoped>
    #keyframes {
        width: 1000px;
        height: 800px;
        background-color: #bfe3dd;
    }
    </style>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138

    效果图
    在这里插入图片描述

    知识点

    主要是AnimationAction 动画对象的应用。动画的速度是通过修改timeScale 来实现的,动作之间的过渡是通过修改weight来实现的,动作过渡没太弄清楚。

    详情内容可以看官方文档。

  • 相关阅读:
    Java贪吃蛇小游戏
    docker 部署Redis集群(三主三从,以及扩容、缩容)
    网页JS自动化脚本(五)修改文字元素的内容和大小
    【服务器数据恢复】hp服务器raid5磁盘掉线导致raid5不可用的数据恢复案例
    分布式事务解决方案之TCC
    Qt的ui文件不能简单复制
    非零基础自学Java (老师:韩顺平) 第4章 运算符 4.5 赋值运算符 && 4.6 三元运算符 && 4.7 运算符优先级
    HTML5 Web SQL: A Comprehensive Guide
    Docker的优势和应用场景
    基于Vue+SpringBoot的大病保险管理系统 开源项目
  • 原文地址:https://blog.csdn.net/weixin_41897680/article/details/127923155