Last active
July 8, 2023 22:41
-
-
Save ConnorRigby/40eea1bb5b14305548e8b021ab3e373e to your computer and use it in GitHub Desktop.
Example TMX TileMap renderer with C++, OpenGL, GLM, GLFW, Glad and stb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
all: test | |
test: test.cpp | |
$(CXX) test.cpp -o test -lglfw -lGL -lX11 -lpthread -lXrandr -lXi -ldl -Ilib/tmx/src/ -I glad/include -I lib/stb/ -Llib/tmx/build -ltmx -lxml2 -lz glad/src/glad.c | |
run: test | |
./test untitled.tmx | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <stdio.h> | |
#include <tmx.h> | |
#include <glad/glad.h> | |
#include <GLFW/glfw3.h> | |
#include <glm/glm.hpp> | |
#include <glm/gtc/matrix_transform.hpp> | |
#define STB_IMAGE_IMPLEMENTATION | |
#include <stb_image.h> | |
#include <vector> | |
#define DISPLAY_W 1920 | |
#define DISPLAY_H 1080 | |
unsigned int image_data_width_in_pixels = 0; | |
unsigned int image_data_height_in_pixels = 0; | |
const char* vertexShaderSource = R"( | |
#version 330 core | |
layout (location = 0) in vec2 aPos; | |
layout (location = 1) in vec2 aTexCoord; | |
out vec2 TexCoord; | |
uniform mat4 projection; | |
void main() | |
{ | |
gl_Position = projection * vec4(aPos, 0.0, 1.0); | |
TexCoord = aTexCoord; | |
} | |
)"; | |
const char* fragmentShaderSource = R"( | |
#version 330 core | |
in vec2 TexCoord; | |
out vec4 FragColor; | |
uniform sampler2D texture1; | |
void main() | |
{ | |
FragColor = texture(texture1, TexCoord); | |
} | |
)"; | |
static GLuint vertexArray, vertexBuffer, indexBuffer; | |
static GLuint shaderProgram; | |
static GLint projectionLocation; | |
struct Vertex { | |
glm::vec2 position; | |
glm::vec2 texCoord; | |
}; | |
std::vector<Vertex> vertices; | |
std::vector<unsigned int> indices; | |
static GLFWwindow* window; | |
void errorCallback(int error, const char* description) | |
{ | |
fputs(description, stderr); | |
} | |
void keyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) | |
{ | |
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) { | |
glfwSetWindowShouldClose(window, GLFW_TRUE); | |
} | |
} | |
GLuint createShader(GLenum type, const char* source) | |
{ | |
GLuint shader = glCreateShader(type); | |
glShaderSource(shader, 1, &source, NULL); | |
glCompileShader(shader); | |
GLint success; | |
glGetShaderiv(shader, GL_COMPILE_STATUS, &success); | |
if (!success) { | |
char infoLog[512]; | |
glGetShaderInfoLog(shader, 512, NULL, infoLog); | |
printf("Shader compilation error:\n%s\n", infoLog); | |
} | |
return shader; | |
} | |
GLuint createShaderProgram(const char* vertexShaderSource, const char* fragmentShaderSource) | |
{ | |
GLuint vertexShader = createShader(GL_VERTEX_SHADER, vertexShaderSource); | |
GLuint fragmentShader = createShader(GL_FRAGMENT_SHADER, fragmentShaderSource); | |
GLuint program = glCreateProgram(); | |
glAttachShader(program, vertexShader); | |
glAttachShader(program, fragmentShader); | |
glLinkProgram(program); | |
GLint success; | |
glGetProgramiv(program, GL_LINK_STATUS, &success); | |
if (!success) { | |
char infoLog[512]; | |
glGetProgramInfoLog(program, 512, NULL, infoLog); | |
printf("Shader program linking error:\n%s\n", infoLog); | |
} | |
glDeleteShader(vertexShader); | |
glDeleteShader(fragmentShader); | |
return program; | |
} | |
void initializeOpenGL() | |
{ | |
if (!glfwInit()) { | |
fputs("GLFW initialization failed\n", stderr); | |
return; | |
} | |
glfwSetErrorCallback(errorCallback); | |
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); | |
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); | |
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); | |
window = glfwCreateWindow(DISPLAY_W, DISPLAY_H, "OpenGL Example", NULL, NULL); | |
if (!window) { | |
fputs("GLFW window creation failed\n", stderr); | |
glfwTerminate(); | |
return; | |
} | |
glfwSetKeyCallback(window, keyCallback); | |
glfwMakeContextCurrent(window); | |
glfwSwapInterval(1); | |
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { | |
fputs("GLAD initialization failed\n", stderr); | |
glfwDestroyWindow(window); | |
glfwTerminate(); | |
return; | |
} | |
// glViewport(0, 0, DISPLAY_W, DISPLAY_H); | |
// Create vertex array object (VAO) | |
glGenVertexArrays(1, &vertexArray); | |
glBindVertexArray(vertexArray); | |
// Create vertex buffer object (VBO) | |
glGenBuffers(1, &vertexBuffer); | |
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); | |
glGenBuffers(1, &indexBuffer); | |
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer); | |
// Specify the vertex attributes | |
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, position)); | |
glEnableVertexAttribArray(0); | |
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, texCoord)); | |
glEnableVertexAttribArray(1); | |
// Compile and link the shader program | |
shaderProgram = createShaderProgram(vertexShaderSource, fragmentShaderSource); | |
// Get the location of the projection matrix uniform | |
projectionLocation = glGetUniformLocation(shaderProgram, "projection"); | |
glClear(GL_COLOR_BUFFER_BIT); | |
} | |
void render_map(tmx_map* map) | |
{ | |
// Bind the vertex buffer | |
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); | |
unsigned int mapWidthTiles = map->width; | |
unsigned int mapHeightTiles = map->height; | |
unsigned int tileWidth = map->tile_width; | |
unsigned int tileHeight = map->tile_height; | |
unsigned int mapWidth = mapWidthTiles * tileWidth; | |
unsigned int mapHeight = mapHeightTiles * tileHeight; | |
unsigned int index = 0; | |
for (unsigned int i = 0; i < map->height; i++) { | |
for (unsigned int j = 0; j < map->width; j++) { | |
unsigned int gid = (map->ly_head->content.gids[(i * map->width) + j]) & TMX_FLIP_BITS_REMOVAL; | |
if (map->tiles[gid] != nullptr) { | |
unsigned int x = map->tiles[gid]->ul_x; | |
unsigned int y = map->tiles[gid]->ul_y; | |
float texCoordLeft = static_cast<float>(x) / static_cast<float>(image_data_width_in_pixels); | |
float texCoordBottom = static_cast<float>(y + tileHeight) / static_cast<float>(image_data_height_in_pixels); | |
float texCoordRight = static_cast<float>(x + tileWidth) / static_cast<float>(image_data_width_in_pixels); | |
float texCoordTop = static_cast<float>(y) / static_cast<float>(image_data_height_in_pixels); | |
Vertex v1 = { { j * tileWidth, i * tileHeight },{ texCoordLeft, texCoordTop} }; | |
Vertex v2 = { { j * tileWidth, (i + 1) * tileHeight },{ texCoordLeft, texCoordBottom } }; | |
Vertex v3 = { { (j + 1) * tileWidth, (i + 1) * tileHeight },{ texCoordRight, texCoordBottom } }; | |
Vertex v4 = { { (j + 1) * tileWidth, i * tileHeight },{ texCoordRight, texCoordTop } }; | |
printf("x/y=(%d, %d)\n", x, y); | |
printf("l=%0.2f t=%0.2f r=%0.2f b=%0.2f\n", texCoordLeft, texCoordTop, texCoordRight, texCoordBottom); | |
printf("ul=(%0.2f, %0.2f)\nbl=(%0.2f, %0.2f)\nbr=(%0.2f,%0.2f)\ntr=(%0.2f, %0.2f) \n\n", | |
texCoordLeft, texCoordTop, | |
texCoordLeft, texCoordBottom, | |
texCoordRight, texCoordBottom, | |
texCoordRight, texCoordTop); | |
vertices.push_back(v1); // top left | |
vertices.push_back(v2); // bottom left | |
vertices.push_back(v3); // bottom right | |
vertices.push_back(v1); // top left | |
vertices.push_back(v3); // botom right | |
vertices.push_back(v4); // top right | |
// first triangle | |
indices.push_back(index + 0); | |
indices.push_back(index + 1); | |
indices.push_back(index + 2); | |
// second triangle | |
indices.push_back(index + 3); | |
indices.push_back(index + 4); | |
indices.push_back(index + 5); | |
index += 6; | |
} | |
} | |
} | |
// Upload the vertex data to the GPU | |
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); | |
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), vertices.data(), GL_STATIC_DRAW); | |
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer); | |
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), indices.data(), GL_STATIC_DRAW); | |
} | |
int main(int argc, char** argv) | |
{ | |
initializeOpenGL(); | |
// Set the callback globs in the main function | |
tmx_img_load_func = [](const char* path) { | |
int width, height, numChannels; | |
stbi_set_flip_vertically_on_load(0); | |
unsigned char* data = stbi_load(path, &width, &height, &numChannels, 4); | |
if (data) { | |
GLuint textureID; | |
glGenTextures(1, &textureID); | |
glBindTexture(GL_TEXTURE_2D, textureID); | |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); | |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); | |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); | |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); | |
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); | |
glGenerateMipmap(GL_TEXTURE_2D); | |
stbi_image_free(data); | |
image_data_width_in_pixels = width; | |
image_data_height_in_pixels = height; | |
return reinterpret_cast<void*>(textureID); | |
} | |
return static_cast<void*>(nullptr); | |
}; | |
tmx_img_free_func = [](void* image) { | |
if (image) { | |
GLuint textureID = static_cast<GLuint>(reinterpret_cast<uintptr_t>(image)); | |
glDeleteTextures(1, &textureID); | |
} | |
}; | |
tmx_map* map = tmx_load(argv[1]); | |
if (!map) { | |
tmx_perror("Cannot load map"); | |
glfwDestroyWindow(window); | |
glfwTerminate(); | |
return 1; | |
} | |
// Set the orthographic projection matrix | |
glm::mat4 projection = glm::ortho(0.0f, static_cast<float>(DISPLAY_W), static_cast<float>(DISPLAY_H), 0.0f, -1.0f, 1.0f); | |
glUseProgram(shaderProgram); | |
glUniformMatrix4fv(projectionLocation, 1, GL_FALSE, &projection[0][0]); | |
render_map(map); | |
while (!glfwWindowShouldClose(window)) { | |
glEnable(GL_DEPTH_TEST); | |
glClearColor(0.1, 0.1, 0.1, 1.0); | |
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); | |
// Draw the tilemap | |
// glDrawArrays(GL_TRIANGLES, 0, vertices.size()); | |
glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0); | |
glfwSwapBuffers(window); | |
glfwPollEvents(); | |
} | |
tmx_map_free(map); | |
glDeleteBuffers(1, &vertexBuffer); | |
glDeleteVertexArrays(1, &vertexArray); | |
glDeleteProgram(shaderProgram); | |
glfwDestroyWindow(window); | |
glfwTerminate(); | |
return 0; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="UTF-8"?> | |
<tileset version="1.8" tiledversion="1.8.0" name="tilemap" tilewidth="8" tileheight="8" tilecount="8" columns="4"> | |
<image source="tilemap.png" width="32" height="16"/> | |
</tileset> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="UTF-8"?> | |
<map version="1.8" tiledversion="1.8.0" orientation="orthogonal" renderorder="right-down" width="4" height="4" tilewidth="8" tileheight="8" infinite="0" nextlayerid="2" nextobjectid="1"> | |
<tileset firstgid="1" source="tilemap.tsx"/> | |
<layer id="1" name="Tile Layer 1" width="4" height="4"> | |
<data encoding="csv"> | |
1,1,2,2, | |
1,1,2,2, | |
3,3,1,4, | |
3,3,5,5 | |
</data> | |
</layer> | |
</map> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment