采樣壞境
使用reflection probes探針
創建粗糙或光滑的鏡面
完成box投影與立方體采樣
混合兩個探針
work in Unity 5.6.6f2
1 環境映射-Environment Mapping
一塊完美的鏡子是不會發生漫反射,但現在我們自己的Shader包含的[光照:環境光、漫反射、高光反射]、紋理、陰影,結果看起來蠻好。但是當把Metallic設為1,Smoothness設位0.95,看起來很亮就很不自然了。從下圖看盡管顏色是白色但整個表面都是黑色,只有一個很小的高亮點。這個亮點形成1是光源的入射,2朝向觀察者的反射。1.1 金屬感高亮點
1.1 間接鏡面反射
之前對於間接光光照計算時,只計算了漫反射沒有計算鏡面反射,默認值給的0。這就是為什么球面是黑色。現在我們簡單的在CreateIndirectLight函數給specular變量賦值,看球的表面有什么變化:
indirectLight.specular = float3(1,0,1);
1.2 Pink!
這就給出突破點,計算間接光specular的正確值,就可以反射周圍環境!
1.3 改變smoothness值,注意邊緣過渡
關於邊緣反射,最著名的就是菲涅耳反射。我們先使用UNITY_BRDF_PBS的版本來計算。
1.2 環境采樣-Sampling Enviroment
為了反射周圍環境,我們需要采樣天空盒。場景內天空盒對應的內置變量是在UnityShaderVariables文件的unity_SpecCube0的,該變量類型取決於目標平台,又由HLSLSupport文件決定。而采樣函數UNITY_SAMPLE_TEXCUBE宏需要兩個參數,1是天空盒,2是uv。先用法線替代uv。同時天空盒支持HDR高動態范圍顏色,所以還需要對HDR解碼到RGB:UnityCG包含DecodeHDR函數
half4 envSample = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, i.normal); indirectLight.specular = DecodeHDR(envSample, unity_SpecCube0_HDR);
1.4 環境采樣
UNITY_SAMPLE_TEXCUBE函數是根據平台自動切換對應的CG函數
DecodeHDR轉RGB:RGBM包含解碼后的RGB,和M系數。最終的RGB要與xM
y
結果相乘。的
// Decodes HDR textures // handles dLDR, RGBM formats inline half3 DecodeHDR (half4 data, half4 decodeInstructions) { // Take into account texture alpha if decodeInstructions.w is true(the alpha value affects the RGB channels) half alpha = decodeInstructions.w * (data.a - 1.0) + 1.0; // If Linear mode is not supported we can skip exponent part #if defined(UNITY_COLORSPACE_GAMMA) return (decodeInstructions.x * alpha) * data.rgb; #else # if defined(UNITY_USE_NATIVE_HDR) return decodeInstructions.x * data.rgb; // Multiplier for future HDRI relative to absolute conversion. # else return (decodeInstructions.x * pow(alpha, decodeInstructions.y)) * data.rgb; # endif #endif }
1.3 反射追蹤-Tracing Reflections
雖然得到正確的顏色,但沒有看見正確的反射結果。因為上面使用了球體的法線采樣環境,且投影不依賴視圖方向,因此就好像在球體上畫了環境。為了得到正確的結果,我們需要得到從相機到表面的方向,然后用表面法線再反射該方向。
UnityIndirect CreateIndirectLight(Interpolators i, float3
viewDir
) { //。。。 #if defined(FORWARD_BASE_PASS)float3 reflectDir = reflect(-viewDir, i.normal); half4 envSample = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, reflectDir);
indirectLight.specular = DecodeHDR(envSample, unity_SpecCube0_HDR);
#endif
return indirectLight;
}
1.5 正確的反射
1.4 反射探針
Unity自帶反射探針組件。通過chuangjia你GameObject/Light/Reflection Probe。參數如下圖的的
1.6 Reflection Probe探針參數
參數詳解
Type可以設置位bake或realtime,不管那種模式都會渲染6次。其中realtime模式下可以讓程序通過代碼配置:采樣頻率、在滿足某種情況下激活采樣,這能適當地節省運行時計算量。而烘焙模式下,需要把物體設置為靜態模式。
2有瑕疵的反射
只有完美的光滑表面才能產生完美的反射,越粗糙的表面它的漫反射越多。如何模擬暗淡的模糊鏡面反射?
開啟MipMaps.
2.1 bake模式下,烘焙后得到的cubeMap
2.1 粗糙鏡面
我們可使用UNITY_SAMPLE_TEXCUBE_LOD宏指定采樣cubeMap的mipmap等級。由於烘焙得到的環境cubeMap使用三線性過濾,所以能混合相鄰mipMapLevel.這可使得根據smoothness大小確定mipmap等級。材質越粗糙,mipMap等級就越高。粗糙值范圍也是[0,1],也就是1-smoothess。Unity提供了UNITY_SPECCUBE_LOD_STEPS宏來計算這個范圍。
/*2Lod采樣*/ float3 reflectDir = reflect(-viewDir, i.normal); float roughness = 1 - _Smoothness; half4 envSample =UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, reflectDir, roughness * UNITY_SPECCUBE_LOD_STEPS);
indirectLight.specular =DecodeHDR(envSample, unity_SpecCube0_HDR);
2.2 smoothness衰減
事實上,粗糙度和mipmap等級不是線性的,Unity使用了1.7r – 0.7r2公式換算。r是原始的粗糙度
float3 reflectDir = reflect(-viewDir, i.normal); float roughness = 1 - _Smoothness;roughness *= 1.7 - 0.7 * roughness;
half4 envSample = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, reflectDir, roughness * UNITY_SPECCUBE_LOD_STEPS); indirectLight.specular = DecodeHDR(envSample, unity_SpecCube0_HDR);
2.3 更早地粗糙
UnityStandardBRDF文件提供了Unity_GlossyEnvironment函數計算roughness、采樣cubeMap、轉換HDR代碼,以及Unity_GlossyEnvironmentData結構體包含了roughness、反射方向。
/*3Unity宏*/ float3 reflectDir = reflect(-viewDir, i.normal);Unity_GlossyEnvironmentData
envData; envData.roughness = 1 - _Smoothness; envData.reflUVW = reflectDir; indirectLight.specular =Unity_GlossyEnvironment
( UNITY_PASS_TEXCUBE(unity_SpecCube0), unity_SpecCube0_HDR, envData );
這是Unity_GlossyEnvironment函數,內部計算細節與我們計算大致相同。

// ---------------------------------------------------------------------------- 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); }

#define UNITY_PASS_TEXCUBE(tex) tex, sampler##tex
2.2 模擬凹凸鏡
給材質球指定一個法線紋理。廉價的水面擾動模擬。
2.3 金屬vs非金屬
2.4 非金屬,0.5、0.75、0.9
2.5 金屬反射上色;非金屬反射不上色(方塊還是白色)
2.4 鏡面和陰影
間接光反射依賴於物體表面的直接光照,最明顯的就是陰影區域。對非金屬而言,陰影區域反倒很亮。
2.6 陰影區域更亮
對於金屬而言,smoothness越光滑陰影越暗
2.7 smoothnees由大到小
3 Box投影
3.1 一個probes,所有球體反射一樣
我們不想每個球體都配一個probe。但是只有一個Probe時為了能得到周圍的反射,我們要計算probe反射方向與每個立方體的交點,然后構造中心probe到此交點的向量,得到最終的反射。
3.1 反射探針box
3.2 box邊界
特性
box尺寸和原點確定了其位置在世界空間的立方體區域
始終與軸對齊,忽略所有旋轉和縮放
Unity使用這些區域決定在渲染時使用哪個探針
box指定了立方區域大小,以該大小進行投影
3.2 調整采樣方向-BoxProjection
增加BoxProjection函數,目的是初始化反射方向。從cubeMap坐標和box邊界采樣得到。
第一步是偏移box到相對該表面頂點為中心
/*初始化box投影方向*/ float3 BoxProjection(float3 direction, float3 position, float3 cubeMapPosition, float3 boxMin, float3 boxMax) { boxMin -= position; boxMax -= position; return direction; }
第二步縮放方向向量,以便從該位置移動到交點位置。
x維度,如果方向向量x分量是正數,它就指向box的max邊界;否則指向box的min邊界。然后用正確邊界值再除以方向向量的x分量,得到適當的標量。
y、z維度同理
取得三個標量,再從中拿到一個最小值。表示哪個邊界面最接近表面。
3.3 計算最近邊界面
float3 BoxProjection(float3 direction, float3 position, float3 cubeMapPosition, float3 boxMin, float3 boxMax) { boxMin -= position; boxMax -= position;float x = (direction.x > 0 ? boxMax.x : boxMin.x) / direction.x; float y = (direction.y > 0 ? boxMax.y : boxMin.y) / direction.y; float z = (direction.z > 0 ? boxMax.z : boxMin.z) / direction.z; float scalar = min(min(x, y), z);
return direction;
}
第三步使用最小標量縮放方向向量找到交點。通過減去cubeMap位置得到新的反射方向。
return direction * scalar + (position - cubeMapPosition);
可以使用任何非零向量對cubemap進行采樣。cubemap采樣基本上和我們剛才做的是一樣的。它指出向量指向哪個面,然后執行除法以找到與cubemap面相交的點。它使用這個點的適當坐標來采樣紋理。
簡化上面三步的代碼如下:
float3 BoxProjection(float3 direction, float3 position, float3 cubeMapPosition, float3 boxMin, float3 boxMax) { boxMin -= position; boxMax -= position; /* float x = (direction.x > 0 ? boxMax.x : boxMin.x) / direction.x; float y = (direction.y > 0 ? boxMax.y : boxMin.y) / direction.y; float z = (direction.z > 0 ? boxMax.z : boxMin.z) / direction.z; float scalar = min(min(x, y), z);*/ float3 scalarVec = (direction > 0 ? boxMax : boxMin) / direction; float scalar = min(min(scalarVec.x, scalarVec.y), scalarVec.z); return direction * scalar + (position - cubeMapPosition); }
3.4 正確的box投影
這樣的盒型投影探針能夠很好的解決多個probe帶來的性能問題。
3.3 可選的投影
組件有個Project toggle開關,用以控制是否使用盒型投影探針。Unity把這個開關值存在cubeMap坐標的第四個分量,如果w值大於0表示開啟盒型投影探針。
/*初始化box投影方向*/ float3 BoxProjection(float3 direction, float3 position,float4
cubeMapPosition, float3 boxMin, float3 boxMax) { boxMin -= position; boxMax -= position; /* float x = (direction.x > 0 ? boxMax.x : boxMin.x) / direction.x; float y = (direction.y > 0 ? boxMax.y : boxMin.y) / direction.y; float z = (direction.z > 0 ? boxMax.z : boxMin.z) / direction.z; float scalar = min(min(x, y), z);*/if (cubeMapPosition.w > 0) { float3 scalarVec = (direction > 0 ? boxMax : boxMin) / direction; float scalar = min(min(scalarVec.x, scalarVec.y), scalarVec.z); direction = direction * scalar + (position - cubeMapPosition); }
return direction;
}
Tips:Unity有一個UNITY_BRANCH宏來要求編譯器提供和和實際編寫時一樣的分支而不是條件賦值語句。不太贊同在shader使用大量分支語句
UNITY_BRANCH
if (cubemapPosition.w > 0) {
…
}
Unity也提供了計算采樣boxProjection反射方向的函數:
BoxProjectedCubemapDirection定義在UnityStandardUtils文件中。不使用的原因是對方向做了歸一化,前面講了任何非零向量都可采樣。
//------------------------------------------------------------------------------------- inline half3 BoxProjectedCubemapDirection (half3 worldRefl, float3 worldPos, float4 cubemapCenter, float4 boxMin, float4 boxMax) { // Do we have a valid reflection probe? UNITY_BRANCH if (cubemapCenter.w > 0.0) { half3 nrdir = normalize(worldRefl); #if 1 half3 rbmax = (boxMax.xyz - worldPos) / nrdir; half3 rbmin = (boxMin.xyz - worldPos) / nrdir; half3 rbminmax = (nrdir > 0.0f) ? rbmax : rbmin; #else // Optimized version half3 rbmax = (boxMax.xyz - worldPos); half3 rbmin = (boxMin.xyz - worldPos); half3 select = step (half3(0,0,0), nrdir); half3 rbminmax = lerp (rbmax, rbmin, select); rbminmax /= nrdir; #endif half fa = min(min(rbminmax.x, rbminmax.y), rbminmax.z); worldPos -= cubemapCenter.xyz; worldRefl = worldPos + nrdir * fa; } return worldRefl; }
4 混合反射探針
在探針組件定義的立方體區域邊界切換時,如何做到自然過渡?
4.1 box 邊界過渡僵硬
4.1 兩個探針插值計算
4.1 兩個探針插值計算
shader支持了兩個探針數據,第二個探針內置變量名是
unity_SpecCube1。我們要采樣兩次環境貼圖並依據哪個更優進行插值。Unity已經做了計算把插值值存在了
unity_SpecCube0_BoxMin的第四個分量w。w為1只是第一個探針,值小於1開始混合。
float3 reflectDir = reflect(-viewDir, i.normal); Unity_GlossyEnvironmentData envData; envData.roughness = 1 - _Smoothness; envData.reflUVW = BoxProjection(reflectDir, i.worldPos, unity_SpecCube0_ProbePosition, unity_SpecCube0_BoxMin, unity_SpecCube0_BoxMax);//reflectDir; float3 probe0 = Unity_GlossyEnvironment( UNITY_PASS_TEXCUBE(unity_SpecCube0), unity_SpecCube0_HDR, envData ); envData.reflUVW = BoxProjection(reflectDir, i.worldPos, unity_SpecCube1_ProbePosition, unity_SpecCube1_BoxMin, unity_SpecCube1_BoxMax); float3 probe1 = Unity_GlossyEnvironment( //UNITY_PASS_TEXCUBE(unity_SpecCube1),//注意:這里由於需要兩個探針過渡,但是場景內只有一個探針。所以用下面代碼消除錯誤。 UNITY_PASS_TEXCUBE_SAMPLER(unity_SpecCube1,unity_SpecCube0), unity_SpecCube1_HDR, envData ); indirectLight.specular = lerp(probe1 ,probe0, unity_SpecCube0_BoxMin.w);
4.2 正確過渡
4.2 探針盒重疊
4.2 探針盒重疊
多個探針重疊有一個權重值
4.3 weight
也可以混合探針和天空盒,其中off是關閉探針只用天空盒
4.4 reflection probes
4.3 優化
4.3 優化
由於計算兩個探針的計算量太大,增加一個分支
#if UNITY_SPECCUBE_BLENDING UNITY_BRANCH if (unity_SpecCube0_BoxMin.w < 0.9999) { envData.reflUVW = BoxProjection(reflectDir, i.worldPos, unity_SpecCube1_ProbePosition, unity_SpecCube1_BoxMin, unity_SpecCube1_BoxMax); float3 probe1 = Unity_GlossyEnvironment( //UNITY_PASS_TEXCUBE(unity_SpecCube1), UNITY_PASS_TEXCUBE_SAMPLER(unity_SpecCube1, unity_SpecCube0), unity_SpecCube1_HDR, envData ); indirectLight.specular = lerp(probe1, probe0, unity_SpecCube0_BoxMin.w); } else { indirectLight.specular = probe0; } #else indirectLight.specular = probe0; #endif
對於BoxProjection函數的優化指令:UNITY_SPECCUBE_BOX_PROJECTION
#if UNITY_SPECCUBE_BOX_PROJECTION UNITY_BRANCH if (cubeMapPosition.w > 0) { float3 scalarVec = (direction > 0 ? boxMax : boxMin) / direction; float scalar = min(min(scalarVec.x, scalarVec.y), scalarVec.z); direction = direction * scalar + (position - cubeMapPosition); } #endif
4.4 模擬反射的反彈
4.4 模擬反射的反彈
這有個光的反彈系數,最高5次。計算量很大。
5原文
贊原作者!