当像素学会跳踢踏舞:用 JavaScript 玩转计算机图形学中的物理引擎集成

253 阅读8分钟

在计算机图形学的奇幻世界里,我们以往绘制的图形就像是舞台上一群精心摆好姿势的 “模特”,无论灯光如何变幻,它们都保持着静态的优雅。但当物理引擎介入后,这些图形突然获得了生命,变成了会跑会跳、能相互碰撞的 “演员”,开始在屏幕上演绎出精彩绝伦的动态故事。今天,我们就来聊聊如何用 JavaScript 将 Bullet、Havok 或 PhysX 这些神奇的物理引擎,与计算机图形学完美集成,让像素开启它们的 “物理狂欢”。

一、物理引擎:图形世界的魔法棒

物理引擎就像是计算机图形世界的 “物理老师”,它熟知现实世界中所有物体运动的 “规矩”。从苹果落地的自由落体,到台球相撞的反弹轨迹,它都能精准模拟。在计算机图形学里,我们创建的 3D 模型、2D 图像,只要交给物理引擎管理,就能遵循真实的物理规律运动。

Bullet、Havok 和 PhysX 这三位 “物理老师” 各有神通。Bullet 是开源界的 “热心肠”,它就像一位乐于分享知识的邻家学霸,代码完全公开,任何人都能深入研究它的 “教学方法”,并且根据自己的需求进行修改和扩展;Havok 则是游戏开发领域的 “名门贵族”,凭借强大的性能和丰富的功能,长期服务于各种大型 3A 游戏,在复杂场景的物理模拟上表现卓越;PhysX 是 NVIDIA 旗下的 “特长生”,它充分利用 GPU 的并行计算能力,在大规模刚体和流体模拟方面堪称一绝,能让你的图形世界充满炫酷的动态效果。

二、JavaScript 与物理引擎的初次邂逅

在 JavaScript 的世界里,要引入物理引擎,我们首先得搭建好 “舞台”。这里我们可以借助一些优秀的 JavaScript 库来简化操作,比如ammo.js,它是 Bullet 物理引擎的 JavaScript 移植版本。

就像我们去超市购物前需要准备购物车一样,在使用物理引擎前,我们要先在 HTML 文件中引入相关的库文件。在标签里添加如下代码:

<script src="ammo.js"></script>

然后,在 JavaScript 代码中,我们开始创建物理世界的 “地基”—— 初始化物理引擎。这就好比搭建一个空的游乐场,等待我们放入各种游乐设施。

// 创建物理世界
const world = new Ammo.btDiscreteDynamicsWorld();
// 设置重力,让物体能“脚踏实地”
const gravity = new Ammo.btVector3(0, -10, 0);
world.setGravity(gravity);

在这段代码中,我们使用Ammo.btDiscreteDynamicsWorld()创建了一个离散动力学世界,它就像一个虚拟的物理实验室,所有的物理模拟都将在这里发生。接着,通过btVector3设置了重力方向和大小,让物体能像在现实世界中一样,受到重力的影响。

三、让图形 “活” 起来:添加物理对象

现在,我们的物理世界已经搭建好了,接下来要做的就是往这个世界里放入 “演员”—— 具有物理属性的图形对象。

假设我们要创建一个从高处落下的立方体。在现实世界中,我们制作一个立方体可能需要木材、工具等,而在代码世界里,我们需要借助 JavaScript 和物理引擎的 “魔法”。

// 创建立方体的形状
const cubeShape = new Ammo.btBoxShape(new Ammo.btVector3(1, 1, 1));
// 创建物理对象的运动状态,记录它的位置和旋转信息
const cubeMotionState = new Ammo.btDefaultMotionState(new Ammo.btTransform(
    new Ammo.btQuaternion(0, 0, 0, 1),
    new Ammo.btVector3(0, 10, 0)
));
// 设置立方体的质量
const mass = 1;
const inertia = new Ammo.btVector3(0, 0, 0);
cubeShape.calculateLocalInertia(mass, inertia);
// 创建刚体,将形状、运动状态、质量和惯性组合在一起
const cubeRigidBody = new Ammo.btRigidBody(
    new Ammo.btRigidBodyConstructionInfo(
        mass,
        cubeMotionState,
        cubeShape,
        inertia
    )
);
// 将刚体添加到物理世界中
world.addRigidBody(cubeRigidBody);

在这段代码中,我们首先使用btBoxShape创建了一个边长为 1 的立方体形状,它定义了物体的外观和碰撞体积。然后,通过btDefaultMotionState记录立方体的初始位置(在高度为 10 的地方)和旋转信息。接着,设置立方体的质量,并计算它的惯性。最后,使用btRigidBody将这些信息组合成一个刚体,就像给立方体赋予了 “身体” 和 “灵魂”,并将它添加到我们之前创建的物理世界中。这样,这个立方体就会在重力的作用下,从高处落下,开启它在虚拟世界中的 “冒险之旅”。

四、与图形渲染引擎的 “梦幻联动”

到目前为止,我们已经在物理世界中创建了会运动的物理对象,但这些对象还只是存在于 “幕后”,我们在屏幕上还看不到它们。这时候,就需要与图形渲染引擎进行联动,让物理对象 “走上舞台”,展现在观众面前。

以 Three.js 这个流行的 3D 图形渲染库为例。我们先创建一个 Three.js 的场景、相机和渲染器:

// 创建Three.js场景
const scene = new THREE.Scene();
// 创建相机
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;
// 创建渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

然后,我们创建一个与物理世界中立方体对应的 Three.js 网格对象,并将其添加到场景中:

// 创建Three.js的立方体几何形状
const geometry = new THREE.BoxGeometry(2, 2, 2);
// 创建材质
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
// 创建网格对象
const cubeMesh = new THREE.Mesh(geometry, material);
scene.add(cubeMesh);

最后,我们需要在每一帧渲染时,将物理世界中立方体的位置和旋转信息同步到 Three.js 的网格对象上,实现物理模拟与图形渲染的实时联动:

function animate() {
    requestAnimationFrame(animate);
    // 进行物理模拟的一步更新
    world.stepSimulation(1 / 60, 10, 1.0 / 60);
    // 获取物理世界中立方体的刚体
    const cubeRigidBody = world.getRigidBodyArray().getObjectAt(0);
    // 获取刚体的世界变换矩阵
    const transform = new Ammo.btTransform();
    cubeRigidBody.getWorldTransform(transform);
    // 将变换矩阵的位置和旋转信息同步到Three.js的网格对象上
    const position = transform.getOrigin();
    cubeMesh.position.x = position.x();
    cubeMesh.position.y = position.y();
    cubeMesh.position.z = position.z();
    const rotation = transform.getRotation();
    cubeMesh.rotation.x = rotation.x();
    cubeMesh.rotation.y = rotation.y();
    cubeMesh.rotation.z = rotation.z();
    renderer.render(scene, camera);
}
animate();

在animate函数中,我们首先调用world.stepSimulation对物理世界进行一步模拟更新,就像时间向前推进了一小步。然后获取物理世界中立方体的刚体,并从刚体中提取出它的位置和旋转信息,将这些信息同步到 Three.js 的网格对象上,最后通过渲染器将场景渲染到屏幕上。这样,我们就能在屏幕上看到立方体在物理引擎的作用下,从高处落下,并与其他物体发生碰撞等一系列真实的物理行为。

五、进阶挑战:处理复杂场景与交互

当我们掌握了基础的物理引擎集成后,就可以尝试挑战更复杂的场景,比如创建多个物体相互碰撞、模拟流体效果,或者实现用户与物理世界的交互。

例如,我们可以通过监听鼠标事件,在鼠标点击的位置创建新的物理对象。这就像在虚拟世界中,用户拥有了 “造物主” 的能力,能随时创造新的物体:

document.addEventListener('mousedown', function (event) {
    const rect = renderer.domElement.getBoundingClientRect();
    const x = (event.clientX - rect.left) / (rect.right - rect.left) * 2 - 1;
    const y = -(event.clientY - rect.top) / (rect.bottom - rect.top) * 2 + 1;
    const raycaster = new THREE.Raycaster();
    raycaster.setFromCamera(new THREE.Vector2(x, y), camera);
    const intersects = raycaster.intersectObjects(scene.children);
    if (intersects.length > 0) {
        const intersection = intersects[0];
        const newCubeShape = new Ammo.btBoxShape(new Ammo.btVector3(0.5, 0.5, 0.5));
        const newCubeMotionState = new Ammo.btDefaultMotionState(new Ammo.btTransform(
            new Ammo.btQuaternion(0, 0, 0, 1),
            new Ammo.btVector3(intersection.point.x, intersection.point.y, intersection.point.z)
        ));
        const newMass = 0.5;
        const newInertia = new Ammo.btVector3(0, 0, 0);
        newCubeShape.calculateLocalInertia(newMass, newInertia);
        const newCubeRigidBody = new Ammo.btRigidBody(
            new Ammo.btRigidBodyConstructionInfo(
                newMass,
                newCubeMotionState,
                newCubeShape,
                newInertia
            )
        );
        world.addRigidBody(newCubeRigidBody);
        const newGeometry = new THREE.BoxGeometry(1, 1, 1);
        const newMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 });
        const newCubeMesh = new THREE.Mesh(newGeometry, newMaterial);
        scene.add(newCubeMesh);
    }
});

在这段代码中,我们首先通过鼠标点击的位置计算出射线的方向,然后使用Raycaster检测射线与场景中物体的交点。如果检测到交点,就在交点位置创建一个新的物理对象和对应的 Three.js 网格对象,并将它们分别添加到物理世界和图形场景中。这样,用户就能通过鼠标点击,在虚拟世界中创造出各种新的物体,与物理世界进行互动,让整个图形世界变得更加生动有趣。

六、总结与展望

通过以上步骤,我们成功地将物理引擎集成到了计算机图形学中,用 JavaScript 为图形赋予了物理属性,让它们在屏幕上 “活” 了起来。从创建物理世界、添加物理对象,到与图形渲染引擎联动,再到实现复杂场景和用户交互,每一步都像是在搭建一座虚拟的 “物理王国”。

随着技术的不断发展,物理引擎的功能也在日益强大,未来我们可以期待更真实的物理模拟效果,比如更精确的布料模拟、更细腻的流体动力学模拟等。同时,随着 Web 技术的进步,JavaScript 在计算机图形学和物理引擎集成领域也将有更广阔的应用空间。也许在不久的将来,我们能通过浏览器,轻松创建出媲美专业游戏的交互式 3D 物理场景,让每个人都能成为虚拟世界的 “造物主”。现在,就拿起你的 “代码魔杖”,在计算机图形学的世界里,创造属于你的物理奇迹吧!

以上文章涵盖了从基础到进阶的物理引擎集成知识。你若对某些部分想深入了解,或想尝试其他物理引擎,都能随时和我说。