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循环后,加入的封边操作,最终画出来的基本上达到了效果。