reckfellENGINE

CSG: черновой уровень с дверным проёмом

Собрать геометрию булевыми операциями, включить коллизию и получить отдельные ресурсы меша и формы.

На этой странице

Что решает CSG

CSG объединяет и вычитает объёмы: из нескольких простых фигур получается один результат. Это удобно, чтобы проверить размеры комнаты, проходы и линии обзора до подготовки финальных моделей. CSG не заменяет моделирование, развёртку UV и подготовку ассетов.

В примере белая стена содержит настоящий дверной проём. Луч через середину проходит, луч через боковую часть встречает коллизию. Синие блоки рядом — отдельный GridMap для сравнения способов сборки уровня.

Белая CSG-стена с проёмом и синие модульные стены GridMap

Это реальный кадр текущей локальной сборки 1.0.0, Compatibility/OpenGL, 640×420. Он показывает результат игры, не интерфейс редактора и не производительность большого уровня.

Запустить полный пример

Нужны C#-проект Reckfell.NET.Sdk 1.0.0 с net10.0 и уровень .rscene с корнем Entity3D. Сохраните оба файла ниже, выполните dotnet build, прикрепите BlockoutLab3D.cs к корню и запустите уровень. Камера и все ресурсы создаются кодом; внешние модели, текстуры и архивы не нужны.

BlockoutLibrary.cs нужен и для материалов CSG, и для библиотеки синих блоков. BlockoutLab3D.cs строит обе композиции. В примере нет управления персонажем; коллизию проёма отдельно проверяли физическими лучами в движке.

BlockoutLibrary.cs
C#
using Reckfell;

public static class BlockoutLibrary
{
    public const int Floor = 0;
    public const int Wall = 1;

    public static StandardMaterial3D Material(Color color) => new()
    {
        AlbedoColor = color,
        ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded
    };

    public static MeshLibrary Create()
    {
        var library = new MeshLibrary();
        AddBox(library, Floor, "Floor", new Vector3(2, 0.2f, 2),
            new Vector3(0, -0.1f, 0), new Color(0.15f, 0.18f, 0.23f));
        AddBox(library, Wall, "Wall", new Vector3(2, 3, 0.4f),
            new Vector3(0, 1.5f, 0), new Color(0.08f, 0.25f, 1));
        return library;
    }

    private static void AddBox(MeshLibrary library, int id, string name,
        Vector3 size, Vector3 offset, Color color)
    {
        library.CreateItem(id);
        library.SetItemName(id, name);
        library.SetItemMesh(id, new BoxMesh { Size = size, Material = Material(color) });
        var transform = new Transform3D(Basis.Identity, offset);
        library.SetItemMeshTransform(id, transform);
        library.SetItemShapes(id, new Reckfell.Collections.Array
        {
            new BoxShape3D { Size = size }, transform
        });
    }
}
BlockoutLab3D.cs
C#
using Reckfell;

public partial class BlockoutLab3D : Entity3D
{
    public CsgCombiner3D Solid { get; private set; } = null!;
    public CsgBox3D Wall { get; private set; } = null!;
    public CsgBox3D DoorCut { get; private set; } = null!;
    public GridMap Grid { get; private set; } = null!;

    public override void _Ready()
    {
        Solid = new CsgCombiner3D
        {
            Position = new Vector3(-4, 1.5f, 0),
            UseCollision = true,
            CollisionLayer = 1,
            CollisionMask = 1
        };
        AddChild(Solid);
        Wall = new CsgBox3D
        {
            Size = new Vector3(5, 3, 0.6f),
            Material = BlockoutLibrary.Material(Colors.White)
        };
        DoorCut = new CsgBox3D
        {
            Size = new Vector3(1.6f, 2.2f, 1.2f),
            Position = new Vector3(0, -0.4f, 0),
            Operation = CsgShape3D.OperationEnum.Subtraction
        };
        Solid.AddChild(Wall);
        Solid.AddChild(DoorCut);

        Grid = new GridMap
        {
            Position = new Vector3(4, 0, 0),
            CellSize = new Vector3(2, 2, 2),
            CellCenterX = true,
            CellCenterY = false,
            CellCenterZ = true,
            MeshLibrary = BlockoutLibrary.Create(),
            CollisionLayer = 1,
            CollisionMask = 1
        };
        AddChild(Grid);
        for (int x = -2; x <= 2; x++)
        for (int z = -1; z <= 1; z++)
            Grid.SetCellItem(new Vector3I(x, 0, z), BlockoutLibrary.Floor);
        foreach (int x in new[] { -2, -1, 1, 2 })
            Grid.SetCellItem(new Vector3I(x, 0, 1), BlockoutLibrary.Wall);

        AddChild(new MeshInstance3D
        {
            Mesh = new PlaneMesh { Size = new Vector2(24, 16) },
            Position = new Vector3(0, -0.21f, 0),
            MaterialOverride = BlockoutLibrary.Material(new Color(0.06f, 0.07f, 0.09f))
        });
        var camera = new Camera3D { Current = true, Position = new Vector3(12, 9, 17), Fov = 55 };
        AddChild(camera);
        camera.LookAt(new Vector3(0, 1, 1));
    }
}

Разобрать порядок операций

Solid — корневой CsgCombiner3D. Его первый ребёнок Wall добавляет коробку 5×3×0.6. Следующий DoorCut с Operation=Subtraction вычитает коробку 1.6×2.2×1.2. Вырезающая коробка толще стены и пересекает её насквозь; нижняя грань проёма совпадает с уровнем пола.

Операция дочернего CSG-объектаРезультат
UnionДобавить объём к накопленной геометрии
SubtractionУдалить пересекающуюся часть накопленного объёма
IntersectionОставить только общую часть объёмов

В этом исполняемом примере проверены объединение исходной стены и вычитание двери. Сложные цепочки Intersection с импортированными мешами не проверялись. Порядок операндов влияет на результат: сначала должна существовать геометрия, из которой вырезают.

Операнды добавлены прямыми детьми CSG-корня. Отдельный CSG-корень рядом не вычитается автоматически из другого. Не вставляйте обычный Entity3D между этими операндами и Solid в ожидании прежней композиции.

Включить физику результата

UseCollision=true задан у Solid. Физическая форма относится к результату корня: DoorCut не становится отдельным невидимым препятствием. CollisionLayer=1 помещает стену на первый слой; запрос или персонаж должен проверять соответствующий бит.

Отрисовка и коллизия обновляются отложенно. После изменения размера или трансформации дождитесь обновления, прежде чем проверять попадание или получать готовый меш. Не перестраивайте сложную CSG-композицию на каждом кадре без профилирования.

В проверке луч через (-4,1) по X/Y не попал в стену, а через (-5.5,1) попал. После UseCollision=false боковой луч тоже перестал попадать, хотя геометрия осталась видимой. Визуальная видимость и физическая коллизия — разные настройки.

Получить статические ресурсы

Вызывайте методы у Solid, после готовности результата:

  1. BakeStaticMesh() получает ArrayMesh. Проверьте результат на null. Метод не создаёт MeshInstance3D и не сохраняет файл; это текущий ресурс root mesh, а не гарантированная независимая глубокая копия.
  2. BakeCollisionShape() создаёт отдельный ConcavePolygonShape3D. Он работает и при UseCollision=false, но сам ресурс не добавляет коллизию в мир.
  3. Для статического варианта назначьте меш MeshInstance3D, а форму — CollisionShape3D внутри StaticBody3D. Сохраните трансформацию корня. Не оставляйте одновременно две одинаковые физические стены.
  4. При сохранении ресурсов вызывайте ResourceSaver.Save отдельно для меша и формы с путями .rdata и проверяйте Error.Ok. Затем загрузите их и проверьте данные; одна успешная запись не доказывает правильность всего уровня.

Нативная проверка сохранила оба ресурса, загрузила их без кэша и сравнила геометрию. Отдельное StaticBody3D с запечённой формой сохранило стену и проём. Полное сохранение дерева уровня и интерфейс команды запекания редактора этим примером не проверялись.

У дочерних Wall/DoorCut bake-методы не возвращают самостоятельный результат: используйте корень композиции. Вогнутую треугольную форму применяйте к статической геометрии; для подвижных физических тел подбирайте примитивы или выпуклые формы отдельно.

Диагностика

СимптомПроверка
Проём не появилсяOperation, порядок прямых CSG-детей и пересечение выреза со стеной
Видимый проход блокирует телоКоллизия старой стены, готовность обновления, дублирующее физическое тело
Персонаж проходит сквозь стенуUseCollision у корня и совместимые физические слои/маски
BakeStaticMesh вернул nullЭто корень? Готов ли отложенный root mesh?
Получен меш, но нет коллизииСозданы ли StaticBody3D и CollisionShape3D с отдельной формой
Текстура выглядит растянутойМасштабы и UV исходных примитивов; CSG не выполняет полноценную художественную развёртку

CsgShape3D API · Сборка уровня из модулей

Документация Reckfell EngineНаверх ↑
БЫСТРЫЙ ПЕРЕХОДНачало работыEntity и иерархияC# & .NET 10Сцены и префабы
Руководства и полный API Reckfell K
reckfellENGINE
RECKFELL ACCOUNT

Вход в аккаунт

Войдите в аккаунт Reckfell.

Один аккаунт для всей платформы Reckfell.