Morph Racing

What Creative Director Game Designer
|
When 1 Month Personal Project
|
Where Solo (AI-Assisted) AI: Gemini 2.5 Pro
|
How Three.js WebGL
01 // PROJECT OVERVIEW

Context

Morph Racing is an experimental personal project where I used Gemini 2.5 Pro to handle the code while I focused entirely on the design and direction side. The goal was to see how fast I could iterate on a complex mechanic when I wasn't bottlenecked by writing the code myself.

My role was closer to a design lead: defining what I wanted, testing what came back, and directing iterations until it felt right. All the visual art is primitive shapes on purpose, to ensure visual cohesion.

02 // CORE PILLARS

Key Accomplishments & Skills

AI Workflow Integration

Learned how to effectively direct an LLM to build complex, interconnected systems over a 1-month period without breaking the codebase.

Advanced Vertex Morphing

Directed the creation of a custom CPU-based vertex morphing system. Tested and iterated on the math until the transition between shapes felt seamless and responsive.

Visual Polish (Toon & Outlines)

Guided the implementation of an 'Inverted Shell' outline shader. Figured out how to get a clean toon-shaded look without relying on heavy post-processing that ruined performance.

03 // DESIGN DEEP DIVE

Engineering The Systems

Dev Stages time lapse

The Goal: Create a simple prototype focused on a speed based morphing mechanic. Nail down the sense of speed and methods to reflect that into the gameplay and visual feedback.

The Limitations: Quick project inside the LLM canvas, testing the capabilities of the LLM's understanding and the difficulties of working inside 1 html file.

The Process: Directed the implementation every feature, bit by bit, tested each step thoroughly and directed the iterations until I was satisfied with the end result.

Early to final: how the prototype evolved over the month

2. Speed-Based Morphing System

Outcome: A visual mechanic that directly maps the player's performance to their character model. When you're fast, you're a sphere. When you're slow, you're a box.

The Problem: The first attempt just swapped the meshes between a box and a sphere. It looked terrible and completely broke the flow. I tried RoundedBox geometry but the static normals caused ugly shadow artifacts.

The Solution: I directed the AI to build a CPU Vertex Morphing system instead. By feeding it prompts to linearly interpolate high-resolution vertex positions and normals, we eventually got a smooth, real-time transition that felt right.

The morphing in action. Cube to sphere based on speed.

3. The "Inverted Shell" Visual Style

Outcome: A clean, high-contrast look that works flawlessly alongside the morphing geometry without killing framerate.

The Problem: I wanted a stylized look, but post-processing Sobel filters ruined the shadow maps and caused input lag. Ribbon trails on the edges were just too computationally heavy.

The Solution: I looked into the 'Inverted Shell' technique and directed the AI to implement it. It duplicates the object, slightly scales it, and inverts the normals — creating a perfect outline that morphs naturally with the mesh without needing extra pixel processing.

The outline shader applied. Clean edges across all morphing states.

4. Sensing Speed (Ghosting & Camera FX)

Outcome: The game actually feels fast. The visuals warp and stretch as the player hits top speed.

The Problem: Moving at 20+ units/sec didn't mean anything if it didn't look fast. The camera felt stiff and detached.

The Solution: I instructed the AI to add Dynamic FOV scaling and Camera Lag. For extra impact, I directed the creation of an Object Pooling system for Ghosting. It took a lot of back-and-forth testing to get the ghosting to accurately mimic the morphing volume without hitching.

What going fast actually looks like. FOV shift, camera lag, and ghost trail all working together.
04 // DESIGN REALITIES

Development Realities & Constraints

Building with AI requires a "Fail Fast" mentality. Many initial approaches were technically correct but felt "wrong" kinesthetically, leading to rapid shifts in system architecture.

Initial Approach Final Solution Design Rationale
Track Generation Cut Converting my 2D track design to 3D proved too complex for the AI to handle and was wasting too much time.
Game Loop Logic Cut Started planning a game loop, but decided against is as the focus was on the mechanic prototype itself and not making a complete game.
Camera and Controls Iterated Switched from world-space WASD to racing based steering and decoupled the camera to act purely as an observation component.
Ribbon Trails Iterated Ribbon meshes felt too "flat" and rigid. Ghosting captures the complex morphing volumes better and adds to the game feel further.

Track Generation

Initially, I was thinking of drawing a 2D track , and then testing with the LLM to see if it could extrapolate the data and create a 3d version of it using primitive meshes. This went through many stages of iteration, until I decided that it was way too complex of a task for the current AI model and decided to cut it and continue focusing on other elements.

Game Loop Logic

Once I was satisfied with the mechanic and the sense of speed, I thought it could be fun to add in a basic game loop that created more interactivity and purpose. The core idea was to collect as many coins that were placed in a way that incentivised weaving and turning, before reaching the end of the track. Mid-way through, I decided to stop because I reminded myself that the core point of the whole experiment is to test and prototype mechanic ideas, not make a fully functioning game.

Camera and Steering

Originally, I started with a world-space based camera and steering system, where the player could move the car in any direction using WASD. However, this felt too clunky, arcady and unintuitive for a racing game, so I decided to switch to a more traditional racing based steering system, where the player could only steer left and right & use "S" for braking/reverse. I also decoupled the camera to act purely as an observation component, to prevent unnecessary twitching since I didnt want it affecting the movement.

‹ Back to All Projects