reckfellENGINE

MeshDataTool: edit vertices and recompute normals

Triangle topology, a raised edge, material handling and replacement instead of another appended surface.

On this page

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.

The surface after vertex edits and normal recomputation

GeometryStage3D.cs
C#
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
C#
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
C#
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.

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.

Reckfell Engine documentationBack to top ↑
QUICK LINKSGetting startedEntity hierarchyC# & .NET 10Scenes and prefabs
Reckfell guides and full API K
reckfellENGINE
RECKFELL ACCOUNT

Sign in

Sign in to your Reckfell account.

One account for the entire Reckfell platform.