🔗 快速导航
📋 目录
点击展开/折叠🎯 概述
透明度渲染(Transparency Rendering) 是 3D 图形学中最具挑战性的问题之一。当场景中包含半透明或透明对象时,简单的深度测试和深度写入无法正确处理渲染顺序,导致视觉错误。
在本教程中,我们将:
- ✅ 理解透明度渲染的挑战
- ✅ 学习深度排序算法
- ✅ 实现快速排序来排序透明对象
- ✅ 分离不透明和透明几何体的渲染
- ✅ 处理 Alpha 混合和深度测试
❓ 为什么透明度渲染很困难?
对于不透明对象,渲染顺序无关紧要,因为深度缓冲区(Z-Buffer)会自动处理遮挡关系:
但对于透明对象,情况完全不同:
| 特性 | 不透明对象 | 透明对象 |
|---|---|---|
| 深度写入 | ✅ 启用 | ❌ 禁用 |
| 深度测试 | ✅ 启用 | ✅ 启用 |
| 渲染顺序 | 任意 | 必须从远到近 |
| 混合模式 | 不混合 | Alpha 混合 |
问题示例:
graph TB
subgraph "错误渲染(任意顺序)"
A1[相机] -.-> B1[透明玻璃 距离:5]
A1 -.-> C1[不透明墙 距离:10]
B1 --> D1[先绘制玻璃]
C1 --> D1
D1 --> E1[❌ 玻璃遮挡墙]
end
subgraph "正确渲染(排序)"
A2[相机] -.-> B2[不透明墙 距离:10]
A2 -.-> C2[透明玻璃 距离:5]
B2 --> D2[1. 先绘制墙]
D2 --> E2[2. 再绘制玻璃]
E2 --> F2[✅ 透过玻璃看到墙]
end
🔍 问题演示
假设场景中有三个对象:
// 场景设置
对象 A: 不透明立方体,距离相机 5 单位
对象 B: 透明玻璃,距离相机 10 单位(在 A 后面)
对象 C: 不透明地面,距离相机 20 单位
错误渲染顺序:B → A → C
1. 绘制 B (透明玻璃)
- 深度缓冲区写入:10
- 颜色缓冲区:玻璃颜色(半透明)
2. 绘制 A (不透明立方体)
- 深度测试:5 < 10,通过
- 深度缓冲区写入:5
- 颜色缓冲区:立方体颜色(覆盖玻璃)
3. 绘制 C (地面)
- 深度测试:20 > 5,失败
- 被丢弃
结果:看不到地面,玻璃被立方体完全覆盖
正确渲染顺序:C → A → B
1. 绘制 C (地面,最远)
- 深度缓冲区写入:20
- 颜色缓冲区:地面颜色
2. 绘制 A (立方体,中等距离)
- 深度测试:5 < 20,通过
- 深度缓冲区写入:5
- 颜色缓冲区:立方体颜色
3. 绘制 B (玻璃,最近的透明对象)
- 深度测试:10 > 5,失败(对于立方体部分)
- 深度测试:10 < 20,通过(对于地面部分)
- 深度缓冲区:不写入(透明对象)
- 颜色缓冲区:与背景混合
结果:✅ 透过玻璃看到地面,立方体正确遮挡
💡 解决方案:深度排序
解决透明度渲染问题的标准方法是深度排序:
关键步骤:
- 分离几何体:将不透明和透明对象分开
- 计算距离:对每个透明对象,计算其中心到相机的距离
- 排序:按距离从远到近排序
- 渲染:
- 先渲染所有不透明对象(任意顺序)
- 再渲染排序后的透明对象(从远到近)
📦 核心数据结构
几何体距离结构
/**
* @brief 用于按距离排序几何体的私有结构
*/
typedef struct geometry_distance {
/** @brief 几何体渲染数据 */
geometry_render_data g;
/** @brief 距离相机的距离 */
f32 distance;
} geometry_distance;
用途:
- 将几何体数据与其距离相机的距离配对
- 作为排序算法的输入
- 排序后提取几何体数据
3D 范围
/**
* @brief 表示 2D 对象的范围
*/
typedef struct extents_2d {
/** @brief 对象的最小范围 */
vec2 min;
/** @brief 对象的最大范围 */
vec2 max;
} extents_2d;
/**
* @brief 表示 3D 对象的范围(边界框)
*/
typedef struct extents_3d {
/** @brief 对象的最小范围 */
vec3 min;
/** @brief 对象的最大范围 */
vec3 max;
} extents_3d;
用途:
- 表示对象的边界框(Bounding Box)
- 用于碰撞检测
- 计算对象中心点
- 视锥体剔除
计算中心点:
// 从范围计算中心
vec3 get_center(extents_3d extents) {
return (vec3) {
(extents.min.x + extents.max.x) * 0.5f,
(extents.min.y + extents.max.y) * 0.5f,
(extents.min.z + extents.max.z) * 0.5f
};
}
🧮 数学基础
向量变换
要计算透明对象到相机的距离,首先需要将对象的局部中心点变换到世界空间:
/**
* @brief 通过矩阵变换向量
* 注意:此函数假定向量 v 是一个点,而不是方向,
* 并且按照 w 分量为 1.0f 的方式计算
*
* @param v 要变换的向量
* @param m 变换矩阵
* @return v 的变换副本
*/
KINLINE vec3 vec3_transform(vec3 v, mat4 m) {
vec3 out;
// x' = x*m00 + y*m10 + z*m20 + 1.0*m30
out.x = v.x * m.data[0 + 0] + v.y * m.data[4 + 0] + v.z * m.data[8 + 0] + 1.0f * m.data[12 + 0];
// y' = x*m01 + y*m11 + z*m21 + 1.0*m31
out.y = v.x * m.data[0 + 1] + v.y * m.data[4 + 1] + v.z * m.data[8 + 1] + 1.0f * m.data[12 + 1];
// z' = x*m02 + y*m12 + z*m22 + 1.0*m32
out.z = v.x * m.data[0 + 2] + v.y * m.data[4 + 2] + v.z * m.data[8 + 2] + 1.0f * m.data[12 + 2];
return out;
}
矩阵布局(列主序):
m = | m0 m4 m8 m12 | (第 0 列, 第 1 列, 第 2 列, 第 3 列)
| m1 m5 m9 m13 |
| m2 m6 m10 m14 |
| m3 m7 m11 m15 |
变换公式(齐次坐标):
[x'] [m0 m4 m8 m12] [x]
[y'] = [m1 m5 m9 m13] [y]
[z'] [m2 m6 m10 m14] [z]
[w'] [m3 m7 m11 m15] [1]
示例:
// 对象的局部中心
vec3 local_center = {0.0f, 1.0f, 0.0f};
// 模型矩阵(平移到世界坐标 (10, 5, 20))
mat4 model = mat4_translation((vec3){10.0f, 5.0f, 20.0f});
// 变换到世界空间
vec3 world_center = vec3_transform(local_center, model);
// 结果: (10.0, 6.0, 20.0)
// 相机位置
vec3 camera_pos = {0.0f, 0.0f, 0.0f};
// 计算距离
f32 distance = vec3_distance(world_center, camera_pos);
// 结果: sqrt(10^2 + 6^2 + 20^2) = sqrt(536) ≈ 23.15
🔧 实现透明度渲染
修改构建数据包函数
b8 render_view_world_on_build_packet(const struct render_view* self,
void* data,
struct render_view_packet* out_packet) {
if (!self || !data || !out_packet) {
KWARN("render_view_world_on_build_packet requires valid pointers.");
return false;
}
mesh_packet_data* mesh_data = (mesh_packet_data*)data;
render_view_world_internal_data* internal_data =
(render_view_world_internal_data*)self->internal_data;
// 创建几何体动态数组
out_packet->geometries = darray_create(geometry_render_data);
out_packet->view = self;
// 设置矩阵
out_packet->projection_matrix = internal_data->projection_matrix;
out_packet->view_matrix = camera_view_get(internal_data->world_camera);
out_packet->view_position = camera_position_get(internal_data->world_camera);
out_packet->ambient_colour = internal_data->ambient_colour;
// 创建透明几何体距离数组
geometry_distance* geometry_distances = darray_create(geometry_distance);
// 遍历所有网格
for (u32 i = 0; i < mesh_data->mesh_count; ++i) {
mesh* m = &mesh_data->meshes[i];
mat4 model = transform_get_world(&m->transform);
for (u32 j = 0; j < m->geometry_count; ++j) {
geometry_render_data render_data;
render_data.geometry = m->geometries[j];
render_data.model = model;
// 检查是否有透明度
if ((m->geometries[j]->material->diffuse_map.texture->flags &
TEXTURE_FLAG_HAS_TRANSPARENCY) == 0) {
// 不透明对象:直接添加到列表
darray_push(out_packet->geometries, render_data);
out_packet->geometry_count++;
} else {
// 透明对象:计算距离并添加到透明列表
// 1. 获取几何体的局部中心点
vec3 local_center = render_data.geometry->center;
// 2. 变换到世界空间
vec3 world_center = vec3_transform(local_center, model);
// 3. 计算到相机的距离
f32 distance = vec3_distance(world_center,
internal_data->world_camera->position);
// 4. 保存几何体和距离
geometry_distance gdist;
gdist.distance = kabs(distance); // 使用绝对值
gdist.g = render_data;
darray_push(geometry_distances, gdist);
}
}
}
// 按距离排序透明对象(从远到近)
u32 transparent_count = darray_length(geometry_distances);
if (transparent_count > 0) {
quick_sort(geometry_distances, 0, transparent_count - 1, false); // false = 降序
// 将排序后的透明对象添加到渲染列表
for (u32 i = 0; i < transparent_count; ++i) {
darray_push(out_packet->geometries, geometry_distances[i].g);
out_packet->geometry_count++;
}
}
// 清理
darray_destroy(geometry_distances);
return true;
}
关键点:
- 分离处理:不透明对象直接添加,透明对象单独处理
- 距离计算:使用
vec3_transform和vec3_distance - 排序:使用快速排序,降序(远到近)
- 合并:先渲染不透明对象,再渲染排序后的透明对象
快速排序算法
/**
* @brief 交换两个 geometry_distance 结构
*/
static void swap(geometry_distance* a, geometry_distance* b) {
geometry_distance temp = *a;
*a = *b;
*b = temp;
}
/**
* @brief 快速排序的分区函数
*
* @param arr 要分区的数组
* @param low_index 低索引
* @param high_index 高索引
* @param ascending true 升序排序,否则降序
* @return 分区点索引
*/
static i32 partition(geometry_distance arr[], i32 low_index, i32 high_index, b8 ascending) {
// 选择最后一个元素作为枢轴
geometry_distance pivot = arr[high_index];
i32 i = (low_index - 1); // 较小元素的索引
for (i32 j = low_index; j <= high_index - 1; ++j) {
// 如果当前元素小于或等于枢轴
if (ascending) {
if (arr[j].distance < pivot.distance) {
++i;
swap(&arr[i], &arr[j]);
}
} else {
// 降序:如果当前元素大于枢轴
if (arr[j].distance > pivot.distance) {
++i;
swap(&arr[i], &arr[j]);
}
}
}
swap(&arr[i + 1], &arr[high_index]);
return i + 1;
}
/**
* @brief 递归快速排序函数
*
* @param arr 要排序的 geometry_distance 数组
* @param low_index 起始索引(通常为 0)
* @param high_index 结束索引(通常为数组长度 - 1)
* @param ascending true 升序排序,否则降序
*/
static void quick_sort(geometry_distance arr[], i32 low_index, i32 high_index, b8 ascending) {
if (low_index < high_index) {
// pi 是分区索引,arr[pi] 现在在正确的位置
i32 partition_index = partition(arr, low_index, high_index, ascending);
// 分别排序分区索引前后的元素
quick_sort(arr, low_index, partition_index - 1, ascending);
quick_sort(arr, partition_index + 1, high_index, ascending);
}
}
快速排序工作原理:
时间复杂度:
- 平均情况:O(n log n)
- 最坏情况:O(n²)(数组已排序)
- 最好情况:O(n log n)
示例:
// 假设有 4 个透明对象
geometry_distance distances[4] = {
{.distance = 10.5f}, // 中等距离
{.distance = 25.0f}, // 最远
{.distance = 5.2f}, // 最近
{.distance = 15.3f} // 中等距离
};
// 降序排序(从远到近)
quick_sort(distances, 0, 3, false);
// 结果顺序: 25.0, 15.3, 10.5, 5.2
// 渲染顺序: 先渲染最远的,最后渲染最近的
🔄 渲染流程对比
传统方式(错误):
深度排序方式(正确):
⚙️ Alpha 混合模式
为了正确渲染透明对象,需要配置 Alpha 混合:
// Vulkan 混合配置
VkPipelineColorBlendAttachmentState blend_state = {};
blend_state.blendEnable = VK_TRUE;
// 颜色混合:(src.rgb * src.a) + (dst.rgb * (1 - src.a))
blend_state.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
blend_state.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
blend_state.colorBlendOp = VK_BLEND_OP_ADD;
// Alpha 混合:(src.a * 1) + (dst.a * 0) = src.a
blend_state.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
blend_state.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
blend_state.alphaBlendOp = VK_BLEND_OP_ADD;
// 写入所有颜色通道
blend_state.colorWriteMask = VK_COLOR_COMPONENT_R_BIT |
VK_COLOR_COMPONENT_G_BIT |
VK_COLOR_COMPONENT_B_BIT |
VK_COLOR_COMPONENT_A_BIT;
混合公式:
最终颜色 = (源颜色 × 源因子) + (目标颜色 × 目标因子)
对于标准 Alpha 混合:
最终颜色 = (源颜色 × 源Alpha) + (目标颜色 × (1 - 源Alpha))
示例:
// 源颜色(半透明红色)
vec4 src = {1.0f, 0.0f, 0.0f, 0.5f}; // RGBA
// 目标颜色(蓝色背景)
vec4 dst = {0.0f, 0.0f, 1.0f, 1.0f};
// 混合计算
vec4 result;
result.r = src.r * src.a + dst.r * (1.0f - src.a);
= 1.0 * 0.5 + 0.0 * 0.5
= 0.5 // 紫红色
result.g = src.g * src.a + dst.g * (1.0f - src.a);
= 0.0 * 0.5 + 0.0 * 0.5
= 0.0
result.b = src.b * src.a + dst.b * (1.0f - src.a);
= 0.0 * 0.5 + 1.0 * 0.5
= 0.5 // 紫蓝色
result.a = src.a * 1.0 + dst.a * 0.0
= 0.5
// 最终颜色: (0.5, 0.0, 0.5, 0.5) - 半透明紫色
深度测试配置:
// 透明对象的深度状态
VkPipelineDepthStencilStateCreateInfo depth_stencil = {};
depth_stencil.depthTestEnable = VK_TRUE; // 启用深度测试
depth_stencil.depthWriteEnable = VK_FALSE; // ❌ 禁用深度写入
depth_stencil.depthCompareOp = VK_COMPARE_OP_LESS; // 更近的通过
🎨 完整实现
完整的 render_view_world.c 透明度支持:
#include "render_view_world.h"
#include "core/logger.h"
#include "core/kmemory.h"
#include "core/event.h"
#include "math/kmath.h"
#include "math/transform.h"
#include "containers/darray.h"
#include "systems/material_system.h"
#include "systems/shader_system.h"
#include "systems/camera_system.h"
#include "renderer/renderer_frontend.h"
typedef struct render_view_world_internal_data {
u32 shader_id;
f32 fov;
f32 near_clip;
f32 far_clip;
mat4 projection_matrix;
camera* world_camera;
vec4 ambient_colour;
u32 render_mode;
} render_view_world_internal_data;
/** @brief 用于按距离排序几何体的私有结构 */
typedef struct geometry_distance {
geometry_render_data g;
f32 distance;
} geometry_distance;
// 前向声明
static void quick_sort(geometry_distance arr[], i32 low_index, i32 high_index, b8 ascending);
// ... 其他函数实现 ...
b8 render_view_world_on_build_packet(const struct render_view* self,
void* data,
struct render_view_packet* out_packet) {
if (!self || !data || !out_packet) {
KWARN("render_view_world_on_build_packet requires valid pointers.");
return false;
}
mesh_packet_data* mesh_data = (mesh_packet_data*)data;
render_view_world_internal_data* internal_data = self->internal_data;
out_packet->geometries = darray_create(geometry_render_data);
out_packet->view = self;
out_packet->projection_matrix = internal_data->projection_matrix;
out_packet->view_matrix = camera_view_get(internal_data->world_camera);
out_packet->view_position = camera_position_get(internal_data->world_camera);
out_packet->ambient_colour = internal_data->ambient_colour;
geometry_distance* geometry_distances = darray_create(geometry_distance);
for (u32 i = 0; i < mesh_data->mesh_count; ++i) {
mesh* m = &mesh_data->meshes[i];
mat4 model = transform_get_world(&m->transform);
for (u32 j = 0; j < m->geometry_count; ++j) {
geometry_render_data render_data;
render_data.geometry = m->geometries[j];
render_data.model = model;
if ((m->geometries[j]->material->diffuse_map.texture->flags &
TEXTURE_FLAG_HAS_TRANSPARENCY) == 0) {
// 不透明对象
darray_push(out_packet->geometries, render_data);
out_packet->geometry_count++;
} else {
// 透明对象:计算距离
vec3 center = vec3_transform(render_data.geometry->center, model);
f32 distance = vec3_distance(center, internal_data->world_camera->position);
geometry_distance gdist;
gdist.distance = kabs(distance);
gdist.g = render_data;
darray_push(geometry_distances, gdist);
}
}
}
// 排序透明对象
u32 geometry_count = darray_length(geometry_distances);
if (geometry_count > 0) {
quick_sort(geometry_distances, 0, geometry_count - 1, false);
for (u32 i = 0; i < geometry_count; ++i) {
darray_push(out_packet->geometries, geometry_distances[i].g);
out_packet->geometry_count++;
}
}
darray_destroy(geometry_distances);
return true;
}
// 快速排序实现
static void swap(geometry_distance* a, geometry_distance* b) {
geometry_distance temp = *a;
*a = *b;
*b = temp;
}
static i32 partition(geometry_distance arr[], i32 low_index, i32 high_index, b8 ascending) {
geometry_distance pivot = arr[high_index];
i32 i = (low_index - 1);
for (i32 j = low_index; j <= high_index - 1; ++j) {
if (ascending) {
if (arr[j].distance < pivot.distance) {
++i;
swap(&arr[i], &arr[j]);
}
} else {
if (arr[j].distance > pivot.distance) {
++i;
swap(&arr[i], &arr[j]);
}
}
}
swap(&arr[i + 1], &arr[high_index]);
return i + 1;
}
static void quick_sort(geometry_distance arr[], i32 low_index, i32 high_index, b8 ascending) {
if (low_index < high_index) {
i32 partition_index = partition(arr, low_index, high_index, ascending);
quick_sort(arr, low_index, partition_index - 1, ascending);
quick_sort(arr, partition_index + 1, high_index, ascending);
}
}
🚀 实践练习
练习 1:实现归并排序
替换快速排序为归并排序,提供稳定排序:
/**
* @brief 归并两个已排序的子数组
*/
static void merge(geometry_distance arr[], i32 left, i32 mid, i32 right, b8 ascending) {
// TODO:
// 1. 创建临时数组
// 2. 归并左右两个子数组
// 3. 复制回原数组
}
/**
* @brief 归并排序主函数
*/
static void merge_sort(geometry_distance arr[], i32 left, i32 right, b8 ascending) {
// TODO:
// 1. 找到中点
// 2. 递归排序左半部分
// 3. 递归排序右半部分
// 4. 归并两个有序数组
}
提示:
- 归并排序是稳定排序(相同距离的对象保持原有顺序)
- 时间复杂度始终为 O(n log n)
- 需要额外的 O(n) 空间
练习 2:优化距离计算
使用距离的平方避免昂贵的平方根运算:
// 当前实现
f32 distance = vec3_distance(center, camera_pos);
gdist.distance = kabs(distance);
// 优化版本
// TODO: 使用 vec3_distance_squared
f32 distance_squared = vec3_distance_squared(center, camera_pos);
gdist.distance = distance_squared; // 不需要 sqrt
注意:排序结果相同,因为平方函数是单调的。
练习 3:实现多层透明度
支持多个透明层的正确混合:
typedef enum transparency_layer {
TRANSPARENCY_LAYER_NONE = 0,
TRANSPARENCY_LAYER_1 = 1, // 窗户
TRANSPARENCY_LAYER_2 = 2, // 水面
TRANSPARENCY_LAYER_3 = 3, // 粒子效果
} transparency_layer;
// TODO:
// - 为每个透明层创建单独的渲染列表
// - 按层级顺序渲染
// - 每层内部按距离排序
⚡ 性能优化
1. 空间分区
使用空间分区减少需要排序的对象数量:
typedef struct spatial_grid {
geometry_distance** cells;
u32 cell_size;
u32 grid_width;
u32 grid_height;
} spatial_grid;
// 只排序可见单元格中的对象
void sort_visible_cells(spatial_grid* grid, frustum* view_frustum) {
for (u32 i = 0; i < grid->grid_width; ++i) {
for (u32 j = 0; j < grid->grid_height; ++j) {
if (cell_in_frustum(i, j, view_frustum)) {
quick_sort(grid->cells[i * grid->grid_width + j], ...);
}
}
}
}
2. 帧间一致性
如果相机和对象移动不大,重用上一帧的排序:
typedef struct transparency_cache {
geometry_distance* sorted_geometries;
vec3 last_camera_position;
f32 movement_threshold;
} transparency_cache;
b8 needs_resort(transparency_cache* cache, vec3 current_camera_pos) {
f32 movement = vec3_distance(cache->last_camera_position, current_camera_pos);
return movement > cache->movement_threshold;
}
3. 批量渲染
对使用相同材质的透明对象进行批处理:
typedef struct transparent_batch {
material* material;
geometry_distance* geometries;
u32 count;
} transparent_batch;
// 先按材质分组,然后每组内部按距离排序
void batch_and_sort(geometry_distance* all_transparent, transparent_batch** out_batches) {
// 1. 按材质分组
// 2. 每组内部排序
// 3. 返回批次数组
}
❓ 常见问题
Q1: 为什么透明对象不能写入深度缓冲区?A: 如果透明对象写入深度,后面的对象会被错误地剔除:
场景:透明玻璃(距离 5)在不透明墙(距离 10)前面
如果玻璃写入深度:
1. 绘制玻璃,深度缓冲区 = 5
2. 尝试绘制墙,深度测试:10 > 5,失败
3. 墙被丢弃
4. 结果:❌ 看不到墙
如果玻璃不写入深度:
1. 绘制墙,深度缓冲区 = 10
2. 绘制玻璃,深度测试:5 < 10,通过
3. 玻璃与墙混合
4. 结果:✅ 透过玻璃看到墙
Q2: 如果两个透明对象相互穿插怎么办?
A: 这是透明度渲染的经典问题,没有完美解决方案:
问题:对象 A 的一部分在 B 前面,另一部分在 B 后面
解决方案:
- 细分几何体:将对象分割成更小的部分
// 将大的透明平面分割成小块
void subdivide_transparent_geometry(geometry* g, u32 subdivisions);
- 深度剥离(Depth Peeling):多次渲染透明层
// 渲染 N 层透明度
for (u32 layer = 0; layer < max_layers; ++layer) {
render_transparency_layer(layer);
}
- Order-Independent Transparency (OIT):
- 使用链表存储所有片段
- 排序并混合每个像素的所有片段
- 需要更多内存和计算
最佳实践:
- 对于大多数游戏,按对象中心排序已经足够
- 避免设计相互穿插的透明对象
- 使用粒子系统处理复杂的透明效果
A: 排序算法对比:
| 算法 | 平均时间 | 最坏时间 | 空间 | 稳定性 | 优点 |
|---|---|---|---|---|---|
| 快速排序 | O(n log n) | O(n²) | O(log n) | ❌ | 平均最快,原地排序 |
| 归并排序 | O(n log n) | O(n log n) | O(n) | ✅ | 稳定,最坏情况好 |
| 堆排序 | O(n log n) | O(n log n) | O(1) | ❌ | 空间效率高 |
| 插入排序 | O(n²) | O(n²) | O(1) | ✅ | 简单,小数组快 |
选择快速排序的原因:
- 平均情况下最快
- 原地排序,不需要额外内存
- 透明对象通常数量不多(< 100),最坏情况罕见
- 稳定性不重要(距离不同的对象)
优化:
// 当数组很小时,使用插入排序
#define QUICKSORT_THRESHOLD 10
static void quick_sort(geometry_distance arr[], i32 low, i32 high, b8 ascending) {
if (high - low < QUICKSORT_THRESHOLD) {
insertion_sort(arr, low, high, ascending);
return;
}
// ... 正常的快速排序 ...
}
Q4: 如何处理半透明粒子系统?
A: 粒子系统需要特殊处理:
typedef struct particle_system {
particle* particles;
u32 count;
b8 sort_particles; // 是否需要排序
} particle_system;
// 渲染粒子
void render_particles(particle_system* ps, vec3 camera_pos) {
if (ps->sort_particles) {
// 按距离排序粒子
for (u32 i = 0; i < ps->count; ++i) {
ps->particles[i].distance =
vec3_distance(ps->particles[i].position, camera_pos);
}
// 快速排序
qsort(ps->particles, ps->count, sizeof(particle), particle_compare);
}
// 渲染排序后的粒子
for (u32 i = 0; i < ps->count; ++i) {
draw_particle(&ps->particles[i]);
}
}
优化:
- 粒子通常很小,可以禁用深度测试
- 使用加法混合代替 alpha 混合(性能更好)
- 考虑使用 GPU 排序(Compute Shader)
A: 性能开销来源:
- 排序开销:
100 个透明对象:~0.1-0.2 ms
1000 个透明对象:~1-2 ms
10000 个透明对象:~10-20 ms
- 过度绘制(Overdraw):
- 透明对象不写入深度,后面的像素仍然会被绘制
- 多层透明会导致同一像素被绘制多次
- 混合操作:
- Alpha 混合需要读取帧缓冲区(带宽密集型)
- 打断Early-Z优化
优化建议:
// 1. 限制透明对象数量
#define MAX_TRANSPARENT_OBJECTS 256
// 2. 使用 LOD 系统
if (distance > far_threshold) {
// 远处使用不透明替代
use_opaque_version();
}
// 3. 使用视锥体剔除
if (!in_frustum(object)) {
continue; // 跳过不可见对象
}
// 4. 批量渲染
batch_by_material(transparent_objects);
📚 总结
透明度渲染是 3D 图形学中的核心挑战之一,本教程涵盖了:
✅ 关键要点
| 概念 | 要点 |
|---|---|
| 问题 | 透明对象需要特殊的渲染顺序 |
| 解决方案 | 按距离相机的距离排序(远→近) |
| 实现 | 快速排序 + 深度测试但不写入 |
| 混合 | Alpha 混合公式 |
| 性能 | 排序开销 + 过度绘制 |
🔑 核心技术
- 几何体分离:不透明和透明对象分开处理
- 距离计算:
vec3_transform+vec3_distance - 快速排序:O(n log n) 平均时间复杂度
- Alpha 混合:
(src × src.a) + (dst × (1 - src.a)) - 深度配置:测试启用,写入禁用
📈 渲染管线
🚀 下一步
在下一篇教程中,我们将学习:
- 教程 47:更多渲染技术和优化
透明度渲染 是实现逼真图形效果的关键技术,正确处理透明度可以大大提升视觉质量!
2309

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



