获取示例代码


本文将给大家介绍法线贴图的相关知识,在游戏中由于GPU资源有限,尤其是在移动设备中,所以无法使用大量的三角形来表示3D模型的细节。这时候法线贴图就成为了折中的渲染方案,既能够带来不错的细节表现效果,还可以减少资源的消耗。

未使用法线贴图的Cube
使用了法线贴图的Cube

通过上面的图可以看出,法线贴图给Cube表面增加了很多光照上的细节。

什么是法线贴图

到目前为止我们接触到的贴图只有漫反射贴图,我们通过UV从漫反射贴图中提取颜色,然后使用光照模型处理。法线贴图同样也是一张图,可以使用UV从中提取颜色,只不过我们需要把颜色转换成法线向量。下面是本文的例子使用的法线贴图。

本文的法线贴图由CrazyBump生成

是不是感觉很奇怪?下面我来揭露这张诡异贴图下隐藏的秘密。

颜色如何转换成法线向量

RGB颜色数据的范围是(0, 0, 0 )(1,1,1),所以要把它转换成法线向量,需要所有元素乘以2再减去1,这样取值范围就变成了(-1, -1, -1 )(1,1,1),这是规范化后的向量该有的取值范围。那么这个时候是不是就能直接使用它计算光照强度了呢?

法线空间

通过颜色转换过来的法线向量并不是世界空间的向量,不能直接用来计算光照。那么什么是世界空间?我们通过一个1维的例子来解释一下。下面是一个数轴,中间是原点,点A的坐标是3。我们可以称坐标3是A在世界空间的坐标。世界空间就是没有经过任何变换的数轴形成的坐标系统。

我们增加点B,它在世界空间的坐标是7,如果此时我们将A设置为原点,那么B的坐标就变成了4。我们可以说B在点A物体空间的坐标是4。我们通过B在点A物体空间中的坐标和A在世界空间的坐标就可以计算出点B在世界空间的坐标7。

我们回到3D世界,每个顶点都有一个对应的法线,我们可以使用这个法线向量和它的两个切线向量形成一个法线空间。通过颜色转换过来的法线向量正是相对于这个法线空间的值。我们使用相对于法线空间的向量值和法线空间相对于世界空间的变换就可以计算出最终的法线向量了。我们可以把通过颜色转换过来的法线向量看做上面点B在点A物体空间的坐标,法线空间则看做点A在世界空间的变换,最后计算出点B在世界空间的坐标。

计算法线空间变换

想要计算法线空间的变换需要一个法线,两个互相垂直的切线。切线和法线是垂直的,所以一个法线可以有很多个切线,为了产生较好的效果,我们选择沿着UV方向的切线。如下图所示,第一个切线Tangent沿着U,第二个切线Bitangent沿着V。(UX, VX)是各个点的UV坐标,Line10P0P1的向量。Line20P0P2的向量。他们满足下面的公式。

Line10 = (U1 - U0) * Tangent + (V1 - V0) * Bitangent
Line20 = (U2 - U0) * Tangent + (V2 - V0) * Bitangent
复制代码

我们将U1 - U0记做ΔU1,V1 - V0记做ΔV1U2 - U0记做ΔU2,V2 - V0记做ΔV2。最终可以推导出下面的公式。

求解出TangentBitangent后就可以将它们和法线组成法线空间变换TBN了,T是Tangent,B是Bitangent,N是Normal

了解完法线贴图的基础知识后,现在我们来开始实践部分。为了实现法线贴图,Shader需要做哪些事情呢?

因为法线贴图一般只对原有法线进行比较小的扰动,所以大部分的值在法线空间中z轴上分量比较多,z轴会被写到rgb的blue分量中,所以法线贴图会呈现出蓝色的主色调。

在Shader中如何使用法线贴图

首先需要在Vertex Shader中增加两个切线的attribute,并把他们传递给Fragment Shader。

attribute vec3 tangent;
attribute vec3 bitangent;
...
varying vec3 fragTangent;
varying vec3 fragBitangent;
...
void main(void) {...fragTangent = tangent;fragBitangent = bitangent;gl_Position = mvp * position;
}
复制代码

在Fragment Shader中需要做一下几件事。

  • 添加接受法线贴图的uniform,uniform sampler2D normalMap;
  • 将法线和切线都使用normalMatrix进行变换,从而变换到世界空间。
vec3 transformedNormal = normalize((normalMatrix * vec4(fragNormal, 1.0)).xyz);
vec3 transformedTangent = normalize((normalMatrix * vec4(fragTangent, 1.0)).xyz);
vec3 transformedBitangent = normalize((normalMatrix * vec4(fragBitangent, 1.0)).xyz);
复制代码
  • 使用法线和切线组成TBN矩阵。
    mat3 TBN = mat3(transformedTangent,transformedBitangent,transformedNormal);
复制代码
  • 取出法线贴图的值并使用TBN变换。
vec3 normalFromMap = (texture2D(normalMap, fragUV).rgb * 2.0 - 1.0);
transformedNormal = TBN * normalFromMap;
复制代码

接下来就是和以前一样的流程了,使用transformedNormal参与你的光照模型的计算。

为顶点计算切线

我只在WavefrontObj类中实现了切线的计算,所有的生成代码如下。

- (void)decompressToVertexArray {NSInteger vertexCount = self.positionIndexData.length / sizeof(GLuint);NSInteger triangleCount = vertexCount / 3;for (int triangleIndex = 0; triangleIndex < triangleCount; ++triangleIndex) {GLKVector3 positions[3];GLKVector2 uvs[3];GLKVector3 normals[3];for (int vertexIndex = triangleIndex * 3; vertexIndex < triangleIndex * 3 + 3; ++vertexIndex) {int positionIndex = 0;[self.positionIndexData getBytes:&positionIndex range:NSMakeRange(vertexIndex * sizeof(GLuint), sizeof(GLuint))];[self.positionData getBytes:&positions[vertexIndex % 3] range:NSMakeRange(positionIndex * 3 * sizeof(GLfloat), 3 * sizeof(GLfloat))];int normalIndex = 0;[self.normalIndexData getBytes:&normalIndex range:NSMakeRange(vertexIndex * sizeof(GLuint), sizeof(GLuint))];[self.normalData getBytes:&normals[vertexIndex % 3] range:NSMakeRange(normalIndex * 3 * sizeof(GLfloat), 3 * sizeof(GLfloat))];int uvIndex = 0;[self.uvIndexData getBytes:&uvIndex range:NSMakeRange(vertexIndex * sizeof(GLuint), sizeof(GLuint))];[self.uvData getBytes:&uvs[vertexIndex % 3] range:NSMakeRange(uvIndex * 2 * sizeof(GLfloat), 2 * sizeof(GLfloat))];}GLKVector3 deltaPos1 = GLKVector3Subtract(positions[1], positions[0]);GLKVector3 deltaPos2 = GLKVector3Subtract(positions[2], positions[0]);GLKVector2 deltaUV1 = GLKVector2Subtract(uvs[1], uvs[0]);GLKVector2 deltaUV2 = GLKVector2Subtract(uvs[2], uvs[0]);float r = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV1.y * deltaUV2.x);GLKVector3 tangent = GLKVector3MultiplyScalar(GLKVector3Subtract(GLKVector3MultiplyScalar(deltaPos1, deltaUV2.y), GLKVector3MultiplyScalar(deltaPos2, deltaUV1.y)), r);GLKVector3 bitangent = GLKVector3MultiplyScalar(GLKVector3Subtract(GLKVector3MultiplyScalar(deltaPos2, deltaUV1.x), GLKVector3MultiplyScalar(deltaPos1, deltaUV2.x)), r);for (int i = 0; i< 3; ++i) {[self.vertexData appendBytes:&positions[i] length:sizeof(GLKVector3)];[self.vertexData appendBytes:&normals[i] length:sizeof(GLKVector3)];[self.vertexData appendBytes:&uvs[i] length:sizeof(GLKVector2)];[self.vertexData appendBytes:&tangent length:sizeof(GLKVector3)];[self.vertexData appendBytes:&bitangent length:sizeof(GLKVector3)];}}
}
复制代码

以三角形为基本单位,计算它的两根切线。下面的deltaPos1对应Line10,deltaPos2对应Line20,deltaUV1和deltaUV2则是UV上的差值,读者可以对应公式理解这里的代码。

GLKVector3 deltaPos1 = GLKVector3Subtract(positions[1], positions[0]);
GLKVector3 deltaPos2 = GLKVector3Subtract(positions[2], positions[0]);
GLKVector2 deltaUV1 = GLKVector2Subtract(uvs[1], uvs[0]);
GLKVector2 deltaUV2 = GLKVector2Subtract(uvs[2], uvs[0]);
float r = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV1.y * deltaUV2.x);GLKVector3 tangent = GLKVector3MultiplyScalar(GLKVector3Subtract(GLKVector3MultiplyScalar(deltaPos1, deltaUV2.y), GLKVector3MultiplyScalar(deltaPos2, deltaUV1.y)), r);
GLKVector3 bitangent = GLKVector3MultiplyScalar(GLKVector3Subtract(GLKVector3MultiplyScalar(deltaPos2, deltaUV1.x), GLKVector3MultiplyScalar(deltaPos1, deltaUV2.x)), r);复制代码

下面是2x2和2x3矩阵的乘法规律,读者可以参考下。

切线计算完后,我们将两根切线放到顶点数据中。

for (int i = 0; i< 3; ++i) {[self.vertexData appendBytes:&positions[i] length:sizeof(GLKVector3)];[self.vertexData appendBytes:&normals[i] length:sizeof(GLKVector3)];[self.vertexData appendBytes:&uvs[i] length:sizeof(GLKVector2)];[self.vertexData appendBytes:&tangent length:sizeof(GLKVector3)];[self.vertexData appendBytes:&bitangent length:sizeof(GLKVector3)];
}
复制代码

修改顶点数据结构

我们可以发现,现在的顶点数据改变了,新增了两根法线,所以绑定VAO的代码也需要改变,下面是WavefrontObj新的genVAO代码。

- (void)genVAO {glGenVertexArraysOES(1, &vao);glBindVertexArrayOES(vao);glBindBuffer(GL_ARRAY_BUFFER, vertexVBO);GLuint positionAttribLocation = glGetAttribLocation(self.context.program, "position");glEnableVertexAttribArray(positionAttribLocation);GLuint colorAttribLocation = glGetAttribLocation(self.context.program, "normal");glEnableVertexAttribArray(colorAttribLocation);GLuint uvAttribLocation = glGetAttribLocation(self.context.program, "uv");glEnableVertexAttribArray(uvAttribLocation);GLuint tangentAttribLocation = glGetAttribLocation(self.context.program, "tangent");glEnableVertexAttribArray(tangentAttribLocation);GLuint bitangentAttribLocation = glGetAttribLocation(self.context.program, "bitangent");glEnableVertexAttribArray(bitangentAttribLocation);glVertexAttribPointer(positionAttribLocation, 3, GL_FLOAT, GL_FALSE, 14 * sizeof(GLfloat), (char *)NULL);glVertexAttribPointer(colorAttribLocation, 3, GL_FLOAT, GL_FALSE, 14 * sizeof(GLfloat), (char *)NULL + 3 * sizeof(GLfloat));glVertexAttribPointer(uvAttribLocation, 2, GL_FLOAT, GL_FALSE, 14 * sizeof(GLfloat), (char *)NULL + 6 * sizeof(GLfloat));glVertexAttribPointer(tangentAttribLocation, 3, GL_FLOAT, GL_FALSE, 14 * sizeof(GLfloat), (char *)NULL + 8 * sizeof(GLfloat));glVertexAttribPointer(bitangentAttribLocation, 3, GL_FLOAT, GL_FALSE, 14 * sizeof(GLfloat), (char *)NULL + 11 * sizeof(GLfloat));glBindVertexArrayOES(0);
}
复制代码

每个顶点的数据长度变为了14个GLfloat的长度,新增了2个切线属性的绑定,对应着我们在Vertex Shader中新增的两个属性。

贴图绑定

我们在WavefrontObj的- (void)draw:(GLContext *)glContext中增加了法线贴图的绑定。

[glContext bindTexture:self.diffuseMap to:GL_TEXTURE0 uniformName:@"diffuseMap"];
[glContext bindTexture:self.normalMap to:GL_TEXTURE1 uniformName:@"normalMap"];
复制代码

我们把漫反射贴图绑定到纹理0通道,法线贴图绑定到纹理1通道。

最后我们在ViewController中创建一个来自Obj文件的Cube模型,并给与它木箱的漫反射贴图和法线贴图。

- (void)createMonkeyFromObj {UIImage *normalImage = [UIImage imageNamed:@"normal.png"];GLKTextureInfo *normalMap = [GLKTextureLoader textureWithCGImage:normalImage.CGImage options:nil error:nil];UIImage *diffuseImage = [UIImage imageNamed:@"texture.jpg"];GLKTextureInfo *diffuseMap = [GLKTextureLoader textureWithCGImage:diffuseImage.CGImage options:nil error:nil];NSString *objFilePath = [[NSBundle mainBundle] pathForResource:@"cube" ofType:@"obj"];self.carModel = [WavefrontOBJ objWithGLContext:self.glContext objFile:objFilePath diffuseMap:diffuseMap normalMap:normalMap];self.carModel.modelMatrix = GLKMatrix4MakeRotation(- M_PI / 2.0, 0, 1, 0);[self.objects addObject:self.carModel];
}
复制代码

为了方便查看纹理贴图的效果,我增加了uniform useNormalMap来开启和关闭法线贴图。

下面是最终运行效果。

学习OpenGL ES之法线贴图相关推荐

  1. 2.x最终照着教程,成功使用OpenGL ES 绘制纹理贴图,添加了灰度图

    在之前成功绘制变色的几何图形之后,今天利用Openg ES的可编程管线绘制出第一张纹理. 学校时候不知道OpenGL的重要性,怕晦涩的语法.没有跟老师学习OpenGL的环境配置,现在仅仅能利用coco ...

  2. 从显示一张图片开始学习OpenGL ES

    前言 网上很多介绍OpenGL ES的文章,但由于OpenGL ES内容太多,所以这些文章难免过于臃肿杂乱,很难抓住重点,对于初学者来说最后还是云里雾里.很多人(包括笔者本人)开始深入了解OpenGL ...

  3. 2.x终于照着教程,成功使用OpenGL ES 绘制纹理贴图,增加了灰度图

    在之前成功绘制变色的几何图形之后,今天利用Openg ES的可编程管线绘制出第一张纹理.学校时候不知道OpenGL的重要性,怕晦涩的语法,没有跟老师学习OpenGL的环境配置,如今只能利用cocos2 ...

  4. 从零开始学习OpenGL ES之五 – 材质

    从零开始学习OpenGL ES之五 – 材质 作者: iPhoneGeek 爱疯极客 09-Jan-10 iPhone Development 浏览次数: 411 |  评论 ↓ Tweet Shar ...

  5. OpenGL shader normals法线贴图的实例

    OpenGL shader normals法线贴图 先上图,再解答. 完整主要的源代码 源代码剖析 先上图,再解答. 完整主要的源代码 #include <glad/glad.h> #in ...

  6. 【OpenGL ES】凸镜贴图

    1 前言 正方形图片贴到圆形上 中将正方形图片上的纹理映射到圆形模型上,同理,也可以将圆形上的纹理映射到凸镜的球形曲面上.如下图,最左边的竖条是原图片的截面(纹理坐标),最右边的竖条是变换后的顶点模型 ...

  7. 【OpenGL ES】立方体贴图(6张图)

    1 前言 本文通过一个立方体贴图的例子,讲解三维纹理贴图的应用,案例中使用 6 张不同的图片给立方体贴图,图片如下: 本文涉及到的知识点主要包含:三维绘图.MVP 矩阵变换.纹理贴图,读者如果对 Op ...

  8. [转载]从零开始学习OpenGL ES之八 – 交叉存取顶点数据

    Technote 2230提出了很多用OpenGL ES来提升iphone程序性能的建议.我们现在远远不能深刻理解OpenGL ES所以你需要学习以下内容.不信?是真的,试试看,我等着你的读后感. 好 ...

  9. 学习OpenGL ES之透明和混合

    获取示例代码 本文主要讲解OpenGL ES对于透明颜色的处理,在例子中我绘制了三个平面,分别赋予绿色半透明纹理,红色半透明纹理,和不透明纹理. 首先为这三张图生成纹理. - (void)genTex ...

  10. OpenGL生成的法线贴图并增加光照

    这一篇将由OpenGL生成法线贴图的基础上再增加光照效果. 思路如下: 准备一张墙壁图片A. 通过A自动生成法线贴图. 设计一个平行光,指定平行光的光照颜色和光照方向. 使用漫反射光照公式,法线贴图和 ...

最新文章

  1. Oracle alter table详解
  2. ibatis 的 This SQL map does not contain a MappedStatement的错误
  3. python打开setting_Django自带日志 settings.py文件配置方法
  4. 一段个性化stringgrid的代码
  5. redis专题:redis的持久化方式有哪些?redis数据的备份和恢复策略
  6. python 字符串find方法怎么用_Python字符串find()方法
  7. python rest api 框架_Python Eve REST API框架
  8. 使用echarts实现半圆饼图
  9. 清代徽州家政与乡族社会的善治
  10. 《数据库原理与应用》习题
  11. linux命令cd 什么意思,Linux命令 cd ./.是什么意思
  12. android的筛选功能,android实现筛选菜单效果
  13. 怎么用cmd打开python
  14. 书到用时方恨少,一大波JS开发工具函数来了
  15. “天空起重机”助力好奇号着陆盖尔撞击坑
  16. 线性代数笔记【空间曲面】
  17. wps的计算机在哪里设置密码,怎么在电脑版WPS中修改密码?
  18. ESP32开发三_蓝牙开发
  19. windows键盘事件处理
  20. 【代码复用之】登录注册原生代码

热门文章

  1. 类中添加log4j日志
  2. 关于移动端设备适配的问题
  3. JS中style属性
  4. Windows上编译zlib
  5. 微软为“离线”做好准备:推出同步框架
  6. jumpserver跳板机docker安装小小趟坑
  7. Response 与 Cookie
  8. [转][python] 常用正则表达式爬取网页信息及分析HTML标签总结
  9. 推荐使用MEF降低耦合(2)
  10. Extjs中设置只读的样式问题