惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
C
CXSECURITY Database RSS Feed - CXSecurity.com
L
LINUX DO - 热门话题
S
Secure Thoughts
TaoSecurity Blog
TaoSecurity Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
T
Threat Research - Cisco Blogs
AI
AI
B
Blog RSS Feed
S
Schneier on Security
雷峰网
雷峰网
Schneier on Security
Schneier on Security
Help Net Security
Help Net Security
Cloudbric
Cloudbric
L
LINUX DO - 最新话题
罗磊的独立博客
有赞技术团队
有赞技术团队
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Apple Machine Learning Research
Apple Machine Learning Research
P
Proofpoint News Feed
酷 壳 – CoolShell
酷 壳 – CoolShell
The Hacker News
The Hacker News
博客园 - Franky
Attack and Defense Labs
Attack and Defense Labs
The Cloudflare Blog
Webroot Blog
Webroot Blog
Last Week in AI
Last Week in AI
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
博客园 - 叶小钗
美团技术团队
L
Lohrmann on Cybersecurity
T
The Blog of Author Tim Ferriss
The Last Watchdog
The Last Watchdog
T
Troy Hunt's Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Vercel News
Vercel News
Know Your Adversary
Know Your Adversary
O
OpenAI News
博客园 - 【当耐特】
Hacker News - Newest:
Hacker News - Newest: "LLM"
C
Cybersecurity and Infrastructure Security Agency CISA
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
www.infosecurity-magazine.com
www.infosecurity-magazine.com
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
PCI Perspectives
PCI Perspectives
H
Heimdal Security Blog
I
InfoQ
GbyAI
GbyAI
T
Threatpost
C
Cisco Blogs

Codrops

Meet the Speakers of the First Three.js Conference | Codrops The Architecture Behind Trionn: Coordinating GSAP, Three.js, Lenis, and Web Audio | Codrops Where Stories Become Interactive Worlds: How makemepulse Brings Ideas to Life Across Web, XR, and Beyond | Codrops The Sleepers: Creating an Atmospheric WebGL Experience with Lightweight Techniques Building an Interactive Wave Propagation Cube Grid with Three.js | Codrops Ten Years Away: Designing an Interactive Comic for Studio375’s Tenth Anniversary | Codrops Building a Scroll-Driven 3D Gallery Using a Blender Camera Path with Three.js and GSAP | Codrops From Years of Client Work to a Next.js + Sanity Starter Kit | Codrops Sculpting a Digital Athlete: Capturing Stefanos Tsitsipas Beyond the Court | Codrops Building Persistent Page Transitions with WebGPU and Vanilla JavaScript | Codrops Inside Bisous: Designing an Editorial Experience for Cinematic CGI | Codrops Engineering the Web Experience Behind Shopify’s Spring ’26 Edition: Everywhere | Codrops Shaping Stories into Experience: The Work of Kevin Lam | Codrops Testing What Users Actually See with Vitest and Chromatic | Codrops Podium: Building a Website Where Running Becomes Storytelling | Codrops Designing Beyond the Surface: How DashDigital Turns Complexity into Clarity | Codrops Creative Entrepreneurship: Designing the Machine | Codrops Exploring 3D Image Rotations on Scroll | Codrops Always Building, Always Learning: Cyd Stumpel’s Journey Through the Modern Web | Codrops Designing A Better Lou: Reducing Cognitive Load Through Design, Content, and Systems | Codrops Building Horeca: Advanced Motion Design in Webflow Without the Performance Trade-Offs | Codrops Building an Interactive Digital Stamp Collection with Shaders, Postcards, and Playful Inspection | Codrops No Two Paths Alike: Inside San Rita’s Approach to Digital Experiences | Codrops The Making of the New Lesse Studio Website: Clarity, Performance, and Intentionality | Codrops Creating a Thumbnail Flow Animation with GSAP MotionPath | Codrops Forging Her Own Path: Houmahani Kane’s Journey in Creative Development | Codrops Beaucoup and the Pursuit of Memorable Experiences | Codrops The Future of Emotional Technology: Comfort Websites for Growth and Self-Awareness | Codrops The Never Ending Story: Building a Seamless Infinite Scroll Experience with GSAP & Lenis | Codrops Whooshes, Snaps and Shaders: Adrien Vanderpotte and the Feeling of the Interface | Codrops Building a Scroll-Driven 3D Cube Gallery in Webflow with GSAP | Codrops Creating Scroll-Driven SVG Map Animations with GSAP | Codrops Made With Gsap: Building a Fun Gravity-Based Mouse Trail | Codrops 80s Business Tech and Seamless Scene Transitions: Inside Shader.se’s Scroll-Driven WebGPU Pipeline | Codrops Merlin: The Code Boutique Turning Motion Into Digital Magic | Codrops Designing Ourselves: The New Obys Identity and Website | Codrops Exploring the HTML-in-Canvas Proposal | Codrops Le:mma Studio: Building the Feeling Behind the Screen | Codrops From Shader Uniforms to Clip-Path Wipes: How GSAP Drives My Portfolio | Codrops Reverse-Engineering Claude AI’s Mascot Animations with SVG and GSAP | Codrops Akaru: Crafting Precision-Driven Digital Experiences | Codrops Designing Against the Gallery: A Two-Year Journey to a Layered Portfolio Experience | Codrops Between Human and Machine: Sofia Papadopoulou’s Approach to Augmented Creativity | Codrops More Than a Portfolio: Building a Scroll-Driven 3D World with Something to Say | Codrops Rhumb Studio: Small but Mighty, Shaped by Curiosity | Codrops Susurrus: Crafting a Cozy Watercolor World with Three.js and Shaders | Codrops Building 4WIDE: Turning Distortion, Blur, and Motion into a Coherent Experience | Codrops A Playful Clip Menu with GSAP’s easeReverse | Codrops False Earth: From WebGL Limits to a WebGPU-Driven World | Codrops Interactive Storytelling for the Web: Building Immersive Stories with Timelines, 3D, and Layered Scenes | Codrops The Art of Complex Motion: Corentin Bernadou’s World of Shaders and Experimentation | Codrops They Call Me Giulio: The Making of a Cinematic Cyberpunk Portfolio | Codrops Lusion: Where Digital Craft Meets Ambitious Experimentation | Codrops The Exat Microsite: Pushing a Typography Showcase to New Creative Extremes | Codrops A Single DM, a First SOTD, and the Start of Chanh M. Ho’s Creative Journey | Codrops Creating Custom Page Transitions in Astro with Barba.js and GSAP | Codrops R—K ’26: The Thinking and Code Behind a Portfolio Led by Presence | Codrops Building the Maxima Therapy Website: React, GSAP, and Dabbling with AI | Codrops Where Engineering Meets Craft: Edoardo Lunardi’s Obsession with the Details | Codrops Animating 160,000 Cubes in Three.js to Visualize Dithering | Codrops Arnaud Rocca’s Portfolio: From a GSAP-Powered Motion System to Fluid WebGL | Codrops Where Confidence Needs No Decoration: The Malvah.Studio Story | Codrops Motion Highlights #17 | Codrops
Sketching the Impossible: A 3D Portfolio Built Without a Single 3D Model | Codrops
By Tomasz Szmajda · 2026-06-11 · via Codrops

I can’t model 3D. That pretty much explains this entire project.

For months, I had been browsing Awwwards and The FWA and found sites like Igloo Inc, 3D combined with infinite scrolling, and I just thought: I need something like this. Then I saw Bruno Simon’s portfolio with the little car. I knew I wanted a 3D portfolio. I also knew I had zero Blender skills and honestly no interest in faking it with someone else’s models.

So I figured, why not just code simple 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 instead. That workaround accidentally became the whole visual identity of the project.

You can find the source code on GitHub.

Why This Exists

Go browse any Facebook group where developers share portfolios. I did. About 90% of them are the same thing: dark background, neon accent colors, text on the left, image on the right. Many of them look like they were generated by AI… they probably were. AI has this very specific aesthetic tendency: black page, neon glow, done.

I don’t have a problem with those sites technically. Their UX is probably better than mine, honestly – it’s hard to get lost on a standard layout. But I wanted something different. I wanted visitors to actually walk through a space, not just scroll down a page about me. If someone sees my portfolio and wants to work with me, they’ll figure out how to reach the Contact room. I’m not worried about that.

So I set out to build a portfolio you can walk through, not scroll through.

Four Months, From Sketch to Sky

The project started in December 2025. Initially, I thought it would be a 2D illustrated website – hand-drawn textures on flat HTML sections. But somewhere in the first few weeks, I realized flat HTML wasn’t going to cut it. This thing needed actual 3D depth. So I moved the whole thing into Three.js and React Three Fiber, and suddenly I was building rooms.

Four months later, it was live. Four months of fighting with camera systems and scroll mechanics I had no idea how to build. Also generating a ton of textures with AI, because there was no way I was drawing all of them by hand.

The Tech Stack

  • React 19 + React Three Fiber 9 + Three.js 0.182 for the 3D environment
  • GSAP 3.14 for all animations: camera flights, door mechanics, reveal transitions
  • Vite 7 for builds and dev server
  • Custom GLSL shaders extending MeshBasicMaterial for the paint-reveal effect
  • WebP textures generated via AI (Google’s image generation), compressed and trimmed to fit flat 3D geometry
  • PostHog for analytics, Lenis heritage in scroll philosophy

The Aesthetic: Why Everything Looks Hand-Drawn

This was never Plan B. From day one, I wanted it to feel like a sketch – like you opened someone’s notebook and the drawings jumped into 3D. The paper texture backgrounds, the ink-line doors, the doodles floating in the corridor. All intentional.

What evolved later was the color. I had all these sketch-style textures, and one day I thought: what if they painted themselves when you hover? What if hovering over something literally paints it with color?

That became the main interaction of the whole portfolio. Every clickable element starts as a black-and-white sketch and fills with color on hover – a brush-stroke reveal driven by a custom shader. It’s basically a visual hint – if something fills with color when you hover, it means you can click it and something will happen.

The PaintRevealMaterial Shader

The effect works by extending Three.js’s MeshBasicMaterial through onBeforeCompile, injecting custom fragment shader logic that blends between a sketch texture and a painted texture using procedural noise:

// Brush-stroke blend: 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 organic feel
    float maskValue = (1.0 - vMapUv.y) + rn;
    float threshold = uProgress * 1.5;
    if (maskValue < threshold) {
        diffuseColor = vec4(paintedColor.rgb, 1.0);
    }
}

The noise function gives it messy, organic edges – so instead of a clean wipe, you get something that actually looks like paint bleeding on paper. uProgress is animated from 0 to 1 by GSAP on hover.

I went with extending MeshBasicMaterial rather than writing a shader from scratch because I needed the standard Three.js texture pipeline (UV mapping, color spaces, transparency) to keep working. The custom logic only decides which pixels show the painted version – everything else stays stock.

Keeping all textures visually consistent was honestly one of the hardest parts. Every texture was AI-generated, and getting AI to generate hundreds of assets in the same hand-drawn style is… painful. Sometimes I just generated 20 versions and picked the one that didn’t look completely different from the rest.

The Infinite Corridor

The idea is simple: you enter a building through sketched double doors, and behind them is a corridor that stretches infinitely in both directions. On the walls, at alternating sides, are four doors – each leading to a room with its own world inside.

The Chunking System

The corridor is built from repeating segments, each 80 units long, managed by InfiniteCorridorManager. Only three segments are ever mounted: the one the camera is in, plus one ahead and one behind. As you scroll, segments spawn and despawn dynamically.

On top of that, each segment is wrapped in a SegmentVisibilityWrapper that uses useFrame to check whether the segment is actually in view. If you’ve scrolled 5 units past a segment, it hides entirely – zero draw calls for geometry the camera isn’t even looking at:

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

    if (groupRef.current.visible !== isVisible) {
        groupRef.current.visible = isVisible;
    }
});

The entrance posed a tricky challenge. When the user first arrives, they see the entrance doors – but behind those doors is the infinite corridor. I needed segment -1 (the one behind the entrance) to exist for shader pre-compilation, but I couldn’t let it be visible or its doors would clip through the entrance doors. The solution: a hideDoorsForSegments array that suppresses specific segment doors during the entrance phase, then reveals them once the user has entered.

The Camera System: 500 Lines of Pain

useInfiniteCamera.js is 500 lines long, and honestly almost every one of those lines exists because of a bug I had to fix.

The camera does several things at the same time:

  • Scroll movement via GSAP Observer (unifies mouse wheel, touch, and trackpad)
  • Mouse parallax on desktop – the camera sways gently as you move the mouse
  • Gyroscope parallax on mobile – tilt your phone, the corridor shifts
  • Auto-glance – as you approach a door, the camera subtly turns toward it
  • Keyboard navigation – arrow keys, spacebar, Page Up/Down for accessibility
  • Camera override mode – when GSAP takes over for door entry/exit animations

The auto-glance system is worth talking about. For each door, the hook calculates proximity using a start/peak/end distance model with eased strength:

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

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

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

The result is a small head-turn that makes it feel like the camera notices the doors on its own. It’s subtle, but it makes the corridor feel way less static.

The hardest part of the entire project, no contest, was the camera system for entering and exiting rooms. When you click a door, the camera needs to:

  1. Align itself in front of the door (position + rotation)
  2. Wait for the room to lazy-load
  3. Animate the door opening (handle down -> door swing)
  4. Fly the camera through the doorway into the room
  5. On exit: reverse the entire sequence without hitting walls or clipping through textures

Every single one of those steps had bugs. The camera would snap instead of moving smoothly. It would clip through wall geometry. It would end up facing the wrong direction after exiting. The rotation math had to account for the door’s angle (sawtooth walls are angled, not straight), the camera’s parallax state, and GSAP’s control handoff.

The DoorSection.jsx component – the one managing all of this – grew to 1,287 lines. I’m not proud of the size, but every line handles a real edge case.

One trick I am proud of: after releasing camera control back to the scroll system, the hook calculates the camera’s current physical rotation and derives an equivalent glance offset, instead of snapping to the “ideal” glance for that position. This prevents a jarring snap when exiting a room near a door:

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

const diff = Math.abs(derivedGlance - initialGlance);
if (diff > 0.02) {
    glanceOffset.current = derivedGlance; // Start from where we ARE
    targetGlance.current = initialGlance;  // Smooth toward ideal
}

The Rooms: Abstraction Over Predictability

The original plan was normal rooms. A room with a desk. A room with shelves. You know – rooms. But that felt boring and predictable. If someone walks down a corridor in a building and opens a door, they expect a room. They don’t expect to suddenly be flying a paper airplane through clouds.

So every room became its own little world:

  • The Gallery: Project cards hanging on an infinite clothesline, like laundry drying in the wind. Scroll sideways to browse. Infinite loop.
  • The Studio: Monitors floating in space, scrolling vertically through my content – videos, blog posts, social media. Infinite in both directions.
  • The About: You fly a paper airplane through an infinite sky filled with clouds and story milestones. Your biography as a flight path.
  • The Contact: A beach by the sea. Social media links are floating barrels you click to connect.

Almost every room has infinity built into it. In the Gallery, the cards loop. In the Studio, the monitors loop. In About, the sky never ends, just like the corridor itself. Only Contact breaks the pattern – and I think that’s actually better. Contact is the destination. It should feel like you’ve arrived somewhere.

The About room had a particularly nasty technical challenge. When you fly through clouds and then exit back to the corridor, the clouds would leak into the corridor view. The problem: the invisible clipping wall behind the camera also clipped things inside the corridor, because the camera enters the room at an angle.

The fix was two things: curving the clipping boundary so it matched the angled entry path, and extending the fly-in distance for the About room specifically – you travel further into About than into any other room, giving the clipping wall more clearance.

The Performance Crisis (or: Two Suns, One Moon, and Zero Visible Shadows)

When I first shared a preview link in Facebook developer groups, the feedback was clear: your site is beautiful, but it runs like a slideshow.

I spent days trying to figure out what was killing performance. I was looking at texture sizes, draw calls, shader complexity – everything. Then I found it.

Two directional lights and an ambient light were casting real-time shadows across the entire scene. These were standard Three.js lights I had added early in development for “proper” lighting. The problem? My scene is made entirely of flat textured planes – the shadow maps were computing complex depth passes for geometry that visually showed no shadow difference at all.

I removed every light. Then I added a slight tint directly to the textures in code – a tiny bit of simulated shadow baked into the color values. Visually? Almost no change. Performance-wise? Night and day.

The Shader Pre-Compilation Trick

Another piece of community feedback: “Why do I wait for the preloader, and then wait AGAIN when I click a door?”

The answer was shader compilation. Three.js compiles shaders lazily – the first time a material renders, the GPU compiles its shader program, causing a visible stutter. Every room had dozens of materials, and they all compiled on first entry.

The solution was RoomWarmup: a component that mounts all four rooms 500 units below the scene during the preloader phase, forces the GPU to compile every shader via gl.compileAsync(), then unmounts everything:

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

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

// In the JSX:
<group position={[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 devices, this eliminated all entry stutter – every room opens instantly because the GPU already knows every shader. On low-end devices, the warmup is skipped entirely (to prevent WebGL context loss from memory pressure), and rooms load on-demand with a slight delay.

I came up with this approach myself after reading the community complaints. No tutorial taught me this – it came from frustration and a lot of console.log debugging.

The KTX2 Experiment That Failed

I read everywhere that KTX2 / Basis Universal textures are the gold standard for WebGL performance – GPU-native decompression, smaller payloads, the works. So I converted everything.

It was a disaster. Textures misaligned. Colors shifted. The preloader got two seconds slower. And the visual quality on the hand-drawn textures – which depend on crisp lines and subtle gradients – degraded noticeably.

I reverted to WebP. The site already ran at 60 FPS on phones and 144 FPS on desktop with WebP textures. Sometimes the “correct” optimization just isn’t worth it if the current solution already works. I know 3D developers might judge me for this, but I’m speaking from the actual user experience perspective – and it’s smooth.

Adaptive Device Tiering

The site detects device capabilities at load time and adjusts accordingly:

const isMobileDevice = /iPhone|iPad|iPod|Android/i.test(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 devices get fewer preloaded textures, no room warmup, and simpler rendering. On top of that, a PerformanceMonitor from drei watches FPS in real-time and automatically downgrades the quality tier if frames start dropping.

On my main PC (3 monitors, 144Hz), the site holds a steady 144 FPS. On phones, it maintains around 60 FPS. On my 80 euro Thinkpad – honestly, it’s a miracle it runs at all – it manages 15-30 FPS. But it runs.

Sound Design: The Detail That Changes Everything

I noticed that the sites I admired most – Bruno Simon, Igloo Inc – all had sound. Sound is what makes a 3D site feel like a place instead of just a page with graphics.

Every room has its own ambient soundscape:

  • Corridor: Background music loop, footstep-like ambience
  • Gallery: City hum
  • Studio: Monitor buzz, electronic hum
  • About: Wind rushing past as you fly
  • Contact: Ocean waves

Door interactions have three distinct sounds: a creak when you hover (the door tilts slightly), a full open sound when you click, and a close sound when it shuts behind you.

Finding the right sounds was genuinely frustrating. Everything had to be free for commercial use, match the aesthetic tonally, and feel cohesive across all rooms. I’m still not 100% happy with every sound, but having some sound, even imperfect, is so much better than total silence.

The Achievement System: Gamification as UX

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

When you enter a new room, a tooltip appears: “Scroll to fly through my story” in About, “Drag to rotate and browse” in Studio. These aren’t just instructions though – they’re achievements waiting to be unlocked. Complete the action, and the tooltip transforms into a completed badge with a chime.

const ACHIEVEMENTS = {
    corridor_enter:  { label: 'Click a door to enter', title: 'Explorer' },
    corridor_explore: { label: 'Scroll to explore the corridor', title: 'Wanderer' },
    about_fly:       { label: 'Scroll to fly through my story', title: 'Sky Walker' },
    studio_interact: { label: 'Drag to rotate and browse', title: 'Director' },
    gallery_inspect: { label: 'Click project to inspect', title: 'Art Critic' },
    contact_choose:  { label: 'Find a contact method', title: 'Sociable' }
};

This solves a real problem: in a non-standard interface, people don’t know what to do. The achievements teach interaction patterns while rewarding exploration. Progress persists in localStorage, and every unlock fires a PostHog analytics event so I can track which rooms get the most engagement.

Recognition

The portfolio picked up some awards along the way:

  • GSAP Site of the Day + added to the official GSAP Showcase
  • FWA of the Day
  • CSSDA Special Kudos + 3 Public Choice Awards
  • Orpetron SOTD
  • CSS Winner SOTD
  • Awwwards Honorable Mention

What I Would Do Differently

Honestly? Almost nothing in terms of approach. The “mechanics first” philosophy – build the scroll, the camera, the corridor logic before touching a single texture – saved me from the trap of polishing visuals on top of broken foundations.

What I would skip: those two directional lights and the moon shader that cost me days of debugging for zero visual benefit. And the KTX2 experiment. Sometimes knowing what NOT to do is the real optimization.

What This Project Taught Me

That I’m still a beginner.

This is maybe my first project that achieved real recognition – the awards, the community response, the chance to write this article. My previous project, a site for the Polish rapper Young Multi, got attention when I first built it. But when I look at it now, I see everything I would do differently.

That’s how I know I’m growing.

This portfolio isn’t perfect either. The UX isn’t as clean as a standard layout – I know that. But people seem to enjoy exploring it, which means something went right.

What Comes Next

My biggest goal right now: building a website for the Polish hip-hop collective 2115 and submitting it to the Webby Awards and Lovie Awards. It’s a high bar. But when you’re getting awards like FWA of the Day at this age, and getting invited to write for Codrops – I mean, there’s really no such thing as impossible.

One Piece of Advice

First: write the screenplay. Before you touch a single line of code, imagine the film. What does the user see? Where does the camera go? What’s the story?

Then build the mechanics with simple shapes. Rectangles. Cubes. No textures, no shaders – just the raw movement and flow. Make it feel right when it looks like nothing.

Only then do you dress the world.

Tech is never the blocker – it’s always your imagination. If you can think of a solution, you can code it. It just takes time and stubbornness.