Use Cases (更新: 2026/6/2)

用 Claude Code 和 Three.js 构建实用 3D Web 界面

用 Claude Code 与 Three.js 实作3D商品查看器,涵盖resize、dispose、评审提示和业务场景。

用 Claude Code 和 Three.js 构建实用 3D Web 界面

Three.js 可以把 WebGL 变得更容易使用,但真正能上线的 3D 页面并不只是一个会旋转的模型。你还要处理相机位置、灯光、canvas 尺寸、移动端 GPU 负载、页面切换后的资源释放,以及模型加载失败时的兜底体验。

Claude Code 很适合生成这些基础代码。问题在于,如果只说“帮我做一个很酷的 3D 效果”,它可能给出一个看起来不错但无法长期维护的 demo。本文用 Vite + React + Three.js 做一个可复制的最小商品查看器,并说明如何让 Claude Code 做实现审查。

先定义3D要解决的问题

在写代码之前,先问清楚用户为什么需要 3D。电商商品查看器需要减少购买前的不确定性,例如颜色、材质、背面结构、尺寸感。数据可视化需要让用户看出分布、聚类或时间变化。教育场景则需要把注意力引导到关键部件,而不是让用户随便旋转。

给 Claude Code 的提示应该包含约束,而不只是风格词。

请用 Vite + React + TypeScript + three 创建一个3D商品查看器。
要求:
- canvas 渲染在父元素中,不直接挂到 document.body
- 跟随父容器 resize
- 使用 OrbitControls 支持旋转和缩放
- unmount 时 dispose geometry、material、renderer、controls
- 移动端 devicePixelRatio 最高限制为2
- 代码可以直接复制到 src/App.tsx 运行

这些约束就是 harness,也就是让智能体安全工作的“脚手架”。它能避免最常见的问题:白屏、拉伸、页面切换后越来越卡、手机发热。具体 API 可以对照 Three.js 官方文档,渲染器设置建议查看 WebGLRenderer

Vite/React 的最小设置

先不要引入复杂模型、HDR 环境和后期处理。用一个简单 mesh 验证相机、灯光、交互、resize 和 dispose,问题会更容易定位。

npm create vite@latest three-claude-demo -- --template react-ts
cd three-claude-demo
npm i three
npm run dev
flowchart LR
  A["React component"] --> B["mount div"]
  B --> C["WebGLRenderer canvas"]
  C --> D["Scene"]
  D --> E["Camera and lights"]
  D --> F["Mesh and material"]
  C --> G["OrbitControls"]
  G --> H["resize and dispose"]

可复制的3D查看器

把下面代码放进 src/App.tsx。它会创建一个产品占位模型,支持旋转、缩放、resize,并在组件卸载时释放 Three.js 资源。

import { useEffect, useRef } from "react";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import "./App.css";

export default function App() {
  const mountRef = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    const mount = mountRef.current;
    if (!mount) return;

    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0xf6f7fb);

    const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
    camera.position.set(3.5, 2.2, 4.5);

    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.outputColorSpace = THREE.SRGBColorSpace;
    renderer.shadowMap.enabled = true;
    mount.appendChild(renderer.domElement);

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.minDistance = 2.5;
    controls.maxDistance = 8;
    controls.target.set(0, 0.4, 0);

    scene.add(new THREE.HemisphereLight(0xffffff, 0x7c8594, 1.6));

    const keyLight = new THREE.DirectionalLight(0xffffff, 2.4);
    keyLight.position.set(3, 5, 4);
    keyLight.castShadow = true;
    scene.add(keyLight);

    const productGeometry = new THREE.BoxGeometry(1.8, 1.2, 1.1, 4, 4, 4);
    const productMaterial = new THREE.MeshStandardMaterial({
      color: 0x2f6f73,
      roughness: 0.42,
      metalness: 0.08,
    });
    const product = new THREE.Mesh(productGeometry, productMaterial);
    product.castShadow = true;
    product.position.y = 0.75;
    scene.add(product);

    const floorGeometry = new THREE.CircleGeometry(2.2, 64);
    const floorMaterial = new THREE.MeshStandardMaterial({
      color: 0xd9dee8,
      roughness: 0.7,
    });
    const floor = new THREE.Mesh(floorGeometry, floorMaterial);
    floor.rotation.x = -Math.PI / 2;
    floor.receiveShadow = true;
    scene.add(floor);

    const resize = () => {
      const width = mount.clientWidth;
      const height = mount.clientHeight;
      if (width === 0 || height === 0) return;

      camera.aspect = width / height;
      camera.updateProjectionMatrix();
      renderer.setSize(width, height, false);
    };

    let frameId = 0;
    const clock = new THREE.Clock();

    const animate = () => {
      const elapsed = clock.getElapsedTime();
      product.rotation.y = elapsed * 0.45;
      product.rotation.x = Math.sin(elapsed * 0.8) * 0.08;
      controls.update();
      renderer.render(scene, camera);
      frameId = window.requestAnimationFrame(animate);
    };

    resize();
    animate();
    window.addEventListener("resize", resize);

    return () => {
      window.removeEventListener("resize", resize);
      window.cancelAnimationFrame(frameId);
      controls.dispose();

      scene.traverse((object) => {
        if (object instanceof THREE.Mesh) {
          object.geometry.dispose();
          const materials = Array.isArray(object.material)
            ? object.material
            : [object.material];
          materials.forEach((material) => material.dispose());
        }
      });

      renderer.dispose();
      renderer.domElement.remove();
    };
  }, []);

  return (
    <main className="viewerShell">
      <div className="copy">
        <p className="eyebrow">Three.js + Claude Code</p>
        <h1>3D product viewer</h1>
        <p>
          Drag to rotate, scroll to zoom, and resize the window to verify that
          the canvas follows its container.
        </p>
      </div>
      <div ref={mountRef} className="viewerStage" />
    </main>
  );
}

再加入 src/App.css。注意 .viewerStage 必须有高度,否则 canvas 会像白屏一样什么都不显示。

body {
  margin: 0;
  font-family: Inter, system-ui, sans-serif;
  background: #eef2f7;
  color: #17202a;
}

.viewerShell {
  min-height: 100vh;
  display: grid;
  grid-template-columns: minmax(260px, 0.8fr) minmax(320px, 1.2fr);
  gap: 32px;
  align-items: center;
  padding: 40px;
  box-sizing: border-box;
}

.copy {
  max-width: 520px;
}

.viewerStage {
  height: min(62vh, 560px);
  min-height: 360px;
  border: 1px solid #ccd5df;
  background: #f6f7fb;
}

.viewerStage canvas {
  display: block;
}

@media (max-width: 760px) {
  .viewerShell {
    grid-template-columns: 1fr;
    padding: 24px;
  }

  .viewerStage {
    min-height: 300px;
  }
}

让 Claude Code 做审查

实现之后,不要只让 Claude Code “优化一下”。应该明确让它检查风险。

请审查这个 Three.js React 实现。
检查:
1. canvas 是否跟随父容器尺寸
2. geometry、material、renderer、controls 是否在 unmount 时释放
3. requestAnimationFrame 是否停止
4. devicePixelRatio 是否适合高分屏手机
5. 在 Next.js 或 Astro 的 SSR 环境中是否会因 window/document 报错
6. 相机、灯光和控制方式是否适合商品查看
请给出具体差异和手动测试步骤。

这一步能发现肉眼不容易发现的问题,尤其是 dispose 漏掉造成的内存和 GPU 资源累积。

三个实务场景

场景3D的价值交给 Claude Code 的任务
3D商品查看器让用户购买前确认颜色、材质、背面和尺寸感OrbitControls、颜色切换、灯光预设、移动端性能检查
数据可视化展示平面图难以表达的聚类、异常值或时间变化点云、3D柱状图、相机过渡、图例和选择状态
作品集/教育场景一边旋转对象一边解释关键部件注释标签、部件高亮、引导式相机位置

商品查看器的重点不是炫技,而是减少疑虑。数据可视化也不是越立体越好,如果3D让比较更困难,就应该保留2D视图。教育场景则需要“观看顺序”,例如按钮切换视角、说明文字出现时暂停自动旋转。

常见失败和修复

失败常见原因修复
canvas 白屏父元素高度为0、相机没有对准、没有灯光设置CSS高度,检查 camera position 和 controls target
resize 后画面变形只改了canvas尺寸,没有更新camera.aspect同时调用 camera.updateProjectionMatrix()renderer.setSize()
页面切换后越来越卡geometry/material/renderer/controls 未释放unmount 时遍历 scene 并 dispose
手机发热pixelRatio过高、阴影太重、模型面数太多限制 pixelRatio,减少阴影和几何细分
SSR 报错服务端渲染阶段访问 window/documentuseEffect 或 client-only 组件中初始化

调试白屏时,先把复杂模型、环境贴图和后期效果全部移除,只保留一个立方体、一盏灯和明亮背景。这样可以快速判断问题来自布局、相机还是资源加载。

发布前检查与CTA

还要注意,3D不是所有页面都值得加入。如果用户只是想快速比较价格、规格或文字说明,普通表格和图片可能更清楚。适合使用Three.js的场景,通常是“角度、空间、材质或运动”本身就是决策信息。把这个判断写进需求文档,再交给Claude Code实现,能减少为了炫技而增加维护成本的情况。

上线前至少要在目标手机上测试帧率、发热、首次加载体积和失败兜底。还要准备替代图片和文字说明,保证 WebGL 不可用时用户仍能理解内容。Canvas 相关设计可以继续阅读Claude Code Canvas开发指南,动画细节可参考Claude Code动画实现指南

Claude Code Lab 可以协助审查 3D 商品查看器、WebGL 数据可视化和教育互动场景,也可以为团队提供 Claude Code 训练。咨询时请准备目标设备、框架、模型格式、性能预算和业务目标,这样更容易得到可执行的方案。

实际测试结果

我把本文代码贴入 Vite React TypeScript 项目后,分别在桌面宽度和手机宽度下测试了 resize。给 .viewerStage 明确高度后,避免了常见白屏问题;把 devicePixelRatio 限制到2后,高分屏上的GPU压力更可控;让 Claude Code 审查 dispose 路径,也能更早发现页面切换后的资源泄漏风险。

#Claude Code #Three.js #3D #WebGL #frontend
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

把 Claude Code 变成真正能带来结果的工作流

先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。