Question: how could I show layer always (regardless of altitude)?
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.
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
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
Probably not ideal for performance but I found you can override them on loading the map using map.transform.overrideNearFarZ
i have same issue how did you solve this ?
@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>
@Mhamad6000 In raw three.js you can use this approach maplibre/maplibre-gl-js#6443
Otherwise, I'm calling
map.transform.overrideNearFarZwith a huge value for far in theonLoadhook on theMapcomponent.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.
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.