使用Three.js搭建自己的3Dweb模型(从0到1无废话版本)

教学视频参考:B站------Three.js教学

教学链接:Three.js中文网 老陈打码 | 麒跃科技

一.什么是Three.js?

Three.js ​ 是一个基于 JavaScript 的 ​3D 图形库,用于在网页浏览器中创建和渲染交互式 3D 内容。它基于 WebGL(一种浏览器原生支持的 3D 图形 API),但通过更简单的抽象层让开发者无需直接编写复杂的 WebGL 代码即可构建 3D 场景。

下面是官网链接:基础 - three.js manualthree.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 中,理解物体位移和父子元素关系是构建复杂场景的基础。

Vector3 -- three.js docs

每个 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)共同构成了物体的完整空间变换。

Euler -- three.js docs

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.基础网络材质

Material -- three.js docs

材质描述了对象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加载器

GLTFLoader -- three.js docs

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>
相关推荐
Mike_jia10 分钟前
Memos:知识工作者的理想开源笔记系统
前端
前端大白话10 分钟前
前端崩溃瞬间救星!10 个 JavaScript 实战技巧大揭秘
前端·javascript
loveoobaby11 分钟前
Shadertoy着色器移植到Three.js经验总结
前端
蓝易云14 分钟前
在Linux、CentOS7中设置shell脚本开机自启动服务
前端·后端·centos
浩龙不eMo15 分钟前
前端获取环境变量方式区分(Vite)
前端·vite
一千柯橘21 分钟前
Nestjs 解决 request entity too large
javascript·后端
土豆骑士25 分钟前
monorepo 实战练习
前端
土豆骑士27 分钟前
monorepo最佳实践
前端
见青..28 分钟前
【学习笔记】文件包含漏洞--本地远程包含、伪协议、加密编码
前端·笔记·学习·web安全·文件包含
举个栗子dhy42 分钟前
如何处理动态地址栏参数,以及Object.entries() 、Object.fromEntries()和URLSearchParams.entries()使用
javascript