Path Path Tracer

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);
}