OpenGL数据可视化(5)绘制三维曲面

对本系列绘制三维地震切片的代码稍作修改,就能够实现三维曲面的绘制。

从二维平面到三维曲面

无论是二维平面还是三维曲面,都是由若干三角形组成的,只是平面可以仅通过绘制两个大三角得到,而三维曲面为了获得高精细度就需要绘制许多的小三角形。

数据与着色器

绘制二维平面时,二维纹理的宽和高对应 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);
    /* ---省略--- */
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值