// 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 u: Uniforms; @group(1) @binding(0) var tex: texture_2d; @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); }