0
\$\begingroup\$

I implemented scene graph hierarchy into my 3D game engine, and there seems to be a problem with the camera's rotation when it is a child of another object. It only happens with the camera, not any other object.

Here's some videos illustrating the problem:

Rotation on normal object when it is a child: https://www.youtube.com/watch?v=3MNFq7NTccE

Rotation on camera when it is a child: https://www.youtube.com/watch?v=jv4CrgjfV0s

Here's some of my code for the game engine:

Node class (Scene Graph):

package net.bigengine.scene;

import java.util.*;

import net.bigengine.EEngine;
import net.bigengine.logic.EControl;
import net.bigengine.math.Matrix4;
import net.bigengine.render.ESceneRenderer;
import net.bigengine.scene.physics.EBoundingVolume;
import net.bigengine.scene.physics.EPhysicsEngine;

public class ENode {

    protected HashMap<String, ENode> children;
    protected ENode parent;

    protected ETransform transform = new ETransform();

    protected final String name;

    private boolean updating;

    public ENode(String name) {
        this.name = name;
        this.children = new HashMap<>();
        this.controls = new ArrayList<>();
        this.boundingVolumes = new ArrayList<>();
        this.visible = true;
    }

    public ETransform getWorldTransform() {
        return ETransform.fromTransformMatrix(getWorldTransformMatrix());
    }

    public Matrix4 getWorldTransformMatrix() {
        Matrix4 localTransform = transform.toTransformMatrix();
        if (parent == null || parent instanceof EScene) return localTransform;
            return Matrix4.mul(parent.getWorldTransformMatrix(), localTransform, null);
    }

    public ETransform getTransform() {
        return transform;
    }

    public void setTransform(ETransform transform) {
        this.transform = transform;
    }

    public String getName() {
        return name;
    }

    public void addChild(ENode child) {
        child.parent = this;
        if (updating) {    // To avoid ConcurrentModificaionExceptions
            @SuppressWarnings("unchecked")
            HashMap<String, ENode> childrenCopy = (HashMap<String, ENode>) children.clone();
            childrenCopy.put(child.name, child);
            children = childrenCopy;
        } else {
            children.put(child.name, child);
        }
    }

    public void removeChild(String child) {
        children.get(child).parent = null;
        if (updating) {    // To avoid ConcurrentModificationExceptions
            @SuppressWarnings("unchecked")
            HashMap<String, ENode> childrenCopy = (HashMap<String, ENode>) children.clone();
            childrenCopy.remove(child);
            children = childrenCopy;
        } else {
            children.remove(child);
        }
    }

    public void removeChild(ENode child) {
        removeChild(child.name);
    }

    public void removeChildHierarchy(String child) {
        if (hasChild(child)) {
            removeChild(child);
        } else {
            for (ENode node : children.values()) {
                if (node.hasChildHierarchy(child)) {
                    node.removeChildHierarchy(child);
                    return;
                }
            }
        }
    }

    public void removeChildHierarchy(ENode child) {
        removeChildHierarchy(child.name);
    }

    public ENode getChild(String name) {
        return children.get(name);
    }

    public ENode getChildHierarchy(String name) {
        if (children.containsKey(name)) return children.get(name);
        for (ENode child : children.values()) {
            ENode result = child.getChildHierarchy(name);
            if (result != null) return result;
        }
        return null;
    }

    public ArrayList<ENode> getChildren() {
        return (ArrayList<ENode>) children.values();
    }

    public boolean hasChild(String name) {
        return children.containsKey(name);
    }

    public boolean hasChild(ENode node) {
        return hasChild(node.name);
    }

    public boolean hasChildHierarchy(String name) {
        if (hasChild(name)) return true;
        for (ENode child : children.values()) {
            if (child.hasChildHierarchy(name)) return true;
        }
        return false;
    }

    public boolean hasChildHierarchy(ENode node) {
        return hasChildHierarchy(node.name);
    }

    public void clearChildren() {
        children.clear();
    }

    public void update(EEngine engine) {
        updating = true;
        for (ENode obj : children.values()) {
            obj.update(engine);
        }
        updating = false;
    }
}

Camera class:

package net.bigengine.scene;

public class ECamera extends ENode {

    private float far;
    private float near;
    private float fov;

    public ECamera(String name, float far, float near, float fov) {
        super(name);
        this.far = far;
        this.near = near;
        this.fov = fov;
    }

    public ECamera(String name) {
        this(name, 1000, 0.1f, 70);
    }

    public void setFar(float far) {
        this.far = far;
    }

    public float getFar() {
        return far;
    }

    public void setNear(float near) {
        this.near = near;
    }

    public float getNear() {
        return near;
    }

    public void setFOV(float fov) {
        this.fov = fov;
    }

    public float getFOV() {
        return fov;
    }

}

Transform class:

package net.bigengine.scene;

import net.bigengine.math.Matrix4;
import net.bigengine.math.Quaternion;
import net.bigengine.math.Vector3;

public class ETransform {

    private Vector3 translation;
    private Quaternion rotation;
    private Vector3 scale;

    public ETransform(Vector3 translation, Quaternion rotation, Vector3 scale) {
        this.translation = translation;
        this.rotation = rotation;
        this.scale = scale;
    }

    public ETransform(Vector3 translation, Vector3 rotation, Vector3 scale) {
        this(translation, new Quaternion(), scale);
        this.rotation.setEuler(rotation);
    }

    public ETransform() {
        this(new Vector3(), new Quaternion(), new Vector3(1, 1, 1));
    }

    public Vector3 getTranslation() {
        return translation;
    }

    public ETransform setTranslation(Vector3 translation) {
        this.translation = translation;
        return this;
    }

    public Quaternion getRotation() {
        return rotation;
    }

    public ETransform setRotation(Quaternion rotation) {
        this.rotation = rotation;
        return this;
    }

    public Vector3 getScale() {
        return scale;
    }

    public ETransform setScale(Vector3 scale) {
        this.scale = scale;
        return this;
    }

    public Matrix4 toTransformMatrix() {
        Matrix4 mat = new Matrix4();
        Matrix4.translate(translation, mat, mat);
        Matrix4.mul(mat, rotation.getMatrix(), mat);
        Matrix4.scale(scale, mat, mat);
        return mat;
    }

    public Matrix4 toViewMatrix() {
        Matrix4 mat = new Matrix4();
        Matrix4.mul(mat, rotation.negate(null).getMatrix(), mat);
        Matrix4.translate(translation.negate(null), mat, mat);
        return mat;
    }

    public static ETransform fromTransformMatrix(Matrix4 mat) {
        Vector3 translation = new Vector3();
        translation.x = mat.m30;
        translation.y = mat.m31;
        translation.z = mat.m32;
        Vector3 scale = new Vector3();
        scale.x = new Vector3(mat.m00, mat.m10, mat.m20).length();
        scale.y = new Vector3(mat.m01, mat.m11, mat.m21).length();
        scale.z = new Vector3(mat.m02, mat.m12, mat.m22).length();
        Quaternion rotation = new Quaternion().setMatrix(mat);
        return new ETransform(translation, rotation, scale);
    }

    @Override
    public ETransform clone() {
        return new ETransform(translation, rotation, scale);
    }

    @Override
    public String toString() {
        return "Transform(translation=" + translation +
                ", rotation=" + rotation + ", scale=" + scale + ")";
    }

}

Quaternion class:

package net.bigengine.math;

public class Quaternion extends Vector<Quaternion> {

    public float x, y, z, w;

    public Quaternion(float x, float y, float z, float w) {
        set(x, y, z, w);
    }

    public Quaternion(Quaternion other) {
        set(other);
    }

    public Quaternion() {
        this(0, 0, 0, 1);
    }

    @Override
    public Quaternion set(Quaternion other) {
        return set(other.x, other.y, other.z, other.w);
    }

    public Quaternion set(float x, float y, float z, float w) {
        this.x = x;
        this.y = y;
        this.z = z;
        this.w = w;
        return this;
    }

    @Override
    public float lengthSquared() {
        return x * x + y * y + z * z + w * w;
    }

    @Override
    public Quaternion normalize(Quaternion dest) {
        float l = length();
        if (dest == null) dest = new Quaternion(x / l, y / l, z / l, w / l);
        else dest.set(x / l, y / l, z / l, w / l);
        return dest;
    }

    @Override
    public Quaternion scale(float scale) {
        x *= scale;
        y *= scale;
        z *= scale;
        w *= scale;
        return this;
    }

    @Override
    public Quaternion add(Quaternion right, Quaternion dest) {
        return add(this, right, dest);
    }

    @Override
    public Quaternion sub(Quaternion right, Quaternion dest) {
        return sub(this, right, dest);
    }

    @Override
    public Quaternion mul(Quaternion right, Quaternion dest) {
        return mul(this, right, dest);
    }

    @Override
    public Quaternion div(Quaternion right, Quaternion dest) {
        return div(this, right, dest);
    }

    @Override
    public Quaternion negate(Quaternion dest) {
        return negate(this, dest);
    }

    public Matrix4 getMatrix() {
        Matrix4 matrix = new Matrix4();
        float xy = x * y;
        float xz = x * z;
        float xw = x * w;
        float yz = y * z;
        float yw = y * w;
        float zw = z * w;
        float xSquared = x * x;
        float ySquared = y * y;
        float zSquared = z * z;
        matrix.m00 = 1 - 2 * (ySquared + zSquared);
        matrix.m01 = 2 * (xy - zw);
        matrix.m02 = 2 * (xz + yw);
        matrix.m03 = 0;
        matrix.m10 = 2 * (xy + zw);
        matrix.m11 = 1 - 2 * (xSquared + zSquared);
        matrix.m12 = 2 * (yz - xw);
        matrix.m13 = 0;
        matrix.m20 = 2 * (xz - yw);
        matrix.m21 = 2 * (yz + xw);
        matrix.m22 = 1 - 2 * (xSquared + ySquared);
        matrix.m23 = 0;
        matrix.m30 = 0;
        matrix.m31 = 0;
        matrix.m32 = 0;
        matrix.m33 = 1;
        return matrix;
    }

    public Vector3 getEuler() {
        Vector3 euler = new Vector3();
        float ySqr = y * y;
        float t0 = 2f * (w * x + y * z);
        float t1 = 1f - 2f * (x * x + ySqr);
        euler.x = MathUtil.atan2(t0, t1);
        float t2 = 2f * (w * y - z * x);
        t2 = t2 >= 1 ? 1 : t2;
        t2 = t2 <= -1 ? -1 : t2;
        euler.y = MathUtil.asin(t2);
        float t3 = 2f * (w * z + x * y);
        float t4 = 1f - 2f * (ySqr + z * z);
        euler.z = MathUtil.atan2(t3, t4);
        euler.scale(MathUtil.RAD_TO_DEG);
        return euler;
    }

    public Quaternion setMatrix(Matrix4 matrix) {
        float diagonal = matrix.m00 + matrix.m11 + matrix.m22;
        if (diagonal > 0) {
            float w4 = MathUtil.sqrt(diagonal + 1f) * 2f;
            w = w4 / 4f;
            x = (matrix.m21 - matrix.m12) / w4;
            y = (matrix.m02 - matrix.m20) / w4;
            z = (matrix.m10 - matrix.m01) / w4;
        } else if ((matrix.m00 > matrix.m11) && (matrix.m00 > matrix.m22)) {
            float x4 = MathUtil.sqrt(1f + matrix.m00 - matrix.m11 - matrix.m22) * 2f;
            w = (matrix.m21 - matrix.m12) / x4;
            x = x4 / 4f;
            y = (matrix.m01 + matrix.m10) / x4;
            z = (matrix.m02 + matrix.m20) / x4;
        } else if (matrix.m11 > matrix.m22) {
            float y4 = MathUtil.sqrt(1f + matrix.m11 - matrix.m00 - matrix.m22) * 2f;
            w = (matrix.m02 - matrix.m20) / y4;
            x = (matrix.m01 + matrix.m10) / y4;
            y = y4 / 4f;
            z = (matrix.m12 + matrix.m21) / y4;
        } else {
            float z4 = MathUtil.sqrt(1f + matrix.m22 - matrix.m00 - matrix.m11) * 2f;
            w = (matrix.m10 - matrix.m01) / z4;
            x = (matrix.m02 + matrix.m20) / z4;
            y = (matrix.m12 + matrix.m21) / z4;
            z = z4 / 4f;
        }
        return this;
    }

    public Quaternion setEuler(Vector3 eulerRot) {
        return setEuler(eulerRot.x, eulerRot.y, eulerRot.z);
    }

    public Quaternion setEuler(float roll, float pitch, float yaw) {
        float rollRad = roll * MathUtil.DEG_TO_RAD;
        float pitchRad = pitch * MathUtil.DEG_TO_RAD;
        float yawRad = yaw * MathUtil.DEG_TO_RAD;
        float cy = MathUtil.cos(yawRad / 2f);
        float sy = MathUtil.sin(yawRad / 2f);
        float cr = MathUtil.cos(rollRad / 2f);
        float sr = MathUtil.sin(rollRad / 2f);
        float cp = MathUtil.cos(pitchRad / 2f);
        float sp = MathUtil.sin(pitchRad / 2f);
        w = cy * cr * cp + sy * sr * sp;
        x = cy * sr * cp - sy * cr * sp;
        y = cy * cr * sp + sy * sr * cp;
        z = sy * cr * cp - cy * sr * sp;
        return this;
    }

    @Override
    public String toString() {
        return "Quaternion(" + x + ", " + y + ", " + z + ", " + w + ")";
    }

    @Override
    public Quaternion clone() {
        return new Quaternion(this);
    }

    public static Quaternion add(Quaternion left, Quaternion right, Quaternion dest) {
        if (dest == null) dest = new Quaternion();
        dest.set(left.x + right.x, left.y + right.y, left.z + right.z, left.w +
                right.w);
        return dest;
    }

    public static Quaternion sub(Quaternion left, Quaternion right, Quaternion dest) {
        if (dest == null) dest = new Quaternion();
        dest.set(left.x - right.x, left.y - right.y, left.z - right.z, left.w -
                right.w);
        return dest;
    }

    public static Quaternion mul(Quaternion left, Quaternion right, Quaternion dest) {
        if (dest == null) dest = new Quaternion();
        dest.set(left.x * right.x, left.y * right.y, left.z * right.z, left.w *
                right.w);
        return dest;
    }

    public static Quaternion div(Quaternion left, Quaternion right, Quaternion dest) {
        if (dest == null) dest = new Quaternion();
        dest.set(left.x / right.x, left.y / right.y, left.z / right.z, left.w /
                right.w);
        return dest;
    }

    public static Quaternion negate(Quaternion src, Quaternion dest) {
        if (dest == null) dest = new Quaternion();
        dest.set(-src.x, -src.y, -src.z, src.w);
        return dest;
    }

}

I get the camera's view matrix by camera.getWorldTransform().toViewMatrix by the way.

\$\endgroup\$
  • \$\begingroup\$ Your videos are "unavailable," but my question is whether the matrix building is doing what you think it is. RotTrans is way different from TransRot, as you probably know. The usual is Scale then Rot then Trans when building the transform matrix. Verify that objects are being transformed properly in data before tackling the camera issue. Divide and conquer when debugging. \$\endgroup\$ – Patrick Hughes Dec 22 '18 at 22:02
0
\$\begingroup\$

It's early for me and I have no way of testing, but I think:

Matrix4.mul(mat, rotation.negate(null).getMatrix(), mat);
Matrix4.translate(translation.negate(null), mat, mat);

Should be

Matrix4.translate(translation.negate(null), mat, mat);
Matrix4.mul(mat, rotation.negate(null).getMatrix(), mat);

In toViewMatrix(). Remember, in matrix multiplication ordering matters.

\$\endgroup\$

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.