docs
Getting Started
Tutorials
How-To Guides
Explanation
Reference
Help
Getting Started
Tutorials
How-To Guides
Explanation
Reference
Help

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:

  1. Lightmaps - Apply unique lightmap textures to objects (Sample)
  2. Reflection Probes - Per-object environment maps (Sample)
  3. 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 uniforms
  • Color - Base color, emissive color, etc.
  • Texture - Albedo maps, normal maps, lightmaps, etc.
  • Vector2, Vector3, Vector4 - UV offsets, custom shader parameters
  • Euler - Rotation values (e.g., envMapRotation)
  • boolean - Flags like transparent
  • null - 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 color

Common 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., color not Color)
  • 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
Suggest changes
Last Updated: 2/24/26, 9:09 PM

Extras

ChatGPT Ask ChatGPT Claude Ask Claude
Copy Markdown

Navigation

  • Getting Started
  • Tutorials
  • How-To Guides
  • Explanation
  • Reference
  • Help

Extras

ChatGPT Ask ChatGPT Claude Ask Claude
Copy Markdown