OpenGL ES 2.0 Android - ошибка вращения куба

1

Я реализовал визуализацию цветного 3d-куба с помощью OpenGL ES 2.0, который используется для приложения для Android. Моя цель: куб должен реагировать на события смахивания (влево, вправо, вверх, вниз), затем куб должен вращаться в соответствующем направлении.

Вращение:

  • останавливается, если (current_angle% 90 == 0) → так что вы проводите от одного лица к другому
  • только одно вращение за раз (и только вокруг x- или оси Y)
  • также следует выполнять пошагово (например, 5 градусов), чтобы это не делалось мгновенно → может быть видно пользователю

Мой код:

public class Cube20 {

    private volatile int angleX;
    private volatile int angleY;

    private volatile Cube.RotateDirection rotateDirection;

    public Cube.RotateDirection getRotateDirection() {
        return rotateDirection;
    }


    public void setRotation(Cube.RotateDirection rotateDirection) {
        this.rotateDirection = Cube.RotateDirection.getDirectionForID(rotateDirection.getId());
    }

    public int getAngleX() {
        return angleX;
    }

    public void setAngleX(int angleX) {
        this.angleX = angleX;
    }

    public int getAngleY() {
        return angleY;
    }

    public void setAngleY(int angleY) {
        this.angleY = angleY;
    }

    private final String vertexShaderCode =
            // This matrix member variable provides a hook to manipulate
            // the coordinates of the objects that use this vertex shader
            "uniform mat4 uMVPMatrix;" +
                    "attribute vec4 vPosition;" +
                    "void main() {" +
                    // The matrix must be included as a modifier of gl_Position.
                    // Note that the uMVPMatrix factor *must be first* in order
                    // for the matrix multiplication product to be correct.
                    "  gl_Position = uMVPMatrix * vPosition;" +
                    "}";

    private final String fragmentShaderCode =
            "precision mediump float;" +
                    "uniform vec4 vColor;" +
                    "void main() {" +
                    "  gl_FragColor = vColor;" +
                    "}";

    private final FloatBuffer vertexBuffer;
    //private final ShortBuffer drawListBuffer;
    private final int mProgram;
    private final ShortBuffer indexBuffer;
    private int mPositionHandle;
    private int mColorHandle;
    private int mMVPMatrixHandle;

    // number of coordinates per vertex in this array
    static final int COORDS_PER_VERTEX = 3;

    private float[] vertices = {  // Vertices of the 6 faces
            // FRONT
            -1.0f, -1.0f, 1.0f,  // 0. left-bottom-front (0)
            1.0f, -1.0f, 1.0f,  // 1. right-bottom-front
            -1.0f, 1.0f, 1.0f,  // 2. left-top-front
            1.0f, 1.0f, 1.0f,  // 3. right-top-front
            // BACK
            1.0f, -1.0f, -1.0f,  // 6. right-bottom-back (4)
            -1.0f, -1.0f, -1.0f,  // 4. left-bottom-back
            1.0f, 1.0f, -1.0f,  // 7. right-top-back
            -1.0f, 1.0f, -1.0f,  // 5. left-top-back
            // LEFT
            -1.0f, -1.0f, -1.0f,  // 4. left-bottom-back (8)
            -1.0f, -1.0f, 1.0f,  // 0. left-bottom-front
            -1.0f, 1.0f, -1.0f,  // 5. left-top-back
            -1.0f, 1.0f, 1.0f,  // 2. left-top-front
            // RIGHT
            1.0f, -1.0f, 1.0f,  // 1. right-bottom-front (12)
            1.0f, -1.0f, -1.0f,  // 6. right-bottom-back
            1.0f, 1.0f, 1.0f,  // 3. right-top-front
            1.0f, 1.0f, -1.0f,  // 7. right-top-back
            // TOP
            -1.0f, 1.0f, 1.0f,  // 2. left-top-front
            1.0f, 1.0f, 1.0f,  // 3. right-top-front
            -1.0f, 1.0f, -1.0f,  // 5. left-top-back
            1.0f, 1.0f, -1.0f,  // 7. right-top-back
            // BOTTOM
            -1.0f, -1.0f, -1.0f,  // 4. left-bottom-back
            1.0f, -1.0f, -1.0f,  // 6. right-bottom-back
            -1.0f, -1.0f, 1.0f,  // 0. left-bottom-front
            1.0f, -1.0f, 1.0f   // 1. right-bottom-front
    };

    private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex

    private float[][] colors = {  // Colors of the 6 faces
            {1.0f, 0.5f, 0.0f, 1.0f},  // 0. orange
            {1.0f, 0.0f, 1.0f, 1.0f},  // 1. violet
            {0.0f, 1.0f, 0.0f, 1.0f},  // 2. green
            {0.0f, 0.0f, 1.0f, 1.0f},  // 3. blue
            {1.0f, 0.0f, 0.0f, 1.0f},  // 4. red
            {1.0f, 1.0f, 0.0f, 1.0f}   // 5. yellow
    };

    short[] indices = {
            0, 1, 2, 2, 1, 3, // FRONT
            4, 5, 6, 6, 5, 7, // BACK
            8, 9, 10, 10, 9, 11, // LEFT
            12, 13, 14, 14, 13, 15, // RIGHT
            16, 17, 18, 18, 17, 19, // TOP
            20, 21, 22, 22, 21, 23, // BOTTOM

    };
    private int numFaces = 6;

    /**
     * Sets up the drawing object data for use in an OpenGL ES context.
     */
    public Cube20() {
        // initialize vertex byte buffer for shape coordinates
        ByteBuffer bb = ByteBuffer.allocateDirect(
                // (# of coordinate values * 4 bytes per float)
                vertices.length * 4);
        bb.order(ByteOrder.nativeOrder());
        vertexBuffer = bb.asFloatBuffer();
        vertexBuffer.put(vertices);
        vertexBuffer.position(0);

        // initialize byte buffer for the draw list
        indexBuffer = ByteBuffer.allocateDirect(indices.length * 2).order(ByteOrder.nativeOrder()).asShortBuffer();
        indexBuffer.put(indices).position(0);

        // prepare shaders and OpenGL program
        int vertexShader = RenderUtils.loadShader(
                GLES20.GL_VERTEX_SHADER,
                vertexShaderCode);
        int fragmentShader = RenderUtils.loadShader(
                GLES20.GL_FRAGMENT_SHADER,
                fragmentShaderCode);

        mProgram = GLES20.glCreateProgram();             // create empty OpenGL Program
        GLES20.glAttachShader(mProgram, vertexShader);   // add the vertex shader to program
        GLES20.glAttachShader(mProgram, fragmentShader); // add the fragment shader to program
        GLES20.glLinkProgram(mProgram);                  // create OpenGL program executables

        this.rotateDirection = Cube.RotateDirection.NONE;
    }

    /**
     * Encapsulates the OpenGL ES instructions for drawing this shape.
     *
     * @param mvpMatrix - The Model View Project matrix in which to draw
     *                  this shape.
     */
    public void draw(float[] mvpMatrix) {
        // Add program to OpenGL environment
        GLES20.glUseProgram(mProgram);

        GLES20.glFrontFace(GLES20.GL_CCW);
        GLES20.glEnable(GLES20.GL_CULL_FACE);
        GLES20.glCullFace(GLES20.GL_BACK);

        // scale
        float scale_matrix[] = new float[16];
        Matrix.setIdentityM(scale_matrix, 0);
        Matrix.scaleM(scale_matrix, 0, 0.5f, 0.5f, 1);
        Matrix.multiplyMM(mvpMatrix, 0, scale_matrix, 0, mvpMatrix, 0);

        // get handle to vertex shader vPosition member
        mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");

        // Enable a handle to the triangle vertices
        GLES20.glEnableVertexAttribArray(mPositionHandle);

        // Prepare the triangle coordinate data
        GLES20.glVertexAttribPointer(
                mPositionHandle, COORDS_PER_VERTEX,
                GLES20.GL_FLOAT, false,
                vertexStride, vertexBuffer);

        // get handle to fragment shader vColor member
        mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");

        // Set color for drawing the triangle
        //GLES20.glUniform4fv(mColorHandle, 1, color, 0);

        // get handle to shape transformation matrix
        mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
        RenderUtils.checkGlError("glGetUniformLocation");

        // Apply the projection and view transformation
        GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
        RenderUtils.checkGlError("glUniformMatrix4fv");

        // Render all the faces
        for (int face = 0; face < numFaces; face++) {
            // Set the color for each of the faces
            GLES20.glUniform4fv(mColorHandle, 1, colors[face], 0);
            indexBuffer.position(face * 6);
            GLES20.glDrawElements(GLES20.GL_TRIANGLES, 6, GLES20.GL_UNSIGNED_SHORT, indexBuffer);
        }

        // Disable vertex array
        GLES20.glDisableVertexAttribArray(mPositionHandle);
        GLES20.glDisable(GLES20.GL_CULL_FACE);
    }
}

public class MyGLSurfaceView extends GLSurfaceView {
    private volatile MyGLRenderer myGLRenderer;

    public MyGLSurfaceView(Context context) {
        super(context);

        // Create an OpenGL ES 2.0 context
        setEGLContextClientVersion(2);

        myGLRenderer = new MyGLRenderer(context);
        setRenderer(myGLRenderer); // Use a custom renderer
        setOnTouchListener(new OnSwipeListener(context));

        setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
    }

    class OnSwipeListener implements View.OnTouchListener {

        private final GestureDetector gestureDetector;

        public OnSwipeListener(Context context) {
            this.gestureDetector = new GestureDetector(context, new OnFlingListener());
        }

        @Override
        public boolean onTouch(View v, MotionEvent event) {
            return this.gestureDetector.onTouchEvent(event);
        }

        private class OnFlingListener extends GestureDetector.SimpleOnGestureListener {
            private final Object LOCK = new Object();

            @Override
            public boolean onDown(MotionEvent e) {
                return true;
            }

            /**
             * @return true if the event is consumed, else false
             */
            @Override
            public boolean onFling(MotionEvent down, MotionEvent up, float velocityX, float velocityY) {
                //super.onFling(down, up, velocityX, velocityY);

                float distanceX = up.getX() - down.getX();
                float distanceY = up.getY() - down.getY();

                final Cube20 cube = myGLRenderer.getCube();
                if (!cube.getRotateDirection().equals(Cube.RotateDirection.NONE)) {
                    return false;
                }

                if (Math.abs(distanceX) > Math.abs(distanceY)) {
                    if (distanceX > 0) {
                        // RIGHT
                        cube.setRotation(Cube.RotateDirection.RIGHT);
                    } else {
                        // LEFT
                        cube.setRotation(Cube.RotateDirection.LEFT);
                    }
                } else {
                    if (distanceY < 0) {
                        // TOP
                        cube.setRotation(Cube.RotateDirection.UP);
                    } else {
                        // DOWN
                        cube.setRotation(Cube.RotateDirection.DOWN);
                    }
                }
                //requestRender();
                return true;
            }
        }
    }
}

public class MyGLRenderer implements GLSurfaceView.Renderer {
    Context context;   // Application context
    private volatile int rotationAngle;
    private volatile float rotateX;
    private volatile float rotateY;
    private boolean firstRotation = true;
    private Cube20 cube;

    // mMVPMatrix is an abbreviation for "Model View Projection Matrix"
    private final float[] mMVPMatrix = new float[16];
    private final float[] mProjectionMatrix = new float[16];
    private final float[] mViewMatrix = new float[16];

    // Constructor with global application context
    public MyGLRenderer(Context context) {
        this.context = context;
    }


    public float getRotateX() {
        return rotateX;
    }

    public void setRotateX(float rotateX) {
        this.rotateX = rotateX;
    }

    public float getRotateY() {
        return rotateY;
    }

    public void setRotateY(float rotateY) {
        this.rotateY = rotateY;
    }

    public float getAngle() {
        return rotationAngle;
    }

    public void setAngle(int angle) {
        rotationAngle = angle;
    }


    // Call back when the surface is first created or re-created
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        // Set the background frame color
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        this.cube = new Cube20();
    }

    // Call back to draw the current frame.
    @Override
    public void onDrawFrame(GL10 gl) {
        // Redraw background color
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);

        // Set the camera position (View matrix)
        Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -1, 0f, 0f, 0f, 0f, 1.0f, 0.0f);

        // Calculate the projection and view transformation
        Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);

        // zoom out a bit
        Matrix.translateM(mMVPMatrix, 0, 0, 0, 4.5f);


        int angleOffset = 5;
        // update the angles for the x and y rotation
        if (cube.getRotateDirection().equals(Cube.RotateDirection.LEFT)) {
            cube.setAngleY(cube.getAngleY() - angleOffset);
        } else if (cube.getRotateDirection().equals(Cube.RotateDirection.RIGHT)) {
            cube.setAngleY(cube.getAngleY() + angleOffset);
        } else if (cube.getRotateDirection().equals(Cube.RotateDirection.UP)) {
            cube.setAngleX(cube.getAngleX() + angleOffset);
        } else if (cube.getRotateDirection().equals(Cube.RotateDirection.DOWN)) {
            cube.setAngleX(cube.getAngleX() - angleOffset);
        }
        firstRotation = false;

        // rotate and draw
        rotate();
        cube.draw(mMVPMatrix);


        // test if rotation should be stopped (lock in each 90° step)
        if (cube.getRotateDirection().equals(Cube.RotateDirection.LEFT) ||
                cube.getRotateDirection().equals(Cube.RotateDirection.RIGHT)) {
            if (!firstRotation && cube.getAngleY() % 90 == 0) {
                cube.setRotation(Cube.RotateDirection.NONE);
            }
        }
        if (cube.getRotateDirection().equals(Cube.RotateDirection.UP) ||
                cube.getRotateDirection().equals(Cube.RotateDirection.DOWN)) {
            if (!firstRotation && cube.getAngleX() % 90 == 0) {
                cube.setRotation(Cube.RotateDirection.NONE);
            }
        }

        Log.i("MyGLRENDER~ ", cube.getRotateDirection().toString());
    }

    private void rotate() {
        float[] rotationMatrix = new float[16];
        Matrix.setIdentityM(rotationMatrix, 0);

        // rotate in x and y direction, apply that to the intermediate matrix
        Matrix.rotateM(rotationMatrix, 0, cube.getAngleX(), 1, 0, 0);
        Matrix.rotateM(rotationMatrix, 0, cube.getAngleY(), 0, 1, 0);
        Matrix.multiplyMM(mMVPMatrix, 0, mMVPMatrix, 0, rotationMatrix, 0);
    }

    // Call back after onSurfaceCreated() or whenever the window size changes
    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        GLES20.glViewport(0, 0, width, height);
        float ratio = (float) width / height;

        // this projection matrix is applied to object coordinates
        // in the onDrawFrame() method
        Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
    }

    public Cube20 getCube() {
        return cube;
    }
}

public class MyGLActivity extends Activity {

    private GLSurfaceView glView;   // Use GLSurfaceView

    // Call back when the activity is started, to initialize the view
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        glView = new MyGLSurfaceView(this); // Allocate a GLSurfaceView
        this.setContentView(glView); // This activity sets to GLSurfaceView
    }

    // Call back when the activity is going into the background
    @Override
    protected void onPause() {
        super.onPause();
        glView.onPause();
    }

    // Call back after onPause()
    @Override
    protected void onResume() {
        super.onResume();
        glView.onResume();
    }
}

Поведение, которое я ожидаю:

  • успешная реакция смахивания и вращение вокруг оси x-
    Изображение 174551

  • успешная реакция смахивания и вращение вокруг оси y
    Изображение 174551

При такой настройке я сталкиваюсь со следующими проблемами:

  1. Иногда вращение делается вокруг оси Z
    Изображение 174551
    Я никогда не реализовывал вращение вокруг оси z для куба, я не уверен, почему это вращение сделано. Я мог только предположить, что, возможно, существует проблема с многопоточностью между слушателем смахивания в MyGLSurfaceView и MyGLRenderer (который содержит ссылку на куб).

Вращение выполняется в MyGLRenderer.rotate. Куб использует отдельные углы (x/y), а также есть свойство, для которого в данный момент выполняется вращение (ВЛЕВО, ВПРАВО, ВВЕРХ, ВНИЗ, НЕТ), которое обновляется в OnSwipeListener.

  1. Также, если произошла ошибка с z-вращением (1.), вращение вокруг оси x или y выполняется в неправильном направлении (влево/вправо, вверх/вниз поменяно местами)

Я предполагаю, что либо углы или RotateDirection не обновляются правильно. Приложение протестировано на OnePlus 3T @Android 8.0, IDE - Android Studio 3.2.1.

Теги:
opengl-es
opengl-es-2.0

1 ответ

0
Лучший ответ

Если вы повернули куб на 90 ° вокруг оси X, то, конечно, вращение вокруг оси Y куба - это вращение вокруг оси Z в мире. Вы должны вращаться вокруг оси Y мира, а не вокруг оси Y куба.
Для этого необходимо сохранить составные вращения куба в матрице вращения и применить новое вращение к этой матрице.

Создайте член для матрицы вращения:

private final float[] mRotationMatrix = new float[16];

Инициализируйте его с помощью идентификационной матрицы:

Matrix.setIdentityM(mRotationMatrix, 0);

В методе rotate необходимо применить текущую анимацию к матрице вращения. Порядок должен быть animationMatrix * mRotationMatrix. Умножение матриц не является коммутативным. Если вы не соблюдаете порядок умножения, вы получите тот же результат, что и раньше, и вращение будет происходить вокруг оси куба, а не вокруг оси мира.

private void rotate() {
    float[] animationMatrix = new float[16];
    Matrix.setIdentityM(animationMatrix, 0);

    // rotate in x and y direction, apply that to the intermediate matrix
    Matrix.rotateM(animationMatrix, 0, cube.getAngleX(), 1, 0, 0);
    Matrix.rotateM(animationMatrix, 0, cube.getAngleY(), 0, 1, 0);

    // concatenate the animation and the rotation matrix; the order is important
    Matrix.multiplyMM(animationMatrix, 0, animationMatrix, 0, mRotationMatrix, 0); 

    Matrix.multiplyMM(mMVPMatrix, 0, mMVPMatrix, 0, animationMatrix, 0);
}

Когда анимация достигает 90 °, вам нужно изменить матрицу вращения и сбросить угол поворота:

if (cube.getRotateDirection().equals(Cube.RotateDirection.LEFT) ||
    cube.getRotateDirection().equals(Cube.RotateDirection.RIGHT)) {
        if (!firstRotation && cube.getAngleY() % 90 == 0) {

            float[] newRotationMatrix = new float[16];
            Matrix.setIdentityM(newRotationMatrix, 0);
            Matrix.rotateM(newRotationMatrix, 0, cube.getAngleY(), 0, 1, 0);

            // concatenate the new 90 rotation to the rotation matrix
            Matrix.multiplyMM(mRotationMatrix, 0, newRotationMatrix, 0, mRotationMatrix, 0);

            // reset the angle
            cube.setAngleY(0);
            cube.setRotation(Cube.RotateDirection.NONE);
        }
    }
    if (cube.getRotateDirection().equals(Cube.RotateDirection.UP) ||
        cube.getRotateDirection().equals(Cube.RotateDirection.DOWN)) {
        if (!firstRotation && cube.getAngleX() % 90 == 0) {

            float[] newRotationMatrix = new float[16];
            Matrix.setIdentityM(newRotationMatrix, 0);
            Matrix.rotateM(newRotationMatrix, 0, cube.getAngleX(), 1, 0, 0);

            // concatenate the new 90 rotation to the rotation matrix
            Matrix.multiplyMM(mRotationMatrix, 0, newRotationMatrix, 0, mRotationMatrix, 0);

            // reset the angle
            cube.setAngleX(0);
            cube.setRotation(Cube.RotateDirection.NONE);
        }
    }
  • 0
    Спасибо, теперь работает как шарм! В общем, у меня уже была эта идея, и я хотел вращаться вокруг оси мира. Но тем временем я фактически вращал всю систему координат мира и неправильно вращал куб, верно?
  • 0
    @MatthiasTietz Проблема в том, что ваш исходный код выполняет rotateX * rotateY . Это будет работать, если первое вращение происходит вокруг оси Y, а второе - вокруг оси X, но оно не будет работать, если первое вращение будет вокруг оси X, а второе - вокруг оси Y. Тогда вам нужно будет выполнить rotateY * rotateX . Если вы хотите 1 вращение вокруг X, чем вокруг Y, и 3-е снова вокруг X, тогда вам придется выполнить rotateX2*rotateY*rotateX1 . Это то, что делает код ответа, потому что все предыдущие повороты собираются в mRotationMatrix .

Ещё вопросы

Сообщество Overcoder
Наверх
Меню