下面这份是「增强版」学习计划,包含:
- 每个阶段的关键知识点(用大白话讲)
- 可直接跑的 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 核心知识点(通俗版)
三大核心对象:
-
Scene(场景)= 舞台
- 所有 3D 物体都要放到场景里,就像演员要上台。
- 代码:
const scene = new THREE.Scene()
-
Camera(相机)= 摄像机
- 决定了"观众从哪个角度看舞台"。
- 常用的是透视相机(PerspectiveCamera),近大远小,符合人眼。
- 关键参数:视角(fov)、宽高比、近裁剪面、远裁剪面。
-
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 里的"点击检测"
- 想象你从相机位置,往鼠标方向射出一条射线。
- 射线会穿过场景,打到的第一个物体就是你点击的物体。
- 步骤:
- 把鼠标坐标转成 Three.js 的"标准设备坐标"(NDC)。
- 用
raycaster.setFromCamera(mouse, camera)。 - 用
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>
✅ 小结:你可以这样用这份计划
- 按阶段把示例代码跑通,先不求完全理解每一行。
- 每跑通一个示例,回看对应"通俗理解",把概念串起来。
- 之后在真实项目中:
- 复制这些代码作为模板
- 根据需求改参数、换模型、加交互
- 遇到问题,再回来看对应知识点,加深理解