Light rays bouncing around.
#path #pathtracer
Log in to post a comment.
// Path Path Tracer. Created by Reinder Nijhoff 2025 - @reindernijhoff
//
// https://turtletoy.net/turtle/01524ccc80
//
const pathInput1 = `M-24,-55C50,-86 -34,87 82,-59`; // type=path, Click here to redraw the path
const pathInput2 = `M-82,64C-38,40 47,12 -26,84`; // type=path, Click here to redraw the path
const pathInput3 = `M-90,-90 L90,-90 L90,90 L-90,90 L-90,-90`; // type=path, Click here to redraw the path
const absorption = 0.25; // min=0, max=1, step=0.0001
const smoothness = 1; // min=0, max=1, step=0.0001
const lightSpread = 0; // min=0, max=30, step=0.0001
const opacity = -0.05; // min=-0.1, max=0.1, step=0.00001
const rays = 3500; // min=1, max=10000, step=1
const maxRecursion = 8; // min=0, max=30, step=1
Canvas.setpenopacity(opacity);
const path1 = Path(pathInput1);
const path2 = Path(pathInput2);
const path3 = Path(pathInput3);
const turtle = new Turtle();
function intersect(s, e) {
const paths = [path1, path2, path3];
return paths.reduce((closest, currentPath) => {
const p = currentPath.intersect(s, e);
if (p && (!closest || dist_sqr(s, p) < dist_sqr(s, closest))) {
return p;
}
return closest;
}, false);
}
function trace(s, e, r=0) {
const rd = normalize(sub(e, s));
let p = intersect(s, e);
turtle.jump(s);
turtle.goto(p || e);
if (p && r < maxRecursion && Math.random() > absorption) {
let n = normalize([-p[3], p[2]]);
const d = dot(n, rd);
if (d > 0) {
n = scale(n, -1);
}
if (0.01 < Math.abs(d)) {
const r0 = Math.sqrt(Math.random());
const diff = normalize(add(scale(n, r0), scale([-n[1], n[0]], Math.sign(Math.random()-.5) * Math.sqrt(Math.abs(1-r0*r0)))));
const refl = normalize(reflect(rd, n));
const d = lerp(diff, refl, smoothness);
trace(add(p, scale(d, 0.1)), scale(d, 200), r+1);
}
}
}
function walk(i) {
const a = i * (3 - Math.sqrt(5))*Math.PI;
const la = Math.random()*2*Math.PI, lr = Math.random()**2 * lightSpread;
trace([Math.cos(la)*lr, Math.sin(la)*lr], [Math.cos(a)*200, Math.sin(a)*200]);
return i < rays-1;
}
//
// 2D Vector math
//
function cross(a, b) { return a[0]*b[1]-a[1]*b[0]; }
function equal(a,b) { return .001>dist_sqr(a,b); }
function scale(a,b) { return [a[0]*b,a[1]*b]; }
function add(a,b) { return [a[0]+b[0],a[1]+b[1]]; }
function sub(a,b) { return [a[0]-b[0],a[1]-b[1]]; }
function dot(a,b) { return a[0]*b[0]+a[1]*b[1]; }
function dist_sqr(a,b) { return (a[0]-b[0])**2+(a[1]-b[1])**2; }
function dist(a,b) { return Math.sqrt(dist_sqr(a,b)); }
function length(a) { return Math.sqrt(dot(a,a)); }
function normalize(a) { return scale(a, 1/length(a)); }
function transform(a, mat) { return [a[0] * mat[0] + a[1] * mat[2], a[0] * mat[1] + a[1] * mat[3]]; }
function reflect(rd, n) { return sub(rd, scale(n, 2 * dot(rd, n))); }
function lerp(a, b, t) { return add(a, scale(sub(b, a), t)); }
//port of http://paulbourke.net/geometry/pointlineplane/Helpers.cs
function segment_intersect(l1p1, l1p2, l2p1, l2p2) {
const d = (l2p2[1] - l2p1[1]) * (l1p2[0] - l1p1[0]) - (l2p2[0] - l2p1[0]) * (l1p2[1] - l1p1[1]);
if (d === 0) return false;
const n_a = (l2p2[0] - l2p1[0]) * (l1p1[1] - l2p1[1]) - (l2p2[1] - l2p1[1]) * (l1p1[0] - l2p1[0]);
const n_b = (l1p2[0] - l1p1[0]) * (l1p1[1] - l2p1[1]) - (l1p2[1] - l1p1[1]) * (l1p1[0] - l2p1[0]);
const ua = n_a / d;
const ub = n_b / d;
if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) {
return [l1p1[0] + ua * (l1p2[0] - l1p1[0]), l1p1[1] + ua * (l1p2[1] - l1p1[1]), ub];
}
return false;
}
////////////////////////////////////////////////////////////////
// Modified path utility code. Created by Reinder Nijhoff 2025
//
// Added a distance method
//
// Parses a single SVG path (only M, C and L statements are
// supported). The p-method will return
// [...position, ...derivative] for a normalized point t.
//
// https://turtletoy.net/turtle/46adb0ad70
////////////////////////////////////////////////////////////////
function Path(svg) {
class MoveTo {
constructor(p) { this.p0 = p; }
p(t, s) { return [...this.p0, 1, 0]; }
length() { return 0; }
intersect(p0, p1) { return false; }
}
class LineTo {
constructor(p0, p1) { this.p0 = p0, this.p1 = p1; }
p(t, s = 1) {
const nt = 1 - t, p0 = this.p0, p1 = this.p1;
return [
nt*p0[0] + t*p1[0],
nt*p0[1] + t*p1[1],
(p1[0] - p0[0]) * s,
(p1[1] - p0[1]) * s,
];
}
length() {
const p0 = this.p0, p1 = this.p1;
return Math.hypot(p0[0]-p1[0], p0[1]-p1[1]);
}
intersect(p0, p1) {
const p = segment_intersect(p0, p1, this.p0, this.p1), hw = this.p(.5);
return p ? [p[0], p[1], hw[2], hw[3], dist_sqr(p0, p)] : false;
}
}
class BezierTo {
constructor(p0, c0, c1, p1) { this.p0 = p0, this.c0 = c0, this.c1 = c1, this.p1 = p1; }
p(t, s = 1) {
const nt = 1 - t, p0 = this.p0, c0 = this.c0, c1 = this.c1, p1 = this.p1;
return [
nt*nt*nt*p0[0] + 3*t*nt*nt*c0[0] + 3*t*t*nt*c1[0] + t*t*t*p1[0],
nt*nt*nt*p0[1] + 3*t*nt*nt*c0[1] + 3*t*t*nt*c1[1] + t*t*t*p1[1],
(3*nt*nt*(c0[0]-p0[0]) + 6*t*nt*(c1[0]-c0[0]) + 3*t*t*(p1[0]-c1[0])) * s,
(3*nt*nt*(c0[1]-p0[1]) + 6*t*nt*(c1[1]-c0[1]) + 3*t*t*(p1[1]-c1[1])) * s,
];
}
length() {
return this._length || (
this._length = Array.from({length:25}, (x, i) => this.p(i/25)).reduce(
(a,c,i,v) => i > 0 ? a + Math.hypot(c[0]-v[i-1][0], c[1]-v[i-1][1]) : a, 0));
}
intersect(p0, p1) {
// todo, analytical solution
let b0 = this.p(0);
const steps = 100;
return Array.from({length:steps}, (_,i)=>i+1).reduce((a,c) => {
const b1 = this.p(c/steps);
const p = segment_intersect(p0, p1, b0, b1);
b0 = [...b1];
if (p && (!a || dist_sqr(p0, p) < dist_sqr(p0, a))) {
const normal = this.p(c/steps + p[2]/steps)
return [p[0], p[1], normal[2], normal[3], dist_sqr(p0, p)];
} else {
return a;
}
}, false);
}
}
class Path {
constructor(svg) {
this.segments = [];
this.parsePath(svg);
}
parsePath(svg) {
const t = svg.match(/([0-9.-]+|[MLC])/g);
for (let s, i=0; i<t.length;) {
switch (t[i++]) {
case 'M': this.add(new MoveTo(s=[t[i++],t[i++]]));
break;
case 'L': this.add(new LineTo(s, s=[t[i++],t[i++]]));
break;
case 'C': this.add(new BezierTo(s, [t[i++],t[i++]], [t[i++],t[i++]], s=[t[i++],t[i++]]));
break;
default: i++;
}
}
}
add(segment) {
this.segments.push(segment);
this._length = 0;
}
length() {
return this._length || (this._length = this.segments.reduce((a,c) => a + c.length(), 0));
}
p(t) {
t = Math.max(Math.min(t, 1), 0) * this.length();
for (let l=0, i=0, sl=0; i<this.segments.length; i++, l+=sl) {
sl = this.segments[i].length();
if (t > l && t <= l + sl) {
return this.segments[i].p((t-l)/sl, sl/this.length());
}
}
return this.segments[Math.min(1, this.segments.length-1)].p(0);
}
intersect(p0, p1) {
return this.segments.reduce((a,c) => {
const p = c && c.intersect(p0, p1);
if (p && (!a || dist_sqr(p0, p) < dist_sqr(p0, a))) {
return [...p, dist_sqr(p0, p)];
} else {
return a;
}
}, false);
}
}
return new Path(svg);
}