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;
})";