网页端3D编程小实验-一种即时战略游戏的编程原型
本文尝试基于Babylon.js引擎(以下简称bbl)和recast2导航库,采用“经典代码组织方式”编写一个可作为即时战略(以下简称rts)游戏编程原型的程序,该程序收集了进行此类开发所需的离线依赖包,总结了该类程序的一种页面和程序结构设计方法,并且基于这些依赖和方法实现了简单的地图构建、单位控制、受地形影响的群组导航功能。
一、代码地址与运行效果
该程序在https://github.com/ljzc002/A-rts-game-fream/tree/main基于MIT许可证开源,它是https://www.bilibili.com/opus/1128695548365242388项目的一个组成部分,运行/html/WH-admin.html可打开基础测试页面:

页面上半部为负责3D渲染的canvas区域,下部为可自定义设计UI的原生html区域,该页面基于百分比定位方法设计,可适用于多种分辨率场景,考虑到兼容移动设备,在canvas区也设计了一些gui按钮以代替键盘和鼠标滚轮功能。
canvas区域中渲染了一张简单的方形地图,地图四周为128*128泥土地,中间为64*64的林地,林地中间的白点为可移动单位的标志物,周围的白色边界为围墙。滚动鼠标滚轮可控制视角上下移动,左键拖动鼠标可同步拖动地图(显然,在不添加额外按钮时,左键拖动地图操作与“左键框选单位”相冲突,需根据实际需要取舍,如需框选单位功能可参考此文中的实现:https://juejin.cn/post/6968635340110168101),按o键可切换为bbl的自由相机视角,按i恢复为rts视角,以下是林地和标志物的侧视图:

rts视角下,右键点击地面则全部标志物将向点击位置移动,移动过程中各个标志物之间将自动保持距离,在林地中移动时的速度为泥土地的40%。
接下来两章先介绍bbl框架离线依赖的手动组织方法,如果读者已经熟悉bbl使用,可跳到第四章。
二、编程框架搭建
1、为什么基于经典方法组织
所谓“基于经典方法组织”指不依赖npm、typescript、umi等需要“编译”环节的框架,手动将依赖包配置在程序的特定位置,在实验性编程中这样作的好处包括:易于调试运行时代码、易于修改第三方依赖包、减少网络依赖性、专注于功能本身而非框架、加深对程序结构了解等。如果开发者需要使用流行的编译框架进行代码组织,则这些基于经典方法组织的代码,也可以容易的转为各种框架下的代码或嵌入到框架中(反之则很难)。
2、手动下载bbl依赖包并离线部署
首先随意打开一个官方训练场场景,例如基础演示场景https://playground.babylonjs.com/#WJXQP0:

左边红框处显示当前最新的bbl版本,例如图中为8.36.1,右边红框为下载按钮,需注意的是,这里下载的只是该训练场的索引html页面,而该页面中包含bbl依赖库的实际下载地址:
<!-- Babylon globals (kept so window.BABYLON is available if your app expects it) --> <script src="https://assets.babylonjs.com/generated/Assets.js"></script> <script src="https://cdn.babylonjs.com/recast.js"></script> <script src="https://cdn.babylonjs.com/ammo.js"></script> <script src="https://cdn.babylonjs.com/havok/HavokPhysics_umd.js"></script> <script src="https://cdn.babylonjs.com/cannon.js"></script> <script src="https://cdn.babylonjs.com/Oimo.js"></script> <script src="https://cdn.babylonjs.com/earcut.min.js"></script> <script src="https://cdn.babylonjs.com/babylon.js"></script> <script src="https://cdn.babylonjs.com/materialsLibrary/babylonjs.materials.min.js"></script> <script src="https://cdn.babylonjs.com/proceduralTexturesLibrary/babylonjs.proceduralTextures.min.js"></script> <script src="https://cdn.babylonjs.com/postProcessesLibrary/babylonjs.postProcess.min.js"></script> <script src="https://cdn.babylonjs.com/loaders/babylonjs.loaders.js"></script> <script src="https://cdn.babylonjs.com/serializers/babylonjs.serializers.min.js"></script> <script src="https://cdn.babylonjs.com/gui/babylon.gui.min.js"></script> <script src="https://cdn.babylonjs.com/addons/babylonjs.addons.min.js"></script> <script src="https://cdn.babylonjs.com/inspector/babylon.inspector.bundle.js"></script>
从上到下,其功能分别为:
官方示例资源地址(无用)
群组导航库地址(旧版,被替代)
ammo物理引擎(较旧,但功能基本可用)
havok物理引擎(较新,官方推荐使用)
cannon物理引擎(较旧,存在bug)
oimo物理引擎(较旧,存在bug)
“挖洞库”(体积不大,建议保留)
核心库
额外材质库(如水、火、毛绒等,按需加载)
程序纹理库(按需加载)
后期处理库(按需加载)
模型加载库(记忆中bbl默认可加载obj和bbl格式模型,如需加载更多格式模型需使用此库)
序列化库(可用来将bbl生成的场景导出为各种格式的三维模型)
gui库(用来在页面中绘制gui按钮,建议保留)
额外内容库(一些额外添加的功能,例如本文所使用的recast2导航库即通过此库加载)
调试库(bbl官方的页面调试工具)
在浏览器中访问对应的url即可下载相应的依赖库,需要注意的是,其中一些库内部又会引用其他依赖库,例如havok物理引擎库会引用havok.wasm文件,此时可通过浏览器调试工具获取其引用地址并进行下载,其中模型加载库的简介依赖包最多,可根据所需加载的模型类别选择性配置。还有个别依赖项缺少对离线原生部署的兼容,需手动修改其代码。
此次实验的离线依赖包包含以下内容:

图中@recast-navigation目录为通过mybabylonjs.addons.min.js文件加载的recast2导航库。
项目整体目录结构为:

其中assets中为图片、音频等资源,lib中为依赖库。
3、入口网页
WH-admin.html文件为程序的入口网页,其代码如下:
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>管理端包含actionloop,负责推演所有单位,并按视野将推演结论发送给player,同时接收player传来的有效指令</title> 6 <style> 7 html, body { 8 overflow: hidden; 9 width: 100%; 10 height: 100%; 11 margin: 0; 12 padding: 0; 13 } 14 #renderCanvas { 15 width: 100%; 16 height: 70%; 17 touch-action: none; 18 } 19 #fps { position: absolute; right: 20px; top: 5em; font-size: 20px; color: white;/*帧数显示*/ 20 text-shadow: 2px 2px 0 black;} 21 #div_bottom div{ 22 position: absolute; 23 } 24 .div_btn{ 25 font-size: 20px;line-height: 20px;position: absolute;cursor:crosshair; 26 } 27 .div_htmlmesh{ 28 font-size: 100%;line-height: 100%;position: absolute;border:1px solid;top:-50%;left:-50% 29 } 30 .mod_group,.mod_unit{ 31 font-size: 100%;line-height: 100%;position: absolute; 32 } 33 </style> 34 <script src="./lib/bbl8c/babylon.js"></script> 35 <script src="./lib/bbl8c/babylon.inspector.bundle.js"></script> 36 <script src="./lib/bbl8c/babylon.gui.min.js"></script> 37 <script src="./lib/bbl8c/mybabylonjs.addons.min.js"></script> 38 <script src="./lib/bbl8c/HavokPhysics_umd.js"></script> 39 <script src="./lib/bbl8c/earcut.min.js"></script> 40 <script src="./lib/newland.js"></script> 41 <script src="./lib/nohurry.js"></script> 42 <script src="./lib/VTools.js"></script> 43 <!--<script src="./lib/sqliteClient.js"></script>--> 44 <script src="./lib/ControlWH.js"></script> 45 <script src="./lib/ControlGui.js"></script> 46 <script src="./lib/ControlCrowd.js"></script> 47 <!--<script src="./lib/drawStellaris6.js"></script>--> 48 <script src="./lib/cyhz.js"></script> 49 <script src="./lib/drawSprites.js"></script> 50 <script src="./lib/createMap.js"></script> 51 <!--<script src="./lib/drawGalaxy6.js"></script>--> 52 <!--<script src="./lib/drawStar.js"></script>--> 53 <!--<script src="./lib/drawPlanet.js"></script>--> 54 </head> 55 <body> 56 <canvas id="renderCanvas" touch-action="none"></canvas> 57 <div id="fps" style="z-index: 302;"></div> 58 <div id="div_bottom" style="z-index: 301;position: absolute;bottom: 0px;width: 100%;height: 30%;font-size: 12px;border-top: 1px solid"> 59 60 </div> 61 <div id="div_comment" style="z-index: 302;position:fixed;display: none;width: 200px;height: 400px;left: 0px;bottom:0px;border: 1px solid black"> 62 </div> 63 <div id="btn_comment" style="z-index: 303;position:fixed;display: block;width: 20px;height: 80px;left: 0px;bottom:150px;text-align: center; 64 background-color:gainsboro;cursor: pointer;border: 1px solid black" 65 onclick="showComment()">提<br/>示<br/>》 66 </div> 67 <div id="div_hidden" style="display: none"> 68 69 </div> 70 </body> 71 <script> 72 console.log(new Date().getTime()); 73 var dir_lib_header="\.\/lib/"; 74 var flag_runningstate="等待场景初始化"; 75 var VERSION=1.0,AUTHOR="1113908055@qq.com"; 76 var machine,canvas,engine,scene,gl,MyGame,camera0,camera_minimap,experience; 77 //var camera_x,camera_x2,camera_y,camera_y2,camera_z,camera_z2;//用来生成即时天空盒 78 machine=navigator; 79 canvas = document.getElementById("renderCanvas"); 80 var div_bottom=document.getElementById("div_bottom"); 81 var divFps = document.getElementById("fps"); 82 var div_hidden= document.getElementById("div_hidden"); 83 var flag_webgpu=false; 84 85 var one=null;//如果有第一人称或第三人称控制的核心对象 86 var int_z=0;//这个值越大代表距离越近,显示的svg细节越仔细,规定在小于-2时减少显示细节 87 var int0=1,int0b=1; 88 var divFps = document.getElementById("fps"); 89 window.onload=beforewebGL; 90 91 async function beforewebGL() 92 { 93 engine=new BABYLON.Engine(canvas, true, { preserveDrawingBuffer: true, stencil: true, disableWebGL2Support: false}); 94 scene = new BABYLON.Scene(engine); 95 Init(); 96 } 97 async function Init() 98 { 99 await initScene(); 100 await initArena(); 101 InitMouse(); 102 initGuiControl(); 103 webGLStart2(); 104 } 105 var mat_global={},hd_camera0=Math.PI/2; 106 var initpos_camera0,initrot_camera0; 107 async function initScene() 108 { 109 //scene.clearColor=new BABYLON.Color3(0,0,0); 110 var light0 = new BABYLON.HemisphericLight("light0", new BABYLON.Vector3(0, 1, 0), scene); 111 light0.diffuse = new BABYLON.Color3(1,1,1);//这道“颜色”是从上向下的,底部收到100%,侧方收到50%,顶部没有 112 light0.specular = new BABYLON.Color3(0,0,0); 113 light0.groundColor = new BABYLON.Color3(1,1,1);//这个与第一道正相反 114 115 camera0= new BABYLON.UniversalCamera("FreeCamera", new BABYLON.Vector3(0, 600, 0), scene); 116 camera0.rotation.x=hd_camera0;//需要45度斜向下视角 117 camera0.maxZ=20000; 118 camera0.minZ=0.1; 119 //scene.activeCameras.push(camera0); 120 camera0.speed=1; 121 camera0.myRotation={x:0,y:0,z:0}; 122 initpos_camera0=camera0.position.clone(); 123 initrot_camera0=camera0.rotation.clone(); 124 var node_hand=new BABYLON.TransformNode("hand"); 125 node_hand.position.z=1; 126 node_hand.parent=camera0; 127 camera0.node_hand=node_hand; 128 var node_pick=new BABYLON.TransformNode("pick"); 129 node_pick.position.z=100; 130 node_pick.parent=camera0; 131 camera0.node_pick=node_pick; 132 scene.activeCameras = [camera0]; 133 var points1=[new BABYLON.Vector3(0,0,0),new BABYLON.Vector3(0.01,0,0)]; 134 var points2=[new BABYLON.Vector3(0,0,0),new BABYLON.Vector3(-0.01,0,0)]; 135 var points3=[new BABYLON.Vector3(0,0,0),new BABYLON.Vector3(0,0.01,0)]; 136 var points4=[new BABYLON.Vector3(0,0,0),new BABYLON.Vector3(0,-0.01,0)]; 137 var lines=new BABYLON.MeshBuilder.CreateLineSystem("LineSystem",{lines:[points1,points2,points3,points4]}); 138 lines.isPickable=false; 139 lines.parent=node_hand; 140 lines.isVisible=false; 141 lines.renderingGroupId=3; 142 lines.color="green"; 143 camera0.lines=lines; 144 lines.alwaysSelectAsActiveMesh=true; 145 146 var mat_green=new BABYLON.StandardMaterial("mat_green", scene); 147 mat_green.diffuseColor = new BABYLON.Color3(0, 1, 0); 148 mat_green.freeze(); 149 mat_global.mat_green=mat_green; 150 var mat_frame=new BABYLON.StandardMaterial("mat_frame", scene); 151 mat_frame.wireframe=true; 152 mat_frame.freeze(); 153 mat_global.mat_frame=mat_frame; 154 var mat_white_e=new BABYLON.StandardMaterial("mat_white_e", scene); 155 mat_white_e.disableLighting = true; 156 mat_white_e.emissiveColor = BABYLON.Color3.White(); 157 mat_white_e.backFaceCulling=false; 158 mat_white_e.freeze(); 159 mat_global.mat_white_e=mat_white_e; 160 } 161 var skybox; 162 var HK,physicsPlugin,observable; 163 //碰撞过滤器 164 var filter_group_l=1; 165 var filter_group_r=2; 166 var filter_group_g=4; 167 var filter_group_a=8; 168 var navigationPlugin,navMesh,navMeshQuery; 169 async function initArena() 170 { 171 // var skybox = BABYLON.Mesh.CreateBox("skyBox", 1500.0, scene);//尺寸存在极限,设为15000后显示异常 172 // var skyboxMaterial = new BABYLON.StandardMaterial("skyBox", scene); 173 // skyboxMaterial.backFaceCulling = false; 174 // skyboxMaterial.reflectionTexture = new BABYLON.CubeTexture("./assets/image/SKYBOX/skybox", scene); 175 // skyboxMaterial.reflectionTexture.coordinatesMode = BABYLON.Texture.SKYBOX_MODE; 176 // skyboxMaterial.diffuseColor = new BABYLON.Color3(0, 0, 0); 177 // skyboxMaterial.specularColor = new BABYLON.Color3(0, 0, 0); 178 // skyboxMaterial.disableLighting = true; 179 // skybox.material = skyboxMaterial; 180 // skybox.renderingGroupId = 1; 181 // skybox.isPickable=false; 182 // skybox.infiniteDistance = true; 183 184 HK=await HavokPhysics(); 185 physicsPlugin = new BABYLON.HavokPlugin(); 186 scene.enablePhysics(new BABYLON.Vector3(0, -9.8, 0), physicsPlugin); 187 observable = physicsPlugin.onCollisionObservable; 188 var observer = observable.add((collisionEvent) => { 189 //fight0(collisionEvent);//碰撞事件 190 }); 191 192 193 194 var mesh_ground = BABYLON.MeshBuilder.CreateGround("ground", {width: 128, height: 128}, scene); 195 mesh_ground.alwaysSelectAsActiveMesh = true; 196 //mesh_ground.renderingGroupId=2;//renderingGroupId会与htmlmesh冲突吗? 197 mesh_ground.position.x=0; 198 mesh_ground.position.z=0; 199 var mat = new BABYLON.StandardMaterial("mat_ground", scene);//1 200 mat.disableLighting = true; 201 mat.emissiveTexture = new BABYLON.Texture("./assets/image/LANDTYPE/terre.png", scene); 202 mat.emissiveTexture.uScale = 8; 203 mat.emissiveTexture.vScale = 8; 204 mat.freeze(); 205 mat_global.mat_ground=mat; 206 mesh_ground.material = mat; 207 mesh_ground.physicsImpostor=new BABYLON.PhysicsAggregate(mesh_ground, BABYLON.PhysicsShapeType.MESH 208 , { mass: 0, restitution: 0.1 ,friction:0.9,move:false,margin:0}, scene); 209 mesh_ground.physicsImpostor.shape.filterMembershipMask=filter_group_g; 210 mesh_ground.myType="ground"; 211 mesh_ground.myType1="base"; 212 //mesh_ground.physicsImpostor.shape.filterCollideMask=filter_group_g; 213 //四周的围栏,它们是一直存在的,随着用户的操作还会生成一些临时的围栏 214 var mesh_ground1 = BABYLON.MeshBuilder.CreateGround("ground1", {width: 128, height: 10}, scene); 215 mesh_ground1.position.x=0; 216 mesh_ground1.position.z=64; 217 mesh_ground1.rotation.x=Math.PI/2; 218 mesh_ground1.physicsImpostor=new BABYLON.PhysicsAggregate(mesh_ground, BABYLON.PhysicsShapeType.MESH 219 , { mass: 0, restitution: 0.1 ,friction:0.9,move:false,margin:0}, scene); 220 mesh_ground1.physicsImpostor.body.mesh_ground=mesh_ground; 221 mesh_ground1.physicsImpostor.shape.filterMembershipMask=filter_group_g; 222 mesh_ground1.material=mat_global.mat_white_e; 223 mesh_ground1.sideOrientation=BABYLON.Mesh.DOUBLESIDE; 224 var mesh_ground2 = BABYLON.MeshBuilder.CreateGround("ground2", {width: 128, height: 10}, scene); 225 mesh_ground2.position.x=0; 226 mesh_ground2.position.z=-64; 227 mesh_ground2.rotation.x=Math.PI/2; 228 mesh_ground2.physicsImpostor=new BABYLON.PhysicsAggregate(mesh_ground, BABYLON.PhysicsShapeType.MESH 229 , { mass: 0, restitution: 0.1 ,friction:0.9,move:false,margin:0}, scene); 230 mesh_ground2.physicsImpostor.body.mesh_ground=mesh_ground; 231 mesh_ground2.physicsImpostor.shape.filterMembershipMask=filter_group_g; 232 mesh_ground2.material=mat_global.mat_white_e; 233 mesh_ground2.sideOrientation=BABYLON.Mesh.DOUBLESIDE; 234 var mesh_ground3 = BABYLON.MeshBuilder.CreateGround("ground3", {width: 10, height: 128}, scene); 235 mesh_ground3.position.x=-64; 236 mesh_ground3.position.z=0; 237 mesh_ground3.rotation.z=Math.PI/2; 238 mesh_ground3.physicsImpostor=new BABYLON.PhysicsAggregate(mesh_ground, BABYLON.PhysicsShapeType.MESH 239 , { mass: 0, restitution: 0.1 ,friction:0.9,move:false,margin:0}, scene); 240 mesh_ground3.physicsImpostor.body.mesh_ground=mesh_ground; 241 mesh_ground3.physicsImpostor.shape.filterMembershipMask=filter_group_g; 242 mesh_ground3.material=mat_global.mat_white_e; 243 mesh_ground3.sideOrientation=BABYLON.Mesh.DOUBLESIDE; 244 var mesh_ground4 = BABYLON.MeshBuilder.CreateGround("ground4", {width: 10, height: 128}, scene); 245 mesh_ground4.position.x=64; 246 mesh_ground4.position.z=0; 247 mesh_ground4.rotation.z=Math.PI/2; 248 mesh_ground4.physicsImpostor=new BABYLON.PhysicsAggregate(mesh_ground, BABYLON.PhysicsShapeType.MESH 249 , { mass: 0, restitution: 0.1 ,friction:0.9,move:false,margin:0}, scene); 250 mesh_ground4.physicsImpostor.body.mesh_ground=mesh_ground; 251 mesh_ground4.physicsImpostor.shape.filterMembershipMask=filter_group_g; 252 mesh_ground4.material=mat_global.mat_white_e; 253 mesh_ground4.sideOrientation=BABYLON.Mesh.DOUBLESIDE; 254 initCrowd([mesh_ground]); 255 initMap(); 256 257 } 258 259 function webGLStart2() 260 { 261 flag_runningstate="场景初始化完成"; 262 //sr_global= new BABYLON.SnapshotRenderingHelper(scene); 263 createManagers(); 264 265 //scene.debugLayer.show(); 266 //initStellaris(); 267 MyBeforeRender0(); 268 //scene.debugLayer.show(); 269 } 270 271 272 273 var flag_comment=false; 274 var div_comment=document.getElementById("div_comment"); 275 var btn_comment=document.getElementById("btn_comment"); 276 function showComment() 277 { 278 if(!flag_comment) 279 { 280 div_comment.style.display="block"; 281 btn_comment.style.left="201px"; 282 btn_comment.innerHTML="提<br/>示<br/>《"; 283 } 284 else { 285 div_comment.style.display="none"; 286 btn_comment.style.left="0px"; 287 btn_comment.innerHTML="提<br/>示<br/>》"; 288 } 289 flag_comment=!flag_comment; 290 } 291 </script> 292 </html>
其中的入口代码为:
window.onload=beforewebGL; async function beforewebGL() { engine=new BABYLON.Engine(canvas, true, { preserveDrawingBuffer: true, stencil: true, disableWebGL2Support: false}); scene = new BABYLON.Scene(engine); Init(); }
此处使用bbl的WebGL模式进行场景渲染,如追求更快的渲染速度可在此处判断运行环境是否支持WebGPU技术,如支持则使用WebGPU渲染,这里考虑到当前版本的WebGPU服务必须在https协议下发布,采用更易部署的WebGL模式。
本实验的初始化方法包括5个方面的初始化操作:
async function Init() { await initScene(); await initArena(); InitMouse(); initGuiControl(); webGLStart2(); }
以下将分别详细介绍每一项初始化操作,如程序有更多功能,则可在这里添加更多初始化方法,如WebSocket初始化、程序纹理初始化、数据库初始化等。
三、初始化操作
1、场景初始化
代码如下:
async function initScene() { //scene.clearColor=new BABYLON.Color3(0,0,0); var light0 = new BABYLON.HemisphericLight("light0", new BABYLON.Vector3(0, 1, 0), scene); light0.diffuse = new BABYLON.Color3(1,1,1);//这道“颜色”是从上向下的,底部收到100%,侧方收到50%,顶部没有 light0.specular = new BABYLON.Color3(0,0,0); light0.groundColor = new BABYLON.Color3(1,1,1);//这个与第一道正相反 camera0= new BABYLON.UniversalCamera("FreeCamera", new BABYLON.Vector3(0, 600, 0), scene); camera0.rotation.x=hd_camera0;//需要垂直向下视角 camera0.maxZ=20000; camera0.minZ=0.1; //scene.activeCameras.push(camera0); camera0.speed=1; camera0.myRotation={x:0,y:0,z:0}; initpos_camera0=camera0.position.clone(); initrot_camera0=camera0.rotation.clone(); var node_hand=new BABYLON.TransformNode("hand"); node_hand.position.z=1; node_hand.parent=camera0; camera0.node_hand=node_hand; var node_pick=new BABYLON.TransformNode("pick"); node_pick.position.z=100; node_pick.parent=camera0; camera0.node_pick=node_pick; scene.activeCameras = [camera0]; var points1=[new BABYLON.Vector3(0,0,0),new BABYLON.Vector3(0.01,0,0)]; var points2=[new BABYLON.Vector3(0,0,0),new BABYLON.Vector3(-0.01,0,0)]; var points3=[new BABYLON.Vector3(0,0,0),new BABYLON.Vector3(0,0.01,0)]; var points4=[new BABYLON.Vector3(0,0,0),new BABYLON.Vector3(0,-0.01,0)]; var lines=new BABYLON.MeshBuilder.CreateLineSystem("LineSystem",{lines:[points1,points2,points3,points4]}); lines.isPickable=false; lines.parent=node_hand; lines.isVisible=false; lines.renderingGroupId=3; lines.color="green"; camera0.lines=lines; lines.alwaysSelectAsActiveMesh=true; var mat_green=new BABYLON.StandardMaterial("mat_green", scene); mat_green.diffuseColor = new BABYLON.Color3(0, 1, 0); mat_green.freeze(); mat_global.mat_green=mat_green; var mat_frame=new BABYLON.StandardMaterial("mat_frame", scene); mat_frame.wireframe=true; mat_frame.freeze(); mat_global.mat_frame=mat_frame; var mat_white_e=new BABYLON.StandardMaterial("mat_white_e", scene); mat_white_e.disableLighting = true; mat_white_e.emissiveColor = BABYLON.Color3.White(); mat_white_e.backFaceCulling=false; mat_white_e.freeze(); mat_global.mat_white_e=mat_white_e; }
包括对全局光源(本次实验采用自发光材质)、相机以及相机附属物(包括相机的准星、相机周围的参考点,还可能包括小地图、代表玩家自身的模型等)、全局材质的初始化
2、场地初始化
async function initArena() { // 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/skybox", 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; HK=await HavokPhysics(); physicsPlugin = new BABYLON.HavokPlugin(); scene.enablePhysics(new BABYLON.Vector3(0, -9.8, 0), physicsPlugin); observable = physicsPlugin.onCollisionObservable; var observer = observable.add((collisionEvent) => { //fight0(collisionEvent);//碰撞事件 }); var mesh_ground = BABYLON.MeshBuilder.CreateGround("ground", {width: 128, height: 128}, scene); mesh_ground.alwaysSelectAsActiveMesh = true; //mesh_ground.renderingGroupId=2;//renderingGroupId会与htmlmesh冲突吗? mesh_ground.position.x=0; mesh_ground.position.z=0; var mat = new BABYLON.StandardMaterial("mat_ground", scene);//1 mat.disableLighting = true; mat.emissiveTexture = new BABYLON.Texture("./assets/image/LANDTYPE/terre.png", scene); mat.emissiveTexture.uScale = 8; mat.emissiveTexture.vScale = 8; mat.freeze(); mat_global.mat_ground=mat; mesh_ground.material = mat; mesh_ground.physicsImpostor=new BABYLON.PhysicsAggregate(mesh_ground, BABYLON.PhysicsShapeType.MESH , { mass: 0, restitution: 0.1 ,friction:0.9,move:false,margin:0}, scene); mesh_ground.physicsImpostor.shape.filterMembershipMask=filter_group_g; mesh_ground.myType="ground"; mesh_ground.myType1="base"; //mesh_ground.physicsImpostor.shape.filterCollideMask=filter_group_g; //四周的围栏,它们是一直存在的,随着用户的操作还会生成一些临时的围栏 var mesh_ground1 = BABYLON.MeshBuilder.CreateGround("ground1", {width: 128, height: 10}, scene); mesh_ground1.position.x=0; mesh_ground1.position.z=64; mesh_ground1.rotation.x=Math.PI/2; mesh_ground1.physicsImpostor=new BABYLON.PhysicsAggregate(mesh_ground, BABYLON.PhysicsShapeType.MESH , { mass: 0, restitution: 0.1 ,friction:0.9,move:false,margin:0}, scene); mesh_ground1.physicsImpostor.body.mesh_ground=mesh_ground; mesh_ground1.physicsImpostor.shape.filterMembershipMask=filter_group_g; mesh_ground1.material=mat_global.mat_white_e; mesh_ground1.sideOrientation=BABYLON.Mesh.DOUBLESIDE; var mesh_ground2 = BABYLON.MeshBuilder.CreateGround("ground2", {width: 128, height: 10}, scene); mesh_ground2.position.x=0; mesh_ground2.position.z=-64; mesh_ground2.rotation.x=Math.PI/2; mesh_ground2.physicsImpostor=new BABYLON.PhysicsAggregate(mesh_ground, BABYLON.PhysicsShapeType.MESH , { mass: 0, restitution: 0.1 ,friction:0.9,move:false,margin:0}, scene); mesh_ground2.physicsImpostor.body.mesh_ground=mesh_ground; mesh_ground2.physicsImpostor.shape.filterMembershipMask=filter_group_g; mesh_ground2.material=mat_global.mat_white_e; mesh_ground2.sideOrientation=BABYLON.Mesh.DOUBLESIDE; var mesh_ground3 = BABYLON.MeshBuilder.CreateGround("ground3", {width: 10, height: 128}, scene); mesh_ground3.position.x=-64; mesh_ground3.position.z=0; mesh_ground3.rotation.z=Math.PI/2; mesh_ground3.physicsImpostor=new BABYLON.PhysicsAggregate(mesh_ground, BABYLON.PhysicsShapeType.MESH , { mass: 0, restitution: 0.1 ,friction:0.9,move:false,margin:0}, scene); mesh_ground3.physicsImpostor.body.mesh_ground=mesh_ground; mesh_ground3.physicsImpostor.shape.filterMembershipMask=filter_group_g; mesh_ground3.material=mat_global.mat_white_e; mesh_ground3.sideOrientation=BABYLON.Mesh.DOUBLESIDE; var mesh_ground4 = BABYLON.MeshBuilder.CreateGround("ground4", {width: 10, height: 128}, scene); mesh_ground4.position.x=64; mesh_ground4.position.z=0; mesh_ground4.rotation.z=Math.PI/2; mesh_ground4.physicsImpostor=new BABYLON.PhysicsAggregate(mesh_ground, BABYLON.PhysicsShapeType.MESH , { mass: 0, restitution: 0.1 ,friction:0.9,move:false,margin:0}, scene); mesh_ground4.physicsImpostor.body.mesh_ground=mesh_ground; mesh_ground4.physicsImpostor.shape.filterMembershipMask=filter_group_g; mesh_ground4.material=mat_global.mat_white_e; mesh_ground4.sideOrientation=BABYLON.Mesh.DOUBLESIDE; initCrowd([mesh_ground]); initMap(); }
包括对天空盒、物理引擎(可选)、地面网格的初始化,本次实验中还包括对导航群组和游戏地图的初始化。
此处还需注意两点:
a、bbl默认支持四级renderingGroupId,一般可用0级渲染隐形物体,1级渲染无限远处物体,2级渲染普通物体,3级渲染强调物体。但本实验预计使用htmlMesh的特性与renderingGroupId属性冲突,故注释renderingGroupId属性配置。
b、物理引擎与导航网格的物理模拟功能相似,区别在于前者适用于需计算多个物体多维度相互碰撞作用的场景,后者则适用于多个物体相互阻挡进行寻路的场景,一般的模拟程序选用其中一种即可,但也可以根据实际需要同时使用这两种基于物理效果的模拟。
3、控制初始化
InitMouse方法位于ControlWH.js文件中:
1 var node_temp,rate_fov,rate_screen; 2 var sr_global=null; 3 var pos_stack_one,pos_stack_rts; 4 function InitMouse() 5 { 6 rate_fov=Math.tan(camera0.fov/2)*2; 7 var sizex=engine.getRenderWidth()//_gl.drawingBufferWidth; 8 var sizey=engine.getRenderHeight()//_gl.drawingBufferHeight; 9 rate_screen=sizex/sizey; 10 canvas.addEventListener("blur",function(evt){//监听失去焦点 11 releaseKeyStateOut(); 12 }) 13 canvas.addEventListener("focus",function(evt){//改为监听获得焦点,因为调试失去焦点时事件的先后顺序不好说 14 releaseKeyStateIn(); 15 }) 16 // canvas.addEventListener("click", function(evt) {//这个监听也会在点击GUI按钮时触发!!<-click仅指鼠标左键单击??!! 17 // onMouseClick(evt);// 18 // }, false); 19 // canvas.addEventListener("dblclick", function(evt) {//是否要用到鼠标双击?? 20 // onMouseDblClick(evt);// 21 // }, false); 22 canvas.addEventListener("pointermove",onMouseMove,false) 23 canvas.addEventListener("pointerdown",onMouseDown,false) 24 canvas.addEventListener("pointerup",onMouseUp,false) 25 window.addEventListener("keydown", onKeyDown, false);//按键按下 26 window.addEventListener("keyup", onKeyUp, false);//按键抬起 27 window.onmousewheel=onMouseWheel; 28 window.addEventListener("resize", function () {//canvas没有resize事件! 29 if (engine) { 30 engine.resize(); 31 var sizex=engine.getRenderWidth()//_gl.drawingBufferWidth; 32 var sizey=engine.getRenderHeight()//_gl.drawingBufferHeight; 33 var rate=sizex/sizey; 34 rate_screen=rate; 35 var size_minimap=150; 36 if(sr_global) 37 { 38 console.log("resize and reset"); 39 sr_global.enableSnapshotRendering(); 40 } 41 } 42 },false); 43 node_temp=new BABYLON.TransformNode("node_temp",scene);//用来提取相机的姿态矩阵 44 node_temp.rotation=camera0.rotation; 45 } 46 function onMouseDblClick(evt) 47 { 48 //var pickInfo = scene.pick(scene.pointerX, scene.pointerY, null, false, camera0); 49 } 50 function onMouseClick(evt) 51 { 52 //console.log("onMouseClick"); 53 //onMouseClick会在onMouseDown和onMouseUp之后发生!所以拖拽被错误的识别为点击!! 54 //if(!flag_drag) 55 //evt.preventDefault(); 56 57 } 58 var lastPointerX,lastPointerY; 59 var flag_view="rts"; 60 var flag_view_index=0;//建立两重切换:按V键时在当前位置切换为自由相机,并把原位置和控制方式入栈,《-数字按键要预留其他作用! 61 var arr_flag_view=[{key:"i",view:"rts"},{key:"o",view:"free"},{key:"p",view:"one"},{key:"Escape",view:"free"}];//iop三键? 62 var obj_keystate=[]; 63 var pso_stack; 64 var flag_moved=false;//在拖拽模式下有没有移动,如果没移动则等同于click 65 var point0,point;//拖拽时点下的第一个点 66 var MaxMovement; 67 function onMouseMove(evt) 68 { 69 if(!evt) 70 { 71 evt={} 72 evt.clientX=scene.pointerX; 73 evt.clientY=scene.pointerY; 74 } 75 76 if(flag_view=="rts"&&flag_drag) 77 { 78 if(evt.preventDefault) 79 evt.preventDefault(); 80 //var dx=-(scene.pointerX-lastPointerX)/window.innerWidth; 81 //var dz=(scene.pointerY-lastPointerY)/window.innerHeight; 82 var dx=-(evt.clientX-lastPointerX)/canvas.width;//engine.getRenderWidth() 83 var dz=(evt.clientY-lastPointerY)/canvas.height; 84 //Field Of View is set in Radians. (default is 0.8) 85 camera0.position.z+=dz*(camera0.position.y)*rate_fov; 86 camera0.position.x+=dx*(camera0.position.y)*rate_fov*rate_screen; 87 // camera0.position.z+=dz*(camera0.position.y/600)*600; 88 // camera0.position.x+=dx*(camera0.position.y/600)*600; 89 } 90 else if(flag_view=="one") 91 { 92 if(evt.preventDefault) 93 evt.preventDefault(); 94 //绕y轴的旋转角度是根据x坐标计算的 95 // var rad_y=((evt.clientX-lastPointerX)/canvas.width)*(Math.PI/1); 96 // var rad_x=((evt.clientY-lastPointerY)/canvas.height)*(Math.PI/1); 97 var rad_y=((evt.movementX)/canvas.width)*(Math.PI/1); 98 var rad_x=((evt.movementY)/canvas.height)*(Math.PI/1); 99 camera0.myRotation.y+=rad_y; 100 camera0.myRotation.x+=rad_x; 101 //console.log(evt.movementX); 102 //one.node.rotation=camera0.rotation.clone(); 103 } 104 lastPointerX=evt.clientX; 105 lastPointerY=evt.clientY; 106 } 107 var flag_drag=false; 108 var downPointerX,downPointerY; 109 var count_myController=0; 110 function onMouseDown(evt) 111 { 112 //bbl内置的pick事件导致半秒钟的延迟?! 113 //不是gui或瘦实例和精灵导致的延迟,后续测试把pointerdown事件关闭 114 //console.log("onMouseDown"); 115 //evt.preventDefault(); 116 if(flag_view=="rts") 117 { 118 119 flag_drag=true; 120 } 121 } 122 function onMouseUp(evt) 123 { 124 //console.log("onMouseUp"); 125 //evt.preventDefault(); 126 if(flag_view=="rts") 127 { 128 129 flag_drag=false; 130 } 131 //左键框选和左键拖动相冲突,可能要添加基于meshname和额外按键的功能判定 132 if(evt.button==2) 133 { 134 //这里的点击是指针点击而不是准心点击 135 //优先点击精灵? 136 var pickResult = scene.pickSprite(evt.clientX, evt.clientY, null, false, camera0); 137 if (pickResult.hit) { 138 console.log(pickResult); 139 console.log(pickResult.pickedSprite.name); 140 //console.log(pickResult); 141 } 142 else { 143 var pickInfo = scene.pick(evt.clientX, evt.clientY, null, false, camera0); 144 if(pickInfo.hit) 145 { 146 //console.log(pickInfo); 147 //console.log(pickInfo.thinInstanceIndex); 148 //console.log(pickInfo.pickedMesh.id); 149 if(pickInfo.pickedMesh.myType=="ground") 150 { 151 var len=agents.length; 152 for (let i = 0; i < len; i++) { 153 crowd.agentGoto(agents[i], navigationPlugin.getClosestPoint(pickInfo.pickedPoint)) 154 } 155 } 156 } 157 } 158 } 159 } 160 function onKeyDown(event) 161 { 162 if(flag_view=="one") { 163 event.preventDefault(); 164 var key = event.key; 165 obj_keystate[key] = 1; 166 if(obj_keystate["Shift"]==1) 167 { 168 obj_keystate[key.toLowerCase()] = 1; 169 } 170 } 171 } 172 function handleView(lastView,nextView) 173 { 174 if(nextView=="free") 175 { 176 flag_drag=false; 177 camera0.attachControl(canvas, true); 178 if(lastView=="rts") 179 { 180 pos_stack_rts=camera0.position.clone(); 181 } 182 else if(lastView=="one") 183 { 184 document.exitPointerLock(); 185 if(camera0.lines) 186 { 187 camera0.lines.isVisible=false; 188 } 189 } 190 flag_view=nextView; 191 releaseKeyStateIn(); 192 } 193 else if(nextView=="rts") 194 { 195 camera0.detachControl(); 196 if(pos_stack_rts) 197 { 198 camera0.position=pos_stack_rts; 199 camera0.rotation=new BABYLON.Vector3(hd_camera0,0,0); 200 } 201 if(lastView=="one") 202 { 203 document.exitPointerLock(); 204 if(camera0.lines) 205 { 206 camera0.lines.isVisible=false; 207 } 208 } 209 flag_view=nextView; 210 releaseKeyStateIn(); 211 } 212 else if(nextView=="one") 213 { 214 if(one) 215 { 216 flag_drag=false; 217 camera0.detachControl(); 218 camera0.position=one.node.node_back.getAbsolutePosition(); 219 camera0.rotation=one.node.rotation.clone(); 220 canvas.requestPointerLock(options = {unadjustedMovement: false}); 221 //camera0.rotation.x=0; 222 //camera0.rotation.y=one.node.rotation.y; 223 if(camera0.lines) 224 { 225 camera0.lines.isVisible=true; 226 } 227 flag_view=nextView; 228 }//如果没有可驾驶的“载具”则这个物理运动方式不能生效!! 229 else 230 { 231 232 } 233 } 234 //sr_global.enableSnapshotRendering(); 235 } 236 function onKeyUp(event) 237 { 238 var key = event.key; 239 var len=arr_flag_view.length; 240 for(var i=0;i<len;i++)//切换视角 241 { 242 var view=arr_flag_view[i]; 243 if(key==view.key) 244 { 245 event.preventDefault(); 246 if(flag_view==view.view) 247 { 248 249 } 250 else { 251 252 handleView(flag_view,view.view); 253 254 } 255 break; 256 } 257 } 258 259 if(flag_view=="rts") { 260 event.preventDefault(); 261 var key2=key.toLowerCase(); 262 if(key2=="w") 263 { 264 camera0.position.z+=step_move*(camera0.position.y/1800); 265 if(camera0.position.z>10000) 266 { 267 camera0.position.z=10000 268 } 269 } 270 else if(key2=="s") 271 { 272 camera0.position.z-=step_move*(camera0.position.y/1800) 273 if(camera0.position.z<-10000) 274 { 275 camera0.position.z=-10000 276 } 277 } 278 else if(key2=="a") 279 { 280 camera0.position.x-=step_move*(camera0.position.y/1800) 281 if(camera0.position.x<-10000) 282 { 283 camera0.position.x=-10000 284 } 285 } 286 else if(key2=="d") 287 { 288 camera0.position.x+=step_move*(camera0.position.y/1800) 289 if(camera0.position.x>10000) 290 { 291 camera0.position.x=10000 292 } 293 } 294 } 295 else if(flag_view=="one") 296 { 297 event.preventDefault(); 298 299 obj_keystate[key] = 0; 300 if(key=="f") 301 { 302 one.mesh.physicsImpostor.body.setLinearVelocity(new BABYLON.Vector3(0,0,0)); 303 one.mesh.physicsImpostor.body.setAngularVelocity(new BABYLON.Vector3(0,0,0)); 304 } 305 else if(key=="c") 306 { 307 if(one.node.node_back.position.y==1) 308 { 309 one.node.node_back.position.y=0.5; 310 } 311 else { 312 one.node.node_back.position.y=1; 313 } 314 } 315 else if(key==" ") 316 { 317 var pos=one.mesh.getAbsolutePosition(); 318 var ray =BABYLON.Ray.CreateNewFromTo(pos,new BABYLON.Vector3(pos.x,-1,pos.z)); 319 var hit=scene.pickWithRay(ray,(mesh)=>(mesh.id!="node_one"&&mesh.id!="mesh_one")); 320 if(hit&&hit.pickedMesh&&hit.distance<1) 321 { 322 console.log(hit.pickedMesh.id,pos); 323 one.mesh.physicsImpostor.body.applyForce(new BABYLON.Vector3(0,600,0),pos); 324 } 325 326 327 } 328 } 329 } 330 331 332 function onMouseWheel(event){ 333 //var delta =event.wheelDelta/120; 334 if(flag_view=="rts"||flag_view=="free") 335 { 336 if(flag_drag==false)//禁止一边拖拽一边缩放 337 { 338 //事件的兼容性写法 339 var oEvent = event || window.event; 340 //oEvent.preventDefault(); 341 if(oEvent.wheelDelta){//非火狐 342 if(oEvent.wheelDelta > 0){//向上滚动 343 if(int_z<16) 344 { 345 int_z++; 346 } 347 348 }else{//向下滚动 349 //if(int_z>0) 350 { 351 int_z--; 352 } 353 } 354 }else if(oEvent.detail){ 355 if(oEvent.detail > 0){//向下滚动 356 //if(int_z>0) 357 { 358 int_z--; 359 } 360 }else{//向上滚动 361 if(int_z<16) 362 { 363 int_z++; 364 } 365 366 } 367 } 368 int0b=Math.pow(2,-int_z/4);//向上滚动,拉近变细,int_z越大y越小 369 //var int=Math.floor(Math.pow(2,int_z/4));//放大倍数 370 371 } 372 } 373 374 } 375 function onContextMenu(evt) 376 { 377 378 } 379 380 function movePOV(node,node2,vector3) 381 { 382 var m_view=node.getWorldMatrix(); 383 var v_delta=BABYLON.Vector3.TransformCoordinates(vector3,m_view); 384 var pos_temp=node2.position.add(v_delta); 385 node2.position=pos_temp; 386 } 387 function releaseKeyStateIn(evt) 388 { 389 for(var key in obj_keystate) 390 { 391 obj_keystate[key]=0; 392 } 393 //lastPointerX=scene.pointerX; 394 //lastPointerY=scene.pointerY; 395 flag_drag=false; 396 camera0.myRotation={x:0,y:0,z:0}; 397 } 398 function releaseKeyStateOut() 399 { 400 for(var key in obj_keystate) 401 { 402 obj_keystate[key]=0; 403 } 404 flag_drag=false; 405 camera0.myRotation={x:0,y:0,z:0}; 406 } 407 408 var pos_last; 409 var delta; 410 var v_delta; 411 var x_lastquery,z_lastquery,y_lastquery; 412 var count_initasync=2;//有两项异步初始化内容 413 function MyBeforeRender0() 414 { 415 MyBeforeRender(); 416 } 417 //var sr_global=null; 418 var sceneInstrumentation; 419 var count_lost=0; 420 var flag_lost=false; 421 var bool_render=true; 422 var flag_showlayerindex=false; 423 function MyBeforeRender() 424 { 425 //sr_global= new BABYLON.SnapshotRenderingHelper(scene); 426 //@@@@engine.resize();//似乎要以某种方式重建canvas才可使用快速SR,否则会在一秒后丢失内容 427 //是某些与SR有关的变量失去了引用,触发了浏览器的CPP GC,在调试模式下则可能是一直有引用存在的!!!! 428 //sr_global.enableSnapshotRendering(); 429 //int0=1*(camera0.position.y/600); 430 //int0b=int0; 431 432 console.log("MyBeforeRender"); 433 //sceneInstrumentation = new BABYLON.SceneInstrumentation(scene);//用来进行计时 434 //sceneInstrumentation.captureFrameTime = true; 435 engine.hideLoadingUI(); 436 //sr_global.enableSnapshotRendering(); 437 438 439 // setTimeout(function(){ 440 // canvas.width=canvas.width-1; 441 // engine.resize(); 442 // sr_global.enableSnapshotRendering(); 443 // },1000) 444 445 scene.registerBeforeRender( 446 function(){ 447 if(skybox) 448 { 449 //sr_global.updateMesh(skybox); 450 } 451 452 453 if(flag_view=="rts"||flag_view=="free") 454 { 455 if(int0b!=int0) 456 { 457 458 if(int0b<1) 459 { 460 //reDrawThings(int0b);//在事件中触发会造成屏幕闪烁!! 461 462 } 463 464 } 465 } 466 if(flag_view=="one") 467 { 468 //one.node.rotation.y=camera0.rotation.y; 469 var flag_speed=2; 470 //var m_view=node_temp.getWorldMatrix(); 471 if(obj_keystate["Shift"]==1)//Shift+w的event.key不是Shift和w,而是W!!!! 472 { 473 flag_speed=10; 474 } 475 delta=engine.getDeltaTime(); 476 flag_speed=flag_speed*engine.getDeltaTime()/10; 477 var v_temp=new BABYLON.Vector3(0,0,0);//用它来改变物体的受力状态 478 if(obj_keystate["w"]==1) 479 { 480 v_temp.z+=1*flag_speed; 481 482 } 483 if(obj_keystate["s"]==1) 484 { 485 v_temp.z-=1*flag_speed; 486 } 487 if(obj_keystate["d"]==1) 488 { 489 v_temp.x+=0.5*flag_speed; 490 } 491 if(obj_keystate["a"]==1) 492 { 493 v_temp.x-=0.5*flag_speed; 494 } 495 if(v_temp.x!=0||v_temp.z!=0) 496 { 497 //console.log("force",v_temp) 498 var force=newland.vecToGlobal(v_temp,one.node); 499 force=force.subtract(one.node.getAbsolutePosition()).scale(1);//取姿态信息,去除位置信息 500 var pos=one.node.getAbsolutePosition(); 501 one.mesh.physicsImpostor.body.applyForce(force,pos); 502 } 503 //var force=new BABYLON.Vector3(0,-1,0); 504 //one.node.position=one.mesh.position.clone();//此修改可改善显示体落后于仿真体的情况,但在高速运动时会发生剧烈抖动,相反的落后式设置在极高速时则保持平稳 505 //v_delta=BABYLON.Vector3.TransformCoordinates(v_temp,m_view); 506 } 507 } 508 ) 509 scene.registerAfterRender( 510 function(){ 511 512 for(var key in obj_agentTrans)//对于每一个导航代理的变换节点 513 { 514 var agentTrans=obj_agentTrans[key]; 515 var agentIndex=agentTrans.mydata.agentIndex; 516 var maxSpeed=agentTrans.mydata.maxSpeed;//当前速度 517 if(crowd._agentDestinationArmed[agentIndex]) 518 //if(crowd.getAgentNextTargetPath(agentIndex))//如果这个导航器有下一移动目标 519 { 520 var flag_found=false; 521 var position=crowd.getAgentPosition(agentIndex); 522 for(var key2 in obj_maparea) 523 { 524 if(flag_found) 525 { 526 break; 527 } 528 // if(key2==maxSpeed)//相同的速度区域不必重复考虑<-也要判断是否在同速区域内,如不在同速区则可能在base区域中!!!! 529 // { 530 // continue; 531 // } 532 var arr_path=obj_maparea[key2]; 533 var len2=arr_path.length; 534 for(var j=0;j<len2;j++) 535 { 536 var path=arr_path[j]; 537 if(queryPtInPolygon({x:position.x,y:position.z},path))//如果在这个速度区域内 538 { 539 if(key2!=(maxSpeed+"")) 540 { 541 var v2=parseFloat(key2); 542 agentTrans.mydata.maxSpeed=v2; 543 agentParams.maxSpeed=v2; 544 crowd.updateAgentParameters(agentIndex,agentParams); 545 } 546 547 flag_found=true; 548 break; 549 } 550 } 551 } 552 if(!flag_found&&maxSpeed!=baseSpeed) 553 { 554 agentTrans.mydata.maxSpeed=baseSpeed; 555 agentParams.maxSpeed=baseSpeed; 556 crowd.updateAgentParameters(agentIndex,agentParams); 557 } 558 } 559 560 } 561 if(flag_view=="rts"||flag_view=="free") 562 { 563 if(int0b!=int0) 564 { 565 camera0.position.y=600*int0b; 566 int0=int0b;//更新上一放大倍数 567 } 568 } 569 570 if(camera0.position.y<75)//较近时显示文字,较远时隐藏 571 { 572 if(!flag_showlayerindex) 573 { 574 flag_showlayerindex=true; 575 objSpriteManager.manager.renderingGroupId=2;//精灵管理器是否受快照影响? 576 //sr_global.enableSnapshotRendering(); 577 } 578 } 579 else 580 { 581 if(flag_showlayerindex) 582 { 583 flag_showlayerindex=false; 584 objSpriteManager.manager.renderingGroupId=0; 585 //sr_global.enableSnapshotRendering(); 586 } 587 } 588 } 589 ) 590 engine.runRenderLoop(function () { 591 //engine.hideLoadingUI(); 592 var int_fps=engine.getFps().toFixed(); 593 if (divFps) { 594 divFps.innerHTML = int_fps+ " fps"; 595 } 596 597 //sr_global.updateMesh(one.node);//物理引擎似乎不受快照模式限制??!! 598 //sr_global.updateMesh(one.mesh); 599 //if(flag_runningstate=="fastsr") 600 //{ 601 scene.render(); 602 //} 603 604 }); 605 } 606 function sort_compare(a,b)//从近到远 607 { 608 return a.distance-b.distance; 609 } 610 function sort_compare2(a,b)//从远到近 611 { 612 return b.distance-a.distance; 613 } 614 var flag_text_near=false;
这一文件中包含对页面失去/获得焦点、光标移动、光标按下、光标抬起、按键按下、按键抬起、鼠标滚轮滚动、窗口大小变化等事件的监听,这些监听都是直接对html元素本身的监听,而不使用bbl内置的scene对象事件监听,因为bbl内置的光标事件会默认进行射线检测,在一些场景下会产生较大的延迟。
每一种事件可能在不同“控制模式下”对应不同的响应方法,例如在rts控制模式下拖动鼠标的响应是移动地图,在自由相机模式下则是改变视角,每一种事件的响应中都需根据不同的控制模式编写对应的响应代码。
web3D中常见的控制模式包括:bbl内置的自由相机和弧形旋转相机、rts、带有物理效果的第一或第三人称控制(例如https://gitee.com/ljzc002/maze)、带有z轴自由度的浮空姿态变换(例如https://www.cnblogs.com/ljzc002/p/11589973.html)等,可使用不同按键在多种控制模式间切换。
该文件还包含对渲染循环的初始化:
registerBeforeRender、registerAfterRender、runRenderLoop分别表示渲染前执行(此时各种与渲染有关的内置变量并未变化)、渲染后执行、渲染时执行。这里没有使用bbl官方推荐的观察者模式,目的是将这些循环代码整理在一处避免分散。
4、gui初始化
initGuiControl方法位于ControlGui.js文件中:
1 var step_move=10; 2 var advancedTexture,global_panel_text,global_panel2; 3 //在移动端情况下,机体上没有实体按键,故gui按钮为必须选择,且在不同控制模式下gui按钮须有不同作用! 4 //在free模式下需要仿制freecamera作选定方向运动,在one模式下需作物理引擎推动,在rts模式下需作水平卷动 5 function initGuiControl() 6 { 7 advancedTexture = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI("ui1"); 8 advancedTexture.layer.layerMask=1; 9 var camera=camera0; 10 //左侧的运动控制按钮 11 var panel2=new BABYLON.GUI.Rectangle(); 12 panel2.width="200px"//0.25; 13 panel2.top="10px"; 14 panel2.left="10px"; 15 panel2.height="170px"//0.25; 16 panel2.horizontalAlignment=BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT; 17 panel2.verticalAlignment=BABYLON.GUI.Control.VERTICAL_ALIGNMENT_TOP; 18 panel2.thickness=0 19 advancedTexture.addControl(panel2); 20 global_panel2=panel2; 21 var button_fw=BABYLON.GUI.Button.CreateSimpleButton("button_fw","复位"); 22 //button_fw.width=0.2; 23 button_fw.height="40px"; 24 button_fw.width="40px"; 25 //button_fw.top="10px"; 26 button_fw.left="-20px"; 27 button_fw.color="white"; 28 button_fw.cornerRadius=20; 29 button_fw.background="green"; 30 button_fw.onPointerUpObservable.add(function(){ 31 if(flag_view=="rts")//在one模式下没有复位效果? 32 { 33 34 camera.position=initpos_camera0.clone(); 35 camera.rotation=initrot_camera0.clone(); 36 } 37 else if(flag_view=="free") 38 { 39 if(pos_stack_rts) 40 { 41 camera.position=pos_stack_rts; 42 //camera0.rotation=new BABYLON.Vector3(hd_camera0,0,0); 43 } 44 45 } 46 else if(flag_view=="one") 47 { 48 one.node.rotation=new BABYLON.Vector3(0,0,0); 49 camera0.position=one.node.node_back.getAbsolutePosition(); 50 camera0.rotation=one.node.rotation.clone(); 51 52 } 53 }); 54 panel2.addControl(button_fw); 55 56 var button_fw=BABYLON.GUI.Button.CreateSimpleButton("button_q","前"); 57 //button_fw.width=0.2; 58 button_fw.height="40px"; 59 button_fw.width="40px"; 60 button_fw.left="-20px"; 61 button_fw.top="-60px"; 62 63 button_fw.color="white"; 64 button_fw.cornerRadius=20; 65 button_fw.background="green"; 66 button_fw.onPointerUpObservable.add(function(){ 67 if(flag_view=="rts") 68 { 69 camera.position.z=camera.position.z+step_move; 70 } 71 }); 72 panel2.addControl(button_fw); 73 74 var button_fw=BABYLON.GUI.Button.CreateSimpleButton("button_h","后"); 75 //button_fw.width=0.2; 76 button_fw.height="40px"; 77 button_fw.width="40px"; 78 button_fw.left="-20px"; 79 button_fw.top="60px"; 80 button_fw.color="white"; 81 button_fw.cornerRadius=20; 82 button_fw.background="green"; 83 button_fw.onPointerUpObservable.add(function(){ 84 if(flag_view=="rts") 85 { 86 87 camera.position.z=camera.position.z-step_move; 88 } 89 }); 90 panel2.addControl(button_fw); 91 92 var button_fw=BABYLON.GUI.Button.CreateSimpleButton("button_s","上"); 93 //button_fw.width=0.2; 94 button_fw.height="40px"; 95 button_fw.width="40px"; 96 button_fw.left="80px"; 97 button_fw.top="-60px"; 98 button_fw.color="white"; 99 button_fw.cornerRadius=20; 100 button_fw.background="green"; 101 button_fw.onPointerUpObservable.add(function(){ 102 if(flag_view=="rts") 103 { 104 int_z--; 105 int0b=Math.pow(2,-int_z/4); 106 //camera.position.y=camera.position.y+step_move; 107 } 108 }); 109 panel2.addControl(button_fw); 110 111 var button_fw=BABYLON.GUI.Button.CreateSimpleButton("button_x","下"); 112 //button_fw.width=0.2; 113 button_fw.height="40px"; 114 button_fw.width="40px"; 115 button_fw.left="80px"; 116 button_fw.top="60px"; 117 button_fw.color="white"; 118 button_fw.cornerRadius=20; 119 button_fw.background="green"; 120 button_fw.onPointerUpObservable.add(function(){ 121 if(flag_view=="rts") 122 { 123 if(int_z<16) 124 { 125 int_z++; 126 } 127 //camera.position.y=camera.position.y-step_move; 128 int0b=Math.pow(2,-int_z/4); 129 } 130 }); 131 panel2.addControl(button_fw); 132 133 var button_fw=BABYLON.GUI.Button.CreateSimpleButton("button_z","左"); 134 //button_fw.width=0.2; 135 button_fw.height="40px"; 136 button_fw.width="40px"; 137 button_fw.left="-80px"; 138 //button_fw.top="-40px"; 139 button_fw.color="white"; 140 button_fw.cornerRadius=20; 141 button_fw.background="green"; 142 button_fw.onPointerUpObservable.add(function(){ 143 if(flag_view=="rts") 144 { 145 146 camera.position.x=camera.position.x-step_move; 147 148 } 149 }); 150 panel2.addControl(button_fw); 151 152 var button_fw=BABYLON.GUI.Button.CreateSimpleButton("button_y","右"); 153 //button_fw.width=0.2; 154 button_fw.height="40px"; 155 button_fw.width="40px"; 156 button_fw.left="40px"; 157 button_fw.color="white"; 158 button_fw.cornerRadius=20; 159 button_fw.background="green"; 160 button_fw.onPointerUpObservable.add(function(){ 161 if(flag_view=="rts") 162 { 163 164 camera.position.x=camera.position.x+step_move; 165 } 166 }); 167 panel2.addControl(button_fw); 168 }
其作用是在三维场景中绘制按钮、文本框等控件,bbl内置gui的功能不如html标签丰富,使用难度也更高,其优点是只存在于3D场景中,不与canvas之外的内容发生关系。
5、启动渲染循环前的其他初始化操作
function webGLStart2() { flag_runningstate="场景初始化完成"; //sr_global= new BABYLON.SnapshotRenderingHelper(scene); createManagers(); //scene.debugLayer.show(); //initStellaris(); MyBeforeRender0(); //scene.debugLayer.show(); }
根据实际需求编写,例如这里createManagers方法用来生成精灵管理器,scene.debugLayer.show();语句则是用来启动bbl的内置调试工具。
6、用精灵管理器实现大量汉字渲染
bbl官方推荐使用gui或自定义纹理在场景中显示文字,但二者均在渲染大量分离文字时,存在渲染速度慢和定位困难问题,本文采用精灵管理器实现rts环境下的大量汉字渲染功能,其代码位于drawSprites.js文件中,cyhz.js文件则是常用汉字字库。
1 var arr_sprites=[]; 2 var char_global="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890+-_.|"+str_cyhz;//通用字符+常用汉字字库 3 var obj_czpos={},objSpriteManager={}; 4 //要取常用的星系名! 5 function createManagers() 6 { 7 for(var zm in obj_czpos) {//一些场景中额外定义的可操作对象 8 var len=zm.length; 9 for(var i=0;i<len;i++) 10 { 11 if(char_global.indexOf(zm[i])==-1)//如果字表中还没有这个字 12 { 13 char_global+=zm[i]; 14 } 15 } 16 } 17 var can_temp_label=document.createElement("canvas"); 18 var len=char_global.length; 19 var fontsize=32; 20 var color="#ffffff";//后期染色? 21 can_temp_label.width=4096;//可能是计算误差?粒子系统取的方块比canvas的方块小一点?一共4000多个字,最大估计10000个字? 22 can_temp_label.height=4096;//预设7225个字的空间! 23 var context=can_temp_label.getContext("2d"); 24 context.fillStyle="rgba(0,0,0,0)";//完全透明的背景 25 context.fillRect(0,0,can_temp_label.width,can_temp_label.height); 26 context.fillStyle = color; 27 context.font = "bold "+fontsize+"px monospace"; 28 for(var i=0;i<len;i++) 29 { 30 var char=char_global[i]; 31 var x=(i%128)*fontsize;//一行85个字,在达到上百个字后误差比较明显《-这个误差是非整数跨行导致的 32 //要让半角字符和全角字符和谐显示,一种可用的方法是修改这个横坐标计算规则?<-在实际添加精灵时修改 33 var y=(Math.ceil((i+0.5)/128))*fontsize; 34 context.fillText(char,x,y-3); 35 } 36 var png=can_temp_label.toDataURL("image/png");//生成PNG图片 37 //console.log(png); 38 //建立精灵管理器 39 var spriteManager = new BABYLON.SpriteManager("spriteManagerLabel", png, 50000, fontsize, scene); 40 spriteManager.renderingGroupId=3; 41 spriteManager.cellWidth=fontsize; 42 spriteManager.cellHeight=fontsize; 43 spriteManager.isPickable = true; 44 //spriteManager.isVisible=false;《-此属性无作用 45 objSpriteManager.manager=spriteManager; 46 } 47 var size_char=12; 48 //在指定位置以精灵方式添加文字 49 function addSpritelabel(str,px,py,id,size,color,manager,arr_sprites,h) 50 { 51 if(!h) 52 { 53 h=2 54 } 55 var len=str.length; 56 var arr=[]; 57 var size_offset=0; 58 for(var i=0;i<len;i++) 59 { 60 var char=str[i]; 61 var s_char=new BABYLON.Sprite("s_big_"+id+"_"+i, manager); 62 var index=char_global.indexOf(char); 63 if(index<0) 64 { 65 index=0;//用A补位 66 } 67 s_char.cellIndex=index; 68 s_char.size=size; 69 s_char.isPickable=true; 70 s_char.position=new BABYLON.Vector3(px+size_offset,h,py); 71 if(index<67) 72 { 73 s_char.isBJ=true;//是半角字符 74 size_offset+=size/2; 75 } 76 else { 77 s_char.isBJ=false;//不是半角字符 78 size_offset+=size; 79 } 80 81 if(color) 82 { 83 s_char.color=color; 84 } 85 else { 86 s_char.color={r:0,g:0,b:0,a:1} 87 } 88 arr.push(s_char); 89 } 90 var len=arr.length; 91 for(var i=0;i<len;i++) 92 { 93 var s=arr[i]; 94 s.position.x-=size_offset/2; 95 } 96 arr_sprites.push({ 97 arr:arr,size_offset:size_offset,id:id,str:str,px:px,py:py,color:color 98 }) 99 }
其基本思路是将常用字符绘制在一张4096*4096的图片中,然后以这张图片建立带有动画效果的精灵管理器,以精灵动画的每个动画帧索引对应图片中的每个字符小块,如此便可基于精灵的高性能特性实现大量汉字渲染。该方法的缺点是,在非rts控制模式下,用户的视点可能移动到精灵后侧,此时精灵的排列顺序将显示为倒序。该方法也考虑了全角字符和半角字符的宽度差异。
四、实现群组导航
在四年前的这篇文章https://www.cnblogs.com/ljzc002/p/15119505.html中,作者基于recast1群组导航库实现了简单的平面群组导航。相较于前代,recast2的优势主要包括:导航性能提升、多层导航、跳跃与传送、基于瓦片的地块分块更新、更精细的导航网格调试、为每个分块设置不同导航属性等。bbl通过babylonjs.addons.min.js文件引入了这个新版导航库,但目前bbl官方引入还存在缺陷,比如:官方示例错误、分块和调试功能支持不完善等,因此作者对bbl官方示例和babylonjs.addons.min.js库进行了一些自定义修改。
创建导航群组的代码在ControlCrowd.js文件中:
1 //用来控制群组导航 2 async function initCrowd(arr_mesh) 3 { 4 //尝试使用TileCacheMeshProcess 技术分块构造导航网格 5 navigationPlugin = await ADDONS.CreateNavigationPluginAsync();//引入导航网格插件,这时会下载@recast-navigation目录下的三个js文件 6 //const cellSize = 0.05; 7 const navmeshParameters = {//建立导航网格的参数 8 cs: 0.1, 9 ch: 0.2,maxObstacles: 64, 10 expectedLayersPerTile: 2, 11 tileCacheMeshProcess:tp,tileSize: 32, 12 } 13 const { navMesh,tileCache, navMeshQuery } = await navigationPlugin.createNavMeshAsync(arr_mesh, navmeshParameters); 14 visualizeCrowd(navMeshQuery); 15 } 16 var count_tile; 17 function tp(navMeshCreateParams,polyAreas,polyFlags){ 18 19 const STAIRS_AREA = 1; 20 const DEFAULT_AREA = 0; 21 const WALK_FLAG = 1;//这是水平移动消耗? 22 const STAIRS_FLAG = 10; 23 count_tile=0; 24 const vertsCount = navMeshCreateParams.vertCount(); 25 const polyCount=navMeshCreateParams.polyCount(); 26 const indicesCount = polyCount; 27 var tileX=navMeshCreateParams.tileX();//这个瓦片的水平偏移量? 28 var tileY=navMeshCreateParams.tileY(); 29 for (let i = 0; i < polyCount; i++) {//对于每个多边形 30 // compute average y of polygon vertices 31 const idx0 = navMeshCreateParams.polys(i * 3 + 0);//polys是顶点的索引,例如一个正方形多边形包括四个顶点 32 const idx1 = navMeshCreateParams.polys(i * 3 + 1); 33 const idx2 = navMeshCreateParams.polys(i * 3 + 2);//polys(4)是65535 34 //verts是顶点数据, 35 //const avgY = (navMeshCreateParams.verts(idx0 * 3 + 1) + navMeshCreateParams.verts(idx1 * 3 + 1) + navMeshCreateParams.verts(idx2 * 3 + 1)) / 3; 36 37 if (tileX>=10) { 38 polyAreas.set(i,STAIRS_AREA)//当前生效写法 39 polyFlags.set(i,STAIRS_FLAG) 40 //polyAreas[i] = STAIRS_AREA;//bbl文档的不生效写法 41 //polyFlags[i] = STAIRS_FLAG; 42 } else { 43 polyAreas.set(i,DEFAULT_AREA) 44 polyFlags.set(i,WALK_FLAG) 45 //polyAreas[i] = DEFAULT_AREA; 46 //polyFlags[i] = WALK_FLAG; 47 } 48 count_tile++; 49 } 50 51 } 52 var agents,crowd,obj_agentv={}; 53 var baseSpeed=5.0;//默认最大速度为5 54 var agentParams = { 55 radius: 0.1 + Math.random() * 0.05, 56 height: 1.5, 57 maxAcceleration: 11.0, 58 maxSpeed: baseSpeed, 59 separationWeight: 1.0, 60 } 61 function visualizeCrowd(navMeshQuery) { 62 crowd = navigationPlugin.createCrowd(100, 0.15, scene); 63 obj_agentv={}; 64 for (let i = 0; i < 100; i++) { 65 66 const { randomPoint: position } = 67 navMeshQuery.findRandomPointAroundCircle({ x: 0, y: 0, z: 0 }, 1); 68 var flag_found=false; 69 for(var key in obj_maparea) 70 { 71 if(flag_found) 72 { 73 break; 74 } 75 var arr_path=obj_maparea[key]; 76 var len2=arr_path.length; 77 for(var j=0;j<len2;j++) 78 { 79 var path=arr_path[j]; 80 if(queryPtInPolygon({x:position.x,y:position.z},path))//如果在这个速度区域内 81 { 82 agentParams.maxSpeed=parseFloat(key); 83 flag_found=true; 84 break; 85 } 86 } 87 } 88 if(!flag_found) 89 { 90 agentParams.maxSpeed=baseSpeed; 91 } 92 createAgent(agentParams, position, crowd) 93 } 94 //agents有固定的id吗? 95 agents = crowd.getAgents();//这个方法返回的是数字索引!! 96 97 //var len= 98 //这些代理器可能被导航到不同的目标 99 // function moveCrowdAgents(pickedPoint) { 100 // for (let i = 0; i < agents.length; i++) { 101 // crowd.agentGoto(agents[i], navigationPlugin.getClosestPoint(pickedPoint)) 102 // } 103 // } 104 // return { moveCrowdAgents, agents } 105 106 } 107 var obj_agentTrans={}; 108 function createAgent(agentParams, position, crowd) { 109 //单位初始化在不同区域,需使用不同的速度初始化参数!!!! 110 111 const agentTransform = new BABYLON.TransformNode(); 112 const agentIndex = crowd.addAgent(position, agentParams, agentTransform); 113 114 agentTransform.name = `agent-transform-${agentIndex}` 115 116 const agentMesh = createAgentMesh(agentParams, agentIndex) 117 agentMesh.parent = agentTransform; 118 agentTransform.mydata={}; 119 agentTransform.mydata.maxSpeed=agentParams.maxSpeed; 120 agentTransform.mydata.agentIndex=agentIndex; 121 obj_agentTrans[agentTransform.name]=agentTransform; 122 return { agentIndex, agentMesh, agentTransform } 123 } 124 function createAgentMesh(agentParams, agentIndex) { 125 const meshName = `agent-${agentIndex}` 126 let agentMesh = scene.getMeshByName(meshName); 127 if (!agentMesh) { 128 agentMesh = BABYLON.MeshBuilder.CreateCylinder(meshName, { height: agentParams.height, diameter: agentParams.radius * 2 }, scene) 129 agentMesh.position.y += agentParams.height / 2 130 agentMesh.bakeCurrentTransformIntoVertices(); 131 agentMesh.material=mat_global.mat_white_e; 132 } 133 134 // const matName = `agent-${agentIndex}` 135 // const matAgent = scene.getMaterialByName(matName) ?? new BABYLON.StandardMaterial(matName, scene) 136 // const variation = Math.random() 137 // matAgent.diffuseColor = new BABYLON.Color3(0.4 + variation * 0.6, 0.3, 1.0 - variation * 0.3) 138 // agentMesh.material = matAgent 139 140 return agentMesh 141 } 142 function isPointinArena(v,path) 143 { 144 var point={x:v[0],y:v[1]} 145 return queryPtInPolygon(point,path); 146 }
1、导航网格配置
其中navmeshParameters是用于建立导航网格对象的参数,recast2可将一个导航网格分为若干个“瓦片”,每个瓦片包含多个方格,然后以瓦片为单位进行导航网格更新,并优化导航计算。参数中tileSize指一个瓦片的尺寸,它的计算单位是方格的尺寸(cs),例如在128*128的地图里,设定cs为0.1,tileSize为32,则会分为40*40=1600个瓦片。
tileCacheMeshProcess参数对应的方法被用来设置每个瓦片的属性,可根据瓦片的索引、位置和瓦片内的分层情况为它设置指定的瓦片类型(polyAreas)和移动力消耗(polyFlags),然后可以为每个导航体分配不同的移动类型,控制其是否可通过特定的地块,或者根据不同瓦片的移动力消耗进行导航线路计算,但现有的web端示例(例如three.js的示例https://navcat.dev/,bbl目前尚无)只实现了瓦片类型设置,并未实现移动力消耗设置,故本文则采用其他方式实现不同地块的速度差异效果。
2、导航体配置
visualizeCrowd方法中随机建立了100个导航体,为在体现不同地块中的速度差异,在createMap.js中建立一些封闭路径,将路径内围成的区域作为移动速度不同的地形区域:
1 var obj_maparea={ 2 2:[]//最大速度为2的地区 3 } 4 function initMap() 5 { 6 var path_tree=[]; 7 path_tree.push(new BABYLON.Vector3(-32,0,32)); 8 path_tree.push(new BABYLON.Vector3(32,0,32)); 9 path_tree.push(new BABYLON.Vector3(32,0,-32)); 10 path_tree.push(new BABYLON.Vector3(-32,0,-32)); 11 var points_tree=[]; 12 var len=path_tree.length; 13 for(var i=0;i<len;i++) 14 { 15 var pos=path_tree[i]; 16 points_tree.push({x:pos.x,y:pos.z}); 17 } 18 obj_maparea[2].push(points_tree); 19 var mesh_extrude=new BABYLON.MeshBuilder.ExtrudePolygon("mesh_extrude" 20 , {shape: path_tree, depth: 1,sideOrientation:BABYLON.Mesh.DOUBLESIDE,updatable:true}); 21 mesh_extrude.position.y=0.5; 22 mesh_extrude.mydata={}; 23 mesh_extrude.mydata.maxSpeed=2; 24 var mat = new BABYLON.StandardMaterial("mat_tree", scene);//1 25 mat.disableLighting = true; 26 mat.emissiveTexture = new BABYLON.Texture("./assets/image/LANDTYPE/yulin.png", scene); 27 //mat.emissiveTexture.uScale = 8;//手动设定 28 //mat.emissiveTexture.vScale = 8; 29 mat.freeze(); 30 mat_global.mat_tree=mat; 31 mesh_extrude.material=mat; 32 //mesh_extrude.renderingGroupId=2; 33 mesh_extrude.myType="ground"; 34 mesh_extrude.myType1="tree"; 35 //根据boundbox比例重调uv? 36 mesh_extrude.convertToFlatShadedMesh();//自动顶点展开 37 var data_position =mesh_extrude.getVerticesData(BABYLON.VertexBuffer.PositionKind); 38 var data_uv=mesh_extrude.getVerticesData(BABYLON.VertexBuffer.UVKind); 39 //var data_uv2=[]; 40 var data_index=mesh_extrude._geometry._indices; 41 var len=data_index.length; 42 var rate_scale=10; 43 for(var i=0;i<len;i+=3)//默认是以2单位长度为1uv单位的,这里改为10 44 { 45 var i1=data_index[i]; 46 var i2=data_index[i+1]; 47 var i3=data_index[i+2]; 48 var vec1=new BABYLON.Vector3(data_position[i1*3],data_position[i1*3+1],data_position[i1*3+2]); 49 var vec2=new BABYLON.Vector3(data_position[i2*3],data_position[i2*3+1],data_position[i2*3+2]); 50 var vec3=new BABYLON.Vector3(data_position[i3*3],data_position[i3*3+1],data_position[i3*3+2]); 51 var mx=Math.max(Math.abs(vec1.x-vec2.x),Math.abs(vec1.x-vec3.x),Math.abs(vec3.x-vec2.x)); 52 var my=Math.max(Math.abs(vec1.y-vec2.y),Math.abs(vec1.y-vec3.y),Math.abs(vec3.y-vec2.y)); 53 var mz=Math.max(Math.abs(vec1.z-vec2.z),Math.abs(vec1.z-vec3.z),Math.abs(vec3.z-vec2.z)); 54 if(my<=mx&&my<=mz)//这个三角形倾向于朝向y轴 55 { 56 57 data_uv[i1*2]=vec1.x/rate_scale; 58 data_uv[i1*2+1]=vec1.z/rate_scale; 59 data_uv[i2*2]=vec2.x/rate_scale; 60 data_uv[i2*2+1]=vec2.z/rate_scale; 61 data_uv[i3*2]=vec3.x/rate_scale; 62 data_uv[i3*2+1]=vec3.z/rate_scale; 63 } 64 else { 65 if(mx>=mz) 66 { 67 var rate=Math.pow(mx*mx+mz*mz,0.5)/mx; 68 data_uv[i1*2]=vec1.x*rate/rate_scale; 69 data_uv[i1*2+1]=vec1.y/rate_scale; 70 data_uv[i2*2]=vec2.x*rate/rate_scale; 71 data_uv[i2*2+1]=vec2.y/rate_scale; 72 data_uv[i3*2]=vec3.x*rate/rate_scale; 73 data_uv[i3*2+1]=vec3.y/rate_scale; 74 } 75 else { 76 var rate=Math.pow(mx*mx+mz*mz,0.5)/mz; 77 data_uv[i1*2]=vec1.z*rate/rate_scale; 78 data_uv[i1*2+1]=vec1.y/rate_scale; 79 data_uv[i2*2]=vec2.z*rate/rate_scale; 80 data_uv[i2*2+1]=vec2.y/rate_scale; 81 data_uv[i3*2]=vec3.z*rate/rate_scale; 82 data_uv[i3*2+1]=vec3.y/rate_scale; 83 } 84 } 85 } 86 mesh_extrude.updateVerticesData(BABYLON.VertexBuffer.UVKind,data_uv); 87 }
例如这里是在地图中央建立一个正方形(实际使用时可随意设置联通的多边形形状)的林地区域,并且将这一区域路径保存在obj_maparea对象中,同时还为它设置了均匀的树林纹理。
然后在建立导航体时,使用isPointinArena方法对其位置是否在这个区域路径中进行判断,如果在这个区域中则应用这个区域的最大速度属性,如果不在任何区域中则应用默认的最大速度,如此即可在不同区域内应用不同的移动速度。
在每次渲染循环后,也会对移动导航体的位置进行判断,如果导航体进入不同的区域则应用新区域的最大速度,其代码位于registerAfterRender函数中:
for(var key in obj_agentTrans)//对于每一个导航体的变换节点 { var agentTrans=obj_agentTrans[key]; var agentIndex=agentTrans.mydata.agentIndex; var maxSpeed=agentTrans.mydata.maxSpeed;//当前速度 if(crowd._agentDestinationArmed[agentIndex])//如果导航体正在移动 //if(crowd.getAgentNextTargetPath(agentIndex))//如果这个导航器有下一移动目标 { var flag_found=false; var position=crowd.getAgentPosition(agentIndex); for(var key2 in obj_maparea) { if(flag_found) { break; } // if(key2==maxSpeed)//相同的速度区域不必重复考虑<-也要判断是否在同速区域内,如不在同速区则可能在base区域中!!!! // { // continue; // } var arr_path=obj_maparea[key2]; var len2=arr_path.length; for(var j=0;j<len2;j++) { var path=arr_path[j]; if(queryPtInPolygon({x:position.x,y:position.z},path))//如果在这个速度区域内 { if(key2!=(maxSpeed+"")) { var v2=parseFloat(key2); agentTrans.mydata.maxSpeed=v2; agentParams.maxSpeed=v2; crowd.updateAgentParameters(agentIndex,agentParams); } flag_found=true; break; } } } if(!flag_found&&maxSpeed!=baseSpeed) { agentTrans.mydata.maxSpeed=baseSpeed; agentParams.maxSpeed=baseSpeed; crowd.updateAgentParameters(agentIndex,agentParams); } } }
3、babylonjs.addons.min.js代码修改
a、在“"@recast-navigation"”字符附近,增加了一个dir_lib_header变量,并且根据实际部署目录结构调整依赖包的路径,这是为了解决本地部署依赖路径错误问题。
b、在“NavigationPlugin: Tile cache is enabled. Recommended tileSize is 32 to 64. Other values may lead to unexpected behavior.”字符附近,添加了TileCacheMeshProcess类的类型转换代码:
//@@@@create TileCacheMeshProcess instance var h=c?rt(t):u?it(t):tt(t); if(h.tileCacheMeshProcess) h.tileCacheMeshProcess=new (Qe().TileCacheMeshProcess)((h.tileCacheMeshProcess));
这是因为bbl库建立的tileCacheMeshProcess对象是普通object对象而非TileCacheMeshProcess类对象。
五、总结与计划
程序成功完成实验目标,bbl官方可能在后续版本修正代码问题,并给出完整的官方案例。
下一步计划在地图中放置可建设发展的“城市”,使用htmlMesh为导航器设计“军队”图标,在渲染循环基础上建立游戏逻辑循环,实现单机、单势力运营。

浙公网安备 33010602011771号