The Art of Performance: Building a High-Stakes 3D Portfolio for the Web
🎏

The Art of Performance: Building a High-Stakes 3D Portfolio for the Web

Tags
Research
Thought Experiments
Published
May 9, 2026
Author
mumeyong
For my latest 3D portfolio mumeyong.dev project, the goal was clear; create an immersive, hand-drawn “inetactive workspace”
 

Intro

In the world of creative web development, we often face a “Performance vs. Beauty” paradox. For my latest 3D portfolio project, the goal was clear: create an immersive, hand-drawn “interactive workspace” that feels like a living sketch, while ensuring it runs at a buttery-smooth 60 FPS even on a three-year-old Android phone.
 
Here is the technical breakdown of how I achieved the signature Brush-Stroke Reveal effect and optimized the engine for mobile devices.
 
Animation Credit: CSS-only coding animation by Chris Dermody
Animation Credit: CSS-only coding animation by Chris Dermody
 
The Visual Magic: GLSL Brush-Stroke Reveal
The centerpiece of the user experience is the organic reveal effect. When you hover over a "sketch" element (like a balloon or a door), it doesn't just fade in; it feels like it’s being painted in real-time.
The Problem Traditional opacity fades look digital and "cheap." To get a "painted" look, I needed a way to discard pixels based on a noisy, hand-drawn mask that travels across the texture.
The Solution: Shader Injection Instead of writing a custom material from scratch, I used Three.js’s onBeforeCompile to inject custom GLSL into the standard MeshBasicMaterial. This allowed me to keep standard features while adding a custom Alpha-Discard logic.
// The core logic inside the Fragment Shader if (uProgress > 0.001) { // 1. Generate organic 2D noise based on UVs float rn = revealNoise(vMapUv * 15.0) * 0.15; // 2. Create a directional mask (Bottom to Top) // vMapUv.y provides the direction, 'rn' adds the rough edge float maskValue = (1.0 - vMapUv.y) + rn; // 3. Compare against the animation progress float threshold = uProgress * 1.5; // 4. Discard pixels to create the "torn" or "painted" edge if (maskValue < threshold) discard; }
đź’ˇ
By scaling the uProgress uniform from 0 to 1 via GSAP, the pixels are discarded progressively from bottom to top, creating a tactile, "hand-painted" reveal.
 
Reducing Draw Calls: The Segmented Architecture
The project features an “infinite corridor” In 3D web development, the number of Draw Calls (requests sent to the GPU) is the primary killer of mobile performance.
 
The Strategy: “Infinite” Culling
Instead of rendering the entire corridor at once, I implemented a Segmented Architecture managed by an InfiniteCorridorManager:
  1. Corridor Partitioning: The world is split into 80-unit segments.
  1. Frustum & Z-Culling: Using a SegmentVisibilityWrapper , I track the camera’s Z-position. Any segment more than 5 units behind the camera is completely unmounted from the scene graph.
  1. Lazy-Loading Rooms: The heavy geometry for “The Gallery” or “The Studio” isn’t just hidden-it doesn’t exist until you are within 10 units of the door.
This keeps the active Draw Call count consistent, regardless of how “infinite” the corridor becomes.
 
Geometry Optimization: Planes over Polygons
To maintain the hand-drawn aesthetic, I made a conscious architectural choice: Everything is a Plane.
  • 2D as 3D: Instead of heavy 3D models for furniture or decorations, I used high-resolution WebP textures on simple PlaneGeometry. This reduced the vertex count by over 95%.
  • MeshBasicMaterial: For elements that don't require dynamic lighting, I used MeshBasicMaterial. This avoids expensive light-source calculations on mobile GPUs while perfectly preserving the flat, "sketchy" look of the art style.
  • Primitive Reuse: By caching geometries and materials using React’s useMemo, I ensured that multiple objects (like the floating balloons) share the same memory footprint.
 
Adaptive Performance Tiers
Not all devices are created equal. I built a PerformanceContext that detects hardware capabilities on load:
  • High Tier (Desktop): Enables 2x Pixel Density (DPR), antialiasing, and decorative "doodles" (floating stars, squiggles).
  • Low Tier (Mobile):
    • Caps DPR at 1.0 to reduce the number of pixels the GPU has to shade.
    • Discards non-essential decorative meshes.
    • Simplifies the Shader Reveal logic for touch devices to save on uniform updates.
 
Conclusion
Performance in WebGL isn't about one "silver bullet"—it's about a surgical approach to asset management. By combining custom GLSL for visual punch with aggressive culling and adaptive tiers, I was able to deliver an experience that feels like a high-end desktop app on a mobile browser.
Tech Stack Used:
  • Engine: Three.js / React Three Fiber
  • Animation: GSAP
  • Shaders: GLSL
  • Performance: Custom Tiering System & Segmented Rendering
 
👉
Follow me on twitter for more awesome stuff like this @mumeyong