summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-03-08 09:13:57 +0100
committerskal <pascal.massimino@gmail.com>2026-03-08 09:13:57 +0100
commit9d114ae4fec465baed381de7782ef42ca77e734b (patch)
tree45483faf97e91f6c97848fd4e6e7723bb0de153e
parentfa0beb7cc3e4ab9edfd123933fd205053dc3ac31 (diff)
fix(shaders): enforce y-up screen-space convention + document coordinate conventions
- Add textureSampleYUp() helper to fullscreen_uv_vs.wgsl to correct y-flip when sampling WebGPU textures with y-up UV coordinates - Use textureSampleYUp() in passthrough, gaussian_blur, combined_postprocess - Fix skybox.wgsl: remove erroneous (1.0 - uv.y) flip (double-flip bug) - Document world/view/screen conventions in doc/3D.md, camera_common.wgsl, and fullscreen_uv_vs.wgsl Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rw-r--r--doc/3D.md24
-rw-r--r--src/shaders/camera_common.wgsl7
-rw-r--r--src/shaders/combined_postprocess.wgsl2
-rw-r--r--src/shaders/gaussian_blur.wgsl2
-rw-r--r--src/shaders/passthrough.wgsl2
-rw-r--r--src/shaders/render/fullscreen_uv_vs.wgsl24
-rw-r--r--src/shaders/skybox.wgsl2
7 files changed, 53 insertions, 10 deletions
diff --git a/doc/3D.md b/doc/3D.md
index ac451c8..c27b034 100644
--- a/doc/3D.md
+++ b/doc/3D.md
@@ -1,5 +1,29 @@
# 3D System and Rendering Pipeline
+## Coordinate Conventions
+
+### World Space: Z-up, Y-forward
+- X = right
+- Y = forward
+- Z = up
+- `look_at` up parameter: `{0, 0, 1}`
+- Camera default position: along +Y axis, looking toward origin
+
+### View Space: X-right, Y-up, -Z-forward (standard NDC)
+- Camera looks down **-Z** in view space
+- `perspective()` encodes this via `m[11] = -1`
+- Independent of world-space convention — set by `look_at` construction
+
+### Screen Space: Y-up, origin at bottom-left
+- `uv` in `[0,1]`: `(0,0)` = bottom-left, `(1,1)` = top-right
+- `st` in `[-1,1]`: NDC directly, y-up
+- WebGPU textures are y-down `(0,0)` at top — use `textureSampleYUp()` when sampling with `uv`
+
+### NDC → Screen
+- WebGPU NDC: y-up (`+1` = top)
+- WebGPU framebuffer: y-down (pixel row 0 at top)
+- `uv.y = 0` → NDC `y = -1` → bottom of framebuffer ✓
+
## Core Concept
Hybrid SDF/rasterization pipeline with physics and collision detection.
diff --git a/src/shaders/camera_common.wgsl b/src/shaders/camera_common.wgsl
index 846d052..67544f1 100644
--- a/src/shaders/camera_common.wgsl
+++ b/src/shaders/camera_common.wgsl
@@ -1,4 +1,9 @@
-// Camera parameters and helpers for SDF raymarching effects
+// Camera parameters and helpers for SDF raymarching effects.
+//
+// Coordinate conventions:
+// World space: Z-up, Y-forward, X-right
+// View space: X-right, Y-up, -Z-forward (camera looks down -Z)
+// getCameraRay: expects uv in [-1,1] y-up (pass in.st from fullscreen_uv_vs)
struct CameraParams {
inv_view: mat4x4f,
diff --git a/src/shaders/combined_postprocess.wgsl b/src/shaders/combined_postprocess.wgsl
index c0acfe7..7402284 100644
--- a/src/shaders/combined_postprocess.wgsl
+++ b/src/shaders/combined_postprocess.wgsl
@@ -11,7 +11,7 @@
@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f {
// Sample base color
- var color = textureSample(input_texture, input_sampler, in.uv);
+ var color = textureSampleYUp(input_texture, input_sampler, in.uv);
// Apply effects in sequence (customize as needed)
// color = apply_solarize(color, 0.4, 0.4, uniforms.time);
diff --git a/src/shaders/gaussian_blur.wgsl b/src/shaders/gaussian_blur.wgsl
index 7f85719..75f8c39 100644
--- a/src/shaders/gaussian_blur.wgsl
+++ b/src/shaders/gaussian_blur.wgsl
@@ -24,7 +24,7 @@ struct GaussianBlurParams {
for (var i = -kernel_size; i <= kernel_size; i++) {
let sample_offset = f32(i) * offset;
let weight = exp(-f32(i * i) / (2.0 * params.radius * params.radius));
- color += textureSample(input_texture, input_sampler, in.uv + sample_offset) * weight;
+ color += textureSampleYUp(input_texture, input_sampler, in.uv + sample_offset) * weight;
weight_sum += weight;
}
diff --git a/src/shaders/passthrough.wgsl b/src/shaders/passthrough.wgsl
index bce377c..90ba08b 100644
--- a/src/shaders/passthrough.wgsl
+++ b/src/shaders/passthrough.wgsl
@@ -7,5 +7,5 @@
@group(0) @binding(2) var<uniform> uniforms: UniformsSequenceParams;
@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f {
- return textureSample(input_texture, input_sampler, in.uv);
+ return textureSampleYUp(input_texture, input_sampler, in.uv);
}
diff --git a/src/shaders/render/fullscreen_uv_vs.wgsl b/src/shaders/render/fullscreen_uv_vs.wgsl
index f9ae427..5f76160 100644
--- a/src/shaders/render/fullscreen_uv_vs.wgsl
+++ b/src/shaders/render/fullscreen_uv_vs.wgsl
@@ -1,15 +1,29 @@
// Common vertex shader for fullscreen post-processing effects.
-// Draws a single triangle that covers the entire screen, with uv
-// coordinates in [0..1]
+// Draws a single triangle that covers the entire screen.
+//
+// Screen-space convention: Y-UP, origin at BOTTOM-LEFT
+// uv in [0,1]: (0,0) = bottom-left, (1,1) = top-right
+// st in [-1,1]: NDC directly, y-up
+//
+// WebGPU textures are Y-DOWN ((0,0) at top-left).
+// Use textureSampleYUp() instead of textureSample() when sampling with uv.
struct VertexOutput {
- @builtin(position) position: vec4f,
- @location(0) uv: vec2f,
+ @builtin(position) position: vec4f, // in pixel screen units
+ @location(0) uv: vec2f, // in [0, 1]
+ @location(1) st: vec2f, // in [-1, 1]
};
@vertex fn vs_main(@builtin(vertex_index) i: u32) -> VertexOutput {
- let pos = array<vec4f, 3>(vec4f(-1, -1, 0, 1.), vec4f(3, -1, 2., 1.), vec4f(-1, 3, 0, -1));
+ let pos = array<vec4f, 3>(vec4f(-1, -1, 0, 0.), vec4f(3, -1, 2., 0.), vec4f(-1, 3, 0, 2.));
var out: VertexOutput;
out.position = vec4f(pos[i].xy, 0.0, 1.0);
out.uv = pos[i].zw;
+ out.st = pos[i].xy;
return out;
}
+
+// Sample a texture using y-up UV coordinates (uv.y=0 at bottom).
+// Flips Y to match WebGPU texture convention (texel (0,0) at top-left).
+fn textureSampleYUp(tex: texture_2d<f32>, s: sampler, uv: vec2f) -> vec4f {
+ return textureSample(tex, s, vec2f(uv.x, 1.0 - uv.y));
+}
diff --git a/src/shaders/skybox.wgsl b/src/shaders/skybox.wgsl
index 075eeb6..652743f 100644
--- a/src/shaders/skybox.wgsl
+++ b/src/shaders/skybox.wgsl
@@ -10,7 +10,7 @@
fn fs_main(in: VertexOutput) -> @location(0) vec4f {
// Convert UV to NDC
let ndc_x = in.uv.x * 2.0 - 1.0;
- let ndc_y = (1.0 - in.uv.y) * 2.0 - 1.0; // Un-flip Y for NDC (Y-up)
+ let ndc_y = in.uv.y * 2.0 - 1.0; // uv is y-up, matches NDC directly
// Unproject to find world direction
// We want the direction from camera to the far plane at this pixel