Visibility Analysis
Main Functions
AsteroidShapeModels.build_face_visibility_graph!
— Functionbuild_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:
- Pre-filter candidate faces based on normal orientations
- Sort candidates by distance for efficient occlusion testing
- Check visibility between face pairs using ray-triangle intersection
- 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
AsteroidShapeModels.compute_face_max_elevations!
— Functioncompute_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:
- Get all faces j visible from face i (from
shape.face_visibility_graph
) - 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)
- Check maximum elevation angles on all three edges using
- Store the maximum elevation angle found
AsteroidShapeModels.view_factor
— Functionview_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 icⱼ::StaticVector{3}
: Center position of face jn̂ᵢ::StaticVector{3}
: Unit normal vector of face in̂ⱼ::StaticVector{3}
: Unit normal vector of face jaⱼ::Real
: Area of face j
Returns
fᵢⱼ::Real
: View factor from face i to face jdᵢⱼ::Real
: Distance between face centersd̂ᵢⱼ::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)
△ --> △
Illumination Analysis
AsteroidShapeModels.isilluminated
— Functionisilluminated(
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 asteroidr☉
: Sun's position in the asteroid-fixed frameface_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 (requiresface_visibility_graph
andface_max_elevations
)
use_elevation_optimization::Bool
: Whether to use elevation-based early-out optimization (default:true
).- Only applies when
with_self_shadowing=true
- Only applies when
Returns
true
if the face is illuminatedfalse
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
AsteroidShapeModels.update_illumination!
— Functionupdate_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 asteroidr☉
: 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 (requiresface_visibility_graph
andface_max_elevations
)
use_elevation_optimization::Bool
: Whether to use elevation-based early-out optimization (default:true
).- Only applies when
with_self_shadowing=true
- Only applies when
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
AsteroidShapeModels.apply_eclipse_shadowing!
— Functionapply_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.
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.
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 frameR₁₂
: 3×3 rotation matrix fromshape1
frame toshape2
framet₁₂
: 3D translation vector fromshape1
frame toshape2
frameshape2
: Occluding shape model that may cast shadows onshape1
(must have BVH built viabuild_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
ifshape2
does not have BVH built. Callbuild_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:
Behind Check: If the occluding body is entirely behind the target relative to the sun, no eclipse can occur.
Lateral Separation Check: If bodies are too far apart laterally (perpendicular to sun direction), no eclipse can occur.
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:
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.
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
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.
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.
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.
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.
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 onshape1
(must have BVH built)r☉₁
: Sun's position in shape1's framer₁₂
: Shape2's position in shape1's frame (e.g., secondary's position from SPICE)R₁₂
: 3×3 rotation matrix fromshape1
frame toshape2
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
ifshape2
does not have BVH built. Callbuild_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
AsteroidShapeModels.EclipseStatus
— TypeEclipseStatus
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).
FaceVisibilityGraph Functions
AsteroidShapeModels.get_visible_face_indices
— Functionget_visible_face_indices(graph::FaceVisibilityGraph, face_idx::Int) -> SubArray
Get indices of faces visible from the specified face.
Arguments
graph
: Face visibility graphface_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
AsteroidShapeModels.get_view_factors
— Functionget_view_factors(graph::FaceVisibilityGraph, face_idx::Int) -> SubArray
Get view factors from the specified face to all its visible faces.
Arguments
graph
: Face visibility graphface_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
AsteroidShapeModels.get_visible_face_distances
— Functionget_visible_face_distances(graph::FaceVisibilityGraph, face_idx::Int) -> SubArray
Get distances to visible faces from the specified face.
Arguments
graph
: Face visibility graphface_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
AsteroidShapeModels.get_visible_face_directions
— Functionget_visible_face_directions(graph::FaceVisibilityGraph, face_idx::Int) -> SubArray
Get unit direction vectors to visible faces from the specified face.
Arguments
graph
: Face visibility graphface_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
AsteroidShapeModels.get_visible_face_data
— Functionget_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 graphface_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 faceview_factor
: View factor between the facesdistance
: Distance between face centersdirection
: 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
AsteroidShapeModels.num_visible_faces
— Functionnum_visible_faces(graph::FaceVisibilityGraph, face_idx::Int) -> Int
Get the number of visible faces for the specified face.
Arguments
graph
: Face visibility graphface_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