From a356a3ad322d0206584840f135bb21b54ccf78eb Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel Date: Thu, 6 Aug 2026 11:50:59 +0200 Subject: [PATCH] fix(sphere): Make seam and pole vertices bit-exact via sinpi/cospi Sphere and Circle tessellations duplicate the wrap-seam vertices (the u = 0 and u = 1 columns) on purpose, so that texture coordinates can differ across the seam. But parametrizing with cos/sin over LinRange(0, 2pi, n) evaluates the seam columns at 0 and 2pi, whose results differ by float rounding (e.g. cos(2pi) = 0.99999994f0 in Float32). Renderers that classify mesh edges by exact position matching (e.g. Makie's edge stroking in MakieOrg/Makie.jl#5727) then see the seam as a mesh boundary and draw it differently. Parametrize with cospi/sinpi over LinRange(0, 2, n) instead, which evaluates the wrap exactly: cospi(2.0) === 1.0 and sinpi(2.0) === 0.0. This also makes all pole-row vertices of the sphere bit-identical (sinpi(1.0) === 0.0, so the bottom pole no longer scatters by cos(phi) * 1.2e-16) and slightly improves accuracy of all other vertices. Cylinder and Cone reuse seam vertices via mod1 indexing and are unaffected. --- src/primitives/spheres.jl | 8 ++++---- test/geometrytypes.jl | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/primitives/spheres.jl b/src/primitives/spheres.jl index f4fff7e4..2de07929 100644 --- a/src/primitives/spheres.jl +++ b/src/primitives/spheres.jl @@ -42,7 +42,7 @@ centered(S::Type{<: HyperSphere}) = S(Point3f(0), 0.5f0) function coordinates(s::Circle, nvertices=64) r = radius(s); o = origin(s) - ps = [r * Point(cos(phi), sin(phi)) + o for phi in LinRange(0, 2pi, nvertices)] + ps = [r * Point(cospi(phi), sinpi(phi)) + o for phi in LinRange(0, 2, nvertices)] # ps[end] = o return ps end @@ -59,9 +59,9 @@ end function coordinates(s::Sphere, nvertices=24) - θ = LinRange(0, pi, nvertices) - φ = LinRange(0, 2pi, nvertices) - inner(θ, φ) = Point(cos(φ) * sin(θ), sin(φ) * sin(θ), cos(θ)) .* s.r .+ s.center + θ = LinRange(0, 1, nvertices) + φ = LinRange(0, 2, nvertices) + inner(θ, φ) = Point(cospi(φ) * sinpi(θ), sinpi(φ) * sinpi(θ), cospi(θ)) .* s.r .+ s.center return [inner(θ, φ) for φ in φ for θ in θ] end diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index f5534610..a34024c1 100644 --- a/test/geometrytypes.jl +++ b/test/geometrytypes.jl @@ -506,6 +506,24 @@ end @test Point2f(-0.5) in circle @test centered(Circle) == Circle(Point2f(0), 0.5f0) @test centered(Circle{Float64}) == Circle(Point2(0.0), 0.5) + + @testset "bit-exact seam and pole vertices" begin + for T in (Float32, Float64) + n = 24 + sphere_points = coordinates(Sphere(Point{3,T}(0.2, -0.3, 0.4), T(1.3)), n) + first_meridian = sphere_points[1:n] + last_meridian = sphere_points[(end - n + 1):end] + @test all(map(===, first_meridian, last_meridian)) + + top_pole = sphere_points[1:n:end] + bottom_pole = sphere_points[n:n:end] + @test all(p === top_pole[1] for p in top_pole) + @test all(p === bottom_pole[1] for p in bottom_pole) + + circle_points = coordinates(Circle(Point{2,T}(0.2, -0.3), T(1.3)), n) + @test circle_points[1] === circle_points[end] + end + end end @testset "LineStrings" begin