You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

LWJGL3下OpenGL场景图父子节点变换继承异常问题求助

场景图父子节点变换异常问题

问题描述

基于OpenGL使用LWJGL3构建场景图,实现父节点变换作用于子节点的逻辑时功能运行异常。
核心问题为子节点的旋转无法正确跟随父节点:子节点确实会围绕父对象中心公转,符合预期逻辑,但公转速度会随父节点旋转角度增大而加快,当父节点旋转满一周回到初始角度时速度又回落至初始值。此前还遇到过子节点相对父节点平移不正确的问题,已做临时修复,具体逻辑可参考下文代码。


相关代码

Transform类(变换逻辑核心实现)

public class Transform {
    
    private Vector3f position, lastPosition;
    private Vector3f rotation, lastRotation;
    private Vector3f scale, lastScale;
    
    private Transform parent;
    private Matrix4f parentMatrix;
    
    public Transform() {
        this(null, null, null);
    }
    
    public Transform(Vector3f position, Vector3f rotation, Vector3f scale) {
        this.position = position != null ? position : new Vector3f(0, 0, 0);
        this.rotation = rotation != null ? rotation : new Vector3f(0, 0, 0);
        this.scale = scale != null ? scale : new Vector3f(1, 1, 1);
        
        this.lastPosition = new Vector3f(0, 0, 0);
        this.lastRotation = new Vector3f(0, 0, 0);
        this.lastScale = new Vector3f(1, 1, 1);
        
        parentMatrix = new Matrix4f().identity();
    }
    
    public boolean requireTMUpdate() { //checks if the matrix has changed and requires an update
        
        if(parent != null) {
            return parent.requireTMUpdate();
        }
        
        if(!position.equals(lastPosition)) {
            lastPosition.set(position);
            return true;
        }
        
        if(!rotation.equals(lastRotation)) {
            lastRotation.set(rotation);
            return true;
        }
        
        if(!scale.equals(lastScale)) {
            lastScale.set(scale);
            return true;
        }
        
        return false;
    }
    
    public Matrix4f getTransformation() {
        
        if((parent != null) && (requireTMUpdate())) {
            if(!(getParent().equals(getParent().getParent()))) {

                parentMatrix.set(parent.getTransformation()); 

                //The above line sets the updated parentMatrix

                setPosition(parentMatrix.transformPosition(position)); 

                //The above line sets the position to where child is supposed to be in 
                //relation the parent, otherwise once the key is stopped being pressed, 
                //it will return to the position at the start of the game / program.
            }
            
        }else {
            parentMatrix.rotationXYZ(0, 0, 0).translation(0, 0, 0).scale(1); 
            
            // The above line is supposed to reset the matrix, otherwise the previous 
            //transformations or rotations add up each frame and the it just gets messed 
            //up.
        }
        
        //System.out.println(parentMatrix.toString());
        
        return parentMatrix.mul(Matrices.transformationMatrix(position, rotation, scale)) 
        
        // The transformationMatrix() method above from the Matrices class is 
        //supposed to return a worldMatrix.
    }
    
    public Vector3f getPosition() {
        return position;
    }
    
    public void setPosition(Vector3f position) {
        this.position = position;
    }
    
    public Vector3f getRotation() {
        return rotation;
    }
    
    public void setRotation(Vector3f rotation) {
        this.rotation = rotation;
    }
    
    public Vector3f getScale() {
        return scale;
    }
    
    public void setScale(Vector3f scale) {
        this.scale = scale;
    }
    
    public Transform getParent() {
        return parent;
    }

    public void setParent(Transform parent) {
        this.parent = parent;
    }
    
    
}

Matrices工具类

public class Matrices {
    
    public static Matrix4f transformationMatrix(Vector3f pos, Vector3f rot, Vector3f scale) {
        Matrix4f result = new Matrix4f();
        
        result.identity();
        
        result.translate(pos.x, pos.y, pos.z);
        
        Quaternionf rotation = 
                new Quaternionf().
                identity().
                rotateX((float) Math.toRadians(rot.x)).
                rotateY((float) Math.toRadians(rot.y)).
                rotateZ((float) Math.toRadians(rot.z));
        
        /*result.rotate((float) Math.toRadians(rot.getX()), 1, 0, 0);
        result.rotate((float) Math.toRadians(rot.getY()), 0, 1, 0);
        result.rotate((float) Math.toRadians(rot.getZ()), 0, 0, 1);*/
        
        result.rotate(rotation);
        
        result.scale(scale.x, scale.y, scale.z);
        
        return result;
        
    }
    
    public static Matrix4f transformationMatrix(Vector3f pos, Quaternionf rotation, Vector3f scale) {
        Matrix4f result = new Matrix4f();
        
        result.identity();
        
        result.translate(pos.x, pos.y, pos.z);
        
        /*Quaternionf rotation = 
                new Quaternionf().
                identity().
                rotateX((float) Math.toRadians(rot.x)).
                rotateY((float) Math.toRadians(rot.y)).
                rotateZ((float) Math.toRadians(rot.z));*/
        
        /*result.rotate((float) Math.toRadians(rot.getX()), 1, 0, 0);
        result.rotate((float) Math.toRadians(rot.getY()), 0, 1, 0);
        result.rotate((float) Math.toRadians(rot.getZ()), 0, 0, 1);*/
        
        result.rotate(rotation);
        
        result.scale(scale.x, scale.y, scale.z);
        
        return result;
        
    }
    
    public static Matrix4f viewMatrix(Vector3f pos, Vector3f rot) {
        Matrix4f result = new Matrix4f();
        
        result.identity();
        
        Quaternionf rotation = 
                new Quaternionf().
                identity().
                rotateX((float) Math.toRadians(rot.x)).
                rotateY((float) Math.toRadians(rot.y)).
                rotateZ((float) Math.toRadians(rot.z));
        
        /*result.rotate((float) Math.toRadians(rot.getX()), new org.joml.Vector3f(1, 0, 0));
        result.rotate((float) Math.toRadians(rot.getY()), new org.joml.Vector3f(0, 1, 0));*/
        
        result.rotate(rotation);
        
        result.translate(-pos.x, -pos.y, -pos.z);
        
        return result;
    }
    
    public static Matrix4f viewMatrix(Vector3f pos, float pitch, float yaw, float roll) {
        Matrix4f result = new Matrix4f();
        
        result.identity();
        
        Quaternionf rotation = 
                new Quaternionf().
                identity().
                rotateX((float) Math.toRadians(pitch)).
                rotateY((float) Math.toRadians(yaw)).
                rotateZ((float) Math.toRadians(roll));
        
        /*result.rotate((float) Math.toRadians(rot.getX()), new org.joml.Vector3f(1, 0, 0));
        result.rotate((float) Math.toRadians(rot.getY()), new org.joml.Vector3f(0, 1, 0));*/
        
        result.rotate(rotation);
        
        result.translate(-pos.x, -pos.y, -pos.z);
        
        return result;
    }
    
    public static Matrix4f projectionMatrix(float FOV, float aspectRatio, 
                                            float NearPlaneDis, float FarPlaneDis) {
        Matrix4f result = new Matrix4f();
        
        result.identity();
        
        result.perspective(FOV, aspectRatio, NearPlaneDis, FarPlaneDis);
        
        /*float y_scale = (float) ((1f / Math.tan(Math.toRadians(FOV / 2f))) * aspectRatio);
        float x_scale = y_scale / aspectRatio;
        float frustum_length = FarPlaneDis - NearPlaneDis;
        
        result.m00(x_scale);
        result.m11(y_scale);
        result.m22(-((FarPlaneDis + NearPlaneDis) / frustum_length));
        result.m23(-1);
        result.m32(-((2 * NearPlaneDis * FarPlaneDis) / frustum_length));
        result.m33(0);*/
        
        return result;
    }
    
    public static Matrix4f translate(Vector3f vec, Matrix4f src, Matrix4f dest) {
        if (dest == null)
            dest = new Matrix4f();

        dest.m30(dest.m30() + src.m00() * vec.x + src.m10() * vec.y + src.m20() * vec.z);
        dest.m31(dest.m31() + src.m01() * vec.x + src.m11() * vec.y + src.m21() * vec.z);
        dest.m32(dest.m32() + src.m02() * vec.x + src.m12() * vec.y + src.m22() * vec.z);
        dest.m33(dest.m33() + src.m03() * vec.x + src.m13() * vec.y + src.m23() * vec.z);

        return dest;
    }
    
    public static Vector4f transform(Matrix4f left, Vector4f right, Vector4f dest) {
        if (dest == null)
            dest = new Vector4f(0, 0, 0, 0);

        float x = left.m00() * right.x + left.m10() * right.y + left.m20() * right.z + left.m30() * right.w;
        float y = left.m01() * right.x + left.m11() * right.y + left.m21() * right.z + left.m31() * right.w;
        float z = left.m02() * right.x + left.m12() * right.y + left.m22() * right.z + left.m32() * right.w;
        float w = left.m03() * right.x + left.m13() * right.y + left.m23() * right.z + left.m33() * right.w;

        dest.x = x;
        dest.y = y;
        dest.z = z;
        dest.w = w;

        return dest;
    }
    
    public static Vector3f scale(Vector3f vector, float scale) {

        vector.x *= scale;
        vector.y *= scale;
        vector.z *= scale;

        return vector;

    }
    
    public static float[] getAll(Matrix4f matrix) {
        float[] f = new float[16];
        return matrix.get(f);
    }
    
    public static float barryCentric(Vector3f p1, Vector3f p2, Vector3f p3, Vector2f pos) {
        float det = (p2.z - p3.z) * (p1.x - p3.x) + (p3.x - p2.x) * (p1.z - p3.z);
        float l1 = ((p2.z - p3.z) * (pos.x - p3.x) + (p3.x - p2.x) * (pos.y - p3.z)) / det;
        float l2 = ((p3.z - p1.z) * (pos.x - p3.x) + (p1.x - p3.x) * (pos.y - p3.z)) / det;
        float l3 = 1.0f - l1 - l2;
        return l1 * p1.y + l2 * p2.y + l3 * p3.y;
    }

}

Renderer类(对象初始化逻辑)

public class Renderer {
    
    private Model model;
    
    private Model model2;
    
    private GameObject root;
    
    private GameObject child;
    
    public Camera camera;
    
    private float time = 0;

    /*private float vertices[] = {
        0.5f, 0.5f, 0f,    //0 - top right
        0.5f, -0.5f, 0f,   //1 - bottom right
        -0.5f, -0.5f, 0f,  //2 - bottom left
        -0.5f, 0.5f, 0f    //3 - top left
    };
    
    private int indices[] = {
            0, 1, 3,   // first triangle
            3, 1, 2    // second triangle
    };*/
    
    public Renderer() {
        model = new Model("/backpack.obj");
        model2 = new Model("/backpack.obj");
        
        camera = new Camera(new Vector3f(0, 0, 0), new Vector3f(0, 0, 0));
        root = new GameObject();
        child = new GameObject();
        root.addComponent(camera);
        root.addComponent(model);
        child.addComponent(model2);
        root.addChild(child);
        
        model.getTransform().setPosition(new Vector3f(0, 0, 0));
        model2.getTransform().setPosition(new Vector3f(10, 0, 10));
        
    }
    
    public void render(Shader shader) {
        if(Input.isKeyDown(GLFW.GLFW_KEY_RIGHT)) {
            model.getTransform().setRotation(new Vector3f(0, time += 1f, 0));
        }
        //model2.getTransform().setRotation(new Vector3f(0, time += 0.01f, 0));
        shader.bind();
        root.input();
        root.update();
        root.render(shader, camera);
        shader.unbind();
    }
    
    public void cleanUp() {
        for(Mesh mesh: model.getMeshes()) {
            mesh.cleanUp();
        }
    }
}

问题根因与修复方案

根因

  1. 本地坐标被错误覆写:getTransformation()中setPosition(parentMatrix.transformPosition(position))逻辑直接将父节点变换后的世界坐标覆盖了子节点的本地坐标属性,导致每帧都会在上一帧的变换结果上再次叠加父节点变换,角度越大叠加的偏移量越高,公转速度就越快,绕满一周后偏移量重置,速度回落。
  2. 更新检查逻辑错误:requireTMUpdate()方法存在父节点时直接返回父节点的更新状态,完全忽略子节点自身的变换变化,会导致子节点修改自身属性后不会触发矩阵更新。
  3. 冗余逻辑干扰:临时修复添加的坐标覆写逻辑、无父节点时的矩阵重置逻辑,破坏了场景图变换的层级计算规则。

修复代码

1. 修正更新检查逻辑

public boolean requireTMUpdate() {
    boolean parentChanged = parent != null && parent.requireTMUpdate();
    boolean selfChanged = !position.equals(lastPosition) || !rotation.equals(lastRotation) || !scale.equals(lastScale);
    if (selfChanged) {
        lastPosition.set(position);
        lastRotation.set(rotation);
        lastScale.set(scale);
    }
    return parentChanged || selfChanged;
}

2. 重写变换矩阵计算逻辑

删除parentMatrix成员变量、坐标覆写逻辑、冗余的矩阵重置逻辑,直接按层级计算世界矩阵:

public Matrix4f getTransformation() {
    // 计算当前节点的本地变换矩阵
    Matrix4f localMatrix = Matrices.transformationMatrix(position, rotation, scale);
    if (parent != null) {
        // 父节点世界矩阵 * 子节点本地矩阵 = 子节点世界矩阵
        return parent.getTransformation().mul(localMatrix);
    }
    // 无父节点时本地矩阵就是世界矩阵
    return localMatrix;
}

修复后子节点的position/rotation/scale永远存储相对父节点的本地值,不会被计算逻辑修改,自然不会出现速度累加的问题。

内容的提问来源于stack exchange,提问作者Dstarred

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.10 13:57:00