5  Building and Visualizing 2D Geometry

5.1 Overview

This chapter introduces the foundation of the approach: creating accurate physical representations of 2D domains. You’ll learn how to build and visualize 2D geometries using RayTraceHeatTransfer.jl, establishing the geometric framework that the analytical methods will operate on. The chapter progresses systematically through increasingly complex geometries:

  • Rectangle: The fundamental building block, essential for later validation against analytical solutions
  • L-shape: Introduces non-convex geometries and demonstrates geometric complexity
  • Star shape: Showcases the flexibility of the approach with intricate boundaries

Each geometry demonstrates how to construct physical domain representations that serve as the foundation for radiative transfer calculations in subsequent chapters.

5.2 What You’ll Learn

  • Building accurate physical representations of 2D domains
  • Visualizing geometries with working Julia code
  • Understanding how geometric complexity affects domain representation
  • Preparing geometric foundations for exchange factor calculations and heat transfer solutions

5.3 Concepts

The graph equilibrium approach builds 2D domains from fundamental radiative transfer elements: control volumes and walls (boundaries). Understanding how these elements are constructed and connected forms the foundation for all subsequent radiative transfer calculations.

Domain Elements

  • Control volumes: Represent participating media regions with size given by 4βV (m²), where β is the extinction coefficient and V is the volume
  • Walls/boundaries: Represent surfaces with area A (m²) that can emit, absorb, and reflect radiation
  • Each element receives a unique incremental number for exchange factor indexing

Radiative Connectivity

Visibility between elements determines radiative exchange and will be calculated through ray tracing in later chapters. Since this method uses Monte Carlo ray tracing rather than angular discretization, it achieves accurate visibility knowledge without directional approximations, enabling analytical solutions that closely represent the true physical domain.

Geometric Construction

Domains are built from convex blocks - either triangles or quadrilaterals - that can be efficiently ray traced. The meshing and ray tracing algorithms handle both geometric primitives seamlessly. Blocks are combined by specifying which walls are ‘solid’ (opaque boundaries) versus ‘penetrable’ (allowing radiation passage between blocks). This modular approach enables complex geometries while maintaining computational efficiency.

Property Assignment

Material properties (known temperature/flux, emissivity, albedo) are assigned during geometry construction using nested inheritance - each mesh element inherits properties from its parent ‘superpiece’. This simplifies property specification compared to defining properties for individual sub-elements while maintaining flexibility through multiple superpieces with different properties.

Visualization and Access

The visualization system displays element numbers, allowing easy identification of specific walls and volumes for result analysis and validation.

5.4 Examples

Now we’ll build three increasingly complex 2D geometries using RayTraceHeatTransfer.jl, starting with the fundamental rectangle that will serve as our validation benchmark.

5.4.1 Rectangle Domain

The rectangle is the most basic geometry and provides an essential validation case since analytical solutions exist for rectangular enclosures. Let’s construct a simple rectangular domain:

using RayTraceHeatTransfer
using StaticArrays
using GeometryBasics

faces_1 = RayTraceHeatTransfer.PolyFace2D{Float64,Float64}[] # create a vector to hold parts

# build the first part
vertices1 = SVector(
    Point2(0.0, 0.0), # 2D vertices
    Point2(1.0, 0.0),
    Point2(1.0, 1.0),
    Point2(0.0, 1.0),
)
solidWall1 = SVector(true, true, true, true) # all walls are impenetrable
face1 = RayTraceHeatTransfer.PolyFace2D{Float64,Float64}(vertices1, solidWall1)

# set the physical properties and state
face1.T_in_w = [1000.0, 0.0, 0.0, 0.0] # known wall temperatures
face1.q_in_w = [0.0, 0.0, 0.0, 0.0] # wall fluxes not used when temperature is specified
face1.T_in_g = -1.0 # unknown gas temperature
face1.q_in_g = 0.0 # known gas flux (radiative equilibrium)
face1.epsilon = [1.0, 1.0, 1.0, 1.0] # wall emissivities
face1.kappa_g = 1.0 # local absorption coefficient
face1.sigma_s_g = 0.0 # local scattering coefficient

push!(faces_1, face1) # push the first part into the vector for meshing
1-element Vector{PolyFace2D{Float64, Float64}}:
 PolyFace2D{Float64, Float64}(Point{2, Float64}[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]], Bool[1, 1, 1, 1], [0.5, 0.5], Point{2, Float64}[[0.5, 0.0], [1.0, 0.5], [0.5, 1.0], [0.0, 0.5]], Point{2, Float64}[[0.0, -1.0], [1.0, -0.0], [0.0, 1.0], [-1.0, -0.0]], 1.0, [1.0, 1.0, 1.0, 1.0], PolyFace2D{Float64, Float64}[], [1.0, 1.0, 1.0, 1.0], 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [1000.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0])

Next, the domain is meshed into a desired resolution:

Ndim = 11 # splits in (x,y) directions
mesh1 = RayTraceHeatTransfer.RayTracingMeshOptim(faces_1, [(Ndim,Ndim)]);
Uniform extinction detected (beta_g=1.0) - using fast uniform ray tracing
Building optimized cache structures...
Pre-computing coarse mesh geometry...
Pre-computing fine mesh geometry...
Optimization cache built successfully!
Building spatial acceleration structures...
  Building coarse mesh acceleration...
  Building fine mesh acceleration...
Spatial acceleration structures built!

Finally, the domain is displayed using Makie:

using CairoMakie
CairoMakie.activate!()
Makie.inline!(true)
fig = Figure(; size = (800, 600))
ax = Axis(fig[1, 1], aspect=AxisAspect(1))
RayTraceHeatTransfer.plotMesh2D(ax, mesh1) # plot the mesh
# save("rectangle_mesh.png", fig) # Save the figure
fig

2D quadrilateral mesh

5.4.2 L-shaped Domain

The L-shaped domain is the first non-convex geometry and provides a more complex enclosure, when compared to the rectangle. Let’s construct a simple L-shaped domain. We’ll build it from 3 rectangles:

using RayTraceHeatTransfer
using StaticArrays
using GeometryBasics

faces_2 = RayTraceHeatTransfer.PolyFace2D{Float64,Float64}[] # create a vector to hold parts
PolyFace2D{Float64, Float64}[]

Rectangle 1

# build the first part
vertices1 = SVector(
    Point2(0.0, 0.0), # 2D vertices
    Point2(1.0, 0.0),
    Point2(1.0, 1.0),
    Point2(0.0, 1.0),
)
solidWall1 = SVector(true, true, false, true) # top wall is open
face1 = RayTraceHeatTransfer.PolyFace2D{Float64,Float64}(vertices1, solidWall1)

# set the physical properties and state
face1.T_in_w = [1000.0, 0.0, 0.0, 0.0] # known wall temperatures
face1.q_in_w = [0.0, 0.0, 0.0, 0.0] # wall fluxes not used when temperature is specified
face1.T_in_g = -1.0 # unknown gas temperature
face1.q_in_g = 0.0 # known gas flux (radiative equilibrium)
face1.epsilon = [1.0, 1.0, 1.0, 1.0] # wall emissivities
face1.kappa_g = 1.0 # local absorption coefficient
face1.sigma_s_g = 0.0 # local scattering coefficient

push!(faces_2, face1) # push the first part into the vector for meshing
1-element Vector{PolyFace2D{Float64, Float64}}:
 PolyFace2D{Float64, Float64}(Point{2, Float64}[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]], Bool[1, 1, 0, 1], [0.5, 0.5], Point{2, Float64}[[0.5, 0.0], [1.0, 0.5], [0.5, 1.0], [0.0, 0.5]], Point{2, Float64}[[0.0, -1.0], [1.0, -0.0], [0.0, 1.0], [-1.0, -0.0]], 1.0, [1.0, 1.0, 1.0, 1.0], PolyFace2D{Float64, Float64}[], [1.0, 1.0, 1.0, 1.0], 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [1000.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0])

Rectangle 2

# build the first part
vertices2 = SVector(
    Point2(0.0, 1.0), # 2D vertices
    Point2(1.0, 1.0),
    Point2(1.0, 2.0),
    Point2(0.0, 2.0),
)
solidWall2 = SVector(false, false, true, true) # two walls are open
face2 = RayTraceHeatTransfer.PolyFace2D{Float64,Float64}(vertices2, solidWall2)

# set the physical properties and state
face2.T_in_w = [0.0, 0.0, 0.0, 0.0] # known wall temperatures
face2.q_in_w = [0.0, 0.0, 0.0, 0.0] # wall fluxes not used when temperature is specified
face2.T_in_g = -1.0 # unknown gas temperature
face2.q_in_g = 0.0 # known gas flux (radiative equilibrium)
face2.epsilon = [1.0, 1.0, 1.0, 1.0] # wall emissivities
face2.kappa_g = 1.0 # local absorption coefficient
face2.sigma_s_g = 0.0 # local scattering coefficient

push!(faces_2, face2) # push the second part into the vector for meshing
2-element Vector{PolyFace2D{Float64, Float64}}:
 PolyFace2D{Float64, Float64}(Point{2, Float64}[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]], Bool[1, 1, 0, 1], [0.5, 0.5], Point{2, Float64}[[0.5, 0.0], [1.0, 0.5], [0.5, 1.0], [0.0, 0.5]], Point{2, Float64}[[0.0, -1.0], [1.0, -0.0], [0.0, 1.0], [-1.0, -0.0]], 1.0, [1.0, 1.0, 1.0, 1.0], PolyFace2D{Float64, Float64}[], [1.0, 1.0, 1.0, 1.0], 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [1000.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0])
 PolyFace2D{Float64, Float64}(Point{2, Float64}[[0.0, 1.0], [1.0, 1.0], [1.0, 2.0], [0.0, 2.0]], Bool[0, 0, 1, 1], [0.5, 1.5], Point{2, Float64}[[0.5, 1.0], [1.0, 1.5], [0.5, 2.0], [0.0, 1.5]], Point{2, Float64}[[0.0, -1.0], [1.0, -0.0], [0.0, 1.0], [-1.0, -0.0]], 1.0, [1.0, 1.0, 1.0, 1.0], PolyFace2D{Float64, Float64}[], [1.0, 1.0, 1.0, 1.0], 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0])

Rectangle 3

# build the first part
vertices3 = SVector(
    Point2(1.0, 1.0), # 2D vertices
    Point2(2.0, 1.0),
    Point2(2.0, 2.0),
    Point2(1.0, 2.0),
)
solidWall3 = SVector(true, true, true, false) # two walls are open
face3 = RayTraceHeatTransfer.PolyFace2D{Float64,Float64}(vertices3, solidWall3)

# set the physical properties and state
face3.T_in_w = [0.0, 0.0, 0.0, 0.0] # known wall temperatures
face3.q_in_w = [0.0, 0.0, 0.0, 0.0] # wall fluxes not used when temperature is specified
face3.T_in_g = -1.0 # unknown gas temperature
face3.q_in_g = 0.0 # known gas flux (radiative equilibrium)
face3.epsilon = [1.0, 1.0, 1.0, 1.0] # wall emissivities
face3.kappa_g = 1.0 # local absorption coefficient
face3.sigma_s_g = 0.0 # local scattering coefficient

push!(faces_2, face3) # push the second part into the vector for meshing
3-element Vector{PolyFace2D{Float64, Float64}}:
 PolyFace2D{Float64, Float64}(Point{2, Float64}[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]], Bool[1, 1, 0, 1], [0.5, 0.5], Point{2, Float64}[[0.5, 0.0], [1.0, 0.5], [0.5, 1.0], [0.0, 0.5]], Point{2, Float64}[[0.0, -1.0], [1.0, -0.0], [0.0, 1.0], [-1.0, -0.0]], 1.0, [1.0, 1.0, 1.0, 1.0], PolyFace2D{Float64, Float64}[], [1.0, 1.0, 1.0, 1.0], 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [1000.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0])
 PolyFace2D{Float64, Float64}(Point{2, Float64}[[0.0, 1.0], [1.0, 1.0], [1.0, 2.0], [0.0, 2.0]], Bool[0, 0, 1, 1], [0.5, 1.5], Point{2, Float64}[[0.5, 1.0], [1.0, 1.5], [0.5, 2.0], [0.0, 1.5]], Point{2, Float64}[[0.0, -1.0], [1.0, -0.0], [0.0, 1.0], [-1.0, -0.0]], 1.0, [1.0, 1.0, 1.0, 1.0], PolyFace2D{Float64, Float64}[], [1.0, 1.0, 1.0, 1.0], 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0])
 PolyFace2D{Float64, Float64}(Point{2, Float64}[[1.0, 1.0], [2.0, 1.0], [2.0, 2.0], [1.0, 2.0]], Bool[1, 1, 1, 0], [1.5, 1.5], Point{2, Float64}[[1.5, 1.0], [2.0, 1.5], [1.5, 2.0], [1.0, 1.5]], Point{2, Float64}[[0.0, -1.0], [1.0, -0.0], [0.0, 1.0], [-1.0, -0.0]], 1.0, [1.0, 1.0, 1.0, 1.0], PolyFace2D{Float64, Float64}[], [1.0, 1.0, 1.0, 1.0], 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0])

Next, the domain is meshed into a desired resolution:

Ndim = 11 # splits in 3 (x,y) directions
mesh2 = RayTraceHeatTransfer.RayTracingMeshOptim(faces_2, [(Ndim,Ndim),(Ndim,Ndim),(Ndim,Ndim)]);
Uniform extinction detected (beta_g=1.0) - using fast uniform ray tracing
Building optimized cache structures...
Pre-computing coarse mesh geometry...
Pre-computing fine mesh geometry...
Optimization cache built successfully!
Building spatial acceleration structures...
  Building coarse mesh acceleration...
  Building fine mesh acceleration...
Spatial acceleration structures built!

Finally, the domain is displayed using Makie:

fig = Figure(; size = (800, 600))
ax = Axis(fig[1, 1], aspect=AxisAspect(1))
RayTraceHeatTransfer.plotMesh2D(ax, mesh2) # plot the mesh
# save("L-shaped_mesh.png", fig) # Save the figure
fig

2D L-shaped mesh

5.4.3 Star-shaped Domain

The star-shaped domain is the first non-convex, complex enclosure we’ll mesh programmatically. We’ll build it from 10 triangles. First, let’s calculate the points of the star:

using RayTraceHeatTransfer
using GeometryBasics
using StaticArrays

# Inner pentagon radius and outer star point radius
inner_radius = 0.3
outer_radius = 1.0

# Generate 5 points for pentagon (72° between points)
inner_points = Vector{Point2{Float64}}()
for i in 1:5
    angle = 2π/5 * (i-1) - π/2  # Start from top (-π/2)
    x = inner_radius * cos(angle)
    y = inner_radius * sin(angle)
    push!(inner_points, Point2(x, y))
end

# Generate 5 outer star points
outer_points = Vector{Point2{Float64}}()
for i in 1:5
    angle = 2π/5 * (i-1) - π/2 + π/5  # Offset by half pentagon angle
    x = outer_radius * cos(angle)
    y = outer_radius * sin(angle)
    push!(outer_points, Point2(x, y))
end

Next, let’s create the faces:

# Create faces
faces_1 = PolyFace2D{Float64,Float64}[]

# Central pentagon (can be divided into triangles)
for i in 1:5
    vertices = SVector(
        Point2(0.0, 0.0),  # Center
        inner_points[i],
        inner_points[mod1(i+1, 5)]
    )
    solidWalls = SVector(false, false, false)
    face = PolyFace2D{Float64,Float64}(vertices, solidWalls)
    face.T_in_w = [0.0, 0.0, 0.0]
    face.q_in_w = [0.0, 0.0, 0.0]
    face.T_in_g = -1.0
    face.q_in_g = 1000.0
    face.epsilon = [1.0, 1.0, 1.0]
    face.kappa_g = 1.0 # local absorption coefficient
    face.sigma_s_g = 0.0 # local scattering coefficient

    push!(faces_1, face)
end

# Star points (triangles)
for i in 1:5
    vertices = SVector(
        inner_points[i],
        outer_points[i],
        inner_points[mod1(i+1, 5)]
    )
    solidWalls = SVector(true, true, false)
    face = PolyFace2D{Float64,Float64}(vertices, solidWalls)
    face.T_in_w = [0.0, 0.0, 0.0]
    face.q_in_w = [0.0, 0.0, 0.0]
    face.T_in_g = -1.0
    face.q_in_g = 1000.0
    face.epsilon = [1.0, 1.0, 1.0]
    face.kappa_g = 1.0 # local absorption coefficient
    face.sigma_s_g = 0.0 # local scattering coefficient

    push!(faces_1, face)
end

Finally, let’s mesh and plot the geometry:

Ndim = 11
mesh1 = RayTracingMeshOptim(faces_1, [(Ndim,Ndim),(Ndim,Ndim),(Ndim,Ndim),(Ndim,Ndim),(Ndim,Ndim),
                                (Ndim,Ndim),(Ndim,Ndim),(Ndim,Ndim),(Ndim,Ndim),(Ndim,Ndim)]);
Uniform extinction detected (beta_g=1.0) - using fast uniform ray tracing
Building optimized cache structures...
Pre-computing coarse mesh geometry...
Pre-computing fine mesh geometry...
Optimization cache built successfully!
Building spatial acceleration structures...
  Building coarse mesh acceleration...
  Building fine mesh acceleration...
Spatial acceleration structures built!
using CairoMakie
CairoMakie.activate!()
Makie.inline!(true)
fig = Figure(; size = (800, 600))
ax = Axis(fig[1, 1], aspect=AxisAspect(1))
RayTraceHeatTransfer.plotMesh2D(ax, mesh1) # plot the mesh
# save("Star-shaped_mesh.png", fig) # Save the figure
fig

2D star-shaped mesh