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

Configurable sortOrder for raycast component #4966

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
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
1,554 changes: 283 additions & 1,271 deletions dist/aframe-master.js

Large diffs are not rendered by default.

84 changes: 20 additions & 64 deletions dist/aframe-master.js.map

Large diffs are not rendered by default.

1,217 changes: 819 additions & 398 deletions dist/aframe-master.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/aframe-master.min.js.map

Large diffs are not rendered by default.

37 changes: 33 additions & 4 deletions docs/components/raycaster.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ AFRAME.registerComponent('collider-check', {
| objects | Query selector to pick which objects to test for intersection. If not specified, all entities will be tested. Note that only objects attached via `.setObject3D` and their recursive children will be tested. | null |
| origin | Vector3 coordinate of where the ray should originate from relative to the entity's origin. | 0, 0, 0 |
| showLine | Whether or not to display the raycaster visually with the [line component][line]. | false |
| sortOrder | When the raycaster matches multiple objects, this allows control of the order in which the matches are returned. Particularly useful for use with components like cursor and laser-controls, which build on raycaster and always act on the first object matched.<br />If no sortOrder is specified, the raycaster returns the objects in distance order, closest first.<br /><br />To sort the objects differently, specify an attribute on the objects whose value should be used as a sort order. Objects will be sorted largest to smallest, with objects without an attribute treated as having value 0. | null |
| useWorldCoordinates | Whether the raycaster origin and direction properties are specified in world coordinates. | false |

## Events
Expand Down Expand Up @@ -103,11 +104,11 @@ The event detail contains intersection objects. They are returned straight from

## Members

| Member | Description |
|----------------|------------------------------------------------------------------------------------------------------------------|
| intersectedEls | Entities currently intersecting the raycaster. |
| Member | Description |
| -------------- | ------------------------------------------------------------ |
| intersectedEls | Entities currently intersecting the raycaster. Ordered by distance from the ray origin, unless a different sortOrder was specified. |
| objects | three.js objects to test for intersections. Will be `scene.children` if `objects` property is not specified. |
| raycaster | three.js raycaster object. |
| raycaster | three.js raycaster object. |

## Methods

Expand Down Expand Up @@ -217,3 +218,31 @@ intersecting any entity. By default, the `far` property defaults to 1000 meters
meaning the line drawn will be 1000 meters long. When the raycaster intersects
an object, the line will get truncated to the intersection point so it doesn't
shoot straight through.

## Controlling the Order

In most applications, it makes sense for the raycaster to match the closest object to the ray origin. However there are cases where you may want a different behavior.

- If a raycaster is used to grab and manipulate objects, you may want the line to be rendered all the way to the grabbed object, even when that object passes behind another raycastable object.
- If objects are rendered "always in front" (e.g. using depthTest=false on the material), you may want the raycaster to prioritize this item, so that the raycasting experience matches the user's visuals.

Raycaster does provide access to all the matched points so it is possible to re-order within your application. However various A-Frame components that build on top of raycaster, like cursor, laser-controls and line assume that they should act on the first raycast object returned. So when using these components, it can be convenient to have the raycaster sort the matched objects on some different criterion.

To implement this:

- Define a custom attribute on all raycastable objects, and give each object a value indicating its priority.
- Specify this attribute in the sortOrder property on the raycaster.

For example:

```
<a-entity raycaster="objects: [raycastable];sortOrder: priority" cursor></a-entity>
<a-entity id="box1" raycastable priority="10" geometry="primitive: box" position="1 0 0"></a-entity>
<a-entity id="box2" raycastable geometry="primitive: box" position="2 0 0"></a-entity>
<a-entity id="box3" raycastable priority="-1" geometry="primitive: box" position="3 0 0"></a-entity>
```

When a raycast crosses all three objects, box1 will always be returned first, then box 2 (no priority specified), and finally box 3 (negative priority).

If two objects have the same sortOrder priority, they will be returned in the default order (closest first).

38 changes: 38 additions & 0 deletions examples/test/raycaster/sort-order.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Raycaster</title>
<meta name="description" content="Raycaster Sort Order- A-Frame">
<script src="../../../dist/aframe-master.js"></script>
</head>
<body>

<script>

AFRAME.registerComponent('clickable', {
init: function () {
this.el.addEventListener('click', function (evt) {
var el = evt.target;
alert(`clicked on ${el.id}`);
});
}
});

</script>
<p>Demonstration of non-standard sort Order for raycast results.</p>
<p>Red plane is furtherst awaty, but has highest priority for raycasting</p>
<p>Yellow plane is closest, but lowest priority for raycasting</p>
<p>Click on a shape, and the alert will tell you which one the raycaster picked</p>
<p>(we also skip depth testing, so that further away shapes are rendered always on-top)</p>

<a-scene>
<a-entity look-controls wasd-controls camera cursor="rayOrigin:mouse" raycaster="objects:[raycastable]; sortOrder:priority"></a-entity>
<a-plane id="yellow" material = "color: #ff0" position = "0 0 -2" raycastable priority="-1" clickable></a-plane>
<a-plane id="blue" material = "color: #00f; depthTest:false" position = "0 0 -3" raycastable clickable></a-plane>
<a-plane id="green" material = "color: #0f0; depthTest:false" position = "0 0 -4" raycastable priority="4" clickable></a-plane>
<a-plane id="red" material = "color: #f00; depthTest:false" position = "0 0 -5" raycastable priority="5" clickable></a-plane>
</a-scene>

</body>
</html>
10 changes: 6 additions & 4 deletions src/components/cursor.js
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,6 @@ module.exports.Component = registerComponent('cursor', {
* Handle intersection.
*/
onIntersection: function (evt) {
var currentIntersection;
var cursorEl = this.el;
var index;
var intersectedEl;
Expand All @@ -304,10 +303,13 @@ module.exports.Component = registerComponent('cursor', {
// Already intersecting this entity.
if (this.intersectedEl === intersectedEl) { return; }

// Ignore events further away than active intersection.
// Ignore events that are lower priority than active intersection.
// Note that the intersected event only include new intersections - there may be
// a higher priority existing intersection.
// See: https://github.com/aframevr/aframe/issues/4974
if (this.intersectedEl) {
currentIntersection = this.el.components.raycaster.getIntersection(this.intersectedEl);
if (currentIntersection && currentIntersection.distance <= intersection.distance) { return; }
const topIntersection = this.el.components.raycaster.intersectedEls[0];
if (intersectedEl !== topIntersection) { return; }
}

// Unset current intersection.
Expand Down
30 changes: 29 additions & 1 deletion src/components/raycaster.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ module.exports.Component = registerComponent('raycaster', {
showLine: {default: false},
lineColor: {default: 'white'},
lineOpacity: {default: 1},
useWorldCoordinates: {default: false}
useWorldCoordinates: {default: false},
sortOrder: {type: 'string', default: null}
},

multiple: true,
Expand Down Expand Up @@ -227,6 +228,33 @@ module.exports.Component = registerComponent('raycaster', {
rawIntersections.length = 0;
this.raycaster.intersectObjects(this.objects, true, rawIntersections);

// THREE raycaster returns elements closest to furthest.
// If a different sort order is required, sort the intersections based on the required order.
if (this.data.sortOrder) {
const orderAttribute = this.data.sortOrder;

rawIntersections.sort((a, b) => {
let priorityA = null;
let priorityB = null;
if (a.object.el) {
priorityA = a.object.el.getAttribute(orderAttribute);
}
if (b.object.el) {
priorityB = b.object.el.getAttribute(orderAttribute);
}

// note that we want to arrange the objects in *descending* priority
// order (highest priority first).
if (priorityA < priorityB) {
return 1;
}
if (priorityA > priorityB) {
return -1;
}
return 0;
});
}

// Only keep intersections against objects that have a reference to an entity.
intersections.length = 0;
intersectedEls.length = 0;
Expand Down
2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ require('./core/a-mixin');
require('./extras/components/');
require('./extras/primitives/');

console.log('A-Frame Version: 1.2.0 (Date 2021-10-18, Commit #b1b13381)');
console.log('A-Frame Version: 1.2.0 (Date 2021-11-17, Commit #944470f4)');
console.log('THREE Version (https://github.com/supermedium/three.js):',
pkg.dependencies['super-three']);
console.log('WebVR Polyfill Version:', pkg.dependencies['webvr-polyfill']);
Expand Down