news 2026/5/9 22:24:44

教程 34 - 多渲染通道

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
教程 34 - 多渲染通道

上一篇:资源系统 | 下一篇:在UI渲染通道中绘制 | 返回目录


📚 快速导航


目录
  • 简介
  • 学习目标
  • 多渲染通道架构
    • 为什么需要多个Renderpass
    • World与UI分离
    • 渲染流程
  • Renderpass枚举
  • UI着色器实现
    • UI顶点着色器
    • UI片段着色器
    • 2D vs 3D坐标系
  • Framebuffer策略
  • Backend接口扩展
  • Vulkan实现
    • Renderpass切换
    • Shader绑定
    • 全局状态更新
  • Render Packet扩展
  • 正交投影矩阵
  • 渲染顺序与Alpha混合
  • 常见问题
  • 练习

📖 简介

在之前的教程中,我们只有一个渲染通道 (Renderpass),用于渲染所有几何体。但游戏引擎通常需要渲染多种类型的内容:3D 世界、2D UI、后处理效果、阴影贴图等。每种内容都有不同的渲染需求。

本教程将介绍多渲染通道架构(Multiple Renderpasses),将 3D 世界渲染和 2D UI 渲染分离到不同的 renderpass 中。通过这种分离,我们可以:

  • 使用不同的着色器 (World 用透视投影,UI 用正交投影)
  • 使用不同的 framebuffer (离屏渲染 vs 直接显示)
  • 控制渲染顺序 (World 先渲染,UI 后渲染)
  • 为未来的后处理效果做准备
Frame 渲染一帧
World Renderpass 世界渲染通道
UI Renderpass UI渲染通道
Begin Frame
开始帧
End Frame
结束帧
Begin Renderpass
UI
Update Global State
正交投影
Draw UI Geometries
绘制2D UI
End Renderpass
UI
Begin Renderpass
WORLD
Update Global State
透视投影 + 视图矩阵
Draw World Geometries
绘制3D几何体
End Renderpass
WORLD

🎯 学习目标

目标描述
理解多Renderpass架构了解为什么需要多个渲染通道
实现UI着色器创建专门用于2D UI渲染的着色器
掌握正交投影理解正交投影与透视投影的区别
Framebuffer分层理解离屏渲染和最终显示的framebuffer
Renderpass切换实现多个renderpass之间的切换

多渲染通道架构

为什么需要多个Renderpass

在单一 renderpass 中混合渲染不同类型的内容会导致问题:

❌ 单一 Renderpass 的问题: ┌────────────────────────────┐ │ Single Renderpass │ │ │ │ - 3D Models (透视投影) │ │ - UI Elements (正交投影?) │ │ - Text (2D) │ │ - Particles (需要混合) │ │ │ │ 问题: │ │ 1. 无法使用不同的投影矩阵 │ │ 2. 深度测试冲突 │ │ 3. 渲染顺序难以控制 │ │ 4. 着色器逻辑复杂 │ └────────────────────────────┘

使用多个 renderpass 可以解决这些问题:

✅ 多 Renderpass 架构: ┌─────────────────────────────┐ │ World Renderpass │ │ - Material Shader │ │ - 透视投影 (Perspective) │ │ - 深度测试启用 │ │ - 渲染 3D 模型 │ └──────────┬──────────────────┘ │ ▼ ┌─────────────────────────────┐ │ UI Renderpass │ │ - UI Shader │ │ - 正交投影 (Orthographic) │ │ - 深度测试禁用 │ │ - 渲染 2D UI │ └─────────────────────────────┘ 优势: 1. 每个 renderpass 使用专门的着色器 2. 独立的投影矩阵和视图矩阵 3. 明确的渲染顺序 4. 更好的性能和可维护性

World与UI分离

我们将渲染分为两个通道:

World Renderpass (世界渲染通道)

  • 用途:渲染 3D 场景
  • 着色器:Material Shader
  • 投影:透视投影 (Perspective Projection)
  • 深度测试:启用
  • Framebuffer:离屏 framebuffer (world_framebuffers)
  • 示例内容:3D 模型、地形、天空盒

UI Renderpass (UI 渲染通道)

  • 用途:渲染 2D 用户界面
  • 着色器:UI Shader
  • 投影:正交投影 (Orthographic Projection)
  • 深度测试:通常禁用
  • Framebuffer:Swapchain framebuffer (直接显示)
  • 示例内容:按钮、文本、图标、HUD

渲染流程

完整的渲染流程:

// 1. 开始帧backend.begin_frame(delta_time);// ==================== World Renderpass ====================// 2. 开始世界渲染通道backend.begin_renderpass(BUILTIN_RENDERPASS_WORLD);// 3. 更新世界全局状态 (透视投影)mat4 projection=mat4_perspective(deg_to_rad(45.0f),aspect_ratio,0.1f,1000.0f);mat4 view=camera_get_view_matrix();backend.update_global_world_state(projection,view,camera_position,ambient_color,0);// 4. 绘制世界几何体for(u32 i=0;i<world_geometry_count;++i){backend.draw_geometry(world_geometries[i]);}// 5. 结束世界渲染通道backend.end_renderpass(BUILTIN_RENDERPASS_WORLD);// ==================== UI Renderpass ====================// 6. 开始 UI 渲染通道backend.begin_renderpass(BUILTIN_RENDERPASS_UI);// 7. 更新 UI 全局状态 (正交投影)mat4 ui_projection=mat4_orthographic(0,screen_width,screen_height,0,-100.0f,100.0f);mat4 ui_view=mat4_identity();backend.update_global_ui_state(ui_projection,ui_view,0);// 8. 绘制 UI 几何体for(u32 i=0;i<ui_geometry_count;++i){backend.draw_geometry(ui_geometries[i]);}// 9. 结束 UI 渲染通道backend.end_renderpass(BUILTIN_RENDERPASS_UI);// 10. 结束帧backend.end_frame(delta_time);

📋 Renderpass枚举

定义内置的 renderpass 类型:

// engine/src/renderer/renderer_types.inl/** * @brief 内置渲染通道枚举 */typedefenumbuiltin_renderpass{BUILTIN_RENDERPASS_WORLD=0x01,// 世界渲染通道BUILTIN_RENDERPASS_UI=0x02// UI 渲染通道}builtin_renderpass;

使用位标志的好处:

// 可以用位运算组合 renderpassu8 renderpass_mask=BUILTIN_RENDERPASS_WORLD|BUILTIN_RENDERPASS_UI;// 检查是否包含某个 renderpassif(renderpass_mask&BUILTIN_RENDERPASS_WORLD){// 包含世界渲染通道}

UI着色器实现

UI顶点着色器

UI 着色器与 Material 着色器的主要区别:

// assets/shaders/Builtin.UIShader.vert.glsl #version 450 // ========== 输入 (2D 顶点) ========== layout(location = 0) in vec2 in_position; // 2D 位置 (x, y) layout(location = 1) in vec2 in_texcoord; // 纹理坐标 // ========== 全局 UBO (正交投影) ========== layout(set = 0, binding = 0) uniform global_uniform_object { mat4 projection; // 正交投影矩阵 mat4 view; // 视图矩阵 (通常是单位矩阵) } global_ubo; // ========== Push Constants (模型矩阵) ========== layout(push_constant) uniform push_constants { mat4 model; // 64 bytes - UI 元素的变换矩阵 } u_push_constants; // ========== 输出 ========== layout(location = 1) out struct dto { vec2 tex_coord; } out_dto; void main() { // 注意:翻转 Y 纹理坐标 // 这样配合翻转的正交矩阵,使 [0,0] 在左上角而不是左下角 out_dto.tex_coord = vec2(in_texcoord.x, 1.0 - in_texcoord.y); // 计算位置:projection * view * model * position // 注意:position 是 vec2,扩展为 vec4(x, y, 0.0, 1.0) gl_Position = global_ubo.projection * global_ubo.view * u_push_constants.model * vec4(in_position, 0.0, 1.0); }

关键区别:

特性Material ShaderUI Shader
输入位置vec3 in_position(3D)vec2 in_position(2D)
投影类型透视投影正交投影
Z 坐标使用真实深度固定为 0.0
纹理坐标翻转不翻转翻转 Y (1.0 - y)
用途3D 模型渲染2D UI 元素渲染

UI片段着色器

UI 片段着色器非常简洁:

// assets/shaders/Builtin.UIShader.frag.glsl #version 450 // ========== 输出 ========== layout(location = 0) out vec4 out_colour; // ========== 材质 UBO ========== layout(set = 1, binding = 0) uniform local_uniform_object { vec4 diffuse_colour; // 颜色 (用于着色) } object_ubo; // ========== 纹理采样器 ========== layout(set = 1, binding = 1) uniform sampler2D diffuse_sampler; // ========== 输入 ========== layout(location = 1) in struct dto { vec2 tex_coord; } in_dto; void main() { // 简单的纹理采样 * 颜色调制 out_colour = object_ubo.diffuse_colour * texture(diffuse_sampler, in_dto.tex_coord); }

为什么这么简单?

UI 渲染不需要复杂的光照计算:

  • 没有法线
  • 没有光照
  • 没有阴影
  • 只需要纹理 + 颜色调制

2D vs 3D坐标系

两种坐标系的对比:

3D 世界坐标系 (透视投影): Y (向上) │ │ │ └─────── X (向右) ╱ ╱ Z (向前) - 透视投影:远处物体变小 - 深度测试:正确的遮挡关系 - 视锥裁剪:near_clip ~ far_clip 2D UI 坐标系 (正交投影): (0,0) ────────► X (向右) │ │ │ ▼ Y (向下) - 正交投影:物体大小不变 - 屏幕空间坐标:[0, screen_width] x [0, screen_height] - 深度范围:通常 -100 ~ +100 (用于分层)

为什么 UI 坐标系 Y 向下?

这是为了匹配屏幕坐标习惯:

传统屏幕坐标: ┌───────────────┐ (0, 0) │ │ │ UI 元素 │ │ │ └───────────────┘ (width, height) OpenGL/Vulkan 默认坐标: ┌───────────────┐ (0, height) │ │ │ │ │ │ └───────────────┘ (0, 0) 解决方案:翻转正交矩阵的 Y 轴

Framebuffer策略

我们使用两套 framebuffer:

// engine/src/renderer/vulkan/vulkan_types.inltypedefstructvulkan_context{// ... 其他成员 ...vulkan_renderpass main_renderpass;// 世界渲染通道vulkan_renderpass ui_renderpass;// UI 渲染通道// World framebuffers - 离屏渲染VkFramebuffer world_framebuffers[3];// 每帧一个vulkan_swapchain swapchain;// swapchain.framebuffers[3] - 最终显示到屏幕// ...}vulkan_context;

Framebuffer 使用策略:

帧渲染流程: ┌──────────────────────────────────┐ │ World Renderpass │ │ │ │ world_framebuffers[image_index] │ │ ├─ Color Attachment (离屏纹理) │ │ └─ Depth Attachment │ └───────────────┬──────────────────┘ │ │ (world 渲染结果作为纹理) │ ▼ ┌──────────────────────────────────┐ │ UI Renderpass │ │ │ │ swapchain.framebuffers[image_idx]│ │ ├─ Color Attachment (swapchain) │ │ │ └─ 包含 world 渲染结果 │ │ └─ Depth Attachment │ └───────────────┬──────────────────┘ │ ▼ Present to Screen 呈现到屏幕

为什么 World 使用离屏 Framebuffer?

  1. 后处理准备: 离屏渲染的结果可以作为纹理输入到后处理着色器
  2. 合成灵活性: UI 可以在 world 渲染结果之上合成
  3. 分辨率独立: World 渲染可以使用不同的分辨率 (如 upscaling/downscaling)
  4. 未来扩展: 为阴影贴图、反射等高级效果做准备

Backend接口扩展

新增的 backend 接口:

// engine/src/renderer/renderer_types.inltypedefstructrenderer_backend{u64 frame_number;b8(*initialize)(structrenderer_backend*backend,constchar*application_name);void(*shutdown)(structrenderer_backend*backend);void(*resized)(structrenderer_backend*backend,u16 width,u16 height);b8(*begin_frame)(structrenderer_backend*backend,f32 delta_time);b8(*end_frame)(structrenderer_backend*backend,f32 delta_time);// ========== 新增:Renderpass 控制 ==========b8(*begin_renderpass)(structrenderer_backend*backend,u8 renderpass_id);b8(*end_renderpass)(structrenderer_backend*backend,u8 renderpass_id);// ========== 新增:分离的全局状态更新 ==========void(*update_global_world_state)(mat4 projection,mat4 view,vec3 view_position,vec4 ambient_colour,i32 mode);void(*update_global_ui_state)(mat4 projection,mat4 view,i32 mode);void(*draw_geometry)(geometry_render_data data);void(*create_texture)(constu8*pixels,structtexture*texture);void(*destroy_texture)(structtexture*texture);b8(*create_material)(structmaterial*material);void(*destroy_material)(structmaterial*material);b8(*create_geometry)(geometry*geometry,u32 vertex_count,constvertex_3d*vertices,u32 index_count,constu32*indices);void(*destroy_geometry)(geometry*geometry);}renderer_backend;

接口变化:

旧接口新接口变化
update_global_state()update_global_world_state()+update_global_ui_state()分离为两个函数
begin_renderpass()新增 renderpass 控制
end_renderpass()新增 renderpass 控制

Vulkan实现

Renderpass切换

实现 renderpass 的开始和结束:

// engine/src/renderer/vulkan/vulkan_backend.cb8vulkan_renderer_begin_renderpass(structrenderer_backend*backend,u8 renderpass_id){vulkan_renderpass*renderpass=0;VkFramebuffer framebuffer=0;vulkan_command_buffer*command_buffer=&context.graphics_command_buffers[context.image_index];// 1. 根据 ID 选择 renderpass 和 framebufferswitch(renderpass_id){caseBUILTIN_RENDERPASS_WORLD:renderpass=&context.main_renderpass;framebuffer=context.world_framebuffers[context.image_index];break;caseBUILTIN_RENDERPASS_UI:renderpass=&context.ui_renderpass;framebuffer=context.swapchain.framebuffers[context.image_index];break;default:KERROR("vulkan_renderer_begin_renderpass called on unrecognized renderpass id: %#02x",renderpass_id);returnfalse;}// 2. 开始 renderpass (记录 vkCmdBeginRenderPass)vulkan_renderpass_begin(command_buffer,renderpass,framebuffer);// 3. 绑定对应的着色器switch(renderpass_id){caseBUILTIN_RENDERPASS_WORLD:vulkan_material_shader_use(&context,&context.material_shader);break;caseBUILTIN_RENDERPASS_UI:vulkan_ui_shader_use(&context,&context.ui_shader);break;}returntrue;}b8vulkan_renderer_end_renderpass(structrenderer_backend*backend,u8 renderpass_id){vulkan_renderpass*renderpass=0;vulkan_command_buffer*command_buffer=&context.graphics_command_buffers[context.image_index];// 1. 根据 ID 选择 renderpassswitch(renderpass_id){caseBUILTIN_RENDERPASS_WORLD:renderpass=&context.main_renderpass;break;caseBUILTIN_RENDERPASS_UI:renderpass=&context.ui_renderpass;break;default:KERROR("vulkan_renderer_end_renderpass called on unrecognized renderpass id: %#02x",renderpass_id);returnfalse;}// 2. 结束 renderpass (记录 vkCmdEndRenderPass)vulkan_renderpass_end(command_buffer,renderpass);returntrue;}

Shader绑定

每个 renderpass 使用不同的着色器:

// vulkan_material_shader_use() - 绑定 Material Shadervoidvulkan_material_shader_use(vulkan_context*context,vulkan_material_shader*shader){u32 image_index=context->image_index;// 绑定 pipeline (包含 vertex shader + fragment shader)vkCmdBindPipeline(context->graphics_command_buffers[image_index].handle,VK_PIPELINE_BIND_POINT_GRAPHICS,shader->pipeline.handle);}// vulkan_ui_shader_use() - 绑定 UI Shadervoidvulkan_ui_shader_use(vulkan_context*context,vulkan_ui_shader*shader){u32 image_index=context->image_index;// 绑定 UI pipelinevkCmdBindPipeline(context->graphics_command_buffers[image_index].handle,VK_PIPELINE_BIND_POINT_GRAPHICS,shader->pipeline.handle);}

全局状态更新

分别更新 World 和 UI 的全局状态:

// engine/src/renderer/vulkan/vulkan_backend.cvoidvulkan_renderer_update_global_world_state(mat4 projection,mat4 view,vec3 view_position,vec4 ambient_colour,i32 mode){vulkan_command_buffer*command_buffer=&context.graphics_command_buffers[context.image_index];// 更新 Material Shader 的全局 UBOvulkan_material_shader_update_global_state(&context,&context.material_shader);// 绑定全局 descriptor setvulkan_material_shader_bind_globals(&context,&context.material_shader);// 设置投影和视图矩阵context.material_shader.global_ubo.projection=projection;context.material_shader.global_ubo.view=view;// TODO: 其他全局状态 (ambient_colour, view_position, etc.)}voidvulkan_renderer_update_global_ui_state(mat4 projection,mat4 view,i32 mode){vulkan_command_buffer*command_buffer=&context.graphics_command_buffers[context.image_index];// 更新 UI Shader 的全局 UBOvulkan_ui_shader_update_global_state(&context,&context.ui_shader,context.frame_delta_time);// 绑定全局 descriptor setvulkan_ui_shader_bind_globals(&context,&context.ui_shader);// 设置投影和视图矩阵context.ui_shader.global_ubo.projection=projection;context.ui_shader.global_ubo.view=view;}

Render Packet扩展

Render Packet 现在包含两个几何体列表:

// engine/src/renderer/renderer_types.inltypedefstructgeometry_render_data{mat4 model;// 模型矩阵geometry*geometry;// 几何体指针}geometry_render_data;typedefstructrender_packet{f32 delta_time;// 世界几何体u32 geometry_count;geometry_render_data*geometries;// UI 几何体u32 ui_geometry_count;geometry_render_data*ui_geometries;}render_packet;

使用示例:

// 应用层准备 render packetrender_packet packet;packet.delta_time=delta_time;// 世界几何体 (3D 模型)geometry_render_data world_objects[10];world_objects[0].model=mat4_translation((vec3){0,0,-5});world_objects[0].geometry=&cube_geometry;// ...packet.geometry_count=10;packet.geometries=world_objects;// UI 几何体 (2D UI)geometry_render_data ui_elements[5];ui_elements[0].model=mat4_translation((vec3){100,100,0});// 屏幕坐标ui_elements[0].geometry=&button_geometry;// ...packet.ui_geometry_count=5;packet.ui_geometries=ui_elements;// 提交渲染renderer_draw_frame(&packet);

正交投影矩阵

正交投影的创建和特性:

// engine/src/math/kmath.h/** * @brief 创建正交投影矩阵 * @param left 左边界 * @param right 右边界 * @param bottom 下边界 * @param top 上边界 * @param near_clip 近裁剪面 * @param far_clip 远裁剪面 * @return 正交投影矩阵 */KINLINE mat4mat4_orthographic(f32 left,f32 right,f32 bottom,f32 top,f32 near_clip,f32 far_clip){mat4 out_matrix=mat4_identity();f32 lr=1.0f/(left-right);f32 bt=1.0f/(bottom-top);f32 nf=1.0f/(near_clip-far_clip);out_matrix.data[0]=-2.0f*lr;out_matrix.data[5]=-2.0f*bt;out_matrix.data[10]=2.0f*nf;out_matrix.data[12]=(left+right)*lr;out_matrix.data[13]=(top+bottom)*bt;out_matrix.data[14]=(far_clip+near_clip)*nf;returnout_matrix;}

UI 正交投影设置:

// engine/src/renderer/renderer_frontend.c// 创建 UI 正交投影// 左上角为 (0, 0),右下角为 (width, height)state_ptr->ui_projection=mat4_orthographic(0,// left1280.0f,// right (屏幕宽度)720.0f,// bottom (屏幕高度)0,// top-100.0f,// near_clip (允许 Z 值从 -100 到 +100)100.0f// far_clip);// UI 视图矩阵通常是单位矩阵 (无相机变换)state_ptr->ui_view=mat4_identity();

坐标映射:

正交投影映射: ┌─────────────────────────┐ │ 屏幕空间 (像素) │ │ (0, 0) ~ (1280, 720) │ └────────────┬────────────┘ │ mat4_orthographic() ▼ ┌─────────────────────────┐ │ NDC (归一化设备坐标) │ │ (-1, 1) ~ (1, -1) │ └─────────────────────────┘ 透视投影 vs 正交投影: ┌─────────────────────┐ ┌─────────────────────┐ │ 透视投影 │ │ 正交投影 │ │ │ │ │ │ ╱│╲ │ │ ││││││ │ │ ╱ │ ╲ │ │ ││││││ │ │ ╱ │ ╲ │ │ ││││││ │ │ ╱ │ ╲ │ │ ││││││ │ │ ╱────┼────╲ │ │ ││││││ │ │ ^ │ │ ^ │ │ 视锥体 │ │ 正交体 │ │ (远处变小) │ │ (大小不变) │ └─────────────────────┘ └─────────────────────┘

渲染顺序与Alpha混合

渲染顺序很重要,特别是对于透明物体:

// 正确的渲染顺序voidrenderer_draw_frame(render_packet*packet){backend.begin_frame(delta_time);// ========== 1. World Renderpass ==========// 先渲染 3D 世界backend.begin_renderpass(BUILTIN_RENDERPASS_WORLD);backend.update_global_world_state(projection,view,...);// 1a. 渲染不透明物体 (从前到后或任意顺序)for(u32 i=0;i<opaque_count;++i){backend.draw_geometry(opaque_geometries[i]);}// 1b. 渲染透明物体 (从后到前,启用 alpha 混合)for(u32 i=0;i<transparent_count;++i){backend.draw_geometry(transparent_geometries[i]);}backend.end_renderpass(BUILTIN_RENDERPASS_WORLD);// ========== 2. UI Renderpass ==========// 后渲染 2D UI (在 world 之上)backend.begin_renderpass(BUILTIN_RENDERPASS_UI);backend.update_global_ui_state(ui_projection,ui_view,0);// UI 通常从后到前绘制 (Painter's Algorithm)for(u32 i=0;i<ui_geometry_count;++i){backend.draw_geometry(ui_geometries[i]);}backend.end_renderpass(BUILTIN_RENDERPASS_UI);backend.end_frame(delta_time);}

Alpha 混合配置:

// Vulkan Pipeline 创建时配置 alpha 混合VkPipelineColorBlendAttachmentState color_blend_attachment_state;color_blend_attachment_state.blendEnable=VK_TRUE;// 启用混合// 标准 alpha 混合:// out_color = src_alpha * src_color + (1 - src_alpha) * dst_colorcolor_blend_attachment_state.srcColorBlendFactor=VK_BLEND_FACTOR_SRC_ALPHA;color_blend_attachment_state.dstColorBlendFactor=VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;color_blend_attachment_state.colorBlendOp=VK_BLEND_OP_ADD;color_blend_attachment_state.srcAlphaBlendFactor=VK_BLEND_FACTOR_ONE;color_blend_attachment_state.dstAlphaBlendFactor=VK_BLEND_FACTOR_ZERO;color_blend_attachment_state.alphaBlendOp=VK_BLEND_OP_ADD;

❓ 常见问题

1. 为什么 World Renderpass 使用离屏 Framebuffer?

原因:

  1. 后处理效果: 离屏渲染的结果可以作为纹理输入到后处理着色器 (如 bloom、blur、tone mapping)

    World → Framebuffer Texture → Post-Process Shader → UI → Screen
  2. 合成灵活性: UI 可以在 world 渲染结果之上合成,而不影响 world 渲染

    World Renderpass: 渲染到 world_framebuffers ↓ UI Renderpass: 在 swapchain framebuffer 上合成 world + UI
  3. 分辨率缩放: World 可以使用不同分辨率渲染 (如 4K 渲染到 1080p 显示)

    World: 3840x2160 → Downscale → UI: 1920x1080 → Screen
  4. 未来扩展: 为多个渲染通道做准备 (阴影、反射、GBuffer 等)

当前实现:

虽然当前 tutorial 还没有实现后处理,但离屏 framebuffer 为未来功能预留了空间。

2. 正交投影和透视投影有什么本质区别?

数学区别:

透视投影 (Perspective):

  • 模拟人眼视觉
  • 远处物体变小 (透视收缩)
  • 投影矩阵包含1/z
  • 用于 3D 场景
透视投影: 视点 ╲ ╲ 近处大 ╲ ╲ 远处小 ╲ ╲ ╲ 视锥体 投影矩阵 (简化): ┌ ┐ │ 1/tan(fov/2) 0 0 0 │ │ 0 aspect*.. 0 0 │ │ 0 0 f/(f-n) -1 │ │ 0 0 -n*f/(f-n) 0 │ └ ┘ 关键:第 3 行有 -1,产生 1/z 效果

正交投影 (Orthographic):

  • 平行投影
  • 物体大小不变
  • 无透视收缩
  • 用于 2D UI、工程图纸
正交投影: │││││││ │││││││ 所有物体 │││││││ 大小相同 │││││││ │││││││ 平行投影线 投影矩阵: ┌ ┐ │ 2/(r-l) 0 0 0 │ │ 0 2/(t-b) 0 0 │ │ 0 0 2/(f-n) 0 │ │ -(r+l)/(r-l) ... ... 1 │ └ ┘ 关键:无 1/z 项,只有线性缩放

视觉对比:

透视投影 (3D 游戏): ┌─────────────────┐ │ ╱────╲ │ 远处的立方体 │ │ │ │ 看起来更小 │ ╲────╱ │ │ │ │ ╱──────╲ │ 近处的立方体 │ │ │ │ 看起来更大 │ ╲──────╱ │ └─────────────────┘ 正交投影 (UI): ┌─────────────────┐ │ ╱────╲ │ 所有按钮 │ │ BTN1 │ │ 大小相同 │ ╲────╱ │ │ │ │ ╱────╲ │ 不管深度 │ │ BTN2 │ │ 如何 │ ╲────╱ │ └─────────────────┘
3. 为什么 UI 纹理坐标要翻转 Y 轴?

问题根源:

OpenGL/Vulkan 和屏幕坐标系统有不同的原点:

Vulkan 纹理坐标 (默认): (0,0) ────► u │ │ ▼ v (1,1) 屏幕坐标: (0,0) ────► x │ │ ▼ y (width, height) 期望的 UI 坐标: (0,0) ────► x │ │ ▼ y (width, height)

解决方案:

有两种方式可以翻转:

方式 1: 翻转纹理坐标 (Kohi 使用)

// UI 顶点着色器 out_dto.tex_coord = vec2(in_texcoord.x, 1.0 - in_texcoord.y);

方式 2: 翻转正交投影矩阵

// 交换 top 和 bottommat4 ui_projection=mat4_orthographic(0,// leftwidth,// rightheight,// bottom ← 本应是 top0,// top ← 本应是 bottom-100.0f,100.0f);

Kohi 使用两种方式结合:

  • 翻转正交矩阵 (bottom/top 交换)
  • 翻转纹理坐标 (1.0 - y)

为什么要这样做?

最终效果:纹理图像正确显示,UI 元素的 (0, 0) 在左上角。

如果不翻转: ┌─────────────┐ │ ▼ │ ← 纹理上下颠倒 │ BUTTON │ │ │ └─────────────┘ 翻转后: ┌─────────────┐ │ BUTTON │ ← 纹理正确 │ ▲ │ │ │ └─────────────┘
4. 如何在 UI Renderpass 中显示 World Renderpass 的渲染结果?

当前实现 (Tutorial 34):

当前还没有实现 world 渲染结果的采样,两个 renderpass 是独立的。

未来实现 (后续教程):

需要将 world_framebuffers 的 color attachment 作为纹理传递给 UI renderpass:

// 1. 创建 world framebuffer 时,使其 color attachment 可采样VkImageCreateInfo image_create_info;image_create_info.usage=VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT|VK_IMAGE_USAGE_SAMPLED_BIT;// ← 允许作为纹理采样// 2. 在 UI renderpass 开始前,转换 image layoutvkCmdPipelineBarrier(command_buffer,VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,0,0,nullptr,0,nullptr,1,&image_memory_barrier);// 3. 在 UI 着色器中采样 world 渲染结果layout(set=0,binding=2)uniform sampler2D world_texture;voidmain(){vec4 world_color=texture(world_texture,screen_uv);vec4 ui_color=texture(ui_sampler,tex_coord);// 合成 world + UIout_color=mix(world_color,ui_color,ui_color.a);}

使用场景:

  • 后处理效果 (bloom、模糊、色调映射)
  • 小地图 (将 world 渲染结果显示在 UI 角落)
  • 相机监控画面
5. 多个 Renderpass 会影响性能吗?

性能影响:

优点:

  1. Tile-based GPU 优化: 移动 GPU 可以为每个 renderpass 优化 tile memory
  2. Clear 操作优化: 每个 renderpass 可以高效清除 framebuffer
  3. Bandwidth 优化: 避免不必要的 framebuffer 读写

缺点:

  1. Renderpass 切换开销: 每次切换有一定的 GPU 开销 (但通常很小)
  2. 内存占用: 离屏 framebuffer 占用额外显存

性能测试 (典型场景):

单一 Renderpass (baseline): - Frame time: 16.6 ms (60 FPS) - GPU memory: 100 MB 多 Renderpass (world + UI): - Frame time: 16.8 ms (~60 FPS) ← 增加 ~1% - GPU memory: 120 MB ← 增加 20 MB (离屏 framebuffer) 结论:性能影响很小,收益 (代码清晰度、可扩展性) 远大于成本

优化建议:

  1. 合并同类 Renderpass: 如果两个 renderpass 使用相同配置,考虑合并
  2. Lazy Transition: 只在必要时转换 image layout
  3. Framebuffer 复用: 多个 renderpass 可以共享 depth buffer

📝 练习

练习 1: 添加 Debug Renderpass

任务:添加第三个 renderpass 用于调试可视化 (如碰撞盒、法线、网格)。

// 1. 添加新的 renderpass 枚举typedefenumbuiltin_renderpass{BUILTIN_RENDERPASS_WORLD=0x01,BUILTIN_RENDERPASS_UI=0x02,BUILTIN_RENDERPASS_DEBUG=0x04// ← 新增}builtin_renderpass;// 2. 创建 debug renderpassvulkan_renderpass debug_renderpass;vulkan_renderpass_create(&context,&debug_renderpass,...);// 3. 创建 debug shader (简单的线框着色器)vulkan_debug_shader debug_shader;vulkan_debug_shader_create(&context,&debug_shader);// 4. 在渲染流程中添加 debug renderpassvoidrenderer_draw_frame(render_packet*packet){backend.begin_frame(delta_time);// World renderpass// ...// UI renderpass// ...// Debug renderpass (绘制在 UI 之上)if(debug_mode_enabled){backend.begin_renderpass(BUILTIN_RENDERPASS_DEBUG);backend.update_global_debug_state(projection,view);// 绘制调试几何体 (碰撞盒、法线等)for(u32 i=0;i<debug_geometry_count;++i){backend.draw_geometry(debug_geometries[i]);}backend.end_renderpass(BUILTIN_RENDERPASS_DEBUG);}backend.end_frame(delta_time);}

要求:

  • Debug 着色器使用线框模式 (VK_POLYGON_MODE_LINE)
  • 可以通过按键切换 debug 模式开关
  • 绘制碰撞盒、坐标轴、法线向量
练习 2: 实现后处理 Renderpass

任务:添加后处理 renderpass,对 world 渲染结果应用灰度滤镜。

// 1. 创建后处理 framebuffer (全屏 quad)geometry fullscreen_quad;create_fullscreen_quad(&fullscreen_quad);// 2. 创建后处理着色器// post_process.vert.glsl#version450layout(location=0)in vec2 in_position;// 全屏四边形 [-1,1]layout(location=1)in vec2 in_texcoord;layout(location=0)out vec2 out_texcoord;voidmain(){out_texcoord=in_texcoord;gl_Position=vec4(in_position,0.0,1.0);}// post_process.frag.glsl#version450layout(location=0)in vec2 in_texcoord;layout(location=0)out vec4 out_color;layout(set=0,binding=0)uniform sampler2D scene_texture;// world 渲染结果voidmain(){vec3 color=texture(scene_texture,in_texcoord).rgb;// 灰度滤镜floatgray=dot(color,vec3(0.299,0.587,0.114));out_color=vec4(vec3(gray),1.0);}// 3. 修改渲染流程voidrenderer_draw_frame(render_packet*packet){backend.begin_frame(delta_time);// World renderpass → 渲染到离屏 texturebackend.begin_renderpass(BUILTIN_RENDERPASS_WORLD);// ... 绘制世界 ...backend.end_renderpass(BUILTIN_RENDERPASS_WORLD);// Post-process renderpass → 采样 world texture,应用滤镜backend.begin_renderpass(BUILTIN_RENDERPASS_POST_PROCESS);bind_texture(world_framebuffer_texture);draw_fullscreen_quad();backend.end_renderpass(BUILTIN_RENDERPASS_POST_PROCESS);// UI renderpass → 在后处理结果上绘制 UIbackend.begin_renderpass(BUILTIN_RENDERPASS_UI);// ... 绘制 UI ...backend.end_renderpass(BUILTIN_RENDERPASS_UI);backend.end_frame(delta_time);}
练习 3: UI 深度分层

任务:实现 UI 元素的深度分层,通过 Z 坐标控制绘制顺序。

// 1. 修改 UI 几何体的 Z 坐标geometry_render_data ui_elements[10];// 背景图片 (Z = 0)ui_elements[0].model=mat4_translation((vec3){0,0,0});ui_elements[0].geometry=&background;// 按钮 (Z = 10)ui_elements[1].model=mat4_translation((vec3){100,100,10});ui_elements[1].geometry=&button;// 文本 (Z = 20,最上层)ui_elements[2].model=mat4_translation((vec3){100,100,20});ui_elements[2].geometry=&text;// 2. 启用 UI renderpass 的深度测试VkPipelineDepthStencilStateCreateInfo depth_stencil;depth_stencil.depthTestEnable=VK_TRUE;// 启用深度测试depth_stencil.depthWriteEnable=VK_TRUE;// 写入深度depth_stencil.depthCompareOp=VK_COMPARE_OP_LESS;// 近的覆盖远的// 3. 调整正交投影的深度范围mat4 ui_projection=mat4_orthographic(0,width,height,0,-100.0f,// near: 允许 Z 从 -100 到 100100.0f// far);// 4. 测试深度分层// 应该看到:background → button → text 的正确遮挡关系

要求:

  • UI 元素可以通过 Z 坐标控制绘制顺序
  • 相同 Z 值的元素,后绘制的在上面
  • 支持负 Z 值 (如背景可以用 Z = -50)

恭喜!你已经掌握了多渲染通道架构!🎉


关注公众号「上手实验室」,获取更多游戏引擎开发教程!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/25 20:00:24

大模型应用开发(十六)_RAG概述

4. RAG概述提高大语言模型(LLM)回答的准确性和一致性通常有三种方式&#xff1a;prompt优化&#xff1a;prompt优化可以在文本生成和行为控制上提供初步帮助。RAG(检索增强生成)&#xff1a;RAG侧重补充训练数据中没有的或过时的信息fine-tuning(微调)&#xff1a;fine-tuning则…

作者头像 李华
网站建设 2026/5/7 20:40:03

Linux系统编程——进程进阶:exec 族、system 与工作路径操作

一、exec族函数核心功能&#xff1a;执行本地任意可执行文件&#xff0c;是进程代码替换的核心工具。典型搭配&#xff1a;常和 fork() 配合。让子进程执行 exec&#xff0c;避免父进程自身代码被替换。内存逻辑&#xff1a;执行 exec 后&#xff0c;原进程的代码段会被新程序完…

作者头像 李华
网站建设 2026/5/2 12:02:45

西门子 S7 - 1200 智能仓库组态仿真全解析

西门子S7-1200智能仓库组态仿真&#xff0c;博途自动化仓库&#xff0c;S7-1200自动化仓库控制系统&#xff0c;组态仿真 包括&#xff1a;西门子S7-1200PLCwincc组态仿真&#xff0c;IO表&#xff0c;接线图&#xff0c;报告等在自动化控制领域&#xff0c;西门子 S7 - 1200 系…

作者头像 李华
网站建设 2026/5/2 23:05:06

上传文件出现“ 413 Request Entity Too Large“错误

今天上传文件的时候提示“ 413 Request Entity Too Large"&#xff0c;HTTP 413错误表示请求体大于服务器允许的最大大小。这个限制可以由服务器配置&#xff08;如Nginx、Apache等&#xff09;或应用自身&#xff08;如Java、Node.js等&#xff09;来控制。在Nginx中&am…

作者头像 李华
网站建设 2026/5/4 4:02:49

CSS样式初识:给网页穿上漂亮的“外衣”

文章目录前言一、CSS是什么&#xff1f;二、CSS的核心作用三、CSS的3种引入方式内联样式&#xff08;行内样式&#xff09;内部样式表外部样式表总结前言 HTML就像搭建好的房屋框架&#xff0c;而CSS就是给房屋装修、刷漆、布置格局的“魔法师”。今天这篇文章&#xff0c;就带…

作者头像 李华