前端 3D 展示:产品模型与交互
理解前端 3D 展示的基础
前端 3D 展示并非遥不可及,它借助浏览器内建的 WebGL 能力,将三维模型直接呈现在网页中。无论是产品展示、数据可视化还是互动叙事,3D 技术都能显著提升用户的沉浸感。最便捷的入门方式是使用封装良好的库,如 Three.js,它屏蔽了大量底层图形学细节,让开发者能够以声明式的方式构建场景、添加光照、导入模型并设置动画。本教程将以 Three.js 为核心,带你一步步实现一个可交互的产品 3D 展示——包括模型加载、旋转查看、环境光与材质调整。
为什么选择 Three.js
- 生态成熟:社区庞大,文档丰富,拥有大量即插即用的加载器(GLTF、OBJ、FBX 等)。
- 性能优化:内置 WebGL 渲染器,支持按需渲染、阴影贴图和后期处理。
- 开发效率:场景、相机、几何体、材质的概念清晰,代码结构直观。
开发环境准备
只需一个 HTML 文件和 CDN 引入的 Three.js。为了保证一致性,建议使用 ES 模块导入或构建工具(如 Vite)。本教程采用 CDN 引入 + 基础 HTML 结构,以保证示例能零门槛运行。
创建一个 product-3d.html,内容骨架如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>产品 3D 展示</title>
<style>
body { margin: 0; overflow: hidden; font-family: sans-serif; }
canvas { display: block; }
</style>
</head>
<body>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.157.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.157.0/examples/jsm/"
}
}
</script>
<script type="module">
// 我们的代码将写在这里
</script>
</body>
</html>
使用 importmap 可以直接在浏览器中用 ES 模块导入 Three.js 及其扩展,而无需打包工具。所有后续代码均放在 <script type="module"> 标签内。
场景、相机与渲染器:三要素的搭建
任何一个 3D 场景都离不开三大核心对象:场景(Scene)、相机(Camera)和渲染器(Renderer)。它们分别负责容纳物体、定义观察者和绘制画面。
初始化渲染器
渲染器需要绑定到一个 <canvas> 元素(Three.js 会自动创建并添加到 body),并设置其尺寸覆盖整个视口。同时开启抗锯齿让边缘更平滑。
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true // 透明背景,方便嵌入页面
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // 限制过高像素比以防性能问题
renderer.shadowMap.enabled = true; // 开启阴影
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
创建场景与相机
场景是3D世界的容器,所有模型、灯光等都需添加至场景。相机采用透视相机(PerspectiveCamera),模拟人眼近大远小的效果。参数定义:视野角度(FOV)、宽高比、近裁面、远裁面。
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xeeeeee); // 浅灰背景,更利于观察模型
const camera = new THREE.PerspectiveCamera(
45, // 视野角度
window.innerWidth / window.innerHeight, // 宽高比
0.1, // 近裁面
100 // 远裁面
);
camera.position.set(5, 3, 8); // 将相机放在模型右前上方
camera.lookAt(0, 0, 0);
设置相机位置为 (5, 3, 8),并让它看向原点,适合展示一个中等大小的产品模型。
处理窗口缩放
当浏览器窗口大小变化时,需同步更新渲染器和相机投影矩阵,以保证画面不变形。
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
灯光系统:让模型立体起来
没有光照,三维模型只是一片黑色。我们使用组合光源来模拟自然观察环境:
- 环境光(AmbientLight):均匀照亮所有面,避免死黑区域。
- 主方向光(DirectionalLight):模拟太阳,产生高光与阴影。
- 辅助点光源或半球光(可选项):补充暗部细节。
添加基础光照
// 环境光提供基础亮度
const ambientLight = new THREE.AmbientLight(0xffffff, 1.2);
scene.add(ambientLight);
// 主光源:方向光,从左上角照射
const directionalLight = new THREE.DirectionalLight(0xffffff, 3);
directionalLight.position.set(5, 10, 5);
directionalLight.castShadow = true; // 允许投射阴影
// 阴影参数优化
directionalLight.shadow.mapSize.width = 1024;
directionalLight.shadow.mapSize.height = 1024;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 50;
directionalLight.shadow.camera.left = -10;
directionalLight.shadow.camera.right = 10;
directionalLight.shadow.camera.top = 10;
directionalLight.shadow.camera.bottom = -10;
scene.add(directionalLight);
// 补光:半球光,天空色与地面色混合,增加自然感
const hemiLight = new THREE.HemisphereLight(0xddeeff, 0x3b3b3b, 0.8);
scene.add(hemiLight);
至此,一个明亮且带有阴影能力的灯光环境就设置好了。
加载产品模型:从 GLB/GLTF 开始
大多数产品 3D 模型采用 glTF 格式(.glb 为二进制版本,.gltf 为 JSON 版本),因其高效、可压缩且广泛支持 PBR 材质。将模型文件放入项目文件夹(例如 model/product.glb),使用 GLTFLoader 异步加载。
使用 GLTF 加载器
首先,在 import map 中已包含 three/addons/ 路径,可直接导入加载器。
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
加载模型并将其添加到场景中心,同时启用阴影投射与接收:
const loader = new GLTFLoader();
let productModel; // 保存模型引用,方便后续交互
loader.load(
'model/product.glb', // 替换为你的模型路径
(gltf) => {
productModel = gltf.scene;
// 遍历模型所有子节点,开启阴影与调整材质(可选)
productModel.traverse((node) => {
if (node.isMesh) {
node.castShadow = true;
node.receiveShadow = true;
// 若材质粗糙度等需要调整,可在此设置
if (node.material) {
node.material.roughness = 0.4;
node.material.metalness = 0.6;
}
}
});
// 将模型置于场景原点并适当缩放
productModel.position.set(0, -0.5, 0);
productModel.scale.set(1, 1, 1);
scene.add(productModel);
console.log('模型加载成功');
},
(progress) => {
// 可选:显示加载进度
const percent = (progress.loaded / progress.total * 100).toFixed(2);
console.log(`加载进度: ${percent}%`);
},
(error) => {
console.error('模型加载失败:', error);
}
);
添加一个简单的底座
在产品展示中,一个基本的圆形平面底座可以增强视觉稳定性,同时承接阴影。
const planeGeometry = new THREE.CircleGeometry(2.5, 64);
const planeMaterial = new THREE.MeshStandardMaterial({
color: 0xcccccc,
roughness: 0.7,
metalness: 0.1
});
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.rotation.x = -Math.PI / 2; // 旋转至水平
plane.position.y = -1.2;
plane.receiveShadow = true;
scene.add(plane);
用户交互:旋转、缩放与自动旋转
默认情况下,用户无法旋转视角。我们需要实现 轨道控制器(OrbitControls),它支持鼠标拖拽旋转、滚轮缩放、右键平移。
引入轨道控制器
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
初始化控制器并绑定到相机和渲染器:
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0, 0); // 环绕目标点
controls.enableDamping = true; // 启用惯性
controls.dampingFactor = 0.05;
controls.minDistance = 3;
controls.maxDistance = 15;
controls.maxPolarAngle = Math.PI / 1.8; // 限制向下旋转角度,避免看到“地下”
controls.update();
添加自动旋转选项
许多产品展示页面会加入“自动旋转”功能,用户禁用后可手动控制。通过 OrbitControls 的 autoRotate 属性轻松实现。
controls.autoRotate = true;
controls.autoRotateSpeed = 1.5; // 旋转速度
// 示例:点击画布可暂停/恢复自动旋转
let autoRotateEnabled = true;
renderer.domElement.addEventListener('click', () => {
autoRotateEnabled = !autoRotateEnabled;
controls.autoRotate = autoRotateEnabled;
});
渲染循环与性能优化
一切就绪后,需要一个持续运行的渲染循环通过 requestAnimationFrame 来更新画面。每一帧我们都更新控制器状态并渲染场景。
function animate() {
requestAnimationFrame(animate);
// 更新控制器(阻尼和自动旋转均依赖此更新)
controls.update();
// 渲染场景
renderer.render(scene, camera);
}
animate();
性能考虑点
- 限制像素比:前面已将
setPixelRatio限制在 2 以内。 - 按需渲染:若模型不常变化,可在无交互时停止渲染循环。但 OrbitControls 需要持续更新,因此保留循环。
- 模型优化:使用压缩的 glTF 文件,多边形数控制在合理范围(通常 5 万面以下即可流畅运行)。
进阶增强:环境映射与后期效果
想让产品更具质感?添加环境贴图(Environment Map)可以让模型反射周围环境,显得更加真实。
使用 RGBELoader 加载 HDR 环境
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
// 加载一张 HDR 环境贴图(可从 Poly Haven 等网站获取免费资源)
new RGBELoader()
.setPath('textures/')
.load('studio.hdr', (texture) => {
texture.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = texture; // 作用于所有标准材质
// 可选:将同一张图设为场景背景,但会遮挡原有背景色
// scene.background = texture;
});
现在,所有使用 MeshStandardMaterial 或 MeshPhysicalMaterial 的物体都会自动反射出环境光,金属和光滑表面的表现会大幅提升。
后期效果:简单泛光(Bloom)
如果想让高光部分产生光晕效果,可以使用 EffectComposer 和 UnrealBloomPass。首先在 import map 中添加相关模块(路径需包含 postprocessing)。以下给出简化集成步骤:
- 导入所需类:
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'; - 创建 composer 并添加 passes:
const composer = new EffectComposer(renderer); composer.addPass(new RenderPass(scene, camera)); const bloomPass = new UnrealBloomPass( new THREE.Vector2(window.innerWidth, window.innerHeight), 0.5, // 强度 0, // 半径 0 // 阈值 ); composer.addPass(bloomPass); - 在 resize 事件中更新 composer 和 bloomPass 的尺寸。
- 在动画循环中将
renderer.render替换为composer.render()。
注意后期处理会消耗额外的 GPU 资源,在移动端需要谨慎评估。
总结与源码整合
至此,你已经完成了一个功能完备的前端 3D 产品展示页面:模型加载、灯光场景、交互控制、环境反射以及可选的后期特效。将上述代码整合进 product-3d.html,替换模型路径即可在本地运行。完整的核心结构如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>产品 3D 展示</title>
<style>
body { margin: 0; overflow: hidden; }
canvas { display: block; }
</style>
</head>
<body>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.157.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.157.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
// 渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
// 场景和相机
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xeeeeee);
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(5, 3, 8);
camera.lookAt(0, 0, 0);
// 灯光
scene.add(new THREE.AmbientLight(0xffffff, 1.2));
const dirLight = new THREE.DirectionalLight(0xffffff, 3);
dirLight.position.set(5, 10, 5);
dirLight.castShadow = true;
dirLight.shadow.mapSize.set(1024, 1024);
dirLight.shadow.camera.near = 0.5;
dirLight.shadow.camera.far = 50;
dirLight.shadow.camera.left = -10;
dirLight.shadow.camera.right = 10;
dirLight.shadow.camera.top = 10;
dirLight.shadow.camera.bottom = -10;
scene.add(dirLight);
scene.add(new THREE.HemisphereLight(0xddeeff, 0x3b3b3b, 0.8));
// 地面
const plane = new THREE.Mesh(
new THREE.CircleGeometry(2.5, 64),
new THREE.MeshStandardMaterial({ color: 0xcccccc, roughness: 0.7, metalness: 0.1 })
);
plane.rotation.x = -Math.PI / 2;
plane.position.y = -1.2;
plane.receiveShadow = true;
scene.add(plane);
// 控制
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0, 0);
controls.enableDamping = true;
controls.autoRotate = true;
controls.autoRotateSpeed = 1.5;
controls.minDistance = 3;
controls.maxDistance = 15;
controls.maxPolarAngle = Math.PI / 1.8;
controls.update();
// 模型加载
const loader = new GLTFLoader();
loader.load('model/product.glb', (gltf) => {
const model = gltf.scene;
model.traverse((node) => {
if (node.isMesh) {
node.castShadow = true;
node.receiveShadow = true;
}
});
model.position.set(0, -0.5, 0);
scene.add(model);
});
// 环境贴图(可选)
new RGBELoader().setPath('textures/').load('studio.hdr', (texture) => {
texture.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = texture;
});
// 动画循环
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>
</body>
</html>
下一步探索
- 边界与手势:限制相机绕 Y 轴的旋转范围,或增加双指触控支持(移动端)。
- UI 叠加:在 HTML 层添加按钮控制自动旋转、切换背景色或更改产品颜色。
- 模型交互:使用 Raycaster 实现点击模型某部位触发事件,例如高亮零件或展示信息标签。
- AR 体验:借助 WebXR API 将模型放置在真实环境中,在移动端实现“查看在你的空间”。
通过本教程,你已掌握前端 3D 展示产品模型与交互的核心技术。开始创建你的第一个可交互 3D 网站吧。