你喜欢的公司网站,南京百度网站推广,dede学校网站模板下载,小企业网站建设怎么做好上一篇#xff1a;资源系统 | 下一篇#xff1a;在UI渲染通道中绘制 | 返回目录 #x1f4da; 快速导航 目录
简介学习目标多渲染通道架构 为什么需要多个RenderpassWorld与UI分离渲染流程 Renderpass枚举UI着色器实现 UI顶点着色器UI片段着色器2D vs 3D坐标系 Framebuffer…上一篇资源系统 | 下一篇在UI渲染通道中绘制 | 返回目录 快速导航目录简介学习目标多渲染通道架构为什么需要多个RenderpassWorld与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 RenderpassUIUpdate Global State正交投影Draw UI Geometries绘制2D UIEnd RenderpassUIBegin RenderpassWORLDUpdate Global State透视投影 视图矩阵Draw World Geometries绘制3D几何体End RenderpassWORLD 学习目标目标描述理解多Renderpass架构了解为什么需要多个渲染通道实现UI着色器创建专门用于2D UI渲染的着色器掌握正交投影理解正交投影与透视投影的区别Framebuffer分层理解离屏渲染和最终显示的framebufferRenderpass切换实现多个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 projectionmat4_perspective(deg_to_rad(45.0f),aspect_ratio,0.1f,1000.0f);mat4 viewcamera_get_view_matrix();backend.update_global_world_state(projection,view,camera_position,ambient_color,0);// 4. 绘制世界几何体for(u32 i0;iworld_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_projectionmat4_orthographic(0,screen_width,screen_height,0,-100.0f,100.0f);mat4 ui_viewmat4_identity();backend.update_global_ui_state(ui_projection,ui_view,0);// 8. 绘制 UI 几何体for(u32 i0;iui_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_WORLD0x01,// 世界渲染通道BUILTIN_RENDERPASS_UI0x02// UI 渲染通道}builtin_renderpass;使用位标志的好处:// 可以用位运算组合 renderpassu8 renderpass_maskBUILTIN_RENDERPASS_WORLD|BUILTIN_RENDERPASS_UI;// 检查是否包含某个 renderpassif(renderpass_maskBUILTIN_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?后处理准备: 离屏渲染的结果可以作为纹理输入到后处理着色器合成灵活性: UI 可以在 world 渲染结果之上合成分辨率独立: World 渲染可以使用不同的分辨率 (如 upscaling/downscaling)未来扩展: 为阴影贴图、反射等高级效果做准备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*renderpass0;VkFramebuffer framebuffer0;vulkan_command_buffer*command_buffercontext.graphics_command_buffers[context.image_index];// 1. 根据 ID 选择 renderpass 和 framebufferswitch(renderpass_id){caseBUILTIN_RENDERPASS_WORLD:renderpasscontext.main_renderpass;framebuffercontext.world_framebuffers[context.image_index];break;caseBUILTIN_RENDERPASS_UI:renderpasscontext.ui_renderpass;framebuffercontext.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*renderpass0;vulkan_command_buffer*command_buffercontext.graphics_command_buffers[context.image_index];// 1. 根据 ID 选择 renderpassswitch(renderpass_id){caseBUILTIN_RENDERPASS_WORLD:renderpasscontext.main_renderpass;break;caseBUILTIN_RENDERPASS_UI:renderpasscontext.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_indexcontext-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_indexcontext-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_buffercontext.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.projectionprojection;context.material_shader.global_ubo.viewview;// TODO: 其他全局状态 (ambient_colour, view_position, etc.)}voidvulkan_renderer_update_global_ui_state(mat4 projection,mat4 view,i32 mode){vulkan_command_buffer*command_buffercontext.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.projectionprojection;context.ui_shader.global_ubo.viewview;}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_timedelta_time;// 世界几何体 (3D 模型)geometry_render_data world_objects[10];world_objects[0].modelmat4_translation((vec3){0,0,-5});world_objects[0].geometrycube_geometry;// ...packet.geometry_count10;packet.geometriesworld_objects;// UI 几何体 (2D UI)geometry_render_data ui_elements[5];ui_elements[0].modelmat4_translation((vec3){100,100,0});// 屏幕坐标ui_elements[0].geometrybutton_geometry;// ...packet.ui_geometry_count5;packet.ui_geometriesui_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_matrixmat4_identity();f32 lr1.0f/(left-right);f32 bt1.0f/(bottom-top);f32 nf1.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](leftright)*lr;out_matrix.data[13](topbottom)*bt;out_matrix.data[14](far_clipnear_clip)*nf;returnout_matrix;}UI 正交投影设置:// engine/src/renderer/renderer_frontend.c// 创建 UI 正交投影// 左上角为 (0, 0),右下角为 (width, height)state_ptr-ui_projectionmat4_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_viewmat4_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 i0;iopaque_count;i){backend.draw_geometry(opaque_geometries[i]);}// 1b. 渲染透明物体 (从后到前,启用 alpha 混合)for(u32 i0;itransparent_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 通常从后到前绘制 (Painters Algorithm)for(u32 i0;iui_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.blendEnableVK_TRUE;// 启用混合// 标准 alpha 混合:// out_color src_alpha * src_color (1 - src_alpha) * dst_colorcolor_blend_attachment_state.srcColorBlendFactorVK_BLEND_FACTOR_SRC_ALPHA;color_blend_attachment_state.dstColorBlendFactorVK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;color_blend_attachment_state.colorBlendOpVK_BLEND_OP_ADD;color_blend_attachment_state.srcAlphaBlendFactorVK_BLEND_FACTOR_ONE;color_blend_attachment_state.dstAlphaBlendFactorVK_BLEND_FACTOR_ZERO;color_blend_attachment_state.alphaBlendOpVK_BLEND_OP_ADD;❓ 常见问题1. 为什么 World Renderpass 使用离屏 Framebuffer?原因:后处理效果: 离屏渲染的结果可以作为纹理输入到后处理着色器 (如 bloom、blur、tone mapping)World → Framebuffer Texture → Post-Process Shader → UI → Screen合成灵活性: UI 可以在 world 渲染结果之上合成,而不影响 world 渲染World Renderpass: 渲染到 world_framebuffers ↓ UI Renderpass: 在 swapchain framebuffer 上合成 world UI分辨率缩放: World 可以使用不同分辨率渲染 (如 4K 渲染到 1080p 显示)World: 3840x2160 → Downscale → UI: 1920x1080 → Screen未来扩展: 为多个渲染通道做准备 (阴影、反射、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 │ │ -(rl)/(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_projectionmat4_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.usageVK_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(set0,binding2)uniform sampler2D world_texture;voidmain(){vec4 world_colortexture(world_texture,screen_uv);vec4 ui_colortexture(ui_sampler,tex_coord);// 合成 world UIout_colormix(world_color,ui_color,ui_color.a);}使用场景:后处理效果 (bloom、模糊、色调映射)小地图 (将 world 渲染结果显示在 UI 角落)相机监控画面5. 多个 Renderpass 会影响性能吗?性能影响:优点:Tile-based GPU 优化: 移动 GPU 可以为每个 renderpass 优化 tile memoryClear 操作优化: 每个 renderpass 可以高效清除 framebufferBandwidth 优化: 避免不必要的 framebuffer 读写缺点:Renderpass 切换开销: 每次切换有一定的 GPU 开销 (但通常很小)内存占用: 离屏 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) 结论:性能影响很小,收益 (代码清晰度、可扩展性) 远大于成本优化建议:合并同类 Renderpass: 如果两个 renderpass 使用相同配置,考虑合并Lazy Transition: 只在必要时转换 image layoutFramebuffer 复用: 多个 renderpass 可以共享 depth buffer 练习练习 1: 添加 Debug Renderpass任务:添加第三个 renderpass 用于调试可视化 (如碰撞盒、法线、网格)。// 1. 添加新的 renderpass 枚举typedefenumbuiltin_renderpass{BUILTIN_RENDERPASS_WORLD0x01,BUILTIN_RENDERPASS_UI0x02,BUILTIN_RENDERPASS_DEBUG0x04// ← 新增}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 i0;idebug_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(location0)in vec2 in_position;// 全屏四边形 [-1,1]layout(location1)in vec2 in_texcoord;layout(location0)out vec2 out_texcoord;voidmain(){out_texcoordin_texcoord;gl_Positionvec4(in_position,0.0,1.0);}// post_process.frag.glsl#version450layout(location0)in vec2 in_texcoord;layout(location0)out vec4 out_color;layout(set0,binding0)uniform sampler2D scene_texture;// world 渲染结果voidmain(){vec3 colortexture(scene_texture,in_texcoord).rgb;// 灰度滤镜floatgraydot(color,vec3(0.299,0.587,0.114));out_colorvec4(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].modelmat4_translation((vec3){0,0,0});ui_elements[0].geometrybackground;// 按钮 (Z 10)ui_elements[1].modelmat4_translation((vec3){100,100,10});ui_elements[1].geometrybutton;// 文本 (Z 20,最上层)ui_elements[2].modelmat4_translation((vec3){100,100,20});ui_elements[2].geometrytext;// 2. 启用 UI renderpass 的深度测试VkPipelineDepthStencilStateCreateInfo depth_stencil;depth_stencil.depthTestEnableVK_TRUE;// 启用深度测试depth_stencil.depthWriteEnableVK_TRUE;// 写入深度depth_stencil.depthCompareOpVK_COMPARE_OP_LESS;// 近的覆盖远的// 3. 调整正交投影的深度范围mat4 ui_projectionmat4_orthographic(0,width,height,0,-100.0f,// near: 允许 Z 从 -100 到 100100.0f// far);// 4. 测试深度分层// 应该看到:background → button → text 的正确遮挡关系要求:UI 元素可以通过 Z 坐标控制绘制顺序相同 Z 值的元素,后绘制的在上面支持负 Z 值 (如背景可以用 Z -50)恭喜!你已经掌握了多渲染通道架构!关注公众号「上手实验室」,获取更多游戏引擎开发教程!