简介
光线追踪(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!