用Physijs在场景中添加物理效果

Posted Heavi的博客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了用Physijs在场景中添加物理效果相关的知识,希望对你有一定的参考价值。

1.创建可用Physijs的基本Three.js场景

    创建一个可用Physijs的Three.js场景非常简单,只要几个步骤即可。首先我们要包含正确的文件, 需要引入physi.js文件。实际模拟物理场景时非常耗费CPU的,如果我么能在render线程中做的话,场景的帧频会受到严重的影响。为了弥补这一点,Physijs选择在后台线程中执行计算。这里的后台是有Web workers(网页线程)规范定义的额,现在大多数浏览器都实现了该功能。

    对Physijs来说也就意味着我们需要配置一个带有执行任务的JavaScipt文件,并告诉Physijs在哪里可以找到用来模拟场景的ammo.js文件。所以需要添加以下代码:

Physijs.scripts.worker = "../libs/physijs_worker.js";
        Physijs.scripts.ammo = "../libs/ammo.js";

    Physijs在Three.js的普通场景外又提供了一个包装器,所以我们代码可以想这样创建场景:

scene = new Physijs.Scene();
            scene.setGravity(new THREE.Vector3(0, -50, 0));

    在模拟物理效果之前,我们需要在场景中添加一些对象。为此,我们可以使用Three.js的普通方法来定义对象,但必须用一个特定的Physijs对象将这些对象包裹起来:

var stoneGeom = new THREE.BoxGeometry(0.6, 6, 2);
                        var stone = new Physijs.BoxMesh(stoneGeom, Physijs.createMaterial(new THREE.MeshPhongMaterial({
                            color: scale(Math.random()).hex(),
                            transparent: true,
                            opacity: 0.8
                        })));
                       ...
                        scene.add(stone);

    我们第一个Physijs场景中的各个部分都有了。剩下要做的就是告诉Physijs模拟物理效果,并更新场景中各对象的位置和角色。为此,我们可以调用创建的场景的simulate方法。修改基础render循环代码:

render = function(){
            requestAnimationFrame(render);
            renderer.render(scene, camera);
            render_stats.update();

            scene.simulate(undefined, 1);
        }

    假设我们要实现下面图片中放倒多米若骨牌的效果。

image

    下面是实现功能的一段核心代码,points是所有多米诺骨牌的点集合。遍历每个骨牌的顶点,创建一个类型为BoxMesh对象(多米诺骨牌)。这里需要注意的是通过stone.lookAt()函数设置了对象的旋转角度,在手动更新了Physijs包装的对象的角度(或位置)之后,我们必须告诉Physijs有什么东西改变了。对于角度,我么可以将__dirtRotation设置为true;对于位置,我们可以将__dirtyPosition设置为true。

this.resetScene = function(){
                    scene.setGravity(new THREE.Vector3(controls.gravityX, controls.gravityY, controls.gravityZ));
                    stones.forEach(function(st){
                        scene.remove(st);
                    });
                    stones = [];

                    points.forEach(function(point){
                        var stoneGeom = new THREE.BoxGeometry(0.6, 6, 2);
                        var stone = new Physijs.BoxMesh(stoneGeom, Physijs.createMaterial(new THREE.MeshPhongMaterial({
                            color: scale(Math.random()).hex(),
                            transparent: true,
                            opacity: 0.8
                        })))
                        //console.log(stone.position);
                        stone.position.copy(point);
                        stone.lookAt(scene.position);
                        stone.__dirtyRotation = true;
                        stone.position.y = 3.5;

                        scene.add(stone);
                        stones.push(stone);
                    });

                    stones[0].rotation.x = 0.2;
                    stones[0].__dirtyRotation = true;
                }

2.材质属性

    Physijs中材质对象最重要的两个属性分别是restitution和firction。restitution设置材质弹性,值越大,弹性越强;值越小弹性越弱。而restitution设置摩擦系数,值越小,摩擦就越小,物体越容易移动;值越大,摩擦越大,物体越难移动。

    假如我们要实现下图的效果。地板一直都在左右旋转,球体也会跟着地板一起移动。这里我们主要看下球体的实现代码如何。

image

    下面的代码是圆球的实现代码。首先生成了一个随机颜色colorSphere,每次我们批量创建五个球体。创建球体对象使用Physijs.SphereMesh类创建。这里主要看下如何创建材质。创建材质和我们普通的方法不同,必须使用Physijs.createMaterial函数创建。第三个参数friction用来设置摩擦系数,范围0到1。第四个参数restitution设置弹性,范围0到1。只要我们修改这两个参数,我们就能看到球体落到地板时以及移动时的效果区别。

this.addSpheres = function () {
                    var colorSphere = scale(Math.random()).hex();
                    for(var i = 0; i < 5; i++){
                        box = new Physijs.SphereMesh(
                                new THREE.SphereGeometry(2, 20),
                                Physijs.createMaterial(
                                        new THREE.MeshPhongMaterial({
                                            color: colorSphere,
                                            opacity: 0.8,
                                            transparent: true
                                        }),
                                        controls.sphereFriction,
                                        controls.sphereRestitution
                                )
                        );
                        box.position.set(
                                Math.random() * 50 - 25,
                                20 + Math.random() * 5,
                                Math.random() * 50 - 25
                        );
                        meshes.push(box);
                        scene.add(box);
                    }
                };

3.基础图形

    Physijs提供了一些可以用来包装几何体的图形类。使用这些几何体唯一要做的就是讲THREE.Mesh的构造函数替换成这些网格对象的构造函数。下表是Physijs中所有网格对象的概览:

    Physijs.PlaneMesh/这个网格可以用来创建一个厚度为0的平面。这样的平面也可以用BoxMesh对象包装一个高度很低的THREE.CubeGeometry来表示

    Physijs.BoxMesh/如果是类似方块的几何体,你可以使用这个网格。例如,它的属性跟THREE.CubeGeometry的属性很相配

    Physijs.SphereMesh/对于球形可以使用这个网格。它跟THREE.SphereGeometry的属性很相配

    Physijs.CylinderMesh/通过设置THREE.Cylinder的属性你可以创建出各种柱状图形。Physijs为各种柱性提供了不同网格。Physijs.CylinderMesh可以用于一般的、上下一致的圆柱形

    Physijs.ConeMesh/如果顶部的半径为0,底部的半径值大于0,那么你可以用THREE.Cylinder创建一个圆锥体。如果你想在这样一个对象上应用物理效果,那么可以使用的、最相匹配的网格类就是ConeMesh

    Physijs.CapsuleMesh(胶囊网格)/跟THREE.Cylinder属性很相似,但其底部和底部是圆的

    Physijs.ConvexMesh(凸包网格)/Physijs.ConvexMesh是一种比较粗略的图形,可用于多数复杂退行。它可以创建一个模拟复杂图形的凸包

    Physijs.ConcaveMesh/ConvexMesh是一个比较粗略的图形,而ConcaveMesh则可以对负责图形进行比较细致的表现。需要注意的是使用ConcaveMesh对效率的影响比较大

    Physijs.HeightfieldMesh(高度场网格)/这是一种非常特别的网格。通过该网格你可以从一个THREE.PlaneGeometry对象创建出一个高度场。

4.使用约束限制对象移动

    我们已经了解到各种图形如何对重力、摩擦和弹性做出反应。并影响碰撞。Physijs还提供了一些高级对象,让i可以限制对象的移动。在Physijs里,这些对象呗称作约束。下表是Physijs中可用约束概览:

    PointConstraint/通过这个约束,你可以将一个对象与另一个对象之间的位置固定下来。例如一个对象动了,另一个对象也会随着移动,它们之间的距离和方向保持不变

    HingeConstraint/通过活页约束,你可以限制一个对象只能像活页一样移动,例如门

    SliderConstraint/将对象的移动限制在一个轴上。例如移门

    ConeTwistConstraint/通过这个约束,你可以用一个对象限制另一个对象的旋转和移动。这个约束的功能类似于一个球削式关节。例如,胳膊在肩关节中的活动

    DOFConstraint/通过自由度约束,你可以限制对象在任意轴上的活动,你可以设置对象活动的额最小、最大角度。这是最灵活的约束方式

5.用PointConstraint限制亮点间的移动

    实现代码如下,我们在这段代码里可以看到,我们使用特定的Physijs网格创建对象,然后将它们添加到场景中。我们使用Physijs.PointConstraint构造函数创建约束。

function createPointToPoint() {
            var obj1 = new THREE.SphereGeometry(2);
            var obj2 = new THREE.SphereGeometry(2);

            var objectOne = new Physijs.SphereMesh(obj1, Physijs.createMaterial(
                    new THREE.MeshPhongMaterial({color: 0xff4444, transparent: true, opacity: 0.7}), 0, 0));
            objectOne.position.z = -18;
            objectOne.position.x = -10;
            objectOne.position.y = 2;
            objectOne.castShadow = true;
            scene.add(objectOne);

            var objectTwo = new Physijs.SphereMesh(obj2, Physijs.createMaterial(
                    new THREE.MeshPhongMaterial({color: 0xff4444, transparent: true, opacity: 0.7}), 0, 0));
            objectTwo.position.z = -5;
            objectTwo.position.x = -20;
            objectTwo.position.y = 2;
            objectTwo.castShadow = true;
            scene.add(objectTwo);

            // if no position two, its fixed to a position. Else fixed to objectTwo and both will move
            var constraint = new Physijs.PointConstraint(objectOne, objectTwo, objectTwo.position);
            scene.addConstraint(constraint);
        }

    构造函数有三个参数。前两个参数指定要连接的两个对象。第三个参数指定约束绑定的位置。一般来说,如果你指向将两个对象连在一起,那么你最好将这个位置设置在第二个对象的位置上。如果你不想将一个对象绑定到另一个对象,而绑定到场景中某个固定的点,那么你可以忽略第二个参数。这样第一个对象就会跟着你指定的位置保持固定距离。

6.用HingeConstraint创建类似们的约束

    顾名思义,通过HingeConstraint你可以创建一个行为类似活页的对象。它可以绕固定的轴旋转,并可限制在一定角度内。假如我们现在要实现下图中框选部分的活页门效果,白色长条随着右边的小方块旋转。

image

    实现代码如下,HingeConstraint构造函数包含四个参数,定义为new Physijs.HingeConstraint(mesh_a, mesh_b, position, axis)。mesh_a第一个对象是将要被约束的对象;mesh_b指定mesh_a受哪个对象约束。这里flipperLeft受flipperLetPivot小方块影响;position约束应用的点。在本例中这个点就是Mesh_a绕着旋转的点;axis活页绕着旋转的轴。在本例中我们将活页设置在水平方向(0, 1, 0)。最后我们还需要设置约束对象的属性,为此我们调用setLimits函数。该函数包含四个参数,分别是low(指定旋转的最下弧度)、high(指定旋转的最大弧度)、bias_factor(该属性指定处于错误位置时,约束进行纠正的速度)、relaxation_factor(改属性指定约束以什么样的比例改变速度)。如果该属性的值越高,哪儿对象在达到最小或最大角度时会被弹回来。

function createLeftFlipper() {
            var flipperLeft = new Physijs.BoxMesh(
                    new THREE.BoxGeometry(12, 2, 2), Physijs.createMaterial(new THREE.MeshPhongMaterial(
                            {opacity: 0.6, transparent: true}
                    )), 0.3
            );
            flipperLeft.position.x = -6;
            flipperLeft.position.y = 2;
            flipperLeft.position.z = 0;
            flipperLeft.castShadow = true;
            scene.add(flipperLeft);
            var flipperLeftPivot = new Physijs.SphereMesh(
                    new THREE.BoxGeometry(1, 1, 1), ground_material, 0);

            flipperLeftPivot.position.y = 1;
            flipperLeftPivot.position.x = -15;
            flipperLeftPivot.position.z = 0;
            flipperLeftPivot.rotation.y = 1.4;
            flipperLeftPivot.castShadow = true;

            scene.add(flipperLeftPivot);

            // when looking at the axis, the axis of object two are used.
            // so as long as that one is the same as the scene, no problems
            // rotation and axis are relative to object2. If position == cube2.position it works as expected
            var constraint = new Physijs.HingeConstraint(flipperLeft, flipperLeftPivot, flipperLeftPivot.position, new THREE.Vector3(0, 1, 0));
            scene.addConstraint(constraint);

            constraint.setLimits(
                    -2.2, // minimum angle of motion, in radians, from the point object 1 starts (going back)
                    -0.6, // maximum angle of motion, in radians, from the point object 1 starts (going forward)
                    0.1, // applied as a factor to constraint error, how big the kantelpunt is moved when a constraint is hit
                    0 // controls bounce at limit (0.0 == no bounce)
            );

            return constraint;
        }

7.用SliderConstraint将移动限制到一个轴

    通过SliderConstraint约束,你可以将某个对象的移动限制到某个轴上。用代码 创建这些约束非常简单:

var constraint = new Physijs.SliderConstraint(sliderMesh, new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 1, 0));

            scene.addConstraint(constraint);
            constraint.setLimits(-10, 10, 0, 0);
            constraint.setRestitution(0.1, 0.1);

    该约束对象接收三个参数(或者四个,如果想将一个对象约束到另外一个对象)。构造函数定义为new Physijs.SliderConstraint(mesh_a, mesh_b, position, axis)。这些参数和HingeConstraint的参数相似。我么还需要通过constraint.setLimits函数限定滑块能滑多远:constriant.setLimits(-10, 10, 0, 0)。参数依次为linear_lower指定对象的线性下限;linear_upper该属性指定对象的线性上限;anguar_lower该属性指定对象的角度下限;angular_higher该属性指定对象的角度上限。

8.用ConeTwistConstraint创建类似球削的约束

    通过ConeTwistConstraint可以创建出一个移动受一系列角度限制的约束。我们可以指定一个对象绕着另一个对象转动时在x、y、z轴上的最小角度和最大角度。理解ConeTwistConstraint最好的方法就是看看创建约束的代码:

function createConeTwist() {
            var baseMesh = new THREE.SphereGeometry(1);
            var armMesh = new THREE.BoxGeometry(2, 12, 3);

            var objectOne = new Physijs.BoxMesh(baseMesh, Physijs.createMaterial(
                    new THREE.MeshPhongMaterial({color: 0x4444ff, transparent: true, opacity: 0.7}), 0, 0), 0);
            objectOne.position.z = 0;
            objectOne.position.x = 20;
            objectOne.position.y = 15.5;
            objectOne.castShadow = true;
            scene.add(objectOne);


            var objectTwo = new Physijs.SphereMesh(armMesh, Physijs.createMaterial(
                    new THREE.MeshPhongMaterial({color: 0x4444ff, transparent: true, opacity: 0.7}), 0, 0), 10);
            objectTwo.position.z = 0;
            objectTwo.position.x = 20;
            objectTwo.position.y = 7.5;
            scene.add(objectTwo);

            objectTwo.castShadow = true;

            //position is the position of the axis, relative to the ref, based on the current position
            var constraint = new Physijs.ConeTwistConstraint(objectOne, objectTwo, objectOne.position);

            scene.addConstraint(constraint);
            // set limit to quarter circle for each axis
            constraint.setLimit(0.5 * Math.PI, 0.5 * Math.PI, 0.5 * Math.PI);
            constraint.setMaxMotorImpulse(1);
            constraint.setMotorTarget(new THREE.Vector3(0, 0, 0)); // desired rotation

            return constraint;
        }

    我们先是创建出几个用约束连接起来的对象:ojectOne(球)和objectTwo(盒子)。ConeTwistConstraint的第一个参数是要约束的对象,第二个参数是第一个参数要约束到的对象,最后一个参数是约束应用的位置(在本例中,这个位置就是objectOne绕着旋转的位置)。将约束添加到场景中之后,我们就可以通过setLimts函数设置它的限制。setLimit函数接收三个弧度值,表示对象绕每个轴旋转的最大角度。

以上是关于用Physijs在场景中添加物理效果的主要内容,如果未能解决你的问题,请参考以下文章

Three.js 进阶之旅:物理效果-3D乒乓球小游戏 🏓

unity按钮添加恒力

意外的物理体现在SpriteKit场景中

Forge Viewer - 如何在场景中访问(或获取渲染/片段代理)克隆的网格?

ArcGIS JS API实现地图场景视频融合

SpriteKit可摧毁物理场景的进一步完善