以前自己已经摸索过libGDX逐像素效果的用法了,但一直没有认真研究过顶点绘制的相关逻辑。现在借着AI问了一下,也算是入门了,写个文章记录一下。
画三角形
虽然底层是画三角形,但这里用画直线来举例子。
实际上画直线就是画一个矩形罢了,而一个矩形可以分割成两个三角形,所以实际上绘制一条直线就是绘制两个三角形。在直线的头尾增加以一半宽度向法线方向扩展,这样就能形成4个点。理论上很简单,不好理解的主要是Mesh的接口实现。
Mesh可以认为只是一个传给GPU的顶点数组,但这个数组要怎么读取要看具体参数怎么设置。
/** Creates a new Mesh with the given attributes.
*
* @param isStatic whether this mesh is static or not. Allows for internal optimizations.
* @param maxVertices the maximum number of vertices this mesh can hold
* @param maxIndices the maximum number of indices this mesh can hold
* @param attributes the {@link VertexAttribute}s. Each vertex attribute defines one property of a vertex such as position,
* normal or texture coordinate
*/
public Mesh (boolean isStatic, int maxVertices, int maxIndices, VertexAttribute... attributes)- isStatic代表的是这个图形是否经常改变,这是为了给显卡优化用的,如果顶点变化并不频繁,那么可以设置为true.
- maxVertices是指这个Mesh有多少个顶点。
- maxIndices则是指上面这些顶点的下标的顺序——Mesh并不是按照顶点数组的顺序处理顶点的,而是另外用一个下标数组来定义顶点处理顺序,原因其实很好理解,因为有些顶点需要可能需要重复使用,所以通过下标来定义顶点读取顺序可以有效减少重复数据的传递。
- attributes则是用来定义每个顶点所含的数据类型,这也是为后面定义顶点数据大小的依据。
下面直接来看一个例子就好了:
Mesh lineMesh;
ShaderProgram lineShader;
void init() {
Mesh lineMesh = new Mesh(true, 4, 6, // 4个顶点,总共6个坐标
new VertexAttribute(VertexAttributes.Usage.Position, 2, ShaderProgram.POSITION_ATTRIBUTE)); // 每个顶点一个Position(就是两个float)
lineMesh.setIndices(new short[]{0, 1, 2, 2, 1, 3}); // 前三个为一个三角形,后三个为一个三角形
lineVertices = new float[4 * 2]; // 4个顶点,每个点2个float
lineShader = new ShaderProgram(Gdx.files.internal("shaders/line.vert"), Gdx.files.internal("shaders/line.frag"));
updateLine(100, 100, 500, 400, 20f);
}
void updateIndexies(float x1, float y1, float x2, float y2, float width) {
Vector2 dir = new Vector2(x2 - x1, y2 - y1);
Vector2 normal = new Vector2(-dir.y, dir.x).nor().scl(width / 2f);
lineVertices[0] = x1 + normal.x; lineVertices[1] = y1 + normal.y;
lineVertices[2] = x1 - normal.x; lineVertices[3] = y1 - normal.y;
lineVertices[4] = x2 + normal.x; lineVertices[5] = y2 + normal.y;
lineVertices[6] = x2 - normal.x; lineVertices[7] = y2 - normal.y;
lineMesh.setVertices(lineVertices);
}
void draw(Batch batch) {
lineShader.bind();
lineShader.setUniformMatrix("u_projModelView", getViewport().getCamera().combined); // 把当前camera的投影矩阵传给shader
lineShader.setUniformf("u_color", Color.CYAN);
lineMesh.render(lineShader, GL20.GL_TRIANGLES);
}// shaders/line.vert
#version 130
in vec4 a_position;
uniform mat4 u_projModelView;
void main() {
gl_Position = u_projModelView * a_position; // 获取当前camera的投影矩阵,从而让传出坐标变成投影后的坐标
}// shaders/line.frag
#version 130
#ifdef GL_ES
precision mediump float;
#endif
uniform vec4 u_color;
void main() {
gl_FragColor = u_color;
}这个例子很简单,看看注释就懂了,需要注意的是,VertexAttribute的构造参数最后一个是alias,会绑定到vert(点着色器)的入参,在这个例子里ShaderProgram.POSITION_ATTRIBUTE其实就是字符串”a_position”,所以数组里的坐标就成了vert里a_position参数了。这下我也总算是知道了这些莫名其妙约定俗成的参数是怎么来的。
填充颜色
虽然上面的例子解决了画形状的问题,但是实际绘制的时候,肯定还需要给形状填充颜色,比如说渐变色,我可以直接通过例如x坐标的变化生成一套渐变。
但是如果我想要让渐变沿线段的方向变化该怎么办?虽说我可以在frag(面着色器)上对屏幕坐标进行判断,但这样做就需要对每个像素点屏幕坐标进行一次计算,性能开销会增大。实际上,这个逻辑应该可以通过点着色器进行插值,从而避免计算下降到面着色器中。
可以在Mesh中提供顶点的uv数值空间,实际操作就是在Mesh中,给每个顶点增加一个新的Attribute:
// 修改后的
void init() {
Mesh lineMesh = new Mesh(true, 4, 6,
new VertexAttribute(VertexAttributes.Usage.Position, 2, ShaderProgram.POSITION_ATTRIBUTE),
// 加上一个材质坐标(范围0-1之间的float坐标点)
new VertexAttribute(VertexAttributes.Usage.TextureCoordinates, 2, ShaderProgram.TEXCOORD_ATTRIBUTE + "0"));
lineMesh.setIndices(new short[]{0, 1, 2, 2, 1, 3});
lineVertices = new float[4 * 4]; // 由于增加了个材质坐标,所以大小翻了一倍
lineShader = new ShaderProgram(Gdx.files.internal("shaders/line.vert"), Gdx.files.internal("shaders/line.frag"));
updateLine(100, 100, 500, 400, 20f);
}
void updateIndexies(float x1, float y1, float x2, float y2, float width) {
Vector2 dir = new Vector2(x2 - x1, y2 - y1);
Vector2 normal = new Vector2(-dir.y, dir.x).nor().scl(width / 2f);
lineVertices[0] = x1 + normal.x; lineVertices[1] = y1 + normal.y;
lineVertices[2] = 0f; lineVertices[3] = 0f; // 加上材质坐标
lineVertices[4] = x1 - normal.x; lineVertices[5] = y1 - normal.y;
lineVertices[6] = 0f; lineVertices[7] = 1f; // 加上材质坐标
lineVertices[8] = x2 + normal.x; lineVertices[9] = y2 + normal.y;
lineVertices[10] = 1f; lineVertices[11] = 0f; // 加上材质坐标
lineVertices[12] = x2 - normal.x; lineVertices[13] = y2 - normal.y;
lineVertices[14] = 1f; lineVertices[15] = 1f; // 加上材质坐标
lineMesh.setVertices(lineVertices);
}
void draw(Batch batch) {
lineShader.bind();
lineShader.setUniformMatrix("u_projModelView", getViewport().getCamera().combined); // 把当前camera的投影矩阵传给shader
lineShader.setUniformf("u_colorStart", Color.CYAN);
lineShader.setUniformf("u_colorEnd", Color.MAGENTA);
lineMesh.render(lineShader, GL20.GL_TRIANGLES);
}// shaders/line.vert
#version 130
in vec4 a_position;
in vec2 a_texCoord0;
out vec2 v_texCoords;
uniform mat4 u_projModelView;
void main() {
v_texCoords = a_texCoord0;
gl_Position = u_projModelView * a_position;
}#version 130
#ifdef GL_ES
precision mediump float;
#endif
in vec2 v_texCoords;
uniform vec4 u_colorStart;
uniform vec4 u_colorEnd;
void main() {
gl_FragColor = mix(u_colorStart, u_colorEnd, v_texCoords.x);
}
这样子,就知道材质坐标的(0, 0)是“绘制材质”的左下角,而(1, 1)是右上角。如果我想沿绘制的线的方向着色,只需要保证起点是0,终点是1,那么对照着计算渐变颜色的插值就行了,在这个例子中取x或y作渐变插值都行。
扇形绘制
如果绘制的图形是圆形或者扇形,有个单独的绘制类型GL_TRIANGLE_FAN来绘制。在这种绘制类型下,可以省略传递顶点坐标,将固定第一个点为圆心,其余的点依次相连作为三角形的对边来绘制,如果总共要绘制N个三角形,那么实际上要传递N+1个点。
int segments = 32; // 用32个三角形近似圆
float radius = 100;
Mesh circleMesh = new Mesh(true, segments + 1, 0, // 索引设为 0
new VertexAttribute(VertexAttributes.Usage.Position, 2, ShaderProgram.POSITION_ATTRIBUTE));
// ......
circleMesh = new float[(segments + 1) * 2];
circleMesh[0].x = 100;
circleMesh[0].y = 100;
for (int i = 1; i <= segments; i++) {
float angle = (float) (i * 2 * Math.PI / segments);
circleMesh[2 * i].x = 100 + radius * Math.cos(angle) * radius;
circleMesh[2 * i + 1].y = 100 + radius * Math.sin(angle) * radius;
}
circleMesh.setVertices(vertices);
// ......
circleMesh.render(lineShader, GL20.GL_TRIANGLE_FAN);三角带绘制
与扇形绘制相似,三角带绘制方式也是一种用来省略顶点坐标的绘制方式,其解析逻辑是直接以数组上邻近的三个点作为坐标依次绘制,所以如果要绘制N个三角形,那么就需要传递N+2个顶点。下面用一个绘制圆环的代码做例子:
int segments = 32;
float innerRadius = 50;
float outerRadius = 100;
Mesh ringMesh = new Mesh(true, segments + 2, 0, // 索引设为 0
new VertexAttribute(VertexAttributes.Usage.Position, 2, ShaderProgram.POSITION_ATTRIBUTE));
// ......
ringMesh = new float[(segments + 2) * 2];
ringMesh[0].x = 100;
ringMesh[0].y = 100;
for (int i = 0; i <= segments; i++) {
float angle = (float) (i * 2 * Math.PI / segments);
ringMesh[4 * i].x = 100 + radius * Math.cos(angle) * innerRadius;
ringMesh[4 * i + 1].y = 100 + radius * Math.sin(angle) * innerRadius;
ringMesh[4 * i + 2].x = 100 + radius * Math.cos(angle) * outerRadius;
ringMesh[4 * i + 3].y = 100 + radius * Math.sin(angle) * outerRadius;
}
circleMesh.setVertices(vertices);
// ......
circleMesh.render(lineShader, GL20.GL_TRIANGLE_STRIP);性能优化
虽然有三种绘制方法,但是其实更多的时候还是只用最通用的三角形绘制方法,因为这个方法最通用,可以把所有绘制都整合到一次Mesh的DrawCall中。而且需要注意,因为Mesh也是单独的一次DrawCall,所以不能跟Batch混用,必须首先结束当前事务。


Leave a Reply