这是three.js系列的第三篇文章。本文的内容包含基础场景的创建、模型加载、模型动画播放、碰撞检测等。
这里是预览地址
一、基础环境的搭建
这个部分主要内容是说明基础环境的搭建,初始化项目后执行npm
命令 npm i three
安装用到的three.js库。随后新建一个html
和js
文件,引入它即可。随后在js
文件中引入three.js
。
js
import * as THREE from 'three'
引入主要内容后我们开始创建以下内容:
- 场景 (所有内容的容器)
- 灯光 (没光怎么看得见呢)
- 相机 (模拟人眼看到的内容,由于我们希望获取画面时保持屏幕的纵横比以获得更好的显示效果,因此这里获取屏幕的宽高进行计算)
- 渲染器 (将相机看到的内容渲染到页面上)
js
//场景
const scene = new THREE.Scene()
//获取屏幕宽高
let w = window.innerWidth
let h = window.innerHeight
//相机
let camera = new THREE.PerspectiveCamera(75, w / h, 0.1, 100) //各个参数的意思这里不再赘述,可查看文档获取更多
二、基础场景的创建
基础场景如上图所示,整个场景由地面、马路、树木、建筑物、云朵、天空构成,下面来一步步的构建场景。
小提示,创建的内容未添加到场景中时是不会显示出来的!!!
地面的构成
首先我们来定义一下承载地面的长宽
js
//地面的长宽
const groundW = 50
const groundH = 10
接下来观察一下上图的实现效果,我们发现地面上有马路、树木、草坪等元素,这些元素其实可以作为视作为和地面是一个整体。所以可以用一个父容器将它们全部包裹起来。在three.js中这个容器叫做Group
,创建后可以将内容通过.add
方法添加到其中。概念与dom
中嵌套差不多。
js
//创建一个地面的父容器
const groundGroup = new THREE.Group()
1、创建马路
与现实中相思,我们图中的马路也是由水泥路面、车道线构成的。这些单独的部分构成了一个马路的整体。所以,我们还是创建一个容器用来承载它们。
js
//创建马路容器
const roadGroup = new THREE.Group()
//使用平面几何体创建马路,参数为宽高,这里我们创建单位为2,高度为10的马路,也许你会疑问为什么是高度,后面会给予解释,或者你可以直接查看官方文档。搜索PlaneGeometry
const roadPlaneG = new THREE.PlaneGeometry(2, groundH)
//定义材质 和 颜色
const roadPlaneM = new THREE.MeshStandardMaterial({ color: 0x4c4a4b })
//创建网格 ,用于组织几何体和材质
const roadPlane = new THREE.Mesh(roadPlaneG, roadPlaneM)
此刻的你也许想看看效果,所以使用以下代码将它添加到了场景中。
js
scene.add(roadPlane)
却发现啥都没有,以至于你在怀疑我骗你。但是我发誓我没有,因为我们还没有添加一个渲染器。下面我们来添加一个渲染器
,并且为了方便如预览地址中可以变化观察视角,再引入轨道控制器
小插曲:添加渲染器、和轨道控制器 并渲染场景
在文件顶部加入以下代码引入轨道控制器
js
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
找一个合适的位置,添加以下代码
js
//创建渲染器
const renderer = new THREE.WebGLRenderer({
antialias: true,//开启抗锯齿
alpha: true
})
const orbitControls = new OrbitControls(camera, renderer.domElement)
设置渲染器
js
//设置渲染区域大小
renderer.setSize(w, h)
renderer.setClearColor(0x95e4e8)
//添加到页面中,不然还是啥都没有
document.body.append(renderer.domElement)
最后调用渲染器的渲染方法
js
renderer.render(scene, camera)
然后,你会发现还是黑不溜秋的。忘了打光啊笨蛋,记得给女朋友拍照的时候也要打光!!!。下面我们添加灯光。
在最开始创建场景相机的代码区域添加以下代码
js
//灯光
scene.add(new THREE.AmbientLight(0xffffff, 0.2)) //环境光
const dLight = new THREE.DirectionalLight(0xffffff) //平行光
dLight.position.set(0, 1, 1)
scene.add(dLight)
const light = new THREE.PointLight(0xffffff, 0.5);//点光源
light.position.set(0, 10, 10);
scene.add(light);
是的,就是这么任性,我加了好多光,女朋友一定会夸我的,发出了桀桀桀的笑声~~·
然后保存,不出意外的话出现了这么一个玩意儿
是的,就是这么一个玩意儿,现在你明白上面说的为啥是设置平面几何体的宽高了吗,桀桀桀~
这时候你躁动不安的右手开始控制着鼠标拖动屏幕了,不出意外的话出了意外,咋不能动,下面我们来解决
添加以下代码,这段代码的作用是产生动画,更新画面帧
js
animation()
function animation() {
requestAnimationFrame(animation)
renderer.render(scene, camera)
orbitControls.update()
}
完事儿后你应该可以动了~~~
这时候你可能会好奇为啥这个马路不能像示例那里一样呢,我可以告诉你可以旋转一下就好,不过不是现在,这样太繁琐了,还记得我们在上面创建的groundGroup
吗,我们最后只要旋转它就好,所以现在先不急。
马路的其他部分 (车道线)
如果你添加了以下代码,请去除。
js
scene.add(roadPlane)
这里开始创建马路上的车道线,
js
//这里是左侧长实线
const leftLine = new THREE.Mesh(
new THREE.PlaneGeometry(0.05, groundH),
new THREE.MeshStandardMaterial({ color: 0xffffff })
)
//设置实线位置
leftLine.position.z = 0.0001
leftLine.position.x = -0.8
//克隆出右侧的实线
const rightLine = leftLine.clone()
rightLine.position.x = 0.8 //同上
const dashLineGroup = new THREE.Group()
let dashNum = 24
for (let i = 0; i < dashNum; i++) {
const m = new THREE.MeshStandardMaterial({ color: 0xffffff })
const g = new THREE.PlaneGeometry(0.01, 0.3)
const mesh = new THREE.Mesh(g, m)
mesh.position.z = 0.0001
mesh.position.y = -groundH / 2 + 0.5 * i
dashLineGroup.add(mesh)
}
上述代码中,创建了左右两侧的实线和中间的虚线。由于虚线可以看作为一个整体,因此也是用了一个Group
来包裹,创建完成后,将马路的所有元素添加的roadGroup
中
js
roadGroup.add(roadPlane, leftLine, rightLine, dashLineGroup)
scene.add(roadGroup)
保存,出现如下画面
2、创建草地
首先删除以下代码
js
scene.add(roadGroup)
对,我们来创建一大片绿绿的东西。人间的青草地~,打住打住
由上图可知草地分为前景和后景,下面开始创建草地
js
//前景草地
const frontGrass = new THREE.Mesh(
new THREE.PlaneGeometry(groundW, groundH / 2),
new THREE.MeshStandardMaterial({ color: 0x61974b })
)
frontGrass.position.z = -0.001
frontGrass.position.y = -groundH / 4
//后景草地
const backGrass = new THREE.Mesh(
new THREE.PlaneGeometry(groundW, groundH / 2),
new THREE.MeshStandardMaterial({ color: 0xb1d744 })
)
backGrass.position.z = -0.001
backGrass.position.y = groundH / 4
3、地面部分完成
将马路、前后景草地添加到地面上
js
groundGroup.add(roadGroup, frontGrass, backGrass)
//添加到场景中
scene.add(groundGroup)
就如下图所示了
接下来把整个地面旋转一下。注意,three.js中的旋转都是以弧度制的
js
groundGroup.rotation.x = -0.5 * Math.PI
下面就是结果了
有模有样的一条马路了。
树的创建
js
const treesGroup = new THREE.Group() //整体树的集合
const leftTreeGroup = new THREE.Group() //左边树的集合
const singTreeGroup = new THREE.Group()//单个树的集合
//树的各个部分
const treeTop = new THREE.Mesh(
new THREE.ConeGeometry(0.2, 0.2, 10),
new THREE.MeshStandardMaterial({ color: 0x64a525 })
)
const treeMid = new THREE.Mesh(
new THREE.ConeGeometry(0.3, 0.3, 10),
new THREE.MeshStandardMaterial({ color: 0x64a525 })
)
//树干
const treeBottom = new THREE.Mesh(
new THREE.CylinderGeometry(0.05, 0.05, 0.5),
new THREE.MeshStandardMaterial({ color: 0x7a5753 })
)
//模拟树的阴影
const treeShadow = new THREE.Mesh(
new THREE.CircleGeometry(0.3, 10),
new THREE.MeshBasicMaterial({ color: 0x3f662d })
)
//设置各个部分的位置
treeTop.position.y = 0.55
treeMid.position.y = 0.4
//旋转阴影
treeShadow.rotation.x = -0.5 * Math.PI
treeBottom.position.y = 0.2
//组合单棵树
singTreeGroup.add(treeTop, treeMid, treeBottom, treeShadow)
//设置位置
singTreeGroup.scale.set(0.5, 0.5, 0.5)
上述代码中创建了一棵树,使用了three.js内置的集中几何体来创建树,并且使用了平面几何体来模拟阴影。接下来我们创建更多的树并添加到场景中
js
//生成树的数量
const treeNum = 50
for (let i = 0; i < treeNum; i++) {
const group = singTreeGroup.clone()
//z轴,默认为朝向屏幕的那一面
group.position.z = -groundH + i * 0.5
group.position.x = -1.2
leftTreeGroup.add(group)
}
//右边的树,直接克隆生成
const rightTreeGroup = leftTreeGroup.clone()
//设置右边的树的位置
rightTreeGroup.position.x = 1.2 * 2
//将树添加树的集合中
treesGroup.add(leftTreeGroup, rightTreeGroup)
此时如果将以上都添加到场景中
js
scene.add(groundGroup,treesGroup,)
会出现如下的画面
可以看到树已经被我们添加到画面中了。
让场景动起来
js
//获取时钟方法
const clock = new THREE.Clock()
function animation() {
const time = clock.getElapsedTime()
//不停的移动树和虚线的位置,产生一种在行进的感觉,下面计算后会重置位置
dashLineGroup.position.y = -time * 1.5 % 3
treesGroup.position.z = time * 1.5 % 3
requestAnimationFrame(animation)
renderer.render(scene, camera)
orbitControls.update()
}
这时候的画面就会如预览地址中一样动起来了。至于预览地址中的建筑物和云朵都是差不多的道理,多个几何体叠加产生的,与css差不多。
三、人物模型的加载
一些简单粗糙的物体模型我们可以自己拼接出来,但是一些复杂带有动画效果的最好就是通过建模软件导出了。这里为了方便,使用的模型是three.js官网示例中的模型。话不多说,首先引入加载器。不同的模型有对应的不同的加载器,这里我们的模型是glb
格式的,所以我们在文件开头添加以下代码
js
//引入加载器
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
然后在合适位置引入模型
js
//创建加载器实例
const loader = new GLTFLoader();
let model;
loader.load('models/RobotExpressive.glb', function (mesh) {
console.log(mesh)
model = mesh.scene;
//设置模型位置
model.scale.set(0.06, 0.06, 0.06)
model.position.set(0, 0, 3)
model.rotateY(Math.PI)
let group = new THREE.Group()
group.add(model)
scene.add(group)
}, undefined, function (e) {
console.error(e);
});
保存后可以看到如下画面
更改观察角度后是这样的
这个时候的模型就只是一个静态的,啥都不会,接下来我们尝试播放它的动画。
四、模型动画的播放
如何知道加载的模型有哪些动画呢,查看以下上述加载模型代码的控制台打印
点开 animations
可以看到这个模型的动画还挺丰富的,我们先尝试加载一下名叫Running
的动画吧。
如何播放动画
那么应该如何去播放它呢,查看文档后知道以下内容。
动画的播放由 AnimationMixer
控制,animations
数组中的叫做动画片段
接下来试试。
1、创建动画混合器(AnimationMixer)
js
let mixer = new THREE.AnimationMixer(mesh.scene);
2、获取动画片段
js
let clip = mesh.animations[0]
3、通过actions播放动画
js
let action = mixer.clipAction(clip)
action.play()
4、更新动画
保存后,突然发现,诶?咋还是不动,因为我们没有调用动画混合器的update
方法去更新动画
在 animate
方法中增加一行
js
mixer.update(0.02)
保存后发现机器人在跳舞了~~
将跳舞的动画替换成Running
,就能看到机器人在马路上奔跑了。
js
let clip = (mesh.animations.filter(ele => ele.name == 'Running'))[0]
五、碰撞检测
由于场景中目前没有可碰撞的物体,因此我先添加一个吧 找一个合适的地方添加以下代码
js
const ball = new THREE.SphereGeometry(0.1, 32, 16);
const ballMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
const ballMesh = new THREE.Mesh(ball, ballMaterial);
ballMesh.position.y = 0.25
scene.add(ballMesh)
这段代码添加了一个球形几何体在场景中,像这样
下面让球也动起来,修改 animation
方法,增加以下代码
js
ballMesh.position.z = time * 1.5 % 3
保存后不出意外的话可以看到小球向我们运动过来了
六、总结
声明:场景内容的创意和部分代码参考了b站up主 进华,在此基础上扩充了模型加载、动画播放、碰撞检测等内容。