Vue.js - 集成Three.js构建三维可视化场景教程6(加载PLY点云)
作者:hangge | 2026-05-21 08:36
在数字孪生、激光/摄影测量点云可视化场景里,点云数据(Point Cloud)是常见且关键的资产格式之一。PLY(Polygon File Format / Stanford Triangle Format)既能保存网格也常用于点云,支持顶点颜色、法线和自定义属性。本文演示如何使用 Three.js 在 Vue(Options API)项目中加载、显示 PLY 点云。


六、加载 PLY 点云
1,点云准备
我们将需要加载的 ply 点云文件放置项目的 public/models/ply 目录下。

2,样例代码
下面代码我使用 Three.js 内置的 PLYLoader 加载这个 ply 格式的飞机点云。
<template>
<!-- Three.js 渲染容器 -->
<div class="three-container" ref="container"></div>
</template>
<script>
// 核心 Three.js
import * as THREE from 'three'
// 轨道控制器
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
// PLY 模型加载器
import { PLYLoader } from 'three/examples/jsm/loaders/PLYLoader.js'
export default {
name: 'ThreeScene',
props: {
enableControls: { type: Boolean, default: true },
width: { type: Number, default: null },
height: { type: Number, default: null }
},
data() {
return {
scene: null,
camera: null,
renderer: null,
controls: null,
rafId: null,
points: null
}
},
mounted() {
this.$nextTick(() => {
this.initThree()
this.start()
window.addEventListener('resize', this.onWindowResize, { passive: true })
document.addEventListener('visibilitychange', this.onVisibilityChange)
})
},
beforeDestroy() {
this.stop()
window.removeEventListener('resize', this.onWindowResize)
document.removeEventListener('visibilitychange', this.onVisibilityChange)
this.disposeThree()
},
methods: {
/**
* 初始化 Three.js 场景
*/
initThree() {
const container = this.$refs.container
// 1) 场景
this.scene = new THREE.Scene()
this.scene.background = new THREE.Color(0x111111)
// 2) 相机
const aspect = this.getWidth() / this.getHeight()
this.camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 1000)
this.camera.position.set(0, 20, 20)
// 3) 渲染器
this.renderer = new THREE.WebGLRenderer({ antialias: true })
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2))
this.renderer.setSize(this.getWidth(), this.getHeight())
this.renderer.outputEncoding = THREE.sRGBEncoding
this.renderer.shadowMap.enabled = true
container.appendChild(this.renderer.domElement)
// 4) 光源(点云一般不需要复杂光照,但放置一盏弱环境光让点在 PBR 场景下表现合理)
this.scene.add(new THREE.AmbientLight(0xffffff, 0.6))
// 5) 控制器
if (this.enableControls) {
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
this.controls.enableDamping = true
}
// 6) 加载点云
this.loadPLY('/models/ply/plane.ply')
},
/**
* PLY点云加载方法
* @param {string} url 模型路径
*/
loadPLY(url) {
const loader = new PLYLoader()
loader.load(
url,
geometry => {
// PLYLoader 返回 BufferGeometry
// (包含 position, normal, color 等属性,取决于 PLY 文件)
geometry.computeBoundingBox()
geometry.computeBoundingSphere()
// 如果 PLY 文件没有颜色,后续我们可以按高度或其它属性生成颜色
const hasColor = geometry.getAttribute('color') != null
// PointsMaterial:支持顶点颜色和 sizeAttenuation(点随距离变化)
const size = 0.1 // 基础点大小(根据数据单位/缩放调整)
const material = new THREE.PointsMaterial({
size: size,
sizeAttenuation: true,
vertexColors: hasColor,
depthWrite: false, // 改善透明/重叠点渲染
transparent: true,
opacity: 0.9
})
// 如果没有顶点颜色,创建颜色属性(按高度做渐变示例)
if (!hasColor) {
const pos = geometry.getAttribute('position')
const count = pos.count
const colors = new Float32Array(count * 3)
const bbox = geometry.boundingBox
const minY = bbox.min.y
const maxY = bbox.max.y
const range = Math.max(1e-6, maxY - minY)
for (let i = 0; i < count; i++) {
const y = pos.getY(i)
const t = (y - minY) / range
// 简单从蓝到黄渐变,可替换为更复杂的颜色映射
const r = t * 1.0
const g = 0.5 + 0.5 * t
const b = 1.0 - t
colors[3 * i] = r
colors[3 * i + 1] = g
colors[3 * i + 2] = b
}
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3))
material.vertexColors = true
}
// 创建 THREE.Points 并加入场景
this.points = new THREE.Points(geometry, material)
// 归一化:根据包围盒缩放与居中(期望模型大致占据视野)
const bbox = geometry.boundingBox
const sizeVec = bbox.getSize(new THREE.Vector3())
const maxSize = Math.max(sizeVec.x, sizeVec.y, sizeVec.z)
const desired = 20 // 期望场景尺度(可按项目调整)
if (maxSize > 0) {
const scale = desired / maxSize
this.points.scale.setScalar(scale)
}
const center = bbox.getCenter(new THREE.Vector3())
this.points.position.set(
-center.x * this.points.scale.x,
-center.y * this.points.scale.y,
-center.z * this.points.scale.z)
// 启用渐进加载 / 分块显示等可在此扩展(本文不演示)
this.scene.add(this.points)
// 将相机缩放/对齐到点云
const sphere = geometry.boundingSphere
if (sphere) {
const r = sphere.radius * (this.points.scale.x || 1)
this.camera.position.set(0, 0, r * 3)
this.controls && this.controls.target.set(0, 0, 0)
this.controls && this.controls.update()
}
},
xhr => {
if (xhr.lengthComputable) {
console.log(`PLY 加载中: ${(xhr.loaded / xhr.total * 100).toFixed(1)}%`)
} else {
console.log(`已加载 ${xhr.loaded} 字节`)
}
},
err => {
console.error('PLY 加载失败:', err)
}
)
},
/**
* 渲染循环(模型保持静止,不旋转)
*/
animate() {
if (this.controls) this.controls.update()
this.renderer.render(this.scene, this.camera)
this.rafId = requestAnimationFrame(this.animate)
},
start() {
if (!this.rafId) this.rafId = requestAnimationFrame(this.animate)
},
stop() {
if (this.rafId) {
cancelAnimationFrame(this.rafId)
this.rafId = null
}
},
getWidth() {
return this.width || this.$refs.container.clientWidth || window.innerWidth
},
getHeight() {
return this.height || this.$refs.container.clientHeight || window.innerHeight
},
onWindowResize() {
if (!this.camera || !this.renderer) return
const w = this.getWidth()
const h = this.getHeight()
this.camera.aspect = w / h
this.camera.updateProjectionMatrix()
this.renderer.setSize(w, h)
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2))
},
onVisibilityChange() {
if (document.hidden) {
this.stop()
} else {
this.start()
}
},
disposeThree() {
this.stop()
if (this.controls) { this.controls.dispose(); this.controls = null }
if (this.points) {
const geom = this.points.geometry
const mat = this.points.material
if (geom) {
for (const name in geom.attributes) {
geom.attributes[name].array && geom.attributes[name].array.length
&& geom.attributes[name].array.fill(0)
geom.attributes[name].dispose && geom.attributes[name].dispose()
}
geom.dispose && geom.dispose()
}
if (mat) {
mat.dispose && mat.dispose()
}
this.scene.remove(this.points)
this.points = null
}
if (this.renderer) {
this.renderer.forceContextLoss()
this.renderer.domElement && this.renderer.domElement.remove()
this.renderer = null
}
this.scene = null
this.camera = null
}
}
}
</script>
<style scoped>
.three-container {
width: 100%;
height: 100%;
min-height: 400px;
position: relative;
overflow: hidden;
}
</style>
3,运行测试
(1)页面加载后会显示处飞机点云。由于我这个 PLY 文件没有顶点颜色,样例代码会在渲染时会自动按高度做从蓝到黄的渐变。

(2)按住鼠标左键拖动可以将视图绕点云旋转,使用滚轮可以缩放视图,按住右键拖动则可平移视角。

全部评论(0)