前言
可视化开发中,尤其是在2d视图下,看到一些非常的好玩的特效,五颜六色的光。好的本篇文章就带你去用canvas去模拟你自己想要的效果。涉及到一些数学知识,不过的都是基础的。我还是争取讲的更加通俗易懂一点。
光照
我们能看到物体,是因为光照照射在物体上然后反射到我们的眼睛中,影响光照的因素非常多,位置,光的颜色,物体表面的颜色,材质和粗糙程度。本篇文章讨论一下光源, 光源又分为环境光, 点光源,平行光, 聚光灯。如下图显示:
平行光
平行光顾名思义光线平行,对于一个平面而言,平面不同区域接收到平行光的入射角一样。对于平行光而言,主要是确定光线的方向,光线方向设定好了,光线的与物体表面入射角就确定了,仅仅设置光线位置是不起作用的。
模拟平行光源的光照非常简单,当光垂直照射到平面上,即光线方向和平面呈90度角时,这时光照是最强的。如果照射的角度不断变大(或者说光线和平面的夹角不断变小),光照也会随之变弱,当光线方向完全和平面平行时,这时没有光能照射到平面上,光强变成了0。
我们用一个垂直于平面的向量去描述平面的朝向,在图形学中,一般把这个向量称为“法向量”。法向量一般只有方向没有长度,下面有个normalize 就是单位长度的1的向量。
我们可以用向量的“点乘”运算来计算光强变化。
❝点乘也叫数量积,是接受在实数R上的两个向量并返回一个实数值标量的二元运算。点乘运算规则非常简单,将两个向量对应坐标的乘积求和就行了。
❞
但是这个只是点乘的数学意义, 但是点乘更重要的是他的几何意义:
- 「用来判断两个向量是否在同一个方向」
- 「判断一个多边形是否正对摄像机」
- 「一个向量在另一个向量上的投影」
看图我给大家解释:
因为点乘的结果是一个标量,所以决定大小的就是向量之间的夹角,cos的函数图像是0-90 是正的, 90-180 是负数嘛。所以点乘和光强的变化十分符合。这里我们计算的是三维向量,我们用数组来表示向量。然后实现一些方法。代码如下:
class Vector3 { constructor(x, y, z) { this.x = x || 0 this.y = y || 0 this.z = z || 0 } //点乘 dot(vec) { return this.x * vec.x + this.y * vec.y + this.z * vec.z } // 克隆 clone() { return new this.constructor(this.x, this.y, this.z) } //求长度 length() { return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z) } multiplyScalar(scalar) { this.x *= scalar this.y *= scalar this.z *= scalar return this } //向量相减 sub(v) { this.x -= v.x this.y -= v.y this.z -= v.z return this } // 单位化 normalize() { return this.multiplyScalar(1 / this.length()) } // 取反 negate() { this.x = -this.x this.y = -this.y this.z = -this.z return this } }
我们假设页面的左上角为原点O,右方向为x轴正方向,下方向为y轴正方向,垂直屏幕向外的方向为z轴正方向。我们可以这样定义一个宽高都为500的平面:
const plane = { center: new Vector3(250, 250, 0), // 平面中心点坐标 width: 500, // 宽 height: 500, // 高 normal: new Vector3(0, 0, 1), // 朝向,即法向量 color: { r: 255, g: 0, b: 0 }, // 颜色为红色 }
对于平行光,只需要关心它的方向和颜色,我们可以这样来定义一个平行光源:
const directionalLight = { direction: new Vector3(0, 0, -1), // 从屏幕外垂直照向屏幕 color: { r: 255, g: 255, b: 255 }, // 颜色为纯白色 }
平行光的光线都是平行的,所以它照射到平面上各个位置的效果都是一样的,换言之,整个平面都应该是同一个颜色。根据上面的规则(光强等于光线反方向向量「点乘」平面法向量),我们可以计算出这个颜色:
const reverseLightDirection = directionalLight.direction.clone().negate() // 计算平行光的反方向向量 const intensity = reverseLightDirection.dot(plane.normal) // 计算两向量点乘 // 计算有光照时的颜色 const 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, }
我写了例子去模拟下这个情况:
、
代码例子在我的github上欢迎fork
点光源
在日常生活中,点光源更加常见,白炽灯、台灯等都可以认为是点光源。
首先,我们先定义一个点光源,对于一个点光源来说,我们只需要关心它的位置和颜色:
const plane = { center: new Vector3(250,250,0), // 平面中心点坐标 width: 500, // 宽 height: 500, // 高 normal: new Vector3(0,0,1), // 朝向,即法向量 color: { r: 0, g: 255, b: 0 } // 颜色为绿色 } const pointLight = { position: new Vector3(250,250,60), color: { r: 255, g: 255, b: 255 } }
初始值设置之后, 这里其实要知道canvas 的「createImageData」 和
「putImageData」 这个方法可以直接填入一个区域的像素颜色值来绘图。光照的效果原理主要是改变图片的每一个像素值, 达到光照的效果;
光强的计算:光强等于光线反方向向量点乘平面法向量。「但是点光源的光是从一个点发射出来,它们照射到平面上时,所有光线的方向都不一样。所以,我们必须挨个计算平面上所有像素的光强。」
const imageData = ctx.createImageData( plane.width, plane.height ); function render() { for ( let x = 0; x < imageData.width; x++ ) { for ( let y = 0; y < imageData.height; y++ ) { let index = y * imageData.width + x; // 每一个像素点 let position = new Vector3(x,y,0); let normal = new Vector3(0,0,1); // 点光源与每个像素点 之间的方向就是 光线的方向 let currentToLight = pointLight.position.clone().sub(position).normalize(); let light = currentToLight.dot(normal); imageData.data[ index * 4 ] = Math.min( 255, ( pointLight.color.r + plane.color.r ) * light); imageData.data[ index * 4 + 1 ] = Math.min( 255, ( pointLight.color.g + plane.color.g ) * light ); imageData.data[ index * 4 + 2 ] = Math.min( 255, ( pointLight.color.b + plane.color.b ) * light ); imageData.data[ index * 4 + 3 ] = 255; } } ctx.putImageData( imageData, 100, 100 ); }
效果图如下所示:
为了看起来更加炫酷, 我增加了move 和 wheel 事件, move 就是改变点光源的x, y 坐标。
document.addEventListener( 'mousemove', function( e ) { pointLight.position.x = e.clientX - 100 pointLight.position.y = e.clientY - 100 render() }, false )
效果如下:
总结
本篇主要是简单的介绍了几种光照并在canvas 下的模拟实现, 主要是理解光强的计算方式:反向向量 和 平面的法向量 做点乘。本篇文章所有代码都在我的github上欢迎自己copy下来玩一玩。最后,文章写作不易,如果看完对你有帮助的话,你的点赞和关注是我持续更新的最大动力。如果你也喜欢图形,喜欢可视化,你可以点个关注,后面我会持续分享高质量的文章, 勿忘初心!