Back to Portfolio

Beyond Static Visualization

This project explores how high-dimensional data can evolve through changes in representation. It begins with a procedural five-dimensional field, where spatial, temporal, and latent axes are treated symmetrically and mapped to color. Distinct views are formed through slicing and projection, visualized via ray marching.

The key insight: fixing temporal and latent dimensions yields a concrete 3D field, but the act of viewing introduces new degrees of freedom. Representation shifts from (x,y,z,w,t) → RGBA to (x,y,z) + view direction → RGBA, effectively trading latent dimensions for angular ones.

This reframing opens two parallel paths for evolution: NeRF-style neural representations as a resampling mechanism, and Taichi-based diffusion dynamics operating on slices of the field.

5D Procedural Field Exploration

The base system generates structure in five dimensions via an iterated nonlinear mapping. By choosing which dimensions map to XYZ, which serves as time, and which as a navigable latent axis, the same underlying field produces radically different visual behavior.

Note: This is a pre-rendered video from the original native implementation. The full interactive viewer with real-time dimension swapping and navigation has not yet been ported to the web.

Evolution via Taichi Diffusion

The Challenge: Evolving Procedural Data

A procedural field is defined by a function—it has no persistent state to modify. You can't "write" to a purely procedural volume. So how can we introduce evolution and feedback?

The solution: sample the procedural field into a discrete 3D tensor, run simulation dynamics on that slice, and track the difference between the evolved state and the original procedural values. This difference field becomes the medium for evolution.

Reaction-Diffusion on Sliced Data

Using Taichi for GPU-accelerated computation, the system samples a 3D slice from the 5D field at fixed (W, T) values, then applies reaction-diffusion dynamics:

# Sample 3D slice from 5D procedural field
slice = sample_5d_field(w=current_w, t=current_t)

# Initialize simulation state from procedural density
state = init_from_procedural(slice)

# Run Gray-Scott / SmoothLife dynamics
for step in range(n_steps):
    state = diffusion_step(state, slice)  # procedural modulates feed rate
    
# Track deviation from original
deviation = state - slice  # This is what "evolved"

The procedural field seeds and modulates the simulation, while the simulation's deviation from the original becomes a form of learned or evolved structure.

Taichi Diffusion-Reaction on 5D slice

Gray-Scott reaction-diffusion running on a sampled slice, with the procedural field modulating local feed rates. Organic patterns emerge from the interplay.

Arbitrary Slicing: Beyond Canonical Axes

Rather than examining the 5D field through orthogonal slices—choosing three of the five canonical axes as XYZ—the implementation supports arbitrary slicing: cutting along any 3D hyperplane through the 5D space.

This transforms navigation from discrete axis-swapping into a continuous, geometry-driven process—smoothly rotating through higher-dimensional space rather than jumping between axis-aligned perspectives.

Arbitrary slicing through 5D space

Arbitrary slice navigation: the viewing hyperplane rotates continuously through 5D, revealing structure that axis-aligned slices would miss.

From Discrete to Continuous Navigation

# Orthogonal slice: pick 3 axes from {x,y,z,w,t}
view_axes = [0, 1, 2]  # XYZ slice at fixed W, T

# Arbitrary slice: define 3D hyperplane by basis vectors
basis_1 = normalize([1, 0, 0.3, 0.1, 0])   # Not axis-aligned
basis_2 = normalize([0, 1, -0.2, 0, 0.1])
basis_3 = normalize(cross_5d(basis_1, basis_2, ...))

# Slice coordinates become linear combinations
point_5d = origin + u*basis_1 + v*basis_2 + w*basis_3

Smooth interpolation between different "views" of the 5D structure becomes possible—the slice plane itself can be animated, creating trajectories through the space of all possible 3D cross-sections.

Two 5D Fields: (x,y,z,w,t) ↔ (x,y,z,θ,φ)

The Key Observation

We start with a procedural field defined over five dimensions. But when we view a 3D slice, the viewing direction itself adds two more degrees of freedom. This gives us another 5D field—structured differently, but parallel in form:

Original Field

(x, y, z, w, t) → RGBA

w, t are latent/temporal axes. We pick values for them, then explore the resulting 3D structure.

View-Based Field

(x, y, z, θ, φ) → RGBA

θ, φ are viewing angles. We pick a viewpoint, then see the resulting 3D appearance.

Both are 5D → appearance mappings. The interpretation differs—the original field's extra dimensions encode latent/temporal state, while the view field's encode observer perspective—but the mathematical structure is identical. This is the foundation for the feedback loop.

View 1 of 5D field

View from angle (θ₁, φ₁)

View 2 of 5D field

View from angle (θ₂, φ₂)

Fixing (w,t) and varying (θ,φ) produces training data for a NeRF—which encodes the same structure, but parameterized by viewing angle instead of latent coordinates.

Two Complementary Feedback Loops

Different Domains, Complementary Roles

The key insight: these two loops operate on different domains and serve complementary purposes. Understanding this distinction is crucial.

Loop 1: NeRF Iteration (Function → Function)

This loop evolves the background field itself. Starting from the purely procedural field (which is just a function—no stored values), we render views and train a NeRF to encode this field in a new parameterization.

# Iteration 0: Start with procedural field
procedural: (x,y,z,w,t) → RGBA     # Pure function, no stored data

# Fix (w,t), render many views
views = [render_procedural(w₀, t₀, θ, φ) for θ, φ in viewpoints]

# Train NeRF to encode this view-field
nerf_1: (x,y,z,θ,φ) → RGBA         # NeRF encodes the field

# Reinterpret: treat (θ,φ) as new (w,t)
# Now nerf_1 can be queried as a new background field!

# Iteration 1+: NeRF → NeRF
views = [render_nerf(nerf_1, w₀, t₀, θ, φ) for θ, φ in viewpoints]
nerf_2 = train_nerf(views)
# ... and so on

After the first iteration, it's NeRF → NeRF all the way down. Each cycle produces a new neural encoding of the transformed field. The background field evolves through representation change.

Loop 2: Taichi Diffusion (Numerical Evolution ON the Field)

This loop operates on top of whatever background field currently exists (procedural or NeRF). It samples a 3D slice, runs reaction-diffusion dynamics, and tracks the deviation.

# Taichi operates on the current background field
background = current_field  # Could be procedural OR nerf_n

# Sample a 3D slice at fixed (w,t)
slice = sample(background, w=w₀, t=t₀)

# Run diffusion dynamics
state = initialize_from(slice)
for step in range(n_steps):
    state = diffusion_step(state, slice)  # BG modulates dynamics

# Track what evolved
deviation = state - slice  # This is the "foreground" structure

This is local, numerical evolution—patterns emerge and evolve on the current background, whatever that background happens to be.

Why Both?

The loops complement each other:

  • NeRF loop: Evolves the background field itself (global transformation)
  • Taichi loop: Evolves structure on the background (local dynamics)

You could run Taichi on a procedural background, or on a NeRF-encoded background, or alternate between them. The NeRF loop changes what you're evolving on; the Taichi loop changes what's happening on it.

Duality iteration between representations

The NeRF feedback loop: field → views → train NeRF → NeRF encodes new field → repeat

The Blending Problem

What does it mean to "blend" two NeRFs or interpolate between fields? Unlike blending two images (just mix pixel values), these are generators—functions that produce values on demand. To blend them, you must keep both in memory.

# Naive blending: query both, interpolate outputs
def blended_field(x, y, z, w, t, alpha):
    val_a = nerf_a(x, y, z, w, t)   # Need nerf_a in memory
    val_b = nerf_b(x, y, z, w, t)   # Need nerf_b in memory
    return (1-alpha) * val_a + alpha * val_b

# After N iterations: N networks in memory!
# Rendering slows down linearly with iteration count

This is a fundamental tension: smooth evolution via blending requires keeping history, but history accumulates. After 100 iterations, you'd need 100 NeRF networks.

Potential Solutions

Several directions might address this:

  • NeRF Distillation: Train a single "student" NeRF to approximate the blended output of multiple "teacher" NeRFs, then discard the teachers.
  • NeRF Merging Research: Recent work explores combining multiple NeRFs into unified representations—could enable "committing" a blend to a single network.
  • Lazy Evaluation: Only materialize the blend when needed, accepting the computational cost during rendering.

This connects to a broader theme in the Symbolic Math project: the tension between symbolic/lazy representations (compact but expensive to evaluate) and materialized/eager ones (fast but memory-heavy). The NeRF iteration faces the same tradeoff.

Taichi: Not Limited to 3D

The examples show Taichi operating on 3D slices, but this is a practical choice, not a fundamental limitation. Reaction-diffusion dynamics work in any dimension—you could run them on a 4D or 5D tensor directly.

# 3D slice (current implementation)
slice_3d = sample_field(w=w₀, t=t₀)  # Shape: (Nx, Ny, Nz)
evolved_3d = run_diffusion(slice_3d)

# 4D slice (fix only t)
slice_4d = sample_field(t=t₀)        # Shape: (Nx, Ny, Nz, Nw)
evolved_4d = run_diffusion(slice_4d)

# Full 5D (no slicing)
full_5d = sample_field()             # Shape: (Nx, Ny, Nz, Nw, Nt)
evolved_5d = run_diffusion(full_5d)  # Memory-intensive!

The constraint is memory: a 256³ volume is ~16M voxels; a 256⁵ volume would be ~1 trillion. But for modest resolutions or sparse representations, higher-dimensional diffusion is entirely feasible.

The Experimental Nature

This project is purely exploratory. There's no theorem guaranteeing convergence, no proof that interesting structure will emerge. The questions are empirical:

  • "What does this iteration look like after 10 cycles? 100?"
  • "Does blending old and new fields produce smoother evolution?"
  • "Can we guide the process toward particular structures?"
  • "What happens if we combine both loops—diffusion AND representation cycling?"

The goal isn't to prove something works—it's to discover what's interesting before deciding what's useful.

Future Directions

Current Limitations

The project is theoretically interesting but visually less compelling than hoped. Some aspects are unnecessarily complex in the current implementation:

  • The highly symmetric procedural fields produce results that aren't substantially different from using tabulated data.
  • We could simplify by pre-computing a finite 5D tensor at startup and applying periodic boundary conditions (wrapping around on all axes). This would eliminate the need to maintain analytical field definitions.

Simplifying the Pipeline

If we start with a numerical 5D field (tabulated tensor), the NeRF iteration becomes more uniform: each cycle performs dense sampling to convert the NeRF encoding back into a numerical field, which then feeds the next iteration.

# Numerical-first pipeline
tensor_5d = precompute_field()           # Dense 5D tensor at startup

# NeRF iteration cycle
views = render_from_tensor(tensor_5d, viewpoints)
nerf = train_nerf(views)
tensor_5d = dense_sample(nerf)           # Back to numerical
# Repeat...

This avoids the complexity of maintaining both analytical field definitions and NeRF networks simultaneously. The tradeoff is memory (tabulated fields are larger), but the pipeline is conceptually cleaner.

Status: Still exploring how this performs in practice—whether the dense sampling preserves enough structure, and how iteration count affects quality.

Where It Gets Interesting

The framework becomes more meaningful with less symmetric, aperiodic fields:

  • Non-periodic procedural functions where the structure genuinely varies across the full 5D domain—here, infinite evaluation matters.
  • Data-driven fields (e.g., volumetric captures, simulations) where the 5D structure comes from real phenomena rather than mathematical formulas.
  • Hybrid approaches: procedural base with learned refinements that break symmetry.

The NeRF iteration machinery is overkill for symmetric fields but could reveal genuinely novel structures when applied to richer, less predictable data.

Dimension Mapping (Original System)

The base visualization assigns five dimensions to different roles:

X, Y, Z

Spatial Axes

3D view space

W

Latent Dimension

Navigate via slider

T

Time

Animates the view

Any permutation is valid—smoothness in 5D implies coherent structure from any projection. Swapping which axis serves as time versus latent dimension produces completely different animations from the same underlying field.

NeRF Input Space: (x, y, z, θ, φ)

A Neural Radiance Field takes a different 5D input: spatial position (x, y, z) plus viewing direction (θ, φ). The network outputs color and density for that point as seen from that angle.

X, Y, Z

Spatial Position

Point in 3D scene

θ, φ

View Direction

Spherical angles

The Equivalence

Both systems map 5D → RGBA. If we have a NeRF trained on arbitrary viewpoints, we can reinterpret it as a time-varying volumetric field: treat one angular dimension as time (θ → t), and the other as a latent axis (φ → w). The network becomes a procedural 5D field—exactly the structure we started with.

# NeRF interpretation
nerf(x, y, z, θ, φ) → RGBA

# Reinterpret as time volume
volume(x, y, z, w=φ, t=θ) → RGBA

# Same function, different semantics

Mathematical Foundation

5D Distance Metric

The fractal is computed using the 5D Euclidean distance:

d = √(x₀² + x₁² + x₂² + x₃² + x₄²)

Iterative Formula

A 5D Mandelbrot-style fractal uses:

z_{n+1} = f(z_n) + c

where z and c are 5-dimensional vectors, and f represents the fractal transformation (often a power operation extended to 5D using geometric algebra or quaternion-like structures).

Projection to 3D

Given dimension assignment (i, j, k, w, t), the 3D point displayed is:

x_3D = dimensions[i]
y_3D = dimensions[j]
z_3D = dimensions[k]

// Fix hidden dimension w at slider value
dimensions[w] = w_value

// Animate time dimension t
dimensions[t] = sin(time * frequency)

Projection Mathematics: Intuition

What is a Projection?

A projection is a way of "squashing" higher-dimensional space into lower dimensions. Think of a 3D object casting a 2D shadow - you lose depth information, but retain shape.

For a 5D point p = (p₀, p₁, p₂, p₃, p₄), projecting to 3D means:

// Choose 3 dimensions to display
Projection(p) = (p_i, p_j, p_k)

// The other dimensions (p_w, p_t) are "collapsed"

Orthogonal vs. Perspective Projection

This viewer uses orthogonal projection - parallel lines in 5D remain parallel in the 3D view. The alternative, perspective projection, would make distant 5D points appear smaller, but is harder to interpret for higher dimensions.

Orthogonal:    (x,y,z,w,t) → (x,y,z)  [simply drop w,t]
Perspective:   (x,y,z,w,t) → (x,y,z)/f(w,t)  [scale by distance]

The Hidden Dimension Slice

When you set the hidden dimension w to a value w₀, you're looking at a 3D slice (technically a 4D slice with time) of the 5D fractal:

Slice at w = w₀:
S = {(x,y,z,w₀,t) : fractal(x,y,z,w₀,t) < threshold}

This is a 4D manifold embedded in 5D space

As you move the slider through different w values, you're traveling through the 5D structure perpendicular to your viewing plane. Each position shows a different cross-section.

Continuity and Smoothness

Why Smooth in 5D Means Smooth in Any View

Here's the key insight: smoothness is an intrinsic property of the 5D fractal, independent of how you view it.

Mathematically, a function f: ℝ⁵ → ℝ is smooth if all its partial derivatives exist and are continuous:

∂f/∂x₀, ∂f/∂x₁, ∂f/∂x₂, ∂f/∂x₃, ∂f/∂x₄ all continuous
∂²f/∂x_i∂x_j all continuous
... and so on

When you project to 3D, you're looking at a restriction of this smooth function:

// Original 5D function
f(x₀, x₁, x₂, x₃, x₄)

// Projected 3D view (fixing x₃=w₀, x₄=t₀)
g(x₀, x₁, x₂) = f(x₀, x₁, x₂, w₀, t₀)

If f was smooth in 5D, then g is automatically smooth in 3D because:

∂g/∂x₀ = ∂f/∂x₀  (just restrict the derivative)

Continuity is preserved under projection. No matter which 4 dimensions you choose to view, the projection inherits the continuity of the original 5D structure.

Smooth Transitions Between Views

When you switch which dimensions map to XYZ, the fractal structure morphs smoothly because you're continuously rotating your viewpoint in 5D space. Think of it like walking around a 3D sculpture - the object doesn't change, only your perspective does.

The transition can be interpolated:

// Old view: dimensions (0,1,2) → XYZ
// New view: dimensions (1,2,3) → XYZ

// Interpolate with rotation matrix R(θ) in 5D
view(θ) = R(θ) · original_view

As θ: 0 → π/2, smoothly rotate from old to new

Coherence Across All Projections

The "coherence" you see - that the fractal looks structured from any angle - comes from the fact that fractals have self-similarity in all directions. The 5D fractal has correlation between all its dimensions:

// A point is in the fractal set if:
|z_n| < threshold for all n iterations

where z_n ∈ ℝ⁵ evolves via:
z_{n+1} = f(z_n) + c

This evolution couples ALL 5 dimensions together

Because the dimensions are coupled through the iterative formula, structure in one dimension influences structure in all others. You can't have chaos in one view and smoothness in another - the 5D dynamics enforce global coherence.

Mathematical Guarantee

For Mandelbrot-type fractals, the boundary (where |z_n| ≈ threshold) is a 4D manifold embedded in 5D space. This manifold is closed and bounded (compact), which guarantees:

  • Any projection to 3D is bounded (won't extend to infinity)
  • Any projection to 3D is continuous (no sudden jumps)
  • Topological features (holes, connectedness) are preserved in generic projections

This is why switching views doesn't break the fractal - you're always looking at different slices of the same underlying 4D boundary surface.

Future Directions

Open Questions

  • Convergence: Does the procedural → NeRF → reinterpret cycle converge to stable structure?
  • Attraction: Can we introduce forces that pull representations toward similarity under projection?
  • Hybrid dynamics: What happens when diffusion and neural resampling operate simultaneously?
  • Topological persistence: Which features survive multiple representation changes?

Technical Implementation

GPU Acceleration

The system uses Taichi for GPU-accelerated simulation (diffusion dynamics on 128³–256³ grids) and custom GLSL shaders for real-time ray marching. Memory management is critical—a 256³ single-channel field requires 64MB, three-channel requires 192MB.

Performance Targets

Project Status: This is an experimental/exploratory project investigating how high-dimensional volumetric data can evolve across representations. The base procedural viewer exists; Taichi diffusion and NeRF integration are in active development. The concepts explored here—arbitrary slicing, representation duality, evolution via resampling—represent ongoing research directions rather than finished tools.