設計目標:借鑑前輩程式設計者的經驗將簡單的配置文字轉化為3D場景,並根據配置檔案在場景中加入圖片和可播放的視訊,最終形成可瀏覽的3D陳列室。
一、使用效果
1、txt配置檔案:
(部落格園的富文字編輯器會改變txt文字的排版,所以用圖片方式呈現文字)
第一行表示陳列室的每一層前後最多有5個房間,左右最多有8個房間,接下來是第一層的地圖:“0”表示普通房間,“+、-、|”表示連線房間的通道,“#”表示地面有洞的房間可用來連線下一層,“^”表示頂部有洞的房間用來連線上一層。“//source”後面是本層的資源,用豎線分隔的引數依次表示前後位置、左右位置、資源“貼在”哪面牆上、資源型別、資源url,比如“//source:2|4|z|mp4|big_buck_bunny.mp4”即表示在第二行第四列的房間的z面(前面)貼上url為big_buck_bunny.mp4的mp4視訊。再下面則是-1層的地圖。
2、顯示效果
房間的整體效果如下:
渲染出了配置檔案中設定的房間佈局,可以通過修改程式碼替換預設的草地和線框紋理。場景預設使用Babylon.js的自由相機進行控制,按“v”鍵可以啟用fps式控制,滑鼠移動直接控制視角,wasd控制前後左右,c和空格控制升降,同時啟用相機與牆壁的碰撞檢測阻止穿牆,再次按v鍵則可關閉fps控制。
進入第二層中間的房間可以看到房間兩側的視訊,點選即可播放:
進入第二層右側的房間,可以看到相鄰的小房間融合為一個大房間:
可以通過https://ljzc002.github.io/Txt2room/HTML/PAGE/room.html檢視線上例項,程式碼沒有進行編譯可以直接線上除錯,在https://github.com/ljzc002/ljzc002.github.io/tree/master/Txt2room檢視專案程式碼。
二、程式碼實現
1、建立房間零件的源網格(master mesh)
為了提高渲染效率,這裡並沒有為每個房間建立獨立的mesh物件,而是將房間拆解為基礎的組成零件,對零件建立源網格,然後用WebGL的instance技術批量生成源網格的例項。
以下是生成預製件源網格的方法:
1 var size_per_u=3;//1紋理座標長度對應場景的3單位長度 2 var size_per_v=3; 3 var positions=[]; 4 var uvs=[]; 5 var normals=[]; 6 var indices=[]; 7 function initMeshClass() 8 {//plan的基礎狀態是一個位於原點,面向z軸負方向的平面 9 add_plan2({x:-4.5,y:4.5,z:0},{x:1.5,y:4.5,z:0},{x:1.5,y:1.5,z:0},{x:-4.5,y:1.5,z:0},0); 10 add_plan2({x:1.5,y:4.5,z:0},{x:4.5,y:4.5,z:0},{x:4.5,y:-1.5,z:0},{x:1.5,y:-1.5,z:0},4,6/size_per_u); 11 add_plan2({x:-1.5,y:-1.5,z:0},{x:4.5,y:-1.5,z:0},{x:4.5,y:-4.5,z:0},{x:-1.5,y:-4.5,z:0},8,3/size_per_u,6/size_per_v); 12 add_plan2({x:-4.5,y:1.5,z:0},{x:-1.5,y:1.5,z:0},{x:-1.5,y:-4.5,z:0},{x:-4.5,y:-4.5,z:0},12,0,3/size_per_v); 13 var mesh=vertexData2Mesh(positions, indices, normals, uvs,"class_hole",mat_grass); 14 mesh.setEnabled(false);//令源網格不顯示 15 // 很奇怪如果不對長通道設定mesh.setEnabled(false);則例項無法正常顯示,但其他類的例項則沒有這種問題。 16 //mesh.setEnabled(true);//預設就是這個 17 obj_meshclass["hole"]=mesh;//帶有洞的牆壁 18 19 positions=[];//新建式清空,理論上不影響引用的資料 20 uvs=[]; 21 normals=[]; 22 indices=[]; 23 add_plan2({x:-4.5,y:4.5,z:0},{x:4.5,y:4.5,z:0},{x:4.5,y:-4.5,z:0},{x:-4.5,y:-4.5,z:0},0); 24 var mesh=vertexData2Mesh(positions, indices, normals, uvs,"class_wall",mat_grass); 25 mesh.setEnabled(false); 26 obj_meshclass["wall"]=mesh;//牆壁 27 28 positions=[]; 29 uvs=[]; 30 normals=[]; 31 indices=[]; 32 add_plan2({x:-1.5,y:1.5,z:0},{x:1.5,y:1.5,z:0},{x:1.5,y:-1.5,z:0},{x:-1.5,y:-1.5,z:0},0); 33 var mesh=vertexData2Mesh(positions, indices, normals, uvs,"class_smallwall",mat_grass); 34 mesh.setEnabled(false); 35 obj_meshclass["smallwall"]=mesh;//小塊牆壁 36 37 positions=[]; 38 uvs=[]; 39 normals=[]; 40 indices=[]; 41 add_plan2({x:-1.5,y:1.5,z:-4.5},{x:-1.5,y:1.5,z:4.5},{x:1.5,y:1.5,z:4.5},{x:1.5,y:1.5,z:-4.5},0); 42 add_plan2({x:1.5,y:1.5,z:-4.5},{x:1.5,y:1.5,z:4.5},{x:1.5,y:-1.5,z:4.5},{x:1.5,y:-1.5,z:-4.5},4); 43 add_plan2({x:1.5,y:-1.5,z:-4.5},{x:1.5,y:-1.5,z:4.5},{x:-1.5,y:-1.5,z:4.5},{x:-1.5,y:-1.5,z:-4.5},8); 44 add_plan2({x:-1.5,y:-1.5,z:-4.5},{x:-1.5,y:-1.5,z:4.5},{x:-1.5,y:1.5,z:4.5},{x:-1.5,y:1.5,z:-4.5},12); 45 var mesh=vertexData2Mesh(positions, indices, normals, uvs,"class_channel",mat_frame); 46 //mesh.setEnabled(false); 47 // 很奇怪如果不對長通道設定mesh.setEnabled(false);則例項無法正常顯示,但其他類的例項則沒有這種問題。 48 mesh.setEnabled(false); 49 obj_meshclass["channel"]=mesh;//長通道 50 51 positions=[]; 52 uvs=[]; 53 normals=[]; 54 indices=[]; 55 add_plan2({x:-1.5,y:1.5,z:1.5},{x:-1.5,y:1.5,z:4.5},{x:1.5,y:1.5,z:4.5},{x:1.5,y:1.5,z:1.5},0); 56 add_plan2({x:1.5,y:1.5,z:1.5},{x:1.5,y:1.5,z:4.5},{x:1.5,y:-1.5,z:4.5},{x:1.5,y:-1.5,z:1.5},4); 57 add_plan2({x:1.5,y:-1.5,z:1.5},{x:1.5,y:-1.5,z:4.5},{x:-1.5,y:-1.5,z:4.5},{x:-1.5,y:-1.5,z:1.5},8); 58 add_plan2({x:-1.5,y:-1.5,z:1.5},{x:-1.5,y:-1.5,z:4.5},{x:-1.5,y:1.5,z:4.5},{x:-1.5,y:1.5,z:1.5},12); 59 var mesh=vertexData2Mesh(positions, indices, normals, uvs,"class_shortchannel",mat_grass); 60 mesh.setEnabled(false); 61 obj_meshclass["shortchannel"]=mesh;//短通道 62 }
其中add_plan2方法用來根據四個給定的頂點生成平整的四邊形頂點資料:
1 //平面四個頂點的座標(從左上角開始順時針排列),第一個頂點的插入索引,uv紋理的座標的偏移量 2 function add_plan2(v1,v2,v3,v4,index,offsetu,offsetv) 3 { 4 positions.push(v1.x); 5 positions.push(v1.y); 6 positions.push(v1.z); 7 positions.push(v2.x); 8 positions.push(v2.y); 9 positions.push(v2.z); 10 positions.push(v3.x); 11 positions.push(v3.y); 12 positions.push(v3.z); 13 positions.push(v4.x); 14 positions.push(v4.y); 15 positions.push(v4.z); 16 //使用和Babylon.js條帶網格相同的頂點順序 17 indices.push(index+3); 18 indices.push(index+2); 19 indices.push(index); 20 indices.push(index+1); 21 indices.push(index); 22 indices.push(index+2); 23 //根據頂點位置計算平整紋理座標 24 //1234對應abcd 25 var vab=v3subtract(v2,v1); 26 var lab=v3length(vab); 27 var vac=v3subtract(v3,v1); 28 var lac=v3length(vac); 29 var vad=v3subtract(v4,v1); 30 var lad=v3length(vad); 31 32 var BAC=Math.acos((vab.x*vac.x+vab.y*vac.y+vab.z*vac.z)/(lab*lac)); 33 var BAD=Math.acos((vab.x*vad.x+vab.y*vad.y+vab.z*vad.z)/(lab*lad)); 34 if(!offsetu) 35 { 36 offsetu=0; 37 } 38 if(!offsetv) 39 { 40 offsetv=0; 41 } 42 uvs.push(offsetu); 43 uvs.push(offsetv); 44 uvs.push(offsetu+lab/size_per_u); 45 uvs.push(offsetv); 46 uvs.push(offsetu+(lac*Math.cos(BAC)/size_per_u)); 47 uvs.push(offsetv+(lac*Math.sin(BAC)/size_per_v)); 48 uvs.push(offsetu+(lad*Math.cos(BAD)/size_per_u)); 49 uvs.push(offsetv+(lad*Math.sin(BAD)/size_per_v)); 50 } 51 function v3subtract(v1,v2)//向量相減 52 { 53 return {x:(v1.x-v2.x),y:(v1.y-v2.y),z:(v1.z-v2.z)} 54 } 55 function v3length(v)//計算向量長度 56 { 57 return Math.pow(v.x*v.x+v.y*v.y+v.z*v.z,0.5) 58 }
計算平整紋理座標使用了向量點乘的性質:vab.x*vac.x+vab.y*vac.y+vab.z*vac.z=vab.vac=lab*lac*cosCAB
這使得我們可以根據三角形三個頂點的座標計算出其中兩個向量的夾角,進而在這兩個向量確定的平面中計算出三個頂點的紋理座標。
可以在https://forum.babylonjs.com/t/which-way-should-i-choose-to-make-a-custom-mesh-from-ribbon/10793檢視一些關於為什麼要進行紋理平整的討論。
vertexData2Mesh方法用來將生成的頂點資料轉化為網格,
1 function vertexData2Mesh(positions, indices, normals, uvs,name,material) 2 { 3 var vertexData= new BABYLON.VertexData();//頂點資料物件 4 BABYLON.VertexData.ComputeNormals(positions, indices, normals);//計演算法線 5 BABYLON.VertexData._ComputeSides(0, positions, indices, normals, uvs); 6 vertexData.indices = indices.concat();//索引 7 vertexData.positions = positions.concat(); 8 vertexData.normals = normals.concat();//position改變法線也要改變!!!! 9 vertexData.uvs = uvs.concat(); 10 var mesh=new BABYLON.Mesh(name,scene); 11 vertexData.applyToMesh(mesh, true); 12 mesh.vertexData=vertexData; 13 mesh.material=material; 14 mesh.renderingGroupId=2; 15 return mesh; 16 }
最後mesh.setEnabled(false)用來隱藏源網格。
2、讀取配置文字,提取房間資訊
1 var str=newland.importString("06.txt"); 2 //console.log(str); 3 var arr=str.split("\r\n")//限於window作業系統下?? 4 var len=arr.length; 5 for(var i=0;i<len;i++)//對於每一行 6 { 7 var line=arr[i]; 8 if(line.substring(0,2)=="//") 9 { 10 var arr2=line.substring(2).split("@"); 11 var len2=arr2.length; 12 for(var j=0;j<len2;j++) 13 { 14 var obj=arr2[j]; 15 var arr3=obj.split(":"); 16 var ptype=arr3[0]; 17 var pvalue=arr3[1]; 18 if(ptype=="seg_z") 19 { 20 seg_z=parseInt(pvalue); 21 } 22 else if(ptype=="seg_x") 23 { 24 seg_x=parseInt(pvalue); 25 } 26 else if(ptype=="floor")//進入了一層 27 { 28 i=handleFloor(pvalue,arr,i+1); 29 } 30 } 31 } 32 }
其中importString是一個讀取服務端文字檔案的方法,其程式碼如下:
1 newland.importString=function(url) 2 { 3 var xhr=new XMLHttpRequest; 4 xhr.open("GET",url,false);//第三個參數列示是同步載入 5 xhr.send(null); 6 var data=xhr.responseText; 7 return data; 8 }
讀入後一行行遍歷文字,發現“//floor”則開始處理這一層的房間資料:
1 function handleFloor(int_floor,arr,index) 2 { 3 var floor=obj_building[int_floor];//在obj_building中儲存所有房間資訊 4 if(!floor) 5 { 6 obj_building[int_floor]={}; 7 floor=obj_building[int_floor]; 8 } 9 var len=arr.length; 10 var count=0; 11 //繼續讀txt文字 12 for(var i=index;i<len;i++) 13 { 14 var line=arr[i]; 15 count++; 16 if(count<=seg_z) 17 { 18 19 if(!floor[count+""]) 20 { 21 floor[count+""]={} 22 } 23 24 for(var j=0;j<seg_x;j++) 25 { 26 if(line[j]) 27 { 28 29 floor[count+""][j+1+""]={type:line[j],arr_source:[]};//這個“陣列”都是從一開始的 30 //addRoom(count-1,j);//行、列,規劃完畢後統一新增渲染 31 } 32 } 33 } 34 else 35 { 36 if(line.substring(0,7)=="//floor")//查詢到另一層 37 { 38 return (index+count-2); 39 } 40 else if(line.substring(0,8)=="//source")//為這個房間設定資源 41 { 42 //var arr2=line.split(":")[1].split("|"); 43 var arr2=line.substring(line.search(":")+1).split("|"); 44 if(floor[arr2[0]]&&floor[arr2[0]][arr2[1]]) 45 { 46 var arr_source=floor[arr2[0]][arr2[1]].arr_source;//這個房間的資源列表 47 var obj={}; 48 obj.sourceSide=arr2[2]; 49 obj.sourceType=arr2[3]; 50 obj.sourceUrl=arr2[4]; 51 arr_source.push(obj); 52 } 53 54 } 55 } 56 57 } 58 return (len);//查詢到檔案末尾 59 }
經過以上處理,配置檔案中的房間資訊都被提取到obj_building中。
3、根據房間資訊排列源網格的例項,並放置資源。
程式碼如下:(有一定冗餘)
1 function handleBuilding() 2 { 3 var len=0; 4 for(var key in obj_building) 5 { 6 len++;//總層數 7 } 8 for(var key in obj_building)//對於每一層 9 { 10 var int_key=parseInt(key); 11 var floor=obj_building[key]; 12 //尋找這一層的上下兩層,這裡假設obj_building是沒有順序的 13 var int_key_shang=int_key,int_key_xia=int_key; 14 var floor_shang=null,floor_xia=null; 15 //for(var i=int_key;i<) 16 for(var key2 in obj_building) 17 { 18 var int_key2=parseInt(key2); 19 if((int_key2>int_key)&&(int_key_shang==int_key||int_key_shang>int_key2)) 20 { 21 int_key_shang=int_key2; 22 floor_shang=obj_building[key2]; 23 } 24 if((int_key2<int_key)&&(int_key_xia==int_key||int_key_xia<int_key2)) 25 { 26 int_key_shang=int_key2; 27 floor_xia=obj_building[key2]; 28 } 29 } 30 for(var i=1;i<=seg_z;i++)//對於本層的每一行房間 31 { 32 var row=floor[i+""]; 33 if(row)//如果有這一行 34 { 35 for(var j=0;j<=seg_x;j++)//對於本行的每一個房間 36 { 37 var room=row[j+""]; 38 //根據房間的型別不同決定是否要檢視其周圍的房間 39 if(room) 40 {//@@@@普通房間,要考慮前後左右的四個房間狀態,要考慮資源放置 41 if(room.type=="0"||room.type=="#"||room.type=="^") 42 { 43 //room.arr_source=[]; 44 //考慮前面 45 if(floor[i-1+""]&&floor[i-1+""][j+""]) 46 { 47 var room2=floor[i-1+""][j+""]; 48 if(!room2)//如果沒有東西,就是普通牆壁 49 { 50 //網格型別,例項名字,位置,姿態 51 drawMesh("wall","wall_z_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(0.5-i)*sizez) 52 ,new BABYLON.Vector3(0,0,0)) 53 } 54 55 else if(room2.type=="|"||room2.type=="+") 56 { 57 drawMesh("hole","hole_z_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(0.5-i)*sizez) 58 ,new BABYLON.Vector3(0,0,0)) 59 } 60 else if(room2.type=="0"||room2.type=="#"||room2.type=="^")//旁邊也是一個房間則合併房間,不繪製牆壁 61 { 62 63 } 64 else//預設繪製牆壁 65 { 66 drawMesh("wall","wall_z_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(0.5-i)*sizez) 67 ,new BABYLON.Vector3(0,0,0)) 68 } 69 } 70 else//預設繪製牆壁 71 { 72 drawMesh("wall","wall_z_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(0.5-i)*sizez) 73 ,new BABYLON.Vector3(0,0,0)) 74 } 75 //後面 76 if(floor[i+1+""]&&floor[i+1+""][j+""]) 77 { 78 var room2=floor[i+1+""][j+""]; 79 if(!room2)//如果沒有東西,就是普通牆壁 80 { 81 //網格型別,例項名字,位置,姿態 82 drawMesh("wall","wall_z-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(-0.5-i)*sizez) 83 ,new BABYLON.Vector3(0,0,0)) 84 } 85 86 else if(room2.type=="|"||room2.type=="+") 87 { 88 drawMesh("hole","hole_z-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(-0.5-i)*sizez) 89 ,new BABYLON.Vector3(0,0,0)) 90 } 91 else if(room2.type=="0"||room2.type=="#"||room2.type=="^")//旁邊也是一個房間則合併房間,不繪製牆壁 92 { 93 94 } 95 else//預設繪製牆壁 96 { 97 drawMesh("wall","wall_z-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(-0.5-i)*sizez) 98 ,new BABYLON.Vector3(0,0,0)) 99 } 100 } 101 else//預設繪製牆壁 102 { 103 drawMesh("wall","wall_z-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(-0.5-i)*sizez) 104 ,new BABYLON.Vector3(0,0,0)) 105 } 106 //左邊 107 if(floor[i+""][j-1+""]) 108 { 109 var room2=floor[i+""][j-1+""]; 110 if(!room2)//如果沒有東西,就是普通牆壁 111 { 112 //網格型別,例項名字,位置,姿態 113 drawMesh("wall","wall_x-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j-0.5)*sizex,int_key*sizey,(-i)*sizez) 114 ,new BABYLON.Vector3(0,Math.PI/2,0)) 115 } 116 117 else if(room2.type=="-"||room2.type=="+") 118 { 119 drawMesh("hole","hole_x-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j-0.5)*sizex,int_key*sizey,(-i)*sizez) 120 ,new BABYLON.Vector3(0,Math.PI/2,0)) 121 } 122 else if(room2.type=="0"||room2.type=="#"||room2.type=="^")//旁邊也是一個房間則合併房間,不繪製牆壁 123 { 124 125 } 126 else//預設繪製牆壁 127 { 128 drawMesh("wall","wall_x-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j-0.5)*sizex,int_key*sizey,(-i)*sizez) 129 ,new BABYLON.Vector3(0,Math.PI/2,0)) 130 } 131 } 132 else//預設繪製牆壁 133 { 134 drawMesh("wall","wall_x-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j-0.5)*sizex,int_key*sizey,(-i)*sizez) 135 ,new BABYLON.Vector3(0,Math.PI/2,0)) 136 } 137 //右邊 138 if(floor[i+""][j+1+""]) 139 { 140 var room2=floor[i+""][j+1+""]; 141 if(!room2)//如果沒有東西,就是普通牆壁 142 { 143 //網格型別,例項名字,位置,姿態 144 drawMesh("wall","wall_x_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j+0.5)*sizex,int_key*sizey,(-i)*sizez) 145 ,new BABYLON.Vector3(0,Math.PI/2,0)) 146 } 147 148 else if(room2.type=="-"||room2.type=="+") 149 { 150 drawMesh("hole","hole_x_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j+0.5)*sizex,int_key*sizey,(-i)*sizez) 151 ,new BABYLON.Vector3(0,Math.PI/2,0)) 152 } 153 else if(room2.type=="0"||room2.type=="#"||room2.type=="^")//旁邊也是一個房間則合併房間,不繪製牆壁 154 { 155 156 } 157 else//預設繪製牆壁 158 { 159 drawMesh("wall","wall_x_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j+0.5)*sizex,int_key*sizey,(-i)*sizez) 160 ,new BABYLON.Vector3(0,Math.PI/2,0)) 161 } 162 } 163 else//預設繪製牆壁 164 { 165 drawMesh("wall","wall_x_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j+0.5)*sizex,int_key*sizey,(-i)*sizez) 166 ,new BABYLON.Vector3(0,Math.PI/2,0)) 167 } 168 //上面 169 if(room.type=="^") 170 { 171 drawMesh("hole","hole_y_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,(int_key+0.5)*sizey,(-i)*sizez) 172 ,new BABYLON.Vector3(Math.PI/2,0,0)) 173 //還要負責向上連線 174 if(floor_shang) 175 { 176 for(var k=int_key+1;k<int_key_shang;k++) 177 { 178 drawMesh("channel","channel_^_"+k+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,(k)*sizey,(-i)*sizez) 179 ,new BABYLON.Vector3(Math.PI/2,0,0)) 180 } 181 } 182 //暫時不設定彈射器,使用失重模式 183 } 184 else 185 { 186 drawMesh("wall","wall_y_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,(int_key+0.5)*sizey,(-i)*sizez) 187 ,new BABYLON.Vector3(Math.PI/2,0,0)) 188 } 189 //下面 190 if(room.type=="#") 191 { 192 drawMesh("hole","hole_y-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,(int_key-0.5)*sizey,(-i)*sizez) 193 ,new BABYLON.Vector3(Math.PI/2,0,0)) 194 } 195 else 196 { 197 drawMesh("wall","wall_y-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,(int_key-0.5)*sizey,(-i)*sizez) 198 ,new BABYLON.Vector3(Math.PI/2,0,0)) 199 } 200 //翻轉方向會影響碰撞檢測嗎? 201 //最後處理資源 202 } 203 //@@@@表示通道的三種符號,要考慮其前後左右的位置 204 else if(room.type=="-"||room.type=="+"||room.type=="|") 205 { 206 if(room.type=="-") 207 {//橫向長通道 208 drawMesh("channel","channel_-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,(int_key)*sizey,(-i)*sizez) 209 ,new BABYLON.Vector3(0,Math.PI/2,0)) 210 } 211 else if(room.type=="|") 212 {//縱向長通道 213 drawMesh("channel","channel_|_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,(int_key)*sizey,(-i)*sizez) 214 ,new BABYLON.Vector3(0,0,0)) 215 } 216 else 217 {//十字連線件 218 //考慮前面 219 if(floor[i-1+""]&&floor[i-1+""][j+""]) 220 { 221 var room2=floor[i-1+""][j+""]; 222 if(!room2)//如果沒有東西,就是普通牆壁 223 { 224 //網格型別,例項名字,位置,姿態 225 drawMesh("smallwall","smallwall_z_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(0.5/3-i)*sizez) 226 ,new BABYLON.Vector3(0,0,0)) 227 } 228 229 else if(room2.type=="|"||room2.type=="+"||room2.type=="0"||room2.type=="#"||room2.type=="^") 230 {//短通道自帶位移 231 drawMesh("shortchannel","shortchannel_z_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(-i)*sizez) 232 ,new BABYLON.Vector3(0,0,0)) 233 } 234 else//預設繪製小型牆壁 235 { 236 drawMesh("smallwall","smallwall_z_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(0.5/3-i)*sizez) 237 ,new BABYLON.Vector3(0,0,0)) 238 } 239 } 240 else//預設繪製小型牆壁 241 { 242 drawMesh("smallwall","smallwall_z_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(0.5/3-i)*sizez) 243 ,new BABYLON.Vector3(0,0,0)) 244 } 245 //後面 246 if(floor[i+1+""]&&floor[i+1+""][j+""]) 247 { 248 var room2=floor[i+1+""][j+""]; 249 if(!room2)//如果沒有東西,就是普通牆壁 250 { 251 //網格型別,例項名字,位置,姿態 252 drawMesh("smallwall","smallwall_z-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(-0.5/3-i)*sizez) 253 ,new BABYLON.Vector3(0,0,0)) 254 } 255 256 else if(room2.type=="|"||room2.type=="+"||room2.type=="0"||room2.type=="#"||room2.type=="^") 257 { 258 drawMesh("shortchannel","shortchannel_z-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(-i)*sizez) 259 ,new BABYLON.Vector3(0,Math.PI,0)) 260 } 261 else//預設繪製小型牆壁 262 { 263 drawMesh("smallwall","smallwall_z-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(-0.5/3-i)*sizez) 264 ,new BABYLON.Vector3(0,0,0)) 265 } 266 } 267 else//預設繪製小型牆壁 268 { 269 drawMesh("smallwall","smallwall_z-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(-0.5/3-i)*sizez) 270 ,new BABYLON.Vector3(0,0,0)) 271 } 272 //左邊 273 if(floor[i+""][j-1+""]) 274 { 275 var room2=floor[i+""][j-1+""]; 276 if(!room2)//如果沒有東西,就是小型牆壁 277 { 278 //網格型別,例項名字,位置,姿態 279 drawMesh("smallwall","smallwall_x-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j-0.5/3)*sizex,int_key*sizey,(-i)*sizez) 280 ,new BABYLON.Vector3(0,Math.PI/2,0)) 281 } 282 283 else if(room2.type=="-"||room2.type=="+"||room2.type=="0"||room2.type=="#"||room2.type=="^") 284 { 285 drawMesh("shortchannel","shortchannel_x-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,int_key*sizey,(-i)*sizez) 286 ,new BABYLON.Vector3(0,-Math.PI/2,0)) 287 } 288 else//預設繪製小型牆壁 289 { 290 drawMesh("smallwall","smallwall_x-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j-0.5/3)*sizex,int_key*sizey,(-i)*sizez) 291 ,new BABYLON.Vector3(0,Math.PI/2,0)) 292 } 293 } 294 else//預設繪製小型牆壁 295 { 296 drawMesh("smallwall","smallwall_x-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j-0.5/3)*sizex,int_key*sizey,(-i)*sizez) 297 ,new BABYLON.Vector3(0,Math.PI/2,0)) 298 } 299 //右邊 300 if(floor[i+""][j+1+""]) 301 { 302 var room2=floor[i+""][j+1+""]; 303 if(!room2)//如果沒有東西,就是普通牆壁 304 { 305 //網格型別,例項名字,位置,姿態 306 drawMesh("smallwall","smallwall_x_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j+0.5/3)*sizex,int_key*sizey,(-i)*sizez) 307 ,new BABYLON.Vector3(0,Math.PI/2,0)) 308 } 309 310 else if(room2.type=="-"||room2.type=="+"||room2.type=="0"||room2.type=="#"||room2.type=="^") 311 { 312 drawMesh("shortchannel","shortchannel_x_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,int_key*sizey,(-i)*sizez) 313 ,new BABYLON.Vector3(0,Math.PI/2,0)) 314 } 315 else//預設繪製牆壁 316 { 317 drawMesh("smallwall","smallwall_x_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j+0.5/3)*sizex,int_key*sizey,(-i)*sizez) 318 ,new BABYLON.Vector3(0,Math.PI/2,0)) 319 } 320 } 321 else//預設繪製牆壁 322 { 323 drawMesh("smallwall","smallwall_x_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j+0.5/3)*sizex,int_key*sizey,(-i)*sizez) 324 ,new BABYLON.Vector3(0,Math.PI/2,0)) 325 } 326 //上面 327 drawMesh("smallwall","smallwall_y_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,(int_key+0.5/3)*sizey,(-i)*sizez) 328 ,new BABYLON.Vector3(Math.PI/2,0,0)) 329 //下面 330 drawMesh("smallwall","smallwall_y-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,(int_key-0.5/3)*sizey,(-i)*sizez) 331 ,new BABYLON.Vector3(Math.PI/2,0,0)) 332 333 } 334 } 335 //如果這個房間有資源 336 if(room.arr_source) 337 { 338 var arr_source=room.arr_source; 339 var len=arr_source.length; 340 for(var k=0;k<len;k++) 341 { 342 var source=arr_source[k]; 343 if(source.sourceType=="mp4"||source.sourceType=="jpg"||source.sourceType=="png") 344 { 345 var mesh_plan=new BABYLON.MeshBuilder.CreatePlane(source.sourceType+"_"+source.sourceSide+"_"+int_key+"_"+i+"_"+j, 346 {height:4.5,width:8},scene);//建立一個平面網格用來展示資源 347 var pos={x:0,y:0,z:0},rot=new BABYLON.Vector3(0,0,0); 348 if(source.sourceSide=="z")//根據資源所在的牆壁不同調整資源網格的位置和姿態 349 { 350 pos.z=0.4 351 }else if(source.sourceSide=="z-") 352 { 353 pos.z=-0.4; 354 rot.y=Math.PI; 355 } 356 else if(source.sourceSide=="x") 357 { 358 pos.x=0.4; 359 rot.y=-Math.PI/2; 360 } 361 else if(source.sourceSide=="x-") 362 { 363 pos.x=-0.4; 364 rot.y=Math.PI/2; 365 } 366 else if(source.sourceSide=="y") 367 { 368 pos.y=0.4; 369 rot.x=Math.PI/2; 370 } 371 else if(source.sourceSide=="z-") 372 { 373 pos.y=-0.4; 374 rot.x=-Math.PI/2; 375 } 376 mesh_plan.position=new BABYLON.Vector3((j+pos.x)*sizex,(int_key+pos.y)*sizey,(-i+pos.z)*sizez); 377 mesh_plan.rotation=rot; 378 mesh_plan.renderingGroupId=2; 379 if(source.sourceType=="jpg"||source.sourceType=="png") 380 { 381 var materialf = new BABYLON.StandardMaterial("mat_"+source.sourceSide+"_"+int_key+"_"+i+"_"+j, scene); 382 383 materialf.diffuseTexture = new BABYLON.Texture(source.sourceUrl, scene); 384 materialf.diffuseTexture.hasAlpha = false; 385 materialf.backFaceCulling = true; 386 materialf.freeze(); 387 mesh_plan.material =materialf; 388 } 389 else if(source.sourceType=="mp4") 390 { 391 var mat = new BABYLON.StandardMaterial("mat_"+source.sourceSide+"_"+int_key+"_"+i+"_"+j, scene); 392 //從Chrome 66開始為了避免標籤產生隨機噪音禁止沒有互動前使用js播放視訊,所以後面要監聽點選啟動播放 393 var videoTexture = new BABYLON.VideoTexture("video_"+source.sourceSide+"_"+int_key+"_"+i+"_"+j, [source.sourceUrl], scene, true, false); 394 //videoTexture.video.autoplay=false;//這兩個設定 395 //videoTexture.video.muted=true;不起作用 396 mat.diffuseTexture = videoTexture;//Babylon.js視訊紋理 397 mat.emissiveColor=new BABYLON.Color3(1,1,1); 398 //監聽到互動需求 399 // videoTexture.onUserActionRequestedObservable.add(() => { 400 // scene.onPointerDown = function (evt) { 401 // if(evt.pickInfo.pickedMesh == mesh_plan) 402 // { 403 // if(videoTexture.video.paused) 404 // { 405 // videoTexture.video.play(); 406 // } 407 // else 408 // { 409 // videoTexture.video.pause(); 410 // } 411 // } 412 // 413 // } 414 // }); 415 //mat.emissiveTexture= videoTexture; 416 mesh_plan.material =mat; 417 obj_videos[mesh_plan.name]=videoTexture; 418 if(false) 419 { 420 scene.onPointerDown = function (evt) {//這個evt是dom的,不會有pickInfo!! 421 if(evt.pickInfo&&(evt.pickInfo.pickedMesh == mesh_plan)) 422 { 423 if(videoTexture.video.paused) 424 { 425 videoTexture.video.play(); 426 } 427 else 428 { 429 videoTexture.video.pause(); 430 } 431 } 432 } 433 } 434 435 } 436 } 437 } 438 } 439 440 } 441 } 442 } 443 } 444 } 445 }
drawMesh方法用來在指定位置生成指定源網格的例項:
1 function drawMesh(type,name,pos,rot) 2 { 3 var instance=obj_meshclass[type].createInstance(name); 4 instance.position=pos; 5 instance.rotation=rot; 6 }
在完成零件組裝後,再根據資源資訊向設定的位置新增資源。
4、運動控制與碰撞檢測
監聽操作者的滑鼠和鍵盤操作:
1 var node_temp; 2 function InitMouse() 3 { 4 canvas.addEventListener("blur",function(evt){//監聽失去焦點 5 releaseKeyStateOut(); 6 }) 7 canvas.addEventListener("focus",function(evt){//改為監聽獲得焦點,因為除錯失去焦點時事件的先後順序不好說 8 releaseKeyStateIn(); 9 }) 10 11 //scene.onPointerPick=onMouseClick;//如果不attachControl onPointerPick不會被觸發,並且onPointerPick必須pick到mesh上才會被觸發 12 canvas.addEventListener("click", function(evt) {//這個監聽也會在點選GUI按鈕時觸發!! 13 onMouseClick(evt);// 14 }, false); 15 canvas.addEventListener("dblclick", function(evt) {//是否要用到滑鼠雙擊?? 16 onMouseDblClick(evt);// 17 }, false); 18 scene.onPointerMove=onMouseMove;//Babylon.js的事件監聽屬性 19 scene.onPointerDown=onMouseDown; 20 scene.onPointerUp=onMouseUp; 21 scene.onKeyDown=onKeyDown; 22 scene.onKeyUp=onKeyUp; 23 node_temp=new BABYLON.TransformNode("node_temp",scene);//用來提取相機的姿態矩陣 24 node_temp.rotation=camera0.rotation; 25 }
滑鼠點選控制視訊播放:
1 function onMouseDblClick(evt) 2 { 3 var pickInfo = scene.pick(scene.pointerX, scene.pointerY, null, false, camera0); 4 if(pickInfo.hit) 5 { 6 var mesh = pickInfo.pickedMesh; 7 if(mesh.name.split("_")[0]=="mp4")//重放視訊 8 { 9 if(obj_videos[mesh.name]) 10 { 11 var videoTexture=obj_videos[mesh.name]; 12 13 videoTexture.video.currentTime =0; 14 15 } 16 } 17 } 18 } 19 function onMouseClick(evt) 20 { 21 var pickInfo = scene.pick(scene.pointerX, scene.pointerY, null, false, camera0); 22 if(pickInfo.hit) 23 { 24 var mesh = pickInfo.pickedMesh; 25 if(mesh.name.split("_")[0]=="mp4")//啟停視訊 26 { 27 if(obj_videos[mesh.name]) 28 { 29 var videoTexture=obj_videos[mesh.name]; 30 if(videoTexture.video.paused) 31 { 32 videoTexture.video.play(); 33 } 34 else 35 { 36 videoTexture.video.pause(); 37 } 38 } 39 } 40 } 41 42 }
在fps模式下通過滑鼠移動控制相機視角
1 var lastPointerX,lastPointerY; 2 var flag_view="free"//free表示預設的自由移動狀態,locked表示鎖定滑鼠的fps模式狀態 3 var flag_locked; 4 var obj_keystate=[]; 5 function onMouseMove(evt) 6 { 7 8 if(flag_view=="locked") 9 { 10 evt.preventDefault(); 11 //繞y軸的旋轉角度是根據x座標計算的 12 var rad_y=((scene.pointerX-lastPointerX)/window.innerWidth)*(Math.PI/1);//將滑鼠位置的變化轉化為相機視角的變化 13 var rad_x=((scene.pointerY-lastPointerY)/window.innerHeight)*(Math.PI/1); 14 camera0.rotation.y+=rad_y; 15 camera0.rotation.x+=rad_x; 16 } 17 lastPointerX=scene.pointerX; 18 lastPointerY=scene.pointerY; 19 } 20 function onMouseDown(evt) 21 { 22 if(flag_view=="locked") { 23 evt.preventDefault(); 24 } 25 } 26 function onMouseUp(evt) 27 { 28 if(flag_view=="locked") { 29 evt.preventDefault(); 30 } 31 }
記錄鍵盤按鍵狀態
1 function onKeyDown(event) 2 { 3 if(flag_view=="locked") { 4 event.preventDefault(); 5 var key = event.key; 6 obj_keystate[key] = 1;//1表示按下 7 } 8 } 9 function onKeyUp(event) 10 { 11 var key = event.key; 12 if(key=="v"||key=="Escape")//按v鍵開閉fps模式 13 { 14 event.preventDefault(); 15 if(flag_view=="locked") 16 { 17 flag_view="free"; 18 document.exitPointerLock(); 19 } 20 else if(flag_view=="free") 21 { 22 flag_view="locked"; 23 canvas.requestPointerLock(); 24 } 25 } 26 if(flag_view=="locked") { 27 event.preventDefault(); 28 29 obj_keystate[key] = 0; 30 } 31 }
接下來在渲染迴圈中根據控制輸入確定相機的位移:
var flag_speed=1; //var m_view=camera0.getViewMatrix(); //var m_view=camera0.getProjectionMatrix(); var m_view=node_temp.getWorldMatrix(); //只檢測其執行方向?-》相對論問題!《-先假設直接外圍環境不移動 if(obj_keystate["Shift"]==1)//Shift+w的event.key不是Shift和w,而是W!!!! { flag_speed=5;//加速移動 } var delta=engine.getDeltaTime();//兩渲染幀之間的時間間隔(毫秒) //console.log(delta); flag_speed=flag_speed*engine.getDeltaTime()/10; var v_temp=new BABYLON.Vector3(0,0,0); if(obj_keystate["w"]==1) { v_temp.z+=0.1*flag_speed; } if(obj_keystate["s"]==1) { v_temp.z-=0.1*flag_speed; } if(obj_keystate["d"]==1) { v_temp.x+=0.05*flag_speed; } if(obj_keystate["a"]==1) { v_temp.x-=0.05*flag_speed; } if(obj_keystate[" "]==1) { v_temp.y+=0.05*flag_speed; } if(obj_keystate["c"]==1) { v_temp.y-=0.05*flag_speed; } //camera0.position=camera0.position.add(BABYLON.Vector3.TransformCoordinates(v_temp,camera0.getWorldMatrix()).subtract(camera0.position)); //engine.getDeltaTime() var pos_temp=camera0.position.add(BABYLON.Vector3.TransformCoordinates(v_temp,m_view));
根據按鍵狀態和兩幀之間的時間計算出相機在這一幀內的位移向量,需要注意的是這個位移向量以相機的區域性座標系為參考,為了在世界座標系中使用它,建立了一個node_temp節點專門用來儲存相機的姿態矩陣,對位移向量施加這個矩陣變化將它轉化為世界座標系中的位移矩陣。
接下來使用射線進行簡單的碰撞檢測:
1 var direction=pos_temp.subtract(pos_last);//pos_last是上一幀的相機位置,取新位置向量減舊位置向量的結果為物體的運動方向 2 //var direction=BABYLON.Vector3.TransformCoordinates(v_temp,m_view);//一次性計算的好處是隻需繪製一條射線,缺點是容易射空 3 var ray = new BABYLON.Ray(camera0.position, direction, 1);//從camera0.position位置向direction方向,繪製長度為1的‘射線’ 4 var arr=scene.multiPickWithRay(ray); 5 arr.sort(sort_compare)//按距離從近到遠排序 6 var len=arr.length; 7 8 var flag_hit=false; 9 for(var k=0;k<len;k++)//對於這條射線擊中的每個三角形 10 { 11 var hit=arr[k]; 12 var mesh=hit.pickedMesh; 13 var distance=hit.distance; 14 if(mesh||mesh.name)//暫不限制mesh種類 15 { 16 console.log(mesh.name); 17 flag_hit=true; 18 break; 19 } 20 } 21 if(!flag_hit)//如果沒有被阻攔,則替換位置 22 { 23 camera0.position=pos_temp; 24 } 25 else 26 { 27 camera0.position=pos_last;//回溯的太遠了 28 }
以上渲染迴圈中的運動控制程式碼主要在fps模式下生效,嘗試通過在檢測到碰撞時呼叫camera0.position=pos_last;來阻止自由相機穿牆,但效果並不好。
三、總結:
程式設計結果基本達到設計目標,但在程式碼冗餘、功能細節除錯方面尚有不足,接下來可以考慮向程式中新增模型資源作為‘雕塑’展示、新增更多型別的零件、新增重力效果、新增WebSocket互動等。