跳到主要内容

定义几何

几何数据是指用于定义图形对象形状的信息,这些数据主要包括顶点、图元及其相关的属性。几何数据是OpenGL渲染管线的输入,用于生成最终的图像。以下是几何数据的主要组成部分:

  • 顶点数据:顶点是几何数据的基本单位,用于定义图形的形状。一个顶点通常包含以下信息:

    • 位置 定义顶点在空间中的位置
    • 法向量 表示顶点的方向,用于光照计算
    • 纹理坐标 用于映射纹理到几何对象
    • 颜色 每个顶点可以携带颜色信息(r,g,b,a),用于直接着色
    • 其他属性 开发者可以定义额外的顶点属性,用于一些特别的场景,比如CAE仿真结果场数据
  • 图元:由顶点组成的基础几何形状,是OpenGL渲染的基础单位。我们的引擎中支持以下几种图元类型:

    • 点(Points) 单个顶点绘制为一个点
    • 线(Lines) 由两个顶点组成的线段。类型包括: GL_LINES:独立的线段。 GL_LINE_STRIP:连续的线段。 GL_LINE_LOOP:连续线段,最后一个顶点与第一个顶点相连。
    • 三角形(Triangles) 由三个顶点组成的三角形,是OpenGL中最常用的图元。类型包括: GL_TRIANGLES:独立的三角形。 GL_TRIANGLE_STRIP:连续的三角形条带。 GL_TRIANGLE_FAN:以一个中心顶点为基础的三角形扇形。
    • 其他图元 GL_QUADS(四边形,已废弃):由四个顶点组成的四边形。 GL_POLYGON(多边形,已废弃):由多个顶点组成的多边形。
  • 顶点索引:用于指定图元中顶点的连接信息。通过索引数组可以重用顶点数据,减少冗余。索引数据存储在索引缓冲对象(Index Buffer Object)中。

接下来,我们可以从零构建一个简单的3D空间渲染场景scene,该场景的几何是一个三角形。

从这个例子中,您可以了解到以下内容:

  • 定义几何:详细介绍了如何利用Illustrator Core库的接口定义绘制场景中的几何数据
  • 使用几何数据:OpenGL的着色器如何使用几何数据,使其呈现在2D屏幕中
  • 构造渲染场景:从零开始构建渲染上下文所需的所有对象

定义几何

在下面的代码段中定义了3D空间中的一个三角形,它的三个顶点的位置坐标分别是:(-0.5, -0.5, 0.0),(0.5, -0.5, 0.0),(0.0, 0.5, 0.0)。

我们利用_geometry接口定义这个几何,包括它的顶点位置、顶点索引。这里需要使用_vertices接口,来声明一个存储顶点数据的顶点缓冲对象,使用_triangles接口,声明了三角形图元的索引缓冲对象。

auto geometry = _geometry(
{
// 所有顶点的位置被保存在一个顶点缓冲中。
{"positions", _vertices({-0.5, -0.5, 0.0, 0.5, -0.5, 0.0, 0.0, 0.5, 0.0})},
{"normals", normals},
{"texCoords", texCoords},
},
//当具备顶点信息后,使用顶点索引数组就可以定义三角形图元。索引数组保存在一个索引缓冲中。
_triangles(3)
);

顶点缓冲对象

在上面的代码段中我们可以看出,顶点缓冲的定义需要使用接口_vertices。一般来说,顶点信息不仅仅包含位置信息,还需要有法向 、纹理。

auto positions = _vertices({...});
auto normals = _vertices({...});
auto texCoord = _vertices({...});
auto geometry = _geometry(
{
// 所有的图元顶点的位置、法向量、纹理坐标,它们分别保存在各自的顶点缓冲中。
{"positions", positions},
{"normals", normals},
{"texCoords", texCoords},
},
triangles,
boundingBox
);
信息

顶点缓冲对象会作为OpenGL的Shader程序的输入,在Shader程序中,您将需要指定顶点缓冲对象的数组维度,比如3D空间中位置是三维的,而2D空间中位置是二维的。法向量是三维的,而纹理坐标是二维的。

索引缓冲对象

两个顶点连接形成一个线段,三个顶点连接形成一个三角形面。当空间中存在大量顶点时,必须通过索引缓冲对象定义这些顶点的连接,不同的连接,最终会成为不同形状的Shell。

比如一个三角形面的顶点索引是数组{0,1,2},另一个三角形面的顶点索引是数组{0,2,3}, 如果这两个三角形面属于同一个几何,那么在该几何的索引缓冲中,就有会这样一个数组片段:{...,0,1,2,0,2,3,...}。我们需要将该实体几何的所有图元的索引数组都写入该IBO中,最终被传输到着色器中。

Illustrator core中提供了多种图元接口,他们本质上是声明了构成某种图元的顶点索引数组对象:

  • _points,创建多个顶点POINTS。

  • _lines,创建多个双顶点线段LINES,每两个顶点指定一条单独线段。

  • _lines_strip,创建不闭合的连续线段LINE_STRIP,每个顶点之后的顶点指定的是线条延伸到的下一个点。

  • _lines_loop,创建闭合的连续线段LINE_LOOP,与LINE_STRIP类似,只不过最后一条线段是由指定的最后一个和第一个顶点连接而成。

  • _triangles,创建多个三角形面TRIANGLES,每三个顶点指定一个三角形面。

  • _triangles_fan,创建扇型连续三角形面TRIANGLE_FAN。指定的第一个顶点充当原点,后继的每个顶点与它前面的一个顶点以及原点,一起构成下一个三角形面。

  • _triangles_strip,创建线型连续三角形面TRIANGLE_STRIP。指定开始的三个顶点之后,后继的每个顶点与它前面两个顶点一起构成下一个三角形面。

基础图元

在这个例子中,我们使用了定义TRIANGLES图元的接口_triangles(3),表示顶点缓冲中的每3个连续的顶点构成了一个三角形。

图元的索引缓冲都可以用两种不同的类型定义,即:ElementIndices类型与ArrayIndices类型。下面以定义三角形面的索引缓冲为例:

// ElementIndices类型的索引缓冲
auto triangles1 = _triangles({...});
// ArrayIndices类型的索引缓冲
auto triangles2 = _triangles(n);
  • ElementIndices类型:_triangles({...})中需要直接写出所有图元的索引数组缓冲,比如_triangles({0,1,2,0,2,3}),它代表该几何有两个三角形面,它们的顶点索引数组分别是{0,1,2}{0,2,3}

  • ArrayIndices类型:_triangles(n)中只需要指定构成所有三角形面的顶点个数,就可以确定该几何所有的三角形图元各自的索引数组了。比如_triangles(6)代表该几何有两个三角形面,分别是{0,1,2}{3,4,5},或者比如_triangles_strip(6)代表该几何有4个三角形面,分别是{0,1,2}{1,2,3}{2,3,4}{3,4,5}

使用几何数据

几何数据通过顶点着色器进入渲染管线。顶点着色器会处理每个顶点的数据,比如位置、法线、纹理坐标等,处理过程就是渲染管线的核心内容,引擎已经将这些过程全部封装起来,开发者不需要关心这部分内容。

不管是几何,还是材质,都会在着色器程序中得到进一步处理,着色器程序是可以运行在GPU上的一段比较小的代码,描述了如何处理顶点数据和材质属性数据,并计算出屏幕中每个像素片段的颜色分布。着色器是OpenGL的基础部分,允许开发者实现各种复杂的图形效果。

这里我们使用了顶点着色器:vertex shader和面元着色器:fragment shader。前者承担了处理和变换顶点信息的功能,比如位置、颜色、贴图坐标等,计算出屏幕中每个像素的位置和颜色。而后者承担了更加复杂的计算,比如光照、阴影、反射等,得到了屏幕中每个像素的最终颜色,让图片有了更加真实的图形效果。

在下面的代码段中,我们可以看到顶点的位置缓冲对象positions出现在了名为vertexSource的shader程序中,并被指定为三维数组。

std::string vertexSource = R"(
#version 330 core

in vec3 positions;

void main()
{
gl_Position = vec4(positions, 1.0);
}
)";

std::string fragmentSource = R"(
#version 330 core

out vec4 fragColor;

void main()
{
fragColor = vec4(0.3, 0.5, 0.7, 1.0);
}
)";

auto material = _material(
_program(
vertexSource, fragmentSource
)
);

构造渲染场景

渲染场景用下面这个树结构来表示:

声明绘制单元

在上面已经声明好了geometrymaterial,二者构成了一个绘制单元。

auto primitive = _primitive(geometry, material);

声明渲染节点

将上面的绘制单元作为网格对象,声明一个渲染节点。

auto node = _node({ primitive });

构造场景

您会发现,我们在渲染通道pass中不仅包含了背景,还包含了上面构造好的渲染节点。

auto background = _simpleBackground(_ivec2(pixelSize), _clr4(0.9, 0.8, 0.7, 1.0));
auto pass = _pass(node, background);
auto scene = _scene({ pass });

程序绘制出来的结果如下:

triangle