Contoured & Curled Alienscape

Revisiting SDF with a new/faster Contour implementation. Lighting with Reinder's Curl from Möbius Ladder and Curl.
This scene is very slow to render, so it will probably timeout with the default values.
The mixed contour/curl render code is just an experiment and super messy.

Log in to post a comment.

// LL 2021

const turtle = new Turtle();

const opacity = 0.8; /// min=0.01 max=1 step=0.01
Canvas.setpenopacity(opacity);

const precision = 500; /// min=10 max=1500 step=1
const slices = 50; /// min=0.01 max=1000 step=0.1

const passes = 0; /// min=0 max=6 step=1 (X,Y,Z,XY,XZ,YZ,XYZ)

const pass_list = [[0], [1], [2], [0,1], [0, 2], [1, 2], [0, 1, 2]][passes];

const cam_dist = 8.;    /// min=0.1, max=10.5, step=0.01
const cam_angle = 0.;   /// min=-3.14159, max=3.15159, step=0.01

let cache;
const r = 100 / precision;

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

const range = [ -1.5, 1.1 ];
const iTime = 0.2; /// min=0 max=1 step=0.01
var iTime_t = iTime;

const round_clip = 1; /// min=0 max=1 step=1 (No,Yes)

const radius = 0.5; /// min=0.01, max=1, step=0.01
const minRadius = 0.1; /// min=0.01, max=1, step=0.01
const maxPathLength = 75;  /// min=1, max=100, step=0.1
const maxTries = 1000;

const grid  = new PoissonDiscGrid(radius);

function walk(i, t) {
    return walkContour(i, t) || walkCurl(i, t);
}

function walkContour(i, t)
{
    if (i == 0) {
        cache = {};
        iTime_t = iTime + t;
    }
    
    if (i >= precision*precision) return false;

    const xx = i % precision;
    const yy = (i / precision) | 0;
    
    const ci00 = (xx-1) + (yy-1) * precision;
    const ci01 = (xx-0) + (yy-1) * precision;
    const ci10 = (xx-1) + (yy-0) * precision;
    const ci11 = (xx-0) + (yy-0) * precision;

    const x0 = ((xx-1) / precision - 0.5) * 200;
    const y0 = ((yy-1) / precision - 0.5) * 200;
    const x1 = ((xx-0) / precision - 0.5) * 200;
    const y1 = ((yy-0) / precision - 0.5) * 200;

    var p11;
    const p00 = cache[ci00], p01 = cache[ci01], p10 = cache[ci10];
    if (ci11 in cache) p11 = cache[ci11]; else cache[ci11] = p11 = zFuncP3([x1, y1]);

    if (p00 !== undefined && p01 !== undefined && p10 !== undefined) {
        var lines = [];
        
        pass_list.forEach(pass => {
            const z00 = terrace((p00[pass]-range[0]) / (range[1]-range[0]));
            const z01 = terrace((p01[pass]-range[0]) / (range[1]-range[0]));
            const z10 = terrace((p10[pass]-range[0]) / (range[1]-range[0]));
            const z11 = terrace((p11[pass]-range[0]) / (range[1]-range[0]));
        
            // A bit like marching cubes
            /* 0123 */ if (z00 != z01 && z00 != z10 && z00 != z11) lines = [[[1,0],[0,1]],[[0,1],[1,2]],[[1,2],[2,1]],[[2,1],[1,0]]];
            /* 0001 */ if (z00 == z01 && z00 == z10 && z00 != z11) lines = [[[2,1],[1,2]]];
            /* 0010 */ if (z00 == z01 && z00 == z11 && z00 != z10) lines = [[[0,1],[1,2]]];
            /* 0011 */ if (z00 == z01 && z10 == z11 && z00 != z10) lines = [[[0,1],[2,1]]];
            /* 0100 */ if (z00 == z10 && z00 == z11 && z00 != z01) lines = [[[1,0],[2,1]]];
            /* 0101 */ if (z00 == z10 && z01 == z11 && z00 != z01) lines = [[[1,0],[1,2]]];
            /* 0110 */ if (z00 == z11 && z01 == z10 && z00 != z01) lines = [[[1,0],[0,1]],[[2,1],[1,2]]];
            /* 0111 */ if (z01 == z10 && z01 == z11 && z00 != z01) lines = [[[1,0],[0,1]]];
        });

        lines.forEach(l => {
            turtle.jump(r/2+x0+l[0][0]*r, r/2+y0+l[0][1]*r);
            turtle.goto(r/2+x0+l[1][0]*r, r/2+y0+l[1][1]*r);
        });
    }

    return true;
}

var last_pos = [0, 0];

function walkCurl(i) {
    turtle.jump(last_pos);
    const p = turtle.pos();

    const curl = curlNoise(p[0], p[1]);
    const dest = [p[0]+curl[0], p[1]+curl[1]];
    dest[2] = getRadius(dest);
    
    if (turtle.traveled < maxPathLength && Math.abs(dest[0]) < 110 && Math.abs(dest[1]) < 110 && grid.insert(dest)) {
        turtle.goto(dest);
        turtle.traveled += Math.hypot(curl[0], curl[1]);
    } else {
        turtle.traveled = 0;
        let r, i = 0;
        do { 
            r =[Math.random()*200-100, Math.random()*200-100];
            r[2] = getRadius(r);
            i ++;
        } while(!grid.insert(r) && i < maxTries);
        if (i >= maxTries) {
            return false;
        }
        turtle.jump(r);
    }
    last_pos = turtle.pos();
    return true;
}

function curlNoise(x, y) {
    const eps = 0.1;
    
    const dx = (zFunc([x, y + eps]) - zFunc([x, y - eps]))/(2 * eps);
    const dy = (zFunc([x + eps, y]) - zFunc([x - eps, y]))/(2 * eps);
    
    const l = Math.hypot(dx, dy) * 10;
    return [dx / l, -dy / l];	
}

function getRadius(p2) {
    const l = getLighting(p2, zFunc(p2));
    return (minRadius * l + radius * (1-l)) / 2;
}

function terrace(value) {
    return Math.floor(value * slices);
}

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 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 floor3(v3) { return [ Math.floor(v3[0]), Math.floor(v3[1]), Math.floor(v3[2]) ]; }
function mul_mat2(v2, m22) { return [ v2[0] * m22[0] + v2[1] * m22[1], v2[0] * m22[2] + v2[1] * m22[3] ]; }
function rotation_mat22(a) { var s = Math.sin(a); var c = Math.cos(a); return [c, -s, s, c]; }
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 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; } // IQ's smooth minium function. 


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

// 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[0], p3[2]); // 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 mat2_r2(th)
{
    var a2 = [ Math.sin(1.5707963 + th), Math.sin(th) ];
    return [ a2[0], a2[1], -a2[1], a2[0] ];
}

// Mobius
// Adapted from https://www.shadertoy.com/view/XldSDs
function sdMobius(q3)
{
    const toroidRadius = 0.8; // The object's disc radius.
    const polRot = 0.5; // Poloidal rotations.
    const ringNum = 16; // Number of quantized objects embedded between the rings.
    
    var p3 = [...q3];
    var a = Math.atan2(p3[2], p3[0]);
    
    var xz = [ p3[0], p3[2] ];
    var r2 = mat2_r2(a);
    xz = mul_mat2(xz, r2);
    p3[0] = xz[0]; p3[2] = xz[1];

    p3[0] -= toroidRadius;
    
    var xy = [ p3[0], p3[1] ];
    r2 = mat2_r2(a*polRot);
    xy = mul_mat2(xy, r2);
    p3[0] = xy[0]; p3[1] = xy[1];

    p3 = abs3(sub3(abs3(p3), [.25, .25, .25]));

    var rail = Math.max(Math.max(p3[0], p3[1]) - .07, (Math.max(p3[1]-p3[0], p3[1] + p3[0])*.7071 - .075));
    
    p3 = [...q3];

    var ia = Math.floor(ringNum * a / 6.2831853);  
  	ia = (ia + .5) / ringNum * 6.2831853; 

    xz = [ p3[0], p3[2] ];
    r2 = mat2_r2(ia);
    xz = mul_mat2(xz, r2);
    p3[0] = xz[0]; p3[2] = xz[1];

    p3[0] -= toroidRadius;

    xy = [ p3[0], p3[1] ];
    r2 = mat2_r2(a*polRot);
    xy = mul_mat2(xy, r2);
    p3[0] = xy[0]; p3[1] = xy[1];

    p3 = abs3(p3);
    var ring = Math.max(p3[0], p3[1]);
    ring = Math.max(Math.max(ring - .275, p3[2] - .03), -(ring - .2));
    
    return smin(ring, rail, .03); 
}

// Terrain - doesn't work
function sdTerrain(p3)
{
    //return p3[1];
    
    var yzx = [ p3[1], p3[2], p3[0] ];
    var n = dot3(tri3(add3(mul3(p3,0.3), tri3(mul3(yzx, 0.15)))), [0.44, 0.44, 0.44]);
    p3 = mul3(p3, 1.57);//1.5773;// - n; // The "n" mixes things up more.
    var vec2 = mul_mat2([p3[1], p3[2]], [.866025, .5, -.5, .866025]);
    p3[1] = vec2[0]; p3[2] = vec2[1];
    vec2 = mul_mat2([p3[0], p3[2]], [.866025, .5, -.5, .866025]);
    p3[0] = vec2[0]; p3[2] = vec2[1];
    yzx = [ p3[1], p3[2], p3[0] ];
    n += dot3(tri3(add3(mul3(p3,0.45), tri3(mul3(yzx,0.225)))), [0.222, 0.222, 0.222]);
    
    return smoothstep(0.3, .95, n);
}

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 = 4;
    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 = 4;
    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;
}

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

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

 	var h1 = fbm_terr_r(add3(mul3(q3, 1.50987), [ 1.9489, 1.435, 1.5483 ]), 1.0244, .454, .454);
 	var n1 = smoothstep(.5, 1., h1);

 	var n = n0 * 4 + n1 * 6;

    const height = 30;
    var d = length3(p3) - radius;
 	d -= n * height * factor;
	d -= displacement(q3, 1) * 0.1;
	d -= displacement(q3, 19) * 0.0125;
	return d;
}

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

// Box 2D
function sdBox2(p2, s2)
{
    return sdBox3([p2[0], p2[1], 0], [s2[0], s2[1], 0]);
}

// Box 3D
function sdBox3(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);
}

function map(p3)
{
    var d = MAX_DIST;
    
    d = Math.min(d, sdPlanet(add3(p3, [0, -17 / cam_dist, 0]), 5, 0.01));
    //d = Math.min(d, sdPlanet(add3(p3, [0, 0, 0]), 5, 0.01));

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

function calcNormal(p3) {
    const e = 0.1 * SURF_DIST;
    return normalize3([ 
         map(add3(p3, [e,0,0])) - map(add3(p3, [-e,0,0])),
         map(add3(p3, [0,e,0])) - map(add3(p3, [0,-e,0])),
         map(add3(p3, [0,0,e])) - map(add3(p3, [0,0,-e]))]);
}

function getLighting(p2, dist) {
    const [ro3, rd3] = getRay(p2);
    const p3 = add3(ro3, mul3(rd3, dist));
    const n3 = calcNormal(p3);
    
    // return .5*n3[0]+.5;
    return .5*dot3(n3, [0.9,0,-.436])+.5;
}


function getRay(p2) {
    // Convert to -1 to 1
    var uv2 = [ p2[0] / 100, p2[1] / 100 ];

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

    // Ray origin
    var ro3 = ro3 = [ cam_dist * Math.cos(cam_angle), -0.1, cam_dist * Math.sin(cam_angle) ];

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

// p: 2D point in -100 to 100 range
// Returns 3D position
function zFuncP3(p2)
{
    var dist = Math.hypot(p2[0], p2[1]);
    //if (round_clip) if (dist > 95) return [dist**0.8, dist**0.8, dist**0.8];
    if (round_clip) if (dist > 95) return [MAX_DIST*2,MAX_DIST*2,MAX_DIST*2];

    // Ray direction
    var ro3, rd3;
    [ro3, rd3] = getRay(p2);

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

    if (dist >= MAX_DIST) return [MAX_DIST, MAX_DIST, MAX_DIST];

    // Get intersection point
    var p3 = add3(ro3, mul3(rd3, dist));

    return p3;
}

let cache2 = {}
function key(p2) {
    return [Math.floor(p2[0]*10000), Math.floor(p2[1]*10000)];
}

// p: 2D point in -100 to 100 range
function zFunc(p2) {
    if (round_clip) if (Math.hypot(p2[0], p2[1]) > 95) return MAX_DIST*2;

    const k = key(p2);
    if (k in cache2) return cache2[p2];

    const [ro3, rd3] = getRay(p2);

    // Get distance to intersection
    const d = RayMarch(ro3, rd3);
    
    cache2[k] = d;
    
    return d;
}

////////////////////////////////////////////////////////////////
// Poisson-Disc utility code. Created by Reinder Nijhoff 2019
// https://turtletoy.net/turtle/b5510898dc
////////////////////////////////////////////////////////////////
function PoissonDiscGrid(t){return new class{constructor(t){this.cellSize=1/Math.sqrt(2)/t,this.cells=[],this.queue=[]}insert(t){const e=t[0]*this.cellSize|0,s=t[1]*this.cellSize|0;for(let i=e-1;i<=e+1;i++)for(let e=s-1;e<=s+1;e++){const s=this.cell(i,e);for(let e=0;e<s.length;e++)if((s[e][0]-t[0])**2+(s[e][1]-t[1])**2<(s[e][2]+t[2])**2)return!1}if(this.queue.push([t,e,s]),this.queue.length>10){const t=this.queue.shift();this.cell(t[1],t[2]).push(t[0])}return!0}cell(t,e){const s=this.cells;return(s[t]?s[t]:s[t]=[])[e]?s[t][e]:s[t][e]=[]}}(t)}