CSG
CSG Operations
OpticSim represents objects as boolean combinations of halfspaces, a method of constructing solid objects known as Constructive Solid Geometry (CSG). A halfspace is a surface which divides 3D space into 3 regions: inside the halfspace,outside the halfspace, on the surface dividing inside from outside.
A simple example of a halfspace is an infinite oriented plane, where oriented means we have defined a direction for the normal to the plane surface. Another simple example is a sphere, or a cylinder.
If the halfspaces have mathematically well difined inside and outside functions then except for edge cases, which are unlikely to occur in an optics setting, then any boolean combination of halfspaces will also have a well defined inside and outside.
You can define optical elements by combining the basic halfspace types defined for you in OpticSim:
- sphere
- infinite cylinder
- plane
- prism
- spherical cap
Many of the surfaces already defined for you in OpticSim, such as sphere, prism,plane, cylinder, describe well defined solid objects, i.e., there is a mathematically defined inside and outside.
Unfortunately, many of the surfaces common in optics do not by themselves define solid objects. None of the parametric surfaces, which includes all the optical asphere types, define solid objects.
Parametric surfaces do have a positive and negative side though determined by the direction of the surface normal. You can create a well defined solid object by taking the intersection of a cylinder with an asphere.
There are three binary csg operations which can construct extremely complex objects from very simple primitives: union ($\cup$), intersection ($\cap$) and subtraction (i.e. difference).
This diagram shows the basic idea: 
The code for this in our system would look this this:
cyl = Cylinder(0.7)
cyl_cross = cyl ∪ leaf(cyl, Geometry.rotationd(90, 0, 0)) ∪ leaf(cyl, Geometry.rotationd(0, 90, 0))
cube = Cuboid(1.0, 1.0, 1.0)
sph = Sphere(1.3)
rounded_cube = cube ∩ sph
result = rounded_cube - cyl_crossCSGGenerator{Float64}(OpticSim.var"#81#82"{CSGGenerator{Float64}, CSGGenerator{Float64}}(CSGGenerator{Float64}(OpticSim.var"#75#76"{CSGGenerator{Float64}, CSGGenerator{Float64}}(CSGGenerator{Float64}(OpticSim.var"#75#76"{CSGGenerator{Float64}, CSGGenerator{Float64}}(CSGGenerator{Float64}(OpticSim.var"#75#76"{CSGGenerator{Float64}, CSGGenerator{Float64}}(CSGGenerator{Float64}(OpticSim.var"#75#76"{CSGGenerator{Float64}, CSGGenerator{Float64}}(CSGGenerator{Float64}(OpticSim.var"#75#76"{CSGGenerator{Float64}, CSGGenerator{Float64}}(CSGGenerator{Float64}(OpticSim.var"#75#76"{CSGGenerator{Float64}, CSGGenerator{Float64}}(CSGGenerator{Float64}(OpticSim.var"#leaf##0#leaf##1"{Plane{Float64, 3}, OpticSim.Geometry.Transform{Float64}}(Plane{Float64}([-1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], NullInterface{Float64}()), OpticSim.Geometry.Transform{Float64}([1.0 0.0 0.0 0.0; 0.0 1.0 0.0 0.0; 0.0 0.0 1.0 0.0; 0.0 0.0 0.0 1.0]))), CSGGenerator{Float64}(OpticSim.var"#leaf##0#leaf##1"{Plane{Float64, 3}, OpticSim.Geometry.Transform{Float64}}(Plane{Float64}([1.0, 0.0, 0.0], [1.0, 0.0, 0.0], NullInterface{Float64}()), OpticSim.Geometry.Transform{Float64}([1.0 0.0 0.0 0.0; 0.0 1.0 0.0 0.0; 0.0 0.0 1.0 0.0; 0.0 0.0 0.0 1.0]))))), CSGGenerator{Float64}(OpticSim.var"#leaf##0#leaf##1"{Plane{Float64, 3}, OpticSim.Geometry.Transform{Float64}}(Plane{Float64}([0.0, -1.0, 0.0], [0.0, -1.0, 0.0], NullInterface{Float64}()), OpticSim.Geometry.Transform{Float64}([1.0 0.0 0.0 0.0; 0.0 1.0 0.0 0.0; 0.0 0.0 1.0 0.0; 0.0 0.0 0.0 1.0]))))), CSGGenerator{Float64}(OpticSim.var"#leaf##0#leaf##1"{Plane{Float64, 3}, OpticSim.Geometry.Transform{Float64}}(Plane{Float64}([0.0, 1.0, 0.0], [0.0, 1.0, 0.0], NullInterface{Float64}()), OpticSim.Geometry.Transform{Float64}([1.0 0.0 0.0 0.0; 0.0 1.0 0.0 0.0; 0.0 0.0 1.0 0.0; 0.0 0.0 0.0 1.0]))))), CSGGenerator{Float64}(OpticSim.var"#leaf##0#leaf##1"{Plane{Float64, 3}, OpticSim.Geometry.Transform{Float64}}(Plane{Float64}([0.0, 0.0, -1.0], [0.0, 0.0, -1.0], NullInterface{Float64}()), OpticSim.Geometry.Transform{Float64}([1.0 0.0 0.0 0.0; 0.0 1.0 0.0 0.0; 0.0 0.0 1.0 0.0; 0.0 0.0 0.0 1.0]))))), CSGGenerator{Float64}(OpticSim.var"#leaf##0#leaf##1"{Plane{Float64, 3}, OpticSim.Geometry.Transform{Float64}}(Plane{Float64}([0.0, 0.0, 1.0], [0.0, 0.0, 1.0], NullInterface{Float64}()), OpticSim.Geometry.Transform{Float64}([1.0 0.0 0.0 0.0; 0.0 1.0 0.0 0.0; 0.0 0.0 1.0 0.0; 0.0 0.0 0.0 1.0]))))), CSGGenerator{Float64}(OpticSim.var"#leaf##0#leaf##1"{Sphere{Float64, 3}, OpticSim.Geometry.Transform{Float64}}(Sphere{Float64, 3}(1.3, NullInterface{Float64}()), OpticSim.Geometry.Transform{Float64}([1.0 0.0 0.0 0.0; 0.0 1.0 0.0 0.0; 0.0 0.0 1.0 0.0; 0.0 0.0 0.0 1.0]))))), CSGGenerator{Float64}(OpticSim.var"#78#79"{CSGGenerator{Float64}, CSGGenerator{Float64}}(CSGGenerator{Float64}(OpticSim.var"#78#79"{CSGGenerator{Float64}, CSGGenerator{Float64}}(CSGGenerator{Float64}(OpticSim.var"#leaf##0#leaf##1"{Cylinder{Float64, 3}, OpticSim.Geometry.Transform{Float64}}(Cylinder{Float64}(0.7, NullInterface{Float64}()), OpticSim.Geometry.Transform{Float64}([1.0 0.0 0.0 0.0; 0.0 1.0 0.0 0.0; 0.0 0.0 1.0 0.0; 0.0 0.0 0.0 1.0]))), CSGGenerator{Float64}(OpticSim.var"#leaf##0#leaf##1"{Cylinder{Float64, 3}, OpticSim.Geometry.Transform{Float64}}(Cylinder{Float64}(0.7, NullInterface{Float64}()), OpticSim.Geometry.Transform{Float64}([1.0 0.0 0.0 0.0; 0.0 6.123233995736766e-17 -1.0 0.0; -0.0 1.0 6.123233995736766e-17 0.0; 0.0 0.0 0.0 1.0]))))), CSGGenerator{Float64}(OpticSim.var"#leaf##0#leaf##1"{Cylinder{Float64, 3}, OpticSim.Geometry.Transform{Float64}}(Cylinder{Float64}(0.7, NullInterface{Float64}()), OpticSim.Geometry.Transform{Float64}([6.123233995736766e-17 0.0 1.0 0.0; 0.0 1.0 0.0 0.0; -1.0 0.0 6.123233995736766e-17 0.0; 0.0 0.0 0.0 1.0])))))))OpticSim.leaf — Function
leaf(surf::ParametricSurface{T}, transform::Transform{T} = identitytransform(T)) -> CSGGenerator{T}Create a leaf node from a parametric surface with a given transform.
Pre-made CSG Shapes
There are also some shortcut methods available to create common CSG objects more easily:
OpticSim.BoundedCylinder — Function
BoundedCylinder(radius::T, height::T; interface::NullOrFresnel{T} = nullinterface(T)) -> CSGGenerator{T}Create a cylinder with planar caps on both ends centered at (0, 0, 0) with axis (0, 0, 1).
OpticSim.Cuboid — Function
Cuboid(halfsizex::T, halfsizey::T, halfsizez::T; interface::NullOrFresnel{T} = nullinterface(T)) -> CSGGenerator{T}Create a cuboid centered at (0, 0, 0).
OpticSim.HexagonalPrism — Function
HexagonalPrism(side_length::T, visheight::T = 2.0; interface::NullOrFresnel{T} = nullinterface(T)) -> CSGGenerator{T}Create an infinitely tall hexagonal prism with axis (0, 0, 1), the longer hexagon diameter is along the x axis. For visualization visheight is used, note that this does not fully represent the surface.
OpticSim.RectangularPrism — Function
RectangularPrism(halfsizex::T, halfsizey::T, visheight::T=2.0; interface::NullOrFresnel{T} = nullinterface(T)) -> CSGGenerator{T}Create an infinitely tall rectangular prism with axis (0, 0, 1). For visualization visheight is used, note that this does not fully represent the surface.
OpticSim.TriangularPrism — Function
TriangularPrism(side_length::T, visheight::T = 2.0; interface::NullOrFresnel{T} = nullinterface(T)) -> CSGGenerator{T}Create an infinitely tall triangular prism with axis (0, 0, 1). For visualization visheight is used, note that this does not fully represent the surface.
OpticSim.Spider — Function
Spider(narms::Int, armwidth::T, radius::T, origin::SVector{3,T} = SVector{3,T}(0.0, 0.0, 0.0), normal::SVector{3,T} = SVector{3,T}(0.0, 0.0, 1.0)) -> Vector{Rectangle{T}}Creates a 'spider' obscuration with narms rectangular arms evenly spaced around a circle defined by origin and normal. Each arm is a rectangle armwidth×radius.
e.g. for 3 and 4 arms we get:
| _|_
/ \ |CSG Types
These are the types of the primary CSG elements, i.e. the nodes in the CSG tree.
OpticSim.CSGTree — Type
Abstract type representing any evaluated CSG structure.
OpticSim.CSGGenerator — Type
CSGGenerator{T<:Real}This is the type you should use when making CSG objects. This type allows for the construction of CSGTree objects with different transforms. When the generator is evaluated, all transforms are propagated down to the LeafNodes and stored there.
Example
a = Cylinder(1.0,1.0)
b = Plane([0.0,0.0,1.0], [0.0,0.0,0.0])
generator = a ∩ b
# now make a csg object that can be ray traced
csgobj = generator(Transform(1.0,1.0,2.0))OpticSim.ComplementNode — Type
ComplementNode{T,C<:CSGTree{T}} <: CSGTree{T}An evaluated complement node within the CSG tree, must be the second child of a IntersectionNode forming a subtraction.
OpticSim.UnionNode — Type
UnionNode{T,L<:CSGTree{T},R<:CSGTree{T}} <: CSGTree{T}An evaluated union node within the CSG tree.
OpticSim.IntersectionNode — Type
IntersectionNode{T,L<:CSGTree{T},R<:CSGTree{T}} <: CSGTree{T}An evaluated intersection node within the CSG tree.
OpticSim.LeafNode — Type
LeafNode{T,S<:ParametricSurface{T}} <: CSGTree{T}An evaluated leaf node in the CSG tree, geometry attribute which contains a ParametricSurface of type S. The leaf node also has a transform associated which is the composition of all nodes above it in the tree. As such, transforming points from the geometry using this transform puts them in world space, and transforming rays by the inverse transform puts them in object space.
Additional Functions and Types
These are the internal types and functions used for geometric/CSG operations.
Functions
OpticSim.surfaceintersection — Method
surfaceintersection(obj::CSGTree{T}, r::AbstractRay{T,N})Calculates the intersection of r with CSG object, obj.
Returns an EmptyInterval if there is no intersection, an Interval if there is one or two intersections and a DisjointUnion if there are more than two intersections.
The ray is intersected with the LeafNodes that make up the CSG object and the resulting Intervals and DisjointUnions are composed with the same boolean operations to give a final result. The ray is transformed by the inverse of the transform associated with the leaf node to put it in object space for that node before the intersection is carried out, typically this object space is centered at the origin, but may differ for each primitive.
Some intersections are culled without actually evaluating them by first checking if the ray intersects the BoundingBox of each node in the CSGTree, this can substantially improve performance in some cases.
OpticSim.inside — Method
inside(obj::CSGTree{T}, point::SVector{3,T}) -> Bool
inside(obj::CSGTree{T}, x::T, y::T, z::T) -> BoolTests whether a 3D point in world space is inside obj.
OpticSim.onsurface — Method
onsurface(obj::CSGTree{T}, point::SVector{3,T}) -> Bool
onsurface(obj::CSGTree{T}, x::T, y::T, z::T) -> BoolTests whether a 3D point in world space is on the surface (i.e. shell) of obj.
Intervals
OpticSim.Interval — Type
Interval{T} <: AbstractRayInterval{T}Datatype representing an interval between two IntervalPoints on a ray.
The lower element can either be RayOrigin or an Intersection. The upper element can either be an Intersection or Infinity.
positivehalfspace(int::Intersection) -> Interval with lower = int, upper = Infinity
rayorigininterval(int::Intersection) -> Interval with lower = RayOrigin, upper = int
Interval(low, high)Has the following accessor methods:
lower(a::Interval{T}) -> Union{RayOrigin{T},Intersection{T,3}}
upper(a::Interval{T}) -> Union{Intersection{T,3},Infinity{T}}OpticSim.EmptyInterval — Type
EmptyInterval{T} <: AbstractRayInterval{T}An interval with no Intersections which is also not infinite.
EmptyInterval(T = Float64)
EmptyInterval{T}()OpticSim.DisjointUnion — Type
Datatype representing an ordered series of disjoint intervals on a ray. An arbitrary array of Intervals can be input to the constructor and they will automatically be processed into a valid DisjointUnion (or a single Interval if appropriate).
DisjointUnion(intervals::AbstractVector{Interval{R}})OpticSim.isemptyinterval — Function
isemptyinterval(a) -> BoolReturns true if a is an EmptyInterval. In performance critical contexts use a isa EmptyInterval{T}.
OpticSim.ispositivehalfspace — Function
ispositivehalfspace(a) -> BoolReturns true if upper(a) is Infinity. In performance critical contexts check directly i.e. upper(a) isa Infinity{T}.
OpticSim.israyorigininterval — Function
israyorigininterval(a) -> BoolReturns true if lower(a) is RayOrigin. In performance critical contexts check directly i.e. lower(a) isa RayOrigin{T}.
OpticSim.halfspaceintersection — Function
halfspaceintersection(a::Interval{T}) -> Intersection{T,3}Returns the Intersection from a half space Interval, throws an error if not a half space.
OpticSim.closestintersection — Function
closestintersection(a::Union{EmptyInterval{T},Interval{T},DisjointUnion{T}}, ignorenull::Bool = true) -> Union{Nothing,Intersection{T,3}}Returns the closest Intersection from an Interval or DisjointUnion. Ignores intersection with null interfaces if ignorenull is true. Will return nothing if there is no valid intersection.
OpticSim.IntervalPool — Type
To prevent allocations we have a manually managed pool of arrays of Intervals which are used to store values during execution. The memory is kept allocated and reused across runs of functions like trace.
threadedintervalpool is a global threadsafe pool which is accessed through the functions:
newinintervalpool!(::Type{T} = Float64, tid::Int = Threads.threadid()) -> Vector{Interval{T}}
indexednewinintervalpool!(::Type{T} = Float64, tid::Int = Threads.threadid()) -> Tuple{Int,Vector{Interval{T}}}
emptyintervalpool!(::Type{T} = Float64, tid::Int = Threads.threadid())
getfromintervalpool([::Type{T} = Float64], id::Int, tid::Int = Threads.threadid()) -> Vector{Interval{T}}Intersections
OpticSim.IntervalPoint — Type
Each Interval consists of two IntervalPoints, one of RayOrigin, Intersection or Infinity.
OpticSim.RayOrigin — Type
RayOrigin{T} <: IntervalPoint{T}Point representing 0 within an Interval, i.e. the start of the ray.
RayOrigin(T = Float64)
RayOrigin{T}()OpticSim.Infinity — Type
Infinity{T} <: IntervalPoint{T}Point representing ∞ within an Interval.
Infinity(T = Float64)
Infinity{T}()OpticSim.Intersection — Type
Intersection{T,N} <: IntervalPoint{T}Represents the point at which an Ray hits a Surface. This consists of the distance along the ray, the intersection point in world space, the normal in world space, the UV on the surface and the OpticalInterface hit.
Has the following accessor methods:
point(a::Intersection{T,N}) -> SVector{N,T}
normal(a::Intersection{T,N}) -> SVector{N,T}
uv(a::Intersection{T,N}) -> SVector{2,T}
u(a::Intersection{T,N}) -> T
v(a::Intersection{T,N}) -> T
α(a::Intersection{T,N}) -> T
interface(a::Intersection{T,N}) -> OpticalInterface{T}
flippednormal(a::Intersection{T,N}) -> BoolOpticSim.isinfinity — Function
isinfinity(a) -> BoolReturns true if a is Infinity. In performance critical contexts use a isa Infinity{T}.
OpticSim.israyorigin — Function
israyorigin(a) -> BoolReturns true if a is RayOrigin. In performance critical contexts use a isa RayOrigin{T}.