前言
精彩的世界杯决赛期间,参加了胖达老师基于 Three.js&Blender 的元宇宙搭建入门实训,趁着年前还有点记忆,来做个笔记。原本想在这篇笔记外面残缺记下整个流程,然而篇幅切实太长了,本文临时以 Blender 摸索为主。
根底环境搭建
Three.js 提供的 API 是能够让咱们基于原生 JavaScript 轻易玩的,然而为了让咱们能在 VSCode 环境下有更好的代码提醒和热更新,咱们能够把 Vite 和 Typescript 利用起来(而且 Three.js 的 API 命名都比拟长,对于我这种中式英语都说不好的人来说,纯手写压力太大)。
package.json 局部配置如下:
{
"name": "vite-dashuailaoyuan",
"version": "0.0.1",
"scripts": {
"start": "vite --host",
// ...
},
"devDependencies": {
"@types/three": "^0.134.0",
"autoprefixer": "^10.4.0",
"prettier": "^2.5.0",
"sass": "^1.43.5",
"typescript": "^4.3.2",
"vite": "^2.6.14"
},
"dependencies": {"three": "^0.134.0"}
}
我本地应用的 node 版本是 v14.18.1,咱们能够通过 npm/pnpm/yarn 任一形式装置依赖。
依赖装置胜利后,咱们能够在 /src
目录下新建一个 JS 文件,比方 study.js
,引入three.js
以便于咱们随后能够随便输入。
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';
console.log('====================================');
console.log(THREE);
console.log('====================================');
// ...
最初,在根目录的 index.html
文件中引入study.js
。
<script type="module" src="/src/study.js"></script>
这个时候,咱们通过 npm run start
来启动我的项目,在浏览器中关上控制台,能够看到 three.js
的 API 被打印了进去。
初遇展馆模型
元宇宙到底火没火,能不能火?我作为一个小小的 web 开发无奈预知,然而基于元宇宙延长的 web3D 交互营销却是在慢热起来,而这些交互必然少不了场景。那咱们就借助 Blender 这个收费开源跨平台的 App 来手撸一个展馆模型,后续就能够在 three.js
中加载应用。
因为 Blender 举荐使用方便的快捷键来操作,一手键盘一手鼠标一把梭,所以咱们的操作过程中就尽可能地相熟快捷键操作。
清场
关上 Blender,新建【惯例】我的项目会默认给咱们创立一个立方体 box,这个时候咱们要清场删掉所有,快捷键 A
全选视图元素,快捷键 X
唤起删除。
当然,咱们也能够点选右侧面板元素,通过快捷键 X
进行删除。
创立展馆
1、增加柱体
咱们先来创立出场馆主体,应用组件快捷键 Shift+A
唤起【增加】面板 -【网格】-【柱体】, 此时面板左下角会有针对咱们以后操作项的一个编辑面板,咱们能够编辑柱体的半径、深度和顶点,特地须要留神的是【柱体】的顶点数,顶点越多会生成越多的面,会使得曲面越圆滑,然而面数越多加载起来就越耗性能,因而咱们要针对需要来 正当设置顶点数 ,比方这里咱们能够设置为120
就够用了。
2、复制面,向内挤出
咱们点选【柱体】并应用快捷键 Tab
来进入【编辑模式】,此时通过快捷键 1
/2
/3
对应切换到点 / 边 / 面的编辑模式。
- 点选【柱体】
- 快捷键
Tab
进入编辑模式,3
进入面编辑模式 - 点选顶部的面,快捷键
I
进入【内切面】模式,按住鼠标左键挪动来管制内切面的大小(调整场馆墙体的厚度),鼠标点击其余区域或者Enter
实现退出模式 - 快捷键
E
进入【挤出】模式,鼠标点按坐标 Z 轴向下挪动,直至到底部立体地位,调整好地位后退出【挤出】模式
3、展馆的大门
为了更真切,咱们须要把关闭的柱体拆出来一个大门来。流程很简略:选中面并删除,缝合顶点。
首先呢,咱们须要理解操作面板右上角的线框 / 实体 / 材质渲染预览的视图着色形式,切换这三种视图形式能够让咱们编辑时更直观选中或预览渲染成果。
- 切换到线框视图模式
- 快捷键
Tab
进入到面编辑模式,通过滑动鼠标滚轮调整咱们的视角,框选要删除的面(能够通过按着Shift
加选面) - 快捷键
X
进入删除模式,删除面
然而此时咱们会发现,删除面当前,两侧的连接处是镂空的,咱们须要把面缝合起来。
- 编辑模式下,快捷键
1
进入到点编辑模式,选中边缘的 4 个顶点 - 右键 - 从顶点创立边 / 面
好啦,咱们的场馆大体根本竣工啦,纯毛坯房啊有木有?这里只是根底的入门笔记,对于 Blender 而言,把握罕用快捷键就够啦。剩下的,就靠咱们的重复 click,就能够一点点搭建出更欠缺的细节,当然这个过程还须要更多的工夫和急躁,以及趣味。
如果你违心给多一点工夫,你能创立出一个本人称心的场景,起码不会比我的差哦。
中文的反对
虽说 Blender 菜单工具栏的国际化中文反对做的很不错,然而咱们要在场景中增加中文文本,还是要略微费一丢丢功夫。
增加文本编辑
比葫芦画瓢,咱们参照创立柱体的办法(组合键Shift
+A
)来增加一个【文本】,默认文本内容是“Text”, 然而咱们怎么编辑文本内容呢?
大家还记得快捷键 Tab
能够疾速进入编辑模式么?同样地,咱们应用这个快捷键,此时进入的就是文本的编辑模式啦。咱们输出 123
还是 abc
都能够,然而就是输出不了中文。不晓得是因为中文字体包太大,还是因为有这个需要的用户比拟少,反正 Blender 目前(v3.4.0)版本预置的文本字体是不反对中文的。
想应用中文怎么办?那咱们就须要本人引入中文字体。
引入中文字体
选中【文本】节点时,在工作区右侧会有一个【物体数据属性】的菜单,点击进入后有【字体】抉择。点击对应字体右侧的目录图标,会主动进入零碎字体目录,抉择本人中意的字体即可。
输出中文文本
选好称心的字体,高高兴兴地输出了 ” 新年快乐 ”,发现仍然输出不进去,这可怎么办呢?
莫慌,咱们有一个经典的土办法:复制粘贴。把想要展现的文本输出到其余编辑器甚至搜寻框任意能够复制的中央,复制粘贴进去。
文本立体化
然而我的文本节点就是一个面,这还怎么玩?那接下来,就要把文本立体化解决。
再次快捷键 Tab
退出编辑模式,快捷键 G
挪动文本节点到适合的为止,快捷键 R
旋转节点到适合的角度。
Tips:
1、挪动节点时,如果咱们放心节点地位乱了,能够锁定轴向进行挪动。譬如咱们想让节点沿着 X 轴挪动,顺次按下快捷键
G
和X
,再拖动就能够。2、旋转节点的时候,会发现原点不在几何核心,右键 - 设置原点 -【原点 -> 几何核心】。
旋转的时候,也能够通过左侧的【旋转】菜单,通过轴向坐标拖拽旋转,旋转时如果有固定角度,能够配合左下角以后编辑面板输出角度值进行旋转。
- 形式一:通过右侧面板【修改器属性】-【增加修改器】-【实体化】增加属性面板,设置【厚度】参数即可。
- 形式二:通过右侧面部【物体数据属性】-【几何数据】,设置【挤出】参数即可。
OK,到这里,对于 Blender 建模的惯例操作曾经根本都蕴含啦,大家能够持续舞起来啦~
代码笔记
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';
import dat from 'dat.gui';
import {Vector3} from 'three';
const gui = new dat.GUI();
const parameters = {
cameraY: 2,
cameraZ: -6
}
let mixer;
let playerMixer;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.01, 1000);
const renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);
camera.position.set(5, 10, 100);
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);
directionLight.castShadow = true;
scene.add(directionLight);
directionLight.shadow.mapSize.width = 2048;
directionLight.shadow.mapSize.height = 2048;
const shadowDistance = 20;
directionLight.shadow.camera.near = 0.5;
directionLight.shadow.camera.far = 50;
directionLight.shadow.camera.left = -shadowDistance;
directionLight.shadow.camera.right = shadowDistance;
directionLight.shadow.camera.top = shadowDistance;
directionLight.shadow.camera.bottom = -shadowDistance;
directionLight.shadow.bias = -0.0001;
directionLight.position.set (10, 10, 10);
directionLight.lookAt(new THREE.Vector3(0, 0, 0));
let playerMesh;
let pointLight;
let actionIdle, actionWalk;
new GLTFLoader().load('../resources/models/player.glb', (gltf) => {console.log(gltf);
gltf.scene.traverse((child) => {
child.castShadow = true;
child.receiveShadow = true;
})
playerMesh = gltf.scene;
scene.add(playerMesh);
playerMesh.position.set(12, -1, 0);
playerMesh.rotateY(Math.PI);
playerMesh.add(camera);
camera.position.set(0, parameters.cameraY, parameters.cameraZ);
camera.lookAt(playerMesh.position);
pointLight = new THREE.PointLight(0xffffff, 0.6);
pointLight.position.set(0, 2, -1);
scene.add(pointLight);
playerMesh.add(pointLight);
playerMixer = new THREE.AnimationMixer(gltf.scene);
const clipIdle = new THREE.AnimationUtils.subclip(gltf.animations[0], 'idle', 31, 281);
actionIdle = playerMixer.clipAction(clipIdle);
const clipWalk= new THREE.AnimationUtils.subclip(gltf.animations[0], 'walk', 0, 30);
actionWalk = playerMixer.clipAction(clipWalk);
});
let isChangeToWalk = true;
const playerHalfHeight = new THREE.Vector3(0, 1, 0);
window.addEventListener('keydown', (e) => {if (e.key === 'ArrowUp') {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].distance > 1) {playerMesh.translateZ(1);
}
if(collisionResultsFrontObjs && collisionResultsFrontObjs.length === 0) {playerMesh.translateZ(1);
}
if(isChangeToWalk) {crossPlay(actionIdle, actionWalk);
isChangeToWalk = false;
}
}
})
window.addEventListener('keyup', (e) => {console.log('keyup', e);
if (e.key === 'ArrowUp') {crossPlay(actionWalk, actionIdle);
isChangeToWalk = true;
}
});
let prePos;
window.addEventListener('mousemove', (e) => {if(prePos) {playerMesh.rotateY(-(e.clientX - prePos) * 0.01);
}
prePos = e.clientX;
});
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}, false)
new GLTFLoader().load('../resources/models/zhanguan.glb', (gltf) => {scene.add(gltf.scene);
gltf.scene.traverse((child) => {
child.castShadow = true;
child.receiveShadow = true;
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;
}
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();});
})
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 (mixer) {mixer.update(0.02);
}
if (playerMixer) {playerMixer.update(0.015);
}
}
animate();
写到最初
不论是 three.js 还是 Blender,内容都太多太多了,开展讲三天三夜都讲不完,何况我只是一个寻求入门的 web 开发。也基于此,在这篇笔记里,我也就临时疏忽了大家可能会比我还相熟的 three.js 局部,只留下我这并不欠缺的代码,着重记录了我第一次接触 Blender 时遇到的卡壳的中央。
Emm,写得不好请见谅,我要去持续摸索了,争取当前能分享给大家更多入门摸索笔记。当然,如果你也感兴趣,那就退出猿创营 (v:dashuailaoyuan),一起交流学习。