Class MarkerPlacement
Where a prefab-tier marker sits in view depth, and how big it is drawn there, so it stays aligned with the tilted map view.
public static class MarkerPlacement
- Inheritance
-
objectMarkerPlacement
Remarks
Pulled out of WorldMarkerManager so it can be tested. LateUpdate reads Camera.main and has no edit-mode path, so anything left in that file is reachable only by eye, and the depth law is the part worth gating.
A constant camera-parallel plane disagrees with a pitched map everywhere except one screen line. The depth law below keeps models on the map across the viewport.
Methods
Depth(double, double)
View depth to place the marker at, so it lands on the same plane the tiles are drawn on.
public static double Depth(double planeDistance, double perspectiveW)
Parameters
Returns
OverlayDepthCoefficients(double, double, double, double, bool)
Coefficients (A, B, C) for the 2D overlay tier's z-test depth, so that overlaySpace.z / w reproduces the real camera's NDC-z-vs-depth law at depth = planeDistance * w for every w, matching the depth the 3D prefab tier is placed at by Depth(double, double).
public static (double a, double b, double c) OverlayDepthCoefficients(double ndcAtPlaneDistance, double ndcAtTwicePlaneDistance, double ndcAtBiasedDistance, double planeDistance, bool orthographicCamera)
Parameters
ndcAtPlaneDistancedoubleNDC z the camera reports for a point at view depth planeDistance.
ndcAtTwicePlaneDistancedoubleNDC z the camera reports for a point at view depth 2*planeDistance.
ndcAtBiasedDistancedoubleNDC z the camera reports one tie-bias depth closer than planeDistance. Its difference from ndcAtPlaneDistance is the tie bias itself, magnitude and sign.
planeDistancedoubleView depth of the map plane at the view center.
orthographicCameraboolTrue when the real camera used to render the 3D prefab tier is orthographic.
Returns
- (double a, double b, double c)
(A, B, C) such that overlaySpace.z = Aw + B + Cww and overlaySpace.z / w is the real camera's NDC z at depth planeDistancew.
Remarks
A perspective camera's NDC(d) is A' + B'/d (hyperbolic in view depth d); an affine-in-w numerator (A'*w + B'/planeDistance) reproduces it exactly everywhere once divided by the shader's own w, because d = planeDistance * w cancels the reciprocal. An orthographic camera's NDC(d) is instead A' + B'd (linear), which an affine-in-w numerator can only match at the two depths it was sampled at (planeDistance and 2planeDistance) - everywhere else the two curves diverge, so an overlay marker's z-test depth stopped tracking its real position and could win against 3D geometry it should have been occluded by. The orthographic branch below adds a w^2 numerator term instead, which the divide turns into the missing linear-in-w term.
OverlayNearClipW(double, double, double)
Smallest map perspective W whose overlay z-test depth still lands inside the GPU's valid NDC z range, for coefficients from OverlayDepthCoefficients(double, double, double, double, bool). Below it the GPU clips the overlay outright, so this is the overlay tier's real near boundary. Returns 0 when no positive W is clipped.
public static double OverlayNearClipW(double a, double b, double c)
Parameters
adoubleCoefficient A: the w term of the shader's z numerator.
bdoubleCoefficient B: the constant term of the shader's z numerator.
cdoubleCoefficient C: the w^2 term of the shader's z numerator.
Returns
Remarks
The shader's z is (Aw + B + Cww) / w = A + B/w + Cw, so a crossing of either limit L solves C*w^2 + (A-L)*w + B = 0. Today's two projection branches each leave one of B and C at zero, which makes that quadratic linear and z monotone, but the general quadratic is solved here anyway: a caller that ever passes both terms gets the right answer instead of a silently wrong one from a branch that assumed the other term away.
The near boundary is the smallest positive root over both limits. z always leaves the valid range as w approaches 0 (B/w diverges, or with B = 0 the constant A + C*w sits outside it, which is what the tie bias guarantees), so the first crossing going outward from 0 is where overlays start surviving. Which limit the near plane maps to depends on reversed-Z, and taking the smallest root settles that without a per-platform branch.
Returns 0, meaning "no floor", when no positive crossing exists. That covers both the never-clipped case and the degenerate case where z is outside the range for every positive w - in the second there is no floor value that would rescue the overlay, so refusing to invent one is the honest answer. Solved rather than searched so a caller can run it every frame with no loop.
PerspectiveW(RenderMatrixRows, double, double, double, double)
The map's perspective term at a marker, including its altitude. Exactly 1 at the view center at ground level, larger further away.
public static double PerspectiveW(RenderMatrixRows centerRows, double anchorRelativeX, double relativeY, double altitudeWCoefficient, double altitudeMercator)
Parameters
centerRowsRenderMatrixRowsanchorRelativeXdoublerelativeYdoublealtitudeWCoefficientdoublealtitudeMercatordouble
Returns
Remarks
The rows are passed in rather than built here because they are a per-frame loop invariant: a row set's constant column is W at its own anchor, so building them per marker returns the same number for more work. Both paths must preserve that identity.
SizePx(double, double, bool)
Pixel size to build the model's world scale from, assuming a perspective Unity camera. Covers only the perspective real-size and perspective scene-size branches; call the 4-arg overload when the camera might be orthographic.
public static double SizePx(double px, double perspectiveW, bool sceneSize)
Parameters
Returns
Remarks
The map shader always performs its own perspective divide. A perspective Unity camera supplies the same divide to a prefab after it is moved to depth P*W: real-size passes through unchanged (depth already applied the 1/W) and scene-size multiplies by W to cancel it back out. Both branches agree with Camera.WorldToScreenPoint on a real perspective camera.
SizePx(double, double, bool, bool)
Pixel size to build the model's world scale from, given the camera projection actually used to draw the prefab. Covers all four branches: perspective real-size and perspective scene-size (via the 3-arg overload above) plus orthographic real-size and orthographic scene-size handled directly here.
public static double SizePx(double px, double perspectiveW, bool sceneSize, bool orthographicCamera)
Parameters
Returns
Remarks
Documented: an orthographic Unity camera never foreshortens with depth, so real-size must apply 1/W explicitly here since the camera supplies none of it, while scene-size passes through untouched because the camera already holds it constant. Enumerated: the LansMap demo camera is orthographic.
VisibilityMarginPx(double, double)
Offscreen-visibility margin widened by a marker's own apparent on-screen size, so a tall real-size model is not hidden just because its ground anchor has scrolled past the edge.
public static double VisibilityMarginPx(double configuredMarginPx, double apparentPx)
Parameters
configuredMarginPxdoubleThe marker's own or the manager's
OffscreenMarginPx.apparentPxdoubleThe marker's apparent on-screen size in px, from SizePx(double, double, bool, bool).
Returns
- double
Margin in screen px to test the anchor's projected point against.
Remarks
A prefab marker's anchor is its geographic point at ground level
(the base of the Eiffel Tower demo, altitude 0), and the
visibility test compares only that one projected point against
the viewport plus OffscreenMarginPx. A model with real
physical height extends well above its own anchor on screen, so
the unwidened test could hide the whole model - including a tall
upper structure still fully on screen - the moment its base
point alone crossed the margin.
The widening is the full apparent size, not half of it: a
prefab's own local geometry can sit entirely to one side of its
anchor rather than being centred on it (the Eiffel Tower demo's
boxes span local Y 0 to 1, all above the anchor at the base), and
this overload assumes no per-prefab bounds - it is the fallback
used when a marker's real visual extent is not known (radius 1,
matching the pre-bounds behavior), which is the only bound that
covers both the base-anchored and centred case without
under-widening. When a marker's real extent is known (the
Runtime tier's WorldMarkerInstance.VisualBoundsRadius), call the
3-arg overload instead.
VisibilityMarginPx(double, double, double)
Offscreen-visibility margin widened by a marker's own apparent on-screen size times its real visual-bounds radius, so a model whose geometry reaches further from its anchor than one local unit (a wide flag, say) gets a proportionally wider margin instead of the flat "assume 1 local unit" fallback.
public static double VisibilityMarginPx(double configuredMarginPx, double apparentPx, double visualBoundsRadius)
Parameters
configuredMarginPxdoubleThe marker's own or the manager's
OffscreenMarginPx.apparentPxdoubleThe marker's apparent on-screen size in px, from SizePx(double, double, bool, bool).
visualBoundsRadiusdoubleMax distance in prefab-local units from the anchor to the marker's render bounds.
Returns
- double
Margin in screen px to test the anchor's projected point against.
Remarks
See the 2-arg overload for the base widening rationale. This
overload replaces its implicit radius of 1 with the caller's own
measured visualBoundsRadius: the max distance
in prefab-local units from the anchor to any renderer's bounds
corner (the Runtime tier's WorldMarkerInstance.VisualBoundsRadius).
One multiply, same as the 2-arg form once radius is folded in, so
this costs nothing extra in the per-frame loop.