在 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 都不支持(貌似 DX11OpenGL 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 支持的.

我们可以看看 Jim3.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, 因为它们两两垂直, 而且 TBUV 对齐, 因此很容易求得 T, 再根据 TN 求得 B, 算法代码如下:

  1. // 根据法线 N 计算 T,B
  2. vec3 tangent;
  3. vec3 binormal;
  4. // 使用顶点属性法线,并归一化
  5. vec3 Normal = normalize(normal*2.0-1.0);
  6. // 通过叉积来计算夹角
  7. vec3 c1 = cross(Normal, vec3(0.0, 0.0, 1.0));
  8. vec3 c2 = cross(Normal, vec3(0.0, 1.0, 0.0));
  9. // 方向朝外的是我们要的
  10. if ( length(c1) > length(c2) ) { tangent = c1; } else { tangent = c2;}
  11. // 归一化切线和次法线
  12. tangent = normalize(tangent);
  13. binormal = normalize(cross(normal, tangent));
  14. vec3 T = normalize(mat3(model) * tangent);
  15. vec3 B = normalize(mat3(model) * binormal);
  16. vec3 N = normalize(mat3(model) * normal);
  17. // 构造出 TBN 矩阵
  18. mat3 TBN = mat3(T, B, N);

得到 TBN 矩阵后, 既可以把其他向量从其他空间变换进正切空间来, 也可以把正切空间的向量变换到其他空间去. 通常意义的做法是:

  • TBN 用于正向变换
  • TBN 的逆阵用于反向变换

不过在 OpenGL 中, 你把矩阵放在向量左边乘, 就是正向变换, 它会按列矩阵处理; 你把矩阵放在向量右边乘就是反向变换, 它会按行矩阵处理. 这样就不需要再进行矩阵求逆的操作了.

视差映射函数

视差贴图 的本质就是根据高度纹理图的不同高度以及视线向量的坐标, 来实时计算纹理坐标在视线下的偏移, 并以此作为新的纹理坐标来从纹理贴图中进行取样.

代码如下:

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

在此基础上提出的 视差遮掩 可以提供更好的视觉效果, 具体原理在代码注释中.

光照模型

最后就是一个非常简单的光照模型, 先从原始纹理中取样, 以此为基础, 缩小10倍作为环境光, 根据前面计算得到的切线来计算光线摄入方向, 再结合法线可以计算出漫射光和反射光, 最后把这些光线混合就得到最终的光照颜色值了.

这个光照模型的好处是简单易用, 不需要另外设置过多参数, 坏处就是不太灵活, 实际使用时可以把参数设置为可变, 或者直接换成其他光照模型也可以(具体的参数值就需要自己调整了).

因为代码自己计算构造了 TBN 变换矩阵, 所以这段 shader 代码具有很好的移植性, 可以轻松地把它用在其他地方.

完整代码

代码如下:

  1. function setup()
  2. displayMode(OVERLAY)
  3. print("试验 OpenGL ES 2.0 中位移贴图的例子")
  4. print("Test the Parallax Mapping in OpenGL ES 2.0")
  5. img1 = readImage("Dropbox:dm")
  6. img2 = readImage("Dropbox:dnm1")
  7. img3 = readImage("Dropbox:dm1")
  8. local w,h = WIDTH,HEIGHT
  9. local c = color(223, 218, 218, 255)
  10. m3 = mesh()
  11. m3i = m3:addRect(w/2,h/2,w/1,h/1)
  12. m3:setColors(c)
  13. m3.texture = img1
  14. m3:setRectTex(m3i,0,0,1,1)
  15. m3.shader = shader(shaders.vs,shaders.fs)
  16. m3.shader.diffuseMap = img1
  17. m3.shader.normalMap = img2
  18. m3.shader.depthMap = img3
  19. -- local tb = m3:buffer("tangent")
  20. -- tb:resize(6)
  21. tchx,tchy = 0,0
  22. end
  23. function draw()
  24. background(40, 40, 50)
  25. perspective()
  26. -- camera(e.x, e.y, e.z, p.x, p.y, p.z)
  27. -- 用于立方体
  28. -- camera(300,300,600, 0,500,0, 0,0,1)
  29. -- 用于平面位移贴图
  30. camera(WIDTH/2,HEIGHT/2,1000, WIDTH/2,HEIGHT/2,-200, 0,1,0)
  31. -- mySp:Sphere(100,100,100,0,0,0,10)
  32. light = vec3(tchx, tchy, 100.75)
  33. view = vec3(tchx, tchy, 300.75)
  34. -- light = vec3(300, 300, 500)
  35. -- rotate(ElapsedTime*5,0,1,0)
  36. -- m3:setRect(m2i,tchx,tchy,WIDTH/100,HEIGHT/100)
  37. setShaderParam(m3)
  38. m3:draw()
  39. end
  40. function touched(touch)
  41. if touch.state == BEGAN or touch.state == MOVING then
  42. tchx=touch.x+10
  43. tchy=touch.y+10
  44. end
  45. end
  46. function setShaderParam(m)
  47. m.shader.model = modelMatrix()
  48. m.shader.lightPos = light
  49. -- m.shader.lightPos = vec3(0.5, 1.0, 900.3)
  50. m.shader.viewPos = vec3(WIDTH/2,HEIGHT/2,5000)
  51. m.shader.viewPos = view
  52. -- m.shader.viewPos = vec3(0.0, 0.0, 90.0)
  53. m.shader.parallax = true
  54. m.shader.height_scale = -0.015
  55. end
  56. -- 试验 视差贴图 中的例子
  57. shaders = {
  58. vs = [[
  59. attribute vec4 position;
  60. attribute vec3 normal;
  61. attribute vec2 texCoord;
  62. //attribute vec3 tangent;
  63. //attribute vec3 bitangent;
  64. varying vec3 vFragPos;
  65. varying vec2 vTexCoords;
  66. varying vec3 vTangentLightPos;
  67. varying vec3 vTangentViewPos;
  68. varying vec3 vTangentFragPos;
  69. uniform mat4 projection;
  70. uniform mat4 view;
  71. uniform mat4 model;
  72. uniform mat4 modelViewProjection;
  73. uniform vec3 lightPos;
  74. uniform vec3 viewPos;
  75. void main()
  76. {
  77. //gl_Position = projection * view * model * position;
  78. gl_Position = modelViewProjection * position;
  79. vFragPos = vec3(model * position);
  80. vTexCoords = texCoord;
  81. // 根据法线 N 计算 T,B
  82. vec3 tangent;
  83. vec3 binormal;
  84. // 使用顶点属性法线,并归一化
  85. vec3 Normal = normalize(normal*2.0-1.0);
  86. vec3 c1 = cross(Normal, vec3(0.0, 0.0, 1.0));
  87. vec3 c2 = cross(Normal, vec3(0.0, 1.0, 0.0));
  88. if ( length(c1) > length(c2) ) { tangent = c1; } else { tangent = c2;}
  89. // 归一化切线和次法线
  90. tangent = normalize(tangent);
  91. binormal = normalize(cross(normal, tangent));
  92. vec3 T = normalize(mat3(model) * tangent);
  93. vec3 B = normalize(mat3(model) * binormal);
  94. vec3 N = normalize(mat3(model) * normal);
  95. mat3 TBN = mat3(T, B, N);
  96. vTangentLightPos = lightPos*TBN;
  97. vTangentViewPos = viewPos*TBN;
  98. vTangentFragPos = vFragPos*TBN;
  99. }
  100. ]],
  101. fs = [[
  102. precision highp float;
  103. varying vec3 vFragPos;
  104. varying vec2 vTexCoords;
  105. varying vec3 vTangentLightPos;
  106. varying vec3 vTangentViewPos;
  107. varying vec3 vTangentFragPos;
  108. uniform sampler2D diffuseMap;
  109. uniform sampler2D normalMap;
  110. uniform sampler2D depthMap;
  111. uniform bool parallax;
  112. uniform float height_scale;
  113. vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir);
  114. vec2 ParallaxMapping1(vec2 texCoords, vec3 viewDir);
  115. // The Parallax Mapping
  116. vec2 ParallaxMapping1(vec2 texCoords, vec3 viewDir)
  117. {
  118. float height = texture2D(depthMap, texCoords).r;
  119. return texCoords - viewDir.xy / viewDir.z * (height * height_scale);
  120. }
  121. // Parallax Occlusion Mapping
  122. vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir)
  123. {
  124. // number of depth layers
  125. const float minLayers = 10.0;
  126. const float maxLayers = 50.0;
  127. float numLayers = mix(maxLayers, minLayers, abs(dot(vec3(0.0, 0.0, 1.0), viewDir)));
  128. // calculate the size of each layer
  129. float layerDepth = 1.0 / numLayers;
  130. // depth of current layer
  131. float currentLayerDepth = 0.0;
  132. // the amount to shift the texture coordinates per layer (from vector P)
  133. vec2 P = viewDir.xy / viewDir.z * height_scale;
  134. vec2 deltaTexCoords = P / numLayers;
  135. // get initial values
  136. vec2 currentTexCoords = texCoords;
  137. float currentDepthMapValue = texture2D(depthMap, currentTexCoords).r;
  138. while(currentLayerDepth < currentDepthMapValue)
  139. {
  140. // shift texture coordinates along direction of P
  141. currentTexCoords -= deltaTexCoords;
  142. // get depthmap value at current texture coordinates
  143. currentDepthMapValue = texture2D(depthMap, currentTexCoords).r;
  144. // get depth of next layer
  145. currentLayerDepth += layerDepth;
  146. }
  147. // -- parallax occlusion mapping interpolation from here on
  148. // get texture coordinates before collision (reverse operations)
  149. vec2 prevTexCoords = currentTexCoords + deltaTexCoords;
  150. // get depth after and before collision for linear interpolation
  151. float afterDepth = currentDepthMapValue - currentLayerDepth;
  152. float beforeDepth = texture2D(depthMap, prevTexCoords).r - currentLayerDepth + layerDepth;
  153. // interpolation of texture coordinates
  154. float weight = afterDepth / (afterDepth - beforeDepth);
  155. vec2 finalTexCoords = prevTexCoords * weight + currentTexCoords * (1.0 - weight);
  156. return finalTexCoords;
  157. }
  158. void main()
  159. {
  160. // Offset texture coordinates with Parallax Mapping
  161. vec3 viewDir = normalize(vTangentViewPos - vTangentFragPos);
  162. vec2 texCoords = vTexCoords;
  163. if(parallax)
  164. texCoords = ParallaxMapping(vTexCoords, viewDir);
  165. // discards a fragment when sampling outside default texture region (fixes border artifacts)
  166. if(texCoords.x > 1.0 || texCoords.y > 1.0 || texCoords.x < 0.0 || texCoords.y < 0.0)
  167. discard;
  168. // Obtain normal from normal map
  169. vec3 normal = texture2D(normalMap, texCoords).rgb;
  170. normal = normalize(normal * 2.0 - 1.0);
  171. // Get diffuse color
  172. vec3 color = texture2D(diffuseMap, texCoords).rgb;
  173. // Ambient
  174. vec3 ambient = 0.1 * color;
  175. // Diffuse
  176. vec3 lightDir = normalize(vTangentLightPos - vTangentFragPos);
  177. float diff = max(dot(lightDir, normal), 0.0);
  178. vec3 diffuse = diff * color;
  179. // Specular
  180. vec3 reflectDir = reflect(-lightDir, normal);
  181. vec3 halfwayDir = normalize(lightDir + viewDir);
  182. float spec = pow(max(dot(normal, halfwayDir), 0.0), 32.0);
  183. vec3 specular = vec3(0.2) * spec;
  184. gl_FragColor = vec4(ambient + diffuse + specular, 1.0);
  185. }
  186. ]]
  187. }

其中法线图(img2),高度图(img3) 都是通过软件 CrazyBump 根据原始纹理(img1)生成的.

你也可以下载它们直接使用:

img1:

img2:

img3:

参考

38 视差贴图

视差贴图(Parallax Mapping)与陡峭视差贴图(Steep Palallax Mapping)

Parallax Occlusion Mapping in GLSL

Jim's GameDev Blog: 置换贴图 Displacement Mapping

在 OpenGL ES 2.0 上实现视差贴图(Parallax Mapping)的更多相关文章

  1. 【AR实验室】OpenGL ES绘制相机(OpenGL ES 1.0版本)

    0x00 - 前言 之前做一些移动端的AR应用以及目前看到的一些AR应用,基本上都是这样一个套路:手机背景显示现实场景,然后在该背景上进行图形学绘制.至于图形学绘制时,相机外参的解算使用的是V-SLA ...

  2. OpenGL ES 3.0 基础知识

    首先要了解OpenGL的图形管线有哪些内容,再分别去了解其中的相关的关系: 管线分别包括了顶点缓冲区/数组对象,定点着色器,纹理,片段着色器,变换反馈,图元装配,光栅化,逐片段操作,帧缓冲区.其中顶点 ...

  3. 利用JNI技术在Android中调用C++形式的OpenGL ES 2.0函数

    1.                 打开Eclipse,File-->New-->Project…-->Android-->AndroidApplication Projec ...

  4. [置顶] 使用Android OpenGL ES 2.0绘图之五:添加运动

    传送门 ☞ 系统架构设计 ☞ 转载请注明 ☞ http://blog.csdn.net/leverage_1229 传送门 ☞ GoF23种设计模式 ☞ 转载请注明 ☞ http://blog.csd ...

  5. OpenGL ES 2.0 渲染管线 学习笔记

    图中展示整个OpenGL ES 2.0可编程管线 图中Vertex Shader和Fragment Shader 是可编程管线: Vertex Array/Buffer objects 顶点数据来源, ...

  6. 【Android 应用开发】OpenGL ES 2.0 -- 制作 3D 彩色旋转三角形 - 顶点着色器 片元着色器 使用详解

    最近开始关注OpenGL ES 2.0 这是真正意义上的理解的第一个3D程序 , 从零开始学习 . 案例下载地址 : http://download.csdn.net/detail/han120201 ...

  7. 一步步实现windows版ijkplayer系列文章之六——SDL2源码分析之OpenGL ES在windows上的渲染过程

    一步步实现windows版ijkplayer系列文章之一--Windows10平台编译ffmpeg 4.0.2,生成ffplay 一步步实现windows版ijkplayer系列文章之二--Ijkpl ...

  8. OpenGL ES 2.0 Shader 调试新思路(二): 做一个可用的原型

    OpenGL ES 2.0 Shader 调试新思路(二): 做一个可用的原型 目录 背景介绍 请参考前文OpenGL ES 2.0 Shader 调试新思路(一): 改变提问方式 优化 ledCha ...

  9. OpenGL ES 2.0 Shader 调试新思路(一): 改变提问方式

    OpenGL ES 2.0 Shader 调试新思路(一): 改变提问方式 --是什么(答案是具体值) VS 是不是(答案是布尔值) 目录 背景介绍 问题描述 Codea 是 iPad 上的一款很方便 ...

随机推荐

  1. Android--TextView 文字显示和修改

    一. 新建一个Activity 和 Layout 首先在layout文件夹中新建一个activity_main.xml,在新建工程的时候一般默认会新建此xml文件,修改其代码如下: <Relat ...

  2. angular(常识)

    我觉得angularjs是前端框架,而jquery只是前端工具,这两个还是没有可比性的. 看知乎上关于jquery和angular的对比http://www.zhihu.com/question/27 ...

  3. Nginx下用webbench进行压力测试

    在运维工作中,压力测试是一项非常重要的工作.比如在一个网站上线之前,能承受多大访问量.在大访问量情况下性能怎样,这些数据指标好坏将会直接影响用户体验. 但是,在压力测试中存在一个共性,那就是压力测试的 ...

  4. 使用Git进行代码管理

    Git简介 Git 是 Linux Torvalds 为了帮助管理 Linux® 内核开发而开发的一个开放源码的版本控制软件. 先讲一下如何把开源项目fork到自己的github中 1.  点击图中的 ...

  5. photoshop将psd导出div+css格式HTML(自动)

    psd切片切好后,导出 web格式,存储时选择html.所有切片,然后,选择其他,选择自定,选择切片,选择生成css css命名有2种方式,根据ID和根据类,一般选择根据类(ID尽量少有,防止js要用 ...

  6. JMeter 测试Web登录

    JMeter测试 前置条件: 1安装JMeter 下载地址:http://jmeter.apache.org/ 2安装badBoy http://www.badboy.com.au/download/ ...

  7. Java 并发-访问量

    有几个常用的措施 1.对常用功能建立缓存模块 .尽量使用缓存,包括用户缓存,信息缓存等,多花点内存来做缓存,可以大量减少与数据库的交互,提高性能.统计的功能尽量做缓存,或按每天一统计或定时统计相关报表 ...

  8. ztree点击文字勾选checkbox,radio实现方法

    ztree的复选框checkbok,单选框radio是用背景图片来模拟的,所以点击文字即使用label括起checkbox,radio文字一起,点击文字也是无法勾选checkbox. 要想点击ztre ...

  9. 9.Android之日期对话框DatePicker控件学习

    设置日期对话框在手机经常用到,今天来学习下. 首先设置好布局文件:如图 xml对应代码 <?xml version="1.0" encoding="utf-8&qu ...

  10. 轻量级应用开发之(06)Autolayout自动布局1

    一 什么是Autolayout Autolayout是一种“自动布局”技术,专门用来布局UI界面的. 自IOS7 (Xcode 5)开始,Autolayout的开发效率得到很大的提高. 苹果官方也推荐 ...