{T}

Three.js 开发环境配置

良好的开发环境能显著提高开发效率和代码质量。本章节将介绍如何搭建完整的 Three.js 开发环境。

开发工具

代码编辑器

Visual Studio Code(推荐)

VS Code 是最流行的前端开发编辑器,对 Three.js 有很好的支持。

推荐插件

  1. Three.js Autocomplete

    • 提供 Three.js API 自动补全
    • 安装:搜索 "three.js autocomplete"
  2. Shader Language Support

    • GLSL 着色器语法高亮和智能提示
    • 安装:搜索 "Shader Language Support"
  3. Live Server

    • 本地开发服务器,支持热重载
    • 安装:搜索 "Live Server"
  4. Prettier

    • 代码格式化工具
    • 安装:搜索 "Prettier - Code formatter"
  5. ESLint

    • JavaScript 代码检查
    • 安装:搜索 "ESLint"

VS Code 配置

在项目根目录创建 .vscode/settings.json

json
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "files.associations": {
    "*.glsl": "glsl",
    "*.vert": "glsl",
    "*.frag": "glsl"
  }
}

WebStorm

JetBrains 出品的专业前端 IDE,功能强大但收费。

优势

  • 内置强大的代码智能提示
  • 集成调试工具
  • Git 集成
  • 数据库工具

浏览器开发工具

Chrome DevTools

Chrome 浏览器的开发者工具是调试 Three.js 的利器。

常用功能

  1. 控制台(Console)

    javascript
    // 查看 Three.js 对象
    console.log(scene)
    console.log(camera)
    console.log(renderer.info) // 查看渲染信息
  2. 元素面板(Elements)

    • 查看 canvas 元素
    • 检查 DOM 结构
  3. 性能面板(Performance)

    • 录制性能数据
    • 分析渲染瓶颈
  4. 内存面板(Memory)

    • 检测内存泄漏
    • 查看对象内存占用

Three.js DevTools 扩展

安装 Chrome 扩展:Three.js DevTools

功能:

  • 查看场景树结构
  • 检查对象属性
  • 实时编辑对象
  • 性能监控

项目构建工具

Vite(推荐)

Vite 是新一代前端构建工具,启动速度快,配置简单。

安装 Vite

bash
npm create vite@latest threejs-project -- --template vanilla
cd threejs-project
npm install three

项目结构

plaintext
threejs-project/
├── index.html
├── src/
│   ├── main.js
│   ├── style.css
│   └── scenes/
│       └── basic.js
├── public/
│   └── models/
├── package.json
└── vite.config.js

配置文件

vite.config.js

javascript
import { defineConfig } from 'vite'
 
export default defineConfig({
  server: {
    port: 3000,
    open: true, // 自动打开浏览器
    host: true // 允许局域网访问
  },
  build: {
    outDir: 'dist',
    assetsDir: 'assets'
  },
  optimizeDeps: {
    include: ['three']
  }
})

npm scripts

json
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  }
}

Webpack

传统但功能强大的构建工具。

安装

bash
npm install -D webpack webpack-cli webpack-dev-server
npm install -D babel-loader @babel/core @babel/preset-env
npm install -D html-webpack-plugin
npm install three

配置文件

webpack.config.js

javascript
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
 
module.exports = {
  entry: './src/main.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.[contenthash].js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader'
        }
      },
      {
        test: /\.(glsl|vert|frag)$/,
        type: 'asset/source'
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './index.html'
    })
  ],
  devServer: {
    static: './dist',
    hot: true,
    port: 3000
  },
  resolve: {
    extensions: ['.js']
  }
}

Parcel

零配置构建工具,适合快速原型开发。

安装

bash
npm install -D parcel
npm install three

使用

bash
# 开发
npx parcel index.html
 
# 构建
npx parcel build index.html

TypeScript 支持

Three.js 自带 TypeScript 类型定义,推荐使用 TypeScript 开发。

配置 TypeScript

安装

bash
npm install -D typescript
npm install three

tsconfig.json

json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "lib": ["ES2020", "DOM"],
    "types": ["vite/client"]
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

TypeScript 示例

typescript
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
 
class ThreeScene {
  private scene: THREE.Scene
  private camera: THREE.PerspectiveCamera
  private renderer: THREE.WebGLRenderer
  private controls: OrbitControls
  private cube: THREE.Mesh
 
  constructor() {
    this.scene = new THREE.Scene()
    this.camera = this.createCamera()
    this.renderer = this.createRenderer()
    this.cube = this.createCube()
    this.controls = this.createControls()
 
    this.addLights()
    this.setupEventListeners()
    this.animate()
  }
 
  private createCamera(): THREE.PerspectiveCamera {
    const aspect = window.innerWidth / window.innerHeight
    const camera = new THREE.PerspectiveCamera(75, aspect, 0.1, 1000)
    camera.position.set(0, 0, 5)
    return camera
  }
 
  private createRenderer(): THREE.WebGLRenderer {
    const renderer = new THREE.WebGLRenderer({ antialias: true })
    renderer.setSize(window.innerWidth, window.innerHeight)
    renderer.setPixelRatio(window.devicePixelRatio)
    document.body.appendChild(renderer.domElement)
    return renderer
  }
 
  private createCube(): THREE.Mesh {
    const geometry = new THREE.BoxGeometry(1, 1, 1)
    const material = new THREE.MeshStandardMaterial({
      color: 0x00ff00,
      metalness: 0.5,
      roughness: 0.5
    })
    const mesh = new THREE.Mesh(geometry, material)
    this.scene.add(mesh)
    return mesh
  }
 
  private createControls(): OrbitControls {
    const controls = new OrbitControls(this.camera, this.renderer.domElement)
    controls.enableDamping = true
    return controls
  }
 
  private addLights(): void {
    const ambientLight = new THREE.AmbientLight(0x404040, 0.5)
    this.scene.add(ambientLight)
 
    const directionalLight = new THREE.DirectionalLight(0xffffff, 1)
    directionalLight.position.set(5, 5, 5)
    this.scene.add(directionalLight)
  }
 
  private setupEventListeners(): void {
    window.addEventListener('resize', this.onWindowResize.bind(this))
  }
 
  private onWindowResize(): void {
    this.camera.aspect = window.innerWidth / window.innerHeight
    this.camera.updateProjectionMatrix()
    this.renderer.setSize(window.innerWidth, window.innerHeight)
  }
 
  private animate(): void {
    requestAnimationFrame(this.animate.bind(this))
 
    this.cube.rotation.x += 0.01
    this.cube.rotation.y += 0.01
 
    this.controls.update()
    this.renderer.render(this.scene, this.camera)
  }
}
 
// 创建场景实例
new ThreeScene()

调试工具

Stats.js

实时显示帧率(FPS)和渲染时间。

安装

bash
npm install stats.js

使用

javascript
import Stats from 'stats.js'
 
const stats = new Stats()
stats.showPanel(0) // 0: fps, 1: ms, 2: mb
document.body.appendChild(stats.dom)
 
function animate() {
  stats.begin()
  
  // 渲染代码
  
  stats.end()
  requestAnimationFrame(animate)
}
 
animate()

面板说明

面板快捷键显示内容
FPS0每秒帧数
MS1每帧渲染时间(毫秒)
MB2内存占用(需浏览器支持)

多面板显示

javascript
// 同时显示多个面板
const statsFPS = new Stats()
statsFPS.showPanel(0)
document.body.appendChild(statsFPS.dom)
 
const statsMS = new Stats()
statsMS.showPanel(1)
statsMS.dom.style.left = '100px'
document.body.appendChild(statsMS.dom)

Three.js Inspector

浏览器扩展,用于检查 Three.js 场景。

安装

使用

  1. 安装扩展
  2. 打开包含 Three.js 的网页
  3. 按 F12 打开开发者工具
  4. 切换到 "Three.js" 标签页

主要功能

  • 查看场景树结构
  • 检查对象属性(位置、旋转、材质等)
  • 实时编辑对象属性
  • 查看几何体和材质信息
  • 性能分析

dat.GUI / lil-gui

创建可视化调试面板,实时调整参数。lil-gui 是 dat.gui 的现代替代品。

安装

bash
# dat.gui(传统)
npm install dat.gui
 
# lil-gui(推荐,更现代)
npm install lil-gui

lil-gui 使用

javascript
import GUI from 'lil-gui'
 
const gui = new GUI({ title: '调试面板' })
 
// 参数对象
const params = {
  rotationSpeed: 0.01,
  color: '#00ff00',
  wireframe: false,
  scale: 1,
  metalness: 0.5,
  roughness: 0.5
}
 
// 添加控制项
gui.add(params, 'rotationSpeed', 0, 0.1).name('旋转速度')
gui.addColor(params, 'color').name('颜色').onChange((value) => {
  cube.material.color.set(value)
})
gui.add(params, 'wireframe').name('线框模式').onChange((value) => {
  cube.material.wireframe = value
})
gui.add(params, 'scale', 0.1, 2).name('缩放').onChange((value) => {
  cube.scale.set(value, value, value)
})
 
// 文件夹组织
const materialFolder = gui.addFolder('材质')
materialFolder.add(params, 'metalness', 0, 1).name('金属度')
materialFolder.add(params, 'roughness', 0, 1).name('粗糙度')
materialFolder.open() // 默认展开
 
// 保存配置
gui.save()

lil-gui 控制类型

方法说明示例
add(obj, prop)数字/布尔值gui.add(params, 'speed', 0, 10)
addColor(obj, prop)颜色选择器gui.addColor(params, 'color')
addFolder(name)创建文件夹gui.addFolder('材质')
addButton(obj, prop)按钮gui.add(params, 'reset')

Chrome DevTools 调试技巧

1. 控制台调试

javascript
// 查看 Three.js 对象
console.log(scene)
console.log(camera)
console.log(renderer.info) // 渲染统计
 
// 查看场景中所有对象
scene.traverse((obj) => {
  console.log(obj.type, obj.uuid, obj.name)
})
 
// 查看对象的几何体信息
console.log(mesh.geometry.attributes.position.count) // 顶点数
console.log(mesh.geometry.index.count / 3) // 三角形数
 
// 检测对象是否在视锥体内
const frustum = new THREE.Frustum()
frustum.setFromProjectionMatrix(
  new THREE.Matrix4().multiplyMatrices(
    camera.projectionMatrix,
    camera.matrixWorldInverse
  )
)
console.log(frustum.intersectsObject(mesh))

2. 使用 console.table

javascript
// 以表格形式显示对象属性
console.table({
  vertices: mesh.geometry.attributes.position.count,
  triangles: mesh.geometry.index.count / 3,
  drawCalls: renderer.info.render.calls
})

3. 性能分析

javascript
// 使用 console.time 测量执行时间
console.time('render')
renderer.render(scene, camera)
console.timeEnd('render')
 
// 使用 Performance API
performance.mark('renderStart')
renderer.render(scene, camera)
performance.mark('renderEnd')
performance.measure('render', 'renderStart', 'renderEnd')
console.log(performance.getEntriesByName('render'))

4. 断点调试

javascript
// 条件断点
function animate() {
  if (cube.position.y > 2) {
    debugger // 当 y > 2 时触发断点
  }
  cube.rotation.y += 0.01
  renderer.render(scene, camera)
  requestAnimationFrame(animate)
}

5. 内存分析

javascript
// 检测内存泄漏
// 1. 打开 Chrome DevTools -> Memory
// 2. 选择 "Heap snapshot"
// 3. 执行操作前后各拍摄一次快照
// 4. 比较两次快照的差异
 
// 手动触发垃圾回收(需 Chrome 启动参数 --expose-gc)
if (typeof gc === 'function') {
  gc()
  console.log('垃圾回收完成')
}
 
// 检查对象引用
console.log(mesh.geometry) // 应该有引用
mesh.geometry.dispose()
mesh.geometry = null // 释放引用

调试辅助函数

javascript
// 创建调试辅助对象
class DebugHelpers {
  constructor(scene) {
    this.scene = scene
  }
 
  // 添加坐标轴
  addAxesHelper(size = 5) {
    const axesHelper = new THREE.AxesHelper(size)
    this.scene.add(axesHelper)
    return axesHelper
  }
 
  // 添加网格
  addGridHelper(size = 10, divisions = 10) {
    const gridHelper = new THREE.GridHelper(size, divisions)
    this.scene.add(gridHelper)
    return gridHelper
  }
 
  // 显示对象包围盒
  showBoundingBox(mesh) {
    const box = new THREE.Box3().setFromObject(mesh)
    const helper = new THREE.Box3Helper(box, 0xff0000)
    this.scene.add(helper)
    return helper
  }
 
  // 显示相机视锥体
  showCameraFrustum(camera) {
    const helper = new THREE.CameraHelper(camera)
    this.scene.add(helper)
    return helper
  }
 
  // 显示光源
  showLightHelper(light) {
    let helper
    if (light instanceof THREE.DirectionalLight) {
      helper = new THREE.DirectionalLightHelper(light, 1)
    } else if (light instanceof THREE.PointLight) {
      helper = new THREE.PointLightHelper(light, 0.5)
    } else if (light instanceof THREE.SpotLight) {
      helper = new THREE.SpotLightHelper(light)
    }
    if (helper) this.scene.add(helper)
    return helper
  }
 
  // 显示法线
  showNormals(mesh, size = 0.1) {
    const helper = new THREE.VertexNormalsHelper(mesh, size)
    this.scene.add(helper)
    return helper
  }
}
 
// 使用
const debug = new DebugHelpers(scene)
debug.addAxesHelper()
debug.addGridHelper()
debug.showCameraFrustum(camera)

性能监控

Renderer Info

Three.js 提供了渲染器信息 API:

javascript
console.log(renderer.info)
// {
//   memory: { geometries: 1, textures: 1 },
//   render: { calls: 2, triangles: 36, points: 0, lines: 0 }
// }

自定义性能监控

javascript
class PerformanceMonitor {
  constructor() {
    this.fps = 0
    this.frames = 0
    this.lastTime = performance.now()
  }
 
  update() {
    this.frames++
    const currentTime = performance.now()
    
    if (currentTime >= this.lastTime + 1000) {
      this.fps = Math.round((this.frames * 1000) / (currentTime - this.lastTime))
      this.frames = 0
      this.lastTime = currentTime
      
      console.log(`FPS: ${this.fps}`)
      console.log(`Draw Calls: ${renderer.info.render.calls}`)
      console.log(`Triangles: ${renderer.info.render.triangles}`)
    }
  }
}
 
// 使用
const monitor = new PerformanceMonitor()
 
function animate() {
  monitor.update()
  
  // 渲染代码
  
  requestAnimationFrame(animate)
}

热重载配置

Vite HMR

Vite 自带热重载功能,修改代码后自动刷新。

javascript
// vite.config.js
export default {
  server: {
    hot: true
  }
}

Webpack HMR

javascript
// webpack.config.js
module.exports = {
  devServer: {
    hot: true,
    liveReload: true
  }
}

ESLint 配置

安装

bash
npm install -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

.eslintrc.js

javascript
module.exports = {
  env: {
    browser: true,
    es2021: true
  },
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended'
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module'
  },
  plugins: ['@typescript-eslint'],
  rules: {
    // 自定义规则
    'no-console': 'warn',
    'no-unused-vars': 'error'
  }
}

Prettier 配置

安装

bash
npm install -D prettier

.prettierrc

json
{
  "semi": false,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "none",
  "printWidth": 100
}

项目模板

创建一个可复用的项目模板:

模板结构

plaintext
threejs-template/
├── .vscode/
│   └── settings.json
├── public/
│   └── models/
├── src/
│   ├── main.ts
│   ├── scenes/
│   │   └── BaseScene.ts
│   ├── utils/
│   │   └── helpers.ts
│   └── styles/
│       └── main.css
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts
├── .eslintrc.js
└── .prettierrc

BaseScene.ts

typescript
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import Stats from 'stats.js'
 
export abstract class BaseScene {
  protected scene: THREE.Scene
  protected camera: THREE.PerspectiveCamera
  protected renderer: THREE.WebGLRenderer
  protected controls: OrbitControls
  protected stats: Stats
  protected clock: THREE.Clock
 
  constructor() {
    this.scene = this.createScene()
    this.camera = this.createCamera()
    this.renderer = this.createRenderer()
    this.controls = this.createControls()
    this.stats = this.createStats()
    this.clock = new THREE.Clock()
 
    this.addLights()
    this.setupEventListeners()
    this.init()
    this.animate()
  }
 
  protected createScene(): THREE.Scene {
    const scene = new THREE.Scene()
    scene.background = new THREE.Color(0x333333)
    return scene
  }
 
  protected createCamera(): THREE.PerspectiveCamera {
    const aspect = window.innerWidth / window.innerHeight
    const camera = new THREE.PerspectiveCamera(75, aspect, 0.1, 1000)
    camera.position.set(0, 0, 5)
    return camera
  }
 
  protected createRenderer(): THREE.WebGLRenderer {
    const renderer = new THREE.WebGLRenderer({ antialias: true })
    renderer.setSize(window.innerWidth, window.innerHeight)
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
    renderer.shadowMap.enabled = true
    document.body.appendChild(renderer.domElement)
    return renderer
  }
 
  protected createControls(): OrbitControls {
    const controls = new OrbitControls(this.camera, this.renderer.domElement)
    controls.enableDamping = true
    controls.dampingFactor = 0.05
    return controls
  }
 
  protected createStats(): Stats {
    const stats = new Stats()
    stats.showPanel(0)
    document.body.appendChild(stats.dom)
    return stats
  }
 
  protected addLights(): void {
    const ambientLight = new THREE.AmbientLight(0x404040, 0.5)
    this.scene.add(ambientLight)
 
    const directionalLight = new THREE.DirectionalLight(0xffffff, 1)
    directionalLight.position.set(5, 5, 5)
    directionalLight.castShadow = true
    this.scene.add(directionalLight)
  }
 
  protected setupEventListeners(): void {
    window.addEventListener('resize', this.onWindowResize.bind(this))
  }
 
  protected onWindowResize(): void {
    this.camera.aspect = window.innerWidth / window.innerHeight
    this.camera.updateProjectionMatrix()
    this.renderer.setSize(window.innerWidth, window.innerHeight)
  }
 
  protected abstract init(): void
 
  protected abstract update(deltaTime: number): void
 
  protected animate(): void {
    requestAnimationFrame(this.animate.bind(this))
 
    this.stats.begin()
 
    const deltaTime = this.clock.getDelta()
    this.update(deltaTime)
 
    this.controls.update()
    this.renderer.render(this.scene, this.camera)
 
    this.stats.end()
  }
}

使用模板

typescript
import { BaseScene } from './scenes/BaseScene'
import * as THREE from 'three'
 
class MyScene extends BaseScene {
  private cube: THREE.Mesh
 
  protected init(): void {
    const geometry = new THREE.BoxGeometry(1, 1, 1)
    const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 })
    this.cube = new THREE.Mesh(geometry, material)
    this.scene.add(this.cube)
  }
 
  protected update(deltaTime: number): void {
    this.cube.rotation.x += 0.01
    this.cube.rotation.y += 0.01
  }
}
 
new MyScene()

最佳实践

1. 使用版本控制

bash
git init
git add .
git commit -m "Initial commit"

添加 .gitignore

plaintext
node_modules/
dist/
.DS_Store
*.log
.env

2. 使用环境变量

bash
# .env.development
VITE_APP_TITLE=Three.js Development
VITE_API_URL=http://localhost:3000
typescript
// 使用环境变量
console.log(import.meta.env.VITE_APP_TITLE)

3. 代码组织

plaintext
src/
├── components/     # 可复用组件
├── scenes/         # 场景类
├── utils/          # 工具函数
├── assets/         # 静态资源
├── loaders/        # 加载器
├── materials/      # 自定义材质
└── shaders/        # 着色器代码

4. 添加文档

bash
npm install -D typedoc
json
{
  "scripts": {
    "doc": "typedoc --out docs src"
  }
}

常见问题解答

Q1: 如何解决 WebGL 上下文丢失问题?

A: WebGL 上下文丢失可能发生在系统资源不足、浏览器切换标签页等情况下。

javascript
// 监听上下文丢失事件
renderer.domElement.addEventListener('webglcontextlost', (event) => {
  event.preventDefault()
  console.log('WebGL 上下文丢失')
  
  // 停止渲染循环
  // 显示提示信息给用户
})
 
// 监听上下文恢复事件
renderer.domElement.addEventListener('webglcontextrestored', () => {
  console.log('WebGL 上下文已恢复')
  
  // 重新初始化资源
  initScene()
  // 恢复渲染循环
  animate()
})
 
// 强制丢失上下文(测试用)
// renderer.forceContextLoss()

Q2: 如何正确处理资源释放避免内存泄漏?

A: Three.js 对象需要手动释放,特别是在单页应用中切换场景时。

javascript
// 释放场景资源的完整函数
function disposeScene(scene, renderer) {
  // 遍历场景中所有对象
  scene.traverse((object) => {
    // 释放几何体
    if (object.geometry) {
      object.geometry.dispose()
    }
    
    // 释放材质(可能有多个材质)
    if (object.material) {
      if (Array.isArray(object.material)) {
        object.material.forEach((material) => disposeMaterial(material))
      } else {
        disposeMaterial(object.material)
      }
    }
  })
  
  // 清空场景
  scene.clear()
  
  // 释放渲染器
  renderer.dispose()
}
 
// 释放材质及其贴图
function disposeMaterial(material) {
  // 释放所有贴图属性
  const textureProperties = [
    'map', 'normalMap', 'roughnessMap', 'metalnessMap',
    'aoMap', 'emissiveMap', 'alphaMap', 'envMap'
  ]
  
  textureProperties.forEach((prop) => {
    if (material[prop] && typeof material[prop].dispose === 'function') {
      material[prop].dispose()
    }
  })
  
  material.dispose()
}

Q3: 如何在 TypeScript 中正确配置 Three.js 类型?

A: Three.js 自带类型定义,但有时需要额外配置。

typescript
// tsconfig.json
{
  "compilerOptions": {
    "types": ["vite/client"],
    "typeRoots": ["./node_modules/@types", "./src/types"]
  }
}
 
// 扩展 Three.js 类型(如果需要)
// src/types/three.d.ts
import 'three'
 
declare module 'three' {
  interface Object3D {
    customProperty?: string
  }
}
 
// 处理导入的类型问题
import type { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
 
// 或者在 vite.config.ts 中配置
import { defineConfig } from 'vite'
 
export default defineConfig({
  optimizeDeps: {
    include: ['three', 'three/examples/jsm/controls/OrbitControls']
  }
})

Q4: 如何配置 VS Code 实现更好的 Three.js 开发体验?

A: 创建完整的 VS Code 配置。

json
// .vscode/settings.json
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit"
  },
  "files.associations": {
    "*.glsl": "glsl",
    "*.vert": "glsl",
    "*.frag": "glsl",
    "*.vs": "glsl",
    "*.fs": "glsl"
  },
  "typescript.preferences.importModuleSpecifier": "relative",
  "javascript.preferences.importModuleSpecifier": "relative"
}
 
// .vscode/launch.json(调试配置)
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "chrome",
      "request": "launch",
      "name": "Launch Chrome",
      "url": "http://localhost:3000",
      "webRoot": "${workspaceFolder}",
      "sourceMaps": true
    }
  ]
}
 
// .vscode/extensions.json(推荐扩展)
{
  "recommendations": [
    "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode",
    "ms-vscode.vscode-typescript-next"
  ]
}

Q5: 如何解决 Vite 开发服务器加载模型报错?

A: Vite 需要正确配置静态资源处理。

javascript
// vite.config.ts
import { defineConfig } from 'vite'
 
export default defineConfig({
  // 1. 配置 public 目录
  publicDir: 'public',
  
  // 2. 配置资源处理
  assetsInclude: ['**/*.gltf', '**/*.glb', '**/*.obj', '**/*.fbx'],
  
  server: {
    // 3. 配置 CORS
    cors: true,
    
    // 4. 配置代理(如果模型在其他服务器)
    proxy: {
      '/models': {
        target: 'http://example.com',
        changeOrigin: true
      }
    }
  },
  
  build: {
    // 5. 配置资源内联阈值
    assetsInlineLimit: 0 // 禁用资源内联
  }
})
 
// 正确加载模型
// 将模型放在 public/models/ 目录下
const loader = new GLTFLoader()
loader.load('/models/scene.glb', (gltf) => {
  scene.add(gltf.scene)
})
 
// 或使用 import 导入(需要配置)
import modelUrl from '../assets/model.glb?url'
loader.load(modelUrl, (gltf) => {
  scene.add(gltf.scene)
})

Q6: 如何实现热重载时保持 Three.js 场景状态?

A: 使用 Vite HMR API 保持状态。

javascript
// main.js
import * as THREE from 'three'
 
// 将状态存储在全局对象中
if (!window.__THREE_STATE__) {
  window.__THREE_STATE__ = {
    cameraPosition: null,
    objects: []
  }
}
 
class SceneManager {
  constructor() {
    this.scene = new THREE.Scene()
    this.camera = new THREE.PerspectiveCamera()
    this.renderer = new THREE.WebGLRenderer()
    
    // 恢复状态
    this.restoreState()
    
    // 监听 HMR
    if (import.meta.hot) {
      // 保存当前状态
      import.meta.hot.dispose(() => {
        window.__THREE_STATE__.cameraPosition = this.camera.position.clone()
      })
      
      // 接受更新
      import.meta.hot.accept(() => {
        console.log('模块更新完成')
      })
    }
  }
  
  restoreState() {
    if (window.__THREE_STATE__.cameraPosition) {
      this.camera.position.copy(window.__THREE_STATE__.cameraPosition)
    }
  }
}
 
new SceneManager()

Q7: 如何配置 ESLint 支持 Three.js 开发?

A: 创建针对 Three.js 的 ESLint 配置。

javascript
// .eslintrc.js
module.exports = {
  env: {
    browser: true,
    es2021: true
  },
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended'
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module'
  },
  plugins: ['@typescript-eslint'],
  rules: {
    // Three.js 常用规则调整
    'no-unused-vars': 'off',
    '@typescript-eslint/no-unused-vars': ['warn', { 
      argsIgnorePattern: '^_',
      varsIgnorePattern: '^_' 
    }],
    
    // 允许全局 THREE 变量(CDN 方式)
    'no-undef': 'off',
    
    // 关闭 any 类型警告(Three.js 类型定义不完善时)
    '@typescript-eslint/no-explicit-any': 'off',
    
    // 自定义规则
    'no-console': ['warn', { allow: ['warn', 'error'] }]
  },
  globals: {
    THREE: 'readonly' // CDN 方式使用
  }
}

Q8: 如何在开发环境快速切换调试模式?

A: 创建开发环境配置工具。

typescript
// src/utils/DevTools.ts
export class DevTools {
  private gui: GUI | null = null
  private stats: Stats | null = null
  private helpers: THREE.Object3D[] = []
  
  constructor(private scene: THREE.Scene, private enabled: boolean = true) {
    if (!enabled) return
    
    this.init()
  }
  
  private init() {
    // 创建 GUI
    this.gui = new GUI({ title: '开发工具' })
    
    // 创建 Stats
    this.stats = new Stats()
    this.stats.showPanel(0)
    document.body.appendChild(this.stats.dom)
    
    // 添加辅助对象
    this.helpers.push(new THREE.AxesHelper(5))
    this.helpers.push(new THREE.GridHelper(10, 10))
    this.helpers.forEach(h => this.scene.add(h))
    
    // 添加全局控制
    const globalFolder = this.gui.addFolder('全局')
    const globalParams = {
      showHelpers: true,
      showStats: true
    }
    
    globalFolder.add(globalParams, 'showHelpers').onChange((v) => {
      this.helpers.forEach(h => h.visible = v)
    })
    globalFolder.add(globalParams, 'showStats').onChange((v) => {
      this.stats.dom.style.display = v ? 'block' : 'none'
    })
  }
  
  begin() {
    this.stats?.begin()
  }
  
  end() {
    this.stats?.end()
  }
  
  dispose() {
    this.gui?.destroy()
    this.stats?.dom.remove()
    this.helpers.forEach(h => {
      this.scene.remove(h)
      h.dispose?.()
    })
  }
}
 
// 使用
const devTools = new DevTools(scene, import.meta.env.DEV)
 
function animate() {
  devTools.begin()
  // ... 渲染代码
  devTools.end()
  requestAnimationFrame(animate)
}

下一步

现在你已经配置好了完整的开发环境,接下来可以:

  1. 场景系统 - 深入理解场景概念
  2. 相机系统 - 学习不同的相机类型
  3. 渲染器 - 掌握渲染器配置

相关资源