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

feat: Add Ray2 class to be used in raytracing/casting #1788

Merged
merged 21 commits into from
Jul 27, 2022
Merged
Show file tree
Hide file tree
Changes from 17 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 change: 1 addition & 0 deletions packages/flame/lib/geometry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export 'src/geometry/circle_component.dart';
export 'src/geometry/line.dart';
export 'src/geometry/line_segment.dart';
export 'src/geometry/polygon_component.dart';
export 'src/geometry/ray2.dart';
export 'src/geometry/rectangle_component.dart';
export 'src/geometry/shape_component.dart';
export 'src/geometry/shape_intersections.dart';
15 changes: 15 additions & 0 deletions packages/flame/lib/src/extensions/double.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export 'dart:ui' show Color;
spydon marked this conversation as resolved.
Show resolved Hide resolved

extension DoubleExtension on double {
/// Converts +-[infinity] to +-[maxFinite].
/// If it is already a finite value, that is returned.
double toFinite() {
if (this == double.infinity) {
return double.maxFinite;
} else if (this == -double.infinity) {
return -double.maxFinite;
} else {
return this;
}
}
}
129 changes: 129 additions & 0 deletions packages/flame/lib/src/geometry/ray2.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import 'dart:math';

import 'package:flame/geometry.dart';
import 'package:flame/src/extensions/double.dart';
import 'package:meta/meta.dart';
import 'package:vector_math/vector_math_64.dart';

/// A ray in the 2d plane.
///
/// The [direction] should be normalized.
class Ray2 {
Ray2(this.origin, Vector2 direction) {
this.direction = direction;
}

Ray2.zero() : this(Vector2.zero(), Vector2(1, 0));

Vector2 origin;
final Vector2 _direction = Vector2.zero();
Vector2 get direction => _direction;
set direction(Vector2 direction) {
assert(
(direction.length2 - 1).abs() < 0.000001,
'direction must be normalized',
);
spydon marked this conversation as resolved.
Show resolved Hide resolved
_direction.setFrom(direction);
directionInvX = (1 / direction.x).toFinite();
directionInvY = (1 / direction.y).toFinite();
}

// These are the inverse of the direction (the normal), they are used to avoid
// a division in [intersectsWithAabb2], since a ray will usually be tried
// against many bounding boxes it's good to pre-calculate it, which is done
// in the direction setter.
@visibleForTesting
late double directionInvX;
@visibleForTesting
late double directionInvY;

/// Whether the ray intersects the [box] or not.
///
/// Rays that originate on the edge of the [box] are considered to be
/// intersecting with the box no matter what direction they have.
// This uses the Branchless Ray/Bounding box intersection method by Tavian,
// but since +-infinity is replaced by +-maxFinite for directionInvX and
// directionInvY, rays that originate on an edge will always be considered to
// intersect with the aabb, no matter what direction they have.
// https://tavianator.com/2011/ray_box.html
spydon marked this conversation as resolved.
Show resolved Hide resolved
// https://tavianator.com/2015/ray_box_nan.html
bool intersectsWithAabb2(Aabb2 box) {
final tx1 = (box.min.x - origin.x) * directionInvX;
final tx2 = (box.max.x - origin.x) * directionInvX;

final ty1 = (box.min.y - origin.y) * directionInvY;
final ty2 = (box.max.y - origin.y) * directionInvY;

final tMin = max(min(tx1, tx2), min(ty1, ty2));
final tMax = min(max(tx1, tx2), max(ty1, ty2));

return tMax >= max(tMin, 0);
}

/// Gives the point at a certain length along the ray.
Vector2 point(double length, {Vector2? out}) {
return ((out?..setFrom(origin)) ?? origin.clone())
..addScaled(direction, length);
}

static final Vector2 _v1 = Vector2.zero();
static final Vector2 _v2 = Vector2.zero();
static final Vector2 _v3 = Vector2.zero();
static final Vector2 _v4 = Vector2.zero();

/// Returns where (length wise) on the ray that the ray intersects the
/// [LineSegment] or null if there is no intersection.
///
/// A ray that is parallel and overlapping with the [segment] is considered to
/// not intersect.
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we can handle this edge case better? For example, adding if (dot == 0) { ... }.

Or was this a deliberate choice?

Copy link
Member Author

Choose a reason for hiding this comment

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

That would be nice, what should we return in that case though, would t1 still be a valid point on the line segment?

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, obviously it can't return t1 since that would be having a division by zero.
I'll try around a bit, if you have a good idea of how it can be done I'm all ears.

Copy link
Member Author

@spydon spydon Jul 26, 2022

Choose a reason for hiding this comment

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

What do you think about the solution that I pushed now?

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, maybe it makes sense that it doesn't collide with parallel rays, because it will always collide with one of the line segments connecting to it anyways?

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess it makes sense, esp since the intersection in this case is not a single point but rather a segment, so returning a single point would be wrong.
But we should probably explain this in a comment, because I suspect we'll soon forget what the reason was.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good idea, I added that in #1785

double? lineSegmentIntersection(LineSegment segment) {
st-pasha marked this conversation as resolved.
Show resolved Hide resolved
_v1
..setFrom(origin)
..sub(segment.from);
_v2
..setFrom(segment.to)
..sub(segment.from);
_v3.setValues(-direction.y, direction.x);

final dot = _v2.dot(_v3);

if (dot == 0) {
// ray is parallel to line
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry I'm on the phone rn, but I'd do smth like this here:

  1. Let dot2=v1.dot(v3). Then if dot2!=0 there's no intersection and we can return null.
  2. Otherwise, let t1=direction.dot(segment.from-ray.origin) and t2=direction.dot(segment.to-ray.origin). (These can be expressed in terms of v1 and v2). Then if both t1 and t2 positive, return the min of them; if both negative return null; otherwise, return 0 because the Ray's origin is inside the segment.

This ensures that we return a point closest to Ray's origin, and correctly handle the case when ray and segment are on parallel lines.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, maybe it makes sense that it doesn't collide with parallel rays, because it will always collide with one of the line segments connecting to it anyways? I'll remove it again for now and then we can re-add it again later if we choose to.

if (segment.containsPoint(origin)) {
return 0;
} else {
final closestPoint = origin.distanceToSquared(segment.to) <
origin.distanceToSquared(segment.from)
? segment.to
: segment.from;
_v4
..setFrom(closestPoint)
..sub(origin);
if (_v4.x.sign == direction.x.sign && _v4.y.sign == direction.y.sign) {
return _v4.length;
}
}
}

final t1 = _v2.cross(_v1) / dot;
final t2 = _v1.dot(_v3) / dot;
if (t1 >= 0 && t2 >= 0 && t2 <= 1) {
return t1;
}
return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This method should probably need to be tested too. In particular, I'm not sure whether the sign of t1 is correct.

Copy link
Member Author

Choose a reason for hiding this comment

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

Added some simple tests for it, please have a look to see whether they are testing what you were wondering about.


/// Deep clones the object, i.e. both [origin] and [direction] are cloned into
/// a new [Ray2] object.
Ray2 clone() => Ray2(origin.clone(), direction.clone());

/// Sets the values by copying them from [other].
void setFrom(Ray2 other) {
spydon marked this conversation as resolved.
Show resolved Hide resolved
setWith(origin: other.origin, direction: other.direction);
}

void setWith({required Vector2 origin, required Vector2 direction}) {
this.origin.setFrom(origin);
this.direction = direction;
}
}
19 changes: 19 additions & 0 deletions packages/flame/test/extensions/double_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import 'package:flame/src/extensions/double.dart';
import 'package:test/test.dart';

void main() {
group('toFinite', () {
test('Properly converts infinite values to maxFinite', () {
const infinity = double.infinity;
expect(infinity.toFinite(), double.maxFinite);
const negativeInfinity = -double.infinity;
expect(negativeInfinity.toFinite(), -double.maxFinite);
});

test('Does not convert already finite value', () {
expect(0.0.toFinite(), 0.0);
expect(double.maxFinite.toFinite(), double.maxFinite);
expect((-double.maxFinite).toFinite(), -double.maxFinite);
});
});
}
Loading