我在前端修马路——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主 进华,在此基础上扩充了模型加载、动画播放、碰撞检测等内容。

相关推荐
bloxed8 分钟前
前端文件下载多方式集合
前端·filedownload
余生H14 分钟前
前端Python应用指南(三)Django vs Flask:哪种框架适合构建你的下一个Web应用?
前端·python·django
LUwantAC22 分钟前
CSS(四)display和float
前端·css
cwtlw26 分钟前
CSS学习记录20
前端·css·笔记·学习
界面开发小八哥31 分钟前
「Java EE开发指南」如何用MyEclipse构建一个Web项目?(一)
java·前端·ide·java-ee·myeclipse
谢道韫66639 分钟前
今日总结 2024-12-24
javascript·vue.js·elementui
一朵好运莲40 分钟前
React引入Echart水球图
开发语言·javascript·ecmascript
米奇妙妙wuu1 小时前
react使用sse流实现chat大模型问答,补充css样式
前端·css·react.js
傻小胖1 小时前
React 生命周期完整指南
前端·react.js
梦境之冢1 小时前
axios 常见的content-type、responseType有哪些?
前端·javascript·http