1. 程式人生 > 實用技巧 >3D網頁小實驗——將txt配置文字轉化為3D陳列室

3D網頁小實驗——將txt配置文字轉化為3D陳列室

設計目標:借鑑前輩程式設計者的經驗將簡單的配置文字轉化為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互動等。