Skip to content

@carrot/engine-materials ​

ts

Schema-driven material system - typed property bags that reference shaders by key, with per-instance overrides and dirty tracking.

Installation ​

bash
npm install @carrot/engine-materials

Architecture / How It Works ​

Material = schema + property values + shader key ​

A Material is an abstract base class. Subclasses declare their properties in the constructor using define() with typed MaterialPropertyDef<T> definitions. Each material references a shader by string key (e.g. core.sprite.default) - the renderer resolves this to a compiled GPU program at draw time.

The schema approach means:

  • Properties are fully typed - getProperty(def) returns T | undefined without casts.
  • Editors and serializers can introspect the schema to build UI automatically.
  • Property definitions carry metadata: label, type, default value, min/max/step, enum options.

MaterialInstance = per-object copy ​

MaterialInstance wraps a Material and provides per-object property overrides. Unmodified properties fall through to the source material. The renderer checks isDirty to know when uniforms need re-uploading.

This is the classic shared-material pattern: one Material definition, many MaterialInstances with individual tweaks (different tint colours, different textures, etc.).

Built-in material library ​

Three ready-to-use materials ship in library/:

  • Material2DSprite - textured + tint, shader core.sprite.default. Defaults transparent = true in constructor (sprites are inherently alpha-blended)
  • Material2DUnlitColor - solid colour, shader core.unlit.color
  • Material2DUnlitTextured - textured + colour, shader core.unlit.textured

MaterialType ​

Categorises materials for render pipeline sorting: Unlit, Simple, Pbr. The renderer can batch by type for optimal draw order.

Dependencies ​

PackageUsed For
@carrot/colorsColorRgbLike type for colour properties
@carrot/engine-texturesTexture type for texture properties

Build ​

bash
npm run build   # runs tsc

Usage Guide ​

Schema-driven material system with typed properties, per-instance overrides, and dirty tracking.

Import ​

ts
import {
  Material, MaterialInstance, MaterialSchema,
  MaterialPropertyDef, defineProperty,
  MaterialPropertyType, MaterialType,
  Material2DSprite, Material2DUnlitColor, Material2DUnlitTextured,
} from '@carrot/engine-materials';

Common Patterns ​

1. Using a built-in material ​

ts
const spriteMat = new Material2DSprite({
  texture: myTexture,
  tint: { r: 1, g: 0.5, b: 0, a: 1 },
});

2. Flat colour material ​

ts
const redMat = new Material2DUnlitColor({ r: 1, g: 0, b: 0, a: 1 });

3. Per-instance overrides ​

ts
const baseMat = new Material2DSprite({ texture: atlasTexture });

// Each enemy gets its own tint
const enemy1 = new MaterialInstance(baseMat);
enemy1.setProperty(baseMat.tintColor, { r: 1, g: 0, b: 0, a: 1 });

const enemy2 = new MaterialInstance(baseMat);
enemy2.setProperty(baseMat.tintColor, { r: 0, g: 0, b: 1, a: 1 });

4. Checking dirty state (renderer) ​

ts
if (instance.isDirty) {
  uploadUniforms(instance);
  instance.clearDirty();
}

5. Defining a custom material ​

ts
class TerrainMaterial extends Material {
  readonly type = MaterialType.Simple;

  readonly heightMap = this.define(
    defineProperty<Texture>('u_HeightMap', 'Height Map', MaterialPropertyType.Texture),
  );

  readonly tileScale = this.define(
    defineProperty<number>('u_TileScale', 'Tile Scale', MaterialPropertyType.Number, {
      defaultValue: 1,
      min: 0.1,
      max: 100,
      step: 0.1,
    }),
  );

  readonly blendColor = this.define(
    defineProperty<ColorRgbLike>('u_BlendColor', 'Blend Color', MaterialPropertyType.Color, {
      defaultValue: { r: 1, g: 1, b: 1, a: 1 },
    }),
  );

  constructor(shaderKey = 'game.terrain.default') {
    super('Terrain', shaderKey);
  }
}

6. Reading properties by key (serialization, editors) ​

ts
const value = material.getPropertyByKey('u_Color');
material.setPropertyByKey('u_Color', { r: 0, g: 1, b: 0, a: 1 });

// Iterate all property definitions
for (const def of material.schema) {
  console.log(def.key, def.label, def.propertyType);
}

7. Instance with shader override ​

ts
const instance = new MaterialInstance(baseMat, 'custom.sprite.outlined');
// Uses a different shader program while keeping the same property schema

8. Resetting overrides ​

ts
instance.clearProperty(baseMat.tintColor); // revert to base material value
instance.resetOverrides();                 // revert all overrides

9. Transparency ​

ts
const glassMat = new Material2DUnlitColor({ r: 1, g: 1, b: 1, a: 0.5 });
glassMat.transparent = true;
// Renderer sorts transparent materials to draw back-to-front

// Note: Material2DSprite defaults to transparent = true on construction,
// since sprites are inherently alpha-blended. No need to set it manually.

Carrot