AI生成的Threejs常用Api学习计划

下面这份是「增强版」学习计划,包含:

  • 每个阶段的关键知识点(用大白话讲)
  • 可直接跑的 Vue 3 + Three.js 示例代码
  • 常见坑和通俗理解

AI生成的Threejs常用Api学习计划

  • [🚀 Vue 3 + Three.js 核心功能学习计划(带示例代码版)](#🚀 Vue 3 + Three.js 核心功能学习计划(带示例代码版))
    • [📅 第一阶段:最小可行场景 ------ 让一个方块转起来](#📅 第一阶段:最小可行场景 —— 让一个方块转起来)
      • 目标
      • [1.1 核心知识点(通俗版)](#1.1 核心知识点(通俗版))
      • [1.2 示例代码:Vue 3 + Three.js 旋转方块](#1.2 示例代码:Vue 3 + Three.js 旋转方块)
      • [1.3 通俗理解](#1.3 通俗理解)
    • [📅 第二阶段:让世界有"光" ------ 光照、材质、模型加载](#📅 第二阶段:让世界有“光” —— 光照、材质、模型加载)
      • 目标
      • [2.1 核心知识点(通俗版)](#2.1 核心知识点(通俗版))
      • [2.2 示例代码:带光照 + OrbitControls 的场景](#2.2 示例代码:带光照 + OrbitControls 的场景)
      • [2.3 通俗理解](#2.3 通俗理解)
    • [📅 第三阶段:交互 ------ 点击物体,弹窗显示信息](#📅 第三阶段:交互 —— 点击物体,弹窗显示信息)
      • 目标
      • [3.1 核心知识点(通俗版)](#3.1 核心知识点(通俗版))
      • [3.2 示例代码:点击物体弹出信息](#3.2 示例代码:点击物体弹出信息)
      • [3.3 通俗理解](#3.3 通俗理解)
    • [📅 第四阶段:粒子与环境 ------ 做好看又轻量的特效](#📅 第四阶段:粒子与环境 —— 做好看又轻量的特效)
      • 目标
      • [4.1 核心知识点(通俗版)](#4.1 核心知识点(通俗版))
      • [4.2 示例代码:简单星空粒子](#4.2 示例代码:简单星空粒子)
      • [4.3 通俗理解](#4.3 通俗理解)
    • [📅 第五阶段:工程化与清理 ------ 准备上线的收尾](#📅 第五阶段:工程化与清理 —— 准备上线的收尾)
      • 目标
      • [5.1 核心知识点(通俗版)](#5.1 核心知识点(通俗版))
      • [5.2 示例代码:统一 dispose 工具函数](#5.2 示例代码:统一 dispose 工具函数)
      • [5.3 封装一个简单 ModelViewer 组件](#5.3 封装一个简单 ModelViewer 组件)
    • [✅ 小结:你可以这样用这份计划](#✅ 小结:你可以这样用这份计划)

🚀 Vue 3 + Three.js 核心功能学习计划(带示例代码版)

适用:会用 Vue 3(Composition API)、想快速上手 Three.js 做项目的人。

原则:专注 80% 常用功能,用代码示例帮你建立"直觉"。


📅 第一阶段:最小可行场景 ------ 让一个方块转起来

目标

在 Vue 3 组件里跑通一个旋转的立方体,理解 Three.js 的"三板斧"。


1.1 核心知识点(通俗版)

三大核心对象:

  1. Scene(场景)= 舞台

    • 所有 3D 物体都要放到场景里,就像演员要上台。
    • 代码:const scene = new THREE.Scene()
  2. Camera(相机)= 摄像机

    • 决定了"观众从哪个角度看舞台"。
    • 常用的是透视相机(PerspectiveCamera),近大远小,符合人眼。
    • 关键参数:视角(fov)、宽高比、近裁剪面、远裁剪面。
  3. Renderer(渲染器)= 电视屏幕

    • 把"舞台 + 摄像机"拍到的画面,画到 <canvas> 上。
    • 常用 WebGLRenderer,底层就是 WebGL。

渲染循环 = 动画片的每一帧

  • 动画本质:每秒画很多张图,切换够快眼睛就以为在动。
  • Three.js 用 requestAnimationFrame 不断调用 renderer.render(scene, camera)

Vue 3 的关键点:

  • Three.js 需要 DOM(canvas),必须在 onMounted 里初始化。
  • 组件销毁时要清理动画帧、Three.js 资源,避免内存泄漏。

1.2 示例代码:Vue 3 + Three.js 旋转方块

你可以新建一个组件 BasicScene.vue,把下面代码贴进去,在路由里访问它。

vue 复制代码
<template>
  <div class="scene-container" ref="containerRef"></div>
</template>

<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import * as THREE from 'three'

// 1. 用来挂载 canvas 的容器
const containerRef = ref<HTMLDivElement>()

// 2. 下面这些 Three.js 对象不需要响应式,用普通变量即可
let scene: THREE.Scene
let camera: THREE.PerspectiveCamera
let renderer: THREE.WebGLRenderer
let cube: THREE.Mesh
let animationId: number

onMounted(() => {
  const container = containerRef.value!
  const width = container.clientWidth
  const height = container.clientHeight

  // 1. 场景
  scene = new THREE.Scene()
  scene.background = new THREE.Color(0x222222) // 背景深灰色

  // 2. 相机(透视)
  camera = new THREE.PerspectiveCamera(
    60,             // 视角
    width / height, // 宽高比
    0.1,            // 近裁剪面
    1000            // 远裁剪面
  )
  camera.position.set(0, 0, 5) // 相机往后退一点,才能看到物体

  // 3. 渲染器
  renderer = new THREE.WebGLRenderer({ antialias: true }) // 抗锯齿
  renderer.setSize(width, height)
  renderer.setPixelRatio(window.devicePixelRatio) // 高清屏适配
  container.appendChild(renderer.domElement)

  // 4. 物体:立方体
  const geometry = new THREE.BoxGeometry(1, 1, 1)
  const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }) // 绿色
  cube = new THREE.Mesh(geometry, material)
  scene.add(cube)

  // 5. 渲染循环
  const animate = () => {
    animationId = requestAnimationFrame(animate)

    cube.rotation.x += 0.01
    cube.rotation.y += 0.01

    renderer.render(scene, camera)
  }
  animate()

  // 6. 窗口大小变化
  const handleResize = () => {
    const w = container.clientWidth
    const h = container.clientHeight
    camera.aspect = w / h
    camera.updateProjectionMatrix()
    renderer.setSize(w, h)
  }
  window.addEventListener('resize', handleResize)
})

onUnmounted(() => {
  // 记得取消动画帧,防止组件切换后还在后台跑
  cancelAnimationFrame(animationId)
  // 简单清理(后面会讲更完整的 dispose)
  renderer.dispose()
})
</script>

<style scoped>
.scene-container {
  width: 100%;
  height: 100vh;
  overflow: hidden;
}
</style>

1.3 通俗理解

  • Scene:一个空的 3D 空间,相当于"空房间"。
  • Camera:你站在房间哪个位置、朝哪看。
  • Renderer:把你看到的画面画到屏幕上的"画家"。
  • Mesh(网格):形状(Geometry)+ 外观(Material)的合体。
  • 渲染循环:不断重新画,每次稍微动一下物体,就是动画。

📅 第二阶段:让世界有"光" ------ 光照、材质、模型加载

目标

从"幼儿园画"变成"产品展示台",学会:

  • 标准材质 + 光照
  • 加载外部模型(GLB/GLTF)
  • 用 OrbitControls 让用户拖拽视角

2.1 核心知识点(通俗版)

材质(Material)

  • MeshBasicMaterial:不受光照影响,颜色就是最终颜色,适合 UI、调试。
  • MeshStandardMaterial:最常用的 PBR 材质,支持金属度、粗糙度,需要光照才好看。

光照(Light)

  • AmbientLight:环境光,整体提高亮度,不产生阴影。
  • DirectionalLight:平行光,像太阳光,可以产生阴影。
  • PointLight:点光源,像灯泡。

模型加载

  • 3D 美术通常给你 .glb / .gltf 文件。
  • 用 GLTFLoader 加载,模型里可能包含动画、相机、灯光等。

OrbitControls

  • 让用户通过鼠标拖拽、滚轮缩放来观察场景。
  • 官方示例写法:new OrbitControls(camera, renderer.domElement),并在每帧调用 controls.update()

2.2 示例代码:带光照 + OrbitControls 的场景

vue 复制代码
<template>
  <div class="scene-container" ref="containerRef"></div>
</template>

<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'

const containerRef = ref<HTMLDivElement>()

let scene: THREE.Scene
let camera: THREE.PerspectiveCamera
let renderer: THREE.WebGLRenderer
let controls: OrbitControls
let animationId: number

onMounted(() => {
  const container = containerRef.value!
  const width = container.clientWidth
  const height = container.clientHeight

  // 场景
  scene = new THREE.Scene()
  scene.background = new THREE.Color(0x222222)

  // 相机
  camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000)
  camera.position.set(5, 5, 5)

  // 渲染器
  renderer = new THREE.WebGLRenderer({ antialias: true })
  renderer.setSize(width, height)
  renderer.setPixelRatio(window.devicePixelRatio)
  renderer.shadowMap.enabled = true // 开启阴影
  container.appendChild(renderer.domElement)

  // 控制器
  controls = new OrbitControls(camera, renderer.domElement)
  controls.enableDamping = true // 阻尼效果,更顺滑

  // 地板
  const planeGeometry = new THREE.PlaneGeometry(20, 20)
  const planeMaterial = new THREE.MeshStandardMaterial({ color: 0x666666 })
  const plane = new THREE.Mesh(planeGeometry, planeMaterial)
  plane.rotation.x = -Math.PI / 2 // 放平
  plane.receiveShadow = true
  scene.add(plane)

  // 立方体
  const boxGeo = new THREE.BoxGeometry(2, 2, 2)
  const boxMat = new THREE.MeshStandardMaterial({ color: 0x00ff00 })
  const box = new THREE.Mesh(boxGeo, boxMat)
  box.position.y = 1 // 抬高一点
  box.castShadow = true
  scene.add(box)

  // 环境光
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
  scene.add(ambientLight)

  // 平行光(像太阳)
  const dirLight = new THREE.DirectionalLight(0xffffff, 1)
  dirLight.position.set(5, 10, 5)
  dirLight.castShadow = true
  scene.add(dirLight)

  // 渲染循环
  const animate = () => {
    animationId = requestAnimationFrame(animate)
    controls.update() // 重要:每帧更新控制器
    renderer.render(scene, camera)
  }
  animate()

  const handleResize = () => {
    const w = container.clientWidth
    const h = container.clientHeight
    camera.aspect = w / h
    camera.updateProjectionMatrix()
    renderer.setSize(w, h)
  }
  window.addEventListener('resize', handleResize)
})

onUnmounted(() => {
  cancelAnimationFrame(animationId)
  controls.dispose()
  renderer.dispose()
})
</script>

<style scoped>
.scene-container {
  width: 100%;
  height: 100vh;
  overflow: hidden;
}
</style>

2.3 通俗理解

  • Material = 物体的"皮肤"
    • BasicMaterial:像一张纸,不管光照,永远是那个颜色。
    • StandardMaterial:像真实物体,有光泽、金属感,需要光。
  • Light = 灯光
    • 没有光,StandardMaterial 看起来就是一片黑。
  • OrbitControls = 摇杆
    • 你不用再写代码控制相机,用户可以自己拖拽看 3D 模型。

📅 第三阶段:交互 ------ 点击物体,弹窗显示信息

目标

学会 Raycaster(射线),实现:

  • 鼠标点击 3D 物体
  • 在 Vue 弹窗里显示该物体的信息

3.1 核心知识点(通俗版)

Raycaster = 3D 里的"点击检测"

  • 想象你从相机位置,往鼠标方向射出一条射线。
  • 射线会穿过场景,打到的第一个物体就是你点击的物体。
  • 步骤:
    1. 把鼠标坐标转成 Three.js 的"标准设备坐标"(NDC)。
    2. raycaster.setFromCamera(mouse, camera)
    3. raycaster.intersectObjects(objects) 得到碰撞结果。

Vue 的响应式陷阱

  • Three.js 对象内部很复杂,Vue 的 reactive 会给它加代理,导致性能问题或报错。
  • 解决:
    • 三维对象不要放在 reactive / ref 里,用普通变量。
    • 如果需要触发视图更新,用 shallowRef

3.2 示例代码:点击物体弹出信息

vue 复制代码
<template>
  <div class="scene-wrapper">
    <div class="scene-container" ref="containerRef"></div>

    <!-- 信息面板 -->
    <div class="info-panel" v-if="selectedObject">
      <p>选中物体:{{ selectedObject.name || '未命名' }}</p>
      <button @click="selectedObject = null">关闭</button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'

const containerRef = ref<HTMLDivElement>()
const selectedObject = ref<THREE.Object3D | null>(null)

let scene: THREE.Scene
let camera: THREE.PerspectiveCamera
let renderer: THREE.WebGLRenderer
let controls: OrbitControls
let raycaster: THREE.Raycaster
let mouse: THREE.Vector2
let animationId: number

onMounted(() => {
  const container = containerRef.value!
  const width = container.clientWidth
  const height = container.clientHeight

  scene = new THREE.Scene()
  scene.background = new THREE.Color(0x222222)

  camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000)
  camera.position.set(5, 5, 5)

  renderer = new THREE.WebGLRenderer({ antialias: true })
  renderer.setSize(width, height)
  renderer.setPixelRatio(window.devicePixelRatio)
  container.appendChild(renderer.domElement)

  controls = new OrbitControls(camera, renderer.domElement)
  controls.enableDamping = true

  // 地板
  const planeGeo = new THREE.PlaneGeometry(20, 20)
  const planeMat = new THREE.MeshStandardMaterial({ color: 0x666666 })
  const plane = new THREE.Mesh(planeGeo, planeMat)
  plane.rotation.x = -Math.PI / 2
  plane.receiveShadow = true
  scene.add(plane)

  // 创建几个可点击的立方体
  const createBox = (x: number, z: number, color: number, name: string) => {
    const geo = new THREE.BoxGeometry(1, 1, 1)
    const mat = new THREE.MeshStandardMaterial({ color })
    const mesh = new THREE.Mesh(geo, mat)
    mesh.position.set(x, 0.5, z)
    mesh.name = name
    mesh.castShadow = true
    scene.add(mesh)
    return mesh
  }

  const boxA = createBox(2, 2, 0xff0000, 'boxA')
  const boxB = createBox(-2, 2, 0x0000ff, 'boxB')

  // 环境光 + 平行光
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
  scene.add(ambientLight)

  const dirLight = new THREE.DirectionalLight(0xffffff, 1)
  dirLight.position.set(5, 10, 5)
  dirLight.castShadow = true
  scene.add(dirLight)

  // 射线相关
  raycaster = new THREE.Raycaster()
  mouse = new THREE.Vector2()

  // 点击事件
  const onClick = (event: MouseEvent) => {
    const rect = container.getBoundingClientRect()
    mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
    mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1

    raycaster.setFromCamera(mouse, camera)
    const intersects = raycaster.intersectObjects(scene.children, false)

    if (intersects.length > 0) {
      const object = intersects[0].object
      selectedObject.value = object
      console.log('点击到:', object)
    }
  }

  container.addEventListener('click', onClick)

  const animate = () => {
    animationId = requestAnimationFrame(animate)
    controls.update()
    renderer.render(scene, camera)
  }
  animate()

  const handleResize = () => {
    const w = container.clientWidth
    const h = container.clientHeight
    camera.aspect = w / h
    camera.updateProjectionMatrix()
    renderer.setSize(w, h)
  }
  window.addEventListener('resize', handleResize)
})

onUnmounted(() => {
  cancelAnimationFrame(animationId)
  controls.dispose()
  renderer.dispose()
})
</script>

<style scoped>
.scene-wrapper {
  width: 100%;
  height: 100vh;
  overflow: hidden;
  position: relative;
}

.scene-container {
  width: 100%;
  height: 100%;
}

.info-panel {
  position: absolute;
  top: 20px;
  right: 20px;
  background: rgba(0, 0, 0, 0.7);
  color: #fff;
  padding: 16px;
  border-radius: 8px;
}
</style>

3.3 通俗理解

  • 射线检测
    • 鼠标在屏幕上点一下 → 转换成 3D 空间里的一条射线 → 看它先打到谁。
  • Vue 和 Three.js 的边界
    • Three.js 负责"画 3D 世界"。
    • Vue 负责"UI、弹窗、状态管理"。
    • 通过 selectedObject.value 把两者连起来。

📅 第四阶段:粒子与环境 ------ 做好看又轻量的特效

目标

  • 星空粒子背景
  • 环境贴图(让模型有反射)

4.1 核心知识点(通俗版)

粒子系统(Points)

  • 不是真的画几千个小球,而是:
    • 一个几何体,里面有很多顶点(点)。
    • Points + PointsMaterial 渲染。
  • 常用于星空、雪花、灰尘。

环境贴图

  • 给物体一个"周围环境"的假象,让它能反射周围景色。
  • 通常用 CubeTexture(立方体贴图):
    • 6 张图:右、左、上、下、前、后。

4.2 示例代码:简单星空粒子

vue 复制代码
<template>
  <div class="scene-container" ref="containerRef"></div>
</template>

<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'

const containerRef = ref<HTMLDivElement>()

let scene: THREE.Scene
let camera: THREE.PerspectiveCamera
let renderer: THREE.WebGLRenderer
let controls: OrbitControls
let animationId: number

onMounted(() => {
  const container = containerRef.value!
  const width = container.clientWidth
  const height = container.clientHeight

  scene = new THREE.Scene()
  scene.background = new THREE.Color(0x000511) // 深蓝黑色夜空

  camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000)
  camera.position.set(0, 0, 30)

  renderer = new THREE.WebGLRenderer({ antialias: true })
  renderer.setSize(width, height)
  renderer.setPixelRatio(window.devicePixelRatio)
  container.appendChild(renderer.domElement)

  controls = new OrbitControls(camera, renderer.domElement)
  controls.enableDamping = true

  // 创建星空粒子
  const starCount = 2000
  const positions = new Float32Array(starCount * 3)
  for (let i = 0; i < starCount; i++) {
    const i3 = i * 3
    positions[i3] = (Math.random() - 0.5) * 200     // x
    positions[i3 + 1] = (Math.random() - 0.5) * 200 // y
    positions[i3 + 2] = (Math.random() - 0.5) * 200 // z
  }

  const starGeo = new THREE.BufferGeometry()
  starGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3))

  const starMat = new THREE.PointsMaterial({
    size: 0.5,
    sizeAttenuation: true, // 近大远小
    color: 0xffffff
  })

  const stars = new THREE.Points(starGeo, starMat)
  scene.add(stars)

  const animate = () => {
    animationId = requestAnimationFrame(animate)
    controls.update()
    renderer.render(scene, camera)
  }
  animate()

  const handleResize = () => {
    const w = container.clientWidth
    const h = container.clientHeight
    camera.aspect = w / h
    camera.updateProjectionMatrix()
    renderer.setSize(w, h)
  }
  window.addEventListener('resize', handleResize)
})

onUnmounted(() => {
  cancelAnimationFrame(animationId)
  controls.dispose()
  renderer.dispose()
})
</script>

<style scoped>
.scene-container {
  width: 100%;
  height: 100vh;
  overflow: hidden;
}
</style>

4.3 通俗理解

  • 粒子 = 很多很多小点
    • 一次性画几千个 Mesh 会卡,用 Points 就很轻量。
  • 环境贴图 = 简单的"假环境"
    • 让物体看起来像在一个真实环境里,而不是空洞的空间。

📅 第五阶段:工程化与清理 ------ 准备上线的收尾

目标

  • 学会正确的资源清理(dispose)
  • 封装一个可复用的 ModelViewer 组件

5.1 核心知识点(通俗版)

为什么要 dispose?

  • Three.js 会占用显存(GPU 内存),不会自动释放。
  • 单页应用(SPA)如果不清理,切页面后:
    • 浏览器内存会越来越高
    • 甚至标签页崩溃

清理什么?

  • Geometry(几何体)
  • Material(材质)
  • Texture(贴图)
  • Renderer、Controls 等

一个常见写法是遍历 scene,统一清理。


5.2 示例代码:统一 dispose 工具函数

ts 复制代码
// utils/three/dispose.ts
import * as THREE from 'three'

export function disposeScene(scene: THREE.Scene) {
  scene.traverse((object) => {
    if (!(object instanceof THREE.Mesh)) return

    const mesh = object as THREE.Mesh

    // 清理几何体
    if (mesh.geometry) {
      mesh.geometry.dispose()
    }

    // 清理材质(可能是数组)
    if (mesh.material) {
      const materials = Array.isArray(mesh.material)
        ? mesh.material
        : [mesh.material]

      materials.forEach((material) => {
        // 清理贴图
        if ((material as THREE.MeshStandardMaterial).map) {
          (material as THREE.MeshStandardMaterial).map!.dispose()
        }
        material.dispose()
      })
    }
  })
}

在组件里使用:

ts 复制代码
import { disposeScene } from '@/utils/three/dispose'

onUnmounted(() => {
  cancelAnimationFrame(animationId)
  controls.dispose()
  disposeScene(scene)
  renderer.dispose()
})

5.3 封装一个简单 ModelViewer 组件

vue 复制代码
<template>
  <div class="model-viewer" ref="containerRef"></div>
</template>

<script setup lang="ts">
import { onMounted, onUnmounted, ref, watch } from 'vue'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { disposeScene } from '@/utils/three/dispose'

const props = defineProps<{
  modelUrl: string
}>()

const containerRef = ref<HTMLDivElement>()

let scene: THREE.Scene
let camera: THREE.PerspectiveCamera
let renderer: THREE.WebGLRenderer
let controls: OrbitControls
let gltfLoader: GLTFLoader
let currentModel: THREE.Object3D | null = null
let animationId: number

onMounted(() => {
  const container = containerRef.value!
  const width = container.clientWidth
  const height = container.clientHeight

  scene = new THREE.Scene()
  scene.background = new THREE.Color(0x222222)

  camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000)
  camera.position.set(0, 2, 5)

  renderer = new THREE.WebGLRenderer({ antialias: true })
  renderer.setSize(width, height)
  renderer.setPixelRatio(window.devicePixelRatio)
  renderer.shadowMap.enabled = true
  container.appendChild(renderer.domElement)

  controls = new OrbitControls(camera, renderer.domElement)
  controls.enableDamping = true

  const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
  scene.add(ambientLight)

  const dirLight = new THREE.DirectionalLight(0xffffff, 1)
  dirLight.position.set(5, 10, 5)
  dirLight.castShadow = true
  scene.add(dirLight)

  gltfLoader = new GLTFLoader()

  const animate = () => {
    animationId = requestAnimationFrame(animate)
    controls.update()
    renderer.render(scene, camera)
  }
  animate()

  const handleResize = () => {
    const w = container.clientWidth
    const h = container.clientHeight
    camera.aspect = w / h
    camera.updateProjectionMatrix()
    renderer.setSize(w, h)
  }
  window.addEventListener('resize', handleResize)

  // 初始加载
  loadModel(props.modelUrl)
})

onUnmounted(() => {
  cancelAnimationFrame(animationId)
  controls.dispose()
  disposeScene(scene)
  renderer.dispose()
})

// 监听 modelUrl 变化,切换模型
watch(
  () => props.modelUrl,
  (newUrl) => {
    loadModel(newUrl)
  }
)

function loadModel(url: string) {
  if (!gltfLoader || !scene) return

  // 先清除旧模型
  if (currentModel) {
    scene.remove(currentModel)
    // 如果你更严格,可以在这里 dispose 掉旧模型
    currentModel = null
  }

  gltfLoader.load(
    url,
    (gltf) => {
      currentModel = gltf.scene
      scene.add(currentModel)

      // 调整模型位置,让它居中并适当缩放
      const box = new THREE.Box3().setFromObject(currentModel)
      const center = box.getCenter(new THREE.Vector3())
      const size = box.getSize(new THREE.Vector3())

      const maxDim = Math.max(size.x, size.y, size.z)
      const scale = 3 / maxDim
      currentModel.scale.setScalar(scale)

      currentModel.position.sub(center)
      currentModel.position.y += 1 // 抬高一点
    },
    (xhr) => {
      console.log((xhr.loaded / xhr.total) * 100 + '% loaded')
    },
    (error) => {
      console.error('模型加载失败:', error)
    }
  )
}
</script>

<style scoped>
.model-viewer {
  width: 100%;
  height: 100%;
  min-height: 400px;
}
</style>

使用示例:

vue 复制代码
<template>
  <div>
    <ModelViewer modelUrl="/models/robot.glb" />
  </div>
</template>

<script setup lang="ts">
import ModelViewer from '@/components/ModelViewer.vue'
</script>

✅ 小结:你可以这样用这份计划

  1. 按阶段把示例代码跑通,先不求完全理解每一行。
  2. 每跑通一个示例,回看对应"通俗理解",把概念串起来。
  3. 之后在真实项目中:
    • 复制这些代码作为模板
    • 根据需求改参数、换模型、加交互
  4. 遇到问题,再回来看对应知识点,加深理解
相关推荐
沄媪2 小时前
CTF备赛学习
学习·ctf备赛·安全入门·windows安全系统
Bin Watson2 小时前
FOC 学习记录(1):自然坐标系建模和 DQ 轴的引出
学习
陈天伟教授2 小时前
人工智能应用- 搜索引擎:04. 网页重要性评估
人工智能·神经网络·搜索引擎·语言模型·自然语言处理
波动几何2 小时前
信息图设计提示词方案
人工智能
audyxiao0012 小时前
AI一周重要会议和活动概览(2.16-2.22)
人工智能·机器学习·一周会议与活动
AI英德西牛仔2 小时前
deepseek word 排版
人工智能
『往事』&白驹过隙;2 小时前
C/C++中的格式化输出与输入snprintf&sscanf
linux·c语言·c++·笔记·学习·iot·系统调用
好好学习天天向上~~3 小时前
12_Linux学习总结_进程地址空间(虚拟地址)
linux·学习
KG_LLM图谱增强大模型3 小时前
LLM能否通过语料库统计量成为可靠的检索触发器?
人工智能·知识图谱