XGRIDS文档
  • 简体中文
  • English
  • 繁體中文
  • 日本語
  • Deutsch
  • Español
  • Italiano
  • Français
  • Русский
  • Polski
  • Nederlands
  • 简体中文
  • English
  • 繁體中文
  • 日本語
  • Deutsch
  • Español
  • Italiano
  • Français
  • Русский
  • Polski
  • Nederlands
  • 灵视 P 系列

    • 灵视 P1

      • 产品概述
      • 设备基本操作
      • 使用 LCC Scan App
      • 维护及保养
      • 常见问题
  • 灵光 K 系列

    • 灵光 K1

      • 产品概述
      • 设备基本操作
      • 设备激活与连接
      • 设备采集
      • 获取绝对坐标点云数据
      • 地图融合
      • 典型场景路线规划建议
      • 注意事项
      • 常见问题
    • 灵光 K2

      • 产品概述
      • 设备基本操作
      • 设备激活与连接
      • 设备采集
      • 获取绝对坐标点云数据
      • 地图融合
      • 典型场景路线规划建议
      • 注意事项
      • 常见问题
  • 灵光 L 系列

    • 灵光 L2 Pro

      • 产品概述
      • 设备基本操作
      • 设备激活与连接
      • 设备采集
      • 获取绝对坐标点云数据
      • 实时测量功能
      • 附录
      • 常见问题
    • 灵光 L3

      • 产品概述
      • 设备基本功能
      • 设备激活与连接
      • 设备采集
      • 获取绝对坐标点云数据操作流程
      • 附录
  • 配件

    • 补光灯

      • 安装指导
      • 基本操作
      • 常见问题
  • Lixel Studio

    • 版权
    • 安装与激活
    • 界面说明
    • 文件
    • 工程处理
    • 工具
    • 平面绘制
    • 行业应用
    • 设置
    • 设备感知
    • 常见问题 / FAQ
  • Lixel CyberColor

    • LCC Studio

      • 入门
      • 版本与更新
      • 下载与安装
      • 界面概览与导航
      • 重建前工作
      • 模型重建
      • 单模型重建
      • 地图融合
      • 空地融合
      • 航拍重建
      • 我的模型
      • 其他功能
      • 设置与账号
      • 转换工具
      • 视频重建
      • 常见问题 / FAQ
    • LCC Scene Editor

      • 版本与更新
      • 账号与登录
      • 产品概览与主页
      • 编辑器界面介绍
      • 三种场景漫游快捷操作
      • 文件
      • 设置
      • 编辑操作
      • 窗口
      • 视图工具栏
      • 资产与属性
      • 左侧工具栏
      • 视点
      • 跳转点
      • 天空盒
      • 标注
      • 测量
      • 场景漫游
      • 场景报告
      • 智能户型图(3D Layout)
      • 小地图
      • 预览模式(Viewer)
      • 帮助
      • 常见问题 / FAQ
      • 出生点
    • LCC Model Editor

      • 版本与更新
      • 新手指引
      • 概览与界面
      • 文件操作
      • 选择器
      • 编辑操作
      • 测量
      • 调色
      • 资产管理
      • 设置与帮助
      • 常见问题 / FAQ
    • 采集指南

      • 概述
      • 采集设备总览
      • 通用采集原则
      • 室内场景采集
      • 室外场景采集
      • 大场景采集(地图融合)
      • 空地融合采集
      • 物体采集
      • 人物采集
      • 视频重建采集
      • 高清补拍
      • 控制点(灵视 P1)
      • 常见问题与排查
    • LCC Studio Linux

      • 产品概要说明
      • 基本架构说明书
      • 安装部署说明书
      • 容器启动参数说明书
      • CodeMeter离线授权部署说明书
      • API指令集说明书
      • 附录-开发者数据说明书
      • 附录-错误码列表
      • 单机多卡集群部署QuickStart
    • 使用技巧

      • 多个大型 3DGS 模型融合工作流
    • 历史版本
  • Plugin & SDK

    • Unreal

      • 介绍
      • 快速入门 - Windows
      • 快速入门 - Linux
      • 快速入门 - Quest3
      • 版本与授权
      • 渲染
      • Tiled 光栅化(实验性)
      • 画面调节
      • 法线与光照
      • 场景编辑
      • 性能参数说明
      • 性能优化指南
      • 第三方与引擎插件集成
      • 代理网格
      • 加载动画
      • 碰撞
      • 寻路系统支持
      • 单层水支持
      • 本地化
      • 常见问题
      • 故障排查
      • 日志与诊断
      • 联系我们
      • 最佳实践

        • 用 LixelStudio 网格为 3DGS 重打光
      • API 参考

        • ALCCActorBase
        • ULCCComponentBase
        • ULCCComponent
        • ULCC2Component
        • SOG / SPZ / PLY Actors
        • ALCC2ProxyMesh
        • ALCCClippingVolume
        • ALCCSectionPlane
        • ALCCLoadVolume
        • ULCCUtilLibrary
        • Enums
        • Structs
      • 更新日志

        • v3.4.0
        • v3.3.1
        • v3.0.0
        • v2.2.1
        • v1.0.0
        • v0.9.0
        • v0.8.0
        • v0.7.1
        • v0.6.1
        • v0.5.2
        • v0.4.1
        • v0.4.0
        • v0.3.0
        • v0.0.5
        • v0.0.4
        • v0.0.3
        • v0.0.2
        • v0.0.1
    • Unity

      • Unity SDK 简介
      • 技术特性
      • 开发环境要求
      • PC 快速开发入门指南
      • 工程设置优化指南
      • Pico 4 Ultra 开发指南
      • Pico 4 Ultra PC 串流指南
      • Quest 3 开发指南
      • Quest 3 PC 串流指南
      • OpenXR 开发指南
      • Pico 4 Ultra 企业串流指南
      • 术语表
      • 常见问题
      • 联系我们
      • 功能开发指南

        • 多场景混合渲染开发指南
        • 阴影接收开发指南
        • 实时光照开发指南
        • 多相机渲染开发指南
        • 高清图像渲染开发指南
        • LCC 与 LCC2 数据格式兼容开发指南
        • 加载特效开发指南
        • Mip 渲染模式切换开发指南
        • 边角点吸附开发指南
        • 投影贴花开发指南
        • 视频流投影开发指南
        • 高亮显示开发指南
        • 实时裁剪开发指南
        • 调色开发指南
        • 获取场景 Mesh 数据开发指南
        • 编辑器模式开发指南
        • 渲染性能参数设置指南
        • 应用与数据协同打包开发指南
        • 碰撞系统开发指南
        • macOS 开发权限问题操作指南
        • iOS 设备编译打包开发指南
      • API 参考

        • LCCManager
        • Renderer
        • 数据类型
      • 更新日志

        • v2.3.0
        • v2.2.10
        • v2.2.0
        • v2.1.19
        • v2.0.14
    • X-Brain

      • 产品概述
      • 硬件接口
      • 快速开始
      • 核心概念
      • 集成指南
      • API 参考
      • 数据类型
      • ROS2 桥接
      • 示例程序
      • 部署与网络
      • 排障与 FAQ
      • 版本与发布
    • Web

      • 介绍
      • 渲染性能优化指南
      • 显卡配置
      • 常见开发问题
      • API参考

        • LCCRender::clearIndexDB
        • LCCRender::dispose
        • LCCRender::load
        • LCCRender::setCamera
        • LCCRender::unload
        • LCCRender::update
        • LCCObject::checkRenderNextFrame
        • LCCObject::clearRenderNextFrame
        • LCCObject::ecef2Prj
        • LCCObject::getBounds
        • LCCObject::getEnvInstancedMesh
        • LCCObject::getInstancedMesh
        • LCCObject::getLodInfos
        • LCCObject::getOriginPosition
        • LCCObject::getProjectionCoordinateSystemInfos
        • LCCObject::hasCollision
        • LCCObject::hasEnvironment
        • LCCObject::hasShcoef
        • LCCObject::intersectsCapsule
        • LCCObject::intersectsRayExt
        • LCCObject::intersectsRayFromOriginExt
        • LCCObject::intersectsSphere
        • LCCObject::lowerToBottom
        • LCCObject::prj2Ecef
        • LCCObject::raiseToTop
        • LCCObject::raycast
        • LCCObject::raycastFromOrigin
        • LCCObject::setAlpha
        • LCCObject::setClipBox
        • LCCObject::setClipPlane
        • LCCObject::setEndLod
        • LCCObject::setLodAutoLevelUp
        • LCCObject::setMaxDistance
        • LCCObject::setMaxNodeSplats
        • LCCObject::setMaxSplats
        • LCCObject::setOriginPosition
        • LCCObject::setPointsColor
        • LCCObject::setRenderState
        • LCCObject::setRotation
        • LCCObject::setScale
        • LCCObject::setSemantic
        • LCCObject::setSemanticColor
        • LCCObject::setSmooth
        • LCCObject::setStartLod
        • LCCObject::setTranslation
        • LCCObject::setVisible
        • LCCObject::togglePointsDisplayMode
        • LCCObject::useEnvironment
        • LCCObject::useShcoef
      • 更新日志

        • v0.6.3
        • v0.6.2
        • v0.6.1
        • v0.6.0
        • v0.5.5
        • v0.5.4
        • v0.5.3
        • v0.5.2
        • v0.5.1
        • v0.5.0
        • v0.4.1
        • v0.4.0
        • v0.3.1
        • v0.3.0
        • v0.2.0
    • Lixel OpenAPI

      • 概述
      • 连接与鉴权
      • 消息模型与编解码
      • 接口清单
      • 实时数据
      • 项目文件访问
      • 错误码
      • Python

        • 快速开始
        • 客户端封装与工具
        • 常见问题
      • C++

        • 快速开始
        • 客户端与示例
        • 常见问题

视频投影贴花开发指南

功能概述

视频投影贴花是在 投影贴花 基础上的扩展:投影内容不再是一张静态图片,而是一段视频。做法是用 Unity 内置的 VideoPlayer 解码视频,把每一帧的画面动态更新到贴花纹理上,于是这段视频就像幻灯机一样被投射到 3DGS 点云表面。

由于贴花本身是世界空间中固定的透视投影,视频画面会随投影仪的位姿"贴"在点云表面。落在投影仪视锥内的点被视频当前帧染色,视锥外不受影响。

说明:

视频贴花与普通贴花一样,仅在 PC / Mac(URP 管线) 下生效。移动端或 VR 平台调用相关接口会被 SDK 忽略并打印警告。

本文只讲解与"视频"相关的增量部分。场景搭建(Main Camera / LCCManager / LCCRenderer / Projector)、URP Renderer 添加 Decal Depth Feature、遮挡剔除调参、贴花 API 详解等基础内容,请先阅读 投影贴花开发指南。

实现原理

SDK 的贴花接口 SetDecalTexture(Texture2D tex) 只接受 Texture2D,但它支持在运行时动态更新这张纹理。而 Unity 的 VideoPlayer 解码输出的是 RenderTexture(GPU 端)。因此整条数据链路是:

VideoPlayer 解码 → RenderTexture(GPU)→ 拷贝到 Texture2D → SetDecalTexture(帧纹理) → 投影到点云

其中有两个关键点,也是本文重点提醒的两处"对齐":

  1. 空间对齐:视频画面投到点云的位置、大小、方向由投影仪参数决定(位姿、FOV、Aspect、Near/Far),见 视频与场景的空间对齐。
  2. 时间对齐:视频帧率往往远低于场景渲染帧率(场景可能跑到几百 FPS),不能每个渲染帧都拷贝并推送一次纹理。通过 VideoPlayer.frame 判断是否出现了新解码帧,只在新帧到来时才更新,见 视频帧率与渲染帧率的对齐。
视频投影贴花数据链路示意

视频投影贴花数据链路:VideoPlayer → RenderTexture → Texture2D → SetDecalTexture

前置条件

视频贴花与普通贴花共享同一套前提,开始前请确认:

  1. 场景中已搭好 Main Camera、LCCManager、LCCRenderer、Projector(做法见 投影贴花开发指南 · 步骤 1)。
  2. 当前使用 URP 管线;若需要遮挡剔除,URP Renderer Asset 中已注册 Decal Depth Feature。
  3. 若 Console 报错且看不到效果,关闭 Render Graph 兼容模式( 参见 投影贴花开发指南 常见问题章节)。
  4. 准备好一段视频素材:可以是工程内的 VideoClip 资源,也可以是磁盘 / 网络上的视频文件 URL(如 file:///D:/sample.mp4)。

步骤 1:编写脚本

新建一个 C# 脚本,命名为 VideoStreaming.cs。它在普通 Decal.cs 的骨架上,把"静态贴花纹理"替换为"运行时创建 VideoPlayer 解码视频,并在 Update() 中按解码帧更新贴花纹理"。可直接复制到工程使用。

using LCCCore;
using UnityEngine;
using UnityEngine.Video;

public class VideoStreaming : MonoBehaviour
{
    [Header("Required")]
    [Tooltip("The LCCManager instance in the scene")]
    public LCCManager m_manager;
    [Tooltip("A valid LCC scene path")]
    public string m_filePath;

    [Header("Video source (decoded frames are projected as the decal)")]
    [Tooltip("Video clip to project. Takes priority over the URL below")]
    public VideoClip m_videoClip;
    [Tooltip("Video file path / URL, used when no clip is assigned (e.g. file:///D:/xxx.mp4)")]
    public string m_videoUrl;
    [Tooltip("Loop playback")]
    public bool m_videoLoop = true;

    [Header("Projector (projects along +Z)")]
    [Tooltip("Projector pose. Leave empty to use this object's transform")]
    public Transform m_projector;
    [Range(1f, 179f)] public float m_fov = 60f;
    [Tooltip("Projection aspect ratio; <=0 uses the video's aspect ratio automatically")]
    public float m_aspect = 0f;
    public float m_near = 0.1f;
    public float m_far = 100f;

    [Header("Decal appearance")]
    [Range(0f, 1f)] public float m_opacity = 1.0f;
    [Range(0f, 0.5f)][Tooltip("Edge fade width (UV ratio). 0 = hard edge")]
    public float m_edgeFade = 0.1f;
    [Tooltip("Refresh projection every frame, so you can drag the projector / tweak params at runtime")]
    public bool m_updateEveryFrame = true;

    [Header("Occlusion culling (PC/Mac only)")]
    [Tooltip("When on, back-facing/occluded points behind front geometry are no longer colored by the decal (slide-projector effect); when off it is X-ray")]
    public bool m_occlusion = false;
    [Range(0f, 1f)][Tooltip("Opacity threshold: only splats with opacity >= this value contribute to the depth map, filtering out floaters/foggy points. 0 = all participate")]
    public float m_occlusionCutoff = 0f;
    [Range(0f, 0.5f)][Tooltip("Relative depth tolerance. Too small -> near-surface self-occlusion; too large -> leaked X-ray coloring. Default 0.05")]
    public float m_occlusionBias = 0.05f;
    [Range(0, 16)][Tooltip("Square footprint radius cap (px), the main knob for removing X-ray bleed-through. 0 = single texel, default 2")]
    public int m_occlusionFootprint = 2;
    [Tooltip("Depth map resolution (px, power of two 256~4096). Lower it to fill grazing-angle holes. Default 1024")]
    public int m_occlusionResolution = 1024;

    // -- 运行时状态 --
    private bool m_loaded;
    private bool m_lastOcclusion;
    private float m_lastCutoff = float.NaN;
    private float m_lastBias = float.NaN;
    private int m_lastFootprint = -1;
    private int m_lastResolution = -1;

    // -- 视频状态 --
    private VideoPlayer m_videoPlayer;
    private RenderTexture m_videoRT;   // VideoPlayer 将视频解码到这里(GPU)
    private Texture2D m_frameTex;      // 当前帧拷贝到这里,喂给贴花
    private bool m_videoReady;
    private long m_lastVideoFrame = -1; // 上一次推给贴花的帧序号

    void Start()
    {
        if (m_manager == null || string.IsNullOrEmpty(m_filePath))
        {
            Debug.LogError("[VideoStreaming] m_manager or m_filePath is not assigned.");
            enabled = false;
            return;
        }

        // 1) 将渲染管线初始化为 PC 单场景编辑器模式
        m_manager.SetPlatformType(PlatformType.PC);
        m_manager.SwitchRenderPass(ActiveRenderMode.SingleRender);
        m_manager.SetEditorMode(true);

        // 2) 与场景加载并行地开始解码视频
        SetupVideo();

        // 3) 加载点云场景,然后启用贴花(贴花是全局状态,在场景就绪后再设置最为稳妥)
        var renderer = m_manager.GetRender(this.transform);
        Debug.Log($"[VideoStreaming] Start loading: {m_filePath}");
        renderer.Load(m_filePath,
            _ => { /* 进度回调,本示例未使用 */ },
            OnSceneLoaded);
    }

    /// <summary>在运行时创建 VideoPlayer 并开始准备 clip/URL。</summary>
    private void SetupVideo()
    {
        m_videoPlayer = gameObject.AddComponent<VideoPlayer>();
        m_videoPlayer.playOnAwake = false;
        m_videoPlayer.isLooping = m_videoLoop;
        m_videoPlayer.renderMode = VideoRenderMode.RenderTexture;
        m_videoPlayer.audioOutputMode = VideoAudioOutputMode.None; // 演示:不接音频

        if (m_videoClip != null)
        {
            m_videoPlayer.source = VideoSource.VideoClip;
            m_videoPlayer.clip = m_videoClip;
        }
        else
        {
            m_videoPlayer.source = VideoSource.Url;
            m_videoPlayer.url = m_videoUrl;
        }

        // 分辨率已知后再分配帧缓冲,然后开始播放。
        m_videoPlayer.prepareCompleted += OnVideoPrepared;
        m_videoPlayer.Prepare();
    }

    private void OnVideoPrepared(VideoPlayer vp)
    {
        int w = (int)vp.width;
        int h = (int)vp.height;

        m_videoRT = new RenderTexture(w, h, 0, RenderTextureFormat.ARGB32);
        m_videoRT.Create();
        vp.targetTexture = m_videoRT;

        m_frameTex = new Texture2D(w, h, TextureFormat.RGBA32, false);

        m_videoReady = true;
        vp.Play();
        Debug.Log($"[VideoStreaming] Video prepared: {w}x{h}, playing.");
    }

    private void OnSceneLoaded()
    {
        Debug.Log("[VideoStreaming] Load complete, enabling decal projection.");

        // -- 基础投影:5 个 API --
        // 纹理内容在 Update() 中按解码帧驱动;这里先把当前已有的帧推上去。
        if (m_frameTex != null) m_manager.SetDecalTexture(m_frameTex);
        m_manager.SetDecalOpacity(m_opacity);        // 整体不透明度
        m_manager.SetDecalEdgeFade(m_edgeFade);      // 边缘羽化
        ApplyProjection();                           // 投影仪位姿(内部调用 SetDecalProjection)
        m_manager.SetDecalEnabled(true);             // 启用贴花(_DECAL keyword)

        // -- 遮挡剔除:2 个 API -- 必须在 ApplyProjection 之后(深度图需要有效的投影仪 VP 矩阵)
        m_manager.SetDecalOcclusionSettings(BuildOcclusionSettings());
        m_manager.SetDecalOcclusion(m_occlusion);
        CacheOcclusionState();

        m_loaded = true;
    }

    void Update()
    {
        if (!m_loaded) return;

        // --- 视频帧更新,与渲染 FPS 解耦 ---
        // VideoPlayer.frame 只按视频自身的播放速率递增。即使场景以数百 FPS 渲染,
        // 相邻两个视频帧之间该序号也保持不变,因此只有当真正出现新解码帧时,
        // 我们才拷贝该帧并重新推送贴花纹理。
        if (m_videoReady && m_videoPlayer.frame != m_lastVideoFrame)
        {
            m_lastVideoFrame = m_videoPlayer.frame;

            // 把 GPU 上的 RenderTexture 拷贝到贴花 API 接受的 Texture2D。
            // 只在视频帧率下执行,因此回读开销可控。
            RenderTexture prev = RenderTexture.active;
            RenderTexture.active = m_videoRT;
            m_frameTex.ReadPixels(new Rect(0, 0, m_videoRT.width, m_videoRT.height), 0, 0, false);
            m_frameTex.Apply(false);
            RenderTexture.active = prev;

            m_manager.SetDecalTexture(m_frameTex);
        }

        // 运行时切换遮挡开关
        if (m_occlusion != m_lastOcclusion)
        {
            m_manager.SetDecalOcclusion(m_occlusion);
            m_lastOcclusion = m_occlusion;
        }

        // 若任一遮挡质量参数发生变化 -> 整体推送一次结构体
        if (!Mathf.Approximately(m_occlusionCutoff, m_lastCutoff) ||
            !Mathf.Approximately(m_occlusionBias, m_lastBias) ||
            m_occlusionFootprint != m_lastFootprint ||
            m_occlusionResolution != m_lastResolution)
        {
            m_manager.SetDecalOcclusionSettings(BuildOcclusionSettings());
            CacheOcclusionState();
        }

        // 每帧刷新投影仪 / 外观,便于运行时实时调参
        if (!m_updateEveryFrame) return;
        m_manager.SetDecalOpacity(m_opacity);
        m_manager.SetDecalEdgeFade(m_edgeFade);
        ApplyProjection();
    }

    /// <summary>根据当前投影仪位姿与透视参数构建投影矩阵。</summary>
    private void ApplyProjection()
    {
        Transform p = m_projector != null ? m_projector : this.transform;
        float aspect = m_aspect > 0f
            ? m_aspect
            : (m_videoRT != null ? (float)m_videoRT.width / m_videoRT.height : 1920.0f / 1080.0f);
        m_manager.SetDecalProjection(p.position, p.rotation, m_fov, aspect, m_near, m_far);
    }

    /// <summary>将 4 个遮挡参数汇集到一个结构体中。</summary>
    private DecalOcclusionSettings BuildOcclusionSettings()
    {
        return new DecalOcclusionSettings
        {
            opacityCutoff  = m_occlusionCutoff,
            depthBias      = m_occlusionBias,
            footprintMaxPx = m_occlusionFootprint,
            resolution     = m_occlusionResolution,
        };
    }

    private void CacheOcclusionState()
    {
        m_lastOcclusion  = m_occlusion;
        m_lastCutoff     = m_occlusionCutoff;
        m_lastBias       = m_occlusionBias;
        m_lastFootprint  = m_occlusionFootprint;
        m_lastResolution = m_occlusionResolution;
    }

    private void OnDestroy()
    {
        if (m_videoPlayer != null) m_videoPlayer.prepareCompleted -= OnVideoPrepared;
        if (m_videoRT != null)
        {
            m_videoRT.Release();
            Destroy(m_videoRT);
        }
        if (m_frameTex != null) Destroy(m_frameTex);
    }

    // -- 便捷调试入口(Inspector 右键) --
    [ContextMenu("Decal On")]  private void DecalOn()  { m_manager?.SetDecalEnabled(true); }
    [ContextMenu("Decal Off")] private void DecalOff() { m_manager?.SetDecalEnabled(false); }
    [ContextMenu("Occlusion On")]  private void OcclusionOn()  { m_occlusion = true;  m_manager?.SetDecalOcclusion(true);  m_lastOcclusion = true; }
    [ContextMenu("Occlusion Off")] private void OcclusionOff() { m_occlusion = false; m_manager?.SetDecalOcclusion(false); m_lastOcclusion = false; }
}

注意:

  • 视频与场景加载并行进行:Start() 中先调用 SetupVideo() 启动 VideoPlayer.Prepare(),再发起点云场景 Load。二者互不阻塞。
  • 帧缓冲在视频就绪后才分配:RenderTexture 与 Texture2D 的尺寸取自视频真实分辨率(vp.width / vp.height),所以放在 prepareCompleted 回调 OnVideoPrepared 里创建。
  • 贴花在场景加载完成后才开启:与普通贴花一致,SetDecalTexture / SetDecalEnabled 等放在 OnSceneLoaded 回调里。若此时视频尚未就绪,m_frameTex 为空则跳过首次纹理设置,等 Update() 里第一帧到来再补上。

步骤 2:挂载脚本与属性设置

  1. 将 VideoStreaming.cs 脚本挂载到 Hierarchy 窗口中的 LCCRenderer 游戏对象上。
  2. 在 Inspector 窗口中,将 LCCManager 游戏对象拖到脚本的 Manager 属性上。
  3. 将 Projector 游戏对象拖到脚本的 Projector 属性上。
  4. 在 File Path 属性中填写 LCC 场景文件路径。
  5. 指定视频源,二选一:
    • 把工程内的 VideoClip 资源拖到 Video Clip 属性;或
    • 在 Video Url 属性填写视频文件路径(如 file:///D:/sample.mp4)。
    • 两者都填时以 Video Clip 优先。
  6. 其他属性可按需调整,含义见下表。
VideoStreaming 脚本的 Inspector 属性设置

VideoStreaming 脚本的 Inspector 属性设置

字段含义默认值
Manager场景中的 LCCManager 实例必填
File Path有效的 LCC 场景路径必填
Video Clip要投影的视频资源,优先级高于 Video Url空
Video Url视频文件路径 / URL,未指定 Clip 时使用空
Video Loop是否循环播放勾选
Projector投影仪位姿,+Z 为投影方向;留空则用本物体 Transform空
Fov投影仪透视视角60
Aspect投影宽高比;小于等于 0 时按视频宽高比自动计算0(自动)
Near / Far投影仪视锥近 / 远裁剪面0.1 / 100
Opacity贴花整体不透明度1
Edge Fade边缘羽化宽度(UV 比例),0 为硬边0.1
Update Every Frame每帧刷新投影仪 / 外观,便于运行时调参勾选
Occlusion是否开启遮挡剔除关
Occlusion Cutoff / Bias / Footprint / Resolution遮挡剔除质量参数,含义见 投影贴花开发指南0 / 0.05 / 2 / 1024

步骤 3:运行效果

在 Unity 中点击 Play。视频会在 Prepare 完成后开始播放,场景加载完成后贴花自动开启,视频画面便逐帧投射到 3DGS 表面。勾选 Update Every Frame 时,可在 Scene 视图直接拖动投影仪,实时看到视频画面在点云表面移动。

运行后视频逐帧投影到 3DGS 表面

运行后视频逐帧投影到 3DGS 表面

视频与场景的空间对齐

"空间对齐"指让视频画面准确地投在点云上你想要的位置、朝向和大小。视频画面所处的位置完全由投影仪(SetDecalProjection)决定,与视频内容无关。影响对齐的参数如下:

参数来源对对齐的影响
投影仪 位置(Projector.position)Projector 的 Transform决定视锥顶点的世界坐标,即视频从"哪里"投出。整体平移画面在点云上的落点。
投影仪 朝向(Projector.rotation)Projector 的 Transform+Z 为投影方向。旋转投影仪即改变画面投向,画面会随之在表面平移 / 倾斜。
Fov(垂直视角)脚本字段 m_fov决定视锥张开角度,直接影响画面在点云上的大小:FOV 越大画面铺得越大,越小越聚拢。范围 [1, 179]。
Aspect(宽高比)脚本字段 m_aspect决定画面横纵比例。填 0 时自动取视频分辨率宽高比(m_videoRT.width / height),这是保证视频不被拉伸变形的推荐做法。手动填错会导致画面拉伸。
Near / Far(近 / 远裁剪面)脚本字段 m_near / m_far决定视锥沿投影方向的有效深度区间。只有落在 [near, far] 内的点才会被染色。Far 太小会截掉远处点云,Near 太大会漏掉近处。需覆盖目标点云所在的深度范围。

对齐的一般步骤:

  1. 先把 Aspect 保持为 0(自动),避免画面变形,专注调位置和大小。
  2. 拖动 Projector 的位置和朝向,让画面中心对准目标区域(勾选 Update Every Frame 可实时预览)。
  3. 调 Fov 控制画面覆盖范围大小。
  4. 确认 Near / Far 区间完整覆盖目标点云的深度,避免画面被裁剪面截断。
  5. 如需强制特定比例(例如把 16:9 视频投成正方形区域),再手动设置 Aspect。

提示:

视频的画面比例由 Aspect 控制,视频的分辨率只影响清晰度与回读开销,不影响投在点云上的物理大小——大小由 Fov 和投影距离共同决定。

投影仪参数对视频画面在点云上落点 / 大小的影响

投影仪位姿 / Fov / Aspect / Near-Far 对视频画面在点云上落点与大小的影响

视频帧率与渲染帧率的对齐

这是视频贴花最关键的一处处理。视频帧率(如 25/30 FPS)通常远低于场景渲染帧率(3DGS 场景可能跑到几百 FPS)。 如果每个渲染帧都做一次"回读 + SetDecalTexture",会有两个问题:

  • 重复劳动:同一视频帧被反复拷贝、反复推送,纯属浪费。
  • 性能拖累:ReadPixels 是 GPU→CPU 回读,代价不低;在高渲染帧率下每帧都做会明显掉帧。

解耦策略:以 VideoPlayer.frame 为节拍

VideoPlayer.frame 是当前已解码到的帧序号,它只按视频自身的播放速率递增。渲染帧率再高,两个视频帧之间这个序号也不会变。据此只在"序号变化"时才更新纹理:

if (m_videoReady && m_videoPlayer.frame != m_lastVideoFrame)
{
    m_lastVideoFrame = m_videoPlayer.frame;

    // 只有新解码帧到来时,才回读一次并推送贴花
    RenderTexture prev = RenderTexture.active;
    RenderTexture.active = m_videoRT;
    m_frameTex.ReadPixels(new Rect(0, 0, m_videoRT.width, m_videoRT.height), 0, 0, false);
    m_frameTex.Apply(false);
    RenderTexture.active = prev;

    m_manager.SetDecalTexture(m_frameTex);
}

这样:

  • 场景以 300 FPS 渲染、视频是 30 FPS 时,300 个渲染帧里只有约 30 次真正做了回读和推送,其余帧被 if 直接跳过。
  • 节拍由播放器自己掌握,无需手写计时器去凑视频帧率,也不会因为渲染帧率波动而丢帧或重复帧。
  • 视频的快慢、循环、暂停都由 VideoPlayer 内部管理,frame 序号会如实反映,脚本无需额外同步。

为什么不用每帧计时器?

手动累加 Time.deltaTime 去逼近视频帧间隔,既要处理渲染帧率抖动,又要处理视频变速 / 跳帧,容易和播放器内部的实际解码进度错位。直接读 VideoPlayer.frame 让播放器做唯一事实源,最简单也最稳。

关于 Update Every Frame:

脚本里的 m_updateEveryFrame 控制的是投影仪位姿 / 不透明度 / 羽化是否每帧刷新,与视频帧更新是两回事。视频帧更新永远按 VideoPlayer.frame 节拍走,不受这个开关影响。

性能与格式注意事项

  • 回读开销与分辨率成正比。 ReadPixels + Apply 是 GPU→CPU→GPU 的往返,分辨率越高越慢。本示例用它是为了简单直观、演示够用。若视频分辨率很高或对性能敏感,可改用纯 GPU 拷贝 Graphics.CopyTexture(m_videoRT, m_frameTex),但要求源和目标格式匹配(RenderTextureFormat.ARGB32 对应 TextureFormat.RGBA32),且部分平台对 CopyTexture 有限制。
  • SetDecalTexture 只接受 Texture2D。 这正是必须把 RenderTexture 回读成 Texture2D 的原因;不能直接把 VideoPlayer 的 RenderTexture 传给贴花接口。
  • 帧缓冲复用,不要每帧 new。 示例中 m_frameTex 只在视频就绪时创建一次,之后每帧复用同一张 Texture2D,避免频繁分配 / GC。
  • 及时释放资源。 OnDestroy 中 Release() 并 Destroy 掉 RenderTexture 与 Texture2D,同时反注册 prepareCompleted,避免泄漏。
  • 音频未接线。 示例把 audioOutputMode 设为 None,只演示画面。需要声音时可改为 AudioSource 模式并挂一个 AudioSource。

与普通贴花的差异小结

方面普通贴花(Decal.cs)视频贴花(VideoStreaming.cs)
贴花内容静态 Texture2D视频逐帧解码得到的 Texture2D
纹理来源Inspector 直接拖入图片VideoPlayer 解码 → RenderTexture → 回读
纹理更新时机一次性设置按 VideoPlayer.frame 节拍逐帧更新
额外组件无运行时 AddComponent<VideoPlayer>()
Aspect 自动值图片宽高比视频宽高比
其余(投影仪 / 遮挡 / API)完全一致完全一致

涉及的关键 API

视频贴花在 SDK 侧只额外用到了 SetDecalTexture 的动态更新能力,其余贴花 API 与普通贴花完全相同,详见 投影贴花开发指南 · 贴花相关 API。

SetDecalTexture(动态更新)

// 返回值:void
public void SetDecalTexture(Texture2D tex)

设置贴花纹理。RGB 为颜色,A 为贴花自身遮罩。该方法支持运行时反复调用:每次传入新的 Texture2D 都会立即替换当前贴花内容,这正是视频逐帧投影的基础——在 Update() 中每出现一个新解码帧就调用一次。

参数类型说明
texTexture2D贴花纹理,可在运行时反复替换

其余相关 Unity 内置类型(VideoPlayer / RenderTexture / Texture2D)为 Unity 标准 API,用法参见 Unity 官方文档。