Creators
Creating WGSL, Manifests, and Project Files in Bass Pixel Motion
This guide explains the current authoring model directly from the JSON schemas, resolver code, WGSL composer, and test fixtures in bass-pixel-motion.
The goal is practical: after reading this page, you should be able to create:
- a new scene WGSL file
- the matching sidecar scene manifest
- a valid project file
- optional video-fx documents
- portable path layouts that work with the virtual content-root resolver
It also explains how our virtual consolidation resolver works across project folders, shared content, user content, system content, development system content, and wgsl-lib imports.
1. The Mental Model
The authoring model is document-driven.
A working scene is not just a .wgsl file. It is a small document set:
1. a scene shader WGSL file 2. a sidecar scene manifest next to that WGSL file 3. a project file that points to the scene 4. optional assets 5. optional audio 6. optional video-fx definitions and their WGSL files
The normal production-facing entry point is the project file.
A project resolves into:
- scene WGSL
- sidecar scene manifest
- scene video-fx definitions and WGSL
- assets
- audio
- scene settings
- mappings
This is the same core used by verify, preview, export, and file-watching.
2. The Three Core File Types
For scene authoring, the core trio is:
your_scene.wgslyour_scene.manifest.jsoncyour_project.project.jsonc
Recommended structure:
my-project/
demo.project.jsonc
shared/
sunrise.wgsl
sunrise.manifest.jsonc
assets/
logo.png
video-fx/
bloom.v1.jsonc
bloom.v1.wgslYou can also place scene files directly beside the project file. The important rule is this:
- the project points to the scene WGSL
- the manifest stays as a sidecar next to that WGSL
3. Scene Resolution Rules
The project schema allows two scene reference forms:
- preferred:
scene.shader - legacy:
scene.manifest
Only one may be present.
Preferred form:
{
"scene": {
"shader": "shared/sunrise.wgsl"
}
}Legacy form still resolves, but the loader emits a warning and prefers the WGSL-first workflow.
When the project uses scene.shader, the resolver automatically finds the sidecar manifest by filename stem.
Example:
shared/sunrise.wgslshared/sunrise.manifest.jsonc
The resolver then validates that the manifest's entry field points back to the same WGSL file. If the manifest says entry: "other_scene.wgsl", loading fails.
4. Path Style Rules
Document paths are intended to be relative and slash-based.
The schemas for scene WGSL and manifest references explicitly expect forward-slash style path strings and reject Windows backslashes in those fields.
Use this style:
"shader": "shared/sunrise.wgsl"Not this:
"shader": "shared\\sunrise.wgsl"Absolute paths are technically accepted by the resolver, but they generate warnings and should not be used for portable content.
5. What the Project Schema Allows
The project schema currently requires these top-level fields:
kindversionscenescene_video_fxmappings
Useful optional fields are:
song_titleband_nameaudioanalysis_editsassetsscene_settingsscene_video_fx_activesystem_watermark_active
A minimal valid project looks like this:
{
"kind": "project",
"version": 1,
"scene": {
"shader": "shared/sunrise.wgsl"
},
"assets": {},
"scene_video_fx": [],
"mappings": []
}A more realistic project:
{
"kind": "project",
"version": 1,
"scene": {
"shader": "shared/sunrise.wgsl"
},
"audio": {
"path": "audio/main.wav"
},
"assets": {
"logo": {
"path": "assets/logo.png",
"kind": "png"
}
},
"scene_settings": {
"scene.sun.glow": 0.7,
"scene.asset.logo": "assets/logo.png"
},
"scene_video_fx": [
{
"effect": "bloom",
"version": 1,
"params": {
"threshold": 1.1,
"intensity": 0.45,
"radius": 2.0,
"soft_knee": 0.6,
"tint": [1.0, 0.95, 0.9]
}
}
],
"mappings": [
{
"source": "audio.band.low",
"target": "scene.sun.glow"
}
]
}6. What the Scene Manifest Publishes
The scene manifest is the public contract for the scene.
WGSL is the execution layer. The manifest is the creator-facing declaration layer.
The scene manifest schema requires:
kind: "scene-shader"idversion: 1nameentryvideo_fx_targetsmapping_targets
Optional sections include:
shader_versionauthortextasset_slotsaudio_sourcesaudio_history_sourcesaudio_history_samples
A minimal scene manifest:
{
"kind": "scene-shader",
"id": "sunrise",
"version": 1,
"name": "Sunrise",
"entry": "sunrise.wgsl",
"video_fx_targets": [
{ "scope": "scene" }
],
"mapping_targets": [
{
"target": "scene.sun.glow",
"type": "float",
"default": 0.4,
"min": 0.0,
"max": 2.0,
"description": "Glow intensity for the sun core."
}
]
}What each manifest section means
entry
- relative path from the manifest to the WGSL scene file
- must match the actual sibling WGSL that resolved the sidecar
asset_slots
- named asset bindings the scene expects
- each slot declares whether it is required and which
kindvalues are allowed
Example:
"asset_slots": {
"logo": {
"required": false,
"kinds": ["png", "jpg", "jpeg", "webp"],
"description": "Optional logo overlay."
}
}video_fx_targets
- publishes which effect scopes are allowed by the scene
- current registry-backed validation allows only the
scenescope in this contract path - if a project attaches scene video-fx, the manifest must publish a
scenetarget
mapping_targets
- publishes stable scene parameters that projects and mappings can address
- this is how
scene_settingsandmappings.targetknow what is legal
Supported manifest mapping target types currently include:
floatintboolvec2vec3vec4colorstringtextfilefontimageresourcepath:fontpath:wgsl
text
- declares generated text settings such as
font_family, optionalfont_path, font sizes, tracking, and punctuation scaling - when text is configured, generated text slots such as
fontandfont_metricshave special runtime handling
audio_sources and audio_history_sources
- publish which audio contracts the scene wants at runtime
- ids resolve through the feature params registry and audio resolution logic
- aliases and drive variants are normalized by runtime code
7. Scene Settings vs Mappings
These two mechanisms are related but not the same.
scene_settings
- static values stored in the project
- validated against the scene manifest's published
mapping_targets - type-checked by the loader
Examples:
- a color override
- a font path
- an image path
- a default numeric tuning value
mappings
- runtime connections from a source to a published target
- example source:
audio.band.low - example target:
scene.sun.glow
So the manifest publishes the target, and the project decides whether it wants to set it statically, map it dynamically, or both.
8. How scene_settings Type Validation Works
The runtime validates project scene_settings directly against the manifest's mapping_targets.
That means:
- unknown targets fail
- wrong value types fail
- path-like types must look like local files, not URLs
Examples:
boolrequires a JSON booleanintrequires an integerfloatrequires a JSON numbervec2requires a 2-number arrayvec3requires a 3-number arrayvec4requires a 4-number arraycoloraccepts 3-number or 4-number arraysfontandpath:fontrequire.ttfor.otfimagerequires image extensions such as.png,.jpg,.jpeg,.webp,.bmppath:wgslrequires.wgsl
This makes the manifest the real source of truth for project-authored scene configuration.
9. A Minimal Working Scene WGSL
The current scene shader contract validator enforces these hard rules:
- the composed WGSL must parse successfully
- the scene must define
@vertex fn vs_main(...) - if
@group(0) @binding(0)exists, it must be a uniform buffer struct - if that uniform struct contains
params, it must usearray<vec4<f32>, N>within runtime slot limits - if that uniform struct contains
audio_scalars, it must usearray<vec4<f32>, N>within runtime slot limits
A minimal working scene from the fixtures looks like this:
struct SceneUniform {
time: f32,
resolution: vec2<f32>,
_padding: f32,
}
@group(0) @binding(0)
var<uniform> scene: SceneUniform;
struct VsOut {
@builtin(position) position: vec4<f32>,
}
@vertex
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VsOut {
var positions = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -3.0),
vec2<f32>(-1.0, 1.0),
vec2<f32>(3.0, 1.0),
);
var out: VsOut;
out.position = vec4<f32>(positions[vertex_index], 0.0, 1.0);
return out;
}
@fragment
fn fs_main() -> @location(0) vec4<f32> {
let glow = 0.4 + 0.1 * sin(scene.time);
return vec4<f32>(1.0, 0.7 + glow * 0.1, 0.2, 1.0);
}For real scenes, you will usually also define the fragment entry and bind any extra runtime resources your scene path expects.
10. The Practical Rule for Scene WGSL
When authoring a new scene WGSL, think in two layers:
Layer 1: WGSL implementation
- vertex and fragment logic
- math
- helper functions
- rendering behavior
- optional imports from
shared/...orwgsl-lib/...
Layer 2: manifest contract
- stable scene id
- user-visible name
- asset slots
- published mapping targets
- allowed video-fx scopes
- audio source requests
- text contract
Do not try to make raw WGSL act as your only reflection system. That is explicitly not how this system is designed.
11. The Virtual Consolidation Resolver
This is the most important authoring rule after the manifest itself.
The path resolver does not only look in the project directory. It can resolve a relative reference against a virtual set of content roots.
Candidate roots
When content roots are active, a relative path is checked against these candidate directories:
1. the project directory 2. the user content root 3. the active system content root
If development system content is configured, it replaces the installed system content root as the active system root.
So the effective search model is a virtual union of:
- project content
- user content
- active system content
Resolution outcome
For each relative reference, the resolver builds all candidate full paths and checks them.
It then requires exactly one match.
- no matches: error
- one match: success
- more than one match: error because the reference is ambiguous
This is deliberate. The system does not silently pick one winner between user and system content.
Why shared/... works
A project can say:
"shader": "shared/scene.wgsl"That does not mean "look only next to the project".
It means "resolve shared/scene.wgsl across the current virtual content roots".
That is why the same project can load a scene from:
project/shared/scene.wgslUser/shared/scene.wgslSystem/shared/scene.wgslDevelopmentSystem/shared/scene.wgsl
As long as exactly one of those exists in the active root set.
Development system override
The code prefers a development system content root over the installed system content root.
That means:
- active system root becomes the development root
- installed system root becomes inactive
- project directories that are physically inside the inactive system root are ignored during candidate-dir construction
This prevents accidental double-scanning and lets development content replace installed content cleanly.
Default configured locations
The service-side content root configuration derives from %LOCALAPPDATA%\BassPixelMotion\Content.
Default roots are:
- user:
%LOCALAPPDATA%\BassPixelMotion\Content\User - system:
%LOCALAPPDATA%\BassPixelMotion\Content\System
An app config can additionally provide a development system content root, which becomes the active system root.
12. What Belongs in Each Root
A practical layout is:
project/
demo.project.jsonc
shared/
sunrise.wgsl
sunrise.manifest.jsonc
assets/
logo.png
audio/
main.wav
video-fx/
local_effect.v1.jsonc
local_effect.v1.wgsl
User/
shared/
video-fx/
assets/
System/
shared/
video-fx/
schema/
registry/
wgsl-lib/Recommended usage:
- project root: project-specific scenes, assets, audio, local overrides
- user root: creator-local reusable content
- system root: shipped shared content, schemas, registry files, shared WGSL library
13. Video-FX Lookup Rules
Scene video-fx definitions are discovered by directory scan, not by direct explicit file path in the project.
For every candidate content root directory, the resolver scans:
<root>/video-fx/*.jsonc
Every .jsonc file found there is loaded as a potential video-fx definition.
Project scene_video_fx entries then match by:
effectversion
If the project asks for bloom version 1, the resolver expects to find a matching definition somewhere in the active video-fx libraries.
If the same effect + version pair appears more than once across roots, loading fails.
This means the virtual consolidated resolver applies not only to direct file references, but also to effect-library discovery.
14. Video-FX File Pairing
A video-fx is also a paired document set:
effect_name.v1.jsonceffect_name.v1.wgsl
Typical location:
video-fx/
bloom.v1.jsonc
bloom.v1.wgslThe definition's entry field points to the WGSL file relative to the definition file.
Example:
{
"kind": "video-fx",
"id": "bloom",
"version": 1,
"targets": ["post", "element"],
"release_status": "shipping",
"entry": "bloom.v1.wgsl",
"params_schema": {
"type": "object",
"properties": {
"threshold": {
"type": "number",
"x-bpm-slot": 0,
"default": 1.0
}
},
"required": ["threshold"],
"additionalProperties": false
}
}15. Video-FX WGSL Contract
The current video-fx WGSL validator enforces stronger ABI rules than the scene validator.
A valid video-fx shader must provide:
@vertex fn vs_main(...)@fragment fn fs_main(...)or pass-specific fragment entries declared inpasses@group(0) @binding(0)as a floattexture_2d@group(0) @binding(1)as a sampler@group(0) @binding(2)as a uniform buffer struct
Its uniform layout must contain these exact fields in the shared ABI:
slots: array<vec4<f32>, 8>texel_size: vec2<f32>_padding: vec2<f32>timeline: vec4<f32>
Extra resource_inputs produce extra required bindings in pairs.
So scene WGSL and video-fx WGSL are not interchangeable. They live under different contracts.
16. WGSL Composition and Imports
Before validation, WGSL goes through the composer.
The composer supports three import forms:
Relative import:
#import "shared/math.wgsl"Library import with angle brackets:
#import <bpm/common/noise.wgsl>Library import with prefix syntax:
#import bpm/common/noise.wgslRelative imports
Quoted imports are resolved relative to the importing WGSL file.
Example:
shared/sunrise.wgslshared/math.wgsl
Then inside sunrise.wgsl:
#import "math.wgsl"or from a parent file:
#import "shared/math.wgsl"Library imports
Library imports resolve against the active WGSL library root:
<documents_root>/wgsl-lib
So:
#import <bpm/video_fx/bloom_core.wgsl>resolves to:
<wgsl-lib>/video_fx/bloom_core.wgslComposer behavior
The composer:
- inlines imported files
- removes
#importlines from final composed WGSL - deduplicates transitive imports
- rejects import cycles
- tracks dependency files for watch/reload logic
This is the correct place for shared methods and reusable WGSL helpers.
17. shared vs wgsl-lib
Use shared/... for content-level WGSL modules that belong to a project, user root, or system root as normal files.
Use wgsl-lib/... for stable reusable library code that should be imported with the composer library syntax.
A good rule is:
shared: scene-local or content-package-local helperswgsl-lib: broadly reusable engine-side or package-side library helpers
18. The Feature Params Registry Matters
The registry fixture and runtime registry validation make one design decision very clear:
the manifest contract is not arbitrary.
It is anchored to a central registry of:
- allowed mapping target value types
- audio source ids and families
- manifest field conventions
- asset slot conventions
- video-fx target conventions
In practice, this means:
- do not invent random audio ids
- do not invent random mapping target type names
- follow the published field and type conventions
Examples of real audio ids used by the system:
audio.band.lowaudio.band.midaudio.band.highaudio.drive.band.lowaudio.drive.rhythm.beataudio.drive.spectrum.band_00
The registry also describes aliases and drive-family derivation behavior, so creator-facing manifests should stay close to those canonical contracts.
19. How to Create a New Scene Step by Step
Step 1: create the WGSL file
Create a new file such as:
shared/sunrise.wgslStart from a minimal full-screen triangle scene.
Step 2: create the sidecar manifest
Create:
shared/sunrise.manifest.jsoncUse the same stem as the WGSL file.
Set:
kind: "scene-shader"idversion: 1nameentry: "sunrise.wgsl"- at least one
video_fx_targetsentry if scene post-fx should be allowed mapping_targetsfor every stable parameter you want projects to address
Step 3: create the project file
Create:
demo.project.jsoncPoint it at the scene WGSL:
"scene": {
"shader": "shared/sunrise.wgsl"
}Step 4: add assets if needed
If the scene declares asset slots, bind matching assets in the project's assets object.
Step 5: add scene_settings if needed
Only use keys that the manifest published through mapping_targets.
Step 6: add mappings if needed
Connect valid runtime sources to valid published targets.
Step 7: verify early
Use:
bpm.exe verify --project path\to\demo.project.jsoncYou can also verify files individually:
bpm.exe verify --file path\to\shared\sunrise.wgsl
bpm.exe verify --file path\to\shared\sunrise.manifest.jsonc
bpm.exe verify --file path\to\video-fx\bloom.v1.jsonc20. How to Create a New Video-FX Step by Step
There is a helper script for new video-fx authoring:
.\new-video-fx.ps1 -Id prism_blurIt creates:
documents\video-fx\prism_blur.v1.jsoncdocuments\video-fx\prism_blur.v1.wgsl
It also:
- enforces snake_case ids
- sets the WGSL entry path
- initializes targets
- reminds you to finish
params_schema,resource_inputs,passes, and fragment implementation
This is currently the main scaffolding tool in the repository for authoring new WGSL-backed effects.
21. The Main Authoring Tools
The practical toolset for creators and technical authors is currently:
bpm.exe verify
- validates project files, manifests, WGSL scene sidecars, and video-fx definitions
- catches schema errors, reference errors, and shader contract errors
bpm.exe preview --project ...
- runs the document-driven preview path
- this is the main iteration path for real project authoring
bpm.exe export --project ...
- runs export from the same project-driven core
bpm.exe inspect-audio --audio ...
- produces audio analysis reports and diagnostics
- useful when authoring audio-reactive scenes
new-video-fx.ps1
- scaffolds new video-fx document pairs
File watching and hot reload
- the resolver tracks dependencies from project, manifest, WGSL, imported WGSL, video-fx, and assets
- changed WGSL triggers shader recompilation
- changed manifests and project files trigger revalidation and reload
22. Authoring Pitfalls to Avoid
Do not do these:
- do not put the scene contract only in WGSL and skip the manifest
- do not reference both
scene.shaderandscene.manifestin one project - do not use backslashes in schema-governed document paths
- do not use absolute paths unless you intentionally accept warnings and non-portability
- do not duplicate the same relative file in user and system roots if the project expects a unique resolution
- do not publish
scene_settingskeys that are not inmapping_targets - do not invent arbitrary mapping target type names
- do not assume a video-fx file is found by direct path; it is resolved through the
video-fxlibrary scan - do not put shared reusable import code only in ad hoc folders if it really belongs in
wgsl-lib
23. Recommended Naming and Layout Conventions
Use these conventions consistently:
- scene ids: lowercase snake or kebab-like ids matching
^[a-z0-9][a-z0-9_-]*$ - effect ids: lowercase snake-like ids
- project paths inside JSONC: forward slashes
- scene pair:
<name>.wgslplus<name>.manifest.jsonc - video-fx pair:
<effect>.v<version>.jsoncplus<effect>.v<version>.wgsl
Recommended folders:
shared/for scenes and local WGSL helpersvideo-fx/for effect definitions and effect WGSLassets/for project assetsaudio/for project audiowgsl-lib/for shared library imports
24. A Complete Small Example
shared/sunrise.wgsl
struct SceneUniform {
time: f32,
resolution: vec2<f32>,
_padding: f32,
}
@group(0) @binding(0)
var<uniform> scene: SceneUniform;
struct VsOut {
@builtin(position) position: vec4<f32>,
}
@vertex
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VsOut {
var positions = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -3.0),
vec2<f32>(-1.0, 1.0),
vec2<f32>(3.0, 1.0),
);
var out: VsOut;
out.position = vec4<f32>(positions[vertex_index], 0.0, 1.0);
return out;
}
@fragment
fn fs_main() -> @location(0) vec4<f32> {
let glow = 0.4 + 0.2 * sin(scene.time);
return vec4<f32>(1.0, 0.72 + glow * 0.08, 0.24, 1.0);
}shared/sunrise.manifest.jsonc
{
"kind": "scene-shader",
"id": "sunrise",
"version": 1,
"name": "Sunrise",
"entry": "sunrise.wgsl",
"asset_slots": {
"logo": {
"required": false,
"kinds": ["png", "jpg", "jpeg", "webp"]
}
},
"video_fx_targets": [
{ "scope": "scene" }
],
"mapping_targets": [
{
"target": "scene.sun.glow",
"type": "float",
"default": 0.4,
"min": 0.0,
"max": 2.0
},
{
"target": "scene.asset.logo",
"type": "image",
"default": "assets/logo.png"
}
]
}demo.project.jsonc
{
"kind": "project",
"version": 1,
"scene": {
"shader": "shared/sunrise.wgsl"
},
"assets": {
"logo": {
"path": "assets/logo.png",
"kind": "png"
}
},
"scene_settings": {
"scene.sun.glow": 0.7,
"scene.asset.logo": "assets/logo.png"
},
"scene_video_fx": [],
"mappings": []
}This is the smallest complete authoring pattern to copy.
25. Final Practical Guidance
If you remember only five rules, remember these:
1. Start from the project file, not from raw WGSL alone. 2. Every scene WGSL should have a sidecar manifest with the same stem. 3. Publish stable creator-facing parameters through mapping_targets. 4. Treat path resolution as a virtual union of project, user, and active system roots, where exactly one match must exist. 5. Use wgsl-lib imports for reusable shader library code and verify early with bpm.exe verify.
That is the current full authoring model implemented by the schemas and source code.