在WebGL场景中使用2DA*寻路
Posted ljzc002
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了在WebGL场景中使用2DA*寻路相关的知识,希望对你有一定的参考价值。
这篇文章将讨论如何在一个自定义的地面网格上进行简单的2D寻路,以及确定路径后如何使用基于物理引擎的运动方式使物体沿路径到达目标地点。读者需要预先对WebGL和Babylonjs知识有一些了解,可以参考我录制的WebGL入门视频教程和翻译的官方入门文档,当然也可以用自己喜欢的其他方式来学习。
文章主要分成如下几部分:
1、自定义地面网格与寻路矩阵
2、生成Babylon格式3D模型
3、使用pathfinding库进行2D寻路
4、基于cannon.js物理引擎使物体沿路径移动
场景可以通过http://ljzc002.github.io/FPS3/index.html访问,完整代码可以在https://github.com/ljzc002/ljzc002.github.io查看。
场景如下图:
使用WASD控制自由相机位置,移动鼠标控制视角,右键点击地面会在地面上放置一个“目标方块”,然后标有“农民”标志的小球会向目标方块移动。
场景中使用了2DA*寻路算法,如下图所示:
当目标方块位于障碍的另一边时,农民会尽量寻找最短的路径绕开障碍物前往目标方块。
1、自定义地面网格与寻路矩阵:
a、在Babylon.js渲染引擎中自定义地面网格
1 var vdata_ground=new BABYLON.VertexData.CreateGround({width:198,height:198,subdivisionsX:99,subdivisionsY:99});//分成了99段,一条边上100个顶点 2 var arr_vposition=vdata_ground.positions; 3 var map=[]; 4 var len=arr_vposition.length/3;//和挨个遍历比起来,似乎有目的的去找更好 5 for(var i=20;i<81;i++)//对于这个范围内的行 6 { 7 for(var j=20;j<23;j++)//对于这个范围内的列 8 { 9 arr_vposition[(j+100*i)*3+1]=1; 10 } 11 } 12 for(var i=20;i<81;i++)//对于这个范围内的行 13 { 14 for(var j=40;j<43;j++)//对于这个范围内的列 15 { 16 arr_vposition[(j+100*i)*3+1]=2; 17 } 18 } 19 for(var i=20;i<81;i++)//对于这个范围内的行 20 { 21 for(var j=60;j<63;j++)//对于这个范围内的列 22 { 23 arr_vposition[(j+100*i)*3+1]=4; 24 } 25 } 26 BABYLON.VertexData._ComputeSides(0, arr_vposition, vdata_ground.indices, vdata_ground.normals 27 , vdata_ground.uvs); 28 var mesh_ground=new BABYLON.Mesh("mesh_ground",scene); 29 mesh_ground.renderingGroupId=2; 30 vdata_ground.applyToMesh(mesh_ground, true);
第一行建立了一个Ground类型的Babylonjs“顶点数据”对象,这个对象包含了建立地面网格所需的顶点位置、法线、纹理坐标、顶点索引数据(建议读者亲自用调试模式看一下这个对象的结构),构造函数中的两个198表示地面的长宽是198,两个99表示每一条边被分为99段(由100个顶点组成,每两个顶点之间的距离为2),至于为什么设置为99段后文会有说明,这时的地面网格如果渲染出来将是一个平面。
要让地面变得凹凸不平有两种思路:在高度变化的点的比例较大时,可以尝试对每个顶点进行遍历,然后按照某种规则改变顶点的高度;在比例较小时建议直接在缓存数组中找到这些顶点进行改变,显然后者速度更快。
第26行根据顶点数据对每个面的正反进行计算,在对网格的顶点信息进行修改后一般都要执行这一条语句,而另一条经常在它之前执行的语句是:“
BABYLON.VertexData.ComputeNormals(positions, indices, normals);
”,它的作用是在顶点数据变化后重新计算法线方向。这里不执行这条语句的原因是Babylonjs中的地面网格是一种“简化”的网格
如图所示:
同样表示两个方块,简化的方式使用六个顶点,顶点之间的片元数据由顶点数据插值而成,因为左边的方块和右边的方块共用了两个顶点,所以这两个方块的法线方向和纹理坐标必定是连续的。而用非简化的方式表示这两个方块,则需要使用八个顶点,缺点是增加了对性能的消耗,优点是法线方向和纹理坐标不必连续,可以进行截然不同的变化。
因为采用了简化的方式,地面网格的同一个顶点处于多个不同的平面中,使用ComputeNormals计算地面网格的顶点的法线方向也就失去了意义,事实上Babylonjs把地面网格顶点的法线方向都默认为竖直向上。
后面的代码建立了一个空的网格,将网格的渲染组设为2,将顶点数据交给这个空网格对象。
生成的网格如下图所示:
b、建立2D寻路矩阵
在寻路场景中使用的pathfinding库需要用一个矩阵(二维数组)来定义障碍物的位置,其中零元素表示这个地块可以通行,不为零的元素表示无法通行,下面是建立这个数组的方法:
1 mesh_ground.mydata={}; 2 mesh_ground.mydata.walkabilityMatrix=MakewalkabilityMatrix(arr_vposition,99,99,2); 3 mesh_ground.mydata.len_x=99; 4 mesh_ground.mydata.len_y=99; 5 mesh_ground.mydata.len_s=2; 6 7 。 8 。 9 。 10 11 //对每个正方形区块的倾斜程度进行计算,得出是否可以通行 12 //顶点数据,寻路空间宽度,寻路空间高度,每个方格区域的边长 13 function MakewalkabilityMatrix(arr,len_x,len_y,len_s) 14 { 15 var arr_Matrix=numeric.rep([len_y,len_x],0); 16 var len_s2=len_s*0.707;//求得平均点到其中一个边线点的水平距离0.7071067811865476 17 //var len_s2a=len_s2; 18 //var len_s2b=len_s2; 19 //var len_s2c=len_s2; 20 for(var i=0;i<len_y;i++)//对于每一行寻路单元格 21 { 22 for(var j=0;j<len_x;j++)//对于这一行里的每一个单元格 23 { 24 var int1=j+i*(len_x+1); 25 var int2=j+i*(len_x+1)+1; 26 var int3=j+(i+1)*(len_x+1); 27 var int4=j+(i+1)*(len_x+1)+1; 28 var y1=arr[int1*3+1]; 29 var y2=arr[int2*3+1]; 30 var y3=arr[int3*3+1]; 31 var y4=arr[int4*3+1]; 32 var ya=(y1+y2+y3+y4)/4; 33 var yb=Math.max(Math.abs(y1-ya),Math.abs(y2-ya),Math.abs(y3-ya),Math.abs(y4-ya)); 34 arr_Matrix[i][j]=parseInt((yb)/0.707);//高度超过了水平距离几倍就认为是几倍的障碍物 35 } 36 } 37 return arr_Matrix; 38 }
这一段代码的思路是:将地面网格垂直方向的正投影作为寻路单元格,取每一个寻路单元格的四个顶点,算出这四个顶点的平均高度与每个顶点高度的差的最大值与寻路单元格中心点到顶点的水平距离的比,将这个比值作为“障碍程度”(简单来说就是顶点高度变化的越剧烈,障碍就越难跨越)。然后为网格添加一个mydata属性(javascript语言的优势),把和寻路矩阵有关的信息放到这个属性里。
这里用到了numeric数学库,可以在http://www.numericjs.com/查看文档。
2、生成babylon格式的3D模型:
网格生成完毕后需要拿到其他程序中使用,这里我选择把它保存为babylon格式的3D模型,babylon是一种json字符串模型文件,其优点是结构简单功能全面。
Babylon.js的官方网站上有完整的格式说明和例子:http://doc.babylonjs.com/generals/file_format_map_(.babylon),遗憾的是例子里的一行少了一个逗号所以会导致导入出错,不知道现在改正了没有。
以下是生成对应json的代码:
1 /** 2 * Created by Administrator on 2017/7/14. 3 */ 4 function Export_mesh(arr_mesh,PngName)//用Babylon格式导出模型 5 { 6 //场景对象 7 var obj_scene= 8 { 9 \'autoClear\': true, 10 \'clearColor\': [0,0,0], 11 \'ambientColor\': [0,0,0], 12 \'gravity\': [0,-9.81,0], 13 \'cameras\': [{ 14 \'name\': \'Camera\', 15 \'id\': \'Camera\', 16 \'position\': [7.4811,5.3437,-6.5076], 17 \'target\': [-0.3174,0.8953,0.3125], 18 \'fov\': 0.8576, 19 \'minZ\': 0.1, 20 \'maxZ\': 100, 21 \'speed\': 1, 22 \'inertia\': 0.9, 23 \'checkCollisions\': false, 24 \'applyGravity\': false, 25 \'ellipsoid\': [0.2,0.9,0.2] 26 }], 27 \'activeCamera\': \'Camera\', 28 \'lights\': [{ 29 \'name\': \'Sun\', 30 \'id\': \'Sun\', 31 \'type\': 1, 32 \'position\': [0.926,7.3608,14.1829], 33 \'direction\': [-0.347,-0.4916,-0.7987], 34 \'intensity\': 1, 35 \'diffuse\': [1,1,1], 36 \'specular\': [1,1,1] 37 }], 38 \'materials\':[{ 39 \'name\': \'mball\', 40 \'id\': \'mball\', 41 \'ambient\': [1,1,1], 42 \'diffuse\': [1,1,1], 43 \'specular\': [1,1,1], 44 \'specularPower\': 50, 45 \'emissive\': [0,0,0], 46 \'alpha\': 1, 47 \'backFaceCulling\': true, 48 \'diffuseTexture\': { 49 \'name\': PngName?PngName:\'snow2.jpg\', 50 \'level\': 1, 51 \'hasAlpha\': 1, 52 \'coordinatesMode\': 0, 53 \'uOffset\': 0, 54 \'vOffset\': 0, 55 \'uScale\': 1, 56 \'vScale\': 1, 57 \'uAng\': 0, 58 \'vAng\': 0, 59 \'wAng\': 0, 60 \'wrapU\': true, 61 \'wrapV\': true, 62 \'coordinatesIndex\': 0 63 } 64 }], 65 \'geometries\': {}, 66 \'meshes\': [], 67 \'multiMaterials\': [], 68 \'shadowGenerators\': [], 69 \'skeletons\': [], 70 \'sounds\': [], 71 \'mydata\':{\'walkabilityMatrix\':[]} 72 }; 73 //所有模型组件的父物体 74 var obj_allbase= 75 { 76 \'name\': \'allbase\', 77 \'id\': \'allbase\', 78 \'materialId\': \'mball\', 79 \'position\': [0,0,0], 80 \'rotation\': [0,0,0], 81 \'scaling\': [1,1,1], 82 \'isVisible\': true, 83 \'isEnabled\': true, 84 \'checkCollisions\': false, 85 \'billboardMode\': 0, 86 \'receiveShadows\': true, 87 \'positions\': [], 88 \'normals\': [], 89 \'uvs\': [], 90 \'indices\': [], 91 \'subMeshes\': [{ 92 \'materialIndex\': 0, 93 \'verticesStart\': 0, 94 \'verticesCount\': 0, 95 \'indexStart\': 0, 96 \'indexCount\': 0 97 }] 98 }; 99 obj_scene.meshes.push(obj_allbase); 100 var len=arr_mesh.length; 101 var all_x=0; 102 var all_y=0; 103 var all_z=0; 104 for(var i=0;i<len;i++) 105 { 106 var obj_child={}; 107 if(arr_mesh[i].geometry._vertexBuffers!=null) 108 { 109 var child=arr_mesh[i]; 110 if(!child.mydata) 111 { 112 child.mydata={} 113 } 114 var vb=child.geometry._vertexBuffers; 115 all_x+=child.position.x; 116 all_y+=child.position.y; 117 all_z+=child.position.z; 118 obj_child= 119 { 120 \'name\': child.name, 121 \'id\': child.id, 122 \'parentID\': \'allbase\', 123 \'materialId\': \'mball\', 124 \'position\': [child.position.x,child.position.y,child.position.z], 125 \'rotation\': [child.rotation.x,child.rotation.y,child.rotation.z], 126 \'scaling\': [child.scaling.x,child.scaling.y,child.scaling.z], 127 \'isVisible\': true, 128 \'isEnabled\': true, 129 \'checkCollisions\': false, 130 \'billboardMode\': 0, 131 \'receiveShadows\': true, 132 \'positions\': vb.position._buffer._data, 133 \'normals\': vb.normal._buffer._data, 134 \'uvs\': vb.uv._buffer._data, 135 \'indices\': child.geometry._indices, 136 \'subMeshes\': [{ 137 \'materialIndex\': 0, 138 \'verticesStart\': 0, 139 \'verticesCount\': vb.position._buffer._data.length, 140 \'indexStart\': 0, 141 \'indexCount\': child.geometry._indices.length 142 }], 143 \'mydata\':child.mydata 144 }; 145 obj_scene.meshes.push(obj_child); 146 } 147 } 148 //不能让模型的主体过于偏离模型的中心 149 all_x=all_x/len; 150 all_y=all_y/len; 151 all_z=all_z/len; 152 for(var i=1;i<len+1;i++) 153 { 154 obj_scene.meshes[i].position[0]-=all_x; 155 obj_scene.meshes[i].position[1]-=all_y; 156 obj_scene.meshes[i].position[2]-=all_z; 157 } 158 var str_data=JSON.stringify(obj_scene); 159 DownloadText(MakeDateStr()+"testscene",str_data,".babylon"); 160 }
可以看出,一个babylon文件可以包含多个网格对象,除了网格对象之外这个模型文件还可以存储场景、光照、相机、动画、骨骼等信息,这些功能可以选择性使用。方法的最后使用DownloadText方法将json文本导出,DownloadText是我参考网络资料编写的字符下载方法,如果不使用DownloadText,直接在Chrome浏览器的调试模式下的命令行里输入“console.log(str_data)”也能得到json字符串。
DownloadText内容如下:
1 /** 2 * Created by Administrator on 2015/3/2. 3 */ 4 /** 5 * 将指定字符写入指定名称的文本文件中,并可以选择本地保存目录,兼容IE11和谷歌浏览器 6 */ 7 function DownloadText(filename,content,filetype) 8 { 9 if(filetype==null) 10 { 11 filetype=".txt"; 12 } 13 if(document.createElement("a").download!=null)//谷歌和火狐 14 { 15 var aLink = document.createElement(\'a\'); 16 var datatype="data:text/plain;charset=UTF-8,"; 17 if(filetype==".xml") 18 { 19 datatype="data:text/xml;charset=UTF-8,"; 20 } 21 if(filetype==".babylon") 22 {//浏览器还没有支持babylon的mime类型!! 23 datatype="data:text/plain;charset=UTF-8,"; 24 } 25 if(filetype==".png"||filetype==".jpeg") 26 { 27 datatype=""; 28 } 29 if(content.length<1000000) 30 { 31 aLink.href = datatype+content;//dataurl格式的字符串" 32 } 33 else 34 {//对于过大的文件普通dataURL不支持,所以使用“二进制流大对象” 35 aLink.href=URL.createObjectURL(new Blob([content],{type:"text/plain"})); 36 } 37 aLink.download = filename; 38 aLink.innerHTML=filename; 39 //aLink.setAttribute("onclick",""); 40 aLink.onclick=function() 41 { 42 document.getElementById("div_choose").style.display="none"; 43 //delete_div(\'div_choose\'); 44 delete_div(\'div_mask\'); 45 } 46 //aLink.style.display="none"; 47 //document.body.appendChild(aLink); 48 /*var evt = document.createEvent("HTMLEvents");//建立一个事件 49 evt.initEvent("click", false, false);//这是一个单击事件 50 evt.eventType = \'message\'; 51 aLink.dispatchEvent(evt);//触发事件*/ 52 //chrome认为点击超链接下载文件是超链接标签的“默认属性”,谷歌认为默认属性不可以用脚本来触发,所以从M53版本开始dispatchEvent无法触发超链接下载 53 //window.open(datatype+content, "_blank"); 54 //document.write(datatype+content); 55 delete_div(\'div_choose\'); 56 delete_div(\'div_mask\'); 57 var evt=evt||window.event; 58 cancelPropagation(evt); 59 var obj=evt.currentTarget?evt.currentTarget:evt.srcElement; 60 61 Open_div("", "div_choose", 240, 180, 400, 80, "", "",1,401);//打开一个带遮罩的弹出框 62 var div_choose=$("#div_choose")[0]; 63 div_choose.style.border="1px solid"; 64 div_choose.innerHTML="<span>谷歌浏览器专用文件生成完毕,请点击下面的文件名下载文件。</span><br>" 65 div_choose.appendChild(aLink); 66 drag(div_choose);//让弹出框可以被拖拽 67 aLink.onmousedown=function() 68 { 69 var evt=evt||window.event; 70 cancelPropagation(evt); 71 } 72 } 73 else//IE 74 { 75 var Folder=BrowseFolder(); 76 if(Folder=="false") 77 { 78 alert("保存失败!"); 79 } 80 else 81 { 82 var fso, tf; 83 fso = new ActiveXObject("Scripting.FileSystemObject");//创建文件系统对象 84 tf = fso.CreateTextFile(Folder + filename+filetype, true,true);//创建一个文件 85 tf.write(content); 86 tf.Close(); 87 alert("保存完毕!"); 88 } 89 } 90 } 91 function BrowseFolder() 92 {//使用ActiveX控件 93 try 94 { 95 var Message = "请选择保存文件夹"; //选择框提示信息 96 var Shell = new ActiveXObject( "Shell.Application" ); 97 var Folder = Shell.BrowseForFolder(0,Message,0x0040,0x11);//起始目录为:我的电脑 98 //var Folder = Shell.BrowseForFolder(0,Message,0); //起始目录为:桌面//选择桌面会报错!! 99 100 if(Folder != null) 101 { 102 Folder = Folder.items(); // 返回 FolderItems 对象 103 Folder = Folder.item(); // 返回 Folderitem 对象 104 Folder = Folder.Path; // 返回路径 105 if(Folder.charAt(Folder.length-1) != "\\\\") 106 { 107 Folder = Folder + "\\\\"; 108 } 109 //document.all.savePath.value=Folder; 110 return Folder; 111 } 112 } 113 catch(e) 114 { 115 return "false"; 116 alert(e.message); 117 } 118 }
接下来,我们要在另一个程序中使用上面生成的模型文件,使用Babylonjs的资源管理器加载网格:
1 this.loader = new BABYLON.AssetsManager(this.scene);//资源管理器 2 3 // 资源数组 4 this.assets = {}; 5 //为资源管理器分配一个任务 6 var meshTask = this.loader.addMeshTask("gun", "", "./assets/", "gun.babylon"); 7 meshTask.onSuccess = function(task) {//这个任务完成 8 _this._initMesh(task); 9 };
//第一个参数表示task的name,第二个参数表示加载模型文件中的哪个网格,为空则用数组形式加载全部,第三个参数表示路径,第四个参数是文件名 10 var meshTask2 = this.loader.addMeshTask("mesh_ground", "", "./assets/arena/", "2017810_14_12_59testscene.babylon"); 11 meshTask2.onSuccess = function(task) { 12 _this._initMesh(task); 13 }; 14 15 this.loader.onFinish = function (tasks)//所有任务完成 16 { 17 。。。 18 } 19 20 。 21 。 22 。 23 24 _initMesh : function(task) 25 { 26 this.assets[task.name] = task.loadedMeshes; 27 for (var i=0; i<task.loadedMeshes.length; i++ ){ 28 var mesh = task.loadedMeshes[i]; 29 mesh.isVisible = false; 30 //预先把所有资源加载下来,但不显示,当需要时再把它显示在需要的位置,或者在需要的位置,建立一个资源的实例(克隆) 31 } 32 } 33
这时,会发生一个小问题:Babylonjs并不支持我们夹带在mesh中的mydata属性。解决方法是在babylon.30.all.max.js的21272行附近修改:
1 if (parsedMesh.metadata !== undefined) { Phong 和 Gouraud 着色 WebGL