1
\$\begingroup\$

Getting my feet wet with Stride (formerly Xenko), I want to build a level editor where a user can drag the mouse to create walls at runtime.

I found documentation on how to immediate draw vertices per frame, but I want those vertices to be persistently stored for a mesh.

So I tried navigating through the API with IntelliSense, but didn't get further than this:

public class MeshScript : SyncScript
{
    public override void Start()
    {
        Mesh mesh = new Mesh();
        // The following is what I expected, but the properties do not exist:
        mesh.Vertices = new Vector3[] { ... };
        mesh.Indices = new ushort[] { 0, 1, 2, 0, 2, 3 };

        Model model = new Model();
        model.Meshes.Add(mesh);

        Entity.Add(new ModelComponent(model));
    }

    public override void Update() { }
}

Does anyone have a link or can explain to me which steps are needed to do this?

\$\endgroup\$
2
\$\begingroup\$

You can also make your own procedural model by inheriting from PrimitiveProceduralModelBase. This has several advantages:

  • Write less code
  • Available in Game Studio with preview and gizmo in scene editor
  • Properties (including material) are editable in Game Studio
  • Auto-generate tangents and bi-tangents required for advanced lighting
  • Auto-generate bounding box and bounding sphere for correct shadow map bias
  • Auto-generate additional texture coordinate channels
  • Model space position offset
  • Better upload/render performance due to correct data types

enter image description here

The workflow to add the model in Game Studio is then:

  • Add any procedural model (Cube, Plane, etc.)
  • Change the dropdown to your custom procedural model implementation

The workflow in code or script is:

  • Add an instance of the script below to any entity

EDIT: Full sources - including model update per frame - are now available here: github.com/tebjan/StrideProceduralModel

Code of the custom model:

[DataContract("MyProceduralModel")]
[Display("MyModel")] // This name shows up in the procedural model dropdown list
public class MyProceduralModel : PrimitiveProceduralModelBase
{
    // A custom property that shows up in Game Studio
    /// <summary>
    /// Gets or sets the size of the model.
    /// </summary>
    public Vector3 Size { get; set; } = Vector3.One;

    protected override GeometricMeshData<VertexPositionNormalTexture> CreatePrimitiveMeshData()
    {
        // First generate the arrays for vertices and indices with the correct size
        var vertexCount = 4;
        var indexCount = 6;
        var vertices = new VertexPositionNormalTexture[vertexCount];
        var indices = new int[indexCount];

        // Create custom vertices, in this case just a quad facing in Y direction
        var normal = Vector3.UnitZ;
        vertices[0] = new VertexPositionNormalTexture(new Vector3(-0.5f, 0.5f, 0) * Size, normal, new Vector2(0, 0));
        vertices[1] = new VertexPositionNormalTexture(new Vector3(0.5f, 0.5f, 0) * Size, normal, new Vector2(1, 0));
        vertices[2] = new VertexPositionNormalTexture(new Vector3(-0.5f, -0.5f, 0) * Size, normal, new Vector2(0, 1));
        vertices[3] = new VertexPositionNormalTexture(new Vector3(0.5f, -0.5f, 0) * Size, normal, new Vector2(1, 1));

        // Create custom indices
        indices[0] = 0;
        indices[1] = 1;
        indices[2] = 2;
        indices[3] = 1;
        indices[4] = 3;
        indices[5] = 2;

        // Create the primitive object for further processing by the base class
        return new GeometricMeshData<VertexPositionNormalTexture>(vertices, indices, isLeftHanded: false) { Name = "MyModel" };
    }
}

Code of the script that adds the model by code:

public class AddMyModelScript : AsyncScript
{
    // Declared public member fields and properties will show in the game studio
    public float RotationSpeed { get; set; } = 1;

    public override async Task Execute()
    {
        // Setup the custom model
        await CreateMyModel();

        while (Game.IsRunning)
        {
            // Do stuff every new frame
            Entity.Transform.Rotation *= Quaternion.RotationY(MathUtil.DegreesToRadians(RotationSpeed));

            await Script.NextFrame();
        }
    }

    async Task CreateMyModel()
    {
        // The model classes
        var myModel = new MyProceduralModel();
        var model = new Model();
        var modelComponent = new ModelComponent(model);

        // Generate the procedual model
        myModel.Generate(Services, model);

        // Add a meterial
        var material = Content.Load<Material>("MyModel Material");
        model.Add(material);

        // Add everything to the entity
        Entity.Add(modelComponent);
    }
}
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Does this also work for meshes created at runtime, and modified for a few moments before they're finished (like the user dragging a wall with the mouse)? \$\endgroup\$ – Ray May 18 at 10:01
  • \$\begingroup\$ The model can be re-generated at any point via the Generate method. If that's what you mean? \$\endgroup\$ – thalm May 18 at 23:37
  • \$\begingroup\$ Hmm, I see. I'd have to feed in the new coordinates to which the wall should go then before calling Generate I guess. I accept this as this seems to be the most accurate solution. \$\endgroup\$ – Ray May 19 at 9:19
  • \$\begingroup\$ If you just want to put the wall at another position, I would use the translation of the entity instead of generating the model. \$\endgroup\$ – thalm May 19 at 9:25
  • \$\begingroup\$ Ah no, the wall is not fully translated. While it has a fixed starting point, it has a variable end point (the user clicks to define the start, then drags the mouse to the end point he wants, and releases the mouse to make it static). \$\endgroup\$ – Ray May 19 at 9:32
3
\$\begingroup\$

I posted example #5 in the mesh-from-code docs that you found.

Since then, my LoadObjTest example has expanded to load WavefrontOBJ files at runtime. It handles the wavefront parsing (with C# code), mesh instantiation, and setting up textures (diffuse, specular, emission). The relevant code is in LoadAsset.cs. If you're looking to get a textured mesh on screen, this is simple runnable reference.

enter image description here

Of particular note to loading WavefrontOBJ and many external formats is the VertexSoup class. Many file formats specify faces with a position and normal per vertex. However, GPU indexed drawing allows only one index per-vertex, so each vertex must be "full configured". Meaning, if edges are sharp, and a vertex position is reused with multiple normal values, in WavefrontOBJ there is only one copy of that vertex position, but in GPU VertexBuffers that position is repeated for every different fully configured instantiation. Instantiating these "unique vertex configurations" is the job of VertexSoup. If you are generating meshes from code algorithms this may not be an issue for you.

@tebjan also suggested using ProceduralModelBase, which can generate tangents, which are important for normal/bump mapping. I have not been able to get the tangent generation working in my example yet, so I may try to switch to that method to see if that's fruitful.

One thing I have not (yet) done in this example is address any way of reusing a generated Model setup for multiple instantiations into the world. And by this i mean, the generated model is not placed into any asset database. The game engine manages this automatically for baked-in assets to avoid wasting CPU and GPU memory. That said, different applications may have different goals here. It might be intended to recreate a unique mesh every time a WavefrontOBJ file its imported (it might have changed externally), or it might be desired to have two different mesh instantiations (if they are somehow editable or modifiable in the game). Regardless, it's an important issue to be aware of.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ I've added an answer that demonstrates how to use the PrimitiveProceduralModelBase. \$\endgroup\$ – thalm May 12 at 18:36
3
\$\begingroup\$

The documentation actually has an article for this which I found while writing the question. Basically, vertices and indices are stored in MeshDraw instances, which are then assigned to the Mesh.Draw property.

A short example demonstrates this quickly by using automatically generated mesh data:

MeshDraw meshDraw = GeometricPrimitive.Sphere.New(GraphicsDevice).ToMeshDraw();
Mesh mesh = new Mesh { Draw = meshDraw }; 

The other more elaborate example uses custom vertex data, which is exactly what I wanted:

// Create vertex buffer.
VertexPositionTexture[] vertices = new VertexPositionTexture[3];
vertices[0].Position = new Vector3(0, 0, 1);
vertices[1].Position = new Vector3(0, 1, 0);
vertices[2].Position = new Vector3(0, 1, 1);
Buffer<VertexPositionTexture> vertexBuffer = Stride.Graphics.Buffer.Vertex.New(GraphicsDevice, vertices);

// Create index buffer.
int[] indices = { 0, 2, 1 };
Buffer<int> indexBuffer = Stride.Graphics.Buffer.Index.New(GraphicsDevice, indices);

// Create the mesh with a MeshDraw instance.
Mesh mesh = new Mesh
{
    Draw = new MeshDraw
    {
        PrimitiveType = PrimitiveType.TriangleList,
        DrawCount = indices.Length,
        IndexBuffer = new IndexBufferBinding(indexBuffer, true, indices.Length),
        VertexBuffers = new[]
        {
            new VertexBufferBinding(vertexBuffer, VertexPositionTexture.Layout, vertexBuffer.ElementCount)
        },
    }
};

And then you'd just add these Mesh instances to Models which you put into ModelComponents:

// Create the model with the mesh.
Model model = new Model();
model.Meshes.Add(mesh);
Entity.Add(new ModelComponent(model));
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ I'm getting the error "The non-generic type 'Buffer' cannot be used with type arguments" \$\endgroup\$ – user2081518 May 8 at 12:34
  • 1
    \$\begingroup\$ You need to import the Stride.Graphics namespace and ensure to use its Buffer class. The linked example is explicit about it due to it being ambiguous to System.Buffer - I shortened code here to fit onto the page, but I made the namespaces explicit now. \$\endgroup\$ – Ray May 8 at 14:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.