Occlusion culling: объекты за непрозрачной стеной
Проверить отсечение по реальному списку отрисовки и не скрыть видимую геометрию неправильным occluder.
Что отсекается
Occlusion culling исключает полностью закрытые объекты из отрисовки. Это отличается от выхода за фрустум камеры и от выбора mesh LOD. Непрозрачная стена может уже закрывать пиксели сферы, но без occlusion сфера всё ещё передаётся в отрисовку и отбрасывается проверкой глубины.
Для culling нужен OccluderInstance3D с геометрией перекрытия. Одна видимая MeshInstance3D-стена не создаёт его автоматически. Occluder сам не рисует стену и не создаёт физическое тело.
Запустить полный пример
Используйте три файла из mesh LOD: GeneratedLodMesh.cs, CountedMesh3D.cs и OptimizationLab3D.cs. Нужны настроенный C#-проект, SDK 1.0.0 и net10.0. Выполните dotnet build и назначьте OptimizationLab3D.cs Entity3D-корню .rscene. До запуска выберите Preview=Occlusion.
Полный файл уровня ниже создаёт видимую стену 5×5 и отдельный QuadOccluder3D 4.9×4.9 в той же плоскости. Небольшой запас оставляет occluder внутри непрозрачной стены. UseOcclusionCulling включается у viewport, который рисует уровень.
OptimizationLab3D.cs
using Reckfell;
public partial class OptimizationLab3D : Entity3D
{
public enum PreviewMode { MeshLod, Occlusion }
[Export] public PreviewMode Preview { get; set; } = PreviewMode.MeshLod;
public CountedMesh3D Target { get; private set; } = null!;
public Camera3D Camera { get; private set; } = null!;
public MeshInstance3D Wall { get; private set; } = null!;
public OccluderInstance3D Occluder { get; private set; } = null!;
private Label counter = null!;
public override void _Ready()
{
var material = new StandardMaterial3D
{
AlbedoColor = Colors.White,
ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded
};
Target = new CountedMesh3D { Mesh = GeneratedLodMesh.Source().GetMesh(), MaterialOverride = material };
AddChild(Target);
var body = new StaticBody3D();AddChild(body);
body.AddChild(new CollisionShape3D { Shape = new SphereShape3D { Radius = 1 } });
Wall = new MeshInstance3D
{
Mesh = new BoxMesh { Size = new Vector3(5, 5, 0.2f) },
Position = new Vector3(0, 0, 2), Visible = Preview == PreviewMode.Occlusion,
MaterialOverride = new StandardMaterial3D
{
AlbedoColor = new Color(0.08f, 0.25f, 1),
ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded
}
};
Occluder = new OccluderInstance3D
{
Occluder = new QuadOccluder3D { Size = new Vector2(4.9f, 4.9f) },
Position = new Vector3(0, 0, 2), Visible = Preview == PreviewMode.Occlusion
};
AddChild(Wall);AddChild(Occluder);
Camera = new Camera3D { Current = true, Position = new Vector3(0, 0, 6), Fov = 60, Far = 200 };
AddChild(Camera);
var canvas = new CanvasLayer();AddChild(canvas);
counter = new Label { Position = new Vector2(12, 12) };canvas.AddChild(counter);
GetViewport().UseOcclusionCulling = Preview == PreviewMode.Occlusion;
}
public override void _Process(double delta)
{
counter.Text = "△ " + GetViewport().GetRenderInfo(Viewport.RenderInfoType.Visible, Viewport.RenderInfo.PrimitivesInFrame);
}
}Preview читается в _Ready. Для проверок после запуска доступны Wall, Occluder, Camera и Target; их нельзя использовать до создания в _Ready. Экспортированное Preview не является готовым переключателем во время игры.
Сравнить результат и стоимость отрисовки
В проверке камера находится перед стеной; автоматический LOD отключён порогом 0, чтобы не смешивать два эффекта. Visible ObjectsInFrame уменьшился с 2 до 1, а пиксели сцены ниже счётчика остались одинаковыми. △ показывает примитивы, поэтому число меняется сильнее, чем количество объектов.
Получены реальные кадры и счётчики в текущих Compatibility/OpenGL и Forward+/Vulkan. Публичные изображения — исходные OpenGL-кадры 512×384, не wireframe редактора и не замер FPS. Для собственного уровня измеряйте итоговое время CPU/GPU: подготовка culling тоже имеет стоимость, особенно если почти всё видно.
Проверить правильность occluder
Occluder должен соответствовать реальной непрозрачной преграде. Не закрывайте им дверные проёмы, окна, прозрачное стекло и пространство за силуэтом стены. Более грубая геометрия полезна только пока остаётся безопасной для видимости.
В тесте скрытие Wall при оставленном активном Occluder продолжило скрывать сферу. Это ожидаемый результат неверных данных, а не доказательство отсутствия сферы. Отключение Occluder.Visible восстановило её. Для движущейся/удаляемой стены согласованно обновляйте её occluder.
Проверены восстановление цели при обходе стены камерой и IgnoreOcclusionCulling=true у меша. Последнее возвращает объект в список отрисовки, но не делает его видимым сквозь непрозрачную стену и не отключает остальные виды culling.
Для запечённых occluder BakeMask выбирает визуальные слои собираемой геометрии, не физические CollisionLayer. BakeSimplificationDistance управляет упрощением, не дистанцией от камеры. Кнопки bake и качество результата для реального импортированного уровня в этом примере не проверены; используемый Quad назначается напрямую.
Что продолжает работать
Скрытая сфера осталась Visible=true и IsVisibleInTree=true, продолжила _Process, а физический луч продолжил попадать в её отдельное тело. Occlusion не удаляет Entity, не останавливает игровые таймеры и не выгружает ресурсы.
Не подменяйте им стриминг мира. Если нужно отключить дорогую симуляцию или освободить ресурсы чанка, нужна отдельная политика состояния и загрузки. Она должна учитывать игровую логику, а не только видимость камеры.
Диагностика
| Проблема | Проверка |
|---|---|
| Счётчик не уменьшается за стеной | Нужный viewport, UseOcclusionCulling, действующий Occluder и полное перекрытие объекта |
| Объект пропал в открытом месте | Слишком большой/устаревший occluder, закрытые им проёмы и преобразования |
| Виден кусок объекта, culling не происходит | Частично видимый объект должен оставаться в отрисовке |
| Bool включён, FPS прежний | Сравнить render list и время CPU/GPU; не выводить ускорение из одного флага |
| Невидимый объект всё ещё считает логику | Это ожидаемо: обработка Entity не выключается |