2D-Bögen analytisch: exakter Kreis-Shader statt Segment-Tessellierung
Bögen im nativen 2D-wgpu-Renderer werden nicht mehr zoomabhängig in Segmente zerlegt, sondern per SDF-Fragment-Shader (ARC_WGSL) mathematisch exakt rund gerendert — bei jeder Zoomstufe ein echter Kreis, kein Vieleck, ohne Neu-Tessellierung. - compile_scene sammelt je Bogen EINE analytische Instanz (ArcInstanceData, Bildschirm-Raum-Parameter + Dash in Modell-Metern), zoom-invariant. - Eigene Arc-Pipeline (ein Frame-Uniform, Quad je Instanz aus vertex_index): radiale Kante, Butt-Cap-Winkelclamp (beide Sweep-Vorzeichen) und Dash (Bogenlänge modulo Muster) analytisch antialiased; Strichbreite mit derselben mm->px-Formel wie die Linien. - tessellate_arc + Zoom-Retessellierungs-Cache (last_scene/arc_px_per_m/ maybe_retessellate) entfernt; upload_scene ohne px_per_m. - Tests auf die neue Semantik umgeschrieben (Winkel-Parität, Bounding-Box, Dash-Mapping), ARC_WGSL per naga validiert.
This commit is contained in:
@@ -101,3 +101,164 @@ fn fs_main() -> @location(0) vec4<f32> {
|
||||
return globals.color;
|
||||
}
|
||||
"#;
|
||||
|
||||
/// Analytische BOGEN-Pipeline: rendert einen Kreisbogen mathematisch exakt (SDF im
|
||||
/// Fragment-Shader) statt als Segment-Kette — bei jedem Zoom ein "richtiger" Kreis,
|
||||
/// nie ein Vieleck.
|
||||
///
|
||||
/// Ein Bogen = EINE Instanz + EIN Quad (6 Vertices, aus `vertex_index` erzeugt).
|
||||
/// Das Quad ist die Bounding-Box des Bogens im BILDSCHIRM-Raum, im Vertex-Shader
|
||||
/// aus der aktuellen px-Skala aufgespannt (Radius + halbe Strichbreite + AA-Rand),
|
||||
/// sodass es bei jedem Zoom passt — OHNE Neu-Tessellierung.
|
||||
///
|
||||
/// Instanz-Layout (alles zoom-invariant, in Bildschirm-Raum bzw. Modell-Metern):
|
||||
/// @location(0) center : vec2 Mittelpunkt (Bildschirm-Raum)
|
||||
/// @location(1) geom : vec4 (r_screen, a0, sweep, r_model)
|
||||
/// @location(2) color : vec4 RGBA
|
||||
/// @location(3) wdash : vec4 (width_mm, dash_total[m], dash_count, _pad)
|
||||
/// @location(4) dash : vec4 bis zu MAX_ARC_DASH(=4) An/Aus-Laengen (Modell-m)
|
||||
///
|
||||
/// Frame-Uniform (`ArcGlobals`): view_proj, viewport_px, px_per_screen (== meet-
|
||||
/// Skala, Geraete-px je Bildschirm-Einheit) und stroke_scale (mm -> Geraete-px,
|
||||
/// dieselbe Formel wie die Linien, `ortho::mm_to_device_px`).
|
||||
///
|
||||
/// Fragment: `d = abs(length(p-center) - r)` gibt den Ring-Abstand; die Kante wird
|
||||
/// analytisch per `smoothstep` (~device-px) geglaettet (MSAA glaettet zusaetzlich).
|
||||
/// Der Winkel wird gegen [0, sweep] geklemmt (beide Sweep-Vorzeichen, sauberer
|
||||
/// Wrap) mit Butt-Cap an den Enden. Dash: Bogenlaenge s = theta_rel * r_model
|
||||
/// (Modell-Meter!) modulo Muster, weicher An/Aus-Uebergang.
|
||||
pub const ARC_WGSL: &str = r#"
|
||||
struct ArcGlobals {
|
||||
view_proj : mat4x4<f32>,
|
||||
viewport_px : vec2<f32>,
|
||||
px_per_screen : f32,
|
||||
stroke_scale : f32,
|
||||
};
|
||||
@group(0) @binding(0) var<uniform> g : ArcGlobals;
|
||||
|
||||
const PI : f32 = 3.14159265358979;
|
||||
// Bildschirm-Einheiten je Modell-Meter (== tessellate::PX_PER_M). Fest, weil die
|
||||
// Instanz-Geometrie bereits in Bildschirm-Raum vorliegt (to_screen skaliert *90).
|
||||
const PX_PER_M_2D : f32 = 90.0;
|
||||
|
||||
struct VsOut {
|
||||
@builtin(position) pos : vec4<f32>,
|
||||
@location(0) frag : vec2<f32>,
|
||||
@location(1) @interpolate(flat) center : vec2<f32>,
|
||||
@location(2) @interpolate(flat) geom : vec4<f32>,
|
||||
@location(3) @interpolate(flat) color : vec4<f32>,
|
||||
@location(4) @interpolate(flat) wdash : vec4<f32>,
|
||||
@location(5) @interpolate(flat) dash : vec4<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_main(
|
||||
@builtin(vertex_index) vidx : u32,
|
||||
@location(0) center : vec2<f32>,
|
||||
@location(1) geom : vec4<f32>,
|
||||
@location(2) color : vec4<f32>,
|
||||
@location(3) wdash : vec4<f32>,
|
||||
@location(4) dash : vec4<f32>,
|
||||
) -> VsOut {
|
||||
// Zwei Dreiecke, Ecken in {-1,+1}^2.
|
||||
var corners = array<vec2<f32>, 6>(
|
||||
vec2<f32>(-1.0, -1.0), vec2<f32>( 1.0, -1.0), vec2<f32>( 1.0, 1.0),
|
||||
vec2<f32>(-1.0, -1.0), vec2<f32>( 1.0, 1.0), vec2<f32>(-1.0, 1.0),
|
||||
);
|
||||
let corner = corners[vidx];
|
||||
|
||||
let r_s = geom.x;
|
||||
// Echte Papierbreite (Geraete-px) -> zurueck in Bildschirm-Einheiten fuer die
|
||||
// Quad-Groesse; plus AA-Rand (~2 px). px_per_screen gegen 0 sichern.
|
||||
let pps = max(g.px_per_screen, 1e-6);
|
||||
let width_px = max(0.6, wdash.x * g.stroke_scale);
|
||||
let half_w_screen = 0.5 * width_px / pps;
|
||||
let aa_screen = 2.0 / pps;
|
||||
let ext = r_s + half_w_screen + aa_screen;
|
||||
|
||||
let p_screen = center + corner * ext;
|
||||
|
||||
var out : VsOut;
|
||||
out.pos = g.view_proj * vec4<f32>(p_screen, 0.0, 1.0);
|
||||
out.frag = p_screen;
|
||||
out.center = center;
|
||||
out.geom = geom;
|
||||
out.color = color;
|
||||
out.wdash = wdash;
|
||||
out.dash = dash;
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
||||
let center = in.center;
|
||||
let r_s = in.geom.x;
|
||||
let a0 = in.geom.y;
|
||||
let sweep = in.geom.z;
|
||||
let r_m = in.geom.w;
|
||||
let width_mm = in.wdash.x;
|
||||
let dash_total = in.wdash.y;
|
||||
let dash_count = i32(in.wdash.z + 0.5);
|
||||
let pps = max(g.px_per_screen, 1e-6);
|
||||
|
||||
let rel = in.frag - center;
|
||||
let dist = length(rel);
|
||||
|
||||
// 1) Radiale Kante (Strichbreite quer zum Bogen), analytisch antialiased.
|
||||
let d_ring_px = abs(dist - r_s) * pps;
|
||||
let half_w_px = 0.5 * max(0.6, width_mm * g.stroke_scale);
|
||||
let cov_radial = 1.0 - smoothstep(half_w_px - 0.6, half_w_px + 0.6, d_ring_px);
|
||||
|
||||
// 2) Winkel-Clamp auf [min(0,sweep), max(0,sweep)] mit Butt-Cap an den Enden.
|
||||
let theta = atan2(rel.y, rel.x);
|
||||
var da = theta - a0;
|
||||
da = da - 2.0 * PI * round(da / (2.0 * PI)); // Wrap nach (-pi, pi]
|
||||
let lo = min(0.0, sweep);
|
||||
let hi = max(0.0, sweep);
|
||||
var sd : f32;
|
||||
if (da < lo) {
|
||||
sd = lo - da;
|
||||
} else if (da > hi) {
|
||||
sd = da - hi;
|
||||
} else {
|
||||
sd = -min(da - lo, hi - da);
|
||||
}
|
||||
let cap_px = sd * r_s * pps; // signierter Abstand zur Kappe (Geraete-px)
|
||||
let cov_cap = 1.0 - smoothstep(-0.5, 0.5, cap_px);
|
||||
|
||||
// 3) Dash: Bogenlaenge ab a0 (Modell-Meter) modulo Muster, weicher Uebergang.
|
||||
var cov_dash = 1.0;
|
||||
if (dash_count > 0 && dash_total > 1e-9) {
|
||||
var progress = da;
|
||||
if (sweep < 0.0) { progress = -da; }
|
||||
progress = max(progress, 0.0);
|
||||
let s_model = progress * r_m;
|
||||
let m = s_model - dash_total * floor(s_model / dash_total);
|
||||
|
||||
var cyc = array<f32, 4>(in.dash.x, in.dash.y, in.dash.z, in.dash.w);
|
||||
var acc = 0.0;
|
||||
var cur_on = true;
|
||||
var edge = dash_total;
|
||||
for (var i = 0; i < 4; i = i + 1) {
|
||||
if (i >= dash_count) { break; }
|
||||
let seg = cyc[i];
|
||||
if (m >= acc && m < acc + seg) {
|
||||
cur_on = (i % 2) == 0; // gerade Segmente = "an"
|
||||
edge = min(m - acc, acc + seg - m); // Abstand zur naechsten Grenze
|
||||
}
|
||||
acc = acc + seg;
|
||||
}
|
||||
let px_per_m = PX_PER_M_2D * pps;
|
||||
let edge_px = edge * px_per_m;
|
||||
// Signierter Abstand: innen "an" positiv, innen "aus" negativ.
|
||||
let sdist = select(-edge_px, edge_px, cur_on);
|
||||
cov_dash = smoothstep(-0.5, 0.5, sdist);
|
||||
}
|
||||
|
||||
let a = in.color.a * cov_radial * cov_cap * cov_dash;
|
||||
if (a <= 0.002) {
|
||||
discard;
|
||||
}
|
||||
return vec4<f32>(in.color.rgb, a);
|
||||
}
|
||||
"#;
|
||||
|
||||
Reference in New Issue
Block a user