OpenGL ES SDK for Android - 8

Metaballs

使用GPU在OpenGL ES 3.0中创建有机外观的三维对象。

本教程演示了如何使用GPU使用OpenGL ES 3.0的变换反馈功能渲染有机外观的3D对象。 所有计算都在GPU的着色器处理器上实现。 使用Marching Cubes算法执行表面三角测量。 Phong模型用于照亮元球对象。 3D纹理用于提供对着色器中三维数组的访问。

Introduction

该应用程序显示在太空中移动的几个元球。 当元球彼此太靠近时,它们在成为有机整体之前平滑地改变它们的球形。 实现这种效果的最简单方法是跟随现实生活并使用带电球体的运动。 根据物理定律,带电球体周围的空间将具有电场。 因为空间中每个点的字段只有一个值,所以通常称为标量字段。 可以为空间中的每个点简单地计算标量场值(重力场是标量场的另一个示例,并且使用类似的公式计算,但应用于球体的权重)。

为了可视化标量场,我们将其表示为表面。 这通常通过选择一些值级别并显示包含所选级别的空间中所有点的表面来实现。 这种表面通常称为等值面。 通常根据视觉质量选择水平值。

我们需要考虑我们用于绘制3D对象的图形系统的功能。 OpenGL ES可以轻松渲染3D对象,但是用于显示OpenGL ES接受的曲面的唯一基元是三角形。 因此,我们拆分模型表面并将其表示为OpenGL ES作为一组三角形。 我们必须在这里近似,因为数学上定义的等值面是平滑的,而由三角形构成的表面则不是。 我们使用Marching Cubes算法近似表面。

为了使Marching Cubes算法起作用,我们对空间进行采样并定义等值面值。 Marching Cubes一次运行八个样本(每个单元角一个),形成基本的立方体单元。 请注意,此示例使用OpenGL ES3.0中的功能。 要使这些功能在设备上运行,必须至少使用Android 4.3或API版本18。

Stages

与处理3D图形的大多数应用程序一样,此示例包含多个组件。 Metaballs项目由五个组件构成:一个在CPU上运行的控制程序和四个在GPU上运行的补充着色器程序。 通常,在GPU上运行的程序由两部分组成:顶点着色器和片段着色器。 在这个例子中,我们使用八个着色器(每个阶段两个),但只有五个着色器包含实际的程序代码; 其余三个着色器是程序对象链接所需的虚拟着色器。

我们将应用程序分为以下几个阶段:

  1. 球体位置的计算:此阶段更新当前时间的空间中的球体位置。 这里我们在示例中第一次使用变换反馈模式。
  2. 标量场生成:此阶段将模型空间分解为samples_per_axis3样本,并计算空间中每个点的标量字段值。
  3. Marching Cubes细胞分裂:此阶段将标量场分解为立方细胞并确定每个细胞的类型。 Cell类型标识isosurface是否通过单元格,如果是,它如何用三角形近似。
  4. Marching Cubes三角形生成和渲染:此阶段为每个单元格生成适合于单元格类型的三角形。

我们在前三个阶段使用变换反馈模式。 变换反馈模式允许我们捕获由顶点着色器生成的输出并将输出记录到缓冲区对象中。 它的用法在球体位置的计算中有详细解释。 在渲染循环中为每个帧执行所有四个阶段。 另外,每个阶段都需要一些初始化操作,这些操作在控制程序的setupGraphics()函数中执行。

Calculation of sphere positions

因为整个场景取决于当前渲染时间的球体位置,所以我们需要更新位置作为第一步。 我们可以在顶点着色器中保留描述球体及其运动方程的信息,因为对于进一步的计算,我们只需要当前的球体位置。 这使我们能够通过最小化CPU和GPU内存之间传输的数据量来提高整体性能。 控制程序中此阶段的顶点着色器(spheres_updater_vert_shader)所需的唯一输入信息是时间值,我们在顶点着色器中将其声明为uniform:

"/** Current time moment. */\n"
"uniform float time;\n"
复制代码

要将值从控制程序传递到着色器,我们将在编译和链接着色器程序后检索uniform的位置。 对于这个阶段,我们只需要在setupGraphics()函数中检索时间uniform的位置:

/* Get input uniform location. */
spheres_updater_uniform_time_id = GL_CHECK(glGetUniformLocation(spheres_updater_program_id, spheres_updater_uniform_time_name));
复制代码

在渲染循环期间,我们使用以下命令将当前时间值传递给顶点着色器:

/* Specify input arguments to vertex shader. */
GL_CHECK(glUniform1f(spheres_updater_uniform_time_id, model_time));
复制代码

现在让我们看一下顶点着色器在此阶段生成的输出值:

"/** Calculated sphere positions. */\n"
"out vec4 sphere_position;\n"
复制代码

正如在其源代码中看到的,着色器仅输出四个浮点值(打包为vec4s):三个球体坐标和一个球体重量。 为了计算n_spheres球体的参数,我们指示OpenGL ES运行顶点着色器n_spheres次。 控制程序通过在渲染循环期间发出命令来完成此操作:

/* Run sphere positions calculation. */
GL_CHECK(glDrawArrays(GL_POINTS, 0, n_spheres));
复制代码

由于球体位置的计算彼此独立,OpenGL ES可能(并且通常会)在多个顶点处理器上同时运行多个球体的计算。

捕获顶点着色器生成的值需要我们在控制程序中指定在链接程序之前我们感兴趣的varying(即着色器输出变量):

/* Generate buffer object id. Define required storage space sufficient to hold sphere positions data. */
GL_CHECK(glGenBuffers(1, &spheres_updater_sphere_positions_buffer_object_id));
GL_CHECK(glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, spheres_updater_sphere_positions_buffer_object_id));
GL_CHECK(glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, n_spheres * n_sphere_position_components * sizeof(GLfloat), NULL, GL_STATIC_DRAW));
GL_CHECK(glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0));
复制代码

接下来,我们分配一个转换反馈对象(TFO),并通过将适当的缓冲区对象(在本例中为spheres_updater_sphere_positions_buffer_object_id)绑定到GL_TRANSFORM_FEEDBACK_BUFFER目标来配置它:

/* Generate and bind transform feedback object. */
GL_CHECK(glGenTransformFeedbacks(1, &spheres_updater_transform_feedback_object_id));
GL_CHECK(glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, spheres_updater_transform_feedback_object_id));
/* Bind buffers to store calculated sphere positions. */
GL_CHECK(glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, spheres_updater_sphere_positions_buffer_object_id));
GL_CHECK(glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, 0));
复制代码

在这些行中,我们告诉OpenGL ES在激活变换反馈模式时将输出值存储在缓冲区中。 通过将其绑定到GL_TRANSFORM_FEEDBACK目标来激活此TFO。 这正是我们在渲染过程中的第一步:

/* Bind buffers to store calculated sphere position values. */
GL_CHECK(glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, spheres_updater_transform_feedback_object_id));
复制代码

之后,我们需要准备变换反馈模式以用于我们的目的:我们不需要在此阶段渲染基元并且我们想要捕获数据。 第一个命令通过丢弃顶点着色器生成的任何基元来缩短OpenGL管道:

/* Shorten GL pipeline: we will use vertex shader only. */
GL_CHECK(glEnable(GL_RASTERIZER_DISCARD));
复制代码

第二个命令激活转换反馈模式本身,指示OpenGL ES捕获指定的基元并将其存储到缓冲区对象中:

/* Activate transform feedback mode. */
GL_CHECK(glBeginTransformFeedback(GL_POINTS));
复制代码

完成后,我们会发出配对命令来停用转换反馈模式:

GL_CHECK(glEndTransformFeedback());
复制代码

另一个是恢复OpenGL ES管道:

GL_CHECK(glDisable(GL_RASTERIZER_DISCARD));
复制代码

总而言之,以下是此阶段的完整列表:

    /* 1. Calculate sphere positions stage.
     *
     * At this stage we calculate new sphere positions in space
     * according to current time moment.
     */
    /* [Stage 1 Bind buffers to store calculated sphere position values] */
    /* Bind buffers to store calculated sphere position values. */
    GL_CHECK(glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, spheres_updater_transform_feedback_object_id));
    /* [Stage 1 Bind buffers to store calculated sphere position values] */
    /* [Stage 1 Enable GL_RASTERIZER_DISCARD] */
    /* Shorten GL pipeline: we will use vertex shader only. */
    GL_CHECK(glEnable(GL_RASTERIZER_DISCARD));
    /* [Stage 1 Enable GL_RASTERIZER_DISCARD] */
    {
        /* Select program for sphere positions generation stage. */
        GL_CHECK(glUseProgram(spheres_updater_program_id));
        /* [Stage 1 Specify input arguments to vertex shader] */
        /* Specify input arguments to vertex shader. */
        GL_CHECK(glUniform1f(spheres_updater_uniform_time_id, model_time));
        /* [Stage 1 Specify input arguments to vertex shader] */
        /* [Stage 1 Activate transform feedback mode] */
        /* Activate transform feedback mode. */
        GL_CHECK(glBeginTransformFeedback(GL_POINTS));
        /* [Stage 1 Activate transform feedback mode] */
        {
            /* [Stage 1 Execute n_spheres times vertex shader] */
            /* Run sphere positions calculation. */
            GL_CHECK(glDrawArrays(GL_POINTS, 0, n_spheres));
            /* [Stage 1 Execute n_spheres times vertex shader] */
        }
        /* [Stage 1 Deactivate transform feedback mode] */
        GL_CHECK(glEndTransformFeedback());
        /* [Stage 1 Deactivate transform feedback mode] */
    }
    /* [Stage 1 Disable GL_RASTERIZER_DISCARD] */
    GL_CHECK(glDisable(GL_RASTERIZER_DISCARD));
    /* [Stage 1 Disable GL_RASTERIZER_DISCARD] */
    /* Unbind buffers used at this stage. */
    GL_CHECK(glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, 0));
复制代码

Scalar field generation

这个阶段与第一阶段之间的控制程序差异很小:

    /* 2. Scalar field generation stage.
     *
     * At this stage we calculate scalar field and store it in buffer
     * and later copy from buffer to texture.
     */
    /* Bind sphere positions data buffer to GL_UNIFORM_BUFFER. */
    GL_CHECK(glBindBuffer(GL_UNIFORM_BUFFER, spheres_updater_sphere_positions_buffer_object_id));
    /* Bind buffer object to store calculated scalar field values. */
    GL_CHECK(glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, scalar_field_transform_feedback_object_id));
    /* Shorten GL pipeline: we will use vertex shader only. */
    GL_CHECK(glEnable(GL_RASTERIZER_DISCARD));
    {
        /* Select program for scalar field generation stage. */
        GL_CHECK(glUseProgram(scalar_field_program_id));
        /* Activate transform feedback mode. */
        GL_CHECK(glBeginTransformFeedback(GL_POINTS));
        {
            /* Run scalar field calculation for all vertices in space. */
            GL_CHECK(glDrawArrays(GL_POINTS, 0, samples_in_3d_space));
        }
        GL_CHECK(glEndTransformFeedback());
    }
    GL_CHECK(glDisable(GL_RASTERIZER_DISCARD));
    /* Unbind buffers used at this stage. */
    GL_CHECK(glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, 0));
复制代码

虽然我们使用不同的程序,缓冲区和变换反馈对象,但控制程序中的一般操作流程与第一阶段相同。 这是因为在这个阶段我们再次使用变换反馈机制,只需要为顶点着色器指定输入和输出变量,这将完成实际的工作。 唯一不同的命令是第一个命令,它将缓冲区对象绑定到GL_UNIFORM_BUFFER目标。 通过发出此命令,我们将在此阶段的顶点着色器可用的第一个阶段中生成并收集在缓冲区对象中的球体位置。 不幸的是,由于OpenGL ES的限制,只能使用这种机制将相对少量的数据传递给着色器。 值得注意的是,我们指示OpenGL ES运行着色器程序samples_in_3d_space次,这将在下面解释。

主要区别在于阶段的顶点着色器,我们将在查看阶段的想法后进行检查。

如所知,在距离物体的距离D处的空间中具有质量M的物体产生的重力场值直接与该距离的质量和平方相关,即:

为了使该公式具有物理意义,我们还需要引入一个常数系数。 由多个对象生成的重力场值被计算为所有场值的总和,即:

在这个例子中,我们将使用术语权重而不是质量,虽然不像质量那样正确但通常更容易理解。

在数学上定义处理表面很困难,因此在许多其他科学中我们通过对空间进行采样来简化任务。 我们对每个轴使用tesselation_level = 32个样本,这为我们提供了32 * 32 * 32或32768个空间点,应该计算标量字段值。 可以尝试增加此值以提高生成图像的质量。

可能会注意到标量字段值仅取决于所有球体的位置和质量(权重)。 这使我们能够在不同的着色器程序中同时计算空间中几个点的字段值。

作为输入数据,顶点着色器需要每个轴上的样本数和存储在缓冲区中的球体位置:

"/* Uniforms: */\n"
"/** Amount of samples taken for each axis of a scalar field; */\n"
"uniform int samples_per_axis;\n"
"\n"
"/** Uniform block encapsulating sphere locations. */\n"
"uniform spheres_uniform_block\n"
"{\n"
"    vec4 input_spheres[N_SPHERES];\n"
"};\n"
复制代码

如所见,通过使用预处理器定义,此处存储在缓冲区中的球体数量是硬编码的。 这是着色器的限制:他们需要知道传入的统一缓冲区中的确切项目数。 如果需要使着色器更加灵活,可以在缓冲区中保留更多空间,并根据某些标记忽略项目或通过验证某些值(例如零权重值)。 另一种方法是使用纹理作为输入数据数组,我们将在标量字段生成中使用它。

在此阶段运行的每个顶点着色器通过解码着色器正在运行的空间中的点来开始执行:

"/** Decode coordinates in space from vertex number.\n"
" *  Assume 3D space of samples_per_axis length for each axis and following encoding:\n"
" *  encoded_position = x + y * samples_per_axis + z * samples_per_axis * samples_per_axis\n"
" *\n"
" *  @param  vertex_index Encoded vertex position\n"
" *  @return              Coordinates of a vertex in space ranged [0 .. samples_per_axis-1]\n"
" */\n"
"ivec3 decode_space_position(in int vertex_index)\n"
"{\n"
"    int   encoded_position = vertex_index;\n"
"    ivec3 space_position;\n"
"\n"
"    /* Calculate coordinates from vertex number. */\n"
"    space_position.x = encoded_position % samples_per_axis;\n"
"    encoded_position = encoded_position / samples_per_axis;\n"
"\n"
"    space_position.y = encoded_position % samples_per_axis;\n"
"    encoded_position = encoded_position / samples_per_axis;\n"
"\n"
"    space_position.z = encoded_position;\n"
"\n"
"    return space_position;\n"
"}\n"
复制代码

这是一种非常广泛使用的技术,其中内置的OpenGL ES着色语言变量gl_VertexID用作隐式参数。 每个执行的顶点着色器都有自己唯一的值,通过gl_VertexID传递,范围为0到传递给glDrawArrays调用的count参数值。 基于gl_VertexID值,着色器可以适当地控制其执行。 在此函数中,着色器将gl_VertexID的值拆分为空间中点的x,y,z坐标,着色器应为其计算标量字段值。

要使标量字段值独立于tesselation_level,着色器将空间坐标中的点标准化为范围[0..1]:

"vec3 normalize_space_position_coordinates(in ivec3 space_position)\n"
"{\n"
"    vec3 normalized_space_position = vec3(space_position) / float(samples_per_axis - 1);\n"
"\n"
"    return normalized_space_position;\n"
"}\n"
复制代码

然后它计算标量字段值:

"float calculate_scalar_field_value(in vec3 position)\n"
"{\n"
"    float field_value = 0.0f;\n"
"\n"
"    /* Field value in given space position influenced by all spheres. */\n"
"    for (int i = 0; i < N_SPHERES; i++)\n"
"    {\n"
"        vec3  sphere_position         = input_spheres[i].xyz;\n"
"        float vertex_sphere_distance  = length(distance(sphere_position, position));\n"
"\n"
"        /* Field value is a sum of all spheres fields in a given space position.\n"
"         * Sphere weight (or charge) is stored in w-coordinate.\n"
"         */\n"
"        field_value += input_spheres[i].w / pow(max(EPSILON, vertex_sphere_distance), 2.0);\n"
"    }\n"
"\n"
"    return field_value;\n"
"}\n"
复制代码

最后,着色器使用着色器中声明的唯一输出变量输出计算的标量字段值:

"/* Output data: */\n"
"/** Calculated scalar field value. */\n"
"out float scalar_field_value;\n"
复制代码

在此阶段结束时,我们计算了标量字段值,并将其存储在空间中每个点的缓冲区对象中。 不幸的是,大缓冲区对象不能用作输入数据。 这就是为什么在这个阶段结束时我们需要再执行一次操作:我们应用另一种广泛的OpenGL ES技术并将数据从缓冲区对象传输到纹理中。 这在控制程序中执行:

    GL_CHECK(glActiveTexture(GL_TEXTURE1));
    GL_CHECK(glBindBuffer   (GL_PIXEL_UNPACK_BUFFER, scalar_field_buffer_object_id));
    GL_CHECK(glTexSubImage3D(GL_TEXTURE_3D,    /* Use texture bound to GL_TEXTURE_3D                                     */
                             0,                /* Base image level                                                       */
                             0,                /* From the texture origin                                                */
                             0,                /* From the texture origin                                                */
                             0,                /* From the texture origin                                                */
                             samples_per_axis, /* Texture have same width as scalar field in buffer                      */
                             samples_per_axis, /* Texture have same height as scalar field in buffer                     */
                             samples_per_axis, /* Texture have same depth as scalar field in buffer                      */
                             GL_RED,           /* Scalar field gathered in buffer has only one component                 */
                             GL_FLOAT,         /* Scalar field gathered in buffer is of float type                       */
                             NULL              /* Scalar field gathered in buffer bound to GL_PIXEL_UNPACK_BUFFER target */
                            ));
复制代码

上面的第一个命令激活纹理单元1,第二个命令指定源数据缓冲区,第三个命令执行从缓冲区对象到当前绑定到特定绑定点的GL_TEXTURE_3D目标的纹理对象的实际数据传输。 我们使用3D纹理作为在下一个着色器阶段中作为3D数组访问数据的便捷方式。

绑定到纹理单元1的GL_TEXTURE_3D目标点的纹理被创建并初始化为在setupGraphics()函数中包含适当数量的标量字段值:

    /* Generate texture object to hold scalar field data. */
    GL_CHECK(glGenTextures(1, &scalar_field_texture_object_id));
    /* Scalar field uses GL_TEXTURE_3D target of texture unit 1. */
    GL_CHECK(glActiveTexture(GL_TEXTURE1));
    GL_CHECK(glBindTexture(GL_TEXTURE_3D, scalar_field_texture_object_id));
    /* Prepare texture storage for scalar field values. */
    GL_CHECK(glTexStorage3D(GL_TEXTURE_3D, 1, GL_R32F, samples_per_axis, samples_per_axis, samples_per_axis));
复制代码

Marching Cubes cell-splitting

最后两个阶段实际上实现了Marching Cubes算法。该算法是创建3D标量场等值面的多边形表面表示的方法之一。该算法将标量场分解为立方体单元。每个单元格角都有一个标量字段值。通过在单元角中的等值面水平和标量场值之间执行简单的比较操作,可以确定角是否在等值面下方。如果单元格的一个角落在等值面下方,而同一个单元格边缘上的另一个角落位于等值面上,则显然等值面与边缘交叉。此外,由于场连续性和平滑性,在同一单元格中将存在其他交叉边缘。使用边缘交叉点,可以生成最接近等值面的多边形。现在让我们来看看另一方面的算法。每个单元格角的状态可以表示为布尔值:角落在等值面上方(适当的位设置为1)或不是(适当的位设置为0)。很明显,在一个单元中有八个角我们有28个,即256个角,它们位于等值面的上方或下方。如果曲面直接穿过一个角,我们假设角落在等值面下方,以保持状态二进制。换句话说,等值面可以以256种方式之一通过单元。因此,对于256个单元类型,我们可以描述要生成的一组三角形,以使单元接近等值面。

在这个阶段,我们通过细胞划分空间并识别细胞类型。 该空间已经被前一阶段的样本划分(标量场生成),并且着色器在单元角中采用八个样本以形成单元。 使用等值面水平值,着色器根据上述算法识别每个单元的单元类型。

我们将在setupGraphics()函数中注意纹理创建:

/* Generate a texture object to hold cell type data. (We will explain why the texture later). */
GL_CHECK(glGenTextures(1, &marching_cubes_cells_types_texture_object_id));
/* Marching cubes cell type data uses GL_TEXTURE_3D target of texture unit 2. */
GL_CHECK(glActiveTexture(GL_TEXTURE2));
GL_CHECK(glBindTexture(GL_TEXTURE_3D, marching_cubes_cells_types_texture_object_id));
/* Prepare texture storage for marching cube cell type data. */
GL_CHECK(glTexStorage3D(GL_TEXTURE_3D, 1, GL_R32I, cells_per_axis, cells_per_axis, cells_per_axis));
复制代码

请注意,这里我们使用纹理单元2并创建单分量整数的3D纹理(GL_R32I),它将为每个31 * 31 * 31单元格保存类型。 每个轴上的单元格数(cells_per_axis)比每个轴上的样本数少一个(samples_per_axis)。 更容易解释为什么使用一个例子:让我们想象我们将黄油砖切成两半(细胞)。 与切割平面平行,我们有两个单元格用于两个单元格(一个由两个半部共享)。 因此,单元的数量比侧面的数量少一个。 控制程序指示GPU为每个单元执行顶点着色器:

/* Run Marching Cubes algorithm cell splitting stage for all cells. */
GL_CHECK(glDrawArrays(GL_POINTS, 0, cells_in_3d_space));
复制代码

在这个阶段的顶点着色器中,我们首先以类似于我们在前一阶段中所做的方式解码由该着色器实例处理的单元的原点的空间位置。 唯一的区别是这里作为除数,我们使用每个轴上的单元格数而不是样本数:

"ivec3 decode_space_position(in int cell_index)\n"
"{\n"
"    ivec3 space_position;\n"
"    int   encoded_position = cell_index;\n"
"\n"
"    /* Calculate coordinates from encoded position */\n"
"    space_position.x       = encoded_position % cells_per_axis;\n"
"    encoded_position       = encoded_position / cells_per_axis;\n"
"\n"
"    space_position.y       = encoded_position % cells_per_axis;\n"
"    encoded_position       = encoded_position / cells_per_axis;\n"
"\n"
"    space_position.z       = encoded_position;\n"
"\n"
"    return space_position;\n"
"}\n"
复制代码

然后,着色器通过从上一阶段创建的纹理中提取标量值(标量字段生成),将当前单元格角的标量字段值收集到数组中:

"    /* Find scalar field values in cell corners. */\n"
"    for (int i = 0; i < corners_in_cell; i++)\n"
"    {\n"
"        /* Calculate cell corner processed at this iteration. */\n"
"        ivec3 cell_corner = space_position + cell_corners_offsets[i];\n"
"\n"
"        /* Calculate cell corner's actual position ([0.0 .. 1.0] range.) */\n"
"        vec3 normalized_cell_corner  = vec3(cell_corner) / scalar_field_normalizers;\n"
"\n"
"        /* Get scalar field value in cell corner from scalar field texture. */\n"
"        scalar_field_in_cell_corners[i] = textureLod(scalar_field, normalized_cell_corner, 0.0).r;\n"
"    }\n"
复制代码

在单元格角中具有标量字段的值和等值面水平值,我们可以确定单元格类型:

"int get_cell_type_index(in float cell_corner_field_value[8], in float isolevel)\n"
"{\n"
"    int cell_type_index = 0;\n"
"\n"
"    /* Iterate through all cell corners. */\n"
"    for (int i = 0; i < 8; i++)\n"
"    {\n"
"        /* If corner is inside isosurface then set bit in cell type index index. */\n"
"        if (cell_corner_field_value[i] < isolevel)\n"
"        {\n"
"            /* Set appropriate corner bit in cell type index. */\n"
"            cell_type_index |= (1<<i);\n"
"        }\n"
"    }\n"
"\n"
"    return cell_type_index;\n"
"}\n"
复制代码

该阶段返回单元格类型并存储在适当的缓冲区中,以便在下一阶段进行进一步处理。

Marching Cubes triangle generation and rendering

Marching Cubes算法近似于通过单元格的表面,最多有五个三角形。 对于单元格类型0和255,它不生成三角形,因为所有单元格角都在等值面的下方或上方。 使用由单元格类型(tri_table)索引的查找表是获取为单元格生成的三角形列表的有效方法。

最后,我们拥有生成一组三角形并渲染它们所需的所有信息。 对于该阶段,控制程序不激活变换反馈模式,因为不需要存储任何生成的三角形顶点。 将生成的三角形顶点传递给片段着色器以进行剔除或渲染更有用:

/* Run triangle generating and rendering program. */
GL_CHECK(glDrawArrays(GL_TRIANGLES, 0, cells_in_3d_space * triangles_per_cell * vertices_per_triangle));
复制代码

如果查看常量值,可能会注意到,我们在此指示OpenGL ES为每个单元格运行着色器十五次。 这个乘数来自Marching Cubes算法:一个穿过一个单元格并且每个边缘相交不超过一次的等值面可以用最多五个三角形近似,我们使用三个顶点来定义一个三角形。 另请注意,算法不使用单元格角来构建三角形,而是使用单元格的“中间”点。 在这种情况下,“中间”点是单元边缘上的一个点,该边缘实际上与等值面相交。 这提供了更好的图像质量。

每个细胞可以根据其细胞类型产生最多五个三角形,但是大多数细胞类型产生的三角形明显更少。 例如,查看tri_table,它用于构建单元格类型之外的三角形。 我们在这里介绍256个中的前四种细胞类型:

const GLint tri_table[mc_cells_types_count * mc_vertices_per_cell] =
{
  -1, -1, -1,    -1, -1, -1,    -1, -1, -1,    -1, -1, -1,    -1, -1, -1,
   0,  8,  3,    -1, -1, -1,    -1, -1, -1,    -1, -1, -1,    -1, -1, -1,
   0,  1,  9,    -1, -1, -1,    -1, -1, -1,    -1, -1, -1,    -1, -1, -1,
   1,  8,  3,     9,  8,  1,    -1, -1, -1,    -1, -1, -1,    -1, -1, -1,
复制代码

表中的每一行代表一种单元格类型。 每个单元格类型最多包含五个三角形。 每个三角形由三个连续的顶点定义。 表中的每个数字是一个单元格边缘的数量,其中“中间”点被视为三角形顶点。 如果单元格类型提供的三角形少于五个,则额外的边数用值-1填充。 例如,单元格类型0(第一个数据行)没有定义任何三角形:它的所有“中间”点都设置为-1。 单元类型1(第二数据线)定义一个三角形,其由单元的边缘0,8和3的“中间”点组成。 单元格类型2再次定义单个三角形,而单元格类型3定义两个三角形。 细心的读者可能会注意到表格中单元格边缘的数量小于12.这是因为单元格立方体只有12个边缘,在表格中编号从0到11(包括0和11)。

每个顶点着色器实例仅处理三角形的一个顶点。 首先,基于gl_VertexID,它以类似于Scalar字段生成的方式解码三角形顶点数和它处理的单元格的位置:

"ivec4 decode_cell_position(in int encoded_position_argument)\n"
"{\n"
"    ivec4 cell_position;\n"
"    int   encoded_position = encoded_position_argument;\n"
"\n"
"    /* Decode combined triangle and vertex number. */\n"
"    cell_position.w  = encoded_position % mc_vertices_per_cell;\n"
"    encoded_position = encoded_position / mc_vertices_per_cell;\n"
"\n"
"    /* Decode coordinates from encoded position. */\n"
"    cell_position.x  = encoded_position % CELLS_PER_AXIS;\n"
"    encoded_position = encoded_position / CELLS_PER_AXIS;\n"
"\n"
"    cell_position.y  = encoded_position % CELLS_PER_AXIS;\n"
"    encoded_position = encoded_position / CELLS_PER_AXIS;\n"
"\n"
"    cell_position.z  = encoded_position;\n"
"\n"
"    return cell_position;\n"
"}\n"
复制代码

这里,解码是一个更长的步骤:除了单元坐标,它还解码组合的三角形和顶点数。 我们将此信息存储在main()函数的局部变量中以供进一步处理:

"    /* Split gl_vertexID into cell position and vertex number processed by this shader instance. */\n"
"    ivec4 cell_position_and_vertex_no = decode_cell_position(gl_VertexID);\n"
"    ivec3 cell_position               = cell_position_and_vertex_no.xyz;\n"
"    int   triangle_and_vertex_number  = cell_position_and_vertex_no.w;\n"
复制代码

获得坐标后,着色器可以检索单元格类型和单元格边数:

"        /* Calculate normalized coordinates in space of cell origin corner. */\n"
"        vec3 cell_origin_corner    = vec3(cell_position) / float(samples_per_axis - 1);\n"
复制代码

具有单元原点的坐标和边数(我们当前正在计算的“中间”点),我们可以找到该特定边开始和结束的单元角顶点:

"        /* Calculate start and end edge coordinates. */\n"
"        vec3 start_corner          = get_edge_coordinates(cell_origin_corner, edge_number, true);\n"
"        vec3 end_corner            = get_edge_coordinates(cell_origin_corner, edge_number, false);\n"
复制代码

现在,在边的开始和结束顶点以及等值面水平值中具有标量场的值,我们可以计算等值面与边缘交叉的确切位置:

"        /* Calculate share of start point of an edge. */\n"
"        float start_vertex_portion = get_start_corner_portion(start_corner, end_corner, iso_level);\n"
"\n"
"        /* Calculate ''middle'' edge vertex. This vertex is moved closer to start or end vertices of the edge. */\n"
"        vec3 edge_middle_vertex    = mix(end_corner, start_corner, start_vertex_portion);\n"
复制代码

对于照明,我们计算等值面的法向量:

"        /* Calculate normal to surface in the ''middle'' vertex. */\n"
"        vec3 vertex_normal         = calc_phong_normal(start_vertex_portion, start_corner, end_corner);\n"
复制代码

边缘“中间”点处表面的法向矢量被计算为开始和结束边缘顶点中的法向矢量的混合。 使用标量场的偏导数计算开始和结束边缘顶点中的法向矢量,其使用与顶点标量场值相邻的标量场值计算。 假设您已经熟悉这种照明技术,因此我们不会专注于详细描述效果。

如果结果着色器处理了一个不应该生成的三角形的顶点(边数为-1),那么它会通过指定与虚拟三角形的所有其他顶点匹配的坐标来处理顶点:

"        /* This cell type generates fewer triangles, and this particular one should be discarded. */\n"
"        gl_Position                = vec4(0);                                    /* Discard vertex by setting its coordinate in infinity. */\n"
"        phong_vertex_position      = gl_Position;\n"
"        phong_vertex_normal_vector = vec3(0);\n"
"        phong_vertex_color         = vec3(0);\n"
复制代码

对于此阶段使用几何着色器会更为理想,它(与顶点着色器不同)可以根据需要生成任意数量的顶点,或者根本不生成任何顶点。 但是,几何着色器是一个OpenGL功能,并不存在于核心OpenGL ES中,因此我们需要在顶点着色器中完成这项工作,通过将所有顶点坐标指定为相同的值来丢弃不适当的三角形。

Terrain Rendering with Geometry Clipmaps

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值