Creative WebGL & Audio

How I Built a 3D Birthday Memory Museum with Three.js, Web Audio DSP & Friendly Roasts

Experience the 3D Birthday Museum Interactive 3D WebGL museum with microphone candle blowing, spatial lighting, and squad roasts.
Launch Live 3D Museum → View GitHub Source ↗

By Kantaraj Luitel (@susantedit) • 10 min read • 🔗 Live Preview Demo • 🎥 Preview Video

"This isn't a simple birthday text message. It's a full 3D interactive museum inside your phone browser."

3D Birthday Memory Museum Cover

🎥 Preview Video


Introduction: Why a Text Message Wasn't Enough

When your best friend Aakash Rai (affectionately known as "kaley vai") turns a year older, typing "Happy Birthday bro! 🎉" into WhatsApp feels like an insult.

When you've survived years of shared adventures, questionable fashion choices, late-night gaming sessions, and countless embarrassing moments together, a generic message simply doesn't cut it. True friendship, especially between close friends, is built on a sacred foundation: the affectionate birthday roast.

I wanted to give Aakash something he'd never forget, a private, interactive 3D WebGL Museum that he could open on his phone.

Imagine opening a single link on your mobile browser. Suddenly, you're standing in a quiet, warmly lit wooden gallery. As you scroll, photos and videos of your squad float along the walls in high-definition 3D frames. Each photo is accompanied by a custom caption roasting your poses, outfits, and past antics. Ambient party lights pulse across the polished floor in sync with custom background music. At the end of the corridor stands a multi-tiered birthday cake under a spotlight, and to reveal the final birthday letter, you actually have to blow into your phone's microphone to extinguish the candles.

Building this wasn't just a fun weekend project; it was an intense engineering exercise in Three.js graphics optimization, Web Audio signal processing, and desktop-to-static build pipelines.

Here is the complete story of how I built the Birthday Memory Museum, translated the jokes, and solved the technical hurdles of running high-end 3D graphics smoothly on mobile devices.


The Culture of the Friendly Roast: Translating the Inside Jokes

In Nepali youth culture, birthday wishes among close friends are rarely formal. We speak in Neplish (a blend of Nepali and English) packed with sarcasm, pop-culture references, and local political humor.

When building the museum, I wanted every frame along the 3D corridor to hit Aakash with a wave of nostalgia followed by a laugh. Here are a few actual captions directly from the museum codebase and screenshots, translated into English:

Caption 1: The Political Entrance

Caption 2: The Shocking Style

Caption 3: The Local Squad Breakdown


The Engineering Challenge

While the content was fun, the technical constraints were formidably serious:

  1. Mobile WebGL Texture Constraints: Modern smartphone cameras take 12MP to 48MP photos (4MB to 10MB each). Loading 16 raw JPEGs into a single Three.js WebGL scene consumes hundreds of megabytes of uncompressed GPU VRAM, causing mobile browsers (especially iOS Safari) to crash instantly.
  2. Microphone Signal Discrimination: Simple Web Audio API volume triggers are useless in real life. If someone laughs, shouts "blow it out!", or if background music is playing, a naive threshold detector fires prematurely. The app needed a Digital Signal Processor (DSP) that could tell the difference between human speech and physical wind turbulence.
  3. Grazing-Angle Texture Blur: In a long architectural corridor viewed down the camera's Z-axis, floor and wall textures meet the view frustum at extreme grazing angles. Standard WebGL mipmapping renders these as blurry beige gradients unless handled specifically.
  4. Zero-Config Publishing for Non-Technical Users: The authoring tool needed to run locally on my PC (http://localhost:4321), process raw media, compile the code, and export a self-contained static site bundle (gift.zip) that could be deployed to Netlify without touching a single cloud server or command-line interface.

What I Built

The Birthday Memory Museum consists of two decoupled architectures:

  1. The Local Authoring Studio: An Express and Node.js desktop environment (http://localhost:4321) where you upload media, arrange frame order, write captions, preview the 3D scene live, and trigger automated builds.
  2. The Published Static Viewer: A mobile-optimized, standalone WebGL web app bundled by Vite. It includes compressed WebP asset derivatives, progressive loading scripts, and custom Three.js shaders.

Birthday Gift Studio 3D Gallery Corridor Live Preview Figure 1: Birthday Gift Studio interface displaying the live 3D gallery corridor preview, custom caption HUD overlay, party banners, and media ordering panel.


Tech Stack

Layer Technology Engineering Purpose
3D Rendering Three.js (0.168.0) Scene graph management, custom geometry, lighting, particle dust, smoke, confetti, camera interpolation
Bundling Engine Vite (5.4.11) + Terser HMR middleware mode for Studio, tree-shaking, production JS chunk minification
Local Backend Node.js + Express (4.21.2) Local REST server, state persistence (state.json), static asset streaming
Image Pipeline Sharp (0.33.5) EXIF rotation parsing, WebP encoding, creation of 1024px, 512px, and 32px LQIP derivatives
Video Pipeline Fluent-FFmpeg + ffmpeg-static Transcoding raw phone videos to mobile H.264 MP4s and extracting poster stills
Audio & Signal DSP Web Audio API (AudioContext, AnalyserNode) Real-time FFT frequency spectrum analysis, spectral flux measurement, acoustic blow classification
Archiver & Exporter Archiver (7.0.1) + qrcode Packaging compiled static web assets into gift.zip and SVG QR generation

How It Works: Interactive System Architecture & Flow

flowchart TD
    subgraph Studio["Local Authoring Studio (Sender PC)"]
        A[User Drag & Drops Photos / Videos / MP3] --> B[Express Server & Multer]
        B --> C[Sharp Image Processing Engine]
        B --> D[FFmpeg Transcoder Pipeline]
        C --> E[Generate WebP Derivatives: Full 1024px, Mid 512px, 32px LQIP]
        D --> F[Generate H.264 MP4 & WebP Poster Stills]
        E --> G[Persist State to state.json]
        F --> G
        G --> H[Vite Bundler & Archiver Engine]
        H --> I[Output dist/ Bundle & gift.zip Archive]
    end

    subgraph Host["Static Hosting Layer"]
        I --> J[Netlify Drop / Netlify CLI Deployment]
        J --> K[Public Shareable HTTPS Static Link]
    end

    subgraph Recipient["Recipient Mobile Phone Browser"]
        K --> L[Fetch gift.json Manifest & LQIP Placeholders]
        L --> M[Execute GPU Benchmark & Hardware Tier Detector]
        M --> N[Initialize Three.js 3D Corridor & Camera Loop]
        N --> O[Web Audio Beat Analysis & Ambient Lighting]
        O --> P[Real-Time Mic Blow Classification DSP]
        P --> Q[Extinguish Candles & Trigger Confetti Finale]
    end

Key Features

1. Dynamic 3D Camera Traversal & Framing

The corridor length scales dynamically based on the frame count. Camera position is tied to page scroll, easing smoothly down the Z-axis while applying subtle rotational turns toward alternating wall frames as they pass through the camera's attention zone.

2. Audio-Reactive Lighting & Particle Physics

The museum environment reacts dynamically to background music. An AnalyserNode computes energy levels across low, mid, and high frequency spectrums. Beat pulses modulate spotlight intensity, wall sconce colors, and ambient airborne dust particle velocity.

3. Acoustic Microphone Blow Detection

Rather than relying on primitive volume thresholds, the museum uses a custom DSP classifier. It evaluates spectral envelope structure, high-frequency sibilance ratios, and temporal flux to distinguish physical wind breath from spoken words or laughter.

4. Adaptive Mobile Hardware Tiering

To guarantee a solid 60 FPS across high-end flagships and budget smartphones alike, the system benchmarks device hardware upon boot. It checks GPU renderer strings (WEBGL_debug_renderer_info), CPU core count (hardwareConcurrency), and device memory, followed by a 450ms runtime frame rate probe (probeFPS) that dynamically toggles post-processing bloom, shadow map resolution, particle counts, and render scales.

Birthday Gift Studio Cake Pedestal & Frame Inspection View Figure 2: The finale scene approaching the 3D birthday cake pedestal with lit candles, spotlight cone, floating atmospheric particles, and personalized letter preview.


Important Technical Implementation

1. Acoustic Blow Classifier (src/audio/blow.js)

The core technical challenge in mic blow detection is separating wind noise from speech.

To analyze this accurately, raw FFT decibel bytes are converted into linear amplitude space before computing band averages across low (0 to 500Hz), mid (500 to 2000Hz), and high (2000 to 8000Hz) bands:

// From src/audio/blow.js
// Precomputing byte -> linear amplitude prevents logarithmic decibel compression from flattening band ratios
const LINEAR = new Float32Array(256);
for (let b = 0; b < 256; b++) {
  LINEAR[b] = b === 0 ? 0 : Math.pow(10, (DB_MIN + (b / 255) * (DB_MAX - DB_MIN)) / 20);
}

export function analyseSpectrum(freqData, sampleRate, prevSpectrum) {
  const binCount = freqData.length;
  const hzPerBin = (sampleRate / 2) / binCount;

  const clampBin = (hz) => Math.min(binCount, Math.max(1, Math.round(hz / hzPerBin)));
  const lowCut = clampBin(BAND_LOW_HZ);
  const midCut = Math.max(lowCut + 1, clampBin(BAND_MID_HZ));
  const topCut = Math.max(midCut + 1, clampBin(BAND_TOP_HZ));

  let lowLin = 0, midLin = 0, highLin = 0, loudSum = 0, flux = 0;

  for (let i = 0; i < topCut; i++) {
    const b = freqData[i];
    const lin = LINEAR[b];
    const db = b / 255;

    if (i < lowCut) { lowLin += lin; loudSum += db; }
    else if (i < midCut) { midLin += lin; loudSum += db; }
    else highLin += lin;

    if (prevSpectrum) {
      const d = db - prevSpectrum[i];
      if (d > 0) flux += d;
      prevSpectrum[i] = db;
    }
  }

  const lowMean = lowLin / lowCut;
  const midMean = midLin / (midCut - lowCut);
  const highMean = highLin / (topCut - midCut);
  const meanSum = lowMean + midMean + highMean;

  return {
    level: loudSum / midCut,
    lowRatio: meanSum > 0 ? lowMean / meanSum : 0,
    highRatio: meanSum > 0 ? highMean / meanSum : 0,
    flux: flux / topCut,
  };
}

The classified metrics must pass four simultaneous conditions sustained over 180ms (SUSTAIN_MS):

export function classifyBlow(m, noiseFloor) {
  const threshold = Math.min(0.92, noiseFloor + LEVEL_OVER_FLOOR);

  const checks = {
    loudEnough: m.level > threshold,
    lowDominant: m.lowRatio > 0.34,   // Low band energy must dominate (breath noise)
    notHissy: m.highRatio < 0.38,     // Low sibilance (rejects 'sh' / 'sss' speech sounds)
    sustained: m.flux < 0.09,          // Low spectral flux (rejects rapid speech transients)
  };

  const isBlow = checks.loudEnough && checks.lowDominant && checks.notHissy && checks.sustained;
  const strength = Math.min(1, Math.max(0, (m.level - threshold) / 0.22));

  return { isBlow, strength, threshold, checks };
}

Crucial Takeaway: Native browser audio constraints (echoCancellation, noiseSuppression, autoGainControl) are explicitly set to false during getUserMedia() invocation. Operating system drivers treat wind turbulence as unwanted noise and attempt to mute the exact signal required to detect candle blowing!


2. Grazing-Angle Sharpness & Hardware Tiering (src/core/tiers.js)

In a corridor viewed down its main axis, floor and wall planes present at extreme grazing angles relative to the camera vector. Default WebGL mipmapping picks low-detail mipmaps for stretched footprints, smearing floor planks and fabric textures.

We configure tier profiles and prioritize anisotropic filtering (anisotropy: 8) even on low-end hardware:

// From src/core/tiers.js
export const TIERS = {
  LOW: {
    name: 'low',
    texture: 512,
    pixelRatio: 1.5,
    antialias: true,
    bloom: false,
    confetti: 250,
    dust: 1800,
    anisotropy: 8,      // Anisotropic sampling prevents grazing-angle corridor blur
    surfaceTex: 1024,   // Procedural canvas textures generated once at boot
  },
  MID: {
    name: 'mid',
    texture: 1024,
    pixelRatio: 2.0,
    antialias: true,
    bloom: true,
    confetti: 500,
    dust: 3000,
    anisotropy: 8,
    surfaceTex: 1024,
  },
  HIGH: {
    name: 'high',
    texture: 1024,
    pixelRatio: 2.5,
    antialias: true,
    bloom: true,
    confetti: 900,
    dust: 6000,
    anisotropy: 16,
    surfaceTex: 2048,
  }
};

3. EXIF Auto-Rotation & Multi-Tier WebP Pipeline (server/pipeline/images.js)

Raw smartphone JPEGs carry orientation tags in EXIF metadata. GPUs do not parse EXIF metadata, so unrotated images land sideways on 3D meshes.

Our pipeline uses Sharp to apply EXIF rotations directly to pixel buffers before generating three WebP derivatives per photo:

// From server/pipeline/images.js
export async function processImage(srcPath, outDir, slug) {
  // `rotate()` bakes EXIF orientation into the image buffer before resizing
  const base = sharp(srcPath).rotate();

  const outputs = {};
  for (const [key, edge] of Object.entries(SIZES)) {
    const file = key === 'full' ? `${slug}.webp` : `${slug}_${key}.webp`;
    const dest = path.join(outDir, file);

    let pipe = sharp(srcPath)
      .rotate()
      .resize({
        width: edge,
        height: edge,
        fit: 'inside',
        withoutEnlargement: true,
      });

    if (key === 'lqip') {
      pipe = pipe.blur(1.4).webp({ quality: 50 });
    } else {
      pipe = pipe.webp({ quality: 78, effort: 5 });
    }

    const info = await pipe.toFile(dest);
    outputs[key] = { file, w: info.width, h: info.height };
  }

  return {
    type: 'photo',
    src: outputs.full.file,       // 1024px full quality
    src512: outputs.mid.file,    // 512px mid tier for mobile devices
    lqip: outputs.lqip.file,      // 32px blurred placeholder
    w: outputs.full.w,
    h: outputs.full.h,
  };
}

Technical Challenges and Solutions

Challenge 1: Mic Blow Detection False Positives

Challenge 2: Mobile Safari VRAM Out-of-Memory Tab Crashes

Challenge 3: Vite Terser Minification Exception in ESM Scope


Project Architecture

Birthday Gift/
├── package.json             # Root configuration & scripts (Three.js, Express, Sharp, Vite)
├── vite.config.js           # Build settings pointing to src/viewer entry point
├── server/                  # Local authoring Studio backend
│   ├── index.js             # Express API server, file routes, HMR preview middleware
│   └── pipeline/            # Asset pipeline (images.js, video.js, audio.js, export.js)
├── studio/                  # Authoring UI frontend (Vanilla JS, CSS)
├── src/                     # 3D Museum Viewer codebase
│   ├── viewer/              # Viewer entry point (main.js, index.html)
│   ├── core/                # Engine, hardware tiers, loop, interaction, scroll
│   ├── scene/               # 3D geometry (room, frames, cake, lighting, decor)
│   ├── fx/                  # Render effects (beams, dust, smoke, confetti, reflections)
│   └── audio/               # Web Audio API engines (blow.js, music.js)
└── dist/                    # Output directory for compiled static distribution

How to Run & Deploy the Project

1. Prerequisites

2. Installation

Clone the repository and install dependencies:

# Clone the repository
git clone https://github.com/susantedit/birthday-special.git
cd birthday-special

# Install project dependencies
npm install

3. Launch the Local Authoring Studio

Start the Studio server:

npm run studio

Open your browser and navigate to:

http://localhost:4321

4. Build & Publish

  1. Upload photos, videos, custom background music, and write captions.
  2. Click Export & Publish in the Studio panel.
  3. Download gift.zip and drag it onto Netlify Drop to get your live HTTPS URL.

Lessons Learned

  1. Linear Amplitude vs Decibels in Audio Analysis: Calculating band ratios on raw FFT byte arrays (decibels) severely compresses signal contrast. Converting bytes to linear amplitude (Math.pow(10, dB / 20)) expands signal resolution, making acoustic classification reliable across diverse hardware.
  2. Anisotropic Filtering is a Game Changer for 3D Rooms: Increasing render resolution on mobile WebGL incurs heavy GPU fill-rate costs. Enabling anisotropic filtering (anisotropy: 8) sharpens floor and wall textures at extreme camera angles with zero fill-rate overhead.
  3. Always Build Graceful Fallbacks: Web features like microphone access depend on HTTPS contexts and user permissions. Providing a prominent "Tap candle to blow out" fallback ensures the experience remains functional even on desktop computers or restricted browser settings.

Future Enhancements


Conclusion

Building the Birthday Memory Museum proved that code can be both a high-performance technical discipline and a deeply personal medium for storytelling.

Aakash got a hilarious, unforgettable birthday gift filled with inside jokes, squad roasts, and memories that brought back years of laughs, and I got to engineer a mobile-optimized 3D WebGL architecture that runs at 60 FPS on any phone.

Kanta Raj Luitel

Written by Kanta Raj Luitel

Full-Stack Developer, AI Builder & Cyber Security Researcher from Nepal. Building high-performance 3D WebGL worlds, reactive Web Audio DSP architectures, and defensible web applications.