所有文章

Bruno → Daisy: Studio 改造日记

把 Bruno Simon 那个开了车撞键盘的 3D portfolio,改造成水彩 anime 风的 Daisy's Garden。从材质替换到 DAISY 字母,从主角换肤到 outline 级联 bug。

约 12 分钟评论 (··) 浏览 (··) 3DThree.js改造日记Bruno

起点

最早看到 Bruno Simon 的网站时,就被那种「开车撞按钮」的交互震到了 —— 一个个人作品集竟然能这么玩。后来他把整个项目开源了 (github.com/brunosimon/my-room-in-3d 的兄弟项目),我就一直想:能不能改造一份属于自己的?

那个项目的核心思路是 以场景为路由。不是点击导航跳页面,而是开车开到对应的区域,触发对应的内容显示。

改造目标

我想要的不是 Bruno 的复刻,而是:

  • 保留交互逻辑:开车 / 撞物体 / 区域触发
  • 替换视觉风格:从 Bruno 那种「米色 + 信息密集」转向「粉紫水彩 + 留白」
  • 替换主角:他车里坐的是他自己(一个 GLB 模型),我要换成 Meshy AI 生成的 Lolita anime 角色
  • 替换字母:原本的 BRUNO 字母换成 DAISY
  • 接入博客:把 3D 部分作为博客的子路径 /play/,主站还是常规的文章 / 项目页

Phase 1-3:清理 Bruno 的"我"痕迹

第一步是把所有「Bruno 是这个网站的主人」的元数据清理掉。包括:

  • <head> 里的 SEO meta(title / description / og:image)
  • index.html 里的 social 链接(推特、领英、个人邮箱)
  • 资源里的「Webby 奖杯」「Awwwards 奖杯」(他得过的,我没得过)
  • DAISY → BRUNO 替换:5 个字母的 GLB 各替换成对应的 D / A / I / S / Y
// World/DaisyLetters.js(节选)
const letters = ['D', 'A', 'I', 'S', 'Y']
letters.forEach((letter, i) => {
    const mesh = new THREE.Mesh(
        new TextGeometry(letter, { font, size: 2.1 }),
        materials.shades.items.purple   // 薰衣草紫 matcap
    )
    mesh.position.x = i * 2.5 - 5
    container.add(mesh)
})

材质这块我做了个 ANIME_PALETTE 调色板(13 个颜色),通过运行时 canvas 生成 matcap 贴图,把 Bruno 的 13 个原始 matcap 全部替换成粉紫色调。

Phase 4:主角接入

这是最难的一步。Meshy AI 生成的 GLB 文件是按 PBR 流程导出的(带 normal map / metallic / roughness),但 Bruno 的场景是 matcap shader(自定义 ShaderMaterial,不接受灯光)。这就导致:

PBR 模型 + 没有灯的场景 = 黑屏

我前后试了 4 个方案:

方案一:加灯

给场景加 AmbientLight + DirectionalLight。✅ Avatar 出现了,但 PBR 阴影把模型搞得很暗,和扁平的 matcap 风格不搭。

方案二:MeshBasicMaterial 直出

把 PBR 替换成 MeshBasicMaterial(无灯,贴图直出)。但 Meshy 的 material.color 默认是黑色(依赖贴图上色),克隆过来变成 (0,0,0) × 贴图 = 黑色

// Character.js: 强制 white tint,让贴图原色出来
const color = hasMap ? new THREE.Color(1, 1, 1) : oldMat.color.clone()
mesh.material = new THREE.MeshBasicMaterial({ color, map })

方案三:色彩空间 + toneMapping

Meshy 的贴图 colorSpace 默认 linear-srgb,渲染过暗。强制 SRGBColorSpace + toneMapped: false 跳过 ACES 色调映射。

map.colorSpace = THREE.SRGBColorSpace
mesh.material.toneMapped = false

方案四:清晰度 + 描边

最大各向异性过滤 + LinearMipmapLinear 滤波 + 反向外壳描边。

map.anisotropy = 16
map.minFilter = THREE.LinearMipmapLinearFilter
// 反向外壳:BackSide 黑色 + scale 1.025

那个 outline 级联 bug

这是整个改造里最经典的 bug。开了 outline 后,整个角色变成了纯黑剪影。诊断后发现:

model.traverse((obj) => {
    if (obj.isMesh) {
        _toonifyMesh(obj)     // ← 替换材质
        _addOutlineMesh(obj)  // ← 加 outline 作为 obj 的子 mesh
    }
})

Object3D.traverse() 在回调返回后,继续遍历 obj.children。此时 children 已经包含我刚加的 outline。outline 被当成新 mesh,再次 _toonifyMesh。这一步把原本 BackSide 黑色的 outline,换成了 FrontSide 黑色实体 —— 整个外壳变成不透明的黑色,覆盖了主体

修复很简单:先收集再处理

// ✅ 先 traverse 收集
const meshes = []
model.traverse(obj => { if (obj.isMesh) meshes.push(obj) })

// ✅ 退出 traverse 后处理(outline 加进去也不会再被扫到)
for (const obj of meshes) {
    _toonifyMesh(obj)
    _addOutlineMesh(obj)
}

经验

  1. PBR 模型 + matcap 场景 = 视觉撕裂。要么全 PBR 加灯,要么全 matcap 转 unlit
  2. traverse() 在迭代中可以「滚雪球」。改 children 时要警惕
  3. Meshy 的贴图色彩空间默认不对。导入时手动改 colorSpace = SRGBColorSpace
  4. toneMapped: false 是 anime 扁平风的关键 —— 跳过全局 ACES 让贴图原色鲜艳

接下来

3D 部分调到 70% 满意,剩余的:

  • 清理一些 Bruno 残留的「胸像」雕塑(dat.gui 里加了切换开关,方便定位)
  • 加 Zsiga / Hello Kitty 等装饰角色(去 Sketchfab 找)
  • information 区贴图换成自己的(Twitter → 微博、LinkedIn → 即刻)

博客部分(你正在看的)刚搭好骨架,下一篇是《Phase B 核心页面 · 用 Tailwind 排出粉紫水彩感》。

总结一句:Bruno 的代码非常干净,但所有 hardcode 都是给 Bruno 自己用的。改造它的过程,就是不断把 hardcode 找出来、命名、替换的过程。

💬 评论

正在加载评论…