MeshDataTool: изменить вершины и пересчитать нормали
Топология треугольной поверхности, поднятый край, материал и замена вместо лишнего Commit.
Запустить редактирование
Добавьте три файла: GeometryStage3D.cs, ArrayQuadLab3D.cs и MeshEditLab3D.cs. В отдельном C#-уровне 1.0.0 прикрепите только MeshEditLab3D к корню Entity3D. Задайте viewport 640×420, соберите и нажмите F6. Один край квадратной поверхности поднимется на 0,9; нормали будут пересчитаны. Показывающая Entity разворачивает готовую поверхность к камере.
GeometryStage3D.cs
using Reckfell;
public static class GeometryStage3D
{
public static StandardMaterial3D Material(bool vertexColors = false, bool unshaded = false)
{
return new StandardMaterial3D
{
AlbedoColor = vertexColors ? Colors.White : new Color(0.24f, 0.44f, 1),
VertexColorUseAsAlbedo = vertexColors,
ShadingMode = unshaded ? BaseMaterial3D.ShadingModeEnum.Unshaded : BaseMaterial3D.ShadingModeEnum.PerPixel,
Roughness = 0.8f
};
}
public static void Setup(Entity3D root, string ru, string en)
{
root.AddChild(new WorldEnvironment { Environment = new Reckfell.Environment
{
BackgroundMode = Reckfell.Environment.BGMode.Color,
BackgroundColor = new Color(0.035f, 0.04f, 0.05f),
AmbientLightSource = Reckfell.Environment.AmbientSource.Color,
AmbientLightColor = Colors.White, AmbientLightEnergy = 0.5f
}});
root.AddChild(new DirectionalLight3D { RotationDegrees = new Vector3(-55, -25, 0), LightEnergy = 1.2f });
root.AddChild(new MeshInstance3D
{
Mesh = new BoxMesh { Size = new Vector3(8, 0.05f, 6) }, Position = new Vector3(0, -0.12f, 0),
MaterialOverride = new StandardMaterial3D { AlbedoColor = new Color(0.10f, 0.11f, 0.13f), Roughness = 1 }
});
var camera = new Camera3D { Position = new Vector3(4, 4.8f, 6), Current = true, Fov = 45 };
root.AddChild(camera); camera.LookAt(new Vector3(0, 0.35f, 0), Vector3.Up);
var canvas = new CanvasLayer(); root.AddChild(canvas);
var label = new Label { Position = new Vector2(24, 20), Text = TranslationServer.GetLocale().StartsWith("ru") ? ru : en };
label.AddThemeFontSizeOverride("font_size", 22); canvas.AddChild(label);
}
}ArrayQuadLab3D.cs
using Reckfell;
public partial class ArrayQuadLab3D : Entity3D
{
public ArrayMesh Generated { get; private set; } = null!;
public static ArrayMesh CreateQuad()
{
Vector3[] vertices = [new(-1, 0, -1), new(1, 0, -1), new(1, 0, 1), new(-1, 0, 1)];
Vector3[] normals = [Vector3.Up, Vector3.Up, Vector3.Up, Vector3.Up];
Vector2[] uv = [new(0, 0), new(1, 0), new(1, 1), new(0, 1)];
int[] indices = [0, 1, 2, 0, 2, 3];
using 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.TexUV] = uv;
arrays[(int)Mesh.ArrayType.Index] = indices;
var mesh = new ArrayMesh();
mesh.AddSurfaceFromArrays(Mesh.PrimitiveType.Triangles, arrays);
return mesh;
}
public override void _Ready()
{
GeometryStage3D.Setup(this, "ArrayMesh: 4 вершины, 2 треугольника", "ArrayMesh: 4 vertices, 2 triangles");
Generated = CreateQuad();
AddChild(new MeshInstance3D { Mesh = Generated, MaterialOverride = GeometryStage3D.Material() });
}
}MeshEditLab3D.cs
using System;
using Reckfell;
public partial class MeshEditLab3D : Entity3D
{
public ArrayMesh Generated { get; private set; } = null!;
public static Error RaiseFarEdge(ArrayMesh mesh, float height)
{
if (mesh.GetSurfaceCount() != 1 || mesh.SurfaceGetPrimitiveType(0) != Mesh.PrimitiveType.Triangles ||
!float.IsFinite(height) || height < 0 || height > 2) return Error.InvalidParameter;
using var data = new MeshDataTool();
Error error = data.CreateFromSurface(mesh, 0);
if (error != Error.Ok) return error;
for (int i = 0; i < data.GetVertexCount(); i++)
{
Vector3 point = data.GetVertex(i);
if (point.Z > 0) point.Y = height;
data.SetVertex(i, point);
}
Vector3[] normals = new Vector3[data.GetVertexCount()];
for (int face = 0; face < data.GetFaceCount(); face++)
{
int a = data.GetFaceVertex(face, 0), b = data.GetFaceVertex(face, 1), c = data.GetFaceVertex(face, 2);
Vector3 normal = (data.GetVertex(c) - data.GetVertex(a)).Cross(data.GetVertex(b) - data.GetVertex(a));
normals[a] += normal; normals[b] += normal; normals[c] += normal;
}
for (int i = 0; i < normals.Length; i++)
{
if (normals[i].IsZeroApprox()) return Error.InvalidData;
data.SetVertexNormal(i, normals[i].Normalized());
}
Material material = data.GetMaterial();
mesh.ClearSurfaces();
error = data.CommitToSurface(mesh, compressionFlags: 0);
if (error == Error.Ok) mesh.SurfaceSetMaterial(0, material);
return error;
}
public override void _Ready()
{
GeometryStage3D.Setup(this, "MeshDataTool: новая форма и нормали", "MeshDataTool: updated shape and normals");
Generated = ArrayQuadLab3D.CreateQuad();
Error error = RaiseFarEdge(Generated, 0.9f);
if (error != Error.Ok) throw new InvalidOperationException("Mesh edit failed: " + error);
AddChild(new MeshInstance3D { Mesh = Generated, RotationDegrees = new Vector3(0, 180, 0), MaterialOverride = GeometryStage3D.Material() });
}
}Что хранит инструмент
CreateFromSurface принимает ArrayMesh и индекс поверхности Triangles. Проверяйте Error. Инструмент строит связи вершин, рёбер и граней в собственной рабочей копии; изменение SetVertex не обновляет отображаемый Mesh до Commit.
У исходного квадрата четыре вершины, две грани и пять рёбер. Одно внутреннее ребро принадлежит двум граням. GetFaceVertex возвращает индекс вершины данной грани; GetEdgeFaces — связанные грани. Не путайте номер ребра с индексом массива позиций.
Изменить геометрию согласованно
Код поднимает вершины с Z > 0 и заново накапливает нормали по треугольникам. Для текущего порядка граней используется (C−A).Cross(B−A), после суммирования результат нормализуется. Это сглаженные нормали для данного простого меша, а не универсальное решение жёстких рёбер и UV-швов.
Исходный квадрат не содержит tangents, skin, blend shapes, LOD или custom-каналов. Если они есть в вашей модели, проверяйте их сохранение и обновление отдельно. Изменённая форма может потребовать новых tangents, AABB и коллайдера; пересчёт нормалей не делает это автоматически.
Commit не перезаписывает источник
CommitToSurface добавляет поверхность. В стенде отдельный вызов увеличивает число поверхностей с одной до двух. В примере исходные поверхности очищаются только после подготовки допустимой рабочей копии; затем записывается одна новая поверхность и сохраняется её материал. GetMaterial — метод MeshDataTool, не свойство Material этого класса в C# SDK 1.0.0.
RaiseFarEdge предназначен для нашего собственного квадратного ресурса. Это не транзакционный редактор любого импортированного меша: если нужна гарантия сохранения старой модели при любой ошибке, сначала подготовьте новый ArrayMesh и замените ссылку лишь после успеха. Не очищайте ресурс, общий для нескольких объектов, без намерения изменить их все.
Проверены реальные связи топологии, поднятые позиции, перпендикулярность новых нормалей, сохранение одной поверхности и визуальный результат. Редакторский Undo/Redo, универсальное сохранение всех каналов модели и физическая форма здесь не реализованы.