XGRIDSDocs
  • 简体中文
  • English
  • 繁體中文
  • 日本語
  • Deutsch
  • Español
  • Italiano
  • Français
  • Русский
  • Polski
  • Nederlands
  • 简体中文
  • English
  • 繁體中文
  • 日本語
  • Deutsch
  • Español
  • Italiano
  • Français
  • Русский
  • Polski
  • Nederlands
  • Seria Lixel PortalCam

    • PortalCam P1

      • Przegląd produktu
      • Podstawowa obsługa
      • Korzystanie z aplikacji LCC Scan
      • Konserwacja i pielęgnacja
      • FAQ
  • Seria Lixel K

    • Lixel K1

      • Przegląd produktu
      • Podstawowa obsługa
      • Aktywacja i połączenie urządzenia
      • Przebieg skanowania
      • Pozyskiwanie danych chmury punktów ze współrzędnymi bezwzględnymi
      • Fuzja map
      • Zalecenia dotyczące planowania trasy dla typowych scen
      • Środki ostrożności
      • FAQ
    • Lixel K2

      • Przegląd produktu
      • Podstawowa obsługa
      • Aktywacja i połączenie urządzenia
      • Przebieg skanowania
      • Pozyskiwanie danych chmury punktów ze współrzędnymi bezwzględnymi
      • Fuzja map
      • Zalecenia dotyczące planowania trasy dla typowych scen
      • Środki ostrożności
      • FAQ
  • Seria Lixel L

    • Lixel L2 Pro

      • Przegląd produktu
      • Podstawowa obsługa
      • Aktywacja i połączenie urządzenia
      • Proces skanowania
      • Pozyskiwanie danych chmury punktów ze współrzędnymi bezwzględnymi
      • Punkt pomiarowy
      • Dodatek
      • FAQ
    • Lixel L3

      • Przegląd produktu
      • Podstawowe funkcje urządzenia
      • Aktywacja i połączenie urządzenia
      • Przechwytywanie danych
      • Przechwytywanie współrzędnych bezwzględnych
      • Dodatek
  • Akcesoria

    • Stick Light

      • Przewodnik instalacji
      • Podstawowa obsługa
      • Często zadawane pytania
  • Lixel Studio

    • Wersja i prawa autorskie
    • Instalacja i aktywacja
    • Interfejs oprogramowania
    • Operacje na plikach
    • Przetwarzanie projektu
    • Narzędzia
    • Rysowanie 2D
    • Zastosowania
    • Ustawienia
    • Połączenie z urządzeniem
  • 07-plugin-sdk

    • Unity

      • Wprowadzenie do Unity SDK
      • Funkcje techniczne
      • Wymagania środowiska deweloperskiego
      • Przewodnik szybkiego startu dla PC
      • Przewodnik optymalizacji ustawień projektu
      • Przewodnik deweloperski dla Pico 4 Ultra
      • Przewodnik strumieniowania PC dla Pico 4 Ultra
      • Przewodnik deweloperski dla Quest 3
      • Przewodnik strumieniowania PC dla Quest 3
      • Przewodnik deweloperski dla OpenXR
      • Przewodnik Business Streaming dla Pico 4 Ultra
      • Słownik
      • Często zadawane pytania (FAQ)
      • Skontaktuj się z nami
      • Przewodnik programowania funkcji

        • Przewodnik mieszanego renderowania wielu scen
        • Przewodnik odbierania cieni
        • Przewodnik oświetlenia w czasie rzeczywistym
        • Przewodnik renderowania wielokamerowego
        • Przewodnik renderowania obrazów wysokiej rozdzielczości
        • Przewodnik zgodności formatów danych LCC i LCC2
        • Przewodnik efektu ładowania
        • Przewodnik przełączania trybu renderowania Mip
        • Przewodnik przyciągania do krawędzi i narożników
        • Przewodnik dekala projekcyjnego
        • Przewodnik wideo dekala projekcyjnego
        • Przewodnik podświetlania
        • Przewodnik przycinania w czasie rzeczywistym
        • Przewodnik korekcji kolorów
        • Przewodnik pobierania danych siatki sceny
        • Przewodnik trybu edytora
        • Przewodnik konfiguracji parametrów wydajności renderowania
        • Przewodnik współpakowania aplikacji i danych
        • Przewodnik systemu kolizji
        • Przewodnik problemów z uprawnieniami w deweloperstwie na macOS
        • Przewodnik kompilacji i pakowania na urządzenia iOS
      • Dokumentacja API

        • LCCManager
        • Renderer
        • Typy danych
      • Dziennik zmian

        • v2.3.0
        • v2.2.10
        • v2.2.0
        • v2.1.19
        • v2.0.14

Przewodnik wideo dekala projekcyjnego

Przegląd funkcji

Wideo dekal projekcyjny jest rozszerzeniem dekala projekcyjnego: rzutowana treść nie jest już statycznym obrazem, lecz wideo. Podejście polega na zdekodowaniu wideo za pomocą wbudowanego w Unity VideoPlayer i dynamicznym aktualizowaniu każdej klatki na teksturze dekala, dzięki czemu wideo jest rzutowane na powierzchnię chmury punktów 3DGS jak rzutnik slajdów.

Ponieważ sam dekal to rzut perspektywiczny stały w przestrzeni świata, klatka wideo jest „naklejana" na powierzchnię chmury punktów zgodnie z pozą projektora. Punkty wewnątrz frustum projektora są kolorowane bieżącą klatką wideo, a punkty poza frustum pozostają nienaruszone.

Uwaga:

Podobnie jak zwykły dekal, dekal wideo działa tylko na PC / Mac (potok URP). Wywoływanie powiązanych interfejsów na platformach mobilnych lub VR jest ignorowane przez SDK i wyświetla ostrzeżenie.

Ten przewodnik wyjaśnia tylko przyrostowe części związane z „wideo". Podstawy — konfiguracja sceny (Kamera główna / LCCManager / LCCRenderer / Projektor), dodanie Decal Depth Feature do renderera URP, dostrajanie odcinania okluzji i szczegółowe API dekala — znajdziesz najpierw w Przewodnik dekala projekcyjnego.

Zasada implementacji

Interfejs dekala SDK SetDecalTexture(Texture2D tex) akceptuje tylko Texture2D, ale obsługuje dynamiczne aktualizowanie tej tekstury w czasie działania. Wyjście dekodowania Unity VideoPlayer to RenderTexture (na GPU). Dlatego cały potok danych to:

VideoPlayer dekoduje → RenderTexture (GPU) → kopiuj do Texture2D → SetDecalTexture(tekstura klatki) → rzutuj na chmurę punktów

Istnieją dwa kluczowe punkty, które są jednocześnie dwoma „wyrównaniami" podkreślanymi w tym przewodniku:

  1. Wyrównanie przestrzenne: pozycja, rozmiar i orientacja klatki wideo rzutowanej na chmurę punktów są określane przez parametry projektora (poza, FOV, Aspect, Near/Far); zobacz Wyrównanie przestrzenne wideo i sceny.
  2. Wyrównanie czasowe: liczba klatek wideo jest często znacznie niższa niż liczba klatek renderowania sceny (scena może działać z setkami FPS), więc nie możesz kopiować i przesyłać tekstury raz na klatkę renderowania. Użyj VideoPlayer.frame, aby wykryć, czy pojawiła się nowa zdekodowana klatka, i aktualizuj tylko wtedy, gdy nadejdzie nowa klatka; zobacz Wyrównywanie liczby klatek wideo i liczby klatek renderowania.
Schemat potoku danych wideo dekala projekcyjnego

Potok danych wideo dekala projekcyjnego: VideoPlayer → RenderTexture → Texture2D → SetDecalTexture

Wymagania wstępne

Dekal wideo dzieli ten sam zestaw wymagań wstępnych co zwykły dekal. Przed rozpoczęciem potwierdź:

  1. Scena ma już skonfigurowane Kamerę główną, LCCManager, LCCRenderer i Projektor (zobacz Przewodnik dekala projekcyjnego · Krok 1).
  2. Obecnie używany jest potok URP; jeśli potrzebne jest odcinanie okluzji, Decal Depth Feature została zarejestrowana w zasobie renderera URP.
  3. Jeśli Konsola zgłasza błędy i nie widać efektu, wyłącz tryb zgodności Render Graph (zobacz sekcję FAQ w Przewodnik dekala projekcyjnego).
  4. Przygotuj zasób wideo: może to być zasób VideoClip w projekcie lub URL pliku wideo na dysku / w sieci (np. file:///D:/sample.mp4).

Krok 1: Napisz skrypt

Utwórz nowy skrypt C# o nazwie VideoStreaming.cs. Opierając się na szkielecie zwykłego Decal.cs, zastępuje „statyczną teksturę dekala" przez „tworzenie VideoPlayer w czasie działania do dekodowania wideo i aktualizowanie tekstury dekala w Update() zgodnie z zdekodowanymi klatkami". Można go skopiować bezpośrednio do projektu.

using LCCCore;
using UnityEngine;
using UnityEngine.Video;

public class VideoStreaming : MonoBehaviour
{
    [Header("Wymagane")]
    [Tooltip("Instancja LCCManager w scenie")]
    public LCCManager m_manager;
    [Tooltip("Prawidłowa ścieżka sceny LCC")]
    public string m_filePath;

    [Header("Źródło wideo (zdekodowane klatki są rzutowane jako dekal)")]
    [Tooltip("Klip wideo do rzutowania. Ma priorytet nad poniższym URL")]
    public VideoClip m_videoClip;
    [Tooltip("Ścieżka pliku wideo / URL, używana, gdy nie przypisano klipu (np. file:///D:/xxx.mp4)")]
    public string m_videoUrl;
    [Tooltip("Odtwarzanie w pętli")]
    public bool m_videoLoop = true;

    [Header("Projektor (rzutuje wzdłuż +Z)")]
    [Tooltip("Poza projektora. Pozostaw puste, aby użyć transformu tego obiektu")]
    public Transform m_projector;
    [Range(1f, 179f)] public float m_fov = 60f;
    [Tooltip("Współczynnik proporcji rzutu; <=0 używa automatycznie proporcji wideo")]
    public float m_aspect = 0f;
    public float m_near = 0.1f;
    public float m_far = 100f;

    [Header("Wygląd dekala")]
    [Range(0f, 1f)] public float m_opacity = 1.0f;
    [Range(0f, 0.5f)][Tooltip("Szerokość zanikania krawędzi (współczynnik UV). 0 = twarda krawędź")]
    public float m_edgeFade = 0.1f;
    [Tooltip("Odświeżaj rzut co klatkę, aby móc przeciągać projektor / dostrajać parametry w czasie działania")]
    public bool m_updateEveryFrame = true;

    [Header("Odcinanie okluzji (tylko PC/Mac)")]
    [Tooltip("Gdy włączone, punkty odwrócone tyłem/zasłonięte za geometrią przednią nie są już kolorowane przez dekal (efekt rzutnika slajdów); gdy wyłączone, jest to prześwietlenie rentgenowskie")]
    public bool m_occlusion = false;
    [Range(0f, 1f)][Tooltip("Próg nieprzezroczystości: tylko splaty o nieprzezroczystości >= tej wartości wnoszą wkład do mapy głębi, filtrując punkty unoszące się/zamglone. 0 = wszystkie uczestniczą")]
    public float m_occlusionCutoff = 0f;
    [Range(0f, 0.5f)][Tooltip("Względna tolerancja głębi. Zbyt mała -> samo-okluzja przy powierzchni; zbyt duża -> przeciekające kolorowanie rentgenowskie. Domyślnie 0.05")]
    public float m_occlusionBias = 0.05f;
    [Range(0, 16)][Tooltip("Górny limit promienia kwadratowego śladu (px), główne pokrętło do usuwania przebijania rentgenowskiego. 0 = pojedynczy teksel, domyślnie 2")]
    public int m_occlusionFootprint = 2;
    [Tooltip("Rozdzielczość mapy głębi (px, potęga dwójki 256~4096). Zmniejsz ją, aby wypełnić dziury przy kątach stycznych. Domyślnie 1024")]
    public int m_occlusionResolution = 1024;

    // -- Stan w czasie działania --
    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;

    // -- Stan wideo --
    private VideoPlayer m_videoPlayer;
    private RenderTexture m_videoRT;   // VideoPlayer dekoduje tutaj wideo (GPU)
    private Texture2D m_frameTex;      // bieżąca klatka jest tutaj kopiowana i przekazywana do dekala
    private bool m_videoReady;
    private long m_lastVideoFrame = -1; // indeks ostatniej klatki przesłanej do dekala

    void Start()
    {
        if (m_manager == null || string.IsNullOrEmpty(m_filePath))
        {
            Debug.LogError("[VideoStreaming] m_manager lub m_filePath nie jest przypisany.");
            enabled = false;
            return;
        }

        // 1) Zainicjalizuj potok renderowania do trybu edytora pojedynczej sceny PC
        m_manager.SetPlatformType(PlatformType.PC);
        m_manager.SwitchRenderPass(ActiveRenderMode.SingleRender);
        m_manager.SetEditorMode(true);

        // 2) Rozpocznij dekodowanie wideo równolegle z ładowaniem sceny
        SetupVideo();

        // 3) Załaduj scenę chmury punktów, a następnie włącz dekal (dekal jest stanem globalnym; najbezpieczniej ustawić go, gdy scena jest gotowa)
        var renderer = m_manager.GetRender(this.transform);
        Debug.Log($"[VideoStreaming] Rozpoczynanie ładowania: {m_filePath}");
        renderer.Load(m_filePath,
            _ => { /* wywołanie zwrotne postępu, nieużywane w tym przykładzie */ },
            OnSceneLoaded);
    }

    /// <summary>Utwórz VideoPlayer w czasie działania i rozpocznij przygotowywanie klipu/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; // demo: bez dźwięku

        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;
        }

        // Przydziel bufor klatki, gdy znana jest rozdzielczość, a następnie rozpocznij odtwarzanie.
        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] Wideo przygotowane: {w}x{h}, odtwarzanie.");
    }

    private void OnSceneLoaded()
    {
        Debug.Log("[VideoStreaming] Ładowanie zakończone, włączanie rzutowania dekala.");

        // -- Rzut podstawowy: 5 API --
        // Zawartość tekstury jest sterowana przez zdekodowane klatki w Update(); tutaj najpierw przesyłamy bieżącą dostępną klatkę.
        if (m_frameTex != null) m_manager.SetDecalTexture(m_frameTex);
        m_manager.SetDecalOpacity(m_opacity);        // ogólna nieprzezroczystość
        m_manager.SetDecalEdgeFade(m_edgeFade);      // zanikanie krawędzi
        ApplyProjection();                           // poza projektora (wewnętrznie wywołuje SetDecalProjection)
        m_manager.SetDecalEnabled(true);             // włącz dekal (słowo kluczowe _DECAL)

        // -- Odcinanie okluzji: 2 API -- muszą być po ApplyProjection (mapa głębi potrzebuje prawidłowej macierzy VP projektora)
        m_manager.SetDecalOcclusionSettings(BuildOcclusionSettings());
        m_manager.SetDecalOcclusion(m_occlusion);
        CacheOcclusionState();

        m_loaded = true;
    }

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

        // --- Aktualizacja klatki wideo, oddzielona od FPS renderowania ---
        // VideoPlayer.frame zwiększa się tylko z częstotliwością odtwarzania wideo. Nawet jeśli scena renderuje z setkami FPS,
        // indeks pozostaje taki sam między dwiema sąsiednimi klatkami wideo, więc tylko wtedy, gdy rzeczywiście pojawi się nowa zdekodowana klatka,
        // kopiujemy tę klatkę i ponownie przesyłamy teksturę dekala.
        if (m_videoReady && m_videoPlayer.frame != m_lastVideoFrame)
        {
            m_lastVideoFrame = m_videoPlayer.frame;

            // Skopiuj RenderTexture na GPU do Texture2D akceptowanej przez API dekala.
            // Działa to tylko z częstotliwością klatek wideo, więc koszt odczytu zwrotnego jest akceptowalny.
            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);
        }

        // przełącz okluzję w czasie działania
        if (m_occlusion != m_lastOcclusion)
        {
            m_manager.SetDecalOcclusion(m_occlusion);
            m_lastOcclusion = m_occlusion;
        }

        // jeśli zmienił się dowolny parametr jakości okluzji -> wyślij strukturę raz
        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();
        }

        // odświeżaj projektor / wygląd co klatkę dla wygodnego dostrajania w czasie działania
        if (!m_updateEveryFrame) return;
        m_manager.SetDecalOpacity(m_opacity);
        m_manager.SetDecalEdgeFade(m_edgeFade);
        ApplyProjection();
    }

    /// <summary>Zbuduj macierz rzutowania z bieżącej pozy projektora i parametrów perspektywy.</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>Zbierz 4 parametry okluzji do struktury.</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);
    }

    // -- Wygodne punkty wejścia debugowania (kliknięcie prawym przyciskiem w Inspektorze) --
    [ContextMenu("Dekal włączony")]  private void DecalOn()  { m_manager?.SetDecalEnabled(true); }
    [ContextMenu("Dekal wyłączony")] private void DecalOff() { m_manager?.SetDecalEnabled(false); }
    [ContextMenu("Okluzja włączona")]  private void OcclusionOn()  { m_occlusion = true;  m_manager?.SetDecalOcclusion(true);  m_lastOcclusion = true; }
    [ContextMenu("Okluzja wyłączona")] private void OcclusionOff() { m_occlusion = false; m_manager?.SetDecalOcclusion(false); m_lastOcclusion = false; }
}

Uwaga:

  • Wideo i ładowanie sceny działają równolegle: w Start() najpierw wywoływane jest SetupVideo(), aby rozpocząć VideoPlayer.Prepare(), a następnie inicjowane jest Load sceny chmury punktów. Oba się nie blokują.
  • Bufor klatki jest przydzielany dopiero po przygotowaniu wideo: rozmiary RenderTexture i Texture2D są pobierane z rzeczywistej rozdzielczości wideo (vp.width / vp.height), więc są tworzone w wywołaniu zwrotnym prepareCompleted OnVideoPrepared.
  • Dekal jest włączany dopiero po zakończeniu ładowania sceny: zgodnie ze zwykłym dekalem, SetDecalTexture / SetDecalEnabled są umieszczane w wywołaniu zwrotnym OnSceneLoaded. Jeśli wideo nie jest wtedy gotowe, m_frameTex jest null i pierwsze ustawienie tekstury jest pomijane; jest wypełniane, gdy nadejdzie pierwsza klatka w Update().

Krok 2: Dołącz skrypt i skonfiguruj właściwości

  1. Dołącz skrypt VideoStreaming.cs do GameObject LCCRenderer w oknie Hierarchia.
  2. W oknie Inspektor przeciągnij GameObject LCCManager na właściwość Manager skryptu.
  3. Przeciągnij GameObject Projector na właściwość Projector skryptu.
  4. Wpisz ścieżkę pliku sceny LCC we właściwości File Path.
  5. Określ źródło wideo, wybierz jedno z dwóch:
    • Przeciągnij zasób VideoClip z projektu na właściwość Video Clip; lub
    • Wpisz ścieżkę pliku wideo we właściwości Video Url (np. file:///D:/sample.mp4).
    • Gdy oba są wypełnione, Video Clip ma priorytet.
  6. Pozostałe właściwości można dostosować w razie potrzeby; ich znaczenia pokazano w tabeli poniżej.
Ustawienia właściwości w Inspektorze dla skryptu VideoStreaming

Ustawienia właściwości w Inspektorze dla skryptu VideoStreaming

PoleZnaczenieWartość domyślna
ManagerInstancja LCCManager w scenieWymagane
File PathPrawidłowa ścieżka sceny LCCWymagane
Video ClipZasób wideo do rzutowania, wyższy priorytet niż Video UrlPuste
Video UrlŚcieżka pliku wideo / URL, używana, gdy nie określono klipuPuste
Video LoopCzy odtwarzać w pętliZaznaczone
ProjectorPoza projektora, +Z to kierunek rzutowania; puste używa Transformu tego obiektuPuste
FovPerspektywiczne pole widzenia projektora60
AspectWspółczynnik proporcji rzutu; gdy mniejszy lub równy 0, jest obliczany automatycznie z proporcji wideo0 (auto)
Near / FarBliska / daleka płaszczyzna przycinania frustum projektora0.1 / 100
OpacityOgólna nieprzezroczystość dekala1
Edge FadeSzerokość zanikania krawędzi (współczynnik UV), 0 to twarda krawędź0.1
Update Every FrameOdświeżaj projektor / wygląd co klatkę, wygodne przy dostrajaniu w czasie działaniaZaznaczone
OcclusionCzy włączyć odcinanie okluzjiWyłączone
Occlusion Cutoff / Bias / Footprint / ResolutionParametry jakości odcinania okluzji; zobacz Przewodnik dekala projekcyjnego0 / 0.05 / 2 / 1024

Krok 3: Uruchom i obserwuj efekt

Kliknij Odtwórz w Unity. Wideo zaczyna odtwarzać się po zakończeniu Prepare; po zakończeniu ładowania sceny dekal włącza się automatycznie, a klatki wideo są rzutowane na powierzchnię 3DGS klatka po klatce. Gdy Update Every Frame jest zaznaczone, możesz bezpośrednio przeciągać projektor w widoku Scena i widzieć, jak klatka wideo przesuwa się po powierzchni chmury punktów w czasie rzeczywistym.

Klatki wideo rzutowane na powierzchnię 3DGS po uruchomieniu

Klatki wideo rzutowane na powierzchnię 3DGS po uruchomieniu

Wyrównanie przestrzenne wideo i sceny

„Wyrównanie przestrzenne" oznacza sprawienie, aby klatka wideo rzutowała dokładnie na chmurę punktów w pozycji, orientacji i rozmiarze, jakich chcesz. Pozycja klatki wideo jest całkowicie określana przez projektor (SetDecalProjection), niezależnie od zawartości wideo. Parametry wpływające na wyrównanie są następujące:

ParametrŹródłoWpływ na wyrównanie
Pozycja projektora (Projector.position)Transform projektoraOkreśla współrzędną świata wierzchołka frustum, czyli skąd wideo jest rzutowane „od". Przesuwanie jej przesuwa miejsce, w którym klatka ląduje na chmurze punktów.
Orientacja projektora (Projector.rotation)Transform projektora+Z to kierunek rzutowania. Obracanie projektora zmienia kierunek rzutowania, a klatka odpowiednio przesuwa się / przechyla na powierzchni.
Fov (pionowe pole widzenia)Pole skryptu m_fovOkreśla kąt otwarcia frustum, bezpośrednio wpływając na rozmiar klatki na chmurze punktów: im większy FOV, tym większa klatka; im mniejszy, tym bardziej skoncentrowana. Zakres [1, 179].
Aspect (współczynnik proporcji)Pole skryptu m_aspectOkreśla poziomy/pionowy stosunek klatki. Gdy ustawione na 0, automatycznie używany jest współczynnik proporcji rozdzielczości wideo (m_videoRT.width / height), co jest zalecanym sposobem uniknięcia rozciągania wideo. Błędna ręczna wartość powoduje rozciągnięcie klatki.
Near / Far (bliska / daleka płaszczyzna przycinania)Pola skryptu m_near / m_farOkreślają efektywny przedział głębokości frustum wzdłuż kierunku rzutowania. Kolorowane są tylko punkty w [near, far]. Zbyt małe Far przycina odległe chmury punktów, a zbyt duże Near pomija bliskie. Powinien obejmować zakres głębokości docelowej chmury punktów.

Ogólne kroki wyrównania:

  1. Najpierw pozostaw Aspect na 0 (auto), aby uniknąć zniekształcenia klatki, i skup się na dostosowaniu pozycji i rozmiaru.
  2. Przeciągaj pozycję i orientację Projektora, aby wyśrodkować klatkę na obszarze docelowym (zaznacz Update Every Frame dla podglądu w czasie rzeczywistym).
  3. Dostosuj Fov, aby kontrolować rozmiar pokrycia klatki.
  4. Potwierdź, że przedział Near / Far w pełni obejmuje głębokość docelowej chmury punktów, aby uniknąć przycinania klatki przez płaszczyzny przycinania.
  5. Jeśli musisz wymusić określony stosunek (np. rzutowanie wideo 16:9 na kwadratowy obszar), ustaw Aspect ręcznie.

Wskazówka:

Proporcje klatki wideo są kontrolowane przez Aspect, a rozdzielczość wideo wpływa tylko na klarowność i koszt odczytu zwrotnego, nie na fizyczny rozmiar rzutowany na chmurę punktów — rozmiar jest określany wspólnie przez Fov i odległość rzutowania.

Wpływ parametrów projektora na to, gdzie i jak duża klatka wideo ląduje na chmurze punktów

Wpływ pozy projektora / Fov / Aspect / Near-Far na to, gdzie i jak duża klatka wideo ląduje na chmurze punktów

Wyrównywanie liczby klatek wideo i liczby klatek renderowania

To najistotniejsza część dekala wideo. Liczba klatek wideo (np. 25/30 FPS) jest zwykle znacznie niższa niż liczba klatek renderowania sceny (scena 3DGS może działać z setkami FPS). Jeśli wykonasz „odczyt zwrotny + SetDecalTexture" co klatkę renderowania, pojawiają się dwa problemy:

  • Zbędna praca: ta sama klatka wideo jest wielokrotnie kopiowana i wielokrotnie przesyłana, co jest czystym marnotrawstwem.
  • Spadek wydajności: ReadPixels to odczyt zwrotny GPU→CPU o nietrywialnym koszcie; wykonywanie go co klatkę przy wysokiej liczbie klatek renderowania powoduje zauważalne spadki klatek.

Strategia rozprzężenia: użyj VideoPlayer.frame jako rytmu

VideoPlayer.frame to bieżący indeks zdekodowanej klatki, który zwiększa się tylko z częstotliwością odtwarzania wideo. Niezależnie od tego, jak wysoka jest liczba klatek renderowania, indeks ten nie zmienia się między dwiema klatkami wideo. Na tej podstawie aktualizuj teksturę tylko wtedy, gdy „indeks się zmienia":

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

    // Odczytaj zwrotnie tylko raz i prześlij dekal, gdy pojawi się nowa zdekodowana klatka
    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);
}

Dzięki temu:

  • Gdy scena renderuje z 300 FPS, a wideo ma 30 FPS, tylko około 30 z 300 klatek renderowania faktycznie wykonuje odczyt zwrotny i przesłanie; pozostałe klatki są pomijane bezpośrednio przez if.
  • Rytm jest kontrolowany przez sam odtwarzacz; nie potrzeba ręcznego timera do dopasowania liczby klatek wideo, a żadne klatki nie są gubione ani duplikowane z powodu wahań liczby klatek renderowania.
  • Szybkość, zapętlanie i wstrzymywanie wideo są zarządzane wewnętrznie przez VideoPlayer, a indeks frame odzwierciedla je wiernie; skrypt nie potrzebuje dodatkowej synchronizacji.

Dlaczego nie użyć timera per-klatkowego?

Ręczne sumowanie Time.deltaTime w celu przybliżenia interwału klatek wideo wymaga obsługi zarówno drgań liczby klatek renderowania, jak i zmian szybkości wideo / pomijania klatek, i może łatwo rozjechać się z rzeczywistym wewnętrznym postępem dekodowania odtwarzacza. Bezpośredni odczyt VideoPlayer.frame czyni odtwarzacz jedynym źródłem prawdy — najprostsze i najbardziej niezawodne.

O Update Every Frame:

m_updateEveryFrame w skrypcie kontroluje, czy poza projektora / nieprzezroczystość / zanikanie są odświeżane co klatkę, co jest odrębną sprawą od aktualizacji klatki wideo. Aktualizacja klatki wideo zawsze podąża za rytmem VideoPlayer.frame i nie zależy od tego przełącznika.

Uwagi dotyczące wydajności i formatów

  • Koszt odczytu zwrotnego jest proporcjonalny do rozdzielczości. ReadPixels + Apply to podróż w obie strony GPU→CPU→GPU; im wyższa rozdzielczość, tym wolniej. Ten przykład używa tego dla prostoty i wystarczającej demonstracji. Jeśli rozdzielczość wideo jest bardzo wysoka lub wydajność jest wrażliwa, możesz przełączyć się na czyste kopiowanie GPU Graphics.CopyTexture(m_videoRT, m_frameTex), ale formaty źródła i celu muszą pasować (RenderTextureFormat.ARGB32 odpowiada TextureFormat.RGBA32), a niektóre platformy ograniczają CopyTexture.
  • SetDecalTexture akceptuje tylko Texture2D. Właśnie dlatego RenderTexture musi być odczytana z powrotem do Texture2D; nie możesz bezpośrednio przekazać RenderTexture z VideoPlayer do interfejsu dekala.
  • Używaj ponownie bufora klatki, nie new co klatkę. W przykładzie m_frameTex jest tworzony tylko raz, gdy wideo jest gotowe, a ta sama Texture2D jest ponownie używana co klatkę, unikając częstej alokacji / GC.
  • Zwalniaj zasoby w odpowiednim czasie. W OnDestroy wywołaj Release() i Destroy dla RenderTexture i Texture2D oraz wyrejestruj prepareCompleted, aby uniknąć wycieków.
  • Dźwięk nie jest podłączony. Przykład ustawia audioOutputMode na None i demonstruje tylko wizualizację. Jeśli potrzebny jest dźwięk, zmień na tryb AudioSource i dołącz AudioSource.

Podsumowanie różnic w stosunku do zwykłego dekala

AspektZwykły dekal (Decal.cs)Dekal wideo (VideoStreaming.cs)
Zawartość dekalaStatyczna Texture2DTexture2D z per-klatkowego dekodowania wideo
Źródło teksturyPrzeciągnij obraz w InspektorzeVideoPlayer dekoduje → RenderTexture → odczyt zwrotny
Moment aktualizacji teksturyUstawiana razAktualizowana klatka po klatce w rytmie VideoPlayer.frame
Dodatkowy komponentBrakAddComponent<VideoPlayer>() w czasie działania
Wartość auto AspectProporcje obrazuProporcje wideo
Reszta (projektor / okluzja / API)Dokładnie taka samaDokładnie taka sama

Kluczowe API

Po stronie SDK dekal wideo dodatkowo wykorzystuje tylko zdolność dynamicznej aktualizacji SetDecalTexture; pozostałe API dekala są dokładnie takie same jak w zwykłym dekalu. Zobacz Przewodnik dekala projekcyjnego · API powiązane z dekalem.

SetDecalTexture (aktualizacja dynamiczna)

// Wartość zwracana: void
public void SetDecalTexture(Texture2D tex)

Ustawia teksturę dekala. RGB to kolor, a A to własna maska dekala. Ta metoda obsługuje wielokrotne wywoływanie w czasie działania: za każdym razem, gdy przekazywana jest nowa Texture2D, bieżąca zawartość dekala jest natychmiast zastępowana — to dokładnie podstawa rzutowania wideo klatka po klatce: wywołaj ją raz, gdy w Update() pojawi się nowa zdekodowana klatka.

ParametrTypOpis
texTexture2DTekstura dekala, może być wielokrotnie zastępowana w czasie działania

Pozostałe powiązane wbudowane typy Unity (VideoPlayer / RenderTexture / Texture2D) to standardowe API Unity; sposób użycia znajdziesz w oficjalnej dokumentacji Unity.