Graphing a function in three dimensions

This last recipe in the chapter is probably also the most advanced in the entire book, but it can be used to create an impressive effect.

In this recipe, we define our own generated 3D object, not based on cubes or other existing shapes. To obtain this result, we define a very basic geometry that we used to build a custom shape.

Getting ready

The starting setup is similar as before. We will define a new class to hold our graph. So copy any of the previous recipes, but remove all references to Graph3D and the data from it.

How to do it...

The idea here is to plot a three-dimensional function:

y = (sin(x)² . cos(z)²) / (5 . x² . z²)

  1. We will use Flash's built-in Vector3D class to clean up some of the code. Vector3D is an object that simply holds the tree's x, y, and z values of a point in the 3D world.
  2. We will work from the bottom up. First create a new class called RectangleGeometry, as shown in the following code:
    package com.graphing 
    {
        import away3d.core.base.SubGeometry;
        import away3d.primitives.PrimitiveBase;
    
        import flash.geom.Vector3D;
        public class RectangleGeometry extends PrimitiveBase
        {
            private var _v0:Vector3D;
            private var _v1:Vector3D;
            private var _v2:Vector3D;
            private var _v3:Vector3D;
    
            public function RectangleGeometry(v0:Vector3D, v1:Vector3D, v2:Vector3D, v3:Vector3D)
            {
                super();
    
                _v0 = v0;
                _v1 = v1;
                _v2 = v2;
                _v3 = v3;
            }
    
            override protected function buildGeometry(target:SubGeometry):void
            {
                var rawVertices:Vector.<Number> = Vector.<Number>([_v0.x, _v0.y, _v0.z, _v1.x, _v1.y, _v1.z, _v2.x, _v2.y, _v2.z, _v3.x, _v3.y, _v3.z]);
                var rawIndices:Vector.<uint> = Vector.<uint>([0, 1, 2, 0, 2, 3]);
    
                target.autoDeriveVertexNormals = true;
                target.autoDeriveVertexTangents = true;
                target.updateVertexData(rawVertices);
                target.updateIndexData(rawIndices);
            }
    
            override protected function buildUVs(target:SubGeometry):void
            {
                //TODO if you want to use textures
            }
        }
    }

    This class is responsible for drawing individual rectangles out of the function's surface.

  3. Now create a Function3D class that will combine data points into a 3D surface, using the RectangleGeometry class:
    package com.graphing
    {
        import away3d.containers.ObjectContainer3D;
        import away3d.entities.Mesh;
        import away3d.materials.ColorMaterial;
        import away3d.materials.lightpickers.LightPickerBase;
        import away3d.materials.lightpickers.StaticLightPicker;
        import away3d.materials.MaterialBase;
        import away3d.primitives.CubeGeometry;
        import away3d.primitives.PlaneGeometry;
        import away3d.tools.commands.Merge;
        import flash.geom.Vector3D;
    
        public class Function3D extends ObjectContainer3D
        {
            private var _data:Array;
            private var _lightPicker:LightPickerBase;
    
            public function Function3D(lightPicker:LightPickerBase) 
            {
                this._lightPicker = lightPicker;
            }
    
            public function set data(value:Array):void {
    _data = value;
    }
    
            public function createGraph():void {
                var material:ColorMaterial = new ColorMaterial(0xff9933);
                material.lightPicker = _lightPicker;
    
                for (var i:int = 1; i < _data.length; i++) {
                for (var j:int = 1; j < _data[i].length; j++) {
                    var v1:Vector3D = _data[i][j];
                    var v2:Vector3D = _data[i][j - 1];
                    var v3:Vector3D = _data[i - 1][j - 1];
                    var v4:Vector3D = _data[i-1][j];
    
                    drawRectangle(v1, v2, v3, v4, material);
                }
            }
    
        }
    
    private function drawRectangle(v1:Vector3D, v2:Vector3D, v3:Vector3D, v4:Vector3D, material:MaterialBase):void
            {
            var rectangleGeometry:RectangleGeometry = new RectangleGeometry(v1, v2, v3, v4);
            var rectangle:Mesh = new Mesh(rectangleGeometry, material);
        addChild(rectangle);
        }
      }
    
    }
  4. In the Main class, add the following helper functions to calculate the surface values:
    private function calculateData():Array {
        _data = [];
        for (var x:Number = -5; x <= 5; x += .25) {
            var line:Array = [];
            for (var z:Number = -5; z <= 5; z += .25) {
                line.push(new Vector3D(x*100, surface(x, z) * 2000, z*100));
            }
            _data.push(line);
        }
        return _data;
    }
    
    private function surface(x:Number, z:Number):Number {
        return (Math.pow(Math.sin(x), 2) * Math.pow(Math.cos(z), 2)) / (5 + x * x + z * z);
    }
  5. And finally, we use these calculated functions to construct the 3D representation of the function. Add the following last bit of code to the constructor of the class:
    _graph = new Function3D(new StaticLightPicker([light]));
    _graph.data = calculateData();
    _graph.createGraph();
    _view.scene.addChild(_graph);

    The result is as shown in the following screenshot:

    How to do it...

How it works...

Our approach is similar to the Creating a line graph based on a function recipe in Chapter 1, Getting Started with Graph Drawing. We calculate the value of the function at discrete intervals and draw straight lines between those values. If the gap between values is small enough, the end result is a smooth function graph.

In this example, computer processing and graphic's power will be the limiting factor. Depending on your computer speed, you may need to increase the gap between values. If the end result is too slow, you should increase the value inside the loops. It's currently at 0.25, but even at 1 it will still give you an idea of the function's shape.

As a base, we use rectangles, because that is the form in which we calculate the coordinates. We've deliberately kept the class as simple as possible. The main thing to notice is that there are four vertices, but six indices. That is because we need to construct every item in the 3D world out of triangles.

Take a look at the following diagram of a random quadrilateral.

How it works...

To draw one rectangle, we need to draw two triangles: a triangle between vertices 0, 1, and 2, and one between vertices 0, 2, and 3.

The indices point into the array of vertices, so we can re-use vertices multiple times to create our geometry.

When looking at the Function3D code, it is important to notice that we are using a different data structure than in the previous recipes. This change was made to make the code easier to read and adapt.

We now store the Vector3D points inside a 2D array (one dimension for x, one for z). This allows us to easily build up the RectangleGeometry instances and add these to the scene.

In the Main class, we've moved the function we want to plot into its very own surface function. This allows you to easily plug in other formulas and see how these look.

The calculateData function will calculate a datapoint at set distances. We've also added a little bit of scaling code (multiplication by 100 and 2000), so that we don't need to reconfigure the camera and controller.

There's more...

If you experiment a little with the application, you'll notice that there are many improvements or changes that can be made. The following are a few.

One geometry

If you look closely at the individual rectangles, you'll notice that, even though they aren't flat, they are rendered perfectly smooth, similar to how a sphere is rendered.

Between rectangles you can see a hard edge.

The reason for this edge is that the different rectangles are individual geometries. If you want a perfectly smooth surface, you will want to create one big geometry. This would involve moving the loops within the createGraph function inside the RectangleGeometry class.

It's a fairly simple tweak that will have an impressive visual effect.

Color change based on height

The surface now has one uniform color. Due do the lighting, it is possible to clearly see the peaks. However, you may also want to experiment with changing the color based on the height of the rectangle. This will give the higher peaks a different color than the lower areas.

Two-sided surfaces

If you spin around the surface, you'll notice that it disappears. That's because we only defined the top surface and not the bottom one.

The top of a rectangle is defined by the direction in which the coordinates are entered. They should always be entered in a clockwise direction, when looking directly at the rectangle.

So you can draw a second down-facing surface, by adding a second rectangle, but with the coordinates in counterclockwise order.

See also

This recipe has used the very basis of a 3D engine, namely triangles. If you want to fully understand what's happening, your best starting point is reading up on how 3D engines work.The Gamedev website has a large section on technical books: http://www.gamedev.net/page/books/index.html/_/technical/graphics-programming-10/.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.58.60.192