Skip to content

Surface projection (k=0 ray-casting)

The first algorithmic step turns an unstructured triangulated STL into a structured array of surface nodes by ray-casting from a single anchor point through a set of pre-chosen direction vectors onto the body. The output is the k=0 layer that all subsequent stages build on.

Inputs and outputs

InputA watertight trimesh.Trimesh and a set of unit direction vectors dirs of shape (N, 3) from a single anchor inside the body.
OutputA (N, 3) array of intersection points on the body surface, one per ray.
Moduleshore.surface.projector.Projector

Anchor selection

The anchor is the common origin of all rays. SHORE places it at the body centroid by default (Body.from_stl), with a mesh.contains check that the chosen point sits inside the closed surface. The body must be star-shaped from the anchor: every ray from the anchor must hit the body exactly once. This rules out concave geometries with re-entrant features visible from the centroid; for those, you'd need to pre-decompose the body into star-shaped pieces.

Anchor and ray-cast

The diagram shows a 2D cross-section: the anchor (red) is inside a slightly distorted blob; rays fan outward; each ray's first intersection with the body is a k=0 node (blue).

Ray-cast machinery

Projector.__init__ builds a trimesh.RayMeshIntersector once and caches it. Subsequent Projector.project(dirs) calls reuse the same acceleration structure (BVH), so the per-ray cost is O(log T) in the triangle count.

python
from shore.surface.projector import Projector

proj = Projector(mesh, anchor)        # builds BVH once
result = proj.project(dirs)           # ray-cast, status array per ray
hits   = result.locations             # (N, 3) hit points
status = result.status                # 'hit' / 'miss' / 'tooNear' / ...

The ProjectionResult.status field flags rays that miss the body, hit too close to the anchor (likely the anchor itself if it grazes a face), or hit too far. By default Projector.project(dirs, on_failure="raise") raises MeshInputError if any ray is non-HIT; pass "warn" or "none" to relax.

Direction vectors

The projector itself is topology-agnostic — it casts whatever directions you give it. The directions for the cubed-sphere topology come from two places:

  1. The cap k=0 builder (shore.surface.cap.build_cap_k0) computes directions for a flat tangent-plane diamond at each pole and feeds them through the projector. See k=0 mesh construction.
  2. The equator builder (shore.surface.equator.build_equator) computes per-meridian slerp-interpolated directions from south seam to north seam and feeds the interior nodes through the projector. The seam nodes themselves are bit-exact copies of the cap edges, so they skip the projector round-trip.

For the legacy single-block path, shore.surface.remesh.sphere_latlon_directions generates a uniform lat-lon grid of directions that Body.project() ray-casts as a single batch.

Performance

A single Projector.project(dirs) call vectorises over all rays. For a 20k-triangle STL with a 60×60 cap (3600 rays) the projection takes single-digit milliseconds on a modern CPU. The BVH build is the dominant cost on first use, ~30 ms for the same triangle count.

Failure modes

  • Anchor outside the body: mesh.contains([anchor]) returns False; raises MeshInputError at Body.from_stl.
  • Body not star-shaped from anchor: some rays miss, some hit twice (only the first hit is recorded). Projector.project raises with the MISS status by default.
  • Mesh not watertight: rays may pass through gaps in the surface; raises with the TOO_FAR status.

See also

Released under the MIT License.