feat(antialias): AA geomètric a les línies amb edge attribute i smoothstep

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.
This commit is contained in:
2026-05-20 20:25:12 +02:00
parent 6063309932
commit b10f2da647
5 changed files with 150 additions and 119 deletions
+17 -4
View File
@@ -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);
}
+3
View File
@@ -16,8 +16,10 @@ layout(set = 1, binding = 0) uniform UBO {
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;
}
@@ -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<uint16_t>(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);
@@ -37,14 +37,14 @@ auto GpuLinePipeline::init(const GpuDevice& device,
return false;
}
// Vertex layout: pos (vec2) + color (vec4) → 6 floats por vertex.
// 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[2];
SDL_GPUVertexAttribute attrs[3];
attrs[0].location = 0; // in_position
attrs[0].buffer_slot = 0;
attrs[0].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
@@ -53,12 +53,16 @@ auto GpuLinePipeline::init(const GpuDevice& device,
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 = 2;
vertex_input.num_vertex_attributes = 3;
// Color target = formato pasado por el caller (offscreen u otro).
// Blending alpha estándar.
@@ -14,6 +14,11 @@ namespace Rendering::GPU {
class GpuDevice;
// 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;
@@ -21,6 +26,7 @@ struct LineVertex {
float g;
float b;
float a;
float edge_dist;
};
// Uniform buffer del vertex shader (debe coincidir con UBO en line.vert.glsl).