【Web】使用Vue3+PlayCanvas开发3D游戏(十二)渲染PCD点云可视化模型

文章目录

前一篇《【Web】使用Vue3+PlayCanvas开发3D游戏(十一)渲染3D高斯泼溅效果

一、效果

1.1、动态图片

csdn文件大小限制,只能压缩成小图,剪辑上传

1.2、静图

二、内容介绍

2.1、前言

点云数据(PCD)是 3D 可视化领域中重要的数据格式,广泛应用于激光扫描、三维重建、自动驾驶等场景。相比于 PLY 格式,PCD 更专注于点云数据的存储与传输,本文将基于 Vue3 + Three.js 技术栈,实现 PCD 点云模型的加载、渲染,并结合高斯泼溅(Gaussian Splatting)核心思路优化点云的视觉呈现效果,让点云数据展示更具立体感和真实感。

2.2、简介

本文基于 Vue3 + Three.js 实现了 PCD 点云模型的完整渲染流程,并结合高斯泼溅的核心思路优化了点云的视觉呈现效果。相比于传统的点云渲染,高斯泼溅效果让点云数据更具立体感和真实感,可直接应用于激光扫描、自动驾驶可视化、三维重建等业务场景。

2.3、PCD与PLY区别

PCD 是专为点云设计的专用格式(PCL/ROS 生态),上一篇文章《【Web】使用Vue3+PlayCanvas开发3D游戏(十一)渲染3D高斯泼溅效果》用的3D 高斯泼溅(3DGS)常用它存点 + 颜色 / 强度;PLY 是斯坦福的通用 3D 格式,既能存点云,也能存三角网格,3DGS 现在主流用 PLY 存完整高斯参数(位置、旋转、缩放、颜色、球谐),兼容性更强。

维度 PCD PLY
设计目标 纯点云专用,PCL/ROS 生态 通用 3D,点云 + 网格 + 自定义属性
3DGS 适配 仅存基础点 (x/y/z/rgb),不支持高斯专属参数 完整存 3DGS 全参数(旋转、缩放、球谐、透明度)
字段 固定 / 半固定,适合点云算法 完全自定义,想加什么属性就加什么
兼容性 主要在 PCL/ROS/ 激光雷达工具 全 3D 软件 / 引擎 / 渲染器通用
典型用途 激光雷达点云、SLAM、PCL 算法 3D 扫描、3DGS 模型、网格交换、3D 打印

三、技术栈与核心依赖

3.1、核心技术选型

Vue3:前端组件化开发框架,负责页面结构与状态管理

Three.js:WebGL 3D 图形库,核心负责 3D 场景、相机、渲染器的构建

PCDLoader:Three.js 官方扩展的 PCD 文件加载器,解析 PCD 点云数据

OrbitControls:Three.js 轨道控制器,实现模型的交互控制(旋转、缩放、平移)

3.2、依赖安装

bash 复制代码
npm install three@0.183.2
npm install three-orbit-controls
npm i --save three-css2drender

四、PCD 点云渲染核心实现

4.1、页面结构设计(Vue 组件模板)

核心分为加载状态区、3D 渲染容器、点云信息面板三部分:

html 复制代码
<template>
  <div style="height: 100%; width: 100%; position: relative;">
    <!-- 加载中状态 -->
    <div v-if="loading" class="loader-container">
      <div class="loader"></div>
      <p class="loading-text">正在加载点云模型...{{ loadingProgress }}%</p>
    </div>
    <!-- Three.js渲染容器 -->
    <div id="three" style="height: 100%; width: 100%"></div>
    <!-- 点云信息面板 -->
    <div v-if="pcdInfo.visible" class="pcd-info-panel">
      <h4>点云模型信息</h4>
      <ul>
        <li><strong>文件路径:</strong>{{ pcdInfo.filePath }}</li>
        <li><strong>文件格式:</strong>{{ pcdInfo.fileFormat }}</li>
        <li><strong>点的数量:</strong>{{ pcdInfo.pointCount.toLocaleString() }}</li>
        <li><strong>加载时间:</strong>{{ pcdInfo.loadTime }}ms</li>
        <li><strong>模型最大维度:</strong>{{ pcdInfo.maxDimension.toFixed(2) }}m</li>
      </ul>
    </div>
  </div>
</template>

4.2、核心逻辑实现(Script 部分)

(1)初始化全局缓存与状态

js 复制代码
<script>
import * as THREE from "three";
import { PCDLoader } from "three/examples/jsm/loaders/PCDLoader.js";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";

// 缓存Three.js核心实例,避免重复创建
let threeCache = {
  scene: null,
  camera: null,
  renderer: null,
  loader: null,
  controls: null,
  animationId: null,
  elem: null,
};

// 帧率控制
const clock = new THREE.Clock();
const FPS = 30;
const renderT = 1 / FPS;
let timeS = 0;

export default {
  data() {
    return {
      loading: true,
      loadingProgress: 0,
      pcdInfo: {
        visible: false,
        filePath: "",
        fileFormat: "PCD (Point Cloud Data)",
        pointCount: 0,
        loadTime: 0,
        maxDimension: 0,
      },
    };
  },
  // 组件销毁前清理资源
  beforeDestroy() {
    this.destroyModel();
  },
  mounted() {
    const startTime = Date.now();
    // 加载指定PCD文件
    this.initModel(`/pcd/src_location_match_fusion_pcds_output (2).pcd`, "three", startTime);
  },

(2)初始化 3D 场景与 PCD 加载

js 复制代码
  methods: {
    /**
     * 初始化3D场景并加载PCD模型
     * @param {string} pcdPath PCD文件路径
     * @param {string} domName 渲染容器DOMID
     * @param {number} startTime 加载开始时间戳
     */
    initModel(pcdPath, domName, startTime) {
      threeCache.elem = document.getElementById(domName);
      this.pcdInfo.filePath = pcdPath;

      // 1. 创建相机
      threeCache.camera = new THREE.PerspectiveCamera(
        30, // 视角
        threeCache.elem.clientWidth / threeCache.elem.clientHeight, // 宽高比
        0.1, // 近裁切面
        1000 // 远裁切面
      );

      // 2. 创建渲染器
      threeCache.renderer = new THREE.WebGLRenderer({
        antialias: true, // 抗锯齿
        alpha: true, // 透明背景
      });
      threeCache.renderer.setClearColor(new THREE.Color(0x303030)); // 背景色
      threeCache.renderer.setSize(threeCache.elem.clientWidth, threeCache.elem.clientHeight);
      threeCache.elem.appendChild(threeCache.renderer.domElement);

      // 3. 创建场景
      threeCache.scene = new THREE.Scene();
      threeCache.loader = new PCDLoader();

      // 4. 加载PCD文件
      threeCache.loader.load(
        pcdPath,
        // 加载成功回调
        (points) => {
          this.pcdInfo.loadTime = Date.now() - startTime;

          // 关键:启用PCD自带顶点颜色
          points.material.vertexColors = true;

          // ========== 高斯泼溅效果核心优化 ==========
          // 1. 替换点材质为粒子材质,模拟高斯泼溅的雾化效果
          const gaussianMaterial = new THREE.PointsMaterial({
            size: 0.05, // 粒子大小(根据点云尺度调整)
            vertexColors: true, // 保留原始顶点颜色
            transparent: true, // 开启透明
            opacity: 0.8, // 透明度
            blending: THREE.AdditiveBlending, // 加法混合,增强泼溅的叠加效果
            depthWrite: false, // 关闭深度写入,避免粒子遮挡问题
            sizeAttenuation: true, // 开启尺寸衰减(近大远小)
          });
          points.material = gaussianMaterial;

          threeCache.scene.add(points);

          // 计算点云包围盒,居中模型
          const boundingBox = new THREE.Box3().setFromObject(points);
          const middle = new THREE.Vector3();
          boundingBox.getCenter(middle);
          points.applyMatrix4(new THREE.Matrix4().makeTranslation(-middle.x, -middle.y, -middle.z));

          // 计算模型最大维度,调整相机位置
          const size = new THREE.Vector3();
          boundingBox.getSize(size);
          const maxDimension = Math.max(size.x, size.y, size.z);

          // 更新点云信息
          this.pcdInfo.pointCount = points.geometry.attributes.position.count;
          this.pcdInfo.maxDimension = maxDimension;
          this.pcdInfo.visible = true;

          // 调整相机位置,适配模型尺度
          threeCache.camera.position.y = maxDimension * 1;

          // 创建轨道控制器,实现交互
          threeCache.controls = new OrbitControls(threeCache.camera, threeCache.renderer.domElement);
          threeCache.controls.enableDamping = true; // 阻尼效果,交互更丝滑
          threeCache.controls.dampingFactor = 0.05;

          this.loading = false;
          this.animate(); // 启动渲染循环
        },
        // 加载进度回调
        (xhr) => {
          this.loadingProgress = Math.floor((xhr.loaded / xhr.total) * 100);
        },
        // 加载失败回调
        (error) => {
          this.loading = false;
          console.error("PCD加载失败:", error);
        }
      );
    },

(3)渲染循环与资源销毁

js 复制代码
    /**
     * 渲染循环(帧率控制)
     */
    animate() {
      threeCache.animationId = requestAnimationFrame(() => this.animate());
      const T = clock.getDelta();
      timeS += T;
      // 固定FPS渲染,避免性能浪费
      if (timeS > renderT) {
        threeCache.renderer?.render(threeCache.scene, threeCache.camera);
        timeS = 0;
      }
      // 更新控制器
      if (threeCache.controls) threeCache.controls.update();
    },

    /**
     * 销毁3D资源,避免内存泄漏
     */
    destroyModel() {
      try {
        if (threeCache.controls) { 
          threeCache.controls.dispose(); 
          threeCache.controls = null; 
        }
        if (threeCache.scene) { 
          threeCache.scene.clear(); 
          threeCache.scene = null; 
        }
        if (threeCache.renderer) {
          threeCache.renderer.dispose();
          threeCache.renderer.forceContextLoss();
          const gl = threeCache.renderer.domElement.getContext("webgl");
          gl?.getExtension("WEBGL_lose_context")?.loseContext();
          threeCache.renderer.domElement.parentNode?.removeChild(threeCache.renderer.domElement);
          threeCache.renderer = null;
        }
        if (threeCache.animationId) { 
          cancelAnimationFrame(threeCache.animationId); 
          threeCache.animationId = null; 
        }
        threeCache.camera = null;
        threeCache.loader = null;
        threeCache.elem = null;
        // 重置状态
        this.loading = true;
        this.loadingProgress = 0;
        this.pcdInfo = { 
          visible: false, 
          filePath: "", 
          fileFormat: "PCD", 
          pointCount: 0, 
          loadTime: 0, 
          maxDimension: 0 
        };
      } catch (e) { 
        console.error("资源销毁失败", e); 
      }
    },
  },
};
</script>

4.3、样式美化(Scoped CSS)

css 复制代码
<style scoped>
/* 加载动画样式 */
.loader-container {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(48, 48, 48, 0.9);
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  z-index: 1000;
}
.loader {
  width: 60px;
  height: 60px;
  border: 8px solid #fff;
  border-top: 8px solid #00ffff;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}
.loading-text {
  color: #fff;
  margin-top: 15px;
}
@keyframes spin {
  0% { transform: rotate(0); }
  100% { transform: rotate(360deg); }
}

/* 点云信息面板样式 */
.pcd-info-panel {
  position: absolute;
  top: 20px;
  right: 20px;
  background: rgba(0,0,0,0.7);
  color: #fff;
  padding: 15px;
  border-radius: 8px;
  z-index: 999;
}
.pcd-info-panel h4 {
  color: #00ffff;
  margin: 0 0 10px 0;
  border-bottom: 1px solid #00ffff;
  padding-bottom: 5px;
}
.pcd-info-panel ul {
  list-style: none;
  padding: 0;
  margin: 0;
}
.pcd-info-panel li {
  margin: 4px 0;
}
.pcd-info-panel strong {
  color: #00ffff;
}
</style>

五、高斯泼溅效果核心优化解析

高斯泼溅(Gaussian Splatting)的核心是通过「粒子雾化 + 颜色混合」模拟高斯分布的视觉效果,本文针对 PCD 点云的优化关键点:

5.1、材质替换

将默认的点材质替换为THREE.PointsMaterial,并开启以下关键配置:

  • sizeAttenuation: true:实现粒子近大远小的透视效果,贴合真实物理规律;
  • AdditiveBlending:加法混合模式,让相邻粒子的颜色叠加,形成类似高斯泼溅的雾化效果;
  • transparent: true + opacity: 0.8:半透明效果,减少粒子的生硬感,增强层次感。

5.2、深度写入控制

设置depthWrite: false,避免粒子之间的深度遮挡问题,让密集点云区域的叠加效果更自然,符合高斯泼溅「模糊雾化」的视觉特征。

5.3、粒子尺寸适配

size: 0.05需根据点云的实际尺度调整:

  • 若点云尺度较大(如自动驾驶点云,单位为米),可设置size: 0.1~0.5;
  • 若为小尺度点云(如零件扫描),可设置size: 0.01~0.05。

六、交互与性能优化

  1. 轨道控制器优化
    启用enableDamping: true和dampingFactor: 0.05,让模型旋转 / 平移时带有阻尼效果,交互更丝滑,避免生硬的瞬间移动。
  2. 帧率控制
    通过clock.getDelta()计算帧间隔,固定 30FPS 渲染,避免高刷新率下的性能浪费,兼顾流畅度与性能。
  3. 资源销毁
    组件销毁时彻底清理 Three.js 实例(场景、渲染器、控制器、动画帧),并释放 WebGL 上下文,避免内存泄漏。

七、核心源码

html 复制代码
<template>
  <div style="height: 100%; width: 100%; position: relative;">
    <div v-if="loading" class="loader-container">
      <div class="loader"></div>
      <p class="loading-text">正在加载点云模型...{{ loadingProgress }}%</p>
    </div>
    <div id="three" style="height: 100%; width: 100%"></div>
    <div v-if="pcdInfo.visible" class="pcd-info-panel">
      <h4>点云模型信息</h4>
      <ul>
        <li><strong>文件路径:</strong>{{ pcdInfo.filePath }}</li>
        <li><strong>文件格式:</strong>{{ pcdInfo.fileFormat }}</li>
        <li><strong>点的数量:</strong>{{ pcdInfo.pointCount.toLocaleString() }}</li>
        <li><strong>加载时间:</strong>{{ pcdInfo.loadTime }}ms</li>
        <li><strong>模型最大维度:</strong>{{ pcdInfo.maxDimension.toFixed(2) }}m</li>
      </ul>
    </div>
  </div>
</template>

<script>
import * as THREE from "three";
import { PCDLoader } from "three/examples/jsm/loaders/PCDLoader.js";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";

let threeCache = {
  scene: null,
  camera: null,
  renderer: null,
  loader: null,
  controls: null,
  animationId: null,
  elem: null,
};

const clock = new THREE.Clock();
const FPS = 30;
const renderT = 1 / FPS;
let timeS = 0;

export default {
  data() {
    return {
      loading: true,
      loadingProgress: 0,
      pcdInfo: {
        visible: false,
        filePath: "",
        fileFormat: "PCD (Point Cloud Data)",
        pointCount: 0,
        loadTime: 0,
        maxDimension: 0,
      },
    };
  },
  beforeDestroy() {
    this.destroyModel();
  },
  mounted() {
    const startTime = Date.now();
    // /pcd/map.pcd
    // /pcd/src_location_match_fusion_pcds_output (2).pcd
    // /pcd/src_location_match_fusion_pcds_output_global (2).pcd
    this.initModel(`/pcd/000022.pcd`, "three", startTime);
  },
  methods: {
    initModel(pcdPath, domName, startTime) {
      threeCache.elem = document.getElementById(domName);
      this.pcdInfo.filePath = pcdPath;

      threeCache.camera = new THREE.PerspectiveCamera(
        30,
        threeCache.elem.clientWidth / threeCache.elem.clientHeight,
        0.1,
        1000
      );

      threeCache.renderer = new THREE.WebGLRenderer({
        antialias: true,
        alpha: true,
      });
      threeCache.renderer.setClearColor(new THREE.Color(0x303030));
      threeCache.renderer.setSize(threeCache.elem.clientWidth, threeCache.elem.clientHeight);
      threeCache.elem.appendChild(threeCache.renderer.domElement);

      threeCache.scene = new THREE.Scene();
      threeCache.loader = new PCDLoader();

      threeCache.loader.load(
        pcdPath,
        (points) => {
          this.pcdInfo.loadTime = Date.now() - startTime;

          // ======================================
          // 关键:开启 PCD 自带颜色
          // ======================================
          points.material.vertexColors = true;

          threeCache.scene.add(points);

          const boundingBox = new THREE.Box3().setFromObject(points);
          const middle = new THREE.Vector3();
          boundingBox.getCenter(middle);
          points.applyMatrix4(new THREE.Matrix4().makeTranslation(-middle.x, -middle.y, -middle.z));

          const size = new THREE.Vector3();
          boundingBox.getSize(size);
          const maxDimension = Math.max(size.x, size.y, size.z);

          this.pcdInfo.pointCount = points.geometry.attributes.position.count;
          this.pcdInfo.maxDimension = maxDimension;
          this.pcdInfo.visible = true;

          threeCache.camera.position.y = maxDimension * 1;

          threeCache.controls = new OrbitControls(threeCache.camera, threeCache.renderer.domElement);
          threeCache.controls.enableDamping = true;
          threeCache.controls.dampingFactor = 0.05;

          this.loading = false;
          this.animate();
        },
        (xhr) => {
          this.loadingProgress = Math.floor((xhr.loaded / xhr.total) * 100);
        },
        (error) => {
          this.loading = false;
          console.error("加载失败:", error);
        }
      );
    },
    animate() {
      threeCache.animationId = requestAnimationFrame(() => this.animate());
      const T = clock.getDelta();
      timeS += T;
      if (timeS > renderT) {
        threeCache.renderer?.render(threeCache.scene, threeCache.camera);
        timeS = 0;
      }
      if (threeCache.controls) threeCache.controls.update();
    },
    destroyModel() {
      try {
        if (threeCache.controls) { threeCache.controls.dispose(); threeCache.controls = null; }
        if (threeCache.scene) { threeCache.scene.clear(); threeCache.scene = null; }
        if (threeCache.renderer) {
          threeCache.renderer.dispose();
          threeCache.renderer.forceContextLoss();
          const gl = threeCache.renderer.domElement.getContext("webgl");
          gl?.getExtension("WEBGL_lose_context")?.loseContext();
          threeCache.renderer.domElement.parentNode?.removeChild(threeCache.renderer.domElement);
          threeCache.renderer = null;
        }
        if (threeCache.animationId) { cancelAnimationFrame(threeCache.animationId); threeCache.animationId = null; }
        threeCache.camera = null;
        threeCache.loader = null;
        threeCache.elem = null;
        this.loading = true;
        this.loadingProgress = 0;
        this.pcdInfo = { visible: false, filePath: "", fileFormat: "PCD", pointCount: 0, loadTime: 0, maxDimension: 0 };
      } catch (e) { console.error("销毁失败", e); }
    },
  },
};
</script>

<style scoped>
.loader-container {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(48, 48, 48, 0.9);
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  z-index: 1000;
}
.loader {
  width: 60px;
  height: 60px;
  border: 8px solid #fff;
  border-top: 8px solid #00ffff;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}
.loading-text {
  color: #fff;
  margin-top: 15px;
}
@keyframes spin {
  0% { transform: rotate(0); }
  100% { transform: rotate(360deg); }
}
.pcd-info-panel {
  position: absolute;
  top: 20px;
  right: 20px;
  background: rgba(0,0,0,0.7);
  color: #fff;
  padding: 15px;
  border-radius: 8px;
  z-index: 999;
}
.pcd-info-panel h4 {
  color: #00ffff;
  margin: 0 0 10px 0;
  border-bottom: 1px solid #00ffff;
  padding-bottom: 5px;
}
.pcd-info-panel ul {
  list-style: none;
  padding: 0;
  margin: 0;
}
.pcd-info-panel li {
  margin: 4px 0;
}
.pcd-info-panel strong {
  color: #00ffff;
}
</style>
相关推荐
ZC跨境爬虫2 小时前
3D 地球卫星轨道可视化平台开发 Day9(AI阈值调控+小众卫星识别+低Token测试模式实战)
人工智能·python·3d·信息可视化·json
ZC跨境爬虫3 小时前
3D 地球卫星轨道可视化平台开发 Day6(SEC数据接口扩展实现)
前端·microsoft·3d·html·json·交互
ZC跨境爬虫5 小时前
3D 地球卫星轨道可视化平台开发 Day5(简介接口对接+规划AI自动化卫星数据生成工作流)
前端·人工智能·3d·ai·自动化
a1117767 小时前
演唱会3D选座网页(HTML 开源)
前端·3d·html
ZC跨境爬虫7 小时前
3D 地球卫星轨道可视化平台开发 Day10(交互升级与接口溯源)
前端·javascript·3d·自动化·交互
大模型实验室Lab4AI8 小时前
算力赋能三维视觉创新,Lab4AI亮相 China3DV 2026
3d
devil-J1 天前
vue3+three.js中国3D地图
开发语言·javascript·3d
m0_743106461 天前
【浙大&南洋理工最新综述】Feed-Forward 3D Scene Modeling(二)
人工智能·算法·计算机视觉·3d·几何学