这是我参与更文挑战的第7天,活动详情查看: 更文挑战
前言
可视化开发中,尤其是在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下来玩一玩。最后,文章写作不易,如果看完对你有帮助的话,你的点赞和关注是我持续更新的最大动力。 如果你也喜欢图形,喜欢可视化,你可以点个关注,后面我会持续分享高质量的文章, 勿忘初心!