嗨,我是Dario Corno,也因SpinningKids的rIo而為大家所知。首先,我想要解釋我為什么決定寫這點指南。我自1989年以來就從事scener的工作。我想要你們去下載一些demo(示例程序,也就是演示——譯者)以幫助你理解什么是Demo并且demo的效果是什么。
Demos是被用來展示恰似風雅的技術一樣無限并且時而嚴酷的譯碼。在今天的演示中你通常總可以發現一些真正迷人的效果。這不是一本迷人的效果指南,但結果將非常的酷!你能夠從http://www.pouet.net和 http://ftp.scene.org. 發現大量的演示收集。
既然緒論超出了我們探討的范圍,我們可以繼續我們的指南了。
我將解釋如何做一個看起來象徑向模糊的eye candy 效果。有時它以測定體積的光線被提到。不要相信,它僅僅是一個冒牌的輻射狀模糊;D
輻射狀模糊效果通常借助于模糊在一個方向上相對于模糊物的中心原始圖象的每一個象素來做的。
借助于現今的硬件用色彩緩沖器來手工作模糊處理是極其困難的(至少在某種程度上它被所有的gfx卡所支持),因此我們需要一些竅門來達到同樣的效果。
作為一個獎勵當學習徑向模糊效果時,你同樣將學到如何輕松地提供材料的紋理。
我決定在這篇指南中使用彈簧作為外形因為它是一個酷的外形,另外還因為我對立方體感到厭煩:}
多留意這篇指南關于如何創建那個效果的指導方針是重要的。我不研究解釋那些代碼的詳情。你應當用心記下它們中的大部分:}
下面是變量的定義和用到的頭文件。
#include <math.h> // 數學庫
float angle; // 用來旋轉那個螺旋
float vertexes[3][3]; // 為3個設置的頂點保存浮點信息
float normal[3]; // 存放法線數據的數組
GLuint BlurTexture; // 存放紋理編號的一個無符號整型
函數EmptyTexture()創建了一個空的紋理并返回紋理的編號。我們剛分配了一些自由空間(準確的是128*128*4無符號整數)。
128*128是紋理的大小(128象素寬和高),4意味著為每一個象素我們想用4byte來存儲紅,綠,藍和ALPHA組件。
GLuint EmptyTexture() // 創建一個空的紋理
{
GLuint txtnumber; // 紋理ID
unsigned int* data; // 存儲數據
// 為紋理數據(128*128*4)建立存儲區
data = (unsigned int*)new GLuint[((128 * 128)* 4 * sizeof(unsigned int))];
在分配完空間之后我們用ZeroMemory函數清0,返回指針(數據)和被清0的存貯區的大小。
另一半需注意的重要的事情是我們設置GL_LINEAR的放大率和縮放率的方法。因為我們將被我們的紋理要求投入全部的精力并且如果被濫用,GL_NEAREST會看起來非常糟糕。
ZeroMemory(data,((128 * 128)* 4 * sizeof(unsigned int))); // 清除存儲區
glGenTextures(1, &txtnumber); // 創建一個紋理
glBindTexture(GL_TEXTURE_2D, txtnumber); // 構造紋理
glTexImage2D(GL_TEXTURE_2D, 0, 4, 128, 128, 0,
GL_RGBA, GL_UNSIGNED_BYTE, data); // 用數據中的信息構造紋理
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
delete [] data; // 釋放數據
return txtnumber; // 返回紋理ID
}
這個函數簡單規格化法線向量的長度。向量被當作有3個浮點類型的元素的數組來表示,第一個元素表示X軸,第二個表示Y,第三個表示Z。一個規格化的向量[Nv]被Vn表達為Vn=[Vox/|Vo|,Voy/|Vo|,Voz/|Vo|],這里Vo是最初的向量,|Vo|是該向量的系數(或長度),X,Y,Z它的組件。之后由向量的長度區分每一個法線向量組件。
void ReduceToUnit(float vector[3]) // 歸一化一個法向量
{ // 一定長度的單位法線向量
float length; // 保存長度
// 計算向量
length = (float)sqrt((vector[0]*vector[0]) + (vector[1]*vector[1]) + (vector[2]*vector[2]));
if(length == 0.0f) // 避免除0錯誤
length = 1.0f; // 如果為0設置為1
vector[0] /= length; // 歸一化向量
vector[1] /= length;
vector[2] /= length;
}
下面各項計算所給的3個頂點向量(總在3個浮點數組中)。我們有兩個參數:v[3][3]和out[3]。當然第一個參數是一個m=3,n=3每一行代表三角形一個頂點的浮點矩陣。Out是我們要放置作為結果的法線向量的位置。
相當簡單的數學。我們將使用著名的交叉乘積運算。理論上說交叉乘積是兩個向量——它返回另一個直交向量到兩個原始向量——之間的操作。法線向量是一個垂直物體表面的直交向量,是與該表面相對的(通常一個規格化的長度)。設想兩個向量是在一個三角形的一側的上方,那么這個三角形兩邊的直交向量(由交叉乘積計算)就是那個三角形的法線。
解釋比實行還難。
我們將著手從現存的頂點0到頂點1,從頂點1到頂點2找到那個向量。這是基本上通過減法——下一個頂點的每個組件減一個頂點的每個組件——作好了的。現在我們已經為我們的三角形的邊找到了那個向量。通過交叉相乘我們為那個三角形找到了法線向量。
看代碼。
V[0][ ]是第一個頂點,v[1][ ]是第二個頂點,v[2][ ]是第三個頂點。每個頂點包括:v[ ][0]是頂點的x坐標,v[ ][1]是頂點的y坐標,v[ ][2]是頂點的z坐標。
通過簡單的減法從一個頂點的每個坐標到另一個頂點每個坐標我們得到了那個VECTOR。v1[0] = v[0][0] - v[1][0],這計算現存的從一個頂點到另一個頂點的向量的X組件,v1[1] = v[0][1] - v[1][1]將計算Y組件,v1[2] = v[0][2] - v[1][2] 計算Z組件等等。
現在我們有了兩個向量,所以我們計算它們的交叉乘積得到那個三角形的法線。
交叉相乘的規則是:
out[x] = v1[y] * v2[z] - v1[z] * v2[y]
out[y] = v1[z] * v2[x] - v1[x] * v2[z]
out[z] = v1[x] * v2[y] - v1[y] * v2[x]
我們最終得到了這個三角形的法線in out[ ]。
void calcNormal(float v[3][3], float out[3]) // 用三點計算一個立方體法線
{
float v1[3],v2[3]; // 向量 1 (x,y,z) 和向量 2 (x,y,z)
static const int x = 0; // 定義 X坐標
static const int y = 1; // 定義 Y 坐標
static const int z = 2; // 定義 Z 坐標
// 用減法在兩點之間得到向量// 從一點到另一點的X,Y,Z坐標// 計算點1到點0的向量
v1[x] = v[0][x] - v[1][x];
v1[y] = v[0][y] - v[1][y];
v1[z] = v[0][z] - v[1][z];
// 計算點2到點1的向量
v2[x] = v[1][x] - v[2][x];
v2[y] = v[1][y] - v[2][y];
v2[z] = v[1][z] - v[2][z];
// 計算交叉乘積為我們提供一個表面的法線
out[x] = v1[y]*v2[z] - v1[z]*v2[y];
out[y] = v1[z]*v2[x] - v1[x]*v2[z];
out[z] = v1[x]*v2[y] - v1[y]*v2[x];
ReduceToUnit(out); // 規格化向量
}
下面的例子正好用gluLookAt設立了一個觀察點。我們設置一個觀察點放置在0,5,50位置——正照看0,0,0并且所屬的向上的向量正仰望(0,1,0)!:D
void ProcessHelix() // 繪制一個螺旋
{
GLfloat x; // 螺旋x坐標
GLfloat y; // 螺旋y坐標
GLfloat z; // 螺旋z坐標
GLfloat phi; // 角
GLfloat theta; // 角
GLfloat v,u; // 角
GLfloat r; // 螺旋半徑
int twists = 5; // 5個螺旋
GLfloat glfMaterialColor[]={0.4f,0.2f,0.8f,1.0f}; // 設置材料色彩
GLfloat specular[]={1.0f,1.0f,1.0f,1.0f}; // 設置鏡象燈光
glLoadIdentity(); // 重置Modelview矩陣
gluLookAt(0, 5, 50, 0, 0, 0, 0, 1, 0); // 場景(0,0,0)的視點中心 (0,5,50),Y軸向上
glPushMatrix(); // 保存Modelview矩陣
glTranslatef(0,0,-50); // 移入屏幕50個單位
glRotatef(angle/2.0f,1,0,0); // 在X軸上以1/2角度旋轉
glRotatef(angle/3.0f,0,1,0); // 在Y軸上以1/3角度旋轉
glMaterialfv(GL_FRONT_AND_BACK,GL_AMBIENT_AND_DIFFUSE,glfMaterialColor);
glMaterialfv(GL_FRONT_AND_BACK,GL_SPECULAR,specular);
然后我們計算螺旋的公式并給彈簧著色。十分簡單,我就不再解釋了,因為它不是這篇指南的主要目的。這段螺旋代碼經過軟件贊助者的許可被借用(并作了一點優化)。這是寫作的簡單的方法,但不是最塊的方法。使用頂點數組可以使它更快!
r=1.5f; // 半徑
glBegin(GL_QUADS); // 開始繪制立方體
for(phi=0; phi <= 360; phi+=20.0) // 以20度的間隔繪制
{
for(theta=0; theta<=360*twists; theta+=20.0)
{
v=(phi/180.0f*3.142f); // 計算第一個點 ( 0 )的角度
u=(theta/180.0f*3.142f); // 計算第一個點 ( 0 )的角度
x=float(cos(u)*(2.0f+cos(v) ))*r; // 計算x的位置(第一個點)
y=float(sin(u)*(2.0f+cos(v) ))*r; // 計算y的位置(第一個位置)
z=float((( u-(2.0f*3.142f)) + sin(v) ) * r); // 計算z的位置(第一個位置)
vertexes[0][0]=x; // 設置第一個頂點的x值
vertexes[0][1]=y; // 設置第一個頂點的y值
vertexes[0][2]=z; // 設置第一個頂點的z值
v=(phi/180.0f*3.142f); // 計算第二個點( 0 )的角度
u=((theta+20)/180.0f*3.142f); // 計算第二個點( 20 )的角度
x=float(cos(u)*(2.0f+cos(v) ))*r; // 計算x位置(第二個點)
y=float(sin(u)*(2.0f+cos(v) ))*r; // 計算y位置(第二個點)
z=float((( u-(2.0f*3.142f)) + sin(v) ) * r); // 計算z位置(第二個點)
vertexes[1][0]=x; // 設置第二個頂點的x值
vertexes[1][1]=y; // 設置第二個頂點的y值
vertexes[1][2]=z; // 設置第二個頂點的z值
v=((phi+20)/180.0f*3.142f); // 計算第三個點 ( 20 )的角度
u=((theta+20)/180.0f*3.142f); // 計算第三個點 ( 20 )的角度
x=float(cos(u)*(2.0f+cos(v) ))*r; // 計算x位置 (第三個點)
y=float(sin(u)*(2.0f+cos(v) ))*r; // 計算y位置 (第三個點)
z=float((( u-(2.0f*3.142f)) + sin(v) ) * r); // 計算z位置 (第三個點)
vertexes[2][0]=x; // 設置第三個頂點的x值
vertexes[2][1]=y; // 設置第三個頂點的y值
vertexes[2][2]=z; // 設置第三個頂點的z值
v=((phi+20)/180.0f*3.142f); // 計算第四個點( 20 )的角度
u=((theta)/180.0f*3.142f); // 計算第四個點( 0 )的角度
x=float(cos(u)*(2.0f+cos(v) ))*r; // 計算x位置 (第四個點)
y=float(sin(u)*(2.0f+cos(v) ))*r; // 計算y位置 (第四個點)
z=float((( u-(2.0f*3.142f)) + sin(v) ) * r); // 計算z位置 (第四個點))
vertexes[3][0]=x; // 設置第四個頂點的x值
vertexes[3][1]=y; // 設置第四個頂點的y值
vertexes[3][2]=z; // 設置第四個頂點的z值
calcNormal(vertexes,normal); // 計算立方體的法線
glNormal3f(normal[0],normal[1],normal[2]); // 設置法線
// 渲染四邊形
glVertex3f(vertexes[0][0],vertexes[0][1],vertexes[0][2]);
glVertex3f(vertexes[1][0],vertexes[1][1],vertexes[1][2]);
glVertex3f(vertexes[2][0],vertexes[2][1],vertexes[2][2]);
glVertex3f(vertexes[3][0],vertexes[3][1],vertexes[3][2]);
}
}
glEnd(); // 繪制結束
glPopMatrix(); // 取出矩陣
}
這兩個事例(ViewOrtho and ViewPerspective)被編碼以使它變得很容易地在一個直交的情形下繪制并且不費力的返回透視圖。
ViewOrtho簡單地設立了這個射影矩陣,然后增加一份現行射影矩陣的拷貝到OpenGL棧上。這個恒等矩陣然后被裝載并且當前屏幕正投影觀察決議被提出。
利用2維坐標以屏幕左上角0,0和屏幕右下角639,479來繪制是可能的。
最后,modelview矩陣為透視材料激活。
ViewPerspective設置射影矩陣模式取回ViewOrtho在堆棧上推進的非正交矩陣。然后樣本視圖被選擇因此我們可以透視材料。
我建議你保留這兩個過程,能夠著色2D而不需擔心射影矩陣很不錯。
void ViewOrtho() // 設置一個z正視圖
{
glMatrixMode(GL_PROJECTION); // 選擇投影矩陣
glPushMatrix(); // 保存當前矩陣
glLoadIdentity(); // 重置矩陣
glOrtho( 0, 640 , 480 , 0, -1, 1 ); // 選擇標準模式
glMatrixMode(GL_MODELVIEW); // 選擇樣本視圖矩陣
glPushMatrix(); // 保存當前矩陣
glLoadIdentity(); // 重置矩陣
}
void ViewPerspective() // 設置透視視圖
{
glMatrixMode( GL_PROJECTION ); // 選擇投影矩陣
glPopMatrix(); // 取出矩陣
glMatrixMode( GL_MODELVIEW ); // 選擇模型變換矩陣
glPopMatrix(); //彈出矩陣
}
現在是解釋那個冒牌的輻射狀的模糊效果是如何作的時候了。
我們需要繪制這個場景——它從中心開始在所有方向上模糊出現。竅門是在沒有主要的性能瓶頸的情況下做出的。我們不能讀寫象素,并且如果我們想和非kick-butt視頻卡兼容,我們不能使用擴展名何驅動程序特殊命令。
沒辦法了嗎?
不,解決方法非常簡單,OpenGL賦予我們“模糊”紋理的能力。OK……并非真正的模糊,但我們利用線性過濾去依比例決定一個紋理,結果(有些想象成分)看起來象高斯模糊。
因此如果我們正確地在3D場景中放了大量的被拉伸的紋理并依比例決定會有什么發生?
答案比你想象的還簡單。
問題一:透視一個紋理
有一個后緩沖器在象素格式下問題容易解決。在沒有后緩沖器的情況下透視一個紋理在眼睛看來是一個真正的痛苦。
透視紋理剛好借助一個函數來完成。我們需要繪制我們的實體然后利用glCopytexImage函數復制這個結果(在交換前,后緩沖器之前)后到紋理。
問題二:在3D實體前精確地協調紋理。
我們知道:如果我們在沒有設置正確的透視的情況下改變了視口,我們就得到一個我們的實體的一個被拉伸的透視圖。例如如果我們設置一個是視口足夠寬我們就得到一個垂直地被拉伸的透視圖。
解決方法是首先設置一個視口正如我們的紋理(128×128)。透視我們的實體到這個紋理之后,我們利用當前屏幕決議著色這個紋理到屏幕。這種方法OpenGL縮減這個實體去適應紋理,并且我們拉伸紋理到全屏大小時,OpenGL重新調整紋理的大小去完美的適應在我們的3d實體頂端。希望我沒有丟掉任何一點。另一個靈活的例子是,如果你取一個640×480大小screenshot,然后調整成為256x256的位圖,你可以以一個紋理裝載這個位圖,并拉伸它使之適合640x480的屏幕。這個質量可能不會以前一樣好,但是這個紋理排列起的效果應當接近最初的640x480圖象。
On to the fun stuff! 這個函數相當簡單,并且是我的首選的“設計竅門”之一。它設置一個與我們的BlurTexture度數相匹配的大小的視口。然后它被彈簧的著色程序調用。彈簧將由于視口被拉伸適應128*128的紋理。
在彈簧被拉伸至128x128視口大小之后,我們約定BlurTexture 且用glCopyTexImage2D從視口拷貝色彩緩沖器到BlurTexture。
參數如下:
GL_TEXTURE_2D指出我們正使用一個2Dimensional紋理,0是我們想要拷貝緩沖器到mip的繪圖等級,默認等級是0。GL_LUMINANCE指出被拷貝的數據格式。我之所以使用GL_LUMINANCE因為最終結果看起來比較好。這種情形緩沖器的亮度部分將被拷貝到紋理。其它參數可以是GL_ALPHA, GL_RGB, GL_INTENSITY等等。
其次的兩個參數告訴OpenGL從(0,0)開始拷貝到哪里。寬度和高度(128,128)是從左到右有多少象素要拷貝并且上下拷貝多少。最后一個參數僅用來指出我們是否想要一個邊界——哪個不想要。
既然在我們的BlurTexture我們已經有了一個色彩緩沖器的副本(和被拉伸的彈簧一致),我們可以清除那個緩沖器,向后設置那個視口到適當的度數(640x480全屏)。
重要:
這個竅門能用在只有雙緩沖器象素格式的情況下。原因是所有這些操作從觀察者面前被隱藏起來。(在后緩沖器完成)。
void RenderToTexture() // 著色到一個紋理
{
glViewport(0,0,128,128); // 設置我們的視口
ProcessHelix(); // 著色螺旋
glBindTexture(GL_TEXTURE_2D,BlurTexture); // 綁定模糊紋理
// 拷貝我們的視口到模糊紋理 (從 0,0 到 128,128... 無邊界)
glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, 0, 0, 128, 128, 0);
glClearColor(0.0f, 0.0f, 0.5f, 0.5); //調整清晰的色彩到中等藍色
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 清屏和深度緩沖
glViewport(0 , 0,640 ,480); // 調整視口 (0,0 to 640x480)
}
DrawBlur函數僅在我們的3D場景前繪制一些混合的方塊——用BlurTexture我們以前已實現。這樣,借由阿爾發和縮放這個紋理,我們得到了真正看起來象輻射狀的模糊的效果。
我首先禁用GEN_S 和 GEN_T(我沉溺于球體影射,因此我的程序通常啟用這些指令:P)。
我們啟用2D紋理,禁用深度測試,調整正確的函數,起用混合然后約束BlurTexture。
下一件我們要作的事情是轉換到標準視圖,那樣比較容易繪制一些完美適應屏幕大小的方塊。這是我們在3D實體頂端排列紋理的方法(通過拉伸紋理匹配屏幕比例)。這是問題二要解決的地方。
void DrawBlur(int times, float inc) // 繪制模糊的圖象
{
float spost = 0.0f; // 紋理坐標偏移量
float alphainc = 0.9f / times; // alpha混合的衰減量
float alpha = 0.2f; // Alpha初值
// 禁用自動生成紋理坐標
glDisable(GL_TEXTURE_GEN_S);
glDisable(GL_TEXTURE_GEN_T);
glEnable(GL_TEXTURE_2D); // 啟用 2D 紋理映射
glDisable(GL_DEPTH_TEST); // 深度測試不可用
glBlendFunc(GL_SRC_ALPHA,GL_ONE); // 設置混合模式
glEnable(GL_BLEND); // 啟用混合
glBindTexture(GL_TEXTURE_2D,BlurTexture); // 綁定混合紋理
ViewOrtho(); // 切換到標準視圖
alphainc = alpha / times; // 減少alpha值
我們多次繪制這個紋理用于創建那個輻射效果, 縮放這個紋理坐標并且每次我們做另一個關口時增大混合因數 。我們繪制25個方塊,每次按照0.015f拉伸這個紋理。
glBegin(GL_QUADS); // 開始繪制方塊
for (int num = 0;num < times;num++) // 著色模糊物的次數
{
glColor4f(1.0f, 1.0f, 1.0f, alpha); // 調整alpha值
glTexCoord2f(0+spost,1-spost);
glVertex2f(0,0);
glTexCoord2f(0+spost,0+spost);
glVertex2f(0,480);
glTexCoord2f(1-spost,0+spost);
glVertex2f(640,480);
glTexCoord2f(1-spost,1-spost);
glVertex2f(640,0);
spost += inc; // 逐漸增加 spost (快速靠近紋理中心)
alpha = alpha - alphainc; // 逐漸增加 alpha (逐漸淡出紋理)
}
glEnd(); // 完成繪制方塊
ViewPerspective(); // 轉換到一個透視視圖
glEnable(GL_DEPTH_TEST); // 深度測試可用
glDisable(GL_TEXTURE_2D); // 2D紋理映射不可用
glDisable(GL_BLEND); // 混合不可用
glBindTexture(GL_TEXTURE_2D,0); // 釋放模糊紋理
}
瞧,這是以前從未見過的最短的繪制程序,有很棒的視覺效果!
我們調用RenderToTexture 函數。幸虧我們視口改變這個函數才著色被拉伸的彈簧。 對于我們的紋理拉伸的彈簧被著色,并且這些緩沖器被清除。
我們之后繪制“真正的”彈簧 (你在屏幕上看到的3D實體) 通過調用 ProcessHelix( )。
最后我們在彈簧前面繪制一些混合的方塊。有織紋的方塊將被拉伸以適應在真正的3D彈簧
上面。
void Draw (void) // 繪制場景
{
glClearColor(0.0f, 0.0f, 0.0f, 0.5); // 將清晰的顏色設定為黑色
glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 清除屏幕和深度緩沖器
glLoadIdentity(); // 重置視圖
RenderToTexture(); // 著色紋理
ProcessHelix(); // 繪制我們的螺旋
DrawBlur(25,0.02f); // 繪制模糊效果
glFlush (); // 強制OpenGL繪制我們所有的圖形
}
我希望你滿意這篇指南,它實在沒有比透視一個紋理講授更多其它內容,但它是一個干脆地添加到你的3D應用程序中有趣的效果。
如果你有任何的注釋建議或者如果你知道一種更好的方法執行這個效果聯系我rio@spinningkids.org。
我也想要委托你去做一列事情(家庭作業):D
1) 更改DrawBlur程序變為一個水平的模糊之物,垂直的模糊之物和一些更好的效果。(轉動模糊之物!)。
2) 玩轉DrawBlur參數(添加,刪除)變為一個好的程序和你的音樂同步。
3) 用GL_LUMINANCE玩弄DrawBlur參數和一個SMALL紋理(驚人的光亮!)。
4) 用暗色紋理代替亮色嘗試大騙(哈哈,自己造的)測定體積的陰影。
好了,這應該是所有的了(到此為止)。
訪問我的站點http://www.spinningkids.org/rio.
獲得更多的最新指南。
Demos是被用來展示恰似風雅的技術一樣無限并且時而嚴酷的譯碼。在今天的演示中你通常總可以發現一些真正迷人的效果。這不是一本迷人的效果指南,但結果將非常的酷!你能夠從http://www.pouet.net和 http://ftp.scene.org. 發現大量的演示收集。
既然緒論超出了我們探討的范圍,我們可以繼續我們的指南了。
我將解釋如何做一個看起來象徑向模糊的eye candy 效果。有時它以測定體積的光線被提到。不要相信,它僅僅是一個冒牌的輻射狀模糊;D
輻射狀模糊效果通常借助于模糊在一個方向上相對于模糊物的中心原始圖象的每一個象素來做的。
借助于現今的硬件用色彩緩沖器來手工作模糊處理是極其困難的(至少在某種程度上它被所有的gfx卡所支持),因此我們需要一些竅門來達到同樣的效果。
作為一個獎勵當學習徑向模糊效果時,你同樣將學到如何輕松地提供材料的紋理。
我決定在這篇指南中使用彈簧作為外形因為它是一個酷的外形,另外還因為我對立方體感到厭煩:}
多留意這篇指南關于如何創建那個效果的指導方針是重要的。我不研究解釋那些代碼的詳情。你應當用心記下它們中的大部分:}
下面是變量的定義和用到的頭文件。
#include <math.h> // 數學庫
float angle; // 用來旋轉那個螺旋
float vertexes[3][3]; // 為3個設置的頂點保存浮點信息
float normal[3]; // 存放法線數據的數組
GLuint BlurTexture; // 存放紋理編號的一個無符號整型
函數EmptyTexture()創建了一個空的紋理并返回紋理的編號。我們剛分配了一些自由空間(準確的是128*128*4無符號整數)。
128*128是紋理的大小(128象素寬和高),4意味著為每一個象素我們想用4byte來存儲紅,綠,藍和ALPHA組件。
GLuint EmptyTexture() // 創建一個空的紋理
{
GLuint txtnumber; // 紋理ID
unsigned int* data; // 存儲數據
// 為紋理數據(128*128*4)建立存儲區
data = (unsigned int*)new GLuint[((128 * 128)* 4 * sizeof(unsigned int))];
在分配完空間之后我們用ZeroMemory函數清0,返回指針(數據)和被清0的存貯區的大小。
另一半需注意的重要的事情是我們設置GL_LINEAR的放大率和縮放率的方法。因為我們將被我們的紋理要求投入全部的精力并且如果被濫用,GL_NEAREST會看起來非常糟糕。
ZeroMemory(data,((128 * 128)* 4 * sizeof(unsigned int))); // 清除存儲區
glGenTextures(1, &txtnumber); // 創建一個紋理
glBindTexture(GL_TEXTURE_2D, txtnumber); // 構造紋理
glTexImage2D(GL_TEXTURE_2D, 0, 4, 128, 128, 0,
GL_RGBA, GL_UNSIGNED_BYTE, data); // 用數據中的信息構造紋理
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
delete [] data; // 釋放數據
return txtnumber; // 返回紋理ID
}
這個函數簡單規格化法線向量的長度。向量被當作有3個浮點類型的元素的數組來表示,第一個元素表示X軸,第二個表示Y,第三個表示Z。一個規格化的向量[Nv]被Vn表達為Vn=[Vox/|Vo|,Voy/|Vo|,Voz/|Vo|],這里Vo是最初的向量,|Vo|是該向量的系數(或長度),X,Y,Z它的組件。之后由向量的長度區分每一個法線向量組件。
void ReduceToUnit(float vector[3]) // 歸一化一個法向量
{ // 一定長度的單位法線向量
float length; // 保存長度
// 計算向量
length = (float)sqrt((vector[0]*vector[0]) + (vector[1]*vector[1]) + (vector[2]*vector[2]));
if(length == 0.0f) // 避免除0錯誤
length = 1.0f; // 如果為0設置為1
vector[0] /= length; // 歸一化向量
vector[1] /= length;
vector[2] /= length;
}
下面各項計算所給的3個頂點向量(總在3個浮點數組中)。我們有兩個參數:v[3][3]和out[3]。當然第一個參數是一個m=3,n=3每一行代表三角形一個頂點的浮點矩陣。Out是我們要放置作為結果的法線向量的位置。
相當簡單的數學。我們將使用著名的交叉乘積運算。理論上說交叉乘積是兩個向量——它返回另一個直交向量到兩個原始向量——之間的操作。法線向量是一個垂直物體表面的直交向量,是與該表面相對的(通常一個規格化的長度)。設想兩個向量是在一個三角形的一側的上方,那么這個三角形兩邊的直交向量(由交叉乘積計算)就是那個三角形的法線。
解釋比實行還難。
我們將著手從現存的頂點0到頂點1,從頂點1到頂點2找到那個向量。這是基本上通過減法——下一個頂點的每個組件減一個頂點的每個組件——作好了的。現在我們已經為我們的三角形的邊找到了那個向量。通過交叉相乘我們為那個三角形找到了法線向量。
看代碼。
V[0][ ]是第一個頂點,v[1][ ]是第二個頂點,v[2][ ]是第三個頂點。每個頂點包括:v[ ][0]是頂點的x坐標,v[ ][1]是頂點的y坐標,v[ ][2]是頂點的z坐標。
通過簡單的減法從一個頂點的每個坐標到另一個頂點每個坐標我們得到了那個VECTOR。v1[0] = v[0][0] - v[1][0],這計算現存的從一個頂點到另一個頂點的向量的X組件,v1[1] = v[0][1] - v[1][1]將計算Y組件,v1[2] = v[0][2] - v[1][2] 計算Z組件等等。
現在我們有了兩個向量,所以我們計算它們的交叉乘積得到那個三角形的法線。
交叉相乘的規則是:
out[x] = v1[y] * v2[z] - v1[z] * v2[y]
out[y] = v1[z] * v2[x] - v1[x] * v2[z]
out[z] = v1[x] * v2[y] - v1[y] * v2[x]
我們最終得到了這個三角形的法線in out[ ]。
void calcNormal(float v[3][3], float out[3]) // 用三點計算一個立方體法線
{
float v1[3],v2[3]; // 向量 1 (x,y,z) 和向量 2 (x,y,z)
static const int x = 0; // 定義 X坐標
static const int y = 1; // 定義 Y 坐標
static const int z = 2; // 定義 Z 坐標
// 用減法在兩點之間得到向量// 從一點到另一點的X,Y,Z坐標// 計算點1到點0的向量
v1[x] = v[0][x] - v[1][x];
v1[y] = v[0][y] - v[1][y];
v1[z] = v[0][z] - v[1][z];
// 計算點2到點1的向量
v2[x] = v[1][x] - v[2][x];
v2[y] = v[1][y] - v[2][y];
v2[z] = v[1][z] - v[2][z];
// 計算交叉乘積為我們提供一個表面的法線
out[x] = v1[y]*v2[z] - v1[z]*v2[y];
out[y] = v1[z]*v2[x] - v1[x]*v2[z];
out[z] = v1[x]*v2[y] - v1[y]*v2[x];
ReduceToUnit(out); // 規格化向量
}
下面的例子正好用gluLookAt設立了一個觀察點。我們設置一個觀察點放置在0,5,50位置——正照看0,0,0并且所屬的向上的向量正仰望(0,1,0)!:D
void ProcessHelix() // 繪制一個螺旋
{
GLfloat x; // 螺旋x坐標
GLfloat y; // 螺旋y坐標
GLfloat z; // 螺旋z坐標
GLfloat phi; // 角
GLfloat theta; // 角
GLfloat v,u; // 角
GLfloat r; // 螺旋半徑
int twists = 5; // 5個螺旋
GLfloat glfMaterialColor[]={0.4f,0.2f,0.8f,1.0f}; // 設置材料色彩
GLfloat specular[]={1.0f,1.0f,1.0f,1.0f}; // 設置鏡象燈光
glLoadIdentity(); // 重置Modelview矩陣
gluLookAt(0, 5, 50, 0, 0, 0, 0, 1, 0); // 場景(0,0,0)的視點中心 (0,5,50),Y軸向上
glPushMatrix(); // 保存Modelview矩陣
glTranslatef(0,0,-50); // 移入屏幕50個單位
glRotatef(angle/2.0f,1,0,0); // 在X軸上以1/2角度旋轉
glRotatef(angle/3.0f,0,1,0); // 在Y軸上以1/3角度旋轉
glMaterialfv(GL_FRONT_AND_BACK,GL_AMBIENT_AND_DIFFUSE,glfMaterialColor);
glMaterialfv(GL_FRONT_AND_BACK,GL_SPECULAR,specular);
然后我們計算螺旋的公式并給彈簧著色。十分簡單,我就不再解釋了,因為它不是這篇指南的主要目的。這段螺旋代碼經過軟件贊助者的許可被借用(并作了一點優化)。這是寫作的簡單的方法,但不是最塊的方法。使用頂點數組可以使它更快!
r=1.5f; // 半徑
glBegin(GL_QUADS); // 開始繪制立方體
for(phi=0; phi <= 360; phi+=20.0) // 以20度的間隔繪制
{
for(theta=0; theta<=360*twists; theta+=20.0)
{
v=(phi/180.0f*3.142f); // 計算第一個點 ( 0 )的角度
u=(theta/180.0f*3.142f); // 計算第一個點 ( 0 )的角度
x=float(cos(u)*(2.0f+cos(v) ))*r; // 計算x的位置(第一個點)
y=float(sin(u)*(2.0f+cos(v) ))*r; // 計算y的位置(第一個位置)
z=float((( u-(2.0f*3.142f)) + sin(v) ) * r); // 計算z的位置(第一個位置)
vertexes[0][0]=x; // 設置第一個頂點的x值
vertexes[0][1]=y; // 設置第一個頂點的y值
vertexes[0][2]=z; // 設置第一個頂點的z值
v=(phi/180.0f*3.142f); // 計算第二個點( 0 )的角度
u=((theta+20)/180.0f*3.142f); // 計算第二個點( 20 )的角度
x=float(cos(u)*(2.0f+cos(v) ))*r; // 計算x位置(第二個點)
y=float(sin(u)*(2.0f+cos(v) ))*r; // 計算y位置(第二個點)
z=float((( u-(2.0f*3.142f)) + sin(v) ) * r); // 計算z位置(第二個點)
vertexes[1][0]=x; // 設置第二個頂點的x值
vertexes[1][1]=y; // 設置第二個頂點的y值
vertexes[1][2]=z; // 設置第二個頂點的z值
v=((phi+20)/180.0f*3.142f); // 計算第三個點 ( 20 )的角度
u=((theta+20)/180.0f*3.142f); // 計算第三個點 ( 20 )的角度
x=float(cos(u)*(2.0f+cos(v) ))*r; // 計算x位置 (第三個點)
y=float(sin(u)*(2.0f+cos(v) ))*r; // 計算y位置 (第三個點)
z=float((( u-(2.0f*3.142f)) + sin(v) ) * r); // 計算z位置 (第三個點)
vertexes[2][0]=x; // 設置第三個頂點的x值
vertexes[2][1]=y; // 設置第三個頂點的y值
vertexes[2][2]=z; // 設置第三個頂點的z值
v=((phi+20)/180.0f*3.142f); // 計算第四個點( 20 )的角度
u=((theta)/180.0f*3.142f); // 計算第四個點( 0 )的角度
x=float(cos(u)*(2.0f+cos(v) ))*r; // 計算x位置 (第四個點)
y=float(sin(u)*(2.0f+cos(v) ))*r; // 計算y位置 (第四個點)
z=float((( u-(2.0f*3.142f)) + sin(v) ) * r); // 計算z位置 (第四個點))
vertexes[3][0]=x; // 設置第四個頂點的x值
vertexes[3][1]=y; // 設置第四個頂點的y值
vertexes[3][2]=z; // 設置第四個頂點的z值
calcNormal(vertexes,normal); // 計算立方體的法線
glNormal3f(normal[0],normal[1],normal[2]); // 設置法線
// 渲染四邊形
glVertex3f(vertexes[0][0],vertexes[0][1],vertexes[0][2]);
glVertex3f(vertexes[1][0],vertexes[1][1],vertexes[1][2]);
glVertex3f(vertexes[2][0],vertexes[2][1],vertexes[2][2]);
glVertex3f(vertexes[3][0],vertexes[3][1],vertexes[3][2]);
}
}
glEnd(); // 繪制結束
glPopMatrix(); // 取出矩陣
}
這兩個事例(ViewOrtho and ViewPerspective)被編碼以使它變得很容易地在一個直交的情形下繪制并且不費力的返回透視圖。
ViewOrtho簡單地設立了這個射影矩陣,然后增加一份現行射影矩陣的拷貝到OpenGL棧上。這個恒等矩陣然后被裝載并且當前屏幕正投影觀察決議被提出。
利用2維坐標以屏幕左上角0,0和屏幕右下角639,479來繪制是可能的。
最后,modelview矩陣為透視材料激活。
ViewPerspective設置射影矩陣模式取回ViewOrtho在堆棧上推進的非正交矩陣。然后樣本視圖被選擇因此我們可以透視材料。
我建議你保留這兩個過程,能夠著色2D而不需擔心射影矩陣很不錯。
void ViewOrtho() // 設置一個z正視圖
{
glMatrixMode(GL_PROJECTION); // 選擇投影矩陣
glPushMatrix(); // 保存當前矩陣
glLoadIdentity(); // 重置矩陣
glOrtho( 0, 640 , 480 , 0, -1, 1 ); // 選擇標準模式
glMatrixMode(GL_MODELVIEW); // 選擇樣本視圖矩陣
glPushMatrix(); // 保存當前矩陣
glLoadIdentity(); // 重置矩陣
}
void ViewPerspective() // 設置透視視圖
{
glMatrixMode( GL_PROJECTION ); // 選擇投影矩陣
glPopMatrix(); // 取出矩陣
glMatrixMode( GL_MODELVIEW ); // 選擇模型變換矩陣
glPopMatrix(); //彈出矩陣
}
現在是解釋那個冒牌的輻射狀的模糊效果是如何作的時候了。
我們需要繪制這個場景——它從中心開始在所有方向上模糊出現。竅門是在沒有主要的性能瓶頸的情況下做出的。我們不能讀寫象素,并且如果我們想和非kick-butt視頻卡兼容,我們不能使用擴展名何驅動程序特殊命令。
沒辦法了嗎?
不,解決方法非常簡單,OpenGL賦予我們“模糊”紋理的能力。OK……并非真正的模糊,但我們利用線性過濾去依比例決定一個紋理,結果(有些想象成分)看起來象高斯模糊。
因此如果我們正確地在3D場景中放了大量的被拉伸的紋理并依比例決定會有什么發生?
答案比你想象的還簡單。
問題一:透視一個紋理
有一個后緩沖器在象素格式下問題容易解決。在沒有后緩沖器的情況下透視一個紋理在眼睛看來是一個真正的痛苦。
透視紋理剛好借助一個函數來完成。我們需要繪制我們的實體然后利用glCopytexImage函數復制這個結果(在交換前,后緩沖器之前)后到紋理。
問題二:在3D實體前精確地協調紋理。
我們知道:如果我們在沒有設置正確的透視的情況下改變了視口,我們就得到一個我們的實體的一個被拉伸的透視圖。例如如果我們設置一個是視口足夠寬我們就得到一個垂直地被拉伸的透視圖。
解決方法是首先設置一個視口正如我們的紋理(128×128)。透視我們的實體到這個紋理之后,我們利用當前屏幕決議著色這個紋理到屏幕。這種方法OpenGL縮減這個實體去適應紋理,并且我們拉伸紋理到全屏大小時,OpenGL重新調整紋理的大小去完美的適應在我們的3d實體頂端。希望我沒有丟掉任何一點。另一個靈活的例子是,如果你取一個640×480大小screenshot,然后調整成為256x256的位圖,你可以以一個紋理裝載這個位圖,并拉伸它使之適合640x480的屏幕。這個質量可能不會以前一樣好,但是這個紋理排列起的效果應當接近最初的640x480圖象。
On to the fun stuff! 這個函數相當簡單,并且是我的首選的“設計竅門”之一。它設置一個與我們的BlurTexture度數相匹配的大小的視口。然后它被彈簧的著色程序調用。彈簧將由于視口被拉伸適應128*128的紋理。
在彈簧被拉伸至128x128視口大小之后,我們約定BlurTexture 且用glCopyTexImage2D從視口拷貝色彩緩沖器到BlurTexture。
參數如下:
GL_TEXTURE_2D指出我們正使用一個2Dimensional紋理,0是我們想要拷貝緩沖器到mip的繪圖等級,默認等級是0。GL_LUMINANCE指出被拷貝的數據格式。我之所以使用GL_LUMINANCE因為最終結果看起來比較好。這種情形緩沖器的亮度部分將被拷貝到紋理。其它參數可以是GL_ALPHA, GL_RGB, GL_INTENSITY等等。
其次的兩個參數告訴OpenGL從(0,0)開始拷貝到哪里。寬度和高度(128,128)是從左到右有多少象素要拷貝并且上下拷貝多少。最后一個參數僅用來指出我們是否想要一個邊界——哪個不想要。
既然在我們的BlurTexture我們已經有了一個色彩緩沖器的副本(和被拉伸的彈簧一致),我們可以清除那個緩沖器,向后設置那個視口到適當的度數(640x480全屏)。
重要:
這個竅門能用在只有雙緩沖器象素格式的情況下。原因是所有這些操作從觀察者面前被隱藏起來。(在后緩沖器完成)。
void RenderToTexture() // 著色到一個紋理
{
glViewport(0,0,128,128); // 設置我們的視口
ProcessHelix(); // 著色螺旋
glBindTexture(GL_TEXTURE_2D,BlurTexture); // 綁定模糊紋理
// 拷貝我們的視口到模糊紋理 (從 0,0 到 128,128... 無邊界)
glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, 0, 0, 128, 128, 0);
glClearColor(0.0f, 0.0f, 0.5f, 0.5); //調整清晰的色彩到中等藍色
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 清屏和深度緩沖
glViewport(0 , 0,640 ,480); // 調整視口 (0,0 to 640x480)
}
DrawBlur函數僅在我們的3D場景前繪制一些混合的方塊——用BlurTexture我們以前已實現。這樣,借由阿爾發和縮放這個紋理,我們得到了真正看起來象輻射狀的模糊的效果。
我首先禁用GEN_S 和 GEN_T(我沉溺于球體影射,因此我的程序通常啟用這些指令:P)。
我們啟用2D紋理,禁用深度測試,調整正確的函數,起用混合然后約束BlurTexture。
下一件我們要作的事情是轉換到標準視圖,那樣比較容易繪制一些完美適應屏幕大小的方塊。這是我們在3D實體頂端排列紋理的方法(通過拉伸紋理匹配屏幕比例)。這是問題二要解決的地方。
void DrawBlur(int times, float inc) // 繪制模糊的圖象
{
float spost = 0.0f; // 紋理坐標偏移量
float alphainc = 0.9f / times; // alpha混合的衰減量
float alpha = 0.2f; // Alpha初值
// 禁用自動生成紋理坐標
glDisable(GL_TEXTURE_GEN_S);
glDisable(GL_TEXTURE_GEN_T);
glEnable(GL_TEXTURE_2D); // 啟用 2D 紋理映射
glDisable(GL_DEPTH_TEST); // 深度測試不可用
glBlendFunc(GL_SRC_ALPHA,GL_ONE); // 設置混合模式
glEnable(GL_BLEND); // 啟用混合
glBindTexture(GL_TEXTURE_2D,BlurTexture); // 綁定混合紋理
ViewOrtho(); // 切換到標準視圖
alphainc = alpha / times; // 減少alpha值
我們多次繪制這個紋理用于創建那個輻射效果, 縮放這個紋理坐標并且每次我們做另一個關口時增大混合因數 。我們繪制25個方塊,每次按照0.015f拉伸這個紋理。
glBegin(GL_QUADS); // 開始繪制方塊
for (int num = 0;num < times;num++) // 著色模糊物的次數
{
glColor4f(1.0f, 1.0f, 1.0f, alpha); // 調整alpha值
glTexCoord2f(0+spost,1-spost);
glVertex2f(0,0);
glTexCoord2f(0+spost,0+spost);
glVertex2f(0,480);
glTexCoord2f(1-spost,0+spost);
glVertex2f(640,480);
glTexCoord2f(1-spost,1-spost);
glVertex2f(640,0);
spost += inc; // 逐漸增加 spost (快速靠近紋理中心)
alpha = alpha - alphainc; // 逐漸增加 alpha (逐漸淡出紋理)
}
glEnd(); // 完成繪制方塊
ViewPerspective(); // 轉換到一個透視視圖
glEnable(GL_DEPTH_TEST); // 深度測試可用
glDisable(GL_TEXTURE_2D); // 2D紋理映射不可用
glDisable(GL_BLEND); // 混合不可用
glBindTexture(GL_TEXTURE_2D,0); // 釋放模糊紋理
}
瞧,這是以前從未見過的最短的繪制程序,有很棒的視覺效果!
我們調用RenderToTexture 函數。幸虧我們視口改變這個函數才著色被拉伸的彈簧。 對于我們的紋理拉伸的彈簧被著色,并且這些緩沖器被清除。
我們之后繪制“真正的”彈簧 (你在屏幕上看到的3D實體) 通過調用 ProcessHelix( )。
最后我們在彈簧前面繪制一些混合的方塊。有織紋的方塊將被拉伸以適應在真正的3D彈簧
上面。
void Draw (void) // 繪制場景
{
glClearColor(0.0f, 0.0f, 0.0f, 0.5); // 將清晰的顏色設定為黑色
glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 清除屏幕和深度緩沖器
glLoadIdentity(); // 重置視圖
RenderToTexture(); // 著色紋理
ProcessHelix(); // 繪制我們的螺旋
DrawBlur(25,0.02f); // 繪制模糊效果
glFlush (); // 強制OpenGL繪制我們所有的圖形
}
我希望你滿意這篇指南,它實在沒有比透視一個紋理講授更多其它內容,但它是一個干脆地添加到你的3D應用程序中有趣的效果。
如果你有任何的注釋建議或者如果你知道一種更好的方法執行這個效果聯系我rio@spinningkids.org。
我也想要委托你去做一列事情(家庭作業):D
1) 更改DrawBlur程序變為一個水平的模糊之物,垂直的模糊之物和一些更好的效果。(轉動模糊之物!)。
2) 玩轉DrawBlur參數(添加,刪除)變為一個好的程序和你的音樂同步。
3) 用GL_LUMINANCE玩弄DrawBlur參數和一個SMALL紋理(驚人的光亮!)。
4) 用暗色紋理代替亮色嘗試大騙(哈哈,自己造的)測定體積的陰影。
好了,這應該是所有的了(到此為止)。
訪問我的站點http://www.spinningkids.org/rio.
獲得更多的最新指南。