现代Minecraft渲染导论 第三章:高等渲染(计算机图形学)

3.1 HDR 渲染

本节目标:理解 HDR 渲染的原理,学会实现 HDR 渲染管线

3.1.1 什么是 HDR?

HDR(High Dynamic Range,高动态范围) 是一种能够表示更大亮度范围的渲染技术。

LDR vs HDR

plaintext 复制代码
LDR(Low Dynamic Range):
- 每个颜色通道 8 位(0-255)
- 亮度范围 [0, 1]
- 无法表示"非常亮"的物体

HDR(High Dynamic Range):
- 每个颜色通道 16/32 位浮点
- 亮度范围 [0, ∞)
- 可以表示太阳、火焰等超亮物体

为什么需要 HDR?

plaintext 复制代码
场景:一个房间,窗外是明亮的太阳

LDR 渲染:
┌─────────────────────────────────┐
│  室内:暗淡                      │
│  窗户:纯白色(已经饱和)         │  ← 无法区分"亮"和"非常亮"
│  太阳:纯白色(和窗户一样)       │
└─────────────────────────────────┘

HDR 渲染:
┌─────────────────────────────────┐
│  室内:0.1 - 0.3                │
│  窗户:1.0 - 2.0                │  ← 可以表示不同亮度级别
│  太阳:10.0 - 100.0             │
└─────────────────────────────────┘
经过色调映射后,保留亮度层次感

3.1.2 HDR 渲染管线

plaintext 复制代码
┌─────────────────────────────────────────────────────────────────┐
│                      HDR 渲染管线                                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. 场景渲染(HDR 帧缓冲)                                       │
│     ┌─────────────┐                                             │
│     │   场景      │ ──► HDR FBO (RGBA16F/RGBA32F)               │
│     │ 颜色可超过1 │                                             │
│     └─────────────┘                                             │
│                                                                 │
│  2. 后处理(可选)                                               │
│     ├─ Bloom 提取                                               │
│     ├─ 模糊处理                                                 │
│     └─ 其他效果                                                 │
│                                                                 │
│  3. 色调映射(Tone Mapping)                                    │
│     ┌─────────────┐      ┌─────────────┐      ┌─────────────┐  │
│     │  HDR 图像   │ ──► │ 色调映射    │ ──► │  LDR 图像   │  │
│     │  [0, ∞)    │      │  着色器     │      │  [0, 1]     │  │
│     └─────────────┘      └─────────────┘      └─────────────┘  │
│                                                                 │
│  4. 输出到屏幕                                                  │
│     └─ 标准 8 位显示                                            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

3.1.3 HDR 帧缓冲格式

常用 HDR 格式

格式 每像素字节 精度 用途
GL_RGBA16F 8 半精度浮点 常用,性能好
GL_RGBA32F 16 单精度浮点 高精度,性能差
GL_R11F_G11F_B10F 4 特殊浮点 无 Alpha,紧凑
GL_RGB9_E5 4 共享指数 只读,紧凑

创建 HDR 帧缓冲

java 复制代码
// 创建 FBO
int fbo = GL30.glGenFramebuffers();
GL30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, fbo);

// 创建 HDR 颜色纹理
int colorTexture = GL11.glGenTextures();
GL11.glBindTexture(GL11.GL_TEXTURE_2D, colorTexture);
GL11.glTexImage2D(
    GL11.GL_TEXTURE_2D,
    0,
    GL30.GL_RGBA16F,      // HDR 格式
    width, height,
    0,
    GL11.GL_RGBA,
    GL11.GL_FLOAT,        // 浮点数据
    (ByteBuffer) null
);

// 设置纹理参数
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE);
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE);

// 附加到 FBO
GL30.glFramebufferTexture2D(
    GL30.GL_FRAMEBUFFER,
    GL30.GL_COLOR_ATTACHMENT0,
    GL11.GL_TEXTURE_2D,
    colorTexture,
    0
);

// 检查完整性
if (GL30.glCheckFramebufferStatus(GL30.GL_FRAMEBUFFER) != GL30.GL_FRAMEBUFFER_COMPLETE) {
    throw new RuntimeException("HDR Framebuffer incomplete!");
}

3.1.4 色调映射(Tone Mapping)

色调映射将 HDR 值压缩到 LDR 范围 [0, 1]。

常用色调映射算法

1. Reinhard 色调映射

glsl 复制代码
// 简单 Reinhard
vec3 reinhard(vec3 hdr) {
    return hdr / (hdr + vec3(1.0));
}

// 扩展 Reinhard(可控制最大亮度)
vec3 reinhardExtended(vec3 hdr, float maxWhite) {
    vec3 numerator = hdr * (1.0 + hdr / (maxWhite * maxWhite));
    return numerator / (1.0 + hdr);
}

2. ACES 色调映射

glsl 复制代码
// ACES Filmic Tone Mapping
vec3 aces(vec3 hdr) {
    float a = 2.51;
    float b = 0.03;
    float c = 2.43;
    float d = 0.59;
    float e = 0.14;
    return clamp((hdr * (a * hdr + b)) / (hdr * (c * hdr + d) + e), 0.0, 1.0);
}

3. Uncharted 2 色调映射

glsl 复制代码
vec3 uncharted2Tonemap(vec3 x) {
    float A = 0.15;
    float B = 0.50;
    float C = 0.10;
    float D = 0.20;
    float E = 0.02;
    float F = 0.30;
    return ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F;
}

vec3 uncharted2(vec3 hdr) {
    float exposureBias = 2.0;
    vec3 curr = uncharted2Tonemap(exposureBias * hdr);
    vec3 whiteScale = 1.0 / uncharted2Tonemap(vec3(11.2));
    return curr * whiteScale;
}

色调映射着色器

glsl 复制代码
#version 150

uniform sampler2D HDRBuffer;
uniform float Exposure;
uniform int ToneMapMode;  // 0=Reinhard, 1=ACES, 2=Uncharted2

in vec2 texCoord;
out vec4 fragColor;

vec3 reinhard(vec3 hdr) {
    return hdr / (hdr + vec3(1.0));
}

vec3 aces(vec3 hdr) {
    float a = 2.51, b = 0.03, c = 2.43, d = 0.59, e = 0.14;
    return clamp((hdr * (a * hdr + b)) / (hdr * (c * hdr + d) + e), 0.0, 1.0);
}

void main() {
    vec3 hdr = texture(HDRBuffer, texCoord).rgb;

    // 应用曝光
    hdr *= Exposure;

    // 色调映射
    vec3 ldr;
    if (ToneMapMode == 0) {
        ldr = reinhard(hdr);
    } else if (ToneMapMode == 1) {
        ldr = aces(hdr);
    } else {
        ldr = hdr;  // 无色调映射
    }

    // Gamma 校正
    ldr = pow(ldr, vec3(1.0 / 2.2));

    fragColor = vec4(ldr, 1.0);
}

3.1.5 曝光控制

手动曝光

glsl 复制代码
vec3 hdr = texture(HDRBuffer, texCoord).rgb;
hdr *= exposure;  // exposure 由用户控制

自动曝光

基于场景平均亮度自动调整曝光:

glsl 复制代码
// 1. 计算场景平均亮度(通常用 compute shader 或多次降采样)
float avgLuminance = calculateAverageLuminance();

// 2. 根据平均亮度计算曝光
float keyValue = 0.18;  // 中灰值
float exposure = keyValue / avgLuminance;

// 3. 应用曝光
vec3 exposed = hdr * exposure;

3.1.6 HDR 渲染的优势

1. Bloom 效果更自然

plaintext 复制代码
LDR Bloom:
- 只能对接近 1.0 的像素做 Bloom
- 效果不自然

HDR Bloom:
- 可以对超过 1.0 的像素做 Bloom
- 亮度越高,Bloom 越强
- 效果更真实

2. 光照计算更准确

glsl 复制代码
// HDR 允许物理正确的光照计算
vec3 lighting = albedo * (ambient + diffuse + specular);
// lighting 可以超过 1.0,后续再做色调映射

3. 后处理效果更好

  • 运动模糊
  • 景深
  • 镜头光晕
  • 都能从 HDR 数据中获益

3.1.7 性能考虑

带宽

plaintext 复制代码
LDR (RGBA8):   4 bytes/pixel
HDR (RGBA16F): 8 bytes/pixel  ← 2倍带宽
HDR (RGBA32F): 16 bytes/pixel ← 4倍带宽

建议

  1. 使用 RGBA16F 而不是 RGBA32F(精度足够,性能更好)
  2. 只在需要的地方使用 HDR(最终输出仍是 LDR)
  3. 考虑使用 R11F_G11F_B10F(如果不需要 Alpha)

小结

概念 说明
HDR 高动态范围,亮度可超过 1.0
LDR 低动态范围,亮度限制在 [0, 1]
RGBA16F 常用 HDR 格式
色调映射 HDR → LDR 的转换
曝光 控制整体亮度

HDR 渲染流程

plaintext 复制代码
场景渲染 (HDR) → 后处理 → 色调映射 → Gamma 校正 → 输出 (LDR)

3.2 Bloom 效果

本节目标:理解 Bloom 效果的原理,学会实现高质量的 Bloom

3.2.1 什么是 Bloom?

Bloom(泛光/辉光) 是一种模拟真实相机或人眼看到强光时产生的光晕效果。

plaintext 复制代码
没有 Bloom:                有 Bloom:
┌─────────────────┐        ┌─────────────────┐
│                 │        │    ░░░░░░░      │
│       ●         │        │  ░░░░●░░░░░    │  ← 光源周围有光晕
│                 │        │    ░░░░░░░      │
│                 │        │                 │
└─────────────────┘        └─────────────────┘

为什么会有 Bloom?

在现实中,Bloom 来自:

  1. 镜头散射:相机镜头内部的光线散射
  2. 眼睛散射:眼球内部的光线散射
  3. 大气散射:空气中的微粒散射光线

3.2.2 Bloom 实现原理

基本流程

plaintext 复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                        Bloom 实现流程                                │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  1. 亮度提取(Bright Pass)                                         │
│     ┌─────────────┐      ┌─────────────┐      ┌─────────────┐      │
│     │  原始图像   │ ──► │ 阈值过滤    │ ──► │  高亮部分   │      │
│     │  (HDR)     │      │ (>threshold)│      │             │      │
│     └─────────────┘      └─────────────┘      └─────────────┘      │
│                                                                     │
│  2. 模糊处理(Blur)                                                │
│     ┌─────────────┐      ┌─────────────┐      ┌─────────────┐      │
│     │  高亮部分   │ ──► │ 高斯模糊    │ ──► │  模糊高亮   │      │
│     │             │      │ (多次)      │      │             │      │
│     └─────────────┘      └─────────────┘      └─────────────┘      │
│                                                                     │
│  3. 合成(Composite)                                               │
│     ┌─────────────┐                                                 │
│     │  原始图像   │ ─┐                                              │
│     └─────────────┘  │    ┌─────────────┐      ┌─────────────┐     │
│                      ├──► │   叠加      │ ──► │  最终图像   │     │
│     ┌─────────────┐  │    └─────────────┘      └─────────────┘     │
│     │  模糊高亮   │ ─┘                                              │
│     └─────────────┘                                                 │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

3.2.3 亮度提取(Bright Pass)

简单阈值

glsl 复制代码
#version 150

uniform sampler2D SourceTexture;
uniform float Threshold;

in vec2 texCoord;
out vec4 fragColor;

void main() {
    vec4 color = texture(SourceTexture, texCoord);

    // 计算亮度
    float brightness = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722));

    // 阈值过滤
    if (brightness > Threshold) {
        fragColor = color;
    } else {
        fragColor = vec4(0.0);
    }
}

软阈值(更平滑)

glsl 复制代码
uniform float Threshold;
uniform float SoftThreshold;  // 0.0 - 1.0

void main() {
    vec4 color = texture(SourceTexture, texCoord);
    float brightness = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722));

    // 软阈值:平滑过渡
    float soft = brightness - Threshold + SoftThreshold;
    soft = clamp(soft / (2.0 * SoftThreshold + 0.00001), 0.0, 1.0);
    soft = soft * soft;

    float contribution = max(soft, brightness - Threshold) / max(brightness, 0.00001);
    fragColor = color * contribution;
}

Photon 的亮度提取着色器

参考Photon项目

glsl 复制代码
// bright_pass.fsh
#version 150

uniform sampler2D DiffuseSampler;
uniform float Threshold;
uniform float Knee;

in vec2 texCoord;
out vec4 fragColor;

void main() {
    vec4 color = texture(DiffuseSampler, texCoord);

    // 计算亮度(使用感知亮度权重)
    float brightness = max(max(color.r, color.g), color.b);

    // 软膝盖曲线
    float soft = brightness - Threshold + Knee;
    soft = clamp(soft / (2.0 * Knee + 0.0001), 0.0, 1.0);
    soft = soft * soft * Knee;

    float contribution = max(soft, brightness - Threshold);
    contribution /= max(brightness, 0.0001);

    fragColor = color * contribution;
}

3.2.4 高斯模糊

两遍分离模糊

高斯模糊可以分解为水平和垂直两遍,大大减少计算量:

复制代码
2D 高斯模糊 (N×N) = 水平模糊 (N×1) + 垂直模糊 (1×N)

计算量:N×N → N+N
例如:9×9=81 → 9+9=18

高斯模糊着色器

glsl 复制代码
// gaussian_blur.fsh
#version 150

uniform sampler2D SourceTexture;
uniform vec2 Direction;  // (1,0) 水平 或 (0,1) 垂直
uniform vec2 TexelSize;  // 1.0 / textureSize

in vec2 texCoord;
out vec4 fragColor;

// 9-tap 高斯权重
const float weights[5] = float[](0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216);

void main() {
    vec4 result = texture(SourceTexture, texCoord) * weights[0];

    vec2 offset = Direction * TexelSize;

    for (int i = 1; i < 5; i++) {
        result += texture(SourceTexture, texCoord + offset * float(i)) * weights[i];
        result += texture(SourceTexture, texCoord - offset * float(i)) * weights[i];
    }

    fragColor = result;
}

优化:线性采样

利用 GPU 的双线性过滤,可以用更少的采样获得相同效果:

glsl 复制代码
// 优化版本:5-tap 实现 9-tap 效果
const float offsets[3] = float[](0.0, 1.3846153846, 3.2307692308);
const float weights[3] = float[](0.2270270270, 0.3162162162, 0.0702702703);

void main() {
    vec4 result = texture(SourceTexture, texCoord) * weights[0];

    vec2 offset = Direction * TexelSize;

    for (int i = 1; i < 3; i++) {
        result += texture(SourceTexture, texCoord + offset * offsets[i]) * weights[i];
        result += texture(SourceTexture, texCoord - offset * offsets[i]) * weights[i];
    }

    fragColor = result;
}

3.2.5 降采样与升采样

为什么需要降采样?

  1. 减少计算量:在低分辨率上模糊更快
  2. 增大模糊范围:相同 kernel 在低分辨率上覆盖更大区域
  3. 多级 Bloom:不同分辨率产生不同大小的光晕

Mipmap 链

复制代码
原始分辨率:1920×1080
    ↓ 降采样
Mip 1:960×540
    ↓ 降采样
Mip 2:480×270
    ↓ 降采样
Mip 3:240×135
    ↓ 降采样
Mip 4:120×67

降采样着色器

glsl 复制代码
// down_sampling.fsh
#version 150

uniform sampler2D DiffuseSampler;
uniform vec2 TexelSize;

in vec2 texCoord;
out vec4 fragColor;

void main() {
    // 4x4 box filter with bilinear sampling
    // 实际采样 13 个点,利用双线性过滤
    vec4 sum = vec4(0.0);

    // 中心
    sum += texture(DiffuseSampler, texCoord) * 4.0;

    // 四角
    sum += texture(DiffuseSampler, texCoord + vec2(-1.0, -1.0) * TexelSize);
    sum += texture(DiffuseSampler, texCoord + vec2( 1.0, -1.0) * TexelSize);
    sum += texture(DiffuseSampler, texCoord + vec2(-1.0,  1.0) * TexelSize);
    sum += texture(DiffuseSampler, texCoord + vec2( 1.0,  1.0) * TexelSize);

    // 四边
    sum += texture(DiffuseSampler, texCoord + vec2(-1.0,  0.0) * TexelSize) * 2.0;
    sum += texture(DiffuseSampler, texCoord + vec2( 1.0,  0.0) * TexelSize) * 2.0;
    sum += texture(DiffuseSampler, texCoord + vec2( 0.0, -1.0) * TexelSize) * 2.0;
    sum += texture(DiffuseSampler, texCoord + vec2( 0.0,  1.0) * TexelSize) * 2.0;

    fragColor = sum / 16.0;
}

升采样着色器

glsl 复制代码
// up_sampling.fsh
#version 150

uniform sampler2D DiffuseSampler;  // 低分辨率
uniform sampler2D HighResSampler;  // 高分辨率(上一级)
uniform vec2 TexelSize;
uniform float BloomIntensity;

in vec2 texCoord;
out vec4 fragColor;

void main() {
    // 3x3 tent filter
    vec4 sum = vec4(0.0);

    sum += texture(DiffuseSampler, texCoord + vec2(-1.0, -1.0) * TexelSize) * 1.0;
    sum += texture(DiffuseSampler, texCoord + vec2( 0.0, -1.0) * TexelSize) * 2.0;
    sum += texture(DiffuseSampler, texCoord + vec2( 1.0, -1.0) * TexelSize) * 1.0;

    sum += texture(DiffuseSampler, texCoord + vec2(-1.0,  0.0) * TexelSize) * 2.0;
    sum += texture(DiffuseSampler, texCoord + vec2( 0.0,  0.0) * TexelSize) * 4.0;
    sum += texture(DiffuseSampler, texCoord + vec2( 1.0,  0.0) * TexelSize) * 2.0;

    sum += texture(DiffuseSampler, texCoord + vec2(-1.0,  1.0) * TexelSize) * 1.0;
    sum += texture(DiffuseSampler, texCoord + vec2( 0.0,  1.0) * TexelSize) * 2.0;
    sum += texture(DiffuseSampler, texCoord + vec2( 1.0,  1.0) * TexelSize) * 1.0;

    vec4 bloom = sum / 16.0;

    // 与高分辨率叠加
    vec4 highRes = texture(HighResSampler, texCoord);
    fragColor = highRes + bloom * BloomIntensity;
}

3.2.6 最终合成

glsl 复制代码
// bloom_composite.fsh
#version 150

uniform sampler2D SceneTexture;   // 原始场景
uniform sampler2D BloomTexture;   // Bloom 结果
uniform float BloomStrength;

in vec2 texCoord;
out vec4 fragColor;

void main() {
    vec4 scene = texture(SceneTexture, texCoord);
    vec4 bloom = texture(BloomTexture, texCoord);

    // 加法混合
    fragColor = scene + bloom * BloomStrength;
}

3.2.7 Bloom 质量调优

参数调整

参数 效果
Threshold 越低,越多像素参与 Bloom
Intensity Bloom 强度
Mip Count 越多,光晕越大越柔和
Blur Iterations 越多,越模糊

常见问题

问题 原因 解决方案
Bloom 太强 Intensity 太高 降低 Intensity
Bloom 有锯齿 降采样不够 增加 Mip 级数
性能差 分辨率太高 在更低分辨率处理
颜色偏移 阈值太低 提高 Threshold

3.2.8 高级技巧

1. 颜色 Bloom

不同颜色可以有不同的 Bloom 强度:

glsl 复制代码
// 根据颜色调整 Bloom
vec3 bloomColor = color.rgb;
bloomColor.r *= 1.2;  // 红色 Bloom 更强
bloomColor.b *= 0.8;  // 蓝色 Bloom 更弱

2. 镜头污垢(Lens Dirt)

glsl 复制代码
uniform sampler2D DirtTexture;

void main() {
    vec4 bloom = texture(BloomTexture, texCoord);
    vec4 dirt = texture(DirtTexture, texCoord);

    // 污垢增强 Bloom
    fragColor = scene + bloom * (1.0 + dirt.r * dirtIntensity);
}

3. 光晕(Lens Flare)

结合 Bloom 实现镜头光晕效果。

小结

概念 说明
Bloom 模拟强光的光晕效果
Bright Pass 提取高亮部分
高斯模糊 产生柔和光晕
降采样/升采样 多级处理,增大范围
合成 叠加到原始图像

Bloom 流程

复制代码
原始图像 → 亮度提取 → 降采样链 → 升采样链 → 合成 → 最终图像

3.3 MRT 多渲染目标

本节目标:理解 MRT 的原理,学会同时输出多个渲染结果

3.3.1 什么是 MRT?

MRT(Multiple Render Targets,多渲染目标) 允许片段着色器同时输出到多个颜色附件。

复制代码
传统渲染(单目标):
片段着色器 ──► 颜色附件 0

MRT 渲染(多目标):
                ┌──► 颜色附件 0(颜色)
片段着色器 ────┼──► 颜色附件 1(法线)
                ├──► 颜色附件 2(位置)
                └──► 颜色附件 3(材质)

3.3.2 为什么需要 MRT?

传统方式:多 Pass

plaintext 复制代码
Pass 1: 渲染颜色 ──► FBO 1
Pass 2: 渲染法线 ──► FBO 2
Pass 3: 渲染深度 ──► FBO 3

问题:
- 场景渲染 3 次
- 性能差
- 顶点处理重复

MRT 方式:单 Pass

plaintext 复制代码
Pass 1: 同时渲染颜色、法线、深度 ──► FBO(3 个颜色附件)

优势:
- 场景只渲染 1 次
- 性能好
- 顶点处理只做一次

3.3.3 MRT 的应用场景

应用 输出内容
延迟渲染 颜色、法线、位置、材质
Bloom 颜色、高亮部分
运动模糊 颜色、速度向量
SSAO 颜色、法线、深度
阴影 颜色、阴影遮罩

3.3.4 创建 MRT 帧缓冲

OpenGL 实现

java 复制代码
// 创建 FBO
int fbo = GL30.glGenFramebuffers();
GL30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, fbo);

// 创建多个颜色纹理
int[] colorTextures = new int[3];
GL11.glGenTextures(colorTextures);

for (int i = 0; i < 3; i++) {
    GL11.glBindTexture(GL11.GL_TEXTURE_2D, colorTextures[i]);
    GL11.glTexImage2D(
        GL11.GL_TEXTURE_2D, 0,
        GL30.GL_RGBA16F,  // HDR 格式
        width, height, 0,
        GL11.GL_RGBA, GL11.GL_FLOAT,
        (ByteBuffer) null
    );

    GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
    GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);

    // 附加到不同的颜色附件
    GL30.glFramebufferTexture2D(
        GL30.GL_FRAMEBUFFER,
        GL30.GL_COLOR_ATTACHMENT0 + i,  // ATTACHMENT0, ATTACHMENT1, ATTACHMENT2
        GL11.GL_TEXTURE_2D,
        colorTextures[i],
        0
    );
}

// 创建深度缓冲
int depthBuffer = GL30.glGenRenderbuffers();
GL30.glBindRenderbuffer(GL30.GL_RENDERBUFFER, depthBuffer);
GL30.glRenderbufferStorage(GL30.GL_RENDERBUFFER, GL30.GL_DEPTH_COMPONENT24, width, height);
GL30.glFramebufferRenderbuffer(
    GL30.GL_FRAMEBUFFER,
    GL30.GL_DEPTH_ATTACHMENT,
    GL30.GL_RENDERBUFFER,
    depthBuffer
);

// 指定绘制缓冲(关键!)
int[] drawBuffers = {
    GL30.GL_COLOR_ATTACHMENT0,
    GL30.GL_COLOR_ATTACHMENT1,
    GL30.GL_COLOR_ATTACHMENT2
};
GL20.glDrawBuffers(drawBuffers);

// 检查完整性
if (GL30.glCheckFramebufferStatus(GL30.GL_FRAMEBUFFER) != GL30.GL_FRAMEBUFFER_COMPLETE) {
    throw new RuntimeException("MRT Framebuffer incomplete!");
}

glDrawBuffers 的重要性

java 复制代码
// 必须调用 glDrawBuffers 指定输出目标
// 否则只有 COLOR_ATTACHMENT0 会被写入

GL20.glDrawBuffers(new int[] {
    GL30.GL_COLOR_ATTACHMENT0,  // layout(location = 0)
    GL30.GL_COLOR_ATTACHMENT1,  // layout(location = 1)
    GL30.GL_COLOR_ATTACHMENT2   // layout(location = 2)
});

3.3.5 MRT 片段着色器

多输出声明

glsl 复制代码
#version 150

// 输入
in vec3 fragPos;
in vec3 fragNormal;
in vec2 texCoord;

// Uniform
uniform sampler2D DiffuseTexture;

// 多个输出(对应不同的颜色附件)
layout(location = 0) out vec4 outColor;    // COLOR_ATTACHMENT0
layout(location = 1) out vec4 outNormal;   // COLOR_ATTACHMENT1
layout(location = 2) out vec4 outPosition; // COLOR_ATTACHMENT2

void main() {
    // 输出颜色
    outColor = texture(DiffuseTexture, texCoord);

    // 输出法线(归一化到 [0,1] 范围)
    outNormal = vec4(normalize(fragNormal) * 0.5 + 0.5, 1.0);

    // 输出位置
    outPosition = vec4(fragPos, 1.0);
}

Photon 的 MRT 着色器

glsl 复制代码
// photon_mrt_particle.fsh
#version 150

uniform sampler2D Sampler0;
uniform vec4 ColorModulator;
uniform float BloomThreshold;

in vec2 texCoord0;
in vec4 vertexColor;

// 双输出:颜色 + Bloom
layout(location = 0) out vec4 fragColor;
layout(location = 1) out vec4 bloomColor;

void main() {
    vec4 color = texture(Sampler0, texCoord0) * vertexColor * ColorModulator;

    if (color.a < 0.01) {
        discard;
    }

    // 输出 1:正常颜色
    fragColor = color;

    // 输出 2:Bloom(超过阈值的部分)
    float brightness = max(max(color.r, color.g), color.b);
    if (brightness > BloomThreshold) {
        bloomColor = color * (brightness - BloomThreshold) / brightness;
    } else {
        bloomColor = vec4(0.0);
    }
}

3.3.6 使用 MRT 渲染

渲染流程

kotlin 复制代码
fun render(poseStack: PoseStack, camera: Camera, partialTick: Float) {
    // 1. 绑定 MRT 目标
    mrtTarget.bindWrite()
    mrtTarget.clear()

    // 2. 设置 MRT 着色器
    RenderSystem.setShader { mrtShader }

    // 3. 渲染场景
    // 着色器会同时输出到两个颜色附件
    renderParticles(poseStack, camera, partialTick)

    // 4. 解绑
    mrtTarget.unbindWrite()

    // 5. 使用渲染结果
    // drawTarget.colorTextureId - 正常颜色
    // bloomTarget.colorTextureId - Bloom 数据
}

后续处理

kotlin 复制代码
fun postProcess() {
    // 对 Bloom 数据进行模糊处理
    bloomRenderer.processBloom(mrtTarget.bloomTarget)

    // 合成最终图像
    compositeToScreen(
        mrtTarget.drawTarget,
        bloomRenderer.getBloomResult()
    )
}

3.3.7 延迟渲染(Deferred Rendering)

MRT 最经典的应用是延迟渲染:

G-Buffer 结构

复制代码
┌─────────────────────────────────────────────────────────────┐
│                      G-Buffer                                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  COLOR_ATTACHMENT0: 漫反射颜色 (RGB) + 镜面强度 (A)         │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  R: Diffuse.r  G: Diffuse.g  B: Diffuse.b  A: Spec  │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  COLOR_ATTACHMENT1: 世界空间法线 (RGB)                      │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  R: Normal.x   G: Normal.y   B: Normal.z   A: -     │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  COLOR_ATTACHMENT2: 世界空间位置 (RGB)                      │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  R: Pos.x      G: Pos.y      B: Pos.z      A: -     │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  DEPTH_ATTACHMENT: 深度                                     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

延迟渲染流程

plaintext 复制代码
1. 几何 Pass(填充 G-Buffer)
   ┌─────────────┐
   │   场景      │ ──► G-Buffer(颜色、法线、位置)
   └─────────────┘

2. 光照 Pass(使用 G-Buffer 计算光照)
   ┌─────────────┐      ┌─────────────┐      ┌─────────────┐
   │  G-Buffer   │ ──► │ 光照计算    │ ──► │  最终图像   │
   │  + 光源     │      │ (全屏 Pass) │      │             │
   └─────────────┘      └─────────────┘      └─────────────┘

延迟渲染着色器

glsl 复制代码
// geometry_pass.fsh - 几何 Pass
#version 150

in vec3 fragPos;
in vec3 fragNormal;
in vec2 texCoord;

uniform sampler2D DiffuseTexture;
uniform float Specular;

layout(location = 0) out vec4 gAlbedoSpec;
layout(location = 1) out vec4 gNormal;
layout(location = 2) out vec4 gPosition;

void main() {
    gAlbedoSpec.rgb = texture(DiffuseTexture, texCoord).rgb;
    gAlbedoSpec.a = Specular;

    gNormal = vec4(normalize(fragNormal), 1.0);

    gPosition = vec4(fragPos, 1.0);
}

// lighting_pass.fsh - 光照 Pass
#version 150

uniform sampler2D gAlbedoSpec;
uniform sampler2D gNormal;
uniform sampler2D gPosition;

uniform vec3 LightPos;
uniform vec3 LightColor;
uniform vec3 ViewPos;

in vec2 texCoord;
out vec4 fragColor;

void main() {
    vec3 albedo = texture(gAlbedoSpec, texCoord).rgb;
    float specular = texture(gAlbedoSpec, texCoord).a;
    vec3 normal = texture(gNormal, texCoord).rgb;
    vec3 position = texture(gPosition, texCoord).rgb;

    // 光照计算
    vec3 lightDir = normalize(LightPos - position);
    vec3 viewDir = normalize(ViewPos - position);
    vec3 halfDir = normalize(lightDir + viewDir);

    // 漫反射
    float diff = max(dot(normal, lightDir), 0.0);
    vec3 diffuse = diff * LightColor * albedo;

    // 镜面反射
    float spec = pow(max(dot(normal, halfDir), 0.0), 32.0);
    vec3 specularColor = spec * LightColor * specular;

    fragColor = vec4(diffuse + specularColor, 1.0);
}

3.3.8 MRT 限制和注意事项

硬件限制

java 复制代码
// 查询最大颜色附件数
int maxAttachments = GL11.glGetInteger(GL30.GL_MAX_COLOR_ATTACHMENTS);
// 通常是 8

// 查询最大绘制缓冲数
int maxDrawBuffers = GL11.glGetInteger(GL20.GL_MAX_DRAW_BUFFERS);
// 通常是 8

格式一致性

java 复制代码
// 所有颜色附件的格式不必相同,但建议保持一致
// 不同格式可能影响性能

混合模式

java 复制代码
// MRT 时,混合模式应用于所有颜色附件
// 如果需要不同的混合模式,需要使用 glBlendFunci (OpenGL 4.0+)
GL40.glBlendFunci(0, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);  // 附件 0
GL40.glBlendFunci(1, GL_ONE, GL_ONE);  // 附件 1:加法混合

清除

java 复制代码
// 清除所有颜色附件
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);

// 或者单独清除某个附件
GL30.glClearBufferfv(GL11.GL_COLOR, 0, new float[]{0, 0, 0, 0});  // 附件 0
GL30.glClearBufferfv(GL11.GL_COLOR, 1, new float[]{0, 0, 0, 0});  // 附件 1

3.3.9 性能考虑

带宽

复制代码
单目标 (RGBA8):     4 bytes/pixel
MRT 2 目标 (RGBA8): 8 bytes/pixel
MRT 4 目标 (RGBA8): 16 bytes/pixel

带宽随目标数量线性增长

建议

  1. 只输出需要的数据:不要创建不使用的附件
  2. 选择合适的格式:不需要 HDR 就用 RGBA8
  3. 考虑打包数据:多个小数据可以打包到一个纹理
  4. 延迟渲染的权衡:场景复杂度 vs 光源数量

小结

概念 说明
MRT 多渲染目标,一次输出多个结果
glDrawBuffers 指定输出目标
layout(location) 着色器中指定输出位置
G-Buffer 延迟渲染的几何缓冲
延迟渲染 分离几何和光照计算

MRT 使用步骤

复制代码
1. 创建 FBO,附加多个颜色纹理
2. 调用 glDrawBuffers 指定输出
3. 着色器使用 layout(location) 声明多个输出
4. 渲染时同时写入所有目标
5. 后续 Pass 使用各个纹理
游客

全部评论 (0)

暂无评论,快来抢沙发吧~