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.
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");
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 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
The fluent API provides a chainable interface for creating complex materials:
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");
// 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
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
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
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
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
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
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");
// 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");
// 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");
// 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());
The system automatically handles shader generation and caching:
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 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 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");
// 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");
// 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");
initializeMaterialSystem(resourceManager) instead of registerBuiltinMaterials()resourceManager.getMaterial() instead of registry.get()// 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
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
}
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
"copper_aged" instead of "mat1""plastic_red" instead of "red_shiny""matte_white" not "matteWhite"initializeMaterialSystem()clearAllMaterials() when needed"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.
// 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.