对本系列绘制三维地震切片的代码稍作修改,就能够实现三维曲面的绘制。
从二维平面到三维曲面
无论是二维平面还是三维曲面,都是由若干三角形组成的,只是平面可以仅通过绘制两个大三角得到,而三维曲面为了获得高精细度就需要绘制许多的小三角形。
数据与着色器
绘制二维平面时,二维纹理的宽和高对应 x 与 y,而 z 是垂直屏幕的所以看不到;而在绘制三维曲面时,二维纹理的宽和高对应 x 与 z,二维纹理在(xi,yi)处的值对应 y。
由于纹理的坐标在 [0, 1] 范围内,我们是将二维纹理完全覆盖在三维曲面上,那么只需向顶点着色器传入 x、y、z 位置坐标即可,纹理坐标可以根据 x、z 计算出来。
准备数据:
// x坐标 x_arr
float* x_arr = (float*)calloc(width, sizeof(float));
for (i = 0; i < width; i++) {
x_arr[i] = xstart+i*xstep; //xstart: x起始值 xstep: x步长
}
// 同样的方法生成z坐标 z_arr
// 分配二维数据data_raw的内存空间
float** data_raw= (float**)malloc(width * sizeof(float*));
for (int i = 0; i < width; i++) {
data_raw[i] = (float*)calloc(height, sizeof(float));
}
// 由x、z坐标根据方程生成二维数据data_raw
for (i = 0; i < width; i++) {
for (j = 0; j < height; j++) {
x = x_arr[i];
z = z_arr[j];
x2 = x * x;
z2 = z * z;
data_raw[i][j] = 3 * (1 - x) * (1 - x) * exp(-(x2)-(z + 1) * (z + 1)) - 10 * (x / 5 - x2 * x - z2 * z2 * z) * exp(-x2 - z2) - 1.0 / 3 * exp(-(x + 1) * (x + 1) - z2);
}
}
为方便计算纹理坐标以及未来添加坐标轴,将位置坐标线性缩放至 [-1, 1]:
// 分配内存空间
float* plt_x = (float*)calloc(width * height, sizeof(float));
float* plt_y = (float*)calloc(width * height, sizeof(float));
float* plt_z = (float*)calloc(width * height, sizeof(float));
float* vertices = (float*)calloc(width*height*3, sizeof(float));
// 循环遍历得到x_arr的最大最小值xmax、xmin,data_raw的最大最小值ymax、ymin,z_arr的最大最小值zmax、zmin
// 将位置坐标线性缩放至 [-1, 1]
int k = 0;
for (j = 0; j < height; j++) {
for (i = 0; i < width; i++) {
plt_x[k] = 2*(x_arr[i] - xmin)/(xmax-xmin)-1;
plt_y[k] = 2*(data_raw[i][j] - ymin)/(ymax-ymin)-1;
plt_z[k] = 2*(z_arr[j]-zmin)/(zmax-zmin)-1;
k += 1;
}
}
// 顶点坐标
for (i = 0; i < width * height; i += 1) {
vertices[3*i] = plt_x[i];
vertices[3*i+1] = plt_y[i];
vertices[3*i+2] = plt_z[i];
}
// 向OpenGL传入顶点坐标
unsigned int VBO, VAO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, width* height * 3 * sizeof(float), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
顶点坐标处理好后,通过二维数据 data_raw 按照本系列之前文章的做法生成二维纹理。
顶点着色器需要从 x、z 坐标计算出纹理坐标,将 [-1, 1] 线性缩放至 [0, 1]:
#version 330 core
layout (location = 0) in vec3 aPos;
out vec2 TexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
TexCoord = vec2(0.5*(aPos.x+1), 0.5*(aPos.z+1));
gl_Position = projection * view * model * vec4(aPos, 1.0f);
}
片段着色器和绘制二维平面时一样:
#version 330 core
in vec2 TexCoord;
out vec4 FragColor;
uniform sampler2D texture1;
void main()
{
FragColor = texture(texture1, TexCoord);
}
绘制三维曲面
每个三角形都需要 3 个顶点坐标,二维数据宽 w 高 h,总三角形个数是 2(w-1)(h-1),但我们并不需要向 OpenGL 传入 6(w-1)(h-1) 组顶点坐标。
由于很多小三角形有公用顶点,只需向 OpenGL 传入 w×h 组顶点坐标,然后用 EBO 告诉OpenGL 应该使用哪些顶点绘制三角形。
GL_TRIANGLES
通过绘制许多小三角形实现三维曲面,这种方法容易理解,不易出错。缺点是需要 6(w-1)(h-1) 个索引,内存开销略大,不推荐使用这种方法。
不使用 EBO 只需 6(w-1)(h-1) 组顶点坐标,使用 EBO 需要 wh 组顶点坐标和 6(w-1)(h-1) 个索引,内存使用更多了吗?
注意单位不同,每组顶点坐标有 x、y、z 三个float,顶点坐标只需一个unsigned int,float与unsigned int都是4字节,那么:
不使用 EBO 需要内存为 6(w-1)(h-1)×3×4
使用 EBO 需要内存为 wh×3×4+6(w-1)(h-1)×4
GL_TRIANGLE_STRIP
通过绘制三角条带实现三维曲面,相比上一种方法,内存开销更小。
Learn OpenGL ES 的方法
从Learn OpenGL ES学到只用一个条带绘制二维数据的方法(下图也是从该网站截取的):
indexBuffer = {
1, 6, 2, 7, 3, 8, 4, 9, 5, 10, 10, 6, 6, 11, 7, 12, 8, 13, 9, 14, 10, 15
}
通过重复添加转折点,类似(5,10,10)的三角形不会绘制,实现了条带的“转弯”。
但当我实现后,却发现这样的问题:(5,10,10)这种三角形还是会以线条的形式出现,如下图本应是凹陷的地方却出现了线条。
往返转折的条带
由于问题是“不应存在的三角形”边长过长导致的,那么只需要让条带往返转折即可解决问题,
indexBuffer = {
1, 6, 2, 7, 3, 8, 4, 9, 5, 10, 10, 15, 9, 14, 8, 13, 7, 12, 6, 11
}
虽然上一个问题解决了,但当开启面剔除后,又出现了新问题:
面剔除即不显示背面,OpenGL默认逆时针三角形为正面。在绘制 GL_TRIANGLE_STRIP 时,为确保方向一致,偶数个三角形的前两个顶点交换顺序。
以第一个矩形 1-2-6-7 为例,(1,6,2)为逆时针,(6,2,7)在绘制时顺序自动变成(2,6,7)逆时针。
分析问题根源,在于转折点处有两个“不存在的三角形”,不会改变后续条带绘制方向,而偶数行的绘制方向为顺时针,是背面,故被剔除。
解决方法,重复添加转折点,使得转折点处有三个“不存在的三角形”,改变后续条带绘制方向。
indexBuffer = {
1, 6, 2, 7, 3, 8, 4, 9, 5, 10, 10, 10, 15, 9, 14, 8, 13, 7, 12, 6, 11
}
图元重启
前述工作大费周章,无非是希望只用一行代码绘制多行数据,实际上,只需启用GL_PRIMITIVE_RESTART,就可以一次绘制多条三角条带,告诉OpenGL需要重启的索引glPrimitiveRestartIndex 即可。
//启用图元重启,设置重启索引为0xFFFF
glEnable(GL_PRIMITIVE_RESTART);
glPrimitiveRestartIndex(0xFFFF);
//索引总数
indices_n = (height - 1) * (2*width+1)-1; //实际应-2
//索引数组
unsigned int* indices = (unsigned int*)calloc(indices_n, sizeof(unsigned int));
for (j = 0; j < height - 1; j++) {
for (i = 0; i < width; i++) {
now_id = 2 * (j * width + i) + j;
indices[now_id] = j * width + i;
indices[now_id + 1] = (j + 1) * width + i;
if (i==width-1)
indices[now_id + 2] = 0xFFFF;
}
}
indices[indices_n - 1] = indices[indices_n - 2]; //最后一个不需要
// 向OpenGL传入索引
unsigned int EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, 4 * indices_n, indices, GL_STATIC_DRAW);
// 渲染循环
while (!glfwWindowShouldClose(window)){
/* ---省略--- */
// 绘制三维曲面
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLE_STRIP, indices_n, GL_UNSIGNED_INT, 0);
/* ---省略--- */
}