關於渲染的中文文章可謂是少之又少,而不少書和中英文技術文章老是大篇幅的進行晦澀難懂的公式推導,這種方式確實表達準確,可苦了數學很差的娃,能找到一篇好的材料進行學習真的是一件很不容易的事情。php
我在學習Parallax Mapping的時候無心間找到這篇文章,圖文並茂,而且把Bump Mapping系列中的各類技術由簡單到複雜逐一介紹給了讀者。開心之下就決定把它翻譯成中文,以饗後人。算法
雖然說簡單,可是它也不是從零開始的。本文要求讀者具有了理解最基本的Normal Mapping的知識,理解切空間。Normal Mapping的文章網上大把抓,切空間的話能夠讀這篇文章。app
下面是正文,原文連接在此。函數
這一課講如何用GLSL和OpenGL實現各類視差映射技術(一樣的技術亦可用DirectX實現)。主要會涵蓋以下幾種技術:視差映射(Parallax Mapping),帶偏移上限的視差映射(Parallax Mapping with Offset Limiting),陡峭視差映射(Steep Parallax Mapping),浮雕視差映射(Relief Parallax Mapping)和視差遮蔽映射(Parallax Occlusion Mapping)。另外本文還會介紹如何實如今視差映射中的自陰影(軟陰影)。下面的幾個圖片展現了幾種視差映射技術和簡單光照或者法線映射的效果對比。性能
我就是給原文截了個圖,建議進到原文去看圖片。學習
在計算機圖形學中視差映射是法線映射的一個加強版本,它不止改變了光照的做用方式,還在平坦的多邊形上建立了3D細節的假象。不會生成任何額外的圖元。上面的圖片展現了視差映射和發現映射的對比。你可能以爲視差映射偏移了原始圖元,但其實它只是偏移了用來獲取顏色和法線的紋理座標。ui
要實現視差映射你須要一張高度貼圖。高度圖中的每一個像素包含了表面高度的信息。紋理中的高度會被轉化成對應的點沉入表面多少的信息。這種狀況你得把高度圖中讀出來的值反過來用。這篇教程中的視差映射會把高度圖中的值當深度來用,黑色(0)表明和表面齊平的高度,白色(1)表明最深的凹陷值。this
下面的例子會用到3張紋理:高度圖,漫反射顏色紋理和法線貼圖。一般法線貼圖都是從高度圖生成出來的。在咱們的栗子中,高度圖被當成深度圖看待,因此在生成法線貼圖以前你得先反轉高度圖(譯者:服了,能不說車軲轆話嗎?)。你還能把高度圖和法線貼圖合併到一張紋理中,把高度存在Alpha通道里,可是爲了表述清楚本文仍是把他們分開用了。下面是這三張圖:spa
視差映射技術的主要任務是修改紋理座標,讓平面看起來像是立體的。主要計算都是在Fragment Shader中進行。看看下面的圖片。水平線0.0表示徹底沒有凹陷的深度,水平線1.0表示凹陷的最大深度。實際的幾何體並沒改變,其實一直都在0.0水平線上。圖中的曲線表明了高度圖中存儲的高度數據。.net
設當前點(譯者:原文中用的是Fragment,片元。)是圖片中用黃色方塊高亮出來的那個點,這個點的紋理座標是T0。向量V是從攝像機到點的方向向量。用座標T0在高度圖上採樣,你能獲得這個點的高度值H(T0)=0.55。這個值不是0,因此點並非在表面上,而是凹陷下去的。因此你得把向量V繼續延長直到與高度圖定義出來的表面最近的一個交點。這個交點咱們說它的深度就是H(T1),它的紋理座標就是T1。因此咱們就應該用T1的紋理座標去對顏色和法線貼圖進行採樣。
因此說,全部視差映射技術的主要目的,就是要精確的計算攝像機的向量V和高度圖定義出來的表面的交點。
視差映射的計算是在切空間進行的(跟法線映射同樣)。因此指向光源的向量(L)和指向攝像機的向量(V)應該先被變換到切空間。在用視差映射計算出來新的紋理座標以後,你能夠用這個座標來計算自陰影,能夠從漫反射貼圖讀取顏色以及從發現貼圖讀取法線。
在這個教程中視差映射的實現是在一個叫parallaxMapping()的函數體中,自陰影是在parallaxSoftShadowMultiplier()中,而後Blinn-Phone光照模型和法線映射的代碼是在normalMappingLighting()函數體中。下面的頂點和片元着色器是視差映射和自陰影的基礎模板。頂點着色器把光照向量和攝像機向量變換到切空間。片元着色器調用視差映射的相關函數,而後計算自陰影係數,並計算最終光照後的顏色值。
// Basic vertex shader for parallax mapping #version 330 // attributes layout(location = 0) in vec3 i_position; // xyz - position layout(location = 1) in vec3 i_normal; // xyz - normal layout(location = 2) in vec2 i_texcoord0; // xy - texture coords layout(location = 3) in vec4 i_tangent; // xyz - tangent, w - handedness // uniforms uniform mat4 u_model_mat; uniform mat4 u_view_mat; uniform mat4 u_proj_mat; uniform mat3 u_normal_mat; uniform vec3 u_light_position; uniform vec3 u_camera_position; // data for fragment shader out vec2 o_texcoords; out vec3 o_toLightInTangentSpace; out vec3 o_toCameraInTangentSpace; /////////////////////////////////////////////////////////////////// void main(void) { // transform to world space vec4 worldPosition = u_model_mat * vec4(i_position, 1); vec3 worldNormal = normalize(u_normal_mat * i_normal); vec3 worldTangent = normalize(u_normal_mat * i_tangent.xyz); // calculate vectors to the camera and to the light vec3 worldDirectionToLight = normalize(u_light_position - worldPosition.xyz); vec3 worldDirectionToCamera = normalize(u_camera_position - worldPosition.xyz); // calculate bitangent from normal and tangent vec3 worldBitangnent = cross(worldNormal, worldTangent) * i_tangent.w; // transform direction to the light to tangent space o_toLightInTangentSpace = vec3( dot(worldDirectionToLight, worldTangent), dot(worldDirectionToLight, worldBitangnent), dot(worldDirectionToLight, worldNormal) ); // transform direction to the camera to tangent space o_toCameraInTangentSpace= vec3( dot(worldDirectionToCamera, worldTangent), dot(worldDirectionToCamera, worldBitangnent), dot(worldDirectionToCamera, worldNormal) ); // pass texture coordinates to fragment shader o_texcoords = i_texcoord0; // calculate screen space position of the vertex gl_Position = u_proj_mat * u_view_mat * worldPosition; }
// basic fragment shader for Parallax Mapping #version 330 // data from vertex shader in vec2 o_texcoords; in vec3 o_toLightInTangentSpace; in vec3 o_toCameraInTangentSpace; // textures layout(location = 0) uniform sampler2D u_diffuseTexture; layout(location = 1) uniform sampler2D u_heightTexture; layout(location = 2) uniform sampler2D u_normalTexture; // color output to the framebuffer out vec4 resultingColor; //////////////////////////////////////// // scale for size of Parallax Mapping effect uniform float parallaxScale; // ~0.1 ////////////////////////////////////////////////////// // Implements Parallax Mapping technique // Returns modified texture coordinates, and last used depth vec2 parallaxMapping(in vec3 V, in vec2 T, out float parallaxHeight) { // ... } ////////////////////////////////////////////////////// // Implements self-shadowing technique - hard or soft shadows // Returns shadow factor float parallaxSoftShadowMultiplier(in vec3 L, in vec2 initialTexCoord, in float initialHeight) { // ... } ////////////////////////////////////////////////////// // Calculates lighting by Blinn-Phong model and Normal Mapping // Returns color of the fragment vec4 normalMappingLighting(in vec2 T, in vec3 L, in vec3 V, float shadowMultiplier) { // restore normal from normal map vec3 N = normalize(texture(u_normalTexture, T).xyz * 2 - 1); vec3 D = texture(u_diffuseTexture, T).rgb; // ambient lighting float iamb = 0.2; // diffuse lighting float idiff = clamp(dot(N, L), 0, 1); // specular lighting float ispec = 0; if(dot(N, L) > 0.2) { vec3 R = reflect(-L, N); ispec = pow(dot(R, V), 32) / 1.5; } vec4 resColor; resColor.rgb = D * (ambientLighting + (idiff + ispec) * pow(shadowMultiplier, 4)); resColor.a = 1; return resColor; } ///////////////////////////////////////////// // Entry point for Parallax Mapping shader void main(void) { // normalize vectors after vertex shader vec3 V = normalize(o_toCameraInTangentSpace); vec3 L = normalize(o_toLightInTangentSpace); // get new texture coordinates from Parallax Mapping float parallaxHeight; vec2 T = parallaxMapping(V, o_texcoords, parallaxHeight); // get self-shadowing factor for elements of parallax float shadowMultiplier = parallaxSoftShadowMultiplier(L, T, parallaxHeight - 0.05); // calculate lighting resultingColor = normalMappingLighting(T, L, V, shadowMultiplier); }
視差映射中最簡單的版本只取一步近似來計算新的紋理座標,這項技術被簡單的稱爲視差映射。視差映射只有在高度圖相對比較平滑,而且不存在複雜的細節時,才能獲得相對能夠接受的效果。若是攝像機向量和法線向量的夾角過大的話,視差映射的效果會是錯誤的。視差映射近似計算的核心思想是:
從高度圖讀取紋理座標T0位置的高度H(T0)
根據H(T0)和攝像機向量V,在初始的紋理座標基礎上進行偏移。
偏移紋理座標的方法以下。由於攝像機向量是在切空間下,而切空間是沿着紋理座標方向創建的,因此向量V的X和Y份量就能夠直接不加換算的用做紋理座標的偏移量。向量V的Z份量是法向份量,垂直於表面。你能夠用Z除X和Y。這就是視差映射技術中對紋理座標的原始計算。你也能夠保留X和Y的值,這樣的實現叫帶偏移上限的視差映射。帶偏移上限的視差映射能夠避免在攝像機向量V和法向量N夾角太大時的一些詭異的結果。而後你把V的X和Y份量加到原始紋理座標上,就獲得了沿着V方向的新的紋理座標。
你得把原紋理位置的深度值H(T0)也算進偏移中,直接把V.xy和H(T0)相乘就行了。
你能夠用一個scale變量來控制視差映射效果的幅度。一樣,你得把它乘給V.xy。最有意義的scale值在0~0.5之間。更高的值每每會致使視差映射計算出錯誤的效果(見上圖)。你也能夠把scale設爲負數,這樣的話你得把法向量的Z份量反轉過來。
下面是偏移後的紋理座標Tp的最終公式:
下圖展現了高度圖中的深度值H(T0)是如何影響紋理座標T0沿着V方向偏移的。此情形下做爲結果的Tp是錯誤的,由於視差映射只是一個近似,而並非找出V和表面的準確交點。
這個方法的主要優勢是隻須要額外對高度圖採樣一次,因此性能上槓槓的。下面是shader函數的實現:
vec2 parallaxMapping(in vec3 V, in vec2 T, out float parallaxHeight) { // get depth for this fragment float initialHeight = texture(u_heightTexture, o_texcoords).r; // calculate amount of offset for Parallax Mapping vec2 texCoordOffset = parallaxScale * V.xy / V.z * initialHeight; // calculate amount of offset for Parallax Mapping With Offset Limiting texCoordOffset = parallaxScale * V.xy * initialHeight; // retunr modified texture coordinates return o_texcoords - texCoordOffset; }
陡峭視差映射,不像簡單的視差映射近似,並不僅是簡單粗暴的對紋理座標進行偏移而不檢查合理性和關聯性,會檢查結果是否接近於正確值。這種方法的核心思想是把表面的深度切分紅等距的若干層。而後從最頂端的一層開始採樣高度圖,每一次會沿着V的方向偏移紋理座標。若是點已經低於了表面(當前的層的深度大於採樣出的深度),中止檢查而且使用最後一次採樣的紋理座標做爲結果。
陡峭視差映射的工做方式在下面的圖片上舉例。深度被分割成8個層,每層的高度值是0.125。每層的紋理座標偏移是V.xy/V.z * scale/numLayers。從頂層黃色方塊的位置開始檢查,下面是手動計算步驟:
層的深度爲0,高度圖深度H(T0)大約爲0.75。採樣到的深度大於層的深度,因此開始下一次迭代。
沿着V方向偏移紋理座標,選定下一層。層深度爲0.125,高度圖深度H(T1)大約爲0.625。採樣到的深度大於層的深度,因此開始下一次迭代。
沿着V方向偏移紋理座標,選定下一層。層深度爲0.25,高度圖深度H(T2)大約爲0.4。採樣到的深度大於層的深度,因此開始下一次迭代。
沿着V方向偏移紋理座標,選定下一層。層深度爲0.375,高度圖深度H(T3)大約爲0.2。採樣到的深度小於層的深度,因此向量V上的當前點在表面之下。咱們找到了紋理座標Tp=T3是實際交點的近似點。
從上圖你能看到,其實紋理座標T3仍是離交點挺遠的。可是這個紋理座標已經比視差映射要接近正確結果了。若是你想獲得更精確的結果,增長層的數量。
陡峭視差映射的主要優點在於它把深度切分紅了有限數量的層。若是層數不少,那性能就會低。但若是層數少,就會有明顯的鋸齒現象產生,就像下面這張圖同樣。你也能夠根據攝像機向量V和多邊形法向N之間的夾角來動態的決定層的數量。性能和鋸齒的問題在下文的浮雕視差映射和視差遮蔽映射中能夠解決。
下面是陡峭視差映射的代碼:
vec2 parallaxMapping(in vec3 V, in vec2 T, out float parallaxHeight) { // determine number of layers from angle between V and N const float minLayers = 5; const float maxLayers = 15; float numLayers = mix(maxLayers, minLayers, abs(dot(vec3(0, 0, 1), V))); // height of each layer float layerHeight = 1.0 / numLayers; // depth of current layer float currentLayerHeight = 0; // shift of texture coordinates for each iteration vec2 dtex = parallaxScale * V.xy / V.z / numLayers; // current texture coordinates vec2 currentTextureCoords = T; // get first depth from heightmap float heightFromTexture = texture(u_heightTexture, currentTextureCoords).r; // while point is above surface while(heightFromTexture > currentLayerHeight) { // to the next layer currentLayerHeight += layerHeight; // shift texture coordinates along vector V currentTextureCoords -= dtex; // get new depth from heightmap heightFromTexture = texture(u_heightTexture, currentTextureCoords).r; } // return results parallaxHeight = currentLayerHeight; return currentTextureCoords; }
浮雕視差映射升級了陡峭視差映射,讓咱們的shader能找到更精確的紋理座標。首先你先用浮雕視差映射,而後你能獲得交點先後的兩個層,和對應的深度值。在下面的圖中這兩個層分別對應紋理座標T2和T3。如今你能夠用二分法來進一步改進你的結果,每一次搜索迭代可使精確度提高一倍。
下圖表達了浮雕視差映射的主要步驟:
(譯者:這一段的內容和原文區別較大,由於直接按照原文翻譯有不少容易混淆的名詞,因此我加入了變量聲明。)
在陡峭視差映射以後,咱們知道交點確定在T2和T3之間。真實的交點在圖上用綠點標出來了。
設每次迭代時的紋理座標變化量ST,它的初始值等於向量V在穿過一個層的深度時的XY份量。
設每次迭代時的深度值變化量SH,它的初始值等於一個層的深度。
把ST和SH都除以2。
把紋理座標T3沿着反方向偏移ST,把層深度沿反方向偏移SH,獲得這次迭代的紋理座標T4和層深度H(T4)。
(*)採樣高度圖,把ST和SH都除以2。
若是高度圖中的深度值大於當前迭代層的深度H(T4),則將當前迭代層的深度增長SH,迭代的紋理座標沿着V的方向增長ST。
若是高度圖中的深度值小於當前迭代層的深度H(T4),則將當前迭代層的深度減小SH,迭代的紋理座標沿着V的相反方向增長ST。
從(*)處循環,繼續二分搜索,直到規定的次數。
最後一步獲得的紋理座標就是浮雕視差映射的結果。
下面是浮雕視差映射的實現:
vec2 parallaxMapping(in vec3 V, in vec2 T, out float parallaxHeight) { // determine required number of layers const float minLayers = 10; const float maxLayers = 15; float numLayers = mix(maxLayers, minLayers, abs(dot(vec3(0, 0, 1), V))); // height of each layer float layerHeight = 1.0 / numLayers; // depth of current layer float currentLayerHeight = 0; // shift of texture coordinates for each iteration vec2 dtex = parallaxScale * V.xy / V.z / numLayers; // current texture coordinates vec2 currentTextureCoords = T; // depth from heightmap float heightFromTexture = texture(u_heightTexture, currentTextureCoords).r; // while point is above surface while(heightFromTexture > currentLayerHeight) { // go to the next layer currentLayerHeight += layerHeight; // shift texture coordinates along V currentTextureCoords -= dtex; // new depth from heightmap heightFromTexture = texture(u_heightTexture, currentTextureCoords).r; } /////////////////////////////////////////////////////////// // Start of Relief Parallax Mapping // decrease shift and height of layer by half vec2 deltaTexCoord = dtex / 2; float deltaHeight = layerHeight / 2; // return to the mid point of previous layer currentTextureCoords += deltaTexCoord; currentLayerHeight -= deltaHeight; // binary search to increase precision of Steep Paralax Mapping const int numSearches = 5; for(int i=0; i<numSearches; i++) { // decrease shift and height of layer by half deltaTexCoord /= 2; deltaHeight /= 2; // new depth from heightmap heightFromTexture = texture(u_heightTexture, currentTextureCoords).r; // shift along or agains vector V if(heightFromTexture > currentLayerHeight) // below the surface { currentTextureCoords -= deltaTexCoord; currentLayerHeight += deltaHeight; } else // above the surface { currentTextureCoords += deltaTexCoord; currentLayerHeight -= deltaHeight; } } // return results parallaxHeight = currentLayerHeight; return currentTextureCoords; }
視差遮蔽映射(POM)是陡峭視差映射的另外一個改進版本。浮雕視差映射用了二分搜索法來提高結果精度,可是搜索下降程序性能。視差遮蔽映射旨在比浮雕視差映射更好的性能下獲得比陡峭視差映射更好的效果。可是POM的效果要比浮雕視差映射差一些。
視差遮蔽映射簡單的對陡峭視差映射的結果進行插值。請看下圖,POM使用相交以後的層深度(0.375,陡峭視差映射中止迭代的層),上一個採樣深度H(T2)和下一個採樣深度H(T3)。從圖片中你能看到,視差遮蔽映射的插值結果是在視向量V和H(T2)和H(T3)高度的連線的交點上。這個交點已經足夠接近實際交點(標記爲綠色的點)了。
圖片對應的手動計算步驟:
nextHeight = H(T3) - currentLayerHeight
prevHeight = H(T2) - (currentLayerHeight - layerHeight)
weight = nextHeight / (nextHeight - prevHeight)
Tp = T(T2) weight + T(T3) (1.0 - weight)
視差遮蔽映射可使用相對較少的採樣次數產生很好的結果。但視差遮蔽映射比浮雕視差映射更容易跳太高度圖中的小細節,也更容易在高度圖數據產生大幅度的變化時獲得錯誤的結果。
這是POM的實現:
vec2 parallaxMapping(in vec3 V, in vec2 T, out float parallaxHeight) { // determine optimal number of layers const float minLayers = 10; const float maxLayers = 15; float numLayers = mix(maxLayers, minLayers, abs(dot(vec3(0, 0, 1), V))); // height of each layer float layerHeight = 1.0 / numLayers; // current depth of the layer float curLayerHeight = 0; // shift of texture coordinates for each layer vec2 dtex = parallaxScale * V.xy / V.z / numLayers; // current texture coordinates vec2 currentTextureCoords = T; // depth from heightmap float heightFromTexture = texture(u_heightTexture, currentTextureCoords).r; // while point is above the surface while(heightFromTexture > curLayerHeight) { // to the next layer curLayerHeight += layerHeight; // shift of texture coordinates currentTextureCoords -= dtex; // new depth from heightmap heightFromTexture = texture(u_heightTexture, currentTextureCoords).r; } /////////////////////////////////////////////////////////// // previous texture coordinates vec2 prevTCoords = currentTextureCoords + texStep; // heights for linear interpolation float nextH = heightFromTexture - curLayerHeight; float prevH = texture(u_heightTexture, prevTCoords).r - curLayerHeight + layerHeight; // proportions for linear interpolation float weight = nextH / (nextH - prevH); // interpolation of texture coordinates vec2 finalTexCoords = prevTCoords * weight + currentTextureCoords * (1.0-weight); // interpolation of depth values parallaxHeight = curLayerHeight + prevH * weight + nextH * (1.0 - weight); // return result return finalTexCoords; }
你能夠經過和陡峭視差映射很接近的算法來肯定一個點是否處於陰影之中。你要向外搜索,而不是向裏。同時紋理座標的偏移應該從點沿着光的方向,而不是沿着攝像機方向。光源向量L應該在切空間中,跟V同樣,它能夠直接被用做偏移紋理座標。自陰影計算的結果是一個陰影係數 - 在[0,1]之間的值。這個數值能夠在後面用來調節漫反射和鏡面反射的光照強度。
要計算硬邊緣的陰影(硬陰影)你要沿着L找到第一個在表面之下的點。若是點在表面之下則陰影係數是0, 不然就是1。好比,在下面的圖片上,高度值H(TL1)小於層的高度值Ha,因此這個點在表面如下,陰影係數是0。若是光向量直到水平面0.0也沒有找到任何點在表面如下,那咱們的片元就應該是在光照中,陰影係數則爲1。陰影的質量極大程度上受到分層數量、scale參數和光向量L和多邊形的法向量N之間的角度的影響。若是設置不恰當,陰影會出現鋸齒或者更糟。
軟陰影會計算沿着光源向量L的多個值,只有在表面如下的點纔會包含進來。半陰影的係數根據當前層深度和當前點高度圖深度之間的差別來得出。你還得把點到片元的舉例計算在內。因此半陰影係數要被乘以(1.0 - stepIndex/numberOfSteps)。要計算最終的陰影係數,你得選出那個最大的半陰影係數。由此獲得計算軟陰影係數的公式:
這裏是軟陰影係數的計算步驟(對應於圖片)
設置shadow factor爲0,迭代步數爲4。
沿着L向前步進到Ha。Ha小於H(TL1),因此該點在表面之下。計算半陰影係數爲Ha-H(TL1)。這是第一次檢查,總共的檢查次數爲4,計算距離影響,將半陰影係數乘以(1.0 - 1.0/4.0)。保存這個半陰影係數。
沿着L向前步進到Hb。Hb小於H(TL2),因此該點在表面之下。計算半陰影係數爲Hb-H(TL2)。這事第二次檢查,總共的檢查次數爲4,計算距離影響,將半陰影係數乘以(1.0 - 2.0/4.0)。保存這個半陰影係數。
沿着L向前步進,這個點在表面之上。
最後一次沿着L向前步進,這個點也在表面之上。
迭代的點已經高於了水平線0.0,結束迭代。
選取最大的半陰影係數做爲最終的陰影係數值。
下面是代碼:
float parallaxSoftShadowMultiplier(in vec3 L, in vec2 initialTexCoord, in float initialHeight) { float shadowMultiplier = 1; const float minLayers = 15; const float maxLayers = 30; // calculate lighting only for surface oriented to the light source if(dot(vec3(0, 0, 1), L) > 0) { // calculate initial parameters float numSamplesUnderSurface = 0; shadowMultiplier = 0; float numLayers = mix(maxLayers, minLayers, abs(dot(vec3(0, 0, 1), L))); float layerHeight = initialHeight / numLayers; vec2 texStep = parallaxScale * L.xy / L.z / numLayers; // current parameters float currentLayerHeight = initialHeight - layerHeight; vec2 currentTextureCoords = initialTexCoord + texStep; float heightFromTexture = texture(u_heightTexture, currentTextureCoords).r; int stepIndex = 1; // while point is below depth 0.0 ) while(currentLayerHeight > 0) { // if point is under the surface if(heightFromTexture < currentLayerHeight) { // calculate partial shadowing factor numSamplesUnderSurface += 1; float newShadowMultiplier = (currentLayerHeight - heightFromTexture) * (1.0 - stepIndex / numLayers); shadowMultiplier = max(shadowMultiplier, newShadowMultiplier); } // offset to the next layer stepIndex += 1; currentLayerHeight -= layerHeight; currentTextureCoords += texStep; heightFromTexture = texture(u_heightTexture, currentTextureCoords).r; } // Shadowing factor should be 1 if there were no points under the surface if(numSamplesUnderSurface < 1) { shadowMultiplier = 1; } else { shadowMultiplier = 1.0 - shadowMultiplier; } } return shadowMultiplier; }