设计目标:借鉴前辈编程者的经验将简单的配置文本转化为3D场景,并根据配置文件在场景中加入图片和可播放的视频,最终形成可浏览的3D陈列室。
一、使用效果
1、txt配置文件:
(博客园的富文本编辑器会改变txt文本的排版,所以用图片方式呈现文本)
第一行表示陈列室的每一层前后最多有5个房间,左右最多有8个房间,接下来是第一层的地图:“0”表示普通房间,“+、-、|”表示连接房间的通道,“#”表示地面有洞的房间可用来连接下一层,“^”表示顶部有洞的房间用来连接上一层。“//source”后面是本层的资源,用竖线分隔的参数依次表示前后位置、左右位置、资源“贴在”哪面墙上、资源类型、资源url,比如“//source:2|4|z|mp4|big_buck_bunny.mp4”即表示在第二行第四列的房间的z面(前面)贴上url为big_buck_bunny.mp4的mp4视频。再下面则是-1层的地图。
2、显示效果
房间的整体效果如下:
渲染出了配置文件中设置的房间布局,可以通过修改代码替换默认的草地和线框纹理。场景默认使用Babylon.js的自由相机进行控制,按“v”键可以启用fps式控制,鼠标移动直接控制视角,wasd控制前后左右,c和空格控制升降,同时启用相机与墙壁的碰撞检测阻止穿墙,再次按v键则可关闭fps控制。
进入第二层中间的房间可以看到房间两侧的视频,点击即可播放:
进入第二层右侧的房间,可以看到相邻的小房间融合为一个大房间:
可以通过https://ljzc002.github.io/Txt2room/HTML/PAGE/room.html查看在线实例,代码没有进行编译可以直接在线调试,在https://github.com/ljzc002/ljzc002.github.io/tree/master/Txt2room查看项目代码。
二、代码实现
1、建立房间零件的源网格(master mesh)
为了提高渲染效率,这里并没有为每个房间建立独立的mesh对象,而是将房间拆解为基础的组成零件,对零件建立源网格,然后用WebGL的instance技术批量生成源网格的实例。
以下是生成预制件源网格的方法:
1 var size_per_u=3;//1纹理坐标长度对应场景的3单位长度 2 var size_per_v=3; 3 var positions=[]; 4 var uvs=[]; 5 var normals=[]; 6 var indices=[]; 7 function initMeshClass() 8 {//plan的基础状态是一个位于原点,面向z轴负方向的平面 9 add_plan2({x:-4.5,y:4.5,z:0},{x:1.5,y:4.5,z:0},{x:1.5,y:1.5,z:0},{x:-4.5,y:1.5,z:0},0); 10 add_plan2({x:1.5,y:4.5,z:0},{x:4.5,y:4.5,z:0},{x:4.5,y:-1.5,z:0},{x:1.5,y:-1.5,z:0},4,6/size_per_u); 11 add_plan2({x:-1.5,y:-1.5,z:0},{x:4.5,y:-1.5,z:0},{x:4.5,y:-4.5,z:0},{x:-1.5,y:-4.5,z:0},8,3/size_per_u,6/size_per_v); 12 add_plan2({x:-4.5,y:1.5,z:0},{x:-1.5,y:1.5,z:0},{x:-1.5,y:-4.5,z:0},{x:-4.5,y:-4.5,z:0},12,0,3/size_per_v); 13 var mesh=vertexData2Mesh(positions, indices, normals, uvs,"class_hole",mat_grass); 14 mesh.setEnabled(false);//令源网格不显示 15 // 很奇怪如果不对长通道设置mesh.setEnabled(false);则实例无法正常显示,但其他类的实例则没有这种问题。 16 //mesh.setEnabled(true);//默认就是这个 17 obj_meshclass["hole"]=mesh;//带有洞的墙壁 18 19 positions=[];//新建式清空,理论上不影响引用的数据 20 uvs=[]; 21 normals=[]; 22 indices=[]; 23 add_plan2({x:-4.5,y:4.5,z:0},{x:4.5,y:4.5,z:0},{x:4.5,y:-4.5,z:0},{x:-4.5,y:-4.5,z:0},0); 24 var mesh=vertexData2Mesh(positions, indices, normals, uvs,"class_wall",mat_grass); 25 mesh.setEnabled(false); 26 obj_meshclass["wall"]=mesh;//墙壁 27 28 positions=[]; 29 uvs=[]; 30 normals=[]; 31 indices=[]; 32 add_plan2({x:-1.5,y:1.5,z:0},{x:1.5,y:1.5,z:0},{x:1.5,y:-1.5,z:0},{x:-1.5,y:-1.5,z:0},0); 33 var mesh=vertexData2Mesh(positions, indices, normals, uvs,"class_smallwall",mat_grass); 34 mesh.setEnabled(false); 35 obj_meshclass["smallwall"]=mesh;//小块墙壁 36 37 positions=[]; 38 uvs=[]; 39 normals=[]; 40 indices=[]; 41 add_plan2({x:-1.5,y:1.5,z:-4.5},{x:-1.5,y:1.5,z:4.5},{x:1.5,y:1.5,z:4.5},{x:1.5,y:1.5,z:-4.5},0); 42 add_plan2({x:1.5,y:1.5,z:-4.5},{x:1.5,y:1.5,z:4.5},{x:1.5,y:-1.5,z:4.5},{x:1.5,y:-1.5,z:-4.5},4); 43 add_plan2({x:1.5,y:-1.5,z:-4.5},{x:1.5,y:-1.5,z:4.5},{x:-1.5,y:-1.5,z:4.5},{x:-1.5,y:-1.5,z:-4.5},8); 44 add_plan2({x:-1.5,y:-1.5,z:-4.5},{x:-1.5,y:-1.5,z:4.5},{x:-1.5,y:1.5,z:4.5},{x:-1.5,y:1.5,z:-4.5},12); 45 var mesh=vertexData2Mesh(positions, indices, normals, uvs,"class_channel",mat_frame); 46 //mesh.setEnabled(false); 47 // 很奇怪如果不对长通道设置mesh.setEnabled(false);则实例无法正常显示,但其他类的实例则没有这种问题。 48 mesh.setEnabled(false); 49 obj_meshclass["channel"]=mesh;//长通道 50 51 positions=[]; 52 uvs=[]; 53 normals=[]; 54 indices=[]; 55 add_plan2({x:-1.5,y:1.5,z:1.5},{x:-1.5,y:1.5,z:4.5},{x:1.5,y:1.5,z:4.5},{x:1.5,y:1.5,z:1.5},0); 56 add_plan2({x:1.5,y:1.5,z:1.5},{x:1.5,y:1.5,z:4.5},{x:1.5,y:-1.5,z:4.5},{x:1.5,y:-1.5,z:1.5},4); 57 add_plan2({x:1.5,y:-1.5,z:1.5},{x:1.5,y:-1.5,z:4.5},{x:-1.5,y:-1.5,z:4.5},{x:-1.5,y:-1.5,z:1.5},8); 58 add_plan2({x:-1.5,y:-1.5,z:1.5},{x:-1.5,y:-1.5,z:4.5},{x:-1.5,y:1.5,z:4.5},{x:-1.5,y:1.5,z:1.5},12); 59 var mesh=vertexData2Mesh(positions, indices, normals, uvs,"class_shortchannel",mat_grass); 60 mesh.setEnabled(false); 61 obj_meshclass["shortchannel"]=mesh;//短通道 62 }
其中add_plan2方法用来根据四个给定的顶点生成平整的四边形顶点数据:
1 //平面四个顶点的坐标(从左上角开始顺时针排列),第一个顶点的插入索引,uv纹理的坐标的偏移量 2 function add_plan2(v1,v2,v3,v4,index,offsetu,offsetv) 3 { 4 positions.push(v1.x); 5 positions.push(v1.y); 6 positions.push(v1.z); 7 positions.push(v2.x); 8 positions.push(v2.y); 9 positions.push(v2.z); 10 positions.push(v3.x); 11 positions.push(v3.y); 12 positions.push(v3.z); 13 positions.push(v4.x); 14 positions.push(v4.y); 15 positions.push(v4.z); 16 //使用和Babylon.js条带网格相同的顶点顺序 17 indices.push(index+3); 18 indices.push(index+2); 19 indices.push(index); 20 indices.push(index+1); 21 indices.push(index); 22 indices.push(index+2); 23 //根据顶点位置计算平整纹理坐标 24 //1234对应abcd 25 var vab=v3subtract(v2,v1); 26 var lab=v3length(vab); 27 var vac=v3subtract(v3,v1); 28 var lac=v3length(vac); 29 var vad=v3subtract(v4,v1); 30 var lad=v3length(vad); 31 32 var BAC=Math.acos((vab.x*vac.x+vab.y*vac.y+vab.z*vac.z)/(lab*lac)); 33 var BAD=Math.acos((vab.x*vad.x+vab.y*vad.y+vab.z*vad.z)/(lab*lad)); 34 if(!offsetu) 35 { 36 offsetu=0; 37 } 38 if(!offsetv) 39 { 40 offsetv=0; 41 } 42 uvs.push(offsetu); 43 uvs.push(offsetv); 44 uvs.push(offsetu+lab/size_per_u); 45 uvs.push(offsetv); 46 uvs.push(offsetu+(lac*Math.cos(BAC)/size_per_u)); 47 uvs.push(offsetv+(lac*Math.sin(BAC)/size_per_v)); 48 uvs.push(offsetu+(lad*Math.cos(BAD)/size_per_u)); 49 uvs.push(offsetv+(lad*Math.sin(BAD)/size_per_v)); 50 } 51 function v3subtract(v1,v2)//向量相减 52 { 53 return {x:(v1.x-v2.x),y:(v1.y-v2.y),z:(v1.z-v2.z)} 54 } 55 function v3length(v)//计算向量长度 56 { 57 return Math.pow(v.x*v.x+v.y*v.y+v.z*v.z,0.5) 58 }
计算平整纹理坐标使用了向量点乘的性质:vab.x*vac.x+vab.y*vac.y+vab.z*vac.z=vab.vac=lab*lac*cosCAB
这使得我们可以根据三角形三个顶点的坐标计算出其中两个向量的夹角,进而在这两个向量确定的平面中计算出三个顶点的纹理坐标。
可以在https://forum.babylonjs.com/t/which-way-should-i-choose-to-make-a-custom-mesh-from-ribbon/10793查看一些关于为什么要进行纹理平整的讨论。
vertexData2Mesh方法用来将生成的顶点数据转化为网格,
1 function vertexData2Mesh(positions, indices, normals, uvs,name,material) 2 { 3 var vertexData= new BABYLON.VertexData();//顶点数据对象 4 BABYLON.VertexData.ComputeNormals(positions, indices, normals);//计算法线 5 BABYLON.VertexData._ComputeSides(0, positions, indices, normals, uvs); 6 vertexData.indices = indices.concat();//索引 7 vertexData.positions = positions.concat(); 8 vertexData.normals = normals.concat();//position改变法线也要改变!!!! 9 vertexData.uvs = uvs.concat(); 10 var mesh=new BABYLON.Mesh(name,scene); 11 vertexData.applyToMesh(mesh, true); 12 mesh.vertexData=vertexData; 13 mesh.material=material; 14 mesh.renderingGroupId=2; 15 return mesh; 16 }
最后mesh.setEnabled(false)用来隐藏源网格。
2、读取配置文本,提取房间信息
1 var str=newland.importString("06.txt"); 2 //console.log(str); 3 var arr=str.split(" ")//限于window操作系统下?? 4 var len=arr.length; 5 for(var i=0;i<len;i++)//对于每一行 6 { 7 var line=arr[i]; 8 if(line.substring(0,2)=="//") 9 { 10 var arr2=line.substring(2).split("@"); 11 var len2=arr2.length; 12 for(var j=0;j<len2;j++) 13 { 14 var obj=arr2[j]; 15 var arr3=obj.split(":"); 16 var ptype=arr3[0]; 17 var pvalue=arr3[1]; 18 if(ptype=="seg_z") 19 { 20 seg_z=parseInt(pvalue); 21 } 22 else if(ptype=="seg_x") 23 { 24 seg_x=parseInt(pvalue); 25 } 26 else if(ptype=="floor")//进入了一层 27 { 28 i=handleFloor(pvalue,arr,i+1); 29 } 30 } 31 } 32 }
其中importString是一个读取服务端文本文件的方法,其代码如下:
1 newland.importString=function(url) 2 { 3 var xhr=new XMLHttpRequest; 4 xhr.open("GET",url,false);//第三个参数表示是同步加载 5 xhr.send(null); 6 var data=xhr.responseText; 7 return data; 8 }
读入后一行行遍历文本,发现“//floor”则开始处理这一层的房间数据:
1 function handleFloor(int_floor,arr,index) 2 { 3 var floor=obj_building[int_floor];//在obj_building中保存所有房间信息 4 if(!floor) 5 { 6 obj_building[int_floor]={}; 7 floor=obj_building[int_floor]; 8 } 9 var len=arr.length; 10 var count=0; 11 //继续读txt文本 12 for(var i=index;i<len;i++) 13 { 14 var line=arr[i]; 15 count++; 16 if(count<=seg_z) 17 { 18 19 if(!floor[count+""]) 20 { 21 floor[count+""]={} 22 } 23 24 for(var j=0;j<seg_x;j++) 25 { 26 if(line[j]) 27 { 28 29 floor[count+""][j+1+""]={type:line[j],arr_source:[]};//这个“数组”都是从一开始的 30 //addRoom(count-1,j);//行、列,规划完毕后统一添加渲染 31 } 32 } 33 } 34 else 35 { 36 if(line.substring(0,7)=="//floor")//查找到另一层 37 { 38 return (index+count-2); 39 } 40 else if(line.substring(0,8)=="//source")//为这个房间设置资源 41 { 42 //var arr2=line.split(":")[1].split("|"); 43 var arr2=line.substring(line.search(":")+1).split("|"); 44 if(floor[arr2[0]]&&floor[arr2[0]][arr2[1]]) 45 { 46 var arr_source=floor[arr2[0]][arr2[1]].arr_source;//这个房间的资源列表 47 var obj={}; 48 obj.sourceSide=arr2[2]; 49 obj.sourceType=arr2[3]; 50 obj.sourceUrl=arr2[4]; 51 arr_source.push(obj); 52 } 53 54 } 55 } 56 57 } 58 return (len);//查找到文件末尾 59 }
经过以上处理,配置文件中的房间信息都被提取到obj_building中。
3、根据房间信息排列源网格的实例,并放置资源。
代码如下:(有一定冗余)
1 function handleBuilding() 2 { 3 var len=0; 4 for(var key in obj_building) 5 { 6 len++;//总层数 7 } 8 for(var key in obj_building)//对于每一层 9 { 10 var int_key=parseInt(key); 11 var floor=obj_building[key]; 12 //寻找这一层的上下两层,这里假设obj_building是没有顺序的 13 var int_key_shang=int_key,int_key_xia=int_key; 14 var floor_shang=null,floor_xia=null; 15 //for(var i=int_key;i<) 16 for(var key2 in obj_building) 17 { 18 var int_key2=parseInt(key2); 19 if((int_key2>int_key)&&(int_key_shang==int_key||int_key_shang>int_key2)) 20 { 21 int_key_shang=int_key2; 22 floor_shang=obj_building[key2]; 23 } 24 if((int_key2<int_key)&&(int_key_xia==int_key||int_key_xia<int_key2)) 25 { 26 int_key_shang=int_key2; 27 floor_xia=obj_building[key2]; 28 } 29 } 30 for(var i=1;i<=seg_z;i++)//对于本层的每一行房间 31 { 32 var row=floor[i+""]; 33 if(row)//如果有这一行 34 { 35 for(var j=0;j<=seg_x;j++)//对于本行的每一个房间 36 { 37 var room=row[j+""]; 38 //根据房间的类型不同决定是否要查看其周围的房间 39 if(room) 40 {//@@@@普通房间,要考虑前后左右的四个房间状态,要考虑资源放置 41 if(room.type=="0"||room.type=="#"||room.type=="^") 42 { 43 //room.arr_source=[]; 44 //考虑前面 45 if(floor[i-1+""]&&floor[i-1+""][j+""]) 46 { 47 var room2=floor[i-1+""][j+""]; 48 if(!room2)//如果没有东西,就是普通墙壁 49 { 50 //网格类型,实例名字,位置,姿态 51 drawMesh("wall","wall_z_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(0.5-i)*sizez) 52 ,new BABYLON.Vector3(0,0,0)) 53 } 54 55 else if(room2.type=="|"||room2.type=="+") 56 { 57 drawMesh("hole","hole_z_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(0.5-i)*sizez) 58 ,new BABYLON.Vector3(0,0,0)) 59 } 60 else if(room2.type=="0"||room2.type=="#"||room2.type=="^")//旁边也是一个房间则合并房间,不绘制墙壁 61 { 62 63 } 64 else//默认绘制墙壁 65 { 66 drawMesh("wall","wall_z_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(0.5-i)*sizez) 67 ,new BABYLON.Vector3(0,0,0)) 68 } 69 } 70 else//默认绘制墙壁 71 { 72 drawMesh("wall","wall_z_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(0.5-i)*sizez) 73 ,new BABYLON.Vector3(0,0,0)) 74 } 75 //后面 76 if(floor[i+1+""]&&floor[i+1+""][j+""]) 77 { 78 var room2=floor[i+1+""][j+""]; 79 if(!room2)//如果没有东西,就是普通墙壁 80 { 81 //网格类型,实例名字,位置,姿态 82 drawMesh("wall","wall_z-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(-0.5-i)*sizez) 83 ,new BABYLON.Vector3(0,0,0)) 84 } 85 86 else if(room2.type=="|"||room2.type=="+") 87 { 88 drawMesh("hole","hole_z-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(-0.5-i)*sizez) 89 ,new BABYLON.Vector3(0,0,0)) 90 } 91 else if(room2.type=="0"||room2.type=="#"||room2.type=="^")//旁边也是一个房间则合并房间,不绘制墙壁 92 { 93 94 } 95 else//默认绘制墙壁 96 { 97 drawMesh("wall","wall_z-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(-0.5-i)*sizez) 98 ,new BABYLON.Vector3(0,0,0)) 99 } 100 } 101 else//默认绘制墙壁 102 { 103 drawMesh("wall","wall_z-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(-0.5-i)*sizez) 104 ,new BABYLON.Vector3(0,0,0)) 105 } 106 //左边 107 if(floor[i+""][j-1+""]) 108 { 109 var room2=floor[i+""][j-1+""]; 110 if(!room2)//如果没有东西,就是普通墙壁 111 { 112 //网格类型,实例名字,位置,姿态 113 drawMesh("wall","wall_x-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j-0.5)*sizex,int_key*sizey,(-i)*sizez) 114 ,new BABYLON.Vector3(0,Math.PI/2,0)) 115 } 116 117 else if(room2.type=="-"||room2.type=="+") 118 { 119 drawMesh("hole","hole_x-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j-0.5)*sizex,int_key*sizey,(-i)*sizez) 120 ,new BABYLON.Vector3(0,Math.PI/2,0)) 121 } 122 else if(room2.type=="0"||room2.type=="#"||room2.type=="^")//旁边也是一个房间则合并房间,不绘制墙壁 123 { 124 125 } 126 else//默认绘制墙壁 127 { 128 drawMesh("wall","wall_x-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j-0.5)*sizex,int_key*sizey,(-i)*sizez) 129 ,new BABYLON.Vector3(0,Math.PI/2,0)) 130 } 131 } 132 else//默认绘制墙壁 133 { 134 drawMesh("wall","wall_x-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j-0.5)*sizex,int_key*sizey,(-i)*sizez) 135 ,new BABYLON.Vector3(0,Math.PI/2,0)) 136 } 137 //右边 138 if(floor[i+""][j+1+""]) 139 { 140 var room2=floor[i+""][j+1+""]; 141 if(!room2)//如果没有东西,就是普通墙壁 142 { 143 //网格类型,实例名字,位置,姿态 144 drawMesh("wall","wall_x_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j+0.5)*sizex,int_key*sizey,(-i)*sizez) 145 ,new BABYLON.Vector3(0,Math.PI/2,0)) 146 } 147 148 else if(room2.type=="-"||room2.type=="+") 149 { 150 drawMesh("hole","hole_x_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j+0.5)*sizex,int_key*sizey,(-i)*sizez) 151 ,new BABYLON.Vector3(0,Math.PI/2,0)) 152 } 153 else if(room2.type=="0"||room2.type=="#"||room2.type=="^")//旁边也是一个房间则合并房间,不绘制墙壁 154 { 155 156 } 157 else//默认绘制墙壁 158 { 159 drawMesh("wall","wall_x_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j+0.5)*sizex,int_key*sizey,(-i)*sizez) 160 ,new BABYLON.Vector3(0,Math.PI/2,0)) 161 } 162 } 163 else//默认绘制墙壁 164 { 165 drawMesh("wall","wall_x_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j+0.5)*sizex,int_key*sizey,(-i)*sizez) 166 ,new BABYLON.Vector3(0,Math.PI/2,0)) 167 } 168 //上面 169 if(room.type=="^") 170 { 171 drawMesh("hole","hole_y_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,(int_key+0.5)*sizey,(-i)*sizez) 172 ,new BABYLON.Vector3(Math.PI/2,0,0)) 173 //还要负责向上连接 174 if(floor_shang) 175 { 176 for(var k=int_key+1;k<int_key_shang;k++) 177 { 178 drawMesh("channel","channel_^_"+k+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,(k)*sizey,(-i)*sizez) 179 ,new BABYLON.Vector3(Math.PI/2,0,0)) 180 } 181 } 182 //暂时不设置弹射器,使用失重模式 183 } 184 else 185 { 186 drawMesh("wall","wall_y_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,(int_key+0.5)*sizey,(-i)*sizez) 187 ,new BABYLON.Vector3(Math.PI/2,0,0)) 188 } 189 //下面 190 if(room.type=="#") 191 { 192 drawMesh("hole","hole_y-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,(int_key-0.5)*sizey,(-i)*sizez) 193 ,new BABYLON.Vector3(Math.PI/2,0,0)) 194 } 195 else 196 { 197 drawMesh("wall","wall_y-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,(int_key-0.5)*sizey,(-i)*sizez) 198 ,new BABYLON.Vector3(Math.PI/2,0,0)) 199 } 200 //翻转方向会影响碰撞检测吗? 201 //最后处理资源 202 } 203 //@@@@表示通道的三种符号,要考虑其前后左右的位置 204 else if(room.type=="-"||room.type=="+"||room.type=="|") 205 { 206 if(room.type=="-") 207 {//横向长通道 208 drawMesh("channel","channel_-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,(int_key)*sizey,(-i)*sizez) 209 ,new BABYLON.Vector3(0,Math.PI/2,0)) 210 } 211 else if(room.type=="|") 212 {//纵向长通道 213 drawMesh("channel","channel_|_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,(int_key)*sizey,(-i)*sizez) 214 ,new BABYLON.Vector3(0,0,0)) 215 } 216 else 217 {//十字连接件 218 //考虑前面 219 if(floor[i-1+""]&&floor[i-1+""][j+""]) 220 { 221 var room2=floor[i-1+""][j+""]; 222 if(!room2)//如果没有东西,就是普通墙壁 223 { 224 //网格类型,实例名字,位置,姿态 225 drawMesh("smallwall","smallwall_z_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(0.5/3-i)*sizez) 226 ,new BABYLON.Vector3(0,0,0)) 227 } 228 229 else if(room2.type=="|"||room2.type=="+"||room2.type=="0"||room2.type=="#"||room2.type=="^") 230 {//短通道自带位移 231 drawMesh("shortchannel","shortchannel_z_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(-i)*sizez) 232 ,new BABYLON.Vector3(0,0,0)) 233 } 234 else//默认绘制小型墙壁 235 { 236 drawMesh("smallwall","smallwall_z_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(0.5/3-i)*sizez) 237 ,new BABYLON.Vector3(0,0,0)) 238 } 239 } 240 else//默认绘制小型墙壁 241 { 242 drawMesh("smallwall","smallwall_z_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(0.5/3-i)*sizez) 243 ,new BABYLON.Vector3(0,0,0)) 244 } 245 //后面 246 if(floor[i+1+""]&&floor[i+1+""][j+""]) 247 { 248 var room2=floor[i+1+""][j+""]; 249 if(!room2)//如果没有东西,就是普通墙壁 250 { 251 //网格类型,实例名字,位置,姿态 252 drawMesh("smallwall","smallwall_z-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(-0.5/3-i)*sizez) 253 ,new BABYLON.Vector3(0,0,0)) 254 } 255 256 else if(room2.type=="|"||room2.type=="+"||room2.type=="0"||room2.type=="#"||room2.type=="^") 257 { 258 drawMesh("shortchannel","shortchannel_z-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(-i)*sizez) 259 ,new BABYLON.Vector3(0,Math.PI,0)) 260 } 261 else//默认绘制小型墙壁 262 { 263 drawMesh("smallwall","smallwall_z-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(-0.5/3-i)*sizez) 264 ,new BABYLON.Vector3(0,0,0)) 265 } 266 } 267 else//默认绘制小型墙壁 268 { 269 drawMesh("smallwall","smallwall_z-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3(j*sizex,int_key*sizey,(-0.5/3-i)*sizez) 270 ,new BABYLON.Vector3(0,0,0)) 271 } 272 //左边 273 if(floor[i+""][j-1+""]) 274 { 275 var room2=floor[i+""][j-1+""]; 276 if(!room2)//如果没有东西,就是小型墙壁 277 { 278 //网格类型,实例名字,位置,姿态 279 drawMesh("smallwall","smallwall_x-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j-0.5/3)*sizex,int_key*sizey,(-i)*sizez) 280 ,new BABYLON.Vector3(0,Math.PI/2,0)) 281 } 282 283 else if(room2.type=="-"||room2.type=="+"||room2.type=="0"||room2.type=="#"||room2.type=="^") 284 { 285 drawMesh("shortchannel","shortchannel_x-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,int_key*sizey,(-i)*sizez) 286 ,new BABYLON.Vector3(0,-Math.PI/2,0)) 287 } 288 else//默认绘制小型墙壁 289 { 290 drawMesh("smallwall","smallwall_x-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j-0.5/3)*sizex,int_key*sizey,(-i)*sizez) 291 ,new BABYLON.Vector3(0,Math.PI/2,0)) 292 } 293 } 294 else//默认绘制小型墙壁 295 { 296 drawMesh("smallwall","smallwall_x-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j-0.5/3)*sizex,int_key*sizey,(-i)*sizez) 297 ,new BABYLON.Vector3(0,Math.PI/2,0)) 298 } 299 //右边 300 if(floor[i+""][j+1+""]) 301 { 302 var room2=floor[i+""][j+1+""]; 303 if(!room2)//如果没有东西,就是普通墙壁 304 { 305 //网格类型,实例名字,位置,姿态 306 drawMesh("smallwall","smallwall_x_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j+0.5/3)*sizex,int_key*sizey,(-i)*sizez) 307 ,new BABYLON.Vector3(0,Math.PI/2,0)) 308 } 309 310 else if(room2.type=="-"||room2.type=="+"||room2.type=="0"||room2.type=="#"||room2.type=="^") 311 { 312 drawMesh("shortchannel","shortchannel_x_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,int_key*sizey,(-i)*sizez) 313 ,new BABYLON.Vector3(0,Math.PI/2,0)) 314 } 315 else//默认绘制墙壁 316 { 317 drawMesh("smallwall","smallwall_x_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j+0.5/3)*sizex,int_key*sizey,(-i)*sizez) 318 ,new BABYLON.Vector3(0,Math.PI/2,0)) 319 } 320 } 321 else//默认绘制墙壁 322 { 323 drawMesh("smallwall","smallwall_x_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j+0.5/3)*sizex,int_key*sizey,(-i)*sizez) 324 ,new BABYLON.Vector3(0,Math.PI/2,0)) 325 } 326 //上面 327 drawMesh("smallwall","smallwall_y_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,(int_key+0.5/3)*sizey,(-i)*sizez) 328 ,new BABYLON.Vector3(Math.PI/2,0,0)) 329 //下面 330 drawMesh("smallwall","smallwall_y-_"+int_key+"_"+i+"_"+j,new BABYLON.Vector3((j)*sizex,(int_key-0.5/3)*sizey,(-i)*sizez) 331 ,new BABYLON.Vector3(Math.PI/2,0,0)) 332 333 } 334 } 335 //如果这个房间有资源 336 if(room.arr_source) 337 { 338 var arr_source=room.arr_source; 339 var len=arr_source.length; 340 for(var k=0;k<len;k++) 341 { 342 var source=arr_source[k]; 343 if(source.sourceType=="mp4"||source.sourceType=="jpg"||source.sourceType=="png") 344 { 345 var mesh_plan=new BABYLON.MeshBuilder.CreatePlane(source.sourceType+"_"+source.sourceSide+"_"+int_key+"_"+i+"_"+j, 346 {height:4.5,8},scene);//建立一个平面网格用来展示资源 347 var pos={x:0,y:0,z:0},rot=new BABYLON.Vector3(0,0,0); 348 if(source.sourceSide=="z")//根据资源所在的墙壁不同调整资源网格的位置和姿态 349 { 350 pos.z=0.4 351 }else if(source.sourceSide=="z-") 352 { 353 pos.z=-0.4; 354 rot.y=Math.PI; 355 } 356 else if(source.sourceSide=="x") 357 { 358 pos.x=0.4; 359 rot.y=-Math.PI/2; 360 } 361 else if(source.sourceSide=="x-") 362 { 363 pos.x=-0.4; 364 rot.y=Math.PI/2; 365 } 366 else if(source.sourceSide=="y") 367 { 368 pos.y=0.4; 369 rot.x=Math.PI/2; 370 } 371 else if(source.sourceSide=="z-") 372 { 373 pos.y=-0.4; 374 rot.x=-Math.PI/2; 375 } 376 mesh_plan.position=new BABYLON.Vector3((j+pos.x)*sizex,(int_key+pos.y)*sizey,(-i+pos.z)*sizez); 377 mesh_plan.rotation=rot; 378 mesh_plan.renderingGroupId=2; 379 if(source.sourceType=="jpg"||source.sourceType=="png") 380 { 381 var materialf = new BABYLON.StandardMaterial("mat_"+source.sourceSide+"_"+int_key+"_"+i+"_"+j, scene); 382 383 materialf.diffuseTexture = new BABYLON.Texture(source.sourceUrl, scene); 384 materialf.diffuseTexture.hasAlpha = false; 385 materialf.backFaceCulling = true; 386 materialf.freeze(); 387 mesh_plan.material =materialf; 388 } 389 else if(source.sourceType=="mp4") 390 { 391 var mat = new BABYLON.StandardMaterial("mat_"+source.sourceSide+"_"+int_key+"_"+i+"_"+j, scene); 392 //从Chrome 66开始为了避免标签产生随机噪音禁止没有交互前使用js播放视频,所以后面要监听点击启动播放 393 var videoTexture = new BABYLON.VideoTexture("video_"+source.sourceSide+"_"+int_key+"_"+i+"_"+j, [source.sourceUrl], scene, true, false); 394 //videoTexture.video.autoplay=false;//这两个设置 395 //videoTexture.video.muted=true;不起作用 396 mat.diffuseTexture = videoTexture;//Babylon.js视频纹理 397 mat.emissiveColor=new BABYLON.Color3(1,1,1); 398 //监听到交互需求 399 // videoTexture.onUserActionRequestedObservable.add(() => { 400 // scene.onPointerDown = function (evt) { 401 // if(evt.pickInfo.pickedMesh == mesh_plan) 402 // { 403 // if(videoTexture.video.paused) 404 // { 405 // videoTexture.video.play(); 406 // } 407 // else 408 // { 409 // videoTexture.video.pause(); 410 // } 411 // } 412 // 413 // } 414 // }); 415 //mat.emissiveTexture= videoTexture; 416 mesh_plan.material =mat; 417 obj_videos[mesh_plan.name]=videoTexture; 418 if(false) 419 { 420 scene.onPointerDown = function (evt) {//这个evt是dom的,不会有pickInfo!! 421 if(evt.pickInfo&&(evt.pickInfo.pickedMesh == mesh_plan)) 422 { 423 if(videoTexture.video.paused) 424 { 425 videoTexture.video.play(); 426 } 427 else 428 { 429 videoTexture.video.pause(); 430 } 431 } 432 } 433 } 434 435 } 436 } 437 } 438 } 439 440 } 441 } 442 } 443 } 444 } 445 }
drawMesh方法用来在指定位置生成指定源网格的实例:
1 function drawMesh(type,name,pos,rot) 2 { 3 var instance=obj_meshclass[type].createInstance(name); 4 instance.position=pos; 5 instance.rotation=rot; 6 }
在完成零件组装后,再根据资源信息向设定的位置添加资源。
4、运动控制与碰撞检测
监听操作者的鼠标和键盘操作:
1 var node_temp; 2 function InitMouse() 3 { 4 canvas.addEventListener("blur",function(evt){//监听失去焦点 5 releaseKeyStateOut(); 6 }) 7 canvas.addEventListener("focus",function(evt){//改为监听获得焦点,因为调试失去焦点时事件的先后顺序不好说 8 releaseKeyStateIn(); 9 }) 10 11 //scene.onPointerPick=onMouseClick;//如果不attachControl onPointerPick不会被触发,并且onPointerPick必须pick到mesh上才会被触发 12 canvas.addEventListener("click", function(evt) {//这个监听也会在点击GUI按钮时触发!! 13 onMouseClick(evt);// 14 }, false); 15 canvas.addEventListener("dblclick", function(evt) {//是否要用到鼠标双击?? 16 onMouseDblClick(evt);// 17 }, false); 18 scene.onPointerMove=onMouseMove;//Babylon.js的事件监听属性 19 scene.onPointerDown=onMouseDown; 20 scene.onPointerUp=onMouseUp; 21 scene.onKeyDown=onKeyDown; 22 scene.onKeyUp=onKeyUp; 23 node_temp=new BABYLON.TransformNode("node_temp",scene);//用来提取相机的姿态矩阵 24 node_temp.rotation=camera0.rotation; 25 }
鼠标点击控制视频播放:
1 function onMouseDblClick(evt) 2 { 3 var pickInfo = scene.pick(scene.pointerX, scene.pointerY, null, false, camera0); 4 if(pickInfo.hit) 5 { 6 var mesh = pickInfo.pickedMesh; 7 if(mesh.name.split("_")[0]=="mp4")//重放视频 8 { 9 if(obj_videos[mesh.name]) 10 { 11 var videoTexture=obj_videos[mesh.name]; 12 13 videoTexture.video.currentTime =0; 14 15 } 16 } 17 } 18 } 19 function onMouseClick(evt) 20 { 21 var pickInfo = scene.pick(scene.pointerX, scene.pointerY, null, false, camera0); 22 if(pickInfo.hit) 23 { 24 var mesh = pickInfo.pickedMesh; 25 if(mesh.name.split("_")[0]=="mp4")//启停视频 26 { 27 if(obj_videos[mesh.name]) 28 { 29 var videoTexture=obj_videos[mesh.name]; 30 if(videoTexture.video.paused) 31 { 32 videoTexture.video.play(); 33 } 34 else 35 { 36 videoTexture.video.pause(); 37 } 38 } 39 } 40 } 41 42 }
在fps模式下通过鼠标移动控制相机视角
1 var lastPointerX,lastPointerY; 2 var flag_view="free"//free表示默认的自由移动状态,locked表示锁定鼠标的fps模式状态 3 var flag_locked; 4 var obj_keystate=[]; 5 function onMouseMove(evt) 6 { 7 8 if(flag_view=="locked") 9 { 10 evt.preventDefault(); 11 //绕y轴的旋转角度是根据x坐标计算的 12 var rad_y=((scene.pointerX-lastPointerX)/window.innerWidth)*(Math.PI/1);//将鼠标位置的变化转化为相机视角的变化 13 var rad_x=((scene.pointerY-lastPointerY)/window.innerHeight)*(Math.PI/1); 14 camera0.rotation.y+=rad_y; 15 camera0.rotation.x+=rad_x; 16 } 17 lastPointerX=scene.pointerX; 18 lastPointerY=scene.pointerY; 19 } 20 function onMouseDown(evt) 21 { 22 if(flag_view=="locked") { 23 evt.preventDefault(); 24 } 25 } 26 function onMouseUp(evt) 27 { 28 if(flag_view=="locked") { 29 evt.preventDefault(); 30 } 31 }
记录键盘按键状态
1 function onKeyDown(event) 2 { 3 if(flag_view=="locked") { 4 event.preventDefault(); 5 var key = event.key; 6 obj_keystate[key] = 1;//1表示按下 7 } 8 } 9 function onKeyUp(event) 10 { 11 var key = event.key; 12 if(key=="v"||key=="Escape")//按v键开闭fps模式 13 { 14 event.preventDefault(); 15 if(flag_view=="locked") 16 { 17 flag_view="free"; 18 document.exitPointerLock(); 19 } 20 else if(flag_view=="free") 21 { 22 flag_view="locked"; 23 canvas.requestPointerLock(); 24 } 25 } 26 if(flag_view=="locked") { 27 event.preventDefault(); 28 29 obj_keystate[key] = 0; 30 } 31 }
接下来在渲染循环中根据控制输入确定相机的位移:
var flag_speed=1; //var m_view=camera0.getViewMatrix(); //var m_view=camera0.getProjectionMatrix(); var m_view=node_temp.getWorldMatrix(); //只检测其运行方向?-》相对论问题!《-先假设直接外围环境不移动 if(obj_keystate["Shift"]==1)//Shift+w的event.key不是Shift和w,而是W!!!! { flag_speed=5;//加速移动 } var delta=engine.getDeltaTime();//两渲染帧之间的时间间隔(毫秒) //console.log(delta); flag_speed=flag_speed*engine.getDeltaTime()/10; var v_temp=new BABYLON.Vector3(0,0,0); if(obj_keystate["w"]==1) { v_temp.z+=0.1*flag_speed; } if(obj_keystate["s"]==1) { v_temp.z-=0.1*flag_speed; } if(obj_keystate["d"]==1) { v_temp.x+=0.05*flag_speed; } if(obj_keystate["a"]==1) { v_temp.x-=0.05*flag_speed; } if(obj_keystate[" "]==1) { v_temp.y+=0.05*flag_speed; } if(obj_keystate["c"]==1) { v_temp.y-=0.05*flag_speed; } //camera0.position=camera0.position.add(BABYLON.Vector3.TransformCoordinates(v_temp,camera0.getWorldMatrix()).subtract(camera0.position)); //engine.getDeltaTime() var pos_temp=camera0.position.add(BABYLON.Vector3.TransformCoordinates(v_temp,m_view));
根据按键状态和两帧之间的时间计算出相机在这一帧内的位移向量,需要注意的是这个位移向量以相机的局部坐标系为参考,为了在世界坐标系中使用它,建立了一个node_temp节点专门用来保存相机的姿态矩阵,对位移向量施加这个矩阵变化将它转化为世界坐标系中的位移矩阵。
接下来使用射线进行简单的碰撞检测:
1 var direction=pos_temp.subtract(pos_last);//pos_last是上一帧的相机位置,取新位置向量减旧位置向量的结果为物体的运动方向 2 //var direction=BABYLON.Vector3.TransformCoordinates(v_temp,m_view);//一次性计算的好处是只需绘制一条射线,缺点是容易射空 3 var ray = new BABYLON.Ray(camera0.position, direction, 1);//从camera0.position位置向direction方向,绘制长度为1的‘射线’ 4 var arr=scene.multiPickWithRay(ray); 5 arr.sort(sort_compare)//按距离从近到远排序 6 var len=arr.length; 7 8 var flag_hit=false; 9 for(var k=0;k<len;k++)//对于这条射线击中的每个三角形 10 { 11 var hit=arr[k]; 12 var mesh=hit.pickedMesh; 13 var distance=hit.distance; 14 if(mesh||mesh.name)//暂不限制mesh种类 15 { 16 console.log(mesh.name); 17 flag_hit=true; 18 break; 19 } 20 } 21 if(!flag_hit)//如果没有被阻拦,则替换位置 22 { 23 camera0.position=pos_temp; 24 } 25 else 26 { 27 camera0.position=pos_last;//回溯的太远了 28 }
以上渲染循环中的运动控制代码主要在fps模式下生效,尝试通过在检测到碰撞时调用camera0.position=pos_last;来阻止自由相机穿墙,但效果并不好。
三、总结:
编程结果基本达到设计目标,但在代码冗余、功能细节调试方面尚有不足,接下来可以考虑向程序中添加模型资源作为‘雕塑’展示、添加更多类型的零件、添加重力效果、添加WebSocket交互等。