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


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

  光

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

  方向光源

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

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

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

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

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

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

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

  阴影

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

  实现DirectionalLight类

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

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

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

01 DirectionalLight = function(irradiance, direction) { this.irradiance = irradiance; this.direction = direction; this.shadow = true; }; 
02   
03 DirectionalLight.prototype = { 
04     initialize: function() { this.L = this.direction.normalize().negate(); }, 
05   
06     sample: function(scene, position) { 
07         // 阴影测试 
08         if (this.shadow) { 
09             var shadowRay = new Ray3(position, this.L); 
10             var shadowResult = scene.intersect(shadowRay); 
11             if (shadowResult.geometry) 
12                 return LightSample.zero; 
13         } 
14   
15         return new LightSample(this.L, this.irradiance); 
16     } 
17 }; 

  渲染幅照度

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

  查看原图(大图)

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

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

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

01 function renderLight(canvas, scene, lights, camera) { 
02     // 从canvas取得imgdata和pixels,跟之前的代码一样 
03     // ... 
04   
05     scene.initialize(); 
06     for (var k in lights) 
07         lights[k].initialize(); 
08     camera.initialize(); 
09   
10     var i = 0; 
11     for (var y = 0; y < h; y++) { 
12         var sy = 1 - y / h; 
13         for (var x = 0; x < w; x++) { 
14             var sx = x / w; 
15             var ray = camera.generateRay(sx, sy); 
16             var result = scene.intersect(ray); 
17             if (result.geometry) { 
18                 var color = Color.black; 
19                 for (var k in lights) { 
20                     var lightSample = lights[k].sample(scene, result.position); 
21   
22                     if (lightSample != lightSample.zero) { 
23                         var NdotL = result.normal.dot(lightSample.L); 
24   
25                         // 夹角小约90度,即光源在平面的前面 
26                         if (NdotL >= 0) 
27                             color = color.add(lightSample.EL.multiply(NdotL)); 
28                     } 
29                 } 
30                 pixels[i] = color.r * 255; 
31                 pixels[i + 1] = color.g * 255; 
32                 pixels[i + 2] = color.b * 255; 
33                 pixels[i + 3] = 255; 
34             } 
35             i += 4; 
36         } 
37     } 
38   
39     ctx.putImageData(imgdata, 0, 0); 
40 } 

   [Ctrl+A 全部选择 提示:你可先修改部分代码,再按运行]

  修改代码试试看

  改变光源的颜色 (也试试超过1的值)

  改变光源的方向 (在DirectionalLight.initialize()里自动做了normalize,这输入不需位单位向量)

  改变光源的幅照度 (也试试超过1的值)

  改变光源的方向 (在DirectionalLight.initialize()里自动做了normalize,这输入不需位单位向量)

  点光源

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

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

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

 

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

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

  实现PointLight类

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

01 PointLight = function(intensity, position) { this.intensity = intensity; this.position = position; this.shadow = true; }; 
02   
03 PointLight.prototype = { 
04     initialize: function() { }, 
05     sample: function(scene, position) { 
06         // 计算L,但保留r和r^2,供之后使用 
07         var delta = this.position.subtract(position); 
08         var rr = delta.sqrLength(); 
09         var r = Math.sqrt(rr); 
10         var L = delta.divide(r); 
11   
12         // 阴影测试 
13         if (this.shadow) { 
14             var shadowRay = new Ray3(position, L); 
15             var shadowResult = scene.intersect(shadowRay); 
16             // 在r以内的相交点才会遮蔽光源 
17             if (shadowResult.geometry && shadowResult.distance <= r) 
18                 return LightSample.zero; 
19         } 
20   
21         // 平方反比衰减 
22         var attenuation = 1 / rr; 
23   
24         // 计算幅照度 
25         return new LightSample(L, this.intensity.multiply(attenuation)); 
26     } 
27 }; 

   [Ctrl+A 全部选择 提示:你可先修改部分代码,再按运行]

  修改代码试试看

  改变幅射强度

  移动光源

  加入多一个点光源

  聚光灯

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

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

  实现SpotLight类

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

01 SpotLight = function(intensity, position, direction, theta, phi, falloff) { 
02     this.intensity = intensity; 
03     this.position = position; 
04     this.direction = direction; 
05     this.theta = theta; 
06     this.phi = phi; 
07     this.falloff = falloff; 
08     this.shadow = true; 
09 }; 
10   
11 SpotLight.prototype = { 
12     initialize: function() { 
13         this.S = this.direction.normalize().negate(); 
14         this.cosTheta = Math.cos(this.theta * Math.PI / 180 / 2); 
15         this.cosPhi = Math.cos(this.phi * Math.PI / 180 / 2); 
16         this.baseMultiplier = 1 / (this.cosTheta - this.cosPhi); 
17     }, 
18   
19     sample: function(scene, position) { 
20         // 计算L,但保留r和r^2,供之后使用 
21         var delta = this.position.subtract(position); 
22         var rr = delta.sqrLength(); 
23         var r = Math.sqrt(rr); 
24         var L = delta.divide(r); 
25   
26         // 计算聚光灯因子 
27         var spot; 
28         var SdotL = this.S.dot(L); 
29         if (SdotL >= this.cosTheta) 
30             spot = 1; 
31         else if (SdotL <= this.cosPhi) 
32             spot = 0; 
33         else 
34             spot = Math.pow((SdotL - this.cosPhi) * this.baseMultiplier, this.falloff); 
35   
36         // 阴影测试 
37         if (this.shadow) { 
38             var shadowRay = new Ray3(position, L); 
39             var shadowResult = scene.intersect(shadowRay); 
40             // 在r以内的相交点才会遮蔽光源 
41             if (shadowResult.geometry && shadowResult.distance <= r) 
42                 return LightSample.zero; 
43         } 
44   
45         // 平方反比衰减 
46         var attenuation = 1 / rr; 
47   
48         // 计算幅照度 
49         return new LightSample(L, this.intensity.multiply(attenuation * spot)); 
50     } 
51 }; 

   [Ctrl+A 全部选择 提示:你可先修改部分代码,再按运行]

  修改代码试试看

  改变各个参数

  例子三原色

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

   [Ctrl+A 全部选择 提示:你可先修改部分代码,再按运行]

  修改代码试试看

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

  很多光源

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

   [Ctrl+A 全部选择 提示:你可先修改部分代码,再按运行]

  修改代码试试看

  把光源放在不同位置(例如接近地面)

  把每个光源的颜色加入差异

  结语

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

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

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

本文示例源代码或素材下载


« 
» 
快速导航

Copyright © 2016 phpStudy | 豫ICP备2021030365号-3