Threejs 和 Blender 打造百万级展馆 | 大帅老猿threejs特训

408 阅读4分钟

场馆自由

image.png

Blender 建模

场馆围墙

先有模型,再用 Threejs 控制

  1. 先添加一个柱体, 设置边个数 160 个, 半径 20m, 高度 4m image.png

  2. 右键设置内插面

  3. 挤出面,把内插面挤扁。

image.png

  1. 删除面, 留做门 切换到 z轴视角,选中一个门的宽度的,然后快捷键 x,删除面

image.png

  1. 此时能看到这个墙是中空的

image.png

选中两条边,然后右键从边创建面

image.png

image.png

场馆中心台座

  1. 新建一个六边形, 然后选中六条边选中 环切并滑移

image.png

然后快捷键 s 并长按左键向四周缩放。

image.png

  1. 创建一个正方体,选中一上面的面,快捷键 s 并按住左键缩放成梯形。然后选中上面的面并向上拉长

image.png

选中四条边右键选择 环切并滑移, 移动到一个合适的位置。

image.png

手动选中最上面的4个面,并向外挤出各个面

image.png

切换到 z 视角,选中4个面,缩放输入值 0.5 快捷键 g,在z 方向拉,使4个面板向上抬起

image.png

场馆显示屏

曲面屏

shift + D 复制曲面屏,分离选中项为独立的曲面屏, 为了让曲面屏有一个厚度挤出面。

陆续创建 4 个长方体屏幕面板。场馆显示屏就准备好了

image.png

场馆迎宾墙

  1. 创建一个长方体作为底座。
  2. 添加文本, tab 键编辑 welcome. 转换成网格,挤出面完成立体的迎宾墙

image.png

贴地板砖

  1. 为了让地板和场馆圆墙贴上不同的材质,我们先把地板分离出为独立的部分。
  2. 然后选中 shading, 新建材质, 分别选中基础色, 糙度和法向

image.png

  1. 选中 UV Editing, 放大被贴图地方,让地板重复贴在场馆地板。 地板就贴好了

image.png

给场馆墙面加上显示屏

  1. 选中 welcome ,然后点击 UV Editding, 选中 UV 展开 块面投影

image.png

  1. 重复这样的操作把,曲面屏,左一屏, 左二屏, 右一屏, 右二屏 分别展开成块面投影

Threejs 赋予模型以灵性

将建好的模型导出为 gltf 格式。

搭建最基础的三大件

场景,相机, 渲染器

import * as THREE from 'three';

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.01, 100);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

导入模型,并让显示屏播放视频

new GLTFLoader().load('../resources/zhanguan1.glb', (gltf) => {
    scene.add(gltf.scene); // 把导入的模型添加到场景
    // 遍历模型中的每一个 mesh,根据名字进行区分并播放不同的是屏
    gltf.scene.traverse((child) => {
        if (child.name === 'welcom') {
            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 === '左一屏') {
            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 === '右二屏' || 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);
    if (mixer) {
        mixer.update(0.02);
    }
}

animate();

导入人物模型

let playMesh;
const lookTarget = new THREE.Vector3(10, 1.8, 0); // 模型的身高是 1.8
new GLTFLoader().load('../resources/models/player.glb', function (gltf) {
	playMesh = gltf.scene;
	scene.add(playMesh);
	playMesh.position.set(10, 0.3, 0); // 场馆的半径是 10 
	playMesh.rotateY(-Math.PI / 2);

	playMesh.add(camera);
	camera.position.set(0, 2.5, -5); // 在人物的后脑勺放一台相机
	camera.lookAt(lookTarget);
});

点击 w 让任务向前走

window.addEventListener('keydown', (e) => {
	if (e.key === 'w') {
		playMesh.translateZ(0.1);
	}
});

让人物随着鼠标的移动转动一定的角度

let prePos;
window.addEventListener('mousemove', (e) => {
    if (prePos) {
        playMesh.rotateY((prePos - e.clientX) * 0.05);
    }
    prePos = e.clientX;
});

让人物走起来

	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(); // 默认是空闲状态

优化人物的走动和停止, 点击 w 的时候,播放走的动画,停的时候播放空闲的动画

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);
    }
});

两个动作切换的时候,不那么僵硬,而是加一些过渡效果

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

将模型的更新添加到 animate 函数中


function animate() {
    requestAnimationFrame(animate);
    renderer.render(scene, camera);
    if (mixer) {
            mixer.update(0.02);
    }
    if (playerMixer) {
            playerMixer.update(0.015);
    }
}

好了,人物这样就可以行走起来了