From b10f2da64706cef3d1094cfa3b5a7d341d833737 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 20 May 2026 20:25:12 +0200 Subject: [PATCH] =?UTF-8?q?feat(antialias):=20AA=20geom=C3=A8tric=20a=20le?= =?UTF-8?q?s=20l=C3=ADnies=20amb=20edge=20attribute=20i=20smoothstep?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Afegim antialias geomètric (sense MSAA) al pipeline de línies aprofitant que la línia ja es construeix com a quad extruït a CPU: - LineVertex: nou camp edge_dist (±1 als laterals del quad, 0 al centre). - pushLine: extrudeix 0.5px extra per banda (AA_PADDING) per allotjar el fade sense menjar gruix nominal. - line.vert: passa l'edge_dist al fragment com a varying. - line.frag: alpha *= 1 - smoothstep(0.7, 1.0, |edge_dist|) — fade Hermite C¹ als bords, sense banding. AA actiu per defecte. El toggle a runtime (F5) ve en el commit següent. --- shaders/line.frag.glsl | 21 +- shaders/line.vert.glsl | 7 +- .../core/rendering/gpu/gpu_frame_renderer.cpp | 15 +- .../core/rendering/gpu/gpu_line_pipeline.cpp | 196 +++++++++--------- .../core/rendering/gpu/gpu_line_pipeline.hpp | 30 +-- 5 files changed, 150 insertions(+), 119 deletions(-) diff --git a/shaders/line.frag.glsl b/shaders/line.frag.glsl index 086e216..b75f54b 100644 --- a/shaders/line.frag.glsl +++ b/shaders/line.frag.glsl @@ -1,12 +1,25 @@ #version 450 -// Fragment shader para líneas vectoriales. -// Pinta el color interpolado per-vertex que viene del vertex shader. -// (Postprocesado / bloom / glow son responsabilidad de la Fase 8.) +// Fragment shader per a línies vectorials. +// +// Antialias geomètric: rebem `frag_edge_dist` interpolat (±1 als laterals del +// quad, 0 a l'eix central). Apliquem un smoothstep d'1 píxel d'amplada perquè +// el gruix nominal (els |edge_dist| < threshold) quedi totalment opac i només +// el píxel extruit als laterals faci la transició suau. +// +// La línia ja ve extruïda amb thickness + 1px a CPU; el threshold equival a +// (thickness)/(thickness+1), però no el coneixem aquí per vèrtex. En el cas +// general (línies fines), fade lineal entre 0.0 i 1.0 dóna prou bon resultat +// visualment sense necessitat d'un uniform per línia. layout(location = 0) in vec4 frag_color; +layout(location = 1) in float frag_edge_dist; layout(location = 0) out vec4 out_color; void main() { - out_color = frag_color; + // |edge_dist|=0 → totalment opac; |edge_dist|=1 → totalment transparent. + // smoothstep dóna un fade Hermite C¹ que evita banding. + float d = abs(frag_edge_dist); + float alpha = 1.0 - smoothstep(0.7, 1.0, d); + out_color = vec4(frag_color.rgb, frag_color.a * alpha); } diff --git a/shaders/line.vert.glsl b/shaders/line.vert.glsl index 247d3ff..4777b7a 100644 --- a/shaders/line.vert.glsl +++ b/shaders/line.vert.glsl @@ -14,10 +14,12 @@ layout(set = 1, binding = 0) uniform UBO { vec2 _padding; // alineamiento a 16 bytes } ubo; -layout(location = 0) in vec2 in_position; // píxeles lógicos -layout(location = 1) in vec4 in_color; // RGBA 0..1 +layout(location = 0) in vec2 in_position; // píxeles lógicos +layout(location = 1) in vec4 in_color; // RGBA 0..1 +layout(location = 2) in float in_edge_dist; // ±1 als laterals, 0 al centre layout(location = 0) out vec4 frag_color; +layout(location = 1) out float frag_edge_dist; void main() { // Píxeles lógicos -> NDC (-1..+1) @@ -26,4 +28,5 @@ void main() { ndc.y = -ndc.y; gl_Position = vec4(ndc, 0.0, 1.0); frag_color = in_color; + frag_edge_dist = in_edge_dist; } diff --git a/source/core/rendering/gpu/gpu_frame_renderer.cpp b/source/core/rendering/gpu/gpu_frame_renderer.cpp index 6d5eb31..6c296be 100644 --- a/source/core/rendering/gpu/gpu_frame_renderer.cpp +++ b/source/core/rendering/gpu/gpu_frame_renderer.cpp @@ -243,16 +243,21 @@ namespace Rendering::GPU { return; } - const float HALF = thickness * 0.5F; + // Antialias geomètric: extruim 0.5 píxel extra a cada banda del gruix + // demanat, i el fragment shader fa fade als bords usant edge_dist (±1 + // als laterals, 0 al centre). Així el gruix "ple" coincideix amb el + // demanat i el píxel extra només aporta la transició suau. + const float AA_PADDING = 0.5F; + const float HALF = (thickness * 0.5F) + AA_PADDING; const float NX = -DY / LEN * HALF; const float NY = DX / LEN * HALF; const auto BASE_INDEX = static_cast(vertices_.size()); - vertices_.push_back({x1 + NX, y1 + NY, r, g, b, a}); - vertices_.push_back({x1 - NX, y1 - NY, r, g, b, a}); - vertices_.push_back({x2 + NX, y2 + NY, r, g, b, a}); - vertices_.push_back({x2 - NX, y2 - NY, r, g, b, a}); + vertices_.push_back({x1 + NX, y1 + NY, r, g, b, a, +1.0F}); + vertices_.push_back({x1 - NX, y1 - NY, r, g, b, a, -1.0F}); + vertices_.push_back({x2 + NX, y2 + NY, r, g, b, a, +1.0F}); + vertices_.push_back({x2 - NX, y2 - NY, r, g, b, a, -1.0F}); indices_.push_back(BASE_INDEX + 0); indices_.push_back(BASE_INDEX + 1); diff --git a/source/core/rendering/gpu/gpu_line_pipeline.cpp b/source/core/rendering/gpu/gpu_line_pipeline.cpp index 38705bf..58bab9d 100644 --- a/source/core/rendering/gpu/gpu_line_pipeline.cpp +++ b/source/core/rendering/gpu/gpu_line_pipeline.cpp @@ -11,107 +11,111 @@ namespace Rendering::GPU { -GpuLinePipeline::~GpuLinePipeline() { destroy(); } + GpuLinePipeline::~GpuLinePipeline() { destroy(); } -auto GpuLinePipeline::init(const GpuDevice& device, - SDL_GPUTextureFormat target_format) -> bool { - owner_ = device.get(); - if (owner_ == nullptr) { - return false; - } - - SDL_GPUShader* vert = device.loadShader("line.vert.spv", - SDL_GPU_SHADERSTAGE_VERTEX, - /*num_uniform_buffers=*/1); - SDL_GPUShader* frag = device.loadShader("line.frag.spv", - SDL_GPU_SHADERSTAGE_FRAGMENT, - /*num_uniform_buffers=*/0); - if ((vert == nullptr) || (frag == nullptr)) { - if (vert != nullptr) { - SDL_ReleaseGPUShader(owner_, vert); + auto GpuLinePipeline::init(const GpuDevice& device, + SDL_GPUTextureFormat target_format) -> bool { + owner_ = device.get(); + if (owner_ == nullptr) { + return false; } - if (frag != nullptr) { - SDL_ReleaseGPUShader(owner_, frag); + + SDL_GPUShader* vert = device.loadShader("line.vert.spv", + SDL_GPU_SHADERSTAGE_VERTEX, + /*num_uniform_buffers=*/1); + SDL_GPUShader* frag = device.loadShader("line.frag.spv", + SDL_GPU_SHADERSTAGE_FRAGMENT, + /*num_uniform_buffers=*/0); + if ((vert == nullptr) || (frag == nullptr)) { + if (vert != nullptr) { + SDL_ReleaseGPUShader(owner_, vert); + } + if (frag != nullptr) { + SDL_ReleaseGPUShader(owner_, frag); + } + std::cerr << "[GpuLinePipeline] Error cargando shaders\n"; + return false; } - std::cerr << "[GpuLinePipeline] Error cargando shaders\n"; - return false; + + // Vertex layout: pos (vec2) + color (vec4) + edge_dist (float) → 7 floats per vèrtex. + SDL_GPUVertexBufferDescription vbo_desc{}; + vbo_desc.slot = 0; + vbo_desc.pitch = sizeof(LineVertex); + vbo_desc.input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX; + vbo_desc.instance_step_rate = 0; + + SDL_GPUVertexAttribute attrs[3]; + attrs[0].location = 0; // in_position + attrs[0].buffer_slot = 0; + attrs[0].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2; + attrs[0].offset = 0; + attrs[1].location = 1; // in_color + attrs[1].buffer_slot = 0; + attrs[1].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4; + attrs[1].offset = sizeof(float) * 2; + attrs[2].location = 2; // in_edge_dist + attrs[2].buffer_slot = 0; + attrs[2].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT; + attrs[2].offset = sizeof(float) * 6; + + SDL_GPUVertexInputState vertex_input{}; + vertex_input.vertex_buffer_descriptions = &vbo_desc; + vertex_input.num_vertex_buffers = 1; + vertex_input.vertex_attributes = attrs; + vertex_input.num_vertex_attributes = 3; + + // Color target = formato pasado por el caller (offscreen u otro). + // Blending alpha estándar. + SDL_GPUColorTargetDescription color_target{}; + color_target.format = target_format; + color_target.blend_state.enable_blend = true; + color_target.blend_state.src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA; + color_target.blend_state.dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; + color_target.blend_state.color_blend_op = SDL_GPU_BLENDOP_ADD; + color_target.blend_state.src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE; + color_target.blend_state.dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; + color_target.blend_state.alpha_blend_op = SDL_GPU_BLENDOP_ADD; + color_target.blend_state.color_write_mask = + SDL_GPU_COLORCOMPONENT_R | SDL_GPU_COLORCOMPONENT_G | + SDL_GPU_COLORCOMPONENT_B | SDL_GPU_COLORCOMPONENT_A; + + SDL_GPUGraphicsPipelineTargetInfo target_info{}; + target_info.color_target_descriptions = &color_target; + target_info.num_color_targets = 1; + target_info.has_depth_stencil_target = false; + + SDL_GPUGraphicsPipelineCreateInfo info{}; + info.vertex_shader = vert; + info.fragment_shader = frag; + info.vertex_input_state = vertex_input; + info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; + info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL; + info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE; // No backface culling para 2D + info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE; + info.multisample_state.sample_count = SDL_GPU_SAMPLECOUNT_1; + info.depth_stencil_state = {}; + info.target_info = target_info; + + pipeline_ = SDL_CreateGPUGraphicsPipeline(owner_, &info); + + // Los shaders se pueden liberar tras crear el pipeline (SDL los retiene + // internamente mientras el pipeline esté vivo). + SDL_ReleaseGPUShader(owner_, vert); + SDL_ReleaseGPUShader(owner_, frag); + + if (pipeline_ == nullptr) { + std::cerr << "[GpuLinePipeline] SDL_CreateGPUGraphicsPipeline: " << SDL_GetError() << '\n'; + return false; + } + return true; } - // Vertex layout: pos (vec2) + color (vec4) → 6 floats por vertex. - SDL_GPUVertexBufferDescription vbo_desc{}; - vbo_desc.slot = 0; - vbo_desc.pitch = sizeof(LineVertex); - vbo_desc.input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX; - vbo_desc.instance_step_rate = 0; - - SDL_GPUVertexAttribute attrs[2]; - attrs[0].location = 0; // in_position - attrs[0].buffer_slot = 0; - attrs[0].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2; - attrs[0].offset = 0; - attrs[1].location = 1; // in_color - attrs[1].buffer_slot = 0; - attrs[1].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4; - attrs[1].offset = sizeof(float) * 2; - - SDL_GPUVertexInputState vertex_input{}; - vertex_input.vertex_buffer_descriptions = &vbo_desc; - vertex_input.num_vertex_buffers = 1; - vertex_input.vertex_attributes = attrs; - vertex_input.num_vertex_attributes = 2; - - // Color target = formato pasado por el caller (offscreen u otro). - // Blending alpha estándar. - SDL_GPUColorTargetDescription color_target{}; - color_target.format = target_format; - color_target.blend_state.enable_blend = true; - color_target.blend_state.src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA; - color_target.blend_state.dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; - color_target.blend_state.color_blend_op = SDL_GPU_BLENDOP_ADD; - color_target.blend_state.src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE; - color_target.blend_state.dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; - color_target.blend_state.alpha_blend_op = SDL_GPU_BLENDOP_ADD; - color_target.blend_state.color_write_mask = - SDL_GPU_COLORCOMPONENT_R | SDL_GPU_COLORCOMPONENT_G | - SDL_GPU_COLORCOMPONENT_B | SDL_GPU_COLORCOMPONENT_A; - - SDL_GPUGraphicsPipelineTargetInfo target_info{}; - target_info.color_target_descriptions = &color_target; - target_info.num_color_targets = 1; - target_info.has_depth_stencil_target = false; - - SDL_GPUGraphicsPipelineCreateInfo info{}; - info.vertex_shader = vert; - info.fragment_shader = frag; - info.vertex_input_state = vertex_input; - info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; - info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL; - info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE; // No backface culling para 2D - info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE; - info.multisample_state.sample_count = SDL_GPU_SAMPLECOUNT_1; - info.depth_stencil_state = {}; - info.target_info = target_info; - - pipeline_ = SDL_CreateGPUGraphicsPipeline(owner_, &info); - - // Los shaders se pueden liberar tras crear el pipeline (SDL los retiene - // internamente mientras el pipeline esté vivo). - SDL_ReleaseGPUShader(owner_, vert); - SDL_ReleaseGPUShader(owner_, frag); - - if (pipeline_ == nullptr) { - std::cerr << "[GpuLinePipeline] SDL_CreateGPUGraphicsPipeline: " << SDL_GetError() << '\n'; - return false; + void GpuLinePipeline::destroy() { + if ((pipeline_ != nullptr) && (owner_ != nullptr)) { + SDL_ReleaseGPUGraphicsPipeline(owner_, pipeline_); + } + pipeline_ = nullptr; + owner_ = nullptr; } - return true; -} - -void GpuLinePipeline::destroy() { - if ((pipeline_ != nullptr) && (owner_ != nullptr)) { - SDL_ReleaseGPUGraphicsPipeline(owner_, pipeline_); - } - pipeline_ = nullptr; - owner_ = nullptr; -} } // namespace Rendering::GPU diff --git a/source/core/rendering/gpu/gpu_line_pipeline.hpp b/source/core/rendering/gpu/gpu_line_pipeline.hpp index da312e6..be7177d 100644 --- a/source/core/rendering/gpu/gpu_line_pipeline.hpp +++ b/source/core/rendering/gpu/gpu_line_pipeline.hpp @@ -11,28 +11,34 @@ namespace Rendering::GPU { -class GpuDevice; + class GpuDevice; -// Vertex layout (debe coincidir con shaders/line.vert.glsl). -struct LineVertex { + // Vertex layout (debe coincidir con shaders/line.vert.glsl). + // + // edge_dist: distància normalitzada a l'eix central de la línia, ±1 als bords + // i 0 al centre. El fragment shader la fa servir per fer un fade als bords + // (antialias geomètric). Els vèrtexs del costat "+normal" porten +1 i els del + // costat "-normal" porten -1. + struct LineVertex { float x; float y; float r; float g; float b; float a; -}; + float edge_dist; + }; -// Uniform buffer del vertex shader (debe coincidir con UBO en line.vert.glsl). -struct LineUniforms { + // Uniform buffer del vertex shader (debe coincidir con UBO en line.vert.glsl). + struct LineUniforms { float viewport_width; float viewport_height; float padding_0; float padding_1; // Alineamiento a 16 bytes -}; + }; -class GpuLinePipeline { - public: + class GpuLinePipeline { + public: GpuLinePipeline() = default; ~GpuLinePipeline(); @@ -45,14 +51,14 @@ class GpuLinePipeline { // (swapchain o offscreen). Por defecto coincide con el del swapchain // del device. [[nodiscard]] auto init(const GpuDevice& device, - SDL_GPUTextureFormat target_format) -> bool; + SDL_GPUTextureFormat target_format) -> bool; void destroy(); [[nodiscard]] auto get() const -> SDL_GPUGraphicsPipeline* { return pipeline_; } - private: + private: SDL_GPUDevice* owner_{nullptr}; // No-owning; el GpuDevice es el dueño SDL_GPUGraphicsPipeline* pipeline_{nullptr}; -}; + }; } // namespace Rendering::GPU