Table of Contents

Partial Torus Mesh

A torus defined parametrically from two angles - one around the tube, one around the ring - and cut short by limiting the second, which turns the same code into an arc, a horseshoe or a full ring without special cases. Normals are computed rather than guessed, because a parametric surface lit with wrong normals looks subtly faceted in a way that is hard to trace back.

The Program.cs file shows how to:

  • Defining a surface from two angle parameters
  • Making the shape partial by constraining the bend angle
  • Computing correct normals for smooth shading
  • Reusing vertices between adjacent triangles through indexing
  • Getting triangle winding right so faces are not culled
  • Using helpers: SceneSetup3D, UseSkybox, CreateMaterial

Partial Torus Mesh

View on GitHub.

using Stride.CommunityToolkit.Bepu;
using Stride.CommunityToolkit.Engine;
using Stride.CommunityToolkit.Rendering;
using Stride.CommunityToolkit.Skyboxes;
using Stride.Core.Mathematics;
using Stride.Engine;
using Stride.Graphics;
using Stride.Rendering;

/*
  A partial torus built vertex by vertex: a parametric surface over two angles, around the tube and
  along the bend, cut short of a full ring by limiting the bend angle. Shows positions, normals,
  triangle winding and vertex reuse through indices. The open ends show back-face culling at work:
  look into one and the tube is see-through, since faces turned away from the camera are not drawn.
*/

// Torus parameters
const float CylinderRadius = 0.3f;
const float TorusAngle = 270.0f;
const float BendRadius = 1.0f;
const int CircumferenceStepsCount = 20;
const int BendSegmentSteps = 40;

using var game = new Game();

game.Run(new SceneSetup3D().UseBepu().UseSkybox(), start: scene =>
{
    CreateMeshEntity(scene, new Vector3(0, 1, 0), b => BuildPartialTorusMesh(b, CylinderRadius, TorusAngle, BendRadius, CircumferenceStepsCount, BendSegmentSteps));
});

Entity CreateMeshEntity(Scene scene, Vector3 position, Action<MeshBuilder> build)
{
    using var meshBuilder = new MeshBuilder();

    build(meshBuilder);

    var model = new Model
    {
        new Mesh { Draw = meshBuilder.ToMeshDraw(game.GraphicsDevice) },
        game.CreateMaterial(new Color(255, 77, 128), metalness: 0f, glossiness: 0.9f),
    };

    var entity = new Entity { Scene = scene, Transform = { Position = position } };

    entity.Add(new ModelComponent { Model = model });

    return entity;
}

void BuildPartialTorusMesh(MeshBuilder meshBuilder, float cylinderRadius, float torusAngle, float bendRadius, int circumferenceStepsCount, int bendSegmentSteps)
{
    // torusAngle is in degrees, up to 360
    meshBuilder.WithIndexType(IndexingType.Int16);
    meshBuilder.WithPrimitiveType(PrimitiveType.TriangleList);

    var position = meshBuilder.WithPosition<Vector3>();
    var normal = meshBuilder.WithNormal<Vector3>();

    // Vertices: a ring around the tube at every step along the bend
    for (var j = 0; j <= bendSegmentSteps; j++)
    {
        // The bend angle phi starts at 0 on the Z axis and reaches the X axis at 90 degrees
        var phi = j * torusAngle / bendSegmentSteps * Math.PI / 180.0;
        var xc = bendRadius * Math.Sin(phi);
        var zc = bendRadius * Math.Cos(phi);

        for (var i = 0; i < circumferenceStepsCount; i++)
        {
            // The angle theta around the tube
            var theta = i * Math.Tau / circumferenceStepsCount;
            var yr = cylinderRadius * Math.Sin(theta);
            var xr = cylinderRadius * Math.Cos(theta) * Math.Sin(phi);
            var zr = cylinderRadius * Math.Cos(theta) * Math.Cos(phi);

            meshBuilder.AddVertex();
            meshBuilder.SetElement(position, new Vector3((float)(xc + xr), (float)yr, (float)(zc + zr)));
            // Unit length: the direction out from the tube's centre line
            meshBuilder.SetElement(normal, Vector3.Normalize(new Vector3((float)xr, (float)yr, (float)zr)));
        }
    }

    // Triangle indices: two per quad between consecutive rings
    for (var j = 0; j < bendSegmentSteps; j++)
    {
        for (var i = 0; i < circumferenceStepsCount; i++)
        {
            var index = i + j * circumferenceStepsCount;
            var next = (i + 1) % circumferenceStepsCount + j * circumferenceStepsCount;

            meshBuilder.AddIndex(index);
            meshBuilder.AddIndex(next + circumferenceStepsCount);
            meshBuilder.AddIndex(index + circumferenceStepsCount);

            meshBuilder.AddIndex(index);
            meshBuilder.AddIndex(next);
            meshBuilder.AddIndex(next + circumferenceStepsCount);
        }
    }
}