前言:3D模型编辑的核心是对顶点位置和纹理颜色的编辑,这个研究的目的在于寻找一种通过编程方式直接对模型进行编辑的方法,这种编辑方法和时下流行的通过鼠标点选、拖拽进行编辑的方法之间的关系,和前端编程中“程序员编写静态网页”与“美工进行网页切图”之间的关系很相似。
一、工具用法:
1、访问 https://ljzc002.github.io/SnowMiku/HTML/MakeRibbon.html打开条带网格生成器页面
在场景世界坐标系的(0,-10,0),(0,0,0),(0,10,0)处各有一个绿色小球作为参考点,使用上下左右和鼠标拖动可以进行场景漫游。
2、按F12键打开Chrome控制台,在控制台中输入:MakeRibbon(MakeRing(5,12),-10,2,11,"mesh_ribbon")回车:
在场景中绘制了一个半径为5,曲面细分度为12,左端位于-10,每两个圆环间距2,共由11个圆环组成的圆柱面。
拉近查看:
3、输入ShowNormals(mesh_origin)将用红色线段显示每个顶点的法线方向
输入DeleteMeshes([lines_normal])可以删除所有的法线,输入DeleteMeshes([mesh_origin])则删除圆柱面网格。
4、鼠标移入网格上的三角形,会显示三角形的顶点信息:
其中“1:2-5”表示这是三角形的第一个顶点,这个顶点位于索引是2的圆环上(第三个圆环),这个顶点在圆环中的索引是5(也就是第六个顶点)。
5、输入PickPoints([[2,5],[3,5],[2,6]],mesh_origin)可以选定这些顶点
被选中顶点所影响的所有边框线标示为黄色,这个“选中”只是改变外观而已。
6、输入TransVertex(mesh_origin,arr_ij,BABYLON.Matrix.Translation(0,0,-10))将所选的顶点向z轴负方向移动10,被移动的顶点和前面选中的顶点其实没有关系,其中arr_ij也可以直接用索引数组[[2,5],[3,5],[2,6]]代替。
另一类变形可以通过输入:TransVertex(mesh_origin,arr_ij,BABYLON.Matrix.RotationX(Math.PI/2))实现,这可以把被选中的顶点绕X中旋转90度。
输入DeleteMeshes([lines_inpicked])取消被选中的效果,输入ChangeMaterial(mesh_origin,mat_blue)将边框换成蓝色纹理:
可以看到变形后的效果,接下来还可以继续选择顶点并变形
7、输入ExportMesh("1",mat_blue),以txt格式导出babylon模型文件,文件名为“1.txt”
8、将导出的txt改名为9.babylon后放入网站目录中,访问https://ljzc002.github.io/SnowMiku/HTML/LoadBabylon.html模型预览页面,在控制台输入ImportMesh("","../ASSETS/SCENE/","9.babylon")即可加载刚才导出的模型。
二、编程思路:
1、首先要建立一个可以进行各种测试的基础场景,使用的代码如下:
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>建立一个条带网格生成器,能够输入参数生成起始条带,然后通过命令行选取并修改pathArray,最后导出生成的条带</title> 6 <link href="../CSS/newland.css" rel="stylesheet"> 7 <link href="../CSS/stat.css" rel="stylesheet"> 8 <script src="../JS/MYLIB/Events.js"></script> 9 <script src="../JS/MYLIB/FileText.js"></script> 10 <script src="../JS/MYLIB/View.js"></script> 11 <script src="../JS/LIB/babylon.32.all.maxs.js"></script><!--V3.2的稳定版本--> 12 <script src="../JS/MYLIB/newland.js"></script> 13 <script src="../JS/LIB/stat.js"></script> 14 </head> 15 <body> 16 <div id="div_allbase"> 17 <canvas id="renderCanvas"></canvas> 18 <div id="fps" style="z-index: 301;"></div> 19 </div> 20 </body> 21 <script> 22 var VERSION=1.0,AUTHOR="lz_newland@163.com"; 23 var machine,canvas,engine,scene,gl,MyGame={}; 24 canvas = document.getElementById("renderCanvas"); 25 engine = new BABYLON.Engine(canvas, true); 26 gl=engine._gl;//可以结合使用原生OpenGL和Babylon.js; 27 scene = new BABYLON.Scene(engine); 28 var divFps = document.getElementById("fps"); 29 30 window.onload=beforewebGL; 31 function beforewebGL() 32 { 33 if(engine._webGLVersion==2.0)//输出ES版本 34 { 35 console.log("ES3.0"); 36 } 37 else{ 38 console.log("ES2.0"); 39 } 40 //MyGame=new Game(0,"first_pick","","http://127.0.0.1:8082/"); 41 /*0-startWebGL 42 * */ 43 webGLStart(); 44 } 45 //从下面开始分成简单测试和对象框架两种架构 46 //简单测试 47 //全局对象 48 var light0//全局光源 49 ,camera0//主相机 50 ; 51 //四种常用材质 52 var mat_frame = new BABYLON.StandardMaterial("mat_frame", scene); 53 mat_frame.wireframe = true; 54 var mat_red = new BABYLON.StandardMaterial("mat_red", scene); 55 mat_red.diffuseColor = new BABYLON.Color3(1, 0, 0); 56 mat_red.backFaceCulling=false; 57 var mat_green = new BABYLON.StandardMaterial("mat_green", scene); 58 mat_green.diffuseColor = new BABYLON.Color3(0, 1, 0); 59 mat_green.backFaceCulling=false; 60 var mat_blue = new BABYLON.StandardMaterial("mat_blue", scene); 61 mat_blue.diffuseColor = new BABYLON.Color3(0, 0, 1); 62 mat_blue.backFaceCulling=false; 63 var mesh_origin; 64 var advancedTexture=BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI("ui1");//全屏GUI 65 function webGLStart() 66 { 67 window.addEventListener("resize", function () {//自动调整视口尺寸 68 engine.resize(); 69 }); 70 camera0 =new BABYLON.FreeCamera("FreeCamera", new BABYLON.Vector3(0, 0, -80), scene); 71 camera0.attachControl(canvas, true); 72 camera0.speed=0.5;//相机移动速度是默认速度的一半 73 camera0.minZ=0.01;//相机位置距前视锥截面的距离,也就是说到相机距离小于0.01的图元都不会显示,这个值不能过小,否则Babylon.js内置的鼠标选取将失效 74 camera0.layerMask=2;//相机的遮罩层次,这个相机将只能显示遮罩层次同为2的网格,如果不设置这个属性,似乎可以显示所有遮罩层次的网格 75 scene.activeCameras.push(camera0);//将相机加入活跃相机列表,默认情况下Babylon.js只使用一个活跃相机,但是也可以强行使用多个 76 light0 = new BABYLON.HemisphericLight("Hemi0", new BABYLON.Vector3(0, 1, 0), scene);//半球光源 77 //三个参照物,MeshBuilder是新版Babylon.js中使用的网格构建对象,之前翻译入门教程时还没有这个对象,它的特点是把一大堆参数统一整理到一个option参数中 78 var mesh_base=new BABYLON.MeshBuilder.CreateSphere("mesh_base",{diameter:1},scene); 79 mesh_base.material=mat_green; 80 mesh_base.position.x=0; 81 mesh_base.layerMask=2; 82 var mesh_base1=new BABYLON.MeshBuilder.CreateSphere("mesh_base1",{diameter:1},scene); 83 mesh_base1.position.y=10; 84 mesh_base1.position.x=0; 85 mesh_base1.material=mat_green; 86 mesh_base1.layerMask=2; 87 var mesh_base2=new BABYLON.MeshBuilder.CreateSphere("mesh_base2",{diameter:1},scene); 88 mesh_base2.position.y=-10; 89 mesh_base2.position.x=0; 90 mesh_base2.material=mat_green; 91 mesh_base2.layerMask=2; 92 。 93 。 94 。 95 MyBeforeRender(); 96 } 97 function MyBeforeRender() 98 { 99 scene.registerBeforeRender(function() { 100 if(scene.isReady()) 101 { 102 。 103 。 104 。 105 。 106 。 107 。 108 } 109 }); 110 engine.runRenderLoop(function () { 111 engine.hideLoadingUI(); 112 if (divFps) { 113 // Fps 114 divFps.innerHTML = engine.getFps().toFixed() + " fps"; 115 } 116 scene.render(); 117 }); 118 119 } 120 </script> 121 </html>
这个3D场景包括了简单测试所需要的一些基本元素,这里使用的是包含全部组件的未压缩版Babylon.js库,在实际使用中考虑到节省带宽,可以使用Babylon.js官网提供的工具定制精简版或压缩版的Babylon.js库
2、建立一个基础网格
计划通过对一个基础网格进行顶点变换来产生各种各样的简单模型,在Babylon.js中“条带”是一种非常适合顶点变换的网格类型,Babylon.js官方教程中有关于条带构造和变形的文档,可以在这里下载中文翻译http://down.51cto.com/data/2449757。
用来建立基础网格的代码如下:
1 //下面这些函数都通过控制台调用 2 //在ZoY平面里建立一个圆环路径 3 //radius:半径,sumpoint:使用几个点 4 function MakeRing(radius,sumpoint) 5 { 6 var arr_point=[]; 7 var radp=Math.PI*2/sumpoint; 8 for(var i=0.0;i<sumpoint;i++) 9 { 10 var x=0; 11 var rad=radp*i; 12 //var y=sswr(radius*Math.sin(rad),null,5);//在这里需要降低一些精确度?否则Babylon.js在计算顶点数据时可能和这里不一致? 13 //var z=sswr(radius*Math.cos(rad),null,5); 14 var y=radius*Math.sin(rad); 15 var z=radius*Math.cos(rad); 16 arr_point.push(new BABYLON.Vector3(x,y,z)); 17 } 18 arr_point.push(arr_point[0].clone());//首尾相连,不能这样相连,否则变形时会多出一个顶点!!,看来这个多出的顶点无法去掉,只能在选取时额外处理它 19 return arr_point; 20 } 21 var arr_path=[];//核心数据 22 //arr_point:单个路径的点数组,xstartl:第一个扁平路径放在最左侧,spacing:路径的间距,sumpath:一共使用几条路径, 23 function MakeRibbon(arr_point,xstartl,spacing,sumpath,name) 24 {//将一条圆环路径扩展成相互平行的多个圆环路径,然后使用这些路径生成条带 25 arr_path=[]; 26 for(var i=0;i<sumpath;i++)//对于每一条路径 27 { 28 var x=xstartl+spacing*i; 29 //var arr=arr_point.concat(null);//为什么拷贝失灵了? 30 //var [ ...arr ] = arr_point;//ES6的新扩展运算符?-》也不好使,因为数组里的元素是指针?!! 31 var len=arr_point.length; 32 var arr=[]; 33 for(var j=0;j<len;j++) 34 { 35 var obj=arr_point[j].clone(); 36 obj.x=x; 37 // 38 arr.push(obj); 39 } 40 arr_path.push(arr); 41 arr=null; 42 } 43 mesh_origin.dispose(); 44 mesh_origin=BABYLON.MeshBuilder.CreateRibbon(name,{pathArray:arr_path,updatable:true,closePath:false,closeArray:false}); 45 //mesh_origin=mesh;//用一个全局变量保存最终会被导出的mesh 46 mesh_origin.sideOrientation=BABYLON.Mesh.DOUBLESIDE;//显示网格的前后两面 47 mesh_origin.material=mat_frame; 48 mesh_origin.layerMask=2; 49 }
编程中遇到的几个问题:
a、路径中设置的坐标值在实际显示时可能发生微小的变化,比如5可能变成4.999999999999999999,但是似乎没什么影响。
b、虽然圆环路径分成12段应该由12个顶点组成,但是如果只有12个顶点,那么在调用CreateRibbon方法时,如果把closePath参数设为true则Babylon.js会自动添加一个不受控制的顶点来闭合圆环路径,如果设为false,则路径无法闭合。为此在第18行再添加一个与起始点
重合的顶点使圆环路径闭合。
c、原计划直接使用数组的concat方法复制arr_point数组产生更多的圆环路径,但concat方法似乎只能对一维数组使用(栈?),而arr_point的每个元素都是一个BABYLON.Vector3对象,所以只好用for循环一个点一个点的生成路径
d、MakeRing是一个生成圆环路径的方法,使用者可以根据需要编写各种其他类型的路径生成方法。
3、调整网格的属性:
可以对网格的属性进行一些调整,但是这些调整只在这个编辑器里生效,并不会被导出。
比如对网格材质的调整:
1 function ChangeMaterial(mesh,mat) 2 { 3 mesh.material=mat; 4 }
4、显示网格顶点法线方向
代码如下:
1 var lines_normal={}; 2 /*ShowNormals(mesh_origin) 3 DeleteMeshes([lines_normal]); 4 * */ 5 //显示所有的顶点法线 6 function ShowNormals(mesh) 7 { 8 //DeleteMeshes(arr_line_normal); 9 if(lines_normal.dispose) 10 { 11 lines_normal.dispose(); 12 } 13 //遍历顶点 14 var vb=mesh.geometry._vertexBuffers; 15 var data_pos=vb.position._buffer._data;//顶点数据 16 var data_mormal=vb.normal._buffer._data;//法线数据 17 var len=data_pos.length; 18 var lines=[]; 19 for(var i=0;i<len;i+=3) 20 {//CreateLineSystem使用一个网格包含很多分立的线段(路径),CreateLines则是一条首尾相连的路径 21 // 22 var vec=new BABYLON.Vector3(data_pos[i],data_pos[i+1],data_pos[i+2]); 23 var vec2=vec.clone().add(new BABYLON.Vector3(data_mormal[i],data_mormal[i+1],data_mormal[i+2]).normalize().scale(1)); 24 lines.push([vec,vec2]); 25 } 26 lines_normal=new BABYLON.MeshBuilder.CreateLineSystem("lines_normal",{lines:lines,updatable:false},scene); 27 lines_normal.color=new BABYLON.Color3(1, 0, 0); 28 }
Babylon.js内置的CreateLineSystem方法可以方便的建立一个由很多条线段组成的网格。
5、删除网格
代码如下:
1 function DeleteMeshes(arr)//假设一个数组里的都是mesh,彻底清空它 2 { 3 var len=arr.length; 4 for(var i=0;i<len;i++) 5 { 6 if(arr[i].dispose) 7 arr[i].dispose(); 8 } 9 arr=[]; 10 }
值得注意的是,直接在控制台里执行mesh.dispose()并不生效。
这里也体现出CreateLineSystem的方便之处——删除法线时只需要dispose一个对象。
6、鼠标移入时显示三角形信息
鼠标动作监听代码:
1 mesh_origin=new BABYLON.Mesh("mesh_origin",scene);//建立一个空的初始网格对象 2 mesh_surface=new BABYLON.Mesh("mesh_surface",scene); 3 mesh_surface0=new BABYLON.Mesh("mesh_surface0",scene); 4 if(true) 5 {//初始化三个GUI标签 6 label_index1=new BABYLON.GUI.TextBlock(); 7 label_index1.text = "label_index1"; 8 label_index1.color="white"; 9 label_index1.isVisible=false;//初始化时标签不可见 10 //label_index1.linkWithMesh(mesh_surface0);//TextBlock并不是顶层元素 11 advancedTexture.addControl(label_index1); 12 label_index2=new BABYLON.GUI.TextBlock(); 13 label_index2.text = "label_index2"; 14 label_index2.color="white"; 15 label_index2.isVisible=false; 16 //label_index2.linkWithMesh(mesh_surface0); 17 advancedTexture.addControl(label_index2); 18 label_index3=new BABYLON.GUI.TextBlock(); 19 label_index3.text = "label_index3"; 20 label_index3.color="white"; 21 label_index3.isVisible=false; 22 //label_index3.linkWithMesh(mesh_surface0); 23 advancedTexture.addControl(label_index3); 24 } 25 //监听鼠标移动 26 canvas.addEventListener("mousemove", function(evt){ 27 var pickInfo = scene.pick(scene.pointerX, scene.pointerY,null,null,camera0);//如果同时有多个激活的相机,则要明确的指出使用哪个相机 28 //cancelPropagation(evt); 29 //cancelEvent(evt); 30 label_index1.isVisible=false; 31 label_index2.isVisible=false; 32 label_index3.isVisible=false; 33 if(mesh_surface.dispose) 34 { 35 mesh_surface.dispose(); 36 } 37 if(mesh_surface0.dispose) 38 { 39 mesh_surface0.dispose(); 40 } 41 if(pickInfo.hit&&(pickInfo.pickedMesh.name=="mesh_origin"||pickInfo.pickedMesh.name=="mesh_ribbon"))//找到鼠标所在的三角形并重绘之 42 { 43 //在鼠标所指的地方绘制一个三角形,表示被选中的三角形 44 var faceId=pickInfo.faceId; 45 var pickedMesh=pickInfo.pickedMesh; 46 var indices=[pickedMesh.geometry._indices[faceId*3] 47 ,pickedMesh.geometry._indices[faceId*3+1],pickedMesh.geometry._indices[faceId*3+2]]; 48 var vb=pickedMesh.geometry._vertexBuffers; 49 var position=vb.position._buffer._data; 50 var normal=vb.normal._buffer._data; 51 var uv=vb.uv._buffer._data; 52 var len=arr_path[0].length; 53 var p1={index:indices[0],position:[position[indices[0]*3],position[indices[0]*3+1],position[indices[0]*3+2]] 54 ,normal:[normal[indices[0]*3],normal[indices[0]*3+1],normal[indices[0]*3+2]] 55 ,uv:[uv[indices[0]*2],uv[indices[0]*2+1]],ppi:("1:"+(Math.round(indices[0]/len)+0))+"-"+(indices[0]%len)};//pathpointindex 56 var p2={index:indices[1],position:[position[indices[1]*3],position[indices[1]*3+1],position[indices[1]*3+2]] 57 ,normal:[normal[indices[1]*3],normal[indices[1]*3+1],normal[indices[1]*3+2]] 58 ,uv:[uv[indices[1]*2],uv[indices[1]*2+1]],ppi:("2:"+(Math.round(indices[1]/len)+0))+"-"+(indices[1]%len)}; 59 var p3={index:indices[2],position:[position[indices[2]*3],position[indices[2]*3+1],position[indices[2]*3+2]] 60 ,normal:[normal[indices[2]*3],normal[indices[2]*3+1],normal[indices[2]*3+2]] 61 ,uv:[uv[indices[2]*2],uv[indices[2]*2+1]],ppi:("3:"+(Math.round(indices[2]/len)+0))+"-"+(indices[2]%len)}; 62 var po=[(p1.position[0]+p2.position[0]+p3.position[0])/3,(p1.position[1]+p2.position[1]+p3.position[1])/3,(p1.position[2]+p2.position[2]+p3.position[2])/3]; 63 var numm=2; 64 mesh_surface0=newland.make_tryangle(p1,p2,p3,"mesh_surface1",scene);//使用三个点绘制一个三角形 65 mesh_surface0.material=mat_green; 66 mesh_surface0.sideOrientation==BABYLON.Mesh.DOUBLESIDE; 67 mesh_surface0.layerMask=2; 68 label_index1.isVisible=true; 69 //label_index1.layerMask=2;//这个属性对于gui被管对象并不生效? 70 label_index1.text=p1.ppi;//ppi表示这个顶点是三角形的第几个顶点,以及这个顶点位于第几条路径的第几个位置 71 var pos1=new BABYLON.Vector3(p1.position[0],p1.position[1],p1.position[2]); 72 label_index1.moveToVector3(pos1,scene); 73 label_index1.itspos=pos1; 74 label_index2.isVisible=true; 75 label_index2.text=p2.ppi; 76 var pos2=new BABYLON.Vector3(p2.position[0],p2.position[1],p2.position[2]); 77 label_index2.moveToVector3(pos2,scene); 78 label_index2.itspos=pos2; 79 label_index3.isVisible=true; 80 label_index3.text=p3.ppi; 81 var pos3=new BABYLON.Vector3(p3.position[0],p3.position[1],p3.position[2]); 82 label_index3.moveToVector3(pos3,scene); 83 label_index3.itspos=pos3; 84 //将三个点移动到场景的中心 85 pt=[(p1.position[0]-po[0])*numm,(p1.position[1]-po[1])*numm,(p1.position[2]-po[2])*numm]; 86 p1.position=pt; 87 pt=[(p2.position[0]-po[0])*numm,(p2.position[1]-po[1])*numm,(p2.position[2]-po[2])*numm]; 88 p2.position=pt; 89 pt=[(p3.position[0]-po[0])*numm,(p3.position[1]-po[1])*numm,(p3.position[2]-po[2])*numm]; 90 p3.position=pt; 91 //在场景的中心再绘制一个大一倍的三角形 92 mesh_surface=newland.make_tryangle(p1,p2,p3,"mesh_surface",scene); 93 mesh_surface.material=mat_green; 94 mesh_surface.sideOrientation==BABYLON.Mesh.DOUBLESIDE; 95 mesh_surface.layerMask=1;//遮罩层次是1 96 97 } 98 99 },false);
需要注意的有以下几点:
a、鼠标指向时显示的是这个点在arr_path中的索引,而不是在“顶点数据对象”中的索引!
b、最初的计划是像魔兽争霸3一样在窗口的左下角建立一个遮罩层次为1的小视口,用来特写被选中的三角形(mesh_surface),但是后来发现Babylon.js的GUI不能设置遮罩层次,也不能像鼠标选取一样设置相对于哪一个相机,所以取消了左下角的小视口。
如果需要添加视口,代码如下:
1 var mm = new BABYLON.FreeCamera("minimap", new BABYLON.Vector3(0,0,-20), scene); 2 mm.layerMask = 1; 3 var xstart = 0.0, 4 ystart = 0.1;//意为占屏幕宽高的比例 5 var width = 0.2, 6 height = 0.2; 7 //视口边界从左下角开始 8 mm.viewport = new BABYLON.Viewport( 9 xstart, 10 ystart, 11 width, 12 height 13 ); 14 scene.activeCameras.push(mm);
c、需要注意的是GUI是一次性绘制在视口上的,默认情况下即使相关的网格位置发生变化,GUI仍然会保持在视口上最初的位置。linkWithMesh方法可以将GUI和网格绑定起来,但是似乎只能对部分“底层的”GUI类型生效,为了使得标签跟随顶点移动,需要手动在每一帧渲染之前更新标签的位置:
1 scene.registerBeforeRender(function() { 2 if(scene.isReady()) 3 { 4 if(label_index1.isVisible==true)//不断刷新gui的位置 5 {//在具有多个激活相机时选择了错误的参考相机?-》会强制选择最新建立的相机 6 label_index1.moveToVector3(label_index1.itspos,scene); 7 label_index2.moveToVector3(label_index2.itspos,scene); 8 label_index3.moveToVector3(label_index3.itspos,scene); 9 } 10 } 11 });
7、选取顶点:
3DsMax和Blender(Babylon.js库在3D模型部分参考了Blender的许多设计)都使用鼠标在网格中选取需要修改的区域,我不是很习惯这种方式,所以考虑用JS函数代替鼠标操作:
1 var lines_inpicked={};//线段系统对象,表示所有被选中点影响的线段 2 var arr_ij=[];//记录被选中的点在arr_path中的索引的数组 3 /*DeleteMeshes([lines_inpicked]); 4 * PickPoints([[0,0],[0,12],[1,1]],mesh_origin) 5 * */ 6 //选定一些顶点 7 function PickPoints(arr,mesh) 8 { 9 if(arr_path.length==0) 10 { 11 alert("尚未生成路径数组!"); 12 return; 13 } 14 if(lines_inpicked.dispose) 15 { 16 lines_inpicked.dispose(); 17 } 18 //arr_ij=[]; 19 //为了后面考虑,需要先对arr整体遍历一遍,把首尾接口处关联起来 20 arr_ij=arr;//把路径和点的位置记录下来 21 var vb=mesh.geometry._vertexBuffers; 22 var data_pos=vb.position._buffer._data; 23 var len_pos=data_pos.length; 24 var data_index=mesh.geometry._indices; 25 var len_index=data_index.length; 26 var len=arr.length; 27 var lines=[]; 28 for(var i=0;i<len;i++)//对于每一个需要显示的被选择点 29 { 30 var int0=arr[i][0]; 31 var int1=arr[i][1]; 32 var vec=arr_path[int0][int1];//获取到路径数组中的一个Vector3对象 33 //假设路径数组和顶点数据是一一对应的?同时假设每一条路径的长度都和第一条相同 34 var arr_index=[int0*arr_path[0].length+int1];//所有位于所选位置的顶点在顶点数据对象中的索引 35 //下面遍历网格的绘制索引,找出这个被选中的顶点每一次的使用情况 36 for(var j=0;j<len_index;j+=3)//根据顶点在三角形中的位置分三种情况讨论 37 {//绘制出和这个顶点相关的所有线段,这样绘制会有很严重的线段重合!观察是否对性能有很大的影响 38 var len2=arr_index.length; 39 for(var k=0;k<len2;k++) 40 { 41 if(arr_index[k]==data_index[j])//三角形的第一个顶点 42 {//把这个顶点和三角形中的另两个顶点用黄线连起来 43 var num2=data_index[j+1]*3; 44 var num3=data_index[j+2]*3; 45 var vec2=new BABYLON.Vector3(data_pos[num2],data_pos[num2+1],data_pos[num2+2]); 46 var vec3=new BABYLON.Vector3(data_pos[num3],data_pos[num3+1],data_pos[num3+2]); 47 lines.push([vec,vec2]); 48 lines.push([vec,vec3]); 49 } 50 else if(arr_index[k]==data_index[j+1])//三角形的第一个顶点 51 { 52 var num2=data_index[j]*3; 53 var num3=data_index[j+2]*3; 54 var vec2=new BABYLON.Vector3(data_pos[num2],data_pos[num2+1],data_pos[num2+2]); 55 var vec3=new BABYLON.Vector3(data_pos[num3],data_pos[num3+1],data_pos[num3+2]); 56 lines.push([vec,vec2]); 57 lines.push([vec,vec3]); 58 } 59 else if(arr_index[k]==data_index[j+2])//三角形的第一个顶点 60 { 61 var num2=data_index[j]*3; 62 var num3=data_index[j+1]*3; 63 var vec2=new BABYLON.Vector3(data_pos[num2],data_pos[num2+1],data_pos[num2+2]); 64 var vec3=new BABYLON.Vector3(data_pos[num3],data_pos[num3+1],data_pos[num3+2]); 65 lines.push([vec,vec2]); 66 lines.push([vec,vec3]); 67 } 68 } 69 } 70 } 71 lines_inpicked=new BABYLON.MeshBuilder.CreateLineSystem("lines_normal",{lines:lines,updatable:false},scene); 72 lines_inpicked.color=new BABYLON.Color3(1, 1, 0); 73 } 74 //需要一些选择选定顶点的方法
有以下几处需要注意:
a、因为MakeRing生成的路径里包括两个重合的点(首尾),为了保证变形后的网格仍然连续,在选择时这两个重合的点必须同时被选中,经过考虑,规定这个确保首尾重合点同时选中的操作在生成arr数组参数时进行。
b、因为基础网格mesh_origin的每一个顶点恰好位置不同,所以一开始我以为“与被选中的点的位置相同的点”应该具有与被选中点相同的变形效果,但是事实上经过网格变形后,完全可能出现两个不相干的顶点位于同一位置的情况,这时应用相同的变形效果会出现错误,比如人可以把手放在腿上,这时手和腿的接触点具有相同的位置,但如果因此对这两个点应用相同的变形效果,就很诡异了。同时如前文所说,有时arr_path中存储的位置和顶点数据中存储的位置可能出现微小的偏差。
最后采取的方法是将arr_path中的数组转化为顶点数据对象中的数组。
c、这里直接选取了几个点进行变形,还需要编写一些按照某种规则批量选取点的方法,也希望大家能帮我想一想怎样选取顶点比较方便。
8、网格变形:
代码如下:
1 /* 2 * TransVertex(mesh_origin,arr_ij,BABYLON.Matrix.RotationX(Math.PI/2)) 3 *TransVertex(mesh_origin,arr_ij,BABYLON.Matrix.Translation(0.5,0,0)) 4 * */ 5 //移动选定的顶点,同时更新网格、选取线、法线 6 function TransVertex(mesh,arr,matrix) 7 { 8 var len=arr.length; 9 for(var i=0;i<len;i++)//移动路径数组里的每个顶点 10 { 11 //var vec=arr_path[arr[i][0]][arr[i][1]]; 12 arr_path[arr[i][0]][arr[i][1]]=BABYLON.Vector3.TransformCoordinates(arr_path[arr[i][0]][arr[i][1]],matrix); 13 } 14 //更新网格,发现设置了closePath:true之后在顶点数据里还是会自动补上一个接续点,这还不如一开始自己添加!起码自己添加可以保证路径数组和顶点数据的一致性!!!! 15 mesh=BABYLON.MeshBuilder.CreateRibbon(mesh.name,{pathArray:arr_path,updatable:true,instance:mesh,closePath:false,closeArray:false}); 16 //上面的更新重绘是默认重算法线方向的,这与直接修改顶点数据不同 17 //清空法线 18 DeleteMeshes([lines_normal]); 19 //更新选取顶点表示 20 PickPoints(arr,mesh); 21 } 22 //需要一些生成变化矩阵的方法
其实这里移动的顶点和前面选择的点没有关系,不选择也可以直接变形,只不过不会有黄线显示罢了。这里也同样需要一些生成更复杂的变换矩阵的方法。
9、导出:
导出方法的调用如下:
1 /*ExportMesh("1",mat_blue)*/ 2 function ExportMesh(filename,mat) 3 { 4 try{ 5 newland.ExportBabylonMesh([mesh_origin],filename,mat); 6 } 7 catch(e) 8 { 9 console.error(e) 10 } 11 }
函数的第一个参数是导出后的文件名(不包括扩展名),第二个参数是网格使用的材质对象(暂时只支持简单的颜色材质)。
其中ExportBabylonMesh是我编写的newland库中的一个方法,这个方法在前面关与3D模型的文章中也有提到过(https://www.cnblogs.com/ljzc002/p/6884252.html),这个方法整体上没有太大的改变。但是在旧版的Babylon.js中,顶点数据对象中的data属性是数组类型(?),而在新版中data属性则是typedArray类型,虽然这两种数据类型看起来很像,但在使用JSON.stringify(arr)转化为JSON字符串时,对于同样的数据[1,2,3],前者会转化为"[1,2,3]"后者则是"{"0":1,"1":2,"2":3}"!
在导入模型文件时后者会报错:“attempt to access out of range vertices in attribute 0”,这意味着在导入模型时顶点数据数组没有正确的载入,我的解决方案是导出前将data属性强制转化为数组类型:
1 //将TypedArray转化为普通array 2 newland.BuffertoArray2=function(arr) 3 { 4 var arr2=[]; 5 var len=arr.length; 6 for(var i=0;i<len;i++) 7 { 8 arr2.push(arr[i]); 9 } 10 return arr2; 11 }
对于旧版的Babylon.js应该可以不加修改的使用这个方法,因为typedArray也具有数组的大部分功能。
10、导入:
在另一个页面里实现模型导入功能,这个页面同样在“基础场景”的基础上编写,其中导入方法如下:
1 function ImportMesh(objname,filepath,filename) 2 { 3 4 BABYLON.SceneLoader.ImportMesh(objname, filepath, filename, scene 5 , function (newMeshes, particleSystems, skeletons) 6 {//载入完成的回调函数 7 if(mesh_origin&&mesh_origin.dispose) 8 { 9 mesh_origin.dispose(); 10 } 11 mesh_origin=newMeshes[0]; 12 //mesh_origin.material=mat_frame; 13 //mesh_origin.layerMask=2; 14 } 15 ); 16 }
20180807修改
使用上述方法建立了一个“农夫山泉4L装塑料桶”的网格,可以通过http://ljzc002.github.io/SnowMiku/HTML/NongFuSpring.html访问。在新增的Make.js文件中编写了几个生成所需顶点路径的方法,包括:生成正多边形路径、生成指定轴对称路径、生成圆弧表面;以及调整已有顶点位置的方法,包括:产生沟壑、按正弦曲线规律变换顶点路径。
最终产生的网格如下图:
实践证明,对于具有一定排布规律的多顶点网格来说,使用编程方法产生网格比较方便。
20180808修改
通过查看文档,发现虽然高级动态纹理在建立时无法指定在哪个相机中进行显示,但他的属性支持多相机设置,需要设置的属性是advancedTexture.layer.layerMask!