自己動手寫一個方法比分析他人的寫的方法困難很多,由此而來的對程式的進一步理解也是分析別人的代碼很難得到的。 一、先來幾張效果圖: 1、場景中有兩個半徑為1的球體,藍色線段從球心出髮指向球體的“正向” 2、物體被選中後改變紋理圖片和透明度,可以使用“w、s、a、d、空格、ctrl”控制物體相對於物體的
自己動手寫一個方法比分析他人的寫的方法困難很多,由此而來的對程式的進一步理解也是分析別人的代碼很難得到的。
一、先來幾張效果圖:
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 }
該方法從物體的中心向物體的每一個頂點做一條碰撞檢測射線,如果碰到第一個物體的碰撞點到物體中心的距離小於物體中心到該頂點的距離,則認為發生碰撞。