在 OpenGL ES 2.0 上實現視差貼圖(Parallax Mapping)
視差貼圖
最近一直在研究如何在我的 iPad 2
(只支持 OpenGL ES 2.0
, 不支持 3.0
) 上實現 視差貼圖
(Parallax Mapping
) 和 位移貼圖
(Displacement Mapping
).
經過一番研究, 搜索閱讀了不少文章, 終於確定, OpenGL ES 2.0
可以支持 視差貼圖
, 不過暫時還沒什么好辦法支持 位移貼圖
.
因為就我目前所了解的位移貼圖
, 有這么兩種方法來實現, 一種是用 Tessellation
來提供多面數網格, 另一種是在頂點着色器中對高度圖進行紋理采樣來計算對應的頂點偏移量.
第一種方法就不必想了, 因為目前移動設備的 OpenGL ES 2.0/3.0
都不支持(貌似 DX11
和 OpenGL 4.0
才支持), 而 OpenGL ES 3.0
衍生自 OpenGL 3.3
.
第二種方法目前看起來只能在 OpenGL ES 3.0
上使用, 請參考這篇文檔Jim's GameDev Blog: 置換貼圖 Displacement Mapping. 不過沒辦法在 OpenGL ES 2.0
上使用, 因為它要求在頂點着色器中進行紋理采樣, 而這個特性恰恰是 2.0
不支持, 3.0
支持的.
我們可以看看 Jim
在 3.0
設備上實現位移貼圖
的效果:
原始圖:
使用位移貼圖
后的效果:
好了, 現在在我們的 2.0
設備上實現我們的 視差貼圖
吧, 先看看效果:
使用不同參數的效果:
- 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
, 因為它們兩兩垂直, 而且 TB
跟 UV
對齊, 因此很容易求得 T
, 再根據 T
和 N
求得 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