Nape柔体教程(5)
还记得我们学习的柔体创建的步骤吗?
一、用多个segment刚体组成柔体形状的轮廓,并用PivotJoint链接起来。
- 柔体的轮廓完全由segment刚体的形状组成,而非PivotJoint关节。
- 两个相邻segmentBody刚体之间用两个PivotJoint关节连接。
多边形柔体的创建过程基本上是一样的,不同之处在于步骤1.a,segment刚体的计算方法。
圆形是一个标准的正圆,它的轮廓通过三角函数和内外径可以轻松计算出来,然后分割成多个顶点,组成一个个小的segment刚体。但是多边形大多是不规则的形状,没有一个万能的公式来计算出这些形状的轮廓,所以我们要在这个问题上绕些弯子。
二、在刚体segment刚体的垂直方向施加作用力,把轮廓支撑起来。
- 获取刚体segment刚体外侧的边。
- 获取作用力方向。
这一点,我们在第4节柔体教程里也学习过了,没有太大的差异。不过第4节的作用力我是通过一个参数定义好之后(比如10),直接调用applyImpulse施加的,这一节我要教你另外一个方法。
首先我们快速浏览一下多边形柔体的创建方法。
- 获取多边形轮廓内外边界顶点,并计算出内外边界
- 针对内外边界进行等分
- 用等分边界生成的顶点创建segment刚体
下面我们来仔细看一下具体每个步骤的实现过程。
获取内外边界顶点
对应多边形顶点的处理,Nape为我准备了一个专门的类GeomPoly(我在用GeomPoly创建多边形刚体中曾经讲过),我们可以将存储了顶点的Array<Vec2>, flash.Vector<Vec2>, Vec2List等类型传入到GeomPoly的构造函数中,实例化一个GeomPoly对象,然后调用相应的函数。
多边形的外边界顶点是很容易获取的,Polygon提供的静态方法box()、rect()和regular()方法都可以返回一个包含了多边形顶点的Vec2List对象,我们可以传递给GeometryPoly进行实例化。
outPoly = new GeomPoly( Polygon.box());
那么怎么获取轮廓的内边界呢?同样还是求助于GeomPloy类了。GeomPoly有一个inflate()函数,用于对存储的多边形进行缩放。还记得Box2D缩放教程吗?inflate方法相对把多边形缩放方法单独放到了一个函数中。
inflate()的参数表示多边形缩放的尺寸,以像素为单位。整数表示多边形放大,负数表示缩小。然后将缩放后的多边形顶点存储到一个GeomPoly对象中返回。下面的代码将outPoly缩小10个像素并返回到innerPoly中去。
innerPoly = outPoly.inflate(-10);
等分内外边界
等分边界之前,要知道等分后的segmentBody的长度,这一点并不难,我可以预先定义好保存到一个变量segmentLength中。
然后用要等分的边界长度outEdge.length除以segmentLength,计算出等分后segment的数量segmentNum,再循环遍历计算每个片段的顶点。
用等分边界生成的顶点创建segment
计算出每个顶点之后就简单多了,和圆形柔体一样,把这些顶点按照指定的顺序组合成一个刚体segment,然后逐个用PivotJoint关节连接起来,形成多边形柔体。
讲了这么多,估计你已经看烦了,还是先看看完成后的效果吧。在下面的示例中,点击右上角的GasON按钮,可以添加或消除柔体的支撑力。
[swfobject]1032[/swfobject]
代码如下:
package learnNape { import com.bit101.components.PushButton; import flash.events.Event; import nape.shape.Edge; import nape.geom.GeomPoly; import nape.phys.Compound; import nape.shape.Polygon; import nape.constraint.PivotJoint; import nape.geom.Vec2; import nape.phys.Body; import learnNape.AbstractNapeTest; /** * @author yangfei */ public class T32_SoftBody3 extends AbstractNapeTest { public function T32_SoftBody3(gravity : Number = 600) { } private var softBodyList : Vector.<Compound> = new Vector.<Compound>(); private var segmentLength:Number = 20; private var isGasOn:Boolean = false; override protected function onNapeWorldReady() : void { createSoftBody(200,100,10,new GeomPoly(Polygon.box(90, 90))); createSoftBody(300,100,10,new GeomPoly(Polygon.regular(60, 60, 5))); addJuggleButton(); } private function addJuggleButton() : void { var btn:PushButton = new PushButton(this,400,20,"GasOFF", function(){ if(btn.label=="GasON"){ btn.label="GasOFF"; isGasOn = false; }else{ btn.label="GasON"; isGasOn = true; } }); } private function createSoftBody(cx:Number,cy:Number,thickness:Number, poly:GeomPoly) : void { var softBody:Compound = new Compound(); var segmentList:Vector.<Body> = new Vector.<Body>(); var segmentNum:int; var outPoly:GeomPoly = poly; var outPoints:Vector.<Vec2>= new Vector.<Vec2>(); var innerPoly:GeomPoly = poly.inflate(-thickness); var innerPoints:Vector.<Vec2>= new Vector.<Vec2>(); var outSegmentEdgeList : Vector.<Edge> = new Vector.<Edge>(); var outStart:Vec2 = outPoly.current(); do{ var outCurrent:Vec2 = outPoly.current(); outPoly.skipForward(1); var outNext:Vec2 = outPoly.current(); var outEdge:Vec2 = outNext.sub(outCurrent); var innerCurrent:Vec2 = innerPoly.current(); innerPoly.skipForward(1); var innerNext:Vec2=innerPoly.current(); var innerEdge:Vec2 = innerNext.sub(innerCurrent); segmentNum = Math.ceil(outEdge.length/segmentLength); for (var i:int=0; i<segmentNum; i++){ var outSegmentCurr:Vec2 = outCurrent.addMul(outEdge, i/segmentNum); var outSegmentNext:Vec2 = outCurrent.addMul(outEdge, (i+1)/segmentNum); var innerSegmentCurr:Vec2 = innerCurrent.addMul(innerEdge, i/segmentNum); var innerSegmentNext:Vec2 = innerCurrent.addMul(innerEdge, (i+1)/segmentNum); var segment:Body = new Body(); var shape:Polygon = new Polygon([outSegmentCurr,outSegmentNext,innerSegmentNext,innerSegmentCurr]); segment.shapes.add(shape); segment.align(); segment.compound=softBody; outPoints.push(outSegmentCurr); innerPoints.push(innerSegmentCurr); segmentList.push(segment); outSegmentEdgeList.push(shape.edges.at(0)); } innerEdge.dispose(); outEdge.dispose(); }while(outPoly.current()!=outStart); var prevBody:Body, currBody:Body; var currentPoint:Vec2; for (var j:int=0;j< segmentList.length; j++){ prevBody=segmentList[(j-1+segmentList.length)%segmentList.length]; currBody=segmentList[j]; currentPoint=outPoints[j]; var outJoint : PivotJoint = new PivotJoint(prevBody, currBody, prevBody.worldPointToLocal(currentPoint,true), currBody.worldPointToLocal(currentPoint,true)); outJoint.compound = softBody; currentPoint=innerPoints[j]; var innerJoint : PivotJoint = new PivotJoint(prevBody, currBody, prevBody.worldPointToLocal(currentPoint,true), currBody.worldPointToLocal(currentPoint,true)); innerJoint.stiff = false; innerJoint.damping = 1; innerJoint.frequency = 10; innerJoint.compound = softBody; innerJoint.ignore = true; } for (i = 0; i < segmentList.length; i++) { segmentList[i].position.addeq(Vec2.weak(cx,cy)); } softBody.userData.area = outPoly.area(); softBody.userData.outSegmentEdgeList = outSegmentEdgeList; softBody.space = napeWorld; softBodyList.push(softBody); } private function gasUp():void{ var sBody: Compound; var pressure : Number; for each(sBody in softBodyList){ pressure = (sBody.userData.area - getArea(sBody))/60; var edges : Vector.<Edge> = sBody.userData.outSegmentEdgeList; for(var i:int = 0; i< edges.length;i++){ var e:Edge = edges[i]; var b:Body = e.polygon.body; b.applyImpulse(e.worldNormal.mul(pressure,true),b.position,true); } } } override protected function loop(event : Event) : void { if(isGasOn) gasUp(); super.loop(event); } private function getArea(s:Compound):Number{ var edges : Vector.<Edge> = s.userData.outSegmentEdgeList; var areaPoly : GeomPoly = new GeomPoly(); var area:Number; for each(var e:Edge in edges){ areaPoly.push(e.worldVertex1); } area= areaPoly.area(); areaPoly.dispose(); return area; } } }
第21行:每个软体由多个简单的刚体组成,这些刚体由一个Compound对象统一管理,你可以把他想象成 一个包含多个Sprite的Sprite容器。所以,用来保存柔体的数组是 Vector.<Compound>类型的。
第22行:segmentLength定义了每个刚体片段的长度
第24行:isGasOn表示是否对柔体施加由内向外的支撑力。
第26~42行:定义好全局变量之后,因为示例继承了AbstractNapeTest类,所以代Nape世界创建好之后,会自动调用 onNapeWorldReady()函数,我们在这个函数里创两个软体,一个矩形、另一个是5边形。然后调用 addJuggleButton()函数添加一个开关按钮。
第44~128行:
这个createSoftBody函数是本节的重点。调用createSoftBody后会创建一个柔体对象,并返回,在返回
之前,我把他存储到45行定义的softBody对象中。
我说过需要把多边形的边分解成多个片段,我们把这些片段保存到segmentList数组中。
实际上,每条边都是要根据前面定义的segmentLength进行等分的,等分后的数量保存到segmentNum变量
中。
第51~53行:分别用outPoly和innerPoly保存了多边形的内外轮廓,这里用到了前面介绍的
GeomPoly.inflate()方法,来获取内边界innerPoly。后面的代码会将内外边界的顶点分别保存到
innerPoints和outPoints数组中。
第55行:多边形分割成多个片段后,将这些片段的最外面的边存储到outSegmentEdgeList中,稍后计算
支撑力方向和多边形面积时会用到。
第57行:记录多边形的第一个顶点,后面我们会逐一遍历多边形的每个顶点,获取每条边进行分割。
第59~94行:这里的do while循环用来遍历每一条边,并对每条边进行分割,就像上图中的第2部。
第60~63行:首先调用outPoly.current()函数记录多边形的起始顶点,然后执行skipForward(1),将顶
点的索引移至下一个顶点,并保存到outNext变量中。然后用outNext减去outCurrent,我们就得到了多
边形的第一条外边界。
第65~68行:用上面同样的方法,计算多边形的内边界。
第70行:计算刚体片段的数量segmentNum。
第72~89行:这里的这个for循环,是循环将当前索引到的多边形边界,分割成多个小的片段,并保存到
segmentList中去。
第73~76行:计算分割后的刚体片段的4个顶点。
第78~82行:通过上一步计算的顶点创建刚体片段。
第84~85行:保存刚体片段的内外顶点,稍后创建PivotJoint关节时会用到。
第86行:将刚体片段保存到segmentList中去。
第88行:将刚体片段的最外面的边保存到outSegmentEdgeList中,稍后计算支撑力方向和面积时用到.
第90~91行:删除临时变量,节约内存
第99~119行:循环遍历segmentList里的刚体,用PivotJoint将它们连接起来。
第120~122行:调整每个刚体的坐标至柔体的坐标位置,因为创建初始,是基于坐标原点(0,0)的
第123~124行:保存柔体的面积,以及所有外边缘的引用,稍后就算支撑力时用到
第129~142行:循环遍历每个柔体,对每个柔体内的刚体施加由内向外的作用力。
第133行:第一层for循环遍历softBodyList中的每个柔体。
第134行:柔体变形后,他的面积会变小,我们可以理解成对气球的挤压,我们挤压的越用力,内部气体对气球的支撑力也越大。所以这里的pressure是用开始我们在第123行保存的面积,减去用getArea实时计算出来的面积,所谓作用力。
第136~139行:遍历第124行我们保存的每个外边缘,通过Edge.worldNormal()方法获取垂直该边缘的方向作为作用力的方法,然后用mul()方法乘以刚才计算出来的pressure,施加给刚体片段
第148~160行:用外边缘的顶点新建一个Polygon,然后用Polygon.area()实时计算柔体的面积
联系作者
一开始以为很复杂很复杂,拉登讲的好到位哈哈