1
\$\begingroup\$

Ok so I have a 2D OpenGL project that was originally a class assignment, and now I am using it to experiment with and to broaden my knowledge. Originally, I had been using the Normalized Devices Coordinates [-1 , 1], which creates headaches when dealing with the logic that handles sprites' position on the screen. Since then I have found out how to manipulate the camera so that all the action takes place inside of the coordinates [0, 1]. Programmatic Ibuprofen!

However, now I am getting a strange behavior that I believe has to do with how I am creating sprites. There is a parent/child relationship that is created along with the first drawn sprite and the one immediately subsequent, i.e. when the first sprite's position is updated all of the others are updated as if it were the first one.

screen shot

The picture above sorta shows what I am talking about. The background should scroll in the direction of the arrow (which it does), but the ship in the center should be stationary. However, it is scrolling downward with the background.

Now back to the method I am using to create the sprites: I have a singleton class that I use to load textures and handle the draw calls. My reasoning for this is that each object that has a sprite that will be drawn to the screen calls all of the exact same functions, the only difference between them is what image we want to draw.

Question

Is my reasoning and choice of implementation a good one? Or should I be using interfaces and a factory pattern? Also is the reason for the pair scrolling together have to do with the fact that Sprite is a static class?

Here are the three classes; Sprite, Space (scrolling background), and Ship.

Sprite class

public class Sprite {
    private final String vertexShaderCode =
            "uniform mat4 uMVPMatrix;" +                                                    // This matrix member variable provides a hook to manipulate
            "attribute vec4 aPosition;" +                                                   // the coordinates of the objects that use this vertex shader
            "attribute vec2 aTexCoordinate;" +                                              // Per-vertex texture coordinate information that is passed in.
            "varying vec2 vTexCoordinate;" +                                                // This will be passed into the fragment shader.
            "void main() {" +
            "  gl_Position = uMVPMatrix * aPosition;" +                                     // The matrix must be included as a modifier of gl_Position
            "  vTexCoordinate = aTexCoordinate;" +                                          // Pass through the texture coordinate.
            "}";                                                                            // Note that the uMVPMatrix factor *must be first* in order
                                                                                            // for the matrix multiplication product to be correct.
    private final String fragmentShaderCode =
            "precision mediump float;" +
            "uniform vec4 vColor;" +
            "varying vec2 vTexCoordinate;" +                                                // Interpolated texture coordinate per fragment.
            "uniform sampler2D uTexture;" +                                                 // The input texture.
            "void main() {" +
            "  gl_FragColor = " +                                                           // Final texture output
            "  texture2D(uTexture, vTexCoordinate);" +
            "}";

    private static FloatBuffer vertexBuffer;
    private static FloatBuffer textureBuffer;

    private int mProgram = -1;
    private int mTextureDataHandle;
    private int mMVPMatrix;
    private int mPositionHandle;
    private float[] mModelView = new float[16];

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

    static float squareCoords[] = {
             1.0f, 0.0f, 0.0f,                                                              // point0 : bottom right
             0.0f, 0.0f, 0.0f,                                                              // point1 : bottom left
             1.0f,  1.0f, 0.0f,                                                             // point2 : top right
             0.0f,  1.0f, 0.0f };                                                           // point3 : top left

    static float textureCoords[] = {
            1.0f, 1.0f,                                                                     // point0 : top right
            0.0f, 1.0f,                                                                     // point1 : top left
            1.0f, 0.0f,                                                                     // point2 : bottom right
            0.0f, 0.0f };                                                                   // point3 : bottom left

    private static Sprite reference = null;

    public static Sprite getInstance() {
        if(reference == null){
            reference = new Sprite();
        }
        return reference;
    }

    public int initialize(int resId) {

        ByteBuffer bb = ByteBuffer.allocateDirect(squareCoords.length * 4);                 // initialize vertex byte buffer for shape coordinates
        bb.order(ByteOrder.nativeOrder());                                                  // (# of coordinate values * 4 bytes per float)
        vertexBuffer = bb.asFloatBuffer();
        vertexBuffer.put(squareCoords);
        vertexBuffer.position(0);

        ByteBuffer tb = ByteBuffer.allocateDirect(textureCoords.length * 4);                // initialize texture byte buffer for texture coordinates
        tb.order(ByteOrder.nativeOrder());                                                  // (# of coordinate values * 4 bytes per float)
        textureBuffer = tb.asFloatBuffer();
        textureBuffer.put(textureCoords);
        textureBuffer.position(0);

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

        mProgram = ShaderCompiler.createProgram(                                            // create OpenGL program executables
                vertexShader,
                fragmentShader,
                new String[]
                        {"aPosition", "aTexCoordinate"});

        return TextureLoader.loadTexture(resId);                                             // load the texture
    }

    public void preDraw(int resId, float[] viewMatrix){
        mTextureDataHandle = resId;
        mModelView = viewMatrix;
        GLES20.glUseProgram(mProgram);                                                      // Add program to OpenGL environment

                                                                                            // Set program handles for drawing.
        mMVPMatrix = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");                   // get handle to shape's transformation matrix
        mPositionHandle = GLES20.glGetAttribLocation(mProgram, "aPosition");                // get handle to vertex shader's vPosition member
        int mTextureUniform = GLES20.glGetUniformLocation(mProgram, "uTexture");            // get handle to fragment shader's uTexture member
        int mTextureCoordinate = GLES20.glGetAttribLocation(mProgram, "aTexCoordinate");    // get handle to vertex shader's aTexCoordinate member

        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);                                         // Set the active texture unit to texture unit 0.
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureDataHandle);                     // Bind the texture to this unit.
        GLES20.glUniform1i(mTextureUniform, 0);                                             // Tell the texture uniform sampler to use this texture
                                                                                            // in the shader by binding to texture unit 0.

        GLES20.glEnableVertexAttribArray(mPositionHandle);                                  // Enable a handle to the triangle vertices
        int vertexStride = COORDS_PER_VERTEX * 4;                                           // 4 bytes per vertex
        GLES20.glVertexAttribPointer(                                                       // Pass in the triangle coordinate data
                mPositionHandle, COORDS_PER_VERTEX,
                GLES20.GL_FLOAT, false,
                vertexStride, vertexBuffer);

        GLES20.glEnableVertexAttribArray(mTextureCoordinate);                               // Enable a handle to the texture vertices
        int mTextureCoordinateDataSize = 2;                                                 // How many values are in each Coordinate
        GLES20.glVertexAttribPointer(                                                       // Pass in the texture coordinate information
                mTextureCoordinate, mTextureCoordinateDataSize,
                GLES20.GL_FLOAT, false,
                0, textureBuffer);

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


    }


    public void move(float x, float y) {
        Matrix.translateM(mModelView, 0, x, y, 0f);                                        // Move the sprite in X and Y directions
    }

    public void rotate(float theta) {
        Matrix.rotateM(mModelView, 0, theta, 0f, 0f, 1.0f);                                // Rotate the sprite CW or CCW
    }

    public void scale(float width, float height) {
        Matrix.scaleM(mModelView, 0, width, height, 0f);                                   // Shrink or Expand the sprite
    }

    public void draw(){
        GLES20.glUniformMatrix4fv(mMVPMatrix, 1, false, mModelView, 0);                    // Apply the projection and view transformation
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP,0,4);                                 // Start at point 0 and draw 4 vertices
        GLES20.glDisableVertexAttribArray(mPositionHandle);                                // Disable vertex array
    } 

Space class

public class Space {
    private int mTextureDataHandle;
    private float[] mMVPMatrix = new float[16];             // mMVPMatrix is an abbreviation for "Model View Projection Matrix"

    private float mCenterX;
    private float mCenterY;
    private float mWidth      = 1.0f;
    private float mHeight     = 2.0f;
    private float mRotation   = 0.0f;

    private World mWorld;
    private Sprite mSprite = Sprite.getInstance();

    public Space(World world){
        mWorld = world;
        initialize();
    }

    public void initialize() {
        mTextureDataHandle =
            mSprite.initialize(R.drawable.space);           // load the texture
    }

    public void update(float[] viewMatrix){
        mMVPMatrix = viewMatrix;

        if( mCenterY <  -1.0f) {                            // If the texture has completely scrolled of screen
             mCenterY = 0.0f + ( mCenterY + 1.0f);          // reset the texture to original position
        }
        mCenterY -= 1.0f * mWorld.getDeltaTime();           // Move the Y-value of each vertex by this amount
    }

    public void draw() {
        mSprite.preDraw(mTextureDataHandle, mMVPMatrix);

        mSprite.move(mCenterX, mCenterY);
        mSprite.rotate(mRotation);
        mSprite.scale(mWidth, mHeight);

        mSprite.draw();
    }

}

Ship class

public class Ship {
    private int mTextureDataHandle;
    private float[] mMVPMatrix = new float[16];                     // mMVPMatrix is an abbreviation for "Model View Projection Matrix"
    private float mCenterX  =  0.5f;
    private float mCenterY  =  0.48f;
    private float mWidth    =  0.08f;
    private float mHeight   =  0.1f;
    private float mRotation =  0.0f;
    public  float mDeltaTime;

    private Sprite mSprite = Sprite.getInstance();
    private Collider hitbox;

    public Ship() {
        hitbox = new Collider(mCenterX,mCenterY,mWidth, mHeight);
        initialize();
    }

    public void initialize() {
      mTextureDataHandle = mSprite.initialize(R.drawable.ship);
    }

    public void update(float shipX, float shipY, float deltatime, float[] viewMatrix){
        mDeltaTime = deltatime;
        mMVPMatrix = viewMatrix;

        if(shipX > 0) {
            if(shipX > mCenterX && mCenterX < 0.6){
                mCenterX += ((shipX))*deltatime;
            }
        } else {
            if(shipX < mCenterX && mCenterX > -0.6) {
                mCenterX -= ((-shipX))*deltatime;
            }
        }

        if(shipY > 0) {
            if(shipY > mCenterY && mCenterY < 0.88){
                mCenterY += ((shipY))*deltatime;
            }
        } else {
            if(shipY < mCenterY && mCenterY > -0.88) {
                mCenterY -= ((-shipY))*deltatime;
            }
        }
        hitbox.update(mCenterX,mCenterY,mWidth, mHeight);
    }

    public void draw() {
        mSprite.preDraw(mTextureDataHandle, mMVPMatrix);

        mSprite.move(mCenterX, mCenterY);
        mSprite.rotate(mRotation);
        mSprite.scale(mWidth, mHeight);

        mSprite.draw();
    }

    public float getCenterX() { return mCenterX; }

    public float getCenterY() { return mCenterY; }

    public float getWidth() { return mWidth; }

    public float getHeight() { return mHeight; }

    public Collider getCollider() {
        return hitbox;
    }

Thanks for any advice / guidance! If you need to know more just comment and I will be happy to oblige.

\$\endgroup\$
1
+50
\$\begingroup\$

Here's what I think is happening: the matrices in your static singleton sprite class (say that three times fast) -- specifically the model-view matrix -- is being modified on drawing every sprite without being reset to how it was before the modification. As such, any transformation you perform for one object is also applied to any subsequently drawn object in that render cycle.

What you might want to do is have a matrix stack, like in legacy OpenGL. Before you transform the model-view matrix, you copy it and push it to a stack. After you've drawn the sprite, you pop it from the stack to revert all transformations, and then move on to the next sprite.

\$\endgroup\$
  • 1
    \$\begingroup\$ Thank you for taking a stab at this! would you be so kind as write some type of sudo code? It's just to get an idea of how you vision it to work. Not that I think of it I believe I tired something similar. I added a method to the sprite class that would save the MVP into a field. and then multiplied it by the mModelview Matrix that into a final modified matrix that I then fed to the final draw call. Something didn't quite work right when I tried it but perhaps your suggest will be different. I believe you are suggesting I use an array list? \$\endgroup\$ – chewbapoclypse May 28 '16 at 2:44
  • 1
    \$\begingroup\$ Now that I look at your code again, I think it should still work since you do overwrite the matrix every time, and the scene graph is only one layer deep pretty much, so it could just be some tiny bug. Anyway, to answer your question, Java has a built in class for stacks (same base class as ArrayList). I did something similar in Java before here if you'd like to take a look: github.com/yousefamar/amar-engine/blob/master/src/com/… Warning: It's two years old though, and knowing what I know now, I'd do things different generally, but the concept is the same. \$\endgroup\$ – Yousef Amar May 28 '16 at 9:32
  • \$\begingroup\$ @chewbapoclypse let me know if these have solved the problem. If it's still there I'll work up some more rep and pop another bounty on it for you. \$\endgroup\$ – Peter David Carter May 28 '16 at 9:53

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.