July 2025 to now, maintained

imranjan.cn

Personal website with a 3D landscape and an AI chat

I have designed, built and maintained this website since July 2025, to tell the project details a CV has no room for. Visitors can walk through a 3D landscape, or talk to an AI version of me that looks up my public code to answer.

Full-Stack DevelopmentWebGL 3DPerformanceAI Agent
imranjan.cn

Why I built this website

In July 2025, preparing my study-abroad applications, I found that a one-page CV and word-limited essays left one or two lines per project: the tools I used and the award it won. Why I designed it that way, how many times the plan changed, what did not work: none of it fit.

So I built this website. Each project gets a page that explains the background, my part, how it works and how it turned out. It also holds what a CV has no place for, such as my photography and some of my ideas about interaction. The first version went live on 10 August 2025, and I have kept changing it since.

The design, the 3D scene, the front end and back end, the site's AI and the deployment all follow my own ideas, built together with Claude and Codex. The home page now has two ways in: walk into the 3D landscape, or talk straight away to an AI version of me. Phones go directly to the chat.

Visitors first choose a way in; no 3D code or assets load before they do

Architecture

Content and pages are kept apart: projects, photos, profile and skills live in data files, every text stored as an English and Chinese pair, and pages only lay them out. Adding a project means adding one entry; its route, prerendering and sitemap entry follow.

The build runs on Vercel: Vite splits the code by page and bundles Three.js on its own; Nitro prerenders content pages to static HTML and writes a Markdown copy of each.

On a visit, pages and assets come straight from Vercel's edge network, and Vercel's image service converts images to WebP on request and caches them. Only the chat, visit counts and likes call the server functions in Singapore, which in turn reach Redis, Postgres and two AI APIs.

The website as a system: build and release above, a visit below

Stack

Front end

Nuxt 4Vue 3Three.jsCanvas 2Dvue-i18n

3D production

BlenderglTFDracoWebP

AI and retrieval

DeepSeekCloudflare Workers AISupabase Postgrespgvector

Services and deployment

VercelNitroUpstash RedisGitHub

Performance and loading

The heaviest parts of the site are the 3D scene and the photos, so loading follows one rule: nothing a visitor did not choose is downloaded, and nothing they did not open is loaded.

about 110 KBof JavaScript on the home page (brotli)
about 190 KBfor Three.js and the scene code, downloaded only after choosing 3D
about 10×smaller page images after conversion to WebP
92%smaller photography (366.7 MB to 27.7 MB)
Load by entrance

The home page asks visitors to choose first; the 3D and chat homes are loaded on demand, so no 3D code is downloaded before a choice. Phones go straight to the chat home.

Images

Images across the site go through @nuxt/image to Vercel's image service, which serves them as WebP on request; photos carry their width and height in the manifest so the layout never jumps; project figures load the original only when opened.

Caching

3D asset names carry a content hash and can be cached for a year; the asset manifest is revalidated every time, so a new scene never arrives as a mix of old and new files.

Photography page

Only the layout for the current screen width is mounted. The horizontal gallery follows the scroll, computes frames only while moving and stops in a hidden tab.

The horizontal gallery: photos shrink and fade the further they are from the centre

Languages and search

English is the default and Chinese lives under /zh, with every text kept as a pair. Each page's title, description, canonical URL, share image and structured data come from one configuration, and hreflang tells search engines where the other language is. The 3D home page carries a readable heading and navigation in its server-rendered HTML, so search engines can read it without WebGL.

More and more people let their own AI assistant read web pages, so every page also gets a Markdown version at build time with the same content: a request with Accept: text/markdown gets it at the same address, and the MD button in the header opens it by hand. The site's own AI reads this same Markdown.

curl -H "Accept: text/markdown" https://www.imranjan.cn/projects/p-portfolio
Requesting the Markdown version of this page from the command line

3D landscape

I had long wanted to turn the pictures in my head into a 3D landscape you can walk into. The moment I saw the GPT-6 Astra launch film in September 2026, I started: in Codex, with Blender's official MCP server and computer use, I built the whole scene from my own ideas, then refined the architectural details, the weather and the performance work with Fable 5.1.

With the 3D entrance, the home page is one continuous landscape. Four places are joined by a single road: a desert with blowing sand, a snowy city, a campus in light rain and a sunny city by the sea. Scroll to follow the tour, or switch to free exploration and walk with WASD and the arrow keys while dragging to look around. My photos stand along the road; click any of them and the camera glides to face it.

Two campuses are modelled from photographs: the Pinxue Hall courtyard at UESTC, with the lettering on the name stone traced from a photo, and NUS University Town as a compressed composition. The mountains are resampled and scaled from SRTM elevation data, and some trees, rocks and materials are CC0 assets from Poly Haven.

Scene asset sources and licences ↗
The three boards at the start; the left one is the chat entrance
5 + 8model groups, 5 always loaded and 8 loaded by position
22.2 MBof scene assets, with photos loaded on demand
12 safter leaving a region, its details are released
30 per sat most, shadow and reflection updates while moving
Photos stand along the road; scroll forward or explore freely
Click a photo and the camera glides to face it

From Blender to the browser

After a change to the scene, one command exports, compresses, splits by region, verifies and installs it; if any step fails, the live site keeps the old version. The browser decides which regions' details to load from the camera position and spreads texture uploads and warm-up over several frames, so walking never stalls.

The 3D scene: (a) build and release, (b) running in the browser

Weather and sound

Each place has its own weather: random sandstorms, snow and light rain, while the city by the sea stays sunny. Each kind of weather is one instanced batch of particles animated on the GPU and released after you leave; sand and rain have ambient sound, and the background music lowers itself in the scene.

The campus in light rain, with Pinxue Hall's courtyard modelled from photos
University Town on a sunny day, a compressed composition from photos

Talk to me

The 2D home page is a chat: a particle portrait you can push around with the mouse, with an input box and suggested questions below. Visitors can ask about my experience and projects, how the code of a project works, or just chat about anything, as they normally would. It answers in the first person, always marked as AI, and never commits to anything on my behalf.

The “Allio!” board on the left of the 3D starting point is the same entrance: once the camera settles, the board's four corners are projected to the screen and the chat is placed on top. 2D and 3D share one conversation.

The chat home page; the portrait is made of particles you can push aside
In 3D, click the board and the chat appears on it

What happens to a question

Suggested questions are answered from a cache. Anything else passes the rate limit and the question check, then goes to the model, which may call tools for up to 4 rounds; a final check runs before the answer streams to the browser. The site's text sits whole in the prompt, where the prefix cache applies; code is found through hybrid search.

The site's AI: (a) answering a question, (b) preparing the knowledge, (c) hybrid code search
Every answer can be expanded to show its tools, tokens and cost
Code answers link to files pinned to a specific commit

Retrieval evaluation

I wrote 32 questions, half in Chinese and half in English, each labelled with the file that answers it. Mixing both searches with equal weight was worse than vectors alone, because keywords push up files like READMEs that mention a bit of everything, so keywords are added only when the question names code. Adding the task instruction Qwen3 expects before each query raised the top-five hit rate of vectors alone from 84% to 97%, and starting every code chunk with its project name and a short description of its file raised the top-one hit rate from 63% to 69%.

Code retrieval evaluation, 32 questions
MethodTop-5 hitTop-1 hitMRR
Keywords only56%41%0.47
Vectors only97%69%0.80
Equal-weight mix88%53%0.68
Mix when code is named (used)97%63%0.76

The model reads the top six chunks when it answers, so top-five hits matter most. On the 11 questions that name a function or a file, the adopted mix finds every answer in the top five, against 91% for vectors alone, so keywords stay. MRR averages one over the rank of the first correct file; closer to 1 is better.

Photo search uses the same embedding model: on 18 questions with matching photos, the top-one hit rate rose from 72% with keyword matching to 89%, and the share of returned photos that were right rose from 45% to 89%.

Anything touching personal privacy is filtered out of the answers; only public repositories are searched, and anything that looks like a key is replaced before indexing. Whenever I change the rules or the model, I rerun a set of fact, code and attack questions.

Hard problems

Switching to English bounced back to Chinese

What happenedEnglish pages live at / and Chinese ones under /zh. If every visit to / followed the remembered language, a visitor clicking back to English would land on / and be sent straight back to Chinese.

What I changedThe remembered language now applies only on entering the site; every explicit link or language switch after that updates the remembered choice.

Closed dialogs downloading originals

What happenedEnlarged project images sit in dialogs, and browsers download the images inside a dialog even while it is closed; the BYOF page fetched 8 originals, 2.55 MB, on arrival.

What I changedThe original is mounted only the first time its dialog opens; previews in the article are still lazy-loaded.

A stall on the way back

What happenedReturning to the start rebuilt the text textures of the three welcome boards. Reading back their large GPU canvas synchronised with the live scene and froze the picture for a moment.

What I changedThe boards are now drawn on the CPU; a snapshot image is made first and uploaded as a texture in a frame of its own.

A visit count that never appeared

What happenedThe count waited for the browser to be idle, and the particle animation kept the main thread busy, so the idle callback never fired.

What I changedIt now fetches shortly after the page mounts, with a timeout and a fallback request.