threejs——可跨越障碍的小车可操作演示

技术栈

  • vite
  • threejs
  • ammojs
  • typescript

jvideo

前言

此项目以vitejs作为基础框架,以typescript为编程语言,结合threejs3D渲染库和ammojs物理引擎工具开发,实现一个可以使用键盘操作汽车行驶,并可以跨越障碍物。下面我们一起来看一下吧

源码

相关源码和模型的下载链接地址点击链接进行跳转

物理世界

ts 复制代码
import { initPhysics, updatePhysics, physicsWorld } from '../utils/physics';
import { createRigidBody, setPointerBttVec, } from '../utils/rigidBody';

physics.tsrigidBody.ts是文中封装的方法,initPhysics是初始化物理引擎,updatePhysics更新物理引擎,physicsWorld为物理世界,可模拟重力。

createRigidBody可以创建刚体,以下是这方法的接受参数

创建刚体

typescript 复制代码
/**
 * 
 * @param object 物体本身
 * @param isShape 是否为实体  false为静态
 * @param mass 是否受引力影响
 * @param pos 起始位置
 * @param quat 四元数 表示位置方向角度等
 * @param vel 线性速度
 * @param angVel 线性角度
 * @param pointFrom 自定义碰撞器形状
 * @returns {模型、刚体实体}
 */

除了基础参数,这里需要讲一下objectpointFrom,这两个是碰撞检测点位的来源,

typescript 复制代码
// 如果不是组对象,则直接提取
if (!shape.isGroup) {
    physicsShape = createConvexHullPhysicsShape(shape.geometry.attributes.position.array);
}
typescript 复制代码
/**
 * 
 * @param coords 物体所有顶点信息
 * @returns 
 */
// 按照外围点创建碰撞器
export function createConvexHullPhysicsShape(coords: number[]) {
    let Ammo = (window as any).Ammo
    // 设置一个收集器
    const shape = new Ammo.btConvexHullShape();
    // 定点偏移,也可通过动态传参的方式自定义,目前为不偏移
    const tempBtVec3_1 = new Ammo.btVector3(0, 0, 0);

    for (let i = 0, il = coords.length; i < il; i += 3) {

        tempBtVec3_1.setValue(coords[i], coords[i + 1], coords[i + 2]);
        const lastOne = (i >= (il - 3));
        shape.addPoint(tempBtVec3_1, lastOne);

    }

    return shape;

}

用自定义模型取碰撞点有一个好处就是可以优化页面,比如有一个点位很多的模型,而碰撞检测又不需要那么精准,就可以用包围盒来做检测,

更新物理引擎

updatePhysics方法需要在renderer.setAnimationLoop(render);的render回调函数中调用,除了创建刚体,还需要在刚体的usedata信息中设置object.userData.PhysUpdate = true,因为在updatePhysics中判定当前刚体是否支持更新,如果不支持更新的话,可以通过callback自行开发。

typescript 复制代码
   ...
    for (let i = 0, il = rigidBodies.length; i < il; i++) {
        const objThree = rigidBodies[i];
        const objPhys = objThree.userData.physicsBody;
        const PhysUpdate = objThree.userData.PhysUpdate;
        const ms = objPhys.getMotionState();
    ...
        if (PhysUpdate) {
                objThree.position.set(px, py, pz);
                // 如果方向锁定传的是true 则不进行方向修改
                if (rl !== true) {
                    objThree.quaternion.set(rx, ry, rz, rw);
                }

            } else {
                cb && cb(new Vector3(px, py, pz), new Vector3(rx, ry, rz), objThree);
            }
    ...

setPointerBttVec设置刚体的物体指针。将创建出的刚体和模型绑定起来

typescript 复制代码
// 设置物体指针
export const setPointerBttVec = (object: Object3D, body: any) => {
    let Ammo = (window as any).Ammo
    const btVecUserData = new Ammo.btVector3(0, 0, 0);
    btVecUserData.threeObject = object;
    body.setUserPointer(btVecUserData);
}

源码中这些都是封装好的方法,开箱即用

背景板

typescript 复制代码
// 设置背景板尺寸
const PlaneSize = 400
// 实际是使用阴影材质作为底板
// var shadowMaterial = new THREE.ShadowMaterial();
var shadowMaterial = new THREE.MeshBasicMaterial({});

const plane = new THREE.Mesh(new THREE.BoxGeometry(PlaneSize, 0.5, PlaneSize, 1, 1, 1), shadowMaterial);
// 接受阴影
plane.receiveShadow = true;

实际中的背景板使用的是 阴影材质(ShadowMaterial),为了在文章中体现的明显一些,使用的是基础网格材质(MeshBasicMaterial),主要是为了将页面的元素都放在这个平台上,当然,汽车移动到平面的外围,还是会掉下去的。

加入物理世界

接下来将底板加入物理世界

typescript 复制代码
   const { object, body } = createRigidBody(plane, true, false, new THREE.Vector3(0, -0, 0), null)
    // 将刚体添加到物理引擎
    physicsWorld.addRigidBody(body);
    // 将刚体body和object绑定 object就是plane
    setPointerBttVec(object, body)
    // 将对象添加到刚体合集,会在updatePhysics方法更新
    rigidBodies.push(object)
    scene.add(object)

通过封装的createRigidBody方法,创建一个底板的刚体,第三个参数为false,表示支持物理碰撞,不支持重力影响,所以在底板上面的刚体可以保持在底板上。

在render中更新物理引擎

typescript 复制代码
const render = () => {
    const dt = playerClock.getDelta();
    // 更新物理世界
    updatePhysics(dt, rigidBodies, (pos: THREE.Vector3, dir: THREE.Vector3, objectThree: THREE.Object3D) => { })
  ...
}

添加汽车

createVehicle 是封装小汽车的方法,接受和返回参数如下

typescript 复制代码
/**
 * @param pos 起始位置
 * @param quat 四元数
 * @param physicsWorld 物理世界
 * @returns 汽车模型,更新方法和汽车刚体
 */
typescript 复制代码
 // 基于ammojs封装的创建基础汽车的方法
    const { group, chassisMesh } = createVehicle(new THREE.Vector3(0, 4, 0), new THREE.Quaternion(0, 0, 0, 1), physicsWorld)
    carMesh = chassisMesh
    // 光源追踪
    light.target = chassisMesh;
    carGroup.add(group)

    lodaCarModel()

设置light.target为汽车开动时候始终有光打到汽车上

汽车换皮

这个小汽车基础控制方向的元素有了,接下来就是换皮,你可以换成跑车,也可以换成F1赛车,下面就是换皮的方法

轮子的映射关系

typescript 复制代码
const wheelNameMap: any = {
    'front_wheel_left_RGB_texture_0': 'FRONT_LEFT',
    'front_wheel_right_RGB_texture_0': 'FRONT_RIGHT',
    'rear_wheel_left_RGB_texture_0': 'BACK_LEFT',
    'rear_wheel_right_RGB_texture_0': 'BACK_RIGHT'
}
typescript 复制代码
// 加载汽车模型
const lodaCarModel = async () => {
    const car = await loadGltf('../../src/assets/models/vehicle/scene.gltf') as any

    // 换皮轮子
    car.scene.traverse((mesh: any) => {
        if (mesh.isMesh) {
            castShadow(mesh)
            const wheelName = wheelNameMap[mesh.name];
            if (wheelName) {
                const wheel = scene.getObjectByName(wheelName);
                if (wheel) {
                    const scale = computedScale(wheel, mesh);
                    mesh.scale.copy(scale.clone())
                    wheel?.add(mesh)
                }
            } else {
                vehicleBodyGroup.add(mesh)

            }

        }
    })
    // 换皮车身
    const vehicleBody = scene.getObjectByName('vehicleBody');
    if (vehicleBody) {
        vehicleBodyGroup.rotation.y = Math.PI;
        vehicleBodyGroup.scale.set(0.013, 0.013, 0.0111)
        vehicleBodyGroup.position.setY(-0.5)
        vehicleBody.add(vehicleBodyGroup)
    }

}

障碍物

字母

创建自己需要的字母,并作为刚体,加入场景中。createLetter是封装的创建字母的方法,下面是方法的参数

typescript 复制代码
/**
 * 
 * @param l 内容
 * @param size 大小
 * @param color 颜色
 * @param height 高度
 * @param change 
 * @returns THREE.Mesh
 */

返回的是一个单独的模型,如果传入一段字母,则创建隶属于同一个模型的字母,所以要创建多个独立字母,需要一些其他处理createLetters方法就是用来创建多个独立字母的。接收参数如下

typescript 复制代码
interface createLitterInterface {
    litters: string[]; // 字母数组
    size: number, // 尺寸
    position: THREE.Vector3, // 初始位置
    color: THREE.Color,// 颜色
    height: number,// 高度
    change?: any  // 修改方向
    weight?: number // 模拟重力
}

创建 helloWorld

typescript 复制代码
 const params = {
    litters: 'helloWorld'.toUpperCase().split(''),
    size: 4,
    position: new THREE.Vector3(-16, 2, 14),
    color: new THREE.Color('0xffffff'),
    height: 2,
    change: {
        rotateY: Math.PI * 0.5
    },
}

// 字母
await createLetters(params)

fontLoader

加载字体文件使用const loader = new FontLoader(); ,结合# 文本缓冲几何体(TextGeometry)创建一个文字实例

ts 复制代码
 loader.load(import.meta.env.VITE_TYPEFACE_URL, function (font) {
    const geometry = new TextGeometry(l, {
        font: font,
        size: size || 3,
        height: height,
    });

    change?.rotateX && geometry.rotateX(change?.rotateX);
    change?.rotateY && geometry.rotateY(change?.rotateY);

    const material = new THREE.MeshStandardMaterial({
        color
    });

    const objectToCurve = new THREE.Mesh(geometry, material);
    resove(objectToCurve)
})

创建石堆

typescript 复制代码
// 创建石堆object
const size = new THREE.Vector3(1.5, 0.8, 1)
const position = new THREE.Vector3(-5, 0.5, 10)
createStone(6, 6, size, position, 'x')


// 创建石头
const createStone = (low: number, hig: number, size: THREE.Vector3, pos: THREE.Vector3, d?: 'x' | 'z') => {
    const dir = d || 'x'
    const { x, y, z } = size
    const material = new THREE.MeshStandardMaterial({ color: new THREE.Color(0xffffff) });
    // low底层砖数量,hig是层数,从最上层开始摆 i=0,一直摆到对下层i=hig
    for (let j = 0; j < hig; j++) {
        const v3 = pos.clone()
        v3.y = y * (j + 0.1) + 0.1
        for (let i = 0; i < low - j; i++) {
            v3[dir] = i * (size[dir] + 0.1)
            if (j > 0) v3[dir] = v3[dir] + size[dir] / 2 * j
            v3[dir] = v3[dir] + pos[dir]
            const stone = createMesh(size, undefined, material)
            stone.position.copy(v3)

            const { object, body } = createRigidBody(stone, true, true, null, null, undefined, undefined)
            castShadow(object)

            physicsWorld.addRigidBody(body);
            object.userData.PhysUpdate = true
            setPointerBttVec(object, body)
            body.setGravity(new AmmoLib.btVector3(0, - 17.8, 0));
            body.setFriction(200)
            rigidBodies.push(object);

            scene.add(object)


        }
    }
}

加载其他元素

石头和树使用const gltfLoader = new GLTFLoader();加载一个gltf模型,再通过遍历模型内的对象,继续将各对象添加到物理世界中,属性和底板相同,支持检测,但位置锁定。

源码

相关源码和模型的下载链接地址点击链接进行跳转

相关推荐
zqx_719 分钟前
随记 前端框架React的初步认识
前端·react.js·前端框架
惜.己36 分钟前
javaScript基础(8个案例+代码+效果图)
开发语言·前端·javascript·vscode·css3·html5
什么鬼昵称1 小时前
Pikachu-csrf-CSRF(get)
前端·csrf
长天一色1 小时前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_2342 小时前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河2 小时前
CSS总结
前端·css
BigYe程普2 小时前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
余生H2 小时前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
程序员-珍2 小时前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
axihaihai2 小时前
网站开发的发展(后端路由/前后端分离/前端路由)
前端