翻译8 Unity Reflections
Posted baolong-chen
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了翻译8 Unity Reflections相关的知识,希望对你有一定的参考价值。
采样坏境
使用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原文
赞原作者!
以上是关于翻译8 Unity Reflections的主要内容,如果未能解决你的问题,请参考以下文章