#include "renderer.h" #include #include #define GL_GLEXT_PROTOTYPES #include constexpr int LINE_WIDTH = 2; constexpr int QUAD_ORDER[] = {0, 1, 2, 0, 2, 3}; constexpr int WHITE_SIZE = 1; constexpr Uint32 WHITE_PIXEL = 0xFFFFFFFF; #ifdef SDL_PLATFORM_EMSCRIPTEN constexpr const char* SHADER_VERSION = "#version 300 es\nprecision highp float;\n"; #else constexpr const char* SHADER_VERSION = "#version 330 core\n"; #endif // Maps pixels to clip space with view: x and y scales, then offsets. constexpr const char* VERTEX_SHADER = R"( layout(location = 0) in vec2 inPosition; layout(location = 1) in vec2 inUv; layout(location = 2) in vec4 inTint; layout(location = 3) in vec4 inOffset; uniform vec4 view; out vec2 uv; out vec4 tint; out vec4 offset; void main() { uv = inUv; tint = inTint; offset = inOffset; gl_Position = vec4(inPosition * view.xy + view.zw, 0.0, 1.0); } )"; // Tints each texel, then adds the color offset. constexpr const char* FRAGMENT_SHADER = R"( in vec2 uv; in vec4 tint; in vec4 offset; uniform sampler2D image; out vec4 color; void main() { color = clamp(texture(image, uv) * tint + offset, 0.0, 1.0); } )"; // Every GL entry point the renderer uses, looked up from the driver once a context exists. #define GL_FUNCTIONS(X) \ X(AttachShader) \ X(BindBuffer) \ X(BindFramebuffer) \ X(BindTexture) \ X(BindVertexArray) \ X(BlendFuncSeparate) \ X(BufferData) \ X(Clear) \ X(ClearColor) \ X(CompileShader) \ X(CreateProgram) \ X(CreateShader) \ X(DeleteShader) \ X(DeleteTextures) \ X(Disable) \ X(DrawArrays) \ X(Enable) \ X(EnableVertexAttribArray) \ X(FramebufferTexture2D) \ X(GenBuffers) \ X(GenFramebuffers) \ X(GenTextures) \ X(GenVertexArrays) \ X(GetShaderInfoLog) \ X(GetUniformLocation) \ X(LinkProgram) \ X(ShaderSource) \ X(TexImage2D) \ X(TexParameteri) \ X(Uniform4f) \ X(UseProgram) \ X(VertexAttribPointer) \ X(Viewport) struct Gl { #define GL_FUNCTION_DECLARE(name) decltype(&gl##name) name = nullptr; GL_FUNCTIONS(GL_FUNCTION_DECLARE) #undef GL_FUNCTION_DECLARE }; static Gl gl; static GLuint shader_compile(GLenum stage, const char* source) { const char* sources[] = {SHADER_VERSION, source}; GLuint shader = gl.CreateShader(stage); gl.ShaderSource(shader, 2, sources, nullptr); gl.CompileShader(shader); char log[1024] = {}; gl.GetShaderInfoLog(shader, sizeof(log), nullptr, log); if (*log) SDL_Log("Shader: %s", log); return shader; } static Texture texture_create(int width, int height, const void* pixels) { Texture texture = {0, height, width}; gl.GenTextures(1, &texture.id); gl.BindTexture(GL_TEXTURE_2D, texture.id); gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); gl.TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); return texture; } void Renderer::init() { bool isLoaded = true; #define GL_FUNCTION_LOAD(name) \ gl.name = (decltype(gl.name))SDL_GL_GetProcAddress("gl" #name); \ isLoaded &= gl.name != nullptr; GL_FUNCTIONS(GL_FUNCTION_LOAD) #undef GL_FUNCTION_LOAD if (!isLoaded) return; program = gl.CreateProgram(); GLuint vertexShader = shader_compile(GL_VERTEX_SHADER, VERTEX_SHADER); GLuint fragmentShader = shader_compile(GL_FRAGMENT_SHADER, FRAGMENT_SHADER); gl.AttachShader(program, vertexShader); gl.AttachShader(program, fragmentShader); gl.LinkProgram(program); gl.DeleteShader(vertexShader); gl.DeleteShader(fragmentShader); viewLocation = gl.GetUniformLocation(program, "view"); gl.GenVertexArrays(1, &vertexArray); gl.GenBuffers(1, &vertexBuffer); gl.BindVertexArray(vertexArray); gl.BindBuffer(GL_ARRAY_BUFFER, vertexBuffer); auto attribute = [](int index, int count, size_t offset) { gl.EnableVertexAttribArray(index); gl.VertexAttribPointer(index, count, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offset); }; attribute(0, 2, offsetof(Vertex, x)); attribute(1, 2, offsetof(Vertex, u)); attribute(2, 4, offsetof(Vertex, tint)); attribute(3, 4, offsetof(Vertex, offset)); white = texture_create(WHITE_SIZE, WHITE_SIZE, &WHITE_PIXEL); isValid = true; } Texture* Renderer::texture_get(const std::string& path) { if (auto found = textures.find(path); found != textures.end()) return found->second.id ? &found->second : nullptr; Texture& texture = textures[path]; SDL_Surface* loaded = SDL_LoadPNG(path.c_str()); SDL_Surface* surface = loaded ? SDL_ConvertSurface(loaded, SDL_PIXELFORMAT_RGBA32) : nullptr; SDL_DestroySurface(loaded); if (!surface) { SDL_Log("Failed to load %s: %s", path.c_str(), SDL_GetError()); return nullptr; } texture = texture_create(surface->w, surface->h, surface->pixels); SDL_DestroySurface(surface); return &texture; } // Draws to a canvas (sized to fit, y flipped so imgui shows it upright), or to the window when canvas is null. void Renderer::canvas_set(Canvas* canvas, int width, int height, Color clear) { flush(); if (canvas) { if (!canvas->framebuffer) gl.GenFramebuffers(1, &canvas->framebuffer); if (canvas->texture.width != width || canvas->texture.height != height) { gl.DeleteTextures(1, &canvas->texture.id); canvas->texture = texture_create(width, height, nullptr); gl.BindFramebuffer(GL_FRAMEBUFFER, canvas->framebuffer); gl.FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, canvas->texture.id, 0); } } gl.BindFramebuffer(GL_FRAMEBUFFER, canvas ? canvas->framebuffer : 0); gl.Viewport(0, 0, width, height); gl.ClearColor(clear.r, clear.g, clear.b, clear.a); gl.Clear(GL_COLOR_BUFFER_BIT); float flip = canvas ? 1 : -1; view[0] = 2.0f / width; view[1] = flip * 2.0f / height; view[2] = -1; view[3] = -flip; } // Draws region (in the texture's pixels) as a quad: the transform takes the region's pixels, from its top left, to the // target's. void Renderer::quad_draw(const Texture& texture, Rect region, Affine transform, Color tint, Color offset) { if (!texture.id || !texture.width || !texture.height) return; if (texture.id != batchTexture) flush(); batchTexture = texture.id; Vec2 corners[] = {{0, 0}, {region.w, 0}, {region.w, region.h}, {0, region.h}}; for (int index : QUAD_ORDER) { Vec2 corner = corners[index]; Vec2 position = transform * corner; float u = (region.x + corner.x) / texture.width; float v = (region.y + corner.y) / texture.height; vertices.push_back({offset, tint, u, v, position.x, position.y}); } } void Renderer::line_draw(Vec2 from, Vec2 to, Color color) { Vec2 delta = to - from; float length = vec2_length(delta); if (length <= 0) return; Vec2 normal = Vec2{-delta.y, delta.x} / length; Affine transform = {delta.x / length, delta.y / length, normal.x, normal.y, from.x, from.y}; quad_draw(white, {0, 0, length, LINE_WIDTH}, transform * Affine{1, 0, 0, 1, 0, -LINE_WIDTH / 2.0f}, color); } void Renderer::outline_draw(Affine transform, float width, float height, Color color) { Vec2 corners[] = {transform * Vec2{0, 0}, transform * Vec2{width, 0}, transform * Vec2{width, height}, transform * Vec2{0, height}}; for (int i = 0; i < 4; i++) line_draw(corners[i], corners[(i + 1) % 4], color); } void Renderer::flush() { if (vertices.empty()) return; gl.Enable(GL_BLEND); gl.Disable(GL_DEPTH_TEST); gl.Disable(GL_SCISSOR_TEST); gl.Disable(GL_CULL_FACE); gl.BlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA); gl.UseProgram(program); gl.Uniform4f(viewLocation, view[0], view[1], view[2], view[3]); gl.BindVertexArray(vertexArray); gl.BindBuffer(GL_ARRAY_BUFFER, vertexBuffer); gl.BufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), vertices.data(), GL_STREAM_DRAW); gl.BindTexture(GL_TEXTURE_2D, batchTexture); gl.DrawArrays(GL_TRIANGLES, 0, (GLsizei)vertices.size()); vertices.clear(); }