350行路徑追蹤渲染器online demo
function CreateFPSCounter() {
var mFrame;
var mTo;
var mFPS;
var mLastTime;
var mDeltaTime;
var iReset = function (time) {
time = time || 0;
mFrame = 0;
mTo = time;
mFPS = 60.0;
mLastTime = time;
mDeltaTime = 0;
}
var iCount = function (time) {
mFrame++;
mDeltaTime = time - mLastTime;
mLastTime = time;
if ((time - mTo) > 500.0) {
mFPS = 1000.0 * mFrame / (time - mTo);
mFrame = 0;
mTo = time;
return true;
}
return false;
}
var iGetFPS = function () {
return mFPS;
}
var iGetDeltaTime = function () {
return mDeltaTime;
}
return { Reset: iReset, Count: iCount, GetFPS: iGetFPS, GetDeltaTime: iGetDeltaTime };
}
function RequestFullScreen(ele) {
if (ele == null) ele = document.documentElement;
if (ele.requestFullscreen) ele.requestFullscreen();
else if (ele.msRequestFullscreen) ele.msRequestFullscreen();
else if (ele.mozRequestFullScreen) ele.mozRequestFullScreen();
else if (ele.webkitRequestFullscreen) ele.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
}
function Init() {
if (window.init_flag === undefined) {
console.log("init_flag");
var iMouse = window.iMouse = { x: 0, y: 0, z: 0, w: 0 };
var cv = document.getElementById("displayPort");
var fpsConter = CreateFPSCounter();
var context = cv.getContext('2d');
var W = cv.clientWidth; H = cv.clientHeight;
var imageData = context.getImageData(0, 0, W, H);
var pixels = imageData.data;
for (var i = 0; i < W * H; ++i)
pixels[4 * i + 3] = 255;
function MainLoop(deltaTime) {
var tracer = window.rayTracer;
if (tracer == null) return;
if (iMouse.z > 0.) tracer.ResetFrames();
var frames = tracer.CountFrames();
var i = 0, color;
for (var y = 0; y < H; ++y) {
for (var x = 0; x < W; ++x, ++i) {
color = tracer.Render(x, y, W, H);
pixels[i++] = (color.r * 255) | 0;
pixels[i++] = (color.g * 255) | 0;
pixels[i++] = (color.b * 255) | 0;
}
}
context.putImageData(imageData, 0, 0);
// console.log(deltaTime, fpsConter.GetFPS())
}
function elementPos(element) {
var x = 0, y = 0;
while (element.offsetParent) {
x += element.offsetLeft;
y += element.offsetTop;
element = element.offsetParent;
}
return { x: x, y: y };
}
function getMouseCoords(ev, canvasElement) {
var pos = elementPos(canvasElement);
var mcx = (ev.pageX - pos.x) * canvasElement.width / canvasElement.offsetWidth;
var mcy = (ev.pageY - pos.y) * canvasElement.height / canvasElement.offsetHeight;
return { x: mcx, y: mcy };
}
cv.onmousemove = function (ev) {
var mouse = getMouseCoords(ev, cv);
iMouse.x = mouse.x;
iMouse.y = mouse.y;
}
cv.onmousedown = function (ev) {
if (ev.button === 0) iMouse.z = 1;
else if (ev.button === 2) {
iMouse.w = 1;
RequestFullScreen(cv);
}
}
document.onmouseup = function (ev) {
if (ev.button === 0) iMouse.z = 0;
else if (ev.button === 2) iMouse.w = 0;
}
; (function (loopFunc) {
var fisrt = true;
function L(timestamp) {
if (fisrt) {
fisrt = false, fpsConter.Reset(timestamp)
} else {
fpsConter.Count(timestamp);
}
loopFunc(fpsConter.GetDeltaTime());
requestAnimationFrame(L)
};
requestAnimationFrame(L);
})(MainLoop);
window.init_flag = true;
}
}
function CreateTracer() {
try {
(new Function(document.getElementById("codeEditor").value))();
window.rayTracer = new window.pt();
window.iMouse.x = window.iMouse.y = 0;
var cv = document.getElementById("displayPort");
var W = cv.clientWidth; H = cv.clientHeight;
window.rayTracer.CreateBackBuffer(W, H);
} catch(e) {
alert(e)
}
}
Init();
CreateTracer();
(function copy() {
var e1 = document.getElementById("oldCode");
var e2 = document.getElementById("codeEditor");
if (e1 && e2) {
e2.value = e1.innerText;
}
})()
window.pt = (function () {
// 3維向量
function Vec3(x, y, z) {
x = x === undefined ? 0 : x;
this.x = x;
this.y = y === undefined ? x : y;
this.z = z === undefined ? x : z;
} Vec3.prototype = {
Set: function (x, y, z) { this.x = x, this.y = y, this.z = z; return this; },
Copy: function (other) { this.x = other.x, this.y = other.y, this.z = other.z; return this; },
Add: function (other) { return new Vec3(this.x + other.x, this.y + other.y, this.z + other.z); },
AddSelf: function (other) { this.x += other.x, this.y += other.y, this.z += other.z; return this; },
Sub: function (other) { return new Vec3(this.x - other.x, this.y - other.y, this.z - other.z); },
SubSelf: function (other) { this.x -= other.x, this.y -= other.y, this.z -= other.z; return this; },
Scale: function (factor) { return new Vec3(this.x * factor, this.y * factor, this.z * factor); },
ScaleSelf: function (factor) { this.x *= factor, this.y *= factor, this.z *= factor; return this; },
Length: function () { return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); },
Normalize: function () { return this.Scale(1. / this.Length()); },
NormalizeSelf: function () { var l = 1. / Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); this.x *= l; this.y *= l; this.z *= l; return this; },
Dot: function (other) { return this.x * other.x + this.y * other.y + this.z * other.z; },
Cross: function (other) { return new Vec3(this.y * other.z - this.z * other.y, this.z * other.x - this.x * other.z, this.x * other.y - this.y * other.x); }
}
var eye = new Vec3(0, 0, -2.41);
var up = new Vec3(0, 1, 0);
var zero = new Vec3(0, 0, 0);
var one = new Vec3(1, 1, 1);
var sqrt2 = Math.sqrt(2);
// 顏色
function Color(r, g, b) { this.r = r; this.g = g; this.b = b; }
Color.prototype = {
Set: function (r, g, b) { this.r = r; this.g = g; this.b = b; return this; },
Copy: function (other) { this.r = other.r, this.g = other.g, this.b = other.b; return this; },
Scale: function (factor) { return new Color(this.r * factor, this.g * factor, this.b * factor); },
ScaleSelf: function (factor) { this.r *= factor, this.g *= factor, this.b *= factor; return this; },
Multiply: function (other) { return new Color(this.r * other.r, this.g * other.g, this.b * other.b); },
MultiplySelf: function (other) { this.r *= other.r, this.g *= other.g, this.b *= other.b; return this; },
Lerp: function (other, t) { return new Color((1 - t) * this.r + t * other.r, (1 - t) * this.g + t * other.g, (1 - t) * this.b + t * other.b); },
Gamma: function (s) { var inv = 1/s; return new Color(Math.pow(this.r, inv), Math.pow(this.g, inv), Math.pow(this.b, inv)); },
GammaSelf: function (s) { var inv = 1/s; return this.Set(Math.pow(this.r, inv), Math.pow(this.g, inv), Math.pow(this.b, inv)); return this; }
}
function MakeColor255(r, g, b) { return new Color(r / 255, g / 255, b / 255); }
var colorWhite = new Color(1, 1, 1);
var colorBlack = new Color(0, 0, 0);
var colorRed = new Color(1, 0, 0);
var colorGreen = new Color(0, 1, 0);
var colorBlue = new Color(0, 0, 1);
var colorSky = new Color(0.5, 0.7, 1.0);
// 光線
function Ray(o, d, t) {
this.origin = o;
this.direction = d;
this.t = t || 1;
} Ray.prototype = {
GetOrigin: function () { return this.origin; },
GetDirection: function () { return this.direction; },
GetT: function () { return this.t; },
GetPoint: function (t) { return this.origin.Add(this.direction.Scale(typeof t === 'number' ? t : this.t)); },
}
function Material(type, parameters) {
this.type = type;
this.parameters = parameters == null ? {} : parameters;
}
// 球體
function Sphere(center, radius, material) {
this.center = center;
this.radius = radius;
this.material = material;
} Sphere.prototype = {
Intersection(ray) {
var oc = ray.GetOrigin().Sub(this.center);
var b = oc.Dot(ray.GetDirection());
var c = oc.Dot(oc) - this.radius * this.radius;
var h = b * b - c;
if (h < 0) return -1;
return -b - Math.sqrt(h);
},
HitNormal(hit) {
return hit.Sub(this.center).ScaleSelf(1. / this.radius);
}
}
// 相機
function Camera(eye, target, up) {
this.eye = new Vec3().Copy(eye);
this.target = new Vec3().Copy(target);
this.up = new Vec3().Copy(up);
this.CalcUVW(eye);
this.an = 0;
this.ro = new Vec3().Copy(this.eye);
} Camera.prototype = {
CalcUVW(eye) {
this.w = this.target.Sub(eye).NormalizeSelf();
this.u = this.w.Cross(this.up).NormalizeSelf();
this.v = this.u.Cross(this.w);
},
GetRay(u, v, width, height) {
if (iMouse.z > 0.) {
var an = 2 * Math.PI * (iMouse.x / width);
this.ro = new Vec3().Set(this.eye.z * Math.sin(an), 0.0, this.eye.z * Math.cos(an));
this.CalcUVW(this.ro);
this.an = an;
}
var ro = this.ro;
var rd = this.target.Sub(this.eye).AddSelf(new Vec3(u * sqrt2, v * sqrt2, 0.)).NormalizeSelf();
var x = rd.x, y = rd.y, z = rd.z;
rd.x = x * this.u.x + y * this.v.x + z * this.w.x;
rd.y = x * this.u.y + y * this.v.y + z * this.w.y;
rd.z = x * this.u.z + y * this.v.z + z * this.w.z;
return new Ray(ro, rd, 0);
}
}
// 后緩沖區
function BackBuffer(width, height, frames) {
this.width = width;
this.height = height;
this.data = new Array(width * height * 4);
} BackBuffer.prototype = {
Clear: function (r, g, b) {
r = r === undefined ? 0 : r;
g = g === undefined ? r : g;
b = b === undefined ? r : b;
var size = this.width * this.height;
for (var i = 0; i < size; ++i) {
this.data[4 * i + 0] = r;
this.data[4 * i + 1] = g;
this.data[4 * i + 2] = b;
this.data[4 * i + 3] = 1;
}
},
Read: function (x, y) {
var offset = (y * this.width + x) * 4;
var r = this.data[offset + 0];
var g = this.data[offset + 1];
var b = this.data[offset + 2];
return [r, g, b];
},
Write: function (x, y, r, g, b) {
var offset = (y * this.width + x) * 4;
this.data[offset + 0] = r;
this.data[offset + 1] = g;
this.data[offset + 2] = b;
}
}
// 追蹤器
function Tracer() {
this.camera = new Camera(eye, zero, up);
this.spheres = new Array(
new Sphere(new Vec3(-0.5, 0.0, 0.), 1.0, new Material(1, { refractRate: 1.5, color: MakeColor255(196, 32, 128) })),
new Sphere(new Vec3(0.825, -0.5, -0.5), 0.5, new Material(2, { color: new Color(0.1, 1., 0.125) })),
new Sphere(new Vec3(-0.75, -0.65, -1.), 0.2, new Material(2, { fuzz: 0.3, color: colorBlue })),
new Sphere(new Vec3(0.25, 0.15, -1.), 0.25, new Material(3, { refractRate: 1.2, glass : true, fuzz: 0.3, color: colorWhite })),
new Sphere(new Vec3(-1.05, -0.25, -1.), 0.25, new Material(3, { refractRate: 1.2, fuzz: 0.3, color: colorWhite })),
new Sphere(new Vec3(0.0, -10000.85, -1.0), 10000.0, new Material(1, null))
);
this.frames = 0;
} Tracer.prototype = {
RandomPointInUnitSphere: function () {
return new Vec3(
Math.random() * 2. - 1.,
Math.random() * 2. - 1.,
Math.random() * 2. - 1.
);
},
// 基向量
Frisvad: function (n, f, r) {
if (n.z < -0.999999) {
f.Set(0., -1, 0);
r.Set(-1, 0, 0);
} else {
var a = 1. / (1. + n.z);
var b = -n.x * n.y * a;
f.Set(1. - n.x * n.x * a, b, -n.x);
r.Set(b, 1. - n.y * n.y * a, -n.y);
}
},
// cos權重隨機半球采樣
CosWeightedRandomHemisphereDirection(normal) {
var x = Math.random(), y = Math.random();
var u = new Vec3(), v = new Vec3(), w = normal;
this.Frisvad(w, u, v);
var ra = Math.sqrt(y);
var rx = ra * Math.cos(2 * Math.PI * x);
var ry = ra * Math.sin(2 * Math.PI * x);
var rz = Math.sqrt(1.0 - y);
u.x = rx * u.x + ry * v.x + rz * w.x;
u.y = rx * u.y + ry * v.y + rz * w.y;
u.z = rx * u.z + ry * v.z + rz * w.z;
return u.NormalizeSelf();
},
Scatered: function (color, ray, sphere) {
var material = sphere.material;
var hit = ray.GetPoint();
if (material.type === 1) {
return this.Diffused(color, hit, sphere.HitNormal(hit), ray.GetDirection())
} else if (material.type === 2) {
return this.Reflected(color, hit, sphere.HitNormal(hit), ray.GetDirection(), material.parameters.fuzz);
} else if (material.type === 3) {
return this.Refracted(color, hit, sphere.HitNormal(hit), ray.GetDirection(), material.parameters.refractRate, material.parameters.glass);
}
},
Diffused: function (color, hit, normal, direction) {
color.ScaleSelf(0.5);
var target = hit.Add(normal).AddSelf(this.RandomPointInUnitSphere());
return new Ray(hit, target.SubSelf(hit).NormalizeSelf());
//return new Ray(hit, this.CosWeightedRandomHemisphereDirection(normal));
},
Reflected: function (color, hit, normal, direction, fuzz) {
function reflect(direction, normal) {
var l = -2 * direction.Dot(normal);
return normal.Scale(l).AddSelf(direction);
}
color.ScaleSelf(0.5);
var n = this.RandomPointInUnitSphere().ScaleSelf(fuzz || 0);
var r = reflect(direction, normal);
return new Ray(hit, r.AddSelf(n).NormalizeSelf());
},
Refracted: function (color, hit, normal, direction, refractRate, glass) {
var glass = glass === undefined ? false : glass;
var outwardNormal, realRate;
var cosWi = direction.Dot(normal);
if (cosWi > 0) {
outwardNormal = normal.Scale(-1);
realRate = glass ? 1.0 / refractRate : refractRate;
//realRate = 1.0 / refractRate;
//realRate = refractRate;
cosWi = direction.Dot(outwardNormal);
} else {
outwardNormal = normal;
realRate = glass ? refractRate : 1.0 / refractRate;
//realRate = refractRate;
//realRate = 1.0 / refractRate;
}
var cos2Wo = 1.0 - realRate * realRate * (1.0 - cosWi * cosWi);
if (cos2Wo > 0) {
var y = outwardNormal.Scale(-Math.sqrt(cos2Wo));
var x = outwardNormal.Scale(-cosWi).AddSelf(direction).ScaleSelf(realRate);
var refractRay = x.AddSelf(y);
return new Ray(hit, refractRay);
} else {
return this.Reflected(color, hit, normal, direction);
}
},
Trace: function (ray, deep) {
var color = new Color().Copy(colorWhite);
if (deep > 5) {
color.Copy(colorBlack);
return color;
}
var hitSomething = false, min = 999, t, sphere;
this.spheres.forEach(function (sp) {
t = sp.Intersection(ray);
if (t > 0. && t < min) {
hitSomething = true;
min = t;
sphere = sp;
}
});
if (hitSomething) {
ray.t = min;
var material = sphere.material;
if (material.parameters.color) color.Copy(material.parameters.color);
var newRay = this.Scatered(color, ray, sphere, color);
color.MultiplySelf(this.Trace(newRay, deep + 1));
} else {
var t = 0.5 * (ray.direction.y + 1.0);
return colorWhite.Lerp(colorSky, t);
}
return color;
},
Render: function (x, y, width, height) {
// antialiasing
var u = (x + Math.random()) / width, v = (y + Math.random()) / height;
u = (u * 2 - 1.) * (width / height);
v = 1. - v * 2;
var ray = this.camera.GetRay(u, v, width, height);
var color = this.Trace(ray, 0);
// backbuffer
var buffer = this.buffer;
if (buffer) {
var frames = this.frames;
var c = buffer.Read(x, y);
var f = (frames - 1) / frames;
var r = c[0], g = c[1], b = c[2];
r = r * f + color.r / frames;
g = g * f + color.g / frames;
b = b * f + color.b / frames;
buffer.Write(x, y, r, g, b);
color.Set(r, g, b)
}
// gamma corrected
color.GammaSelf(2.2);
return color;
},
CreateBackBuffer: function (width, height) {
var buffer = new BackBuffer(width, height);
buffer.Clear();
this.buffer = buffer;
},
CountFrames: function () {
return ++this.frames;
},
ResetFrames: function () {
this.frames = 0;
return this.frames;
},
}
return Tracer;
})()
這是一個簡單的路徑追蹤demo
移動視角:左鍵按下+鼠標移動
全屏查看:右鍵按下