使用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会自动创建一个<canvas>
元素,以至于我们通过renderer.domElement
可以获取这个canvas,并通过
document.body.appendChild(renderer.domElement)
直接将canvas插入body。(这就是不写<canvas>也可以渲染的原因)
随后创建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(); // 启动动画
下面是完整代码:
<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. 设置渲染器大小并添加到DOMrenderer.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);// 初始化 GUIgui = 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. 初始化 GUIgui = 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. 初始化GUIgui = 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>