Skip to content

案例展示

摩擦力模拟

<template>
    <div id="TO"></div>
</template>

<script setup>
import { onMounted } from 'vue'

// 模型数据
const modelList = [
    {
        modelName: '立方体-1',
        modelType: 'cube',
        scale: { x: 1, y: 1, z: 1 },
        position: { x: -3, y: 0.5, z: 0 },
        rotation: { x: 0, y: 0, z: 0 },
        base: {
            depth: 1,
            width: 1,
            height: 1
        },
        material: {
            color: 'yellow'
        },
        userData: {
            physics: {
                rigidType: 'dynamic', // 刚体类型: dynamic动态, static静态
                shapeType: 'cube', // 形状类型: cube立方体
                helperLines: {
                    active: true,
                    cubeBase: {
                        width: 1,
                        height: 1,
                        depth: 1
                    }
                },
                material: {
                    staticFriction: 0,
                    dynamicFriction: 0,
                    restitution: 1
                },
                mass: 1,
                linearVelocity: {
                    x: 5,
                    y: 0,
                    z: 0
                }
            }
        }
    },
    {
        modelName: '立方体-2',
        modelType: 'cube',
        scale: { x: 1, y: 1, z: 1 },
        position: { x: 3, y: 0.5, z: 0 },
        rotation: { x: 0, y: 0, z: 0 },
        base: {
            depth: 1,
            width: 1,
            height: 1
        },
        material: {
            color: 'yellow'
        },
        userData: {
            physics: {
                rigidType: 'dynamic', // 刚体类型: dynamic动态, static静态
                shapeType: 'cube', // 形状类型: cube立方体
                helperLines: {
                    active: true,
                    cubeBase: {
                        width: 1,
                        height: 1,
                        depth: 1
                    }
                },
                material: {
                    staticFriction: 1, //静摩擦力
                    dynamicFriction: 1, //动摩擦力
                    restitution: 1 //弹性系数
                },
                mass: 1,
                linearVelocity: {
                    x: 5,
                    y: 0,
                    z: 0
                }
            }
        }
    },
    {
        modelName: '平面',
        modelType: 'cube',
        scale: { x: 1, y: 1, z: 1 },
        position: { x: 0, y: 0, z: 0 },
        rotation: { x: 0, y: 0, z: 0 },
        base: {
            depth: 20,
            width: 20,
            height: 0.1
        },
        material: {
            color: 0x525252
        },
        userData: {
            physics: {
                rigidType: 'static', // 刚体类型: dynamic动态, static静态
                shapeType: 'cube', // 形状类型: cube立方体
                helperLines: {
                    active: true,
                    cubeBase: {
                        width: 20,
                        height: 0.1,
                        depth: 20
                    }
                },
                material: {
                    staticFriction: 0.5,
                    dynamicFriction: 0,
                    restitution: 0
                },
                mass: 1,
                linearVelocity: {
                    x: 0,
                    y: 0,
                    z: 0
                }
            }
        }
    }
]
// 场景参数
const sceneData = {
    camera: {
        position: {
            x: 0,
            y: 10,
            z: 30
        }
    },
    helper: {
        axes: {
            active: false
        },
        grid: {
            active: false
        }
    },
    modelList: modelList
}

let pxScene
// 存储物理对象与Three.js模型的映射关系
const physicsObjects = [] // 格式: { name: 名称, pxActor: 物理对象, threeMesh: 渲染模型, pxShape?: 形状对象 }
let physics
let PhysXInstance
let tmpFilterData
let shapeFlags
// 控制物理模拟的状态
let animationFrameId = null
let TO = null
onMounted(() => {
    //初始化场景
    TO = new ThingOrigin('easyBuild', document.getElementById('TO'), sceneData, (TO) => {
        initPhysx()
    })
})
// 初始化物理场景
const initPhysx = async () => {
    const result = await TO.physx.initPhysx()
    if (result.success === false) {
        console.warn('[initPhysx] PhysX 未加载,后续物理逻辑已跳过', result.error)
        return
    }
    ;({ physics, PhysXInstance } = result)

    pxScene = TO.physx.createScene({
        x: 0,
        y: -9.81,
        z: 0
    })

    tmpFilterData = new PhysXInstance.PxFilterData(1, 1, 0, 0)

    shapeFlags = TO.physx.createShapeFlags({
        sceneQueryShape: true,
        simulationShape: true
    })

    initPhysxBody()
}
// 初始化物理对象
const initPhysxBody = () => {
    modelList.forEach((item) => {
        loadPhysxModel(item)
    })
    simulationLoop()
}

//加载物理刚体模型
const loadPhysxModel = (item) => {
    const modelInfo = item
    const tmpVec = TO.physx.createVec3(modelInfo.position)
    var tmpPose = new PhysXInstance.PxTransform(PhysXInstance.PxIDENTITYEnum.PxIdentity) // 变换
    tmpPose.set_p(tmpVec)

    const physicsData = modelInfo.userData.physics

    const boxMaterial = TO.physx.createMaterial(physicsData.material)
    shapeFlags = TO.physx.createShapeFlags({
        sceneQueryShape: true,
        simulationShape: true
    })
    let shape = null
    if (physicsData.shapeType === 'cube') {
        shape = TO.physx.createBoxShape(physicsData.helperLines.cubeBase, shapeFlags, boxMaterial)
    } else if (physicsData.shapeType === 'sphere') {
        shape = TO.physx.createSphereShape(physicsData.helperLines.sphereBase, shapeFlags, boxMaterial)
    } else if (physicsData.shapeType === 'capsule') {
        shape = TO.physx.createCapsuleShape(physicsData.helperLines.capsuleBase, shapeFlags, boxMaterial)
    }

    const actor =
        physicsData.rigidType === 'dynamic'
            ? physics.createRigidDynamic(tmpPose)
            : physics.createRigidStatic(tmpPose)

    shape.setSimulationFilterData(tmpFilterData)
    actor.attachShape(shape)

    if (physicsData.rigidType === 'dynamic') {
        actor.setMass(physicsData.mass)
        let linearVelocity = TO.physx.createVec3(physicsData.linearVelocity)
        actor.setLinearVelocity(linearVelocity)
    }

    pxScene.addActor(actor)

    // 对应的Three.js模型
    const boxMesh = TO.scene.getObjectByName(modelInfo.modelName)

    // 添加物理对象到物理对象列表
    physicsObjects.push({
        name: modelInfo.modelName,
        pxActor: actor,
        threeMesh: boxMesh,
        pxShape: shape,
        modelInfo: modelInfo // 保存完整的modelInfo以便重新创建物理对象
    })
}

// 物理模拟与渲染循环
const simulationLoop = () => {
    let lastFrame = 0
    function loop(hrTime) {
        var timeStep = Math.min(0.03, (hrTime - lastFrame) / 1000)
        lastFrame = hrTime // 重要:更新 lastFrame

        // 1. 执行物理模拟
        pxScene.simulate(timeStep)
        pxScene.fetchResults(true)

        // 2. 同步物理对象到Three.js模型
        physicsObjects.forEach(({ pxActor, threeMesh, name }) => {
            const pose = pxActor.getGlobalPose()
            const position = pose.get_p()
            const rotation = pose.get_q() // 四元数

            // 同步位置
            threeMesh.position.set(position.get_x(), position.get_y(), position.get_z())

            // 同步旋转(PhysX四元数格式为(x,y,z,w),Three.js为(x,y,z,w),直接对应)
            threeMesh.quaternion.set(rotation.get_x(), rotation.get_y(), rotation.get_z(), rotation.get_w())
        })
        // 3. 渲染场景
        animationFrameId = requestAnimationFrame(loop)
    }
    animationFrameId = requestAnimationFrame(loop)
}
</script>

<style scoped>
#TO {
    width: 100%;
    height: 400px;
    position: relative;
}
</style>