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 和 功能不完善 的地方

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

欢迎关注

相关推荐
子非鱼92118 分钟前
【前端】ES6:Set与Map
前端·javascript·es6
想退休的搬砖人31 分钟前
vue选项式写法项目案例(购物车)
前端·javascript·vue.js
啥子花道1 小时前
Vue3.4 中 v-model 双向数据绑定新玩法详解
前端·javascript·vue.js
麒麟而非淇淋1 小时前
AJAX 入门 day3
前端·javascript·ajax
茶茶只知道学习1 小时前
通过鼠标移动来调整两个盒子的宽度(响应式)
前端·javascript·css
清汤饺子1 小时前
实践指南之网页转PDF
前端·javascript·react.js
蒟蒻的贤1 小时前
Web APIs 第二天
开发语言·前端·javascript
清灵xmf1 小时前
揭开 Vue 3 中大量使用 ref 的隐藏危机
前端·javascript·vue.js·ref
蘑菇头爱平底锅1 小时前
十万条数据渲染到页面上如何优化
前端·javascript·面试
2301_801074151 小时前
TypeScript异常处理
前端·javascript·typescript