跳转到内容

GLSL 编程/Unity/凹凸表面的投影

来自 Wikibooks,开放世界中的开放书籍
英格兰的一面干石墙。注意一些石头是如何突出墙壁的。

本教程介绍(单步)视差贴图

它扩展并基于“凹凸表面的光照”部分

改进法线贴图

[编辑 | 编辑源代码]

“凹凸表面的光照”部分中介绍的法线贴图技术只改变平面的光照效果,以创造凹凸和凹陷的错觉。如果一个人直视表面(即朝向表面法线向量方向),这种方法效果很好。但是,如果一个人从其他角度观察表面(如左侧图像所示),凹凸也应该从表面突出,而凹陷应该凹进表面。当然,可以通过几何建模凹凸和凹陷来实现这一点;但是,这将需要处理更多的顶点。另一方面,单步视差贴图是一种类似于法线贴图的非常高效的技术,它不需要额外的三角形,但仍然可以将虚拟凹凸移动几个像素,使它们从平面表面突出。然而,这种技术仅限于高度较小的凹凸和凹陷,需要一些微调才能获得最佳效果。

视差贴图中的向量和距离:视向量 V、表面法线向量 N、高度图高度 h、视线与高度为 h 的表面交点偏移量 o。

视差贴图解释

[编辑 | 编辑源代码]

视差贴图是由 Tomomichi Kaneko 等人于 2001 年在其论文“视差贴图的详细形状表示”(ICAT 2001)中提出的。基本思想是对用于表面纹理的纹理坐标(尤其是法线贴图)进行偏移。如果这种纹理坐标偏移计算得当,则可以移动纹理的一部分(例如凹凸),就好像它们从表面突出一样。

左侧的插图显示了指向观察者的视向量 V 和在片段着色器中光栅化的表面点的表面法线向量 N。视差贴图分三步进行

  • 在光栅化点查找高度图中的高度 ,该高度图由插图底部直线上方的波浪线表示。
  • 计算方向为 V 的视线与高度为 的表面(平行于渲染表面)的交点。距离 是光栅化表面点在 N 方向上移动 后的点与该交点的距离。如果将这两个点投影到渲染的表面上,则 也是光栅化点与表面上一个新点的距离(插图中用十字标记)。如果表面被高度图位移,这个新的表面点更接近于方向为 V 的视线实际上可见的点。
  • 将偏移量 转换为纹理坐标空间,以便为所有后续纹理查找计算纹理坐标偏移量。

为了计算 ,我们需要光栅化点的高度图高度 ,这在示例中通过纹理属性 _ParallaxMap 的 A 分量的纹理查找来实现,它应该是一个灰度图像,如“凹凸表面的光照”部分中所述,表示高度。我们还需要以法线向量( 轴)、切线向量( 轴)和副法线向量( 轴)形成的局部表面坐标系中的视方向 V,这也在“凹凸表面的光照”部分中介绍过。为此,我们计算从局部表面坐标到物体空间的变换,方法是:

其中 TBN 在物体坐标系中给出。(“凹凸表面的光照”部分 中我们有一个类似的矩阵,但向量在世界坐标系中。)

我们在物体空间中计算视角方向 V(作为光栅化位置和从世界空间变换到物体空间的相机位置之间的差),然后使用矩阵 将其变换到局部表面空间,该矩阵可以计算为

这是可能的,因为 TBN 是正交且归一化的。(实际上,情况稍微复杂一些,因为我们不会归一化这些向量,而是使用它们的长度进行另一个变换;见下文。)因此,为了将 V 从物体空间变换到局部表面空间,我们必须用转置矩阵 进行乘法。在 GLSL 中,这是通过将向量从左侧乘以矩阵 来实现的。

一旦我们在局部表面坐标系中得到 V,其中 轴指向法向量 N 的方向,我们就可以使用相似三角形(与插图比较)计算偏移量 (在 方向)和 (在 方向)。

  and  .

因此

  and  .

请注意,这里不需要对 **V** 进行归一化,因为我们只使用其分量的比例,而比例不受归一化影响。

最后,我们需要将 转换到纹理空间。如果没有 Unity 的帮助,这将非常困难:Tangent 属性实际上已经适当缩放,并且具有第四个分量 Tangent.w 用于缩放副法线向量,以使观察方向 **V** 的转换能够适当缩放 ,以便在纹理坐标空间中获得 ,而无需进行进一步的计算。

实现

[edit | edit source]

实现代码与 “凹凸表面照明”部分 的代码共享大部分内容。特别是,使用相同的副法线向量缩放与 Tangent 属性的第四个分量,以便将偏移量从局部表面空间到纹理空间的映射考虑在内。

           vec3 binormal = cross(gl_Normal, vec3(Tangent)) * Tangent.w;

在顶点着色器中,我们需要为局部表面坐标系中的观察向量 **V** 添加一个变化量(考虑到映射到纹理空间的轴缩放)。这个变化量称为 viewDirInScaledSurfaceCoords。它通过从左侧乘以矩阵 localSurface2ScaledObject)来计算观察向量,如上所述。

            vec3 viewDirInObjectCoords = vec3(
               modelMatrixInverse * vec4(_WorldSpaceCameraPos, 1.0) 
               - gl_Vertex);
            mat3 localSurface2ScaledObject = 
               mat3(vec3(Tangent), binormal, gl_Normal); 
               // vectors are orthogonal
            viewDirInScaledSurfaceCoords = 
               viewDirInObjectCoords * localSurface2ScaledObject; 
               // we multiply with the transpose to multiply with 
               // the "inverse" (apart from the scaling)

顶点着色器的其余部分与法线贴图相同,请参阅 “凹凸表面照明”部分

在片段着色器中,我们首先查询高度图以获取栅格化点的深度。该深度由纹理 _ParallaxMap 的 A 分量指定。0 到 1 之间的数值通过着色器属性 _Parallax 转换为 -_Parallax/2 到 +_Parallax 的范围,以便提供一些用户对效果强度的控制(并与备用着色器兼容)。

           float height = _Parallax 
               * (-0.5 + texture2D(_ParallaxMap, _ParallaxMap_ST.xy 
               * textureCoordinates.xy + _ParallaxMap_ST.zw).a);

偏移量 然后按照上述方法计算。但是,我们还将每个偏移量限制在一个用户指定的范围 -_MaxTexCoordOffset_MaxTexCoordOffset 之间,以确保偏移量保持在合理的范围内。(如果高度图由更多或更少的具有恒定深度的平坦平台组成,这些平台之间平滑过渡,则 _MaxTexCoordOffset 应该小于这些过渡区域的厚度;否则采样点可能会位于具有不同深度的另一个平台上,这意味着交点的近似值是任意不好的)。代码如下:

           vec2 texCoordOffsets = 
              clamp(height * viewDirInScaledSurfaceCoords.xy 
              / viewDirInScaledSurfaceCoords.z,
              -_MaxTexCoordOffset, +_MaxTexCoordOffset);

在下面的代码中,我们需要将偏移量应用于所有纹理查找中的纹理坐标;也就是说,我们需要用 (textureCoordinates.xy + texCoordOffsets) 替换 vec2(textureCoordinates)(或等效地 textureCoordinates.xy),例如:

             vec4 encodedNormal = texture2D(_BumpMap, 
               _BumpMap_ST.xy * (textureCoordinates.xy 
               + texCoordOffsets) + _BumpMap_ST.zw);

片段着色器的其余代码与 “凹凸表面照明”部分 中的代码相同。

完整着色器代码

[edit | edit source]

如上一节所述,此代码的大部分内容来自 “凹凸表面照明”部分。请注意,如果你想在具有 OpenGL ES 的移动设备上使用此代码,请确保更改法线贴图的解码方式,如该教程中所述。

关于视差贴图的部分实际上只有几行代码。大多数着色器属性的名称是根据备用着色器选择的;用户界面标签更具描述性。

Shader "GLSL parallax mapping" {
   Properties {
      _BumpMap ("Normal Map", 2D) = "bump" {}
      _ParallaxMap ("Heightmap (in A)", 2D) = "black" {}
      _Parallax ("Max Height", Float) = 0.01
      _MaxTexCoordOffset ("Max Texture Coordinate Offset", Float) = 
         0.01
      _Color ("Diffuse Material Color", Color) = (1,1,1,1) 
      _SpecColor ("Specular Material Color", Color) = (1,1,1,1) 
      _Shininess ("Shininess", Float) = 10
   }
   SubShader {
      Pass {      
         Tags { "LightMode" = "ForwardBase" } 
            // pass for ambient light and first light source
 
         GLSLPROGRAM
 
         // User-specified properties
         uniform sampler2D _BumpMap; 
         uniform vec4 _BumpMap_ST;
         uniform sampler2D _ParallaxMap; 
         uniform vec4 _ParallaxMap_ST;
         uniform float _Parallax;
         uniform float _MaxTexCoordOffset;
         uniform vec4 _Color; 
         uniform vec4 _SpecColor; 
         uniform float _Shininess;
 
         // The following built-in uniforms (except _LightColor0) 
         // are also defined in "UnityCG.glslinc", 
         // i.e. one could #include "UnityCG.glslinc" 
         uniform vec3 _WorldSpaceCameraPos; 
            // camera position in world space
         uniform mat4 _Object2World; // model matrix
         uniform mat4 _World2Object; // inverse model matrix
         uniform vec4 unity_Scale; // w = 1/uniform scale; 
            // should be multiplied to _World2Object
         uniform vec4 _WorldSpaceLightPos0; 
            // direction to or position of light source
         uniform vec4 _LightColor0; 
            // color of light source (from "Lighting.cginc")
 
         varying vec4 position; 
            // position of the vertex (and fragment) in world space 
         varying vec4 textureCoordinates; 
         varying mat3 localSurface2World; // mapping from 
            // local surface coordinates to world coordinates
         varying vec3 viewDirInScaledSurfaceCoords;
 
         #ifdef VERTEX
 
         attribute vec4 Tangent;
 
         void main()
         {                                
            mat4 modelMatrix = _Object2World;
            mat4 modelMatrixInverse = _World2Object * unity_Scale.w;
 
            localSurface2World[0] = normalize(vec3(
               modelMatrix * vec4(vec3(Tangent), 0.0)));
            localSurface2World[2] = normalize(vec3(
               vec4(gl_Normal, 0.0) * modelMatrixInverse));
            localSurface2World[1] = normalize(
               cross(localSurface2World[2], localSurface2World[0]) 
               * Tangent.w);

            vec3 binormal = 
               cross(gl_Normal, vec3(Tangent)) * Tangent.w; 
               // appropriately scaled tangent and binormal 
               // to map distances from object space to texture space
 
            vec3 viewDirInObjectCoords = vec3(modelMatrixInverse 
               * vec4(_WorldSpaceCameraPos, 1.0) - gl_Vertex);
            mat3 localSurface2ScaledObject = 
               mat3(vec3(Tangent), binormal, gl_Normal); 
               // vectors are orthogonal
            viewDirInScaledSurfaceCoords = 
               viewDirInObjectCoords * localSurface2ScaledObject; 
               // we multiply with the transpose to multiply 
               // with the "inverse" (apart from the scaling)

            position = modelMatrix * gl_Vertex;
            textureCoordinates = gl_MultiTexCoord0;
            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }
 
         #endif
 
         #ifdef FRAGMENT
 
         void main()
         {
            // parallax mapping: compute height and 
            // find offset in texture coordinates 
            // for the intersection of the view ray 
            // with the surface at this height
            
            float height = 
               _Parallax * (-0.5 + texture2D(_ParallaxMap,  
               _ParallaxMap_ST.xy * textureCoordinates.xy 
               + _ParallaxMap_ST.zw).a);
            vec2 texCoordOffsets = 
               clamp(height * viewDirInScaledSurfaceCoords.xy 
               / viewDirInScaledSurfaceCoords.z,
               -_MaxTexCoordOffset, +_MaxTexCoordOffset);

            // normal mapping: lookup and decode normal from bump map
            
            // in principle we have to normalize the columns 
            // of "localSurface2World" again; however, the potential 
            // problems are small since we use this matrix only 
            // to compute "normalDirection", which we normalize anyways
            vec4 encodedNormal = texture2D(_BumpMap, 
               _BumpMap_ST.xy * (textureCoordinates.xy 
               + texCoordOffsets) + _BumpMap_ST.zw);
            vec3 localCoords = 
               vec3(2.0 * encodedNormal.ag - vec2(1.0), 0.0);
            localCoords.z = sqrt(1.0 - dot(localCoords, localCoords));
               // approximation without sqrt: localCoords.z = 
               // 1.0 - 0.5 * dot(localCoords, localCoords);
            vec3 normalDirection = 
               normalize(localSurface2World * localCoords);
 
            // per-pixel lighting using the Phong reflection model 
            // (with linear attenuation for point and spot lights)
 
            vec3 viewDirection = 
               normalize(_WorldSpaceCameraPos - vec3(position));
            vec3 lightDirection;
            float attenuation;
 
            if (0.0 == _WorldSpaceLightPos0.w) // directional light?
            {
               attenuation = 1.0; // no attenuation
               lightDirection = normalize(vec3(_WorldSpaceLightPos0));
            } 
            else // point or spot light
            {
               vec3 vertexToLightSource = 
                  vec3(_WorldSpaceLightPos0 - position);
               float distance = length(vertexToLightSource);
               attenuation = 1.0 / distance; // linear attenuation 
               lightDirection = normalize(vertexToLightSource);
            }
 
            vec3 ambientLighting = 
               vec3(gl_LightModel.ambient) * vec3(_Color);
 
            vec3 diffuseReflection = 
               attenuation * vec3(_LightColor0) * vec3(_Color) 
               * max(0.0, dot(normalDirection, lightDirection));
 
            vec3 specularReflection;
            if (dot(normalDirection, lightDirection) < 0.0) 
               // light source on the wrong side?
            {
               specularReflection = vec3(0.0, 0.0, 0.0); 
                  // no specular reflection
            }
            else // light source on the right side
            {
               specularReflection = attenuation * vec3(_LightColor0) 
                  * vec3(_SpecColor) * pow(max(0.0, dot(
                  reflect(-lightDirection, normalDirection), 
                  viewDirection)), _Shininess);
            }
 
            gl_FragColor = vec4(ambientLighting + diffuseReflection 
               + specularReflection, 1.0);
         }
 
         #endif
 
         ENDGLSL
      }
 
      Pass {      
         Tags { "LightMode" = "ForwardAdd" } 
            // pass for additional light sources
         Blend One One // additive blending 
 
         GLSLPROGRAM
 
         // User-specified properties
         uniform sampler2D _BumpMap; 
         uniform vec4 _BumpMap_ST;
         uniform sampler2D _ParallaxMap; 
         uniform vec4 _ParallaxMap_ST;
         uniform float _Parallax;
         uniform float _MaxTexCoordOffset;
         uniform vec4 _Color; 
         uniform vec4 _SpecColor; 
         uniform float _Shininess;
 
         // The following built-in uniforms (except _LightColor0) 
         // are also defined in "UnityCG.glslinc", 
         // i.e. one could #include "UnityCG.glslinc" 
         uniform vec3 _WorldSpaceCameraPos; 
            // camera position in world space
         uniform mat4 _Object2World; // model matrix
         uniform mat4 _World2Object; // inverse model matrix
         uniform vec4 unity_Scale; // w = 1/uniform scale; 
            // should be multiplied to _World2Object
         uniform vec4 _WorldSpaceLightPos0; 
            // direction to or position of light source
         uniform vec4 _LightColor0; 
            // color of light source (from "Lighting.cginc")
 
         varying vec4 position; 
            // position of the vertex (and fragment) in world space 
         varying vec4 textureCoordinates; 
         varying mat3 localSurface2World; // mapping
            // from local surface coordinates to world coordinates
         varying vec3 viewDirInScaledSurfaceCoords;
 
         #ifdef VERTEX
 
         attribute vec4 Tangent;
 
         void main()
         {                                
            mat4 modelMatrix = _Object2World;
            mat4 modelMatrixInverse = _World2Object * unity_Scale.w;
 
            localSurface2World[0] = normalize(vec3(
               modelMatrix * vec4(vec3(Tangent), 0.0)));
            localSurface2World[2] = normalize(vec3(
               vec4(gl_Normal, 0.0) * modelMatrixInverse));
            localSurface2World[1] = normalize(
               cross(localSurface2World[2], localSurface2World[0]) 
               * Tangent.w);

            vec3 binormal = 
               cross(gl_Normal, vec3(Tangent)) * Tangent.w; 
               // appropriately scaled tangent and binormal 
               // to map distances from object space to texture space
 
            vec3 viewDirInObjectCoords = vec3(modelMatrixInverse 
               * vec4(_WorldSpaceCameraPos, 1.0) - gl_Vertex);
            mat3 localSurface2ScaledObject = 
               mat3(vec3(Tangent), binormal, gl_Normal); 
               // vectors are orthogonal
            viewDirInScaledSurfaceCoords = 
               viewDirInObjectCoords * localSurface2ScaledObject; 
               // we multiply with the transpose to multiply 
               // with the "inverse" (apart from the scaling)

            position = modelMatrix * gl_Vertex;
            textureCoordinates = gl_MultiTexCoord0;
            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }
 
         #endif
 
         #ifdef FRAGMENT
 
         void main()
         {
            // parallax mapping: compute height and 
            // find offset in texture coordinates 
            // for the intersection of the view ray 
            // with the surface at this height
            
            float height = 
               _Parallax * (-0.5 + texture2D(_ParallaxMap,  
               _ParallaxMap_ST.xy * textureCoordinates.xy 
               + _ParallaxMap_ST.zw).a);
            vec2 texCoordOffsets = 
               clamp(height * viewDirInScaledSurfaceCoords.xy 
               / viewDirInScaledSurfaceCoords.z,
               -_MaxTexCoordOffset, +_MaxTexCoordOffset);

            // normal mapping: lookup and decode normal from bump map
            
            // in principle we have to normalize the columns 
            // of "localSurface2World" again; however, the potential 
            // problems are small since we use this matrix only to 
            // compute "normalDirection", which we normalize anyways
            vec4 encodedNormal = texture2D(_BumpMap, 
               _BumpMap_ST.xy * (textureCoordinates.xy 
               + texCoordOffsets) + _BumpMap_ST.zw);
            vec3 localCoords = 
               vec3(2.0 * encodedNormal.ag - vec2(1.0), 0.0);
            localCoords.z = sqrt(1.0 - dot(localCoords, localCoords));
               // approximation without sqrt: localCoords.z = 
               // 1.0 - 0.5 * dot(localCoords, localCoords);
            vec3 normalDirection = 
               normalize(localSurface2World * localCoords);
 
            // per-pixel lighting using the Phong reflection model 
            // (with linear attenuation for point and spot lights)
 
            vec3 viewDirection = 
               normalize(_WorldSpaceCameraPos - vec3(position));
            vec3 lightDirection;
            float attenuation;
 
            if (0.0 == _WorldSpaceLightPos0.w) // directional light?
            {
               attenuation = 1.0; // no attenuation
               lightDirection = normalize(vec3(_WorldSpaceLightPos0));
            } 
            else // point or spot light
            {
               vec3 vertexToLightSource = 
                  vec3(_WorldSpaceLightPos0 - position);
               float distance = length(vertexToLightSource);
               attenuation = 1.0 / distance; // linear attenuation 
               lightDirection = normalize(vertexToLightSource);
            }
 
            vec3 diffuseReflection = 
               attenuation * vec3(_LightColor0) * vec3(_Color) 
               * max(0.0, dot(normalDirection, lightDirection));
 
            vec3 specularReflection;
            if (dot(normalDirection, lightDirection) < 0.0) 
               // light source on the wrong side?
            {
               specularReflection = vec3(0.0, 0.0, 0.0); 
                  // no specular reflection
            }
            else // light source on the right side
            {
               specularReflection = attenuation * vec3(_LightColor0) 
                  * vec3(_SpecColor) * pow(max(0.0, dot(
                  reflect(-lightDirection, normalDirection), 
                  viewDirection)), _Shininess);
            }
 
            gl_FragColor = 
               vec4(diffuseReflection + specularReflection, 1.0);
         }
 
         #endif
 
         ENDGLSL
      }
   } 
   // The definition of a fallback shader should be commented out 
   // during development:
   // Fallback "Parallax Specular"
}

总结

[edit | edit source]

恭喜!如果你真的理解了整个着色器,那么你已经走了很远了。事实上,着色器包含了许多概念(坐标系之间的变换,从左侧乘以正交矩阵的逆矩阵来应用它,Phong 反射模型,法线贴图,视差贴图……)。更具体地说,我们已经看到了

  • 视差贴图是如何改进法线贴图的。
  • 视差贴图是如何用数学方式描述的。
  • 视差贴图是如何实现的。

进一步阅读

[edit | edit source]

如果你想了解更多

  • 关于着色器代码的细节,你应该阅读 “凹凸表面照明”部分
  • 关于视差贴图,您可以阅读 Kaneko Tomomichi 等人撰写的原始出版物:“Detailed shape representation with parallax mapping”,ICAT 2001,第 205-208 页,可在 网上 获取。


< GLSL 编程/Unity

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