Visibility Analysis

Main Functions

AsteroidShapeModels.build_face_visibility_graph!Function
build_face_visibility_graph!(shape::ShapeModel)

Build face-to-face visibility graph for the shape model.

This function computes which faces are visible from each face and stores the results in a FaceVisibilityGraph structure using CSR (Compressed Sparse Row) format.

Arguments

  • shape : Shape model of an asteroid

Algorithm

The implementation uses an optimized non-BVH algorithm with candidate filtering:

  1. Pre-filter candidate faces based on normal orientations
  2. Sort candidates by distance for efficient occlusion testing
  3. Check visibility between face pairs using ray-triangle intersection
  4. Store results in memory-efficient CSR format

Performance Considerations

  • BVH acceleration was found to be less efficient for face visibility pair searches compared to the optimized candidate filtering approach (slower ~0.5x)
  • The non-BVH implementation with distance-based sorting provides better performance due to the specific nature of face-to-face visibility queries
  • Distance-based sorting provides ~2x speedup over naive approaches

Notes

  • The visibility graph is stored in shape.face_visibility_graph
  • This is a computationally intensive operation, especially for large models
  • The resulting graph contains view factors, distances, and direction vectors
source
AsteroidShapeModels.compute_face_max_elevations!Function
compute_face_max_elevations!(shape::ShapeModel)

Compute the maximum elevation angle for each face based on the face visibility graph.

Description

For each face, this function calculates the maximum elevation angle from which the face can potentially be shadowed by any visible face. If the sun's elevation is higher than this angle, the face is guaranteed to be illuminated (if facing the sun).

The elevation is calculated as the angle between the horizon plane (perpendicular to the face normal) and the direction to the highest visible face.

Arguments

  • shape: ShapeModel with facevisibilitygraph already built

Returns

  • Nothing (modifies shape.face_max_elevations in-place)

Algorithm

For each face i:

  1. Get all faces j visible from face i (from shape.face_visibility_graph)
  2. For each visible face j:
    • Check maximum elevation angles on all three edges using compute_edge_max_elevation
    • Note: compute_edge_max_elevation handles both edge interiors and endpoints (vertices)
  3. Store the maximum elevation angle found
source
AsteroidShapeModels.view_factorFunction
view_factor(cᵢ, cⱼ, n̂ᵢ, n̂ⱼ, aⱼ) -> fᵢⱼ, dᵢⱼ, d̂ᵢⱼ

Calculate the view factor from face i to face j, assuming Lambertian emission.

Arguments

  • cᵢ::StaticVector{3}: Center position of face i
  • cⱼ::StaticVector{3}: Center position of face j
  • n̂ᵢ::StaticVector{3}: Unit normal vector of face i
  • n̂ⱼ::StaticVector{3}: Unit normal vector of face j
  • aⱼ::Real : Area of face j

Returns

  • fᵢⱼ::Real: View factor from face i to face j
  • dᵢⱼ::Real: Distance between face centers
  • d̂ᵢⱼ::StaticVector{3}: Unit direction vector from face i to face j

Notes

The view factor is calculated using the formula:

fᵢⱼ = (cosθᵢ * cosθⱼ) / (π * dᵢⱼ²) * aⱼ

where θᵢ and θⱼ are the angles between the line connecting the faces and the respective normal vectors.

The view factor is automatically zero when:

  • Face i is facing away from face j (cosθᵢ ≤ 0)
  • Face j is facing away from face i (cosθⱼ ≤ 0)
  • Both conditions ensure that only mutually visible faces have non-zero view factors

Visual representation

(i)   fᵢⱼ   (j)
 △    -->    △
source

Illumination Analysis

AsteroidShapeModels.isilluminatedFunction
isilluminated(
    shape::ShapeModel, r☉::StaticVector{3}, face_idx::Integer; 
    with_self_shadowing::Bool, use_elevation_optimization::Bool=true,
) -> Bool

Check if a face is illuminated by the sun.

Arguments

  • shape : Shape model of an asteroid
  • r☉ : Sun's position in the asteroid-fixed frame
  • face_idx : Index of the face to be checked

Keyword Arguments

  • with_self_shadowing::Bool : Whether to include self-shadowing effects.
    • false: Use pseudo-convex model (face orientation only)
    • true: Include self-shadowing (requires face_visibility_graph and face_max_elevations)
  • use_elevation_optimization::Bool : Whether to use elevation-based early-out optimization (default: true).
    • Only applies when with_self_shadowing=true

Returns

  • true if the face is illuminated
  • false if the face is in shadow or facing away from the sun

Performance

  • Pseudo-convex model: O(1) - single dot product
  • With self-shadowing: O(n) worst case, but typically much faster due to:
    • Early-out optimization using face orientation check
    • Visibility graph limits checks to potentially occluding faces only

Examples

# Without self-shadowing (pseudo-convex model)
illuminated = isilluminated(shape, sun_position, face_idx; with_self_shadowing=false)

# With self-shadowing (requires face_visibility_graph)
illuminated = isilluminated(shape, sun_position, face_idx; with_self_shadowing=true)

See also: update_illumination! for batch processing

source
AsteroidShapeModels.update_illumination!Function
update_illumination!(
    illuminated_faces::AbstractVector{Bool}, shape::ShapeModel, r☉::StaticVector{3}; 
    with_self_shadowing::Bool, use_elevation_optimization::Bool=true,
)

Update illumination state for all faces of a shape model.

Arguments

  • illuminated_faces : Boolean vector to store illumination state (must have length equal to number of faces)
  • shape : Shape model of an asteroid
  • r☉ : Sun's position in the asteroid-fixed frame

Keyword Arguments

  • with_self_shadowing::Bool : Whether to include self-shadowing effects.
    • false: Use pseudo-convex model (face orientation only)
    • true: Include self-shadowing (requires face_visibility_graph and face_max_elevations)
  • use_elevation_optimization::Bool : Whether to use elevation-based early-out optimization (default: true).
    • Only applies when with_self_shadowing=true

Performance

  • Pseudo-convex model: O(n) where n is number of faces
  • With self-shadowing: O(n²) worst case, but typically O(n·k) where k is average visible faces per face

Examples

# Prepare illumination vector
illuminated_faces = Vector{Bool}(undef, length(shape.faces))

# Without self-shadowing (pseudo-convex model)
update_illumination!(illuminated_faces, shape, sun_position; with_self_shadowing=false)

# With self-shadowing
update_illumination!(illuminated_faces, shape, sun_position; with_self_shadowing=true)

See also: isilluminated for single face checks, apply_eclipse_shadowing! for binary asteroid shadowing

source
AsteroidShapeModels.apply_eclipse_shadowing!Function
apply_eclipse_shadowing!(
    illuminated_faces::AbstractVector{Bool}, shape1::ShapeModel, r☉₁::StaticVector{3}, 
    R₁₂::StaticMatrix{3,3}, t₁₂::StaticVector{3}, shape2::ShapeModel
) -> EclipseStatus

Apply eclipse shadowing effects from another shape onto already illuminated faces.

Deprecated

This function signature will be removed in v0.5.0. Please use the new signature: apply_eclipse_shadowing!(illuminated_faces, shape1, shape2, r☉₁, r₁₂, R₁₂) which directly accepts shape2's position instead of the transformation parameter.

Note

As of v0.4.0, shape2 must have BVH pre-built before calling this function. Use either with_bvh=true when loading or call build_bvh!(shape2) explicitly.

Arguments

  • illuminated_faces : Boolean vector with current illumination state (will be modified)
  • shape1 : Target shape model being shadowed (the shape receiving shadows)
  • r☉₁ : Sun's position in shape1's frame
  • R₁₂ : 3×3 rotation matrix from shape1 frame to shape2 frame
  • t₁₂ : 3D translation vector from shape1 frame to shape2 frame
  • shape2 : Occluding shape model that may cast shadows on shape1 (must have BVH built via build_bvh!)

Returns

  • NO_ECLIPSE: No eclipse occurs (bodies are misaligned).
  • PARTIAL_ECLIPSE: Some faces that were illuminated are now in shadow by the occluding body.
  • TOTAL_ECLIPSE: All faces that were illuminated are now in shadow.

Throws

  • ArgumentError if shape2 does not have BVH built. Call build_bvh!(shape2) before using this function.

Description

This function ONLY checks for mutual shadowing (eclipse) effects. It assumes that the illuminated_faces vector already contains the result of face orientation and/or self-shadowing checks. Only faces marked as true in the input will be tested for occlusion by the other body.

This separation allows flexible control of shadowing effects in thermal modeling:

  • Call update_illumination_* first for self-shadowing (or face orientation only)
  • Then call this function to add mutual shadowing effects

Performance Optimizations

The function includes early-out checks at two levels:

Body-level optimizations:

  1. Behind Check: If the occluding body is entirely behind the target relative to the sun, no eclipse can occur.

  2. Lateral Separation Check: If bodies are too far apart laterally (perpendicular to sun direction), no eclipse can occur.

  3. Total Eclipse Check: If the target is completely within the occluding body's shadow, all illuminated faces are set to false without individual ray checks.

Face-level optimizations:

  1. Ray-Sphere Intersection Check: For each face, checks if the ray to the sun can possibly intersect the occluding body's bounding sphere. Skips ray-shape test if the ray clearly misses the sphere.

  2. Inscribed Sphere Check: If the ray passes through the occluding body's inscribed sphere, the face is guaranteed to be shadowed, avoiding the expensive ray-shape intersection test.

These optimizations use maximum_radius and minimum_radius for accurate sphere calculations.

Coordinate Systems

The transformation from shape1 frame to shape2 frame is given by: p_shape2 = R₁₂ * p_shape1 + t₁₂

Example

# Check self-shadowing first
update_illumination!(illuminated_faces1, shape1, sun_position1; with_self_shadowing=true)
update_illumination!(illuminated_faces2, shape2, sun_position2; with_self_shadowing=true)

# Or if you want to ignore self-shadowing:
update_illumination!(illuminated_faces1, shape1, sun_position1; with_self_shadowing=false)
update_illumination!(illuminated_faces2, shape2, sun_position2; with_self_shadowing=false)

# Then check eclipse shadowing
# For checking mutual shadowing, apply to both shape1 and shape2:
status1 = apply_eclipse_shadowing!(illuminated_faces1, shape1, sun_position1, R12, t12, shape2)
status2 = apply_eclipse_shadowing!(illuminated_faces2, shape2, sun_position2, R21, t21, shape1)

# Handle eclipse status
if status1 == NO_ECLIPSE
    println("Shape1 is not eclipsed by shape2.")
elseif status1 == PARTIAL_ECLIPSE
    println("Shape1 is partially eclipsed by shape2.")
elseif status1 == TOTAL_ECLIPSE
    println("Shape1 is totally eclipsed by shape2.")
end
source
apply_eclipse_shadowing!(
    illuminated_faces::AbstractVector{Bool}, shape1::ShapeModel, shape2::ShapeModel,
    r☉₁::StaticVector{3}, r₁₂::StaticVector{3}, R₁₂::StaticMatrix{3,3}
) -> EclipseStatus

Apply eclipse shadowing effects from shape2 onto shape1's already illuminated faces.

This is the recommended API as of v0.4.1, with more intuitive parameter ordering and direct use of shape2's position.

Note

As of v0.4.0, shape2 must have BVH pre-built before calling this function. Use either with_bvh=true when loading or call build_bvh!(shape2) explicitly.

New in v0.4.1

This function signature directly accepts r₁₂ (shape2's position in shape1's frame), which is more intuitive when working with SPICE data. The older signature using t₁₂ is maintained for backward compatibility but will be removed in v0.5.0.

OPTIMIZE

Current implementation calls intersect_ray_shape per face, causing ~200 allocations per call. For binary asteroid thermophysical simulations, this results in ~200 allocations × 2 bodies × number of time steps. Future optimization should implement true batch ray tracing for mutual shadowing to reduce allocation overhead.

TODO
  • Parallel processing: Add multi-threading support using @threads for face-level calculations. Each face's shadow test is independent, making this function ideal for parallelization.

  • Spatial optimization: Implement spatial data structures (e.g., octree) to pre-filter faces that could potentially be shadowed, reducing unnecessary ray tests.

  • Caching for temporal coherence: For simulations with small time steps, implement caching to reuse shadow information from previous time steps when relative positions change gradually.

Arguments

  • illuminated_faces : Boolean vector with current illumination state (will be modified)
  • shape1 : Target shape model being shadowed (the shape receiving shadows)
  • shape2 : Occluding shape model that may cast shadows on shape1 (must have BVH built)
  • r☉₁ : Sun's position in shape1's frame
  • r₁₂ : Shape2's position in shape1's frame (e.g., secondary's position from SPICE)
  • R₁₂ : 3×3 rotation matrix from shape1 frame to shape2 frame

Returns

  • NO_ECLIPSE: No eclipse occurs (bodies are misaligned).
  • PARTIAL_ECLIPSE: Some faces that were illuminated are now in shadow by the occluding body.
  • TOTAL_ECLIPSE: All faces that were illuminated are now in shadow.

Throws

  • ArgumentError if shape2 does not have BVH built. Call build_bvh!(shape2) before using this function.

Description

This function ONLY checks for mutual shadowing (eclipse) effects. It assumes that the illuminated_faces vector already contains the result of face orientation and/or self-shadowing checks. Only faces marked as true in the input will be tested for occlusion by the other body.

Example with SPICE integration

# Get positions and orientations from SPICE
et = ...               # Ephemeris time
sun_pos1 = ...         # Sun's position in primary's frame
secondary_pos = ...    # Secondary's position in primary's frame  
P2S = ...              # Rotation matrix from primary to secondary frame

# Calcuate required transformation
sun_pos2 = P2S * sun_pos1           # Sun's position in secondary's frame
S2P = P2S'                          # Inverse rotation
primary_pos = -S2P * secondary_pos  # Primary's position in secondary's frame

# Check self-shadowing first
update_illumination!(illuminated_faces1, shape1, sun_pos1; with_self_shadowing=true)
update_illumination!(illuminated_faces2, shape2, sun_pos2; with_self_shadowing=true)

# For primary eclipsed by secondary
status1 = apply_eclipse_shadowing!(illuminated_faces1, shape1, shape2, sun_pos1, secondary_pos, P2S)

# For secondary eclipsed by primary
status2 = apply_eclipse_shadowing!(illuminated_faces2, shape2, shape1, sun_pos2, primary_pos, S2P)

See also: update_illumination!, EclipseStatus

source
AsteroidShapeModels.EclipseStatusType
EclipseStatus

Enum representing the eclipse status between binary pairs.

Values

  • NO_ECLIPSE : No eclipse occurs (bodies are misaligned).
  • PARTIAL_ECLIPSE : Some faces are eclipsed by the occluding body.
  • TOTAL_ECLIPSE : All illuminated faces are eclipsed (complete shadow).
source

FaceVisibilityGraph Functions

AsteroidShapeModels.get_visible_face_indicesFunction
get_visible_face_indices(graph::FaceVisibilityGraph, face_idx::Int) -> SubArray

Get indices of faces visible from the specified face.

Arguments

  • graph : Face visibility graph
  • face_idx: Source face index (1-based)

Returns

  • SubArray{Int}: View of face indices that are visible from the source face

Example

# Get all faces visible from face 100
visible_face_indices = get_visible_face_indices(graph, 100)
println("Face 100 can see $(length(visible_face_indices)) faces")

# Iterate over visible faces
for j in visible_face_indices
    println("Face 100 can see face $j")
end

See also: get_view_factors, get_visible_face_data

source
AsteroidShapeModels.get_view_factorsFunction
get_view_factors(graph::FaceVisibilityGraph, face_idx::Int) -> SubArray

Get view factors from the specified face to all its visible faces.

Arguments

  • graph : Face visibility graph
  • face_idx : Source face index (1-based)

Returns

  • SubArray{Float64}: View of view factors corresponding to visible faces

Notes

The returned array has the same length and ordering as get_visible_face_indices. View factors represent the fraction of radiation leaving face face_idx that directly reaches each visible face.

Example

visible_face_indices = get_visible_face_indices(graph, 100)
view_factors = get_view_factors(graph, 100)

# Sum of view factors (always between 0 and 1)
# - Near 0 for convex shapes (most radiation escapes to space)  
# - Larger for concave regions (more reabsorption due to facing surfaces)
total_vf = sum(view_factors)
println("Total view factor from face 100: $total_vf")

# Pair indices with view factors
for (j, vf) in zip(visible_face_indices, view_factors)
    println("Face 100 -> Face $j: view factor = $vf")
end

See also: get_visible_face_indices, view_factor

source
AsteroidShapeModels.get_visible_face_distancesFunction
get_visible_face_distances(graph::FaceVisibilityGraph, face_idx::Int) -> SubArray

Get distances to visible faces from the specified face.

Arguments

  • graph : Face visibility graph
  • face_idx : Source face index (1-based)

Returns

  • SubArray{Float64}: View of distances (in meters) from face center to visible face centers

Notes

The returned array has the same length and ordering as get_visible_face_indices. Distances are computed between face centers during visibility graph construction.

Example

visible_face_indices = get_visible_face_indices(graph, 100)
distances = get_visible_face_distances(graph, 100)

# Find the closest visible face
min_dist, idx = findmin(distances)
closest_face_idx = visible_face_indices[idx]
println("Closest visible face to face 100 is face $closest_face_idx at distance $min_dist m")

See also: get_visible_face_indices, get_visible_face_directions

source
AsteroidShapeModels.get_visible_face_directionsFunction
get_visible_face_directions(graph::FaceVisibilityGraph, face_idx::Int) -> SubArray

Get unit direction vectors to visible faces from the specified face.

Arguments

  • graph : Face visibility graph
  • face_idx : Source face index (1-based)

Returns

  • SubArray{SVector{3,Float64}}: View of unit direction vectors from source face center to visible face centers

Notes

The returned array has the same length and ordering as get_visible_face_indices. Each vector points from the source face center to a visible face center and has unit length.

Example

visible_face_indices = get_visible_face_indices(graph, 100)
directions = get_visible_face_directions(graph, 100)

# Calculate angle to each visible face
face_normal = shape.face_normals[100]
for (j, dir) in zip(visible_face_indices, directions)
    angle = acosd(face_normal ⋅ dir)  # Angle in degrees
    println("Face 100 -> Face $j: angle = $(round(angle, digits=1))°")
end

See also: get_visible_face_indices, get_visible_face_distances

source
AsteroidShapeModels.get_visible_face_dataFunction
get_visible_face_data(graph::FaceVisibilityGraph, face_idx::Int, idx::Int)

Get the idx-th visible face data for the specified face.

Arguments

  • graph : Face visibility graph
  • face_idx : Source face index (1-based)
  • idx : Index within the visible faces list (1-based)

Returns

Named tuple with fields:

  • face_idx : Index of the visible face
  • view_factor : View factor between the faces
  • distance : Distance between face centers
  • direction : Unit direction vector from source to visible face

Example

# Get the third visible face from face 100
data = get_visible_face_data(graph, 100, 3)
println("Face 100 can see face $(data.face_idx) with view factor $(data.view_factor)")

See also: get_visible_face_indices, get_view_factors

source
AsteroidShapeModels.num_visible_facesFunction
num_visible_faces(graph::FaceVisibilityGraph, face_idx::Int) -> Int

Get the number of visible faces for the specified face.

Arguments

  • graph : Face visibility graph
  • face_idx : Source face index (1-based)

Returns

  • Int: Number of faces visible from the source face

Example

# Count visible faces for each face
for i in 1:graph.nfaces
    n = num_visible_faces(graph, i)
    if n > 100
        println("Face $i can see $n other faces.")
    end
end

# Find the face with most visible faces
max_visible = 0
max_face = 0
for i in 1:graph.nfaces
    n = num_visible_faces(graph, i)
    if n > max_visible
        max_visible = n
        max_face = i
    end
end
println("Face $max_face has the most visible faces: $max_visible")

See also: get_visible_face_indices, FaceVisibilityGraph

source