-
-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve sort order #5341
Improve sort order #5341
Conversation
docs/components/renderer.md
Outdated
By default, 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. | ||
In may cases, the order in which objects is rendered doesn't matter much, but there are a few cases where sorting is important: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is documentation of general A-Frame object sorting behaviour, rather than this particular property.
Not sure if there's a better place for this in the docs, but I couldn't see an obvious one, and didn't want to restructure docs too much.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small typo: may -> many
The text seems a bit out of place compared to the rest as it's considerably more in-depth and technical. While I personally don't mind it too much, it's usefulness to new(er) users is probably limited as a result. Surprisingly the original text still mostly holds (as it already pertained to transparent objects). Touching that up and linking to a different page or resource describing it more in-depth (preferably with concrete pros and cons) would IMHO be ideal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What you suggest is the approach I wanted to take, but I couldn't find a good place in the docs to slip in a section on render order. It's too detailed for me to want to put it into the core docs sections that work as a guide for newcomers.
Have looked again and I think the FAQ https://aframe.io/docs/1.4.0/introduction/faq.html is somewhere that could probably accommodate something.
Probably a question "What order does A-Frame render objects in?" though it could also be "Why are my transparent objects blocking other objects?".
Sound OK?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds okay to me, I would go for the "Why are my transparent objects blocking other objects?" or something along those lines. It's closer to the question people likely have when facing the issue, especially if they don't know it has to do with sorting. (And those that do know it has to do with sorting will likely find the sortTransparentObjects
property first which links to the relevant FAQ question).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've left it as "what order..." - there are other ordering issues that are nothing to do with transparency (e.g. the hider material issues that got me into looking at this in the 1st place, and a bunch of the content has nothing to do with transparency).
src/core/scene/sortFunctions.js
Outdated
@@ -0,0 +1,56 @@ | |||
// Custom A-Frame sort functions. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think putting these into a new module is right. Not sure whether this is the right place in the directory structure, or if there's a more suitable place.
I didn't think these were utils
as they aren't part of the exposed A-Frame API. Couldn't see another obvious directory to put them in, so put them here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would put them in the renderer file (define at the bottom) where they are used. see setupCanvas
function in a-scene https://github.com/aframevr/aframe/blob/master/src/core/scene/a-scene.js#L863 No need to prepend aframe
to the function name
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can think of modularizing later if the functions turn to be useful somewhere else (like those in utils)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure about this? They are used in both a-scene, and renderer. Defining them in renderer means that a core module (a-scene) will have a dependency on a systems module (renderer).
That seems like the wrong direction for a dependency to me.
It might be preferable to define them in a-scene?
Agree with removing aframe
from the function names, and have made that change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry missed they're used in a-scene as well. Do we need them in both places? Can we consolidate in just one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see the functions used in a-scene. I might be missing something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, my mistake. They were used in a-scene until I did some refactoring proposed by @mrxz . I'll fix as you suggest now.
var sceneEl = createScene(); | ||
sceneEl.setAttribute('renderer', 'sortObjects: true;'); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fact that Unit Tests mock out the setupRenderer
function means that I was limited in how much of the code changes I could Unit Test in a straightforward way.
Should we be looking for a way to improve UT coverage of the setupRenderer
function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems that the configuration of the renderer is a bit split between both setupRenderer
and the init
function of the renderer system. It might be easier to move the setting of sortObjects
and sorting functions into the init
function as well. That way the tests here can:
- Verify that the default values are set properly (currently the test
'default initialization'
asserts that sortObjects defaults to false and doesn't fail) - Verify that changing the
sortTransparentObjects
result in the correct sorting functions being set
This unit test now also tests that the sorting behaviour is correct, but I would split that off into a new sortFunctions.test.js
that specifically tests them and let this unit test here cover only the responsibility of the renderer system of setting the right sort functions.
Finding a way to improve test coverage of setupRenderer
would still be nice, but I think with the above the need for it in the scope of this change is mostly alleviated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pushed some changes to address these comments...
It might be easier to move the setting of sortObjects and sorting functions into the init function as well.
The renderer init
function will only be called if renderer
system is initialized. It does appear that's always the case today (even if the renderer
property is not explicitly specified on the scene), but I'm not sure it's good to bake that assumption into core code. So I'd argue these properties should be set by the AScene
class, and setupRenderer()
seems like the right place to do it.
With the updated code, renderer.sortObjects
is always true
, and the renderer
component has no bearing on it at all, so I'd argue it is cleaner to not have the renderer
code touch renderer.sortObjects
.
Sorting functions are set in the renderer update
function, as well as in setupRenderer()
. No reason to move them to init
, which just restricts them from being changed later.
currently the test 'default initialization' asserts that sortObjects defaults to false and doesn't fail
Removed that check from the test. renderer.sortObjects
no longer has anything to do with the renderer
component, so that check doesn't belong here any more.
This unit test now also tests that the sorting behaviour is correct, but I would split that off into a new sortFunctions.test.js that specifically tests them and let this unit test here cover only the responsibility of the renderer system of setting the right sort functions.
That's fair. Since the since the sort functions are internal to A-frame, I hadn't twigged that I can just import them, and then compare the functions directly. Have now done that and it works fine.
A full UTs of the sort functions is also a sensible idea - I have added.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The renderer init function will only be called if renderer system is initialized. It does appear that's the case today, but I'm not sure it's good to bake that assumption into core code. So I'd argue these properties should be set by the AScene class, and setupRenderer() seems like the right place to do it.
I kind of see it the other way around. It's the AScene class that is effectively overstepping its boundaries by depending on both the camera
and renderer
systems. Conceptually in an ECS if the renderer system would be left out, you'd be left with a headless instance. From a pragmatic and legacy perspective I can see how this situation came to be and it's now unfeasible to every change (lots of code depends on the scene holding the renderer and camera).
That does leave us in the situation where renderer initialization is effectively split in two parts AScene.setupRenderer
and renderer.init
. The logical split I see at the moment is that setupRenderer
only sets the properties needed at WebGLRenderer
construction time and renderer.init
sets anything that can change 'on the fly'. The object sorting is the only odd one out (even before this PR).
That being said, the actual sortObjects
property can be left default (= true) so neither needs to assign anything to this. Unless we explicitly want to assert this default (and potentially shield us against three.js ever changing that default). That only leaves the sorting functions which, given the current state of the code, feel more at home in init
to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure I completely agree, as most of the renderer
settings can't be changed on the fly (according to the docs, at least).
https://aframe.io/docs/1.4.0/components/renderer.html
But happy this is the right direction of travel, so I made the changes you suggested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, you're right, seems I overlooked the few that are set by renderer but not changeable on the fly. The docs and the schema do paint a clear picture that all these settings do belong to the renderer system, yet a good chunk of them are handled (parsed even) by the AScene
, which in an ideal setup wouldn't have been the case, but alas.
Anyway, the PR looks good. Just two more ES6 destructuring assignments in renderer.test.js and sortFunctions.test.js and then it's all good to go IMO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My understanding was that the ES5 restriction only applies to /src.
Plenty of existing code under /test uses let, const, arrow functions etc.
See e.g. animation.test.js
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, then everything is good to go IMO :-)
@@ -46,10 +46,12 @@ setup(function () { | |||
}, | |||
dispose: function () {}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reordered this list to be alphabetic
@mrxz What are your thoughts? Thanks! |
src/systems/renderer.js
Outdated
@@ -1,6 +1,7 @@ | |||
var registerSystem = require('../core/system').registerSystem; | |||
var utils = require('../utils/'); | |||
var THREE = require('../lib/three'); | |||
var {aframeSortTransparentDefault, aframeSortTransparentSpatial} = require('../core/scene/sortFunctions'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe destructuring is an ES6 feature.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pushed a fix for this (is it ok to require
the same module twice?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling require
more than once on the same module gives the same result, but I think it's a tad cleaner to assign it to an intermediate, i.e. var sortFunctions = require('../core/scene/sortFunctions')
src/core/scene/sortFunctions.js
Outdated
module.exports = { | ||
aframeSortOpaqueDefault, | ||
aframeSortTransparentDefault, | ||
aframeSortTransparentSpatial |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These shorthands are also an ES6 features, I'm afraid...
(Why are we torturing ourselves with this ES5-only policy, again? 😓 )
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll fix.
FYI, I have been working on a proposal to add linting rules to enforce ES5:
#5218
Blocked on the fact that A-Frame now uses class
in several places, which is an ES6 feature.
Looking good, tested it on a couple of scenes and works as expected. Left a couple of comments. |
…property description into FAQ
src/core/scene/sortFunctions.js
Outdated
// - sort front-to-back by z-depth from camera (this should minimize overdraw) | ||
// - otherwise leave objects in default order (object tree order) | ||
|
||
function sortOpaqueDefault (a, b) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Function names are a bit cryptic. sortOpaqueObjectsFirst
maybe? Default has not meaning without more context
src/systems/renderer.js
Outdated
} | ||
|
||
// exports needed for Unit Tests | ||
module.exports.sortOpaqueDefault = sortOpaqueDefault; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I appreciate the tests. I'll merge but in general I lean towards just unit testing public APIs. Internal functions are implementation details and tests add friction to modify, refactor, improve the code.
src/systems/renderer.js
Outdated
// - sort front-to-back by z-depth from camera (this should minimize overdraw) | ||
// - otherwise leave objects in default order (object tree order) | ||
|
||
function sortOpaqueDefault (a, b) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we find a bit more descriptive names for these functions? I had suggested sortOpaqueObjectsFirst
but not sure if good enough. can suggest other names.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we can make this function names a bit more descriptive this is almost ready to merge. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for slow response on this.
I just pushed a change that updated:
sortOpaqueDefault
-> sortSpatialFrontToBack
sortTransparentDefault
-> sortRenderOrderOnly
sortTransparentSpatial
-> sortSpatialBackToFront
This describes what the functions implement rather than what they are used for (glossing over the detail that the spatial sorts also sort by renderOrder
first, in the interest of keeping the function names at a sensible length).
src/systems/renderer.js
Outdated
// - sort front-to-back by z-depth from camera (this should minimize overdraw) | ||
// - otherwise leave objects in default order (object tree order) | ||
|
||
function sortSpatialFrontToBack (a, b) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does spatial means in this context? Maybe redundant? sortFrontToBack
maybe simpler?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agree FrontToBack implies Spatial, so it's redundant. I'll fix.
src/systems/renderer.js
Outdated
} else if (a.z !== b.z) { | ||
return a.z - b.z; | ||
} else { | ||
return 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this statement might be redundant. If one does a.z - b.z
if they are equal they will return 0 as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
similarly:
return (a.groupOrder - b.groupOrder) || (a.renderOrder - b.renderOrder) || (a.z - b.z);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't it make sense to retain the ID tie-breaker (return a.id - b.id
) used by Three.js as well? That should ensure that, everything else being equal, the order is consistent.
As for compacting it to a single expression, I personally prefer the current more verbose version. It's easier to see what's going on and it resembles the referenced original Three.js code, making it easy to compare. I did experiment with some very crude benchmarking and while both perform about the same, a significant difference forms once some of the properties are (mostly) consistent. It seems the if-else chain allows for better branch-prediction:
- 0 groupOrder, 1% renderOrder, random Z: If-else chain faster on Chrome/Quest 2, slower on Firefox
- random groupOrder, random renderOrder, random z: If-else chain slightly slower on Chrome and Firefox
- 1 groupOrder, 0 renderOrder, random z If-else completely dominates the compacted expression, both on Chrome and Firefox
- 0 groupOrder, 0 renderOrder, random z: About equal, if-else slightly faster on Chrome, but slightly slower on Firefox
Result obviously may vary and I don't think the performance of this sorting function would really matter at the end of the day as most VR experiences tend to have relatively low object counts anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't it make sense to retain the ID tie-breaker (return a.id - b.id) used by Three.js as well? That should ensure that, everything else being equal, the order is consistent.
That would sort the objects in creation order, rather than DOM order. My understanding is that returning 0 will leave order as-is, which implies DOM order, which matches current behaviour, is explicable in A-Frame terms, and gives the user a level of control by inserting items earlier in the DOM.
IMO that's superior to object id / creation order.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As for compacting it to a single expression, I personally prefer the current more verbose version.
Thanks for doing the perf testing, looks to me like the more verbose version is both easier to read, and probably also more performant (I don't think random data is particularly realistic, and even in that case, if-else seems to be only marginally slower).
So I think that weighs in favour of the code as-is. Which also matches the equivalent THREE.js code.
There may be a case for eliminating the final if test and simply returning a.z - b.z
though, so I'll make that change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would sort the objects in creation order, rather than DOM order. My understanding is that returning 0 will leave order as-is, which implies DOM order, which matches current behaviour, is explicable in A-Frame terms, and gives the user a level of control by inserting items earlier in the DOM.
I meant only for the cases where we do sort on Z first, where it would act as a tie-breaker. Returning 0 will indeed leave the order in tact (stable sort), though strictly speaking that is the insertion order in the render list, which, arguable, only happens to match the tree-order in Three.js' current implementation. But I'm fine with leaving it out, it's unlikely to cause any issues and if the behaviour in three ever changes, we can simply follow suit.
src/systems/renderer.js
Outdated
} else if (a.renderOrder !== b.renderOrder) { | ||
return a.renderOrder - b.renderOrder; | ||
} else { | ||
return 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you could do return (a.groupOrder - b.groupOrder) || (a.renderOrder !== b.renderOrder)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per above, I believe this change reduces readability, diverges unnecessarily from equivalent THREE.js code, and doesn't improve (and may even harm) performance. So I'd prefer to leave as-is.
src/systems/renderer.js
Outdated
} else if (a.z !== b.z) { | ||
return b.z - a.z; | ||
} else { | ||
return 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
similarly:
return (a.groupOrder - b.groupOrder) || (a.renderOrder - b.renderOrder) || (b.z - a.z);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per above, I believe this change reduces readability, diverges unnecessarily from equivalent THREE.js code, and doesn't improve (and may even harm) performance. So I'd prefer to leave as-is.
Thanks so much for the patience! This is awesome |
Description:
A PR to implement the changes proposed in #5332
Changes proposed:
renderer
sortObjects
property withsortTransparentObjects
property that sets a different A-Frame sort function for transparent objects