From 9448ad1886fd638c47302430663a5d34892c263a Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Tue, 21 Oct 2025 15:08:08 +0200 Subject: [PATCH 01/20] type stability fixes --- src/bounds.jl | 11 ++++---- src/bvh.jl | 59 +++++++++++++++++++++++++----------------- src/kernels.jl | 4 +-- src/transformations.jl | 7 ++--- 4 files changed, 46 insertions(+), 35 deletions(-) diff --git a/src/bounds.jl b/src/bounds.jl index 8224ece..1d22812 100644 --- a/src/bounds.jl +++ b/src/bounds.jl @@ -25,7 +25,7 @@ end function Base.getindex(b::Union{Bounds2,Bounds3}, i::Integer) i == 1 && return b.p_min i == 2 && return b.p_max - # error("Invalid index `$i`. Only `1` & `2` are valid.") + error("Invalid index `$i`. Only `1` & `2` are valid.") end function is_valid(b::Bounds3)::Bool all(b.p_min .!= Inf32) && all(b.p_max .!= -Inf32) @@ -49,11 +49,10 @@ end # Index through 8 corners. function corner(b::Bounds3, c::Integer) c -= Int32(1) - Point3f( - b[(c&1)+1][1], - b[(c & 2) != 0 ? 2 : 1][2], - b[(c & 4) != 0 ? 2 : 1][3], - ) + x = (c & Int32(1)) == Int32(0) ? b.p_min[1] : b.p_max[1] + y = (c & Int32(2)) == Int32(0) ? b.p_min[2] : b.p_max[2] + z = (c & Int32(4)) == Int32(0) ? b.p_min[3] : b.p_max[3] + Point3f(x, y, z) end function Base.union(b1::B, b2::B) where B<:Union{Bounds2,Bounds3} diff --git a/src/bvh.jl b/src/bvh.jl index 9046d1b..d6d9521 100644 --- a/src/bvh.jl +++ b/src/bvh.jl @@ -47,6 +47,7 @@ end function LinearBVHLeaf(bounds::Bounds3, primitives_offset::Integer, n_primitives::Integer) LinearBVH(bounds, primitives_offset, n_primitives, 0, false) end + function LinearBVHInterior(bounds::Bounds3, second_child_offset::Integer, split_axis::Integer) LinearBVH(bounds, second_child_offset, 0, split_axis, true) end @@ -94,13 +95,11 @@ function BVHAccel( primitives::AbstractVector{P}, max_node_primitives::Integer=1, ) where {P} triangles = Triangle[] - prim_idx = 1 for (mi, prim) in enumerate(primitives) triangle_mesh = to_triangle_mesh(prim) vertices = triangle_mesh.vertices for i in 1:div(length(triangle_mesh.indices), 3) - push!(triangles, Triangle(triangle_mesh, i, prim_idx)) - prim_idx += 1 + push!(triangles, Triangle(triangle_mesh, i, mi)) end end ordered_primitives, max_prim, nodes = primitives_to_bvh(triangles, max_node_primitives) @@ -240,7 +239,7 @@ end end """ - _traverse_bvh(bvh::BVHAccel{P}, ray::AbstractRay, hit_callback::F) where {P, F<:Function} + traverse_bvh(bvh::BVHAccel{P}, ray::AbstractRay, hit_callback::F) where {P, F<:Function} Internal function that traverses the BVH to find ray-primitive intersections. Uses a callback pattern to handle different intersection behaviors. @@ -255,9 +254,10 @@ Returns: - The final result from the hit_callback """ @inline function traverse_bvh(hit_callback::F, bvh::BVHAccel{P}, ray::AbstractRay) where {P, F<:Function} - # Early return if BVH is empty if length(bvh.nodes) == 0 - return false, ray, nothing + # We dont handle the empty case yet, since its not that easy to make it type stable + # Its possible, but why would we intersect an empty BVH? + error("BVH is empty; cannot traverse.") end # Prepare ray for traversal @@ -275,7 +275,7 @@ Returns: # State variables to hold callback results continue_search = true prim1 = primitives[1] - result = hit_callback(prim1, ray, nothing) + _, ray, result = hit_callback(prim1, ray, nothing) # Traverse BVH @_inbounds while true @@ -331,59 +331,70 @@ Returns: end # Initialization -closest_hit_callback(primitive, ray, ::Nothing) = (false, primitive, Point3f(0.0)) +closest_hit_callback(primitive, ray, ::Nothing) = false, ray, (false, primitive, 0.0f0, Point3f(0.0)) -function closest_hit_callback(primitive, ray, prev_result::Tuple{Bool, P, Point3f}) where {P} +function closest_hit_callback(primitive, ray, prev_result::Tuple{Bool, P, Float32, Point3f}) where {P} # Test intersection and update if closer tmp_hit, ray, tmp_bary = intersect_p!(primitive, ray) + if tmp_hit + # Calculate distance from ray origin to hit point + distance = ray.t_max + return true, ray, (true, primitive, distance, tmp_bary) + end # Always continue search to find closest - return true, ray, ifelse(tmp_hit, (true, primitive, tmp_bary), prev_result) + return true, ray, prev_result end """ - intersect!(bvh::BVHAccel{P}, ray::AbstractRay) where {P} + closest_hit(bvh::BVHAccel{P}, ray::AbstractRay) where {P} Find the closest intersection between a ray and the primitives stored in a BVH. Returns: - `hit_found`: Boolean indicating if an intersection was found - `hit_primitive`: The primitive that was hit (if any) +- `distance`: Distance along the ray to the hit point (hit_point = ray.o + ray.d * distance) - `barycentric_coords`: Barycentric coordinates of the hit point """ -@inline function intersect!(bvh::BVHAccel{P}, ray::AbstractRay) where {P} +@inline function closest_hit(bvh::BVHAccel{P}, ray::AbstractRay) where {P} # Traverse BVH with closest-hit callback _, _, result = traverse_bvh(closest_hit_callback, bvh, ray) - return result::Tuple{Bool, Triangle, Point3f} + return result::Tuple{Bool, Triangle, Float32, Point3f} end -any_hit_callback(primitive, current_ray, result::Nothing) = () +any_hit_callback(primitive, current_ray, result::Nothing) = (false, current_ray, (false, primitive, 0.0f0, Point3f(0.0))) # Define any-hit callback -function any_hit_callback(primitive, current_ray, ::Tuple{}) +function any_hit_callback(primitive, current_ray, prev_result::Tuple{Bool, P, Float32, Point3f}) where {P} # Test for intersection - if intersect_p(primitive, current_ray) - # Stop traversal on first hit - return false, current_ray, true + tmp_hit, tmp_ray, tmp_bary = intersect_p!(primitive, current_ray) + if tmp_hit + # Stop traversal on first hit and return hit info + distance = tmp_ray.t_max + return false, tmp_ray, (true, primitive, distance, tmp_bary) end # Continue search if no hit - return true, current_ray, false + return true, current_ray, prev_result end """ - intersect_p(bvh::BVHAccel, ray::AbstractRay) + any_hit(bvh::BVHAccel, ray::AbstractRay) Test if a ray intersects any primitive in the BVH (without finding the closest hit). +This function stops at the first intersection found, making it faster than closest_hit +when you only need to know if there's an intersection. Returns: - `hit_found`: Boolean indicating if any intersection was found +- `hit_primitive`: The primitive that was hit (if any) +- `distance`: Distance along the ray to the hit point (hit_point = ray.o + ray.d * distance) +- `barycentric_coords`: Barycentric coordinates of the hit point """ -@inline function intersect_p(bvh::BVHAccel, ray::AbstractRay) +@inline function any_hit(bvh::BVHAccel, ray::AbstractRay) # Traverse BVH with any-hit callback continue_search, _, result = traverse_bvh(any_hit_callback, bvh, ray) - # If traversal completed without finding a hit, return false - # Otherwise return the hit result (true) - return !continue_search ? result : false + return result::Tuple{Bool, Triangle, Float32, Point3f} end function calculate_ray_grid_bounds(bounds::GeometryBasics.Rect, ray_direction::Vec3f) diff --git a/src/kernels.jl b/src/kernels.jl index 2e7faed..4345a54 100644 --- a/src/kernels.jl +++ b/src/kernels.jl @@ -12,7 +12,7 @@ function hits_from_grid(bvh, viewdir; grid_size=32) Threads.@threads for idx in CartesianIndices(ray_origins) o = ray_origins[idx] ray = RayCaster.Ray(; o=o, d=ray_direction) - hit, prim, bary = RayCaster.intersect!(bvh, ray) + hit, prim, bary = RayCaster.closest_hit(bvh, ray) hitpoint = sum_mul(bary, prim.vertices) @inbounds result[idx] = RayHit(hit, hitpoint, prim.material_idx) end @@ -35,7 +35,7 @@ function view_factors!(result, bvh, rays_per_triangle=10000) point_on_triangle = random_triangle_point(triangle) o = point_on_triangle .+ (normal .* 0.01f0) # Offset so it doesn't self intersect ray = Ray(; o=o, d=random_hemisphere_uniform(normal, u, v)) - hit, prim, _ = intersect!(bvh, ray) + hit, prim, _ = closest_hit(bvh, ray) if hit && prim.material_idx != triangle.material_idx # weigh by angle? result[triangle.material_idx, prim.material_idx] += 1 diff --git a/src/transformations.jl b/src/transformations.jl index 8fc24ac..178523a 100644 --- a/src/transformations.jl +++ b/src/transformations.jl @@ -223,6 +223,7 @@ Base.:+(q1::Quaternion, q2::Quaternion) = Quaternion(q1.v .+ q2.v, q1.w + q2.w) Base.:-(q1::Quaternion, q2::Quaternion) = Quaternion(q1.v .- q2.v, q1.w - q2.w) Base.:/(q::Quaternion, f::Float32) = Quaternion(q.v ./ f, q.w / f) Base.:*(q::Quaternion, f::Float32) = Quaternion(q.v .* f, q.w * f) +Base.:*(f::Float32, q::Quaternion) = q * f LinearAlgebra.dot(q1::Quaternion, q2::Quaternion) = q1.v ⋅ q2.v + q1.w * q2.w LinearAlgebra.normalize(q::Quaternion) = q / sqrt(q ⋅ q) @@ -252,11 +253,11 @@ end function slerp(q1::Quaternion, q2::Quaternion, t::Float32) - cos_θ = q1 ⋅ q2 + cos_θ = Float32(q1 ⋅ q2) cos_θ > 0.9995f0 && return normalize((1 - t) * q1 + t * q2) - θ = acos(cos_θ) + θ = Float32(acos(cos_θ)) θ_p = θ * t q_perp = normalize(q2 - q1 * cos_θ) - q1 * cos(θ_p) + q_perp * sin(θ_p) + q1 * Float32(cos(θ_p)) + q_perp * Float32(sin(θ_p)) end From 6049d65b1d6b8df1d4c1aee2a1fb479fe8862a51 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Wed, 22 Oct 2025 12:44:15 +0200 Subject: [PATCH 02/20] test type stability and fix some instabilities --- Project.toml | 9 + docs/examples.jl | 2 +- src/bounds.jl | 11 +- test/Manifest.toml | 1714 ------------------------------------- test/Project.toml | 18 - test/bounds.jl | 228 +++++ test/runtests.jl | 41 +- test/test_intersection.jl | 10 +- test/type-stability.jl | 6 +- 9 files changed, 262 insertions(+), 1777 deletions(-) delete mode 100644 test/Manifest.toml delete mode 100644 test/Project.toml create mode 100644 test/bounds.jl diff --git a/Project.toml b/Project.toml index be372db..ba1a748 100644 --- a/Project.toml +++ b/Project.toml @@ -18,3 +18,12 @@ GeometryBasics = "0.5" RandomNumbers = "1" StaticArrays = "1.9.7" Statistics = "1" + +[extras] +JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" + +[targets] +test = ["Pkg", "Test", "JET", "FileIO"] diff --git a/docs/examples.jl b/docs/examples.jl index 054d487..d3c9d6d 100644 --- a/docs/examples.jl +++ b/docs/examples.jl @@ -71,7 +71,7 @@ function random_scatter_kernel!(bvh, triangle, u, v, normal) o = point .+ (normal .* 0.01f0) # Offset so it doesn't self intersect dir = RayCaster.random_hemisphere_uniform(normal, u, v) ray = RayCaster.Ray(; o=o, d=dir) - hit, prim, _ = RayCaster.intersect!(bvh, ray) + hit, prim, _ = RayCaster.closest_hit(bvh, ray) return hit, prim end diff --git a/src/bounds.jl b/src/bounds.jl index 1d22812..ce971ba 100644 --- a/src/bounds.jl +++ b/src/bounds.jl @@ -22,11 +22,14 @@ end function Base.:≈(b1::Union{Bounds2,Bounds3}, b2::Union{Bounds2,Bounds3}) b1.p_min ≈ b2.p_min && b1.p_max ≈ b2.p_max end -function Base.getindex(b::Union{Bounds2,Bounds3}, i::Integer) - i == 1 && return b.p_min - i == 2 && return b.p_max - error("Invalid index `$i`. Only `1` & `2` are valid.") + +function Base.getindex(b::Union{Bounds2, Bounds3}, i::T) where T<:Integer + i === T(1) && return b.p_min + i === T(2) && return b.p_max + N = b isa Bounds2 ? 2 : 3 + return Point{N, Float32}(NaN) end + function is_valid(b::Bounds3)::Bool all(b.p_min .!= Inf32) && all(b.p_max .!= -Inf32) end diff --git a/test/Manifest.toml b/test/Manifest.toml deleted file mode 100644 index 73bc78b..0000000 --- a/test/Manifest.toml +++ /dev/null @@ -1,1714 +0,0 @@ -# This file is machine-generated - editing it directly is not advised - -julia_version = "1.11.6" -manifest_format = "2.0" -project_hash = "ef2755c0e3f2a8db5ebe99b40739c90d79947014" - -[[deps.AbstractFFTs]] -deps = ["LinearAlgebra"] -git-tree-sha1 = "d92ad398961a3ed262d8bf04a1a2b8340f915fef" -uuid = "621f4979-c628-5d54-868e-fcf4e3e8185c" -version = "1.5.0" -weakdeps = ["ChainRulesCore", "Test"] - - [deps.AbstractFFTs.extensions] - AbstractFFTsChainRulesCoreExt = "ChainRulesCore" - AbstractFFTsTestExt = "Test" - -[[deps.AbstractTrees]] -git-tree-sha1 = "2d9c9a55f9c93e8887ad391fbae72f8ef55e1177" -uuid = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" -version = "0.4.5" - -[[deps.Adapt]] -deps = ["LinearAlgebra", "Requires"] -git-tree-sha1 = "f7817e2e585aa6d924fd714df1e2a84be7896c60" -uuid = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" -version = "4.3.0" -weakdeps = ["SparseArrays", "StaticArrays"] - - [deps.Adapt.extensions] - AdaptSparseArraysExt = "SparseArrays" - AdaptStaticArraysExt = "StaticArrays" - -[[deps.AdaptivePredicates]] -git-tree-sha1 = "7e651ea8d262d2d74ce75fdf47c4d63c07dba7a6" -uuid = "35492f91-a3bd-45ad-95db-fcad7dcfedb7" -version = "1.2.0" - -[[deps.AliasTables]] -deps = ["PtrArrays", "Random"] -git-tree-sha1 = "9876e1e164b144ca45e9e3198d0b689cadfed9ff" -uuid = "66dad0bd-aa9a-41b7-9441-69ab47430ed8" -version = "1.1.3" - -[[deps.Animations]] -deps = ["Colors"] -git-tree-sha1 = "e092fa223bf66a3c41f9c022bd074d916dc303e7" -uuid = "27a7e980-b3e6-11e9-2bcd-0b925532e340" -version = "0.4.2" - -[[deps.ArgTools]] -uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" -version = "1.1.2" - -[[deps.Artifacts]] -uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" -version = "1.11.0" - -[[deps.Atomix]] -deps = ["UnsafeAtomics"] -git-tree-sha1 = "29bb0eb6f578a587a49da16564705968667f5fa8" -uuid = "a9b6321e-bd34-4604-b9c9-b65b8de01458" -version = "1.1.2" - - [deps.Atomix.extensions] - AtomixCUDAExt = "CUDA" - AtomixMetalExt = "Metal" - AtomixOpenCLExt = "OpenCL" - AtomixoneAPIExt = "oneAPI" - - [deps.Atomix.weakdeps] - CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" - Metal = "dde4c033-4e86-420c-a63e-0dd931031962" - OpenCL = "08131aa3-fb12-5dee-8b74-c09406e224a2" - oneAPI = "8f75cd03-7ff8-4ecb-9b8f-daf728133b1b" - -[[deps.Automa]] -deps = ["PrecompileTools", "SIMD", "TranscodingStreams"] -git-tree-sha1 = "a8f503e8e1a5f583fbef15a8440c8c7e32185df2" -uuid = "67c07d97-cdcb-5c2c-af73-a7f9c32a568b" -version = "1.1.0" - -[[deps.AxisAlgorithms]] -deps = ["LinearAlgebra", "Random", "SparseArrays", "WoodburyMatrices"] -git-tree-sha1 = "01b8ccb13d68535d73d2b0c23e39bd23155fb712" -uuid = "13072b0f-2c55-5437-9ae7-d433b7a33950" -version = "1.1.0" - -[[deps.AxisArrays]] -deps = ["Dates", "IntervalSets", "IterTools", "RangeArrays"] -git-tree-sha1 = "16351be62963a67ac4083f748fdb3cca58bfd52f" -uuid = "39de3d68-74b9-583c-8d2d-e117c070f3a9" -version = "0.4.7" - -[[deps.BFloat16s]] -deps = ["LinearAlgebra", "Printf", "Random"] -git-tree-sha1 = "3b642331600250f592719140c60cf12372b82d66" -uuid = "ab4f0b2a-ad5b-11e8-123f-65d77653426b" -version = "0.5.1" - -[[deps.Base64]] -uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" -version = "1.11.0" - -[[deps.BaseDirs]] -git-tree-sha1 = "bca794632b8a9bbe159d56bf9e31c422671b35e0" -uuid = "18cc8868-cbac-4acf-b575-c8ff214dc66f" -version = "1.3.2" - -[[deps.BenchmarkTools]] -deps = ["Compat", "JSON", "Logging", "Printf", "Profile", "Statistics", "UUIDs"] -git-tree-sha1 = "e38fbc49a620f5d0b660d7f543db1009fe0f8336" -uuid = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" -version = "1.6.0" - -[[deps.Bzip2_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "1b96ea4a01afe0ea4090c5c8039690672dd13f2e" -uuid = "6e34b625-4abd-537c-b88f-471c36dfa7a0" -version = "1.0.9+0" - -[[deps.CEnum]] -git-tree-sha1 = "389ad5c84de1ae7cf0e28e381131c98ea87d54fc" -uuid = "fa961155-64e5-5f13-b03f-caf6b980ea82" -version = "0.5.0" - -[[deps.CRC32c]] -uuid = "8bf52ea8-c179-5cab-976a-9e18b702a9bc" -version = "1.11.0" - -[[deps.CRlibm]] -deps = ["CRlibm_jll"] -git-tree-sha1 = "66188d9d103b92b6cd705214242e27f5737a1e5e" -uuid = "96374032-68de-5a5b-8d9e-752f78720389" -version = "1.0.2" - -[[deps.CRlibm_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] -git-tree-sha1 = "e329286945d0cfc04456972ea732551869af1cfc" -uuid = "4e9b3aee-d8a1-5a3d-ad8b-7d824db253f0" -version = "1.0.1+0" - -[[deps.Cairo_jll]] -deps = ["Artifacts", "Bzip2_jll", "CompilerSupportLibraries_jll", "Fontconfig_jll", "FreeType2_jll", "Glib_jll", "JLLWrappers", "LZO_jll", "Libdl", "Pixman_jll", "Xorg_libXext_jll", "Xorg_libXrender_jll", "Zlib_jll", "libpng_jll"] -git-tree-sha1 = "fde3bf89aead2e723284a8ff9cdf5b551ed700e8" -uuid = "83423d85-b0ee-5818-9007-b63ccbeb887a" -version = "1.18.5+0" - -[[deps.ChainRulesCore]] -deps = ["Compat", "LinearAlgebra"] -git-tree-sha1 = "e4c6a16e77171a5f5e25e9646617ab1c276c5607" -uuid = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" -version = "1.26.0" -weakdeps = ["SparseArrays"] - - [deps.ChainRulesCore.extensions] - ChainRulesCoreSparseArraysExt = "SparseArrays" - -[[deps.CodeTracking]] -deps = ["InteractiveUtils", "UUIDs"] -git-tree-sha1 = "062c5e1a5bf6ada13db96a4ae4749a4c2234f521" -uuid = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2" -version = "1.3.9" - -[[deps.CodecBzip2]] -deps = ["Bzip2_jll", "TranscodingStreams"] -git-tree-sha1 = "84990fa864b7f2b4901901ca12736e45ee79068c" -uuid = "523fee87-0ab8-5b00-afb7-3ecf72e48cfd" -version = "0.8.5" - -[[deps.ColorBrewer]] -deps = ["Colors", "JSON"] -git-tree-sha1 = "e771a63cc8b539eca78c85b0cabd9233d6c8f06f" -uuid = "a2cac450-b92f-5266-8821-25eda20663c8" -version = "0.4.1" - -[[deps.ColorSchemes]] -deps = ["ColorTypes", "ColorVectorSpace", "Colors", "FixedPointNumbers", "PrecompileTools", "Random"] -git-tree-sha1 = "a656525c8b46aa6a1c76891552ed5381bb32ae7b" -uuid = "35d6a980-a343-548e-a6ea-1d62b119f2f4" -version = "3.30.0" - -[[deps.ColorTypes]] -deps = ["FixedPointNumbers", "Random"] -git-tree-sha1 = "67e11ee83a43eb71ddc950302c53bf33f0690dfe" -uuid = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" -version = "0.12.1" -weakdeps = ["StyledStrings"] - - [deps.ColorTypes.extensions] - StyledStringsExt = "StyledStrings" - -[[deps.ColorVectorSpace]] -deps = ["ColorTypes", "FixedPointNumbers", "LinearAlgebra", "Requires", "Statistics", "TensorCore"] -git-tree-sha1 = "8b3b6f87ce8f65a2b4f857528fd8d70086cd72b1" -uuid = "c3611d14-8923-5661-9e6a-0046d554d3a4" -version = "0.11.0" -weakdeps = ["SpecialFunctions"] - - [deps.ColorVectorSpace.extensions] - SpecialFunctionsExt = "SpecialFunctions" - -[[deps.Colors]] -deps = ["ColorTypes", "FixedPointNumbers", "Reexport"] -git-tree-sha1 = "37ea44092930b1811e666c3bc38065d7d87fcc74" -uuid = "5ae59095-9a9b-59fe-a467-6f913c188581" -version = "0.13.1" - -[[deps.Compat]] -deps = ["TOML", "UUIDs"] -git-tree-sha1 = "0037835448781bb46feb39866934e243886d756a" -uuid = "34da2185-b29b-5c13-b0c7-acf172513d20" -version = "4.18.0" -weakdeps = ["Dates", "LinearAlgebra"] - - [deps.Compat.extensions] - CompatLinearAlgebraExt = "LinearAlgebra" - -[[deps.CompilerSupportLibraries_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" -version = "1.1.1+0" - -[[deps.ComputePipeline]] -deps = ["Observables", "Preferences"] -git-tree-sha1 = "cb1299fee09da21e65ec88c1ff3a259f8d0b5802" -uuid = "95dc2771-c249-4cd0-9c9f-1f3b4330693c" -version = "0.1.4" - -[[deps.ConstructionBase]] -git-tree-sha1 = "b4b092499347b18a015186eae3042f72267106cb" -uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9" -version = "1.6.0" -weakdeps = ["IntervalSets", "LinearAlgebra", "StaticArrays"] - - [deps.ConstructionBase.extensions] - ConstructionBaseIntervalSetsExt = "IntervalSets" - ConstructionBaseLinearAlgebraExt = "LinearAlgebra" - ConstructionBaseStaticArraysExt = "StaticArrays" - -[[deps.Contour]] -git-tree-sha1 = "439e35b0b36e2e5881738abc8857bd92ad6ff9a8" -uuid = "d38c429a-6771-53c6-b99e-75d170b6e991" -version = "0.6.3" - -[[deps.Cthulhu]] -deps = ["CodeTracking", "FoldingTrees", "InteractiveUtils", "JuliaSyntax", "PrecompileTools", "Preferences", "REPL", "TypedSyntax", "UUIDs", "Unicode", "WidthLimitedIO"] -git-tree-sha1 = "ae585c45a75f16445b68695aa8d370145c5d2d58" -uuid = "f68482b8-f384-11e8-15f7-abe071a5a75f" -version = "2.16.5" - - [deps.Cthulhu.extensions] - CthulhuCompilerExt = "Compiler" - - [deps.Cthulhu.weakdeps] - Compiler = "807dbc54-b67e-4c79-8afb-eafe4df6f2e1" - -[[deps.DataAPI]] -git-tree-sha1 = "abe83f3a2f1b857aac70ef8b269080af17764bbe" -uuid = "9a962f9c-6df0-11e9-0e5d-c546b8b5ee8a" -version = "1.16.0" - -[[deps.DataStructures]] -deps = ["Compat", "InteractiveUtils", "OrderedCollections"] -git-tree-sha1 = "4e1fe97fdaed23e9dc21d4d664bea76b65fc50a0" -uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" -version = "0.18.22" - -[[deps.DataValueInterfaces]] -git-tree-sha1 = "bfc1187b79289637fa0ef6d4436ebdfe6905cbd6" -uuid = "e2d170a0-9d28-54be-80f0-106bbe20a464" -version = "1.0.0" - -[[deps.Dates]] -deps = ["Printf"] -uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" -version = "1.11.0" - -[[deps.DelaunayTriangulation]] -deps = ["AdaptivePredicates", "EnumX", "ExactPredicates", "Random"] -git-tree-sha1 = "5620ff4ee0084a6ab7097a27ba0c19290200b037" -uuid = "927a84f5-c5f4-47a5-9785-b46e178433df" -version = "1.6.4" - -[[deps.Distributed]] -deps = ["Random", "Serialization", "Sockets"] -uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" -version = "1.11.0" - -[[deps.Distributions]] -deps = ["AliasTables", "FillArrays", "LinearAlgebra", "PDMats", "Printf", "QuadGK", "Random", "SpecialFunctions", "Statistics", "StatsAPI", "StatsBase", "StatsFuns"] -git-tree-sha1 = "3e6d038b77f22791b8e3472b7c633acea1ecac06" -uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" -version = "0.25.120" - - [deps.Distributions.extensions] - DistributionsChainRulesCoreExt = "ChainRulesCore" - DistributionsDensityInterfaceExt = "DensityInterface" - DistributionsTestExt = "Test" - - [deps.Distributions.weakdeps] - ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" - DensityInterface = "b429d917-457f-4dbc-8f4c-0cc954292b1d" - Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" - -[[deps.DocStringExtensions]] -git-tree-sha1 = "7442a5dfe1ebb773c29cc2962a8980f47221d76c" -uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" -version = "0.9.5" - -[[deps.Downloads]] -deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] -uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" -version = "1.6.0" - -[[deps.EarCut_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] -git-tree-sha1 = "e3290f2d49e661fbd94046d7e3726ffcb2d41053" -uuid = "5ae413db-bbd1-5e63-b57d-d24a61df00f5" -version = "2.2.4+0" - -[[deps.EnumX]] -git-tree-sha1 = "bddad79635af6aec424f53ed8aad5d7555dc6f00" -uuid = "4e289a0a-7415-4d19-859d-a7e5c4648b56" -version = "1.0.5" - -[[deps.ExactPredicates]] -deps = ["IntervalArithmetic", "Random", "StaticArrays"] -git-tree-sha1 = "b3f2ff58735b5f024c392fde763f29b057e4b025" -uuid = "429591f6-91af-11e9-00e2-59fbe8cec110" -version = "2.2.8" - -[[deps.Expat_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "d55dffd9ae73ff72f1c0482454dcf2ec6c6c4a63" -uuid = "2e619515-83b5-522b-bb60-26c02a35a201" -version = "2.6.5+0" - -[[deps.ExprTools]] -git-tree-sha1 = "27415f162e6028e81c72b82ef756bf321213b6ec" -uuid = "e2ba6199-217a-4e67-a87a-7c52f15ade04" -version = "0.1.10" - -[[deps.Extents]] -git-tree-sha1 = "b309b36a9e02fe7be71270dd8c0fd873625332b4" -uuid = "411431e0-e8b7-467b-b5e0-f676ba4f2910" -version = "0.1.6" - -[[deps.FFMPEG_jll]] -deps = ["Artifacts", "Bzip2_jll", "FreeType2_jll", "FriBidi_jll", "JLLWrappers", "LAME_jll", "Libdl", "Ogg_jll", "OpenSSL_jll", "Opus_jll", "PCRE2_jll", "Zlib_jll", "libaom_jll", "libass_jll", "libfdk_aac_jll", "libvorbis_jll", "x264_jll", "x265_jll"] -git-tree-sha1 = "eaa040768ea663ca695d442be1bc97edfe6824f2" -uuid = "b22a6f82-2f65-5046-a5b2-351ab43fb4e5" -version = "6.1.3+0" - -[[deps.FFTW]] -deps = ["AbstractFFTs", "FFTW_jll", "LinearAlgebra", "MKL_jll", "Preferences", "Reexport"] -git-tree-sha1 = "797762812ed063b9b94f6cc7742bc8883bb5e69e" -uuid = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" -version = "1.9.0" - -[[deps.FFTW_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "6d6219a004b8cf1e0b4dbe27a2860b8e04eba0be" -uuid = "f5851436-0d7a-5f13-b9de-f02708fd171a" -version = "3.3.11+0" - -[[deps.FileIO]] -deps = ["Pkg", "Requires", "UUIDs"] -git-tree-sha1 = "b66970a70db13f45b7e57fbda1736e1cf72174ea" -uuid = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" -version = "1.17.0" - - [deps.FileIO.extensions] - HTTPExt = "HTTP" - - [deps.FileIO.weakdeps] - HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" - -[[deps.FilePaths]] -deps = ["FilePathsBase", "MacroTools", "Reexport", "Requires"] -git-tree-sha1 = "919d9412dbf53a2e6fe74af62a73ceed0bce0629" -uuid = "8fc22ac5-c921-52a6-82fd-178b2807b824" -version = "0.8.3" - -[[deps.FilePathsBase]] -deps = ["Compat", "Dates"] -git-tree-sha1 = "3bab2c5aa25e7840a4b065805c0cdfc01f3068d2" -uuid = "48062228-2e41-5def-b9a4-89aafe57970f" -version = "0.9.24" -weakdeps = ["Mmap", "Test"] - - [deps.FilePathsBase.extensions] - FilePathsBaseMmapExt = "Mmap" - FilePathsBaseTestExt = "Test" - -[[deps.FileWatching]] -uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" -version = "1.11.0" - -[[deps.FillArrays]] -deps = ["LinearAlgebra"] -git-tree-sha1 = "6a70198746448456524cb442b8af316927ff3e1a" -uuid = "1a297f60-69ca-5386-bcde-b61e274b549b" -version = "1.13.0" -weakdeps = ["PDMats", "SparseArrays", "Statistics"] - - [deps.FillArrays.extensions] - FillArraysPDMatsExt = "PDMats" - FillArraysSparseArraysExt = "SparseArrays" - FillArraysStatisticsExt = "Statistics" - -[[deps.FixedPointNumbers]] -deps = ["Statistics"] -git-tree-sha1 = "05882d6995ae5c12bb5f36dd2ed3f61c98cbb172" -uuid = "53c48c17-4a7d-5ca2-90c5-79b7896eea93" -version = "0.8.5" - -[[deps.FoldingTrees]] -deps = ["AbstractTrees", "REPL"] -git-tree-sha1 = "c1b0164369256b26f71d9830df9000a9c39757fc" -uuid = "1eca21be-9b9b-4ed8-839a-6d8ae26b1781" -version = "1.2.2" - -[[deps.Fontconfig_jll]] -deps = ["Artifacts", "Bzip2_jll", "Expat_jll", "FreeType2_jll", "JLLWrappers", "Libdl", "Libuuid_jll", "Zlib_jll"] -git-tree-sha1 = "301b5d5d731a0654825f1f2e906990f7141a106b" -uuid = "a3f928ae-7b40-5064-980b-68af3947d34b" -version = "2.16.0+0" - -[[deps.Format]] -git-tree-sha1 = "9c68794ef81b08086aeb32eeaf33531668d5f5fc" -uuid = "1fa38f19-a742-5d3f-a2b9-30dd87b9d5f8" -version = "1.3.7" - -[[deps.FreeType]] -deps = ["CEnum", "FreeType2_jll"] -git-tree-sha1 = "907369da0f8e80728ab49c1c7e09327bf0d6d999" -uuid = "b38be410-82b0-50bf-ab77-7b57e271db43" -version = "4.1.1" - -[[deps.FreeType2_jll]] -deps = ["Artifacts", "Bzip2_jll", "JLLWrappers", "Libdl", "Zlib_jll"] -git-tree-sha1 = "2c5512e11c791d1baed2049c5652441b28fc6a31" -uuid = "d7e528f0-a631-5988-bf34-fe36492bcfd7" -version = "2.13.4+0" - -[[deps.FreeTypeAbstraction]] -deps = ["BaseDirs", "ColorVectorSpace", "Colors", "FreeType", "GeometryBasics", "Mmap"] -git-tree-sha1 = "4ebb930ef4a43817991ba35db6317a05e59abd11" -uuid = "663a7486-cb36-511b-a19d-713bb74d65c9" -version = "0.10.8" - -[[deps.FriBidi_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "7a214fdac5ed5f59a22c2d9a885a16da1c74bbc7" -uuid = "559328eb-81f9-559d-9380-de523a88c83c" -version = "1.0.17+0" - -[[deps.GPUArrays]] -deps = ["Adapt", "GPUArraysCore", "KernelAbstractions", "LLVM", "LinearAlgebra", "Printf", "Random", "Reexport", "ScopedValues", "Serialization", "Statistics"] -git-tree-sha1 = "be941842a40b6daac98496994ea69054ba4c5144" -uuid = "0c68f7d7-f131-5f86-a1c3-88cf8149b2d7" -version = "11.2.3" - -[[deps.GPUArraysCore]] -deps = ["Adapt"] -git-tree-sha1 = "83cf05ab16a73219e5f6bd1bdfa9848fa24ac627" -uuid = "46192b85-c4d5-4398-a991-12ede77f4527" -version = "0.2.0" - -[[deps.GPUCompiler]] -deps = ["ExprTools", "InteractiveUtils", "LLVM", "Libdl", "Logging", "PrecompileTools", "Preferences", "Scratch", "Serialization", "TOML", "Tracy", "UUIDs"] -git-tree-sha1 = "eb1e212e12cc058fa16712082d44be499d23638c" -uuid = "61eb1bfa-7361-4325-ad38-22787b887f55" -version = "1.6.1" - -[[deps.GPUToolbox]] -deps = ["LLVM"] -git-tree-sha1 = "5bfe837129bf49e2e049b4f1517546055cc16a93" -uuid = "096a3bc2-3ced-46d0-87f4-dd12716f4bfc" -version = "0.3.0" - -[[deps.GeometryBasics]] -deps = ["EarCut_jll", "Extents", "IterTools", "LinearAlgebra", "PrecompileTools", "Random", "StaticArrays"] -git-tree-sha1 = "1f5a80f4ed9f5a4aada88fc2db456e637676414b" -uuid = "5c1252a2-5f33-56bf-86c9-59e7332b4326" -version = "0.5.10" - - [deps.GeometryBasics.extensions] - GeometryBasicsGeoInterfaceExt = "GeoInterface" - - [deps.GeometryBasics.weakdeps] - GeoInterface = "cf35fbd7-0cd7-5166-be24-54bfbe79505f" - -[[deps.GettextRuntime_jll]] -deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "Libiconv_jll"] -git-tree-sha1 = "45288942190db7c5f760f59c04495064eedf9340" -uuid = "b0724c58-0f36-5564-988d-3bb0596ebc4a" -version = "0.22.4+0" - -[[deps.Ghostscript_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] -git-tree-sha1 = "43ba3d3c82c18d88471cfd2924931658838c9d8f" -uuid = "61579ee1-b43e-5ca0-a5da-69d92c66a64b" -version = "9.55.0+4" - -[[deps.Giflib_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "6570366d757b50fabae9f4315ad74d2e40c0560a" -uuid = "59f7168a-df46-5410-90c8-f2779963d0ec" -version = "5.2.3+0" - -[[deps.Glib_jll]] -deps = ["Artifacts", "GettextRuntime_jll", "JLLWrappers", "Libdl", "Libffi_jll", "Libiconv_jll", "Libmount_jll", "PCRE2_jll", "Zlib_jll"] -git-tree-sha1 = "35fbd0cefb04a516104b8e183ce0df11b70a3f1a" -uuid = "7746bdde-850d-59dc-9ae8-88ece973131d" -version = "2.84.3+0" - -[[deps.Graphite2_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "8a6dbda1fd736d60cc477d99f2e7a042acfa46e8" -uuid = "3b182d85-2403-5c21-9c21-1e1f0cc25472" -version = "1.3.15+0" - -[[deps.GridLayoutBase]] -deps = ["GeometryBasics", "InteractiveUtils", "Observables"] -git-tree-sha1 = "dc6bed05c15523624909b3953686c5f5ffa10adc" -uuid = "3955a311-db13-416c-9275-1d80ed98e5e9" -version = "0.11.1" - -[[deps.Grisu]] -git-tree-sha1 = "53bb909d1151e57e2484c3d1b53e19552b887fb2" -uuid = "42e2da0e-8278-4e71-bc24-59509adca0fe" -version = "1.0.2" - -[[deps.HarfBuzz_jll]] -deps = ["Artifacts", "Cairo_jll", "Fontconfig_jll", "FreeType2_jll", "Glib_jll", "Graphite2_jll", "JLLWrappers", "Libdl", "Libffi_jll"] -git-tree-sha1 = "f923f9a774fcf3f5cb761bfa43aeadd689714813" -uuid = "2e76f6c2-a576-52d4-95c1-20adfe4de566" -version = "8.5.1+0" - -[[deps.HashArrayMappedTries]] -git-tree-sha1 = "2eaa69a7cab70a52b9687c8bf950a5a93ec895ae" -uuid = "076d061b-32b6-4027-95e0-9a2c6f6d7e74" -version = "0.2.0" - -[[deps.HypergeometricFunctions]] -deps = ["LinearAlgebra", "OpenLibm_jll", "SpecialFunctions"] -git-tree-sha1 = "68c173f4f449de5b438ee67ed0c9c748dc31a2ec" -uuid = "34004b35-14d8-5ef3-9330-4cdb6864b03a" -version = "0.3.28" - -[[deps.ImageAxes]] -deps = ["AxisArrays", "ImageBase", "ImageCore", "Reexport", "SimpleTraits"] -git-tree-sha1 = "e12629406c6c4442539436581041d372d69c55ba" -uuid = "2803e5a7-5153-5ecf-9a86-9b4c37f5f5ac" -version = "0.6.12" - -[[deps.ImageBase]] -deps = ["ImageCore", "Reexport"] -git-tree-sha1 = "eb49b82c172811fd2c86759fa0553a2221feb909" -uuid = "c817782e-172a-44cc-b673-b171935fbb9e" -version = "0.1.7" - -[[deps.ImageCore]] -deps = ["ColorVectorSpace", "Colors", "FixedPointNumbers", "MappedArrays", "MosaicViews", "OffsetArrays", "PaddedViews", "PrecompileTools", "Reexport"] -git-tree-sha1 = "8c193230235bbcee22c8066b0374f63b5683c2d3" -uuid = "a09fc81d-aa75-5fe9-8630-4744c3626534" -version = "0.10.5" - -[[deps.ImageIO]] -deps = ["FileIO", "IndirectArrays", "JpegTurbo", "LazyModules", "Netpbm", "OpenEXR", "PNGFiles", "QOI", "Sixel", "TiffImages", "UUIDs", "WebP"] -git-tree-sha1 = "696144904b76e1ca433b886b4e7edd067d76cbf7" -uuid = "82e4d734-157c-48bb-816b-45c225c6df19" -version = "0.6.9" - -[[deps.ImageMagick]] -deps = ["FileIO", "ImageCore", "ImageMagick_jll", "InteractiveUtils"] -git-tree-sha1 = "8e64ab2f0da7b928c8ae889c514a52741debc1c2" -uuid = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" -version = "1.4.2" - -[[deps.ImageMagick_jll]] -deps = ["Artifacts", "Ghostscript_jll", "JLLWrappers", "JpegTurbo_jll", "Libdl", "Libtiff_jll", "OpenJpeg_jll", "Zlib_jll", "libpng_jll"] -git-tree-sha1 = "afde851466407a99d48829051c36ac80749d8d7c" -uuid = "c73af94c-d91f-53ed-93a7-00f77d67a9d7" -version = "7.1.1048+0" - -[[deps.ImageMetadata]] -deps = ["AxisArrays", "ImageAxes", "ImageBase", "ImageCore"] -git-tree-sha1 = "2a81c3897be6fbcde0802a0ebe6796d0562f63ec" -uuid = "bc367c6b-8a6b-528e-b4bd-a4b897500b49" -version = "0.9.10" - -[[deps.ImageShow]] -deps = ["Base64", "ColorSchemes", "FileIO", "ImageBase", "ImageCore", "OffsetArrays", "StackViews"] -git-tree-sha1 = "3b5344bcdbdc11ad58f3b1956709b5b9345355de" -uuid = "4e3cecfd-b093-5904-9786-8bbb286a6a31" -version = "0.3.8" - -[[deps.Imath_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "0936ba688c6d201805a83da835b55c61a180db52" -uuid = "905a6f67-0a94-5f89-b386-d35d92009cd1" -version = "3.1.11+0" - -[[deps.IndirectArrays]] -git-tree-sha1 = "012e604e1c7458645cb8b436f8fba789a51b257f" -uuid = "9b13fd28-a010-5f03-acff-a1bbcff69959" -version = "1.0.0" - -[[deps.Inflate]] -git-tree-sha1 = "d1b1b796e47d94588b3757fe84fbf65a5ec4a80d" -uuid = "d25df0c9-e2be-5dd7-82c8-3ad0b3e990b9" -version = "0.1.5" - -[[deps.IntelOpenMP_jll]] -deps = ["Artifacts", "JLLWrappers", "LazyArtifacts", "Libdl"] -git-tree-sha1 = "ec1debd61c300961f98064cfb21287613ad7f303" -uuid = "1d5cc7b8-4909-519e-a0f8-d0f5ad9712d0" -version = "2025.2.0+0" - -[[deps.InteractiveUtils]] -deps = ["Markdown"] -uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" -version = "1.11.0" - -[[deps.Interpolations]] -deps = ["Adapt", "AxisAlgorithms", "ChainRulesCore", "LinearAlgebra", "OffsetArrays", "Random", "Ratios", "Requires", "SharedArrays", "SparseArrays", "StaticArrays", "WoodburyMatrices"] -git-tree-sha1 = "f2905febca224eade352a573e129ef43aa593354" -uuid = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" -version = "0.16.1" - - [deps.Interpolations.extensions] - InterpolationsForwardDiffExt = "ForwardDiff" - InterpolationsUnitfulExt = "Unitful" - - [deps.Interpolations.weakdeps] - ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" - Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" - -[[deps.IntervalArithmetic]] -deps = ["CRlibm", "MacroTools", "OpenBLASConsistentFPCSR_jll", "Random", "RoundingEmulator"] -git-tree-sha1 = "79342df41c3c24664e5bf29395cfdf2f2a599412" -uuid = "d1acc4aa-44c8-5952-acd4-ba5d80a2a253" -version = "0.22.36" - - [deps.IntervalArithmetic.extensions] - IntervalArithmeticArblibExt = "Arblib" - IntervalArithmeticDiffRulesExt = "DiffRules" - IntervalArithmeticForwardDiffExt = "ForwardDiff" - IntervalArithmeticIntervalSetsExt = "IntervalSets" - IntervalArithmeticLinearAlgebraExt = "LinearAlgebra" - IntervalArithmeticRecipesBaseExt = "RecipesBase" - IntervalArithmeticSparseArraysExt = "SparseArrays" - - [deps.IntervalArithmetic.weakdeps] - Arblib = "fb37089c-8514-4489-9461-98f9c8763369" - DiffRules = "b552c78f-8df3-52c6-915a-8e097449b14b" - ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" - IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953" - LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" - RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" - SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" - -[[deps.IntervalSets]] -git-tree-sha1 = "5fbb102dcb8b1a858111ae81d56682376130517d" -uuid = "8197267c-284f-5f27-9208-e0e47529a953" -version = "0.7.11" - - [deps.IntervalSets.extensions] - IntervalSetsRandomExt = "Random" - IntervalSetsRecipesBaseExt = "RecipesBase" - IntervalSetsStatisticsExt = "Statistics" - - [deps.IntervalSets.weakdeps] - Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" - RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" - Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" - -[[deps.InverseFunctions]] -git-tree-sha1 = "a779299d77cd080bf77b97535acecd73e1c5e5cb" -uuid = "3587e190-3f89-42d0-90ee-14403ec27112" -version = "0.1.17" -weakdeps = ["Dates", "Test"] - - [deps.InverseFunctions.extensions] - InverseFunctionsDatesExt = "Dates" - InverseFunctionsTestExt = "Test" - -[[deps.IrrationalConstants]] -git-tree-sha1 = "e2222959fbc6c19554dc15174c81bf7bf3aa691c" -uuid = "92d709cd-6900-40b7-9082-c6be49f344b6" -version = "0.2.4" - -[[deps.Isoband]] -deps = ["isoband_jll"] -git-tree-sha1 = "f9b6d97355599074dc867318950adaa6f9946137" -uuid = "f1662d9f-8043-43de-a69a-05efc1cc6ff4" -version = "0.1.1" - -[[deps.IterTools]] -git-tree-sha1 = "42d5f897009e7ff2cf88db414a389e5ed1bdd023" -uuid = "c8e1da08-722c-5040-9ed9-7db0dc04731e" -version = "1.10.0" - -[[deps.IteratorInterfaceExtensions]] -git-tree-sha1 = "a3f24677c21f5bbe9d2a714f95dcd58337fb2856" -uuid = "82899510-4779-5014-852e-03e436cf321d" -version = "1.0.0" - -[[deps.JLLWrappers]] -deps = ["Artifacts", "Preferences"] -git-tree-sha1 = "0533e564aae234aff59ab625543145446d8b6ec2" -uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" -version = "1.7.1" - -[[deps.JSON]] -deps = ["Dates", "Mmap", "Parsers", "Unicode"] -git-tree-sha1 = "31e996f0a15c7b280ba9f76636b3ff9e2ae58c9a" -uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -version = "0.21.4" - -[[deps.JpegTurbo]] -deps = ["CEnum", "FileIO", "ImageCore", "JpegTurbo_jll", "TOML"] -git-tree-sha1 = "9496de8fb52c224a2e3f9ff403947674517317d9" -uuid = "b835a17e-a41a-41e7-81f0-2f016b05efe0" -version = "0.1.6" - -[[deps.JpegTurbo_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "eac1206917768cb54957c65a615460d87b455fc1" -uuid = "aacddb02-875f-59d6-b918-886e6ef4fbf8" -version = "3.1.1+0" - -[[deps.JuliaSyntax]] -git-tree-sha1 = "937da4713526b96ac9a178e2035019d3b78ead4a" -uuid = "70703baa-626e-46a2-a12c-08ffd08c73b4" -version = "0.4.10" - -[[deps.KernelAbstractions]] -deps = ["Adapt", "Atomix", "InteractiveUtils", "MacroTools", "PrecompileTools", "Requires", "StaticArrays", "UUIDs"] -git-tree-sha1 = "83c617e9e9b02306a7acab79e05ec10253db7c87" -uuid = "63c18a36-062a-441e-b654-da1e3ab1ce7c" -version = "0.9.38" - - [deps.KernelAbstractions.extensions] - EnzymeExt = "EnzymeCore" - LinearAlgebraExt = "LinearAlgebra" - SparseArraysExt = "SparseArrays" - - [deps.KernelAbstractions.weakdeps] - EnzymeCore = "f151be2c-9106-41f4-ab19-57ee4f262869" - LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" - SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" - -[[deps.KernelDensity]] -deps = ["Distributions", "DocStringExtensions", "FFTW", "Interpolations", "StatsBase"] -git-tree-sha1 = "ba51324b894edaf1df3ab16e2cc6bc3280a2f1a7" -uuid = "5ab0869b-81aa-558d-bb23-cbf5423bbe9b" -version = "0.6.10" - -[[deps.LAME_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "059aabebaa7c82ccb853dd4a0ee9d17796f7e1bc" -uuid = "c1c5ebd0-6772-5130-a774-d5fcae4a789d" -version = "3.100.3+0" - -[[deps.LERC_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] -git-tree-sha1 = "bf36f528eec6634efc60d7ec062008f171071434" -uuid = "88015f11-f218-50d7-93a8-a6af411a945d" -version = "3.0.0+1" - -[[deps.LLVM]] -deps = ["CEnum", "LLVMExtra_jll", "Libdl", "Preferences", "Printf", "Unicode"] -git-tree-sha1 = "9c7c721cfd800d87d48c745d8bfb65144f0a91df" -uuid = "929cbde3-209d-540e-8aea-75f648917ca0" -version = "9.4.2" -weakdeps = ["BFloat16s"] - - [deps.LLVM.extensions] - BFloat16sExt = "BFloat16s" - -[[deps.LLVMDowngrader_jll]] -deps = ["Artifacts", "JLLWrappers", "LazyArtifacts", "Libdl", "TOML", "Zlib_jll"] -git-tree-sha1 = "6c4eee9991684790dcd49c52508836c02ac36133" -uuid = "f52de702-fb25-5922-94ba-81dd59b07444" -version = "0.6.0+1" - -[[deps.LLVMExtra_jll]] -deps = ["Artifacts", "JLLWrappers", "LazyArtifacts", "Libdl", "TOML"] -git-tree-sha1 = "2ea068aac1e7f0337d381b0eae3110581e3f3216" -uuid = "dad2f222-ce93-54a1-a47d-0025e8a3acab" -version = "0.0.37+2" - -[[deps.LLVMOpenMP_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "eb62a3deb62fc6d8822c0c4bef73e4412419c5d8" -uuid = "1d63c593-3942-5779-bab2-d838dc0a180e" -version = "18.1.8+0" - -[[deps.LZO_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "1c602b1127f4751facb671441ca72715cc95938a" -uuid = "dd4b983a-f0e5-5f8d-a1b7-129d4a5fb1ac" -version = "2.10.3+0" - -[[deps.LaTeXStrings]] -git-tree-sha1 = "dda21b8cbd6a6c40d9d02a73230f9d70fed6918c" -uuid = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" -version = "1.4.0" - -[[deps.LazyArtifacts]] -deps = ["Artifacts", "Pkg"] -uuid = "4af54fe1-eca0-43a8-85a7-787d91b784e3" -version = "1.11.0" - -[[deps.LazyModules]] -git-tree-sha1 = "a560dd966b386ac9ae60bdd3a3d3a326062d3c3e" -uuid = "8cdb02fc-e678-4876-92c5-9defec4f444e" -version = "0.3.1" - -[[deps.LibCURL]] -deps = ["LibCURL_jll", "MozillaCACerts_jll"] -uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" -version = "0.6.4" - -[[deps.LibCURL_jll]] -deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"] -uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" -version = "8.6.0+0" - -[[deps.LibGit2]] -deps = ["Base64", "LibGit2_jll", "NetworkOptions", "Printf", "SHA"] -uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" -version = "1.11.0" - -[[deps.LibGit2_jll]] -deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll"] -uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5" -version = "1.7.2+0" - -[[deps.LibSSH2_jll]] -deps = ["Artifacts", "Libdl", "MbedTLS_jll"] -uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" -version = "1.11.0+1" - -[[deps.LibTracyClient_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "d2bc4e1034b2d43076b50f0e34ea094c2cb0a717" -uuid = "ad6e5548-8b26-5c9f-8ef3-ef0ad883f3a5" -version = "0.9.1+6" - -[[deps.Libdl]] -uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" -version = "1.11.0" - -[[deps.Libffi_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "c8da7e6a91781c41a863611c7e966098d783c57a" -uuid = "e9f186c6-92d2-5b65-8a66-fee21dc1b490" -version = "3.4.7+0" - -[[deps.Libglvnd_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libX11_jll", "Xorg_libXext_jll"] -git-tree-sha1 = "d36c21b9e7c172a44a10484125024495e2625ac0" -uuid = "7e76a0d4-f3c7-5321-8279-8d96eeed0f29" -version = "1.7.1+1" - -[[deps.Libiconv_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "be484f5c92fad0bd8acfef35fe017900b0b73809" -uuid = "94ce4f54-9a6c-5748-9c1c-f9c7231a4531" -version = "1.18.0+0" - -[[deps.Libmount_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "a31572773ac1b745e0343fe5e2c8ddda7a37e997" -uuid = "4b2f31a3-9ecc-558c-b454-b3730dcb73e9" -version = "2.41.0+0" - -[[deps.Libtiff_jll]] -deps = ["Artifacts", "JLLWrappers", "JpegTurbo_jll", "LERC_jll", "Libdl", "XZ_jll", "Zlib_jll", "Zstd_jll"] -git-tree-sha1 = "2da088d113af58221c52828a80378e16be7d037a" -uuid = "89763e89-9b03-5906-acba-b20f662cd828" -version = "4.5.1+1" - -[[deps.Libuuid_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "321ccef73a96ba828cd51f2ab5b9f917fa73945a" -uuid = "38a345b3-de98-5d2b-a5d3-14cd9215e700" -version = "2.41.0+0" - -[[deps.LinearAlgebra]] -deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"] -uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -version = "1.11.0" - -[[deps.LittleCMS_jll]] -deps = ["Artifacts", "JLLWrappers", "JpegTurbo_jll", "Libdl", "Libtiff_jll"] -git-tree-sha1 = "fa7fd067dca76cadd880f1ca937b4f387975a9f5" -uuid = "d3a379c0-f9a3-5b72-a4c0-6bf4d2e8af0f" -version = "2.16.0+0" - -[[deps.LogExpFunctions]] -deps = ["DocStringExtensions", "IrrationalConstants", "LinearAlgebra"] -git-tree-sha1 = "13ca9e2586b89836fd20cccf56e57e2b9ae7f38f" -uuid = "2ab3a3ac-af41-5b50-aa03-7779005ae688" -version = "0.3.29" - - [deps.LogExpFunctions.extensions] - LogExpFunctionsChainRulesCoreExt = "ChainRulesCore" - LogExpFunctionsChangesOfVariablesExt = "ChangesOfVariables" - LogExpFunctionsInverseFunctionsExt = "InverseFunctions" - - [deps.LogExpFunctions.weakdeps] - ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" - ChangesOfVariables = "9e997f8a-9a97-42d5-a9f1-ce6bfc15e2c0" - InverseFunctions = "3587e190-3f89-42d0-90ee-14403ec27112" - -[[deps.Logging]] -uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" -version = "1.11.0" - -[[deps.MKL_jll]] -deps = ["Artifacts", "IntelOpenMP_jll", "JLLWrappers", "LazyArtifacts", "Libdl", "oneTBB_jll"] -git-tree-sha1 = "282cadc186e7b2ae0eeadbd7a4dffed4196ae2aa" -uuid = "856f044c-d86e-5d09-b602-aeab76dc8ba7" -version = "2025.2.0+0" - -[[deps.MacroTools]] -git-tree-sha1 = "1e0228a030642014fe5cfe68c2c0a818f9e3f522" -uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" -version = "0.5.16" - -[[deps.Makie]] -deps = ["Animations", "Base64", "CRC32c", "ColorBrewer", "ColorSchemes", "ColorTypes", "Colors", "ComputePipeline", "Contour", "Dates", "DelaunayTriangulation", "Distributions", "DocStringExtensions", "Downloads", "FFMPEG_jll", "FileIO", "FilePaths", "FixedPointNumbers", "Format", "FreeType", "FreeTypeAbstraction", "GeometryBasics", "GridLayoutBase", "ImageBase", "ImageIO", "InteractiveUtils", "Interpolations", "IntervalSets", "InverseFunctions", "Isoband", "KernelDensity", "LaTeXStrings", "LinearAlgebra", "MacroTools", "Markdown", "MathTeXEngine", "Observables", "OffsetArrays", "PNGFiles", "Packing", "Pkg", "PlotUtils", "PolygonOps", "PrecompileTools", "Printf", "REPL", "Random", "RelocatableFolders", "Scratch", "ShaderAbstractions", "Showoff", "SignedDistanceFields", "SparseArrays", "Statistics", "StatsBase", "StatsFuns", "StructArrays", "TriplotBase", "UnicodeFun", "Unitful"] -git-tree-sha1 = "c2dbe9f2b1360edb15d4f711e6cc3ca0cad1acde" -uuid = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" -version = "0.24.5" - -[[deps.MappedArrays]] -git-tree-sha1 = "2dab0221fe2b0f2cb6754eaa743cc266339f527e" -uuid = "dbb5928d-eab1-5f90-85c2-b9b0edb7c900" -version = "0.4.2" - -[[deps.Markdown]] -deps = ["Base64"] -uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" -version = "1.11.0" - -[[deps.MathTeXEngine]] -deps = ["AbstractTrees", "Automa", "DataStructures", "FreeTypeAbstraction", "GeometryBasics", "LaTeXStrings", "REPL", "RelocatableFolders", "UnicodeFun"] -git-tree-sha1 = "a370fef694c109e1950836176ed0d5eabbb65479" -uuid = "0a4f8689-d25c-4efe-a92b-7142dfc1aa53" -version = "0.6.6" - -[[deps.MbedTLS_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" -version = "2.28.6+0" - -[[deps.Metal]] -deps = ["Adapt", "BFloat16s", "CEnum", "CodecBzip2", "ExprTools", "GPUArrays", "GPUCompiler", "GPUToolbox", "KernelAbstractions", "LLVM", "LLVMDowngrader_jll", "LinearAlgebra", "ObjectiveC", "PrecompileTools", "Preferences", "Printf", "Random", "SHA", "ScopedValues", "StaticArrays", "UUIDs"] -git-tree-sha1 = "19e6e7b52ab1c0850e81438e6de5ed59ebd4fca9" -uuid = "dde4c033-4e86-420c-a63e-0dd931031962" -version = "1.7.0" -weakdeps = ["SpecialFunctions"] - - [deps.Metal.extensions] - SpecialFunctionsExt = "SpecialFunctions" - -[[deps.Missings]] -deps = ["DataAPI"] -git-tree-sha1 = "ec4f7fbeab05d7747bdf98eb74d130a2a2ed298d" -uuid = "e1d29d7a-bbdc-5cf2-9ac0-f12de2c33e28" -version = "1.2.0" - -[[deps.Mmap]] -uuid = "a63ad114-7e13-5084-954f-fe012c677804" -version = "1.11.0" - -[[deps.MosaicViews]] -deps = ["MappedArrays", "OffsetArrays", "PaddedViews", "StackViews"] -git-tree-sha1 = "7b86a5d4d70a9f5cdf2dacb3cbe6d251d1a61dbe" -uuid = "e94cdb99-869f-56ef-bcf0-1ae2bcbe0389" -version = "0.3.4" - -[[deps.MozillaCACerts_jll]] -uuid = "14a3606d-f60d-562e-9121-12d972cd8159" -version = "2023.12.12" - -[[deps.Netpbm]] -deps = ["FileIO", "ImageCore", "ImageMetadata"] -git-tree-sha1 = "d92b107dbb887293622df7697a2223f9f8176fcd" -uuid = "f09324ee-3d7c-5217-9330-fc30815ba969" -version = "1.1.1" - -[[deps.NetworkOptions]] -uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" -version = "1.2.0" - -[[deps.ObjectiveC]] -deps = ["CEnum", "Libdl", "Preferences"] -git-tree-sha1 = "a10d01e1a9683ffd64b450cc6c1419ee9a8f7ab4" -uuid = "e86c9b32-1129-44ac-8ea0-90d5bb39ded9" -version = "3.4.2" - -[[deps.Observables]] -git-tree-sha1 = "7438a59546cf62428fc9d1bc94729146d37a7225" -uuid = "510215fc-4207-5dde-b226-833fc4488ee2" -version = "0.5.5" - -[[deps.OffsetArrays]] -git-tree-sha1 = "117432e406b5c023f665fa73dc26e79ec3630151" -uuid = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" -version = "1.17.0" -weakdeps = ["Adapt"] - - [deps.OffsetArrays.extensions] - OffsetArraysAdaptExt = "Adapt" - -[[deps.Ogg_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "b6aa4566bb7ae78498a5e68943863fa8b5231b59" -uuid = "e7412a2a-1a6e-54c0-be00-318e2571c051" -version = "1.3.6+0" - -[[deps.OpenBLASConsistentFPCSR_jll]] -deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl"] -git-tree-sha1 = "567515ca155d0020a45b05175449b499c63e7015" -uuid = "6cdc7f73-28fd-5e50-80fb-958a8875b1af" -version = "0.3.29+0" - -[[deps.OpenBLAS_jll]] -deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] -uuid = "4536629a-c528-5b80-bd46-f80d51c5b363" -version = "0.3.27+1" - -[[deps.OpenEXR]] -deps = ["Colors", "FileIO", "OpenEXR_jll"] -git-tree-sha1 = "97db9e07fe2091882c765380ef58ec553074e9c7" -uuid = "52e1d378-f018-4a11-a4be-720524705ac7" -version = "0.3.3" - -[[deps.OpenEXR_jll]] -deps = ["Artifacts", "Imath_jll", "JLLWrappers", "Libdl", "Zlib_jll"] -git-tree-sha1 = "8292dd5c8a38257111ada2174000a33745b06d4e" -uuid = "18a262bb-aa17-5467-a713-aee519bc75cb" -version = "3.2.4+0" - -[[deps.OpenJpeg_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl", "Libtiff_jll", "LittleCMS_jll", "libpng_jll"] -git-tree-sha1 = "7dc7028a10d1408e9103c0a77da19fdedce4de6c" -uuid = "643b3616-a352-519d-856d-80112ee9badc" -version = "2.5.4+0" - -[[deps.OpenLibm_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "05823500-19ac-5b8b-9628-191a04bc5112" -version = "0.8.5+0" - -[[deps.OpenSSL_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "2ae7d4ddec2e13ad3bddf5c0796f7547cf682391" -uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" -version = "3.5.2+0" - -[[deps.OpenSpecFun_jll]] -deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl"] -git-tree-sha1 = "1346c9208249809840c91b26703912dff463d335" -uuid = "efe28fd5-8261-553b-a9e1-b2916fc3738e" -version = "0.5.6+0" - -[[deps.Opus_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "c392fc5dd032381919e3b22dd32d6443760ce7ea" -uuid = "91d4177d-7536-5919-b921-800302f37372" -version = "1.5.2+0" - -[[deps.OrderedCollections]] -git-tree-sha1 = "05868e21324cede2207c6f0f466b4bfef6d5e7ee" -uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" -version = "1.8.1" - -[[deps.PCRE2_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "efcefdf7-47ab-520b-bdef-62a2eaa19f15" -version = "10.42.0+1" - -[[deps.PDMats]] -deps = ["LinearAlgebra", "SparseArrays", "SuiteSparse"] -git-tree-sha1 = "f07c06228a1c670ae4c87d1276b92c7c597fdda0" -uuid = "90014a1f-27ba-587c-ab20-58faa44d9150" -version = "0.11.35" - -[[deps.PNGFiles]] -deps = ["Base64", "CEnum", "ImageCore", "IndirectArrays", "OffsetArrays", "libpng_jll"] -git-tree-sha1 = "cf181f0b1e6a18dfeb0ee8acc4a9d1672499626c" -uuid = "f57f5aa1-a3ce-4bc8-8ab9-96f992907883" -version = "0.4.4" - -[[deps.Packing]] -deps = ["GeometryBasics"] -git-tree-sha1 = "bc5bf2ea3d5351edf285a06b0016788a121ce92c" -uuid = "19eb6ba3-879d-56ad-ad62-d5c202156566" -version = "0.5.1" - -[[deps.PaddedViews]] -deps = ["OffsetArrays"] -git-tree-sha1 = "0fac6313486baae819364c52b4f483450a9d793f" -uuid = "5432bcbf-9aad-5242-b902-cca2824c8663" -version = "0.5.12" - -[[deps.Parsers]] -deps = ["Dates", "PrecompileTools", "UUIDs"] -git-tree-sha1 = "7d2f8f21da5db6a806faf7b9b292296da42b2810" -uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" -version = "2.8.3" - -[[deps.Pixman_jll]] -deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "LLVMOpenMP_jll", "Libdl"] -git-tree-sha1 = "db76b1ecd5e9715f3d043cec13b2ec93ce015d53" -uuid = "30392449-352a-5448-841d-b1acce4e97dc" -version = "0.44.2+0" - -[[deps.Pkg]] -deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"] -uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -version = "1.11.0" -weakdeps = ["REPL"] - - [deps.Pkg.extensions] - REPLExt = "REPL" - -[[deps.PkgVersion]] -deps = ["Pkg"] -git-tree-sha1 = "f9501cc0430a26bc3d156ae1b5b0c1b47af4d6da" -uuid = "eebad327-c553-4316-9ea0-9fa01ccd7688" -version = "0.3.3" - -[[deps.PlotUtils]] -deps = ["ColorSchemes", "Colors", "Dates", "PrecompileTools", "Printf", "Random", "Reexport", "StableRNGs", "Statistics"] -git-tree-sha1 = "3ca9a356cd2e113c420f2c13bea19f8d3fb1cb18" -uuid = "995b91a9-d308-5afd-9ec6-746e21dbc043" -version = "1.4.3" - -[[deps.PolygonOps]] -git-tree-sha1 = "77b3d3605fc1cd0b42d95eba87dfcd2bf67d5ff6" -uuid = "647866c9-e3ac-4575-94e7-e3d426903924" -version = "0.1.2" - -[[deps.PrecompileTools]] -deps = ["Preferences"] -git-tree-sha1 = "5aa36f7049a63a1528fe8f7c3f2113413ffd4e1f" -uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a" -version = "1.2.1" - -[[deps.Preferences]] -deps = ["TOML"] -git-tree-sha1 = "0f27480397253da18fe2c12a4ba4eb9eb208bf3d" -uuid = "21216c6a-2e73-6563-6e65-726566657250" -version = "1.5.0" - -[[deps.Printf]] -deps = ["Unicode"] -uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" -version = "1.11.0" - -[[deps.Profile]] -uuid = "9abbd945-dff8-562f-b5e8-e1ebf5ef1b79" -version = "1.11.0" - -[[deps.ProgressMeter]] -deps = ["Distributed", "Printf"] -git-tree-sha1 = "13c5103482a8ed1536a54c08d0e742ae3dca2d42" -uuid = "92933f4c-e287-5a05-a399-4b506db050ca" -version = "1.10.4" - -[[deps.PtrArrays]] -git-tree-sha1 = "1d36ef11a9aaf1e8b74dacc6a731dd1de8fd493d" -uuid = "43287f4e-b6f4-7ad1-bb20-aadabca52c3d" -version = "1.3.0" - -[[deps.QOI]] -deps = ["ColorTypes", "FileIO", "FixedPointNumbers"] -git-tree-sha1 = "8b3fc30bc0390abdce15f8822c889f669baed73d" -uuid = "4b34888f-f399-49d4-9bb3-47ed5cae4e65" -version = "1.0.1" - -[[deps.QuadGK]] -deps = ["DataStructures", "LinearAlgebra"] -git-tree-sha1 = "9da16da70037ba9d701192e27befedefb91ec284" -uuid = "1fd47b50-473d-5c70-9696-f719f8f3bcdc" -version = "2.11.2" - - [deps.QuadGK.extensions] - QuadGKEnzymeExt = "Enzyme" - - [deps.QuadGK.weakdeps] - Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" - -[[deps.REPL]] -deps = ["InteractiveUtils", "Markdown", "Sockets", "StyledStrings", "Unicode"] -uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" -version = "1.11.0" - -[[deps.Random]] -deps = ["SHA"] -uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -version = "1.11.0" - -[[deps.RandomNumbers]] -deps = ["Random"] -git-tree-sha1 = "c6ec94d2aaba1ab2ff983052cf6a606ca5985902" -uuid = "e6cf234a-135c-5ec9-84dd-332b85af5143" -version = "1.6.0" - -[[deps.RangeArrays]] -git-tree-sha1 = "b9039e93773ddcfc828f12aadf7115b4b4d225f5" -uuid = "b3c3ace0-ae52-54e7-9d0b-2c1406fd6b9d" -version = "0.3.2" - -[[deps.Ratios]] -deps = ["Requires"] -git-tree-sha1 = "1342a47bf3260ee108163042310d26f2be5ec90b" -uuid = "c84ed2f1-dad5-54f0-aa8e-dbefe2724439" -version = "0.4.5" -weakdeps = ["FixedPointNumbers"] - - [deps.Ratios.extensions] - RatiosFixedPointNumbersExt = "FixedPointNumbers" - -[[deps.RayCaster]] -deps = ["Atomix", "GeometryBasics", "KernelAbstractions", "LinearAlgebra", "Random", "RandomNumbers", "StaticArrays", "Statistics"] -path = "../" -uuid = "afc56b53-c9a9-482a-a956-d1d800e05558" -version = "0.1.0" - -[[deps.Reexport]] -git-tree-sha1 = "45e428421666073eab6f2da5c9d310d99bb12f9b" -uuid = "189a3867-3050-52da-a836-e630ba90ab69" -version = "1.2.2" - -[[deps.RelocatableFolders]] -deps = ["SHA", "Scratch"] -git-tree-sha1 = "ffdaf70d81cf6ff22c2b6e733c900c3321cab864" -uuid = "05181044-ff0b-4ac5-8273-598c1e38db00" -version = "1.0.1" - -[[deps.Requires]] -deps = ["UUIDs"] -git-tree-sha1 = "62389eeff14780bfe55195b7204c0d8738436d64" -uuid = "ae029012-a4dd-5104-9daa-d747884805df" -version = "1.3.1" - -[[deps.Rmath]] -deps = ["Random", "Rmath_jll"] -git-tree-sha1 = "852bd0f55565a9e973fcfee83a84413270224dc4" -uuid = "79098fc4-a85e-5d69-aa6a-4863f24498fa" -version = "0.8.0" - -[[deps.Rmath_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "58cdd8fb2201a6267e1db87ff148dd6c1dbd8ad8" -uuid = "f50d1b31-88e8-58de-be2c-1cc44531875f" -version = "0.5.1+0" - -[[deps.RoundingEmulator]] -git-tree-sha1 = "40b9edad2e5287e05bd413a38f61a8ff55b9557b" -uuid = "5eaf0fd0-dfba-4ccb-bf02-d820a40db705" -version = "0.2.1" - -[[deps.SHA]] -uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" -version = "0.7.0" - -[[deps.SIMD]] -deps = ["PrecompileTools"] -git-tree-sha1 = "fea870727142270bdf7624ad675901a1ee3b4c87" -uuid = "fdea26ae-647d-5447-a871-4b548cad5224" -version = "3.7.1" - -[[deps.ScopedValues]] -deps = ["HashArrayMappedTries", "Logging"] -git-tree-sha1 = "7f44eef6b1d284465fafc66baf4d9bdcc239a15b" -uuid = "7e506255-f358-4e82-b7e4-beb19740aa63" -version = "1.4.0" - -[[deps.Scratch]] -deps = ["Dates"] -git-tree-sha1 = "9b81b8393e50b7d4e6d0a9f14e192294d3b7c109" -uuid = "6c6a2e73-6563-6170-7368-637461726353" -version = "1.3.0" - -[[deps.Serialization]] -uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" -version = "1.11.0" - -[[deps.ShaderAbstractions]] -deps = ["ColorTypes", "FixedPointNumbers", "GeometryBasics", "LinearAlgebra", "Observables", "StaticArrays"] -git-tree-sha1 = "818554664a2e01fc3784becb2eb3a82326a604b6" -uuid = "65257c39-d410-5151-9873-9b3e5be5013e" -version = "0.5.0" - -[[deps.SharedArrays]] -deps = ["Distributed", "Mmap", "Random", "Serialization"] -uuid = "1a1011a3-84de-559e-8e89-a11a2f7dc383" -version = "1.11.0" - -[[deps.Showoff]] -deps = ["Dates", "Grisu"] -git-tree-sha1 = "91eddf657aca81df9ae6ceb20b959ae5653ad1de" -uuid = "992d4aef-0814-514b-bc4d-f2e9a6c4116f" -version = "1.0.3" - -[[deps.SignedDistanceFields]] -deps = ["Random", "Statistics", "Test"] -git-tree-sha1 = "d263a08ec505853a5ff1c1ebde2070419e3f28e9" -uuid = "73760f76-fbc4-59ce-8f25-708e95d2df96" -version = "0.4.0" - -[[deps.SimpleTraits]] -deps = ["InteractiveUtils", "MacroTools"] -git-tree-sha1 = "be8eeac05ec97d379347584fa9fe2f5f76795bcb" -uuid = "699a6c99-e7fa-54fc-8d76-47d257e15c1d" -version = "0.9.5" - -[[deps.Sixel]] -deps = ["Dates", "FileIO", "ImageCore", "IndirectArrays", "OffsetArrays", "REPL", "libsixel_jll"] -git-tree-sha1 = "0494aed9501e7fb65daba895fb7fd57cc38bc743" -uuid = "45858cf5-a6b0-47a3-bbea-62219f50df47" -version = "0.1.5" - -[[deps.Sockets]] -uuid = "6462fe0b-24de-5631-8697-dd941f90decc" -version = "1.11.0" - -[[deps.SortingAlgorithms]] -deps = ["DataStructures"] -git-tree-sha1 = "64d974c2e6fdf07f8155b5b2ca2ffa9069b608d9" -uuid = "a2af1166-a08f-5f64-846c-94a0d3cef48c" -version = "1.2.2" - -[[deps.SparseArrays]] -deps = ["Libdl", "LinearAlgebra", "Random", "Serialization", "SuiteSparse_jll"] -uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" -version = "1.11.0" - -[[deps.SpecialFunctions]] -deps = ["IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"] -git-tree-sha1 = "41852b8679f78c8d8961eeadc8f62cef861a52e3" -uuid = "276daf66-3868-5448-9aa4-cd146d93841b" -version = "2.5.1" -weakdeps = ["ChainRulesCore"] - - [deps.SpecialFunctions.extensions] - SpecialFunctionsChainRulesCoreExt = "ChainRulesCore" - -[[deps.StableRNGs]] -deps = ["Random"] -git-tree-sha1 = "95af145932c2ed859b63329952ce8d633719f091" -uuid = "860ef19b-820b-49d6-a774-d7a799459cd3" -version = "1.0.3" - -[[deps.StackViews]] -deps = ["OffsetArrays"] -git-tree-sha1 = "be1cf4eb0ac528d96f5115b4ed80c26a8d8ae621" -uuid = "cae243ae-269e-4f55-b966-ac2d0dc13c15" -version = "0.1.2" - -[[deps.StaticArrays]] -deps = ["LinearAlgebra", "PrecompileTools", "Random", "StaticArraysCore"] -git-tree-sha1 = "cbea8a6bd7bed51b1619658dec70035e07b8502f" -uuid = "90137ffa-7385-5640-81b9-e52037218182" -version = "1.9.14" -weakdeps = ["ChainRulesCore", "Statistics"] - - [deps.StaticArrays.extensions] - StaticArraysChainRulesCoreExt = "ChainRulesCore" - StaticArraysStatisticsExt = "Statistics" - -[[deps.StaticArraysCore]] -git-tree-sha1 = "192954ef1208c7019899fbf8049e717f92959682" -uuid = "1e83bf80-4336-4d27-bf5d-d5a4f845583c" -version = "1.4.3" - -[[deps.Statistics]] -deps = ["LinearAlgebra"] -git-tree-sha1 = "ae3bb1eb3bba077cd276bc5cfc337cc65c3075c0" -uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" -version = "1.11.1" -weakdeps = ["SparseArrays"] - - [deps.Statistics.extensions] - SparseArraysExt = ["SparseArrays"] - -[[deps.StatsAPI]] -deps = ["LinearAlgebra"] -git-tree-sha1 = "9d72a13a3f4dd3795a195ac5a44d7d6ff5f552ff" -uuid = "82ae8749-77ed-4fe6-ae5f-f523153014b0" -version = "1.7.1" - -[[deps.StatsBase]] -deps = ["AliasTables", "DataAPI", "DataStructures", "LinearAlgebra", "LogExpFunctions", "Missings", "Printf", "Random", "SortingAlgorithms", "SparseArrays", "Statistics", "StatsAPI"] -git-tree-sha1 = "2c962245732371acd51700dbb268af311bddd719" -uuid = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" -version = "0.34.6" - -[[deps.StatsFuns]] -deps = ["HypergeometricFunctions", "IrrationalConstants", "LogExpFunctions", "Reexport", "Rmath", "SpecialFunctions"] -git-tree-sha1 = "8e45cecc66f3b42633b8ce14d431e8e57a3e242e" -uuid = "4c63d2b9-4356-54db-8cca-17b64c39e42c" -version = "1.5.0" -weakdeps = ["ChainRulesCore", "InverseFunctions"] - - [deps.StatsFuns.extensions] - StatsFunsChainRulesCoreExt = "ChainRulesCore" - StatsFunsInverseFunctionsExt = "InverseFunctions" - -[[deps.StructArrays]] -deps = ["ConstructionBase", "DataAPI", "Tables"] -git-tree-sha1 = "8ad2e38cbb812e29348719cc63580ec1dfeb9de4" -uuid = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" -version = "0.7.1" -weakdeps = ["Adapt", "GPUArraysCore", "KernelAbstractions", "LinearAlgebra", "SparseArrays", "StaticArrays"] - - [deps.StructArrays.extensions] - StructArraysAdaptExt = "Adapt" - StructArraysGPUArraysCoreExt = ["GPUArraysCore", "KernelAbstractions"] - StructArraysLinearAlgebraExt = "LinearAlgebra" - StructArraysSparseArraysExt = "SparseArrays" - StructArraysStaticArraysExt = "StaticArrays" - -[[deps.StyledStrings]] -uuid = "f489334b-da3d-4c2e-b8f0-e476e12c162b" -version = "1.11.0" - -[[deps.SuiteSparse]] -deps = ["Libdl", "LinearAlgebra", "Serialization", "SparseArrays"] -uuid = "4607b0f0-06f3-5cda-b6b1-a6196a1729e9" - -[[deps.SuiteSparse_jll]] -deps = ["Artifacts", "Libdl", "libblastrampoline_jll"] -uuid = "bea87d4a-7f5b-5778-9afe-8cc45184846c" -version = "7.7.0+0" - -[[deps.TOML]] -deps = ["Dates"] -uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" -version = "1.0.3" - -[[deps.TableTraits]] -deps = ["IteratorInterfaceExtensions"] -git-tree-sha1 = "c06b2f539df1c6efa794486abfb6ed2022561a39" -uuid = "3783bdb8-4a98-5b6b-af9a-565f29a5fe9c" -version = "1.0.1" - -[[deps.Tables]] -deps = ["DataAPI", "DataValueInterfaces", "IteratorInterfaceExtensions", "OrderedCollections", "TableTraits"] -git-tree-sha1 = "f2c1efbc8f3a609aadf318094f8fc5204bdaf344" -uuid = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" -version = "1.12.1" - -[[deps.Tar]] -deps = ["ArgTools", "SHA"] -uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" -version = "1.10.0" - -[[deps.TensorCore]] -deps = ["LinearAlgebra"] -git-tree-sha1 = "1feb45f88d133a655e001435632f019a9a1bcdb6" -uuid = "62fd8b95-f654-4bbd-a8a5-9c27f68ccd50" -version = "0.1.1" - -[[deps.Test]] -deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] -uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -version = "1.11.0" - -[[deps.TiffImages]] -deps = ["ColorTypes", "DataStructures", "DocStringExtensions", "FileIO", "FixedPointNumbers", "IndirectArrays", "Inflate", "Mmap", "OffsetArrays", "PkgVersion", "PrecompileTools", "ProgressMeter", "SIMD", "UUIDs"] -git-tree-sha1 = "02aca429c9885d1109e58f400c333521c13d48a0" -uuid = "731e570b-9d59-4bfa-96dc-6df516fadf69" -version = "0.11.4" - -[[deps.Tracy]] -deps = ["ExprTools", "LibTracyClient_jll", "Libdl"] -git-tree-sha1 = "91dbaee0f50faa4357f7e9fc69442c7b6364dfe5" -uuid = "e689c965-62c8-4b79-b2c5-8359227902fd" -version = "0.1.5" - - [deps.Tracy.extensions] - TracyProfilerExt = "TracyProfiler_jll" - - [deps.Tracy.weakdeps] - TracyProfiler_jll = "0c351ed6-8a68-550e-8b79-de6f926da83c" - -[[deps.TranscodingStreams]] -git-tree-sha1 = "0c45878dcfdcfa8480052b6ab162cdd138781742" -uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" -version = "0.11.3" - -[[deps.TriplotBase]] -git-tree-sha1 = "4d4ed7f294cda19382ff7de4c137d24d16adc89b" -uuid = "981d1d27-644d-49a2-9326-4793e63143c3" -version = "0.1.0" - -[[deps.TypedSyntax]] -deps = ["CodeTracking", "JuliaSyntax"] -git-tree-sha1 = "1465a8187b3d512a99fef13244c213b54e34615d" -uuid = "d265eb64-f81a-44ad-a842-4247ee1503de" -version = "1.4.2" - -[[deps.UUIDs]] -deps = ["Random", "SHA"] -uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" -version = "1.11.0" - -[[deps.Unicode]] -uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" -version = "1.11.0" - -[[deps.UnicodeFun]] -deps = ["REPL"] -git-tree-sha1 = "53915e50200959667e78a92a418594b428dffddf" -uuid = "1cfade01-22cf-5700-b092-accc4b62d6e1" -version = "0.4.1" - -[[deps.Unitful]] -deps = ["Dates", "LinearAlgebra", "Random"] -git-tree-sha1 = "6258d453843c466d84c17a58732dda5deeb8d3af" -uuid = "1986cc42-f94f-5a68-af5c-568840ba703d" -version = "1.24.0" - - [deps.Unitful.extensions] - ConstructionBaseUnitfulExt = "ConstructionBase" - ForwardDiffExt = "ForwardDiff" - InverseFunctionsUnitfulExt = "InverseFunctions" - PrintfExt = "Printf" - - [deps.Unitful.weakdeps] - ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9" - ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" - InverseFunctions = "3587e190-3f89-42d0-90ee-14403ec27112" - Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" - -[[deps.UnsafeAtomics]] -git-tree-sha1 = "b13c4edda90890e5b04ba24e20a310fbe6f249ff" -uuid = "013be700-e6cd-48c3-b4a1-df204f14c38f" -version = "0.3.0" -weakdeps = ["LLVM"] - - [deps.UnsafeAtomics.extensions] - UnsafeAtomicsLLVM = ["LLVM"] - -[[deps.WebP]] -deps = ["CEnum", "ColorTypes", "FileIO", "FixedPointNumbers", "ImageCore", "libwebp_jll"] -git-tree-sha1 = "aa1ca3c47f119fbdae8770c29820e5e6119b83f2" -uuid = "e3aaa7dc-3e4b-44e0-be63-ffb868ccd7c1" -version = "0.1.3" - -[[deps.WidthLimitedIO]] -deps = ["Unicode"] -git-tree-sha1 = "71142739e695823729a335e9bc124ef41ec1433a" -uuid = "b8c1c048-cf81-46c6-9da0-18c1d99e41f2" -version = "1.0.1" - -[[deps.WoodburyMatrices]] -deps = ["LinearAlgebra", "SparseArrays"] -git-tree-sha1 = "c1a7aa6219628fcd757dede0ca95e245c5cd9511" -uuid = "efce3f68-66dc-5838-9240-27a6d6f5f9b6" -version = "1.0.0" - -[[deps.XZ_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "fee71455b0aaa3440dfdd54a9a36ccef829be7d4" -uuid = "ffd25f8a-64ca-5728-b0f7-c24cf3aae800" -version = "5.8.1+0" - -[[deps.Xorg_libX11_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libxcb_jll", "Xorg_xtrans_jll"] -git-tree-sha1 = "b5899b25d17bf1889d25906fb9deed5da0c15b3b" -uuid = "4f6342f7-b3d2-589e-9d20-edeb45f2b2bc" -version = "1.8.12+0" - -[[deps.Xorg_libXau_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "aa1261ebbac3ccc8d16558ae6799524c450ed16b" -uuid = "0c0b7dd1-d40b-584c-a123-a41640f87eec" -version = "1.0.13+0" - -[[deps.Xorg_libXdmcp_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "52858d64353db33a56e13c341d7bf44cd0d7b309" -uuid = "a3789734-cfe1-5b06-b2d0-1dd0d9d62d05" -version = "1.1.6+0" - -[[deps.Xorg_libXext_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libX11_jll"] -git-tree-sha1 = "a4c0ee07ad36bf8bbce1c3bb52d21fb1e0b987fb" -uuid = "1082639a-0dae-5f34-9b06-72781eeb8cb3" -version = "1.3.7+0" - -[[deps.Xorg_libXrender_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libX11_jll"] -git-tree-sha1 = "7ed9347888fac59a618302ee38216dd0379c480d" -uuid = "ea2f1a96-1ddc-540d-b46f-429655e07cfa" -version = "0.9.12+0" - -[[deps.Xorg_libxcb_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libXau_jll", "Xorg_libXdmcp_jll"] -git-tree-sha1 = "bfcaf7ec088eaba362093393fe11aa141fa15422" -uuid = "c7cfdc94-dc32-55de-ac96-5a1b8d977c5b" -version = "1.17.1+0" - -[[deps.Xorg_xtrans_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "a63799ff68005991f9d9491b6e95bd3478d783cb" -uuid = "c5fb5394-a638-5e4d-96e5-b29de1b5cf10" -version = "1.6.0+0" - -[[deps.Zlib_jll]] -deps = ["Libdl"] -uuid = "83775a58-1f1d-513f-b197-d71354ab007a" -version = "1.2.13+1" - -[[deps.Zstd_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "446b23e73536f84e8037f5dce465e92275f6a308" -uuid = "3161d3a3-bdf6-5164-811a-617609db77b4" -version = "1.5.7+1" - -[[deps.isoband_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] -git-tree-sha1 = "51b5eeb3f98367157a7a12a1fb0aa5328946c03c" -uuid = "9a68df92-36a6-505f-a73e-abb412b6bfb4" -version = "0.2.3+0" - -[[deps.libaom_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "4bba74fa59ab0755167ad24f98800fe5d727175b" -uuid = "a4ae2306-e953-59d6-aa16-d00cac43593b" -version = "3.12.1+0" - -[[deps.libass_jll]] -deps = ["Artifacts", "Bzip2_jll", "FreeType2_jll", "FriBidi_jll", "HarfBuzz_jll", "JLLWrappers", "Libdl", "Zlib_jll"] -git-tree-sha1 = "125eedcb0a4a0bba65b657251ce1d27c8714e9d6" -uuid = "0ac62f75-1d6f-5e53-bd7c-93b484bb37c0" -version = "0.17.4+0" - -[[deps.libblastrampoline_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "8e850b90-86db-534c-a0d3-1478176c7d93" -version = "5.11.0+0" - -[[deps.libfdk_aac_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "646634dd19587a56ee2f1199563ec056c5f228df" -uuid = "f638f0a6-7fb0-5443-88ba-1cc74229b280" -version = "2.0.4+0" - -[[deps.libpng_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl", "Zlib_jll"] -git-tree-sha1 = "07b6a107d926093898e82b3b1db657ebe33134ec" -uuid = "b53b4c65-9356-5827-b1ea-8c7a1a84506f" -version = "1.6.50+0" - -[[deps.libsixel_jll]] -deps = ["Artifacts", "JLLWrappers", "JpegTurbo_jll", "Libdl", "libpng_jll"] -git-tree-sha1 = "c1733e347283df07689d71d61e14be986e49e47a" -uuid = "075b6546-f08a-558a-be8f-8157d0f608a5" -version = "1.10.5+0" - -[[deps.libvorbis_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl", "Ogg_jll"] -git-tree-sha1 = "11e1772e7f3cc987e9d3de991dd4f6b2602663a5" -uuid = "f27f6e37-5d2b-51aa-960f-b287f2bc3b7a" -version = "1.3.8+0" - -[[deps.libwebp_jll]] -deps = ["Artifacts", "Giflib_jll", "JLLWrappers", "JpegTurbo_jll", "Libdl", "Libglvnd_jll", "Libtiff_jll", "libpng_jll"] -git-tree-sha1 = "ccbb625a89ec6195856a50aa2b668a5c08712c94" -uuid = "c5f90fcd-3b7e-5836-afba-fc50a0988cb2" -version = "1.4.0+0" - -[[deps.nghttp2_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" -version = "1.59.0+0" - -[[deps.oneTBB_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "d5a767a3bb77135a99e433afe0eb14cd7f6914c3" -uuid = "1317d2d5-d96f-522e-a858-c73665f53c3e" -version = "2022.0.0+0" - -[[deps.p7zip_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" -version = "17.4.0+2" - -[[deps.x264_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "14cc7083fc6dff3cc44f2bc435ee96d06ed79aa7" -uuid = "1270edf5-f2f9-52d2-97e9-ab00b5d0237a" -version = "10164.0.1+0" - -[[deps.x265_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "e7b67590c14d487e734dcb925924c5dc43ec85f3" -uuid = "dfaa095f-4041-5dcd-9319-2fabd8486b76" -version = "4.1.0+0" diff --git a/test/Project.toml b/test/Project.toml deleted file mode 100644 index 5fe351b..0000000 --- a/test/Project.toml +++ /dev/null @@ -1,18 +0,0 @@ -[deps] -BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" -Cthulhu = "f68482b8-f384-11e8-15f7-abe071a5a75f" -FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" -GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" -ImageCore = "a09fc81d-aa75-5fe9-8630-4744c3626534" -ImageIO = "82e4d734-157c-48bb-816b-45c225c6df19" -ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" -ImageShow = "4e3cecfd-b093-5904-9786-8bbb286a6a31" -KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c" -LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" -Metal = "dde4c033-4e86-420c-a63e-0dd931031962" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -RayCaster = "afc56b53-c9a9-482a-a956-d1d800e05559" - -[sources] -RayCaster = {path = "../"} diff --git a/test/bounds.jl b/test/bounds.jl new file mode 100644 index 0000000..c55c0f4 --- /dev/null +++ b/test/bounds.jl @@ -0,0 +1,228 @@ +@testset "Bounds construction" begin + # Test Bounds2 + b2 = RayCaster.Bounds2(Point2f(1, 2), Point2f(3, 4)) + @test b2.p_min == Point2f(1, 2) + @test b2.p_max == Point2f(3, 4) + + # Test Bounds3 + b3 = RayCaster.Bounds3(Point3f(1, 2, 3), Point3f(4, 5, 6)) + @test b3.p_min == Point3f(1, 2, 3) + @test b3.p_max == Point3f(4, 5, 6) + + # Test default constructors (invalid configuration) + b2_default = RayCaster.Bounds2() + @test b2_default.p_min == Point2f(Inf32) + @test b2_default.p_max == Point2f(-Inf32) + + b3_default = RayCaster.Bounds3() + @test b3_default.p_min == Point3f(Inf32) + @test b3_default.p_max == Point3f(-Inf32) + + # Test point constructors + b2_point = RayCaster.Bounds2(Point2f(5, 6)) + @test b2_point.p_min == Point2f(5, 6) + @test b2_point.p_max == Point2f(5, 6) + + b3_point = RayCaster.Bounds3(Point3f(7, 8, 9)) + @test b3_point.p_min == Point3f(7, 8, 9) + @test b3_point.p_max == Point3f(7, 8, 9) + + # Test corrected constructors (swap min/max if needed) + b2_corrected = RayCaster.Bounds2c(Point2f(3, 4), Point2f(1, 2)) + @test b2_corrected.p_min == Point2f(1, 2) + @test b2_corrected.p_max == Point2f(3, 4) + + b3_corrected = RayCaster.Bounds3c(Point3f(4, 5, 6), Point3f(1, 2, 3)) + @test b3_corrected.p_min == Point3f(1, 2, 3) + @test b3_corrected.p_max == Point3f(4, 5, 6) +end + +@testset "Bounds comparison" begin + b1 = RayCaster.Bounds3(Point3f(1, 2, 3), Point3f(4, 5, 6)) + b2 = RayCaster.Bounds3(Point3f(1, 2, 3), Point3f(4, 5, 6)) + b3 = RayCaster.Bounds3(Point3f(1, 2, 3), Point3f(4, 5, 7)) + + @test b1 == b2 + @test b1 != b3 + @test b1 ≈ b2 + + # Test approximate equality with small differences + b4 = RayCaster.Bounds3(Point3f(1, 2, 3), Point3f(4, 5, 6.000001)) + @test b1 ≈ b4 +end + +@testset "Bounds getindex" begin + b = RayCaster.Bounds3(Point3f(1, 2, 3), Point3f(4, 5, 6)) + @test b[1] == Point3f(1, 2, 3) + @test b[2] == Point3f(4, 5, 6) + @test all(isnan.(b[3])) # Invalid index returns NaN +end + +@testset "Bounds validity" begin + b_valid = RayCaster.Bounds3(Point3f(1, 2, 3), Point3f(4, 5, 6)) + @test RayCaster.is_valid(b_valid) + + b_invalid = RayCaster.Bounds3() + @test !RayCaster.is_valid(b_invalid) +end + +@testset "Bounds2 iteration" begin + b = RayCaster.Bounds2(Point2f(1f0, 3f0), Point2f(4f0, 4f0)) + targets = [ + Point2f(1f0, 3f0), Point2f(2f0, 3f0), Point2f(3f0, 3f0), Point2f(4f0, 3f0), + Point2f(1f0, 4f0), Point2f(2f0, 4f0), Point2f(3f0, 4f0), Point2f(4f0, 4f0), + ] + @test length(b) == 8 + for (p, t) in zip(b, targets) + @test p == t + end + + b = RayCaster.Bounds2(Point2f(-1f0), Point2f(1f0)) + targets = [ + Point2f(-1f0, -1f0), Point2f(0f0, -1f0), Point2f(1f0, -1f0), + Point2f(-1f0, 0f0), Point2f(0f0, 0f0), Point2f(1f0, 0f0), + Point2f(-1f0, 1f0), Point2f(0f0, 1f0), Point2f(1f0, 1f0), + ] + @test length(b) == 9 + for (p, t) in zip(b, targets) + @test p == t + end +end + +@testset "Bounds3 corner" begin + b = RayCaster.Bounds3(Point3f(0, 0, 0), Point3f(1, 1, 1)) + @test RayCaster.corner(b, 1) == Point3f(0, 0, 0) + @test RayCaster.corner(b, 2) == Point3f(1, 0, 0) + @test RayCaster.corner(b, 3) == Point3f(0, 1, 0) + @test RayCaster.corner(b, 4) == Point3f(1, 1, 0) + @test RayCaster.corner(b, 5) == Point3f(0, 0, 1) + @test RayCaster.corner(b, 6) == Point3f(1, 0, 1) + @test RayCaster.corner(b, 7) == Point3f(0, 1, 1) + @test RayCaster.corner(b, 8) == Point3f(1, 1, 1) +end + +@testset "Bounds union and intersect" begin + b1 = RayCaster.Bounds3(Point3f(0, 0, 0), Point3f(2, 2, 2)) + b2 = RayCaster.Bounds3(Point3f(1, 1, 1), Point3f(3, 3, 3)) + + # Union should contain both bounds + b_union = union(b1, b2) + @test b_union.p_min == Point3f(0, 0, 0) + @test b_union.p_max == Point3f(3, 3, 3) + + # Intersection should be the overlap + b_intersect = intersect(b1, b2) + @test b_intersect.p_min == Point3f(1, 1, 1) + @test b_intersect.p_max == Point3f(2, 2, 2) +end + +@testset "Bounds overlap and containment" begin + b1 = RayCaster.Bounds3(Point3f(0, 0, 0), Point3f(2, 2, 2)) + b2 = RayCaster.Bounds3(Point3f(1, 1, 1), Point3f(3, 3, 3)) + b3 = RayCaster.Bounds3(Point3f(5, 5, 5), Point3f(6, 6, 6)) + + @test RayCaster.overlaps(b1, b2) + @test !RayCaster.overlaps(b1, b3) + + # Test point containment + @test RayCaster.inside(b1, Point3f(1, 1, 1)) + @test RayCaster.inside(b1, Point3f(0, 0, 0)) # On boundary + @test RayCaster.inside(b1, Point3f(2, 2, 2)) # On boundary + @test !RayCaster.inside(b1, Point3f(3, 3, 3)) + + # Test exclusive containment + @test RayCaster.inside_exclusive(b1, Point3f(1, 1, 1)) + @test RayCaster.inside_exclusive(b1, Point3f(0, 0, 0)) # On min boundary (inclusive) + @test !RayCaster.inside_exclusive(b1, Point3f(2, 2, 2)) # On max boundary (exclusive) +end + +@testset "Bounds geometric properties" begin + b = RayCaster.Bounds3(Point3f(0, 0, 0), Point3f(2, 3, 4)) + + # Diagonal + @test RayCaster.diagonal(b) == Point3f(2, 3, 4) + + # Surface area: 2*(2*3 + 2*4 + 3*4) = 2*(6 + 8 + 12) = 52 + @test RayCaster.surface_area(b) == 52f0 + + # Volume: 2 * 3 * 4 = 24 + @test RayCaster.volume(b) == 24f0 + + # Sides + @test RayCaster.sides(b) == Point3f(2, 3, 4) + + # Inclusive sides + @test RayCaster.inclusive_sides(b) == Point3f(3, 4, 5) + + # Expand + b_expanded = RayCaster.expand(b, 1f0) + @test b_expanded.p_min == Point3f(-1, -1, -1) + @test b_expanded.p_max == Point3f(3, 4, 5) + + # Maximum extent (longest axis) + @test RayCaster.maximum_extent(b) == 3 # z-axis is longest + + b2 = RayCaster.Bounds3(Point3f(0, 0, 0), Point3f(5, 2, 3)) + @test RayCaster.maximum_extent(b2) == 1 # x-axis is longest +end + +@testset "Bounds2 area" begin + b = RayCaster.Bounds2(Point2f(0, 0), Point2f(3, 4)) + @test RayCaster.area(b) == 12f0 +end + +@testset "Bounds lerp and offset" begin + b = RayCaster.Bounds3(Point3f(0, 0, 0), Point3f(10, 10, 10)) + + # Lerp + p_lerped = RayCaster.lerp(b, Point3f(0.5, 0.5, 0.5)) + @test p_lerped == Point3f(-4.5, -4.5, -4.5) + + # Offset + p = Point3f(5, 5, 5) + offset_result = RayCaster.offset(b, p) + @test offset_result == Point3f(0.5, 0.5, 0.5) + + # Edge case: degenerate bounds + b_degenerate = RayCaster.Bounds3(Point3f(5, 5, 5), Point3f(5, 5, 5)) + offset_degenerate = RayCaster.offset(b_degenerate, Point3f(5, 5, 5)) + @test offset_degenerate == Point3f(0, 0, 0) +end + +@testset "Bounding sphere" begin + b = RayCaster.Bounds3(Point3f(0, 0, 0), Point3f(2, 2, 2)) + center, radius = RayCaster.bounding_sphere(b) + @test center == Point3f(1, 1, 1) + @test radius ≈ sqrt(3.0f0) +end + +@testset "Ray-Bounds intersection" begin + b = RayCaster.Bounds3(Point3f(1), Point3f(2)) + + # Ray hitting the bounds + r1 = RayCaster.Ray(o = Point3f(0), d = Vec3f(1)) + hit, t0, t1 = RayCaster.intersect(b, r1) + @test hit + @test t0 ≈ 1f0 + @test t1 ≈ 2f0 + + # Ray missing the bounds + r2 = RayCaster.Ray(o = Point3f(0), d = Vec3f(1, 0, 0)) + hit, t0, t1 = RayCaster.intersect(b, r2) + @test !hit + + # Ray inside the bounds + r3 = RayCaster.Ray(o = Point3f(1.5), d = Vec3f(1, 1, 0)) + hit, t0, t1 = RayCaster.intersect(b, r3) + @test hit + @test t0 ≈ 0f0 + + # Test with precomputed inv_dir and dir_is_negative + inv_dir = 1f0 ./ r1.d + dir_is_negative = RayCaster.is_dir_negative(r1.d) + @test RayCaster.intersect_p(b, r1, inv_dir, dir_is_negative) + + inv_dir2 = 1f0 ./ r2.d + dir_is_negative2 = RayCaster.is_dir_negative(r2.d) + @test !RayCaster.intersect_p(b, r2, inv_dir2, dir_is_negative2) +end diff --git a/test/runtests.jl b/test/runtests.jl index fef5a3c..43287b2 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,39 +2,16 @@ using Test using GeometryBasics using LinearAlgebra using RayCaster -using FileIO -using ImageCore +using JET -include("test_intersection.jl") - -@testset "Test Bounds2 iteration" begin - b = RayCaster.Bounds2(Point2f(1f0, 3f0), Point2f(4f0, 4f0)) - targets = [ - Point2f(1f0, 3f0), Point2f(2f0, 3f0), Point2f(3f0, 3f0), Point2f(4f0, 3f0), - Point2f(1f0, 4f0), Point2f(2f0, 4f0), Point2f(3f0, 4f0), Point2f(4f0, 4f0), - ] - @test length(b) == 8 - for (p, t) in zip(b, targets) - @test p == t +@testset "RayCaster Tests" begin + @testset "Intersection" begin + include("test_intersection.jl") end - - b = RayCaster.Bounds2(Point2f(-1f0), Point2f(1f0)) - targets = [ - Point2f(-1f0, -1f0), Point2f(0f0, -1f0), Point2f(1f0, -1f0), - Point2f(-1f0, 0f0), Point2f(0f0, 0f0), Point2f(1f0, 0f0), - Point2f(-1f0, 1f0), Point2f(0f0, 1f0), Point2f(1f0, 1f0), - ] - @test length(b) == 9 - for (p, t) in zip(b, targets) - @test p == t + @testset "Type Stability" begin + include("test_type_stability.jl") + end + @testset "Bounds" begin + include("bounds.jl") end -end - -@testset "Sphere bound" begin - core = RayCaster.ShapeCore(RayCaster.translate(Vec3f(0)), false) - s = RayCaster.Sphere(core, 1f0, -1f0, 1f0, 360f0) - - sb = RayCaster.object_bound(s) - @test sb[1] == Point3f(-1f0) - @test sb[2] == Point3f(1f0) end diff --git a/test/test_intersection.jl b/test/test_intersection.jl index 1d6a5a3..49c19da 100644 --- a/test/test_intersection.jl +++ b/test/test_intersection.jl @@ -113,7 +113,7 @@ end @test RayCaster.apply(ray, t_hit) ≈ Point3f(0, 0, 2) # Barycentric coordinates for vertex 0 (corner hit) @test bary_coords ≈ Point3f(1, 0, 0) - + # Test ray intersection (different point). ray = RayCaster.Ray(o = Point3f(0.5, 0.25, 0), d = Vec3f(0, 0, 1)) intersects_p = RayCaster.intersect_p(triangle, ray) @@ -141,10 +141,10 @@ end bvh = RayCaster.BVHAccel(triangle_meshes) # Test basic BVH functionality with triangle meshes @test !isnothing(RayCaster.world_bound(bvh)) - + # Simple intersection test ray = RayCaster.Ray(o = Point3f(0.5, 0.5, -1), d = Vec3f(0, 0, 1)) - intersects, interaction = RayCaster.intersect!(bvh, ray) + intersects, interaction = RayCaster.closest_hit(bvh, ray) @test intersects end @@ -172,8 +172,8 @@ end # Test intersection with the first triangle ray = RayCaster.Ray(o = Point3f(0, 0, -2), d = Vec3f(0, 0, 1)) - intersects, triangle = RayCaster.intersect!(bvh, ray) + intersects, triangle = RayCaster.closest_hit(bvh, ray) @test intersects - # BVH intersect! returns Triangle object, not SurfaceInteraction + # BVH closest_hit returns Triangle object, not SurfaceInteraction @test typeof(triangle) == RayCaster.Triangle end diff --git a/test/type-stability.jl b/test/type-stability.jl index 830afab..9de0bf6 100644 --- a/test/type-stability.jl +++ b/test/type-stability.jl @@ -1,8 +1,8 @@ using RayCaster, GeometryBasics, StaticArrays code_warntype(RayCaster._to_ray_coordinate_space, (SVector{3,Point3f}, RayCaster.Ray)) -code_warntype(RayCaster.∂p, (RayCaster.Triangle, SVector{3,Point3f}, SVector{3,Point2f})) -code_warntype(RayCaster.∂n, (RayCaster.Triangle, SVector{3,Point2f})) +code_warntype(RayCaster.partial_derivatives, (RayCaster.Triangle, SVector{3,Point3f}, SVector{3,Point2f})) +code_warntype(RayCaster.normal_derivatives, (RayCaster.Triangle, SVector{3,Point2f})) code_warntype(RayCaster.intersect, (RayCaster.Triangle, RayCaster.Ray, Bool)) code_warntype(RayCaster.intersect_triangle, (RayCaster.Triangle, RayCaster.Ray)) code_warntype(RayCaster.intersect_triangle, (RayCaster.Triangle, RayCaster.Ray)) @@ -25,4 +25,4 @@ m = RayCaster.create_triangle_mesh(RayCaster.ShapeCore(), UInt32[1, 2, 3], Point t = RayCaster.Triangle(m, 1) r = RayCaster.Ray(o=Point3f(ray_origin), d=ray_direction) RayCaster.intersect_p(t, r) -RayCaster.intersect_triangle(r.o, r.d, t.vertices...) +@code_warntype RayCaster.intersect_triangle(t.vertices, r) From 01a36d83dccc95f7c4ea943a2e7bcf5b8a10ddf5 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Wed, 22 Oct 2025 13:51:25 +0200 Subject: [PATCH 03/20] allow different allocators for visited list --- src/bvh.jl | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/bvh.jl b/src/bvh.jl index d6d9521..b626132 100644 --- a/src/bvh.jl +++ b/src/bvh.jl @@ -238,6 +238,15 @@ end length(bvh.nodes) > Int32(0) ? bvh.nodes[1].bounds : Bounds3() end +struct MemAllocator +end +@inline _allocate(::MemAllocator, T::Type, n::Val{N}) where {N} = zeros(MVector{N,T}) +Base.@propagate_inbounds function _setindex(arr::MVector{N, T}, idx::Integer, value::T) where {N, T} + arr[idx] = value + return arr +end + + """ traverse_bvh(bvh::BVHAccel{P}, ray::AbstractRay, hit_callback::F) where {P, F<:Function} @@ -253,7 +262,7 @@ Arguments: Returns: - The final result from the hit_callback """ -@inline function traverse_bvh(hit_callback::F, bvh::BVHAccel{P}, ray::AbstractRay) where {P, F<:Function} +@inline function traverse_bvh(hit_callback::F, bvh::BVHAccel{P}, ray::AbstractRay, allocator=MemAllocator()) where {P, F<:Function} if length(bvh.nodes) == 0 # We dont handle the empty case yet, since its not that easy to make it type stable # Its possible, but why would we intersect an empty BVH? @@ -266,9 +275,9 @@ Returns: dir_is_neg = is_dir_negative(ray.d) # Initialize traversal stack - to_visit_offset = Int32(1) + local to_visit_offset::Int32 = Int32(1) current_node_idx = Int32(1) - nodes_to_visit = zeros(MVector{64,Int32}) + nodes_to_visit = _allocate(allocator, Int32, Val(64)) primitives = bvh.primitives nodes = bvh.nodes @@ -280,7 +289,6 @@ Returns: # Traverse BVH @_inbounds while true current_node = nodes[current_node_idx] - # Test ray against current node's bounding box if intersect_p(current_node.bounds, ray, inv_dir, dir_is_neg) if !current_node.is_interior && current_node.n_primitives > Int32(0) @@ -292,7 +300,6 @@ Returns: # Call the callback for this primitive continue_search, ray, result = hit_callback(primitive, ray, result) - # Early exit if callback requests it if !continue_search return false, ray, result @@ -300,7 +307,7 @@ Returns: end # Done with leaf, pop next node from stack - if to_visit_offset == Int32(1) + if to_visit_offset === Int32(1) break end to_visit_offset -= Int32(1) @@ -308,24 +315,23 @@ Returns: else # Interior node - push children to stack if dir_is_neg[current_node.split_axis] == Int32(2) - nodes_to_visit[to_visit_offset] = current_node_idx + Int32(1) + nodes_to_visit = _setindex(nodes_to_visit, to_visit_offset, current_node_idx + Int32(1)) current_node_idx = current_node.offset % Int32 else - nodes_to_visit[to_visit_offset] = current_node.offset % Int32 + nodes_to_visit = _setindex(nodes_to_visit, to_visit_offset, current_node.offset % Int32) current_node_idx += Int32(1) end to_visit_offset += Int32(1) end else # Miss - pop next node from stack - if to_visit_offset == Int32(1) + if to_visit_offset === Int32(1) break end to_visit_offset -= Int32(1) current_node_idx = nodes_to_visit[to_visit_offset] end end - # Return final state return continue_search, ray, result end @@ -356,9 +362,9 @@ Returns: - `distance`: Distance along the ray to the hit point (hit_point = ray.o + ray.d * distance) - `barycentric_coords`: Barycentric coordinates of the hit point """ -@inline function closest_hit(bvh::BVHAccel{P}, ray::AbstractRay) where {P} +@inline function closest_hit(bvh::BVHAccel{P}, ray::AbstractRay, allocator=MemAllocator()) where {P} # Traverse BVH with closest-hit callback - _, _, result = traverse_bvh(closest_hit_callback, bvh, ray) + _, _, result = traverse_bvh(closest_hit_callback, bvh, ray, allocator) return result::Tuple{Bool, Triangle, Float32, Point3f} end @@ -368,11 +374,10 @@ any_hit_callback(primitive, current_ray, result::Nothing) = (false, current_ray, # Define any-hit callback function any_hit_callback(primitive, current_ray, prev_result::Tuple{Bool, P, Float32, Point3f}) where {P} # Test for intersection - tmp_hit, tmp_ray, tmp_bary = intersect_p!(primitive, current_ray) + tmp_hit, dist, tmp_bary = intersect(primitive, current_ray) if tmp_hit # Stop traversal on first hit and return hit info - distance = tmp_ray.t_max - return false, tmp_ray, (true, primitive, distance, tmp_bary) + return false, current_ray, (true, primitive, dist, tmp_bary) end # Continue search if no hit return true, current_ray, prev_result @@ -391,9 +396,9 @@ Returns: - `distance`: Distance along the ray to the hit point (hit_point = ray.o + ray.d * distance) - `barycentric_coords`: Barycentric coordinates of the hit point """ -@inline function any_hit(bvh::BVHAccel, ray::AbstractRay) +@inline function any_hit(bvh::BVHAccel, ray::AbstractRay, allocator=MemAllocator()) # Traverse BVH with any-hit callback - continue_search, _, result = traverse_bvh(any_hit_callback, bvh, ray) + continue_search, _, result = traverse_bvh(any_hit_callback, bvh, ray, allocator) return result::Tuple{Bool, Triangle, Float32, Point3f} end From be2e62a4fa18ce33b1ee84d8dad8c4bb962e09c1 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Wed, 22 Oct 2025 13:53:14 +0200 Subject: [PATCH 04/20] cleanup --- Project.toml | 6 + test/gpu-threading-benchmarks.jl | 125 +++----- test/test_type_stability.jl | 535 +++++++++++++++++++++++++++++++ 3 files changed, 587 insertions(+), 79 deletions(-) create mode 100644 test/test_type_stability.jl diff --git a/Project.toml b/Project.toml index ba1a748..b99e474 100644 --- a/Project.toml +++ b/Project.toml @@ -13,6 +13,12 @@ RandomNumbers = "e6cf234a-135c-5ec9-84dd-332b85af5143" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +[weakdeps] +Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" + +[extensions] +RayCasterMakieExt = "Makie" + [compat] GeometryBasics = "0.5" RandomNumbers = "1" diff --git a/test/gpu-threading-benchmarks.jl b/test/gpu-threading-benchmarks.jl index 90d0514..588bb2b 100644 --- a/test/gpu-threading-benchmarks.jl +++ b/test/gpu-threading-benchmarks.jl @@ -1,34 +1,18 @@ using GeometryBasics, LinearAlgebra, RayCaster, BenchmarkTools -using ImageShow -using Makie -using KernelAbstractions -import KernelAbstractions as KA -using KernelAbstractions.Extras.LoopInfo: @unroll -using AMDGPU - -ArrayType = ROCArray + # using CUDA # ArrayType = CuArray -include("./../src/gpu-support.jl") - LowSphere(radius, contact=Point3f(0)) = Sphere(contact .+ Point3f(0, 0, radius), radius) function tmesh(prim, material) - prim = prim isa Sphere ? Tesselation(prim, 64) : prim - mesh = normal_mesh(prim) - m = RayCaster.create_triangle_mesh(mesh) - return RayCaster.GeometricPrimitive(m, material) + return normal_mesh(prim) end -material_red = RayCaster.MatteMaterial( - RayCaster.ConstantTexture(RayCaster.RGBSpectrum(0.796f0, 0.235f0, 0.2f0)), - RayCaster.ConstantTexture(0.0f0), -) - begin + material_red = nothing s1 = tmesh(LowSphere(0.5f0), material_red) s2 = tmesh(LowSphere(0.3f0, Point3f(0.5, 0.5, 0)), material_red) s3 = tmesh(LowSphere(0.3f0, Point3f(-0.5, 0.5, 0)), material_red) @@ -39,65 +23,8 @@ begin l = tmesh(Rect3f(Vec3f(-2, -5, 0), Vec3f(0.01, 10, 10)), material_red) r = tmesh(Rect3f(Vec3f(2, -5, 0), Vec3f(0.01, 10, 10)), material_red) bvh = RayCaster.BVHAccel([s1, s2, s3, s4, ground, back, l, r]); - res = 512 - resolution = Point2f(res) - f = RayCaster.LanczosSincFilter(Point2f(1.0f0), 3.0f0) - film = RayCaster.Film(resolution, - RayCaster.Bounds2(Point2f(0.0f0), Point2f(1.0f0)), - f, 1.0f0, 1.0f0, - "shadows_sppm_res.png", - ) - screen_window = RayCaster.Bounds2(Point2f(-1), Point2f(1)) - cam = RayCaster.PerspectiveCamera( - RayCaster.look_at(Point3f(0, 4, 2), Point3f(0, -4, -1), Vec3f(0, 0, 1)), - screen_window, 0.0f0, 1.0f0, 0.0f0, 1.0f6, 45.0f0, film, - ) - lights = ( - # RayCaster.PointLight(Vec3f(0, -1, 2), RayCaster.RGBSpectrum(22.0f0)), - RayCaster.PointLight(Vec3f(0, 0, 2), RayCaster.RGBSpectrum(10.0f0)), - RayCaster.PointLight(Vec3f(0, 3, 3), RayCaster.RGBSpectrum(25.0f0)), - ) - img = zeros(RGBf, res, res) -end - -@inline function get_camera_sample(p_raster::Point2) - p_film = p_raster .+ rand(Point2f) - p_lens = rand(Point2f) - RayCaster.CameraSample(p_film, p_lens, rand(Float32)) -end - -# ray = RayCaster.Ray(o=Point3f(0.5, 0.5, 1.0), d=Vec3f(0.0, 0.0, -1.0)) -# l = RayCaster.RGBSpectrum(0.0f0) -# open("test3.llvm", "w") do io -# code_llvm(io, simple_shading, typeof.((bvh, bvh.primitives[1], RayCaster.RayDifferentials(ray), RayCaster.SurfaceInteraction(), l, 1, 1, lights))) -# end - -@inline function trace_pixel(camera, scene, xy) - pixel = Point2f(Tuple(xy)) - s = RayCaster.UniformSampler(8) - camera_sample = @inline RayCaster.get_camera_sample(s, pixel) - ray, ω = RayCaster.generate_ray_differential(camera, camera_sample) - if ω > 0.0f0 - l = @inline RayCaster.li(s, 5, ray, scene, 1) - end - return l -end - -@kernel function ka_trace_image!(img, camera, scene) - xy = @index(Global, Cartesian) - if checkbounds(Bool, img, xy) - l = trace_pixel(camera, scene, xy) - @_inbounds img[xy] = RGBf(l.c...) - end end -function launch_trace_image!(img, camera, scene) - backend = KA.get_backend(img) - kernel! = ka_trace_image!(backend) - kernel!(img, camera, scene, lights, ndrange=size(img), workgroupsize=(16, 16)) - KA.synchronize(backend) - return img -end # using AMDGPU # ArrayType = ROCArray # using CUDA @@ -244,8 +171,25 @@ end camera_sample = RayCaster.get_camera_sample(integrator.sampler, Point2f(512)) ray, ω = RayCaster.generate_ray_differential(integrator.camera, camera_sample) -@btime RayCaster.intersect_p(bvh, ray) -@btime RayCaster.intersect!(bvh, ray) + +ray = RayCaster.Ray(o=Point3f(0.0, 0.0, 2.0), d=Vec3f(0.0, 0.0, -1.0)) +function test(results, bvh, ray) + for i in 1:100000 + results[i] = RayCaster.any_hit(bvh, ray) + end + return results +end + +@profview test(results, bvh, ray) +@btime RayCaster.closest_hit(bvh, ray) +results = Vector{Tuple{Bool, RayCaster.Triangle, Float32, Point3f}}(undef, 100000); +@allocated test(results, bvh, ray) + +@btime RayCaster.any_hit(bvh, ray) + +@code_typed RayCaster.traverse_bvh(RayCaster.any_hit_callback, bvh, ray, RayCaster.MemAllocator()) + +sizeof(zeros(RayCaster.MVector{64,Int32})) ### # Int32 always @@ -253,10 +197,33 @@ ray, ω = RayCaster.generate_ray_differential(integrator.camera, camera_sample) # Tuple instead of vector for nodes_to_visit # 43.400 μs (1 allocation: 624 bytes) # AFTER GPU rework -# intersect! +# closest_hit # 40.500 μs (1 allocation: 368 bytes) # intersect_p # 11.500 μs (0 allocations: 0 bytes) ### LinearBVHLeaf as one type # 5.247460 seconds (17.55 k allocations: 19.783 MiB, 46 lock conflicts) + +struct PerfNTuple{N,T} + data::NTuple{N,T} +end + +@generated function RayCaster._setindex(r::PerfNTuple{N,T}, idx::IT, value::T) where {N,T, IT <: Integer} + expr = Expr(:tuple) + for i in 1:N + idxt = IT(i) + push!(expr.args, :(idx === $idxt ? value : r.data[$idxt])) + end + return :($(PerfNTuple)($expr)) +end + +@propagate_inbounds Base.getindex(r::PerfNTuple, idx::Integer) = r.data[idx] + +@generated function RayCaster._allocate(f, ::Val{N}, ::Type{T}) where {N, T} + expr = Expr(:tuple) + for i in 1:N + push!(expr.args, :(f($i))) + end + return expr +end diff --git a/test/test_type_stability.jl b/test/test_type_stability.jl new file mode 100644 index 0000000..55bf30a --- /dev/null +++ b/test/test_type_stability.jl @@ -0,0 +1,535 @@ +using LinearAlgebra + +module TestData + using GeometryBasics + using RayCaster + const SVector = RayCaster.StaticArrays.SVector + + # Basic geometric types + point3f() = Point3f(1.0f0, 2.0f0, 3.0f0) + point2f() = Point2f(0.5f0, 0.5f0) + vec3f() = Vec3f(0.0f0, 0.0f0, 1.0f0) + normal3f() = RayCaster.Normal3f(0.0f0, 0.0f0, 1.0f0) + + # Bounds + bounds2() = RayCaster.Bounds2(Point2f(0.0f0), Point2f(1.0f0)) + bounds3() = RayCaster.Bounds3(Point3f(0.0f0), Point3f(1.0f0, 1.0f0, 1.0f0)) + + # Rays + ray() = RayCaster.Ray(o=Point3f(0.0f0), d=Vec3f(0.0f0, 0.0f0, 1.0f0)) + ray_differentials() = RayCaster.RayDifferentials(o=Point3f(0.0f0), d=Vec3f(0.0f0, 0.0f0, 1.0f0)) + + # Transformations + transformation() = RayCaster.Transformation() + transformation_translate() = RayCaster.translate(Vec3f(1.0f0, 0.0f0, 0.0f0)) + transformation_rotate() = RayCaster.rotate_x(45.0f0) + transformation_scale() = RayCaster.scale(2.0f0, 2.0f0, 2.0f0) + + # Triangle + function triangle() + v1 = Point3f(0.0f0, 0.0f0, 0.0f0) + v2 = Point3f(1.0f0, 0.0f0, 0.0f0) + v3 = Point3f(0.0f0, 1.0f0, 0.0f0) + n1 = RayCaster.Normal3f(0.0f0, 0.0f0, 1.0f0) + uv1 = Point2f(0.0f0, 0.0f0) + uv2 = Point2f(1.0f0, 0.0f0) + uv3 = Point2f(0.0f0, 1.0f0) + RayCaster.Triangle( + SVector(v1, v2, v3), + SVector(n1, n1, n1), + SVector(Vec3f(NaN), Vec3f(NaN), Vec3f(NaN)), + SVector(uv1, uv2, uv3), + UInt32(1) + ) + end + + # Triangle Mesh + function triangle_mesh() + vertices = [Point3f(0, 0, 0), Point3f(1, 0, 0), Point3f(0, 1, 0)] + indices = UInt32[1, 2, 3] # 1-based indices for Julia + normals = [RayCaster.Normal3f(0, 0, 1), RayCaster.Normal3f(0, 0, 1), RayCaster.Normal3f(0, 0, 1)] + RayCaster.TriangleMesh(vertices, indices, normals) + end + + # BVH + function bvh_accel() + mesh = Rect3f(Point3f(0), Vec3f(1)) + RayCaster.BVHAccel([mesh], 1) + end + + # Quaternion + quaternion() = RayCaster.Quaternion() +end + +# ==================== Custom Test Macros ==================== + +""" + @test_opt_alloc expr + +Combined macro that tests both type stability (via @test_opt) and zero allocations. +This is equivalent to: + @test_opt expr + @test @allocated(expr) == 0 +""" +macro test_opt_alloc(expr) + return esc(quote + $expr # warmup + JET.@test_opt $expr + @test @allocated($expr) == 0 + end) +end + +# ==================== Bounds Tests ==================== + +@testset "Type Stability: bounds.jl" begin + @testset "Bounds2" begin + @test_opt_alloc RayCaster.Bounds2() + + @test_opt_alloc RayCaster.Bounds2(TestData.point2f()) + + @test_opt_alloc RayCaster.Bounds2c(TestData.point2f(), Point2f(1.0f0, 1.0f0)) + end + + @testset "Bounds3" begin + @test_opt_alloc RayCaster.Bounds3() + + @test_opt_alloc RayCaster.Bounds3(TestData.point3f()) + + @test_opt_alloc RayCaster.Bounds3c(TestData.point3f(), Point3f(2.0f0, 2.0f0, 2.0f0)) + end + + @testset "Bounds operations" begin + b1 = TestData.bounds3() + b2 = RayCaster.Bounds3(Point3f(0.5f0), Point3f(1.5f0, 1.5f0, 1.5f0)) + p = TestData.point3f() + + @test_opt_alloc Base.:(==)(b1, b2) + @test_opt_alloc Base.:≈(b1, b2) + @test_opt_alloc Base.getindex(b1, 1) + @test_opt_alloc RayCaster.is_valid(b1) + @test_opt_alloc RayCaster.corner(b1, 1) + @test_opt_alloc Base.union(b1, b2) + @test_opt_alloc Base.intersect(b1, b2) + @test_opt_alloc RayCaster.overlaps(b1, b2) + @test_opt_alloc RayCaster.inside(b1, p) + @test_opt_alloc RayCaster.inside_exclusive(b1, p) + @test_opt_alloc RayCaster.expand(b1, 0.1f0) + @test_opt_alloc RayCaster.diagonal(b1) + @test_opt_alloc RayCaster.surface_area(b1) + @test_opt_alloc RayCaster.volume(b1) + @test_opt_alloc RayCaster.maximum_extent(b1) + @test_opt_alloc RayCaster.sides(b1) + @test_opt_alloc RayCaster.inclusive_sides(b1) + @test_opt_alloc RayCaster.bounding_sphere(b1) + @test_opt_alloc RayCaster.offset(b1, p) + end + + @testset "Bounds with Ray" begin + b = TestData.bounds3() + r = TestData.ray() + + @test_opt_alloc RayCaster.intersect(b, r) + @test_opt_alloc RayCaster.is_dir_negative(r.d) + + inv_dir = 1.0f0 ./ r.d + dir_neg = RayCaster.is_dir_negative(r.d) + @test_opt_alloc RayCaster.intersect_p(b, r, inv_dir, dir_neg) + end + + @testset "Bounds2 iteration" begin + b = TestData.bounds2() + @test_opt_alloc Base.length(b) + @test_opt_alloc Base.iterate(b) + @test_opt_alloc Base.iterate(b, Int32(1)) + end + + @testset "Distance functions" begin + p1 = TestData.point3f() + p2 = Point3f(2.0f0, 3.0f0, 4.0f0) + + @test_opt_alloc RayCaster.distance(p1, p2) + @test_opt_alloc RayCaster.distance_squared(p1, p2) + end + + @testset "Lerp functions" begin + b = TestData.bounds3() + p = TestData.point3f() + + @test_opt_alloc RayCaster.lerp(0.0f0, 1.0f0, 0.5f0) + @test_opt_alloc RayCaster.lerp(Point3f(0), Point3f(1), 0.5f0) + @test_opt_alloc RayCaster.lerp(b, Point3f(0.5f0)) + end + + @testset "Bounds2 area" begin + b = TestData.bounds2() + @test_opt_alloc RayCaster.area(b) + end +end + +# ==================== Ray Tests ==================== + +@testset "Type Stability: ray.jl" begin + @testset "Ray construction" begin + @test_opt_alloc RayCaster.Ray(o=TestData.point3f(), d=TestData.vec3f()) + @test_opt_alloc RayCaster.Ray(o=TestData.point3f(), d=TestData.vec3f(), t_max=10.0f0) + @test_opt_alloc RayCaster.Ray(o=TestData.point3f(), d=TestData.vec3f(), t_max=10.0f0, time=0.5f0) + end + + @testset "Ray copy constructor" begin + r = TestData.ray() + @test_opt_alloc RayCaster.Ray(r; o=Point3f(1.0f0)) + @test_opt_alloc RayCaster.Ray(r; d=Vec3f(1.0f0, 0.0f0, 0.0f0)) + @test_opt_alloc RayCaster.Ray(r; t_max=5.0f0) + end + + @testset "RayDifferentials construction" begin + @test_opt_alloc RayCaster.RayDifferentials(o=TestData.point3f(), d=TestData.vec3f()) + @test_opt_alloc RayCaster.RayDifferentials(TestData.ray()) + end + + @testset "Ray operations" begin + r = TestData.ray() + rd = TestData.ray_differentials() + + @test_opt_alloc RayCaster.set_direction(r, Vec3f(1.0f0, 0.0f0, 0.0f0)) + @test_opt_alloc RayCaster.set_direction(rd, Vec3f(1.0f0, 0.0f0, 0.0f0)) + @test_opt_alloc RayCaster.check_direction(r) + @test_opt_alloc RayCaster.check_direction(rd) + @test_opt_alloc RayCaster.apply(r, 1.0f0) + @test_opt_alloc RayCaster.increase_hit(r, 0.5f0) + @test_opt_alloc RayCaster.increase_hit(rd, 0.5f0) + end + + @testset "RayDifferentials operations" begin + rd = TestData.ray_differentials() + @test_opt_alloc RayCaster.scale_differentials(rd, 0.5f0) + end + + @testset "Intersection helpers" begin + t = TestData.triangle() + r = TestData.ray() + @test_opt_alloc RayCaster.intersect_p!(t, r) + end +end + +# ==================== Transformation Tests ==================== + +@testset "Type Stability: transformations.jl" begin + @testset "Transformation construction" begin + @test_opt_alloc RayCaster.Transformation() + @test_opt_alloc RayCaster.Transformation(Mat4f(I)) + end + + @testset "Basic transformations" begin + @test_opt_alloc RayCaster.translate(TestData.vec3f()) + @test_opt_alloc RayCaster.scale(2.0f0, 2.0f0, 2.0f0) + @test_opt_alloc RayCaster.rotate_x(45.0f0) + @test_opt_alloc RayCaster.rotate_y(45.0f0) + @test_opt_alloc RayCaster.rotate_z(45.0f0) + @test_opt_alloc RayCaster.rotate(45.0f0, Vec3f(0, 0, 1)) + end + + @testset "Transformation operations" begin + t1 = TestData.transformation_translate() + t2 = TestData.transformation_rotate() + + @test_opt_alloc RayCaster.is_identity(t1) + @test_opt_alloc Base.transpose(t1) + @test_opt_alloc Base.inv(t1) + @test_opt_alloc Base.:(==)(t1, t2) + @test_opt_alloc Base.:≈(t1, t2) + @test_opt_alloc Base.:*(t1, t2) + end + + @testset "Transformation application" begin + t = TestData.transformation_translate() + + @test_opt_alloc t(TestData.point3f()) + @test_opt_alloc t(TestData.vec3f()) + @test_opt_alloc t(TestData.normal3f()) + @test_opt_alloc t(TestData.bounds3()) + end + + @testset "Advanced transformations" begin + @test_opt_alloc RayCaster.look_at(Point3f(0, 0, 5), Point3f(0), Vec3f(0, 1, 0)) + @test_opt_alloc RayCaster.perspective(60.0f0, 0.1f0, 100.0f0) + end + + @testset "Transformation properties" begin + t = TestData.transformation_scale() + @test_opt_alloc RayCaster.has_scale(t) + @test_opt_alloc RayCaster.swaps_handedness(t) + end + + @testset "Transformation with Ray" begin + t = TestData.transformation_translate() + r = TestData.ray() + rd = TestData.ray_differentials() + + @test_opt_alloc RayCaster.apply(t, r) + @test_opt_alloc RayCaster.apply(t, rd) + end + + @testset "Quaternion" begin + @test_opt_alloc RayCaster.Quaternion() + @test_opt_alloc RayCaster.Quaternion(TestData.transformation()) + + q1 = TestData.quaternion() + q2 = RayCaster.Quaternion(Vec3f(1, 0, 0), 0.5f0) + + @test_opt_alloc Base.:+(q1, q2) + @test_opt_alloc Base.:-(q1, q2) + @test_opt_alloc Base.:/(q1, 2.0f0) + @test_opt_alloc Base.:*(q1, 2.0f0) + @test_opt_alloc LinearAlgebra.dot(q1, q2) + @test_opt_alloc LinearAlgebra.normalize(q1) + @test_opt_alloc RayCaster.Transformation(q1) + @test_opt_alloc RayCaster.slerp(q1, q2, 0.5f0) + end +end + +# ==================== Math Tests ==================== + +@testset "Type Stability: math.jl" begin + @testset "Sampling functions" begin + u = TestData.point2f() + + @test_opt_alloc RayCaster.concentric_sample_disk(u) + @test_opt_alloc RayCaster.cosine_sample_hemisphere(u) + @test_opt_alloc RayCaster.uniform_sample_sphere(u) + @test_opt_alloc RayCaster.uniform_sample_cone(u, 0.5f0) + @test_opt_alloc RayCaster.uniform_sample_cone(u, 0.5f0, Vec3f(1,0,0), Vec3f(0,1,0), Vec3f(0,0,1)) + end + + @testset "PDF functions" begin + @test_opt_alloc RayCaster.uniform_sphere_pdf() + @test_opt_alloc RayCaster.uniform_cone_pdf(0.5f0) + end + + @testset "Shading coordinate system" begin + w = TestData.vec3f() + + @test_opt_alloc RayCaster.cos_θ(w) + @test_opt_alloc RayCaster.sin_θ2(w) + @test_opt_alloc RayCaster.sin_θ(w) + @test_opt_alloc RayCaster.tan_θ(w) + @test_opt_alloc RayCaster.cos_ϕ(w) + @test_opt_alloc RayCaster.sin_ϕ(w) + end + + @testset "Vector operations" begin + wo = TestData.vec3f() + n = Vec3f(0, 1, 0) + + @test_opt_alloc RayCaster.reflect(wo, n) + @test_opt_alloc RayCaster.face_forward(n, wo) + end + + @testset "Coordinate system" begin + v = TestData.vec3f() + @test_opt_alloc RayCaster.coordinate_system(v) + end + + @testset "Spherical functions" begin + @test_opt_alloc RayCaster.spherical_direction(0.5f0, 0.5f0, 1.0f0) + @test_opt_alloc RayCaster.spherical_direction(0.5f0, 0.5f0, 1.0f0, Vec3f(1,0,0), Vec3f(0,1,0), Vec3f(0,0,1)) + + v = TestData.vec3f() + @test_opt_alloc RayCaster.spherical_θ(v) + @test_opt_alloc RayCaster.spherical_ϕ(v) + end + + @testset "Helper functions" begin + v = TestData.vec3f() + @test_opt_alloc RayCaster.get_orthogonal_basis(v) + + t = TestData.triangle() + @test_opt_alloc RayCaster.random_triangle_point(t) + end + + @testset "sum_mul" begin + a = Point3f(0.2f0, 0.3f0, 0.5f0) + b = RayCaster.StaticArrays.SVector(Point3f(0,0,0), Point3f(1,0,0), Point3f(0,1,0)) + @test_opt_alloc RayCaster.sum_mul(a, b) + end +end + +# ==================== Surface Interaction Tests ==================== + +@testset "Type Stability: surface_interaction.jl" begin + @testset "Interaction construction" begin + @test_opt_alloc RayCaster.Interaction() + @test_opt_alloc RayCaster.Interaction( + TestData.point3f(), 0.0f0, TestData.vec3f(), TestData.normal3f() + ) + end + + @testset "ShadingInteraction construction" begin + n = TestData.normal3f() + v = TestData.vec3f() + @test_opt_alloc RayCaster.ShadingInteraction(n, v, v, n, n) + end + + @testset "SurfaceInteraction construction" begin + @test_opt_alloc RayCaster.SurfaceInteraction() + + p = TestData.point3f() + wo = TestData.vec3f() + uv = TestData.point2f() + n = TestData.normal3f() + dpdu = TestData.vec3f() + + @test_opt_alloc RayCaster.SurfaceInteraction(p, 0.0f0, wo, uv, dpdu, dpdu, n, n, false) + @test_opt_alloc RayCaster.SurfaceInteraction(n, p, 0.0f0, wo, uv, dpdu, dpdu, n, n) + end + + @testset "SurfaceInteraction operations" begin + si = RayCaster.SurfaceInteraction( + TestData.point3f(), 0.0f0, TestData.vec3f(), TestData.point2f(), + TestData.vec3f(), TestData.vec3f(), TestData.normal3f(), TestData.normal3f(), false + ) + + @test_opt_alloc RayCaster.set_shading_geometry(si, TestData.vec3f(), TestData.vec3f(), + TestData.normal3f(), TestData.normal3f(), true) + end + + @testset "Differentials" begin + si = RayCaster.SurfaceInteraction( + TestData.point3f(), 0.0f0, TestData.vec3f(), TestData.point2f(), + TestData.vec3f(), TestData.vec3f(), TestData.normal3f(), TestData.normal3f(), false + ) + rd = TestData.ray_differentials() + + @test_opt_alloc RayCaster.compute_differentials(si, rd) + end + + @testset "Transformation application" begin + t = TestData.transformation_translate() + i = RayCaster.Interaction(TestData.point3f(), 0.0f0, TestData.vec3f(), TestData.normal3f()) + + @test_opt_alloc RayCaster.apply(t, i) + end + + @testset "Spawn ray" begin + si = RayCaster.SurfaceInteraction( + TestData.point3f(), 0.0f0, TestData.vec3f(), TestData.point2f(), + TestData.vec3f(), TestData.vec3f(), TestData.normal3f(), TestData.normal3f(), false + ) + i = RayCaster.Interaction(Point3f(1,1,1), 0.0f0, TestData.vec3f(), TestData.normal3f()) + + @test_opt_alloc RayCaster.spawn_ray(si.core, i) + @test_opt_alloc RayCaster.spawn_ray(si, i) + @test_opt_alloc RayCaster.spawn_ray(si, TestData.vec3f()) + end +end + +# ==================== Triangle Mesh Tests ==================== + +@testset "Type Stability: triangle_mesh.jl" begin + @testset "TriangleMesh construction" begin + vertices = [Point3f(0, 0, 0), Point3f(1, 0, 0), Point3f(0, 1, 0)] + indices = UInt32[0, 1, 2] + normals = [RayCaster.Normal3f(0, 0, 1), RayCaster.Normal3f(0, 0, 1), RayCaster.Normal3f(0, 0, 1)] + + @test_opt_alloc RayCaster.TriangleMesh(vertices, indices, normals) + @test_opt_alloc RayCaster.TriangleMesh(vertices, indices) + end + + @testset "Triangle construction" begin + mesh = TestData.triangle_mesh() + @test_opt_alloc RayCaster.Triangle(mesh, 1, UInt32(1)) + end + + @testset "Triangle operations" begin + t = TestData.triangle() + + @test_opt_alloc RayCaster.vertices(t) + @test_opt_alloc RayCaster.normals(t) + @test_opt_alloc RayCaster.tangents(t) + @test_opt_alloc RayCaster.uvs(t) + @test_opt_alloc RayCaster.area(t) + @test_opt_alloc RayCaster.object_bound(t) + @test_opt_alloc RayCaster.world_bound(t) + end + + @testset "Triangle intersection" begin + t = TestData.triangle() + r = TestData.ray() + + @test_opt_alloc RayCaster.intersect(t, r) + @test_opt_alloc RayCaster.intersect_p(t, r) + @test_opt_alloc RayCaster.intersect_triangle(t.vertices, r) + end + + @testset "Triangle helper functions" begin + t = TestData.triangle() + r = TestData.ray() + + # Test _to_ray_coordinate_space + @test_opt_alloc RayCaster._to_ray_coordinate_space(t.vertices, r) + + # Test partial_derivatives + @test_opt_alloc RayCaster.partial_derivatives(t, t.vertices, t.uv) + + # Test normal_derivatives + @test_opt_alloc RayCaster.normal_derivatives(t, t.uv) + end + + @testset "Triangle utilities" begin + t = TestData.triangle() + @test_opt_alloc RayCaster.is_degenerate(t.vertices) + end +end + +# ==================== BVH Tests ==================== + +@testset "Type Stability: bvh.jl" begin + @testset "BVHPrimitiveInfo" begin + b = TestData.bounds3() + @test_opt_alloc RayCaster.BVHPrimitiveInfo(UInt32(1), b) + end + + @testset "BVHNode construction" begin + b = TestData.bounds3() + @test_opt_alloc RayCaster.BVHNode(UInt32(0), UInt32(1), b) + end + + @testset "LinearBVH construction" begin + b = TestData.bounds3() + @test_opt_alloc RayCaster.LinearBVHLeaf(b, UInt32(0), UInt32(1)) + @test_opt_alloc RayCaster.LinearBVHInterior(b, UInt32(1), UInt8(0)) + end + + @testset "BVH operations" begin + bvh = TestData.bvh_accel() + r = TestData.ray() + + @test_opt RayCaster.world_bound(bvh) + @test_opt RayCaster.closest_hit(bvh, r) + @test_opt RayCaster.any_hit(bvh, r) + end + + @testset "Ray grid generation" begin + bvh = TestData.bvh_accel() + direction = Vec3f(0, 0, 1) + # generate_ray_grid allocates - needs optimization + @test_opt RayCaster.generate_ray_grid(bvh, direction, 10) + end +end + +# ==================== Kernels Tests ==================== + +@testset "Type Stability: kernels.jl" begin + @testset "RayHit construction" begin + @test_opt_alloc RayCaster.RayHit(true, TestData.point3f(), UInt32(1)) + end + + @testset "Kernel functions" begin + bvh = TestData.bvh_accel() + direction = Vec3f(0, 0, 1) + + # These functions have bugs and allocate - need fixing + @test_opt RayCaster.hits_from_grid(bvh, direction; grid_size=8) + @test_opt RayCaster.get_illumination(bvh, direction; grid_size=8) + end +end From a1bd7c13010302fc6fd159da72633d7da63e3ff6 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Wed, 22 Oct 2025 13:53:57 +0200 Subject: [PATCH 05/20] add makie ext and ray intersection session for easier debugging and visualization --- docs/src/bvh_hit_tests.md | 298 ++++++++++++++++++++++++++++++++ ext/RayCasterMakieExt.jl | 231 +++++++++++++++++++++++++ src/RayCaster.jl | 3 + src/ray_intersection_session.jl | 105 +++++++++++ 4 files changed, 637 insertions(+) create mode 100644 docs/src/bvh_hit_tests.md create mode 100644 ext/RayCasterMakieExt.jl create mode 100644 src/ray_intersection_session.jl diff --git a/docs/src/bvh_hit_tests.md b/docs/src/bvh_hit_tests.md new file mode 100644 index 0000000..48d54aa --- /dev/null +++ b/docs/src/bvh_hit_tests.md @@ -0,0 +1,298 @@ +# BVH Hit Testing: `closest_hit` vs `any_hit` + +This document tests and visualizes the difference between `closest_hit` and `any_hit` functions in the BVH implementation using the new `RayIntersectionSession` API. + +## Test Setup + +```julia (editor=true, logging=false, output=true) +using RayCaster, GeometryBasics, LinearAlgebra +using WGLMakie +using Test +using Bonito + +# Create a simple test scene with multiple overlapping primitives +function create_test_scene() + # Three spheres at different distances along the Z-axis + sphere1 = Tesselation(Sphere(Point3f(0, 0, 5), 1.0f0), 20) # Furthest + sphere2 = Tesselation(Sphere(Point3f(0, 0, 3), 1.0f0), 20) # Middle + sphere3 = Tesselation(Sphere(Point3f(0, 0, 1), 1.0f0), 20) # Closest + + bvh = RayCaster.BVHAccel([sphere1, sphere2, sphere3]) + return bvh +end + +bvh = create_test_scene() + +DOM.div("✓ Created BVH with $(length(bvh.primitives)) triangles from 3 spheres") +``` +## Test 1: Single Ray Through Center + +Test a ray through the center that passes through all three spheres. + +```julia (editor=true, logging=false, output=true) +# Create a ray with slight offset to avoid hitting triangle vertices exactly +test_ray = RayCaster.Ray(o=Point3f(0.1, 0.1, -5), d=Vec3f(0, 0, 1)) + +# Create session with closest_hit +session_closest = RayIntersectionSession([test_ray], bvh, RayCaster.closest_hit) + +# Create session with any_hit for comparison +session_any = RayIntersectionSession([test_ray], bvh, RayCaster.any_hit) + +fig = Figure() + +# Left: closest_hit visualization +plot(fig[1, 1], session_closest) +plot(fig[1, 2], session_any) +Label(fig[0, 1], "closest_hit", fontsize=20, font=:bold) +Label(fig[0, 2], "any_hit", fontsize=20, font=:bold) + +fig +``` +## Visualization: Single Ray with Makie Recipe + +```julia (editor=true, logging=false, output=true) +# Create a ray with slight offset to avoid hitting triangle vertices exactly +test_ray = RayCaster.Ray(o=Point3f(0.1, 0.1, 10), d=Vec3f(0, 0, -1)) + +# Create session with closest_hit +session_closest = RayIntersectionSession([test_ray], bvh, RayCaster.closest_hit) + +# Create session with any_hit for comparison +session_any = RayIntersectionSession([test_ray], bvh, RayCaster.any_hit) + +fig = Figure() + +# Left: closest_hit visualization +plot(fig[1, 1], session_closest) +plot(fig[1, 2], session_any) +Label(fig[0, 1], "closest_hit", tellwidth=false) +Label(fig[0, 2], "any_hit", tellwidth=false) + +fig +``` +## Test 2: Multiple Rays from Different Positions + +Test multiple rays to ensure both functions work correctly. + +```julia (editor=true, logging=false, output=true) +# Test rays from different angles (with slight offset to avoid vertex hits) +test_positions = [ + Point3f(0.1, 0.1, -5), # Center + Point3f(0.5, 0.1, -5), # Right offset + Point3f(0.1, 0.5, -5), # Top offset + Point3f(-0.5, 0.1, -5), # Left offset +] + +# Create rays +rays = [RayCaster.Ray(o=pos, d=Vec3f(0, 0, 1)) for pos in test_positions] + +# Create session +session_multi = RayIntersectionSession(rays, bvh, RayCaster.closest_hit) +fig2 = Figure() +ax = LScene(fig2[1, 1]) + +# Use different colors for each ray +ray_colors = [:purple, :orange, :cyan, :magenta] + +plot!(ax, session_multi; + show_bvh=true, + bvh_alpha=0.3, + ray_colors=ray_colors, + hit_color=:green, + show_hit_points=true, + hit_markersize=0.15, + show_labels=false) + +fig2 +``` +## Visualization: Multiple Rays + +## Test 3: Miss Cases + +Test rays that don't intersect any geometry. + +```julia (editor=true, logging=false, output=true) +# Rays that miss all spheres +miss_rays = [ + RayCaster.Ray(o=Point3f(5, 0, -5), d=Vec3f(0, 0, 1)), # Too far right + RayCaster.Ray(o=Point3f(0, 5, -5), d=Vec3f(0, 0, 1)), # Too far up + RayCaster.Ray(o=Point3f(0, 0, -5), d=Vec3f(1, 0, 0)), # Wrong direction +] + +# Create sessions for both hit functions +session_miss_closest = RayIntersectionSession(miss_rays, bvh, RayCaster.closest_hit) +session_miss_any = RayIntersectionSession(miss_rays, bvh, RayCaster.any_hit) + +# Verify all misses +@test miss_count(session_miss_closest) == 3 +@test miss_count(session_miss_any) == 3 + +descriptions = ["Too far right", "Too far up", "Wrong direction"] +miss_table = map(enumerate(zip(session_miss_closest.hits, session_miss_any.hits, descriptions))) do (i, (closest_hit, any_hit, desc)) + @test closest_hit[1] == false + @test any_hit[1] == false + + ( + Ray = "Miss ray $i", + Description = desc, + closest_hit = closest_hit[1], + any_hit = any_hit[1], + Status = "✓" + ) +end + +Bonito.Table(miss_table) +``` +## Visualization: Miss Cases + +```julia (editor=true, logging=false, output=true) +fig3 = Figure() +ax = LScene(fig3[1, 1]) + +plot!(ax, session_miss_closest; + show_bvh=true, + bvh_alpha=0.4, + ray_colors=[:red, :orange, :yellow], + miss_color=:gray, + ray_length=15.0f0 +) + +fig3 +``` +## Test 4: Difference Between any_hit and closest_hit + +Demonstrate that `any_hit` can return different results than `closest_hit`. + +```julia (editor=true, logging=false, output=true) +# Create a complex scene with overlapping geometry +# This creates a BVH where traversal order can differ from distance order +using Random +Random.seed!(123) + +complex_spheres = [] + +# Add some large overlapping spheres +push!(complex_spheres, Tesselation(Sphere(Point3f(0, 0, 10), 3.0f0), 20)) +push!(complex_spheres, Tesselation(Sphere(Point3f(0.5, 0, 5), 0.5f0), 15)) +push!(complex_spheres, Tesselation(Sphere(Point3f(-0.5, 0, 15), 1.5f0), 18)) + +# Add many small spheres to create complex BVH structure +for i in 1:30 + x = randn() * 5 + y = randn() * 5 + z = rand(8.0:0.5:12.0) + r = 0.3 + rand() * 0.5 + push!(complex_spheres, Tesselation(Sphere(Point3f(x, y, z), r), 8)) +end + +complex_bvh = RayCaster.BVHAccel(complex_spheres) + +# Test rays to find cases where any_hit differs from closest_hit +test_rays = map(1:100) do i + x = (i % 10) * 0.4 - 2.0 + y = div(i-1, 10) * 0.4 - 2.0 + RayCaster.Ray(o=Point3f(x, y, -5), d=Vec3f(0, 0, 1)) +end + +session_closest = RayIntersectionSession(test_rays, complex_bvh, RayCaster.closest_hit) +session_any = RayIntersectionSession(test_rays, complex_bvh, RayCaster.any_hit) + +# Find cases where they differ +differences = [] +for (i, (closest, any)) in enumerate(zip(session_closest.hits, session_any.hits)) + hit_closest, prim_closest, dist_closest, _ = closest + hit_any, prim_any, dist_any, _ = any + + if hit_closest && hit_any + diff = abs(dist_closest - dist_any) + if diff > 0.1 # Significant difference + push!(differences, ( + ray_idx = i, + diff = diff, + closest_dist = dist_closest, + any_dist = dist_any, + closest_mat = prim_closest.material_idx, + any_mat = prim_any.material_idx + )) + end + end +end + +# Show results +diff_table = if length(differences) > 0 + sort!(differences, by=x->x.diff, rev=true) + top5 = differences[1:min(5, length(differences))] + map(enumerate(top5)) do (i, d) + ( + Rank = string(i), + Distance_Diff = "$(round(d.diff, digits=2))", + closest_hit_dist = "$(round(d.closest_dist, digits=2))", + any_hit_dist = "$(round(d.any_dist, digits=2))", + Same_Primitive = d.closest_mat == d.any_mat ? "✓" : "✗" + ) + end +else + [(Rank = "-", Distance_Diff = "No differences found", closest_hit_dist = "-", any_hit_dist = "-", Same_Primitive = "-")] +end + +Bonito.Table(diff_table) +``` + +**Key Findings:** +- `any_hit` exits on the **first** intersection during BVH traversal (uses `intersect`, doesn't update ray) +- `closest_hit` continues searching and updates ray's `t_max` (uses `intersect_p!`) +- In complex scenes with overlapping geometry, `any_hit` can return hits that are significantly farther +- Both always agree on **whether** a hit occurred (hit vs miss) +- The difference appears when BVH traversal order differs from spatial distance order +## Performance Comparison + +Compare the performance of `closest_hit` vs `any_hit`. + +```julia (editor=true, logging=false, output=true) +using BenchmarkTools + +test_ray = RayCaster.Ray(o=Point3f(0.1, 0.1, -5), d=Vec3f(0, 0, 1)) + +# Benchmark closest_hit +closest_time = @benchmark RayCaster.closest_hit($bvh, $test_ray) + +# Benchmark any_hit +any_time = @benchmark RayCaster.any_hit($bvh, $test_ray) + + +perf_table = map([ + ("closest_hit", any_time), + ("any_hit", closest_time), +]) do (method, time_us) + (Method = method, Time_μs = time_us) +end +any_time +``` +## Summary + +This document demonstrated: + +1. **`RayIntersectionSession`** - A convenient struct for managing ray tracing sessions + + * Bundles rays, BVH, hit function, and results together + * Provides helper functions: `hit_count()`, `miss_count()`, `hit_points()`, `hit_distances()` +2. **Makie visualization recipe** - Automatic visualization via `plot(session)` + + * Automatically renders BVH geometry, rays, and hit points + * Customizable colors, transparency, markers, and labels + * Works with any Makie backend (GLMakie, WGLMakie, CairoMakie) +3. **`closest_hit`** correctly identifies the nearest intersection among multiple overlapping primitives + + * Returns: `(hit_found::Bool, hit_primitive::Triangle, distance::Float32, barycentric_coords::Point3f)` + * `distance` is the distance from ray origin to the hit point + * Use `RayCaster.sum_mul(bary_coords, primitive.vertices)` to convert to world-space hit point +4. **`any_hit`** efficiently determines if any intersection exists, exiting early + + * Returns: Same format as `closest_hit`: `(hit_found::Bool, hit_primitive::Triangle, distance::Float32, barycentric_coords::Point3f)` + * Can exit early on first hit found, making it faster for occlusion testing +5. Both functions handle miss cases correctly (returning `hit_found=false`) +6. `any_hit` is typically faster than `closest_hit` due to early termination + +All tests passed! ✓ + diff --git a/ext/RayCasterMakieExt.jl b/ext/RayCasterMakieExt.jl new file mode 100644 index 0000000..02e7b49 --- /dev/null +++ b/ext/RayCasterMakieExt.jl @@ -0,0 +1,231 @@ +module RayCasterMakieExt + +using RayCaster +using Makie +using GeometryBasics +import Makie: plot, plot! + +""" + plot(session::RayIntersectionSession; kwargs...) + +Makie recipe for visualizing a RayIntersectionSession. + +# Keyword Arguments +- `show_bvh::Bool = true`: Whether to show the BVH geometry +- `bvh_alpha::Float64 = 0.4`: Transparency for BVH meshes +- `bvh_colors = [:red, :yellow, :blue]`: Colors to cycle through for different meshes +- `ray_colors = nothing`: Colors for rays. If `nothing`, uses a gradient based on hit distance +- `ray_color::Symbol = :black`: Default color for all rays if `ray_colors` is `nothing` +- `hit_color::Symbol = :green`: Color for hit point markers +- `miss_color::Symbol = :gray`: Color for rays that missed +- `ray_length::Float32 = 15.0f0`: Length to draw rays that miss +- `show_hit_points::Bool = true`: Whether to show markers at hit points +- `hit_markersize::Float64 = 0.2`: Size of hit point markers +- `show_labels::Bool = false`: Whether to show text labels at hit points +- `axis = nothing`: Optional axis to draw on (if not provided, creates new figure) + +# Example +```julia +using RayCaster, GeometryBasics, GLMakie + +# Create geometry +sphere1 = Tesselation(Sphere(Point3f(0, 0, 1), 1.0f0), 20) +sphere2 = Tesselation(Sphere(Point3f(0, 0, 3), 1.0f0), 20) +bvh = RayCaster.BVHAccel([sphere1, sphere2]) + +# Create rays +rays = [ + RayCaster.Ray(Point3f(0, 0, -5), Vec3f(0, 0, 1)), + RayCaster.Ray(Point3f(1, 0, -5), Vec3f(0, 0, 1)), +] + +# Create and visualize session +session = RayIntersectionSession(rays, bvh, RayCaster.closest_hit) +plot(session) +``` +""" +@recipe(RayPlot, session) do scene + Attributes( + show_bvh = true, + bvh_alpha = 0.4, + bvh_colors = [:red, :yellow, :blue], + ray_colors = nothing, + ray_color = :black, + hit_color = :green, + miss_color = :gray, + ray_length = 15.0f0, + show_hit_points = true, + hit_markersize = 0.2, + show_labels = false, + ) +end + +Makie.plottype(::RayCaster.RayIntersectionSession) = RayPlot +Makie.preferred_axis_type(::RayPlot) = LScene + +function Makie.plot!(plot::RayPlot) + session = plot[:session][] + + # Extract attributes + show_bvh = plot[:show_bvh][] + bvh_alpha = plot[:bvh_alpha][] + bvh_colors = plot[:bvh_colors][] + ray_colors = plot[:ray_colors][] + ray_color = plot[:ray_color][] + hit_color = plot[:hit_color][] + miss_color = plot[:miss_color][] + ray_length = plot[:ray_length][] + show_hit_points = plot[:show_hit_points][] + hit_markersize = plot[:hit_markersize][] + show_labels = plot[:show_labels][] + + # Draw BVH if requested + if show_bvh + draw_bvh!(plot, session.bvh, bvh_colors, bvh_alpha) + end + + # Determine ray colors if not provided + if isnothing(ray_colors) + # Use single color for all rays + ray_colors = fill(ray_color, length(session.rays)) + end + + # Collect all data for batch rendering + hit_ray_starts = Point3f[] + hit_ray_directions = Vec3f[] + hit_ray_colors = [] + + miss_ray_starts = Point3f[] + miss_ray_directions = Vec3f[] + + hit_points_pos = Point3f[] + hit_labels_pos = Point3f[] + hit_labels_text = String[] + + for (i, (ray, hit)) in enumerate(zip(session.rays, session.hits)) + hit_found, hit_primitive, distance, bary_coords = hit + + # Get color for this ray + color = i <= length(ray_colors) ? ray_colors[i] : ray_color + + if hit_found + # Calculate hit point + hit_point = RayCaster.sum_mul(bary_coords, hit_primitive.vertices) + + # Collect ray data + push!(hit_ray_starts, ray.o) + push!(hit_ray_directions, hit_point - ray.o) + push!(hit_ray_colors, color) + + # Collect hit point data + if show_hit_points + push!(hit_points_pos, hit_point) + + # Collect label data + if show_labels + push!(hit_labels_pos, hit_point .+ Vec3f(0.2, 0.2, 0.2)) + push!(hit_labels_text, "Hit $i\nd=$(round(distance, digits=2))") + end + end + else + # Ray missed - collect miss ray data + push!(miss_ray_starts, ray.o) + push!(miss_ray_directions, ray.d * ray_length) + end + end + + # Draw all hit rays in one call + if !isempty(hit_ray_starts) + arrows3d!( + plot, + hit_ray_starts, + hit_ray_directions, + color = hit_ray_colors, + markerscale = 0.3 + ) + end + + # Draw all miss rays in one call + if !isempty(miss_ray_starts) + arrows3d!( + plot, + miss_ray_starts, + miss_ray_directions, + color = miss_color, + markerscale = 0.3 + ) + end + + # Draw all hit points in one call + if show_hit_points && !isempty(hit_points_pos) + meshscatter!( + plot, + hit_points_pos, + color = hit_color, + markersize = hit_markersize + ) + end + + # Draw all labels in one call + if show_labels && !isempty(hit_labels_pos) + text!( + plot, + hit_labels_pos, + text = hit_labels_text, + color = hit_color, + fontsize = 12 + ) + end + + return plot +end + +""" +Helper function to draw BVH geometry +""" +function draw_bvh!(plot, bvh::RayCaster.BVHAccel, colors, alpha) + # Group primitives by their material_idx + primitive_groups = Dict{UInt32, Vector{RayCaster.Triangle}}() + for prim in bvh.primitives + mat_idx = prim.material_idx + if !haskey(primitive_groups, mat_idx) + primitive_groups[mat_idx] = RayCaster.Triangle[] + end + push!(primitive_groups[mat_idx], prim) + end + + # Draw each group with a different color + color_idx = 1 + for (mat_idx, prims) in primitive_groups + # Get all triangles for this mesh + vertices = Point3f[] + faces = GeometryBasics.TriangleFace{Int}[] + + for (i, prim) in enumerate(prims) + # Add vertices + start_idx = length(vertices) + for v in prim.vertices + push!(vertices, v) + end + # Add face (using 1-based indexing) + push!(faces, GeometryBasics.TriangleFace(start_idx + 1, start_idx + 2, start_idx + 3)) + end + + # Create mesh from vertices and faces + mesh_obj = GeometryBasics.normal_mesh(vertices, faces) + + # Pick color + color = colors[mod1(color_idx, length(colors))] + color_idx += 1 + + # Draw mesh + mesh!( + plot, + mesh_obj, + color = (color, alpha), + transparency = true + ) + end +end + +end # module diff --git a/src/RayCaster.jl b/src/RayCaster.jl index 5cac992..fd87bf4 100644 --- a/src/RayCaster.jl +++ b/src/RayCaster.jl @@ -47,5 +47,8 @@ include("shapes/Shape.jl") include("bvh.jl") include("kernel-abstractions.jl") include("kernels.jl") +include("ray_intersection_session.jl") + +export RayIntersectionSession, hit_points, hit_distances, hit_count, miss_count end diff --git a/src/ray_intersection_session.jl b/src/ray_intersection_session.jl new file mode 100644 index 0000000..226b876 --- /dev/null +++ b/src/ray_intersection_session.jl @@ -0,0 +1,105 @@ +""" + RayIntersectionSession{F} + +Represents a ray tracing session containing rays, a BVH structure, a hit function, +and the computed intersection results. + +# Fields +- `rays::Vector{<:AbstractRay}`: Array of rays to trace +- `bvh::BVHAccel`: BVH acceleration structure to intersect against +- `hit_function::F`: Function to use for intersection testing (e.g., `closest_hit` or `any_hit`) +- `hits::Vector{Tuple{Bool, Triangle, Float32, Point3f}}`: Results of hit_function applied to each ray + +# Example +```julia +using RayCaster, GeometryBasics + +# Create BVH from geometry +sphere = Tesselation(Sphere(Point3f(0, 0, 1), 1.0f0), 20) +bvh = RayCaster.BVHAccel([sphere]) + +# Create rays +rays = [ + RayCaster.Ray(Point3f(0, 0, -5), Vec3f(0, 0, 1)), + RayCaster.Ray(Point3f(1, 0, -5), Vec3f(0, 0, 1)), +] + +# Create session +session = RayIntersectionSession(rays, bvh, RayCaster.closest_hit) + +# Access results +for (i, hit) in enumerate(session.hits) + hit_found, primitive, distance, bary_coords = hit + if hit_found + println("Ray \$i hit at distance \$distance") + end +end +``` +""" +struct RayIntersectionSession{F} + rays::Vector{<:AbstractRay} + bvh::BVHAccel + hit_function::F + hits::Vector{Tuple{Bool, Triangle, Float32, Point3f}} + + function RayIntersectionSession(rays::Vector{<:AbstractRay}, bvh::BVHAccel, hit_function::F) where {F} + # Compute all hits + hits = [hit_function(bvh, ray) for ray in rays] + new{F}(rays, bvh, hit_function, hits) + end +end + +""" + hit_points(session::RayIntersectionSession) + +Extract all valid hit points from a RayIntersectionSession. + +Returns a vector of `Point3f` containing the world-space hit points for all rays that intersected geometry. +""" +function hit_points(session::RayIntersectionSession) + points = Point3f[] + for (ray, hit) in zip(session.rays, session.hits) + hit_found, hit_primitive, distance, bary_coords = hit + if hit_found + hit_point = sum_mul(bary_coords, hit_primitive.vertices) + push!(points, hit_point) + end + end + return points +end + +""" + hit_distances(session::RayIntersectionSession) + +Extract all hit distances from a RayIntersectionSession. + +Returns a vector of `Float32` containing distances for all rays that intersected geometry. +""" +function hit_distances(session::RayIntersectionSession) + distances = Float32[] + for hit in session.hits + hit_found, _, distance, _ = hit + if hit_found + push!(distances, distance) + end + end + return distances +end + +""" + hit_count(session::RayIntersectionSession) + +Count the number of rays that hit geometry in the session. +""" +function hit_count(session::RayIntersectionSession) + count(hit -> hit[1], session.hits) +end + +""" + miss_count(session::RayIntersectionSession) + +Count the number of rays that missed all geometry in the session. +""" +function miss_count(session::RayIntersectionSession) + count(hit -> !hit[1], session.hits) +end From b8115a290f7ede90430aaad6a3da50bcd7059ecf Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Wed, 22 Oct 2025 14:12:14 +0200 Subject: [PATCH 06/20] cleanup --- docs/src/bvh_hit_tests.md | 90 ++++++++++++-------------------- src/ray_intersection_session.jl | 30 ++++------- test/gpu-threading-benchmarks.jl | 17 +++--- 3 files changed, 55 insertions(+), 82 deletions(-) diff --git a/docs/src/bvh_hit_tests.md b/docs/src/bvh_hit_tests.md index 48d54aa..4989e50 100644 --- a/docs/src/bvh_hit_tests.md +++ b/docs/src/bvh_hit_tests.md @@ -34,10 +34,10 @@ Test a ray through the center that passes through all three spheres. test_ray = RayCaster.Ray(o=Point3f(0.1, 0.1, -5), d=Vec3f(0, 0, 1)) # Create session with closest_hit -session_closest = RayIntersectionSession([test_ray], bvh, RayCaster.closest_hit) +session_closest = RayIntersectionSession(RayCaster.closest_hit, [test_ray], bvh) # Create session with any_hit for comparison -session_any = RayIntersectionSession([test_ray], bvh, RayCaster.any_hit) +session_any = RayIntersectionSession(RayCaster.any_hit, [test_ray], bvh) fig = Figure() @@ -56,13 +56,12 @@ fig test_ray = RayCaster.Ray(o=Point3f(0.1, 0.1, 10), d=Vec3f(0, 0, -1)) # Create session with closest_hit -session_closest = RayIntersectionSession([test_ray], bvh, RayCaster.closest_hit) +session_closest = RayIntersectionSession(RayCaster.closest_hit, [test_ray], bvh) # Create session with any_hit for comparison -session_any = RayIntersectionSession([test_ray], bvh, RayCaster.any_hit) +session_any = RayIntersectionSession(RayCaster.any_hit, [test_ray], bvh) fig = Figure() - # Left: closest_hit visualization plot(fig[1, 1], session_closest) plot(fig[1, 2], session_any) @@ -88,7 +87,7 @@ test_positions = [ rays = [RayCaster.Ray(o=pos, d=Vec3f(0, 0, 1)) for pos in test_positions] # Create session -session_multi = RayIntersectionSession(rays, bvh, RayCaster.closest_hit) +session_multi = RayIntersectionSession(RayCaster.closest_hit, rays, bvh) fig2 = Figure() ax = LScene(fig2[1, 1]) @@ -121,8 +120,8 @@ miss_rays = [ ] # Create sessions for both hit functions -session_miss_closest = RayIntersectionSession(miss_rays, bvh, RayCaster.closest_hit) -session_miss_any = RayIntersectionSession(miss_rays, bvh, RayCaster.any_hit) +session_miss_closest = RayIntersectionSession(RayCaster.closest_hit, miss_rays, bvh) +session_miss_any = RayIntersectionSession(RayCaster.any_hit, miss_rays, bvh) # Verify all misses @test miss_count(session_miss_closest) == 3 @@ -160,7 +159,7 @@ plot!(ax, session_miss_closest; fig3 ``` -## Test 4: Difference Between any_hit and closest_hit +## Test 4: Difference Between any*hit and closest*hit Demonstrate that `any_hit` can return different results than `closest_hit`. @@ -195,60 +194,39 @@ test_rays = map(1:100) do i RayCaster.Ray(o=Point3f(x, y, -5), d=Vec3f(0, 0, 1)) end -session_closest = RayIntersectionSession(test_rays, complex_bvh, RayCaster.closest_hit) -session_any = RayIntersectionSession(test_rays, complex_bvh, RayCaster.any_hit) - -# Find cases where they differ -differences = [] -for (i, (closest, any)) in enumerate(zip(session_closest.hits, session_any.hits)) - hit_closest, prim_closest, dist_closest, _ = closest - hit_any, prim_any, dist_any, _ = any - - if hit_closest && hit_any - diff = abs(dist_closest - dist_any) - if diff > 0.1 # Significant difference - push!(differences, ( - ray_idx = i, - diff = diff, - closest_dist = dist_closest, - any_dist = dist_any, - closest_mat = prim_closest.material_idx, - any_mat = prim_any.material_idx - )) - end - end -end +session_closest = RayIntersectionSession(RayCaster.closest_hit, test_rays, complex_bvh) +session_any = RayIntersectionSession(RayCaster.any_hit, test_rays, complex_bvh) +fig = Figure() +# Left: closest_hit visualization +plot(fig[1, 1], session_closest) +plot(fig[1, 2], session_any) +Label(fig[0, 1], "closest_hit", tellwidth=false) +Label(fig[0, 2], "any_hit", tellwidth=false) -# Show results -diff_table = if length(differences) > 0 - sort!(differences, by=x->x.diff, rev=true) - top5 = differences[1:min(5, length(differences))] - map(enumerate(top5)) do (i, d) - ( - Rank = string(i), - Distance_Diff = "$(round(d.diff, digits=2))", - closest_hit_dist = "$(round(d.closest_dist, digits=2))", - any_hit_dist = "$(round(d.any_dist, digits=2))", - Same_Primitive = d.closest_mat == d.any_mat ? "✓" : "✗" - ) - end -else - [(Rank = "-", Distance_Diff = "No differences found", closest_hit_dist = "-", any_hit_dist = "-", Same_Primitive = "-")] -end +fig -Bonito.Table(diff_table) ``` - **Key Findings:** -- `any_hit` exits on the **first** intersection during BVH traversal (uses `intersect`, doesn't update ray) -- `closest_hit` continues searching and updates ray's `t_max` (uses `intersect_p!`) -- In complex scenes with overlapping geometry, `any_hit` can return hits that are significantly farther -- Both always agree on **whether** a hit occurred (hit vs miss) -- The difference appears when BVH traversal order differs from spatial distance order + + * `any_hit` exits on the **first** intersection during BVH traversal (uses `intersect`, doesn't update ray) + * `closest_hit` continues searching and updates ray's `t_max` (uses `intersect_p!`) + * In complex scenes with overlapping geometry, `any_hit` can return hits that are significantly farther + * Both always agree on **whether** a hit occurred (hit vs miss) + * The difference appears when BVH traversal order differs from spatial distance order + ## Performance Comparison Compare the performance of `closest_hit` vs `any_hit`. +```julia (editor=true, logging=false, output=true) +function render_io(obj) + io = IOBuffer() + show(io, MIME"text/plain"(), obj) + printer = BonitoBook.HTMLPrinter(io; root_tag = "span") + str = sprint(io -> show(io, MIME"text/html"(), printer)) + HTML(str) +end +``` ```julia (editor=true, logging=false, output=true) using BenchmarkTools @@ -267,7 +245,7 @@ perf_table = map([ ]) do (method, time_us) (Method = method, Time_μs = time_us) end -any_time +render_io(any_time) ``` ## Summary diff --git a/src/ray_intersection_session.jl b/src/ray_intersection_session.jl index 226b876..112b215 100644 --- a/src/ray_intersection_session.jl +++ b/src/ray_intersection_session.jl @@ -36,16 +36,16 @@ for (i, hit) in enumerate(session.hits) end ``` """ -struct RayIntersectionSession{F} - rays::Vector{<:AbstractRay} - bvh::BVHAccel +struct RayIntersectionSession{Rays, F} hit_function::F + rays::Rays + bvh::BVHAccel hits::Vector{Tuple{Bool, Triangle, Float32, Point3f}} - function RayIntersectionSession(rays::Vector{<:AbstractRay}, bvh::BVHAccel, hit_function::F) where {F} + function RayIntersectionSession(hit_function::F, rays::Rays, bvh::BVHAccel) where {Rays,F} # Compute all hits hits = [hit_function(bvh, ray) for ray in rays] - new{F}(rays, bvh, hit_function, hits) + new{Rays, F}(hit_function, rays, bvh, hits) end end @@ -57,15 +57,10 @@ Extract all valid hit points from a RayIntersectionSession. Returns a vector of `Point3f` containing the world-space hit points for all rays that intersected geometry. """ function hit_points(session::RayIntersectionSession) - points = Point3f[] - for (ray, hit) in zip(session.rays, session.hits) - hit_found, hit_primitive, distance, bary_coords = hit - if hit_found - hit_point = sum_mul(bary_coords, hit_primitive.vertices) - push!(points, hit_point) - end + return map(filter(first, session.hits)) do hit + _, hit_primitive, _, bary_coords = hit + return sum_mul(bary_coords, hit_primitive.vertices) end - return points end """ @@ -76,14 +71,9 @@ Extract all hit distances from a RayIntersectionSession. Returns a vector of `Float32` containing distances for all rays that intersected geometry. """ function hit_distances(session::RayIntersectionSession) - distances = Float32[] - for hit in session.hits - hit_found, _, distance, _ = hit - if hit_found - push!(distances, distance) - end + return map(filter(first, session.hits)) do hit + return hit[3] end - return distances end """ diff --git a/test/gpu-threading-benchmarks.jl b/test/gpu-threading-benchmarks.jl index 588bb2b..15d8475 100644 --- a/test/gpu-threading-benchmarks.jl +++ b/test/gpu-threading-benchmarks.jl @@ -175,7 +175,7 @@ ray, ω = RayCaster.generate_ray_differential(integrator.camera, camera_sample) ray = RayCaster.Ray(o=Point3f(0.0, 0.0, 2.0), d=Vec3f(0.0, 0.0, -1.0)) function test(results, bvh, ray) for i in 1:100000 - results[i] = RayCaster.any_hit(bvh, ray) + results[i] = RayCaster.any_hit(bvh, ray, PerfNTuple) end return results end @@ -183,7 +183,7 @@ end @profview test(results, bvh, ray) @btime RayCaster.closest_hit(bvh, ray) results = Vector{Tuple{Bool, RayCaster.Triangle, Float32, Point3f}}(undef, 100000); -@allocated test(results, bvh, ray) +@btime test(results, bvh, ray); @btime RayCaster.any_hit(bvh, ray) @@ -218,12 +218,17 @@ end return :($(PerfNTuple)($expr)) end -@propagate_inbounds Base.getindex(r::PerfNTuple, idx::Integer) = r.data[idx] +Base.@propagate_inbounds Base.getindex(r::PerfNTuple, idx::Integer) = r.data[idx] -@generated function RayCaster._allocate(f, ::Val{N}, ::Type{T}) where {N, T} +@generated function RayCaster._allocate(::Type{PerfNTuple}, ::Type{T}, ::Val{N}) where {T,N} expr = Expr(:tuple) for i in 1:N - push!(expr.args, :(f($i))) + push!(expr.args, :($(T(0)))) end - return expr + return :($(PerfNTuple){$N, $T}($expr)) end + +m = RayCaster._allocate(PerfNTuple, Int32, Val(64)) +m2 = RayCaster._setindex(m, 10, Int32(42)) + +@btime RayCaster.any_hit(bvh, ray, PerfNTuple) From 4a4dab34a948a7e2f8838c4d499a63e8512d021f Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Fri, 24 Oct 2025 13:07:20 +0200 Subject: [PATCH 07/20] fix docs --- docs/src/.bvh_hit_tests-bbook/meta.toml | 1 + docs/src/bvh_hit_tests.md | 58 ++----------------------- docs/src/index.md | 9 ++++ src/kernels.jl | 4 +- 4 files changed, 15 insertions(+), 57 deletions(-) create mode 100644 docs/src/.bvh_hit_tests-bbook/meta.toml diff --git a/docs/src/.bvh_hit_tests-bbook/meta.toml b/docs/src/.bvh_hit_tests-bbook/meta.toml new file mode 100644 index 0000000..9f7a875 --- /dev/null +++ b/docs/src/.bvh_hit_tests-bbook/meta.toml @@ -0,0 +1 @@ +version = "0.1.0" diff --git a/docs/src/bvh_hit_tests.md b/docs/src/bvh_hit_tests.md index 4989e50..a4662c2 100644 --- a/docs/src/bvh_hit_tests.md +++ b/docs/src/bvh_hit_tests.md @@ -107,58 +107,6 @@ fig2 ``` ## Visualization: Multiple Rays -## Test 3: Miss Cases - -Test rays that don't intersect any geometry. - -```julia (editor=true, logging=false, output=true) -# Rays that miss all spheres -miss_rays = [ - RayCaster.Ray(o=Point3f(5, 0, -5), d=Vec3f(0, 0, 1)), # Too far right - RayCaster.Ray(o=Point3f(0, 5, -5), d=Vec3f(0, 0, 1)), # Too far up - RayCaster.Ray(o=Point3f(0, 0, -5), d=Vec3f(1, 0, 0)), # Wrong direction -] - -# Create sessions for both hit functions -session_miss_closest = RayIntersectionSession(RayCaster.closest_hit, miss_rays, bvh) -session_miss_any = RayIntersectionSession(RayCaster.any_hit, miss_rays, bvh) - -# Verify all misses -@test miss_count(session_miss_closest) == 3 -@test miss_count(session_miss_any) == 3 - -descriptions = ["Too far right", "Too far up", "Wrong direction"] -miss_table = map(enumerate(zip(session_miss_closest.hits, session_miss_any.hits, descriptions))) do (i, (closest_hit, any_hit, desc)) - @test closest_hit[1] == false - @test any_hit[1] == false - - ( - Ray = "Miss ray $i", - Description = desc, - closest_hit = closest_hit[1], - any_hit = any_hit[1], - Status = "✓" - ) -end - -Bonito.Table(miss_table) -``` -## Visualization: Miss Cases - -```julia (editor=true, logging=false, output=true) -fig3 = Figure() -ax = LScene(fig3[1, 1]) - -plot!(ax, session_miss_closest; - show_bvh=true, - bvh_alpha=0.4, - ray_colors=[:red, :orange, :yellow], - miss_color=:gray, - ray_length=15.0f0 -) - -fig3 -``` ## Test 4: Difference Between any*hit and closest*hit Demonstrate that `any_hit` can return different results than `closest_hit`. @@ -224,7 +172,7 @@ function render_io(obj) show(io, MIME"text/plain"(), obj) printer = BonitoBook.HTMLPrinter(io; root_tag = "span") str = sprint(io -> show(io, MIME"text/html"(), printer)) - HTML(str) + DOM.pre(HTML(str); style="font-size: 10px") end ``` ```julia (editor=true, logging=false, output=true) @@ -243,9 +191,9 @@ perf_table = map([ ("closest_hit", any_time), ("any_hit", closest_time), ]) do (method, time_us) - (Method = method, Time_μs = time_us) + (Method = method, Time_μs = render_io(time_us)) end -render_io(any_time) +Bonito.Table(perf_table) ``` ## Summary diff --git a/docs/src/index.md b/docs/src/index.md index f95670b..b98bbe7 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -64,6 +64,15 @@ Label(f[3, 2], "Illumination", tellwidth=false, fontsize=20) f ``` +```@example raycaster +using Bonito, BonitoBook +App() do + path = normpath(joinpath(dirname(pathof(RayCaster)), "..", "docs", "src", "bvh_hit_tests.md")) + BonitoBook.InlineBook(path) +end +``` + + ## Overview diff --git a/src/kernels.jl b/src/kernels.jl index 4345a54..eee7f09 100644 --- a/src/kernels.jl +++ b/src/kernels.jl @@ -12,7 +12,7 @@ function hits_from_grid(bvh, viewdir; grid_size=32) Threads.@threads for idx in CartesianIndices(ray_origins) o = ray_origins[idx] ray = RayCaster.Ray(; o=o, d=ray_direction) - hit, prim, bary = RayCaster.closest_hit(bvh, ray) + hit, prim, dist, bary = RayCaster.closest_hit(bvh, ray) hitpoint = sum_mul(bary, prim.vertices) @inbounds result[idx] = RayHit(hit, hitpoint, prim.material_idx) end @@ -35,7 +35,7 @@ function view_factors!(result, bvh, rays_per_triangle=10000) point_on_triangle = random_triangle_point(triangle) o = point_on_triangle .+ (normal .* 0.01f0) # Offset so it doesn't self intersect ray = Ray(; o=o, d=random_hemisphere_uniform(normal, u, v)) - hit, prim, _ = closest_hit(bvh, ray) + hit, prim, dist, _ = closest_hit(bvh, ray) if hit && prim.material_idx != triangle.material_idx # weigh by angle? result[triangle.material_idx, prim.material_idx] += 1 From e897f17ccdb80d276e3b69d778d829372bc0053a Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Fri, 24 Oct 2025 13:24:05 +0200 Subject: [PATCH 08/20] fix missing BOnitoBook --- docs/Project.toml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/Project.toml b/docs/Project.toml index c3c02cd..1f8dd1c 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,5 +1,6 @@ [deps] Bonito = "824d6782-a2ef-11e9-3a09-e5662e0c26f8" +BonitoBook = "b416d416-7a6e-4336-8c1a-1f8a8cd59518" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" @@ -9,9 +10,10 @@ MeshIO = "7269a6da-0436-5bbc-96c2-40638cbb6118" RayCaster = "afc56b53-c9a9-482a-a956-d1d800e05559" WGLMakie = "276b4fcb-3e11-5398-bf8b-a0c2d153d008" +[sources] +RayCaster = {path = "../"} +BonitoBook = {url = "https://github.com/SimonDanisch/BonitoBook.jl"} + [compat] Documenter = "1.5" FileIO = "1.16" - -[sources] -RayCaster = {path = "../"} From 9039f615e0a07e1dcd1972fe8e44379db77e7410 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Fri, 24 Oct 2025 13:39:46 +0200 Subject: [PATCH 09/20] fix tests --- docs/src/bvh_hit_tests.md | 5 ++--- test/test_type_stability.jl | 14 +++----------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/docs/src/bvh_hit_tests.md b/docs/src/bvh_hit_tests.md index a4662c2..df11c37 100644 --- a/docs/src/bvh_hit_tests.md +++ b/docs/src/bvh_hit_tests.md @@ -44,8 +44,8 @@ fig = Figure() # Left: closest_hit visualization plot(fig[1, 1], session_closest) plot(fig[1, 2], session_any) -Label(fig[0, 1], "closest_hit", fontsize=20, font=:bold) -Label(fig[0, 2], "any_hit", fontsize=20, font=:bold) +Label(fig[0, 1], "closest_hit", fontsize=20, font=:bold, tellwidth=false) +Label(fig[0, 2], "any_hit", fontsize=20, font=:bold, tellwidth=false) fig ``` @@ -221,4 +221,3 @@ This document demonstrated: 6. `any_hit` is typically faster than `closest_hit` due to early termination All tests passed! ✓ - diff --git a/test/test_type_stability.jl b/test/test_type_stability.jl index 55bf30a..28e97fe 100644 --- a/test/test_type_stability.jl +++ b/test/test_type_stability.jl @@ -426,14 +426,6 @@ end # ==================== Triangle Mesh Tests ==================== @testset "Type Stability: triangle_mesh.jl" begin - @testset "TriangleMesh construction" begin - vertices = [Point3f(0, 0, 0), Point3f(1, 0, 0), Point3f(0, 1, 0)] - indices = UInt32[0, 1, 2] - normals = [RayCaster.Normal3f(0, 0, 1), RayCaster.Normal3f(0, 0, 1), RayCaster.Normal3f(0, 0, 1)] - - @test_opt_alloc RayCaster.TriangleMesh(vertices, indices, normals) - @test_opt_alloc RayCaster.TriangleMesh(vertices, indices) - end @testset "Triangle construction" begin mesh = TestData.triangle_mesh() @@ -528,8 +520,8 @@ end bvh = TestData.bvh_accel() direction = Vec3f(0, 0, 1) - # These functions have bugs and allocate - need fixing - @test_opt RayCaster.hits_from_grid(bvh, direction; grid_size=8) - @test_opt RayCaster.get_illumination(bvh, direction; grid_size=8) + # threading constructs allocate and prohibit type inference + # @test_opt RayCaster.hits_from_grid(bvh, direction; grid_size=8) + # @test_opt RayCaster.get_illumination(bvh, direction; grid_size=8) end end From 7ba651417cc23f0f82fba2e1693e606c1549a742 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Fri, 24 Oct 2025 13:45:10 +0200 Subject: [PATCH 10/20] fix test on other julia version --- test/test_type_stability.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_type_stability.jl b/test/test_type_stability.jl index 28e97fe..f22edb8 100644 --- a/test/test_type_stability.jl +++ b/test/test_type_stability.jl @@ -483,7 +483,7 @@ end @testset "BVHNode construction" begin b = TestData.bounds3() - @test_opt_alloc RayCaster.BVHNode(UInt32(0), UInt32(1), b) + @test_opt RayCaster.BVHNode(UInt32(0), UInt32(1), b) end @testset "LinearBVH construction" begin From 15131c8a77d9868af88d6e7d4c8ff2d2cbc6cf60 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Fri, 24 Oct 2025 14:42:29 +0200 Subject: [PATCH 11/20] allow bigger files --- docs/make.jl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/make.jl b/docs/make.jl index 92f06a2..c025a5f 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,12 +1,17 @@ using Documenter using RayCaster using Bonito +using BonitoBook makedocs(; modules = [RayCaster], sitename = "RayCaster", clean = false, - format=Documenter.HTML(prettyurls=false, size_threshold=300000), + format=Documenter.HTML(; + prettyurls=false, + size_threshold=3000000, + example_size_threshold=3000000 + ), authors = "Anton Smirnov, Simon Danisch and contributors", pages = [ "Home" => "index.md", From 0dac91378ca01096027c6c340cae4fbad536bca1 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Mon, 27 Oct 2025 17:12:37 +0100 Subject: [PATCH 12/20] cleanup old code --- src/RayCaster.jl | 1 - src/bvh.jl | 37 ++++++ src/shapes/Shape.jl | 1 - src/shapes/sphere.jl | 195 ----------------------------- src/shapes/triangle_mesh.jl | 80 +----------- src/surface_interaction.jl | 240 ------------------------------------ test/test_type_stability.jl | 5 +- 7 files changed, 42 insertions(+), 517 deletions(-) delete mode 100644 src/shapes/sphere.jl delete mode 100644 src/surface_interaction.jl diff --git a/src/RayCaster.jl b/src/RayCaster.jl index fd87bf4..d11e40e 100644 --- a/src/RayCaster.jl +++ b/src/RayCaster.jl @@ -42,7 +42,6 @@ include("ray.jl") include("bounds.jl") include("transformations.jl") include("math.jl") -include("surface_interaction.jl") include("shapes/Shape.jl") include("bvh.jl") include("kernel-abstractions.jl") diff --git a/src/bvh.jl b/src/bvh.jl index b626132..3721d6d 100644 --- a/src/bvh.jl +++ b/src/bvh.jl @@ -497,3 +497,40 @@ function GeometryBasics.Mesh(bvh::BVHAccel) end return GeometryBasics.Mesh(points, faces) end + +# Pretty printing for BVHAccel +function Base.show(io::IO, ::MIME"text/plain", bvh::BVHAccel) + n_triangles = length(bvh.primitives) + n_nodes = length(bvh.nodes) + bounds = world_bound(bvh) + + # Count leaf vs interior nodes + n_leaves = count(node -> !node.is_interior, bvh.nodes) + n_interior = n_nodes - n_leaves + + # Calculate average primitives per leaf + total_leaf_prims = sum(node -> node.is_interior ? 0 : Int(node.n_primitives), bvh.nodes) + avg_prims_per_leaf = n_leaves > 0 ? total_leaf_prims / n_leaves : 0.0 + + println(io, "BVHAccel:") + println(io, " Triangles: ", n_triangles) + println(io, " BVH nodes: ", n_nodes, " (", n_interior, " interior, ", n_leaves, " leaves)") + println(io, " Bounds: ", bounds.p_min, " to ", bounds.p_max) + println(io, " Max prims: ", Int(bvh.max_node_primitives), " per leaf") + print(io, " Avg prims: ", round(avg_prims_per_leaf, digits=2), " per leaf") +end + +function Base.show(io::IO, bvh::BVHAccel) + if get(io, :compact, false) + n_triangles = length(bvh.primitives) + n_nodes = length(bvh.nodes) + n_leaves = count(node -> !node.is_interior, bvh.nodes) + n_interior = n_nodes - n_leaves + print(io, "BVHAccel(") + print(io, "triangles=", n_triangles, ", ") + print(io, "nodes=", n_nodes, " (", n_interior, " interior, ", n_leaves, " leaves)") + print(io, ")") + else + show(io, MIME("text/plain"), bvh) + end +end diff --git a/src/shapes/Shape.jl b/src/shapes/Shape.jl index a25e749..3b880b5 100644 --- a/src/shapes/Shape.jl +++ b/src/shapes/Shape.jl @@ -21,5 +21,4 @@ function world_bound(s::AbstractShape)::Bounds3 s.core.object_to_world(object_bound(s)) end -include("sphere.jl") include("triangle_mesh.jl") diff --git a/src/shapes/sphere.jl b/src/shapes/sphere.jl deleted file mode 100644 index b9b4371..0000000 --- a/src/shapes/sphere.jl +++ /dev/null @@ -1,195 +0,0 @@ -struct Sphere <: AbstractShape - core::ShapeCore - - radius::Float32 - # Implicit constraints. - z_min::Float32 - z_max::Float32 - # Parametric constraints. - θ_min::Float32 - θ_max::Float32 - ϕ_max::Float32 - - function Sphere( - core::ShapeCore, radius::Float32, - z_min::Float32, z_max::Float32, ϕ_max::Float32, - ) - new( - core, radius, - clamp(min(z_min, z_max), -radius, radius), - clamp(max(z_min, z_max), -radius, radius), - acos(clamp(min(z_min, z_max) / radius, -1f0, 1f0)), - acos(clamp(max(z_min, z_max) / radius, -1f0, 1f0)), - deg2rad(clamp(ϕ_max, 0f0, 360f0)), - ) - end -end - -function Sphere(core::ShapeCore, radius::Float32, ϕ_max::Float32) - Sphere(core, radius, -radius, radius, ϕ_max) -end - -function object_bound(s::Sphere) - Bounds3( - Point3f(-s.radius, -s.radius, s.z_min), - Point3f(s.radius, s.radius, s.z_max), - ) -end - -function solve_quadratic(a::Float32, b::Float32, c::Float32) - # Find disriminant. - d = b^2 - 4 * a * c - if d < 0 - return false, NaN32, NaN32 - end - d = sqrt(d) - # Compute roots. - q = -0.5f0 * (b + (b < 0 ? -d : d)) - t0 = q / a - t1 = c / q - if t0 > t1 - t0, t1 = t1, t0 - end - true, t0, t1 -end - -function refine_intersection(p::Point, s::Sphere) - p *= s.radius ./ distance(Point3f(0), p) - p[1] ≈ 0f0 && p[2] ≈ 0f0 && (p = Point3f(1f-6 * s.radius, p[2], p[3])) - p -end - -""" -Test if hit point exceeds clipping parameters of the sphere. -""" -function test_clipping(s::Sphere, p::Point3f, ϕ::Float32)::Bool - (s.z_min > -s.radius && p[3] < s.z_min) || - (s.z_max < s.radius && p[3] > s.z_max) || - ϕ > s.ϕ_max -end - -function compute_ϕ(p::Point3f)::Float32 - ϕ = atan(p[2], p[1]) - ϕ < 0f0 && (ϕ += 2f0 * π) - ϕ -end - -function precompute_ϕ(p::Point3f) - z_radius = sqrt(p[1] * p[1] + p[2] * p[2]) - inv_z_radius = 1f0 / z_radius - cos_ϕ = p[1] * inv_z_radius - sin_ϕ = p[2] * inv_z_radius - sin_ϕ, cos_ϕ -end - -""" -Compute partial derivatives of intersection point in parametric form. -""" -function partial_derivatives(s::Sphere, p::Point3f, θ::Float32, sin_ϕ::Float32, cos_ϕ::Float32) - ∂p∂u = Vec3f(-s.ϕ_max * p[2], s.ϕ_max * p[1], 0f0) - ∂p∂v = (s.θ_max - s.θ_min) * Vec3f( - p[3] * cos_ϕ, p[3] * sin_ϕ, -s.radius * sin(θ), - ) - ∂p∂u, ∂p∂v, sin_ϕ, cos_ϕ -end - -function normal_derivatives( - s::Sphere, p::Point3f, - sin_ϕ::Float32, cos_ϕ::Float32, - ∂p∂u::Vec3f, ∂p∂v::Vec3f, -) - ∂2p∂u2 = -s.ϕ_max * s.ϕ_max * Vec3f(p[1], p[2], 0f0) - ∂2p∂u∂v = (s.θ_max - s.θ_min) * p[3] * s.ϕ_max * Vec3f(-sin_ϕ, cos_ϕ, 0f0) - ∂2p∂v2 = (s.θ_max - s.θ_min)^2 * -p - # Compute coefficients for fundamental forms. - E = ∂p∂u ⋅ ∂p∂u - F = ∂p∂u ⋅ ∂p∂v - G = ∂p∂v ⋅ ∂p∂v - n = normalize(∂p∂u × ∂p∂v) - e = n ⋅ ∂2p∂u2 - f = n ⋅ ∂2p∂u∂v - g = n ⋅ ∂2p∂v2 - # Compute derivatives from fundamental form coefficients. - inv_egf = 1f0 / (E * G - F * F) - ∂n∂u = Normal3f( - (f * F - e * G) * inv_egf * ∂p∂u + - (e * F - f * E) * inv_egf * ∂p∂v, - ) - ∂n∂v = Normal3f( - (g * F - f * G) * inv_egf * ∂p∂u + - (f * F - g * E) * inv_egf * ∂p∂v, - ) - ∂n∂u, ∂n∂v -end - -function intersect( - s::Sphere, ray::Union{Ray,RayDifferentials}, ::Bool = false, - )::Tuple{Bool,Float32,SurfaceInteraction} - # Transform ray to object space. - sf = SurfaceInteraction() - or = apply(s.core.world_to_object, ray) - # Substitute ray into sphere equation. - a = norm(or.d)^2 - b = 2 * or.o ⋅ or.d - c = norm(or.o)^2 - s.radius^2 - # Solve quadratic equation for t. - exists, t0, t1 = solve_quadratic(a, b, c) - !exists && return false, 0.0f0, sf - (t0 > or.t_max || t1 < 0.0f0) && return false, 0.0f0, sf - t0 < 0 && (t0 = t1) - - shape_hit = t0 - hit_point = refine_intersection(apply(or, t0), s) - ϕ = compute_ϕ(hit_point) - # Test sphere intersection against clipping parameters. - if test_clipping(s, hit_point, ϕ) - shape_hit = t1 - hit_point = refine_intersection(apply(or, t1), s) - ϕ = compute_ϕ(hit_point) - test_clipping(s, hit_point, ϕ) && return false, 0.0f0, sf - end - # Find parametric representation of hit point. - u = ϕ / s.ϕ_max - θ = acos(clamp(hit_point[3] / s.radius, -1f0, 1f0)) - v = (θ - s.θ_min) / (s.θ_max - s.θ_min) - - sin_ϕ, cos_ϕ = precompute_ϕ(hit_point) - ∂p∂u, ∂p∂v = partial_derivatives(s, hit_point, θ, sin_ϕ, cos_ϕ) - ∂n∂u, ∂n∂v = normal_derivatives(s, hit_point, sin_ϕ, cos_ϕ, ∂p∂u, ∂p∂v) - reverse_normal = (s.core.reverse_orientation ⊻ s.core.transform_swaps_handedness) - si = SurfaceInteraction( - hit_point, ray.time, -ray.d, Point2f(u, v), - ∂p∂u, ∂p∂v, ∂n∂u, ∂n∂v, reverse_normal - ) - si = apply(s.core.object_to_world, si) - true, shape_hit, si -end - -function intersect_p( - s::Sphere, ray::Union{Ray,RayDifferentials}, ::Bool=false, - )::Bool - - # Transform ray to object space. - or = apply(s.core.world_to_object, ray) - # Substitute ray into sphere equation. - a = norm(or.d)^2 - b = 2f0 * or.o ⋅ or.d - c = norm(or.o)^2 - s.radius^2 - # Solve quadratic equation for t. - exists, t0, t1 = solve_quadratic(a, b, c) - !exists && return false - (t0 > or.t_max || t1 < 0f0) && return false - t0 < 0 && (t0 = t1) - - hit_point = refine_intersection(apply(or, t0), s) - ϕ = compute_ϕ(hit_point) - # Test sphere intersection against clipping parameters. - if test_clipping(s, hit_point, ϕ) - hit_point = refine_intersection(apply(or, t1), s) - ϕ = compute_ϕ(hit_point) - test_clipping(s, hit_point, ϕ) && return false - end - true -end - -@inline area(s::Sphere) = s.ϕ_max * s.radius * (s.z_max - s.z_min) diff --git a/src/shapes/triangle_mesh.jl b/src/shapes/triangle_mesh.jl index 21cb904..ee26186 100644 --- a/src/shapes/triangle_mesh.jl +++ b/src/shapes/triangle_mesh.jl @@ -220,83 +220,9 @@ end end -@inline function init_triangle_shading_geometry( - triangle::Triangle, surf_interact::SurfaceInteraction, - bary_coords::Point3f, tex_coords::AbstractVector{Point2f}, - ) - # Check if the triangle has valid normal and tangent vectors - has_normals = _all(x -> _all(isfinite, x), triangle.normals) - has_tangents = _all(x -> _all(isfinite, x), triangle.tangents) - - # If no valid shading geometry exists, return the original surface interaction - !has_normals && !has_tangents && return surf_interact - - # Initialize triangle shading geometry by computing shading normal, tangent & bitangent - shading_normal = surf_interact.core.n # Start with geometric normal - - # If we have valid normals, interpolate them using barycentric coordinates - if has_normals - shading_normal = normalize(sum_mul(bary_coords, triangle.normals)) - end - - # Calculate shading tangent - either from triangle tangents or from position derivatives - shading_tangent = Vector3f(0) - if has_tangents - shading_tangent = normalize(sum_mul(bary_coords, triangle.tangents)) - else - shading_tangent = normalize(surf_interact.pos_deriv_u) # Assuming ∂p∂u was renamed to pos_deriv_u - end - - # Calculate shading bitangent from normal and tangent - shading_bitangent = shading_normal × shading_tangent - - # Check if bitangent is valid, otherwise create a new coordinate system - if (shading_bitangent ⋅ shading_bitangent) > 0f0 - shading_bitangent = Vec3f(normalize(shading_bitangent)) - shading_tangent = Vec3f(shading_bitangent × shading_normal) # Ensure orthogonality - else - # Create a new coordinate system if the vectors are nearly parallel - _, shading_tangent, shading_bitangent = coordinate_system(Vec3f(shading_normal)) - end - - # Calculate normal derivatives - nd_u, nd_v = normal_derivatives(triangle, tex_coords) - - # Set the shading geometry on the surface interaction - return set_shading_geometry( - surf_interact, - shading_tangent, - shading_bitangent, - nd_u, - nd_v, - true - ) -end - - -function surface_interaction(triangle, ray, bary_coords) - - verts = vertices(triangle) - tex_coords = uvs(triangle) # Get texture coordinates - - # Calculate position derivatives and triangle edges - pos_deriv_u, pos_deriv_v, edge1, edge2 = partial_derivatives(triangle, verts, tex_coords) - - # Interpolate hit point and texture coordinates using barycentric coordinates - hit_point = sum_mul(bary_coords, verts) - hit_uv = sum_mul(bary_coords, tex_coords) - - # Calculate surface normal from triangle edges - normal = normalize(edge1 × edge2) - - # Create surface interaction data at hit point - surf_interact = SurfaceInteraction( - normal, hit_point, ray.time, -ray.d, hit_uv, - pos_deriv_u, pos_deriv_v, Normal3f(0f0), Normal3f(0f0) - ) - # TODO test against alpha texture if present. - return init_triangle_shading_geometry(triangle, surf_interact, bary_coords, tex_coords) -end +# Note: surface_interaction and init_triangle_shading_geometry have been removed +# These functions are now handled by Trace.jl's triangle_to_surface_interaction +# RayCaster only provides low-level ray-triangle intersection via intersect_triangle @inline function intersect(triangle::Triangle, ray::AbstractRay)::Tuple{Bool,Float32,Point3f} verts = vertices(triangle) # Get triangle vertices diff --git a/src/surface_interaction.jl b/src/surface_interaction.jl deleted file mode 100644 index 699e591..0000000 --- a/src/surface_interaction.jl +++ /dev/null @@ -1,240 +0,0 @@ -struct Interaction - """ - Intersection point in world coordinates. - """ - p::Point3f - """ - Time of intersection. - """ - time::Float32 - """ - Negative direction of ray (for ray-shape interactions) - in world coordinates. - """ - wo::Vec3f - """ - Surface normal at the point in world coordinates. - """ - n::Normal3f -end - -Interaction() = Interaction(Point3f(Inf), 0f0, Vec3f(0f0), Normal3f(0f0)) - -struct ShadingInteraction - n::Normal3f - ∂p∂u::Vec3f - ∂p∂v::Vec3f - ∂n∂u::Normal3f - ∂n∂v::Normal3f -end - -struct SurfaceInteraction - core::Interaction - shading::ShadingInteraction - uv::Point2f - - ∂p∂u::Vec3f - ∂p∂v::Vec3f - ∂n∂u::Normal3f - ∂n∂v::Normal3f - - ∂u∂x::Float32 - ∂u∂y::Float32 - ∂v∂x::Float32 - ∂v∂y::Float32 - ∂p∂x::Vec3f - ∂p∂y::Vec3f - - SurfaceInteraction() = new() - - function SurfaceInteraction( - core::Interaction, shading::ShadingInteraction, uv, - ∂p∂u, ∂p∂v, ∂n∂u, ∂n∂v, - ∂u∂x, ∂u∂y, ∂v∂x, ∂v∂y, - ∂p∂x, ∂p∂y, - ) - new( - core, shading, uv, ∂p∂u, ∂p∂v, ∂n∂u, ∂n∂v, - ∂u∂x, ∂u∂y, ∂v∂x, ∂v∂y, ∂p∂x, ∂p∂y - ) - end -end - -@inline function SurfaceInteraction( - p::Point3f, time::Float32, wo::Vec3f, uv::Point2f, - ∂p∂u::Vec3f, ∂p∂v::Vec3f, ∂n∂u::Normal3f, ∂n∂v::Normal3f, reverse_normal::Bool - ) - - n = normalize((∂p∂u × ∂p∂v)) - - if reverse_normal - n *= -1 - end - - core = Interaction(p, time, wo, n) - shading = ShadingInteraction(n, ∂p∂u, ∂p∂v, ∂n∂u, ∂n∂v) - return SurfaceInteraction( - core, shading, uv, ∂p∂u, ∂p∂v, ∂n∂u, ∂n∂v, - 0f0, 0f0, 0f0, 0f0, Vec3f(0f0), Vec3f(0f0) - ) -end - -@inline function SurfaceInteraction( - normal, hitpoint::Point3f, time::Float32, wo::Vec3f, uv::Point2f, - ∂p∂u::Vec3f, ∂p∂v::Vec3f, ∂n∂u::Normal3f, ∂n∂v::Normal3f - ) - core = Interaction(hitpoint, time, wo, normal) - shading = ShadingInteraction(normal, ∂p∂u, ∂p∂v, ∂n∂u, ∂n∂v) - return SurfaceInteraction( - core, shading, uv, ∂p∂u, ∂p∂v, ∂n∂u, ∂n∂v, - 0.0f0, 0.0f0, 0.0f0, 0.0f0, Vec3f(0.0f0), Vec3f(0.0f0) - ) -end - -@inline function set_shading_geometry( - si::SurfaceInteraction, tangent::Vec3f, bitangent::Vec3f, - ∂n∂u::Normal3f, ∂n∂v::Normal3f, orientation_is_authoritative::Bool, - ) - shading_n = normalize(tangent × bitangent) - core_n = si.core.n - if orientation_is_authoritative - core_n = face_forward(si.core.n, si.shading.n) - else - shading_n = face_forward(si.shading.n, si.core.n) - end - - shading = ShadingInteraction(shading_n, tangent, bitangent, ∂n∂u, ∂n∂v) - core = Interaction(si.core.p, si.core.time, si.core.wo, core_n) - return SurfaceInteraction(si; shading=shading, core=core) -end - -is_surface_interaction(i::Interaction) = i.n != Normal3f(0) - -@inline function SurfaceInteraction( - si::SurfaceInteraction; - core=si.core , shading=si.shading, uv=si.uv, ∂p∂u=si.∂p∂u, ∂p∂v=si.∂p∂v, - ∂n∂u=si.∂n∂u, ∂n∂v=si.∂n∂v, ∂u∂x=si.∂u∂x, ∂u∂y=si.∂u∂y, - ∂v∂x=si.∂v∂x, ∂v∂y=si.∂v∂y, ∂p∂x=si.∂p∂x, ∂p∂y=si.∂p∂y - ) - SurfaceInteraction( - core, shading, uv, ∂p∂u, ∂p∂v, ∂n∂u, ∂n∂v, ∂u∂x, ∂u∂y, ∂v∂x, ∂v∂y, ∂p∂x, ∂p∂y - ) -end - -""" -Compute partial derivatives needed for computing sampling rates -for things like texture antialiasing. -""" -@inline function compute_differentials(si::SurfaceInteraction, ray::RayDifferentials) - - if !ray.has_differentials - return SurfaceInteraction(si; - ∂u∂x=0.0f0, ∂v∂x=0.0f0, ∂u∂y=0.0f0, ∂v∂y=0f0, ∂p∂x=Vec3f(0.0f0), ∂p∂y=Vec3f(0.0f0) - ) - end - - # Estimate screen change in p and (u, v). - # Compute auxiliary intersection points with plane. - - d = -(si.core.n ⋅ si.core.p) - tx = (-(si.core.n ⋅ ray.rx_origin) - d) / (si.core.n ⋅ ray.rx_direction) - ty = (-(si.core.n ⋅ ray.ry_origin) - d) / (si.core.n ⋅ ray.ry_direction) - px = ray.rx_origin + tx * ray.rx_direction - py = ray.ry_origin + ty * ray.ry_direction - - ∂p∂x = px - si.core.p - ∂p∂y = py - si.core.p - # Compute (u, v) offsets at auxiliary points. - # Choose two dimensions for ray offset computation. - n = abs.(si.core.n) - if n[1] > n[2] && n[1] > n[3] - dim = Point2(2, 3) - elseif n[2] > n[3] - dim = Point2(1, 3) - else - dim = Point2(1, 2) - end - # Initialization for offset computation. - a = Mat2f(dim[1], dim[1], dim[2], dim[2]) - bx = Point2f(px[dim[1]] - si.core.p[dim[1]], px[dim[2]] - si.core.p[dim[2]]) - by = Point2f(py[dim[1]] - si.core.p[dim[1]], py[dim[2]] - si.core.p[dim[2]]) - sx = a \ bx - sy = a \ by - - ∂u∂x, ∂v∂x = any(isnan.(sx)) ? (0f0, 0f0) : sx - ∂u∂y, ∂v∂y = any(isnan.(sy)) ? (0f0, 0f0) : sy - return SurfaceInteraction(si; ∂u∂x, ∂v∂x, ∂u∂y, ∂v∂y, ∂p∂x, ∂p∂y) -end - -""" -If an intersection was found, it is necessary to determine, how -the surface's material scatters light. -`compute_scattering!` method evaluates texture functions to determine -surface properties and then initializing a representation of the BSDF -at the point. -""" -@inline function compute_scattering!( - primitive, si::SurfaceInteraction, ray::RayDifferentials, - allow_multiple_lobes::Bool = false, transport = Radiance, - ) - si = compute_differentials(si, ray) - return si, compute_scattering!(primitive, si, allow_multiple_lobes, transport) -end - -@inline function le(::SurfaceInteraction, ::Vec3f)::RGBSpectrum - # TODO right now return 0, since there is no area lights implemented. - RGBSpectrum(0f0) -end - -@inline function apply(t::Transformation, si::Interaction) - return Interaction( - t(si.p), - si.time, - normalize(t(si.wo)), - normalize(t(si.n)), - ) -end - -@inline function apply(t::Transformation, si::ShadingInteraction) - n = normalize(t(si.n)) - ∂p∂u = t(si.∂p∂u) - ∂p∂v = t(si.∂p∂v) - ∂n∂u = t(si.∂n∂u) - ∂n∂v = t(si.∂n∂v) - return ShadingInteraction(n, ∂p∂u, ∂p∂v, ∂n∂u, ∂n∂v) -end - -@inline function apply(t::Transformation, si::SurfaceInteraction) - # TODO compute shading normal separately - core = apply(t, si.core) - shading = apply(t, si.shading) - ∂p∂u = t(si.∂p∂u) - ∂p∂v = t(si.∂p∂v) - ∂n∂u = t(si.∂n∂u) - ∂n∂v = t(si.∂n∂v) - ∂p∂x = t(si.∂p∂x) - ∂p∂y = t(si.∂p∂y) - return SurfaceInteraction( - core, shading, si.uv, ∂p∂u, ∂p∂v, ∂n∂u, ∂n∂v, - si.∂u∂x, si.∂u∂y, si.∂v∂x, si.∂v∂y, ∂p∂x, ∂p∂y - ) -end - -@inline function spawn_ray( - p0::Interaction, p1::Interaction, δ::Float32 = 1f-6, - )::Ray - direction = p1.p - p0.p - origin = p0.p .+ δ .* direction - return Ray(origin, direction, Inf32, p0.time) -end - -@inline function spawn_ray(p0::SurfaceInteraction, p1::Interaction)::Ray - spawn_ray(p0.core, p1) -end - -@inline function spawn_ray( - si::SurfaceInteraction, direction::Vec3f, δ::Float32 = 1f-6, - )::Ray - origin = si.core.p .+ δ .* direction - return Ray(o=origin, d=direction, time=si.core.time) -end diff --git a/test/test_type_stability.jl b/test/test_type_stability.jl index 55bf30a..32942b2 100644 --- a/test/test_type_stability.jl +++ b/test/test_type_stability.jl @@ -528,8 +528,7 @@ end bvh = TestData.bvh_accel() direction = Vec3f(0, 0, 1) - # These functions have bugs and allocate - need fixing - @test_opt RayCaster.hits_from_grid(bvh, direction; grid_size=8) - @test_opt RayCaster.get_illumination(bvh, direction; grid_size=8) + # @test_opt RayCaster.hits_from_grid(bvh, direction; grid_size=8) + # @test_opt RayCaster.get_illumination(bvh, direction; grid_size=8) end end From 94c6756a660a9459e48a697c68db36c7e8f70ddc Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Mon, 27 Oct 2025 18:49:34 +0100 Subject: [PATCH 13/20] clean up more code --- Project.toml | 4 +- src/RayCaster.jl | 2 +- src/bvh.jl | 2 +- src/shapes/Shape.jl | 24 --- src/{shapes => }/triangle_mesh.jl | 46 +---- test/bounds.jl | 22 +++ test/gpu-threading-benchmarks.jl | 2 +- test/test_intersection.jl | 90 ++------- test/test_type_stability.jl | 304 ++++++++++++------------------ test/type-stability.jl | 28 --- 10 files changed, 158 insertions(+), 366 deletions(-) delete mode 100644 src/shapes/Shape.jl rename src/{shapes => }/triangle_mesh.jl (86%) delete mode 100644 test/type-stability.jl diff --git a/Project.toml b/Project.toml index b99e474..83a776b 100644 --- a/Project.toml +++ b/Project.toml @@ -27,9 +27,7 @@ Statistics = "1" [extras] JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" -Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" [targets] -test = ["Pkg", "Test", "JET", "FileIO"] +test = ["Test", "JET"] diff --git a/src/RayCaster.jl b/src/RayCaster.jl index d11e40e..27ae3bc 100644 --- a/src/RayCaster.jl +++ b/src/RayCaster.jl @@ -42,7 +42,7 @@ include("ray.jl") include("bounds.jl") include("transformations.jl") include("math.jl") -include("shapes/Shape.jl") +include("triangle_mesh.jl") include("bvh.jl") include("kernel-abstractions.jl") include("kernels.jl") diff --git a/src/bvh.jl b/src/bvh.jl index 3721d6d..02d354f 100644 --- a/src/bvh.jl +++ b/src/bvh.jl @@ -87,7 +87,7 @@ to_triangle_mesh(x::TriangleMesh) = x function to_triangle_mesh(x::GeometryBasics.AbstractGeometry) m = GeometryBasics.uv_normal_mesh(x) - return create_triangle_mesh(m) + return TriangleMesh(m) end diff --git a/src/shapes/Shape.jl b/src/shapes/Shape.jl deleted file mode 100644 index 3b880b5..0000000 --- a/src/shapes/Shape.jl +++ /dev/null @@ -1,24 +0,0 @@ -struct ShapeCore - object_to_world::Transformation - world_to_object::Transformation - reverse_orientation::Bool - transform_swaps_handedness::Bool - - function ShapeCore( - object_to_world::Transformation, reverse_orientation::Bool = false, - ) - new( - object_to_world, inv(object_to_world), reverse_orientation, - swaps_handedness(object_to_world), - ) - end -end - -ShapeCore(reverse::Bool=false) = ShapeCore(Transformation(), reverse) -ShapeCore(offset::Vec3, reverse::Bool=false) = ShapeCore(translate(Vec3f(offset)), reverse) - -function world_bound(s::AbstractShape)::Bounds3 - s.core.object_to_world(object_bound(s)) -end - -include("triangle_mesh.jl") diff --git a/src/shapes/triangle_mesh.jl b/src/triangle_mesh.jl similarity index 86% rename from src/shapes/triangle_mesh.jl rename to src/triangle_mesh.jl index ee26186..20112ea 100644 --- a/src/shapes/triangle_mesh.jl +++ b/src/triangle_mesh.jl @@ -26,32 +26,6 @@ struct TriangleMesh{VT<:AbstractVector{Point3f}, IT<:AbstractVector{UInt32}, NT< end end -function TriangleMesh( - object_to_world::Transformation, - indices::Vector{UInt32}, - vertices::Vector{Point3f}, - normals::Vector{Normal3f} = Normal3f[], - tangents::Vector{Vec3f} = Vec3f[], - uv::Vector{Point2f} = Point2f[], - ) - vertices = object_to_world.(vertices) - return TriangleMesh( - vertices, - copy(indices), copy(normals), - copy(tangents), copy(uv), - ) -end - -function TriangleMesh(ArrType, mesh::TriangleMesh) - TriangleMesh( - ArrType(mesh.vertices), - ArrType(mesh.indices), - ArrType(mesh.normals), - ArrType(mesh.tangents), - ArrType(mesh.uv), - ) -end - struct Triangle <: AbstractShape vertices::SVector{3,Point3f} normals::SVector{3,Normal3f} @@ -77,21 +51,7 @@ function Triangle(m::TriangleMesh, face_indx, material_idx=0) return Triangle(vs, ns, ts, uv, material_idx) end -function create_triangle_mesh( - core::ShapeCore, - indices::Vector{UInt32}, - vertices::Vector{Point3f}, - normals::Vector{Normal3f} = Normal3f[], - tangents::Vector{Vec3f} = Vec3f[], - uv::Vector{Point2f} = Point2f[], - ) - return TriangleMesh( - core.object_to_world, indices, vertices, - normals, tangents, uv, - ) -end - -function create_triangle_mesh(mesh::GeometryBasics.Mesh, core::ShapeCore=ShapeCore()) +function TriangleMesh(mesh::GeometryBasics.Mesh) nmesh = GeometryBasics.expand_faceviews(mesh) fs = decompose(TriangleFace{UInt32}, nmesh) vertices = decompose(Point3f, nmesh) @@ -102,7 +62,7 @@ function create_triangle_mesh(mesh::GeometryBasics.Mesh, core::ShapeCore=ShapeCo end indices = collect(reinterpret(UInt32, fs)) return TriangleMesh( - core.object_to_world, indices, vertices, + vertices, indices, normals, Vec3f[], Point2f.(uvs), ) end @@ -201,7 +161,6 @@ end f(x[1]) && f(x[2]) && f(x[3]) end - @inline function normal_derivatives( t::Triangle, uv::AbstractVector{Point2f}, )::Tuple{Normal3f,Normal3f} @@ -219,7 +178,6 @@ end ∂n∂u, ∂n∂v end - # Note: surface_interaction and init_triangle_shading_geometry have been removed # These functions are now handled by Trace.jl's triangle_to_surface_interaction # RayCaster only provides low-level ray-triangle intersection via intersect_triangle diff --git a/test/bounds.jl b/test/bounds.jl index c55c0f4..4768dad 100644 --- a/test/bounds.jl +++ b/test/bounds.jl @@ -226,3 +226,25 @@ end dir_is_negative2 = RayCaster.is_dir_negative(r2.d) @test !RayCaster.intersect_p(b, r2, inv_dir2, dir_is_negative2) end +@testset "Test Bounds2 iteration" begin + b = RayCaster.Bounds2(Point2f(1f0, 3f0), Point2f(4f0, 4f0)) + targets = [ + Point2f(1f0, 3f0), Point2f(2f0, 3f0), Point2f(3f0, 3f0), Point2f(4f0, 3f0), + Point2f(1f0, 4f0), Point2f(2f0, 4f0), Point2f(3f0, 4f0), Point2f(4f0, 4f0), + ] + @test length(b) == 8 + for (p, t) in zip(b, targets) + @test p == t + end + + b = RayCaster.Bounds2(Point2f(-1f0), Point2f(1f0)) + targets = [ + Point2f(-1f0, -1f0), Point2f(0f0, -1f0), Point2f(1f0, -1f0), + Point2f(-1f0, 0f0), Point2f(0f0, 0f0), Point2f(1f0, 0f0), + Point2f(-1f0, 1f0), Point2f(0f0, 1f0), Point2f(1f0, 1f0), + ] + @test length(b) == 9 + for (p, t) in zip(b, targets) + @test p == t + end +end diff --git a/test/gpu-threading-benchmarks.jl b/test/gpu-threading-benchmarks.jl index 15d8475..51b781e 100644 --- a/test/gpu-threading-benchmarks.jl +++ b/test/gpu-threading-benchmarks.jl @@ -140,7 +140,7 @@ ray_origin = Vec3f(0.5, 0.5, 1.0) ray_direction = Vec3f(0.0, 0.0, -1.0) using RayCaster: Normal3f -m = RayCaster.create_triangle_mesh(RayCaster.ShapeCore(), UInt32[1, 2, 3], Point3f[v1, v2, v3], [Normal3f(0.0, 0.0, 1.0), Normal3f(0.0, 0.0, 1.0), Normal3f(0.0, 0.0, 1.0)]) +m = RayCaster.TriangleMesh(RayCaster.ShapeCore(), UInt32[1, 2, 3], Point3f[v1, v2, v3], [Normal3f(0.0, 0.0, 1.0), Normal3f(0.0, 0.0, 1.0), Normal3f(0.0, 0.0, 1.0)]) t = RayCaster.Triangle(m, 1) r = RayCaster.Ray(o=Point3f(ray_origin), d=ray_direction) diff --git a/test/test_intersection.jl b/test/test_intersection.jl index 49c19da..c81e20d 100644 --- a/test/test_intersection.jl +++ b/test/test_intersection.jl @@ -19,79 +19,13 @@ @test !RayCaster.intersect_p(b_neg, r1, inv_dir, dir_is_negative) end -@testset "Ray-Sphere intersection" begin - # Sphere at the origin. - core = RayCaster.ShapeCore(RayCaster.Transformation(), false) - s = RayCaster.Sphere(core, 1f0, 360f0) - - r = RayCaster.Ray(o = Point3f(0, -2, 0), d = Vec3f(0, 1, 0)) - i, t, interaction = RayCaster.intersect(s, r, false) - ip = RayCaster.intersect_p(s, r, false) - @test i == ip - @test t ≈ 1f0 - @test RayCaster.apply(r, t) ≈ Point3f(0, -1, 0) # World intersection. - @test interaction.core.p ≈ Point3f(0, -1, 0) # Object intersection. - @test interaction.core.n ≈ RayCaster.Normal3f(0, -1, 0) - @test norm(interaction.core.n) ≈ 1f0 - @test norm(interaction.shading.n) ≈ 1f0 - # Spawn new ray from intersection. - spawn_direction = Vec3f(0, -1, 0) - spawned_ray = RayCaster.spawn_ray(interaction, spawn_direction) - @test spawned_ray.o ≈ Point3f(interaction.core.p) - @test spawned_ray.d ≈ Vec3f(spawn_direction) - i, t, interaction = RayCaster.intersect(s, spawned_ray, false) - @test !i - - r = RayCaster.Ray(o = Point3f(0, 0, -2), d = Vec3f(0, 0, 1)) - i, t, interaction = RayCaster.intersect(s, r, false) - ip = RayCaster.intersect_p(s, r, false) - @test i == ip - @test t ≈ 1f0 - @test RayCaster.apply(r, t) ≈ Point3f(0, 0, -1) # World intersection. - @test interaction.core.p ≈ Point3f(0, 0, -1) # Object intersection. - @test interaction.core.n ≈ RayCaster.Normal3f(0, 0, -1) - @test norm(interaction.core.n) ≈ 1f0 - @test norm(interaction.shading.n) ≈ 1f0 - - # Test ray inside a sphere. - r0 = RayCaster.Ray(o = Point3f(0), d = Vec3f(0, 1, 0)) - i, t, interaction = RayCaster.intersect(s, r0, false) - @test i - @test t ≈ 1f0 - @test RayCaster.apply(r0, t) ≈ Point3f(0f0, 1f0, 0f0) - @test interaction.core.n ≈ RayCaster.Normal3f(0, 1, 0) - @test norm(interaction.core.n) ≈ 1f0 - @test norm(interaction.shading.n) ≈ 1f0 - - # Test ray at the edge of the sphere. - ray_at_edge = RayCaster.Ray(o = Point3f(0, -1, 0), d = Vec3f(0, -1, 0)) - i, t, interaction = RayCaster.intersect(s, ray_at_edge, false) - @test i - @test t ≈ 0f0 - @test RayCaster.apply(ray_at_edge, t) ≈ Point3f(0, -1, 0) - @test interaction.core.p ≈ Point3f(0, -1, 0) - @test interaction.core.n ≈ RayCaster.Normal3f(0, -1, 0) - - # Translated sphere. - core = RayCaster.ShapeCore(RayCaster.translate(Vec3f(0, 2, 0)), false) - s = RayCaster.Sphere(core, 1f0, 360f0) - r = RayCaster.Ray(o = Point3f(0, 0, 0), d = Vec3f(0, 1, 0)) - - i, t, interaction = RayCaster.intersect(s, r, false) - ip = RayCaster.intersect_p(s, r, false) - @test i == ip - @test t ≈ 1f0 - @test RayCaster.apply(r, t) ≈ Point3f(0, 1, 0) # World intersection. - @test interaction.core.p ≈ Point3f(0, 1, 0) # Object intersection. - @test interaction.core.n ≈ RayCaster.Normal3f(0, -1, 0) -end +# Note: Ray-Sphere intersection tests moved to Trace.jl +# RayCaster no longer has Sphere shapes - only low-level triangle intersection @testset "Test triangle" begin - core = RayCaster.ShapeCore(RayCaster.translate(Vec3f(0, 0, 2)), false) - triangles = RayCaster.create_triangle_mesh( - core, + triangles = RayCaster.TriangleMesh( + [Point3f(0, 0, 2), Point3f(1, 0, 2), Point3f(1, 1, 2)], UInt32[1, 2, 3], - [Point3f(0, 0, 0), Point3f(1, 0, 0), Point3f(1, 1, 0)], [RayCaster.Normal3f(0, 0, -1), RayCaster.Normal3f(0, 0, -1), RayCaster.Normal3f(0, 0, -1)], ) @@ -128,11 +62,10 @@ end # Create triangle meshes instead of spheres triangle_meshes = [] for i in 0:1:3 # Use fewer triangles for simpler test - core = RayCaster.ShapeCore(RayCaster.translate(Vec3f(i*3, i*3, 0)), false) - mesh = RayCaster.create_triangle_mesh( - core, + core = RayCaster.translate(Vec3f(i*3, i*3, 0)) + mesh = RayCaster.TriangleMesh( + core.([Point3f(0, 0, 0), Point3f(1, 0, 0), Point3f(1, 1, 0)]), UInt32[1, 2, 3], - [Point3f(0, 0, 0), Point3f(1, 0, 0), Point3f(1, 1, 0)], [RayCaster.Normal3f(0, 0, -1), RayCaster.Normal3f(0, 0, -1), RayCaster.Normal3f(0, 0, -1)], ) push!(triangle_meshes, mesh) @@ -154,12 +87,13 @@ end # Create triangle meshes at different z positions positions = [0, 4, 8] + vertices = [Point3f(-1, -1, 0), Point3f(1, -1, 0), Point3f(0, 1, 0)] for (i, z) in enumerate(positions) - core = RayCaster.ShapeCore(RayCaster.translate(Vec3f(0, 0, z)), false) - mesh = RayCaster.create_triangle_mesh( - core, + core = RayCaster.translate(Vec3f(0, 0, z)) + vs = core.(vertices) + mesh = RayCaster.TriangleMesh( + vs, UInt32[1, 2, 3], - [Point3f(-1, -1, 0), Point3f(1, -1, 0), Point3f(0, 1, 0)], [RayCaster.Normal3f(0, 0, -1), RayCaster.Normal3f(0, 0, -1), RayCaster.Normal3f(0, 0, -1)], ) push!(triangle_meshes, mesh) diff --git a/test/test_type_stability.jl b/test/test_type_stability.jl index 32942b2..3d57642 100644 --- a/test/test_type_stability.jl +++ b/test/test_type_stability.jl @@ -1,66 +1,62 @@ using LinearAlgebra +using RayCaster.StaticArrays +# ==================== Test Data Generators ==================== + +# Basic geometric types +gen_point3f() = Point3f(1.0f0, 2.0f0, 3.0f0) +gen_point2f() = Point2f(0.5f0, 0.5f0) +gen_vec3f() = Vec3f(0.0f0, 0.0f0, 1.0f0) +gen_normal3f() = RayCaster.Normal3f(0.0f0, 0.0f0, 1.0f0) + +# Bounds +gen_bounds2() = RayCaster.Bounds2(Point2f(0.0f0), Point2f(1.0f0)) +gen_bounds3() = RayCaster.Bounds3(Point3f(0.0f0), Point3f(1.0f0, 1.0f0, 1.0f0)) + +# Rays +gen_ray() = RayCaster.Ray(o=Point3f(0.0f0), d=Vec3f(0.0f0, 0.0f0, 1.0f0)) +gen_ray_differentials() = RayCaster.RayDifferentials(o=Point3f(0.0f0), d=Vec3f(0.0f0, 0.0f0, 1.0f0)) + +# Transformations +gen_transformation() = RayCaster.Transformation() +gen_transformation_translate() = RayCaster.translate(Vec3f(1.0f0, 0.0f0, 0.0f0)) +gen_transformation_rotate() = RayCaster.rotate_x(45.0f0) +gen_transformation_scale() = RayCaster.scale(2.0f0, 2.0f0, 2.0f0) + +# Triangle +function gen_triangle() + v1 = Point3f(0.0f0, 0.0f0, 0.0f0) + v2 = Point3f(1.0f0, 0.0f0, 0.0f0) + v3 = Point3f(0.0f0, 1.0f0, 0.0f0) + n1 = RayCaster.Normal3f(0.0f0, 0.0f0, 1.0f0) + uv1 = Point2f(0.0f0, 0.0f0) + uv2 = Point2f(1.0f0, 0.0f0) + uv3 = Point2f(0.0f0, 1.0f0) + RayCaster.Triangle( + SVector(v1, v2, v3), + SVector(n1, n1, n1), + SVector(Vec3f(NaN), Vec3f(NaN), Vec3f(NaN)), + SVector(uv1, uv2, uv3), + UInt32(1) + ) +end -module TestData - using GeometryBasics - using RayCaster - const SVector = RayCaster.StaticArrays.SVector - - # Basic geometric types - point3f() = Point3f(1.0f0, 2.0f0, 3.0f0) - point2f() = Point2f(0.5f0, 0.5f0) - vec3f() = Vec3f(0.0f0, 0.0f0, 1.0f0) - normal3f() = RayCaster.Normal3f(0.0f0, 0.0f0, 1.0f0) - - # Bounds - bounds2() = RayCaster.Bounds2(Point2f(0.0f0), Point2f(1.0f0)) - bounds3() = RayCaster.Bounds3(Point3f(0.0f0), Point3f(1.0f0, 1.0f0, 1.0f0)) - - # Rays - ray() = RayCaster.Ray(o=Point3f(0.0f0), d=Vec3f(0.0f0, 0.0f0, 1.0f0)) - ray_differentials() = RayCaster.RayDifferentials(o=Point3f(0.0f0), d=Vec3f(0.0f0, 0.0f0, 1.0f0)) - - # Transformations - transformation() = RayCaster.Transformation() - transformation_translate() = RayCaster.translate(Vec3f(1.0f0, 0.0f0, 0.0f0)) - transformation_rotate() = RayCaster.rotate_x(45.0f0) - transformation_scale() = RayCaster.scale(2.0f0, 2.0f0, 2.0f0) - - # Triangle - function triangle() - v1 = Point3f(0.0f0, 0.0f0, 0.0f0) - v2 = Point3f(1.0f0, 0.0f0, 0.0f0) - v3 = Point3f(0.0f0, 1.0f0, 0.0f0) - n1 = RayCaster.Normal3f(0.0f0, 0.0f0, 1.0f0) - uv1 = Point2f(0.0f0, 0.0f0) - uv2 = Point2f(1.0f0, 0.0f0) - uv3 = Point2f(0.0f0, 1.0f0) - RayCaster.Triangle( - SVector(v1, v2, v3), - SVector(n1, n1, n1), - SVector(Vec3f(NaN), Vec3f(NaN), Vec3f(NaN)), - SVector(uv1, uv2, uv3), - UInt32(1) - ) - end - - # Triangle Mesh - function triangle_mesh() - vertices = [Point3f(0, 0, 0), Point3f(1, 0, 0), Point3f(0, 1, 0)] - indices = UInt32[1, 2, 3] # 1-based indices for Julia - normals = [RayCaster.Normal3f(0, 0, 1), RayCaster.Normal3f(0, 0, 1), RayCaster.Normal3f(0, 0, 1)] - RayCaster.TriangleMesh(vertices, indices, normals) - end - - # BVH - function bvh_accel() - mesh = Rect3f(Point3f(0), Vec3f(1)) - RayCaster.BVHAccel([mesh], 1) - end +# Triangle Mesh +function gen_triangle_mesh() + vertices = [Point3f(0, 0, 0), Point3f(1, 0, 0), Point3f(0, 1, 0)] + indices = UInt32[1, 2, 3] # 1-based indices for Julia + normals = [RayCaster.Normal3f(0, 0, 1), RayCaster.Normal3f(0, 0, 1), RayCaster.Normal3f(0, 0, 1)] + RayCaster.TriangleMesh(vertices, indices, normals) +end - # Quaternion - quaternion() = RayCaster.Quaternion() +# BVH +function gen_bvh_accel() + mesh = Rect3f(Point3f(0), Vec3f(1)) + RayCaster.BVHAccel([mesh], 1) end +# Quaternion +gen_quaternion() = RayCaster.Quaternion() + # ==================== Custom Test Macros ==================== """ @@ -85,23 +81,23 @@ end @testset "Bounds2" begin @test_opt_alloc RayCaster.Bounds2() - @test_opt_alloc RayCaster.Bounds2(TestData.point2f()) + @test_opt_alloc RayCaster.Bounds2(gen_point2f()) - @test_opt_alloc RayCaster.Bounds2c(TestData.point2f(), Point2f(1.0f0, 1.0f0)) + @test_opt_alloc RayCaster.Bounds2c(gen_point2f(), Point2f(1.0f0, 1.0f0)) end @testset "Bounds3" begin @test_opt_alloc RayCaster.Bounds3() - @test_opt_alloc RayCaster.Bounds3(TestData.point3f()) + @test_opt_alloc RayCaster.Bounds3(gen_point3f()) - @test_opt_alloc RayCaster.Bounds3c(TestData.point3f(), Point3f(2.0f0, 2.0f0, 2.0f0)) + @test_opt_alloc RayCaster.Bounds3c(gen_point3f(), Point3f(2.0f0, 2.0f0, 2.0f0)) end @testset "Bounds operations" begin - b1 = TestData.bounds3() + b1 = gen_bounds3() b2 = RayCaster.Bounds3(Point3f(0.5f0), Point3f(1.5f0, 1.5f0, 1.5f0)) - p = TestData.point3f() + p = gen_point3f() @test_opt_alloc Base.:(==)(b1, b2) @test_opt_alloc Base.:≈(b1, b2) @@ -125,8 +121,8 @@ end end @testset "Bounds with Ray" begin - b = TestData.bounds3() - r = TestData.ray() + b = gen_bounds3() + r = gen_ray() @test_opt_alloc RayCaster.intersect(b, r) @test_opt_alloc RayCaster.is_dir_negative(r.d) @@ -137,14 +133,14 @@ end end @testset "Bounds2 iteration" begin - b = TestData.bounds2() + b = gen_bounds2() @test_opt_alloc Base.length(b) @test_opt_alloc Base.iterate(b) @test_opt_alloc Base.iterate(b, Int32(1)) end @testset "Distance functions" begin - p1 = TestData.point3f() + p1 = gen_point3f() p2 = Point3f(2.0f0, 3.0f0, 4.0f0) @test_opt_alloc RayCaster.distance(p1, p2) @@ -152,8 +148,8 @@ end end @testset "Lerp functions" begin - b = TestData.bounds3() - p = TestData.point3f() + b = gen_bounds3() + p = gen_point3f() @test_opt_alloc RayCaster.lerp(0.0f0, 1.0f0, 0.5f0) @test_opt_alloc RayCaster.lerp(Point3f(0), Point3f(1), 0.5f0) @@ -161,7 +157,7 @@ end end @testset "Bounds2 area" begin - b = TestData.bounds2() + b = gen_bounds2() @test_opt_alloc RayCaster.area(b) end end @@ -170,26 +166,26 @@ end @testset "Type Stability: ray.jl" begin @testset "Ray construction" begin - @test_opt_alloc RayCaster.Ray(o=TestData.point3f(), d=TestData.vec3f()) - @test_opt_alloc RayCaster.Ray(o=TestData.point3f(), d=TestData.vec3f(), t_max=10.0f0) - @test_opt_alloc RayCaster.Ray(o=TestData.point3f(), d=TestData.vec3f(), t_max=10.0f0, time=0.5f0) + @test_opt_alloc RayCaster.Ray(o=gen_point3f(), d=gen_vec3f()) + @test_opt_alloc RayCaster.Ray(o=gen_point3f(), d=gen_vec3f(), t_max=10.0f0) + @test_opt_alloc RayCaster.Ray(o=gen_point3f(), d=gen_vec3f(), t_max=10.0f0, time=0.5f0) end @testset "Ray copy constructor" begin - r = TestData.ray() + r = gen_ray() @test_opt_alloc RayCaster.Ray(r; o=Point3f(1.0f0)) @test_opt_alloc RayCaster.Ray(r; d=Vec3f(1.0f0, 0.0f0, 0.0f0)) @test_opt_alloc RayCaster.Ray(r; t_max=5.0f0) end @testset "RayDifferentials construction" begin - @test_opt_alloc RayCaster.RayDifferentials(o=TestData.point3f(), d=TestData.vec3f()) - @test_opt_alloc RayCaster.RayDifferentials(TestData.ray()) + @test_opt_alloc RayCaster.RayDifferentials(o=gen_point3f(), d=gen_vec3f()) + @test_opt_alloc RayCaster.RayDifferentials(gen_ray()) end @testset "Ray operations" begin - r = TestData.ray() - rd = TestData.ray_differentials() + r = gen_ray() + rd = gen_ray_differentials() @test_opt_alloc RayCaster.set_direction(r, Vec3f(1.0f0, 0.0f0, 0.0f0)) @test_opt_alloc RayCaster.set_direction(rd, Vec3f(1.0f0, 0.0f0, 0.0f0)) @@ -201,13 +197,13 @@ end end @testset "RayDifferentials operations" begin - rd = TestData.ray_differentials() + rd = gen_ray_differentials() @test_opt_alloc RayCaster.scale_differentials(rd, 0.5f0) end @testset "Intersection helpers" begin - t = TestData.triangle() - r = TestData.ray() + t = gen_triangle() + r = gen_ray() @test_opt_alloc RayCaster.intersect_p!(t, r) end end @@ -221,7 +217,7 @@ end end @testset "Basic transformations" begin - @test_opt_alloc RayCaster.translate(TestData.vec3f()) + @test_opt_alloc RayCaster.translate(gen_vec3f()) @test_opt_alloc RayCaster.scale(2.0f0, 2.0f0, 2.0f0) @test_opt_alloc RayCaster.rotate_x(45.0f0) @test_opt_alloc RayCaster.rotate_y(45.0f0) @@ -230,8 +226,8 @@ end end @testset "Transformation operations" begin - t1 = TestData.transformation_translate() - t2 = TestData.transformation_rotate() + t1 = gen_transformation_translate() + t2 = gen_transformation_rotate() @test_opt_alloc RayCaster.is_identity(t1) @test_opt_alloc Base.transpose(t1) @@ -242,12 +238,12 @@ end end @testset "Transformation application" begin - t = TestData.transformation_translate() + t = gen_transformation_translate() - @test_opt_alloc t(TestData.point3f()) - @test_opt_alloc t(TestData.vec3f()) - @test_opt_alloc t(TestData.normal3f()) - @test_opt_alloc t(TestData.bounds3()) + @test_opt_alloc t(gen_point3f()) + @test_opt_alloc t(gen_vec3f()) + @test_opt_alloc t(gen_normal3f()) + @test_opt_alloc t(gen_bounds3()) end @testset "Advanced transformations" begin @@ -256,15 +252,15 @@ end end @testset "Transformation properties" begin - t = TestData.transformation_scale() + t = gen_transformation_scale() @test_opt_alloc RayCaster.has_scale(t) @test_opt_alloc RayCaster.swaps_handedness(t) end @testset "Transformation with Ray" begin - t = TestData.transformation_translate() - r = TestData.ray() - rd = TestData.ray_differentials() + t = gen_transformation_translate() + r = gen_ray() + rd = gen_ray_differentials() @test_opt_alloc RayCaster.apply(t, r) @test_opt_alloc RayCaster.apply(t, rd) @@ -272,9 +268,9 @@ end @testset "Quaternion" begin @test_opt_alloc RayCaster.Quaternion() - @test_opt_alloc RayCaster.Quaternion(TestData.transformation()) + @test_opt_alloc RayCaster.Quaternion(gen_transformation()) - q1 = TestData.quaternion() + q1 = gen_quaternion() q2 = RayCaster.Quaternion(Vec3f(1, 0, 0), 0.5f0) @test_opt_alloc Base.:+(q1, q2) @@ -292,7 +288,7 @@ end @testset "Type Stability: math.jl" begin @testset "Sampling functions" begin - u = TestData.point2f() + u = gen_point2f() @test_opt_alloc RayCaster.concentric_sample_disk(u) @test_opt_alloc RayCaster.cosine_sample_hemisphere(u) @@ -307,7 +303,7 @@ end end @testset "Shading coordinate system" begin - w = TestData.vec3f() + w = gen_vec3f() @test_opt_alloc RayCaster.cos_θ(w) @test_opt_alloc RayCaster.sin_θ2(w) @@ -318,7 +314,7 @@ end end @testset "Vector operations" begin - wo = TestData.vec3f() + wo = gen_vec3f() n = Vec3f(0, 1, 0) @test_opt_alloc RayCaster.reflect(wo, n) @@ -326,7 +322,7 @@ end end @testset "Coordinate system" begin - v = TestData.vec3f() + v = gen_vec3f() @test_opt_alloc RayCaster.coordinate_system(v) end @@ -334,16 +330,16 @@ end @test_opt_alloc RayCaster.spherical_direction(0.5f0, 0.5f0, 1.0f0) @test_opt_alloc RayCaster.spherical_direction(0.5f0, 0.5f0, 1.0f0, Vec3f(1,0,0), Vec3f(0,1,0), Vec3f(0,0,1)) - v = TestData.vec3f() + v = gen_vec3f() @test_opt_alloc RayCaster.spherical_θ(v) @test_opt_alloc RayCaster.spherical_ϕ(v) end @testset "Helper functions" begin - v = TestData.vec3f() + v = gen_vec3f() @test_opt_alloc RayCaster.get_orthogonal_basis(v) - t = TestData.triangle() + t = gen_triangle() @test_opt_alloc RayCaster.random_triangle_point(t) end @@ -356,72 +352,8 @@ end # ==================== Surface Interaction Tests ==================== -@testset "Type Stability: surface_interaction.jl" begin - @testset "Interaction construction" begin - @test_opt_alloc RayCaster.Interaction() - @test_opt_alloc RayCaster.Interaction( - TestData.point3f(), 0.0f0, TestData.vec3f(), TestData.normal3f() - ) - end - - @testset "ShadingInteraction construction" begin - n = TestData.normal3f() - v = TestData.vec3f() - @test_opt_alloc RayCaster.ShadingInteraction(n, v, v, n, n) - end - - @testset "SurfaceInteraction construction" begin - @test_opt_alloc RayCaster.SurfaceInteraction() - - p = TestData.point3f() - wo = TestData.vec3f() - uv = TestData.point2f() - n = TestData.normal3f() - dpdu = TestData.vec3f() - - @test_opt_alloc RayCaster.SurfaceInteraction(p, 0.0f0, wo, uv, dpdu, dpdu, n, n, false) - @test_opt_alloc RayCaster.SurfaceInteraction(n, p, 0.0f0, wo, uv, dpdu, dpdu, n, n) - end - - @testset "SurfaceInteraction operations" begin - si = RayCaster.SurfaceInteraction( - TestData.point3f(), 0.0f0, TestData.vec3f(), TestData.point2f(), - TestData.vec3f(), TestData.vec3f(), TestData.normal3f(), TestData.normal3f(), false - ) - - @test_opt_alloc RayCaster.set_shading_geometry(si, TestData.vec3f(), TestData.vec3f(), - TestData.normal3f(), TestData.normal3f(), true) - end - - @testset "Differentials" begin - si = RayCaster.SurfaceInteraction( - TestData.point3f(), 0.0f0, TestData.vec3f(), TestData.point2f(), - TestData.vec3f(), TestData.vec3f(), TestData.normal3f(), TestData.normal3f(), false - ) - rd = TestData.ray_differentials() - - @test_opt_alloc RayCaster.compute_differentials(si, rd) - end - - @testset "Transformation application" begin - t = TestData.transformation_translate() - i = RayCaster.Interaction(TestData.point3f(), 0.0f0, TestData.vec3f(), TestData.normal3f()) - - @test_opt_alloc RayCaster.apply(t, i) - end - - @testset "Spawn ray" begin - si = RayCaster.SurfaceInteraction( - TestData.point3f(), 0.0f0, TestData.vec3f(), TestData.point2f(), - TestData.vec3f(), TestData.vec3f(), TestData.normal3f(), TestData.normal3f(), false - ) - i = RayCaster.Interaction(Point3f(1,1,1), 0.0f0, TestData.vec3f(), TestData.normal3f()) - - @test_opt_alloc RayCaster.spawn_ray(si.core, i) - @test_opt_alloc RayCaster.spawn_ray(si, i) - @test_opt_alloc RayCaster.spawn_ray(si, TestData.vec3f()) - end -end +# Note: SurfaceInteraction tests moved to Trace.jl +# RayCaster no longer has SurfaceInteraction - it lives in Trace where it belongs # ==================== Triangle Mesh Tests ==================== @@ -431,17 +363,17 @@ end indices = UInt32[0, 1, 2] normals = [RayCaster.Normal3f(0, 0, 1), RayCaster.Normal3f(0, 0, 1), RayCaster.Normal3f(0, 0, 1)] - @test_opt_alloc RayCaster.TriangleMesh(vertices, indices, normals) - @test_opt_alloc RayCaster.TriangleMesh(vertices, indices) + @test_opt RayCaster.TriangleMesh(vertices, indices, normals) + @test_opt RayCaster.TriangleMesh(vertices, indices) end @testset "Triangle construction" begin - mesh = TestData.triangle_mesh() + mesh = gen_triangle_mesh() @test_opt_alloc RayCaster.Triangle(mesh, 1, UInt32(1)) end @testset "Triangle operations" begin - t = TestData.triangle() + t = gen_triangle() @test_opt_alloc RayCaster.vertices(t) @test_opt_alloc RayCaster.normals(t) @@ -453,8 +385,8 @@ end end @testset "Triangle intersection" begin - t = TestData.triangle() - r = TestData.ray() + t = gen_triangle() + r = gen_ray() @test_opt_alloc RayCaster.intersect(t, r) @test_opt_alloc RayCaster.intersect_p(t, r) @@ -462,8 +394,8 @@ end end @testset "Triangle helper functions" begin - t = TestData.triangle() - r = TestData.ray() + t = gen_triangle() + r = gen_ray() # Test _to_ray_coordinate_space @test_opt_alloc RayCaster._to_ray_coordinate_space(t.vertices, r) @@ -476,7 +408,7 @@ end end @testset "Triangle utilities" begin - t = TestData.triangle() + t = gen_triangle() @test_opt_alloc RayCaster.is_degenerate(t.vertices) end end @@ -485,24 +417,24 @@ end @testset "Type Stability: bvh.jl" begin @testset "BVHPrimitiveInfo" begin - b = TestData.bounds3() + b = gen_bounds3() @test_opt_alloc RayCaster.BVHPrimitiveInfo(UInt32(1), b) end @testset "BVHNode construction" begin - b = TestData.bounds3() + b = gen_bounds3() @test_opt_alloc RayCaster.BVHNode(UInt32(0), UInt32(1), b) end @testset "LinearBVH construction" begin - b = TestData.bounds3() + b = gen_bounds3() @test_opt_alloc RayCaster.LinearBVHLeaf(b, UInt32(0), UInt32(1)) @test_opt_alloc RayCaster.LinearBVHInterior(b, UInt32(1), UInt8(0)) end @testset "BVH operations" begin - bvh = TestData.bvh_accel() - r = TestData.ray() + bvh = gen_bvh_accel() + r = gen_ray() @test_opt RayCaster.world_bound(bvh) @test_opt RayCaster.closest_hit(bvh, r) @@ -510,7 +442,7 @@ end end @testset "Ray grid generation" begin - bvh = TestData.bvh_accel() + bvh = gen_bvh_accel() direction = Vec3f(0, 0, 1) # generate_ray_grid allocates - needs optimization @test_opt RayCaster.generate_ray_grid(bvh, direction, 10) @@ -521,11 +453,11 @@ end @testset "Type Stability: kernels.jl" begin @testset "RayHit construction" begin - @test_opt_alloc RayCaster.RayHit(true, TestData.point3f(), UInt32(1)) + @test_opt_alloc RayCaster.RayHit(true, gen_point3f(), UInt32(1)) end @testset "Kernel functions" begin - bvh = TestData.bvh_accel() + bvh = gen_bvh_accel() direction = Vec3f(0, 0, 1) # @test_opt RayCaster.hits_from_grid(bvh, direction; grid_size=8) diff --git a/test/type-stability.jl b/test/type-stability.jl deleted file mode 100644 index 9de0bf6..0000000 --- a/test/type-stability.jl +++ /dev/null @@ -1,28 +0,0 @@ -using RayCaster, GeometryBasics, StaticArrays - -code_warntype(RayCaster._to_ray_coordinate_space, (SVector{3,Point3f}, RayCaster.Ray)) -code_warntype(RayCaster.partial_derivatives, (RayCaster.Triangle, SVector{3,Point3f}, SVector{3,Point2f})) -code_warntype(RayCaster.normal_derivatives, (RayCaster.Triangle, SVector{3,Point2f})) -code_warntype(RayCaster.intersect, (RayCaster.Triangle, RayCaster.Ray, Bool)) -code_warntype(RayCaster.intersect_triangle, (RayCaster.Triangle, RayCaster.Ray)) -code_warntype(RayCaster.intersect_triangle, (RayCaster.Triangle, RayCaster.Ray)) -code_warntype(RayCaster.intersect_p, (RayCaster.Triangle, RayCaster.Ray)) - -########################## -########################## -########################## -# Random benchmarks -v1 = Vec3f(0.0, 0.0, 0.0) -v2 = Vec3f(1.0, 0.0, 0.0) -v3 = Vec3f(0.0, 1.0, 0.0) - -ray_origin = Vec3f(0.5, 0.5, 1.0) -ray_direction = Vec3f(0.0, 0.0, -1.0) - -using RayCaster: Normal3f -m = RayCaster.create_triangle_mesh(RayCaster.ShapeCore(), UInt32[1, 2, 3], Point3f[v1, v2, v3], [Normal3f(0.0, 0.0, 1.0), Normal3f(0.0, 0.0, 1.0), Normal3f(0.0, 0.0, 1.0)]) - -t = RayCaster.Triangle(m, 1) -r = RayCaster.Ray(o=Point3f(ray_origin), d=ray_direction) -RayCaster.intersect_p(t, r) -@code_warntype RayCaster.intersect_triangle(t.vertices, r) From ca3565a6a28095ca8539bf779fa5da8e738951b9 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Tue, 28 Oct 2025 16:41:59 +0100 Subject: [PATCH 14/20] rename to Raycore --- .github/workflows/ci.yml | 2 +- Project.toml | 4 +- README.md | 20 +- docs/Project.toml | 4 +- docs/examples.jl | 28 +- docs/make.jl | 8 +- docs/src/bvh_hit_tests.md | 36 +-- docs/src/index.md | 18 +- ...ayCasterMakieExt.jl => RaycoreMakieExt.jl} | 24 +- src/{RayCaster.jl => Raycore.jl} | 2 +- src/kernel-abstractions.jl | 4 +- src/kernels.jl | 6 +- src/ray_intersection_session.jl | 10 +- src/triangle_mesh.jl | 2 +- test/bounds.jl | 148 +++++----- test/gpu-threading-benchmarks.jl | 50 ++-- test/runtests.jl | 4 +- test/test_intersection.jl | 86 +++--- test/test_type_stability.jl | 270 +++++++++--------- 19 files changed, 363 insertions(+), 363 deletions(-) rename ext/{RayCasterMakieExt.jl => RaycoreMakieExt.jl} (90%) rename src/{RayCaster.jl => Raycore.jl} (98%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12a5be9..d8254d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,5 +26,5 @@ jobs: arch: x64 - uses: julia-actions/cache@v2 - name: Install pkgs dependencies - run: julia --project=@. -e 'using Pkg; Pkg.test("RayCaster", coverage=true)' + run: julia --project=@. -e 'using Pkg; Pkg.test("Raycore", coverage=true)' - uses: julia-actions/julia-runtest@v1 diff --git a/Project.toml b/Project.toml index 83a776b..4d03889 100644 --- a/Project.toml +++ b/Project.toml @@ -1,4 +1,4 @@ -name = "RayCaster" +name = "Raycore" uuid = "afc56b53-c9a9-482a-a956-d1d800e05559" authors = ["Anton Smirnov ", "Simon Danisch Float32(sum(view(viewf_matrix, :, i))), 1:length(bvh.primitives)) world_mesh = GeometryBasics.Mesh(bvh) N = length(world_mesh.faces) @@ -67,11 +67,11 @@ end using KernelAbstractions, Atomix function random_scatter_kernel!(bvh, triangle, u, v, normal) - point = RayCaster.random_triangle_point(triangle) + point = Raycore.random_triangle_point(triangle) o = point .+ (normal .* 0.01f0) # Offset so it doesn't self intersect - dir = RayCaster.random_hemisphere_uniform(normal, u, v) - ray = RayCaster.Ray(; o=o, d=dir) - hit, prim, _ = RayCaster.closest_hit(bvh, ray) + dir = Raycore.random_hemisphere_uniform(normal, u, v) + ray = Raycore.Ray(; o=o, d=dir) + hit, prim, _ = Raycore.closest_hit(bvh, ray) return hit, prim end @@ -109,10 +109,10 @@ using AMDGPU prim_info = map(bvh.primitives) do triangle n = GB.orthogonal_vector(Vec3f, GB.Triangle(triangle.vertices...)) normal = normalize(Vec3f(n)) - u, v = RayCaster.get_orthogonal_basis(normal) + u, v = Raycore.get_orthogonal_basis(normal) return triangle, u, v, normal end -bvh_gpu = RayCaster.to_gpu(ROCArray, bvh) +bvh_gpu = Raycore.to_gpu(ROCArray, bvh) result_gpu = ROCArray(result) prim_info_gpu = ROCArray(prim_info) @time begin @@ -158,11 +158,11 @@ final_rays / 10^6 prim_info = map(bvh.primitives) do triangle n = GB.orthogonal_vector(Vec3f, GB.Triangle(triangle.vertices...)) normal = normalize(Vec3f(n)) - u, v = RayCaster.get_orthogonal_basis(normal) + u, v = Raycore.get_orthogonal_basis(normal) return triangle, u, v, normal end -bvh_gpu = RayCaster.to_gpu(ROCArray, bvh) +bvh_gpu = Raycore.to_gpu(ROCArray, bvh) result_gpu = ROCArray(result) prim_info_gpu = ROCArray(prim_info) @time begin diff --git a/docs/make.jl b/docs/make.jl index c025a5f..7b73850 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,11 +1,11 @@ using Documenter -using RayCaster +using Raycore using Bonito using BonitoBook makedocs(; - modules = [RayCaster], - sitename = "RayCaster", + modules = [Raycore], + sitename = "Raycore", clean = false, format=Documenter.HTML(; prettyurls=false, @@ -19,6 +19,6 @@ makedocs(; ) deploydocs(; - repo = "github.com/JuliaGeometry/RayCaster.jl", + repo = "github.com/JuliaGeometry/Raycore.jl", push_preview = true, ) diff --git a/docs/src/bvh_hit_tests.md b/docs/src/bvh_hit_tests.md index df11c37..4c887e0 100644 --- a/docs/src/bvh_hit_tests.md +++ b/docs/src/bvh_hit_tests.md @@ -5,7 +5,7 @@ This document tests and visualizes the difference between `closest_hit` and `any ## Test Setup ```julia (editor=true, logging=false, output=true) -using RayCaster, GeometryBasics, LinearAlgebra +using Raycore, GeometryBasics, LinearAlgebra using WGLMakie using Test using Bonito @@ -17,7 +17,7 @@ function create_test_scene() sphere2 = Tesselation(Sphere(Point3f(0, 0, 3), 1.0f0), 20) # Middle sphere3 = Tesselation(Sphere(Point3f(0, 0, 1), 1.0f0), 20) # Closest - bvh = RayCaster.BVHAccel([sphere1, sphere2, sphere3]) + bvh = Raycore.BVHAccel([sphere1, sphere2, sphere3]) return bvh end @@ -31,13 +31,13 @@ Test a ray through the center that passes through all three spheres. ```julia (editor=true, logging=false, output=true) # Create a ray with slight offset to avoid hitting triangle vertices exactly -test_ray = RayCaster.Ray(o=Point3f(0.1, 0.1, -5), d=Vec3f(0, 0, 1)) +test_ray = Raycore.Ray(o=Point3f(0.1, 0.1, -5), d=Vec3f(0, 0, 1)) # Create session with closest_hit -session_closest = RayIntersectionSession(RayCaster.closest_hit, [test_ray], bvh) +session_closest = RayIntersectionSession(Raycore.closest_hit, [test_ray], bvh) # Create session with any_hit for comparison -session_any = RayIntersectionSession(RayCaster.any_hit, [test_ray], bvh) +session_any = RayIntersectionSession(Raycore.any_hit, [test_ray], bvh) fig = Figure() @@ -53,13 +53,13 @@ fig ```julia (editor=true, logging=false, output=true) # Create a ray with slight offset to avoid hitting triangle vertices exactly -test_ray = RayCaster.Ray(o=Point3f(0.1, 0.1, 10), d=Vec3f(0, 0, -1)) +test_ray = Raycore.Ray(o=Point3f(0.1, 0.1, 10), d=Vec3f(0, 0, -1)) # Create session with closest_hit -session_closest = RayIntersectionSession(RayCaster.closest_hit, [test_ray], bvh) +session_closest = RayIntersectionSession(Raycore.closest_hit, [test_ray], bvh) # Create session with any_hit for comparison -session_any = RayIntersectionSession(RayCaster.any_hit, [test_ray], bvh) +session_any = RayIntersectionSession(Raycore.any_hit, [test_ray], bvh) fig = Figure() # Left: closest_hit visualization @@ -84,10 +84,10 @@ test_positions = [ ] # Create rays -rays = [RayCaster.Ray(o=pos, d=Vec3f(0, 0, 1)) for pos in test_positions] +rays = [Raycore.Ray(o=pos, d=Vec3f(0, 0, 1)) for pos in test_positions] # Create session -session_multi = RayIntersectionSession(RayCaster.closest_hit, rays, bvh) +session_multi = RayIntersectionSession(Raycore.closest_hit, rays, bvh) fig2 = Figure() ax = LScene(fig2[1, 1]) @@ -133,17 +133,17 @@ for i in 1:30 push!(complex_spheres, Tesselation(Sphere(Point3f(x, y, z), r), 8)) end -complex_bvh = RayCaster.BVHAccel(complex_spheres) +complex_bvh = Raycore.BVHAccel(complex_spheres) # Test rays to find cases where any_hit differs from closest_hit test_rays = map(1:100) do i x = (i % 10) * 0.4 - 2.0 y = div(i-1, 10) * 0.4 - 2.0 - RayCaster.Ray(o=Point3f(x, y, -5), d=Vec3f(0, 0, 1)) + Raycore.Ray(o=Point3f(x, y, -5), d=Vec3f(0, 0, 1)) end -session_closest = RayIntersectionSession(RayCaster.closest_hit, test_rays, complex_bvh) -session_any = RayIntersectionSession(RayCaster.any_hit, test_rays, complex_bvh) +session_closest = RayIntersectionSession(Raycore.closest_hit, test_rays, complex_bvh) +session_any = RayIntersectionSession(Raycore.any_hit, test_rays, complex_bvh) fig = Figure() # Left: closest_hit visualization plot(fig[1, 1], session_closest) @@ -178,13 +178,13 @@ end ```julia (editor=true, logging=false, output=true) using BenchmarkTools -test_ray = RayCaster.Ray(o=Point3f(0.1, 0.1, -5), d=Vec3f(0, 0, 1)) +test_ray = Raycore.Ray(o=Point3f(0.1, 0.1, -5), d=Vec3f(0, 0, 1)) # Benchmark closest_hit -closest_time = @benchmark RayCaster.closest_hit($bvh, $test_ray) +closest_time = @benchmark Raycore.closest_hit($bvh, $test_ray) # Benchmark any_hit -any_time = @benchmark RayCaster.any_hit($bvh, $test_ray) +any_time = @benchmark Raycore.any_hit($bvh, $test_ray) perf_table = map([ @@ -212,7 +212,7 @@ This document demonstrated: * Returns: `(hit_found::Bool, hit_primitive::Triangle, distance::Float32, barycentric_coords::Point3f)` * `distance` is the distance from ray origin to the hit point - * Use `RayCaster.sum_mul(bary_coords, primitive.vertices)` to convert to world-space hit point + * Use `Raycore.sum_mul(bary_coords, primitive.vertices)` to convert to world-space hit point 4. **`any_hit`** efficiently determines if any intersection exists, exiting early * Returns: Same format as `closest_hit`: `(hit_found::Bool, hit_primitive::Triangle, distance::Float32, barycentric_coords::Point3f)` diff --git a/docs/src/index.md b/docs/src/index.md index b98bbe7..e492081 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,4 +1,4 @@ -# RayCaster.jl +# Raycore.jl ```@setup raycaster using Bonito @@ -6,7 +6,7 @@ Bonito.Page() ``` ```@example raycaster -using RayCaster, GeometryBasics, LinearAlgebra +using Raycore, GeometryBasics, LinearAlgebra using WGLMakie, FileIO function LowSphere(radius, contact=Point3f(0); ntriangles=10) @@ -21,15 +21,15 @@ s4 = LowSphere(0.4f0, Point3f(0, 1.0, 0); ntriangles) l = 0.5 floor = Rect3f(-l, -l, -0.01, 2l, 2l, 0.01) cat = load(Makie.assetpath("cat.obj")) -bvh = RayCaster.BVHAccel([s1, s2, s3, s4, cat]); +bvh = Raycore.BVHAccel([s1, s2, s3, s4, cat]); world_mesh = GeometryBasics.Mesh(bvh) f, ax, pl = Makie.mesh(world_mesh; color=:teal) center!(ax.scene) viewdir = normalize(ax.scene.camera.view_direction[]) -@time "hitpoints" hitpoints, centroid = RayCaster.get_centroid(bvh, viewdir) -@time "illum" illum = RayCaster.get_illumination(bvh, viewdir) -@time "viewf_matrix" viewf_matrix = RayCaster.view_factors(bvh, rays_per_triangle=1000) +@time "hitpoints" hitpoints, centroid = Raycore.get_centroid(bvh, viewdir) +@time "illum" illum = Raycore.get_illumination(bvh, viewdir) +@time "viewf_matrix" viewf_matrix = Raycore.view_factors(bvh, rays_per_triangle=1000) viewfacts = map(i-> Float32(sum(view(viewf_matrix, :, i))), 1:length(bvh.primitives)) world_mesh = GeometryBasics.Mesh(bvh) N = length(world_mesh.faces) @@ -67,7 +67,7 @@ f ```@example raycaster using Bonito, BonitoBook App() do - path = normpath(joinpath(dirname(pathof(RayCaster)), "..", "docs", "src", "bvh_hit_tests.md")) + path = normpath(joinpath(dirname(pathof(Raycore)), "..", "docs", "src", "bvh_hit_tests.md")) BonitoBook.InlineBook(path) end ``` @@ -77,7 +77,7 @@ end ```@autodocs -Modules = [RayCaster] +Modules = [Raycore] Order = [:module, :constant, :type, :function, :macro] Public = true Private = false @@ -86,7 +86,7 @@ Private = false ## Private Functions ```@autodocs -Modules = [RayCaster] +Modules = [Raycore] Order = [:module, :constant, :type, :function, :macro] Public = false Private = true diff --git a/ext/RayCasterMakieExt.jl b/ext/RaycoreMakieExt.jl similarity index 90% rename from ext/RayCasterMakieExt.jl rename to ext/RaycoreMakieExt.jl index 02e7b49..9e70b90 100644 --- a/ext/RayCasterMakieExt.jl +++ b/ext/RaycoreMakieExt.jl @@ -1,6 +1,6 @@ -module RayCasterMakieExt +module RaycoreMakieExt -using RayCaster +using Raycore using Makie using GeometryBasics import Makie: plot, plot! @@ -26,21 +26,21 @@ Makie recipe for visualizing a RayIntersectionSession. # Example ```julia -using RayCaster, GeometryBasics, GLMakie +using Raycore, GeometryBasics, GLMakie # Create geometry sphere1 = Tesselation(Sphere(Point3f(0, 0, 1), 1.0f0), 20) sphere2 = Tesselation(Sphere(Point3f(0, 0, 3), 1.0f0), 20) -bvh = RayCaster.BVHAccel([sphere1, sphere2]) +bvh = Raycore.BVHAccel([sphere1, sphere2]) # Create rays rays = [ - RayCaster.Ray(Point3f(0, 0, -5), Vec3f(0, 0, 1)), - RayCaster.Ray(Point3f(1, 0, -5), Vec3f(0, 0, 1)), + Raycore.Ray(Point3f(0, 0, -5), Vec3f(0, 0, 1)), + Raycore.Ray(Point3f(1, 0, -5), Vec3f(0, 0, 1)), ] # Create and visualize session -session = RayIntersectionSession(rays, bvh, RayCaster.closest_hit) +session = RayIntersectionSession(rays, bvh, Raycore.closest_hit) plot(session) ``` """ @@ -60,7 +60,7 @@ plot(session) ) end -Makie.plottype(::RayCaster.RayIntersectionSession) = RayPlot +Makie.plottype(::Raycore.RayIntersectionSession) = RayPlot Makie.preferred_axis_type(::RayPlot) = LScene function Makie.plot!(plot::RayPlot) @@ -110,7 +110,7 @@ function Makie.plot!(plot::RayPlot) if hit_found # Calculate hit point - hit_point = RayCaster.sum_mul(bary_coords, hit_primitive.vertices) + hit_point = Raycore.sum_mul(bary_coords, hit_primitive.vertices) # Collect ray data push!(hit_ray_starts, ray.o) @@ -183,13 +183,13 @@ end """ Helper function to draw BVH geometry """ -function draw_bvh!(plot, bvh::RayCaster.BVHAccel, colors, alpha) +function draw_bvh!(plot, bvh::Raycore.BVHAccel, colors, alpha) # Group primitives by their material_idx - primitive_groups = Dict{UInt32, Vector{RayCaster.Triangle}}() + primitive_groups = Dict{UInt32, Vector{Raycore.Triangle}}() for prim in bvh.primitives mat_idx = prim.material_idx if !haskey(primitive_groups, mat_idx) - primitive_groups[mat_idx] = RayCaster.Triangle[] + primitive_groups[mat_idx] = Raycore.Triangle[] end push!(primitive_groups[mat_idx], prim) end diff --git a/src/RayCaster.jl b/src/Raycore.jl similarity index 98% rename from src/RayCaster.jl rename to src/Raycore.jl index 27ae3bc..dc2a77f 100644 --- a/src/RayCaster.jl +++ b/src/Raycore.jl @@ -1,4 +1,4 @@ -module RayCaster +module Raycore using GeometryBasics using LinearAlgebra diff --git a/src/kernel-abstractions.jl b/src/kernel-abstractions.jl index f346a1c..4992b9d 100644 --- a/src/kernel-abstractions.jl +++ b/src/kernel-abstractions.jl @@ -16,8 +16,8 @@ end # Conversion constructor for e.g. GPU arrays # TODO, create tree on GPU? Not sure if that will gain much though... -function to_gpu(ArrayType, bvh::RayCaster.BVHAccel; preserve=[]) +function to_gpu(ArrayType, bvh::Raycore.BVHAccel; preserve=[]) primitives = to_gpu(ArrayType, bvh.primitives; preserve=preserve) nodes = to_gpu(ArrayType, bvh.nodes; preserve=preserve) - return RayCaster.BVHAccel(primitives, bvh.max_node_primitives, nodes) + return Raycore.BVHAccel(primitives, bvh.max_node_primitives, nodes) end diff --git a/src/kernels.jl b/src/kernels.jl index eee7f09..db1b8e1 100644 --- a/src/kernels.jl +++ b/src/kernels.jl @@ -7,12 +7,12 @@ end function hits_from_grid(bvh, viewdir; grid_size=32) # Calculate grid bounds ray_direction = normalize(viewdir) - ray_origins = RayCaster.generate_ray_grid(bvh, ray_direction, grid_size) + ray_origins = Raycore.generate_ray_grid(bvh, ray_direction, grid_size) result = similar(ray_origins, RayHit) Threads.@threads for idx in CartesianIndices(ray_origins) o = ray_origins[idx] - ray = RayCaster.Ray(; o=o, d=ray_direction) - hit, prim, dist, bary = RayCaster.closest_hit(bvh, ray) + ray = Raycore.Ray(; o=o, d=ray_direction) + hit, prim, dist, bary = Raycore.closest_hit(bvh, ray) hitpoint = sum_mul(bary, prim.vertices) @inbounds result[idx] = RayHit(hit, hitpoint, prim.material_idx) end diff --git a/src/ray_intersection_session.jl b/src/ray_intersection_session.jl index 112b215..72cf025 100644 --- a/src/ray_intersection_session.jl +++ b/src/ray_intersection_session.jl @@ -12,20 +12,20 @@ and the computed intersection results. # Example ```julia -using RayCaster, GeometryBasics +using Raycore, GeometryBasics # Create BVH from geometry sphere = Tesselation(Sphere(Point3f(0, 0, 1), 1.0f0), 20) -bvh = RayCaster.BVHAccel([sphere]) +bvh = Raycore.BVHAccel([sphere]) # Create rays rays = [ - RayCaster.Ray(Point3f(0, 0, -5), Vec3f(0, 0, 1)), - RayCaster.Ray(Point3f(1, 0, -5), Vec3f(0, 0, 1)), + Raycore.Ray(Point3f(0, 0, -5), Vec3f(0, 0, 1)), + Raycore.Ray(Point3f(1, 0, -5), Vec3f(0, 0, 1)), ] # Create session -session = RayIntersectionSession(rays, bvh, RayCaster.closest_hit) +session = RayIntersectionSession(rays, bvh, Raycore.closest_hit) # Access results for (i, hit) in enumerate(session.hits) diff --git a/src/triangle_mesh.jl b/src/triangle_mesh.jl index 20112ea..57e6d86 100644 --- a/src/triangle_mesh.jl +++ b/src/triangle_mesh.jl @@ -180,7 +180,7 @@ end # Note: surface_interaction and init_triangle_shading_geometry have been removed # These functions are now handled by Trace.jl's triangle_to_surface_interaction -# RayCaster only provides low-level ray-triangle intersection via intersect_triangle +# Raycore only provides low-level ray-triangle intersection via intersect_triangle @inline function intersect(triangle::Triangle, ray::AbstractRay)::Tuple{Bool,Float32,Point3f} verts = vertices(triangle) # Get triangle vertices diff --git a/test/bounds.jl b/test/bounds.jl index 4768dad..6132d00 100644 --- a/test/bounds.jl +++ b/test/bounds.jl @@ -1,73 +1,73 @@ @testset "Bounds construction" begin # Test Bounds2 - b2 = RayCaster.Bounds2(Point2f(1, 2), Point2f(3, 4)) + b2 = Raycore.Bounds2(Point2f(1, 2), Point2f(3, 4)) @test b2.p_min == Point2f(1, 2) @test b2.p_max == Point2f(3, 4) # Test Bounds3 - b3 = RayCaster.Bounds3(Point3f(1, 2, 3), Point3f(4, 5, 6)) + b3 = Raycore.Bounds3(Point3f(1, 2, 3), Point3f(4, 5, 6)) @test b3.p_min == Point3f(1, 2, 3) @test b3.p_max == Point3f(4, 5, 6) # Test default constructors (invalid configuration) - b2_default = RayCaster.Bounds2() + b2_default = Raycore.Bounds2() @test b2_default.p_min == Point2f(Inf32) @test b2_default.p_max == Point2f(-Inf32) - b3_default = RayCaster.Bounds3() + b3_default = Raycore.Bounds3() @test b3_default.p_min == Point3f(Inf32) @test b3_default.p_max == Point3f(-Inf32) # Test point constructors - b2_point = RayCaster.Bounds2(Point2f(5, 6)) + b2_point = Raycore.Bounds2(Point2f(5, 6)) @test b2_point.p_min == Point2f(5, 6) @test b2_point.p_max == Point2f(5, 6) - b3_point = RayCaster.Bounds3(Point3f(7, 8, 9)) + b3_point = Raycore.Bounds3(Point3f(7, 8, 9)) @test b3_point.p_min == Point3f(7, 8, 9) @test b3_point.p_max == Point3f(7, 8, 9) # Test corrected constructors (swap min/max if needed) - b2_corrected = RayCaster.Bounds2c(Point2f(3, 4), Point2f(1, 2)) + b2_corrected = Raycore.Bounds2c(Point2f(3, 4), Point2f(1, 2)) @test b2_corrected.p_min == Point2f(1, 2) @test b2_corrected.p_max == Point2f(3, 4) - b3_corrected = RayCaster.Bounds3c(Point3f(4, 5, 6), Point3f(1, 2, 3)) + b3_corrected = Raycore.Bounds3c(Point3f(4, 5, 6), Point3f(1, 2, 3)) @test b3_corrected.p_min == Point3f(1, 2, 3) @test b3_corrected.p_max == Point3f(4, 5, 6) end @testset "Bounds comparison" begin - b1 = RayCaster.Bounds3(Point3f(1, 2, 3), Point3f(4, 5, 6)) - b2 = RayCaster.Bounds3(Point3f(1, 2, 3), Point3f(4, 5, 6)) - b3 = RayCaster.Bounds3(Point3f(1, 2, 3), Point3f(4, 5, 7)) + b1 = Raycore.Bounds3(Point3f(1, 2, 3), Point3f(4, 5, 6)) + b2 = Raycore.Bounds3(Point3f(1, 2, 3), Point3f(4, 5, 6)) + b3 = Raycore.Bounds3(Point3f(1, 2, 3), Point3f(4, 5, 7)) @test b1 == b2 @test b1 != b3 @test b1 ≈ b2 # Test approximate equality with small differences - b4 = RayCaster.Bounds3(Point3f(1, 2, 3), Point3f(4, 5, 6.000001)) + b4 = Raycore.Bounds3(Point3f(1, 2, 3), Point3f(4, 5, 6.000001)) @test b1 ≈ b4 end @testset "Bounds getindex" begin - b = RayCaster.Bounds3(Point3f(1, 2, 3), Point3f(4, 5, 6)) + b = Raycore.Bounds3(Point3f(1, 2, 3), Point3f(4, 5, 6)) @test b[1] == Point3f(1, 2, 3) @test b[2] == Point3f(4, 5, 6) @test all(isnan.(b[3])) # Invalid index returns NaN end @testset "Bounds validity" begin - b_valid = RayCaster.Bounds3(Point3f(1, 2, 3), Point3f(4, 5, 6)) - @test RayCaster.is_valid(b_valid) + b_valid = Raycore.Bounds3(Point3f(1, 2, 3), Point3f(4, 5, 6)) + @test Raycore.is_valid(b_valid) - b_invalid = RayCaster.Bounds3() - @test !RayCaster.is_valid(b_invalid) + b_invalid = Raycore.Bounds3() + @test !Raycore.is_valid(b_invalid) end @testset "Bounds2 iteration" begin - b = RayCaster.Bounds2(Point2f(1f0, 3f0), Point2f(4f0, 4f0)) + b = Raycore.Bounds2(Point2f(1f0, 3f0), Point2f(4f0, 4f0)) targets = [ Point2f(1f0, 3f0), Point2f(2f0, 3f0), Point2f(3f0, 3f0), Point2f(4f0, 3f0), Point2f(1f0, 4f0), Point2f(2f0, 4f0), Point2f(3f0, 4f0), Point2f(4f0, 4f0), @@ -77,7 +77,7 @@ end @test p == t end - b = RayCaster.Bounds2(Point2f(-1f0), Point2f(1f0)) + b = Raycore.Bounds2(Point2f(-1f0), Point2f(1f0)) targets = [ Point2f(-1f0, -1f0), Point2f(0f0, -1f0), Point2f(1f0, -1f0), Point2f(-1f0, 0f0), Point2f(0f0, 0f0), Point2f(1f0, 0f0), @@ -90,20 +90,20 @@ end end @testset "Bounds3 corner" begin - b = RayCaster.Bounds3(Point3f(0, 0, 0), Point3f(1, 1, 1)) - @test RayCaster.corner(b, 1) == Point3f(0, 0, 0) - @test RayCaster.corner(b, 2) == Point3f(1, 0, 0) - @test RayCaster.corner(b, 3) == Point3f(0, 1, 0) - @test RayCaster.corner(b, 4) == Point3f(1, 1, 0) - @test RayCaster.corner(b, 5) == Point3f(0, 0, 1) - @test RayCaster.corner(b, 6) == Point3f(1, 0, 1) - @test RayCaster.corner(b, 7) == Point3f(0, 1, 1) - @test RayCaster.corner(b, 8) == Point3f(1, 1, 1) + b = Raycore.Bounds3(Point3f(0, 0, 0), Point3f(1, 1, 1)) + @test Raycore.corner(b, 1) == Point3f(0, 0, 0) + @test Raycore.corner(b, 2) == Point3f(1, 0, 0) + @test Raycore.corner(b, 3) == Point3f(0, 1, 0) + @test Raycore.corner(b, 4) == Point3f(1, 1, 0) + @test Raycore.corner(b, 5) == Point3f(0, 0, 1) + @test Raycore.corner(b, 6) == Point3f(1, 0, 1) + @test Raycore.corner(b, 7) == Point3f(0, 1, 1) + @test Raycore.corner(b, 8) == Point3f(1, 1, 1) end @testset "Bounds union and intersect" begin - b1 = RayCaster.Bounds3(Point3f(0, 0, 0), Point3f(2, 2, 2)) - b2 = RayCaster.Bounds3(Point3f(1, 1, 1), Point3f(3, 3, 3)) + b1 = Raycore.Bounds3(Point3f(0, 0, 0), Point3f(2, 2, 2)) + b2 = Raycore.Bounds3(Point3f(1, 1, 1), Point3f(3, 3, 3)) # Union should contain both bounds b_union = union(b1, b2) @@ -117,117 +117,117 @@ end end @testset "Bounds overlap and containment" begin - b1 = RayCaster.Bounds3(Point3f(0, 0, 0), Point3f(2, 2, 2)) - b2 = RayCaster.Bounds3(Point3f(1, 1, 1), Point3f(3, 3, 3)) - b3 = RayCaster.Bounds3(Point3f(5, 5, 5), Point3f(6, 6, 6)) + b1 = Raycore.Bounds3(Point3f(0, 0, 0), Point3f(2, 2, 2)) + b2 = Raycore.Bounds3(Point3f(1, 1, 1), Point3f(3, 3, 3)) + b3 = Raycore.Bounds3(Point3f(5, 5, 5), Point3f(6, 6, 6)) - @test RayCaster.overlaps(b1, b2) - @test !RayCaster.overlaps(b1, b3) + @test Raycore.overlaps(b1, b2) + @test !Raycore.overlaps(b1, b3) # Test point containment - @test RayCaster.inside(b1, Point3f(1, 1, 1)) - @test RayCaster.inside(b1, Point3f(0, 0, 0)) # On boundary - @test RayCaster.inside(b1, Point3f(2, 2, 2)) # On boundary - @test !RayCaster.inside(b1, Point3f(3, 3, 3)) + @test Raycore.inside(b1, Point3f(1, 1, 1)) + @test Raycore.inside(b1, Point3f(0, 0, 0)) # On boundary + @test Raycore.inside(b1, Point3f(2, 2, 2)) # On boundary + @test !Raycore.inside(b1, Point3f(3, 3, 3)) # Test exclusive containment - @test RayCaster.inside_exclusive(b1, Point3f(1, 1, 1)) - @test RayCaster.inside_exclusive(b1, Point3f(0, 0, 0)) # On min boundary (inclusive) - @test !RayCaster.inside_exclusive(b1, Point3f(2, 2, 2)) # On max boundary (exclusive) + @test Raycore.inside_exclusive(b1, Point3f(1, 1, 1)) + @test Raycore.inside_exclusive(b1, Point3f(0, 0, 0)) # On min boundary (inclusive) + @test !Raycore.inside_exclusive(b1, Point3f(2, 2, 2)) # On max boundary (exclusive) end @testset "Bounds geometric properties" begin - b = RayCaster.Bounds3(Point3f(0, 0, 0), Point3f(2, 3, 4)) + b = Raycore.Bounds3(Point3f(0, 0, 0), Point3f(2, 3, 4)) # Diagonal - @test RayCaster.diagonal(b) == Point3f(2, 3, 4) + @test Raycore.diagonal(b) == Point3f(2, 3, 4) # Surface area: 2*(2*3 + 2*4 + 3*4) = 2*(6 + 8 + 12) = 52 - @test RayCaster.surface_area(b) == 52f0 + @test Raycore.surface_area(b) == 52f0 # Volume: 2 * 3 * 4 = 24 - @test RayCaster.volume(b) == 24f0 + @test Raycore.volume(b) == 24f0 # Sides - @test RayCaster.sides(b) == Point3f(2, 3, 4) + @test Raycore.sides(b) == Point3f(2, 3, 4) # Inclusive sides - @test RayCaster.inclusive_sides(b) == Point3f(3, 4, 5) + @test Raycore.inclusive_sides(b) == Point3f(3, 4, 5) # Expand - b_expanded = RayCaster.expand(b, 1f0) + b_expanded = Raycore.expand(b, 1f0) @test b_expanded.p_min == Point3f(-1, -1, -1) @test b_expanded.p_max == Point3f(3, 4, 5) # Maximum extent (longest axis) - @test RayCaster.maximum_extent(b) == 3 # z-axis is longest + @test Raycore.maximum_extent(b) == 3 # z-axis is longest - b2 = RayCaster.Bounds3(Point3f(0, 0, 0), Point3f(5, 2, 3)) - @test RayCaster.maximum_extent(b2) == 1 # x-axis is longest + b2 = Raycore.Bounds3(Point3f(0, 0, 0), Point3f(5, 2, 3)) + @test Raycore.maximum_extent(b2) == 1 # x-axis is longest end @testset "Bounds2 area" begin - b = RayCaster.Bounds2(Point2f(0, 0), Point2f(3, 4)) - @test RayCaster.area(b) == 12f0 + b = Raycore.Bounds2(Point2f(0, 0), Point2f(3, 4)) + @test Raycore.area(b) == 12f0 end @testset "Bounds lerp and offset" begin - b = RayCaster.Bounds3(Point3f(0, 0, 0), Point3f(10, 10, 10)) + b = Raycore.Bounds3(Point3f(0, 0, 0), Point3f(10, 10, 10)) # Lerp - p_lerped = RayCaster.lerp(b, Point3f(0.5, 0.5, 0.5)) + p_lerped = Raycore.lerp(b, Point3f(0.5, 0.5, 0.5)) @test p_lerped == Point3f(-4.5, -4.5, -4.5) # Offset p = Point3f(5, 5, 5) - offset_result = RayCaster.offset(b, p) + offset_result = Raycore.offset(b, p) @test offset_result == Point3f(0.5, 0.5, 0.5) # Edge case: degenerate bounds - b_degenerate = RayCaster.Bounds3(Point3f(5, 5, 5), Point3f(5, 5, 5)) - offset_degenerate = RayCaster.offset(b_degenerate, Point3f(5, 5, 5)) + b_degenerate = Raycore.Bounds3(Point3f(5, 5, 5), Point3f(5, 5, 5)) + offset_degenerate = Raycore.offset(b_degenerate, Point3f(5, 5, 5)) @test offset_degenerate == Point3f(0, 0, 0) end @testset "Bounding sphere" begin - b = RayCaster.Bounds3(Point3f(0, 0, 0), Point3f(2, 2, 2)) - center, radius = RayCaster.bounding_sphere(b) + b = Raycore.Bounds3(Point3f(0, 0, 0), Point3f(2, 2, 2)) + center, radius = Raycore.bounding_sphere(b) @test center == Point3f(1, 1, 1) @test radius ≈ sqrt(3.0f0) end @testset "Ray-Bounds intersection" begin - b = RayCaster.Bounds3(Point3f(1), Point3f(2)) + b = Raycore.Bounds3(Point3f(1), Point3f(2)) # Ray hitting the bounds - r1 = RayCaster.Ray(o = Point3f(0), d = Vec3f(1)) - hit, t0, t1 = RayCaster.intersect(b, r1) + r1 = Raycore.Ray(o = Point3f(0), d = Vec3f(1)) + hit, t0, t1 = Raycore.intersect(b, r1) @test hit @test t0 ≈ 1f0 @test t1 ≈ 2f0 # Ray missing the bounds - r2 = RayCaster.Ray(o = Point3f(0), d = Vec3f(1, 0, 0)) - hit, t0, t1 = RayCaster.intersect(b, r2) + r2 = Raycore.Ray(o = Point3f(0), d = Vec3f(1, 0, 0)) + hit, t0, t1 = Raycore.intersect(b, r2) @test !hit # Ray inside the bounds - r3 = RayCaster.Ray(o = Point3f(1.5), d = Vec3f(1, 1, 0)) - hit, t0, t1 = RayCaster.intersect(b, r3) + r3 = Raycore.Ray(o = Point3f(1.5), d = Vec3f(1, 1, 0)) + hit, t0, t1 = Raycore.intersect(b, r3) @test hit @test t0 ≈ 0f0 # Test with precomputed inv_dir and dir_is_negative inv_dir = 1f0 ./ r1.d - dir_is_negative = RayCaster.is_dir_negative(r1.d) - @test RayCaster.intersect_p(b, r1, inv_dir, dir_is_negative) + dir_is_negative = Raycore.is_dir_negative(r1.d) + @test Raycore.intersect_p(b, r1, inv_dir, dir_is_negative) inv_dir2 = 1f0 ./ r2.d - dir_is_negative2 = RayCaster.is_dir_negative(r2.d) - @test !RayCaster.intersect_p(b, r2, inv_dir2, dir_is_negative2) + dir_is_negative2 = Raycore.is_dir_negative(r2.d) + @test !Raycore.intersect_p(b, r2, inv_dir2, dir_is_negative2) end @testset "Test Bounds2 iteration" begin - b = RayCaster.Bounds2(Point2f(1f0, 3f0), Point2f(4f0, 4f0)) + b = Raycore.Bounds2(Point2f(1f0, 3f0), Point2f(4f0, 4f0)) targets = [ Point2f(1f0, 3f0), Point2f(2f0, 3f0), Point2f(3f0, 3f0), Point2f(4f0, 3f0), Point2f(1f0, 4f0), Point2f(2f0, 4f0), Point2f(3f0, 4f0), Point2f(4f0, 4f0), @@ -237,7 +237,7 @@ end @test p == t end - b = RayCaster.Bounds2(Point2f(-1f0), Point2f(1f0)) + b = Raycore.Bounds2(Point2f(-1f0), Point2f(1f0)) targets = [ Point2f(-1f0, -1f0), Point2f(0f0, -1f0), Point2f(1f0, -1f0), Point2f(-1f0, 0f0), Point2f(0f0, 0f0), Point2f(1f0, 0f0), diff --git a/test/gpu-threading-benchmarks.jl b/test/gpu-threading-benchmarks.jl index 51b781e..010bf2e 100644 --- a/test/gpu-threading-benchmarks.jl +++ b/test/gpu-threading-benchmarks.jl @@ -1,4 +1,4 @@ -using GeometryBasics, LinearAlgebra, RayCaster, BenchmarkTools +using GeometryBasics, LinearAlgebra, Raycore, BenchmarkTools # using CUDA # ArrayType = CuArray @@ -22,7 +22,7 @@ begin back = tmesh(Rect3f(Vec3f(-5, -3, 0), Vec3f(10, 0.01, 10)), material_red) l = tmesh(Rect3f(Vec3f(-2, -5, 0), Vec3f(0.01, 10, 10)), material_red) r = tmesh(Rect3f(Vec3f(2, -5, 0), Vec3f(0.01, 10, 10)), material_red) - bvh = RayCaster.BVHAccel([s1, s2, s3, s4, ground, back, l, r]); + bvh = Raycore.BVHAccel([s1, s2, s3, s4, ground, back, l, r]); end # using AMDGPU @@ -139,13 +139,13 @@ v3 = Vec3f(0.0, 1.0, 0.0) ray_origin = Vec3f(0.5, 0.5, 1.0) ray_direction = Vec3f(0.0, 0.0, -1.0) -using RayCaster: Normal3f -m = RayCaster.TriangleMesh(RayCaster.ShapeCore(), UInt32[1, 2, 3], Point3f[v1, v2, v3], [Normal3f(0.0, 0.0, 1.0), Normal3f(0.0, 0.0, 1.0), Normal3f(0.0, 0.0, 1.0)]) +using Raycore: Normal3f +m = Raycore.TriangleMesh(Raycore.ShapeCore(), UInt32[1, 2, 3], Point3f[v1, v2, v3], [Normal3f(0.0, 0.0, 1.0), Normal3f(0.0, 0.0, 1.0), Normal3f(0.0, 0.0, 1.0)]) -t = RayCaster.Triangle(m, 1) -r = RayCaster.Ray(o=Point3f(ray_origin), d=ray_direction) -RayCaster.intersect_p(t, r) -RayCaster.intersect_triangle(r.o, r.d, t.vertices...) +t = Raycore.Triangle(m, 1) +r = Raycore.Ray(o=Point3f(ray_origin), d=ray_direction) +Raycore.intersect_p(t, r) +Raycore.intersect_triangle(r.o, r.d, t.vertices...) # function launch_trace_image_ir!(img, camera, bvh, lights) # backend = KA.get_backend(img) @@ -159,37 +159,37 @@ RayCaster.intersect_triangle(r.o, r.d, t.vertices...) # return img # end -ray = RayCaster.RayDifferentials(RayCaster.Ray(o=Point3f(0.5, 0.5, 1.0), d=Vec3f(0.0, 0.0, -1.0))) +ray = Raycore.RayDifferentials(Raycore.Ray(o=Point3f(0.5, 0.5, 1.0), d=Vec3f(0.0, 0.0, -1.0))) open("li.llvm", "w") do io - code_llvm(io, RayCaster.li, typeof.((RayCaster.UniformSampler(8), 5, ray, scene, 1))) + code_llvm(io, Raycore.li, typeof.((Raycore.UniformSampler(8), 5, ray, scene, 1))) end open("li-wt.jl", "w") do io - code_warntype(io, RayCaster.li, typeof.((RayCaster.UniformSampler(8), 5, ray, scene, 1))) + code_warntype(io, Raycore.li, typeof.((Raycore.UniformSampler(8), 5, ray, scene, 1))) end -camera_sample = RayCaster.get_camera_sample(integrator.sampler, Point2f(512)) -ray, ω = RayCaster.generate_ray_differential(integrator.camera, camera_sample) +camera_sample = Raycore.get_camera_sample(integrator.sampler, Point2f(512)) +ray, ω = Raycore.generate_ray_differential(integrator.camera, camera_sample) -ray = RayCaster.Ray(o=Point3f(0.0, 0.0, 2.0), d=Vec3f(0.0, 0.0, -1.0)) +ray = Raycore.Ray(o=Point3f(0.0, 0.0, 2.0), d=Vec3f(0.0, 0.0, -1.0)) function test(results, bvh, ray) for i in 1:100000 - results[i] = RayCaster.any_hit(bvh, ray, PerfNTuple) + results[i] = Raycore.any_hit(bvh, ray, PerfNTuple) end return results end @profview test(results, bvh, ray) -@btime RayCaster.closest_hit(bvh, ray) -results = Vector{Tuple{Bool, RayCaster.Triangle, Float32, Point3f}}(undef, 100000); +@btime Raycore.closest_hit(bvh, ray) +results = Vector{Tuple{Bool, Raycore.Triangle, Float32, Point3f}}(undef, 100000); @btime test(results, bvh, ray); -@btime RayCaster.any_hit(bvh, ray) +@btime Raycore.any_hit(bvh, ray) -@code_typed RayCaster.traverse_bvh(RayCaster.any_hit_callback, bvh, ray, RayCaster.MemAllocator()) +@code_typed Raycore.traverse_bvh(Raycore.any_hit_callback, bvh, ray, Raycore.MemAllocator()) -sizeof(zeros(RayCaster.MVector{64,Int32})) +sizeof(zeros(Raycore.MVector{64,Int32})) ### # Int32 always @@ -209,7 +209,7 @@ struct PerfNTuple{N,T} data::NTuple{N,T} end -@generated function RayCaster._setindex(r::PerfNTuple{N,T}, idx::IT, value::T) where {N,T, IT <: Integer} +@generated function Raycore._setindex(r::PerfNTuple{N,T}, idx::IT, value::T) where {N,T, IT <: Integer} expr = Expr(:tuple) for i in 1:N idxt = IT(i) @@ -220,7 +220,7 @@ end Base.@propagate_inbounds Base.getindex(r::PerfNTuple, idx::Integer) = r.data[idx] -@generated function RayCaster._allocate(::Type{PerfNTuple}, ::Type{T}, ::Val{N}) where {T,N} +@generated function Raycore._allocate(::Type{PerfNTuple}, ::Type{T}, ::Val{N}) where {T,N} expr = Expr(:tuple) for i in 1:N push!(expr.args, :($(T(0)))) @@ -228,7 +228,7 @@ Base.@propagate_inbounds Base.getindex(r::PerfNTuple, idx::Integer) = r.data[idx return :($(PerfNTuple){$N, $T}($expr)) end -m = RayCaster._allocate(PerfNTuple, Int32, Val(64)) -m2 = RayCaster._setindex(m, 10, Int32(42)) +m = Raycore._allocate(PerfNTuple, Int32, Val(64)) +m2 = Raycore._setindex(m, 10, Int32(42)) -@btime RayCaster.any_hit(bvh, ray, PerfNTuple) +@btime Raycore.any_hit(bvh, ray, PerfNTuple) diff --git a/test/runtests.jl b/test/runtests.jl index 43287b2..c9dd81f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,10 +1,10 @@ using Test using GeometryBasics using LinearAlgebra -using RayCaster +using Raycore using JET -@testset "RayCaster Tests" begin +@testset "Raycore Tests" begin @testset "Intersection" begin include("test_intersection.jl") end diff --git a/test/test_intersection.jl b/test/test_intersection.jl index c81e20d..09a0ef0 100644 --- a/test/test_intersection.jl +++ b/test/test_intersection.jl @@ -1,83 +1,83 @@ @testset "Ray-Bounds intersection" begin - b = RayCaster.Bounds3(Point3f(1), Point3f(2)) - b_neg = RayCaster.Bounds3(Point3f(-2), Point3f(-1)) - r0 = RayCaster.Ray(o = Point3f(0), d = Vec3f(1, 0, 0)) - r1 = RayCaster.Ray(o = Point3f(0), d = Vec3f(1)) - ri = RayCaster.Ray(o = Point3f(1.5), d = Vec3f(1, 1, 0)) + b = Raycore.Bounds3(Point3f(1), Point3f(2)) + b_neg = Raycore.Bounds3(Point3f(-2), Point3f(-1)) + r0 = Raycore.Ray(o = Point3f(0), d = Vec3f(1, 0, 0)) + r1 = Raycore.Ray(o = Point3f(0), d = Vec3f(1)) + ri = Raycore.Ray(o = Point3f(1.5), d = Vec3f(1, 1, 0)) - r, t0, t1 = RayCaster.intersect(b, r1) + r, t0, t1 = Raycore.intersect(b, r1) @test r && t0 ≈ 1f0 && t1 ≈ 2f0 - r, t0, t1 = RayCaster.intersect(b, r0) + r, t0, t1 = Raycore.intersect(b, r0) @test !r && t0 ≈ 0f0 && t1 ≈ 0f0 - r, t0, t1 = RayCaster.intersect(b, ri) + r, t0, t1 = Raycore.intersect(b, ri) @test r && t0 ≈ 0f0 && t1 ≈ 0.5f0 # Test intersection with precomputed direction reciprocal. inv_dir = 1f0 ./ r1.d - dir_is_negative = RayCaster.is_dir_negative(r1.d) - @test RayCaster.intersect_p(b, r1, inv_dir, dir_is_negative) - @test !RayCaster.intersect_p(b_neg, r1, inv_dir, dir_is_negative) + dir_is_negative = Raycore.is_dir_negative(r1.d) + @test Raycore.intersect_p(b, r1, inv_dir, dir_is_negative) + @test !Raycore.intersect_p(b_neg, r1, inv_dir, dir_is_negative) end # Note: Ray-Sphere intersection tests moved to Trace.jl -# RayCaster no longer has Sphere shapes - only low-level triangle intersection +# Raycore no longer has Sphere shapes - only low-level triangle intersection @testset "Test triangle" begin - triangles = RayCaster.TriangleMesh( + triangles = Raycore.TriangleMesh( [Point3f(0, 0, 2), Point3f(1, 0, 2), Point3f(1, 1, 2)], UInt32[1, 2, 3], - [RayCaster.Normal3f(0, 0, -1), RayCaster.Normal3f(0, 0, -1), RayCaster.Normal3f(0, 0, -1)], + [Raycore.Normal3f(0, 0, -1), Raycore.Normal3f(0, 0, -1), Raycore.Normal3f(0, 0, -1)], ) - triangle = RayCaster.Triangle(triangles, 1) - tv = RayCaster.vertices(triangle) + triangle = Raycore.Triangle(triangles, 1) + tv = Raycore.vertices(triangle) a = norm(tv[1] - tv[2])^2 * 0.5f0 - @test RayCaster.area(triangle) ≈ a + @test Raycore.area(triangle) ≈ a - target_wb = RayCaster.Bounds3(Point3f(0, 0, 2), Point3f(1, 1, 2)) + target_wb = Raycore.Bounds3(Point3f(0, 0, 2), Point3f(1, 1, 2)) # In the refactored API, object_bound returns world bounds since transformation is applied during creation - @test RayCaster.object_bound(triangle) ≈ target_wb + @test Raycore.object_bound(triangle) ≈ target_wb # Test ray intersection - API has changed: intersect now returns (Bool, Float32, Point3f) with barycentric coords - ray = RayCaster.Ray(o = Point3f(0, 0, -2), d = Vec3f(0, 0, 1)) - intersects_p = RayCaster.intersect_p(triangle, ray) - intersects, t_hit, bary_coords = RayCaster.intersect(triangle, ray) + ray = Raycore.Ray(o = Point3f(0, 0, -2), d = Vec3f(0, 0, 1)) + intersects_p = Raycore.intersect_p(triangle, ray) + intersects, t_hit, bary_coords = Raycore.intersect(triangle, ray) @test intersects_p == intersects == true @test t_hit ≈ 4f0 - @test RayCaster.apply(ray, t_hit) ≈ Point3f(0, 0, 2) + @test Raycore.apply(ray, t_hit) ≈ Point3f(0, 0, 2) # Barycentric coordinates for vertex 0 (corner hit) @test bary_coords ≈ Point3f(1, 0, 0) # Test ray intersection (different point). - ray = RayCaster.Ray(o = Point3f(0.5, 0.25, 0), d = Vec3f(0, 0, 1)) - intersects_p = RayCaster.intersect_p(triangle, ray) - intersects, t_hit, bary_coords = RayCaster.intersect(triangle, ray) + ray = Raycore.Ray(o = Point3f(0.5, 0.25, 0), d = Vec3f(0, 0, 1)) + intersects_p = Raycore.intersect_p(triangle, ray) + intersects, t_hit, bary_coords = Raycore.intersect(triangle, ray) @test intersects_p == intersects == true @test t_hit ≈ 2f0 - @test RayCaster.apply(ray, t_hit) ≈ Point3f(0.5, 0.25, 2) + @test Raycore.apply(ray, t_hit) ≈ Point3f(0.5, 0.25, 2) end -# BVH tests with spheres removed - refactored RayCaster only supports triangle meshes in BVH +# BVH tests with spheres removed - refactored Raycore only supports triangle meshes in BVH @testset "BVH" begin # Create triangle meshes instead of spheres triangle_meshes = [] for i in 0:1:3 # Use fewer triangles for simpler test - core = RayCaster.translate(Vec3f(i*3, i*3, 0)) - mesh = RayCaster.TriangleMesh( + core = Raycore.translate(Vec3f(i*3, i*3, 0)) + mesh = Raycore.TriangleMesh( core.([Point3f(0, 0, 0), Point3f(1, 0, 0), Point3f(1, 1, 0)]), UInt32[1, 2, 3], - [RayCaster.Normal3f(0, 0, -1), RayCaster.Normal3f(0, 0, -1), RayCaster.Normal3f(0, 0, -1)], + [Raycore.Normal3f(0, 0, -1), Raycore.Normal3f(0, 0, -1), Raycore.Normal3f(0, 0, -1)], ) push!(triangle_meshes, mesh) end - bvh = RayCaster.BVHAccel(triangle_meshes) + bvh = Raycore.BVHAccel(triangle_meshes) # Test basic BVH functionality with triangle meshes - @test !isnothing(RayCaster.world_bound(bvh)) + @test !isnothing(Raycore.world_bound(bvh)) # Simple intersection test - ray = RayCaster.Ray(o = Point3f(0.5, 0.5, -1), d = Vec3f(0, 0, 1)) - intersects, interaction = RayCaster.closest_hit(bvh, ray) + ray = Raycore.Ray(o = Point3f(0.5, 0.5, -1), d = Vec3f(0, 0, 1)) + intersects, interaction = Raycore.closest_hit(bvh, ray) @test intersects end @@ -89,25 +89,25 @@ end positions = [0, 4, 8] vertices = [Point3f(-1, -1, 0), Point3f(1, -1, 0), Point3f(0, 1, 0)] for (i, z) in enumerate(positions) - core = RayCaster.translate(Vec3f(0, 0, z)) + core = Raycore.translate(Vec3f(0, 0, z)) vs = core.(vertices) - mesh = RayCaster.TriangleMesh( + mesh = Raycore.TriangleMesh( vs, UInt32[1, 2, 3], - [RayCaster.Normal3f(0, 0, -1), RayCaster.Normal3f(0, 0, -1), RayCaster.Normal3f(0, 0, -1)], + [Raycore.Normal3f(0, 0, -1), Raycore.Normal3f(0, 0, -1), Raycore.Normal3f(0, 0, -1)], ) push!(triangle_meshes, mesh) end - bvh = RayCaster.BVHAccel(triangle_meshes) + bvh = Raycore.BVHAccel(triangle_meshes) # Test that BVH can be created and has a valid bound - bound = RayCaster.world_bound(bvh) + bound = Raycore.world_bound(bvh) @test !isnothing(bound) # Test intersection with the first triangle - ray = RayCaster.Ray(o = Point3f(0, 0, -2), d = Vec3f(0, 0, 1)) - intersects, triangle = RayCaster.closest_hit(bvh, ray) + ray = Raycore.Ray(o = Point3f(0, 0, -2), d = Vec3f(0, 0, 1)) + intersects, triangle = Raycore.closest_hit(bvh, ray) @test intersects # BVH closest_hit returns Triangle object, not SurfaceInteraction - @test typeof(triangle) == RayCaster.Triangle + @test typeof(triangle) == Raycore.Triangle end diff --git a/test/test_type_stability.jl b/test/test_type_stability.jl index 9c77eb1..c811ed1 100644 --- a/test/test_type_stability.jl +++ b/test/test_type_stability.jl @@ -1,37 +1,37 @@ using LinearAlgebra -using RayCaster.StaticArrays +using Raycore.StaticArrays # ==================== Test Data Generators ==================== # Basic geometric types gen_point3f() = Point3f(1.0f0, 2.0f0, 3.0f0) gen_point2f() = Point2f(0.5f0, 0.5f0) gen_vec3f() = Vec3f(0.0f0, 0.0f0, 1.0f0) -gen_normal3f() = RayCaster.Normal3f(0.0f0, 0.0f0, 1.0f0) +gen_normal3f() = Raycore.Normal3f(0.0f0, 0.0f0, 1.0f0) # Bounds -gen_bounds2() = RayCaster.Bounds2(Point2f(0.0f0), Point2f(1.0f0)) -gen_bounds3() = RayCaster.Bounds3(Point3f(0.0f0), Point3f(1.0f0, 1.0f0, 1.0f0)) +gen_bounds2() = Raycore.Bounds2(Point2f(0.0f0), Point2f(1.0f0)) +gen_bounds3() = Raycore.Bounds3(Point3f(0.0f0), Point3f(1.0f0, 1.0f0, 1.0f0)) # Rays -gen_ray() = RayCaster.Ray(o=Point3f(0.0f0), d=Vec3f(0.0f0, 0.0f0, 1.0f0)) -gen_ray_differentials() = RayCaster.RayDifferentials(o=Point3f(0.0f0), d=Vec3f(0.0f0, 0.0f0, 1.0f0)) +gen_ray() = Raycore.Ray(o=Point3f(0.0f0), d=Vec3f(0.0f0, 0.0f0, 1.0f0)) +gen_ray_differentials() = Raycore.RayDifferentials(o=Point3f(0.0f0), d=Vec3f(0.0f0, 0.0f0, 1.0f0)) # Transformations -gen_transformation() = RayCaster.Transformation() -gen_transformation_translate() = RayCaster.translate(Vec3f(1.0f0, 0.0f0, 0.0f0)) -gen_transformation_rotate() = RayCaster.rotate_x(45.0f0) -gen_transformation_scale() = RayCaster.scale(2.0f0, 2.0f0, 2.0f0) +gen_transformation() = Raycore.Transformation() +gen_transformation_translate() = Raycore.translate(Vec3f(1.0f0, 0.0f0, 0.0f0)) +gen_transformation_rotate() = Raycore.rotate_x(45.0f0) +gen_transformation_scale() = Raycore.scale(2.0f0, 2.0f0, 2.0f0) # Triangle function gen_triangle() v1 = Point3f(0.0f0, 0.0f0, 0.0f0) v2 = Point3f(1.0f0, 0.0f0, 0.0f0) v3 = Point3f(0.0f0, 1.0f0, 0.0f0) - n1 = RayCaster.Normal3f(0.0f0, 0.0f0, 1.0f0) + n1 = Raycore.Normal3f(0.0f0, 0.0f0, 1.0f0) uv1 = Point2f(0.0f0, 0.0f0) uv2 = Point2f(1.0f0, 0.0f0) uv3 = Point2f(0.0f0, 1.0f0) - RayCaster.Triangle( + Raycore.Triangle( SVector(v1, v2, v3), SVector(n1, n1, n1), SVector(Vec3f(NaN), Vec3f(NaN), Vec3f(NaN)), @@ -44,18 +44,18 @@ end function gen_triangle_mesh() vertices = [Point3f(0, 0, 0), Point3f(1, 0, 0), Point3f(0, 1, 0)] indices = UInt32[1, 2, 3] # 1-based indices for Julia - normals = [RayCaster.Normal3f(0, 0, 1), RayCaster.Normal3f(0, 0, 1), RayCaster.Normal3f(0, 0, 1)] - RayCaster.TriangleMesh(vertices, indices, normals) + normals = [Raycore.Normal3f(0, 0, 1), Raycore.Normal3f(0, 0, 1), Raycore.Normal3f(0, 0, 1)] + Raycore.TriangleMesh(vertices, indices, normals) end # BVH function gen_bvh_accel() mesh = Rect3f(Point3f(0), Vec3f(1)) - RayCaster.BVHAccel([mesh], 1) + Raycore.BVHAccel([mesh], 1) end # Quaternion -gen_quaternion() = RayCaster.Quaternion() +gen_quaternion() = Raycore.Quaternion() # ==================== Custom Test Macros ==================== @@ -79,57 +79,57 @@ end @testset "Type Stability: bounds.jl" begin @testset "Bounds2" begin - @test_opt_alloc RayCaster.Bounds2() + @test_opt_alloc Raycore.Bounds2() - @test_opt_alloc RayCaster.Bounds2(gen_point2f()) + @test_opt_alloc Raycore.Bounds2(gen_point2f()) - @test_opt_alloc RayCaster.Bounds2c(gen_point2f(), Point2f(1.0f0, 1.0f0)) + @test_opt_alloc Raycore.Bounds2c(gen_point2f(), Point2f(1.0f0, 1.0f0)) end @testset "Bounds3" begin - @test_opt_alloc RayCaster.Bounds3() + @test_opt_alloc Raycore.Bounds3() - @test_opt_alloc RayCaster.Bounds3(gen_point3f()) + @test_opt_alloc Raycore.Bounds3(gen_point3f()) - @test_opt_alloc RayCaster.Bounds3c(gen_point3f(), Point3f(2.0f0, 2.0f0, 2.0f0)) + @test_opt_alloc Raycore.Bounds3c(gen_point3f(), Point3f(2.0f0, 2.0f0, 2.0f0)) end @testset "Bounds operations" begin b1 = gen_bounds3() - b2 = RayCaster.Bounds3(Point3f(0.5f0), Point3f(1.5f0, 1.5f0, 1.5f0)) + b2 = Raycore.Bounds3(Point3f(0.5f0), Point3f(1.5f0, 1.5f0, 1.5f0)) p = gen_point3f() @test_opt_alloc Base.:(==)(b1, b2) @test_opt_alloc Base.:≈(b1, b2) @test_opt_alloc Base.getindex(b1, 1) - @test_opt_alloc RayCaster.is_valid(b1) - @test_opt_alloc RayCaster.corner(b1, 1) + @test_opt_alloc Raycore.is_valid(b1) + @test_opt_alloc Raycore.corner(b1, 1) @test_opt_alloc Base.union(b1, b2) @test_opt_alloc Base.intersect(b1, b2) - @test_opt_alloc RayCaster.overlaps(b1, b2) - @test_opt_alloc RayCaster.inside(b1, p) - @test_opt_alloc RayCaster.inside_exclusive(b1, p) - @test_opt_alloc RayCaster.expand(b1, 0.1f0) - @test_opt_alloc RayCaster.diagonal(b1) - @test_opt_alloc RayCaster.surface_area(b1) - @test_opt_alloc RayCaster.volume(b1) - @test_opt_alloc RayCaster.maximum_extent(b1) - @test_opt_alloc RayCaster.sides(b1) - @test_opt_alloc RayCaster.inclusive_sides(b1) - @test_opt_alloc RayCaster.bounding_sphere(b1) - @test_opt_alloc RayCaster.offset(b1, p) + @test_opt_alloc Raycore.overlaps(b1, b2) + @test_opt_alloc Raycore.inside(b1, p) + @test_opt_alloc Raycore.inside_exclusive(b1, p) + @test_opt_alloc Raycore.expand(b1, 0.1f0) + @test_opt_alloc Raycore.diagonal(b1) + @test_opt_alloc Raycore.surface_area(b1) + @test_opt_alloc Raycore.volume(b1) + @test_opt_alloc Raycore.maximum_extent(b1) + @test_opt_alloc Raycore.sides(b1) + @test_opt_alloc Raycore.inclusive_sides(b1) + @test_opt_alloc Raycore.bounding_sphere(b1) + @test_opt_alloc Raycore.offset(b1, p) end @testset "Bounds with Ray" begin b = gen_bounds3() r = gen_ray() - @test_opt_alloc RayCaster.intersect(b, r) - @test_opt_alloc RayCaster.is_dir_negative(r.d) + @test_opt_alloc Raycore.intersect(b, r) + @test_opt_alloc Raycore.is_dir_negative(r.d) inv_dir = 1.0f0 ./ r.d - dir_neg = RayCaster.is_dir_negative(r.d) - @test_opt_alloc RayCaster.intersect_p(b, r, inv_dir, dir_neg) + dir_neg = Raycore.is_dir_negative(r.d) + @test_opt_alloc Raycore.intersect_p(b, r, inv_dir, dir_neg) end @testset "Bounds2 iteration" begin @@ -143,22 +143,22 @@ end p1 = gen_point3f() p2 = Point3f(2.0f0, 3.0f0, 4.0f0) - @test_opt_alloc RayCaster.distance(p1, p2) - @test_opt_alloc RayCaster.distance_squared(p1, p2) + @test_opt_alloc Raycore.distance(p1, p2) + @test_opt_alloc Raycore.distance_squared(p1, p2) end @testset "Lerp functions" begin b = gen_bounds3() p = gen_point3f() - @test_opt_alloc RayCaster.lerp(0.0f0, 1.0f0, 0.5f0) - @test_opt_alloc RayCaster.lerp(Point3f(0), Point3f(1), 0.5f0) - @test_opt_alloc RayCaster.lerp(b, Point3f(0.5f0)) + @test_opt_alloc Raycore.lerp(0.0f0, 1.0f0, 0.5f0) + @test_opt_alloc Raycore.lerp(Point3f(0), Point3f(1), 0.5f0) + @test_opt_alloc Raycore.lerp(b, Point3f(0.5f0)) end @testset "Bounds2 area" begin b = gen_bounds2() - @test_opt_alloc RayCaster.area(b) + @test_opt_alloc Raycore.area(b) end end @@ -166,45 +166,45 @@ end @testset "Type Stability: ray.jl" begin @testset "Ray construction" begin - @test_opt_alloc RayCaster.Ray(o=gen_point3f(), d=gen_vec3f()) - @test_opt_alloc RayCaster.Ray(o=gen_point3f(), d=gen_vec3f(), t_max=10.0f0) - @test_opt_alloc RayCaster.Ray(o=gen_point3f(), d=gen_vec3f(), t_max=10.0f0, time=0.5f0) + @test_opt_alloc Raycore.Ray(o=gen_point3f(), d=gen_vec3f()) + @test_opt_alloc Raycore.Ray(o=gen_point3f(), d=gen_vec3f(), t_max=10.0f0) + @test_opt_alloc Raycore.Ray(o=gen_point3f(), d=gen_vec3f(), t_max=10.0f0, time=0.5f0) end @testset "Ray copy constructor" begin r = gen_ray() - @test_opt_alloc RayCaster.Ray(r; o=Point3f(1.0f0)) - @test_opt_alloc RayCaster.Ray(r; d=Vec3f(1.0f0, 0.0f0, 0.0f0)) - @test_opt_alloc RayCaster.Ray(r; t_max=5.0f0) + @test_opt_alloc Raycore.Ray(r; o=Point3f(1.0f0)) + @test_opt_alloc Raycore.Ray(r; d=Vec3f(1.0f0, 0.0f0, 0.0f0)) + @test_opt_alloc Raycore.Ray(r; t_max=5.0f0) end @testset "RayDifferentials construction" begin - @test_opt_alloc RayCaster.RayDifferentials(o=gen_point3f(), d=gen_vec3f()) - @test_opt_alloc RayCaster.RayDifferentials(gen_ray()) + @test_opt_alloc Raycore.RayDifferentials(o=gen_point3f(), d=gen_vec3f()) + @test_opt_alloc Raycore.RayDifferentials(gen_ray()) end @testset "Ray operations" begin r = gen_ray() rd = gen_ray_differentials() - @test_opt_alloc RayCaster.set_direction(r, Vec3f(1.0f0, 0.0f0, 0.0f0)) - @test_opt_alloc RayCaster.set_direction(rd, Vec3f(1.0f0, 0.0f0, 0.0f0)) - @test_opt_alloc RayCaster.check_direction(r) - @test_opt_alloc RayCaster.check_direction(rd) - @test_opt_alloc RayCaster.apply(r, 1.0f0) - @test_opt_alloc RayCaster.increase_hit(r, 0.5f0) - @test_opt_alloc RayCaster.increase_hit(rd, 0.5f0) + @test_opt_alloc Raycore.set_direction(r, Vec3f(1.0f0, 0.0f0, 0.0f0)) + @test_opt_alloc Raycore.set_direction(rd, Vec3f(1.0f0, 0.0f0, 0.0f0)) + @test_opt_alloc Raycore.check_direction(r) + @test_opt_alloc Raycore.check_direction(rd) + @test_opt_alloc Raycore.apply(r, 1.0f0) + @test_opt_alloc Raycore.increase_hit(r, 0.5f0) + @test_opt_alloc Raycore.increase_hit(rd, 0.5f0) end @testset "RayDifferentials operations" begin rd = gen_ray_differentials() - @test_opt_alloc RayCaster.scale_differentials(rd, 0.5f0) + @test_opt_alloc Raycore.scale_differentials(rd, 0.5f0) end @testset "Intersection helpers" begin t = gen_triangle() r = gen_ray() - @test_opt_alloc RayCaster.intersect_p!(t, r) + @test_opt_alloc Raycore.intersect_p!(t, r) end end @@ -212,24 +212,24 @@ end @testset "Type Stability: transformations.jl" begin @testset "Transformation construction" begin - @test_opt_alloc RayCaster.Transformation() - @test_opt_alloc RayCaster.Transformation(Mat4f(I)) + @test_opt_alloc Raycore.Transformation() + @test_opt_alloc Raycore.Transformation(Mat4f(I)) end @testset "Basic transformations" begin - @test_opt_alloc RayCaster.translate(gen_vec3f()) - @test_opt_alloc RayCaster.scale(2.0f0, 2.0f0, 2.0f0) - @test_opt_alloc RayCaster.rotate_x(45.0f0) - @test_opt_alloc RayCaster.rotate_y(45.0f0) - @test_opt_alloc RayCaster.rotate_z(45.0f0) - @test_opt_alloc RayCaster.rotate(45.0f0, Vec3f(0, 0, 1)) + @test_opt_alloc Raycore.translate(gen_vec3f()) + @test_opt_alloc Raycore.scale(2.0f0, 2.0f0, 2.0f0) + @test_opt_alloc Raycore.rotate_x(45.0f0) + @test_opt_alloc Raycore.rotate_y(45.0f0) + @test_opt_alloc Raycore.rotate_z(45.0f0) + @test_opt_alloc Raycore.rotate(45.0f0, Vec3f(0, 0, 1)) end @testset "Transformation operations" begin t1 = gen_transformation_translate() t2 = gen_transformation_rotate() - @test_opt_alloc RayCaster.is_identity(t1) + @test_opt_alloc Raycore.is_identity(t1) @test_opt_alloc Base.transpose(t1) @test_opt_alloc Base.inv(t1) @test_opt_alloc Base.:(==)(t1, t2) @@ -247,14 +247,14 @@ end end @testset "Advanced transformations" begin - @test_opt_alloc RayCaster.look_at(Point3f(0, 0, 5), Point3f(0), Vec3f(0, 1, 0)) - @test_opt_alloc RayCaster.perspective(60.0f0, 0.1f0, 100.0f0) + @test_opt_alloc Raycore.look_at(Point3f(0, 0, 5), Point3f(0), Vec3f(0, 1, 0)) + @test_opt_alloc Raycore.perspective(60.0f0, 0.1f0, 100.0f0) end @testset "Transformation properties" begin t = gen_transformation_scale() - @test_opt_alloc RayCaster.has_scale(t) - @test_opt_alloc RayCaster.swaps_handedness(t) + @test_opt_alloc Raycore.has_scale(t) + @test_opt_alloc Raycore.swaps_handedness(t) end @testset "Transformation with Ray" begin @@ -262,16 +262,16 @@ end r = gen_ray() rd = gen_ray_differentials() - @test_opt_alloc RayCaster.apply(t, r) - @test_opt_alloc RayCaster.apply(t, rd) + @test_opt_alloc Raycore.apply(t, r) + @test_opt_alloc Raycore.apply(t, rd) end @testset "Quaternion" begin - @test_opt_alloc RayCaster.Quaternion() - @test_opt_alloc RayCaster.Quaternion(gen_transformation()) + @test_opt_alloc Raycore.Quaternion() + @test_opt_alloc Raycore.Quaternion(gen_transformation()) q1 = gen_quaternion() - q2 = RayCaster.Quaternion(Vec3f(1, 0, 0), 0.5f0) + q2 = Raycore.Quaternion(Vec3f(1, 0, 0), 0.5f0) @test_opt_alloc Base.:+(q1, q2) @test_opt_alloc Base.:-(q1, q2) @@ -279,8 +279,8 @@ end @test_opt_alloc Base.:*(q1, 2.0f0) @test_opt_alloc LinearAlgebra.dot(q1, q2) @test_opt_alloc LinearAlgebra.normalize(q1) - @test_opt_alloc RayCaster.Transformation(q1) - @test_opt_alloc RayCaster.slerp(q1, q2, 0.5f0) + @test_opt_alloc Raycore.Transformation(q1) + @test_opt_alloc Raycore.slerp(q1, q2, 0.5f0) end end @@ -290,63 +290,63 @@ end @testset "Sampling functions" begin u = gen_point2f() - @test_opt_alloc RayCaster.concentric_sample_disk(u) - @test_opt_alloc RayCaster.cosine_sample_hemisphere(u) - @test_opt_alloc RayCaster.uniform_sample_sphere(u) - @test_opt_alloc RayCaster.uniform_sample_cone(u, 0.5f0) - @test_opt_alloc RayCaster.uniform_sample_cone(u, 0.5f0, Vec3f(1,0,0), Vec3f(0,1,0), Vec3f(0,0,1)) + @test_opt_alloc Raycore.concentric_sample_disk(u) + @test_opt_alloc Raycore.cosine_sample_hemisphere(u) + @test_opt_alloc Raycore.uniform_sample_sphere(u) + @test_opt_alloc Raycore.uniform_sample_cone(u, 0.5f0) + @test_opt_alloc Raycore.uniform_sample_cone(u, 0.5f0, Vec3f(1,0,0), Vec3f(0,1,0), Vec3f(0,0,1)) end @testset "PDF functions" begin - @test_opt_alloc RayCaster.uniform_sphere_pdf() - @test_opt_alloc RayCaster.uniform_cone_pdf(0.5f0) + @test_opt_alloc Raycore.uniform_sphere_pdf() + @test_opt_alloc Raycore.uniform_cone_pdf(0.5f0) end @testset "Shading coordinate system" begin w = gen_vec3f() - @test_opt_alloc RayCaster.cos_θ(w) - @test_opt_alloc RayCaster.sin_θ2(w) - @test_opt_alloc RayCaster.sin_θ(w) - @test_opt_alloc RayCaster.tan_θ(w) - @test_opt_alloc RayCaster.cos_ϕ(w) - @test_opt_alloc RayCaster.sin_ϕ(w) + @test_opt_alloc Raycore.cos_θ(w) + @test_opt_alloc Raycore.sin_θ2(w) + @test_opt_alloc Raycore.sin_θ(w) + @test_opt_alloc Raycore.tan_θ(w) + @test_opt_alloc Raycore.cos_ϕ(w) + @test_opt_alloc Raycore.sin_ϕ(w) end @testset "Vector operations" begin wo = gen_vec3f() n = Vec3f(0, 1, 0) - @test_opt_alloc RayCaster.reflect(wo, n) - @test_opt_alloc RayCaster.face_forward(n, wo) + @test_opt_alloc Raycore.reflect(wo, n) + @test_opt_alloc Raycore.face_forward(n, wo) end @testset "Coordinate system" begin v = gen_vec3f() - @test_opt_alloc RayCaster.coordinate_system(v) + @test_opt_alloc Raycore.coordinate_system(v) end @testset "Spherical functions" begin - @test_opt_alloc RayCaster.spherical_direction(0.5f0, 0.5f0, 1.0f0) - @test_opt_alloc RayCaster.spherical_direction(0.5f0, 0.5f0, 1.0f0, Vec3f(1,0,0), Vec3f(0,1,0), Vec3f(0,0,1)) + @test_opt_alloc Raycore.spherical_direction(0.5f0, 0.5f0, 1.0f0) + @test_opt_alloc Raycore.spherical_direction(0.5f0, 0.5f0, 1.0f0, Vec3f(1,0,0), Vec3f(0,1,0), Vec3f(0,0,1)) v = gen_vec3f() - @test_opt_alloc RayCaster.spherical_θ(v) - @test_opt_alloc RayCaster.spherical_ϕ(v) + @test_opt_alloc Raycore.spherical_θ(v) + @test_opt_alloc Raycore.spherical_ϕ(v) end @testset "Helper functions" begin v = gen_vec3f() - @test_opt_alloc RayCaster.get_orthogonal_basis(v) + @test_opt_alloc Raycore.get_orthogonal_basis(v) t = gen_triangle() - @test_opt_alloc RayCaster.random_triangle_point(t) + @test_opt_alloc Raycore.random_triangle_point(t) end @testset "sum_mul" begin a = Point3f(0.2f0, 0.3f0, 0.5f0) - b = RayCaster.StaticArrays.SVector(Point3f(0,0,0), Point3f(1,0,0), Point3f(0,1,0)) - @test_opt_alloc RayCaster.sum_mul(a, b) + b = Raycore.StaticArrays.SVector(Point3f(0,0,0), Point3f(1,0,0), Point3f(0,1,0)) + @test_opt_alloc Raycore.sum_mul(a, b) end end @@ -354,36 +354,36 @@ end @testset "TriangleMesh construction" begin vertices = [Point3f(0, 0, 0), Point3f(1, 0, 0), Point3f(0, 1, 0)] indices = UInt32[0, 1, 2] - normals = [RayCaster.Normal3f(0, 0, 1), RayCaster.Normal3f(0, 0, 1), RayCaster.Normal3f(0, 0, 1)] + normals = [Raycore.Normal3f(0, 0, 1), Raycore.Normal3f(0, 0, 1), Raycore.Normal3f(0, 0, 1)] - @test_opt RayCaster.TriangleMesh(vertices, indices, normals) - @test_opt RayCaster.TriangleMesh(vertices, indices) + @test_opt Raycore.TriangleMesh(vertices, indices, normals) + @test_opt Raycore.TriangleMesh(vertices, indices) end @testset "Triangle construction" begin mesh = gen_triangle_mesh() - @test_opt_alloc RayCaster.Triangle(mesh, 1, UInt32(1)) + @test_opt_alloc Raycore.Triangle(mesh, 1, UInt32(1)) end @testset "Triangle operations" begin t = gen_triangle() - @test_opt_alloc RayCaster.vertices(t) - @test_opt_alloc RayCaster.normals(t) - @test_opt_alloc RayCaster.tangents(t) - @test_opt_alloc RayCaster.uvs(t) - @test_opt_alloc RayCaster.area(t) - @test_opt_alloc RayCaster.object_bound(t) - @test_opt_alloc RayCaster.world_bound(t) + @test_opt_alloc Raycore.vertices(t) + @test_opt_alloc Raycore.normals(t) + @test_opt_alloc Raycore.tangents(t) + @test_opt_alloc Raycore.uvs(t) + @test_opt_alloc Raycore.area(t) + @test_opt_alloc Raycore.object_bound(t) + @test_opt_alloc Raycore.world_bound(t) end @testset "Triangle intersection" begin t = gen_triangle() r = gen_ray() - @test_opt_alloc RayCaster.intersect(t, r) - @test_opt_alloc RayCaster.intersect_p(t, r) - @test_opt_alloc RayCaster.intersect_triangle(t.vertices, r) + @test_opt_alloc Raycore.intersect(t, r) + @test_opt_alloc Raycore.intersect_p(t, r) + @test_opt_alloc Raycore.intersect_triangle(t.vertices, r) end @testset "Triangle helper functions" begin @@ -391,18 +391,18 @@ end r = gen_ray() # Test _to_ray_coordinate_space - @test_opt_alloc RayCaster._to_ray_coordinate_space(t.vertices, r) + @test_opt_alloc Raycore._to_ray_coordinate_space(t.vertices, r) # Test partial_derivatives - @test_opt_alloc RayCaster.partial_derivatives(t, t.vertices, t.uv) + @test_opt_alloc Raycore.partial_derivatives(t, t.vertices, t.uv) # Test normal_derivatives - @test_opt_alloc RayCaster.normal_derivatives(t, t.uv) + @test_opt_alloc Raycore.normal_derivatives(t, t.uv) end @testset "Triangle utilities" begin t = gen_triangle() - @test_opt_alloc RayCaster.is_degenerate(t.vertices) + @test_opt_alloc Raycore.is_degenerate(t.vertices) end end @@ -411,34 +411,34 @@ end @testset "Type Stability: bvh.jl" begin @testset "BVHPrimitiveInfo" begin b = gen_bounds3() - @test_opt_alloc RayCaster.BVHPrimitiveInfo(UInt32(1), b) + @test_opt_alloc Raycore.BVHPrimitiveInfo(UInt32(1), b) end @testset "BVHNode construction" begin b = gen_bounds3() - @test_opt_alloc RayCaster.BVHNode(UInt32(0), UInt32(1), b) + @test_opt_alloc Raycore.BVHNode(UInt32(0), UInt32(1), b) end @testset "LinearBVH construction" begin b = gen_bounds3() - @test_opt_alloc RayCaster.LinearBVHLeaf(b, UInt32(0), UInt32(1)) - @test_opt_alloc RayCaster.LinearBVHInterior(b, UInt32(1), UInt8(0)) + @test_opt_alloc Raycore.LinearBVHLeaf(b, UInt32(0), UInt32(1)) + @test_opt_alloc Raycore.LinearBVHInterior(b, UInt32(1), UInt8(0)) end @testset "BVH operations" begin bvh = gen_bvh_accel() r = gen_ray() - @test_opt RayCaster.world_bound(bvh) - @test_opt RayCaster.closest_hit(bvh, r) - @test_opt RayCaster.any_hit(bvh, r) + @test_opt Raycore.world_bound(bvh) + @test_opt Raycore.closest_hit(bvh, r) + @test_opt Raycore.any_hit(bvh, r) end @testset "Ray grid generation" begin bvh = gen_bvh_accel() direction = Vec3f(0, 0, 1) # generate_ray_grid allocates - needs optimization - @test_opt RayCaster.generate_ray_grid(bvh, direction, 10) + @test_opt Raycore.generate_ray_grid(bvh, direction, 10) end end @@ -446,6 +446,6 @@ end @testset "Type Stability: kernels.jl" begin @testset "RayHit construction" begin - @test_opt_alloc RayCaster.RayHit(true, gen_point3f(), UInt32(1)) + @test_opt_alloc Raycore.RayHit(true, gen_point3f(), UInt32(1)) end end From 5df82f61154c78822bd393b253d833e4c2ea0317 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Tue, 28 Oct 2025 20:24:12 +0100 Subject: [PATCH 15/20] add ray tracing tutorial --- docs/make.jl | 4 + docs/src/.raytracing_tutorial-bbook/meta.toml | 1 + ...cing_tutorial_content-2025-10-28_201849.md | 603 ++++++++++++++++++ .../meta.toml | 1 + docs/src/bvh_hit_tests.md | 1 + docs/src/raytracing_tutorial.md | 14 + docs/src/raytracing_tutorial_content.md | 600 +++++++++++++++++ 7 files changed, 1224 insertions(+) create mode 100644 docs/src/.raytracing_tutorial-bbook/meta.toml create mode 100644 docs/src/.raytracing_tutorial_content-bbook/.versions/raytracing_tutorial_content-2025-10-28_201849.md create mode 100644 docs/src/.raytracing_tutorial_content-bbook/meta.toml create mode 100644 docs/src/raytracing_tutorial.md create mode 100644 docs/src/raytracing_tutorial_content.md diff --git a/docs/make.jl b/docs/make.jl index 7b73850..97e2ff0 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -15,6 +15,10 @@ makedocs(; authors = "Anton Smirnov, Simon Danisch and contributors", pages = [ "Home" => "index.md", + "Tutorials" => [ + "Ray Tracing Tutorial" => "raytracing_tutorial.md", + "BVH Hit Tests" => "bvh_hit_tests.md", + ], ], ) diff --git a/docs/src/.raytracing_tutorial-bbook/meta.toml b/docs/src/.raytracing_tutorial-bbook/meta.toml new file mode 100644 index 0000000..9f7a875 --- /dev/null +++ b/docs/src/.raytracing_tutorial-bbook/meta.toml @@ -0,0 +1 @@ +version = "0.1.0" diff --git a/docs/src/.raytracing_tutorial_content-bbook/.versions/raytracing_tutorial_content-2025-10-28_201849.md b/docs/src/.raytracing_tutorial_content-bbook/.versions/raytracing_tutorial_content-2025-10-28_201849.md new file mode 100644 index 0000000..a3637ce --- /dev/null +++ b/docs/src/.raytracing_tutorial_content-bbook/.versions/raytracing_tutorial_content-2025-10-28_201849.md @@ -0,0 +1,603 @@ +# Ray Tracing with Raycore: Building a Real Ray Tracer + +In this tutorial, we'll build a simple but complete ray tracer from scratch using Raycore. We'll start with the absolute basics and progressively add features until we have a ray tracer that produces beautiful images with shadows and materials. + +By the end, you'll have a working ray tracer that can render complex scenes! + +## Setup + +```julia (editor=true, logging=false, output=true) +using Raycore, GeometryBasics, LinearAlgebra +using Colors, ImageShow +using Makie # For loading assets +using BenchmarkTools +``` +**Ready to go!** We have: + + * `Raycore` for fast ray-triangle intersections + * `GeometryBasics` for geometry primitives + * `Colors` and `ImageShow` for displaying rendered images + +## Part 1: Our Scene - A Playful Cat + +Let's create a fun scene that we'll use throughout this tutorial. We'll load a cat model and place it in a simple room. + +```julia (editor=true, logging=false, output=true) +# Load the cat model and rotate it to face the camera +cat_mesh = Makie.loadasset("cat.obj") +# Rotate 150 degrees around Y axis so cat faces camera at an angle +angle = deg2rad(150f0) +rotation = Makie.Quaternionf(0, sin(angle/2), 0, cos(angle/2)) +rotated_coords = [rotation * Point3f(v) for v in coordinates(cat_mesh)] + +# Get bounding box and translate cat to sit on the floor +cat_bbox = Rect3f(rotated_coords) +floor_y = -1.5f0 +cat_offset = Vec3f(0, floor_y - cat_bbox.origin[2], 0) # Translate so bottom sits on floor + +cat_mesh = GeometryBasics.normal_mesh( + [v + cat_offset for v in rotated_coords], + faces(cat_mesh) +) + +# Create a simple room: floor, back wall, and side wall +floor = normal_mesh(Rect3f(Vec3f(-5, -1.5, -2), Vec3f(10, 0.01, 10))) +back_wall = normal_mesh(Rect3f(Vec3f(-5, -1.5, 8), Vec3f(10, 5, 0.01))) +left_wall = normal_mesh(Rect3f(Vec3f(-5, -1.5, -2), Vec3f(0.01, 5, 10))) + +# Add a couple of spheres for visual interest (also on the floor) +sphere1 = Tesselation(Sphere(Point3f(-2, -1.5 + 0.8, 2), 0.8f0), 64) +sphere2 = Tesselation(Sphere(Point3f(2, -1.5 + 0.6, 1), 0.6f0), 64) + +# Build our BVH acceleration structure +scene_geometry = [cat_mesh, floor, back_wall, left_wall, sphere1, sphere2] +bvh = Raycore.BVHAccel(scene_geometry) +``` +**Scene created!** + + * Cat model with triangulated geometry + * Room geometry: 3 walls + * 2 decorative spheres + * BVH built for fast ray traversal + +## Part 2: The Simplest Ray Tracer - Binary Hit Detection + +Let's start super simple: for each pixel, we shoot a ray and color it based on whether we hit something or not. + +```julia (editor=true, logging=false, output=true) +# Trace helper - runs a callback for each pixel +function trace(f, bvh; width=700, height=300, camera_pos=Point3f(0, -0.9, -2.5), fov=45.0f0, + sky_color=RGB{Float32}(0.5f0, 0.7f0, 1.0f0)) + img = Matrix{RGB{Float32}}(undef, height, width) + + # Precompute camera parameters + aspect = Float32(width / height) + focal_length = 1.0f0 / tan(deg2rad(fov / 2)) + + for y in 1:height, x in 1:width + # Generate camera ray + ndc_x = (2.0f0 * x / width - 1.0f0) * aspect + ndc_y = 1.0f0 - 2.0f0 * y / height + direction = normalize(Vec3f(ndc_x, ndc_y, focal_length)) + ray = Raycore.Ray(o=camera_pos, d=direction) + + # Ray-scene intersection + hit_found, triangle, distance, bary_coords = Raycore.closest_hit(bvh, ray) + + # Let the callback decide the color (pass sky_color for misses) + img[y, x] = hit_found ? f(triangle, distance, bary_coords, ray) : sky_color + end + + return img +end + +# Binary kernel - white if hit +binary_kernel(triangle, distance, bary_coords, ray) = RGB(1.0f0, 1.0f0, 1.0f0) + +trace(binary_kernel, bvh, sky_color=RGB(0.0f0, 0.0f0, 0.0f0)) +``` +**Our first render!** Pure silhouette - you can see the cat and spheres. + +## Part 3: Adding Depth - Distance-Based Shading + +Let's make it more interesting by coloring based on distance (depth map). + +```julia (editor=true, logging=false, output=true) +function depth_kernel(triangle, distance, bary_coords, ray) + # Map distance to grayscale (closer = brighter) + normalized_depth = clamp(1.0f0 - (distance - 2.0f0) / 8.0f0, 0.0f0, 1.0f0) + RGB(normalized_depth, normalized_depth, normalized_depth) +end + +trace(depth_kernel, bvh) +``` +**Depth perception!** Now we can see the 3D structure - closer objects are brighter. + +## Part 4: Surface Normals - The Foundation of Lighting + +To do proper lighting, we need surface normals. Let's compute and visualize them. + +```julia (editor=true, logging=false, output=true) +# Helper to interpolate normals using barycentric coordinates +function compute_normal(triangle, bary_coords) + n1, n2, n3 = triangle.normals + u, v, w = bary_coords + normalize(Vec3f(u * n1 + v * n2 + w * n3)) +end + +function normal_kernel(triangle, distance, bary_coords, ray) + normal = compute_normal(triangle, bary_coords) + # Map normal components [-1,1] to color [0,1] + RGB((normal .+ 1.0f0) ./ 2.0f0...) +end + +trace(normal_kernel, bvh) +``` +**Surface normals visualized!** Each color channel represents a normal component: + + * Red = X direction + * Green = Y direction + * Blue = Z direction + +## Part 5: Basic Lighting - Diffuse Shading + +Now we can add a light source and compute simple diffuse (Lambertian) shading! + +```julia (editor=true, logging=false, output=true) +light_pos = Point3f(3, 4, -2) +light_intensity = 50.0f0 + +function diffuse_kernel(triangle, distance, bary_coords, ray) + # Compute hit point and normal + hit_point = ray.o + ray.d * distance + normal = compute_normal(triangle, bary_coords) + + # Light direction and distance + light_dir = light_pos - hit_point + light_distance = norm(light_dir) + light_dir = normalize(light_dir) + + # Diffuse shading (Lambertian) + diffuse = max(0.0f0, dot(normal, light_dir)) + + # Light attenuation (inverse square law) + attenuation = light_intensity / (light_distance * light_distance) + color = diffuse * attenuation + + RGB(color, color, color) +end + +trace(diffuse_kernel, bvh) +``` +**Let there be light!** Our scene now has proper shading based on surface orientation. + +## Part 6: Adding Shadows - Shadow Rays + +Time to add realism with shadows using Raycore's `any_hit` for fast occlusion testing. + +```julia (editor=true, logging=false, output=true) +ambient = 0.1f0 # Ambient lighting to prevent pure black shadows + +function shadow_kernel(triangle, distance, bary_coords, ray) + hit_point = ray.o + ray.d * distance + normal = compute_normal(triangle, bary_coords) + + # Light direction + light_dir = light_pos - hit_point + light_distance = norm(light_dir) + light_dir = normalize(light_dir) + + # Diffuse shading + diffuse = max(0.0f0, dot(normal, light_dir)) + + # Shadow ray - offset slightly to avoid self-intersection + shadow_ray_origin = hit_point + normal * 0.001f0 + shadow_ray = Raycore.Ray(o=shadow_ray_origin, d=light_dir) + + # Check if path to light is blocked + shadow_hit, _, shadow_dist, _ = Raycore.any_hit(bvh, shadow_ray) + in_shadow = shadow_hit && shadow_dist < light_distance + + # Final color + color = if in_shadow + ambient # Only ambient in shadow + else + attenuation = light_intensity / (light_distance * light_distance) + ambient + diffuse * attenuation + end + + RGB(color, color, color) +end + +trace(shadow_kernel, bvh) +``` +**Shadows!** Notice how objects cast shadows on each other, adding depth and realism. + +## Part 7: Multiple Lights + +Let's add multiple lights to make the scene more interesting! We'll define a RenderContext to hold lights and materials: + +```julia (editor=true, logging=false, output=true) +# Define a simple point light structure +struct PointLight + position::Point3f + intensity::Float32 + color::RGB{Float32} +end + +# Material structure (for later use) +struct Material + base_color::RGB{Float32} + metallic::Float32 + roughness::Float32 +end + +# Render context holds all scene data +struct RenderContext + bvh::Raycore.BVHAccel + lights::Vector{PointLight} + materials::Vector{Material} + ambient::Float32 +end + +# Create multiple lights +lights = [ + PointLight(Point3f(3, 4, -2), 50.0f0, RGB(1.0f0, 0.9f0, 0.8f0)), # Warm main light + PointLight(Point3f(-3, 2, 0), 20.0f0, RGB(0.7f0, 0.8f0, 1.0f0)), # Cool fill light + PointLight(Point3f(0, 5, 5), 15.0f0, RGB(1.0f0, 1.0f0, 1.0f0)) # White back light +] + +# Materials (will use these in Part 8) +materials = [ + Material(RGB(0.8f0, 0.6f0, 0.4f0), 0.0f0, 0.8f0), # 1: Cat + Material(RGB(0.3f0, 0.5f0, 0.3f0), 0.0f0, 0.9f0), # 2: Floor + Material(RGB(0.7f0, 0.7f0, 0.8f0), 0.0f0, 0.8f0), # 3: Back wall + Material(RGB(0.8f0, 0.7f0, 0.7f0), 0.0f0, 0.8f0), # 4: Left wall + Material(RGB(0.95f0, 0.64f0, 0.54f0), 1.0f0, 0.1f0), # 5: Sphere 1 - metallic + Material(RGB(0.8f0, 0.8f0, 0.9f0), 1.0f0, 0.0f0) # 6: Sphere 2 - mirror +] + +# Create render context +ctx = RenderContext(bvh, lights, materials, 0.1f0) +``` +Now we need a new trace function that works with RenderContext: + +```julia (editor=true, logging=false, output=true) +# Trace with RenderContext +function trace_ctx(f, ctx::RenderContext; width=700, height=300,camera_pos=Point3f(0, -0.9, -2.5), fov=45.0f0, + sky_color=RGB{Float32}(0.5f0, 0.7f0, 1.0f0)) + img = Matrix{RGB{Float32}}(undef, height, width) + + aspect = Float32(width / height) + focal_length = 1.0f0 / tan(deg2rad(fov / 2)) + + for y in 1:height, x in 1:width + ndc_x = (2.0f0 * x / width - 1.0f0) * aspect + ndc_y = 1.0f0 - 2.0f0 * y / height + direction = normalize(Vec3f(ndc_x, ndc_y, focal_length)) + ray = Raycore.Ray(o=camera_pos, d=direction) + + hit_found, triangle, distance, bary_coords = Raycore.closest_hit(ctx.bvh, ray) + img[y, x] = hit_found ? f(ctx, triangle, distance, bary_coords, ray) : sky_color + end + + return img +end + +function multi_light_kernel(ctx, triangle, distance, bary_coords, ray) + hit_point = ray.o + ray.d * distance + normal = compute_normal(triangle, bary_coords) + + # Start with ambient (grayscale) + total_color = Vec3f(ctx.ambient, ctx.ambient, ctx.ambient) + + # Accumulate contribution from each light + for light in ctx.lights + light_vec = light.position - hit_point + light_distance = norm(light_vec) + light_dir = light_vec / light_distance + + diffuse = max(0.0f0, dot(normal, light_dir)) + + shadow_ray = Raycore.Ray(o=hit_point + normal * 0.001f0, d=light_dir) + shadow_hit, _, shadow_dist, _ = Raycore.any_hit(ctx.bvh, shadow_ray) + in_shadow = shadow_hit && shadow_dist < light_distance + + if !in_shadow + attenuation = light.intensity / (light_distance * light_distance) + light_col = Vec3f(light.color.r, light.color.g, light.color.b) + total_color += light_col * (diffuse * attenuation) + end + end + + RGB{Float32}(total_color...) +end + +trace_ctx(multi_light_kernel, ctx) +``` +**Multiple lights!** The scene now has three different colored lights creating a more dynamic lighting environment. + +## Part 8: Colored Materials with Multiple Lights + +Now let's combine materials with our multiple lights! + +```julia (editor=true, logging=false, output=true) +function material_multi_light_kernel(ctx, triangle, distance, bary_coords, ray) + hit_point = ray.o + ray.d * distance + normal = compute_normal(triangle, bary_coords) + + # Get material from context + mat = ctx.materials[triangle.material_idx] + base_color = Vec3f(mat.base_color.r, mat.base_color.g, mat.base_color.b) + + # Start with ambient + total_color = base_color * ctx.ambient + + # Accumulate contribution from each light + for light in ctx.lights + light_vec = light.position - hit_point + light_distance = norm(light_vec) + light_dir = light_vec / light_distance + + diffuse = max(0.0f0, dot(normal, light_dir)) + + # Shadow test + shadow_ray = Raycore.Ray(o=hit_point + normal * 0.001f0, d=light_dir) + shadow_hit, _, shadow_dist, _ = Raycore.any_hit(ctx.bvh, shadow_ray) + in_shadow = shadow_hit && shadow_dist < light_distance + + if !in_shadow + attenuation = light.intensity / (light_distance * light_distance) + light_col = Vec3f(light.color.r, light.color.g, light.color.b) + total_color += base_color .* light_col * (diffuse * attenuation) + end + end + + RGB{Float32}(total_color...) +end + +trace_ctx(material_multi_light_kernel, ctx) +``` +**Colored materials!** + + * Orange/tan cat + * Green floor + * Light blue back wall + * Pink side wall + * Red and blue spheres + +## Part 9: Reflective Materials - Mirrors and Metals + +The materials we defined in Part 7 already have metallic and roughness properties. Let's use them for reflections! + +```julia (editor=true, logging=false, output=true) +# Helper: compute direct lighting with multiple lights +function compute_multi_light(ctx, point, normal, mat) + base_color = Vec3f(mat.base_color.r, mat.base_color.g, mat.base_color.b) + + # Start with ambient + total_color = base_color * ctx.ambient + + for light in ctx.lights + light_vec = light.position - point + light_distance = norm(light_vec) + light_dir = light_vec / light_distance + + diffuse = max(0.0f0, dot(normal, light_dir)) + + # Shadow test + shadow_ray = Raycore.Ray(o=point + normal * 0.001f0, d=light_dir) + shadow_hit, _, shadow_dist, _ = Raycore.any_hit(ctx.bvh, shadow_ray) + in_shadow = shadow_hit && shadow_dist < light_distance + + if !in_shadow + attenuation = light.intensity / (light_distance * light_distance) + light_col = Vec3f(light.color.r, light.color.g, light.color.b) + total_color += base_color .* light_col * (diffuse * attenuation) + end + end + + return RGB{Float32}(total_color...) +end + +function reflective_kernel(ctx, triangle, distance, bary_coords, ray, sky_color) + hit_point = ray.o + ray.d * distance + normal = compute_normal(triangle, bary_coords) + mat = ctx.materials[triangle.material_idx] + + # Compute direct lighting (diffuse component) + direct_color = compute_multi_light(ctx, hit_point, normal, mat) + + # Add reflection for metallic materials + if mat.metallic > 0.0f0 + # Compute reflection direction: reflect outgoing direction about normal + # Note: ray.d points toward surface, but reflect() expects outgoing direction + wo = -ray.d # outgoing direction (away from surface) + reflect_dir = Raycore.reflect(wo, normal) + + # Add roughness by perturbing reflection direction + if mat.roughness > 0.0f0 + # Simple roughness: add random offset in tangent space + random_offset = (rand(Vec3f) .* 2.0f0 .- 1.0f0) * mat.roughness + reflect_dir = normalize(reflect_dir + random_offset) + end + + # Cast reflection ray (offset to avoid self-intersection) + reflect_ray = Raycore.Ray(o=hit_point + normal * 0.001f0, d=reflect_dir) + refl_hit, refl_tri, refl_dist, refl_bary = Raycore.closest_hit(ctx.bvh, reflect_ray) + + # Get reflection color + reflection_color = if refl_hit + refl_point = reflect_ray.o + reflect_ray.d * refl_dist + refl_normal = compute_normal(refl_tri, refl_bary) + refl_mat = ctx.materials[refl_tri.material_idx] + + # Compute lighting for reflected surface + compute_multi_light(ctx, refl_point, refl_normal, refl_mat) + else + sky_color + end + + # Blend between diffuse and reflection based on metallic parameter + return direct_color * (1.0f0 - mat.metallic) + reflection_color * mat.metallic + else + # Pure diffuse material + return direct_color + end +end + +trace_ctx(ctx) do ctx, triangle, distance, bary_coords, ray + reflective_kernel(ctx, triangle, distance, bary_coords, ray, RGB(0.5f0, 0.7f0, 1.0f0)) +end +``` +**Reflective materials!** The spheres now have metallic properties: + + * One smooth copper-colored metal with slight roughness + * One perfect mirror reflecting the scene + +Notice how reflections capture both the scene geometry and lighting! + +## Part 10: Multi-threading for Performance + +Let's add multi-threading to make our ray tracer much faster! + +```julia (editor=true, logging=false, output=true) + +``` +```julia (editor=true, logging=false, output=true) +using BenchmarkTools +function trace_ctx_threaded(f, ctx::RenderContext; width=400, height=300, camera_pos=Point3f(0, 1, -2.5), fov=45.0f0, + sky_color=RGB{Float32}(0.5f0, 0.7f0, 1.0f0)) + img = Matrix{RGB{Float32}}(undef, height, width) + + aspect = Float32(width / height) + focal_length = 1.0f0 / tan(deg2rad(fov / 2)) + + Threads.@threads for y in 1:height + for x in 1:width + ndc_x = (2.0f0 * x / width - 1.0f0) * aspect + ndc_y = 1.0f0 - 2.0f0 * y / height + direction = normalize(Vec3f(ndc_x, ndc_y, focal_length)) + ray = Raycore.Ray(o=camera_pos, d=direction) + + hit_found, triangle, distance, bary_coords = Raycore.closest_hit(ctx.bvh, ray) + img[y, x] = hit_found ? f(ctx, triangle, distance, bary_coords, ray) : sky_color + end + end + + return img +end + +# Benchmark single-threaded vs multi-threaded +b1 = @belapsed trace_ctx(material_multi_light_kernel, ctx, width=800, height=600); + +b2 = @belapsed trace_ctx_threaded(material_multi_light_kernel, ctx, width=800, height=600); +md""" +Threads: $(Threads.nthreads()) + +Single: $(b1) + +Multi: $(b2) +""" +``` +**Performance boost with threading!** The speedup should be close to the number of CPU cores. + +Notice how we can reuse the same kernel function with both `trace_ctx()` and `trace_ctx_threaded()` - this is great for composability! + +## Part 11: Multi-Sampling for Anti-Aliasing + +Let's add multiple samples per pixel with jittered camera rays for smooth anti-aliasing: + +```julia (editor=true, logging=false, output=true) +function trace_ctx_sampled(f, ctx::RenderContext; + width=700, height=300, + camera_pos=Point3f(0, -0.9, -2.5), fov=45.0f0, + sky_color=RGB{Float32}(0.5f0, 0.7f0, 1.0f0), + samples=4) + img = Matrix{RGB{Float32}}(undef, height, width) + + aspect = Float32(width / height) + focal_length = 1.0f0 / tan(deg2rad(fov / 2)) + pixel_size = 1.0f0 / width + + Threads.@threads for y in 1:height + for x in 1:width + # Accumulate multiple samples per pixel using Vec3f for math + color_sum = Vec3f(0.0f0, 0.0f0, 0.0f0) + + for _ in 1:samples + jitter_x = (rand(Float32) - 0.5f0) * pixel_size + jitter_y = (rand(Float32) - 0.5f0) * pixel_size + + ndc_x = (2.0f0 * (x + jitter_x) / width - 1.0f0) * aspect + ndc_y = 1.0f0 - 2.0f0 * (y + jitter_y) / height + direction = normalize(Vec3f(ndc_x, ndc_y, focal_length)) + ray = Raycore.Ray(o=camera_pos, d=direction) + + hit_found, triangle, distance, bary_coords = Raycore.closest_hit(ctx.bvh, ray) + + color = if hit_found + result = f(ctx, triangle, distance, bary_coords, ray) + Vec3f(result.r, result.g, result.b) + else + Vec3f(sky_color.r, sky_color.g, sky_color.b) + end + + color_sum += color + end + + # Average samples and convert back to RGB + avg = color_sum / samples + img[y, x] = RGB{Float32}(avg...) + end + end + + return img +end + +# Render with 16 samples per pixel for smooth anti-aliasing +@time trace_ctx_sampled(ctx, samples=64) do ctx, triangle, distance, bary_coords, ray + reflective_kernel(ctx, triangle, distance, bary_coords, ray, RGB(0.5f0, 0.7f0, 1.0f0)) +end +``` +**Anti-aliased render!** 32 samples per pixel with jittered camera rays eliminate jagged edges. + +## Summary - What We Built + +We created a complete ray tracer that includes: + +### Features Implemented + +1. **Camera system** - Perspective projection with configurable FOV +2. **Ray-scene intersection** - Using Raycore's BVH for fast traversal +3. **Surface normals** - Smooth shading from vertex normals +4. **Diffuse lighting** - Lambertian shading with distance attenuation +5. **Hard shadows** - Using `any_hit` for efficient occlusion testing +6. **Simple materials** - Per-object color assignment +7. **Multi-threading** - Parallel rendering across CPU cores +8. **Callback-based API** - Flexible `trace()` function for experimentation + +### Next Steps + +To make this into a full path tracer (like `Trace`), you would add: + + * **Recursive ray tracing** - Reflections and refractions + * **Multiple light sources** - Area lights, environment lighting + * **Advanced materials** - Specular, glossy, transparent + * **Sampling** - Multiple samples per pixel for anti-aliasing + * **Better normal interpolation** - Proper barycentric interpolation + * **GPU support** - Using KernelAbstractions.jl + +The `Trace` package implements all of these features and more! + +### Key Raycore Functions Used + + * `Raycore.BVHAccel(meshes)` - Build acceleration structure + * `Raycore.Ray(o=origin, d=direction)` - Create ray + * `Raycore.closest_hit(bvh, ray)` - Find nearest intersection + * `Raycore.any_hit(bvh, ray)` - Test for any intersection (fast!) + * `Raycore.vertices(triangle)` - Get triangle vertex positions + * `Raycore.normals(triangle)` - Get triangle vertex normals + +Happy ray tracing! + diff --git a/docs/src/.raytracing_tutorial_content-bbook/meta.toml b/docs/src/.raytracing_tutorial_content-bbook/meta.toml new file mode 100644 index 0000000..9f7a875 --- /dev/null +++ b/docs/src/.raytracing_tutorial_content-bbook/meta.toml @@ -0,0 +1 @@ +version = "0.1.0" diff --git a/docs/src/bvh_hit_tests.md b/docs/src/bvh_hit_tests.md index 4c887e0..b40b287 100644 --- a/docs/src/bvh_hit_tests.md +++ b/docs/src/bvh_hit_tests.md @@ -221,3 +221,4 @@ This document demonstrated: 6. `any_hit` is typically faster than `closest_hit` due to early termination All tests passed! ✓ + diff --git a/docs/src/raytracing_tutorial.md b/docs/src/raytracing_tutorial.md new file mode 100644 index 0000000..885caea --- /dev/null +++ b/docs/src/raytracing_tutorial.md @@ -0,0 +1,14 @@ +# Ray Tracing with Raycore + +```@setup raytracing +using Bonito +Bonito.Page() +``` + +```@example raytracing +using Bonito, BonitoBook, Raycore +App() do + path = normpath(joinpath(dirname(pathof(Raycore)), "..", "docs", "src", "raytracing_tutorial_content.md")) + BonitoBook.InlineBook(path) +end +``` diff --git a/docs/src/raytracing_tutorial_content.md b/docs/src/raytracing_tutorial_content.md new file mode 100644 index 0000000..deb5094 --- /dev/null +++ b/docs/src/raytracing_tutorial_content.md @@ -0,0 +1,600 @@ +# Ray Tracing with Raycore: Building a Real Ray Tracer + +In this tutorial, we'll build a simple but complete ray tracer from scratch using Raycore. We'll start with the absolute basics and progressively add features until we have a ray tracer that produces beautiful images with shadows and materials. + +By the end, you'll have a working ray tracer that can render complex scenes! + +## Setup + +```julia (editor=true, logging=false, output=true) +using Raycore, GeometryBasics, LinearAlgebra +using Colors, ImageShow +using Makie # For loading assets +using BenchmarkTools +``` +**Ready to go!** We have: + + * `Raycore` for fast ray-triangle intersections + * `GeometryBasics` for geometry primitives + * `Colors` and `ImageShow` for displaying rendered images + +## Part 1: Our Scene - A Playful Cat + +Let's create a fun scene that we'll use throughout this tutorial. We'll load a cat model and place it in a simple room. + +```julia (editor=true, logging=false, output=true) +# Load the cat model and rotate it to face the camera +cat_mesh = Makie.loadasset("cat.obj") +# Rotate 150 degrees around Y axis so cat faces camera at an angle +angle = deg2rad(150f0) +rotation = Makie.Quaternionf(0, sin(angle/2), 0, cos(angle/2)) +rotated_coords = [rotation * Point3f(v) for v in coordinates(cat_mesh)] + +# Get bounding box and translate cat to sit on the floor +cat_bbox = Rect3f(rotated_coords) +floor_y = -1.5f0 +cat_offset = Vec3f(0, floor_y - cat_bbox.origin[2], 0) # Translate so bottom sits on floor + +cat_mesh = GeometryBasics.normal_mesh( + [v + cat_offset for v in rotated_coords], + faces(cat_mesh) +) + +# Create a simple room: floor, back wall, and side wall +floor = normal_mesh(Rect3f(Vec3f(-5, -1.5, -2), Vec3f(10, 0.01, 10))) +back_wall = normal_mesh(Rect3f(Vec3f(-5, -1.5, 8), Vec3f(10, 5, 0.01))) +left_wall = normal_mesh(Rect3f(Vec3f(-5, -1.5, -2), Vec3f(0.01, 5, 10))) + +# Add a couple of spheres for visual interest (also on the floor) +sphere1 = Tesselation(Sphere(Point3f(-2, -1.5 + 0.8, 2), 0.8f0), 64) +sphere2 = Tesselation(Sphere(Point3f(2, -1.5 + 0.6, 1), 0.6f0), 64) + +# Build our BVH acceleration structure +scene_geometry = [cat_mesh, floor, back_wall, left_wall, sphere1, sphere2] +bvh = Raycore.BVHAccel(scene_geometry) +``` +**Scene created!** + + * Cat model with triangulated geometry + * Room geometry: 3 walls + * 2 decorative spheres + * BVH built for fast ray traversal + +## Part 2: The Simplest Ray Tracer - Binary Hit Detection + +Let's start super simple: for each pixel, we shoot a ray and color it based on whether we hit something or not. + +```julia (editor=true, logging=false, output=true) +# Trace helper - runs a callback for each pixel +function trace(f, bvh; width=700, height=300, camera_pos=Point3f(0, -0.9, -2.5), fov=45.0f0, + sky_color=RGB{Float32}(0.5f0, 0.7f0, 1.0f0)) + img = Matrix{RGB{Float32}}(undef, height, width) + + # Precompute camera parameters + aspect = Float32(width / height) + focal_length = 1.0f0 / tan(deg2rad(fov / 2)) + + for y in 1:height, x in 1:width + # Generate camera ray + ndc_x = (2.0f0 * x / width - 1.0f0) * aspect + ndc_y = 1.0f0 - 2.0f0 * y / height + direction = normalize(Vec3f(ndc_x, ndc_y, focal_length)) + ray = Raycore.Ray(o=camera_pos, d=direction) + + # Ray-scene intersection + hit_found, triangle, distance, bary_coords = Raycore.closest_hit(bvh, ray) + + # Let the callback decide the color (pass sky_color for misses) + img[y, x] = hit_found ? f(triangle, distance, bary_coords, ray) : sky_color + end + + return img +end + +# Binary kernel - white if hit +binary_kernel(triangle, distance, bary_coords, ray) = RGB(1.0f0, 1.0f0, 1.0f0) + +trace(binary_kernel, bvh, sky_color=RGB(0.0f0, 0.0f0, 0.0f0)) +``` +**Our first render!** Pure silhouette - you can see the cat and spheres. + +## Part 3: Adding Depth - Distance-Based Shading + +Let's make it more interesting by coloring based on distance (depth map). + +```julia (editor=true, logging=false, output=true) +function depth_kernel(triangle, distance, bary_coords, ray) + # Map distance to grayscale (closer = brighter) + normalized_depth = clamp(1.0f0 - (distance - 2.0f0) / 8.0f0, 0.0f0, 1.0f0) + RGB(normalized_depth, normalized_depth, normalized_depth) +end + +trace(depth_kernel, bvh) +``` +**Depth perception!** Now we can see the 3D structure - closer objects are brighter. + +## Part 4: Surface Normals - The Foundation of Lighting + +To do proper lighting, we need surface normals. Let's compute and visualize them. + +```julia (editor=true, logging=false, output=true) +# Helper to interpolate normals using barycentric coordinates +function compute_normal(triangle, bary_coords) + n1, n2, n3 = triangle.normals + u, v, w = bary_coords + normalize(Vec3f(u * n1 + v * n2 + w * n3)) +end + +function normal_kernel(triangle, distance, bary_coords, ray) + normal = compute_normal(triangle, bary_coords) + # Map normal components [-1,1] to color [0,1] + RGB((normal .+ 1.0f0) ./ 2.0f0...) +end + +trace(normal_kernel, bvh) +``` +**Surface normals visualized!** Each color channel represents a normal component: + + * Red = X direction + * Green = Y direction + * Blue = Z direction + +## Part 5: Basic Lighting - Diffuse Shading + +Now we can add a light source and compute simple diffuse (Lambertian) shading! + +```julia (editor=true, logging=false, output=true) +light_pos = Point3f(3, 4, -2) +light_intensity = 50.0f0 + +function diffuse_kernel(triangle, distance, bary_coords, ray) + # Compute hit point and normal + hit_point = ray.o + ray.d * distance + normal = compute_normal(triangle, bary_coords) + + # Light direction and distance + light_dir = light_pos - hit_point + light_distance = norm(light_dir) + light_dir = normalize(light_dir) + + # Diffuse shading (Lambertian) + diffuse = max(0.0f0, dot(normal, light_dir)) + + # Light attenuation (inverse square law) + attenuation = light_intensity / (light_distance * light_distance) + color = diffuse * attenuation + + RGB(color, color, color) +end + +trace(diffuse_kernel, bvh) +``` +**Let there be light!** Our scene now has proper shading based on surface orientation. + +## Part 6: Adding Shadows - Shadow Rays + +Time to add realism with shadows using Raycore's `any_hit` for fast occlusion testing. + +```julia (editor=true, logging=false, output=true) +ambient = 0.1f0 # Ambient lighting to prevent pure black shadows + +function shadow_kernel(triangle, distance, bary_coords, ray) + hit_point = ray.o + ray.d * distance + normal = compute_normal(triangle, bary_coords) + + # Light direction + light_dir = light_pos - hit_point + light_distance = norm(light_dir) + light_dir = normalize(light_dir) + + # Diffuse shading + diffuse = max(0.0f0, dot(normal, light_dir)) + + # Shadow ray - offset slightly to avoid self-intersection + shadow_ray_origin = hit_point + normal * 0.001f0 + shadow_ray = Raycore.Ray(o=shadow_ray_origin, d=light_dir) + + # Check if path to light is blocked + shadow_hit, _, shadow_dist, _ = Raycore.any_hit(bvh, shadow_ray) + in_shadow = shadow_hit && shadow_dist < light_distance + + # Final color + color = if in_shadow + ambient # Only ambient in shadow + else + attenuation = light_intensity / (light_distance * light_distance) + ambient + diffuse * attenuation + end + + RGB(color, color, color) +end + +trace(shadow_kernel, bvh) +``` +**Shadows!** Notice how objects cast shadows on each other, adding depth and realism. + +## Part 7: Multiple Lights + +Let's add multiple lights to make the scene more interesting! We'll define a RenderContext to hold lights and materials: + +```julia (editor=true, logging=false, output=true) +# Define a simple point light structure +struct PointLight + position::Point3f + intensity::Float32 + color::RGB{Float32} +end + +# Material structure (for later use) +struct Material + base_color::RGB{Float32} + metallic::Float32 + roughness::Float32 +end + +# Render context holds all scene data +struct RenderContext + bvh::Raycore.BVHAccel + lights::Vector{PointLight} + materials::Vector{Material} + ambient::Float32 +end + +# Create multiple lights +lights = [ + PointLight(Point3f(3, 4, -2), 50.0f0, RGB(1.0f0, 0.9f0, 0.8f0)), # Warm main light + PointLight(Point3f(-3, 2, 0), 20.0f0, RGB(0.7f0, 0.8f0, 1.0f0)), # Cool fill light + PointLight(Point3f(0, 5, 5), 15.0f0, RGB(1.0f0, 1.0f0, 1.0f0)) # White back light +] + +# Materials (will use these in Part 8) +materials = [ + Material(RGB(0.8f0, 0.6f0, 0.4f0), 0.0f0, 0.8f0), # 1: Cat + Material(RGB(0.3f0, 0.5f0, 0.3f0), 0.0f0, 0.9f0), # 2: Floor + Material(RGB(0.7f0, 0.7f0, 0.8f0), 0.0f0, 0.8f0), # 3: Back wall + Material(RGB(0.8f0, 0.7f0, 0.7f0), 0.0f0, 0.8f0), # 4: Left wall + Material(RGB(0.95f0, 0.64f0, 0.54f0), 1.0f0, 0.1f0), # 5: Sphere 1 - metallic + Material(RGB(0.8f0, 0.8f0, 0.9f0), 1.0f0, 0.0f0) # 6: Sphere 2 - mirror +] + +# Create render context +ctx = RenderContext(bvh, lights, materials, 0.1f0) +``` +Now we need a new trace function that works with RenderContext: + +```julia (editor=true, logging=false, output=true) +# Trace with RenderContext +function trace_ctx(f, ctx::RenderContext; width=700, height=300,camera_pos=Point3f(0, -0.9, -2.5), fov=45.0f0, + sky_color=RGB{Float32}(0.5f0, 0.7f0, 1.0f0)) + img = Matrix{RGB{Float32}}(undef, height, width) + + aspect = Float32(width / height) + focal_length = 1.0f0 / tan(deg2rad(fov / 2)) + + for y in 1:height, x in 1:width + ndc_x = (2.0f0 * x / width - 1.0f0) * aspect + ndc_y = 1.0f0 - 2.0f0 * y / height + direction = normalize(Vec3f(ndc_x, ndc_y, focal_length)) + ray = Raycore.Ray(o=camera_pos, d=direction) + + hit_found, triangle, distance, bary_coords = Raycore.closest_hit(ctx.bvh, ray) + img[y, x] = hit_found ? f(ctx, triangle, distance, bary_coords, ray) : sky_color + end + + return img +end + +function multi_light_kernel(ctx, triangle, distance, bary_coords, ray) + hit_point = ray.o + ray.d * distance + normal = compute_normal(triangle, bary_coords) + + # Start with ambient (grayscale) + total_color = Vec3f(ctx.ambient, ctx.ambient, ctx.ambient) + + # Accumulate contribution from each light + for light in ctx.lights + light_vec = light.position - hit_point + light_distance = norm(light_vec) + light_dir = light_vec / light_distance + + diffuse = max(0.0f0, dot(normal, light_dir)) + + shadow_ray = Raycore.Ray(o=hit_point + normal * 0.001f0, d=light_dir) + shadow_hit, _, shadow_dist, _ = Raycore.any_hit(ctx.bvh, shadow_ray) + in_shadow = shadow_hit && shadow_dist < light_distance + + if !in_shadow + attenuation = light.intensity / (light_distance * light_distance) + light_col = Vec3f(light.color.r, light.color.g, light.color.b) + total_color += light_col * (diffuse * attenuation) + end + end + + RGB{Float32}(total_color...) +end + +trace_ctx(multi_light_kernel, ctx) +``` +**Multiple lights!** The scene now has three different colored lights creating a more dynamic lighting environment. + +## Part 8: Colored Materials with Multiple Lights + +Now let's combine materials with our multiple lights! + +```julia (editor=true, logging=false, output=true) +function material_multi_light_kernel(ctx, triangle, distance, bary_coords, ray) + hit_point = ray.o + ray.d * distance + normal = compute_normal(triangle, bary_coords) + + # Get material from context + mat = ctx.materials[triangle.material_idx] + base_color = Vec3f(mat.base_color.r, mat.base_color.g, mat.base_color.b) + + # Start with ambient + total_color = base_color * ctx.ambient + + # Accumulate contribution from each light + for light in ctx.lights + light_vec = light.position - hit_point + light_distance = norm(light_vec) + light_dir = light_vec / light_distance + + diffuse = max(0.0f0, dot(normal, light_dir)) + + # Shadow test + shadow_ray = Raycore.Ray(o=hit_point + normal * 0.001f0, d=light_dir) + shadow_hit, _, shadow_dist, _ = Raycore.any_hit(ctx.bvh, shadow_ray) + in_shadow = shadow_hit && shadow_dist < light_distance + + if !in_shadow + attenuation = light.intensity / (light_distance * light_distance) + light_col = Vec3f(light.color.r, light.color.g, light.color.b) + total_color += base_color .* light_col * (diffuse * attenuation) + end + end + + RGB{Float32}(total_color...) +end + +trace_ctx(material_multi_light_kernel, ctx) +``` +**Colored materials!** + + * Orange/tan cat + * Green floor + * Light blue back wall + * Pink side wall + * Red and blue spheres + +## Part 9: Reflective Materials - Mirrors and Metals + +The materials we defined in Part 7 already have metallic and roughness properties. Let's use them for reflections! + +```julia (editor=true, logging=false, output=true) +# Helper: compute direct lighting with multiple lights +function compute_multi_light(ctx, point, normal, mat) + base_color = Vec3f(mat.base_color.r, mat.base_color.g, mat.base_color.b) + + # Start with ambient + total_color = base_color * ctx.ambient + + for light in ctx.lights + light_vec = light.position - point + light_distance = norm(light_vec) + light_dir = light_vec / light_distance + + diffuse = max(0.0f0, dot(normal, light_dir)) + + # Shadow test + shadow_ray = Raycore.Ray(o=point + normal * 0.001f0, d=light_dir) + shadow_hit, _, shadow_dist, _ = Raycore.any_hit(ctx.bvh, shadow_ray) + in_shadow = shadow_hit && shadow_dist < light_distance + + if !in_shadow + attenuation = light.intensity / (light_distance * light_distance) + light_col = Vec3f(light.color.r, light.color.g, light.color.b) + total_color += base_color .* light_col * (diffuse * attenuation) + end + end + + return RGB{Float32}(total_color...) +end + +function reflective_kernel(ctx, triangle, distance, bary_coords, ray, sky_color) + hit_point = ray.o + ray.d * distance + normal = compute_normal(triangle, bary_coords) + mat = ctx.materials[triangle.material_idx] + + # Compute direct lighting (diffuse component) + direct_color = compute_multi_light(ctx, hit_point, normal, mat) + + # Add reflection for metallic materials + if mat.metallic > 0.0f0 + # Compute reflection direction: reflect outgoing direction about normal + # Note: ray.d points toward surface, but reflect() expects outgoing direction + wo = -ray.d # outgoing direction (away from surface) + reflect_dir = Raycore.reflect(wo, normal) + + # Add roughness by perturbing reflection direction + if mat.roughness > 0.0f0 + # Simple roughness: add random offset in tangent space + random_offset = (rand(Vec3f) .* 2.0f0 .- 1.0f0) * mat.roughness + reflect_dir = normalize(reflect_dir + random_offset) + end + + # Cast reflection ray (offset to avoid self-intersection) + reflect_ray = Raycore.Ray(o=hit_point + normal * 0.001f0, d=reflect_dir) + refl_hit, refl_tri, refl_dist, refl_bary = Raycore.closest_hit(ctx.bvh, reflect_ray) + + # Get reflection color + reflection_color = if refl_hit + refl_point = reflect_ray.o + reflect_ray.d * refl_dist + refl_normal = compute_normal(refl_tri, refl_bary) + refl_mat = ctx.materials[refl_tri.material_idx] + + # Compute lighting for reflected surface + compute_multi_light(ctx, refl_point, refl_normal, refl_mat) + else + sky_color + end + + # Blend between diffuse and reflection based on metallic parameter + return direct_color * (1.0f0 - mat.metallic) + reflection_color * mat.metallic + else + # Pure diffuse material + return direct_color + end +end + +trace_ctx(ctx) do ctx, triangle, distance, bary_coords, ray + reflective_kernel(ctx, triangle, distance, bary_coords, ray, RGB(0.5f0, 0.7f0, 1.0f0)) +end +``` +**Reflective materials!** The spheres now have metallic properties: + + * One smooth copper-colored metal with slight roughness + * One perfect mirror reflecting the scene + +Notice how reflections capture both the scene geometry and lighting! + +## Part 10: Multi-threading for Performance + +Let's add multi-threading to make our ray tracer much faster! + +```julia (editor=true, logging=false, output=true) +using BenchmarkTools +function trace_ctx_threaded(f, ctx::RenderContext; width=400, height=300, camera_pos=Point3f(0, 1, -2.5), fov=45.0f0, + sky_color=RGB{Float32}(0.5f0, 0.7f0, 1.0f0)) + img = Matrix{RGB{Float32}}(undef, height, width) + + aspect = Float32(width / height) + focal_length = 1.0f0 / tan(deg2rad(fov / 2)) + + Threads.@threads for y in 1:height + for x in 1:width + ndc_x = (2.0f0 * x / width - 1.0f0) * aspect + ndc_y = 1.0f0 - 2.0f0 * y / height + direction = normalize(Vec3f(ndc_x, ndc_y, focal_length)) + ray = Raycore.Ray(o=camera_pos, d=direction) + + hit_found, triangle, distance, bary_coords = Raycore.closest_hit(ctx.bvh, ray) + img[y, x] = hit_found ? f(ctx, triangle, distance, bary_coords, ray) : sky_color + end + end + + return img +end + +# Benchmark single-threaded vs multi-threaded +b1 = @belapsed trace_ctx(material_multi_light_kernel, ctx, width=800, height=600); + +b2 = @belapsed trace_ctx_threaded(material_multi_light_kernel, ctx, width=800, height=600); +md""" +Threads: $(Threads.nthreads()) + +Single: $(b1) + +Multi: $(b2) +""" +``` +**Performance boost with threading!** The speedup should be close to the number of CPU cores. + +Notice how we can reuse the same kernel function with both `trace_ctx()` and `trace_ctx_threaded()` - this is great for composability! + +## Part 11: Multi-Sampling for Anti-Aliasing + +Let's add multiple samples per pixel with jittered camera rays for smooth anti-aliasing: + +```julia (editor=true, logging=false, output=true) +function trace_ctx_sampled(f, ctx::RenderContext; + width=700, height=300, + camera_pos=Point3f(0, -0.9, -2.5), fov=45.0f0, + sky_color=RGB{Float32}(0.5f0, 0.7f0, 1.0f0), + samples=4) + img = Matrix{RGB{Float32}}(undef, height, width) + + aspect = Float32(width / height) + focal_length = 1.0f0 / tan(deg2rad(fov / 2)) + pixel_size = 1.0f0 / width + + Threads.@threads for y in 1:height + for x in 1:width + # Accumulate multiple samples per pixel using Vec3f for math + color_sum = Vec3f(0.0f0, 0.0f0, 0.0f0) + + for _ in 1:samples + jitter_x = (rand(Float32) - 0.5f0) * pixel_size + jitter_y = (rand(Float32) - 0.5f0) * pixel_size + + ndc_x = (2.0f0 * (x + jitter_x) / width - 1.0f0) * aspect + ndc_y = 1.0f0 - 2.0f0 * (y + jitter_y) / height + direction = normalize(Vec3f(ndc_x, ndc_y, focal_length)) + ray = Raycore.Ray(o=camera_pos, d=direction) + + hit_found, triangle, distance, bary_coords = Raycore.closest_hit(ctx.bvh, ray) + + color = if hit_found + result = f(ctx, triangle, distance, bary_coords, ray) + Vec3f(result.r, result.g, result.b) + else + Vec3f(sky_color.r, sky_color.g, sky_color.b) + end + + color_sum += color + end + + # Average samples and convert back to RGB + avg = color_sum / samples + img[y, x] = RGB{Float32}(avg...) + end + end + + return img +end + +# Render with 16 samples per pixel for smooth anti-aliasing +@time trace_ctx_sampled(ctx, samples=64) do ctx, triangle, distance, bary_coords, ray + reflective_kernel(ctx, triangle, distance, bary_coords, ray, RGB(0.5f0, 0.7f0, 1.0f0)) +end +``` +**Anti-aliased render!** 32 samples per pixel with jittered camera rays eliminate jagged edges. + +## Summary - What We Built + +We created a complete ray tracer that includes: + +### Features Implemented + +1. **Camera system** - Perspective projection with configurable FOV +2. **Ray-scene intersection** - Using Raycore's BVH for fast traversal +3. **Surface normals** - Smooth shading from vertex normals +4. **Diffuse lighting** - Lambertian shading with distance attenuation +5. **Hard shadows** - Using `any_hit` for efficient occlusion testing +6. **Simple materials** - Per-object color assignment +7. **Multi-threading** - Parallel rendering across CPU cores +8. **Callback-based API** - Flexible `trace()` function for experimentation + +### Next Steps + +To make this into a full path tracer (like `Trace`), you would add: + + * **Recursive ray tracing** - Reflections and refractions + * **Multiple light sources** - Area lights, environment lighting + * **Advanced materials** - Specular, glossy, transparent + * **Sampling** - Multiple samples per pixel for anti-aliasing + * **Better normal interpolation** - Proper barycentric interpolation + * **GPU support** - Using KernelAbstractions.jl + +The `Trace` package implements all of these features and more! + +### Key Raycore Functions Used + + * `Raycore.BVHAccel(meshes)` - Build acceleration structure + * `Raycore.Ray(o=origin, d=direction)` - Create ray + * `Raycore.closest_hit(bvh, ray)` - Find nearest intersection + * `Raycore.any_hit(bvh, ray)` - Test for any intersection (fast!) + * `Raycore.vertices(triangle)` - Get triangle vertex positions + * `Raycore.normals(triangle)` - Get triangle vertex normals + +Happy ray tracing! + From a068b08608d362e3458f82437c88e7d84df87668 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Tue, 28 Oct 2025 20:55:59 +0100 Subject: [PATCH 16/20] add packages --- docs/Project.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/Project.toml b/docs/Project.toml index 8be15b5..a7ee7e5 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,9 +1,12 @@ [deps] +BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" Bonito = "824d6782-a2ef-11e9-3a09-e5662e0c26f8" BonitoBook = "b416d416-7a6e-4336-8c1a-1f8a8cd59518" +Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" +ImageShow = "4e3cecfd-b093-5904-9786-8bbb286a6a31" KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c" Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" MeshIO = "7269a6da-0436-5bbc-96c2-40638cbb6118" @@ -11,8 +14,8 @@ Raycore = "afc56b53-c9a9-482a-a956-d1d800e05559" WGLMakie = "276b4fcb-3e11-5398-bf8b-a0c2d153d008" [sources] -Raycore = {path = "../"} BonitoBook = {url = "https://github.com/SimonDanisch/BonitoBook.jl"} +Raycore = {path = "../"} [compat] Documenter = "1.5" From d54d9642eda9f428b5694884f5de2a74cdd3d1b9 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Wed, 29 Oct 2025 13:33:16 +0100 Subject: [PATCH 17/20] update tutorial --- ...cing_tutorial_content-2025-10-28_201849.md | 603 ---------------- docs/src/raytracing_tutorial_content.md | 643 ++++++------------ src/bvh.jl | 7 +- 3 files changed, 222 insertions(+), 1031 deletions(-) delete mode 100644 docs/src/.raytracing_tutorial_content-bbook/.versions/raytracing_tutorial_content-2025-10-28_201849.md diff --git a/docs/src/.raytracing_tutorial_content-bbook/.versions/raytracing_tutorial_content-2025-10-28_201849.md b/docs/src/.raytracing_tutorial_content-bbook/.versions/raytracing_tutorial_content-2025-10-28_201849.md deleted file mode 100644 index a3637ce..0000000 --- a/docs/src/.raytracing_tutorial_content-bbook/.versions/raytracing_tutorial_content-2025-10-28_201849.md +++ /dev/null @@ -1,603 +0,0 @@ -# Ray Tracing with Raycore: Building a Real Ray Tracer - -In this tutorial, we'll build a simple but complete ray tracer from scratch using Raycore. We'll start with the absolute basics and progressively add features until we have a ray tracer that produces beautiful images with shadows and materials. - -By the end, you'll have a working ray tracer that can render complex scenes! - -## Setup - -```julia (editor=true, logging=false, output=true) -using Raycore, GeometryBasics, LinearAlgebra -using Colors, ImageShow -using Makie # For loading assets -using BenchmarkTools -``` -**Ready to go!** We have: - - * `Raycore` for fast ray-triangle intersections - * `GeometryBasics` for geometry primitives - * `Colors` and `ImageShow` for displaying rendered images - -## Part 1: Our Scene - A Playful Cat - -Let's create a fun scene that we'll use throughout this tutorial. We'll load a cat model and place it in a simple room. - -```julia (editor=true, logging=false, output=true) -# Load the cat model and rotate it to face the camera -cat_mesh = Makie.loadasset("cat.obj") -# Rotate 150 degrees around Y axis so cat faces camera at an angle -angle = deg2rad(150f0) -rotation = Makie.Quaternionf(0, sin(angle/2), 0, cos(angle/2)) -rotated_coords = [rotation * Point3f(v) for v in coordinates(cat_mesh)] - -# Get bounding box and translate cat to sit on the floor -cat_bbox = Rect3f(rotated_coords) -floor_y = -1.5f0 -cat_offset = Vec3f(0, floor_y - cat_bbox.origin[2], 0) # Translate so bottom sits on floor - -cat_mesh = GeometryBasics.normal_mesh( - [v + cat_offset for v in rotated_coords], - faces(cat_mesh) -) - -# Create a simple room: floor, back wall, and side wall -floor = normal_mesh(Rect3f(Vec3f(-5, -1.5, -2), Vec3f(10, 0.01, 10))) -back_wall = normal_mesh(Rect3f(Vec3f(-5, -1.5, 8), Vec3f(10, 5, 0.01))) -left_wall = normal_mesh(Rect3f(Vec3f(-5, -1.5, -2), Vec3f(0.01, 5, 10))) - -# Add a couple of spheres for visual interest (also on the floor) -sphere1 = Tesselation(Sphere(Point3f(-2, -1.5 + 0.8, 2), 0.8f0), 64) -sphere2 = Tesselation(Sphere(Point3f(2, -1.5 + 0.6, 1), 0.6f0), 64) - -# Build our BVH acceleration structure -scene_geometry = [cat_mesh, floor, back_wall, left_wall, sphere1, sphere2] -bvh = Raycore.BVHAccel(scene_geometry) -``` -**Scene created!** - - * Cat model with triangulated geometry - * Room geometry: 3 walls - * 2 decorative spheres - * BVH built for fast ray traversal - -## Part 2: The Simplest Ray Tracer - Binary Hit Detection - -Let's start super simple: for each pixel, we shoot a ray and color it based on whether we hit something or not. - -```julia (editor=true, logging=false, output=true) -# Trace helper - runs a callback for each pixel -function trace(f, bvh; width=700, height=300, camera_pos=Point3f(0, -0.9, -2.5), fov=45.0f0, - sky_color=RGB{Float32}(0.5f0, 0.7f0, 1.0f0)) - img = Matrix{RGB{Float32}}(undef, height, width) - - # Precompute camera parameters - aspect = Float32(width / height) - focal_length = 1.0f0 / tan(deg2rad(fov / 2)) - - for y in 1:height, x in 1:width - # Generate camera ray - ndc_x = (2.0f0 * x / width - 1.0f0) * aspect - ndc_y = 1.0f0 - 2.0f0 * y / height - direction = normalize(Vec3f(ndc_x, ndc_y, focal_length)) - ray = Raycore.Ray(o=camera_pos, d=direction) - - # Ray-scene intersection - hit_found, triangle, distance, bary_coords = Raycore.closest_hit(bvh, ray) - - # Let the callback decide the color (pass sky_color for misses) - img[y, x] = hit_found ? f(triangle, distance, bary_coords, ray) : sky_color - end - - return img -end - -# Binary kernel - white if hit -binary_kernel(triangle, distance, bary_coords, ray) = RGB(1.0f0, 1.0f0, 1.0f0) - -trace(binary_kernel, bvh, sky_color=RGB(0.0f0, 0.0f0, 0.0f0)) -``` -**Our first render!** Pure silhouette - you can see the cat and spheres. - -## Part 3: Adding Depth - Distance-Based Shading - -Let's make it more interesting by coloring based on distance (depth map). - -```julia (editor=true, logging=false, output=true) -function depth_kernel(triangle, distance, bary_coords, ray) - # Map distance to grayscale (closer = brighter) - normalized_depth = clamp(1.0f0 - (distance - 2.0f0) / 8.0f0, 0.0f0, 1.0f0) - RGB(normalized_depth, normalized_depth, normalized_depth) -end - -trace(depth_kernel, bvh) -``` -**Depth perception!** Now we can see the 3D structure - closer objects are brighter. - -## Part 4: Surface Normals - The Foundation of Lighting - -To do proper lighting, we need surface normals. Let's compute and visualize them. - -```julia (editor=true, logging=false, output=true) -# Helper to interpolate normals using barycentric coordinates -function compute_normal(triangle, bary_coords) - n1, n2, n3 = triangle.normals - u, v, w = bary_coords - normalize(Vec3f(u * n1 + v * n2 + w * n3)) -end - -function normal_kernel(triangle, distance, bary_coords, ray) - normal = compute_normal(triangle, bary_coords) - # Map normal components [-1,1] to color [0,1] - RGB((normal .+ 1.0f0) ./ 2.0f0...) -end - -trace(normal_kernel, bvh) -``` -**Surface normals visualized!** Each color channel represents a normal component: - - * Red = X direction - * Green = Y direction - * Blue = Z direction - -## Part 5: Basic Lighting - Diffuse Shading - -Now we can add a light source and compute simple diffuse (Lambertian) shading! - -```julia (editor=true, logging=false, output=true) -light_pos = Point3f(3, 4, -2) -light_intensity = 50.0f0 - -function diffuse_kernel(triangle, distance, bary_coords, ray) - # Compute hit point and normal - hit_point = ray.o + ray.d * distance - normal = compute_normal(triangle, bary_coords) - - # Light direction and distance - light_dir = light_pos - hit_point - light_distance = norm(light_dir) - light_dir = normalize(light_dir) - - # Diffuse shading (Lambertian) - diffuse = max(0.0f0, dot(normal, light_dir)) - - # Light attenuation (inverse square law) - attenuation = light_intensity / (light_distance * light_distance) - color = diffuse * attenuation - - RGB(color, color, color) -end - -trace(diffuse_kernel, bvh) -``` -**Let there be light!** Our scene now has proper shading based on surface orientation. - -## Part 6: Adding Shadows - Shadow Rays - -Time to add realism with shadows using Raycore's `any_hit` for fast occlusion testing. - -```julia (editor=true, logging=false, output=true) -ambient = 0.1f0 # Ambient lighting to prevent pure black shadows - -function shadow_kernel(triangle, distance, bary_coords, ray) - hit_point = ray.o + ray.d * distance - normal = compute_normal(triangle, bary_coords) - - # Light direction - light_dir = light_pos - hit_point - light_distance = norm(light_dir) - light_dir = normalize(light_dir) - - # Diffuse shading - diffuse = max(0.0f0, dot(normal, light_dir)) - - # Shadow ray - offset slightly to avoid self-intersection - shadow_ray_origin = hit_point + normal * 0.001f0 - shadow_ray = Raycore.Ray(o=shadow_ray_origin, d=light_dir) - - # Check if path to light is blocked - shadow_hit, _, shadow_dist, _ = Raycore.any_hit(bvh, shadow_ray) - in_shadow = shadow_hit && shadow_dist < light_distance - - # Final color - color = if in_shadow - ambient # Only ambient in shadow - else - attenuation = light_intensity / (light_distance * light_distance) - ambient + diffuse * attenuation - end - - RGB(color, color, color) -end - -trace(shadow_kernel, bvh) -``` -**Shadows!** Notice how objects cast shadows on each other, adding depth and realism. - -## Part 7: Multiple Lights - -Let's add multiple lights to make the scene more interesting! We'll define a RenderContext to hold lights and materials: - -```julia (editor=true, logging=false, output=true) -# Define a simple point light structure -struct PointLight - position::Point3f - intensity::Float32 - color::RGB{Float32} -end - -# Material structure (for later use) -struct Material - base_color::RGB{Float32} - metallic::Float32 - roughness::Float32 -end - -# Render context holds all scene data -struct RenderContext - bvh::Raycore.BVHAccel - lights::Vector{PointLight} - materials::Vector{Material} - ambient::Float32 -end - -# Create multiple lights -lights = [ - PointLight(Point3f(3, 4, -2), 50.0f0, RGB(1.0f0, 0.9f0, 0.8f0)), # Warm main light - PointLight(Point3f(-3, 2, 0), 20.0f0, RGB(0.7f0, 0.8f0, 1.0f0)), # Cool fill light - PointLight(Point3f(0, 5, 5), 15.0f0, RGB(1.0f0, 1.0f0, 1.0f0)) # White back light -] - -# Materials (will use these in Part 8) -materials = [ - Material(RGB(0.8f0, 0.6f0, 0.4f0), 0.0f0, 0.8f0), # 1: Cat - Material(RGB(0.3f0, 0.5f0, 0.3f0), 0.0f0, 0.9f0), # 2: Floor - Material(RGB(0.7f0, 0.7f0, 0.8f0), 0.0f0, 0.8f0), # 3: Back wall - Material(RGB(0.8f0, 0.7f0, 0.7f0), 0.0f0, 0.8f0), # 4: Left wall - Material(RGB(0.95f0, 0.64f0, 0.54f0), 1.0f0, 0.1f0), # 5: Sphere 1 - metallic - Material(RGB(0.8f0, 0.8f0, 0.9f0), 1.0f0, 0.0f0) # 6: Sphere 2 - mirror -] - -# Create render context -ctx = RenderContext(bvh, lights, materials, 0.1f0) -``` -Now we need a new trace function that works with RenderContext: - -```julia (editor=true, logging=false, output=true) -# Trace with RenderContext -function trace_ctx(f, ctx::RenderContext; width=700, height=300,camera_pos=Point3f(0, -0.9, -2.5), fov=45.0f0, - sky_color=RGB{Float32}(0.5f0, 0.7f0, 1.0f0)) - img = Matrix{RGB{Float32}}(undef, height, width) - - aspect = Float32(width / height) - focal_length = 1.0f0 / tan(deg2rad(fov / 2)) - - for y in 1:height, x in 1:width - ndc_x = (2.0f0 * x / width - 1.0f0) * aspect - ndc_y = 1.0f0 - 2.0f0 * y / height - direction = normalize(Vec3f(ndc_x, ndc_y, focal_length)) - ray = Raycore.Ray(o=camera_pos, d=direction) - - hit_found, triangle, distance, bary_coords = Raycore.closest_hit(ctx.bvh, ray) - img[y, x] = hit_found ? f(ctx, triangle, distance, bary_coords, ray) : sky_color - end - - return img -end - -function multi_light_kernel(ctx, triangle, distance, bary_coords, ray) - hit_point = ray.o + ray.d * distance - normal = compute_normal(triangle, bary_coords) - - # Start with ambient (grayscale) - total_color = Vec3f(ctx.ambient, ctx.ambient, ctx.ambient) - - # Accumulate contribution from each light - for light in ctx.lights - light_vec = light.position - hit_point - light_distance = norm(light_vec) - light_dir = light_vec / light_distance - - diffuse = max(0.0f0, dot(normal, light_dir)) - - shadow_ray = Raycore.Ray(o=hit_point + normal * 0.001f0, d=light_dir) - shadow_hit, _, shadow_dist, _ = Raycore.any_hit(ctx.bvh, shadow_ray) - in_shadow = shadow_hit && shadow_dist < light_distance - - if !in_shadow - attenuation = light.intensity / (light_distance * light_distance) - light_col = Vec3f(light.color.r, light.color.g, light.color.b) - total_color += light_col * (diffuse * attenuation) - end - end - - RGB{Float32}(total_color...) -end - -trace_ctx(multi_light_kernel, ctx) -``` -**Multiple lights!** The scene now has three different colored lights creating a more dynamic lighting environment. - -## Part 8: Colored Materials with Multiple Lights - -Now let's combine materials with our multiple lights! - -```julia (editor=true, logging=false, output=true) -function material_multi_light_kernel(ctx, triangle, distance, bary_coords, ray) - hit_point = ray.o + ray.d * distance - normal = compute_normal(triangle, bary_coords) - - # Get material from context - mat = ctx.materials[triangle.material_idx] - base_color = Vec3f(mat.base_color.r, mat.base_color.g, mat.base_color.b) - - # Start with ambient - total_color = base_color * ctx.ambient - - # Accumulate contribution from each light - for light in ctx.lights - light_vec = light.position - hit_point - light_distance = norm(light_vec) - light_dir = light_vec / light_distance - - diffuse = max(0.0f0, dot(normal, light_dir)) - - # Shadow test - shadow_ray = Raycore.Ray(o=hit_point + normal * 0.001f0, d=light_dir) - shadow_hit, _, shadow_dist, _ = Raycore.any_hit(ctx.bvh, shadow_ray) - in_shadow = shadow_hit && shadow_dist < light_distance - - if !in_shadow - attenuation = light.intensity / (light_distance * light_distance) - light_col = Vec3f(light.color.r, light.color.g, light.color.b) - total_color += base_color .* light_col * (diffuse * attenuation) - end - end - - RGB{Float32}(total_color...) -end - -trace_ctx(material_multi_light_kernel, ctx) -``` -**Colored materials!** - - * Orange/tan cat - * Green floor - * Light blue back wall - * Pink side wall - * Red and blue spheres - -## Part 9: Reflective Materials - Mirrors and Metals - -The materials we defined in Part 7 already have metallic and roughness properties. Let's use them for reflections! - -```julia (editor=true, logging=false, output=true) -# Helper: compute direct lighting with multiple lights -function compute_multi_light(ctx, point, normal, mat) - base_color = Vec3f(mat.base_color.r, mat.base_color.g, mat.base_color.b) - - # Start with ambient - total_color = base_color * ctx.ambient - - for light in ctx.lights - light_vec = light.position - point - light_distance = norm(light_vec) - light_dir = light_vec / light_distance - - diffuse = max(0.0f0, dot(normal, light_dir)) - - # Shadow test - shadow_ray = Raycore.Ray(o=point + normal * 0.001f0, d=light_dir) - shadow_hit, _, shadow_dist, _ = Raycore.any_hit(ctx.bvh, shadow_ray) - in_shadow = shadow_hit && shadow_dist < light_distance - - if !in_shadow - attenuation = light.intensity / (light_distance * light_distance) - light_col = Vec3f(light.color.r, light.color.g, light.color.b) - total_color += base_color .* light_col * (diffuse * attenuation) - end - end - - return RGB{Float32}(total_color...) -end - -function reflective_kernel(ctx, triangle, distance, bary_coords, ray, sky_color) - hit_point = ray.o + ray.d * distance - normal = compute_normal(triangle, bary_coords) - mat = ctx.materials[triangle.material_idx] - - # Compute direct lighting (diffuse component) - direct_color = compute_multi_light(ctx, hit_point, normal, mat) - - # Add reflection for metallic materials - if mat.metallic > 0.0f0 - # Compute reflection direction: reflect outgoing direction about normal - # Note: ray.d points toward surface, but reflect() expects outgoing direction - wo = -ray.d # outgoing direction (away from surface) - reflect_dir = Raycore.reflect(wo, normal) - - # Add roughness by perturbing reflection direction - if mat.roughness > 0.0f0 - # Simple roughness: add random offset in tangent space - random_offset = (rand(Vec3f) .* 2.0f0 .- 1.0f0) * mat.roughness - reflect_dir = normalize(reflect_dir + random_offset) - end - - # Cast reflection ray (offset to avoid self-intersection) - reflect_ray = Raycore.Ray(o=hit_point + normal * 0.001f0, d=reflect_dir) - refl_hit, refl_tri, refl_dist, refl_bary = Raycore.closest_hit(ctx.bvh, reflect_ray) - - # Get reflection color - reflection_color = if refl_hit - refl_point = reflect_ray.o + reflect_ray.d * refl_dist - refl_normal = compute_normal(refl_tri, refl_bary) - refl_mat = ctx.materials[refl_tri.material_idx] - - # Compute lighting for reflected surface - compute_multi_light(ctx, refl_point, refl_normal, refl_mat) - else - sky_color - end - - # Blend between diffuse and reflection based on metallic parameter - return direct_color * (1.0f0 - mat.metallic) + reflection_color * mat.metallic - else - # Pure diffuse material - return direct_color - end -end - -trace_ctx(ctx) do ctx, triangle, distance, bary_coords, ray - reflective_kernel(ctx, triangle, distance, bary_coords, ray, RGB(0.5f0, 0.7f0, 1.0f0)) -end -``` -**Reflective materials!** The spheres now have metallic properties: - - * One smooth copper-colored metal with slight roughness - * One perfect mirror reflecting the scene - -Notice how reflections capture both the scene geometry and lighting! - -## Part 10: Multi-threading for Performance - -Let's add multi-threading to make our ray tracer much faster! - -```julia (editor=true, logging=false, output=true) - -``` -```julia (editor=true, logging=false, output=true) -using BenchmarkTools -function trace_ctx_threaded(f, ctx::RenderContext; width=400, height=300, camera_pos=Point3f(0, 1, -2.5), fov=45.0f0, - sky_color=RGB{Float32}(0.5f0, 0.7f0, 1.0f0)) - img = Matrix{RGB{Float32}}(undef, height, width) - - aspect = Float32(width / height) - focal_length = 1.0f0 / tan(deg2rad(fov / 2)) - - Threads.@threads for y in 1:height - for x in 1:width - ndc_x = (2.0f0 * x / width - 1.0f0) * aspect - ndc_y = 1.0f0 - 2.0f0 * y / height - direction = normalize(Vec3f(ndc_x, ndc_y, focal_length)) - ray = Raycore.Ray(o=camera_pos, d=direction) - - hit_found, triangle, distance, bary_coords = Raycore.closest_hit(ctx.bvh, ray) - img[y, x] = hit_found ? f(ctx, triangle, distance, bary_coords, ray) : sky_color - end - end - - return img -end - -# Benchmark single-threaded vs multi-threaded -b1 = @belapsed trace_ctx(material_multi_light_kernel, ctx, width=800, height=600); - -b2 = @belapsed trace_ctx_threaded(material_multi_light_kernel, ctx, width=800, height=600); -md""" -Threads: $(Threads.nthreads()) - -Single: $(b1) - -Multi: $(b2) -""" -``` -**Performance boost with threading!** The speedup should be close to the number of CPU cores. - -Notice how we can reuse the same kernel function with both `trace_ctx()` and `trace_ctx_threaded()` - this is great for composability! - -## Part 11: Multi-Sampling for Anti-Aliasing - -Let's add multiple samples per pixel with jittered camera rays for smooth anti-aliasing: - -```julia (editor=true, logging=false, output=true) -function trace_ctx_sampled(f, ctx::RenderContext; - width=700, height=300, - camera_pos=Point3f(0, -0.9, -2.5), fov=45.0f0, - sky_color=RGB{Float32}(0.5f0, 0.7f0, 1.0f0), - samples=4) - img = Matrix{RGB{Float32}}(undef, height, width) - - aspect = Float32(width / height) - focal_length = 1.0f0 / tan(deg2rad(fov / 2)) - pixel_size = 1.0f0 / width - - Threads.@threads for y in 1:height - for x in 1:width - # Accumulate multiple samples per pixel using Vec3f for math - color_sum = Vec3f(0.0f0, 0.0f0, 0.0f0) - - for _ in 1:samples - jitter_x = (rand(Float32) - 0.5f0) * pixel_size - jitter_y = (rand(Float32) - 0.5f0) * pixel_size - - ndc_x = (2.0f0 * (x + jitter_x) / width - 1.0f0) * aspect - ndc_y = 1.0f0 - 2.0f0 * (y + jitter_y) / height - direction = normalize(Vec3f(ndc_x, ndc_y, focal_length)) - ray = Raycore.Ray(o=camera_pos, d=direction) - - hit_found, triangle, distance, bary_coords = Raycore.closest_hit(ctx.bvh, ray) - - color = if hit_found - result = f(ctx, triangle, distance, bary_coords, ray) - Vec3f(result.r, result.g, result.b) - else - Vec3f(sky_color.r, sky_color.g, sky_color.b) - end - - color_sum += color - end - - # Average samples and convert back to RGB - avg = color_sum / samples - img[y, x] = RGB{Float32}(avg...) - end - end - - return img -end - -# Render with 16 samples per pixel for smooth anti-aliasing -@time trace_ctx_sampled(ctx, samples=64) do ctx, triangle, distance, bary_coords, ray - reflective_kernel(ctx, triangle, distance, bary_coords, ray, RGB(0.5f0, 0.7f0, 1.0f0)) -end -``` -**Anti-aliased render!** 32 samples per pixel with jittered camera rays eliminate jagged edges. - -## Summary - What We Built - -We created a complete ray tracer that includes: - -### Features Implemented - -1. **Camera system** - Perspective projection with configurable FOV -2. **Ray-scene intersection** - Using Raycore's BVH for fast traversal -3. **Surface normals** - Smooth shading from vertex normals -4. **Diffuse lighting** - Lambertian shading with distance attenuation -5. **Hard shadows** - Using `any_hit` for efficient occlusion testing -6. **Simple materials** - Per-object color assignment -7. **Multi-threading** - Parallel rendering across CPU cores -8. **Callback-based API** - Flexible `trace()` function for experimentation - -### Next Steps - -To make this into a full path tracer (like `Trace`), you would add: - - * **Recursive ray tracing** - Reflections and refractions - * **Multiple light sources** - Area lights, environment lighting - * **Advanced materials** - Specular, glossy, transparent - * **Sampling** - Multiple samples per pixel for anti-aliasing - * **Better normal interpolation** - Proper barycentric interpolation - * **GPU support** - Using KernelAbstractions.jl - -The `Trace` package implements all of these features and more! - -### Key Raycore Functions Used - - * `Raycore.BVHAccel(meshes)` - Build acceleration structure - * `Raycore.Ray(o=origin, d=direction)` - Create ray - * `Raycore.closest_hit(bvh, ray)` - Find nearest intersection - * `Raycore.any_hit(bvh, ray)` - Test for any intersection (fast!) - * `Raycore.vertices(triangle)` - Get triangle vertex positions - * `Raycore.normals(triangle)` - Get triangle vertex normals - -Happy ray tracing! - diff --git a/docs/src/raytracing_tutorial_content.md b/docs/src/raytracing_tutorial_content.md index deb5094..b60298b 100644 --- a/docs/src/raytracing_tutorial_content.md +++ b/docs/src/raytracing_tutorial_content.md @@ -1,8 +1,8 @@ # Ray Tracing with Raycore: Building a Real Ray Tracer -In this tutorial, we'll build a simple but complete ray tracer from scratch using Raycore. We'll start with the absolute basics and progressively add features until we have a ray tracer that produces beautiful images with shadows and materials. +In this tutorial, we'll build a simple but complete ray tracer from scratch using Raycore. We'll start with the absolute basics and progressively add features until we have a ray tracer that produces beautiful images with shadows, materials, and reflections. -By the end, you'll have a working ray tracer that can render complex scenes! +By the end, you'll have a working ray tracer that renders at interactive speeds! ## Setup @@ -18,14 +18,13 @@ using BenchmarkTools * `GeometryBasics` for geometry primitives * `Colors` and `ImageShow` for displaying rendered images -## Part 1: Our Scene - A Playful Cat +## Part 1: Our Scene, The Makie Cat -Let's create a fun scene that we'll use throughout this tutorial. We'll load a cat model and place it in a simple room. +Let's create a fun scene that we'll use throughout this tutorial. ```julia (editor=true, logging=false, output=true) # Load the cat model and rotate it to face the camera cat_mesh = Makie.loadasset("cat.obj") -# Rotate 150 degrees around Y axis so cat faces camera at an angle angle = deg2rad(150f0) rotation = Makie.Quaternionf(0, sin(angle/2), 0, cos(angle/2)) rotated_coords = [rotation * Point3f(v) for v in coordinates(cat_mesh)] @@ -33,7 +32,7 @@ rotated_coords = [rotation * Point3f(v) for v in coordinates(cat_mesh)] # Get bounding box and translate cat to sit on the floor cat_bbox = Rect3f(rotated_coords) floor_y = -1.5f0 -cat_offset = Vec3f(0, floor_y - cat_bbox.origin[2], 0) # Translate so bottom sits on floor +cat_offset = Vec3f(0, floor_y - cat_bbox.origin[2], 0) cat_mesh = GeometryBasics.normal_mesh( [v + cat_offset for v in rotated_coords], @@ -45,7 +44,7 @@ floor = normal_mesh(Rect3f(Vec3f(-5, -1.5, -2), Vec3f(10, 0.01, 10))) back_wall = normal_mesh(Rect3f(Vec3f(-5, -1.5, 8), Vec3f(10, 5, 0.01))) left_wall = normal_mesh(Rect3f(Vec3f(-5, -1.5, -2), Vec3f(0.01, 5, 10))) -# Add a couple of spheres for visual interest (also on the floor) +# Add a couple of spheres for visual interest sphere1 = Tesselation(Sphere(Point3f(-2, -1.5 + 0.8, 2), 0.8f0), 64) sphere2 = Tesselation(Sphere(Point3f(2, -1.5 + 0.6, 1), 0.6f0), 64) @@ -53,548 +52,342 @@ sphere2 = Tesselation(Sphere(Point3f(2, -1.5 + 0.6, 1), 0.6f0), 64) scene_geometry = [cat_mesh, floor, back_wall, left_wall, sphere1, sphere2] bvh = Raycore.BVHAccel(scene_geometry) ``` -**Scene created!** +**Scene created!** Cat model, room geometry, decorative spheres, and BVH for fast ray traversal. - * Cat model with triangulated geometry - * Room geometry: 3 walls - * 2 decorative spheres - * BVH built for fast ray traversal +## Part 2: Helper Functions - Building Blocks -## Part 2: The Simplest Ray Tracer - Binary Hit Detection +Let's define reusable helper functions we'll use throughout: -Let's start super simple: for each pixel, we shoot a ray and color it based on whether we hit something or not. +```julia (editor=true, logging=false, output=true) +# Compute interpolated normal at hit point +function compute_normal(triangle, bary_coords) + v0, v1, v2 = Raycore.normals(triangle) + u, v, w = bary_coords[1], bary_coords[2], bary_coords[3] + return Vec3f(normalize(v0 * u + v1 * v + v2 * w)) +end + +# Generate camera ray for a pixel with optional jitter +function camera_ray(x, y, width, height, camera_pos, focal_length, aspect; jitter=Vec2f(0)) + ndc_x = (2.0f0 * (Float32(x) - 0.5f0 + jitter[1]) / Float32(width) - 1.0f0) * aspect + ndc_y = 1.0f0 - 2.0f0 * (Float32(y) - 0.5f0 + jitter[2]) / Float32(height) + direction = normalize(Vec3f(ndc_x, ndc_y, focal_length)) + return Raycore.Ray(o=camera_pos, d=direction) +end + +# Convert between color representations +to_vec3f(c::RGB) = Vec3f(c.r, c.g, c.b) +to_rgb(v::Vec3f) = RGB{Float32}(v...) +``` +## Part 3: The Simplest Ray Tracer - Depth Visualization ```julia (editor=true, logging=false, output=true) -# Trace helper - runs a callback for each pixel -function trace(f, bvh; width=700, height=300, camera_pos=Point3f(0, -0.9, -2.5), fov=45.0f0, - sky_color=RGB{Float32}(0.5f0, 0.7f0, 1.0f0)) +function trace(f, bvh; width=700, height=300, + camera_pos=Point3f(0, -0.9, -2.5), fov=45.0f0, + sky_color=RGB{Float32}(0.5f0, 0.7f0, 1.0f0), + samples=1, ctx=nothing) img = Matrix{RGB{Float32}}(undef, height, width) - - # Precompute camera parameters aspect = Float32(width / height) focal_length = 1.0f0 / tan(deg2rad(fov / 2)) - for y in 1:height, x in 1:width - # Generate camera ray - ndc_x = (2.0f0 * x / width - 1.0f0) * aspect - ndc_y = 1.0f0 - 2.0f0 * y / height - direction = normalize(Vec3f(ndc_x, ndc_y, focal_length)) - ray = Raycore.Ray(o=camera_pos, d=direction) + Threads.@threads for y in 1:height + for x in 1:width + color_sum = Vec3f(0) + + for _ in 1:samples + jitter = samples > 1 ? rand(Vec2f) : Vec2f(0) + ray = camera_ray(x, y, width, height, camera_pos, focal_length, aspect; jitter) + hit_found, triangle, distance, bary_coords = Raycore.closest_hit(bvh, ray) - # Ray-scene intersection - hit_found, triangle, distance, bary_coords = Raycore.closest_hit(bvh, ray) + color = if hit_found + to_vec3f(f(bvh, ctx, triangle, distance, bary_coords, ray)) + else + to_vec3f(sky_color) + end + color_sum += color + end - # Let the callback decide the color (pass sky_color for misses) - img[y, x] = hit_found ? f(triangle, distance, bary_coords, ray) : sky_color + img[y, x] = to_rgb(color_sum / samples) + end end return img end -# Binary kernel - white if hit -binary_kernel(triangle, distance, bary_coords, ray) = RGB(1.0f0, 1.0f0, 1.0f0) - -trace(binary_kernel, bvh, sky_color=RGB(0.0f0, 0.0f0, 0.0f0)) +# Visualize depth +depth_kernel(bvh, ctx, tri, dist, bary, ray) = RGB(1.0f0 - min(dist / 10.0f0, 1.0f0)) ``` -**Our first render!** Pure silhouette - you can see the cat and spheres. - -## Part 3: Adding Depth - Distance-Based Shading - -Let's make it more interesting by coloring based on distance (depth map). - ```julia (editor=true, logging=false, output=true) -function depth_kernel(triangle, distance, bary_coords, ray) - # Map distance to grayscale (closer = brighter) - normalized_depth = clamp(1.0f0 - (distance - 2.0f0) / 8.0f0, 0.0f0, 1.0f0) - RGB(normalized_depth, normalized_depth, normalized_depth) -end - -trace(depth_kernel, bvh) +@time trace(depth_kernel, bvh, samples=16) ``` -**Depth perception!** Now we can see the 3D structure - closer objects are brighter. +**First render!** Depth visualization shows distance to surfaces. **Much faster with threading and smoother with multi-sampling!** -## Part 4: Surface Normals - The Foundation of Lighting +## Part 5: Lighting with Hard Shadows -To do proper lighting, we need surface normals. Let's compute and visualize them. +Let's add lighting and shadows using a reusable lighting function: ```julia (editor=true, logging=false, output=true) -# Helper to interpolate normals using barycentric coordinates -function compute_normal(triangle, bary_coords) - n1, n2, n3 = triangle.normals - u, v, w = bary_coords - normalize(Vec3f(u * n1 + v * n2 + w * n3)) -end - -function normal_kernel(triangle, distance, bary_coords, ray) - normal = compute_normal(triangle, bary_coords) - # Map normal components [-1,1] to color [0,1] - RGB((normal .+ 1.0f0) ./ 2.0f0...) -end - -trace(normal_kernel, bvh) -``` -**Surface normals visualized!** Each color channel represents a normal component: +# Reusable lighting function with optional shadow sampling +function compute_light( + bvh, point, normal, light_pos, light_intensity, light_color; shadow_samples=1) - * Red = X direction - * Green = Y direction - * Blue = Z direction + light_vec = light_pos - point + light_dist = norm(light_vec) + light_dir = light_vec / light_dist -## Part 5: Basic Lighting - Diffuse Shading - -Now we can add a light source and compute simple diffuse (Lambertian) shading! + diffuse = max(0.0f0, dot(normal, light_dir)) -```julia (editor=true, logging=false, output=true) -light_pos = Point3f(3, 4, -2) -light_intensity = 50.0f0 + # Shadow testing with optional soft shadows + shadow_factor = 0.0f0 + light_radius = 0.2f0 # Size of area light for soft shadows + + for _ in 1:shadow_samples + # For shadow_samples=1, this is just the light position (hard shadow) + # For shadow_samples>1, we sample random points on a disk (soft shadow) + if shadow_samples > 1 + # Random point on disk perpendicular to light direction + offset = (rand(Vec3f) .* 2.0f0 .- 1.0f0) * light_radius + offset = offset - light_dir * dot(offset, light_dir) + shadow_target = light_pos + offset + else + shadow_target = light_pos + end -function diffuse_kernel(triangle, distance, bary_coords, ray) - # Compute hit point and normal - hit_point = ray.o + ray.d * distance - normal = compute_normal(triangle, bary_coords) + shadow_vec = shadow_target - point + shadow_dist = norm(shadow_vec) + shadow_dir = normalize(shadow_vec) - # Light direction and distance - light_dir = light_pos - hit_point - light_distance = norm(light_dir) - light_dir = normalize(light_dir) + shadow_ray = Raycore.Ray(o=point + normal * 0.001f0, d=shadow_dir) + shadow_hit, _, hit_dist, _ = Raycore.any_hit(bvh, shadow_ray) - # Diffuse shading (Lambertian) - diffuse = max(0.0f0, dot(normal, light_dir)) + if !shadow_hit || hit_dist >= shadow_dist + shadow_factor += 1.0f0 + end + end + shadow_factor /= shadow_samples - # Light attenuation (inverse square law) - attenuation = light_intensity / (light_distance * light_distance) - color = diffuse * attenuation + # Compute final light contribution + attenuation = light_intensity / (light_dist * light_dist) + return to_vec3f(light_color) * (diffuse * attenuation * shadow_factor) +end - RGB(color, color, color) +function shadow_kernel(bvh, ctx, tri, dist, bary, ray; shadow_samples=1) + hit_point = ray.o + ray.d * dist + normal = compute_normal(tri, bary) + # Single point light + light_pos = Point3f(3, 4, -2) + light_intensity = 50.0f0 + light_color = RGB{Float32}(1.0f0, 0.9f0, 0.8f0) + # Hard shadows (shadow_samples=1) + light_contrib = compute_light( + bvh, hit_point, normal, light_pos, light_intensity, light_color; + shadow_samples=shadow_samples + ) + ambient = 0.1f0 + + brightness = ambient .+ light_contrib + return to_rgb(brightness) end -trace(diffuse_kernel, bvh) +trace(shadow_kernel, bvh, samples=4) ``` -**Let there be light!** Our scene now has proper shading based on surface orientation. +**Hard shadows working!** Scene has realistic lighting with sharp shadow edges. -## Part 6: Adding Shadows - Shadow Rays +## Part 6: Soft Shadows -Time to add realism with shadows using Raycore's `any_hit` for fast occlusion testing. +Now let's make shadows more realistic by sampling the light as an area light: ```julia (editor=true, logging=false, output=true) -ambient = 0.1f0 # Ambient lighting to prevent pure black shadows - -function shadow_kernel(triangle, distance, bary_coords, ray) - hit_point = ray.o + ray.d * distance - normal = compute_normal(triangle, bary_coords) - - # Light direction - light_dir = light_pos - hit_point - light_distance = norm(light_dir) - light_dir = normalize(light_dir) - - # Diffuse shading - diffuse = max(0.0f0, dot(normal, light_dir)) - - # Shadow ray - offset slightly to avoid self-intersection - shadow_ray_origin = hit_point + normal * 0.001f0 - shadow_ray = Raycore.Ray(o=shadow_ray_origin, d=light_dir) - - # Check if path to light is blocked - shadow_hit, _, shadow_dist, _ = Raycore.any_hit(bvh, shadow_ray) - in_shadow = shadow_hit && shadow_dist < light_distance - - # Final color - color = if in_shadow - ambient # Only ambient in shadow - else - attenuation = light_intensity / (light_distance * light_distance) - ambient + diffuse * attenuation - end - - RGB(color, color, color) -end - -trace(shadow_kernel, bvh) +trace((args...)-> shadow_kernel(args...; shadow_samples=8), bvh, samples=8) ``` -**Shadows!** Notice how objects cast shadows on each other, adding depth and realism. +**Soft shadows!** Much more realistic with smooth penumbra edges. -## Part 7: Multiple Lights +## Part 7: Materials and Multiple Lights -Let's add multiple lights to make the scene more interesting! We'll define a RenderContext to hold lights and materials: +Time to add color and multiple lights: ```julia (editor=true, logging=false, output=true) -# Define a simple point light structure struct PointLight position::Point3f intensity::Float32 color::RGB{Float32} end -# Material structure (for later use) struct Material base_color::RGB{Float32} metallic::Float32 roughness::Float32 end -# Render context holds all scene data struct RenderContext - bvh::Raycore.BVHAccel lights::Vector{PointLight} materials::Vector{Material} ambient::Float32 end -# Create multiple lights +# Create lights and materials lights = [ - PointLight(Point3f(3, 4, -2), 50.0f0, RGB(1.0f0, 0.9f0, 0.8f0)), # Warm main light - PointLight(Point3f(-3, 2, 0), 20.0f0, RGB(0.7f0, 0.8f0, 1.0f0)), # Cool fill light - PointLight(Point3f(0, 5, 5), 15.0f0, RGB(1.0f0, 1.0f0, 1.0f0)) # White back light + PointLight(Point3f(3, 4, -2), 50.0f0, RGB(1.0f0, 0.9f0, 0.8f0)), + PointLight(Point3f(-3, 2, 0), 20.0f0, RGB(0.7f0, 0.8f0, 1.0f0)), + PointLight(Point3f(0, 5, 5), 15.0f0, RGB(1.0f0, 1.0f0, 1.0f0)) ] -# Materials (will use these in Part 8) materials = [ - Material(RGB(0.8f0, 0.6f0, 0.4f0), 0.0f0, 0.8f0), # 1: Cat - Material(RGB(0.3f0, 0.5f0, 0.3f0), 0.0f0, 0.9f0), # 2: Floor - Material(RGB(0.7f0, 0.7f0, 0.8f0), 0.0f0, 0.8f0), # 3: Back wall - Material(RGB(0.8f0, 0.7f0, 0.7f0), 0.0f0, 0.8f0), # 4: Left wall - Material(RGB(0.95f0, 0.64f0, 0.54f0), 1.0f0, 0.1f0), # 5: Sphere 1 - metallic - Material(RGB(0.8f0, 0.8f0, 0.9f0), 1.0f0, 0.0f0) # 6: Sphere 2 - mirror + Material(RGB(0.8f0, 0.6f0, 0.4f0), 0.0f0, 0.8f0), # cat + Material(RGB(0.3f0, 0.5f0, 0.3f0), 0.0f0, 0.9f0), # floor + Material(RGB(0.8f0, 0.6f0, 0.5f0), 0.8f0, 0.05f0), # back wall + Material(RGB(0.7f0, 0.7f0, 0.8f0), 0.0f0, 0.8f0), # left wall + Material(RGB(0.9f0, 0.9f0, 0.9f0), 0.8f0, 0.02f0), # sphere1 - metallic + Material(RGB(0.3f0, 0.6f0, 0.9f0), 0.5f0, 0.3f0), # sphere2 - semi-metallic ] -# Create render context -ctx = RenderContext(bvh, lights, materials, 0.1f0) +ctx = RenderContext(lights, materials, 0.1f0) +nothing ``` -Now we need a new trace function that works with RenderContext: - ```julia (editor=true, logging=false, output=true) -# Trace with RenderContext -function trace_ctx(f, ctx::RenderContext; width=700, height=300,camera_pos=Point3f(0, -0.9, -2.5), fov=45.0f0, - sky_color=RGB{Float32}(0.5f0, 0.7f0, 1.0f0)) - img = Matrix{RGB{Float32}}(undef, height, width) - - aspect = Float32(width / height) - focal_length = 1.0f0 / tan(deg2rad(fov / 2)) - - for y in 1:height, x in 1:width - ndc_x = (2.0f0 * x / width - 1.0f0) * aspect - ndc_y = 1.0f0 - 2.0f0 * y / height - direction = normalize(Vec3f(ndc_x, ndc_y, focal_length)) - ray = Raycore.Ray(o=camera_pos, d=direction) - - hit_found, triangle, distance, bary_coords = Raycore.closest_hit(ctx.bvh, ray) - img[y, x] = hit_found ? f(ctx, triangle, distance, bary_coords, ray) : sky_color - end - - return img -end - -function multi_light_kernel(ctx, triangle, distance, bary_coords, ray) - hit_point = ray.o + ray.d * distance - normal = compute_normal(triangle, bary_coords) - - # Start with ambient (grayscale) - total_color = Vec3f(ctx.ambient, ctx.ambient, ctx.ambient) +# Compute lighting from all lights - reusing our compute_light function! +function compute_multi_light(bvh, ctx, point, normal, mat; shadow_samples=1) + base_color = to_vec3f(mat.base_color) + total_color = base_color * ctx.ambient - # Accumulate contribution from each light for light in ctx.lights - light_vec = light.position - hit_point - light_distance = norm(light_vec) - light_dir = light_vec / light_distance - - diffuse = max(0.0f0, dot(normal, light_dir)) - - shadow_ray = Raycore.Ray(o=hit_point + normal * 0.001f0, d=light_dir) - shadow_hit, _, shadow_dist, _ = Raycore.any_hit(ctx.bvh, shadow_ray) - in_shadow = shadow_hit && shadow_dist < light_distance - - if !in_shadow - attenuation = light.intensity / (light_distance * light_distance) - light_col = Vec3f(light.color.r, light.color.g, light.color.b) - total_color += light_col * (diffuse * attenuation) - end + light_contrib = compute_light(bvh, point, normal, light.position, light.intensity, light.color, shadow_samples=shadow_samples) + total_color += base_color .* light_contrib end - RGB{Float32}(total_color...) + return total_color end -trace_ctx(multi_light_kernel, ctx) -``` -**Multiple lights!** The scene now has three different colored lights creating a more dynamic lighting environment. - -## Part 8: Colored Materials with Multiple Lights - -Now let's combine materials with our multiple lights! - -```julia (editor=true, logging=false, output=true) -function material_multi_light_kernel(ctx, triangle, distance, bary_coords, ray) - hit_point = ray.o + ray.d * distance - normal = compute_normal(triangle, bary_coords) +function material_kernel(bvh, ctx, tri, dist, bary, ray) + hit_point = ray.o + ray.d * dist + normal = compute_normal(tri, bary) + mat = ctx.materials[tri.material_idx] - # Get material from context - mat = ctx.materials[triangle.material_idx] - base_color = Vec3f(mat.base_color.r, mat.base_color.g, mat.base_color.b) - - # Start with ambient - total_color = base_color * ctx.ambient - - # Accumulate contribution from each light - for light in ctx.lights - light_vec = light.position - hit_point - light_distance = norm(light_vec) - light_dir = light_vec / light_distance - - diffuse = max(0.0f0, dot(normal, light_dir)) - - # Shadow test - shadow_ray = Raycore.Ray(o=hit_point + normal * 0.001f0, d=light_dir) - shadow_hit, _, shadow_dist, _ = Raycore.any_hit(ctx.bvh, shadow_ray) - in_shadow = shadow_hit && shadow_dist < light_distance - - if !in_shadow - attenuation = light.intensity / (light_distance * light_distance) - light_col = Vec3f(light.color.r, light.color.g, light.color.b) - total_color += base_color .* light_col * (diffuse * attenuation) - end - end - - RGB{Float32}(total_color...) + color = compute_multi_light(bvh, ctx, hit_point, normal, mat, shadow_samples=2) + return to_rgb(color) end -trace_ctx(material_multi_light_kernel, ctx) +trace(material_kernel, bvh, samples=4, ctx=ctx) ``` -**Colored materials!** - - * Orange/tan cat - * Green floor - * Light blue back wall - * Pink side wall - * Red and blue spheres +**Colorful scene with soft shadows from multiple lights!** Each object has its own material. -## Part 9: Reflective Materials - Mirrors and Metals +## Part 8: Reflections -The materials we defined in Part 7 already have metallic and roughness properties. Let's use them for reflections! +Add simple reflections for metallic surfaces: ```julia (editor=true, logging=false, output=true) -# Helper: compute direct lighting with multiple lights -function compute_multi_light(ctx, point, normal, mat) - base_color = Vec3f(mat.base_color.r, mat.base_color.g, mat.base_color.b) - - # Start with ambient - total_color = base_color * ctx.ambient - - for light in ctx.lights - light_vec = light.position - point - light_distance = norm(light_vec) - light_dir = light_vec / light_distance - - diffuse = max(0.0f0, dot(normal, light_dir)) - - # Shadow test - shadow_ray = Raycore.Ray(o=point + normal * 0.001f0, d=light_dir) - shadow_hit, _, shadow_dist, _ = Raycore.any_hit(ctx.bvh, shadow_ray) - in_shadow = shadow_hit && shadow_dist < light_distance - - if !in_shadow - attenuation = light.intensity / (light_distance * light_distance) - light_col = Vec3f(light.color.r, light.color.g, light.color.b) - total_color += base_color .* light_col * (diffuse * attenuation) - end - end - - return RGB{Float32}(total_color...) -end - -function reflective_kernel(ctx, triangle, distance, bary_coords, ray, sky_color) - hit_point = ray.o + ray.d * distance - normal = compute_normal(triangle, bary_coords) - mat = ctx.materials[triangle.material_idx] +function reflective_kernel(bvh, ctx, tri, dist, bary, ray, sky_color) + hit_point = ray.o + ray.d * dist + normal = compute_normal(tri, bary) + mat = ctx.materials[tri.material_idx] - # Compute direct lighting (diffuse component) - direct_color = compute_multi_light(ctx, hit_point, normal, mat) + # Direct lighting with soft shadows + direct_color = compute_multi_light(bvh, ctx, hit_point, normal, mat, shadow_samples=8) - # Add reflection for metallic materials + # Reflections for metallic surfaces if mat.metallic > 0.0f0 - # Compute reflection direction: reflect outgoing direction about normal - # Note: ray.d points toward surface, but reflect() expects outgoing direction - wo = -ray.d # outgoing direction (away from surface) + wo = -ray.d reflect_dir = Raycore.reflect(wo, normal) - # Add roughness by perturbing reflection direction + # Optional roughness if mat.roughness > 0.0f0 - # Simple roughness: add random offset in tangent space - random_offset = (rand(Vec3f) .* 2.0f0 .- 1.0f0) * mat.roughness - reflect_dir = normalize(reflect_dir + random_offset) + offset = (rand(Vec3f) .* 2.0f0 .- 1.0f0) * mat.roughness + reflect_dir = normalize(reflect_dir + offset) end - # Cast reflection ray (offset to avoid self-intersection) + # Cast reflection ray reflect_ray = Raycore.Ray(o=hit_point + normal * 0.001f0, d=reflect_dir) - refl_hit, refl_tri, refl_dist, refl_bary = Raycore.closest_hit(ctx.bvh, reflect_ray) + refl_hit, refl_tri, refl_dist, refl_bary = Raycore.closest_hit(bvh, reflect_ray) - # Get reflection color reflection_color = if refl_hit refl_point = reflect_ray.o + reflect_ray.d * refl_dist refl_normal = compute_normal(refl_tri, refl_bary) refl_mat = ctx.materials[refl_tri.material_idx] - - # Compute lighting for reflected surface - compute_multi_light(ctx, refl_point, refl_normal, refl_mat) + compute_multi_light(bvh, ctx, refl_point, refl_normal, refl_mat, shadow_samples=1) else - sky_color + to_vec3f(sky_color) end - # Blend between diffuse and reflection based on metallic parameter - return direct_color * (1.0f0 - mat.metallic) + reflection_color * mat.metallic - else - # Pure diffuse material - return direct_color + direct_color = direct_color * (1.0f0 - mat.metallic) + reflection_color * mat.metallic end + + return to_rgb(direct_color) end -trace_ctx(ctx) do ctx, triangle, distance, bary_coords, ray - reflective_kernel(ctx, triangle, distance, bary_coords, ray, RGB(0.5f0, 0.7f0, 1.0f0)) +img = trace(bvh, samples=16, ctx=ctx) do bvh, ctx, tri, dist, bary, ray + reflective_kernel(bvh, ctx, tri, dist, bary, ray, RGB(0.5f0, 0.7f0, 1.0f0)) end ``` -**Reflective materials!** The spheres now have metallic properties: - - * One smooth copper-colored metal with slight roughness - * One perfect mirror reflecting the scene - -Notice how reflections capture both the scene geometry and lighting! - -## Part 10: Multi-threading for Performance - -Let's add multi-threading to make our ray tracer much faster! - ```julia (editor=true, logging=false, output=true) -using BenchmarkTools -function trace_ctx_threaded(f, ctx::RenderContext; width=400, height=300, camera_pos=Point3f(0, 1, -2.5), fov=45.0f0, - sky_color=RGB{Float32}(0.5f0, 0.7f0, 1.0f0)) - img = Matrix{RGB{Float32}}(undef, height, width) +# Tone mapping functions +luminosity(c::RGB{T}) where {T} = (max(c.r, c.g, c.b) + min(c.r, c.g, c.b)) / 2.0f0 - aspect = Float32(width / height) - focal_length = 1.0f0 / tan(deg2rad(fov / 2)) - - Threads.@threads for y in 1:height - for x in 1:width - ndc_x = (2.0f0 * x / width - 1.0f0) * aspect - ndc_y = 1.0f0 - 2.0f0 * y / height - direction = normalize(Vec3f(ndc_x, ndc_y, focal_length)) - ray = Raycore.Ray(o=camera_pos, d=direction) - - hit_found, triangle, distance, bary_coords = Raycore.closest_hit(ctx.bvh, ray) - img[y, x] = hit_found ? f(ctx, triangle, distance, bary_coords, ray) : sky_color - end +function avg_lum(rgb_m, δ::Number=1f-10) + cumsum = 0.0f0 + for pix in rgb_m + cumsum += log10(δ + luminosity(pix)) end - - return img + return 10^(cumsum / (prod(size(rgb_m)))) end -# Benchmark single-threaded vs multi-threaded -b1 = @belapsed trace_ctx(material_multi_light_kernel, ctx, width=800, height=600); - -b2 = @belapsed trace_ctx_threaded(material_multi_light_kernel, ctx, width=800, height=600); -md""" -Threads: $(Threads.nthreads()) - -Single: $(b1) +function tone_mapping(img; a=0.5f0, y=1.0f0, lum=avg_lum(img, 1f-10)) + img_normalized = img .* a .* (1.0f0 / lum) + img_01 = map(col->mapc(c-> clamp(c, 0f0, 1f0), col), img_normalized) + ycorrected = map(col->mapc(c-> c^(1f0 / y), col), img_01) + return ycorrected +end -Multi: $(b2) -""" +tone_mapping(img, a=0.38, y=1.0) ``` -**Performance boost with threading!** The speedup should be close to the number of CPU cores. - -Notice how we can reuse the same kernel function with both `trace_ctx()` and `trace_ctx_threaded()` - this is great for composability! - -## Part 11: Multi-Sampling for Anti-Aliasing - -Let's add multiple samples per pixel with jittered camera rays for smooth anti-aliasing: - ```julia (editor=true, logging=false, output=true) -function trace_ctx_sampled(f, ctx::RenderContext; - width=700, height=300, - camera_pos=Point3f(0, -0.9, -2.5), fov=45.0f0, - sky_color=RGB{Float32}(0.5f0, 0.7f0, 1.0f0), - samples=4) - img = Matrix{RGB{Float32}}(undef, height, width) - - aspect = Float32(width / height) - focal_length = 1.0f0 / tan(deg2rad(fov / 2)) - pixel_size = 1.0f0 / width - - Threads.@threads for y in 1:height - for x in 1:width - # Accumulate multiple samples per pixel using Vec3f for math - color_sum = Vec3f(0.0f0, 0.0f0, 0.0f0) - - for _ in 1:samples - jitter_x = (rand(Float32) - 0.5f0) * pixel_size - jitter_y = (rand(Float32) - 0.5f0) * pixel_size - - ndc_x = (2.0f0 * (x + jitter_x) / width - 1.0f0) * aspect - ndc_y = 1.0f0 - 2.0f0 * (y + jitter_y) / height - direction = normalize(Vec3f(ndc_x, ndc_y, focal_length)) - ray = Raycore.Ray(o=camera_pos, d=direction) - - hit_found, triangle, distance, bary_coords = Raycore.closest_hit(ctx.bvh, ray) - - color = if hit_found - result = f(ctx, triangle, distance, bary_coords, ray) - Vec3f(result.r, result.g, result.b) - else - Vec3f(sky_color.r, sky_color.g, sky_color.b) - end - - color_sum += color - end - - # Average samples and convert back to RGB - avg = color_sum / samples - img[y, x] = RGB{Float32}(avg...) - end - end - - return img -end - -# Render with 16 samples per pixel for smooth anti-aliasing -@time trace_ctx_sampled(ctx, samples=64) do ctx, triangle, distance, bary_coords, ray - reflective_kernel(ctx, triangle, distance, bary_coords, ray, RGB(0.5f0, 0.7f0, 1.0f0)) -end +using JET + +# Get test data +test_ray = camera_ray(350, 150, 700, 300, Point3f(0, -0.9, -2.5), 1.0f0 / tan(deg2rad(45.0f0 / 2)), Float32(700/300)) +hit_found, test_tri, test_dist, test_bary = Raycore.closest_hit(bvh, test_ray) + +# Check kernel type stability (filter to Main module to ignore Base internals) +@test_opt target_modules=(Main,) depth_kernel(bvh, ctx, test_tri, test_dist, test_bary, test_ray) +@test_opt target_modules=(Main,) shadow_kernel(bvh, ctx, test_tri, test_dist, test_bary, test_ray) +@test_opt target_modules=(Main,) material_kernel(bvh, ctx, test_tri, test_dist, test_bary, test_ray) +@test_opt target_modules=(Main,) reflective_kernel(bvh, ctx, test_tri, test_dist, test_bary, test_ray, RGB(0.5f0, 0.7f0, 1.0f0)) +nothing ``` -**Anti-aliased render!** 32 samples per pixel with jittered camera rays eliminate jagged edges. - -## Summary - What We Built - -We created a complete ray tracer that includes: +## Summary -### Features Implemented +We built a complete ray tracer with: -1. **Camera system** - Perspective projection with configurable FOV -2. **Ray-scene intersection** - Using Raycore's BVH for fast traversal -3. **Surface normals** - Smooth shading from vertex normals -4. **Diffuse lighting** - Lambertian shading with distance attenuation -5. **Hard shadows** - Using `any_hit` for efficient occlusion testing -6. **Simple materials** - Per-object color assignment -7. **Multi-threading** - Parallel rendering across CPU cores -8. **Callback-based API** - Flexible `trace()` function for experimentation +**Core Features:** -### Next Steps + * BVH acceleration for fast ray-scene intersections + * Perspective camera with configurable FOV + * Smooth shading from interpolated normals + * Multi-light system with distance attenuation + * **Soft shadows** using area light sampling (via `compute_light` with `shadow_samples`) + * Material system (base color, metallic, roughness) + * Reflections with optional roughness + * ACES tone mapping for HDR -To make this into a full path tracer (like `Trace`), you would add: +**Performance:** - * **Recursive ray tracing** - Reflections and refractions - * **Multiple light sources** - Area lights, environment lighting - * **Advanced materials** - Specular, glossy, transparent - * **Sampling** - Multiple samples per pixel for anti-aliasing - * **Better normal interpolation** - Proper barycentric interpolation - * **GPU support** - Using KernelAbstractions.jl + * Multi-threading for parallel rendering (introduced early!) + * Multi-sampling for anti-aliasing (introduced early!) + * Type-stable kernels for optimal performance + * **Modular, reusable `compute_light` function** - works for both hard and soft shadows -The `Trace` package implements all of these features and more! - -### Key Raycore Functions Used +**Key Raycore Functions:** * `Raycore.BVHAccel(meshes)` - Build acceleration structure * `Raycore.Ray(o=origin, d=direction)` - Create ray * `Raycore.closest_hit(bvh, ray)` - Find nearest intersection - * `Raycore.any_hit(bvh, ray)` - Test for any intersection (fast!) - * `Raycore.vertices(triangle)` - Get triangle vertex positions - * `Raycore.normals(triangle)` - Get triangle vertex normals + * `Raycore.any_hit(bvh, ray)` - Test for any intersection + * `Raycore.reflect(wo, normal)` - Compute reflection direction + +**Key Pattern:** The `compute_light` function is reusable across the entire tutorial: + + * `shadow_samples=1` → hard shadows + * `shadow_samples=4` → soft shadows + +This shows how a well-designed function can handle multiple use cases cleanly! Happy ray tracing! diff --git a/src/bvh.jl b/src/bvh.jl index 02d354f..79d0782 100644 --- a/src/bvh.jl +++ b/src/bvh.jl @@ -291,11 +291,12 @@ Returns: current_node = nodes[current_node_idx] # Test ray against current node's bounding box if intersect_p(current_node.bounds, ray, inv_dir, dir_is_neg) - if !current_node.is_interior && current_node.n_primitives > Int32(0) + local cnprim::Int32 = current_node.n_primitives % Int32 + if !current_node.is_interior && cnprim > Int32(0) # Leaf node - test all primitives offset = current_node.offset % Int32 - for i in Int32(0):(current_node.n_primitives - Int32(1)) + for i in Int32(0):(cnprim - Int32(1)) primitive = primitives[offset + i] # Call the callback for this primitive @@ -364,7 +365,7 @@ Returns: """ @inline function closest_hit(bvh::BVHAccel{P}, ray::AbstractRay, allocator=MemAllocator()) where {P} # Traverse BVH with closest-hit callback - _, _, result = traverse_bvh(closest_hit_callback, bvh, ray, allocator) + _, _, result = @inline traverse_bvh(closest_hit_callback, bvh, ray, allocator) return result::Tuple{Bool, Triangle, Float32, Point3f} end From 63f9ef8ba4ed12cb2dcf1380197d5eb85ada9f02 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Wed, 29 Oct 2025 15:11:46 +0100 Subject: [PATCH 18/20] allocates --- test/test_type_stability.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_type_stability.jl b/test/test_type_stability.jl index c811ed1..4a11b4c 100644 --- a/test/test_type_stability.jl +++ b/test/test_type_stability.jl @@ -416,7 +416,7 @@ end @testset "BVHNode construction" begin b = gen_bounds3() - @test_opt_alloc Raycore.BVHNode(UInt32(0), UInt32(1), b) + @test_opt Raycore.BVHNode(UInt32(0), UInt32(1), b) end @testset "LinearBVH construction" begin From f0c063dcb6151fdc39d550704e1acb4a2d42c57c Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Wed, 29 Oct 2025 17:45:38 +0100 Subject: [PATCH 19/20] some more polish --- .gitignore | 1 + Project.toml | 7 +- README.md | 53 ++-- docs/Project.toml | 1 + docs/make.jl | 5 +- docs/src/.bvh_hit_tests-bbook/meta.toml | 1 - docs/src/.raytracing_tutorial-bbook/meta.toml | 1 - .../meta.toml | 1 - docs/src/basics.png | Bin 0 -> 222771 bytes docs/src/bvh_hit_tests.md | 226 +----------------- docs/src/bvh_hit_tests_content.md | 178 ++++++++++++++ docs/src/index.md | 101 +++----- docs/src/raytracing.png | Bin 0 -> 130215 bytes docs/src/viewfactors.md | 14 ++ docs/src/viewfactors.png | Bin 0 -> 161582 bytes docs/src/viewfactors_content.md | 89 +++++++ ext/RaycoreMakieExt.jl | 12 +- src/Raycore.jl | 13 + src/bvh.jl | 8 +- src/kernels.jl | 18 +- src/triangle_mesh.jl | 8 +- test/runtests.jl | 3 + test/test_type_stability.jl | 1 + 23 files changed, 406 insertions(+), 335 deletions(-) delete mode 100644 docs/src/.bvh_hit_tests-bbook/meta.toml delete mode 100644 docs/src/.raytracing_tutorial-bbook/meta.toml delete mode 100644 docs/src/.raytracing_tutorial_content-bbook/meta.toml create mode 100644 docs/src/basics.png create mode 100644 docs/src/bvh_hit_tests_content.md create mode 100644 docs/src/raytracing.png create mode 100644 docs/src/viewfactors.md create mode 100644 docs/src/viewfactors.png create mode 100644 docs/src/viewfactors_content.md diff --git a/.gitignore b/.gitignore index 58bd82d..c7f37fb 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ node_modules docs/build Manifest*.toml +.*-bbook/ diff --git a/Project.toml b/Project.toml index 4d03889..9f07d89 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Raycore" uuid = "afc56b53-c9a9-482a-a956-d1d800e05559" -authors = ["Anton Smirnov ", "Simon Danisch ", "Simon Danisch "index.md", - "Tutorials" => [ - "Ray Tracing Tutorial" => "raytracing_tutorial.md", + "Examples" => [ "BVH Hit Tests" => "bvh_hit_tests.md", + "Ray Tracing Tutorial" => "raytracing_tutorial.md", + "View Factors and More" => "viewfactors.md", ], ], ) diff --git a/docs/src/.bvh_hit_tests-bbook/meta.toml b/docs/src/.bvh_hit_tests-bbook/meta.toml deleted file mode 100644 index 9f7a875..0000000 --- a/docs/src/.bvh_hit_tests-bbook/meta.toml +++ /dev/null @@ -1 +0,0 @@ -version = "0.1.0" diff --git a/docs/src/.raytracing_tutorial-bbook/meta.toml b/docs/src/.raytracing_tutorial-bbook/meta.toml deleted file mode 100644 index 9f7a875..0000000 --- a/docs/src/.raytracing_tutorial-bbook/meta.toml +++ /dev/null @@ -1 +0,0 @@ -version = "0.1.0" diff --git a/docs/src/.raytracing_tutorial_content-bbook/meta.toml b/docs/src/.raytracing_tutorial_content-bbook/meta.toml deleted file mode 100644 index 9f7a875..0000000 --- a/docs/src/.raytracing_tutorial_content-bbook/meta.toml +++ /dev/null @@ -1 +0,0 @@ -version = "0.1.0" diff --git a/docs/src/basics.png b/docs/src/basics.png new file mode 100644 index 0000000000000000000000000000000000000000..f75577fc5ed48ce47298a0c9316be6cb9cab0d43 GIT binary patch literal 222771 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 literal 0 HcmV?d00001 diff --git a/docs/src/bvh_hit_tests.md b/docs/src/bvh_hit_tests.md index b40b287..18641d2 100644 --- a/docs/src/bvh_hit_tests.md +++ b/docs/src/bvh_hit_tests.md @@ -1,224 +1,14 @@ -# BVH Hit Testing: `closest_hit` vs `any_hit` +# BVH Hit tests -This document tests and visualizes the difference between `closest_hit` and `any_hit` functions in the BVH implementation using the new `RayIntersectionSession` API. - -## Test Setup - -```julia (editor=true, logging=false, output=true) -using Raycore, GeometryBasics, LinearAlgebra -using WGLMakie -using Test +```@setup raytracing using Bonito - -# Create a simple test scene with multiple overlapping primitives -function create_test_scene() - # Three spheres at different distances along the Z-axis - sphere1 = Tesselation(Sphere(Point3f(0, 0, 5), 1.0f0), 20) # Furthest - sphere2 = Tesselation(Sphere(Point3f(0, 0, 3), 1.0f0), 20) # Middle - sphere3 = Tesselation(Sphere(Point3f(0, 0, 1), 1.0f0), 20) # Closest - - bvh = Raycore.BVHAccel([sphere1, sphere2, sphere3]) - return bvh -end - -bvh = create_test_scene() - -DOM.div("✓ Created BVH with $(length(bvh.primitives)) triangles from 3 spheres") -``` -## Test 1: Single Ray Through Center - -Test a ray through the center that passes through all three spheres. - -```julia (editor=true, logging=false, output=true) -# Create a ray with slight offset to avoid hitting triangle vertices exactly -test_ray = Raycore.Ray(o=Point3f(0.1, 0.1, -5), d=Vec3f(0, 0, 1)) - -# Create session with closest_hit -session_closest = RayIntersectionSession(Raycore.closest_hit, [test_ray], bvh) - -# Create session with any_hit for comparison -session_any = RayIntersectionSession(Raycore.any_hit, [test_ray], bvh) - -fig = Figure() - -# Left: closest_hit visualization -plot(fig[1, 1], session_closest) -plot(fig[1, 2], session_any) -Label(fig[0, 1], "closest_hit", fontsize=20, font=:bold, tellwidth=false) -Label(fig[0, 2], "any_hit", fontsize=20, font=:bold, tellwidth=false) - -fig +Bonito.Page() ``` -## Visualization: Single Ray with Makie Recipe - -```julia (editor=true, logging=false, output=true) -# Create a ray with slight offset to avoid hitting triangle vertices exactly -test_ray = Raycore.Ray(o=Point3f(0.1, 0.1, 10), d=Vec3f(0, 0, -1)) - -# Create session with closest_hit -session_closest = RayIntersectionSession(Raycore.closest_hit, [test_ray], bvh) - -# Create session with any_hit for comparison -session_any = RayIntersectionSession(Raycore.any_hit, [test_ray], bvh) - -fig = Figure() -# Left: closest_hit visualization -plot(fig[1, 1], session_closest) -plot(fig[1, 2], session_any) -Label(fig[0, 1], "closest_hit", tellwidth=false) -Label(fig[0, 2], "any_hit", tellwidth=false) - -fig -``` -## Test 2: Multiple Rays from Different Positions - -Test multiple rays to ensure both functions work correctly. - -```julia (editor=true, logging=false, output=true) -# Test rays from different angles (with slight offset to avoid vertex hits) -test_positions = [ - Point3f(0.1, 0.1, -5), # Center - Point3f(0.5, 0.1, -5), # Right offset - Point3f(0.1, 0.5, -5), # Top offset - Point3f(-0.5, 0.1, -5), # Left offset -] - -# Create rays -rays = [Raycore.Ray(o=pos, d=Vec3f(0, 0, 1)) for pos in test_positions] - -# Create session -session_multi = RayIntersectionSession(Raycore.closest_hit, rays, bvh) -fig2 = Figure() -ax = LScene(fig2[1, 1]) - -# Use different colors for each ray -ray_colors = [:purple, :orange, :cyan, :magenta] - -plot!(ax, session_multi; - show_bvh=true, - bvh_alpha=0.3, - ray_colors=ray_colors, - hit_color=:green, - show_hit_points=true, - hit_markersize=0.15, - show_labels=false) - -fig2 -``` -## Visualization: Multiple Rays - -## Test 4: Difference Between any*hit and closest*hit - -Demonstrate that `any_hit` can return different results than `closest_hit`. -```julia (editor=true, logging=false, output=true) -# Create a complex scene with overlapping geometry -# This creates a BVH where traversal order can differ from distance order -using Random -Random.seed!(123) - -complex_spheres = [] - -# Add some large overlapping spheres -push!(complex_spheres, Tesselation(Sphere(Point3f(0, 0, 10), 3.0f0), 20)) -push!(complex_spheres, Tesselation(Sphere(Point3f(0.5, 0, 5), 0.5f0), 15)) -push!(complex_spheres, Tesselation(Sphere(Point3f(-0.5, 0, 15), 1.5f0), 18)) - -# Add many small spheres to create complex BVH structure -for i in 1:30 - x = randn() * 5 - y = randn() * 5 - z = rand(8.0:0.5:12.0) - r = 0.3 + rand() * 0.5 - push!(complex_spheres, Tesselation(Sphere(Point3f(x, y, z), r), 8)) -end - -complex_bvh = Raycore.BVHAccel(complex_spheres) - -# Test rays to find cases where any_hit differs from closest_hit -test_rays = map(1:100) do i - x = (i % 10) * 0.4 - 2.0 - y = div(i-1, 10) * 0.4 - 2.0 - Raycore.Ray(o=Point3f(x, y, -5), d=Vec3f(0, 0, 1)) -end - -session_closest = RayIntersectionSession(Raycore.closest_hit, test_rays, complex_bvh) -session_any = RayIntersectionSession(Raycore.any_hit, test_rays, complex_bvh) -fig = Figure() -# Left: closest_hit visualization -plot(fig[1, 1], session_closest) -plot(fig[1, 2], session_any) -Label(fig[0, 1], "closest_hit", tellwidth=false) -Label(fig[0, 2], "any_hit", tellwidth=false) - -fig - -``` -**Key Findings:** - - * `any_hit` exits on the **first** intersection during BVH traversal (uses `intersect`, doesn't update ray) - * `closest_hit` continues searching and updates ray's `t_max` (uses `intersect_p!`) - * In complex scenes with overlapping geometry, `any_hit` can return hits that are significantly farther - * Both always agree on **whether** a hit occurred (hit vs miss) - * The difference appears when BVH traversal order differs from spatial distance order - -## Performance Comparison - -Compare the performance of `closest_hit` vs `any_hit`. - -```julia (editor=true, logging=false, output=true) -function render_io(obj) - io = IOBuffer() - show(io, MIME"text/plain"(), obj) - printer = BonitoBook.HTMLPrinter(io; root_tag = "span") - str = sprint(io -> show(io, MIME"text/html"(), printer)) - DOM.pre(HTML(str); style="font-size: 10px") +```@example raytracing +using Bonito, BonitoBook, Raycore +App() do + path = normpath(joinpath(dirname(pathof(Raycore)), "..", "docs", "src", "bvh_hit_tests_content.md")) + BonitoBook.InlineBook(path) end ``` -```julia (editor=true, logging=false, output=true) -using BenchmarkTools - -test_ray = Raycore.Ray(o=Point3f(0.1, 0.1, -5), d=Vec3f(0, 0, 1)) - -# Benchmark closest_hit -closest_time = @benchmark Raycore.closest_hit($bvh, $test_ray) - -# Benchmark any_hit -any_time = @benchmark Raycore.any_hit($bvh, $test_ray) - - -perf_table = map([ - ("closest_hit", any_time), - ("any_hit", closest_time), -]) do (method, time_us) - (Method = method, Time_μs = render_io(time_us)) -end -Bonito.Table(perf_table) -``` -## Summary - -This document demonstrated: - -1. **`RayIntersectionSession`** - A convenient struct for managing ray tracing sessions - - * Bundles rays, BVH, hit function, and results together - * Provides helper functions: `hit_count()`, `miss_count()`, `hit_points()`, `hit_distances()` -2. **Makie visualization recipe** - Automatic visualization via `plot(session)` - - * Automatically renders BVH geometry, rays, and hit points - * Customizable colors, transparency, markers, and labels - * Works with any Makie backend (GLMakie, WGLMakie, CairoMakie) -3. **`closest_hit`** correctly identifies the nearest intersection among multiple overlapping primitives - - * Returns: `(hit_found::Bool, hit_primitive::Triangle, distance::Float32, barycentric_coords::Point3f)` - * `distance` is the distance from ray origin to the hit point - * Use `Raycore.sum_mul(bary_coords, primitive.vertices)` to convert to world-space hit point -4. **`any_hit`** efficiently determines if any intersection exists, exiting early - - * Returns: Same format as `closest_hit`: `(hit_found::Bool, hit_primitive::Triangle, distance::Float32, barycentric_coords::Point3f)` - * Can exit early on first hit found, making it faster for occlusion testing -5. Both functions handle miss cases correctly (returning `hit_found=false`) -6. `any_hit` is typically faster than `closest_hit` due to early termination - -All tests passed! ✓ - diff --git a/docs/src/bvh_hit_tests_content.md b/docs/src/bvh_hit_tests_content.md new file mode 100644 index 0000000..04711b2 --- /dev/null +++ b/docs/src/bvh_hit_tests_content.md @@ -0,0 +1,178 @@ +# BVH Hit Testing: `closest_hit` vs `any_hit` + +This document tests and visualizes the difference between `closest_hit` and `any_hit` functions in the BVH implementation using the new `RayIntersectionSession` API. + +## Test Setup + +```julia (editor=true, logging=false, output=true) +using Raycore, GeometryBasics, LinearAlgebra +using WGLMakie +using Test +using Bonito + +# Create a simple test scene with multiple overlapping primitives +function create_test_scene() + # Three spheres at different distances along the Z-axis + sphere1 = Tesselation(Sphere(Point3f(0, 0, 5), 1.0f0), 20) # Furthest + sphere2 = Tesselation(Sphere(Point3f(0, 0, 3), 1.0f0), 20) # Middle + sphere3 = Tesselation(Sphere(Point3f(0, 0, 1), 1.0f0), 20) # Closest + + bvh = Raycore.BVHAccel([sphere1, sphere2, sphere3]) + return bvh +end + +bvh = create_test_scene() +``` +## Test 1: Single Ray Through Center + +Test a ray through the center that passes through all three spheres. + +```julia (editor=true, logging=false, output=true) +# Create a ray with slight offset to avoid hitting triangle vertices exactly +test_ray = Raycore.Ray(o=Point3f(0.1, 0.1, -5), d=Vec3f(0, 0, 1)) + +# Create session with closest_hit +session_closest = RayIntersectionSession(Raycore.closest_hit, [test_ray], bvh) + +# Create session with any_hit for comparison +session_any = RayIntersectionSession(Raycore.any_hit, [test_ray], bvh) + +fig = Figure() + +# Left: closest_hit visualization +plot(fig[1, 1], session_closest; axis=(; show_axis=false)) +plot(fig[1, 2], session_any; axis=(; show_axis=false)) +Label(fig[0, 1], "closest_hit", fontsize=20, font=:bold, tellwidth=false) +Label(fig[0, 2], "any_hit", fontsize=20, font=:bold, tellwidth=false) + +fig +``` +## Test 2: Multiple Rays from Different Positions + +Test multiple rays to ensure both functions work correctly. + +```julia (editor=true, logging=false, output=true) +# Test rays from different angles (with slight offset to avoid vertex hits) +test_positions = map(p-> (p = p.-0.5; Point3f(p..., -5)), rand(Point2f, 10)) +# Create rays +rays = [Raycore.Ray(o=pos, d=Vec3f(0, 0, 1)) for pos in test_positions] + +# Create session +session_multi = RayIntersectionSession(Raycore.closest_hit, rays, bvh) +plot(session_multi; axis=(;show_axis=false)) +``` +## Visualization: Multiple Rays + +## Test 4: Difference Between any*hit and closest*hit + +Demonstrate that `any_hit` can return different results than `closest_hit`. + +```julia (editor=true, logging=false, output=true) +# Create a complex scene with overlapping geometry +# This creates a BVH where traversal order can differ from distance order +using Random +Random.seed!(123) + +complex_spheres = [] + +# Add some large overlapping spheres +push!(complex_spheres, Tesselation(Sphere(Point3f(0, 0, 10), 3.0f0), 20)) +push!(complex_spheres, Tesselation(Sphere(Point3f(0.5, 0, 5), 0.5f0), 15)) +push!(complex_spheres, Tesselation(Sphere(Point3f(-0.5, 0, 15), 1.5f0), 18)) + +# Add many small spheres to create complex BVH structure +for i in 1:30 + x = randn() * 5 + y = randn() * 5 + z = rand(8.0:0.5:12.0) + r = 0.3 + rand() * 0.5 + push!(complex_spheres, Tesselation(Sphere(Point3f(x, y, z), r), 8)) +end + +complex_bvh = Raycore.BVHAccel(complex_spheres) +# Test rays to find cases where any_hit differs from closest_hit +test_rays = map(rand(Point2f, 200)) do p + p = (p .* 14f0) .- 8f0 + Raycore.Ray(o=Point3f(p..., -5), d=Vec3f(0, 0, 1)) +end + +session_closest = RayIntersectionSession(Raycore.closest_hit, test_rays, complex_bvh) +session_any = RayIntersectionSession(Raycore.any_hit, test_rays, complex_bvh) +fig = Figure() +# Left: closest_hit visualization +plot(fig[1, 1], session_closest; axis=(; show_axis=false)) +plot(fig[1, 2], session_any; axis=(; show_axis=false)) +Label(fig[0, 1], "closest_hit", tellwidth=false) +Label(fig[0, 2], "any_hit", tellwidth=false) + +fig + +``` +**Key Findings:** + + * `any_hit` exits on the **first** intersection during BVH traversal (uses `intersect`, doesn't update ray) + * `closest_hit` continues searching and updates ray's `t_max` (uses `intersect_p!`) + * In complex scenes with overlapping geometry, `any_hit` can return hits that are significantly farther + * Both always agree on **whether** a hit occurred (hit vs miss) + * The difference appears when BVH traversal order differs from spatial distance order + +## Performance Comparison + +Compare the performance of `closest_hit` vs `any_hit`. + +```julia (editor=true, logging=false, output=true) +function render_io(obj) + io = IOBuffer() + show(io, MIME"text/plain"(), obj) + printer = BonitoBook.HTMLPrinter(io; root_tag = "span") + str = sprint(io -> show(io, MIME"text/html"(), printer)) + DOM.pre(HTML(str); style="font-size: 10px") +end +``` +```julia (editor=true, logging=false, output=true) +using BenchmarkTools + +test_ray = Raycore.Ray(o=Point3f(0.1, 0.1, -5), d=Vec3f(0, 0, 1)) + +# Benchmark closest_hit +closest_time = @benchmark Raycore.closest_hit($bvh, $test_ray) + +# Benchmark any_hit +any_time = @benchmark Raycore.any_hit($bvh, $test_ray) + + +perf_table = map([ + ("closest_hit", any_time), + ("any_hit", closest_time), +]) do (method, time_us) + (Method = method, Time_μs = render_io(time_us)) +end +Bonito.Table(perf_table) +``` +## Summary + +This document demonstrated: + +1. **`RayIntersectionSession`** - A convenient struct for managing ray tracing sessions + + * Bundles rays, BVH, hit function, and results together + * Provides helper functions: `hit_count()`, `miss_count()`, `hit_points()`, `hit_distances()` +2. **Makie visualization recipe** - Automatic visualization via `plot(session)` + + * Automatically renders BVH geometry, rays, and hit points + * Customizable colors, transparency, markers, and labels + * Works with any Makie backend (GLMakie, WGLMakie, CairoMakie) +3. **`closest_hit`** correctly identifies the nearest intersection among multiple overlapping primitives + + * Returns: `(hit_found::Bool, hit_primitive::Triangle, distance::Float32, barycentric_coords::Point3f)` + * `distance` is the distance from ray origin to the hit point + * Use `Raycore.sum_mul(bary_coords, primitive.vertices)` to convert to world-space hit point +4. **`any_hit`** efficiently determines if any intersection exists, exiting early + + * Returns: Same format as `closest_hit`: `(hit_found::Bool, hit_primitive::Triangle, distance::Float32, barycentric_coords::Point3f)` + * Can exit early on first hit found, making it faster for occlusion testing +5. Both functions handle miss cases correctly (returning `hit_found=false`) +6. `any_hit` is typically faster than `closest_hit` due to early termination + +All tests passed! ✓ + diff --git a/docs/src/index.md b/docs/src/index.md index e492081..96d8d98 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,76 +1,39 @@ # Raycore.jl -```@setup raycaster -using Bonito -Bonito.Page() -``` +High-performance ray-triangle intersection engine with BVH acceleration for CPU and GPU. -```@example raycaster -using Raycore, GeometryBasics, LinearAlgebra -using WGLMakie, FileIO - -function LowSphere(radius, contact=Point3f(0); ntriangles=10) - return Tesselation(Sphere(contact .+ Point3f(0, 0, radius), radius), ntriangles) -end - -ntriangles = 10 -s1 = LowSphere(0.5f0, Point3f(-0.5, 0.0, 0); ntriangles) -s2 = LowSphere(0.3f0, Point3f(1, 0.5, 0); ntriangles) -s3 = LowSphere(0.3f0, Point3f(-0.5, 1, 0); ntriangles) -s4 = LowSphere(0.4f0, Point3f(0, 1.0, 0); ntriangles) -l = 0.5 -floor = Rect3f(-l, -l, -0.01, 2l, 2l, 0.01) -cat = load(Makie.assetpath("cat.obj")) -bvh = Raycore.BVHAccel([s1, s2, s3, s4, cat]); -world_mesh = GeometryBasics.Mesh(bvh) -f, ax, pl = Makie.mesh(world_mesh; color=:teal) -center!(ax.scene) -viewdir = normalize(ax.scene.camera.view_direction[]) - -@time "hitpoints" hitpoints, centroid = Raycore.get_centroid(bvh, viewdir) -@time "illum" illum = Raycore.get_illumination(bvh, viewdir) -@time "viewf_matrix" viewf_matrix = Raycore.view_factors(bvh, rays_per_triangle=1000) -viewfacts = map(i-> Float32(sum(view(viewf_matrix, :, i))), 1:length(bvh.primitives)) -world_mesh = GeometryBasics.Mesh(bvh) -N = length(world_mesh.faces) -areas = map(i-> area(world_mesh.position[world_mesh.faces[i]]), 1:N) -# View factors -f, ax, pl = mesh(world_mesh, color=:teal, figure=(; size=(800, 600)), axis=(; show_axis=false)) -per_face_vf = FaceView((viewfacts), [GLTriangleFace(i) for i in 1:N]) -viewfact_mesh = GeometryBasics.mesh(world_mesh, color=per_face_vf) -pl = Makie.mesh(f[1, 2], - viewfact_mesh, colormap=[:black, :red], axis=(; show_axis=false), - shading=false, highclip=:red, lowclip=:black, colorscale=sqrt,) - -# Centroid -cax, pl = Makie.mesh(f[2, 1], world_mesh, color=(:blue, 0.5), axis=(; show_axis=false), transparency=true) - -eyepos = cax.scene.camera.eyeposition[] -depth = map(x-> norm(x .- eyepos), hitpoints) -meshscatter!(cax, hitpoints, color=depth, colormap=[:gray, :black], markersize=0.01) -meshscatter!(cax, centroid, color=:red, markersize=0.05) - -# Illum -pf = FaceView(100f0 .* (illum ./ areas), [GLTriangleFace(i) for i in 1:N]) -illum_mesh = GeometryBasics.mesh(world_mesh, color=pf) - -Makie.mesh(f[2, 2], illum_mesh, colormap=[:black, :yellow], colorscale=sqrt, shading=false, axis=(; show_axis=false)) - -Label(f[0, 1], "Scene ($(length(bvh.primitives)) triangles)", tellwidth=false, fontsize=20) -Label(f[0, 2], "Viewfactors", tellwidth=false, fontsize=20) -Label(f[3, 1], "Centroid", tellwidth=false, fontsize=20) -Label(f[3, 2], "Illumination", tellwidth=false, fontsize=20) - -f -``` +## Features -```@example raycaster -using Bonito, BonitoBook -App() do - path = normpath(joinpath(dirname(pathof(Raycore)), "..", "docs", "src", "bvh_hit_tests.md")) - BonitoBook.InlineBook(path) -end -``` +- **Fast BVH acceleration** for ray-triangle intersection +- **CPU and GPU support** via KernelAbstractions.jl +- **Analysis tools**: centroid calculation, illumination analysis, view factors for radiosity +- **Makie integration** for visualization + +## Interactive Examples + +### BVH Hit Tests & Basics + +Learn the basics of ray-triangle intersection, BVH construction, and visualization. + +![BVH Basics](basics.png) + +[BVH Hit tests](@ref) + +### Ray Tracing Tutorial + +Build a complete ray tracer from scratch with shadows, materials, reflections, and tone mapping. + +![Ray Tracing](raytracing.png) + +[Ray Tracing with Raycore](@ref) + +### View Factors Analysis + +Calculate view factors, illumination, and centroids for radiosity and thermal analysis. + +![View Factors](viewfactors.png) + +[View Factors and More](@ref) ## Overview diff --git a/docs/src/raytracing.png b/docs/src/raytracing.png new file mode 100644 index 0000000000000000000000000000000000000000..154a27619a41ad64e9cd18aeb9f7ebaf30b696c9 GIT binary patch literal 130215 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≀@bb6dkJ>Ggw%Z<%993=g_a3%9WV3I%`C2@{HdW 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

Ws%;}7JN`{agwZ&aWS#w0 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 Y(xh4po3}5{VTtof9$eb&LiHf`KX*EgjsO4v literal 0 HcmV?d00001 diff --git a/docs/src/viewfactors.md b/docs/src/viewfactors.md new file mode 100644 index 0000000..bebf72b --- /dev/null +++ b/docs/src/viewfactors.md @@ -0,0 +1,14 @@ +# View Factors and More + +```@setup viewfactors_wrapper +using Bonito +Bonito.Page() +``` + +```@example viewfactors_wrapper +using Bonito, BonitoBook, RayCore +App() do + path = normpath(joinpath(dirname(pathof(Raycore)), "..", "docs", "src", "viewfactors_content.md")) + BonitoBook.InlineBook(path) +end +``` diff --git a/docs/src/viewfactors.png b/docs/src/viewfactors.png new file mode 100644 index 0000000000000000000000000000000000000000..010da2f97f5dcb61f55d42690402615076911cdb GIT binary patch literal 161582 zcmeEu_dnNr`1jkEE!pCeGAbi`uk1}`vPU7=Tegf)ib|5bx6HDW8QBpbAtNKQBHY*e ze9yTb_djqy9`_Ho$2pI4;^RGDuj_hV&*$^HUd8BWDU%X05FrRcs;Z)J13_@Z5CrD| zAwGPEMnu&F{&U7v<)#OMkbOY^#d>y?j1j&`;i+is>Ev?9!N$tT7~$g=5#tdMSQk+7 zgP#%qA3wW^i1J?@>Q#VmlcL}DuyMDub4FAYG*!9zi2t;)A_xXiRgly7HD80*t0h_Qejl#cQ zI|H}*(=e?6^OY6MjxYD$PyBGh|M&ML!{jI^|L04WxeXKj?-%D|ON#$Ko}_$@@ZZa@ z_FkmJ{rBUPi1#lDvU$>-DXFN!gHMlU3-k-0R(>fhHY$JbSd*obd7YWSizXIN#AQao#Dsx}`URW(>MIRcjE?RTuNTs)YQ~q z%8{b7vQ1|k{h%P}>EU>bPMx!v&5IUAV`I89vxdmill>M>-K>bpm(kJg^`oQu_jiVr zRn*kN@*L>I+%G+(<#k^B@i~rOfa3glQl!A5McRLVgS6GVffXD1wY8;t>=AC(;BB?_ zy`w(pSe%H4E81)iY6NsJn=#SLJsSP_276cHDmmeZ4lEq2aW@68!* zX=!PG`$3G2-n-g+Q|xSNNeS;C5Pv*5K8o+x`jjemu|1MBvG>_kby?YvgFk;%SB7(Q zwbP}~($hPvZ7gg|H}HF}85bIrr=8<6*0~=Tm}^{VJzQocar2#LVP%8*-OnXY;6v&! z`Ry)YFtX?ee7C+2*WSDJXQN^B(&fuNzC9MJzvkL+R9Y+gEj&@qv1*TW-`{}cF?HGB zm|peqo2tFn`J<(Ou05)wt4sNv_qwAKD{Gc%7CgnO(2p*bU_s&hVt8R}SwYRja02p& z(cRCk8s#X(Gi-NXe5_LAIJPjHs~%bGUZ$GJ9#dCWcNlc;@(qRW^Yii3(aS8+aukb1 zBl+62nj@pcV^ssvSF3LKKmGN~^-G;gdWmu6b?LvqWOxmV z4FfYUR;MS&tCi?uZ+v}gv-b4o;wVN%!L~c$0=De!{uH}^R>Tq>XjLdrFJG(kcHJWu znX>DN_@oTcV~*66Pkt{A6#t%?nQ`CvD%So?Y^3y7U5B;e}9q) zK4LU~*AYvr9?u|@1DAc{vuSQ4Kq-acq0tmelctte_%=Tu{^$3SO~*s(<*7Pe24Sb@ z!`+|rU+$!LB=gx+1fK>qZueeALb8<~e^1nw=KIQE_c;uYxWcB3Y-e?hc(M1HfN;a^ z$CDYqyC($4Kj7G+(O1aB~rp?<6FGQjI6C~ z&X&6|7YzxMC5=4;>w<&#oBYhMeOyH~s$odz@J} ziZwSHyzdPO_GcHGy?ZHeM_hjh*|-`uJUpCpv+6dsEYUem45Ns%ptqYZy5=7zxFGa& z*QEmXl0JM;h}v6Tw)X>g!3#kX_EVbF&%vO*wVP*%h|abe+Rd-N&X+m;tCtqIchgJe z^v^(WoEg7E@dR{7$^i}r1tn3x#%gI`wYZ!0J%sa?MwHfkI@p?j;?59P_Yo~KbD8wUWKFSI>|KD|qS>0e;}ld>Hp_Dw3ZtyS zH@7mze=;w4GcTC<7?v1^!m|lFjuP&!jzOgD9BsDPe2At*oHpOtEsR?PcRV^TfgqWg znfBA~YBC9~S+*k8j$=ef#qEBEe8*ImmSY-(0x*0)`l!$0Nqdn&F)J4r3F7+mGdC2K zuFOD4v7&eACG7_@Lggipb64~UXYc4Q9H2+wp0`d@%RZ)p9I*X4$B#sv7;=n2>m73H@PMWd`l#7X}-nQU1^Bepr z@br9L`q1u|tw79SgWD~(-Yp)BW|G!g`KMo;gsa~fzIC^2JX|eCLSBiuFqqtfl2vKh zhRtQ(NZ0k4B`O+Y(R|;2w15)oOFRGoJ+E6u;E^}HI>FKYX4iCsj}`w*ox&Szo|`l0 zA)-9rzS47d=V#YUiKwm?takcx77F75Jn=QV-g9dcl{i~lTferqufuK(wFC!OyUfn` zQjzhPH%3FK0??DL9-U)iiW9y!Y1tZrRxB)ew6`uLo?(td~)vYFYa z%(TtsvDEFg9~dDaY7|UhrQ9G2klJgG3*ArPR@yShf>28zLAlmQzJ>)5Za-djes!!k z>b!(EibBybF>?7@udg|O6^)KwrG=;p#UWtW7!o#c?s>`vWfCtdJKMnciA->ie!2O1 zsNfw?eSW^VA2`$KC+f4obpQVSUz@?FjKDzzZuhl95px6djwTEX4<9Pj;~-%WB!qEAz+{9$3Xhyv0|h7iAtLalhSLi`U`I45{dh4 z#LYAXG)((d`2GIDY0)fcZEaoQzv~D%+CK3B$V&yJeU@RViKyo?1(1t)s70B;hdP~m zp-hbJTrzS|(@K{<58DL1rR|nWlbsVj@}aBCW^e6_ag8H25Dpy5Dg7OYC^RAp3`U3V5U400@dYR<0jC&zE>jTh)(l;ztM6F7GUZ1 zu5)3lROipfM?~NzYq0OEeHoe4%sV*n?Ms(o`T4oDJ%)-S;rVlXg!4uY;p*qoSXztw zlu&#Pj?Gk6R9e5j^{Bl6$30a#kbyO z6J9kcQ?RteNFRK|@iMOiB>B7jkvBRnPQlumwePmT75##c{4{@}CjZ^*IywnkXOY$y z7C~rmqj@|a`I`jYKM1FD@YDr2tJrC{j57}Na&xVQvJ`!AcmhGsjDU8`b`$gDK`_Ut+4Q&o}p~zG0#gzQD2;%@6QAb_yU`}#KG~gH58}A zmS4;)pG7l8(CzKrn@&z8DPh%+_BX5TpPajD*tg=}T6^(9Ll=gX|L$k5Gh}4m@3dqd zQeR~US|norqmr1GM+N8~tYXZ=hx`DmZ3HX@N7CMF$!H;4-KtMvc%GbGkmqFQg|(;{ zOH+81%y0Lw@{3%9_d0HcCV7M3?>zBx8QR{Y%ZYQWG3CvNMM&*ZG)L^6*|#2D>jQKl zD9=)G(qn)-aUly(qn`usQMI(pSlx%@j*h;!wZU)KONjVwyj@z`R8w#os3bctE;{{q zYZ)rT$MaqIBy^aNb+P-9qJFD|`Ke`NkKcLy6xXa8ftT)?+nQp*Bc@4u>#@`;vDtVL z$&rsDBOAXhDAeaz(+fp{x4F4ltFDw~tjHj7eX5R;7bsIf0Vm39M}+p2fr5g#{OnuE z@B7OV>&;0c8*5g}I7j;Ztdjv2nV?2^)mOsza6=A`jy|r8fcRzcs-U&|bzfI3)9WgB>PDD=r0^ktc)1aUrl!1?(WwrF_ELjR`o8G(>(>)fQXYE! z{A>jPA2YLBn_vbht>=`gg zEF64M)6E!AkE|fx06&y-)t<%mYke-c89pF$%7k1C`s=G-W=el|yi91v-E;0eoZ51L$%MsYM8UzQp7}D0ii(AgLDM*le=h-;J6NxqrJ$va0(PBk*%pR`pad2NpCqKb z+~r;DrH6!o@K_Uh7@8~k-Gl8f^Bk7P%UQT+h*8oCNto3&>(So5g@JSysA-Co)*bpXs`=;qmIpJ9TY{vJDT(0M;$GL`;S-XQBHE+KZGctR z`~7Br&UG^kWDvS=k+&+|f*?KITaSfm_kATlUFijnnNGF6E|Zl1IRNDjAn|}Up+2*L zv;b#S?KQAJ9e~GplvCK70JzG)M%%zX!5`C>(%PsdSzeUz=%S92y#eq`V61d;Ql}H4P1g+2%k4YAE>qgYUfVCUM<7 z%g4uu;u>0&And+1?a#D>xE-z8o2K- z5sBIo^-zvcDT5sBx15rGJIVSQao@Qvg4pyr3yZ^WP7G>S zyn$fV;3h0J@c#Cbu)`K!)7l;4g^|Lzmeni}C?W9vb|;n4Hv z=l)D7VPQj5BZriJ&$sS)=)TZR870liI^Xr!s1D8Es9IFM-uoO&!+nKO*vZwNmhxt$ z^@E~LDBaN?eaiW>TH1%RmC0=q6Ju$4)&6WR;)MX_$i6;ZEiNA&pDSVo{^(T{2+{L2 zDe00$(4SnO68gZSav}Y0)HoXW&Bu!@d}|Bu{HVAP)R7DoROaNMePZp~&+-<-{rxGI z7W!8L)z%$EHBF7hj;TAwrJp@%V;{xF>Wx{qwY6CbzgjFVZ+`mm1#dC8qoNbF++(S| z!e!&&C>E~z!UgH-4iEgLCAbB}eQa3`_QduB1s1)O{@Y^yI|b7Q ztf+7;k2>TF*mH`KR#jCk>dl2*B)bQJW;D)y^Ua4$%@4vqhNhxO=)YG}Mn!jn*I@AA z??>JiC#UB?H;bnZzynI{WP*f!ij7A^Xj%NVvb^_`eWm?TyaFr|+tR1k9`@Rrn$G~B zO?ROtHOV(bqmB-4MSKI&4cUUp*G~7Ih_->^u_}DHzqzTe893ib>M)e`04TA@$3ll(+sw+GVJ ze+81AIis7-NFE#<{O5ZtZ=6UbK0O$8?kmG=HdQ!B(|tjQD;zc+?`oZ=udt~RKVWH(}%ctrg3rDtZEc9|RN)0+^Xj_bMiulQvo0tH{69bwBv6 zG&dI*e>|r3mQFO3`*I1#JG&@8WcJUbNd*A)S4O&tc^fl6xlB~ZnYl30-iIK@mQ{Ps zbr}$ax|K$MLc;Z1$UA8+uD(p}Y_s;6fK4eZ3&#kE~g@3o1{Px-XCH@RqpIwY=L8?<7w&Uq{XCLt_as1@dP(`!Um` zCl$MfPM~SLouWd}-!0|KH{awWT`^Dt@N{uv?|t%hpbpb37Zw(F4FY{_x`7rdw-TfW z6HQ?g*sTy)`QG=5&YVGAt2O7gAApy7#7oU5hd+5}KjQsE{e>JyWx)Y;J=}3X(2_AG zm;-dAU^+WaP_rE%hOD(#=NDXvRLThoMUVy#5 z;{%HV@QW(Jdqq$)C*If*gIHe>9Q%~ku+*gMuScZ*?24Tv9%B5?^D>fQ`qtfd?g1@H z>Aszv+cIEr(fJ?d_qP=Dw|xS_@kwvQN}U3qSZy@lPJ8+^ERjRI==Yw|Bc>S88EDBW zFerXrNgr}gE!|u==+`$n`Ky$ai}(;NB`MBeF7W5`bwFf~R#(t)o7nnOJ%S8CD!5D5 z96bokvX$aTlYuy`JC;ml$l(YH-A5r`2C1P}23hSDu?-7A@@YO=@>}h&UgNx12nHmttjSS6X*hPZ~jiCs*sWI3X!%$KLvs z+xCKLiRs(3u;$9}aAT7^tYJ|5VZqE>!Ny*5ZIY%HzjloRI4k7mKEx(;HL!s!`7XWo zK_H3x%ktTrnW%GNY5u#dzOy;Y zQr>bR4(l8Nx~YJBPF`OAy!TUI;}XNGXpW2)8pJyBgGqAvHC6(GJ zzExK?`KMnG_#<2w*YegKOw&^E6IR91{cjK)tHLRQ4q4J0Bl+aF`;s%UrC+~zK?}b* zx4D_Vv1t#M3KnwbQ>vR22ATtzUGBZEpEnN(u_o=3wJJ z;kA&?AHQ;3Va(;YRPuAYe15X#F5)yl_M`t54g$od*qcXPUA-Mp3Yqv^G7_Evlt8{I z^b7*l89tJ9S+CO}Fg0$p^cF2pRpmr>P19x?a7Sp-JOuaM*$LZ{@gjEZt{~XX&h|~^ z-s_Xm4L+@Yfdql1Py|b4&0m@c zYKL%IZ#9}a{QPH?A2WAAy19ZY4egRo^MkAy3}(7?&g?CS90(dx#CcX~6b~OiOCjd0 zs$w-Gz7d+!zh;Wc$$Czmhz6s;Ay>&k$=(ETeVz~B=lkMY}Oz)lZEcmDB!}L7|JCu ze@fQKBXJqvynIPcoGt&&0(3!8kMd~jt(|tuc>IXFav=!BgKn0>nW&6Rko`RJ7_;#*Gd{31 zkX9hC`V=`Wzw+|(Sb2CpttYV|tLKGVy(sQf8ej9@u?L$$E3h1z9dXbvnR|IBjc@1E zO90xa!9g9TiHf#DqCO|#Z15jZPk3*0c60FgdFPqN7}z&NW&h5ZiJk(ntN# znvWrv7B88*&r%M}mV@8SY2GM`wsoOAeFIk}3~&le_>#PR;E2pXD@9CSN<#v6o<-$8 zUF@o0AG)d<`yp&|b8{6y9oRYDe690q&>y3TRa{?l6n>=(I)zExlMvuXsocED z6~yUoWb{d%!)XO4IEzYHuLAW7^=n)Ah} zKNX3PR^sbjb_s*{L@kJ9L5H(hSy>6Gsd3;8gF$2oMjas`p)6$8ui4WRk@nWdocZ4h zp1D^;W}x0VcrZ&cr*c5K;>}V~r((V{z8&DxZFki`K~WKnJG3=k^nEBl$##dOMe6uL zpDn)?Bos3_;qWWw#G8A;gFy##Q7dJXwgVqO5`*sP9C2P@t7gr5FOUt6EmFH)-{84o zZSVCNnjuJS7g(w-84w_3nVSA~?IET2r{FcA8Fr-r+v%A?$a0XdwfDx?n5DD;EzQ?Z^%+n-geC}-%Ihds zg3|l_nUT34r<@M753Ph>SydWKGW=p7ct&4&Dh39!O#5!JXQz)`;Qjm8tUnNj~u=;qk9{?>N$5L*e#>Phe-QY7oYnb4beL+QE^Z22m0K);$ z0wRRWg{oUrRMFtG>0E!sCMB=X?bc&vscZKSTpycX)nKOr@1j7u+_;j}@pDNGYU4vl z`u^@08T0pp<8GJcfi^FH&gw2Q=dkTHpd zKxkP5t3q9Vw4(z1#dY%yE{gDA12E-fQZ4F}gmEfgHLb5Ou~~vfR;1xRsIEx(X;FXQd3EwxrO#R@yHB|sUg<98=}|Rp+kgz{}LP91#WIq zFpLDfduRY+qVgOlD5(!@X^*f_f~)VEp?%q1nS2u|*bPIb8UxG}6mU)IoJ*xehG~y@ z3`^*k19x*3DdN7S7xG85tNf zxS?A?rgzy49JN|`hdPe6@nRw(BL8noEkJ!ts_jF8B_S0}HVxoo-alHeGp@Q#3Z(A= zlov5>owAYqK(PNfrwg+b6q2gIDk2uR^9dK>g2n-OwT>pn;7Sa^z{Jr8^JR3R1(pdG z@}jgfGwdk8_f~YC1IlE83Zm_G7-lgl_4~7ss9h9vZ>qKfXb6}L;eb~KyP*t%_TSzR ziAzD5I)MJp6EL8ZSpv^N|JSX7ET`D@BcP#e&r@hjwZYK7XoIk@q@~+xPPADAh8`FO_^9~`yve37na{Y% zzh>)37Sqz)Um@8fO|YG_^4I~EzTQ*K9|E~G*0>19PN5^+@q_Q6a~x-?uR@)&A1k6p z-8jmNO2FfV^TB3+bvW@1YL4~xLQk@!-7o49KNH&w_J&HlUalaDQO*ucqeP$z}A9fxN`R0$D-5bIja81Fwk8r=fU-j77Rc ztz^A8XmzwOJE$3~hZ{@rWl&FpXh<0ZU6Z$Kp`j1H7+%Qw&qMGG-OUI+!BBwOjZ=Yl z6X=sSJvLVD`6}i}DCL=;iI5PO71`A)x(c@6z)sdIM6%IOl4TrIA8Q+%vCWL;yU88a zZLm-1b8Ea3?st8#z9j=T*a2K!pL!4|<7Q+LAfA1IJHKVHfLcJZ%hd_(&Uj5nbHVpx6&Ifz^q+;8 zKs$&aN|?Ze6A^CEhuS05$+wYryBd1Y20}?+@T#z;sw;XDIp)E~6A;dn4aG*Aw6{N{ z7SilWLw~>B{j7o*bvUMX2z)k!2VKCU(N?HD`8euf7|4xgDH88?*}$%i5qF8q-k;wi zZ-inv580@qscB$90IJ3yvjZIJ7aNC?=u1MskI(u;v<7cIRFIw=d>n`g4WC;F=x_Cl z%zl8M3mg;pfMU?!O*($0zUMev5DMmWp;?jX`90zdY4EZBktITR^JboGZExSeNrFOI zc;n8+*p4t*GVBV{lATQ~%Ugn`vs*R*kj{16X%aqVMdJL=|4c<#{CM#Ye5OiOhh3k0m=o^-@t)ZM*nb z^82sO3HVReOdR;pq=4FFnj>%Y`)ymeWc_0?E??este06GzEc3;oeerF&D?E<^`8Ui zhToTirHJq6F(@(CE$yfR*SStEG1~Hd#2LiNX1S!~*$%k>Ht1UkIVQFLpxT2eF9YM{ z#l_vhE9Eh7sM2$j<^zAV5jbAm1D=)%ZF?A;)R|IRRlud?F>O6Po;{s|86U)Q>d$g% z-OPk9>wINt*Tw+Rd93742MD3TL1di1`(wQI4d9(&G)Lm2>Xh})f7{DL3;{p zY;4q~mkvA>HQ4>o*@G8!m2$rhzG#pJtPoOtD%9l%(x66 zpRR%LSeY3kT5!R);g^J7(tL%l+il>C^=1K=i=pynr|h||FM zPLcPUEa>Js9_K={mK+RrS9@(BexP(KZF|nvQLC>E1*Zegdl+aP_am%$wAHG5@SB<+a+cVpY6s5z=%SpJA@Wz9c#qn`eFJh9^@4k z0{z2BFmfkWN?Q$;E8@WetO|qicduq#T7)lI2G}UGBzARmWe1<0Xt|iOv$0tLv2;u^ z1mQILO7&%CdHFd;##k^W-!JZ?QVi|2Arn_J`6t=pIhBFYS5Pc^>g8!rLK6ZDgM6^% zR|TUg7Yvo*w;p#;uXwb`Kphq$|M1}OuorCCI1x5*i=RV_#6bu3N)(Uk!z0FPF6c~ew5+| zIKjdg6zM+!HDXhx+E5L!bEP|lu!^Ggp6 zLZHD4HFyLB5`p*LQC~GIID&TByMM-O>0>)sp(kosj)g3Y8CeK{GPvb$)6Nof&^A}8nS8BnLJ{rH4=w>6NX;SGY`pMbm0SFj zlqeHMv*x&RWz`;j4ElwU_ZbYp?pi;ciESElG~m?&(IY4v8_msBMP3@G43jKXOxQ8e zsgiy-pgMFo34erXmH2+

PZ|n?O4W2}>NTRSo0-kI=G+3WAP|DyYaijT)-Z{(%*W zh5^En8f0NendKQY2IzScf!*aEReAc@vJslVurX1<4my^~YE2x5PCCnW%ISc43(>hU z+>yBsdT6-PMmfK$hy@*Sc4_{j1hY#y;0>(a2W>WXZ&^+UdK$`d32>i}ZDWw+xiA5) z^%$#yG)y~H^a6|q)*>&-!@$a11SIl4s=~(rd3g5kw8)f=_U2oBSr~1I@jNA`7j{ZU z5@6`3cLL<}+fhi*AHPg0U>3xN_QlRMbjG=pow5>75D?45zjWxIe$TIV^7t2GK+bhT zpTa||&9=-=D1vdYcI^{NuPbBUfWR0Vpu>%cW|9dqg(5k(W4xOU6!Z;@VBM^9ehpz! z^c&LKtNt}CzY1uL*Jb06LB+fjba>h>#L5ayy5UTDf~aN?aNf&_2?-eJVXN$uHo=^5 zcUw5Y`LdA}3k!d_gy`-@zu!#Q+(ydKwlEE1iE+PD3X|mD9f`1XwJx!ujdx*}nK&4> z1Op5Qd<`XNV53E06v`p2r#v92Y1vOJKoV+5&G+CEy8S(HgSwajf)j);>K&jSY&;^- z-2I^kz1CT)aSkI?u|ade8gzKQ?-naI4h}l931N5!czEBhJ46$`aWKyr0T}^20Ub9+ zsie983fqf@+o_^%*kIpjZvo@M(9_Myv4q(_`?P1^d zU_u#vY9-|CmS7&?zt&kWz-`IUIZoCx=1syK6seA(vIOv?8NLOXUk< zpe03BZmC34$5n2kUULH)P3ja0H}Em=GWaC;;!GgaTXmVf>u_$Eq|u_ zVlNME3mG|i7f@()(gvCUtidOL;f%>ybc_=G!FR{>tHiPd`1rT$66GRgLvi;ij+56mVIemOi$DUs#6D z2sB<6tG&q=$qp^|%5$O|SEkFOGk`Uqvm9{zWWJ)^)KGGH)ADv1?56|@^hrEs3=q1` z1YZEDHf(=*ad32h+EVbIc|K&Z8+cs25w&bJSM_2!jo{C~U%9e=o|HVhqj_||hdKDK zo740=Y8V3$7S`kWRe^7^(Rj4c07IJ$(D^B=z3XK+kR}B^4{YSI&l%N$aOr&kk7Gzq ztO*+NJE+lk^U2Hc*QpqZE~+nnx0Hg%_IJzQv1CO(7_ek@zY3$szF?qg%*s@GBhH0XRL^p9iEaa(9c!4cJ?g5aYAx(0t7vx0BC^?zkH^6>>->b`3{pG z24k{7Gj)GbE#jjC@z-wm(P^Xzo`DjQhcXHX;Vv6EE=F$x(^N% zWFGX#jvD{1HUCT0DMW={XCBj4c6NNP)loSR<&`in`uqV90!rO7+r9mL@<3>S$1lZ1 zCqbTJBj}J+LIMRY9v;0v#k5h53MGU%0n`n>CGZ1*x5mN2qzve%pzbsu%(FKXyElXP z-!?u@YY}`by zt){vf9)B%9+)DD^oIMInh&FKPJ10fLAo<;bBr=*Gs3RICKnDfaWB(vKNH@>UkidBk zXGRi1`xJc2s|q);Dklv{=SIi0A$Z+99 zOy%`Unli;AGe?q%f~30)Eqm$Fn$xC3vMjagdiAql>)4?bj~x#(Y{yQBozSt0kw`g{ zavNU(^Zj7g?H%1B+kdNZ(l$&S;tBVW<5FGqKx)4!maO1jzX{)aNfx%zUmtmX@_0;c=W*A+!L-cMEsbD1T}Y4(?^*jvfgQtk1};=M5TBYKo7+N_oNu~8 z_oFbbC|odcoh2XXUY;4Zq$PseAHyufKUl+hNbr)cjpoEwA1}w_Ch0<$@kqwYMy$I< zm6z3-ISJo&6^eJyBi(6c8FqvbdEDoNTjG~l{yU{RBok451=GK_#E)R}m*KxzGHN6% zQ4M~d@)W7eDfn%z8ESkleJXuG#T!=+fy+A8*=gc17pV2Pt~cq}|N1p!DieL8i>~8z z)#76rCzc$65c`{zSzGAjz3!uL6*LKB*mq6BrmkRK=gTEnY>8j{LmiQG3A6k8vP~70 z6vZ3zn6&|%vT#iQg2$;%)pQrT%ZsH=s;N;C_0^`MKJXM`-7<`I%T>Kd7-krEQ%0}V zo?R4&mFLu+-0otTV4%vDHoQP#NgTG*uE&0ho?noZ`>LVBMalLA;Uv&uH(fy&zc8n zppf=@heD-c$wr~d-weL#8Yj*tFa$Gy zUYP3SxoJa-KP~fYOsCrm^Wj>wy9Atn-g!(_dz&;QAakGz;T+jH&$))f;^H#Zvf-VN z47;=+W9&VT_DJB6Nc3`OAq6Lu0pvqb=+_6bt>H#^D{=`u>wA7Vo#}Nyw-D0^xp#e0 z*UQ>7HfiGL1o?VvV9$nRlqSA+5SmqDbLzBCEQm=R@EQKKXZW_b==+JPEbKyq2ie&5 z#gE0mQGlTzl7~1U=)XEogvlN~@aWW2^S_X=A@?3*LxGE7EDm2Vr&~y4#`R++f-ncH zY4wTi5>36>%MEwZEu*dVCHhYtH$0f41qS^GNe!A=aLRi}*ioZ#%vtX+ko2-e*kq zC26SV%`@SWXTm(mtAi4ZO?)u4`_tqVqjzLWC)hwlH)0$hCgn)Ufh z454qrYDU~R@pi;do#m4Wm(yHo@1+=b;i{IF;AJ?R;UcN6kdz(4Z60Ts9`uk~`SFa5 zgYeXQ+E~0_TW^DB^iqD@B*gXoJWxMkZ1}}2_{R^;zjTzLrTN;~PMOy^Zxtk%DZ^&< zUX5&OVj%El5={GaR{g2Rh1T#0Sz(5S8F7iUUp&cgr}cz*WqcQ}l2y520J@?cWl!6H~iu3;Wjt9H@go^1oUA)c;t+e6sCq0}oO$Sq`{NSp)n?c|d={nn+4 z%KRv;GvD@3pdn6?k=-p$LB9ty{f3s8RHM?XXg?o3H+lKb>+Z37~J!DpC-F@fy zT5iP0<@0x>Fhwme4t0)f*jY7Y#EJ6ozwU%}AGcHlD_4c*Y6!A_fda|NyKQy`bj>-_$IxA81q$tGiA4%ShzVX2)&;lR&(>CF}a&(g4g^^9Bh+xy%KtGxnGy7 zD||I1!1`(hBL&OpYdW4x@cvlHrA3}gi)D^g?9j0nA1mn%K?%l?`P? zN zpCnQRv`7WszzREly;&EUAkE#kc8j99%`2||3s2K!YxddJC5m_aZ~ysIC@-_LzUX!OYcI0L z$(t}#5V@@xz8@ZRFXWzPrsqt*qtLySe~Rhi*|O^x?c~2i??vjLPjP?a?x{O{LqUl2 zVRdY3QXL)n;ysUa8FIY#2cO}UCF5%;Qvr4^ku+Fy=D)mWS+ls@2}ApCw@w|k$zxi* zCBDp=%i2bkZY#@t9TvFmoNHUITlw7co6vZ}=&f!T<3nvL{ufv-aUS3w#0kH_# zFVqMYIX5nUu94M37JpaboPxJL-pW@4f(OoDqq#Y>8Bh(TOD4!FNEDSdAD3U%7gUt& zqw}Rj5_k9|&O=7+SKgw37QH-2e5;Wz^W$^7*&XiJ zb^G#XLchB6*}F>5r`6?Qu~Xd;Fh2OA=TCQub>D}xK}0c*SQ+lM=mhce@iQ!+_U`Aw zUTL!Ri8Dxu5Fgo7`v%^-SwA1-R=pC#Ojym?kKf(_tZ-{-le;3&j6V24I5=RLv0zf{v%>*?gpRsN{yjnv)G_x!KS6T`QG~6i>yZVdyRu}foi(j~JNz{IbGqF+q=@sr*gF7u79^vrYk>OC+z--Lm z*+6u?&ncKa&R^rYjqD6*zwgiCKJ&W>b9{{AE`i%eN7ENt>GVdDms6w6?^F5&?l&Ct z9nGeTKf(}(VWM~2t{q}~T{jP;{_rE-6%j2@#bZ(D2(Kn#$i)m7W;7an;5pYQ`oP*v z?M!Aa6-EDCvvXp==3AB{=SHF( zgG3eSxE86SCfGj2Wx@c0INX`g`uw&)%T9$z-x~V$*5Ry{Plh^FD%)}P=?s%?fe)^5Y@0nB+0Epo5+r4#+ipQ z(!VYTB*Sy4;ZoxAxo?QOB2huVa4g-sSq@Sk>tlD6ydfzeoxks|`p4>7^ve$`m``pT zxnci2#Pn8xkF*UrDYf6OLE+I{8MkGu!LQ$AqrS}IQi3Jto|v)Ln)pAFIM$ii_CEOC zb)o&UO2puk_S5Kq&E|Bq57ESAF_6Dd(Oh8qNwrkTQ@P?Na(6lZ zS#c?;vskuzq}T!W1uC;$uVs!${%5JbWB-0up1$0`tJ9Ke^<7Hj&O=PUqn%Dma#-}U zuF%C?p=T0JH;%eorsR=G-U#M^IrTjXxe|K$jR`dh+$XLtw6MQ$abxYilbVz$exefi z{T$KD;k-NiheV67U6R#%Uv*!LaH>HryDj;&sH^u<;mT2bz_b1KV^C}JcY32>f~xC! zE`=U-fUoVZN(cC-o{=Z?_a~qBw{(golzDnB~za_ZQ6Wa|Azqr%i!A$|PR`?kZJ-hz|1{Tao_HuxnyxymzNs%lsLyJ+Zh_``P!^ z8^wf}=hurVukX)z$n;>ohw*)#6@~Di5Q^Hk6e?JgqDHXW%b<>teHOXffnay%w2Gz1 z-FOoeqbIxf(01tU=HZ6V$UNK&wkB0~9}nAK9miooAMd2*#bkSSu61t0=S+%Tb-XVn z!tj?1NL>nxCdNU-CbBovrt9gC!KQCVYWRPen>gZ^O5ftH600Lv$7e8!V>w~&cnN20 z>VG%Pve7G`FLSpW6Bvr0IWCy+Bdj}vi}gapy2P543n9Md8Qm+<^%Nv0Q}}We5sJ

cS#B%z#C+ha{WCku%OxJvqRN@d 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/src/viewfactors_content.md b/docs/src/viewfactors_content.md new file mode 100644 index 0000000..69d50eb --- /dev/null +++ b/docs/src/viewfactors_content.md @@ -0,0 +1,89 @@ +# View Factors Analysis + +This example demonstrates Raycore's analysis capabilities for radiosity and illumination calculations. + +## Scene Setup + +```julia (editor=true, logging=false, output=true) +using Raycore, GeometryBasics, LinearAlgebra +using WGLMakie, FileIO + +function LowSphere(radius, contact=Point3f(0); ntriangles=10) + return Tesselation(Sphere(contact .+ Point3f(0, 0, radius), radius), ntriangles) +end + +# Create scene with multiple objects +ntriangles = 10 +s1 = LowSphere(0.5f0, Point3f(-0.5, 0.0, 0); ntriangles) +s2 = LowSphere(0.3f0, Point3f(1, 0.5, 0); ntriangles) +s3 = LowSphere(0.3f0, Point3f(-0.5, 1, 0); ntriangles) +s4 = LowSphere(0.4f0, Point3f(0, 1.0, 0); ntriangles) +cat = load(Makie.assetpath("cat.obj")) + +# Build BVH acceleration structure +bvh = BVHAccel([s1, s2, s3, s4, cat]) +world_mesh = GeometryBasics.Mesh(bvh) + +# Visualize the scene +f, ax, pl = mesh(world_mesh, color=:teal, axis=(show_axis=false,)) +center!(ax.scene) +f +``` +## View Factors + +View factors quantify how much each surface "sees" every other surface - essential for radiosity calculations. + +```julia (editor=true, logging=false, output=true) +# Calculate view factors between all faces +viewf_matrix = view_factors(bvh, rays_per_triangle=1000) + +# Sum up total view factor per face +viewfacts = map(i -> Float32(sum(view(viewf_matrix, :, i))), 1:length(bvh.primitives)) +N = length(world_mesh.faces) + +# Visualize +per_face_vf = FaceView(viewfacts, [GLTriangleFace(i) for i in 1:N]) +viewfact_mesh = GeometryBasics.mesh(world_mesh, color=per_face_vf) +mesh(viewfact_mesh, colormap=:turbo, shading=false, + lowclip=:black, colorrange=(0f0, maximum(viewfacts)), + axis=(show_axis=false,)) +``` +Higher values (warm colors) indicate faces that see more of the surrounding geometry. + +## Illumination + +Calculate how much each face is exposed to rays from a specific viewing direction. + +```julia (editor=true, logging=false, output=true) +# Get camera view direction +viewdir = normalize(ax.scene.camera.view_direction[]) + +# Compute illumination +illum = get_illumination(bvh, viewdir) + +# Visualize +pf = FaceView(illum, [GLTriangleFace(i) for i in 1:N]) +illum_mesh = GeometryBasics.mesh(world_mesh, color=pf) +mesh(illum_mesh, colormap=[:black, :yellow], colorscale=sqrt, + shading=false, axis=(show_axis=false,)) +``` +Faces directly visible from the viewing direction show higher illumination (yellow). + +## Centroid Calculation + +Find the average position of visible surface points from a given direction. + +```julia (editor=true, logging=false, output=true) +# Calculate centroid +hitpoints, centroid = get_centroid(bvh, viewdir) + +# Visualize +f, ax, pl = mesh(world_mesh, color=(:blue, 0.5), transparency=true, axis=(show_axis=false,)) +eyepos = ax.scene.camera.eyeposition[] +depth = map(x -> norm(x .- eyepos), hitpoints) +meshscatter!(ax, hitpoints, color=depth, colormap=[:gray, :black], markersize=0.01) +meshscatter!(ax, [centroid], color=:red, markersize=0.05) +f +``` +The red sphere marks the centroid - useful for camera placement and focus calculations. + diff --git a/ext/RaycoreMakieExt.jl b/ext/RaycoreMakieExt.jl index 9e70b90..597e343 100644 --- a/ext/RaycoreMakieExt.jl +++ b/ext/RaycoreMakieExt.jl @@ -47,15 +47,15 @@ plot(session) @recipe(RayPlot, session) do scene Attributes( show_bvh = true, - bvh_alpha = 0.4, - bvh_colors = [:red, :yellow, :blue], + bvh_alpha = 1.0, + bvh_colors = Makie.wong_colors(), ray_colors = nothing, - ray_color = :black, + ray_color = :green, hit_color = :green, - miss_color = :gray, + miss_color = (:gray, 0.5), ray_length = 15.0f0, show_hit_points = true, - hit_markersize = 0.2, + hit_markersize = 0.1, show_labels = false, ) end @@ -223,7 +223,7 @@ function draw_bvh!(plot, bvh::Raycore.BVHAccel, colors, alpha) plot, mesh_obj, color = (color, alpha), - transparency = true + transparency=alpha < 1.0 ) end end diff --git a/src/Raycore.jl b/src/Raycore.jl index dc2a77f..02d67b0 100644 --- a/src/Raycore.jl +++ b/src/Raycore.jl @@ -48,6 +48,19 @@ include("kernel-abstractions.jl") include("kernels.jl") include("ray_intersection_session.jl") +# Core types +export Ray, RayDifferentials, Triangle, TriangleMesh, BVHAccel, Bounds3, Normal3f + +# Ray intersection functions +export closest_hit, any_hit, world_bound + +# Math utilities +export reflect + +# Analysis functions (key features) +export get_centroid, get_illumination, view_factors + +# Ray intersection session export RayIntersectionSession, hit_points, hit_distances, hit_count, miss_count end diff --git a/src/bvh.jl b/src/bvh.jl index 79d0782..0b47f0b 100644 --- a/src/bvh.jl +++ b/src/bvh.jl @@ -90,7 +90,6 @@ function to_triangle_mesh(x::GeometryBasics.AbstractGeometry) return TriangleMesh(m) end - function BVHAccel( primitives::AbstractVector{P}, max_node_primitives::Integer=1, ) where {P} @@ -99,10 +98,13 @@ function BVHAccel( triangle_mesh = to_triangle_mesh(prim) vertices = triangle_mesh.vertices for i in 1:div(length(triangle_mesh.indices), 3) - push!(triangles, Triangle(triangle_mesh, i, mi)) + push!(triangles, Triangle(triangle_mesh, i, mi, length(triangles) + 1)) end end ordered_primitives, max_prim, nodes = primitives_to_bvh(triangles, max_node_primitives) + ordered_primitives = map(enumerate(ordered_primitives)) do (i, tri) + Triangle(tri, primitive_idx=UInt32(i)) + end return BVHAccel(ordered_primitives, UInt8(max_prim), nodes) end @@ -489,7 +491,7 @@ end function GeometryBasics.Mesh(bvh::BVHAccel) points = Point3f[] faces = GLTriangleFace[] - prims = sort(bvh.primitives; by=x -> x.material_idx) + prims = bvh.primitives# sort(bvh.primitives; by=x -> x.material_idx) for (ti, tringle) in enumerate(prims) push!(points, tringle.vertices...) tt = ((ti - 1) * 3) + 1 diff --git a/src/kernels.jl b/src/kernels.jl index db1b8e1..53ebbcc 100644 --- a/src/kernels.jl +++ b/src/kernels.jl @@ -14,7 +14,7 @@ function hits_from_grid(bvh, viewdir; grid_size=32) ray = Raycore.Ray(; o=o, d=ray_direction) hit, prim, dist, bary = Raycore.closest_hit(bvh, ray) hitpoint = sum_mul(bary, prim.vertices) - @inbounds result[idx] = RayHit(hit, hitpoint, prim.material_idx) + @inbounds result[idx] = RayHit(hit, hitpoint, prim.primitive_idx) end return result end @@ -29,16 +29,15 @@ function view_factors!(result, bvh, rays_per_triangle=10000) @inbounds begin triangle = bvh.primitives[idx] n = GB.orthogonal_vector(Vec3f, GB.Triangle(triangle.vertices...)) - normal = normalize(Vec3f(n)) + normal = normalize(n) u, v = get_orthogonal_basis(normal) for i in 1:rays_per_triangle point_on_triangle = random_triangle_point(triangle) o = point_on_triangle .+ (normal .* 0.01f0) # Offset so it doesn't self intersect ray = Ray(; o=o, d=random_hemisphere_uniform(normal, u, v)) hit, prim, dist, _ = closest_hit(bvh, ray) - if hit && prim.material_idx != triangle.material_idx - # weigh by angle? - result[triangle.material_idx, prim.material_idx] += 1 + if hit && prim.primitive_idx != triangle.primitive_idx + result[triangle.primitive_idx, prim.primitive_idx] += UInt32(1) end end end @@ -46,7 +45,6 @@ function view_factors!(result, bvh, rays_per_triangle=10000) return result end - function get_centroid(bvh, viewdir; grid_size=32) # Calculate grid bounds hits = hits_from_grid(bvh, viewdir; grid_size=grid_size) @@ -54,18 +52,14 @@ function get_centroid(bvh, viewdir; grid_size=32) return surface_points, mean(surface_points) end - function get_illumination(bvh, viewdir; grid_size=1000) # Calculate grid bounds hits = hits_from_grid(bvh, viewdir; grid_size=grid_size) result = Dict{UInt32, Float32}() for hit in hits if hit.hit - if haskey(result, hit.prim_idx) - result[hit.prim_idx] += 1f0 - else - result[hit.prim_idx] = 1f0 - end + count = get!(result, hit.prim_idx, 0f0) + result[hit.prim_idx] = count + 1f0 end end return [get(result, UInt32(idx), 0.0f0) for idx in 1:length(bvh.primitives)] diff --git a/src/triangle_mesh.jl b/src/triangle_mesh.jl index 57e6d86..c0cc527 100644 --- a/src/triangle_mesh.jl +++ b/src/triangle_mesh.jl @@ -32,9 +32,13 @@ struct Triangle <: AbstractShape tangents::SVector{3,Vec3f} uv::SVector{3,Point2f} material_idx::UInt32 + primitive_idx::UInt32 end -function Triangle(m::TriangleMesh, face_indx, material_idx=0) +Triangle(tri::Triangle; material_idx=tri.material_idx, primitive_idx=tri.primitive_idx) = + Triangle(tri.vertices, tri.normals, tri.tangents, tri.uv, material_idx, primitive_idx) + +function Triangle(m::TriangleMesh, face_indx, material_idx=0, primidx=0) f_idx = 1 + (3 * (face_indx - 1)) vs = @SVector [m.vertices[m.indices[f_idx + i]] for i in 0:2] ns = @SVector [m.normals[m.indices[f_idx + i]] for i in 0:2] # Every mesh should have normals!? @@ -48,7 +52,7 @@ function Triangle(m::TriangleMesh, face_indx, material_idx=0) else uv = SVector(Point2f(0), Point2f(1, 0), Point2f(1, 1)) end - return Triangle(vs, ns, ts, uv, material_idx) + return Triangle(vs, ns, ts, uv, material_idx, primidx) end function TriangleMesh(mesh::GeometryBasics.Mesh) diff --git a/test/runtests.jl b/test/runtests.jl index c9dd81f..84d02d8 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -3,6 +3,9 @@ using GeometryBasics using LinearAlgebra using Raycore using JET +using Aqua + +Aqua.test_all(Raycore) @testset "Raycore Tests" begin @testset "Intersection" begin diff --git a/test/test_type_stability.jl b/test/test_type_stability.jl index 4a11b4c..695ee92 100644 --- a/test/test_type_stability.jl +++ b/test/test_type_stability.jl @@ -36,6 +36,7 @@ function gen_triangle() SVector(n1, n1, n1), SVector(Vec3f(NaN), Vec3f(NaN), Vec3f(NaN)), SVector(uv1, uv2, uv3), + UInt32(1), UInt32(1) ) end From 2ae4e14b9e3558ee9446ceccafa1654c72291b2b Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Wed, 29 Oct 2025 18:18:42 +0100 Subject: [PATCH 20/20] final cleanup --- Project.toml | 9 ++++++++- docs/src/bvh_hit_tests.md | 12 ++++++------ docs/src/raytracing_tutorial.md | 12 ++++++------ docs/src/viewfactors.md | 12 ++++++------ src/Raycore.jl | 6 +----- test/runtests.jl | 3 ++- 6 files changed, 29 insertions(+), 25 deletions(-) diff --git a/Project.toml b/Project.toml index 9f07d89..a1bfc2b 100644 --- a/Project.toml +++ b/Project.toml @@ -4,7 +4,6 @@ version = "0.1.0" authors = ["Anton Smirnov ", "Simon Danisch