canvas中模拟光照效果——绿的你发慌

简介: 前言可视化开发中,尤其是在2d视图下,看到一些非常的好玩的特效,五颜六色的光。好的本篇文章就带你去用canvas去模拟你自己想要的效果。涉及到一些数学知识,不过的都是基础的。我还是争取讲的更加通俗易懂一点。

前言


可视化开发中,尤其是在2d视图下,看到一些非常的好玩的特效,五颜六色的光。好的本篇文章就带你去用canvas去模拟你自己想要的效果。涉及到一些数学知识,不过的都是基础的。我还是争取讲的更加通俗易懂一点。


光照



我们能看到物体,是因为光照照射在物体上然后反射到我们的眼睛中,影响光照的因素非常多,位置,光的颜色,物体表面的颜色,材质和粗糙程度。本篇文章讨论一下光源, 光源又分为环境光, 点光源,平行光, 聚光灯。如下图显示:


640.png


平行光


平行光顾名思义光线平行,对于一个平面而言,平面不同区域接收到平行光的入射角一样。对于平行光而言,主要是确定光线的方向,光线方向设定好了,光线的与物体表面入射角就确定了,仅仅设置光线位置是不起作用的。


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


我们用一个垂直于平面的向量去描述平面的朝向,在图形学中,一般把这个向量称为“法向量”。法向量一般只有方向没有长度,下面有个normalize 就是单位长度的1的向量。


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


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


但是这个只是点乘的数学意义, 但是点乘更重要的是他的几何意义:


  1. 「用来判断两个向量是否在同一个方向」


  1. 「判断一个多边形是否正对摄像机」


  1. 「一个向量在另一个向量上的投影」


看图我给大家解释:



640 (1).png



因为点乘的结果是一个标量,所以决定大小的就是向量之间的夹角,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,
}


我写了例子去模拟下这个情况:

640.gif




代码例子在我的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 );
  }

效果图如下所示:

640 (1).gif


为了看起来更加炫酷, 我增加了move 和 wheel 事件, move 就是改变点光源的x, y 坐标。


document.addEventListener( 'mousemove', function( e ) {
      pointLight.position.x = e.clientX - 100
      pointLight.position.y = e.clientY - 100
      render()
  }, false )


效果如下:


640 (2).png


总结



本篇主要是简单的介绍了几种光照并在canvas 下的模拟实现, 主要是理解光强的计算方式:反向向量 和 平面的法向量 做点乘。本篇文章所有代码都在我的github上欢迎自己copy下来玩一玩。最后,文章写作不易,如果看完对你有帮助的话,你的点赞和关注是我持续更新的最大动力。如果你也喜欢图形,喜欢可视化,你可以点个关注,后面我会持续分享高质量的文章, 勿忘初心!

相关文章
|
存储 编解码 算法
凹凸贴图和法线贴图的区别
凹凸贴图适用于一些简单的凹凸效果,而法线贴图则更适合提供更高精度和真实感的凹凸纹理效果
289 1
|
1月前
|
人工智能 编解码 前端开发
深入了解 Three.js 中的材质与光照
Three.js 是一个强大的 JavaScript 库,用于在浏览器中创建和渲染 3D 场景。它的易用性和灵活性使得开发者能够轻松构建丰富的视觉体验。在 Three.js 中,材质与光照是影响物体外观和场景氛围的关键因素。本文将深入探讨 Three.js 中的材质类型、光源类型、光照模型,以及如何将它们结合以实现逼真的效果。
46 4
|
1月前
ThreeJs绘制圆柱体
这篇文章介绍了在Three.js中绘制圆柱体的方法,包括创建圆柱体几何体、设置材质以及将其正确放置在三维场景中的技巧。
47 0
ThreeJs绘制圆柱体
|
3月前
|
存储 编解码 算法
第5章-着色基础-5.4-锯齿和抗锯齿
第5章-着色基础-5.4-锯齿和抗锯齿
28 1
|
3月前
|
机器学习/深度学习
第5章-着色基础-5.2-光源
第5章-着色基础-5.2-光源
27 0
|
6月前
|
前端开发 计算机视觉 Python
OpenCV中绘制运动的小球动画,
要在OpenCV中绘制运动的小球动画,并且将动画显示在Python GUI窗口内,可以按照以下步骤进行:
107 2
|
前端开发
WebGL雾和物体旋转
WebGL雾和物体旋转
【Three.js入门】纹理及其常用属性、透明纹理、环境遮挡贴图与强度
【Three.js入门】纹理及其常用属性、透明纹理、环境遮挡贴图与强度
498 0
|
API
使用Threejs创建几何体并添加材质、光源、阴影、动画,添加坐标轴
使用Threejs创建几何体并添加材质、光源、阴影、动画,添加坐标轴
476 0
使用Threejs创建几何体并添加材质、光源、阴影、动画,添加坐标轴
110.绘制彩色抛物线
110.绘制彩色抛物线
103 0