欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

自定义Mesh动态绘制

程序员文章站 2024-03-22 16:10:04
...

自定义Mesh动态绘制
自定义Mesh动态绘制
自定义Mesh动态绘制
自定义Mesh动态绘制
自定义Mesh动态绘制
自定义Mesh动态绘制

1 前言(该部分为技术介绍,可跳读)

本文分享一种自定义Mesh动态绘制的方法,gif中的游戏效果即为本方法的实际效果。自定义Mesh,即是开发者或用户任意绘制的3D模型(本方法是对Laya底层createBox()的拓展,所以3D模型多为线体);动态绘制则体现本方法的实时性。

本方法的设计背景系本人实习期对一款名为《画个腿快跑》的H5游戏开发。当今动手益智类小游戏中运用绘制模型吸引儿童玩家的实例不在少数,而简单的使用Box堆叠模型显然低效。另外,在类似《画腿》这样需要绘制的模型是刚体时,对模型组件的规约便十分复杂(该结论总结于与老同事《画车》项目的对比)。

最后,在老公司做完技术分享后,我便有了写作该文的想法,至今提笔。目录见下图1.1:

自定义Mesh动态绘制
图1.1 自定义Mesh动态绘制介绍目录


预备知识介绍(有相关基础者,可跳读)

本方法涉及知识包括:计算机图形学、LayaAir引擎(非物理)与平面几何。首先,读者要知道计算机中的3D模型都是通过三角形面片组合而成,直观地看(如图2.1),读者也可自行去Laya官网体验:https://layaair.ldc.layabox.com/demo2/?language=zh&category=3d&group=Mesh&name=MeshLoad。由此可见模型的形态是以何种形式的数据在计算机中存储的,本文介绍的方法便是打算从三角形面片入手来实时绘制3D模型(对Laya底层中的CreateBox()进行升级)。

 

自定义Mesh动态绘制
图2.1 计算机3D模型(Mesh)的形态

在模型绘制时遇到角度计算则运用简单的平面几何知识。本人非数学专业,仅仅拿数学当工具,在这一节里系统地概括运用到的数学方法对本人来说困难了,望读者海涵。

最后,分享会上也有人问:用户的2D形状描述要怎么实现?由于这一部分(Graphic API)要介绍的话,篇幅不会比我想介绍的createLine()算法内容少,当时不想本末倒置(现在也不想),并且官网和论坛上已经有很好的介绍读物和入门教程了。https://blog.csdn.net/wangmx1993328/article/details/84997876绘制时保存下来的点集就是就是用户自定义模型的数据描述;绘制后的图形就是形态描述。

CreateBox介绍(正题)

从小节标题上看,这节还**在介绍。不过本节内容可以说是学会自定义Mesh动态绘制的最关键,因为理解了Laya底层的CreateBox,读者才能知道Laya是怎么在计算机上画出3D模型的。

new Laya.MeshSprite3D(Laya.PrimitiveMesh.createBox());

首先createBox()是Laya.PrimitiveMesh提供的一个方法,用来生成一个立方体网格模型。值得一提的是PrimitiveMesh里已经提供了常用的3D模型的生成方法,例如球、锥等等,详细用法读者可参考官方文档https://ldc2.layabox.com/doc/?language=zh&nav=zh-ts-4-8-3。效果如图3.1

自定义Mesh动态绘制
图3.1 PrimitiveMesh内基本模型的生成效果

 

说回createBox(),就用程序员最喜欢的方式介绍了。上代码(代码来源Laya.d3.js,该代码中的注释最为重要):

static createBox(long = 1, height = 1, width = 1) {
    var vertexDeclaration = VertexMesh.getVertexDeclaration("POSITION,NORMAL,UV");//这里获取顶点声明, POSITION 表示每个点的前三个数值代表这个点的位置XYZ;NORMAL 指中间的三个数值,这三个数值在-1到1之间,会影响到光照;最后UV后面两个点的数值会影响到贴图,可以理解成贴图在模型上的位置。
    var halfLong = long / 2; 
    var halfHeight = height / 2;
    var halfWidth = width / 2; //halfLong,halfHeight,halfWidth用来确定立方体中心;
    var vertices = new Float32Array([
	            -halfLong, halfHeight, -halfWidth, 0, 1, 0, 0, 0, halfLong, halfHeight, -halfWidth, 0, 1, 0, 1, 0, halfLong, halfHeight, halfWidth, 0, 1, 0, 1, 1, -halfLong, halfHeight, halfWidth, 0, 1, 0, 0, 1,
	            -halfLong, -halfHeight, -halfWidth, 0, -1, 0, 0, 1, halfLong, -halfHeight, -halfWidth, 0, -1, 0, 1, 1, halfLong, -halfHeight, halfWidth, 0, -1, 0, 1, 0, -halfLong, -halfHeight, halfWidth, 0, -1, 0, 0, 0,
	            -halfLong, halfHeight, -halfWidth, -1, 0, 0, 0, 0, -halfLong, halfHeight, halfWidth, -1, 0, 0, 1, 0, -halfLong, -halfHeight, halfWidth, -1, 0, 0, 1, 1, -halfLong, -halfHeight, -halfWidth, -1, 0, 0, 0, 1,
	            halfLong, halfHeight, -halfWidth, 1, 0, 0, 1, 0, halfLong, halfHeight, halfWidth, 1, 0, 0, 0, 0, halfLong, -halfHeight, halfWidth, 1, 0, 0, 0, 1, halfLong, -halfHeight, -halfWidth, 1, 0, 0, 1, 1,
	            -halfLong, halfHeight, halfWidth, 0, 0, 1, 0, 0, halfLong, halfHeight, halfWidth, 0, 0, 1, 1, 0, halfLong, -halfHeight, halfWidth, 0, 0, 1, 1, 1, -halfLong, -halfHeight, halfWidth, 0, 0, 1, 0, 1,
	            -halfLong, halfHeight, -halfWidth, 0, 0, -1, 1, 0, halfLong, halfHeight, -halfWidth, 0, 0, -1, 0, 0, halfLong, -halfHeight, -halfWidth, 0, 0, -1, 0, 1, -halfLong, -halfHeight, -halfWidth, 0, 0, -1, 1, 1
	        ]);//vertices数组用来存储所用三角形面片的顶点信息,根据开始vertexDeclaration的声明来确定8位一组,和每位的意义。
    var indices = new Uint16Array([
	        	0, 1, 2, 2, 3, 0,
	            4, 7, 6, 6, 5, 4,
	            8, 9, 10, 10, 11, 8,
	            12, 15, 14, 14, 13, 12,
	            16, 17, 18, 18, 19, 16,
	            20, 23, 22, 22, 21, 20
        ]);//indices索引数组,记录了vertices的连接方式,链接方向(顺逆时针)与法线决定了面片的正反面,有时看不见绘制的面片就是由于视线看到的是面片的反面。
    return PrimitiveMesh._createMesh(vertexDeclaration, vertices, indices);//最后给createMesh方法传入处理好的参数即可绘制。
}		

再来具体分析一下,以一面为例,即根据indices中[0,1,2,2,3,0]的连接而绘制的面。0号点在vertices数组中是[-halfLong, halfHeight, -halfWidth, 0, 1, 0, 0, 0](0-7位);1号点在vertices数组中是[ halfLong, halfHeight, -halfWidth, 0, 1, 0, 1, 0](8-15位);2号点3号点以次类推。在确定数据点和连接方式后便可按下图3.2绘出立方体的一个面。其余各面的绘制同理。

自定义Mesh动态绘制
图3.2 一个面的绘制

4 createLine设计与实现(核心)

在了解底层Mesh的绘制后,就可以按自己的需求随心所欲的绘制Mesh模型了。本人的createLine()是根据《画腿》游戏的需求设计的自定义Mesh模型(本人称之为线体,立方线体)。作为一个整体方便加入刚体组件,也方便2D图形转入3D世界。话不多说,上代码(该代码建议copy或学会思路后重写,不建议理解):

	    static createLine(point = []) {//需要传入参数,点击[x0,y0,x1,y1...]
	        var vertexDeclaration = VertexMesh.getVertexDeclaration("POSITION,NORMAL,UV");//顶点声明
			var z = -0.8;//因为线体在x-y平面上延伸,所以在确定顶点是最好事先在3D世界内根据需求定好平面位置。
			
			var width = 0.4;//线体宽度
	        //1-上下;2-前后 3-左右
			var halfWidth = width / 2;
			var v_a = new Array();//顶点数组
			var i_a = new Array();//索引数组,至于为什么先用Array(),是因为可以使用push方法避免对存储位置进行计算。
			//前面(正面)
			for(var i = 0; i < point.length-2; i=i+2){
				var changeAngle = Math.atan2((point[3+i] - point[1+i]),(point[2+i] - point[0+i]));
				//0
				v_a.push(point[0+i]+halfWidth*Math.cos(1.5*Math.PI+changeAngle));
				v_a.push(z);
				v_a.push(point[1+i]+halfWidth*Math.sin(1.5*Math.PI+changeAngle));
				v_a.push(0);v_a.push(0);v_a.push(-1); v_a.push(0);v_a.push(1);
				//1
				v_a.push(point[2+i]+halfWidth*Math.cos(1.5*Math.PI+changeAngle));
				v_a.push(z);
				v_a.push(point[3+i]+halfWidth*Math.sin(1.5*Math.PI+changeAngle));
				v_a.push(0);v_a.push(0);v_a.push(-1); v_a.push(0);v_a.push(1);
				//2
				v_a.push(point[2+i]+halfWidth*Math.cos(0.5*Math.PI+changeAngle));
				v_a.push(z);
				v_a.push(point[3+i]+halfWidth*Math.sin(0.5*Math.PI+changeAngle));
				v_a.push(0);v_a.push(0);v_a.push(-1); v_a.push(0);v_a.push(1);
				//3
				v_a.push(point[0+i]+halfWidth*Math.cos(0.5*Math.PI+changeAngle));
				v_a.push(z);
				v_a.push(point[1+i]+halfWidth*Math.sin(0.5*Math.PI+changeAngle));
				v_a.push(0);v_a.push(0);v_a.push(-1); v_a.push(0);v_a.push(1);
				i_a.push(0+4*(i/2));i_a.push(3+4*(i/2));i_a.push(2+4*(i/2));i_a.push(2+4*(i/2));i_a.push(1+4*(i/2));i_a.push(0+4*(i/2));
				if(i != point.length-4){
					i_a.push(1+4*(i/2));i_a.push(2+4*(i/2));i_a.push(3+4*((i/2)+1));i_a.push(3+4*((i/2)+1));i_a.push(0+4*((i/2)+1));i_a.push(1+4*(i/2));
				}
			}
			var total = v_a.length/8;
			var f0 = total-1-2;var f1= total-1-1;
			//不可见 
			for(var i = 0; i < point.length-2; i=i+2){
				var changeAngle = Math.atan2((point[3+i] - point[1+i]),(point[2+i] - point[0+i]));
				//0
				v_a.push(point[0+i]+halfWidth*Math.cos(1.5*Math.PI+changeAngle));
				v_a.push(z/3*2);
				v_a.push(point[1+i]+halfWidth*Math.sin(1.5*Math.PI+changeAngle));
				v_a.push(0);v_a.push(0);v_a.push(-1); v_a.push(0);v_a.push(1);
				//1
				v_a.push(point[2+i]+halfWidth*Math.cos(1.5*Math.PI+changeAngle));
				v_a.push(z/3*2);
				v_a.push(point[3+i]+halfWidth*Math.sin(1.5*Math.PI+changeAngle));
				v_a.push(0);v_a.push(0);v_a.push(-1); v_a.push(0);v_a.push(1);
				//2
				v_a.push(point[2+i]+halfWidth*Math.cos(0.5*Math.PI+changeAngle));
				v_a.push(z/3*2);
				v_a.push(point[3+i]+halfWidth*Math.sin(0.5*Math.PI+changeAngle));
				v_a.push(0);v_a.push(0);v_a.push(-1); v_a.push(0);v_a.push(1);
				//3
				v_a.push(point[0+i]+halfWidth*Math.cos(0.5*Math.PI+changeAngle));
				v_a.push(z/3*2);
				v_a.push(point[1+i]+halfWidth*Math.sin(0.5*Math.PI+changeAngle));
				v_a.push(0);v_a.push(0);v_a.push(-1); v_a.push(0);v_a.push(1);
				i_a.push(0+4*(i/2)+total);i_a.push(1+4*(i/2)+total);i_a.push(2+4*(i/2)+total);i_a.push(2+4*(i/2)+total);i_a.push(3+4*(i/2)+total);i_a.push(0+4*(i/2)+total);
				if(i != point.length-4){
					i_a.push(3+4*(i/2)+total);i_a.push(2+4*(i/2)+total);i_a.push(1+4*((i/2)+1)+total);i_a.push(1+4*((i/2)+1)+total);i_a.push(0+4*((i/2)+1)+total);i_a.push(3+4*(i/2)+total);
				}
				i_a.push(0+4*(i/2));i_a.push(0+4*(i/2)+total);i_a.push(1+4*(i/2));i_a.push(1+4*(i/2)+total);i_a.push(1+4*(i/2));i_a.push(0+4*(i/2)+total);
				if(i != point.length-4){
					i_a.push(1+4*((i/2)+1));i_a.push(1+4*((i/2)+1)+total);i_a.push(0+4*(i/2));i_a.push(0+4*(i/2)+total);i_a.push(0+4*(i/2));i_a.push(1+4*((i/2)+1)+total);
				}
				i_a.push(2+4*(i/2)+total);i_a.push(3+4*(i/2)+total);i_a.push(3+4*(i/2));i_a.push(3+4*(i/2));i_a.push(2+4*(i/2));i_a.push(2+4*(i/2)+total);
				if(i != point.length-4){
					i_a.push(2+4*(i/2));i_a.push(3+4*(i/2));i_a.push(3+4*((i/2)+1)+total);i_a.push(3+4*((i/2)+1)+total);i_a.push(2+4*((i/2)+1)+total);i_a.push(2+4*(i/2));
				}
			}
			var total1 = v_a.length/8;
			var f3 = total1-1-2;var f2= total1-1-1;
			i_a.push(f0);i_a.push(f3);i_a.push(f2);i_a.push(f2);i_a.push(f1);i_a.push(f0);
			for(var i = 0; i < point.length-2; i=i+2){
				var changeAngle = Math.atan2((point[3+i] - point[1+i]),(point[2+i] - point[0+i]));
				//0
				v_a.push(-(point[0+i]+halfWidth*Math.cos(1.5*Math.PI+changeAngle)));
				v_a.push(-z/3*2);
				v_a.push(-(point[1+i]+halfWidth*Math.sin(1.5*Math.PI+changeAngle)));
				v_a.push(0);v_a.push(0);v_a.push(-1); v_a.push(0);v_a.push(1);
				//1
				v_a.push(-(point[2+i]+halfWidth*Math.cos(1.5*Math.PI+changeAngle)));
				v_a.push(-z/3*2);
				v_a.push(-(point[3+i]+halfWidth*Math.sin(1.5*Math.PI+changeAngle)));
				v_a.push(0);v_a.push(0);v_a.push(-1); v_a.push(0);v_a.push(1);
				//2
				v_a.push(-(point[2+i]+halfWidth*Math.cos(0.5*Math.PI+changeAngle)));
				v_a.push(-z/3*2);
				v_a.push(-(point[3+i]+halfWidth*Math.sin(0.5*Math.PI+changeAngle)));
				v_a.push(0);v_a.push(0);v_a.push(-1); v_a.push(0);v_a.push(1);
				//3
				v_a.push(-(point[0+i]+halfWidth*Math.cos(0.5*Math.PI+changeAngle)));
				v_a.push(-z/3*2);
				v_a.push(-(point[1+i]+halfWidth*Math.sin(0.5*Math.PI+changeAngle)));
				v_a.push(0);v_a.push(0);v_a.push(-1); v_a.push(0);v_a.push(1);
				i_a.push(0+4*(i/2)+total1);i_a.push(3+4*(i/2)+total1);i_a.push(2+4*(i/2)+total1);i_a.push(2+4*(i/2)+total1);i_a.push(1+4*(i/2)+total1);i_a.push(0+4*(i/2)+total1);
				if(i != point.length-4){
					i_a.push(1+4*(i/2)+total1);i_a.push(2+4*(i/2)+total1);i_a.push(3+4*((i/2)+1)+total1);i_a.push(3+4*((i/2)+1)+total1);i_a.push(0+4*((i/2)+1)+total1);i_a.push(1+4*(i/2)+total1);
				}
			}	
			var total2 = v_a.length/8;
			var ff3 = total2-1-2;var ff2= total2-1-1;
			//不可见 后
			for(var i = 0; i < point.length-2; i=i+2){
				var changeAngle = Math.atan2((point[3+i] - point[1+i]),(point[2+i] - point[0+i]));
				//0
				v_a.push(-(point[0+i]+halfWidth*Math.cos(1.5*Math.PI+changeAngle)));
				v_a.push(-z);
				v_a.push(-(point[1+i]+halfWidth*Math.sin(1.5*Math.PI+changeAngle)));
				v_a.push(0);v_a.push(0);v_a.push(-1); v_a.push(0);v_a.push(1);
				//1
				v_a.push(-(point[2+i]+halfWidth*Math.cos(1.5*Math.PI+changeAngle)));
				v_a.push(-z);
				v_a.push(-(point[3+i]+halfWidth*Math.sin(1.5*Math.PI+changeAngle)));
				v_a.push(0);v_a.push(0);v_a.push(-1); v_a.push(0);v_a.push(1);
				//2
				v_a.push(-(point[2+i]+halfWidth*Math.cos(0.5*Math.PI+changeAngle)));
				v_a.push(-z);
				v_a.push(-(point[3+i]+halfWidth*Math.sin(0.5*Math.PI+changeAngle)));
				v_a.push(0);v_a.push(0);v_a.push(-1); v_a.push(0);v_a.push(1);
				//3
				v_a.push(-(point[0+i]+halfWidth*Math.cos(0.5*Math.PI+changeAngle)));
				v_a.push(-z);
				v_a.push(-(point[1+i]+halfWidth*Math.sin(0.5*Math.PI+changeAngle)));
				v_a.push(0);v_a.push(0);v_a.push(-1); v_a.push(0);v_a.push(1);
				i_a.push(0+4*(i/2)+total2);i_a.push(1+4*(i/2)+total2);i_a.push(2+4*(i/2)+total2);i_a.push(2+4*(i/2)+total2);i_a.push(3+4*(i/2)+total2);i_a.push(0+4*(i/2)+total2);
				if(i != point.length-4){
					i_a.push(3+4*(i/2)+total2);i_a.push(0+4*((i/2)+1)+total2);i_a.push(1+4*((i/2)+1)+total2);i_a.push(1+4*((i/2)+1)+total2);i_a.push(2+4*(i/2)+total2);i_a.push(3+4*(i/2)+total2);
				}
				i_a.push(1+4*(i/2)+total1);i_a.push(0+4*(i/2)+total2);i_a.push(0+4*(i/2)+total1);i_a.push(1+4*(i/2)+total1);i_a.push(1+4*(i/2)+total2);i_a.push(0+4*(i/2)+total2);
				if(i != point.length-4){
					i_a.push(0+4*((i/2)+1)+total1);i_a.push(1+4*(i/2)+total2);i_a.push(1+4*(i/2)+total1);i_a.push(1+4*(i/2)+total2);i_a.push(0+4*((i/2)+1)+total1);i_a.push(0+4*((i/2)+1)+total2);
				}
				i_a.push(2+4*(i/2)+total2);i_a.push(3+4*(i/2)+total2);i_a.push(3+4*(i/2)+total1);i_a.push(3+4*(i/2)+total1);i_a.push(2+4*(i/2)+total1);i_a.push(2+4*(i/2)+total2);
				if(i != point.length-4){
					i_a.push(3+4*(i/2)+total2);i_a.push(2+4*((i/2)+1)+total2);i_a.push(2+4*((i/2)+1)+total1);i_a.push(2+4*((i/2)+1)+total1);i_a.push(3+4*(i/2)+total1);i_a.push(3+4*(i/2)+total2);
				}
			}
			var total3 = v_a.length/8;
			var ff0 = total3-1-2;var ff1= total3-1-1;
			i_a.push(ff0);i_a.push(ff3);i_a.push(ff2);i_a.push(ff2);i_a.push(ff1);i_a.push(ff0);
			var vertices = new Float32Array(v_a);//存成createMesh所要求的形式。
			var indices = new Uint16Array(i_a);
	        // var indices = new Uint16Array([
			// 	0, 3, 2, 2, 1, 0, //要这个
			// 	//0, 1, 2, 2, 3, 0, //要这个
	        // ]);
	        return PrimitiveMesh._createMesh(vertexDeclaration, vertices, indices);
	    }

由于定点和索引的确定是个烦琐的流程,且总共6面需要绘制,所以代码冗长,不建议理解代码和数学计算。如果读者的开发需求与本人相似,建议理解以下设计思路后直接套用代码;若不同,则可以学习设计思路自主设计顶点和索引的确定方法。

设计思路如下:

1.通过Graphic API来实现需求描述,获取一点集,如图4.1

自定义Mesh动态绘制
图4.1 获取点集数据

2.在点集中,每两点绘制一个立方体面,其中顶点位置要根据两点连线的斜率去确定,步骤如图4.2;

自定义Mesh动态绘制
图4.2 线体一面的绘制过程

当然同理可绘制其他各面。注意:(1)若先绘制了前面和后面,线体的上面和下面的顶点已经存在,只要确定索引即可。(2)在实际操作中,可以先使用Array来生成顶点和索引数组,这是因为push方法可以避免数据下标的计算。不过最后还得记得转化为Float32Array和Unit16Array,具体用法见不易理解的那段代码。(3)createLine()方法放在Laya.d3.js的createBox()下即可,同时别忘了在LayaAir.d.ts下进行函数声明,也声明在createBox下面即可。

		/**
		 * 创建一个线体模型
		 * @param point 参考点集
		 */
		static createLine(point?:Array<number>):laya.d3.resource.models.Mesh;

5 补面方案设计

根据以上方法实现的线体在近、中、远景中基本上是看不见缝隙的(模型不过分大,且点集数据不过分贫乏)。不过也有老同事在特殊项目中指出了本人的绘制方法存在缺缝。本人简单地提两种补面方案。

方案一,缺缝处双面绘制。就是在两个面之间再根据已有顶点绘制新面,且新面正反都绘制。

方案二,先根据斜率判断缺缝类型,再有针对地进行单面绘制。

 

本人不太会讲话,但还是有心想给有需要的人提供些思路,效果见文前gif