Three.js 核心技术:相机(正交 / 透视)、动画与资源加载、音视频播放、事件绑定等解析
一、简介二、正交相机使用三、透视相机使用四、动画五、批量加载资源、音视频播放、事件绑定
一、简介
Three.js 是一个基于 WebGL 的 JavaScript 3D 库,它简化了在浏览器中创建和渲染 3D 图形的过程,让开发者无需深入掌握底层 WebGL 细节,就能快速实现高质量的 3D 交互效果。
核心特点
1.封装底层复杂性:WebGL 是浏览器渲染 3D 图形的底层 API,但直接使用 WebGL 需要处理大量图形学细节(如着色器、顶点缓冲、矩阵运算等)。Three.js 封装了这些复杂操作,提供简洁的 API,让开发者专注于 3D 场景逻辑。
2.跨平台兼容性:基于 Web 标准(HTML5 + JavaScript),可在所有支持 WebGL 的浏览器中运行(包括 PC、移动端),无需安装插件,直接通过网页访问。
3.丰富的功能模块,内置了创建 3D 场景所需的核心组件:
场景(Scene):3D 世界的容器,所有物体、灯光、相机都需要添加到场景中。相机(Camera):模拟人眼视角,决定场景中哪些部分会被渲染(常用透视相机、正交相机)。渲染器(Renderer):将场景和相机的 “视角” 渲染到 DOM 元素(如 )中。几何体(Geometry):定义 3D 物体的形状(如立方体、球体、自定义模型)。材质(Material):定义物体的外观(颜色、纹理、反光、透明度等)。灯光(Light):照亮场景,影响物体的可见性和材质表现(如环境光、平行光、点光源)。动画系统:支持物体位置、旋转、缩放的动画,以及骨骼动画、变形动画等。交互支持:可通过射线投射(Raycaster)实现鼠标 / 触摸与 3D 物体的交互(如点击、选中)。
4.扩展性强:持加载外部 3D 模型(如 glTF、OBJ、FBX 等格式)、纹理图片、字体等资源,也可与其他库(如 React、Vue、GSAP 动画库)结合使用。
常见应用场景
1.数据可视化:用 3D 图表展示复杂数据(如人口分布、地形模型)。
2.游戏开发:浏览器端 3D 小游戏(如迷宫、解谜、休闲游戏)。
3.虚拟展示:产品 3D 预览(如家具、汽车)、虚拟展厅、博物馆数字化。
4.教育与仿真:3D 模型交互教学(如人体结构、化学分子)、物理仿真实验。
5.创意艺术:交互式 3D 艺术作品、动态视觉效果。
为什么选择 Three.js?
1.低门槛:对新手友好,无需深厚的图形学知识即可入门。
2.活跃社区:开源项目,文档丰富,社区问题解答及时,插件和示例资源多。
3.性能优化:内置多种性能优化方案(如层级渲染、实例化、LOD 细节层次),适合处理复杂场景。
简单来说,Three.js 是 Web 端 3D 开发的 “瑞士军刀”,让开发者能以更低成本在浏览器中实现媲美原生应用的 3D 体验
二、正交相机使用
在 three.js 中,正交相机(OrthographicCamera)是一种采用正交投影方式的相机,其核心特点是物体在渲染结果中显示的大小与物体到相机的距离无关,不会产生近大远小的透视效果,适合用于 2D 渲染、工程制图、UI 界面、等距视图等场景。正交投影中,相机的视锥体是一个长方体(轴对齐的平行六面体),而非透视相机的棱锥体。所有平行的线条在最终渲染中仍然保持平行,物体的尺寸在视图中保持一致,不会因距离相机远近而变化。
1.创建2D场景,原点为容器中心点
<template>
<div id="container" ref="threeRef"></div>
</template>
<script setup>
import * as THREE from 'three';
import { ref, onMounted } from 'vue';
const threeRef = ref(null);
// Vue组件挂载完成后执行的生命周期钩子
onMounted(async () => {
// 获取Three.js渲染容器的DOM元素
const container = threeRef.value;
// 安全检查:确保容器存在,避免空指针错误
if (!container) return;
// 解构获取容器的实际宽度和高度(包含padding和border)
const { offsetWidth: width, offsetHeight: height } = container;
// ========== 场景设置 ==========
// 创建三维场景,作为所有3D对象(网格、光源、相机等)的容器
const scene = new THREE.Scene();
// 设置场景背景颜色为白色(十六进制格式)
scene.background = new THREE.Color('#ffffff');
// ========== 相机设置 ==========
const halfWidth = width / 2;
const halfHeight = height / 2;
// 创建正交投影相机(适合2D/UI元素,无透视变形,物体大小不随距离变化)
const camera = new THREE.OrthographicCamera(
-halfWidth, // left:左裁剪平面(基于宽高比调整,确保不失真)
halfWidth, // right:右裁剪平面(与左边界对称)
halfHeight, // top:上裁剪平面(从中心向上)
-halfHeight, // bottom:下裁剪平面(从中心向下,与上边界对称)
0.1, // near:近裁剪面,距离相机0.1单位内的物体不渲染
1000 // far:远裁剪面,距离相机1000单位外的物体不渲染
);
// 设置相机在三维空间中的位置(X, Y, Z坐标)
// Z=100将相机放置在场景前方足够远的位置,确保能看到整个可视区域
camera.position.set(0, 0, 100);
// 设置相机朝向场景的中心点(原点),确保相机正对场景
camera.lookAt(0, 0, 0);
// ========== 渲染器设置 ==========
// 创建WebGL渲染器实例,用于将3D场景绘制到canvas上
const renderer = new THREE.WebGLRenderer({
antialias: true, // 开启抗锯齿,使3D模型边缘更平滑
alpha: true // 开启透明度通道,允许CSS背景渐变透过canvas显示
});
// 设置渲染器输出canvas的尺寸,与容器大小一致
renderer.setSize(width, height);
// 设置设备像素比率,限制最大为2以平衡显示质量与性能
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// 将渲染器的canvas元素添加到DOM容器中,开始显示3D内容
container.appendChild(renderer.domElement);
// ========== 光源设置 ==========
// 创建环境光:提供均匀的场景基础照明,无方向性
const ambientLight = new THREE.AmbientLight('#ffffff', 0.6); // 颜色白色,强度0.6
scene.add(ambientLight);
// 创建第一个方向光:模拟主光源,产生阴影和明暗效果
const directionalLight1 = new THREE.DirectionalLight('#ffffff', 0.8); // 颜色白色,强度0.8
directionalLight1.position.set(10, 10, 5); // 设置光源位置(右上前方)
scene.add(directionalLight1);
// 创建第二个方向光:作为补充光源,减少阴影区域的黑暗
const directionalLight2 = new THREE.DirectionalLight('#ffffff', 0.4); // 颜色白色,强度0.4
directionalLight2.position.set(-10, -10, -5); // 设置光源位置(左下后方)
scene.add(directionalLight2);
renderer.render(scene, camera);
});
</script>
<style lang="scss" scoped>
#container {
width: 100%;
height: 100%;
}
</style>
2.绘制文本,绘制文本需用到字体json,字体文件转json:https://gero3.github.io/facetype.js/
<template>
<div id="container" ref="threeRef"></div>
</template>
<script setup>
import * as THREE from 'three';
import { ref, onMounted } from 'vue';
import { FontLoader } from 'three/addons/loaders/FontLoader.js';
import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
const threeRef = ref(null);
// Vue组件挂载完成后执行的生命周期钩子
onMounted(async () => {
// 获取Three.js渲染容器的DOM元素
const container = threeRef.value;
// 安全检查:确保容器存在,避免空指针错误
if (!container) return;
// 解构获取容器的实际宽度和高度(包含padding和border)
const { offsetWidth: width, offsetHeight: height } = container;
// ========== 场景设置 ==========
// 创建三维场景,作为所有3D对象(网格、光源、相机等)的容器
const scene = new THREE.Scene();
// 设置场景背景颜色为白色(十六进制格式)
scene.background = new THREE.Color('#ffffff');
// ========== 相机设置 ==========
const halfWidth = width / 2;
const halfHeight = height / 2;
// 创建正交投影相机(适合2D/UI元素,无透视变形,物体大小不随距离变化)
const camera = new THREE.OrthographicCamera(
-halfWidth, // left:左裁剪平面(基于宽高比调整,确保不失真)
halfWidth, // right:右裁剪平面(与左边界对称)
halfHeight, // top:上裁剪平面(从中心向上)
-halfHeight, // bottom:下裁剪平面(从中心向下,与上边界对称)
0.1, // near:近裁剪面,距离相机0.1单位内的物体不渲染
1000 // far:远裁剪面,距离相机1000单位外的物体不渲染
);
// 设置相机在三维空间中的位置(X, Y, Z坐标)
// Z=100将相机放置在场景前方足够远的位置,确保能看到整个可视区域
camera.position.set(0, 0, 100);
// 设置相机朝向场景的中心点(原点),确保相机正对场景
----------
camera.lookAt(0, 0, 0);
// ========== 渲染器设置 ==========
// 创建WebGL渲染器实例,用于将3D场景绘制到canvas上
const renderer = new THREE.WebGLRenderer({
antialias: true, // 开启抗锯齿,使3D模型边缘更平滑
alpha: true // 开启透明度通道,允许CSS背景渐变透过canvas显示
});
// 设置渲染器输出canvas的尺寸,与容器大小一致
renderer.setSize(width, height);
// 设置设备像素比率,限制最大为2以平衡显示质量与性能
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// 将渲染器的canvas元素添加到DOM容器中,开始显示3D内容
container.appendChild(renderer.domElement);
// ========== 光源设置 ==========
// 创建环境光:提供均匀的场景基础照明,无方向性
const ambientLight = new THREE.AmbientLight('#ffffff', 0.6); // 颜色白色,强度0.6
scene.add(ambientLight);
// 创建第一个方向光:模拟主光源,产生阴影和明暗效果
const directionalLight1 = new THREE.DirectionalLight('#ffffff', 0.8); // 颜色白色,强度0.8
directionalLight1.position.set(10, 10, 5); // 设置光源位置(右上前方)
scene.add(directionalLight1);
// 创建第二个方向光:作为补充光源,减少阴影区域的黑暗
const directionalLight2 = new THREE.DirectionalLight('#ffffff', 0.4); // 颜色白色,强度0.4
directionalLight2.position.set(-10, -10, -5); // 设置光源位置(左下后方)
scene.add(directionalLight2);
// ========== 字体和3D文字创建 ==========
// 创建字体加载器实例,用于加载外部字体文件
const fontLoader = new FontLoader();
// 异步加载字体文件(来自Three.js官方示例字体库)
fontLoader.load(
// 字体文件的URL路径
'https://unpkg.com/three@0.160.0/examples/fonts/helvetiker_regular.typeface.json',
// 字体加载成功回调函数
(font) => {
// 创建3D文字几何体 - 将文本字符串转换为三维网格
const textGeometry = new TextGeometry('Hello Three.js', {
font: font, // 使用的字体对象
size: 18, // 字体大小(单位:世界坐标系单位)
height: 0.3, // 文字挤出厚度(3D深度,值越大文字越厚)
curveSegments: 12, // 曲线分段数(值越高曲线越平滑,但性能开销越大)
bevelEnabled: true, // 启用斜面效果,创建立体文字的倾斜边缘
bevelThickness: 0.03, // 斜面厚度(边缘倾斜部分的深度)
bevelSize: 0.02, // 斜面大小(边缘倾斜部分的宽度)
bevelOffset: 0, // 斜面偏移量(控制斜面起始位置)
bevelSegments: 5 // 斜面分段数(值越高斜面越平滑)
});
// ========== 文字居中处理 ==========
// 计算几何体的边界框(获取文字的实际尺寸信息)
textGeometry.computeBoundingBox();
// 解构获取边界框的最小和最大坐标点
const { min, max } = textGeometry.boundingBox;
// 计算X轴居中偏移量:将文字中心点对齐到场景X轴中心
const centerX = -(max.x - min.x) / 2;
// 计算Y轴居中偏移量:将文字中心点对齐到场景Y轴中心
const centerY = -(max.y - min.y) / 2;
// ========== 材质和网格创建 ==========
// 创建Phong材质(支持高光反射和复杂光照计算)
const textMaterial = new THREE.MeshPhongMaterial({
color: '#000000', // 字体颜色
specular: '#ffffff', // 高光反射颜色:白色
shininess: 100 // 高光强度:0-100,值越大高光点越小越亮
});
// 创建网格对象:将几何体与材质结合,形成可渲染的3D对象
const textMesh = new THREE.Mesh(textGeometry, textMaterial);
// 设置文字在场景中的位置(居中到场景原点,Z轴为0即在相机正前方)
textMesh.position.set(centerX, centerY, 0);
// 将文字网格添加到场景中,使其成为场景的一部分
scene.add(textMesh);
// ========== 执行渲染 ==========
// 使用当前场景和相机进行单次渲染,将3D内容绘制到canvas上
renderer.render(scene, camera);
},
// 加载进度回调函数(可选,此处未定义)- 可用于显示加载进度条
undefined,
// 加载错误回调函数(可选,此处未定义)- 可用于处理字体加载失败情况
undefined
);
});
</script>
<style lang="scss" scoped>
#container {
width: 100%;
height: 100%;
}
</style>

3.绘制图片
<template>
<div id="container" ref="threeRef"></div>
</template>
<script setup>
import * as THREE from 'three';
import { ref, onMounted } from 'vue';
const threeRef = ref(null);
onMounted(async () => {
const container = threeRef.value;
if (!container) return;
const { offsetWidth: width, offsetHeight: height } = container;
// 场景设置
const scene = new THREE.Scene();
scene.background = new THREE.Color('#ffffff');
// 相机设置
const halfWidth = width / 2;
const halfHeight = height / 2;
const camera = new THREE.OrthographicCamera(
-halfWidth,
halfWidth,
halfHeight,
-halfHeight,
0.1,
1000
);
camera.position.set(0, 0, 100);
camera.lookAt(0, 0, 0);
// 渲染器设置
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
container.appendChild(renderer.domElement);
// 光源设置
const ambientLight = new THREE.AmbientLight('#ffffff', 0.6);
scene.add(ambientLight);
const directionalLight1 = new THREE.DirectionalLight('#ffffff', 0.8);
directionalLight1.position.set(10, 10, 5);
scene.add(directionalLight1);
const directionalLight2 = new THREE.DirectionalLight('#ffffff', 0.4);
directionalLight2.position.set(-10, -10, -5);
scene.add(directionalLight2);
// 添加图片到场景
// 创建纹理加载器
const textureLoader = new THREE.TextureLoader();
try {
const texture = await textureLoader.loadAsync('https://picsum.photos/800/600');
// 创建平面几何体(矩形)
// 参数:宽度、高度、宽度分段数、高度分段数
const planeGeometry = new THREE.PlaneGeometry(400, 300);
// 创建材质(使用加载的纹理)
const planeMaterial = new THREE.MeshPhongMaterial({
map: texture, // 应用纹理
specular: '#ffffff', // 高光反射颜色:白色
shininess: 100 // 高光强度:0-100,值越大高光点越小越亮
});
// 创建网格(几何体+材质)
const imagePlane = new THREE.Mesh(planeGeometry, planeMaterial);
// 设置图片位置(场景中心)
imagePlane.position.set(0, 0, 0);
// 添加到场景
scene.add(imagePlane);
renderer.render(scene, camera);
} catch (error) {
console.error('图片加载失败', error);
renderer.render(scene, camera);
}
});
</script>
<style lang="scss" scoped>
#container {
width: 100vw;
height: 100vh;
}
</style>

4.绘制canvas贴图,如:绘制文字,canvas贴图又分精灵贴图、平面贴图,精灵贴图始终正对摄像机,平面贴图会随场景旋转改变角度
<template>
<div id="container" ref="threeRef"></div>
</template>
<script setup>
import * as THREE from 'three';
import { ref, onMounted } from 'vue';
import html2canvas from 'html2canvas';
const threeRef = ref(null);
onMounted(async () => {
const container = threeRef.value;
if (!container) return;
const { offsetWidth: width, offsetHeight: height } = container;
// 场景设置
const scene = new THREE.Scene();
scene.background = new THREE.Color('#ffffff');
// 正交相机设置 - 适合2D图形展示
const halfWidth = width / 2;
const halfHeight = height / 2;
const camera = new THREE.OrthographicCamera(
-halfWidth,
halfWidth,
halfHeight,
-halfHeight,
0.1,
1000
);
camera.position.set(0, 0, 100);
camera.lookAt(0, 0, 0);
// 渲染器设置
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
container.appendChild(renderer.domElement);
// 光源设置
const ambientLight = new THREE.AmbientLight('#ffffff', 0.6);
scene.add(ambientLight);
const directionalLight1 = new THREE.DirectionalLight('#ffffff', 0.8);
directionalLight1.position.set(10, 10, 50);
scene.add(directionalLight1);
const directionalLight2 = new THREE.DirectionalLight('#ffffff', 0.4);
directionalLight2.position.set(-10, -10, 50);
scene.add(directionalLight2);
const spriteDiv = document.createElement('div');
spriteDiv.style.backgroundColor = 'red';
spriteDiv.style.borderRadius = '20px';
spriteDiv.style.color = '#ffffff';
spriteDiv.style.fontSize = '24px';
spriteDiv.style.textAlign = 'center';
spriteDiv.style.lineHeight = '40px';
spriteDiv.textContent = '精灵贴图';
document.body.appendChild(spriteDiv);
const spriteCanvas = await html2canvas(spriteDiv);
const spriteTexture = new THREE.CanvasTexture(spriteCanvas);
const spriteMaterial = new THREE.SpriteMaterial({ map: spriteTexture });
const sprite = new THREE.Sprite(spriteMaterial);
sprite.position.set(0, -40, 1);
sprite.scale.set(180, 40, 1);
scene.add(sprite);
spriteDiv.textContent = '平面贴图';
const meshCanvas = await html2canvas(spriteDiv);
const meshTexture = new THREE.CanvasTexture(meshCanvas);
let material = new THREE.MeshBasicMaterial({
map: meshTexture,
side: THREE.DoubleSide
});
let geometry = new THREE.PlaneGeometry();
let mesh = new THREE.Mesh(geometry, material);
mesh.position.set(0, 40, 1);
mesh.scale.set(180, 40, 1);
scene.add(mesh);
document.body.removeChild(spriteDiv);
renderer.render(scene, camera);
});
</script>
<style lang="scss" scoped>
#container {
width: 100vw;
height: 100vh;
overflow: hidden;
}
</style>

5.绘制dom,将dom添加到场景中,CSS3DRenderer和CSS2DRenderer渲染的本质还是dom,可以直接添加dom事件
<template>
<div id="container" ref="threeRef"></div>
</template>
<script setup>
/**
CSS2DRenderer特点:
渲染2D元素,始终面向相机,不受3D透视影响。
元素大小和位置固定,不随摄像机视角变化。
性能更轻量,适合大量标签或简单弹窗。
交互上类似HUD叠加在场景中,不随场景旋转变化。
支持常规CSS,但不能使用3D transform123456。
CSS3DRenderer特点:
渲染真正的3D对象,具有深度信息。
元素大小和位置随摄像机视角变化,可旋转、缩放、透视。
性能相对更重,适合复杂UI或网页窗口。
交互上完全沉浸式,随场景旋转变化。
支持完整CSS,包括3D transform和动画12789
**/
import * as THREE from 'three';
import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
import { CSS3DRenderer, CSS3DObject } from 'three/addons/renderers/CSS3DRenderer.js';
import { ref, onMounted } from 'vue';
const threeRef = ref(null);
onMounted(() => {
const container = threeRef.value;
if (!container) return;
const { offsetWidth: width, offsetHeight: height } = container;
// 场景设置
const scene = new THREE.Scene();
scene.background = new THREE.Color('#ffffff');
// 正交相机设置
const halfWidth = width / 2;
const halfHeight = height / 2;
const camera = new THREE.OrthographicCamera(
-halfWidth,
halfWidth,
halfHeight,
-halfHeight,
0.1,
1000
);
camera.position.set(0, 0, 100);
camera.lookAt(0, 0, 0);
// 基础WebGL渲染器(用于场景背景,可省略)
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
container.appendChild(renderer.domElement);
// CSS2D渲染器(替代精灵贴图)
const css2DRenderer = new CSS2DRenderer();
css2DRenderer.setSize(width, height);
css2DRenderer.domElement.style.position = 'absolute';
css2DRenderer.domElement.style.top = '0';
css2DRenderer.domElement.style.pointerEvents = 'none'; // 允许鼠标穿透到WebGL场景
container.appendChild(css2DRenderer.domElement);
// CSS3D渲染器(替代平面贴图)
const css3DRenderer = new CSS3DRenderer();
css3DRenderer.setSize(width, height);
css3DRenderer.domElement.style.position = 'absolute';
css3DRenderer.domElement.style.top = '0';
css3DRenderer.domElement.style.pointerEvents = 'none';
container.appendChild(css3DRenderer.domElement);
// 创建CSS2D元素(替代精灵)s
const css2DElement = document.createElement('div');
css2DElement.style.width = '180px';
css2DElement.style.height = '40px';
css2DElement.style.backgroundColor = 'red';
css2DElement.style.borderRadius = '20px';
css2DElement.style.color = '#ffffff';
css2DElement.style.fontSize = '24px';
css2DElement.style.textAlign = 'center';
css2DElement.style.lineHeight = '40px';
css2DElement.textContent = 'CSS2D DOM';
css2DElement.onclick = () => {
console.log('点击了CSS2D元素');
};
const css2DObject = new CSS2DObject(css2DElement);
css2DObject.position.set(0, -40, 1); // 与原精灵位置一致
scene.add(css2DObject);
// 创建CSS3D元素(替代平面)
const css3DElement = document.createElement('div');
css3DElement.style.width = '180px';
css3DElement.style.height = '40px';
css3DElement.style.backgroundColor = 'red';
css3DElement.style.borderRadius = '20px';
css3DElement.style.color = '#ffffff';
css3DElement.style.fontSize = '24px';
css3DElement.style.textAlign = 'center';
css3DElement.style.lineHeight = '40px';
css3DElement.textContent = 'CSS3D DOM';
css3DElement.onclick = () => {
console.log('点击了CSS3D元素');
};
const css3DObject = new CSS3DObject(css3DElement);
css3DObject.position.set(0, 40, 1); // 与原平面位置一致
// CSS3D可以添加3D变换(例如旋转)
// css3DObject.rotation.y = Math.PI / 4; // 演示3D旋转效果
scene.add(css3DObject);
renderer.render(scene, camera);
css2DRenderer.render(scene, camera);
css3DRenderer.render(scene, camera);
});
</script>
<style lang="scss" scoped>
#container {
width: 100vw;
height: 100vh;
overflow: hidden;
position: relative; // 确保CSS渲染器的绝对定位有效
}
</style>

6.绘制平面几何体,如:长方形、正方形、圆形、平行四边形、梯形、不规则图形
<template>
<div id="container" ref="threeRef"></div>
</template>
<script setup>
import * as THREE from 'three';
import { ref, onMounted } from 'vue';
const threeRef = ref(null);
onMounted(async () => {
// 获取Three.js渲染容器的DOM元素
const container = threeRef.value;
// 解构获取容器的实际宽度和高度(包含padding和border)
const { offsetWidth: width, offsetHeight: height } = container;
// ========== 场景设置 ==========
// 创建三维场景,作为所有3D对象(网格、光源、相机等)的容器
const scene = new THREE.Scene();
// 设置场景背景颜色为白色(十六进制格式)
scene.background = new THREE.Color('#ffffff');
// ========== 相机设置 ==========
const halfWidth = width / 2;
const halfHeight = height / 2;
// 创建正交投影相机(适合2D/UI元素,无透视变形,物体大小不随距离变化)
const camera = new THREE.OrthographicCamera(
-halfWidth, // left:左裁剪平面(基于宽高比调整,确保不失真)
halfWidth, // right:右裁剪平面(与左边界对称)
halfHeight, // top:上裁剪平面(从中心向上)
-halfHeight, // bottom:下裁剪平面(从中心向下,与上边界对称)
0.1, // near:近裁剪面,距离相机0.1单位内的物体不渲染
1000 // far:远裁剪面,距离相机1000单位外的物体不渲染
);
// 设置相机在三维空间中的位置(X, Y, Z坐标)
// Z=100将相机放置在场景前方足够远的位置,确保能看到整个可视区域
camera.position.set(0, 0, 100);
// 设置相机朝向场景的中心点(原点),确保相机正对场景
camera.lookAt(0, 0, 0);
// ========== 渲染器设置 ==========
// 创建WebGL渲染器实例,用于将3D场景绘制到canvas上
const renderer = new THREE.WebGLRenderer({
antialias: true, // 开启抗锯齿,使3D模型边缘更平滑
alpha: true // 开启透明度通道,允许CSS背景渐变透过canvas显示
});
// 设置渲染器输出canvas的尺寸,与容器大小一致
renderer.setSize(width, height);
// 设置设备像素比率,限制最大为2以平衡显示质量与性能
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// 将渲染器的canvas元素添加到DOM容器中,开始显示3D内容
container.appendChild(renderer.domElement);
// ========== 光源设置 ==========
// 创建环境光:提供均匀的场景基础照明,无方向性
const ambientLight = new THREE.AmbientLight('#ffffff', 0.6); // 颜色白色,强度0.6
scene.add(ambientLight);
// 创建第一个方向光:模拟主光源,产生阴影和明暗效果
const directionalLight1 = new THREE.DirectionalLight('#ffffff', 0.8); // 颜色白色,强度0.8
directionalLight1.position.set(10, 10, 5); // 设置光源位置(右上前方)
scene.add(directionalLight1);
// 创建第二个方向光:作为补充光源,减少阴影区域的黑暗
const directionalLight2 = new THREE.DirectionalLight('#ffffff', 0.4); // 颜色白色,强度0.4
directionalLight2.position.set(-10, -10, -5); // 设置光源位置(左下后方)
scene.add(directionalLight2);
// 通用材质设置
const materials = {
red: new THREE.MeshBasicMaterial({ color: '#ff4444' }),
green: new THREE.MeshBasicMaterial({ color: '#44dd44' }),
blue: new THREE.MeshBasicMaterial({ color: '#4444ff' }),
yellow: new THREE.MeshBasicMaterial({ color: '#dddd44' }),
purple: new THREE.MeshBasicMaterial({ color: '#dd44dd' }),
orange: new THREE.MeshBasicMaterial({ color: '#ffaa44' })
};
// 1. 长方形
const rectangleGeometry = new THREE.PlaneGeometry(100, 60);
const rectangle = new THREE.Mesh(rectangleGeometry, materials.red);
const oneLineX = -halfWidth + 100 / 2 + 20;
const oneLineY = halfHeight - 60 / 2 - 20;
rectangle.position.set(oneLineX, oneLineY); //顶部区域上边距20、左边距20
scene.add(rectangle);
// 2. 正方形
const squareGeometry = new THREE.PlaneGeometry(70, 70);
const square = new THREE.Mesh(squareGeometry, materials.green);
square.position.set(oneLineX + 100, oneLineY); //顶部区域上边距20、左边距20
scene.add(square);
// 3. 圆形 (通过CircleGeometry实现)
const circleGeometry = new THREE.CircleGeometry(35, 32); // 半径35,32段
const circle = new THREE.Mesh(circleGeometry, materials.blue);
circle.position.set(oneLineX + 190, oneLineY); // 左中位置
scene.add(circle);
// 4. 平行四边形(使用ShapeGeometry)
const parallelogramShape = new THREE.Shape();
parallelogramShape.moveTo(-40, 30); // 左上
parallelogramShape.lineTo(30, 30); // 右上
parallelogramShape.lineTo(40, -30); // 右下
parallelogramShape.lineTo(-30, -30); // 左下
parallelogramShape.closePath(); // 闭合路径
const twoLineX = -halfWidth + 80 / 2 + 20;
const twoLineY = oneLineY - 90;
const parallelogram = new THREE.Mesh(
new THREE.ShapeGeometry(parallelogramShape),
materials.yellow
);
parallelogram.position.set(twoLineX, twoLineY);
scene.add(parallelogram);
// 5. 梯形(使用ShapeGeometry)
const trapezoidShape = new THREE.Shape();
trapezoidShape.moveTo(-50, 30); // 上左
trapezoidShape.lineTo(50, 30); // 上右
trapezoidShape.lineTo(60, -30); // 下右
trapezoidShape.lineTo(-60, -30); // 下左
trapezoidShape.closePath();
const trapezoid = new THREE.Mesh(
new THREE.ShapeGeometry(trapezoidShape),
materials.purple
);
trapezoid.position.set(twoLineX + 130, twoLineY);
scene.add(trapezoid);
// 6. 不规则图形(五边形,使用ShapeGeometry)
const irregularShape = new THREE.Shape();
irregularShape.moveTo(0, 40); // 顶部
irregularShape.lineTo(40, 10); // 右上
irregularShape.lineTo(25, -30); // 右下
irregularShape.lineTo(-25, -30); // 左下
irregularShape.lineTo(-40, 10); // 左上
irregularShape.closePath();
const irregular = new THREE.Mesh(
new THREE.ShapeGeometry(irregularShape),
materials.orange
);
irregular.position.set(twoLineX + 240, twoLineY);
scene.add(irregular);
// 7. 圆环
const ringGeometry = new THREE.RingGeometry( 30, 40, 32 );
const threeLineX = -halfWidth + 80 / 2 + 20;
const threeLineY = oneLineY - 180;
const ringMesh = new THREE.Mesh(ringGeometry, materials.orange);
ringMesh.position.set(threeLineX, threeLineY);
scene.add(ringMesh );
// 8. 带边圆角的矩形
function createRoundedRectShape(width, height, radius) {
const shape = new THREE.Shape();
// 计算半宽和半高(以中心为原点)
const halfW = width / 2;
const halfH = height / 2;
// 从右上角开始绘制(顺时针方向)
shape.moveTo(halfW - radius, halfH);
// 右上圆角(使用二次贝塞尔曲线)
shape.quadraticCurveTo(halfW, halfH, halfW, halfH - radius);
// 右边缘
shape.lineTo(halfW, -halfH + radius);
// 右下圆角
shape.quadraticCurveTo(halfW, -halfH, halfW - radius, -halfH);
// 下边缘
shape.lineTo(-halfW + radius, -halfH);
// 左下圆角
shape.quadraticCurveTo(-halfW, -halfH, -halfW, -halfH + radius);
// 左边缘
shape.lineTo(-halfW, halfH - radius);
// 左上圆角
shape.quadraticCurveTo(-halfW, halfH, -halfW + radius, halfH);
// 闭合路径
shape.closePath();
return shape;
}
const reactWidth = 80;
const reactHeight = 80;
const radius = 15;
//圆角矩形
const roundedRectShape = createRoundedRectShape(
reactWidth,
reactHeight,
radius
);
const roundedRectGeometry = new THREE.ShapeGeometry(roundedRectShape);
const roundedRect = new THREE.Mesh(roundedRectGeometry, materials.purple);
roundedRect.position.set(threeLineX + 100, threeLineY);
scene.add(roundedRect);
renderer.render(scene, camera);
});
</script>
<style lang="scss" scoped>
#container {
width: 100%;
height: 100%;
}
</style>

7.绘制平面3D几何体,如:长方体、正方体、圆柱、圆锥、不规则
<template>
<div id="container" ref="threeRef"></div>
</template>
<script setup>
import * as THREE from 'three';
import { ref, onMounted } from 'vue';
const threeRef = ref(null);
onMounted(async () => {
// 获取Three.js渲染容器的DOM元素
const container = threeRef.value;
// 解构获取容器的实际宽度和高度(包含padding和border)
const { offsetWidth: width, offsetHeight: height } = container;
// ========== 场景设置 ==========
const scene = new THREE.Scene();
scene.background = new THREE.Color('#ffffff');
// ========== 相机设置 ==========
const halfWidth = width / 2;
const halfHeight = height / 2;
const camera = new THREE.OrthographicCamera(
-halfWidth,
halfWidth,
halfHeight,
-halfHeight,
0.1,
1000
);
camera.position.set(0, 0, 100);
camera.lookAt(0, 0, 0);
// ========== 渲染器设置 ==========
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
container.appendChild(renderer.domElement);
// ========== 光源设置 ==========
const ambientLight = new THREE.AmbientLight('#ffffff', 0.6);
scene.add(ambientLight);
const directionalLight1 = new THREE.DirectionalLight('#ffffff', 0.8);
directionalLight1.position.set(10, 10, 5);
scene.add(directionalLight1);
const directionalLight2 = new THREE.DirectionalLight('#ffffff', 0.4);
directionalLight2.position.set(-10, -10, -5);
scene.add(directionalLight2);
// ========== 几何体创建 ==========
// 1. 正方体 (长宽高相等的立方体)
// 创建立方体几何体(宽, 高, 深)
const cubeGeometry = new THREE.BoxGeometry(50, 50, 50);
// 创建材质(绿色)
const cubeMaterial = new THREE.MeshPhongMaterial({ color: 0x4CAF50 });
// 创建网格对象
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
// 设置位置(左侧)
cube.position.set(-150, -10, 0);
cube.rotation.set(2, 2, 0);
scene.add(cube);
// 2. 长方体 (长宽高不等)
const boxGeometry = new THREE.BoxGeometry(80, 40, 30);
const boxMaterial = new THREE.MeshPhongMaterial({ color: 0x2196F3 });
const box = new THREE.Mesh(boxGeometry, boxMaterial);
// 设置位置(左中)
box.position.set(-50, 0, 0);
box.rotation.set(2, 2, 0);
scene.add(box);
// 3. 圆柱体
// CylinderGeometry(顶部半径, 底部半径, 高度, 侧面分段数)
const cylinderGeometry = new THREE.CylinderGeometry(25, 25, 60, 32);
const cylinderMaterial = new THREE.MeshPhongMaterial({ color: 0xFFC107 });
const cylinder = new THREE.Mesh(cylinderGeometry, cylinderMaterial);
// 设置位置(中间)
cylinder.position.set(50, 0, 0);
cylinder.rotation.set(2, 2, 0);
scene.add(cylinder);
// 4. 圆锥体
// ConeGeometry(底部半径, 高度, 侧面分段数)
const coneGeometry = new THREE.ConeGeometry(25, 60, 32);
const coneMaterial = new THREE.MeshPhongMaterial({ color: 0xF44336 });
const cone = new THREE.Mesh(coneGeometry, coneMaterial);
// 设置位置(右中)
cone.position.set(150, 0, 0);
cone.rotation.set(2, 2, 0);
scene.add(cone);
// 5. 不规则几何体 (自定义顶点创建)
// 创建空几何体
const customGeometry = new THREE.BufferGeometry();
// 定义顶点坐标 (x, y, z)
const vertices = new Float32Array([
// 前面顶点
0, 40, 0, // 顶部
30, -20, 0, // 右下
-30,-20, 0, // 左下
// 后面顶点
0, 20, -30, // 顶部
20, -10, -30, // 右下
-20,-10, -30 // 左下
]);
// 定义面 (由哪三个顶点组成)
const indices = new Uint16Array([
0, 1, 2, // 前面三角形
3, 4, 5, // 后面三角形
0, 3, 5, // 左侧连接面
0, 5, 2, // 左侧连接面
0, 3, 1, // 右侧连接面
3, 4, 1 // 右侧连接面
]);
// 设置几何体属性
customGeometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
customGeometry.setIndex(new THREE.BufferAttribute(indices, 1));
// 计算法向量(用于正确光照)
customGeometry.computeVertexNormals();
const customMaterial = new THREE.MeshPhongMaterial({
color: 0x9C27B0,
side: THREE.DoubleSide // 双面可见
});
const customShape = new THREE.Mesh(customGeometry, customMaterial);
// 设置位置(下方)
customShape.position.set(0, -100, 0);
customShape.rotation.set(4, 4, 0 );
scene.add(customShape);
renderer.render(scene, camera);
});
</script>
<style lang="scss" scoped>
#container {
width: 100vw;
height: 100vh;
}
</style>

三、透视相机使用
在 Three.js 中,透视相机(PerspectiveCamera) 是模拟人眼视觉效果的相机类型,它能让场景中的物体呈现出 “近大远小” 的透视效果,是 3D 场景中最常用的相机之一。透视相机通过模拟现实世界中光线进入人眼(或相机镜头)的方式工作:远处的物体看起来更小,近处的物体看起来更大,且平行的线条会在远处汇聚于一点(消失点),从而产生真实的 3D 空间感
1.创建3D场景,原点为容器中心点
<template>
<div id="container" ref="threeRef"></div>
</template>
<script setup>
import * as THREE from 'three';
import { ref, onMounted } from 'vue';
const threeRef = ref(null);
onMounted(async () => {
const container = threeRef.value;
if (!container) return;
const { offsetWidth: width, offsetHeight: height } = container;
// 场景设置
const scene = new THREE.Scene();
scene.background = new THREE.Color('#ffffff');
// ========== 透视相机设置 ==========
// 创建透视相机(模拟人眼视觉,有近大远小的透视效果)
const camera = new THREE.PerspectiveCamera(
75, // fov:视场角,垂直方向的可视角度(单位:度)
width / height, // aspect:宽高比,通常为容器的宽/高
0.1, // near:近裁剪面
1000 // far:远裁剪面
);
// 透视相机需要一定的Z轴距离才能看到物体
camera.position.set(0, 0, 100);
camera.lookAt(0, 0, 0); // 保持相机朝向场景中心
// 渲染器设置
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
container.appendChild(renderer.domElement);
// 光源设置
const ambientLight = new THREE.AmbientLight('#ffffff', 0.6);
scene.add(ambientLight);
const directionalLight1 = new THREE.DirectionalLight('#ffffff', 0.8);
directionalLight1.position.set(10, 10, 5);
scene.add(directionalLight1);
const directionalLight2 = new THREE.DirectionalLight('#ffffff', 0.4);
directionalLight2.position.set(-10, -10, -5);
scene.add(directionalLight2);
renderer.render(scene, camera);
});
</script>
<style lang="scss" scoped>
#container {
width: 100%;
height: 100vh; // 改为100vh方便测试全屏效果
}
</style>
2.透视相机和正式相机的绘制差不多,唯一区别就是有了z轴的世界单位,相机同样的z表现出来的并不一样,把之前的正交相机示例的代码中相机替换为透视相机,可以看到同样的z,物体明显太大,而且使用TextGeometry绘制的字体是立体的,如果想在透视相机绘制2d字体就需要使用canvas了


3.解决同样的物体在正交相机和透视相机表现不同的情况就需要根据可视区域动态计算透视相机z的值
<template>
<div id="container" ref="threeRef"></div>
</template>
<script setup>
import * as THREE from 'three';
import { ref, onMounted } from 'vue';
const threeRef = ref(null);
onMounted(async () => {
const container = threeRef.value;
if (!container) return;
const { offsetWidth: width, offsetHeight: height } = container;
// 场景设置
const scene = new THREE.Scene();
scene.background = new THREE.Color('#ffffff');
// 相机设置
// const halfWidth = width / 2;
// const halfHeight = height / 2;
// const camera = new THREE.OrthographicCamera(
// -halfWidth, // left:左裁剪平面(基于宽高比调整,确保不失真)
// halfWidth, // right:右裁剪平面(与左边界对称)
// halfHeight, // top:上裁剪平面(从中心向上)
// -halfHeight, // bottom:下裁剪平面(从中心向下,与上边界对称)
// 0.1, // near:近裁剪面,距离相机0.1单位内的物体不渲染
// 1000 // far:远裁剪面,距离相机1000单位外的物体不渲染
// );
// const maxZ = 100;
const fov = 75;
const vFOV = THREE.MathUtils.degToRad(fov);
const maxZ = height / (2 * Math.tan(vFOV / 2));
const camera = new THREE.PerspectiveCamera(
fov, // fov:视场角,垂直方向的可视角度(单位:度)
width / height, // aspect:宽高比,通常为容器的宽/高
0.1, // near:近裁剪面
height // far:远裁剪面
);
// 设置相机在三维空间中的位置(X, Y, Z坐标)
camera.position.set(0, 0, maxZ);
// 设置相机朝向场景的中心点(原点),确保相机正对场景
camera.lookAt(0, 0, 0);
// 渲染器设置
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
container.appendChild(renderer.domElement);
// 光源设置
const ambientLight = new THREE.AmbientLight('#ffffff', 0.6);
scene.add(ambientLight);
const directionalLight1 = new THREE.DirectionalLight('#ffffff', 0.8);
directionalLight1.position.set(10, 10, 5);
scene.add(directionalLight1);
const directionalLight2 = new THREE.DirectionalLight('#ffffff', 0.4);
directionalLight2.position.set(-10, -10, -5);
scene.add(directionalLight2);
// 添加图片到场景
// 创建纹理加载器
const textureLoader = new THREE.TextureLoader();
try {
const texture = await textureLoader.loadAsync('https://picsum.photos/800/600');
// 创建平面几何体(矩形)
// 参数:宽度、高度、宽度分段数、高度分段数
const planeGeometry = new THREE.PlaneGeometry(400, 300);
// 创建材质(使用加载的纹理)
const planeMaterial = new THREE.MeshPhongMaterial({
map: texture, // 应用纹理
specular: '#ffffff', // 高光反射颜色:白色
shininess: 100 // 高光强度:0-100,值越大高光点越小越亮
});
// 创建网格(几何体+材质)
const imagePlane = new THREE.Mesh(planeGeometry, planeMaterial);
// 设置图片位置(场景中心)
imagePlane.position.set(0, 0, 0);
// 添加到场景
scene.add(imagePlane);
renderer.render(scene, camera);
} catch (error) {
console.error('图片加载失败', error);
renderer.render(scene, camera);
}
});
</script>
<style lang="scss" scoped>
#container {
width: 100vw;
height: 100vh;
}
</style>

4.除了动态计算出z值,还可以根据z值计算出世界单位与屏幕单位之间的比例进行换算
<template>
<div id="container" ref="threeRef"></div>
</template>
<script setup>
import * as THREE from 'three';
import { ref, onMounted } from 'vue';
const threeRef = ref(null);
onMounted(async () => {
const container = threeRef.value;
if (!container) return;
const { offsetWidth: width, offsetHeight: height } = container;
// 场景设置
const scene = new THREE.Scene();
scene.background = new THREE.Color('#ffffff');
// 相机设置
// const halfWidth = width / 2;
// const halfHeight = height / 2;
// const camera = new THREE.OrthographicCamera(
// -halfWidth, // left:左裁剪平面(基于宽高比调整,确保不失真)
// halfWidth, // right:右裁剪平面(与左边界对称)
// halfHeight, // top:上裁剪平面(从中心向上)
// -halfHeight, // bottom:下裁剪平面(从中心向下,与上边界对称)
// 0.1, // near:近裁剪面,距离相机0.1单位内的物体不渲染
// 1000 // far:远裁剪面,距离相机1000单位外的物体不渲染
// );
// const maxZ = 100;
// const pxToWorld = 1;
const fov = 75;
const vFOV = THREE.MathUtils.degToRad(fov);
// const maxZ = height / (2 * Math.tan(vFOV / 2));
const maxZ = 100;
const heightAtZ = 2 * Math.tan(vFOV / 2) * maxZ; // z 处的视口高度(世界单位)
const widthAtZ = heightAtZ * (width / height); // z 处的视口宽度(世界单位,基于宽高比)
const pxToWorld = widthAtZ / width; // 1px 对应的世界单位
const camera = new THREE.PerspectiveCamera(
fov, // fov:视场角,垂直方向的可视角度(单位:度)
width / height, // aspect:宽高比,通常为容器的宽/高
0.1, // near:近裁剪面
1000 // far:远裁剪面
);
// 设置相机在三维空间中的位置(X, Y, Z坐标)
camera.position.set(0, 0, maxZ);
// 设置相机朝向场景的中心点(原点),确保相机正对场景
camera.lookAt(0, 0, 0);
// 渲染器设置
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
container.appendChild(renderer.domElement);
// 光源设置
const ambientLight = new THREE.AmbientLight('#ffffff', 0.6);
scene.add(ambientLight);
const directionalLight1 = new THREE.DirectionalLight('#ffffff', 0.8);
directionalLight1.position.set(10 * pxToWorld, 10 * pxToWorld, 5 * pxToWorld);
scene.add(directionalLight1);
const directionalLight2 = new THREE.DirectionalLight('#ffffff', 0.4);
directionalLight2.position.set(-10 * pxToWorld, -10 * pxToWorld, -5 * pxToWorld);
scene.add(directionalLight2);
// 添加图片到场景
// 创建纹理加载器
const textureLoader = new THREE.TextureLoader();
try {
const texture = await textureLoader.loadAsync('https://picsum.photos/800/600');
// 创建平面几何体(矩形)
// 参数:宽度、高度、宽度分段数、高度分段数
const planeGeometry = new THREE.PlaneGeometry(400 * pxToWorld, 300 * pxToWorld);
// 创建材质(使用加载的纹理)
const planeMaterial = new THREE.MeshPhongMaterial({
map: texture, // 应用纹理
specular: '#ffffff', // 高光反射颜色:白色
shininess: 100 // 高光强度:0-100,值越大高光点越小越亮
});
// 创建网格(几何体+材质)
const imagePlane = new THREE.Mesh(planeGeometry, planeMaterial);
// 设置图片位置(场景中心)
imagePlane.position.set(0 * pxToWorld, 0 * pxToWorld, 0 * pxToWorld);
// 添加到场景
scene.add(imagePlane);
renderer.render(scene, camera);
} catch (error) {
console.error('图片加载失败', error);
renderer.render(scene, camera);
}
});
</script>
<style lang="scss" scoped>
#container {
width: 100vw;
height: 100vh;
}
</style>

四、动画
1.使用renderer.setAnimationLoop添加动画
<template>
<button @click="onStartAnimation">开始动画</button>
<div id="container" ref="threeRef"></div>
</template>
<script setup>
import * as THREE from 'three';
import { ref, onMounted } from 'vue';
const threeRef = ref(null);
let isAnimating = ref(false); // 动画状态
const onStartAnimation = ()=>{
isAnimating.value = true;
};
onMounted(async () => {
const container = threeRef.value;
if (!container) return;
const { offsetWidth: width, offsetHeight: height } = container;
// 场景设置
const scene = new THREE.Scene();
scene.background = new THREE.Color('#ffffff');
// 相机设置
// const halfWidth = width / 2;
// const halfHeight = height / 2;
// const camera = new THREE.OrthographicCamera(
// -halfWidth, // left:左裁剪平面(基于宽高比调整,确保不失真)
// halfWidth, // right:右裁剪平面(与左边界对称)
// halfHeight, // top:上裁剪平面(从中心向上)
// -halfHeight, // bottom:下裁剪平面(从中心向下,与上边界对称)
// 0.1, // near:近裁剪面,距离相机0.1单位内的物体不渲染
// 1000 // far:远裁剪面,距离相机1000单位外的物体不渲染
// );
// const maxZ = 100;
const fov = 75;
const vFOV = THREE.MathUtils.degToRad(fov);
const maxZ = height / (2 * Math.tan(vFOV / 2));
const camera = new THREE.PerspectiveCamera(
fov, // fov:视场角,垂直方向的可视角度(单位:度)
width / height, // aspect:宽高比,通常为容器的宽/高
0.1, // near:近裁剪面
1000 // far:远裁剪面
);
// 设置相机在三维空间中的位置(X, Y, Z坐标)
camera.position.set(0, 0, maxZ);
// 设置相机朝向场景的中心点(原点),确保相机正对场景
camera.lookAt(0, 0, 0);
// 渲染器设置
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
container.appendChild(renderer.domElement);
// 光源设置
const ambientLight = new THREE.AmbientLight('#ffffff', 0.6);
scene.add(ambientLight);
const directionalLight1 = new THREE.DirectionalLight('#ffffff', 0.8);
directionalLight1.position.set(10, 10, 5);
scene.add(directionalLight1);
const directionalLight2 = new THREE.DirectionalLight('#ffffff', 0.4);
directionalLight2.position.set(-10, -10, -5);
scene.add(directionalLight2);
const cubeGeometry = new THREE.BoxGeometry(50, 50, 50);
const cubeMaterial = new THREE.MeshBasicMaterial({ color: '#ff4444' });
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
cubeMaterial.wireframe = true;
cube.position.set(0, (-height + 50) / 2 + 80, 0); //顶部区域上边距20、左边距20
scene.add(cube);
const startPoint = new THREE.Vector3(0, (-height + 50) / 2 + 80, 0); // 起点:下方
const endPoint = new THREE.Vector3(0, height / 2 - 60, -maxZ); // 终点:上方
let progress = 0; // 动画进度(0-1之间)
let totalDuration = 2; // 总动画时长(秒)
let startTime = null; // 动画开始时间
let accumulatedTime = 0; // 累计动画时间
let hasRemoved = false; // 防止重复移除的标记
renderer.setAnimationLoop((timestamp)=>{
if (isAnimating.value && progress < 1 && !hasRemoved) {
// 初始化开始时间
if (!startTime) {
startTime = timestamp;
}
// 计算累计动画时间(秒)
accumulatedTime = (timestamp - startTime) / 1000;
// 计算进度(当前时间/总时长)
progress = Math.min(accumulatedTime / totalDuration, 1); // 限制最大进度为1
const currentPosition = new THREE.Vector3().lerpVectors(
startPoint,
endPoint,
progress
);
cube.position.copy(currentPosition);
// 当进度达到1(到达终点)且需要移除时,执行移除操作
if (progress === 1) {
scene.remove(cube);
hasRemoved = true; // 标记已移除,防止重复执行
}
}
renderer.render(scene, camera);
});
});
</script>
<style lang="scss" scoped>
#container {
width: 100vw;
height: 100vh;
}
</style>

2.使用requestAnimationFrame添加动画
<template>
<button @click="onStartAnimation">开始动画</button>
<div id="container" ref="threeRef"></div>
</template>
<script setup>
import * as THREE from 'three';
import { ref, onMounted } from 'vue';
const threeRef = ref(null);
let mixer = null;
let animationClip = null;
let scene,cube;
const onStartAnimation = () => {
if (mixer && animationClip) {
const action = mixer.clipAction(animationClip);
action.loop = THREE.LoopOnce;
action.clampWhenFinished = true;
action.play();
action.onFinish = () => {
scene.remove(cube);
};
}
};
onMounted(async () => {
const container = threeRef.value;
if (!container) return;
const { offsetWidth: width, offsetHeight: height } = container;
// 场景设置
scene = new THREE.Scene();
scene.background = new THREE.Color('#ffffff');
// 相机设置
// const halfWidth = width / 2;
// const halfHeight = height / 2;
// const camera = new THREE.OrthographicCamera(
// -halfWidth, // left:左裁剪平面(基于宽高比调整,确保不失真)
// halfWidth, // right:右裁剪平面(与左边界对称)
// halfHeight, // top:上裁剪平面(从中心向上)
// -halfHeight, // bottom:下裁剪平面(从中心向下,与上边界对称)
// 0.1, // near:近裁剪面,距离相机0.1单位内的物体不渲染
// 1000 // far:远裁剪面,距离相机1000单位外的物体不渲染
// );
// const maxZ = 100;
const fov = 75;
const vFOV = THREE.MathUtils.degToRad(fov);
const maxZ = height / (2 * Math.tan(vFOV / 2));
const camera = new THREE.PerspectiveCamera(
fov, // fov:视场角,垂直方向的可视角度(单位:度)
width / height, // aspect:宽高比,通常为容器的宽/高
0.1, // near:近裁剪面
1000 // far:远裁剪面
);
// 设置相机在三维空间中的位置(X, Y, Z坐标)
camera.position.set(0, 0, maxZ);
// 设置相机朝向场景的中心点(原点),确保相机正对场景
camera.lookAt(0, 0, 0);
// 渲染器设置
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
container.appendChild(renderer.domElement);
// 光源设置
const ambientLight = new THREE.AmbientLight('#ffffff', 0.6);
scene.add(ambientLight);
const directionalLight1 = new THREE.DirectionalLight('#ffffff', 0.8);
directionalLight1.position.set(10, 10, 5);
scene.add(directionalLight1);
const directionalLight2 = new THREE.DirectionalLight('#ffffff', 0.4);
directionalLight2.position.set(-10, -10, -5);
scene.add(directionalLight2);
const cubeGeometry = new THREE.BoxGeometry(50, 50, 50);
const cubeMaterial = new THREE.MeshBasicMaterial({ color: '#ff4444' });
cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
cubeMaterial.wireframe = true;
cube.position.set(0, (-height + 50) / 2 + 80, 0); //顶部区域上边距20、左边距20
scene.add(cube);
const cubeGroup = new THREE.AnimationObjectGroup();
cubeGroup.add(cube);
const startPoint = new THREE.Vector3(0, (-height + 50) / 2 + 80, 0); // 起点:下方
const endPoint = new THREE.Vector3(0, height / 2 - 60, -maxZ); // 终点:上方
let totalDuration = 2; // 总动画时长(秒)
const positionTrack = new THREE.VectorKeyframeTrack(
'.position', // 动画属性路径
[0, totalDuration], // 关键帧时间点(秒)
[
startPoint.x, startPoint.y, startPoint.z, // 起始位置
endPoint.x, endPoint.y, startPoint.z// 结束位置
]
);
const scaleTrack = new THREE.VectorKeyframeTrack(
'.scale', // 属性路径:对应 cube.scale
[0, totalDuration], // 时间点:与位置动画同步
[
1, 1, 1, // 初始缩放(1倍)
0, 0, 0 // 结束缩放(2倍)
]
);
// 创建动画片段
animationClip = new THREE.AnimationClip(
'moveAndRemove', // 动画名称
totalDuration, // 持续时间
[positionTrack, scaleTrack] // 关键帧轨道数组
);
// 创建动画混合器
mixer = new THREE.AnimationMixer(cubeGroup);
let lastTimestamp = 0;
function animate(timestamp) {
if (lastTimestamp === 0) lastTimestamp = timestamp;
const deltaTime = (timestamp - lastTimestamp) / 1000; // 转换为秒
mixer.update(deltaTime);
lastTimestamp = timestamp;
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
});
</script>
<style lang="scss" scoped>
#container {
width: 100vw;
height: 100vh;
}
</style>
3.使用动画API创建动画
<template>
<button @click="onStartAnimation">开始动画</button>
<div id="container" ref="threeRef"></div>
</template>
<script setup>
import * as THREE from 'three';
import { ref, onMounted } from 'vue';
const threeRef = ref(null);
let mixer = null;
let animationClip = null;
let scene,cube;
const onStartAnimation = () => {
if (mixer && animationClip) {
const action = mixer.clipAction(animationClip);
action.loop = THREE.LoopOnce;
action.clampWhenFinished = true;
action.play();
action.onFinish = () => {
scene.remove(cube);
};
}
};
onMounted(async () => {
const container = threeRef.value;
if (!container) return;
const { offsetWidth: width, offsetHeight: height } = container;
// 场景设置
scene = new THREE.Scene();
scene.background = new THREE.Color('#ffffff');
// 相机设置
// const halfWidth = width / 2;
// const halfHeight = height / 2;
// const camera = new THREE.OrthographicCamera(
// -halfWidth, // left:左裁剪平面(基于宽高比调整,确保不失真)
// halfWidth, // right:右裁剪平面(与左边界对称)
// halfHeight, // top:上裁剪平面(从中心向上)
// -halfHeight, // bottom:下裁剪平面(从中心向下,与上边界对称)
// 0.1, // near:近裁剪面,距离相机0.1单位内的物体不渲染
// 1000 // far:远裁剪面,距离相机1000单位外的物体不渲染
// );
// const maxZ = 100;
const fov = 75;
const vFOV = THREE.MathUtils.degToRad(fov);
const maxZ = height / (2 * Math.tan(vFOV / 2));
const camera = new THREE.PerspectiveCamera(
fov, // fov:视场角,垂直方向的可视角度(单位:度)
width / height, // aspect:宽高比,通常为容器的宽/高
0.1, // near:近裁剪面
1000 // far:远裁剪面
);
// 设置相机在三维空间中的位置(X, Y, Z坐标)
camera.position.set(0, 0, maxZ);
// 设置相机朝向场景的中心点(原点),确保相机正对场景
camera.lookAt(0, 0, 0);
// 渲染器设置
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
container.appendChild(renderer.domElement);
// 光源设置
const ambientLight = new THREE.AmbientLight('#ffffff', 0.6);
scene.add(ambientLight);
const directionalLight1 = new THREE.DirectionalLight('#ffffff', 0.8);
directionalLight1.position.set(10, 10, 5);
scene.add(directionalLight1);
const directionalLight2 = new THREE.DirectionalLight('#ffffff', 0.4);
directionalLight2.position.set(-10, -10, -5);
scene.add(directionalLight2);
const cubeGeometry = new THREE.BoxGeometry(50, 50, 50);
const cubeMaterial = new THREE.MeshBasicMaterial({ color: '#ff4444' });
cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
cubeMaterial.wireframe = true;
cube.position.set(0, (-height + 50) / 2 + 80, 0); //顶部区域上边距20、左边距20
scene.add(cube);
const cubeGroup = new THREE.AnimationObjectGroup();
cubeGroup.add(cube);
const startPoint = new THREE.Vector3(0, (-height + 50) / 2 + 80, 0); // 起点:下方
const endPoint = new THREE.Vector3(0, height / 2 - 60, -maxZ); // 终点:上方
let totalDuration = 2; // 总动画时长(秒)
const positionTrack = new THREE.VectorKeyframeTrack(
'.position', // 动画属性路径
[0, totalDuration], // 关键帧时间点(秒)
[
startPoint.x, startPoint.y, startPoint.z, // 起始位置
endPoint.x, endPoint.y, startPoint.z// 结束位置
]
);
const scaleTrack = new THREE.VectorKeyframeTrack(
'.scale', // 属性路径:对应 cube.scale
[0, totalDuration], // 时间点:与位置动画同步
[
1, 1, 1, // 初始缩放(1倍)
0, 0, 0 // 结束缩放(2倍)
]
);
// 创建动画片段
animationClip = new THREE.AnimationClip(
'moveAndRemove', // 动画名称
totalDuration, // 持续时间
[positionTrack, scaleTrack] // 关键帧轨道数组
);
// 创建动画混合器
mixer = new THREE.AnimationMixer(cubeGroup);
function animate(timestamp) {
mixer.update(0.016); // 固定时间步长更新(约60fps)
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
});
</script>
<style lang="scss" scoped>
#container {
width: 100vw;
height: 100vh;
}
</style>
五、批量加载资源、音视频播放、事件绑定
1.批量加载资源:如游戏进入场景时批量加载资源
<template>
<button @click="onStartLoader">开始加载资源</button>
<div class="loader-container" v-if="showLoader">
<div class="loading-wrapper">
<!-- 加载进度条 -->
<div class="progress-container">
<div class="progress-bar"
:style="{ width: progress + '%' }"></div>
</div>
<!-- 进度百分比显示 -->
<div class="progress-text">{{ progress }}%</div>
<!-- 加载状态提示 -->
<div class="status-text">{{ statusText }}</div>
</div>
</div>
<div id="container" v-else></div>
</template>
<script setup>
import * as THREE from 'three';
import { ref, onMounted } from 'vue';
import { FontLoader } from 'three/addons/loaders/FontLoader.js';
import { FileLoader } from 'three';
// 状态管理
const showLoader = ref(false);
const progress = ref(0);
const statusText = ref('准备加载资源...');
const loadError = ref(false);
const threeLoader = (options) => {
if (!options.resources) {
console.error('缺少加载的资源对象');
return;
}
const state = {
totalResources: 0,
processedResources: 0,
resources: {
images: {},
fonts: {},
videos: {},
audios: {}
},
errors: []
};
for (const key in options.resources) {
state.totalResources += options.resources[key].length;
}
const manager = new THREE.LoadingManager();
function updateProgress() {
const currentProgress = state.totalResources > 0
? Math.min(Math.round((state.processedResources / state.totalResources) * 100), 100)
: 0;
// 更新进度状态
progress.value = currentProgress;
// 更新状态文本
if (currentProgress < 100) {
statusText.value = `正在加载资源...(${state.processedResources}/${state.totalResources})`;
} else {
statusText.value = '资源加载完成,准备初始化场景...';
}
if (options.onProgress) options.onProgress(currentProgress);
if (currentProgress === 100 && options.onLoad) {
// 加载完成后短暂延迟以展示完成状态
setTimeout(() => {
options.onLoad(state.resources);
}, 500);
}
}
const textureLoader = new THREE.TextureLoader(manager);
const fontLoader = new FontLoader(manager);
const audioLoader = new THREE.AudioLoader();
const resourceLoadError = (type, url) => {
const fileName = url.split('/').pop();
const errorMsg = {
images: '图片',
fonts: '字体',
videos: '视频',
audios: '音频'
};
console.error(`${errorMsg[type]}加载失败: ${decodeURIComponent(url)}`);
state.errors.push(fileName);
statusText.value = `加载失败: ${errorMsg[type]} ${fileName}`;
loadError.value = true;
if (type === 'videos') updateProgress();
manager.itemEnd(url);
};
const handleResourceLoad = async (type, loader, info) => {
try {
statusText.value = `正在加载: ${info.id}`;
state.resources[type][info.id] = await loader.loadAsync(info.url);
} catch (error) {
resourceLoadError(type, info.url);
} finally {
state.processedResources++;
updateProgress();
}
};
// 加载图片资源
options.resources.images?.forEach(async (item) => {
await handleResourceLoad('images', textureLoader, item);
});
// 加载字体资源
options.resources.fonts?.forEach(async (item) => {
fontLoader.load = function (url, onLoad, onProgress, onError) {
const scope = this;
const loader = new FileLoader(this.manager);
loader.setPath(this.path);
loader.setRequestHeader(this.requestHeader);
loader.setWithCredentials(this.withCredentials);
loader.load(url, function (text) {
try {
const font = scope.parse(JSON.parse(text));
if (onLoad) onLoad(font);
} catch (e) {
onError();
}
}, onProgress, onError);
};
await handleResourceLoad('fonts', fontLoader, item);
});
// 加载音频资源
options.resources.audios?.forEach(async (item) => {
await handleResourceLoad('audios', audioLoader, item);
});
// 加载视频资源
options.resources.videos?.forEach((item) => {
manager.itemStart(item.url);
const video = document.createElement('video');
video.src = item.url;
video.crossOrigin = 'anonymous';
video.preload = 'auto';
video.addEventListener('loadeddata', () => {
const texture = new THREE.VideoTexture(video);
texture.minFilter = THREE.LinearFilter;
texture.magFilter = THREE.LinearFilter;
texture.format = THREE.RGBFormat;
state.resources.videos[item.id] = texture;
state.processedResources++;
manager.itemEnd(item.url);
updateProgress();
});
video.addEventListener('error', () => {
resourceLoadError('videos', item.url);
});
});
};
// 示例资源配置 - 替换为实际资源
const resources = {
images: [
{ id: 'image1', url: 'https://picsum.photos/seed/img1/400/300' },
{ id: 'image2', url: 'https://picsum.photos/seed/img2/400/300' },
{ id: 'sprite1', url: 'https://picsum.photos/seed/sprite1/200/200' }
],
fonts: [
{ id: 'helvetiker', url: 'https://threejs.org/examples/fonts/helvetiker_regular.typeface.json' }
],
videos: [
{ id: 'video1', url: 'https://threejs.org/examples/textures/sintel.mp4' }
],
audios: [
{ id: 'audio1', url: 'https://threejs.org/examples/sounds/ambient.ogg' }
]
};
const onStartLoader = () => {
showLoader.value = true;
threeLoader({
resources,
onProgress: (currentProgress) => {
// 可以在这里添加额外的进度处理逻辑
},
onLoad: (loadedResources) => {
// 资源加载完成,隐藏加载动画
showLoader.value = false;
console.log('所有资源加载完成', loadedResources);
// 在这里初始化你的Three.js场景
initScene(loadedResources);
}
});
};
// 初始化Three.js场景
function initScene(resources) {
// 这里添加你的场景初始化代码
const scene = new THREE.Scene();
// ...
}
</script>
<style lang="scss" scoped>
.loader-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: #1a1a1a;
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
transition: opacity 0.5s ease;
}
.loading-wrapper {
width: 80%;
max-width: 600px;
color: white;
text-align: center;
}
.progress-container {
height: 8px;
background: rgba(255, 255, 255, 0.2);
border-radius: 4px;
overflow: hidden;
margin-bottom: 16px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3) inset;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);
transition: width 0.3s ease;
border-radius: 4px;
}
.progress-text {
font-size: 24px;
font-weight: bold;
margin-bottom: 8px;
color: #4facfe;
}
.status-text {
font-size: 14px;
color: rgba(255, 255, 255, 0.8);
min-height: 20px;
}
#container {
width: 100vw;
height: 100vh;
}
</style>

2.音频播放
<template>
<button @click="onStartPlay">开始播放音乐</button>
<div id="container" ref="threeRef"></div>
</template>
<script setup>
import * as THREE from 'three';
import { ref, onMounted } from 'vue';
const threeRef = ref(null);
let sound;
const onStartPlay = ()=>{
sound.play();
};
onMounted(async () => {
const container = threeRef.value;
if (!container) return;
const { offsetWidth: width, offsetHeight: height } = container;
// 场景设置
const scene = new THREE.Scene();
scene.background = new THREE.Color('#ffffff');
// 相机设置
const halfWidth = width / 2;
const halfHeight = height / 2;
const camera = new THREE.OrthographicCamera(
-halfWidth,
halfWidth,
halfHeight,
-halfHeight,
0.1,
1000
);
camera.position.set(0, 0, 100);
camera.lookAt(0, 0, 0);
// 渲染器设置
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
container.appendChild(renderer.domElement);
// 光源设置
const ambientLight = new THREE.AmbientLight('#ffffff', 0.6);
scene.add(ambientLight);
const directionalLight1 = new THREE.DirectionalLight('#ffffff', 0.8);
directionalLight1.position.set(10, 10, 5);
scene.add(directionalLight1);
const directionalLight2 = new THREE.DirectionalLight('#ffffff', 0.4);
directionalLight2.position.set(-10, -10, -5);
scene.add(directionalLight2);
// 添加音频播放功能
const listener = new THREE.AudioListener();
camera.add(listener);
sound = new THREE.Audio(listener);
const audioLoader = new THREE.AudioLoader();
// 这里替换为你自己的MP3音频文件路径
const musicUrl = new URL('horse.mp3', import.meta.url).href;
audioLoader.load(musicUrl, (buffer) => {
sound.setBuffer(buffer);
sound.setLoop(false);
sound.setVolume(0.5);
});
renderer.render(scene, camera);
});
</script>
<style lang="scss" scoped>
#container {
width: 100%;
height: 100%;
}
</style>
3.视频播放
<template>
<div class="container">
<div id="threeContainer" ref="threeRef"></div>
<div class="controls">
<button class="play-btn" @click="togglePlay">
{{ isPlaying ? '暂停' : '播放' }}
</button>
</div>
</div>
</template>
<script setup>
import * as THREE from 'three';
import { ref, onMounted, onUnmounted } from 'vue';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const threeRef = ref(null);
const isPlaying = ref(false);
let scene, camera, renderer, controls;
let video, videoTexture, videoMesh;
onMounted(async () => {
const container = threeRef.value;
if (!container) return;
const { offsetWidth: width, offsetHeight: height } = container;
// 场景设置
scene = new THREE.Scene();
scene.background = new THREE.Color('#1a1a1a');
// 相机设置
camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
camera.position.z = 5;
// 渲染器设置
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
container.appendChild(renderer.domElement);
// 控制器
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// 创建视频元素
createVideoElement();
// 窗口大小调整
window.addEventListener('resize', onWindowResize);
// 动画循环
function animate() {
requestAnimationFrame(animate);
controls.update();
// 视频纹理需要每帧更新
if (videoTexture) {
videoTexture.needsUpdate = true;
}
renderer.render(scene, camera);
}
animate();
});
// 创建视频元素和纹理
function createVideoElement() {
// 创建HTML视频元素
video = document.createElement('video');
// 这里替换为你自己的视频频文件路径
video.src = new URL('video.mp4', import.meta.url).href;
video.crossOrigin = 'anonymous'; // 允许跨域
video.loop = true; // 循环播放
video.muted = true; // 静音(浏览器通常禁止自动播放带声音的视频)
video.setAttribute('playsinline', 'true'); // 移动端内联播放
// 创建视频纹理
videoTexture = new THREE.VideoTexture(video);
videoTexture.minFilter = THREE.LinearFilter;
videoTexture.magFilter = THREE.LinearFilter;
videoTexture.format = THREE.RGBFormat;
// 创建平面几何体(视频播放平面)
const aspect = 16 / 9; // 视频宽高比
const width = 4;
const height = width / aspect;
const geometry = new THREE.PlaneGeometry(width, height);
// 创建材质(使用视频纹理)
const material = new THREE.MeshBasicMaterial({ map: videoTexture });
// 创建网格并添加到场景
videoMesh = new THREE.Mesh(geometry, material);
scene.add(videoMesh);
}
// 播放/暂停控制
function togglePlay() {
if (!video) return;
if (isPlaying.value) {
video.pause();
} else {
video.play().then(() => {
console.log('视频开始播放');
}).catch((error) => {
console.error('播放失败:', error);
// 处理播放失败(通常是浏览器自动播放政策限制)
alert('请点击页面后再尝试播放(浏览器政策限制)');
});
}
isPlaying.value = !isPlaying.value;
}
// 窗口大小调整
function onWindowResize() {
const container = threeRef.value;
if (!container) return;
const { offsetWidth: width, offsetHeight: height } = container;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
}
// 组件卸载时清理
onUnmounted(() => {
if (video) {
video.pause();
video.remove();
}
window.removeEventListener('resize', onWindowResize);
});
</script>
<style lang="scss" scoped>
.container {
width: 100vw;
height: 100vh;
position: relative;
overflow: hidden;
}
#threeContainer {
width: 100%;
height: 100%;
}
.controls {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
padding: 10px 20px;
background: rgba(0, 0, 0, 0.7);
border-radius: 30px;
}
.play-btn {
padding: 8px 16px;
border: none;
border-radius: 20px;
background: #4285f4;
color: white;
cursor: pointer;
transition: background 0.3s;
&:hover {
background: #3367d6;
}
}
</style>

4.事件绑定
<template>
<div id="container" ref="threeRef"></div>
</template>
<script setup>
import * as THREE from 'three';
import { ref, onMounted, onUnmounted } from 'vue';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const threeRef = ref(null);
let scene, camera, renderer, controls;
let raycaster, mouse;
let cubes = []; // 存储所有可交互物体
let selectedObject = null; // 当前选中的物体
onMounted(async () => {
const container = threeRef.value;
if (!container) return;
const { offsetWidth: width, offsetHeight: height } = container;
// 场景设置
scene = new THREE.Scene();
scene.background = new THREE.Color('#f0f0f0');
// 透视相机设置
camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
camera.position.set(0, 0, 10);
camera.lookAt(0, 0, 0);
// 渲染器设置
renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
container.appendChild(renderer.domElement);
// 光源设置
const ambientLight = new THREE.AmbientLight('#ffffff', 0.6);
scene.add(ambientLight);
const directionalLight1 = new THREE.DirectionalLight('#ffffff', 0.8);
directionalLight1.position.set(10, 10, 5);
scene.add(directionalLight1);
const directionalLight2 = new THREE.DirectionalLight('#ffffff', 0.4);
directionalLight2.position.set(-10, -10, -5);
scene.add(directionalLight2);
// 控制器
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// 射线投射器(用于物体交互检测)
raycaster = new THREE.Raycaster();
mouse = new THREE.Vector2();
// 创建可交互物体
createInteractiveObjects();
// 绑定事件
bindEvents(container);
// 动画循环
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
// 窗口大小调整
window.addEventListener('resize', onWindowResize);
});
// 创建交互物体
function createInteractiveObjects() {
// 创建立方体
const geometries = [
new THREE.BoxGeometry(1, 1, 1), // 立方体
new THREE.SphereGeometry(0.7, 32, 32), // 球体
new THREE.ConeGeometry(0.7, 2, 32), // 锥体
new THREE.CylinderGeometry(0.5, 0.5, 2, 32) // 圆柱体
];
const colors = ['#ff0000', '#00ff00', '#0000ff', '#ffff00'];
const positions = [[-3, 0, 0], [0, 0, 0], [3, 0, 0], [0, 3, 0]];
geometries.forEach((geometry, index) => {
const material = new THREE.MeshStandardMaterial({
color: colors[index],
transparent: true,
opacity: 0.9
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(...positions[index]);
mesh.userData.name = ['立方体', '球体', '锥体', '圆柱体'][index];
mesh.userData.isInteractive = true; // 标记为可交互
scene.add(mesh);
cubes.push(mesh);
});
}
// 绑定事件
function bindEvents(container) {
// 鼠标移动事件(用于悬停检测)
container.addEventListener('mousemove', onMouseMove);
// 鼠标点击事件
container.addEventListener('click', onMouseClick);
// 鼠标双击事件
container.addEventListener('dblclick', onMouseDblClick);
// 鼠标按下事件(用于拖拽开始)
container.addEventListener('mousedown', onMouseDown);
// 鼠标释放事件(用于拖拽结束)
window.addEventListener('mouseup', onMouseUp);
// 鼠标离开容器事件
container.addEventListener('mouseleave', onMouseLeave);
}
// 鼠标移动处理
function onMouseMove(event) {
// 计算鼠标在标准化设备坐标中的位置 (-1 到 1)
const rect = event.target.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(cubes);
// 重置所有物体状态
cubes.forEach((cube) => {
cube.material.color.set(cube.userData.originalColor || cube.material.color.getHex());
cube.scale.set(1, 1, 1);
});
// 处理悬停状态
if (intersects.length > 0) {
const hoveredObject = intersects[0].object;
// 存储原始颜色(如果尚未存储)
if (!hoveredObject.userData.originalColor) {
hoveredObject.userData.originalColor = hoveredObject.material.color.getHex();
}
// 悬停效果
hoveredObject.material.color.set('#ffffff');
hoveredObject.scale.set(1.1, 1.1, 1.1);
document.body.style.cursor = 'pointer';
} else {
document.body.style.cursor = 'default';
}
}
// 鼠标点击处理
function onMouseClick(event) {
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(cubes);
if (intersects.length > 0) {
const clickedObject = intersects[0].object;
console.log(`点击了: ${clickedObject.userData.name}`);
// 选中效果
if (selectedObject) {
selectedObject.material.opacity = 0.9;
}
selectedObject = clickedObject;
selectedObject.material.opacity = 1;
}
}
// 鼠标双击处理
function onMouseDblClick(event) {
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(cubes);
if (intersects.length > 0) {
const doubleClickedObject = intersects[0].object;
console.log(`双击了: ${doubleClickedObject.userData.name}`);
// 双击旋转效果
doubleClickedObject.userData.rotating = !doubleClickedObject.userData.rotating;
animateRotation();
}
}
// 拖拽相关变量
let isDragging = false;
let draggedObject = null;
// 鼠标按下处理(拖拽开始)
function onMouseDown(event) {
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(cubes);
if (intersects.length > 0) {
isDragging = true;
draggedObject = intersects[0].object;
console.log(`开始拖拽: ${draggedObject.userData.name}`);
event.preventDefault();
}
}
// 鼠标释放处理(拖拽结束)
function onMouseUp() {
if (isDragging && draggedObject) {
console.log(`结束拖拽: ${draggedObject.userData.name}`);
isDragging = false;
draggedObject = null;
}
}
// 鼠标离开容器处理
function onMouseLeave() {
if (isDragging && draggedObject) {
console.log(`拖拽离开: ${draggedObject.userData.name}`);
isDragging = false;
draggedObject = null;
}
}
// 窗口大小调整
function onWindowResize() {
const container = threeRef.value;
const { offsetWidth: width, offsetHeight: height } = container;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
}
// 旋转动画
function animateRotation() {
function rotate() {
cubes.forEach((cube) => {
if (cube.userData.rotating) {
cube.rotation.x += 0.02;
cube.rotation.y += 0.02;
}
});
requestAnimationFrame(rotate);
}
rotate();
}
// 清理事件
onUnmounted(() => {
const container = threeRef.value;
if (container) {
container.removeEventListener('mousemove', onMouseMove);
container.removeEventListener('click', onMouseClick);
container.removeEventListener('dblclick', onMouseDblClick);
container.removeEventListener('mousedown', onMouseDown);
container.removeEventListener('mouseleave', onMouseLeave);
}
window.removeEventListener('mouseup', onMouseUp);
window.removeEventListener('resize', onWindowResize);
});
</script>
<style lang="scss" scoped>
#container {
width: 100%;
height: 100vh;
overflow: hidden;
}
</style>



