Table of Contents

Class MarkerPlacement

Namespace
LansMap.Core.Markers

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
object
MarkerPlacement

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

planeDistance double
perspectiveW double

Returns

double

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

ndcAtPlaneDistance double

NDC z the camera reports for a point at view depth planeDistance.

ndcAtTwicePlaneDistance double

NDC z the camera reports for a point at view depth 2*planeDistance.

ndcAtBiasedDistance double

NDC z the camera reports one tie-bias depth closer than planeDistance. Its difference from ndcAtPlaneDistance is the tie bias itself, magnitude and sign.

planeDistance double

View depth of the map plane at the view center.

orthographicCamera bool

True 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

a double

Coefficient A: the w term of the shader's z numerator.

b double

Coefficient B: the constant term of the shader's z numerator.

c double

Coefficient C: the w^2 term of the shader's z numerator.

Returns

double

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

centerRows RenderMatrixRows
anchorRelativeX double
relativeY double
altitudeWCoefficient double
altitudeMercator double

Returns

double

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

px double
perspectiveW double
sceneSize bool

Returns

double

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

px double
perspectiveW double
sceneSize bool
orthographicCamera bool

Returns

double

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

configuredMarginPx double

The marker's own or the manager's OffscreenMarginPx.

apparentPx double

The 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

configuredMarginPx double

The marker's own or the manager's OffscreenMarginPx.

apparentPx double

The marker's apparent on-screen size in px, from SizePx(double, double, bool, bool).

visualBoundsRadius double

Max 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.