使用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内编写代码:

首先初始化基础环境:

// 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正方体:

参数类型作用
geometryTHREE.BufferGeometry定义物体的形状(如立方体、球体等)
materialTHREE.Material定义物体的外观(颜色、纹理、反光等)
// 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轴并对准原点

camera.position.z = 5; // 相机沿z轴后退5个单位
camera.lookAt(0, 0, 0); // 相机对准场景中心

最后使用递归animate()方法不断调用来让正方体展示并旋转:

function animate() {
  requestAnimationFrame(animate); // 循环调用自身
  cube.rotation.x += 0.01; // x轴旋转
  cube.rotation.y += 0.01; // y轴旋转
  renderer.render(scene, camera); // 渲染场景
}
animate(); // 启动动画

下面是完整代码: 

<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轴(蓝色)​​:垂直于屏幕(正向朝外)
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 场景​ 或 ​交互式展示
<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正方体不断的被我们拉动位置。

在这里可以设置是否带有阻尼,也就是是否带有惯性:

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

// 创建轨道控制器
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 三个分量:

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)按名称查找子对象
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】世界坐标

特点:

  • 物体在全局场景中的绝对位置
  • 需要计算得到(考虑所有父级变换)
  • 常用于碰撞检测、物理计算等
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 (四元数)
// 分别绕各轴旋转
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);

 旋转与父子关系:

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 的变换顺序是:​缩放 → 旋转 → 平移 

假如父组件被缩放,那么子组件也会跟着父组件被缩放的倍数进行缩放。

// 以下两个操作不等价
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 应用的基础。

// 监听窗口的变化
window.addEventListener('resize', () => {
  // 重置渲染器宽高比
  renderer.setSize(window.innerWidth, window.innerHeight);
  // 重置相机的宽高比
  camera.aspect = window.innerWidth / window.innerHeight;
  // 更新相机投影矩阵
  camera.updateProjectionMatrix();
});

现在注册一个按钮监听点击事件来让其全屏:

// 监听按钮点击事件
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项目的参数调节。

下载依赖:

npm install lil-gui

导入lilGUI:

import GUI from 'three/examples/jsm/libs/lil-gui.module.min.js';

我们以实现全屏按钮为例:

// 监听按钮点击事件
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调节立方体的位置:

// 随机控制立方体位置
gui.add(cube.position, 'x', -5, 5).name('立方体X轴位置'); 
// 也可以是下面这样
gui.add(cube.position, 'x').min(-5).max(5).step(1).name('立方体X轴位置');

也可以使用folder创建下拉框:

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轴位置');

也可以绑定监听事件:

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轴位置');

也可以监听最后停下的事件:

folder.add(cube.position, 'y', -5, 5).onFinishChange(()=>{
  console.log('立方体Y轴位置:', cube.position.y);
}).name('立方体Y轴位置');

 也可以使用布尔值设置是否为线框模式:

const gui = new GUI();
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
gui.add(material, 'wireframe').name('线框模式');

也可以选择颜色:

// 选择颜色
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)构造,下面的代码用来构造一个矩形:        

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 );

使用下面代码查看我们构造的矩形:

<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>

也可以使用索引来索引顶点位置进行构建:

// 创建几何体 - 使用索引绘制
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.几何体划分顶点组设置不同材质

下面代码展示了正方体每个面由不同的颜色组成:

<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常见的几何体:

下面是网站链接:

常见的几何体

// 常见几何体
// 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的外观。它们的定义方式与渲染器无关, 因此,如果我们决定使用不同的渲染器,不必重写材质。

我们先准备一个平面的渲染代码:

<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(),将指定路径的纹理贴在创建的平面上:

// 初始化 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);

 然后设置允许透明度以及双面渲染:

// 初始化 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:

import { RGBELoader } from 'three/examples/jsm/Addons.js';
<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 中用于模拟大气效果的重要功能,它能创造深度感和距离感,使场景看起来更加真实。

const scene = new THREE.Scene();
scene.fog = new THREE.Fog(0xcccccc, 10, 100); // 线性雾
scene.fog = new THREE.FogExp2(0xcccccc, 0.01); // 指数雾

下面以极其长的长方体为例展示雾的效果:

<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 模型加载(未压缩)

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 格式)​

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 压缩模型加载

 安装解码器:

npm install three/examples/jsm/libs/draco

将 draco 文件夹复制到 public/libs/ 下。

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);
  }
);

 下面是完整演示代码:

<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>

 还有一种可以观看小车的外壳:

<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>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

记得开心一点嘛

您的打赏是对我最大的鼓励与期待

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值