XGRIDSДокументация
  • 简体中文
  • English
  • 繁體中文
  • 日本語
  • Deutsch
  • Español
  • Italiano
  • Français
  • Русский
  • Polski
  • Nederlands
  • 简体中文
  • English
  • 繁體中文
  • 日本語
  • Deutsch
  • Español
  • Italiano
  • Français
  • Русский
  • Polski
  • Nederlands
  • Lixel CyberColor

    • Руководство пользователя LCC Studio

      • Начало работы
      • Версия и обновления
      • Загрузка и установка
      • Обзор интерфейса и навигация
      • Перед реконструкцией
      • Реконструкция модели
      • Реконструкция одиночной модели
      • Объединение карт
      • Воздушно-наземное объединение
      • Воздушная реконструкция
      • Мои модели
      • Другие функции
      • Настройки и учётная запись
      • Конвертер
      • Реконструкция по видео
      • Часто задаваемые вопросы
    • LCC Scene Editor

      • Версия и обновления
      • Учётная запись и вход
      • Обзор продукта и главная страница
      • Интерфейс редактора
      • Режимы навигации по сцене
      • Файл
      • Настройки
      • Операции редактирования
      • Окно
      • Глобальная панель инструментов
      • Ресурсы и свойства
      • Левая панель инструментов
      • Точки обзора
      • Портал
      • Skybox
      • Аннотации
      • Измерение
      • Flythrough
      • Отчёт по сцене
      • 3D Layout
      • Мини-карта
      • Preview Mode (Viewer)
      • Справка
      • Часто задаваемые вопросы
      • Точка появления
    • LCC Model Editor

      • Версия и обновления
      • Руководство пользователя
      • Обзор и интерфейс
      • Операции с файлами
      • Инструменты выбора
      • Редактирование моделей
      • Измерение
      • Цветокоррекция
      • Управление ресурсами
      • Настройки и справка
      • Часто задаваемые вопросы
    • Рекомендации по применению

      • Объединение нескольких крупных моделей 3DGS
  • Plugin & SDK

    • Unreal

      • Введение
      • Быстрый старт — Windows
      • Быстрый старт — Linux
      • Быстрый старт — Quest3
      • Редакции и лицензирование
      • Рендеринг
      • Растеризация Tiled (экспериментально)
      • Визуальные настройки
      • Нормали и освещение
      • Редактирование сцены
      • Параметры производительности
      • Руководство по оптимизации производительности
      • Интеграция со сторонними и движковыми плагинами
      • Прокси-меш
      • Анимация загрузки
      • Коллизии
      • Поддержка системы навигации
      • Поддержка однослойной воды
      • Локализация
      • Частые вопросы
      • Устранение неполадок
      • Журналы и диагностика
      • Свяжитесь с нами
      • Рекомендуемые практики

        • Переосвещение 3DGS с помощью меша LixelStudio
      • Справочник API

        • ALCCActorBase
        • ULCCComponentBase
        • ULCCComponent
        • ULCC2Component
        • Actor SOG / SPZ / PLY
        • 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
      • Глоссарий
      • FAQ
      • Свяжитесь с нами
      • Руководство по разработке функций

        • Руководство по разработке смешанного рендеринга нескольких сцен
        • Руководство по разработке приёма теней
        • Руководство по разработке освещения в реальном времени
        • Руководство по разработке многокамерного рендеринга
        • Руководство по разработке рендеринга изображений высокого разрешения
        • Руководство по разработке совместимости форматов данных LCC и LCC2
        • Руководство по разработке эффекта загрузки
        • Руководство по разработке переключения режима рендеринга Mip
        • Руководство по разработке привязки к рёбрам и углам
        • Руководство по разработке проецируемых декалей
        • Руководство по разработке видеопроецируемой декали
        • Руководство по разработке подсветки
        • Руководство по разработке обрезки в реальном времени
        • Руководство по разработке коррекции цвета
        • Руководство по разработке получения данных Mesh сцены
        • Руководство по разработке в режиме редактора
        • Руководство по настройке параметров производительности рендеринга
        • Руководство по разработке совместной упаковки приложения и данных
        • Руководство по разработке системы коллизий
        • Руководство по проблемам с разрешениями при разработке на macOS
        • Руководство по разработке компиляции и упаковки для устройств iOS
      • Справочник по API

        • LCCManager
        • Renderer
        • Типы данных
      • Журнал изменений

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

Руководство по разработке видеопроецируемой декали

Обзор возможностей

Видеопроецируемая декаль — это расширение Проецируемой декали: проецируемое содержимое больше не является статичным изображением, а видео. Подход заключается в декодировании видео встроенным в Unity VideoPlayer и динамическом обновлении каждого кадра на текстуре декали, чтобы видео проецировалось на поверхность облака точек 3DGS, подобно слайд-проектору.

Поскольку сама декаль — это перспективная проекция, зафиксированная в мировом пространстве, видеокадр «наклеивается» на поверхность облака точек в соответствии с позой проектора. Точки внутри фрустума проектора окрашиваются текущим видеокадром, а точки вне фрустума не затрагиваются.

Примечание:

Как и обычная декаль, видеодекаль действует только на PC / Mac (конвейер URP). Вызовы связанных интерфейсов на платформах mobile или VR игнорируются SDK и выводят предупреждение.

Это руководство объясняет только инкрементальные части, связанные с «видео». По основам — настройке сцены (Main Camera / LCCManager / LCCRenderer / Projector), добавлению Decal Depth Feature в URP Renderer, настройке отсечения окклюзией и подробным API декалей — сначала прочтите Руководство по разработке проецируемой декали.

Принцип реализации

Интерфейс декали SDK SetDecalTexture(Texture2D tex) принимает только Texture2D, но поддерживает динамическое обновление этой текстуры во время выполнения. Результатом декодирования VideoPlayer Unity является 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; если требуется отсечение окклюзией, Decal Depth Feature зарегистрирован в URP Renderer Asset.
  3. Если Console сообщает об ошибках и эффект не виден, отключите режим совместимости Render Graph (см. раздел FAQ Руководство по разработке проецируемой декали).
  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("Обязательные")]
    [Tooltip("Экземпляр LCCManager в сцене")]
    public LCCManager m_manager;
    [Tooltip("Действительный путь к LCC-сцене")]
    public string m_filePath;

    [Header("Источник видео (декодированные кадры проецируются как декаль)")]
    [Tooltip("Видеоклип для проецирования. Имеет приоритет над URL ниже")]
    public VideoClip m_videoClip;
    [Tooltip("Путь к видеофайлу / URL, используется, когда клип не назначен (например, file:///D:/xxx.mp4)")]
    public string m_videoUrl;
    [Tooltip("Зацикленное воспроизведение")]
    public bool m_videoLoop = true;

    [Header("Проектор (проецирует вдоль +Z)")]
    [Tooltip("Поза проектора. Оставьте пустым, чтобы использовать Transform этого объекта")]
    public Transform m_projector;
    [Range(1f, 179f)] public float m_fov = 60f;
    [Tooltip("Соотношение сторон проекции; при <=0 автоматически используется соотношение сторон видео")]
    public float m_aspect = 0f;
    public float m_near = 0.1f;
    public float m_far = 100f;

    [Header("Внешний вид декали")]
    [Range(0f, 1f)] public float m_opacity = 1.0f;
    [Range(0f, 0.5f)][Tooltip("Ширина затухания края (отношение UV). 0 = жёсткий край")]
    public float m_edgeFade = 0.1f;
    [Tooltip("Обновлять проекцию каждый кадр, чтобы можно было перетаскивать проектор / менять параметры во время выполнения")]
    public bool m_updateEveryFrame = true;

    [Header("Отсечение окклюзией (только PC/Mac)")]
    [Tooltip("При включении задние грани/окклюдированные точки за передней геометрией больше не окрашиваются декалью (эффект слайд-проектора); при выключении — эффект рентгена")]
    public bool m_occlusion = false;
    [Range(0f, 1f)][Tooltip("Порог непрозрачности: только сплаты с непрозрачностью >= этого значения участвуют в карте глубины, отфильтровывая плавающие/туманные точки. 0 = участвуют все")]
    public float m_occlusionCutoff = 0f;
    [Range(0f, 0.5f)][Tooltip("Относительный допуск глубины. Слишком мало -> самоокклюзия у поверхности; слишком много -> утечка рентгеновского окрашивания. По умолчанию 0.05")]
    public float m_occlusionBias = 0.05f;
    [Range(0, 16)][Tooltip("Ограничение квадратного радиуса следа (px), главная ручка для устранения рентгеновского просвечивания. 0 = один тексель, по умолчанию 2")]
    public int m_occlusionFootprint = 2;
    [Tooltip("Разрешение карты глубины (px, степень двойки 256~4096). Уменьшите, чтобы заполнить дыры на скользящих углах. По умолчанию 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 или m_filePath не назначены.");
            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] Начинаем загрузку: {m_filePath}");
        renderer.Load(m_filePath,
            _ => { /* обратный вызов прогресса, в примере не используется */ },
            OnSceneLoaded);
    }

    /// <summary>Создаёт VideoPlayer во время выполнения и начинает подготовку клипа/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] Видео подготовлено: {w}x{h}, воспроизведение.");
    }

    private void OnSceneLoaded()
    {
        Debug.Log("[VideoStreaming] Загрузка завершена, включение проекции декали.");

        // -- Базовая проекция: 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)

        // -- Отсечение окклюзией: 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;

            // Копируем RenderTexture на GPU в Texture2D, принимаемый API декали.
            // Это выполняется только с частотой кадров видео, поэтому стоимость чтения приемлема.
            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("Включить декаль")]  private void DecalOn()  { m_manager?.SetDecalEnabled(true); }
    [ContextMenu("Выключить декаль")] private void DecalOff() { m_manager?.SetDecalEnabled(false); }
    [ContextMenu("Включить окклюзию")]  private void OcclusionOn()  { m_occlusion = true;  m_manager?.SetDecalOcclusion(true);  m_lastOcclusion = true; }
    [ContextMenu("Выключить окклюзию")] 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 равен null, и первая установка текстуры пропускается; она заполняется при появлении первого кадра в Update().

Шаг 2: Прикрепите скрипт и настройте свойства

  1. Прикрепите скрипт VideoStreaming.cs к GameObject LCCRenderer в окне Hierarchy.
  2. В окне Inspector перетащите GameObject LCCManager на свойство Manager скрипта.
  3. Перетащите GameObject Projector на свойство Projector скрипта.
  4. Заполните путь к файлу LCC-сцены в свойстве File Path.
  5. Укажите источник видео, выбрав одно из двух:
    • Перетащите ассет VideoClip из проекта на свойство Video Clip; или
    • Заполните путь к видеофайлу в свойстве Video Url (например, file:///D:/sample.mp4).
    • Когда заполнены оба, Video Clip имеет приоритет.
  6. Остальные свойства можно настроить по мере необходимости; их значения показаны в таблице ниже.
Настройки свойств Inspector для скрипта VideoStreaming

Настройки свойств Inspector для скрипта VideoStreaming

ПолеЗначениеЗначение по умолчанию
ManagerЭкземпляр LCCManager в сценеОбязательное
File PathДействительный путь к LCC-сценеОбязательное
Video ClipВидеоассет для проецирования, приоритет выше, чем Video UrlПусто
Video UrlПуть к видеофайлу / URL, используется, когда клип не указанПусто
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: Запустите и наблюдайте эффект

Нажмите Play в Unity. Видео начнёт воспроизводиться после завершения Prepare; после завершения загрузки сцены декаль включится автоматически, и видеокадры будут проецироваться на поверхность 3DGS кадр за кадром. При отмеченном Update Every Frame вы можете напрямую перетаскивать проектор в окне Scene и видеть движение видеокадра по поверхности облака точек в реальном времени.

Видеокадры, спроецированные на поверхность 3DGS после запуска

Видеокадры, спроецированные на поверхность 3DGS после запуска

Пространственное выравнивание видео и сцены

«Пространственное выравнивание» означает точную проекцию видеокадра на облако точек в нужной позиции, ориентации и размере. Позиция видеокадра полностью определяется проектором (SetDecalProjection), независимо от содержимого видео. Параметры, влияющие на выравнивание, следующие:

ПараметрИсточникВлияние на выравнивание
Позиция проектора (Projector.position)Transform проектораОпределяет мировую координату вершины фрустума, то есть откуда проецируется видео. Перемещение сдвигает место посадки кадра на облаке точек.
Ориентация проектора (Projector.rotation)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, только около 30 из 300 кадров рендеринга фактически выполняют чтение и отправку; остальные кадры пропускаются напрямую через 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; нельзя напрямую передать RenderTexture VideoPlayer в интерфейс декали.
  • Переиспользуйте буфер кадра, не создавайте new каждый кадр. В примере m_frameTex создаётся только один раз при готовности видео, и тот же Texture2D переиспользуется каждый кадр, избегая частого выделения / GC.
  • Своевременно освобождайте ресурсы. В OnDestroy вызовите Release() и Destroy для RenderTexture и Texture2D, а также отпишитесь от prepareCompleted, чтобы избежать утечек.
  • Звук не подключён. В примере audioOutputMode задан как None и демонстрируется только визуальная часть. Если нужен звук, измените на режим AudioSource и прикрепите AudioSource.

Сводка отличий от обычной декали

АспектОбычная декаль (Decal.cs)Видеодекаль (VideoStreaming.cs)
Содержимое декалиСтатичный Texture2DTexture2D из покадрового декодирования видео
Источник текстурыПеретаскивание изображения в InspectorVideoPlayer декодирует → 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) — это стандартные API Unity; по их использованию см. официальную документацию Unity.