Constraining a UniversalCamera on a Navigation Mesh

Moving this conversation from the Navigation Mesh PR.

@tibotiber:
[Navigation Mesh] looks really cool @Cedric. Just tried it, an it’s super easy to setup and compute the navigation mesh. Is there any way this could be used to constraint the walkable area for a UniversalCamera?

@Cedric:
Hi Thibaut

It should be possible to attach a camera to a transform agent. Something like:

camera.parent = agent.transform;

But you’ll have to create an agent as it’s the only object constrained by the navigation automatically. Or, every frame, you can do it manually with a getClosestPoint call and set the camera position relative to that point.

@tibotiber:
Hi! So I’ve tested what you suggested Cédric and some things are worth reporting for whoever attempts something similar.

Setting the camera’s parent to the agent’s transform node:
This is pretty neat. If you implement the picking to move agents to a location demonstrated in your PG demo, you get a pick-to-move-camera à la Google Street View. It doesn’t mix well with keyboard inputs however since the FreeCameraKeyboardMoveInput will update the camera position, while the agent will update the parent’s position, resulting in a hard to control global position which almost always gets out of the navmesh. As long as you stay away from the keyboard or remove that input, it’s pretty interesting though. To be combined with a camera rotation input using keyboard or swipe (I’ll test that next).

using getClosestPoint at every frame:
This clearly is the simplest way to make sure the camera remains on the navmesh, and it’s compatible out of the box with the FreeCameraKeyboardMoveInput . There are some weird behaviours though. For example, when you walk under some stairs which are part of the navmesh, the camera can sometimes jump on top of the stairs. I have seen other jump behaviours near edges of the navmesh. I haven’t got a full multi-storey map to test further unfortunately. From the first tests, it’s as if getClosestPoint ignored the height (y value) when picking the closest point. Is that possible?

Quick question also:
Why are we disconnecting the camera in your PG demo when picking an agents’ location?

1 Like

Pinging @Cedric

Hi @tibotiber

Welcome to the forum :slight_smile:

When you compute the reference point to call getClosestPoint with, can you try to set it slightly below. Y component should be used for getting the closest point so setting it a bit below might give you better result. If you have a playground with multilevel geometry, I’d be curious to give it a try.

No particular reason for disconnection the camera. I just found it easier to show the agents and do the picking.

Hi @Cedric

Thank you, glad to be here :slight_smile:.

I don’t have an easy to share geometry because my app allows me to generate the geometry (floorplan) I’m testing so I can’t just extract it and put it in a PG. I could give you access to the app if you want to check it out from a “user” perspective but no easy code access. However, I searched a bit the PG and found a multilevel house which I modified to generate the navmesh and add a navmesh-bound camera. You can test it here: https://www.babylonjs-playground.com/#RNCYVM#64.

Notes:

  1. you can use AWDS keys to rotate the camera. (bindings may be weird, I’m not a gamer :stuck_out_tongue:)
  2. in this example, the stairs don’t get picked up in the navmesh. I can’t understand why, and that’s not a problem I have in my app. See screenshot below.

For the y-component, I have tried lowering it but it doesn’t seem to help even with big values. So in the screenshot above, if I walk behind the stairs (there is a navmesh path) it’s ok, but as soon as I step out of the navmesh, the calculated closest point is on top of the stairs, not the ground floor navmesh right beside it :thinking:.

One more thing, the camera sometimes manages to walk through walls. I could increase the cs value but it would get harder to get through doors then. Any idea on this?

To fix both of these, I wonder if we could somehow skew the getClosestPoint result towards the last known point on the navmesh, so these “jumps” would not happen.

(ahah, ok for the disconnected camera, no magic potion there then)

Yes, if you lose details from your mesh, the only solution is to increase cs/ch until the navmesh corresponds to what you want.

For the stairs not picked up by the system, increase the walkableClimb value ( I used 3 in my test). Then, because of the floor, there is a zone that might not be reachable in the middle of the stairs. decrease walkableHeight. Any height (roof-floor) smaller than that will be discarded.

image

PG : https://www.babylonjs-playground.com/#RNCYVM#65

For the closest point thing, I don’t think that the logic you expose should be part of the navigation. Can you try to make a couple calls to getClosestPoint (with different heights) and select the one that suits you well?

One more thing. If you let camera move freely then change its position thanks to getClosestPoint, the camera might move way beyond the navmesh. getclosestpoint will find the best candidate but way forward the navmesh. Recast provides a function to ‘cast’ a segment along the navmesh (not exposed yet in the API).
With that function, you would compute the camera destination, substract that position with the current camera position. And limit that vector using Recast. Then add that constraint vector to the current camera position. I’m sure this eliminate the jumps between floors.

I’ll add that functionality in the coming days.

I did the PR for the moveAlong function.

This function casts a segment on the navmesh and returns the furthest position along that segment that is on the navmesh. Call this with (previousCameraPosition, nextCameraPosition)
I also found on your test scene that setting maxSimplificationError to 0 makes a more blocky navmesh but better result with the stairs. So, more tweaking for you is expected :slight_smile:

Oh yes, good catch. I did increase walkableClimb previously but I didn’t realise the roof getting closer when you get up the stairs. Tall people problems haha.

Now that you’ve fixed this, I guess you are able to observe the “jump” behaviour. In your PG, I’m getting it easily when walking near the top of the stairs. That’s also what I think, the issue is that the camera moves too far out of the navmesh before getClosestPoint is executed for correction. Great idea for moveAlong, and thanks for adding it so fast :rocket:. I’ll try that and update the PG (once the preview release is published), thank you so much for the great help :pray:.

Noted for maxSimplificationError, will tweak off once I get to fine tuning.

Hi @Cedric

I took a while to test this and revert as I missed that the preview release was updated. I’ve just tried it. A few things.

  1. moveAlong seems to work well to avoid the jumps (through wall or near stairs) but it seems to be a 2D method only, height is not taken into account so there is no issue changing the Y coordinate of the camera in my case (like a helicopter take off), and I walk through stairs instead of up onto them. I’ve checked Recast and I guess your implementation relies on dtNavMeshQuery::moveAlongSurface (docs). There doesn’t seem to be a 3D equivalent. Quoting the docs, we may need to add getPolyHeight as well to get build a 3D version ourselves.

    resultPos is not projected onto the surface of the navigation mesh. Use getPolyHeight if this is needed.

    I have tried to combine moveAlong for the XZ position and then getClosestPoint for the Y position, but I’m back to stairs jumps again. Do you think things would be better with getPolyHeight?

  2. Looking at the 2D moveAlong issue, I wonder if that’s not something similar we’re hitting with getClosestPoint. Which Recast method is that using? I’ve found dtNavMeshQuery::closestPointOnPoly (docs) and dtNavMeshQuery::closestPointOnPolyBoundary (docs), the 2nd one stating that “the height detail is not used”.

  3. I’ve an updated PG but the latest recast preview has apparently not been published so we’re getting TypeError: this.navMesh.moveAlong is not a function.

  4. Something new and unrelated: I’ve consistently experienced a weird bug where the world origin Vector3.Zero() is considered part of the navmesh whether it belong to it or not. See screenshot below. Any idea why?

Once again, thanks a lot for your help!

Bonus question: is the source for the recast.js available anywhere?

Hi @tibotiber

1-2 Let me check for the height value. I’m sure I can improve that.
3 That’s weird! I’m checking that too
4 (0,0,0) is the value returned when recast fails at finding a result
5 yes, the source is available in extensions GitHub - BabylonJS/Extensions: Extensions for Babylon.js
and recastjs is accessible here Extensions/recastjs at master · BabylonJS/Extensions · GitHub

  1. It’s the first function that’s used
  2. I know what I did wrong. I’ll update the recast.js very soon.

I’ve added a new function in this PR


That allows to set the extent box query. Basically, set a small value for finer result. bigger value for broader result query.
So, setting a small value on the navmesh (like 0.1,0.1,0.1) should make your navigation much better.

Hi @Cedric

Thank you for your answers and adding the glue for extent params, looks like it’ll be a good help. I’m guessing the getClosestPoint failure could be solved by increasing the extent(?)(was it (1,1,1) by default until now?). And yes, looking forward to try the navigation with a smaller extent, especially vertically.

I’ll update the PG once the PR is merge and preview release updated.

Thanks also for indicating that (0,0,0) is a failure by Recast to perform a spatial query. This silent failure seems a bit unnatural, I’m guessing this is something internal to Recast, and not addressable by your wrapper. Maybe we could document this? I don’t mind doing it if you point me to the best place, here maybe?

To get a closer point you have to use a smaller extent. Imagine a bounding box around the parameter point that Recast uses to compute a valid solution.

I’m updating the documentation. The extensions doc is on GitHub at this address : Documentation/content/extensions/Navigation at master · BabylonJS/Documentation · GitHub

Doc update is merged precision on queries and query extent by CedricGuillemet · Pull Request #1666 · BabylonJS/Documentation · GitHub

Thanks @Cedric, this has been super useful. The updated PG is here: https://www.babylonjs-playground.com/#RNCYVM#70.

I’m now setting a larger extent when I’m calculating the camera’s initial position so that it works fine even if the navmesh is far from the origin. It’s not really an issue on the PG itself but it is in my app. I do find confusing that getRandomPointAround has its own range parameter, but that’s applied on top of the “global” query’s extent. That’s a nitpick though, I can live with it :wink:

A weird thing, even with a very low extent, I was still getting camera jumps when walking under the stairs and trying to walk across the stairs. I can get around this by checking the difference in Y value between the last and new positions of the camera and making sure it’s not over the walkableClimb value. But it is not normal this even happens. To test this, on the PG, I have been more verbose with the console logs and you should be able to observe when a jump happens the values of the camera position before (last), after moveAlong (mA) and after getClosestPoint (new). You can get a 2m jump despite the extent being set at 0.15. See screenshot, the gray thing is the stairs, I’ve moving left at that position. Am I missing something?

Oh, and the improved docs are great. Thanks, once more :slight_smile:

The recast navmesh simplification will not make the mesh flat. It may end up using a vertex from the stairs with vertices from the ground to build a triangle.

I see two possibilities:

  • lower the simplification
  • adapt your camera height from the height returned by the navmesh (low band filter, threshold,…)

I think you can lower the extent to 0.01 per component, at least on Y.

Oh, I must have explained wrongly, sorry. The issue is not on the side where you go up the stairs, there is indeed a triangle there but that’s completely acceptable.

The issue is if you walk under the stairs and towards them (like you’re gonna collide with the stairs from the bottom). At some point the navmesh ends (due to walkableHeight) and that’s completely normal. The expected behaviour is that the camera should then stop there due to moveAlong (it does), but the subsequent call to getClosestPoint puts the camera one level up (in the middle of the stairs actually).

Numbers wise, the moveAlong position is { x: 2.63, y: -2.30, z: 2.54 }, then I call getClosestPoint on it and get { x: 2.63, -0.01, z: 2.54 } despite having set a cubic extent of 0.15. From my understanding, this result is not even in scope. (What’s the unit for the extent actually?)

This issue is easy to get around to be honest, so I don’t particularly need a fix or anything. I just wanted to share in case that’s good input for you.

About this idea

Having solved the issues thanks to @Cedric, I’m pretty happy with the result we got. A navmesh bound universal camera, letting me dump gravity and collision detection for something calculated only once per scene and only small operations executed at render time.

Pros:

  • better render performance
  • easier to setup
  • autopilot camera by attaching it to an agent

Cons:

  • bundle size takes a hit
  • may need tinkering with the navigation mesh parameters to adapt to specific scenes

What do you think of the approach? Good? Ugly? Any inconvenience I may be missing?

I understand now your issue with the stairs. it’s odd indeed. It’s great you found a fix until I come up with a better solution.

The next feature I’ll work on is to lower the bundle size. Separate the navmesh computation from the navigation and save a compressed version of the navmesh for future reuse.

Yes, tweaking the navmesh computation can be tedious. This gives me the idea to make a tool to compute the navmesh and save it. Maybe with more debug info during tweaking it would be easier.

Cool, looking forward to all of that. Feel free to reach out if you need testing or feedback. and thanks again for all the help on this thread. Really appreciate it :slight_smile: .

2 Likes

First of all just want to say thank you @tibotiber and @Cedric for this thread, I learned a lot from it and even re-purposed some of the playground code for my app which also uses a universal camera and constrains it to the nav mesh. This works beautifully on desktop and mobile so far.

I’m trying to take this a step further though, and use the nav mesh as the “floorMesh” in XR so that it is used as the area in which users can successfully teleport when they are in VR headsets. I’ve tried something basic like:

const xr = await scene.createDefaultXRExperienceAsync({
  floorMeshes: [navigationPlugin.navMesh]

});

…but that doesn’t seem to cut it, as I still can’t teleport anywhere on the nav mesh. Any possible ideas or leads on how I might be able to use the nav mesh as my floorMesh for the XR experience?

2 Likes