tree.ts

examples/fractals/tree.ts · raw · open the example · all examples · site home

Exports: treeKind

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
// A fractal tree with blossoms. Each sample walks from the root down a
// random path of branches (every branch splits into `branches` children,
// turned by the spread about an axis that twists from level to level, the
// first child leading when there are three or more), so the tree is never
// stored: a branch's jitter comes from a hash of its path, the same for
// every sample on it. Wood samples pick their depth from a table weighting
// each level by its branches' length over their width (thin twigs need as
// many splats as thick ones of the same length); blossoms walk to a tip.
// The draw sways each sample by its flex and lets some petals fall.
import { TSL, type TslNode } from "@sparkjsdev/radix";
import { paramUniforms, type BakeUniforms, type Kind } from "./kind.js";
import { Rng, pcg, sampleWriter, storeSample } from "./sample.js";

const { Loop, clamp, cos, cross, dot, float, floor, int, max, min, mix, normalize, select, sin, uint, uniformArray, vec3, vec4 } = TSL;

const MAX_DEPTH = 13;
const TRUNK = { base: -1.1, length: 0.5, radius: 0.05 };

// v turned about the unit axis k by angle a (Rodrigues)
const rotate = (v: TslNode, k: TslNode, a: TslNode) => v.mul(cos(a)).add(cross(k, v).mul(sin(a))).add(k.mul(dot(k, v).mul(float(1).sub(cos(a)))));

export function treeKind(bake: BakeUniforms): Kind {
  const params = {
    depth: { value: 10, min: 4, max: MAX_DEPTH - 1, step: 1 },
    branches: { value: 3, min: 2, max: 4, step: 1 },
    spread: { value: 0.62, min: 0.1, max: 1.4, step: 0.01 },
    lengthRatio: { value: 0.72, min: 0.5, max: 0.9, step: 0.01, label: "length ratio" },
    widthRatio: { value: 0.64, min: 0.4, max: 0.85, step: 0.01, label: "width ratio" },
    twist: { value: 2.4, min: 0, max: 3.14, step: 0.01 },
    droop: { value: 0.12, min: -0.6, max: 0.8, step: 0.01 },
    jitter: { value: 0.45, min: 0, max: 1.5, step: 0.01 },
    blossoms: { value: 0.2, min: 0, max: 0.8, step: 0.01 },
    blossomSize: { value: 0.0075, min: 0.003, max: 0.05, step: 0.001, label: "blossom size" },
    seed: { value: 1, min: 0, max: 999, step: 1 },
  };
  const u = paramUniforms(params);
  const depthTable = uniformArray(new Array(MAX_DEPTH).fill(1), "float");
  let writer: ReturnType<typeof sampleWriter> | undefined;

  const make = () => sampleWriter((index, out) => {
    const rng = new Rng(uint(index).bitXor(pcg(bake.seed)));
    const depth = int(u.depth);
    const blossom = rng.next().lessThan(u.blossoms);
    const x = rng.next();
    const level = int(0).toVar();
    Loop({ start: int(0), end: depth, type: "int", condition: "<" }, ({ i }: { i: TslNode }) => {
      level.addAssign(select(depthTable.element(i).lessThan(x), int(1), int(0)));
    });
    const d = select(blossom, depth, min(level, depth)).toVar();

    const pos = vec3(0, TRUNK.base, 0).toVar();
    const dir = vec3(0, 1, 0).toVar();
    const side = vec3(1, 0, 0).toVar();
    const len = float(TRUNK.length).toVar();
    const rad = float(TRUNK.radius).toVar();
    const path = pcg(uint(u.seed).add(uint(17))).toVar();
    const branches = float(u.branches);
    Loop({ start: int(0), end: d, type: "int", condition: "<" }, ({ i }: { i: TslNode }) => {
      pos.addAssign(dir.mul(len));
      const child = floor(rng.next().mul(branches));
      path.assign(pcg(path.bitXor(uint(child).add(1).mul(uint(2654435761))).add(uint(i))));
      const j1 = float(path.shiftRight(uint(8))).mul(1 / 16777216).sub(0.5).toVar();
      const j2 = float(pcg(path).shiftRight(uint(8))).mul(1 / 16777216).sub(0.5).toVar();
      const azimuth = child.mul(Math.PI * 2).div(branches).add(float(i).mul(u.twist)).add(j1.mul(u.jitter).mul(1.5));
      const axis = normalize(rotate(side, dir, azimuth)).toVar();
      const leads = child.lessThan(0.5).and(branches.greaterThan(2.5));
      const tilt = u.spread.mul(select(leads, float(0.22), float(1))).mul(j2.mul(u.jitter).add(1));
      const turned = normalize(rotate(dir, axis, tilt).add(vec3(0, u.droop.negate().mul(float(i).add(1)).div(float(depth)), 0))).toVar();
      side.assign(normalize(cross(turned, axis)));
      dir.assign(turned);
      len.mulAssign(u.lengthRatio.mul(j1.mul(u.jitter).mul(0.4).add(1)));
      rad.mulAssign(u.widthRatio);
    });

    const t = rng.next();
    const flex = float(d).add(t).div(float(depth)).toVar();
    const wood = {
      pos: pos.add(dir.mul(len.mul(t))),
      size: max(rad.mul(float(1).sub(t.mul(0.35))), 0.0018),
    };
    const size = select(blossom, u.blossomSize.mul(rng.next().mul(0.7).add(0.5)), wood.size).toVar();
    const tip = pos.add(dir.mul(len)).add(rng.ball().mul(u.blossomSize.mul(3)));
    const falls = rng.next().lessThan(0.03);
    const motion = select(blossom, select(falls, rng.next().add(0.01).negate(), float(1)), flex);
    const shade = mix(float(0.55), float(1), clamp(flex, 0, 1));

    storeSample(out, {
      pos: vec4(select(blossom, tip, wood.pos), 0),
      normal: select(blossom, rng.direction(), dir),
      shape: select(blossom, float(0.35), clamp(len.mul(0.2).div(size), 1.01, 7)),
      trapA: select(blossom, rng.next().mul(0.25).add(0.75), float(d).div(float(depth)).mul(0.14).add(0.06)),
      trapB: select(blossom, rng.next(), float(0)),
      ao: select(blossom, float(1), shade),
      shadow: float(1),
      motion,
      lit: select(blossom, float(0.8), float(1)),
      size,
      alpha: float(1),
    });
  });

  return {
    id: "tree",
    label: "Fractal tree",
    family: "tree",
    params,
    uniforms: u,
    writer: () => (writer ??= make()),
    prepare: (values) => {
      const depth = Math.round(values.depth);
      const weights = Array.from({ length: depth + 1 }, (_, level) => {
        const length = TRUNK.length * values.lengthRatio ** level;
        const radius = Math.max(TRUNK.radius * values.widthRatio ** level, 0.0018);
        return Math.round(values.branches) ** level * (length / radius);
      });
      const total = weights.reduce((sum, w) => sum + w, 0);
      let cumulative = 0;
      const table = depthTable.array as number[];
      table.fill(1);
      weights.forEach((w, level) => {
        cumulative += w / total;
        table[level] = cumulative;
      });
    },
    look: { flatness: 1, pulse: 0, twinkle: 0 },
  };
}