咱們能看到物體,是由於光照射在物體上而後反射到咱們的眼睛當中。其中的影響因素很是多:觀察者的位置、光源的位置、光的顏色、物體表面的顏色、材質和粗糙程度等等。之後咱們將會詳細探究如何模擬物體的材質,在這篇文章中咱們只討論光源。javascript
太陽的尺度相對地球來講很是大,因此能夠認爲從太陽照射來的光線都是平行的,即太陽是一個平行光源。html
模擬平行光源的光照很是簡單,當光垂直照射到平面上,即光線方向和平面呈90度角時,這時光照是最強的。若是照射的角度不斷變大(或者說光線和平面的夾角不斷變小),光照也會隨之變弱,當光線方向徹底和平面平行時,這時沒有光能照射到平面上,光強變成了0。前端
能夠總結出,平行光的光照狀況和兩個方向有關:光線的方向和受光照平面的朝向。java
咱們用一個垂直於平面的向量去描述平面的朝向,在圖形學中,通常把這個向量稱爲「法向量」。git
咱們能夠用向量的「點乘」運算來計算光強變化。github
點乘也叫數量積,是接受在實數R上的兩個向量並返回一個實數值標量的二元運算。點乘運算規則很是簡單,將兩個向量對應座標的乘積求和就好了。
這裏咱們計算的是三維向量,咱們用數組來表示向量,寫一個簡單的方法來計算點乘:canvas
/** * 點乘運算 * @param {Array<number>} v1 向量v1 * @param {Array<number>} v2 向量v2 * @return {number} 點乘結果 */ function dot( v1, v2 ) { return v1[ 0 ] * v2[ 0 ] + v1[ 1 ] * v2[ 1 ] + v1[ 2 ] * v2[ 2 ]; }
還有幾個重要的向量運算咱們也會用到,在這裏咱們提早定義好,爲減少篇幅,這裏省略掉具體實現,代碼能夠看最後的實例源碼。數組
/** * 將向量轉爲單位向量 * @param {Array<number>} v * @return {Array<number>} 單位向量 */ function normalize( v ) { /* ... */ } /** * 兩向量相減 * @param {Array<number>} v1 * @param {Array<number>} v2 * @return {Array<number>} */ function sub( v1, v2 ) { /* ... */ } /** * 計算一個向量的反方向向量 * @param {Array<number>} v * @return {Array<number>} */ function negate( v ) { /* ... */ }
咱們假設頁面的左上角爲原點O,右方向爲x軸正方向,下方向爲y軸正方向,垂直屏幕向外的方向爲z軸正方向。咱們能夠這樣定義一個寬高都爲500的平面:svg
var plane = { center: [ 250, 250, 0 ], // 平面中心點座標 width: 500, // 寬 height: 500, // 高 normal: [ 0, 0, 1 ], // 朝向,即法向量 color: { r: 255, g: 0, b: 0 } // 顏色爲紅色 }
對於平行光,只須要關心它的方向和顏色,咱們能夠這樣來定義一個平行光源:spa
var directionalLight = { direction: [ 0, 0, -1 ], // 從屏幕外垂直照向屏幕 color: { r: 255, g: 255, b: 255 } // 顏色爲純白色 }
平行光的光線都是平行的,因此它照射到平面上各個位置的效果都是同樣的,換言之,整個平面都應該是同一個顏色。
根據上面的規則(光強等於光線反方向向量點乘平面法向量),咱們能夠計算出這個顏色:
// ... var reverseLightDirection = negate( directionalLight.direction ); // 計算平行光的反方向向量 var intensity = dot( reverseLightDirection, plane.normal ); // 計算兩向量點乘 // 計算有光照時的顏色 var color = { r: intensity * plane.color.r + intensity * directionalLight.r, g: intensity * plane.color.g + intensity * directionalLight.g, b: intensity * plane.color.b + intensity * directionalLight.g, } var canvas = document.getElementById( 'canvas' ); var ctx = canvas.getElementById( '2d' ); ctx.rect( plane.center[ 0 ], plane.center[ 1 ], plane.width, plane.height ); ctx.fillStyle = 'rgb(' + color.r + ',' + color.g + ',' + color.b ')'; ctx.fill();
我寫了一個示例,能夠調整光線方向來觀察不一樣方向下的光照效果。
在線運行示例
在平常生活中,點光源更加常見,白熾燈、檯燈等均可以認爲是點光源。
首先,咱們先定義一個點光源,對於一個點光源來講,咱們只須要關心它的位置和顏色:
var pointLight = { position: [ 250, 250, 100 ], // 光源位於平面中心上方100處 color: { r: 255, g: 255, b: 255 } // 顏色爲純白色 }
光強的計算規則仍然不變:光強等於光線反方向向量點乘平面法向量。可是點光源的光是從一個點發射出來,它們照射到平面上時,全部光線的方向都不同。因此,咱們必須挨個計算平面上全部像素的光強。
這裏須要用到canvas提供的putImageData,這個方法能夠直接填入一個區域的像素顏色值來繪圖。代碼以下:
// ... var imageData = ctx.createImageData( 500, 500 ); // 建立一個ImageData,用來保存像素數據 for ( var x = 0; x < imageData.width; x++ ) { for ( var y = 0; y < imageData.height; y++ ) { var index = y * imageData.width + x; // 當前計算的像素點的索引 var point = [ x, y, 0 ]; var normal = [ 0, 0, 1 ]; var reverseLightDirection = normalize( sub( pointLight.position, point ) ); // 光線方向的反方向向量 var light = dot( reverseLightDirection, normal ); imageData.data[ index * 4 ] = pointLight.color.r * intensity + plane.color.r * intensity; imageData.data[ index * 4 + 1 ] = pointLight.color.g * intensity + plane.color.g * intensity; imageData.data[ index * 4 + 2 ] = pointLight.color.b * intensity + plane.color.b * intensity; imageData.data[ index * 4 + 3 ] = 255; } } ctx.putImageData( imageData, 100, 100 );
這樣就能夠看到結果了:
我寫了一個更復雜一點的例子,能夠經過鼠標去移動光源,滑動滾輪來改變光源高度:
在線運行示例
動態圖看起來有不少圈圈,實際上並無,能夠本身玩一下
對於一個500*500
的平面,咱們去計算它在點光源光照下的顏色,須要挨個計算平面上全部點,須要循環500*500=250000
次,這實際上是很是低效的。而且在作複雜場景的渲染時,不會只有一個光源,並且還會有投影等計算,計算量將會很是大。
從更底層的角度來講,這是由於每次計算都是由CPU完成的,而CPU只能串行計算,它只能完成一個計算之後才能開始下一次計算,因此很是緩慢。
這種複雜的渲染其實更適合用WebGL來作,由於每一次計算其實先後無關,WebGL能夠利用GPU的並行計算能力,同時去計算全部點的光照強度。一個500*500
的平面,理論上只須要花一次計算的時間,這個提高是很是大的。
這篇文章也是想經過這個簡單的光照計算來引出WebGL,後面的文章我會用WebGL來從新實現這個效果。
WebGL渲染的光照效果
這篇文章到這裏就結束了。
我計劃寫一系列關於前端圖形渲染的文章,將會涵蓋經常使用的前端圖形繪製技術:canvas、svg和WebGL。但願經過這一系列文章能讓讀者對前端的各類圖形繪製接口以及圖像處理、圖形學的基礎知識有所瞭解。但願在分享的同時,也能鞏固和複習本身所學知識,和你們共同進步。
系列博客地址: https://github.com/hujiulong/...
若是能幫助到你,歡迎star,這樣也能及時追蹤博客的更新。