From 7b3ee506ac5249c091f58dd4d1c305d2231d58ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= <49535803+damacaa@users.noreply.github.com> Date: Wed, 29 Apr 2026 20:00:10 +0200 Subject: [PATCH 1/7] Implemented a uniform grid to speed up distance rendering dramatically --- .../include/MouseCollisionScene.h | 2 +- .../systems/2DSDFShaderGenerationSystem.h | 4 +- .../weird-renderer/core/SDF2DRenderPipeline.h | 16 +- include/weird-renderer/resources/DataBuffer.h | 56 +++- include/weird-renderer/resources/Shader.h | 5 + src/weird-renderer/core/Renderer.cpp | 8 +- .../core/SDF2DRenderPipeline.cpp | 257 +++++++++++++++--- .../shaders/2d/sdf_distance.frag | 46 ++-- 8 files changed, 332 insertions(+), 62 deletions(-) diff --git a/examples/sample-scenes/include/MouseCollisionScene.h b/examples/sample-scenes/include/MouseCollisionScene.h index 7ca3e25c..2f900df8 100644 --- a/examples/sample-scenes/include/MouseCollisionScene.h +++ b/examples/sample-scenes/include/MouseCollisionScene.h @@ -20,7 +20,7 @@ class MouseCollisionScene : public Scene m_debugInput = true; m_debugFly = true; - for (size_t i = 0; i < 600; i++) + for (size_t i = 0; i < 9900; i++) { float y = (int)(i / 20); diff --git a/include/weird-engine/systems/2DSDFShaderGenerationSystem.h b/include/weird-engine/systems/2DSDFShaderGenerationSystem.h index 2a67f691..573c66c2 100644 --- a/include/weird-engine/systems/2DSDFShaderGenerationSystem.h +++ b/include/weird-engine/systems/2DSDFShaderGenerationSystem.h @@ -107,8 +107,8 @@ namespace WeirdEngine::SDFShaderGenerationSystem2D oss << "int idx = dataOffset + " << 2 * i << ";\n"; // Fetch parameters - oss << "vec4 parameters0 = texelFetch(t_shapeBuffer, ivec2(idx, 0), 0);\n"; - oss << "vec4 parameters1 = texelFetch(t_shapeBuffer, ivec2(idx + 1, 0), 0);\n"; + oss << "vec4 parameters0 = texelFetch(t_shapeBuffer, ivec2(idx % 16384, idx / 16384), 0);\n"; + oss << "vec4 parameters1 = texelFetch(t_shapeBuffer, ivec2((idx + 1) % 16384, (idx + 1) / 16384), 0);\n"; // Get distance function auto fragmentCode = sdfs[shape.distanceFieldId]->print(); diff --git a/include/weird-renderer/core/SDF2DRenderPipeline.h b/include/weird-renderer/core/SDF2DRenderPipeline.h index df0b59c1..10a8a11b 100644 --- a/include/weird-renderer/core/SDF2DRenderPipeline.h +++ b/include/weird-renderer/core/SDF2DRenderPipeline.h @@ -7,6 +7,7 @@ #include "weird-renderer/resources/Shader.h" #include "weird-renderer/resources/Texture.h" #include "weird-renderer/scene/Camera.h" +#include namespace WeirdEngine { @@ -107,6 +108,16 @@ namespace WeirdEngine RenderTarget m_litSceneRender; DataBuffer m_shapeDataBuffer; + DataBuffer m_gridHeaderBuffer; + DataBuffer m_gridIndicesBuffer; + + // Reusable scratch buffers for grid building — allocated once, reused every frame + struct ObjBounds { int minX, minY, maxX, maxY; }; + std::vector m_gridObjBounds; + std::vector m_gridCellCounts; + std::vector m_gridCellOffsets; + std::vector m_gridHeader; + std::vector m_gridIndices; glm::mat4 m_oldCameraMatrix; glm::mat4 @@ -115,8 +126,9 @@ namespace WeirdEngine bool horizontal = true; glm::vec3 cameraPositionChange; - - void renderDistanceField(vec4* shapeData, uint32_t dataSize, uint32_t shapeCount, const Camera& camera, + struct GridInfo { float minX, minY, stepX, stepY; int gridCols, gridRows, indexTexWidth, indexTexHeight; }; + GridInfo buildAccelerationGrid(vec4* shapeData, uint32_t dataSize, uint32_t shapeCount, + const Camera& camera); void renderDistanceField(vec4* shapeData, uint32_t dataSize, uint32_t shapeCount, const Camera& camera, double time, double delta); void applyJumpFloodCorrection(double time); void upscaleDistance(); diff --git a/include/weird-renderer/resources/DataBuffer.h b/include/weird-renderer/resources/DataBuffer.h index 77b2823c..81208227 100644 --- a/include/weird-renderer/resources/DataBuffer.h +++ b/include/weird-renderer/resources/DataBuffer.h @@ -1,5 +1,7 @@ #pragma once #include +#include +#include #include namespace WeirdEngine @@ -40,14 +42,43 @@ namespace WeirdEngine if (byteSize == 0 || (byteSize % texelBytes != 0)) return; - GLsizei width = static_cast(byteSize / texelBytes); + GLint maxTexSize = 16384; + // glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxTexSize); + + size_t totalTexels = byteSize / texelBytes; + GLsizei texW, texH; + + auto s = static_cast(totalTexels); + if (s <= maxTexSize) + { + texW = static_cast(totalTexels); + texH = 1; + } + else + { + // Wrap into 2D: use maxTexSize as row width, add enough rows + texW = static_cast(maxTexSize); + texH = static_cast((totalTexels + maxTexSize - 1) / maxTexSize); + } + + // When wrapping to 2D, texW*texH may exceed totalTexels (due to ceiling + // division). Pad with zeros so OpenGL never reads past the source buffer. + const void* uploadPtr = data; + std::vector paddedData; + size_t paddedTexels = static_cast(texW) * static_cast(texH); + if (paddedTexels > totalTexels) + { + paddedData.resize(paddedTexels * 4, 0.0f); // 4 floats per RGBA32F texel + std::memcpy(paddedData.data(), data, byteSize); + uploadPtr = paddedData.data(); + } GLint previousActiveTexture = 0; GLint previousTextureBinding2D = 0; glGetIntegerv(GL_ACTIVE_TEXTURE, &previousActiveTexture); glGetIntegerv(GL_TEXTURE_BINDING_2D, &previousTextureBinding2D); glBindTexture(GL_TEXTURE_2D, m_texture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, 1, 0, GL_RGBA, GL_FLOAT, data); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, texW, texH, 0, GL_RGBA, GL_FLOAT, uploadPtr); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); @@ -61,6 +92,27 @@ namespace WeirdEngine uploadRawData(data, count * sizeof(T)); } + template void uploadData2D(const T* data, size_t width, size_t height) const + { + size_t byteSize = width * height * sizeof(T); + const size_t texelBytes = 4 * sizeof(float); // one RGBA32F texel + if (byteSize == 0 || (byteSize % texelBytes != 0)) + return; + + GLint previousActiveTexture = 0; + GLint previousTextureBinding2D = 0; + glGetIntegerv(GL_ACTIVE_TEXTURE, &previousActiveTexture); + glGetIntegerv(GL_TEXTURE_BINDING_2D, &previousTextureBinding2D); + glBindTexture(GL_TEXTURE_2D, m_texture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, static_cast(width), static_cast(height), 0, GL_RGBA, GL_FLOAT, data); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glBindTexture(GL_TEXTURE_2D, static_cast(previousTextureBinding2D)); + glActiveTexture(static_cast(previousActiveTexture)); + } + GLuint getTexture() const { return m_texture; diff --git a/include/weird-renderer/resources/Shader.h b/include/weird-renderer/resources/Shader.h index b3e80b08..30158168 100644 --- a/include/weird-renderer/resources/Shader.h +++ b/include/weird-renderer/resources/Shader.h @@ -54,6 +54,11 @@ namespace WeirdEngine glUniform1i(getUniformLocation(name), value); } + void setUniform(const std::string& name, const glm::ivec2& value) const + { + glUniform2iv(getUniformLocation(name), 1, &value[0]); + } + void setUniform(const std::string& name, const glm::vec2& value) const { glUniform2fv(getUniformLocation(name), 1, &value[0]); diff --git a/src/weird-renderer/core/Renderer.cpp b/src/weird-renderer/core/Renderer.cpp index 5cc2f0e7..b8b95844 100644 --- a/src/weird-renderer/core/Renderer.cpp +++ b/src/weird-renderer/core/Renderer.cpp @@ -116,6 +116,7 @@ namespace WeirdEngine auto& result = renderScene(scene, time, clampedDelta); output(scene, result, clampedDelta); + glFinish(); { PROFILE_SCOPE("Synchronization"); @@ -273,9 +274,11 @@ namespace WeirdEngine bool enable3DSDFs = true; - // Render geometry + // 3D if (enable3D) { + PROFILE_SCOPE("3D Render"); + auto& renderQueue = scene.getDrawQueue(); // TODO: sort and then draw it // Set up framebuffer for 3D scene rendering @@ -330,6 +333,7 @@ namespace WeirdEngine // Draw the render plane with ray marching shader m_renderPlane.draw(m_3DsdfShaderProgram); + glFinish(); } // Render 3D geometry objects (with depth writing) @@ -347,6 +351,8 @@ namespace WeirdEngine // Draw objects in the scene (3D models) scene.renderModels(m_3DSceneRender, m_geometryShaderProgram, m_instancedGeometryShaderProgram); } + + glFinish(); if (!enable2D) { diff --git a/src/weird-renderer/core/SDF2DRenderPipeline.cpp b/src/weird-renderer/core/SDF2DRenderPipeline.cpp index 011f8065..9ce38031 100644 --- a/src/weird-renderer/core/SDF2DRenderPipeline.cpp +++ b/src/weird-renderer/core/SDF2DRenderPipeline.cpp @@ -1,5 +1,7 @@ #include "weird-renderer/core/SDF2DRenderPipeline.h" #include "weird-engine/vec.h" +#include "weird-engine/Profiler.h" + #include namespace WeirdEngine @@ -289,46 +291,233 @@ namespace WeirdEngine renderBackground(camera, time); applyLighting(camera, time, backgroundTexture); + glFinish(); // Makes sense for profiler + return m_litSceneTexture; } + SDF2DRenderPipeline::GridInfo SDF2DRenderPipeline::buildAccelerationGrid(vec4* shapeData, uint32_t dataSize, + uint32_t shapeCount, + const Camera& camera) + { + PROFILE_SCOPE(m_config.isUI ? "Build grid (UI)" : "Build grid (World)"); + + // Build acceleration grid using CPU padding spatial partitioning. + // Grid bounds are derived entirely from the camera frustum, so off-screen circles are + // never inserted. This relies on the 2D camera convention where: + // zoom = -view[3].z (positive distance from origin) + // camXY = view[3].xy (camera translation in world space) + // If the camera representation changes, update the frustum derivation below accordingly. + constexpr int indexTexWidth = 1024; + + int objectCount = static_cast(dataSize) - (2 * static_cast(shapeCount)); + + float objectRadius = m_config.isUI ? 5.0f : 1.0f; + float cellPad = m_config.ballK + objectRadius; + + // Derive the visible world-space rectangle from the camera matrix + float zoom = -camera.view[3].z; + float camX = camera.view[3].x; + float camY = camera.view[3].y; + float aspectRatio = static_cast(m_distanceSampleWidth) / static_cast(m_distanceSampleHeight); + float overscan = std::clamp(m_config.distanceOverscan, 0.0f, 0.5f); + + float uvXMin, uvYMin, uvXMax, uvYMax; + if (m_config.isUI) + { + uvXMin = 0.0f; + uvYMin = 0.0f; + uvXMax = 1.0f * aspectRatio; + uvYMax = 1.0f; + } + else + { + uvXMin = (-1.0f - overscan) * aspectRatio; + uvYMin = (-1.0f - overscan); + uvXMax = (1.0f + overscan) * aspectRatio; + uvYMax = (1.0f + overscan); + } + + float viewMinX = zoom * uvXMin - camX; + float viewMinY = zoom * uvYMin - camY; + float viewMaxX = zoom * uvXMax - camX; + float viewMaxY = zoom * uvYMax - camY; + if (viewMinX > viewMaxX) + std::swap(viewMinX, viewMaxX); + if (viewMinY > viewMaxY) + std::swap(viewMinY, viewMaxY); + + // Expand frustum by cellPad so circles that straddle the view edge are included + float minX = viewMinX - cellPad; + float minY = viewMinY - cellPad; + float maxX = viewMaxX + cellPad; + float maxY = viewMaxY + cellPad; + + float idealCellSize = cellPad; + if (idealCellSize < 0.5f) + idealCellSize = 0.5f; + int gridCols = static_cast(std::ceil((maxX - minX) / idealCellSize)); + int gridRows = static_cast(std::ceil((maxY - minY) / idealCellSize)); + + if (gridCols < 1) + gridCols = 1; + if (gridRows < 1) + gridRows = 1; + if (gridCols > 2048) + gridCols = 2048; + if (gridRows > 2048) + gridRows = 2048; + + float stepX = (maxX - minX) / gridCols; + float stepY = (maxY - minY) / gridRows; + + int numCells = gridCols * gridRows; + + // Reuse member scratch buffers — no heap allocation unless the scene grows + m_gridObjBounds.clear(); + m_gridObjBounds.resize(objectCount); + m_gridCellCounts.assign(numCells, 0); + + // Single pass: compute per-object cell ranges and tally cell counts. + // Objects whose cell range is entirely outside [0, gridCols/Rows) are naturally skipped. + int visibleObjects = 0; + for (int i = 0; i < objectCount; ++i) + { + float cx = shapeData[i].x; + float cy = shapeData[i].y; + // +1/-1: one extra cell of buffer so the SDF never drops close to 0 at the outer + // insertion boundary, preventing JFA from propagating a "ghost" surface into empty cells. + int minCellX = static_cast((cx - cellPad - minX) / stepX) - 1; + int minCellY = static_cast((cy - cellPad - minY) / stepY) - 1; + int maxCellX = static_cast((cx + cellPad - minX) / stepX) + 1; + int maxCellY = static_cast((cy + cellPad - minY) / stepY) + 1; + + // Cull objects entirely outside the frustum-aligned grid + if (maxCellX < 0 || maxCellY < 0 || minCellX >= gridCols || minCellY >= gridRows) + { + m_gridObjBounds[i] = {-1, -1, -1, -1}; + continue; + } + + minCellX = std::max(0, minCellX); + minCellY = std::max(0, minCellY); + maxCellX = std::min(gridCols - 1, maxCellX); + maxCellY = std::min(gridRows - 1, maxCellY); + + m_gridObjBounds[i] = {minCellX, minCellY, maxCellX, maxCellY}; + ++visibleObjects; + + for (int cyI = minCellY; cyI <= maxCellY; ++cyI) + for (int cxI = minCellX; cxI <= maxCellX; ++cxI) + m_gridCellCounts[cyI * gridCols + cxI]++; + } + + // Prefix sum to compute cell offsets + m_gridCellOffsets.resize(numCells); + int totalIndices = 0; + for (int i = 0; i < numCells; ++i) + { + m_gridCellOffsets[i] = totalIndices; + totalIndices += m_gridCellCounts[i]; + } + + // Pre-allocate index buffer at final padded size to avoid realloc during fill + int indexTexHeight = std::max(1, (totalIndices + indexTexWidth - 1) / indexTexWidth); + int paddedSize = indexTexWidth * indexTexHeight; + + m_gridHeader.resize(numCells); + m_gridIndices.assign(paddedSize, glm::vec4(0.0f)); + + // Fill index buffer using offset-as-write-cursor directly (no extra copy) + for (int i = 0; i < objectCount; ++i) + { + const ObjBounds& b = m_gridObjBounds[i]; + if (b.minX < 0) + continue; // culled + for (int cyI = b.minY; cyI <= b.maxY; ++cyI) + { + for (int cxI = b.minX; cxI <= b.maxX; ++cxI) + { + int cellIdx = cyI * gridCols + cxI; + m_gridIndices[m_gridCellOffsets[cellIdx]++] = + glm::vec4(static_cast(i), 0.0f, 0.0f, 0.0f); + } + } + } + + // Restore offsets for header (they were incremented above as write cursors) + int writePos = 0; + for (int i = 0; i < numCells; ++i) + { + m_gridHeader[i] = + glm::vec4(static_cast(writePos), static_cast(m_gridCellCounts[i]), 0.0f, 0.0f); + writePos += m_gridCellCounts[i]; + } + + return {minX, minY, stepX, stepY, gridCols, gridRows, indexTexWidth, indexTexHeight}; + } + void SDF2DRenderPipeline::renderDistanceField(vec4* shapeData, uint32_t dataSize, uint32_t shapeCount, const Camera& camera, double time, double delta) { - int previousDistanceIndex = m_distanceTextureDoubleBufferIdx; - m_distanceTextureDoubleBufferIdx = (m_distanceTextureDoubleBufferIdx + 1) % 2; - - m_distanceTextureDoubleBuffer[m_distanceTextureDoubleBufferIdx]->bind(); - m_distanceShader.use(); - - // Set uniforms - m_prevFrameCameraMatrix = - m_oldCameraMatrix; // save before overwrite so other shaders can access the true previous frame matrix - m_distanceShader.setUniform("u_camMatrix", camera.view); - m_distanceShader.setUniform("u_oldCamMatrix", m_oldCameraMatrix); - m_oldCameraMatrix = camera.view; - - cameraPositionChange = camera.position - m_lastCameraPosition; - m_lastCameraPosition = camera.position; - m_distanceShader.setUniform("u_camPositionChange", cameraPositionChange); - - m_distanceShader.setUniform("u_time", time); - m_distanceShader.setUniform("u_deltaTime", static_cast(delta)); - m_distanceShader.setUniform("u_resolution", glm::vec2(m_distanceSampleWidth, m_distanceSampleHeight)); - m_distanceShader.setUniform("u_overscan", std::clamp(m_config.distanceOverscan, 0.0f, 0.5f)); - m_distanceShader.setUniform("u_motionBlurBlendSpeed", m_config.motionBlurBlendSpeed); - m_distanceShader.setUniform("u_k", m_config.ballK); - - m_distanceShader.setUniform("t_colorTexture", 0); - m_distanceTextureDoubleBuffer[previousDistanceIndex]->getColorAttachment()->bind(0); - - m_distanceShader.setUniform("u_loadedObjects", (int)dataSize); - m_distanceShader.setUniform("u_customShapeCount", static_cast(shapeCount)); - m_shapeDataBuffer.uploadData(shapeData, dataSize); - m_distanceShader.setUniform("t_shapeBuffer", 1); - m_shapeDataBuffer.bind(1); - - m_renderPlane.draw(m_distanceShader); + { + PROFILE_SCOPE(m_config.isUI ? "Upload data (UI)" : "Upload data (World)"); + + int previousDistanceIndex = m_distanceTextureDoubleBufferIdx; + m_distanceTextureDoubleBufferIdx = (m_distanceTextureDoubleBufferIdx + 1) % 2; + + GridInfo grid = buildAccelerationGrid(shapeData, dataSize, shapeCount, camera); + m_gridHeaderBuffer.uploadData2D(m_gridHeader.data(), grid.gridCols, grid.gridRows); + m_gridIndicesBuffer.uploadData2D(m_gridIndices.data(), grid.indexTexWidth, + grid.indexTexHeight); + + m_distanceTextureDoubleBuffer[m_distanceTextureDoubleBufferIdx]->bind(); + m_distanceShader.use(); + + // Set uniforms + m_prevFrameCameraMatrix = m_oldCameraMatrix; // save before overwrite so other shaders can access the + // true previous frame matrix + m_distanceShader.setUniform("u_camMatrix", camera.view); + m_distanceShader.setUniform("u_oldCamMatrix", m_oldCameraMatrix); + m_oldCameraMatrix = camera.view; + + cameraPositionChange = camera.position - m_lastCameraPosition; + m_lastCameraPosition = camera.position; + m_distanceShader.setUniform("u_camPositionChange", cameraPositionChange); + + m_distanceShader.setUniform("u_time", time); + m_distanceShader.setUniform("u_deltaTime", static_cast(delta)); + m_distanceShader.setUniform("u_resolution", glm::vec2(m_distanceSampleWidth, m_distanceSampleHeight)); + m_distanceShader.setUniform("u_overscan", std::clamp(m_config.distanceOverscan, 0.0f, 0.5f)); + m_distanceShader.setUniform("u_motionBlurBlendSpeed", m_config.motionBlurBlendSpeed); + m_distanceShader.setUniform("u_k", m_config.ballK); + + m_distanceShader.setUniform("t_colorTexture", 0); + m_distanceTextureDoubleBuffer[previousDistanceIndex]->getColorAttachment()->bind(0); + + m_distanceShader.setUniform("u_loadedObjects", (int)dataSize); + m_distanceShader.setUniform("u_customShapeCount", static_cast(shapeCount)); + m_shapeDataBuffer.uploadData(shapeData, dataSize); + m_distanceShader.setUniform("t_shapeBuffer", 1); + m_shapeDataBuffer.bind(1); + + m_distanceShader.setUniform("u_gridBoundsMin", glm::vec2(grid.minX, grid.minY)); + m_distanceShader.setUniform("u_gridStep", glm::vec2(grid.stepX, grid.stepY)); + m_distanceShader.setUniform("u_gridCols", grid.gridCols); + m_distanceShader.setUniform("u_gridRows", grid.gridRows); + + m_distanceShader.setUniform("t_gridHeader", 2); + m_gridHeaderBuffer.bind(2); + m_distanceShader.setUniform("t_gridIndices", 3); + m_gridIndicesBuffer.bind(3); + } + + // Upload grid + { + PROFILE_SCOPE(m_config.isUI ? "Render distance (UI)" : "Render distance (World)"); + m_renderPlane.draw(m_distanceShader); + glFinish(); // Makes sense for profiler + } } void SDF2DRenderPipeline::applyJumpFloodCorrection(double time) diff --git a/src/weird-renderer/shaders/2d/sdf_distance.frag b/src/weird-renderer/shaders/2d/sdf_distance.frag index a81eaaab..74546dae 100644 --- a/src/weird-renderer/shaders/2d/sdf_distance.frag +++ b/src/weird-renderer/shaders/2d/sdf_distance.frag @@ -30,6 +30,13 @@ uniform sampler2D t_colorTexture; uniform int u_loadedObjects; uniform highp sampler2D t_shapeBuffer; +uniform highp sampler2D t_gridHeader; +uniform highp sampler2D t_gridIndices; + +uniform vec2 u_gridBoundsMin; +uniform vec2 u_gridStep; +uniform int u_gridCols; +uniform int u_gridRows; uniform vec2 u_resolution; uniform float u_overscan; @@ -109,40 +116,39 @@ vec3 getColor(vec2 p, vec2 uv) float inv_k = 1.0 / u_k; - for (int i = 0; i < u_loadedObjects - (2 * u_customShapeCount); i++) + ivec2 cellCoord = ivec2((p - u_gridBoundsMin) / u_gridStep); + cellCoord = clamp(cellCoord, ivec2(0), ivec2(u_gridCols - 1, u_gridRows - 1)); + + vec2 cellData = texelFetch(t_gridHeader, cellCoord, 0).xy; + int startIndex = int(cellData.x); + int count = int(cellData.y); + + for (int i = 0; i < count; i++) { - vec4 positionSizeMaterial = texelFetch(t_shapeBuffer, ivec2(i, 0), 0); + int flatIndex = startIndex + i; + ivec2 indexCoord = ivec2(flatIndex % 1024, flatIndex / 1024); + int objectIndex = int(texelFetch(t_gridIndices, indexCoord, 0).x); + + // Shape buffer wraps to 2D at 16384 texels wide (matches DataBuffer::uploadRawData on ES drivers) + vec4 positionSizeMaterial = texelFetch(t_shapeBuffer, ivec2(objectIndex % 16384, objectIndex / 16384), 0); int materialId = int(positionSizeMaterial.w); #ifdef UI_PIPELINE - float objectDist = shape_circle(p - positionSizeMaterial.xy, 5.0); + float objectDist = shape_circle(p - positionSizeMaterial.xy, 5.0); #else - float objectDist = shape_circle(p - positionSizeMaterial.xy); + float objectDist = shape_circle(p - positionSizeMaterial.xy); #endif - // Inside ball mask is set to 0 - mask = objectDist <= 0.0 ? -objectDist * 4.0 : mask; - // mask = objectDist <= 0 ? 1.0 : mask; + // Inside ball mask is set to 0 + mask = objectDist <= 0.0 ? -objectDist * 4.0 : mask; #ifdef BLEND_SHAPES - finalMaterialId = objectDist <= minColorDist ? materialId : finalMaterialId; - -#else - - finalMaterialId = objectDist <= minDist ? materialId : finalMaterialId; - -#endif - -#ifdef BLEND_SHAPES - minDist = fOpUnionSoft(objectDist, minDist, u_k, inv_k); minColorDist = min(minColorDist, objectDist); - #else - + finalMaterialId = objectDist <= minDist ? materialId : finalMaterialId; minDist = min(minDist, objectDist); - #endif } From b77c53cfd2ac950c344e9a00e7fb8a9009b2ecc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= <49535803+damacaa@users.noreply.github.com> Date: Wed, 29 Apr 2026 20:00:16 +0200 Subject: [PATCH 2/7] Update Profiler.h --- include/weird-engine/Profiler.h | 47 +++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/include/weird-engine/Profiler.h b/include/weird-engine/Profiler.h index a2871a53..e104fde7 100644 --- a/include/weird-engine/Profiler.h +++ b/include/weird-engine/Profiler.h @@ -137,8 +137,18 @@ namespace WeirdEngine << std::setw(10) << "% Frame" << "\n"; std::cout << "-----------------------------------------------------------------\n"; - for (const auto& stat : m_stats) + printStatsRange(0, m_stats.size(), totalFrameTimeMs); + + std::cout << "=================================================================\n\n"; + } + + void printStatsRange(size_t start, size_t end, double totalFrameTimeMs) + { + size_t i = start; + while (i < end) { + const auto& stat = m_stats[i]; + std::string indent(stat.depth * 4, ' '); std::string name = indent + stat.name; double avgTimeMs = stat.totalTimeMs / stat.count; @@ -150,8 +160,41 @@ namespace WeirdEngine std::cout << std::left << std::setw(40) << name << std::right << std::setw(15) << std::fixed << std::setprecision(3) << avgTimeMs << std::setw(9) << std::fixed << std::setprecision(1) << percentage << "%\n"; + + // Find end of this stat's subtree + size_t subEnd = i + 1; + while (subEnd < end && m_stats[subEnd].depth > stat.depth) + subEnd++; + + if (subEnd > i + 1) + { + // Sum direct children total times + double childrenTotalMs = 0.0; + for (size_t j = i + 1; j < subEnd; j++) + { + if (m_stats[j].depth == stat.depth + 1) + childrenTotalMs += m_stats[j].totalTimeMs; + } + + // Print children recursively + printStatsRange(i + 1, subEnd, totalFrameTimeMs); + + // If unaccounted time exceeds 1% of frame, print an "Other" row + double unaccountedMs = stat.totalTimeMs - childrenTotalMs; + double unaccountedPct = totalFrameTimeMs > 0.0 ? (unaccountedMs / totalFrameTimeMs) * 100.0 : 0.0; + if (unaccountedPct > 1.0) + { + std::string otherIndent((stat.depth + 1) * 4, ' '); + std::string otherName = otherIndent + "Other"; + double otherAvgMs = unaccountedMs / stat.count; + std::cout << std::left << std::setw(40) << otherName << std::right << std::setw(15) + << std::fixed << std::setprecision(3) << otherAvgMs << std::setw(9) << std::fixed + << std::setprecision(1) << unaccountedPct << "%\n"; + } + } + + i = subEnd; } - std::cout << "=================================================================\n\n"; } struct StackItem From a0bf7e223e9d42a5283c6af69eb1a77a1b0f967a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= <49535803+damacaa@users.noreply.github.com> Date: Thu, 30 Apr 2026 18:15:47 +0200 Subject: [PATCH 3/7] Fixed distance debug --- src/weird-renderer/shaders/2d/lighting.frag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/weird-renderer/shaders/2d/lighting.frag b/src/weird-renderer/shaders/2d/lighting.frag index 7ba87e67..5144d7e3 100644 --- a/src/weird-renderer/shaders/2d/lighting.frag +++ b/src/weird-renderer/shaders/2d/lighting.frag @@ -312,7 +312,7 @@ void main() #endif // float debugDistance = 10.0 * distance; - float debugDistance = 0.5 * texture(t_distanceCorrectedTexture, screenUV).x; + float debugDistance = 0.5 * texture(t_distanceSampledTexture, screenUV).x; float value = 0.5 * (cos(500.0 * debugDistance) + 1.0); // value = debugDistance * 10.; From 53a459ae2de05ccf5602e6294cf950b5537ad33a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= <49535803+damacaa@users.noreply.github.com> Date: Fri, 1 May 2026 12:11:07 +0200 Subject: [PATCH 4/7] Debug grid --- .../weird-renderer/core/SDF2DRenderPipeline.h | 1 + .../core/SDF2DRenderPipeline.cpp | 6 ++++++ .../shaders/2d/sdf_distance.frag | 21 +++++++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/include/weird-renderer/core/SDF2DRenderPipeline.h b/include/weird-renderer/core/SDF2DRenderPipeline.h index 9779f67a..9a69bca4 100644 --- a/include/weird-renderer/core/SDF2DRenderPipeline.h +++ b/include/weird-renderer/core/SDF2DRenderPipeline.h @@ -37,6 +37,7 @@ namespace WeirdEngine float motionBlurBlendSpeed; bool debugDistanceField; bool debugMaterialColors; + bool debugGrid = false; float ballK; float ambienOcclusionRadius; float ambienOcclusionStrength; diff --git a/src/weird-renderer/core/SDF2DRenderPipeline.cpp b/src/weird-renderer/core/SDF2DRenderPipeline.cpp index 8f07737e..9a312aaa 100644 --- a/src/weird-renderer/core/SDF2DRenderPipeline.cpp +++ b/src/weird-renderer/core/SDF2DRenderPipeline.cpp @@ -767,6 +767,12 @@ namespace WeirdEngine else m_lightingShader.removeDefine("DEBUG_SHOW_COLORS"); } + if (ImGui::Checkbox("Show grid", &m_config.debugGrid)) + { + if (m_config.debugGrid) m_distanceShader.addDefine("DEBUG_SHOW_GRID"); + else m_distanceShader.removeDefine("DEBUG_SHOW_GRID"); + } + ImGui::SeparatorText("Ambient Occlusion"); ImGui::SliderFloat("AO Radius", &m_config.ambienOcclusionRadius, 0.0f, 20.0f); ImGui::SliderFloat("AO Strength", &m_config.ambienOcclusionStrength, 0.0f, 1.0f); diff --git a/src/weird-renderer/shaders/2d/sdf_distance.frag b/src/weird-renderer/shaders/2d/sdf_distance.frag index 74546dae..d81a5be6 100644 --- a/src/weird-renderer/shaders/2d/sdf_distance.frag +++ b/src/weird-renderer/shaders/2d/sdf_distance.frag @@ -7,6 +7,7 @@ precision highp sampler2D; // #define BLEND_SHAPES // #define MOTION_BLUR +// #define DEBUG_SHOW_GRID out vec4 FragColor; @@ -286,6 +287,26 @@ void main() // finalDistance = abs(finalDistance - pixelSize) - (pixelSize); #endif +#ifdef DEBUG_SHOW_GRID + vec2 localP = pos - u_gridBoundsMin; + bool inBounds = localP.x >= 0.0 && localP.y >= 0.0 && + localP.x < float(u_gridCols) * u_gridStep.x && + localP.y < float(u_gridRows) * u_gridStep.y; + if (inBounds) + { + vec2 fracLocal = mod(localP, u_gridStep); + vec2 distToLine = min(fracLocal, u_gridStep - fracLocal); + float gridDistWorld = min(distToLine.x, distToLine.y); + float gridDistNorm = (gridDistWorld / zoom) * (0.5 / aspectRatio); + float lineHalfWidth = 1.0 / min(u_resolution.x, u_resolution.y); + if (gridDistNorm < lineHalfWidth) + { + finalDistance = gridDistNorm - lineHalfWidth; + material = 16.0 - material; // Highlight out-of-bounds areas with a different color + } + } +#endif + FragColor = vec4(finalDistance, material, mask, 0.0); // This has no visual effect (multiplying by zero), but it forces From 5d7bacfac202078af4f9704c28a7f9a6237fbdf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= <49535803+damacaa@users.noreply.github.com> Date: Fri, 1 May 2026 12:16:45 +0200 Subject: [PATCH 5/7] More info on debug grid --- src/weird-renderer/shaders/2d/sdf_distance.frag | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/weird-renderer/shaders/2d/sdf_distance.frag b/src/weird-renderer/shaders/2d/sdf_distance.frag index d81a5be6..7d699e26 100644 --- a/src/weird-renderer/shaders/2d/sdf_distance.frag +++ b/src/weird-renderer/shaders/2d/sdf_distance.frag @@ -159,6 +159,10 @@ vec3 getColor(vec2 p, vec2 uv) minDist = min(minDist, 10.0); // Clamp max distance in UI mode #endif +#ifdef DEBUG_SHOW_GRID + finalMaterialId = count / 10; // Color cells based on how many objects they contain, for debugging purposes +#endif + return vec3(minDist, max(float(finalMaterialId), 0.0), mask); } @@ -302,7 +306,7 @@ void main() if (gridDistNorm < lineHalfWidth) { finalDistance = gridDistNorm - lineHalfWidth; - material = 16.0 - material; // Highlight out-of-bounds areas with a different color + // material = 16.0 - material; // Highlight out-of-bounds areas with a different color } } #endif From 73e5551fad1a3569c815656293d93a4706cb0675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= <49535803+damacaa@users.noreply.github.com> Date: Fri, 1 May 2026 12:17:42 +0200 Subject: [PATCH 6/7] Naming --- src/weird-renderer/shaders/2d/sdf_distance.frag | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/weird-renderer/shaders/2d/sdf_distance.frag b/src/weird-renderer/shaders/2d/sdf_distance.frag index 7d699e26..35300e3a 100644 --- a/src/weird-renderer/shaders/2d/sdf_distance.frag +++ b/src/weird-renderer/shaders/2d/sdf_distance.frag @@ -95,7 +95,7 @@ float sdPolygon(in vec2[SEGMENTS] v, in vec2 p) return s * sqrt(d); } -vec3 getColor(vec2 p, vec2 uv) +vec3 getDistanceMaterialMask(vec2 p, vec2 uv) { float minDist = 100000.0; float minColorDist = minDist; @@ -218,7 +218,7 @@ void main() float zoom = -u_camMatrix[3].z; vec2 pos = (zoom * uv) - u_camMatrix[3].xy; - vec3 result = getColor(pos, v_texCoord); + vec3 result = getDistanceMaterialMask(pos, v_texCoord); float d = result.x; #ifndef UI_PIPELINE From fad7be446207bcee83411040310cd58f7226e3e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= <49535803+damacaa@users.noreply.github.com> Date: Sat, 2 May 2026 00:06:53 +0200 Subject: [PATCH 7/7] Fixed grid artifacts --- include/weird-renderer/core/SDF2DRenderPipeline.h | 8 ++++---- src/weird-renderer/core/SDF2DRenderPipeline.cpp | 14 +++++++------- src/weird-renderer/shaders/2d/lighting.frag | 7 +++++-- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/include/weird-renderer/core/SDF2DRenderPipeline.h b/include/weird-renderer/core/SDF2DRenderPipeline.h index 9a69bca4..c8e6e1f3 100644 --- a/include/weird-renderer/core/SDF2DRenderPipeline.h +++ b/include/weird-renderer/core/SDF2DRenderPipeline.h @@ -128,10 +128,10 @@ namespace WeirdEngine bool horizontal = true; glm::vec3 cameraPositionChange; - struct GridInfo { float minX, minY, stepX, stepY; int gridCols, gridRows, indexTexWidth, indexTexHeight; }; - GridInfo buildAccelerationGrid(vec4* shapeData, uint32_t dataSize, uint32_t shapeCount, - const Camera& camera); void renderDistanceField(vec4* shapeData, uint32_t dataSize, uint32_t shapeCount, const Camera& camera, - double time, double delta); + struct GridInfo { float minX, minY, stepX, stepY; int gridCols, gridRows, indexTexWidth, indexTexHeight; }; + GridInfo buildAccelerationGrid(vec4* shapeData, uint32_t dataSize, uint32_t shapeCount, + const Camera& camera); void renderDistanceField(vec4* shapeData, uint32_t dataSize, uint32_t shapeCount, const Camera& camera, + double time, double delta); void applyJumpFloodCorrection(double time); void upscaleDistance(); void renderMaterialColors(const Camera& camera, double time, double delta); diff --git a/src/weird-renderer/core/SDF2DRenderPipeline.cpp b/src/weird-renderer/core/SDF2DRenderPipeline.cpp index 9a312aaa..ca1bf0fa 100644 --- a/src/weird-renderer/core/SDF2DRenderPipeline.cpp +++ b/src/weird-renderer/core/SDF2DRenderPipeline.cpp @@ -356,7 +356,9 @@ namespace WeirdEngine float maxX = viewMaxX + cellPad; float maxY = viewMaxY + cellPad; - float idealCellSize = cellPad; + // Cell size is the object diameter so a circle fits in ~1 cell (2x2 worst-case). + // ballK only affects the insertion radius, not the cell size. + float idealCellSize = 2.0f * objectRadius; if (idealCellSize < 0.5f) idealCellSize = 0.5f; int gridCols = static_cast(std::ceil((maxX - minX) / idealCellSize)); @@ -388,12 +390,10 @@ namespace WeirdEngine { float cx = shapeData[i].x; float cy = shapeData[i].y; - // +1/-1: one extra cell of buffer so the SDF never drops close to 0 at the outer - // insertion boundary, preventing JFA from propagating a "ghost" surface into empty cells. - int minCellX = static_cast((cx - cellPad - minX) / stepX) - 1; - int minCellY = static_cast((cy - cellPad - minY) / stepY) - 1; - int maxCellX = static_cast((cx + cellPad - minX) / stepX) + 1; - int maxCellY = static_cast((cy + cellPad - minY) / stepY) + 1; + int minCellX = static_cast((cx - cellPad - minX) / stepX); + int minCellY = static_cast((cy - cellPad - minY) / stepY); + int maxCellX = static_cast((cx + cellPad - minX) / stepX); + int maxCellY = static_cast((cy + cellPad - minY) / stepY); // Cull objects entirely outside the frustum-aligned grid if (maxCellX < 0 || maxCellY < 0 || minCellX >= gridCols || minCellY >= gridRows) diff --git a/src/weird-renderer/shaders/2d/lighting.frag b/src/weird-renderer/shaders/2d/lighting.frag index 5144d7e3..dda9cf18 100644 --- a/src/weird-renderer/shaders/2d/lighting.frag +++ b/src/weird-renderer/shaders/2d/lighting.frag @@ -190,8 +190,11 @@ void main() #ifdef ANTIALIASING - // Distance change over one pixel - float smoothing = 1.0 * fwidth(distance); + // Distance change over one pixel. + // Cap to ~1 pixel of distance in finalDistance space so fwidth discontinuities at + // empty grid cell boundaries don't widen the AA range and create false shape lines. + float maxSmoothing = 2.0 * overscanScale / u_resolution.y; + float smoothing = min(1.0 * fwidth(distance), maxSmoothing); // Convert the distance to a factor between 0.0 and 1.0 float shapeFactor = 1.0 - smoothstep(-smoothing, smoothing, distance);