欢迎光临
我们一直在努力

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

文章目录

  • 一、效果
  • 二、简介
    • 2.1、本文介绍
    • 2.2、什么是GaussianSplats3D
  • 三、技术选型与核心依赖
  • 四、核心组件实现(PlyCanvasViewer.vue)
    • 4.1、组件结构设计
    • 4.2、核心功能解析
  • 五、核心代码

一、效果

在这里插入图片描述

二、简介

2.1、本文介绍

高斯泼溅(Gaussian Splatting)是当前 3D 可视化领域的热门技术,凭借其超高的渲染精度和视觉表现力,成为 3D 重建、数字孪生等场景的优选方案。本文将基于 Vue3 + @mkkellogg/gaussian-splats-3d 库实现 3D 高斯泼溅效果的渲染,并整合交互、相机控制、自适应布局等核心能力,延续 Vue3+PlayCanvas 系列的开发风格,完成高斯泼溅模型的可视化落地。

2.2、什么是GaussianSplats3D

GaussianSplats3D 是由开发者 Matthew Kellogg 开发的一个轻量级 JavaScript 库,专为在 Web 浏览器 中渲染 3D Gaussian Splatting 场景 而设计。它基于 WebGL 构建,支持 .ply 格式的高斯点云数据(通常由 3D Gaussian Splatting 论文 的官方实现导出),并提供了:

  • 实时渲染
  • 相机控制(OrbitControls)
  • 渐进式加载(Progressive Loading)
  • 点选与悬停交互
  • 响应式布局支持 该库封装了底层复杂的着色器逻辑和数据解析流程,让前端开发者可以像使用普通 3D 查看器一样轻松集成高斯点云。

GitHub 仓库:https://github.com/mkkellogg/gaussian-splats-3d

三、技术选型与核心依赖

  • 核心依赖 Vue3 (Setup 语法糖):作为前端框架,负责组件化开发、生命周期管理、Props 传参; @mkkellogg/gaussian-splats-3d:专门用于解析和渲染高斯泼溅模型(.ply 格式)的高性能库,内置 WebGL 渲染管线和相机控制; ResizeObserver:监听容器尺寸变化,保证 Canvas 自适应渲染; 自定义交互封装:实现高斯泼溅模型的 hover/select 事件监听,输出空间坐标等交互信息。
  • 前置准备 确保项目已安装依赖:
  • npm install @mkkellogg/gaussian-splats-3d –save

    四、核心组件实现(PlyCanvasViewer.vue)

    4.1、组件结构设计

    组件分为 3 个核心区域:

    • 渲染容器:承载高斯泼溅模型的 Canvas 画布;
    • 状态 Overlay:展示加载状态、错误信息、调试日志;
    • 交互 Overlay:展示鼠标悬停 / 选中的 3D 空间坐标信息。

    4.2、核心功能解析

  • 高斯泼溅 Viewer 初始化 通过GaussianSplats3D.Viewer创建渲染实例,核心配置项说明:
  • rootElement:渲染容器 DOM 节点; selfDrivenMode:自驱动渲染模式(自动维护渲染循环); cameraUp:相机向上方向(设置为[0,0,1]适配高斯泼溅模型坐标系); useBuiltInControls:是否启用内置相机控制(通过lockCameraProps 控制); initialCameraPosition/initialCameraLookAt:相机初始位置和看向目标。

  • 相机控制优化 默认情况下,高斯泼溅 Viewer 的相机旋转存在角度限制(垂直方向 ±85°),通过修改控制器参数解除限制:
  • // 解除垂直旋转角度限制
    viewer.controls.maxPolarAngle = Math.PI
    viewer.controls.minPolarAngle = 0
    // 解除水平旋转角度限制
    viewer.controls.maxAzimuthAngle = Infinity
    viewer.controls.minAzimuthAngle = Infinity
    // 调整旋转速度
    viewer.controls.rotateSpeed = 1.2

  • 模型加载兼容处理 适配不同版本的@mkkellogg/gaussian-splats-3d API,提供 3 种加载方式:
    • addSplatScene:基础加载方式;
    • downloadAndBuildSingleSplatSceneProgressiveLoad:渐进式加载(适合大模型);
    • downloadAndBuildSingleSplatSceneStandardLoad:标准加载。
  • 交互能力整合 通过setupSplatInteractions封装高斯泼溅的 hover/select 交互:
    • onHover:鼠标悬停时输出 3D 空间坐标;
    • onSelect:鼠标点击时输出坐标 + 距离信息;
    • 交互信息通过 Overlay 展示在画布底部,不遮挡渲染内容。
  • 自适应渲染 使用ResizeObserver监听容器尺寸变化,触发forceRenderNextFrame保证 Canvas 自适应:
  • resizeObserver = new ResizeObserver(() => {
    try {
    viewer?.forceRenderNextFrame?.()
    } catch { /* ignore */ }
    })
    resizeObserver.observe(containerRef.value)

  • 资源销毁 在组件卸载前,通过disposeViewer方法释放所有资源:
    • 断开ResizeObserver监听;
    • 销毁交互实例;
    • 停止渲染循环并销毁 Viewer 实例,避免内存泄漏。

    五、核心代码

    PlyCanvasViewer.vue

    <template>
    <div style="width:100%;height:100vh;background:#000">
    <div ref="containerRef" class="viewer">
    <!– 加载/错误/调试信息 overlay –>
    <div v-if="isLoading || errorText || debugText" class="overlay">
    <div v-if="isLoading" class="overlayLine">加载中: {{ loadedCount }} / 1</div>
    <div v-if="errorText" class="overlayLine">{{ errorText }}</div>
    <div v-if="debugText" class="overlayLine debug">{{ debugText }}</div>
    </div>
    <!– 悬停/选中信息 overlay –>
    <div v-if="hoverText || selectText" class="overlay overlay2">
    <div v-if="hoverText" class="overlayLine">{{ hoverText }}</div>
    <div v-if="selectText" class="overlayLine">{{ selectText }}</div>
    </div>
    <!– 原容器节点 –>
    <div id="container"></div>
    </div>
    </div>
    </template>

    <script setup>
    import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
    import * as GaussianSplats3D from '@mkkellogg/gaussian-splats-3d'
    import { setupSplatInteractions } from '../comment/splatInteraction'

    // 定义 props,允许通过 props 或路由参数传入 URL
    const props = defineProps({
    url: {
    type: String,
    default: '', // 默认为空,将从路由参数获取
    },
    lockCamera: {
    type: Boolean,
    default: false,
    },
    cameraPosition: {
    type: Array,
    default: () => [0, 10, 5],
    },
    cameraLookAt: {
    type: Array,
    default: () => [0, 0, 0],
    },
    progressive: {
    type: Boolean,
    default: true,
    },
    })

    const containerRef = ref(null)
    const isLoading = ref(false)
    const errorText = ref('')
    const debugText = ref('')
    const hoverText = ref('')
    const selectText = ref('')
    const loadedCount = ref(0)

    const modelUrl = '/ply/room.ply'

    let viewer
    let resizeObserver
    let interactionHandle

    function disposeViewer() {
    try {
    resizeObserver?.disconnect?.()
    } catch {
    // ignore
    }

    try {
    interactionHandle?.dispose?.()
    } catch {
    // ignore
    }

    try {
    viewer?.stop?.()
    } catch {
    // ignore
    }

    try {
    viewer?.dispose?.()
    } catch {
    // ignore
    }

    viewer = undefined
    resizeObserver = undefined
    interactionHandle = undefined
    }

    async function initViewerAndLoad() {
    errorText.value = ''
    isLoading.value = true
    loadedCount.value = 0

    if (!containerRef.value) return

    try {
    viewer = new GaussianSplats3D.Viewer({
    rootElement: containerRef.value,
    selfDrivenMode: true,
    cameraUp: [0, 0, 1],
    useBuiltInControls: !props.lockCamera,
    initialCameraPosition: props.cameraPosition,
    initialCameraLookAt: props.cameraLookAt,
    })
    // ========== 解除控制器角度限制 ==========
    if (viewer.controls && !props.lockCamera) {
    // 解除垂直旋转角度限制(默认是{ min: -85, max: 85 })
    viewer.controls.maxPolarAngle = Math.PI // 允许旋转到任意垂直角度
    viewer.controls.minPolarAngle = 0 // 可选:如果需要完全无限制,设为 0 或 -Math.PI
    // 解除水平旋转角度限制(默认无限制,如需确认可显式设置)
    viewer.controls.maxAzimuthAngle = Infinity
    viewer.controls.minAzimuthAngle = Infinity
    // 可选:调整旋转速度(如果需要)
    viewer.controls.rotateSpeed = 1.2 // 数值越大旋转越快
    }
    // ===========================================
    resizeObserver = new ResizeObserver(() => {
    try {
    viewer?.forceRenderNextFrame?.()
    } catch {
    // ignore
    }
    })
    resizeObserver.observe(containerRef.value)

    debugText.value = `renderMode=${String(viewer?.renderMode ?? '')}\\nLoading: ${modelUrl}`

    if (viewer?.addSplatScene) {
    await viewer.addSplatScene(modelUrl)
    loadedCount.value = 1
    } else if (props.progressive && viewer?.downloadAndBuildSingleSplatSceneProgressiveLoad) {
    await viewer.downloadAndBuildSingleSplatSceneProgressiveLoad(modelUrl)
    loadedCount.value = 1
    } else if (viewer?.downloadAndBuildSingleSplatSceneStandardLoad) {
    await viewer.downloadAndBuildSingleSplatSceneStandardLoad(modelUrl)
    loadedCount.value = 1
    } else {
    throw new Error('GaussianSplats3D Viewer API not compatible: cannot find load method')
    }

    applyFullSizeCanvas()
    applyLockedCamera()

    interactionHandle = setupSplatInteractions(viewer, {
    onHover: (hit) => {
    if (!hit?.point) {
    hoverText.value = ''
    return
    }
    const p = hit.point
    hoverText.value = `悬停: [${p.x.toFixed(3)}, ${p.y.toFixed(3)}, ${p.z.toFixed(3)}]`
    },
    onSelect: (hit) => {
    if (!hit?.point) {
    selectText.value = '选中: (无)'
    return
    }
    const p = hit.point
    selectText.value =
    `选中: [${p.x.toFixed(3)}, ${p.y.toFixed(3)}, ${p.z.toFixed(3)}]` +
    (hit.distance != null ? ` 距离=${Number(hit.distance).toFixed(3)}` : '')
    },
    onError: (err) => {
    console.warn('高斯交互错误:', err)
    },
    debug: true,
    })

    viewer?.start?.()
    } catch (e) {
    console.error(e)
    errorText.value = e?.message ? String(e.message) : String(e)
    } finally {
    isLoading.value = false
    }
    }

    function applyFullSizeCanvas() {
    const canvas = containerRef.value?.querySelector('canvas')
    if (!canvas) return
    canvas.style.width = '100%'
    canvas.style.height = '100%'
    canvas.style.display = 'block'
    }

    function applyLockedCamera() {
    if (!props.lockCamera) return

    const cam = viewer?.camera
    if (!cam) return

    const p = Array.isArray(props.cameraPosition) ? props.cameraPosition : [0, 10, 5]
    const t = Array.isArray(props.cameraLookAt) ? props.cameraLookAt : [0, 0, 0]

    cam.position.set(Number(p[0] ?? 0), Number(p[1] ?? 10), Number(p[2] ?? 5))
    cam.lookAt(Number(t[0] ?? 0), Number(t[1] ?? 0), Number(t[2] ?? 0))
    cam.updateMatrixWorld?.()

    // 禁用交互,让相机保持固定
    if (viewer?.controls) viewer.controls.enabled = false
    }

    onMounted(async () => {
    await initViewerAndLoad()
    })

    watch(
    () => modelUrl,
    async (next, prev) => {
    if (String(next || '') === String(prev || '')) return
    disposeViewer()
    await initViewerAndLoad()
    }
    )

    onBeforeUnmount(() => {
    disposeViewer()
    })
    </script>

    <style scoped>
    .viewer {
    position: relative;
    width: 100%;
    height: 100%;
    overflow: hidden;
    background: #0b0f19;
    }

    .overlay {
    position: absolute;
    top: 12px;
    left: 12px;
    z-index: 10;
    padding: 8px 10px;
    background: rgba(0, 0, 0, 0.55);
    color: #fff;
    font-size: 12px;
    border-radius: 6px;
    pointer-events: none;
    }

    .overlayLine {
    line-height: 1.4;
    }

    .overlay2 {
    top: auto;
    bottom: 12px;
    }

    .debug {
    white-space: pre-wrap;
    max-width: 70vw;
    }
    </style>

    赞(0)
    未经允许不得转载:171主机测评 » 【Web】使用Vue3开发3D游戏(十一)渲染3D高斯泼溅效果
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址