Manufactured environment materials
Assets declare physical material, finish, rendering, structure, and content. Shaders decide how those facts look. Classify once, shade many times.
ToonLab assets and reusable environment shaders share one portable contract: classify once, shade many times.
This contract covers non-character, non-vegetation manufactured environments: props, vehicles, buildings, streets, furniture, rooms, appliances, signs, and indoor clutter. An asset records what each surface physically is. A shader decides how those facts look in its own style. Changing the shader must not require reclassifying the asset.
The older urbanSurface role is supported as a compatibility input. New
assets should author the layered urbanMaterial object described below.
The ownership boundary
| Owner | Stores | Must not store |
|---|---|---|
| Asset / GLB | Stable material classification, source color and PBR maps, optional material-ID and condition masks | Pastel amount, cel thresholds, palette overrides, reflection strength, time-of-day values |
| Shader definition | Global look settings and sparse profiles for material, finish, rendering, and structural axes | Asset-specific mesh names or one-off color corrections |
| Scene | Lights, reflection environment, exposure, weather, and interior/exterior context | Permanent material classification |
| Import audit | Confidence, warnings, and a documented correction for genuinely ambiguous source material | A replacement look preset |
The classification describes facts, not a screenshot-matching instruction. A black solar panel remains a display-like glass/composite surface in every style. One shader may give it broad anime reflections and another narrow ink highlights, but neither should relabel it.
The five axes
Do not create a flat role for every object part. lid, roof, trim,
furniture, and carPaint are not physical materials. Describe a material
with independent axes so the same small vocabulary composes across asset
categories.
Base material
baseMaterial accepts:
| ID | Includes |
|---|---|
metal |
Steel, aluminum, iron, copper, brass, and other conductive surfaces |
mineral |
Brick, concrete, plaster, stucco, stone, marble, asphalt, drywall, and roofing minerals |
wood |
Timber, boards, veneer, plywood, and manufactured wood |
polymer |
Rigid plastic, acrylic, vinyl, resin, and foam |
rubber |
Tires, seals, flexible bumpers, and rubberized parts |
glass |
Windows, mirrors, lenses, and transparent glazing |
ceramic |
Porcelain, pottery, tile, sinks, and glazed ceramic |
textile |
Fabric, upholstery, curtains, canvas, carpet, and rugs |
leather |
Leather and suede |
paper |
Paper, cardboard, cartons, and printed sheets |
composite |
Carbon fiber, fiberglass, laminates, and mixed engineered sheets |
fluid |
Liquids represented by an asset material rather than a scene water system |
genericDielectric |
Safe fallback when the source cannot be identified confidently |
Finish
finish accepts:
raw, painted, varnished, clearCoated, polished, brushed,
glazed, anodized, mirror, or matte.
Finish describes the top optical layer. A clear-coated car body is
metal + clearCoated; a dumpster body is metal + painted; a chrome faucet
is metal + polished; a wooden table may be
wood + varnished.
Source roughness, metalness, normal, and clearcoat data remain authoritative inside the classification. The finish supplies shader priors and stylization policy when maps are incomplete.
Render mode
renderMode accepts:
opaque, alphaCutout, translucent, transmissive, or unlit.
Emission is not a render mode because an opaque or transmissive material may also emit light.
Structural role
structuralRole accepts:
| ID | Meaning |
|---|---|
primaryMass |
Main readable body or architectural mass |
secondaryStructure |
Covers, roofs, braces, secondary panels, and attachments |
trim |
Frames, molding, borders, and deliberately subordinate detail |
fastener |
Handles, hinges, rails, pipes, bolts, and small hardware |
cavity |
Recesses, interiors, voids, and deliberately dark inset regions |
window |
Architectural or vehicle glazing aperture |
graphic |
Readable sign, poster, printed panel, or decal-dominated zone |
lightEmitter |
Visible lamp, light strip, bulb, or luminous fixture surface |
Structural role controls line weight, detail hierarchy, and a small number of
response multipliers. It never replaces the base material. A metal roof and a
slate roof can share secondaryStructure while retaining different material
responses.
Content and condition masks
contentFlags may contain graphic, display, and emissive.
Continuous condition maps should carry wear, dirt, rust/oxidation, chipped
coating, wetness, burn/soot, snow/frost, and other coverage. These are masks,
not new material classes. Rust over painted steel does not turn the entire
surface into metal + raw.
Authoring metadata
Put a versioned urbanMaterial object in glTF extras on the material:
{
"name": "MAT_car_body",
"extras": {
"urbanMaterial": {
"version": 1,
"baseMaterial": "metal",
"finish": "clearCoated",
"renderMode": "opaque",
"structuralRole": "primaryMass",
"contentFlags": []
}
}
}
Three.js exposes extras through userData:
material.userData.urbanMaterial = {
version: 1,
baseMaterial: 'glass',
finish: 'polished',
renderMode: 'transmissive',
structuralRole: 'window',
contentFlags: [],
};
A node-level value is a default for every material below it. Material metadata
wins when a multi-material mesh needs different classifications. Reopen every
exported GLB and inspect userData; do not assume the DCC exporter preserved
custom properties.
Individual compatibility fields are also accepted:
urbanBaseMaterial, urbanFinish, urbanRenderMode,
urbanStructuralRole, and urbanContentFlags. Prefer the nested object for
new assets.
Mixed atlases and material-ID masks
One classification cannot describe an atlas containing incompatible surfaces. For example, a facade atlas may contain brick, timber, painted metal, and window glass. Either:
- split those zones into separate materials, or
- provide a material-ID mask and a channel-to-classification table.
A practical packed mask might use R for masonry, G for wood, B for metal, and A for glass. The assignment is authored once and remains stable across every shader. Do not pick the one class that happens to make the current beauty render look least wrong.
This is why some meshes require a split or ID mask before glass, wood, and masonry can react independently. A draw call has one material classification. If brick and window pixels share that draw call and there is no pixel mask, the shader has no information that distinguishes them. Polygon count is not the issue; missing semantic boundaries are. Split materials are mandatory when render modes differ (for example opaque masonry beside transmissive glass). An ID mask is appropriate when multiple opaque classes can remain in one atlas.
Resolution and fallbacks
The reference classifier resolves classification in this order:
- Material
userData.urbanMaterial - Mesh/node
userData.urbanMaterial - Individual
urbanBaseMaterialand companion compatibility fields - Legacy
urbanSurface - Canonical tokens and conservative semantic name inference
- PBR cues such as transparency, emission, and strong metalness
genericDielectric
Inference exists to make unprepared third-party GLBs inspectable. It is not a production authoring substitute. The benchmark exposes fallback counts so low-confidence atlases remain visible instead of silently masquerading as painted metal.
Automatic detection versus visual analysis
classifyManufacturedMaterial(object, material) and
analyzeManufacturedAsset(root) provide the automatic code path. They use:
- explicit
urbanMaterial/legacy metadata; - material, object, and short ancestry names;
- alpha test, opacity, transparency, transmission, and unlit state;
- emissive maps/colors and strong metalness; and
- stable source metadata preserved by the loader.
This is intended to cover the normal 80%+ import path when assets have clean PBR separation and useful names. It will not guess a semantic material from base-color pixels alone. A visual model can propose regions for an anonymous mixed atlas, but should run as an offline import assistant with confidence and review. Its output is not a secret runtime shader branch: approve it into material splits, an ID-mask channel table, or a sidecar assignment.
const audit = analyzeManufacturedAsset(root);
if (audit.summary.fallbackCount || audit.summary.lowConfidenceCount) {
showImportReview(audit);
}
Object class
Object class is a profile selector, not another material axis. Put
urbanObjectClass on the asset root. Supported values are:
generic, prop, vehicle, buildingExterior, buildingInterior,
furniture, fixture, appliance, infrastructure, signage,
industrialMachine, and clutter.
For example, buildingExterior can reduce close-up normal emphasis or tune
line hierarchy across a facade while brick remains mineral, window remains
glass, and metal flashing remains metal. Do not assign one
buildingExterior material to the whole building.
Runtime overlay and durable persistence
The runtime attaches classification to the loaded Three.js graph. It does not rewrite geometry, UVs, or texture pixels. Choose one durable representation:
- embed
urbanMaterialin glTF material/nodeextras(preferred for owned GLB assets), or - keep an adjacent
*.toonlab-materials.jsonsidecar (preferred for third-party, FBX, OBJ, USDZ, VRM, PMX/PMD, or read-only sources).
{
"type": "toonlab/manufactured-material-manifest",
"version": 1,
"assetId": "city-bus-stop-a",
"objectClass": "infrastructure",
"assignments": [
{
"selector": { "materialName": "M_Glass" },
"classification": {
"version": 1,
"baseMaterial": "glass",
"finish": "polished",
"renderMode": "transmissive",
"structuralRole": "window",
"contentFlags": []
}
}
]
}
Call applyManufacturedMaterialManifest(root, manifest) after any supported
loader returns its Object3D. Use stable material names, object names/paths,
and assetId; do not persist Three.js UUIDs.
The Manufactured Material Lab is the official review surface. Its tag switches are a temporary overlay. “Export sidecar JSON” creates the durable data; it does not silently modify the source model.
Current lab migration
The Manufactured Material Lab uses an import annotation table for third-party
GLBs that do not yet contain urbanMaterial extras. This table represents the
one-time asset classification step; it is not a shader override. When an asset
is published into the ToonLab library, move the same facts into the GLB or its
asset manifest.
| Benchmark asset | Current contract state |
|---|---|
| Dumpster | Mesh-level metal, rubber, painted coating, trim, and fastener assignments |
| Bus station | Material-level metal, composite/display, light-emitter, graphic, glass, and mineral assignments |
| Apartment building | Material-level mineral assignments with distinct trim, roof/gable, asphalt, and polished marble finishes |
| Streetcar | Mixed reprojected atlases; uses genericDielectric and remains flagged for an ID-mask or material-split audit |
| Beach props | One atlas spans several physical materials; uses genericDielectric and remains flagged for an ID-mask or material-split audit |
| Ground-floor kit | One atlas mixes masonry, wood, metal, and recesses; uses genericDielectric and remains flagged for an ID-mask or material-split audit |
Do not clear an audit by labeling a mixed atlas with whichever base material occupies the most pixels. Split the zones or add a stable material-ID mask.
Legacy urbanSurface migration
The following compatibility roles still load:
| Legacy role | Layered interpretation |
|---|---|
paintedMetal |
metal + painted + opaque + primaryMass |
paintedTrim |
metal + painted + opaque + trim |
bareMetal |
metal + raw + opaque + secondaryStructure |
rubber |
rubber + matte + opaque + secondaryStructure |
lid |
metal + painted + opaque + secondaryStructure |
graphicPanel |
genericDielectric + matte + opaque + graphic, with graphic content |
technicalSurface |
genericDielectric + matte + opaque + secondaryStructure, with display content |
lid is intentionally not present in the new base-material list. A dumpster
cover, vehicle hood, building roof, and storage-box lid may all use different
materials even though they are similarly shaped parts.
Shader profiles
A reusable shader should compose four layers:
- Global look — cel bands, shadow treatment, exposure response, palette policy, line work, global reflection scale, and time-of-day behavior.
- Material and finish profiles — sparse priors for pastel eligibility, texture authority, roughness breakup, reflection, Fresnel, wear, and value limits.
- Structural/content modifiers — line hierarchy, cavity retention, graphic legibility, display behavior, emission, and window response.
- Scene context — exterior day/night or interior day/night lighting, reflection environment, ambient fill, and portal/local-light behavior.
These layers are data, not independent shader forks. A custom shader tunes global settings and profile tables; it does not reassign asset classification. ToonLab only forks shader implementation for incompatible render behavior, not for every physical base material.
const lockedEnvironmentLook = {
version: 1,
default: {
parameters: {
specularStrength: 0.12,
},
},
baseMaterials: {
mineral: {
parameters: { specularStrength: 0.04 },
},
glass: {
parameters: { specularStrength: 0.42 },
},
},
objectClasses: {
buildingExterior: {
parameters: { normalMapStrength: 0.72 },
},
},
};
The exact resolution order is:
- global environment catch-all;
materialLook.default;baseMaterials[classification.baseMaterial];finishes[classification.finish];renderModes[classification.renderMode];structuralRoles[classification.structuralRole];- each matching
contentFlagsprofile in canonical order; objectClasses[root.userData.urbanObjectClass]; andassets[stableAssetId].
The final asset layer is for rare hero-object response, not a substitute for
correct classification. Environment preset documents store materialLook
alongside features, parameters, rig, and scenario variants; style
bundles preserve it.
Classification examples
| Surface | Classification |
|---|---|
| Dumpster body | metal + painted + opaque + primaryMass |
| Dumpster cover | metal/polymer + painted/matte + opaque + secondaryStructure |
| Bare handle | metal + raw + opaque + fastener |
| Car body | metal + clearCoated + opaque + primaryMass |
| Windshield | glass + polished + transmissive + window |
| Tire | rubber + matte + opaque + secondaryStructure |
| Brick facade | mineral + raw + opaque + primaryMass |
| Painted interior wall | mineral + painted + opaque + primaryMass |
| Wooden table | wood + varnished + opaque + primaryMass |
| Sofa upholstery | textile + matte + opaque + primaryMass |
| Ceramic sink | ceramic + glazed + opaque + primaryMass |
| Television screen | glass/polymer + polished + opaque + secondaryStructure, with display and emissive as appropriate |
| Poster | paper + matte + opaque + graphic, with graphic content |
Adding schema values
Schema evolution is not shader customization. Add a base material, finish, render mode, structural role, or content flag only when:
- multiple unrelated assets need it;
- the existing axes cannot represent it without recurring hacks;
- at least two styles need to treat it differently; and
- it describes what the source is, not how one reference image looks.
Update the enum, classifier, validators, documentation, compatibility mapping, and official shader fallback tables together. Unknown values must warn and fall back safely.
Model-generation and import checklist
- Inventory every distinct material zone.
- Preserve source albedo/color, normal, roughness, metalness, AO, emissive, opacity, clearcoat, transmission, and UV data.
- Assign the five classification axes per material.
- Split incompatible zones or add a material-ID mask.
- Keep condition coverage in masks rather than class names.
- Export
urbanMaterialin glTF extras. - Reload the GLB and inspect classifications and warnings.
- Test at multiple times of day and camera angles.
- Test graphics for legibility, black materials for value retention, and reflective surfaces for view-dependent variation.
- Test the same asset with more than one shader without changing metadata.
The asset passes when its identity survives multiple styles without classification edits. A shader passes when one profile works across unrelated assets without model-name conditionals.