NOTE: This site has just upgraded to Forester 5.x and is still having some style and functionality issues, we will fix them ASAP.

test 3D Bifurcation Diagram [uts-001C]

2025-06-06 UPDATE: Known to be broken for now. Errors:

```
Uncaught (in promise) JsValue(OperationError: Failed to execute 'requestDevice' on 'GPUAdapter': The limit "maxInterStageShaderComponents" with a non-undefined value is not recognized.
OperationError: Failed to execute 'requestDevice' on 'GPUAdapter': The limit "maxInterStageShaderComponents" with a non-undefined value is not recognized.
```

Adapted from :

#storage atomic_storage array<atomic<i32>>

const MaxSamples = 256.0;
const FOV = 0.6;
const PI = 3.14159265;
const TWO_PI = 6.28318530718;
const STEP = 0.01;
const LARGENUM = 1e10;
const ATOMIC_SCALE = 1024.0;

struct Camera
{
  pos: float3,
  cam: float3x3,
  fov: float,
  size: float2
}

struct Ray
{
    ro: float3,
    rd: float3,
}

var<private> camera : Camera;
var<private> state : uint4;

fn pcg4d(a: uint4) -> uint4
{
	var v = a * 1664525u + 1013904223u;
    v.x += v.y*v.w; v.y += v.z*v.x; v.z += v.x*v.y; v.w += v.y*v.z;
    v = v ^  ( v >> uint4(16u) );
    v.x += v.y*v.w; v.y += v.z*v.x; v.z += v.x*v.y; v.w += v.y*v.z;
    return v;
}

fn rand4() -> float4
{
    state = pcg4d(state);
    return float4(state)/float(0xffffffffu);
}

fn nrand4(sigma: float, mean: float4) -> float4
{
    let Z = rand4();
    return mean + sigma * sqrt(-2.0 * log(Z.xxyy)) *
           float4(cos(TWO_PI * Z.z),sin(TWO_PI * Z.z),cos(TWO_PI * Z.w),sin(TWO_PI * Z.w));
}

fn udir(rng: float2) -> float3
{
    let r = float2(2.*PI*rng.x, acos(2.*rng.y - 1.0));
    let c = cos(r);
    let s = sin(r);
    return float3(c.x*s.y, s.x*s.y, c.y);
}

fn disk(rng: float2) -> float2
{
    return float2(sin(TWO_PI*rng.x), cos(TWO_PI*rng.x))*sqrt(rng.y);
}

fn Rotate(t: float) -> float2x2
{
    return float2x2(
        cos(t), sin(t),
      - sin(t), cos(t),
    );
}

fn RotXY(x: float3, t: float) -> float3
{
    return float3(Rotate(t)*x.xy, x.z);
}

fn GetCameraMatrix(ang: float2) -> float3x3
{
    let x_dir = float3(cos(ang.x)*sin(ang.y), cos(ang.y), sin(ang.x)*sin(ang.y));
    let y_dir = normalize(cross(x_dir, float3(0.0,1.0,0.0)));
    let z_dir = normalize(cross(y_dir, x_dir));
    return float3x3(-x_dir, y_dir, z_dir);
}

fn SetCamera()
{
    let screen_size = int2(textureDimensions(screen));
    let screen_size_f = float2(screen_size);
    let ang = float2(mouse.pos.xy)*float2(-TWO_PI, PI)/screen_size_f + float2(0.4, 0.4);

    camera.fov = FOV;
    camera.cam = GetCameraMatrix(ang);
    camera.pos = - (camera.cam*float3(8.0*custom.Radius+0.5,0.0,0.0));
    camera.size = screen_size_f;
}

//project to clip space
fn Project(cam: Camera, p: float3) -> float3
{
    let td = distance(cam.pos, p);
    let dir = (p - cam.pos)/td;
    let screen = dir*cam.cam;
    return float3(screen.yz*cam.size.y/(cam.fov*screen.x) + 0.5*cam.size,screen.x*td);
}

const max_iterations = 256;
const color_thresholds = float4(255.0, 130.0, 80.0, 255.0);


fn AdditiveBlend(color: float3, depth: float, index: int)
{
    let scaledColor = int3(floor(ATOMIC_SCALE*color/(depth*depth + 0.2) + rand4().xyz));

    if(scaledColor.x>0)
    {
        atomicAdd(&atomic_storage[index*4+0], scaledColor.x);
    }

    if(scaledColor.y>0)
    {
        atomicAdd(&atomic_storage[index*4+1], scaledColor.y);
    }

    if(scaledColor.z>0)
    {
        atomicAdd(&atomic_storage[index*4+2], scaledColor.z);
    }
}

fn RasterizePoint(pos: float3, color: float3)
{
    let screen_size = int2(camera.size);
    let projectedPos = Project(camera, pos);
    let screenCoord = int2(projectedPos.xy+0.5*rand4().xy);

    //outside of our view
    if(screenCoord.x < 0 || screenCoord.x >= screen_size.x ||
        screenCoord.y < 0 || screenCoord.y >= screen_size.y || projectedPos.z < 0.0)
    {
        return;
    }

    let idx = screenCoord.x + screen_size.x * screenCoord.y;
    AdditiveBlend(color, projectedPos.z, idx);
}


fn saturate(x: f32) -> f32 {
    return min(1.0, max(0.0, x));
}

fn saturate_vec3(x: vec3<f32>) -> vec3<f32> {
    return min(vec3<f32>(1.0, 1.0, 1.0), max(vec3<f32>(0.0, 0.0, 0.0), x));
}

fn bump3y(x: vec3<f32>, yoffset: vec3<f32>) -> vec3<f32> {
    var y: vec3<f32> = vec3<f32>(1.0, 1.0, 1.0) - x * x;
    y = saturate_vec3(y - yoffset);
    return y;
}

fn spectral_zucconi(w: f32) -> vec3<f32> {
    let x: f32 = saturate((w - 400.0) / 300.0);

    let cs: vec3<f32> = vec3<f32>(3.54541723, 2.86670055, 2.29421995);
    let xs: vec3<f32> = vec3<f32>(0.69548916, 0.49416934, 0.28269708);
    let ys: vec3<f32> = vec3<f32>(0.02320775, 0.15936245, 0.53520021);

    return bump3y(cs * (x - xs), ys);
}

fn hue(v: float) -> float3 {
    return .6 + .6 * cos(6.3 * v + float3(0.,23.,21.));
}

fn bifurcation(iters: i32) {
    var p = rand4().xyz * float3(4.0, 4.0, 5.0) - float3(2.0, 2.0, -0.25);
    let s = rand4().x;

    let alpha = custom.Alpha;
    let beta = custom.Beta + custom.BetaAnim*(0.5 * sin(time.elapsed) + 0.5) + custom.BetaS*s;


    for (var j: i32 = 0; j <= iters; j = j + 1) {
        let p0 = p;
        p.x = p.z - p0.y*(beta * p0.x + (1.0 - beta) * p0.y);
        p.y = p0.x + p0.y * p0.y * alpha;

        if(j < iters - int(custom.Samples*MaxSamples + 1.0)) {continue;}
        var color = spectral_zucconi(350 + 350.0*s);
        color = pow(color, vec3(1.0));
        RasterizePoint(float3(1,1,2.5)*(p - float3(0.5, 0.5, 1.5)), 32.0*color/(custom.Samples*MaxSamples + 1.0));
    }
}

@compute @workgroup_size(16, 16)
fn Clear(@builtin(global_invocation_id) id: uint3) {
    let screen_size = int2(textureDimensions(screen));
    let idx0 = int(id.x) + int(screen_size.x * int(id.y));

    atomicStore(&atomic_storage[idx0*4+0], 0);
    atomicStore(&atomic_storage[idx0*4+1], 0);
    atomicStore(&atomic_storage[idx0*4+2], 0);
    atomicStore(&atomic_storage[idx0*4+3], 0);
}

@compute @workgroup_size(16, 16)
fn Rasterize(@builtin(global_invocation_id) id: uint3)
{
    SetCamera();

    //RNG state
    state = uint4(id.x, id.y, id.z, uint(custom.NoiseAnimation)*time.frame);

    bifurcation(int(MaxSamples*2.0));
}

fn Sample(pos: int2) -> float3
{
    let screen_size = int2(textureDimensions(screen));
    let idx = pos.x + screen_size.x * pos.y;

    var color: float3;

    let x = float(atomicLoad(&atomic_storage[idx*4+0]));
    let y = float(atomicLoad(&atomic_storage[idx*4+1]));
    let z = float(atomicLoad(&atomic_storage[idx*4+2]));

    color = float3(x,y,z)/ATOMIC_SCALE;

    return abs(color);
}

@compute @workgroup_size(16, 16)
fn FinalPass(@builtin(global_invocation_id) id: uint3)
{
    let screen_size = uint2(textureDimensions(screen));

    // Prevent overdraw for workgroups on the edge of the viewport
    if (id.x >= screen_size.x || id.y >= screen_size.y) { return; }

    // Pixel coordinates (centre of pixel, origin at bottom left)
    let fragCoord = float2(float(id.x) + .5, float(id.y) + .5);


    let oldColor = textureLoad(pass_in, int2(id.xy), 0, 0);

    var color = float4(Sample(int2(id.xy)), 1.0);


    if(mouse.click != 1)
    {
       color += oldColor * custom.Accumulation;
    }

    let exposed = 1.0 - exp(-5.0*custom.Exposure*color.xyz/color.w);

    // Output to buffer
    textureStore(pass_out, int2(id.xy), 0, color);


    textureStore(screen, int2(id.xy), float4(exposed, 1.));
}