CubeMap采樣
Unity提供了Unity_GlossyEnvironment函數來對cubemap進行采樣。該函數的實現如下:
half3 Unity_GlossyEnvironment (UNITY_ARGS_TEXCUBE(tex), half4 hdr, Unity_GlossyEnvironmentData glossIn)
{
half perceptualRoughness = glossIn.roughness /* perceptualRoughness */ ;
// TODO: CAUTION: remap from Morten may work only with offline convolution, see impact with runtime convolution!
// For now disabled
#if 0
float m = PerceptualRoughnessToRoughness(perceptualRoughness); // m is the real roughness parameter
const float fEps = 1.192092896e-07F; // smallest such that 1.0+FLT_EPSILON != 1.0 (+1e-4h is NOT good here. is visibly very wrong)
float n = (2.0/max(fEps, m*m))-2.0; // remap to spec power. See eq. 21 in --> https://dl.dropboxusercontent.com/u/55891920/papers/mm_brdf.pdf
n /= 4; // remap from n_dot_h formulatino to n_dot_r. See section "Pre-convolved Cube Maps vs Path Tracers" --> https://s3.amazonaws.com/docs.knaldtech.com/knald/1.0.0/lys_power_drops.html
perceptualRoughness = pow( 2/(n+2), 0.25); // remap back to square root of real roughness (0.25 include both the sqrt root of the conversion and sqrt for going from roughness to perceptualRoughness)
#else
// MM: came up with a surprisingly close approximation to what the #if 0'ed out code above does.
perceptualRoughness = perceptualRoughness*(1.7 - 0.7*perceptualRoughness);
#endif
half mip = perceptualRoughnessToMipmapLevel(perceptualRoughness);
half3 R = glossIn.reflUVW;
half4 rgbm = UNITY_SAMPLE_TEXCUBE_LOD(tex, R, mip);
return DecodeHDR(rgbm, hdr);
}
UNITY_ARGS_TEXCUBE宏是一個用來定義cubemap作為函數參數的宏,用於函數的聲明,我們在調用函數時需要相應地使用UNITY_PASS_TEXCUBE宏進行cubmap參數傳遞。hdr參數用於當cubemap中包含hdr顏色時,需要將hdr轉換到rgb顏色,一般直接傳unity_SpecCube0_HDR即可。Unity_GlossyEnvironmentData是unity定義的一個數據結構,我們需要設置它的roughness和reflUVW屬性,roughness就是材質的粗糙程度,越粗糙物體的反射越模糊;reflUVW就是反射向量,用於采樣cubemap。我們可以這樣調用該函數:
float3 reflectionDir = reflect(-viewDir, i.normal);
Unity_GlossyEnvironmentData envData;
envData.roughness = 1 - _Smoothness;
envData.reflUVW = reflectionDir;
float3 specular = Unity_GlossyEnvironment(UNITY_PASS_TEXCUBE(unity_SpecCube0), unity_SpecCube0_HDR, envData);
該函數首先對roughness進行變換,因為roughness和cubemap的mipmap level並非是線性關系,Unity使用了一個近似的公式來進行模擬:
因為roughness越大,物體的反射越模糊,這就類似於采樣的cubemap的mipmap level越高。得到對應的mipmap level之后,我們就可以用UNITY_SAMPLE_TEXCUBE_LOD對cubemap進行采樣,得到采樣的結果。如果是hdr格式,進一步轉換到rgb格式。
box projection
使用Unity提供的反射探針,我們可以方便地實現反射的效果。如果要顯示反射效果的物體是會移動的,我們需要在反射探針中勾選Box Projection,這樣反射探針的box會隨着物體移動而移動,從而只用一個反射探針,也可以實現不同的反射效果。同樣地,unity提供了BoxProjectedCubemapDirection函數來方便我們計算box projection下的反射向量:
inline float3 BoxProjectedCubemapDirection (float3 worldRefl, float3 worldPos, float4 cubemapCenter, float4 boxMin, float4 boxMax)
{
// Do we have a valid reflection probe?
UNITY_BRANCH
if (cubemapCenter.w > 0.0)
{
float3 nrdir = normalize(worldRefl);
#if 1
float3 rbmax = (boxMax.xyz - worldPos) / nrdir;
float3 rbmin = (boxMin.xyz - worldPos) / nrdir;
float3 rbminmax = (nrdir > 0.0f) ? rbmax : rbmin;
#else // Optimized version
float3 rbmax = (boxMax.xyz - worldPos);
float3 rbmin = (boxMin.xyz - worldPos);
float3 select = step (float3(0,0,0), nrdir);
float3 rbminmax = lerp (rbmax, rbmin, select);
rbminmax /= nrdir;
#endif
float fa = min(min(rbminmax.x, rbminmax.y), rbminmax.z);
worldPos -= cubemapCenter.xyz;
worldRefl = worldPos + nrdir * fa;
}
return worldRefl;
}
worldRefl即為世界空間的反射向量,worldPos即為要計算的點的世界坐標,cubemapCenter是反射探針的坐標,boxMin和boxMax是反射探針包圍盒的最小最大坐標。我們可以這樣調用BoxProjectedCubemapDirection:
float3 reflectionDir = reflect(-viewDir, i.normal);
float3 reflUVW = BoxProjectedCubemapDirection(reflectionDir, i.worldPos, unity_SpecCube0_ProbePosition, unity_SpecCube0_BoxMin, unity_SpecCube0_BoxMax);
繼續看函數內部實現,這里有個條件判斷,當cubemapCenter.w > 0時,說明反射探針啟用了box projection,我們只需要在啟用的情況下進行計算。
在啟用box projection時,傳入的worldRefl向量並非恰好是我們對cubemap采樣的向量,如圖所示,I為當前點,C為反射探針的中心點,P為cubemap的采樣點,我們要求的就是向量W。
首先,我們需要根據cubemap采樣的原理,計算出向量U。向量U的方向與worldRefl向量一致,長度為與最近的包圍盒的面相交的點的距離。容易知道,六個面到點I的距離可以表示為兩組三維向量:
float3 rbmax = (boxMax.xyz - worldPos);
float3 rbmin = (boxMin.xyz - worldPos);
然后根據歸一化后的worldRefl向量,也就是worldRefl向量的方向,可以得到沿該方向去,到達六個面所需要的時間:
float3 nrdir = normalize(worldRefl);
float3 rbmax = (boxMax.xyz - worldPos) / nrdir;
float3 rbmin = (boxMin.xyz - worldPos) / nrdir;
我們要求的所需要的最短時間必然是大於0的時間中的最小值:
float3 rbminmax = (nrdir > 0.0f) ? rbmax : rbmin;
float fa = min(min(rbminmax.x, rbminmax.y), rbminmax.z);
進而,可以求出向量U:
float3 u = nrdir * fa;
向量V的值很顯然就是兩個點世界坐標的差值:
float3 v = cubemapCenter.xyz - worldPos;
那么,向量W = U - V為:
float3 w = nrdir * fa + worldPos - cubemapCenter.xyz;
反射探針插值
Unity允許讓我們對兩個反射探針采樣的值進行插值融合,得到一個過渡的效果。Unity提供了UNITY_SPECCUBE_BLENDING宏來判斷當前平台是否支持反射探針融合。另外,unity_SpecCube0_BoxMin的w分量存儲了第一個反射探針所占的權重,如果權重比較大,我們的實現代碼就可以忽略第二個反射探針的存在,避免不必要的性能開銷:
#if UNITY_SPECCUBE_BLENDING
float interpolator = unity_SpecCube0_BoxMin.w;
UNITY_BRANCH
if (interpolator < 0.99999) {
float3 probe1 = Unity_GlossyEnvironment(
UNITY_PASS_TEXCUBE_SAMPLER(unity_SpecCube1, unity_SpecCube0),
unity_SpecCube0_HDR, envData
);
float3 specular = lerp(probe1, probe0, interpolator);
}
else {
float3 specular = probe0;
}
#else
float3 specular = probe0;
#endif
多次反射
為了實現鏡中鏡之類的效果,Unity支持對反射探針的cubemap進行多次渲染,這樣就可以把反射的信息也渲染到cubemap中。相關的設置在Window/Rendering/Lighting Settings中:
Reference
[1] Reflections
如果你覺得我的文章有幫助,歡迎關注我的微信公眾號(大齡社畜的游戲開發之路)-