翻译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);
}
View Code
技术图片
#define UNITY_PASS_TEXCUBE(tex) tex, sampler##tex
View Code

 

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边界

特性

  1. box尺寸和原点确定了其位置在世界空间的立方体区域
  2. 始终与轴对齐,忽略所有旋转和缩放
  3. Unity使用这些区域决定在渲染时使用哪个探针
  4. 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;
}

第二步缩放方向向量,以便从该位置移动到交点位置。

  1. x维度,如果方向向量x分量是正数,它就指向box的max边界;否则指向box的min边界。然后用正确边界值再除以方向向量的x分量,得到适当的标量。
  2. y、z维度同理
  3. 取得三个标量,再从中拿到一个最小值。表示哪个边界面最接近表面。

技术图片

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 两个探针插值计算

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.3 weight

也可以混合探针和天空盒,其中off是关闭探针只用天空盒

技术图片

4.4 reflection probes

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 模拟反射的反弹

技术图片

这有个光的反弹系数,最高5次。计算量很大。

5原文

赞原作者!

以上是关于翻译8 Unity Reflections的主要内容,如果未能解决你的问题,请参考以下文章

Unity2017.1官方UGUI文档翻译——Text

如何将此 JavaScript 代码片段翻译成 Parenscript?

Shader HLSL片段说明

反思 - Java 8 - 无效的常量类型

Java非常好用的反射框架Reflections

翻译5 Unity Advanced Lighting