返回笔记首页

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>
    <script src="https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.156.1/build/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.156.1/examples/js/controls/OrbitControls.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.156.1/examples/js/loaders/GLTFLoader.js"></script>
    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      body {
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        height: 100vh;
        overflow: hidden;
      }
      #app {
        width: 100%;
        height: 100%;
        display: flex;
        flex-direction: column;
      }
      .header {
        background: rgba(255, 255, 255, 0.95);
        padding: 20px 30px;
        box-shadow: 0 2px 10px rgba(0,0,0,0.1);
      }
      .header h1 {
        font-size: 24px;
        color: #333;
        margin-bottom: 5px;
      }
      .header p {
        color: #666;
        font-size: 14px;
      }
      .main-content {
        flex: 1;
        display: flex;
        padding: 20px;
        gap: 20px;
        overflow: hidden;
      }
      .viewer-container {
        flex: 1;
        background: rgba(255, 255, 255, 0.95);
        border-radius: 12px;
        box-shadow: 0 10px 40px rgba(0,0,0,0.2);
        position: relative;
        overflow: hidden;
      }
      #canvas-container {
        width: 100%;
        height: 100%;
      }
      .loading {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        text-align: center;
        z-index: 10;
      }
      .loading-spinner {
        width: 50px;
        height: 50px;
        border: 4px solid #f3f3f3;
        border-top: 4px solid #667eea;
        border-radius: 50%;
        animation: spin 1s linear infinite;
        margin: 0 auto 15px;
      }
      @keyframes spin {
        0% { transform: rotate(0deg); }
        100% { transform: rotate(360deg); }
      }
      .loading-text {
        color: #666;
        font-size: 16px;
      }
      .controls-panel {
        width: 320px;
        background: rgba(255, 255, 255, 0.95);
        border-radius: 12px;
        box-shadow: 0 10px 40px rgba(0,0,0,0.2);
        padding: 25px;
        overflow-y: auto;
      }
      .control-section {
        margin-bottom: 30px;
      }
      .control-section h3 {
        font-size: 16px;
        color: #333;
        margin-bottom: 15px;
        padding-bottom: 10px;
        border-bottom: 2px solid #667eea;
      }
      .control-item {
        margin-bottom: 20px;
      }
      .control-item label {
        display: block;
        color: #666;
        font-size: 14px;
        margin-bottom: 8px;
      }
      .color-options {
        display: grid;
        grid-template-columns: repeat(4, 1fr);
        gap: 10px;
      }
      .color-btn {
        width: 100%;
        aspect-ratio: 1;
        border: 3px solid transparent;
      border-radius: 8px;
      cursor: pointer;
      transition: all 0.3s;
      position: relative;
    }
    .color-btn:hover {
      transform: scale(1.1);
    }
    .color-btn.active {
      border-color: #667eea;
      box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.3);
    }
    .slider-container {
      display: flex;
      align-items: center;
      gap: 12px;
    }
    .slider {
      flex: 1;
      height: 6px;
      border-radius: 3px;
      background: #e0e0e0;
      outline: none;
      -webkit-appearance: none;
    }
    .slider::-webkit-slider-thumb {
      -webkit-appearance: none;
      appearance: none;
      width: 18px;
      height: 18px;
      border-radius: 50%;
      background: #667eea;
      cursor: pointer;
      transition: all 0.3s;
    }
    .slider::-webkit-slider-thumb:hover {
      background: #764ba2;
      transform: scale(1.2);
    }
    .slider-value {
      min-width: 40px;
      text-align: right;
      color: #333;
      font-weight: 600;
      font-size: 14px;
    }
    .btn {
      width: 100%;
      padding: 12px;
      border: none;
      border-radius: 8px;
      font-size: 14px;
      font-weight: 600;
      cursor: pointer;
      transition: all 0.3s;
      margin-bottom: 10px;
    }
    .btn-primary {
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
    }
    .btn-primary:hover {
      transform: translateY(-2px);
      box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
    }
    .btn-secondary {
      background: #f5f5f5;
      color: #333;
    }
    .btn-secondary:hover {
      background: #e0e0e0;
    }
    .stats {
      position: absolute;
      bottom: 15px;
      left: 15px;
      background: rgba(0, 0, 0, 0.7);
      color: white;
      padding: 10px 15px;
      border-radius: 8px;
      font-size: 12px;
      font-family: 'Courier New', monospace;
    }
    .stats div {
      margin-bottom: 5px;
    }
    .stats div:last-child {
      margin-bottom: 0;
    }
  </style>
</head>
<body>
  <div id="app">
    <div class="header">
      <h1>3D产品展示系统</h1>
      <p>支持360度旋转、材质切换、灯光调节等交互功能</p>
    </div>

    <div class="main-content">
      <div class="viewer-container">
        <div id="canvas-container"></div>
        <div v-if="loading" class="loading">
          <div class="loading-spinner"></div>
          <div class="loading-text">加载中... {{ loadingProgress }}%</div>
        </div>
        <div class="stats" v-if="showStats">
          <div>FPS: {{ fps }}</div>
          <div>三角面: {{ triangles }}</div>
          <div>Draw Calls: {{ drawCalls }}</div>
        </div>
      </div>

      <div class="controls-panel">
        <div class="control-section">
          <h3>产品颜色</h3>
          <div class="color-options">
            <button
              v-for="color in colors"
              :key="color.name"
              class="color-btn"
              :class="{ active: selectedColor === color.name }"
              :style="{ background: color.value }"
              @click="changeColor(color)"
              :title="color.name"
            ></button>
          </div>
        </div>

        <div class="control-section">
          <h3>灯光设置</h3>
          <div class="control-item">
            <label>环境光强度</label>
            <div class="slider-container">
              <input
                type="range"
                class="slider"
                v-model="ambientIntensity"
                min="0"
                max="2"
                step="0.1"
                @input="updateLighting"
              >
              <span class="slider-value">{{ ambientIntensity }}</span>
            </div>
          </div>
          <div class="control-item">
            <label>点光源强度</label>
            <div class="slider-container">
              <input
                type="range"
                class="slider"
                v-model="pointIntensity"
                min="0"
                max="3"
                step="0.1"
                @input="updateLighting"
              >
              <span class="slider-value">{{ pointIntensity }}</span>
            </div>
          </div>
        </div>

        <div class="control-section">
          <h3>场景控制</h3>
          <button class="btn btn-primary" @click="toggleRotation">
            {{ autoRotate ? '停止旋转' : '自动旋转' }}
          </button>
          <button class="btn btn-secondary" @click="resetCamera">
            重置视角
          </button>
          <button class="btn btn-secondary" @click="toggleStats">
            {{ showStats ? '隐藏' : '显示' }}性能统计
          </button>
        </div>

        <div class="control-section">
          <h3>关于</h3>
          <p style="color: #666; font-size: 13px; line-height: 1.6;">
            这是一个基于Three.js的3D产品展示系统示例。支持产品360度查看、材质实时切换、灯光调节等功能。
          </p>
        </div>
      </div>
    </div>
  </div>

  <script>
    const { createApp } = Vue;
    const { Scene, PerspectiveCamera, WebGLRenderer, AmbientLight, PointLight,
            MeshStandardMaterial, BoxGeometry, Mesh, Color } = THREE;

    createApp({
      data() {
        return {
          loading: true,
          loadingProgress: 0,
          scene: null,
          camera: null,
          renderer: null,
          controls: null,
          product: null,
          ambientLight: null,
          pointLight: null,
          animationId: null,

          // 控制参数
          colors: [
            { name: '黑色', value: '#2c3e50' },
            { name: '白色', value: '#ecf0f1' },
            { name: '蓝色', value: '#3498db' },
            { name: '红色', value: '#e74c3c' },
            { name: '绿色', value: '#2ecc71' },
            { name: '黄色', value: '#f39c12' },
            { name: '紫色', value: '#9b59b6' },
            { name: '粉色', value: '#fd79a8' }
          ],
          selectedColor: '黑色',
          ambientIntensity: 0.8,
          pointIntensity: 1.5,
          autoRotate: true,
          showStats: true,

          // 性能统计
          fps: 0,
          triangles: 0,
          drawCalls: 0,
          lastTime: performance.now(),
          frames: 0
        };
      },

      mounted() {
        this.initScene();
        this.createProduct();
        this.animate();
        window.addEventListener('resize', this.onWindowResize);

        // 模拟加载进度
        const progressInterval = setInterval(() => {
          this.loadingProgress += 10;
          if (this.loadingProgress >= 100) {
            this.loadingProgress = 100;
            setTimeout(() => {
              this.loading = false;
            }, 300);
            clearInterval(progressInterval);
          }
        }, 100);
      },

      beforeUnmount() {
        window.removeEventListener('resize', this.onWindowResize);
        if (this.animationId) {
          cancelAnimationFrame(this.animationId);
        }
        this.dispose();
      },

      methods: {
        initScene() {
          const container = document.getElementById('canvas-container');
          const width = container.clientWidth;
          const height = container.clientHeight;

          // 创建场景
          this.scene = new Scene();
          this.scene.background = new Color(0xf0f0f0);

          // 创建相机
          this.camera = new PerspectiveCamera(45, width / height, 0.1, 1000);
          this.camera.position.set(5, 3, 5);
          this.camera.lookAt(0, 0, 0);

          // 创建渲染器
          this.renderer = new WebGLRenderer({ antialias: true });
          this.renderer.setSize(width, height);
          this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
          this.renderer.shadowMap.enabled = true;
          container.appendChild(this.renderer.domElement);

          // 创建控制器
          this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
          this.controls.enableDamping = true;
          this.controls.dampingFactor = 0.05;
          this.controls.autoRotate = this.autoRotate;
          this.controls.autoRotateSpeed = 2;

          // 创建灯光
          this.ambientLight = new AmbientLight(0xffffff, this.ambientIntensity);
          this.scene.add(this.ambientLight);

          this.pointLight = new PointLight(0xffffff, this.pointIntensity, 100);
          this.pointLight.position.set(5, 5, 5);
          this.pointLight.castShadow = true;
          this.scene.add(this.pointLight);

          // 添加第二个补光
          const fillLight = new PointLight(0xffffff, 0.5, 100);
          fillLight.position.set(-5, 3, -5);
          this.scene.add(fillLight);
        },

        createProduct() {
          // 创建产品模型 - 这里用简单的几何体模拟手机
          const phoneGroup = new THREE.Group();

          // 手机主体
          const bodyGeometry = new BoxGeometry(1, 2, 0.1);
          const bodyMaterial = new MeshStandardMaterial({
            color: 0x2c3e50,
            metalness: 0.7,
            roughness: 0.3
          });
          const body = new Mesh(bodyGeometry, bodyMaterial);
          body.castShadow = true;
          body.receiveShadow = true;
          phoneGroup.add(body);

          // 屏幕
          const screenGeometry = new BoxGeometry(0.9, 1.8, 0.02);
          const screenMaterial = new MeshStandardMaterial({
            color: 0x1a1a1a,
            metalness: 0.9,
            roughness: 0.1
          });
          const screen = new Mesh(screenGeometry, screenMaterial);
          screen.position.z = 0.06;
          phoneGroup.add(screen);

          // 摄像头
          const cameraGeometry = new THREE.CylinderGeometry(0.08, 0.08, 0.05, 32);
          const cameraMaterial = new MeshStandardMaterial({
            color: 0x111111,
            metalness: 0.8,
            roughness: 0.2
          });
          const camera = new Mesh(cameraGeometry, cameraMaterial);
          camera.rotation.x = Math.PI / 2;
          camera.position.set(-0.3, 0.7, -0.08);
          phoneGroup.add(camera);

          this.product = phoneGroup;
          this.product.userData.bodyMaterial = bodyMaterial;
          this.scene.add(phoneGroup);
        },

        changeColor(color) {
          this.selectedColor = color.name;
          if (this.product && this.product.userData.bodyMaterial) {
            this.product.userData.bodyMaterial.color.set(color.value);
          }
        },

        updateLighting() {
          if (this.ambientLight) {
            this.ambientLight.intensity = parseFloat(this.ambientIntensity);
          }
          if (this.pointLight) {
            this.pointLight.intensity = parseFloat(this.pointIntensity);
          }
        },

        toggleRotation() {
          this.autoRotate = !this.autoRotate;
          if (this.controls) {
            this.controls.autoRotate = this.autoRotate;
          }
        },

        resetCamera() {
          if (this.camera && this.controls) {
            this.camera.position.set(5, 3, 5);
            this.controls.target.set(0, 0, 0);
            this.controls.update();
          }
        },

        toggleStats() {
          this.showStats = !this.showStats;
        },

        updateStats() {
          this.frames++;
          const currentTime = performance.now();

          if (currentTime >= this.lastTime + 1000) {
            this.fps = Math.round((this.frames * 1000) / (currentTime - this.lastTime));
            this.frames = 0;
            this.lastTime = currentTime;

            if (this.renderer && this.scene && this.camera) {
              const info = this.renderer.info;
              this.triangles = info.render.triangles;
              this.drawCalls = info.render.calls;
            }
          }
        },

        animate() {
          this.animationId = requestAnimationFrame(this.animate);

          if (this.controls) {
            this.controls.update();
          }

          if (this.renderer && this.scene && this.camera) {
            this.renderer.render(this.scene, this.camera);
          }

          this.updateStats();
        },

        onWindowResize() {
          const container = document.getElementById('canvas-container');
          if (!container) return;

          const width = container.clientWidth;
          const height = container.clientHeight;

          if (this.camera) {
            this.camera.aspect = width / height;
            this.camera.updateProjectionMatrix();
          }

          if (this.renderer) {
            this.renderer.setSize(width, height);
          }
        },

        dispose() {
          // 清理资源
          if (this.scene) {
            this.scene.traverse((object) => {
              if (object.geometry) {
                object.geometry.dispose();
              }
              if (object.material) {
                if (Array.isArray(object.material)) {
                  object.material.forEach(material => material.dispose());
                } else {
                  object.material.dispose();
                }
              }
            });
          }

          if (this.renderer) {
            this.renderer.dispose();
          }

          if (this.controls) {
            this.controls.dispose();
          }
        }
      }
    }).mount('#app');
  </script>
</body>
</html>