资讯专栏INFORMATION COLUMN

光照渲染——用canvas模拟光照效果

jokester / 1342人阅读

摘要:光照我们能看到物体,是因为光照射在物体上然后反射到我们的眼睛当中。这篇文章也是想通过这个简单的光照计算来引出,后面的文章我会用来重新实现这个效果。渲染的光照效果关于我的博客这篇文章到这里就结束了。

光照

我们能看到物体,是因为光照射在物体上然后反射到我们的眼睛当中。其中的影响因素非常多:观察者的位置、光源的位置、光的颜色、物体表面的颜色、材质和粗糙程度等等。以后我们将会详细探究如何模拟物体的材质,在这篇文章中我们只讨论光源。

平行光源

太阳的尺度相对地球来说非常大,所以可以认为从太阳照射来的光线都是平行的,即太阳是一个平行光源。

模拟平行光源的光照非常简单,当光垂直照射到平面上,即光线方向和平面呈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。希望通过这一系列文章能让读者对前端的各种图形绘制接口以及图像处理、图形学的基础知识有所了解。希望在分享的同时,也能巩固和复习自己所学知识,和大家共同进步。

系列博客地址:https://github.com/hujiulong/...

如果能帮助到你,欢迎star,这样也能及时追踪博客的更新。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/90764.html

相关文章

  • three.js 简介

    摘要:对于自身不能发光的物体,需要给场景添加光源从而达到可视的效果。中渲染阴影的开销比较大,所以默认物体是没有阴影的,需要单独开启。主要用于检测动画运行时的帧数,可以显示表示每秒多少帧和每帧多少毫秒,越大越好,但太大会影响性能,一般为左右。 一、WebGL 与 three.js WebGL(Web Graphics Library)是一种3D绘图协议,它允许把JavaScript和OpenG...

    yankeys 评论0 收藏0
  • 产品三维模型在线预览

    摘要:次时代传统的方式就是创建次时代模型,对应中的材质是高光网格材质对象,通常贴图文件包含颜色贴图法线贴图和高光贴图。 产品在线展示案例预览 玉镯在线预览:http://www.yanhuangxueyuan.co... 汽车在线预览:http://www.yanhuangxueyuan.co... Web3D技术历史 可通过插件或WebGL技术实现Web3D,在线网页上预览操作三维...

    DirtyMind 评论0 收藏0
  • 【Three.js】Three.js学习记录

    摘要:上帝觉得缺少了些生气,便用泥巴捏了一个小人儿,不叫亚当,她叫小芳。接下来预先恭喜你,你可以成为这网页世界的一个小上帝。使用可以向场景中发射光线。在下述案例中,从摄像机的位置向场景中鼠标的点击位置发射光线。 先得摆出几个关键词:场景、灯光、模型、材质、贴图与纹理、相机、渲染器。然后我开始装模作样地解释: 上帝说,要有场景!于是就有了场景,场景去纳这万事万物。 上帝说,要有光!于是就有了光...

    chanthuang 评论0 收藏0

发表评论

0条评论

jokester

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<