Skip to main content
 首页 » 资源教程 » Unity3D教程

C#渲染教程4--为物体打光(下)

2016年10月11日 19:20:435460蛮牛网

漫反射带反射率在伽马空间和线性空间中不同情况

1 镜面阴影

相对于漫反射而已,还有镜面反射。光线碰到表面后并不会扩散,而是直接从表面反射,这就使得我们能开到自己的在镜子中的样子。

与漫反射不同,镜面角度观察者的位置很重要,只有你正对着反射光线才能开的到,在其他角度,我们是看不见的。

所以我们需要知道反射后我们的观察角度。这就需要知道世界坐标下的表面位置和相机位置了。

我们可以确定表面的顶点坐标的位置,然后通过object-to-world方法转换矩阵,然后将结果传给fragment程序。

[C#] 纯文本查看 复制代码

?

01
02
03
04
05
06
07
08
09
10
11
12
13
14
struct Interpolators {
        float4 position : SV_POSITION;
        float2 uv : TEXCOORD0;
        float3 normal : TEXCOORD1;
        float3 worldPos : TEXCOORD2;
};
Interpolators MyVertexProgram (VertexData v) {
        Interpolators i;
        i.position = mul(UNITY_MATRIX_MVP, v.position);
        i.worldPos = mul(unity_ObjectToWorld, v.position);
        i.normal = UnityObjectToWorldNormal(v.normal);
        i.uv = TRANSFORM_TEX(v.uv, _MainTex);
        return i;
}

相机的位置我们可以通过方为float3类型的_WorldSpaceCameraPos变量得到,这定义在UnityShaderVariables中。我们发现可以通过减去表面坐标进行规范化。

[C#] 纯文本查看 复制代码

?

1
2
3
4
5
6
7
8
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
        i.normal = normalize(i.normal);
        float3 lightDir = _WorldSpaceLightPos0.xyz;
        float3 viewDir = normalize(_WorldSpaceCameraPos - i.worldPos);
        float3 lightColor = _LightColor0.rgb;
        float3 albedo = tex2D(_MainTex, i.uv).rgb * _Tint.rgb;
        float3 diffuse = albedo * lightColor * DotClamped(lightDir, i.normal);
        return float4(diffuse, 1);
}

1.1 反射光

要知道反射的光线,我们可以使用标准的reflect方法。这使得入射光线沿着法线反射。所以我们要反转光线的方向。

[C#] 纯文本查看 复制代码

?

1
float3 reflection\= reflect(-lightDir, i.normal);
        return float4(reflectionDir * 0.5 + 0.5, 1);

C#渲染教程4--为物体打光(下) Unity3D教程 第1张

反射方向

反射向量如何工作?

你可以将方向D通过法线N用计算D-2N(ND)得到。

对于完全光滑的镜子,只有在合适的角度我们才能看见管,在其他的角度,光线会错过我们,让表面看起来是黑色的。但是通常情况下表面不可能完美的光滑,总会有很多疙瘩,这就意味着表面有很多的法线。

所以我们能够看见一些反射光线,即便方向不是正对着我们的。离反射的方向越远,我们能看见的越少。这里我们再一次用到了clamped点积方法计算多少光线反射到达我们这。

[C#] 纯文本查看 复制代码

?


return DotClamped(viewDir, reflectionDir);

C#渲染教程4--为物体打光(下) Unity3D教程 第2张

C#渲染教程4--为物体打光(下) Unity3D教程 第3张

镜面反射

1.1 平滑

突出所产生的这种影响的大小取决于材料的粗糙度。光滑的材料集中光线更好,所以他们有更小的亮点。我们可以控制这个材料的平滑属性。它通常被定义为一个值在0和1之间的数,这里我们把它定义成为一个滑块。

[C#] 纯文本查看 复制代码

?

1
2
3
4
5
6
Properties {
        _Tint ("Tint", Color) = (1, 1, 1, 1)
        _MainTex ("Texture", 2D) = "white" {}
        _Smoothness ("Smoothness", Range(0, 1)) = 0.5
        }
        
        float _Smoothness;

我们要把高亮提高到一定等级。我们在这里使用平滑值,但是它要远大于1才能有明显的影响。所以我们把它乘以100.

[C#] 纯文本查看 复制代码

?

1
2
3
return pow(
        DotClamped(viewDir, reflectionDir),
        _Smoothness * 100
        );

C#渲染教程4--为物体打光(下) Unity3D教程 第4张

C#渲染教程4--为物体打光(下) Unity3D教程 第5张

漂亮的高光

1.1 Blinn-Phong光照模式

我们正在计算的内容是根据模拟金属反射的反射模型。但Blinn-Phong是经常被使用的另一种模型。它使用一个介于光线方向和视图方向的向量。法线和半矢量之间的点积决定了镜面的权值

[C#] 纯文本查看 复制代码

?

1
2
3
4
float3 halfVector = normalize(lightDir + viewDir);
        return pow(
                DotClamped(halfVector, i.normal),
                _Smoothness * 100
        );

C#渲染教程4--为物体打光(下) Unity3D教程 第6张

C#渲染教程4--为物体打光(下) Unity3D教程 第7张

Blinn-Phong镜面

这种方法会产生一个更大的亮点,但我看通过调节反射平滑度的值来控制。结果可以从视觉上看出这种实现比Phong方式更好一些,虽然这两种方法仍近似。其中一个限制是它会在物体背后产生产生无效的亮点。

C#渲染教程4--为物体打光(下) Unity3D教程 第8张

不正确的镜面,平滑值0.01

这些物件变得明显在使用低平滑值的情况下。他们可以通过使用阴影隐藏这些问题,或者通过消除镜面的基础光照角度。Unity以前的着色器也有这个问题,所以我们不担心。我们很快就会使用另一个照明方案。

镜面颜色

当然镜面反射也需要匹配光源的颜色。我们现在来实现。

[C#] 纯文本查看 复制代码

?

1
2
3
4
5
float3 halfVector = normalize(lightDir + viewDir);
float3 specular = lightColor * pow(
        DotClamped(halfVector, i.normal),
        _Smoothness * 100
);
return float4(specular, 1);

但也并不全是这样。反射的颜色也取决于材料。这和反射率不大相同。金属如果有很少的反射率,然而却通常有着强大的彩色镜面反射率。相比之下,非金属矿物往往有独特的反照率,而他们的镜面反射率较弱,而且不是彩色的。

我们可以添加一个纹理和色彩定义高光颜色,就像我们在反照率中使用的一样。但是我们不会对另一个纹理使用色彩。

[C#] 纯文本查看 复制代码

?

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
Properties {
        _Tint ("Tint", Color) = (1, 1, 1, 1)
        _MainTex ("Albedo", 2D) = "white" {}
        _SpecularTint ("Specular", Color) = (0.5, 0.5, 0.5)
        _Smoothness ("Smoothness", Range(0, 1)) = 0.1
        }
        
        float4 _SpecularTint;
        float _Smoothness;
        
        float4 MyFragmentProgram (Interpolators i) : SV_TARGET     {
                                
        float3 halfVector = normalize(lightDir + viewDir);
        float3 specular = _SpecularTint.rgb * lightColor * pow(
        DotClamped(halfVector, i.normal),
        _Smoothness * 100
        );
        return float4(specular, 1);
}

我们可以控制着色的力度力量和镜面反射颜色属性。

C#渲染教程4--为物体打光(下) Unity3D教程 第9张

C#渲染教程4--为物体打光(下) Unity3D教程 第10张

带颜色的镜面反射

我们不能使用颜色的alpha通道存储平衡平滑值?

这的确是可以的。你也可以把高光颜色和平滑值存储在一个单独的纹理中。

漫反射和镜面

解决的镜面反射和漫反射的难题,我们可以把他应用到我们的图片中让其更完美。

[C#] 纯文本查看 复制代码

?


return float4(diffuse + specular, 1);

C#渲染教程4--为物体打光(下) Unity3D教程 第11张

C#渲染教程4--为物体打光(下) Unity3D教程 第12张

漫反射加上镜面反射在伽马空间和线性空间中个不同表现

能量转换

如果只是单纯的将漫反射和镜面反射结合一起将会出现一个问题。就是结合后的结果将变得比原光源更加的明亮。特别是将白色高光和低平滑值相结合的时候。

C#渲染教程4--为物体打光(下) Unity3D教程 第13张

白色镜面反射,0.1平滑值,过亮问题

当光线碰撞到表面的时候,一部分的光线已镜面光的形式反射回来。另外的部分已漫反射的形式反射,或者被吸收。但是我们没有把这部分因素考虑进去。我们的反射包含了镜面反射和漫反射所有的部分。所以我们得到了两倍于原来光线的能量。

我们要使得漫反射和镜面反射光线的总和不大于1。这保证了我们不会凭空创作出更多的光线。如果光线总和小于1是可行的,这意味着一部分光线别吸收了。

当我们使用一个固定反射率的值的时候,我们可以通过简单的条件反射率的值乘上1减去镜面反射。但是这样做的缺点是手动调节不方便,特别是我们需要使用特殊反射率颜色的时候。所以我们在shader中这么实现。

[C#] 纯文本查看 复制代码

?

1
float3 albedo = tex2D(_MainTex, i.uv).rgb * _Tint.rgb;
        albedo *= 1 - _SpecularTint.rgb;

C#渲染教程4--为物体打光(下) Unity3D教程 第14张

现在漫反射和镜面反射已经结合在一起了。镜面反射强,漫反射弱。黑色的镜面0反射率,这保证了我们看到玩吗的白色镜面反射,其他的反射问题得到了解决。

C#渲染教程4--为物体打光(下) Unity3D教程 第15张

能量转换

单色

在镜面反射会灰色的适合,这种方式运行的很好。但是在其他颜色的情况下。例如红色的镜面反射,因为反射的红色的光线,结果让物体看起来变成了青色。

C#渲染教程4--为物体打光(下) Unity3D教程 第16张

红色镜面反射,青色的反射率

为了防止这种情况出现,我们使用单色。这意味着我们使用最强的反射率的颜色去减少反射率。

[C#] 纯文本查看 复制代码

?


albedo *= 1 - max(_SpecularTint.r, max(_SpecularTint.g, _SpecularTint.b));

C#渲染教程4--为物体打光(下) Unity3D教程 第17张

效能函数

正如你所期待的,unity中提供了方便的能量转换方法。它是EnergyConservationBetweenDiffuseAndSpecular,它被定义在UnityStandardUtils中。

[C#] 纯文本查看 复制代码

?

1
#include "UnityStandardBRDF.cginc"
#include "UnityStandardUtils.cginc"

C#渲染教程4--为物体打光(下) Unity3D教程 第18张

UnityStandardUtils层级包含关系

这个方法使用反照率和镜面反射颜色作为输入参数,输出参数为调整后的反射率。但是他还有第三个输入参数,就是1减去反色率。这就是我们减去镜面强度乘以反照率的原因。它是一个额外的输出,因为反射率在计算其他照明情况下是必要的。

vationBetweenDiffuseAndSpecular方法的具体实现?

这里定义了三种模式,不论是保护,单色还是彩色,我们可以通过定义#define来处理不同的情况,默认情况为单色。

[C#] 纯文本查看 复制代码

?

1
2
3
4
float3 albedo = tex2D(_MainTex, i.uv).rgb * _Tint.rgb;
float oneMinusReflectivity;
        albedo = EnergyConservationBetweenDiffuseAndSpecular(
        albedo, _SpecularTint.rgb, oneMinusReflectivity
);

EnergyConser

[C#] 纯文本查看 复制代码

?

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
half SpecularStrength(half3 specular) {
        #if (SHADER_TARGET < 30)
        // SM2.0: instruction count limitation
        // SM2.0: simplified SpecularStrength
        // Red channel - because most metals are either monochrome
        // or with redish/yellowish tint
                return specular.r;
        #else
                return max(max(specular.r, specular.g), specular.b);
        #endif
}
// Diffuse/Spec Energy conservation
inline half3 EnergyConservationBetweenDiffuseAndSpecular (
        half3 albedo, half3 specColor, out half oneMinusReflectivity
) {
        on\= 1 - SpecularStrength(specColor);
        #if !UNITY_CONSERVE_ENERGY
                return albedo;
        #elif UNITY_CONSERVE_ENERGY_MONOCHROME
                return albedo * oneMinusReflectivity;
        #else
                return albedo * (half3(1, 1, 1) - specColor);
        #endif
}

金属工作流

有两种最基本的材料是我们所关心的。他们是金属和非金属。后者又被称为导电材料。目前,我们可以通过一个强烈的镜面色彩效果创造金属的质感。我们可以通过使用一个弱单色的镜面创建导电曹植。这就是镜面的工作流程。

它很简单,如果我们可以只在金属和非金属之间切换。金属没有反照率,我们可以直接使用颜色数据,镜面颜色相反。非金属没有彩色镜面,所以我们不需要一个单独的高光色。这被称为金属工作流程。让我们一起实现吧。

哪一种是更好的工作流?

这两种方法都很好。这就是为什么Unity对每个都有一个统一的shader标准。金属的工作流程更简单,因为你只需要一种颜色来源加上一个滑块。这是能够创造真实的材料。镜面工作流可以产生相同的结果,但是因为你需要控制更多,不现实的材料也是可以的。

我们可以使用另一个滑块属性来作为控制金属的开关,用它来更换镜面颜色。通常情况下,它应该被设置为0或者1,因为用来表示是或不是金属。之间的其他值代表混合的金属和非金属特效的组件。

C#渲染教程4--为物体打光(下) Unity3D教程 第19张

金属滑块

现在我们可以推导出高光色调的反照率和金属属性。反照率可以简单地乘以一个负的金属的价。

[C#] 纯文本查看 复制代码

?

01
02
03
04
05
06
07
08
09
10
11
Properties {
                _Tint ("Tint", Color) = (1, 1, 1, 1)
                _MainTex ("Albedo", 2D) = "white" {}
                _Metallic ("Metallic", Range(0, 1)) = 0
                _Smoothness ("Smoothness", Range(0, 1)) = 0.1
[mw_shl_code=csharp,true]float3 specularTint = albedo * _Metallic;
float on\= 1 - _Metallic;
albedo *= oneMinusReflectivity;
float3 diffuse = albedo * lightColor * DotClamped(lightDir, i.normal);
float3 halfVector = normalize(lightDir + viewDir);
float3 specular = specularTint * lightColor * pow(DotClamped(halfVector, i.normal), _Smoothness * 100
);

        }

float _Metallic;

        float _Smoothness;[/mw_shl_code]

然而这样的处理过于简单化。即使是纯粹的导电体仍然有一些镜面反射。所以高光强度和反射值不能精确的匹配金属滑块的值。这也是影响颜色空间。幸运的是UnityStandardUtils中DiffuseAndSpecularFromMetallic函数能帮助我们解决这个问题。

[C#] 纯文本查看 复制代码

?

1
2
3
float3 specularTint;
float oneMinusReflectivity;
albedo = DiffuseAndSpecularFromMetallic(albedo, _Metallic, specularTint, oneMinusReflectivity
);

C#渲染教程4--为物体打光(下) Unity3D教程 第20张

金属工作流

DiffuseAndSpecularFromMetallic这个方法的实现是什么?

这里使用了half4类型的unity_ColorSpaceDielectricSpec变量,这里包含的Unity的基本颜色空间。

[C#] 纯文本查看 复制代码

?

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
inline half OneMinusReflectivityFromMetallic(half metallic) {
        // We'll need oneMinusReflectivity, so
        //   1-reflectivity = 1-lerp(dielectricSpec, 1, metallic)
        // = lerp(1-dielectricSpec, 0, metallic)
        // store (1-dielectricSpec) in unity_ColorSpaceDielectricSpec.a, then
        //         1-reflectivity = lerp(alpha, 0, metallic)
        //                  = alpha + metallic*(0 - alpha)
        //                  = alpha - metallic * alpha
        half on\= unity_ColorSpaceDielectricSpec.a;
        return oneMinusDielectricSpec - metallic * oneMinusDielectricSpec;
}
inline half3 DiffuseAndSpecularFromMetallic (
        half3 albedo, half metallic,
        out half3 specColor, out half oneMinusReflectivity
) {
        specColor = lerp(unity_ColorSpaceDielectricSpec.rgb, albedo, metallic);
        on\= OneMinusReflectivityFromMetallic(metall[mw_shl_code=csharp,true][Gamma] _Metallic ("Metallic", Range(0, 1)) = 0

ic);

        return albedo * oneMinusReflectivity;

}[/mw_shl_code]

其中一个细节是金属滑块本身应该在伽马空间。但单个值不会自动在伽马空间中被Unity纠正,在线性空间显示。我们可以使用伽马属性告诉Unity,它还应该吧伽马校正应用到金属滑块。

不幸的是现在的镜面反射已经变得想非金属一样,相当的模糊。为了改善这种情况,我们需要一个更好的方法来计算照明。

1 基于物理的阴影

Blinn-Phong一直是游戏产业的主力,但是现在基于实物的阴影被称为PBS是风靡一时。这是有其中充分理由的,因为它表现的更为现实的和可预测。理想情况下,游戏引擎和建模工具都需要使用相同的阴影算法。这使得创建内容更加容易。工业生产的情况正在慢慢融合到一个统一的PBS的实现下。

统一的标准着色器使用PBS的方法。Unity中有多种实现。它决定使用是就与目标平台,硬件和API级别不同和选择的。该算法通过UNITY_BRDF_PBS宏定义在UnityPBSLighting。BRDF代表双向反射分布函数。

[C#] 纯文本查看 复制代码

?


#include "UnityPBSLighting.cginc"

C#渲染教程4--为物体打光(下) Unity3D教程 第21张

UnityPBSLighting文件层次关系

UNITY_BRDF_PBS的内部实现?

这是Unity 中BRDF方法的别名。UNITY_PBS_USE_BRDF1在平台中被定义为Unity的默认值。除非目标的Unity平台版本低于3.0,否则它将被选为最佳的Shader。

[C#] 纯文本查看 复制代码

?

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
// Default BRDF to use:
#if !defined (UNITY_BRDF_PBS)
        // allow to explicitly override BRDF in custom shader
        // still add safe net for low shader models,
        // otherwise we might end up with shaders failing to compile
        #if SHADER_TARGET < 30
                #define UNITY_BRDF_PBS BRDF3_Unity_PBS
        #elif UNITY_PBS_USE_BRDF3
                #define UNITY_BRDF_PBS BRDF3_Unity_PBS
        #elif UNITY_PBS_USE_BRDF2
                #define UNITY_BRDF_PBS BRDF2_Unity_PBS
        #elif UNITY_PBS_USE_BRDF1
                #define UNITY_BRDF_PBS BRDF1_Unity_PBS
        #elif defined(SHADER_TARGET_SURFACE_ANALYSIS)
                // we do preprocess pass during shader analysis and we dont
                // actually care about brdf as we need only inputs/outputs
                #define UNITY_BRDF_PBS BRDF1_Unity_PBS
        #else
                #error something broke in auto-choosing BRDF
        #endif
#endif

我没有包含实际的功能,因为它太庞大了。你可以通过下载Unity的头文件,或者找到你自己的Unity安装文件。他们在UnityStandardBRDF中。

这些方法包含了大量的数学计算,所以我打算展开来说。它们依然是计算漫反射和镜面反射的,只不过采用了和Blinn-Phong不同的方式。除此之外,还有一个Fresnel反射组件。它包含该了当可视物体反射角很大时候的处理。特别是环境反射的时候。

为了保证我们的Unity能够选择最好的BRDF方法,我们的平台shader版本至少需要3.0.我们把他写到编译指令中。

[C#] 纯文本查看 复制代码

?

1
2
3
CGPROGRAM
#pragma target 3.0
#pragma vertex MyVertexProgram
#pragma fragment MyFragmentProgram

Unity的BRDF方法返回RGBA的颜色,alpha通道总是1.所以我们在片段程序中直接返回结果。

[C#] 纯文本查看 复制代码

?


return UNITY_BRDF_PBS();

当然我们需要通过参数调用它。这个方法需要八个参数。头两个是漫反射和镜面反射材质的颜色。我们已经有了。

[C#] 纯文本查看 复制代码

?

1
2
return UNITY_BRDF_PBS(
        albedo, specularTint
);

接下来两个参数是反射率和粗糙度。这些参数必须是1减去某某的形式,便于优化。我们已经拥有了oneMinusReflectivity相对于DiffuseAndSpecularFromMetallic的输出。平滑值的相反就是粗糙度,所以我们能直接使用它们。

[C#] 纯文本查看 复制代码

?

1
2
3
return UNITY_BRDF_PBS(
albedo, specularTint,
oneMinusReflectivity, _Smoothness
);

当然表面法线和观察角度也是必须的。他们就是第五和第六个参数。

[C#] 纯文本查看 复制代码

?

1
2
3
4
return UNITY_BRDF_PBS(
albedo, specularTint,
oneMinusReflectivity, _Smoothness,
i.normal, viewDir
);

最后两个参数是直接光源和间接光源。

光照结构

UnityLightingCommon中定义了一个简单的UnityLight结构,用于Unity在shader中处理光照数据。它里面包含了光的颜色,方向,和ndotl值,这是一个漫反射术语。要记住,这个结构只是为了方便我们计算。并不影响编码。

我们现在知道了所以我们都想要的信息。所以我们现在要做的是把光照结构当做第七个参数传入。

[C#] 纯文本查看 复制代码

?

1
2
3
4
5
6
7
8
9
UnityLight light;
light.color = lightColor;
light.dir = lightDir;
light.ndotl = DotClamped(i.normal, lightDir);
return UNITY_BRDF_PBS(
        albedo, specularTint,
        oneMinusReflectivity, _Smoothness,
        i.normal, viewDir,
        light
);

为什么光照数据包含了漫反射术语?

想BRDF方法所有它需要的都通过自己计算获得,为什么我们还要额外提供呢?因为需要把光照结构当做其他的上下文使用。

实际上,GGX BRDF的版本不使用ndotl。它通过自己计算,就像正常的小提琴一样。而且,shader编译器总是能找到无用的代码,所以我们不必过分担心这个问题。

最后一个参数是间接光源。我们使用unityIndirect结构,它同样定义在UnityLightingCommon中。它包含了两种颜色,一个是漫反射,一个是镜面反射。漫反射颜色代表了环境光,镜面反射代表了环境反射。

我们稍后使用间接光源,这里我们先用简单的黑色代替它。

[C#] 纯文本查看 复制代码

?

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
                                i.normal = normalize(i.normal);
                                float3 lightDir = _WorldSpaceLightPos0.xyz;
                                float3 viewDir = normalize(_WorldSpaceCameraPos - i.worldPos);
                                float3 lightColor = _LightColor0.rgb;
                                float3 albedo = tex2D(_MainTex, i.uv).rgb * _Tint.rgb;
                                float3 specularTint;
                                float oneMinusReflectivity;
                                albedo = DiffuseAndSpecularFromMetallic(
                                        albedo, _Metallic, specularTint, oneMinusReflectivity
                                );
                                UnityLight light;
                                light.color = lightColor;
                                light.dir = lightDir;
                                light.ndotl = DotClamped(i.normal, lightDir);
                                UnityIndirect indirectLight;
                                indirectLight.diffuse = 0;
                                indirectLight.specular = 0;
                                return UNITY_BRDF_PBS(
                                        albedo, specularTint,
                                        oneMinusReflectivity, _Smoothness,
                                        i.normal, viewDir,
                                        light, indirectLight
                                );
                        }

C#渲染教程4--为物体打光(下) Unity3D教程 第22张

C#渲染教程4--为物体打光(下) Unity3D教程 第23张

C#渲染教程4--为物体打光(下) Unity3D教程 第24张

C#渲染教程4--为物体打光(下) Unity3D教程 第25张

非金属和金属在伽马空间和线性空间下的表现

评论列表暂无评论
发表评论