Outer Worlds

Close up on tiny planet.
Takes a few seconds to get started.

Log in to post a comment.

// LL 2021

const turtle = new Turtle();

//Canvas.setpenopacity(-1);

const density = 101;        // min=1, max=301, step=2
const precision = 1.;      // min=0.1, max=3, step=0.1
const passes = 1;          // min=1, max=3, step=1 (X, XY, XYZ)
const height = 0.8;        // min=0, max=2, step=0.1
const cam_dist = 0.3;      /// min=0.1, max=10.5, step=0.01
const cam_angle = 1.5;     /// min=-3.14159, max=3.15159, step=0.01
const shape = 0;           // min=0, max=1, step=1 (Sphere,Cube)

const MAX_STEPS = 256;
const MAX_DIST = 100;
const SURF_DIST = .001;

const range = [ -2, 1 ];

function length2(v2) { return Math.sqrt(v2[0]*v2[0] + v2[1]*v2[1]); }
function length3(v3) { return Math.sqrt(v3[0]*v3[0] + v3[1]*v3[1] + v3[2]*v3[2]); }
function normalize3(v3) { var l = length3(v3); if (l<0.00001) l=1; return [v3[0]/l, v3[1]/l, v3[2]/l]; }
function mul2(a2, f) { return [a2[0]*f, a2[1]*f]; }
function mul3(a3, f) { return [a3[0]*f, a3[1]*f, a3[2]*f]; }
function add3(a3, b3) { return [a3[0]+b3[0], a3[1]+b3[1], a3[2]+b3[2]]; }
function sub3(a3, b3) { return [a3[0]-b3[0], a3[1]-b3[1], a3[2]-b3[2]]; }
function cross3(a3, b3) { return [ a3[1] * b3[2] - a3[2] * b3[1], a3[2] * b3[0] - a3[0] * b3[2], a3[0] * b3[1] - a3[1] * b3[0] ]; }
function fract3(v3) { return [ v3[0]-Math.floor(v3[0]), v3[1]-Math.floor(v3[1]), v3[2]-Math.floor(v3[2]) ]; }
function floor3(v3) { return [ Math.floor(v3[0]), Math.floor(v3[1]), Math.floor(v3[2]) ]; }
function abs3(v3) { return [ Math.abs(v3[0]), Math.abs(v3[1]), Math.abs(v3[2]) ]; }
function dot3(a3, b3) { return a3[0] * b3[0] + a3[1] * b3[1] + a3[2] * b3[2]; }
function mul_mat2(v2, m22) { return [ v2[0] * m22[0] + v2[1] * m22[1], v2[0] * m22[2] + v2[1] * m22[3] ]; }
function clamp(x, min, max) { return Math.min(max, Math.max(min, x)); }
function smoothstep(edge0, edge1, x) { x = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); return x * x * (3 - 2 * x); }
function tri3(x3) { return abs3(sub3(fract3(x3), .5)); }
function mix(x, y, a) { return x * (1-a) + y * a; }
function mix3(a3, b3, f) { return [ mix(a3[0], b3[0], f), mix(a3[1], b3[1], f), mix(a3[2], b3[2], f) ]; }
function smin(a, b , s) { var h = clamp( 0.5 + 0.5*(b-a)/s, 0. , 1.); return mix(b, a, h) - h*(1.0-h)*s; }

function rotation_mat22(a)
{
    var s = Math.sin(a);
    var c = Math.cos(a);
    return [c, -s, s, c];
}

function GetRayDir(uv2, p3, l3, z)
{
    var f3 = normalize3(sub3(l3, p3));
    var r3 = normalize3(cross3([0,1,0], f3));
    var u3 = cross3(f3, r3);
    var c3 = mul3(f3, z);
    var i3 = add3(add3(c3, mul3(r3, uv2[0])), mul3(u3, uv2[1]));
    var d3 = normalize3(i3);
    return d3;
}

// Box
function sdBox(p3, s3)
{
    p3 = sub3(abs3(p3), s3);
    var r = Math.min(Math.max(p3[0], Math.max(p3[1], p3[2])), 0);
    var q3 = [Math.max(p3[0], 0), Math.max(p3[1], 0), Math.max(p3[2], 0)];
    q3 = add3(q3, [r,r,r]);
	return length3(q3);
}

// Torus
function sdTorus(p3, r1, r2)
{
    var cp2 = [ length2([p3[0], p3[2]]) - r1, p3[1] ];
    var d = length2(cp2) - r2;

    return d;
}

// Knot
function sdKnot(p3, r1, r2, twist, split)
{
    var cp2 = [ length2([p3[0], p3[2]]) - r1, p3[1] ];
    
    var a = Math.atan2(p3[2], p3[0]); // polar angle between -pi and pi
    cp2 = mul_mat2(cp2, rotation_mat22(a * twist));
    cp2[1] = Math.abs(cp2[1]) - split;

    var d = length2(cp2) - r2;

    return d * .8;
}

// Sphere
function sdSphere(p3, r)
{
    var d = length3(p3) - r;
    return d;
}


function warp3(p3)
{
    const f = 0.1;
    const t = 100.0 * p3[1] + 2.0 * 1;
    return [p3[0] + Math.sin(t) * f, p3[1], p3[2] + Math.cos(t) * f];
}

function displacement(p3)
{
    return Math.sin(21*p3[0])*Math.sin(21*p3[1])*Math.sin(21*p3[2]);
}


function hash(n)
{
    var f = Math.sin(n) * 753.5453123;
	return f - Math.trunc(f);
}

function noise3(v3)
{
	var p3 = floor3(v3);
	var f3 = fract3(v3);

	f3[0] = f3[0] * f3[0] * (3.0 - 2.0 * f3[0]);
	f3[1] = f3[1] * f3[1] * (3.0 - 2.0 * f3[1]);
	f3[2] = f3[2] * f3[2] * (3.0 - 2.0 * f3[2]);

    var n = p3[0] + p3[1] * 157.0 + 113.0 * p3[2];

    return mix(mix(mix( hash(n +   0.0), hash(n +   1.0),f3[0]),
                   mix( hash(n + 157.0), hash(n + 158.0),f3[0]),f3[1]),
               mix(mix( hash(n + 113.0), hash(n + 114.0),f3[0]),
                   mix( hash(n + 270.0), hash(n + 271.0),f3[0]),f3[1]),f3[2]);
}

function fbm_terr(p3, lacunarity, init_gain, gain)
{
    const _octaves = 3;
    var q3 = [...p3];
    var H = init_gain;
    var t = 0.;
    for (var i = 0; i < _octaves; i++)
    {
        t += noise3(q3) * H;
        q3 = mul3(q3, lacunarity);
        H *= gain;
    }
    return t;
}

function fbm_terr_r(p3, lacunarity, init_gain, gain)
{
    const _octaves = 3;
    var q3 = [...p3];
    var H = init_gain;
    var t = 0.;
    for (var i = 0; i < _octaves; i++)
    {
        t += (1. - Math.abs(noise3(q3) * 2. - 1.)) * H;
        q3 = mul3(q3, lacunarity);
        H *= gain;
    }
    return t;
}

// Adapted from https://www.shadertoy.com/view/ldyXRw
function sdPlanet(p3, radius, factor)
{
    var q3 = [p3[1], p3[0], p3[2]];
    
 	var h0 = fbm_terr(mul3(q3, 2.0987), 2.0244, .454, .454);
 	var n0 = smoothstep(.35, 1., h0);

 	var h1 = fbm_terr_r(add3(mul3(q3, 1.50987), [ 1.9489, 2.435, .5483 ]), 2.0244, .454, .454);
 	var n1 = smoothstep(.5, 1., h1);

 	var n = n0 * 6 + n1;

    var d = (shape == 0) ? length3(p3) - radius : sdBox(q3,[radius,radius,radius]);
	d -= n * height * factor;
	if (shape == 0) d -= displacement(q3) * 0.02 / factor;
	return d;
}

function map(p3)
{
    var d = MAX_DIST;
    
    d = Math.min(d, sdPlanet(add3(p3, [0, -5.2, 0]), 5, 1));

    d = Math.min(d, sdPlanet(add3(p3, [1, (shape==0)?0.5:0.7, 2]), (shape==0)?0.5:0.2, 0.5));

    return d;
}

function RayMarch(ro3, rd3)
{
	var dO = 0;
    
    for (var i = 0; i < MAX_STEPS; i++)
    {
    	var p3 = add3(ro3, mul3(rd3, dO));
        var dS = map(p3);
        dO += dS;
        if (dO > MAX_DIST) return MAX_DIST;
        if (Math.abs(dS)<SURF_DIST) break;
    }
    
    return dO;
}

var pass = -1;

function key(p2) { return p2[0] * 10000 + p2[1] * 10 + pass; }

var cache = {};

// p: 2D point in -100 to 100 range
function zFunc(p2)
{
    // Ray origin
    var ro3 = [ cam_dist * Math.cos(cam_angle), -0.1, cam_dist * Math.sin(cam_angle) ];

    // Look at
    var l3 = [ 0, 0, 0 ];

    // Convert to -1 to 1
    var uv2 = [ p2[0] / 100, p2[1] / 100 ];

    // Ray direction
    var rd3 = GetRayDir(uv2, ro3, l3, 1.);

    var key_p2 = key(p2);
    
    if (key_p2 in cache)
    {
        var dist = cache[key_p2];
        if (dist < MAX_DIST)
        {
            // Get intersection point
            var p3 = add3(ro3, mul3(rd3, dist));
            dist = (p3[(pass + 2) % 3]-range[0]) / (range[1]-range[0]) * density;
        }
        return dist;
    }

    // Get distance to intersection
    var dist = RayMarch(ro3, rd3);

    cache[key_p2] = dist;
    
    if (dist < MAX_DIST)
    {
        // Get intersection point
        var p3 = add3(ro3, mul3(rd3, dist));
        dist = (p3[pass]-range[0]) / (range[1]-range[0]) * density;
    }

    return dist;
}

function floor(x) { const precision = 0.25; return Math.round(x / precision) * precision; }

function line_key(line)
{
    return Math.min(line[0][0], line[1][0]) * 1000000000 + Math.min(line[0][1], line[1][1]) * 1000000 + Math.max(line[0][0], line[1][0]) * 1000 + Math.max(line[0][1], line[1][1]);
}

var line_cache = {};
function is_unique(line)
{
    const key = line_key(line);
    if (key in line_cache) return false
    line_cache[key] = 1;
    return true;
}

function floor_line(line)
{
    return [ [ floor(line[0][0]), floor(line[0][1]) ], [ floor(line[1][0]), floor(line[1][1]) ] ];
}

var unique_line_count = 0;
var total_line_count = 0;
var start_time = performance.now();

function walk(i) {
    pass = Math.floor(i / (density+1));
    if (pass >= passes)
    {
        const elapsed = ((performance.now() - start_time) / 1000).toFixed(1);
        const percent = (unique_line_count * 100 / total_line_count).toFixed(1);
        console.log(`Time: ${elapsed} s | Unique lines: ${unique_line_count} of ${total_line_count} (${percent}%)`)
        
        return false;
    }
    
    const lines = ContourLines(i%density, 1/precision, zFunc);
    lines.forEach(line => {
        total_line_count++;
        var line2 = floor_line(line);
        if (is_unique(line2))
        {
            turtle.jump(line2[0]);
            turtle.goto(line2[1]);
            unique_line_count++;
        }
    });

    return true;
}

// Metaball Contour Lines. Created by Reinder Nijhoff 2020 - @reindernijhoff
// The MIT License
// https://turtletoy.net/turtle/104c4775c5
function ContourLines(z, step, zFunc) {
    const intersectSegmentZ = (z, v1, v2) => {
    	if (v1[2] === v2[2]) return false;
    	const t = (z - v1[2]) / (v2[2] - v1[2]);
    	if (t <= 0 || t > 1) return false;
    	return [v1[0]+(v2[0]-v1[0])*t, v1[1]+(v2[1]-v1[1])*t];
    }
    const intersectTriangleZ = (z, p1, p2, p3) => {
        const p = [];
    	const v1 = intersectSegmentZ(z, p1, p2);
    	const v2 = intersectSegmentZ(z, p2, p3);
    	const v3 = intersectSegmentZ(z, p3, p1);
    	if (v1 && v2) p.push([v1, v2]);
        if (v1 && v3) p.push([v1, v3]);
    	if (v2 && v3) p.push([v2, v3]);
		return p;
    }
	const result = [];
	for (let x = -100; x <= 100; x += step) {
    	for (let y = -100; y <= 100; y += step) {
			const corners = [[x, y], [x+step, y], [x+step, y+step], [x, y+step]];
			corners.forEach( c => c[2] = zFunc(c) );
			const c3 = [x+step/2, y+step/2, zFunc([x+step/2, y+step/2])];
			for (let i=0; i<4; i++) {
			    result.push(...intersectTriangleZ(z, corners[i], corners[(i+1) & 3], c3));
			}
		}
	}
	return result;
}