WebGPU Composable Material System

Overview

The Composable Material System is a modern, fluent API for creating and managing materials in the WebGPU renderer. It eliminates the need for a separate MaterialRegistry and uses ResourceManager as the sole source of truth for all materials, providing better integration with GPU resource management.

Key Features

Quick Start

import { initializeMaterialSystem, flat, phong, Colors, Presets } from './materials/index.ts';

// Initialize the system with your ResourceManager
initializeMaterialSystem(resourceManager);

// Create materials using the fluent API
flat(Colors.RED, "My Red").register("red_material");

phong([0.8, 0.6, 0.4, 1.0], [0.9, 0.9, 0.7], 64, "Gold")
  .register("gold_material");

// Use presets for common materials
Presets.silver().register("silver_material");

// Get materials from ResourceManager
const redMaterial = resourceManager.getMaterial("red_material");

Core Components

Material Type

export type Material = {
  label?: string;                    // Human-readable name
  shader?: string;                   // Custom WGSL shader code
  color?: Vec4;                      // Flat color (RGBA)
  diffuse?: Vec4;                    // Diffuse color for lighting
  specular?: Vec3;                   // Specular color
  shininess?: number;                // Shininess factor
  normalmap?: "rainbow" | "rgb" | "grayscale";
  uniforms?: Record<string, unknown>; // Custom uniform data
  opacity?: number;                  // Transparency (0-1)
  transparent?: boolean;             // Force transparency
  blendMode?: "alpha" | "additive" | "multiply";
  matcap?: string;                   // Matcap texture name
  [key: string]: unknown;            // Extensible
};

ResourceManager Integration

ResourceManager now handles all material operations:

// Material storage and retrieval
resourceManager.registerMaterial(name: string, material: Material): void
resourceManager.getMaterial(name: string): Material | null
resourceManager.hasMaterial(name: string): boolean
resourceManager.updateMaterial(name: string, updates: Partial<Material>): boolean
resourceManager.deleteMaterial(name: string): boolean

// Batch operations
resourceManager.getAllMaterials(): Record<string, Material>
resourceManager.getMaterialNames(): string[]
resourceManager.clearMaterials(): void

// Cloning and variants
resourceManager.cloneMaterial(sourceName: string, newName: string): boolean

// Built-in materials
resourceManager.registerBuiltinMaterials(): void

Fluent API (MaterialBuilder)

The fluent API provides a chainable interface for creating complex materials:

Basic Usage

import { material, flat, phong, normalmap, custom } from './materials/index.ts';

// Basic flat color
flat([1.0, 0.0, 0.0, 1.0], "Red").register("my_red");

// Complex material with multiple properties
material()
  .label("Holographic Metal")
  .diffuse([0.7, 0.8, 1.0, 1.0])
  .specular([1.0, 1.0, 1.0])
  .shininess(256)
  .transparent(true)
  .opacity(0.8)
  .uniforms({ hologramStrength: 0.9 })
  .register("holographic");

Available Methods

// Material builder methods
.label(label: string)                    // Set display name
.color(color: Vec4Arg)                   // Flat color
.diffuse(diffuse: Vec4Arg)              // Diffuse color (Phong)
.specular(specular: Vec3Arg)            // Specular color (Phong)
.shininess(shininess: number)           // Shininess factor
.normalmap(type: "rainbow"|"rgb"|"grayscale") // Normalmap visualization
.shader(shaderCode: string)             // Custom WGSL shader
.opacity(opacity: number)               // Transparency (0-1)
.transparent(transparent: boolean)       // Force transparency
.blendMode(mode: "alpha"|"additive"|"multiply") // Blend mode
.uniforms(uniforms: Record<string, unknown>) // Batch uniforms
.uniform(name: string, value: unknown)  // Single uniform
.matcap(matcapName: string)            // Matcap texture

// Final operations
.build(): Material                      // Build material object
.register(name: string): Material       // Build and register
.asVariantOf(base: string, variant: string): Material // Create variant

Factory Functions

Quick material creation functions:

// Basic types
flat(color: Vec4Arg, label?: string): MaterialBuilder
phong(diffuse: Vec4Arg, specular?: Vec3Arg, shininess?: number, label?: string): MaterialBuilder
normalmap(type?: "rainbow"|"rgb"|"grayscale", label?: string): MaterialBuilder
custom(shaderCode: string, uniforms?: Record<string, unknown>, label?: string): MaterialBuilder
matcap(matcapName: string, label?: string): MaterialBuilder

// Generic builder
material(): MaterialBuilder

Constants and Presets

Color Constants

import { Colors } from './materials/index.ts';

Colors.WHITE     // [1.0, 1.0, 1.0, 1.0]
Colors.BLACK     // [0.0, 0.0, 0.0, 1.0] 
Colors.RED       // [1.0, 0.0, 0.0, 1.0]
Colors.GREEN     // [0.0, 1.0, 0.0, 1.0]
Colors.BLUE      // [0.0, 0.0, 1.0, 1.0]
Colors.YELLOW    // [1.0, 1.0, 0.0, 1.0]
Colors.CYAN      // [0.0, 1.0, 1.0, 1.0]
Colors.MAGENTA   // [1.0, 0.0, 1.0, 1.0]
Colors.GRAY      // [0.5, 0.5, 0.5, 1.0]
// ... more colors

Specular Constants

import { Speculars } from './materials/index.ts';

Speculars.NONE     // [0.0, 0.0, 0.0] - No reflection
Speculars.LOW      // [0.2, 0.2, 0.2] - Matte
Speculars.MEDIUM   // [0.5, 0.5, 0.5] - Semi-gloss
Speculars.HIGH     // [0.9, 0.9, 0.9] - Glossy
Speculars.METAL    // [1.0, 1.0, 1.0] - Mirror-like
Speculars.GOLD     // [0.9, 0.9, 0.7] - Warm metal
Speculars.COPPER   // [0.8, 0.6, 0.4] - Warm metal

Shininess Constants

import { Shininess } from './materials/index.ts';

Shininess.MATTE      // 4   - Very dull
Shininess.LOW        // 16  - Slightly shiny
Shininess.MEDIUM     // 64  - Moderately shiny
Shininess.HIGH       // 128 - Very shiny
Shininess.VERY_HIGH  // 256 - Mirror-like
Shininess.MIRROR     // 512 - Perfect mirror

Material Presets

import { Presets } from './materials/index.ts';

// Basic colors
Presets.white()     // White flat color
Presets.red()       // Red flat color
Presets.green()     // Green flat color
// ... more basic colors

// Metals
Presets.gold()      // Realistic gold Phong material
Presets.silver()    // Realistic silver Phong material  
Presets.copper()    // Realistic copper Phong material

// Plastics
Presets.plasticRed()   // Red plastic with high shininess
Presets.plasticBlue()  // Blue plastic with high shininess
Presets.plasticWhite() // White plastic with high shininess

// Matte materials
Presets.matteWhite()   // White with minimal specular
Presets.matteBlack()   // Black with no specular

// Normalmaps
Presets.rainbowNormals()    // Rainbow normalmap visualization
Presets.rgbNormals()        // RGB normalmap visualization
Presets.grayscaleNormals()  // Grayscale normalmap visualization

Usage Examples

Example 1: Basic Material Creation

import { initializeMaterialSystem, flat, phong, Colors, Speculars, Shininess } from './materials/index.ts';

// Initialize system
initializeMaterialSystem(resourceManager);

// Create flat colors
flat(Colors.RED, "Bright Red").register("red");
flat(Colors.BLUE, "Ocean Blue").register("blue");

// Create Phong materials
phong(
  [0.8, 0.6, 0.4, 1.0],  // Gold diffuse
  Speculars.GOLD,         // Gold specular
  Shininess.MEDIUM,       // Medium shininess
  "Custom Gold"
).register("my_gold");

Example 2: Advanced Materials

// Transparent glass
material()
  .label("Clear Glass")
  .diffuse([0.9, 0.95, 1.0, 0.1])
  .specular([1.0, 1.0, 1.0])
  .shininess(512)
  .transparent(true)
  .opacity(0.1)
  .blendMode("alpha")
  .register("glass");

// Custom shader with uniforms
custom(`
  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    let time = uniforms.time;
    let pulse = sin(time * 2.0) * 0.5 + 0.5;
    return vec4f(1.0, pulse, 0.0, 1.0);
  }
`)
  .uniforms({ time: 0.0, speed: 2.0 })
  .label("Pulsing Orange")
  .register("pulsing");

Example 3: Material Variants

// Create base material
Presets.gold().register("base_gold");

// Create variants
material()
  .shininess(512)        // Make it very shiny
  .uniform("reflectivity", 0.95)
  .asVariantOf("base_gold", "mirror_gold");

material()
  .specular(Speculars.LOW)     // Make it matte
  .shininess(Shininess.MATTE)
  .asVariantOf("base_gold", "matte_gold");

Example 4: Runtime Material Management

// Check if material exists
if (resourceManager.hasMaterial("gold")) {
  console.log("Gold material is available");
}

// Get material
const goldMaterial = resourceManager.getMaterial("gold");

// Update material properties
resourceManager.updateMaterial("gold", {
  shininess: 256,
  uniforms: { reflectivity: 0.9 }
});

// Clone material
resourceManager.cloneMaterial("gold", "gold_copy");

// List all materials
console.log("Materials:", resourceManager.getMaterialNames());

Shader Integration

The system automatically handles shader generation and caching:

Flat Color Optimization

All flat color materials use a shared shader with uniform-based colors for better performance:

// These all share the same cached shader
flat(Colors.RED).register("red");
flat(Colors.GREEN).register("green");
flat(Colors.BLUE).register("blue");

// Colors are passed as uniforms, not baked into shaders
// Better GPU pipeline caching and reduced compilation time

Normalmap Shaders

Normalmap materials automatically get appropriate shaders:

normalmap("rainbow").register("rainbow_normals");
// Automatically uses SHADER_PRESETS.RAINBOW()

normalmap("rgb").register("rgb_normals"); 
// Automatically uses SHADER_PRESETS.RGB_NORMALMAP()

normalmap("grayscale").register("grayscale_normals");
// Automatically uses SHADER_PRESETS.GRAYSCALE_NORMALMAP()

Custom Shaders

Custom shaders are supported with full uniform integration:

const noiseShader = `
  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    let scale = uniforms.noiseScale;
    let time = uniforms.time;
    let noise = sin(uv.x * scale + time) * cos(uv.y * scale);
    return vec4f(noise, noise, noise, 1.0);
  }
`;

custom(noiseShader)
  .uniforms({
    noiseScale: 10.0,
    time: 0.0
  })
  .register("noise_material");

Performance Benefits

Shared Pipeline Caching

Memory Efficiency

Compilation Speed

Migration from Old System

Before (MaterialRegistry)

// Old way - separate registry
import { getMaterialRegistry, registerBuiltinMaterials } from './materials/index.ts';

registerBuiltinMaterials();
const registry = getMaterialRegistry();

registry.register("my_material", {
  diffuse: [0.8, 0.6, 0.4, 1.0],
  specular: [0.9, 0.9, 0.7],
  shininess: 64
});

const material = registry.get("my_material");

After (ResourceManager + Composable API)

// New way - ResourceManager integration
import { initializeMaterialSystem, phong, Speculars } from './materials/index.ts';

initializeMaterialSystem(resourceManager);

phong([0.8, 0.6, 0.4, 1.0], Speculars.GOLD, 64, "My Material")
  .register("my_material");

const material = resourceManager.getMaterial("my_material");

Key Changes

  1. Initialization: Call initializeMaterialSystem(resourceManager) instead of registerBuiltinMaterials()
  2. Material Creation: Use fluent API builders instead of manual object creation
  3. Material Access: Use resourceManager.getMaterial() instead of registry.get()
  4. No Singletons: Pass ResourceManager explicitly instead of relying on global registry

API Reference

Core Functions

// System initialization
initializeMaterialSystem(resourceManager: ResourceManager): void

// Material creation
material(): MaterialBuilder
flat(color: Vec4Arg, label?: string): MaterialBuilder
phong(diffuse: Vec4Arg, specular?: Vec3Arg, shininess?: number, label?: string): MaterialBuilder
normalmap(type?: "rainbow"|"rgb"|"grayscale", label?: string): MaterialBuilder
custom(shader: string, uniforms?: Record<string, unknown>, label?: string): MaterialBuilder
matcap(matcapName: string, label?: string): MaterialBuilder

// Utility functions
listMaterials(): string[]
getAllMaterials(): Record<string, Material>
clearAllMaterials(): void
registerCommonMaterials(): void

// Legacy compatibility functions (factory functions still work)
defaultMeshMaterial(): Material
flatColorMaterial(color: Vec4Arg): Material
phongMaterial(diffuse: Vec4Arg, specular?: Vec3Arg, shininess?: number): Material
customMaterial(shader: string, uniforms?: Record<string, unknown>): Material
normalmapMaterial(type?: "rainbow"|"rgb"|"grayscale"): Material

MaterialBuilder Methods

class MaterialBuilder {
  label(label: string): MaterialBuilder
  color(color: Vec4Arg): MaterialBuilder
  diffuse(diffuse: Vec4Arg): MaterialBuilder
  specular(specular: Vec3Arg): MaterialBuilder
  shininess(shininess: number): MaterialBuilder
  normalmap(type: "rainbow"|"rgb"|"grayscale"): MaterialBuilder
  shader(shaderCode: string): MaterialBuilder
  opacity(opacity: number): MaterialBuilder
  transparent(transparent: boolean): MaterialBuilder
  blendMode(mode: "alpha"|"additive"|"multiply"): MaterialBuilder
  uniforms(uniforms: Record<string, unknown>): MaterialBuilder
  uniform(name: string, value: unknown): MaterialBuilder
  matcap(matcapName: string): MaterialBuilder
  
  build(): Material
  register(name: string): Material
  asVariantOf(baseName: string, variantName: string): Material
}

Testing

Test the new system with these global functions (available in browser console):

// Quick verification
quickComposableTest(resourceManager)

// Full demonstration
runAllComposableMaterialExamples(sceneState, parentId, resourceManager)

// Material animation (sets up pulsing materials)
animateMaterialStep() // Call in your animation loop

Best Practices

Material Naming

Performance

Organization

Resource Management

Troubleshooting

Common Issues

"Global ResourceManager not set": Call initializeMaterialSystem(resourceManager) before using material functions.

Material not found: Check material name spelling and ensure it was registered.

Shader compilation errors: Validate WGSL syntax in custom shaders.

Pipeline cache misses: Ensure materials with same properties use identical builders for proper caching.

Debugging

// Check what materials are available
console.log("Available materials:", resourceManager.getMaterialNames());

// Inspect a specific material
const material = resourceManager.getMaterial("my_material");
console.log("Material properties:", material);

// Check ResourceManager state
console.log("Total materials:", resourceManager.getMaterialNames().length);

This new composable material system provides a cleaner, more maintainable, and better-performing approach to material management while eliminating the complexity of separate registries and singleton dependencies.