Skip to main content

Rendering: Material Showroom

A slowly spinning sphere wearing a single Standard PBR material. Tap anywhere to cycle through four PBR presets — Matte Plaster, Glossy Red Plastic, Brushed Steel, and Polished Gold — each defined as a triple of _AlbedoColor, _MRAOMetallic, and _MRAORoughness. The script never creates a new Material; it mutates the same shared instance with Material.setColor(...) and Material.setFloat(...), and the renderer picks up the change on the next frame.

Material Showroom demo running in Effect House preview

What you'll build

  • A Subject sphere at the origin with localScale = (0.45, 0.45, 0.45), reading at a comfortable size against the live camera-feed framing.
  • A dedicated Standard PBR Material resource referenced by the Subject's MeshRenderer.materials[0]. Initial state: light grey albedo, metallic 0, roughness 0.5.
  • A KeyLight (DirectionalLight) at (8, 6, 8) aimed at the subject and a RimLight (SpotLight) at (0, 4, -6) reversed. The auto-created default Directional Light is hidden so the demo's custom lights are the only contributors.
  • A TitleText and StatusText 2D HUD that mirrors the active preset.
  • A GameController empty SceneObject hosting MaterialShowroom — the script that subscribes to global EventType.Touch, cycles the presets, and slowly Y-spins the sphere so the lighting catches different parts of the surface.

Open the demo

↓ material-showroom.zip

Unzip and open in Effect House (5.9.0+). The opening scene contains:

  • Camera — default 3D perspective camera at (0, 0, 14) looking at the world origin.
  • 2D Camera — auto-created by the first 2D Text.
  • Subject — the sphere mesh whose material we mutate at runtime.
  • Standard PBR — the material resource referenced by Subject's MeshRenderer. The script holds a direct @serializeProperty material reference to this resource so it can call setColor / setFloat without an intermediate component lookup.
  • Directional Light (auto-created) — disabled in the inspector.
  • Environment Light — kept on for the global ambient floor.
  • KeyLight / RimLight — the two custom lights that make metallic + roughness changes legible.
  • TitleText — "Material Showroom" at the top.
  • StatusText<n> of 4 plus the preset name near the bottom.
  • GameController — empty SceneObject hosting MaterialShowroom.

Read the script

MaterialShowroom.ts

@component()
export class MaterialShowroom extends APJS.BasicScriptComponent {
// The Material instance on the Subject's MeshRenderer. Wired directly so
// the script can mutate Material properties (setColor / setFloat) without
// an intermediate getComponent lookup. The same instance is shared across
// every renderer that references it — keep one material per showroom.
@serializeProperty material!: APJS.Material;

// The Subject SceneObject we rotate so the user sees light catching
// different parts of the surface (metallic and roughness changes don't
// read on a static sphere).
@serializeProperty subject!: APJS.SceneObject;

// The status label that shows the current preset name.
@serializeProperty statusText!: APJS.SceneObject;

// Spin rate, degrees per second.
@serializeProperty rotationDegPerSec: number = 30;

private statusComp!: APJS.Text;
private subjectTransform!: APJS.Transform;
private touchCallback!: (e: APJS.IEvent) => void;
private cycleIndex: number = -1;
private inited: boolean = false;

// Each preset is a triple of PBR settings demonstrating a different point
// in metallic / roughness / albedo space. Presets are chosen so that the
// metallic + roughness change is visually obvious side-by-side:
// - 0: Matte plaster — non-metal, very rough.
// - 1: Glossy red plastic — non-metal, very smooth.
// - 2: Brushed steel — full metal, mid roughness.
// - 3: Polished gold — full metal, very smooth, warm tint.
private static readonly PRESETS: { name: string; albedo: APJS.Color; metallic: number; roughness: number }[] = [
{ name: "Matte Plaster", albedo: new APJS.Color(0.95, 0.95, 0.93, 1), metallic: 0.00, roughness: 0.85 },
{ name: "Glossy Red Plastic", albedo: new APJS.Color(0.85, 0.10, 0.15, 1), metallic: 0.00, roughness: 0.18 },
{ name: "Brushed Steel", albedo: new APJS.Color(0.70, 0.72, 0.78, 1), metallic: 0.95, roughness: 0.45 },
{ name: "Polished Gold", albedo: new APJS.Color(1.00, 0.78, 0.34, 1), metallic: 1.00, roughness: 0.12 },
];

onUpdate(dt: number): void {
if (!this.inited) {
if (!this.material || !this.subject || !this.statusText) return;

this.statusComp = this.statusText.getComponent("Text") as APJS.Text;
this.subjectTransform = this.subject.getComponent("Transform") as APJS.Transform;

this.touchCallback = (event: APJS.IEvent) => {
const t = event.args[0] as APJS.TouchData;
if (t.phase !== APJS.TouchPhase.Began) return;
this.advance();
};
APJS.EventManager.getGlobalEmitter()
.on(APJS.EventType.Touch, this.touchCallback, this);

// Apply the first preset so the preview reads correctly before any tap.
this.advance();
this.inited = true;
console.log("[MaterialShowroom] ready — " + MaterialShowroom.PRESETS.length + " presets wired");
return;
}

// Slow Y-axis spin so the lighting catches different angles.
const eul = this.subjectTransform.localEulerAngles;
const newY = (eul.y + this.rotationDegPerSec * dt) % 360;
this.subjectTransform.localEulerAngles = new APJS.Vector3f(eul.x, newY, eul.z);
}

onDestroy(): void {
if (this.touchCallback) {
APJS.EventManager.getGlobalEmitter()
.off(APJS.EventType.Touch, this.touchCallback, this);
}
}

private advance(): void {
this.cycleIndex = (this.cycleIndex + 1) % MaterialShowroom.PRESETS.length;
const p = MaterialShowroom.PRESETS[this.cycleIndex];

// Material.setColor / setFloat write directly to the shader uniforms.
// The same Material instance is shared across every MeshRenderer that
// references it, so this single assignment updates every visible copy.
this.material.setColor("_AlbedoColor", p.albedo);
this.material.setFloat("_MRAOMetallic", p.metallic);
this.material.setFloat("_MRAORoughness", p.roughness);

this.statusComp.text = (this.cycleIndex + 1) + " of " + MaterialShowroom.PRESETS.length + "\n" + p.name;
console.log("[MaterialShowroom] preset " + p.name + " — metallic=" + p.metallic + " roughness=" + p.roughness);
}
}

The Rendering-namespace calls of interest:

  • Material.setColor(name, color) — writes a color value into a named shader property. For Standard PBR, _AlbedoColor is the base surface color.
  • Material.setFloat(name, value) — writes a scalar value into a named shader property. For Standard PBR, _MRAOMetallic and _MRAORoughness are the two levers that change a "non-metal vs. metal" + "matte vs. glossy" axis.
  • Material.setVector(name, value) — writes a Vector2f / 3f / 4f into a named shader property. Not used here, but it's how you'd drive _Tiling or _Offset when the material has UVControl enabled.
  • Material.setTexture(name, texture) — swaps the texture plugged into a named slot (e.g., _AlbedoTexture). Combine with _EnableAlbedoTexture to actually sample it.
  • MeshRenderer is the component that ties a Mesh to one or more Material resources. The Subject's MeshRenderer is wired in the inspector — at runtime we never touch it; we mutate the shared Material directly and the renderer picks up the change on the next frame.
  • Standard PBR is the built-in material type. Created via add_builtin_resource(resource_type="Standard PBR") in the editor pipeline. Its full property table — including _AO, Emissive, _NormalTexture, _RimHighlightColor, RimHighlight, ThinFilm, and the per-pass render-state fields — is in the Material reference.

Preset table

IndexNameAlbedo (RGB)MetallicRoughness
0Matte Plaster(0.95, 0.95, 0.93)0.000.85
1Glossy Red Plastic(0.85, 0.10, 0.15)0.000.18
2Brushed Steel(0.70, 0.72, 0.78)0.950.45
3Polished Gold(1.00, 0.78, 0.34)1.000.12

Each row reads the same physical scene — only setColor and setFloat differ.

Customize

On GameControllerMaterialShowroom:

  • material — drop in any Material resource. The script doesn't hardcode the Standard PBR property names; if you swap to an Unlit material, edit the setColor / setFloat keys to match (e.g., _BaseColor instead of _AlbedoColor).
  • subject — the SceneObject whose Transform we spin. Doesn't have to be the same object as the renderer; useful when the subject is a parent group and the renderer lives on a child.
  • statusText — any 2D Text SceneObject. The script looks up its Text component at runtime.
  • rotationDegPerSec — set to 0 to disable the spin (good for side-by-side screenshot comparisons), or push to 120+ for a spinning hero turntable.
  • PRESETS — a static readonly array at the top of the script. Add entries to extend the cycle; the modulo-wrap kicks in automatically.

In the editor, on the Material resource:

  • _AO — the Standard PBR ambient-occlusion baseline (default 1). Drop to ~0.6 for a softer, dustier look.
  • Emissive + _EmissiveColor + _EmissiveIntensity — turn on the macro and set color + intensity for a self-lit material. Useful if you extend PRESETS with a "neon" entry.
  • _NormalTexture + _NormalStrength — plug in a normal map for surface detail (brushed-steel scratches, gold engraving).
  • RimHighlight + _RimHighlightColor — adds a Fresnel-style edge lift; pairs well with the brushed-steel preset for a cinematic hero look.

Suggestions for further play:

  • Cycle textures instead of colors. Wire a @serializeProperty textures: APJS.Texture[] and call material.setTexture("_AlbedoTexture", textures[i]) on each tap. The Texture reference covers how textures are loaded and addressed.
  • Add a per-instance MaterialPropertyBlock so two spheres share the same Material asset but display different colors. Assign via renderer.properties = block. The MaterialPropertyBlock reference is the canonical pattern for batched per-instance overrides.
  • Combine with the Lighting tutorial — wire the lighting cycle and the material cycle to two different gestures (long-press vs. tap) so the user can compare how a single material reads under multiple lighting setups.

What you learned

This tutorial used:

  • Material.setColor(name, color), .setFloat(name, value) for runtime shader-uniform mutation. Same instance shared across every renderer that references it.
  • MeshRenderer.materials — wired in the inspector; never touched at runtime in this demo.
  • Standard PBR properties_AlbedoColor, _MRAOMetallic, and _MRAORoughness as the three-axis space we cycle through.
  • @serializeProperty Material — yes, Material is a valid serializable resource type. Wire the asset directly in the inspector rather than looking it up via the renderer at runtime.
  • SceneObject.getComponent("Transform") in lazy-init for the subject's Transform handle, then per-frame localEulerAngles assignment for the slow Y-spin.

Read the full Material reference, the MaterialPropertyBlock reference, the MeshRenderer reference, the Mesh reference, the Texture reference, and the Rendering namespace overview.

For the touch-event subscription pattern used here, see the Events & Input tutorial. For adding accent lighting that makes metallic / roughness changes legible, see the Lighting tutorial.

Copyright © 2026 TikTok. All rights reserved.
About TikTokHelp CenterCareersContactLegalTerms of ServicePrivacy PolicyCookies