用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); }
假设我们要实现下面图片中放倒多米若骨牌的效果。
下面是实现功能的一段核心代码,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设置摩擦系数,值越小,摩擦就越小,物体越容易移动;值越大,摩擦越大,物体越难移动。
假如我们要实现下图的效果。地板一直都在左右旋转,球体也会跟着地板一起移动。这里我们主要看下球体的实现代码如何。
下面的代码是圆球的实现代码。首先生成了一个随机颜色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你可以创建一个行为类似活页的对象。它可以绕固定的轴旋转,并可限制在一定角度内。假如我们现在要实现下图中框选部分的活页门效果,白色长条随着右边的小方块旋转。
实现代码如下,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函数接收三个弧度值,表示对象绕每个轴旋转的最大角度。