WebGL場景的兩種地面構造方法

ljzc002發表於2019-06-29

總述:
大部分3D程式設計都涉及到地面元素,在場景中我們使用地面作為其他物體的承載基礎,同時也用地面限制場景使用者的移動範圍,還可以通過設定地塊的屬性為場景的不同位置設定對應的計算規則。本文在WebGL平臺上藉助Babylon.js庫探索並實現了兩種地面構造方法,除了兩種確定的構造方法外,本文還包含了對一些其他選擇的探討和一些對電子遊戲藝術的看法。建議在閱讀本文前,先學習3D程式設計入門知識和Babylon.js的官方入門教程,前者可以在 https://space.bilibili.com/25346426/channel/detail?cid=14552找到一些介紹基礎概念的視訊教程,後者可以在https://github.com/ljzc002/ljzc002.github.io/tree/master/BABYLON101找到英中對照版本,本篇文章所用到的程式碼可以在https://github.com/ljzc002/ljzc002.github.io/tree/master/EmptyTalk下載。

一、方法一——使用標準地塊拼裝構造地面
1、我們先製作5種標準地塊:

標準地塊都是邊長為1的正方體,每一種標準地塊使用對應的紋理表示特定的地貌,我們可以用這些標準地塊的複製體拼接成複雜的地面構造。在本文中我使用正方體作為標準地塊,垂直排布的生成地形,你也可以使用六稜柱等其他形狀作為標準地塊,或者將地塊繞y軸旋轉一些角度後進行排布。
製作標準地塊的程式碼如下:

 1         var size_per=1;//每個單元格的尺寸
 2         var obj_landtype={};
 3 //建立網格
 4         var box_grass=new BABYLON.MeshBuilder.CreateBox("box_grass",{size:size_per},scene);
 5         var box_tree=new BABYLON.MeshBuilder.CreateBox("box_tree",{size:size_per},scene);
 6         var box_stone=new BABYLON.MeshBuilder.CreateBox("box_stone",{size:size_per},scene);
 7         var box_shallowwater=new BABYLON.MeshBuilder.CreateBox("box_shallowwater",{size:size_per},scene);
 8         var box_deepwater=new BABYLON.MeshBuilder.CreateBox("box_deepwater",{size:size_per},scene);
 9         box_grass.renderingGroupId = 2;
10         box_tree.renderingGroupId = 2;
11         box_stone.renderingGroupId = 2;
12         box_shallowwater.renderingGroupId = 2;
13         box_deepwater.renderingGroupId = 2;
14         box_grass.position.y=-100*size_per;
15         box_tree.position.y=-101*size_per;
16         box_stone.position.y=-102*size_per;
17         box_shallowwater.position.y=-103*size_per;
18         box_deepwater.position.y=-104*size_per;
19         obj_landtype.box_grass=box_grass;
20         obj_landtype.box_tree=box_tree;
21         obj_landtype.box_stone=box_stone;
22         obj_landtype.box_shallowwater=box_shallowwater;
23         obj_landtype.box_deepwater=box_deepwater;
24         OptimizeMesh(box_grass);
25         OptimizeMesh(box_tree);
26         OptimizeMesh(box_stone);
27         OptimizeMesh(box_shallowwater);
28         OptimizeMesh(box_deepwater);
29 //建立材質
30         var mat_grass = new BABYLON.StandardMaterial("mat_grass", scene);//1
31         mat_grass.diffuseTexture = new BABYLON.Texture("../../ASSETS/IMAGE/LANDTYPE/grass.jpg", scene);
32         mat_grass.freeze();
33         box_grass.material=mat_grass;
34         var mat_tree = new BABYLON.StandardMaterial("mat_tree", scene);//1
35         mat_tree.diffuseTexture = new BABYLON.Texture("../../ASSETS/IMAGE/LANDTYPE/yulin.png", scene);
36         mat_tree.freeze();
37         box_tree.material=mat_tree;
38         var mat_stone = new BABYLON.StandardMaterial("mat_stone", scene);//1
39         mat_stone.diffuseTexture = new BABYLON.Texture("../../ASSETS/IMAGE/LANDTYPE/stone.png", scene);
40         mat_stone.freeze();
41         box_stone.material=mat_stone;
42         var mat_shallowwater = new BABYLON.StandardMaterial("mat_shallowwater", scene);//1
43         mat_shallowwater.diffuseTexture = new BABYLON.Texture("../../ASSETS/IMAGE/LANDTYPE/lake.png", scene);
44         mat_shallowwater.freeze();
45         box_shallowwater.material=mat_shallowwater;
46         var mat_deepwater = new BABYLON.StandardMaterial("mat_deepwater", scene);//1
47         mat_deepwater.diffuseTexture = new BABYLON.Texture("../../ASSETS/IMAGE/LANDTYPE/sea.png", scene);
48         mat_deepwater.freeze();
49         box_deepwater.material=mat_deepwater;        

這段程式碼製作了“草地”、“森林”、“岩石”、“淺水”、“深水”五種標準地塊,對於地塊的網格,使用OptimizeMesh方法進行了一些顯示優化,OptimizeMesh方法內容如下:

1 function OptimizeMesh(mesh)
2     {
3         mesh.convertToFlatShadedMesh();//使用頂點顏色計算代替片元顏色計算
4         mesh.freezeWorldMatrix();//凍結世界座標系
5         // mesh.material.needDepthPrePass = true;//啟用深度預通過
6         //mesh.convertToUnIndexedMesh();//使用三角形繪製代替索引繪製
7     }

對於地塊材質,使用freeze方法凍結了材質物件的屬性,避免渲染引擎頻繁重新整理材質狀態。

2、接下來我們用草地地塊拼接一個最簡單的平原地形
地形渲染效果如下:

 

這片草原是由10201個草地地塊拼接而成的,這裡我使用了OpenGL的“多例項渲染”技術,來降低繪製大量重複物件對計算效能的消耗,Babylon.js庫在createInstance方法中封裝了這一技術:

 1     var arr_instance=[];
 2     var segs_x=100;//橫向分段次數
 3     var segs_y=100;//縱向分段次數
 4     
 5     //以高度0為海平面,以xy00為大地原點
 6         //形成初始地塊:101*101個格子,中心格的中心是原點
 7         for(var i=0;i<=segs_x;i++)
 8         {
 9             arr_instance[i]=[];
10             for(var j=0;j<=segs_y;j++)
11             {
12                 arr_instance[i][j]=[];
13                 var instance=obj_landtype.box_grass.createInstance("ground_"+i+"_"+j+"_0");
14                 instance.mydata={i:i,j:j,k:0,landclass:obj_landtype.box_grass};
15                 instance.position=new BABYLON.Vector3((i-(segs_x/2))*size_per,0,(j-(segs_y/2))*size_per);//xz方向上都是從負向正堆疊
16                 arr_instance[i][j].push(instance);//把每個例項用全域性物件儲存起來
17             }
18         }

這裡我們為每個例項物件設定了一個mydata屬性,將地塊的一些資訊儲存到這個屬性裡,以備之後的場景互動使用。

3、為單元格標記xz方向上的索引

現在每個地塊都是沿著x軸和z軸整齊排列的,為方便區分,我們將xz平面上的每個方塊位置叫做“單元格”,每個單元格中可能有多個地塊例項。每個單元格的位置以其x、z軸上的索引表示,我們現在需要一種方式將這一索引值顯示出來。
這裡我們先嚐試為每個單元格顯示一個索引文字,渲染效果如下:(可以訪問https://ljzc002.github.io/EmptyTalk/HTML/TEST/testfloor.html檢視)

可以看出標記效果並不是很理想,同時數以萬計的索引文字也降低了場景的渲染速度,這種方法可能並不適用於當前的單元格標記需求,但包含的技術可能用在其他地方:

a、首先建立一個精靈管理器以及一張包含數字和減號的圖片

 1 //準備十種數字以及減號的紋理
 2     var can_temp=document.createElement("canvas");
 3         can_temp.width=132//264;
 4         can_temp.height=24;
 5         var context=can_temp.getContext("2d");
 6         context.fillStyle="rgba(0,0,0,0)";//完全透明的背景
 7         context.fillRect(0,0,can_temp.width,can_temp.height);
 8         context.fillStyle = "#ffffff";
 9         context.font = "bold 24px monospace";
10         for(var i=0;i<10;i++)
11         {
12             context.fillText(i,i*12,24);
13         }
14         context.fillText("-",120,24);
15         //context.fillText("0123456789-",0,24);//預設為半形,為了在作為精靈使用時整齊的分塊必須一個一個單獨繪製
16         var png=can_temp.toDataURL("image/png");//生成PNG圖片
17         //建立精靈管理器
18         var spriteManager = new BABYLON.SpriteManager("spriteManager", png, (segs_x+1)*(segs_y+1)*7, 24, scene);
19         spriteManager.renderingGroupId=2;
20         spriteManager.cellWidth=12;
21         spriteManager.cellHeight=24;

b、在生成地塊例項的迴圈里加入精靈生成程式碼:

 1 //新增精靈,估計地圖最大為1000*1000
 2                 var number1 = new BABYLON.Sprite("number1", spriteManager);
 3                 var number2 = new BABYLON.Sprite("number2", spriteManager);
 4                 var number3 = new BABYLON.Sprite("number3", spriteManager);
 5                 var number4 = new BABYLON.Sprite("number4", spriteManager);
 6                 var number5 = new BABYLON.Sprite("number5", spriteManager);
 7                 var number6 = new BABYLON.Sprite("number6", spriteManager);
 8                 var number7 = new BABYLON.Sprite("number7", spriteManager);
 9                 //為缺少的數位填充0,生成三位數字
10                 stri=(i+1000+"").substr(1);
11                 strj=(j+1000+"").substr(1);
12 
13                 number1.cellIndex=parseInt(stri[0]);
14                 number2.cellIndex=parseInt(stri[1]);
15                 number3.cellIndex=parseInt(stri[2]);
16                 number4.cellIndex=10;//減號
17                 number5.cellIndex=parseInt(strj[0]);
18                 number6.cellIndex=parseInt(strj[1]);
19                 number7.cellIndex=parseInt(strj[2]);
20                 //定位精靈,7個精靈垂直排列作為一條文字
21                 number1.size=0.2*size_per;
22                 number1.position=instance.position.clone();
23                 number1.position.y=2*size_per;
24                 number1.position.x+=0.3*size_per;
25                 number1.position.z+=0.3*size_per;
26 
27                 number2.size=0.2*size_per;
28                 number2.position=instance.position.clone();
29                 number2.position.y=1.8*size_per;
30                 number2.position.x+=0.3*size_per;
31                 number2.position.z+=0.3*size_per;
32                 number3.size=0.2*size_per;
33                 number3.position=instance.position.clone();
34                 number3.position.y=1.6*size_per;
35                 number3.position.x+=0.3*size_per;
36                 number3.position.z+=0.3*size_per;
37                 number4.size=0.2*size_per;
38                 number4.position=instance.position.clone();
39                 number4.position.y=1.4*size_per;
40                 number4.position.x+=0.3*size_per;
41                 number4.position.z+=0.3*size_per;
42                 number5.size=0.2*size_per;
43                 number5.position=instance.position.clone();
44                 number5.position.y=1.2*size_per;
45                 number5.position.x+=0.3*size_per;
46                 number5.position.z+=0.3*size_per;
47                 number6.size=0.2*size_per;
48                 number6.position=instance.position.clone();
49                 number6.position.y=1.0*size_per;
50                 number6.position.x+=0.3*size_per;
51                 number6.position.z+=0.3*size_per;
52                 number7.size=0.2*size_per;
53                 number7.position=instance.position.clone();
54                 number7.position.y=0.8*size_per;
55                 number7.position.x+=0.3*size_per;
56                 number7.position.z+=0.3*size_per;

考慮到計算效能,這裡使用精靈作為文字的載體(但建立了7萬多個精靈之後,幀率還是降低了很多),因為水平排列的精靈在相機水平移動時會相互遮擋,所以垂直排列精靈來降低影響,也許可以通過重設精靈的旋轉軸位置來徹底解決這一問題。

除了為每個地塊標註索引之外,我們還可以使用帶有索引的小地圖、在單獨的視口中顯示選定地塊的特寫、在選定地塊旁邊生成標記等等方式來標明地塊的索引,後文將使用在場景中放置參考物的方式來標示地塊索引。

4、生成地形起伏
我們抬升了xz平面中左下角的兩格單元格,並將這兩個單元設為“岩石”地貌:

 

a、首先建立兩個工具方法:

 1 //disposeCube(0,0)
 2     function disposeCube(i,j)//移除一個xz位置上的所有可能存在的方塊
 3     {
 4         var len=arr_instance[i][j].length;
 5         for(var k=0;k<len;k++)
 6         {
 7             var instance=arr_instance[i][j][k];
 8             instance.dispose();
 9             instance=null;
10         }
11 
12     }
13     //在指定單元格、指定高度建立指定型別的地塊
14     //createCube(0,0,2,obj_landtype.box_stone)
15     //i,j必定是整數,k可能是小數,都表示單位長度的數量
16     function createCube(i,j,k,landclass)
17     {
18         var instance=landclass.createInstance("ground_"+i+"_"+j+"_"+k);
19         instance.mydata={i:i,j:j,k:k,landclass:landclass};
20         instance.position=new BABYLON.Vector3((i-(segs_x/2))*size_per,k*size_per,(j-(segs_y/2))*size_per);//都是從負向正堆疊?-》規定每個單元格的地塊陣列都是從低到高排列
21         //arr_instance[i][j].push(instance);
22         arr_instance[i][j].unshift(instance);
23     }

b、接下來修改一些被選中的單元格

我們把“被選中的單元格”放在一個陣列裡,將這個陣列命名為“配置陣列”。

 1 //用對應的方塊填充一條路徑上所有的xz單元格,先清空單元格內原有方塊,然後在指定高度建立一個方塊
 2     // ,接著比對所有周圍方塊的高度(比對四個方向),填補漏出的部分,在填補時注意越低的方塊在陣列中越靠前。
 3     //createCubePath([{i:0,j:0,k:1,landclass:obj_landtype.box_stone},{i:1,j:1,k:2.5,landclass:obj_landtype.box_stone}])
 4    
 5     function createCubePath(cubepath)
 6     {
 7         var len=cubepath.length;
 8         for(var i=0;i<len;i++)//對於每一個xz單元格
 9         {
10             var cube=cubepath[i];
11             disposeCube(cube.i,cube.j);
12             createCube(cube.i,cube.j,cube.k,cube.landclass);
13         }
14         //初次繪製後進行二次對比,初次繪製的必定是xz單元格中的最高點
15         for(var index=0;index<len;index++)
16         {
17             var cube=cubepath[index];
18             var i=cube.i;
19             var j=cube.j;
20             var k=cube.k;
21             //上右下左
22             //取四方的最高
23             var k1=999;
24             if(arr_instance[i])
25             {
26                 var arr1=arr_instance[i][j+1];
27                 if(arr1)
28                 {
29                     var ins_cube1=arr1[arr1.length-1];
30                     k1=ins_cube1.mydata.k;
31                 }
32             }
33             var k2=999;
34             if(arr_instance[i+1])
35             {
36                 var arr2=arr_instance[i+1][j];
37                 if(arr2) {
38                     var ins_cube2 = arr2[arr2.length - 1];
39                     k2=ins_cube2.mydata.k;
40                 }
41             }
42             var k3=999;
43             if(arr_instance[i])
44             {
45                 var arr3=arr_instance[i][j-1];
46                 if(arr3) {
47                     var ins_cube3=arr3[arr3.length-1];
48                     k3=ins_cube3.mydata.k;
49                 }
50             }
51             var k4=999;
52             if(arr_instance[i-1])
53             {
54                 var arr4=arr_instance[i-1][j+1];
55                 if(arr4) {
56                     var ins_cube4=arr4[arr4.length-1];
57                     k4=ins_cube4.mydata.k;
58                 }
59             }
60          
61             //在四方最高中找最低
62             var mink=Math.min(k1,k2,k3,k4);
63 
64             var len2=Math.floor((k-mink)/size_per);
65             for(var index2=1;index2<=len2;index2++)
66             {
67                 createCube(i,j,k-index2,cube.landclass);
68                 //arr_instance[i][j].unshift()
69             }
70         }
71     }

這段程式碼包含兩個迴圈,第一個迴圈負責放置選中單元格中最高的那個地塊,第二個迴圈則負責填充最高地塊下面的支撐,比如高山的山體或者深谷的谷壁。這種填充是靠比較選中的單元格和四面單元格的高度實現的,這個演算法的一個缺點是在需要生成低谷時,谷地周圍的一圈高度不變的平地也需要放入配置陣列,否則地形會出現斷裂。
直接在瀏覽器控制檯中執行createCubePath([{i:0,j:0,k:1,landclass:obj_landtype.box_stone},{i:1,j:1,k:2.5,landclass:obj_landtype.box_stone}])命令即可改變地形。這裡你可能想要看到那種“在場景中拖動滑鼠地形隨之起伏”的效果,但我認為這種執行時程式碼注入的控制方式反而是WebGL技術相對於傳統桌面3D程式的一大優勢,藉此我們可能實現遠超傳統ui的精細化控制。

可以訪問https://ljzc002.github.io/EmptyTalk/HTML/TEST/testfloor2.html進行測試

5、小結
綜上我們編寫了一個簡單的地形生成方法,但還有更多的工作沒有做,我們需要一些根據某種規則生成配置陣列的方法、一些根據規則在同一單元格的不同高度分配不同地塊的方法(關於地形規則的制定也許可以參考這篇隨機生成行星表面地形的文章https://www.cnblogs.com/ljzc002/p/9134272.html),對於不習慣控制檯輸入的使用者還要考慮編寫實用的互動ui。這種空間上離散的地形比較適合編寫時間上離散的回合制3D場景,接下來我們將要討論更適合即時3D場景的連續地形。

二、第二種方法——改進的地面網格

1、Babylon.js內建地面網格的不足
Babylon.js內建了一種平面地面網格和一種高度圖地面網格,但這兩鍾網格存在一些不足:
甲:只能設定相同的x向分段數和z向分段數
乙:地面網格里沒有xz索引資訊,只能通過修改底層頂點位置改變地形
丙:無法表現垂直斷崖和反斜面之類變化劇烈的地形

詳細解釋一下問題丙,地面網格的頂點排布如下圖所示:

使用者將發現他無法在HAOG單元格和ABCO單元格之間生成垂直斷崖,因為地面網格使用的是“簡化的網格”,在AO兩處都各只有一個頂點,無法表現懸崖的上下兩邊,這時使用者只好使用盡量小的單元格生成儘量陡的斜坡來模擬懸崖(這一點正好和無法生成斜坡的地塊拼接法完全相反)。另一方面,每個頂點(比如頂點O)周圍的六條楞線夾角並不均勻,難以生成端正的形狀。

計劃用“條帶網格”替換地面網格來解決問題甲和問題乙,但條帶網格的頂點排布規律與地面網格類似,問題丙仍然存在。
經過觀察,問題丙只在地形變化劇烈時表現明顯,所以決定用條帶網格表現較為平緩的地形大勢(程式碼裡命名為ground_base),把一些專門定製的“地形附著物網格”(也就是模型)放置在ground_base之上表現劇烈變化的地形,如果需要,再使用某種方式將ground_base與地形附著物融合在一起。

2、生成類似方法一的平坦草原地形,並標註xz索引
渲染效果如下:(可以訪問https://ljzc002.github.io/EmptyTalk/HTML/TEST/testframe2.html檢視效果)

括號中是當前指示物座標,括號後是xz索引值

a、設定紋理重複
與地塊拼接法不同,條帶網預設用一張紋理圖包覆全體頂點,為了能像方法一一樣一格一格的顯示地塊,對地塊材質程式碼做如下修改:

1 mat_grass = new BABYLON.StandardMaterial("mat_grass", scene);//1
2         mat_grass.diffuseTexture = new BABYLON.Texture("../../ASSETS/IMAGE/LANDTYPE/grass.jpg", scene);
3         mat_grass.diffuseTexture.uScale = segs_x+1;//紋理重複效果
4         mat_grass.diffuseTexture.vScale = segs_z+1;
5         mat_grass.freeze();

b、生成條帶網格地面

 1 var arr_path=[];//路徑陣列
 2         for(var i=0;i<=segs_x+1;i++)
 3         {
 4             var posx=(i-((segs_x+1)/2))*size_per;
 5             var path=[];
 6             for(var j=0;j<=segs_z+1;j++)
 7             {
 8                 var posz=(j-((segs_z+1)/2))*size_per;
 9                 path.push(new BABYLON.Vector3(posx,0,posz));
10             }
11             arr_path.push(path);
12         }
13         ground_base=BABYLON.MeshBuilder.CreateRibbon("ground_base"
14             ,{pathArray:arr_path,updatable:true,closePath:false,closeArray:false,sideOrientation:BABYLON.Mesh.DOUBLESIDE});
15         ground_base.sideOrientation=BABYLON.Mesh.DOUBLESIDE;
16         ground_base.material=mat_grass;
17         ground_base.renderingGroupId=2;
18         ground_base.metadata={};
19         ground_base.metadata.arr_path=arr_path;
20         obj_ground.ground_base=ground_base;

注意需要把CreateRibbon方法引數中的updatable屬性設為true,否則建立條帶網格之後將不能修改地形。

 c、製作一些藍色小球作為地形參照物:

 1 //5個藍色小球
 2 var mesh_sphereup=new BABYLON.MeshBuilder.CreateSphere("mesh_sphereup",{diameter:0.5},scene);
 3         mesh_sphereup.material=mat_blue;
 4         mesh_sphereup.renderingGroupId=2;
 5         mesh_sphereup.direction=new BABYLON.Vector3(0,-1,2);
 6         mesh_sphereup.isPickable=false;
 7         mesh_sphereup.rayHelper = null;
 8         obj_plane.mesh_sphereup=mesh_sphereup;
 9         var mesh_sphereright=new BABYLON.MeshBuilder.CreateSphere("mesh_sphereright",{diameter:0.5},scene);
10         mesh_sphereright.material=mat_blue;
11         mesh_sphereright.renderingGroupId=2;
12         mesh_sphereright.direction=new BABYLON.Vector3(2,-1,0);
13         mesh_sphereright.isPickable=false;
14         mesh_sphereright.rayHelper = null;
15         obj_plane.mesh_sphereright=mesh_sphereright;
16         var mesh_spheredown=new BABYLON.MeshBuilder.CreateSphere("mesh_spheredown",{diameter:0.5},scene);
17         mesh_spheredown.material=mat_blue;
18         mesh_spheredown.renderingGroupId=2;
19         mesh_spheredown.direction=new BABYLON.Vector3(0,-1,-2);
20         mesh_spheredown.isPickable=false;
21         mesh_spheredown.rayHelper = null;
22         obj_plane.mesh_spheredown=mesh_spheredown;
23         var mesh_sphereleft=new BABYLON.MeshBuilder.CreateSphere("mesh_sphereleft",{diameter:0.5},scene);
24         mesh_sphereleft.material=mat_blue;
25         mesh_sphereleft.renderingGroupId=2;
26         mesh_sphereleft.direction=new BABYLON.Vector3(-2,-1,0);
27         mesh_sphereleft.isPickable=false;
28         mesh_sphereleft.rayHelper = null;
29         obj_plane.mesh_sphereleft=mesh_sphereleft;
30         var mesh_spheremiddle=new BABYLON.MeshBuilder.CreateSphere("mesh_spheremiddle",{diameter:0.5},scene);
31         mesh_spheremiddle.material=mat_blue;
32         mesh_spheremiddle.renderingGroupId=2;
33         mesh_spheremiddle.direction=new BABYLON.Vector3(0,-1,0);
34         mesh_spheremiddle.isPickable=false;
35         mesh_spheremiddle.rayHelper = null;
36         obj_plane.mesh_spheremiddle=mesh_spheremiddle;
37 //為每個小球繫結一個gui標籤
38         for(var key in obj_plane)
39         {
40             var label = new BABYLON.GUI.Rectangle(key);
41             label.background = "black";
42             label.height = "30px";
43             label.alpha = 0.5;
44             label.width = "240px";
45             label.cornerRadius = 20;
46             label.thickness = 1;
47             label.linkOffsetY = 30;//位置偏移量??
48             fsUI.addControl(label);
49             label.linkWithMesh(obj_plane[key]);
50             var text1 = new BABYLON.GUI.TextBlock();
51             text1.text = "";
52             text1.color = "white";
53             label.addControl(text1);
54             label.isVisible=true;
55             //label.layerMask=2;
56             label.text=text1;
57             obj_plane[key].lab=label;
58         }

可以訪問https://www.cnblogs.com/ljzc002/p/7699162.html 檢視Babylon.js gui功能的中文文件

d、根據相機的位置修改路標的位置:

 1 scene.registerAfterRender(
 2             function() {
 3                 //更新5個標記球的位置
 4                 var origin=camera0.position;
 5                 var length=200;
 6                 for(key in obj_plane)
 7                 {
 8                     var mesh=obj_plane[key];
 9                     var direction=mesh.direction;
10                     var ray = new BABYLON.Ray(origin, direction, length);
11                     /*if(mesh.rayHelper)
12                     {
13                         mesh.rayHelper.dispose();
14                     }*/
15                     //mesh.rayHelper = new BABYLON.RayHelper(ray);//這時還沒有_renderLine屬性
16                     //mesh.rayHelper._renderLine.renderingGroupId=2;
17                     //mesh.rayHelper.show(scene);//連續使用兩次show會崩潰?
18                     //難道一幀裡只能用一個pick?
19                     //console.log(key);
20                     var hit = scene.pickWithRay(ray,predicate);
21                     if (hit.pickedMesh){
22                         //console.log(key+"2");
23                         mesh.isVisible=true;
24                         var posp=hit.pickedPoint;
25                         mesh.position=posp.clone();
26                         mesh.lab.isVisible=true;
27                         //顯示命中點的座標以及命中點所在方塊的左下角的兩層索引
28                         var index_x=Math.floor((posp.x+(segs_x+1)*size_per/2)/size_per);
29                         var index_z=Math.floor((posp.z+(segs_z+1)*size_per/2)/size_per);
30                         mesh.lab.text.text="("+posp.x.toFixed(2)+","+posp.y.toFixed(2)+","+posp.z.toFixed(2)+")*"
31                             +index_x+"-"+index_z;
32                     }
33                     else
34                     {//如果沒命中地面則不顯示路標
35                         mesh.lab.isVisible=false;
36                         mesh.isVisible=false;
37                     }
38                 }
39 
40 
41 
42             }
43         )
44     function predicate(mesh){//過濾網格,只允許射線擊中地面系網格,
45         if (mesh.name.substr(0,6)=="ground"){
46             return true;
47         }
48         else
49         {
50             return false;
51         }
52 
53     }

這段程式碼在每次渲染後執行,從相機出發向五個方位發射5條射線,將射線和地面的交點做為路標放置點,同時修改gui文字內容。曾經嘗試在這裡使用RayHelper功能顯示射線,但發現在未渲染前RayHelper無法設定渲染組,而渲染後的RayHelper又需立刻換成新物件,舊的渲染組屬性作廢,希望官方能夠優化rayHelper的用法,如果確實需要顯示射線,也許可以用Line功能代替rayHelper。

e、對一些選定的頂點施加矩陣變化:

在控制檯中執行TransVertex(obj_ground.ground_base,[[0,0],[0,1],[1,0]],BABYLON.Matrix.Translation(0,2,0))抬起了左下角的三個頂點:

TransVertex方法如下

 1 function TransVertex(mesh,arr,matrix)
 2     {
 3         var len=arr.length;
 4         var arr_path=mesh.metadata.arr_path;
 5         for(var i=0;i<len;i++)//移動路徑陣列裡的每個頂點
 6         {//注意這裡操縱的是路徑陣列而非底層的頂點資料
 7             arr_path[arr[i][0]][arr[i][1]]=BABYLON.Vector3.TransformCoordinates(arr_path[arr[i][0]][arr[i][1]],matrix);
 8         }
 9         mesh=BABYLON.MeshBuilder.CreateRibbon(mesh.name
10             ,{pathArray:arr_path,updatable:true,instance:mesh,closePath:false,closeArray:false,sideOrientation:BABYLON.Mesh.DOUBLESIDE});
11 
12     }

與方法一的地塊抬升類似,這裡也需要一些生成“配置陣列”和分配變化方式的方法,下面將編寫幾個簡單的此類方法。

2、生成帶有隨機起伏的圓形山丘

渲染效果如圖所示:

程式碼實現:
a、選取一定範圍內的頂點:

 1 //選取區域,將區域條件轉為路徑索引,這裡應該有多種多樣的選取方法
 2     //選取距某個點一定距離的頂點
 3     //FindZoneBYDistance(obj_ground.ground_base,new BABYLON.Vector3(-50,0,-50),45)
 4     function FindZoneBYDistance(mesh,pos,distance)
 5     {
 6         var arr_res=[];
 7         var arr_path=mesh.metadata.arr_path;
 8         var len=arr_path.length;
 9         for(var i=0;i<len;i++)//對於每一條路徑
10         {
11             var path=arr_path[i];
12             var len2=path.length;
13             for(var j=0;j<len2;j++)//對於路徑上的每一個頂點
14             {
15                 var vec=path[j];
16                 var length=pos.clone().subtract(vec).length();//取到這個頂點到引數位置的距離
17                 if(length<=distance)//如果在引數位置的一定範圍內
18                 {
19                     arr_res.push([i,j,length]);
20                 }
21             }
22         }
23         return arr_res;
24     }
25     //只考慮XZ平面上的距離
26     function FindZoneBYDistanceXZ(mesh,pos,distance)
27     {
28         var arr_res=[];
29         var arr_path=mesh.metadata.arr_path;
30         var len=arr_path.length;
31         for(var i=0;i<len;i++)//對於每一條路徑
32         {
33             var path=arr_path[i];
34             var len2=path.length;
35             for(var j=0;j<len2;j++)//對於路徑上的每一個頂點
36             {
37                 var vec=path[j];
38                 var vec2=pos.clone().subtract(vec)
39                 var length=Math.pow(vec2.x*vec2.x+vec2.z*vec2.z,0.5);//取到這個頂點到引數位置的距離
40                 if(length<=(distance))//如果在引數位置的一定範圍內
41                 {
42                     arr_res.push([i,j,length]);
43                 }
44             }
45         }
46         return arr_res;
47     }

b、用梯度隨機法生成起伏不平但又符合大勢的山頭:

 1 //按照一定規則進行矩陣變換:這裡應該有多種多樣的插值方法
 2     //這個是越靠近pos點提高的越多,仿照粒子系統的梯度用法
 3     function TransVertexGradiently(mesh,arr,arr_gradient)
 4     {
 5         var len=arr.length;
 6         var len2=arr_gradient.length;
 7         var arr_path=mesh.metadata.arr_path;
 8         for(var i=0;i<len;i++)//對於每一個要變換的頂點
 9         {
10             var matrix=null;
11             var arr2=arr[i];
12             var vec=arr_path[arr2[0]][arr2[1]];//vec並非基礎量,但為什麼不能直接修改?
13             var dis=arr2[2];
14             if(dis<arr_gradient[0][0])
15             {
16                 dis=arr_gradient[0][0];
17             }
18             else if(dis>arr_gradient[len2-1][0])
19             {
20                 dis=arr_gradient[len2-1][0];
21             }
22             //接下來遍歷梯度陣列,規定梯度必是從低到高排列的
23             for(var j=1;j<len2;j++)
24             {
25                 var gradient=arr_gradient[j];
26                 if(dis<=gradient[0])
27                 {//計算這一梯度插值層級
28                     //前一個梯度
29                     var gradient0=arr_gradient[j-1];
30                     //比率
31                     var ratio=((dis-gradient0[0])/(gradient[0]-gradient0[0]));
32                     //小端
33                     var a=gradient0[1]+(gradient[1]-gradient0[1])*ratio;
34                     //大端
35                     var b=gradient0[2]+(gradient[2]-gradient0[2])*ratio;
36                     //在範圍內取隨機高度
37                     var c=b-a;
38                     var res=a+c*Math.random();
39                     matrix=new BABYLON.Matrix.Translation(0,res,0);
40                     break;
41                 }
42             }
43             if(matrix)
44             {
45                 arr_path[arr2[0]][arr2[1]]=BABYLON.Vector3.TransformCoordinates(arr_path[arr2[0]][arr2[1]],matrix);
46             }
47         }
48         mesh=BABYLON.MeshBuilder.CreateRibbon(mesh.name
49             ,{pathArray:arr_path,updatable:true,instance:mesh,closePath:false,closeArray:false,sideOrientation:BABYLON.Mesh.DOUBLESIDE});
50     }

程式碼中的梯度隨機演算法可以參考Babylon.js入門教程中關於粒子系統的章節。

實現圖中效果所用的命令為:

 1 TransVertexGradiently(obj_ground.ground_base,FindZoneBYDistance(obj_ground.ground_base,new BABYLON.Vector3(-50,0,-50),45)
 2             ,[[0,29,30],[15,14,15],[30,11,12],[45,0,1]]);
 3         TransVertexGradiently(obj_ground.ground_base,FindZoneBYDistance(obj_ground.ground_base,new BABYLON.Vector3(-50,0,50),30)
 4             ,[[0,14,15],[15,4,5],[30,0,1]]);
 5         TransVertexGradiently(obj_ground.ground_base,FindZoneBYDistance(obj_ground.ground_base,new BABYLON.Vector3(50,0,-50),30)
 6             ,[[0,14,15],[15,4,5],[30,0,1]]);
 7         TransVertexGradiently(obj_ground.ground_base,FindZoneBYDistance(obj_ground.ground_base,new BABYLON.Vector3(-50,0,0),30)
 8             ,[[0,14,15],[15,4,5],[30,0,1]]);
 9         TransVertexGradiently(obj_ground.ground_base,FindZoneBYDistance(obj_ground.ground_base,new BABYLON.Vector3(0,0,-50),30)
10             ,[[0,14,15],[15,4,5],[30,0,1]]);

你也可以直接把這些命令寫在程式的地形初始化部分,這會比執行時注入程式碼執行更快。
ground_base處理完畢後,我們開始新增地形附著物。

3、貼合丘陵地形的森林和保持水平的湖泊
森林和湖泊的效果如圖所示:

樹木隨著地勢生長,所以表示森林的地形附著物要具備和山丘相同的地形起伏,水面則平滑如鏡,無論水下情況如何地形附著物都要保持平整。同時這兩種地形附著物還應具有和ground_base同步的紋理重複效果。
程式碼實現:

  1 //關鍵難點在於如何提取和重組地形網格的頂點、索引、uv,這種貼合紋理也可以用在模型表面繪製上
  2     function MakeLandtype1(mesh,arr,mat,name,sameheight,height)
  3     {
  4         //ground_base的頂點資料
  5         var vb=mesh.geometry._vertexBuffers;//地面網格的頂點資料
  6         var data_pos=vb.position._buffer._data;//頂點位置資料
  7         var data_index=mesh.geometry._indices;//網格索引資料
  8         var data_uv=vb.uv._buffer._data;//地面網格的紋理座標資料
  9         var len_index=data_index.length;
 10 
 11         var len=arr.length;
 12         var arr_path=mesh.metadata.arr_path;//路徑陣列
 13 
 14         //要生成的地形附著物的頂點資料
 15         var arr_index=[];
 16         var data_pos2=[];
 17         var data_index2=[];//第二次迴圈時填充
 18         var data_uv2=[];
 19         console.log("開始生成地形附著物");
 20         //生成頂點陣列、紋理座標陣列
 21         for(var i=0;i<len;i++){//對於每一個選中的路徑節點
 22 
 23             var int0=arr[i][0];
 24             var int1=arr[i][1];
 25             var vec=arr_path[int0][int1];//獲取到路徑陣列中的一個Vector3物件
 26             //這裡有兩種思路,一是從頂點資料入手,完全復刻地形的高度;二是從條帶的路徑索引入手,可以更貼近的生成附著物的多邊形輪廓,但在高度方面可能不精確(不貼合),
 27             //->結合使用二者?《-可以實現但過於複雜
 28             //假設路徑陣列和頂點資料是一一對應的?同時假設每一條路徑的長度都和第一條相同,如果先剔除三角形就無法這樣使用了!
 29             var index_v=int0*arr_path[0].length+int1//這個頂點的索引
 30             arr_index.push(index_v);//將ground_base中的每次對應的頂點繪製儲存起來
 31             data_pos2.push(vec.x);
 32             if(sameheight)//如果要求所有頂點等高,則取設定高度
 33             {
 34                 data_pos2.push(height);
 35             }
 36             else
 37             {
 38                 data_pos2.push(vec.y);
 39             }
 40             data_pos2.push(vec.z);
 41             data_uv2.push(data_uv[index_v*2]);
 42             data_uv2.push(data_uv[index_v*2+1]);
 43 
 44         }
 45         //生成附著物的索引陣列
 46         len=arr_index.length;
 47         console.log("開始設定地形附著物的索引");
 48         for(var i=0;i<len;i++)//對於每個頂點索引,它可能被用到多次
 49         {
 50             console.log(i+"/"+len);
 51             var index_v=arr_index[i];
 52             for(var j=0;j<len_index;j+=3)//遍歷ground_base的索引陣列,找到所有被繪製的頂點
 53             {
 54                 var num2=-1;
 55                 var num3=-1;
 56                 //var arr_temp=[];
 57                 var flag_type=null;
 58                     if(index_v==data_index[j])//三角形的第一個頂點
 59                     {//在這裡要考慮另兩個頂點是否在附著物範圍內,如果在,則使用附著物紋理,如果不在則使用混合紋理??
 60                         num2=data_index[j+1];//*3;//實際去頂點陣列中取頂點時要乘以3,但作為頂點索引時不用乘以3
 61                         num3=data_index[j+2];
 62                         flag_type=1;
 63                     }
 64                     else if(index_v==data_index[j+1])//三角形的第二個頂點
 65                     {
 66                         num2=data_index[j];
 67                         num3=data_index[j+2];
 68                         flag_type=2;
 69                     }
 70                     else if(index_v==data_index[j+2])//三角形的第三個頂點
 71                     {
 72                         num2=data_index[j];
 73                         num3=data_index[j+1];
 74                         flag_type=3;
 75                     }
 76                 if(num2!=-1&&num3!=-1)
 77                 {//檢視num2和num3這兩個索引對應的頂點,在不在選定頂點範圍內,如果不在則不在附著物裡繪製這個三角形
 78                     //(其實更好的方案是,如果不在,則繪製地形網格和附著物的混合紋理)
 79                     var flag2=-1;
 80                     var flag3=-1;
 81                     for(var i2=0;i2<len;i2++)
 82                     {
 83                         var index2=arr_index[i2];
 84                         if(index2==num2)
 85                         {
 86                             flag2=i2;//在新的頂點陣列中找到這個頂點的索引
 87                         }
 88                         if(index2==num3)
 89                         {
 90                             flag3=i2;
 91                         }
 92                         if(flag2!=-1&&flag3!=-1)
 93                         {
 94                             break;//都已經找到
 95                         }
 96                     }
 97                     if(flag2!=-1&&flag3!=-1)
 98                     {//如果這個三角形的三個頂點都屬於地形附著物
 99                         if(flag_type==1)
100                         {
101                             data_index2.push(i);
102                             data_index2.push(flag2);
103                             data_index2.push(flag3);
104                         }
105                         else if(flag_type==2)
106                         {
107                             data_index2.push(flag2);
108                             data_index2.push(i);
109                             data_index2.push(flag3);
110                         }
111                         else if(flag_type==3)
112                         {
113                             data_index2.push(flag2);
114                             data_index2.push(flag3);
115                             data_index2.push(i);
116                         }
117 
118                     }
119                 }
120             }
121         }
122         //資料整理完畢,開始生成幾何體
123         var normals=[];
124         BABYLON.VertexData.ComputeNormals(data_pos2, data_index2, normals);//計演算法線
125         BABYLON.VertexData._ComputeSides(0, data_pos2, data_index2, normals, data_uv2);//根據法線分配紋理朝向
126         var vertexData= new BABYLON.VertexData();
127         vertexData.indices = data_index2;//索引
128         vertexData.positions = data_pos2;
129         vertexData.normals = normals;//position改變法線也要改變!!!!
130         vertexData.uvs = data_uv2;
131 
132         var mesh=new BABYLON.Mesh(name,scene);
133         vertexData.applyToMesh(mesh, true);
134         mesh.vertexData=vertexData;
135         mesh.renderingGroupId=2;
136         mesh.material=mat;
137         obj_ground[name]=mesh;
138     }

提取ground_base的地形時有多種可選的演算法,假設ground_base的頂點排布如圖所示:

設ABF右上側包括ABF在內的所有頂點都被選中,則我們可能只提取圖中畫斜線的單元格,也可能提取ACF右上的所有三角形(比前者多了ABC部分),還可能提取AF右上的所有區域(這時需要新增額外的三角形,假設JIHG與CDEF高度不同,則又要考慮不同的額外三角形生成方式),或者提取所有和ABCDEF相鄰的單元格。。。不同的提取方法會產生不同的地形細節效果,這裡我選擇第二種提取方法。

在處理不同紋理交界處時也存在多種不同的選擇,比如基於三角形的實際位置讓紋理相互交錯,或者在交界處使用混合兩種紋理圖的過渡紋理等等,這裡我簡單的保持每種地貌的原本紋理,用後繪製的覆蓋先繪製的,用靠近相機的遮擋遠離相機的。

生成上圖的命令如下:

1 MakeLandtype1(obj_ground.ground_base,FindZoneBYDistanceXZ(obj_ground.ground_base,new BABYLON.Vector3(-50,0,10),30)
2             ,mat_tree,"ground_tree1");
3         MakeLandtype1(obj_ground.ground_base,FindZoneBYDistanceXZ(obj_ground.ground_base,new BABYLON.Vector3(0,0,0),35)
4             ,mat_shallowwater,"ground_shallowwater1",true,0);

 

4、向地形中匯入預製的模型生成劇烈變化的地形
向場景中加入了一個“墜毀的宇宙飛船”模型:

從下面看:(左邊只顯示森林紋理並不是因為草地三角形不存在,而是因為網格位置和渲染組都相同時,後繪製的三角形會覆蓋先繪製的三角形)

使用單純的地面網格是難以生成圖中的反斜面地形的。
匯入Babylon格式模型的程式碼如下:

 1 //如果這裡load一個相同內容的txt檔案,會報警告,但似乎也成功匯入了!!
 2     function ImportMesh(objname,filepath,filename,obj_p)
 3     {
 4         BABYLON.SceneLoader.ImportMesh(objname, filepath, filename, scene
 5             , function (newMeshes, particleSystems, skeletons)
 6             {//載入完成的回撥函式
 7                 var mesh=newMeshes[0];
 8                 mesh.position=obj_p.position;
 9                 mesh.rotation=obj_p.rotation;
10                 mesh.scaling=obj_p.scaling;
11                 mesh.name=obj_p.name;
12                 mesh.id=obj_p.name;
13                 var mat=obj_p.material.clone();
14                 mat.backFaceCulling=false;
15                 mat.name=obj_p.material.name;
16                 mesh.material=mat;
17                 mesh.renderingGroupId=2;
18                 mesh.sideOrientation=BABYLON.Mesh.DOUBLESIDE;
19                 obj_ground[obj_p.name]=mesh;
20             }
21         );
22     }

呼叫命令如下:

1 ImportMesh("","../../ASSETS/SCENE/","SpaceCraft.babylon"
2             ,{position:new BABYLON.Vector3(10,-2,10),rotation:new BABYLON.Vector3(0,-Math.PI/4,Math.PI/6)
3                 ,scaling:new BABYLON.Vector3(1,1,1),name:"ground_spacecraft"
4                 ,material:mat_stone});

5、儲存地面編輯進度
你可能想在做完一些操作後將當前的場景儲存起來以備再次載入。

a、存檔程式碼如下:

 1 //匯出正在編輯的地面工程,其中地面網格保持metadata屬性,下載文字時參考xlsx的方式
 2     function ExportObjGround()
 3     {
 4         var obj_scene=MakeBasicBabylon();//建立一個基礎場景所需的全部屬性
 5         for(key in obj_ground)//在Babylon檔案中不配置材質,在匯入後能否自動對應新場景中的材質id?-》可以,但是會報警告
 6         {
 7             var obj_mesh={};
 8             var mesh=obj_ground[key];
 9             obj_mesh.name=mesh.name;
10             obj_mesh.id=mesh.id;
11             obj_mesh.materialId=mesh.material.name;
12             obj_mesh.position=[mesh.position.x,mesh.position.y,mesh.position.z];
13             obj_mesh.rotation=[mesh.rotation.x,mesh.rotation.y,mesh.rotation.z];
14             obj_mesh.scaling=[mesh.scaling.x,mesh.scaling.y,mesh.scaling.z];
15             obj_mesh.isVisible=true;
16             obj_mesh.isEnabled=true;
17             obj_mesh.checkCollisions=false;
18             obj_mesh.billboardMode=0;
19             obj_mesh.receiveShadows=true;
20             obj_mesh.renderingGroupId=mesh.renderingGroupId;
21             obj_mesh.metadata=mesh.metadata;
22             obj_mesh.sideOrientation=mesh.sideOrientation;
23             if(mesh.geometry)//是有實體的網格
24             {
25                 var vb=mesh.geometry._vertexBuffers;
26                 obj_mesh.positions=BuffertoArray2(vb.position._buffer._data);
27                 obj_mesh.normals=BuffertoArray2(vb.normal._buffer._data);
28                 obj_mesh.uvs= BuffertoArray2(vb.uv._buffer._data);
29                 obj_mesh.indices=BuffertoArray2(mesh.geometry._indices);
30                 obj_mesh.subMeshes=[{
31                     'materialIndex': 0,
32                     'verticesStart': 0,
33                     'verticesCount': mesh.geometry._totalVertices,
34                     'indexStart': 0,
35                     'indexCount': mesh.geometry._indices.length,
36                 }];
37                 obj_mesh.parentId=mesh.parent?mesh.parent.id:null;
38             }
39             else//非實體網格
40             {
41                 obj_mesh.positions=[];
42                 obj_mesh.normals=[];
43                 obj_mesh.uvs=[];
44                 obj_mesh.indices=[];
45                 obj_mesh.subMeshes=[{
46                     'materialIndex': 0,
47                     'verticesStart': 0,
48                     'verticesCount': 0,
49                     'indexStart': 0,
50                     'indexCount': 0
51                 }];
52                 obj_mesh.parentId=null;
53             }
54             obj_scene.meshes.push(obj_mesh);
55         }
56         var str_data=JSON.stringify(obj_scene);
57         //試試看行不行-》行
58         var tmpDown = new Blob([s2ab(str_data)]
59             ,{
60                 type: ""
61             }
62         );
63         saveAs(tmpDown,"ObjGround.babylon")
64     }

其中MakeBasicBabylon方法儲存了一個最基礎的場景物件所需的資料:

 1 //建立一個最基礎的Babylon物件結構
 2     function MakeBasicBabylon()
 3     {
 4         var obj_scene=
 5             {//最簡場景物件
 6                 'autoClear': true,
 7                 'clearColor': [0,0,0],
 8                 'ambientColor': [0,0,0],
 9                 'gravity': [0,-9.81,0],
10                 'cameras':[],
11                 'activeCamera': null,
12                 'lights':[],
13                 'materials':[],
14                 'geometries': {},
15                 'meshes': [],
16                 'multiMaterials': [],
17                 'shadowGenerators': [],
18                 'skeletons': [],
19                 'sounds': []//,
20                 //'metadata':{'walkabilityMatrix':[]}
21             };
22         return obj_scene;
23     }

BuffertoArray2是一個將buffer型資料轉為陣列的方法:

 1 function BuffertoArray2(arr)
 2     {
 3         var arr2=[];
 4         var len=arr.length;
 5         for(var i=0;i<len;i++)
 6         {
 7             arr2.push(arr[i]);
 8         }
 9         return arr2;
10     }

儲存Babylon模型檔案時使用了 https://www.jianshu.com/p/9a465d7d1448部落格介紹的檔案匯出方法:

 1 function s2ab(s) {
 2         if (typeof ArrayBuffer !== 'undefined') {
 3             var buf = new ArrayBuffer(s.length);
 4             var view = new Uint8Array(buf);
 5             for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
 6             return buf;
 7         } else {
 8             var buf = new Array(s.length);
 9             for (var i = 0; i != s.length; ++i) buf[i] = s.charCodeAt(i) & 0xFF;
10             return buf;
11         }
12     }
13     saveAs=function(obj, fileName)
14     {
15         var tmpa = document.createElement("a");
16         tmpa.download = fileName || "下載";
17         tmpa.href = URL.createObjectURL(obj);
18         tmpa.click();
19         setTimeout(function () {
20             URL.revokeObjectURL(obj);
21         }, 100);
22     };

 

6、小結:

以上內容可以訪問https://ljzc002.github.io/EmptyTalk/HTML/TEST/testframe3.html進行測試

在之前研究基於程式設計的模型編輯器時( https://www.cnblogs.com/ljzc002/p/9353101.html,https://www.cnblogs.com/ljzc002/p/9473438.html  ),如何在自定義的模型上繪製紋理一直是一個難題,曾經在更早期的編輯器Demo(https://www.cnblogs.com/ljzc002/p/6884252.html)中嘗試過用選擇三角形設定圖素,之後按照圖素生成圖片的方式繪製紋理圖,但也只能作為原理解釋Demo而難以實用。這裡編寫的貼合表面的附著物演算法在稍加修改後將可以解決在一個模型上繪製多種紋理的難題。當然,這種“紋理附著物”的方法會在一個模型中建立多個網格,並且在網格相互覆蓋的地方產生一些多餘的三角形,在後文中我將嘗試用“降雨法”剔除這些多餘的三角形。(事實上剔除多餘三角形後渲染速度並沒有提升很多。。。)

三、

使用降雨法剔除被遮擋的三角形
1、首先匯入我們之前儲存的存檔,執行以下命令匯入之前做好的一個存檔:

ImportObjGround("../../ASSETS/SCENE/","ObjGround.babylon",webGLStart3);

ImportObjGround方法程式碼如下:

 1 function ImportObjGround(filepath,filename,func)
 2     {
 3         BABYLON.SceneLoader.ImportMesh("", filepath, filename, scene
 4             , function (newMeshes, particleSystems, skeletons)
 5             {//載入完成的回撥函式
 6                 var len=newMeshes.length;
 7                 for(var i=0;i<len;i++)
 8                 {
 9                     var mesh=newMeshes[i];
10                     mesh.renderingGroupId=2;
11                     mesh.sideOrientation=BABYLON.Mesh.DOUBLESIDE;
12                     obj_ground[mesh.name]=mesh;
13                     if(mesh.name=="ground_base")
14                     {//宣告頂點位置是可變的!!
15                         mesh.markVerticesDataAsUpdatable(BABYLON.VertexBuffer.PositionKind//其實就是“position”,除此之外還有“normal”等
16                             ,true);
17                     }
18                     if(mesh.metadata&&mesh.metadata.arr_path)
19                     {//要把array重新變成Vector3!!!!
20                         var arr_path=mesh.metadata.arr_path;
21                         var len1=arr_path.length;
22                         for(var j=0;j<len1;j++)
23                         {
24                             var path=arr_path[j];
25                             var len2=path.length;
26                             for(var k=0;k<len2;k++)
27                             {
28                                 var vec=path[k];
29                                 var vec2=new BABYLON.Vector3(vec.x,vec.y,vec.z);
30                                 path[k]=vec2;
31                             }
32                         }
33                     }
34                 }
35                 func();//匯入完成後執行
36             }
37         );
38     }

這裡要注意的是,匯入模型後網格的updatable屬性會自動變為false,這時需要使用markVerticesDataAsUpdatable方法重新啟用網格的更新能力。同時Babylon.js的Vector3物件在轉為JSON字串時,會自動的退化為JavaScript的Object物件,我們在載入時需要重新將它轉為Vector3。

2、準備下雨

降雨演算法並不複雜,思路是從相機可能的觀察方向發出密集的射線,如果射線擊中某一三角形,則把三角形的三個頂點“淋溼”(所有淋溼的頂點組成“淋溼陣列”),在降雨完成後,剔除掉沒有淋溼的頂點及對應頂點索引,使模型得到簡化。
與傳統的網格融合方法相比,降雨法不會在網格的介面處生成大量不受控的三角形;與傳統網格簡化方法相比,則不會丟棄可見的三角形細節;缺點則是計算耗時較長(也許可以優化?)
首先準備下雨:

 1 function PrepareRain()
 2     {
 3         console.log("準備下雨");
 4         mesh_DropFrom=new BABYLON.Mesh("mesh_DropFrom",scene);
 5         for(var key in obj_ground)
 6         {
 7             var mesh=obj_ground[key];
 8             var obj={};
 9             obj.vb=mesh.geometry._vertexBuffers;//地面網格的頂點資料
10             obj.data_pos=obj.vb.position._buffer._data;//頂點位置資料
11             obj.data_index=mesh.geometry._indices;//網格索引資料
12             obj.data_uv=obj.vb.uv._buffer._data;//地面網格的紋理座標資料
13             obj.len_index=obj.data_index.length;
14             obj.len_pos=obj.data_pos.length/3;
15             obj.data_wet=[];//每個頂點是否被淋溼
16             for(var i=0;i<obj.len_pos;i+=1)
17             {
18                 obj.data_wet.push(0);
19             }
20             obj.arr_index=[];
21             obj.data_pos2=[];
22             obj.data_index2=[];//第二次迴圈時填充
23             obj.data_uv2=[];
24             obj_wet[key]=obj;
25         }
26         console.log("準備完畢");
27     }
28     function PrepareRain2()
29     {
30         console.log("讀取本地淋溼陣列");
31         mesh_DropFrom=new BABYLON.Mesh("mesh_DropFrom",scene);
32         for(var key in obj_ground)
33         {
34             var mesh=obj_ground[key];
35             var obj={};
36             obj.vb=mesh.geometry._vertexBuffers;//地面網格的頂點資料
37             obj.data_pos=obj.vb.position._buffer._data;//頂點位置資料
38             obj.data_index=mesh.geometry._indices;//網格索引資料
39             obj.data_uv=obj.vb.uv._buffer._data;//地面網格的紋理座標資料
40             obj.len_index=obj.data_index.length;
41             obj.len_pos=obj.data_pos.length/3;
42             obj.data_wet=localStorage.getItem(key);//每個頂點是否被淋溼
43             obj.arr_index=[];
44             obj_wet[key]=obj;
45         }
46         console.log("準備完畢");
47     }

第一個方法提取出了降雨所需的地面物件中的每個網格的資料,其中obj.data_wet是一個長度與頂點陣列相同的的全零陣列,0表示沒有淋溼。第二個方法,則從瀏覽器的本地儲存中讀取已經算好的淋溼陣列,用預先算好的資料節省下雨所需的時間。mesh_DropFrom網格則是後面降雨所用到的參照物網格。

3、開始下雨(這時一個非常耗時的計算)

 1 //寬度分段、深度分段、每塊尺寸(在這個尺寸內有四條射線),“射線”長度,所有射線出發點的中心
 2     //DropRain(100,100,1,100,new BABYLON.Vector3(0,50,0),new BABYLON.Vector3(0,0,0))
 3     //DropRain(200,200,0.5,100,new BABYLON.Vector3(0,50,0),new BABYLON.Vector3(0,0,0))
 4     function DropRain(count_x,count_z,size,length,from,to)
 5     {
 6         mesh_DropFrom.position=from;
 7         mesh_DropFrom.lookAt(to);//這時網格的WorldMatrix和AbsoulutPosition還未改變!!
 8         //其實應該是網格的負Y方向指向to!!!!這個矩陣的最終效果應該是x,y,z左移一位
 9         mesh_DropFrom.computeWorldMatrix();
10         var matrix=mesh_DropFrom.getWorldMatrix();//取參考網格的世界矩陣
11         var size41=size/4;
12         var direction=to.subtract(from);//雨絲在世界座標系中的方向
13         //遍歷101*101個方塊,降雨角度不同時設計不同的分段數
14         console.log("開始下雨");
15         for(var i=0;i<=count_x;i++)
16         {
17             for(var j=0;j<=count_z;j++)
18             {
19                 console.log(i+"/"+count_x+"_"+j+"/"+count_z);
20                 var arr_wet=[];
21                 var pos0=new BABYLON.Vector3((j-(count_z/2))*size,(i-(count_x/2))*size,0);//預先右移一位?
22                 //左上,右上,右下,左下
23                 //建立四條射線,區域性座標系中的變換
24                 var pos1=BABYLON.Vector3.TransformCoordinates(pos0.clone().add(new BABYLON.Vector3(size41,-size41,0)),matrix);
25                 var pos2=BABYLON.Vector3.TransformCoordinates(pos0.clone().add(new BABYLON.Vector3(size41,size41,0)),matrix);
26                 var pos3=BABYLON.Vector3.TransformCoordinates(pos0.clone().add(new BABYLON.Vector3(-size41,size41,0)),matrix);
27                 var pos4=BABYLON.Vector3.TransformCoordinates(pos0.clone().add(new BABYLON.Vector3(-size41,-size41,0)),matrix);
28                 //var ray=new BABYLON.Ray(new BABYLON.Vector3(-50,50,0), new BABYLON.Vector3(0,-1,0), 100);
29                 var ray1 = new BABYLON.Ray(pos1, direction, length);
30                 var ray2 = new BABYLON.Ray(pos2, direction, length);
31                 var ray3 = new BABYLON.Ray(pos3, direction, length);
32                 var ray4 = new BABYLON.Ray(pos4, direction, length);
33                 //對於每一束射線,如果擊中的第一個網格不是"ground_alpha",則只淋溼第一個網格
34                 // ,否則還如此檢查第二個,四條射線的檢查結果都放在同一陣列中
35                 //,擊中了處於相同位置的ground_base和其他ground,優先選拔其他ground
36                 testRay(ray1,size);//檢查射線淋溼的網格
37                 testRay(ray2,size);
38                 testRay(ray3,size);
39                 testRay(ray4,size);
40             }
41         }
42         //為了節省時間,把淋溼陣列儲存在本地儲存裡
43         for(key in obj_wet)
44         {
45             localStorage.setItem(key,obj_wet[key].data_wet);
46         }
47         console.log("降雨結束");
48     }

考慮到可能斜著下雨,我們使用矩陣變換調整每一條雨絲的起點和方向,在mesh_DropFrom的區域性座標系中,每條雨絲的降雨起點都像地塊單元格一樣平整的排列,用lookAt方法將mesh_DropFrom傾斜一些則所有雨絲也跟著傾斜了。這裡要注意的是lookAt方法預設將mesh_DropFrom的“正面”指向目標(to),而我們需要的則是將mesh_DropFrom的“下面”指向目標,這一差異會使得最終世界座標系中的的座標左移一位,也就是(x,y,z)變成了(y,z,x),所以我們在mesh_DropFrom的區域性座標系內計算時就預先將座標右移一位。

降雨的示意圖:

從每個mesh_DropFrom的“單元格”中射出四條射線,使用testRay方法判斷這些射線與地面網格的接觸情況:

 1 function sort_compare(a,b)
 2     {
 3         return a.distance-b.distance;
 4     }
 5     function testRay(ray,size)
 6     {
 7         var arr=scene.multiPickWithRay(ray,predicate);//射線的多重選取,這樣獲取的arr並不是按distance順序排序的!!!!
 8         var len=arr.length;
 9         arr.sort(sort_compare)//按距離從近到遠排序
10         var lastHit=null;
11         for(var k=0;k<len;k++)//對於這條射線擊中的每個三角形
12         {
13             var hit=arr[k];
14             var mesh=hit.pickedMesh;
15             var distance=hit.distance;
16             if(mesh)
17             {
18                 if(lastHit)//已經有上一層
19                 {//如果上一層是半透明的,則下一層必定被淋溼,如果上一層是ground_base,則要看兩層之間的距離
20                     if(lastHit.pickedMesh.name.substr(0,11)=="ground_base")
21                     {
22                         if((distance-lastHit.distance)>(size/1000))//如果距離太大,則不會淋溼
23                         {
24                             getWet(lastHit);
25                         }
26                         else//如果距離較近,則優先淋溼地形附著物
27                         {
28                             getWet(hit);
29                         }
30                     }
31                 }
32                 else//沒有上一層
33                 {
34                     if(mesh.name.substr(0,11)!="ground_base")//如果是地面網格,則還不確定是否淋溼,其他網格必定淋溼
35                     {
36                         getWet(hit);
37                     }
38                     else if(k==(len-1))//已經遍歷到最後一層
39                     {
40                         getWet(hit);
41                     }
42                 }
43                 var name=mesh.name;
44                 if(name&&(name.substr(0,12)=="ground_alpha"||name.substr(0,11)=="ground_base"))
45                 {
46                     lastHit=hit;
47                 }
48                 else
49                 {
50                     lastHit=null;
51                     break;//如果上一層就是其他型別的網格,則不再繼續深入檢測
52                 }
53             }
54             else
55             {
56                 lastHit=null;
57                 break;
58             }
59         }
60     }

這裡將可能出現的地形網格分為三類:半透明地形網格(以ground_alpha為字首),基礎地形網格(以ground_base為字首),地形附著物網格(以ground為字首),規定半透明網格不會阻擋雨水,附著物網格和基礎網格距離較近時附著物網格優先淋溼。
確定會淋溼後,用getWet方法將三角形的頂點放入淋溼陣列:

 1 function getWet(hit)
 2     {
 3         var mesh=hit.pickedMesh;
 4         var name=mesh.name;
 5         var faceId=hit.faceId;
 6         var indices = mesh.getIndices();
 7         var index0 = indices[faceId * 3];
 8         var index1 = indices[faceId * 3 + 1];
 9         var index2 = indices[faceId * 3 + 2];
10         var wet=obj_wet[name];//這個頂點被淋溼
11         wet.data_wet[index0]=1;
12         wet.data_wet[index1]=1;
13         wet.data_wet[index2]=1;
14     }

4、根據淋溼陣列剔除三角形:(這是一個更加耗時的計算)

 1 function SpliceRain(obj_ground)//通過改變資料結構,可以只測試其中的一個網格
 2     {
 3         for(var key in obj_ground)
 4         {
 5             console.log("清理"+key);
 6             var obj=obj_wet[key];
 7             var len=obj.len_pos;
 8             var data_wet=obj.data_wet;//淋溼陣列,長度是頂點陣列的三分之一
 9             var data_pos=obj.data_pos;//頂點陣列
10             var data_index=obj.data_index//頂點索引
11             var data_uv=obj.data_uv//紋理座標
12             //var count_splice=0;
13             for(var i=0;i<data_wet.length;i++)//對於每一個頂點,這裡一定要注意順序
14             {//如果這個頂點沒有被淋溼,則要清除這個頂點,如果不清除頂點只是清除索引,能不能快一些?
15                 console.log(i+"/"+data_wet.length);
16                 if(!data_wet[i])//如果沒有淋溼
17                 {
18                     data_pos.splice(i*3,3);
19                     data_uv.splice(i*2,2);
20                     data_wet.splice(i,1);
21 
22                     //count_splice++;
23                     var len2=obj.len_index;
24                     for(var j=0;j<obj.len_index;j++)
25                     {
26                         if(data_index[j]>i)//如果這個索引值大於被剔除的頂點
27                         {
28                             data_index[j]-=1;//count_splice;
29                         }
30                         else if(data_index[j]==i)//如果這個索引正是被剔除的頂點
31                         {
32                             var int_temp=j%3;
33                             if(int_temp==0)//三角形的第一個頂點
34                             {
35                                 data_index.splice(j,3);
36                                 j-=1;
37                             }
38                             else if(int_temp==1)//三角形的第二個頂點
39                             {
40                                 data_index.splice(j-1,3);
41                                 j-=2;
42                             }
43                             else if(int_temp==2)//三角形的第三個頂點
44                             {
45                                 data_index.splice(j-2,3);
46                                 j-=3;
47                             }
48                         }
49                     }
50                     i--;
51                 }
52             }
53             //剔除之後開始生成網格
54             var normals=[];
55             BABYLON.VertexData.ComputeNormals(data_pos, data_index, normals);//計演算法線
56             BABYLON.VertexData._ComputeSides(0, data_pos, data_index, normals, data_uv);//根據法線分配紋理朝向
57             var vertexData= new BABYLON.VertexData();
58             vertexData.indices = data_index;//索引
59             vertexData.positions = data_pos;
60             vertexData.normals = normals;//position改變法線也要改變!!!!
61             vertexData.uvs = data_uv;
62 
63             var mesh=obj_ground[key];
64             var mat=mesh.material;
65             var pos=mesh.position;
66             var rot=mesh.rotation;
67             var scal=mesh.scaling;
68             mesh.dispose();
69             mesh=new BABYLON.Mesh(key,scene);
70             //mesh
71             //mesh=new BABYLON.Mesh(name,scene);
72             vertexData.applyToMesh(mesh, true);
73             mesh.vertexData=vertexData;
74             mesh.sideOrientation=BABYLON.Mesh.DOUBLESIDE;
75             mesh.renderingGroupId=2;
76             mesh.material=mat;
77             mesh.position=pos;
78             mesh.rotation=rot;
79             mesh.scaling=scal;
80             obj_ground[key]=mesh;
81         }
82     }

這裡需要從頂點陣列和頂點索引陣列中剔除未淋溼的頂點,同時要根據頂點數目的變化減小頂點索引陣列中所有超過剔除頂點的值。其他的計算和生成地形附著物相似。
剔除之後的渲染效果如圖所示:

較稀疏的雨絲:(DropRain(100,100,1,100,new BABYLON.Vector3(0,50,0),new BABYLON.Vector3(0,0,0)))

可以看到,沒有被淋溼的淺水三角形被剔除了,另一方面因為飛船的網格比雨絲更密集,很多網格沒有被淋溼。

較密集的雨絲:(DropRain(200,200,0.5,100,new BABYLON.Vector3(0,50,0),new BABYLON.Vector3(0,0,0)))

可以看到模型中還存在一些空洞,再新增一些其他方向的小規模降雨即可解決。
除了這種類似“方向光”的降雨方法外,還可以根據實際需要編寫其他的降雨方式,比如參考“點光源”從一個點向周圍發出射線,也可以使用一些邊界判斷方法直接剔除一定範圍內的所有頂點(邊界判斷方法可以參考 https://www.cnblogs.com/ljzc002/p/10168547.html)

總結:

地基已經搭好,接下來可以向場景中新增各種角色並進行互動了。

相關文章