跳轉到內容

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

除非另有說明,否則此頁面上的所有示例原始碼均歸屬公共領域。
華夏公益教科書