本教程介绍了曲面法线向量的变换。
本教程的目的是实现一种效果:半透明对象的轮廓比该对象的其余部分更不透明。即使没有照明,这也增加了三维形状的印象。事实证明,转换法线对于获得此效果至关重要。
光滑表面的轮廓
在光滑表面的情况下,轮廓表面上的点可通过法线矢量来表示特征,该法线矢量平行于观察平面,因此正交于观察者的方向。在左图中,图顶部轮廓上的蓝色法线矢量平行于观察平面,而其他法线矢量指向观察者(或相机)的方向更多。通过计算指向观察者的方向和法线向量并测试他们是否(几乎)彼此正交,因此我们可以测试点是否(几乎)在轮廓上。
更具体的说,如果V
是观察者的归一化方向,N
是归一化的表面法线向量,如果点积结果为0:V·N=0,那么者两个向量是正交的。在实际中,这种情况是很少的。但是如果点积V·N结果接近0,我们可以假设该点接近轮廓。
增加轮廓上的不透明度
为了达到我们想要的效果,因此,当点积V·N结果接近0,我们应该增加不透明度α
。有多种方法可以增加观察者方向和法线矢量点积很小的不透明度。这是其中之一:从material的常规不透明度的alpha中增加不透明度alpha‘:
*
检查这样的方程的极端情况是有意义的。考虑一个接近轮廓的点的情况 V·N ≈ 0.在这种情况下,常规不透明度α将被一个小的正数除。(请注意,CPU通常会非常好的处理被0除的情况,因此,我们不必担心这一点)。因此,无论α的值是多少,α与小的正数的比例都会更大。取最小值函数min将使得所产生的不透明度不会大于1。
另一方面,对于远离轮廓的点,我们有V·N≈1。也就是说这些点的透明度不会发生太大的变化。这正是我们想要的。因此,我们刚刚检查了该方程至少是合理的。
在shader中实现方程
为了在着色器中实现类似α的方程,第一个问题应该是:应该在顶点着色器中实现还是在片元着色器中实现?在某些情况下,答案很明确,因为该实现需要纹理映射,而纹理映射通常仅在片段着色器中可用。但是,在许多情况下,没有通用的答案。顶点着色器的实现往往更快(因为顶点通常少于片段),但图像质量较低(因为法向矢量和其他顶点属性可能在顶点之间突然改变)。因此,如果您最关心性能,则在顶点着色器中实现可能是更好的选择。另一方面,如果您最关心图像质量,则在片段着色器中实施可能是更好的选择。
下一个问题是:方程应在哪个坐标系中实现?同样,也没有通用答案。 但是,在世界坐标中实现通常是Unity中的一个不错的选择,因为在世界坐标中指定了许多统一变量。
在实现方程式之前的最后一个问题是:我们从哪里得到方程式的参数?常规的不透明度alpha是通过一个shader的属性指定的。法线向量是标准顶点输入参数。观察者方向可以在顶点着色器中计算,可以看作是该顶点在世界坐标中的位置到世界空间中的相机位置(即_WorldSpaceCameraPos
)的方向,该向量又Unity提供。
因此,在执行方程式之前,我们只需要将顶点位置和法线向量转换到世界空间即可。Unity提供了从对象空间到世界空间的变换矩阵unity_ObjectToWorld
及其逆unity_WorldToObject
。基本结果是通过将点和方向乘以变换矩阵即可对其进行变换。 将modelMatrix
设置为unity_ObjectToWorld
:
output.viewDir = normalize(_WorldSpaceCameraPos - mul(modelMatrix, input.vertex).xyz);
另外,通过将法线向量与转置的逆变换矩阵相乘来变换法向量。由于Unity为我们提供了逆变换矩阵(unity_WorldToObject),因此更好的选择是将法线向量从左侧乘以逆矩阵,这等效于将其从右侧乘以转置后的逆矩阵:
output.normal = normalize(mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
现在,我们拥有编写着色器所需的所有内容。
shader 代码
Shader "Cg silhouette enhancement" {
Properties {
_Color ("Color", Color) = (1, 1, 1, 0.5)
// user-specified RGBA color including opacity
}
SubShader {
Tags { "Queue" = "Transparent" }
// draw after all opaque geometry has been drawn
Pass {
ZWrite Off // don't occlude other objects
Blend SrcAlpha OneMinusSrcAlpha // standard alpha blending
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
uniform float4 _Color; // define shader property for shaders
struct vertexInput {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct vertexOutput {
float4 pos : SV_POSITION;
float3 normal : TEXCOORD0;
float3 viewDir : TEXCOORD1;
};
vertexOutput vert(vertexInput input)
{
vertexOutput output;
float4x4 modelMatrix = unity_ObjectToWorld;
float4x4 modelMatrixInverse = unity_WorldToObject;
output.normal = normalize(
mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
output.viewDir = normalize(_WorldSpaceCameraPos
- mul(modelMatrix, input.vertex).xyz);
output.pos = UnityObjectToClipPos(input.vertex);
return output;
}
float4 frag(vertexOutput input) : COLOR
{
float3 normalDirection = normalize(input.normal);
float3 viewDirection = normalize(input.viewDir);
float newOpacity = min(1.0, _Color.a
/ abs(dot(viewDirection, normalDirection)));
return float4(_Color.rgb, newOpacity);
}
ENDCG
}
}
}
请注意,我们在顶点着色器中(因为我们希望在方向之间进行插值,而对每个方向都没有施加更多或更少的权重)和片元着色器的开始处(因为插值可以在一定程度上扭曲我们的归一化)归一化了顶点输出参数output.normal
和output.viewDir
。但是,在许多情况下,不需要在顶点着色器中对output.normal
进行归一化。 同样,在大多数情况下,片段着色器中的output.viewDir
归一化也是不必要的。
更多的艺术控制
尽管所描述的轮廓增强是基于物理模型的,但是却缺乏艺术上的控制。即,CG艺术家无法轻易创建比物理模型建议的更细或更粗的轮廓。为了进行更多的艺术控制,您可以引入另一个(正)浮点数属性,并采用点积| V·N |。 在上面的公式中使用它之前,请使用此数字的幂(使用内置的Cg函数pow(float x,float y)
)。这将使CG艺术家可以独立于基色的不透明度创建更薄或更厚的轮廓。
总结
本节中,我们讨论了:
- 如何找到光滑表面的轮廓(使用法线向量和视图方向的点积)
- 如何增强这些轮廓的不透明度。( α ′ = m i n ( 1 , α ∣ V ⋅ N ∣ ) α' = min(1,\frac{\alpha }{|V\cdot N|}) α′=min(1,∣V⋅N∣α))
- 如何在着色器中实现方程式。
- 如何将点和法线向量从对象空间转换为世界空间(对法向向量使用转置逆模型矩阵)。
- 如何计算观察方向(从摄影机位置到顶点位置的差)
- 如何插值归一化方向(即两次归一化:在顶点着色器和片段着色器中)。
- 如何对轮廓的厚度提供更多的艺术控制。