You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义OpenGL引擎PBR效果不及Sketchfab/UE5,求问题排查

PBR实现与Sketchfab/UE5效果差异问题

我按照LearnOpenGL的PBR教程完成了实现,输出效果达标,但和Sketchfab或UE5里的效果相比,真实度差距显著。想确认是我的实现存在问题,还是这些平台有引擎专属的额外处理逻辑。

对比显示:UE版本的金属质感明显更强,我的实现金属表现偏柔,但我已正确传入金属度纹理。此外我的引擎目前不支持自发光材质,但除HUD外其他输出无差异。

使用模型:Battle Damaged Sci-fi Helmet PBR

核心实现代码:

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

vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness)
{
    return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}   

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 k)
{
    float NdotV = max(0.0, dot(N, V));
    float NdotL = max(0.0, dot(N, L));
    return geometrySchlickGGX(NdotV, k) * geometrySchlickGGX(NdotL, k);
}

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

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

    return nom / denom;
}

struct Surface
{
    vec3 fragPos;
    vec3 V;
    vec3 N;
    vec3 H;
    vec3 L;
    vec3 F0;
    vec3 albedo;
    float metallic;
    float roughness;
    
};

vec3 calculateBRDF(Surface s)
{
    // Calculate BRDF
    // Calculate Fresnel Schlick 
    vec3 F = fresnelSchlick(max(0.0, dot(s.H, s.V)), s.F0);

    vec3 ks = F;
    vec3 kd = 1.0 - ks;
    kd *= 1.0 - s.metallic;

    // Calculate NDF
    float NDF = distributionGGX(s.N, s.H, s.roughness);

    // Calculate Schlick-GGX
    float G = geometrySmith(s.N, s.V, s.L, s.roughness);

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

    return (specular + kd * s.albedo / PI);
}

vec3 PointLightRadiance(PointLight pLight, Surface s)
{
    s.L = normalize(pLight.position.rgb - s.fragPos); // from frag pos to light pos
    s.H = normalize(s.V + s.L);

    // Calculate Li
    float distance = length(s.L);
    float attenuation = 1.0 / (distance * distance);
    vec3 radiance = pLight.color.rgb * attenuation;

    // Calculate cosTheta
    float cosTheta = max(0.0, dot(s.N, s.L));

    return calculateBRDF(s) * radiance * cosTheta;
}

vec3 DirLightRadiance(DirLight dLight, Surface s)
{
    s.L = normalize(-dLight.direction.xyz);
    s.H = normalize(s.V + s.L);

    // Calculate Li
    vec3 radiance = dLight.color.rgb;

    // Calculate cosTheta
    float cosTheta = max(0.0, dot(s.N, s.L));

    return calculateBRDF(s) * radiance * cosTheta;
}

float shadowCalculations(vec4 fragPos)
{
    // perform perspective divide
    vec3 projCoords = fragPos.xyz / fragPos.w;
    
    vec2 texelSize = 1.0 / textureSize(gShadowMap, 0);
    
    projCoords = projCoords * 0.5 + 0.5; 
    
    float borderBias =  max(texelSize.x, texelSize.y) * 2;
    
    if(projCoords.x >= 1.0 - borderBias || projCoords.x <= borderBias ||
        projCoords.y >= 1.0 - borderBias || projCoords.y <= borderBias ||
        projCoords.z >= 1.0 - borderBias || projCoords.z <= borderBias)
        return 0.0;
    
    float shadow = 0;
    float bias = 0.005;
    float currentDepth = projCoords.z;
    
    
    for(int x = -1; x <= 1; ++x)
    {
        for(int y = -1; y <= 1; ++y)
        {
            float pcfDepth = texture(gShadowMap, projCoords.xy + vec2(x, y) * texelSize).r;
            shadow += (currentDepth - bias > pcfDepth) ? 1.0 : 0.0;
        }
    }
    
    shadow /= 9.0;
    
    return shadow;
}

void main() 
{ 
    // retrieve data from G-buffer
    vec3 fragPos = texture(gPosition, TexCoords).rgb;
    vec3 normal = texture(gNormal, TexCoords).rgb;
    vec3 albedo = pow(texture(gAlbedo, TexCoords).rgb, vec3(2.2));
    float metallic = texture(gMRA, TexCoords).r;
    float roughness = texture(gMRA, TexCoords).g;
    float ao = texture(gMRA, TexCoords).b;

    vec4 fragPosInLightSpace = lightSpaceMatrix * vec4(fragPos, 1.f);
    float shadow = shadowCalculations(fragPosInLightSpace);

    vec3 F0 = vec3(0.04); // every dieltctric object has F0 = 0.04
    F0 = mix(F0, albedo, metallic);

    // Reflectance equation
    // L0(P, W0) = integral[ BRDF(P, W0, Wi, roughness) * Li(P, Wi) * cosTheta(n, Wi) * dw ]
    // BRDF = [ DFG / (4 * dot(n, w0) * dot(n, wi)) ] + Kd * albedo / PI
    vec3 N = normalize(normal);
    vec3 V = normalize(cameraPos - fragPos);
    vec3 R = reflect(-V, N);
    vec3 L0 = vec3(0.0);

    Surface s;
    s.fragPos = fragPos;
    s.V = V;
    s.N = N;
    s.F0 = F0;
    s.metallic = metallic;
    s.roughness = roughness;
    s.albedo = albedo;

    for(int i = 0; i < pointLightCount; ++i)
    {
        L0 += PointLightRadiance(pointLights[i], s);
    }

    for(int i = 0; i < dirLightCount; ++i)
    {
        L0 += DirLightRadiance(dirLight[i], s) * (1.0 - shadow);
    }

    // generate Kd to accomodate only for diffuse (exclude specular)
    vec3 F = fresnelSchlickRoughness(max(0.0, dot(N, V)), F0, roughness);
    
    vec3 prefilterColor = textureLod(gPrefilterEnvMap, R, roughness * MAX_REFLECTION_LOD).rgb;
    vec2 envBRDF = texture(gBRDFIntegrationLUT, vec2(max(dot(N, V), 0.0), roughness)).rg;
    vec3 specular = prefilterColor * (envBRDF.x * F + envBRDF.y); 

    vec3 ks = F;
    vec3 kd = 1.0 - ks;

    // ambient diffuse irradiance
    vec3 irradiance = texture(gIrradianceMap, N).rgb;
    vec3 diffuse = irradiance * albedo;
    vec3 ambient = (kd * diffuse + specular) * ao * vec3(1.f);

    // combine results
    vec3 color = L0 + ambient;

    // HDR
    color = color / (color + vec3(1.0));

    // Gamma correction
    color = pow(color, vec3(1.0/2.2));

#ifdef CUSTOM_SHADER
    frag(color);
#endif
    
    FragColor = vec4(color, 1.0);
} 

差异原因分析与修正方案

1. 粗糙度的空间转换错误

多数PBR纹理的粗糙度通道存储在sRGB空间,需转换为线性空间才能参与正确的物理计算。你的代码直接读取原始采样值,导致粗糙度被低估,高光扩散过度,金属质感偏柔。

修正:

// 将粗糙度从sRGB转线性空间
float roughness = pow(texture(gMRA, TexCoords).g, 2.2);

也可在纹理采样时开启sRGB采样模式(需匹配引擎纹理设置)。

2. GGX分布函数的α参数错误

标准PBR中,GGX分布函数的α参数应为roughness²(粗糙度是感知线性,α是物理线性)。你的代码直接传入原始粗糙度,导致低粗糙度表面的高光不够锐利。

修正:
在calculateBRDF函数中修改NDF计算:

// 计算物理线性的α
float a = s.roughness * s.roughness;
float NDF = distributionGGX(s.N, s.H, a);

3. 几何函数的k值传递错误

你的geometrySmith函数需要传入预计算的k值,而非原始粗糙度。当前代码直接传递粗糙度,导致几何遮蔽/阴影计算错误,高光强度被削弱。

修正:
在calculateBRDF函数中修改几何计算:

// 计算直接光照下的k值
float r = s.roughness + 1.0;
float k = (r*r) / 8.0;
float G = geometrySmith(s.N, s.V, s.L, k);

注:IBL光照下的k值为roughness² / 2.0,但你的环境光计算通过LUT处理,无需修改。

4. 光照与色调映射的差异

  • 光照单位:UE5/Sketchfab使用物理正确的光照强度(如cd/m²),若你的光照颜色值未做物理缩放,会导致整体亮度和高光表现偏差。
  • 色调映射:你的实现使用Reinhard色调映射,而UE使用ACES,后者能更好保留高光细节,提升金属质感的对比度。可尝试替换为ACES近似实现:
// ACES近似色调映射
vec3 aces(vec3 x) {
    const float a = 2.51;
    const float b = 0.03;
    const float c = 2.43;
    const float d = 0.59;
    const float e = 0.14;
    return clamp((x*(a*x + b))/(x*(c*x + d) + e), 0.0, 1.0);
}

// 替换原Reinhard代码
vec3 color = aces(L0 + ambient);

5. IBL细节优化

  • 预过滤环境图精度:检查MAX_REFLECTION_LOD是否足够大(建议设为10~16),确保高粗糙度表面能采样到足够模糊的环境反射。
  • F0微调:部分金属材质的F0可轻微调整(如增加蓝色通道),但属于细节优化,非核心问题。

内容的提问来源于stack exchange,提问作者stav12212

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 19:30:54