The Transform class

A 3D virtual reality scene will be constructed from various objects, each with a position, rotation, and scale in 3D dimensional space defined by a Transform.

It will also be naturally useful to permit transforms to be grouped hierarchically. This grouping also creates a distinction between local space and world space, where children only keep track of the difference between their translation, rotation, and scale (TRS) and that of their parent (local space). The actual data that we are storing is the local position (we'll use the words position and translation interchangeably), rotation, and scale. Global position, rotation, and scale are computed by combining the local TRS all the way up the chain of parents.

First, let's define the Transform class. In the Android Studio hierarchy panel, right-click on renderbox/, go to New | Java Class, and name it Transform.

Each Transform may have one or more associated components. Typically there is just one, but it is possible to add as many as you want (as we'll see in the other projects in this book). We'll maintain a list of components in the transform, as follows:

public class Transform {
    private static final String TAG = "RenderBox.Transform";

    List<Component> components = new ArrayList<Component>();
    
     public Transform() {}

    public Transform addComponent(Component component){
        component.transform = this;
        return this;
    }
    public List<Component> getComponents(){
        return components;
    }
}

We will define the Component class in the next topic. If it really bothers you to reference it now before it's defined, you can start with an empty Component Java class in the renderbox/components folder.

Now back to the Transform class. A Transform object has a location, orientation, and scale in space, defined by its localPosition, localRotation, and localScale variables. Let's define these private variables, and then add the methods to manipulate them.

Also, as transforms can be arranged in a hierarchy, we'll include a reference to a possible parent transform, as follows:

    private Vector3 localPosition = new Vector3(0,0,0);
    private Quaternion localRotation = new Quaternion();
    private Vector3 localScale = new Vector3(1,1,1);

    private Transform parent = null;

The position, rotation, and scale values are initialized to identity values, that is, no positional offset, rotation, or resizing until they are explicitly set elsewhere. Note that the identity scale is (1, 1, 1).

The parent transform variable allows each transform to have a single parent in the hierarchy. You can keep the list of children in the transform, but you might be surprised to know how far you can get without having to move down the hierarchy. If you can avoid it, as we have, you can save a good deal of branching when setting/unsetting a parent reference. Maintaining the list of children means an O(n) operation every time you unparent an object, and an extra O(1) insertion cost on setting a parent. It is also not very efficient to hunt through children looking for a particular object.

Parent methods

The transform can be added or removed from its position in the hierarchy with the setParent and unParent methods, respectively. Let's define them now:

    public Transform setParent(Transform Parent){
        setParent(parent, true);
        return this;
    }

    public Transform setParent(Transform parent, boolean updatePosition){
        if(this.parent == parent)
        //Early-out if setting same parent--don't do anything
            return this;
        if(parent == null){
            unParent(updatePosition);
            return this;
        }

        if(updatePosition){
            Vector3 tmp_position = getPosition();
            this.parent = parent;
            setPosition(tmp_position);
        } else {
            this.parent = parent;
        }
        return this;
    }

    public Transform upParent(){
        unParent(true);
        return this;
    }

    public Transform unParent(boolean updatePosition){
        if(parent == null)
        //Early out--we already have no parent
            return this;
        if(updatePosition){
            localPosition = getPosition();
        }
        parent = null;
        return this;
    }

Simply, the setParent method sets this.parent to the given parent transform. Optionally, you can specify that the position is updated relative to the parent. We added an optimization to skip this procedure if the parent is already set. Setting the parent to null is equivalent to calling unParent.

The unParent method removes the transform from the hierarchy. Optionally, you can specify that the position is updated relative to the (previous) parents, so that the transform is now disconnected from the hierarchy but remains in the same position in world space.

Note that the rotation and scale can, and should, also be updated when parenting and unparenting. We don't need that in the projects in this book, so they have been left as an exercise for the reader. Also, note that our setParent methods include an argument for whether to update the position. If it is false, the operation runs a little faster, but the global state of the object will change if the parent transform was not set to identity (no translation, rotation, or scale). For convenience, you may set updatePosition to true, which will apply the current global transformation to the local variables, keeping the object fixed in space, with its current rotation and scale.

Position methods

The setPosition methods set the transform position relative to the parent, or apply absolute world position to the local variable if there is no parent. Two overloads are provided if you want to use a vector or individual component values. getPosition will compute the world space position based on parent transforms, if they exist. Note that this will have a CPU cost related to the depth of the transform hierarchy. As an optimization, you may want to include a system to cache world space positions within the Transform class, invalidating the cache whenever a parent transform is modified. A simpler alternative would be to make sure that you store the position in a local variable right after calling getPosition. The same optimization applies for rotation and scale.

Define the position getters and setters as follows:

    public Transform setPosition(float x, float y, float z){
        if(parent != null){
            localPosition = new Vector3(x,y,z).subtract(parent.getPosition());
        } else {
            localPosition = new Vector3(x, y, z);
        }
        return this;
    }

    public Transform setPosition(Vector3 position){
        if(parent != null){
            localPosition = new Vector3(position).subtract(parent.getPosition());
        } else {
            localPosition = position;
        }
        return this;
    }

    public Vector3 getPosition(){
        if(parent != null){
            return Matrix4.TRS(parent.getPosition(), parent.getRotation(), parent.getScale()).multiplyPoint3x4(localPosition);
        }
        return localPosition;
    }

    public Transform setLocalPosition(float x, float y, float z){
        localPosition = new Vector3(x, y, z);
        return this;
    }

    public Transform setLocalPosition(Vector3 position){
        localPosition = position;
        return this;
    }

    public Vector3 getLocalPosition(){
        return localPosition;
    }

Rotation methods

The setRotation methods set the transform rotation relative to the parent, or the absolute world rotation is applied to the local variable if there is no parent. Again, multiple overloads provide options for different input data. Define the rotation getters and setters as follows:

    public Transform setRotation(float pitch, float yaw, float roll){
        if(parent != null){
            localRotation = new Quaternion(parent.getRotation()).multiply(new Quaternion().setEulerAngles(pitch, yaw, roll).conjugate()).conjugate();
        } else {
            localRotation = new Quaternion().setEulerAngles(pitch, yaw, roll);
        }
        return this;
    }

    /**
     * Set the rotation of the object in global space
     * Note: if this object has a parent, setRoation modifies the input rotation!
     * @param rotation
     */
    public Transform setRotation(Quaternion rotation){
        if(parent != null){
            localRotation = new Quaternion(parent.getRotation()).multiply(rotation.conjugate()).conjugate();
        } else {
            localRotation = rotation;
        }
        return this;
    }

    public Quaternion getRotation(){
        if(parent != null){
            return new Quaternion(parent.getRotation()).multiply(localRotation);
        }
        return localRotation;
    }

    public Transform setLocalRotation(float pitch, float yaw, float roll){
        localRotation = new Quaternion().setEulerAngles(pitch, yaw, roll);
        return this;
    }

    public Transform setLocalRotation(Quaternion rotation){
        localRotation = rotation;
        return this;
    }
    
    public Quaternion getLocalRotation(){
        return localRotation;
    }

    public Transform rotate(float pitch, float yaw, float roll){
        localRotation.multiply(new Quaternion().setEulerAngles(pitch, yaw, roll));
        return this;
    }

Scale methods

The setScale methods set the transform scale relative to the parent, or apply the absolute scale to the local variable if there is no parent. Define getters and setters for the scale as follows:

    public Vector3 getScale(){
        if(parent != null){
            Matrix4 result = new Matrix4();
            result.setRotate(localRotation);
            return new Vector3(parent.getScale())
                .scale(localScale);
        }
        return localScale;
    }

    public Transform setLocalScale(float x, float y, float z){
        localScale = new Vector3(x,y,z);
        return this;
    }

    public Transform setLocalScale(Vector3 scale){
        localScale = scale;
        return this;
    }

    public Vector3 getLocalScale(){
        return localScale;
    }

    public Transform scale(float x, float y, float z){
        localScale.scale(x, y, z);
        return this;
    }

Transform to matrix and draw

The last thing we need to do with the Transform class is transform an identity matrix into one that will tell OpenGL how to draw the object correctly. To do this, we translate, rotate, and scale the matrix, in that order. Technically, we can also do cool things with matrices, such as shearing and skewing models, but the math is complicated enough as it is. If you want to learn more, type transformation matrix, quaternion to matrix, and some of the other terms that we have been throwing around into a search engine. The actual math behind all of this is fascinating and way too detailed to explain in a single paragraph.

We also provide the drawMatrix() function that sets up the lighting and model matrices for a draw call. Since the lighting model is an intermediate step, it makes sense to combine this call;

    public float[] toFloatMatrix(){
        return Matrix4.TRS(getPosition(), getRotation(), getScale()).val;
    }

    public float[] toLightMatrix(){
        return Matrix4.TR(getPosition(), getRotation()).val;
    }

    /**
     * Set up the lighting model and model matrices for a draw call
     * Since the lighting model is an intermediate step, it makes sense to combine this call
     */
    public void drawMatrices() {
        Matrix4 modelMatrix = Matrix4.TR(getPosition(), getRotation());
        RenderObject.lightingModel = modelMatrix.val;
        modelMatrix = new Matrix4(modelMatrix);
        RenderObject.model = modelMatrix.scale(getScale()).val;
    }

The drawMatrices method uses variables from the RenderObject class, which will be defined later. It might seem very anti-Java that we are just setting our matrices to static variables in the RenderObject class. As you will see, there is actually no need for multiple instances of the lightingModel object and model to exist. They are always calculated just in time for each object as they are drawn. If we were to introduce optimizations that avoid recomputing this matrix all the time, it would make sense to keep the information around. For the sake of simplicity, we just recalculate the matrix every time each object is drawn, since it might have changed since the last frame.

Next, we'll see how the Transform class gets used when we implement the Component class, which will be extended by a number of classes that define objects in the 3D scene.

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

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