GLSL 程式設計/Unity/凹凸表面的投影

本教程介紹(單步)視差貼圖。
它擴充套件並基於“凹凸表面的光照”部分。
在“凹凸表面的光照”部分中介紹的法線貼圖技術只改變平面的光照效果,以創造凹凸和凹陷的錯覺。如果一個人直視表面(即朝向表面法線向量方向),這種方法效果很好。但是,如果一個人從其他角度觀察表面(如左側影像所示),凹凸也應該從表面突出,而凹陷應該凹進表面。當然,可以通過幾何建模凹凸和凹陷來實現這一點;但是,這將需要處理更多的頂點。另一方面,單步視差貼圖是一種類似於法線貼圖的非常高效的技術,它不需要額外的三角形,但仍然可以將虛擬凹凸移動幾個畫素,使它們從平面表面突出。然而,這種技術僅限於高度較小的凹凸和凹陷,需要一些微調才能獲得最佳效果。

視差貼圖是由 Tomomichi Kaneko 等人於 2001 年在其論文“視差貼圖的詳細形狀表示”(ICAT 2001)中提出的。基本思想是對用於表面紋理的紋理座標(尤其是法線貼圖)進行偏移。如果這種紋理座標偏移計算得當,則可以移動紋理的一部分(例如凹凸),就好像它們從表面突出一樣。
左側的插圖顯示了指向觀察者的視向量 V 和在片段著色器中光柵化的表面點的表面法線向量 N。視差貼圖分三步進行
- 在光柵化點查詢高度圖中的高度 ,該高度圖由插圖底部直線上方的波浪線表示。
- 計算方向為 V 的視線與高度為 的表面(平行於渲染表面)的交點。距離 是光柵化表面點在 N 方向上移動 後的點與該交點的距離。如果將這兩個點投影到渲染的表面上,則 也是光柵化點與表面上一個新點的距離(插圖中用十字標記)。如果表面被高度圖位移,這個新的表面點更接近於方向為 V 的視線實際上可見的點。
- 將偏移量 轉換為紋理座標空間,以便為所有後續紋理查詢計算紋理座標偏移量。
為了計算 ,我們需要光柵化點的高度圖高度 ,這在示例中透過紋理屬性 _ParallaxMap 的 A 分量的紋理查詢來實現,它應該是一個灰度影像,如“凹凸表面的光照”部分中所述,表示高度。我們還需要以法線向量( 軸)、切線向量( 軸)和副法線向量( 軸)形成的區域性表面座標系中的視方向 V,這也在“凹凸表面的光照”部分中介紹過。為此,我們計算從區域性表面座標到物體空間的變換,方法是:
其中 T、B 和 N 在物體座標系中給出。(“凹凸表面的光照”部分 中我們有一個類似的矩陣,但向量在世界座標系中。)
我們在物體空間中計算視角方向 V(作為光柵化位置和從世界空間變換到物體空間的相機位置之間的差),然後使用矩陣 將其變換到區域性表面空間,該矩陣可以計算為
這是可能的,因為 T、B 和 N 是正交且歸一化的。(實際上,情況稍微複雜一些,因為我們不會歸一化這些向量,而是使用它們的長度進行另一個變換;見下文。)因此,為了將 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 頁,可在 網上 獲取。