自定义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
相关产品推荐
相关产品推荐

