简介
光线追踪(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;
}
}
由于没有获取交点坐标,无法计算视点到球体上点的距离,所以无法进行球体深度渲染。所以得到了以下的图像:
获取交点
所以现在目的很明确,不仅要判定相交不相交,还需要找到交点的坐标。当然,上面的方法不是一无是处,可以进行一些初步坐标的筛选(如果性能好于找交点计算一个数量级的话,这个待测试)。那么怎么获取射线与球体的交点呢?该点要满足以下两个条件:
1.交点在在光线上
x=S+dt2.交点在球上
|x-C|=r
C 表示球心,r 表示半径,光线起点是 S,方向是 d(单位向量),交点 x。
所有小球的代码如下所示:
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;
}
在线演示
请使用现代浏览器,你的浏览器过时了! 修改里面的参数试一试!
var ball = new Ball(new Vector3(10, 150, -200), 100); var camera = { p: new Vector3(0, 200, 200) }; var canvas = document.getElementById('myCanvas'); var ctx = canvas.getContext('2d'); ctx.clearRect(0,0,canvas.width,canvas.height); var imgdata = ctx.getImageData(0, 0, canvas.width, canvas.height); var pixels = imgdata.data, i = 0, sideLength = 100, planeLength = 4400; for (var y = 0; y < canvas.height; y++) { for (var x = 0; x < canvas.width; x++) { var screenP = new Vector3(-canvas.width / 2 + x, canvas.height - y, 0); var cv = new Vector3(camera.p.y * screenP.x / (camera.p.y - screenP.y), 0, camera.p.z * screenP.y / (screenP.y - camera.p.y)); 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; } else if (cv.z > -planeLength && cv.z < 0) { 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; } } ctx.putImageData(imgdata, 0, 0);
参考文献
用JavaScript玩转计算机图形学(一)光线追踪入门
光线跟踪 - 维基百科,自由的百科全书
Have Fun!