@needle-tools/engine
Version:
Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.
346 lines (345 loc) • 13.3 kB
TypeScript
import { Color, Euler, Material, Object3D, Texture, Vector2, Vector3, Vector4 } from "three";
/**
* Valid types that can be used as material property overrides
*/
type MaterialPropertyType = number | number[] | Color | Texture | Vector2 | Vector3 | Vector4 | null | Euler;
/**
* Defines offset and repeat transformations for texture coordinates
*/
export interface TextureTransform {
/** UV offset applied to the texture */
offset?: Vector2;
/** UV repeat/scale applied to the texture */
repeat?: Vector2;
}
/**
* Represents a single material property override with optional texture transformation
* @template T The type of the property value
*/
export interface PropertyBlockOverride<T extends MaterialPropertyType = MaterialPropertyType> {
/** The name of the material property to override (e.g., "color", "map", "roughness") */
name: string;
/** The value to set for this property */
value: T;
/** Optional texture coordinate transformation (only used when value is a Texture) */
textureTransform?: TextureTransform;
}
/**
* Utility type that extracts only non-function property names from a type
* @template T The type to extract property names from
*/
type NonFunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];
/**
* MaterialPropertyBlock allows per-object material property overrides without creating new material instances.
* This is useful for rendering multiple objects with the same base material but different properties
* (e.g., different colors, textures, or shader parameters).
*
* ## How Property Blocks Work
*
* **Important**: Overrides are registered on the **Object3D**, not on the material.
* This means:
* - If you change the object's material, the overrides will still be applied to the new material
* - Multiple objects can share the same material but have different property overrides
* - If you don't want overrides applied after changing a material, you must remove them using {@link removeOveride}, {@link clearAllOverrides}, or {@link dispose}
*
* The property block system works by:
* - Temporarily applying overrides in onBeforeRender
* - Restoring original values in onAfterRender
* - Managing shader defines and program cache keys for correct shader compilation
* - Supporting texture coordinate transforms per object
*
* ## Common Use Cases
*
* - **Lightmaps**: Apply unique lightmap textures to individual objects sharing the same material
* - **Reflection Probes**: Apply different environment maps per object for localized reflections
* - **See-through effects**: Temporarily override transparency/transmission properties for X-ray effects
*
* ## Getting a MaterialPropertyBlock
*
* **Important**: Do not use the constructor directly. Instead, use the static {@link MaterialPropertyBlock.get} method:
*
* ```typescript
* const block = MaterialPropertyBlock.get(myMesh);
* ```
*
* This method will either return an existing property block or create a new one if it doesn't exist.
* It automatically:
* - Creates the property block instance
* - Registers it in the internal registry
* - Attaches the necessary render callbacks to the object
* - Handles Groups by applying overrides to all child meshes
*
* @example Basic usage
* ```typescript
* // Get or create a property block for an object
* const block = MaterialPropertyBlock.get(myMesh);
*
* // Override the color property
* block.setOverride("color", new Color(1, 0, 0));
*
* // Override a texture with custom UV transform (useful for lightmaps)
* block.setOverride("lightMap", myLightmapTexture, {
* offset: new Vector2(0.5, 0.5),
* repeat: new Vector2(2, 2)
* });
*
* // Set a shader define
* block.setDefine("USE_CUSTOM_FEATURE", 1);
* ```
*
* @example Material swapping behavior
* ```typescript
* const mesh = new Mesh(geometry, materialA);
* const block = MaterialPropertyBlock.get(mesh);
* block.setOverride("color", new Color(1, 0, 0));
*
* // The color override is red for materialA
*
* // Swap the material - overrides persist and apply to the new material!
* mesh.material = materialB;
* // The color override is now red for materialB too
*
* // If you don't want overrides on the new material, remove them:
* block.clearAllOverrides(); // Remove all overrides
* // or
* block.removeOveride("color"); // Remove specific override
* // or
* block.dispose(); // Remove the entire property block
* ```
*
* @example Lightmap usage
* ```typescript
* const block = MaterialPropertyBlock.get(mesh);
* block.setOverride("lightMap", lightmapTexture);
* block.setOverride("lightMapIntensity", 1.5);
* ```
*
* @example See-through effect
* ```typescript
* const block = MaterialPropertyBlock.get(mesh);
* block.setOverride("transparent", true);
* block.setOverride("opacity", 0.3);
* ```
*
* @template T The material type this property block is associated with
*/
export declare class MaterialPropertyBlock<T extends Material = Material> {
private _overrides;
private _defines;
private _object;
/** The object this property block is attached to */
get object(): Object3D | null;
/**
* Creates a new MaterialPropertyBlock
* @param object The object this property block is for (optional)
*/
protected constructor(object?: Object3D | null);
/**
* Gets or creates a MaterialPropertyBlock for the given object.
* This is the recommended way to obtain a property block instance.
*
* @template T The material type
* @param object The object to get/create a property block for
* @returns The MaterialPropertyBlock associated with this object
*
* @example
* ```typescript
* const block = MaterialPropertyBlock.get(myMesh);
* block.setOverride("roughness", 0.5);
* ```
*/
static get<T extends Material = Material>(object: Object3D): MaterialPropertyBlock<T>;
/**
* Checks if an object has any property overrides
* @param object The object to check
* @returns True if the object has a property block with overrides
*/
static hasOverrides(object: Object3D): boolean;
/**
* Disposes this property block and cleans up associated resources.
* After calling dispose, this property block should not be used.
*/
dispose(): void;
/**
* Sets or updates a material property override.
* The override will be applied to the material during rendering.
*
* @param name The name of the material property to override (e.g., "color", "map", "roughness")
* @param value The value to set
* @param textureTransform Optional UV transform (only used when value is a Texture)
*
* @example
* ```typescript
* // Override a simple property
* block.setOverride("roughness", 0.8);
*
* // Override a color
* block.setOverride("color", new Color(0xff0000));
*
* // Override a texture with UV transform
* block.setOverride("map", texture, {
* offset: new Vector2(0, 0),
* repeat: new Vector2(2, 2)
* });
* ```
*/
setOverride<K extends NonFunctionPropertyNames<T>>(name: K, value: T[K], textureTransform?: TextureTransform): void;
setOverride(name: string, value: MaterialPropertyType, textureTransform?: TextureTransform): void;
/**
* Gets the override for a specific property with type-safe value inference
* @param name The property name to get
* @returns The PropertyBlockOverride with correctly typed value if it exists, undefined otherwise
*
* @example
* ```typescript
* const block = MaterialPropertyBlock.get<MeshStandardMaterial>(mesh);
*
* // Value is inferred as number | undefined
* const roughness = block.getOverride("roughness")?.value;
*
* // Value is inferred as Color | undefined
* const color = block.getOverride("color")?.value;
*
* // Value is inferred as Texture | null | undefined
* const map = block.getOverride("map")?.value;
*
* // Explicitly specify the type for properties not on the base material type
* const transmission = block.getOverride<number>("transmission")?.value;
*
* // Or use a more specific material type
* const physicalBlock = block as MaterialPropertyBlock<MeshPhysicalMaterial>;
* const transmissionTyped = physicalBlock.getOverride("transmission")?.value; // number
* ```
*/
getOverride<K extends NonFunctionPropertyNames<T>>(name: K): PropertyBlockOverride<T[K] & MaterialPropertyType> | undefined;
getOverride<V extends MaterialPropertyType = MaterialPropertyType>(name: string): PropertyBlockOverride<V> | undefined;
/**
* Removes a specific property override.
* After removal, the material will use its original property value for this property.
*
* @param name The property name to remove the override for
*
* @example
* ```typescript
* const block = MaterialPropertyBlock.get(mesh);
*
* // Set some overrides
* block.setOverride("color", new Color(1, 0, 0));
* block.setOverride("roughness", 0.5);
* block.setOverride("lightMap", lightmapTexture);
*
* // Remove a specific override - the material will now use its original color
* block.removeOveride("color");
*
* // Other overrides (roughness, lightMap) remain active
* ```
*/
removeOveride<K extends NonFunctionPropertyNames<T>>(name: K | ({} & string)): void;
/**
* Removes all property overrides from this block.
* After calling this, the material will use its original values for all properties.
*
* **Note**: This does NOT remove shader defines. Use {@link clearDefine} or {@link dispose} for that.
*
* @example Remove all overrides but keep the property block
* ```typescript
* const block = MaterialPropertyBlock.get(mesh);
*
* // Set multiple overrides
* block.setOverride("color", new Color(1, 0, 0));
* block.setOverride("roughness", 0.5);
* block.setOverride("lightMap", lightmapTexture);
*
* // Later, remove all overrides at once
* block.clearAllOverrides();
*
* // The material now uses its original values
* // The property block still exists and can be reused with new overrides
* ```
*
* @example Temporarily disable all overrides
* ```typescript
* const block = MaterialPropertyBlock.get(mesh);
*
* // Save current overrides if you want to restore them later
* const savedOverrides = [...block.overrides];
*
* // Clear all overrides temporarily
* block.clearAllOverrides();
*
* // Do some rendering without overrides...
*
* // Restore overrides
* savedOverrides.forEach(override => {
* block.setOverride(override.name, override.value, override.textureTransform);
* });
* ```
*
* @see {@link removeOveride} - To remove a single override
* @see {@link dispose} - To completely remove the property block and clean up resources
*/
clearAllOverrides(): void;
/**
* Gets all property overrides as a readonly array
* @returns Array of all property overrides
*/
get overrides(): readonly PropertyBlockOverride[];
/**
* Checks if this property block has any overrides
* @returns True if there are any overrides set
*/
hasOverrides(): boolean;
/**
* Set a shader define that will be included in the program cache key.
* This allows different objects sharing the same material to have different shader programs.
*
* Defines affect shader compilation and are useful for enabling/disabling features per-object.
*
* @param name The define name (e.g., "USE_LIGHTMAP", "ENABLE_REFLECTIONS")
* @param value The define value (typically a boolean, number, or string)
*
* @example
* ```typescript
* // Enable a feature for this specific object
* block.setDefine("USE_CUSTOM_SHADER", true);
* block.setDefine("QUALITY_LEVEL", 2);
* ```
*/
setDefine(name: string, value: string | number | boolean): void;
/**
* Remove a shader define
* @param name The define name to remove
*/
clearDefine(name: string): void;
/**
* Get all defines set on this property block
* @returns A readonly record of all defines
*/
getDefines(): Readonly<Record<string, string | number | boolean>>;
/**
* Generates a cache key based on the current overrides and defines.
* This key is used internally to ensure correct shader program selection
* when objects share materials but have different property blocks.
*
* @returns A string representing the current state of this property block
* @internal
*/
getCacheKey(): string;
}
/**
* Checks if an object has a MaterialPropertyBlock attached to it.
*
* @param object The object to check
* @returns True if the object has a property block registered
*
* @example
* ```typescript
* if (objectHasPropertyBlock(myMesh)) {
* console.log("This mesh has property overrides");
* }
* ```
*/
export declare function objectHasPropertyBlock(object: Object3D): boolean;
export {};