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

      • 入門
      • 版本與更新
      • 下載及安裝
      • 介面概覽及導覽
      • 重建前準備
      • 模型重建
      • 單模型重建
      • 地圖融合
      • 空地融合
      • 航拍重建
      • 我的模型
      • 其他功能
      • 設定與帳戶
      • 設置與賬號
      • Converter
      • 影片重建
      • 視頻重建
      • 常見問題
    • 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
    • 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

視頻投影貼花開發指南

功能概述

視頻投影貼花是在 投影貼花 基礎上的擴展:投影內容不再是一張靜態圖片,而是一段視頻。做法是用 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 官方文檔。