教程 32 - 几何体系统

上一篇:材质系统 | 下一篇:资源系统 | 返回目录


📚 快速导航


📋 目录

📖 引言

在前面的教程中,我们实现了纹理系统和材质系统,能够定义和管理物体的外观。但是要渲染一个3D物体,我们还需要几何体数据:顶点位置、纹理坐标、索引等。

目前我们的渲染代码中,顶点数据是硬编码的:

vertex_3d verts[4];
verts[0].position = vec3(-0.5, -0.5, 0);
// ...
u32 indices[6] = {0, 1, 2, 0, 3, 1};

这种方式有诸多问题:

  1. 难以复用 - 相同形状的物体需要重复定义
  2. 无法管理 - 不知道加载了哪些几何体
  3. 内存浪费 - 重复的几何体占用多份内存
  4. 缺乏抽象 - 几何体和材质没有关联

几何体系统解决了这些问题,提供了:

  • 几何体的创建和管理
  • 引用计数和自动释放
  • 与材质系统的集成
  • 程序化几何体生成

🎯 学习目标

目标描述
🎯 理解几何体概念掌握顶点、索引、材质的关系
🎯 实现几何体系统使用与纹理/材质相同的模式
🎯 程序化生成创建平面、立方体等基础形状
🎯 渲染包设计批量提交几何体到渲染器
🎯 系统集成连接几何体、材质、渲染器

几何体概念

什么是几何体?

几何体(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是最基础的可渲染单元

几何体数据流

导出
加载
geometry_config
创建
上传
绘制
3D建模软件
.obj/.fbx文件
顶点/索引数组
几何体系统
geometry对象
GPU缓冲区
屏幕输出

几何体数据结构

几何体结构

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              │
  └──────────────────────────────┘

为什么直接嵌入?

  1. 简化查找:不需要二次索引
  2. 减少内存碎片:数据连续存储
  3. 几何体数据较小:不像纹理有大量像素数据

系统内存布局

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;

渲染包使用流程

游戏逻辑 渲染器 Vulkan后端 准备render_packet 添加几何体到packet geometries[i] = {model, geometry} loop [每个可见对象] renderer_draw_frame(packet) begin_frame() update_global_state(proj, view) draw_geometry(geom_data) 更新材质UBO vkCmdDrawIndexed() loop [每个几何体] end_frame() vkQueueSubmit() 游戏逻辑 渲染器 Vulkan后端

更新渲染循环

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]

原因

  1. 几何体通常不需要名称查找
  2. 几何体经常动态创建(程序化生成)
  3. 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, ...);

为什么不在系统内部释放?

  1. 调用者可能需要修改配置
  2. 调用者可能复用配置创建多个几何体
  3. 遵循"谁分配谁释放"原则
❓ internal_id是什么?

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);
查看提示
  1. 遍历所有顶点
  2. 查找重复的顶点(position和texcoord都相同)
  3. 建立旧索引到新索引的映射
  4. 重新映射索引数组
  5. 创建新的顶点数组
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开始)

解析步骤:

  1. 逐行读取文件
  2. 识别 v, vt, f 开头的行
  3. 提取数值并存储
  4. 构建geometry_config

🔗 下一步

在本教程中,我们实现了完整的几何体系统:

  • ✅ 几何体数据结构(顶点+索引+材质)
  • ✅ 几何体系统(引用计数+ID索引)
  • ✅ 程序化生成(平面生成器)
  • ✅ 渲染包系统(批量提交)
  • ✅ 与材质系统集成

几何体系统是渲染管线的最后一块拼图。现在我们有了:

  • 纹理系统:管理图像数据
  • 材质系统:管理外观属性
  • 几何体系统:管理形状数据

下一步,我们可以实现:

  1. 资源系统 - 统一管理所有资源类型
  2. 场景系统 - 组织场景中的对象
  3. 模型加载器 - 从文件加载3D模型
  4. 实例化渲染 - 高效渲染大量相同几何体

结语

恭喜你完成了本教程!几何体系统让我们可以高效管理和渲染3D形状。

通过程序化生成、引用计数、渲染包等机制,我们构建了一个灵活且高效的几何体管理系统。结合之前的纹理和材质系统,Kohi引擎现在已经具备了渲染复杂3D场景的基础能力。

关键要点回顾

  • 几何体 = 顶点 + 索引 + 材质
  • ID索引比名称查找更适合几何体
  • 程序化生成可以创建基础形状
  • 渲染包支持批量提交几何体
  • 引用计数自动管理资源生命周期

随着系统的成熟,可以添加更多高级特性:骨骼动画、变形目标、GPU实例化、LOD系统等。

如果你有任何问题或建议,欢迎在GitHub上提出issue。


关注公众号,获取更多引擎开发资讯:

上手实验室


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

打赏二维码


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


上一篇:材质系统 | 下一篇:资源系统

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值