Skip to content
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

enable non-recursive raycaster (fixes #2329) #2331

Merged
merged 4 commits into from May 16, 2017

Conversation

machenmusik
Copy link
Contributor

fixes #2329

@dmarcos
Copy link
Member

dmarcos commented Jan 30, 2017

We need to fix the tests

@machenmusik
Copy link
Contributor Author

machenmusik commented Jan 30, 2017

this is turning out to be more insidious than expected... still working on it

i can see now why the implementation did what it did, in that once the change makes entities require geometry in order to intersect, a slew of the tests break because they never actually cared, and doing it correctly is not as simple as one would expect.

(frustrating, because functionally things work correctly...)

@machenmusik
Copy link
Contributor Author

looks like there is an issue with refreshObjects() happening before geometry is attached (and thus there are no children of the entity's object3D to add yet). if there is some better way to understand when refreshObjects() can be invoked correctly, that would be most welcome...

@machenmusik
Copy link
Contributor Author

ok now I am a little baffled, as npm run test:chrome works locally with all non-skipped tests passing, but apparently something in travis-ci changes, and things fail.

@ngokevin
Copy link
Member

Probably just some race condition or timing issue.

@codecov-io
Copy link

codecov-io commented Jan 31, 2017

Codecov Report

Merging #2331 into master will increase coverage by 0.03%.

@@            Coverage Diff            @@
##           master   #2331      +/-   ##
=========================================
+ Coverage   85.97%     86%   +0.03%     
=========================================
  Files         109     109              
  Lines        4519    4538      +19     
=========================================
+ Hits         3885    3903      +18     
- Misses        634     635       +1
Impacted Files Coverage Δ
src/index.js 97.72% <100%> (ø)
src/components/raycaster.js 98.79% <96.29%> (-1.21%)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 652f23f...d9c5ed2. Read the comment docs.

Copy link

@usernamealreadyis usernamealreadyis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

T< TIME> [FATAL] AppErrorHandler: Error #1132: Invalid JSON parse input.
SyntaxError: Error #1132: Invalid JSON parse input.
at Error$/throwError()
at JSON$/parse()
at com.vmware.vsphere.client.model.vm.config.devices.connection::DeviceControllerServerConnection$/getConnectedCdroms()
at com.vmware.vsphere.client.views.vm.config.devices.connection::DeviceSummaryEntry/onMenuButtonClick()
Logger Stack Trace:
at com.vmware.vsphere.client.util::AppErrorHandler/onUncaughtError()

@machenmusik
Copy link
Contributor Author

machenmusik commented Jan 31, 2017

latest PR incorporates this...

ngokevin [8:00 PM] 
i suggest in the raycaster component, we do a refresh on:
1. `sceneEl.addEventListener('loaded')`
2. `sceneEl.addEventListener('child-attached')` + `loaded` on that attached child

@ngokevin ngokevin added this to the 0.5.0 milestone Feb 1, 2017
@ngokevin ngokevin removed this from the 0.5.0 milestone Feb 7, 2017
@machenmusik machenmusik force-pushed the fix-recursive-raycaster branch 2 times, most recently from 5d719c1 to 7aac032 Compare February 7, 2017 23:30
Copy link

@usernamealreadyis usernamealreadyis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?/?

@maissani
Copy link

Any update about this fix ?

@machenmusik
Copy link
Contributor Author

all the tests work, the latest version of PR uses strategy suggested,
maybe it got a bit buried in the push to 0.5.0...

I believe the raycaster interval fix was merged into 0.5.0 separately,
but the remainder is sitting ready and waiting

@machenmusik
Copy link
Contributor Author

@dmarcos @ngokevin now that 0.5.0 rush is over, are we ok to take this?

@ngokevin
Copy link
Member

What did we think about just removing the recursive property altogether and being recursive by default? I remember something about hard to figure out what element exactly was intersected in a big tree?

@machenmusik
Copy link
Contributor Author

it can be significantly more expensive to do recursive raycast where non-recursive suffices as well, but perhaps @maissani and others can comment

@ngokevin ngokevin added this to the 0.6.0 milestone Mar 30, 2017
@cvan
Copy link
Contributor

cvan commented Apr 18, 2017

@machenmusik did you still want to get this merged?

@machenmusik
Copy link
Contributor Author

if it still works, sure... @maissani @donmccurdy is this PR still useful to you?

@donmccurdy
Copy link
Member

donmccurdy commented Apr 19, 2017

I've forgotten the context here a bit, but as far as I remember: With raycaster="recursive:false, the raycaster tests against each entity's root Object3D, and will never detect anything. That's no good; we should either fix that or remove the option.

  1. If we can simplify this PR a bit, great. I'll add proposed changes, if we choose to move forward, in separate comments.
  2. Removing the recursive option would be OK, too. For users who understand the performance profile and scene graph enough to care, setting up a custom component with a THREE.Raycaster is not that difficult. If they're doing that kind of performance tuning, they should probably consider taking the querySelector out of the picture anyway.

el.removeEventListener('loaded', nowRefresh);
self.refreshObjects();
});
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is waiting for the current element, presumably the cursor, to load, right? Seems like a flaky indicator for the state of the rest of the scene. Plausible alternatives:

  • Refresh on first tick.
  • Refresh on scene load.
  • Refresh every time the scene has a loaded event, to pick up new entities.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wow this was from a long time ago... refreshing my memory as well...
I think this was supposed to be the last one, but listener should be on sceneEl not el

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking at it a little more closely now, shouldn't this happen when children are attached somewhere in the scene?
this.el.sceneEl.addEventListener('child-attached', this.refreshOnceChildLoaded);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I originally had some changes that would defer this work until next actual raycast (to coalesce / avoid spurious invocations) - but that seems to have gone, should we bring it back?

return;
// If objects not defined, intersect with everything.
if (objectsAreEls) {
objects = this.el.sceneEl.querySelectorAll(data.objects);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO this code would be more readable if, by the end of this first if/else block, the objects array is guaranteed to contain only Object3Ds. And, we can avoid calling objects.length on a NodeList in a loop; that's O(n²). Instead:

objects = this.el.sceneEl.querySelectorAll(data.objects);
for (var i = 0, len = objects.length; i < len; i++) {
  this.objects.push(objectEls[i].object3D);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

had been assuming JIT would hoist the length call, but you're right that it can be explicit.

// If there aren't any children, then until a refresh after geometry loads,
// raycast won't see this object... but that should happen automatically.
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the change above, this block reduces to:

for (i = 0; i < objects.length; i++) {
  var children = objects[i].children;
  if (children) {
    this.objects.push.apply(this.objects, children);
  }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(presumably this wants the explicit length hoisting described above)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually not here, on the explicit length hoisting. With normal arrays, doing i = 0; i < array.length; i++ is completely fine, clean, and fast.

But if you have a NodeList instead of an array (the result of querySelectorAll() is a NodeList, not an array) then calling .length is slower, as described here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heh, ok it's more complicated than I thought, there are "live" and "static" NodeLists, and the result of querySelectorAll() is the latter, which means it can probably be hoisted anyway. Live NodeLists are more trouble. Feel free to ignore my concern and I'll just patch it if I actually run into a noticeable performance issue. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, that's how I thought it worked, but I already proactively patched since code is still quite readable that way

since there appears to be a wrapper around an entity's object3D, use its children instead
… box

add geometry so that raycaster actually adds something

wait until next tick to satisfy Firefox; remove extra tick
@machenmusik
Copy link
Contributor Author

(also rebased against something more recent)

@machenmusik
Copy link
Contributor Author

@donmccurdy @dmarcos @ngokevin anything else on this one?

@ngokevin ngokevin changed the title Fix recursive raycaster enable non-recursive raycaster (fixes #2329) Apr 21, 2017
*/
refreshOnceChildLoaded: function (evt) {
var self = this;
var el = evt.detail.el;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd use childEl to not confuse with the raycaster.

var self = this;
var el = evt.detail.el;
if (!el) { return; }
if (el.hasLoaded) { this.refreshObjects(); } else {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's put else blocks on their own level.

if (clause) {
  // ...
} else {
  // ...

}

var objectEls;
var len;
var objects;
var objectsAreEls = data.objects ? this.el.sceneEl.querySelectorAll(data.objects) : null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the logic change here? Previously, we had:

if (data.objects) {
  // Run query selector.
} else {
  // Intersect with everything.
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see @donmccurdy recommendations above -- basically, this.objects should have only object3D when done, so the first part is exactly that, and the second part grabs the child object3D values because the top-level group one is useless.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: objectsAreEls sounds like a boolean to me. Maybe call it objectEls?

for (i = 0; i < objectEls.length; i++) {
this.objects.push(objectEls[i].object3D);
if (objectsAreEls) {
for (objects = [], i = 0, len = objectsAreEls.length; i < len; i++) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The objects = [] assignment in the loop is a bit hard to follow. We also don't need len variable for a for loop for (i = 0; i < objectEls.length; i++).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

len came from @donmccurdy request - recanted after already done - be easier if y'all would agree with each other first ;-)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, sounds fine. We can move the assignment out though

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha yeah sorry. My worry about objectsAreEls.length performance turned out to be unnecessary. This loop can just use normal syntax. :)

}

// If objects not defined, intersect with everything.
this.objects = this.el.sceneEl.object3D.children;
for (this.objects = [], i = 0, len = objects.length; i < len; i++) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this only necessary for non-recursive raycasting?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see @donmccurdy recommendations above -- basically, this.objects should have only object3D when done, so we always do this second step now

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, my preference would be to give objects a consistent type Array<Object3D> as early as possible, rather than checking type in later loops.

And yes — this entire loop is only necessary for non-recursive raycasting. Maybe saves the raycaster some very trivial amount of work, too.

sceneEl.appendChild(newEl);

function eventuallyDoAssert () {
if (newEl.hasLoaded) { doAssert(); } else { newEl.addEventListener('loaded', doAssert); }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here as well:

if (clause) { 

} else {

}

targetEl.setAttribute('position', '0 0 -1');
targetEl.addEventListener('loaded', function finishSetup () {
el.sceneEl.appendChild(targetEl);
// `npm run test:forefox` needs the timeout for the tests to succeed.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not worth mentioning, appendChild is asynchronous so it's a good idea to have always.

</a-scene>

<script>
var el = document.createElement('a-entity');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Within an ad-hoc component would be cool to contain the logic

el.setAttribute('position', '0 0 -2');
document.querySelector('a-scene').appendChild(el);

document.querySelector('a-entity[geometry]').addEventListener('raycaster-intersected', function (evt) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

query selector needed? this is el, right?

document.querySelector('a-scene').appendChild(el);

document.querySelector('a-entity[geometry]').addEventListener('raycaster-intersected', function (evt) {
// getting two intersection events per tick; same element, different faces...
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this comment mean? A bug, or what we expect?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the behavior is permitted.

@donmccurdy
Copy link
Member

If we choose to keep the recursive:false option around, then I'm happy with this PR after the remaining comments.

But I don't have a strong opinion about whether that option should exist, one way or another.

@machenmusik
Copy link
Contributor Author

@dmarcos @ngokevin - @donmccurdy and I think this one is done, did we want to merge or close?

@ngokevin ngokevin merged commit 59b3cf0 into aframevr:master May 16, 2017
@ngokevin
Copy link
Member

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

feat(raycaster): Raycaster doesn't trigger objects when recursive is set to false (refs: v0.4.0)
8 participants