SubViewport: отдельная камера и живая текстура
Показать отдельный 3D-мир в интерфейсе и управлять обновлением кадра.
Запустить живую текстуру
Скачать проект. Нужны Reckfell 1.0.0 с C# SDK и Forward+/Vulkan. Распакуйте архив, откройте project.reckfell, соберите C# и запустите Main.rscene. Все модели и текстуры включены: интерьер Pawn Shop от Poly Haven, CC0, со светом и материалами, адаптированными для Reckfell.
У корня установите Preview=Original до запуска. В этом примере 3D-комната действительно рендерится отдельной камерой в SubViewport; TextureRect показывает её текущую текстуру. Это не сохранённая фотография.
Разделить рендер и вывод
| Объект | Ответственность |
|---|---|
| SourceView: SubViewport | собственный World3D, камера комнаты, размер рендера и частота обновления |
| SourceView.GetTexture() | живая ViewportTexture с результатом SourceView |
| Display: TextureRect | место и размер показа этой текстуры в интерфейсе |
| TonePass, Copy, InvertPass | обработка уже показанного изображения; разбор в следующем руководстве |
SubViewport сам по себе не создаёт прямоугольник на экране. Его результат должен использовать TextureRect, материал или другой потребитель текстуры. В примере камера находится внутри SourceView. Явный World3D отделяет комнату от мира родительского viewport. Если World3D не задан, FindWorld3D ищет доступный мир у самого viewport и его предков.
Размер SourceView.Size задаётся в пикселях. Размер Display.Size управляет отображением и не меняет разрешение рендера. Здесь оба равны 2560×1440, но обязательного соотношения 2:1 у SubViewport нет. При изменении Size уже полученная ViewportTexture сообщает новый размер; повторно назначать Display.Texture не требуется.
Использовать готовый контроллер
ViewportDisplay.cs — корневой Control. PawnShop3D.cs и каталог Assets уже находятся в архиве. GetTexture возвращает ресурс для показа; GetTexture().GetImage() получает отдельное изображение для чтения на CPU. Для живого экрана не нужно вызывать GetImage каждый кадр или сохранять PNG.
using Reckfell;
public partial class ViewportDisplay : Control
{
public enum Mode { Original, Grayscale, Chained }
[Export] public Mode Preview { get; set; } = Mode.Grayscale;
public SubViewport SourceView { get; private set; } = null!;
public PawnShop3D Room { get; private set; } = null!;
public TextureRect Display { get; private set; } = null!;
public ColorRect TonePass { get; private set; } = null!;
public ColorRect InvertPass { get; private set; } = null!;
public BackBufferCopy Copy { get; private set; } = null!;
public ShaderMaterial ToneMaterial { get; private set; } = null!;
public override void _Ready()
{
var size=new Vector2I(2560,1440);
SourceView=new SubViewport { Size=size,World3D=new World3D(),
RenderTargetUpdateMode=SubViewport.UpdateMode.Always,
Msaa3D=Viewport.Msaa.Msaa8X,UseTaa=true,Scaling3DScale=1.5f,
PositionalShadowAtlasSize=4096 };
AddChild(SourceView);
Room=new PawnShop3D();SourceView.AddChild(Room);
// Bind the viewport's live texture; do not replace it with GetImage().
Display=new TextureRect { Texture=SourceView.GetTexture(),Size=size,
ExpandMode=TextureRect.ExpandModeEnum.IgnoreSize,
StretchMode=TextureRect.StretchModeEnum.Scale,MouseFilter=MouseFilterEnum.Ignore };
AddChild(Display);
ToneMaterial=new ShaderMaterial { Shader=ResourceLoader.Load<Shader>("asset://ScreenTone.reckfellshader") };
TonePass=new ColorRect { Size=size,Material=ToneMaterial,MouseFilter=MouseFilterEnum.Ignore };
AddChild(TonePass);
// Draw order is important: display -> tone -> copy -> invert.
Copy=new BackBufferCopy { CopyMode=BackBufferCopy.CopyModeEnum.Disabled };AddChild(Copy);
InvertPass=new ColorRect { Size=size,MouseFilter=MouseFilterEnum.Ignore,
Material=new ShaderMaterial { Shader=ResourceLoader.Load<Shader>("asset://ScreenInvert.reckfellshader") } };
AddChild(InvertPass);
SetPreview(Preview);
}
public void SetPreview(Mode mode)
{
TonePass.Position=Vector2.Zero;TonePass.Size=new Vector2(2560,1440);
InvertPass.Position=Vector2.Zero;InvertPass.Size=TonePass.Size;
TonePass.Visible=mode!=Mode.Original;
ToneMaterial.SetShaderParameter("desaturate",1.0f);
Copy.CopyMode=mode==Mode.Chained?BackBufferCopy.CopyModeEnum.Viewport:BackBufferCopy.CopyModeEnum.Disabled;
InvertPass.Visible=mode==Mode.Chained;
}
// Disable only drawing. Scripts and physics do not pause with the render target.
public void Freeze()=>SourceView.RenderTargetUpdateMode=SubViewport.UpdateMode.Disabled;
public void RefreshOnce()=>SourceView.RenderTargetUpdateMode=SubViewport.UpdateMode.Once;
public void Resume()=>SourceView.RenderTargetUpdateMode=SubViewport.UpdateMode.Always;
}Обновлять только когда нужно
Вызывайте методы после _Ready:
| Действие | Метод примера | Что происходит |
|---|---|---|
| непрерывный рендер | Resume() | RenderTargetUpdateMode=Always |
| сохранить последний кадр | Freeze() | Disabled останавливает перерисовку |
| получить ещё один кадр | RefreshOnce() | Once запрашивает одну отрисовку |
Freeze останавливает только обновление render target. Это не пауза EntityTree, скриптов или физики. Если остановить viewport до его первой отрисовки, готового кадра ещё не будет.
В текущей сборке getter RenderTargetUpdateMode после выполненного Once по-прежнему возвращает Once. Остановка происходит в RenderingServer. Не стройте ожидание завершения на проверке свойства на Disabled. Повторное присваивание Once запрашивает следующую отрисовку, даже если getter уже возвращал Once.
Для чтения изображения дождитесь RenderingServer.FramePostDraw; в C# используйте ToSignal(RenderingServer.Singleton, RenderingServer.SignalName.FramePostDraw). Этот сигнал означает завершённый кадр, а не полную сходимость GI или готовность любого внешнего асинхронного ресурса.
Диагностика
| Симптом | Проверка |
|---|---|
| пустой прямоугольник | SourceView уже в дереве, есть активная Camera3D, модель загружена, прошла отрисовка |
| виден чужой мир | принадлежность камеры к viewport и собственный World3D |
| картинка растягивается или размыта | отдельно сравните Size источника, Size потребителя и режим TextureRect.StretchMode |
| изменение камеры не видно | не оставлен ли Disabled; для одноразового обновления снова присвойте Once |
| снимок сделан слишком рано | чтение после FramePostDraw, а не сразу после AddChild |
| интерфейс не реагирует на мышь | TextureRect показывает текстуру, но этот пример не пересылает ввод в SubViewport |
Кадры: Forward+/Vulkan, 2560×1440, внутренний масштаб 3D 1.5, MSAA 8× и TAA. Это тяжёлый профиль иллюстраций. Управление вводом, экспорт приложения, все режимы UpdateMode и работа примера в Compatibility здесь не проверены.