33.8 C
New York
Thursday, June 11, 2026

Sketching the Inconceivable: A 3D Portfolio Constructed And not using a Single 3D Mannequin



I can’t mannequin 3D. That just about explains this whole undertaking.

For months, I had been looking Awwwards and The FWA and located websites like Igloo Inc, 3D mixed with infinite scrolling, and I simply thought: I want one thing like this. Then I noticed Bruno Simon’s portfolio with the little automobile. I knew I needed a 3D portfolio. I additionally knew I had zero Blender abilities and truthfully no real interest in faking it with another person’s fashions.

So I figured, why not simply code easy rectangles, planes, cubes, flat geometry, and wrap them in hand-drawn textures? I couldn’t sculpt a world in Blender, so I sketched one on flat rectangles as a substitute. That workaround by chance turned the entire visible id of the undertaking.

Yow will discover the supply code on GitHub.

[codrops_course_ad id=”115731″]

Why This Exists

Go browse any Fb group the place builders share portfolios. I did. About 90% of them are the identical factor: darkish background, neon accent colours, textual content on the left, picture on the correct. Lots of them seem like they have been generated by AI… they in all probability have been. AI has this very particular aesthetic tendency: black web page, neon glow, executed.

I don’t have an issue with these websites technically. Their UX might be higher than mine, truthfully – it’s onerous to get misplaced on a regular format. However I needed one thing completely different. I needed guests to really stroll by an area, not simply scroll down a web page about me. If somebody sees my portfolio and desires to work with me, they’ll work out how one can attain the Contact room. I’m not anxious about that.

So I got down to construct a portfolio you possibly can stroll by, not scroll by.

4 Months, From Sketch to Sky

The undertaking began in December 2025. Initially, I believed it will be a 2D illustrated web site – hand-drawn textures on flat HTML sections. However someplace within the first few weeks, I spotted flat HTML wasn’t going to chop it. This factor wanted precise 3D depth. So I moved the entire thing into Three.js and React Three Fiber, and all of a sudden I used to be constructing rooms.

4 months later, it was stay. 4 months of preventing with digicam methods and scroll mechanics I had no concept how one can construct. Additionally producing a ton of textures with AI, as a result of there was no manner I used to be drawing all of them by hand.

The Tech Stack

  • React 19 + React Three Fiber 9 + Three.js 0.182 for the 3D setting
  • GSAP 3.14 for all animations: digicam flights, door mechanics, reveal transitions
  • Vite 7 for builds and dev server
  • Customized GLSL shaders extending MeshBasicMaterial for the paint-reveal impact
  • WebP textures generated by way of AI (Google’s picture technology), compressed and trimmed to suit flat 3D geometry
  • PostHog for analytics, Lenis heritage in scroll philosophy

The Aesthetic: Why Every little thing Seems Hand-Drawn

This was by no means Plan B. From day one, I needed it to really feel like a sketch – such as you opened somebody’s pocket book and the drawings jumped into 3D. The paper texture backgrounds, the ink-line doorways, the doodles floating within the hall. All intentional.

What developed later was the colour. I had all these sketch-style textures, and someday I believed: what in the event that they painted themselves once you hover? What if hovering over one thing actually paints it with coloration?

That turned the principle interplay of the entire portfolio. Each clickable aspect begins as a black-and-white sketch and fills with coloration on hover – a brush-stroke reveal pushed by a customized shader. It’s mainly a visible trace – if one thing fills with coloration once you hover, it means you possibly can click on it and one thing will occur.

The PaintRevealMaterial Shader

The impact works by extending Three.js’s MeshBasicMaterial by onBeforeCompile, injecting customized fragment shader logic that blends between a sketch texture and a painted texture utilizing procedural noise:

// Brush-stroke mix: progressively swap sketch -> painted
if (uProgress > 0.001) {
    vec4 paintedColor = texture2D(uMapPainted, vMapUv);
    float rn = paintNoise(vMapUv * 15.0) * 0.15;
    // Reveal from bottom-left to top-right for natural really feel
    float maskValue = (1.0 - vMapUv.y) + rn;
    float threshold = uProgress * 1.5;
    if (maskValue < threshold) {
        diffuseColor = vec4(paintedColor.rgb, 1.0);
    }
}

The noise operate offers it messy, natural edges – so as a substitute of a clear wipe, you get one thing that really seems to be like paint bleeding on paper. uProgress is animated from 0 to 1 by GSAP on hover.

I went with extending MeshBasicMaterial quite than writing a shader from scratch as a result of I wanted the usual Three.js texture pipeline (UV mapping, coloration areas, transparency) to maintain working. The customized logic solely decides which pixels present the painted model – all the things else stays inventory.

Conserving all textures visually constant was truthfully one of many hardest components. Each texture was AI-generated, and getting AI to generate a whole lot of belongings in the identical hand-drawn type is… painful. Generally I simply generated 20 variations and picked the one which didn’t look utterly completely different from the remaining.

The Infinite Hall

The concept is straightforward: you enter a constructing by sketched double doorways, and behind them is a hall that stretches infinitely in each instructions. On the partitions, at alternating sides, are 4 doorways – every resulting in a room with its personal world inside.

The Chunking System

The hall is constructed from repeating segments, every 80 items lengthy, managed by InfiniteCorridorManager. Solely three segments are ever mounted: the one the digicam is in, plus one forward and one behind. As you scroll, segments spawn and despawn dynamically.

On high of that, every phase is wrapped in a SegmentVisibilityWrapper that makes use of useFrame to test whether or not the phase is definitely in view. In the event you’ve scrolled 5 items previous a phase, it hides fully – zero draw requires geometry the digicam isn’t even :

useFrame(() => {
    const isBehindCamera = digicam.place.z < endZ - 5;
    const isFarAhead = digicam.place.z > startZ + 30;
    const isVisible = !(isBehindCamera || isFarAhead);

    if (groupRef.present.seen !== isVisible) {
        groupRef.present.seen = isVisible;
    }
});

The doorway posed a tough problem. When the consumer first arrives, they see the doorway doorways – however behind these doorways is the infinite hall. I wanted phase -1 (the one behind the doorway) to exist for shader pre-compilation, however I couldn’t let it’s seen or its doorways would clip by the doorway doorways. The answer: a hideDoorsForSegments array that suppresses particular phase doorways through the entrance section, then reveals them as soon as the consumer has entered.

The Digicam System: 500 Traces of Ache

useInfiniteCamera.js is 500 strains lengthy, and truthfully nearly each a type of strains exists due to a bug I needed to repair.

The digicam does a number of issues on the similar time:

  • Scroll motion by way of GSAP Observer (unifies mouse wheel, contact, and trackpad)
  • Mouse parallax on desktop – the digicam sways gently as you progress the mouse
  • Gyroscope parallax on cellular – tilt your telephone, the hall shifts
  • Auto-glance – as you method a door, the digicam subtly turns towards it
  • Keyboard navigation – arrow keys, spacebar, Web page Up/Down for accessibility
  • Digicam override mode – when GSAP takes over for door entry/exit animations

The auto-glance system is value speaking about. For every door, the hook calculates proximity utilizing a begin/peak/finish distance mannequin with eased power:

for (const door of DOOR_POSITIONS) {
    const doorGlobalZ = zOffset + door.z;
    const dist = z - doorGlobalZ;

    let power = 0;
    if (dist > PEAK_DIST && dist < START_DIST) {
        power = (START_DIST - dist) / (START_DIST - PEAK_DIST);
    } else if (dist <= PEAK_DIST && dist > END_DIST) {
        power = (dist - END_DIST) / (PEAK_DIST - END_DIST);
    }

    if (power > 0) {
        const easedStrength = power * (2 - power); // easeOutQuad
        const dir = door.aspect === 'left' ? -1 : 1;
        // ...
    }
}

The result’s a small head-turn that makes it really feel just like the digicam notices the doorways by itself. It’s delicate, but it surely makes the hall really feel manner much less static.

The toughest a part of your complete undertaking, no contest, was the digicam system for coming into and exiting rooms. Whenever you click on a door, the digicam must:

  1. Align itself in entrance of the door (place + rotation)
  2. Look forward to the room to lazy-load
  3. Animate the door opening (deal with down -> door swing)
  4. Fly the digicam by the doorway into the room
  5. On exit: reverse your complete sequence with out hitting partitions or clipping by textures

Each single a type of steps had bugs. The digicam would snap as a substitute of shifting easily. It will clip by wall geometry. It will find yourself going through the improper path after exiting. The rotation math needed to account for the door’s angle (sawtooth partitions are angled, not straight), the digicam’s parallax state, and GSAP’s management handoff.

The DoorSection.jsx element – the one managing all of this – grew to 1,287 strains. I’m not pleased with the scale, however each line handles an actual edge case.

One trick I’m pleased with: after releasing digicam management again to the scroll system, the hook calculates the digicam’s present bodily rotation and derives an equal look offset, as a substitute of snapping to the “supreme” look for that place. This prevents a jarring snap when exiting a room close to a door:

const currentRotationY = digicam.rotation.y;
const parallaxContribution = parallax.present.x * 0.3;
const derivedGlance = (currentRotationY - parallaxContribution) / 3;

const diff = Math.abs(derivedGlance - initialGlance);
if (diff > 0.02) {
    glanceOffset.present = derivedGlance; // Begin from the place we ARE
    targetGlance.present = initialGlance;  // Clean towards supreme
}

The Rooms: Abstraction Over Predictability

The unique plan was regular rooms. A room with a desk. A room with cabinets. You understand – rooms. However that felt boring and predictable. If somebody walks down a hall in a constructing and opens a door, they count on a room. They don’t count on to all of a sudden be flying a paper airplane by clouds.

So each room turned its personal little world:

  • The Gallery: Mission playing cards hanging on an infinite clothesline, like laundry drying within the wind. Scroll sideways to browse. Infinite loop.
  • The Studio: Displays floating in house, scrolling vertically by my content material – movies, weblog posts, social media. Infinite in each instructions.
  • The About: You fly a paper airplane by an infinite sky stuffed with clouds and story milestones. Your biography as a flight path.
  • The Contact: A seashore by the ocean. Social media hyperlinks are floating barrels you click on to attach.

Virtually each room has infinity constructed into it. Within the Gallery, the playing cards loop. Within the Studio, the screens loop. In About, the sky by no means ends, identical to the hall itself. Solely Contact breaks the sample – and I feel that’s really higher. Contact is the vacation spot. It ought to really feel such as you’ve arrived someplace.

The About room had a very nasty technical problem. Whenever you fly by clouds after which exit again to the hall, the clouds would leak into the hall view. The issue: the invisible clipping wall behind the digicam additionally clipped issues inside the hall, as a result of the digicam enters the room at an angle.

The repair was two issues: curving the clipping boundary so it matched the angled entry path, and increasing the fly-in distance for the About room particularly – you journey additional into About than into some other room, giving the clipping wall extra clearance.

The Efficiency Disaster (or: Two Suns, One Moon, and Zero Seen Shadows)

After I first shared a preview hyperlink in Fb developer teams, the suggestions was clear: your website is gorgeous, but it surely runs like a slideshow.

I spent days making an attempt to determine what was killing efficiency. I used to be texture sizes, draw calls, shader complexity – all the things. Then I discovered it.

Two directional lights and an ambient mild have been casting real-time shadows throughout your complete scene. These have been customary Three.js lights I had added early in improvement for “correct” lighting. The issue? My scene is made fully of flat textured planes – the shadow maps have been computing advanced depth passes for geometry that visually confirmed no shadow distinction in any respect.

I eliminated each mild. Then I added a slight tint on to the textures in code – a tiny little bit of simulated shadow baked into the colour values. Visually? Virtually no change. Efficiency-wise? Night time and day.

The Shader Pre-Compilation Trick

One other piece of neighborhood suggestions: “Why do I anticipate the preloader, after which wait AGAIN after I click on a door?”

The reply was shader compilation. Three.js compiles shaders lazily – the primary time a fabric renders, the GPU compiles its shader program, inflicting a visual stutter. Each room had dozens of supplies, and so they all compiled on first entry.

The answer was RoomWarmup: a element that mounts all 4 rooms 500 items under the scene through the preloader section, forces the GPU to compile each shader by way of gl.compileAsync(), then unmounts all the things:

const RoomWarmup = ({ onWarmupComplete, isLowTier }) => {
    // Mount all rooms 500 items under the scene
    // Wait 3 frames for all the things to render
    // Then force-compile all shaders

    if (gl.compileAsync) {
        gl.compileAsync(scene, digicam, scene)
            .then(finishWarmup)
            .catch(() => {
                gl.compile(scene, digicam); // sync fallback
                finishWarmup();
            });
    }
};

// Within the JSX:
<group place={[0, -500, 0]}>
    <GalleryRoom showRoom={true} isWarmup={true} />
    <StudioRoom showRoom={true} isWarmup={true} />
    <AboutRoom showRoom={true} isWarmup={true} />
    <ContactRoom showRoom={true} isWarmup={true} />
</group>

On high-end units, this eradicated all entry stutter – each room opens immediately as a result of the GPU already is aware of each shader. On low-end units, the warmup is skipped fully (to forestall WebGL context loss from reminiscence strain), and rooms load on-demand with a slight delay.

I got here up with this method myself after studying the neighborhood complaints. No tutorial taught me this – it got here from frustration and a whole lot of console.log debugging.

The KTX2 Experiment That Failed

I learn in all places that KTX2 / Foundation Common textures are the gold customary for WebGL efficiency – GPU-native decompression, smaller payloads, the works. So I transformed all the things.

It was a catastrophe. Textures misaligned. Colours shifted. The preloader obtained two seconds slower. And the visible high quality on the hand-drawn textures – which depend upon crisp strains and delicate gradients – degraded noticeably.

I reverted to WebP. The location already ran at 60 FPS on telephones and 144 FPS on desktop with WebP textures. Generally the “appropriate” optimization simply isn’t value it if the present answer already works. I do know 3D builders may choose me for this, however I’m talking from the precise consumer expertise perspective – and it’s easy.

Adaptive Gadget Tiering

The location detects gadget capabilities at load time and adjusts accordingly:

const isMobileDevice = /iPhone|iPad|iPod|Android/i.take a look at(navigator.userAgent);
const isWeakCPU = navigator.hardwareConcurrency <= 4;
const isLowRAM = navigator.deviceMemory <= 4;
const isSmallScreen = window.innerWidth < 450;
const isLowEnd = isMobileDevice || isWeakCPU || isLowRAM || isSmallScreen;

Low-end units get fewer preloaded textures, no room warmup, and easier rendering. On high of that, a PerformanceMonitor from drei watches FPS in real-time and mechanically downgrades the standard tier if frames begin dropping.

On my predominant PC (3 screens, 144Hz), the location holds a gradual 144 FPS. On telephones, it maintains round 60 FPS. On my 80 euro Thinkpad – truthfully, it’s a miracle it runs in any respect – it manages 15-30 FPS. But it surely runs.

Sound Design: The Element That Modifications Every little thing

I observed that the websites I admired most – Bruno Simon, Igloo Inc – all had sound. Sound is what makes a 3D website really feel like a spot as a substitute of only a web page with graphics.

Each room has its personal ambient soundscape:

  • Hall: Background music loop, footstep-like atmosphere
  • Gallery: Metropolis hum
  • Studio: Monitor buzz, digital hum
  • About: Wind speeding previous as you fly
  • Contact: Ocean waves

Door interactions have three distinct sounds: a creak once you hover (the door tilts barely), a full open sound once you click on, and an in depth sound when it shuts behind you.

Discovering the correct sounds was genuinely irritating. Every little thing needed to be free for industrial use, match the aesthetic tonally, and really feel cohesive throughout all rooms. I’m nonetheless not 100% proud of each sound, however having some sound, even imperfect, is so significantly better than complete silence.

The Achievement System: Gamification as UX

Impressed by Bruno Simon’s portfolio (the place you possibly can knock over bowling pins and earn achievements), I added an achievement system that doubles as a tutorial.

Whenever you enter a brand new room, a tooltip seems: “Scroll to fly by my story” in About, “Drag to rotate and browse” in Studio. These aren’t simply directions although – they’re achievements ready to be unlocked. Full the motion, and the tooltip transforms right into a accomplished badge with a chime.

const ACHIEVEMENTS = {
    corridor_enter:  { label: 'Click on a door to enter', title: 'Explorer' },
    corridor_explore: { label: 'Scroll to discover the hall', title: 'Wanderer' },
    about_fly:       { label: 'Scroll to fly by my story', title: 'Sky Walker' },
    studio_interact: { label: 'Drag to rotate and browse', title: 'Director' },
    gallery_inspect: { label: 'Click on undertaking to examine', title: 'Artwork Critic' },
    contact_choose:  { label: 'Discover a contact methodology', title: 'Sociable' }
};

This solves an actual drawback: in a non-standard interface, individuals don’t know what to do. The achievements educate interplay patterns whereas rewarding exploration. Progress persists in localStorage, and each unlock fires a PostHog analytics occasion so I can monitor which rooms get probably the most engagement.

Recognition

The portfolio picked up some awards alongside the way in which:

  • GSAP Website of the Day + added to the official GSAP Showcase
  • FWA of the Day
  • CSSDA Particular Kudos + 3 Public Alternative Awards
  • Orpetron SOTD
  • CSS Winner SOTD
  • Awwwards Honorable Point out

What I Would Do Otherwise

Truthfully? Virtually nothing by way of method. The “mechanics first” philosophy – construct the scroll, the digicam, the hall logic earlier than touching a single texture – saved me from the entice of sprucing visuals on high of damaged foundations.

What I’d skip: these two directional lights and the moon shader that price me days of debugging for zero visible profit. And the KTX2 experiment. Generally figuring out what NOT to do is the actual optimization.

What This Mission Taught Me

That I’m nonetheless a newbie.

That is perhaps my first undertaking that achieved actual recognition – the awards, the neighborhood response, the prospect to put in writing this text. My earlier undertaking, a website for the Polish rapper Younger Multi, obtained consideration after I first constructed it. However after I have a look at it now, I see all the things I’d do otherwise.

That’s how I do know I’m rising.

This portfolio isn’t excellent both. The UX isn’t as clear as a regular format – I do know that. However individuals appear to get pleasure from exploring it, which implies one thing went proper.

What Comes Subsequent

My largest aim proper now: constructing an internet site for the Polish hip-hop collective 2115 and submitting it to the Webby Awards and Lovie Awards. It’s a excessive bar. However once you’re getting awards like FWA of the Day at this age, and getting invited to put in writing for Codrops – I imply, there’s actually no such factor as not possible.

One Piece of Recommendation

First: write the screenplay. Earlier than you contact a single line of code, think about the movie. What does the consumer see? The place does the digicam go? What’s the story?

Then construct the mechanics with easy shapes. Rectangles. Cubes. No textures, no shaders – simply the uncooked motion and circulation. Make it really feel proper when it seems to be like nothing.

Solely then do you costume the world.

Tech is rarely the blocker – it’s at all times your creativeness. In the event you can consider an answer, you possibly can code it. It simply takes time and stubbornness.



Supply hyperlink

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles