Changelog¶
Format follows Keep a Changelog.
This page is the authoritative record of released mod versions. The newest entry is the current
mod version (2.5.0, read from mod_version in the build). Each ## heading names a release; where
a release shipped with a guide revision the heading shows both numbers (v1.2.0 / Guide v32), and a
heading titled only Guide vN is a guide change that shipped without a mod version bump. The two
numbers are independent: the mod version is what you download, the guide revision is how many
times this documentation has been revised. The authoritative record of guide revisions is the
Guide changelog. Which jar to download for your Minecraft line and loader is in
the download table. Add new entries at the top, in the same PR as the behaviour
change.
Unreleased¶
Added¶
- A composition, built entirely by a datapack:
compositions/rift. A 16-block disc of dark stone hanging at y=200, an obelisk narrowing to a single amethyst column, six standing stones with wide light beams, seven turned blocks floating above it (block_display entities, a shape no builder can make from blocks), an allay held still inside the light and outlined by the mod, and a bridge to a watching platform. Nothing is placed by hand:/function rift:goclears, builds 1500 blocks, summons the entities, teleports the viewer, sets the time and starts onecollectionthat stages nine effect definitions in the order the eye should find them./function rift:clearremoves all of it, so the scene is repeatable. The colour is carried by two masks over one sphere:grade_outdesaturates everything outside it (invert),spectraladds a prism smear inside it, and both usevolume: "aura"so the boundary is a sphere of light rather than a coat of paint on the blocks - the colour boundary is the mask edge. The geometry is generated (compositions/rift/generate.py) because a radial floor is unreadable as hand-written setblocks; the effect definitions are ordinary vfx JSON. Two new checks:scripts/check-composition.py(every function target and effect id exists, every command has the right arity, and every placed block sits inside a boxclearerases) andscripts/check-showcase-pack-parse.ps1 -Dir(the same parse sweep, pointed at any pack; it reports 11 parsed, 0 failed for this one). No effect type, param, UBO layout orPROTOCOL_VERSIONchanged - the composition is content. Documented in the guide. Not verified in game: the JSON parses and the functions pass the static checks, but nobody has stood on the platform yet.
Changed¶
- The showcase datapack covers the 2.5.0 effects, and its source lives in the repository. The published pack (
vfx-demos-showcase.zip, linked from the guide) was built before this release, so it had no demo fordatamosh,torn_edge,spectral,hex_grid,heartbeat,hum,noiseorvolume_duck- eight of the effects a reader is most likely to try first. The pack now ships all eight asshow_*definitions in the same looping, recordable shape as the rest (82 definitions in total), and the recording cheat sheet inside the zip lists them. The pack is no longer a binary with no source: it is theshowcase/directory of the repository, packed byscripts/build-showcase-pack.pywith forward-slash entry names and fixed timestamps, so an unchanged pack repacks byte-identically.scripts/check-showcase-pack-parse.ps1parses every definition in the pack throughVFXDefinition.parse, the same guard the built-ins get. Additive: no definition is renamed, no effect parameter changes, and the mod itself is untouched.
2.5.0 - 2026-10-22¶
Added¶
hex_grid: a triangular grid coloured by the screen position. A new screen effect draws a triangular lattice over the frame - three line directions (two at 60 degrees, one vertical) on acell_sizecell (46 px by default) anchored at the screen centre - and paints the lines with the prism ramp picked by the position (prism7(x * 0.6 + y * 0.4 + time * speed), drifting at 0.5 per second): the lines at 28 % of that colour over near-black(0.01, 0.008, 0.02), and the highlights of the picture (brighter thancore, 0.68 by default - absolute, not local, so no outline appears at a brightness step) in white;amountcross-fades the whole look (default1.0, neutral0.0sofade_ticksfades it) and no param has an upper cap - only the lower guards that avoid degenerate maths (cell_size >= 1px,width >= 0.0001). Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change,PROTOCOL_VERSIONunchanged. Documented in the guide. Asserted byscripts/check-vfx-schema.ps1,scripts/check-shader-identifiers.ps1andscripts/check-builtins-parse.ps1; the GLSL/pixels cannot be verified headless here. Not verified in game.spectral: a rainbow prism look over the frame. A new screen effect draws the picture through a seven-stop prism ramp - red(1.0, 0.07, 0.1), orange(1.0, 0.42, 0.03), yellow(1.0, 0.88, 0.06), green(0.1, 0.95, 0.28), blue(0.06, 0.48, 1.0), violet(0.28, 0.14, 1.0), magenta(0.74, 0.16, 1.0)- swept by the angle aroundcenter_x/center_y, the distance from it (0.9 per screen height) and the clock (0.7 per second);modepicks the variant: 0 the prism duotone (the frame is split into a dark and a bright half by its own brightness,mid0.5 by default - absolute, not local, so no outline appears at a brightness step; the bright half takes the prism colour and the highlights,core0.68, go white), 1 the chromatic smear (seven taps along the outward direction, spread by0.0035 + 0.004 * distance, every tap's edgesmoothstep(0.02, 0.30)painted with its own prism stop - a wide ramp, so it stays a smear and not a drawn outline) and 2 the spectral ring (the luminance picks a bandfloor(luma * 6.999), the distance picks a ringfloor(distance * 9 - time * 6), the two together pick a spectral stop and the highlights (core0.68) go white;amountcross-fades the whole look (default1.0, neutral0.0sofade_ticksfades it),modeis an enum clamped to 0..2 and no other param has an upper cap. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change,PROTOCOL_VERSIONunchanged. Documented in the guide. Asserted byscripts/check-vfx-schema.ps1,scripts/check-shader-identifiers.ps1andscripts/check-builtins-parse.ps1; the GLSL/pixels cannot be verified headless here. Not verified in game.torn_edge: a ragged tear with a glowing line down it. A new screen effect cuts the frame along a straight line throughcenter_x/center_ywhose angle is re-rolled every beat (speed, 20 per second by default): a value-noise ripple ofragged(0.02 of the screen height, 38 cycles across the tear) makes the edge wander, the two sides of the cut are drawn at opposite brightness (a pixel brighter thanmid, 0.5 by default - the split is absolute, not local, so no outline appears at a brightness step), and the tear itself is a white line (an exponential falloff ofglow, 0.0035 of the screen height, scaled by 1.6);amountcross-fades the whole look (default1.0, neutral0.0sofade_ticksfades it), and no param has an upper cap - only the lower guards that avoid degenerate maths (speed >= 0,glow >= 0.0005). Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change,PROTOCOL_VERSIONunchanged. Documented in the guide. Asserted byscripts/check-vfx-schema.ps1,scripts/check-shader-identifiers.ps1andscripts/check-builtins-parse.ps1; the GLSL/pixels cannot be verified headless here. Not verified in game.datamosh: the frame is decoded against a reference that never arrives, and the movement search runs once per cell instead of once per pixel. A new screen effect is a feedback effect: it keeps the picture it produced last frame in the frame's history buffer, and that history is the reference the decoder gets instead of a key frame. The key frame never arrives: the history only refreshes through the per-piece I-frame below, so the decoder keeps painting the screen as it was half a second ago. The movement is estimated the way a codec searches its reference frame for the matching block: one Lucas-Kanade step between the live picture and the reference (the reference carries the previous live frame's luma in its alpha) gives the velocity of the content, and a greedy search over 8, 4, 2 and 1 pixel finds the integer vector that best matches it, with a penalty that gives exactly zero where nothing moves. The search used to run per pixel (about 174 texture reads per pixel, a full-resolution pass the frame rate could not afford): every pixel of a piece needs the same vector, so the search now runs in its own pass at ⅛ resolution - one texel per 8x8 cell - and the update pass reads the result back. The picture is unchanged, the cost is about 6.5 reads per pixel instead of 174. The decoder then carries the pixels over as they are (point sampled, no filter, no leftover of the live frame), so a moving object drags sharp pixels of the old background with it; the luma is taken at the measured vector and the colour at1 + chromatimes it, the way a 4:2:0 stream smears colour when it is decoded against the wrong reference - every colour is the picture's own, there is no random noise. The pieces come from a static hash quadtree (8 to 64 px), so neighbouring pieces differ in size and the layout never changes, and each piece returns to the real picture on its own phase everyiframeseconds, so there is no global flash.smear(1) is the vector multiplier andchroma(0.5) the colour pull; only the lower guards exist (chromaandamountclamped to 0..1,smearandiframenot negative), no param has an upper cap. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change,PROTOCOL_VERSIONunchanged. Honest limits, stated on the page: the melt lives in the same history buffer asafterimage, so the two chain when both run and each overwrites the alpha the other needs; the reference is the screen's own past rather than another shot; the movement needs some structure in the picture, so in flat areas (sky, a plain wall) nothing is dragged; and the motion field is one texel per 8x8 cell, so a slower movement than 8 px is not resolved. Documented in the guide. Asserted byscripts/check-vfx-schema.ps1,scripts/check-shader-identifiers.ps1,scripts/check-post-fusion.ps1andscripts/check-builtins-parse.ps1; the GLSL/pixels cannot be verified headless here. Not verified in game.heartbeat: a heartbeat positioned in the world. A new audio effect plays a recording of one lub-dub that ships inside the mod (ratebeat rate in bpm, default60;volumelinear gain0..1, neutral0.0sofade_ticksfades it;pitchplayback-speed multiplier, default1= as recorded) straight into the OS mixer — no resource pack needed, identical on every Minecraft line. Every beat restarts the recording, and the recording is time-scaled to the tempo: at 60 bpm it plays as recorded (0.85 s in a 1 s beat) and at 140 bpm it is compressed to about 0.36 s in a 0.43 s beat, with the pause inside the lub-dub closing up in the same proportion. The scaling keeps the tone where it was, so a fast heart is a fast recording on the same pitch rather than a higher one;pitchis a separate tone multiplier that does move the pitch; the pause between beats is whatever the recording leaves free, not a separate re-timing. A cut playback is faded over 5 ms so a restart never clicks, andrateandpitchare read fresh every tick, so keyframes/expr/bindings ride them live and the phase integrates per sample, so a rate change glides. Positioning, distance falloff (full within 8 blocks, silent past 48), clock semantics (pause freezes, seek resyncs silently), fade-out on single stop and immediatestopAll/world exit/client shutdown close all mirror thehum/noisevoices; the same honest limits apply (vanilla mixer bypassed, no-audio-device stays silent with a one-time warning). Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change,PROTOCOL_VERSIONunchanged, no new dependency. Documented in the guide. Asserted byscripts/check-vfx-schema.ps1andscripts/check-builtins-parse.ps1; audible output cannot be verified headless here. Not verified in game.volume_duck: scale a vanilla sound category down while active. A new global (non-positional) audio effect sets one game-options category level to its pre-duck level timesvolume(0..1, neutral1.0sofade_ticksfades the duck in and out), selected by thechannelparam as a case-insensitive name (master,music,records,weather,blocks,hostile,neutral,players,ambient,voice,ui) or the historicalSoundSourceordinal (0..10, back-compat); an unknown name warns once and safely no-ops instead of ducking the wrong category. Each frame only ducked channels are written (base x last-writer-wins scale across stacked ducks); the captured level is restored the frame a channel's last duck ends and on single stop, replay seek,stopAll, world exit and client shutdown, and nothing is ever written tooptions.txt. Honest limits, stated on the page: the options slider shows the scaled level and a mid-duck slider drag on that channel is overwritten, then lost on restore. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change,PROTOCOL_VERSIONunchanged, no new dependency. Documented in the guide. Asserted byscripts/check-vfx-schema.ps1andscripts/check-builtins-parse.ps1; audible output cannot be verified headless here. Not verified in game.noise: generated white noise positioned in the world. A new audio effect synthesises mono white noise per effect instance (volumelinear gain0..1, neutral0.0sofade_ticksfades it;brightnesscrisp/soft blend0..1, default0.5;jitterrandom-walk step withjitter_ratespeed multiplier for a playback-rate and volume wander;seedreseeds the texture) straight into the OS mixer — no audio file in any pack, identical on every Minecraft line. Positioning, distance falloff (full within 8 blocks, silent past 48), clock semantics (pause freezes, seek resyncs silently), fade-out on single stop and immediatestopAll/world exit/client shutdown close all mirror thehumvoice; the same honest limits apply (vanilla mixer bypassed, no-audio-device stays silent with a one-time warning). Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change,PROTOCOL_VERSIONunchanged, no new dependency. Documented in the guide. Asserted byscripts/check-vfx-schema.ps1andscripts/check-builtins-parse.ps1; audible output cannot be verified headless here. Not verified in game.hum: a generated sine hum positioned in the world. A new audio effect synthesises a mono sine tone per effect instance (frequencyin Hz, default440;volumelinear gain0..1, neutral0.0sofade_ticksfades it) straight into the OS mixer — no audio file in any pack, identical on every Minecraft line. The voice is positioned like a world overlay (move position, first staticpositionsentry, first entity anchor, authoredpos_x/y/z, else the local-player snapshot) with a linear distance falloff (full within 8 blocks, silent past 48); it follows the shared effect clock, so pause freezes it and a replay seek resyncs silently. Stopping one instance fades it out;stopAll/world exit/client shutdown closes the voices at once. Honest limits, stated on the page: the vanilla mixer is bypassed (volume sliders do not apply — usevolume), and a machine with no audio device stays silent with a one-time warning. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change,PROTOCOL_VERSIONunchanged, no new dependency. Documented in the guide. Asserted byscripts/check-vfx-schema.ps1andscripts/check-builtins-parse.ps1; audible output cannot be verified headless here. Not verified in game.
Fixed¶
- The frame rate no longer reaches the sound: the voices moved to their own audio thread. The
hum,noiseandheartbeatvoices were synthesised on the render thread, one chunk per frame, so the frame rate leaked into the sound: the gain ramps were one frame long, a long frame could starve the line's buffer (that is the crackling), and the heartbeat's time-scale work spiked the frame. The voices now live on a dedicated audio thread that generates fixed 5 ms blocks from its own wall clock and keeps every line about 60 ms ahead of its playhead, so a slow frame, a lost frame or an unfocused window cannot starve a line or stretch a ramp - the sound is the same at 30 fps and at 200 fps. The render thread only publishes the parameters and the shared effect clock; the voice state belongs to the audio thread, so there is nothing to lock. A paused game (or a paused replay) is still the one thing that stops it: the shared effect clock freezes and the thread renders silence until it moves again. The heartbeat's time-scale work moved to the audio thread with the rest. No param renamed, no wire, UBO or API change,PROTOCOL_VERSIONunchanged. Documented on the hum, noise and heartbeat pages. Not verified in game. - The synthesised voices no longer crackle, and a heartbeat plays out instead of being cut. Three click sources are gone. A voice that resumes after a silence gap (a pause, a game hitch, a fade) now fades in over 5 ms instead of starting at full amplitude. A voice whose effect ended is no longer cut at the playhead: the
humand thenoisefade out and their line is released once it has drained, so the stop is a fade and not a step. And the last 5 ms of a heartbeat recording fade out, so a beat ends without a click. The heartbeat'sdurationnow decides when the sound stops repeating: when the instance ends, the thump stops repeating right there and the beat that is sounding plays to its end instead of being cut in the middle. No param renamed, no wire, UBO or API change,PROTOCOL_VERSIONunchanged. Documented on the heartbeat page. Not verified in game. - Synthesised voices no longer stutter or crackle on slow frames. The
hum/noise/heartbeatvoices counted samples from frame ticks, so an unfocused window (a few frames per second) pushed each frame past the 4-tick cap and the voice emitted nothing (chunk-gap-chunk), and the 0.25 s line buffer underran whenever the game loop stalled. Each voice now counts samples on its own steady wall clock gated by the effect clock: slow frames and replay seeks emit the wall-clock debt sized to the line's free buffer space (the write never blocks the frame), phases stay continuous so there are no bursts or boundary clicks, and pause (or a paused replay) still freezes synthesis exactly as before. The buffer grows to 0.5 s. No param renamed, no UBO or wire change,PROTOCOL_VERSIONunchanged. Documented on the hum, noise and heartbeat pages; audible output cannot be verified headless here. Not verified in game. - Local-contrast halos around geometry in
halftone,comicandaccent_glow— local averaging removed. The 8-tap averaging ring made the tone mapping a de-facto edge detector: near any luminance step the local mean skewed and the tone saturated into flat paper-coloured bands with no dots, which reads as glowing outlines and is useless for halftones. All three effects now map absolute pixel luminance instead (halftonekeeps its adjustablegaincontrast around mid grey, default2.5= neutral;comicgains a tunablemidsplit point, default0.5;accent_glowgains tunablemid/corethresholds, defaults0.5/0.68), so halos cannot form by construction — plus 8 fewer texture fetches per pixel. Dot grids grow from the screen centre instead of a corner, so changingcell_sizerescales around the middle of the frame. Three new optional params (midoncomic;mid,coreonaccent_glow), no UBO or wire change,PROTOCOL_VERSIONunchanged. Not verified in game.
2.4.1 - 2026-10-14¶
Added¶
accent_glow: dark accent glow on the highlights. A new screen effect compares each pixel's luma against the mean of 8 ring taps (radius 0.045, x divided by the aspect,step(avg + 0.004, luma)) and prints the highlights in the accent colour (accent_r/g/bdefault0.35/0.9/1.0), the hottest pixels (step(avg + 0.18, luma)) in white, and 5 px halftone dots rotated 45 degrees gated to the mid tones (smoothstep(0.15, 0.35, tone)up,smoothstep(0.65, 0.85, tone)down, scaled0.75), all multiplied bystrength(default1.0); everything else goes black, andamountcross-fades the look (fading1 -> 0); no param has an upper cap. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change,PROTOCOL_VERSIONunchanged. Documented in the guide. Asserted byscripts/check-vfx-schema.ps1,scripts/check-shader-identifiers.ps1andscripts/check-builtins-parse.ps1; the GLSL/pixels cannot be verified headless here. Not verified in game.comic: duotone comic in one shader pass. A new screen effect splits each pixel around the local mean of 8 ring taps (radius 0.045, x divided by the aspect,step(avg + 0.004, luma)) into paper/ink colours (paper_r/g/bdefault1/1/1,ink_r/g/bdefault0/0/0), stamps 5 px halftone dots rotated 45 degrees gated to the mid tones (smoothstep(0.15, 0.35, tone)up,smoothstep(0.65, 0.85, tone)down, scaled0.8), draws a Sobel contour (smoothstep(0.06, 0.18), tap step1.5 / OutSize) and cross-fades the look withamount(fading1 -> 0); no param has an upper cap. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change,PROTOCOL_VERSIONunchanged. Documented in the guide. Asserted byscripts/check-vfx-schema.ps1,scripts/check-shader-identifiers.ps1andscripts/check-builtins-parse.ps1; the GLSL/pixels cannot be verified headless here. Not verified in game.ink: Sobel comic-ink contour overlay. A new screen effect runs a 3x3 Sobel on the local luminance (tap step1.5 / OutSize) and mixes the frame towards black bysmoothstep(edge_low, edge_high, |g|)scaled withamount(defaultsedge_low 0.06,edge_high 0.18,amountfading1 -> 0); no param has an upper cap. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change,PROTOCOL_VERSIONunchanged. Documented in the guide. Asserted byscripts/check-vfx-schema.ps1,scripts/check-shader-identifiers.ps1andscripts/check-builtins-parse.ps1; the GLSL/pixels cannot be verified headless here. Not verified in game.halftone: print-style halftone dots driven by the local luminance. A new screen effect compares each pixel's luma against the mean of 8 ring taps (radius 0.045, x divided by the aspect) with an adjustablegain, then stamps a rotated dot grid (cell_sizein pixels,anglein degrees, default 45) whose dot radius grows in the dark areas;amountcross-fades the pattern over the frame.cell_sizekeeps only its lower guard (>= 1in-shader); no param has an upper cap. Additive: one new effect type and its params, no existing datapack field renamed, no UBO layout change,PROTOCOL_VERSIONunchanged. Documented in the guide. Asserted byscripts/check-vfx-schema.ps1,scripts/check-shader-identifiers.ps1andscripts/check-builtins-parse.ps1; the GLSL/pixels cannot be verified headless here. Not verified in game.particlesshape params,dustsize,block_chainspacing/scale, outline widths,light_beamtop_scale,pulse_ringthickness,guide_linewidth/dashes, spark speed/size andentity_displacescale no longer have an upper cap. Every lifted param keeps only its lower guard (particles:radius >= 0.05,height >= 0.5,turns >= 0.25,speed/accel/lifetime >= 0,vel_yunbounded,dust size >= 0.05;block_chain:spacing >= 0.25,scale >= 0.1; outlines/ring/guide-line widths and thickness/gap>= 0,dash_length >= 0.1;light_beam top_scale >= 0.1; sparkspeed >= 0,size >= 0.01;entity_displace scale >= 0.5mirroring the renderer floor), and the editor schema mirrors the same floor-only bounds. The floors that remain are the ones that avoid degenerate geometry or division by zero (spacing, dash_length, displace scale); no geometry loop count depends on any lifted param, so the caps had no perf basis. No datapack field renamed, no wire, UBO or API change,PROTOCOL_VERSIONunchanged. Documented on particles, block_chain, light_beam, entity_displace and spark presets.light_beamradius/height andpulse_ringradius no longer have an upper cap.light_beamkeeps its lower guards (radius >= 0.1,height >= 1) andpulse_ringkeepsradius >= 0(degenerate/inverted geometry guard), but the16/256and64caps are gone: the geometry cost is constant (a fixed shell/slice count), so radius/height only scale positions. No datapack field renamed, no wire, UBO or API change,PROTOCOL_VERSIONunchanged. Documented on light_beam and pulse_ring.
2.4.0 - 2026-10-14¶
Added¶
- Visual node editor v1.1 "quick wins". A tab may now be left empty (delete the effect node; a + Add effect placeholder retypes it); undo/redo is snapshot-based (one drag or one action = one step, up to 100) and recompiles the whole effect; a broad set of keyboard shortcuts was added (
Del,Ctrl+C/V/D/A,Ctrl+Z,Space,R,K,F/Home,Ctrl+S,Tab,Ctrl+G,[/],?); the timeline gained a tick ruler, keyframe clamping to[0, duration]on drag with an out-of-schema-range red highlight, and a draggable duration bar; numeric fields edit inline (select-on-focus) with no floating popup, and wired slots/params render read-only with a source badge and a "go to node" affordance; thebindvalue node shows a binding dropdown and labels its output by the binding's unit.COLLECTIONis no longer offered by the schema the editor palette is built from. Editor-only: no datapack field, UBO, API or wire change,PROTOCOL_VERSIONunchanged. Documented in the guide. Not verified in game. - Visual node editor: build, live-play and export effects in a local web UI.
/vfxeditorstarts (and a second call stops) a loopback HTTP/SSE bridge that serves a vendored LiteGraph editor from the jar and drives the running client: a schema-driven palette, one tab per effect, an inspector (constant / start-end / keyframes / binding /expr), a timeline with draggable keys and play/stop/loop transport, and live incremental edits gated by a monotonicrevision. It exports a real datapack to a worlddatapacksfolder, a loose folder or a zip (per-Minecraft-linepack.mcmeta, plus a sidecar holding the graph layout), imports a folder or zip back, reconnects the event stream with exponential backoff (1 s/2 s/4 s…) and re-sends the full effect when the revision diverges, and undoes by restoring the model snapshot then recompiling the whole effect (never an incremental game undo). Client-only, no new dependency, no datapack field, UBO, API or wire change,PROTOCOL_VERSIONunchanged. Documented in the guide. Not verified in game. - Authored preview mode in the visual editor. The transport switches Exact/Authored: Authored plays the collection root with authored timings, so fades, loops and durations read as they will in game. Editor-only: no datapack field, UBO, API or wire change,
PROTOCOL_VERSIONunchanged. Documented in the guide. - Reference nodes and nested collections. A tab can instance another tab (cycle-guarded, nesting depth capped at 4); Exact preview recursively expands nested collections with accumulating delays. Editor-only, no wire change. Documented in the guide.
- Scrub is pause; export v2; editor QoL. Scrubbing the playhead pauses instead of auto-resuming; export carries
vfx_curves/vfx_particlesblocks verbatim, writes multi-namespace packs in one pass, and migrates the sidecar to v2; socket groups start collapsed, effect nodes gained a canvas solo badge, andCtrl+Ggroups persist as named frames. Editor-only, no wire change. Documented in the guide.
2.3.0 - 2026-10-04¶
Added¶
screen_image: draw a picture over the game frame. A new screen effect composites a resource-pack PNG - or an image a mod supplied from code withVFXAPI.registerImage- onto the game's own framebuffer inside an animatable rectangle.pos_x/pos_yplace it,size_w/size_hsize it and animate the picture's own proportions (size_w:size_his exactly the on-screen width:height, so equal values are a square on any window),opacityfades it, andframes/frame_timestep a strip or a vanilla.png.mcmetaanimation. The canvas is the Minecraft window, not the monitor, so the picture scales with the window and nothing outside the game frame is drawn;screen_layerpicks whether it draws under the GUI (the default), above the first-person hand, or over everything including chat and hotbar. A.mcmetasibling animation wins overframes/frame_time;interpolate: trueis not supported. An id that does not resolve draws nothing and warns once instead of blanking the frame. Additive: one new effect type and its optional params, no existing datapack field renamed, no UBO layout change,PROTOCOL_VERSIONunchanged. Documented in the guide. Asserted byscripts/check-screen-image.ps1; the GLSL/pixels cannot be verified headless here. Not verified in game.VFXAPI.registerImage/unregisterImage: show an image a mod renders itself, anywhere a texture is accepted. A mod hands over ARGB pixels (row-major, row 0 the top row,0xAARRGGBB, the orderNativeImage.getPixels()returns) under a resource id, and that id then works exactly like a packed texture id: effect fields (surface_pattern,sky_pattern, field textures) resolve it through the game's texture manager, and awindow_createpicture uploads it into the aux window's own GL context. This is the missing piece for an item icon, a live preview or a generated texture that does not exist as a.pngin any pack. The registry is a client singleton bounded at 256 images; registering an id again replaces it (the previous GPU texture is released). Additive:defaultmethods onVFXLocalDispatcher(a dispatcher compiled before this still links), no wire or UBO change,PROTOCOL_VERSIONunchanged. Documented in the API. Asserted byscripts/check-image-registry.ps1; the GLSL/pixels cannot be verified headless here. Not verified in game.- Two effects that open a real picture window on the desktop:
window_createandwindow_control. Awindow_createopens a named frameless, transparent, click-through window covering the work area of the monitor the game is on, and shows a picture in it;window_controlthen moves, resizes, fades and retitles that window by name. Neither is a post-processing pass nor world geometry, and nothing about the Minecraft window changes. The window is undecorated, non-resizable, always on top and click-through (GLFW_MOUSE_PASSTHROUGH) at all times, and never takes focus, so it cannot steal input from the game. It is created hidden, sized to the work area and shown last, and is never moved or resized afterwards - position and size animate the picture's rectangle inside the canvas, so an animation costs no OS call per frame. Up to 8 windows may be live at once; a secondwindow_createunder a name that is already live is a no-op (first one wins).titlesplus an animatedtitle_indexpick the title (clamped),opacityanimates, and a frame sheet steps onframe_time. Wayland is best-effort. Additive: two new effect types and their optional params, noVFXAPIchange, no wire or UBO layout change,PROTOCOL_VERSIONunchanged. Documented in the guide. Not verified in game - in particular the game must keep presenting normally on 26.2 with an aux GL window open, which a green build cannot prove.
2.2.1 - 2026-10-01¶
Added¶
-
Two optional box bounds for a GLSL mask shape — a containment box and a skip box. A world plugin leaf on
"volume": "aura"may now declare an axis-aligned box as its broad phase, in two further optional functions (vfx_shape_custom_bounds_box,vfx_shape_custom_skip_bounds). The containment box must contain the whole region where the field is<= 0; the skip box must lie entirely inside a region where the field is> 0and be inscribed in that hole — the AABB of a set of circles does not qualify, because its corners sit in the coverage region and would skip real coverage. Getting the two the wrong way round is a declaration bug rather than a cull: it buys nothing, because the coverage region outside such a box is unbounded. A miss on the containment box's padded interval culls the leaf with no march at all, and a camera inside the skip box starts the march at its far side instead of at the camera. The slab test is padded bysoftness / 2and the skip keeps the lastsoftnessbefore the range limit sampled, so no cliff appears at a box face and the plugin never has to knowsoftness. Both are optional and consulted per leaf (skip, then box, then the existing sphere, then no bounds), so a plugin that declares none of them renders exactly as before. Additive: two default methods onVFXMaskShapeGlsl, no newVFXAPIoverload, no datapack field, no wire or UBO layout change,PROTOCOL_VERSIONunchanged. See API. Not verified in game; the contracts and the arithmetic are asserted byscripts/check-aura-bounds.ps1— the GLSL cannot compile headless here. -
"aura_steps"on a custom aura mask leaf — an optional per-leaf cap on the march. The march budget is an upper bound a typical ray never approaches (it leaves early on a saturated sample, on the dive bound and on the range limit, and spends 1 to 20 of the loop's 40 + 16 iterations on the check's ray classes), so the new number is a cap, not a spend — a last resort for a leaf that really does burn the budget, and a declared broad phase is the real lever. Accepted only on a custom leaf with"volume": "aura"(a parse error naming the shape anywhere else, since only a plugin leaf has a march to bound); a negative value is a parse error, and a value above the compiled budget is clamped to it. Absent, or0, means no cap and the leaf is identical to today. A truncated march is best-effort and never saturates; because a march cut short while still inside meets the shippedtExit = tLimitrule, a low cap can read slightly higher than uncapped (measured: +0.23 at a cap of 2, bounded by the uncapped silhouette factor). Additive: one new optional leaf field on an existing reserved slot, no UBO or Config field added, no field renamed,PROTOCOL_VERSIONunchanged. See masks. Not verified in game; asserted byscripts/check-aura-bounds.ps1— the GLSL cannot compile headless here.
2.2.0 - 2026-09-29¶
Added¶
-
/vfxconfig— an in-game settings screen. A vanilla screen (no Cloth Config, Mod Menu or YACL, no new dependency) edits the three post-chain settings: chain resolution (Full / Half, default Half), fusion (default On) and the remap (default On, with a description label). Changes apply without a restart and persist toconfig/vfxweaver.json; a JVM system property still overrides the file. Client-side and available to every player (no operator rights): the command is registered on the client and never reaches the server, so it works on any server, including a dedicated one. No datapack, API, wire or UBO change. -
Post-chain fusion: a run of pointwise screen effects renders as one shader pass. Adjacent
POINTscreen effects (color_grade,screen_flash,vignette,invert,posterize,scanlines,film_grain) and each masked effect's coverage consumer are compiled into a single generated program, so a chain of four masked pointwise effects costs one pass instead of twelve.blur-class effects (anything that samples neighbours, includingbloom,vhsandmotion_blur),afterimage/stop_motionand every unannotated effect cut the chain and render exactly as before. Output is re-quantised per stage so a fused chain matches the unfused one. On by default;-Dvfxweaver.fusion=falsedisables it. The one-tap remaps (distortion,vortex,pixelate,noise_warp,slice_shift) are fused too, on by default now that the owner's in-game A/B showed no visible difference;-Dvfxweaver.fusion.remap=falsedisables them. No datapack, API, wire or UBO change. -
Half-resolution effect runs. A run of the heavy multi-tap screen passes (
blur's two passes,bloom,depth_of_field,vhsanddigital_glitch) can render at half resolution, wrapped in an exact downsample/upsample pair, once the run's summed tap count reaches 8 (vhsanddigital_glitchcarry four taps each, so they scale only beside another scalable pass). Nothing else about the chain changes: pointwise and fused stages, mask consumers and their coverage, the pre-effect image,afterimage/stop_motionand every unlisted effect render exactly as before. On by default;-Dvfxweaver.chainres=0.5is the default and-Dvfxweaver.chainres=1.0disables it. Only those two values are accepted - anything else warns once and is treated as0.5. Inside a scaled pass the output is slightly softer, which is the point. No datapack, API, wire or UBO change.
2.1.1 - 2026-09-27¶
Added¶
-
shape: "depth"— a mask leaf driven by the scene distance. Coverage is 0.5 exactly at the leaf'sdistanceand falls to 0 atdistance + softness/2(the leaf's ownsoftnessis the falloff width, so there is one knob, not two), and 1 nearer than that. Bind a post effect to it and you get depth-of-field masking: sharp up close, blurred past the focus distance, with the sky on the uncovered side unless you compose it away.invertflips it, and adifferenceof two depth leaves is a band around one distance. It has no geometry of its own and needs no per-frame driver, so it is the camera-relative alternative to a sphere leaf bound to the player. See masks. -
"occlusion": falseon an aura leaf — a fill that ignores the depth buffer. Translucent surfaces (stained glass, ice, water) write depth, so a wall of aura behind a glass window was correctly-but-unhelpfully treated as occluded and the fill disappeared. The flag turns the occlusion term off, so the volume shows through glass. It is an aura-only leaf field, defaults to the current behaviour, and costs no extra sampling; being all-or-nothing it also draws the volume over nearer terrain and over anything else in front of it. See masks. -
"occlusion_softness"on an aura leaf — the occlusion width as its own knob. The occlusion ramp used to be denominated in the leaf's ownsoftness, so a deliberately soft silhouette also decided how softly the fill was hidden by terrain, and a blocky hillside (a 1-block staircase) turned into a stack of ~45 bands along the silhouette. The new field is that width in world blocks: 0.5 where the volume's entry sits on the surface, falling to 0 or 1 over the given distance. Absent, the ramp keeps followingsoftnessexactly as before, animation included, so no existing mask changes. A sub-block value gives a crisp silhouette that follows the terrain instead of a 45-band fade. See masks. -
order- the compositing order of screen effects inside a layer. Screen effects accept anorderparam next toscreen_layer: a float, lower runs earlier, default0, and effects that leave it alone keep the order they were started in, so no existing pack changes.screen_layerstays the outer, semantic axis (which part of the frame the pass belongs to) andorderonly sequences the effects that layer already selected; being fractional,10.5slots an effect between two at10and11. It is an ordinary param, so the datapack, the command param-map,setParamand keyframes all reach it, and the wire format, UBO andPROTOCOL_VERSIONare untouched. See screen effects. -
A plugin can declare its field's gradient bound.
VFXAPI.registerMaskShapeGlsl(id, plugin, lipschitz)is a new overload: the plugin field must be a conservative distance (|grad d| <= 1), and a field that is perturbed (noise, a biased blend) can no longer stay silent about it — either scale the value down or declare a true upper bound, which the aura march and the envelope then respect. A variant of several plugins compiles against the largest declared bound. The mask guide now states the rule next to the plugin contract. Additive: an existing registration compiles exactly as before. See masks and the API.
Fixed¶
-
volume: "aura"on a GLSL-plugin mask leaf: a smooth soft edge instead of a banded ramp with a hard cliff. A wide, soft plugin volume rendered its silhouette in visible steps and dropped from ~50% coverage straight to zero within a fraction of a degree. The plugin aura estimated the ray's deep point as the sampled minimum of its march, so the edge resolved into sample-sized steps, and any entered ray scored at least 0.5 while a miss scored exactly zero — a built-in ~0.5 jump at the silhouette. Both are gone by construction: the deep point is now a Lipschitz cone-envelope of the samples already taken (continuous in screen space, exact for creasedmax/minfields, never thinner than the truth), a ray that misses the volume fades over the leaf's ownsoftnessinstead of vanishing, and the entry distance is bracketed by false-position, which also dequantises the occlusion ramp. The edge lands in the same place, the interior still saturates to full coverage, and the cost is unchanged - no extra SDF evaluations. Standing inside the volume keeps filling the view, growing towards full coverage with depth. See masks. -
The aura march grades a ray by its deepest penetration, not its first exit. Entry and exit were two marches and the second stopped at the first
d > 0, so on a wall with a bump the solid mass behind the bump was never sampled and the silhouette reported the bump's shallow depth - the coverage dipped across it (the reported arch at the bottom of a wall). One pooled loop over the same 40 + 16 steps now runs to a real termination: the cone bound stays global over the whole ray (so the mass behind a bump is what gets measured), the first entry still drives the occlusion ramp and the last exit brackets the horizon fade. Saturation stays a sample certificate and budget exhaustion deliberately does not saturate - a long shallow corridor would light up. An additive anti-crawl step keeps a grazing approach or the gap behind a bump from eating the budget; it is bounded, which is why a ray that never enters can now reach at most 0.6 rather than 0.5 of coverage. Measured: the bump profile goes from1.00 ×9, 0.58 0.70 0.80 0.85 0.80 0.70 0.58, 1.00 ×9to a flat1.00, the fixture's flat wings drop from 22.0 to 8.9 SDF calls per ray, and a receding ray terminates in 5 instead of burning all 40. See masks. -
The occlusion ramp is kept at least 48 pixels wide on screen. A depth-driven edge inherits the depth buffer's granularity: blocky terrain steps by a block at every block edge, and at a grazing view of the ground those steps land less than a pixel apart, so the mask's silhouette aliased into a comb. The ramp is widened with the per-pixel depth slope wherever the depth moves fast, capped at 4x the authored width so a depth discontinuity (a cliff edge) cannot smear the edge; the authored
occlusion_softnessstays the floor, so a near face-on occluder is unchanged. Measured at 2.8 blocks per pixel: the ramp runs 44.8 → 134.4 blocks and the coverage step per terrain block edge falls from 0.0223 to 0.0074 - below what an 8-bit target can show. No uniform, no UBO/wire change, no new knob. See masks.
2.1.0 — 2026-09-25¶
Added¶
-
film_graincan be coloured (chroma). A newchromaparam (0, monochrome, default;1, fully coloured) mixes an independent per-channel grain into the shared monochrome grain and stops halfway, so at1the grain keeps its luminance structure instead of dissolving into plain colour noise. Animated like any other param (keyframes,expr,setParam,fade_ticks). Additive: absent = exactly the previous monochrome look, no datapack field renamed, no wire change. See film_grain. -
fog_modifier— change the vanilla fog. A new value-modifier effect (likefov_modifier, not a post pass):fog_start_scale/fog_end_scalemultiply the vanilla fog start/end distance (neutral1.0;>1sees further,<1pulls the fog closer) and optionalfog_r/fog_g/fog_b(neutral "unset") override the fog colour.fog_color_amount(0..1, neutral1/unset) scales how far the colour moves from vanilla — animating it ramps smoothly from the dimension's own fog colour to the authored one, and0is a true no-op that never writes the colour. The mod rewrites the fog at the return of vanillaFogRenderer.setupFog, mutating the instance the game produced in place before anything consumes it: the same values then reach the fog uniforms (FogEnvironmental/FogRenderDistancestart+end,FogSkyEnd/FogCloudsEndandFogColor), the level renderer and the sky/cloud shaders, so vanilla terrain, entities, the horizon band and the entity-effect geometry all see the change; absent params leave the fog bit-for-bit untouched. Scales combine additively around neutral (1 + Σ((s−1)·weight)) and the colour is a weighted average blended from vanilla (order-independent, clamped; each effect's colour weight is itsweight·fog_color_amount), so overlapping effects stack predictably. All six params are ordinary animatable params, sokeyframes,expr, graphs,setParamandfade_tickswork. The start/end scales apply to every distance-based fog on all three nodes — overworld, nether (fog_start/fog_enddistances) and End, plus water/lava/snow/blindness/darkness, andfog_end_scalealso scales the sky/cloud fade ends; these versions have no density-fog field or attribute, so nothing is left out. Honest limits: under an Iris shaderpack this effect is a no-op (a pack computes its own fog and ignores the vanilla values); and theblock_tint/block_outline/glow/spark overlays use a fog-free shader, so only the entity-effect geometry inherits the change. No shader change, no UBO change, no new dependency;PROTOCOL_VERSIONunchanged. The mixin hooks the return of vanillaFogRenderer.setupFogatpriority 900(before Sodium's fog mixin) and mutates the returned instance in place, so the fog uniforms, the level renderer and the sky/cloud shaders all see it. Not verified in game: the visual result; the static contract — the per-nodeFogRenderer.setupFoghook point (return ofsetupFog, mutated in place), the colour-amount combination maths and the type/neutral/isPostProcessing wiring — is asserted byscripts/check-fog-modifier.ps1. -
sky_pattern— follow the sun, the moon or the stars (anchor). The overnight sky body is a new optional top-levelanchorstring: omitted (or"dome") is the world-fixedanchor_yaw/anchor_pitchauthoring;"sun"and"moon"make the pattern track the vanilla body;"stars"locks the pattern to the rotating star sphere (the shader inverts vanilla's star pose exactly — see Fixed below). The body angles are read from the client's own sky render state on the CPU and fed to the existinganchor_yaw/anchor_pitchuniforms (plus two internal star fields), so there is no mixin — which is what keeps it working under an Iris shaderpack (the world time, hence the angles, is still computed by the game). When the sky state is unavailable (no overworld sky, or the render state is not ready) the effect contributes nothing instead of painting at a guessed spot, and warns once. Additive: a new top-level field only, no existing effect changed, no datapack field renamed,PROTOCOL_VERSIONunchanged, and no UBO/Config change. Honest limits: the effect is an overlay, so the vanilla sun/moon are still drawn underneath (size the pattern to cover them: sun ≈33° apparent width, moon ≈24°), and because the vanilla star colour is not reached without aSkyRenderermixin,"stars"positions a pattern at the star sphere rather than recolouring the vanilla stars. The rendering is not verified in game by the author. -
sky_pattern— a datapack figure or texture drawn on the sky dome. A new screen post effect and the sky sibling ofsurface_pattern: the same structuralpatternblock, shared shape library andpattern.textureaddressing, but the pixel's view ray is projected onto the dome instead of a depth-reconstructed world surface (the projection is chosen bysky_mode— see thesky_modeentry below). The dome anchor is authored asanchor_yaw/anchor_pitch(degrees) and the image can be spun about world Y withdome_rotation;repeat: [3, 3]draws nine dots with no texture, and apattern.texturewith asheetplus the animatableframeparam draws an animated sky. The pass is gated on the depth sky test, so it never touches terrain, the hand or the GUI. Additive: a new effect type only, no existing effect changed, no datapack field renamed,PROTOCOL_VERSIONunchanged, and no existing shader's UBO layout touched. The rendering is not verified in game by the author; like every post effect it composes over an Iris shaderpack's sky. -
light_beamcan end on a chosen point. Three optional params,end_at_x/end_at_y/end_at_z, name a world point the far end of the beam lands on: the anchor is derived from it (end − axis × height), so the geometry finishes exactly there — with the default axis the beam hangs straight down fromheightabove the point, which is how a beam comes out of the sky. They are ordinary animatable params, so the landing point can be keyframed, driven by an expression or bound, and a beam from A to B isend_at = Bwith the axis and length computed between the two points. Unset, the beam behaves exactly as before. No shader, pipeline, UBO or wire change. -
light_beamcan point in any direction. Three new params,dir_x/dir_y/dir_z(defaults0/1/0), replace the fixed vertical axis, so a beam can be aimed at an angle — e.g. from a point on the sky. They are ordinary animatable params, so they can be keyframed, driven by an expression or bound, including to the camera look ({"bind": "look_x"}etc.). With the defaults the beam renders exactly as before. No shader, pipeline, UBO or wire change. -
Sky masks. Two new mask leaves address the sky instead of the world:
sky(the whole visible sky, no parameters) and a 2D shape (circle/ellipse/rect/polygon) in the newspace: "dome", which places it on an equirectangular map of the sky with the centre authored as[yaw, pitch]degrees. A dome leaf is sky-occluded by construction — a hill or a wall occupying a dome direction is never tinted — and both fail closed (zero coverage) when no trustworthy depth is available. This is what lets any existing screen effect be restricted to the sky, e.g. a green or glitching sky. Additive: no datapack field was renamed, the wire format andPROTOCOL_VERSIONare unchanged and the coverage UBO layout did not change. The far-depth test is verified in game on all three lines (26.2, 26.1.2, 1.21.11). The sun, the moon and the clouds are drawn without depth, so asky/domeleaf covers them too (a green sky turns the sun green); the horizon fog is baked into the terrain, so it is not recoloured and a strong tint can leave a seam there. -
volume: "aura"on a world GLSL-plugin mask leaf. A custom mask leaf registered withVFXAPI.registerMaskShapeGlslcan now be a real volume instead of a surface-only shape: the pixel's view ray is sphere-traced through the plugin's own distance field and the effect fills the volume — air and sky included — wherever the scene does not occlude it, with the same silhouette, edge fade and occlusion maths as a built-insphere/boxaura. A composed custom leaf (no raw SDF to march) and a screen-space plugin leaf are per-file parse errors naming the reason rather than silent fallbacks. An optionalvec4 vfx_shape_custom_bounds()in the plugin source (world centre and radius) acts as an analytic broad phase: a ray that misses it is rejected without evaluating the SDF, which makes the average cost proportional to the volume's screen area instead of the whole frame. A plugin that declares neither function compiles and behaves exactly as before; the wire format,PROTOCOL_VERSIONand the coverage UBO layout are unchanged.
Changed¶
-
sky_patternaddresses the sky withpatchorfill— the legacy equirectangulardomemode was removed before release. A single global chart of a sphere is topologically unable to tile it cleanly:uis undefined at the poles (a whole-sky pattern winds into a funnel at the zenith) andumust wrap at ±180° yaw (a mandatory visible seam / mirror axis) — properties of the chart, not formula bugs, so no formula fixes them. The effect therefore offers only local-chart modes:patch(default,0) is one gnomonic decal at the anchor (no pole, no seam, clean discard past the decal horizon) andfill(1) covers the whole sphere with three orthographic charts blended by a sharpened partition of unity (no pole convergence, no seam anywhere; the charts' coverage and colour are blended, never their UVs)."dome"and any other unknown value are now a per-file parse error naming the accepted values.sky_modeis appended last to the Config UBO, so no existing shader's layout changes; no datapack field was renamed andPROTOCOL_VERSIONis unchanged. The effect is not in any release yet, so removingdomebreaks nothing. A full skybox cube remains a separate future feature. -
A custom mask leaf in
surfacemode no longer tints the sky. The surface path classifies only what the depth buffer contains, so a sky pixel contributes no coverage. Built-in shapes already behaved this way (their bounded distance field puts a far-plane point outside the shape); the gate is now explicit for custom leaves, whose SDF may be intentionally unbounded. A plugin that relied on painting the sky from asurfaceleaf will no longer do so. -
A built-in world GLSL mask plugin,
vfxweaver:blobs_glsl. The only shipped GLSL plugin wasvfxweaver:ringed_glsl, a screen-space 2D figure, sospace: "world"plugin leaves had no volume to march and the dynamic-data demo could not show the data driving anything.vfxweaver:blobs_glslis a genuine world-space SDF — a union of up to eight spheres, negative inside — whose sphere centres/radii come from the leaf's dynamic data (mask.p<N>.d0..d31, four floats per sphere) and whosep0..p7are global controls (offset, radius scale/bias, count). It also declares the optionalvfx_shape_custom_bounds()broad phase over the same data+params extent, sovolume: "aura"with it is cheap. It works for bothsurfaceandaura; the three mask demos use it. The plugin leaf shape is inert on the1.21.11node (no shader-source hook). Additive:vfxweaver:ringed_glslstays the screen reference, no existing pass's UBO/wire layout changes,PROTOCOL_VERSIONunchanged. See masks.
Fixed¶
-
film_grainnoise no longer repeats in a fixed pattern. The cells were hashed with afract(sin(dot(...)))hash, which is only well distributed for large inputs: on the small integers a pixel cell produces it repeats in a visible lattice, so the frame read as one small tile stamped over and over. Cells are now hashed with an integer bit-mix, and the per-frame offset is an integer tick mix rather than afract()-scaled float. -
film_grainandpixelatecell grids grow from the centre, not the corner. Both derived their grid straight from the UV, so an animated cell size expanded the pattern out of the bottom-left corner; the grid is now measured from the screen centre. Effects whose pattern is per-scanline (scanlines,vhs,digital_glitch) are deliberately unchanged — those bands run from the top edge and have no centre to grow from. -
sky_patternanchor: "stars"really tracks the star field. The anchor previously drovedome_rotation, a world-Y spin, while vanilla rotates the star sphere about a different axis (its pose isRy(-90°) · Rx(starAngle)), so astarspattern was only an approximation and turned about the wrong axis. The sky shader now takes the sampled direction into the star sphere's own frame with the exact inverse, so the pattern is locked to the star material.dome_rotationis not hijacked — it stays the author's world-Y spin, applied first. Two internal Config fields are appended aftersky_mode; no datapack field was renamed andPROTOCOL_VERSIONis unchanged. See sky_pattern. -
sky_patternpatchdecals have no built-in clip, and the ring demo no longer circles the player. Apatchis a flat sign on the tangent plane: it is evaluated out to the tangent-plane horizon, where a pixel behind the decal (dot(dir, anchor) <= 0) is discarded - no wrapping, no pole convergence and no built-in clip. A large patch can therefore show that horizon as a straight cut; restrict the effect with a mask if you need a clean edge. (An earlier unit-disc bound was removed: it visibly cut the figure.) The demos use each projection for what it is for:show_sky_pattern_texture(a whole-sky texture) andsky_cracksarefill, whileshow_sky_pattern(the ring) andnine_red_pixelsare smallpatchdecals. The ring demo no longer animatesdome_rotation- inpatchmode that moves the decal along its latitude, around the sky, so it now animates onlytile_scaleandopacityand stays at its spot. The effect is not in any release yet; no datapack field was renamed andPROTOCOL_VERSIONis unchanged.
2.0.2 — 2026-09-23¶
2.0.2 lets a GLSL mask plugin shape read live per-leaf float data, so its geometry can move every
tick without recompiling the shader variant. It also raises the network parameter cap to fit a large
mask's resolved map. No datapack format or wire format changed — PROTOCOL_VERSION stays 6 — and an
existing plugin that ignores the new helper compiles and behaves exactly as before.
Added¶
- GLSL mask plugin shapes can read live per-leaf float data. A custom mask leaf now carries 32
reserved dynamic floats,
mask.p<N>.d0 … d31, authored as a"data": [ … ]array on the leaf or set every tick with the newVFXAPI.maskData/VFXAPI.sendMaskData(orsetParam/sendSetParamon a single slot). A plugin reads its own leaf's values through the wrapper helpervfx_mask_data(vfx_shape_data_base + j), so a set of moving primitives far larger than the eight animatable params can be driven without recompiling the shader variant (the variant is keyed only by the set of plugin ids). Backward compatible: an existing plugin that ignores the helper compiles and behaves exactly as before. Two limits remain: a mask's custom leaves may reference only one distinct GLSL plugin id (two different plugin ids in one mask fail to compile and fall back to neutral coverage), and on the1.21.11node the missing shader-source hook leaves a GLSL-plugin shape inert. See Masks and the Java API.
Changed¶
- The mask coverage UBO gains a
shape_dataarray (avec4-packed, per-leaf slice; 18 fields, 2336 bytes, appended after every existing field). Thecheck-mask-ubo.ps1layout guard covers it. - The network parameter cap is raised 32 → 256 (
VFXTriggerPayload.MAX_PARAMS,VFXTimeline.MAX_OVERRIDES,ParamMapArgument.MAX_PARAMS, and the Flashback replay snapshot cap) so a large mask's resolved parameter map (two custom leaves × 32 data slots plus the other leaf slots) fits. The wire format is unchanged, soPROTOCOL_VERSIONstays 6; the cap is a read-side bound, so a 2.0.1 client still caps the map at 32 and cannot decode a larger play packet — a many-slot mask needs a 2.0.2 client.
2.0.1 — 2026-09-22¶
2.0.1 is a correctness release. It fixes the blur effect's animation stepping, makes client entity
selectors resolve the arguments they were silently ignoring, and brings the Flashback replay
integration in line with the replay clock. Everything here is a bug fix — no datapack format, network
protocol or Java API change — so existing worlds and packs keep working.
Added¶
- Client entity selectors now support
tag=,name=,distance=,limit=andsort=. A binding such as@e[tag=vfx_showcase,limit=1]used to resolve through a reader that understood onlytype=, so it fell through to the generic branch and matched the nearest loaded entity regardless of the tag — silently binding the wrong entity, or nothing usable. The client subset of the vanilla selector grammar now covers@s/@p/@a/@r/@e/bare name plustype=,tag=,name=(each optionally negated or quoted),distance=(a number or aN..M/..M/N..range),limit=andsort=(nearest/furthest/random/arbitrary). A selector outside that subset (nbt=,scores=,gamemode=,x/y/z, …) now fails closed and reports once throughVFXLog.warnOnce, naming the argument, instead of quietly matching the wrong entity.
Changed¶
- The
blureffect uses a fixed-tap separable Gaussian, so an animated radius no longer steps. The pass derived its tap count from the radius (int(r * 0.5 + 2), clamped), so as an animated radius crossed a threshold a whole symmetric pair of taps appeared or vanished at once and the normalised result jumped — a visible strength pop. Both passes now always take 12 taps per side with fixed weights spanning ±3σ, and only the sample spacing scales with the radius, so the Gaussian stays well sampled and the blur fades smoothly at every radius.
Fixed¶
- The replay-timeline integration works on 1.21.11 Fabric again. Flashback 0.39.9 (1.21.11)
declares
ReplayServer.jumpToTickas a private field, while 0.43.x (26.2) declares it public. The old all-or-nothing init read it with a baregetField, threwNoSuchFieldException: jumpToTick, and aborted the entire compatibility layer — recording silently stopped and every effect kept running on the wall clock, so pausing the replay no longer paused the effects. The playback symbols are now resolved individually and tolerantly (public field first, then the declared/private one), so a visibility change degrades only the feature that needs it; a missing, renamed or non-public symbol is reported once throughVFXLog.warnOnce, naming the symbol and the installed Flashback version, instead of no-oping silently. 26.2 behaviour is unchanged. - An effect triggered by another mod is removed when a replay is scrubbed back past its trigger,
and an effect no longer disappears after a single frame during playback. A backward scrub used
to leave an instance the replay controller did not own — e.g. a mace-hit mod's effect reaching the
manager through the network receiver — running, so a seek now stops every active instance and
re-places only the effects whose recorded trigger is at or before the new replay time. That also
fixes the regression where an effect flashed for one frame: the previous fix dropped the flagged
instance in the per-frame update, which killed the replay's own re-delivered play the frame after
it was created. Nothing is dropped between seeks any more; the removal happens only on the seek
path. Both trigger paths (the client-local
VFXAPI.playEffect/playEffectIddispatcher and the server→client network receiver) already recorded into the replay. - An entity tint in a Flashback replay keeps its timeline parameters and its target. A recorded
play carried the effect id, its trigger params and a world anchor, but not the entity UUIDs the
server had resolved for an entity effect (
entity_tint/entity_outline/entity_displace), so a replayed tint had no entity to attach to. The recorded action now carries those UUIDs (optional and trailing, so older recordings still decode) and the replay controller passes them into the play. A parameter the definition animates (keyframes, start/end,expr, a world binding or a graph input) is no longer frozen into the recording-start snapshot: the definition snapshot rebuilds the timeline and the value is evaluated from the effect's replay age, so the tint animates exactly as it did live. - A paused Flashback replay can still be inspected with
stop_motionrunning. The stop-motion hold pass kept compositing the captured pre-pause frame over the live one while the replay was paused (the effect's age, and therefore its quantised hold slot, is frozen), which hid every camera move. The hold is now bypassed while a replay is paused — the paused frame passes through so the camera can be moved — and the captured frame is refreshed on the first unpaused frame, so the stop-motion visual resumes during playback. - Scrubbing a paused Flashback replay back past an effect's trigger removes the effect. While
paused the replay server is frozen, so the polled replay time (
getPartialReplayTick, which returns the pre-scrub tick) can lag behind a scrub that is still pending inReplayServer.jumpToTick; the controller saw an unchanged time, skipped its rebuild and left the effect on screen. The replay clock now follows the pending seek target while paused, and any backward time move is classified as a rebuild, so scrubbing back drops effects whose trigger is in the future and re-places the rest. - A player's effects survive a re-login and keep counting while they are offline. The
server-side effect memory used to be wiped on disconnect (a leak fix), so every effect vanished
when a player re-joined. It is now kept — bounded to 256 players — and re-applied on re-join at
the age the effect would have reached, computed from its original start: time spent offline counts
(an effect that was 30 % through comes back 30 % plus however long the player was away). A finite
effect that would have ended during the absence is not resurrected, while looping and persistent
effects come back at the phase they would be in instead of restarting. No wire or protocol change
(
elapsedTicksalready carried the offset). - VFX effects in Flashback replays now follow the replay timeline instead of the wall clock. Pausing a replay holds the effects, seeking places every recorded effect at the age it should have at the new replay time (an effect whose trigger is in the future, or already past its end, is not playing), and a recorded play or live edit only takes effect when the replay time reaches its recorded tick — so scrubbing back and forth shows the same frame instead of restarting the effect. Normal, non-replay gameplay is unchanged. Flashback is Fabric-only.
- Looping and persistent effects are recorded into Flashback replays. They used to be skipped:
a persistent play (
-1) was dropped outright and an already-running looping/persistent effect was left out of the recording-start snapshot, on the (now obsolete) assumption that it would loop forever without a stop event. With the replay-timeline controller a recorded play is placed at its tick and kept alive until a recorded stop — or for the whole replay when it was never stopped — which is exactly how it ran, so an infinite effect now reproduces like a finite one. A looping showcase such asvfxweaver:graph_demoorvfxweaver:graph_logic_demoreplays correctly.
2.0.0 — 2026-09-20¶
2.0.0 gathers everything that landed after the previous release. Nearly all of it is additive — existing datapacks keep working unchanged — and it is the first release to carry the six Fabric + NeoForge jars for the 26.2 / 26.1.x / 1.21.11 lines.
Added¶
- Value graphs — an effect definition can drive any numeric input from a small node graph
(
graph+inputs) instead of one constant. Nodes:constant,time,random,noise,curve,math,mix,clamp,remap,bind,exprand the logic nodescompare,boolean,if,switch; reusablesubgraphsact as macros. Wiring is optional and lives outsideparams, so an older build ignores it and you can remove it without touching the rest. Reference built-ins:vfxweaver:graph_demoandvfxweaver:graph_logic_demo. - Per-pixel fields — a field-capable input can vary per pixel instead of once per frame, using
the built-in
noise,shape,gradient,curve,texture,depth,normal_facing,screen_uvandworld_posfunctions and simple compositions. First consumers:dent.intensityandcolor_grade.tint_r(built-insvfxweaver:dent_field_demo,vfxweaver:tint_field_demo). Depth/world fields needscreen_layer: 0; elsewhere they return the neutral value. - Masks — restrict where a post-processing effect applies, from a tree of screen shapes, world
volumes (
sphere/boxwithvolume: "surface"or"aura"), block-geometry leaves and code-registered GLSL shapes. A leaf whose source entity cannot be resolved drops only that leaf, never the whole mask, and never expands coverage.VFXAPI.sendMaskMove/maskMovemoves one leaf from the server or every tick. surface_pattern— projects a figure (or a texture) onto the terrain behind each pixel, anchored to a world position, with a structuralsurfaceblock choosing which faces receive it (faces, an inclusivemin/maxband,band_softness) and an optionalpattern.texture. Additive; needsscreen_layer: 0.- Spark particles — the
particleseffect can emit glowing additive sprites ("particle": "spark", or avfx_particlespreset with"kind": "spark") withcount,speed,life,gravity,bounce,size,trail,glowand size/colour curves. Built-in presets and playable references:vfxweaver:ember,vfxweaver:sparks. /vfx stop [<player>]— stops every active effect of a player (default: the executor). The old/vfx stop <effect> [players]form is unchanged; a bare token is read as an effect first, so target a player with a selector (@p/@a).
Changed¶
- Scene depth works on every supported Minecraft line, not only 26.2.
surface_pattern, the depth/world field functions and the depth-based mask shapes used to render on 26.2 alone. They now render on 26.1.2 and 1.21.11 as well; 26.2 behaviour is unchanged, and where no trustworthy depth is available the effect still fails closed rather than sampling the wrong convention. surface_patternno longer trails the player by one frame atscreen_layer: 0— the effect clock and camera/player snapshots now advance before layer 0.- Two plays of one masked definition share a single coverage (the first play's animated centre/radius/softness wins). Put the two masks in distinct definitions if they must differ.
- Mask coverage is screen-space, computed at layer 0 against the intact scene depth. A masked
effect running at a later layer can therefore tint the first-person hand where a masked block lies
behind it; run the masked effect at
screen_layer: 0, or depth-occlude the consumer, to avoid it. - The bundled mask demos were reworked for a clearer A/B:
vfxweaver:mask_entity_demoandvfxweaver:mask_world_demouse a fixed 4-block radius, andvfxweaver:mask_pulse_demoshows the growing-sphere, distance-derived look.
Fixed¶
VFXAPI.sendSetParamExprandsendMoveover the network now do what their names say. Both were misread by the client as a play and silently restarted the effect instead of editing or moving it; each action now has its own handler, so live edits work and replay correctly.- A
persistent: trueeffect withoutloopanimates and emits again. Itsstart/endparams used to freeze at their start value and particle/spark emission budgets could round down to nothing. - Long sessions no longer leak per-effect particle state — particle budgets, aimed-particle lists, rope simulations and spark budgets are cleaned up when an effect stops.
- Textured
surface_patternfigures resolve on every node, a missing sprite now shows the vanilla missing texture instead of drawing nothing, and the projections are crisp (NEAREST sampling, no mip bleed). - A cached texture view no longer goes stale after
/reloador a resource-pack change. surface_patternband edges andnormal_mask: 1.0no longer flicker.- Mask correctness batch — animated
softnessnow applies, right-nested compositions and over-cap leaves are per-file errors instead of mis-evaluating, and a block leaf's centre is animatable again. - A purely screen-space mask renders on every node and every layer, including where scene depth is unavailable.
- A malformed field
textureid fails only that one effect, not every post effect for the frame. surface_patternparse and projection fixes — an emptyfaceslist,pattern.center_x/y/zand entity-anchoredpositionsare now parse errors that name the problem.- A repeated explicit instance id no longer stacks duplicates — replaying a play with a non-zero
id restarts that instance in place, so
sendStop/sendMovestill address it. - A disconnecting player's server-side effect memory is released, so a long-lived server no longer accumulates per-player state for players who left.
- An unknown
particlesshapestops that frame cleanly instead of discarding the frame's emission budget. - Less per-frame work in the post chain (fewer lookups, cached block selection, parsed texture ids, no per-frame allocation in the sprite probe), and a mid-frame uniform-arena growth no longer risks using freed memory on the following frame.
Technical notes (for developers)¶
The user-visible list above is the changelog proper; these are the implementation details behind it.
- Per-node depth convention, proven from the real client jars. 26.2 is reversed
(
glClipControl(GL_LOWER_LEFT, GL_ZERO_TO_ONE)plus a near/far-swapped projection: near = 1, far = 0,CompareOp.GREATER_THAN_OR_EQUAL); 26.1.2 and 1.21.11 are standard (near = 0, far = 1). The flag is a per-node compile-time constant (VFXShaderPrograms.DEPTH_REVERSED) injected as theVFX_DEPTH_REVERSEDshader define, and the single shared reconstruction ininclude/camera.glslconverts the raw depth, picks the sky test and the block-occlusion comparison.depthRecipeVerified()gates every depth-needing pass. - The effect clock moved from the
FogRenderer.endFramehook (which runs afterrenderLevel) to before layer 0; layers ½ reuse the same snapshot and nothing advances twice. - One shared depth/world reconstruction in
include/camera.glsl, imported byfield.glslandsurface_pattern.fsh(was duplicated);normal_facingnow returns an outward, camera-facing normal. - Network receiver dispatch gives
STOP,SET_EXPRandMOVEeach one reachable handler (they were nested inside theSTOPbranch); the wire format is unchanged. - Persistence is a lifecycle flag, not an
Integer.MAX_VALUE-tick timeline, so animated params and emission budgets behave normally until stopped. - Per-instance state (vanilla-particle budgets, aimed-particle lists,
block_chainrope simulations, spark budgets) is keyed by instance id and pruned against the live set every frame. - Texture resolution has a real per-node implementation: 26.2/26.1.2 via the sprite
AtlasManager(SpriteIdkeyed byTextureAtlas.location()), 1.21.11 via the remapped modelAtlasManagerandTextureAtlas.getSprite; every source form funnels through oneresolved(...)factory that sets theRESOLVEDflag bit, and a standalone…/textures/…id gets.png. - The std140 layout guard queries the bare member name first with a qualified fallback, skips members the driver does not list, and logs at ERROR without throwing, so a driver that reports only bare names cannot drop the post layer.
- The cached texture view is re-derived from its descriptor on every use, because a reload closes and recreates the loaders' GPU view while reusing the descriptor.
normal_mask: 1.0now tests the snapped normal (abs(n.y) >= 0.5) instead of the raw normal, andnormal_maskis clamped to0..1to avoid a collapsedsmoothstepedge.- Animated mask
softnessis written intoshape_op[i].zwith the parse-time default as fallback, and also drives a composed custom leaf's falloff (previously hard-fixed at0.25). - Mask composition folds left-associatively, so a right-nested
op, a third custom leaf and a second block leaf are per-file parse errors rather than silent mis-evaluation. - The coverage prepass binds the main target's depth view (else its colour view) as the
DepthSamplerplaceholder when depth is untrusted, avoiding a feedback loop with the coverage target's own texture. - Parse fixes: an empty
surface.facesis an error,pattern.center_x/y/zis rejected in favour ofpattern.center, an entity-anchoredpositionsentry on asurface_patternis rejected, and an invalidpattern.texture.id/atlasis a per-file error. - Crisp pattern textures: NEAREST filtering, no mipmaps, each repeat spans
tile_scaleblocks, sprite-sheet cells are inset by half a texel,preserveuses the cell's real pixel aspect, andframewraps into0..cols*rows-1. - Internal cleanup: removed the write-only
ProgramInfo.coverage/VFXPass.coverageflag (the coverage prepass is identified by its pipeline/PassRole).
v1.2.0 / Guide v32¶
Added¶
- Block-model particles (
particlesblock mode +vfx_particlespresets) — theparticleseffect can now emit real block models instead of vanilla particles, chosen inline ("particle": "block"with ablockstate) or by preset id (data/<namespace>/vfx_particles/<name>.json, orVFXAPI.registerBlockParticle). Each particle has block-display brightness (-1= world light,[blockLight, skyLight]), gravity, air friction, optional world collision with surface friction and bounce, size, lifetime and spin; theparticlesparams override the preset's defaults.VFXAPI.spawnBlockParticle(spec, position, velocity)spawns a single particle immediately on the client. Presets are client-local and never synced, live in a two-layer registry (datapack wins over code), and render as client-side display entities so vanilla interpolates their motion. - Item-model particles (
particlesitem mode) — the same particle engine can now draw real item models:"particle": "item"with anitemid (e.g."minecraft:skeleton_skull"), or avfx_particlespreset declaring"item"instead of"block"(exactly one of the two is required; a missing or unknown entry is a per-file parse error). Items render asItemDisplayentities, with the same brightness/physics params as block particles. This is what makes skull/head particles possible — the block form has no baked model (a block-entity renderer draws it), the item form does.VFXBlockParticleSpec.item(ItemStack)/builder(ItemStack)build the spec from code; the API methods are unchanged. - Configurable block/item particle rotation —
vfx_particlespresets (and theVFXBlockParticleSpecbuilder) gainedspin_mode(tumble= the physical full-3D spin,yaw= a uniform spin about one axis,none= no rotation),spin_axis(random/x/y/z/[x, y, z]),spin_random(0..1: how random the initial orientation and tumble axis are;0= strictly upright and identical for every particle),spin_friction(0..1: how much the contacted block's friction damps the spin;0= never decays) andspin_roll(0..1: how much tangential impact speed feeds the tumble;0= no roll transfer). Thespin/spin_random/spin_friction/spin_rolleffect params override a named preset; the defaults reproduce the previous tumbling behaviour exactly, so existing presets are unchanged. - NeoForge support for all three Minecraft lines — a NeoForge node is built beside each Fabric node from the same source tree (
26.2-neoforge,26.1.2-neoforge,1.21.11-neoforge), so the project produces six jars, one per (Minecraft line, loader). The build scripts are split per loader (build.fabric.gradle/build.neoforge.gradle) and a platform layer (dev.vfxweaver.platform/client.platform) hides every loader API so the render, datapack, API and command code stays loader-agnostic. The only per-line loader API split is<26.1: NeoForge21.11has no submit-geometry event and reads the collector fromLevelRenderer.submitNodeStorage, while NeoForge26.1.2matches26.2; dependency ranges stay per line (deps.neo_compatfor NeoForge,deps.mc_compatfor Minecraft). Flashback recording is Fabric-only — Flashback has no NeoForge build, so the compatibility layer no-ops there. vfxweaver:block_chainbuilt-in demo — theblock_chaineffect type now ships a built-in definition, so it appears in/vfxtab-completion and/vfx play vfxweaver:block_chainworks without writing a datapack. It hangs a 6-linkminecraft:iron_chainfrom the local player (pos_x/y/zbound toplayer_x/y/z,physics: 1,length: 6,sway: 0.4), the same self-anchoring pattern as thevfxweaver:particlesdemo; built-in definitions now number 44.
Changed¶
- Block/item particles tumble like real falling cubes —
spin(degrees/tick) is now the magnitude of a full 3D angular velocity about a random axis, with a random full-3D initial orientation, instead of a yaw around world Y. The orientation is integrated each physics tick and slerped between ticks for rendering. On contact the tumble is damped by the contacted block's own friction (the sameBlock#getFrictionruleblock_chainuses), so a cube lands and stops spinning; a little of the tangential slip becomes roll, and a particle with no slip gains none.spin = 0now yields a static but randomly oriented model (previously upright).
Fixed¶
- Block/item particles move smoothly and spin on a sane axis — the model particles were submitted as geometry with their own pose interpolation, which read as jerky stepping and rotated around an odd pivot. They are now driven through client-side
BlockDisplay/ItemDisplayentities, so vanilla interpolates position between ticks,spindrives the model's orientation, and brightness/scale map onto the display's own brightness override and transformation. One display entity per live particle, removed on death/effect stop/world unload; the physics is unchanged. - Physics
block_chainropes now settle on surfaces instead of sliding forever — the depenetration is a pure displacement (pos and prev shift by the same correction, the player-push idiom) so the push itself injects no velocity, and on contact the inward velocity is dropped while the remaining tangential velocity is damped every contacting tick by the contacted block's own friction (Block#getFriction: stone/air 0.6, ice 0.98). A rope therefore grips stone, glides on ice, and a joint lifted out of the ground gains no residual horizontal velocity. - Physics
block_chainropes no longer sink through the ground — a rope joint that moved more than half a block into terrain in one tick was being ejected through the block's far face (nearest-face resolution), so the chain tunnelled downwards; the resolver now sweeps the path from the joint's previous position, stops at the first contact and pushes the joint back out through the face it entered, dropping only the inward normal component of its velocity and keeping the tangential part. Links rest on surfaces without sinking, a joint that spawns inside a block still comes free, and a moving anchor drags the chain across the ground (instead of the joint being reverted to its previous position and freezing there). Multi-box collision shapes are approximated by their union box. - NeoForge world overlays render on the
26.1.2line — overlays now capture the camera, the render buffers and the level renderer's submit storage from the level-render stage event; the earlierSubmitCustomGeometryEventpath left those unset on26.1.2, so every buffer-based overlay threw andblock_chaingeometry was submitted in a phase that was never drawn.
v1.1.4 / Guide v31¶
Changed¶
speed_lineslines are no longer evenly spaced - a newpos_randparam (default 1.0) jitters each line's angular position inside its own slice, so the layout is uneven instead of one line per equal sector.seednow drives a line's position as well as its length (animating it churns the whole layout),pos_rand: 0restores the old even spacing, and a line pushed against a slice boundary is measured to the nearest centre so it is not cut off. Also fixed the shader's uniform name for the line length (line_length->length, matching the param), which used to log "Found unknown but potentially supported uniform line_length" at every startup.
Fixed¶
- Replay recording was silently disabled when Flashback was installed - the mod registered two custom Flashback actions, and Flashback keys its action registry by the action class; both reflection proxies share one generated class, so the second registration threw
Action already registered, the whole compatibility init was aborted and plays, stops and live edits never reached the recording. Everything now travels through a single action (the datapack definitions snapshot is marked by a reserved id in its payload), and recordings made by earlier builds still decode. pulse_ringwiththickness: 0drew a hairline instead of nothing - the band width was clamped to a 0.05 minimum, so asking for a zero-width ring still produced a visible thin ring.0is now accepted and the ring is skipped entirely (the natural reading of "no band"), while every other value behaves as before.
v1.1.3 / Guide v30¶
Added¶
- Code-registered effect definitions (for client-only mods) -
VFXAPI.registerDefinitions(Map<Identifier, String>)andVFXAPI.unregisterDefinition(Identifier)add definitions at runtime with exactly the datapack validation, in a separate local layer that survives/reloadand a server sync. Before this a client-only companion mod could not own effect ids on a server at all: a mod-provideddata/<ns>/vfx/*.jsononly loads on a multiplayer client in single player (Fabric never reloadsSERVER_DATApacks there), andVFXDefinitionManager.applySyncedreplaced the whole definition set on join, soplayEffect("mymod:thing", ...)could not resolve. Local definitions stay private to the client (never synced to other players), the datapack/server layer wins for the same id, and broken entries are logged, skipped and surfaced by/vfx validate.
v1.1.2 / Guide v29¶
Added¶
- Client-local anchored playback -
VFXAPI.playEffect/playEffectIdnow accept an optional world position (Vec3) and an optional entity-UUID list when playing locally (no packet), so screen-space effects (dent,shockwave,vortex, ...) land where the event happened instead of at the definition's default spot; the position re-anchors the definition's spatial world bindings (screen_x,screen_y,proximity, ...), exactly like/vfx playat.VFXAPI.moveEffect(effectId, instanceId, worldPos)re-anchors a running instance locally - call it every tick to follow a moving point or entity. Positions win over the definition's entity anchors; with a null position the supplied UUIDs are zipped with the definition's entity anchors in declaration order (datapackentity_selectors stay server-side, so a client-side caller resolves the entities itself). Anchored local plays are recorded into Flashback replays with their anchor. - Client-side live control -
VFXAPI.setParam,VFXAPI.setParamExprandVFXAPI.setKeyframemirror the network actions locally (sendSetParam/sendSetParamExpr/sendKeyframe), so a pure client-side mod can drive effects without a server round-trip, next to the already localplayEffect/playEffectId/moveEffect/stopEffect. A null or blank easing means linear for a keyframe segment. - Chained animation segments ("from here") - a live keyframe with a negative time pins the value the parameter has right now at the current time and runs the segment to the new value over
|time|ticks, so animation variations chain without the caller knowing the current value: ramp a blur up and let it hold, thensendKeyframe(player, effect, "radius", -20, 0.0F, easing)fades it back out from where it stands. The effect stays a single running instance with one continuous curve (nothing is applied twice), and the wire format is unchanged - only the meaning of a negativetimeis new. - More
exprmath functions -floor,ceil,round,fract,sign,clamp(x, lo, hi),lerp/mix(a, b, t),step(edge, x),smoothstep(e0, e1, x),mod(a, b)(positive, like GLSL),tan,atan(y)/atan(y, x)(= atan2),expandlog(natural) join the existingsin,cos,abs,min,max,pow,sqrt,random()andnoise(x,y,z). Argument counts are now validated when the expression is compiled, with a message naming the function and the expected count - a wrong count used to fail per frame while evaluating instead. - Live edits are recorded into Flashback replays -
setParam,setParamExprandsetKeyframe(client-local and server-driven alike) now write a live-edit action into the active replay, so a replay reproduces a mod animating a running effect instead of only its initial play. The encoding reuses the trigger action with new negative sentinels (-3set-param,-4keyframe,-5set-expr), so recordings made by older builds still decode; only a newer recording replayed on an older build is unsupported.
v1.1.1 / Guide v27¶
Added¶
- Cubic-Bézier easing curves - standard CSS-style easing. Inline:
"easing": { "cubicBezier": [x1, y1, x2, y2] }; named:data/<namespace>/vfx_curves/<name>.jsonwith{ "cubicBezier": [...] }. Endpoints are fixed at (0,0)/(1,1); the y ordinates may leave 0..1 for anticipation/overshoot (e.g. ease-out-back =[0.34, 1.56, 0.64, 1]). Evaluated by solving the curve parameter for the given progress, so the motion is smooth instead of a piecewise-linear polyline. - Multi-version builds - the project now ships from one source for Minecraft
26.1.2and1.21.11(Stonecutter; per-node dependencies and Loom). Effect behaviour, the datapack format and the network protocol are identical across both. - Server-synchronized
scoreboardbindings - the server derives the tracked(objective, holder)pairs from the effect definitions it sends (anullholder resolves to the receiving player's name) and pushes value diffs once per tick; the client caches them (VFXScoreboardCache, the vanilla client-scoreboard mirror stays a fallback). This fixesscoreboardbinds on objectives that are not displayed in a slot - the vanilla client only mirrors displayed objectives, so a client-side read alone returned nothing. Pairs come only from definitions the server sent (no client subscription), all collections are bounded. The network protocol version is bumped - update client and server together.
Changed¶
- Verbose logging is now off by default. Per-request messages (a packet received, an effect started or scheduled, a replay applied) moved from
INFOtoDEBUG, so an integration that firessendSetParam/sendKeyframeevery tick no longer floods the game log; enable thevfxweaverlogger atDEBUGto see them. Warnings that a caller could repeat every tick (unknown effect, protocol mismatch, missing permission,playEffectwithout a client, an override for an undeclared parameter) are now emitted once per distinct key through a boundedVFXLog.warnOnceinstead of on every call. Startup and datapack-reload summaries (client initialized,Loaded N effect definitions,Loaded N VFX curves) stay atINFO. - Per-node Minecraft range and Fabric Loader floor. The
26.1.xjar now declaresminecraft >=26.1 <26.2(it used to emit~26.1.2, so Fabric Loader refused to load it on 26.1 and 26.1.1 even though the Modrinth page listed them), and the loader requirement is per node (>=0.18.4for the 26.x jars,>=0.17.3for1.21.11) instead of one>=0.19.5.
Fixed¶
- A
nulleasing no longer NPEs on the client-local play path.VFXClientAPIcalledEasingFunction.builtIn(null)(and the Flashback recorder calledeasing.name()), so a caller passing the documentednull("use the definition default") crashed before the effect manager's own null fallback could run; both now pass a null function through, and a recorded replay marks "no easing override" with a blank name. - Custom easing curves silently degraded to LINEAR. Named curves were resolved while a definition was parsed, which happens before the curve registry has loaded (reload listeners
prepare()beforeapply()), so the reference fell back to LINEAR; inline curves also lost their control points over the network (only a name travels). Named curves now resolve lazily (cached, with a warning on a genuine miss), inline curves send a blank name so the client uses its own definition copy, and the client applies synced curves before definitions. - Animated
start/endparameters now pass the end value through the easing. For a curve whose value at t=1 is not 1 (a triangle/wave), the final tick snapped to the rawendvalue instead of the eased one. /vfx stop <collection>now cancels the whole pending subtree, including nested collections, instead of matching only a child's own definition id.- Player-bound world overlays (
pos_x/y/zwithbind: player_x/y/z) no longer jitter at the 20 Hz tick rate and are no longer offset by +0.5 block on X/Z: dynamic (bound/expression) positions use exact sub-block coordinates and the player position is interpolated per frame.
Guide v26¶
Added¶
camera_shakehandparam - first-person hand multiplier (0..1, default 0.5): scales how strongly the camera-shake offset is re-applied to the held-item pose; at 0 the hand stays still while the world shakes, at 1 it moves together. The minimum across active shakes wins.camera_rolltilt is unaffected.particlesworld-overlay effect - emits vanilla particles in animated shapes with zero custom textures: definition fieldsparticle(any simple vanilla id;dusttakes animatablecolor_r/g/b+size) andshape(sphere/ring/helix/line/cube/point;linespans the first twopositionsslots). Animatable params:rate(per second, × fade weight),radius,height/turns/spin(helix),speed(radial launch velocity),vel_y. Emission is framerate-independent (budgeted per instance, clamped 1024/s and 256/frame), entity anchors andsendMovework as for other world overlays, rendering uses the vanilla particle path so shaderpacks stay compatible. New builtin demo:vfxweaver:particles(golden dust helix).particles: aimed accelerating streams -aim: 1launches every particle towards the secondpositionsslot (any shape/particle; drag-free ballistics via a Fabric access widener on Particle velocity fields, conespread, per-tickaccel,lifetimeoverride) — accelerating energy lines that fly into a target block or track entity-anchored slots.block_chainworld-overlay effect - a line of real textured block-model links between two anchors (theblockdefinition field picks the block;spacing/arc/scale/alignparams), rendered through the vanilla submit pipeline (COLLECT_SUBMITS+submitMovingBlock, shaderpack-safe). Links tile the span end-to-end and stretch when pulled taut.block_chain: verlet rope physics -physics: 1turns the chain into a client-side rope simulation at a fixed tick rate: gravity sag, world collision (links catch on blocks), the local player pushes links away,swaywind wobble. One anchor = hanging chain (lengthblocks); two anchors = rope pinned at both ends,lengthsets the total chain length (more than the span = deeper sag, less = taut). Joint-count changes resample the rope shape, so animatedlengthnever snaps. Links capped at 512; no server-side collision.- Entity anchors:
point-{"entity": "@s", "point": "center"}selects the reference point on the entity:feet(default),center(bounding-box centre) oreyes. - Entity anchors:
dir: "look"+distance- the anchor is pushed along the tracked entity's live look direction (eyes + look × 24 = a laser target where the entity is looking).
Fixed¶
particlescubeshape - two face axes reused one random value, collapsing points onto face diagonals; all six faces now sample uniformly.block_chainrendering - submit poses are camera-relative (links previously rendered off-screen); the physics rope integrates at a fixed tick rate with a time accumulator (rope froze at >20 FPS); joint-based rendering keeps links connected at bends, and joint-count changes resample the rope shape instead of resetting it.
Guide v25¶
Added¶
/vfx validate [namespace]command - dry-run definition health report: loaded count plus every broken datapack file with its parse error, optionally filtered by namespace (tab-completed). Operator-only.scoreboardworld binding - params can follow scoreboard values:{"bind": "scoreboard", "objective": "my_obj", "holder": "optional_name"}; default holder is the local player's own score, normalized onrange(default 16),invert/scaleas usual; usable as a value or amultiplymultiplier; missing objective/score evaluates to 0.- Java API: live expression override -
sendSetParamExpr(player, effectId, param, exprSource)swaps a running effect's parameter for a compiled math expression (same syntax as JSONexpr, per-instance seed) without restarting the timeline. Protocol actionSET_EXPR. - Java API: instance move -
sendMove(player, effectId, instanceId, Vec3)moves a running world-overlay instance to an exact point; per-tick calls produce smooth scripted motion. Protocol actionMOVE. - Serverbound effect requests - client mods can ask the server to play an effect via the new
vfxweaver:vfx_requestpacket: withoutbroadcastit plays only for the requester;broadcast: trueplays for every connected player and is gated behind operator (gamemaster) permission on the server.
Changed¶
- Built-in effects are datapack JSON now - all 42 built-in definitions moved from code to
data/vfxweaver/vfx/*.jsonresources inside the mod jar: they load through the regular datapack pipeline (so a broken built-in surfaces in/vfx list), sync to clients like any datapack file, and can be overridden/copied by packs (jar data is the lowest-priority layer). - Sub-block position precision - world-overlay geometry (
light_beam,pulse_ring,guide_line,block_tint,block_outline) consumes exactVec3coordinates: entity-anchored slots and API moves track at sub-block precision instead of snapping to the containing block; staticpositionsentries keep the historical block-centre behaviour. Network protocol version bumped 5 -> 6.
Guide v24¶
Added¶
- Entity-anchored
positionsfor world overlays - apositionsentry may be{"entity": "<selector>", "offset": [x,y,z]}(offset relative to the entity's feet, optional): the server resolves the selector once per play (plain/vfx play; fails when it matches nothing), the client follows the entity every frame. Slot order is preserved, soguide_lineendpoints can mix static and anchored entries; works forblock_tint,block_outline,light_beam,pulse_ring,guide_line./vfx playator a network position override wins over anchors. Java API: pass anchor UUIDs viaEffectRequest.target()in anchor order. No protocol change (reuses theentityUuidsfield). - Collections: full parameter specs on children - child
paramsvalues may bestart/end,keyframes,bind,expr,multiply(merged into a derived child definition); plain numbers keep working as constants.
Guide v23¶
Added¶
light_beambottom_fadeparam - fades the column alpha toward the bottom (0..1, default 0), mirroringtop_fade.
Fixed¶
light_beamunder shaderpacks - packs that declare vertex colourflat(e.g. Complementary) take each triangle's colour from one vertex, so the height fade rendered as clearly visible triangles ("broken" cylinders). Faded shells are now split into 32 narrow vertical slices, each quad carrying one uniform colour: flat-colour programs show a clean stepped fade with no triangle artifacts, vanilla gets an imperceptible stepped gradient.
v1.1.0 / Guide v22¶
Added¶
slice_shiftscreen effect - a straight line slices the frame; the halves slide along it with wrap/mirror fill (angle,offset,shift,mirror).noise_warpscreen effect - animated value-noise field warps the picture in fluid patches (scale,amplitude,contrast,coherence,speed,drift_x/y;time-driven morph).solarizescreen effect - bright pixels invert, dark stay (threshold,softness,intensity).double_visionscreen effect - two ghost copies with slow drift, energy-preserving blend (offset,ghost_opacity,drift,intensity).eyelidsscreen effect - two curved dark lids with correct open-state geometry, composited over the frame (openness,softness,curve).iris_wipescreen effect - old-film iris transition (radius,softness,center_x/y,zoom).digital_glitchscreen effect - band tearing + RGB split in slot-gated bursts with achanceparameter (not permanent tearing).vhsscreen effect - worn tape: a real crawling tracking band, wobble, bleed, washed contrast.shockwavescreen effect - refraction ring with full-weight composite (center_x/y,radius,width,amplitude,sharpness).afterimagescreen effect (feedback buffer) - decaying history echo with desaturation and optional drift zoom; history double-buffered, cleared on resize.stop_motionscreen effect - CPU hold-gated frame freezing at N updates/second (fps;<=1= full speed).entity_displaceentity effect - flat per-vertex displaced echo over the intact model (amplitude,scale,seed,alpha, color,through_blocks).light_beam/pulse_ring/guide_lineworld effects - additive world quad effects (columns, rings, dashed parabola).camera_rollmisc effect - dutch-angle camera tilt with optional sinusoidal wobble.
Changed¶
light_beam- layered cylindrical shells with cubic softness falloff (opaque core fading to a soft edge), optionaltop_scalefor tapering beams.pulse_ring- camera-facing billboard mode (flat ring always perpendicular to the camera) and arotparam for orientation in billboard mode.afterimage- history echo now uses an island blend (the ghost does not overwrite the live frame's transparency).camera_shake/camera_roll- now also shake/tilt the first-person hand, not just the world camera.entity_displace- vertex displacement over the intact model with a quantised (snap-glitch) field.
Removed¶
block_displace- opaque per-vertex block tearing effect. Cut before release (the look was not useful enough to keep the world-space hash + block-model pipeline).god_rays- additive body-beam entity effect. Cut before release (superseded bylight_beam).scan_sweep- sweep-sheet world effect. Cut before release (too close tolight_beam).hud_fade- HUD hide effect (binary F1-style hide +hide_hand). Cut before release (reverted; to be redesigned later).
Notes¶
- Screen effects that animate procedurally accept the auto-filled
timeparameter (effect age in ticks); film-grain-style shaders already used it. - The
exprparameter syntax (e.g. steppedseed) works in datapack definitions; command param-maps ({[...]}) accept floats only. - A
gradlew build-verified implementation batch; visual effects should be verified withgradlew runClient.
Fixed¶
- Effects replayed fresh on every world join and never expired. The reconnect memory used the server tick counter as its clock, which resets when the server instance is recreated (every singleplayer world reload): elapsed time collapsed to zero, so all previously played effects were re-applied at full duration on each join. The reconnect memory now uses wall-clock time (1 tick = 50 ms), stable across world reloads and restarts.
- Item frame overlay drawn twice / offset. The overlay hook fired on every PoseStack.popPose in the vanilla submit and the frame-local pose was rebuilt from identity - the quads landed at world origin or offset by the item transforms. It is now drawn once, anchored to the frame model pose with model-space coordinates matching the panel plane (z ~ 0.97).
v1.0.6 / Guide v21¶
Docs¶
- Documentation rewritten. Every effect now has a per-parameter reference (type, default, what it actually does) and copy-pasteable command/JSON examples.
Added¶
- Item frames as entity effect targets.
entity_tint/entity_outlinenow also apply to item frames (non-living entities): the UUID is captured from the frame renderer and a flat tint quad / rectangular outline is drawn on the frame plane, aligned with the frame model. vertex_displaceeffect (entities + blocks) — all vertices of the target model are randomly displaced; amount, field detail and the random seed are parameters, and the seed itself can be animated/bound to drive the glitch motion (stepped snaps or smooth morphing).look_atworld binding. Likelook, but the target direction is derived from a worldpos: [x,y,z]anchor instead of explicit yaw/pitch (rangedefault 90, supportsinvert/scale).
v1.0.5 / Guide v20¶
Added¶
- First-person hand effects.
entity_tint/entity_outlineactive on the local player now render on the first-person arm as well (the arm bypasses the normal entity submit path, so it needed its own hook).through_blocksis ignored there - the hand always draws on top of the world. camera_shakefrequencyparameter - noise oscillations per second, default 7 (previous fixed value).screen_layerparameter for all screen effects - where the effect applies:0= below the first-person hand and the GUI,1= above the hand below the GUI (default, previous behaviour),2= above everything including the GUI.- Datapack
region: [x0,y0,z0,x1,y1,z1]syntax for block effect positions. VFXAPI.EffectRequestfluent builder for play/send.
Fixed¶
- Persistent effects were dropped from reconnect memory instead of re-applied.
applyTotreated the-1duration of persistent effects as "expired" and deleted them when their viewer rejoined - a permanent entity tint disappeared forever after one relog. Persistent plays are now re-sent as-is on every join. - Parameter overrides for names not declared in the definition were silently dropped. Setting
through_blocksvia/vfx playentityon the built-in entity/block effects did nothing becausecreateTimelineonly applied overrides for declared params. Undeclared overrides now land as constants;through_blocksis also declared on the four built-ins (tab-completion). - Outlines now always render under their target. Entity outline with
through_blocks:1used to cover the entity with the shell colour; it is now drawn as an opaque shell before the body pass at a lower submit order, so only the rim around the silhouette survives while staying visible through walls. Block outlines ignorethrough_blocksentirely (always occluded) for the same reason.
v1.0.4¶
Added¶
[players]argument in/vfx playentity. The command now accepts an optional player list at the end — who sees the effect. Previously an entity effect was always sent only to the player who ran the command, so there was no way to show it to someone else (e.g. to all players in cutscene maps).
Examples:
- /vfx playentity vfxweaver:entity_outline @e[type=!player,distance=..10] @a — everyone sees the outlines;
- /vfx playentity vfxweaver:entity_tint @e[tag=boss] Alice Bob — only Alice and Bob see the tint.
Without [players] behaviour is unchanged (the executing player sees it). Each viewer gets their own copy of the effect, so it also survives their reconnects independently.
Changed¶
durationnow ends every non-looping instance. The one path that ignored it was/vfx seton a not-running effect: it used to start an immortal persistent instance whose animation was stretched overInteger.MAX_VALUEticks — visually frozen on its first frame, never removed on its own, and replayed from scratch after every reconnect. Such instances now start with the definition's owndurationand end on schedule like a normal play. Definitions explicitly marked"persistent": truekeep their until-stopped semantics.- Protocol version 4 → 5 (
vfxweaver:vfx_trigger). The play packet now carries a resume offset used when re-applying effects after a reconnect (see Fixed). A 1.0.4 client ignores packets from older servers and vice versa — update both sides together.
Removed¶
- The
/vfx keycommand. Nobody used it; runtime keyframing stays available to mods viaVFXAPI.sendKeyframeand the networkKEYFRAMEaction, which are unchanged.
Fixed¶
- Effects no longer restart from the first keyframe after a reconnect. The server keeps a per-player memory of running effects; on rejoin it re-sends each still-running one with an elapsed-time offset plus any runtime keyframes, so the animation continues exactly where it left off instead of starting over. Keyframes past the nominal duration also extend the effect's lifetime correctly — previously such effects were either dropped early or replayed from the beginning.
- Invisible effects no longer linger until the cap. A non-looping instance whose runtime edits have all animated down to zero (or been set to
0) is invisible but used to keep occupying one of the 64 active-effect slots until the oldest-effect eviction kicked in. It is now removed as soon as every edited parameter rests at zero. Definition-driven animations and looping effects are unaffected.
v1.0.3¶
Fixed¶
- Expression parser now accepts
_in identifiers — documented variables (player_x/y/z,light_level,time_of_day) were declared in theexprswitch but could never be parsed.
v1.0.2 / Guide v17¶
Added¶
- Flashback compatibility — client-local VFX effects (started via
VFXAPI.playEffector other mods on the client) are written into Flashback replays as custom actions and re-triggered during playback; effects already running when a recording starts are snapshotted into the replay. Flashback is a soft dependency (suggests, reflection-based, no compile-time coupling). Server-triggered effects already travel asvfxweaver:vfx_triggerpackets which Flashback replays on its own. - Server-side effect memory (
VFXServerEffects) — effects sent viaVFXAPI.sendEffectare remembered per player and re-applied on reconnect/join with their remaining duration (persistent-1effects always; finite ones while not expired). Pruned when expired; disabled during Flashback replay playback so replays are not doubled.
Changed¶
- Minecraft support widened to 26.1 – 26.1.2 (built against 26.1.2; the mod metadata's Minecraft range covers the whole 26.1 line, verified to compile on 26.1.2 without changes).
speed_linesreworked — lines now emanate from the screen borders as wedges (full width at the edge, clipped by it, tapering to a point towards the centre) with sharp step edges, instead of a radial band around the centre. Newlength_randparam (0..1) controls how much the per-line length varies with the seed.
v1.0.0 / Guide v16¶
Added¶
- Entity effects
entity_tint/entity_outline(by UUID),/vfx playentity. through_blocks(0/1) on both entity effect types.
Added¶
- Parameter overrides in
/vfx play,playat,playentity. These commands now accept an optional param-map{[name:value],...}(like/vfx set) that overrides the definition's default params at trigger time — including world coordinates (pos_x/y/z). This gives the command/datapack the same capability as the Java API (sendEffect(...overrides)).
Added¶
- New bindings and player variables. Bindings:
distance(raw distance from the camera toposin blocks),look_x/look_y/look_z(components of the camera's look vector),player_x/player_y/player_z(the local player's position). Math expressions (expr) now expose player variables:health,hunger,speed,light_level,time_of_day,player_x/y/z. Datapack examples:test_expr_health(screen_flash,expr: 1.0 - health),test_distance(vignette viabind: distance).
Added¶
- Feedback for broken datapacks.
/vfx listnow prints the list of datapack files that failed to parse on the last/reload, with the error text — previously they were silently skipped (log only).VFXDefinitionManagerstoresparseErrors(id → message).
Added¶
entity_selectorin the effect definition. Theentity_selectorfield (a selector string, e.g."@e[type=minecraft:zombie,distance=..10]") lets an entity effect (entity_tint/entity_outline) find its own targets: the server resolves the selector into UUIDs on every play, so/vfx play <effect>works withoutplayentity. Datapack example —test_zombie_outline.
Added¶
gradient_map: gradient build mode and colour coordinate. New paramsmode(0 = linear, 1 = constant/stepped) andpos(0..1, colour coordinate). In linear modeposshifts the transition centre (0.5 — no shift); in stepped mode it is a hard threshold: brighter thanpos→ colourto, darker →from(useful for masks/stylized shadows). The built-invfxweaver:gradient_mapgot defaultsmode: 0,pos: 0.5. True grayscale — linear withfrom=black,to=white,pos=0.5; a hard black/white mask — constant withpos=0.5 (0 — black, 0.5+ — white). Datapack examples:test_grayscale,test_grayscale_constant,test_gradient_constant.posterize: clean colour reduction. Removed the per-pixel dithering that produced large random colour steps at high strength ("pixelation"). The shader now does clean quantization (255 → 2 levels) without grain.
Added¶
- Entity effects respect the entity texture.
entity_tint/entity_outlinenow bind the entity texture (Sampler0) and use it as an alpha mask (like vanillarendertype_outline): transparent pixels are discarded, so the effect follows the texture silhouette rather than a flat box around the model.entity_tintgained atextureparam (0/1):1— recolour the texture (texture × colour, pattern visible),0— flat colour with the texture only as a mask. Render types memoized per entity texture.
Added¶
- Entity tint/outline (
entity_tint,entity_outline) — new effect types targeting entities by UUID. New subcommand/vfx playentity <effect> <targets>collects target UUIDs (up to 16) and sends them invfxweaver:vfx_trigger; the client stores the UUID on the render state of living entities (mixinLivingEntityRenderState) and, in a second pass, redraws the entity model with a custom render type:entity_tint— a solid translucent fill of the effect colour,entity_outline— an inverted hull (inflated silhouette with front faces discarded), thickness viawidth. Both supportthrough_blocks(0 — hidden behind walls, 1 — visible through them). Pipelines registered on the client, shaders —assets/vfxweaver/shaders/core/entity_fx.{vsh,fsh}.
Fixed¶
- Camera shake works again: the per-instance seed (
instanceSeed) shifted the noise domain byseed * 0.0001— for a random 64-bit seed that's ~10¹⁴,SimplexNoise.fastFlooroverflows the int cast, and all noise samples became exactly 0 → the shake silently didn't play. The seed is now masked to 32 bits (phase range ~4.3e5, safe for the int lattice).
Added¶
- Math expressions in params — a new way to set a param via
"expr": "sin(t * 0.1) + noise(x, y, z) * 0.2". Variables:t(ticks since start),x/y/z(camera coordinates), constantspi/e; functionssin,cos,abs,min,max,pow,sqrt,random()(0..1),noise(x,y,z)(simplex 3D, -1..1). The string compiles to an AST once (Recursive Descent Parser) when the instance is created and is evaluated every frame viaeval(t,x,y,z)— no per-frame parsing.random()/noise()are deterministic per-instance seed, so each instance gets its own noise. - Unique camera shake —
VFXActiveEffectcarries a randominstanceSeed;CameraShakeManagershifts the noise domain by this seed, so every/vfx play vfxweaver:camera_shakegives a non-repeating shake. SimplexNoisemoved to the shared (common) source (com.tom.vfx.noise) — now available both to math expressions and to camera shake.- Sound params
volume/pitch— the effect sound (thesoundfield) can now set volume and pitch via the reservedvolume/pitchparams, supporting all modes (constant, animation, world/camera bind, expression). Values are read once at start time (one-shot sound). Example:"volume": { "bind": "proximity", "pos": [8,80,8], "range": 32 }— louder near the point. - Positional sound
sound_pos— thesound_pos: [x,y,z]field plays the sound in the world at coordinates via the vanilla mechanism (like/playsound ... x y z, with distance falloff); without it the sound plays directly to the player. The position is overridable via the API (sound_pos_x/y/zinsendEffectoverrides).
Security¶
- Server packet sizes bounded: the param map in
vfxweaver:vfx_trigger— max 32 entries; the definition/curve maps invfxweaver:vfx_sync— 1024/256 entries (strings already capped byByteBufCodecs.STRING_UTF8). Protects the client from OOM on a hostile/broken server. VFXSyncPayloadgotprotocolVersion(checked on the client before applying).VFXEffectManager.playcaps server-supplied duration (MAX_DURATION_TICKS= 1 hour); persistent/loop semantics from the definition are preserved, but an arbitrary negative/hugedurationTicksfrom the server no longer creates an infinite effect.VFXEffectManager.stop(effectId, instanceId)checks that the instance belongs to the given effect — a server-supplied instance id cannot stop another instance.- The mutating
/vfxsubcommands (play,playat,stop,set,key) require operator rights (gamemaster level 2);/vfx liststays open. - The client log no longer prints the packet position (less noise on spam).
Fixed¶
- Datapack VFX effects and curves now sync with dedicated-server clients. Previously
VFXDefinitionManager/VFXCurveManagerloaded definitions only fromPackType.SERVER_DATA, so a dedicated-server client held only the built-invfxweaver:*and ignored custom datapack effects (Ignoring unknown VFX effect). Added a server→clientvfxweaver:vfx_syncpacket (raw definition/curve JSON) sent to each player on join (ServerLifecycleEvents.SYNC_DATA_PACK_CONTENTS) and to everyone after/reload(END_DATA_PACK_RELOAD); the client merges them over the built-ins.
Added¶
- Direct world-position passing in
VFXAPI.sendEffect(player, effectId, Vec3 worldPos, ...)— the client re-anchors spatial bindings (screen_x/y,proximity) to the point without thepos_x/y/zhack. positionandinstanceIdfields in thevfxweaver:vfx_triggerpacket — stopping a specific effect instance viasendStop(player, effectId, instanceId).- Custom easing curves: named files
data/<ns>/vfx_curves/<name>.json(an array of control pointspoints) and inline objects"easing": { "curve": [[t,v],...] }— anywhere an easing is expected (effect default, keyframe, collection child). - Multiplicative param modifier:
"strength": { "keyframes": [...], "multiply": { "bind": "proximity", ... } }— final value = base × multiplier (e.g. an animated dent fading with distance to a point). - Client
VFXAPI.playEffectId(...)returns the id of the created instance;VFXAPI.stopEffect(long instanceId)stops one specific instance.
Changed¶
- BREAKING:
PROTOCOL_VERSION2 → 4: the packet carries an optional position and instance id; the easing field is now a string name (built-in or custom curve id). /vfx playatmoved to the new packet position field (backward compat with the oldpos_x/y/ztrick kept).VFXTimelinesupports param multipliers alongside bindings (position rebinding reconfigures both).VFXDefinitionManager/VFXCurveManagerstore raw JSON sources for network sync; parsing moved intoreload().
Fixed¶
VFXDefinitionManager.prepare()no longer aborts loading all VFX definitions because of one malformed datapack file —catchwidened toIllegalArgumentException(previously an unknowntypeor brokenpositionswould hit it).
Changed¶
getModelQuads()(VFXWorldOverlayRenderer) now logs an error when collecting block geometry instead of silently swallowing it.
Docs¶
- The user guide was restructured, and the project gained separate Java API, architecture and changelog documents alongside the README and contribution guide.
v11¶
block_outlinehas two modes by the booleanshell(default0):0— extruded walls,1— a classic scaled shell with back faces, clipped by the block via the depth buffer.
v10¶
- The outline was reworked from a scaled shell to "walls" (each face is extruded outwards along its normal by
width/2) — the contour physically cannot cover the block in either mode. block_outlinethrough_blocksdefault is now0(occluded by other blocks),block_tint—1(see-through).
v9¶
through_blocks(0/1) onblock_tint/block_outline— visibility through blocks or with occlusion.- The outline no longer covers the block itself (the shell is drawn only with back faces and masked by the block's own depth).
/vfx playatcorrectly sets the position again (command positions take priority over the definition'spositions).soundoncollectionnow plays (locally, only to targeted players).
v8¶
- Added
vignette,screen_flash,motion_blur,fov_modifier. block_tintrestored.block_outlinerewritten as a scaled model shell with multi-block support (positions) and depth test disabled.- The network protocol gained an
actionfield (PLAY/STOP) and a version (protocolVersion). - Datapacks support
soundandpositions. - Adaptive blur;
distortionsupports negativeamountvalues. - The
/vfx playatcommand for quick block-effect testing by coordinates.
v7¶
block_tintremoved; the outline rewritten onto the custom shadervfxweaver:core/block_outline.
v6¶
- Tint fixed under Iris (custom pipeline instead of
debug_filled_box). - Overlay fault tolerance (try/catch per effect and on flush).
- The
lookbinding (yaw/pitch/range) to bind effects to the camera rotation.
v5¶
- Model geometry for tint/outline; outline with depth test (outside only);
loop; guide created.
v4¶
block_tint,block_outline,persistent/fade_ticks,collection. (historically: cube tint)
v3¶
- World bindings (
bind),dent.
v2¶
- Keyframes, two-pass blur, posterize, command hints.
v1¶
- Base shader effects,
camera_shake, datapacks, commands, network.