咱们能看到物体,是由于光照射在物体上而后反射到咱们的眼睛当中。其中的影响因素很是多:观察者的位置、光源的位置、光的颜色、物体表面的颜色、材质和粗糙程度等等。之后咱们将会详细探究如何模拟物体的材质,在这篇文章中咱们只讨论光源。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,这样也能及时追踪博客的更新。