獲取示例代碼
本文主要介紹如何使用Shader實現平行光的效果。什么是平行光呢?我們可以拿激光做比喻,平行光的方向不會隨著離光源的距離而改變。所以我們在模擬平行光的時候僅僅需要使用一個光照方向即可。
我們有了光照方向,接下來還需要一個重要數據,平面的朝向。一個平面如果剛好面朝光線,那自然是最亮的。當然還有些材質的平面可以反射光線,反射光線的強度和你觀察的角度相關,不過這些本文都不會介紹。后面會有專門一篇介紹復雜的光照模型。
我們用法線向量來表示平面朝向,在具體實現中,每個點都會有一個法線向量。所謂法線向量就是垂直于平面的一個三維向量,如下圖所示。
圖中展示了兩種法線向量的表示方法,左邊是每個多邊形的每個點有一個法線向量,右邊是每個點有一個法線向量,共享點的法線向量是這個點在所有平面上的法線向量之和。法線向量應該總是被規范化成單位向量。本文的例子中使用的是左邊的方式。
如果你對向量相關的知識不是很了解,可以參考百度百科
有了法線向量和光照方向之后,只要將它們相乘即可得到光照強度。接下來開始分析代碼。
兩個單位向量相乘,結果是cos(向量夾角),夾角越大,cos(向量夾角)越小,剛好符合前面說的規律。
首先我們來看Vertex Shader。
attribute vec4 position;
attribute vec3 normal;
uniform float elapsedTime;
uniform mat4 projectionMatrix;
uniform mat4 cameraMatrix;
uniform mat4 modelMatrix;
varying vec3 fragNormal;
void main(void) {
mat4 mvp = projectionMatrix * cameraMatrix * modelMatrix;
fragNormal = normal;
gl_Position = mvp * position;
}
我將attribute vec4 color;
換成了attribute vec3 normal;
,不再傳遞顏色數據,改為法線向量。然后將法線向量傳遞給Fragment ShaderfragNormal = normal;
。
接下來是Fragment Shader。
precision highp float;
varying vec3 fragNormal;
uniform float elapsedTime;
uniform vec3 lightDirection;
uniform mat4 normalMatrix;
void main(void) {
vec3 normalizedLightDirection = normalize(-lightDirection);
vec3 transformedNormal = normalize((normalMatrix * vec4(fragNormal, 1.0)).xyz);
float diffuseStrength = dot(normalizedLightDirection, transformedNormal);
diffuseStrength = clamp(diffuseStrength, 0.0, 1.0);
vec3 diffuse = vec3(diffuseStrength);
vec3 ambient = vec3(0.3);
vec4 finalLightStrength = vec4(ambient + diffuse, 1.0);
vec4 materialColor = vec4(1.0, 0.0, 0.0, 1.0);
gl_FragColor = finalLightStrength * materialColor;
}
我增加了光線方向uniform vec3 lightDirection;
,法線變換矩陣uniform mat4 normalMatrix;
。
法線不能直接使用modelMatrix進行變換,需要使用modelMatrix的逆轉置矩陣,參考維基百科
因為光線是照射到平面的方向,而法線是從平面往外的方向,所以他們相乘之前需要把光照方向反過來,并且要規范化。
vec3 normalizedLightDirection = normalize(-lightDirection);
接著我們將法線變換后再規范化,我們就得到了關鍵的兩個向量。下面是示意圖。
將它們相乘最后得到
diffuse
,可以稱它為漫反射強度。漫反射就是投射在粗糙表面上的光向各個方向反射的現象。我們求解diffuse
就是模擬的漫反射現象。代碼最后還有一個
vec3 ambient = vec3(0.3);
是什么呢?根據漫反射的公式,總會有強度為0的地方,為了使場景不那么暗,就增加了一個基本光照強度,也可稱為環境光強度。環境光強度加上漫反射強度就是最后的光照強度
finalLightStrength
了。光照強度乘以材質本身的顏色materialColor
得到最終的顏色,這里材質本身的顏色我用的是紅色。
看完Shader,我們回到OC代碼。首先,我將綁定Shader屬性color的代碼改為了綁定normal。
- (void)bindAttribs:(GLfloat *)triangleData {
// 啟用Shader中的兩個屬性
// attribute vec4 position;
// attribute vec4 color;
GLuint positionAttribLocation = glGetAttribLocation(self.shaderProgram, "position");
glEnableVertexAttribArray(positionAttribLocation);
GLuint colorAttribLocation = glGetAttribLocation(self.shaderProgram, "normal");
glEnableVertexAttribArray(colorAttribLocation);
// 為shader中的position和color賦值
// glVertexAttribPointer (GLuint indx, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid* ptr)
// indx: 上面Get到的Location
// size: 有幾個類型為type的數據,比如位置有x,y,z三個GLfloat元素,值就為3
// type: 一般就是數組里元素數據的類型
// normalized: 暫時用不上
// stride: 每一個點包含幾個byte,本例中就是6個GLfloat,x,y,z,r,g,b
// ptr: 數據開始的指針,位置就是從頭開始,顏色則跳過3個GLFloat的大小
glVertexAttribPointer(positionAttribLocation, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (char *)triangleData);
glVertexAttribPointer(colorAttribLocation, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (char *)triangleData + 3 * sizeof(GLfloat));
}
接著準備法線向量的數據。
- (void)drawXPlanes {
static GLfloat triangleData[] = {
// X軸0.5處的平面
0.5, -0.5, 0.5f, 1, 0, 0,
0.5, -0.5f, -0.5f, 1, 0, 0,
0.5, 0.5f, -0.5f, 1, 0, 0,
0.5, 0.5, -0.5f, 1, 0, 0,
0.5, 0.5f, 0.5f, 1, 0, 0,
0.5, -0.5f, 0.5f, 1, 0, 0,
// X軸-0.5處的平面
-0.5, -0.5, 0.5f, -1, 0, 0,
-0.5, -0.5f, -0.5f, -1, 0, 0,
-0.5, 0.5f, -0.5f, -1, 0, 0,
-0.5, 0.5, -0.5f, -1, 0, 0,
-0.5, 0.5f, 0.5f, -1, 0, 0,
-0.5, -0.5f, 0.5f, -1, 0, 0,
};
[self bindAttribs:triangleData];
glDrawArrays(GL_TRIANGLES, 0, 12);
}
以X軸上的兩個平面為例,X軸0.5處的平面法線方向是X軸正向,X軸-0.5處的平面法線方向是X軸反向。這樣我們才能讓朝外的面接收到光線。法線是三維向量,所以剛好填滿了之前顏色的數據區間。
下一步準備一個三維向量存放光照的方向。
@property (assign, nonatomic) GLKVector3 lightDirection; // 平行光光照方向
并給它賦值。讓它向下照射,所以向量為-Y軸(0,-1,0)。
// 設置平行光方向
self.lightDirection = GLKVector3Make(0, -1, 0);
最后給uniform光照方向和法線變換矩陣賦值。
bool canInvert;
GLKMatrix4 normalMatrix = GLKMatrix4InvertAndTranspose(self.modelMatrix, &canInvert);
if (canInvert) {
GLuint modelMatrixUniformLocation = glGetUniformLocation(self.shaderProgram, "normalMatrix");
glUniformMatrix4fv(modelMatrixUniformLocation, 1, 0, normalMatrix.m);
}
GLuint lightDirectionUniformLocation = glGetUniformLocation(self.shaderProgram, "lightDirection");
glUniform3fv(lightDirectionUniformLocation, 1,self.lightDirection.v);
這里我們使用了GLKit的GLKMatrix4InvertAndTranspose
計算modelMatrix的逆轉置矩陣,然后傳遞給Shader。傳遞光照方向時使用glUniform3fv
來傳遞三維數組。
到此,基本的平行光光照模型就完成了。下面是效果圖。
下一篇是基礎篇的最后一篇,介紹紋理的加載和使用。后續會在進階篇中介紹高級光照,3D模型加載,粒子系統等更深入的知識。