在 OpenGL ES 2.0 上實現視差貼圖(Parallax Mapping)

在 OpenGL ES 2.0 上實現視差貼圖(Parallax Mapping)

視差貼圖

最近一直在研究如何在個人 iPad 2(只支持 OpenGL ES 2.0, 不支持 3.0) 上實現 視差貼圖(Parallax Mapping) 和 位移貼圖(Displacement Mapping).php

通過一番研究, 搜索閱讀了很多文章, 終於肯定, OpenGL ES 2.0 能夠支持 視差貼圖, 不過暫時還沒什麼好辦法支持 位移貼圖.html

由於就我目前所瞭解的位移貼圖, 有這麼兩種方法來實現, 一種是用 Tessellation 來提供多面數網格, 另外一種是在頂點着色器中對高度圖進行紋理採樣來計算對應的頂點偏移量.git

第一種方法就沒必要想了, 由於目前移動設備的 OpenGL ES 2.0/3.0 都不支持(貌似 DX11OpenGL 4.0 才支持), 而 OpenGL ES 3.0 衍生自 OpenGL 3.3.github

第二種方法目前看起來只能在 OpenGL ES 3.0 上使用, 請參考這篇文檔Jim's GameDev Blog: 置換貼圖 Displacement Mapping. 不過沒辦法在 OpenGL ES 2.0 上使用, 由於它要求在頂點着色器中進行紋理採樣, 而這個特性偏偏是 2.0 不支持, 3.0 支持的.算法

咱們能夠看看 Jim3.0 設備上實現位移貼圖的效果:app

原始圖:ide

使用位移貼圖後的效果:函數

好了, 如今在咱們的 2.0 設備上實現咱們的 視差貼圖 吧, 先看看效果:.net

使用不一樣參數的效果:3d

  • height_scale = -0.015

  • height_scale = -0.055

  • height_scale = -0.095

你能夠靈活調整這些參數:

  • lightPos: 光源位置
  • viewPos: 眼睛位置
  • height_scale: 高度圖取樣值縮放比例

看看這個視頻 video:

實現細節

關鍵技術點就這麼幾個:

手動構造正切空間 TBN 變換矩陣

若是你使用比較大的引擎, 好比 Unity, 它會幫你計算好法線,切線次法線, 若是本身開發, 沒有使用這些引擎, 那麼極可能就須要本身手動構造了.

目前我發現有 3 種根據法線手動計算 TBN 的近似算法, 其中一種既能在 OpenGL ES 2.0 的頂點着色器內使用, 也能在片斷着色器內使用, 就是咱們下面要提到的這種, 主要原理就是已知了法線 Normal, 要據此求出對應的切線 Tangent 和 次法線 Binormal, 由於它們兩兩垂直, 並且 TBUV 對齊, 所以很容易求得 T, 再根據 TN 求得 B, 算法代碼以下:

// 根據法線 N 計算 T,B
    vec3 tangent; 
    vec3 binormal; 

    // 使用頂點屬性法線,並歸一化
    vec3 Normal = normalize(normal*2.0-1.0);

    // 經過叉積來計算夾角
    vec3 c1 = cross(Normal, vec3(0.0, 0.0, 1.0)); 
    vec3 c2 = cross(Normal, vec3(0.0, 1.0, 0.0)); 
    
    // 方向朝外的是咱們要的
    if ( length(c1) > length(c2) ) { tangent = c1;  } else { tangent = c2;}
    
    // 歸一化切線和次法線
    tangent = normalize(tangent);
    binormal = normalize(cross(normal, tangent)); 
    
    vec3 T = normalize(mat3(model) * tangent);
    vec3 B = normalize(mat3(model) * binormal);
    vec3 N = normalize(mat3(model) * normal);
    
    // 構造出 TBN 矩陣
    mat3 TBN = mat3(T, B, N);

獲得 TBN 矩陣後, 既能夠把其餘向量從其餘空間變換進正切空間來, 也能夠把正切空間的向量變換到其餘空間去. 一般意義的作法是:

  • TBN 用於正向變換
  • TBN 的逆陣用於反向變換

不過在 OpenGL 中, 你把矩陣放在向量左邊乘, 就是正向變換, 它會按列矩陣處理; 你把矩陣放在向量右邊乘就是反向變換, 它會按行矩陣處理. 這樣就不須要再進行矩陣求逆的操做了.

視差映射函數

視差貼圖 的本質就是根據高度紋理圖的不一樣高度以及視線向量的座標, 來實時計算紋理座標在視線下的偏移, 並以此做爲新的紋理座標來從紋理貼圖中進行取樣.

代碼以下:

// The Parallax Mapping
vec2 ParallaxMapping1(vec2 texCoords, vec3 viewDir)
{ 
    float height =  texture2D(depthMap, texCoords).r;     
    return texCoords - viewDir.xy / viewDir.z * (height * height_scale);            
}

在此基礎上提出的 視差遮掩 能夠提供更好的視覺效果, 具體原理在代碼註釋中.

光照模型

最後就是一個很是簡單的光照模型, 先從原始紋理中取樣, 以此爲基礎, 縮小10倍做爲環境光, 根據前面計算獲得的切線來計算光線攝入方向, 再結合法線能夠計算出漫射光和反射光, 最後把這些光線混合就獲得最終的光照顏色值了.

這個光照模型的好處是簡單易用, 不須要另外設置過多參數, 壞處就是不太靈活, 實際使用時能夠把參數設置爲可變, 或者直接換成其餘光照模型也能夠(具體的參數值就須要本身調整了).

由於代碼本身計算構造了 TBN 變換矩陣, 因此這段 shader 代碼具備很好的移植性, 能夠輕鬆地把它用在其餘地方.

完整代碼

代碼以下:

function setup()
    displayMode(OVERLAY)
    print("試驗 OpenGL ES 2.0 中位移貼圖的例子")
    print("Test the Parallax Mapping in OpenGL ES 2.0")

    img1 = readImage("Dropbox:dm")
    img2 = readImage("Dropbox:dnm1")
    img3 = readImage("Dropbox:dm1")
    local w,h = WIDTH,HEIGHT
    local c = color(223, 218, 218, 255)
    m3 = mesh()
    m3i = m3:addRect(w/2,h/2,w/1,h/1)
    m3:setColors(c)
    m3.texture = img1
    m3:setRectTex(m3i,0,0,1,1)
    m3.shader = shader(shaders.vs,shaders.fs)
    m3.shader.diffuseMap = img1
    m3.shader.normalMap = img2
    m3.shader.depthMap =  img3

    -- local tb = m3:buffer("tangent")
    -- tb:resize(6)
    tchx,tchy = 0,0
end

function draw()
    background(40, 40, 50)

    perspective()
    -- camera(e.x, e.y, e.z, p.x, p.y, p.z)
    -- 用於立方體
    -- camera(300,300,600, 0,500,0, 0,0,1)
    -- 用於平面位移貼圖
    camera(WIDTH/2,HEIGHT/2,1000, WIDTH/2,HEIGHT/2,-200, 0,1,0)
    
    -- mySp:Sphere(100,100,100,0,0,0,10)
    light = vec3(tchx, tchy, 100.75)
    view = vec3(tchx, tchy, 300.75)
    -- light = vec3(300, 300, 500)
    -- rotate(ElapsedTime*5,0,1,0)
    -- m3:setRect(m2i,tchx,tchy,WIDTH/100,HEIGHT/100)

    setShaderParam(m3)
    m3:draw()   
end

function touched(touch)
    if touch.state == BEGAN or touch.state == MOVING then
        tchx=touch.x+10
        tchy=touch.y+10
    end
end

function setShaderParam(m)
    m.shader.model = modelMatrix()
    m.shader.lightPos = light 
    -- m.shader.lightPos = vec3(0.5, 1.0, 900.3)
    m.shader.viewPos = vec3(WIDTH/2,HEIGHT/2,5000) 
    m.shader.viewPos = view
    -- m.shader.viewPos = vec3(0.0, 0.0, 90.0)
    m.shader.parallax = true
    m.shader.height_scale = -0.015
end

-- 試驗 視差貼圖 中的例子
shaders = {
vs = [[
attribute vec4 position;
attribute vec3 normal;
attribute vec2 texCoord;
//attribute vec3 tangent;
//attribute vec3 bitangent;

varying vec3 vFragPos;
varying vec2 vTexCoords;
varying vec3 vTangentLightPos;
varying vec3 vTangentViewPos;
varying vec3 vTangentFragPos;

uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;
uniform mat4 modelViewProjection;

uniform vec3 lightPos;
uniform vec3 viewPos;

void main()
{
    //gl_Position = projection * view * model * position;
    gl_Position = modelViewProjection * position;
    vFragPos = vec3(model * position);   
    vTexCoords = texCoord;
    
    // 根據法線 N 計算 T,B
    vec3 tangent; 
    vec3 binormal; 

    // 使用頂點屬性法線,並歸一化
    vec3 Normal = normalize(normal*2.0-1.0);

    vec3 c1 = cross(Normal, vec3(0.0, 0.0, 1.0)); 
    vec3 c2 = cross(Normal, vec3(0.0, 1.0, 0.0)); 
    
    if ( length(c1) > length(c2) ) { tangent = c1;  } else { tangent = c2;}
    
    // 歸一化切線和次法線
    tangent = normalize(tangent);
    binormal = normalize(cross(normal, tangent)); 
    
    vec3 T = normalize(mat3(model) * tangent);
    vec3 B = normalize(mat3(model) * binormal);
    vec3 N = normalize(mat3(model) * normal);
    mat3 TBN = mat3(T, B, N);

    vTangentLightPos = lightPos*TBN;
    vTangentViewPos  = viewPos*TBN;
    vTangentFragPos  = vFragPos*TBN;
}
]],

fs = [[
precision highp float;

varying vec3 vFragPos;
varying vec2 vTexCoords;
varying vec3 vTangentLightPos;
varying vec3 vTangentViewPos;
varying vec3 vTangentFragPos;

uniform sampler2D diffuseMap;
uniform sampler2D normalMap;
uniform sampler2D depthMap;

uniform bool parallax;
uniform float height_scale;

vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir);
vec2 ParallaxMapping1(vec2 texCoords, vec3 viewDir);

// The Parallax Mapping
vec2 ParallaxMapping1(vec2 texCoords, vec3 viewDir)
{ 
    float height =  texture2D(depthMap, texCoords).r;     
    return texCoords - viewDir.xy / viewDir.z * (height * height_scale);            
}

// Parallax Occlusion Mapping
vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir)
{ 
    // number of depth layers
    const float minLayers = 10.0;
    const float maxLayers = 50.0;
    float numLayers = mix(maxLayers, minLayers, abs(dot(vec3(0.0, 0.0, 1.0), viewDir)));  
    // calculate the size of each layer
    float layerDepth = 1.0 / numLayers;
    // depth of current layer
    float currentLayerDepth = 0.0;
    // the amount to shift the texture coordinates per layer (from vector P)
    vec2 P = viewDir.xy / viewDir.z * height_scale; 
    vec2 deltaTexCoords = P / numLayers;
  
    // get initial values
    vec2  currentTexCoords     = texCoords;
    float currentDepthMapValue = texture2D(depthMap, currentTexCoords).r;
      
    while(currentLayerDepth < currentDepthMapValue)
    {
        // shift texture coordinates along direction of P
        currentTexCoords -= deltaTexCoords;
        // get depthmap value at current texture coordinates
        currentDepthMapValue = texture2D(depthMap, currentTexCoords).r;  
        // get depth of next layer
        currentLayerDepth += layerDepth;  
    }
    
    // -- parallax occlusion mapping interpolation from here on
    // get texture coordinates before collision (reverse operations)
    vec2 prevTexCoords = currentTexCoords + deltaTexCoords;

    // get depth after and before collision for linear interpolation
    float afterDepth  = currentDepthMapValue - currentLayerDepth;
    float beforeDepth = texture2D(depthMap, prevTexCoords).r - currentLayerDepth + layerDepth;
 
    // interpolation of texture coordinates
    float weight = afterDepth / (afterDepth - beforeDepth);
    vec2 finalTexCoords = prevTexCoords * weight + currentTexCoords * (1.0 - weight);

    return finalTexCoords;
}

void main()
{           
    // Offset texture coordinates with Parallax Mapping
    vec3 viewDir = normalize(vTangentViewPos - vTangentFragPos);
    vec2 texCoords = vTexCoords;
    if(parallax)
        texCoords = ParallaxMapping(vTexCoords,  viewDir);
        
    // discards a fragment when sampling outside default texture region (fixes border artifacts)
    if(texCoords.x > 1.0 || texCoords.y > 1.0 || texCoords.x < 0.0 || texCoords.y < 0.0)
       discard;

    // Obtain normal from normal map
    vec3 normal = texture2D(normalMap, texCoords).rgb;
    normal = normalize(normal * 2.0 - 1.0);   
   
    // Get diffuse color
    vec3 color = texture2D(diffuseMap, texCoords).rgb;
    // Ambient
    vec3 ambient = 0.1 * color;
    // Diffuse
    vec3 lightDir = normalize(vTangentLightPos - vTangentFragPos);
    float diff = max(dot(lightDir, normal), 0.0);
    vec3 diffuse = diff * color;
    // Specular    
    vec3 reflectDir = reflect(-lightDir, normal);
    vec3 halfwayDir = normalize(lightDir + viewDir);  
    float spec = pow(max(dot(normal, halfwayDir), 0.0), 32.0);

    vec3 specular = vec3(0.2) * spec;
    gl_FragColor = vec4(ambient + diffuse + specular, 1.0);
}
]]
}

其中法線圖(img2),高度圖(img3) 都是經過軟件 CrazyBump 根據原始紋理(img1)生成的.

你也能夠下載它們直接使用:

img1:

img2:

img3:

參考

38 視差貼圖
視差貼圖(Parallax Mapping)與陡峭視差貼圖(Steep Palallax Mapping)
Parallax Occlusion Mapping in GLSL
Jim's GameDev Blog: 置換貼圖 Displacement Mapping

相關文章
相關標籤/搜索