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

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


教学视频参考: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内编写代码:

首先初始化基础环境:

// 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会自动创建一个元素,以至于我们通过renderer.domElement可以获取这个canvas,并通过

document.body.appendChild(renderer.domElement)直接将canvas插入body。

(这就是不写也可以渲染的原因)

随后创建3D正方体:

参数 类型 作用 geometry THREE.BufferGeometry 定义物体的形状(如立方体、球体等) material THREE.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(); // 启动动画

下面是完整代码: 

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(); // 开始动画循环 
* { margin: 0; padding: 0;}/* 3D效果都是画在canvas画布上 */canvas{ display: block; position: fixed; left: 0; top: 0; width: 100vw; height: 100vh;}

 三. 基础操作

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 场景​ 或 ​交互式展示
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);});  
* { margin: 0; padding: 0;}canvas { display: block; position: fixed; left: 0; top: 0; width: 100vw; height: 100vh;}

在这里缩放是我们的相机在不断的变换位置,以至于看到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 );

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

 
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(); });});.three-container { position: fixed; top: 0; left: 0; width: 100%; height: 100%; overflow: hidden; margin: 0; padding: 0;}

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

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

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

 
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(); });});.three-container { position: fixed; top: 0; left: 0; width: 100%; height: 100%; overflow: hidden; margin: 0; padding: 0;}

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

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

 
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); });});.three-container { position: fixed; top: 0; left: 0; width: 100%; height: 100%; overflow: hidden; margin: 0; padding: 0;}

为了将指定照片作为纹理贴在上面,我们添加一个纹理加载器THREE.TextureLoader(),将指定路径的纹理贴在创建的平面上:

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

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

// 初始化 GUIgui = 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\';
 
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); });});.three-container { position: fixed; top: 0; left: 0; width: 100%; height: 100%; overflow: hidden; margin: 0; padding: 0;}

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); // 指数雾

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

 
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); });});.three-container { position: fixed; top: 0; left: 0; width: 100%; height: 100%; overflow: hidden; margin: 0; padding: 0;}

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

 下面是完整演示代码:

 
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); });});.three-container { position: fixed; top: 0; left: 0; width: 100%; height: 100%; overflow: hidden; margin: 0; padding: 0;}

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

 
<div v-if=\"loadingProgress
{{ loadingProgress.toFixed(0) }}%
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(); });});.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);}