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

  • 2019 年 10 月 3 日
  • 筆記

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

可以訪問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(time_now);  31             }  32         }  33         else{  34             time_last=time_now;  35         }  36  37         requestAnimationFrame(Think);//不可用window但可用requestAnimationFrame!  38     }  39  40 }

 

耗時較長的任務可以放在這個線程里和主線程分開執行,如此可以充分利用多線程計算機的計算力降低主線程渲染延遲。

但需要注意的是,從概念定義上講線程之間應該可以相互訪問對方的內存,但出於保護線程安全的目的Chrome限制了主線程和workers線程之間的內存調用,workers線程操作

 window、document等主線程固有對象會報錯,訪問主線程命名空間中的自建對象則為undefiend,用戶只能通過postMessage方法在線程之間相互發送信息或者使用navigator對象共享數據,當然也可以通過網絡或者session、localStorage共享數據,但速度可能更慢。

本demo程序中並沒有長耗時任務,所以workers線程什麼也沒做。

 2、在scene.registerBeforeRender里設置每一幀根據相機的姿態反轉羅盤,使得羅盤能夠正確指示相機當前方向(33-42),同時刷新飛船在羅盤上的指示物。

 在scene.registerAfterRender里設置了對按鍵q和e的相應,如果這一幀內按鍵狀態為按下則左右傾斜相機,這裡的傾斜有兩種思路:一是如demo中每一幀變化固定的角度,缺點是幀數變化會導致操縱效果變化,二是用角度變化速度乘以本幀時間,缺點是不利於斷點調試程序。

八、總結:以上完成了宇宙飛船模擬的一些技術驗證,下一步將使用網格拼裝出“更像”一些的飛船,並編寫一些飛船內部的處理邏輯(如能量分配、模塊耐久度),編寫一套專用的火箭控制UI,再添加WebSocket聯網功能。計劃最終編寫出一個類似戰艦世界的宇宙飛船模擬程序。