我在前端修马路——three.js

这是three.js系列的第三篇文章。本文的内容包含基础场景的创建、模型加载、模型动画播放、碰撞检测等。

这里是预览地址

一、基础环境的搭建

这个部分主要内容是说明基础环境的搭建,初始化项目后执行npm命令 npm i three 安装用到的three.js库。随后新建一个htmljs文件,引入它即可。随后在js文件中引入three.js

js 复制代码
import * as THREE from 'three'

引入主要内容后我们开始创建以下内容:

  1. 场景 (所有内容的容器)
  2. 灯光 (没光怎么看得见呢)
  3. 相机 (模拟人眼看到的内容,由于我们希望获取画面时保持屏幕的纵横比以获得更好的显示效果,因此这里获取屏幕的宽高进行计算)
  4. 渲染器 (将相机看到的内容渲染到页面上)
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主 进华,在此基础上扩充了模型加载、动画播放、碰撞检测等内容。

相关推荐
你挚爱的强哥5 分钟前
✅✅✅【Vue.js】sd.js基于jQuery Ajax最新原生完整版for凯哥API版本
javascript·vue.js·jquery
y先森40 分钟前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy40 分钟前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu108301891143 分钟前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿2 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡3 小时前
commitlint校验git提交信息
前端
虾球xz3 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇3 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒3 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员4 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js