SoftBody3D: ткань, закрепления и деформация
Полный пример ткани 9×9: физическая сетка, закреплённый край, импульс и освобождение вершин.
Запустить ткань
Добавьте SoftBodyLab3D.cs в C#-проект. Прикрепите к корню Entity3D отдельного уровня, соберите и запустите F6. Оставьте корень без масштаба/поворота; физика — 60 шагов/с, гравитация 9.8 вниз. Код создаёт сетку, материал, пол и камеру. Выберите реальный backend 3D, не Dummy.
Изначально прямоугольник лежит горизонтально на высоте 3. Один край закреплён, остальные вершины провисают под гравитацией. Space толкает ткань по Z, R освобождает край. Для первоначального состояния перезапустите уровень. PinsReady означает завершённую начальную настройку, а не обещание, что после R точки всё ещё закреплены.
SoftBodyLab3D.cs
using Reckfell;
public partial class SoftBodyLab3D : Entity3D
{
public const int Columns = 9;
public const int Rows = 9;
public SoftBody3D Cloth { get; private set; } = null!;
public bool PinsReady { get; private set; }
public override void _Ready()
{
var vertices = new Vector3[Columns * Rows];
var normals = new Vector3[vertices.Length];
for (int row = 0; row < Rows; row++)
for (int col = 0; col < Columns; col++)
{
int i = row * Columns + col;
vertices[i] = new Vector3(-1 + 2f * col / (Columns - 1), 3, 2f * row / (Rows - 1));
normals[i] = Vector3.Up;
}
var indices = new int[(Columns - 1) * (Rows - 1) * 6]; int index = 0;
for (int row = 0; row < Rows - 1; row++)
for (int col = 0; col < Columns - 1; col++)
{
int a = row * Columns + col, b = a + 1, c = a + Columns, d = c + 1;
foreach (int point in new[] { a, c, b, b, c, d }) indices[index++] = point;
}
var arrays = new Reckfell.Collections.Array(); arrays.Resize((int)Mesh.ArrayType.Max);
arrays[(int)Mesh.ArrayType.Vertex] = vertices; arrays[(int)Mesh.ArrayType.Normal] = normals; arrays[(int)Mesh.ArrayType.Index] = indices;
var mesh = new ArrayMesh(); mesh.AddSurfaceFromArrays(Mesh.PrimitiveType.Triangles, arrays, flags: Mesh.ArrayFormat.FlagUseDynamicUpdate);
Cloth = new SoftBody3D { Name = "Cloth", Mesh = mesh, TotalMass = 1, LinearStiffness = .5f,
SimulationPrecision = 5, PressureCoefficient = 0, CollisionLayer = 4, CollisionMask = 1,
MaterialOverride = new StandardMaterial3D { AlbedoColor = new Color(.25f,.45f,1), ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded, CullMode = BaseMaterial3D.CullModeEnum.Disabled } };
AddChild(Cloth);
var floor = new StaticBody3D { Position = new Vector3(0,-.2f,0), CollisionLayer = 1, CollisionMask = 0 };
floor.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = new Vector3(8,.4f,6) } });
floor.AddChild(new MeshInstance3D { Mesh = new BoxMesh { Size = new Vector3(8,.4f,6) }, MaterialOverride =
new StandardMaterial3D { AlbedoColor = new Color(.55f,.55f,.6f), ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded } }); AddChild(floor);
var camera = new Camera3D { Position = new Vector3(4,3,6), Current = true }; AddChild(camera); camera.LookAt(ToGlobal(new Vector3(0,1.5f,0)));
}
public override void _PhysicsProcess(double delta)
{
if (PinsReady || Cloth.PointCount != Columns * Rows) return;
for (int col = 0; col < Columns; col++) Cloth.SetPointPinned(col, true, new EntityPath(""), -1);
PinsReady = true;
}
public void ReleaseTop()
{
if (!PinsReady) return;
for (int col = 0; col < Columns; col++) Cloth.SetPointPinned(col, false, new EntityPath(""), -1);
}
public override void _UnhandledKeyInput(InputEvent @event)
{
if (@event is InputEventKey { Pressed: true, Echo: false } key)
{
if (key.PhysicalKeycode == Key.Space && PinsReady) Cloth.ApplyCentralImpulse(new Vector3(0,0,1));
if (key.PhysicalKeycode == Key.R) ReleaseTop();
}
}
}Сетка и индексы
SoftBody3D использует треугольную сетку, а не набор дочерних CollisionShape3D. В примере один ArrayMesh с одной поверхностью: 81 уникальная вершина, 128 треугольников, первый ряд 0–8 закрепляется в _PhysicsProcess после добавления тела в мир. PointCount проверяет число вершин первой поверхности; сам по себе он не доказывает готовность произвольного тела к симуляции. GetPointTransform возвращает мировую позицию физической точки.
У импортированной модели порядок вершин может измениться после оптимизации, переимпорта, объединения поверхностей или смены LOD. Не копируйте индексы 0–8 на произвольный меш. Сначала проверьте подходящую поверхность, плотность треугольников, дубликаты на швах и реальные точки закрепления. Очень подробная сетка повышает стоимость; начните с упрощённой равномерной геометрии.
В текущей реализации SoftBody делает собственную изменяемую копию первой поверхности и обновляет данные рендера из симуляции. Не рассчитывайте, что общий импортированный ресурс будет одновременно удобной независимой тканью и неизменяемой моделью для всех пользователей. После начала симуляции вершины обновляются в мировой системе; постоянное перемещение обычным transform не заменяет управление физической тканью.
Закрепить плащ к персонажу
SetPointPinned связывает индекс точки с закреплением; attachmentPath может задавать сущность привязки в иерархии. Для плаща создайте устойчивые точки крепления у плеч, убедитесь в правильной системе координат и проверьте движение/поворот персонажа. Закрепить все вершины — значит почти убрать возможность деформации; отсутствие закреплений не создаёт привязку к скелету автоматически.
Отдельно настройте Layer/Mask, форму коллизии персонажа и при необходимости ParentCollisionIgnore. Это не взаимозаменяемые настройки. Проверьте пересечение у плеч, быстрый разворот, телепорт, паузу и удаление персонажа. Пример закрепляет край в мире; он не является готовым плащом с импортированной моделью и сложными креплениями.
Параметры и ограничения
| Параметр | Назначение |
|---|---|
| TotalMass | Общая масса; не задавайте ноль ради закрепления |
| LinearStiffness | Сопротивление изменению формы; значения не гарантируют одну жёсткость для любой сетки/backend |
| SimulationPrecision | Точность расчёта мягкого тела; при повышении проверяйте нагрузку на CPU |
| PressureCoefficient | Давление; для открытого листа оставлено 0, объёмная оболочка требует подходящей замкнутой топологии |
| DampingCoefficient / DragCoefficient | Сопротивление; высокая величина может скрыть движение, но не исправляет топологию |
| CollisionLayer / CollisionMask | Фильтрация взаимодействий, отдельно от закреплений |
Проверены ReckfellPhysics3D и Jolt Physics: точки создаются, край закрепляется, свободные точки двигаются, импульс деформирует ткань, после снятия закрепления край падает. Изображение снято с ReckfellPhysics3D. Это не подтверждает все контакты со сложными импортированными мешами, все Area-поля, self-collision или экспорт на каждое устройство. Используйте проверку backend и измеряйте свою сцену.