代码改变世界

码农干货系列【9】--javascript光线追踪基础

2013-03-12 16:31 【当耐特】 阅读(...) 评论(...) 编辑 收藏

简介

光线追踪(ray tracing)(也叫raytracing或者光束投射法)是一个在二维(2D)屏幕上呈现三维(3D)图像的方法。为了尝试光线追踪算法,并且尽可能得保证javascript代码精炼,我做了一些尝试。

射线与球体相交检测

最开始尝试了射线与球体的相交检测(不计算交点),只判断相交还是未相交。代码如下所示:

var Vector3 = function (x, y, z) { this.x = x; this.y = y; this.z = z; };
Vector3.prototype ={
    dot: function (v) { return this.x * v.x + this.y * v.y + this.z * v.z; },
    sub: function (v) { return new Vector3(this.x - v.x, this.y - v.y, this.z - v.z); },
    normalize: function () {      return this.divideScalar(this.length());  },
    divideScalar: function (s) {  return new Vector3(this.x/s, this.y/s, this.z/s);   },
    length: function () {     return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);  },
    sqrDistanceToline:function(a,b){
        var ab = b.sub(a), ac = this.sub(a), bc = this.sub(b);
        var e = ac.dot(ab.normalize());
        var f = ac.length();
        return f * f - e * e;
    }
} 

其中sqrDistanceToline为计算点到直线之间的距离的平方(开跟号性能损耗大)。使用如下:

ball.p.sqrDistanceToline(v, camera.p) < sqrBallR

其中v是屏幕上的点的坐标,camera.p为视点的坐标,ball.p为球体中心坐标,sqrBallR为球体半径的平方。当上面返回true时,判定为相交;反之亦然。

相交测试

for (var y = 0; y < canvas.height; y++) {
    for (var x = 0; x < canvas.width; x++) {
        var v = new Vector3(-canvas.width / 2 + x, canvas.height - y, 0);
        var cv = new Vector3(camera.p.y * v.x / (camera.p.y - v.y), 0, camera.p.z * v.y / (v.y - camera.p.y));
           
        if (cv.z > -planeLength && cv.z < 0) {
            if (ball.p.sqrDistanceToline(v, camera.p) < sqrBallR) {
                pixels[i] = pixels[i + 1] = pixels[i + 2] = 111;
            } else {
                pixels[i] = pixels[i + 1] = pixels[i + 2] = (Math.ceil(cv.x / sideLength) + Math.ceil(cv.z / sideLength)) % 2 === 0 ? 148 : 0;
            }
            pixels[i + 3] = 255 * (planeLength - Math.abs(cv.z)) / planeLength;
        }
        i += 4;
    }
}

由于没有获取交点坐标,无法计算视点到球体上点的距离,所以无法进行球体深度渲染。所以得到了以下的图像:

image

获取交点

所以现在目的很明确,不仅要判定相交不相交,还需要找到交点的坐标。当然,上面的方法不是一无是处,可以进行一些初步坐标的筛选(如果性能好于找交点计算一个数量级的话,这个待测试)。那么怎么获取射线与球体的交点呢?该点要满足以下两个条件:

1.交点在在光线上
x=S+dt

2.交点在球上
|x-C|=r

C 表示球心,r 表示半径,光线起点是 S,方向是 d(单位向量),交点 x。

所以得到:8140727a73c4b1e91b3ec987e4d780e0

简化 dfd1908c7d8603bab746c9e5a487fb06

那么a27ce21daa6b36c14bc25f8646d44e79

所以a44286afce8014729e85436a846749fd

最后得到:039de90713792a92c20398f4a059962a

所有小球的代码如下所示:

var Ball = function (p, r) {
    this.p = p;
    this.r = r;
    this.sqrR = this.r * this.r;
}
Ball.prototype = {
    intersect: function (p1, p2) {
        var v = p1.sub(this.p);
        var a0 = v.sqrLength() - this.sqrR;
        var np = p2.sub(p1).normalize();
        var dotV = np.dot(v);
        if (dotV <= 0) {
            var discr = dotV * dotV - a0;
            if (discr >= 0) {
                return p1.add(np.multiplyScalar(-dotV - Math.sqrt(discr)));
            }
        }
        return null;
    }
}

拿到了交点坐标,现在可以做深度渲染:

var result = ball.intersect(camera.p, screenP);
if (result) {
    (pixels[i] = pixels[i + 1] = pixels[i + 2] = ((result.z - ball.p.z) / ball.r) * 255)
    pixels[i + 3] = 255;
}

在线演示

请使用现代浏览器,你的浏览器过时了!

修改里面的参数试一试!

参考文献

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

光线跟踪 - 维基百科,自由的百科全书

 

Have Fun!