[OpenGL ES 07-2]Per-Vertex Light及深度緩存
[OpenGL ES 07-2]Per-Vertex Light及深度緩存
羅朝輝 (http://www.rzrgm.cn/kesalin/)
本文遵循“署名-非商業(yè)用途-保持一致”創(chuàng)作公用協(xié)議
這是《OpenGL ES 教程》的第八篇,前七篇請(qǐng)參考如下鏈接:
[OpenGL ES 01]iOS上OpenGL ES之初體驗(yàn)
[OpenGL ES 02]OpenGL ES渲染管線與著色器
[OpenGL ES 03]3D變換:模型,視圖,投影與Viewport
[OpenGL ES 04]3D變換實(shí)踐篇:平移,旋轉(zhuǎn),縮放
[OpenGL ES 05]相對(duì)空間變換及顏色
[OpenGL ES 06]使用VBO:頂點(diǎn)緩存
[OpenGL ES 07-1]光照原理
前言
在前文《[OpenGL ES 07-1]光照原理》中已經(jīng)介紹 Opengl 中的光照原理,接下來將演示如何將這些原理用 OpenGL ES 2.0 來實(shí)現(xiàn)。今天的這篇文章將介紹 Per-Vertex Light 以及深度緩存,下一篇文章將介紹 Per-Pixel Light 以及卡通效果。還記得在第六篇文章的末尾留了一個(gè)小作業(yè),用頂點(diǎn)緩存描繪一個(gè)立方體么 Cube?在這篇文章就會(huì)用到它。Per-Vertex Light 示例源碼在這里,運(yùn)行效果如下:

在開始之前,先來回顧一下 Per-Vertex Light 是怎么回事。Per-Vertex Light 也稱為 Gauroud 著色,它是在頂點(diǎn)著色階段對(duì)每一個(gè)頂點(diǎn)進(jìn)行顏色計(jì)算,然后在光柵化階段利用這些頂點(diǎn)顏色進(jìn)行線性插值形成片元的顏色。
一,準(zhǔn)備工作
1,新建工程
和前面的文章一樣,新建名為 Tutorial07 的 Single View Application,導(dǎo)入 OpenGLES.framework 和 QuartzCore.framework。然后將 Tutorial06 中的 Utils, Shader,Surface三個(gè)目錄以及 OpenGLView.h/m 兩個(gè)文件拷貝到 Tutorial07 中,并在 XCode 中將它們加入進(jìn)來。
2,添加 Cube 類型的 VBO
將第六篇的小作業(yè):為 Cube 的 VBO 的那部分代碼 - (DrawableVBO *)createVBOsForCube 加入到 OpenGLView.m 中,并修改 - (void)setupVBOs 的實(shí)現(xiàn),在 _vboArray 加入 Cube 這個(gè)類型。這部分代碼與本文主題不相干,所以就不在這里累述了,詳情請(qǐng)參看源代碼。
3,添加控制控件
參照效果圖,在 Storyboard 中添加相關(guān)控件:

4,添加響應(yīng)代碼
和前面的示例一樣,在 ViewController 中加入相關(guān)響應(yīng)代碼,并使用拖拽技巧與 Storyboard 中的對(duì)應(yīng)控件關(guān)聯(lián)起來。下面只列出一部分代碼,完整代碼請(qǐng)參考源代碼。
@property (nonatomic, strong) IBOutlet OpenGLView * openGLView; @property (nonatomic, strong) IBOutlet UISlider * lightXSlider; // ... - (IBAction)lightXSliderValueChanged:(id)sender; // ... - (IBAction)segmentSelectionChanged:(id)sender;
二,Per-Vertex Light 實(shí)現(xiàn)
1,修改頂點(diǎn)著色器
在本文中,光照計(jì)算的實(shí)際工作都是在頂點(diǎn)著色器中進(jìn)行的,因此首先修改頂點(diǎn)著色器 VertexShader.glsl 如下:
uniform mat4 projection;
uniform mat4 modelView;
attribute vec4 vPosition;
uniform mat3 normalMatrix;
uniform vec3 vLightPosition;
uniform vec4 vAmbientMaterial;
uniform vec4 vSpecularMaterial;
uniform float shininess;
attribute vec3 vNormal;
attribute vec4 vDiffuseMaterial;
varying vec4 vDestinationColor;
void main(void)
{
gl_Position = projection * modelView * vPosition;
vec3 N = normalMatrix * vNormal;
vec3 L = normalize(vLightPosition);
vec3 E = vec3(0, 0, 1);
vec3 H = normalize(L + E);
float df = max(0.0, dot(N, L));
float sf = max(0.0, dot(N, H));
sf = pow(sf, shininess);
vDestinationColor = vAmbientMaterial + df * vDiffuseMaterial + sf * vSpecularMaterial;
//vDestinationColor = vec4(1.0, 0.0, 0.0, 1.0);
}
在這里,添加了光源位置:vLightPosition,以及三種類型的材質(zhì)屬性:環(huán)境材質(zhì) vAmbientMaterial,漫反射材質(zhì) vDiffuseMaterial 和鏡面反射材質(zhì) vSpecularMaterial。這些都在前文《光照原理》中介紹過了,在這里就不再重復(fù)了。光照計(jì)算的最終顏色等于三種類型的光照效果的累加:
vDestinationColor = vAmbientMaterial + df * vDiffuseMaterial + sf * vSpecularMaterial;
df 漫反射因子:它是光線與頂點(diǎn)法線向量的點(diǎn)積,幾何意義就是光線 L 與法線 N 之間夾角的 cos 值;
sf 鏡面反射因子:它是視線 E 與光線 L 形成的夾角的平分線 H 與頂點(diǎn)法線向量的點(diǎn)積(幾何意義就是平分線 H 與頂點(diǎn)法線 N 之間夾角的 cos 值),再 shininess 次方;
平分線向量 H:它是通過將視線向量 E 與 光線向量 L 相加,并規(guī)范化計(jì)算而來;
shininess 光澤強(qiáng)度:它由 OpenGL 程序傳入。當(dāng)然啦,最終是在 UI 通過 shininess 這個(gè)滑塊控制的,該值越小,光澤強(qiáng)度越大。
前面提到過,OpenGL 中的很多計(jì)算都要將向量規(guī)劃化,在這里就體現(xiàn)出來了,比如法線,平分線,位置向量等。在上面的代碼中,還從 OpenGL 程序中傳入了法線變換矩陣 normalMatrix,這個(gè)值得一說。
2,法線變換矩陣
為什么需要法線變換矩陣呢?因?yàn)榉ň€向量與頂點(diǎn)向量一樣,是在物體的模型空間中,而光照計(jì)算通常是在視圖空間中進(jìn)行的,因此我們需要將模型空間中的法線向量變換到視圖空間,這是原因之一?;蛟S你或說,這個(gè)變換直接用和用于頂點(diǎn)變換的模型視圖變換矩陣 modelView 就可以了呀。誠(chéng)然,當(dāng)模型視圖變換是剛體變換時(shí),法線變換矩陣與模型視圖變換矩陣完全一樣;但如果模型視圖變換不是剛體變換時(shí),兩者就不相同了。所謂剛體變換就是說物體在 x, y, z 三個(gè)方向進(jìn)行了等比的縮放操作。只有剛體變換這種情況下,頂點(diǎn)的法線向量方向才不會(huì)改變,而在非剛體變換時(shí),法線向量的方向會(huì)有變化。試想一下,圓球上頂點(diǎn)的法線向量是從球心指向頂點(diǎn),當(dāng)將圓球在 y 方向進(jìn)行縮放而 x,z 方向保持不變,經(jīng)過這樣的非剛體變換之后,這個(gè)壓扁的“橄欖球”上的頂點(diǎn)的法線向量肯定有一部分不再是從球心指向頂點(diǎn)了。非剛體變換的法線變換矩陣計(jì)算公式如下:
非剛體變換的法線變換矩陣 = 模型視圖變換矩陣的逆矩陣的倒置矩陣
這個(gè)計(jì)算過程分為兩步:首先對(duì)模型視圖變換矩陣求逆,然后再倒置(即交換行列元素)。進(jìn)行剛體變換的模型視圖變換矩陣的逆矩陣的倒置矩陣就等于模型視圖變換矩陣自身,所以在進(jìn)行剛體變換時(shí)(如本例),只需要將,法線變換矩陣的值設(shè)置為模型視圖變換矩陣的值即可。
三,設(shè)置光照
1,訪問頂點(diǎn)著色器變量
和教程 6 一樣,需要在 OpenGLView 中添加訪問頂點(diǎn)著色器中變量的相關(guān)槽位變量,以及設(shè)置光照參數(shù)的變量。在OpenGLView.h 中,添加如下變量:
GLuint _positionSlot;
GLuint _modelViewSlot;
GLuint _projectionSlot;
GLuint _normalMatrixSlot;
GLuint _lightPositionSlot;
GLint _normalSlot;
GLint _ambientSlot;
GLint _diffuseSlot;
GLint _specularSlot;
GLint _shininessSlot;
KSMatrix4 _modelViewMatrix;
KSMatrix4 _projectionMatrix;
KSVec3 _lightPosition;
KSColor _ambient;
KSColor _diffuse;
KSColor _specular;
GLfloat _shininess;
由于我們需要在 UI 上控制一些光照參數(shù),因此需要將上面中的一些變量聲明為屬性,以方便 UI 更新它們,這些光照參數(shù)在更新之后需要重繪才能立即看到效果:
// OpenGLView.h
//
@property (nonatomic, assign) KSVec3 lightPosition;
@property (nonatomic, assign) KSColor ambient;
@property (nonatomic, assign) KSColor diffuse;
@property (nonatomic, assign) KSColor specular;
@property (nonatomic, assign) GLfloat shininess;
// OpenGLView.m
//
@synthesize lightPosition = _lightPosition;
@synthesize ambient = _ambient;
@synthesize diffuse = _diffuse;
@synthesize specular = _specular;
@synthesize shininess = _shininess;
#pragma mark Properties
-(void)setAmbient:(KSColor)ambient
{
_ambient = ambient;
[self render];
}
-(void)setSpecular:(KSColor)specular
{
_specular = specular;
[self render];
}
- (void)setLightPosition:(KSVec3)lightPosition
{
_lightPosition = lightPosition;
[self render];
}
-(void)setDiffuse:(KSColor)diffuse
{
_diffuse = diffuse;
[self render];
}
-(void)setShininess:(GLfloat)shininess
{
_shininess = shininess;
[self render];
}
2,訪問槽位
在 OpenGLView.m 中添加 getSlotsFromProgram 方法,并在 setupProgram 方法的最后調(diào)用它。
- (void)getSlotsFromProgram
{
// Get the attribute and uniform slot from program
//
_projectionSlot = glGetUniformLocation(_programHandle, "projection");
_modelViewSlot = glGetUniformLocation(_programHandle, "modelView");
_normalMatrixSlot = glGetUniformLocation(_programHandle, "normalMatrix");
_lightPositionSlot = glGetUniformLocation(_programHandle, "vLightPosition");
_ambientSlot = glGetUniformLocation(_programHandle, "vAmbientMaterial");
_specularSlot = glGetUniformLocation(_programHandle, "vSpecularMaterial");
_shininessSlot = glGetUniformLocation(_programHandle, "shininess");
_positionSlot = glGetAttribLocation(_programHandle, "vPosition");
_normalSlot = glGetAttribLocation(_programHandle, "vNormal");
_diffuseSlot = glGetAttribLocation(_programHandle, "vDiffuseMaterial");
}
3,初始化和更新光照參數(shù)
在 OpenGLView.m 中添加 setupLights 和 updateLights 的方法,對(duì)光照參數(shù)進(jìn)行初始化和更新光照參數(shù)到頂點(diǎn)著色器中:
- (void)setupLights
{
// Initialize various state.
//
glEnableVertexAttribArray(_positionSlot);
glEnableVertexAttribArray(_normalSlot);
// Set up some default material parameters.
//
_lightPosition.x = _lightPosition.y = _lightPosition.z = 1.0;
_ambient.r = _ambient.g = _ambient.b = 0.04;
_specular.r = _specular.g = _specular.b = 0.5;
_diffuse.r = 0.0;
_diffuse.g = 0.5;
_diffuse.b = 1.0;
_shininess = 10;
}
- (void)updateLights
{
glUniform3f(_lightPositionSlot, _lightPosition.x, _lightPosition.y, _lightPosition.z);
glUniform4f(_ambientSlot, _ambient.r, _ambient.g, _ambient.b, _ambient.a);
glUniform4f(_specularSlot, _specular.r, _specular.g, _specular.b, _specular.a);
glVertexAttrib4f(_diffuseSlot, _diffuse.r, _diffuse.g, _diffuse.b, _diffuse.a);
glUniform1f(_shininessSlot, _shininess);
}
setupLights 方法在 - (id)initWithCoder:(NSCoder *)aDecoder 中 setProjection 之后被調(diào)用,而 updateLights 在 updateSurface 的最后被調(diào)用,該方法在每次渲染時(shí)都會(huì)被調(diào)用(其實(shí)只需要在光照參數(shù)有變換時(shí)調(diào)用即可,在這里偷懶沒有做這個(gè)優(yōu)化了)。
前面說過,在頂點(diǎn)著色器中需要利用法線變換矩陣變換法線到視圖空間,因此,也需要在程序中設(shè)置法線變換矩陣。在這里進(jìn)行的是剛體變換,所以只需要將模型變換矩陣的值賦值給法線變換矩陣即可。這個(gè)賦值是在 updateSurface 方法中進(jìn)行的,下面是 updateSurface 的完整代碼:
- (void)updateSurface
{
ksMatrixLoadIdentity(&_modelViewMatrix);
ksTranslate(&_modelViewMatrix, 0.0, 0.0, -8);
ksMatrixMultiply(&_modelViewMatrix, &_rotationMatrix, &_modelViewMatrix);
// Load the model-view matrix
glUniformMatrix4fv(_modelViewSlot, 1, GL_FALSE, (GLfloat*)&_modelViewMatrix.m[0][0]);
// Load the normal matrix.
// It's orthogonal, so its Inverse-Transpose is itself!
//
KSMatrix3 normalMatrix3;
ksMatrix4ToMatrix3(&normalMatrix3, &_modelViewMatrix);
glUniformMatrix3fv(_normalMatrixSlot, 1, GL_FALSE, (GLfloat*)&normalMatrix3.m[0][0]);
[self updateLights];
}
4,至此,光照設(shè)置完成。如果沒出什么差錯(cuò)的話,編譯應(yīng)該是能夠運(yùn)行了。效果如下:

在上圖中,我們確實(shí)可以看到光照的效果了。但是,這樣的效果實(shí)在太二了!為什么會(huì)有不該出現(xiàn)的陰影呢?哪些陰影就是啥?且看下段分解!
四,Depth Buffer-深度緩存
1,上面的陰影問題分析
從上面的圖中我們可以看到圓球的表面有一部分似乎被一些陰影給遮蓋了,沒錯(cuò),確實(shí)是這樣的。那這些陰影又是從何而來呢?它們也是球體的一部分,只不過是屬于另外一個(gè)半球的-后半球面上的。在現(xiàn)實(shí)生活中,當(dāng)我們看到一個(gè)球時(shí),只能看到一個(gè)球的前半球,后半球是被擋住了,看不到。但在 OpenGL 中渲染一個(gè)球時(shí),前半球和后半球都會(huì)被渲染出來,這樣就會(huì)出現(xiàn)前后兩個(gè)半球上的x,y相同只是z值不同兩個(gè)點(diǎn)會(huì)被描繪到屏幕上的同一個(gè)像素上,先渲染的點(diǎn)會(huì)被后渲染的點(diǎn)給覆蓋掉。因此,如果前半球面上的點(diǎn)(Z值較小的那一個(gè))先被渲染,隨后后半球面上的點(diǎn)(Z值較大的那一個(gè))被渲染,會(huì)覆蓋前半球面上的點(diǎn),因而出現(xiàn)了上面那樣的情況。解決這個(gè)問題的辦法就是在渲染之前,對(duì)Z值進(jìn)行比較,不渲染 Z 值較大的點(diǎn)。在 OpenGL 中,這是通過 Depth Test 實(shí)現(xiàn)的,之所以叫做深度測(cè)試就是因?yàn)楸容^的是 Z 值 - 深度-從屏幕往里。還記得教程02中渲染管線流程圖么?深度測(cè)試是在模版測(cè)試之后,blending 之前。

2,Depth Buffer 簡(jiǎn)介
要進(jìn)行Z值的比較(深度測(cè)試),那么 OpenGL 需要有一個(gè)地方來保存Z值,這個(gè)地方就是 Depth Buffer。Depth Buffer 與 Stencil Buffer,Color Buffer 并稱 OpenGL 三大緩存。Depth Buffer 和 Stencil Buffer 也是 render buffer,但與 Color Buffer 不同(RGBA四元組),它們均只有一個(gè)組成元素,Depth Buffer 只需要保存Z值,而 Stencil Buffer(模版緩存)也只需要保存一個(gè)用于模版測(cè)試的值(后面會(huì)有文章介紹)。
3,使用 Depth Buffer
既然 Depth Buffer 也是 render buffer,那么其創(chuàng)建與刪除與之前在教程01中介紹的 color buffer 別無二樣:
// OpenGLView.h
//
GLuint _depthRenderBuffer;
// OpenGLView.m
//
// Create a depth buffer that has the same size as the color buffer.
int width, height;
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &width);
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &height);
glGenRenderbuffers(1, &_depthRenderBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, _depthRenderBuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
GL_RENDERBUFFER, _depthRenderBuffer);
刪除也是使用 glDeleteRenderbuffers 方法。
默認(rèn)情況下,OpenGL 是不會(huì)開啟深度測(cè)試的,因此需要明確調(diào)用 glEnable(GL_DEPTH_TEST) 來開啟。在 setupProjection 方法的最后添加這一句即可。
4,編譯運(yùn)行,啊哈,一切OK!

在本示例中,有很多滑塊用來控制各種光照參數(shù)(環(huán)境材質(zhì),漫反射材質(zhì),鏡面材質(zhì)以及光澤強(qiáng)度),并有兩個(gè)模型可供切換。不妨多多滑動(dòng),體驗(yàn)下不同的參數(shù)會(huì)有什么效果,從而加深對(duì) OpenGL 光照的理解。
五,結(jié)語
有了前文《光照原理》 的理論基礎(chǔ),今天來實(shí)踐 Per-Vextex 光照,非常容易吧。接下來將介紹 Per-Pixel 光照以及卡通效果,性急的童鞋可以先瀏覽源代碼,看看能不能看個(gè)明白。BTW,教程代碼已經(jīng)非常超前了-寫到教程13了,寫文章的速度大大落后了。寫文章花費(fèi)的時(shí)間和精力實(shí)在是不少的,深度體會(huì)在中國(guó)寫一本書的辛苦,而往往回報(bào)遠(yuǎn)不及付出,嗯,要多多支持正版。
浙公網(wǎng)安備 33010602011771號(hào)