1. 程式人生 > >基於Babylon.js編寫宇宙飛船模擬程式1——程式基礎結構、物理引擎使用、三維羅盤

基於Babylon.js編寫宇宙飛船模擬程式1——程式基礎結構、物理引擎使用、三維羅盤

計劃做一個宇宙飛船模擬程式,首先做一些技術準備。

可以訪問https://ljzc002.github.io/test/Spacetest/HTML/PAGE/spacetestwp2.html檢視測試場景,按住qe鍵可以左右傾斜相機。可以在https://github.com/ljzc002/ljzc002.github.io/tree/master/test/Spacetest檢視程式程式碼,因時間有限,github上的程式碼可能和本文中的程式碼有少許出入。

主要內容:

一、程式基礎結構

二、場景初始化

三、地形初始化

四、事件初始化

五、UI初始化

六、單位初始化

七、主迴圈初始化

八、總結

 

一、程式基礎結構:

入口檔案spacetestwp2.html程式碼如下:

 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>三種物理引擎的加速度效果對比測試</title>
 6     <link href="../../CSS/newland.css" rel="stylesheet">
 7     <script src="../../JS/LIB/babylon.max.js"></script><!--Babylon.js主庫,這裡包含了babylon格式的模型匯入,但不包含gltf等其他格式模型的匯入,包含了後期處理庫-->
 8     <script src="../../JS/LIB/babylon.gui.min.js"></script><!--gui庫-->
 9     <script src="../../JS/LIB/babylonjs.loaders.min.js"></script><!--模型匯入庫合集-->
10     <script src="../../JS/LIB/babylonjs.materials.min.js"></script><!--材質庫合集,包括基於shader的水流、火焰等高階預設材質-->
11     <script src="../../JS/LIB/earcut.min.js"></script><!--用來在平面網格上“挖洞”的庫-->
12     <script src="../../JS/LIB/babylonjs.proceduralTextures.min.js"></script><!--程式紋理庫合集-->
13     <script src="../../JS/LIB/oimo.min.js"></script><!--oimo物理引擎庫-->
14     <script src="../../JS/LIB/ammo.js"></script><!--ammo物理引擎庫-->
15     <script src="../../JS/LIB/cannon.js"></script><!--cannon物理引擎庫-->
16     <!--script src="../../JS/LIB/dat.gui.min.js"></script--><!--官方比較喜歡用的一套html ui庫,和WebGL3D無關-->
17     <script src="../../JS/MYLIB/newland.js"></script><!--自己編寫的輔助工具庫-->
18     <script src="../../JS/MYLIB/CREATE_XHR.js"></script><!--自己編寫的AJAX庫-->
19 
20 </head>
21 <body>
22 <div id="div_allbase">
23     <canvas id="renderCanvas"></canvas>
24     <div id="fps" style="z-index: 302;"></div>
25 </div>
26 </body>
27 <script>
28     var VERSION=1.0,AUTHOR="[email protected]";
29     var machine/*裝置資訊*/,canvas/*html5畫布標籤*/,engine/*Babylon.js引擎*/,scene/*Babylon場景*/,gl/*底層WebGL物件*/,MyGame/*用來儲存各種變數*/;
30     canvas = document.getElementById("renderCanvas");
31     engine = new BABYLON.Engine(canvas, true);
32     engine.displayLoadingUI();
33     gl=engine._gl;
34     scene = new BABYLON.Scene(engine);
35     var divFps = document.getElementById("fps");/*用來顯示每秒幀數的標籤*/
36 
37     window.onload=beforewebGL;
38     function beforewebGL()
39     {
40         MyGame=new Game(0,"first_pick","","","","");
41         initWs(webGLStart,"no");//離線測試,不使用WebSocket
42         //webGLStart();
43     }
44     function webGLStart()
45     {//是否有必要嚴格控制初始化流程的同步性?
46         initScene();//初始化基礎場景,包括光照、相機物件
47         initArena();//初始化地形,要包括出生點、可放置區域(6*9)
48         initEvent();//初始化事件
49         initUI();//初始化場景UI
50         initObj();//初始化一開始存在的可互動的物體
51         initLoop();//初始化渲染迴圈
52         initAI();//初始化AI計算任務
53         MyGame.init_state=1;
54         engine.hideLoadingUI();
55     }
56 </script>
57 <script src="../../JS/PAGE/SpaceTest/WsHandler.js"></script>
58 <script src="../../JS/PAGE/SpaceTest/SpaceTest2.js"></script>
59 <script src="../../JS/MYLIB/Game.js"></script>
60 <script src="../../JS/PAGE/SpaceTest/Control.js"></script>
61 <script src="../../JS/PAGE/SpaceTest/FullUI.js"></script>
62 <script src="../../JS/PAGE/SpaceTest/Campass.js"></script>
63 <script src="../../JS/PAGE/CHARACTER/Rocket2.js"></script>
64 </html>

1、Babylon.js庫下載

在4.0正式版之前,Babylon.js官方提供了一款帶有圖形介面的打包工具,可以根據使用者需求方便的將各種庫打包為一個js檔案,但官方網站改版後這個打包工具已經不再可用。可以在這裡找到使用這一打包工具生成的最後一個版本:

https://github.com/ljzc002/ljzc002.github.io/tree/master/EmptyTalk/JS/LIB,這個紀念版本基於4.0測試版打包,包含了除物理引擎以外的絕大部分功能。

回到現在,Babylon.js官方推薦使用cdn或npm使用程式包,方法見:https://github.com/BabylonJS/Babylon.js。但我個人更喜歡明確的呼叫本地檔案,所以我在這裡整理了一套較新的Babylon.js程式包:https://github.com/ljzc002/ljzc002.github.io/tree/master/test/Spacetest/JS/LIB,你也可以自己在https://github.com/BabylonJS/Babylon.js/tree/master/dist裡挑選最新版本的程式包下載。

2、程式初始化流程:

a、28-35行定義了一些程式中可能用到的全域性變數;

b、40行建立一個Game類例項,用來管理場景中的各種變數,Game類的程式碼如下:

  1 Game=function(init_state,flag_view,wsUri,h2Uri,userid,wsToken)
  2 {
  3     var _this = this;
  4     this.scene=scene;
  5     this.loader =  new BABYLON.AssetsManager(scene);;//資源管理器
  6     //控制者陣列
  7     this.arr_allplayers=null;
  8     this.arr_myplayers={};
  9     this.arr_webplayers={};
 10     this.arr_npcs={};
 11     //this.player={};//對於world使用者這兩者相等?
 12     //this.player.arr_units=[];//這些不在這裡設定,在initscene中設定
 13     this.world={};
 14     this.world.arr_units=[];
 15     //this.arr_
 16     this.count={};
 17     this.count.count_name_npcs=0;
 18     this.Cameras={};
 19     this.ws=null;
 20     this.lights={};
 21     this.fsUI=BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI("ui1");/*全屏UI*/
 22     this.hl=new BABYLON.HighlightLayer("hl1", scene);
 23     this.hl.blurVerticalSize = 1.0;//這個影響的並不是高光的粗細程度,而是將它分成 多條以產生模糊效果,數值表示多條間的間隙尺寸
 24     this.hl.blurHorizontalSize =1.0;
 25     this.hl.innerGlow = false;
 26     this.hl.alphaBlendingMode=3;
 27     //this.hl.isStroke=true;
 28     //this.hl.blurTextureSizeRatio=2;
 29     //this.hl.mainTextureFixedSize=100;
 30     //this.hl.renderingGroupId=3;
 31     //this.hl._options.mainTextureRatio=1000;
 32 
 33     this.wsUri=wsUri;
 34     this.wsConnected=false;
 35     this.init_state=init_state;//當前執行狀態
 36     /*0-startWebGL
 37     1-WebGLStarted
 38     2-PlanetDrawed
 39      * */
 40     this.h2Uri=h2Uri;
 41     //我是誰
 42     this.WhoAmI=userid;//newland.randomString(8);
 43     this.userid=userid;
 44     this.wsToken=wsToken;
 45     //this.arr_webplayers
 46 
 47     this.materials={};/*預設材質*/
 48     var mat_frame = new BABYLON.StandardMaterial("mat_frame", scene);
 49     mat_frame.wireframe = true;
 50     //mat_frame.useLogarithmicDepth = true;
 51     mat_frame.freeze();
 52     this.materials.mat_frame=mat_frame;
 53     var mat_red=new BABYLON.StandardMaterial("mat_red", scene);
 54     mat_red.diffuseColor = new BABYLON.Color3(1, 0, 0);
 55     //mat_red.useLogarithmicDepth = true;
 56     mat_red.freeze();
 57     var mat_green=new BABYLON.StandardMaterial("mat_green", scene);
 58     mat_green.diffuseColor = new BABYLON.Color3(0, 1, 0);
 59     //mat_green.useLogarithmicDepth = true;
 60     mat_green.freeze();
 61     var mat_blue=new BABYLON.StandardMaterial("mat_blue", scene);
 62     mat_blue.diffuseColor = new BABYLON.Color3(0, 0, 1);
 63     mat_blue.freeze();
 64     var mat_black=new BABYLON.StandardMaterial("mat_black", scene);
 65     mat_black.diffuseColor = new BABYLON.Color3(0, 0, 0);
 66     //mat_black.useLogarithmicDepth = true;
 67     mat_black.freeze();
 68     var mat_orange=new BABYLON.StandardMaterial("mat_orange", scene);
 69     mat_orange.diffuseColor = new BABYLON.Color3(1, 0.5, 0);
 70     //mat_orange.useLogarithmicDepth = true;
 71     mat_orange.freeze();
 72     var mat_yellow=new BABYLON.StandardMaterial("mat_yellow", scene);
 73     mat_yellow.diffuseColor = new BABYLON.Color3(1, 1, 0);
 74     //mat_yellow.useLogarithmicDepth = true;
 75     mat_yellow.freeze();
 76     var mat_gray=new BABYLON.StandardMaterial("mat_gray", scene);
 77     mat_gray.diffuseColor = new BABYLON.Color3(0.5, 0.5, 0.5);
 78     //mat_gray.useLogarithmicDepth = true;
 79     mat_gray.freeze();
 80     this.materials.mat_red=mat_red;
 81     this.materials.mat_green=mat_green;
 82     this.materials.mat_blue=mat_blue;
 83     this.materials.mat_black=mat_black;
 84     this.materials.mat_orange=mat_orange;
 85     this.materials.mat_yellow=mat_yellow;
 86     this.materials.mat_gray=mat_gray;
 87 
 88     this.models={};/*預設模型*/
 89     this.textures={};/*預設紋理*/
 90     this.textures["grained_uv"]=new BABYLON.Texture("../../ASSETS/IMAGE/grained_uv.png", scene);//磨砂表面
 91     this.texts={};
 92 
 93     this.flag_startr=0;//開始渲染並且地形初始化完畢
 94     this.flag_starta=0;
 95     this.list_nohurry=[];
 96     this.nohurry=0;//一個計時器,讓一些計算不要太頻繁
 97     this.flag_online=false;
 98     this.flag_view=flag_view;//first/third/input/free
 99     this.flag_controlEnabled = false;
100     this.arr_keystate=[];
101     this.obj_keystate={};
102     this.SpriteManager=new BABYLON.SpriteManager("treesManagr", "../../ASSETS/IMAGE/CURSOR/palm.png", 2000, 100, scene);/*預設粒子生成器*/
103     this.SpriteManager.renderingGroupId=2;
104     this.obj_ground={};//存放地面物件(地形)
105     this.arr_startpoint=[];//場景的所有出生點
106     this.currentarea=null;
107 }

這裡預定義了一些變數,以方便之後通過MyGame物件呼叫,其中一些變數對於這次的宇宙飛船模擬並沒有作用,可以根據實際需求對它們進行增減。

需要考量的是47到86行建立預設材質的程式碼,其中mat_frame.useLogarithmicDepth = true;表示將該材質的深度計算改為對數形式,這種設定可以有效避免平面相互貼近時的閃爍現象和過於遙遠物體的深度計算溢位問題,但Babylon.js中的一些功能(如程式紋理和粒子系統)並不支援這一設定,這時同一渲染組中的非對數深度材質將總是顯示在對數深度材質的後面,所以要根據場景的具體需求決定是否使用對數深度材質。

c、46-52行依次對模擬程式各個方面進行初始化。(初始化流程參考自《Windows遊戲程式設計大師技巧》和《WebGL入門指南》)

 

二、場景初始化:

initScene方法程式碼如下:(在SpaceTest2.js檔案中)

 1 function initScene()
 2 {
 3     console.log("初始化宇宙場景");
 4     var light1 = new BABYLON.HemisphericLight("hemi", new BABYLON.Vector3(0, 50, 100), scene);//光照
 5     light1.diffuseColor = new BABYLON.Color3(0, 10, 10);
 6 
 7     var camera0= new BABYLON.UniversalCamera("FreeCamera", new BABYLON.Vector3(0, 0, -10), scene);//由FreeCamera改為新版本的“通用相機”,據說可以預設支援各種操作裝置。
 8     camera0.minZ=0.001;//視錐體近平面距離,如果物體距相機的距離小於這個數值,物體將因為脫離視錐體而不可見
 9     camera0.attachControl(canvas,true);
10     //camera0.speed=50;
11     scene.activeCameras.push(camera0);
12 
13     MyGame.player={//將一些可能用到的變數儲存到MyGame物件的player屬性中
14         name:MyGame.userid,//顯示的名字
15         id:MyGame.userid,//WebSocket Sessionid
16         camera:camera0,
17         methodofmove:"free",
18         mesh:new BABYLON.Mesh("mesh_"+MyGame.userid,scene),
19         cardinhand:[],
20         arr_units:[],
21         handpoint:new BABYLON.Mesh("mesh_handpoint_"+MyGame.userid,scene),
22         scal:5,
23     };
24     MyGame.player.handpoint.position=new BABYLON.Vector3(0,-14,31);
25     MyGame.player.handpoint.parent=MyGame.player.mesh;
26     MyGame.Cameras.camera0=camera0;
27     //啟用物理引擎
28     //var physicsPlugin =new BABYLON.CannonJSPlugin(false);
29     //var physicsPlugin = new BABYLON.OimoJSPlugin(false);
30     var physicsPlugin = new BABYLON.AmmoJSPlugin();
31     physicsPlugin.setTimeStep(1/120);
32     var physicsEngine = scene.enablePhysics(new BABYLON.Vector3(0, 0.1, 0.2), physicsPlugin);//重力new BABYLON.Vector3(0, 0.1, 0.2)
33 }

Babylon.js預設支援三種物理引擎Cannon.js、Oimo.js、Ammo.js,也支援繫結自定義物理引擎。這裡簡單對比一下三種預設支援的物理引擎:

 

 

 建議根據實際需求選擇使用何種物理引擎。

 

三、地形初始化:

initArena方法程式碼如下:

 1 function initArena()
 2 {
 3     console.log("初始化地形");
 4     var skybox = BABYLON.Mesh.CreateBox("skyBox", 1500.0, scene);//尺寸存在極限,設為15000後顯示異常
 5     var skyboxMaterial = new BABYLON.StandardMaterial("skyBox", scene);
 6     skyboxMaterial.backFaceCulling = false;
 7     skyboxMaterial.reflectionTexture = new BABYLON.CubeTexture("../../ASSETS/IMAGE/SKYBOX/nebula", scene);
 8     skyboxMaterial.reflectionTexture.coordinatesMode = BABYLON.Texture.SKYBOX_MODE;
 9     skyboxMaterial.diffuseColor = new BABYLON.Color3(0, 0, 0);
10     skyboxMaterial.specularColor = new BABYLON.Color3(0, 0, 0);
11     skyboxMaterial.disableLighting = true;
12     skybox.material = skyboxMaterial;
13     skybox.renderingGroupId = 1;
14     skybox.isPickable=false;
15     skybox.infiniteDistance = true;
16 
17     //三個參照物
18     var mesh_base=new BABYLON.MeshBuilder.CreateSphere("mesh_base",{diameter:10},scene);
19     mesh_base.material=MyGame.materials.mat_frame;
20     mesh_base.position.x=0;
21     mesh_base.renderingGroupId=2;
22     //mesh_base.layerMask=2;
23     var mesh_base1=new BABYLON.MeshBuilder.CreateSphere("mesh_base1",{diameter:10},scene);
24     mesh_base1.position.y=100;
25     mesh_base1.position.x=0;
26     mesh_base1.material=MyGame.materials.mat_frame;
27     mesh_base1.renderingGroupId=2;
28     //mesh_base1.layerMask=2;
29     var mesh_base2=new BABYLON.MeshBuilder.CreateSphere("mesh_base2",{diameter:10},scene);
30     mesh_base2.position.y=-100;
31     mesh_base2.position.x=0;
32     mesh_base2.material=MyGame.materials.mat_frame;
33     mesh_base2.renderingGroupId=2;
34 }

這是一個空曠的宇宙空間,除了天空盒與參照物沒有別的東西

 

四、事件初始化

事件初始化程式碼如下:

 1 function initEvent()
 2 {
 3     console.log("初始化控制事件");
 4     InitMouse();
 5     window.addEventListener("resize", function () {
 6         if (engine) {
 7             engine.resize();
 8         }
 9     },false);
10     window.addEventListener("keydown", onKeyDown, false);//按鍵按下
11     window.addEventListener("keyup", onKeyUp, false);//按鍵擡起
12 }
//Control.js
1 function InitMouse() 2 { 3 canvas.addEventListener("blur",function(evt){//監聽失去焦點 4 releaseKeyState(); 5 }) 6 canvas.addEventListener("focus",function(evt){//監聽獲得焦點 7 releaseKeyState(); 8 }) 9 10 } 11 //注意考慮到手機平臺,在正式使用時以沒有鍵盤為考慮 12 function onKeyDown(event) 13 { 14 var key=event.key 15 MyGame.obj_keystate[key]=1; 16 } 17 function onKeyUp(event) 18 { 19 var key=event.key 20 MyGame.obj_keystate[key]=0; 21 } 22 function releaseKeyState() 23 { 24 for(key in MyGame.obj_keystate) 25 { 26 MyGame.obj_keystate[key]=0; 27 } 28 }

考慮到使用者可能使用觸屏裝置,這裡沒有新增對“游標鎖定”(canvas.requestPointerLock)的支援,並且計劃未來將鍵盤監聽改為視窗上的gui按鈕。

 

五、UI初始化

 1、UI初始化程式碼如下:

1 function initUI()
2 {
3     console.log("初始化全域性UI");
4     MakeFullUI(MyGame.Cameras.camera0);
5 }
 1 //FullUI.js
 2 function MakeFullUI(camera0)
 3 {
 4     var node_z=new BABYLON.TransformNode("node_z",scene);
 5     node_z.position.z=32;
 6     node_z.parent=camera0;
 7     var node_y=new BABYLON.TransformNode("node_y",scene);
 8     node_y.position.z=32;
 9     node_y.position.y=13;
10     node_y.parent=camera0;
11     var node_x=new BABYLON.TransformNode("node_x",scene);
12     node_x.position.z=32;
13     node_x.position.x=28;
14     node_x.parent=camera0;
15 
16     //繪製羅盤
17     var compassz = Campass.MakeRingZ(12,36,0,0.5,node_z);
18     var compassy = Campass.MakeRingY(28,36,0,1,node_y);
19     var compassx = Campass.MakeRingX(12,36,0,1,node_x);
20 
21     camera0.node_z=node_z;
22     camera0.node_y=node_y;
23     camera0.node_x=node_x;
24     camera0.compassz=compassz;
25     camera0.compassy=compassy;
26     camera0.compassx=compassx;
27 
28     camera0.arr_myship=[];
29     camera0.arr_friendship=[];
30     camera0.arr_enemyship=[];
31 
32 
33 }

2、UI階段需要解決的一個問題是如何顯示相機在三維空間中的姿態,經過思考決定在相機前部建立一個與相機同步運動的三維羅盤:

  1 //Campass.js 建立非通用性的羅盤,因為這不是一個可以大量例項化的類,所以不放在CHARACTER路徑裡
  2 var Campass={};
  3 Campass.MakeRingX=function(radius,sumpoint,posx,sizec,parent){
  4     var lines_x=[];
  5     var arr_point=[];
  6     var radp=Math.PI*2/sumpoint;
  7     for(var i=0.0;i<sumpoint;i++)
  8     {
  9         var x=posx||0;
 10         var rad=radp*i;
 11         var y=radius*Math.sin(rad);
 12         var z=radius*Math.cos(rad);
 13         var pos=new BABYLON.Vector3(x,y,z)
 14         arr_point.push(pos);
 15         var pos2=pos.clone();
 16         pos2.x-=sizec;
 17         lines_x.push([pos,pos2]);
 18         var node=new BABYLON.Mesh("node_X"+rad,scene);
 19         node.parent=parent;
 20         node.position=pos2;
 21         var label = new BABYLON.GUI.Rectangle("label_X"+rad);
 22         label.background = "black";
 23         label.height = "14px";
 24         label.alpha = 0.5;
 25         label.width = "36px";
 26         //label.cornerRadius = 20;
 27         label.thickness = 0;
 28         //label.linkOffsetX = 30;//位置偏移量??
 29         MyGame.fsUI.addControl(label);
 30         label.linkWithMesh(node);
 31         var text1 = new BABYLON.GUI.TextBlock();
 32         text1.text = Math.round((rad/Math.PI)*180)+"";
 33         text1.color = "white";
 34         label.addControl(text1);
 35         label.isVisible=true;
 36         label.text=text1;
 37 
 38     }
 39     arr_point.push(arr_point[0].clone());//首尾相連,
 40     lines_x.push(arr_point);
 41     var compassx = new BABYLON.MeshBuilder.CreateLineSystem("compassx",{lines:lines_x,updatable:false},scene);
 42     compassx.renderingGroupId=2;
 43     compassx.color=new BABYLON.Color3(0, 1, 0);
 44     compassx.useLogarithmicDepth = true;//這句應該沒用
 45     //compassx.position=node_x.position.clone();
 46     compassx.parent=parent;
 47     compassx.mainpath=arr_point;
 48     compassx.sumpoint=sumpoint;
 49     compassx.radius=radius;
 50     return compassx;
 51 }
 52 
 53 Campass.MakeRingY=function(radius,sumpoint,posy,sizec,parent){
 54     var lines_y=[];
 55     var arr_point=[];
 56     var radp=Math.PI*2/sumpoint;
 57     for(var i=0.0;i<sumpoint;i++)
 58     {
 59         var y=posy||0;
 60         var rad=radp*i;
 61         var z=radius*Math.sin(rad);
 62         var x=radius*Math.cos(rad);
 63         var pos=new BABYLON.Vector3(x,y,z)
 64         arr_point.push(pos);
 65         var pos2=pos.clone();
 66         pos2.y-=sizec;
 67         lines_y.push([pos,pos2]);
 68         var node=new BABYLON.Mesh("node_Y"+rad,scene);
 69         node.parent=parent;
 70         node.position=pos2;
 71         var label = new BABYLON.GUI.Rectangle("label_Y"+rad);
 72         label.background = "black";
 73         label.height = "14px";
 74         label.alpha = 0.5;
 75         label.width = "36px";
 76         //label.cornerRadius = 20;
 77         label.thickness = 0;
 78         //label.linkOffsetX = 30;//位置偏移量??
 79         MyGame.fsUI.addControl(label);
 80         label.linkWithMesh(node);//對TransformNode使用會造成定位異常
 81         var text1 = new BABYLON.GUI.TextBlock();
 82         var num=Math.round((rad/Math.PI)*180);
 83         if(num>=90)
 84         {
 85             num-=90;
 86         }
 87         else
 88         {
 89             num+=270;
 90         }
 91         text1.text = num+"";
 92         text1.color = "white";
 93         label.addControl(text1);
 94         label.isVisible=true;
 95         label.text=text1;
 96     }
 97     arr_point.push(arr_point[0].clone());//首尾相連,
 98     lines_y.push(arr_point);
 99     var compassy = new BABYLON.MeshBuilder.CreateLineSystem("compassy",{lines:lines_y,updatable:false},scene);
100     compassy.renderingGroupId=2;
101     compassy.color=new BABYLON.Color3(0, 1, 0);
102     compassy.useLogarithmicDepth = true;
103     //compassy.position=node_y.position.clone();
104     compassy.parent=parent;
105     compassy.mainpath=arr_point;
106     compassy.sumpoint=sumpoint;
107     compassy.radius=radius;
108     return compassy;
109 }
110 
111 Campass.MakeRingZ=function(radius,sumpoint,posz,sizec,parent){
112     var lines_z=[];
113     var arr_point=[];
114     var radp=Math.PI*2/sumpoint;
115     parent.arr_node=[];
116     for(var i=0.0;i<sumpoint;i++)
117     {
118         var z=posz||0;
119         var rad=radp*i;
120         var x=radius*Math.sin(rad);
121         var y=radius*Math.cos(rad);
122         var pos=new BABYLON.Vector3(x,y,z);
123         arr_point.push(pos);
124         var pos2=pos.clone();
125         pos2.normalizeFromLength(radius/(radius-sizec));//裡面的數字表示座標值除以幾
126         lines_z.push([pos,pos2]);
127         var node=new BABYLON.Mesh("node_Z"+rad,scene);
128         node.parent=parent;
129         node.position=pos2;
130         parent.arr_node.push(node);
131         var label = new BABYLON.GUI.Rectangle("label_Z"+rad);
132         label.background = "black";
133         label.height = "14px";
134         label.alpha = 0.5;
135         label.width = "36px";
136         //label.cornerRadius = 20;
137         label.thickness = 0;
138         label.rotation=rad;
139         label.startrot=rad;
140         //label.linkOffsetX = 30;//位置偏移量??
141         MyGame.fsUI.addControl(label);
142         label.linkWithMesh(node);
143         var text1 = new BABYLON.GUI.TextBlock();
144         text1.text = Math.round((rad/Math.PI)*180)+"";//不顯式轉換會報錯
145         text1.color = "white";
146         label.addControl(text1);
147         label.isVisible=true;
148         label.text=text1;
149         node.label=label;
150     }
151     arr_point.push(arr_point[0].clone());//首尾相連,
152     lines_z.push(arr_point);
153     var compassz = new BABYLON.MeshBuilder.CreateLineSystem("compassz",{lines:lines_z,updatable:false},scene);
154     compassz.renderingGroupId=2;
155     compassz.color=new BABYLON.Color3(0, 1, 0);
156     compassz.useLogarithmicDepth = true;
157     compassz.parent=parent;
158     compassz.mainpath=arr_point;
159     compassz.sumpoint=sumpoint;
160     compassz.radius=radius;
161     return compassz;
162 }

羅盤的主體是三個圓環,圓環上有表示角度的刻度和數字,其結構示意圖如下:

  圖一

  圖二

圖中白色四稜錐表示相機的視錐體,compassx和compassy距相機較近的半圓正好在視錐體以外,故不可見。關於相機姿態改變時羅盤如何運動,將在初始化迴圈中介紹。另外也許可以將compassx和compassy的一圈設為720度,這樣就可以在視野中看到所有角度的情況,或者使用類似html走馬燈的gui代替立體羅盤,時間有限並未測試這些思路。

應該在螢幕頂部和右側的中間新增兩個指標,這樣將能夠更精確的指出當前角度,計劃下個版本新增。

3、這裡再說一點和Babylon.js視錐體有關的內容,Babylon.js官方文件裡很少提及視錐體的屬性和設定方法(似乎是封裝在相機的投影矩陣方法裡),於是自己編寫程式碼測試視錐體屬性:

  1 <!DOCTYPE html>
  2 <html lang="en">
  3 <head>
  4     <meta charset="UTF-8">
  5     <title>改為直接用頂點構造視錐體</title>
  6     <link href="../../CSS/newland.css" rel="stylesheet">
  7     <script src="../../JS/LIB/babylon.min.js"></script><!--這裡包含了babylon格式的模型匯入,但不包含gltf等其他格式,包含了後期處理-->
  8     <script src="../../JS/LIB/babylon.gui.min.js"></script>
  9     <script src="../../JS/LIB/babylonjs.loaders.min.js"></script>
 10     <script src="../../JS/LIB/babylonjs.materials.min.js"></script>
 11     <script src="../../JS/LIB/earcut.min.js"></script>
 12     <script src="../../JS/LIB/babylonjs.proceduralTextures.min.js"></script>
 13     <script src="../../JS/LIB/oimo.min.js"></script>
 14     <script src="../../JS/LIB/ammo.js"></script>
 15     <script src="../../JS/LIB/cannon.js"></script>
 16     <script src="../../JS/LIB/dat.gui.min.js"></script>
 17     <script src="../../JS/MYLIB/newland.js"></script>
 18     <script src="../../JS/MYLIB/CREATE_XHR.js"></script>
 19 </head>
 20 <body>
 21 <div id="div_allbase">
 22     <canvas id="renderCanvas"></canvas>
 23     <div id="fps" style="z-index: 302;"></div>
 24 </div>
 25 </body>
 26 <script>
 27     var VERSION=1.0,AUTHOR="[email protected]";
 28     var machine,canvas,engine,scene,gl,MyGame;
 29     canvas = document.getElementById("renderCanvas");
 30     engine = new BABYLON.Engine(canvas, true);
 31     engine.displayLoadingUI();
 32     gl=engine._gl;
 33     scene = new BABYLON.Scene(engine);
 34     var divFps = document.getElementById("fps");
 35 
 36     window.onload=beforewebGL;
 37     function beforewebGL()
 38     {
 39         webGLStart();
 40     }
 41     function webGLStart()
 42     {
 43         createScene();
 44         //scene.debugLayer.show();
 45         MyBeforeRender();
 46     }
 47     var createScene = function () {
 48         camera0= new BABYLON.UniversalCamera("FreeCamera", new BABYLON.Vector3(0, 0, 0), scene);//FreeCamera
 49         camera0.minZ=0.001;
 50         camera0.attachControl(canvas,true);
 51         scene.activeCameras.push(camera0);
 52 
 53         var light1 = new BABYLON.HemisphericLight("hemi", new BABYLON.Vector3(0, 1, 0), scene);
 54         light1.diffuse = new BABYLON.Color3(1,1,1);//這道“顏色”是從上向下的,底部收到100%,側方收到50%,頂部沒有
 55         light1.specular = new BABYLON.Color3(0,0,0);
 56         light1.groundColor = new BABYLON.Color3(1,1,1);//這個與第一道正相反
 57 
 58         var skybox = BABYLON.Mesh.CreateBox("skyBox", 1500.0, scene);
 59         var skyboxMaterial = new BABYLON.StandardMaterial("skyBox", scene);
 60         skyboxMaterial.backFaceCulling = false;
 61         skyboxMaterial.reflectionTexture = new BABYLON.CubeTexture("../../ASSETS/IMAGE/SKYBOX/nebula", scene);
 62         skyboxMaterial.reflectionTexture.coordinatesMode = BABYLON.Texture.SKYBOX_MODE;
 63         skyboxMaterial.diffuseColor = new BABYLON.Color3(0, 0, 0);
 64         skyboxMaterial.specularColor = new BABYLON.Color3(0, 0, 0);
 65         skyboxMaterial.disableLighting = true;
 66         skybox.material = skyboxMaterial;
 67         skybox.renderingGroupId = 1;
 68         skybox.isPickable=false;
 69         skybox.infiniteDistance = true;
 70 
 71         var mat_frame = new BABYLON.StandardMaterial("mat_frame", scene);
 72         mat_frame.wireframe = true;
 73     //測試視錐體
 74         var vertexData= new BABYLON.VertexData();
 75         var w=50;//錐體底部矩形寬度的一半
 76         var h=60;//錐體底部到視點的距離
 77         var r=0.5;//錐體底部矩形的高寬比
 78         var positions=[0,0,0,-w,w*r,h,-w,-w*r,h,w,-w*r,h,w,w*r,h];
 79         var uvs=[0.5,0.5,0,0,0,1,1,1,1,0];
 80         var normals=[];
 81         var indices=[0,1,2,0,2,3,0,3,4,0,4,1];
 82         BABYLON.VertexData.ComputeNormals(positions, indices, normals);//計演算法線
 83         BABYLON.VertexData._ComputeSides(0, positions, indices, normals, uvs);
 84         vertexData.indices = indices.concat();//索引
 85         vertexData.positions = positions.concat();
 86         vertexData.normals = normals.concat();//position改變法線也要改變!!!!
 87         vertexData.uvs = uvs.concat();
 88 
 89         var mesh=new BABYLON.Mesh(name,scene);
 90         vertexData.applyToMesh(mesh, true);
 91         mesh.vertexData=vertexData;
 92         mesh.renderingGroupId=2;
 93         mesh.material=mat_frame;
 94 
 95         var node_z=new BABYLON.TransformNode("node_z",scene);
 96         node_z.position.z=32;
 97         //node_z.parent=camera0;
 98         var node_y=new BABYLON.TransformNode("node_y",scene);
 99         node_y.position.z=32;
100         node_y.position.y=13;
101         //node_y.parent=camera0;
102         var node_x=new BABYLON.TransformNode("node_x",scene);
103         node_x.position.z=32;
104         node_x.position.x=28;
105         //node_x.parent=camera0;
106         //繪製羅盤
107         var compassz = Campass.MakeRingZ(12,36,0,0.5,node_z);
108         var compassy = Campass.MakeRingY(28,36,0,1,node_y);
109         var compassx = Campass.MakeRingX(12,36,0,1,node_x);
110 
111     }
112     function MyBeforeRender()
113     {
114         scene.registerBeforeRender(
115             function(){
116                 //camera0.position.x=0;
117                 //camera0.position.y=0;
118             }
119         )
120         scene.registerAfterRender(
121             function() {
122             }
123         )
124         engine.runRenderLoop(function () {
125             engine.hideLoadingUI();
126             if (divFps) {
127                 // Fps
128                 divFps.innerHTML = engine.getFps().toFixed() + " fps";
129             }
130             //lastframe=new Date().getTime();
131             scene.render();
132         });
133     }
134     var Campass={};
135     Campass.MakeRingX=function(radius,sumpoint,posx,sizec,parent){
136         var lines_x=[];
137         var arr_point=[];
138         var radp=Math.PI*2/sumpoint;
139         for(var i=0.0;i<sumpoint;i++)
140         {
141             var x=posx||0;
142             var rad=radp*i;
143             var y=radius*Math.sin(rad);
144             var z=radius*Math.cos(rad);
145             var pos=new BABYLON.Vector3(x,y,z)
146             arr_point.push(pos);
147             var pos2=pos.clone();
148             pos2.x-=sizec;
149             lines_x.push([pos,pos2]);
150             var node=new BABYLON.Mesh("node_X"+rad,scene);
151             node.parent=parent;
152             node.position=pos2;
153         }
154         arr_point.push(arr_point[0].clone());//首尾相連,不能這樣相連,否則變形時會多出一個頂點!!,看來這個多出的頂點無法去掉,只能在選取時額外處理它
155         lines_x.push(arr_point);
156         var compassx = new BABYLON.MeshBuilder.CreateLineSystem("compassx",{lines:lines_x,updatable:false},scene);
157         compassx.renderingGroupId=2;
158         compassx.color=new BABYLON.Color3(0, 1, 0);
159         compassx.useLogarithmicDepth = true;
160         //compassx.position=node_x.position.clone();
161         compassx.parent=parent;
162         compassx.mainpath=arr_point;
163         compassx.sumpoint=sumpoint;
164         compassx.radius=radius;
165         return compassx;
166     }
167 
168     Campass.MakeRingY=function(radius,sumpoint,posy,sizec,parent){
169         var lines_y=[];
170         var arr_point=[];
171         var radp=Math.PI*2/sumpoint;
172         for(var i=0.0;i<sumpoint;i++)
173         {
174             var y=posy||0;
175             var rad=radp*i;
176             var z=radius*Math.sin(rad);
177             var x=radius*Math.cos(rad);
178             var pos=new BABYLON.Vector3(x,y,z)
179             arr_point.push(pos);
180             var pos2=pos.clone();
181             pos2.y-=sizec;
182             lines_y.push([pos,pos2]);
183             var node=new BABYLON.Mesh("node_Y"+rad,scene);
184             node.parent=parent;
185             node.position=pos2;
186         }
187         arr_point.push(arr_point[0].clone());//首尾相連,不能這樣相連,否則變形時會多出一個頂點!!,看來這個多出的頂點無法去掉,只能在選取時額外處理它
188         lines_y.push(arr_point);
189         var compassy = new BABYLON.MeshBuilder.CreateLineSystem("compassy",{lines:lines_y,updatable:false},scene);
190         compassy.renderingGroupId=2;
191         compassy.color=new BABYLON.Color3(0, 1, 0);
192         compassy.useLogarithmicDepth = true;
193         //compassy.position=node_y.position.clone();
194         compassy.parent=parent;
195         compassy.mainpath=arr_point;
196         compassy.sumpoint=sumpoint;
197         compassy.radius=radius;
198         return compassy;
199     }
200 
201     Campass.MakeRingZ=function(radius,sumpoint,posz,sizec,parent){
202         var lines_z=[];
203         var arr_point=[];
204         var radp=Math.PI*2/sumpoint;
205         parent.arr_node=[];
206         for(var i=0.0;i<sumpoint;i++)
207         {
208             var z=posz||0;
209             var rad=radp*i;
210             var x=radius*Math.sin(rad);
211             var y=radius*Math.cos(rad);
212             var pos=new BABYLON.Vector3(x,y,z);
213             arr_point.push(pos);
214             var pos2=pos.clone();
215             pos2.normalizeFromLength(radius/(radius-sizec));//裡面的數字表示座標值除以幾
216             lines_z.push([pos,pos2]);
217             var node=new BABYLON.Mesh("node_Z"+rad,scene);
218             node.parent=parent;
219             node.position=pos2;
220             parent.arr_node.push(node);
221         }
222         arr_point.push(arr_point[0].clone());//首尾相連,不能這樣相連,否則變形時會多出一個頂點!!,看來這個多出的頂點無法去掉,只能在選取時額外處理它
223         lines_z.push(arr_point);
224         var compassz = new BABYLON.MeshBuilder.CreateLineSystem("compassz",{lines:lines_z,updatable:false},scene);
225         compassz.renderingGroupId=2;
226         compassz.color=new BABYLON.Color3(0, 1, 0);
227         compassz.useLogarithmicDepth = true;
228         compassz.parent=parent;
229         compassz.mainpath=arr_point;
230         compassz.sumpoint=sumpoint;
231         compassz.radius=radius;
232         return compassz;
233     }
234 </script>
235 </html>

從73行開始,調整h引數,當圖一中的白色邊界恰好消失時,場景中的錐形網格即與視錐體形狀相同。測得Babylon.js預設視錐體底面矩形的高寬比為0.5,錐寬和錐高比約為100比59,水平視野角度約為80.56度((Math.atan(50/59)*2/Math.PI)*180),因為暫時不需要,沒有研究如何修改這些屬性。可以在https://ljzc002.github.io/test/Spacetest/HTML/TEST2/testCylinder2.html檢視這一測試頁面。

在3D程式設計的世界裡,長度並沒有實際的物理意義,距離視點100大小為50的物體和距離視點200大小為100的物體看起來是一樣大的,但這並不意味著我們可以任意設定物體的尺寸,在設定尺寸時我們需要考慮物體是否在視錐體的近平面和遠平面之間、物體之間的相互遮擋關係、過大或過小的值是否會導致計算溢位,以及各種庫對尺寸的支援,比如Babylon.js的天空盒尺寸如果設定過大(比如15000)會導致天空紋理顯示異常、再比如某個物理引擎預設只支援0.1到10的尺寸範圍,這類庫對尺寸的限制往往缺少文件說明,需要經過測試方可得知。

六、單位初始化:

initObj方法程式碼如下:

 1 function initObj()
 2 {//假設一單位長度對應100m
 3     console.log("初始化單位");
 4     var ship=new BABYLON.MeshBuilder.CreateBox("ship_target",{size:5},scene);//建立一個立方體作為飛船
 5     ship.position=new BABYLON.Vector3(-5,0,0);
 6     ship.material=MyGame.materials.mat_green;
 7     ship.renderingGroupId=2;
 8     //ship.v={x:0,y:0,z:0}
 9     ship.physicsImpostor = new BABYLON.PhysicsImpostor(ship, BABYLON.PhysicsImpostor.BoxImpostor//SphereImpostor//
10         , { mass: 1, restitution: 0.0005 ,friction:0,damping:0,linearDamping:"a"}, scene);//物理模擬器
11     ship.physicsImpostor.damping=0;
12     MyGame.player.ship=ship;
13     //在羅盤裡為這個ship新增一個標誌
14     var camera0=MyGame.Cameras.camera0;
15     Campass.AddShip(camera0,"my",ship);
16     /*scene.onReadyObservable.add(function(){//這個應該在更早的時候執行過了!!
17         ship.physicsImpostor.physicsBody.linearDamping=0;
18         ship.physicsImpostor.physicsBody.angularDamping=0;
19     })*/
20     newland.DisposeDamping(ship);
21     //在左下角顯示ship的當前位置
22     var advancedTexture = MyGame.fsUI;
23     var UiPanel = new BABYLON.GUI.StackPanel();
24     UiPanel.width = "220px";
25     UiPanel.fontSize = "14px";
26     UiPanel.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
27     UiPanel.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM;
28     UiPanel.color = "white";
29     advancedTexture.addControl(UiPanel);
30     MyGame.player.ship.label_pos=UiPanel;//所以把這個UI相關設定放在了initObj裡
31     var text1 = new BABYLON.GUI.TextBlock();
32     text1.text = ""
33     text1.color = "white";
34     text1.paddingTop = "0px";
35     text1.width = "220px";
36     text1.height = "20px";
37     UiPanel.addControl(text1);
38     UiPanel.text1=text1;
39     var text2 = new BABYLON.GUI.TextBlock();
40     text2.text = ""
41     text2.color = "white";
42     text2.paddingTop = "0px";
43     text2.width = "220px";
44     text2.height = "20px";
45     UiPanel.addControl(text2);
46     UiPanel.text2=text2;
47     var text3 = new BABYLON.GUI.TextBlock();
48     text3.text = ""
49     text3.color = "white";
50     text3.paddingTop = "0px";
51     text3.width = "220px";
52     text3.height = "20px";
53     UiPanel.addControl(text3);
54     UiPanel.text3=text3;
55 
56     var mesh_rocket=new BABYLON.MeshBuilder.CreateCylinder("mesh_rocket"//為飛船新增一個圓錐形的火箭推進器
57         ,{height:2,diameterTop:0.1,diameterBottom :1},scene);
58     mesh_rocket.renderingGroupId = 2;
59     mesh_rocket.material=MyGame.materials.mat_gray;
60     mesh_rocket.rotation=new BABYLON.Vector3(Math.PI,0,0);
61     mesh_rocket.position=new BABYLON.Vector3(0,-1,0);
62     var rocket=new Rocket();
63     ship.rocket=rocket;
64     var obj_p={ship:ship,mesh:mesh_rocket,name:"testrocket1"
65     ,mass:1000,cost2power:function(cost){return cost*1;}
66     ,pos:new BABYLON.Vector3(0,0,-3.5),rot:new BABYLON.Vector3(-Math.PI/2,0,0)};
67     rocket.init(obj_p);//初始化火箭物件
68     rocket.fire({firebasewidth:0.5,cost:1,firescaling:1});//發動火箭
69 
70     var shipb=new BABYLON.MeshBuilder.CreateBox("ship_targetb",{size:5},scene);//再建立一個飛船作為對比
71     shipb.position=new BABYLON.Vector3(5,0,0);
72     shipb.material=MyGame.materials.mat_green;
73     shipb.renderingGroupId=2;
74     //ship.v={x:0,y:0,z:0}
75     shipb.physicsImpostor = new BABYLON.PhysicsImpostor(shipb, BABYLON.PhysicsImpostor.BoxImpostor//SphereImpostor//
76         , { mass: 1, restitution: 0.0005 ,friction:0}, scene);//物理模擬器
77     shipb.mass=1000000000;
78     MyGame.player.shipb=shipb;
79     //在羅盤裡為這個ship新增一個標誌
80     var camera0=MyGame.Cameras.camera0;
81     Campass.AddShip(camera0,"my",shipb);
82     newland.DisposeDamping(shipb);
83 
84     var mesh_rocketb=new BABYLON.MeshBuilder.CreateCylinder("mesh_rocketb"
85         ,{height:2,diameterTop:0.1,diameterBottom :1},scene);
86     mesh_rocketb.renderingGroupId = 2;
87     mesh_rocketb.material=MyGame.materials.mat_gray;
88     mesh_rocketb.rotation=new BABYLON.Vector3(Math.PI,0,0);
89     mesh_rocketb.position=new BABYLON.Vector3(0,-1,0);
90     var rocketb=new Rocket();
91     shipb.rocket=rocketb;
92     var obj_pb={ship:shipb,mesh:mesh_rocketb,name:"testrocket1b"
93         ,mass:1000,cost2power:function(cost){return cost*1;}
94         ,pos:new BABYLON.Vector3(0,0,-3.5),rot:new BABYLON.Vector3(-Math.PI/2,0,0)};
95     rocketb.init(obj_pb);
96     rocketb.fire({firebasewidth:0.5,cost:1,firescaling:1});
97 }

 1、首先建立了一個立方體網格代表宇宙飛船,然後在第九行為飛船設定物理模擬器。這裡需要注意damping引數,這個引數表示物理引擎對加速度的無條件阻礙,預設值為0.1,與表示摩擦係數的friction引數不同,即使模擬器不與任何其他物體接觸也會一直受到這一削減作用,具體表現為加速度每秒鐘減少0.1直到減少為0,這也就意味著加速度小於0.1的力不會對物體造成任何影響。按照Babylon.js的設計初衷,這一屬性應該能通過PhysicsImpostor的建構函式設定,但遺憾的是隨著物理引擎的升級迭代,在建構函式中使用這一引數並沒有任何作用,使用者必須自己前往物理引擎的底層修改這一引數(事實上是兩個引數:線速度衰減和角速度衰減):

 1 //移除網格的物理外套的預設加速度衰減
 2 newland.DisposeDamping=function(mesh)
 3 {
 4     //cannon使用
 5     mesh.physicsImpostor.physicsBody.linearDamping=0;
 6     mesh.physicsImpostor.physicsBody.angularDamping=0;
 7     //ammo使用
 8     if(mesh.physicsImpostor.physicsBody.setDamping)
 9     {
10         mesh.physicsImpostor.physicsBody.setDamping(0,0);
11     }
12 }

以上是cannon和ammo的衰減移除方法,oimo似乎缺少這方面的限制。

這裡再介紹一下physicsImpostor和physicsBody的關係,physicsImpostor是Babylon.js建立的物件,我們可以通過它用差不多的方式操作多種物理引擎,而physicsBody則是指向具體物理引擎底層資料的指標,每一種物理引擎的physicsBody結構都各不相同。關於二者關係的詳細說明可以參考官方論壇https://forum.babylonjs.com/t/a-question-on-how-applyforce-work/5841。

2、接下來需要在羅盤裡指示出飛船的方向,在Campass.js中

 1 Campass.AddShip=function(camera0,type,ship)
 2 {
 3     //渲染組3突出顯示
 4     var vec_ship=ship.position.clone().subtract(camera0.position);//由視點指向飛船的向量
 5     vec_ship=newland.VecTo2Local(vec_ship,camera0);//轉化為區域性座標系座標
 6     var pointerz= new BABYLON.MeshBuilder.CreateSphere("pointerz_"+ship.name,{diameter:1},scene);//球體標記
 7     pointerz.parent=camera0.compassz.parent;
 8     pointerz.position=new BABYLON.Vector3(vec_ship.x,vec_ship.y,0).normalize().scale(camera0.compassz.radius);
 9     pointerz.renderingGroupId=3;
10     ship.pointerz=pointerz;
11     if(type=="my")//自己控制的飛船顯示為綠色
12     {
13         camera0.arr_myship.push(ship);
14         pointerz.material=MyGame.materials.mat_green;
15     }
16     else if(type=="friend")//友方為藍色
17     {
18         camera0.arr_friendship.push(ship);
19         pointerz.material=MyGame.materials.mat_blue;
20     }
21     else if(type=="enemy")//敵方為紅色
22     {
23         camera0.arr_enemyship.push(ship);
24         pointerz.material=MyGame.materials.mat_red;
25     }
26     var label = new BABYLON.GUI.Rectangle("label_pointerz_"+ship.name);//文字框
27     label.background = "black";
28     label.height = "14px";
29     label.alpha = 0.5;
30     label.width = "120px";
31     label.thickness = 0;
32     //label.linkOffsetX = 30;//位置偏移量??
33     MyGame.fsUI.addControl(label);
34     label.linkWithMesh(pointerz);
35     var text1 = new BABYLON.GUI.TextBlock();
36     text1.text = ship.name;
37     text1.color = "white";
38     label.addControl(text1);
39     label.isVisible=true;
40     label.text=text1;
41     pointerz.label=label;
42 }
43 Campass.ComputePointerPos=function(ship)//重新整理飛船的方位
44 {
45     var camera0=MyGame.Cameras.camera0;
46     var pointerz=ship.pointerz;
47     var vec_ship=ship.position.clone().subtract(camera0.position);
48     /*var v=new BABYLON.Vector3(vec_ship.x,vec_ship.y,0)
49     var m = camera0.getWorldMatrix();
50     var v = BABYLON.Vector3.TransformCoordinates(vector, m);*/
51     vec_ship=newland.VecTo2Local(vec_ship,camera0);
52     pointerz.position=(new BABYLON.Vector3(vec_ship.x,vec_ship.y,0)).normalize().scale(camera0.compassz.radius);
53 
54 }

3、21到54行在螢幕左下角建立三個文字框顯示飛船的位置。

4、56到68行為飛船添加了一個火箭推進器,Rocket類在Rocket2.js檔案中:

 1 //工質發動機(粒子系統版,低粒子量、低亮度、低閃爍)
 2 Rocket=function()
 3 {
 4 
 5 }
 6 Rocket.prototype.init=function(param)
 7 {
 8     param = param || {};
 9     this.name=param.name;
10     this.ship=param.ship;
11     this.node=new BABYLON.TransformNode("node_rocket_"+this.name,scene);//用變換節點代替空網格
12     this.node.position=param.pos;
13     this.node.rotation=param.rot;
14     this.node.parent=this.ship;
15     this.mesh=param.mesh;//噴口網格,也可能只是instance
16     this.mesh.parent=this.node;
17     this.mass=param.mass;
18     this.ship.mass+=this.mass
19     this.cost2power=param.cost2power;//供能轉換為推力的公式
20     this.cost2demage=param.cost2demage;//供能對引擎造成損壞的公式,其中包括對故障率的影響
21     this.hp=param.hp;
22     this.cost=null;//當前供能
23     this.power=null;//當前推力
24     this.failurerate=param.failurerate;//故障率引數
25 
26 
27     //this.scaling=param.scaling||1;
28 
29     this.rotxl=param.rotxl;//引擎在x軸上的擺動範圍
30     this.rotyl=param.rotyl;
31     this.rotzl=param.rotzl;
32 
33 
34 }
35 Rocket.prototype.fire=function(param)
36 {
37     this.cost=param.cost;
38     this.power=this.cost2power(this.cost);
39     this.firebasewidth=param.firebasewidth||1;//火焰底部的寬度
40     this.firescaling=param.firescaling||1;//噴射火焰尺寸
41 
42     var particleSystem;
43     particleSystem = new BABYLON.GPUParticleSystem("particles", { capacity:50000 }, scene);//粒子系統,可用粒子為50000個
44     particleSystem.activeParticleCount = 50000;//活動粒子數50000
45     particleSystem.emitRate = 10000;//每秒發射10000個
46     particleSystem.particleTexture = new BABYLON.Texture("../../ASSETS/IMAGE/TEXTURES/fire/flare.png", scene);//粒子紋理
47     particleSystem.maxLifeTime = 10;//最大生存時間
48     particleSystem.minSize = 0.01//*this.firescaling;
49     particleSystem.maxSize = 0.1//*this.firescaling;
50     particleSystem.emitter = this.node;
51 
52     var radius = this.firebasewidth;
53     var angle = Math.PI;
54     var coneEmitter = new BABYLON.ConeParticleEmitter(radius, angle);//錐形發射器
55     coneEmitter.radiusRange = 1;
56     coneEmitter.heightRange = 0;
57     particleSystem.particleEmitterType = coneEmitter;
58 
59     particleSystem.renderingGroupId=2;
60     particleSystem.start();//啟動粒子系統
61     //var force=new BABYLON.Vector3(0,-this.power*100000/this.ship.mass,0);
62     var force=new BABYLON.Vector3(0,-1,0);
63     force=newland.vecToGlobal(force,this.node);
64     force=force.subtract(this.node.getAbsolutePosition()).scale(this.power);
65     //this.ship.physicsImpostor.applyForce(force,this.node.position)//
66     //this.ship.physicsImpostor.applyImpulse(force,new BABYLON.Vector3(0,0,-3.5))//這個相當於只加速一秒
67     //this.ship.physicsImpostor.applyForce(force,new BABYLON.Vector3(0,0,-3.5))//Oimo doesn't support applying force. Using impule instead.
68 
69     var rocket=this;
70     //this.ship.physicsImpostor.applyImpulse(new BABYLON.Vector3(0,0,1),new BABYLON.Vector3(0,0,-2.5));
71     /*MyGame.AddNohurry("task_rocketfire_"+this.name,1000,0,//每秒執行一次
72         function(){
73             var force=new BABYLON.Vector3(0,-1,0);
74             force=newland.vecToGlobal(force,rocket.node);
75             force=force.subtract(rocket.node.getAbsolutePosition()).scale(rocket.power);
76             rocket.ship.physicsImpostor.applyForce(force,rocket.node.getAbsolutePosition());
77         },0)*/
78     scene.registerAfterRender(function(){//每幀渲染後執行
79         var force=new BABYLON.Vector3(0,-1,0);
80         force=newland.vecToGlobal(force,rocket.node);
81         force=force.subtract(rocket.node.getAbsolutePosition()).scale(rocket.power);
82         var pos=rocket.node.getAbsolutePosition();
83         rocket.ship.physicsImpostor.applyForce(force,pos);
84         console.log(rocket.ship.physicsImpostor.getLinearVelocity());
85     })
86 
87     //this.ship.physicsImpostor.applyForce(force,this.node.getAbsolutePosition());//只執行一次
88 }

火箭推進器由一個圓錐形的噴口和從噴口噴出的粒子組成,注意第50行的particleSystem.emitter = this.node;和第57行的particleSystem.particleEmitterType = coneEmitter;的區別,前者表示整個粒子系統隨著火箭移動,後者則表示粒子發射區域的形狀。使用變換節點代替空網格的原因可以參考https://www.cnblogs.com/ljzc002/p/10005921.html,粒子系統的使用方法可以檢視:https://ljzc002.github.io/BABYLON101/15Particles%20-%20Babylon.js%20Documentation.htm。第43行為了提高渲染效率使用了GPU粒子系統,在實際使用時可以考慮進一步降低可用粒子數量和粒子發射率。

另一種思路是使用火焰材質或火焰紋理而非粒子來表現火箭的尾焰,在一些情況下這種尾焰表現的很不錯(Rocket.js):

跳動的火焰和靜謐的太空形成了奇妙的對比

但是這種基於蒙版貼圖的紋理在轉變觀察角度時會產生一系列的問題,並且也無法模擬飛船轉彎時的拖尾效果,因此沒有采用。 

從第62行開始為火箭施加推力:

a、因為Babylon.js建立的圓錐體預設底面朝下,建立後經過旋轉變換並繼承父物體的姿態後才變成現在指向飛船後部的姿態,所以第62到64行首先建立一個垂直向下的力,然後對這個力施加火箭的世界矩陣,又因為火箭的世界矩陣中包含的位置變化會影響力向量,錯誤的改變力的大小和方向,所以再將力向量減去火箭的絕對位置,如此就得到了火箭噴力在全域性座標系下的方向,然後再乘以噴力大小即可得火箭噴力向量。

b、接下來為飛船的物理模擬器施加力作用,Babylon.js為使用者提供了兩種施加外力的方式(https://doc.babylonjs.com/how_to/forces#impulses)——applyImpulse與applyForce,二者的引數都是全域性座標系中的力向量和力作用點,每次執行前者相當於以這種引數配置加速模擬器1秒鐘,後者每執行一次則表示以這種引數配置加速物體當前幀的時間,顯然後者的加速更為精確平滑,所以選擇使用applyForce方法。又因為Oimo引擎不支援applyForce(內部自動替換為applyImpulse),選用Ammo引擎。另外applyImpulse與applyForce的力向量引數單位都是“力”,飛船的質量不同將會產生不同的加速度。

出於省事,這裡沒有把火箭本身的質量加到飛船質量中,也沒有考慮火箭推進對工質質量的消耗。

5、接著建立一個類似的飛船shipb作為對比

七、主迴圈初始化:

 1 var posz_temp1=0;//上一次的位置
 2 var posz_temp2=0;//上一次的速度
 3 var posz_temp1b=0;//上一次的位置
 4 var posz_temp2b=0;//上一次的速度
 5 function initLoop()
 6 {
 7     console.log("初始化主迴圈");
 8     var _this=MyGame;
 9     MyGame.AddNohurry("task_logpos",1000,0,function(){//每秒鐘輸出一些資訊並且更新飛船的位置顯示
10         var posz=MyGame.player.ship.position.z;
11         var poszb=MyGame.player.shipb.position.z;
12         //console.log("---"+(new Date().getTime())+"\n"+posz+"_"+(posz-posz_temp1)+"_"+(posz-posz_temp1-posz_temp2)+"@"+MyGame.player.ship.physicsImpostor.getLinearVelocity()
13         //    +"\n"+poszb+"_"+(poszb-posz_temp1b)+"_"+(poszb-posz_temp1b-posz_temp2b)+"@"+MyGame.player.shipb.physicsImpostor.getLinearVelocity());
14         //console.log(MyGame.player.ship.physicsImpostor.getLinearVelocity());
15         posz_temp2=posz-posz_temp1;
16         posz_temp1=posz;
17         posz_temp2b=poszb-posz_temp1b;
18         posz_temp1b=poszb;
19         var ship_main=MyGame.player.ship;
20         var UiPanel=ship_main.label_pos;
21         UiPanel.text1.text="x:"+ship_main.position.x;
22         UiPanel.text2.text="y:"+ship_main.position.y;
23         UiPanel.text3.text="z:"+ship_main.position.z;
24     },0)//name,delay,lastt,todo,count
25 
26     scene.registerBeforeRender(
27         function(){
28 
29             var camera0=MyGame.Cameras.camera0;
30             var node_z=camera0.node_z;
31             var node_y=camera0.node_y;
32             var node_x=camera0.node_x;
33 
34             node_z.rotation.z=-camera0.rotation.z;//反轉羅盤
35             var len=node_z.arr_node.length;
36             for(var i=0;i<len;i++)
37             {
38                 var label=node_z.arr_node[i].label;
39                 label.rotation=label.startrot+camera0.rotation.z;
40             }
41             node_y.rotation.y=-camera0.rotation.y;
42             node_x.rotation.x=-camera0.rotation.x;
43             //艦船標誌更新放在每一幀裡還是每秒執行一次?
44             var len1=camera0.arr_myship.length;
45             for(var i=0;i<len1;i++)
46             {
47                 var ship=camera0.arr_myship[i];
48                 Campass.ComputePointerPos(ship);
49             }
50             var len1=camera0.arr_friendship.length;
51             for(var i=0;i<len1;i++)
52             {
53                 var ship=camera0.arr_friendship[i];
54                 Campass.ComputePointerPos(ship);
55             }
56             var len1=camera0.arr_enemyship.length;
57             for(var i=0;i<len1;i++)
58             {
59                 var ship=camera0.arr_enemyship[i];
60                 Campass.ComputePointerPos(ship);
61             }
62 
63 
64         }
65     )
66     scene.registerAfterRender(
67         function() {
68             MyGame.HandleNoHurry();//為了和物理引擎相合把它放在這裡?
69             var camera0=MyGame.Cameras.camera0;
70             if(MyGame.obj_keystate.q==1)
71             {
72                 camera0.rotation.z+=0.01;
73             }
74             if(MyGame.obj_keystate.e==1)
75             {
76                 camera0.rotation.z-=0.01;//同時按就相互抵消了
77             }
78         }
79     )
80     engine.runRenderLoop(function () {
81         engine.hideLoadingUI();
82         if (divFps) {
83             // Fps
84             divFps.innerHTML = engine.getFps().toFixed() + " fps";
85         }
86         //MyGame.HandleNoHurry();
87         //lastframe=new Date().getTime();
88         scene.render();
89     });
90 
91 }

1、三種迴圈

在互動式3D場景中需要週期性做的事大概有三類:

一是必不可少的渲染迴圈,在這方面Babylon.js已經為我們做好了準備。在Babylon.js中每次渲染都可以分為BeforeRender(26-65)、render(80-89)、AfterRender(66-79)三個階段,你可以在每個階段的行內函式裡新增需要在每一幀的對應階段執行的程式碼,主執行緒按照一定的頻率(一般為60HZ)執行渲染迴圈。因為引擎只會在一幀裡的所有程式碼執行完畢後執行下一幀,所以如果某一幀內的程式碼執行時間+顯示卡渲染時間超過了1/60s,則下一幀的執行將會被延遲,進而導致場景幀率降低。另外,值得注意的是registerBeforeRender和registerAfterRender並沒有必要一定和engine.runRenderLoop寫在一起,這裡這樣做只是為了程式規整,如果需要完全可以在你喜歡的地方註冊多個register,正如Rocket2.js裡所作的一樣。

二是每隔一段時間做一次的低耗時任務,比如顯示飛船當前的位置或者輸出飛船當前的線速度,我們完全沒有必要每一幀都做這些事,每一秒鐘做一次就是很好的選擇。為此我編寫了Nohurry方法:

 1 //Game.js
 2 Game.prototype={
 3     AddNohurry:function(name,delay,lastt,todo,count)//新增週期性任務
 4     {
 5         var _this=this;
 6        
 7         var len=_this.list_nohurry.length;
 8         if(len==0)
 9         {
10             _this.list_nohurry.push({delay:delay,lastt:lastt,todo:todo,name:name
11                 ,count:count})
12         }
13         else {
14             for(var i=0;i<len;i++)
15             {
16                 var obj_nohurry=_this.list_nohurry[i];
17                 if(obj_nohurry.name==name)//如果已經有同名任務
18                 {
19                     return;
20                 }
21                 if(delay>obj_nohurry.delay)//如果新任務耗時更長
22                 {
23                     continue;
24                 }
25                 else {
26                     _this.list_nohurry.splice(i,0,{delay:delay,lastt:lastt,todo:todo,name:name
27                         ,count:count});
28                     break;
29                 }
30             }
31         }
32 
33     },
34     RemoveNohurry:function(name)
35     {
36         //delete this.list_nohurry[name];
37     },
38     HandleNoHurry:function()//執行週期性任務
39     {
40         var _this=this;
41         if( _this.flag_startr==0)//開始渲染並且地形初始化完畢!!
42         {
43             engine.hideLoadingUI();
44             _this.flag_startr=1;
45             _this.lastframet=new Date().getTime();
46             _this.firstframet=_this.lastframet;
47             _this.DeltaTime=0;
48         }
49         else
50         {
51             _this.currentframet=new Date().getTime();
52             _this.DeltaTime=_this.currentframet-_this.lastframet;//取得兩幀之間的時間
53             _this.lastframet=_this.currentframet;
54             /*_this.nohurry+=_this.DeltaTime;
55 
56             if(MyGame&&_this.nohurry>1000)//每一秒進行一次導航修正
57             {
58                 _this.nohurry=0;
59 
60             }*/
61             //var time_start=_this.currentframet-_this.firstframet;//當前時間到最初過了多久
62             for(var i=0;i<_this.list_nohurry.length;i++)
63             {
64                 var obj_nohurry=_this.list_nohurry[i];
65                 if(obj_nohurry.lastt==0)
66                 {
67                     obj_nohurry.lastt=new Date().getTime();
68                 }
69                 else
70                 {
71                     var time_start=_this.currentframet-obj_nohurry.lastt;
72                     if(time_start>obj_nohurry.delay)//如果經過的時間超過了每次執行週期乘以執行次數加一,則執行一次
73                     {
74                         obj_nohurry.todo();
75                         obj_nohurry.count++;
76                         obj_nohurry.lastt=_this.currentframet;
77                         //改變策略,把耗時操作放到work執行緒裡執行,再主執行緒執行所有任務,包括呼叫work執行緒
78                         //break;//每一幀最多隻做一個費時任務,週期更短的任務放在佇列前面,獲得更多執行機會
79                     }
80                 }
81 
82             }
83             if(_this.flag_starta==1)//如果開始進行ai計算,否則只處理和基本ui有關的內容
84             {
85 
86             }
87         }
88     }
89 }

這段程式碼的思路是在MyGame中維護一個任務陣列list_nohurry和當前時間,同時在數組裡的每個任務中維護一個上次執行時間,在渲染迴圈的每一幀進行檢查,如果當前時間-上次執行時間>=任務執行週期則執行對應的任務。

三是有時需要做的耗時較長可能拖慢主執行緒的任務(比如複雜的AI運算)可以使用html5 workers執行緒處理這種任務:

 1 function initAI()//接下來新增懶惰雷達和工質噴射控制-》雷達耗時較少,且對主執行緒有變數要求,所以放在nohurry裡面
 2 {
 3     MyGame.worker=new Worker("AIThread.js");
 4     MyGame.worker.postMessage("start");
 5 
 6     MyGame.worker.onmessage=function(event)
 7     {
 8         console.log(event.data);
 9     }
10 
11 }

 

AIThread.js:(放在html的同目錄)

 1 var flag_thinking=false;
 2 var time_now=0;
 3 var time_last=0;
 4 
 5 onmessage=function(event)
 6 {
 7     var data=event.data;
 8     if(data=="start"&&!flag_thinking)
 9     {
10         flag_thinking=true;
11         //console.log("開始思考");
12         Think()
13     }
14     else if(data=="stop"){
15         flag_thinking=false;
16         close();
17     }
18 }
19 function Think()
20 {
21     if(flag_thinking)
22     {//如果正在思考
23         time_now=new Date().getTime();
24         if(time_last!=0)
25         {
26             if(time_now-time_last>1000)//每一秒執行一次
27             {
28                 time_last=time_now;
29                 //console.log(time_now);
30                 postMessage(t