Modular PBR material layering system for Three.js with TSL node-based blending
npm install @interverse/three-layered-materialA procedural, multi-layer physically based material system built using Three.js TSL and MeshPhysicalNodeMaterial that brings Substance Painter-style layering into Three.js runtime.
``bash`
npm install @interverse/three-layered-materialor
yarn add @interverse/three-layered-material
Peer Dependencies:
- three >= 0.182.0
Think of this as Substance Painter + Gaea terrain shader โ inside Three.js. Each layer is a complete material slice that gets blended together on the GPU with advanced masking, blending, and procedural effects.
`typescript
import { LayeredMaterial } from '@interverse/three-layered-material';
import * as THREE from 'three';
const loader = new THREE.TextureLoader();
const material = new LayeredMaterial({
layers: [
{
name: 'Base Metal',
map: {
color: loader.load('metal_color.jpg'),
normal: loader.load('metal_normal.jpg'),
roughness: loader.load('metal_roughness.jpg'),
},
roughness: 0.3,
metalness: 0.8,
},
{
name: 'Rust Overlay',
map: {
color: loader.load('rust_color.jpg'),
normal: loader.load('rust_normal.jpg'),
},
mask: {
map: loader.load('rust_mask.png'),
channel: 'r',
useNoise: true,
},
blendMode: {
color: 'overlay',
normal: 'rnb',
}
}
]
});
// Apply to any mesh
const mesh = new THREE.Mesh(geometry, material);
`
Each layer supports these properties:
typescript
map: {
color?: THREE.Texture, // Base color
normal?: THREE.Texture, // Normal map
roughness?: THREE.Texture, // Roughness
metalness?: THREE.Texture, // Metalness
ao?: THREE.Texture, // Ambient Occlusion
height?: THREE.Texture, // Height map for parallax/blending
arm?: THREE.Texture, // Packed ARM (AO/Rough/Metal in RGB)
}
`$3
`typescript
scale?: number; // Texture tiling scale
roughness?: number; // Fallback roughness (0-1)
metalness?: number; // Fallback metalness (0-1)
colorTint?: { // Color multiplier (NEW!)
r: number; // Red channel (0-1, default 1)
g: number; // Green channel (0-1, default 1)
b: number; // Blue channel (0-1, default 1)
};
`๐ Using Materials as Layer Inputs
One of the most powerful features is the ability to use existing Three.js materials as layer inputs. This allows you to:
- Reuse pre-built materials from your library
- Combine node materials with layered blending
- Apply masking and features to existing materials
$3
`typescript
import { LayeredMaterial } from '@interverse/three-layered-material';
import { MeshPhysicalNodeMaterial } from 'three/webgpu';// Create or import an existing node material
const existingMaterial = new MeshPhysicalNodeMaterial({
color: 0x4488ff,
roughness: 0.3,
metalness: 0.8
});
// Use it as a layer
const material = new LayeredMaterial({
layers: [
{
name: 'Base',
map: { color: baseTexture }
},
{
name: 'Overlay Material',
materialInput: existingMaterial, // Use material as layer!
mask: {
useSlope: true,
slopeMin: 0.3,
slopeMax: 0.7
}
}
]
});
`$3
Control how the material is processed:
`typescript
{
materialInput: myNodeMaterial,
materialTransform: {
extractTextures: true, // Extract textures for triplanar/bombing
overrideScale: true, // Apply layer scale to material
respectMaterialSettings: true // Keep material's original settings
},
// Features still work with material inputs!
edgeWear: { enable: true, intensity: 1.5 },
triplanar: { enable: true }
}
`$3
The system processes layer inputs in this order:
1.
materialInput - MeshPhysicalNodeMaterial (highest priority)
2. map - Texture maps (color, normal, roughness, etc.)
3. color - Solid color fallback (lowest priority)$3
Any
MeshPhysicalNodeMaterial can be used, including those with:
- Custom shader nodes (colorNode, normalNode, etc.)
- Standard texture maps
- Subsurface scattering, transmission, clearcoat`typescript
// Example: Subsurface material as a layer
const skinMaterial = new MeshPhysicalNodeMaterial({
color: 0xffccaa,
subsurfaceColor: 0xff6666,
subsurface: 0.5
});const characterMaterial = new LayeredMaterial({
layers: [
{ materialInput: skinMaterial },
{
name: 'Makeup',
map: { color: makeupTexture },
mask: { map: makeupMaskTexture },
blendMode: { color: 'overlay' }
}
]
});
`๐ญ Advanced Features
$3
Realistic wear and tear on edges and corners:`typescript
edgeWear: {
enable: true,
intensity: 1.5, // How much wear to apply
threshold: 0.1, // Curvature threshold for wear
falloff: 0.3, // Smooth falloff range
sharpness: 2.0, // Non-linear shaping power
color: {r: 0.8, g: 0.7, b: 0.6}, // Exposed material color
affectsMaterial: true, // Also change roughness/metalness
roughness: 0.2, // Roughness at worn edges
metalness: 1.0, // Metalness at worn edges
wearPattern: 'curvature', // 'curvature'|'ambient_occlusion'|'world_space'|'combined'
curvatureMethod: 'normal', // 'normal'|'position'|'simplified'|'world'|'laplace'
useNoise: false, // Add noise variation to wear
}
`#### Wear Patterns
| Pattern | Description | Best For |
|---------|-------------|----------|
|
curvature | Detects edges via surface curvature | Metal edges, corners |
| ambient_occlusion | Uses surface orientation (upward-facing) | Sheltered areas |
| world_space | Combines wind direction + height + orientation | Environmental weathering |
| combined | All patterns weighted together | Most realistic results |#### Curvature Methods Performance
| Method | Performance | Quality | Description |
|--------|-------------|---------|-------------|
|
normal | โก Fast | โ
โ
โ
โ
| Normal derivative (default, best balance) |
| simplified | โกโก Fastest | โ
โ
โ
| Z-component only, very cheap |
| world | โก Fast | โ
โ
โ
โ
| World normal derivative |
| position | โก Medium | โ
โ
โ
โ
โ
| Second derivative of position |
| laplace | ๐ข Slow | โ
โ
โ
โ
โ
| Most accurate, computationally expensive |Recommendations:
- Use
normal (default) for most cases
- Use simplified for mobile/low-end GPUs
- Use laplace only when maximum accuracy is needed$3
Eliminate tiling artifacts with stochastic sampling:`typescript
textureBombing: {
enable: true,
blend: 0.6, // Blend factor between samples (0-1)
}
`$3
Seamless projection on complex geometry:`typescript
triplanar: {
enable: true,
useWorldPosition: true, // Use world or object space
}
`$3
#### Slope-Based Masking (Terrain)
`typescript
mask: {
useSlope: true,
slopeMin: 0.3, // Minimum slope angle (0=flat, 1=vertical)
slopeMax: 0.7, // Maximum slope angle
}
`#### Height-Based Masking
`typescript
mask: {
useHeight: true,
heightMin: 0.0, // Minimum world height
heightMax: 10.0, // Maximum world height
}
`#### Procedural Noise Masking
`typescript
mask: {
useNoise: true,
noiseType: 'perlin', // 'perlin'|'voronoi'|'fbm'
noiseScale: 2.0,
noiseOctaves: 4,
noiseThreshold: 0.5, // Cutoff for binary masks
}
`$3
Control how layers interact:`typescript
blendMode: {
color: 'overlay', // 'normal'|'multiply'|'overlay'|'screen'|'add'|etc.
normal: 'rnb', // 'rnb'|'linear'|'whiteout'|'udn'|'partial_derivative'
roughness: 'max', // 'normal'|'min'|'max'|'multiply'|'average'
metalness: 'min', // 'normal'|'min'|'max'|'multiply'|'average'
ao: 'multiply', // 'normal'|'min'|'max'|'multiply'
}
`$3
Use height maps for realistic transitions:`typescript
heightBlend: {
enable: true,
strength: 2.0, // How much height affects blending
sharpness: 4.0, // Sharpness of the blend edge
}
`$3
Create depth effects without geometry displacement:`typescript
parallax: {
enable: true,
method: 'pom', // 'simple' | 'steep' | 'pom' (best quality)
scale: 0.1, // Parallax depth strength
steps: 16, // Ray marching steps (higher = better quality)
quality: 'high', // 'low' | 'medium' | 'high'
}
`Methods:
-
simple - Fast single offset, good for subtle effects
- steep - Fixed-step ray marching, good balance
- pom - Full Parallax Occlusion Mapping with interpolation (best quality)> Note: Requires geometry with tangent attributes. Use
geometry.computeTangents() if you see warnings.๐ช Real-World Examples
$3
`typescript
const terrainMaterial = new LayeredMaterial({
layers: [
{
name: 'Grass Base',
map: { color: grassColor, normal: grassNormal, roughness: grassRoughness },
triplanar: { enable: true },
textureBombing: { enable: true, blend: 0.6 },
scale: 0.5,
},
{
name: 'Cliff Rock',
map: { color: rockColor, normal: rockNormal, height: rockHeight },
mask: {
useSlope: true,
slopeMin: 0.4,
slopeMax: 0.8,
useNoise: true,
noiseType: 'voronoi',
},
heightBlend: { enable: true, strength: 3.0 },
blendMode: { normal: 'rnb' },
},
{
name: 'Sand Path',
map: { color: sandColor, normal: sandNormal },
mask: {
map: pathMask,
channel: 'r',
useNoise: true,
},
edgeWear: {
enable: true,
intensity: 1.2,
wearPattern: 'world_space',
},
}
]
});
`$3
`typescript
const metalMaterial = new LayeredMaterial({
layers: [
{
name: 'Base Metal',
map: { color: metalColor, normal: metalNormal, roughness: metalRoughness },
roughness: 0.1,
metalness: 0.9,
},
{
name: 'Paint Layer',
map: { color: paintColor, normal: paintNormal, roughness: paintRoughness },
mask: { map: paintMask, channel: 'r' },
edgeWear: {
enable: true,
intensity: 2.0,
color: {r: 0.9, g: 0.8, b: 0.7}, // Worn metal color
affectsMaterial: true,
roughness: 0.3,
metalness: 0.95,
wearPattern: 'combined',
},
blendMode: {
color: 'normal',
normal: 'whiteout', // Sharp edges for paint chips
},
}
]
});
`๐ง Runtime API
$3
`typescript
// Add new layer at the top
material.addLayer({
name: 'New Dirt Layer',
map: { color: dirtColor, normal: dirtNormal },
// ... config
});// Insert layer at specific position (NEW!)
material.insertLayer(1, {
name: 'Middle Layer',
map: { color: middleTexture },
});
// Move layer from one position to another (NEW!)
material.moveLayer(0, 2); // Move layer 0 to position 2
// Swap two layers (NEW!)
material.swapLayers(0, 1); // Swap layer 0 and 1
// Get layer count (NEW!)
const count = material.getLayerCount();
// Get layer by index (NEW!)
const layer = material.getLayer(0);
// Update existing layer
material.updateLayer(1, {
scale: 2.0,
roughness: 0.8,
colorTint: { r: 1.0, g: 0.9, b: 0.8 }, // Warm tint
});
// Remove layer
material.removeLayer(0);
`$3
All changes automatically trigger shader recompilation:
`typescript
// Enable/disable features at runtime
material.updateLayer(0, {
edgeWear: { enable: true, intensity: 1.5 },
textureBombing: { enable: false },
});// Change blend modes dynamically
material.updateLayer(1, {
blendMode: { color: 'multiply', normal: 'linear' }
});
// Apply color tint (NEW!)
material.updateLayer(0, {
colorTint: { r: 0.8, g: 0.8, b: 1.0 } // Cool blue tint
});
`$3
For real-time transitions without shader rebuilds, use
UniformDynamicMaterial:`typescript
import { UniformDynamicMaterial } from '@interverse/three-layered-material';// Create material with uniform-based properties
const material = new UniformDynamicMaterial({
layers: [
{
name: 'Ground',
map: { color: groundTexture },
scale: 1.0,
roughness: 0.8
}
]
});
// Define target state
const wetLayers = [
{
name: 'Wet Ground',
map: { color: wetTexture },
scale: 0.8,
roughness: 0.3 // Wet surfaces are smoother
}
];
// Animate transition at 60fps WITHOUT shader rebuilds!
function animate(deltaTime) {
const progress = / your animation logic /;
// This only updates uniform values - no shader recompilation
material.setTransitionFast(wetLayers, progress);
}
// Complete transition when done
material.completeTransition();
`Performance Comparison:
| Method | Shader Rebuild | Frame Impact |
|--------|----------------|--------------|
|
DynamicLayeredMaterial.setTransition() | โ Every frame | 50-200ms stutter |
| UniformDynamicMaterial.setTransitionFast() | โ
None | <1ms (GPU uniform update) |> Use
UniformDynamicMaterial for: Day/night cycles, weather transitions, real-time sliders, any smooth animation.๐ฏ Use Cases
$3
- Terrain systems - Grass, rock, sand, snow layers with slope-based masking
- Weathered props - Paint wear, rust, dirt accumulation
- Architectural materials - Plaster, brick, concrete with edge wear
- Vehicle materials - Paint, dirt, scratches, exposed metal
- Character materials - Skin, fabric, armor with layered details$3
- โ
Tiling artifacts - Texture bombing breaks up repetition
- โ
Seamless complex geometry - Triplanar mapping
- โ
Realistic material transitions - Height-based blending
- โ
Procedural wear and tear - Curvature-based edge detection
- โ
Runtime material authoring - No shader compilation needed๐ง Mental Model
Think of the pipeline as:
`
Layer 1 โ Sample textures โ Apply edge wear โ Output LayerData
Layer 2 โ Sample textures โ Apply edge wear โ Output LayerData
...Blender:
Base = Layer 1
For each additional layer:
Mask = Calculate mask (texture + slope + height + noise)
Blend = Height-based blend between base and layer using mask
Base = Result
Return final blended material
`๐ฎ What's Next?
The system is ready for:
- GUI Inspector (dat.GUI or Leva integration)
- Debug Visualizer - View masks, curvature, wear patterns in real-time
- Material Presets - Common configurations (metal, terrain, fabric)
- Export System - Bake final materials to textures
---
๐ก Pro Tips
1. Start Simple - Begin with 2-3 layers and add complexity gradually
2. Use ARM Maps - Pack AO/Roughness/Metalness for better performance
3. Height Maps Matter - Essential for realistic blending between layers
4. Noise is Your Friend - Add organic variation to break up patterns
5. Test Different Blend Modes - Each material interaction benefits from different blending approaches
This system gives you the power of offline material authoring tools with the flexibility of real-time procedural generation. No shader expertise required - just creative layer configuration!
---
๐๏ธ Terrain Integration
Use layered materials with terrain systems that require vertex displacement:
$3
Compatible with
@interverse/three-terrain-lod:`typescript
import { LayeredTerrainMaterialProvider } from '@interverse/three-layered-material';
import { TerrainLOD } from '@interverse/three-terrain-lod';// Create layered terrain material
const terrainMaterial = new LayeredTerrainMaterialProvider({
layers: [
{
name: 'Grass',
map: {
color: grassTexture,
normal: grassNormal,
height: grassHeight // Used for height blending
},
scale: 0.5
},
{
name: 'Rock',
map: {
color: rockTexture,
normal: rockNormal,
height: rockHeight
},
mask: {
useSlope: true,
slopeMin: 0.4,
slopeMax: 0.8
},
heightBlend: { enable: true, strength: 2.0 }
},
{
name: 'Snow',
map: { color: snowTexture },
mask: {
useHeight: true,
heightMin: 50,
heightMax: 100
}
}
],
// Terrain-specific options
useLayerHeightBlending: true, // Blend layer heights into displacement
layerHeightInfluence: 0.3, // How much layer heights affect terrain
displacementScale: 1.0 // Global displacement multiplier
});
// Apply to terrain
const terrain = new TerrainLOD({
heightMapUrl: '/terrain/heightmap.png',
worldSize: 1000,
maxHeight: 100
});
terrain.setMaterialProvider(terrainMaterial);
`$3
- Slope-based layer masking - Grass on flat, rock on slopes
- Height-based layer masking - Snow above certain altitude
- Layer height blending - Mix layer heights into displacement for detail
- Full layered material features - Edge wear, texture bombing, parallax, etc.---
---
MaterialVariant Class Documentation
Overview
The
MaterialVariant class extends LayeredMaterial to create material variations that automatically inherit and sync with a base material while applying custom overrides. Think of it as "smart material inheritance" - when the base material changes, all variants automatically update while maintaining their unique customizations.Key Features
- ๐ Automatic Synchronization: Variants update when base material changes
- ๐จ Layer Overrides: Customize specific properties while inheriting others
- โก Performance Optimized: Two implementation strategies available
- ๐ง Runtime Flexibility: Update overrides dynamically
- ๐ฏ Non-Destructive: Base material remains unchanged
Installation & Import
`typescript
import {
LayeredMaterial,
MaterialVariant,
ObservableLayeredMaterial,
ObservableMaterialVariant
} from '@interverse/three-layered-material';
`Basic Usage
$3
`typescript
// Create base material
const baseMaterial = new LayeredMaterial({
layers: [
{
name: 'Ground',
map: { color: groundTexture, normal: groundNormal },
scale: 1.0,
roughness: 0.8,
metalness: 0.0
},
{
name: 'Grass',
map: { color: grassTexture, normal: grassNormal },
scale: 0.5,
roughness: 0.9,
metalness: 0.0
}
]
});// Create variant with overrides
const dryGroundVariant = new MaterialVariant(baseMaterial, [
{
// Override ground layer
scale: 2.0, // Double the scale
roughness: 0.95, // Make drier/more rough
metalness: 0.1 // Slight metallic sheen
},
{
// Override grass layer
scale: 0.3, // Smaller grass pattern
roughness: 0.7 // Less rough grass
}
]);
// Apply to mesh
const terrainMesh = new THREE.Mesh(geometry, dryGroundVariant);
`$3
`typescript
// Use ObservableLayeredMaterial for better performance
const observableBase = new ObservableLayeredMaterial({
layers: [/ your layers /]
});const observableVariant = new ObservableMaterialVariant(observableBase, [
{ scale: 2.0, roughness: 0.9 },
{ metalness: 0.2 }
]);
`API Reference
$3
`typescript
new MaterialVariant(baseMaterial: LayeredMaterial, overrides: Partial[])
`Parameters:
-
baseMaterial: The source LayeredMaterial to inherit from
- overrides: Array of partial layer configurations (one per base layer)Example:
`typescript
const variant = new MaterialVariant(baseMaterial, [
{ scale: 2.0 }, // Override layer 0 scale
{ roughness: 0.5 }, // Override layer 1 roughness
{}, // No override for layer 2 (if exists)
{ metalness: 0.8 } // Override layer 3 metalness
]);
`$3
####
setOverrides(overrides: PartialUpdate all overrides at once.
`typescript
// Change all overrides
variant.setOverrides([
{ scale: 3.0, roughness: 0.3 },
{ metalness: 0.5, scale: 0.8 }
]);
`####
updateOverride(layerIndex: number, override: PartialUpdate a specific layer's override.
`typescript
// Update only the ground layer
variant.updateOverride(0, {
scale: 1.5,
roughness: 0.6
});// Add override to a new layer
variant.updateOverride(2, {
metalness: 0.3
});
`####
getOverrides(): PartialGet current overrides array.
`typescript
const currentOverrides = variant.getOverrides();
console.log(currentOverrides[0].scale); // 2.0
`####
getBaseMaterial(): LayeredMaterialGet the base material instance.
`typescript
const base = variant.getBaseMaterial();
base.addLayer(newLayerConfig); // Variant will auto-update
`####
clone(): MaterialVariantCreate a deep clone of the variant.
`typescript
const variantCopy = variant.clone();
`####
dispose(): voidClean up resources and listeners.
`typescript
variant.dispose();
`Advanced Usage Patterns
$3
`typescript
// Define material presets
const MaterialPresets = {
DRY: [
{ scale: 2.0, roughness: 0.95 },
{ scale: 0.3, roughness: 0.7 }
],
WET: [
{ scale: 0.8, roughness: 0.3 },
{ scale: 0.6, roughness: 0.2 }
],
SNOWY: [
{ scale: 1.2, roughness: 0.4 },
{ scale: 0.4, roughness: 0.3 }
]
};// Create variant and apply preset
const weatherVariant = new MaterialVariant(baseMaterial, MaterialPresets.DRY);
// Change weather dynamically
function setWeather(weatherType: 'DRY' | 'WET' | 'SNOWY') {
weatherVariant.setOverrides(MaterialPresets[weatherType]);
}
`$3
`typescript
class WearableMaterialVariant extends MaterialVariant {
private wearLevel: number = 0;
setWear(level: number) {
this.wearLevel = level;
// Increase roughness and reduce scale with wear
this.setOverrides([
{
scale: 1.0 + level * 0.5, // Pattern stretches with wear
roughness: 0.8 + level * 0.2, // Gets rougher
metalness: level * 0.5 // Metal shows through
},
{
scale: 0.5 - level * 0.2, // Grass thins out
roughness: 0.9 + level * 0.1
}
]);
}
}
`$3
`typescript
class SeasonalMaterialVariant extends MaterialVariant {
private seasonProgress: number = 0; // 0 = spring, 0.25 = summer, etc.
updateSeason(progress: number) {
this.seasonProgress = progress;
// Spring to Summer transition
const springToSummer = this.interpolateOverrides(
[
{ scale: 1.0, roughness: 0.8 }, // Spring
{ scale: 0.5, roughness: 0.6 } // Summer grass
],
[
{ scale: 0.8, roughness: 0.7 }, // Summer
{ scale: 0.3, roughness: 0.9 } // Dry summer grass
],
progress * 4 // Scale to spring-summer range
);
this.setOverrides(springToSummer);
}
private interpolateOverrides(
start: Partial[],
end: Partial[],
factor: number
): Partial[] {
// Implementation for smooth interpolation between override sets
return start.map((startOverride, i) => ({
...this.lerpOverride(startOverride, end[i] || {}, factor)
}));
}
}
`$3
`typescript
class MaterialVariantManager {
private variants = new Map();
private baseMaterial: LayeredMaterial;
constructor(baseMaterial: LayeredMaterial) {
this.baseMaterial = baseMaterial;
}
createVariant(name: string, overrides: Partial[]): MaterialVariant {
const variant = new MaterialVariant(this.baseMaterial, overrides);
this.variants.set(name, variant);
return variant;
}
getVariant(name: string): MaterialVariant | undefined {
return this.variants.get(name);
}
updateAllVariants(): void {
// Force update all variants (useful after base material changes)
this.variants.forEach(variant => {
variant.setOverrides(variant.getOverrides());
});
}
}// Usage
const manager = new MaterialVariantManager(baseMaterial);
manager.createVariant('dry', [{ scale: 2.0, roughness: 0.9 }]);
manager.createVariant('wet', [{ scale: 0.8, roughness: 0.3 }]);
const dryVariant = manager.getVariant('dry');
`Real-World Examples
$3
`typescript
// Base terrain material
const baseTerrain = new LayeredMaterial({
layers: [
{ name: 'Soil', map: { color: soilTexture }, scale: 1.0 },
{ name: 'Grass', map: { color: grassTexture }, scale: 0.5 },
{ name: 'Rock', map: { color: rockTexture }, scale: 0.3 }
]
});// Biome variants
const forestVariant = new MaterialVariant(baseTerrain, [
{ scale: 1.2 }, // Rich soil
{ scale: 0.8 }, // Dense grass
{ scale: 0.1 } // Few rocks
]);
const desertVariant = new MaterialVariant(baseTerrain, [
{ scale: 0.6 }, // Sandy soil
{ scale: 0.1 }, // Sparse grass
{ scale: 0.8 } // Many rocks
]);
const arcticVariant = new MaterialVariant(baseTerrain, [
{ scale: 0.3 }, // Frozen ground
{ scale: 0.0 }, // No grass
{ scale: 0.5 } // Ice-covered rocks
]);
`$3
`typescript
// Base building material
const newBuilding = new LayeredMaterial({
layers: [
{ name: 'Paint', map: { color: freshPaint }, roughness: 0.3 },
{ name: 'Concrete', map: { color: cleanConcrete }, roughness: 0.5 }
]
});// Age variants
const aged5Years = new MaterialVariant(newBuilding, [
{ roughness: 0.5 }, // Paint faded
{ roughness: 0.6 } // Concrete weathered
]);
const aged20Years = new MaterialVariant(newBuilding, [
{ roughness: 0.8 }, // Paint heavily worn
{ roughness: 0.9 } // Concrete eroded
]);
`$3
`typescript
// Base character material
const baseCharacter = new LayeredMaterial({
layers: [
{ name: 'Skin', map: { color: baseSkin }, roughness: 0.4 },
{ name: 'Clothing', map: { color: baseClothing }, roughness: 0.6 }
]
});// Character state variants
const healthyVariant = new MaterialVariant(baseCharacter, [
{ / Default skin / },
{ / Default clothing / }
]);
const injuredVariant = new MaterialVariant(baseCharacter, [
{
map: { color: paleSkinTexture }, // Pale when injured
roughness: 0.6 // Sweaty skin
},
{
map: { color: bloodStainedTexture }, // Blood on clothing
roughness: 0.8 // Rough from damage
}
]);
const poweredUpVariant = new MaterialVariant(baseCharacter, [
{
map: { color: glowingSkinTexture }, // Glowing effect
roughness: 0.2 // Smooth, energized
}
]);
`Performance Considerations
$3
`typescript
// โ
Good - More efficient
const observableBase = new ObservableLayeredMaterial({ layers: [] });
const observableVariant = new ObservableMaterialVariant(observableBase, overrides);// โ ๏ธ Acceptable - Less efficient but works with any LayeredMaterial
const regularVariant = new MaterialVariant(anyBaseMaterial, overrides);
`$3
`typescript
// โ
Good - Single update
variant.setOverrides(newOverrides);// โ Avoid - Multiple updates
variant.updateOverride(0, { scale: 2.0 });
variant.updateOverride(1, { roughness: 0.5 });
variant.updateOverride(2, { metalness: 0.3 });
`$3
`typescript
// For high-performance scenarios, consider:
class MaterialVariantPool {
private pool: MaterialVariant[] = [];
getVariant(base: LayeredMaterial, overrides: Partial[]): MaterialVariant {
let variant = this.pool.find(v =>
v.getBaseMaterial() === base &&
JSON.stringify(v.getOverrides()) === JSON.stringify(overrides)
);
if (!variant) {
variant = new MaterialVariant(base, overrides);
this.pool.push(variant);
}
return variant;
}
}
`Common Pitfalls
$3
`typescript
// โ Don't create variants of variants
const variant1 = new MaterialVariant(base, overrides1);
const variant2 = new MaterialVariant(variant1, overrides2); // Avoid// โ
Create all variants from the same base
const variant1 = new MaterialVariant(base, overrides1);
const variant2 = new MaterialVariant(base, overrides2);
`$3
`typescript
// Always dispose variants when no longer needed
const variant = new MaterialVariant(base, overrides);// When done with variant:
variant.dispose();
`$3
`typescript
// โ
Clear what gets overridden
const overrides = [
{ scale: 2.0, roughness: 0.8 }, // Override scale and roughness
{ metalness: 0.5 } // Override only metalness
];// โ Unclear overrides
const confusingOverrides = [
{ scale: 2.0 },
{ / Empty - but is this intentional? / }
];
`Migration Tips
$3
Before:
`typescript
// Manual copying (error-prone)
const variantLayers = JSON.parse(JSON.stringify(baseMaterial.layers));
variantLayers[0].scale = 2.0;
const variant = new LayeredMaterial({ layers: variantLayers });// Need to manually update when base changes? ๐ฅ
`After:
`typescript
// Automatic synchronization
const variant = new MaterialVariant(baseMaterial, [
{ scale: 2.0 }
]);
// Automatically updates when base changes! ๐
`The
MaterialVariant system provides a robust way to create material variations that stay in sync with their base, reducing bugs and making your material system more maintainable and dynamic!DynamicLayeredMaterial Class Documentation
Overview
The
DynamicLayeredMaterial enables real-time material transitions between different material states. It allows you to smoothly blend from a source material configuration to a target configuration, perfect for dynamic environments, weather effects, time-of-day changes, and material state transitions.Key Features
- ๐ Smooth Transitions: Interpolate between material states with full control
- โฑ๏ธ Temporal Control: Control transition speed and timing
- ๐จ Property Interpolation: Different interpolation strategies per property type
- ๐ State Management: Track transition progress and state
- ๐ Runtime Flexibility: Start, cancel, or modify transitions at any time
Basic Usage
$3
`typescript
import { DynamicLayeredMaterial } from '@interverse/three-layered-material';// Create dynamic material with initial state
const dynamicMaterial = new DynamicLayeredMaterial({
layers: [
{
name: 'Ground',
map: { color: dryGroundTexture, normal: groundNormal },
scale: 1.0,
roughness: 0.8,
metalness: 0.0
}
]
});
`$3
`typescript
// Define target state
const wetGroundConfig = [
{
name: 'Wet Ground',
map: { color: wetGroundTexture, normal: wetGroundNormal },
scale: 0.8,
roughness: 0.3, // Wet surfaces are smoother
metalness: 0.1 // Water reflection effect
}
];// Start transition over 3 seconds
dynamicMaterial.setTransition(wetGroundConfig, 0.5); // 50% progress
// Complete transition
setTimeout(() => {
dynamicMaterial.setTransition(wetGroundConfig, 1.0); // 100% complete
}, 3000);
`API Reference
$3
`typescript
new DynamicLayeredMaterial(options: LayeredMaterialOptions)
`Parameters:
-
options: Standard LayeredMaterial options for initial stateExample:
`typescript
const material = new DynamicLayeredMaterial({
layers: [
{ name: 'Base', map: { color: baseColor }, roughness: 0.5 }
],
blendSharpness: 8.0
});
`$3
####
setTransition(targetLayers: LayerConfig[], factor: number): voidStart or update a transition to target layers.
Parameters:
-
targetLayers: Array of layer configurations for the target state
- factor: Transition progress (0 = source, 1 = target)Example:
`typescript
// Gradual transition over time
let progress = 0;
function updateTransition() {
progress += 0.01;
material.setTransition(targetLayers, progress);
if (progress < 1) requestAnimationFrame(updateTransition);
}
updateTransition();
`####
completeTransition(): voidImmediately complete the current transition.
`typescript
// Snap to target state
material.completeTransition();
`####
cancelTransition(): voidCancel current transition and revert to source state.
`typescript
// Abort transition
material.cancelTransition();
`####
getTransitionProgress(): numberGet current transition progress (0-1).
`typescript
const progress = material.getTransitionProgress();
console.log(Transition ${(progress * 100).toFixed(1)}% complete);
`####
isTransitioning(): booleanCheck if material is currently transitioning.
`typescript
if (material.isTransitioning()) {
console.log('Material is currently changing');
}
`Advanced Usage
$3
`typescript
class WeatherMaterialSystem {
private material: DynamicLayeredMaterial;
private currentWeather: string = 'clear';
constructor() {
this.material = new DynamicLayeredMaterial({
layers: [
{
name: 'Terrain',
map: { color: clearTerrainTexture, normal: terrainNormal },
scale: 1.0,
roughness: 0.7
},
{
name: 'Vegetation',
map: { color: clearVegetationTexture },
scale: 0.5,
roughness: 0.9
}
]
});
}
setWeather(weatherType: 'clear' | 'rainy' | 'snowy', duration: number = 5) {
const targetConfig = this.getWeatherConfig(weatherType);
this.currentWeather = weatherType;
// Animate transition
this.animateTransition(targetConfig, duration);
}
private getWeatherConfig(weatherType: string): LayerConfig[] {
const configs = {
clear: [
{ scale: 1.0, roughness: 0.7, metalness: 0.0 },
{ scale: 0.5, roughness: 0.9 }
],
rainy: [
{
scale: 0.8,
roughness: 0.3, // Wet surfaces are smoother
metalness: 0.2 // Water reflections
},
{
scale: 0.6,
roughness: 0.4 // Wet vegetation
}
],
snowy: [
{
scale: 1.2,
roughness: 0.2, // Smooth snow
metalness: 0.0
},
{
scale: 0.1, // Less visible vegetation
roughness: 0.3
}
]
};
return configs[weatherType];
}
private animateTransition(targetConfig: LayerConfig[], duration: number) {
const startTime = Date.now();
const endTime = startTime + duration * 1000;
const update = () => {
const now = Date.now();
const progress = Math.min((now - startTime) / (duration * 1000), 1);
this.material.setTransition(targetConfig, progress);
if (progress < 1) {
requestAnimationFrame(update);
} else {
this.material.completeTransition();
}
};
update();
}
}// Usage
const weatherSystem = new WeatherMaterialSystem();
weatherSystem.setWeather('rainy', 3); // Transition to rainy over 3 seconds
`$3
`typescript
class TimeOfDayMaterial extends DynamicLayeredMaterial {
private time: number = 0; // 0-1, where 0=midnight, 0.5=noon
constructor() {
super({
layers: [
{
name: 'Day Material',
map: { color: dayTexture, normal: dayNormal },
roughness: 0.6,
metalness: 0.0
}
]
});
}
setTimeOfDay(time: number) {
this.time = time;
// Calculate day/night blend factor
const dayFactor = Math.sin(time * Math.PI); // 0 at night, 1 at day
const nightConfig = [
{
map: { color: nightTexture, normal: nightNormal },
roughness: 0.8, // Rougher at night (dew, moisture)
metalness: 0.1 // Slight metallic for moonlight
}
];
this.setTransition(nightConfig, 1 - dayFactor);
}
update(deltaTime: number) {
// Advance time (1 unit = 24 hours)
this.time += deltaTime / (24 60 60);
this.time %= 1;
this.setTimeOfDay(this.time);
}
}// Usage in game loop
const timeMaterial = new TimeOfDayMaterial();
function gameLoop(deltaTime: number) {
timeMaterial.update(deltaTime);
}
`$3
`typescript
class DamageableMaterial extends DynamicLayeredMaterial {
private health: number = 1.0;
constructor() {
super({
layers: [
{
name: 'Healthy Material',
map: { color: healthyTexture, normal: healthyNormal },
roughness: 0.3,
metalness: 0.0,
edgeWear: {
enable: true,
intensity: 0.5,
threshold: 0.1
}
}
]
});
}
takeDamage(damage: number) {
this.health = Math.max(0, this.health - damage);
this.updateMaterialState();
}
repair(amount: number) {
this.health = Math.min(1, this.health + amount);
this.updateMaterialState();
}
private updateMaterialState() {
const damagedConfig = [
{
roughness: 0.3 + (1 - this.health) * 0.6, // 0.3 to 0.9
metalness: (1 - this.health) * 0.8, // 0.0 to 0.8
edgeWear: {
enable: true,
intensity: 0.5 + (1 - this.health) * 2.0, // 0.5 to 2.5
threshold: 0.1 - (1 - this.health) * 0.08, // 0.1 to 0.02
color: {
r: 0.7 + (1 - this.health) * 0.3,
g: 0.6,
b: 0.5 - (1 - this.health) * 0.3
}
}
}
];
this.setTransition(damagedConfig, 1 - this.health);
}
}// Usage
const armorMaterial = new DamageableMaterial();
armorMaterial.takeDamage(0.3); // 30% damaged
armorMaterial.repair(0.1); // Repair 10%
`$3
`typescript
class SeasonalMaterial extends DynamicLayeredMaterial {
private season: number = 0; // 0=spring, 1=summer, 2=autumn, 3=winter
constructor() {
super({
layers: [
{
name: 'Spring Ground',
map: { color: springGroundTexture },
scale: 1.0,
roughness: 0.6
},
{
name: 'Spring Vegetation',
map: { color: springVegetationTexture },
scale: 0.5,
roughness: 0.8
}
]
});
}
setSeason(season: number, transitionDuration: number = 10) {
const targetConfig = this.getSeasonConfig(season);
// Animate seasonal transition
this.animateSeasonTransition(targetConfig, transitionDuration);
this.season = season;
}
private getSeasonConfig(season: number): LayerConfig[] {
const seasons = {
0: [ // Spring
{ scale: 1.0, roughness: 0.6 },
{ scale: 0.5, roughness: 0.8 }
],
1: [ // Summer
{ scale: 0.8, roughness: 0.7 },
{ scale: 0.7, roughness: 0.9 }
],
2: [ // Autumn
{ scale: 1.2, roughness: 0.8 },
{
scale: 0.4,
roughness: 0.7,
map: { color: autumnVegetationTexture } // Different texture
}
],
3: [ // Winter
{ scale: 1.5, roughness: 0.3 },
{ scale: 0.1, roughness: 0.4 }
]
};
return seasons[season];
}
private animateSeasonTransition(targetConfig: LayerConfig[], duration: number) {
let progress = 0;
const startTime = Date.now();
const animate = () => {
const elapsed = (Date.now() - startTime) / 1000;
progress = Math.min(elapsed / duration, 1);
this.setTransition(targetConfig, progress);
if (progress < 1) {
requestAnimationFrame(animate);
} else {
this.completeTransition();
}
};
animate();
}
}// Usage
const seasonalMaterial = new SeasonalMaterial();
seasonalMaterial.setSeason(2, 5); // Transition to autumn over 5 seconds
`$3
`typescript
class InteractiveMaterial extends DynamicLayeredMaterial {
private states: Map = new Map();
private currentState: string = 'default';
constructor() {
super({ layers: [] });
this.initializeStates();
}
private initializeStates() {
// Define different material states
this.states.set('default', [
{ name: 'Default', map: { color: defaultTexture }, roughness: 0.5 }
]);
this.states.set('highlighted', [
{
name: 'Highlighted',
map: { color: highlightedTexture },
roughness: 0.3,
metalness: 0.2
}
]);
this.states.set('selected', [
{
name: 'Selected',
map: { color: selectedTexture },
roughness: 0.2,
metalness: 0.4
}
]);
this.states.set('disabled', [
{
name: 'Disabled',
map: { color: disabledTexture },
roughness: 0.8,
metalness: 0.0
}
]);
}
setState(state: string, instant: boolean = false) {
const targetConfig = this.states.get(state);
if (!targetConfig) return;
this.currentState = state;
if (instant) {
this.completeTransition();
this.setTransition(targetConfig, 1.0);
} else {
// Smooth transition
this.animateStateTransition(targetConfig);
}
}
private animateStateTransition(targetConfig: LayerConfig[]) {
let progress = 0;
const animate = () => {
progress += 0.05;
this.setTransition(targetConfig, progress);
if (progress < 1) {
requestAnimationFrame(animate);
} else {
this.completeTransition();
}
};
animate();
}
getCurrentState(): string {
return this.currentState;
}
}// Usage
const interactiveMaterial = new InteractiveMaterial();
interactiveMaterial.setState('highlighted'); // Smooth highlight
interactiveMaterial.setState('selected', true); // Instant selection
`Performance Considerations
$3
`typescript
// โ
Good - Batch transitions
material.setTransition(targetConfig, progress);// โ Avoid - Rapid small updates
for (let i = 0; i <= 100; i++) {
material.setTransition(targetConfig, i / 100);
}
// โ
Better - Throttled updates
let lastUpdate = 0;
function updateTransition(progress: number) {
const now = Date.now();
if (now - lastUpdate > 16) { // ~60fps
material.setTransition(targetConfig, progress);
lastUpdate = now;
}
}
`$3
`typescript
class OptimizedDynamicMaterial extends DynamicLayeredMaterial {
private textureCache: Map = new Map();
preloadTextures(configs: LayerConfig[][]) {
configs.forEach(config => {
config.forEach(layer => {
if (layer.map?.color) {
this.cacheTexture(layer.map.color);
}
// Cache other textures...
});
});
}
private cacheTexture(texture: THREE.Texture) {
const key = texture.uuid;
if (!this.textureCache.has(key)) {
this.textureCache.set(key, texture);
}
}
}// Preload all transition states
const material = new OptimizedDynamicMaterial({ layers: initialConfig });
material.preloadTextures([sunnyConfig, rainyConfig, snowyConfig]);
`$3
`typescript
// Clean up when done
material.dispose();// Monitor transition states
const transitionCount = / count active transitions /;
if (transitionCount > 10) {
console.warn('Too many simultaneous transitions');
}
`Common Patterns
$3
`typescript
class CyclicMaterial extends DynamicLayeredMaterial {
private cycleConfigs: LayerConfig[][];
private currentCycle: number = 0;
startCycling(interval: number = 2000) {
setInterval(() => {
this.currentCycle = (this.currentCycle + 1) % this.cycleConfigs.length;
this.setTransition(this.cycleConfigs[this.currentCycle], 1);
}, interval);
}
}
`$3
`typescript
class TriggerMaterial extends DynamicLayeredMaterial {
private triggers: Map = new Map();
addTrigger(name: string, config: LayerConfig[], duration: number = 1) {
this.triggers.set(name, { config, duration });
}
trigger(name: string) {
const trigger = this.triggers.get(name);
if (trigger) {
this.animateTransition(trigger.config, trigger.duration);
}
}
}// Usage
material.addTrigger('hit', hitConfig, 0.5);
material.addTrigger('powerup', powerupConfig, 2.0);
// Trigger effects
material.trigger('hit'); // Quick hit effect
material.trigger('powerup'); // Longer powerup effect
`$3
`typescript
class LayeredTransitionMaterial extends DynamicLayeredMaterial {
private activeTransitions: Map = new Map();
addLayerTransition(layerId: string, targetConfig: LayerConfig[], progress: number) {
this.activeTransitions.set(layerId, progress);
this.updateCombinedTransition();
}
private updateCombinedTransition() {
// Combine multiple layer transitions
const combinedConfig = / merge based on activeTransitions /;
this.setTransition(combinedConfig, 1.0);
}
}
`The
DynamicLayeredMaterial transforms static materials into living, breathing surfaces that can respond to game events, environmental changes, and player interactions in real-time!๐ฏ Purpose & Behavior
$3
- Purpose: Surface applications that sit on top of the material
- Behavior: Like stickers, paint, logos, or temporary markings
- Relationship: Additive - they add new visual elements
- Examples:
- Company logos on vehicles
- Graffiti on walls
- Temporary paint markings
- Blood splatters (in games)
- Mud splashes$3
- Purpose: Surface alterations that reveal underlying materials
- Behavior: Like wear, erosion, or material removal
- Relationship: Subtractive/Revealing - they expose what's underneath
- Examples:
- Scratches showing metal under paint
- Worn edges revealing wood under varnish
- Corrosion eating through surfaces
- Cracks exposing interior materials๐ง Technical Differences
$3
`typescript
// Decals typically BLEND or OVERWRITE
blendMode: {
color: 'normal', // Complete color replacement
normal: 'normal', // Overwrite normals
roughness: 'normal', // Direct replacement
}
// Result: Decal appears ON TOP of existing material
`$3
`typescript
// Damages typically REVEAL or MODIFY
blendMode: {
color: 'multiply', // Darken or reveal underlying
normal: 'rnb', // Blend normals to show depth
roughness: 'max', // Make areas rougher
}
// Result: Damage shows WHAT'S UNDERNEATH existing material
`๐จ Visual Characteristics
$3
- Opacity: Usually opaque or semi-transparent
- Edges: Sharp or soft, but clearly defined boundaries
- Depth: Sit on the surface plane
- Interaction: Don't affect underlying material properties much$3
- Opacity: Often reveal completely different materials
- Edges: Organic, irregular, based on wear patterns
- Depth: Show actual material depth (scratches, dents)
- Interaction: Significantly alter material properties (roughness, metalness)โฑ๏ธ Temporal Behavior
$3
`typescript
// Often temporary or removable
{
lifetime: 30, // Fades after 30 seconds
fadeStartTime: Date.now(),
priority: 1 // Can be layered
}
`$3
`typescript
// Often permanent or slowly healing
{
permanent: false,
healRate: 0.02, // Very slow natural healing
intensity: 0.8 // Current damage level
}
`๐ช Real-World Examples
$3
`typescript
// Adding a racing stripe to a car
material.addDecal({
position: new THREE.Vector3(0, 1.2, 0),
size: new THREE.Vector3(2, 0.1, 0.1),
layer: {
map: { color: racingStripeTexture },
// Doesn't change the car's material properties
}
});// Adding a temporary mud splatter
material.addDecal({
position: new THREE.Vector3(0, 0.5, -1),
lifetime: 60, // Washes off after 60 seconds
layer: {
map: { color: mudTexture, roughness: mudRoughness },
// Mud sits on top of existing paint
}
});
`$3
`typescript
// Adding wear on door edges
material.addDamage({
type: 'scratch',
position: new THREE.Vector3(0.8, 0.5, 0),
layer: {
map: {
color: exposedMetalColor, // Shows metal under paint
roughness: scratchedRoughness, // Rougher than paint
metalness: 0.8 // Metal is more metallic
},
// Actually changes the material properties
}
});// Adding corrosion damage
material.addDamage({
type: 'corrosion',
position: new THREE.Vector3(-0.5, 0.3, 0),
layer: {
map: {
color: rustColor, // Rust replaces paint
normal: corrodedNormal, // Pitted surface
roughness: 0.9 // Very rough corroded surface
},
// Permanently alters the material
}
});
`๐ Material Interaction
$3
`
Base Material โ Decal Applied
[Paint] + [Logo] = [Paint with Logo on top]
Properties mostly unchanged
`$3
`
Base Material โ Damage Applied
[Paint over Metal] + [Scratch] = [Paint with Metal showing through]
Properties significantly changed in damaged areas
`๐ฎ Game Context Examples
$3
- Blood splatters after combat
- Bullet impact marks on walls
- Footprints in snow/mud
- Temporary spray paint
- Projected light patterns$3
- Armor wear showing underlying metal
- Weapon scratches from usage
- Vehicle denting from collisions
- Building erosion from weather
- Character scarring from injuries๐ก When to Use Which
$3
- Adding temporary visual elements
- Applying surface markings that don't alter material
- Need quick runtime application/removal
- Want layered visual effects (multiple decals)$3
- Showing material wear and aging
- Revealing underlying material layers
- Creating permanent surface alterations
- Simulating physical erosion or destruction๐ง Hybrid Approach
Sometimes you might use both together:
`typescript
// A bullet hole might be both:
// - A decal (the impact mark on the surface)
// - A damage (the exposed material underneath)// Impact decal (surface marking)
material.addDecal({
position: bulletHitPos,
layer: { map: { color: impactSmoke } },
lifetime: 5
});
// Structural damage (material alteration)
material.addDamage({
type: 'bullet',
position: bulletHitPos,
layer: {
map: {
color: exposedMetalColor,
roughness: 0.9,
metalness: 0.8
}
},
permanent: true
});
``| Aspect | Decals | Damages |
|--------|---------|----------|
| Purpose | Add surface elements | Reveal underlying materials |
| Blending | Normal/Overlay | Multiply/Reveal |
| Duration | Often temporary | Often permanent |
| Effect | Visual addition | Material alteration |
| Examples | Logos, paint, blood | Scratches, corrosion, wear |
TL;DR: Decals add to the surface, damages reveal what's underneath!