计划做一个宇宙飞船模拟程序,首先做一些技术准备。
可以访问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> 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 17 <script src="../../JS/MYLIB/newland.js">script> 18 <script src="../../JS/MYLIB/CREATE_XHR.js">script> 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) 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 ) 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 ) 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> 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) 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 ) 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 ) 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 ) 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) 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联网功能。计划最终编写出一个类似战舰世界的宇宙飞船模拟程序。