Skip to content

Commit

Permalink
Improve sort order (#5341)
Browse files Browse the repository at this point in the history
* Updated sort functions for A-Frame

* Test cases WIP

* Documentation updates

* Improve renderer unit tests

* simplify path

* UTs for new sort functions

* Destructuring is not allowed in ES5

* Use longhand initialization of object (shorthand unsupported in ES5)

* Move object sorting detail away from renderer sortTransparentObjects property description into FAQ

* More ES5 compliance clean-up

* Fix typo may -> many

* Move initialization of object sorting config from AScene to renderer system

* sortTransparentObjects can be set dynamically.

* rename aframeSortX functions to sortX

* Move sortFunctions.js module into systems/renderer.js

* Give sort functions more meaningful names

* Remove redundant 'Spatial' from function names

* Remove unnecessary final else-if branches.

* Early returns to remove "else if" statements and corresponding indentation

---------

Co-authored-by: Diego Marcos <diego.marcos@gmail.com>
  • Loading branch information
diarmidmackenzie and dmarcos authored Aug 11, 2023
1 parent 67e34d6 commit 3c6cc78
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 14 deletions.
16 changes: 12 additions & 4 deletions docs/components/renderer.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ It also configures presentation attributes when entering WebVR/WebXR.
| colorManagement | Whether to use a color-managed linear workflow. | true |
| highRefreshRate | Increases frame rate from the default (for browsers that support control of frame rate). | false |
| foveationLevel | Amount of foveation used in VR to improve perf, from 0 (min) to 1 (max). | 1 |
| sortObjects | Whether to sort objects before rendering. | false |
| sortTransparentObjects | Whether to sort transparent objects (far to near) before rendering | false |
| physicallyCorrectLights | Whether to use physically-correct light attenuation. | false |
| maxCanvasWidth | Maximum canvas width. Uses the size multiplied by device pixel ratio. Does not limit canvas width if set to -1. | 1920 |
| maxCanvasHeight | Maximum canvas height. Behaves the same as maxCanvasWidth. | 1920 |
Expand All @@ -43,7 +43,7 @@ It also configures presentation attributes when entering WebVR/WebXR.
| exposure | When any toneMapping other than "no" is used this can be used to make the overall scene brighter or darker | 1 |
| anisotropy | Default anisotropic filtering sample rate to use for textures | 1 |

> **NOTE:** Once the scene is initialized, none of these properties may no longer be changed apart from "exposure" and "toneMapping" which can be set dynamically.
> **NOTE:** Once the scene is initialized, none of these properties may no longer be changed apart from "exposure", "toneMapping", and "sortTransparentObjects" which can be set dynamically.
### antialias

Expand Down Expand Up @@ -82,14 +82,22 @@ Controls the amount of foveation which renders fewer pixels near the edges of th
when in stereo rendering mode on certain systems. The value should be in the range of 0 to 1, where
0 is the minimum and 1 the maximum amount of foveation. This is currently supported by the Oculus Browser.

### sortObjects


### sortTransparentObjects

[sorting]: ../introduction/faq.md#what-order-does-a-frame-render-objects-in

Sorting is used to attempt to properly render objects that have some degree of transparency.
Due to various limitations, proper transparency often requires some amount of careful setup.
By default, objects are not sorted, and the order of elements in the DOM determines order of
By default, transparent objects are not sorted, and the order of elements in the DOM determines order of
rendering. Re-ordering DOM elements provides one way of forcing a consistent behavior, whereas
use of `renderer="sortObjects: true"` may cause unwanted changes as the camera moves.

Some more background on how A-Frame sorts objects for rendering can be found [here][sorting]



### physicallyCorrectLights

By default, point and spot lights attenuate (or, appear dimmer as they become farther away)
Expand Down
18 changes: 18 additions & 0 deletions docs/introduction/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,3 +343,21 @@ Phones with Adreno 300 series GPUs are notoriously problematic. Set [renderer pr
## Can I use A-Frame offline or self hosted?

Using A-Frame online sometimes is not possible or inconvenient, like for instance when traveling or during public events with poor Internet connectivity. A-Frame is mostly self-contained so including the build (aframe.min.js) in your project will be sufficient in many cases. Some specific parts are lazy loaded and only fetched when used. This is for example the case of the fonts for the text component and the 3D models for controllers. In order to make an A-Frame build work either offline or without relying on A-Frame hosting infrastructure (typically cdn.aframe.io), you can monitor network requests on your browser console. This will show precisely what assets are being loaded and thus as required for your specific experience. Fonts can be found via FONT_BASE_URL in the whereas controllers via MODEL_URLS. Both can be modified in the source and included in your own [custom build](https://github.com/aframevr/aframe#generating-builds)

## What order does A-Frame render objects in?

[sortTransparentObjects]: ../components/renderer.md#sorttransparentobjects

In many cases, the order in which objects is rendered doesn't matter much - most scenes will look the same whatever order the objects are rendered in - but there are a few cases where sorting is important:

- for transparent objects, it's typically better to render objects furthest to nearest (although some cases are more complex and require [more sophisticated approaches](https://threejs.org/manual/?q=transp[#en/transparency)). However, when the camera and/or objects are moving, this can result in undesirable visual effects when objects switch in terms of their relative distance from the camera
- for performance reasons, it's typically desirable to render objects nearest to furthest, so that GPU doesn't spend time drawing pixels that end up being drawn over.
- for AR "hider material" used to hide parts of the scene to create the appearance of occlusion by real-world objects, it's typically necessary to render these before the rest of the scene.

By default, A-Frame sorts objects as follows:

- for all objects, if [`renderOrder`](https://threejs.org/docs/index.html?q=object3D#api/en/core/Object3D.renderOrder) is set on the Object3D or a Group that it is a member of, the specified order will be respected
- otherwise, opaque objects are rendered furthest to nearest, and transparent objects are rendered in the order they appear in the THREE.js Object tree (in most cases, this is the same as the order they appear in the DOM)

The `renderer` system has a [`sortTransparentObjects`][sortTransparentObjects] property, which can be used to render transparent objects furthest to nearest, rather than in DOM order.

2 changes: 1 addition & 1 deletion src/core/scene/a-scene.js
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,7 @@ class AScene extends AEntity {

renderer = this.renderer = new THREE.WebGLRenderer(rendererConfig);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.sortObjects = false;

if (this.camera) { renderer.xr.setPoseTarget(this.camera.el.object3D); }
this.addEventListener('camera-set-active', function () {
renderer.xr.setPoseTarget(self.camera.el.object3D);
Expand Down
66 changes: 64 additions & 2 deletions src/systems/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ module.exports.System = registerSystem('renderer', {
toneMapping: {default: 'no', oneOf: ['no', 'ACESFilmic', 'linear', 'reinhard', 'cineon']},
precision: {default: 'high', oneOf: ['high', 'medium', 'low']},
anisotropy: {default: 1},
sortObjects: {default: false},
sortTransparentObjects: {default: false},
colorManagement: {default: true},
alpha: {default: true},
foveationLevel: {default: 1}
Expand All @@ -32,7 +32,7 @@ module.exports.System = registerSystem('renderer', {
var toneMappingName = this.data.toneMapping.charAt(0).toUpperCase() + this.data.toneMapping.slice(1);
// This is the rendering engine, such as THREE.js so copy over any persistent properties from the rendering system.
var renderer = sceneEl.renderer;
renderer.sortObjects = data.sortObjects;

renderer.useLegacyLights = !data.physicallyCorrectLights;
renderer.toneMapping = THREE[toneMappingName + 'ToneMapping'];
THREE.Texture.DEFAULT_ANISOTROPY = data.anisotropy;
Expand All @@ -47,6 +47,10 @@ module.exports.System = registerSystem('renderer', {
if (sceneEl.hasAttribute('logarithmicDepthBuffer')) {
warn('Component `logarithmicDepthBuffer` is deprecated. Use `renderer="logarithmicDepthBuffer: true"` instead.');
}

// These properties are always the same, regardless of rendered oonfiguration
renderer.sortObjects = true;
renderer.setOpaqueSort(sortFrontToBack);
},

update: function () {
Expand All @@ -57,6 +61,15 @@ module.exports.System = registerSystem('renderer', {
renderer.toneMapping = THREE[toneMappingName + 'ToneMapping'];
renderer.toneMappingExposure = data.exposure;
renderer.xr.setFoveation(data.foveationLevel);

if (data.sortObjects) {
warn('`sortObjects` property is deprecated. Use `renderer="sortTransparentObjects: true"` instead.');
}
if (data.sortTransparentObjects) {
renderer.setTransparentSort(sortBackToFront);
} else {
renderer.setTransparentSort(sortRenderOrderOnly);
}
},

applyColorCorrection: function (texture) {
Expand All @@ -83,3 +96,52 @@ module.exports.System = registerSystem('renderer', {
}
}
});

// Custom A-Frame sort functions.
// Variations of Three.js default sort orders here:
// https://github.com/mrdoob/three.js/blob/ebbaecf9acacf259ea9abdcba7b6fb25cfcea2ab/src/renderers/webgl/WebGLRenderLists.js#L1
// See: https://github.com/aframevr/aframe/issues/5332

// Default sort for opaque objects:
// - respect groupOrder & renderOrder settings
// - sort front-to-back by z-depth from camera (this should minimize overdraw)
// - otherwise leave objects in default order (object tree order)

function sortFrontToBack (a, b) {
if (a.groupOrder !== b.groupOrder) {
return a.groupOrder - b.groupOrder;
}

Check failure on line 113 in src/systems/renderer.js

View workflow job for this annotation

GitHub Actions / Test Cases (16.x, latest)

Trailing spaces not allowed
if (a.renderOrder !== b.renderOrder) {
return a.renderOrder - b.renderOrder;
}

Check failure on line 116 in src/systems/renderer.js

View workflow job for this annotation

GitHub Actions / Test Cases (16.x, latest)

Trailing spaces not allowed
return a.z - b.z;
}

// Default sort for transparent objects:
// - respect groupOrder & renderOrder settings
// - otherwise leave objects in default order (object tree order)
function sortRenderOrderOnly (a, b) {
if (a.groupOrder !== b.groupOrder) {

Check failure on line 124 in src/systems/renderer.js

View workflow job for this annotation

GitHub Actions / Test Cases (16.x, latest)

Trailing spaces not allowed
return a.groupOrder - b.groupOrder;
}

Check failure on line 126 in src/systems/renderer.js

View workflow job for this annotation

GitHub Actions / Test Cases (16.x, latest)

Trailing spaces not allowed
return a.renderOrder - b.renderOrder;
}

// Spatial sort for transparent objects:
// - respect groupOrder & renderOrder settings
// - sort back-to-front by z-depth from camera
// - otherwise leave objects in default order (object tree order)
function sortBackToFront (a, b) {
if (a.groupOrder !== b.groupOrder) {
return a.groupOrder - b.groupOrder;
}
if (a.renderOrder !== b.renderOrder) {
return a.renderOrder - b.renderOrder;
}

Check failure on line 140 in src/systems/renderer.js

View workflow job for this annotation

GitHub Actions / Test Cases (16.x, latest)

Trailing spaces not allowed
return b.z - a.z;
}

// exports needed for Unit Tests
module.exports.sortFrontToBack = sortFrontToBack;
module.exports.sortRenderOrderOnly = sortRenderOrderOnly;
module.exports.sortBackToFront = sortBackToFront;
6 changes: 4 additions & 2 deletions tests/__init.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,12 @@ setup(function () {
},
dispose: function () {},
getContext: function () { return undefined; },
render: function () {},
setAnimationLoop: function () {},
setSize: function () {},
setOpaqueSort: function () {},
setPixelRatio: function () {},
render: function () {},
setSize: function () {},
setTransparentSort: function () {},
shadowMap: {enabled: false}
};
});
Expand Down
109 changes: 104 additions & 5 deletions tests/systems/renderer.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
/* global assert, suite, test, setup, teardown, THREE */
var {sortFrontToBack,
sortRenderOrderOnly,
sortBackToFront} = require('systems/renderer');

suite('renderer', function () {
function createScene () {
Expand All @@ -9,22 +12,26 @@ suite('renderer', function () {

test('default initialization', function (done) {
var sceneEl = createScene();
var sortFunction;
sceneEl.renderer.setOpaqueSort = function (s) { sortFunction = s; };
sceneEl.addEventListener('loaded', function () {
// Verify the properties which are part of the renderer system schema.
var rendererSystem = sceneEl.getAttribute('renderer');
assert.strictEqual(rendererSystem.foveationLevel, 1);
assert.strictEqual(rendererSystem.highRefreshRate, false);
assert.strictEqual(rendererSystem.physicallyCorrectLights, false);
assert.strictEqual(rendererSystem.sortObjects, false);
assert.strictEqual(rendererSystem.sortTransparentObjects, false);
assert.strictEqual(rendererSystem.colorManagement, true);
assert.strictEqual(rendererSystem.anisotropy, 1);

// Verify properties that are transferred from the renderer system to the rendering engine.
var renderingEngine = sceneEl.renderer;
assert.strictEqual(renderingEngine.outputColorSpace, THREE.SRGBColorSpace);
assert.notOk(renderingEngine.sortObjects);
assert.ok(renderingEngine.sortObjects);
assert.strictEqual(sortFunction, sortFrontToBack);
assert.strictEqual(renderingEngine.useLegacyLights, true);
assert.strictEqual(THREE.Texture.DEFAULT_ANISOTROPY, 1);

done();
});
document.body.appendChild(sceneEl);
Expand All @@ -41,11 +48,26 @@ suite('renderer', function () {
document.body.appendChild(sceneEl);
});

test('change renderer sortObjects', function (done) {
test('change renderer sortTransparentObjects', function (done) {
var sceneEl = createScene();

var sortFunction;
sceneEl.renderer.setTransparentSort = function (s) { sortFunction = s; };
sceneEl.setAttribute('renderer', 'sortTransparentObjects: true;');
sceneEl.addEventListener('loaded', function () {
assert.strictEqual(sortFunction, sortBackToFront);
done();
});
document.body.appendChild(sceneEl);
});

test('default renderer sortTransparentObjects', function (done) {
var sceneEl = createScene();
sceneEl.setAttribute('renderer', 'sortObjects: true;');

var sortFunction;
sceneEl.renderer.setTransparentSort = function (s) { sortFunction = s; };
sceneEl.addEventListener('loaded', function () {
assert.ok(sceneEl.renderer.sortObjects);
assert.strictEqual(sortFunction, sortRenderOrderOnly);
done();
});
document.body.appendChild(sceneEl);
Expand Down Expand Up @@ -208,4 +230,81 @@ suite('renderer', function () {
console.warn = oldConsoleWarn;
});
});

suite('sortFunctions', function () {
var objects;
var objectsRenderOrder;
var objectsGroupOrder;

setup(function () {
objects = [
{ name: 'a', renderOrder: 0, z: 1 },
{ name: 'b', renderOrder: 0, z: 3 },
{ name: 'c', renderOrder: 0, z: 2 }
];

objectsRenderOrder = [
{ name: 'a', renderOrder: 1, z: 1 },
{ name: 'b', renderOrder: 0, z: 3 },
{ name: 'c', renderOrder: -1, z: 2 }
];

objectsGroupOrder = [
{ name: 'a', groupOrder: 0, renderOrder: 1, z: 1 },
{ name: 'b', groupOrder: 0, renderOrder: 0, z: 3 },
{ name: 'c', groupOrder: 1, renderOrder: -1, z: 2 }
];
});

function checkOrder (objects, array) {
array.forEach((item, index) => {
assert.equal(objects[index].name, item);
});
}

test('Opaque sort sorts front-to-back', function () {
objects.sort(sortFrontToBack);
checkOrder(objects, ['a', 'c', 'b']);
});

test('Opaque sort respects renderOrder', function () {
objectsRenderOrder.sort(sortFrontToBack);
checkOrder(objectsRenderOrder, ['c', 'b', 'a']);
});

test('Opaque sort respects groupOrder, then renderOrder', function () {
objectsGroupOrder.sort(sortFrontToBack);
checkOrder(objectsGroupOrder, ['b', 'a', 'c']);
});

test('Transparent default sort sorts in DOM order', function () {
objects.sort(sortRenderOrderOnly);
checkOrder(objects, ['a', 'b', 'c']);
});

test('Transparent default sort respects renderOrder', function () {
objectsRenderOrder.sort(sortRenderOrderOnly);
checkOrder(objectsRenderOrder, ['c', 'b', 'a']);
});

test('Transparent default sort respects groupOrder, then renderOrder', function () {
objectsGroupOrder.sort(sortRenderOrderOnly);
checkOrder(objectsGroupOrder, ['b', 'a', 'c']);
});

test('Transparent spatial sort sorts back-to-front', function () {
objects.sort(sortBackToFront);
checkOrder(objects, ['b', 'c', 'a']);
});

test('Transparent spatial sort respects renderOrder', function () {
objectsRenderOrder.sort(sortBackToFront);
checkOrder(objectsRenderOrder, ['c', 'b', 'a']);
});

test('Transparent spatial sort respects groupOrder, then renderOrder', function () {
objectsGroupOrder.sort(sortBackToFront);
checkOrder(objectsGroupOrder, ['b', 'a', 'c']);
});
});
});

0 comments on commit 3c6cc78

Please sign in to comment.