MeshDataTool: edit vertices and recompute normals
Triangle topology, a raised edge, material handling and replacement instead of another appended surface.
Run the edit
Add GeometryStage3D.cs, ArrayQuadLab3D.cs and MeshEditLab3D.cs. In a separate 1.0.0 C# level, attach only MeshEditLab3D to the Entity3D root. Set a 640×420 viewport, build and press F6. One quad edge rises by 0.9 and normals are recomputed. The displaying Entity rotates the completed surface toward the camera.
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() });
}
}What the tool stores
CreateFromSurface accepts an ArrayMesh and a Triangles surface index. Check Error. The tool constructs vertex/edge/face connections in its own working copy; SetVertex does not update the displayed Mesh before Commit.
The source quad has four vertices, two faces and five edges. One interior edge belongs to two faces. GetFaceVertex returns a face's vertex index; GetEdgeFaces returns adjacent faces. Edge numbers are not position-array indices.
Update related geometry
The code raises vertices with Z > 0 and accumulates new triangle normals. Its winding uses (C−A).Cross(B−A), then normalizes the accumulated vectors. These are smooth normals for this simple mesh, not a universal hard-edge/UV-seam solution.
The source quad has no tangents, skin, blend shapes, LOD or custom channels. Check their preservation/update separately when present. Shape edits may need new tangents, AABB and collision geometry; normal recomputation does not perform all of those tasks.
Commit does not overwrite the source
CommitToSurface appends a surface. A separate fixture call increases surface count from one to two. This example clears source surfaces only after preparing a valid working copy, writes one new surface and preserves its material. GetMaterial is a MeshDataTool method, not a Material property in the 1.0.0 C# SDK.
RaiseFarEdge targets our owned quad resource. It is not a transactional editor for arbitrary imported models: prepare a new ArrayMesh and replace the reference after success when the original must survive any failure. Do not clear a resource shared by several objects unless all should change.
Checks cover actual topology, raised positions, perpendicular recomputed normals, retained single-surface count and visible output. Editor Undo/Redo, universal channel preservation and physics shape updates are not implemented here.