跳转到内容

GLSL 编程/Blender/轮廓增强

来自 Wikibooks,开放世界中的开放书籍
一只半透明的水母。注意轮廓处的透明度增加了。

本教程涵盖了表面法线向量的变换。它假设你熟悉透明度教程中讨论的alpha混合以及视图空间着色教程中讨论的uniform。

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

表面法线向量(简称法线)在一个表面块上。

平滑表面的轮廓

[编辑 | 编辑源代码]

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

更准确地说,如果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 着色)之间也存在同样的权衡。

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

在实现方程式之前,最后一个问题是:我们从哪里获取方程式的参数?常规的不透明度 由游戏属性指定(在 RGBA 颜色中)(请参见 关于在视图空间中着色的教程)。法线向量 gl_Normal 是标准的顶点属性(请参见 关于着色器调试的教程)。朝向观察者的方向可以在顶点着色器中计算为从视图空间中的顶点位置到视图空间中的摄像机位置的向量,而摄像机位置始终是原点,即 ,如 “顶点变换” 中所述。换句话说,我们应该计算 减去视图空间中的位置,这仅仅是视图空间中的负位置(除了第四个坐标,对于方向,它应该为 0)。

因此,我们只需要在实现方程式之前将顶点位置和法线向量变换到视图空间。从对象空间到视图空间的变换矩阵 gl_ModelViewMatrix 以及它的逆矩阵 gl_ModelViewMatrixInverse 和逆矩阵的转置 gl_ModelViewMatrixInverseTranspose 都由 Blender 提供,如 关于在视图空间中着色的教程 中所述。将变换矩阵应用于点和法线向量的过程在 “应用矩阵变换” 中进行了详细讨论。基本结果是点和方向只是通过将它们乘以变换矩阵来变换,例如

vec4 positionInViewSpace = gl_ModelViewMatrix * gl_Vertex;
vec3 viewDirection = -vec3(positionInViewSpace); // == vec3(0.0, 0.0, 0.0) - vec3(positionInViewSpace)

另一方面,法线向量通过将它们乘以转置的逆变换矩阵来变换。由于 Blender 为我们提供了转置的逆变换矩阵 gl_ModelViewMatrixInverseTranspose,因此可以用来变换法线。另一种方法是从左边将法线向量乘以逆矩阵 gl_ModelViewMatrixInverse,这等效于将其从右边乘以转置的逆矩阵,如 “应用矩阵变换” 中所述。但是,最好的方法是使用 3×3 矩阵 gl_NormalMatrix,它是模型视图矩阵的转置的逆 3×3 矩阵

vec3 normalInViewSpace = gl_NormalMatrix * gl_Normal;

现在我们已经拥有了编写着色器所需的所有组件。

着色器代码

[edit | edit source]

片段着色器使用 alpha 混合,如 关于透明度的教程 中所述

import bge

cont = bge.logic.getCurrentController()

VertexShader = """
         varying vec3 varyingNormalDirection; 
            // normalized surface normal vector
         varying vec3 varyingViewDirection; 
            // normalized view direction 
                  
         void main()
         {				
            varyingNormalDirection = 
               normalize(gl_NormalMatrix * gl_Normal);
            varyingViewDirection = 
               -normalize(vec3(gl_ModelViewMatrix * gl_Vertex));

            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }
"""

FragmentShader = """
         varying vec3 varyingNormalDirection; 
            // normalized surface normal vector
         varying vec3 varyingViewDirection; 
            // normalized view direction 
                  
         const vec4 color = vec4(1.0, 1.0, 1.0, 0.3);

         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);
         }
"""

mesh = cont.owner.meshes[0]
for mat in mesh.materials:
    shader = mat.getShader()
    if shader != None:
        if not shader.isValid():
            shader.setSource(VertexShader, FragmentShader, 1)
            mat.setBlending(bge.logic.BL_SRC_ALPHA, 
                            bge.logic.BL_ONE_MINUS_SRC_ALPHA)

newOpacity 的赋值是对方程式的几乎逐字翻译

请注意,我们在顶点着色器中对 varyings varyingNormalDirectionvaryingViewDirection 进行了归一化(因为我们希望在方向之间进行插值,而不会对任何方向赋予更多或更少的权重),并在片段着色器的开头进行归一化(因为插值可能会在一定程度上扭曲我们的归一化)。但是,在许多情况下,顶点着色器中 varyingNormalDirection 的归一化是不必要的。类似地,在大多数情况下,片段着色器中 varyingViewDirection 的归一化也是不必要的。

更多艺术控制

[edit | edit source]

片段着色器使用一个常量 color 来指定表面的 RGBA 颜色。如果将该常量替换为四个游戏属性(请参见 关于在视图空间中着色的教程),那么 CG 艺术家更容易修改该颜色。

此外,这种轮廓增强缺乏艺术控制;即,CG 艺术家不能轻易地创建比物理模型暗示的更薄或更厚的轮廓。为了允许更多的艺术控制,您可以引入另一个(正的)浮点型数字属性,并在使用上面的方程式之前,将点积 |V·N| 提高到该数字的幂。这将允许 CG 艺术家独立于基本颜色的不透明度来创建更薄或更厚的轮廓。

总结

[edit | edit source]

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

  • 如何找到光滑表面的轮廓(使用法线向量和观察方向的点积)。
  • 如何在这些轮廓处增强不透明度。
  • 如何在着色器中实现方程式。
  • 如何将点和法线向量从对象空间变换到视图空间。
  • 如何计算观察方向(作为从摄像机位置到顶点位置的差)。
  • 如何插值归一化方向(即进行两次归一化:在顶点着色器和片段着色器中)。
  • 如何为轮廓的厚度提供更多艺术控制。

进一步阅读

[edit | edit source]

如果您想了解更多信息

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


< GLSL Programming/Blender

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