基于Babylon.js编写宇宙飞船模拟程序1——程序基础结构、物理引擎使用、三维罗盘
计划做一个宇宙飞船模拟程序,首先做一些技术准备。
可以访问https://ljzc002.github.io/test/Spacetest/HTML/PAGE/spacetestwp2.html查看测试场景,按住qe键可以左右倾斜相机。可以在https://github.com/ljzc002/ljzc002.github.io/tree/master/test/Spacetest查看程序代码,因时间有限,github上的代码可能和本文中的代码有少许出入。
主要内容:
一、程序基础结构
二、场景初始化
三、地形初始化
四、事件初始化
五、UI初始化
六、单位初始化
七、主循环初始化
八、总结
一、程序基础结构:
入口文件spacetestwp2.html代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>三种物理引擎的加速度效果对比测试</title>
<link href="../../CSS/newland.css" rel="stylesheet">
<script src="../../JS/LIB/babylon.max.js"></script><!--Babylon.js主库,这里包含了babylon格式的模型导入,但不包含gltf等其他格式模型的导入,包含了后期处理库-->
<script src="../../JS/LIB/babylon.gui.min.js"></script><!--gui库-->
<script src="../../JS/LIB/babylonjs.loaders.min.js"></script><!--模型导入库合集-->
<script src="../../JS/LIB/babylonjs.materials.min.js"></script><!--材质库合集,包括基于shader的水流、火焰等高级预设材质-->
<script src="../../JS/LIB/earcut.min.js"></script><!--用来在平面网格上“挖洞”的库-->
<script src="../../JS/LIB/babylonjs.proceduralTextures.min.js"></script><!--程序纹理库合集-->
<script src="../../JS/LIB/oimo.min.js"></script><!--oimo物理引擎库-->
<script src="../../JS/LIB/ammo.js"></script><!--ammo物理引擎库-->
<script src="../../JS/LIB/cannon.js"></script><!--cannon物理引擎库-->
<!--script src="../../JS/LIB/dat.gui.min.js"></script--><!--官方比较喜欢用的一套html ui库,和WebGL3D无关-->
<script src="../../JS/MYLIB/newland.js"></script><!--自己编写的辅助工具库-->
<script src="../../JS/MYLIB/CREATE_XHR.js"></script><!--自己编写的AJAX库--> </head>
<body>
<div id="div_allbase">
<canvas id="renderCanvas"></canvas>
<div id="fps" style="z-index: 302;"></div>
</div>
</body>
<script>
var VERSION=1.0,AUTHOR="lz_newland@163.com";
var machine/*设备信息*/,canvas/*html5画布标签*/,engine/*Babylon.js引擎*/,scene/*Babylon场景*/,gl/*底层WebGL对象*/,MyGame/*用来存储各种变量*/;
canvas = document.getElementById("renderCanvas");
engine = new BABYLON.Engine(canvas, true);
engine.displayLoadingUI();
gl=engine._gl;
scene = new BABYLON.Scene(engine);
var divFps = document.getElementById("fps");/*用来显示每秒帧数的标签*/ window.onload=beforewebGL;
function beforewebGL()
{
MyGame=new Game(0,"first_pick","","","","");
initWs(webGLStart,"no");//离线测试,不使用WebSocket
//webGLStart();
}
function webGLStart()
{//是否有必要严格控制初始化流程的同步性?
initScene();//初始化基础场景,包括光照、相机对象
initArena();//初始化地形,要包括出生点、可放置区域(6*9)
initEvent();//初始化事件
initUI();//初始化场景UI
initObj();//初始化一开始存在的可交互的物体
initLoop();//初始化渲染循环
initAI();//初始化AI计算任务
MyGame.init_state=1;
engine.hideLoadingUI();
}
</script>
<script src="../../JS/PAGE/SpaceTest/WsHandler.js"></script>
<script src="../../JS/PAGE/SpaceTest/SpaceTest2.js"></script>
<script src="../../JS/MYLIB/Game.js"></script>
<script src="../../JS/PAGE/SpaceTest/Control.js"></script>
<script src="../../JS/PAGE/SpaceTest/FullUI.js"></script>
<script src="../../JS/PAGE/SpaceTest/Campass.js"></script>
<script src="../../JS/PAGE/CHARACTER/Rocket2.js"></script>
</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类的代码如下:
Game=function(init_state,flag_view,wsUri,h2Uri,userid,wsToken)
{
var _this = this;
this.scene=scene;
this.loader = new BABYLON.AssetsManager(scene);;//资源管理器
//控制者数组
this.arr_allplayers=null;
this.arr_myplayers={};
this.arr_webplayers={};
this.arr_npcs={};
//this.player={};//对于world用户这两者相等?
//this.player.arr_units=[];//这些不在这里设置,在initscene中设置
this.world={};
this.world.arr_units=[];
//this.arr_
this.count={};
this.count.count_name_npcs=0;
this.Cameras={};
this.ws=null;
this.lights={};
this.fsUI=BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI("ui1");/*全屏UI*/
this.hl=new BABYLON.HighlightLayer("hl1", scene);
this.hl.blurVerticalSize = 1.0;//这个影响的并不是高光的粗细程度,而是将它分成 多条以产生模糊效果,数值表示多条间的间隙尺寸
this.hl.blurHorizontalSize =1.0;
this.hl.innerGlow = false;
this.hl.alphaBlendingMode=3;
//this.hl.isStroke=true;
//this.hl.blurTextureSizeRatio=2;
//this.hl.mainTextureFixedSize=100;
//this.hl.renderingGroupId=3;
//this.hl._options.mainTextureRatio=1000; this.wsUri=wsUri;
this.wsConnected=false;
this.init_state=init_state;//当前运行状态
/*0-startWebGL
1-WebGLStarted
2-PlanetDrawed
* */
this.h2Uri=h2Uri;
//我是谁
this.WhoAmI=userid;//newland.randomString(8);
this.userid=userid;
this.wsToken=wsToken;
//this.arr_webplayers this.materials={};/*预设材质*/
var mat_frame = new BABYLON.StandardMaterial("mat_frame", scene);
mat_frame.wireframe = true;
//mat_frame.useLogarithmicDepth = true;
mat_frame.freeze();
this.materials.mat_frame=mat_frame;
var mat_red=new BABYLON.StandardMaterial("mat_red", scene);
mat_red.diffuseColor = new BABYLON.Color3(1, 0, 0);
//mat_red.useLogarithmicDepth = true;
mat_red.freeze();
var mat_green=new BABYLON.StandardMaterial("mat_green", scene);
mat_green.diffuseColor = new BABYLON.Color3(0, 1, 0);
//mat_green.useLogarithmicDepth = true;
mat_green.freeze();
var mat_blue=new BABYLON.StandardMaterial("mat_blue", scene);
mat_blue.diffuseColor = new BABYLON.Color3(0, 0, 1);
mat_blue.freeze();
var mat_black=new BABYLON.StandardMaterial("mat_black", scene);
mat_black.diffuseColor = new BABYLON.Color3(0, 0, 0);
//mat_black.useLogarithmicDepth = true;
mat_black.freeze();
var mat_orange=new BABYLON.StandardMaterial("mat_orange", scene);
mat_orange.diffuseColor = new BABYLON.Color3(1, 0.5, 0);
//mat_orange.useLogarithmicDepth = true;
mat_orange.freeze();
var mat_yellow=new BABYLON.StandardMaterial("mat_yellow", scene);
mat_yellow.diffuseColor = new BABYLON.Color3(1, 1, 0);
//mat_yellow.useLogarithmicDepth = true;
mat_yellow.freeze();
var mat_gray=new BABYLON.StandardMaterial("mat_gray", scene);
mat_gray.diffuseColor = new BABYLON.Color3(0.5, 0.5, 0.5);
//mat_gray.useLogarithmicDepth = true;
mat_gray.freeze();
this.materials.mat_red=mat_red;
this.materials.mat_green=mat_green;
this.materials.mat_blue=mat_blue;
this.materials.mat_black=mat_black;
this.materials.mat_orange=mat_orange;
this.materials.mat_yellow=mat_yellow;
this.materials.mat_gray=mat_gray; this.models={};/*预设模型*/
this.textures={};/*预设纹理*/
this.textures["grained_uv"]=new BABYLON.Texture("../../ASSETS/IMAGE/grained_uv.png", scene);//磨砂表面
this.texts={}; this.flag_startr=0;//开始渲染并且地形初始化完毕
this.flag_starta=0;
this.list_nohurry=[];
this.nohurry=0;//一个计时器,让一些计算不要太频繁
this.flag_online=false;
this.flag_view=flag_view;//first/third/input/free
this.flag_controlEnabled = false;
this.arr_keystate=[];
this.obj_keystate={};
this.SpriteManager=new BABYLON.SpriteManager("treesManagr", "../../ASSETS/IMAGE/CURSOR/palm.png", 2000, 100, scene);/*预设粒子生成器*/
this.SpriteManager.renderingGroupId=2;
this.obj_ground={};//存放地面对象(地形)
this.arr_startpoint=[];//场景的所有出生点
this.currentarea=null;
}
这里预定义了一些变量,以方便之后通过MyGame对象调用,其中一些变量对于这次的宇宙飞船模拟并没有作用,可以根据实际需求对它们进行增减。
需要考量的是47到86行建立预设材质的代码,其中mat_frame.useLogarithmicDepth = true;表示将该材质的深度计算改为对数形式,这种设置可以有效避免平面相互贴近时的闪烁现象和过于遥远物体的深度计算溢出问题,但Babylon.js中的一些功能(如程序纹理和粒子系统)并不支持这一设置,这时同一渲染组中的非对数深度材质将总是显示在对数深度材质的后面,所以要根据场景的具体需求决定是否使用对数深度材质。
c、46-52行依次对模拟程序各个方面进行初始化。(初始化流程参考自《Windows游戏编程大师技巧》和《WebGL入门指南》)
二、场景初始化:
initScene方法代码如下:(在SpaceTest2.js文件中)
function initScene()
{
console.log("初始化宇宙场景");
var light1 = new BABYLON.HemisphericLight("hemi", new BABYLON.Vector3(0, 50, 100), scene);//光照
light1.diffuseColor = new BABYLON.Color3(0, 10, 10); var camera0= new BABYLON.UniversalCamera("FreeCamera", new BABYLON.Vector3(0, 0, -10), scene);//由FreeCamera改为新版本的“通用相机”,据说可以默认支持各种操作设备。
camera0.minZ=0.001;//视锥体近平面距离,如果物体距相机的距离小于这个数值,物体将因为脱离视锥体而不可见
camera0.attachControl(canvas,true);
//camera0.speed=50;
scene.activeCameras.push(camera0); MyGame.player={//将一些可能用到的变量保存到MyGame对象的player属性中
name:MyGame.userid,//显示的名字
id:MyGame.userid,//WebSocket Sessionid
camera:camera0,
methodofmove:"free",
mesh:new BABYLON.Mesh("mesh_"+MyGame.userid,scene),
cardinhand:[],
arr_units:[],
handpoint:new BABYLON.Mesh("mesh_handpoint_"+MyGame.userid,scene),
scal:5,
};
MyGame.player.handpoint.position=new BABYLON.Vector3(0,-14,31);
MyGame.player.handpoint.parent=MyGame.player.mesh;
MyGame.Cameras.camera0=camera0;
//启用物理引擎
//var physicsPlugin =new BABYLON.CannonJSPlugin(false);
//var physicsPlugin = new BABYLON.OimoJSPlugin(false);
var physicsPlugin = new BABYLON.AmmoJSPlugin();
physicsPlugin.setTimeStep(1/120);
var physicsEngine = scene.enablePhysics(new BABYLON.Vector3(0, 0.1, 0.2), physicsPlugin);//重力new BABYLON.Vector3(0, 0.1, 0.2)
}
Babylon.js默认支持三种物理引擎Cannon.js、Oimo.js、Ammo.js,也支持绑定自定义物理引擎。这里简单对比一下三种默认支持的物理引擎:
建议根据实际需求选择使用何种物理引擎。
三、地形初始化:
initArena方法代码如下:
function initArena()
{
console.log("初始化地形");
var skybox = BABYLON.Mesh.CreateBox("skyBox", 1500.0, scene);//尺寸存在极限,设为15000后显示异常
var skyboxMaterial = new BABYLON.StandardMaterial("skyBox", scene);
skyboxMaterial.backFaceCulling = false;
skyboxMaterial.reflectionTexture = new BABYLON.CubeTexture("../../ASSETS/IMAGE/SKYBOX/nebula", scene);
skyboxMaterial.reflectionTexture.coordinatesMode = BABYLON.Texture.SKYBOX_MODE;
skyboxMaterial.diffuseColor = new BABYLON.Color3(0, 0, 0);
skyboxMaterial.specularColor = new BABYLON.Color3(0, 0, 0);
skyboxMaterial.disableLighting = true;
skybox.material = skyboxMaterial;
skybox.renderingGroupId = 1;
skybox.isPickable=false;
skybox.infiniteDistance = true; //三个参照物
var mesh_base=new BABYLON.MeshBuilder.CreateSphere("mesh_base",{diameter:10},scene);
mesh_base.material=MyGame.materials.mat_frame;
mesh_base.position.x=0;
mesh_base.renderingGroupId=2;
//mesh_base.layerMask=2;
var mesh_base1=new BABYLON.MeshBuilder.CreateSphere("mesh_base1",{diameter:10},scene);
mesh_base1.position.y=100;
mesh_base1.position.x=0;
mesh_base1.material=MyGame.materials.mat_frame;
mesh_base1.renderingGroupId=2;
//mesh_base1.layerMask=2;
var mesh_base2=new BABYLON.MeshBuilder.CreateSphere("mesh_base2",{diameter:10},scene);
mesh_base2.position.y=-100;
mesh_base2.position.x=0;
mesh_base2.material=MyGame.materials.mat_frame;
mesh_base2.renderingGroupId=2;
}
这是一个空旷的宇宙空间,除了天空盒与参照物没有别的东西
四、事件初始化
事件初始化代码如下:
function initEvent()
{
console.log("初始化控制事件");
InitMouse();
window.addEventListener("resize", function () {
if (engine) {
engine.resize();
}
},false);
window.addEventListener("keydown", onKeyDown, false);//按键按下
window.addEventListener("keyup", onKeyUp, false);//按键抬起
}
//Control.js
1 function InitMouse()
{
canvas.addEventListener("blur",function(evt){//监听失去焦点
releaseKeyState();
})
canvas.addEventListener("focus",function(evt){//监听获得焦点
releaseKeyState();
}) }
//注意考虑到手机平台,在正式使用时以没有键盘为考虑
function onKeyDown(event)
{
var key=event.key
MyGame.obj_keystate[key]=1;
}
function onKeyUp(event)
{
var key=event.key
MyGame.obj_keystate[key]=0;
}
function releaseKeyState()
{
for(key in MyGame.obj_keystate)
{
MyGame.obj_keystate[key]=0;
}
}
考虑到用户可能使用触屏设备,这里没有添加对“光标锁定”(canvas.requestPointerLock)的支持,并且计划未来将键盘监听改为窗口上的gui按钮。
五、UI初始化
1、UI初始化代码如下:
function initUI()
{
console.log("初始化全局UI");
MakeFullUI(MyGame.Cameras.camera0);
}
//FullUI.js
function MakeFullUI(camera0)
{
var node_z=new BABYLON.TransformNode("node_z",scene);
node_z.position.z=32;
node_z.parent=camera0;
var node_y=new BABYLON.TransformNode("node_y",scene);
node_y.position.z=32;
node_y.position.y=13;
node_y.parent=camera0;
var node_x=new BABYLON.TransformNode("node_x",scene);
node_x.position.z=32;
node_x.position.x=28;
node_x.parent=camera0; //绘制罗盘
var compassz = Campass.MakeRingZ(12,36,0,0.5,node_z);
var compassy = Campass.MakeRingY(28,36,0,1,node_y);
var compassx = Campass.MakeRingX(12,36,0,1,node_x); camera0.node_z=node_z;
camera0.node_y=node_y;
camera0.node_x=node_x;
camera0.compassz=compassz;
camera0.compassy=compassy;
camera0.compassx=compassx; camera0.arr_myship=[];
camera0.arr_friendship=[];
camera0.arr_enemyship=[]; }
2、UI阶段需要解决的一个问题是如何显示相机在三维空间中的姿态,经过思考决定在相机前部建立一个与相机同步运动的三维罗盘:
//Campass.js 建立非通用性的罗盘,因为这不是一个可以大量实例化的类,所以不放在CHARACTER路径里
var Campass={};
Campass.MakeRingX=function(radius,sumpoint,posx,sizec,parent){
var lines_x=[];
var arr_point=[];
var radp=Math.PI*2/sumpoint;
for(var i=0.0;i<sumpoint;i++)
{
var x=posx||0;
var rad=radp*i;
var y=radius*Math.sin(rad);
var z=radius*Math.cos(rad);
var pos=new BABYLON.Vector3(x,y,z)
arr_point.push(pos);
var pos2=pos.clone();
pos2.x-=sizec;
lines_x.push([pos,pos2]);
var node=new BABYLON.Mesh("node_X"+rad,scene);
node.parent=parent;
node.position=pos2;
var label = new BABYLON.GUI.Rectangle("label_X"+rad);
label.background = "black";
label.height = "14px";
label.alpha = 0.5;
label.width = "36px";
//label.cornerRadius = 20;
label.thickness = 0;
//label.linkOffsetX = 30;//位置偏移量??
MyGame.fsUI.addControl(label);
label.linkWithMesh(node);
var text1 = new BABYLON.GUI.TextBlock();
text1.text = Math.round((rad/Math.PI)*180)+"";
text1.color = "white";
label.addControl(text1);
label.isVisible=true;
label.text=text1; }
arr_point.push(arr_point[0].clone());//首尾相连,
lines_x.push(arr_point);
var compassx = new BABYLON.MeshBuilder.CreateLineSystem("compassx",{lines:lines_x,updatable:false},scene);
compassx.renderingGroupId=2;
compassx.color=new BABYLON.Color3(0, 1, 0);
compassx.useLogarithmicDepth = true;//这句应该没用
//compassx.position=node_x.position.clone();
compassx.parent=parent;
compassx.mainpath=arr_point;
compassx.sumpoint=sumpoint;
compassx.radius=radius;
return compassx;
} Campass.MakeRingY=function(radius,sumpoint,posy,sizec,parent){
var lines_y=[];
var arr_point=[];
var radp=Math.PI*2/sumpoint;
for(var i=0.0;i<sumpoint;i++)
{
var y=posy||0;
var rad=radp*i;
var z=radius*Math.sin(rad);
var x=radius*Math.cos(rad);
var pos=new BABYLON.Vector3(x,y,z)
arr_point.push(pos);
var pos2=pos.clone();
pos2.y-=sizec;
lines_y.push([pos,pos2]);
var node=new BABYLON.Mesh("node_Y"+rad,scene);
node.parent=parent;
node.position=pos2;
var label = new BABYLON.GUI.Rectangle("label_Y"+rad);
label.background = "black";
label.height = "14px";
label.alpha = 0.5;
label.width = "36px";
//label.cornerRadius = 20;
label.thickness = 0;
//label.linkOffsetX = 30;//位置偏移量??
MyGame.fsUI.addControl(label);
label.linkWithMesh(node);//对TransformNode使用会造成定位异常
var text1 = new BABYLON.GUI.TextBlock();
var num=Math.round((rad/Math.PI)*180);
if(num>=90)
{
num-=90;
}
else
{
num+=270;
}
text1.text = num+"";
text1.color = "white";
label.addControl(text1);
label.isVisible=true;
label.text=text1;
}
arr_point.push(arr_point[0].clone());//首尾相连,
lines_y.push(arr_point);
var compassy = new BABYLON.MeshBuilder.CreateLineSystem("compassy",{lines:lines_y,updatable:false},scene);
compassy.renderingGroupId=2;
compassy.color=new BABYLON.Color3(0, 1, 0);
compassy.useLogarithmicDepth = true;
//compassy.position=node_y.position.clone();
compassy.parent=parent;
compassy.mainpath=arr_point;
compassy.sumpoint=sumpoint;
compassy.radius=radius;
return compassy;
} Campass.MakeRingZ=function(radius,sumpoint,posz,sizec,parent){
var lines_z=[];
var arr_point=[];
var radp=Math.PI*2/sumpoint;
parent.arr_node=[];
for(var i=0.0;i<sumpoint;i++)
{
var z=posz||0;
var rad=radp*i;
var x=radius*Math.sin(rad);
var y=radius*Math.cos(rad);
var pos=new BABYLON.Vector3(x,y,z);
arr_point.push(pos);
var pos2=pos.clone();
pos2.normalizeFromLength(radius/(radius-sizec));//里面的数字表示坐标值除以几
lines_z.push([pos,pos2]);
var node=new BABYLON.Mesh("node_Z"+rad,scene);
node.parent=parent;
node.position=pos2;
parent.arr_node.push(node);
var label = new BABYLON.GUI.Rectangle("label_Z"+rad);
label.background = "black";
label.height = "14px";
label.alpha = 0.5;
label.width = "36px";
//label.cornerRadius = 20;
label.thickness = 0;
label.rotation=rad;
label.startrot=rad;
//label.linkOffsetX = 30;//位置偏移量??
MyGame.fsUI.addControl(label);
label.linkWithMesh(node);
var text1 = new BABYLON.GUI.TextBlock();
text1.text = Math.round((rad/Math.PI)*180)+"";//不显式转换会报错
text1.color = "white";
label.addControl(text1);
label.isVisible=true;
label.text=text1;
node.label=label;
}
arr_point.push(arr_point[0].clone());//首尾相连,
lines_z.push(arr_point);
var compassz = new BABYLON.MeshBuilder.CreateLineSystem("compassz",{lines:lines_z,updatable:false},scene);
compassz.renderingGroupId=2;
compassz.color=new BABYLON.Color3(0, 1, 0);
compassz.useLogarithmicDepth = true;
compassz.parent=parent;
compassz.mainpath=arr_point;
compassz.sumpoint=sumpoint;
compassz.radius=radius;
return compassz;
}
罗盘的主体是三个圆环,圆环上有表示角度的刻度和数字,其结构示意图如下:
图一
图二
图中白色四棱锥表示相机的视锥体,compassx和compassy距相机较近的半圆正好在视锥体以外,故不可见。关于相机姿态改变时罗盘如何运动,将在初始化循环中介绍。另外也许可以将compassx和compassy的一圈设为720度,这样就可以在视野中看到所有角度的情况,或者使用类似html走马灯的gui代替立体罗盘,时间有限并未测试这些思路。
应该在屏幕顶部和右侧的中间添加两个指针,这样将能够更精确的指出当前角度,计划下个版本添加。
3、这里再说一点和Babylon.js视锥体有关的内容,Babylon.js官方文档里很少提及视锥体的属性和设置方法(似乎是封装在相机的投影矩阵方法里),于是自己编写代码测试视锥体属性:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>改为直接用顶点构造视锥体</title>
<link href="../../CSS/newland.css" rel="stylesheet">
<script src="../../JS/LIB/babylon.min.js"></script><!--这里包含了babylon格式的模型导入,但不包含gltf等其他格式,包含了后期处理-->
<script src="../../JS/LIB/babylon.gui.min.js"></script>
<script src="../../JS/LIB/babylonjs.loaders.min.js"></script>
<script src="../../JS/LIB/babylonjs.materials.min.js"></script>
<script src="../../JS/LIB/earcut.min.js"></script>
<script src="../../JS/LIB/babylonjs.proceduralTextures.min.js"></script>
<script src="../../JS/LIB/oimo.min.js"></script>
<script src="../../JS/LIB/ammo.js"></script>
<script src="../../JS/LIB/cannon.js"></script>
<script src="../../JS/LIB/dat.gui.min.js"></script>
<script src="../../JS/MYLIB/newland.js"></script>
<script src="../../JS/MYLIB/CREATE_XHR.js"></script>
</head>
<body>
<div id="div_allbase">
<canvas id="renderCanvas"></canvas>
<div id="fps" style="z-index: 302;"></div>
</div>
</body>
<script>
var VERSION=1.0,AUTHOR="lz_newland@163.com";
var machine,canvas,engine,scene,gl,MyGame;
canvas = document.getElementById("renderCanvas");
engine = new BABYLON.Engine(canvas, true);
engine.displayLoadingUI();
gl=engine._gl;
scene = new BABYLON.Scene(engine);
var divFps = document.getElementById("fps"); window.onload=beforewebGL;
function beforewebGL()
{
webGLStart();
}
function webGLStart()
{
createScene();
//scene.debugLayer.show();
MyBeforeRender();
}
var createScene = function () {
camera0= new BABYLON.UniversalCamera("FreeCamera", new BABYLON.Vector3(0, 0, 0), scene);//FreeCamera
camera0.minZ=0.001;
camera0.attachControl(canvas,true);
scene.activeCameras.push(camera0); var light1 = new BABYLON.HemisphericLight("hemi", new BABYLON.Vector3(0, 1, 0), scene);
light1.diffuse = new BABYLON.Color3(1,1,1);//这道“颜色”是从上向下的,底部收到100%,侧方收到50%,顶部没有
light1.specular = new BABYLON.Color3(0,0,0);
light1.groundColor = new BABYLON.Color3(1,1,1);//这个与第一道正相反 var skybox = BABYLON.Mesh.CreateBox("skyBox", 1500.0, scene);
var skyboxMaterial = new BABYLON.StandardMaterial("skyBox", scene);
skyboxMaterial.backFaceCulling = false;
skyboxMaterial.reflectionTexture = new BABYLON.CubeTexture("../../ASSETS/IMAGE/SKYBOX/nebula", scene);
skyboxMaterial.reflectionTexture.coordinatesMode = BABYLON.Texture.SKYBOX_MODE;
skyboxMaterial.diffuseColor = new BABYLON.Color3(0, 0, 0);
skyboxMaterial.specularColor = new BABYLON.Color3(0, 0, 0);
skyboxMaterial.disableLighting = true;
skybox.material = skyboxMaterial;
skybox.renderingGroupId = 1;
skybox.isPickable=false;
skybox.infiniteDistance = true; var mat_frame = new BABYLON.StandardMaterial("mat_frame", scene);
mat_frame.wireframe = true;
//测试视锥体
var vertexData= new BABYLON.VertexData();
var w=50;//锥体底部矩形宽度的一半
var h=60;//锥体底部到视点的距离
var r=0.5;//锥体底部矩形的高宽比
var positions=[0,0,0,-w,w*r,h,-w,-w*r,h,w,-w*r,h,w,w*r,h];
var uvs=[0.5,0.5,0,0,0,1,1,1,1,0];
var normals=[];
var indices=[0,1,2,0,2,3,0,3,4,0,4,1];
BABYLON.VertexData.ComputeNormals(positions, indices, normals);//计算法线
BABYLON.VertexData._ComputeSides(0, positions, indices, normals, uvs);
vertexData.indices = indices.concat();//索引
vertexData.positions = positions.concat();
vertexData.normals = normals.concat();//position改变法线也要改变!!!!
vertexData.uvs = uvs.concat(); var mesh=new BABYLON.Mesh(name,scene);
vertexData.applyToMesh(mesh, true);
mesh.vertexData=vertexData;
mesh.renderingGroupId=2;
mesh.material=mat_frame; var node_z=new BABYLON.TransformNode("node_z",scene);
node_z.position.z=32;
//node_z.parent=camera0;
var node_y=new BABYLON.TransformNode("node_y",scene);
node_y.position.z=32;
node_y.position.y=13;
//node_y.parent=camera0;
var node_x=new BABYLON.TransformNode("node_x",scene);
node_x.position.z=32;
node_x.position.x=28;
//node_x.parent=camera0;
//绘制罗盘
var compassz = Campass.MakeRingZ(12,36,0,0.5,node_z);
var compassy = Campass.MakeRingY(28,36,0,1,node_y);
var compassx = Campass.MakeRingX(12,36,0,1,node_x); }
function MyBeforeRender()
{
scene.registerBeforeRender(
function(){
//camera0.position.x=0;
//camera0.position.y=0;
}
)
scene.registerAfterRender(
function() {
}
)
engine.runRenderLoop(function () {
engine.hideLoadingUI();
if (divFps) {
// Fps
divFps.innerHTML = engine.getFps().toFixed() + " fps";
}
//lastframe=new Date().getTime();
scene.render();
});
}
var Campass={};
Campass.MakeRingX=function(radius,sumpoint,posx,sizec,parent){
var lines_x=[];
var arr_point=[];
var radp=Math.PI*2/sumpoint;
for(var i=0.0;i<sumpoint;i++)
{
var x=posx||0;
var rad=radp*i;
var y=radius*Math.sin(rad);
var z=radius*Math.cos(rad);
var pos=new BABYLON.Vector3(x,y,z)
arr_point.push(pos);
var pos2=pos.clone();
pos2.x-=sizec;
lines_x.push([pos,pos2]);
var node=new BABYLON.Mesh("node_X"+rad,scene);
node.parent=parent;
node.position=pos2;
}
arr_point.push(arr_point[0].clone());//首尾相连,不能这样相连,否则变形时会多出一个顶点!!,看来这个多出的顶点无法去掉,只能在选取时额外处理它
lines_x.push(arr_point);
var compassx = new BABYLON.MeshBuilder.CreateLineSystem("compassx",{lines:lines_x,updatable:false},scene);
compassx.renderingGroupId=2;
compassx.color=new BABYLON.Color3(0, 1, 0);
compassx.useLogarithmicDepth = true;
//compassx.position=node_x.position.clone();
compassx.parent=parent;
compassx.mainpath=arr_point;
compassx.sumpoint=sumpoint;
compassx.radius=radius;
return compassx;
} Campass.MakeRingY=function(radius,sumpoint,posy,sizec,parent){
var lines_y=[];
var arr_point=[];
var radp=Math.PI*2/sumpoint;
for(var i=0.0;i<sumpoint;i++)
{
var y=posy||0;
var rad=radp*i;
var z=radius*Math.sin(rad);
var x=radius*Math.cos(rad);
var pos=new BABYLON.Vector3(x,y,z)
arr_point.push(pos);
var pos2=pos.clone();
pos2.y-=sizec;
lines_y.push([pos,pos2]);
var node=new BABYLON.Mesh("node_Y"+rad,scene);
node.parent=parent;
node.position=pos2;
}
arr_point.push(arr_point[0].clone());//首尾相连,不能这样相连,否则变形时会多出一个顶点!!,看来这个多出的顶点无法去掉,只能在选取时额外处理它
lines_y.push(arr_point);
var compassy = new BABYLON.MeshBuilder.CreateLineSystem("compassy",{lines:lines_y,updatable:false},scene);
compassy.renderingGroupId=2;
compassy.color=new BABYLON.Color3(0, 1, 0);
compassy.useLogarithmicDepth = true;
//compassy.position=node_y.position.clone();
compassy.parent=parent;
compassy.mainpath=arr_point;
compassy.sumpoint=sumpoint;
compassy.radius=radius;
return compassy;
} Campass.MakeRingZ=function(radius,sumpoint,posz,sizec,parent){
var lines_z=[];
var arr_point=[];
var radp=Math.PI*2/sumpoint;
parent.arr_node=[];
for(var i=0.0;i<sumpoint;i++)
{
var z=posz||0;
var rad=radp*i;
var x=radius*Math.sin(rad);
var y=radius*Math.cos(rad);
var pos=new BABYLON.Vector3(x,y,z);
arr_point.push(pos);
var pos2=pos.clone();
pos2.normalizeFromLength(radius/(radius-sizec));//里面的数字表示坐标值除以几
lines_z.push([pos,pos2]);
var node=new BABYLON.Mesh("node_Z"+rad,scene);
node.parent=parent;
node.position=pos2;
parent.arr_node.push(node);
}
arr_point.push(arr_point[0].clone());//首尾相连,不能这样相连,否则变形时会多出一个顶点!!,看来这个多出的顶点无法去掉,只能在选取时额外处理它
lines_z.push(arr_point);
var compassz = new BABYLON.MeshBuilder.CreateLineSystem("compassz",{lines:lines_z,updatable:false},scene);
compassz.renderingGroupId=2;
compassz.color=new BABYLON.Color3(0, 1, 0);
compassz.useLogarithmicDepth = true;
compassz.parent=parent;
compassz.mainpath=arr_point;
compassz.sumpoint=sumpoint;
compassz.radius=radius;
return compassz;
}
</script>
</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方法代码如下:
function initObj()
{//假设一单位长度对应100m
console.log("初始化单位");
var ship=new BABYLON.MeshBuilder.CreateBox("ship_target",{size:5},scene);//建立一个立方体作为飞船
ship.position=new BABYLON.Vector3(-5,0,0);
ship.material=MyGame.materials.mat_green;
ship.renderingGroupId=2;
//ship.v={x:0,y:0,z:0}
ship.physicsImpostor = new BABYLON.PhysicsImpostor(ship, BABYLON.PhysicsImpostor.BoxImpostor//SphereImpostor//
, { mass: 1, restitution: 0.0005 ,friction:0,damping:0,linearDamping:"a"}, scene);//物理仿真器
ship.physicsImpostor.damping=0;
MyGame.player.ship=ship;
//在罗盘里为这个ship添加一个标志
var camera0=MyGame.Cameras.camera0;
Campass.AddShip(camera0,"my",ship);
/*scene.onReadyObservable.add(function(){//这个应该在更早的时候执行过了!!
ship.physicsImpostor.physicsBody.linearDamping=0;
ship.physicsImpostor.physicsBody.angularDamping=0;
})*/
newland.DisposeDamping(ship);
//在左下角显示ship的当前位置
var advancedTexture = MyGame.fsUI;
var UiPanel = new BABYLON.GUI.StackPanel();
UiPanel.width = "220px";
UiPanel.fontSize = "14px";
UiPanel.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
UiPanel.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM;
UiPanel.color = "white";
advancedTexture.addControl(UiPanel);
MyGame.player.ship.label_pos=UiPanel;//所以把这个UI相关设定放在了initObj里
var text1 = new BABYLON.GUI.TextBlock();
text1.text = ""
text1.color = "white";
text1.paddingTop = "0px";
text1.width = "220px";
text1.height = "20px";
UiPanel.addControl(text1);
UiPanel.text1=text1;
var text2 = new BABYLON.GUI.TextBlock();
text2.text = ""
text2.color = "white";
text2.paddingTop = "0px";
text2.width = "220px";
text2.height = "20px";
UiPanel.addControl(text2);
UiPanel.text2=text2;
var text3 = new BABYLON.GUI.TextBlock();
text3.text = ""
text3.color = "white";
text3.paddingTop = "0px";
text3.width = "220px";
text3.height = "20px";
UiPanel.addControl(text3);
UiPanel.text3=text3; var mesh_rocket=new BABYLON.MeshBuilder.CreateCylinder("mesh_rocket"//为飞船添加一个圆锥形的火箭推进器
,{height:2,diameterTop:0.1,diameterBottom :1},scene);
mesh_rocket.renderingGroupId = 2;
mesh_rocket.material=MyGame.materials.mat_gray;
mesh_rocket.rotation=new BABYLON.Vector3(Math.PI,0,0);
mesh_rocket.position=new BABYLON.Vector3(0,-1,0);
var rocket=new Rocket();
ship.rocket=rocket;
var obj_p={ship:ship,mesh:mesh_rocket,name:"testrocket1"
,mass:1000,cost2power:function(cost){return cost*1;}
,pos:new BABYLON.Vector3(0,0,-3.5),rot:new BABYLON.Vector3(-Math.PI/2,0,0)};
rocket.init(obj_p);//初始化火箭对象
rocket.fire({firebasewidth:0.5,cost:1,firescaling:1});//发动火箭 var shipb=new BABYLON.MeshBuilder.CreateBox("ship_targetb",{size:5},scene);//再建立一个飞船作为对比
shipb.position=new BABYLON.Vector3(5,0,0);
shipb.material=MyGame.materials.mat_green;
shipb.renderingGroupId=2;
//ship.v={x:0,y:0,z:0}
shipb.physicsImpostor = new BABYLON.PhysicsImpostor(shipb, BABYLON.PhysicsImpostor.BoxImpostor//SphereImpostor//
, { mass: 1, restitution: 0.0005 ,friction:0}, scene);//物理仿真器
shipb.mass=1000000000;
MyGame.player.shipb=shipb;
//在罗盘里为这个ship添加一个标志
var camera0=MyGame.Cameras.camera0;
Campass.AddShip(camera0,"my",shipb);
newland.DisposeDamping(shipb); var mesh_rocketb=new BABYLON.MeshBuilder.CreateCylinder("mesh_rocketb"
,{height:2,diameterTop:0.1,diameterBottom :1},scene);
mesh_rocketb.renderingGroupId = 2;
mesh_rocketb.material=MyGame.materials.mat_gray;
mesh_rocketb.rotation=new BABYLON.Vector3(Math.PI,0,0);
mesh_rocketb.position=new BABYLON.Vector3(0,-1,0);
var rocketb=new Rocket();
shipb.rocket=rocketb;
var obj_pb={ship:shipb,mesh:mesh_rocketb,name:"testrocket1b"
,mass:1000,cost2power:function(cost){return cost*1;}
,pos:new BABYLON.Vector3(0,0,-3.5),rot:new BABYLON.Vector3(-Math.PI/2,0,0)};
rocketb.init(obj_pb);
rocketb.fire({firebasewidth:0.5,cost:1,firescaling:1});
}
1、首先建立了一个立方体网格代表宇宙飞船,然后在第九行为飞船设置物理仿真器。这里需要注意damping参数,这个参数表示物理引擎对加速度的无条件阻碍,默认值为0.1,与表示摩擦系数的friction参数不同,即使仿真器不与任何其他物体接触也会一直受到这一削减作用,具体表现为加速度每秒钟减少0.1直到减少为0,这也就意味着加速度小于0.1的力不会对物体造成任何影响。按照Babylon.js的设计初衷,这一属性应该能通过PhysicsImpostor的构造函数设置,但遗憾的是随着物理引擎的升级迭代,在构造函数中使用这一参数并没有任何作用,使用者必须自己前往物理引擎的底层修改这一参数(事实上是两个参数:线速度衰减和角速度衰减):
//移除网格的物理外套的默认加速度衰减
newland.DisposeDamping=function(mesh)
{
//cannon使用
mesh.physicsImpostor.physicsBody.linearDamping=0;
mesh.physicsImpostor.physicsBody.angularDamping=0;
//ammo使用
if(mesh.physicsImpostor.physicsBody.setDamping)
{
mesh.physicsImpostor.physicsBody.setDamping(0,0);
}
}
以上是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中
Campass.AddShip=function(camera0,type,ship)
{
//渲染组3突出显示
var vec_ship=ship.position.clone().subtract(camera0.position);//由视点指向飞船的向量
vec_ship=newland.VecTo2Local(vec_ship,camera0);//转化为局部坐标系坐标
var pointerz= new BABYLON.MeshBuilder.CreateSphere("pointerz_"+ship.name,{diameter:1},scene);//球体标记
pointerz.parent=camera0.compassz.parent;
pointerz.position=new BABYLON.Vector3(vec_ship.x,vec_ship.y,0).normalize().scale(camera0.compassz.radius);
pointerz.renderingGroupId=3;
ship.pointerz=pointerz;
if(type=="my")//自己控制的飞船显示为绿色
{
camera0.arr_myship.push(ship);
pointerz.material=MyGame.materials.mat_green;
}
else if(type=="friend")//友方为蓝色
{
camera0.arr_friendship.push(ship);
pointerz.material=MyGame.materials.mat_blue;
}
else if(type=="enemy")//敌方为红色
{
camera0.arr_enemyship.push(ship);
pointerz.material=MyGame.materials.mat_red;
}
var label = new BABYLON.GUI.Rectangle("label_pointerz_"+ship.name);//文本框
label.background = "black";
label.height = "14px";
label.alpha = 0.5;
label.width = "120px";
label.thickness = 0;
//label.linkOffsetX = 30;//位置偏移量??
MyGame.fsUI.addControl(label);
label.linkWithMesh(pointerz);
var text1 = new BABYLON.GUI.TextBlock();
text1.text = ship.name;
text1.color = "white";
label.addControl(text1);
label.isVisible=true;
label.text=text1;
pointerz.label=label;
}
Campass.ComputePointerPos=function(ship)//刷新飞船的方位
{
var camera0=MyGame.Cameras.camera0;
var pointerz=ship.pointerz;
var vec_ship=ship.position.clone().subtract(camera0.position);
/*var v=new BABYLON.Vector3(vec_ship.x,vec_ship.y,0)
var m = camera0.getWorldMatrix();
var v = BABYLON.Vector3.TransformCoordinates(vector, m);*/
vec_ship=newland.VecTo2Local(vec_ship,camera0);
pointerz.position=(new BABYLON.Vector3(vec_ship.x,vec_ship.y,0)).normalize().scale(camera0.compassz.radius); }
3、21到54行在屏幕左下角建立三个文本框显示飞船的位置。
4、56到68行为飞船添加了一个火箭推进器,Rocket类在Rocket2.js文件中:
//工质发动机(粒子系统版,低粒子量、低亮度、低闪烁)
Rocket=function()
{ }
Rocket.prototype.init=function(param)
{
param = param || {};
this.name=param.name;
this.ship=param.ship;
this.node=new BABYLON.TransformNode("node_rocket_"+this.name,scene);//用变换节点代替空网格
this.node.position=param.pos;
this.node.rotation=param.rot;
this.node.parent=this.ship;
this.mesh=param.mesh;//喷口网格,也可能只是instance
this.mesh.parent=this.node;
this.mass=param.mass;
this.ship.mass+=this.mass
this.cost2power=param.cost2power;//供能转换为推力的公式
this.cost2demage=param.cost2demage;//供能对引擎造成损坏的公式,其中包括对故障率的影响
this.hp=param.hp;
this.cost=null;//当前供能
this.power=null;//当前推力
this.failurerate=param.failurerate;//故障率参数 //this.scaling=param.scaling||1; this.rotxl=param.rotxl;//引擎在x轴上的摆动范围
this.rotyl=param.rotyl;
this.rotzl=param.rotzl; }
Rocket.prototype.fire=function(param)
{
this.cost=param.cost;
this.power=this.cost2power(this.cost);
this.firebasewidth=param.firebasewidth||1;//火焰底部的宽度
this.firescaling=param.firescaling||1;//喷射火焰尺寸 var particleSystem;
particleSystem = new BABYLON.GPUParticleSystem("particles", { capacity:50000 }, scene);//粒子系统,可用粒子为50000个
particleSystem.activeParticleCount = 50000;//活动粒子数50000
particleSystem.emitRate = 10000;//每秒发射10000个
particleSystem.particleTexture = new BABYLON.Texture("../../ASSETS/IMAGE/TEXTURES/fire/flare.png", scene);//粒子纹理
particleSystem.maxLifeTime = 10;//最大生存时间
particleSystem.minSize = 0.01//*this.firescaling;
particleSystem.maxSize = 0.1//*this.firescaling;
particleSystem.emitter = this.node; var radius = this.firebasewidth;
var angle = Math.PI;
var coneEmitter = new BABYLON.ConeParticleEmitter(radius, angle);//锥形发射器
coneEmitter.radiusRange = 1;
coneEmitter.heightRange = 0;
particleSystem.particleEmitterType = coneEmitter; particleSystem.renderingGroupId=2;
particleSystem.start();//启动粒子系统
//var force=new BABYLON.Vector3(0,-this.power*100000/this.ship.mass,0);
var force=new BABYLON.Vector3(0,-1,0);
force=newland.vecToGlobal(force,this.node);
force=force.subtract(this.node.getAbsolutePosition()).scale(this.power);
//this.ship.physicsImpostor.applyForce(force,this.node.position)//
//this.ship.physicsImpostor.applyImpulse(force,new BABYLON.Vector3(0,0,-3.5))//这个相当于只加速一秒
//this.ship.physicsImpostor.applyForce(force,new BABYLON.Vector3(0,0,-3.5))//Oimo doesn't support applying force. Using impule instead. var rocket=this;
//this.ship.physicsImpostor.applyImpulse(new BABYLON.Vector3(0,0,1),new BABYLON.Vector3(0,0,-2.5));
/*MyGame.AddNohurry("task_rocketfire_"+this.name,1000,0,//每秒执行一次
function(){
var force=new BABYLON.Vector3(0,-1,0);
force=newland.vecToGlobal(force,rocket.node);
force=force.subtract(rocket.node.getAbsolutePosition()).scale(rocket.power);
rocket.ship.physicsImpostor.applyForce(force,rocket.node.getAbsolutePosition());
},0)*/
scene.registerAfterRender(function(){//每帧渲染后执行
var force=new BABYLON.Vector3(0,-1,0);
force=newland.vecToGlobal(force,rocket.node);
force=force.subtract(rocket.node.getAbsolutePosition()).scale(rocket.power);
var pos=rocket.node.getAbsolutePosition();
rocket.ship.physicsImpostor.applyForce(force,pos);
console.log(rocket.ship.physicsImpostor.getLinearVelocity());
}) //this.ship.physicsImpostor.applyForce(force,this.node.getAbsolutePosition());//只执行一次
}
火箭推进器由一个圆锥形的喷口和从喷口喷出的粒子组成,注意第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作为对比
七、主循环初始化:
var posz_temp1=0;//上一次的位置
var posz_temp2=0;//上一次的速度
var posz_temp1b=0;//上一次的位置
var posz_temp2b=0;//上一次的速度
function initLoop()
{
console.log("初始化主循环");
var _this=MyGame;
MyGame.AddNohurry("task_logpos",1000,0,function(){//每秒钟输出一些信息并且更新飞船的位置显示
var posz=MyGame.player.ship.position.z;
var poszb=MyGame.player.shipb.position.z;
//console.log("---"+(new Date().getTime())+"\n"+posz+"_"+(posz-posz_temp1)+"_"+(posz-posz_temp1-posz_temp2)+"@"+MyGame.player.ship.physicsImpostor.getLinearVelocity()
// +"\n"+poszb+"_"+(poszb-posz_temp1b)+"_"+(poszb-posz_temp1b-posz_temp2b)+"@"+MyGame.player.shipb.physicsImpostor.getLinearVelocity());
//console.log(MyGame.player.ship.physicsImpostor.getLinearVelocity());
posz_temp2=posz-posz_temp1;
posz_temp1=posz;
posz_temp2b=poszb-posz_temp1b;
posz_temp1b=poszb;
var ship_main=MyGame.player.ship;
var UiPanel=ship_main.label_pos;
UiPanel.text1.text="x:"+ship_main.position.x;
UiPanel.text2.text="y:"+ship_main.position.y;
UiPanel.text3.text="z:"+ship_main.position.z;
},0)//name,delay,lastt,todo,count scene.registerBeforeRender(
function(){ var camera0=MyGame.Cameras.camera0;
var node_z=camera0.node_z;
var node_y=camera0.node_y;
var node_x=camera0.node_x; node_z.rotation.z=-camera0.rotation.z;//反转罗盘
var len=node_z.arr_node.length;
for(var i=0;i<len;i++)
{
var label=node_z.arr_node[i].label;
label.rotation=label.startrot+camera0.rotation.z;
}
node_y.rotation.y=-camera0.rotation.y;
node_x.rotation.x=-camera0.rotation.x;
//舰船标志更新放在每一帧里还是每秒执行一次?
var len1=camera0.arr_myship.length;
for(var i=0;i<len1;i++)
{
var ship=camera0.arr_myship[i];
Campass.ComputePointerPos(ship);
}
var len1=camera0.arr_friendship.length;
for(var i=0;i<len1;i++)
{
var ship=camera0.arr_friendship[i];
Campass.ComputePointerPos(ship);
}
var len1=camera0.arr_enemyship.length;
for(var i=0;i<len1;i++)
{
var ship=camera0.arr_enemyship[i];
Campass.ComputePointerPos(ship);
} }
)
scene.registerAfterRender(
function() {
MyGame.HandleNoHurry();//为了和物理引擎相合把它放在这里?
var camera0=MyGame.Cameras.camera0;
if(MyGame.obj_keystate.q==1)
{
camera0.rotation.z+=0.01;
}
if(MyGame.obj_keystate.e==1)
{
camera0.rotation.z-=0.01;//同时按就相互抵消了
}
}
)
engine.runRenderLoop(function () {
engine.hideLoadingUI();
if (divFps) {
// Fps
divFps.innerHTML = engine.getFps().toFixed() + " fps";
}
//MyGame.HandleNoHurry();
//lastframe=new Date().getTime();
scene.render();
}); }
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方法:
//Game.js
Game.prototype={
AddNohurry:function(name,delay,lastt,todo,count)//添加周期性任务
{
var _this=this; var len=_this.list_nohurry.length;
if(len==0)
{
_this.list_nohurry.push({delay:delay,lastt:lastt,todo:todo,name:name
,count:count})
}
else {
for(var i=0;i<len;i++)
{
var obj_nohurry=_this.list_nohurry[i];
if(obj_nohurry.name==name)//如果已经有同名任务
{
return;
}
if(delay>obj_nohurry.delay)//如果新任务耗时更长
{
continue;
}
else {
_this.list_nohurry.splice(i,0,{delay:delay,lastt:lastt,todo:todo,name:name
,count:count});
break;
}
}
} },
RemoveNohurry:function(name)
{
//delete this.list_nohurry[name];
},
HandleNoHurry:function()//执行周期性任务
{
var _this=this;
if( _this.flag_startr==0)//开始渲染并且地形初始化完毕!!
{
engine.hideLoadingUI();
_this.flag_startr=1;
_this.lastframet=new Date().getTime();
_this.firstframet=_this.lastframet;
_this.DeltaTime=0;
}
else
{
_this.currentframet=new Date().getTime();
_this.DeltaTime=_this.currentframet-_this.lastframet;//取得两帧之间的时间
_this.lastframet=_this.currentframet;
/*_this.nohurry+=_this.DeltaTime; if(MyGame&&_this.nohurry>1000)//每一秒进行一次导航修正
{
_this.nohurry=0; }*/
//var time_start=_this.currentframet-_this.firstframet;//当前时间到最初过了多久
for(var i=0;i<_this.list_nohurry.length;i++)
{
var obj_nohurry=_this.list_nohurry[i];
if(obj_nohurry.lastt==0)
{
obj_nohurry.lastt=new Date().getTime();
}
else
{
var time_start=_this.currentframet-obj_nohurry.lastt;
if(time_start>obj_nohurry.delay)//如果经过的时间超过了每次执行周期乘以执行次数加一,则执行一次
{
obj_nohurry.todo();
obj_nohurry.count++;
obj_nohurry.lastt=_this.currentframet;
//改变策略,把耗时操作放到work线程里执行,再主线程执行所有任务,包括调用work线程
//break;//每一帧最多只做一个费时任务,周期更短的任务放在队列前面,获得更多执行机会
}
} }
if(_this.flag_starta==1)//如果开始进行ai计算,否则只处理和基本ui有关的内容
{ }
}
}
}
这段代码的思路是在MyGame中维护一个任务数组list_nohurry和当前时间,同时在数组里的每个任务中维护一个上次执行时间,在渲染循环的每一帧进行检查,如果当前时间-上次执行时间>=任务执行周期则执行对应的任务。
三是有时需要做的耗时较长可能拖慢主线程的任务(比如复杂的AI运算)可以使用html5 workers线程处理这种任务:
function initAI()//接下来添加懒惰雷达和工质喷射控制-》雷达耗时较少,且对主线程有变量要求,所以放在nohurry里面
{
MyGame.worker=new Worker("AIThread.js");
MyGame.worker.postMessage("start"); MyGame.worker.onmessage=function(event)
{
console.log(event.data);
} }
AIThread.js:(放在html的同目录)
var flag_thinking=false;
var time_now=0;
var time_last=0; onmessage=function(event)
{
var data=event.data;
if(data=="start"&&!flag_thinking)
{
flag_thinking=true;
//console.log("开始思考");
Think()
}
else if(data=="stop"){
flag_thinking=false;
close();
}
}
function Think()
{
if(flag_thinking)
{//如果正在思考
time_now=new Date().getTime();
if(time_last!=0)
{
if(time_now-time_last>1000)//每一秒执行一次
{
time_last=time_now;
//console.log(time_now);
postMessage(time_now);
}
}
else{
time_last=time_now;
} requestAnimationFrame(Think);//不可用window但可用requestAnimationFrame!
} }
耗时较长的任务可以放在这个线程里和主线程分开执行,如此可以充分利用多线程计算机的计算力降低主线程渲染延迟。
但需要注意的是,从概念定义上讲线程之间应该可以相互访问对方的内存,但出于保护线程安全的目的Chrome限制了主线程和workers线程之间的内存调用,workers线程操作
window、document等主线程固有对象会报错,访问主线程命名空间中的自建对象则为undefiend,用户只能通过postMessage方法在线程之间相互发送信息或者使用navigator对象共享数据,当然也可以通过网络或者session、localStorage共享数据,但速度可能更慢。
本demo程序中并没有长耗时任务,所以workers线程什么也没做。
2、在scene.registerBeforeRender里设置每一帧根据相机的姿态反转罗盘,使得罗盘能够正确指示相机当前方向(33-42),同时刷新飞船在罗盘上的指示物。
在scene.registerAfterRender里设置了对按键q和e的相应,如果这一帧内按键状态为按下则左右倾斜相机,这里的倾斜有两种思路:一是如demo中每一帧变化固定的角度,缺点是帧数变化会导致操纵效果变化,二是用角度变化速度乘以本帧时间,缺点是不利于断点调试程序。
八、总结:以上完成了宇宙飞船模拟的一些技术验证,下一步将使用网格拼装出“更像”一些的飞船,并编写一些飞船内部的处理逻辑(如能量分配、模块耐久度),编写一套专用的火箭控制UI,再添加WebSocket联网功能。计划最终编写出一个类似战舰世界的宇宙飞船模拟程序。
基于Babylon.js编写宇宙飞船模拟程序1——程序基础结构、物理引擎使用、三维罗盘的更多相关文章
- 基于Babylon.js编写简单的骨骼动画生成器
使用骨骼动画技术可以将网格的顶点分配给若干骨头,通过给骨头设定关键帧和父子关系,可以赋予网格高度动态并具有传递性的变形 效果.这里结合之前的相关研究在网页端使用JavaScript实现了一个简单的骨骼 ...
- 3D网页小实验-基于Babylon.js与recast.js实现RTS式单位控制
一.运行效果 1.建立一幅具有地形起伏和不同地貌纹理的地图: 地图中间为凹陷的河道,两角为突出的高地,高地和低地之间以斜坡通道相连. 水下为沙土材质,沙土材质网格贴合地形,河流材质网格则保持水平. 2 ...
- HTML5之2D物理引擎 Box2D for javascript Games 系列 翻外篇--如何结合createJS应用box2d.js
太久没有更新了,新年回来工作,突然有收到网友的邮件提问,居然还有人在关注,惭愧,找了下电脑上还有一点儿存着,顺便先发这一个番外篇吧,好歹可以看到真实的效果,等我考完英语,一定会更新下一章," ...
- 用编程方式编写Babylon格式的宇宙飞船3D模型
使用上一篇文章(https://www.cnblogs.com/ljzc002/p/9353101.html)中提出的方法,编写一个简单的宇宙飞船3D模型,在这篇文章中对模型制作流程和数学计算步骤进行 ...
- Breach - HTML5 时代,基于 JS 编写的浏览器
Breach 是一款属于 HTML5 时代的开源浏览器项目,,完全用 Javascript 编写的.免费.模块化.易于扩展.这个浏览器中的一切都是模块,Web 应用程序在其自己的进程运行.通过选择合适 ...
- [译]基于Vue.js的10个最佳UI框架,用于构建移动应用程序
原文查看10 Best Vue.js based UI Frameworks for Building Mobile Apps 如果您期待使用Vue.js构建移动应用程序,那么您可以选择许多可用的UI ...
- 微信小程序实战篇:基于wxcharts.js绘制移动报表
前言 微信小程序图表插件(wx-charts)是基于canvas绘制,体积小巧,支持图表类型饼图.线图.柱状图 .区域图等图表图形绘制,目前wx-charts是微信小程序图表插件中比较强大好使的一个. ...
- 基于Node.js+MySQL开发的开源微信小程序B2C商城(页面高仿网易严选)
界面高仿网易严选商城(主要是2016年wap版) 测试数据采集自网易严选商城 功能和数据库参考ecshop 服务端api基于Node.js+ThinkJS+MySQL 计划添加基于Vue.js的后台管 ...
- 网页3D引擎“Babylon.JS”入门教程翻译总结
使用三个月的业余时间把官方教程的入门部分译为中文并上传到github,在下一步编程前做一个总结. 历程: 最早接触游戏编程是在大三下学期,用汇编语言和实验室里的单片机.触摸屏.电机(提供声效)编的打地 ...
随机推荐
- Docker学习总结(七)--Docker私有仓库
创建私有仓库 1) 拉取私有仓库镜像 docker pull registry 2)启动私有仓库容器 docker run -di --name-registry -p 5000:5000 regis ...
- YARN底层基础库
YARN基础库是其他一切模块的基础,它的设计直接决定了YARN的稳定性和扩展性,YARN借用了MRV1的一些底层基础库,比如RPC库等,但因为引入了很多新的软件设计方式,所以它的基础库更多,包括直 ...
- HDU 6299
题意略. 思路: 我们先把所有字符串处理成 ")))((((" 这样的形式.然后我们把这些字符串排个序,按照min(l,r)来排,小的在前,在我的代码中,l为 ( 的个数,r 为 ...
- 2020届京东秋招正式批一面记录-Java开发-2019.08.31
京东一面总结 总共时间持续时间约40分钟 1.你用过集合类里面哪些是线程安全的,哪些是线程不安全的?分别举两个例子? 线程安全:HashTable以及ConcurrentHashMap 非线程安全:A ...
- MSIL实用指南-生成内部类
生成内部类用TypeBuilder的DefineNestedType方法,得到另一个TypeBuilder.内部类的可访问性都是TypeAttributes的“Nested”开头一些成员.实例代码:y ...
- Jmeter 从数据库查询多个字段,依次传给登录接口怎么实现?
问题背景: 博文“Jmeter 如何把数据库的数据依次获取作为参数传入下一个请求?附栗子”某天有人留言如下: 看了下当时写的文章,如果从数据库查询多个字段,依次传给登录接口,确实不能合理实现,所以,特 ...
- 最简单流处理引擎——Kafka Streaming简介
Kafka在0.10.0.0版本以前的定位是分布式,分区化的,带备份机制的日志提交服务.而kafka在这之前也没有提供数据处理的顾服务.大家的流处理计算主要是还是依赖于Storm,Spark Stre ...
- JMeter更改语言为英文
1. 进入目录apache-jmeter-2.13\bin 2. 打开jmeter.properties 3. 取消“language=en”前的注释 4. 重新打开JMeter,即可看到语言已经变成 ...
- xcode7中搭建python开发环境
1. 双击打开Xcode 2. 点击File->New->New Project 3. 在左边的面板选择Other,右边选择External Build Sytem,点击Next 4. 输 ...
- codeforce303C-Minimum Modular-剪枝,暴力
Minimum Modular 题意:就是在一堆数字中,每一个数字对m取模不能等于这堆数字中的其他数字,同时给了K个机会可以删除一些数字.求最小的m: 思路:我一开始完全没思路,队长说的并查集什么的不 ...