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

Physics API Thoughts #736

Closed
bjornbytes opened this issue Feb 1, 2024 · 13 comments
Closed

Physics API Thoughts #736

bjornbytes opened this issue Feb 1, 2024 · 13 comments
Labels

Comments

@bjornbytes
Copy link
Owner

With #735 open and high likelihood that LÖVR will switch to Jolt soon, we should start thinking about ways that the Lua API for lovr.physics can be improved! The goals would be to A) make it easier to use, B) make it align better with Jolt. This issue can serve as a place to collect all of the ideas.

@bjornbytes bjornbytes added the design hmm label Feb 1, 2024
@bjornbytes
Copy link
Owner Author

  • Remove gravity from lovr.physics.newWorld! I think it's there because LÖVE did it that way, but 95% of the time I don't care about gravity and just want to use the default of -9.81 or whatever. And it's the first thing you have to say when you create a world, which is annoying when you're usually interested in setting the tag list. I'd rather just call :setGravity after making the world.
  • Remove the custom collision system: the World:update callback function, World:computeOverlaps, World:overlaps, and World:collide. This currently serves as A) collision filtering and B) collision detection, but it's super awkward. Jolt has callbacks for ignoring contacts and detecting when they're added/persisted/removed. This seems better.
  • Raycast direction vs. endpoint: Currently World:raycast takes an origin and an endpoint, not an origin and a direction which might be more intuitive?
  • Remove stuff unsupported by Jolt:
    • World:get/setResponseTime
    • World:get/setTightness
    • World:get/setLinearDamping
    • World:get/setAngularDamping
    • World:is/setSleepingAllowed
    • The threshold param on Collider:setLinear/AngularDamping
    • Inertia matrix?
  • Change "gravity ignored" setting to "gravity factor"?

@xiejiangzhi
Copy link
Contributor

When an API-friendly but incomplete physics engine is put in front of me versus an API-unfriendly but fully-featured physics engine, I'd rather use the one that's fully functional but API-unfriendly.
Because the former can directly complete the task, while the latter, although good-looking, cannot complete many tasks.

On love2d, it can basically achieve what box2d can do, at least the content in the box2d document can be achieved. I just hope that lovr can also try its best to output the complete functions of the physics engine, instead of just making the API simple and beautiful.

@jmiskovic
Copy link
Contributor

jmiskovic commented Feb 1, 2024

@xiejiangzhi I also don't want a toy engine. Jolt is still growing and we will add more features along the way. We should still take this opportunity to remove some of accumulated complexity to make the API more orthogonal and tidier.

@bjornbytes

  • Yes, the newWorld parameters were a bit of a mixed soup. Good improvement.
  • YES! I don't know how performant it will be, needs checking.
  • I don't have any preference over direction vs endpoint. BTW I see my jolt integration doesn't stop at the endpoint.
  • I don't see much other choice than to remove what we cannot map to Jolt. The inertia seems to be doable through the MotionProperties.SetInverseInertia .
  • Yeah, useful.

While we are changing the API, we should also adapt the querying. The jolt integration supports full shape casting (any shape, shape moving through the space).

I would suggest to keep just one raycasting function, having three is confusing.

The biggest hurdle is multiple shapes per body. Jolt supports them through compound shape. We have few options:

  1. Keep the existing Collider:addShape API, with one compound shape to each collider
  2. Adapt to jolt API - expose the CompoundShape and CompoundShape:addShape in lovr API; rework the Collider API
  3. Nudge users to weld the shapes together (with either the FixedJoint, or by converting them to a single mesh shape)

Sometime in the future we could add more features:

  • All constraints support the spring action at the limits, I'd like to have those
  • Restricting DOFs for MotionProperties is neat for upright capsules that correctly respond to collisions
  • Vehicles, soft bodies, character controllers, big worlds! Maybe not in core lovr, but packaged as plugins

@bjornbytes
Copy link
Owner Author

  • The inertia seems to be doable through the MotionProperties.SetInverseInertia .

Thanks, didn't know about this.

While we are changing the API, we should also adapt the querying. The jolt integration supports full shape casting (any shape, shape moving through the space).

Yes, would definitely like shape querying and shape casting!

I would suggest to keep just one raycasting function, having three is confusing.

They're pretty convenient and efficient when you're just trying to find the closest shape, which is common. And it would be good to have some way to hook into Jolt's "early out" params for casts, though I haven't wrapped my head around them yet...

The biggest hurdle is multiple shapes per body. Jolt supports them through compound shape.

Yeah this seems complicated and important. Another related issue I noticed is that the Jolt shapes are immutable (can't change radius/size/etc. of shapes once they're created).

There's been some discussion in chat (1, 2) around how multiple shapes is kind of annoying. Merging shapes and colliders somehow would be pretty cool, gotta think about it more.

CompoundShape seems good...it might require you to do if shape:type() == 'CompoundShape' and use recursion in places (e.g. drawing colliders in phywire), but that's probably not a dealbreaker.


I came up with a few more small things while reading through the PR today:

  • PhysicsSystem in Jolt has a quick "are 2 bodies touching" function. This might be nice -- more convenient than tracking things with the callbacks and more efficient than doing some kind of collision test (it just hashes the 2 body IDs).
  • There's a "get an AABB of all the colliders" function that could be added to World.
  • queryPoint

@bjornbytes
Copy link
Owner Author

So far I'm kinda leaning away from the CompoundShape idea as well as the merged BoxCollider/SphereCollider/CompoundCollider/etc. object.

For CompoundCollider you still need a way to add/remove shapes and get/set their properties, and Shape objects are best for that. And if you're going to have Shape objects anyway, you may as well separate them from the Collider object so that all the methods are in one place, you can reassign a Shape to a collider, etc.

For CompoundShape, it's nothing more than a list of shapes with per-shape transforms. I think it's friendlier for LÖVR to manage this for you internally, rather than having this weird "shape that's not a shape" object. So basically, current API is pretty good... Unless I just have stockholm syndrome or something.

The implementation for Jolt is kind of annoying. We would have to switch between CompoundShape depending on number of attached shapes, maybe recreate shapes or add "decorator" shapes if some of their properties get modified. Are there any reasons it couldn't work though?

@jmiskovic
Copy link
Contributor

Yes, we can continue with the current API around Colliders and Shapes. We can also test using the CompoundShape even for one-shape colliders and see if we lose any performance. It would simplify things. But the switching between shape primitives and compound shapes is also doable.

Re:

  • "are 2 bodies touching" function - I never needed this. I often need the callback for specific collider or tag (most often "hand") to trigger some behavior, and a general callback for any collision for triggering the sound effects. Jolt also has concept of physics materials for supporting different sound effects, but maybe that's too much for lovr.
  • "get an AABB of all the colliders" function - I could only think of using this for debugging visualization, and in that case I can iterate over colliders and fetch their AABBs. Not that I mind having it, of course.
  • queryPoint - this seems like a duplicate of raycasting with direction vector set to zero length, or origin=endpoint. Just tested, it works well and also gives you the normal as fastest way to exit the shape.

Back to raycasting for a moment. I said before that the physics_jolt.c doesn't stop at endpoint but taking another look at Jolt's side, the ray takes into account the direction vector length and correctly stops at endpoint. I believe lovr always uses the term "direction" as a normalized vector (I found Quat(Vec3), Curve:getTangent(), World:getLocal/WorldVector()). Using "direction" for directed distance vector in this case would be a bit confusing IMO. I would prefer to use the "endpoint" for both the raycast and spherecast because it better captures the intention.

All cast functions should also accept an optional tag parameter, currently only two variants have it. The result of raycastAny is not deterministic, it would have to be quite a bit faster than raycastClosest to earn its place.

The returning from casting early feature is a tad too complicated for me, with the C++ -> C -> Lua callback -> C -> C++ roundtrip. I would rather we collect all results and instead provide the guarantee to users that they are sorted. We can still limit the number of results with tags and the short casting distance, and the raycastClosest variant. What do you think?

@bjornbytes
Copy link
Owner Author

From chat:

  • Jolt has a triangle shape, this would make it easier to add World:queryTriangle, which was proposed before in Add world:queryTriangle(v1, v2, v3, function(shape) end) #689
  • World:raycastAny is weird
  • When filtering by tags in queries/raycasts, it would be useful to be able to filter by multiple tags in a single call. The current design requires you to issue multiple queries for this

@bjornbytes
Copy link
Owner Author

bjornbytes commented Apr 3, 2024

Here's my up-to-date list:

  • CompoundShape or otherwise supporting multiple shapes per collider in Jolt (CompoundShape #762)
  • World contact callbacks instead of "overlaps" system
  • lovr.physics.newWorld takes options table or just the tag list
  • Deprecate World:get/setResponseTime and World:get/setTightness (and joint methods for it)
  • Deprecate World:get/setLinearDamping and World:get/setAngularDamping (maybe)
  • Deprecate "threshold" parameters for damping
  • Deprecate "gravity ignored" collider setting and add "gravity scale" accessor
  • Queries: queryShape, castShape, queryTriangle, queryPoint, Shape:testPoint, Shape:raycast (or some subset)
  • Queries take zero or more tags to filter by (can provide numeric mask for multi-tag, add World:getTagMask?)
  • Probably remove World:raycastAny
  • Fold step count into World:update instead of separate accessor (maybe)
  • Either move sensor state to Collider or emulate sensor shape with contact callbacks
  • Add Collider:get/setMotionQuality (discrete vs. continuous collision checking)
  • Maybe add support for static colliders in addition to kinematic
  • Add Collider:applyLinearImpulse/applyAngularimpulse
  • Do Fixed Timestep Physics Interpolation #707
  • Add convex hull shape
  • Use lovr's job system

@bjornbytes
Copy link
Owner Author

bjornbytes commented Apr 7, 2024

I did a deeper dive on queries and came up with the following design:

  • World:raycast
  • World:raycastAny
  • World:raycastAll
  • World:collidePoint
  • World:collideTriangle
  • World:collideShape
  • World:castShape
  • World:queryBox
  • World:querySphere
  • Shape:collidePoint
  • Shape:raycast

With args:

  • collider, x, y, z, nx, ny, nz, child = World:raycast(origin, endpoint, filter)
  • collider, x, y, z, nx, ny, nz, child = World:raycastAny(origin, endpoint, filter)
  • World:raycastAll(origin, endpoint, filter, function(collider, x, y, z, nx, ny, nz, child))
  • World:collidePoint(x, y, z, filter, function(collider, child))
  • World:collideTriangle(x1, y1, z1, x2, y2, z2, x3, y3, z3, filter, function(collider, child))
  • World:collideShape(shape, transform, filter, function(collider, x, y, z, nx, ny, nz, depth, child1, child2) end)
  • World:castShape(shape, transform, direction, filter, function(collider, x, y, z, nx, ny, nz, depth, child1, child2) end)
  • any = World:queryBox(position, size, filter, [function(collider)])
  • any = World:querySphere(position, radius, filter, [function(collider)])
  • Shape:collidePoint(point, [transform])
  • Shape:raycast(origin, endpoint, [transform])

Notes:

  • ALL queries take an optional filter, which can be a single tag string or a number representing a bitmask of tags (World:getTagMask(...tags) will return one for you)
  • World:raycast defaults to closest hit now, that seems like the most common use
  • I kept World:raycastAny...I think I'm the only one that likes it but it's nice to have the option to early-exit
  • I also kept World:collidePoint, this test is cheaper than a 0-direction raycast (esp against e.g. ConvexHull)
  • Maybe cast naming should match: World:shapecast/World:raycast or World:castRay/World:castShape. Second one is better but I really like the way "raycast" looks for some reason.
  • The World:queryBox and World:querySphere methods are going to be broadphase queries -- they do quick checks to see if collider bounding boxes overlap the shape. This can be used to quickly exit before doing a more expensive test.
    • Example 1: You only want to re-render a point light shadow map if there are dynamic objects in its range, so you do a quick World:querySphere(light.position, light.radius, 'dynamic') check to see if it needs to be regenerated.
    • Example 2: You have a turret that should target the closest enemy in range with line-of-sight. You could do a raycast/shapecast to each enemy, but this could get really slow if you have a lot of turrets or a lot of enemies, so instead you can do a sphere query to quickly get the set of enemies in range and then do a cast for each one.
  • Shape:raycast and Shape:collidePoint are nice if you just need collision and don't need a full physics simulation. For example, you could create a MeshShape for a Model and do raycasts to enable drawing on the mesh with a pen.
  • It might be annoying to manually pass the shape pose when doing :collideShape/:castShape, maybe there can be variants that take a collider (and sub-shape index) which uses the collider's pose.

@bjornbytes
Copy link
Owner Author

A simpler raycast API would be:

collider, x, y, z, nx, ny, nz, shape = World:raycast(start, end, filter)
World:raycast(start, end, filter, function(collider, x, y, z, nx, ny, nz, shape) end)
  • Closest hit: If you leave the callback out, we do raycastClosest and return the closest hit.
  • All hit: If you pass a callback, we call it for every hit.
  • Any hit: You can return true from the callback to tell the physics engine to early exit.

Thought about doing a "hit fraction" system but it's too complicated. It does allow for a "custom closest hit" type thing but I think it's okay to omit this.

This way, we can get away with having only a single raycast function.

@bjornbytes
Copy link
Owner Author

Simplifying further (similar to Josip's previous suggestions)

  • raycast with zero/nil direction could map to CollidePoint
  • shapecast with zero/nil direction could map to CollideShape
  • collideTriangle can be replaced with a shapecast of a mesh shape
  • querySphere can take a zero/nil radius for a point query (supported by Jolt)
  • queryBox could take optional rotation for an OBB query (supported by Jolt)

That would leave us with the following for all queries:

World:raycast(origin, direction, filter, callback)
World:shapecast(shape, position, scale, orientation, direction, filter, callback)
World:queryBox(position, size, orientation, filter, callback)
World:querySphere(position, radius, filter, callback)

It's nice and compact, but I hope it's not too confusing to document all the "tricks" that are supported.

@bjornbytes
Copy link
Owner Author

bjornbytes commented Apr 22, 2024

From chat: we should add degree of freedom restriction. There's SixDOF joint, but a simpler approach might be the allowedDofs param of MotionProperties::SetMassProperties (world space only though).

Example API might be Collider:setLockedAxes([translation], [rotation]) where each one can be strings with "xyz" chars.

@bjornbytes
Copy link
Owner Author

Ok pretty much all of the ideas here have been implemented, so I'm going to close this.

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

No branches or pull requests

3 participants