GLSL 编程/Unity/轮廓增强
本教程涵盖了表面法线向量变换。它假设您熟悉“透明度”一节中讨论的 Alpha 混合,以及“世界空间中的着色”一节中讨论的着色器属性。
本教程的目标是实现左边照片中可见的效果:半透明物体的轮廓往往比物体其他部分更不透明。这即使没有光照,也会增加对三维形状的印象。事实证明,变换后的法线对于获得这种效果至关重要。
在光滑表面的情况下,轮廓上表面的点以平行于视平面的法线向量为特征,因此与观察者的方向正交。在左边的图中,图顶部的轮廓处的蓝色法线向量平行于视平面,而其他法线向量更多地指向观察者(或摄像机)方向。通过计算观察者方向和法线向量,并测试它们是否(几乎)相互正交,我们可以因此测试一个点是否(几乎)在轮廓上。
更具体地说,如果V是归一化(即长度为 1)的观察者方向,而N是归一化的表面法线向量,那么如果点积为 0,这两个向量就正交:V·N = 0。在实践中,这种情况很少发生。但是,如果点积V·N接近 0,我们可以假设该点接近轮廓。
因此,对于我们的效果,我们应该增加透明度,如果点积V·N接近 0。对于观察者方向和法线向量之间的较小点积,有几种方法可以增加透明度。以下是一种方法(它实际上背后有一个物理模型,在这篇出版物的第 5.1 节中进行了描述),用于从材料的常规透明度 计算增加的透明度
检查像这样的公式的极端情况总是很有意义的。考虑接近轮廓的点的情况:V·N ≈ 0。在这种情况下,常规透明度 将被一个小的正数除。(请注意,GLSL 保证以优雅的方式处理除以零的情况;因此,我们不必担心它。)因此,无论 是什么, 和一个小的正数的比率将更大。 函数将确保得到的透明度 永远不会大于 1。
另一方面,对于远离轮廓的点,我们有V·N ≈ 1。在这种情况下,α' ≈ min(1, α) ≈ α;也就是说,这些点的透明度不会发生太大变化。这正是我们想要的。因此,我们刚刚验证了该公式至少是合理的。
为了在着色器中实现类似于的方程,第一个问题应该是:应该在顶点着色器还是片段着色器中实现它?在某些情况下,答案很明确,因为实现需要纹理映射,而纹理映射通常只在片段着色器中可用。但是,在许多情况下,没有普遍的答案。在顶点着色器中实现往往更快(因为通常顶点的数量少于片段的数量),但图像质量较低(因为法线向量和其他顶点属性在顶点之间可能会发生突然变化)。因此,如果您最关心性能,那么在顶点着色器中实现可能是一个更好的选择。另一方面,如果您最关心图像质量,那么在像素着色器中实现可能是一个更好的选择。在每个顶点照明(即 Gouraud 着色,将在“镜面高光”部分中讨论)和每个片段照明(即 Phong 着色,将在“平滑镜面高光”部分中讨论)之间存在着同样的权衡。
下一个问题是:应该在哪个坐标系中实现这个方程?(有关标准坐标系的描述,请参阅“顶点变换”部分。)同样,也没有普遍的答案。但是,在 Unity 中,在世界坐标系中实现通常是一个不错的选择,因为许多统一变量是在世界坐标系中指定的。(在其他环境中,在视图坐标系中实现非常常见。)
在实现方程之前,最后一个问题是:我们从哪里获得方程的参数?常规的不透明度是由着色器属性指定的(在 RGBA 颜色中,请参阅“在世界空间中着色”部分)。法线向量 gl_Normal
是标准顶点属性(请参阅“着色器的调试”部分)。指向观察者的方向可以在顶点着色器中计算为从世界空间中的顶点位置到世界空间中的相机位置 _WorldSpaceCameraPos
的向量,它由 Unity 提供。
因此,我们只需要将顶点位置和法线向量变换到世界空间,然后才能实现方程。从物体空间到世界空间的变换矩阵 _Object2World
及其逆矩阵 _World2Object
由 Unity 提供,如“在世界空间中着色”部分中所述。有关将变换矩阵应用于点和法线向量的详细讨论,请参阅“应用矩阵变换”部分。基本结果是点和方向只需乘以变换矩阵即可进行变换,例如
uniform mat4 _Object2World;
...
vec4 positionInWorldSpace = _Object2World * gl_Vertex;
vec3 viewDirection = _WorldSpaceCameraPos - vec3(positionInWorldSpace);
另一方面,法线向量通过乘以转置的逆变换矩阵进行变换。由于 Unity 为我们提供了逆变换矩阵(除了右下角元素之外,它等于 _World2Object * unity_Scale.w
),因此更好的选择是从左侧将法线向量乘以逆矩阵,这等效于从右侧将其乘以转置的逆矩阵,如“应用矩阵变换”部分中所述。
uniform mat4 _World2Object; // the inverse of _Object2World
// (after multiplication with unity_Scale.w)
uniform vec4 unity_Scale;
...
vec3 normalInWorldSpace = vec3(vec4(gl_Normal, 0.0) * _World2Object
* unity_Scale.w); // corresponds to a multiplication of the
// transposed inverse of _Object2World with gl_Normal
请注意,不正确的右下角矩阵元素没有问题,因为它始终乘以 0。此外,如果缩放不重要,则乘以 unity_Scale.w
是不必要的;例如,如果我们对所有变换后的向量进行归一化。
现在,我们拥有了编写着色器所需的所有部分。
着色器代码
[edit | edit source]Shader "GLSL 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
GLSLPROGRAM
uniform vec4 _Color; // define shader property for shaders
// The following built-in uniforms are also defined in
// "UnityCG.glslinc", which could be #included
uniform vec3 _WorldSpaceCameraPos;
// camera position in world space
uniform mat4 _Object2World; // model matrix
uniform mat4 _World2Object; // inverse model matrix
// (apart from the factor unity_Scale.w)
varying vec3 varyingNormalDirection;
// normalized surface normal vector
varying vec3 varyingViewDirection;
// normalized view direction
#ifdef VERTEX
void main()
{
mat4 modelMatrix = _Object2World;
mat4 modelMatrixInverse = _World2Object;
// multiplication with unity_Scale.w is unnecessary
// because we normalize transformed vectors
varyingNormalDirection = normalize(
vec3(vec4(gl_Normal, 0.0) * modelMatrixInverse));
varyingViewDirection = normalize(_WorldSpaceCameraPos
- vec3(modelMatrix * gl_Vertex));
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
void main()
{
vec3 normalDirection = normalize(varyingNormalDirection);
vec3 viewDirection = normalize(varyingViewDirection);
float newOpacity = min(1.0, _Color.a
/ abs(dot(viewDirection, normalDirection)));
gl_FragColor = vec4(vec3(_Color), newOpacity);
}
#endif
ENDGLSL
}
}
}
对 newOpacity
的赋值几乎是方程的字面翻译
请注意,我们在顶点着色器中对 varyings varyingNormalDirection
和 varyingViewDirection
进行归一化(因为我们希望在方向之间进行插值,而不会对任何方向赋予更大的权重或更小的权重),并在片段着色器的开头进行归一化(因为插值可能会在一定程度上扭曲我们的归一化)。但是,在许多情况下,顶点着色器中的 varyingNormalDirection
的归一化是不必要的。类似地,在大多数情况下,片段着色器中的 varyingViewDirection
的归一化是不必要的。
更多艺术控制
[edit | edit source]虽然描述的轮廓增强基于物理模型,但它缺乏艺术控制;也就是说,CG 艺术家不能轻松地创建比物理模型建议的更薄或更厚的轮廓。为了允许更多艺术控制,您可以引入另一个(正)浮点数属性,并将点积 |V·N| 提高到这个数字的幂(使用内置的 GLSL 函数 pow(float x, float y)
),然后再在上述方程中使用它。这将允许 CG 艺术家独立于基本颜色的不透明度创建更薄或更厚的轮廓。
总结
[edit | edit source]恭喜您完成了本教程。我们已经讨论了
- 如何找到平滑表面的轮廓(使用法线向量和视线方向的点积)。
- 如何增强这些轮廓处的透明度。
- 如何在着色器中实现方程。
- 如何将点和法线向量从物体空间变换到世界空间(对法线向量使用转置的逆模型矩阵)。
- 如何计算视线方向(作为从相机位置到顶点位置的差)。
- 如何对归一化的方向进行插值(即进行两次归一化:在顶点着色器和片段着色器中)。
- 如何提供对轮廓厚度进行更多艺术控制。
进一步阅读
[edit | edit source]如果您还想了解更多信息
- 关于物体空间和世界空间,您应该阅读“顶点变换”部分中的描述。
- 关于如何将变换矩阵应用于点、方向和法线向量,您应该阅读“应用矩阵变换”部分。
- 关于渲染透明物体的基本知识,您应该阅读“透明度”部分。
- 关于 Unity 提供的统一变量和着色器属性,您应该阅读“在世界空间中着色”部分。
- 关于轮廓增强的数学原理,您可以阅读 Martin Kraus 在 2005 年 IEEE 可视化大会上发表的论文“尺度不变体积渲染”的第 5.1 节,该论文可在 网上 获取。