name: add-effect description: Use when adding a new transform effect, creating a post-process shader, or planning effect implementation. Provides the complete checklist of files and commonly-missed steps.
Adding a Transform Effect
Follow this checklist when adding a new transform effect to AudioJones. Effects use a self-registering modular structure — config, runtime state, lifecycle functions, and registration all live together in src/effects/.
Checklist Overview
Transform effects require changes across 5 files. The REGISTER_EFFECT macro at the bottom of the .cpp file handles lifecycle registration, descriptor metadata, UI dispatch, and pipeline integration — no central lists to edit. Per-effect runtime state lives in a file-local static <Name>Effect g_<field>State declared by the macro and accessed through pe->effectStates[TRANSFORM_<NAME>]; no PostEffect changes are needed.
Steps commonly missed:
- TransformOrderConfig::order array - Effect won't appear in reorder UI
- REGISTER_EFFECT macro at bottom of
.cpp- Effect won't init, won't appear in pipeline Get<Name>Effect()accessor and non-staticSetup<Name>()bridge - Macro can't link the setup binding
Phase 1: Effect Module
Create src/effects/{effect_name}.h:
#ifndef {EFFECT_NAME}_EFFECT_H
#define {EFFECT_NAME}_EFFECT_H
#include "raylib.h"
#include <stdbool.h>
// Brief description of what the effect does
struct {EffectName}Config {
bool enabled = false;
// Add effect-specific parameters with defaults
float speed = 1.0f; // Animation rate (radians/second)
};
#define {EFFECT_NAME}_CONFIG_FIELDS enabled, speed
typedef struct {EffectName}Effect {
Shader shader;
int {param}Loc; // One int per uniform
float time; // Animation accumulator (if animated)
} {EffectName}Effect;
bool {EffectName}EffectInit({EffectName}Effect* e);
void {EffectName}EffectSetup({EffectName}Effect* e, const {EffectName}Config* cfg, float deltaTime);
void {EffectName}EffectUninit({EffectName}Effect* e);
void {EffectName}RegisterParams({EffectName}Config *cfg);
#endif
Create src/effects/{effect_name}.cpp:
#include "{effect_name}.h"
#include "automation/mod_sources.h"
#include "automation/modulation_engine.h"
#include "config/constants.h"
#include "config/effect_descriptor.h"
#include "render/post_effect.h"
#include "imgui.h"
#include "ui/modulatable_slider.h"
#include <stddef.h>
bool {EffectName}EffectInit({EffectName}Effect* e) {
e->shader = LoadShader(NULL, "shaders/{effect_name}.fs");
if (e->shader.id == 0) return false;
e->{param}Loc = GetShaderLocation(e->shader, "{param}");
e->time = 0.0f;
return true;
}
void {EffectName}EffectSetup({EffectName}Effect* e, const {EffectName}Config* cfg, float deltaTime) {
e->time += cfg->speed * deltaTime;
SetShaderValue(e->shader, e->{param}Loc, &cfg->{param}, SHADER_UNIFORM_FLOAT);
}
void {EffectName}EffectUninit({EffectName}Effect* e) {
UnloadShader(e->shader);
}
void {EffectName}RegisterParams({EffectName}Config *cfg) {
ModEngineRegisterParam("{effectName}.{param}", &cfg->{param}, {min}f, {max}f);
}
{EffectName}Effect *Get{EffectName}Effect(PostEffect *pe) {
return ({EffectName}Effect *)pe->effectStates[TRANSFORM_{EFFECT_NAME}];
}
void Setup{EffectName}(PostEffect *pe) {
{EffectName}EffectSetup(Get{EffectName}Effect(pe), &pe->effects.{effectName},
pe->currentDeltaTime);
}
// === UI ===
static void Draw{EffectName}Params(EffectConfig *e, const ModSources *ms,
ImU32 glow) {
(void)glow;
{EffectName}Config *cfg = &e->{effectName};
ModulatableSlider("Param##{effectName}", &cfg->{param},
"{effectName}.{param}", "%.2f", ms);
}
// clang-format off
REGISTER_EFFECT(TRANSFORM_{EFFECT_NAME}, {EffectName}, {effectName},
"Effect Name", "{CAT}", {sectionIndex}, EFFECT_FLAG_NONE,
Setup{EffectName}, NULL, Draw{EffectName}Params)
// clang-format on
The REGISTER_EFFECT macro at the bottom handles:
- Descriptor metadata (display name, category badge, section index, flags, enabledOffset)
- Lifecycle registration (init, uninit, registerParams, getShader)
- Setup function dispatch binding
- UI draw callback (the dispatch system calls
drawParamsper effect)
The // === UI === section contains the colocated Draw{EffectName}Params() function with signature (EffectConfig*, const ModSources*, ImU32). The dispatch system in imgui_effects_dispatch.cpp handles section begin/end, enable checkbox, and transform reordering — the draw function only renders effect-specific sliders.
The setup function (Setup{EffectName}) is non-static (referenced by name in the registration macro) and lives just above the // === UI === divider, alongside the non-static Get{EffectName}Effect() accessor. The accessor casts pe->effectStates[TRANSFORM_{EFFECT_NAME}] to the effect's type; the bridge calls the module's {EffectName}EffectSetup() with that pointer plus &pe->effects.{effectName}. The Draw{EffectName}Params UI callback is static.
Use snake_case for filename, PascalCase for struct/function names.
Macro Variants
Pick the right macro based on the Init signature:
| Macro | Init signature | Use when |
|---|---|---|
REGISTER_EFFECT |
Init(Effect*) |
Most effects (simple init) |
REGISTER_EFFECT_CFG |
Init(Effect*, Config*) |
Init needs config (e.g., LUT setup) |
REGISTER_EFFECT_SIZED |
Init(Effect*, w, h) |
Init needs resolution |
REGISTER_EFFECT_FULL |
Init(Effect*, Config*, w, h) |
Init needs both |
REGISTER_GENERATOR |
Init(Effect*, Config*) |
Generator (bakes GEN/10/BLEND) |
REGISTER_GENERATOR_FULL |
Init(Effect*, Config*, w, h) |
Generator with resize |
REGISTER_SIM_BOOST |
No init | Sim boost (no lifecycle) |
For multi-pass effects with composite shaders (like bloom, anamorphic streak), write a manual EffectDescriptorRegister() call with custom GetShader/Resize wrappers instead of using a macro.
Macro Parameters
REGISTER_EFFECT(Type, Name, field, displayName, badge, section, flags, SetupFn, ResizeFn, DrawParamsFnArg)
Type:TransformEffectTypeenum value (e.g.,TRANSFORM_SINE_WARP)Name: PascalCase prefix for functions (e.g.,SineWarp)field: camelCase field name on PostEffect and EffectConfig (e.g.,sineWarp)displayName: UI display string (e.g.,"Sine Warp")badge: Category badgesection: Category section indexflags:EFFECT_FLAG_NONE,EFFECT_FLAG_HALF_RES,EFFECT_FLAG_NEEDS_RESIZESetupFn: Setup function name (e.g.,SetupSineWarp) — defined asstaticabove the macro in the same.cppResizeFn: Resize function orNULLDrawParamsFnArg: Colocated UI draw function (e.g.,DrawSineWarpParams) — signature:void (*)(EffectConfig*, const ModSources*, ImU32)
REGISTER_GENERATOR(Type, Name, field, displayName, SetupFn, ScratchSetupFn, section, DrawParamsFnArg, DrawOutputFnArg)
SetupFn: Blend compositor setup functionScratchSetupFn: Generator shader setup functionsection: Generator sub-category section index (10=Geometric, 11=Filament, 12=Texture, 13=Atmosphere)DrawParamsFnArg: Effect-specific UI sliders functionDrawOutputFnArg: Output section function (typicallyDrawOutput_{field}generated bySTANDARD_GENERATOR_OUTPUT)
Category badges: "SYM" (0), "WARP" (1), "CELL" (2), "MOT" (3), "ART" (4), "GFX" (5), "RET" (6), "OPT" (7), "COL" (8), "SIM" (9), "GEN" (10=Geometric, 11=Filament, 12=Texture, 13=Atmosphere).
Phase 2: Config Registration
Modify src/config/effect_config.h:
Add include at top with other effect headers:
#include "effects/{effect_name}.h"Add enum entry in
TransformEffectTypebeforeTRANSFORM_EFFECT_COUNT:TRANSFORM_{EFFECT_NAME},Add to order array in
TransformOrderConfig::orderinitialization:TRANSFORM_{EFFECT_NAME}Place at end of array, before closing brace. COMMONLY MISSED.
Add config member to
EffectConfigstruct:{EffectName}Config {effectName};
Phase 3: Shader
Create shaders/{effect_name}.fs.
Attribution (REQUIRED if derived from external source): If the research doc has an ## Attribution section, the shader file MUST begin with an attribution comment block before the #version line:
// Based on "Shader Title" by AuthorName
// https://www.shadertoy.com/view/XXXX
// License: CC BY-NC-SA 3.0 Unported
// Modified: [brief description of changes for AudioJones]
#version 330
in vec2 fragTexCoord;
out vec4 finalColor;
uniform sampler2D texture0;
uniform vec2 resolution;
// Add effect-specific uniforms
void main() {
vec2 uv = fragTexCoord;
vec4 color = texture(texture0, uv);
// Effect algorithm here
finalColor = color;
}
Phase 4: Preset Serialization
Modify src/config/effect_serialization.cpp:
Add include for the effect header (so the
*_CONFIG_FIELDSmacro is visible):#include "effects/{effect_name}.h"Add JSON macro with the other per-config macros, referencing the header's field-list macro:
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT({EffectName}Config, {EFFECT_NAME}_CONFIG_FIELDS)Add field name to the
EFFECT_CONFIG_FIELDS(X)X-macro table:X({effectName}) \This single entry handles both
to_json(writes if enabled) andfrom_json(reads with default).
Verification
After implementation, verify:
- Effect appears in transform order pipeline
- Effect shows correct category badge (not "???")
- Effect can be reordered via drag-drop
- Enabling effect adds it to the pipeline list
- UI controls modify effect in real-time
- Preset save/load preserves settings
- Modulation routes to registered parameters (if applicable)
- Build succeeds with no warnings
File Summary
| File | Changes |
|---|---|
src/effects/{effect}.h |
Config struct, Effect struct, lifecycle + RegisterParams declarations |
src/effects/{effect}.cpp |
Init, Setup, Uninit, RegisterParams, Get<Name>Effect() accessor, non-static Setup<Name>() bridge, colocated Draw*Params, REGISTER_EFFECT macro |
src/config/effect_config.h |
Include, enum, order array, config member |
shaders/{effect}.fs |
Create fragment shader |
src/config/effect_serialization.cpp |
JSON macro, X-macro field entry |