PBR技術簡介(四):直接光照的程式碼實現
阿新 • • 發佈:2020-08-16
之前介紹了有關PBR技術的一些理論知識,今天來講一下利用程式碼如何實現相應的光照演算法。
我們提到,我們最終要求解的其實就是這麼一個積分:
積分中kd的部分代表光照所產生的漫反射,ks的部分代表光照所產生的高光反射。如果充分考慮間接光照的效果(也就是從光源發射出光線後,不斷碰撞反射,最終進入人眼),那這個積分事實上是極難求解的,但是我們可以先暫時不考慮間接光照,只考慮直接光照的部分。那麼只需要在shader中,將所有的光源累加到該積分裡就可以了,這個相對來說還是比較好做的。
我們首先把Cook-Torrance BRDF相關的程式碼實現一下(用HLSL實現),基本上就是對著公式寫:
// 法向分佈函式 N float DistributionGGX(float3 N, float3 H, float roughness) { float pi = 3.14159265; 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; } //幾何函式G float GeometrySmith(float3 N, float3 V, float3 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; } //菲涅爾公式F float3 fresnelSchlick(float cosTheta, float3 F0) { return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); }
然後具體的光照計算程式碼如下(以點光源為例,方向光原理):
void ComputePointLight(Material mat, PointLight light, float3 pos, float3 N, float3 V, float3 F0, out float3 lo) { float3 L = light.Position - pos; float distance = length(L); // Range test. if( distance > light.Range ) return; // Normalize the light vector. L /= distance; float3 H = normalize(V + L); float attenuation = 1.0 / (distance * distance); float3 radiance = light.Diffuse.rgb * attenuation; // cook-torrance brdf float NDF = DistributionGGX(N, H, mat.roughness); float G = GeometrySmith(N, V, L, mat.roughness); float3 F = fresnelSchlick(max(dot(H, V), 0.0), F0); float3 kS = F; float3 kD = 1.0 - kS; kD *= (1.0 - mat.metallic); float3 numerator = NDF * G * F; float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0); float3 specular = numerator / max(denominator, 0.001); float pi = 3.14159265; float NdotL = max(dot(N, L), 0.0); lo = (kD * mat.albedo / pi + specular) * radiance * NdotL; }
上述程式碼中,V就是著色位置到相機的方向,N是法向,F0是前面提到的物體和菲涅爾效應有關的屬性,lo是該光源在物體上最終產生的輻射。
材質的定義如下:
struct Material
{
float3 albedo;
float roughness;
float metallic;
};
其中albedo可以認為是物體本身的顏色,roughness就是粗糙度,metallic則是金屬度。
PixelShader的程式碼如下所示:
float4 CustomPS(VertexOut pin, uniform int gPointLightCount, uniform int gDirLightCount, uniform bool gUseShadowMap, uniform bool gUseSSAO) : SV_Target { float3 color = float3(0.0f, 0.0f, 0.0f); pin.NormalW = normalize(pin.NormalW); float3 V = normalize(gEyePosW - pin.PosW); V = normalize(V); // 根據金屬度計算物體的F0 float3 F0 = float3(0.04, 0.04, 0.04); F0 = lerp(F0, gMaterial.albedo, gMaterial.metallic); float3 L0 = float3(0.0, 0.0, 0.0); float shadow = 1.0; if (gUseShadowMap) shadow = CalcShadowFactor(samShadow, gShadowMap, pin.ShadowPosH); float ambient_weight = 1.0; if (gUseSSAO) { pin.SSAOPosH /= pin.SSAOPosH.w; ambient_weight = gSSAOMap.Sample(samLinear, pin.SSAOPosH.xy, 0.0f).r; } float3 ambient = gMaterial.albedo * 0.03 * ambient_weight; // // Lighting. // if (gPointLightCount + gDirLightCount > 0) { [unroll] for (int i = 0; i < gDirLightCount; ++i) { float3 lo = float3(0.0, 0.0, 0.0); ComputeDirectionalLight(gMaterial, gDirLights[i], pin.NormalW, V, F0, lo); color += shadow * lo; } [unroll] for (int i = 0; i < gPointLightCount; i++) { float3 lo = float3(0.0, 0.0, 0.0); ComputePointLight(gMaterial, gPointLights[i], pin.PosW, pin.NormalW, V, F0, lo); color += shadow * lo; } } color += ambient; if (enableHDR) { float exposure = max(0.0, HDRexposure); color = 1.0 - exp(color * exposure); } if (gammaCorrection) { float gamma_ratio = 1.0 / 2.2; color = pow(color, gamma_ratio); } return float4(color, 1.0); }
基本計算過程就如上所示。最後展示一下不同粗糙度和金屬度下渲染結果。
圖中共有25個球,從左到右其粗糙度越來越大,從上到下其金屬度越來越大。