學(xué)習(xí)WebGL之基本光照

本系列所有文章目錄

下面是本文例子的運(yùn)行截圖,可以前往我的博客查看代碼演示。

本文主要介紹如何使用Shader實(shí)現(xiàn)平行光的效果。什么是平行光呢?我們可以拿激光做比喻,平行光的方向不會隨著離光源的距離而改變。所以我們在模擬平行光的時(shí)候僅僅需要使用一個(gè)光照方向即可。
我們有了光照方向,接下來還需要一個(gè)重要數(shù)據(jù),平面的朝向。一個(gè)平面如果剛好面朝光線,那自然是最亮的。當(dāng)然還有些材質(zhì)的平面可以反射光線,反射光線的強(qiáng)度和你觀察的角度相關(guān),不過這些本文都不會介紹。后面會有專門一篇介紹復(fù)雜的光照模型。
我們用法線向量來表示平面朝向,在具體實(shí)現(xiàn)中,每個(gè)點(diǎn)都會有一個(gè)法線向量。所謂法線向量就是垂直于平面的一個(gè)三維向量,如下圖所示。


圖中展示了兩種法線向量的表示方法,左邊是每個(gè)多邊形的每個(gè)點(diǎn)有一個(gè)法線向量,右邊是每個(gè)點(diǎn)有一個(gè)法線向量,共享點(diǎn)的法線向量是這個(gè)點(diǎn)在所有平面上的法線向量之和。法線向量應(yīng)該總是被規(guī)范化成單位向量。本文的例子中使用的是左邊的方式。

如果你對向量相關(guān)的知識不是很了解,可以參考百度百科

有了法線向量和光照方向之后,只要將它們相乘即可得到光照強(qiáng)度。接下來開始分析代碼。

兩個(gè)單位向量相乘,結(jié)果是cos(向量夾角),夾角越大,cos(向量夾角)越小,剛好符合前面說的規(guī)律。

首先我們來看Vertex Shader。

attribute vec4 position;
attribute vec3 normal;

varying vec3 fragNormal;

uniform float elapsedTime;
uniform mat4 projectionMatrix;
uniform mat4 cameraMatrix;
uniform mat4 modelMatrix;
void main() {
    fragNormal = normal;
    gl_Position = projectionMatrix * cameraMatrix * modelMatrix * position;
}

我添加了法線向量屬性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進(jìn)行變換,需要使用modelMatrix的逆轉(zhuǎn)置矩陣,參考維基百科

因?yàn)楣饩€是照射到平面的方向,而法線是從平面往外的方向,所以他們相乘之前需要把光照方向反過來,并且要規(guī)范化。

vec3 normalizedLightDirection = normalize(-lightDirection);

接著我們將法線變換后再規(guī)范化,我們就得到了關(guān)鍵的兩個(gè)向量。下面是示意圖。


將它們相乘最后得到diffuse,可以稱它為漫反射強(qiáng)度。漫反射就是投射在粗糙表面上的光向各個(gè)方向反射的現(xiàn)象。我們求解diffuse就是模擬的漫反射現(xiàn)象。
代碼最后還有一個(gè)vec3 ambient = vec3(0.3);是什么呢?根據(jù)漫反射的公式,總會有強(qiáng)度為0的地方,為了使場景不那么暗,就增加了一個(gè)基本光照強(qiáng)度,也可稱為環(huán)境光強(qiáng)度。
環(huán)境光強(qiáng)度加上漫反射強(qiáng)度就是最后的光照強(qiáng)度finalLightStrength了。光照強(qiáng)度乘以材質(zhì)本身的顏色materialColor得到最終的顏色,這里材質(zhì)本身的顏色我用的是紅色。

看完Shader,我們回到JS代碼。首先,我們需要為法線向量屬性提供數(shù)據(jù)。將buffer改為如下形式,每個(gè)頂點(diǎn)數(shù)據(jù)擴(kuò)展了3個(gè)float作為法線向量數(shù)據(jù)。

function makeBuffer() {
  var triangle = [
      // Z軸上的平面
      -0.5,   0.5,    0.5,  0, 0, 1,
      -0.5,   -0.5,   0.5,  0, 0, 1,
      0.5,    -0.5,   0.5,  0, 0, 1,
      0.5,    -0.5,   0.5,  0, 0, 1,
      0.5,    0.5,    0.5,  0, 0, 1,
      -0.5,   0.5,    0.5,  0, 0, 1,
      -0.5,   0.5,    -0.5, 0, 0, -1,
      -0.5,   -0.5,   -0.5, 0, 0, -1,
      0.5,    -0.5,   -0.5, 0, 0, -1,
      0.5,    -0.5,   -0.5, 0, 0, -1,
      0.5,    0.5,    -0.5, 0, 0, -1,
      -0.5,   0.5,    -0.5, 0, 0, -1,
      // X軸上的平面
      0.5,    -0.5,   0.5,  1, 0, 0,
      0.5,    -0.5,   -0.5, 1, 0, 0,
      0.5,    0.5,    -0.5, 1, 0, 0,
      0.5,    0.5,    -0.5, 1, 0, 0,
      0.5,    0.5,    0.5,  1, 0, 0,
      0.5,    -0.5,   0.5,  1, 0, 0,
      -0.5,   -0.5,   0.5,  -1, 0, 0,
      -0.5,   -0.5,   -0.5, -1, 0, 0,
      -0.5,   0.5,    -0.5, -1, 0, 0,
      -0.5,   0.5,    -0.5, -1, 0, 0,
      -0.5,   0.5,    0.5,  -1, 0, 0,
      -0.5,   -0.5,   0.5,  -1, 0, 0,
      // Y軸上的平面
      -0.5,   0.5,    0.5,  0, 1, 0,
      -0.5,   0.5,    -0.5, 0, 1, 0, 
      0.5,    0.5,    -0.5, 0, 1, 0,
      0.5,    0.5,    -0.5, 0, 1, 0,
      0.5,    0.5,    0.5,  0, 1, 0,
      -0.5,   0.5,    0.5,  0, 1, 0,
      -0.5,   -0.5,   0.5,  0, -1, 0,
      -0.5,   -0.5,   -0.5, 0, -1, 0,
      0.5,    -0.5,   -0.5, 0, -1, 0,
      0.5,    -0.5,   -0.5, 0, -1, 0,
      0.5,    -0.5,   0.5,  0, -1, 0,
      -0.5,   -0.5,   0.5,  0, -1, 0,
  ];
  buffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(triangle), gl.STATIC_DRAW);
  return buffer;
}

以X軸上的兩個(gè)平面為例,X軸0.5處的平面法線方向是X軸正向,X軸-0.5處的平面法線方向是X軸反向。這樣我們才能讓朝外的面接收到光線。

接著在渲染時(shí)綁定法線向量數(shù)據(jù)到Shader中的屬性。

normalLoc = gl.getAttribLocation(program, 'normal');
gl.enableVertexAttribArray(normalLoc);
gl.vertexAttribPointer(normalLoc, 3, gl.FLOAT, false, 4 * 6, 4 * 3);

4 * 6代表一個(gè)頂點(diǎn)數(shù)據(jù)的字節(jié)數(shù),目前是6個(gè)float,4 * 3代表法線向量數(shù)據(jù)從每個(gè)頂點(diǎn)數(shù)據(jù)的4 * 3字節(jié)偏移量處開始。

下一步準(zhǔn)備一個(gè)三維向量存放光照的方向。

var lightDirection = null;

并給它賦值。讓它向下照射,所以向量為-Y軸(0,-1,0)。

lightDirection = vec3.fromValues(0, -1, 0);

最后給uniform光照方向和法線變換矩陣賦值。

// 設(shè)置光照方向
var lightDirectionUniformLocation = gl.getUniformLocation(program, "lightDirection");
gl.uniform3fv(lightDirectionUniformLocation, lightDirection);


var modelMatrixUniformLoc = gl.getUniformLocation(program, 'modelMatrix');
gl.uniformMatrix4fv(modelMatrixUniformLoc, false, modelMatrix);

var normalMatrix = mat4.create();
mat4.invert(normalMatrix, modelMatrix);
mat4.transpose(normalMatrix, normalMatrix);
var modelMatrixUniformLocation = gl.getUniformLocation(program, "normalMatrix");
gl.uniformMatrix4fv(modelMatrixUniformLocation, false, normalMatrix);

這里我們使用mat4.invertmat4.transpose計(jì)算modelMatrix的逆轉(zhuǎn)置矩陣,然后傳遞給Shader。傳遞光照方向時(shí)使用gl.uniform3fv來傳遞三維數(shù)組。

到此,基本的平行光光照模型就完成了。

下一篇是基礎(chǔ)篇的最后一篇,介紹紋理的加載和使用。后續(xù)會在進(jìn)階篇中介紹高級光照,3D模型加載等更深入的知識。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容