From dc8cab991e2bb5a6c2f5611a354c4bb187890ac0 Mon Sep 17 00:00:00 2001 From: Oznogon Date: Tue, 3 Mar 2026 10:42:10 -0800 Subject: [PATCH 1/3] Define arbitrary clipping regions, translate renders Implement GL_SCISSOR_TEST at the RenderTarget level to support arbitrary clipping of rendered areas, and implement translation functions to support scrolling of arbitrary rendered areas. These primarily support the implementation of nested GUI elements that can be visibly clipped within and scrollable by a parent container. - Add scissor_stack vector, push/popScissorRect() to define and access visible areas defined as sp::Rects. - Add translation_stack vector, push/popTranslation() to define and access positional deltas, translate them to pixel coordinates, and track the render's total translation value. - Add getTranslation() to expose the total current translation value, to allow for relative positioning to a translated render. - Add static applyProjectionMatrix() to apply a translation to a 2D projection matrix, for mapping a virtual coordinate to a relative screen position. This allows the definition of a GUI container that renders and interacts with only what is within a scissor rect, while allowing overflow content to be scrolled (translated) into that rect. For example, given: void GuiScrollContainer::drawElements( glm::vec2 mouse_position, sp::Rect /* parent_rect */, sp::RenderTarget& renderer ) { sp::Rect content_rect = getContentRect(); renderer.pushScissorRect(content_rect); renderer.pushTranslation({0.0f, -scroll_offset}); glm::vec2 layout_mouse = mouse_position + glm::vec2{ 0.0f, scroll_offset }; for (auto it = children.begin(); it != children.end(); ) { ... pass translated mouse events through to scrolled contents ... } renderer.popTranslation(); renderer.popScissorRect(); } Rendering of all child elements of this container is clipped to the parent's content_rect dimensions and shifted vertically by the translation offset. Mouse events are also passed through to the translated positions of the child elements. --- src/graphics/renderTarget.cpp | 119 +++++++++++++++++++++++++++++++++- src/graphics/renderTarget.h | 23 +++++-- 2 files changed, 135 insertions(+), 7 deletions(-) diff --git a/src/graphics/renderTarget.cpp b/src/graphics/renderTarget.cpp index 8226b130..8649d8fc 100644 --- a/src/graphics/renderTarget.cpp +++ b/src/graphics/renderTarget.cpp @@ -10,6 +10,7 @@ #include "vectorUtils.h" #include #include +#include #include @@ -31,6 +32,9 @@ static std::vector lines_index_data; static std::vector points_vertex_data; static std::vector points_index_data; +static std::vector> scissor_stack; +static std::vector translation_stack; +static glm::vec2 current_translation{0.0f, 0.0f}; struct ImageInfo { @@ -1222,7 +1226,6 @@ void RenderTarget::applyBuffer(sp::Texture* texture, std::vector &da void RenderTarget::finish(sp::Texture* texture) { - applyBuffer(texture, vertex_data, index_data, GL_TRIANGLES); applyBuffer(texture, lines_vertex_data, lines_index_data, GL_LINES); applyBuffer(texture, points_vertex_data, points_index_data, GL_POINTS); @@ -1241,9 +1244,119 @@ glm::ivec2 RenderTarget::getPhysicalSize() return physical_size; } -glm::ivec2 RenderTarget::virtualToPixelPosition(glm::vec2 v) +glm::ivec2 RenderTarget::virtualToPixelPosition(glm::vec2 virtual_position) +{ + return {virtual_position.x * physical_size.x / virtual_size.x, virtual_position.y * physical_size.y / virtual_size.y}; +} + +static void applyProjectionMatrix(glm::vec2 virtual_size, glm::vec2 translation) +{ + // Simple pan reprojection of translated virtual coordinates to pixels via + // shader. + // Not sure if this will conflict with other shader operations! + glm::mat3 m{1.0f}; + m[0][0] = 2.0f / virtual_size.x; + m[1][1] = -2.0f / virtual_size.y; + m[2][0] = -1.0f + (2.0f * translation.x / virtual_size.x); + m[2][1] = 1.0f - (2.0f * translation.y / virtual_size.y); + glUniformMatrix3fv(shader->getUniformLocation("u_projection"), 1, GL_FALSE, glm::value_ptr(m)); +} + +void RenderTarget::pushScissorRect(sp::Rect virtual_rect) +{ + // Flush geometry + finish(); + + // Apply the current translation (if any) so the clipped region aligns to + // the translated visual position. + glm::vec2 adj = virtual_rect.position + current_translation; + glm::ivec2 px_min = virtualToPixelPosition(adj); + glm::ivec2 px_max = virtualToPixelPosition(adj + virtual_rect.size); + GLint px = px_min.x; + GLint py = px_min.y; + GLint pw = px_max.x - px_min.x; + GLint ph = px_max.y - px_min.y; + + // Convert coordinates to OpenGL bottom-left origin. + GLint py_gl = physical_size.y - (py + ph); + + // Add the scissor rect. + std::array new_rect = {px, py_gl, pw, ph}; + + // If nested, intersect this rect with the previous rect in the stack. + // Newly stacked rects shouldn't expand the active clipping region. + if (!scissor_stack.empty()) + { + const auto& prev = scissor_stack.back(); + GLint ix = std::max(new_rect[0], prev[0]); + GLint iy = std::max(new_rect[1], prev[1]); + GLint ix2 = std::min(new_rect[0] + new_rect[2], prev[0] + prev[2]); + GLint iy2 = std::min(new_rect[1] + new_rect[3], prev[1] + prev[3]); + new_rect[0] = ix; + new_rect[1] = iy; + new_rect[2] = std::max(0, ix2 - ix); + new_rect[3] = std::max(0, iy2 - iy); + } + + // Clip the render to the scissor rect. + scissor_stack.push_back(new_rect); + glScissor(new_rect[0], new_rect[1], new_rect[2], new_rect[3]); + glEnable(GL_SCISSOR_TEST); +} + +void RenderTarget::popScissorRect() +{ + // Flush geometry + finish(); + + // Pop the top scissor rect. + if (!scissor_stack.empty()) scissor_stack.pop_back(); + + // If that was the last rect, or there weren't any, stop clipping. + // Otherwise, clip to the back rect. + if (scissor_stack.empty()) + glDisable(GL_SCISSOR_TEST); + else + { + const auto& top = scissor_stack.back(); + glScissor(top[0], top[1], top[2], top[3]); + } +} + +void RenderTarget::pushTranslation(glm::vec2 offset) +{ + // Flush geometry + finish(); + + // Push a translation onto the stack and add it to the current translation + // value. + translation_stack.push_back(offset); + current_translation += offset; + + // Reproject with the new translation value. + applyProjectionMatrix(virtual_size, current_translation); +} + +void RenderTarget::popTranslation() +{ + // Flush geometry + finish(); + + // Pop a translation off the stack (if there is one) and remove it from the + // current translation value. + if (!translation_stack.empty()) + { + current_translation -= translation_stack.back(); + translation_stack.pop_back(); + } + + // Reproject with the new translation value. + applyProjectionMatrix(virtual_size, current_translation); +} + +glm::vec2 RenderTarget::getTranslation() const { - return {v.x * physical_size.x / virtual_size.x, v.y * physical_size.y / virtual_size.y}; + return current_translation; } } diff --git a/src/graphics/renderTarget.h b/src/graphics/renderTarget.h index 9d404615..054cfe5d 100644 --- a/src/graphics/renderTarget.h +++ b/src/graphics/renderTarget.h @@ -1,5 +1,4 @@ -#ifndef SP_GRAPHICS_RENDERTARGET_H -#define SP_GRAPHICS_RENDERTARGET_H +#pragma once #include #include @@ -70,6 +69,24 @@ class RenderTarget : sp::NonCopyable void drawStretchedHVClipped(sp::Rect rect, sp::Rect clip_rect, float corner_size, std::string_view texture, glm::u8vec4 color={255,255,255,255}); void finish(); + + // Functions for using rect masks with glScissor to clip a render. + + // Stack nested rects to determine the mask's bounds. + void pushScissorRect(sp::Rect virtual_rect); + // Remove a nested rect from the mask. + void popScissorRect(); + + // Functions for translating nested renders between virtual and real pixels. + + // Stack nested translation vectors to determine their offsets relative to + // the container and each other. + void pushTranslation(glm::vec2 virtual_offset); + // Remove a nested translation vector. + void popTranslation(); + // Get the total translation vector from the top of the stack. + glm::vec2 getTranslation() const; + struct VertexData { glm::vec2 position; @@ -89,5 +106,3 @@ class RenderTarget : sp::NonCopyable }; } - -#endif//SP_GRAPHICS_RENDERTARGET_H From 724fd06a8e7ab351bfb38d306cc75facec9cb3de Mon Sep 17 00:00:00 2001 From: oznogon Date: Sat, 21 Mar 2026 11:46:30 -0700 Subject: [PATCH 2/3] Rename ScissorRect refs to ClipRegion --- src/graphics/renderTarget.cpp | 24 ++++++++++++------------ src/graphics/renderTarget.h | 8 ++++---- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/graphics/renderTarget.cpp b/src/graphics/renderTarget.cpp index 8649d8fc..2525ef50 100644 --- a/src/graphics/renderTarget.cpp +++ b/src/graphics/renderTarget.cpp @@ -32,7 +32,7 @@ static std::vector lines_index_data; static std::vector points_vertex_data; static std::vector points_index_data; -static std::vector> scissor_stack; +static std::vector> clip_region_stack; static std::vector translation_stack; static glm::vec2 current_translation{0.0f, 0.0f}; @@ -1262,7 +1262,7 @@ static void applyProjectionMatrix(glm::vec2 virtual_size, glm::vec2 translation) glUniformMatrix3fv(shader->getUniformLocation("u_projection"), 1, GL_FALSE, glm::value_ptr(m)); } -void RenderTarget::pushScissorRect(sp::Rect virtual_rect) +void RenderTarget::pushClipRegion(sp::Rect virtual_rect) { // Flush geometry finish(); @@ -1280,14 +1280,14 @@ void RenderTarget::pushScissorRect(sp::Rect virtual_rect) // Convert coordinates to OpenGL bottom-left origin. GLint py_gl = physical_size.y - (py + ph); - // Add the scissor rect. + // Add the clip region. std::array new_rect = {px, py_gl, pw, ph}; // If nested, intersect this rect with the previous rect in the stack. // Newly stacked rects shouldn't expand the active clipping region. - if (!scissor_stack.empty()) + if (!clip_region_stack.empty()) { - const auto& prev = scissor_stack.back(); + const auto& prev = clip_region_stack.back(); GLint ix = std::max(new_rect[0], prev[0]); GLint iy = std::max(new_rect[1], prev[1]); GLint ix2 = std::min(new_rect[0] + new_rect[2], prev[0] + prev[2]); @@ -1298,27 +1298,27 @@ void RenderTarget::pushScissorRect(sp::Rect virtual_rect) new_rect[3] = std::max(0, iy2 - iy); } - // Clip the render to the scissor rect. - scissor_stack.push_back(new_rect); + // Clip the render to the clip region. + clip_region_stack.push_back(new_rect); glScissor(new_rect[0], new_rect[1], new_rect[2], new_rect[3]); glEnable(GL_SCISSOR_TEST); } -void RenderTarget::popScissorRect() +void RenderTarget::popClipRegion() { // Flush geometry finish(); - // Pop the top scissor rect. - if (!scissor_stack.empty()) scissor_stack.pop_back(); + // Pop the top clip region. + if (!clip_region_stack.empty()) clip_region_stack.pop_back(); // If that was the last rect, or there weren't any, stop clipping. // Otherwise, clip to the back rect. - if (scissor_stack.empty()) + if (clip_region_stack.empty()) glDisable(GL_SCISSOR_TEST); else { - const auto& top = scissor_stack.back(); + const auto& top = clip_region_stack.back(); glScissor(top[0], top[1], top[2], top[3]); } } diff --git a/src/graphics/renderTarget.h b/src/graphics/renderTarget.h index 054cfe5d..31256dd4 100644 --- a/src/graphics/renderTarget.h +++ b/src/graphics/renderTarget.h @@ -72,10 +72,10 @@ class RenderTarget : sp::NonCopyable // Functions for using rect masks with glScissor to clip a render. - // Stack nested rects to determine the mask's bounds. - void pushScissorRect(sp::Rect virtual_rect); - // Remove a nested rect from the mask. - void popScissorRect(); + // Stack nested rects to determine the clip region's bounds. + void pushClipRegion(sp::Rect virtual_rect); + // Remove a nested rect from the clip region. + void popClipRegion(); // Functions for translating nested renders between virtual and real pixels. From 44cd890227e66410737ef6e62b3f7b058cda20b4 Mon Sep 17 00:00:00 2001 From: oznogon Date: Sat, 21 Mar 2026 12:19:25 -0700 Subject: [PATCH 3/3] Remove render translation implementation Make this the game's layout system's responsibility instead. --- src/graphics/renderTarget.cpp | 58 ++--------------------------------- src/graphics/renderTarget.h | 10 ------ 2 files changed, 2 insertions(+), 66 deletions(-) diff --git a/src/graphics/renderTarget.cpp b/src/graphics/renderTarget.cpp index 2525ef50..18fc9f9e 100644 --- a/src/graphics/renderTarget.cpp +++ b/src/graphics/renderTarget.cpp @@ -33,8 +33,6 @@ static std::vector points_vertex_data; static std::vector points_index_data; static std::vector> clip_region_stack; -static std::vector translation_stack; -static glm::vec2 current_translation{0.0f, 0.0f}; struct ImageInfo { @@ -1249,29 +1247,13 @@ glm::ivec2 RenderTarget::virtualToPixelPosition(glm::vec2 virtual_position) return {virtual_position.x * physical_size.x / virtual_size.x, virtual_position.y * physical_size.y / virtual_size.y}; } -static void applyProjectionMatrix(glm::vec2 virtual_size, glm::vec2 translation) -{ - // Simple pan reprojection of translated virtual coordinates to pixels via - // shader. - // Not sure if this will conflict with other shader operations! - glm::mat3 m{1.0f}; - m[0][0] = 2.0f / virtual_size.x; - m[1][1] = -2.0f / virtual_size.y; - m[2][0] = -1.0f + (2.0f * translation.x / virtual_size.x); - m[2][1] = 1.0f - (2.0f * translation.y / virtual_size.y); - glUniformMatrix3fv(shader->getUniformLocation("u_projection"), 1, GL_FALSE, glm::value_ptr(m)); -} - void RenderTarget::pushClipRegion(sp::Rect virtual_rect) { // Flush geometry finish(); - // Apply the current translation (if any) so the clipped region aligns to - // the translated visual position. - glm::vec2 adj = virtual_rect.position + current_translation; - glm::ivec2 px_min = virtualToPixelPosition(adj); - glm::ivec2 px_max = virtualToPixelPosition(adj + virtual_rect.size); + glm::ivec2 px_min = virtualToPixelPosition(virtual_rect.position); + glm::ivec2 px_max = virtualToPixelPosition(virtual_rect.position + virtual_rect.size); GLint px = px_min.x; GLint py = px_min.y; GLint pw = px_max.x - px_min.x; @@ -1323,40 +1305,4 @@ void RenderTarget::popClipRegion() } } -void RenderTarget::pushTranslation(glm::vec2 offset) -{ - // Flush geometry - finish(); - - // Push a translation onto the stack and add it to the current translation - // value. - translation_stack.push_back(offset); - current_translation += offset; - - // Reproject with the new translation value. - applyProjectionMatrix(virtual_size, current_translation); -} - -void RenderTarget::popTranslation() -{ - // Flush geometry - finish(); - - // Pop a translation off the stack (if there is one) and remove it from the - // current translation value. - if (!translation_stack.empty()) - { - current_translation -= translation_stack.back(); - translation_stack.pop_back(); - } - - // Reproject with the new translation value. - applyProjectionMatrix(virtual_size, current_translation); -} - -glm::vec2 RenderTarget::getTranslation() const -{ - return current_translation; -} - } diff --git a/src/graphics/renderTarget.h b/src/graphics/renderTarget.h index 31256dd4..29dde8ef 100644 --- a/src/graphics/renderTarget.h +++ b/src/graphics/renderTarget.h @@ -77,16 +77,6 @@ class RenderTarget : sp::NonCopyable // Remove a nested rect from the clip region. void popClipRegion(); - // Functions for translating nested renders between virtual and real pixels. - - // Stack nested translation vectors to determine their offsets relative to - // the container and each other. - void pushTranslation(glm::vec2 virtual_offset); - // Remove a nested translation vector. - void popTranslation(); - // Get the total translation vector from the top of the stack. - glm::vec2 getTranslation() const; - struct VertexData { glm::vec2 position;