All examples

Roughness and metalness

Models3D
  • Mouse
  • Touch
  • Gamepad

See how roughness and metalness change a material. Metalness increases from left to right, and roughness from bottom to top. Adjust each range or rotate the environment to compare the reflections.

Off airLivePaused

Roughness and metalness

Models · 3D

A six-by-six grid of spheres against a dark blue-grey background: mirror-like spheres reflecting two soft studio panels along the bottom row, evenly diffuse ones along the top, pale plastic on the left and dark metal on the right.
Open standalone

WebGPU: checking…

Try this

  1. Set both metalness ends to one: every sphere becomes a metal, and the base colour now tints the reflection instead of the surface.
  2. Set Top row to 0.3 to compare only the smoother end of the roughness range.
  3. Rotate the environment and watch the reflections slide across every sphere at once.
Show source code
main.ts
import { Camera, createMaterialAsset, Environment, MeshAsset, MeshRenderer, pbrMaterialDefinition } from "ignifx";import { bootExample } from "../_kit/boot.ts";import { bindColor, srgbFromHex } from "../_kit/color.ts";import { twoPlaces } from "../_kit/format.ts";import { attachOrbit, FREE_MIN_PITCH } from "../_kit/orbit.ts";import { bind, color, readout, slider } from "../_kit/panel.ts";import { createLightRig, loadEnvironment } from "../_kit/stage.ts";import type { PanelSliderControl } from "../_kit/panel.ts";import type { AssetHandle, ColorLike, MaterialAsset } from "ignifx";/** * Thirty-six spheres, one per pair of numbers: metalness grows from left to right and roughness * from bottom to top. Every sphere shares one mesh and differs only in two material factors. * * A metal has no diffuse colour, so its base colour tints what it reflects; a non-metal's base * colour is its diffuse colour, with a colourless reflection on top. The reflections come from the * studio environment, and the sliders write material uniforms, so dragging one changes all * thirty-six spheres on the next frame with no shader rebuild. *//** Which end of which axis a slider writes. */type RangeEnd = "metalHigh" | "metalLow" | "roughHigh" | "roughLow";/** How many columns of metalness, and rows of roughness. Thirty-six materials, one mesh. */const GRID = 6;/** Each sphere's diameter, in metres. */const SPHERE_DIAMETER = 0.5;/** Segments per sphere. Enough that a mirror surface shows no facet at the size the grid is seen. */const SPHERE_SEGMENTS = 24;/** Centre-to-centre spacing, in metres: a tenth of a diameter of air between neighbours. */const SPACING = 0.55;/** The base colour every material starts with: a neutral light grey, so metal and plastic differ only by their two numbers. */const START_COLOR: ColorLike = srgbFromHex("#c8cad2");/** * The background: a dark slate that the scene's exposure and ACES curve present close to the site's * `--surface`, `#14181F`. Not black, because a smooth metal shows only its surroundings and the * right-hand column would lose its outline against black. */const CLEAR_COLOR: ColorLike = srgbFromHex("#292d37");/** The probe's exposure: low enough that the rough plastics keep their ramp, high enough to light the metals. */const EXPOSURE = 1.25;/** * Interpolates one axis of the grid. * * @param low - The value in the first row or column. * @param high - The value in the last one. * @param index - Which row or column, from `0` to `GRID - 1`. * @returns The value for that row or column. */function ramp(low: number, high: number, index: number): number {  return low + ((high - low) * index) / (GRID - 1);}bootExample({  title: "Roughness and metalness",  settings: {    // No `features.shadows`: the grid has no floor to cast onto.    rendering: { clearColor: CLEAR_COLOR },  },  async setup({ app, panel }) {    const eye = app.world.createEntity("Main Camera");    eye.addComponent(Camera, { near: 0.05, far: 100, fov: 40 });    // Straight at the grid, the only view in which every square is comparable. There is no floor, so    // the camera may orbit below it too: a reflection that does not move with the eye is not one.    const orbit = attachOrbit(app, eye, {      yaw: 0,      pitch: 0,      minPitch: FREE_MIN_PITCH,      minDistance: 1.5,      maxDistance: 14,    });    orbit.frame({ center: { x: 0, y: 0, z: 0 }, radius: (SPACING * GRID) / 2 }, 1.1);    // Three lamps at low intensity, and no shadows. The probe does most of the lighting; the lamps    // put a recognisable highlight on the rough plastics, which the probe alone leaves flat.    createLightRig(app, {      shadows: false,      keyIntensity: 1.4,      fillIntensity: 0.25,      rimIntensity: 0.6,      focus: { x: 0, y: 0, z: 0 },    });    const environment = loadEnvironment(app, "studio");    await environment.promise;    // `studio.environment.json` loads with its skybox off, so the component says the same rather than    // being reported as `IGX-0711`. The background is the clear colour.    const sky = app.world      .createEntity("Environment")      .addComponent(Environment, { environment, skybox: { enabled: false, size: 20 } });    sky.imageProcessing.toneMapping = "aces";    sky.imageProcessing.exposure = EXPOSURE;    // One mesh for all thirty-six spheres: a `MeshAsset` is an asset, so assigning the same handle    // to every renderer shares the geometry rather than copying it.    const sphere = MeshAsset.sphere(app, { diameter: SPHERE_DIAMETER, segments: SPHERE_SEGMENTS });    /** Every material, in row-major order: `materials[row * GRID + column]`. */    const materials: AssetHandle<MaterialAsset>[] = [];    const offset = (SPACING * (GRID - 1)) / 2;    for (let row = 0; row < GRID; row += 1) {      for (let column = 0; column < GRID; column += 1) {        const material = createMaterialAsset(          app,          pbrMaterialDefinition({            name: `grid/${String(row)}-${String(column)}`,            baseColor: START_COLOR,            metallic: ramp(0, 1, column),            roughness: ramp(0, 1, row),          }),          [],        );        materials.push(material);        const entity = app.world.createEntity(`Sphere ${String(row)}-${String(column)}`);        entity.transform.localPosition.set(column * SPACING - offset, row * SPACING - offset, 0);        entity.addComponent(MeshRenderer, { mesh: sphere, materials: [material], castShadows: false });      }    }    /** The live ranges the four sliders write, and the colour the swatch writes. */    const ranges = { metalLow: 0, metalHigh: 1, roughLow: 0, roughHigh: 1 };    const look = { color: START_COLOR };    /**     * Rewrites every material's metalness and roughness from {@link ranges}: thirty-six uniform     * writes on an already-compiled pipeline, and no shader work at all.     */    const applyRanges = (): void => {      materials.forEach((material: AssetHandle<MaterialAsset>, index: number): void => {        const row = Math.floor(index / GRID);        const column = index % GRID;        material.value?.setMetallicRoughness(          ramp(ranges.metalLow, ranges.metalHigh, column),          ramp(ranges.roughLow, ranges.roughHigh, row),        );      });    };    /** Gives every material the swatch's colour, in sRGB. */    const applyColor = (colour: ColorLike): void => {      for (const material of materials) {        material.value?.setBaseColor(colour);      }    };    /** One end of one axis — the `key` of {@link ranges} — as a slider labelled `label`. */    const end = (label: string, key: RangeEnd): PanelSliderControl =>      slider(        label,        { min: 0, max: 1, step: 0.01, format: twoPlaces },        {          value: ranges[key],          change: (value: number): void => {            ranges[key] = value;            applyRanges();          },        },      );    panel({      title: "Roughness and metalness",      groups: [        {          label: "Metalness, left to right",          controls: [end("Left column", "metalLow"), end("Right column", "metalHigh")],        },        {          label: "Roughness, bottom to top",          controls: [end("Bottom row", "roughLow"), end("Top row", "roughHigh")],        },        {          label: "Surface",          controls: [            color("Base colour", bindColor(look, "color", applyColor)),            // Rotating the probe moves every reflection at once without touching a material. What            // separates a mirror from a satin is the roughness rows above, not the environment.            slider(              "Probe rotation",              { min: 0, max: 360, step: 1, format: (value: number): string => `${String(value)}°` },              bind(sky, "rotation"),            ),          ],        },        {          label: "Frame",          collapsed: true,          controls: [readout("Draw calls", (): string => String(app.renderer.drawCalls))],        },      ],    });  },});
Uses
pbrMaterialDefinitioncreateMaterialAssetMaterialAsset.setMetallicRoughnessMaterialAsset.setBaseColorMeshAsset.sphereEnvironment
Assets
Studio environment — CC-BY 4.0, Babylon.js contributors