Examples

This page collects a few examples for using ImplicitGeometries.jl.

Smooth boolean operations

The following constructs the canonical geometry showcasing boolean operations on primitive geometries. Here, we will use the smooth counterparts of the standard boolean operations. The factor k controls the smoothness.

julia> using ImplicitGeometries
julia> k = 0.1; # smoothness factor
julia> b1 = Box(; w=2.0, h=2.0, d=2.0)Box{3, Float64}
julia> s1 = Sphere(; r=1.3)Sphere{3, Float64}
julia> i1 = SmoothIntersection(s1, b1; k = k)SmoothIntersection{3, Float64}
julia> c1 = Cylinder(; h = 2.0, r = 0.8)Cylinder{3, Float64}
julia> c2 = Rotation(Cylinder(; h = 2.0, r = 0.8); ϕ = π/2, θ = 0.0, ψ = 0.0)Rotation{3, Float64}
julia> c3 = Rotation(Cylinder(; h = 2.0, r = 0.8); ϕ = 0.0, θ = 0.0, ψ = π/2)Rotation{3, Float64}
julia> u1 = SmoothUnion(c1, c2; k = k)SmoothUnion{3, Float64}
julia> u2 = SmoothUnion(u1, c3; k = k)SmoothUnion{3, Float64}
julia> s2 = SmoothSubtraction(i1, u2; k = k)SmoothSubtraction{3, Float64}
julia> geometry = Scaling(s2; s=7.0)Scaling{3, Float64}

To print the construction tree, you can use

julia> using AbstractTrees
julia> print_tree(geometry; maxdepth=10)Scaling (s = 7.0) └─ SmoothSubtraction ├─ SmoothIntersection │ ├─ Sphere (r = 1.3) │ └─ Box (w = 2.0, h = 2.0, d = 2.0) └─ SmoothUnion ├─ SmoothUnion │ ├─ Cylinder (r = 0.8, h = 2.0) │ └─ Rotation (ϕ = 90.0°, θ = 0.0°, ψ = 0.0°, dx = 0.0, dy = 0.0, dz = 0.0) │ └─ Cylinder (r = 0.8, h = 2.0) └─ Rotation (ϕ = 0.0°, θ = 0.0°, ψ = 90.0°, dx = 0.0, dy = 0.0, dz = 0.0) └─ Cylinder (r = 0.8, h = 2.0)

The signed distance function can be evaluated at some position p as follows

julia> using StaticArrays
julia> p = SVector(1.0, 2.0, 3.0)3-element StaticArraysCore.SVector{3, Float64} with indices SOneTo(3): 1.0 2.0 3.0
julia> geometry(p)3.3639320225002107

Visualization

ImplicitGeometries.jl only defines implicit geometries. External tools are needed for visualization.

Clip by scalar

One way to visualize the implicitly defined geometry is to sample the signed distance function on a grid of points and export it a VTK file using WriteVTK.jl.

using WriteVTK

# define sampling grid
Ix = LinRange(-7.5, 7.5, 100)
Iy = LinRange(-7.5, 7.5, 100)
Iz = LinRange(-7.5, 7.5, 100)
x = Base.Generator(p -> SVector{3}(p), Iterators.product(Ix, Iy, Iz))

# sample geometry, gradient and normals
ϕ = broadcast(geometry, x);
∇ϕ = broadcast(gradient, x);
n = broadcast(normal, x);

# export to vtk file
vtk_grid("sdf", Ix, Iy, Iz) do vtk
    vtk["ϕ"] = ϕ
    vtk["∇ϕ"] = ∇ϕ
    vtk["n"] = n
end

To show the zero levelset one can load the VTK file in Paraview and clip the dataset by a scalar (i.e. the signed distance function at zero). The normals can be used to improve the lighting. The final result is shown below.

Smooth boolean operations

Marching cubes

Another way, which is arguably faster and gives better results for three-dimensional signed distance functions, is to use MarchingCubes.jl to extract the surface. The normals can be then evaluated on that extracted surface only. Finally, the dataset can be exported using WriteVTK.jl.

Consider the following geometry

julia> geometry = Ring(Rectangle(; w=1.0, h=0.05), r=0.2);
julia> geometry = Revolution(geometry, o=4.0);
julia> geometry = Elongation(geometry; dx=0.5);
julia> rotated1 = Rotation(geometry; ϕ=π/2);
julia> rotated2 = Rotation(geometry; ϕ=π/2+π/4);
julia> rotated3 = Rotation(geometry; ϕ=π/2-π/4);
julia> geometry = SmoothUnion(geometry, rotated1);
julia> geometry = SmoothUnion(geometry, rotated2);
julia> geometry = SmoothUnion(geometry, rotated3);
julia> geometry = SmoothUnion(geometry, Translation(Rotation(Ring(Cylinder(; r=0.5, h=2.0); r=0.5); ψ=π/2); dx = -5.0));
julia> geometry = SmoothUnion(geometry, Translation(Rotation(Ring(Cylinder(; r=0.5, h=2.0); r=0.5); ψ=π/2); dx = 5.0));
julia> print_tree(geometry; maxdepth=10)SmoothUnion ├─ SmoothUnion │ ├─ SmoothUnion │ │ ├─ SmoothUnion │ │ │ ├─ SmoothUnion │ │ │ │ ├─ Elongation (dx = 0.5, dy = 0.0, dz = 0.0) │ │ │ │ │ └─ Revolution (o = 4.0) │ │ │ │ │ └─ Ring (r = 0.2) │ │ │ │ │ └─ Rectangle (w = 1.0, h = 0.05) │ │ │ │ └─ Rotation (ϕ = 90.0°, θ = 0.0°, ψ = 0.0°, dx = 0.0, dy = 0.0, dz = 0.0) │ │ │ │ └─ Elongation (dx = 0.5, dy = 0.0, dz = 0.0) │ │ │ │ └─ Revolution (o = 4.0) │ │ │ │ └─ Ring (r = 0.2) │ │ │ │ └─ Rectangle (w = 1.0, h = 0.05) │ │ │ └─ Rotation (ϕ = 135.0°, θ = 0.0°, ψ = 0.0°, dx = 0.0, dy = 0.0, dz = 0.0) │ │ │ └─ Elongation (dx = 0.5, dy = 0.0, dz = 0.0) │ │ │ └─ Revolution (o = 4.0) │ │ │ └─ Ring (r = 0.2) │ │ │ └─ Rectangle (w = 1.0, h = 0.05) │ │ └─ Rotation (ϕ = 45.0°, θ = 0.0°, ψ = 0.0°, dx = 0.0, dy = 0.0, dz = 0.0) │ │ └─ Elongation (dx = 0.5, dy = 0.0, dz = 0.0) │ │ └─ Revolution (o = 4.0) │ │ └─ Ring (r = 0.2) │ │ └─ Rectangle (w = 1.0, h = 0.05) │ └─ Translation (dx = -5.0, dy = 0.0, dz = 0.0) │ └─ Rotation (ϕ = 0.0°, θ = 0.0°, ψ = 90.0°, dx = 0.0, dy = 0.0, dz = 0.0) │ └─ Ring (r = 0.5) │ └─ Cylinder (r = 0.5, h = 2.0) └─ Translation (dx = 5.0, dy = 0.0, dz = 0.0) └─ Rotation (ϕ = 0.0°, θ = 0.0°, ψ = 90.0°, dx = 0.0, dy = 0.0, dz = 0.0) └─ Ring (r = 0.5) └─ Cylinder (r = 0.5, h = 2.0)

The extraction of the surface using marching cubes is straightforward,

using MarchingCubes, WriteVTK

# define sampling grid
Ω = ((-7.5, 7.5), (-5.0, 5.0), (-5.0, 5.0))
Ix = LinRange(Ω[1]..., 400)
Iy = LinRange(Ω[2]..., 400)
Iz = LinRange(Ω[3]..., 400)
x = Base.Generator(p -> SVector{3}(p), Iterators.product(Ix, Iy, Iz))

# sample sdf at the grid of points
ϕ = broadcast(geometry, x);

# initialize and execute marching cubes
mc = MC(ϕ, Int32, x=Ix, y=Iy, z=Iz);
march(mc)

# collect vertex points, triangualr cells, normals and export
points = reduce(hcat, mc.vertices)
cells = map(t -> MeshCell(VTKCellTypes.VTK_TRIANGLE, t), mc.triangles)
vtk_grid("mc", points, cells) do vtk
    vtk["n"] = broadcast(normal, mc.vertices)
end

The result is a surface mesh shown below

Marching cubes surface extraction

A key advantage of this approach is that the resulting surface mesh can be imported into MeshLab, which allows for generating meshes suitable for analysis. This makes it possible to perform both immersed and boundary-fitted finite element computations on geometries modeled with ImplicitGeometries.jl.

Marching cubes surface repaired mesh

Quadratic Bezier curves

Two-dimensional signed distance function can be generated from closed multisegment Bezier curves. Consider the following simple airfoil modeled by three quadratic Bezier segments

julia> airfoil = [
                  SVector(-1.0,  0.0), SVector( 0.91070,  0.39097), SVector( 1.0,  0.0),
                  SVector( 1.0,  0.0), SVector( 1.06054, -0.26505), SVector( 0.4, -0.1),
                  SVector( 0.4, -0.1), SVector(-0.20900,  0.05217), SVector(-1.0,  0.0)
       ];
julia> geometry = QuadraticBezier(; v=airfoil)QuadraticBezier{2, Float64}
julia> geometry = Ring(geometry; r=0.005)Ring{2, Float64}
julia> geometry = Rotation(geometry; θ = deg2rad(15))Rotation{2, Float64}
julia> print_tree(geometry)Rotation (θ = 15.0°, dx = 0.0, dy = 0.0) └─ Ring (r = 0.005) └─ QuadraticBezier (nseg = 3)

The result together with the gradient of the signed distance function and the normals is shown below.

Two-dimensional airfoil using Bezier

The two-dimensional airfoil can be extruded to obtain a three-dimensional airfoil

julia> geometry = QuadraticBezier(; v=airfoil)QuadraticBezier{2, Float64}
julia> geometry = Rotation(geometry; θ = deg2rad(15))Rotation{2, Float64}
julia> geometry = Scaling(geometry; s=2.0)Scaling{2, Float64}
julia> geometry = Extrusion(geometry; h=5.0)Extrusion{3, Float64}
julia> geometry = Ring(geometry; r=0.0125)Ring{3, Float64}
julia> print_tree(geometry)Ring (r = 0.0125) └─ Extrusion (h = 5.0) └─ Scaling (s = 2.0) └─ Rotation (θ = 15.0°, dx = 0.0, dy = 0.0) └─ QuadraticBezier (nseg = 3)

Three-dimensional airfoil using extrusion