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倍带宽
建议
- 使用 RGBA16F 而不是 RGBA32F(精度足够,性能更好)
- 只在需要的地方使用 HDR(最终输出仍是 LDR)
- 考虑使用 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 来自:
- 镜头散射:相机镜头内部的光线散射
- 眼睛散射:眼球内部的光线散射
- 大气散射:空气中的微粒散射光线
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 降采样与升采样
为什么需要降采样?
- 减少计算量:在低分辨率上模糊更快
- 增大模糊范围:相同 kernel 在低分辨率上覆盖更大区域
- 多级 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
带宽随目标数量线性增长
建议
- 只输出需要的数据:不要创建不使用的附件
- 选择合适的格式:不需要 HDR 就用 RGBA8
- 考虑打包数据:多个小数据可以打包到一个纹理
- 延迟渲染的权衡:场景复杂度 vs 光源数量
小结
| 概念 | 说明 |
|---|---|
| MRT | 多渲染目标,一次输出多个结果 |
| glDrawBuffers | 指定输出目标 |
| layout(location) | 着色器中指定输出位置 |
| G-Buffer | 延迟渲染的几何缓冲 |
| 延迟渲染 | 分离几何和光照计算 |
MRT 使用步骤
1. 创建 FBO,附加多个颜色纹理
2. 调用 glDrawBuffers 指定输出
3. 着色器使用 layout(location) 声明多个输出
4. 渲染时同时写入所有目标
5. 后续 Pass 使用各个纹理
全部评论 (0)
暂无评论,快来抢沙发吧~