WebGL+Three.js:打造网页3D模型展厅

文章目录


前言

随着Web技术的飞速发展,3D可视化已不再是游戏和影视领域的专属。如今,通过WebGL技术,我们可以在浏览器中直接渲染复杂的3D模型,为用户带来身临其境的沉浸式体验。Three.js作为最流行的WebGL库,将复杂的3D图形学概念封装成简洁易用的API,让开发者无需深入了解底层细节就能创建惊艳的3D场景。


提示:以下是本篇文章正文内容,下面案例可供参考

一、完整代码

html 复制代码
<!DOCTYPE html>
<html>

<head>
  <title>3D模型展示器</title>
  <style>
    body {
      margin: 0;
      font-family: Arial, sans-serif;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    }

    #container {
      width: 100vw;
      height: 100vh;
    }

    .controls-panel {
      position: fixed;
      top: 20px;
      left: 20px;
      background: rgba(255, 255, 255, 0.9);
      padding: 20px;
      border-radius: 10px;
      box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
      z-index: 1000;
      width: 300px;
    }

    .controls-panel h2 {
      margin-top: 0;
      color: #333;
    }

    .control-group {
      margin: 15px 0;
    }

    label {
      display: block;
      margin-bottom: 5px;
      color: #555;
      font-weight: bold;
    }

    input[type="range"],
    select,
    button {
      width: 100%;
      padding: 8px;
      margin-bottom: 10px;
      border: 1px solid #ddd;
      border-radius: 4px;
    }

    button {
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      border: none;
      cursor: pointer;
      font-weight: bold;
      transition: transform 0.2s;
    }

    button:hover {
      transform: translateY(-2px);
    }

    .model-info {
      background: rgba(0, 0, 0, 0.8);
      color: white;
      padding: 10px;
      border-radius: 5px;
      font-size: 14px;
      position: fixed;
      bottom: 20px;
      left: 20px;
      z-index: 1000;
    }

    .loading {
      position: fixed;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      color: white;
      font-size: 20px;
      z-index: 1001;
    }
  </style>
</head>

<body>
  <!-- 加载提示 -->
  <div class="loading" id="loading">加载3D模型中...</div>

  <!-- 3D渲染容器 -->
  <div id="container"></div>

  <!-- 控制面板 -->
  <div class="controls-panel">
    <h2>3D模型控制器</h2>

    <div class="control-group">
      <label>模型选择</label>
      <select id="modelSelect">
        <option value="duck">示例鸭子</option>
        <option value="robot">机器人</option>
        <option value="car">汽车</option>
        <option value="custom">自定义模型</option>
      </select>
    </div>

    <div class="control-group">
      <label>旋转速度</label>
      <input type="range" id="rotationSpeed" min="0" max="0.1" step="0.001" value="0.005">
    </div>

    <div class="control-group">
      <label>缩放比例</label>
      <input type="range" id="scale" min="0.1" max="5" step="0.1" value="1">
    </div>

    <div class="control-group">
      <label>背景颜色</label>
      <input type="color" id="backgroundColor" value="#f0f0f0">
    </div>

    <div class="control-group">
      <label>模型颜色</label>
      <input type="color" id="modelColor" value="#ffffff">
    </div>

    <button id="resetView">重置视图</button>
    <button id="toggleRotation">暂停旋转</button>
    <button id="toggleAxes">隐藏坐标轴</button>
    <button id="toggleGrid">隐藏网格</button>
    <button id="screenshot">截图保存</button>

    <!-- 自定义模型上传 -->
    <div class="control-group" style="margin-top: 20px;">
      <label>上传自定义模型 (GLTF/GLB格式)</label>
      <input type="file" id="modelUpload" accept=".gltf,.glb">
    </div>
  </div>

  <!-- 模型信息显示 -->
  <div class="model-info">
    <div>模型信息: <span id="modelName">加载中...</span></div>
    <div>多边形数: <span id="polyCount">-</span></div>
    <div>内存使用: <span id="memoryUsage">-</span> MB</div>
  </div>

  <!-- 引入Three.js库 -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
  <!-- GLTF加载器 -->
  <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/GLTFLoader.js"></script>
  <!-- DRACO加载器(用于压缩模型) -->
  <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/DRACOLoader.js"></script>
  <!-- 轨道控制器 -->
  <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>

  <script>
    // 初始化变量
    let scene, camera, renderer, controls, model;
    let isRotating = true;
    let rotationSpeed = 0.005;
    let axesVisible = true;
    let gridVisible = true;

    // 可用模型库
    const modelLibrary = {
      duck: 'https://cdn.jsdelivr.net/gh/mrdoob/three.js@dev/examples/models/gltf/Duck/glTF/Duck.gltf',
      robot: 'https://cdn.jsdelivr.net/gh/mrdoob/three.js@dev/examples/models/gltf/RobotExpressive/glTF/RobotExpressive.gltf',
      car: 'https://cdn.jsdelivr.net/gh/mrdoob/three.js@dev/examples/models/gltf/Lamborghini/glTF/Lamborghini.gltf'
    };

    // 初始化Three.js场景
    function init() {
      // 创建场景
      scene = new THREE.Scene();
      scene.background = new THREE.Color(0xf0f0f0);

      // 创建相机
      camera = new THREE.PerspectiveCamera(
        75,
        window.innerWidth / window.innerHeight,
        0.1,
        1000
      );
      camera.position.set(5, 5, 5);

      // 创建渲染器
      renderer = new THREE.WebGLRenderer({
        antialias: true,
        alpha: true
      });
      renderer.setSize(window.innerWidth, window.innerHeight);
      renderer.shadowMap.enabled = true;
      renderer.shadowMap.type = THREE.PCFSoftShadowMap;
      document.getElementById('container').appendChild(renderer.domElement);

      // 添加光源
      addLights();

      // 添加轨道控制器
      controls = new THREE.OrbitControls(camera, renderer.domElement);
      controls.enableDamping = true;
      controls.dampingFactor = 0.05;
      controls.screenSpacePanning = false;
      controls.maxPolarAngle = Math.PI;
      controls.minDistance = 1;
      controls.maxDistance = 50;

      // 加载初始模型
      loadModel(modelLibrary.duck, '示例鸭子');

      // 添加辅助器
      addHelpers();

      // 开始动画循环
      animate();

      // 添加事件监听
      setupEventListeners();

      // 隐藏加载提示
      setTimeout(() => {
        document.getElementById('loading').style.display = 'none';
      }, 1000);
    }

    // 添加光源
    function addLights() {
      // 环境光
      const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
      scene.add(ambientLight);

      // 方向光
      const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
      directionalLight.position.set(10, 20, 5);
      directionalLight.castShadow = true;
      directionalLight.shadow.camera.left = -10;
      directionalLight.shadow.camera.right = 10;
      directionalLight.shadow.camera.top = 10;
      directionalLight.shadow.camera.bottom = -10;
      scene.add(directionalLight);

      // 点光源
      const pointLight = new THREE.PointLight(0xffffff, 0.5);
      pointLight.position.set(-10, 10, -5);
      scene.add(pointLight);
    }

    // 添加辅助器
    function addHelpers() {
      // 坐标轴辅助器
      const axesHelper = new THREE.AxesHelper(5);
      axesHelper.name = 'axesHelper';
      scene.add(axesHelper);

      // 网格辅助器
      const gridHelper = new THREE.GridHelper(20, 20, 0x888888, 0x444444);
      gridHelper.name = 'gridHelper';
      scene.add(gridHelper);
    }

    // 加载3D模型
    function loadModel(url, modelName) {
      const loader = new THREE.GLTFLoader();

      // 设置DRACO解码器路径(用于压缩模型)
      const dracoLoader = new THREE.DRACOLoader();
      dracoLoader.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/');
      loader.setDRACOLoader(dracoLoader);

      // 显示加载提示
      document.getElementById('loading').style.display = 'block';

      // 移除旧模型
      if (model) {
        scene.remove(model);
        // 清理旧模型的材质和几何体
        model.traverse(child => {
          if (child.isMesh) {
            child.geometry.dispose();
            child.material.dispose();
          }
        });
      }

      loader.load(
        url,

        // 加载成功回调
        function (gltf) {
          model = gltf.scene;

          // 设置模型属性
          model.scale.set(2, 2, 2);
          model.position.set(0, 0, 0);

          // 启用阴影
          model.traverse(child => {
            if (child.isMesh) {
              child.castShadow = true;
              child.receiveShadow = true;
            }
          });

          scene.add(model);

          // 更新模型信息
          updateModelInfo(modelName);

          // 隐藏加载提示
          document.getElementById('loading').style.display = 'none';

          console.log(`${modelName} 加载成功!`);
        },

        // 加载进度回调
        function (xhr) {
          const percentComplete = (xhr.loaded / xhr.total * 100);
          document.getElementById('loading').innerHTML =
            `加载3D模型中... ${percentComplete.toFixed(1)}%`;
        },

        // 加载错误回调
        function (error) {
          console.error('模型加载失败:', error);
          document.getElementById('loading').innerHTML =
            '模型加载失败,正在显示备用模型...';

          // 显示备用立方体
          showFallbackModel();
          updateModelInfo('备用模型');
        }
      );
    }

    // 显示备用模型
    function showFallbackModel() {
      const geometry = new THREE.BoxGeometry(2, 2, 2);
      const material = new THREE.MeshPhongMaterial({
        color: 0x00ff00,
        shininess: 100
      });

      model = new THREE.Mesh(geometry, material);
      model.castShadow = true;
      scene.add(model);

      setTimeout(() => {
        document.getElementById('loading').style.display = 'none';
      }, 1000);
    }

    // 更新模型信息
    function updateModelInfo(name) {
      document.getElementById('modelName').textContent = name;

      // 计算多边形数量
      let polyCount = 0;
      if (model) {
        model.traverse(child => {
          if (child.isMesh && child.geometry) {
            polyCount += child.geometry.attributes.position.count / 3;
          }
        });
      }
      document.getElementById('polyCount').textContent = polyCount.toLocaleString();

      // 估算内存使用(粗略估算)
      const memoryMB = (polyCount * 32) / (1024 * 1024);
      document.getElementById('memoryUsage').textContent = memoryMB.toFixed(2);
    }

    // 设置事件监听
    function setupEventListeners() {
      // 模型选择
      document.getElementById('modelSelect').addEventListener('change', function (e) {
        const selectedModel = e.target.value;
        if (selectedModel === 'custom') {
          document.getElementById('modelUpload').click();
        } else if (modelLibrary[selectedModel]) {
          loadModel(modelLibrary[selectedModel], selectedModel);
        }
      });

      // 自定义模型上传
      document.getElementById('modelUpload').addEventListener('change', function (e) {
        const file = e.target.files[0];
        if (file) {
          const url = URL.createObjectURL(file);
          loadModel(url, '自定义模型');
        }
      });

      // 旋转速度控制
      document.getElementById('rotationSpeed').addEventListener('input', function (e) {
        rotationSpeed = parseFloat(e.target.value);
      });

      // 缩放控制
      document.getElementById('scale').addEventListener('input', function (e) {
        if (model) {
          const scale = parseFloat(e.target.value);
          model.scale.set(scale, scale, scale);
        }
      });

      // 背景颜色控制
      document.getElementById('backgroundColor').addEventListener('input', function (e) {
        scene.background = new THREE.Color(e.target.value);
      });

      // 模型颜色控制
      document.getElementById('modelColor').addEventListener('input', function (e) {
        if (model) {
          model.traverse(child => {
            if (child.isMesh && child.material) {
              child.material.color.set(e.target.value);
            }
          });
        }
      });

      // 重置视图
      document.getElementById('resetView').addEventListener('click', function () {
        controls.reset();
        camera.position.set(5, 5, 5);
        if (model) {
          model.rotation.set(0, 0, 0);
        }
      });

      // 切换旋转
      document.getElementById('toggleRotation').addEventListener('click', function () {
        isRotating = !isRotating;
        this.textContent = isRotating ? '暂停旋转' : '恢复旋转';
      });

      // 切换坐标轴
      document.getElementById('toggleAxes').addEventListener('click', function () {
        axesVisible = !axesVisible;
        const axesHelper = scene.getObjectByName('axesHelper');
        if (axesHelper) {
          axesHelper.visible = axesVisible;
        }
        this.textContent = axesVisible ? '隐藏坐标轴' : '显示坐标轴';
      });

      // 切换网格
      document.getElementById('toggleGrid').addEventListener('click', function () {
        gridVisible = !gridVisible;
        const gridHelper = scene.getObjectByName('gridHelper');
        if (gridHelper) {
          gridHelper.visible = gridVisible;
        }
        this.textContent = gridVisible ? '隐藏网格' : '显示网格';
      });

      // 截图保存
      document.getElementById('screenshot').addEventListener('click', function () {
        renderer.render(scene, camera);
        const dataURL = renderer.domElement.toDataURL('image/png');

        const link = document.createElement('a');
        link.download = '3d-model-screenshot.png';
        link.href = dataURL;
        link.click();

        alert('截图已保存!');
      });

      // 窗口大小调整
      window.addEventListener('resize', function () {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
      });

      // 键盘控制
      window.addEventListener('keydown', function (event) {
        if (!model) return;

        switch (event.key) {
          case 'ArrowUp':
            model.position.y += 0.5;
            break;
          case 'ArrowDown':
            model.position.y -= 0.5;
            break;
          case 'ArrowLeft':
            model.position.x -= 0.5;
            break;
          case 'ArrowRight':
            model.position.x += 0.5;
            break;
          case ' ':
            isRotating = !isRotating;
            break;
        }
      });

      // 鼠标点击交互
      const raycaster = new THREE.Raycaster();
      const mouse = new THREE.Vector2();

      window.addEventListener('click', function (event) {
        // 计算鼠标位置
        mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
        mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

        // 更新射线
        raycaster.setFromCamera(mouse, camera);

        // 检测相交
        if (model) {
          const intersects = raycaster.intersectObject(model, true);

          if (intersects.length > 0) {
            // 点击到模型
            const intersect = intersects[0];

            // 添加点击效果
            const originalColor = intersect.object.material.color.getHex();
            intersect.object.material.color.setHex(0xff0000);

            setTimeout(() => {
              intersect.object.material.color.setHex(originalColor);
            }, 200);

            // 显示点击位置信息
            console.log('点击位置:', intersect.point);
          }
        }
      });
    }

    // 动画循环
    function animate() {
      requestAnimationFrame(animate);

      // 模型旋转
      if (model && isRotating) {
        model.rotation.y += rotationSpeed;
      }

      controls.update();
      renderer.render(scene, camera);
    }

    // 初始化应用
    init();
  </script>
</body>

</html>

总结

由于时间原因,未能一个个知识点列出来进行解析,此篇文章在于全面体验3D的可视化实现。

相关推荐
萧曵 丶5 小时前
Vue 中父子组件之间最常用的业务交互场景
javascript·vue.js·交互
Amumu121386 小时前
Vue3扩展(二)
前端·javascript·vue.js
NEXT066 小时前
JavaScript进阶:深度剖析函数柯里化及其在面试中的底层逻辑
前端·javascript·面试
牛奶8 小时前
你不知道的 JS(上):原型与行为委托
前端·javascript·编译原理
牛奶8 小时前
你不知道的JS(上):this指向与对象基础
前端·javascript·编译原理
牛奶8 小时前
你不知道的JS(上):作用域与闭包
前端·javascript·电子书
大江东去浪淘尽千古风流人物9 小时前
【SLAM】Hydra-Foundations 层次化空间感知:机器人如何像人类一样理解3D环境
深度学习·算法·3d·机器人·概率论·slam
pas13610 小时前
45-mini-vue 实现代码生成三种联合类型
前端·javascript·vue.js
颜酱11 小时前
数组双指针部分指南 (快慢·左右·倒序)
javascript·后端·算法
兆子龙11 小时前
我成了🤡, 因为不想看广告,花了40美元自己写了个鸡肋挂机脚本
android·javascript