theme: channing-cyan
highlight: a11y-dark
场馆自由
Blender 建模
场馆围墙
先有模型,再用 Threejs 控制
- 先添加一个柱体, 设置边个数 160 个, 半径 20m, 高度 4m
- 右键设置内插面
- 挤出面,把内插面挤扁。
- 删除面, 留做门
切换到 z轴视角,选中一个门的宽度的,然后快捷键 x,删除面
- 此时能看到这个墙是中空的
选中两条边,然后右键从边创建面
场馆中心台座
- 新建一个六边形, 然后选中六条边选中 环切并滑移
然后快捷键 s 并长按左键向四周缩放。
- 创建一个正方体,选中一上面的面,快捷键 s 并按住左键缩放成梯形。然后选中上面的面并向上拉长
选中四条边右键选择 环切并滑移, 移动到一个合适的位置。
手动选中最上面的4个面,并向外挤出各个面
切换到 z 视角,选中4个面,缩放输入值 0.5
快捷键 g,在z 方向拉,使4个面板向上抬起
场馆显示屏
曲面屏
shift + D 复制曲面屏,分离选中项为独立的曲面屏, 为了让曲面屏有一个厚度挤出面。
陆续创建 4 个长方体屏幕面板。场馆显示屏就准备好了
场馆迎宾墙
- 创建一个长方体作为底座。
- 添加文本, tab 键编辑 welcome. 转换成网格,挤出面完成立体的迎宾墙
贴地板砖
- 为了让地板和场馆圆墙贴上不同的材质,我们先把地板分离出为独立的部分。
- 然后选中 shading, 新建材质, 分别选中基础色, 糙度和法向
- 选中 UV Editing, 放大被贴图地方,让地板重复贴在场馆地板。
地板就贴好了
给场馆墙面加上显示屏
- 选中 welcome ,然后点击 UV Editding, 选中 UV 展开 块面投影
- 重复这样的操作把,曲面屏,左一屏, 左二屏, 右一屏, 右二屏 分别展开成块面投影
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);
}
}
好了,人物这样就可以行走起来了
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。