splat.ts

examples/fxedit/fx/nodes/splat.ts · raw · open the example · all examples · site home

Exports: splatNodes

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
// The output node: writes the splat. Every input is optional; an absent input
// leaves that part of the splat as it was. Writes are plain statements on the
// splat var (a Fn whose result is unused is never built), in this order:
// center (set), offset (add), scale (multiply), size / sizeMix (blend toward
// a sphere), blur (add a size in quadrature, keeping total weight), rgb
// (set), alpha (set), opacity (multiply), keep (false: opacity 0, which the
// projection culls before the sort).
import { TSL, type TslNode } from "@sparkjsdev/radix";
import type { NodeDef, PortType } from "../types.js";

const { float, max, select, sqrt } = TSL;

const optional = (type: PortType, value: number | number[] | boolean, min?: number, max?: number) => ({ type, default: value, optional: true, min, max });

export const splatNodes: NodeDef[] = [
  {
    id: "splat.write",
    label: "Splat output",
    description: "Writes the splat: move it, recolor it, fade, resize or remove it",
    category: "splat",
    inputs: {
      center: optional("vec3", [0, 0, 0], -1, 1),
      offset: optional("vec3", [0, 0, 0], -1, 1),
      scale: optional("float", 1, 0, 4),
      size: optional("float", 0.01, 0, 0.2),
      sizeMix: optional("float", 1, 0, 1),
      blur: optional("float", 0, 0, 0.2),
      rgb: optional("color", [1, 1, 1]),
      alpha: optional("float", 1, 0, 1),
      opacity: optional("float", 1, 0, 1),
      keep: optional("bool", true),
    },
    outputs: {},
    output: true,
    build: ({ center, offset, scale, size, sizeMix, blur, rgb, alpha, opacity, keep }, { splat }) => {
      if (center) {
        splat.get("center").assign(center);
      }
      if (offset) {
        splat.get("center").addAssign(offset);
      }
      if (scale) {
        splat.get("scales").mulAssign(max(scale, 0.0));
      }
      if (sizeMix) {
        // Blend toward a sphere of radius `size` in quadrature
        const m = sizeMix.clamp(0, 1).toVar();
        const keepOwn = float(1).sub(m).pow(2).toVar();
        const target = (size ?? float(0.01)).max(1e-6).mul(m).pow(2).toVar();
        const scales = splat.get("scales");
        scales.assign(sqrt(scales.mul(scales).mul(keepOwn).add(target)));
      }
      let fade: TslNode | undefined;
      if (blur) {
        // Convolve with an isotropic gaussian of std-dev `blur`: variances
        // add, and opacity keeps the footprint's total weight (the ratio of
        // determinants over the two largest axes, as a 2D projection sees)
        const b2 = max(blur, 0.0).pow(2).toVar();
        const variance = splat.get("scales").mul(splat.get("scales")).toVar();
        const ratio = variance.div(variance.add(b2).max(1e-12)).toVar();
        fade = sqrt(ratio.x.mul(ratio.y).mul(ratio.z).div(ratio.x.min(ratio.y).min(ratio.z).max(1e-8))).toVar();
        splat.get("scales").assign(sqrt(variance.add(b2)));
      }
      if (rgb) {
        splat.get("rgb").assign(rgb);
      }
      if (alpha) {
        splat.get("opacity").assign(alpha.clamp(0, 1));
      }
      if (opacity) {
        splat.get("opacity").mulAssign(opacity);
      }
      if (fade) {
        splat.get("opacity").mulAssign(fade);
      }
      if (keep) {
        splat.get("opacity").assign(select(keep, splat.get("opacity"), float(0)));
      }
      return {};
    },
  },
];