0基础实现人物看展特效|大帅老猿threejs特训

480 阅读5分钟

一.搭建最简易的基础场景

关于threejs基本知识:

  • 场景scene

  • 摄像机camera

  • 渲染器 renderer

  • 几何体geometry

    //引入three.js
    import * as THREE from 'three';
    import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
    ​
    //1.创建场景
    const scene = new THREE.Scene()
    //2.创建相机
    const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.01, 10)
    //设置相机的位置,相机需放到物体的外面才能看到物体的展示
    camera.position.set(0.3, 0.3, 1.5)
    ​
    //3.创建渲染器
    const renderer = new THREE.WebGLRenderer({antialias: true})
    renderer.setSize(window.innerWidth, window.innerHeight)
    document.body.appendChild(renderer.domElement)
    ​
    //4.创建物体
     const cube = new THREE.Mesh(
      new THREE.BoxGeometry(1, 1, 1),
      new THREE.MeshBasicMaterial({color: 0x00ff00})
     )
     scene.add(cube)
    ​
     const controls = new OrbitControls(camera, renderer.domElement)
    ​
     function animate() {
      requestAnimationFrame(animate)
      renderer.render(scene, camera)
        controls.update()
    }
    animate()
    

    效果:
    简易场景.gif

二.加载展厅空间

需要用到的资源:

展馆的模型: 展馆模型.gif 贴图: image-20230103161400816.png 视频: image-20230103163838516.png

import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
new GLTFLoader().load('../resources/models/zhanguan.glb', (gltf) => {
    scene.add(gltf.scene);
    gltf.scene.traverse((child)=>{
        if (child.name === '2023') {
            const video = document.createElement('video');
            video.src = "./resources/yanhua.mp4";
            video.muted = true;
            video.autoplay = "autoplay";
            video.loop = true;
            video.play();
            
            const videoTexture = new THREE.VideoTexture(video);
            const videoMaterial = new THREE.MeshBasicMaterial({map: videoTexture});
        
            child.material = videoMaterial;
        }
        if (child.name === '大屏幕01') {
            const video = document.createElement('video');
            video.src = "./resources/video01.mp4";
            video.muted = true;
            video.autoplay = "autoplay";
            video.loop = true;
            video.play();
            
            const videoTexture = new THREE.VideoTexture(video);
            const videoMaterial = new THREE.MeshBasicMaterial({map: videoTexture});
        
            child.material = videoMaterial;
        }
        if (child.name === '大屏幕02' || child.name === '操作台屏幕') {
            const video = document.createElement('video');
            video.src = "./resources/video01.mp4";
            video.muted = true;
            video.autoplay = "autoplay";
            video.loop = true;
            video.play();
            
            const videoTexture = new THREE.VideoTexture(video);
            const videoMaterial = new THREE.MeshBasicMaterial({map: videoTexture});
        
            child.material = videoMaterial;
        }
        if (child.name === '环形屏幕') {
            const video = document.createElement('video');
            video.src = "./resources/video02.mp4";
            video.muted = true;
            video.autoplay = "autoplay";
            video.loop = true;
            video.play();
            
            const videoTexture = new THREE.VideoTexture(video);
            const videoMaterial = new THREE.MeshBasicMaterial({map: videoTexture});
        
            child.material = videoMaterial;
        }
        if (child.name === '柱子屏幕') {
            const video = document.createElement('video');
            video.src = "./resources/yanhua.mp4";
            video.muted = true;
            video.autoplay = "autoplay";
            video.loop = true;
            video.play();
            
            const videoTexture = new THREE.VideoTexture(video);
            const videoMaterial = new THREE.MeshBasicMaterial({map: videoTexture});
        
            child.material = videoMaterial;
        }
    })
​
    mixer = new THREE.AnimationMixer(gltf.scene);
    const clips = gltf.animations; // 播放所有动画
    clips.forEach(function (clip) {
        const action = mixer.clipAction(clip);
        action.loop = THREE.LoopOnce;
        // 停在最后一帧
        action.clampWhenFinished = true;
        action.play();
    });
​
})
function animate() {
    requestAnimationFrame(animate);
    renderer.render(scene, camera);
    controls.update();
    if (mixer) {
        mixer.update(0.02);
    }
}
animate();

效果:

展馆效果.gif

三.加载角色和控制角色

实现角色的走动需要把OrbitControls禁用:

// const controls = new OrbitControls(camera, renderer.domElement); // controls.update();

增加相机对象指向坐标原点:

camera.lookAt(new THREE.Vector3(0, 0, 0));

let playerMesh;
let actionWalk, actionIdle;
const lookTarget = new THREE.Vector3(0, 2, 0);
new GLTFLoader().load('../resources/models/player.glb', (gltf) => {
    playerMesh = gltf.scene;
    scene.add(gltf.scene);
​
    playerMesh.traverse((child)=>{
        child.receiveShadow = true;
        child.castShadow = true;
    })
​
    playerMesh.position.set(0, 0, 11.5);
    //控制人物朝向朝里,旋转180弧度
    playerMesh.rotateY(Math.PI);
    playerMesh.add(camera);
    //设置相机的位置
    camera.position.set(0, 2, -5);
    //实现相机跟随人移动
    camera.lookAt(lookTarget);
    
    //设置点光源,提高人物的亮度
    const pointLight = new THREE.PointLight(0xffffff, 1.5);
    playerMesh.add(pointLight);
    pointLight.position.set(0, 1.8, -1);
​
    playerMixer = new THREE.AnimationMixer(gltf.scene);
    // THREE.AnimationUtils.subclip动作面片剪辑工具
    //剪辑走路动作
    const clipWalk = THREE.AnimationUtils.subclip(gltf.animations[0], 'walk', 0, 30);
    actionWalk = playerMixer.clipAction(clipWalk);
    // actionWalk.play();
    //剪辑暂停动作
    const clipIdle = THREE.AnimationUtils.subclip(gltf.animations[0], 'idle', 31, 281);
    actionIdle = playerMixer.clipAction(clipIdle);
    actionIdle.play();
});

实现角色根据鼠标偏移方向左右偏移

let preClientX;
window.addEventListener('mousemove', (e) => {
    if (preClientX && playerMesh) {
        playerMesh.rotateY(-(e.clientX - preClientX) * 0.01);
    }
    preClientX = e.clientX;
});

如果直接一个动画切换到另外一个动画,看起来比较生硬,我们使用渐变切换动画工作函数过渡动画:

function crossPlay(curAction, newAction) {
    curAction.fadeOut(0.3);
    newAction.reset();
    newAction.setEffectiveWeight(1);
    newAction.play();
    newAction.fadeIn(0.3);
}

实现人物走路动画:

//isWalk是否走路的标识,用来标记渐变动作切换函数crossPlay只调用一次
let isWalk = false;
const playerHalfHeight = new THREE.Vector3(0, 0.8, 0);
window.addEventListener('keydown', (e) => {
    if (e.key === 'w') {
        if (!isWalk) {
            crossPlay(actionIdle, actionWalk);
            isWalk = true;
        }
    }
})
​
window.addEventListener('keyup', (e) => {
    if (e.key === 'w') {
        crossPlay(actionWalk, actionIdle);
        isWalk = false;
    }
});

效果:

角色走动.gif

四.射线碰撞

const playerHalfHeight = new THREE.Vector3(0, 0.8, 0);
//curPos:记录角色当前的位置
const curPos = playerMesh.position.clone();
//frontPos:角色向前一步的位置,并把位置记录下来
playerMesh.translateZ(1);
const frontPos = playerMesh.position.clone();
//角色退回原来的位置
playerMesh.translateZ(-1);
//frontPos向量减去curPos的向量,normalize把向量缩放成1
const frontVector3 = frontPos.sub(curPos).normalize()
​
//playerMesh.position.clone().add(playerHalfHeight) 角色的位置增加半个高度
const raycasterFront = new THREE.Raycaster(playerMesh.position.clone().add(playerHalfHeight), frontVector3);
//检测哪些物体
const collisionResultsFrontObjs = raycasterFront.intersectObjects(scene.children);
//如果角色与场景的距离大于1,就可以向前移动
if (collisionResultsFrontObjs && collisionResultsFrontObjs[0] && collisionResultsFrontObjs[0].distance > 1) {
    playerMesh.translateZ(0.1);
}

效果:

角色走动动画.gif

五.阴影

阴影四步:

  1. 打开renderer阴影

     const renderer = new THREE.WebGLRenderer();
     renderer.shadowMap.enabled = true;
    
  2. 2.1打开灯光阴影,设置灯光阴影大小

    const light = new THREE.DirectionalLight( 0xffffff, 1 );
    light.position.set( 0, 1, 0 );
    light.castShadow = true; // 默认值false
    scene.add( light );
    

    2.2设置灯光阴影贴图大小

    light.shadow.mapSize.width = 512; //默认值
    light.shadow.mapSize.height = 512; //默认值
    
  3. 设置阴影体远近大小,不在这个之内的显示不出来

    light.shadow.camera.near = 0.5; //默认值
    light.shadow.camera.far = 500; //默认值
    light.shadow.camera.left = -shadowDistance;
    light.shadow.camera.right = shadowDistance;
    light.shadow.camera.top = shadowDistance;
    light.shadow.camera.bottom = -shadowDistance;
    
  4. 设置mesh投射,接收阴影

    gltf.scene.traverse((child) => {
        child.castShadow = true;
        child.receiveShadow = true;
    })
    

    阴影调试辅助工具

    const helper = new THREE.CameraHelper( light.shadow.camera );
    scene.add( helper );
    

效果: 下载.png

六.结尾

完整代码展示:

import * as THREE from 'three';
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader';
​
let mixer;
let playerMixer;
​
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.01, 50);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.shadowMap.enabled = true;
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
​
camera.position.set(5, 10, 25);
​
// const controls = new OrbitControls(camera, renderer.domElement);scene.background = new THREE.Color(0.2, 0.2, 0.2);
​
const ambientLight = new THREE.AmbientLight(0xffffff, 0.1);
scene.add(ambientLight);
​
const directionLight = new THREE.DirectionalLight(0xffffff, 0.2);
scene.add(directionLight);
​
// directionLight.position.set (10, 10, 10);
directionLight.lookAt(new THREE.Vector3(0, 0, 0));directionLight.castShadow = true;directionLight.shadow.mapSize.width = 2048;
directionLight.shadow.mapSize.height = 2048;
​
const shadowDistance = 20;
directionLight.shadow.camera.near = 0.1;
directionLight.shadow.camera.far = 40;
directionLight.shadow.camera.left = -shadowDistance;
directionLight.shadow.camera.right = shadowDistance;
directionLight.shadow.camera.top = shadowDistance;
directionLight.shadow.camera.bottom = -shadowDistance;
directionLight.shadow.bias = -0.001;
​
​
// const boxGeometry = new THREE.BoxGeometry(1,1,1);
// const boxMaterial = new THREE.MeshBasicMaterial({color: 0x00ff00});
// const boxMesh = new THREE.Mesh(boxGeometry, boxMaterial);
// scene.add(boxMesh);
​
// const axesHelper = new THREE.AxesHelper(10);
// scene.add(axesHelper)
​
let playerMesh;
let actionWalk, actionIdle;
const lookTarget = new THREE.Vector3(0, 2, 0);
new GLTFLoader().load('../resources/models/player.glb', (gltf) => {
    playerMesh = gltf.scene;
    scene.add(gltf.scene);
​
    playerMesh.traverse((child)=>{
        child.receiveShadow = true;
        child.castShadow = true;
    })
​
    playerMesh.position.set(0, 0, 11.5);
    playerMesh.rotateY(Math.PI);
​
    playerMesh.add(camera);
    camera.position.set(0, 2, -5);
    camera.lookAt(lookTarget);
​
    const pointLight = new THREE.PointLight(0xffffff, 1.5);
    playerMesh.add(pointLight);
    pointLight.position.set(0, 1.8, -1);
​
    playerMixer = new THREE.AnimationMixer(gltf.scene);
​
    const clipWalk = THREE.AnimationUtils.subclip(gltf.animations[0], 'walk', 0, 30);
    actionWalk = playerMixer.clipAction(clipWalk);
    // actionWalk.play();
​
    const clipIdle = THREE.AnimationUtils.subclip(gltf.animations[0], 'idle', 31, 281);
    actionIdle = playerMixer.clipAction(clipIdle);
    actionIdle.play();
​
​
    // const clips = gltf.animations; // 播放所有动画
    // clips.forEach(function (clip) {
    //     const action = mixer.clipAction(clip);
    //     action.loop = THREE.LoopOnce;
    //     // 停在最后一帧
    //     action.clampWhenFinished = true;
    //     action.play();
    // });
});
​
let isWalk = false;
const playerHalfHeight = new THREE.Vector3(0, 0.8, 0);
window.addEventListener('keydown', (e) => {
    if (e.key === 'w') {
        // playerMesh.translateZ(0.1);
​
        const curPos = playerMesh.position.clone();
        playerMesh.translateZ(1);
        const frontPos = playerMesh.position.clone();
        playerMesh.translateZ(-1);
​
        const frontVector3 = frontPos.sub(curPos).normalize()
​
        const raycasterFront = new THREE.Raycaster(playerMesh.position.clone().add(playerHalfHeight), frontVector3);
        const collisionResultsFrontObjs = raycasterFront.intersectObjects(scene.children);
​
        console.log(collisionResultsFrontObjs);
​
​
        if (collisionResultsFrontObjs && collisionResultsFrontObjs[0] && collisionResultsFrontObjs[0].distance > 1) {
            playerMesh.translateZ(0.1);
        }
​
​
​
        if (!isWalk) {
            crossPlay(actionIdle, actionWalk);
            isWalk = true;
        }
    }
    if (e.key === 's') {
        playerMesh.translateZ(-0.1);
    }
})
​
window.addEventListener('keyup', (e) => {
    if (e.key === 'w') {
        crossPlay(actionWalk, actionIdle);
        isWalk = false;
    }
});
​
let preClientX;
window.addEventListener('mousemove', (e) => {
​
    if (preClientX && playerMesh) {
        playerMesh.rotateY(-(e.clientX - preClientX) * 0.01);
    }
    preClientX = e.clientX;
});
​
new GLTFLoader().load('../resources/models/zhanguan.glb', (gltf) => {
​
    // console.log(gltf);
    scene.add(gltf.scene);
​
    gltf.scene.traverse((child) => {
        // console.log(child.name);
​
        child.castShadow = true;
        child.receiveShadow = true;
​
        if (child.name === '2023') {
            const video = document.createElement('video');
            video.src = "./resources/yanhua.mp4";
            video.muted = true;
            video.autoplay = "autoplay";
            video.loop = true;
            video.play();
​
            const videoTexture = new THREE.VideoTexture(video);
            const videoMaterial = new THREE.MeshBasicMaterial({ map: videoTexture });
​
            child.material = videoMaterial;
        }
        if (child.name === '大屏幕01' || child.name === '大屏幕02' || child.name === '操作台屏幕' || child.name === '环形屏幕2') {
            const video = document.createElement('video');
            video.src = "./resources/video01.mp4";
            video.muted = true;
            video.autoplay = "autoplay";
            video.loop = true;
            video.play();
​
            const videoTexture = new THREE.VideoTexture(video);
            const videoMaterial = new THREE.MeshBasicMaterial({ map: videoTexture });
​
            child.material = videoMaterial;
        }
        if (child.name === '环形屏幕') {
            const video = document.createElement('video');
            video.src = "./resources/video02.mp4";
            video.muted = true;
            video.autoplay = "autoplay";
            video.loop = true;
            video.play();
​
            const videoTexture = new THREE.VideoTexture(video);
            const videoMaterial = new THREE.MeshBasicMaterial({ map: videoTexture });
​
            child.material = videoMaterial;
        }
        if (child.name === '柱子屏幕') {
            const video = document.createElement('video');
            video.src = "./resources/yanhua.mp4";
            video.muted = true;
            video.autoplay = "autoplay";
            video.loop = true;
            video.play();
​
            const videoTexture = new THREE.VideoTexture(video);
            const videoMaterial = new THREE.MeshBasicMaterial({ map: videoTexture });
​
            child.material = videoMaterial;
        }
    })
​
    mixer = new THREE.AnimationMixer(gltf.scene);
    const clips = gltf.animations; // 播放所有动画
    clips.forEach(function (clip) {
        const action = mixer.clipAction(clip);
        action.loop = THREE.LoopOnce;
        // 停在最后一帧
        action.clampWhenFinished = true;
        action.play();
    });
​
})
​
new RGBELoader()
    .load('../resources/sky.hdr', function (texture) {
        // scene.background = texture;
        texture.mapping = THREE.EquirectangularReflectionMapping;
        scene.environment = texture;
        renderer.outputEncoding = THREE.sRGBEncoding;
        renderer.render(scene, camera);
});
​
function crossPlay(curAction, newAction) {
    curAction.fadeOut(0.3);
    newAction.reset();
    newAction.setEffectiveWeight(1);
    newAction.play();
    newAction.fadeIn(0.3);
}
​
function animate() {
    requestAnimationFrame(animate);
    renderer.render(scene, camera);
    // controls.update();
    // if (donuts){
    //     donuts.rotation.y += 0.01;
    // }

    if (mixer) {
        mixer.update(0.02);
    }
    if (playerMixer) {
        playerMixer.update(0.015);
    }
}
​
animate();

以上就是在大帅老师特训营中学到的0基础实现人物看展效果,加入猿创营 (v:dashuailaoyuan),一起交流学习吧~