Bildweite Effekte gehören nicht in den Szenen-Shader. Cel-Ramp und Sobel-Outline sollen dazukommen, ohne einander (oder scene.wgsl) zu kennen — also eine Kette gleichrangiger Fullscreen-Pässe zwischen Szene und Blit, Ping-Pong über zwei interne Targets. Der Vertrag ist für alle gleich: Farbtextur rein (group 0/binding 0), Farbe raus. Die Vertex-Stufe teilen sich alle über fullscreen.wgsl, damit eine Effekt-Datei nur aus ihrem fs_main besteht. Ein neuer Effekt ist eine .wgsl plus eine Zeile in EFFECTS. Bei 640×480 kostet ein eigener Pass je Effekt praktisch nichts — deshalb kein Über-Shader. Erster Bewohner ist die RGB555-Quantisierung samt Bayer-Dither, aus scene.wgsl herausgelöst. Das UI zeichnet jetzt in einem eigenen Pass hinter der Kette, sonst würde der Dither auch den Text quantisieren, den er bisher nicht anfasst; damit braucht SpritePass kein Tiefen-Attachment mehr. Geschaltet wird über den exec-Trichter (`post`, `post <e> on|off`). Die Namen kennt nur das Frontend: es meldet seine Kette beim Start in engine::post an, wie render::audio seine Emitter aus engine::audio zieht. Der Kern bekommt dadurch keine GPU zu sehen. Das Bild soll unverändert bleiben — gleicher Bayer-Kernel, gleiche Koordinaten, gleiche Reihenfolge relativ zum UI. Verifiziert sind Tests, clippy und ein Start ohne Validation-Fehler; ein Pixelvergleich steht aus. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
57 lines
2.1 KiB
WebGPU Shading Language
57 lines
2.1 KiB
WebGPU Shading Language
// Szenen-Pipeline: Geometrie und Textur, sonst nichts. Der Pixel-Snap im
|
|
// Vertex-Shader (PS1-Vertex-Jitter) gehört hierher, weil er Geometrie ist;
|
|
// alles Bildweite — Farbtiefe, Dither, später Cel-Ramp und Outline — läuft
|
|
// als eigenes Kettenglied in render::post.
|
|
//
|
|
// 1-Bit-Alpha-Test (Cutouts), kein Blending.
|
|
//
|
|
// UVs werden perspektivkorrekt interpoliert (@interpolate(perspective)). Das
|
|
// klassische PS1-Textur-Wobbeln (affin, @interpolate(linear)) ist mit einer
|
|
// frei beweglichen Kamera und großen Faces zu störend.
|
|
|
|
struct Uniforms {
|
|
mvp: mat4x4f,
|
|
// Halbe interne Auflösung (NDC läuft -1..1, Spanne 2 → Skalierung mit
|
|
// der halben Auflösung trifft das Pixelraster). Kommt aus dem Renderer.
|
|
half_res: vec2f,
|
|
};
|
|
@group(0) @binding(0) var<uniform> u: Uniforms;
|
|
|
|
@group(1) @binding(0) var tex: texture_2d<f32>;
|
|
@group(1) @binding(1) var smp: sampler;
|
|
|
|
struct VsOut {
|
|
@builtin(position) pos: vec4f,
|
|
// perspektivkorrekt interpoliert (WGSL-Default) — kein Affin-Wobbeln.
|
|
@location(0) @interpolate(perspective) uv: vec2f,
|
|
};
|
|
|
|
@vertex
|
|
fn vs_main(@location(0) pos: vec3f, @location(1) uv: vec2f) -> VsOut {
|
|
var clip = u.mvp * vec4f(pos, 1.0);
|
|
// Pixel-Snap: xy nach der Projektion aufs interne Raster runden und
|
|
// zurück in den Clip-Raum. Nur vor der Kamera (w>0) — dahinter würde
|
|
// die Division Unsinn liefern, das Hardware-Clipping übernimmt.
|
|
if clip.w > 0.0 {
|
|
let ndc = clip.xy / clip.w;
|
|
clip = vec4f(round(ndc * u.half_res) / u.half_res * clip.w, clip.zw);
|
|
}
|
|
var out: VsOut;
|
|
out.pos = clip;
|
|
out.uv = uv;
|
|
return out;
|
|
}
|
|
|
|
// LOD-Bias für die Mip-Auswahl: negativ = schärfer (greift einen höher
|
|
// aufgelösten Mip, als die UV-Ableitungen verlangen), positiv = weicher.
|
|
// 0 = neutral. Bei -1.0 verdoppelt sich praktisch die Texelrate (mehr
|
|
// Schärfe, etwas mehr Flimmern); -0.5 ist ein sanfter Mittelweg.
|
|
const MIP_BIAS: f32 = -1.0;
|
|
|
|
@fragment
|
|
fn fs_main(in: VsOut) -> @location(0) vec4f {
|
|
let texel = textureSampleBias(tex, smp, in.uv, MIP_BIAS);
|
|
if texel.a < 0.5 { discard; } // 1-Bit-Alpha (Cutouts), noch ungenutzt
|
|
return vec4f(texel.rgb, 1.0);
|
|
}
|