最近一直在研究如何在个人 iPad 2(只支持 OpenGL ES 2.0, 不支持 3.0) 上实现 视差贴图(Parallax Mapping) 和 位移贴图(Displacement Mapping).php
iPad 2
OpenGL ES 2.0
3.0
视差贴图
Parallax Mapping
位移贴图
Displacement Mapping
通过一番研究, 搜索阅读了很多文章, 终于肯定, OpenGL ES 2.0 能够支持 视差贴图, 不过暂时还没什么好办法支持 位移贴图.html
由于就我目前所了解的位移贴图, 有这么两种方法来实现, 一种是用 Tessellation 来提供多面数网格, 另外一种是在顶点着色器中对高度图进行纹理采样来计算对应的顶点偏移量.git
Tessellation
第一种方法就没必要想了, 由于目前移动设备的 OpenGL ES 2.0/3.0 都不支持(貌似 DX11 和 OpenGL 4.0 才支持), 而 OpenGL ES 3.0 衍生自 OpenGL 3.3.github
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 支持的.算法
2.0
咱们能够看看 Jim 在 3.0 设备上实现位移贴图的效果:app
Jim
原始图:ide
使用位移贴图后的效果:函数
好了, 如今在咱们的 2.0 设备上实现咱们的 视差贴图 吧, 先看看效果:.net
使用不一样参数的效果:3d
你能够灵活调整这些参数:
看看这个视频 video:
关键技术点就这么几个:
若是你使用比较大的引擎, 好比 Unity, 它会帮你计算好法线,切线和次法线, 若是本身开发, 没有使用这些引擎, 那么极可能就须要本身手动构造了.
Unity
法线
切线
次法线
目前我发现有 3 种根据法线手动计算 TBN 的近似算法, 其中一种既能在 OpenGL ES 2.0 的顶点着色器内使用, 也能在片断着色器内使用, 就是咱们下面要提到的这种, 主要原理就是已知了法线 Normal, 要据此求出对应的切线 Tangent 和 次法线 Binormal, 由于它们两两垂直, 并且 TB 跟 UV 对齐, 所以很容易求得 T, 再根据 T 和 N 求得 B, 算法代码以下:
3
TBN
Normal
Tangent
Binormal
TB
UV
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 矩阵后, 既能够把其余向量从其余空间变换进正切空间来, 也能够把正切空间的向量变换到其余空间去. 一般意义的作法是:
不过在 OpenGL 中, 你把矩阵放在向量左边乘, 就是正向变换, 它会按列矩阵处理; 你把矩阵放在向量右边乘就是反向变换, 它会按行矩阵处理. 这样就不须要再进行矩阵求逆的操做了.
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 代码具备很好的移植性, 能够轻松地把它用在其余地方.
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