欢迎光临
我们一直在努力

Building a Simple Engine -- Loading Models -- Scene rendering

场景渲染

场景渲染概述

场景渲染是将 3D 场景描述转换为可在屏幕上显示的 2D 图像的过程。在本引擎中,该过程包括遍历 scene graph、应用变换、设置材质属性,并向 GPU 下发绘制指令。

场景渲染是渲染管线的核心环节,所有已开发的组件都会在此整合协同:

  • 模型系统提供 scene graph 结构与 mesh 数据
  • 材质系统定义对象的视觉外观
  • 相机系统确定观察视角
  • 光照系统为场景提供光照效果

本章将探讨这些组件如何协同工作,实现完整场景的渲染。

Scene Graph 遍历

Scene graph 是一种层级树形结构,用于组织场景中的对象。树中的每个节点可包含变换信息(位置、旋转、缩放),也可包含待渲染的 mesh;节点还可拥有子节点,子节点会继承父节点的变换属性。

要渲染 scene graph,需采用深度优先的方式进行遍历,计算每个节点的全局变换矩阵,并渲染遍历过程中遇到的所有 mesh:

// 遍历入口
glm::mat4 rootMatrix = glm::mat4(1.0f);
renderNode(commandBuffer, model.nodes, rootMatrix);

renderNode函数是场景渲染系统的核心,它会递归遍历 scene graph,计算每个节点的全局变换矩阵,并渲染节点中包含的所有 mesh。

节点遍历与变换计算

渲染过程始于对 scene graph 的系统性遍历,每个节点的变换矩阵由其局部变换与父节点的累积变换矩阵组合计算得到。

void renderNode(const vk::raii::CommandBuffer& commandBuffer, const std::vector<Node*>& nodes, const glm::mat4& parentMatrix) {
for (const auto node : nodes) {
// 计算从根节点到当前节点的累积变换矩阵
// 将父节点的世界变换与当前节点的局部变换进行组合
glm::mat4 nodeMatrix = parentMatrix * node->getLocalMatrix();

变换矩阵的计算是层级 scene graph 渲染的核心。每个节点的getLocalMatrix()方法会返回其相对于父节点的变换矩阵,我们通过矩阵乘法将其与父节点的累积变换矩阵结合。这一数学操作会将变换关系沿层级链式传递,确保移动父节点时,其所有子节点会在世界空间中随之移动。

此处的矩阵乘法顺序至关重要:parentMatrix * nodeLocalMatrix 确保节点先执行局部变换(在自身局部坐标系中),再执行父节点的变换(将节点放置到世界空间中)。该顺序保留了子节点相对于父节点定位的层级关系。

Mesh 有效性验证与渲染准备

渲染前,必须验证节点是否包含有效的 mesh 数据,且数据是否已正确上传至 GPU 缓冲区,以此保证渲染的鲁棒性,处理不完整或无效的 scene graph 节点。

// 验证当前节点是否包含完整、可渲染的mesh数据
// 需满足所有条件,才能保证GPU安全渲染
if (!node->mesh.vertices.empty() && !node->mesh.indices.empty() &&
node->vertexBufferIndex >= 0 && node->indexBufferIndex >= 0) {

该验证步骤可避免因 scene graph 节点不完整导致的渲染错误。scene graph 中的并非所有节点都包含可渲染的几何图形 —— 部分节点仅用于组织场景,或作为子对象的变换锚点。通过检查顶点 / 索引数组非空且缓冲区索引有效,我们能确保仅尝试渲染已完成 GPU 资源准备的节点。

缓冲区索引检查(>= 0)用于确认 mesh 数据已成功上传至 GPU 缓冲区,且在缓冲区管理系统中被分配了有效索引(0 或更大数值)。注意,0 是有效的缓冲区索引,负值(通常为 – 1)表示缓冲区未初始化或分配失败。

材质属性配置

此步骤会将高层级的材质描述转换为 GPU 可识别的 push constants,由该常量控制渲染几何图形的外观与光照属性。

// 初始化push constants结构体,用于材质数据传输
PushConstantBlock pushConstants{};

// 若节点分配了有效材质,则配置材质属性
if (node->mesh.materialIndex >= 0 && node->mesh.materialIndex < static_cast<int>(model.materials.size())) {
const auto& material = model.materials[node->mesh.materialIndex];

// 设置控制表面外观的PBR材质因子
pushConstants.baseColorFactor = material.baseColorFactor; // 表面颜色色调
pushConstants.metallicFactor = material.metallicFactor; // 金属度/非金属属性
pushConstants.roughnessFactor = material.roughnessFactor; // 表面粗糙度

// 配置纹理绑定索引(-1表示无对应纹理)
pushConstants.baseColorTextureSet = material.baseColorTextureIndex >= 0 ? 1 : -1;
pushConstants.physicalDescriptorTextureSet = material.metallicRoughnessTextureIndex >= 0 ? 2 : -1;
pushConstants.normalTextureSet = material.normalTextureIndex >= 0 ? 3 : -1;
pushConstants.occlusionTextureSet = material.occlusionTextureIndex >= 0 ? 4 : -1;
pushConstants.emissiveTextureSet = material.emissiveTextureIndex >= 0 ? 5 : -1;
} else {
// 为未分配材质的节点应用合理的默认材质属性
pushConstants.baseColorFactor = glm::vec4(1.0f); // 白色基础色
pushConstants.metallicFactor = 1.0f; // 完全金属性(安全默认值)
pushConstants.roughnessFactor = 1.0f; // 完全粗糙(安全默认值)
pushConstants.baseColorTextureSet = -1; // 默认材质无纹理
pushConstants.physicalDescriptorTextureSet = -1; // 无金属度/粗糙度纹理
pushConstants.normalTextureSet = -1; // 无法线贴图
pushConstants.occlusionTextureSet = -1; // 无环境光遮蔽纹理
pushConstants.emissiveTextureSet = -1; // 无自发光纹理
}

材质配置系统是连接美术制作的材质与 GPU 着色器参数的桥梁。Push constants 为更新每个对象的材质数据提供了最快路径 —— 它绕过 GPU 的内存层级,可被着色器核心直接访问,非常适合在绘制调用之间频繁变化的材质属性。

纹理索引映射系统(未使用为 – 1,启用绑定为正整数)允许着色器根据纹理的可用性条件采样,该方式提升了灵活性:部分材质可包含法线贴图,而其他材质则无,且无需使用不同的着色器变体或复杂的分支逻辑。

默认材质属性的选择遵循保守原则,可防止材质缺失或配置错误时出现渲染伪影。金属度和粗糙度值设为 1.0 时,能在不同光照条件下产生视觉上可接受的结果,尽管这可能与材质的预期外观不符。

GPU 资源绑定与绘制指令执行

渲染的最后阶段会绑定 GPU 资源,并下发实际的绘制指令,将 scene graph 节点转换为屏幕上的渲染像素。

// 通过push constants将材质属性上传至GPU
// 为每次绘制调用提供快速的材质参数更新
commandBuffer.pushConstants(*pipelineLayout, vk::ShaderStageFlagBits::eFragment,
0, sizeof(PushConstantBlock), &pushConstants);

// 绑定几何数据缓冲区,供GPU访问
// 顶点缓冲区包含位置、法线、纹理坐标数据
commandBuffer.bindVertexBuffers(0, *vertexBuffers[node->vertexBufferIndex], {0});
// 索引缓冲区定义三角形的连接关系,实现顶点复用
commandBuffer.bindIndexBuffer(*indexBuffers[node->indexBufferIndex], 0, vk::IndexType::eUint32);

// 执行绘制指令,渲染当前mesh
// GPU处理索引生成三角形,并运行顶点/片元着色器
commandBuffer.drawIndexed(static_cast<uint32_t>(node->mesh.indices.size()), 1, 0, 0, 0);
}

资源绑定顺序遵循 Vulkan 的显式绑定模型,每种资源类型必须在使用前完成绑定。顶点缓冲区提供每个顶点的属性数据(位置、法线、纹理坐标),索引缓冲区定义顶点如何连接形成三角形。这种索引渲染方式通过允许多个三角形复用顶点,减少了内存占用。

drawIndexed指令会触发 GPU 为当前 mesh 执行整个图形管线。GPU 会处理每个索引以获取顶点数据,运行顶点着色器完成几何变换,光栅化三角形生成片元,再执行片元着色器确定最终的像素颜色。我们通过 push constants 配置的所有材质属性,都会在该过程中对片元着色器生效。

层级递归

最后,通过使用累积变换矩阵递归处理子节点,确保 scene graph 遍历的完整性,在整个渲染过程中保持层级结构。

// 使用累积变换矩阵递归处理子节点
// 沿scene graph向下保持层级变换链
if (!node->children.empty()) {
renderNode(commandBuffer, node->children, nodeMatrix);
}
}
}

该遍历方式可确保:

  • 每个节点的变换与父节点的变换正确组合
  • 子节点以正确的全局变换进行渲染
  • 渲染过程中保留 scene graph 的层级结构
  • 理解渲染过程

    我们对渲染过程进行更详细的拆解:

    变换计算

    渲染节点的第一步是计算其全局变换矩阵:

    // 计算当前节点的全局矩阵
    glm::mat4 nodeMatrix = parentMatrix * node->getLocalMatrix();

    该操作将节点的局部变换(位置、旋转、缩放)与父节点的全局变换结合,生成一个将节点从局部空间转换到世界空间的矩阵。

    getLocalMatrix方法(在Node类中定义)会组合节点的平移、旋转和缩放属性:

    glm::mat4 getLocalMatrix() {
    return glm::translate(glm::mat4(1.0f), translation) *
    glm::toMat4(rotation) *
    glm::scale(glm::mat4(1.0f), scale) *
    matrix;
    }

    材质设置

    我们已在 PBR 渲染相关章节介绍过 PBR 材质理论和着色器细节,此处不再赘述。本节重点讲解关联逻辑:在本章的场景中,材质属性如何被打包到 push constants 中,并被绘制调用使用。

    若节点包含 mesh,需在渲染前设置其材质属性:

    // 为材质属性设置push constants
    PushConstantBlock pushConstants{};

    if (node->mesh.materialIndex >= 0 && node->mesh.materialIndex < static_cast<int>(model.materials.size())) {
    const auto& material = model.materials[node->mesh.materialIndex];
    pushConstants.baseColorFactor = material.baseColorFactor;
    pushConstants.metallicFactor = material.metallicFactor;
    pushConstants.roughnessFactor = material.roughnessFactor;
    pushConstants.baseColorTextureSet = material.baseColorTextureIndex >= 0 ? 1 : -1;
    pushConstants.physicalDescriptorTextureSet = material.metallicRoughnessTextureIndex >= 0 ? 2 : -1;
    pushConstants.normalTextureSet = material.normalTextureIndex >= 0 ? 3 : -1;
    pushConstants.occlusionTextureSet = material.occlusionTextureIndex >= 0 ? 4 : -1;
    pushConstants.emissiveTextureSet = material.emissiveTextureIndex >= 0 ? 5 : -1;
    } else {
    // 默认材质属性
    pushConstants.baseColorFactor = glm::vec4(1.0f);
    pushConstants.metallicFactor = 1.0f;
    pushConstants.roughnessFactor = 1.0f;
    pushConstants.baseColorTextureSet = -1;
    pushConstants.physicalDescriptorTextureSet = -1;
    pushConstants.normalTextureSet = -1;
    pushConstants.occlusionTextureSet = -1;
    pushConstants.emissiveTextureSet = -1;
    }

    // 更新push constants
    commandBuffer.pushConstants(*pipelineLayout, vk::ShaderStageFlagBits::eFragment, 0, sizeof(PushConstantBlock), &pushConstants);

    上述代码的执行逻辑:

  • 获取与 mesh 关联的材质
  • 用材质属性初始化 push constants
  • 通过 push constants 将这些属性传递给片元着色器
  • 材质属性包含:

    • 基础颜色因子(反照率)
    • 金属度因子
    • 粗糙度因子
    • 各类材质贴图的纹理集索引(基础颜色、金属度 – 粗糙度、法线、环境光遮蔽、自发光)
    Mesh 渲染

    完成变换和材质设置后,即可渲染 mesh:

    // 绑定顶点缓冲区和索引缓冲区
    commandBuffer.bindVertexBuffers(0, *vertexBuffers[node->vertexBufferIndex], {0});
    commandBuffer.bindIndexBuffer(*indexBuffers[node->indexBufferIndex], 0, vk::IndexType::eUint32);

    // 绘制mesh
    commandBuffer.drawIndexed(static_cast<uint32_t>(node->mesh.indices.size()), 1, 0, 0, 0);

    上述代码的执行逻辑:

  • 绑定包含 mesh 顶点的顶点缓冲区
  • 绑定包含 mesh 索引的索引缓冲区
  • 下发绘制指令渲染 mesh
  • 递归遍历

    渲染当前节点后,递归遍历其子节点:

    // 递归渲染子节点
    if (!node->children.empty()) {
    renderNode(commandBuffer, node->children, nodeMatrix);
    }

    这一操作确保 scene graph 中的所有节点都被访问,并以正确的变换进行渲染。

    在主循环中整合场景渲染

    要在主渲染循环中使用场景渲染系统,需设置必要的 Vulkan 状态并调用renderScene函数。为便于理解,可将单帧渲染分为 5 个步骤:

  • 初始化并描述渲染附件(动态渲染输入)
  • 开始渲染、绑定管线、设置视口 / 裁剪区域
  • 更新相机 UBO(视图 / 投影矩阵)
  • 遍历 scene graph,为每个 mesh 下发绘制指令
  • 结束渲染并展示帧画面
  • void drawFrame() {
    // …(标准Vulkan帧初始化代码)

    // 开始记录命令缓冲区
    commandBuffer.begin({});

    // 转换图像布局以支持渲染
    transition_image_layout(
    imageIndex,
    vk::ImageLayout::eUndefined,
    vk::ImageLayout::eColorAttachmentOptimal,
    {},
    vk::AccessFlagBits2::eColorAttachmentWrite,
    vk::PipelineStageFlagBits2::eTopOfPipe,
    vk::PipelineStageFlagBits2::eColorAttachmentOutput
    );

    // 配置渲染附件
    vk::ClearValue clearColor = vk::ClearColorValue(0.0f, 0.0f, 0.0f, 1.0f);
    vk::ClearValue clearDepth = vk::ClearDepthStencilValue(1.0f, 0);

    vk::RenderingAttachmentInfo colorAttachmentInfo = {
    .imageView = swapChainImageViews[imageIndex],
    .imageLayout = vk::ImageLayout::eColorAttachmentOptimal,
    .loadOp = vk::AttachmentLoadOp::eClear,
    .storeOp = vk::AttachmentStoreOp::eStore,
    .clearValue = clearColor
    };

    vk::RenderingAttachmentInfo depthAttachmentInfo = {
    .imageView = depthImageView,
    .imageLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal,
    .loadOp = vk::AttachmentLoadOp::eClear,
    .storeOp = vk::AttachmentStoreOp::eStore,
    .clearValue = clearDepth
    };

    vk::RenderingInfo renderingInfo = {
    .renderArea = { .offset = { 0, 0 }, .extent = swapChainExtent },
    .layerCount = 1,
    .colorAttachmentCount = 1,
    .pColorAttachments = &colorAttachmentInfo,
    .pDepthAttachment = &depthAttachmentInfo
    };

    // 开始动态渲染
    commandBuffer.beginRendering(renderingInfo);

    // 绑定渲染管线
    commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, graphicsPipeline);

    // 设置视口和裁剪区域
    commandBuffer.setViewport(0, vk::Viewport(0.0f, 0.0f, static_cast<float>(swapChainExtent.width), static_cast<float>(swapChainExtent.height), 0.0f, 1.0f));
    commandBuffer.setScissor(0, vk::Rect2D(vk::Offset2D(0, 0), swapChainExtent));

    // 绑定包含统一缓冲区和纹理的描述符集
    commandBuffer.bindDescriptorSets(
    vk::PipelineBindPoint::eGraphics,
    pipelineLayout,
    0,
    1,
    &descriptorSets[currentFrame],
    0,
    nullptr
    );

    // 更新统一缓冲区中的视图和投影矩阵
    UniformBufferObject ubo{};
    ubo.view = camera.getViewMatrix();
    ubo.proj = camera.getProjectionMatrix(swapChainExtent.width / (float)swapChainExtent.height);
    ubo.proj[1][1] *= -1; // Vulkan的Y轴方向是翻转的

    // 拷贝至统一缓冲区(按帧同步)
    memcpy(uniformBuffers[currentFrame].mapped, &ubo, sizeof(ubo));

    // 渲染场景
    renderScene(commandBuffer, model, ubo.view, ubo.proj);

    // 结束动态渲染
    commandBuffer.endRendering();

    // 转换图像布局以支持展示
    transition_image_layout(
    imageIndex,
    vk::ImageLayout::eColorAttachmentOptimal,
    vk::ImageLayout::ePresentSrcKHR,
    vk::AccessFlagBits2::eColorAttachmentWrite,
    {},
    vk::PipelineStageFlagBits2::eColorAttachmentOutput,
    vk::PipelineStageFlagBits2::eBottomOfPipe
    );

    // 结束命令缓冲区记录
    commandBuffer.end();

    // …(提交命令缓冲区并展示帧画面)
    }

    上述代码的执行逻辑:

  • 设置 Vulkan 渲染状态(命令缓冲区、图像布局转换、渲染附件)
  • 绑定图形管线和描述符集
  • 更新统一缓冲区中的视图和投影矩阵
  • 调用renderScene渲染整个场景
  • 完成渲染并展示结果
  • 性能优化要点

    渲染复杂场景对性能要求较高,以下是优化场景渲染的常用技术:

    视锥剔除(Frustum Culling)

    视锥剔除指跳过相机视锥体外对象的渲染,通过减少绘制调用的数量,可显著提升性能:

    bool isNodeVisible(const Node* node, const glm::mat4& viewProjection) {
    // 计算节点的世界空间包围球
    glm::vec3 center = glm::vec3(node->getGlobalMatrix() * glm::vec4(node->boundingSphere.center, 1.0f));
    float radius = node->boundingSphere.radius * glm::length(glm::vec3(node->getGlobalMatrix()[0])); // 按最大缩放因子缩放包围球半径

    // 检查包围球是否在视锥体内可见
    for (int i = 0; i < 6; i++) {
    // 从视图-投影矩阵中提取视锥平面
    glm::vec4 plane = getFrustumPlane(viewProjection, i);

    // 计算包围球中心到平面的符号距离
    float distance = glm::dot(glm::vec4(center, 1.0f), plane);

    // 若包围球完全在平面后方,则不可见
    if (distance < -radius) {
    return false;
    }
    }

    return true;
    }

    void renderNodeWithCulling(const vk::raii::CommandBuffer& commandBuffer, const std::vector<Node*>& nodes, const glm::mat4& parentMatrix, const glm::mat4& viewProjection) {
    for (const auto node : nodes) {
    // 计算当前节点的全局矩阵
    glm::mat4 nodeMatrix = parentMatrix * node->getLocalMatrix();

    // 检查节点是否可见
    if (isNodeVisible(node, viewProjection)) {
    // 渲染节点(逻辑与之前一致)
    // …

    // 递归渲染子节点
    if (!node->children.empty()) {
    renderNodeWithCulling(commandBuffer, node->children, nodeMatrix, viewProjection);
    }
    }
    }
    }

    细节层次(Level of Detail, LOD)

    LOD 技术指对距离相机较远的对象,使用简化版本的模型进行渲染:

    void renderNodeWithLOD(const vk::raii::CommandBuffer& commandBuffer, const std::vector<Node*>& nodes, const glm::mat4& parentMatrix, const glm::vec3& cameraPosition) {
    for (const auto node : nodes) {
    // 计算当前节点的全局矩阵
    glm::mat4 nodeMatrix = parentMatrix * node->getLocalMatrix();

    // 计算到相机的距离
    glm::vec3 nodePosition = glm::vec3(nodeMatrix[3]);
    float distanceToCamera = glm::distance(nodePosition, cameraPosition);

    // 根据距离选择LOD层级
    int lodLevel = 0;
    if (distanceToCamera > 50.0f) {
    lodLevel = 2; // 低细节
    } else if (distanceToCamera > 20.0f) {
    lodLevel = 1; // 中等细节
    }

    // 使用选定的LOD层级渲染节点
    // …

    // 递归渲染子节点
    if (!node->children.empty()) {
    renderNodeWithLOD(commandBuffer, node->children, nodeMatrix, cameraPosition);
    }
    }
    }

    遮挡剔除(Occlusion Culling)

    遮挡剔除指跳过被其他对象遮挡的对象的渲染:

    void renderNodeWithOcclusion(const vk::raii::CommandBuffer& commandBuffer, const std::vector<Node*>& nodes, const glm::mat4& parentMatrix) {
    // 按到相机的距离对节点进行排序(从近到远)
    std::vector<std::pair<Node*, float>> sortedNodes;
    for (const auto node : nodes) {
    glm::mat4 nodeMatrix = parentMatrix * node->getLocalMatrix();
    glm::vec3 nodePosition = glm::vec3(nodeMatrix[3]);
    float distanceToCamera = glm::length(nodePosition – cameraPosition);
    sortedNodes.push_back({node, distanceToCamera});
    }
    std::sort(sortedNodes.begin(), sortedNodes.end(), [](const auto& a, const auto& b) {
    return a.second < b.second;
    });

    // 从近到远渲染节点
    for (const auto& [node, distance] : sortedNodes) {
    // 计算当前节点的全局矩阵
    glm::mat4 nodeMatrix = parentMatrix * node->getLocalMatrix();

    // 开始遮挡查询
    vk::QueryPool occlusionQueryPool = createOcclusionQueryPool();
    commandBuffer.beginQuery(occlusionQueryPool, 0, {});

    // 渲染节点的包围盒,仅写入深度缓冲区,不写入颜色缓冲区
    renderBoundingBox(commandBuffer, node, nodeMatrix);

    // 结束遮挡查询
    commandBuffer.endQuery(occlusionQueryPool, 0);

    // 检查节点是否可见
    uint64_t occlusionResult = getOcclusionQueryResult(occlusionQueryPool);
    if (occlusionResult > 0) {
    // 节点可见,执行渲染
    // …

    // 递归渲染子节点
    if (!node->children.empty()) {
    renderNodeWithOcclusion(commandBuffer, node->children, nodeMatrix);
    }
    }
    }
    }

    实例化渲染(Instanced Rendering)

    对于包含大量相同对象的场景,实例化渲染可显著提升性能:

    void renderInstanced(const vk::raii::CommandBuffer& commandBuffer, const std::vector<Node*>& nodes, const std::vector<glm::mat4>& instanceMatrices) {
    for (const auto node : nodes) {
    // 若节点包含mesh,则以实例化方式渲染
    if (!node->mesh.vertices.empty() && !node->mesh.indices.empty() &&
    node->vertexBufferIndex >= 0 && node->indexBufferIndex >= 0) {

    // 设置材质属性(逻辑与之前一致)
    // …

    // 绑定顶点缓冲区和索引缓冲区
    commandBuffer.bindVertexBuffers(0, *vertexBuffers[node->vertexBufferIndex], {0});
    commandBuffer.bindIndexBuffer(*indexBuffers[node->indexBufferIndex], 0, vk::IndexType::eUint32);

    // 创建并绑定实例缓冲区
    vk::raii::Buffer instanceBuffer = createInstanceBuffer(instanceMatrices);
    commandBuffer.bindVertexBuffers(1, *instanceBuffer, {0});

    // 以实例化方式绘制mesh
    commandBuffer.drawIndexedInstanced(
    static_cast<uint32_t>(node->mesh.indices.size()),
    static_cast<uint32_t>(instanceMatrices.size()),
    0, 0, 0
    );
    }

    // 递归渲染子节点
    if (!node->children.empty()) {
    renderInstanced(commandBuffer, node->children, instanceMatrices);
    }
    }
    }

    高级场景渲染技术

    除基础的场景渲染外,还有多种高级技术可提升渲染器的视觉质量和性能:

    层级剔除(Hierarchical Culling)

    层级剔除利用 scene graph 的结构加速剔除操作:

    bool isNodeAndChildrenVisible(const Node* node, const glm::mat4& viewProjection, const glm::mat4& parentMatrix) {
    // 计算当前节点的全局矩阵
    glm::mat4 nodeMatrix = parentMatrix * node->getLocalMatrix();

    // 检查节点的包围体是否可见
    if (!isNodeVisible(node, viewProjection, nodeMatrix)) {
    // 若节点不可见,则其子节点也均不可见
    return false;
    }

    // 节点可见,检查是否包含待渲染的mesh
    bool hasVisibleContent = !node->mesh.vertices.empty() && !node->mesh.indices.empty();

    // 递归检查子节点
    for (const auto child : node->children) {
    hasVisibleContent |= isNodeAndChildrenVisible(child, viewProjection, nodeMatrix);
    }

    return hasVisibleContent;
    }

    void renderNodeHierarchical(const vk::raii::CommandBuffer& commandBuffer, const std::vector<Node*>& nodes, const glm::mat4& parentMatrix, const glm::mat4& viewProjection) {
    for (const auto node : nodes) {
    // 计算当前节点的全局矩阵
    glm::mat4 nodeMatrix = parentMatrix * node->getLocalMatrix();

    // 检查节点及其子节点是否可见
    if (isNodeAndChildrenVisible(node, viewProjection, glm::mat4(1.0f))) {
    // 若节点包含mesh,则执行渲染
    if (!node->mesh.vertices.empty() && !node->mesh.indices.empty() &&
    node->vertexBufferIndex >= 0 && node->indexBufferIndex >= 0) {
    // 渲染节点(逻辑与之前一致)
    // …
    }

    // 递归渲染子节点
    if (!node->children.empty()) {
    renderNodeHierarchical(commandBuffer, node->children, nodeMatrix, viewProjection);
    }
    }
    }
    }

    本文介绍的层级剔除算法逻辑正确,但相对基础。在当前实现形式下,其性能提升效果未必优于简单的剔除方法。若要在生产环境中实现显著的性能提升,需要更高效、更复杂的处理方式,例如:

    • 结合高效的空间数据结构,使用包围体层级(BVH)
    • 通过计算着色器实现基于 GPU 的剔除
    • 利用时间一致性,跨帧缓存可见性结果
    • 使用遮挡查询,跳过被其他几何图形遮挡的整个子树

    延迟渲染(Deferred Rendering)

    延迟渲染将几何处理和光照处理阶段分离,可提升包含大量光源的场景的渲染性能:

    void renderSceneDeferred(const vk::raii::CommandBuffer& commandBuffer, Model& model) {
    // 几何处理阶段:将场景渲染至G缓冲区
    beginGeometryPass(commandBuffer);
    renderNode(commandBuffer, model.nodes, glm::mat4(1.0f));
    endGeometryPass(commandBuffer);

    // 光照处理阶段:对G缓冲区应用光照效果
    beginLightingPass(commandBuffer);
    for (const auto& light : lights) {
    renderLight(commandBuffer, light);
    }
    endLightingPass(commandBuffer);
    }

    分簇渲染(Clustered Rendering)

    分簇渲染将相机视锥体划分为多个 3D 单元格,可高效处理大量光源:

    void setupLightClusters() {
    // 将相机视锥体划分为3D网格状的簇
    const int clusterCountX = 16;
    const int clusterCountY = 9;
    const int clusterCountZ = 24;

    // 根据光源的位置和半径,将光源分配至对应簇
    for (const auto& light : lights) {
    for (int x = 0; x < clusterCountX; x++) {
    for (int y = 0; y < clusterCountY; y++) {
    for (int z = 0; z < clusterCountZ; z++) {
    if (lightAffectsCluster(light, x, y, z)) {
    lightClusters[x][y][z].push_back(light.index);
    }
    }
    }
    }
    }

    // 将光源簇数据上传至GPU
    updateLightClusterBuffer();
    }

    void renderSceneClustered(const vk::raii::CommandBuffer& commandBuffer, Model& model) {
    // 绑定光源簇缓冲区
    commandBuffer.bindDescriptorSets(
    vk::PipelineBindPoint::eGraphics,
    pipelineLayout,
    1,
    1,
    &lightClusterDescriptorSet,
    0,
    nullptr
    );

    // 按常规方式渲染场景
    renderNode(commandBuffer, model.nodes, glm::mat4(1.0f));
    }

    总结

    本章探讨了如何使用 scene graph 实现场景渲染,包括 scene graph 的遍历、变换矩阵的计算、材质的应用以及 mesh 的渲染,同时也介绍了多种性能优化技术。

    我们构建的场景渲染系统具备灵活性和可扩展性,可渲染包含多个对象、多种材质和不同光照条件的复杂场景。下一章,我们将在此基础上实现动画效果,让场景变得生动起来。

    赞(0)
    未经允许不得转载:171主机测评 » Building a Simple Engine -- Loading Models -- Scene rendering
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址