DEV Community

Cover image for Building a 3D Formula Visualizer: Lessons Learned
Muhammad Fahmi Rasyid
Muhammad Fahmi Rasyid

Posted on

Building a 3D Formula Visualizer: Lessons Learned

Hey folks! 👋 A while ago, I embarked on a journey to build a 3D formula visualization tool using React, Three.js, and TypeScript. It sounded simple at first—just render some mathematical formulas in 3D and let users tweak parameters in real-time. But as with any project, the reality was far from straightforward. Along the way, I hit roadblocks, made mistakes, and learned a ton. If you're thinking of doing something similar, let me share what worked (and what didn’t) so you can hopefully avoid some of the pitfalls I encountered.

App Screenshoot

Lesson 1: Future-Proof Your Code

When I first started, I knew I needed a way to manage different formulas cleanly. At first, I had a bunch of functions scattered everywhere, each handling its own calculations and geometry generation. This quickly became a nightmare. Every time I wanted to add a new formula, I had to copy-paste logic and tweak it manually—definitely not sustainable.

Initially, I stored formulas in a simple object-based structure, something like this:

export const formulas: Record<string, Formula> = {
  gielis: {
    metadata: {
      name: "Gielis",
      description: "A variation of the superformula",
      parameters: {
        a: { name: "a", min: 0.1, max: 10, step: 0.1 },
        b: { name: "b", min: 0.1, max: 10, step: 0.1 },
        m: { name: "m", min: 0, max: 20, step: 1 },
        n1: { name: "n1", min: 0.1, max: 10, step: 0.1 },
      },
    },
    calculate: ({ phi, a, b, m, n1, n2, n3 }) => {
      return Math.pow(
        Math.pow(Math.abs(Math.cos((m * phi) / 4) / a), n2) +
        Math.pow(Math.abs(Math.sin((m * phi) / 4) / b), n3),
        -1 / n1
      );
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

This worked at first, but as I added more formulas, maintaining this structure became unwieldy. So, I decided to go with an abstract base class to enforce structure. The idea was to have a consistent API that every formula would follow, making expansion easier and reducing redundancy. Here’s what I came up with:

// filepath: /formulas/BaseFormula.ts
abstract class BaseFormula implements Formula {
  abstract metadata: FormulaMetadata;
  abstract calculate(params: FormulaParams): number;
  abstract createGeometry(params: FormulaParams): THREE.BufferGeometry;
}
Enter fullscreen mode Exit fullscreen mode

To further improve maintainability, I transitioned to a class-based registry system:

import { Formula } from "../types/Formula";
import { GielisFormula } from "./GielisFormula";
import { TerrainFormula } from "./TerrainFormula";
import { SineInterferenceFormula } from "./SineInterferenceFormula";
import { GyroidFormula } from "./GyroidFormula";
import { CellularNoiseFormula } from "./CellularNoiseFormula";
import { MobiusFormula } from "./MobiusFormula";

export const formulaRegistry: Record<string, Formula> = {
  gielis: new GielisFormula(),
  terrainGen: new TerrainFormula(),
  sineInterference: new SineInterferenceFormula(),
  gyroid: new GyroidFormula(),
  cellularNoise: new CellularNoiseFormula(),
  mobius: new MobiusFormula(),
};

export const getFormula = (type: string): Formula => {
  const formula = formulaRegistry[type];
  if (!formula) {
    throw new Error(`Formula type '${type}' not found`);
  }
  return formula;
};
Enter fullscreen mode Exit fullscreen mode

This simple refactor had a huge impact. Every formula now extended from this base class, keeping things modular and predictable. The calculations were separate from the Three.js geometry logic, making everything easier to debug and maintain. Plus, with TypeScript enforcing structure, I avoided a ton of runtime errors. It was a game-changer for scalability.

Lesson 2: Real-Time 3D Rendering is Tricky

With the formula structure in place, I thought the hard part was over. But then came the real challenge—real-time rendering. I wanted users to tweak parameters and see their changes reflected instantly in the 3D visualization. Simple in theory, but in practice? Not so much.

The first version of my app was sluggish. Every update caused unnecessary re-renders, and the scene management was all over the place. The solution? I had to rethink how updates were handled. I found two key improvements that made a world of difference.

First, I centralized my scene management. Instead of each formula handling its own objects independently, I created a single place where objects were added, updated, and removed dynamically. This ensured that changes in parameters translated directly into visual updates without causing conflicts or memory leaks.

Second, I made React state the driver of updates. Instead of manually modifying the Three.js scene, I let React’s state management handle the reactivity. Whenever a user adjusted a parameter, the state changed, triggering a recalculation and an efficient update to the scene. This cut down on unnecessary re-renders and made the visualization feel much smoother.

What’s Next? 🚀

Building this 3D formula visualizer was an eye-opening experience. While I solved many challenges, there’s still so much more to explore. In future iterations, I want to add support for custom formulas, letting users define their own equations and see them rendered in real-time. I also want to build a more intuitive parameter control system to make tweaking values feel seamless.

You can check out the project on GitHub: UltraFormula Repo and try the live demo here: UltraFormula on Vercel.

Right now, the app supports six different formula visualizations:

  • Gielis Formula
  • Perlin Noise Terrain Generator
  • Sine Interference Pattern
  • Gyroid Surface
  • Cellular Noise
  • Möbius Strip

Have you worked on something similar? What challenges did you face? I’d love to hear about your experiences—drop a comment below! 👇

Top comments (1)

Collapse
 
bharanidharan_natarajan_1 profile image
Bharanidharan Natarajan

Nice article