Balder Tutorial : Creating a Mesh programatically

Balder has been throughout the development had the focus of extensibility, everything should be extensible. In Balder one has a set of object types, both 2D and 3D. For loading 3D models one has something called Mesh. This will automatically pick the right AssetLoader for the filetype based upon the file extension and parse the file and generate a 3D Mesh for Balder to render. Some of the other objects, which are considered primitives such as Box, Cylinder and Ring – are generated on the fly.

This tutorial will show how one can create an object that generates the Mesh data on the fly.

The process is quite simple. A Geometry holds something called a GeometryContext which is represented by the interface IGeometryContext, within the GeometryContext one has the ability to create something called a GeometryDetailLevel. The GeometryDetailLevel is the object being rendered depending on the detail level Balder wants to render it in. For the most part, Balder renders using the Full detail level. For Geometry there is a convenience property for the full detail level called FullDetailLevel in which one can use to generate its geometry.

To get started, the first thing we need to do is to create our object and inherit from Geometry.

using Balder.Objects.Geometries;
public class MyBox : Geometry
{
}

Now that we have our class and its inheriting Geometry, for Silverlight this means we can add it to our Silverlight page. Add a namespace for Balder Execution and View in your page Xaml file (xmlns:Execution=”clr-namespace:Balder.Execution;assembly=Balder” and xmlns:View=”clr-namespace:Balder.View;assembly=Balder”). Then you need to add an Xml namespace to the namespace of your MyBox class (something like this : xmlns:Custom=”clr-namespace:Balder.Silverlight.SampleBrowser.Samples.Geometries.Custom”).

Now you can add the Xml for Balder and MyBox inside the container you want it to be in the Xaml :


It will not show anything yet, as we don’t have any geometry data in the object yet.

The Geometry has a method on can override for preparing the object, its called Prepare().

public override void Prepare(Viewport viewport)
{
}

In the prepare method we can now start generating the 3D mesh data. We’ll start by adding the vertices; 3D points that our geometry data (lines or faces) will be connected to. We create method called GenerateVertices() that we’ll call from the Prepare() method. Each point in 3D is represented by something called a Vertex. A box consists of 8 vertices.

private void GenerateVertices()
{
var dimensionAsVector = new Vector(5f,5f,5f);
var halfDimension = dimensionAsVector / 2f;
var frontUpperLeft = new Vertex(-halfDimension.X, halfDimension.Y, -halfDimension.Z);
var frontUpperRight = new Vertex(halfDimension.X, halfDimension.Y, -halfDimension.Z);
var frontLowerLeft = new Vertex(-halfDimension.X, -halfDimension.Y, -halfDimension.Z);
var frontLowerRight = new Vertex(halfDimension.X, -halfDimension.Y, -halfDimension.Z);
var backUpperLeft = new Vertex(-halfDimension.X, halfDimension.Y, halfDimension.Z);
var backUpperRight = new Vertex(halfDimension.X, halfDimension.Y, halfDimension.Z);
var backLowerLeft = new Vertex(-halfDimension.X, -halfDimension.Y, halfDimension.Z);
var backLowerRight = new Vertex(halfDimension.X, -halfDimension.Y, halfDimension.Z);
FullDetailLevel.AllocateVertices(8);
FullDetailLevel.SetVertex(0, frontUpperLeft);
FullDetailLevel.SetVertex(1, frontUpperRight);
FullDetailLevel.SetVertex(2, frontLowerLeft);
FullDetailLevel.SetVertex(3, frontLowerRight);
FullDetailLevel.SetVertex(4, backUpperLeft);
FullDetailLevel.SetVertex(5, backUpperRight);
FullDetailLevel.SetVertex(6, backLowerLeft);
FullDetailLevel.SetVertex(7, backLowerRight);
}

Notice the first line in the method, it just defines the size of the box in 3D space.
The next thing we’ll do is to add some lines to it. We’ll add a method called GenerateLines() that we’ll call from the Prepare() method.

private void GenerateLines()
{
FullDetailLevel.AllocateLines(12);
FullDetailLevel.SetLine(0, new Line(0, 1));
FullDetailLevel.SetLine(1, new Line(2, 3));
FullDetailLevel.SetLine(2, new Line(0, 2));
FullDetailLevel.SetLine(3, new Line(1, 3));
FullDetailLevel.SetLine(4, new Line(4, 5));
FullDetailLevel.SetLine(5, new Line(6, 7));
FullDetailLevel.SetLine(6, new Line(4, 6));
FullDetailLevel.SetLine(7, new Line(5, 7));
FullDetailLevel.SetLine(8, new Line(0, 4));
FullDetailLevel.SetLine(9, new Line(1, 5));
FullDetailLevel.SetLine(10, new Line(2, 6));
FullDetailLevel.SetLine(11, new Line(3, 7));
}

The way this work is that every line is referring to the vertex index generated by the GenerateVertices(). Take the first line, it has the parameters 0 and 1, that means it is telling Balder to use vertex 0 and 1 to generate the line. When Balder has rotated all vertices according to the objects world and the view and then projected everything onto the 2D screen, it will use the result of the vertices to draw the line.

Running everything now should give you a result like the below.

If you want to use solids, making the object completely solid – we have to use Faces instead of Lines. A face is a triangle, and similar to a Line it references the vertices that represents the triangle. Only difference is that the order in which one connects the vertices are important. The reason for this is that Balder uses something known as backface culling, which means that triangles pointing away from the viewer is not drawn. Its important that one generates the faces with a counter clockwise connections between the vertices, as clockwise triangles will not show.

Let’s create a GenerateFaces() method and swap it for the GenerateLines() method.

private void GenerateFaces()
{
FullDetailLevel.AllocateFaces(12);
FullDetailLevel.SetFace(0, new Face(2,1,0));
FullDetailLevel.SetFace(1, new Face(1,2,3));
FullDetailLevel.SetFace(2, new Face(4,5,6));
FullDetailLevel.SetFace(3, new Face(7,6,5));
FullDetailLevel.SetFace(4, new Face(0,4,2));
FullDetailLevel.SetFace(5, new Face(6,2,4));
FullDetailLevel.SetFace(6, new Face(3,5,1));
FullDetailLevel.SetFace(7, new Face(5,3,7));
FullDetailLevel.SetFace(8, new Face(0,1,4));
FullDetailLevel.SetFace(9, new Face(5,4,1));
FullDetailLevel.SetFace(10, new Face(6,3,2));
FullDetailLevel.SetFace(11, new Face(3,6,7));
}

Running this should yield the following result:

As you’ll notice the box is all black. There are two reasons for this; we haven’t added a light to the scene yet and most importantly, in order for Balder to be able to calculate lights it needs to have something called a normal ve
ctor on the vertices and faces. Luckily, there is something called a GeometryHelper that can be used to generate these. You simple add the following two lines of code in the prepare method after you’ve generated the vertices and faces :

GeometryHelper.CalculateFaceNormals(FullDetailLevel);
GeometryHelper.CalculateVertexNormals(FullDetailLevel);

Then in the Xaml of the page, we’ll need to import the Lighting namespace from Balder (xmlns:Lighting=”clr-namespace:Balder.Lighting;assembly=Balder”) and then we can add a light to the scene:


The box has a property called Color, which it inherits – so setting it to Blue (Color=”Blue”) will give the box a specific color, instead of a random one, which is the default behavior.

You should be seeing something like this:

Summary
Its fairly easy to generate meshes in Balder, but there are more possibilities that this tutorial does not cover, which will be covered by other tutorials. One thing to mention, if you need to regenerate the mesh data you can call the InvalidatePrepare() method, and your prepare method will be called again. Also worth mentioning, if you want to have a texture on the object, you need to generate TextureCoordinates, which is also done through the GeometryDetailLevel. Each face has something called DiffuseA,B,C which in a similar way as with vertices connects the different corners of the triangle to a specific texture coordinate. A texture coordinate is a relative coordinate ranging from 0 to 1 within a texture.


12 thoughts on “Balder Tutorial : Creating a Mesh programatically

  1. hxu says:

    I love this tutorial. It is clear, very helpful and useful. Thank you!

  2. einar says:

    Thanks, glad you liked it.

  3. csder says:

    You are great Programmer.

  4. seo indore says:

    Its really a great information, I was searching for this since a long time.

    Looking for more updates on same.

    Thanks.

  5. Filip says:

    Hello Einar,
    Thank you very much for your post. I have tried to add whole MyBox programmatically.
    The problem is that I am not able to see the second MyBox. I do it in OnClick event. May I ask you if it is possible to do it like that and if yes where the problem is? CS and XAML is bellow.
    thank you
    Filip

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Animation;
    using System.Windows.Shapes;
    using Balder.Execution;
    using Balder.Objects.Geometries;
    using Balder.View;
    using Balder.Math;
    using Balder.Display;
    using Balder.Materials;
    using Balder.Imaging;
    using Balder.Assets;
    using Ninject;

    namespace SilverlightApplication3
    {

    public partial class MainPage : UserControl
    {

    public MainPage()
    {
    InitializeComponent();
    Loaded += Content_Loaded;
    }

    void Content_Loaded(object sender, System.Windows.RoutedEventArgs e)
    {
    }

    private void add_Click(object sender, RoutedEventArgs e)
    {
    MyBox NB = new MyBox();
    NB.Name = "krabice2";
    NB.PrepareFR();
    Hra.Children.Add(NB);
    }
    }

    public class MyBox : Balder.Objects.Geometries.Geometry
    {
    public override void Prepare(Viewport viewport)
    {
    PrepareFR();
    }

    public void PrepareFR()
    {
    GenerateVertices();
    GenerateLines();
    GenerateFaces();
    GeometryHelper.CalculateFaceNormals(FullDetailLevel);
    GeometryHelper.CalculateVertexNormals(FullDetailLevel);
    GenerateColors();
    }

    private void GenerateVertices()
    {
    var dimensionAsVector = new Vector(5f, 5f, 5f);
    var halfDimension = dimensionAsVector / 2f;
    var frontUpperLeft = new Vertex(-halfDimension.X, halfDimension.Y, -halfDimension.Z);
    var frontUpperRight = new Vertex(halfDimension.X, halfDimension.Y, -halfDimension.Z);
    var frontLowerLeft = new Vertex(-halfDimension.X, -halfDimension.Y, -halfDimension.Z);
    var frontLowerRight = new Vertex(halfDimension.X, -halfDimension.Y, -halfDimension.Z);
    var backUpperLeft = new Vertex(-halfDimension.X, halfDimension.Y, halfDimension.Z);
    var backUpperRight = new Vertex(halfDimension.X, halfDimension.Y, halfDimension.Z);
    var backLowerLeft = new Vertex(-halfDimension.X, -halfDimension.Y, halfDimension.Z);
    var backLowerRight = new Vertex(halfDimension.X, -halfDimension.Y, halfDimension.Z);
    FullDetailLevel.AllocateVertices(8);
    FullDetailLevel.SetVertex(0, frontUpperLeft);
    FullDetailLevel.SetVertex(1, frontUpperRight);
    FullDetailLevel.SetVertex(2, frontLowerLeft);
    FullDetailLevel.SetVertex(3, frontLowerRight);
    FullDetailLevel.SetVertex(4, backUpperLeft);
    FullDetailLevel.SetVertex(5, backUpperRight);
    FullDetailLevel.SetVertex(6, backLowerLeft);
    FullDetailLevel.SetVertex(7, backLowerRight);
    }

    private void GenerateLines()
    {
    FullDetailLevel.AllocateLines(12);
    FullDetailLevel.SetLine(0, new Balder.Objects.Geometries.Line(0, 1));
    FullDetailLevel.SetLine(1, new Balder.Objects.Geometries.Line(2, 3));
    FullDetailLevel.SetLine(2, new Balder.Objects.Geometries.Line(0, 2));
    FullDetailLevel.SetLine(3, new Balder.Objects.Geometries.Line(1, 3));
    FullDetailLevel.SetLine(4, new Balder.Objects.Geometries.Line(4, 5));
    FullDetailLevel.SetLine(5, new Balder.Objects.Geometries.Line(6, 7));
    FullDetailLevel.SetLine(6, new Balder.Objects.Geometries.Line(4, 6));
    FullDetailLevel.SetLine(7, new Balder.Objects.Geometries.Line(5, 7));
    FullDetailLevel.SetLine(8, new Balder.Objects.Geometries.Line(0, 4));
    FullDetailLevel.SetLine(9, new Balder.Objects.Geometries.Line(1, 5));
    FullDetailLevel.SetLine(10, new Balder.Objects.Geometries.Line(2, 6));
    FullDetailLevel.SetLine(11, new Balder.Objects.Geometries.Line(3, 7));
    }

    private void GenerateFaces()
    {
    FullDetailLevel.AllocateFaces(12);
    FullDetailLevel.SetFace(0, new Face(2, 1, 0));
    FullDetailLevel.SetFace(1, new Face(1, 2, 3));
    FullDetailLevel.SetFace(2, new Face(4, 5, 6));
    FullDetailLevel.SetFace(3, new Face(7, 6, 5));
    FullDetailLevel.SetFace(4, new Face(0, 4, 2));
    FullDetailLevel.SetFace(5, new Face(6, 2, 4));
    FullDetailLevel.SetFace(6, new Face(3, 5, 1));
    FullDetailLevel.SetFace(7, new Face(5, 3, 7));
    FullDetailLevel.SetFace(8, new Face(0, 1, 4));
    FullDetailLevel.SetFace(9, new Face(5, 4, 1));
    FullDetailLevel.SetFace(10, new Face(6, 3, 2));
    FullDetailLevel.SetFace(11, new Face(3, 6, 7));
    }

    private void GenerateColors()
    {
    Face f = null;
    for (int i = 0; i<FullDetailLevel.FaceCount; i++)
    {
    f = FullDetailLevel.GetFace(i);
    f.Material = new Material();
    if (i < 2)
    {
    f.Material.Ambient = Colors.Black;
    f.Material.Diffuse = Colors.Black;
    }
    else if (i<4)
    {
    f.Material.Ambient = Colors.Orange;
    f.Material.Diffuse = Colors.Orange;
    }
    else if (i < 6)
    {
    f.Material.Ambient = Colors.Red;
    f.Material.Diffuse = Colors.Red;
    }
    else if (i < 8)
    {
    f.Material.Ambient = Colors.Blue;
    f.Material.Diffuse = Colors.Blue;
    }
    else if (i < 10)
    {
    f.Material.Ambient = Colors.Green;
    f.Material.Diffuse = Colors.Green;
    }
    else
    {
    f.Material.Ambient = Colors.Magenta;
    f.Material.Diffuse = Colors.Magenta;
    }
    f.Material.Specular = Colors.White;
    f.Material.Shade = MaterialShade.Gouraud;
    }
    }
    }
    }

    <UserControl x:Class="SilverlightApplication3.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml&quot;
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008&quot;
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006&quot;
    xmlns:Execution="clr-namespace:Balder.Execution;assembly=Balder"
    xmlns:View="clr-namespace:Balder.View;assembly=Balder"
    xmlns:Custom="clr-namespace:SilverlightApplication3"
    xmlns:Lighting="clr-namespace:Balder.Lighting;assembly=Balder"
    xmlns:Materials="clr-namespace:Balder.Materials;assembly=Balder"
    xmlns:Geometries="clr-namespace:Balder.Objects.Geometries;assembly=Balder"
    xmlns:Animation="clr-namespace:Balder.Animation.Silverlight;assembly=Balder"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">

    <Grid x:Name="LayoutRoot" Background="White">

    <Grid.Triggers>
    <EventTrigger RoutedEvent="Grid.Loaded">
    <BeginStoryboard>
    <Storyboard RepeatBehavior="Forever">
    <Animation:StoryboardExtensions.CoordinateAnimation>
    <Animation:CoordinateAnimation
    From="0,0,0"
    To="359,359,0"
    Duration="00:00:05"
    TargetName="krabice"
    TargetProperty="(Node.Rotation)"/>
    </Animation:StoryboardExtensions.CoordinateAnimation>

    </Storyboard>
    </BeginStoryboard>
    </EventTrigger>
    </Grid.Triggers>

    <Button x:Name="add" Content="add" Width="50" Height="20" HorizontalAlignment="Left" VerticalAlignment="Top" Click="add_Click" />

    <Execution:Game x:Name="Hra" Width="800" Height="600">
    <Execution:Game.Camera>
    <View:Camera x:Name="cam" Position="-10,0,-20"/>
    </Execution:Game.Camera>

    <Lighting:OmniLight Position="0,2,-100" Strength="0.5" Diffuse="Yellow"/>

    <Custom:MyBox x:Name="krabice" Color="Blue">
    </Custom:MyBox>
    </Execution:Game>
    </Grid>
    </UserControl>

  6. einar says:

    It should work, but what you could probably do, which would be easier – is to add both boxes to the Xaml but set the IsVisible flag to false for the one that you don’t need before the OnClick is called. In OnClick you simply then set IsVisible for that box to true.

  7. Filip says:

    You are right, but I dont know how many objects would user like to add. So this is the reason why I am try to do it in C# rather than in XAML.

  8. einar says:

    Ok. Thats a good point.

    Let me run a test over the weekend and see if your original question; adding nodes in the Click event is something that should work. In theory it should work.

    I notice one thing in your code though that could be the problem not seeing any additional boxes. You don’t set a new position for the additional boxes. The default position of any 3D geometry is 0,0,0 – which would mean that any boxes you add would be just at the same location. Could that be your problem?

  9. Filip says:

    This is what I also tried. The rotation storyboard is applied only on box defined in XAML (krabice). The programmatically added box (krabice2) has no storyboard. So there should be visible really two boxes – one rotating and second without rotation.

  10. Sorry – very late feedback on this.

    I guess your problem is related to this : http://balder.codeplex.com/workitem/13571

  11. Filip says:

    great!!!
    thanks a lot

    bad: Hra.Children.Add(NB);
    right: Hra.Scene.AddNode(NB);

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: