本章示例展示了如何使用渲染器绘制单个红色三角形。它整合了本章介绍的多个概念和代码片段,形成了一个可运行、可调试的示例,更重要的是,你可以在此基础上构建自己的代码。项目引用了 Scene 和 Renderer,无需直接调用OpenGL接口——所有渲染操作均通过公开的渲染器类型完成。
GLFWwindow* window;
SceneState* sceneState;
ClearState* clearState;
DrawState* drawState;
实现渲染所需的核心组件包括:一个用于渲染的窗口,以及用于发起绘制调用的各类状态对象。当然,绘制状态(draw state)包含着色器程序、顶点数组和渲染状态,但这些并未直接作为示例类的成员。
大部分工作在构造函数中完成。构造函数会创建窗口,并注册窗口大小调整和渲染帧的事件回调。由于窗口在底层会创建OpenGL上下文,因此必须先创建窗口,再初始化其他渲染器类型。
首先创建默认的场景状态(scene state)和清除状态(clear state)。回顾前文可知,场景状态包含用于设置自动 uniform 的状态,清除状态包含用于清除帧缓冲区的状态。本示例中,默认状态即可满足需求:场景状态中,相机默认位置为(0, -1, 0),朝向原点,上向量为(0, 0, 1),即相机正对 xz 平面(x轴向右,z轴向上)。
接下来创建着色器程序,其顶点着色器和片段着色器源码硬编码在字符串中。若着色器逻辑更复杂,可按照前面章节所述,将源码存储在单独文件中并作为嵌入资源加载。顶点着色器通过自动 uniform 变量ce_modelViewPerspectiveMatrix对输入位置进行变换,因此示例无需显式设置变换矩阵;片段着色器基于u_color uniform 输出纯色(此处设为红色)。
随后创建顶点数组:先定义一个位于xz平面的等腰直角三角形(直角边长度为1)的网格(Mesh),再调用context.createVertexArray生成顶点数组。
接着创建渲染状态,禁用面片剔除(facet culling)和深度测试。尽管这是OpenGL的默认状态,但在我们的渲染器中,这些状态的默认值为true(因这是更常见的使用场景)。本示例为演示目的禁用了它们,不过即使保持默认值也不会影响输出结果。
最后创建绘制状态,整合上述创建的着色器程序、顶点数组和渲染状态。相机的zoomToTarget辅助方法会调整相机位置,确保三角形完全可见。
所有渲染器对象创建完成后,渲染过程就非常简单了。清除帧缓冲区,然后通过一个绘制调用绘制三角形,该调用会指示渲染器将顶点解释为三角形。
class Triangle {
public:
Triangle();
~Triangle();
void render();
private:
static constexpr const char* vs = R"(#version 330 core // version 必须在第一行
layout (location = ce_positionVertexLocation) in vec4 position;
uniform mat4 ce_modelViewPerspectiveMatrix;
void main()
{
gl_Position = ce_modelViewPerspectiveMatrix * position;
}
)";
static constexpr const char* fs = R"(#version 330 core
out vec4 FragColor;
uniform vec3 u_color;
void main()
{
FragColor = vec4(u_color, 1.0);
}
)";
Context* context_;
SceneState* sceneState_;
ClearState* clearState_;
DrawState* drawState_;
VertexArray* va{};
};
Triangle::Triangle() {
sceneState_ = new SceneState();
clearState_ = new ClearState();
ShaderProgram* sp = Device::createShaderProgram(vs, fs);
std::unordered_map<std::string, Uniform*> uniforms = sp->uniforms();
dynamic_cast<UniformT<glm::vec3>*>(uniforms["u_color"])->setValue(glm::vec3(1.0f, 0.0f, 0.0f));
Mesh mesh;
VertexAttributeFloatVector3* positionAttribute = new VertexAttributeFloatVector3("position", 3);
mesh.attributes()->add(positionAttribute);
IndicesUnsignedShort* indices = new IndicesUnsignedShort(3);
mesh.setIndices(indices);
auto& positions = positionAttribute->values();
positions.push_back(glm::vec3(0.0f, 0.0f, 0.0f));
positions.push_back(glm::vec3(1.0f, 0.0f, 0.0f));
positions.push_back(glm::vec3(0.0f, 0.0f, 1.0f));
indices->addTriangle(TriangleIndicesUnsignedShort(0, 1, 2));
VertexArray* va = context_->createVertexArray(&mesh, sp->vertexAttributes(), BufferHint::StaticDraw);
RenderState* renderState = new RenderState();
renderState->facetCulling.enabled = false;
renderState->depthTest.enabled = false;
drawState_ = new DrawState(renderState, sp, va);
sceneState_->camera->zoomToTarget(1.0);
}
Triangle::~Triangle() {
delete context_;
delete sceneState_;
delete clearState_;
delete drawState_;
delete va;
}
void Triangle::render() {
context_->clear(*clearState_);
context_->draw(PrimitiveType::Triangles, *drawState_, *sceneState_);
}
问题:
如何修改OpenGL渲染器的实现,以消除“必须先创建窗口才能创建其他渲染器类型”这一限制?这样做是否值得?
解答:通过离屏上下文 + 资源共享可消除 “窗口优先” 限制,核心是将 OpenGL 上下文与窗口的创建分离。是否值得取决于应用需求:
- 对于需要后台加载、离屏渲染或模块化设计的应用,修改带来的灵活性收益大于复杂度;
- 对于简单应用或兼容性要求严格的场景,保持现状更合适。 要消除 “必须先创建窗口才能创建其他渲染器类型” 的限制,核心在于将 OpenGL 上下文与窗口的生命周期解耦,允许在窗口创建前初始化渲染器资源。
想一想:
将PrimitiveType.Triangles参数改为PrimitiveType.LineLoop,结果会是什么?
解答:结果是绘制线框效果
在发起绘制调用时,我们也可以显式指定索引的偏移量和数量,例如下面的代码使用偏移量0和数量3:
context.draw(PrimitiveType.Triangles, 0, 3, _drawState, _sceneState);
当然,对于这个包含3个索引的索引缓冲区来说,显式指定偏移量和数量与不指定的结果相同——都会使用整个索引缓冲区进行绘制。尽管绘制三角形是一个简单示例,但许多示例的渲染代码几乎都同样简洁:只需调用context.draw,渲染器就会自动绑定着色器程序、设置自动uniform变量、绑定顶点数组、配置渲染状态,最终执行绘制!唯一的区别是,更复杂的示例可能还需要分配帧缓冲区、纹理和采样器,以及设置着色器uniform变量。渲染过程通常很简洁,大部分工作都集中在创建和更新渲染器对象上。
试一试:
修改项目,为三角形应用纹理。使用接受Bitmap参数的Device::createTexture2D重载方法,从磁盘上的图像创建纹理。通过VertexAttributeHalfFloatVector2为网格添加纹理坐标。将纹理和采样器分配到纹理单元,并相应地修改顶点着色器和片段着色器。
试一试:
修改项目,先渲染到帧缓冲区,再通过带纹理的全屏四边形将帧缓冲区的颜色附件渲染到屏幕上。使用Context.createFramebuffer创建帧缓冲区,并为帧缓冲区的某个颜色附件分配一个与窗口尺寸相同的纹理。可以考虑使用Scene中的ViewportQuad来渲染这个读取纹理并输出到屏幕的四边形。

参考:
- Cozi, Patrick; Ring, Kevin. 3D Engine Design for Virtual Globes. CRC Press, 2011.

8万+

被折叠的 条评论
为什么被折叠?



