返回 导航

Vue.js

hangge.com

Vue.js - 集成Three.js构建三维可视化场景教程6(加载PLY点云)

作者:hangge | 2026-05-21 08:36
    在数字孪生、激光/摄影测量点云可视化场景里,点云数据(Point Cloud)是常见且关键的资产格式之一。PLYPolygon File Format / Stanford Triangle Format)既能保存网格也常用于点云,支持顶点颜色、法线和自定义属性。本文演示如何使用 Three.jsVueOptions 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)

回到顶部