javascript
用JavaScript玩转计算机图形学(一)光线追踪入门
系列簡(jiǎn)介
記得小時(shí)候讀過一本關(guān)于計(jì)算機(jī)圖形學(xué)(computer graphics, CG)的入門書,從此就愛上了CG。本系列希望,采用很多人認(rèn)識(shí)的JavaScript語言去分享CG,令更多人有機(jī)會(huì)接觸,并愛上CG。
本系列的特點(diǎn)之一,是讀者能在瀏覽器里直接執(zhí)行代碼,也可重覆修改代碼測(cè)試。透過這種互動(dòng),也許能更深刻體會(huì)內(nèi)容。讀者只要懂得JavaScript(因?yàn)镴avaScript很簡(jiǎn)單,學(xué)過Java/C/C++/C#之類的語言也應(yīng)沒問題)和一點(diǎn)點(diǎn)線性代數(shù)(linear algebra)就可以了。
筆者在大學(xué)期間并沒有修讀CG課程,雖然看過相關(guān)書籍,始終未親手做過全域光照的渲染器,本文也作為個(gè)人的學(xué)習(xí)分享。此外,筆者也差不多十年沒接觸JavaScript,希望各位不吝賜教。
本文簡(jiǎn)介
多數(shù)程序員聽到3D CG,就會(huì)聯(lián)想到Direct3D、OpenGL等API。事實(shí)上,這些流行的API主要為實(shí)時(shí)渲染(real-time rendering)而設(shè),一般采用光柵化(rasterization)方式,渲染大量的三角形(或其他幾何圖元種類(primitive types))。這種基于光柵化的渲染系統(tǒng),只支持局部光照(local illumination)。換句話說,渲染幾何圖形的一個(gè)像素時(shí),光照計(jì)算只能取得該像素的資訊,而不能訪問其他幾何圖形資訊。理論上,陰影(shadow)、反射(reflection)、折射(refraction)等為全局光照(global illumination)效果,實(shí)際上,柵格化渲染系統(tǒng)可以使用預(yù)處理(如陰影貼圖(shadow mapping)、環(huán)境貼圖(environment mapping))去模擬這些效果。
全局光照計(jì)算量大,一般也沒有特殊硬件加速(通常只使用CPU而非GPU),所以只適合離線渲染(offline rendering),例如3D Studio Max、Maya等工具。其中一個(gè)支持全局光照的方法,稱為光線追蹤(ray tracing)。光線追蹤能簡(jiǎn)單直接地支持陰影、反射、折射,實(shí)現(xiàn)起來亦非常容易。本文的例子里,只用了數(shù)十行JavaScript代碼(除canvas外不需要其他特殊插件和庫),就能實(shí)現(xiàn)一個(gè)支持反射的光線追蹤渲染器。光線追蹤可以用來學(xué)習(xí)很多計(jì)算機(jī)圖形學(xué)的課題,也許比學(xué)習(xí)Direct3D/OpenGL更容易。現(xiàn)在,先介紹點(diǎn)理論吧。
光線追蹤
光柵化渲染,簡(jiǎn)單地說,就是把大量三角形畫到屏幕上。當(dāng)中會(huì)采用深度緩沖(depth buffer, z-buffer),來解決多個(gè)三角形重疊時(shí)的前后問題。三角形數(shù)目影響效能,但三角形在屏幕上的總面積才是主要瓶頸。
光線追蹤,簡(jiǎn)單地說,就是從攝影機(jī)的位置,通過影像平面上的像素位置(比較正確的說法是取樣(sampling)位置),發(fā)射一束光線到場(chǎng)景,求光線和幾何圖形間最近的交點(diǎn),再求該交點(diǎn)的著色。如果該交點(diǎn)的材質(zhì)是反射性的,可以在該交點(diǎn)向反射方向繼續(xù)追蹤。光線追蹤除了容易支持一些全局光照效果外,亦不局限于三角形作為幾何圖形的單位。任何幾何圖形,能與一束光線計(jì)算交點(diǎn)(intersection point),就能支持。
上圖(來源)顯示了光線追蹤的基本方式。要計(jì)算一點(diǎn)是否在陰影之內(nèi),也只須發(fā)射一束光線到光源,檢測(cè)中間有沒有障礙物而已。不過光源和陰影留待下回分解。
初試畫板
光線追蹤的輸出只是一個(gè)影像(image),所謂影像,就是二維顏色數(shù)組。
要在瀏覽器內(nèi),用JavaScript生成一個(gè)影像,目前可以使用HTML 5的<canvas>。但現(xiàn)時(shí)Internet Explorer(直至版本8)還不支持<canvas>,其他瀏覽器如Chrome、Firefox、Opera等就可以。
以下是一個(gè)簡(jiǎn)單的實(shí)驗(yàn),把每個(gè)象素填入顏色,左至右越來越紅,上至下越來越綠。
Run
| ? | 左邊的canvas定義如下:
修改代碼試試看
|
這實(shí)驗(yàn)說明,從canvas取得的影像資料canvas.getImageData(...).data是個(gè)一維數(shù)組,該數(shù)組每四個(gè)元素代表一個(gè)象素(按紅, 綠, 藍(lán), alpha排列),這些象素在影像中從上至下、左至右排列。
解決實(shí)驗(yàn)平臺(tái)的技術(shù)問題后,可開始從基礎(chǔ)類別開始實(shí)現(xiàn)。
基礎(chǔ)類
筆者使用基于物件(object-based)的方式編寫JavaScript。
三維向量
三維向量(3D vector)可謂CG里最常用型別了。這里三維向量用Vector3類實(shí)現(xiàn),用(x, y, z)表示。 Vector3亦用來表示空間中的點(diǎn)(point),而不另建類。先看代碼:
| 1234567891011121314151617 | Vector3 = function(x, y, z) { this.x = x; this.y = y; this.z = z; };Vector3.prototype = {????copy : function() { return?new?Vector3(this.x, this.y, this.z); },????length : function() { return?Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); },????sqrLength : function() { return?this.x * this.x + this.y * this.y + this.z * this.z; },????normalize : function() { var?inv = 1/this.length(); return?new?Vector3(this.x * inv, this.y * inv, this.z * inv); },????negate : function() { return?new?Vector3(-this.x, -this.y, -this.z); },????add : function(v) { return?new?Vector3(this.x + v.x, this.y + v.y, this.z + v.z); },????subtract : function(v) { return?new?Vector3(this.x - v.x, this.y - v.y, this.z - v.z); },????multiply : function(f) { return?new?Vector3(this.x * f, this.y * f, this.z * f); },????divide : function(f) { var?invf = 1/f; return?new?Vector3(this.x * invf, this.y * invf, this.z * invf); },????dot : function(v) { return?this.x * v.x + this.y * v.y + this.z * v.z; },????cross : function(v) { return?new?Vector3(-this.z * v.y + this.y * v.z, this.z * v.x - this.x * v.z, -this.y * v.x + this.x * v.y); }};Vector3.zero = new?Vector3(0, 0, 0); |
這些類方法(如normalize、negate、add等),如果傳回Vector3類對(duì)象,都會(huì)傳回一個(gè)新建構(gòu)的Vector3。這些三維向量的功能很簡(jiǎn)單,不在此詳述。注意multiply和divide是與純量(scalar)相乘和相除。
Vector3.zero用作常量,避免每次重新構(gòu)建。值得一提,這些常量必需在prototype設(shè)定之后才能定義。
光線
所謂光線(ray),從一點(diǎn)向某方向發(fā)射也。數(shù)學(xué)上可用參數(shù)函數(shù)(parametric function)表示:
當(dāng)中,o即發(fā)謝起點(diǎn)(origin),d為方向。在本文的例子里,都假設(shè)d為單位向量(unit vector),因此t為距離。實(shí)現(xiàn)如下:
| 12345 | Ray3 = function(origin, direction) { this.origin = origin; this.direction = direction; }Ray3.prototype = {????getPoint : function(t) { return?this.origin.add(this.direction.multiply(t)); }}; |
球體
球體(sphere)是其中一個(gè)最簡(jiǎn)單的立體幾何圖形。這里只考慮球體的表面(surface),中心點(diǎn)為c、半徑為r的球體表面可用等式(equation)表示:
如前文所述,需要計(jì)算光線和球體的最近交點(diǎn)。只要把光線x = r(t)代入球體等式,把該等式求解就是交點(diǎn)。為簡(jiǎn)化方程,設(shè)v=o - c,則:
因?yàn)閐為單位向量,所以二次方的系數(shù)可以消去。 t的二次方程式的解為
若根號(hào)內(nèi)為負(fù)數(shù),即相交不發(fā)生。另外,由于這里只需要取最近的交點(diǎn),因此正負(fù)號(hào)只需取負(fù)號(hào)。代碼實(shí)現(xiàn)如下:
| 1234567891011121314151617181920212223242526272829 | Sphere = function(center, radius) { this.center = center; this.radius = radius; };Sphere.prototype = {????copy : function() { return?new?Sphere(this.center.copy(), this.radius.copy()); },????initialize : function() {????????this.sqrRadius = this.radius * this.radius;????},????intersect : function(ray) {????????var?v = ray.origin.subtract(this.center);????????var?a0 = v.sqrLength() - this.sqrRadius;????????var?DdotV = ray.direction.dot(v);????????if?(DdotV <= 0) {????????????var?discr = DdotV * DdotV - a0;????????????if?(discr >= 0) {????????????????var?result = new?IntersectResult();????????????????result.geometry = this;????????????????result.distance = -DdotV - Math.sqrt(discr);????????????????result.position = ray.getPoint(result.distance);????????????????result.normal = result.position.subtract(this.center).normalize();????????????????return?result;????????????}????????}????????return?IntersectResult.noHit;????}}; |
實(shí)現(xiàn)代碼時(shí),盡快用最少的運(yùn)算剔除沒相交的情況(Math.sqrt是比較慢的函數(shù))。另外,預(yù)計(jì)算了球體半徑r的平方,此為一個(gè)優(yōu)化。
這里用到一個(gè)IntersectResult類,這個(gè)類只用來記錄交點(diǎn)的幾何物件(geometry)、距離(distance)、位置(position)和法向量(normal)。 IntersectResult.noHit的geometry為null,代表光線沒有和任何幾何物件相交。
| 12345678 | IntersectResult = function() {????this.geometry = null;????this.distance = 0;????this.position = Vector3.zero;????this.normal = Vector3.zero;};IntersectResult.noHit = new?IntersectResult(); |
攝影機(jī)
攝影機(jī)在光線追蹤系統(tǒng)里,負(fù)責(zé)把影像的取樣位置,生成一束光線。
由于影像的大小是可變的(多少像素寬x多少像素高),為方便計(jì)算,這里設(shè)定一個(gè)統(tǒng)一的取樣座標(biāo)(sx, sy),以左下角為(0,0),右上角為(1 ,1)。
從數(shù)學(xué)角度來說,攝影機(jī)透過投影(projection),把三維空間投射到二維空間上。常見的投影有正投影(orthographic projection)、透視投影(perspective projection)等等。這里首先實(shí)現(xiàn)透視投影。 ]]>
透視攝影機(jī)
透視攝影機(jī)比較像肉眼和真實(shí)攝影機(jī)的原理,能表現(xiàn)遠(yuǎn)小近大的觀察方式。透視投影從視點(diǎn)(view point/eye position),向某個(gè)方向觀察場(chǎng)景,觀察的角度范圍稱為視野(field of view, FOV)。除了定義觀察的向前(forward)是那個(gè)方向,還需要定義在影像平面中,何謂上下和左右。為簡(jiǎn)單起見,暫時(shí)不考慮寬高不同的影像,FOV同時(shí)代表水平和垂直方向的視野角度。
上圖顯示,從攝影機(jī)上方顯示的幾個(gè)參數(shù)。 forward和right分別是向前和向右的單位向量。
因?yàn)橐朁c(diǎn)是固定的,光線的起點(diǎn)不變。要生成光線,只須用取樣座標(biāo)(sx, sy)計(jì)算其方向d。留意FOV和s的關(guān)系為:
把sx從[0, 1]映射到[-1,1],就可以用right向量和s,來計(jì)算r向量,代碼如下:
| 123456789101112131415 | PerspectiveCamera = function(eye, front, up, fov) { this.eye = eye; this.front = front; this.refUp = up; this.fov = fov; };PerspectiveCamera.prototype = {????initialize : function() {????????this.right = this.front.cross(this.refUp);????????this.up = this.right.cross(this.front);????????this.fovScale = Math.tan(this.fov * 0.5 * Math.PI / 180) * 2;????},????generateRay : function(x, y) {????????var?r = this.right.multiply((x - 0.5) * this.fovScale);????????var?u = this.up.multiply((y - 0.5) * this.fovScale);????????return?new?Ray3(this.eye, this.front.add(r).add(u).normalize());????}}; |
代碼中fov為度數(shù),轉(zhuǎn)為弧度才能使用Math.tan()。另外,fovScale預(yù)先乘了2,因?yàn)閟x映射到[-1,1]每次都要乘以2。 sy和sx的做法一樣,把兩個(gè)在影像平面的向量,加上forward向量,就成為光線方向d。因之后的計(jì)算需要,最后把d變成單位向量。
渲染測(cè)試
寫了Vector3、Ray3、Sphere、IntersectResult、Camera五個(gè)類之后,終于可以開始渲染一點(diǎn)東西出來!
基本的做法是遍歷影像的取樣座標(biāo)(sx, sy),用Camera把(sx, sy)轉(zhuǎn)為Ray3,和場(chǎng)景(例如Sphere)計(jì)算最近交點(diǎn),把該交點(diǎn)的屬性轉(zhuǎn)為顏色,寫入影像的相對(duì)位置里。
把不同的屬性渲染出來,是CG編程里經(jīng)常用的測(cè)試和調(diào)試手法。筆者也是用此方法,修正了一些錯(cuò)誤。
渲染深度
深度(depth)就是從IntersectResult取得最近相交點(diǎn)的距離,因深度的范圍是從零至無限,為了把它顯示出來,可以把它的一個(gè)區(qū)間映射到灰階。這里用[0, maxDepth]映射至[255, 0],即深度0的像素為白色,深度達(dá)maxDepth的像素為黑色。
| 12345678910111213141516171819202122232425262728 | // renderDepth.htmfunction?renderDepth(canvas, scene, camera, maxDepth) {????// 從canvas取得imgdata和pixels,跟之前的代碼一樣????// ...????scene.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?depth = 255 - Math.min((result.distance / maxDepth) * 255, 255);????????????????pixels[i??? ] = depth;????????????????pixels[i + 1] = depth;????????????????pixels[i + 2] = depth;????????????????pixels[i + 3] = 255;????????????}????????????i += 4;????????}????}????ctx.putImageData(imgdata, 0, 0);} |
Run
| ? | 這里的觀看方向是,正X軸向右,正Y軸向上,正Z軸向后。 修改代碼試試看
|
渲染法向量
相交測(cè)試也計(jì)算了幾何物件在相交位置的法向量,這里也可把它視覺化。法向量是一個(gè)單位向量,其每個(gè)元素的范圍是[-1, 1]。把單位向量映射到顏色的常用方法為,把(x, y, z)映射至(r, g, b),范圍從[-1, 1]映射至[0, 255]。
| 1 2 3 4 5 6 7 8 9 10 11 | // renderNormal.htm function?renderNormal(canvas, scene, camera) { ????// ... ????????????if?(result.geometry) { ????????????????pixels[i??? ] = (result.normal.x + 1) * 128; ????????????????pixels[i + 1] = (result.normal.y + 1) * 128; ????????????????pixels[i + 2] = (result.normal.z + 1) * 128; ????????????????pixels[i + 3] = 255; ????????????} ????// ... } |
Run
| ? | 球體上方的法向量是接近(0, 1, 0),所以是淺綠色(0.5, 1, 0.5)。 修改代碼試試看
|
材質(zhì)
渲染深度和法向量只為測(cè)試和調(diào)試,要顯示物件的"真實(shí)"顏色,需要定義該交點(diǎn)向某方向(如往視點(diǎn)的方向)發(fā)出的光的顏色,稱之為幾個(gè)圖形的材質(zhì)(material )。
材質(zhì)的接口為function sample(ray, posiiton, normal) ,傳回顏色Color的對(duì)象。這是個(gè)極簡(jiǎn)陋的接口,臨時(shí)做一些效果出來,有機(jī)會(huì)再詳談。
顏色
顏色在CG里最簡(jiǎn)單是用紅、綠、藍(lán)三個(gè)通道(color channel)。為實(shí)現(xiàn)簡(jiǎn)單的Phong材質(zhì),還加入了對(duì)顏色的簡(jiǎn)單操作。
| 1234567891011121314 | Color = function(r, g, b) { this.r = r; this.g = g; this.b = b };Color.prototype = {????copy : function() { return?new?Color(this.r, this.g, this.b); },????add : function(c) { return?new?Color(this.r + c.r, this.g + c.g, this.b + c.b); },????multiply : function(s) { return?new?Color(this.r * s, this.g * s, this.b * s); },????modulate : function(c) { return?new?Color(this.r * c.r, this.g * c.g, this.b * c.b); }};Color.black = new?Color(0, 0, 0);Color.white = new?Color(1, 1, 1);Color.red = new?Color(1, 0, 0);Color.green = new?Color(0, 1, 0);Color.blue = new?Color(0, 0, 1); |
這Color類很像Vector3類,值得留意的是,顏色有調(diào)制(modulate)操作,其意義為兩個(gè)顏色中每個(gè)顏色通道相乘。
格子材質(zhì)
CG世界里,國(guó)際象棋棋盤是最常見的測(cè)試用紋理(texture)。這里不考慮紋理貼圖(texture mapping)的問題,只憑(x, z)坐標(biāo)計(jì)算某位置發(fā)出黑色或白色的光(黑色的光不叫光吧,哈哈)。
| 1234567 | CheckerMaterial = function(scale, reflectiveness) { this.scale = scale; this.reflectiveness = reflectiveness; };CheckerMaterial.prototype = {????sample : function(ray, position, normal) {????????return?Math.abs((Math.floor(position.x * 0.1) + Math.floor(position.z * this.scale)) % 2) < 1 ? Color.black : Color.white;????}}; |
代碼中scale的意義為1坐標(biāo)單位有多少個(gè)格子,例如scale=0.1即一個(gè)格子的大小為10x10。
Phong材質(zhì)
這里實(shí)現(xiàn)簡(jiǎn)單的Phong材質(zhì),因?yàn)槲从泄庠聪到y(tǒng),只用全域變量設(shè)置一個(gè)臨時(shí)的光源方向,并只計(jì)算漫射(diffuse)和鏡射(specular)。
| 123456789101112131415161718192021 | PhongMaterial = function(diffuse, specular, shininess, reflectiveness) {????this.diffuse = diffuse;????this.specular = specular;????this.shininess = shininess;????this.reflectiveness = reflectiveness;};// global tempvar?lightDir = new?Vector3(1, 1, 1).normalize();var?lightColor = Color.white;PhongMaterial.prototype = {????sample: function(ray, position, normal) {????????var?NdotL = normal.dot(lightDir);????????var?H = (lightDir.subtract(ray.direction)).normalize();????????var?NdotH = normal.dot(H);????????var?diffuseTerm = this.diffuse.multiply(Math.max(NdotL, 0));????????var?specularTerm = this.specular.multiply(Math.pow(Math.max(NdotH, 0), this.shininess));????????return?lightColor.modulate(diffuseTerm.add(specularTerm));????}}; |
Phong的內(nèi)容不在此述。
渲染材質(zhì)
修改之前的渲染代碼,當(dāng)碰到相交時(shí),就向幾何對(duì)象取得material屬性,并調(diào)用sample方法函數(shù)取得顏色。
| 123456789101112 | // rayTrace.htmfunction?rayTrace(canvas, scene, camera) {????// ...????????????if?(result.geometry) {????????????????var?color = result.geometry.material.sample(ray, result.position, result.normal);????????????????pixels[i] = color.r * 255;????????????????pixels[i + 1] = color.g * 255;????????????????pixels[i + 2] = color.b * 255;????????????????pixels[i + 3] = 255;????????????}????// ...} |
Run
| ? | 修改代碼試試看
|
多個(gè)幾何物件
只渲染一個(gè)幾何物件太乏味,這節(jié)再加入一個(gè)無限平面,和介紹如何組合多個(gè)幾何物件。
平面
一個(gè)(無限)平面(Plane)在數(shù)學(xué)上可用等式定義:
n為平面的法向量,d為空間原點(diǎn)至平面的最短距離。光線和平面的相交計(jì)算很簡(jiǎn)單,這里不詳述了。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | Plane = function(normal, d) { this.normal = normal; this.d = d; }; Plane.prototype = { ????copy : function() { return?new?plane(this.normal.copy(), this.d); }, ????initialize : function() { ????????this.position = this.normal.multiply(this.d); ????}, ????? ????intersect : function(ray) { ????????var?a = ray.direction.dot(this.normal); ????????if?(a >= 0) ????????????return?IntersectResult.noHit; ????????var?b = this.normal.dot(ray.origin.subtract(this.position)); ????????var?result = new?IntersectResult(); ????????result.geometry = this; ????????result.distance = -b / a; ????????result.position = ray.getPoint(result.distance); ????????result.normal = this.normal; ????????return?result; ????} }; |
并集
把多個(gè)幾何物件結(jié)合起來,可以使用集(set)的概念。這里最容易實(shí)現(xiàn)的操作,就是并集(union),即光線要找到一組幾個(gè)圖形的最近交點(diǎn)。無需改其他代碼,只加入一個(gè)Union類就可以:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | Union = function(geometries) { this.geometries = geometries; }; Union.prototype = { ????initialize: function() { ????????for?(var?i in?this.geometries) ????????????this.geometries[i].initialize(); ????}, ????? ????intersect: function(ray) { ????????var?minDistance = Infinity; ????????var?minResult = IntersectResult.noHit; ????????for?(var?i in?this.geometries) { ????????????var?result = this.geometries[i].intersect(ray); ????????????if?(result.geometry && result.distance < minDistance) { ????????????????minDistance = result.distance; ????????????????minResult = result; ????????????} ????????} ????????return?minResult; ????} }; |
可以看到,這里利用Javascript的多型(polymorphism)的特性,完全不用修改原來的代碼,就可以擴(kuò)展功能。
如前所述,這里只考慮幾何幾何圖形的表面。如果考慮幾何圖形是實(shí)心的,就可以用構(gòu)造實(shí)體幾何(constructive solid geometry, CSG)方法,提供并集、交集、補(bǔ)集等操作。容后再談。
反射
以上實(shí)現(xiàn)的,也只是局部照明。只要再加入一點(diǎn)點(diǎn)代碼,就可以實(shí)現(xiàn)反射。
下圖說明反射向量的計(jì)算方法:
把d投射到n上(因n是單位向量,只需要點(diǎn)乘即可),就可以計(jì)算d在n上的長(zhǎng)度,把d減去這長(zhǎng)度兩倍的法向量,就是反射向量r。數(shù)學(xué)上可寫成:
<img src="http://latex.codecogs.com/png.latex?\mathbf{r}%20=%20\mathbfozvdkddzhkzd%20-%202(\mathbf{d%20\cdot%20n})\bf{n}" "="" style="border: 0px; display: block; margin-left: auto; margin-right: auto; max-width: 900px;">一般材質(zhì)并非完全反射(鏡子除外),因此這里為材質(zhì)加上一個(gè)反射度(reflectiveness)的屬性。反射的功能很簡(jiǎn)單,只要在碰到反射度非零的材質(zhì),就繼續(xù)向反射方向追蹤,并把結(jié)果按反射度來混合。例如一個(gè)材質(zhì)的反射度為25%,則它傳回的顏色是75%本身顏色,加上25%反射傳回來的顏色。
另外,不斷反射會(huì)做成大量的運(yùn)算,甚至乎永遠(yuǎn)不能停止(考慮攝影機(jī)在兩個(gè)鏡子中間)。因此要限制反射的次數(shù)。含反射功能的光線追蹤代碼如下:
| 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 | function?rayTraceRecursive(scene, ray, maxReflect) { ????var?result = scene.intersect(ray); ????? ????if?(result.geometry) { ????????var?reflectiveness = result.geometry.material.reflectiveness; ????????var?color = result.geometry.material.sample(ray, result.position, result.normal); ????????color = color.multiply(1 - reflectiveness); ????????? ????????if?(reflectiveness > 0 && maxReflect > 0) { ????????????var?r = result.normal.multiply(-2 * result.normal.dot(ray.direction)).add(ray.direction); ????????????ray = new?Ray3(result.position, r); ????????????var?reflectedColor = rayTraceRecursive(scene, ray, maxReflect - 1); ????????????color = color.add(reflectedColor.multiply(reflectiveness)); ????????} ????????return?color; ????} ????else ????????return?Color.black; } function?rayTraceReflection(canvas, scene, camera, maxReflect) { ????// 從canvas取得imgdata和pixels,跟之前的代碼一樣 ????// ... ????scene.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?color = rayTraceRecursive(scene, ray, maxReflect); ????????????pixels[i++] = color.r * 255; ????????????pixels[i++] = color.g * 255; ????????????pixels[i++] = color.b * 255; ????????????pixels[i++] = 255; ????????} ????} ????ctx.putImageData(imgdata, 0, 0); } |
Run
| ? | 修改代碼試試看
|
結(jié)語
能體會(huì)到計(jì)算機(jī)圖形學(xué)的有趣之處么?百多行簡(jiǎn)單的JavaScript代碼,就繪畫出像真的影像,那種滿足感實(shí)非筆墨所能形容。
本文實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的光線追蹤渲染器,支持球體、平面、Phong材質(zhì)、格子材質(zhì)、多重反射等功能。讀者可以下載這組代碼,加入不同的擴(kuò)展,也可以嘗試翻譯做熟悉的編程語言。很多光線追蹤用到的計(jì)算機(jī)圖形技術(shù),也可以應(yīng)用到實(shí)時(shí)圖形編程里,例如光源和材質(zhì)的計(jì)算,基本上可以簡(jiǎn)易翻譯做實(shí)時(shí)圖形的著色器(shader)編程。
游戲里采用光柵化渲染技術(shù)已有二十年以上,這幾年的硬件發(fā)展,使其他渲染方法也能用于實(shí)時(shí)應(yīng)用。光線追蹤和其他類似的方法,有個(gè)當(dāng)今重要優(yōu)點(diǎn),就是能高度平行化。采樣之間并沒有依賴性,例如256x256=65536個(gè)采樣,理論上,可使用65536個(gè)機(jī)器/核心獨(dú)立執(zhí)行追蹤,那么完成時(shí)間只是最慢的一個(gè)取樣所需的時(shí)間。
筆者希望繼續(xù)撰寫這系列,例如包括以下內(nèi)容:
- 其他幾何圖形(長(zhǎng)方體、柱體、三角形、曲面、高度場(chǎng)、等值面、……)
- 光源(方向光源、點(diǎn)光源、聚光燈、陰影、ambient occlusion)
- 材質(zhì)(Phong-Blinn、Oren-Nayar、Torrance-Sparrow、折射、 Fresnel、BRDF、BSDF……)
- 紋理(紋理座標(biāo)、采樣、Perlin noise)
- 攝影機(jī)模型(正投射、全景、景深)
- 成像流程(漸進(jìn)渲染、反鋸齒、后期處理)
- 優(yōu)化方法(場(chǎng)景剖分、低階優(yōu)化)
- 其他全局光照渲染方法
祈望得到大家的意見反饋。
參考
- Matt Pharr, Greg Humphreys, Physically Based Rendering, Morgan Kaufmann, 2004
- Wikipedia,?Ray Tracing
- Slime,?The JavaScript Raytracer
- SIGGRAPH HyperGraph Education Project,?Ray Tracing
更新
- 2010年3月31日,網(wǎng)友HouSisong把本文代碼以C++實(shí)現(xiàn),并完全保留了原設(shè)計(jì),代碼可於他的博文下載。
from:?http://www.cnblogs.com/miloyip/archive/2010/03/29/1698953.html
總結(jié)
以上是生活随笔為你收集整理的用JavaScript玩转计算机图形学(一)光线追踪入门的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 清华大学计算机图形学课程
- 下一篇: 用JavaScript玩转计算机图形学(