Skip to content
Permalink
Fetching contributors…
Cannot retrieve contributors at this time
443 lines (279 sloc) 28 KB

id: cb4208bd-7759-4878-9e28-72c87a06da15 uti: com.xamarin.workbook title: "UrhoSharp: Exploring Coordinates" platforms:

  • Console packages:
  • id: UrhoSharp version: 1.5.22

Exploring Urho Coordinates

UrhoSharp is a powerful cross-platform 3D graphics engine that Xamarin and .NET developers can use for games or visualization. This workbook is intended as a basic introduction to UrhoSharp and 3D concepts, focusing on the 3D coordinate system.

#r "Urho"

This workbook requires only two of the dozen or so namespaces for UrhoSharp types:

using Urho;
using Urho.Shapes;

If you’re writing a standalone UrhoSharp program using Visual Studio, generally you derive a class from Application, and override several methods for all your code, animations, and interaction. The program then instantiates that derived class and calls the Run method on it. The Run method is blocking: It doesn’t return until the UrhoSharp application has been terminated.

That architecture would not work very well in an interactive workbook, so a class named SimpleApplication is available instead. SimpleApplication derives from Application and defines a static Show method that instantiates the SimpleApplication object, performs some initialization, and returns the object, which you can then use for constructing a 3D scene. This SimpleApplication class is described in the UrhoSharp API documentation here but you might also want to look at the source code for the class, and particularly the Start method, which performs initialization.

To create a UrhoSharp window in a workbook, call the static SimpleApplication.Show method, passing to it an argument of ApplicationOptions. The easiest options consist of simply a width and height of a window:

ApplicationOptions appOptions = new ApplicationOptions
{
    Width = 1000,
    Height = 1000
};
SimpleApplication app = SimpleApplication.Show(appOptions);

You should see a window appear on the desktop as the Show method completes.

Inside SimpleApplication

The SimpleApplication class creates two objects that are essential in a 3D scene: a camera and a light source.

The camera defines how you see 3D objects in the window. The Urho window is similar to the screen of a digital camera or the camera on your phone. As you know, what you see on a camera screen depends on:

  • where the camera is positioned in 3D space,

  • the direction that the camera lens is pointed,

  • the orientation of the camera (portrait or landscape or something in between), and

  • the degree of zoom that you’ve selected

All these characteristics (and more) are part of the Urho3D camera.

The use of a camera metaphor is perhaps the biggest difference between 3D graphics and 2D graphics. When working with 2D graphics, generally you specify coordinates of graphics objects relative to a screen or canvas. The coordinate values might be pixels or units that convert to pixels.

With 3D graphics, what you see on the screen depends on the relative locations of the camera and the 3D objects. Coordinate values are all relative. You can increase or decrease all the coordinates and sizes in a 3D scene by a factor of 10 (for example) and everything will look the same.

When you use a camera in real life, you know that you can’t take a picture of something behind the camera or otherwise out of its range. Similarly, in working with 3D scenes, you might not see anything in the UrhoSharp window if the 3D objects are not in the range of the camera. Helping you get a feel for this relationship is one of the goals of this workbook.

The other essential object that SimpleApplication creates is a light source. This serves to illuminate 3D objects and provide an illusion of three-dimensional space and curvature.

A Tree of Nodes

Urho organizes 3D objects in a tree of nodes. Every node except the top-level node has a parent, and every node potentially has multiple children. This organization allows you to group related 3D objects and treat them together. For example, you can move a whole group of objects by moving the parent node. Here’s the documentation of the Node class.

SimpleApplication creates a top-level node, which is a special node of type Scene, and which is accessible through the Scene property of SimpleApplication. Both the camera and light are subnodes of the Scene node. (You’ll see shortly how you can access the camera and light source from a workbook.)

The Start method in SimpleApplication also creates a node called RootNode. That particular node is not used in this workbook, and it’s not used for anything within the SimpleApplication class. Instead, this workbook creates creates something quite similar — a child node of Scene that will be used throughout this workbook as a parent to other nodes.

To create a new child node of an existing node (for example, the node referenced by the Scene property of the SimpleApplication object), call CreateNode on the node you want to create a child of.

When you experiment with code in a workbook, however, the code cells can be executed multiple times. Generally you’ll want to avoid creating nodes that duplicate existing nodes. For that reason, any code in a workbook that creates a new node should be prefaced with code that removes the node that might have been created in a previous execution of the code cell. That’s why this CreateChild call is preferenced with calls to GetChild and RemoveChild:

// Remove node named "mainNode" so there are no duplicates
app.Scene.RemoveChild(app.Scene.GetChild("mainNode", false));

// Create node named "mainNode" as child of Scene
Node mainNode = app.Scene.CreateChild("mainNode");

The node is given a name in the CreateChild call that is referenced in the GetChild call. But that first statement might seem risky to you when this code block is executed the first time: If a node with the name “mainNode” does not exist, GetChild returns null. Fortunately, if the argument to RemoveChild is null, it simply does nothing. The second argument to GetChild inhibits searching for the node recursively through levels of node.

Similarly, the following code creates a child node of mainNode named boxNode after first removing a subnode with that name:

// Remove node named "boxNode" so there are no duplicates
mainNode.RemoveChild(mainNode.GetChild("boxNode", false));

// Create a node for a Box component
Node boxNode = mainNode.CreateChild("boxNode");
Box box = boxNode.CreateComponent<Box>();

The last two statements demonstrate the simplest way to create a 3D object in the scene: first by creating a child node, and then to associate that child node with a component. (The camera and light created in the Start method of SimpleApplication are components as well.) Notice the generic argument to the CreateComponent call. The Box component is one of seven classes for basic shapes that derive from the abstract Shape class and which can be found in the Urho.Shapes namespace. The CreateComponent method returns an object of type Box.

But where is it?

3D Space

That Box object that you’ve just created is not yet visible. Understanding why requires some background in 3D coordinate systems.

A 3D graphics system like Urho allows objects to be positioned in a three-dimensional coordinate space. Two of the dimensions are just like a classic two-dimensional Cartesian coordinate system: Two-dimensional coordinate system

By default the origin is in the center of the Urho window.

A 3D coordinate system has a third axis labeled Z that is at right angles to both the X and Y axis. In Urho, the positive Z axis goes into the screen (conceptually speaking), and the negative Z axis comes out of the screen. Here’s the whole coordinate system viewed from an angle:

Three-dimensional coordinate system Urho’s coordinate system is known as a left-hand system. If you point the forefinger of your left hand in the direction of positive X coordinates (to the right) and your third finger in the direction of position Y coordinates (up), your thumb points in the direction of positive Z coordinates (into the screen). The only alternative is a right-hand system.

It’s sometimes convenient to speak of the coordinate system as defining planes. the X and Y axes define the XY plane, while the X and Z axes define the XZ plane, and the Y and Z axes define the YZ plane.

A 3D coordinate point is notated as (x, y, z). The origin of the coordinate system is the point (0, 0, 0).

There’s a good reason why you can’t see the box you created in the Urho window: By default, the camera is positioned at the origin of the 3D coordinate system, and so is the box. The camera is inside the box! The camera and box must be moved apart before the camera can see the box.

The camera has a direction as well as a position. Directions are indicated by 3D vectors, which are notated just like points but usually displayed in boldface: (x, y, z). In UrhoSharp, both points and vectors are values of type Vector3.

By default, the camera is positioned at the origin and pointing into the screen in the direction of the positive Z axis.

Or rather, it should be. To override some settings in SimpleApplication used to create the libraries used with this workbook, set the camera to what should be the default settings:

app.CameraNode.Position = new Vector3(0, 0, 0);
app.CameraNode.SetDirection(new Vector3(0, 0, 1));

Although the Position property is readable and writeable, the Direction property is read-only so SetDirection must be used to set it.

The following code moves the box so that it has a Z coordinate of 7 and sits in front of the camera. Notice that Position is a property of the Node object and not the Box object:

boxNode.Position = new Vector3(0, 0, 7);

And there it is.

It doesn’t look much like a box, however. It looks like a square. The problem now is that the camera is pointed straight at the box, so only one side of the box is visible. You can shift the box to the right by increasing the X coordinate of the Position property, and to the left by decreasing the X coordinate. You can move the box up by increasing the Y coordinate, and down by decreasing the Y coordinate. Here’s one possibility:

boxNode.Position = new Vector3(2, -1.5f, 7);

Notice the parameters to the Vector3 structure are defined as float so an f suffix is required for floating point values.

And now it looks a little more like a box. The camera is still pointing straight down the Z axis, but the box has been moved to the right and down, and you can see the top and left side.

You can probably also tell that the rear of the box is a little smaller than the front. By default, the camera employs a perspective projection which results in objects further from the camera being smaller than those closer to the camera. Urho also supports an orthographic camera, which does not employ projection and makes 3D objects look more like those in an engineering drawing.

Adjusting the Light

The illusion of three dimensions is also influenced by different shading on the three visible sides of the box. However, it appears that the left and front of the box are the same shade, which does not help the 3D effect.

UrhoSharp supports three basic types of light:

  • Directional: Light streaming in a particular direction from a seemingly infinite distance, much like the sun.

  • Point: Light streaming in all directions from a particular point, much like a lightbulb hanging from a wire.

  • Spot: Light coming from a particular point, but only streaming in a particular direction, much like a flashlight.

These are the three members of the LightType enumeration. Directional light has a direction; point light has a position; and spot light has both a position and direction. In addition, these different types of light sources can be customized by different properties of the Light class.

SimpleApplication creates a point light source with a particular position that you can query like so:

Vector3 lightPosition = app.LightNode.Position;

Because the light node is a subnode of the camera node (as you can verify by examining the Start method in SimpleApplication), the position of the light is relative to the position of the camera.

You can see a difference if you shift the light to the center of the coordinate system and raise it a bit:

app.LightNode.Position = new Vector3(0, 15, 0);

Now you’ll see all three sides of the box illuminated with different degrees of light. There is also some ambient light defined in the scene.

The Camera Field of View

By changing the Position property of the boxNode object, you can move the box nearer to the camera or further away, and you can move it anywhere around the window. If the box is very close to the camera, you don’t have much leeway in the X and Y coordinates. Try this:

boxNode.Position = new Vector3(-1, 0.75f, 3);

The box is large because it’s close to the camera, but it’s partially out of the window. Now try this:

boxNode.Position = new Vector3(10, 15, 50);

The box seems very far away but it’s safely within the window. You can experiment with other values (and you definitely should) to get a feel for this, but you might also have a need to quantify this relationship.

One of the crucial properties of the Camera class is named Fov, which stands for field of view. It’s an angle in degrees, but in practice it’s roughly equivalent to a zoom factor. By default the value is 45 degrees. You can access the Camera object that is created by SimpleApplication through the Camera property. Try changing the Fov property to 90:

app.Camera.Fov = 90;

The box seems to move away. We’ve zoomed out. Try changing it to a value less than 45 degrees to zoom in. When you’re finished experimenting, change it back to the default value of 45 because the rest of this discussion is based on that value:

app.Camera.Fov = 45;

The camera is located at the origin of the 3D coordinate system and pointing in the direction of the positive Z axis. Viewed from the side (from the vantage point of the positive X axis), the camera’s field of view looks like this:

Camera Point of View

The camera is shown as a little box on the origin, and the field of view is shown as two slanted blue lines. Almost everything between those two blue lines is within the range of the camera and visible within the window. (But not quite: The range of a perspective camera is actually described by a square frustum. which is a four-sided pyramid with the top shaved off. The Camera class defines two properties named NearClip and FarClip that define a range of distance that is visible to the camera. By default these properties have values of 0.1 and 1000. In most normal cases, you don’t have to worry about these values, but they’re necessary for the internal camera math to work.)

The diagram above shows the default Fov property value of 45 degrees. If you increase the field of view, a larger range becomes visible to the camera. It’s similar to zooming out. You can zoom into a scene by decreasing the field of view.

Graphics objects in the window are rendered so that the height of the window corresponds to the field of view. The width of the window might be the same as the height, less than the height, or greater than the height, which might correspond to the same field of view, or a a lower field of view, or a greater field of view. The ratio of the window’s width to its height is available as the AspectRatio property of the camera.

To quantify the relationship between the distance of an object from the camera and its visual size within the window, draw a line that bisects the Z axis, and label it H for height. The distance from that line to the camera is labeled D for distance:

Camera Distance and Width

Now you’ve defined two similar triangles, and it becomes a problem in trigonometry:

tan(FOV / 2) = (H / 2) / D

or:

H = 2D tan (FOV / 2)

For example, suppose a 3D object is 5 units from the camera. How tall would the object need to be to occupy the full height of the window? If FOV is 45 degrees, the tanget of 22.5 degrees is 0.414, and H equals 4.14 units.

The dimension of the Box object is a unit square: 1 unit by 1 unit by 1 unit. The value of the Position property corresponds to the center of the Box. To position the front of the Box five units from the camera, the Z coordinate of the Position property must be set to 5.5.

To position the box precisely in the upper-right corner of the window, the X and Y coordinates of the Position property must be set to 0.5 units less than half the width and half the height of the window. The following code cell performs this calculation. Notice the Urho MathHelper.DegreesToRadians call for converting the angle in degrees to radians for the Math.Tan function:

float D = 5.0f;
float FOV = app.Camera.Fov;
float y = 2 * D * (float)Math.Tan(MathHelper.DegreesToRadians(FOV / 2)) / 2;
float x = app.Camera.AspectRatio * y;

boxNode.Position = new Vector3(x - 0.5f, y - 0.5f, D + 0.5f);

Regardless of the window size (set in the ApplicationOptions argument to the SimpleApplication.Show method) and the camera field of view, the box should be positioned in the upper-right corner of the window.

Exploring Shapes

The following code creates another Node with a component for a donut-like shape known as a torus. The code is similar to the creation of boxNode:

// Remove node named "shapeNode" so there are no duplicates
mainNode.RemoveChild(mainNode.GetChild("shapeNode", false));

// Create a node for a Torus component
Node shapeNode = mainNode.CreateChild("shapeNode");
Shape shape = shapeNode.CreateComponent<Torus>();

Now that’s interesting! The camera is sitting in the middle of the torus (where the hole of the donut is) and part of the torus is visible from that perspective.

To see the whole torus, you can change its position (as with the box) but another solution is to change the position of the camera. This statement moves the camera back along the negative Z axis, as well as moving it to the left and up:

app.CameraNode.Position = new Vector3(-1, 1.5f, -6);

After running that code, you might have expected the torus to remain in the center of the screen. But keep in mind that the camera is still pointed in the positive Z direction. To position the torus in the center of the screen, the camera must be pointed at the torus, which is at the origin of the coordinate system.

Pointing the camera at the origin is actually easier than it might seem. The vector that points from one position to another position is the difference between the coordinates. You can calculate the vector that points from the camera to the origin by subtracting the camera position from the origin. This is equivalent to the negative of the camera position:

app.CameraNode.SetDirection(-app.CameraNode.Position);

That code will always point the camera to the 3D origin. And now the torus is in the center of the screen.

You can also move and zoom the camera using the keyboard. Make sure the Urho3D window has the input focus, and press W and S to zoom the camera in and out, and A and D to move the camera left and right. These four keys form an inverted T on the keyboard. As you move the camera left or right, notice that 3D objects seem to move opposititely.

You can also move the camera up, down, left, and right with the mouse. Move the mouse pointer inside the window, press the mouse button, and drag. This moves the camera, and 3D objects seem to move in the opposite direction.

Try the other shapes in the code cell above that creates the shape object using the CreateComponent generic function. They are: Cone, Cylinder, Plane, Pyramid, and Sphere. For Plane you’ll need to fully qualify the class name: Urho.Shapes.Plane.

Of course, you can change the color of this object. The color is not something common to all nodes, so it’s not a Node property. Instead, set the Color property of the Shape object. Several static fields are availble for common colors:

shape.Color = Color.Cyan;

Or, you can use a constructor of the Color structure:

shape.Color = new Color(0.5f, 1.0f, 0.25f, 1);

The arguments range from 0 to 1 and are in the order red, green, blue, and alpha, which governs transparency.

Before proceeding to the next section, move whatever shape you’re now looking at away from the origin:

shapeNode.Position = new Vector3(-2, 2, 10);

Translation, Scaling, and Rotation

The following code positions a cylinder on the origin:

// Remove node named "shapeNode" so there are no duplicates
mainNode.RemoveChild(mainNode.GetChild("cylinderNode", false));

// Create a node for a Cylinder component
Node cylinderNode = mainNode.CreateChild("cylinderNode");
Cylinder cylinder = cylinderNode.CreateComponent<Cylinder>();

The cylinder is one unit tall and has a one unit diameter. But what if you want a cylinder of a different size or proportions? Or perhaps you need to tip it over so that the ends are parallel to the YZ plane rather than the XZ plane.

These are jobs for graphics transforms, of which there are three basic types:

  • Translation: change the location of an object

  • Scaling: change the size of an object

  • Rotation: change the orientation of an object

Very often you’ll combine these transforms.

The transforms are applied to the Node object, and they affect all the node’s children as well. This feature allows you to create a composite object and move, scale, or rotate it as a whole.

The three properties that you set to realize these transforms are:

  • Position of type Vector3, which you’ve already seen

  • Scale of type Vector3

  • Rotation of type Quaternion

There are also some methods that you can use for transforms. For example, the Translate method changes the Position property, but the effect is accumulative: Every time you call Translate, the Position property is changed by that amount.

Similarly, the ScaleNode method is accumulative but the SetScale method is not. The Pitch, Yaw, and Roll methods are named after terms used in aerodynamics and perform accumulative rotations around the X, Y, and Z axes, respectively.

Transforms can often be difficult to understand, and rotations in particular can be very complex. This discussion is intended as a brief introduction.

The default Scale property is the Vector3 value (1, 1, 1). You can control the scaling factors on all three dimensions independently. Set them to values greater than 1 to increase the size. For example, a value of 2 doubles the size in that dimension. Set them to values less than 1 to decrease the size.

For example, suppose you want to make the cylinder skinnier but with double the length:

cylinderNode.Scale = new Vector3(0.5f, 2, 0.5f);

It’s necessary to set the X and Z scaling factors to the same values if you want to keep the cylinder round, but nothing prevents you from setting them to different values for an elliptical cylindar.

Watch out: The default Scale property is (1, 1, 1) and not (0, 0, 0). If you set Scale to (0, 0, 0), the object will disappear.

Three-dimensional rotation is an exceptionally complex topic, which you might have discerned from the type of the Rotation property being the Quaternion structure. A quaternion is a mathematical descripton of a 3D rotation and can be difficult to grasp intuitively, mostly because it involves three different types of imaginary numbers. The big advantage of quaternions in 3D computer graphics is that they interpolate well, which results in smooth animations.

Fortunately, there are simpler ways to describe 3D rotation. Perhaps easiest to visualize is the axis-angle rotation. You specify an axis, for instance, the X axis, and a rotational angle around that axis. A simple static method converts that rotation into a Quaternion value.

Often you’ll want to rotate something around the X, Y, or Z axis. The Vector3 structure defines three static readonly fields named UnitX, UnitY, and UnitZ which are vectors that equal (1, 0, 0), (0, 1, 0), and (0, 0, 1), respectively. Here’s how to rotate the cylinder 45 degrees around the Z axis:

cylinderNode.Rotation = Quaternion.FromAxisAngle(Vector3.UnitZ, 45);

You can predict the direction of rotation from another left hand rule: Point the thumb of your left hand in the direction of the axis of rotation, in this case the postive Z axis going into the screen. The other fingers of your left hand curl in the direction of positive rotation angles. You can change the direction by changing the sign of the rotation angle, or the direction of the axis.

You can now set the Position property to shift the cylinder so the bottom end is at the origin:

cylinderNode.Position = new Vector3(-0.707f, 0.707f, 0);

Before continuing, move the cylinder to the background:

cylinderNode.Position = new Vector3(-0.707f, 0.707f, 12);

Displaying the Axes

Very thin cylinders can be used to draw some rudimentary straight lines in 3D space. Here’s a function that accepts a Node argument and creates 60 subnodes to draw the axes of the 3D coordinate system. Each cylinder is scaled to a size of (0.025, 0.90, 0.25) and 10 each are stacked in the positive and negative X, Y, and Z directions:

void CreateAxes(Node axesNode)
{
    Vector3[] unitVectors = { Vector3.UnitX, Vector3.UnitY, Vector3.UnitZ };

    foreach (Vector3 unitVector in unitVectors)
    {
        for (int i = -10; i < 10; i++)
        {
            Node node = axesNode.CreateChild();
            Cylinder cylinder = node.CreateComponent<Cylinder>();
            cylinder.Color = Color.Black;
            cylinder.CastShadows = false;
            node.Scale = new Vector3(0.025f, 0.90f, 0.025f);

            if (unitVector == Vector3.UnitX)
            {
                node.Rotation = Quaternion.FromAxisAngle(Vector3.UnitZ, 90);
            }
            else if (unitVector == Vector3.UnitZ)
            {
                node.Rotation = Quaternion.FromAxisAngle(Vector3.UnitX, 90);
            }
            node.Position = (i + 0.5f) * unitVector;
        }
    }
}

Very little in this function should be new. The CastShadows property of the Cylinder is set to false; this prevents the axes from casting shadows on other objects. Also, notice how the Position property of each Node is set: The Vector3 structure supports multiplication of a vector by a number, often called a scalar in this context. The number is multiplied by all three components of the vector, but in this case two of them are zero.

To render these axes, create a child node and pass it to the CreateAxes function:

// Remove node named "axesNode" so there are no duplicates
mainNode.RemoveChild(mainNode.GetChild("axesNode", false));

// Create node for axes
Node axesNode = mainNode.CreateChild("axesNode");
CreateAxes(axesNode);

Now move the Box object so that the front left bottom corner is aligned with the origin:

boxNode.Position = new Vector3(0.5f, 0.5f, 0.5f);

There’s nothing that prevents multiple 3D objects from occupying the same space. Only unobscured surfaces are rendered, so if part of one 3D object is inside another, the hidden surfaces are simply not drawn. The box hides a quarter sliver of each of the three cylinders that it abuts.

This workbook has introduced you to the Urho 3D coordinate system, but the best way to get more familiar with it is by experimentation with these shapes and by creating others. Have fun!

You can’t perform that action at this time.