物理基渲染(Physically Based Rendering, PBR)是一种基于物理原理的渲染方法,旨在实现更真实的光照和材质效果。PBR渲染流程通常包括以下几个主要步骤:

1. 材质定义

PBR材质通常由一组参数定义,这些参数描述了材质的物理属性。常见的PBR材质参数包括:

  • Albedo(反照率):材质的基本颜色,不包含任何光照信息。
  • Normal Map(法线贴图):用于细化表面细节,改变表面的法线方向。
  • Metallic(金属度):描述材质是否为金属。金属材质的值为1,非金属材质的值为0。
  • Roughness(粗糙度):描述材质表面的光滑程度。值越高,表面越粗糙,反射越模糊。
  • Ambient Occlusion(环境光遮蔽):描述表面在环境光照下的遮蔽程度。

2. 几何处理

在渲染开始之前,需要对场景中的几何体进行处理。这包括顶点变换、法线计算等。通常在顶点着色器中完成这些操作。

#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec2 aTexCoords;

out vec3 FragPos;
out vec3 Normal;
out vec2 TexCoords;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    FragPos = vec3(model * vec4(aPos, 1.0));
    Normal = mat3(transpose(inverse(model))) * aNormal;
    TexCoords = aTexCoords;
    
    gl_Position = projection * view * vec4(FragPos, 1.0);
}

3. 光照计算

光照计算是PBR渲染的核心部分。PBR通常使用基于物理的光照模型,如Cook-Torrance模型。这个模型包括以下几个部分:

  • Fresnel效应:描述光在不同角度下的反射率变化。
  • 法线分布函数(NDF):描述微表面法线的分布。
  • 几何遮蔽函数(Geometry Function):描述光线在微表面之间的遮蔽和阴影。
#version 330 core
out vec4 FragColor;

in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoords;

uniform vec3 camPos;
uniform sampler2D albedoMap;
uniform sampler2D normalMap;
uniform sampler2D metallicMap;
uniform sampler2D roughnessMap;
uniform sampler2D aoMap;

struct Light {
    vec3 position;
    vec3 color;
};

uniform Light lights[4];
uniform int lightCount;

const float PI = 3.14159265359;

vec3 getNormalFromMap()
{
    vec3 tangentNormal = texture(normalMap, TexCoords).xyz * 2.0 - 1.0;

    vec3 Q1  = dFdx(FragPos);
    vec3 Q2  = dFdy(FragPos);
    vec2 st1 = dFdx(TexCoords);
    vec2 st2 = dFdy(TexCoords);

    vec3 N   = normalize(Normal);
    vec3 T  = normalize(Q1 * st2.t - Q2 * st1.t);
    vec3 B  = -normalize(cross(N, T));
    mat3 TBN = mat3(T, B, N);

    return normalize(TBN * tangentNormal);
}

float DistributionGGX(vec3 N, vec3 H, float roughness)
{
    float a = roughness * roughness;
    float a2 = a * a;
    float NdotH = max(dot(N, H), 0.0);
    float NdotH2 = NdotH * NdotH;

    float num = a2;
    float denom = (NdotH2 * (a2 - 1.0) + 1.0);
    denom = PI * denom * denom;

    return num / denom;
}

float GeometrySchlickGGX(float NdotV, float roughness)
{
    float r = (roughness + 1.0);
    float k = (r * r) / 8.0;

    float num = NdotV;
    float denom = NdotV * (1.0 - k) + k;

    return num / denom;
}

float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness)
{
    float NdotV = max(dot(N, V), 0.0);
    float NdotL = max(dot(N, L), 0.0);
    float ggx2 = GeometrySchlickGGX(NdotV, roughness);
    float ggx1 = GeometrySchlickGGX(NdotL, roughness);

    return ggx1 * ggx2;
}

vec3 fresnelSchlick(float cosTheta, vec3 F0)
{
    return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}

void main()
{
    vec3 albedo = pow(texture(albedoMap, TexCoords).rgb, vec3(2.2));
    float metallic = texture(metallicMap, TexCoords).r;
    float roughness = texture(roughnessMap, TexCoords).r;
    float ao = texture(aoMap, TexCoords).r;

    vec3 N = getNormalFromMap();
    vec3 V = normalize(camPos - FragPos);
    vec3 F0 = vec3(0.04);
    F0 = mix(F0, albedo, metallic);

    vec3 Lo = vec3(0.0);
    for(int i = 0; i < lightCount; ++i)
    {
        vec3 L = normalize(lights[i].position - FragPos);
        vec3 H = normalize(V + L);
        float distance = length(lights[i].position - FragPos);
        float attenuation = 1.0 / (distance * distance);
        vec3 radiance = lights[i].color * attenuation;

        float NDF = DistributionGGX(N, H, roughness);
        float G = GeometrySmith(N, V, L, roughness);
        vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0);

        vec3 numerator = NDF * G * F;
        float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0);
        vec3 specular = numerator / max(denominator, 0.001);

        vec3 kS = F;
        vec3 kD = vec3(1.0) - kS;
        kD *= 1.0 - metallic;

        float NdotL = max(dot(N, L), 0.0);
        Lo += (kD * albedo / PI + specular) * radiance * NdotL;
    }

    vec3 ambient = vec3(0.03) * albedo * ao;
    vec3 color = ambient + Lo;

    color = color / (color + vec3(1.0));
    color = pow(color, vec3(1.0/2.2));

    FragColor = vec4(color, 1.0);
}

4. 环境光照

环境光照是PBR渲染中不可或缺的一部分。通常使用环境贴图(如HDR贴图)来模拟环境光照。环境光照可以分为两部分:

  • 环境反射:使用预过滤的环境贴图来模拟镜面反射。
  • 环境漫反射:使用辐照度贴图来模拟漫反射。

5. 后处理

在完成光照计算后,通常会进行一些后处理操作,如色调映射、伽马校正等,以提高最终图像的质量。

结语

PBR渲染流程通过物理模型和材质参数的结合,实现了更为真实的光照和材质效果。通过合理的几何处理、光照计算和环境光照模拟,可以在实时渲染中实现高质量的视觉效果。希望这些内容能够帮助你理解和实现PBR渲染。

12. 实践中的PBR实现

在实际项目中,PBR的实现不仅仅是理论和代码的结合,还需要考虑到艺术设计、资源管理和性能优化等多个方面。以下是一些实践中的建议和技巧:

12.1 资源管理
  • 纹理压缩:使用纹理压缩技术(如BCn、ASTC)来减少显存占用和带宽消耗。
  • Mipmap:为纹理生成mipmap,以提高渲染效率和减少纹理失真。
  • 材质库:建立一个标准化的材质库,方便艺术家和开发者复用和管理材质。
12.2 艺术设计
  • 一致性:确保所有材质的参数范围和单位一致,以避免不一致的视觉效果。
  • 参考资料:使用真实世界的材质和光照作为参考,确保PBR材质的真实性。
  • 调试工具:开发调试工具,帮助艺术家和开发者实时查看和调整材质参数。
12.3 性能优化
  • 动态分辨率:根据场景复杂度和帧率动态调整渲染分辨率。
  • 延迟渲染:使用延迟渲染技术,将光照计算推迟到后期处理阶段,以减少前期计算量。
  • 多线程:利用多线程技术,将几何处理、光照计算和后处理分配到不同的线程中,提高渲染效率。

13. 高级PBR技术

13.1 次表面散射(Subsurface Scattering, SSS)

次表面散射是模拟光在半透明材质(如皮肤、蜡)内部散射的效果。常见的实现方法包括基于屏幕空间的次表面散射(SSSS)和基于体积的次表面散射。

vec3 subsurfaceScattering(vec3 color, vec3 lightDir, vec3 viewDir, vec3 normal)
{
    // 简单的SSS实现
    float scatter = max(dot(lightDir, normal), 0.0);
    vec3 sssColor = color * scatter;
    return sssColor;
}
13.2 体积光照(Volumetric Lighting)

体积光照用于模拟光在空气、雾等介质中的散射和吸收。常见的实现方法包括体积光照贴图和基于屏幕空间的体积光照。

vec3 volumetricLighting(vec3 color, vec3 lightDir, vec3 viewDir, vec3 normal)
{
    // 简单的体积光照实现
    float scatter = exp(-length(lightDir) * 0.1);
    vec3 volumeColor = color * scatter;
    return volumeColor;
}
13.3 反射和折射

反射和折射是模拟光在不同介质界面上的反射和折射效果。常见的实现方法包括环境贴图和屏幕空间反射(SSR)。

vec3 reflection(vec3 color, vec3 normal, vec3 viewDir)
{
    vec3 reflectDir = reflect(-viewDir, normal);
    vec3 reflectColor = texture(environmentMap, reflectDir).rgb;
    return reflectColor;
}

vec3 refraction(vec3 color, vec3 normal, vec3 viewDir, float eta)
{
    vec3 refractDir = refract(-viewDir, normal, eta);
    vec3 refractColor = texture(environmentMap, refractDir).rgb;
    return refractColor;
}

14. 实例:实现一个简单的PBR渲染器

以下是一个简单的PBR渲染器的完整实现,包括顶点着色器和片段着色器。

顶点着色器
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec2 aTexCoords;

out vec3 FragPos;
out vec3 Normal;
out vec2 TexCoords;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    FragPos = vec3(model * vec4(aPos, 1.0));
    Normal = mat3(transpose(inverse(model))) * aNormal;
    TexCoords = aTexCoords;
    
    gl_Position = projection * view * vec4(FragPos, 1.0);
}
片段着色器
#version 330 core
out vec4 FragColor;

in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoords;

uniform vec3 camPos;
uniform sampler2D albedoMap;
uniform sampler2D normalMap;
uniform sampler2D metallicMap;
uniform sampler2D roughnessMap;
uniform sampler2D aoMap;

struct Light {
    vec3 position;
    vec3 color;
};

uniform Light lights[4];
uniform int lightCount;

const float PI = 3.14159265359;

vec3 getNormalFromMap()
{
    vec3 tangentNormal = texture(normalMap, TexCoords).xyz * 2.0 - 1.0;

    vec3 Q1  = dFdx(FragPos);
    vec3 Q2  = dFdy(FragPos);
    vec2 st1 = dFdx(TexCoords);
    vec2 st2 = dFdy(TexCoords);

    vec3 N   = normalize(Normal);
    vec3 T  = normalize(Q1 * st2.t - Q2 * st1.t);
    vec3 B  = -normalize(cross(N, T));
    mat3 TBN = mat3(T, B, N);

    return normalize(TBN * tangentNormal);
}

float DistributionGGX(vec3 N, vec3 H, float roughness)
{
    float a = roughness * roughness;
    float a2 = a * a;
    float NdotH = max(dot(N, H), 0.0);
    float NdotH2 = NdotH * NdotH;

    float num = a2;
    float denom = (NdotH2 * (a2 - 1.0) + 1.0);
    denom = PI * denom * denom;

    return num / denom;
}

float GeometrySchlickGGX(float NdotV, float roughness)
{
    float r = (roughness + 1.0);
    float k = (r * r) / 8.0;

    float num = NdotV;
    float denom = NdotV * (1.0 - k) + k;

    return num / denom;
}

float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness)
{
    float NdotV = max(dot(N, V), 0.0);
    float NdotL = max(dot(N, L), 0.0);
    float ggx2 = GeometrySchlickGGX(NdotV, roughness);
    float ggx1 = GeometrySchlickGGX(NdotL, roughness);

    return ggx1 * ggx2;
}

vec3 fresnelSchlick(float cosTheta, vec3 F0)
{
    return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}

void main()
{
    vec3 albedo = pow(texture(albedoMap, TexCoords).rgb, vec3(2.2));
    float metallic = texture(metallicMap, TexCoords).r;
    float roughness = texture(roughnessMap, TexCoords).r;
    float ao = texture(aoMap, TexCoords).r;

    vec3 N = getNormalFromMap();
    vec3 V = normalize(camPos - FragPos);
    vec3 F0 = vec3(0.04);
    F0 = mix(F0, albedo, metallic);

    vec3 Lo = vec3(0.0);
    for(int i = 0; i < lightCount; ++i)
    {
        vec3 L = normalize(lights[i].position - FragPos);
        vec3 H = normalize(V + L);
        float distance = length(lights[i].position - FragPos);
        float attenuation = 1.0 / (distance * distance);
        vec3 radiance = lights[i].color * attenuation;

        float NDF = DistributionGGX(N, H, roughness);
        float G = GeometrySmith(N, V, L, roughness);
        vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0);

        vec3 numerator = NDF * G * F;
        float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0);
        vec3 specular = numerator / max(denominator, 0.001);

        vec3 kS = F;
        vec3 kD = vec3(1.0) - kS;
        kD *= 1.0 - metallic;

        float NdotL = max(dot(N, L), 0.0);
        Lo += (kD * albedo / PI + specular) * radiance * NdotL;
    }

    vec3 ambient = vec3(0.03) * albedo * ao;
    vec3 color = ambient + Lo;

    color = color / (color + vec3(1.0));
    color = pow(color, vec3(1.0/2.2));

    FragColor = vec4(color, 1.0);
}

15. 结语

PBR渲染是一种强大且灵活的渲染技术,能够实现高度真实的光照和材质效果。通过理解PBR的基本原理和实现细节,并结合实际项目中的优化和艺术设计,你可以创建出令人惊叹的视觉效果。希望这些信息对你有所帮助,祝你在PBR渲染的学习和实践中取得成功!

物理基础渲染(PBR)逻辑框架

物理基础渲染(PBR,Physically Based Rendering)是一种基于物理原理的渲染方法,旨在模拟光与物体表面交互的真实效果。PBR的核心思想是通过物理模型来描述光的反射、折射和散射,从而实现更加逼真的渲染效果。以下是PBR渲染的逻辑框架。

1. 材质模型

PBR使用基于物理的材质模型来描述物体表面的光学特性。常见的PBR材质模型包括:

  • 漫反射(Diffuse):描述光在表面上的散射。
  • 镜面反射(Specular):描述光在表面上的反射。
  • 金属度(Metalness):描述表面是金属还是非金属。
  • 粗糙度(Roughness):描述表面的光滑程度。
  • 环境光遮蔽(Ambient Occlusion, AO):描述表面在环境光下的遮蔽程度。
2. 光照模型

PBR使用基于物理的光照模型来计算光与物体表面的交互。常见的光照模型包括:

  • 朗伯反射(Lambertian Reflection):用于计算漫反射光。
  • 库克-托伦斯反射(Cook-Torrance Reflection):用于计算镜面反射光。
3. BRDF(双向反射分布函数)

BRDF(Bidirectional Reflectance Distribution Function)是PBR的核心,用于描述光在表面上的反射特性。常见的BRDF模型包括:

  • 朗伯BRDF(Lambertian BRDF):用于漫反射。
  • GGX BRDF:用于镜面反射,具有更好的物理真实性。
4. 环境光照

PBR通常使用环境光照(IBL,Image-Based Lighting)来模拟复杂的光照环境。环境光照包括:

  • 环境漫反射(Diffuse IBL):使用环境贴图计算漫反射光。
  • 环境镜面反射(Specular IBL):使用预过滤环境贴图计算镜面反射光。
5. 渲染流程

PBR渲染的基本流程如下:

  1. 材质参数准备:从材质贴图中获取漫反射颜色、金属度、粗糙度、环境光遮蔽等参数。
  2. 光照计算:使用光照模型计算直接光照和间接光照。
  3. BRDF计算:使用BRDF模型计算光的反射。
  4. 环境光照计算:使用环境贴图计算环境光照。
  5. 合成最终颜色:将直接光照、间接光照和环境光照合成,得到最终的像素颜色。
6. 代码示例

以下是一个简单的PBR渲染代码示例,使用GLSL编写的片段着色器:

#version 330 core
out vec4 FragColor;

in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoords;

uniform vec3 camPos;
uniform sampler2D albedoMap;
uniform sampler2D normalMap;
uniform sampler2D metallicMap;
uniform sampler2D roughnessMap;
uniform sampler2D aoMap;
uniform samplerCube irradianceMap;
uniform samplerCube prefilterMap;
uniform sampler2D brdfLUT;

struct Light {
    vec3 position;
    vec3 color;
};

uniform Light lights[4];
uniform int lightCount;

const float PI = 3.14159265359;

vec3 getNormalFromMap()
{
    vec3 tangentNormal = texture(normalMap, TexCoords).xyz * 2.0 - 1.0;

    vec3 Q1  = dFdx(FragPos);
    vec3 Q2  = dFdy(FragPos);
    vec2 st1 = dFdx(TexCoords);
    vec2 st2 = dFdy(TexCoords);

    vec3 N   = normalize(Normal);
    vec3 T  = normalize(Q1 * st2.t - Q2 * st1.t);
    vec3 B  = -normalize(cross(N, T));
    mat3 TBN = mat3(T, B, N);

    return normalize(TBN * tangentNormal);
}

float DistributionGGX(vec3 N, vec3 H, float roughness)
{
    float a = roughness*roughness;
    float a2 = a*a;
    float NdotH = max(dot(N, H), 0.0);
    float NdotH2 = NdotH*NdotH;

    float num = a2;
    float denom = (NdotH2 * (a2 - 1.0) + 1.0);
    denom = PI * denom * denom;

    return num / denom;
}

float GeometrySchlickGGX(float NdotV, float roughness)
{
    float r = (roughness + 1.0);
    float k = (r*r) / 8.0;

    float num = NdotV;
    float denom = NdotV * (1.0 - k) + k;

    return num / denom;
}

float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness)
{
    float NdotV = max(dot(N, V), 0.0);
    float NdotL = max(dot(N, L), 0.0);
    float ggx2 = GeometrySchlickGGX(NdotV, roughness);
    float ggx1 = GeometrySchlickGGX(NdotL, roughness);

    return ggx1 * ggx2;
}

vec3 fresnelSchlick(float cosTheta, vec3 F0)
{
    return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}

void main()
{
    vec3 albedo = pow(texture(albedoMap, TexCoords).rgb, vec3(2.2));
    float metallic = texture(metallicMap, TexCoords).r;
    float roughness = texture(roughnessMap, TexCoords).r;
    float ao = texture(aoMap, TexCoords).r;

    vec3 N = getNormalFromMap();
    vec3 V = normalize(camPos - FragPos);
    vec3 R = reflect(-V, N);

    vec3 F0 = vec3(0.04);
    F0 = mix(F0, albedo, metallic);

    vec3 Lo = vec3(0.0);
    for(int i = 0; i < lightCount; ++i)
    {
        vec3 L = normalize(lights[i].position - FragPos);
        vec3 H = normalize(V + L);
        float distance = length(lights[i].position - FragPos);
        float attenuation = 1.0 / (distance * distance);
        vec3 radiance = lights[i].color * attenuation;

        float NDF = DistributionGGX(N, H, roughness);
        float G = GeometrySmith(N, V, L, roughness);
        vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0);

        vec3 nominator = NDF * G * F;
        float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0);
        vec3 specular = nominator / max(denominator, 0.001);

        vec3 kS = F;
        vec3 kD = vec3(1.0) - kS;
        kD *= 1.0 - metallic;

        float NdotL = max(dot(N, L), 0.0);
        Lo += (kD * albedo / PI + specular) * radiance * NdotL;
    }

    vec3 ambient = vec3(0.03) * albedo * ao;
    vec3 color = ambient + Lo;

    color = color / (color + vec3(1.0));
    color = pow(color, vec3(1.0/2.2));

    FragColor = vec4(color, 1.0);
}

结语

PBR渲染通过基于物理的材质模型和光照模型,实现了更加逼真的渲染效果。本文介绍了PBR渲染的逻辑框架,包括材质模型、光照模型、BRDF、环境光照和渲染流程,并提供了一个简单的代码示例。希望通过本文的介绍,读者能够对PBR渲染有更深入的理解,并在实际开发中应用这些知识,创建出高效、逼真的渲染效果。

进一步深入PBR渲染

在前面的介绍中,我们已经了解了PBR渲染的基本逻辑框架和一个简单的代码示例。接下来,我们将进一步深入探讨PBR渲染中的一些高级概念和技术。

1. 预计算环境贴图

为了提高渲染效率,PBR通常会预计算环境贴图。这包括:

  • 辐照度贴图(Irradiance Map):用于计算环境漫反射光。
  • 预过滤环境贴图(Prefiltered Environment Map):用于计算环境镜面反射光。
  • BRDF查找表(BRDF LUT):用于优化BRDF计算。

这些预计算的贴图可以显著减少实时渲染时的计算量,从而提高渲染性能。

2. 重要性采样

在PBR渲染中,重要性采样(Importance Sampling)是一种优化技术,用于减少噪声并提高渲染质量。重要性采样通过优先采样对最终结果影响较大的方向,从而减少不必要的计算。

3. 多重散射

多重散射(Multiple Scattering)是指光在物体表面多次反射和散射的现象。在PBR渲染中,多重散射可以通过以下方法来模拟:

  • 双向散射分布函数(BSDF):扩展BRDF以考虑多重散射。
  • 次表面散射(Subsurface Scattering, SSS):用于模拟光在半透明材料(如皮肤、蜡)中的散射。
4. 微表面模型

微表面模型(Microfacet Model)是PBR渲染中的一个重要概念,用于描述表面微小不平整的反射特性。常见的微表面分布函数包括:

  • GGX分布:具有更好的物理真实性,适用于大多数材质。
  • 贝克曼分布(Beckmann Distribution):适用于光滑表面。
5. 线性空间渲染

PBR渲染通常在线性空间中进行计算,以确保光照和颜色的计算更加准确。线性空间渲染包括:

  • 伽马校正(Gamma Correction):将颜色从伽马空间转换到线性空间进行计算,最后再转换回伽马空间显示。
  • HDR渲染(High Dynamic Range Rendering):使用高动态范围的颜色表示,以捕捉更多的光照细节。
6. 实时PBR渲染优化

为了在实时应用中实现高效的PBR渲染,可以采用以下优化技术:

  • 延迟渲染(Deferred Rendering):将几何信息和材质信息存储在G缓冲区中,延迟光照计算。
  • 级联阴影贴图(Cascaded Shadow Maps, CSM):用于高效地渲染大范围场景中的阴影。
  • 屏幕空间反射(Screen Space Reflections, SSR):用于模拟镜面反射,减少对环境贴图的依赖。

代码示例:高级PBR渲染

以下是一个包含预计算环境贴图和重要性采样的高级PBR渲染代码示例:

#version 330 core
out vec4 FragColor;

in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoords;

uniform vec3 camPos;
uniform sampler2D albedoMap;
uniform sampler2D normalMap;
uniform sampler2D metallicMap;
uniform sampler2D roughnessMap;
uniform sampler2D aoMap;
uniform samplerCube irradianceMap;
uniform samplerCube prefilterMap;
uniform sampler2D brdfLUT;

struct Light {
    vec3 position;
    vec3 color;
};

uniform Light lights[4];
uniform int lightCount;

const float PI = 3.14159265359;

vec3 getNormalFromMap()
{
    vec3 tangentNormal = texture(normalMap, TexCoords).xyz * 2.0 - 1.0;

    vec3 Q1  = dFdx(FragPos);
    vec3 Q2  = dFdy(FragPos);
    vec2 st1 = dFdx(TexCoords);
    vec2 st2 = dFdy(TexCoords);

    vec3 N   = normalize(Normal);
    vec3 T  = normalize(Q1 * st2.t - Q2 * st1.t);
    vec3 B  = -normalize(cross(N, T));
    mat3 TBN = mat3(T, B, N);

    return normalize(TBN * tangentNormal);
}

float DistributionGGX(vec3 N, vec3 H, float roughness)
{
    float a = roughness*roughness;
    float a2 = a*a;
    float NdotH = max(dot(N, H), 0.0);
    float NdotH2 = NdotH*NdotH;

    float num = a2;
    float denom = (NdotH2 * (a2 - 1.0) + 1.0);
    denom = PI * denom * denom;

    return num / denom;
}

float GeometrySchlickGGX(float NdotV, float roughness)
{
    float r = (roughness + 1.0);
    float k = (r*r) / 8.0;

    float num = NdotV;
    float denom = NdotV * (1.0 - k) + k;

    return num / denom;
}

float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness)
{
    float NdotV = max(dot(N, V), 0.0);
    float NdotL = max(dot(N, L), 0.0);
    float ggx2 = GeometrySchlickGGX(NdotV, roughness);
    float ggx1 = GeometrySchlickGGX(NdotL, roughness);

    return ggx1 * ggx2;
}

vec3 fresnelSchlick(float cosTheta, vec3 F0)
{
    return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}

void main()
{
    vec3 albedo = pow(texture(albedoMap, TexCoords).rgb, vec3(2.2));
    float metallic = texture(metallicMap, TexCoords).r;
    float roughness = texture(roughnessMap, TexCoords).r;
    float ao = texture(aoMap, TexCoords).r;

    vec3 N = getNormalFromMap();
    vec3 V = normalize(camPos - FragPos);
    vec3 R = reflect(-V, N);

    vec3 F0 = vec3(0.04);
    F0 = mix(F0, albedo, metallic);

    vec3 Lo = vec3(0.0);
    for(int i = 0; i < lightCount; ++i)
    {
        vec3 L = normalize(lights[i].position - FragPos);
        vec3 H = normalize(V + L);
        float distance = length(lights[i].position - FragPos);
        float attenuation = 1.0 / (distance * distance);
        vec3 radiance = lights[i].color * attenuation;

        float NDF = DistributionGGX(N, H, roughness);
        float G = GeometrySmith(N, V, L, roughness);
        vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0);

        vec3 nominator = NDF * G * F;
        float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0);
        vec3 specular = nominator / max(denominator, 0.001);

        vec3 kS = F;
        vec3 kD = vec3(1.0) - kS;
        kD *= 1.0 - metallic;

        float NdotL = max(dot(N, L), 0.0);
        Lo += (kD * albedo / PI + specular) * radiance * NdotL;
    }

    vec3 F = fresnelSchlick(max(dot(N, V), 0.0), F0);
    vec3 kS = F;
    vec3 kD = 1.0 - kS;
    kD *= 1.0 - metallic;

    vec3 irradiance = texture(irradianceMap, N).rgb;
    vec3 diffuse = irradiance * albedo;

    const float MAX_REFLECTION_LOD = 4.0;
    vec3 prefilteredColor = textureLod(prefilterMap, R, roughness * MAX_REFLECTION_LOD).rgb;
    vec2 brdf = texture(brdfLUT, vec2(max(dot(N, V), 0.0), roughness)).rg;
    vec3 specular = prefilteredColor * (F * brdf.x + brdf.y);

    vec3 ambient = (kD * diffuse + specular) * ao;

    vec3 color = ambient + Lo;

    color = color / (color + vec3(1.0));
    color = pow(color, vec3(1.0/2.2));

    FragColor = vec4(color, 1.0);
}

结语

通过进一步深入探讨PBR渲染中的高级概念和技术,我们可以实现更加逼真和高效的渲染效果。本文介绍了预计算环境贴图、重要性采样、多重散射、微表面模型、线性空间渲染和实时PBR渲染优化等内容,并提供了一个高级PBR渲染的代码示例。希望这些内容能够帮助读者在实际开发中应用PBR渲染技术,创建出更加逼真的视觉效果。

进一步优化和扩展PBR渲染

在前面的讨论中,我们已经涵盖了PBR渲染的许多重要方面。接下来,我们将探讨一些进一步优化和扩展PBR渲染的方法,以实现更高的性能和更逼真的效果。

1. 屏幕空间环境光遮蔽(SSAO)

屏幕空间环境光遮蔽(Screen Space Ambient Occlusion, SSAO)是一种用于模拟环境光遮蔽效果的技术。SSAO通过在屏幕空间中计算遮蔽因子,来增强场景的深度感和细节。

#version 330 core
out float FragColor;

in vec2 TexCoords;

uniform sampler2D gPosition;
uniform sampler2D gNormal;
uniform sampler2D texNoise;

uniform vec3 samples[64];

uniform mat4 projection;
uniform vec2 noiseScale;

const float radius = 0.5;
const float bias = 0.025;

void main()
{
    vec3 fragPos = texture(gPosition, TexCoords).xyz;
    vec3 normal = normalize(texture(gNormal, TexCoords).rgb);
    vec3 randomVec = normalize(texture(texNoise, TexCoords * noiseScale).xyz);

    vec3 tangent = normalize(randomVec - normal * dot(randomVec, normal));
    vec3 bitangent = cross(normal, tangent);
    mat3 TBN = mat3(tangent, bitangent, normal);

    float occlusion = 0.0;
    for(int i = 0; i < 64; ++i)
    {
        vec3 samplePos = TBN * samples[i];
        samplePos = fragPos + samplePos * radius;

        vec4 offset = vec4(samplePos, 1.0);
        offset = projection * offset;
        offset.xyz /= offset.w;
        offset.xyz = offset.xyz * 0.5 + 0.5;

        float sampleDepth = texture(gPosition, offset.xy).z;
        float rangeCheck = smoothstep(0.0, 1.0, radius / abs(fragPos.z - sampleDepth));
        occlusion += (sampleDepth >= samplePos.z + bias ? 1.0 : 0.0) * rangeCheck;
    }
    occlusion = 1.0 - (occlusion / 64.0);

    FragColor = occlusion;
}
2. 屏幕空间反射(SSR)

屏幕空间反射(Screen Space Reflections, SSR)是一种用于模拟镜面反射的技术。SSR通过在屏幕空间中追踪反射光线,来实现实时反射效果。

#version 330 core
out vec4 FragColor;

in vec2 TexCoords;

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

uniform mat4 view;
uniform mat4 projection;
uniform vec3 camPos;

const int MAX_STEPS = 50;
const float STEP_SIZE = 0.1;
const float MAX_DISTANCE = 100.0;

vec3 getViewRay(vec2 uv)
{
    vec4 clipPos = vec4(uv * 2.0 - 1.0, 1.0, 1.0);
    vec4 viewPos = inverse(projection) * clipPos;
    viewPos /= viewPos.w;
    return normalize(viewPos.xyz);
}

void main()
{
    vec3 fragPos = texture(gPosition, TexCoords).xyz;
    vec3 normal = normalize(texture(gNormal, TexCoords).rgb);
    vec3 albedo = texture(gAlbedoSpec, TexCoords).rgb;
    float specular = texture(gAlbedoSpec, TexCoords).a;

    vec3 viewDir = normalize(camPos - fragPos);
    vec3 reflectDir = reflect(viewDir, normal);

    vec3 rayOrigin = fragPos;
    vec3 rayDir = reflectDir;

    float currentDepth = texture(gDepth, TexCoords).r;
    float rayLength = 0.0;
    vec3 hitColor = vec3(0.0);

    for(int i = 0; i < MAX_STEPS; ++i)
    {
        rayLength += STEP_SIZE;
        vec3 rayPos = rayOrigin + rayDir * rayLength;

        vec4 clipPos = projection * view * vec4(rayPos, 1.0);
        clipPos /= clipPos.w;
        vec2 uv = clipPos.xy * 0.5 + 0.5;

        if(uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0)
            break;

        float sampleDepth = texture(gDepth, uv).r;
        if(sampleDepth < rayPos.z)
        {
            hitColor = texture(gAlbedoSpec, uv).rgb;
            break;
        }

        if(rayLength > MAX_DISTANCE)
            break;
    }

    vec3 color = mix(albedo, hitColor, specular);
    FragColor = vec4(color, 1.0);
}
3. 次表面散射(SSS)

次表面散射(Subsurface Scattering, SSS)用于模拟光在半透明材料(如皮肤、蜡)中的散射。SSS可以通过多种方法实现,包括屏幕空间技术和基于体积的技术。

#version 330 core
out vec4 FragColor;

in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoords;

uniform vec3 camPos;
uniform sampler2D albedoMap;
uniform sampler2D normalMap;
uniform sampler2D sssMap;

struct Light {
    vec3 position;
    vec3 color;
};

uniform Light lights[4];
uniform int lightCount;

const float PI = 3.14159265359;

vec3 getNormalFromMap()
{
    vec3 tangentNormal = texture(normalMap, TexCoords).xyz * 2.0 - 1.0;

    vec3 Q1  = dFdx(FragPos);
    vec3 Q2  = dFdy(FragPos);
    vec2 st1 = dFdx(TexCoords);
    vec2 st2 = dFdy(TexCoords);

    vec3 N   = normalize(Normal);
    vec3 T  = normalize(Q1 * st2.t - Q2 * st1.t);
    vec3 B  = -normalize(cross(N, T));
    mat3 TBN = mat3(T, B, N);

    return normalize(TBN * tangentNormal);
}

float fresnelSchlick(float cosTheta, vec3 F0)
{
    return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}

void main()
{
    vec3 albedo = pow(texture(albedoMap, TexCoords).rgb, vec3(2.2));
    vec3 sssColor = texture(sssMap, TexCoords).rgb;

    vec3 N = getNormalFromMap();
    vec3 V = normalize(camPos - FragPos);

    vec3 Lo = vec3(0.0);
    for(int i = 0; i < lightCount; ++i)
    {
        vec3 L = normalize(lights[i].position - FragPos);
        vec3 H = normalize(V + L);
        float distance = length(lights[i].position - FragPos);
        float attenuation = 1.0 / (distance * distance);
        vec3 radiance = lights[i].color * attenuation;

        float NdotL = max(dot(N, L), 0.0);
        vec3 diffuse = albedo / PI;

        vec3 F0 = vec3(0.04);
        vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0);
        vec3 kS = F;
        vec3 kD = 1.0 - kS;

        vec3 sss = sssColor * NdotL;

        Lo += (kD * diffuse + sss) * radiance * NdotL;
    }

    vec3 color = Lo;
    color = color / (color + vec3(1.0));
    color = pow(color, vec3(1.0/2.2));

    FragColor = vec4(color, 1.0);
}

结语

通过引入屏幕空间环境光遮蔽(SSAO)、屏幕空间反射(SSR)和次表面散射(SSS)等技术,我们可以进一步优化和扩展PBR渲染,实现更高的性能和更逼真的效果。这些技术在现代游戏和实时渲染中得到了广泛应用,能够显著提升视觉质量。希望这些内容能够帮助读者在实际开发中应用这些技术,创建出更加逼真的视觉效果。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐