前言:3D模型編輯的核心是對頂點位置和紋理顏色的編輯,這個研究的目的在於尋找一種通過編程方式直接對模型進行編輯的方法,這種編輯方法和時下流行的通過滑鼠點選、拖拽進行編輯的方法之間的關係,和前端編程中“程式員編寫靜態網頁”與“美工進行網頁切圖”之間的關係很相似。 一、工具用法: 1、訪問 https: ...
前言: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="[email protected]"; 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