javascript
用JavaScript玩转计算机图形学(二)基本光源
上一篇介紹了簡單的光線追蹤,湊合了臨時用的光源去渲染效果。這次將講解三種基本光源,及一些背景理論。過分簡化的教材和現成API(OpenGL/Direct3D等)可能會做成一些錯誤理解。在此,希望文章能簡單之余,又不失背后理論。讀者明白之后,可把概念簡化,或按實際情況調整。
本文代碼可在此下載(10KiB)。
讀者若喜歡本文,可按推薦按鈕以示鼓勵。如果寫得不夠清楚,或有錯誤之處,可留言相告。
光
在物理上,光(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 2 | LightSample = function(L, EL) { this.L = L; this.EL = EL; }; LightSample.zero = new?LightSample(Vector3.zero, Color.black); |
以下是方向光源的代碼,預設使用陰影:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | 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); } |
Run
| ? | 修改代碼試試看
|
點光源
點光源/點光燈(point light),又稱全向光源/泛光源/泛光燈(omnidirectional light/omni light),是指一個無限小的點,向所有光向平均地散射光。
其光向量,就是表面位置往點光源位置的方向:
學習物理時,經常有這種往所有方向發射的情況(例如引力、聲音等)。類比可知,接收到的能量和距離的關系,是成平方反比定律的:
當中I為幅射強度(intensity, radiant intensity),當r=1時,幅射強度和幅照度相等。
通常稱為衰減(attenuation)系數。有時候會為各種需求,寫一些非物理正確的衰減系數。
實現PointLight類
以下代碼中,不直接使用normalize(),令r和其平方可以在之后分別使用,算是簡單的優化。
| 123456789101112131415161718192021222324252627 | 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));????}}; |
Run
| ? | 修改代碼試試看
|
聚光燈
現實中,并不存在理想的點光源,放射的光在不同方向是有差異的。聚光燈(spot light)是常用的一種模式,它在點光源的基礎上,加入圓錐形的范圍。聚光燈可以有不同的模型,以下采用Direct3D固定功能管道(fixed-function pipeline)用的模型做示范。
聚光燈有一個主要方向s,再設置兩個圓錐范圍,稱為內圓錐和外圓錐,兩圓錐之間的范圍稱為半影(penumbra)。內外圓錐的內角分別為和。聚光燈可計算一個聚光燈系數,范圍為[0,1],代表某方向的放射比率。內圓錐中系數為1(最亮),內圓錐和外圓錐之間系數由1逐漸變成0。另外,可用另一參數p代表衰減(falloff),決定內圓錐和外圓錐之間系數變化。方程式如下:
實現SpotLight類
SpotLight類只是多了那幾個參數,以計算聚光燈系數,最后結合到幅照度。很多參數可在initialize()里預計算,減少在sample()里重復運算。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | 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)); ????} }; |
Run
| ? | 修改代碼試試看
|
例子
三原色
這個例子把三原色聚光燈重疊射度地板,可以看到它們的顏色混合。
Run
| ? | 修改代碼試試看
|
很多光源
這個例子在天花加了36個點光源,和一個從后往前的填充用方向光源。有時候燈光師會加入填充光源(fill light),去加強對象的輪廓及立體感(有時候用上冷暖色的對比)。這個渲染比較慢,可能要半分鐘啊!
Run
| ? | 修改代碼試試看
|
結語
本文簡單介紹了三種基本的光源,這些光源除了應用在光線追蹤渲染器上,也常用在光柵化渲染器中。
除這三種以外,還有一類比較高階的光源──面光源(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
總結
以上是生活随笔為你收集整理的用JavaScript玩转计算机图形学(二)基本光源的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 用JavaScript玩转计算机图形学(
- 下一篇: C# vs C++ 全局照明渲染性能比试