分享一个前段时间写的一个基于 Threejs 的个人作品
灵感主要来自 Bruno Simon 的个人网站 bruno-simon.com/
当然 我实现的就相对比较粗糙了
这个项目大部分的时间花在【Blender场景的设计】和【模型的制作】
代码的实现基本就是水到渠成的事情
涉及的部分技术点
- Matcap材质球的制作
- 烘焙纹理材质的制作
- Threejs相机的基本运动
- GLSL自定义纹理
- Cannon-es在Threejs中的应用
- 音频库howler.js的操作
材质贴图
这里主要是用了2种材质贴图
1. Mathcap
Matcap可以在3D软件中将 光源 和 材质 信息烘焙到一张 材质球 贴图上
这样可以在 Threejs 中没有 光源 的情况下,模拟出物体的光照表现
可以很大程度上提高性能
matcao贴图需要在 Blender 中制作并导出
然后可以在Threejs中直接使用 MeshMatcapMaterial 实现
使用 Matcap贴图时,在导出模型时需要给每个模型指定对应的颜色
- 可以直接用 颜色 给模型命名,Threejs中再通过 正则表达式 的方式匹配颜色
- 或 Blender 中添加 自定义属性,Threejs中可以在 userData 找到对应信息
2. 阴影贴图
为了增加场景的 真实感 和 立体感
单独将 地面的阴影 作为贴图导出使用
Blender中 烘焙 是制作贴图的常用手法,可以B站搜索相关教程了解
模型处理
整个场景的模型相对比较复杂,不能全部一起导出
根据 功能 和 开发便利,大概这样分类导出
- 静态物体可以统一导出到一个文件
- 重复的物体 只选择一个 导出,代码里在单独做复制处理
- 积木模型单独导出,后续要做随机处理
- 汽车模型单独导出,需要单独绑定物理数据
- 列车轨迹单独导出
- 部分物理碰撞刚体单独导出
模型的加载相关代码
js
build (resources: any) {
console.log('resources', resources)
const modelPlayground = resources['model-playground'].scene
const modelCarScene = resources['model-car'].scene
const dataRailPoints = resources['data-rail-points']
this.railCar.addPathLine(dataRailPoints)
const models = [...modelPlayground.children, ...modelCarScene.children]
models.forEach((e: any) => {
const data = e.userData
// set matcap color
if (data.matcap) {
e.material = matcapMaterial(resources[`matcap-${data.matcap}`])
}
// set shadow
if (data['shadow-color']) {
e.material = groundShadowMaterial(resources['texture-shadow'], data['shadow-color'])
}
if (data.name === 'sun') {
console.log('data', e)
}
// models to dunplicate
if (this.repeats.contains(data.name)) {
this.repeats.add(data.name, e)
}
// modles to animate
if (data.name === 'rail-car') {
this.railCar.add(e)
}
if (data.name === 'windmill') {
this.windmill.add(e)
}
if (data.name === 'carousel-rotation') {
this.carousel.add(e)
}
if (data.name.includes('ship')) {
this.ship.add(e)
}
if (data.name.includes('drop-rotation')) {
this.dropRotation.add(e)
}
if (data.name.includes('drop-up-seat')) {
this.dropUp.add(e)
}
if (data.name.includes('ferris')) {
this.ferris.add(e)
}
if (data.physics === 'static') {
this.physics.createBody({ mesh: e, mass: 0, shapeType: SHAPE_TYPES.BOX, collideSound: CollideSoundName.Wall })
}
if (data.name === 'area') {
this.shields.add(e)
}
if (data.name.includes('tree-')) {
this.trees.add(e)
}
if (data.name === 'coffee-smoke') {
this.coffeeSmoke.add(e)
}
})
this.scene.add(modelPlayground)
this.repeats.build()
this.scene.add(this.repeats.main)
this.carousel.build()
this.scene.add(this.carousel.main)
this.ship.build()
this.scene.add(this.ship.main)
this.dropRotation.build()
this.scene.add(this.dropRotation.main)
this.dropUp.build()
this.scene.add(this.dropUp.main)
this.ferris.build()
this.scene.add(this.ferris.main)
this.scene.add(this.shields.main)
// init rail car
this.railCar.build()
this.scene.add(this.railCar.main)
// init car
this.car.build(modelCarScene)
this.scene.add(this.car.main)
this.bricks.add(resources)
this.isReady = true
this.camera.ready(() => {
gsap.to('.actions', { top: 0})
})
}
物理绑定
物理绑定 这里使用了 cannon-es
汽车的运动 使用了 RaycastVehicle
掌握 cannon-es 还需要多看官方文档多上手
在 cannon-es 中,physics world的是单独存在的,每个物体在cannon中都需要有对应的Body最作作为碰撞物体。在每次 update 中,物体通过拷贝对应 Body 的位置和旋转来实现模拟物理。
在本项目中,由于碰撞的物体形状比较简单,就通过
js
new Box3().setFromObject(mesh)
来计算物体的大概形状,从而创建出合适的 Body
js
...
createBody (config: BodyConfig) {
const body = new Body({
mass: config.mass,
material: this.materials.default,
type: config.type || BODY_TYPES.DYNAMIC
})
body.allowSleep = true
body.sleep()
const box = new Box3().setFromObject(config.mesh)
const size = new Vec3 (
(box.max.x - box.min.x) / 2,
(box.max.y - box.min.y) / 2,
(box.max.z - box.min.z) / 2
)
const center = new Vec3(
(box.max.x + box.min.x) / 2,
(box.max.y + box.min.y) / 2,
(box.max.z + box.min.z) / 2
)
body.position.copy(center)
switch (config.shapeType) {
case SHAPE_TYPES.CYLINDER: {
const radius = size.x
const height = size.z
const quaternion = new Quaternion()
quaternion.setFromAxisAngle(new Vec3(1, 0, 0), Math.PI / 2)
const shape = new Cylinder(radius, radius, height * 2, 12)
body.addShape(shape, new Vec3(), quaternion)
break
}
case SHAPE_TYPES.BOX: {
const shape = new Box(size)
body.addShape(shape, new Vec3())
break
}
}
body.addEventListener('collide', () => {
if (config.collideSound) {
const sound = this.collideSounds[config.collideSound]
sound.play()
}
})
this.world.addBody(body)
return body
}
...
简单的练习项目
难免有 bug 和 功能不完善 的地方
以后有时间还会多更新完善
欢迎关注