Web3D编程总结——3D碰撞检测初探

freenet583 9年前

来自: http://www.cnblogs.com/ljzc002/p/5182709.html

自己动手写一个方法比分析他人的写的方法困难很多,由此而来的对程序的进一步理解也是分析别人的代码很难得到的。

一、先来几张效果图:

1、场景中有两个半径为1的球体,蓝色线段从球心出发指向球体的“正向”

2、物体被选中后改变纹理图片和透明度,可以使用“w、s、a、d、空格、ctrl”控制物体相对于物体的正向“前、后、左、右、上、下”移动,按住按键时间越长移动速度越快,绿色线段由球心指向物体运动方向,速度越快露出物体表面的部分越长,按“g”停止所有移动,再次点击物体取消选中状态。

3、可以选取多个物体同时移动。

4、两物体发生碰撞后停止移动,红色线段由物体球心指向运动方向上遇到的第一个其他物体

二、碰撞检测原理:

借用THREE.Raycaster来进行碰撞检测,因为Raycaster不能检测到物体的“内表面”,所以使用反射法。

三、程序实现:

完整程序代码可以在http://files.cnblogs.com/files/ljzc002/App2.zip下载查看,其中包括详细注释,这里解释一下几个比较重要的段落。

1、绘制示意物体运动情况的线段

1 this.line1;//自有方向线  2 this.line2;//运动方向线  3 this.line3;//碰撞检测线  4 var vector2=this.v0.clone().multiplyScalar(2).add(this.object3D.position);//通过向量算出线段的结束点  5 this.line1=this.createLine2(this.object3D.position,vector2,0x0000ff,this.planetGroup,"line1");//物体在水平方向的朝向线  6 this.line2=this.createLine2(this.object3D.position,this.object3D.position,0x00ff00,this.planetGroup,"line2");  7 this.line3=this.createLine2(this.object3D.position,this.object3D.position,0xff0000,this.planetGroup,"line3");

其中点vector2由向量v0乘以2加上this.object3D.position得到,作为直线line1的结束点。注意v0后的“.clone()”如果去掉则v0本身也会应用这些变化,最终变得与vector2相同。

line2和line3被初始化为一个点。

这里建立的“line”对象并没有碰撞检测功能,纯粹是用来看的。

 1 if (this.speedw != 0 || this.speeda != 0||this.speedc!=0)   2 {//如果物体在某个方向有速度   3     var vector4=new THREE.Vector3(0,this.speedc*1000,0);   4     var vector3=(this.v1.clone().multiplyScalar(this.speeda*1000)).add(this.v0.clone().multiplyScalar(this.speedw*1000)).add(vector4);   5     var vector5=vector3.clone().normalize().multiplyScalar(this.size);   6     this.vector3=vector3.clone().add(vector5);   7     this.updateLine2(this.line2.uuid,new THREE.Vector3(0,0,0),this.vector3,0x00ff00);//从物体质心向物体运动方向,做一条长度和速度成正比的线段   8     //line2是planetGroup的子元素,它本身就会和this.object3D.position一起移动,如果再加上一个this.object3D.position就重复了,   9     //这个也是某种意义上的“相对运动”  10     this.testCollision();//碰撞检测  11     //检测无误后,最终确定下一帧位置  12     if(this.flag_coll==0) //没有发生碰撞  13     {  14         this.object3D.position.add(this.v0.clone().multiplyScalar(this.speedw));  15         this.object3D.position.add(this.v1.clone().multiplyScalar(this.speeda));  16         this.object3D.position.y += this.speedc;//直接使用等号会设置失败!!  17     }  18     else  19     {  20         this.speeda=0;  21         this.speedc=0;  22         this.speedw=0;  23         this.flag_coll=0;//重置为没有发生碰撞的状态  24         currentlyPressedKeys[65]=false;  25         currentlyPressedKeys[68]=false;  26         currentlyPressedKeys[87]=false;  27         currentlyPressedKeys[83]=false;  28         currentlyPressedKeys[32]=false;  29         currentlyPressedKeys[17]=false;  30     }  31 }

根据物体的运动情况更新line2的方向和长度。在这里vector3由物体在各个方向上的速度分量组合而成,vector5负责把vector3调整为适合显示的长度,this.vector3和vector3是不同的对象,表示line2一个端点的位移。

updateLine2更新line2的端点,表现出速度的变化。我们可以看到它的两个端点位置参数是(0,0,0)和一个向量而不是前面图中的球心位置和“球心位置加向量得到的点”,这体现出了Three.js父子物体间的相对性。(简单的碰撞示例原本不需要使用子物体,我是不是自讨苦吃?)

该示例延续自上一篇中的太阳系模型,物体间的关系如下图:

planetOrbitGroup和planetGroup是“Object3D”对象,这种“物体”没有大小、颜色属性只有位置和姿态属性(默认位于父物体的原点),“Mesh”和“Line”多重继承于Object3D具有顶点(geometry)、纹理(material)属性。

planetOrbitGroup是Scene的子物体位于世界坐标系原点,planetGroup是planetOrbitGroup的子物体强制定位于世界坐标系x=3处,globeMesh和line2是planetGroup的子物体默认位于planetGroup的原点。

当planetOrbitGroup移动时(改变this.object3D.position),这个移动效果会被它所有的后代对象继承,所以我们把line2的一个顶点设为(0,0,0)后会自动继承它所有祖先元素的移动效果(this.object3D.position+(3,0,0))。

2、基于射线的碰撞测试

1 var raycaster= new THREE.Raycaster(this.planetGroup.position.clone().add(this.object3D.position),this.vector3.clone().normalize());//从物体中心向实际移动方向发出一条射线  2 raycaster.far=this.object3D.inter_length;//射线“长度”  3 var intersects = raycaster.intersectObjects(scene.children,true);

建立第一条射线,与前面的线段不同,这里的射线是数学意义上的射线,它不是一个物体也无法被渲染出来。因为它不是任何物体的子物体,所以raycaster的端点位置取世界坐标而非相对坐标,注意和前面同样位置的线段端点的不同。

因为我们使用了子物体,所以intersectObjects的第二个参数必须设为true以强制检查每一个子物体,否则raycaster只会检查第一个参数这一层的物体而忽略掉globeMesh。

 1      if ( intersects.length > 0 ) {   2         var flag_safe=0;//应该可以省掉这个变量   3         //安然走完下面的循环说明,在碰撞区内没有任何其他物体   4         for(var i=0;i<intersects.length;i++)   5         {   6             //规定碰撞的物体必须是可见的,必须是有面的,必须不是原物体,必须在碰撞检测范围以内(其实是因为射线穿过子物体结果的不确定性)   7             if (intersects[i].object.visible && intersects[i].face&&(this.object3D.inter_group!=intersects[i].object.inter_group)&&intersects[i].distance<this.object3D.inter_length) {   8    9                 var intersected = intersects[i];  10                 this.updateLine2(this.line3.uuid,new THREE.Vector3(0,0,0),intersected.point.clone().sub(this.planetGroup.position.clone().add(this.object3D.position)),0xff0000);//碰撞检测线

前面提到raycaster无法检测到物体的内表面,但在涉及到子物体检测时,这一命题变得不确定了,所以要加上更多的判断条件(这是不是自己坑自己。。。)

 1                 var raycaster2= new THREE.Raycaster(intersected.point,this.vector3.clone().negate().normalize());//射线与物体相交后反射回来   2                 raycaster2.far=this.object3D.inter_length;   3                 var intersects2 = raycaster2.intersectObjects(scene.children,true);   4                 //既然已经碰到了别的物体,就必须反射回原物体,才能保证不碰撞(反射回原物体另一面的情况由碰撞边界值剔除)   5                 if ( intersects2.length > 0 )   6                 {   7                     flag_safe=1;   8                     for(var j=0;j<intersects2.length;j++)   9                     {  10                         if (intersects2[j].object.visible && intersects2[j].face&&(intersected.object.inter_group!=intersects2[j].object.inter_group)&&intersects2[j].distance<this.object3D.inter_length)  11                         {  12                             if(intersects2[j].object.inter_group==this.object3D.inter_group)//inter_group属性相同,返回了原物体  13                             {  14                                 flag_safe=0;//没有发生碰撞  15                             }  16                             break;  17                         }  18                     }  19                     if(flag_safe==1)  20                     {  21                         this.flag_coll = 1;  22                     }  23                 }

这里的逻辑还不够优雅,下次再调整吧

四、优化方向:

1、现在的“3D碰撞检测”实现了最简单情况下的碰撞检测,但算法仍具有很大的局限性,比如这种情况下,碰撞检测射线永远无法穿过其他物体:

对此我想到的解决方法是:

将raycaster扩充为检测方向上的多条平行射线来检测物体边缘碰撞的情况,而这需要用到线性代数的相关知识,需再复习一下。

2、在没有设置半透明的情况下运行,可能会发生物体重叠但没有判断碰撞的情况,怀疑是因为我采用的是“先碰撞后检测”的方法,Three.js认为重叠部分的图元不需要绘制将其自动舍弃,导致射线检测不到重叠部分。

五、扩展:

昨天发现美国前辈Lee Stemkoski的Three.js示例展示了另一种碰撞检测方法,和我的方法相比各有优缺点

演示地址:http://stemkoski.github.io/Three.js/Collision-Detection.html

核心代码:

 1     for (var vertexIndex = 0; vertexIndex < MovingCube.geometry.vertices.length; vertexIndex++)   2     {           3         var localVertex = MovingCube.geometry.vertices[vertexIndex].clone();   4         var globalVertex = localVertex.applyMatrix4( MovingCube.matrix );   5         var directionVector = globalVertex.sub( MovingCube.position );   6            7         var ray = new THREE.Raycaster( originPoint, directionVector.clone().normalize() );   8         var collisionResults = ray.intersectObjects( collidableMeshList );   9         if ( collisionResults.length > 0 && collisionResults[0].distance < directionVector.length() )   10             appendText(" Hit ");  11     }

该方法从物体的中心向物体的每一个顶点做一条碰撞检测射线,如果碰到第一个物体的碰撞点到物体中心的距离小于物体中心到该顶点的距离,则认为发生碰撞。