本教程涵盖了各向异性镜面高光。
这是几个关于光照教程中的其中一个教程,这个光照超出了Phone反射模型的范围。但是,它是基于章节“镜面高光”(逐顶点光照)以及章节“光滑镜面高光”(逐像素光照)中描述的Phone反射模型光照。如果你没阅读过那两章,建议先阅读一下。
对于纸张、塑料以及一些其它各向同性的材质来说,Phone反射模型看上去还是相当不错的。本教程特别关注各向异性反射材质(即非圆形高光),就比如上图中的拉丝铝。
Ward的各向异性反射模型
Gregory Ward在他的论文“各向异性反射测量与建模”,计算机图形学(SIGGRAPH ’92 Proceedings), pp. 265–272, July 1992中公布了一种合适的各向异性反射模型。(网上可以找到该论文。)这个模型用BRDF(双向反射分布函数)来描述反射,它是一个四维函数,描述了任意方向的光线是如何反射到任意其它方向上。他的BRDF模型包含了两项:一个漫反射项,以及一个比较复杂的镜面反射项。
让我们先来看一下漫射项:π是一个常量(约是3.14159),
指定了漫反射率。原则上每个波长的反射率是必要的;但是,通常对三种颜色分量(红、绿和蓝)中的每一个都指定一个反射率。如果我们包含了常量π,
就表示漫射材质颜色
,我们第一次看到它是在章节“漫反射”中,但是它也出现在Phone反射模型中(参考章节“镜面高光”)。你可能疑惑为什么因子max(0, L·N)没有出现在BRDF中。原因在于BRDF就是如此定义的,这个因子没有包含进来(因为它实际并不是材质的一个属性),但是当计算任何光照的时候应该把它乘以BRDF。
于是,为了对不透明材质实现一个指定的BRDF,我们必须用max(0, L·N)乘以BRDF所有的项,以及除非我们想要实现物理上正确的光照,我们可以把任何常量因子替换成用户指定的颜色,它通常比物理量更容易控制。
对于BRDF模型中特殊的项,Ward在他的论文中提出了一个近似方程5b。我稍微修改了一下,这样就可以使用归一化的表面法向量N,指向观察者的归一化向量V,指向光源的归一化向量L,以及归一化的中间向量H(V + L) / | V + L |。利用这些向量,Ward对于这个特殊项的近似就成为了这个样子:
这里,是镜面反射率,它描述了颜色和镜面高光的强度;
和
是描述高光形状和大小的材质常量。既然所有这些变量就是材质常量,我们可以把它们整合到一个常量
中去。于是我们得到一个稍微简短点的版本:
记住当在着色器中实现它时,我们仍然需要把BRDF项乘以L·N,并且当L·N小于0的时候把它设置为0。此外,如果L·N的值小于0它也应该是0,也就是如果从“错”的面看表面的话。
还有两个向量没有描述过:T和B。T是表面上刷(拉丝)的方向,B垂直于T并且也在表面上。Unity为我们提供了一个表面上的切线向量作为一个顶点属性(参考章节“着色器调试”),我们就把它用作向量T。计算N和T的叉乘可以得到向量B,它垂直于N和T。
Ward的BRDF模型的实现
我们的实现基于章节“光滑镜面高光”中的逐像素光照着色器。对于切线向量T(也就是刷的方向)我们需要另一个顶点输出参数tangentDir
,并且我们也需要在顶点着色器中计算viewDir
这样可以在片元着色器中节省一些指令。在片元着色器中,我们计算额外两个方向:中间向量H的halfwayVector
和副法线向量B的binormalDirection
。属性_Color
就是,
_SpecColor
就是,
_AlphaX
就是,
_AlphaY
就是。
这个片元着色器跟章节“光滑镜面高光”中的版本非常类似,除了它会计算halfwayVector
和binormalDirection
,为镜面部分实现一个不同的等式。此外,这个着色器只计算一次点乘L·N并且把值存储在dotLN
中,这样就可以重复使用而无需再计算了。
float4 frag(vertexOutput input) : COLOR
{
float3 lightDirection;
float attenuation;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
attenuation = 1.0; // no attenuation
lightDirection = normalize(_WorldSpaceLightPos0.xyz);
}
else // point or spot light
{
float3 vertexToLightSource =
_WorldSpaceLightPos0.xyz - input.posWorld.xyz;
float distance = length(vertexToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(vertexToLightSource);
}
float3 halfwayVector =
normalize(lightDirection + input.viewDir);
float3 binormalDirection =
cross(input.normalDir, input.tangentDir);
float dotLN = dot(lightDirection, input.normalDir);
// compute this dot product only once
float3 ambientLighting =
UNITY_LIGHTMODEL_AMBIENT.rgb * _Color.rgb;
float3 diffuseReflection =
attenuation * _LightColor0.rgb * _Color.rgb
* max(0.0, dotLN);
float3 specularReflection;
if (dotLN < 0.0) // light source on the wrong side?
{
specularReflection = float3(0.0, 0.0, 0.0);
// no specular reflection
}
else // light source on the right side
{
float dotHN = dot(halfwayVector, input.normalDir);
float dotVN = dot(input.viewDir, input.normalDir);
float dotHTAlphaX =
dot(halfwayVector, input.tangentDir) / _AlphaX;
float dotHBAlphaY = dot(halfwayVector,
binormalDirection) / _AlphaY;
specularReflection =
attenuation * _LightColor0.rgb * _SpecColor.rgb
* sqrt(max(0.0, dotLN / dotVN))
* exp(-2.0 * (dotHTAlphaX * dotHTAlphaX
+ dotHBAlphaY * dotHBAlphaY) / (1.0 + dotHN));
}
return float4(ambientLighting + diffuseReflection
+ specularReflection, 1.0);
}
注意sqrt(max(0, dotLN / dotVN))
这项,它由乘以(L·N)得到。这个保证了结果始终大于0。
完整的着色器代码只是定义了合适的属性并且为切线添加了另一个顶点输入参数。它也需要第二个通道,里面有加性混合但没有额外光源的环境光照。
Shader "Cg anisotropic per-pixel lighting" {
Properties {
_Color ("Diffuse Material Color", Color) = (1,1,1,1)
_SpecColor ("Specular Material Color", Color) = (1,1,1,1)
_AlphaX ("Roughness in Brush Direction", Float) = 1.0
_AlphaY ("Roughness orthogonal to Brush Direction", Float) = 1.0
}
SubShader {
Pass {
Tags { "LightMode" = "ForwardBase" }
// pass for ambient light and first light source
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
uniform float4 _LightColor0;
// color of light source (from "Lighting.cginc")
// User-specified properties
uniform float4 _Color;
uniform float4 _SpecColor;
uniform float _AlphaX;
uniform float _AlphaY;
struct vertexInput {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};
struct vertexOutput {
float4 pos : SV_POSITION;
float4 posWorld : TEXCOORD0;
// position of the vertex (and fragment) in world space
float3 viewDir : TEXCOORD1;
// view direction in world space
float3 normalDir : TEXCOORD2;
// surface normal vector in world space
float3 tangentDir : TEXCOORD3;
// brush direction in world space
};
vertexOutput vert(vertexInput input)
{
vertexOutput output;
float4x4 modelMatrix = _Object2World;
float4x4 modelMatrixInverse = _World2Object;
output.posWorld = mul(modelMatrix, input.vertex);
output.viewDir = normalize(_WorldSpaceCameraPos
- output.posWorld.xyz);
output.normalDir = normalize(
mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
output.tangentDir = normalize(
mul(modelMatrix, float4(input.tangent.xyz, 0.0)).xyz);
output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
return output;
}
float4 frag(vertexOutput input) : COLOR
{
float3 lightDirection;
float attenuation;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
attenuation = 1.0; // no attenuation
lightDirection = normalize(_WorldSpaceLightPos0.xyz);
}
else // point or spot light
{
float3 vertexToLightSource =
_WorldSpaceLightPos0.xyz - input.posWorld.xyz;
float distance = length(vertexToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(vertexToLightSource);
}
float3 halfwayVector =
normalize(lightDirection + input.viewDir);
float3 binormalDirection =
cross(input.normalDir, input.tangentDir);
float dotLN = dot(lightDirection, input.normalDir);
// compute this dot product only once
float3 ambientLighting =
UNITY_LIGHTMODEL_AMBIENT.rgb * _Color.rgb;
float3 diffuseReflection =
attenuation * _LightColor0.rgb * _Color.rgb
* max(0.0, dotLN);
float3 specularReflection;
if (dotLN < 0.0) // light source on the wrong side?
{
specularReflection = float3(0.0, 0.0, 0.0);
// no specular reflection
}
else // light source on the right side
{
float dotHN = dot(halfwayVector, input.normalDir);
float dotVN = dot(input.viewDir, input.normalDir);
float dotHTAlphaX =
dot(halfwayVector, input.tangentDir) / _AlphaX;
float dotHBAlphaY = dot(halfwayVector,
binormalDirection) / _AlphaY;
specularReflection =
attenuation * _LightColor0.rgb * _SpecColor.rgb
* sqrt(max(0.0, dotLN / dotVN))
* exp(-2.0 * (dotHTAlphaX * dotHTAlphaX
+ dotHBAlphaY * dotHBAlphaY) / (1.0 + dotHN));
}
return float4(ambientLighting + diffuseReflection
+ specularReflection, 1.0);
}
ENDCG
}
Pass {
Tags { "LightMode" = "ForwardAdd" }
// pass for additional light sources
Blend One One // additive blending
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
uniform float4 _LightColor0;
// color of light source (from "Lighting.cginc")
// User-specified properties
uniform float4 _Color;
uniform float4 _SpecColor;
uniform float _AlphaX;
uniform float _AlphaY;
struct vertexInput {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};
struct vertexOutput {
float4 pos : SV_POSITION;
float4 posWorld : TEXCOORD0;
// position of the vertex (and fragment) in world space
float3 viewDir : TEXCOORD1;
// view direction in world space
float3 normalDir : TEXCOORD2;
// surface normal vector in world space
float3 tangentDir : TEXCOORD3;
// brush direction in world space
};
vertexOutput vert(vertexInput input)
{
vertexOutput output;
float4x4 modelMatrix = _Object2World;
float4x4 modelMatrixInverse = _World2Object;
output.posWorld = mul(modelMatrix, input.vertex);
output.viewDir = normalize(_WorldSpaceCameraPos
- output.posWorld.xyz);
output.normalDir = normalize(
mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
output.tangentDir = normalize(
mul(modelMatrix, float4(input.tangent.xyz, 0.0)).xyz);
output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
return output;
}
float4 frag(vertexOutput input) : COLOR
{
float3 lightDirection;
float attenuation;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
attenuation = 1.0; // no attenuation
lightDirection = normalize(_WorldSpaceLightPos0.xyz);
}
else // point or spot light
{
float3 vertexToLightSource =
_WorldSpaceLightPos0.xyz - input.posWorld.xyz;
float distance = length(vertexToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(vertexToLightSource);
}
float3 halfwayVector =
normalize(lightDirection + input.viewDir);
float3 binormalDirection =
cross(input.normalDir, input.tangentDir);
float dotLN = dot(lightDirection, input.normalDir);
// compute this dot product only once
float3 diffuseReflection =
attenuation * _LightColor0.rgb * _Color.rgb
* max(0.0, dotLN);
float3 specularReflection;
if (dotLN < 0.0) // light source on the wrong side?
{
specularReflection = float3(0.0, 0.0, 0.0);
// no specular reflection
}
else // light source on the right side
{
float dotHN = dot(halfwayVector, input.normalDir);
float dotVN = dot(input.viewDir, input.normalDir);
float dotHTAlphaX =
dot(halfwayVector, input.tangentDir) / _AlphaX;
float dotHBAlphaY = dot(halfwayVector,
binormalDirection) / _AlphaY;
specularReflection =
attenuation * _LightColor0.rgb * _SpecColor.rgb
* sqrt(max(0.0, dotLN / dotVN))
* exp(-2.0 * (dotHTAlphaX * dotHTAlphaX
+ dotHBAlphaY * dotHBAlphaY) / (1.0 + dotHN));
}
return float4(diffuseReflection
+ specularReflection, 1.0);
}
ENDCG
}
}
Fallback "Specular"
}
总结
恭喜,你完成了一个相当高级的教程!我们看到了:
- 什么是BRDF(双向反射分布函数)。
- 什么是各向异性反射的Ward BRDF模型。
- 如何实现Ward的BRDF模型。