2019.7.24 09:49更新
之前計算繪制頂點數的時候,代碼如下
// 封邊頂點 let edgeVertex = sectorAngle < 360 ? 6 : 0 let edgeIndex = sectorAngle < 360 ? 4 : 0 // 需要畫的頂點 let drawVertex = slices * (sectorAngle / 360).toFixed(1)
使用toFixed(1),想將數量位數控制一下,我在后文還提到說不能出現小數。其實應該是要在這里將結果轉為Number類型,保留整數就行。如果這里的頂點數不夠,會導致繪制出來的度數不夠。
// 需要畫的頂點 let drawVertex = Number(slices * (sectorAngle / 360).toFixed(0))
--------------------------------------------------
2019.7.17 18:12更新
之前畫封邊的時候,共用了內徑的上下頂點,后來在加入貼圖的時候,UV坐標導入之后,會使得貼圖在兩個封邊上成鏡像效果,所以改成不共用頂點。
// 內徑上頂點 vertices[vc++] = 0 vertices[vc++] = halfHeight vertices[vc++] = 0 vertices[vc++] = 0 vertices[vc++] = 1 vertices[vc++] = 0 vertices[vc++] = 1 vertices[vc++] = 0 // 內徑下頂點 vertices[vc++] = 0 vertices[vc++] = -halfHeight vertices[vc++] = 0 vertices[vc++] = 0 vertices[vc++] = 1 vertices[vc++] = 0 vertices[vc++] = 1 vertices[vc++] = 1 // 外徑上頂點 起點邊 vertices[vc++] = radius vertices[vc++] = halfHeight vertices[vc++] = 0 vertices[vc++] = 0 vertices[vc++] = 1 vertices[vc++] = 0 vertices[vc++] = 0 vertices[vc++] = 0 // 外徑下頂點 起點邊 vertices[vc++] = radius vertices[vc++] = -halfHeight vertices[vc++] = 0 vertices[vc++] = 0 vertices[vc++] = 1 // 這個會影響光照 vertices[vc++] = 0 vertices[vc++] = 0 vertices[vc++] = 1 // 畫兩個三角形(內上、內下、外上, 外下、外上、內下) 起點封邊 indices[ic++] = verticeCount + 0 indices[ic++] = verticeCount + 1 indices[ic++] = verticeCount + 2 indices[ic++] = verticeCount + 3 indices[ic++] = verticeCount + 2 indices[ic++] = verticeCount + 1 // 內徑上頂點 vertices[vc++] = 0 vertices[vc++] = halfHeight vertices[vc++] = 0 vertices[vc++] = 0 vertices[vc++] = 1 vertices[vc++] = 0 vertices[vc++] = 0 vertices[vc++] = 0 // 內徑下頂點 vertices[vc++] = 0 vertices[vc++] = -halfHeight vertices[vc++] = 0 vertices[vc++] = 0 vertices[vc++] = 1 vertices[vc++] = 0 vertices[vc++] = 0 vertices[vc++] = 1 // 外徑上頂點 結束邊 vertices[vc++] = posX vertices[vc++] = halfHeight vertices[vc++] = posZ vertices[vc++] = 0 vertices[vc++] = 1 vertices[vc++] = 0 vertices[vc++] = 1 vertices[vc++] = 0 // 外徑下頂點 結束邊 vertices[vc++] = posX vertices[vc++] = -halfHeight vertices[vc++] = posZ vertices[vc++] = 0 vertices[vc++] = 1 // 這個會影響光照 vertices[vc++] = 0 vertices[vc++] = 1 vertices[vc++] = 1 // 畫兩個三角形(內上、內下、外上, 外下、外上、內下) 結束封邊 indices[ic++] = verticeCount + 4 indices[ic++] = verticeCount + 6 indices[ic++] = verticeCount + 5 indices[ic++] = verticeCount + 7 indices[ic++] = verticeCount + 5 indices[ic++] = verticeCount + 6 verticeCount += edgeVertex
上述代碼標紅的地方,就是新增的加入UV坐標,這樣封邊就能正常的顯示貼圖了。另外,Laya中UV坐標系好像是以右上角為原點的,表現在UV的坐標原點,需要是右上角的頂點,比如上述代碼中外徑上頂點的UV坐標就是(0,0).
--------------------------------------------------
2019.7.17 14:34更新
// 索引數量 let indexCount = 3 * drawVertex + edgeIndex + 6 * drawVertex + 3 * drawVertex
上述關於計算索引總數,需要 edgeIndex * 3,具體為什么要 * 3這個還需要再了解,但是不*3,會導致最后畫底部圓面/扇面,缺少幾個三角形。
--------------------------------------------------
2019.7.16 21:30更新
再理解了一下這篇文章中提到的,三個點在一條直線上,畫出來的三角形看不到,需要跳到下一行的第一個點,關鍵代碼:
triangles[0] = 0; triangles[1] = 1; triangles[2] = xSize + 1;
之前一直沒理解triangles[2] = xSize + 1,為啥是這么寫。再想一下,這里的頂點數據是在一個一維數組中,然后一行xSize個頂點,第一行的第一個數據下標是0,那么下一行第一個數據的下標就是xSize+1了。。。
--------------------------------------------------
最近想用Laya模仿微信小游戲《歡樂球球》,做為學習3D引擎的一個項目。做這個項目之前,先用2D做了另外一款簡單的小游戲,簡單熟悉了一下Laya的IDE,及引擎的一些基本知識。之前的3D知識基本為零,只照着網上的教程試着用過幾天unity3d,但是都是皮毛的東西。
《歡樂球球》這款游戲,核心玩法就是旋轉圓柱,讓小球通過圓柱上的扇狀環,且不掉落在特定的標記為紅色的環塊上,通過一層環得分,最終根據得分進行排名。
最開始接觸這款游戲的時候,就大概知道核心點在於扇狀環的形成。之前沒有接觸3D引擎的時候,以為引擎有現成的api來生成這樣的3D物體,覺得做這款小游戲應該不復雜,后來發現沒這么簡單。Laya引擎里面提供的api能畫Box,膠囊體,圓錐,圓柱,平面,四邊形,球體,沒有現成的畫扇狀體的api,而且在論壇搜索,谷歌/百度搜索除了在論壇找到一個說要修改底層,並且提到了一個關鍵詞Mesh編程,就沒有再找到有網友提供相關的教程,或許是我檢索的關鍵詞不對吧。
但是檢索“自定義扇形”這樣的關鍵詞,就找到了關於Unity3D自定義扇形相關的教程,而且還是針對《歡樂球球》這款游戲,而且還就是針對生成里面的扇形環3D物體的教程。大致看了一下,也是涉及到上面提到的一個關鍵詞Mesh編程,並且分享了一篇專門講Mesh編程的文章,里面有幾個關鍵詞:頂點,三角形,索引,vertices,triangles,uv。一開始覺得挺難的,unity3d的接口laya能用?相關的術語、api等相通?但是沒辦法,找了很久就是沒有找到Laya相關的,只能硬着頭皮上!
先是再認真看一下Laya官方創建簡單網格的api,Laya.PrimitiveMesh.createXXXX,注意到這里有個關鍵詞Mesh,感覺有戲,繼續追蹤進去看(這里看的是laya.d3.js,這個是ide中集成的基礎類庫,需要手動導入),隨便拿了一個api,createBox,看看源碼:
var vertexCount=24; var indexCount=36; var vertexDeclaration=VertexMesh.getVertexDeclaration("POSITION,NORMAL,UV");
上面簡單的摘了一段代碼下來,主要是看到了幾個關鍵詞:vertexCount,indexCount,UV,感覺跟unity3d那篇講Mesh編程里面涉及到的關鍵詞相似,再一次感覺有戲!聯想到在laya的論壇有看到說涉及到底層編程,自己寫mesh代碼之類的,然后看了官方提供的api,覺得應該是可以自己參考着寫的,所以花了點時間來試試。
首先扇形狀3D物體,跟圓柱體類似,是圓柱體的一部分,所以我直接拿官方生成圓柱體的代碼改一下。首先看一下官方的代碼:
(radius === void 0) && (radius = 0.5); (height === void 0) && (height = 2); (slices === void 0) && (slices = 32); var vertexCount = (slices + 1 + 1) + (slices + 1) * 2 + (slices + 1 + 1); var indexCount = 3 * slices + 6 * slices + 3 * slices; var vertexDeclaration = VertexMesh.getVertexDeclaration("POSITION,NORMAL,UV"); var vertexFloatStride = vertexDeclaration.vertexStride / 4; var vertices = new Float32Array(vertexCount * vertexFloatStride); var indices = new Uint16Array(indexCount); var sliceAngle = (Math.PI * 2.0) / slices; var halfHeight = height / 2; var curAngle = 0; var verticeCount = 0; var posX = 0; var posY = 0; var posZ = 0; var vc = 0; var ic = 0; // 部分一 for (var tv = 0; tv <= slices; tv++) { if (tv === 0) { vertices[vc++] = 0; vertices[vc++] = halfHeight; vertices[vc++] = 0; vertices[vc++] = 0; vertices[vc++] = 1; vertices[vc++] = 0; vertices[vc++] = 0.5; vertices[vc++] = 0.5; } curAngle = tv * sliceAngle; posX = Math.cos(curAngle) * radius; posY = halfHeight; posZ = Math.sin(curAngle) * radius; vertices[vc++] = posX; vertices[vc++] = posY; vertices[vc++] = posZ; vertices[vc++] = 0; vertices[vc++] = 1; vertices[vc++] = 0; vertices[vc++] = 0.5 + Math.cos(curAngle) * 0.5; vertices[vc++] = 0.5 + Math.sin(curAngle) * 0.5; } for (var ti = 0; ti < slices; ti++) { indices[ic++] = 0; indices[ic++] = ti + 1; indices[ic++] = ti + 2; } verticeCount += slices + 1 + 1; // 部分二 for (var rv = 0; rv <= slices; rv++) { curAngle = rv * sliceAngle; posX = Math.cos(curAngle + Math.PI) * radius; posY = halfHeight; posZ = Math.sin(curAngle + Math.PI) * radius; vertices[vc++] = posX; vertices[vc + (slices + 1) * 8 - 1] = posX; vertices[vc++] = posY; vertices[vc + (slices + 1) * 8 - 1] = -posY; vertices[vc++] = posZ; vertices[vc + (slices + 1) * 8 - 1] = posZ; vertices[vc++] = posX; vertices[vc + (slices + 1) * 8 - 1] = posX; vertices[vc++] = 0; vertices[vc + (slices + 1) * 8 - 1] = 0; vertices[vc++] = posZ; vertices[vc + (slices + 1) * 8 - 1] = posZ; vertices[vc++] = 1 - rv * 1 / slices; vertices[vc + (slices + 1) * 8 - 1] = 1 - rv * 1 / slices; vertices[vc++] = 0; vertices[vc + (slices + 1) * 8 - 1] = 1; } vc += (slices + 1) * 8; for (var ri = 0; ri < slices; ri++) { indices[ic++] = ri + verticeCount + (slices + 1); indices[ic++] = ri + verticeCount + 1; indices[ic++] = ri + verticeCount; indices[ic++] = ri + verticeCount + (slices + 1); indices[ic++] = ri + verticeCount + (slices + 1) + 1; indices[ic++] = ri + verticeCount + 1; } verticeCount += 2 * (slices + 1); // 部分三 for (var bv = 0; bv <= slices; bv++) { if (bv === 0) { vertices[vc++] = 0; vertices[vc++] = -halfHeight; vertices[vc++] = 0; vertices[vc++] = 0; vertices[vc++] = -1; vertices[vc++] = 0; vertices[vc++] = 0.5; vertices[vc++] = 0.5; } curAngle = bv * sliceAngle; posX = Math.cos(curAngle + Math.PI) * radius; posY = -halfHeight; posZ = Math.sin(curAngle + Math.PI) * radius; vertices[vc++] = posX; vertices[vc++] = posY; vertices[vc++] = posZ; vertices[vc++] = 0; vertices[vc++] = -1; vertices[vc++] = 0; vertices[vc++] = 0.5 + Math.cos(curAngle) * 0.5; vertices[vc++] = 0.5 + Math.sin(curAngle) * 0.5; } for (var bi = 0; bi < slices; bi++) { indices[ic++] = 0 + verticeCount; indices[ic++] = bi + 2 + verticeCount; indices[ic++] = bi + 1 + verticeCount; } verticeCount += slices + 1 + 1; return PrimitiveMesh._createMesh(vertexDeclaration, vertices, indices);
這段代碼有點長,我剛看的時候也是一臉懵逼,因為這段代碼沒有注釋,對於沒有3D知識的我來說內容全靠猜。我最初是一部分一部分注釋,看看注釋之后的效果是什么,才慢慢理解了這段代碼。我將代碼主要分為三個部分,兩個for循環為一部分。根據變量的命名,我最終理解的是,每部分兩個for循環,第一個for循環是生成頂點數據,第二個for循環是生成頂點序號。三個部分,第一個部分畫出圓柱的上部圓面,第二個部分畫出圓柱的柱包圍,第三個部分畫出圓柱的下部圓面,所以我要扣出扇狀物體,就是要從這三個部分中去扣。
首先,我試着將每個for循環的限制條件值slices改到原來的0.7,試着看看效果:
嗯。。。有點樣子了,不過旋轉看下發現有缺陷,缺口部分沒有封起來,空空的。然后開始了折騰的過程了。
回看上面提到的Mesh編程教程里面的內容,3D世界中的物體都是通過無數個三角形組成的,所以這兩個缺口應該是需要畫三角形來填好。然后再想一下,缺口其實就是兩個矩形,不就是畫4個三角形就填好了么,挺簡單的嘛。。。但是怎么畫呢?之前可從來沒玩過這個。
依葫蘆畫瓢,我先畫一個三角形看看:
// 內徑上頂點 vertices[vc++] = 0; vertices[vc++] = halfHeight; vertices[vc++] = 0; vertices[vc++] = 0; vertices[vc++] = 1; vertices[vc++] = 0; vertices[vc++] = 0.5; vertices[vc++] = 0.5; // 內徑下頂點 vertices[vc++] = 0; vertices[vc++] = -halfHeight; vertices[vc++] = 0; vertices[vc++] = 0; vertices[vc++] = 1; vertices[vc++] = 0; vertices[vc++] = 0.5; vertices[vc++] = 0.5; // 外徑上頂點 起點邊 vertices[vc++] = radius; vertices[vc++] = halfHeight; vertices[vc++] = 0; vertices[vc++] = 0; vertices[vc++] = 1; vertices[vc++] = 0; vertices[vc++] = 0.5; vertices[vc++] = 0.5; // 畫三角形 indices[ic++] = verticeCount + 0; indices[ic++] = verticeCount + 1; indices[ic++] = verticeCount + 2;
找到三個點是挺簡單的。但是一開始我不知道在Laya中應該怎么描述出來,后來反反復復的看官方的例子,大概猜了一下,一個頂點由8位數據描述,前三個是坐標,最后兩個影響貼圖,中間三個不確定,依葫蘆畫瓢先找三個點再說。。。然后是頂點索引,前面的教程有提到頂點索引的順時針、逆時針順序,會影響最終顯示出來的三角形,一直不理解是什么意思,一直改來改去的看效果,最終理解的是:0,1,2,表示先第一個頂點,再第二個,再第三個為順時針方向,如果0,2,1,則表示先第一個,再第三個,再第二個為逆時針方向,會影響顯示面(前面教程中提到0,1,2為直線,會導致看不見,第三個點需要到下一行的第一個。這句話還是沒理解透)。最終摸索出上述代碼,畫出來一個三角形。然后再折騰好久,兩個封邊都畫出來了,上最終代碼:
radius = radius === undefined ? 0.5 : radius height = height === undefined ? 2 : height slices = slices === undefined ? 32 : slices sectorAngle = sectorAngle === undefined ? 360 : sectorAngle // 封邊頂點 let edgeVertex = sectorAngle < 360 ? 6 : 0 let edgeIndex = sectorAngle < 360 ? 4 : 0 // 需要畫的頂點 let drawVertex = slices * (sectorAngle / 360).toFixed(1) // 頂點數量(上圓面,前后封邊,中間厚度,下圓面) let vertexCount = (drawVertex + 1 + 1) + edgeVertex + (drawVertex + 1) * 2 + (drawVertex + 1 + 1) // 索引數量 let indexCount = 3 * drawVertex + edgeIndex * 3 + 6 * drawVertex + 3 * drawVertex // 頂點聲明 let vertexDeclaration = Laya.VertexMesh.getVertexDeclaration("POSITION,NORMAL,UV") let vertexFloatStride = vertexDeclaration.vertexStride / 4 // 申請數據 let vertices = new Float32Array(vertexCount * vertexFloatStride) let indices = new Uint16Array(indexCount) // 切片角度 let sliceAngle = (Math.PI * 2.0) / slices // 半高(模型處於坐標中心點) let halfHeight = height * 0.5 // let curAngle = 0 // 當前頂點數量 let verticeCount = 0 // 坐標位置 let posX = 0 let posY = 0 let posZ = 0 // 數組索引 let vc = 0 let ic = 0 // 調整切片總數 //slices *= (sectorAngle / 360).toFixed(1) // 這個值如果是個小數,會出現問題 slices = drawVertex // 每8個數據,描述一個點信息:前三個為坐標,中間三個好像是影響光照效果,后兩個不確定 // 上圓面頂點數據 for (let tv = 0; tv <= slices; tv++) { if (tv === 0) { vertices[vc++] = 0 vertices[vc++] = halfHeight vertices[vc++] = 0 vertices[vc++] = 0 vertices[vc++] = 1 vertices[vc++] = 0 vertices[vc++] = 0.5 vertices[vc++] = 0.5 } curAngle = tv * sliceAngle posX = Math.cos(curAngle) * radius posY = halfHeight posZ = Math.sin(curAngle) * radius vertices[vc++] = posX vertices[vc++] = posY vertices[vc++] = posZ vertices[vc++] = 0 vertices[vc++] = 1 vertices[vc++] = 0 vertices[vc++] = 0.5 + Math.cos(curAngle) * 0.5 vertices[vc++] = 0.5 + Math.sin(curAngle) * 0.5 } // 上部圓面三角索引數據 for (var ti = 0; ti < slices; ti++) { indices[ic++] = 0 indices[ic++] = ti + 1 indices[ic++] = ti + 2 } verticeCount += slices + 1 + 1 // 畫封邊 起點封邊和結束封邊,總共6個點,要畫4個三角形 if (edgeVertex > 0) { // 內徑上頂點 vertices[vc++] = 0 vertices[vc++] = halfHeight vertices[vc++] = 0 vertices[vc++] = 0 vertices[vc++] = 1 vertices[vc++] = 0 vertices[vc++] = 0.5 vertices[vc++] = 0.5 // 內徑下頂點 vertices[vc++] = 0 vertices[vc++] = -halfHeight vertices[vc++] = 0 vertices[vc++] = 0 vertices[vc++] = 1 vertices[vc++] = 0 vertices[vc++] = 0.5 vertices[vc++] = 0.5 // 外徑上頂點 起點邊 vertices[vc++] = radius vertices[vc++] = halfHeight vertices[vc++] = 0 vertices[vc++] = 0 vertices[vc++] = 1 vertices[vc++] = 0 vertices[vc++] = 0.5 vertices[vc++] = 0.5 // 外徑下頂點 起點邊 vertices[vc++] = radius vertices[vc++] = -halfHeight vertices[vc++] = 0 vertices[vc++] = 0 vertices[vc++] = 1 // 這個會影響光照 vertices[vc++] = 0 vertices[vc++] = 0.5 vertices[vc++] = 0.5 // 外徑上頂點 結束邊 vertices[vc++] = posX vertices[vc++] = halfHeight vertices[vc++] = posZ vertices[vc++] = 0 vertices[vc++] = 1 vertices[vc++] = 0 vertices[vc++] = 0.5 vertices[vc++] = 0.5 // 外徑下頂點 結束邊 vertices[vc++] = posX vertices[vc++] = -halfHeight vertices[vc++] = posZ vertices[vc++] = 0 vertices[vc++] = 1 // 這個會影響光照 vertices[vc++] = 0 vertices[vc++] = 0.5 vertices[vc++] = 0.5 // 畫兩個三角形(內上、內下、外上, 外下、外上、內下) 起點封邊 indices[ic++] = verticeCount + 0 indices[ic++] = verticeCount + 1 indices[ic++] = verticeCount + 2 indices[ic++] = verticeCount + 3 indices[ic++] = verticeCount + 2 indices[ic++] = verticeCount + 1 // 畫兩個三角形(內上、內下、外上, 外下、外上、內下) 結束封邊 indices[ic++] = verticeCount + 0 indices[ic++] = verticeCount + 4 indices[ic++] = verticeCount + 1 indices[ic++] = verticeCount + 5 indices[ic++] = verticeCount + 1 indices[ic++] = verticeCount + 4 verticeCount += edgeVertex } // 畫出厚度外圈 for (let rv = 0; rv <= slices; rv++) { curAngle = rv * sliceAngle posX = Math.cos(curAngle) * radius posY = halfHeight posZ = Math.sin(curAngle) * radius vertices[vc++] = posX vertices[vc + (slices + 1) * 8 - 1] = posX vertices[vc++] = posY vertices[vc + (slices + 1) * 8 - 1] = -posY vertices[vc++] = posZ vertices[vc + (slices + 1) * 8 - 1] = posZ vertices[vc++] = posX vertices[vc + (slices + 1) * 8 - 1] = posX vertices[vc++] = 0 vertices[vc + (slices + 1) * 8 - 1] = 0 vertices[vc++] = posZ vertices[vc + (slices + 1) * 8 - 1] = posZ vertices[vc++] = 1 - rv * 1 / slices vertices[vc + (slices + 1) * 8 - 1] = 1 - rv * 1 / slices vertices[vc++] = 0 vertices[vc + (slices + 1) * 8 - 1] = 1 } vc += (slices + 1) * 8 // z軸三角 for (let ri = 0; ri < slices; ri++) { indices[ic++] = ri + verticeCount + (slices + 1) indices[ic++] = ri + verticeCount + 1 indices[ic++] = ri + verticeCount indices[ic++] = ri + verticeCount + (slices + 1) indices[ic++] = ri + verticeCount + (slices + 1) + 1 indices[ic++] = ri + verticeCount + 1 } verticeCount += 2 * (slices + 1) // 畫出下圓面 for (let bv = 0; bv <= slices; bv++) { if (bv === 0) { vertices[vc++] = 0 vertices[vc++] = -halfHeight vertices[vc++] = 0 vertices[vc++] = 0 vertices[vc++] = -1 vertices[vc++] = 0 vertices[vc++] = 0.5 vertices[vc++] = 0.5 } curAngle = bv * sliceAngle posX = Math.cos(curAngle) * radius posY = -halfHeight posZ = Math.sin(curAngle) * radius vertices[vc++] = posX vertices[vc++] = posY vertices[vc++] = posZ vertices[vc++] = 0 vertices[vc++] = -1 vertices[vc++] = 0 vertices[vc++] = 0.5 + Math.cos(curAngle) * 0.5 vertices[vc++] = 0.5 + Math.sin(curAngle) * 0.5 } for (let bi = 0; bi < slices; bi++) { indices[ic++] = 0 + verticeCount indices[ic++] = bi + 2 + verticeCount indices[ic++] = bi + 1 + verticeCount } verticeCount += slices + 1 + 1 return Laya.PrimitiveMesh._createMesh(vertexDeclaration, vertices, indices)
上述代碼是在官方生成圓柱體的基礎上改的,加了一部分我理解的注釋,可能有誤。核心的地方在第一部分for循環后,加入的封邊操作,最終畫出來的基本上達到了效果。