📚 快速导航
📋 目录
📖 引言
在前面的教程中,我们实现了纹理系统和材质系统,能够定义和管理物体的外观。但是要渲染一个3D物体,我们还需要几何体数据:顶点位置、纹理坐标、索引等。
目前我们的渲染代码中,顶点数据是硬编码的:
vertex_3d verts[4];
verts[0].position = vec3(-0.5, -0.5, 0);
// ...
u32 indices[6] = {0, 1, 2, 0, 3, 1};
这种方式有诸多问题:
- 难以复用 - 相同形状的物体需要重复定义
- 无法管理 - 不知道加载了哪些几何体
- 内存浪费 - 重复的几何体占用多份内存
- 缺乏抽象 - 几何体和材质没有关联
几何体系统解决了这些问题,提供了:
- 几何体的创建和管理
- 引用计数和自动释放
- 与材质系统的集成
- 程序化几何体生成
🎯 学习目标
| 目标 | 描述 |
|---|---|
| 🎯 理解几何体概念 | 掌握顶点、索引、材质的关系 |
| 🎯 实现几何体系统 | 使用与纹理/材质相同的模式 |
| 🎯 程序化生成 | 创建平面、立方体等基础形状 |
| 🎯 渲染包设计 | 批量提交几何体到渲染器 |
| 🎯 系统集成 | 连接几何体、材质、渲染器 |
几何体概念
什么是几何体?
几何体(Geometry)是3D物体的形状定义,包含:
几何体 = 顶点数据 + 索引数据 + 材质
顶点数据:
vertex[0] = {position: (x, y, z), texcoord: (u, v)}
vertex[1] = {position: (x, y, z), texcoord: (u, v)}
...
索引数据:
indices = [0, 1, 2, 0, 2, 3] // 定义三角形
材质:
指向material*的引用
几何体 vs 网格 vs 模型
模型(Model):
┌────────────────────────┐
│ 人物角色 │
├────────────────────────┤
│ 网格0: 头部 │ 多个网格组成
│ 网格1: 身体 │
│ 网格2: 武器 │
└────────────────────────┘
网格(Mesh):
┌────────────────────────┐
│ 头部网格 │ 一个逻辑单元
├────────────────────────┤
│ 几何体0: LOD 0 (高精度)│ 不同LOD级别
│ 几何体1: LOD 1 (中精度)│
│ 几何体2: LOD 2 (低精度)│
└────────────────────────┘
几何体(Geometry):
┌────────────────────────┐
│ 顶点缓冲区 │ GPU资源
│ 索引缓冲区 │
│ 材质引用 │
└────────────────────────┘
在Kohi引擎中,Geometry是最基础的可渲染单元。
几何体数据流
几何体数据结构
几何体结构
engine/src/resources/resource_types.h
#define GEOMETRY_NAME_MAX_LENGTH 256
/**
* @brief 几何体:世界中的实际几何形状
* 通常(但不总是)与材质配对
*/
typedef struct geometry {
u32 id; // 几何体ID(系统索引)
u32 internal_id; // 渲染器内部ID(GPU资源)
u32 generation; // 版本号
char name[GEOMETRY_NAME_MAX_LENGTH]; // 几何体名称
material* material; // 关联的材质
} geometry;
几何体配置
engine/src/systems/geometry_system.h
/**
* @brief 几何体配置:用于创建几何体
*/
typedef struct geometry_config {
u32 vertex_count; // 顶点数量
vertex_3d* vertices; // 顶点数组
u32 index_count; // 索引数量
u32* indices; // 索引数组
char name[GEOMETRY_NAME_MAX_LENGTH]; // 几何体名称
char material_name[MATERIAL_NAME_MAX_LENGTH]; // 材质名称
} geometry_config;
顶点结构回顾
typedef struct vertex_3d {
vec3 position; // 位置 (x, y, z)
vec2 texcoord; // 纹理坐标 (u, v)
} vertex_3d;
数据关系
geometry "plane_01"
├─ id: 5
├─ internal_id: 12 (Vulkan顶点缓冲区偏移)
├─ name: "plane_01"
└─ material → material "wood"
├─ diffuse_colour: (0.6, 0.4, 0.2, 1.0)
└─ diffuse_map → texture "wood_diffuse"
└─ GPU纹理资源
渲染时:
1. 从geometry获取material
2. 从material获取diffuse_colour和diffuse_map
3. 使用geometry.internal_id绘制顶点数据
几何体系统架构
几何体系统的设计延续了纹理和材质系统的模式。
系统状态
engine/src/systems/geometry_system.c
/**
* @brief 几何体引用信息
*/
typedef struct geometry_reference {
u64 reference_count; // 引用计数
geometry geometry; // 几何体数据(直接嵌入,而非指针)
b8 auto_release; // 是否自动释放
} geometry_reference;
/**
* @brief 几何体系统状态
*/
typedef struct geometry_system_state {
geometry_system_config config; // 配置
geometry default_geometry; // 默认几何体(四边形)
// 已注册几何体数组
geometry_reference* registered_geometries;
} geometry_system_state;
static geometry_system_state* state_ptr = 0;
配置结构
typedef struct geometry_system_config {
/**
* @brief 最大几何体数量
* 注意:应该远大于静态网格数量,因为每个网格可能有多个几何体(LOD等)
*/
u32 max_geometry_count;
} geometry_system_config;
与纹理/材质系统的差异
纹理系统:
┌──────────────────────────────┐
│ texture_reference[] │
│ - reference_count │
│ - handle (索引到texture[]) │ 间接引用
│ - auto_release │
└──────────────────────────────┘
┌──────────────────────────────┐
│ texture[] │ 单独数组
│ - 纹理数据 │
└──────────────────────────────┘
几何体系统:
┌──────────────────────────────┐
│ geometry_reference[] │
│ - reference_count │
│ - geometry (直接嵌入) │ 直接嵌入
│ - auto_release │
└──────────────────────────────┘
为什么直接嵌入?
- 简化查找:不需要二次索引
- 减少内存碎片:数据连续存储
- 几何体数据较小:不像纹理有大量像素数据
系统内存布局
Geometry System内存布局:
┌────────────────────────────────────────┐
│ geometry_system_state (结构体) │
├────────────────────────────────────────┤
│ registered_geometries[4096] (数组) │
│ - geometry_reference 0 │
│ - reference_count │
│ - geometry (嵌入) │
│ - auto_release │
│ - geometry_reference 1 │
│ - ... │
│ - geometry_reference 4095 │
└────────────────────────────────────────┘
注意:没有哈希表!
- 纹理/材质系统使用名称查找
- 几何体系统使用ID直接索引
几何体配置与创建
系统初始化
b8 geometry_system_initialize(u64* memory_requirement, void* state, geometry_system_config config) {
if (config.max_geometry_count == 0) {
KFATAL("geometry_system_initialize - config.max_geometry_count must be > 0.");
return false;
}
// 计算内存需求
u64 struct_requirement = sizeof(geometry_system_state);
u64 array_requirement = sizeof(geometry_reference) * config.max_geometry_count;
*memory_requirement = struct_requirement + array_requirement;
if (!state) {
return true; // 第一次调用,返回内存需求
}
state_ptr = state;
state_ptr->config = config;
// 设置数组指针
void* array_block = state + struct_requirement;
state_ptr->registered_geometries = array_block;
// 初始化所有几何体为无效
u32 count = state_ptr->config.max_geometry_count;
for (u32 i = 0; i < count; ++i) {
state_ptr->registered_geometries[i].geometry.id = INVALID_ID;
state_ptr->registered_geometries[i].geometry.internal_id = INVALID_ID;
state_ptr->registered_geometries[i].geometry.generation = INVALID_ID;
}
// 创建默认几何体
if (!create_default_geometry(state_ptr)) {
KFATAL("Failed to create default geometry.");
return false;
}
return true;
}
从配置获取几何体
/**
* @brief 从配置创建并获取几何体
* @param config 几何体配置
* @param auto_release 引用计数归零时是否自动释放
* @return 几何体指针,失败返回NULL
*/
geometry* geometry_system_acquire_from_config(geometry_config config, b8 auto_release) {
geometry* g = 0;
// 查找空闲槽位
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; // ID就是数组索引
break;
}
}
if (!g) {
KERROR("Unable to obtain free slot for geometry. Adjust configuration.");
return 0;
}
// 创建几何体
if (!create_geometry(state_ptr, config, g)) {
KERROR("Failed to create geometry.");
return 0;
}
return g;
}
通过ID获取几何体
/**
* @brief 通过ID获取现有几何体
* @param id 几何体ID
* @return 几何体指针,失败返回NULL
*/
geometry* geometry_system_acquire_by_id(u32 id) {
if (id != INVALID_ID && state_ptr->registered_geometries[id].geometry.id != INVALID_ID) {
state_ptr->registered_geometries[id].reference_count++;
return &state_ptr->registered_geometries[id].geometry;
}
KERROR("geometry_system_acquire_by_id cannot load invalid geometry id.");
return 0;
}
释放几何体
/**
* @brief 释放几何体引用
* @param geometry 要释放的几何体
*/
void geometry_system_release(geometry* geometry) {
if (geometry && geometry->id != INVALID_ID) {
geometry_reference* ref = &state_ptr->registered_geometries[geometry->id];
u32 id = geometry->id;
if (ref->geometry.id == geometry->id) {
if (ref->reference_count > 0) {
ref->reference_count--;
}
// 引用计数归零且auto_release=true时销毁
if (ref->reference_count < 1 && ref->auto_release) {
destroy_geometry(state_ptr, &ref->geometry);
ref->reference_count = 0;
ref->auto_release = false;
}
} else {
KFATAL("Geometry id mismatch. Check registration logic.");
}
return;
}
KWARN("geometry_system_release cannot release invalid geometry id.");
}
几何体创建与销毁
/**
* @brief 创建几何体
*/
b8 create_geometry(geometry_system_state* state, geometry_config config, geometry* g) {
// 将几何体发送到渲染器上传到GPU
if (!renderer_create_geometry(g, config.vertex_count, config.vertices, 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;
return false;
}
// 获取材质
if (string_length(config.material_name) > 0) {
g->material = material_system_acquire(config.material_name);
if (!g->material) {
g->material = material_system_get_default();
}
}
return true;
}
/**
* @brief 销毁几何体
*/
void destroy_geometry(geometry_system_state* state, geometry* g) {
// 销毁渲染器资源
renderer_destroy_geometry(g);
g->internal_id = INVALID_ID;
g->generation = INVALID_ID;
g->id = INVALID_ID;
string_empty(g->name);
// 释放材质引用
if (g->material && string_length(g->material->name) > 0) {
material_system_release(g->material->name);
g->material = 0;
}
}
默认几何体
b8 create_default_geometry(geometry_system_state* state) {
vertex_3d verts[4];
kzero_memory(verts, sizeof(vertex_3d) * 4);
const f32 f = 10.0f;
// 创建一个四边形
// 布局:
// 0────3
// │ │
// 2────1
verts[0].position.x = -0.5 * f;
verts[0].position.y = -0.5 * f;
verts[0].texcoord.x = 0.0f;
verts[0].texcoord.y = 0.0f;
verts[1].position.x = 0.5 * f;
verts[1].position.y = 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, 4, verts, 6, indices)) {
KFATAL("Failed to create default geometry.");
return false;
}
// 获取默认材质
state->default_geometry.material = material_system_get_default();
return true;
}
程序化几何体生成
几何体系统提供了程序化生成基础形状的功能。
平面生成器
/**
* @brief 生成平面几何体配置
* @param width 平面宽度
* @param height 平面高度
* @param x_segment_count X轴分段数
* @param y_segment_count Y轴分段数
* @param tile_x X轴纹理平铺次数
* @param tile_y Y轴纹理平铺次数
* @param name 几何体名称
* @param material_name 材质名称
* @return 几何体配置
*/
geometry_config geometry_system_generate_plane_config(
f32 width, f32 height,
u32 x_segment_count, u32 y_segment_count,
f32 tile_x, f32 tile_y,
const char* name, const char* material_name) {
// 参数验证
if (width == 0) {
KWARN("Width must be nonzero. Defaulting to one.");
width = 1.0f;
}
if (height == 0) {
KWARN("Height must be nonzero. Defaulting to one.");
height = 1.0f;
}
if (x_segment_count < 1) {
KWARN("x_segment_count must be positive. Defaulting to one.");
x_segment_count = 1;
}
if (y_segment_count < 1) {
KWARN("y_segment_count must be positive. Defaulting to one.");
y_segment_count = 1;
}
if (tile_x == 0) {
KWARN("tile_x must be nonzero. Defaulting to one.");
tile_x = 1.0f;
}
if (tile_y == 0) {
KWARN("tile_y must be nonzero. Defaulting to one.");
tile_y = 1.0f;
}
geometry_config config;
// 计算顶点和索引数量
// 每个分段需要4个顶点和6个索引(2个三角形)
config.vertex_count = x_segment_count * y_segment_count * 4;
config.vertices = kallocate(sizeof(vertex_3d) * config.vertex_count, MEMORY_TAG_ARRAY);
config.index_count = x_segment_count * y_segment_count * 6;
config.indices = kallocate(sizeof(u32) * config.index_count, MEMORY_TAG_ARRAY);
// 计算分段尺寸
f32 seg_width = width / x_segment_count;
f32 seg_height = height / y_segment_count;
f32 half_width = width * 0.5f;
f32 half_height = height * 0.5f;
// 生成每个分段
for (u32 y = 0; y < y_segment_count; ++y) {
for (u32 x = 0; x < x_segment_count; ++x) {
// 计算分段边界
f32 min_x = (x * seg_width) - half_width;
f32 min_y = (y * seg_height) - half_height;
f32 max_x = min_x + seg_width;
f32 max_y = min_y + seg_height;
// 计算UV坐标
f32 min_uvx = (x / (f32)x_segment_count) * tile_x;
f32 min_uvy = (y / (f32)y_segment_count) * tile_y;
f32 max_uvx = ((x + 1) / (f32)x_segment_count) * tile_x;
f32 max_uvy = ((y + 1) / (f32)y_segment_count) * tile_y;
// 顶点偏移
u32 v_offset = ((y * x_segment_count) + x) * 4;
vertex_3d* v0 = &config.vertices[v_offset + 0];
vertex_3d* v1 = &config.vertices[v_offset + 1];
vertex_3d* v2 = &config.vertices[v_offset + 2];
vertex_3d* v3 = &config.vertices[v_offset + 3];
// 生成4个顶点(逆时针)
// v0──v3
// │ │
// v2──v1
v0->position.x = min_x;
v0->position.y = min_y;
v0->texcoord.x = min_uvx;
v0->texcoord.y = min_uvy;
v1->position.x = max_x;
v1->position.y = max_y;
v1->texcoord.x = max_uvx;
v1->texcoord.y = max_uvy;
v2->position.x = min_x;
v2->position.y = max_y;
v2->texcoord.x = min_uvx;
v2->texcoord.y = max_uvy;
v3->position.x = max_x;
v3->position.y = min_y;
v3->texcoord.x = max_uvx;
v3->texcoord.y = min_uvy;
// 生成6个索引(2个三角形)
u32 i_offset = ((y * x_segment_count) + x) * 6;
config.indices[i_offset + 0] = v_offset + 0; // 三角形1
config.indices[i_offset + 1] = v_offset + 1;
config.indices[i_offset + 2] = v_offset + 2;
config.indices[i_offset + 3] = v_offset + 0; // 三角形2
config.indices[i_offset + 4] = v_offset + 3;
config.indices[i_offset + 5] = v_offset + 1;
}
}
// 设置名称
if (name && string_length(name) > 0) {
string_ncopy(config.name, name, GEOMETRY_NAME_MAX_LENGTH);
} else {
string_ncopy(config.name, DEFAULT_GEOMETRY_NAME, GEOMETRY_NAME_MAX_LENGTH);
}
// 设置材质名称
if (material_name && string_length(material_name) > 0) {
string_ncopy(config.material_name, material_name, MATERIAL_NAME_MAX_LENGTH);
} else {
string_ncopy(config.material_name, DEFAULT_MATERIAL_NAME, MATERIAL_NAME_MAX_LENGTH);
}
return config;
}
平面生成示意图
1x1分段平面(最简单情况):
v0────v3
│ ╱ │
│╱ │
v2────v1
顶点:v0, v1, v2, v3
索引:0,1,2, 0,3,1
2x2分段平面:
v0──v3──v6──v9
│ ╱ │ ╱ │ ╱ │
v2──v5──v8──v11
│ ╱ │ ╱ │ ╱ │
v1──v4──v7──v10
│ ╱ │ ╱ │ ╱ │
v0──v3──v6──v9
4个分段 = 16个顶点,24个索引
纹理平铺(tile_x=2, tile_y=2):
UV坐标会超过[0,1]范围,触发纹理重复
┌───┬───┐
│ T │ T │ T = 纹理
├───┼───┤
│ T │ T │
└───┴───┘
🔗 渲染器集成
渲染器接口更新
engine/src/renderer/renderer_frontend.h
// 几何体操作
b8 renderer_create_geometry(geometry* geometry, u32 vertex_count, const vertex_3d* vertices, u32 index_count, const u32* indices);
void renderer_destroy_geometry(geometry* geometry);
Vulkan后端实现
几何体在Vulkan中对应顶点和索引缓冲区的偏移:
engine/src/renderer/vulkan/vulkan_backend.c
b8 vulkan_renderer_create_geometry(geometry* geometry, u32 vertex_count, const vertex_3d* vertices, u32 index_count, const u32* indices) {
if (!vertex_count || !vertices) {
KERROR("vulkan_renderer_create_geometry requires vertex data.");
return false;
}
// 检查是否有索引数据
b8 is_indexed = index_count > 0 && indices;
// 为几何体分配内部ID(缓冲区偏移)
geometry->internal_id = context.geometry_vertex_offset;
// 上传顶点数据
u32 size = sizeof(vertex_3d) * vertex_count;
if (!upload_data_range(&context, context.device.graphics_command_pool, 0, context.device.graphics_queue,
&context.object_vertex_buffer, context.geometry_vertex_offset, size, vertices)) {
KERROR("Failed to upload geometry vertices.");
return false;
}
context.geometry_vertex_offset += vertex_count;
// 上传索引数据
if (is_indexed) {
size = sizeof(u32) * index_count;
if (!upload_data_range(&context, context.device.graphics_command_pool, 0, context.device.graphics_queue,
&context.object_index_buffer, context.geometry_index_offset, size, indices)) {
KERROR("Failed to upload geometry indices.");
return false;
}
context.geometry_index_offset += index_count;
}
return true;
}
void vulkan_renderer_destroy_geometry(geometry* geometry) {
if (geometry && geometry->internal_id != INVALID_ID) {
// TODO: 回收顶点/索引缓冲区空间
// 当前实现不回收,只增长
geometry->internal_id = INVALID_ID;
}
}
💡 注意:当前实现使用单个大缓冲区,顶点和索引数据追加式上传。这是一个简化实现,生产环境应该支持缓冲区回收和碎片整理。
渲染包系统
为了高效渲染多个几何体,我们引入**渲染包(Render Packet)**概念。
渲染包结构
engine/src/renderer/renderer_types.inl
/**
* @brief 几何体渲染数据
*/
typedef struct geometry_render_data {
mat4 model; // 模型变换矩阵
geometry* geometry; // 几何体指针
} geometry_render_data;
/**
* @brief 渲染包:一帧中要渲染的所有数据
*/
typedef struct render_packet {
f32 delta_time; // 帧间隔时间
u32 geometry_count; // 几何体数量
geometry_render_data* geometries; // 几何体数组
} render_packet;
渲染包使用流程
更新渲染循环
engine/src/renderer/renderer_frontend.c
b8 renderer_draw_frame(render_packet* packet) {
if (renderer_begin_frame(packet->delta_time)) {
// 更新全局状态
state_ptr->backend.update_global_state(
state_ptr->projection,
state_ptr->view,
vec3_zero(),
vec4_one(),
0);
// 绘制所有几何体
for (u32 i = 0; i < packet->geometry_count; ++i) {
state_ptr->backend.draw_geometry(packet->geometries[i]);
}
// 结束帧
b8 result = renderer_end_frame(packet->delta_time);
if (!result) {
KERROR("renderer_end_frame failed.");
return false;
}
}
return true;
}
后端绘制函数
void vulkan_renderer_draw_geometry(geometry_render_data data) {
// 更新材质着色器
vulkan_material_shader_set_model(&context, &context.material_shader, data.model);
vulkan_material_shader_apply_material(&context, &context.material_shader, data.geometry->material);
// 绑定顶点和索引缓冲区
VkDeviceSize offsets[1] = {data.geometry->internal_id * sizeof(vertex_3d)};
vkCmdBindVertexBuffers(command_buffer, 0, 1, &context.object_vertex_buffer.handle, offsets);
if (data.geometry->index_count > 0) {
vkCmdBindIndexBuffer(command_buffer, context.object_index_buffer.handle,
data.geometry->internal_id * sizeof(u32), VK_INDEX_TYPE_UINT32);
vkCmdDrawIndexed(command_buffer, data.geometry->index_count, 1, 0, 0, 0);
} else {
vkCmdDraw(command_buffer, data.geometry->vertex_count, 1, 0, 0);
}
}
使用示例
示例1:创建简单几何体
void create_simple_quad() {
// 生成平面配置
geometry_config config = geometry_system_generate_plane_config(
10.0f, 10.0f, // 宽度、高度
1, 1, // 1x1分段
1.0f, 1.0f, // 纹理平铺1次
"my_quad", // 几何体名称
"wood" // 材质名称
);
// 获取几何体
geometry* quad = geometry_system_acquire_from_config(config, true);
// 使用几何体...
// 释放
geometry_system_release(quad);
// 清理配置中分配的内存
kfree(config.vertices, sizeof(vertex_3d) * config.vertex_count, MEMORY_TAG_ARRAY);
kfree(config.indices, sizeof(u32) * config.index_count, MEMORY_TAG_ARRAY);
}
示例2:渲染多个几何体
void render_scene(render_packet* packet) {
packet->geometry_count = 3;
packet->geometries = kallocate(sizeof(geometry_render_data) * 3, MEMORY_TAG_ARRAY);
// 地板
packet->geometries[0].geometry = floor_geometry;
packet->geometries[0].model = mat4_translation(vec3(0, -1, 0));
// 墙壁1
packet->geometries[1].geometry = wall_geometry;
packet->geometries[1].model = mat4_translation(vec3(-5, 0, 0));
// 墙壁2
packet->geometries[2].geometry = wall_geometry;
packet->geometries[2].model = mat4_translation(vec3(5, 0, 0));
renderer_draw_frame(packet);
kfree(packet->geometries, sizeof(geometry_render_data) * 3, MEMORY_TAG_ARRAY);
}
示例3:程序化生成地形
void create_terrain() {
// 生成大平面作为地形
geometry_config config = geometry_system_generate_plane_config(
100.0f, 100.0f, // 100x100单位
10, 10, // 10x10分段(100个四边形)
10.0f, 10.0f, // 纹理平铺10次
"terrain",
"grass"
);
geometry* terrain = geometry_system_acquire_from_config(config, false); // 常驻不释放
// 清理
kfree(config.vertices, sizeof(vertex_3d) * config.vertex_count, MEMORY_TAG_ARRAY);
kfree(config.indices, sizeof(u32) * config.index_count, MEMORY_TAG_ARRAY);
}
示例4:共享几何体
// 创建一个立方体几何体
geometry* cube_geom = create_cube_geometry();
// 多个对象共享同一几何体,但使用不同变换
void render_cubes(render_packet* packet) {
packet->geometry_count = 10;
packet->geometries = kallocate(sizeof(geometry_render_data) * 10, MEMORY_TAG_ARRAY);
for (u32 i = 0; i < 10; ++i) {
packet->geometries[i].geometry = cube_geom; // 共享几何体
packet->geometries[i].model = mat4_translation(vec3(i * 2, 0, 0)); // 不同位置
}
renderer_draw_frame(packet);
}
❓ 常见问题
❓ 几何体和材质是如何关联的?几何体结构中有一个 material* 指针:
typedef struct geometry {
// ...
material* material;
} geometry;
在创建几何体时,如果config中指定了材质名称,系统会自动获取材质:
if (string_length(config.material_name) > 0) {
g->material = material_system_acquire(config.material_name);
}
渲染时,从几何体获取材质:
void draw_geometry(geometry_render_data data) {
material* mat = data.geometry->material;
// 使用材质的颜色和纹理...
}
❓ 为什么几何体系统没有哈希表?
设计差异:
纹理/材质系统:
- 通过名称查找(“wood”, "metal"等)
- 需要哈希表:name → handle → resource
几何体系统:
- 通过ID直接索引
- 数组访问:registered_geometries[id]
原因:
- 几何体通常不需要名称查找
- 几何体经常动态创建(程序化生成)
- ID直接对应数组索引,更高效
如果需要名称查找,可以自己维护一个映射:
hashtable geometry_name_to_id;
u32 id = hashtable_get(geometry_name_to_id, "my_quad");
geometry* g = geometry_system_acquire_by_id(id);
❓ generate_plane_config为什么分配内存?
因为顶点和索引数组的大小是动态的:
config.vertex_count = x_segment_count * y_segment_count * 4;
config.vertices = kallocate(sizeof(vertex_3d) * config.vertex_count, ...);
调用者负责释放:
geometry_config config = geometry_system_generate_plane_config(...);
geometry* g = geometry_system_acquire_from_config(config, true);
// 几何体已上传到GPU,可以释放CPU内存
kfree(config.vertices, ...);
kfree(config.indices, ...);
为什么不在系统内部释放?
- 调用者可能需要修改配置
- 调用者可能复用配置创建多个几何体
- 遵循"谁分配谁释放"原则
internal_id 是几何体在GPU缓冲区中的偏移:
// 创建时
geometry->internal_id = context.geometry_vertex_offset; // 如:0, 4, 8, ...
// 绘制时
VkDeviceSize offset = geometry->internal_id * sizeof(vertex_3d);
vkCmdBindVertexBuffers(..., &offset);
示例:
顶点缓冲区:
┌──────────┬──────────┬──────────┐
│ Quad (4) │ Cube (8) │Plane (16)│
├──────────┼──────────┼──────────┤
│ Offset │ Offset │ Offset │
│ 0 │ 4 │ 12 │
└──────────┴──────────┴──────────┘
↑ ↑ ↑
internal_id internal_id internal_id
❓ 如何支持多个LOD级别?
LOD(Level of Detail)可以通过创建多个几何体实现:
typedef struct mesh {
geometry* lod0; // 高精度
geometry* lod1; // 中精度
geometry* lod2; // 低精度
} mesh;
geometry* select_lod(mesh* m, f32 distance) {
if (distance < 10.0f) return m->lod0;
if (distance < 50.0f) return m->lod1;
return m->lod2;
}
// 渲染时
geometry* geom = select_lod(my_mesh, camera_distance);
render_data.geometry = geom;
📝 练习与挑战
练习1:实现立方体生成器
创建一个函数生成立方体几何体:
geometry_config geometry_system_generate_cube_config(f32 size, const char* name, const char* material_name);
查看提示
立方体有6个面,每个面是2个三角形(4顶点,6索引):
// 总共:24个顶点,36个索引
config.vertex_count = 24;
config.index_count = 36;
// 前面
vertices[0-3] = ...
indices[0-5] = {0, 1, 2, 0, 2, 3};
// 后面
vertices[4-7] = ...
indices[6-11] = {4, 5, 6, 4, 6, 7};
// ... 其他4个面
练习2:顶点去重
generate_plane_config生成了重复顶点。实现一个函数去重:
void deduplicate_vertices(geometry_config* config);
查看提示
- 遍历所有顶点
- 查找重复的顶点(position和texcoord都相同)
- 建立旧索引到新索引的映射
- 重新映射索引数组
- 创建新的顶点数组
for (u32 i = 0; i < old_vertex_count; ++i) {
b8 found = false;
for (u32 j = 0; j < new_vertex_count; ++j) {
if (vertices_equal(old_vertices[i], new_vertices[j])) {
index_map[i] = j;
found = true;
break;
}
}
if (!found) {
new_vertices[new_vertex_count] = old_vertices[i];
index_map[i] = new_vertex_count++;
}
}
// 重新映射索引
for (u32 i = 0; i < index_count; ++i) {
indices[i] = index_map[indices[i]];
}
练习3:从OBJ文件加载几何体
实现一个简单的OBJ加载器:
b8 load_obj_file(const char* path, geometry_config* out_config);
查看提示
OBJ格式示例:
v 0.0 0.0 0.0 # 顶点位置
vt 0.0 0.0 # 纹理坐标
f 1/1 2/2 3/3 # 面(索引从1开始)
解析步骤:
- 逐行读取文件
- 识别
v,vt,f开头的行 - 提取数值并存储
- 构建geometry_config
🔗 下一步
在本教程中,我们实现了完整的几何体系统:
- ✅ 几何体数据结构(顶点+索引+材质)
- ✅ 几何体系统(引用计数+ID索引)
- ✅ 程序化生成(平面生成器)
- ✅ 渲染包系统(批量提交)
- ✅ 与材质系统集成
几何体系统是渲染管线的最后一块拼图。现在我们有了:
- 纹理系统:管理图像数据
- 材质系统:管理外观属性
- 几何体系统:管理形状数据
下一步,我们可以实现:
- 资源系统 - 统一管理所有资源类型
- 场景系统 - 组织场景中的对象
- 模型加载器 - 从文件加载3D模型
- 实例化渲染 - 高效渲染大量相同几何体
结语
恭喜你完成了本教程!几何体系统让我们可以高效管理和渲染3D形状。
通过程序化生成、引用计数、渲染包等机制,我们构建了一个灵活且高效的几何体管理系统。结合之前的纹理和材质系统,Kohi引擎现在已经具备了渲染复杂3D场景的基础能力。
关键要点回顾:
- 几何体 = 顶点 + 索引 + 材质
- ID索引比名称查找更适合几何体
- 程序化生成可以创建基础形状
- 渲染包支持批量提交几何体
- 引用计数自动管理资源生命周期
随着系统的成熟,可以添加更多高级特性:骨骼动画、变形目标、GPU实例化、LOD系统等。
如果你有任何问题或建议,欢迎在GitHub上提出issue。
关注公众号,获取更多引擎开发资讯:

觉得有帮助?请作者喝杯咖啡:

📧 作者: 上手实验室
🔗 项目地址: https://github.com/travisvroman/kohi
1038

被折叠的 条评论
为什么被折叠?



