教学视频参考:B站------Three.js教学
教学链接:Three.js中文网 老陈打码 | 麒跃科技
一.什么是Three.js?
Three.js 是一个基于 JavaScript 的 3D 图形库,用于在网页浏览器中创建和渲染交互式 3D 内容。它基于 WebGL(一种浏览器原生支持的 3D 图形 API),但通过更简单的抽象层让开发者无需直接编写复杂的 WebGL 代码即可构建 3D 场景。
下面是官网链接:基础 - three.js manual、three.js docs

二.入门 ------ Vue3编写一个可旋转的正方体页面
在App.vue内编写代码:
首先初始化基础环境:
javascript
// 1.1 创建场景(容器)
const scene = new THREE.Scene();
// 1.2 创建透视相机
const camera = new THREE.PerspectiveCamera(
75, // 视野角度
window.innerWidth / window.innerHeight, // 宽高比
0.1, // 近裁剪面
1000 // 远裁剪面
);
// 1.3 创建WebGL渲染器(启用抗锯齿)
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight); // 设置渲染尺寸
document.body.appendChild(renderer.domElement); // 将画布添加到页面
当调用
new THREE.WebGLRenderer()
时,Three.js会自动创建一个<canvas>
元素,以至于我们通过renderer.domElement
可以获取这个canvas,并通过
document.body.appendChild(renderer.domElement)
直接将canvas插入body。(这就是不写<canvas>也可以渲染的原因)
随后创建3D正方体:
参数 | 类型 | 作用 |
---|---|---|
geometry |
THREE.BufferGeometry | 定义物体的形状(如立方体、球体等) |
material |
THREE.Material | 定义物体的外观(颜色、纹理、反光等) |
javascript
// 2.1 创建立方体几何体
const geometry = new THREE.BoxGeometry(1, 1, 1); // 1x1x1的立方体
// 2.2 创建绿色基础材质
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
// 2.3 组合几何体和材质为网格对象
const cube = new THREE.Mesh(geometry, material);
// 2.4 将立方体添加到场景
scene.add(cube);
之后设置相机位置:
这里是直接设置成在z轴并对准原点
javascript
camera.position.z = 5; // 相机沿z轴后退5个单位
camera.lookAt(0, 0, 0); // 相机对准场景中心
最后使用递归animate()方法不断调用来让正方体展示并旋转:
javascript
function animate() {
requestAnimationFrame(animate); // 循环调用自身
cube.rotation.x += 0.01; // x轴旋转
cube.rotation.y += 0.01; // y轴旋转
renderer.render(scene, camera); // 渲染场景
}
animate(); // 启动动画
下面是完整代码:
javascript
<script setup>
import * as THREE from 'three'
// 创建场景
const scene = new THREE.Scene();
// 创建相机
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); // 视野角度, 宽高比, 最近可见距离, 最远可见距离
// 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true }); // 抗锯齿
renderer.setSize(window.innerWidth, window.innerHeight); // 设置渲染器大小
document.body.appendChild(renderer.domElement); // 将渲染器添加到页面中
// 创建几何体
const geometry = new THREE.BoxGeometry(1, 1, 1); // 创建一个立方体几何体
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); // 创建一个绿色的材质
const cube = new THREE.Mesh(geometry, material); // 创建一个网格对象
scene.add(cube); // 将网格对象添加到场景中
// 设置相机位置
camera.position.z = 5; // 设置相机位置
camera.lookAt(0, 0, 0); // 设置相机朝向原点
// 渲染循环
function animate() {
requestAnimationFrame(animate); // 请求下一帧动画
cube.rotation.x += 0.01; // 旋转立方体
cube.rotation.y += 0.01; // 旋转立方体
// 渲染
renderer.render(scene, camera); // 渲染场景和相机
}
animate(); // 开始动画循环
</script>
<template>
<div>
</div>
</template>
<style scoped>
* {
margin: 0;
padding: 0;
}
/* 3D效果都是画在canvas画布上 */
canvas{
display: block;
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
}
</style>

三. 基础操作
1.坐标辅助器与轨道辅助器
坐标辅助器(AxesHelper) 是可视化 3D 坐标系(X/Y/Z 轴),能够帮助开发者快速理解场景的空间方向。
- X轴(红色):水平向右
- Y轴(绿色):垂直向上
- Z轴(蓝色):垂直于屏幕(正向朝外)
javascript
import * as THREE from 'three';
// 创建场景
const scene = new THREE.Scene();
// 添加坐标辅助器(参数:坐标轴长度)
const axesHelper = new THREE.AxesHelper(5); // 5个单位长度
scene.add(axesHelper);

由于我们的相机正对着z轴拍摄,所以z轴只是一个点。在上图可以清晰的看见y轴x轴。
而我们想要用鼠标来改变相机的位置就需要使用轨道控制器:
轨道控制器:
- 允许用户 用鼠标交互控制相机 ,实现:
- 旋转(左键拖动)
- 缩放(滚轮)
- 平移(右键拖动)
- 适用于 调试 3D 场景 或 交互式展示。
javascript
<script setup>
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
// 创建场景
const scene = new THREE.Scene();
// 初始化相机和渲染器
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 创建几何体
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// 设置相机位置
camera.position.z = 5;
camera.lookAt(0, 0, 0);
// 添加坐标辅助器
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
// 创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
// 动画循环
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
// 处理窗口大小变化
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
<template>
<!-- 空模板即可,Three.js会自动管理canvas -->
<div></div>
</template>
<style scoped>
* {
margin: 0;
padding: 0;
}
canvas {
display: block;
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
}
</style>
在这里缩放是我们的相机在不断的变换位置,以至于看到3D正方体不断的被我们拉动位置。
在这里可以设置是否带有阻尼,也就是是否带有惯性:
javascript
controls.enableDamping = true; // 启用阻尼(惯性效果)
controls.dampingFactor = 0.05; // 阻尼系数,越大停的越快
controls.autoRotate = true; // 设置旋转速度
如果我们想要换一个对象监听,可以将轨道控制器 new OrbitControls(camera, renderer.domElement) 使用 new OrbitControls(camera, domElement.body) 来监听,同时要修改CSS:Controls -- three.js docs
javascript
// 创建轨道控制器
const controls = new OrbitControls(camera, domElement.body);
// 样式渲染(不写可能页面看不到)
body {
width: 100vw;
height: 100vh;
}
2.物体位移与父子元素
在 Three.js 中,理解物体位移和父子元素关系是构建复杂场景的基础。
每个 Three.js 物体(Object3D)都有 position
属性,它是一个 Vector3
对象,包含 x、y、z 三个分量:
javascript
const cube = new THREE.Mesh(geometry, material); // 创建一个新的 3D 网格物体(Mesh)
// 设置位置
cube.position.set(1, 2, 3); // x=1, y=2, z=3
// 或者单独设置
cube.position.x = 1;
cube.position.y = 2;
cube.position.z = 3;
// 也可以使用set方法
cube.position.set(1,2,3);
如何让其位移呢?
世界坐标 = 父级世界坐标 + 子级局部坐标
在讲解父子元素前需要了解 ->
什么是局部坐标,什么是世界坐标呢?
相对坐标(局部坐标) | 世界坐标 | |
---|---|---|
定义 | 相对于父级容器的坐标 | 相对于场景原点的绝对坐标 |
表示 | object.position |
通过计算得到 |
影响 | 受父级变换影响 | 不受父级变换影响 |
用途 | 物体在父容器内的布局 | 场景中的绝对定位 |
世界坐标 = 父级世界坐标 + 子级局部坐标
【 存在旋转/缩放时,必须用
getWorldPosition()
计算**】**
【1】相对坐标(局部坐标)
特点:
存储在
object.position
中所有变换操作默认基于局部坐标系
子对象继承父对象的变换
在 Three.js 中,const parent = new THREE.Group();
用于创建一个空容器对象(Group),它是组织和管理 3D 场景中多个物体的核心工具。继承自
THREE.Object3D
,但没有几何体(Geometry)和材质(Material)仅用于逻辑分组,自身不可见,不参与渲染
方法 作用 .add(object1, object2...)
添加子对象 .remove(object)
移除子对象 .clear()
清空所有子对象 .getObjectByName(name)
按名称查找子对象
javascript
const parent = new THREE.Group();
parent.position.set(2, 0, 0);
const child = new THREE.Mesh(geometry, material);
child.position.set(1, 0, 0); // 相对于父级的坐标
parent.add(child);
// 此时child的局部坐标是(1,0,0),世界坐标是(3,0,0)
【2】世界坐标
特点:
- 物体在全局场景中的绝对位置
- 需要计算得到(考虑所有父级变换)
- 常用于碰撞检测、物理计算等
javascript
const worldPosition = new THREE.Vector3();
object.getWorldPosition(worldPosition);
const worldRotation = new THREE.Quaternion();
object.getWorldQuaternion(worldRotation);
const worldScale = new THREE.Vector3();
object.getWorldScale(worldScale);
3.物体的缩放与旋转
在 Three.js 中,缩放(scale)和旋转(rotation)是物体变换(transform)的两个核心操作,它们与位移(position)共同构成了物体的完整空间变换。
Three.js 提供了多种旋转表示方式:(旋转顺序默认为 'XYZ')
rotation
(欧拉角,默认)quaternion
(四元数)
javascript
// 分别绕各轴旋转
cube.rotation.x = Math.PI/4; // 绕X轴旋转45度
cube.rotation.y = Math.PI/2; // 绕Y轴旋转90度
// 使用set方法
cube.rotation.set(Math.PI/4, 0, 0);
旋转与父子关系:
javascript
const parent = new THREE.Group();
parent.rotation.y = Math.PI/2;
const child = new THREE.Mesh(geometry, material);
child.position.set(1, 0, 0);
parent.add(child);
// child会继承parent的旋转,其世界位置会变化
Three.js 的变换顺序是:缩放 → 旋转 → 平移
假如父组件被缩放,那么子组件也会跟着父组件被缩放的倍数进行缩放。
javascript
// 以下两个操作不等价
cube.scale.set(2, 1, 1);
cube.rotation.y = Math.PI/4;
// 与
cube.rotation.y = Math.PI/4;
cube.scale.set(2, 1, 1);
4.画布自适应窗口:
在 Three.js 开发中,实现画布(Canvas)自适应窗口大小是创建响应式 3D 应用的基础。
javascript
// 监听窗口的变化
window.addEventListener('resize', () => {
// 重置渲染器宽高比
renderer.setSize(window.innerWidth, window.innerHeight);
// 重置相机的宽高比
camera.aspect = window.innerWidth / window.innerHeight;
// 更新相机投影矩阵
camera.updateProjectionMatrix();
});
现在注册一个按钮监听点击事件来让其全屏:
javascript
// 监听按钮点击事件
const button = document.createElement('button');
button.innerHTML = '点击全屏';
button.style.position = 'absolute';
button.style.top = '10px';
button.style.left = '10px';
button.style.zIndex = '1000';
button.style.backgroundColor = '#fff';
button.onclick = () => {
// 全屏
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
document.documentElement.requestFullscreen();
}
};
document.body.appendChild(button);
// 监听全屏事件
document.addEventListener('fullscreenchange', () => {
if (document.fullscreenElement) {
button.innerHTML = '退出全屏';
} else {
button.innerHTML = '点击全屏';
}
});
左侧就是渲染的效果。
5.lilGUI
Lil-GUI(原名为 dat.GUI)是一个轻量级的JavaScript库,专门用于创建调试控制面板,特别适合Three.js等WebGL项目的参数调节。
下载依赖:
javascript
npm install lil-gui
导入lilGUI:
javascript
import GUI from 'three/examples/jsm/libs/lil-gui.module.min.js';
我们以实现全屏按钮为例:
javascript
// 监听按钮点击事件
const gui = new GUI();
// 定义事件
const event = {
FullScreen: () => {
document.documentElement.requestFullscreen();
},
ExitFullscreen: () => {
document.exitFullscreen();
},
ChangeColor: () => {
cube.material.color.set(Math.random() * 0xffffff);
},
};
// 添加按钮
gui.add(event, 'FullScreen').name('全屏');
gui.add(event, 'ExitFullscreen').name('退出全屏');
左侧图片就是我们的渲染效果。
还可以使用lilGUI调节立方体的位置:
javascript
// 随机控制立方体位置
gui.add(cube.position, 'x', -5, 5).name('立方体X轴位置');
// 也可以是下面这样
gui.add(cube.position, 'x').min(-5).max(5).step(1).name('立方体X轴位置');

也可以使用folder创建下拉框:
javascript
const folder = gui.addFolder('立方体位置');
folder.add(cube.position, 'x', -5, 5).name('立方体X轴位置');
folder.add(cube.position, 'y', -5, 5).name('立方体Y轴位置');
folder.add(cube.position, 'z', -5, 5).name('立方体Z轴位置');

也可以绑定监听事件:
javascript
const folder = gui.addFolder('立方体位置');
folder.add(cube.position, 'x', -5, 5)
.onChange(() => {
console.log('立方体X轴位置:', cube.position.x);
})
.name('立方体X轴位置');
folder.add(cube.position, 'y', -5, 5).name('立方体Y轴位置');
folder.add(cube.position, 'z', -5, 5).name('立方体Z轴位置');

也可以监听最后停下的事件:
javascript
folder.add(cube.position, 'y', -5, 5).onFinishChange(()=>{
console.log('立方体Y轴位置:', cube.position.y);
}).name('立方体Y轴位置');
也可以使用布尔值设置是否为线框模式:
javascript
const gui = new GUI();
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
gui.add(material, 'wireframe').name('线框模式');

也可以选择颜色:
javascript
// 选择颜色
gui.addColor(material, 'color').name('颜色选择器').onChange((val) => {
cube.material.color.set(val);
console.log('立方体颜色:', material.color.getHexString());
});

四.几何体
几何体是 Three.js 中定义3D物体形状的基础组件。它们由顶点(vertices)、面(faces)、边(edges)等元素构成,决定了物体的基本形状和结构。
BufferGeometry -- three.js docs
1.几何体_顶点_索引
由于一个矩形是由两个三角形构成,所以需要两组顶点数据(2*3=6)构造,下面的代码用来构造一个矩形:
javascript
const geometry = new THREE.BufferGeometry();
// 创建一个简单的矩形. 在这里我们左上和右下顶点被复制了两次。
// 因为在两个三角面片里,这两个顶点都需要被用到。
// 创建顶点数据
const vertices = new Float32Array( [
-1.0, -1.0, 1.0,
1.0, -1.0, 1.0,
1.0, 1.0, 1.0,
1.0, 1.0, 1.0,
-1.0, 1.0, 1.0,
-1.0, -1.0, 1.0
] );
// itemSize = 3 因为每个顶点都是一个三元组。
geometry.setAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) );
const material = new THREE.MeshBasicMaterial( { color: 0xff0000 } );
const mesh = new THREE.Mesh( geometry, material );
使用下面代码查看我们构造的矩形:
javascript
<template>
<div ref="container" class="three-container"></div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const container = ref(null);
onMounted(() => {
// 1. 创建场景、相机和渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
const renderer = new THREE.WebGLRenderer({ antialias: true });
// 2. 设置渲染器大小并添加到DOM
renderer.setSize(window.innerWidth, window.innerHeight);
container.value.appendChild(renderer.domElement);
// 3. 创建几何体和材质(线框模式)
const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array([
-1.0, -1.0, 1.0,
1.0, -1.0, 1.0,
1.0, 1.0, 1.0,
1.0, 1.0, 1.0,
-1.0, 1.0, 1.0,
-1.0, -1.0, 1.0
]);
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
// 使用MeshBasicMaterial并启用线框模式
const material = new THREE.MeshBasicMaterial({
color: 0xff0000,
wireframe: true // 启用线框模式
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 4. 添加坐标轴辅助器(红色-X,绿色-Y,蓝色-Z)
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
// 5. 添加网格辅助器(地面网格)
const gridHelper = new THREE.GridHelper(10, 10);
scene.add(gridHelper);
// 6. 设置相机位置
camera.position.set(3, 3, 5);
camera.lookAt(0, 0, 0);
// 7. 添加轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // 启用阻尼效果
controls.dampingFactor = 0.05;
// 8. 动画循环
const animate = () => {
requestAnimationFrame(animate);
controls.update(); // 更新控制器
renderer.render(scene, camera);
};
animate();
// 9. 窗口大小调整处理
const onWindowResize = () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
};
window.addEventListener('resize', onWindowResize);
// 10. 组件卸载时清理
onUnmounted(() => {
window.removeEventListener('resize', onWindowResize);
container.value?.removeChild(renderer.domElement);
geometry.dispose();
material.dispose();
controls.dispose();
});
});
</script>
<style scoped>
.three-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
margin: 0;
padding: 0;
}
</style>

也可以使用索引来索引顶点位置进行构建:
javascript
// 创建几何体 - 使用索引绘制
const geometry = new THREE.BufferGeometry();
// 定义4个顶点(矩形只需要4个顶点,而不是之前的6个重复顶点)
const vertices = new Float32Array([
-1.0, -1.0, 1.0, // 顶点0 - 左下
1.0, -1.0, 1.0, // 顶点1 - 右下
1.0, 1.0, 1.0, // 顶点2 - 右上
-1.0, 1.0, 1.0 // 顶点3 - 左上
]);
// 定义索引(用2个三角形组成矩形)
const indices = new Uint16Array([
0, 1, 2, // 第一个三角形
0, 2, 3 // 第二个三角形
]);
// 设置几何体属性
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
geometry.setIndex(new THREE.BufferAttribute(indices, 1)); // 1表示每个索引是1个数字
2.几何体划分顶点组设置不同材质
下面代码展示了正方体每个面由不同的颜色组成:
javascript
<template>
<div ref="container" class="three-container"></div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const container = ref(null);
onMounted(() => {
// 1. 创建场景、相机和渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
const renderer = new THREE.WebGLRenderer({ antialias: true });
// 2. 设置渲染器
renderer.setSize(window.innerWidth, window.innerHeight);
container.value.appendChild(renderer.domElement);
// 3. 创建多材质立方体
const createMultiMaterialCube = () => {
const geometry = new THREE.BoxGeometry(2, 2, 2);
// 为每个面创建不同材质
const materials = [
new THREE.MeshBasicMaterial({ color: 0xff0000 }), // 右 - 红
new THREE.MeshBasicMaterial({ color: 0x00ff00 }), // 左 - 绿
new THREE.MeshBasicMaterial({ color: 0x0000ff }), // 上 - 蓝
new THREE.MeshBasicMaterial({ color: 0xffff00 }), // 下 - 黄
new THREE.MeshBasicMaterial({ color: 0xff00ff }), // 前 - 紫
new THREE.MeshBasicMaterial({ color: 0x00ffff }) // 后 - 青
];
return new THREE.Mesh(geometry, materials);
};
const cube = createMultiMaterialCube();
scene.add(cube);
// 4. 添加辅助工具
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
const gridHelper = new THREE.GridHelper(10, 10);
scene.add(gridHelper);
// 5. 设置相机
camera.position.set(3, 3, 5);
camera.lookAt(0, 0, 0);
// 6. 添加控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// 7. 动画循环
const animate = () => {
requestAnimationFrame(animate);
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
controls.update();
renderer.render(scene, camera);
};
animate();
// 8. 响应式处理
const onWindowResize = () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
};
window.addEventListener('resize', onWindowResize);
// 9. 清理
onUnmounted(() => {
window.removeEventListener('resize', onWindowResize);
container.value?.removeChild(renderer.domElement);
controls.dispose();
});
});
</script>
<style scoped>
.three-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
margin: 0;
padding: 0;
}
</style>
3.threejs常见的几何体:
下面是网站链接:
javascript
// 常见几何体
// BoxGeometry (立方体)
const geometry = new THREE.BoxGeometry(width, height, depth);
// SphereGeometry (球体)
const geometry = new THREE.SphereGeometry(radius, widthSegments, heightSegments);
// CylinderGeometry (圆柱体)
const geometry = new THREE.CylinderGeometry(radiusTop, radiusBottom, height, radialSegments);
// ConeGeometry (圆锥体)
const geometry = new THREE.ConeGeometry(radius, height, radialSegments);
// TorusGeometry (圆环)
const geometry = new THREE.TorusGeometry(radius, tube, radialSegments, tubularSegments);
// 平面几何体
// PlaneGeometry (平面)
const geometry = new THREE.PlaneGeometry(width, height, widthSegments, heightSegments);
// CircleGeometry (圆形)
const geometry = new THREE.CircleGeometry(radius, segments);
// RingGeometry (环形)
const geometry = new THREE.RingGeometry(innerRadius, outerRadius, thetaSegments);
4.基础网络材质
材质描述了对象objects的外观。它们的定义方式与渲染器无关, 因此,如果我们决定使用不同的渲染器,不必重写材质。
我们先准备一个平面的渲染代码:
javascript
<template>
<div ref="container" class="three-container"></div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GUI } from 'lil-gui';
const container = ref(null);
let gui = null;
let controls = null;
onMounted(() => {
// 1. 初始化场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
container.value.appendChild(renderer.domElement);
// 初始化 GUI
gui = new GUI();
// 创建平面
const planeGeometry = new THREE.PlaneGeometry(1, 1);
const planeMaterial = new THREE.MeshBasicMaterial({
color: 0xffffff,
});
const planeMesh = new THREE.Mesh(planeGeometry, planeMaterial);
scene.add(planeMesh);
// 设置相机位置
camera.position.z = 3;
camera.lookAt(0, 0, 0);
// 添加轨道控制器
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// 动画循环
const animate = () => {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
};
animate();
// 窗口大小调整
const onWindowResize = () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
};
window.addEventListener('resize', onWindowResize);
// 清理资源
onUnmounted(() => {
window.removeEventListener('resize', onWindowResize);
if (gui) gui.destroy();
if (controls) controls.dispose();
planeGeometry.dispose();
planeMaterial.dispose();
renderer.dispose();
container.value?.removeChild(renderer.domElement);
});
});
</script>
<style scoped>
.three-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
margin: 0;
padding: 0;
}
</style>
为了将指定照片作为纹理贴在上面,我们添加一个纹理加载器THREE.TextureLoader(),将指定路径的纹理贴在创建的平面上:
javascript
// 初始化 GUI
gui = new GUI();
// 创建纹理加载器
const textureLoader = new THREE.TextureLoader();
// 2. 创建平面
const planeGeometry = new THREE.PlaneGeometry(1, 1);
const planeMaterial = new THREE.MeshBasicMaterial({
color: 0xffffff,
map: textureLoader.load('/src/assets/jinggai.jpg') // 纹理路径
});
const planeMesh = new THREE.Mesh(planeGeometry, planeMaterial);
scene.add(planeMesh);

然后设置允许透明度以及双面渲染:
javascript
// 初始化 GUI
gui = new GUI();
// 创建纹理加载器
const textureLoader = new THREE.TextureLoader();
// 2. 创建平面
const planeGeometry = new THREE.PlaneGeometry(1, 1);
const planeMaterial = new THREE.MeshBasicMaterial({
color: 0xffffff,
map: textureLoader.load('/src/assets/jinggai.jpg'),
side: THREE.DoubleSide, // 双面渲染
transparent: true, // 透明
});
const planeMesh = new THREE.Mesh(planeGeometry, planeMaterial);
scene.add(planeMesh);
然后插入hdr格式照片来作为我们的全景环境:
先导入RGBELoader:
javascript
import { RGBELoader } from 'three/examples/jsm/Addons.js';
javascript
<template>
<div ref="container" class="three-container"></div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
import { GUI } from 'lil-gui';
const container = ref(null);
let gui = null;
let controls = null;
onMounted(() => {
// 1. 初始化场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
const renderer = new THREE.WebGLRenderer({
antialias: true,
toneMapping: THREE.ACESFilmicToneMapping, // 启用色调映射
toneMappingExposure: 1.0 // 设置曝光
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace; // 设置色彩空间
container.value.appendChild(renderer.domElement);
// 2. 初始化 GUI
gui = new GUI();
const params = {
envMapIntensity: 1.0,
exposure: 1.0
};
// 3. 加载 HDR 环境贴图
const rgbeLoader = new RGBELoader();
rgbeLoader.load(
'/src/assets/environment.hdr', // 替换为你的HDR文件路径
(texture) => {
// 设置球形映射
texture.mapping = THREE.EquirectangularReflectionMapping;
// 设置场景环境贴图
scene.environment = texture;
scene.background = texture;
// 可选:创建平面材质
const planeGeometry = new THREE.PlaneGeometry(1, 1);
const planeMaterial = new THREE.MeshStandardMaterial({
color: 0xffffff,
metalness: 0.5,
roughness: 0.1,
envMap: texture, // 使用环境贴图
envMapIntensity: params.envMapIntensity
});
const planeMesh = new THREE.Mesh(planeGeometry, planeMaterial);
scene.add(planeMesh);
// GUI 控制
gui.add(params, 'envMapIntensity', 0, 2).onChange((value) => {
planeMaterial.envMapIntensity = value;
});
gui.add(params, 'exposure', 0, 2).onChange((value) => {
renderer.toneMappingExposure = value;
});
},
undefined, // 进度回调
(error) => {
console.error('加载HDR环境贴图失败:', error);
}
);
// 4. 添加光源(增强效果)
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// 5. 设置相机
camera.position.z = 3;
camera.lookAt(0, 0, 0);
// 6. 添加控制器
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// 7. 动画循环
const animate = () => {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
};
animate();
// 8. 窗口大小调整
const onWindowResize = () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
};
window.addEventListener('resize', onWindowResize);
// 9. 清理资源
onUnmounted(() => {
window.removeEventListener('resize', onWindowResize);
if (gui) gui.destroy();
if (controls) controls.dispose();
renderer.dispose();
container.value?.removeChild(renderer.domElement);
});
});
</script>
<style scoped>
.three-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
margin: 0;
padding: 0;
}
</style>
5.雾fog:
雾效(Fog)是 Three.js 中用于模拟大气效果的重要功能,它能创造深度感和距离感,使场景看起来更加真实。
javascript
const scene = new THREE.Scene();
scene.fog = new THREE.Fog(0xcccccc, 10, 100); // 线性雾
scene.fog = new THREE.FogExp2(0xcccccc, 0.01); // 指数雾
下面以极其长的长方体为例展示雾的效果:
javascript
<template>
<div ref="container" class="three-container"></div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GUI } from 'lil-gui';
const container = ref(null);
let gui = null;
let controls = null;
onMounted(() => {
// 1. 初始化场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
0.1,
1000
);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
container.value.appendChild(renderer.domElement);
// 2. 添加雾效
scene.fog = new THREE.FogExp2(0xcccccc, 0.01); // 使用指数雾
scene.background = new THREE.Color(0xcccccc); // 背景色与雾色一致
// 3. 创建长形长方体
const length = 50; // 长度
const width = 2; // 宽度
const height = 2; // 高度
const geometry = new THREE.BoxGeometry(width, height, length);
const material = new THREE.MeshStandardMaterial({
color: 0x3498db,
metalness: 0.3,
roughness: 0.7
});
const longBox = new THREE.Mesh(geometry, material);
scene.add(longBox);
// 4. 添加地面参考平面
const groundGeometry = new THREE.PlaneGeometry(100, 100);
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0x2c3e50,
side: THREE.DoubleSide
});
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -height / 2;
scene.add(ground);
// 5. 添加光源
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(10, 20, 10);
scene.add(directionalLight);
// 6. 设置相机位置
camera.position.set(10, 10, 10);
camera.lookAt(0, 0, 0);
// 7. 添加控制器
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
// 8. 初始化GUI
gui = new GUI();
const fogParams = {
color: '#cccccc',
density: 0.01,
type: 'exp2'
};
gui.addColor(fogParams, 'color').onChange(value => {
scene.fog.color.set(value);
scene.background.set(value);
});
gui.add(fogParams, 'density', 0, 0.1).onChange(value => {
if (scene.fog instanceof THREE.FogExp2) {
scene.fog.density = value;
}
});
gui.add(fogParams, 'type', ['linear', 'exp2']).onChange(value => {
if (value === 'linear') {
scene.fog = new THREE.Fog(parseInt(fogParams.color.replace('#', '0x')), 5, 50);
} else {
scene.fog = new THREE.FogExp2(parseInt(fogParams.color.replace('#', '0x')), fogParams.density);
}
});
// 9. 动画循环
const animate = () => {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
};
animate();
// 10. 窗口大小调整
const onWindowResize = () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
};
window.addEventListener('resize', onWindowResize);
// 11. 清理资源
onUnmounted(() => {
window.removeEventListener('resize', onWindowResize);
gui?.destroy();
controls?.dispose();
renderer.dispose();
container.value?.removeChild(renderer.domElement);
});
});
</script>
<style scoped>
.three-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
margin: 0;
padding: 0;
}
</style>

五.GLTF加载器
glTF(gl传输格式)是一种开放格式的规范 (open format specification), 用于更高效地传输、加载3D内容。该类文件以JSON(.gltf)格式或二进制(.glb)格式提供, 外部文件存储贴图(.jpg、.png)和额外的二进制数据(.bin)。一个glTF组件可传输一个或多个场景, 包括网格、材质、贴图、蒙皮、骨架、变形目标、动画、灯光以及摄像机。
可以去下面链接获取3D模型:Log in to your Sketchfab account - Sketchfab
1.标准 GLTF 模型加载(未压缩)
javascript
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const loader = new GLTFLoader();
loader.load(
// 参数1: 资源路径
'/models/character.glb',
// 参数2: 加载完成回调
(gltf) => {
// 3.1 模型预处理
const model = gltf.scene;
model.scale.set(0.8, 0.8, 0.8);
// 3.2 材质适配
model.traverse((node) => {
if (node.isMesh) {
node.material.fog = true; // 启用雾效影响
node.castShadow = true; // 启用阴影
}
});
scene.add(model);
},
// 参数3: 加载进度回调
(xhr) => {
console.log(`加载进度: ${(xhr.loaded / xhr.total * 100).toFixed(1)}%`);
},
// 参数4: 错误处理
(error) => {
console.error('加载失败:', error);
// 可在此处添加备用方案
}
);
需同时有
.gltf
(JSON 描述文件) +.bin
(二进制数据) + 贴图
2.压缩模型加载(.glb 格式)
javascript
loader.load(
'/models/compressed/model.glb',
(gltf) => {
const model = gltf.scene;
// 遍历模型设置阴影和材质
model.traverse((node) => {
if (node.isMesh) {
node.castShadow = true;
node.material.metalness = 0.1; // 修改材质参数示例
}
});
scene.add(model);
},
undefined, // 不显示进度
(error) => console.error(error)
);
3.DRACO 压缩模型加载
安装解码器:
javascript
npm install three/examples/jsm/libs/draco
将 draco
文件夹复制到 public/libs/
下。
javascript
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
const loader = new GLTFLoader();
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/libs/draco/'); // 设置解码器路径
loader.setDRACOLoader(dracoLoader);
loader.load(
'/models/compressed/dragon.glb', // Draco压缩的模型
(gltf) => {
gltf.scene.scale.set(0.5, 0.5, 0.5);
scene.add(gltf.scene);
}
);
下面是完整演示代码:
javascript
<template>
<div ref="container" class="three-container"></div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { GUI } from 'lil-gui';
const container = ref(null);
let gui = null;
let controls = null;
let carModel = null; // 存储加载的汽车模型
onMounted(() => {
// ==================== 1. 初始化场景 ====================
const scene = new THREE.Scene();
// 创建透视相机 (视野角度, 宽高比, 近裁面, 远裁面)
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
// 创建WebGL渲染器(开启抗锯齿)
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true; // 启用阴影
container.value.appendChild(renderer.domElement);
// ==================== 2. 设置雾效 ====================
// 使用指数雾(颜色,密度)
scene.fog = new THREE.FogExp2(0xcccccc, 0.02);
// 设置背景色与雾色一致
scene.background = new THREE.Color(0xcccccc);
// ==================== 3. 添加光源 ====================
// 环境光(柔和的基础照明)
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);
// 定向光(主光源,产生阴影)
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 10, 7);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048; // 阴影质量
directionalLight.shadow.mapSize.height = 2048;
scene.add(directionalLight);
// ==================== 4. 添加地面 ====================
const groundGeometry = new THREE.PlaneGeometry(20, 20);
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0x3a3a3a,
roughness: 0.8
});
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2; // 旋转使平面水平
ground.receiveShadow = true; // 地面接收阴影
scene.add(ground);
// ==================== 5. 加载汽车模型 ====================
const loader = new GLTFLoader();
// 创建加载进度显示
const progressBar = document.createElement('div');
progressBar.style.cssText = `
position: absolute;
top: 10px;
left: 10px;
color: white;
font-family: Arial;
background: rgba(0,0,0,0.7);
padding: 5px 10px;
border-radius: 3px;
`;
container.value.appendChild(progressBar);
// 开始加载模型
loader.load(
// 模型路径(注意:Vite会自动处理src/assets路径)
'/models/car.glb',
// 加载成功回调
(gltf) => {
carModel = gltf.scene;
// 遍历模型所有部分
carModel.traverse((child) => {
if (child.isMesh) {
// 确保所有网格都能投射阴影
child.castShadow = true;
// 确保材质受雾效影响
child.material.fog = true;
}
});
// 调整模型位置和大小
carModel.position.y = 0.5; // 稍微抬高避免与地面穿插
carModel.scale.set(0.8, 0.8, 0.8);
// 计算模型中心点并居中
const box = new THREE.Box3().setFromObject(carModel);
const center = box.getCenter(new THREE.Vector3());
carModel.position.sub(center);
scene.add(carModel);
progressBar.textContent = '汽车模型加载完成';
setTimeout(() => progressBar.remove(), 2000);
},
// 加载进度回调
(xhr) => {
const percent = (xhr.loaded / xhr.total * 100).toFixed(2);
progressBar.textContent = `加载进度: ${percent}%`;
},
// 加载失败回调
(error) => {
console.error('模型加载失败:', error);
progressBar.textContent = '加载失败: ' + error.message;
progressBar.style.color = 'red';
}
);
// ==================== 6. 设置相机 ====================
camera.position.set(5, 2, 5); // 相机初始位置
camera.lookAt(0, 0.5, 0); // 看向模型中心
// ==================== 7. 添加控制器 ====================
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // 启用阻尼惯性
controls.dampingFactor = 0.05; // 阻尼系数
controls.minDistance = 3; // 最小缩放距离
controls.maxDistance = 20; // 最大缩放距离
// ==================== 8. GUI控制面板 ====================
gui = new GUI();
const fogParams = {
color: '#cccccc',
density: 0.02,
type: 'exp2'
};
// 雾效控制
const fogFolder = gui.addFolder('雾效设置');
fogFolder.addColor(fogParams, 'color').onChange(value => {
scene.fog.color.set(value);
scene.background.set(value);
});
fogFolder.add(fogParams, 'density', 0.001, 0.1, 0.001).onChange(value => {
if (scene.fog instanceof THREE.FogExp2) {
scene.fog.density = value;
}
});
fogFolder.add(fogParams, 'type', ['linear', 'exp2']).onChange(value => {
if (value === 'linear') {
scene.fog = new THREE.Fog(parseInt(fogParams.color.replace('#', '0x')), 5, 30);
} else {
scene.fog = new THREE.FogExp2(parseInt(fogParams.color.replace('#', '0x')), fogParams.density);
}
});
fogFolder.open();
// ==================== 9. 动画循环 ====================
const animate = () => {
requestAnimationFrame(animate);
controls.update(); // 更新控制器
renderer.render(scene, camera); // 渲染场景
};
animate();
// ==================== 10. 窗口大小调整 ====================
const onWindowResize = () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
};
window.addEventListener('resize', onWindowResize);
// ==================== 11. 组件卸载清理 ====================
onUnmounted(() => {
window.removeEventListener('resize', onWindowResize);
gui?.destroy();
controls?.dispose();
renderer.dispose();
container.value?.removeChild(renderer.domElement);
});
});
</script>
<style scoped>
.three-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
margin: 0;
padding: 0;
}
</style>

还有一种可以观看小车的外壳:
javascript
<template>
<div ref="container" class="three-container">
<div v-if="loadingProgress < 100" class="loading-overlay">
<div class="progress-bar">
<div class="progress" :style="{ width: `${loadingProgress}%` }"></div>
</div>
<div class="progress-text">{{ loadingProgress.toFixed(0) }}%</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const container = ref(null);
const loadingProgress = ref(0);
let controls = null;
let model = null;
// 自适应调整模型大小和相机位置
function fitCameraToObject(camera, object, offset = 1.5) {
const boundingBox = new THREE.Box3().expandByObject(object);
const center = boundingBox.getCenter(new THREE.Vector3());
const size = boundingBox.getSize(new THREE.Vector3());
const maxDim = Math.max(size.x, size.y, size.z);
const fov = camera.fov * (Math.PI / 180);
let cameraZ = Math.abs((maxDim / 2) / Math.tan(fov / 2)) * offset;
// 限制最小距离
cameraZ = Math.max(cameraZ, maxDim * 0.5);
camera.position.copy(center);
camera.position.z += cameraZ;
camera.lookAt(center);
// 更新控制器目标
if (controls) {
controls.target.copy(center);
controls.maxDistance = cameraZ * 3;
controls.minDistance = maxDim * 0.5;
controls.update();
}
}
onMounted(() => {
// 1. 初始化场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf0f0f0);
// 2. 设置相机(使用更大的远裁切面)
const camera = new THREE.PerspectiveCamera(
50, // 更小的FOV减少透视变形
window.innerWidth / window.innerHeight,
0.1,
5000 // 增大远裁切面
);
// 3. 高性能渲染器配置
const renderer = new THREE.WebGLRenderer({
antialias: true,
powerPreference: "high-performance"
});
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
container.value.appendChild(renderer.domElement);
// 4. 添加雾效(范围更大)
scene.fog = new THREE.FogExp2(0xf0f0f0, 0.002); // 更低的密度
// 5. 增强光照
const ambientLight = new THREE.AmbientLight(0x404040, 0.8);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5);
directionalLight.position.set(10, 20, 15);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
directionalLight.shadow.camera.far = 500;
scene.add(directionalLight);
// 6. 添加地面网格辅助查看
const gridHelper = new THREE.GridHelper(100, 50, 0x888888, 0xcccccc);
scene.add(gridHelper);
// 7. 加载模型(使用Vite的public目录)
const loader = new GLTFLoader();
loader.load(
'/models/car.glb', // 替换为你的模型路径
(gltf) => {
model = gltf.scene;
// 7.1 启用所有子元素的阴影
model.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
// 优化大模型材质
if (child.material) {
child.material.side = THREE.DoubleSide;
child.material.shadowSide = THREE.BackSide;
}
}
});
scene.add(model);
// 7.2 自适应调整相机和控制器
fitCameraToObject(camera, model);
// 7.3 添加辅助线框查看边界
const bbox = new THREE.Box3().setFromObject(model);
const bboxHelper = new THREE.Box3Helper(bbox, 0xffff00);
scene.add(bboxHelper);
loadingProgress.value = 100;
},
(xhr) => {
loadingProgress.value = (xhr.loaded / xhr.total) * 100;
},
(error) => {
console.error('加载失败:', error);
loadingProgress.value = -1; // 显示错误状态
}
);
// 8. 控制器配置
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.screenSpacePanning = true;
controls.maxPolarAngle = Math.PI * 0.9; // 限制垂直旋转角度
// 9. 响应式处理
const onWindowResize = () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
// 如果模型已加载,重新调整相机
if (model) fitCameraToObject(camera, model);
};
window.addEventListener('resize', onWindowResize);
// 10. 动画循环
const animate = () => {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
};
animate();
onUnmounted(() => {
window.removeEventListener('resize', onWindowResize);
controls?.dispose();
renderer.dispose();
});
});
</script>
<style scoped>
.three-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
.loading-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
z-index: 100;
}
.progress-bar {
width: 300px;
height: 20px;
background: rgba(255,255,255,0.2);
border-radius: 10px;
overflow: hidden;
margin-bottom: 10px;
}
.progress {
height: 100%;
background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);
transition: width 0.3s ease;
}
.progress-text {
color: white;
font-family: Arial, sans-serif;
text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
}
</style>