Value graphs¶
An effect can drive any of its numeric inputs from a small value graph that the client evaluates once per frame. The block is optional and additive: a definition without graph/inputs behaves exactly as before, and a mod that does not know graphs ignores them entirely, because graph wiring lives in a separate top-level inputs block (or in graph edges) and never inside params.
{
"type": "blur",
"duration": 400,
"loop": true,
"params": { "radius": 2.0 },
"graph": {
"version": 1,
"nodes": [
{ "id": "phase", "kind": "time", "inputs": { "speed": 0.25 } },
{ "id": "pulse", "kind": "curve", "inputs": {
"points": [
{ "time": 0, "value": 0.0 },
{ "time": 50, "value": 8.0 },
{ "time": 100, "value": 0.0 }
]
} }
],
"edges": [ { "from": "phase", "to": "pulse", "input": "time" } ],
"meta": { "phase": { "pos": [40, 60] }, "pulse": { "pos": [200, 60] } }
},
"inputs": { "radius": { "from": "pulse" } }
}
graph fields:
| Field | Type | Default | Description |
|---|---|---|---|
version |
int | 1 | Format version. Only 1 exists — any other value is a per-file error. |
nodes |
array | — (required) | 1–128 node objects. |
edges |
array | [] |
0–512 edges (node→node or node→effect input). |
meta |
object | — | Editor-only info (canvas positions, groups, comments). The engine ignores it; it may hold anything. |
Every node has id (string, unique in the graph), kind (one of the kinds below) and an inputs object whose values are either a number or { "from": "<node-id>" }.
kind |
Inputs (default) | Result |
|---|---|---|
constant |
value (0) |
The literal value. |
time |
speed (1), offset (0) |
elapsed × speed + offset; elapsed is the effect's time in ticks. |
random |
min (0), max (1), index (0) |
A stable per-instance random number in min..max (the same every frame for one index; animating index re-rolls). |
noise |
x (elapsed), y (0), z (0), scale (1), octaves (1), gain (0.5), lacunarity (2) |
3D simplex noise, roughly −1..1; octaves is clamped to 1–8. |
curve |
points (required), time (elapsed) |
The curve sampled at time. |
math |
a, b (both required), top-level op |
a op b; op is add, subtract, multiply, divide, min, max, pow or mod (divide/mod by zero → 0). |
mix |
a, b (both required), factor (0.5) |
a + (b − a) × factor. |
clamp |
value (required), min (0), max (1) |
value clamped into min..max. |
remap |
value (required), in_min (0), in_max (1), out_min (0), out_max (1) |
Linear remap from the input range to the output range. |
bind |
a world/camera binding as the node's inputs, plus optional fallback |
The same value a bound param would produce. |
expr |
expr (required string, ≤1024 chars) |
The math expression from Expressions (expr) (same t/x/y/z variables and functions), compiled per instance and evaluated every frame. |
A curve node's points are { "time": <ticks>, "value": <number>, "easing": "<name>" } (up to 64, times strictly ascending; easing is optional and defaults to linear). A point's easing eases the segment from that point to the next — the last point's easing is unused — exactly like keyframes.
Node-to-node edges: { "from": "<node>", "to": "<node>", "input": "<socket>" }.
Wiring an effect input. Two equivalent ways:
- the top-level
inputsblock —"inputs": { "radius": { "from": "pulse" } }; - a graph edge whose
tois the effect input instead of a node —{ "from": "pulse", "to": "radius" }(noinputfield).
A numeric literal in inputs ("inputs": { "radius": 6 }) is also accepted and overrides a params entry of the same name. Wiring the same input twice (both ways, or twice in the edge list) is a parse error. Every effect input keeps its numeric default — an input wired but not declared in params gets a constant 0.0.
Caps and errors. version must be 1; at most 128 nodes, 512 edges and a chain depth of 32; expr source at most 1024 characters; at most 64 curve points; octaves clamped to 8. A graph that breaks any rule fails that file only, with an error naming the node id and input — /vfx validate lists it and the rest of the pack keeps loading.
Reference example. The built-in vfxweaver:graph_demo is exactly the definition above: /vfx play vfxweaver:graph_demo plays a blur whose radius follows a time → curve pulse. Built-in ids need their namespace; there is no minecraft: fallback.
Subgraphs (macros)¶
An effect file may declare reusable node blocks under a top-level subgraphs array. A graph node
of kind subgraph stamps one in; the loader expands it before the graph is validated, so macros
cost nothing per frame.
{
"subgraphs": [
{
"id": "fade_noise",
"inputs": { "scale": 1.0, "speed": 1.0 },
"nodes": [
{ "id": "s1", "kind": "time", "inputs": { "speed": "$speed" } },
{ "id": "s2", "kind": "noise", "inputs": { "scale": "$scale", "octaves": 3 } },
{ "id": "s3", "kind": "curve", "inputs": { "points": [ { "time": 0, "value": 1.0 },
{ "time": 100, "value": 0.0 } ] } }
],
"edges": [ { "from": "s2", "to": "s3", "input": "a" } ],
"outputs": { "out": "s3" }
}
],
"graph": {
"nodes": [ { "id": "n1", "kind": "subgraph", "subgraph": "fade_noise",
"inputs": { "scale": 2.0, "speed": 0.5 } } ],
"edges": [ { "from": "n1", "to": "radius" } ]
}
}
inputson a subgraph declares parameters with defaults. A node value that is the whole string"$name"is replaced by the instance's binding (or the default)."$name"is only recognised as a whole node-input value; it is never substituted insideexprtext or insidepoints.- Node ids inside a subgraph are local. On expansion they are prefixed with the instance id
(
n1.s2), so the same macro can be used twice in one graph without collisions. outputsis a non-empty map of output name to a local node id.outputsorder matters: the first entry is the default output. An edge may select one with{ "from": "n1", "output": "intensity", "to": "beam.intensity" }; with one output, or whenoutputis omitted, the first declared output is used. An effectinputsentry{ "from": "n1" }always uses the first output.- Subgraphs may nest and may not reference themselves, directly or transitively. The number of subgraph definitions is capped at 64 and nesting at 8; exceeding either fails that file only. The expanded node and edge counts must stay within the graph caps (128 / 512).
- A fault inside a macro names both ends, e.g.
subgraph 'fade_noise' (node 'n1') → node 's3': input 'a' is not connected and has no default. - A
subgraphnode without asubgraphsblock is an error; macros are defined in the same file in this version.
Logic nodes¶
Four kinds produce a value from comparisons and selection. Booleans are 1.0 (true) and 0.0
(false); any non-zero input is true.
| kind | structural field | inputs | result |
|---|---|---|---|
compare |
op: eq, ne, lt, le, gt, ge |
a, b (both required) |
1.0 or 0.0 |
boolean |
op: and, or, xor, not |
a (required); b required for and/or/xor, unused for not |
1.0 or 0.0 |
if |
— | condition (required), then, else (default 0) |
then when condition is non-zero, else else |
switch |
— | index (required), case_0, case_1, …, default (default 0) |
case_<round(index)> when present, else default |
{ "id": "gate", "kind": "compare", "op": "gt", "inputs": { "a": { "from": "level" }, "b": 0.5 } },
{ "id": "out", "kind": "if", "inputs": { "condition": { "from": "gate" }, "then": 1.0, "else": 0.0 } }
if, switch, boolean and and boolean or evaluate only the branch that can change the
result; the other side is never computed.
The built-in vfxweaver:graph_logic_demo combines both: a pulse macro (a time/math/compare/
remap/if chain with a $period/$peak parameter) drives the blur's radius, ramping it up for
the first half of each $period and holding it at 0 for the second.
Masks are implemented (see Masks): an optional top-level mask block restricts where a post-processing effect applies, evaluated once per frame in a coverage prepass at screen layer 0.
Showcase¶
show_graph_demo¶
Its datapack definition:
{
"type": "blur",
"duration": 200,
"loop": true,
"persistent": true,
"fade_ticks": 20,
"params": {
"radius": 2.0
},
"graph": {
"version": 1,
"nodes": [
{
"id": "phase",
"kind": "time",
"inputs": {
"speed": 0.5
}
},
{
"id": "pulse",
"kind": "curve",
"inputs": {
"points": [
{
"time": 0,
"value": 0.0
},
{
"time": 50,
"value": 8.0
},
{
"time": 100,
"value": 0.0
}
]
}
}
],
"edges": [
{
"from": "phase",
"to": "pulse",
"input": "time"
}
],
"meta": {
"phase": {
"pos": [
40,
60
]
},
"pulse": {
"pos": [
200,
60
]
}
}
},
"inputs": {
"radius": {
"from": "pulse"
}
}
}
show_graph_logic_demo¶
Its datapack definition:
{
"type": "blur",
"duration": 160,
"loop": true,
"persistent": true,
"fade_ticks": 20,
"params": {
"radius": 0.0
},
"subgraphs": [
{
"id": "pulse",
"inputs": {
"period": 160.0,
"peak": 8.0,
"duty": 0.5
},
"nodes": [
{
"id": "t",
"kind": "time",
"inputs": {
"speed": 1.0
}
},
{
"id": "phase",
"kind": "math",
"op": "mod",
"inputs": {
"a": {
"from": "t"
},
"b": "$period"
}
},
{
"id": "half",
"kind": "math",
"op": "multiply",
"inputs": {
"a": "$period",
"b": "$duty"
}
},
{
"id": "on",
"kind": "compare",
"op": "lt",
"inputs": {
"a": {
"from": "phase"
},
"b": {
"from": "half"
}
}
},
{
"id": "ramp",
"kind": "remap",
"inputs": {
"value": {
"from": "phase"
},
"in_min": 0.0,
"in_max": 80.0,
"out_min": 0.0,
"out_max": "$peak"
}
},
{
"id": "out",
"kind": "if",
"inputs": {
"condition": {
"from": "on"
},
"then": {
"from": "ramp"
},
"else": 0.0
}
}
],
"outputs": {
"amount": "out",
"raw": "ramp"
}
}
],
"graph": {
"version": 1,
"nodes": [
{
"id": "n1",
"kind": "subgraph",
"subgraph": "pulse",
"inputs": {
"period": 160.0,
"peak": 8.0
}
}
],
"edges": [
{
"from": "n1",
"output": "amount",
"to": "radius"
}
]
},
"meta": {
"n1": {
"pos": [
60,
60
]
}
}
}