Spiga

用JavaScript玩转计算机图形学(二)基本光源

2010-04-02 20:44 by Milo Yip, 11155 visits, 收藏, 编辑

上一篇介绍了简单的光线追踪,凑合了临时用的光源去渲染效果。这次将讲解三种基本光源,及一些背景理论。过分简化的教材和现成API(OpenGL/Direct3D等)可能会做成一些错误理解。在此,希望文章能简单之余,又不失背后理论。读者明白之后,可把概念简化,或按实际情况调整。

本文代码可在此下载(10KiB)。

读者若喜欢本文,可按推荐按钮以示鼓励。如果写得不够清楚,或有错误之处,可留言相告。

在物理上,光(light)可以视为电磁波(electromagnetic wave)或光子(photon)。在计算机图形学的领域里,通常只会用到光的部份物理性质,例如假设光是直线前进(不受因引力影响),忽略光的速度,通常不考虑衍射(diffraction)、干涉(interference )等等(好吧,也不考虑量子行为☺)。因为,计算机图形学不是物理学,最终目标(笔者认为)只是要渲染视觉上美的事物,只要模拟到某个合适层次的模型,有时候还为了美观而采用非物理/非真实的方式。

方向光源

光源(light source)放射(emit)光,而非散射(scatter)或吸收(absorb)光。

最简单的光源模型,是方向光源(directional light),又称平行光源。这种光源假设光在无限远放射,在任何位置,放射方向都是一致的,可以模拟类似太阳的光线(虽然实际上太阳并非无限远)。

方向光源的方向,通常用光向量(light vector)\mathbf{l}去表示。为方便计算,通常\mathbf{l}是单位向量,并且和光的放射方向相反

方向光源的另一个属性,是指定其照明的量。量度光的科学叫幅射度量学(radiometry),本文暂且略过其细节。这里只用到光的其中一个量度方式,就是每秒通过每单位面积平面的光子总能量,称为幅照度(irradiance)。

光的颜色,是由不同频率的光波及其频谱,在人类视觉上形成的。详细内容又涉及光度测定(photometry)、比色法(colorimetry)、视觉感知(visual perception)、甚至哲学等,有机会再谈。这里只使用常见的红绿蓝三个颜色通道(color channel)。光源的幅照度也可以用这三通道来描述,因此,仍可用前文的Color类来描述幅照度。但注意,光的幅照度范围是零到无限大,并不是[0,1]或[0,255]。光的"颜色"和材质的"颜色"并非同一个概念,关于这点,读者可思考以下一个简单命题

客观上,有接近白色的纸,但没有白色的光

关于这个命题,和材质的"颜色",将于下回分解。

阴影

一个光源的阴影(shadow),是因不透明障碍物,以致其不能到达的地方。我们可使用已有的几何相交功能,去检测某一位置,在\mathbf{l}方向上有否障碍物。光源追踪方法在阴影处理上很简单,光删化方法就复杂得多。

实现DirectionalLight类

在编程时,需要为不同种类的光源设计一个共通接口。渲染器要从光源取得,在某个空间位置,其光向量和幅照度。在此,定义光源有一成员函数sample(scene, position),并传回一个LightSample对象:

LightSample = function(L, EL) { this.L = L; this.EL = EL; };
LightSample.zero = new LightSample(Vector3.zero, Color.black);

以下是方向光源的代码,预设使用阴影:

DirectionalLight = function(irradiance, direction) { this.irradiance = irradiance; this.direction = direction; this.shadow = true; };

DirectionalLight.prototype = {
    initialize: function() { this.L = this.direction.normalize().negate(); },

    sample: function(scene, position) {
        // 阴影测试
        if (this.shadow) {
            var shadowRay = new Ray3(position, this.L);
            var shadowResult = scene.intersect(shadowRay);
            if (shadowResult.geometry)
                return LightSample.zero;
        }

        return new LightSample(this.L, this.irradiance);
    }
};

渲染幅照度

sample()函数可以传回相对光向量的幅照度,但物体表面并不一定垂直于光向量。光源越接近平面,每面积接受的能量就越少。可以想像太阳在中午是最亮的,日出日落时是最暗的。如下图所示,平面法向量方向的面积,是光向量方向的面积的1/\cos\theta_i"倍,而幅照度则为其倒数,即\cos\theta_i"倍。

因此,设光源的光向量方向幅照度为E_L,平面接收到的幅照度为

\begin{align*} E&=E_L\max(\cos\theta_i, 0) \\ &=E_L\max(\mathbf{n }\cdot\mathbf{l}, 0) \end{align*}

幅照度是能量,可以累加,所以多个光源下,平面接收到的总幅照度为

\begin{align*} E&=\sum_{k=1}^{n}E_{L_k}\max(\cos\theta_{ i_k} 0) \\ &=\sum_{k=1}^{n}E_{L_k}\max(\mathbf{n}\cdot\mathbf{l}_k, 0) \end{align*}

以下的简单代码,测试一个方向光源在场境中的总幅照度:

function renderLight(canvas, scene, lights, camera) {
    // 从canvas取得imgdata和pixels,跟之前的代码一样
    // ...

    scene.initialize();
    for (var k in lights)
        lights[k].initialize();
    camera.initialize();

    var i = 0;
    for (var y = 0; y < h; y++) {
        var sy = 1 - y / h;
        for (var x = 0; x < w; x++) {
            var sx = x / w;
            var ray = camera.generateRay(sx, sy);
            var result = scene.intersect(ray);
            if (result.geometry) {
                var color = Color.black;
                for (var k in lights) {
                    var lightSample = lights[k].sample(scene, result.position);

                    if (lightSample != lightSample.zero) {
                        var NdotL = result.normal.dot(lightSample.L);

                        // 夹角小约90度,即光源在平面的前面
                        if (NdotL >= 0)
                            color = color.add(lightSample.EL.multiply(NdotL));
                    }
                }
                pixels[i] = color.r * 255;
                pixels[i + 1] = color.g * 255;
                pixels[i + 2] = color.b * 255;
                pixels[i + 3] = 255;
            }
            i += 4;
        }
    }

    ctx.putImageData(imgdata, 0, 0);
}

 

修改代码试试看

  • 改變光源的顏色 (也試試超過1的值)
    改變光源的方向 (在DirectionalLight.initialize()裡自動做了normalize,這輸入不需位單位向量)
    改變光源的幅照度 (也試試超過1的值)
  • 改變光源的方向 (在DirectionalLight.initialize()裡自動做了normalize,這輸入不需位單位向量)

点光源

点光源/点光灯(point light),又称全向光源/泛光源/泛光灯(omnidirectional light/omni light),是指一个无限小的点,向所有光向平均地散射光。

其光向量,就是表面位置往点光源位置的方向:

学习物理时,经常有这种往所有方向发射的情况(例如引力、声音等)。类比可知,接收到的能量和距离的关系,是成平方反比定律的:

E_L=\frac{I_L}{r^2}

当中I为幅射强度(intensity, radiant intensity),当r=1时,幅射强度和幅照度相等。

1/r^2通常称为衰减(attenuation)系数。有时候会为各种需求,写一些非物理正确的衰减系数。

实现PointLight类

以下代码中,不直接使用normalize(),令r和其平方可以在之后分别使用,算是简单的优化。

PointLight = function(intensity, position) { this.intensity = intensity; this.position = position; this.shadow = true; };

PointLight.prototype = {
    initialize: function() { },
    sample: function(scene, position) {
        // 计算L,但保留r和r^2,供之后使用
        var delta = this.position.subtract(position);
        var rr = delta.sqrLength();
        var r = Math.sqrt(rr);
        var L = delta.divide(r);

        // 阴影测试
        if (this.shadow) {
            var shadowRay = new Ray3(position, L);
            var shadowResult = scene.intersect(shadowRay);
            // 在r以内的相交点才会遮蔽光源
            if (shadowResult.geometry && shadowResult.distance <= r)
                return LightSample.zero;
        }

        // 平方反比衰减
        var attenuation = 1 / rr;

        // 计算幅照度
        return new LightSample(L, this.intensity.multiply(attenuation));
    }
};

 

修改代码试试看

  • 改变幅射强度
  • 移动光源
  • 加入多一个点光源

聚光灯

现实中,并不存在理想的点光源,放射的光在不同方向是有差异的。聚光灯(spot light)是常用的一种模式,它在点光源的基础上,加入圆锥形的范围。聚光灯可以有不同的模型,以下采用Direct3D固定功能管道(fixed-function pipeline)用的模型做示范。

聚光灯有一个主要方向s,再设置两个圆锥范围,称为内圆锥和外圆锥,两圆锥之间的范围称为半影(penumbra)。内外圆锥的内角分别为\theta\phi。聚光灯可计算一个聚光灯系数,范围为[0,1],代表某方向的放射比率。内圆锥中系数为1(最亮),内圆锥和外圆锥之间系数由1逐渐变成0。另外,可用另一参数p代表衰减(falloff),决定内圆锥和外圆锥之间系数变化。方程式如下:

spot(\alpha)= \begin{cases} 1, & \text{where} \cos\alpha \geq \cos \frac{\theta}{2} \\ \left ( \dfrac{\cos\alpha - \cos{\frac{\phi}{2}}}{\cos\frac{\theta}{2}-\cos\frac{\phi}{2} } \right )^p, & \text{where} \cos\frac{\phi}{2} < \cos\alpha < \cos\frac{\theta}{2} \\ 0, & \text{where} \cos\alpha \leq \cos\frac{\phi}{2} \end{cases}

实现SpotLight类

SpotLight类只是多了那几个参数,以计算聚光灯系数,最后结合到幅照度。很多参数可在initialize()里预计算,减少在sample()里重复运算。

SpotLight = function(intensity, position, direction, theta, phi, falloff) {
    this.intensity = intensity;
    this.position = position;
    this.direction = direction;
    this.theta = theta;
    this.phi = phi;
    this.falloff = falloff;
    this.shadow = true;
};

SpotLight.prototype = {
    initialize: function() {
        this.S = this.direction.normalize().negate();
        this.cosTheta = Math.cos(this.theta * Math.PI / 180 / 2);
        this.cosPhi = Math.cos(this.phi * Math.PI / 180 / 2);
        this.baseMultiplier = 1 / (this.cosTheta - this.cosPhi);
    },

    sample: function(scene, position) {
        // 计算L,但保留r和r^2,供之后使用
        var delta = this.position.subtract(position);
        var rr = delta.sqrLength();
        var r = Math.sqrt(rr);
        var L = delta.divide(r);

        // 计算聚光灯因子
        var spot;
        var SdotL = this.S.dot(L);
        if (SdotL >= this.cosTheta)
            spot = 1;
        else if (SdotL <= this.cosPhi)
            spot = 0;
        else
            spot = Math.pow((SdotL - this.cosPhi) * this.baseMultiplier, this.falloff);

        // 阴影测试
        if (this.shadow) {
            var shadowRay = new Ray3(position, L);
            var shadowResult = scene.intersect(shadowRay);
            // 在r以内的相交点才会遮蔽光源
            if (shadowResult.geometry && shadowResult.distance <= r)
                return LightSample.zero;
        }

        // 平方反比衰减
        var attenuation = 1 / rr;

        // 计算幅照度
        return new LightSample(L, this.intensity.multiply(attenuation * spot));
    }
};

 

修改代码试试看

  • 改变各个参数

例子

三原色

这个例子把三原色聚光灯重叠射度地板,可以看到它们的颜色混合。

 

修改代码试试看

  • 如果,幅射强度是负值的话,会怎么样?(虽然未证实反光子(antiphoton)的存在,但读者能想到图形学上的功能么?)

很多光源

这个例子在天花加了36个点光源,和一个从后往前的填充用方向光源。有时候灯光师会加入填充光源(fill light),去加强对象的轮廓及立体感(有时候用上冷暖色的对比)。这个渲染比较慢,可能要半分钟啊!

 

修改代码试试看

  • 把光源放在不同位置(例如接近地面)
  • 把每个光源的颜色加入差异

结语

本文简单介绍了三种基本的光源,这些光源除了应用在光线追踪渲染器上,也常用在光栅化渲染器中。

除这三种以外,还有一类比较高阶的光源──面光源(area light)。面光源比这三种光源更真实,也能完美地做到真实的柔和阴影。如果能实现面光源,基本上也不用特定做「光源」这种类,取而代之,可以设定某些材质本身能发光即可。当然,没有免费午餐,随之而来的时间复杂度也增加。

有了光源,下一篇大概会开始谈材质,讲述光源和材质间的互动。

参考

  • Tomas Möller, Eric Haines, Naty Hoffman, Real-time Rendering 3rd Edition, AK Peters 2008
  • Matt Pharr, Greg Humphreys, Physically Based Rendering, Morgan Kaufmann, 2004
Add your comment

42 条回复

  1. #1楼 麒麟      2010-04-02 20:44
    sf
     回复 引用 查看   
  2. #2楼 FrogTan      2010-04-02 20:46
    牛叉,推荐按钮不见了、、、
    推荐完毕
     回复 引用 查看   
  3. #3楼 悟空空      2010-04-02 20:46
    占个位子学习学习哈哈
     回复 引用 查看   
  4. #4楼 浪雪      2010-04-02 21:17
    占位学习~~~~
     回复 引用 查看   
  5. #5楼 Blink182      2010-04-02 21:18
    强了!
     回复 引用 查看   
  6. #6楼 麦舒      2010-04-02 21:23
    很不错的文章,不过现在已经没有耐心去研究这些了。
     回复 引用 查看   
  7. #7楼 Sunny Peng      2010-04-02 21:24
    非常不错。
     回复 引用 查看   
  8. #8楼 Justin      2010-04-02 21:36
    强!好好向楼主学习
    特别喜欢楼主对推荐框的改进
    我参考这个效果去改进一下我的快捷回复功能
    先谢了!!!
     回复 引用 查看   
  9. #9楼 Sangplus      2010-04-02 21:39
    最近地铁上一直用手机看这个帖子。思考用c#也做一个。
     回复 引用 查看   
  10. #10楼 EricZhang(T2噬菌体)      2010-04-02 22:11
    我没有研究过图形学和VR方面的东西,不过楼主的文章很通俗易懂,让我学到不少知识,谢谢!
     回复 引用 查看   
  11. #11楼 andi.cao      2010-04-02 23:22
    学习。深入浅出,再加上动手实践。简直无敌了。
     回复 引用 查看   
  12. #12楼 Martin Qin      2010-04-03 11:45
    非常感兴趣.
     回复 引用 查看   
  13. #13楼 Coki      2010-04-03 16:01
    非常赞赏LZ使用JS来当做例子,这样就不能直接调用各种各样的框架,使用纯算法解释这些问题,让人真正理解。
     回复 引用 查看   
  14. #14楼 pease      2010-04-03 17:29
    真是受益匪浅,希望您坚持把这个系列写完!
     回复 引用 查看   
  15. #15楼 Leepy      2010-04-03 17:53
    真牛!
     回复 引用 查看   
  16. #16楼 Jertun      2010-04-04 09:37
    膜拜
     回复 引用 查看   
  17. #17楼 Mica.Yankee      2010-04-04 10:35
    博主写的文章 格式很漂亮啊 赞一个
     回复 引用 查看   
  18. #18楼 装配脑袋      2010-04-04 22:13
    55,争取这个假期把LZ第一篇文章的算法翻译成DirectCompute GPU并行算法,不知道什么时候才能研究这一篇
     回复 引用 查看   
  19. #19楼[楼主] Milo Yip      2010-04-05 06:31
    @FrogTan
    甚麼瀏覽器?
     回复 引用 查看   
  20. #20楼[楼主] Milo Yip      2010-04-05 06:33
    @Justin
    引用Justin:
    强!好好向楼主学习
    特别喜欢楼主对推荐框的改进
    我参考这个效果去改进一下我的快捷回复功能
    先谢了!!!

    實際上,我是看了你的文章才去改的。因為覺得太長的文章好像很難找到推薦按鈕。
     回复 引用 查看   
  21. #21楼[楼主] Milo Yip      2010-04-05 06:35
    @pease
    引用pease:真是受益匪浅,希望您坚持把这个系列写完!

    謝謝,我會盡力,不過如果能定期出文,或許寫不完更好 :)
     回复 引用 查看   
  22. #22楼 Justin      2010-04-05 11:15
    @Milo Yip
    哎!我那个博客模板的文档类型还是
    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
    ,使用这种方式设置div的位置还有问题,在IE下显示不正常
    我还在跟博客园反应,希望他们能把文档类型改成
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
     回复 引用 查看   
  23. #23楼 徐培华      2010-04-05 15:24
    博主,您好,我想知道你写这篇文章涉及到哪些知识。
    您可以说下么?
    比如哪方面的数学。javascript.还有其他等等的一些知识。
    能否推荐些书籍学习下。
    Thank you
    最主要想问的就是这图里面涉及到的是什么数学?
    [img]http://latex.codecogs.com/gif.latex?\begin{align*}%20E&=\sum_{k=1}^{n}E_{L_k}\max(\cos\theta_{%20i_k}%200)%20\\%20&=\sum_{k=1}^{n}E_{L_k}\max(\mathbf{n}\cdot\mathbf{l}_k,%200)%20\end{align*}[/img]
     回复 引用 查看   
  24. #24楼 徐培华      2010-04-05 15:24
    就是这个链接,打复制打开看下。
    这里贴上去没反应,
    http://latex.codecogs.com/gif.latex?\begin{align*}%20E&=\sum_{k=1}^{n}E_{L_k}\max(\cos\theta_{%20i_k}%200)%20\\%20&=\sum_{k=1}^{n}E_{L_k}\max(\mathbf{n}\cdot\mathbf{l}_k,%200)%20\end{align*}
     回复 引用 查看   
  25. #25楼 阿斯兰      2010-04-05 19:26
    弓虽!
    好文章,期盼下集。。。
     回复 引用 查看   
  26. #26楼 lib      2010-04-05 19:48
    好,近来学习下
     回复 引用 查看   
  27. #27楼 infinte      2010-04-06 01:30
    有种Pixel Shader的感觉。
     回复 引用 查看   
  28. #28楼 天天不在      2010-04-06 08:14
    膜拜下.
     回复 引用 查看   
  29. #29楼 andi.cao      2010-04-06 10:35
    @徐培华
    你可以Google 一下 latex。
    MiloYip老师是用了latex书写公式的。
    使用了一个在线公式编辑器,见:http://www.codecogs.com/components/equationeditor/equationeditor.php


     回复 引用 查看   
  30. #30楼 王克伟      2010-04-06 11:19
    我非常喜欢,回去好好研究下
     回复 引用 查看   
  31. #31楼 徐培华      2010-04-06 11:55
    引用andi.cao:
    @徐培华
    你可以Google 一下 latex。
    MiloYip老师是用了latex书写公式的。
    使用了一个在线公式编辑器,见:http://www.codecogs.com/components/equationeditor/equationeditor.php



    谢谢你的回答。
    其实我想问的不是怎么写出这样的公式
    问的是这公式是什么意思
    在数学里面属于哪些知识点,
     回复 引用 查看   
  32. #32楼[楼主] Milo Yip      2010-04-06 12:43
    @徐培华
    引用徐培华:
    博主,您好,我想知道你写这篇文章涉及到哪些知识。
    您可以说下么?
    比如哪方面的数学。javascript.还有其他等等的一些知识。
    能否推荐些书籍学习下。
    Thank you
    最主要想问的就是这图里面涉及到的是什么数学?

    你好,目前這兩篇僅需要一些基礎的綫性代數(linear algebra)和解析幾何(analytic geometry),例如坐標系統、向量、點積等。

    JavaScript方面,我只看了Mozilla的https://developer.mozilla.org/en/JavaScript。因為JS語法及庫都非常精簡,學習容易。JS和常見的C++系語言(Java/C#等)的主要差別在於基於物件(object-based)而非面向對象(object-oriented)。

    對於你所指的算式,可能我解釋得不夠詳細。點積(dot product)和餘弦的關係經常在圖形學上使用。對於兩個任意向量A、B, A dot B = ||A|| ||B|| cos theta,theta為兩向量的夾角。如果A和B均為單位向量,即||A||=||B||=1,那麼 A dot B = cos theta。另外,當A和B的夾角超過90度,A dot B為負數,在本文的這應用中,不會受光的照射(也不會形成負能量),因此加入max(A dot B, 0)的方式。

    對於多個光源,由於能量可以疊加,因此可以用summation把各個光源貢獻於法綫方向的幅照度算出來。

    如果有不清楚的地方,可繼續提出意見,我盡量作出改善。謝謝。
     回复 引用 查看   
  33. #33楼[楼主] Milo Yip      2010-04-06 12:46
    @infinte
    引用infinte:有种Pixel Shader的感觉。

    是的,本文大部份算式都可以直接用GPU的pixel shader實現,用在光柵化的渲染上。唯一例外是陰影部份,PS需要用shadow map等技術,精確度有所限制,亦有很多不同的問題及解決方法。
     回复 引用 查看   
  34. #34楼 yen1985      2010-04-12 15:32
    认真看完第二篇,期待第三篇
     回复 引用 查看   
  35. #35楼 harrywy      2010-04-27 11:41
    问个问题
    如果要光线跟踪再加上阴影,是不是renderLight和rayTraceRecursive的结果叠加就好了?
     回复 引用 查看   
  36. #36楼[楼主] Milo Yip      2010-04-27 11:48
    @harrywy
    引用harrywy:
    问个问题
    如果要光线跟踪再加上阴影,是不是renderLight和rayTraceRecursive的结果叠加就好了?

    這個問題比較難答。在一般情況下不可以,例如一個鏡面球,其反射的影象也會有光影。

    但現時,實時渲染會使用一個叫Pre-Light Pass的技術,是把光源分開渲染,最後再用來計算著色。但這種方式會有一些限制。
     回复 引用 查看   
  37. #37楼 harrywy      2010-04-27 12:01
    引用Milo Yip:
    @harrywy
    引用harrywy:
    问个问题
    如果要光线跟踪再加上阴影,是不是renderLight和rayTraceRecursive的结果叠加就好了?

    這個問題比較難答。在一般情況下不可以,例如一個鏡面球,其反射的影象也會有光影。

    但現時,實時渲染會使用一個叫Pre-Light Pass的技術,是把光源分開渲染,最後再用來計算著色。但這種方式會有一些限制。


    哦,理解了,那这就不好办了我们老师要求我们光线跟踪部分要实现阴影,那我先去找找Pre-Light Pass技术应该怎么实现吧~ 多谢了
     回复 引用 查看   
  38. #38楼 JimLiu      2010-05-28 14:07
    Milo老师好久没发新的JavaScript Computer Graphics系列了,期待啊~~
     回复 引用 查看   
  39. #39楼 +-+      2010-05-28 16:52
    @Milo Yip
    当A*B=0时候,就是两个向量垂直。书上得来终觉浅啊,这个是平面向量的知识,书上学到后,几乎不会联想的编程,等到去编程了其实用到的都是这些知识。
     回复 引用 查看   
  40. #40楼 唐老鸭      2010-06-01 11:54
    再死一次!!!!!
     回复 引用 查看   
  41. #41楼 Kevan      2010-06-18 18:27
    @唐老鸭
    我也去死了。虽说学无止境,但是悬梁刺股我也学不了,估计!
     回复 引用 查看   
  42. #42楼 【当耐特砖家】      2011-11-08 21:36
    已选入HTML5实验室目录 , 多谢分享!http://www.cnblogs.com/iamzhanglei/archive/2011/11/06/2237870.html
     回复 引用 查看