javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > Three.js构建3D商品展示空间

Three.js构建一个 3D 商品展示空间完整实战项目

作者:EndingCoder

Three.js是一个强大的JavaScript库,专用于在Web浏览器中创建3D图形,这篇文章主要介绍了Three.js构建一个3D商品展示空间的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下

引言

Three.js 作为强大的 WebGL 框架,广泛应用于交互式 3D 场景开发,尤其适合创建沉浸式商品展示空间。本文将通过一个完整的实战项目,展示如何构建一个 3D 商品展示空间,涵盖项目架构、资源组织、多模型切换、交互热点绑定、移动端适配和帧率优化。项目以展示虚拟商品(如家具或电子产品)为核心,结合用户交互,基于 Vite、TypeScript 和 Tailwind CSS,支持 ES Modules,确保响应式布局,遵循 WCAG 2.1 可访问性标准。本文适合希望通过综合项目掌握 Three.js 开发的开发者。

通过本篇文章,你将学会:

项目核心技术

1. 项目架构与资源组织

一个清晰的项目架构和资源组织是高效开发和维护的基础。以下是推荐的项目结构和资源管理方式:

2. 多模型切换、交互热点绑定

3. 移动端适配与帧率优化

4. 可访问性要求

为确保 3D 场景对残障用户友好,遵循 WCAG 2.1:

实践案例:3D 商品展示空间

我们将构建一个 3D 商品展示空间,支持多模型切换、交互热点绑定,优化移动端体验和帧率。场景包含一个展厅和多个商品模型(椅子、桌子、台灯),用户可通过按钮或键盘切换模型,点击热点查看商品信息。

1. 项目结构

threejs-product-showcase/
├── index.html
├── src/
│   ├── index.css
│   ├── main.ts
│   ├── assets/
│   │   ├── models/
│   │   │   ├── chair.glb
│   │   │   ├── table.glb
│   │   │   ├── lamp.glb
│   │   ├── textures/
│   │   │   ├── floor-texture.jpg
│   │   │   ├── wall-texture.jpg
│   │   │   ├── hotspot.png
│   ├── components/
│   │   ├── scene.ts
│   │   ├── controls.ts
│   ├── tests/
│   │   ├── showcase.test.ts
└── package.json

2. 环境搭建

初始化 Vite 项目

npm create vite@latest threejs-product-showcase -- --template vanilla-ts
cd threejs-product-showcase
npm install three@0.157.0 @types/three@0.157.0 tailwindcss postcss autoprefixer stats.js
npx tailwindcss init

配置 TypeScript (tsconfig.json):

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist"
  },
  "include": ["src/**/*"]
}

配置 Tailwind CSS (tailwind.config.js):

/** @type {import('tailwindcss').Config} */
export default {
  content: ['./index.html', './src/**/*.{html,js,ts}'],
  theme: {
    extend: {
      colors: {
        primary: '#3b82f6',
        secondary: '#1f2937',
        accent: '#22c55e',
      },
    },
  },
  plugins: [],
};

CSS (src/index.css):

@tailwind base;
@tailwind components;
@tailwind utilities;

.dark {
  @apply bg-gray-900 text-white;
}

#canvas {
  @apply w-full max-w-4xl mx-auto h-[600px] sm:h-[700px] md:h-[800px] rounded-lg shadow-lg;
}

.controls {
  @apply p-4 bg-white dark:bg-gray-800 rounded-lg shadow-md mt-4 text-center;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  border: 0;
}

.progress-bar {
  @apply w-full h-4 bg-gray-200 rounded overflow-hidden;
}

.progress-fill {
  @apply h-4 bg-primary transition-all duration-300;
}

3. 初始化场景与交互

src/components/scene.ts:

import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

export class ShowcaseScene {
  scene: THREE.Scene;
  camera: THREE.PerspectiveCamera;
  renderer: THREE.WebGLRenderer;
  controls: OrbitControls;
  models: { [key: string]: THREE.Group };
  currentModel: THREE.Group | null = null;
  hotspots: THREE.Sprite[] = [];
  raycaster: THREE.Raycaster;
  mouse: THREE.Vector2;

  constructor(canvas: HTMLCanvasElement, sceneDesc: HTMLDivElement) {
    this.scene = new THREE.Scene();
    this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    this.camera.position.set(0, 2, 5);
    this.renderer = new THREE.WebGLRenderer({ antialias: true });
    this.renderer.setSize(window.innerWidth, window.innerHeight);
    this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
    canvas.appendChild(this.renderer.domElement);
    this.controls = new OrbitControls(this.camera, this.renderer.domElement);
    this.controls.enableDamping = true;
    this.models = {};
    this.raycaster = new THREE.Raycaster();
    this.mouse = new THREE.Vector2();

    // 加载纹理
    const textureLoader = new THREE.TextureLoader();
    const floorTexture = textureLoader.load('/src/assets/textures/floor-texture.jpg');
    const wallTexture = textureLoader.load('/src/assets/textures/wall-texture.jpg');

    // 添加展厅
    const floor = new THREE.Mesh(
      new THREE.PlaneGeometry(10, 10),
      new THREE.MeshStandardMaterial({ map: floorTexture })
    );
    floor.rotation.x = -Math.PI / 2;
    this.scene.add(floor);
    const wall = new THREE.Mesh(
      new THREE.PlaneGeometry(10, 5),
      new THREE.MeshStandardMaterial({ map: wallTexture })
    );
    wall.position.set(0, 2.5, -5);
    this.scene.add(wall);

    // 添加光源
    const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
    this.scene.add(ambientLight);
    const pointLight = new THREE.PointLight(0xffffff, 0.5, 100);
    pointLight.position.set(2, 3, 2);
    this.scene.add(pointLight);

    // 加载模型
    this.loadModels(sceneDesc);
  }

  async loadModels(sceneDesc: HTMLDivElement) {
    const loader = new GLTFLoader();
    const modelNames = ['chair', 'table', 'lamp'];
    const progressFill = document.querySelector('.progress-fill') as HTMLDivElement;
    for (let i = 0; i < modelNames.length; i++) {
      const name = modelNames[i];
      const gltf = await loader.loadAsync(`/src/assets/models/${name}.glb`);
      this.models[name] = gltf.scene;
      this.models[name].visible = false;
      this.scene.add(this.models[name]);
      progressFill.style.width = `${((i + 1) / modelNames.length) * 100}%`;
    }
    this.currentModel = this.models['chair'];
    this.currentModel.visible = true;
    this.addHotspots();
    progressFill.parentElement!.style.display = 'none';
    sceneDesc.textContent = '商品模型加载完成,当前展示:椅子';
  }

  addHotspots() {
    const textureLoader = new THREE.TextureLoader();
    const hotspotMaterial = new THREE.SpriteMaterial({
      map: textureLoader.load('/src/assets/textures/hotspot.png'),
      transparent: true,
    });
    const hotspot = new THREE.Sprite(hotspotMaterial);
    hotspot.position.set(0, 1, 0);
    hotspot.scale.set(0.5, 0.5, 0.5);
    hotspot.userData = { info: '椅子:¥999,现代简约风格' };
    this.hotspots.push(hotspot);
    this.scene.add(hotspot);
  }

  switchModel(name: string, sceneDesc: HTMLDivElement) {
    if (this.currentModel) this.currentModel.visible = false;
    this.currentModel = this.models[name];
    this.currentModel.visible = true;
    this.hotspots.forEach((hotspot) => {
      hotspot.userData.info = `${name}:¥${name === 'chair' ? 999 : name === 'table' ? 1999 : 499},现代简约风格`;
    });
    sceneDesc.textContent = `切换到商品:${name}`;
  }

  handleInteraction(event: MouseEvent, sceneDesc: HTMLDivElement) {
    this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
    this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
    this.raycaster.setFromCamera(this.mouse, this.camera);
    const intersects = this.raycaster.intersectObjects(this.hotspots);
    if (intersects.length > 0) {
      sceneDesc.textContent = intersects[0].object.userData.info;
    }
  }

  animate() {
    this.controls.update();
    this.renderer.render(this.scene, this.camera);
    requestAnimationFrame(() => this.animate());
  }

  resize() {
    this.camera.aspect = window.innerWidth / window.innerHeight;
    this.camera.updateProjectionMatrix();
    this.renderer.setSize(window.innerWidth, window.innerHeight);
  }
}

src/main.ts:

import * as THREE from 'three';
import Stats from 'stats.js';
import { ShowcaseScene } from './components/scene';
import './index.css';

// 初始化场景
const canvas = document.getElementById('canvas') as HTMLDivElement;
const sceneDesc = document.createElement('div');
sceneDesc.id = 'scene-desc';
sceneDesc.className = 'sr-only';
sceneDesc.setAttribute('aria-live', 'polite');
sceneDesc.textContent = '3D 商品展示空间加载中';
document.body.appendChild(sceneDesc);

const showcase = new ShowcaseScene(canvas, sceneDesc);

// 性能监控
const stats = new Stats();
stats.showPanel(0); // 显示 FPS
document.body.appendChild(stats.dom);

// 渲染循环
function animate() {
  stats.begin();
  showcase.animate();
  stats.end();
  requestAnimationFrame(animate);
}
animate();

// 交互控件:切换模型
const modelButtons = [
  { name: 'chair', label: '椅子' },
  { name: 'table', label: '桌子' },
  { name: 'lamp', label: '台灯' },
];
modelButtons.forEach(({ name, label }, index) => {
  const button = document.createElement('button');
  button.className = 'p-2 bg-primary text-white rounded ml-4';
  button.textContent = label;
  button.setAttribute('aria-label', `切换到${label}`);
  document.querySelector('.controls')!.appendChild(button);
  button.addEventListener('click', () => showcase.switchModel(name, sceneDesc));
});

// 键盘控制:切换模型
canvas.addEventListener('keydown', (e: KeyboardEvent) => {
  if (e.key === '1') showcase.switchModel('chair', sceneDesc);
  else if (e.key === '2') showcase.switchModel('table', sceneDesc);
  else if (e.key === '3') showcase.switchModel('lamp', sceneDesc);
});

// 鼠标交互:热点
canvas.addEventListener('click', (event) => showcase.handleInteraction(event, sceneDesc));

// 响应式调整
window.addEventListener('resize', () => showcase.resize());

4. HTML 结构

index.html:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Three.js 3D 商品展示空间</title>
  <link rel="stylesheet" href="./src/index.css" rel="external nofollow"  />
</head>
<body class="bg-gray-100 dark:bg-gray-900">
  <div class="min-h-screen p-4">
    <h1 class="text-2xl md:text-3xl font-bold text-center text-gray-900 dark:text-white mb-4">
      3D 商品展示空间
    </h1>
    <div id="canvas" class="h-[600px] w-full max-w-4xl mx-auto rounded-lg shadow"></div>
    <div class="controls">
      <p class="text-gray-900 dark:text-white">使用数字键 1-3 或按钮切换商品,点击热点查看详情</p>
      <div class="progress-bar">
        <div class="progress-fill"></div>
      </div>
    </div>
  </div>
  <script type="module" src="./src/main.ts"></script>
</body>
</html>

资源文件

5. 响应式适配

使用 Tailwind CSS 确保画布和控件自适应:

#canvas {
  @apply h-[600px] sm:h-[700px] md:h-[800px] w-full max-w-4xl mx-auto;
}

.controls {
  @apply p-2 sm:p-4;
}

6. 可访问性优化

7. 性能测试

src/tests/showcase.test.ts:

import Benchmark from 'benchmark';
import * as THREE from 'three';
import Stats from 'stats.js';

async function runBenchmark() {
  const suite = new Benchmark.Suite();
  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000);
  const renderer = new THREE.WebGLRenderer({ antialias: true });
  const stats = new Stats();
  const geometry = new THREE.BoxGeometry(1, 1, 1);
  const material = new THREE.MeshStandardMaterial();
  const mesh = new THREE.Mesh(geometry, material);
  scene.add(mesh);

  suite
    .add('Scene Render', () => {
      stats.begin();
      renderer.render(scene, camera);
      stats.end();
    })
    .on('cycle', (event: any) => {
      console.log(String(event.target));
    })
    .run({ async: true });
}

runBenchmark();

测试结果

测试工具

扩展功能

1. 动态调整模型缩放

添加控件调整模型大小:

const scaleInput = document.createElement('input');
scaleInput.type = 'range';
scaleInput.min = '0.5';
scaleInput.max = '2';
scaleInput.step = '0.1';
scaleInput.value = '1';
scaleInput.className = 'w-full mt-2';
scaleInput.setAttribute('aria-label', '调整模型大小');
document.querySelector('.controls')!.appendChild(scaleInput);
scaleInput.addEventListener('input', () => {
  if (showcase.currentModel) {
    const scale = parseFloat(scaleInput.value);
    showcase.currentModel.scale.set(scale, scale, scale);
    sceneDesc.textContent = `模型缩放调整为 ${scale.toFixed(1)}`;
  }
});

2. 动态光源控制

添加按钮切换光源强度:

const lightButton = document.createElement('button');
lightButton.className = 'p-2 bg-secondary text-white rounded ml-4';
lightButton.textContent = '切换光源';
lightButton.setAttribute('aria-label', '切换光源强度');
document.querySelector('.controls')!.appendChild(lightButton);
lightButton.addEventListener('click', () => {
  pointLight.intensity = pointLight.intensity === 0.5 ? 1.0 : 0.5;
  sceneDesc.textContent = `光源强度调整为 ${pointLight.intensity}`;
});

常见问题与解决方案

1. 模型加载失败

问题:模型未显示。
解决方案

2. 移动端卡顿

问题:低性能设备帧率低。
解决方案

3. 热点交互失效

问题:点击热点无反应。
解决方案

4. 可访问性问题

问题:屏幕阅读器无法识别交互。
解决方案

部署与优化

1. 本地开发

运行本地服务器:

npm run dev

2. 生产部署(阿里云)

部署到阿里云 OSS

3. 优化建议

注意事项

总结

本文通过一个 3D 商品展示空间案例,详细解析了项目架构、资源组织、多模型切换、交互热点绑定、移动端适配和帧率优化。结合 Vite、TypeScript 和 Tailwind CSS,场景实现了动态交互、可访问性优化和高效性能。测试结果表明场景流畅,WCAG 2.1 合规性确保了包容性。本案例为开发者提供了 Three.js 综合项目实践的基础。

到此这篇关于Three.js构建一个 3D 商品展示空间的文章就介绍到这了,更多相关Three.js构建3D商品展示空间内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文