教程 46 - 透明度渲染

2025博客之星年度评选已开启 10w+人浏览 1.7k人参与


🔗 快速导航


📋 目录

点击展开/折叠

🎯 概述

透明度渲染(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,通过(对于地面部分)
   - 深度缓冲区:不写入(透明对象)
   - 颜色缓冲区:与背景混合

结果:✅ 透过玻璃看到地面,立方体正确遮挡

💡 解决方案:深度排序

解决透明度渲染问题的标准方法是深度排序

收集所有几何体
有透明度?
不透明列表
透明列表
直接渲染
任意顺序
计算距离相机的距离
按距离排序
从远到近
渲染排序后的列表
最终渲染结果

关键步骤

  1. 分离几何体:将不透明和透明对象分开
  2. 计算距离:对每个透明对象,计算其中心到相机的距离
  3. 排序:按距离从远到近排序
  4. 渲染
    • 先渲染所有不透明对象(任意顺序)
    • 再渲染排序后的透明对象(从远到近)

📦 核心数据结构

几何体距离结构

/**
 * @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;
}

关键点

  1. 分离处理:不透明对象直接添加,透明对象单独处理
  2. 距离计算:使用 vec3_transformvec3_distance
  3. 排序:使用快速排序,降序(远到近)
  4. 合并:先渲染不透明对象,再渲染排序后的透明对象

快速排序算法

/**
 * @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);
    }
}

快速排序工作原理

初始数组: 3.5, 1.2, 5.7, 2.1, 4.3
选择枢轴: 4.3
分区
小于 4.3: 3.5, 1.2, 2.1
枢轴: 4.3
大于 4.3: 5.7
递归排序左侧
递归排序右侧
1.2, 2.1, 3.5
5.7
最终结果: 1.2, 2.1, 3.5, 4.3, 5.7

时间复杂度

  • 平均情况: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
// 渲染顺序: 先渲染最远的,最后渲染最近的

🔄 渲染流程对比

传统方式(错误)

应用程序 渲染器 绘制对象 A(透明) 深度测试 ✓ 写入颜色 ❌ 不写入深度 绘制对象 B(不透明) 深度测试 ✓ 写入颜色 写入深度 ❌ 对象 A 后面的内容 可能被错误遮挡 应用程序 渲染器

深度排序方式(正确)

应用程序 视图系统 渲染器 提供所有几何体 分离不透明和透明对象 计算透明对象距离 快速排序(远→近) 1. 渲染所有不透明对象 深度测试 + 深度写入 2. 渲染透明对象(已排序) 深度测试 Alpha 混合 ❌ 不写入深度 loop [从远到近] ✅ 正确的透明效果 应用程序 视图系统 渲染器

⚙️ 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 后面

解决方案

  1. 细分几何体:将对象分割成更小的部分
// 将大的透明平面分割成小块
void subdivide_transparent_geometry(geometry* g, u32 subdivisions);
  1. 深度剥离(Depth Peeling):多次渲染透明层
// 渲染 N 层透明度
for (u32 layer = 0; layer < max_layers; ++layer) {
    render_transparency_layer(layer);
}
  1. Order-Independent Transparency (OIT)
    • 使用链表存储所有片段
    • 排序并混合每个像素的所有片段
    • 需要更多内存和计算

最佳实践

  • 对于大多数游戏,按对象中心排序已经足够
  • 避免设计相互穿插的透明对象
  • 使用粒子系统处理复杂的透明效果
Q3: 为什么使用快速排序而不是其他排序算法?

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)
Q5: 透明度渲染对性能的影响有多大?

A: 性能开销来源:

  1. 排序开销
100 个透明对象:~0.1-0.2 ms
1000 个透明对象:~1-2 ms
10000 个透明对象:~10-20 ms
  1. 过度绘制(Overdraw)
  • 透明对象不写入深度,后面的像素仍然会被绘制
  • 多层透明会导致同一像素被绘制多次
  1. 混合操作
  • 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 混合公式
性能排序开销 + 过度绘制

🔑 核心技术

  1. 几何体分离:不透明和透明对象分开处理
  2. 距离计算vec3_transform + vec3_distance
  3. 快速排序:O(n log n) 平均时间复杂度
  4. Alpha 混合(src × src.a) + (dst × (1 - src.a))
  5. 深度配置:测试启用,写入禁用

📈 渲染管线

收集几何体
分离不透明/透明
渲染不透明对象
计算透明对象距离
快速排序
渲染透明对象
最终图像

🚀 下一步

在下一篇教程中,我们将学习:

  • 教程 47:更多渲染技术和优化

透明度渲染 是实现逼真图形效果的关键技术,正确处理透明度可以大大提升视觉质量!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值