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.

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 defaultDirectional Lightis hidden so the demo's custom lights are the only contributors. - A
TitleTextandStatusText2D HUD that mirrors the active preset. - A
GameControllerempty SceneObject hostingMaterialShowroom— the script that subscribes to globalEventType.Touch, cycles the presets, and slowly Y-spins the sphere so the lighting catches different parts of the surface.
Open the demo
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 materialreference to this resource so it can callsetColor/setFloatwithout 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 4plus 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,_AlbedoColoris the base surface color.Material.setFloat(name, value)— writes a scalar value into a named shader property. For Standard PBR,_MRAOMetallicand_MRAORoughnessare the two levers that change a "non-metal vs. metal" + "matte vs. glossy" axis.Material.setVector(name, value)— writes aVector2f / 3f / 4finto a named shader property. Not used here, but it's how you'd drive_Tilingor_Offsetwhen the material hasUVControlenabled.Material.setTexture(name, texture)— swaps the texture plugged into a named slot (e.g.,_AlbedoTexture). Combine with_EnableAlbedoTextureto actually sample it.MeshRendereris the component that ties aMeshto one or moreMaterialresources. 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 PBRis the built-in material type. Created viaadd_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
| Index | Name | Albedo (RGB) | Metallic | Roughness |
|---|---|---|---|---|
| 0 | Matte Plaster | (0.95, 0.95, 0.93) | 0.00 | 0.85 |
| 1 | Glossy Red Plastic | (0.85, 0.10, 0.15) | 0.00 | 0.18 |
| 2 | Brushed Steel | (0.70, 0.72, 0.78) | 0.95 | 0.45 |
| 3 | Polished Gold | (1.00, 0.78, 0.34) | 1.00 | 0.12 |
Each row reads the same physical scene — only setColor and
setFloat differ.
Customize
On GameController → MaterialShowroom:
material— drop in anyMaterialresource. The script doesn't hardcode the Standard PBR property names; if you swap to an Unlit material, edit thesetColor/setFloatkeys to match (e.g.,_BaseColorinstead 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 itsTextcomponent at runtime.rotationDegPerSec— set to0to disable the spin (good for side-by-side screenshot comparisons), or push to120+for a spinning hero turntable.PRESETS— astatic readonlyarray 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 extendPRESETSwith 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 callmaterial.setTexture("_AlbedoTexture", textures[i])on each tap. The Texture reference covers how textures are loaded and addressed. - Add a per-instance
MaterialPropertyBlockso two spheres share the same Material asset but display different colors. Assign viarenderer.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 PBRproperties —_AlbedoColor,_MRAOMetallic, and_MRAORoughnessas 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-framelocalEulerAnglesassignment 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.