Cg 编程/Unity/拉丝金属
本教程涵盖各向异性镜面高光。
它是关于光照的几个教程之一,超出了 Phong 反射模型。然而,它基于 Phong 反射模型的光照,如“镜面高光”部分(用于每个顶点光照)和“平滑镜面高光”部分(用于每个像素光照)中所述。如果您还没有阅读这些教程,您应该先阅读它们。
虽然 Phong 反射模型对于纸张、塑料和一些其他具有各向同性反射(即圆形高光)的材料来说相当不错,但本教程特别关注具有各向异性反射(即非圆形高光)的材料,例如左侧照片中的拉丝铝。
Gregory Ward 在其作品“测量和建模各向异性反射”中发表了一个合适的各向异性反射模型,计算机图形学(SIGGRAPH ’92 论文集),第 265-272 页,1992 年 7 月。(该论文的副本可在网上获得。)该模型根据 BRDF(双向反射分布函数)来描述反射,它是一个四维函数,描述了来自任何方向的光线是如何反射到任何其他方向的。他的 BRDF 模型包含两项:一个漫反射项,它是,以及一个更复杂的镜面反射项。
让我们先看看漫反射项:只是一个常数(大约 3.14159),而指定了漫反射率。原则上,每个波长都需要一个反射率;然而,通常为三个颜色分量(红色、绿色和蓝色)中的每一个指定一个反射率。如果我们包括常数,仅仅代表漫反射材质颜色,我们第一次在“漫反射”部分中看到它,但它也出现在 Phong 反射模型中(参见“镜面高光”部分)。您可能想知道为什么因子 max(0, L·N) 没有出现在 BRDF 中。答案是 BRDF 的定义方式是不包含这个因子(因为它实际上不是材料的属性),但在进行任何光照计算时,应该将其与 BRDF 相乘。
因此,为了对不透明材料实现给定的 BRDF,我们必须将 BRDF 的所有项乘以 max(0, L·N) 并且 - 除非我们想实现物理上正确的光照 - 我们可以将任何常数因子替换为用户指定的颜色,这通常比物理量更容易控制。
对于他的 BRDF 模型的镜面项,Ward 在他的论文的等式 5b 中给出了一个近似值。我稍微修改了它,使其使用归一化的表面法线向量 N,归一化的观察者方向 V,归一化的光源方向 L 以及归一化的中间向量 H,它是 (V + L) / |V + L|。使用这些向量,Ward 对镜面项的近似值变为
这里, 是镜面反射率,它描述了镜面高光的颜色和强度; 和 是描述高光形状和大小的材料常数。由于所有这些变量都是材料常数,我们可以将它们组合成一个常数 。因此,我们得到一个稍短的版本
请记住,在着色器中实现它时,我们仍然必须将此 BRDF 项乘以 L·N,如果 L·N 小于 0,则将其设置为 0。此外,如果 V·N 小于 0,即我们从“错误”的一侧查看表面,它也应该为 0。
还有两个向量尚未描述:T 和 B。T 是表面上的刷子方向,而 B 与 T 正交,但也在表面上。Unity 为我们提供了一个表面上的切线向量作为顶点属性(见 “着色器调试”部分),我们将把它用作向量 T。计算 N 和 T 的叉积会生成一个向量 B,它与 N 和 T 正交,正如它应该的那样。
我们基于 “平滑镜面高光”章节 中的逐像素光照着色器实现。我们还需要一个顶点输出参数 `tangentDir` 来表示切线向量 **T**(即刷痕方向),并且我们在顶点着色器中计算 `viewDir` 以节省片元着色器中的指令。在片元着色器中,我们计算另外两个方向:`halfwayVector` 表示半程向量 **H**,`binormalDirection` 表示副法线向量 **B**。属性包括 `_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))` 这个项,它是从 乘以 得到的。这确保了所有值都大于 0。
完整的着色器代码
[edit | edit source]完整的着色器代码仅定义了适当的属性,并添加了另一个用于切线的顶点输入参数。此外,它需要一个带有累加混合但没有环境光的第二通道,用于处理额外的光源。
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 = unity_ObjectToWorld;
float4x4 modelMatrixInverse = unity_WorldToObject;
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 = unity_ObjectToWorld;
float4x4 modelMatrixInverse = unity_WorldToObject;
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"
}
总结
[edit | edit source]恭喜你完成了一个相当高级的教程!我们已经了解了
- 什么是 BRDF(双向反射分布函数)。
- 什么是 Ward 用于各向异性反射的 BRDF 模型。
- 如何实现 Ward 的 BRDF 模型。
进一步阅读
[edit | edit source]如果你想了解更多关于
- 使用 Phong 反射模型进行光照,你应该阅读 “镜面高光”章节。
- 逐像素光照(即 Phong 着色),你应该阅读 “平滑镜面高光”章节。
- Ward 的 BRDF 模型,你应该阅读他的文章“测量和建模各向异性反射”,计算机图形学(SIGGRAPH ’92 论文集),第 265-272 页,1992 年 7 月。(该论文的副本可在 网上 获得。)或者你可以阅读 Randi Rost 等人 2009 年由 Addison-Wesley 出版的三版“OpenGL 着色语言”的第 14.3 节,或者阅读 Wolfgang Engel、Jack Hoxley、Ralf Kornmann、Niko Suni 和 Jason Zink 的“编程顶点、几何和像素着色器”(第二版,2008 年)照明章节的第 8 节(可在 网上 获得)。