Коллизии 3D: простые формы и составные тела
Полный уровень с падающим ящиком, полом, стеной и выбором мышью; границы применения convex и trimesh.
Запустить пример 3D
Добавьте LabBody3D.cs, ScreenPicker3D.cs и CollisionLab3D.cs в C#-проект. Создайте отдельный уровень с корнем Entity3D, прикрепите CollisionLab3D, соберите и запустите F6. Оставьте корень без масштаба/поворота, физику 60 шагов/с и гравитацию 3D 9.8 с направлением (0, -1, 0). Камера, свет, видимые меши и коллизии создаются кодом.
Ящик падает на пол. Space один раз прикладывает импульс вверх; R запрашивает сброс через _IntegrateForces. Левый щелчок по ящику записывает результат в Picker.LastHit; это данные запроса без автоматического выделения материала. Луч Ray проверяет стену и доступен для просмотра через IsColliding/GetCollisionPoint.
LabBody3D.cs
using System;
using Reckfell;
public partial class LabBody3D : RigidBody3D
{
private Vector3? _resetPosition;
public int ResetCount { get; private set; }
public void RequestReset(Vector3 worldPosition)
{
if (!worldPosition.IsFinite()) throw new ArgumentOutOfRangeException(nameof(worldPosition));
_resetPosition = worldPosition;
Sleeping = false;
}
public override void _IntegrateForces(PhysicsDirectBodyState3D state)
{
if (_resetPosition is not Vector3 position) return;
state.Transform = new Transform3D(Basis.Identity, position);
state.LinearVelocity = Vector3.Zero;
state.AngularVelocity = Vector3.Zero;
_resetPosition = null;
ResetCount++;
}
}ScreenPicker3D.cs
using Reckfell;
using Hit = Reckfell.Collections.Dictionary;
public partial class ScreenPicker3D : Entity
{
[Export] public Camera3D? Camera { get; set; }
[Export] public uint Mask { get; set; } = 4;
[Export] public float Distance { get; set; } = 100;
private Vector2? _pending;
public Hit LastHit { get; private set; } = new();
public int QueryCount { get; private set; }
public void RequestPick(Vector2 viewportPosition) { if (viewportPosition.IsFinite()) _pending = viewportPosition; }
public override void _UnhandledInput(InputEvent @event)
{
if (@event is InputEventMouseButton { Pressed: true, ButtonIndex: MouseButton.Left } click)
RequestPick(click.Position);
}
public override void _PhysicsProcess(double delta)
{
if (_pending is not Vector2 screen) return;
_pending = null; LastHit = new();
if (Camera is null || !ReckfellObject.IsInstanceValid(Camera) || !Camera.IsInsideTree() || !float.IsFinite(Distance) || Distance <= 0) return;
Vector3 origin = Camera.ProjectRayOrigin(screen);
Vector3 end = origin + Camera.ProjectRayNormal(screen) * Distance;
var query = PhysicsRayQueryParameters3D.Create(origin, end, Mask);
query.CollideWithAreas = false;
LastHit = Camera.GetWorld3D().DirectSpaceState.IntersectRay(query);
QueryCount++;
}
}CollisionLab3D.cs
using Reckfell;
using Hit = Reckfell.Collections.Dictionary;
public partial class CollisionLab3D : Entity3D
{
public LabBody3D Crate { get; private set; } = null!;
public StaticBody3D Floor { get; private set; } = null!;
public StaticBody3D Wall { get; private set; } = null!;
public RayCast3D Ray { get; private set; } = null!;
public Camera3D Camera { get; private set; } = null!;
public ScreenPicker3D Picker { get; private set; } = null!;
public int Contacts { get; private set; }
public override void _Ready()
{
Floor = StaticBox("Floor", new Vector3(0, -.25f, 0), new Vector3(12, .5f, 8));
Wall = StaticBox("Wall", new Vector3(3, 1.5f, 0), new Vector3(.5f, 3, 4));
Crate = new LabBody3D { Name = "Crate", Position = new Vector3(0, 3, 0), Mass = 2,
LockRotation = true, CollisionLayer = 4, CollisionMask = 1, ContactMonitor = true, MaxContactsReported = 8,
PhysicsMaterialOverride = new PhysicsMaterial { Bounce = 0, Friction = 1 } };
Crate.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = Vector3.One } });
AddVisual(Crate, Vector3.One, new Color(.25f, .45f, 1));
Crate.BodyEntered += body => { if (body == Floor) Contacts++; };
AddChild(Crate);
Ray = new RayCast3D { Position = new Vector3(-4, 1.5f, 0), TargetPosition = new Vector3(8, 0, 0),
CollisionMask = 1, CollideWithAreas = false, Enabled = true }; AddChild(Ray);
Camera = new Camera3D { Position = new Vector3(8, 7, 10), Current = true }; AddChild(Camera);
Camera.LookAt(ToGlobal(new Vector3(0, 1, 0)));
Picker = new ScreenPicker3D { Camera = Camera }; AddChild(Picker);
var light = new DirectionalLight3D { RotationDegrees = new Vector3(-55, -30, 0), LightEnergy = 1.3f }; AddChild(light);
var environment = new Reckfell.Environment { BackgroundMode = Reckfell.Environment.BGMode.Color,
BackgroundColor = new Color(.035f, .035f, .045f), AmbientLightSource = Reckfell.Environment.AmbientSource.Color,
AmbientLightColor = Colors.White, AmbientLightEnergy = .55f };
AddChild(new WorldEnvironment { Environment = environment });
}
private StaticBody3D StaticBox(string name, Vector3 position, Vector3 size)
{
var body = new StaticBody3D { Name = name, Position = position, CollisionLayer = 1, CollisionMask = 0 };
body.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = size } });
AddVisual(body, size, new Color(.3f, .3f, .34f)); AddChild(body); return body;
}
private static void AddVisual(Entity3D parent, Vector3 size, Color color)
{
parent.AddChild(new MeshInstance3D { Mesh = new BoxMesh { Size = size }, MaterialOverride =
new StandardMaterial3D { AlbedoColor = color, Roughness = .85f } });
}
public Hit Cast(Vector3 worldFrom, Vector3 worldTo, uint mask = 1, Rid? exclude = null, bool inside = false)
{
var query = PhysicsRayQueryParameters3D.Create(worldFrom, worldTo, mask);
query.HitFromInside = inside;
if (exclude is Rid rid) query.Exclude = new Reckfell.Collections.Array<Rid> { rid };
return GetWorld3D().DirectSpaceState.IntersectRay(query);
}
public override void _UnhandledKeyInput(InputEvent @event)
{
if (@event is not InputEventKey { Pressed: true, Echo: false } key) return;
if (key.PhysicalKeycode == Key.R) Crate.RequestReset(ToGlobal(new Vector3(0, 3, 0)));
if (key.PhysicalKeycode == Key.Space) Crate.ApplyCentralImpulse(Vector3.Up * 10);
}
}Иерархия и размеры
CollisionShape3D должен быть прямым ребёнком тела или Area3D. MeshInstance3D рядом с ним отвечает только за изображение. У пола BoxShape3D.Size = (12, 0.5, 8), у ящика (1, 1, 1). Size — полный размер, не половинные extents. Положение дочерней формы задаёт её смещение относительно тела.
Для составного объекта добавьте несколько CollisionShape3D под одно тело. Это позволяет представить вогнутую вещь набором простых/выпуклых частей без динамического trimesh. Layer и Mask задаются у тела целиком. Для выбора отдельной части используйте индекс shape из результата запроса и сопоставление владельцев форм, а не предполагайте совпадение индекса с GetChild.
Какую форму выбрать
| Геометрия | Подходит |
|---|---|
| BoxShape3D, SphereShape3D | Простые ящики, стены, круглые предметы |
| CapsuleShape3D | Персонаж; радиус и полная высота, без острых нижних углов |
| CylinderShape3D | Цилиндрические предметы; отдельно проверяйте устойчивость контактов выбранного backend |
| ConvexPolygonShape3D | Выпуклая оболочка; вырезы и отверстия оболочка заполнит |
| Несколько convex/примитивов | Подвижный предмет сложной формы |
| ConcavePolygonShape3D / trimesh | Неподвижная треугольная геометрия окружения; не объём динамического тела |
| HeightMapShape3D | Статический рельеф с ограничениями представления карты высот |
| WorldBoundaryShape3D | Бесконечная статическая граница |
Concave/trimesh — поверхность треугольников; объект целиком внутри замкнутого меша не обязательно определяется как пересечение заполненного объёма. Не назначайте его обычному динамическому RigidBody3D или CharacterBody3D. Создание convex из модели меняет геометрию; проверьте отверстия, ступени и число полученных частей. Автоматическое создание не гарантирует подходящую игровую форму.
Масштаб, ресурсы и цена
Оставляйте масштаб физической ветви единичным, задавайте размеры в ресурсе. В частности, движок предупреждает о неравномерном масштабе CollisionShape3D. Масштаб родительской сущности тоже наследуется. Отделяйте визуальные дочерние объекты от физической геометрии.
Shape3D — разделяемый ресурс. Если несколько экземпляров используют один BoxShape3D, изменение Size затронет их все; Duplicate изолирует форму. Отключайте CollisionShape3D через SetDeferred из физического сигнала. Disabled убирает форму из запросов/контактов после применения изменения; скрытие MeshInstance этого не делает.
Примитивы обычно дают простые контакты, но результат зависит от числа тел, форм, пересечений и backend. Слишком много convex-частей тоже дорого. Не переделывайте все статические меши в отдельные динамические тела. Профилируйте репрезентативный уровень; пример с одним ящиком не подтверждает бюджет большого мира или устойчивость высокой стопки.