Blender + Threejs 实现一个游乐园交互场景

分享一个前段时间写的一个基于 Threejs 的个人作品

灵感主要来自 Bruno Simon 的个人网站 bruno-simon.com/

当然 我实现的就相对比较粗糙了

Live: playground-luosijie.vercel.app

Code: github.com/luosijie/pl...

这个项目大部分的时间花在【Blender场景的设计】和【模型的制作】

代码的实现基本就是水到渠成的事情

涉及的部分技术点
  1. Matcap材质球的制作
  2. 烘焙纹理材质的制作
  3. Threejs相机的基本运动
  4. GLSL自定义纹理
  5. Cannon-es在Threejs中的应用
  6. 音频库howler.js的操作

材质贴图

这里主要是用了2种材质贴图

1. Mathcap

Matcap可以在3D软件中将 光源 和 材质 信息烘焙到一张 材质球 贴图上

这样可以在 Threejs 中没有 光源 的情况下,模拟出物体的光照表现

可以很大程度上提高性能

matcao贴图需要在 Blender 中制作并导出

然后可以在Threejs中直接使用 MeshMatcapMaterial 实现

使用 Matcap贴图时,在导出模型时需要给每个模型指定对应的颜色

  1. 可以直接用 颜色 给模型命名,Threejs中再通过 正则表达式 的方式匹配颜色
  2. 或 Blender 中添加 自定义属性,Threejs中可以在 userData 找到对应信息
2. 阴影贴图

为了增加场景的 真实感 和 立体感

单独将 地面的阴影 作为贴图导出使用

Blender中 烘焙 是制作贴图的常用手法,可以B站搜索相关教程了解

模型处理

整个场景的模型相对比较复杂,不能全部一起导出

根据 功能 和 开发便利,大概这样分类导出

  1. 静态物体可以统一导出到一个文件
  2. 重复的物体 只选择一个 导出,代码里在单独做复制处理
  3. 积木模型单独导出,后续要做随机处理
  4. 汽车模型单独导出,需要单独绑定物理数据
  5. 列车轨迹单独导出
  6. 部分物理碰撞刚体单独导出
模型的加载相关代码
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 和 功能不完善 的地方

以后有时间还会多更新完善

欢迎关注

相关推荐
阿伟来咯~15 分钟前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端20 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱23 分钟前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai32 分钟前
uniapp
前端·javascript·vue.js·uni-app
也无晴也无风雨33 分钟前
在JS中, 0 == [0] 吗
开发语言·javascript
王哲晓2 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
理想不理想v2 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云2 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
aPurpleBerry3 小时前
JS常用数组方法 reduce filter find forEach
javascript
ZL不懂前端3 小时前
Content Security Policy (CSP)
前端·javascript·面试