---
title: "Materials and looks PBR surfaces color metalness and textures"
description: "A mesh needs a look: a color, a metal or plastic feel, glow, transparency, or a texture. Materials are authored through semantic PBR operations, not shader code. Twelve ai_safe PBR ops cover the whole PBR surface; the node-graph topology ops are ai_safe as well, with a small node catalog (see the shader ceiling section)."
engineVersion: v1.0.234
date: 2026-09-28
license: "(c) Gessa, proprietary. Cite with attribution to https://gessa.ai/docs/. Terms: https://gessa.ai/terms/."
canonical: https://gessa.ai/docs/knowledge/playbooks/materials-and-looks/
---

# Materials and looks PBR surfaces color metalness and textures

## When to use

A mesh needs a look: a color, a metal or plastic feel, glow, transparency, or a
texture. Materials are authored through semantic PBR operations, not shader code.
Twelve ai_safe PBR ops cover the whole PBR surface; the node-graph topology ops
are ai_safe as well, with a small node catalog (see the shader ceiling section).

## The twelve ai_safe ops

All go through `apply_material_semantic_patch` (`project.material.patch.apply`),
a batch of ops applied to a `material` asset:

1. `createBasicPbrSurface` creates or resets a graph-native OpenPBR surface. Run
   this first.
2. `setPbrBaseColor` sets the base color (`baseColor`, a `#rrggbb`/`#rrggbbaa`
   or normalized color).
3. `setPbrColor` sets a color-valued field: `field` one of `baseColor`,
   `emissiveColor`, `sheenColor`, `attenuationColor`; `value` a color.
4. `setPbrScalar` sets a scalar `field` with a numeric `value`. Fields:
   `metallic` (0..1), `roughness` (0..1), `alphaCutoff`, `emissiveIntensity`
   (0..100), `normalStrength`, `occlusionIntensity`, `clearcoat`,
   `clearcoatRoughness`, `sheen`, `sheenRoughness`, `transmission`,
   `transmissionRoughness`, `ior` (1..2.333), `attenuationDistance`,
   `anisotropy`, `anisotropyRotation`.
5. `setAlphaMode` sets `alphaMode` one of `opaque`, `masked`, `blend`.
6. `setDoubleSided` sets `doubleSided` (render both faces, for foliage cards).
7. `setTextureSlot` assigns a texture asset to a `slot`: `baseColor`, `normal`,
   `metallicRoughness`, `emissive`, `occlusion`, `lightMap`, `clearcoatNormal`,
   `sheenColor`, `transmission`; with optional `uvTiling`/`uvOffset`/
   `uvRotation`/`uvSet`.
8. `clearTextureSlot` removes a texture from a slot.
9. `setInstanceAtlas` / 10. `clearInstanceAtlas` declare (or clear) the
   per-instance texture-array atlas an instanced material MAY select a tile from.
11. `setPreviewGeometry` sets editor preview geometry only (`sphere`, `cube`,
   `plane`, `customMesh`).
12. `createWaterFoamMaterial` builds an appearance-only water material graph.

## The recipe

1. Create a material asset: `project_create_asset` (`project.asset.create`) with
   `assetKind: "material"`.
2. Apply the surface: `apply_material_semantic_patch`
   (`project.material.patch.apply`) with an op batch, for example a brushed
   metal:
   - `{ "op":"createBasicPbrSurface" }`
   - `{ "op":"setPbrBaseColor", "baseColor":"#b8bcc4" }`
   - `{ "op":"setPbrScalar", "field":"metallic", "value": 1 }`
   - `{ "op":"setPbrScalar", "field":"roughness", "value": 0.35 }`
   For a glowing lamp: `setPbrColor` `emissiveColor` plus `setPbrScalar`
   `emissiveIntensity`. For glass: `setAlphaMode` `blend` plus `setPbrScalar`
   `transmission` and `ior`.
3. Skin a mesh: set the mesh entity's `RenderableComponent.materialRef` to the
   material asset id (see model-authoring-parametric-csg). One material reused
   across many meshes is cheaper than many near-identical materials.
4. Finish the look with the environment: apply a renderer look with
   `world_apply_visual_profile` (`renderer.visual_profile.apply`) for tone
   mapping and sky, and light the scene per atmosphere-sky-and-lighting. A
   material only reads as intended under matching lighting and exposure.

## The shader ceiling (do not fight it)

Raw shader source is hard-rejected by design. Any parameter key matching
`shaderSource`, `wgsl`, `glsl`, `rawShader`, `fragmentShader`, or `vertexShader`
raises `raw_shader_source_forbidden`. The material NODE-GRAPH ops
(`addCatalogNode`, `connectSockets`, `setNodeParameter` and friends) ARE
ai_safe, the same alphabet the visual editor gets (`replaceGraph` stays
import-only, and `trustedOnly` shader-plugin nodes are rejected for every
source), but the node CATALOG is small: OpenPBR surface, output, constants,
texture sample, water depth, scalar math, color mix, procedural noise. Do not
attempt custom shaders. Express the look with the twelve PBR ops; if an effect
truly needs a procedural shader beyond that catalog, say so plainly rather than
emitting shader strings that will be rejected.

## Pitfalls

- Run `createBasicPbrSurface` before setting fields, or there is no surface to
  set.
- Colors are `#rrggbb`/`#rrggbbaa`; scalar values must sit inside their field
  range (for example `ior` is 1..2.333, `metallic` is 0..1).
- Transparency needs `setAlphaMode` `blend` (or `masked` with `alphaCutoff`);
  setting only `transmission` on an opaque material will not look transparent.
- `setInstanceAtlas` only DECLARES what an instance may vary; the per-instance
  tile selection does not travel through this patch.

## Verify

- `project_get_graph_snapshot` to confirm the material asset and the mesh's
  `materialRef` binding.
- `qa_capture_renderer_viewport` to confirm color, metalness, glow, or
  transparency read under the scene lighting.

## Toon shading on props (provisional)

Shading is a per-material choice: the material's `shadingModel` is one of
`pbr`, `toon` or `unlit`. It belongs to one material, never to the world, and
setting it on one material changes no other material. `toon` today is the stock
toon response, a hard lit and shadow step, with no ramp, rim light or shadow
tint yet.

It reaches props only: meshes whose `RenderableComponent.materialRef` names the
material. Terrain with texture layers, foliage, water, imported-model material
slots and baked lighting ignore it (a lighting bake may refuse a material that
is not `pbr`), so a whole-world toon look is not available yet.

Two spellings write the same field:

- New material: put it in the surface you create, for example
  `{ "op":"createBasicPbrSurface", "material": { "shadingModel":"toon", "baseColor":"#336699" } }`.
- Existing material: read the material asset's graph with
  `project_get_graph_snapshot`, find the node whose `nodeType` is
  `openpbr.surface` (a graph made from plain material values names it
  `surface`), then send
  `{ "op":"setNodeParameter", "nodeId":"surface", "key":"shadingModel", "value":"toon" }`.
  Do not reach for `createBasicPbrSurface` here: it resets every other field.
  In the material editor a person uses the Shading control.

The op spelling is provisional until a typed shading op lands; the field and
its values are the durable part. Pitfalls:

- The key is exactly `shadingModel` and the value one of `pbr`, `toon` or
  `unlit`; anything else fails the whole patch.
- Write no ramp, band or rim keys: the material has no such fields yet.
- A wrong `nodeId` leaves the surface unchanged, so confirm `shadingModel` on
  the asset with `project_get_graph_snapshot` and the banding with
  `qa_capture_renderer_viewport`.
- For the grade, the Toon Day environment template (Environment inspector)
  keeps the clear-day light and changes only the grade.
