光照
我们能看到物体,是因为光照射在物体上然后反射到我们的眼睛当中。其中的影响因素非常多:观察者的位置、光源的位置、光的颜色、物体表面的颜色、材质和粗糙程度等等。以后我们将会详细探究如何模拟物体的材质,在这篇文章中我们只讨论光源。
平行光源
太阳的尺度相对地球来说非常大,所以可以认为从太阳照射来的光线都是平行的,即太阳是一个平行光源。
模拟平行光源的光照非常简单,当光垂直照射到平面上,即光线方向和平面呈90度角时,这时光照是最强的。如果照射的角度不断变大(或者说光线和平面的夹角不断变小),光照也会随之变弱,当光线方向完全和平面平行时,这时没有光能照射到平面上,光强变成了0。
可以总结出,平行光的光照情况和两个方向有关:光线的方向和受光照平面的朝向。
我们用一个垂直于平面的向量去描述平面的朝向,在图形学中,一般把这个向量称为“法向量”。
我们可以用向量的“点乘”运算来计算光强变化。
点乘也叫数量积,是接受在实数R上的两个向量并返回一个实数值标量的二元运算。点乘运算规则非常简单,将两个向量对应坐标的乘积求和就行了。
这里我们计算的是三维向量,我们用数组来表示向量,写一个简单的方法来计算点乘:
/**
* 点乘运算
* @param {Array} v1 向量v1
* @param {Array} v2 向量v2
* @return {number} 点乘结果
*/
function dot( v1, v2 ) {
return v1[ 0 ] * v2[ 0 ] + v1[ 1 ] * v2[ 1 ] + v1[ 2 ] * v2[ 2 ];
}
还有几个重要的向量运算我们也会用到,在这里我们提前定义好,为减小篇幅,这里省略掉具体实现,代码可以看最后的实例源码。
/**
* 将向量转为单位向量
* @param {Array} v
* @return {Array} 单位向量
*/
function normalize( v ) { /* ... */ }
/**
* 两向量相减
* @param {Array} v1
* @param {Array} v2
* @return {Array}
*/
function sub( v1, v2 ) { /* ... */ }
/**
* 计算一个向量的反方向向量
* @param {Array} v
* @return {Array}
*/
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();
我写了一个示例,可以调整光线方向来观察不同方向下的光照效果。
在线运行示例
点光源
在日常生活中,点光源更加常见,白炽灯、台灯等都可以认为是点光源。
首先,我们先定义一个点光源,对于一个点光源来说,我们只需要关心它的位置和颜色:
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 );
这样就可以看到结果了:
我写了一个更复杂一点的例子,可以通过鼠标去移动光源,滑动滚轮来改变光源高度:
在线运行示例
动态图看起来有很多圈圈,实际上并没有,可以自己玩一下
WebGL的优势
对于一个500*500的平面,我们去计算它在点光源光照下的颜色,需要挨个计算平面上所有点,需要循环500*500=250000次,这其实是非常低效的。并且在做复杂场景的渲染时,不会只有一个光源,而且还会有投影等计算,计算量将会非常大。
从更底层的角度来说,这是因为每次计算都是由CPU完成的,而CPU只能串行计算,它只能完成一个计算以后才能开始下一次计算,所以非常缓慢。
这种复杂的渲染其实更适合用WebGL来做,因为每一次计算其实前后无关,WebGL可以利用GPU的并行计算能力,同时去计算所有点的光照强度。一个500*500的平面,理论上只需要花一次计算的时间,这个提升是非常大的。
这篇文章也是想通过这个简单的光照计算来引出WebGL,后面的文章我会用WebGL来重新实现这个效果。
WebGL渲染的光照效果
关于我的博客
这篇文章到这里就结束了。
我计划写一系列关于前端图形渲染的文章,将会涵盖常用的前端图形绘制技术:canvas、svg和WebGL。希望通过这一系列文章能让读者对前端的各种图形绘制接口以及图像处理、图形学的基础知识有所了解。希望在分享的同时,也能巩固和复习自己所学知识,和大家共同进步。
如果能帮助到你,欢迎star,这样也能及时追踪博客的更新。