基于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联网功能。计划最终编写出一个类似战舰世界的宇宙飞船模拟程序。