MultiMesh: вершинная анимация множества экземпляров
Одна сетка, отдельная фаза каждого экземпляра и правильные границы отсечения.
Начать с полного примера
Используйте пять файлов и шаги запуска, оставив ParticleMotion=false. В этом режиме C# один раз задаёт расположение, а vertex-шейдер деформирует сетку при отрисовке.
Данные экземпляра
TransformFormat и UseCustomData задаются до InstanceCount. После выделения ненулевого количества менять формат нельзя без перераспределения. InstanceCount — ёмкость, VisibleInstanceCount — число рисуемых первых экземпляров: 0 скрывает группу, -1 разрешает всю ёмкость. Это не автоматическое отсечение каждой рыбы.
VertexSchool3D.cs
using Reckfell;
public partial class VertexSchool3D : MultiMeshInstance3D
{
public ShaderMaterial SwimMaterial { get; private set; } = null!;
public bool TryBuild(int count)
{
if (count < 1 || count > 8192) return false;
var layout = SchoolMesh.Layout(count);
SwimMaterial = new ShaderMaterial { Shader = new Shader { Code = SchoolShaders.Swim } };
var data = new MultiMesh
{
TransformFormat = MultiMesh.TransformFormatEnum.Transform3D,
UseCustomData = true,
Mesh = SchoolMesh.Create(),
InstanceCount = count,
CustomAabb = layout.Bounds
};
for (int i = 0; i < count; i++)
{
float x = (i % layout.Columns - (layout.Columns - 1) * 0.5f) * 0.85f;
float y = (i / layout.Columns - (layout.Rows - 1) * 0.5f) * 0.6f;
data.SetInstanceTransform(i, new Transform3D(Basis.Identity.Scaled(Vector3.One * 0.6f),
new Vector3(x, y, 0)));
data.SetInstanceCustomData(i, new Color(Mathf.PosMod(i * 0.6180339f, 1), 0.5f, 0, 1));
}
Multimesh = data;
MaterialOverride = SwimMaterial;
CastShadow = ShadowCastingSetting.Off;
return true;
}
public void SetMotion(float clock, float amplitude)
{
SwimMaterial.SetShaderParameter("clock", clock);
SwimMaterial.SetShaderParameter("amplitude", Mathf.Clamp(amplitude, 0, 0.4f));
}
}SchoolMesh.cs и SchoolShaders.cs берутся из общего примера. В custom data канал X задаёт фазу, Y — добавку к частоте. Позиции загружаются один раз; в каждом кадре меняется общий uniform clock, а не 2048 отдельных Transform из C#.
Полный vertex-шейдер
shader_type spatial;
render_mode unshaded, cull_disabled;
uniform float clock = 0.0;
uniform float amplitude = 0.22;
varying float shade;
void vertex() {
float phase = INSTANCE_CUSTOM.x * TAU;
float tail = clamp((0.6 - VERTEX.x) / 1.2, 0.0, 1.0);
VERTEX.y += sin(clock * (2.0 + INSTANCE_CUSTOM.y) + phase + VERTEX.x * 4.0)
* amplitude * tail * tail;
shade = 0.65 + INSTANCE_CUSTOM.x * 0.35;
}
void fragment() {
ALBEDO = vec3(shade * 0.84, shade * 0.91, shade);
}VERTEX.x направлен вдоль тела. Множитель tail*tail удерживает голову и сильнее двигает хвост. INSTANCE_CUSTOM различается между экземплярами: общий материал не заставляет их колебаться синхронно. Пример не использует скелетную анимацию и не доказывает, что скелеты всегда медленнее.
Границы и разбиение группы
Изменение VERTEX на GPU не пересчитывает автоматически подходящие CPU-границы для всех положений анимации. CustomAabb здесь покрывает сетку расположений и запас на колебание. Слишком маленькие границы могут обрезать видимую стаю, слишком большие ухудшают отсечение.
Большая общая группа отсеивается как один объект. Если объекты распределены по большому уровню, создавайте пространственные группы с отдельными границами. Это разбиение отрисовки, не готовый стриминг мира и не индивидуальная игровая логика экземпляров.