react-three-map icon indicating copy to clipboard operation
react-three-map copied to clipboard

Question: how could I show layer always (regardless of altitude)?

Open heathhenley opened this issue 3 months ago • 7 comments

TL;DR - I want to make things on my custom layer that are below map in altitude always visible, haven't figured out the right way to do it.

I'm working on drawing a r3f scene on a maplibre map. I'm drawing things below the map (at negative altitude - though my canvas is a 0), but I'm having trouble understanding how to control their display. For the use case I'm working on, they should not be occluded (even though they are below). They seem to be or not be depending on the zoom / pitch or with minor rotations in the camera angle.

This doesn't seem to depend on any gl depth testing, depth writing, or render order settings on my r3f components that I've tried, and it's happening with overlay mode on or not. I think I can see the same behavior in the sun example. Eg, if I understand correctly the near part of the loop in the video is always under the map, but wiggling the camera just a bit changes if it's visible or not.

https://github.com/user-attachments/assets/ba7368ff-3f91-4a10-8267-68e51e3ff4f2

If anyone an provide any help with understanding what is happening there it would be greatly appreciated. I tried dumping camera settings each frame and I don't see anything obviously different between the occluded / clipped / culled positions and not.

If I move the whole thing up so the that the lowest thing in my canvas is at an altitude >= 0, it never does this, but I don't think I can make that set up work.

I know there's probably something trivially simple I just don't understand yet, I would welcome any suggestions.

heathhenley avatar Sep 18 '25 13:09 heathhenley

Here's a minimal sandbox: https://codesandbox.io/p/sandbox/6432yj

I would like to know to make sure the blue box is always visible no matter the camera angle... 🤔

https://github.com/user-attachments/assets/14f1cfcf-eaa1-49ba-ba8d-c68c0f2a6fc1

heathhenley avatar Sep 18 '25 20:09 heathhenley

This seemed to be because of the far plane of the view port that maplibre-gl uses. I found a way to hack in new near/far values to make sure it's not clipped. Very much a hack and only relevant to maplibre-gl but as a POC to see how it functionally can be done it looks like this:

diff --git a/src/core/canvas-in-layer/use-render.ts b/src/core/canvas-in-layer/use-render.ts
index 6988751..5b0a039 100644
--- a/src/core/canvas-in-layer/use-render.ts
+++ b/src/core/canvas-in-layer/use-render.ts
@@ -1,5 +1,5 @@
 import { RootState } from "@react-three/fiber";
-import { Matrix4Tuple, PerspectiveCamera } from "three";
+import { Matrix4, Matrix4Tuple, PerspectiveCamera } from "three";
 import { UseBoundStore } from "zustand";
 import { MapInstance } from "../generic-map";
 import { syncCamera } from "../sync-camera";
@@ -15,13 +15,39 @@ export function useRender({
   frameloop?: 'always' | 'demand',
   r3m: R3M
 }) {
+
+  const newNear = 0.01;
+  const newFar = 1e10;
+
+  const adjustNearFar = (pvOriginal: Matrix4, pOriginal: Matrix4, near: number, far: number) => {        
+    const pv = pvOriginal.clone();
+    const pInv = pOriginal.clone().invert();
+    // hack new near and far
+    const newP = pOriginal.clone();
+    newP.elements[10] = (far + near) / (near - far);
+    newP.elements[14] = (2 * far * near) / (near - far);
+
+    // re-multiply by v
+    const v = pInv.multiply(pv);
+    const newPV = newP.multiply(v);
+    return newPV;
+  }
+
+
   const render = useFunction((_gl: WebGL2RenderingContext, projViewMx: number[] | {defaultProjectionData: {mainMatrix: Record<string, number>}}) => {
-    const pVMx = 'defaultProjectionData' in projViewMx ? Object.values(projViewMx.defaultProjectionData.mainMatrix) : projViewMx;
-    r3m.viewProjMx.splice(0, 16, ...pVMx)
+    //const pVMx = 'defaultProjectionData' in projViewMx ? Object.values(projViewMx.defaultProjectionData.mainMatrix) : projViewMx;
+    const pv = adjustNearFar(
+      new Matrix4().fromArray(projViewMx.defaultProjectionData.mainMatrix),
+      new Matrix4().fromArray(projViewMx.projectionMatrix),
+      newNear,
+      newFar
+    );
+    //console.log('pv', pv.elements);
+    r3m.viewProjMx.splice(0, 16, ...pv.elements);
     const state = useThree.getState();
     const camera = state.camera as PerspectiveCamera;
     const {gl, advance} = state;
-    syncCamera(camera as PerspectiveCamera, origin, pVMx as Matrix4Tuple);
+    syncCamera(camera as PerspectiveCamera, origin, pv.elements);
     gl.resetState();
     advance(Date.now() * 0.001, true);
     if (!frameloop || frameloop === 'always') map.triggerRepaint();

Then things are no longer culled inconsistently based on zoom. I'm not sure if there's already a way that this is exposed so that I could do the near/far hack in client code instead? Obviously that would be better than modifying r3m. Happy to contribute though if you have some guidance on how / what should be exposed to either make it possible via client code or to add some option to override near/far. I haven't looked into what would be needed for it work with mapbox.

Ex:

https://github.com/user-attachments/assets/439af416-8c1e-4011-8543-a80d337d49c1

heathhenley avatar Sep 22 '25 20:09 heathhenley

Probably not ideal for performance but I found you can override them on loading the map using map.transform.overrideNearFarZ

heathhenley avatar Sep 22 '25 20:09 heathhenley

i have same issue how did you solve this ?

Mhamad6000 avatar Oct 04 '25 15:10 Mhamad6000

@Mhamad6000 In raw three.js you can use this approach https://github.com/maplibre/maplibre-gl-js/issues/6443

Otherwise, I'm calling map.transform.overrideNearFarZ with a huge value for far in the onLoad hook on the Map component.

Something like:

 <Map
  onLoad={(e: MapEvent) => {
    const map = e.target;
      map.transform.overrideNearFarZ(0.1, 1e13);
    }}    
  // other props
>

... canvas child etc

</Map>

heathhenley avatar Oct 04 '25 20:10 heathhenley

@Mhamad6000 In raw three.js you can use this approach maplibre/maplibre-gl-js#6443

Otherwise, I'm calling map.transform.overrideNearFarZ with a huge value for far in the onLoad hook on the Map component.

Something like:

<Map onLoad={(e: MapEvent) => { const map = e.target; map.transform.overrideNearFarZ(0.1, 1e13); }}
// other props

... canvas child etc

Thanks for the suggestion! I tried that code, but when I zoom out, the entire map becomes invisible. It seems like setting a very large far value causes rendering issues.

Mhamad6000 avatar Oct 05 '25 06:10 Mhamad6000

Yeah you'll probably have to dig in then for your use case. If it disappears when far away, maybe your value isn't actually large enough - you could probably find a way to log the defaults (without the override) to see what they are at different pitches / zooms.

Ideally, you wouldn't override them on maplibre's render, only when when setting the values for the three.js render (the linked issue is an example of that). It's easy to do with raw custom layer (like the in maplibre-gl three.js example), but I didn't see an easy way to hook into only that part on react-three-map - unless I missed it it's not exposed. One hacky way is above.

heathhenley avatar Oct 05 '25 19:10 heathhenley