Spiga

用JavaScript玩转计算机图形学(一)光线追踪入门

2010-03-29 00:05 by Milo Yip, 23059 visits, 收藏, 编辑

系列简介

记得小时候读过一本关于计算机图形学(computer graphics, CG)的入门书,从此就爱上了CG。本系列希望,采用很多人认识的JavaScript语言去分享CG,令更多人有机会接触,并爱上CG。

本系列的特点之一,是读者能在浏览器里直接执行代码,也可重覆修改代码测试。透过这种互动,也许能更深刻体会内容。读者只要懂得JavaScript(因为JavaScript很简单,学过Java/C/C++/C#之类的语言也应没问题)和一点点线性代数(linear algebra)就可以了。

笔者在大学期间并没有修读CG课程,虽然看过相关书籍,始终未亲手做过全域光照的渲染器,本文也作为个人的学习分享。此外,笔者也差不多十年没接触JavaScript,希望各位不吝赐教。

本文简介

多数程序员听到3D CG,就会联想到Direct3D、OpenGL等API。事实上,这些流行的API主要为实时渲染(real-time rendering)而设,一般采用光栅化(rasterization)方式,渲染大量的三角形(或其他几何图元种类(primitive types))。这种基于光栅化的渲染系统,只支持局部光照(local illumination)。换句话说,渲染几何图形的一个像素时,光照计算只能取得该像素的资讯,而不能访问其他几何图形资讯。理论上,阴影(shadow)、反射(reflection)、折射(refraction)等为全局光照(global illumination)效果,实际上,栅格化渲染系统可以使用预处理(如阴影贴图(shadow mapping)、环境贴图(environment mapping))去模拟这些效果。

全局光照计算量大,一般也没有特殊硬件加速(通常只使用CPU而非GPU),所以只适合离线渲染(offline rendering),例如3D Studio Max、Maya等工具。其中一个支持全局光照的方法,称为光线追踪(ray tracing)。光线追踪能简单直接地支持阴影、反射、折射,实现起来亦非常容易。本文的例子里,只用了数十行JavaScript代码(除canvas外不需要其他特殊插件和库),就能实现一个支持反射的光线追踪渲染器。光线追踪可以用来学习很多计算机图形学的课题,也许比学习Direct3D/OpenGL更容易。现在,先介绍点理论吧。

光线追踪

光栅化渲染,简单地说,就是把大量三角形画到屏幕上。当中会采用深度缓冲(depth buffer, z-buffer),来解决多个三角形重叠时的前后问题。三角形数目影响效能,但三角形在屏幕上的总面积才是主要瓶颈。

光线追踪,简单地说,就是从摄影机的位置,通过影像平面上的像素位置(比较正确的说法是取样(sampling)位置),发射一束光线到场景,求光线和几何图形间最近的交点,再求该交点的著色。如果该交点的材质是反射性的,可以在该交点向反射方向继续追踪。光线追踪除了容易支持一些全局光照效果外,亦不局限于三角形作为几何图形的单位。任何几何图形,能与一束光线计算交点(intersection point),就能支持。

上图(來源)显示了光线追踪的基本方式。要计算一点是否在阴影之内,也只须发射一束光线到光源,检测中间有没有障碍物而已。不过光源和阴影留待下回分解。

初试画板

光线追踪的输出只是一个影像(image),所谓影像,就是二维颜色数组。

要在浏览器内,用JavaScript生成一个影像,目前可以使用HTML 5的<canvas>。但现时Internet Explorer(直至版本8)还不支持<canvas>,其他浏览器如Chrome、Firefox、Opera等就可以。

以下是一个简单的实验,把每个象素填入颜色,左至右越来越红,上至下越来越绿。

 

左邊的canvas定義如下:

<canvas width="256" height="256" id="testCanvas"></canvas>

修改代码试试看

  • 把第三个pixels[i++] = 0 改为255 (即蓝色全开)
  • 把第四个pixels[i++] = 255 改为128 (alpha=128)
  • 可以只修改两个for循环里面的代码,画一个国际象棋棋盘么?

这实验说明,从canvas取得的影像资料canvas.getImageData(...).data是个一维数组,该数组每四个元素代表一个象素(按红, 绿, 蓝, alpha排列),这些象素在影像中从上至下、左至右排列。

解决实验平台的技术问题后,可开始从基础类别开始实现。

基础类

笔者使用基于物件(object-based)的方式编写JavaScript。

三维向量

三维向量(3D vector)可谓CG里最常用型别了。这里三维向量用Vector3类实现,用(x, y, z)表示。 Vector3亦用来表示空间中的点(point),而不另建类。先看代码:

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类对象,都会传回一个新建构的Vector3。这些三维向量的功能很简单,不在此详述。注意multiply和divide是与纯量(scalar)相乘和相除。

Vector3.zero用作常量,避免每次重新构建。值得一提,这些常量必需在prototype设定之后才能定义。

光线

所谓光线(ray),从一点向某方向发射也。数学上可用参数函数(parametric function)表示:

\mathbf{r}(t) = \mathbf{o} + t\mathbf{d}, t \geq 0

当中,o即发谢起点(origin),d为方向。在本文的例子里,都假设d为单位向量(unit vector),因此t为距离。实现如下:

Ray3 = function(origin, direction) { this.origin = origin; this.direction = direction; }

Ray3.prototype = {
    getPoint : function(t) { return this.origin.add(this.direction.multiply(t)); }
};

球体

球体(sphere)是其中一个最简单的立体几何图形。这里只考虑球体的表面(surface),中心点为c、半径为r的球体表面可用等式(equation)表示:

\left \| \mathbf{x} - \mathbf{c} \right \| = r

如前文所述,需要计算光线和球体的最近交点。只要把光线x = r(t)代入球体等式,把该等式求解就是交点。为简化方程,设v=o - c,则:

\begin{align*} \left\| \mathbf{v} +t\mathbf{d} \right\| &= r \\ \left\| \mathbf{v} +t\mathbf{d} \right\|^2 &= r^2 \\ (\mathbf{v}+t\mathbf{d}) \cdot (\mathbf{v}+t\mathbf{d}) - r^2 &= 0 \\ \mathbf{v}^2 + 2t(\mathbf{d} \cdot \mathbf{v}) +t^2 \mathbf{d}^2 - r^2 &= 0 \\ (\mathbf{d}^2)t^2 + (2\mathbf{d} \cdot \mathbf{v})t + (\mathbf{v}^2 - r^2) &= 0 \end{align*}

因为d为单位向量,所以二次方的系数可以消去。 t的二次方程式的解为

\begin{align*} t&=\frac{-2\mathbf{d} \cdot \mathbf{v}\pm \sqrt{(2\mathbf{d} \cdot \mathbf{v})^2 - 4(\mathbf{v}^ 2 - r^2)} }{2} \\ &= -\mathbf{d} \cdot \mathbf{v}\pm \sqrt{(\mathbf{d} \cdot \mathbf{v})^2 - (\mathbf {v}^2 - r^2)} \end{align*}

若根号内为负数,即相交不发生。另外,由于这里只需要取最近的交点,因此正负号只需取负号。代码实现如下:

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;
    }
};

实现代码时,尽快用最少的运算剔除没相交的情况(Math.sqrt是比较慢的函数)。另外,预计算了球体半径r的平方,此为一个优化。

这里用到一个IntersectResult类,这个类只用来记录交点的几何物件(geometry)、距离(distance)、位置(position)和法向量(normal)。 IntersectResult.noHit的geometry为null,代表光线没有和任何几何物件相交。

IntersectResult = function() {
    this.geometry = null;
    this.distance = 0;
    this.position = Vector3.zero;
    this.normal = Vector3.zero;
};

IntersectResult.noHit = new IntersectResult();

摄影机

摄影机在光线追踪系统里,负责把影像的取样位置,生成一束光线。

由于影像的大小是可变的(多少像素宽x多少像素高),为方便计算,这里设定一个统一的取样座标(sx, sy),以左下角为(0,0),右上角为(1 ,1)。

从数学角度来说,摄影机透过投影(projection),把三维空间投射到二维空间上。常见的投影有正投影(orthographic projection)、透视投影(perspective projection)等等。这里首先实现透视投影。 ]]>

透视摄影机

透视摄影机比较像肉眼和真实摄影机的原理,能表现远小近大的观察方式。透视投影从视点(view point/eye position),向某个方向观察场景,观察的角度范围称为视野(field of view, FOV)。除了定义观察的向前(forward)是那个方向,还需要定义在影像平面中,何谓上下和左右。为简单起见,暂时不考虑宽高不同的影像,FOV同时代表水平和垂直方向的视野角度。

上图显示,从摄影机上方显示的几个参数。 forward和right分别是向前和向右的单位向量。

因为视点是固定的,光线的起点不变。要生成光线,只须用取样座标(sx, sy)计算其方向d。留意FOV和s的关系为:

\tan \frac{FOV}{2} = s

把sx从[0, 1]映射到[-1,1],就可以用right向量和s,来计算r向量,代码如下:

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为度数,转为弧度才能使用Math.tan()。另外,fovScale预先乘了2,因为sx映射到[-1,1]每次都要乘以2。 sy和sx的做法一样,把两个在影像平面的向量,加上forward向量,就成为光线方向d。因之后的计算需要,最后把d变成单位向量。

渲染测试

写了Vector3、Ray3、Sphere、IntersectResult、Camera五个类之后,终于可以开始渲染一点东西出来!

基本的做法是遍历影像的取样座标(sx, sy),用Camera把(sx, sy)转为Ray3,和场景(例如Sphere)计算最近交点,把该交点的属性转为颜色,写入影像的相对位置里。

把不同的属性渲染出来,是CG编程里经常用的测试和调试手法。笔者也是用此方法,修正了一些错误。

渲染深度

深度(depth)就是从IntersectResult取得最近相交点的距离,因深度的范围是从零至无限,为了把它显示出来,可以把它的一个区间映射到灰阶。这里用[0, maxDepth]映射至[255, 0],即深度0的像素为白色,深度达maxDepth的像素为黑色。

// renderDepth.htm
function 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);
}

 

这里的观看方向是,正X轴向右,正Y轴向上,正Z轴向后。

修改代码试试看

  • 改变球体的位置
  • 改变球体的半径
  • 改变fov(PerspectiveCamera最后的参数)
  • 改变maxDepth(renderDepth最后的参数)
  • 改变摄影机的方向,例如向左转一点点(记得要是单位向量啊!可以用new Vector(...).normalize())

渲染法向量

相交测试也计算了几何物件在相交位置的法向量,这里也可把它视觉化。法向量是一个单位向量,其每个元素的范围是[-1, 1]。把单位向量映射到颜色的常用方法为,把(x, y, z)映射至(r, g, b),范围从[-1, 1]映射至[0, 255]。

// 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;
            }
    // ...
}

 

球体上方的法向量是接近(0, 1, 0),所以是浅绿色(0.5, 1, 0.5)。

修改代码试试看

  • 从球体的正上方往下看

材质

渲染深度和法向量只为测试和调试,要显示物件的"真实"颜色,需要定义该交点向某方向(如往视点的方向)发出的光的颜色,称之为几个图形的材质(material )。

材质的接口为function sample(ray, posiiton, normal) ,传回颜色Color的对象。这是个极简陋的接口,临时做一些效果出来,有机会再详谈。

颜色

颜色在CG里最简单是用红、绿、蓝三个通道(color channel)。为实现简单的Phong材质,还加入了对颜色的简单操作。

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类,值得留意的是,颜色有调制(modulate)操作,其意义为两个颜色中每个颜色通道相乘。

格子材质

CG世界里,国际象棋棋盘是最常见的测试用纹理(texture)。这里不考虑纹理贴图(texture mapping)的问题,只凭(x, z)坐标计算某位置发出黑色或白色的光(黑色的光不叫光吧,哈哈)。

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坐标单位有多少个格子,例如scale=0.1即一个格子的大小为10x10。

Phong材质

这里实现简单的Phong材质,因为未有光源系统,只用全域变量设置一个临时的光源方向,并只计算漫射(diffuse)和镜射(specular)。

PhongMaterial = function(diffuse, specular, shininess, reflectiveness) {
    this.diffuse = diffuse;
    this.specular = specular;
    this.shininess = shininess;
    this.reflectiveness = reflectiveness;
};

// global temp
var 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的内容不在此述。

渲染材质

修改之前的渲染代码,当碰到相交时,就向几何对象取得material属性,并调用sample方法函数取得颜色。

// rayTrace.htm
function 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;
            }
    // ...
}

 

修改代码试试看

  • 改变fov,有了格子地板效果应该很明显
  • 改变CheckerMaterial的scale
  • 把原来红色的球改为绿色
  • 把原来红色的球改为黄色
  • 改变shininess(PhongMaterial最后一个参数)

多个几何物件

只渲染一个几何物件太乏味,这节再加入一个无限平面,和介绍如何组合多个几何物件。

平面

一个(无限)平面(Plane)在数学上可用等式定义:

\mathbf{n} \cdot \mathbf{x} = d

n为平面的法向量,d为空间原点至平面的最短距离。光线和平面的相交计算很简单,这里不详述了。

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;
    }
};

并集

把多个几何物件结合起来,可以使用集(set)的概念。这里最容易实现的操作,就是并集(union),即光线要找到一组几个图形的最近交点。无需改其他代码,只加入一个Union类就可以:

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)的特性,完全不用修改原来的代码,就可以扩展功能。

如前所述,这里只考虑几何几何图形的表面。如果考虑几何图形是实心的,就可以用构造实体几何(constructive solid geometry, CSG)方法,提供并集、交集、补集等操作。容后再谈。

反射

以上实现的,也只是局部照明。只要再加入一点点代码,就可以实现反射。

下图说明反射向量的计算方法:

把d投射到n上(因n是单位向量,只需要点乘即可),就可以计算d在n上的长度,把d减去这长度两倍的法向量,就是反射向量r。数学上可写成:

\mathbf{r} = \mathbf{d} - 2(\mathbf{d \cdot n})\bf{n}"

一般材质并非完全反射(镜子除外),因此这里为材质加上一个反射度(reflectiveness)的属性。反射的功能很简单,只要在碰到反射度非零的材质,就继续向反射方向追踪,并把结果按反射度来混合。例如一个材质的反射度为25%,则它传回的颜色是75%本身颜色,加上25%反射传回来的颜色。

另外,不断反射会做成大量的运算,甚至乎永远不能停止(考虑摄影机在两个镜子中间)。因此要限制反射的次数。含反射功能的光线追踪代码如下:

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);
}

 

修改代码试试看

  • 改变一个球的reflectiveness,试试0、1及之间的数值
  • 改变maxReflect(rayTraceReflection最后一个参数)
  • 加入更多的球体(可用for循环啊……不过小心渲染时间太长)

结语

能体会到计算机图形学的有趣之处么?百多行简单的JavaScript代码,就绘画出像真的影像,那种满足感实非笔墨所能形容。

本文实现了一个简单的光线追踪渲染器,支持球体、平面、Phong材质、格子材质、多重反射等功能。读者可以下载这组代码,加入不同的扩展,也可以尝试翻译做熟悉的编程语言。很多光线追踪用到的计算机图形技术,也可以应用到实时图形编程里,例如光源和材质的计算,基本上可以简易翻译做实时图形的著色器(shader)编程。

游戏里采用光栅化渲染技术已有二十年以上,这几年的硬件发展,使其他渲染方法也能用于实时应用。光线追踪和其他类似的方法,有个当今重要优点,就是能高度平行化。采样之间并没有依赖性,例如256x256=65536个采样,理论上,可使用65536个机器/核心独立执行追踪,那么完成时间只是最慢的一个取样所需的时间。

笔者希望继续撰写这系列,例如包括以下内容:

  • 其他几何图形(长方体、柱体、三角形、曲面、高度场、等值面、……)
  • 光源(方向光源、点光源、聚光灯、阴影、ambient occlusion)
  • 材质(Phong-Blinn、Oren-Nayar、Torrance-Sparrow、折射、 Fresnel、BRDF、BSDF……)
  • 纹理(纹理座标、采样、Perlin noise)
  • 摄影机模型(正投射、全景、景深)
  • 成像流程(渐进渲染、反锯齿、后期处理)
  • 优化方法(场景剖分、低阶优化)
  • 其他全局光照渲染方法

祈望得到大家的意见反馈。

参考

更新

  • 2010年3月31日,网友HouSisong把本文代码以C++实现,并完全保留了原设计,代码可於他的博文下载。
Add your comment

88 条回复

  1. #1楼 pboyin      2010-03-29 00:11
    牛叉
     回复 引用 查看   
  2. #2楼 浪雪      2010-03-29 00:20
    很不错啊……
     回复 引用 查看   
  3. #3楼 月光鸟      2010-03-29 08:31
    好棒的文章,拜读了。
     回复 引用 查看   
  4. #4楼 assiwe      2010-03-29 08:33
    这么简单的图像在chrome上渲染居然要2秒.ff上要4秒.....JavaScript的效率比c慢了多少啊?

     回复 引用 查看   
  5. #5楼 麒麟      2010-03-29 08:48
     回复 引用 查看   
  6. #6楼 atyuwen      2010-03-29 08:49
    写得真详细,等着你讲BSDF
     回复 引用 查看   
  7. #7楼 Justin      2010-03-29 08:55
    牛叉!
     回复 引用 查看   
  8. #8楼 Wuya      2010-03-29 08:55
    这么强力的文章,不得不顶~~
     回复 引用 查看   
  9. #9楼[楼主] Milo Yip      2010-03-29 09:02
    @assiwe
    引用assiwe:
    这么简单的图像在chrome上渲染居然要2秒.ff上要4秒.....JavaScript的效率比c慢了多少啊?

    除了本身語言及腳本引擎的差異外,為了容易閱讀,作教學用途,本文的例子並沒進行較低階的優化。例如,可把inner-loop裡大部份的new除去(現時每個Vector3.add()等函數都用new傳回新對象),也可以把很多函數做inline expansion。

    我對JavaScript的效能和優化也沒經驗,請不吝賜教。

    有興趣的朋友,也可移植代碼到其他語言比較效能,或許也是不錯的學習方法。
     回复 引用 查看   
  10. #10楼 悟空空      2010-03-29 09:02
    哈哈好玩儿!
    占个位置学习
     回复 引用 查看   
  11. #11楼 lazylu      2010-03-29 09:26
    很精彩,超级精彩!
     回复 引用 查看   
  12. #12楼 arrow_air      2010-03-29 09:26
    厉害啊~
     回复 引用 查看   
  13. #13楼 assiwe      2010-03-29 09:38
    @Milo Yip
    引用Milo Yip:
    @assiwe
    引用assiwe:
    这么简单的图像在chrome上渲染居然要2秒.ff上要4秒.....JavaScript的效率比c慢了多少啊?

    除了本身語言及腳本引擎的差異外,為了容易閱讀,作教學用途,本文的例子並沒進行較低階的優化。例如,可把inner-loop裡大部份的new除去(現時每個Vector3.add()等函數都用new傳回新對象),也可以把很多函數做inline expansion。

    我對JavaScript的效能和優化也沒經驗,請不吝賜教。

    有興趣的朋友,也可移植代碼到其他語言比較效能,或許也是不錯的學習方法。

    我也不太懂JavaScript,只不过看了您的标题,以为可以在网页游戏上作出3D效果.结果对JavaScript的效率小小的失望了一下
     回复 引用 查看   
  14. #14楼 吉日嘎拉 不仅权限设计      2010-03-29 09:55
    真正的技术文章,图文并茂,推荐+1。
     回复 引用 查看   
  15. #15楼 yyww      2010-03-29 10:28
    引用assiwe:
    @Milo Yip
    引用Milo Yip:
    @assiwe
    引用assiwe:
    这么简单的图像在chrome上渲染居然要2秒.ff上要4秒.....JavaScript的效率比c慢了多少啊?

    除了本身語言及腳本引擎的差異外,為了容易閱讀,作教學用途,本文的例子並沒進行較低階的優化。例如,可把inner-loop裡大部份的new除去(現時每個Vector3.add()等函數都用new傳回新對象),也可以把很多函數做inline expansion。

    我對JavaScript的效能和優化也沒經驗,請不吝賜教。

    有興趣的朋友,也可移植代碼到其他語言比較效能,或許也是不錯的學習方法。

    我也不太懂JavaScript,只不过看了您的标题,以为可以在网页游戏上作出3D效果.结果对JavaScript的效率小小的失望了一下


    Javascript是脚本语言,效率比C之类的慢2-3个数量级,不适合搞图形方面的东西
     回复 引用 查看   
  16. #16楼 Coki      2010-03-29 10:35
    相当强悍!
    回家之后就尝试把LZ的文章搬到WPF上试试去,效果不效果的无所谓,算法和原理才是重头戏~
     回复 引用 查看   
  17. #17楼 pease      2010-03-29 10:57
    正在学图形方面的知识,您的这篇文章对我太有帮助了,谢谢!
     回复 引用 查看   
  18. #18楼 钱小柜      2010-03-29 11:17
    阁下图形学功底真好,学习了!
     回复 引用 查看   
  19. #19楼 ※潇洒※      2010-03-29 11:51
    强帖...膜拜了....
     回复 引用 查看   
  20. #20楼 New      2010-03-29 12:16
    从LZ这篇文章的第一句话,就开始猜测LZ非常的强大。。
     回复 引用 查看   
  21. #21楼 菩提树下的杨过      2010-03-29 12:20
    楼主About里的“美食从天而降”,就是那部天上 下汉堡雨的NB电影吗?
     回复 引用 查看   
  22. #22楼 杰梅因      2010-03-29 13:06
    牛B 呀,我看的混掉,收藏
     回复 引用 查看   
  23. #23楼 Rindy      2010-03-29 13:10
    噢噢噢噢哦,难道阁下就是,从0bug知道你,大师级人物
     回复 引用 查看   
  24. #24楼 黄明      2010-03-29 13:25
    好厉害!大师级别人物呢。 强烈支持。
     回复 引用 查看   
  25. #25楼 卡索      2010-03-29 13:26
    呵呵!非常不错啊!javascript语言在浏览器这个宿主环境中需要实现楼主的这个功能,效率确实是个问题...
     回复 引用 查看   
  26. #26楼 Iron      2010-03-29 13:52
    厉害~支持作者写下去~
     回复 引用 查看   
  27. #27楼 尉迟方      2010-03-29 15:21
    不错,很有意思

    试了象棋棋格(第一个例子最后一个题目),果然如此 :)

    var temp = 0;
    if ((Math.floor(x*8/w) + Math.floor(y*8/h)) % 2)
    temp = 255;
    pixels[i++] = temp;
    pixels[i++] = temp;
    pixels[i++] = temp;
    pixels[i++] = 255;
     回复 引用 查看   
  28. #28楼 devil0153      2010-03-29 15:27
    牛A<LZ<牛C
     回复 引用 查看   
  29. #29楼 hoodlum1980      2010-03-29 17:26
    一拉滚动条,这么长的文章在喧嚣浮躁的博客园甚是少见,写出来想必得花不少力气。原来是香港同胞的文章。
     回复 引用 查看   
  30. #30楼 monkey-猴子      2010-03-29 17:38
    为什么我运行不了楼主的例子,我的浏览器是IE 7.0
     回复 引用 查看   
  31. #31楼 王洪剑      2010-03-29 17:46
    拜服!
     回复 引用 查看   
  32. #32楼 Fionn      2010-03-29 18:02
    能用JavaScript来干这个!不是一般的牛X
     回复 引用 查看   
  33. #33楼 麦舒      2010-03-29 19:27
    很牛!佩服!
     回复 引用 查看   
  34. #34楼 GWPBrian      2010-03-29 20:02
    Nice
    可惜没怎么研究过这方面
     回复 引用 查看   
  35. #35楼[楼主] Milo Yip      2010-03-29 20:05
    @菩提树下的杨过
    引用菩提树下的杨过:楼主About里的“美食从天而降”,就是那部天上 下汉堡雨的NB电影吗?

    應該是你所指的吧。
     回复 引用 查看   
  36. #36楼[楼主] Milo Yip      2010-03-29 20:08
    @hoodlum1980
    引用hoodlum1980:一拉滚动条,这么长的文章在喧嚣浮躁的博客园甚是少见,写出来想必得花不少力气。原来是香港同胞的文章。

    寫這程序和文章各花了一整天時間呢,可能是我花最多時間的博文。希望大家多給意見,改善不足。
     回复 引用 查看   
  37. #37楼[楼主] Milo Yip      2010-03-29 20:10
    @尉迟方
    引用尉迟方:
    不错,很有意思

    试了象棋棋格(第一个例子最后一个题目),果然如此 :)

    var temp = 0;
    if ((Math.floor(x*8/w) + Math.floor(y*8/h)) % 2)
    temp = 255;
    pixels[i++] = temp;
    pixels[i++] = temp;
    pixels[i++] = temp;
    pixels[i++] = 255;

    看到網友真的去嘗試,使用測試功能,心感滿足。
     回复 引用 查看   
  38. #38楼[楼主] Milo Yip      2010-03-29 20:11
    @monkey-猴子
    引用monkey-猴子:为什么我运行不了楼主的例子,我的浏览器是IE 7.0

    文中提及了,IE直至版本8也不支持HTML5的Canvas tag。我自己想想辦法,也請有解決方案的朋友指導。
     回复 引用 查看   
  39. #39楼 麒麟      2010-03-29 20:34
    楼主收不收徒弟呀。
     回复 引用 查看   
  40. #40楼 烙馅饼喽      2010-03-29 21:23
    那个象棋格子没把我给试吐血,总算是弄出来了
    var chesex= parseInt(x/32);
    var chesey=parseInt(y/32);
    var wb;

    if( chesex %2 == chesey %2 ){
    wb=0;
    }
    else
    {
    wb=1;
    }

    pixels[i++] =wb * 255;
    pixels[i++] = wb * 255;
    pixels[i++] =wb * 255;
    pixels[i++] = 255;
     回复 引用 查看   
  41. #41楼 voop      2010-03-29 21:25
    LZ不仅写出这么NB的文章,更重要的是认真回复了每一条留言,冲这点不顶不行啊
     回复 引用 查看   
  42. #42楼 啊不才      2010-03-29 22:36
    见到传说中的牛人了
     回复 引用 查看   
  43. #43楼 DiryBoy      2010-03-29 23:48
    膜拜啊!
     回复 引用 查看   
  44. #44楼 刘国忠      2010-03-30 08:41
    牛X,顶一下LZ。
     回复 引用 查看   
  45. #45楼 蚂蚁跳楼      2010-03-30 09:19
    文章写得很仔细。学习了
     回复 引用 查看   
  46. #46楼 liy      2010-03-30 09:23
    我的计算机图形学完全白学了
     回复 引用 查看   
  47. #47楼 一线风      2010-03-30 09:25
    纯支持,NB的人呀~
     回复 引用 查看   
  48. #48楼 李彬      2010-03-30 10:19
    用了不该用的东西,但是我们不否认这种专研精神。
     回复 引用 查看   
  49. #49楼 Mingle      2010-03-30 11:03
    太专业了!牛!顺便问下楼主:数学公式部分你在计算机上编写使用什么软件或编辑器?比如数学中开方,立方的等。
     回复 引用 查看   
  50. #50楼[楼主] Milo Yip      2010-03-30 12:27
    @李彬
    引用李彬:用了不该用的东西,但是我们不否认这种专研精神。

    本系列旨在教計算機圖形學,並非實際用在產品上的。
     回复 引用 查看   
  51. #51楼[楼主] Milo Yip      2010-03-30 12:33
    @Mingle
    引用Mingle:太专业了!牛!顺便问下楼主:数学公式部分你在计算机上编写使用什么软件或编辑器?比如数学中开方,立方的等。


    我是用這個 Online LaTeX Equation Editor

    編輯完,下面選HTML,就可以複製HTML代碼。很簡單的
     回复 引用 查看   
  52. #52楼 Tonny Lau      2010-04-01 10:23
    很棒的文章--图文并茂。楼主钻研精神可嘉可敬~~威!武!
     回复 引用 查看   
  53. #53楼 Jimixu      2010-04-01 12:40
    啊,看评论才发现时Milo哥的作品,膜拜ing
     回复 引用 查看   
  54. #54楼 装配脑袋      2010-04-01 19:14
    准备用GPU通用计算实现一个LZ的光线追踪并行计算版试试看
     回复 引用 查看   
  55. #55楼 yen1985      2010-04-02 12:25
    把复杂的概念简单化,继续关注Milo大哥的文章~
     回复 引用 查看   
  56. #56楼 luckuny      2010-04-04 01:31
    拜读了,
     回复 引用 查看   
  57. #57楼 装配脑袋      2010-04-04 22:09
    今天费了一晚上力气,总算把第一个例子(画那个渐变色的方块)移植成GPU并行程序了。哎。。这玩意主要瓶颈还是在艰难晦涩的编程模型上啊~~~
     回复 引用 查看   
  58. #58楼[楼主] Milo Yip      2010-04-05 06:24
    @装配脑袋
    引用装配脑袋:今天费了一晚上力气,总算把第一个例子(画那个渐变色的方块)移植成GPU并行程序了。哎。。这玩意主要瓶颈还是在艰难晦涩的编程模型上啊~~~

    努力啊。
     回复 引用 查看   
  59. #59楼[楼主] Milo Yip      2010-04-05 06:29
    @烙馅饼喽
    忘了回覆,你的答案可以的。不過做了兩次取餘,其實一次就可以了。
     回复 引用 查看   
  60. #60楼 装配脑袋      2010-04-05 13:55
    HLSL不支持指针和多态真是麻烦啊,处理形状和材质那里无法使用这么简化的语法。幸好Shader Mode 5.0支持了interface和class,但还是有诸多限制,步履维艰~
     回复 引用 查看   
  61. #61楼 ChrisPei      2010-04-05 18:18
    谢谢博主给我们带来如此精彩的文章,收获很大
    将持续关注,期待博主其他文章。
     回复 引用 查看   
  62. #62楼 装配脑袋      2010-04-05 20:15
    写了一天总算写到这一步了~~~ 真是不容易啊
     回复 引用 查看   
  63. #63楼[楼主] Milo Yip      2010-04-06 12:48
    @装配脑袋
    能支持較複雜的場境數據麼?
     回复 引用 查看   
  64. #64楼 装配脑袋      2010-04-06 18:54
    引用Milo Yip:
    @装配脑袋
    能支持較複雜的場境數據麼?


    现在主要就是几个难点,一个是没有语言上多态性的支持,因此支持多个形状的时候没法用特别简单一致的方式来处理形状联合。另外处理材质的代码基本上就是靠if分支了,想必不是特别适合大场景。最后也是最难的一点,没有栈,所以不能递归。我必须把那个反射算法改写成非递归的算法。

    我也是刚刚入手GPU算法,这次争取能够实现出来,也算学到了不少东西。至于是不是比CPU快很多,那得等我实现之后才能知道了,呵呵。
     回复 引用 查看   
  65. #65楼 Dreampuf      2010-04-08 16:47
    var n = (((x / 32)|0)%2 == 0)
    ? (((y / 32)|0)%2 == 1)
    ? 0
    : 255
    : (((y / 32)|0)%2 == 1)
    ? 255
    : 0
    ;
    pixels[i++] = n;
    pixels[i++] = n;
    pixels[i++] = n;
    pixels[i++] = 255;
    ===============
    国际象棋那.
    请继续这个系列.谢谢.
     回复 引用 查看   
  66. #66楼 装配脑袋      2010-04-11 22:57
    哈哈,虽然费了很大的力气,但最后还是成功了。看看效果还不错吧?
    真正完全在显卡上计算的光线追踪,性能还是相当不错的哦。不过我的程序列还是有一些多余的判断,也许性能还有不少优化空间。不过我还是想和那个C++版本的比较一下速度看看。准备这两天实验。

     回复 引用 查看   
  67. #67楼 装配脑袋      2010-04-18 00:14
    今天稍微测试了一下,渲染2048x2048尺寸,5次反射;C++版(运行在Core i7 920)用时1639毫秒;DirectCompute版(运行在Ati 5850)用时230毫秒。不过CPU版只用了也单线程,所以只快出7倍貌似不是很好的成绩。也许场景复杂一些,计算量再大一些的话,GPU成绩会更有优势。
     回复 引用 查看   
  68. #68楼 SteveF      2010-04-19 09:25
    在学java 顺便就用java实现了一下 碰到了一点小问题 不过还是解决了

    "正X轴向右,正Y轴向上,正Z轴向后" 这里正x轴应该向左 可以试着把7.1中的 camera front改成就 new Vector3(0.5, 0, -1).normalize() 就发现问题了

    在Phong材质上sample获取的颜色回超出[0, 1]的范围 在firefox 3.6以前的版本上跑的时候会出现诡异的高光 新版本的firefox应该加入自动rounding功能了 chrome和safari没有问题

     回复 引用 查看   
  69. #69楼 RamondLee      2010-04-23 23:59
    为什么颜色那里,用255代替1,然后将代码的其他地方也做相应改动后颜色就全变了?楼主能不能推荐一篇文章怎么调配的颜色?我用C#实现了一个,颜色总是不能象例子中那么靓
     回复 引用 查看   
  70. #70楼 harrywy      2010-04-25 17:35
    嗯,我用C#实现了一下,觉得相当厉害,但是我不知道如果要加入其它几何物体该怎么改...
    我试着写了一个cube但是败了,不知博主能不能指导一下~
     回复 引用 查看   
  71. #71楼[楼主] Milo Yip      2010-04-25 17:44
    @RamondLee
    這麼說我也很難替你解決問題。或許你可以用網頁JS調試器,和C#版本同步追蹤,看看錯在那兒。
     回复 引用 查看   
  72. #72楼[楼主] Milo Yip      2010-04-25 17:46
    @harrywy
    要實現其他幾個物體和光綫的相交,可參考這個表:

    http://realtimerendering.com/intersections.html

    有時間我也會談這方面的。但下一個題目預計會先談材質。
     回复 引用 查看   
  73. #73楼[楼主] Milo Yip      2010-04-25 17:47
    @装配脑袋
    希望你有空也能寫篇博文,讓我學習一下。
     回复 引用 查看   
  74. #74楼[楼主] Milo Yip      2010-04-25 17:54
    @SteveF
    沒太明白你預到的問題。問題解決了麼?
     回复 引用 查看   
  75. #75楼 harrywy      2010-04-25 18:29
    @Milo Yip
    Many many thanks!

    哇,还有材质~太好了
    膜拜~
     回复 引用 查看   
  76. #76楼 harrywy      2010-04-25 19:16
    @Milo Yip
    引用Milo Yip:
    @harrywy
    要實現其他幾個物體和光綫的相交,可參考這個表:

    http://realtimerendering.com/intersections.html

    有時間我也會談這方面的。但下一個題目預計會先談材質。


    可惜代码打不开...
     回复 引用 查看   
  77. #77楼 SteveF      2010-04-27 02:31
    @Milo Yip
    解决了 :)
    其实就是发现了文章里的两个小问题
    1. 坐标系应该是正x轴向左 正y轴向上 正z轴向后 (可以用右手法则验证一下)
    2. Phong材质sample()函数最后一行代码lightColor.modulate(diffuseTerm.add(specularTerm))会导致颜色值溢出 超出[0,1]的范围
     回复 引用 查看   
  78. #78楼 唐老鸭      2010-06-01 11:51
    我想死,,,你把Javascript推向了另一个高潮!!!
     回复 引用 查看   
  79. #79楼 noremorse      2010-07-03 20:47
    Ray Trace没啥稀奇的,Javascript也没啥稀奇的但是把Ray Trace和Javascript结合起来写成这么一篇可以交互可以运行的深入浅出的博客真是太棒了!真是爱不释手啊。
     回复 引用 查看   
  80. #80楼 Joyer Huang      2010-07-18 12:51
    好文章,好文章。。决定用心开始学习pbrt了。

    区区编写了一个emacs的版本,对lisp感兴趣的朋友可以看一下~:
    http://blog.csdn.net/DelphiNew/archive/2010/07/18/5744145.aspx
     回复 引用 查看   
  81. #81楼 luoting      2010-07-19 17:35
    拜读了!楼主太无私了!
     回复 引用 查看   
  82. #82楼 SatanULtra      2010-09-13 10:43
    lz厉害!真想认识你!
     回复 引用 查看   
  83. #83楼 myzingy      2011-01-11 11:47
    鉴于作者的强大,我只能留位学习,感谢中。。。
     回复 引用 查看   
  84. #84楼 wingc      2011-07-07 10:50
    读到此文实在晚了,但迟到总比不到好,也凑个热闹吧。先把那个画国际象棋棋盘的贴上:
            pixels[i] = ((parseInt(x * 8 / w) + parseInt(y * 8 / h)) % 2) * 255;
            pixels[i + 1] = pixels[i];
            pixels[i + 2] = pixels[i];
            pixels[i + 3] = 255;
            i += 4;
    
    

    其他朋友在玩port,我也来参与吧,呵呵,打算port到C#用XNA上实现一遍去,至少Vector3/Intersect/Union这些有现成的吧。
     回复 引用 查看   
  85. #85楼 网易小前      2011-10-10 11:16
    写得非常好,可以很朋理论没有看懂,呵
     回复 引用 查看   
  86. #86楼 relucent      2011-11-02 10:42
    非常好的博文,内容循序渐进。学习。
     回复 引用 查看   
  87. #87楼 【当耐特砖家】      2011-11-08 21:36
    已选入HTML5实验室目录,多谢分享!http://www.cnblogs.com/iamzhanglei/archive/2011/11/06/2237870.html
     回复 引用 查看   
  88. #88楼 Hwa      2012-01-07 22:50
    Good article. But I think it is better to add some EPSILON value to the ray in rayTraceReflection.
     回复 引用 查看