【OpenGL 渲染器开发笔记】8 综合实践:渲染一个三角形

本章示例展示了如何使用渲染器绘制单个红色三角形。它整合了本章介绍的多个概念和代码片段,形成了一个可运行、可调试的示例,更重要的是,你可以在此基础上构建自己的代码。项目引用了 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.
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值