Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/components/material.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ examples:
src: https://glitch.com/edit/#!/aframe-displacement-offset-registershader?path=public/index.html
---

[amaterial]: ../primitives/a-material.md
[fog]: ./fog.md
[geometry]: ./geometry.md

Expand Down Expand Up @@ -48,6 +49,18 @@ Here is an example of using an example custom material:
material="shader: ocean; color: blue; wave-height: 10"></a-entity>
```

Here is an example of sharing one material instance across entities using a
[`<a-material>` asset][amaterial]:

```html
<a-assets>
<a-material id="gold" color="#ffc65d" metalness="0.9" roughness="0.2"></a-material>
</a-assets>

<a-entity geometry="primitive: box" material="material: #gold"></a-entity>
<a-entity geometry="primitive: sphere" material="material: #gold"></a-entity>
```

## Properties

[flat]: #flat
Expand All @@ -62,6 +75,7 @@ depending on the material type applied.
| 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 an [`<a-material>` asset][amaterial] (e.g., `#wood`). When set, all other properties are ignored and the material is entirely defined by the asset. | None |
| offset | Texture offset to be used. | {x: 0, y: 0} |
| opacity | Extent of transparency. If the `transparent` property is not `true`, then the material will remain opaque and `opacity` will only affect color. | 1.0 |
| premultipliedAlpha | Whether the material's RGB channels are premultiplied by its alpha channel. Automatically forced to `true` when `blending` is `multiply` or `subtractive`. | false |
Expand Down
3 changes: 2 additions & 1 deletion docs/core/asset-management-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ scenes don't try to fetch assets while rendering.
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
- `<a-asset-item>` - Miscellaneous assets such as 3D models
- `<a-material>` - Materials and their textures
- `<audio>` - Sound files
- `<img>` - Image textures
- `<video>` - Video textures
Expand Down
1 change: 1 addition & 0 deletions docs/core/component.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ types:
| color | Currently doesn't do any parsing. Primarily used by the A-Frame Inspector to present a color picker. Also, it is required to use color type for color animations to work. | #FFF |
| int | Calls `parseInt` (e.g., `"124.5"` to `124`). | 0 |
| map | Same parsing as the `asset` property type. Will possibly be used by the A-Frame Inspector to present texture assets. | '' |
| material | For material assets. Resolves an ID selector to a `<a-material>` element (e.g., `#gold`) to the underlying shared `THREE.Material` instance. Also accepts an inline definition (e.g., `material(color: red)`) or a `THREE.Material` instance directly. | null |
| model | Same parsing as the `asset` property type. Will possibly be used by the A-Frame Inspector to present model assets. | '' |
| number | Calls `parseFloat` (e.g., `"124.5"` to `124.5`). | 0 |
| selector | Calls `querySelector` (e.g., `"#box"` to `<a-entity id="box">`). | null |
Expand Down
111 changes: 111 additions & 0 deletions docs/primitives/a-material.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
---
title: <a-material>
type: primitives
layout: docs
parent_section: primitives
source_code: src/core/a-material.js
---

[assets]: ../core/asset-management-system.md
[material]: ../components/material.md
[shaders]: ../components/material.md#built-in-materials

The material asset element defines a reusable material within the [asset
management system][assets]. The material (and its textures) is created during
scene loading and the same `THREE.Material` instance can then be shared by any
number of entities through the [material component][material]'s `material`
property or through the `material` property type.

Sharing a single material instance saves memory, avoids duplicate shader
compilations and texture uploads at runtime, and reduces material switching
in the renderer.

## Example

```html
<a-scene>
<a-assets>
<img id="wood-texture" src="wood.png">
<a-material id="wood" src="#wood-texture" roughness="0.8"></a-material>
<a-material id="red" shader="flat" color="red"></a-material>
</a-assets>

<!-- Both boxes share the exact same THREE.Material instance. -->
<a-box position="-1 1 -3" material="material: #wood"></a-box>
<a-box position="1 1 -3" material="material: #wood"></a-box>
<a-sphere position="0 2 -3" material="material: #red"></a-sphere>
</a-scene>
```

## Attributes

The `shader` attribute selects the shader to create the material with (e.g.,
`standard`, `flat` or any registered custom shader), defaulting to
`standard`. All other attributes follow the [material component][material]
base properties and the properties of the selected [shader][shaders], defined
as individual HTML attributes:

```html
<a-material id="hologram" shader="flat" color="#08f" opacity="0.5"
side="double" transparent="true"></a-material>
```

Attribute names are matched case-insensitively against the schema, so
camelCase properties can be written as-is or hyphen-free lowercase (e.g.,
`normalMap` or `normalmap`).

Changing an attribute after creation updates the shared material for every
entity using it. The `shader` attribute cannot be changed after creation.

## Referencing a Material

Entities reference the material asset via the material component's `material`
property:

```html
<a-entity geometry="primitive: box" material="material: #wood"></a-entity>
```

When the `material` property is set, all other material component properties
are ignored; the material is entirely defined by the `<a-material>` asset.
Removing the reference (e.g., `el.setAttribute('material', 'material', '')`)
makes the entity manage its own material again.

Components can also declare a property of type `material` in their schema to
receive the `THREE.Material` instance directly:

```js
AFRAME.registerComponent('my-component', {
schema: {
highlightMaterial: {type: 'material'}
},
update: function () {
// this.data.highlightMaterial is a THREE.Material or null.
}
});
```

```html
<a-entity my-component="highlightMaterial: #red"></a-entity>
```

## Inline Materials

For one-off materials, the `material` property type also accepts an inline
`material(...)` definition instead of a selector, using the same properties
as `<a-material>` attributes in style syntax:

```html
<a-entity hand-tracking-controls="hand: right; handMaterial: material(shader: flat; color: red)"></a-entity>
<a-box material="material: material(color: #8B4513; roughness: 0.9)"></a-box>
```

Identical inline definitions (same string) share a single material instance.
Each inline definition is backed by an `<a-material>` element automatically
attached under the scene's `<a-assets>`.

## Lifecycle

The scene waits for `<a-material>` elements (including their textures) to
load before rendering, like other assets. Removing the `<a-material>` element
from the DOM disposes the material and its textures.
43 changes: 43 additions & 0 deletions examples/test/material-asset/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Material Assets</title>
<meta name="description" content="Materials defined as assets, shared across entities">
<script src="../../../dist/aframe-master.js"></script>
</head>
<body>
<a-scene stats>
<a-assets>
<img id="gradient" src="../../assets/img/gray-gradient.jpg">
<a-material id="brand" shader="flat" src="../../assets/img/mozvr.png" transparent="true"></a-material>
<a-material id="gold" color="#FFC65D" metalness="0.9" roughness="0.2"></a-material>
<a-material id="glass" shader="flat" color="#7BC8A4" opacity="0.5" side="double"></a-material>
<a-material id="gradient-mat" src="#gradient" roughness="1"></a-material>
</a-assets>

<!-- All these entities share the same THREE.Material instances. -->
<a-box position="-3 1 -4" material="material: #gold"></a-box>
<a-sphere position="-1 1.25 -4" radius="0.75" material="material: #gold"></a-sphere>
<a-cylinder position="1 1 -4" radius="0.5" material="material: #gold"></a-cylinder>

<a-box position="-3 2.75 -4" material="material: #brand"></a-box>
<a-plane position="3 1.5 -4" width="2" height="2" material="material: #brand"></a-plane>

<a-box position="-1 2.75 -4" material="material: #glass"></a-box>
<a-sphere position="1 2.75 -4" radius="0.5" material="material: #glass"></a-sphere>

<a-cylinder position="3 3.25 -4" radius="0.5" material="material: #gradient-mat"></a-cylinder>

<!-- Inline material definitions; identical definitions share one instance. -->
<a-box position="-3 4.25 -4" scale="0.5 0.5 0.5"
material="material: material(shader: flat; color: #EF2D5E)"></a-box>
<a-sphere position="-1.5 4.25 -4" radius="0.35"
material="material: material(shader: flat; color: #EF2D5E)"></a-sphere>

<a-plane rotation="-90 0 0" width="12" height="12" material="material: #gradient-mat"></a-plane>

<a-sky color="#ECECEC"></a-sky>
</a-scene>
</body>
</html>
139 changes: 139 additions & 0 deletions examples/test/material-asset/perf.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Material Assets - Performance Comparison</title>
<meta name="description" content="Compare shared material assets against per-entity materials">
<script src="../../../dist/aframe-master.js"></script>
<style>
#hud {
background: rgba(0, 0, 0, 0.75);
color: #fff;
font-family: monospace;
font-size: 13px;
left: 10px;
padding: 10px;
position: fixed;
top: 10px;
white-space: pre;
z-index: 9999;
}
#hud a { color: #7bc8a4; }
</style>
<script>
var params = new URLSearchParams(window.location.search);
var SHARED = params.get('shared') !== 'false';
var COUNT = parseInt(params.get('n'), 10) || 1000;
var FRAMES = 500;

// Collects timing and renderer statistics, displayed in the #hud element.
AFRAME.registerComponent('perf-stats', {
init: function () {
this.frameTimes = [];
this.lastTime = 0;
this.done = false;
this.loadTime = 0;
var self = this;
this.el.addEventListener('loaded', function () {
self.loadTime = performance.now();
});
},

tick: function (time) {
if (this.done) { return; }
if (this.lastTime) { this.frameTimes.push(time - this.lastTime); }
this.lastTime = time;
if (this.frameTimes.length >= FRAMES) {
this.done = true;
this.report();
}
},

report: function () {
var sceneEl = this.el;
var renderer = sceneEl.renderer;
var materials = new Set();
var textures = new Set();
sceneEl.object3D.traverse(function (node) {
if (!node.material) { return; }
materials.add(node.material);
if (node.material.map) { textures.add(node.material.map); }
});

var frameTimes = this.frameTimes.slice().sort(function (a, b) { return a - b; });
var sum = frameTimes.reduce(function (a, b) { return a + b; }, 0);
var stats = {
mode: SHARED ? 'shared <a-material> asset' : 'per-entity materials',
entities: COUNT,
loadTimeMs: Math.round(this.loadTime),
uniqueMaterials: materials.size,
uniqueTextures: textures.size,
programs: renderer.info.programs.length,
texturesGPU: renderer.info.memory.textures,
frameAvgMs: +(sum / frameTimes.length).toFixed(2),
frameP50Ms: +frameTimes[Math.floor(frameTimes.length * 0.5)].toFixed(2),
frameP90Ms: +frameTimes[Math.floor(frameTimes.length * 0.9)].toFixed(2),
frameP99Ms: +frameTimes[Math.floor(frameTimes.length * 0.99)].toFixed(2)
};
window.perfStats = stats;
console.log('perf-stats', JSON.stringify(stats));
document.getElementById('results').textContent =
'mode: ' + stats.mode + '\n' +
'entities: ' + stats.entities + '\n' +
'load time: ' + stats.loadTimeMs + ' ms\n' +
'unique materials: ' + stats.uniqueMaterials + '\n' +
'unique textures: ' + stats.uniqueTextures + '\n' +
'GL programs: ' + stats.programs + '\n' +
'GPU textures: ' + stats.texturesGPU + '\n' +
'frame avg: ' + stats.frameAvgMs + ' ms\n' +
'frame p50/p90/p99: ' + stats.frameP50Ms + ' / ' + stats.frameP90Ms +
' / ' + stats.frameP99Ms + ' ms';
}
});

window.addEventListener('DOMContentLoaded', function () {
var sceneEl = document.querySelector('a-scene');
var container = document.getElementById('container');
var side = Math.ceil(Math.cbrt(COUNT));
var i;
var entity;
var x;
var y;
var z;

for (i = 0; i < COUNT; i++) {
x = (i % side) - side / 2;
y = Math.floor(i / side) % side;
z = -4 - Math.floor(i / (side * side));
entity = document.createElement('a-entity');
entity.setAttribute('geometry', 'primitive: box; width: 0.4; height: 0.4; depth: 0.4');
entity.setAttribute('position', (x * 0.6) + ' ' + (y * 0.6) + ' ' + (z * 0.6));
if (SHARED) {
entity.setAttribute('material', 'material: #shared');
} else {
entity.setAttribute('material',
'src: ../../assets/img/gray-gradient.jpg; ' +
'metalness: 0.4; roughness: 0.6; color: #7BC8A4');
}
container.appendChild(entity);
}
sceneEl.setAttribute('perf-stats', '');
});
</script>
</head>
<body>
<div id="hud"><span id="results">collecting…</span>

<a href="?shared=true">shared materials</a> | <a href="?shared=false">per-entity materials</a></div>
<a-scene>
<a-assets>
<a-material id="shared" src="../../assets/img/gray-gradient.jpg"
metalness="0.4" roughness="0.6" color="#7BC8A4"></a-material>
</a-assets>
<a-entity id="container"></a-entity>
<a-entity light="type: ambient; intensity: 0.6"></a-entity>
<a-entity light="type: directional; intensity: 1" position="1 2 1"></a-entity>
<a-sky color="#222"></a-sky>
</a-scene>
</body>
</html>
Loading
Loading