bump Mapping Using CG(3rd Edition)

bump Mapping Using CG(3rd Edition)

 

你可能听说许多人做过bump mapping,并且在doom 3Half-life中见过很酷的效果。而现在,你可能独自坐着、旁边放着你的新显卡、心里有个这样的疑问:我是否能在自己的游戏中实现同样的效果。

好,今天是你的幸运日。我最近读了很多关于bump mapping书,并且连续在互联网上搜集了很多资料,我认为最终我发现了许多很棒的教程。然而这些教程包含了许多不必要不相关的代码,这就需要你从中甄选出真材实料!因此,最终我决定自己做一个教程。这里,我尽可能解释的多一点,但是如果你已经知道了一些基础理论, 那么你可以速读理论部分,然后直接进入到代码。这个教程主要的目的就是让尽可能多的人读得懂!

这个教程用OpenGLNvidiaCG shader语言实现。之所以用CG是因为它和C语言很像,因此很容易使用。个人认为,用CG来实现光影特效比用纯OpenGL扩展实现要简单。

总之,不再废话了,让我们直捣黄龙:什么是bump mapping和我们如何创造它!

不承诺

这个教程里用到的石头纹理是从NeHe's 关于bump mapping的论文里拿来的,在引用[4]中可以看到。纹理对应的normal map是利用高度图到normal map的转换器从我自己的一张高度图中转来的。

开始之前你需要知道的

l 对如何建立一个OpenGL窗口和OpenGL是如何运行起来的有一个基本的认识,包括模型视图和投影视图.

l 基本的3D数学知识,包括对向量和矩阵的基本认识.

所需的软件

NvdiaCG工具库,你可以从这里获得http:://developer.nvidia.com/object/cg_toolkit.html.

包含在工具库里的CG 使用手册详细地介绍了语言和运行时,是一个很好的参考。在你开始CG编程之前,很有必要读一下其中"CG语言介绍""运用CG运行时库"两部分内容。

OglExt,会让使用OpenGl扩展十分容易:http://www.julius.caesar.de/index.php/OglExt.

什么是Bump mapping

通俗来说,bump mapping就是让2D纹理看起来像处于3D中一样的艺术,如下图所示:

 “逐像素光照”bump mapping“这两个名词经常一块出现,因为我们经常用bump mapping来计算给定纹理上任意一像素点上的光照强度。

背后的数学

想象一张原始的2D纹理。这张纹理的表面完全是平的,因此纹理表面的法线全部垂直向上,如同下图中所示。

这就是为什么一个游戏中的木头盒子看起来好像没有深度;这就是我们运用讨人厌的2D纹理的后果。 

这种问题的解决方法就是当我们渲染的时候,依次改变纹理周围的光照强度。

看一下右上角的图片(Bump Mapping Picture),并想象在图片的左上角有一个光源。就像你看到的,这导致面向光源的边角石头看起来比远离光源的那些石头要亮一些。那正是我们要做的。

我们想着给定纹理一个假深度,我们能够计算出每一个像素上的颜色强度,并且带给玩家一种他们面对的是真正的3D石头那种感觉。 为了能够计算每一个像素点的光照强度,我们需要知道每一个像素点的法线,这就是normal map的由来。

normal map里保存着每个像素点法线的朝向,下面是一张真是的normal map(第一眼看起来可能有点怪): 

注意看下我再那张纹理左下角画的3个坐标轴。X轴指向右边,Y轴指向上边,Z轴指向外屏幕。X轴为红色,Y轴为绿色。Z轴为蓝色。如果你看的足够仔细,你能够发现:垂直于X轴正向的边缘是红色,垂直于Y轴正向的是绿色,垂直表面的边缘是蓝色。因为纹理中大部分的法线是指出纹理的(和Z轴正向一致),这就解释了为什么normal map大部分的颜色是蓝色

我们需要做得就是把每个像素点的颜色翻译成那个位置上法线的朝向。法线朝向同样由3个轴向定义[x,y,z],3个坐标取值范围为[-1,1].但是纹理的RGB颜色取值范围是[0,1],我们把R分量当作X轴,G分量当作Y轴,B分量当作Z轴。因此需要把颜色值从[0,1] 映射到[-1,1].

我们把这个过程被称作将颜色值解压缩到normal map上。等式如下:x = 2.0 * (colorValue - 0.5). 现在我们有方法从normal map读取和翻译成每一点的法线朝向了,下面我们继续

光照模型是处理动态光照前必须了解的。它长这样: I = A + Att * (D + S),其中: 

l I 是最终的颜色强度 

l A是漫反射颜色。这就是说为了还原真实世界并且避免出现暗色区域,我们总是为每一个像素增加一个默认的光照。 

l Att是光照的衰减。所有光源都有光能到达的最大值,这个距离决定着基于光源中心位置我们应该如何衰减当前的像素

l D diffuse color,及光照到物体上让你真实看到的颜色 

l S specular color,及物体的镜面反射颜色

如果你想要光照模型更详细的一些描述或者更加正确的一些数学解释,可看一下参考[1],那个写得相当出色!

为了让开始更容易一些,我们简化一下光照模型: I = D

那也就是说,开始我们所需处理的只是diffuse部分,如下面的定义: I = D1 * Dm * clamp(LN,0,1), 其中:

l D1是光的diffuse颜色

l Dm是物体材质的diffuse颜色

l L是物体表面到光源的向量(光方向向量)

l N是当前像素的表面法线 LNLN的点乘结果

下面这幅图很好的阐述了这个概念(从www.delphi3d.net中得来,参见[1]

根据上面的光照模型等式,我们需要3个参数去正确得计算得到给定像素点的正确光照强度。 

l 让我们从LN的点乘结果开始。 

l 用当前顶点的位置减去光源的位置,可以很容易得到L

l N更容易得到,因为它就是从我们之前讨论的normal map中提取的。 

l Dm是当前像素的材质颜色。这里我们用当前的纹理颜色。例如,如果你看一下石头的纹理,Dm很可能取值成浅灰色的石头区域。 

l D1是光源本身的颜色,一般是白色。

好,一切进展顺利。我们几乎接触到了bump mapping背后的所有理论。还有一小方面非常重要,需要我们注意。

作为一个OpenGL程序员,你可能早就知道modelviewProjection矩阵。你用这连个矩阵把一个顶点从物体坐标系转换到裁剪坐标系。物体坐标系是你编程时处理的坐标系。比如当你用glTranslatef(10.0f,0.0f,-20.0f)放置物体位置的时候。为了在设备上正确得渲染物体我们需要把所有的物体坐标系中的顶点转换到裁剪坐标系中。这需要把物体坐标系中的顶点诚意modelviewprojection矩阵(尽管当我们不用shaders的时候,OpenGl自动帮我们做了这些)。

 如果要深入讨论这些,我们可以查阅下参考[2]参考[3]。

下面这张图标识了不同物体空间的关系以及如何在他们之间转换。 

你可能想知道什么是tangent space,以及为什么我们需要它。如果我们所要做得仅仅是在物体空间内直接指定顶点。 tangent space和物体表面相关,如下图所示: 把切线S当作X轴,切线T当作Y轴,法线当作Z轴指向屏幕外。

如同你在图中看到的,表面的每一个顶点都有他们自己的tangent空间(总共四个顶点,一个角落一个)。顶点的三个轴向定义了空间叫做tangent 空间(如果你想,也可以叫texture space)。如果你把这些轴放到一个矩阵里,那么你就得到了TBN矩阵(TangentBinormalNormal),其中Tangent是切线SBinormal是切线T: 

如果你把切线空间内的一点乘以这个切线矩阵,那么你就能把这一点转换进物体空间。参考[6]去看一下tangent space的详细解释。

为了计算给定顶点的tangent basis,我们利用参考[6]中提到的等式。我们把这个等式提出来备用,这里不在讨论它的来历(参考[6]中会提到):

对那些线性代数不强的同学,我们这里给出了TB的计算方法:

 

其中: 

就像前面提到的,当讨论到normal map和光照等式的时候,我们想要得到表面任意点的法线和光方向向量的点乘。表面发现存在与tangent space,同时光方向向量存在于 object space。因此,为了让点乘有意义,我们需要把这两个向量至于同一个坐标系,因此我们有如下的两种选项:

1. 把光向量置于tangent space.

2. normal map中所有的表面法线置于object space.

很明显,第一种方式更好,因为我们仅仅把一个向量做一次转换,而另外一种是把normal map中所有的发现转换到object space。鉴于TBN矩阵是把从tangent space 转换到object space,因此我们需要反过来计算(把光向向量从object space 转换到tangent space)。我们需要TBN矩阵的逆矩阵,如下所示

其中有一个可优化的地方。

如同在参考[6]中提到的,如果我们仅仅处理三角形,那么我们对每个三角形仅仅计算一次TBN矩阵的逆矩阵,而不是我们先前提到过的每个顶点。然而,很有必要强调:任何时候你需要旋转一个三角形,你都要计算他的TBN矩阵的逆矩阵。然而移动一个三角形不需要重复计算。

所以,我们所需要做得,就是把光向矩阵乘以TBN矩阵的逆矩阵,然后我们就得到了tangent space 中的光向,继而就能做点乘运算。

当然,这里,我们能够把矩阵转置得到TBN的逆矩阵。因为TBN矩阵是由三个正交向量组成(TBN vectors)。想像一下,如果你有一个物体,比如一个球,由许多个三角形组成。那种情况下,你不需要法线正交于三角形面而是需要发现正交于球面。那意味着,法线是基于顶点的,而不是基于三角形的。因此,我们需要针对每一个顶点计算TBN矩阵。

上面我们提到了通用的处理过程。但是,你一定要记住:如果你的模型是是flat shaded(法线基于三角形)而不是smooth shaded(法线基于顶点),那么要计算TBN矩阵的逆矩阵仅仅需要转置它。

下面,让我们总结一下bump mapping的处理过程:

l 计算所有三角形的TBN矩阵(仅仅需要开始时做一次,以后在有旋转发生时再做)

l 计算光向,并把它从物体空间转到切线空间

l 从normal map中得到切线向量,做解压缩,把它的范围变到[-1,1],而不是[0,1]

l 计算最终的diffuse color:光向和法线点乘,然后乘以光的颜色,乘以物体表面材质的颜色

l 对给定表面的所有像素重复那个过程

什么是CG

CGNvida在shader语言领域的一个尝试。shader 就是你能够下载到图形显卡GPU上的一段小程序。因为可以直接运行在GPU上,并且能够直接访问到贴图和矩阵,因此shader非常强大。

CG的运行时

有两种shader: 顶点shader和像素shader。 每次我们调用glVertex3f()函数在屏幕上花顶点时都会调用到顶点shader。想像一下我们绘制如下三角形,这会产生3glVertex3f()调用。这就导致调用了3次顶点shader。然后到我们在三个顶点之间进行绘制的时候,我们会插值填入蓝色,直至所有东西绘制完成。对于每个三角形上插值的每一个像素而言,都会调用到一次像素shader

基本上,顶点shader负责把顶点变换到我们需要的空间中去。例如,把光向向量从物体空间变换到tangent 空间,最好的地方是顶点shader。另一方面,像素shader是真正魔法发生的地方。因为,我们为每一像素进行bump mapping计算过程时,调用的正是像素shader

为了在你的程序里面成功的实现CG,我们需要放进一些很简单的strings

这里不再对CG语言做更深的阐述了(因为你能够而且应该读一下CG的操作手册)。下面我将会快速阐述一下建立基本的CG程序的几个步骤:

首先我们要处理一个CG上下文。一个CG上下文包含了多个CG程序。一个CG程序要么是顶点shader要么是像素shader。当我们初始化程序时,我们将要建立CG上下文,创建两个CG程序,一个顶点shader和一个像素shader,然后把这两个shader绑定到CG上下文上。

然后,我们会接触到一个叫做CG profile的东西。因为图形显卡千奇百怪,它们不会全部都支持全部的CG's shader特性。因此,CG profile定义了一些只会在特定硬件平台上起作用的CG语言。每一个shader我们都需要一个对应的CG profile,因为顶点shader和像素shader有着不同的特性。

当我们创建CG context的时候,我们为每一个shader调用了一次CG Profile函数,然后这个函数会返回运行当前程序的电脑最适合的CG Profile。然后我们把这些profiles附加到顶点和像素shader上。这里面最cool的事情就是,我们没有针对特定的profile进行硬编码。相反,这都是由CG profile函数在运行时决定当前机器最适合的profile的。多么整洁的方式:)

另一件处理CGshader时比较有意思的事情是:你能够决定启用还是不启用shader,就如同你在OpenGL中处理textures一样。这意味着你能够在程序里放置很多shader程序(尽管同一时间只有一个顶点shader和一个像素shader在运行),并且根据需要随时转换他们。还有,如果你突然想要OpenGL处理一次渲染,那么你可以禁用所有的CG shader

最后一件值得一提的事情是你既可以在运行前编译你的shader,也可以在运行时编译。运行时编译的好处是,你能够得到最新版本的CG编译器(这能够让shader运行得更加流畅)。如果你想要更进一步了解这两种编译方式的不同点,你可以参考一下CG Toolkit中的CG手册。

CG shader

现在,不再废话了,让我们直接操作这头怪兽!

shader程序本身开始。首先我们看一下顶点shader。它负责把光向向量从object space转换到tangent space。还有,既然我们的shader接管了标准OpenGl对顶点的处理,我们需要手动把当前顶点位置从object space变换到clip space(记住,我们绘制的每一个顶点都对应着一次顶点shader的调用)。就像前面提到过的,这可以轻而易举得通过把modelView矩阵和projection矩阵相乘,然后乘以顶点位置得到。

 

变量名字后面的冒号操作符,告诉CG,我们把那个变量绑定到特定的OpenGL call上,比如

l "POSITION",会取到glVertex3f()的结果

l ":TEXCOORD0"会取到glMultiTexCoord2fARB(GLTEXTURE0ARB, ...)的结果

": POSITION"被称作"binding"semantic

你可能在猜想为什么我们把texture坐标和normal map坐标分开传到像素处理器中!毕竟,他们的值相同。原因是:为了兼容老版本的显卡,我们必须把这两个值分开传。 Uniform参数是指不会经常改变的那些。比如,光向、modelView矩阵和projection矩阵相乘的结果在我们渲染组成四边形的四个顶点的时候都不会改变。因此在我们绘制任何东西前,我们设置如下两个uniform值:

 

同样注意一下我们绑定输出参数的语意。这保证了我们从像素处理器中直接读到他们。就像你马上看到的那样,vLightVector被绑定到TEXCOORD2上,能够被像素处理器已TEXCOORD2的语意当作参数读取到。

如果你能够理解早先讨论到的数学,那么你很容易理解上面的顶点shader

下面是像素shader

读懂这个shader在做什么应该很容易。把他和早前讨论的bump mapping理论做一个对比你就什么都明白了。

上面讨论到的是shader部分,下面让我们看下如何在程序中运用CG

The Main Application

让我们从必要的头文件开始。我们假定CG 头文件已经安装到编译器包含路径中叫做CG的子文件中了。这就如同OpenGL的头文件保存在GL子文件夹下(假定OglExt 库已经被安装到工程文件夹或者编译器的LIBRARY 路径中):

接下来是InitCG函数,负责设定CG并且设置CG shader程序:

接下来我们通过两个三角形定义一个四边形,并且定制对应的纹理坐标。注意,我们仅仅用到了两个TBN矩阵。因为,就像早前提到的,我们处理的是三角形并且每个三角形对应1TBN矩阵。实际上,基于我们知道渲染的是四边形,而四边形是一块平滑的表面。因此这里我们能够只用一个TBN矩阵。

下面是一个异常回调函数,每当我们的CG shader出现错误的时候就会被调到,比如当我们加载并编译shader时:

当一切完成的时候,不要忘了清理工作:

在我们进行渲染之前,我们需要为每一个三角形计算TBN矩阵。这项工作在CalculateTBNMatrix()函数中进行:

有三个输入参数:

l 要计算TBN矩阵的三角形(牢记每个三角形对应1TBN矩阵)

l 给定三角形的纹理坐标

l 被计算的TBN矩阵。

下面是函数的实现。注意,自本文第二版修正后,这个函数也被更改了:

终于,我们到了所有魔法发生的地方:Render()函数。这里,我们设定多重纹理,我们驱动我们的shader,我们利用RenderQuad()函数把我们的四边形渲染到屏幕中间。

RenderQuad()的实现如下。你可能注意到我们做了点手脚,利用GL_TRIANGLE_FAN代替GL_TRIANGLE渲染三角形。这里,用GL_TRIANGLE_FAN的话,我们只需要指定四个顶点而不需要指定六个顶点分开绘制两个三角形。

你应该注意到我们通过glMultiTexCoord3fARB()函数指定TBN矩阵。

一切完成了!我们终于用CG语言实现了bump mapping

结论

很容易扩展教程中的代码,让它能支持镜面反射部分。你所需要做得仅仅是阅读参考[1]中的镜面发射条目,然后你就能够在顶点和像素处理器中实现他们。如果你没有时间做,别担心,我们现在正在做这样一个教程。

很有必要注意:你们能够很快用正狡猾的cubemaps处理整个过程,但是这个教程的主要目的是让整件事情足够简单相比较其他教程。对正交化得cubeMap的讨论会在以后的文章中出现。

希望你和我一样享受这个过程。如果你需要帮助,或者发现有些东西不够清楚,那尽情给我发右键。当然,建议和对这个教程的校正也非常受欢迎:)

确保你下载了下面链接中提供的demo,以便钻进真正的代码中!

致谢

l 非常感谢我的兄弟Jakob Gath(Hel)提供了很多建议和校正,以及发现了demo中的一个编译错误

l 感谢Philipp Karbach([PR]JazzD)在他自己做之前让我做出了这个教程,并且指出了可能在旧显卡上出现的很多潜在的问题。

l 感谢我的兄弟和ProPuke帮我把这个教程转成HTML

l 感谢www.sulaco.co.za网的McClaw提供了很多有意义的反馈并且传播了这个文章

Demo

我是一个vs2013的使用者,如果你想要用另外一个IDE的话,你所需要做得仅仅是创建一个空的窗口工程,然后手动加上所有的.h.cpp文件。 这里下载demo,确保开始前阅读了Readme.txt.

这个demoGeforce3-Geforce6显卡上测过,没有任何问题。然后,如果你运行这个demo,并且收到“Profile not supported”错误的话,那就说明你的显卡不支持demo所需要的shader特性!

l Edit: 某些显卡,运行最新版本的demo,在像素处理器中,会出现不能正确光方向的单位化的现象。所以,这里我提供了一些老的shader 文件(。。链接。。。)(感谢jeroen 让我注意到了这个问题)

l Edit28/03/06): Philippe Komma让我注意到了:CalculateTBNMatrix函数中的for循环是多余的,会导致整个TBN矩阵每个循环都会被计算一次 这个问题现在已经被纠正,循环已经完全被移除了。

l Edit(06/10/07): 请注意,这个demo可能会在新版本的Visual Studio里出现一些编译和链接错误。如果你想改动一些代码,并且不能够成功编译和链接这个demo,我建议你移除两个依赖的外部库(TextureMgrLibrary.libLogLibrary.lib),转而用自己的。很抱歉起先创造这个的时候没有用动态链接库,鉴于我的codebase已经改了这么多,如果要做改动的话,会是很大的工作量。

参考:

[1] - http://www.delphi3d.net/articles/viewarticle.php?article=phong.htm
[2] - http://www.paulsprojects.net/tutorials/simplebump/simplebump.html
[3] - http://developer.nvidia.com/object/mathematicsofperpixellighting.html
[4] - http://nehe.gamedev.net/data/articles/article.asp?article=20
[5] - http://www.gamedev.net/columns/hardcore/cgbumpmapping
[6] http://www.blacksmith-studios.dk/projects/downloads/tangent_matrix_derivation.php

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值