0
\$\begingroup\$

Preface: Linux (Ubuntu 16.04 Gnome), C++11, SDL2, GLEW 2.0, OpenGL 4.5

Hello ladies/gents,

My conundrum is this: How do I handle draw a repeated texture (sprite) as a single object, at various dimensions and rotations, without manually draw each tile?

I posted a similar question earlier about rotating a row of textures, to which the user Lasse provided insight on utilizing matrices. After two days of reading up on (and playing with) glm::translate and glm::rotate, I have not been able to rotate a row of sprites in place (as they always scale off of the 0,0 origin in center of screen, regardless of translations, producing increasingly less desirable motions). Additionally, rotating one seems to be rotating all sprites, but I am suspect that this is related to how I am drawing the sprites in batches. My understanding of uniforms is still, admittedly, embarrassing. I can provide snippets of the process involving the uniforms and the actual drawing, if it would help.

Is it possible to utilize uniforms and glm::translate/glm::rotate to rotate specific objects (such as level terrain pieces on a platformer)? I have found multiple examples through tutorials online, but almost all utilize a single object, with normalized device coordinates, and never with any type of moving/scaling projection matrix (read: camera).

I had a thought about potentially "drawing" the object and creating a 2D texture from the "drawn" object, so it could simply be titled as a single object with some shenanigans against the vertices. This is another, potential approach but I am unsure how useful (or treacherous) it is to implement.

If it's not abundantly clear by this point, my OpenGL understanding is woefully unequal to what I am attempting, but learning occurs best for me when I'm buried, lost, and overwhelmed in a new topic. I greatly appreciate any and all incoming feedback, advice, and information! :)


EDIT: Thanks to MalphasWats for pointing out something, I'm using glm::translate and glm::rotate, not glTranslate and glRotate. From reading so many dated tutorials, I had a mental 'slip', so to speak.

From MalphasWats' other comment regarding uniforms applying to the entire buffer waiting to be drawn, that does help explain some "oddities". It gives me some ideas on how to approach it as simply changing the rotation for each terrain object (across dozens of terrain objects) is simply not feasible as it would result in many extra draw calls for one or a few objects at at time.

The current 'workflow' (at least the relevant bits) for my level editor looks something like this:

  • Camera is created and initialized on screen creation (at 1440x900 with 32x scale for no particular reason)
  • update() function is called (GUI updates to position, size, etc; FPS calculation; updates to objects movement)
  • draw() function is called. Here is where my repeating texture drawing (and issue) occurs. During the draw function, one of three sprite drawing actions happens (normal sprite, rotated single-sprite, repeating-sprite).
  • draw() function is wrapped up, with all spritebatches drawn.

Camera step:

void Camera2D::init(int scrWidth, int scrHeight)
{
    m_screenWidth = scrWidth;
    m_screenHeight = scrHeight;
    m_orthoMatrix = glm::ortho(0.0f, static_cast<float>(m_screenWidth), 0.0f, static_cast<float>(m_screenHeight)); // building orthographic matrix
}

void Camera2D::update()
{
    if(m_hasChanged)
    {
        // camera translation
        glm::vec3 translate(-m_position.x + m_screenWidth/2, -m_position.y + m_screenHeight/2, 0.0f);
        m_cameraMatrix = glm::translate(m_orthoMatrix, translate);
        m_unscaledCameraMatrix = m_cameraMatrix;
        // camera scale
        glm::vec3 scale(m_scale, m_scale, 0.0f);
        m_cameraMatrix = glm::scale(glm::mat4(1.0f), scale) * m_cameraMatrix;
        m_hasChanged = false;
    }
}

Omitting update() function as it's not relevant.

draw() function:

void LevelEditScreen::draw()
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glClearColor(0.45f, 0.5f, 0.55f, 1.0f);

    m_textureProgram.use();

    GLint textureUniform = m_textureProgram.getUniformLocation("mySampler");
    glUniform1i(textureUniform, 0);
    glActiveTexture(GL_TEXTURE0);

    // camera matrix
    glm::mat4 projectMatrix = m_camera.getCameraMatrix(); // mat4 ortho matrix (0, 1440, 0, 900), scaled by 32
    GLint pUniform = m_textureProgram.getUniformLocation("translation");
    glUniformMatrix4fv(pUniform, 1, GL_FALSE, &projectMatrix[0][0]);

    // handle sprites
    m_spriteBatch->begin();
    m_spriteBatch->draw(glm::vec4(0.0f, 0.0f, 1.0f, 1.0f), m_cursor.uvRect, m_cursor.textureID, 0.0f, GameEngine101::COLOR_RED); // debug box, 1 "tile", red, at center
    glm::vec4 sizeBounds = glm::vec4(0.0f);
    if(!rbNoRepeat->isTicked())
    {
        // this step repeats the texture, by drawing numerous sprites together, in an instant burst
        // NOTE: The associated drawing function is somewhat WIP, so it's messy, inefficient, and barely functional
        sizeBounds = glm::vec4( m_cursor.destRect.x, m_cursor.destRect.y, m_cursor.repeatSize[0], m_cursor.repeatSize[1] );
        m_spriteBatch->drawRepeat(m_cursor.destRect, m_cursor.uvRect, sizeBounds, m_cursor.textureID, 0.0f, m_cursor.color, m_cursor.rotation, projectMatrix, m_camera.getScale());
    }
    else
    {
        // draw a normal sprite that can be batch drawn
        sizeBounds = glm::vec4( m_cursor.destRect.x, m_cursor.destRect.y, m_cursor.destRect.z, m_cursor.destRect.w );
        m_spriteBatch->draw(m_cursor.destRect, m_cursor.uvRect, m_cursor.textureID, 0.0f, m_cursor.color, m_cursor.rotation);
    }

    // tidy up sprite batches, generate batches
    m_spriteBatch->end();

    // actually draw batches
    m_spriteBatch->renderBatch();

    m_textureProgram.unuse();

    // debug rendering
    m_debugRenderer->drawBox(sizeBounds, m_cursor.color, m_cursor.rotation);
    m_debugRenderer->end();
    m_debugRenderer->render(projectMatrix, 2.0f);

    m_gui->draw();
}

Finally, the basic Glyph constructor and the drawRepeat() function:

Glyph::Glyph(const glm::vec4& destRect, const glm::vec4& uvRect, GLuint Texture, float Depth, const ColorRGBA8& color) : texture(Texture), depth(Depth)
{
    // top left
    topLeft.setPosition(destRect.x, destRect.y + destRect.w);
    topLeft.setUV(uvRect.x, uvRect.y + uvRect.w);
    topLeft.color = color;
    // bottom left
    bottomLeft.setPosition(destRect.x, destRect.y);
    bottomLeft.setUV(uvRect.x, uvRect.y);
    bottomLeft.color = color;
    // top right
    topRight.setPosition(destRect.x + destRect.z, destRect.y + destRect.w);
    topRight.setUV(uvRect.x + uvRect.z, uvRect.y + uvRect.w);
    topRight.color = color;
    // bottom right
    bottomRight.setPosition(destRect.x + destRect.z, destRect.y);
    bottomRight.setUV(uvRect.x + uvRect.z, uvRect.y);
    bottomRight.color = color;
}

void SpriteBatch::drawRepeat(const glm::vec4& glyphSize, const glm::vec4& uvRect, const glm::vec4& sizeBounds, GLuint texture, float depth, const ColorRGBA8& color, float angle, glm::mat4& translation, float scale) // angle in radians
{
    // fail without info provided
    //if(!m_screenHeight || !m_screenWidth || !m_scale) { GameEngine101::fatalError("No screen width/height or scale specified for drawRepeat!"); }

    int x_iterations = static_cast<int>(sizeBounds.z / glyphSize.z);
    int y_iterations = static_cast<int>(sizeBounds.w / glyphSize.w);

    std::vector<Glyph> glyphs;
    glyphs.resize(x_iterations * y_iterations);
    std::vector<Vertex> vertices;
    std::vector<RenderBatch> renderBatches;
    int cv = 0; // current vertex
    int glyphOffset = 0;

    // tmp variable
    glm::vec4 tmpDestRect = glyphSize;

    // rotate by angle, converted from radians to degrees, on the z-axis
    //glm::mat4 groupRotation = translation;
    //glm::mat4 groupRotation = glm::mat4(1.0f);
    glm::mat4 groupRotation;
    groupRotation = glm::translate(groupRotation, glm::vec3(sizeBounds.x, sizeBounds.y, 0.0f));
    //groupRotation = glm::translate(groupRotation, glm::vec3(0.5f * sizeBounds.z, 0.5f * sizeBounds.w, 0.0f));
    groupRotation = glm::rotate(groupRotation, angle, glm::vec3(0.0f, 0.0f, 1.0f));
    //groupRotation = glm::translate(groupRotation, glm::vec3(-0.5f * sizeBounds.z, -0.5f * sizeBounds.w, 0.0f));
    //groupRotation = glm::scale(groupRotation, glm::vec3(scale, scale, 0.0f));
    groupRotation = glm::translate(groupRotation, glm::vec3(-sizeBounds.x, -sizeBounds.y, 0.0f));
    GLint pUniformM = m_glsl->getUniformLocation("rotation");
    glUniformMatrix4fv(pUniformM, 1, GL_FALSE, &groupRotation[0][0]);

    // create glyphs
    for(uint ix = 0; ix < x_iterations; ix++)
    {
        tmpDestRect.x = glyphSize.x + ( glyphSize.z * ix ); // set X position

        for(uint iy = 0; iy < y_iterations; iy++)
        {
            tmpDestRect.y = glyphSize.y + ( glyphSize.w * iy ); // set Y position
            glyphs.emplace_back(tmpDestRect, uvRect, texture, depth, color);
        }
    }

    // resize verticies vector
    vertices.resize(glyphs.size() * 6);

    // emplace_back uses the constructor for whatever is being stored
    //      in this case, it's RenderBatch
    renderBatches.emplace_back(glyphOffset, 6, texture);

    vertices[cv++] = glyphs[0].topLeft; // execute, THEN add 1 to cv
    vertices[cv++] = glyphs[0].bottomLeft; // execute, THEN add 1 to cv
    vertices[cv++] = glyphs[0].bottomRight; // execute, THEN add 1 to cv
    vertices[cv++] = glyphs[0].bottomRight; // execute, THEN add 1 to cv
    vertices[cv++] = glyphs[0].topRight; // execute, THEN add 1 to cv
    vertices[cv++] = glyphs[0].topLeft; // execute, THEN add 1 to cv
    glyphOffset += 6;

    for(uint cg = 1; cg < glyphs.size(); cg++)
    {
        if(glyphs[cg].texture != glyphs[cg - 1].texture) { renderBatches.emplace_back(glyphOffset, 6, glyphs[cg].texture); }
        else { renderBatches.back().numVertices += 6; }

        vertices[cv++] = glyphs[cg].topLeft; // execute, THEN add 1 to cv
        vertices[cv++] = glyphs[cg].bottomLeft; // execute, THEN add 1 to cv
        vertices[cv++] = glyphs[cg].bottomRight; // execute, THEN add 1 to cv
        vertices[cv++] = glyphs[cg].bottomRight; // execute, THEN add 1 to cv
        vertices[cv++] = glyphs[cg].topRight; // execute, THEN add 1 to cv
        vertices[cv++] = glyphs[cg].topLeft; // execute, THEN add 1 to cv
        glyphOffset += 6;
    }

    glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
    glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), nullptr, GL_DYNAMIC_DRAW); // orphan the buffer
    glBufferSubData(GL_ARRAY_BUFFER, 0, vertices.size() * sizeof(Vertex), vertices.data());

    // unbind buffer on completion
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    // actually render
    glBindVertexArray(m_vao);
    for(uint i = 0; i < renderBatches.size(); i++)
    {
        glBindTexture(GL_TEXTURE_2D, renderBatches[i].texture);
        glDrawArrays(GL_TRIANGLES, renderBatches[i].offset, renderBatches[i].numVertices);
    }
    glBindVertexArray(0);
}

I understand debugging code based on snippets is hilariously difficult, but I am under the impression that I'm doing something obviously wrong. Hopefully this helps clear some of it up. If there is anything else that I can provide, please let me know!

Oh and shaders:

const char* GLVERT_SRC = R"(
#version 130
// vert shader

// input data from the VBO. Each vertex is 2 floats
in vec2 vertexPosition;
in vec4 vertexColor;
in vec2 vertexUV;

out vec2 fragmentPosition;
out vec4 fragmentColor;
out vec2 fragmentUV;

// projection matrix
uniform mat4 translation;
// model matrix (rotation)
uniform mat4 rotation;
// scale matrix
//uniform mat4 scaling;

void main()
{
    // Set the x,y position on the screen
    //gl_Position = translation * rotation * scaling * vec4(vertexPosition.xy, 0.0, 1.0);
    // gl_Position.xy = (projection * vec4(vertexPosition, 0.0, 1.0)).xy;
    gl_Position = translation * rotation * vec4(vertexPosition.xy, 0.0, 1.0);

    // the z position is zero since we are in 2D
    gl_Position.z = 0.0;

    //Indicate that the coordinates are normalized
    gl_Position.w = 1.0;

    fragmentPosition = vertexPosition;

    fragmentColor = vertexColor;

    fragmentUV = vec2(vertexUV.x, 1.0 - vertexUV.y);
})";

const char* GLFRAG_SRC = R"(
#version 130
// frag shader

in vec2 fragmentPosition;
in vec4 fragmentColor;
in vec2 fragmentUV;

// This is the 3 component float vector that gets outputted to the screen
// for each pixel.
out vec4 color;

uniform sampler2D mySampler;

void main()
{
    // cos(x) returns a number between -1 and 1. To convert it into the range 0 to 1
    // you simply do (cos(x) + 1.0) * 0.5
    vec4 textureColor = texture(mySampler, fragmentUV);

    // Make crazy colors using time and position
    color = fragmentColor * textureColor;
})";
\$\endgroup\$
  • 1
    \$\begingroup\$ Are you actually using shaders or are you just using the old OpenGL 1.x immediate pipeline? \$\endgroup\$ – Honeybunch Jan 13 '17 at 22:34
  • \$\begingroup\$ Honeybunch, thank you for pointing this out. MalphasWats pointed this out as well. I am NOT using immediate mode, just a mental slip up. glm::rotate and glm::translate, not glRotate and glTranslate. Sorry! \$\endgroup\$ – Beau B. Jan 14 '17 at 3:55
2
\$\begingroup\$

I'm about 2 questions ahead of you in my own OpenGL journey, but I'm starting to get a handle on things now. My project started in Python and then I ported to Javascript, and I'm not using SDL so it's a little different to you but the GL principles should be the same.

firstly, using matrices makes things simpler in the longer term, so if you can get your head around them, keep at it. Try this tutorial that came in handy for me. In a nutshell though, any matrix transforms you apply will apply to everything currently in a buffer being drawn, so your rotations will apply to everything currently being drawn, which is probably not what you want.

Depending on how many things are being rotated and how often, you will likely want to rework your pipeline a little so that the things being rotated are drawn individually, but this gets 'expensive' the more things you draw this way.

Your origin issue can be solved elegantly with a matrix. You mention glTranslate() and glRotate() which suggests you're using the older style 'fixed pipeline' openGL stuff, and I'm less familiar with that.

I'm building a 2D gl framework, and I use the following matrix to convert 'pixel coordinates' that I pass as vertices (30, 30), (120, 192) etc, into gl coordinates between (-1.0 and 1.0). It also flips the y-axis so top is -1 and then it translates it all up and left so top-left becomes (0,0):

projection_matrix = [
        2 / viewport_width, 0, 0, 0,
        0, -2 / viewport_height, 0, 0,
        0, 0, 2 / viewport_height, 0,
        -1, 1, 0, 1,
    ];

That's all very complicated, so let's break it down into a few steps:

identity_matrix = [
    1, 0, 0, 0,
    0, 1, 0, 0,
    0, 0, 1, 0,
    0, 0, 0, 1,
]

This is a special matrix that basically does nothing, whatever you pass in comes out the same the other end.

We can then translate things along the x, y, z axes with the bottom row:

translation_matrix = [
    1, 0, 0, 0,
    0, 1, 0, 0,
    0, 0, 1, 0,
    -1, -1, 0, 1,
]

This would translate everything up and left, so you would effectively be working with gl coordinates (0.0 - 2.0), so (0,0) should be at the top-left (where I prefer it).

I will point out that I finally learnt all of this about 2 days ago tearing my hair out trying to work out how to make a pixel-ndc coordinate 'projection' matrix (say that 5 times fast!).

In my top matrix, where I do this conversion, we can think of it like this:

I have a screen that is 500 pixels wide and 2.0 gl-thingies (sorry, I have no idea what they are called).

2.0 / 500 = 0.004 gl-thingies.

meaning each of the 500 pixels I want to draw is 0.004 gl-thingies. So if I want to draw something at x=50, I can work out the gl-coordinates by multiplying that by 0.004, so:

50 * 0.004 = 0.2

I can build that into the matrix, which moves all the maths onto the GPU, which is probably where you want it (but I'm not the boss of you so...):

projection_matrix = [
    0.004, 0, 0, 0,
    0, 1, 0, 0,
    0, 0, 1, 0,
    -1, -1, 0, 1,
]

then obviously you can do the same for the other axes, say the screen is 400pixels high:

2.0 / 400 = 0.005

projection_matrix = [
    0.004, 0,     0, 0,
    0,     0.005, 0, 0,
    0,     0,     1, 0,
    -1,   -1,     0, 1,
]

and so on for z (I'm glossing over because my work has been 2d based so I just make up useful depths for layering things and just make it the same as the height).

So you can also build in the rotation and any scaling you want to do, but it will apply to everything you multiply by this matrix.

What you would want to do is something like:

  1. create a buffer for all the 'main stuff'
  2. load that buffer up with the data
  3. set the matrix
  4. draw the buffer

  5. create a buffer for things rotated x degrees

  6. load this stuff in
  7. set the new matrix including rotation
  8. draw the buffer

For performance purposes, you should only do 1 and 5 once per program.

2 and 6 should only be done when something in those buffers has changed, similarly, 3 and 7 should only be done when something about the view has changed - in my project, I write an entire level worth of tiles into my tile buffer at the start, then use a second matrix to translate things around as the player moves (I don't know how to apply 2 'translation' type matrices using fixed function OpenGL, so I guess that would have to be done in your code?).

4 and 8 should be done every frame - stuff stays in the buffers until you change it, uniforms keep their values until you write new ones in. (this is something I learnt way too late!!!).

So your question:

Is it possible to utilize uniforms and glTranslate/glRotate to rotate specific objects

no, I don't think it is, at least, not with fixed-function, you could potentially do something mindbendingly clever with your own custom shaders, however, that's waaaaay out of my expertise! I'm wondering if you could create another buffer for rotation matrices and pass a matrix in for each vertex, but I don't know if that's sensible/possible... certainly worth trying, probably a bit expensive though? People use this word "expensive" a lot - it's basically about how long it takes the CPU to shove stuff into the GPU/GraphicsRAM, can it do it faster than the magic 16.66milliseconds you need to achieve 60 frames per second!

I'd certainly like to know how to do something like you want to do without having to draw individual sprites 1-at-a-time!

I will likely do it by applying the rotation transformation to the vertex co-ordinates before I send them off to the buffers (my sprites move a lot so I write to the buffer every frame), but I'm definitely wondering about the matrix buffer thing now too...

Finally, some self promotion - you can see what I've achieved with all this voodoo on my WIP game framework page. Which may or may not be actually working properly at any given moment as I tinker with it!! It's almost certainly not the "best way" to do what I want, but I'm slowly starting to realise that the people who know the best way are earning the big bucks making AAA+ games, not writing online tutorials and answering stack overflow questions sadly :(

\$\endgroup\$
  • \$\begingroup\$ My friend, glorious answer. Well typed and plenty of info! I will edit my original post to include code snippets shortly. I am not using glRotate and glTranslate, which was my mistake, but rather glm::rotate and glm::translate (will correct in original post). \$\endgroup\$ – Beau B. Jan 14 '17 at 3:30

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.