MaterialPropertyBlocks
MaterialPropertyBlocks let you customize material properties on individual objects while sharing the same base material. This provides a clean API for per-object variations without manually cloning materials.
Available since 4.14.0
MaterialPropertyBlocks were introduced in Needle Engine 4.14.0.
What are MaterialPropertyBlocks?
In traditional Three.js, if you want multiple objects to have different colors but use the same material, you'd need to clone the material for each object. This is tedious, can lead to memory bloat with many material instances, and makes it harder to manage variants that share some settings - for example, objects that need individual lightmaps or custom reflection probes while sharing the same base material.
MaterialPropertyBlocks solve this by allowing per-object property overrides that are applied dynamically during rendering, without manually creating new material instances.
How they work:
- Override properties are temporarily applied in
onBeforeRender - Original values are restored in
onAfterRender - Shader defines are managed automatically for correct shader compilation
- Per-object texture transforms are supported
Benefits:
- ✅ Clean API for per-object material variations
- ✅ No manual material cloning required
- ✅ Automatic memory management via WeakMaps
- ✅ Works transparently with Groups and child meshes
- ✅ Supports any Three.js material property
Note on Batching
MaterialPropertyBlocks don't change Three.js's default rendering behavior - each mesh is still rendered as a separate draw call, just as it would be without property blocks. For GPU-level batching, you'd need to use InstancedMesh or BatchedMesh regardless of whether you use MaterialPropertyBlocks.
Built-in Features Using MaterialPropertyBlocks
Needle Engine uses MaterialPropertyBlocks under the hood for several features - so you get the benefits automatically without writing any code:
- Lightmaps - Apply unique lightmap textures to objects (Sample)
- Reflection Probes - Per-object environment maps (Sample)
- See-Through Component - Dynamic transparency effects (Sample)
Quick Start
Basic Usage
import { Behaviour, serializable, MaterialPropertyBlock } from "@needle-tools/engine";
import { Color, Object3D } from "three";
export class ColorVariation extends Behaviour {
@serializable(Object3D)
targetObject?: Object3D;
awake() {
if (!this.targetObject) return;
// Get or create a MaterialPropertyBlock for the object
const block = MaterialPropertyBlock.get(this.targetObject);
// Override the color property
block.setOverride("color", new Color(1, 0, 0)); // Red color
}
}Important: Always use MaterialPropertyBlock.get(object) to obtain a property block. The constructor is protected and should not be called directly.
Core API
Getting a MaterialPropertyBlock
// Get or create a property block for an object
const block = MaterialPropertyBlock.get(myMesh);
// Check if an object has overrides
if (MaterialPropertyBlock.hasOverrides(myMesh)) {
console.log("This object has property overrides");
}Setting Property Overrides
import { Vector2 } from "three";
const block = MaterialPropertyBlock.get(myMesh);
// Override a number property
block.setOverride("roughness", 0.8);
// Override a color
block.setOverride("color", new Color(0xff0000));
// Override a texture
block.setOverride("map", myTexture);
// Override a texture with UV transform
block.setOverride("lightMap", lightmapTexture, {
offset: new Vector2(0.5, 0.5),
repeat: new Vector2(2, 2)
});Supported property types:
number- Roughness, metalness, opacity, etc.number[]- Array uniformsColor- Base color, emissive color, etc.Texture- Albedo maps, normal maps, lightmaps, etc.Vector2,Vector3,Vector4- UV offsets, custom shader parametersEuler- Rotation values (e.g.,envMapRotation)boolean- Flags liketransparentnull- To clear a property
Essentially, any property that exists on a Three.js material can be overridden.
Getting Override Values
// Get a specific override
const roughness = block.getOverride("roughness")?.value;
// Get all overrides
const allOverrides = block.overrides;
for (const override of allOverrides) {
console.log(`${override.name} = ${override.value}`);
}Removing Overrides
// Remove a specific property override
block.removeOveride("roughness");
// Clear all overrides
block.clearAllOverrides();
// Dispose the entire property block
block.dispose();Advanced Features
Shader Defines
Set shader defines that affect shader compilation per object:
const block = MaterialPropertyBlock.get(myMesh);
// Enable a shader feature for this object only
block.setDefine("USE_LIGHTMAP", 1);
block.setDefine("QUALITY_LEVEL", 2);
// Remove a define
block.clearDefine("USE_LIGHTMAP");
// Get all defines
const defines = block.getDefines();Use cases:
- Enable/disable shader features per object
- Quality level variations
- Custom shader behavior per instance
Type-Safe Overrides
When using TypeScript with specific material types, you get full type safety:
import { MeshStandardMaterial, MeshPhysicalMaterial } from "three";
// Type-safe with MeshStandardMaterial
const block = MaterialPropertyBlock.get<MeshStandardMaterial>(myMesh);
// "roughness" is correctly typed as number
block.setOverride("roughness", 0.5);
// For MeshPhysicalMaterial properties
const physicalBlock = MaterialPropertyBlock.get<MeshPhysicalMaterial>(myMesh);
physicalBlock.setOverride("transmission", 0.8);
physicalBlock.setOverride("thickness", 2.0);Working with Groups
MaterialPropertyBlocks automatically handle Three.js Group objects:
// Setting overrides on a Group applies to all child meshes
const group = myObject; // a THREE.Group
const block = MaterialPropertyBlock.get(group);
block.setOverride("color", new Color(1, 0, 0));
// All meshes in the group will render with red colorCommon Use Cases
1. Lightmapping
Apply unique lightmap textures to objects sharing the same material:
export class LightmapApplier extends Behaviour {
@serializable(Texture)
lightmapTexture?: Texture;
@serializable()
lightmapIntensity: number = 1.0;
awake() {
const block = MaterialPropertyBlock.get(this.gameObject);
if (this.lightmapTexture) {
block.setOverride("lightMap", this.lightmapTexture);
block.setOverride("lightMapIntensity", this.lightmapIntensity);
}
}
}2. Reflection Probes
Apply different environment maps per object:
import { Euler } from "three";
export class ReflectionProbeApplier extends Behaviour {
@serializable(Texture)
envMap?: Texture;
@serializable()
envMapIntensity: number = 1.0;
@serializable()
envMapRotationY: number = 0; // Rotation in radians
awake() {
const block = MaterialPropertyBlock.get(this.gameObject);
if (this.envMap) {
block.setOverride("envMap", this.envMap);
block.setOverride("envMapIntensity", this.envMapIntensity);
// envMapRotation is an Euler in Three.js MeshStandardMaterial
block.setOverride("envMapRotation", new Euler(0, this.envMapRotationY, 0));
}
}
}3. See-Through Effect
Create X-ray or see-through effects by dynamically changing transparency:
export class SeeThrough extends Behaviour {
private block?: MaterialPropertyBlock;
private originalTransparent?: boolean;
awake() {
this.block = MaterialPropertyBlock.get(this.gameObject);
}
enableSeeThrough() {
if (!this.block) return;
// Make the object transparent
this.block.setOverride("transparent", true);
this.block.setOverride("opacity", 0.3);
}
disableSeeThrough() {
if (!this.block) return;
// Restore original state
this.block.removeOveride("transparent");
this.block.removeOveride("opacity");
}
}4. Color Variations
Create color variations across instanced objects:
export class RandomColorizer extends Behaviour {
awake() {
const block = MaterialPropertyBlock.get(this.gameObject);
// Random color for this instance
const randomColor = new Color(
Math.random(),
Math.random(),
Math.random()
);
block.setOverride("color", randomColor);
}
}5. Dynamic Material Properties
Animate or change material properties at runtime:
export class PulsingEmission extends Behaviour {
@serializable()
pulseSpeed: number = 1.0;
private block?: MaterialPropertyBlock;
private time: number = 0;
awake() {
this.block = MaterialPropertyBlock.get(this.gameObject);
}
update() {
if (!this.block) return;
this.time += this.context.time.deltaTime * this.pulseSpeed;
const intensity = Math.sin(this.time) * 0.5 + 0.5;
this.block.setOverride("emissiveIntensity", intensity);
}
}6. Texture Tiling and Offset
Apply different UV transforms per object:
export class TextureTransform extends Behaviour {
@serializable(Vector2)
offset: Vector2 = new Vector2(0, 0);
@serializable(Vector2)
tiling: Vector2 = new Vector2(1, 1);
awake() {
const renderer = this.gameObject.getComponent(Renderer);
if (!renderer?.sharedMaterial) return;
const block = MaterialPropertyBlock.get(this.gameObject);
const mainTexture = renderer.sharedMaterial.map;
if (mainTexture) {
block.setOverride("map", mainTexture, {
offset: this.offset,
repeat: this.tiling
});
}
}
}Transparent and Opaque Rendering
One unique feature of MaterialPropertyBlocks is the ability to render the same material as both transparent and opaque on different objects.
// Object A - render as opaque
const blockA = MaterialPropertyBlock.get(objectA);
blockA.setOverride("transparent", false);
blockA.setOverride("opacity", 1.0);
// Object B - render as transparent (same material!)
const blockB = MaterialPropertyBlock.get(objectB);
blockB.setOverride("transparent", true);
blockB.setOverride("opacity", 0.5);This is handled automatically by the engine's render list management system.
Performance Considerations
How it Works Under the Hood
MaterialPropertyBlocks modify material properties temporarily during rendering:
- Properties are applied in
onBeforeRender - Original values are restored in
onAfterRender - Uniforms are re-uploaded per object when values differ
This has the same rendering cost as cloning materials would. The key advantage is that MaterialPropertyBlocks provide a much cleaner API and make it easier to reason about your code.
Benefits
Developer Experience:
- ✅ No manual material cloning or management
- ✅ Clean API for per-object variations
- ✅ Easy to add/remove overrides dynamically
- ✅ Works transparently with the component system
Memory:
- ✅ No cloned Material objects in JavaScript heap
- ✅ Automatic cleanup via WeakMaps
- ✅ Single shader program shared across objects (when defines match)
Best Practices
✅ Do:
- Use for per-object variations (colors, textures, parameters)
- Use for dynamic property changes at runtime
- Use for lightmaps and environment maps
❌ Don't:
- Use if all objects should have the same value (just modify the shared material directly)
Troubleshooting
Properties not updating
Problem: Override values not visible in rendered output.
Solutions:
- Ensure you're calling
MaterialPropertyBlock.get()(not creating instances manually) - Verify the property name matches the material's property exactly (e.g.,
colornotColor) - Check that the object has a material assigned
- Confirm the object is actually rendering
Shader defines not working
Problem: Custom defines not affecting shader behavior.
Solutions:
- Ensure defines are set before the first render
- Check that define values are the correct type (usually numbers)
- Verify the shader actually uses the define you're setting
TypeScript API Reference
For complete API documentation including all methods, properties, and type definitions, see:
MaterialPropertyBlock API Documentation
Examples
Complete Example: Object Highlighter
import { Behaviour, serializable, MaterialPropertyBlock } from "@needle-tools/engine";
import { Color } from "three";
export class ObjectHighlighter extends Behaviour {
@serializable(Color)
highlightColor: Color = new Color(1, 1, 0); // Yellow
@serializable()
emissiveIntensity: number = 2.0;
private block?: MaterialPropertyBlock;
private isHighlighted: boolean = false;
awake() {
this.block = MaterialPropertyBlock.get(this.gameObject);
}
onPointerEnter() {
this.highlight(true);
}
onPointerExit() {
this.highlight(false);
}
private highlight(enable: boolean) {
if (!this.block) return;
if (enable) {
// Apply highlight
this.block.setOverride("emissive", this.highlightColor);
this.block.setOverride("emissiveIntensity", this.emissiveIntensity);
} else {
// Remove highlight
this.block.removeOveride("emissive");
this.block.removeOveride("emissiveIntensity");
}
this.isHighlighted = enable;
}
}Related Documentation
- Lightmapping in Blender - Baking lightmaps
- Unity Lightmapping - Unity lightmap workflow
- See-Through Component - X-ray effect component
- Custom Shaders - Creating custom materials
- Scripting Guide - Creating components
Need Help?
- Discord Community - Ask questions about MaterialPropertyBlocks
- Forum - Share your use cases and examples
- GitHub Issues - Report bugs or request features