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.
Â

Â
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:- Corridor Partitioning: The world is split into 80-unit segments.
- 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.
- 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 usedMeshBasicMaterial. 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
