Back
Documentation

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.wgsl
  • your_scene.manifest.jsonc
  • your_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.wgsl

You 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.wgsl
  • shared/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:

  • kind
  • version
  • scene
  • scene_video_fx
  • mappings

Useful optional fields are:

  • song_title
  • band_name
  • audio
  • analysis_edits
  • assets
  • scene_settings
  • scene_video_fx_active
  • system_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"
  • id
  • version: 1
  • name
  • entry
  • video_fx_targets
  • mapping_targets

Optional sections include:

  • shader_version
  • author
  • text
  • asset_slots
  • audio_sources
  • audio_history_sources
  • audio_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 kind values 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 scene scope in this contract path
  • if a project attaches scene video-fx, the manifest must publish a scene target

mapping_targets

  • publishes stable scene parameters that projects and mappings can address
  • this is how scene_settings and mappings.target know what is legal

Supported manifest mapping target types currently include:

  • float
  • int
  • bool
  • vec2
  • vec3
  • vec4
  • color
  • string
  • text
  • file
  • font
  • image
  • resource
  • path:font
  • path:wgsl

text

  • declares generated text settings such as font_family, optional font_path, font sizes, tracking, and punctuation scaling
  • when text is configured, generated text slots such as font and font_metrics have 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:

  • bool requires a JSON boolean
  • int requires an integer
  • float requires a JSON number
  • vec2 requires a 2-number array
  • vec3 requires a 3-number array
  • vec4 requires a 4-number array
  • color accepts 3-number or 4-number arrays
  • font and path:font require .ttf or .otf
  • image requires image extensions such as .png, .jpg, .jpeg, .webp, .bmp
  • path:wgsl requires .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 use array<vec4<f32>, N> within runtime slot limits
  • if that uniform struct contains audio_scalars, it must use array<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/... or wgsl-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.wgsl
  • User/shared/scene.wgsl
  • System/shared/scene.wgsl
  • DevelopmentSystem/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:

  • effect
  • version

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.jsonc
  • effect_name.v1.wgsl

Typical location:

video-fx/
  bloom.v1.jsonc
  bloom.v1.wgsl

The 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 in passes
  • @group(0) @binding(0) as a float texture_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.wgsl
Relative imports

Quoted imports are resolved relative to the importing WGSL file.

Example:

  • shared/sunrise.wgsl
  • shared/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.wgsl
Composer behavior

The composer:

  • inlines imported files
  • removes #import lines 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 helpers
  • wgsl-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.low
  • audio.band.mid
  • audio.band.high
  • audio.drive.band.low
  • audio.drive.rhythm.beat
  • audio.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.wgsl

Start from a minimal full-screen triangle scene.

Step 2: create the sidecar manifest

Create:

shared/sunrise.manifest.jsonc

Use the same stem as the WGSL file.

Set:

  • kind: "scene-shader"
  • id
  • version: 1
  • name
  • entry: "sunrise.wgsl"
  • at least one video_fx_targets entry if scene post-fx should be allowed
  • mapping_targets for every stable parameter you want projects to address
Step 3: create the project file

Create:

demo.project.jsonc

Point 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.jsonc

You 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.jsonc

20. 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_blur

It creates:

  • documents\video-fx\prism_blur.v1.jsonc
  • documents\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.shader and scene.manifest in 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_settings keys that are not in mapping_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-fx library 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>.wgsl plus <name>.manifest.jsonc
  • video-fx pair: <effect>.v<version>.jsonc plus <effect>.v<version>.wgsl

Recommended folders:

  • shared/ for scenes and local WGSL helpers
  • video-fx/ for effect definitions and effect WGSL
  • assets/ for project assets
  • audio/ for project audio
  • wgsl-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.