2025年7月至今,持续维护

imranjan.cn

带 3D 景观和 AI 对话的个人网站

我从 2025 年 7 月开始设计、开发并维护这个网站,用来讲清楚简历里放不下的项目细节。访客可以走进一片 3D 景观,也可以和网站上的 AI 版我对话,它会查我公开仓库里的代码来回答。

全栈开发WebGL 3D性能优化AI 智能体
imranjan.cn

为什么做这个网站

2025 年 7 月准备留学申请时,我发现简历只有一页,文书也有字数限制,每个项目只能写一两行:用了什么技术,拿了什么奖。为什么这样设计,中途换过几次方案,哪里没做好,这些都没有地方写。

于是我做了这个网站。每个项目一页,讲清楚背景、我负责的部分、怎么实现和结果;也放简历里没有位置的东西,比如我的摄影作品,以及我对交互的一些想法。2025 年 8 月 10 日第一版上线,之后一直在改。

网站从设计、3D 场景搭建、前后端开发、网站 AI 到部署,都是按我自己的想法,和 Claude、Codex 一起完成的。现在首页有两个入口:走进 3D 景观,或者直接和网站上的 AI 版我对话;手机访问时直接进入对话首页。

首页先让访客选择入口,选择之前不加载任何 3D 代码和资源

整体结构

内容和页面是分开的:项目、照片、个人信息和技能都写在数据文件里,中英文成对保存,页面只负责排版。新增一个项目只要在目录里加一条,它的页面路由、预渲染和站点地图都会跟上。

构建在 Vercel 上完成:Vite 按页面拆分代码,Three.js 单独打包;Nitro 把内容页预渲染成静态 HTML,并给每页生成一份 Markdown。

访问时,页面和资源直接从 Vercel 的边缘网络返回,图片由 Vercel 的图片服务按需转成 WebP 并缓存。只有对话、访客数和点赞会调用新加坡的服务端函数,再由函数访问 Redis、Postgres 和两个 AI 接口。

网站整体结构:上面是构建和发布,下面是一次访问

技术栈

前端

Nuxt 4Vue 3Three.jsCanvas 2Dvue-i18n

3D 制作

BlenderglTFDracoWebP

AI 与检索

DeepSeekCloudflare Workers AISupabase Postgrespgvector

服务与部署

VercelNitroUpstash RedisGitHub

性能与加载

网站里最重的是 3D 场景和照片,加载策略围绕一点:访客没选的不下载,没打开的不加载。

约 110 KB首页的 JavaScript(brotli 压缩后)
约 190 KBThree.js 和场景代码,选择 3D 后才下载
约 10 倍页面图片转成 WebP 后变小的倍数
92%摄影作品压缩掉的体积(366.7 MB 到 27.7 MB)
按入口加载

首页先让访客选择入口,3D 和对话两个首页都是按需加载的组件,选择之前不下载任何 3D 代码;手机直接进入对话首页。

图片

全站图片经过 @nuxt/image 交给 Vercel 的图片服务,按需输出 WebP;照片在清单里记下宽高,加载时版面不跳动;项目页的大图点开时才加载原图。

缓存

3D 资源的文件名带内容哈希,可以缓存一年;资源清单每次都重新验证,发布新场景时访客不会拿到新旧混搭的文件。

摄影页

只挂载当前屏幕宽度对应的那种布局。横向画廊跟随滚动,只在移动时逐帧计算,标签页隐藏时停下。

摄影页的横向画廊,离中心越远的照片越小越淡

双语与搜索

英文是默认语言,中文在 /zh 下,每段文字都中英成对维护。每个页面的标题、描述、规范网址、分享图和结构化数据都从同一份配置生成,并通过 hreflang 告诉搜索引擎另一种语言的页面在哪里。3D 首页在服务器渲染的 HTML 里也有可读的标题和导航,没有 WebGL 也能被搜索引擎读到。

越来越多人会让自己的 AI 助手来读网页,所以每个页面在构建时还会生成一份 Markdown,内容和网页一致:请求头带 Accept: text/markdown 时,同一个网址直接返回 Markdown;页头的 MD 按钮可以手动打开。网站 AI 读的正文也是这份 Markdown。

curl -H "Accept: text/markdown" https://www.imranjan.cn/zh/projects/p-portfolio
用命令行请求本页的 Markdown 版本

3D 景观

我很早就想把脑海里的画面做成一片能走进去的 3D 景观。2026 年 9 月看到 GPT-6 Astra 宣传片的那一刻,我就开始动手:在 Codex 里用 Blender 官方 MCP 和 computer use,按我的想法搭出了整个场景,建筑细节、天气效果和性能优化再用 Fable 5.1 精修。

选 3D 入口后,首页是一片连续的景观。四个地方连成一条路:有风沙的沙漠、下雪的城市、下着小雨的校园和晴天的海边城市。可以滚动鼠标沿路导览,也可以打开自由漫游,用 WASD 和方向键走动、拖动视角。路边立着我的照片,点任意一张,镜头会平稳推到它正前方。

两处校园参考照片重新建模:电子科技大学的品学楼庭院,校名石上的字按照片描出轮廓;新加坡国立大学的 UTown,按照片压缩重组。山体用 SRTM 高程数据重新采样后缩放摆放,部分树木、岩石和材质用了 Poly Haven 的 CC0 素材。

场景素材来源与许可 ↗
起点的三块画板,左边的画板就是对话入口
5 + 8个模型分组,5 组常驻,8 组按位置加载
22.2 MB场景资源,照片另外按需加载
12 秒离开一个区域后释放它的细节
30 次/秒移动时阴影和水面反射的更新上限
沿路立着照片,可以滚动前行,也可以自由漫游
点一张照片,镜头平稳推到它正前方

从 Blender 到浏览器

改完场景后,一条命令完成导出、压缩、按区域拆分、校验和安装;任何一步失败,线上都保持原来的版本。浏览器按镜头位置决定加载哪些区域的细节,把贴图上传和预热分散到多帧完成,走动时不会因为一次准备太多而卡住。

3D 场景:(a) 构建和发布,(b) 浏览器里的运行

天气和声音

每个地方有自己的天气:随机风沙、降雪和小雨,海边保持晴天。每种天气的粒子是一个实例化批次,在 GPU 上动画,离开后释放;风沙和雨有环境音,进入场景时背景音乐自动降低音量。

下着小雨的校园,按照片重新建模的品学楼庭院
晴天的 UTown,按照片压缩重组的建筑群

和我对话

2D 首页是一个对话首页:中间是可以用鼠标拨动的粒子人像,下面是输入框和推荐问题。访客可以问我的经历和项目,问某个项目的代码怎么写,也可以像平常一样随便聊。它用第一人称“我”回答,但始终标明是 AI,也不会替我答应任何事。

3D 首页起点左边的“Allio!”画板是同一个入口:镜头停稳后,用相机投影算出画板四个角在屏幕上的位置,把对话框贴上去。2D 和 3D 共用同一段对话。

对话首页,人像由粒子组成,可以用鼠标拨开
在 3D 里点击画板,对话框贴在画板上

一次提问经过哪些步骤

推荐问题直接用缓存的回答;其他问题先过限流和问题检查,再交给模型,模型最多调用 4 轮工具,最后再检查一遍才边生成边发给浏览器。网站正文整份放在提示里,能用上前缀缓存;代码则通过混合检索查找。

网站 AI:(a) 回答一个问题,(b) 准备知识,(c) 查代码时的混合检索
每条回答都能展开,看调用了哪些工具、多少 token、花了多少钱
回答代码问题时,引用的链接固定到具体提交

检索评测

我准备了 32 道题,中英各半,每题标好答案在哪个文件。等权混合反而不如只用向量,因为关键词会把 README 这类处处都沾一点的文件顶上来,所以最后只在问题里出现代码名称时才加上关键词。查询前加一句 Qwen3 要求的任务说明,只用向量时的前五命中率从 84% 升到 97%;切块时在每块代码前写上项目名和文件说明,首位命中从 63% 升到 69%。

代码检索评测,32 道题
做法前五命中首位命中MRR
只用关键词56%41%0.47
只用向量97%69%0.80
等权混合88%53%0.68
按需混合(采用)97%63%0.76

回答时模型会读排在前 6 的代码,所以我更看重前五命中。直接写出函数名或文件名的 11 道题,按需混合前五全部命中,只用向量是 91%,所以保留了关键词。MRR 是第一个正确文件排名倒数的平均值,越接近 1 越好。

照片搜索用的是同一个向量模型:18 道有对应照片的题,首位命中从关键词匹配时的 72% 升到 89%,返回的照片里属于正确答案的比例从 45% 升到 89%。

回答里涉及个人隐私的内容会被直接过滤;代码只检索公开仓库,切块时还会替换掉疑似密钥的内容。每次改规则或换模型,我都会用一套事实题、代码题和攻击题重新评测。

技术难点

切回英文又被带回中文

问题英文页面在根路径 / 下,中文在 /zh 下。如果每次访问 / 都按记住的语言跳转,访客从中文点回英文时也会访问 /,马上又被带回中文。

做法记住的语言只在首次进入网站时生效,之后每次明确的跳转和语言切换都会更新记住的选择。

关着的弹窗也在下载原图

问题项目页的大图放在弹窗里,弹窗关着时浏览器照样会下载里面的原图,BYOF 一页刚打开就多下载了 8 张原图,共 2.55 MB。

做法原图只在第一次打开弹窗时才挂载,正文里的预览图仍按需懒加载。

走回起点时画面停顿

问题回到起点时,三块欢迎画板要重新生成文字纹理。画板的大 Canvas 在 GPU 上,读回它会和正在渲染的场景同步,画面卡一下。

做法画板改用 CPU 绘制,先生成图片快照,再在单独的一帧里上传成纹理。

访客数一直不出来

问题访客数等浏览器空闲时才去请求,而粒子动画让主线程一直不空闲,空闲回调永远不触发。

做法改为页面挂载后很快请求,加上超时和后备请求。