news 2025/12/26 10:05:56

教程 35 - 在UI渲染通道中绘制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
教程 35 - 在UI渲染通道中绘制

上一篇:多渲染通道 | 下一篇:方向光照 | 返回目录


📚 快速导航


目录
  • 简介
  • 学习目标
  • 2D顶点格式
    • vertex_2d定义
    • 与vertex_3d的区别
    • 顶点布局对比
  • 材质类型扩展
    • 材质类型枚举
    • UI材质配置
    • 材质加载器更新
  • 泛型几何体创建
    • 接口修改
    • 顶点大小参数
    • Vulkan后端适配
  • 默认2D几何体
    • 创建默认2D Quad
    • 2D坐标系统
  • UI几何体创建
    • 定义UI顶点
    • 几何体配置
    • 从配置获取几何体
  • 渲染流程集成
    • 准备Render Packet
    • UI几何体绘制
  • 坐标空间转换
  • 常见问题
  • 练习

📖 简介

在上一教程中,我们实现了多渲染通道架构,将 World Renderpass 和 UI Renderpass 分离。现在我们将实现在 UI Renderpass 中实际绘制 2D UI 元素。

要在 UI Renderpass 中绘制,我们需要:

  • 2D 顶点格式(vertex_2d):只包含位置和纹理坐标的简化顶点
  • UI 材质类型:区分 3D 世界材质和 2D UI 材质
  • 泛型几何体创建:支持不同顶点格式的统一接口
  • 默认 2D 几何体:用于测试的基础 2D quad
Renderpasses 渲染通道
Geometry Creation 几何体创建
Material Types 材质类型
Vertex Formats 顶点格式
World Renderpass
世界渲染
UI Renderpass
UI渲染
create_geometry
泛型接口
MATERIAL_TYPE_WORLD
世界材质
MATERIAL_TYPE_UI
UI材质
vertex_3d
3D顶点
vertex_2d
2D顶点

🎯 学习目标

目标描述
定义2D顶点格式实现简化的 vertex_2d 结构
扩展材质类型区分 WORLD 和 UI 材质
泛型几何体创建支持多种顶点格式的统一接口
创建默认2D几何体实现默认的 2D quad
UI几何体渲染在 UI renderpass 中绘制 2D 几何体

📐 2D顶点格式

vertex_2d定义

2D UI 元素不需要 3D 渲染的所有数据:

// engine/src/math/math_types.h/** * @brief 3D 顶点结构 (用于世界几何体) */typedefstructvertex_3d{vec3 position;// 3D 位置 (12 bytes)vec2 texcoord;// 纹理坐标 (8 bytes)vec3 normal;// 法线 (12 bytes)vec3 tangent;// 切线 (12 bytes)// 总共: 44 bytes}vertex_3d;/** * @brief 2D 顶点结构 (用于 UI 几何体) */typedefstructvertex_2d{vec2 position;// 2D 位置 (8 bytes)vec2 texcoord;// 纹理坐标 (8 bytes)// 总共: 16 bytes}vertex_2d;

与vertex_3d的区别

对比两种顶点格式:

属性vertex_3dvertex_2d说明
positionvec3(x, y, z)vec2(x, y)2D 只需 x, y
texcoordvec2vec2纹理坐标相同
normalvec3UI 不需要法线
tangentvec3UI 不需要切线
大小44 bytes16 bytes2D 节省 64% 内存

顶点布局对比

vertex_3d 内存布局 (44 bytes): ┌─────────────────────────────────────────────┐ │ position (vec3) │ 12 bytes │ ├────────────────────────┼────────────────────┤ │ texcoord (vec2) │ 8 bytes │ ├────────────────────────┼────────────────────┤ │ normal (vec3) │ 12 bytes │ ├────────────────────────┼────────────────────┤ │ tangent (vec3) │ 12 bytes │ └─────────────────────────────────────────────┘ vertex_2d 内存布局 (16 bytes): ┌─────────────────────────────────────────────┐ │ position (vec2) │ 8 bytes │ ├────────────────────────┼────────────────────┤ │ texcoord (vec2) │ 8 bytes │ └─────────────────────────────────────────────┘ 优势: ✓ 更小的顶点缓冲区 ✓ 更少的带宽占用 ✓ 更快的顶点着色器 ✓ 简化的顶点属性设置

Vulkan 顶点输入描述:

// 3D 顶点输入VkVertexInputAttributeDescription vertex_3d_attributes[4];vertex_3d_attributes[0].location=0;vertex_3d_attributes[0].format=VK_FORMAT_R32G32B32_SFLOAT;// position (vec3)vertex_3d_attributes[0].offset=offsetof(vertex_3d,position);vertex_3d_attributes[1].location=1;vertex_3d_attributes[1].format=VK_FORMAT_R32G32_SFLOAT;// texcoord (vec2)vertex_3d_attributes[1].offset=offsetof(vertex_3d,texcoord);vertex_3d_attributes[2].location=2;vertex_3d_attributes[2].format=VK_FORMAT_R32G32B32_SFLOAT;// normal (vec3)vertex_3d_attributes[2].offset=offsetof(vertex_3d,normal);vertex_3d_attributes[3].location=3;vertex_3d_attributes[3].format=VK_FORMAT_R32G32B32_SFLOAT;// tangent (vec3)vertex_3d_attributes[3].offset=offsetof(vertex_3d,tangent);// 2D 顶点输入 (简化)VkVertexInputAttributeDescription vertex_2d_attributes[2];vertex_2d_attributes[0].location=0;vertex_2d_attributes[0].format=VK_FORMAT_R32G32_SFLOAT;// position (vec2)vertex_2d_attributes[0].offset=offsetof(vertex_2d,position);vertex_2d_attributes[1].location=1;vertex_2d_attributes[1].format=VK_FORMAT_R32G32_SFLOAT;// texcoord (vec2)vertex_2d_attributes[1].offset=offsetof(vertex_2d,texcoord);

🎨 材质类型扩展

材质类型枚举

为了区分 3D 世界材质和 2D UI 材质,我们添加了材质类型枚举:

// engine/src/resources/resource_types.h/** * @brief 材质类型 */typedefenummaterial_type{MATERIAL_TYPE_WORLD,// 世界材质 (3D)MATERIAL_TYPE_UI// UI 材质 (2D)}material_type;typedefstructmaterial{u32 id;u32 generation;u32 internal_id;material_type type;// ← 新增:材质类型charname[MATERIAL_NAME_MAX_LENGTH];vec4 diffuse_colour;texture_map diffuse_map;}material;typedefstructmaterial_config{charname[MATERIAL_NAME_MAX_LENGTH];material_type type;// ← 新增:材质类型b8 auto_release;vec4 diffuse_colour;chardiffuse_map_name[TEXTURE_NAME_MAX_LENGTH];}material_config;

UI材质配置

UI 材质配置文件示例:

# assets/materials/test_ui_material.kmt # 材质版本 version=0.1 # 材质名称 name=test_ui_material # 漫反射颜色 (RGBA) diffuse_colour=1.0 1.0 1.0 1.0 # 漫反射贴图名称 diffuse_map_name=orange_lines_512 # 材质类型:ui 或 world type=ui

对比世界材质和 UI 材质:

世界材质 (test_material.kmt): version=0.1 name=test_material diffuse_colour=1.0 1.0 1.0 1.0 diffuse_map_name=paving type=world ← 3D 世界材质 UI 材质 (test_ui_material.kmt): version=0.1 name=test_ui_material diffuse_colour=1.0 1.0 1.0 1.0 diffuse_map_name=orange_lines_512 type=ui ← 2D UI 材质

材质加载器更新

材质加载器需要解析type字段:

// engine/src/resources/loaders/material_loader.cstaticb8material_loader_load(resource_loader*self,constchar*name,resource*out_resource){// ... 打开文件、读取配置 ...// 设置默认值material_config*resource_data=kallocate(sizeof(material_config),MEMORY_TAG_MATERIAL);resource_data->type=MATERIAL_TYPE_WORLD;// 默认为世界材质resource_data->auto_release=false;string_ncopy(resource_data->name,name,MATERIAL_NAME_MAX_LENGTH);resource_data->diffuse_colour=vec4_one();kzero_memory(resource_data->diffuse_map_name,TEXTURE_NAME_MAX_LENGTH);// 逐行解析while(filesystem_read_line(&f,511,&p,&line_length)){char*trimmed=string_trim(line_buf);// 跳过注释和空行if(line_length<1||trimmed[0]=='#'){continue;}// 解析 key=valuei32 equal_index=string_index_of(trimmed,'=');if(equal_index==-1){continue;}charkey[64]="";string_mid(key,trimmed,0,equal_index);char*key_trimmed=string_trim(key);charvalue[512]="";string_mid(value,trimmed,equal_index+1,-1);char*value_trimmed=string_trim(value);// 解析具体字段if(strings_equali(key_trimmed,"version")){// 版本号}elseif(strings_equali(key_trimmed,"name")){string_ncopy(resource_data->name,value_trimmed,MATERIAL_NAME_MAX_LENGTH);}elseif(strings_equali(key_trimmed,"diffuse_map_name")){string_ncopy(resource_data->diffuse_map_name,value_trimmed,TEXTURE_NAME_MAX_LENGTH);}elseif(strings_equali(key_trimmed,"diffuse_colour")){if(!string_to_vec4(value_trimmed,&resource_data->diffuse_colour)){KWARN("Failed to parse diffuse_colour");}}elseif(strings_equali(key_trimmed,"type")){// ========== 新增:解析材质类型 ==========if(strings_equali(value_trimmed,"ui")){resource_data->type=MATERIAL_TYPE_UI;}elseif(strings_equali(value_trimmed,"world")){resource_data->type=MATERIAL_TYPE_WORLD;}else{KWARN("Unknown material type '%s', defaulting to 'world'",value_trimmed);resource_data->type=MATERIAL_TYPE_WORLD;}}}filesystem_close(&f);// 填充 resourceout_resource->full_path=string_duplicate(full_path);out_resource->name=name;out_resource->data=resource_data;out_resource->data_size=sizeof(material_config);out_resource->loader_id=self->id;returntrue;}

🔧 泛型几何体创建

接口修改

为了支持不同的顶点格式 (vertex_3d, vertex_2d),我们将create_geometry接口改为泛型:

// engine/src/renderer/renderer_types.inltypedefstructrenderer_backend{// ... 其他函数 ...// ========== 旧接口 (只支持 vertex_3d) ==========// b8 (*create_geometry)(geometry* geometry, u32 vertex_count, const vertex_3d* vertices, u32 index_count, const u32* indices);// ========== 新接口 (支持任意顶点格式) ==========b8(*create_geometry)(geometry*geometry,u32 vertex_size,// ← 新增:顶点大小 (字节)u32 vertex_count,// 顶点数量constvoid*vertices,// ← 改为 void* (泛型指针)u32 index_size,// ← 新增:索引大小 (字节)u32 index_count,// 索引数量constvoid*indices// ← 改为 void* (泛型指针));void(*destroy_geometry)(geometry*geometry);}renderer_backend;

顶点大小参数

为什么需要vertex_size参数?

// 使用旧接口 (固定类型)vertex_3d vertices[4];// ...backend.create_geometry(&geometry,4,vertices,6,indices);// 问题:只能传递 vertex_3d*,无法支持其他顶点格式// 使用新接口 (泛型)vertex_2d ui_vertices[4];// ...backend.create_geometry(&geometry,sizeof(vertex_2d),// ← 明确告诉后端顶点大小4,ui_vertices,// ← void* 可以接受任意类型sizeof(u32),6,indices);vertex_3d world_vertices[100];// ...backend.create_geometry(&geometry,sizeof(vertex_3d),// ← 不同的顶点大小100,world_vertices,sizeof(u32),300,indices);

优势:

泛型接口的优势: ┌────────────────────────────────────┐ │ Renderer Backend (渲染后端) │ │ │ │ create_geometry(vertex_size, ...) │ └─────────────┬──────────────────────┘ │ │ 接受任意顶点格式 │ ┌─────────┼─────────┐ │ │ │ ▼ ▼ ▼ ┌───────┐ ┌───────┐ ┌───────┐ │vertex │ │vertex │ │custom │ │_3d │ │_2d │ │_vertex│ └───────┘ └───────┘ └───────┘ 44 bytes 16 bytes 任意大小 后端计算缓冲区大小: buffer_size = vertex_size * vertex_count

Vulkan后端适配

Vulkan 后端的实现:

// engine/src/renderer/vulkan/vulkan_backend.cb8vulkan_renderer_create_geometry(geometry*geometry,u32 vertex_size,u32 vertex_count,constvoid*vertices,u32 index_size,u32 index_count,constvoid*indices){if(!vertex_count||!vertices){KERROR("vulkan_renderer_create_geometry requires vertex data, and none was supplied. vertex_count=%d, vertices=%p",vertex_count,vertices);returnfalse;}// 检查是否有索引数据b8 is_indexed=index_count>0&&indices!=0;// 计算缓冲区大小u64 vertex_buffer_size=vertex_size*vertex_count;u64 index_buffer_size=is_indexed?(index_size*index_count):0;// 存储几何体数据vulkan_geometry_data*internal_data=&context.geometries[geometry->internal_id];internal_data->id=geometry->id;internal_data->generation=geometry->generation;internal_data->vertex_count=vertex_count;internal_data->vertex_size=vertex_size;// ← 保存顶点大小internal_data->vertex_buffer_offset=context.geometry_vertex_offset;internal_data->index_count=index_count;internal_data->index_size=index_size;// ← 保存索引大小internal_data->index_buffer_offset=context.geometry_index_offset;// 上传顶点数据到 GPUvulkan_buffer_load_range(&context,&context.object_vertex_buffer,internal_data->vertex_buffer_offset,vertex_buffer_size,vertices// ← void* 可以接受任意类型);// 上传索引数据到 GPUif(is_indexed){vulkan_buffer_load_range(&context,&context.object_index_buffer,internal_data->index_buffer_offset,index_buffer_size,indices);}// 更新偏移量context.geometry_vertex_offset+=vertex_buffer_size;if(is_indexed){context.geometry_index_offset+=index_buffer_size;}returntrue;}

📦 默认2D几何体

创建默认2D Quad

几何体系统创建两个默认几何体:3D quad 和 2D quad。

// engine/src/systems/geometry_system.ctypedefstructgeometry_system_state{geometry_system_config config;geometry default_geometry;// 3D 默认几何体geometry default_2d_geometry;// ← 新增:2D 默认几何体geometry_reference*registered_geometries;}geometry_system_state;b8create_default_geometries(geometry_system_state*state){// ========== 创建默认 3D 几何体 ==========vertex_3d verts[4];kzero_memory(verts,sizeof(vertex_3d)*4);constf32 f=10.0f;verts[0].position.x=-0.5*f;// 0 3verts[0].position.y=-0.5*f;//verts[0].texcoord.x=0.0f;//verts[0].texcoord.y=0.0f;// 2 1verts[1].position.y=0.5*f;verts[1].position.x=0.5*f;verts[1].texcoord.x=1.0f;verts[1].texcoord.y=1.0f;verts[2].position.x=-0.5*f;verts[2].position.y=0.5*f;verts[2].texcoord.x=0.0f;verts[2].texcoord.y=1.0f;verts[3].position.x=0.5*f;verts[3].position.y=-0.5*f;verts[3].texcoord.x=1.0f;verts[3].texcoord.y=0.0f;u32 indices[6]={0,1,2,0,3,1};// 上传到 GPU (使用泛型接口)if(!renderer_create_geometry(&state->default_geometry,sizeof(vertex_3d),// ← 顶点大小4,verts,sizeof(u32),6,indices)){KFATAL("Failed to create default geometry. Application cannot continue.");returnfalse;}state->default_geometry.material=material_system_get_default();// ========== 创建默认 2D 几何体 ==========vertex_2d verts2d[4];kzero_memory(verts2d,sizeof(vertex_2d)*4);verts2d[0].position.x=-0.5*f;// 0 3verts2d[0].position.y=-0.5*f;//verts2d[0].texcoord.x=0.0f;//verts2d[0].texcoord.y=0.0f;// 2 1verts2d[1].position.y=0.5*f;verts2d[1].position.x=0.5*f;verts2d[1].texcoord.x=1.0f;verts2d[1].texcoord.y=1.0f;verts2d[2].position.x=-0.5*f;verts2d[2].position.y=0.5*f;verts2d[2].texcoord.x=0.0f;verts2d[2].texcoord.y=1.0f;verts2d[3].position.x=0.5*f;verts2d[3].position.y=-0.5*f;verts2d[3].texcoord.x=1.0f;verts2d[3].texcoord.y=0.0f;// 上传 2D 几何体到 GPUif(!renderer_create_geometry(&state->default_2d_geometry,sizeof(vertex_2d),// ← 2D 顶点大小 (16 bytes)4,verts2d,// ← vertex_2d 数组sizeof(u32),6,indices)){KFATAL("Failed to create default 2D geometry. Application cannot continue.");returnfalse;}state->default_2d_geometry.material=material_system_get_default();returntrue;}// 获取默认 3D 几何体geometry*geometry_system_get_default(){if(state_ptr){return&state_ptr->default_geometry;}KFATAL("geometry_system_get_default called before system was initialized. Returning nullptr.");return0;}// 获取默认 2D 几何体geometry*geometry_system_get_default_2d(){if(state_ptr){return&state_ptr->default_2d_geometry;}KFATAL("geometry_system_get_default_2d called before system was initialized. Returning nullptr.");return0;}

2D坐标系统

默认 2D quad 的顶点坐标:

2D Quad 顶点布局: ┌────────────────────┐ │ (-5, -5) (5, -5)│ ← 顶点 0 和 3 │ 0 3 │ │ │ │ │ │ │ │ 2 1 │ │ (-5, 5) (5, 5)│ ← 顶点 2 和 1 └────────────────────┘ 纹理坐标映射: ┌────────────────────┐ │ (0,0) (1,0) │ ← 顶点 0 和 3 │ 0 3 │ │ │ │ │ │ │ │ 2 1 │ │ (0,1) (1,1) │ ← 顶点 2 和 1 └────────────────────┘ 索引顺序 (逆时针): Triangle 1: 0 → 1 → 2 Triangle 2: 0 → 3 → 1

🎯 UI几何体创建

定义UI顶点

在应用层创建 UI 几何体:

// engine/src/core/application.c// 定义 UI 几何体配置geometry_config ui_config;ui_config.vertex_size=sizeof(vertex_2d);// ← 2D 顶点大小ui_config.vertex_count=4;ui_config.index_size=sizeof(u32);ui_config.index_count=6;string_ncopy(ui_config.material_name,"test_ui_material",MATERIAL_NAME_MAX_LENGTH);string_ncopy(ui_config.name,"test_ui_geometry",GEOMETRY_NAME_MAX_LENGTH);// 创建 512x512 的 UI quadconstf32 f=512.0f;vertex_2d uiverts[4];uiverts[0].position.x=0.0f;// 0 3uiverts[0].position.y=0.0f;//uiverts[0].texcoord.x=0.0f;//uiverts[0].texcoord.y=0.0f;// 2 1uiverts[1].position.y=f;uiverts[1].position.x=f;uiverts[1].texcoord.x=1.0f;uiverts[1].texcoord.y=1.0f;uiverts[2].position.x=0.0f;uiverts[2].position.y=f;uiverts[2].texcoord.x=0.0f;uiverts[2].texcoord.y=1.0f;uiverts[3].position.x=f;uiverts[3].position.y=0.0f;uiverts[3].texcoord.x=1.0f;uiverts[3].texcoord.y=0.0f;ui_config.vertices=uiverts;// 索引 (逆时针)u32 uiindices[6]={2,1,0,3,0,1};ui_config.indices=uiindices;

顶点布局可视化:

UI Quad (512x512 像素): ┌─────────────────────┐ (512, 0) │ (0, 0) 3 │ │ 0 │ │ │ │ │ │ │ │ │ │ 2 1 │ │ (512, 512) └─────────────────────┘ 屏幕坐标系: (0, 0) ───────► X (向右) │ │ ▼ Y (向下) 模型矩阵变换后: model = mat4_translation((vec3){100, 100, 0}) 最终屏幕位置: (100, 100) 到 (612, 612)

几何体配置

geometry_config结构用于配置几何体:

// engine/src/systems/geometry_system.htypedefstructgeometry_config{u32 vertex_size;// 顶点大小 (字节)u32 vertex_count;// 顶点数量void*vertices;// 顶点数据指针u32 index_size;// 索引大小 (字节)u32 index_count;// 索引数量void*indices;// 索引数据指针charname[GEOMETRY_NAME_MAX_LENGTH];// 几何体名称charmaterial_name[MATERIAL_NAME_MAX_LENGTH];// 材质名称}geometry_config;

从配置获取几何体

使用配置创建几何体:

// engine/src/core/application.c// 从配置获取 UI 几何体app_state->test_ui_geometry=geometry_system_acquire_from_config(ui_config,true);if(!app_state->test_ui_geometry){KERROR("Failed to acquire UI geometry");returnfalse;}

geometry_system_acquire_from_config的实现:

// engine/src/systems/geometry_system.cgeometry*geometry_system_acquire_from_config(geometry_config config,b8 auto_release){geometry*g=0;// 1. 查找空闲槽位for(u32 i=0;i<state_ptr->config.max_geometry_count;++i){if(state_ptr->registered_geometries[i].geometry.id==INVALID_ID){// 找到空闲槽位state_ptr->registered_geometries[i].auto_release=auto_release;state_ptr->registered_geometries[i].reference_count=1;g=&state_ptr->registered_geometries[i].geometry;g->id=i;break;}}if(!g){KERROR("Unable to obtain free slot for geometry. Adjust configuration to allow more space.");return0;}// 2. 创建几何体 (上传到 GPU)if(!create_geometry(state_ptr,config,g)){KERROR("Failed to create geometry. Returning nullptr.");return0;}returng;}b8create_geometry(geometry_system_state*state,geometry_config config,geometry*g){// 上传到 GPUif(!renderer_create_geometry(g,config.vertex_size,config.vertex_count,config.vertices,config.index_size,config.index_count,config.indices)){// 创建失败,清理state->registered_geometries[g->id].reference_count=0;state->registered_geometries[g->id].auto_release=false;g->id=INVALID_ID;g->generation=INVALID_ID;g->internal_id=INVALID_ID;returnfalse;}// 获取材质if(string_length(config.material_name)>0){g->material=material_system_acquire(config.material_name);if(!g->material){g->material=material_system_get_default();}}returntrue;}

🔗 渲染流程集成

准备Render Packet

在应用主循环中准备 render packet:

// engine/src/core/application.cb8application_run(){// ... 主循环 ...while(app_state->is_running){// ... 更新逻辑 ...// ========== 准备 Render Packet ==========render_packet packet;packet.delta_time=delta;// 3D 世界几何体geometry_render_data test_world_render;test_world_render.geometry=app_state->test_geometry;test_world_render.model=mat4_identity();packet.geometry_count=1;packet.geometries=&test_world_render;// 2D UI 几何体geometry_render_data test_ui_render;test_ui_render.geometry=app_state->test_ui_geometry;test_ui_render.model=mat4_translation((vec3){0,0,0});// 屏幕左上角packet.ui_geometry_count=1;packet.ui_geometries=&test_ui_render;// 提交渲染renderer_draw_frame(&packet);// ...}returntrue;}

UI几何体绘制

渲染器前端处理 UI 几何体:

// engine/src/renderer/renderer_frontend.cb8renderer_draw_frame(render_packet*packet){// 开始帧if(state_ptr->backend.begin_frame(&state_ptr->backend,packet->delta_time)){// ========== World Renderpass ==========backend.begin_renderpass(BUILTIN_RENDERPASS_WORLD);backend.update_global_world_state(projection,view,...);// 绘制世界几何体u32 count=packet->geometry_count;for(u32 i=0;i<count;++i){backend.draw_geometry(packet->geometries[i]);}backend.end_renderpass(BUILTIN_RENDERPASS_WORLD);// ========== UI Renderpass ==========backend.begin_renderpass(BUILTIN_RENDERPASS_UI);backend.update_global_ui_state(ui_projection,ui_view,0);// 绘制 UI 几何体count=packet->ui_geometry_count;for(u32 i=0;i<count;++i){backend.draw_geometry(packet->ui_geometries[i]);// ← 使用 vertex_2d}backend.end_renderpass(BUILTIN_RENDERPASS_UI);// 结束帧b8 result=backend.end_frame(&state_ptr->backend,packet->delta_time);backend.frame_number++;if(!result){KERROR("renderer_end_frame failed. Application shutting down...");returnfalse;}}returntrue;}

🌐 坐标空间转换

UI 几何体的坐标空间转换:

1. 本地空间 (Local Space): 顶点定义: (0, 0) 到 (512, 512) ┌────────┐ │ │ 512x512 quad └────────┘ 2. 模型变换 (Model Transform): model = mat4_translation((vec3){100, 100, 0}) ┌────────┐ │ │ 移动到 (100, 100) └────────┘ 3. 视图变换 (View Transform): view = mat4_identity() (UI 无相机) ┌────────┐ │ │ 无变化 └────────┘ 4. 投影变换 (Projection Transform): projection = mat4_orthographic(0, 1280, 720, 0, -100, 100) ┌────────┐ │ │ 映射到 NDC [-1, 1] └────────┘ 5. 视口变换 (Viewport Transform): NDC → 屏幕坐标 (0, 0) 到 (1280, 720) ┌────────┐ │ │ 最终显示在屏幕 (100, 100) 到 (612, 612) └────────┘

完整变换管道:

// UI 顶点着色器中的变换voidmain(){// 本地空间顶点vec2 local_pos=in_position;// (0, 0) ~ (512, 512)// 应用模型矩阵 (平移到屏幕位置)vec4 world_pos=u_push_constants.model*vec4(local_pos,0.0,1.0);// world_pos = (100, 100, 0, 1)// 应用视图矩阵 (UI 通常是单位矩阵)vec4 view_pos=global_ubo.view*world_pos;// view_pos = (100, 100, 0, 1)// 应用正交投影矩阵vec4 clip_pos=global_ubo.projection*view_pos;// clip_pos = NDC 坐标gl_Position=clip_pos;}

❓ 常见问题

1. 为什么需要 vertex_2d?直接用 vertex_3d 设置 z=0 不行吗?

技术上可行,但不推荐:

// 方案 1:使用 vertex_3d (浪费)vertex_3d ui_verts[4];ui_verts[0].position=(vec3){0,0,0};// z=0ui_verts[0].texcoord=(vec2){0,0};ui_verts[0].normal=(vec3){0,0,1};// ← 浪费:UI 不需要法线ui_verts[0].tangent=(vec3){1,0,0};// ← 浪费:UI 不需要切线// 内存占用:44 bytes * 4 = 176 bytes// 方案 2:使用 vertex_2d (优化)vertex_2d ui_verts[4];ui_verts[0].position=(vec2){0,0};ui_verts[0].texcoord=(vec2){0,0};// 内存占用:16 bytes * 4 = 64 bytes (节省 64%)

为什么 vertex_2d 更好:

  1. 内存效率: 节省 64% 内存 (44 bytes → 16 bytes)
  2. 带宽效率: 减少 GPU 内存带宽占用
  3. 性能: 更少的数据传输,更快的顶点着色器
  4. 语义清晰: 明确表示这是 2D 几何体
  5. 管线优化: 可以为 2D 几何体创建专门的优化管线

实际影响 (1000 个 UI 元素):

使用 vertex_3d: - 顶点缓冲区: 44 bytes * 4 * 1000 = 176 KB - 索引缓冲区: 4 bytes * 6 * 1000 = 24 KB - 总计: 200 KB 使用 vertex_2d: - 顶点缓冲区: 16 bytes * 4 * 1000 = 64 KB ← 节省 112 KB - 索引缓冲区: 4 bytes * 6 * 1000 = 24 KB - 总计: 88 KB 节省: 56% 内存
2. 材质类型 (WORLD/UI) 和 renderpass 有什么关系?

材质类型决定使用哪个着色器:

材质类型 → 着色器 → Renderpass ┌──────────────────┐ │ MATERIAL_TYPE_ │ │ WORLD │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ Material Shader │ │ - Perspective │ │ - Lighting │ │ - Normal mapping │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ World Renderpass │ └──────────────────┘ ┌──────────────────┐ │ MATERIAL_TYPE_ │ │ UI │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ UI Shader │ │ - Orthographic │ │ - No lighting │ │ - Simple texture │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ UI Renderpass │ └──────────────────┘

系统根据材质类型自动选择着色器:

// engine/src/renderer/vulkan/vulkan_backend.cb8vulkan_renderer_begin_renderpass(renderer_backend*backend,u8 renderpass_id){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;}// 绘制几何体时,根据材质类型应用不同的着色器状态voidvulkan_renderer_draw_geometry(geometry_render_data data){// 根据材质类型更新 shader 状态if(data.geometry->material->type==MATERIAL_TYPE_WORLD){vulkan_material_shader_apply_material(&context,&context.material_shader,data.geometry->material);}elseif(data.geometry->material->type==MATERIAL_TYPE_UI){vulkan_ui_shader_apply_material(&context,&context.ui_shader,data.geometry->material);}// 绘制vkCmdDrawIndexed(...);}

错误示例 (材质类型与 renderpass 不匹配):

// ❌ 错误:在 World Renderpass 中使用 UI 材质geometry_render_data data;data.geometry=ui_geometry;// material->type = MATERIAL_TYPE_UIdata.model=mat4_identity();packet.geometry_count=1;packet.geometries=&data;// ← 将 UI 几何体放到 World Renderpass// 结果:渲染错误,因为 Material Shader 期望 vertex_3d,但得到 vertex_2d

正确使用:

// ✓ 正确:UI 材质在 UI Renderpasspacket.ui_geometry_count=1;packet.ui_geometries=&ui_data;// MATERIAL_TYPE_UI → UI Renderpass// ✓ 正确:World 材质在 World Renderpasspacket.geometry_count=1;packet.geometries=&world_data;// MATERIAL_TYPE_WORLD → World Renderpass
3. 为什么 UI 顶点使用屏幕像素坐标而不是归一化坐标?

屏幕像素坐标更直观:

// 方案 1:像素坐标 (Kohi 使用)vertex_2d ui_verts[4];ui_verts[0].position=(vec2){0,0};// 左上角ui_verts[1].position=(vec2){512,512};// 右下角// 优势:// - 直观:512 像素就是 512 像素// - 设计工具友好:设计师提供的尺寸可以直接使用// - 无需转换:100x100 的按钮就是 100x100 像素// 方案 2:归一化坐标 (0.0 ~ 1.0)vertex_2d ui_verts[4];ui_verts[0].position=(vec2){0.0,0.0};ui_verts[1].position=(vec2){0.4,0.7};// ← 不直观:这是多少像素?// 缺点:// - 不直观:需要心算转换// - 窗口大小相关:调整窗口大小需要重新计算// - 设计工具不友好:设计师需要转换单位

坐标转换由正交投影矩阵完成:

// 正交投影自动将像素坐标转换为 NDCmat4 ui_projection=mat4_orthographic(0,// left: 0 像素1280,// right: 1280 像素720,// bottom: 720 像素0,// top: 0 像素-100.0f,// near100.0f// far);// 顶点变换:// 像素坐标 (512, 512) → NDC (0.3, 0.7) → 屏幕坐标 (512, 512)

布局示例:

// 使用像素坐标布局 UI// 1. 按钮 (100x50),位于 (50, 50)vertex_2d button_verts[4];button_verts[0].position=(vec2){0,0};button_verts[1].position=(vec2){100,50};// model = mat4_translation((vec3){50, 50, 0})// 最终位置:(50, 50) 到 (150, 100)// 2. 图标 (64x64),位于 (200, 100)vertex_2d icon_verts[4];icon_verts[0].position=(vec2){0,0};icon_verts[1].position=(vec2){64,64};// model = mat4_translation((vec3){200, 100, 0})// 最终位置:(200, 100) 到 (264, 164)// 所见即所得:代码中的坐标 = 屏幕上的像素
4. 泛型 create_geometry 接口会影响类型安全吗?

是的,但可以通过包装函数缓解:

// 原始泛型接口 (类型不安全)b8renderer_create_geometry(geometry*geometry,u32 vertex_size,u32 vertex_count,constvoid*vertices,// ← void* 可以接受任意类型u32 index_size,u32 index_count,constvoid*indices);// 问题:可能传入错误的 vertex_sizevertex_2d verts[4];renderer_create_geometry(&geometry,sizeof(vertex_3d),// ← 错误!应该是 sizeof(vertex_2d)4,verts,sizeof(u32),6,indices);// 结果:GPU 读取错误的顶点数据,渲染混乱

解决方案:提供类型安全的包装函数

// 类型安全的包装函数b8renderer_create_geometry_3d(geometry*geometry,u32 vertex_count,constvertex_3d*vertices,u32 index_count,constu32*indices){returnrenderer_create_geometry(geometry,sizeof(vertex_3d),// ← 自动填充正确的大小vertex_count,vertices,sizeof(u32),index_count,indices);}b8renderer_create_geometry_2d(geometry*geometry,u32 vertex_count,constvertex_2d*vertices,u32 index_count,constu32*indices){returnrenderer_create_geometry(geometry,sizeof(vertex_2d),// ← 自动填充正确的大小vertex_count,vertices,sizeof(u32),index_count,indices);}// 使用包装函数 (类型安全)vertex_2d ui_verts[4];renderer_create_geometry_2d(&geometry,4,ui_verts,6,indices);// 编译器会检查类型:ui_verts 必须是 vertex_2d*vertex_3d world_verts[100];renderer_create_geometry_3d(&geometry,100,world_verts,300,indices);// 编译器会检查类型:world_verts 必须是 vertex_3d*

或者使用 C++ 模板 (如果使用 C++):

template<typenameVertexType>boolrenderer_create_geometry_typed(geometry*geometry,u32 vertex_count,constVertexType*vertices,u32 index_count,constu32*indices){returnrenderer_create_geometry(geometry,sizeof(VertexType),// ← 自动推导大小vertex_count,vertices,sizeof(u32),index_count,indices);}// 使用 (完全类型安全)vertex_2d ui_verts[4];renderer_create_geometry_typed(&geometry,4,ui_verts,6,indices);vertex_3d world_verts[100];renderer_create_geometry_typed(&geometry,100,world_verts,300,indices);
5. 如何调试 UI 几何体不显示的问题?

常见原因和调试步骤:

1. 检查材质类型:

// 确保 UI 几何体使用 UI 材质KASSERT(ui_geometry->material->type==MATERIAL_TYPE_UI);// 检查材质文件// test_ui_material.kmt 必须包含: type=ui

2. 检查顶点格式:

// 确保使用 vertex_2dgeometry_config ui_config;ui_config.vertex_size=sizeof(vertex_2d);// 必须是 16,不是 44KASSERT(ui_config.vertex_size==16);

3. 检查坐标范围:

// UI 坐标必须在屏幕范围内// 假设屏幕大小 1280x720vertex_2d verts[4];verts[0].position=(vec2){0,0};verts[1].position=(vec2){512,512};// ← 必须 < (1280, 720)// 检查模型矩阵mat4 model=mat4_translation((vec3){100,100,0});// 平移到可见区域// 最终位置:(100, 100) 到 (612, 612) ← 在屏幕内// 错误示例:mat4 model=mat4_translation((vec3){2000,2000,0});// ← 超出屏幕!

4. 检查渲染顺序:

// UI 必须在 UI Renderpass 中绘制render_packet packet;// 世界几何体 → World Renderpasspacket.geometry_count=1;packet.geometries=&world_data;// UI 几何体 → UI Renderpasspacket.ui_geometry_count=1;packet.ui_geometries=&ui_data;// ← 确保在 ui_geometries,不是 geometries

5. 检查深度测试:

// UI Renderpass 的深度测试可能导致问题// 确保 Z 坐标在正交投影范围内mat4 ui_projection=mat4_orthographic(0,1280,720,0,-100.0f,100.0f);// ^ ^// near farmat4 model=mat4_translation((vec3){100,100,0});// Z=0 (在范围内)// 错误示例:mat4 model=mat4_translation((vec3){100,100,-200});// Z=-200 (超出范围!)

6. 使用 Vulkan 验证层:

# 启用验证层查看错误exportVK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation# 检查输出:# - 顶点缓冲区绑定错误# - 顶点属性不匹配# - Descriptor set 未绑定

7. 检查纹理:

// 确保 UI 材质有有效的纹理texture*diffuse_tex=ui_geometry->material->diffuse_map.texture;KASSERT(diffuse_tex!=0);KASSERT(diffuse_tex->id!=INVALID_ID);// 如果纹理加载失败,会回退到默认纹理 (白色或粉色)if(!diffuse_tex){KWARN("UI material has no texture, using default");diffuse_tex=texture_system_get_default_texture();}

📝 练习

练习 1: 创建多个 UI 元素

任务:创建3个不同大小和位置的 UI 元素。

// 1. 背景图片 (全屏)geometry_config bg_config;bg_config.vertex_size=sizeof(vertex_2d);bg_config.vertex_count=4;bg_config.index_size=sizeof(u32);bg_config.index_count=6;string_ncopy(bg_config.material_name,"background_material",MATERIAL_NAME_MAX_LENGTH);vertex_2d bg_verts[4];bg_verts[0].position=(vec2){0,0};bg_verts[1].position=(vec2){1280,720};// 全屏// ... 设置其他顶点和纹理坐标 ...geometry*background=geometry_system_acquire_from_config(bg_config,true);// 2. 按钮 (200x80)geometry_config button_config;// ... 类似设置 ...vertex_2d button_verts[4];button_verts[0].position=(vec2){0,0};button_verts[1].position=(vec2){200,80};// ...geometry*button=geometry_system_acquire_from_config(button_config,true);// 3. 图标 (64x64)geometry_config icon_config;// ... 类似设置 ...vertex_2d icon_verts[4];icon_verts[0].position=(vec2){0,0};icon_verts[1].position=(vec2){64,64};// ...geometry*icon=geometry_system_acquire_from_config(icon_config,true);// 4. 在渲染循环中绘制geometry_render_data ui_elements[3];// 背景 (Z=0,最后面)ui_elements[0].geometry=background;ui_elements[0].model=mat4_translation((vec3){0,0,0});// 按钮 (Z=10)ui_elements[1].geometry=button;ui_elements[1].model=mat4_translation((vec3){540,320,10});// 居中// 图标 (Z=20,最前面)ui_elements[2].geometry=icon;ui_elements[2].model=mat4_translation((vec3){50,50,20});// 左上角packet.ui_geometry_count=3;packet.ui_geometries=ui_elements;
练习 2: 实现 UI 元素动画

任务:实现一个简单的 UI 元素滑动动画。

// 定义动画状态typedefstructui_animation{vec2 start_pos;vec2 end_pos;f32 duration;f32 elapsed;b8 active;}ui_animation;ui_animation button_anim;button_anim.start_pos=(vec2){-200,320};// 屏幕左边外button_anim.end_pos=(vec2){540,320};// 屏幕中央button_anim.duration=1.0f;// 1 秒button_anim.elapsed=0.0f;button_anim.active=true;// 更新函数voidupdate_ui_animation(ui_animation*anim,f32 delta_time){if(!anim->active)return;anim->elapsed+=delta_time;if(anim->elapsed>=anim->duration){anim->elapsed=anim->duration;anim->active=false;}// 线性插值f32 t=anim->elapsed/anim->duration;vec2 current_pos=vec2_lerp(anim->start_pos,anim->end_pos,t);// 更新模型矩阵button_render.model=mat4_translation((vec3){current_pos.x,current_pos.y,0});}// 在主循环中调用voidapplication_update(f32 delta_time){update_ui_animation(&button_anim,delta_time);}

进阶:缓动函数 (Easing)

// 缓动函数:ease-in-outf32ease_in_out(f32 t){returnt<0.5f?2.0f*t*t:-1.0f+(4.0f-2.0f*t)*t;}// 使用缓动f32 t=anim->elapsed/anim->duration;f32eased_t=ease_in_out(t);vec2 current_pos=vec2_lerp(anim->start_pos,anim->end_pos,eased_t);
练习 3: 实现 9-Slice UI

任务:实现9-slice (九宫格) UI 元素,可以任意缩放而不失真。

/** * 9-Slice 布局: * ┌───┬───────┬───┐ * │ 0 │ 1 │ 2 │ ← 顶部 (不拉伸 Y) * ├───┼───────┼───┤ * │ 3 │ 4 │ 5 │ ← 中间 (拉伸 X 和 Y) * ├───┼───────┼───┤ * │ 6 │ 7 │ 8 │ ← 底部 (不拉伸 Y) * └───┴───────┴───┘ * ↑ ↑ ↑ * 不拉伸 拉伸 不拉伸 */typedefstructnine_slice_config{f32 width;// 目标宽度f32 height;// 目标高度f32 border_left;// 左边框大小f32 border_right;// 右边框大小f32 border_top;// 上边框大小f32 border_bottom;// 下边框大小constchar*texture_name;}nine_slice_config;geometry*create_nine_slice_ui(nine_slice_config config){// 创建9个 quad,每个 quad 对应一个区域// 区域 0:左上角 (固定大小)vertex_2d verts_0[4];verts_0[0].position=(vec2){0,0};verts_0[1].position=(vec2){config.border_left,config.border_top};verts_0[0].texcoord=(vec2){0,0};verts_0[1].texcoord=(vec2){0.33f,0.33f};// 纹理前 1/3// 区域 1:顶部中间 (拉伸 X)vertex_2d verts_1[4];verts_1[0].position=(vec2){config.border_left,0};verts_1[1].position=(vec2){config.width-config.border_right,config.border_top};verts_1[0].texcoord=(vec2){0.33f,0};verts_1[1].texcoord=(vec2){0.67f,0.33f};// 纹理中间 1/3// ... 类似创建区域 2-8 ...// 区域 4:中心 (拉伸 X 和 Y)vertex_2d verts_4[4];verts_4[0].position=(vec2){config.border_left,config.border_top};verts_4[1].position=(vec2){config.width-config.border_right,config.height-config.border_bottom};verts_4[0].texcoord=(vec2){0.33f,0.33f};verts_4[1].texcoord=(vec2){0.67f,0.67f};// 合并所有顶点和索引// ...// 创建几何体geometry_config geo_config;geo_config.vertex_size=sizeof(vertex_2d);geo_config.vertex_count=36;// 9 quad * 4 verticesgeo_config.index_count=54;// 9 quad * 6 indices// ...returngeometry_system_acquire_from_config(geo_config,true);}// 使用nine_slice_config panel_config;panel_config.width=400;panel_config.height=300;panel_config.border_left=16;panel_config.border_right=16;panel_config.border_top=16;panel_config.border_bottom=16;panel_config.texture_name="panel_border";geometry*panel=create_nine_slice_ui(panel_config);

恭喜!你已经掌握了在 UI Renderpass 中绘制!🎉


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


Tutorial written by 上手实验室

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

原神帧率解锁终极指南:彻底告别60帧限制

原神帧率解锁终极指南&#xff1a;彻底告别60帧限制 【免费下载链接】genshin-fps-unlock unlocks the 60 fps cap 项目地址: https://gitcode.com/gh_mirrors/ge/genshin-fps-unlock genshin-fps-unlock是一款专为《原神》玩家设计的帧率解锁工具&#xff0c;通过直接修…

作者头像 李华
网站建设 2025/12/16 23:32:54

FreeMove终极指南:快速解决C盘空间不足的免费神器

FreeMove终极指南&#xff1a;快速解决C盘空间不足的免费神器 【免费下载链接】FreeMove Move directories without breaking shortcuts or installations 项目地址: https://gitcode.com/gh_mirrors/fr/FreeMove 还在为C盘爆满而烦恼吗&#xff1f;FreeMove这款开源工具…

作者头像 李华
网站建设 2025/12/23 14:13:06

【C++ 实战】公交路线最少乘车次数计算(核心思路 + 精华解析)

在公交路线规划场景中&#xff0c;“最少乘车次数” 是典型的图论最短路径问题&#xff0c;其核心解法是线路级 BFS&#xff08;广度优先搜索&#xff09; —— 这是比传统车站级 BFS 效率高一个量级的关键思路。本文抛开冗余代码&#xff0c;聚焦核心逻辑与关键设计&#xff0…

作者头像 李华
网站建设 2025/12/16 23:32:21

深扒AI电影解说软件乱象:为什么90%的“一键生成”做不出爆款?

2025年了&#xff0c;如果你还在迷信市面上那些几十块钱的“一键生成”软件&#xff0c;那你大概率正在制造“工业垃圾”。很多试图通过影视解说赛道变现的MCN机构和创业者都踩过这个坑&#xff1a;买了一堆所谓的自动化工具&#xff0c;把电影文件丢进去&#xff0c;文案自动生…

作者头像 李华
网站建设 2025/12/21 12:58:53

DownKyi终极指南:快速掌握B站视频获取完整教程

还在为无法离线观看B站精彩内容而烦恼吗&#xff1f;DownKyi作为一款专业的哔哩哔哩视频获取工具&#xff0c;能够帮你轻松解决这一困扰。本文将为新手用户提供完整的操作指南&#xff0c;让你快速上手这款实用工具。 【免费下载链接】downkyi 哔哩下载姬downkyi&#xff0c;哔…

作者头像 李华
网站建设 2025/12/16 23:30:08

Zotero Style插件完整使用指南:文献管理可视化与智能标签系统

Zotero Style是一款专为学术研究人员设计的Zotero插件&#xff0c;通过可视化阅读进度和智能标签管理&#xff0c;显著提升文献管理效率。该插件集成了多种实用功能&#xff0c;让文献整理工作更加直观便捷。 【免费下载链接】zotero-style zotero-style - 一个 Zotero 插件&am…

作者头像 李华