Materials as assets and property type#5846
Conversation
Extract parseSide, parseBlending, disposeMaterial and the base material property assignment (updateBaseMaterial) from the material component to utils/material.js so they can be reused outside the component. No behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mevr#5457) Materials can now be defined once in <a-assets> and shared across entities: <a-assets> <a-material id="wood" src="#woodTexture" roughness="0.8"></a-material> </a-assets> <a-box material="material: #wood"></a-box> - <a-material> creates a THREE.Material through the registered shaders (standard, flat, custom) during scene loading, including textures, and <a-assets> blocks rendering until it is ready. - New `material` property type resolves an ID selector to the shared THREE.Material so any component can accept a material in its schema. - The material component accepts a `material` property; when set, the shared material is used as-is and the component does not own (dispose/register) it. - Attribute changes on <a-material> update the shared material live for every entity using it; removing the element disposes it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The new `material` property in the material component schema collided with the material component name in the auto-generated mesh primitive mappings, triggering a "mapping collision" console warning and a useless `material-material` attribute on every mesh primitive. Exclude the property from the mappings; primitives set it through `material="material: #ref"` like any other entity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The `material` property type creates the material on demand. When that parse happens before the scene initialized its systems (e.g., an entity setAttribute at DOMContentLoaded referencing the material asset), texture loading crashed on the missing material system. Defer the texture-loading shader update until the material system is available; it then runs when the <a-material> connects to the loaded scene. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generalize the unclosed-url() special case (added for data URIs) to any parenthesized value, so function-like values such as the upcoming inline `material(...)` syntax can contain semicolons: material="material: material(shader: flat; color: red); side: double" Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
In addition to referencing an <a-material> asset by selector, the
`material` property type accepts an inline definition:
<a-entity hand-tracking-controls="hand: right;
handMaterial: material(shader: flat; color: red)">
<a-box material="material: material(color: #8B4513; roughness: 0.9)">
Each inline definition is backed by an <a-material> element attached
under the scene's <a-assets>, and identical definitions are cached so
they share a single THREE.Material instance.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
styleParse pools its chunks array across calls. Assigning a parsed value into the target object can synchronously trigger a nested parse (the component attrValueProxy setter parsing an inline `material(...)` definition), which clobbered the pooled array the outer call was still iterating, leaking inner keys into the outer object and dropping outer chunks. Nested calls now use their own array. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two <a-material> assets (PBR and textured) shared across entities, plus an inline material(...) definition, to test the material property type. Requires an A-Frame build with aframevr/aframe#5846; with older builds these entities render with a default material and a warning. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| | alphaTest | Alpha test threshold for transparency. | 0 | | ||
| | depthTest | Whether depth testing is enabled when rendering the material. | true | | ||
| | flatShading | Use `THREE.FlatShading` rather than `THREE.StandardShading`. | false | | ||
| | material | Selector to a [`<a-material>` asset][amaterial] to use as shared material (e.g., `#wood`). When set, all other properties are ignored and the material is entirely defined by the asset. | None | |
There was a problem hiding this comment.
Nitpick: an <a-material>
I'd also drop the word "shared". While one of the primary benefits is material sharing, it's not inherently required. Nothing wrong with a user creating an <a-material> and only using it for a single object, for example.
There was a problem hiding this comment.
Fixed in 93d7c09: "an <a-material>" and dropped the "shared" wording.
| @@ -22,6 +22,7 @@ We place assets within `<a-assets>`, and we place `<a-assets>` within | |||
| `<a-scene>`. Assets include: | |||
|
|
|||
| - `<a-asset-item>` - Miscellaneous assets such as 3D models and materials | |||
There was a problem hiding this comment.
This description should probably no longer mention materials to avoid confusion.
There was a problem hiding this comment.
Done in 93d7c09, the <a-asset-item> line no longer mentions materials.
| `<a-scene>`. Assets include: | ||
|
|
||
| - `<a-asset-item>` - Miscellaneous assets such as 3D models and materials | ||
| - `<a-material>` - Shared materials, created (with their textures) at load time |
There was a problem hiding this comment.
Nitpick: I don't think "shared" needs to be highlighted here, as this applies to most assets. You can "share" the same audio, image tag as well. Something simple like "Materials and their textures" probably suffices.
There was a problem hiding this comment.
Done in 93d7c09, now reads "Materials and their textures".
| var sep = ';'; | ||
| // Re-entrant calls must not clobber the pooled array an outer call is | ||
| // still iterating over. | ||
| var chunks = reentrant ? [] : pooledChunks; |
There was a problem hiding this comment.
Instead of instantiating new empty arrays for nested calls, we could turn pooledChunks into an array of arrays. One entry for each level.
In fact, we could even consider limiting the amount of "nesting" we allow. Maybe even as low as 2 levels, and throwing an Error if this is exceeded. With the idea that under normal usage you wouldn't encounter this (or if you did, you probably wouldn't want it).
For the getKeyValueChunks method it could receive the chunks array to use and the styleParse function could pass it using pooledChunks[parseDepth] (as well as throw if we introduce an upper bound)
There was a problem hiding this comment.
Good idea, done in 6f786c5: pooledChunks is now an array of arrays indexed by parse depth, getKeyValueChunks receives the chunks array to use, and nesting is capped at two levels (outer style string + nested parse from a property assignment, e.g. an inline material value) — exceeding the cap throws. parseDepth is decremented in a finally so a throwing property setter cannot poison subsequent parses. Added a test for the cap and recovery.
| if (key === 'alphaTest' || key === 'side' || key === 'vertexColorsEnabled') { | ||
| this.material.needsUpdate = true; | ||
| } |
There was a problem hiding this comment.
Minor point, but the material would now also be marked as needing an update if the attribute is changed to the same value, as attributeChangedCallback still fires in that case.
But looking at it more closely, I think we could move this logic into updateBaseMaterial and actually check if any of the relevant properties changed. That would also make the logic in material.js easier, as it now bases it on the oldData properties, whereas the material itself should be the source of truth.
There was a problem hiding this comment.
Good point, done in ef50899: updateBaseMaterial now compares against the material itself and only flags needsUpdate when alphaTest/side/vertexColors actually change. That removed the oldData diff from the material component (updateMaterial is now a one-liner) and the explicit flag in <a-material>'s attributeChangedCallback. Added a test that setting an attribute to its current value does not bump material.version while a real change does.
Fix article (an <a-material>), drop the "shared" emphasis since sharing is a benefit but not a requirement, and stop mentioning materials in the <a-asset-item> description to avoid confusion with <a-material>. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Instead of allocating a fresh array for nested parses, pool one chunks array per nesting level and pass it to getKeyValueChunks. Nesting is capped at two levels (an outer style string and a nested parse triggered by a property assignment, e.g. an inline material value); exceeding the cap throws. parseDepth unwinds in a finally block so a throwing property setter cannot poison subsequent parses. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Flag needsUpdate only when a property requiring a program rebuild (alphaTest, side, vertexColors) actually changes, comparing against the material itself as the source of truth. This removes the oldData diff from the material component and stops <a-material> from flagging a rebuild when an attribute is set to its current value. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Changes for the inspector: aframevr/aframe-inspector#859 |
Fixes #5457
Description
Materials can now be defined once in
<a-assets>and shared across any number of entities, instead of every entity creating its ownTHREE.Material:<a-material>asset element (src/core/a-material.js)standard,flat, or any custom shader) during scene loading, including its textures;<a-assets>blocks scene rendering until the material and its textures are ready.normalmap→normalMap).shadercannot be changed after creation).materialproperty type (src/core/propertyTypes.js)Resolves an ID selector to the underlying shared
THREE.Material. Any component can accept a material in its schema (e.g. thehand-tracking-controlsuse case from the issue):It also accepts a
THREE.Materialinstance directly, and inline definitions (below). Stringification round-trips to#id/material(...)forflushToDOM.Material component integration (
src/components/material.js)materialproperty:material="material: #wood". When set, the shared material is used as-is and all other material component properties are ignored (the asset fully defines the material).remove()/material switching leaves them intact. Settingmaterialback to empty returns the entity to its own component-managed material.<a-box material="material: #wood">.Inline
material(...)definitionsFor one-off materials, the property type also accepts an inline definition (the optional syntax proposed in the issue):
Each inline definition is backed by an
<a-material>element automatically attached under the scene's<a-assets>; identical definition strings are cached and share a singleTHREE.Materialinstance.To support semicolons inside the parentheses,
styleParserno longer splits style strings inside parenthesized values — a generalization of the existing unclosed-url(special case for data URIs.Commits
parseSide,parseBlending,disposeMaterialand base property assignment (updateBaseMaterial) extracted from the material component for reuse.<a-material>asset element and material property type — the core feature, tests, docs, examples.materialproperty — the new schema property collided with the material component name in auto-generated mesh primitive mappings, spamming a "mapping collision" warning per primitive.<a-material>texture loading when created before scene systems exist — on-demand creation by an early property parse (e.g.setAttributeatDOMContentLoaded) crashed on the missing material system; texture loading is now deferred until the system is available.material()definitions in the material property type.attrValueProxysetter synchronously parses the inlinematerial(...)value, and that nested parse clobbered the pooled chunks array of the outer parse (leaking inner keys intoattrValue, dropping outer properties).Performance
Measured with
examples/test/material-asset/perf.html(Chrome, Linux, 500-frame sample;?shared=&n=to reproduce):<a-material>GL program count and GPU texture count are identical in both modes (three.js already deduplicates programs, and the texture source cache deduplicates uploads), so the wins are per-material CPU overhead in the render loop, loading time, and memory.
Tests
tests/core/a-material.test.js(13 tests): creation from attributes, shader defaults, texture load blocking, instance sharing, live attribute updates, dispose ownership (entity switch/removal vs asset removal), early-creation regression, inline materials (cache sharing, single-property schema).tests/core/propertyTypes.test.js:materialtype parse/stringify incl. inline round-trip.tests/extras/primitives/primitives.test.js: nomaterial/material-materialprimitive mapping.tests/utils/styleParser.test.js(new file): paren-aware splitting incl. data URIs and nested parens.Full suite: 1200+ passing; the only failures are the 2 look-controls touch-drag tests that fail identically on master in this environment (canvas-size assertion, unrelated).
Docs & examples
docs/primitives/a-material.md; updates todocs/components/material.md,docs/core/component.md(property types table),docs/core/asset-management-system.md.examples/test/material-asset/index.html(demo: shared texture/PBR/transparent materials, inline materials) andexamples/test/material-asset/perf.html(benchmark used for the numbers above).🤖 Generated with Claude Code