注:这份代码年代比较久远了,直接跑会提示各种未定义标识符,经过查阅发现这些都是glew中的。如果想要通过编译,首先需要配置并include glew.h。我用网上下的glew.h库编译这个代码,发现总是链接错误,所以后来我从原作者的shadow mapping中拷贝了一份glee.h通过了编译(这是一个简化版的glew库),最终通过了编译。此外还会有一个映像的bug,只要在网上搜一下就能解决。
在过去的几周,我看到许多人在论坛里讨论在应用程序中使用凹凸纹理的帖子。尽管如此,并没有简单的教程来介绍这个效果是如何实现的。所以我就尝试来解释一下这一技术。
这是我第一次写教程,欢迎你随时告诉我你的想法。
我们将会使用一些OpenGL扩展来完成这一功能,它们并不仅仅适用于特定的供应商,而是能在任何NVidia Geforce、ATI Radeon以及其它系列卡上工作。如果你之前没有使用过OpengGL扩展,我将会解释有哪些必要的设置。不过,这篇教程的主要面向对象是已经掌握多重纹理,最好是立方体贴图相关的知识的人。
我将会使用glut来管理窗口,以保证这个教程足够的简单。
以下是我们将完成的。左边的图片显示了使用凹凸纹理的圆环,右边的图片显示了使用标准OpenGL顶点光照的相同圆环。你可以在这个页面的底部下载这个demo。
红宝书中这样定义Opengl的光照方程:
顶点颜色 = 自发射光+全局环境光+ sum(衰减系数*点光源*(环境光+ max{L.N,0}*漫反射光)+(max{H.N,0} ^镜面反射系数)*镜面反射光
其中:
|
我们的目标是逐像素点评估光照方程。然后,通过改变普通映射,我们实现一个凹凸的效果我们没有办法去评估以上所有的内容,尤其是在不使用任何片段程序,寄存混合等的情况下。所以,我们将简化这个方程。
没有环境光
没有自发射光
只有一个光源
没有衰减或者聚光灯效果
没有镜面反射
因此,我们的每像素光照方程被简化为:
Color = max{l.n,0}*diffuse
为了了解凹凸纹理映射,接下来你需要分清坐标空间。这个教程里用到了很多坐标空间。下面的图描述了这些坐标空间以及它们之间的转换关系。
这里是我们将使用的五个坐标空间。我假设你已经非常熟悉标准的OpenGL模型视图矩阵以及投影矩阵。从上面的图片,你可以看到模型视图矩阵把物体空间的坐标转换到了视图空间,然后投影矩阵将视图空间的坐标转换到裁剪空间。在裁剪空间之后,你的坐标将进行透视变换,然后是视口变换,最终得到了可以绘制在屏幕上的窗口坐标。
当你指定顶点的时候,你实际上是在物体空间做这件事。因此,模型视图矩阵和投影矩阵将一起从物体空间变换到裁剪空间。如果你想要从视图空间转换回物体空间,你可以通过乘以模型矩阵的逆矩阵来变换你的顶点。
那么,这个切线空间(tangent space)是什么?TBN矩阵又是什么?
好的。这可能是最难理解的一部分了。切线空间是一个我们模型表面的一个局部空间,让我们来考虑一个单一的四方体:
现在你们应该都知道什么是法线了吧。它是一个从四方形里指出的向量(在这个例子中它是标准化的,也就是单位长度的)每个顶点的法线都指向屏幕外。
现在,我们需要找到一个顶点的两个切线。“S切线”指向s纹理坐标增加的方向,’T切线”指向T纹理坐标增加的方向。
这两个切线以及法线形成了这个顶点的基底。它们定义了一个坐标空间——切线空间。把s切线、t切线以及法线分别想象成这个坐标空间的x,y,z坐标轴。TBN矩阵就是从物体空间转换到切线空间的矩阵,它的组成像这样:
( Sx Sy Sz )
( Tx Ty Tz )
( Nx Ny Nz )
其中Sx,Sy,Sz是S切线的元素,Tx,Ty,Tz是T切线的元素,Nx,Ny,Nz是法线的元素。
顺便说一句,TBN矩阵的名字源于“切线(Tangent),次法线(Binormal),法线(Normal)”
“Binormal”是切线的另一个叫法。我相信切线t这个名字能更清晰地表达这个向量的含义。
所以,我们该如何在每像素基底上改变法线呢?和你使用纹理映射改变一个模型的每个像素颜色基本一样。但是,法线映射需要保存标准化向量,而不是颜色。
我们的法线看起来是这样的:
(x)
(y)
(z)
由于它们是单位长度的,(x*x)+(y*y)+(z*z),因而x,y,z的取值范围在[-1,1]。我们可以通过用x,y,z分量代替rgb分量在一个纹理映射中表达它们。我们用来表达向量的颜色分量必须分布在0,1之间,所以,我们设置:
r = (x+1)/2;
g = (y+1)/2;
b = (z+1)/2;
花几分钟来让自己确信:每一个可能的单位法线都可以通过这种方式存储在颜色里。
我们放在纹理映射中的法线将用在切线空间。在切线空间中,法线通常在z方向上。因此,直法线的RGB颜色为(0.5,0.5,1.0),这就是为什么我们的法线映射图是蓝色的。
这是我们的例子将会用到的一张法线映射。
回顾一下,一个2D的纹理是一个平坦的四方形,一个颜色的二维数组。想象六个正方形的二维纹理,把它们放在一起组成立方体的六个面。这就是一个立方体映射。为了访问立方体映射中纹理颜色,我们使用了三维纹理坐标,即使用glTexCoord3f,而不是glTexCoord2f,或者说在我们的例子中,相应的顶点数组结构。
在这个demo中,我们将会使用一个标准化立方体映射。这意味着,我们将在立方体映射中存储向量,而不是颜色,就和法线映射一样。使用纹理下标(ka,kb,kc)。我们将会存储向量:
OpenGL的最大优点之一就是它的可扩展性,扩展可以由硬件供应商书写,以便更好利用硬件功能。我们在这里使用的扩展都是ARB扩展,这意味着它们是由OpenGL Architecture Review Board(OpenGL架构审查委员会,制定OpenGL标准的人)批准的。这些扩展是OpenGL最新1.4版本的所有核心特点。
bool SetUpARB_multitexture() {
得到扩展的字符串,遍历它寻找”GL_ARB_multitexture”。如果找到了,令ARB_multitexture_supported为真。
char * extensionString=(char *)glGetString(GL_EXTENSIONS); char * extensionName="GL_ARB_multitexture"; char * endOfString; //store pointer to end of string unsigned int distanceToSpace; //distance to next space endOfString=extensionString+strlen(extensionString); //loop through string while(extensionString
如果我们找不到GL_ARB_multitexture,输出错误信息,并返回假。
if(!ARB_multitexture_supported) { printf("ARB_multitexture unsupported!\n"); return false; } printf("ARB_multitexture supported!\n");
如果我们运行到了这里,ARB_multitexture已经被支持了。为了使用ARB_multitexture函数,我们需要为每个函数创建函数指针。接下来,我们使用wglGetProcAddress来初始化函数指针:
PFNGLACTIVETEXTUREARBPROC glActiveTextureARB =NULL; glActiveTextureARB = (PFNGLACTIVETEXTUREARBPROC) wglGetProcAddress("glActiveTextureARB");
函数指针是全局声明的,并且在头文件中extern声明过。通过include正确的头文件,并且在我们程序的开头调用SetUpARB_multitexture,我们现在就可以使用这些函数了。
其它的三个扩展使用相同的方式设置的,但是它们会更简单,因为我们不需要函数指针。
创建标准化立方体映射
函数“GenerateNormalisationCubeMap”所做的正如它的名字。
bool GenerateNormalisationCubeMap() {
首先我们创建空间来存储每个面的数据。每个面的大小是32X32,然后我们需要存储每个点的RGB分量。
unsigned char * data=new unsigned char[32*32*3]; if(!data) { printf("Unable to allocate memory for texture data for cube map\n"); return false; }
声明一些有用的变量。
//some useful variables int size=32; float offset=0.5f; float halfSize=16.0f; VECTOR3D tempVector; unsigned char * bytePtr;
接下来,我们将为每个面执行一次这样的操作。我将展示正的x面是怎样做的,其它的面非常类似。你可以在源代码中看到更多细节。
//positive x bytePtr=data;
遍历面中的每个像素:
for(int j=0; j计算从正方体的中心到纹理的向量:
tempVector.SetX(halfSize); tempVector.SetY(-(j+offset-halfSize)); tempVector.SetZ(-(i+offset-halfSize));
我们标准化这一向量,把它转换到[0,1]之间,这样它就能被存在颜色中 。我们把它存为纹理数据:
tempVector.Normalize(); tempVector.PackTo01(); bytePtr[0]=(unsigned char)(tempVector.GetX()*255); bytePtr[1]=(unsigned char)(tempVector.GetY()*255); bytePtr[2]=(unsigned char)(tempVector.GetZ()*255); bytePtr+=3; } }
现在我们把立方体的这个面交给OpenGL。我们告诉它这是当前立方体映射的x方向的正面,还告诉它可以在哪里找到数据。
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X_ARB, 0, GL_RGBA8, 32, 32, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
为立方体的每个面重复这一过程后,我们终于完成了。不要忘记删除临时数据存储空间。
delete [] data; return true; }
绘制圆环
我们将圆环的信息存储在一个类中。首先,我们有一个存储一个顶点的简单类。我们存储了位置,纹理坐标,即切线和法线,最后,我们存储每个顶点的切线空间的光线向量。这是切线空间中从顶点指向光源的向量。
class TORUS_VERTEX { public: VECTOR3D position; float s, t; VECTOR3D sTangent, tTangent; VECTOR3D normal; VECTOR3D tangentSpaceLight; };
现在,我们有了主要的圆环类了。它仅仅存储了一系列顶点和一系列索引,以及它们的大小。它同样包含了填充这些列表元素的函数:InitTorus。
class TORUS { public: TORUS(); ~TORUS(); bool InitTorus(); int numVertices; int numIndices; unsigned int * indices; TORUS_VERTEX * vertices; };
我们定义我们的圆环细分度为48。这也就意味着我们创建它时,每一环有48个顶点。
const int torusPrecision=48;
圆环的构造函数调用了初始化函数。
TORUS::TORUS() { InitTorus(); }
圆环的析构函数删除了索引、顶点列表来释放内存。
TORUS::~TORUS() { if(indices) delete [] indices; indices=NULL; if(vertices) delete [] vertices; vertices=NULL; }
这是我们圆环的初始化函数。
bool TORUS::InitTorus() {
我们计算顶点以及索引的个数,然后为顶点索引列表申请空间。
numVertices=(torusPrecision+1)*(torusPrecision+1); numIndices=2*torusPrecision*torusPrecision*3; vertices=new TORUS_VERTEX[numVertices]; if(!vertices) { printf("Unable to allocate memory for torus vertices\n"); return false; } indices=new unsigned int[numIndices]; if(!indices) { printf("Unable to allocate memory for torus indices\n"); return false; }
现在,我们计算圆环的顶点位置,法线等等。
首先,我们在xy平面产生48个顶点形成的环,以右边为原点。
这些顶点的法线正如它们所显示的那样。我们同样设置T纹理坐标随着i线性增长,S纹理坐标为0。因此,T切线指向i增长的方向。S切线指向屏幕里,我们接下来会介绍原因。
//calculate the first ring - inner radius 4, outer radius 1.5 for(int i=0; i接下来,我们绕着Y轴以2*pi/48的步长旋转整个圆环。这将会产生另一个环形成的环面。所有的法线、切线都旋转了,T纹理坐标和初始圆环相同,S纹理坐标随着一个圆环到下一个而增加。这就是第一个圆环S切线指向屏幕内的原因。下面这张照片是一张俯视图。
//rotate the first ring to get the other rings for(int ring=1; ring现在,我们计算我们刚刚放置好的顶点的三角形面片的索引。
//calculate the indices for(ring=0; ring好了,现在圆环画好了。
return true; }
最终,在我们的main文件中
首先,我们需要包含必要的头文件。
“WIN32_LEAN_AND_MEAN” 是为了告诉windows.h不要包含大量我们用不到的晦涩的东西。
然后,我们包含“glut.h”,它自身包含了"gl.h"和“glu.h”
“glext.h”包含了我们即将使用的扩展所需要的东西。
接下来,我们包含一些扩展的头文件。IMAGE.h是一个加载法线映射和纹理映射的图像类。“Maths.h”包含了我们将使用到的顶点和矩阵类。“TORUS.h”包含了我们即将绘制的圆环的细节,“Normalisation cube map.h”包含了……嗯,这个我将留给你自己去分辨。
#define WIN32_LEAN_AND_MEAN #include#include #include #include #include "Extensions/ARB_multitexture_extension.h" #include "Extensions/ARB_texture_cube_map_extension.h" #include "Extensions/ARB_texture_env_combine_extension.h" #include "Extensions/ARB_texture_env_dot3_extension.h" #include "Image/IMAGE.h" #include "Maths/Maths.h" #include "TORUS.h" #include "Normalisation Cube Map.h"
现在我们要声明一些全局对象,包括我们是否要加载凹凸纹理以及颜色纹理的布尔量。
//Our torus TORUS torus; //Normal map GLuint normalMap; //Decal texture GLuint decalTexture; //Normalisation cube map GLuint normalisationCubeMap; //Light position in world space VECTOR3D worldLightPosition=VECTOR3D(10.0f, 10.0f, 10.0f); bool drawBumps=true; bool drawColor=true;
现在,我们创建一个叫Init的函数。这个在程序一开始时调用,不过应该在窗体被创建之后。
//Called for initiation void Init(void) {
首先,我们设置我们将要用到的扩展。如果扩展设置返回假,那么这个扩展就不被支持,我们就没有办法运行这个demo,输出错误并退出。
//Check for and set up extensions if( !SetUpARB_multitexture() || !SetUpARB_texture_cube_map() || !SetUpARB_texture_env_combine() || !SetUpARB_texture_env_dot3()) { printf("Required Extension Unsupported\n"); exit(0); }
接下来我们设置OpenGL状态。如果你看过NeHe‘s的教程,接下来的代码你可能会很眼熟。我们加载单位模型视图矩阵,设置颜色以及深度状态,然后允许背面剔除。
//Load identity modelview glMatrixMode(GL_MODELVIEW); glLoadIdentity(); //Shading states glShadeModel(GL_SMOOTH); glClearColor(0.2f, 0.4f, 0.2f, 0.0f); glColor4f(1.0f, 1.0f, 1.0f, 1.0f); glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); //Depth states glClearDepth(1.0f); glDepthFunc(GL_LEQUAL); glEnable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE);
现在,我们加载我们的纹理。首先我们使用IMAGE类来加载映射。然后,既然我们的纹理映射使用的是调色板色(每个像素8位),我们在发送给OpenGL前,将其扩展到24位真彩色。我们使用glTexImage2D来发送数据,然后设置纹理参数。事实上,我们做的和普通纹理映射一样。
//Load decal texture IMAGE decalImage; decalImage.Load("decal.bmp"); decalImage.ExpandPalette(); //Convert normal map to texture glGenTextures(1, &decalTexture); glBindTexture(GL_TEXTURE_2D, decalTexture); glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA8, decalImage.width, decalImage.height, 0, decalImage.format, GL_UNSIGNED_BYTE, decalImage.data); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); //Load normal map IMAGE normalMapImage; normalMapImage.Load("Normal map.bmp"); normalMapImage.ExpandPalette(); //Convert normal map to texture glGenTextures(1, &normalMap); glBindTexture(GL_TEXTURE_2D, normalMap); glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA8, normalMapImage.width, normalMapImage.height, 0, normalMapImage.format, GL_UNSIGNED_BYTE, normalMapImage.data); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
接下来,我们创建我们的标准化立方体映射。我们就像之前一样创建一个纹理,但是我们不再使用GL_TEXTURE_2D,而是使用GL_TEXTURE_CUBE_MAP_ARB。末尾的ARB示意这是ARB扩展的一部分。我们不再使用LoadTexture,而是创建立方体映射并通过前面提到的GenerateNormalisationCubeMap函数发送给OpenGL。
//Create normalisation cube map glGenTextures(1, &normalisationCubeMap); glBindTexture(GL_TEXTURE_CUBE_MAP_ARB, normalisationCubeMap); GenerateNormalisationCubeMap(); glTexParameteri(GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); }
好了,初始化到这里就结束了。接下来我们可以画点什么了。
//Called to draw scene void Display(void) {
首先,我们清除颜色和深度缓存,并且设置模型视图矩阵为单位矩阵。
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity();
现在设置模型视图矩阵。
首先,我们执行视区变换,也就是上面图表中的视图矩阵。这将我们的摄像机摆放到了场景中。我们使用gluLookAt来将相机放置到(0.0,10.0,10.0)位置,望向原点。
然后,我们设置模型矩阵。记得模型视图矩阵也就是模型矩阵紧跟着视图矩阵。但是,OpenGL要求我们以相反的顺序来应用变换。我们想要让圆环绕着Y轴旋转。我们使用一个静态变量来维护这个旋转的过程,我们要做的仅仅是增加它的值,然后把旋转交给OpenGL。
//use gluLookAt to look at torus gluLookAt(0.0f,10.0f,10.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f); //rotate torus static float angle=0.0f; angle+=0.1f; glRotatef(angle, 0.0f, 1.0f, 0.0f);
我们现在计算模型矩阵的逆,正如你所知道的,这将把我们从世界空间转换到物体空间。
首先,我们保存当前的模型视图矩阵,然后我们把它重设为单位矩阵。旋转a度角的逆就是绕着同样的坐标轴旋转-a度角。我们把这个旋转交给OpenGL,然后使用GetFloatv来得到当前矩阵。然后我们再恢复我们存储的模型视图矩阵。
//Get the inverse model matrix MATRIX4X4 inverseModelMatrix; glPushMatrix(); glLoadIdentity(); glRotatef(-angle, 0.0f, 1.0f, 0.0f); glGetFloatv(GL_MODELVIEW_MATRIX, inverseModelMatrix); glPopMatrix();
现在,我们使用刚刚计算出来的矩阵将光的位置转换到物体空间。
//Get the object space light vector VECTOR3D objectLightPosition=inverseModelMatrix*worldLightPosition;
现在,我们需要计算切线空间每个顶点的光向量。我们遍历所有顶点,并且填充向量。
观察上面的图表(再一次),我们看到TSB矩阵将物体空间的点转换到切线空间。
我们知道光源在物体空间的位置。我们将其与顶点位置坐标作差,来得到从顶点到光源的向量。我们使用TSB矩阵将其转换到切线空间。TSB矩阵的形式如下:
( Sx Sy Sz )
( Tx Ty Tz )
( Nx Ny Nz )
所以,切线空间光向量的第一个分量是(Sx,Sy,Sz).(Lx,ly,Lz)中,L是从顶点到光源的向量。类似的,其它两个分量也是简单的点乘。
//Loop through vertices for(int i=0; i
现在,我们可以绘制圆环了。我们将绘制两遍来完成这个凹凸纹理圆环。第一遍,我们仅仅用颜色纹理来绘制。我们可以开启或者关闭这个绘制,来得到不同的效果。在一开始,如果有必要,我们将绘制普通纹理。
//Draw bump pass if(drawBumps) {
首先,我们混合我们即将用到的纹理。我们将普通纹理设置为第0层,然后开启二维纹理。然后我们将法线立方体映射设置为第1层,然后开启立方体纹理映射。接下来我们重设当前单元为0.
//Bind normal map to texture unit 0 glBindTexture(GL_TEXTURE_2D, normalMap); glEnable(GL_TEXTURE_2D); //Bind normalisation cube map to texture unit 1 glActiveTextureARB(GL_TEXTURE1_ARB); glBindTexture(GL_TEXTURE_CUBE_MAP_ARB, normalisationCubeMap); glEnable(GL_TEXTURE_CUBE_MAP_ARB); glActiveTextureARB(GL_TEXTURE0_ARB);
现在,我们设置我们即将用到的顶点数组。
我们设置顶点指针指向位置,并且开启顶点数组。
纹理坐标数组将为每个纹理单元设置。
第0层纹理坐标仅仅包含s和t纹理坐标。所以,普通的映射将和一个标准的纹理映射一样应用到圆环上。
第1层纹理坐标仅仅是切线空间的光源向量。由于我们把纹理1设置为法线立方体映射,纹理1将会包含每个像素的标准化的切线空间光线向量。
//Set vertex arrays for torus glVertexPointer(3, GL_FLOAT, sizeof(TORUS_VERTEX), &torus.vertices[0].position); glEnableClientState(GL_VERTEX_ARRAY); //Send texture coords for normal map to unit 0 glTexCoordPointer(2, GL_FLOAT, sizeof(TORUS_VERTEX), &torus.vertices[0].s); glEnableClientState(GL_TEXTURE_COORD_ARRAY); //Send tangent space light vectors for normalisation to unit 1 glClientActiveTextureARB(GL_TEXTURE1_ARB); glTexCoordPointer(3, GL_FLOAT, sizeof(TORUS_VERTEX), &torus.vertices[0].tangentSpaceLight); glEnableClientState(GL_TEXTURE_COORD_ARRAY); glClientActiveTextureARB(GL_TEXTURE0_ARB);
我们在纹理0中存储普通映射,在纹理1中存储标准化切线空间光线向量。记得我们要计算的目标:
color = max{l.n, 0}*diffuse
我们将在第二次计算过程中乘以漫反射光,所以我们需要求出tex0点乘tex1的值。所以,我们使用ARB_texture_env_combine和ARB_texture_env_dot3扩展来完成这一点。第一个纹理单元将会纹理颜色替换面片颜色,例如,普通的映射。
第二个纹理单元将会将其与texture1点乘,例如,光线向量。因为它们都处在切线空间,点乘是有意义的。同样,由于输出是一个颜色,它将被限制到[0,1]范围内。所以,我们可以开始着手于我们的等式的第一部分了。在这个demo中按下2,你将会看到这一部分长成什么样子。
//Set up texture environment to do (tex0 dot tex1)*color glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE_ARB); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_RGB_ARB, GL_TEXTURE); glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB_ARB, GL_REPLACE); glActiveTextureARB(GL_TEXTURE1_ARB); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE_ARB); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_RGB_ARB, GL_TEXTURE); glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB_ARB, GL_DOT3_RGB_ARB); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_RGB_ARB, GL_PREVIOUS_ARB); glActiveTextureARB(GL_TEXTURE0_ARB);
如果你留意到的话,你可能会为向量在纹理中转换到了[0,1],但却没有转回[-1,1]感到困惑。不用担心,DOT3_RGB_ARG纹理环境将会自动为我们完成这件事情。
现在,我们可以绘制圆环了。
//Draw torus glDrawElements(GL_TRIANGLES, torus.numIndices, GL_UNSIGNED_INT, torus.indices);
我们已经在这个设置中绘制了我们需要绘制的东西,所以我们可以禁止纹理和顶点数组了。我们同样将纹理环境重设为标准模型。
//Disable textures glDisable(GL_TEXTURE_2D); glActiveTextureARB(GL_TEXTURE1_ARB); glDisable(GL_TEXTURE_CUBE_MAP_ARB); glActiveTextureARB(GL_TEXTURE0_ARB); //disable vertex arrays glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_TEXTURE_COORD_ARRAY); glClientActiveTextureARB(GL_TEXTURE1_ARB); glDisableClientState(GL_TEXTURE_COORD_ARRAY); glClientActiveTextureARB(GL_TEXTURE0_ARB); //Return to standard modulate texenv glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); }
下一步是绘制漫反射颜色,我们的纹理圆环。如果我们绘制了凹凸纹理以及纹理,我们可以通过相乘将它们合并起来。我们开启相乘混合模式:
//If we are drawing both passes, enable blending to multiply them together if(drawBumps && drawColor) { //Enable multiplicative blending glBlendFunc(GL_DST_COLOR, GL_ZERO); glEnable(GL_BLEND); }
我们可以开始绘制纹理部分了。
//Perform a second pass to color the torus if(drawColor) {
如果我们不绘制凹凸纹理部分,我们可以允许标准OpenGL光源来实现无凹凸纹理映射的圆环与有凹凸纹理映射的圆环的比较。你可以点击3来观察标准照射下的圆环。
if(!drawBumps) { glLightfv(GL_LIGHT1, GL_POSITION, VECTOR4D(objectLightPosition)); glLightfv(GL_LIGHT1, GL_DIFFUSE, white); glLightfv(GL_LIGHT1, GL_AMBIENT, black); glLightModelfv(GL_LIGHT_MODEL_AMBIENT, black); glEnable(GL_LIGHT1); glEnable(GL_LIGHTING); glMaterialfv(GL_FRONT, GL_DIFFUSE, white); }
我们绑定贴图纹理到纹理0上,并开启二维纹理。
//Bind decal texture glBindTexture(GL_TEXTURE_2D, decalTexture); glEnable(GL_TEXTURE_2D);
现在,我们和上面一样开始顶点数组和纹理坐标数组。这一次,我们同样为标准OpenGL光线开启法线数组。
//Set vertex arrays for torus glVertexPointer(3, GL_FLOAT, sizeof(TORUS_VERTEX), &torus.vertices[0].position); glEnableClientState(GL_VERTEX_ARRAY); glNormalPointer(GL_FLOAT, sizeof(TORUS_VERTEX), &torus.vertices[0].normal); glEnableClientState(GL_NORMAL_ARRAY); glTexCoordPointer(2, GL_FLOAT, sizeof(TORUS_VERTEX), &torus.vertices[0].s); glEnableClientState(GL_TEXTURE_COORD_ARRAY);
现在,我们绘制圆环并重设Opengl状态(灯光,纹理等)
//Draw torus glDrawElements(GL_TRIANGLES, torus.numIndices, GL_UNSIGNED_INT, torus.indices); if(!drawBumps) glDisable(GL_LIGHTING); //Disable texture glDisable(GL_TEXTURE_2D); //disable vertex arrays glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_NORMAL_ARRAY); glDisableClientState(GL_TEXTURE_COORD_ARRAY); } //Disable blending if it is enabled if(drawBumps && drawColor) glDisable(GL_BLEND);
glFinish告诉OpenGL结束绘制。
glutSwapBuffers交换前后两个颜色矩阵。
glutPostRedisplay告诉glut我们想要尽快的绘制下一帧。
glFinish(); glutSwapBuffers(); glutPostRedisplay(); }
这就是绘制的全部内容!
现在我们需要为glut创建多一些函数。如果你之前使用过glut,你应该对它们非常熟悉。
Reshape()在窗口大小被改变时调用。它重设视口为整个屏幕大小,并且重设投影矩阵到正确的视角。
//Called on window resize void Reshape(int w, int h) { //Set viewport if(h==0) h=1; glViewport(0, 0, w, h); //Set up projection matrix glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective( 45.0f, (GLfloat)w/(GLfloat)h, 1.0f, 100.0f); glMatrixMode(GL_MODELVIEW); }
Keyboard() 在按键后被调用。
//Called when a key is pressed void Keyboard(unsigned char key, int x, int y) {
如果用户点击esp,那么退出。
//If escape is pressed, exit if(key==27) exit(0);
如果点击1,我们将会绘制凹凸映射和普通颜色。所以,将两个布尔量都设为true。
//'1' draws both passes if(key=='1') { drawBumps=true; drawColor=true; }
如果点击2,我们只绘制凹凸纹理,将它设置为true,另一个为false。
//'2' draws only bumps if(key=='2') { drawBumps=true; drawColor=false; }
如果点击3,我们只绘制普通颜色,所以,要禁止凹凸纹理。
//'3' draws only color if(key=='3') { drawBumps=false; drawColor=true; }
如果点击w,我们设置绘制模式为现线框。
如果点击f,我们设置绘制模式为实体。
//'W' draws in wireframe if(key=='W' || key=='w') glPolygonMode(GL_FRONT, GL_LINE); //'F' return to fill if(key=='F' || key=='f') glPolygonMode(GL_FRONT, GL_FILL); }
最终,在我们的main函数中,这就是一个标准的glut事务了。我们初始化glut,创建一个双缓冲,RGB模式,支持深度缓存的窗口。然后,我们调用我们的前面的初始化函数。接下来,我们告诉glut哪里调用显示,重绘以及键盘响应函数。这些就是注册回调。之后,我们告诉glut开始绘制。Glut从这里开始接管我们的程序。
int main(int argc, char** argv) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH); glutInitWindowSize(640, 480); glutCreateWindow("Simple Bumpmapping"); Init(); glutDisplayFunc(Display); glutReshapeFunc(Reshape); glutKeyboardFunc(Keyboard); glutMainLoop(); return 0; }
以上就是全部内容了,我希望你能理解大部分内容,并且期待着你能自己使用它们!