在canvas中模拟光照效果——光照下颜色的计算

光照

我们能看到物体,是因为光照射在物体上然后反射到我们的眼睛当中。其中的影响因素非常多:观察者的位置、光源的位置、光的颜色、物体表面的颜色、材质和粗糙程度等等。以后我们将会详细探究如何模拟物体的材质,在这篇文章中我们只讨论光源。


平行光源

太阳的尺度相对地球来说非常大,所以可以认为从太阳照射来的光线都是平行的,即太阳是一个平行光源。


模拟平行光源的光照非常简单,当光垂直照射到平面上,即光线方向和平面呈90度角时,这时光照是最强的。如果照射的角度不断变大(或者说光线和平面的夹角不断变小),光照也会随之变弱,当光线方向完全和平面平行时,这时没有光能照射到平面上,光强变成了0。


可以总结出,平行光的光照情况和两个方向有关:光线的方向和受光照平面的朝向。


我们用一个垂直于平面的向量去描述平面的朝向,在图形学中,一般把这个向量称为“法向量”。


我们可以用向量的“点乘”运算来计算光强变化。

点乘也叫数量积,是接受在实数R上的两个向量并返回一个实数值标量的二元运算。点乘运算规则非常简单,将两个向量对应坐标的乘积求和就行了。


这里我们计算的是三维向量,我们用数组来表示向量,写一个简单的方法来计算点乘:

/**
 * 点乘运算
 * @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的平面:

var plane = {
    center: [ 250, 250, 0 ],    // 平面中心点坐标
    width: 500,                 //
    height: 500,                //
    normal: [ 0, 0, 1 ],        // 朝向,即法向量     
    color: { r: 255, g: 0, b: 0 }   // 颜色为红色
}


对于平行光,只需要关心它的方向和颜色,我们可以这样来定义一个平行光源:

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();


我写了一个示例,可以调整光线方向来观察不同方向下的光照效果。

在线运行示例
4 点光源

在日常生活中,点光源更加常见,白炽灯、台灯等都可以认为是点光源。


首先,我们先定义一个点光源,对于一个点光源来说,我们只需要关心它的位置和颜色:

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 );


这样就可以看到结果了:

tim 20180107034502


我写了一个更复杂一点的例子,可以通过鼠标去移动光源,滑动滚轮来改变光源高度:

在线运行示例

5

动态图看起来有很多圈圈,实际上并没有,可以自己玩一下


WebGL的优势

对于一个500*500的平面,我们去计算它在点光源光照下的颜色,需要挨个计算平面上所有点,需要循环500*500=250000次,这其实是非常低效的。并且在做复杂场景的渲染时,不会只有一个光源,而且还会有投影等计算,计算量将会非常大。


从更底层的角度来说,这是因为每次计算都是由CPU完成的,而CPU只能串行计算,它只能完成一个计算以后才能开始下一次计算,所以非常缓慢。


这种复杂的渲染其实更适合用WebGL来做,因为每一次计算其实前后无关,WebGL可以利用GPU的并行计算能力,同时去计算所有点的光照强度。一个500*500的平面,理论上只需要花一次计算的时间,这个提升是非常大的。


这篇文章也是想通过这个简单的光照计算来引出WebGL。

tim 20180107040503
WebGL渲染的光照效果


小白   逍遥游 正在技术中挣扎挣扎挣扎

逍遥游 正在技术中挣扎挣扎挣扎
     扫一扫立刻加入iGeekBar会员QQ群(545980198)
    和更多iG客会员交流分享吧~