跳转到内容

Cg 编程/Unity/轮廓增强

来自维基教科书,开放世界开放书籍
半透明的水母。请注意轮廓处的透明度增加。

本教程涵盖了**表面法线向量变换**。它假设你熟悉“透明度”部分中讨论的alpha混合以及“世界空间中的着色”部分中讨论的着色器属性。

本教程的目标是实现左侧照片中可见的效果:半透明物体的轮廓往往比物体其他部分更不透明。这在没有照明的情况下,也增加了三维形状的印象。事实证明,变换后的法线对于获得这种效果至关重要。

表面法线向量(简称:法线)在表面斑块上。

平滑表面的轮廓

[编辑 | 编辑源代码]

在平滑表面情况下,轮廓上表面的点以平行于观察平面的法线向量为特征,因此垂直于观察者方向。在左侧的图形中,图形顶部轮廓处的蓝色法线向量平行于观察平面,而其他法线向量更多地指向观察者(或相机)方向。通过计算观察者方向和法线向量,并测试它们是否(几乎)相互正交,我们因此可以测试一个点是否(几乎)在轮廓上。

更具体地说,如果V是观察者方向的归一化(即长度为 1)向量,而N是归一化的表面法线向量,则当点积为 0 时,这两个向量是正交的:V·N = 0。在实践中,这种情况很少发生。但是,如果点积V·N接近 0,我们可以假设该点接近轮廓。

增加轮廓处的透明度

[编辑 | 编辑源代码]

对于我们的效果,因此应该增加透明度,如果点积V·N接近 0。有各种方法可以增加观察者方向和法线向量之间点积较小时的透明度。以下是一种方法(它实际上有一个物理模型作为其基础,这在该出版物的第 5.1 节中进行了描述)来计算从材料的常规透明度 中增加的透明度

检查像这样的方程式的极端情况总是很有意义。考虑接近轮廓的点的案例:V·N ≈ 0。在这种情况下,常规透明度 将除以一个小的正数。(请注意,GPU 通常会优雅地处理除以零的情况;因此,我们不必担心它。)因此,无论 是多少, 和一个小正数的比率将更大。该 函数将确保生成的透明度 永远不会大于 1。

另一方面,对于远离轮廓的点,我们有V·N ≈ 1。在这种情况下,α' ≈ min(1, α) ≈ α;也就是说,这些点的透明度不会发生太大变化。这正是我们想要的。因此,我们刚刚检查了该方程至少是合理的。

在着色器中实现方程式

[编辑 | 编辑源代码]

为了在着色器中实现像 这样的公式,第一个问题应该是:应该在顶点着色器还是片段着色器中实现?在某些情况下,答案很明显,因为实现需要纹理映射,而纹理映射通常只能在片段着色器中使用。然而,在许多情况下,没有普遍的答案。在顶点着色器中实现往往更快(因为通常顶点的数量比片段少),但图像质量较低(因为法线和其他顶点属性在顶点之间可能会发生突然变化)。因此,如果您最关心的是性能,那么在顶点着色器中实现可能是一个更好的选择。另一方面,如果您最关心的是图像质量,那么在片段着色器中实现可能是一个更好的选择。在每顶点光照(即 Gouraud 着色,在 “镜面高光”部分 中讨论)和每片段光照(即 Phong 着色,在 “平滑镜面高光”部分 中讨论)之间也存在同样的权衡。

下一个问题是:应该在哪个坐标系中实现公式?(请参阅 “顶点变换”部分,以了解标准坐标系的描述。)同样,也没有普遍的答案。但是,在世界坐标系中实现通常是 Unity 中一个不错的选择,因为许多统一变量是在世界坐标系中指定的。(在其他环境中,在视图坐标系中实现非常常见。)

在实现公式之前,最后一个问题是:我们从哪里获取公式的参数?常规的不透明度 是由着色器属性指定的(在一个 RGBA 颜色中)(请参阅 “在世界空间中着色”部分)。法线向量 normal 是一个标准的顶点输入参数(请参阅 “着色器的调试”部分)。指向观察者的方向可以在顶点着色器中计算出来,作为从世界空间中的顶点位置到世界空间中的相机位置 _WorldSpaceCameraPos 的向量,该向量由 Unity 提供。

因此,我们只需要在实现公式之前将顶点位置和法线向量变换到世界空间。从物体空间到世界空间的变换矩阵 unity_ObjectToWorld 及其逆矩阵 unity_WorldToObject 由 Unity 提供,如 “在世界空间中着色”部分 所述。将变换矩阵应用于点和法线向量将在 “应用矩阵变换”部分 中详细讨论。基本结果是,点和方向只需通过将它们与变换矩阵相乘即可进行变换,例如,将 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);

现在我们拥有了编写着色器所需的所有部分。

着色器代码

[edit | edit source]
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
      }
   }
}

newOpacity 的赋值是对该公式的几乎直接翻译

请注意,我们在顶点着色器中对顶点输出参数 output.normaloutput.viewDir 进行了归一化(因为我们希望在方向之间进行插值,而不会对任何方向赋予更多或更少的权重),并在片段着色器开始时进行归一化(因为插值会在一定程度上扭曲我们的归一化)。但是,在许多情况下,在顶点着色器中对 output.normal 进行归一化并不是必需的。类似地,在大多数情况下,在片段着色器中对 output.viewDir 进行归一化也是没有必要的。

更多艺术控制

[edit | edit source]

虽然描述的轮廓增强基于物理模型,但它缺乏艺术控制;也就是说,CG 艺术家无法轻松地创建比物理模型建议的更细或更粗的轮廓。为了允许更多艺术控制,您可以引入另一个(正)浮点数字属性,并在使用上面的公式之前,将点积 |V·N| 乘以该数字的幂(使用内置的 Cg 函数 pow(float x, float y))。这将允许 CG 艺术家独立于基本颜色的不透明度创建更细或更粗的轮廓。

总结

[edit | edit source]

恭喜,您已经完成了本教程。我们讨论了

  • 如何找到平滑表面的轮廓(使用法线向量和视角方向的点积)。
  • 如何在这些轮廓处增强不透明度。
  • 如何在着色器中实现公式。
  • 如何将点和法线向量从物体空间变换到世界空间(对法线向量使用转置逆模型矩阵)。
  • 如何计算视角方向(作为从相机位置到顶点位置的差)。
  • 如何插值归一化方向(即归一化两次:在顶点着色器和片段着色器中)。
  • 如何提供对轮廓粗细的更多艺术控制。

进一步阅读

[edit | edit source]

如果您仍然想了解更多

  • 关于物体空间和世界空间,您应该阅读 “顶点变换”部分 中的描述。
  • 关于如何将变换矩阵应用于点、方向和法线向量,您应该阅读 “应用矩阵变换”部分
  • 关于渲染透明物体的基础知识,您应该阅读 “透明度”部分
  • 关于 Unity 提供的统一变量和着色器属性,您应该阅读 “在世界空间中着色”部分
  • 关于轮廓增强的数学原理,您可以阅读 Martin Kraus 在 2005 年 IEEE 可视化大会上发表的论文“Scale-Invariant Volume Rendering”的第 5.1 节,该论文可在 网上 获取。

< Cg 编程/Unity

除非另有说明,否则本页面上的所有示例源代码均授予公有领域。
华夏公益教科书