Current section
Files
Jump to
Current section
Files
src/expresso@collision.erl
-module(expresso@collision).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/expresso/collision.gleam").
-export([build_bvh/1, detect_collisions_with_bvh/2, detect_collisions/1, detect_collisions_with_index/2, bodies_to_bvh_items/1, extract_bodies_from_results/2, filter_by_radius/3, filter_by_region/3, find_nearest/2, analyze_distribution/1, calculate_world_bounds/1, raycast/6]).
-export_type([spatial_index/1, contact/1, raycast_hit/1]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Collision detection with spatial acceleration.\n"
"\n"
" This module provides efficient collision detection using spatial partitioning\n"
" data structures from the `spatial` library.\n"
"\n"
" ## Features\n"
"\n"
" - **Multi-shape support**: Sphere, Box, Capsule, and Cylinder colliders\n"
" - **Spatial acceleration**: BVH (O(n log n)) and Grid (O(c + k))\n"
" - **Adaptive selection**: Auto-choose BVH or Grid based on distribution\n"
" - **Accurate detection**: Uses `spatial` library's collision algorithms\n"
"\n"
" ## Collision Detection Strategies\n"
"\n"
" ### Brute Force (< 10 bodies)\n"
" - Complexity: O(n²)\n"
" - Used automatically for small scenes\n"
" - No spatial index overhead\n"
"\n"
" ### BVH (10-1000+ bodies, dynamic)\n"
" - Complexity: O(n log n + k) where k = collision pairs\n"
" - Best for dynamic scenes with moving objects\n"
" - Handles non-uniform distributions well\n"
"\n"
" ### Grid (100-1000+ bodies, uniform)\n"
" - Complexity: O(c + k) where c = cells checked, k = collision pairs\n"
" - Best for particle systems and uniformly distributed bodies\n"
" - Excellent for 1000s of particles\n"
"\n"
" ## Usage\n"
"\n"
" Most users won't call this module directly - the `world` module handles\n"
" collision detection automatically. However, advanced users can use these\n"
" functions for custom collision detection:\n"
"\n"
" ```gleam\n"
" import expresso/collision\n"
" import gleam/dict\n"
"\n"
" // Simple collision detection (builds BVH internally)\n"
" let contacts = collision.detect_collisions(bodies)\n"
"\n"
" // With pre-built BVH (more efficient if reusing)\n"
" let bvh = collision.build_bvh(bodies)\n"
" let contacts = collision.detect_collisions_with_bvh(bodies, option.Some(bvh))\n"
" ```\n"
).
-type spatial_index(MAH) :: {b_v_h_index, spatial@bvh:b_v_h(MAH)} |
{grid_index, spatial@grid:grid(MAH)}.
-type contact(MAI) :: {contact,
MAI,
MAI,
vec@vec3:vec3(float()),
float(),
vec@vec3:vec3(float())}.
-type raycast_hit(MAJ) :: {raycast_hit,
MAJ,
expresso@body:body(MAJ),
vec@vec3:vec3(float()),
vec@vec3:vec3(float()),
float()}.
-file("src/expresso/collision.gleam", 147).
?DOC(" Internal: Build BVH from body list\n").
-spec build_bvh_internal(list({MBH, expresso@body:body(MBH)})) -> {ok,
spatial@bvh:b_v_h({MBH, expresso@body:body(MBH)})} |
{error, nil}.
build_bvh_internal(Body_list) ->
Bvh_items = gleam@list:map(
Body_list,
fun(Pair) ->
{Id, Body} = Pair,
Pos_3d = {vec3,
erlang:element(2, erlang:element(3, Body)),
erlang:element(3, erlang:element(3, Body)),
+0.0},
{Pos_3d, {Id, Body}}
end
),
spatial@bvh:from_items(Bvh_items, 8).
-file("src/expresso/collision.gleam", 140).
?DOC(" Build a BVH spatial index from bodies\n").
-spec build_bvh(gleam@dict:dict(MAZ, expresso@body:body(MAZ))) -> {ok,
spatial@bvh:b_v_h({MAZ, expresso@body:body(MAZ)})} |
{error, nil}.
build_bvh(Bodies) ->
build_bvh_internal(maps:to_list(Bodies)).
-file("src/expresso/collision.gleam", 298).
?DOC(
" Check if two bodies are colliding using their colliders\n"
"\n"
" Uses the spatial library's collider.intersects() for accurate collision detection.\n"
" Respects collision layers - bodies only collide if their layers match.\n"
).
-spec check_collision(expresso@body:body(MCZ), expresso@body:body(MCZ)) -> {ok,
contact(MCZ)} |
{error, nil}.
check_collision(Body_a, Body_b) ->
case expresso@body:should_collide(Body_a, Body_b) of
false ->
{error, nil};
true ->
Collider_a = expresso@body:world_collider(Body_a),
Collider_b = expresso@body:world_collider(Body_b),
case spatial@collider:intersects(Collider_a, Collider_b) of
false ->
{error, nil};
true ->
Center_a = spatial@collider:center(Collider_a),
Center_b = spatial@collider:center(Collider_b),
Delta = vec@vec3f:subtract(Center_b, Center_a),
Distance_3d = vec@vec3f:length(Delta),
Size_a = spatial@collider:size(Collider_a),
Size_b = spatial@collider:size(Collider_b),
Radius_a = gleam@float:max(
erlang:element(2, Size_a),
erlang:element(3, Size_a)
)
/ 2.0,
Radius_b = gleam@float:max(
erlang:element(2, Size_b),
erlang:element(3, Size_b)
)
/ 2.0,
Penetration = (Radius_a + Radius_b) - Distance_3d,
Normal = case Distance_3d > 0.0001 of
true ->
vec@vec3f:scale(Delta, case Distance_3d of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> 1.0 / Gleam@denominator
end);
false ->
{vec3, 1.0, +0.0, +0.0}
end,
Contact_point = vec@vec3f:add(
Center_a,
vec@vec3f:scale(Delta, 0.5)
),
{ok,
{contact,
erlang:element(2, Body_a),
erlang:element(2, Body_b),
Normal,
Penetration,
Contact_point}}
end
end.
-file("src/expresso/collision.gleam", 161).
?DOC(" Brute force collision detection for small body counts\n").
-spec detect_collisions_brute_force(list({MBO, expresso@body:body(MBO)})) -> list(contact(MBO)).
detect_collisions_brute_force(Body_list) ->
gleam@list:flat_map(
Body_list,
fun(Pair_a) ->
{Id_a, Body_a} = Pair_a,
gleam@list:filter_map(
Body_list,
fun(Pair_b) ->
{Id_b, Body_b} = Pair_b,
case Id_a =:= Id_b of
false ->
check_collision(Body_a, Body_b);
_ ->
{error, nil}
end
end
)
end
).
-file("src/expresso/collision.gleam", 349).
?DOC(" Calculate the maximum bounding radius among all bodies\n").
-spec calculate_max_radius(list({MDF, expresso@body:body(MDF)})) -> float().
calculate_max_radius(Body_list) ->
gleam@list:fold(
Body_list,
+0.0,
fun(Max_r, Pair) ->
{_, Body} = Pair,
gleam@float:max(Max_r, expresso@body:bounding_radius(Body))
end
).
-file("src/expresso/collision.gleam", 180).
?DOC(" BVH-accelerated collision detection\n").
-spec detect_collisions_with_bvh_internal(
list({MBT, expresso@body:body(MBT)}),
spatial@bvh:b_v_h({MBT, expresso@body:body(MBT)})
) -> list(contact(MBT)).
detect_collisions_with_bvh_internal(Body_list, Bvh_tree) ->
gleam@list:flat_map(
Body_list,
fun(Pair_a) ->
{Id_a, Body_a} = Pair_a,
Max_radius = calculate_max_radius(Body_list),
Query_radius = expresso@body:bounding_radius(Body_a) + Max_radius,
Pos_3d = {vec3,
erlang:element(2, erlang:element(3, Body_a)),
erlang:element(3, erlang:element(3, Body_a)),
+0.0},
Nearby = spatial@bvh:query_radius(Bvh_tree, Pos_3d, Query_radius),
gleam@list:filter_map(
Nearby,
fun(Candidate) ->
{_, {Id_b, Body_b}} = Candidate,
case Id_a =:= Id_b of
true ->
{error, nil};
false ->
case Id_a =:= Id_b of
false ->
check_collision(Body_a, Body_b);
_ ->
{error, nil}
end
end
end
)
end
).
-file("src/expresso/collision.gleam", 112).
?DOC(
" Detect collisions using an optional pre-built BVH for acceleration\n"
"\n"
" This is the recommended API for performance-critical code.\n"
).
-spec detect_collisions_with_bvh(
gleam@dict:dict(MAQ, expresso@body:body(MAQ)),
gleam@option:option(spatial@bvh:b_v_h({MAQ, expresso@body:body(MAQ)}))
) -> list(contact(MAQ)).
detect_collisions_with_bvh(Bodies, Bvh_option) ->
Body_list = maps:to_list(Bodies),
case Bvh_option of
{some, Bvh_tree} ->
detect_collisions_with_bvh_internal(Body_list, Bvh_tree);
none ->
case erlang:length(Body_list) < 10 of
true ->
detect_collisions_brute_force(Body_list);
false ->
Bvh_tree@1 = build_bvh_internal(Body_list),
case Bvh_tree@1 of
{error, nil} ->
[];
{ok, Tree} ->
detect_collisions_with_bvh_internal(Body_list, Tree)
end
end
end.
-file("src/expresso/collision.gleam", 105).
?DOC(
" Detect all collisions between bodies\n"
"\n"
" Returns a list of contacts for all overlapping pairs.\n"
" \n"
" **Performance:**\n"
" - For best performance, use `detect_collisions_with_index()` with a pre-built spatial index\n"
" - This function uses brute force or builds a BVH internally\n"
).
-spec detect_collisions(gleam@dict:dict(MAK, expresso@body:body(MAK))) -> list(contact(MAK)).
detect_collisions(Bodies) ->
detect_collisions_with_bvh(Bodies, none).
-file("src/expresso/collision.gleam", 231).
?DOC(" Detect collisions using BVH (optimized version with ID-based storage)\n").
-spec detect_collisions_with_bvh_new(
gleam@dict:dict(MCH, expresso@body:body(MCH)),
list({MCH, expresso@body:body(MCH)}),
spatial@bvh:b_v_h(MCH)
) -> list(contact(MCH)).
detect_collisions_with_bvh_new(Bodies, Body_list, Bvh_tree) ->
gleam@list:flat_map(
Body_list,
fun(Pair_a) ->
{Id_a, Body_a} = Pair_a,
Max_radius = calculate_max_radius(Body_list),
Query_radius = expresso@body:bounding_radius(Body_a) + Max_radius,
Pos_3d = {vec3,
erlang:element(2, erlang:element(3, Body_a)),
erlang:element(3, erlang:element(3, Body_a)),
+0.0},
Nearby = spatial@bvh:query_radius(Bvh_tree, Pos_3d, Query_radius),
gleam@list:filter_map(
Nearby,
fun(Candidate) ->
{_, Id_b} = Candidate,
case Id_a =:= Id_b of
true ->
{error, nil};
false ->
case gleam_stdlib:map_get(Bodies, Id_b) of
{ok, Body_b} ->
check_collision(Body_a, Body_b);
{error, _} ->
{error, nil}
end
end
end
)
end
).
-file("src/expresso/collision.gleam", 263).
?DOC(" Detect collisions using a Grid spatial index (with ID-based storage)\n").
-spec detect_collisions_with_grid_new(
gleam@dict:dict(MCQ, expresso@body:body(MCQ)),
list({MCQ, expresso@body:body(MCQ)}),
spatial@grid:grid(MCQ)
) -> list(contact(MCQ)).
detect_collisions_with_grid_new(Bodies, Body_list, Grid_index) ->
gleam@list:flat_map(
Body_list,
fun(Pair_a) ->
{Id_a, Body_a} = Pair_a,
Max_radius = calculate_max_radius(Body_list),
Query_radius = expresso@body:bounding_radius(Body_a) + Max_radius,
Pos_3d = {vec3,
erlang:element(2, erlang:element(3, Body_a)),
erlang:element(3, erlang:element(3, Body_a)),
+0.0},
Nearby = spatial@grid:query_radius(Grid_index, Pos_3d, Query_radius),
gleam@list:filter_map(
Nearby,
fun(Candidate) ->
{_, Id_b} = Candidate,
case Id_a =:= Id_b of
true ->
{error, nil};
false ->
case gleam_stdlib:map_get(Bodies, Id_b) of
{ok, Body_b} ->
check_collision(Body_a, Body_b);
{error, _} ->
{error, nil}
end
end
end
)
end
).
-file("src/expresso/collision.gleam", 218).
?DOC(" Detect collisions using a spatial index (BVH or Grid)\n").
-spec detect_collisions_with_index(
gleam@dict:dict(MCA, expresso@body:body(MCA)),
spatial_index(MCA)
) -> list(contact(MCA)).
detect_collisions_with_index(Bodies, Spatial_index) ->
Body_list = maps:to_list(Bodies),
case Spatial_index of
{b_v_h_index, Tree} ->
detect_collisions_with_bvh_new(Bodies, Body_list, Tree);
{grid_index, Grid} ->
detect_collisions_with_grid_new(Bodies, Body_list, Grid)
end.
-file("src/expresso/collision.gleam", 361).
?DOC(" Convert bodies to BVH items\n").
-spec bodies_to_bvh_items(list({binary(), expresso@body:body(MDI)})) -> list({vec@vec3:vec3(float()),
expresso@body:body(MDI)}).
bodies_to_bvh_items(Body_list) ->
gleam@list:map(
Body_list,
fun(Pair) ->
{_, Body} = Pair,
Pos_3d = {vec3,
erlang:element(2, erlang:element(3, Body)),
erlang:element(3, erlang:element(3, Body)),
+0.0},
{Pos_3d, Body}
end
).
-file("src/expresso/collision.gleam", 372).
?DOC(" Extract body IDs from query results and lookup bodies from dict\n").
-spec extract_bodies_from_results(
list({vec@vec3:vec3(float()), MDP}),
gleam@dict:dict(MDP, expresso@body:body(MDP))
) -> list({MDP, expresso@body:body(MDP)}).
extract_bodies_from_results(Results, Bodies) ->
gleam@list:filter_map(
Results,
fun(Result) ->
{_, Id} = Result,
case gleam_stdlib:map_get(Bodies, Id) of
{ok, Body} ->
{ok, {Id, Body}};
{error, _} ->
{error, nil}
end
end
).
-file("src/expresso/collision.gleam", 386).
?DOC(" Filter bodies by radius (brute force)\n").
-spec filter_by_radius(
list({MDW, expresso@body:body(MDW)}),
vec@vec3:vec3(float()),
float()
) -> list({MDW, expresso@body:body(MDW)}).
filter_by_radius(Body_list, Center, Radius) ->
gleam@list:filter(
Body_list,
fun(Pair) ->
{_, Body} = Pair,
Distance = vec@vec3f:distance(Center, erlang:element(3, Body)),
Distance =< (Radius + expresso@body:bounding_radius(Body))
end
).
-file("src/expresso/collision.gleam", 399).
?DOC(" Filter bodies by rectangular region (brute force)\n").
-spec filter_by_region(
list({MEC, expresso@body:body(MEC)}),
vec@vec3:vec3(float()),
vec@vec3:vec3(float())
) -> list({MEC, expresso@body:body(MEC)}).
filter_by_region(Body_list, Min, Max) ->
gleam@list:filter(
Body_list,
fun(Pair) ->
{_, Body} = Pair,
(((((erlang:element(2, erlang:element(3, Body)) >= erlang:element(
2,
Min
))
andalso (erlang:element(2, erlang:element(3, Body)) =< erlang:element(
2,
Max
)))
andalso (erlang:element(3, erlang:element(3, Body)) >= erlang:element(
3,
Min
)))
andalso (erlang:element(3, erlang:element(3, Body)) =< erlang:element(
3,
Max
)))
andalso (erlang:element(4, erlang:element(3, Body)) >= erlang:element(
4,
Min
)))
andalso (erlang:element(4, erlang:element(3, Body)) =< erlang:element(
4,
Max
))
end
).
-file("src/expresso/collision.gleam", 416).
?DOC(" Find the nearest body to a point\n").
-spec find_nearest(list({MEJ, expresso@body:body(MEJ)}), vec@vec3:vec3(float())) -> gleam@option:option({MEJ,
expresso@body:body(MEJ),
float()}).
find_nearest(Candidates, Point) ->
gleam@list:fold(
Candidates,
none,
fun(Best, Pair) ->
{Id, Body} = Pair,
Distance = vec@vec3f:distance(Point, erlang:element(3, Body)),
case Best of
none ->
{some, {Id, Body, Distance}};
{some, {_, _, Best_dist}} ->
case Distance < Best_dist of
true ->
{some, {Id, Body, Distance}};
false ->
Best
end
end
end
).
-file("src/expresso/collision.gleam", 471).
?DOC(" Calculate variance in positions (simplified)\n").
-spec calculate_position_variance(list(vec@vec3:vec3(float()))) -> float().
calculate_position_variance(Positions) ->
case erlang:length(Positions) of
0 ->
+0.0;
N ->
Sum = gleam@list:fold(
Positions,
{vec3, +0.0, +0.0, +0.0},
fun(Acc, Pos) -> vec@vec3f:add(Acc, Pos) end
),
Centroid = vec@vec3f:scale(Sum, case erlang:float(N) of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> 1.0 / Gleam@denominator
end),
Total_sq_dist = gleam@list:fold(
Positions,
+0.0,
fun(Acc@1, Pos@1) ->
Dist = vec@vec3f:distance(Centroid, Pos@1),
Acc@1 + (Dist * Dist)
end
),
case erlang:float(N) of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator@1 -> Total_sq_dist / Gleam@denominator@1
end
end.
-file("src/expresso/collision.gleam", 438).
?DOC(
" Analyze body distribution to determine if uniform\n"
"\n"
" Returns #(is_uniform, average_bounding_radius)\n"
).
-spec analyze_distribution(gleam@dict:dict(MEP, expresso@body:body(MEP))) -> {boolean(),
float()}.
analyze_distribution(Bodies) ->
Body_list = maps:to_list(Bodies),
Count = erlang:length(Body_list),
case Count of
0 ->
{false, 1.0};
_ ->
Total_radius = gleam@list:fold(
Body_list,
+0.0,
fun(Sum, Pair) ->
{_, Body} = Pair,
Sum + expresso@body:bounding_radius(Body)
end
),
Avg_radius = case erlang:float(Count) of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> Total_radius / Gleam@denominator
end,
Positions = gleam@list:map(
Body_list,
fun(Pair@1) ->
{_, Body@1} = Pair@1,
erlang:element(3, Body@1)
end
),
Variance = calculate_position_variance(Positions),
Is_uniform = Variance < 100.0,
{Is_uniform, Avg_radius}
end.
-file("src/expresso/collision.gleam", 495).
?DOC(" Calculate world bounds for Grid creation\n").
-spec calculate_world_bounds(gleam@dict:dict(MEV, expresso@body:body(MEV))) -> spatial@collider:internal_collider().
calculate_world_bounds(Bodies) ->
Body_list = maps:to_list(Bodies),
case Body_list of
[] ->
spatial@collider:box(
{vec3, -100.0, -100.0, -1.0},
{vec3, 100.0, 100.0, 1.0}
);
_ ->
Init_min = {vec3, 1.0e10, 1.0e10, 1.0e10},
Init_max = {vec3, -1.0e10, -1.0e10, -1.0e10},
{Min_3d, Max_3d} = gleam@list:fold(
Body_list,
{Init_min, Init_max},
fun(Acc, Pair) ->
{Current_min, Current_max} = Acc,
{_, Body} = Pair,
Radius = expresso@body:bounding_radius(Body),
Body_min = {vec3,
erlang:element(2, erlang:element(3, Body)) - Radius,
erlang:element(3, erlang:element(3, Body)) - Radius,
erlang:element(4, erlang:element(3, Body)) - Radius},
Body_max = {vec3,
erlang:element(2, erlang:element(3, Body)) + Radius,
erlang:element(3, erlang:element(3, Body)) + Radius,
erlang:element(4, erlang:element(3, Body)) + Radius},
{{vec3,
gleam@float:min(
erlang:element(2, Current_min),
erlang:element(2, Body_min)
),
gleam@float:min(
erlang:element(3, Current_min),
erlang:element(3, Body_min)
),
gleam@float:min(
erlang:element(4, Current_min),
erlang:element(4, Body_min)
)},
{vec3,
gleam@float:max(
erlang:element(2, Current_max),
erlang:element(2, Body_max)
),
gleam@float:max(
erlang:element(3, Current_max),
erlang:element(3, Body_max)
),
gleam@float:max(
erlang:element(4, Current_max),
erlang:element(4, Body_max)
)}}
end
),
Padding = 10.0,
spatial@collider:box(
{vec3,
erlang:element(2, Min_3d) - Padding,
erlang:element(3, Min_3d) - Padding,
erlang:element(4, Min_3d) - Padding},
{vec3,
erlang:element(2, Max_3d) + Padding,
erlang:element(3, Max_3d) + Padding,
erlang:element(4, Max_3d) + Padding}
)
end.
-file("src/expresso/collision.gleam", 659).
?DOC(" Test ray against a single body's collider\n").
-spec raycast_body(
vec@vec3:vec3(float()),
vec@vec3:vec3(float()),
float(),
MFM,
expresso@body:body(MFM)
) -> {ok, raycast_hit(MFM)} | {error, nil}.
raycast_body(Origin, Direction, Max_distance, Body_id, Body_data) ->
Collider_center = erlang:element(3, Body_data),
Radius = expresso@body:bounding_radius(Body_data),
To_center = vec@vec3f:subtract(Collider_center, Origin),
Projection = vec@vec3f:dot(To_center, Direction),
case Projection < +0.0 of
true ->
{error, nil};
false ->
Closest_point = vec@vec3f:add(
Origin,
vec@vec3f:scale(Direction, Projection)
),
Distance_to_center = vec@vec3f:distance(
Closest_point,
Collider_center
),
case Distance_to_center =< Radius of
false ->
{error, nil};
true ->
Offset_sq = (Radius * Radius) - (Distance_to_center * Distance_to_center),
Offset = case Offset_sq of
+0.0 ->
+0.0;
_ ->
case begin
_pipe = Offset_sq,
gleam@float:square_root(_pipe)
end of
{ok, O} ->
O;
{error, _} ->
+0.0
end
end,
Hit_distance = Projection - Offset,
case (Hit_distance >= +0.0) andalso (Hit_distance =< Max_distance) of
false ->
{error, nil};
true ->
Hit_point = vec@vec3f:add(
Origin,
vec@vec3f:scale(Direction, Hit_distance)
),
Normal_vec = vec@vec3f:subtract(
Hit_point,
Collider_center
),
Normal_length = vec@vec3f:length(Normal_vec),
Normal = case Normal_length > 0.0001 of
true ->
vec@vec3f:scale(
Normal_vec,
case Normal_length of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> 1.0 / Gleam@denominator
end
);
false ->
{vec3, 1.0, +0.0, +0.0}
end,
{ok,
{raycast_hit,
Body_id,
Body_data,
Hit_point,
Normal,
Hit_distance}}
end
end
end.
-file("src/expresso/collision.gleam", 569).
?DOC(
" Cast a ray and find the first body it hits\n"
"\n"
" Returns the closest hit along the ray, or None if no hit.\n"
).
-spec raycast(
gleam@dict:dict(MEZ, expresso@body:body(MEZ)),
gleam@option:option(spatial_index(MEZ)),
vec@vec3:vec3(float()),
vec@vec3:vec3(float()),
float(),
gleam@option:option(integer())
) -> gleam@option:option(raycast_hit(MEZ)).
raycast(Bodies, Spatial_index, Origin, Direction, Max_distance, Layer_mask) ->
Dir_length = vec@vec3f:length(Direction),
Dir_normalized = case Dir_length > 0.0001 of
true ->
vec@vec3f:scale(Direction, case Dir_length of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> 1.0 / Gleam@denominator
end);
false ->
{vec3, 1.0, +0.0, +0.0}
end,
Candidates = case Spatial_index of
none ->
maps:to_list(Bodies);
{some, {b_v_h_index, Tree}} ->
Endpoint = vec@vec3f:add(
Origin,
vec@vec3f:scale(Dir_normalized, Max_distance)
),
Center_3d = {vec3,
(erlang:element(2, Origin) + erlang:element(2, Endpoint)) / 2.0,
(erlang:element(3, Origin) + erlang:element(3, Endpoint)) / 2.0,
(erlang:element(4, Origin) + erlang:element(4, Endpoint)) / 2.0},
Radius = (Max_distance / 2.0) + 10.0,
Results = spatial@bvh:query_radius(Tree, Center_3d, Radius),
extract_bodies_from_results(Results, Bodies);
{some, {grid_index, Grid}} ->
Endpoint@1 = vec@vec3f:add(
Origin,
vec@vec3f:scale(Dir_normalized, Max_distance)
),
Center_3d@1 = {vec3,
(erlang:element(2, Origin) + erlang:element(2, Endpoint@1)) / 2.0,
(erlang:element(3, Origin) + erlang:element(3, Endpoint@1)) / 2.0,
(erlang:element(4, Origin) + erlang:element(4, Endpoint@1)) / 2.0},
Radius@1 = (Max_distance / 2.0) + 10.0,
Results@1 = spatial@grid:query_radius(Grid, Center_3d@1, Radius@1),
extract_bodies_from_results(Results@1, Bodies)
end,
Hits = gleam@list:filter_map(
Candidates,
fun(Pair) ->
{Id, Candidate_body} = Pair,
case Layer_mask of
{some, Mask} ->
case erlang:'band'(erlang:element(14, Candidate_body), Mask)
=:= 0 of
true ->
{error, nil};
false ->
raycast_body(
Origin,
Dir_normalized,
Max_distance,
Id,
Candidate_body
)
end;
none ->
raycast_body(
Origin,
Dir_normalized,
Max_distance,
Id,
Candidate_body
)
end
end
),
gleam@list:fold(Hits, none, fun(Closest, Hit) -> case Closest of
none ->
{some, Hit};
{some, Closest_hit} ->
case erlang:element(6, Hit) < erlang:element(6, Closest_hit) of
true ->
{some, Hit};
false ->
Closest
end
end end).