topazcube 0.1.31 → 0.1.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +0 -0
- package/README.md +0 -0
- package/dist/Renderer.cjs +20844 -0
- package/dist/Renderer.cjs.map +1 -0
- package/dist/Renderer.js +20827 -0
- package/dist/Renderer.js.map +1 -0
- package/dist/client.cjs +91 -260
- package/dist/client.cjs.map +1 -1
- package/dist/client.js +68 -215
- package/dist/client.js.map +1 -1
- package/dist/server.cjs +165 -432
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +117 -370
- package/dist/server.js.map +1 -1
- package/dist/terminal.cjs +113 -200
- package/dist/terminal.cjs.map +1 -1
- package/dist/terminal.js +50 -51
- package/dist/terminal.js.map +1 -1
- package/dist/utils-CRhi1BDa.cjs +259 -0
- package/dist/utils-CRhi1BDa.cjs.map +1 -0
- package/dist/utils-D7tXt6-2.js +260 -0
- package/dist/utils-D7tXt6-2.js.map +1 -0
- package/package.json +19 -15
- package/src/{client.ts → network/client.js} +170 -403
- package/src/{compress-browser.ts → network/compress-browser.js} +2 -4
- package/src/{compress-node.ts → network/compress-node.js} +8 -14
- package/src/{server.ts → network/server.js} +229 -317
- package/src/{terminal.js → network/terminal.js} +0 -0
- package/src/{topazcube.ts → network/topazcube.js} +2 -2
- package/src/network/utils.js +375 -0
- package/src/renderer/Camera.js +191 -0
- package/src/renderer/DebugUI.js +703 -0
- package/src/renderer/Geometry.js +1049 -0
- package/src/renderer/Material.js +64 -0
- package/src/renderer/Mesh.js +211 -0
- package/src/renderer/Node.js +112 -0
- package/src/renderer/Pipeline.js +645 -0
- package/src/renderer/Renderer.js +1496 -0
- package/src/renderer/Skin.js +792 -0
- package/src/renderer/Texture.js +584 -0
- package/src/renderer/core/AssetManager.js +394 -0
- package/src/renderer/core/CullingSystem.js +308 -0
- package/src/renderer/core/EntityManager.js +541 -0
- package/src/renderer/core/InstanceManager.js +343 -0
- package/src/renderer/core/ParticleEmitter.js +358 -0
- package/src/renderer/core/ParticleSystem.js +564 -0
- package/src/renderer/core/SpriteSystem.js +349 -0
- package/src/renderer/gltf.js +563 -0
- package/src/renderer/math.js +161 -0
- package/src/renderer/rendering/HistoryBufferManager.js +333 -0
- package/src/renderer/rendering/ProbeCapture.js +1495 -0
- package/src/renderer/rendering/ReflectionProbeManager.js +352 -0
- package/src/renderer/rendering/RenderGraph.js +2258 -0
- package/src/renderer/rendering/passes/AOPass.js +308 -0
- package/src/renderer/rendering/passes/AmbientCapturePass.js +593 -0
- package/src/renderer/rendering/passes/BasePass.js +101 -0
- package/src/renderer/rendering/passes/BloomPass.js +420 -0
- package/src/renderer/rendering/passes/CRTPass.js +724 -0
- package/src/renderer/rendering/passes/FogPass.js +445 -0
- package/src/renderer/rendering/passes/GBufferPass.js +730 -0
- package/src/renderer/rendering/passes/HiZPass.js +744 -0
- package/src/renderer/rendering/passes/LightingPass.js +753 -0
- package/src/renderer/rendering/passes/ParticlePass.js +841 -0
- package/src/renderer/rendering/passes/PlanarReflectionPass.js +456 -0
- package/src/renderer/rendering/passes/PostProcessPass.js +405 -0
- package/src/renderer/rendering/passes/ReflectionPass.js +157 -0
- package/src/renderer/rendering/passes/RenderPostPass.js +364 -0
- package/src/renderer/rendering/passes/SSGIPass.js +266 -0
- package/src/renderer/rendering/passes/SSGITilePass.js +305 -0
- package/src/renderer/rendering/passes/ShadowPass.js +2072 -0
- package/src/renderer/rendering/passes/TransparentPass.js +831 -0
- package/src/renderer/rendering/passes/VolumetricFogPass.js +715 -0
- package/src/renderer/rendering/shaders/ao.wgsl +182 -0
- package/src/renderer/rendering/shaders/bloom.wgsl +97 -0
- package/src/renderer/rendering/shaders/bloom_blur.wgsl +80 -0
- package/src/renderer/rendering/shaders/crt.wgsl +455 -0
- package/src/renderer/rendering/shaders/depth_copy.wgsl +17 -0
- package/src/renderer/rendering/shaders/geometry.wgsl +580 -0
- package/src/renderer/rendering/shaders/hiz_reduce.wgsl +114 -0
- package/src/renderer/rendering/shaders/light_culling.wgsl +204 -0
- package/src/renderer/rendering/shaders/lighting.wgsl +932 -0
- package/src/renderer/rendering/shaders/lighting_common.wgsl +143 -0
- package/src/renderer/rendering/shaders/particle_render.wgsl +672 -0
- package/src/renderer/rendering/shaders/particle_simulate.wgsl +440 -0
- package/src/renderer/rendering/shaders/postproc.wgsl +293 -0
- package/src/renderer/rendering/shaders/render_post.wgsl +289 -0
- package/src/renderer/rendering/shaders/shadow.wgsl +117 -0
- package/src/renderer/rendering/shaders/ssgi.wgsl +266 -0
- package/src/renderer/rendering/shaders/ssgi_accumulate.wgsl +114 -0
- package/src/renderer/rendering/shaders/ssgi_propagate.wgsl +132 -0
- package/src/renderer/rendering/shaders/volumetric_blur.wgsl +80 -0
- package/src/renderer/rendering/shaders/volumetric_composite.wgsl +80 -0
- package/src/renderer/rendering/shaders/volumetric_raymarch.wgsl +634 -0
- package/src/renderer/utils/BoundingSphere.js +439 -0
- package/src/renderer/utils/Frustum.js +281 -0
- package/src/renderer/utils/Raycaster.js +761 -0
- package/dist/client.d.cts +0 -211
- package/dist/client.d.ts +0 -211
- package/dist/server.d.cts +0 -120
- package/dist/server.d.ts +0 -120
- package/dist/terminal.d.cts +0 -64
- package/dist/terminal.d.ts +0 -64
- package/src/utils.ts +0 -403
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
const PI = 3.14159265359;
|
|
2
|
+
|
|
3
|
+
struct VertexOutput {
|
|
4
|
+
@builtin(position) position: vec4<f32>,
|
|
5
|
+
@location(0) uv: vec2<f32>,
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
struct Uniforms {
|
|
9
|
+
inverseProjection: mat4x4<f32>,
|
|
10
|
+
projection: mat4x4<f32>,
|
|
11
|
+
view: mat4x4<f32>,
|
|
12
|
+
aoSize: vec2f, // AO render target size (may be scaled)
|
|
13
|
+
gbufferSize: vec2f, // GBuffer full resolution size
|
|
14
|
+
// AO params: x = intensity, y = radius, z = fadeDistance, w = bias
|
|
15
|
+
aoParams: vec4f,
|
|
16
|
+
// Noise params: x = size, y = offsetX, z = offsetY, w = time
|
|
17
|
+
noiseParams: vec4f,
|
|
18
|
+
// Camera params: x = near, y = far
|
|
19
|
+
cameraParams: vec2f,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
23
|
+
@group(0) @binding(1) var gDepth: texture_depth_2d;
|
|
24
|
+
@group(0) @binding(2) var gNormal: texture_2d<f32>;
|
|
25
|
+
@group(0) @binding(3) var gArm: texture_2d<f32>;
|
|
26
|
+
@group(0) @binding(4) var noiseTexture: texture_2d<f32>;
|
|
27
|
+
|
|
28
|
+
// Poisson disk offsets for 16 samples - well distributed pattern
|
|
29
|
+
const OFFSETS_16 = array<vec2f, 16>(
|
|
30
|
+
vec2f(0.063, 0.000),
|
|
31
|
+
vec2f(0.079, 0.097),
|
|
32
|
+
vec2f(-0.039, 0.183),
|
|
33
|
+
vec2f(-0.223, 0.113),
|
|
34
|
+
vec2f(-0.285, -0.127),
|
|
35
|
+
vec2f(-0.097, -0.362),
|
|
36
|
+
vec2f(0.257, -0.354),
|
|
37
|
+
vec2f(0.499, -0.026),
|
|
38
|
+
vec2f(0.376, 0.418),
|
|
39
|
+
vec2f(-0.098, 0.617),
|
|
40
|
+
vec2f(-0.595, 0.344),
|
|
41
|
+
vec2f(-0.700, -0.269),
|
|
42
|
+
vec2f(-0.251, -0.773),
|
|
43
|
+
vec2f(0.477, -0.734),
|
|
44
|
+
vec2f(0.932, -0.098),
|
|
45
|
+
vec2f(0.707, 0.707)
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// Convert linear depth buffer value back to view-space distance
|
|
49
|
+
// Inverse of: depth = (z - near) / (far - near)
|
|
50
|
+
// Result: z = near + depth * (far - near)
|
|
51
|
+
fn depthToLinear(depth: f32) -> f32 {
|
|
52
|
+
let near = uniforms.cameraParams.x;
|
|
53
|
+
let far = uniforms.cameraParams.y;
|
|
54
|
+
return near + depth * (far - near);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Get linear depth at pixel coordinate (converts from depth buffer)
|
|
58
|
+
fn getDepth(coord: vec2i) -> f32 {
|
|
59
|
+
let bufferDepth = textureLoad(gDepth, coord, 0);
|
|
60
|
+
return depthToLinear(bufferDepth);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Sample noise at screen position
|
|
64
|
+
fn sampleNoise(screenPos: vec2f) -> f32 {
|
|
65
|
+
let noiseSize = i32(uniforms.noiseParams.x);
|
|
66
|
+
let noiseOffsetX = i32(uniforms.noiseParams.y * f32(noiseSize));
|
|
67
|
+
let noiseOffsetY = i32(uniforms.noiseParams.z * f32(noiseSize));
|
|
68
|
+
|
|
69
|
+
let texCoord = vec2i(
|
|
70
|
+
(i32(screenPos.x) + noiseOffsetX) % noiseSize,
|
|
71
|
+
(i32(screenPos.y) + noiseOffsetY) % noiseSize
|
|
72
|
+
);
|
|
73
|
+
return textureLoad(noiseTexture, texCoord, 0).r;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Rotate a 2D vector by angle
|
|
77
|
+
fn rotate2D(v: vec2f, angle: f32) -> vec2f {
|
|
78
|
+
let s = sin(angle);
|
|
79
|
+
let c = cos(angle);
|
|
80
|
+
return vec2f(v.x * c - v.y * s, v.x * s + v.y * c);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@vertex
|
|
84
|
+
fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
85
|
+
var output: VertexOutput;
|
|
86
|
+
let x = f32(vertexIndex & 1u) * 4.0 - 1.0;
|
|
87
|
+
let y = f32(vertexIndex >> 1u) * 4.0 - 1.0;
|
|
88
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
89
|
+
output.uv = vec2f((x + 1.0) * 0.5, (1.0 - y) * 0.5);
|
|
90
|
+
return output;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@fragment
|
|
94
|
+
fn fragmentMain(input: VertexOutput) -> @location(0) f32 {
|
|
95
|
+
// Use UV to sample GBuffer at correct position regardless of AO resolution
|
|
96
|
+
let gbufferCoord = vec2i(floor(input.uv * uniforms.gbufferSize));
|
|
97
|
+
let aoCoord = vec2i(floor(input.uv * uniforms.aoSize));
|
|
98
|
+
let screenPos = input.position.xy;
|
|
99
|
+
|
|
100
|
+
// Get linear depth in meters (sample from full-res GBuffer)
|
|
101
|
+
let depth = getDepth(gbufferCoord);
|
|
102
|
+
let normal = normalize(textureLoad(gNormal, gbufferCoord, 0).xyz);
|
|
103
|
+
|
|
104
|
+
// Parameters
|
|
105
|
+
let aoIntensity = uniforms.aoParams.x;
|
|
106
|
+
let aoRadius = uniforms.aoParams.y;
|
|
107
|
+
let aoFadeDistance = uniforms.aoParams.z;
|
|
108
|
+
let aoBias = uniforms.aoParams.w;
|
|
109
|
+
let far = uniforms.cameraParams.y;
|
|
110
|
+
|
|
111
|
+
// Early out for sky
|
|
112
|
+
if (depth > far * 0.95) {
|
|
113
|
+
return 1.0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Distance fade
|
|
117
|
+
let distanceFade = 1.0 - smoothstep(aoFadeDistance * 0.5, aoFadeDistance, depth);
|
|
118
|
+
if (distanceFade < 0.01) {
|
|
119
|
+
return 1.0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Blue noise jitter
|
|
123
|
+
let noise = sampleNoise(screenPos);
|
|
124
|
+
let rotationAngle = noise * PI * 2.0;
|
|
125
|
+
|
|
126
|
+
// Sample radius in pixels - scale with depth so AO is consistent across distances
|
|
127
|
+
let sampleRadius = aoRadius / max(depth * 0.2, 1.0);
|
|
128
|
+
|
|
129
|
+
var occlusion = 0.0;
|
|
130
|
+
var validSamples = 0.0;
|
|
131
|
+
|
|
132
|
+
// Occlusion threshold in meters - scales with depth
|
|
133
|
+
let occlusionThreshold = 0.1 + depth * 0.02; // 10cm + 2% of depth
|
|
134
|
+
let maxOcclusionDist = occlusionThreshold * 5.0;
|
|
135
|
+
|
|
136
|
+
// Scale factor from AO space to GBuffer space
|
|
137
|
+
let scaleToGBuffer = uniforms.gbufferSize / uniforms.aoSize;
|
|
138
|
+
|
|
139
|
+
for (var i = 0; i < 16; i++) {
|
|
140
|
+
let offset = rotate2D(OFFSETS_16[i], rotationAngle) * sampleRadius;
|
|
141
|
+
// Scale offset from AO-space to GBuffer-space pixels
|
|
142
|
+
let scaledOffset = offset * scaleToGBuffer;
|
|
143
|
+
let sampleCoord = gbufferCoord + vec2i(i32(scaledOffset.x), i32(scaledOffset.y));
|
|
144
|
+
|
|
145
|
+
// Bounds check against GBuffer size
|
|
146
|
+
if (sampleCoord.x < 0 || sampleCoord.x >= i32(uniforms.gbufferSize.x) ||
|
|
147
|
+
sampleCoord.y < 0 || sampleCoord.y >= i32(uniforms.gbufferSize.y)) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let sampleDepth = getDepth(sampleCoord);
|
|
152
|
+
|
|
153
|
+
// Simple depth-based AO like the reference shader
|
|
154
|
+
// ddiff > 0 means sample is closer to camera (we're behind it = occluded)
|
|
155
|
+
let ddiff = depth - sampleDepth;
|
|
156
|
+
|
|
157
|
+
// If sample is closer (ddiff > 0), we're occluded
|
|
158
|
+
// If sample is further or same (ddiff <= 0), we're not occluded
|
|
159
|
+
let unoccluded = select(1.0, 0.0, ddiff > aoBias);
|
|
160
|
+
|
|
161
|
+
// Ignore samples that are much closer (edge/discontinuity)
|
|
162
|
+
let relevant = 1.0 - smoothstep(occlusionThreshold, maxOcclusionDist, ddiff);
|
|
163
|
+
|
|
164
|
+
occlusion += unoccluded * relevant;
|
|
165
|
+
validSamples += relevant;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Final AO calculation
|
|
169
|
+
// occlusion = sum of unoccluded samples, validSamples = sum of relevant weights
|
|
170
|
+
// Higher unoccluded ratio = less darkening
|
|
171
|
+
var ao = 1.0;
|
|
172
|
+
if (validSamples > 0.5) {
|
|
173
|
+
let unoccludedRatio = occlusion / (validSamples * 0.5); // Like reference shader
|
|
174
|
+
let aoFactor = 1.0 - clamp(unoccludedRatio, 0.0, 1.0);
|
|
175
|
+
ao = 1.0 - aoFactor * aoIntensity * 0.5;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Apply distance fade
|
|
179
|
+
ao = mix(1.0, ao, distanceFade);
|
|
180
|
+
|
|
181
|
+
return ao;
|
|
182
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Bloom bright pass shader
|
|
2
|
+
// Extracts bright and emissive pixels from HDR lighting output
|
|
3
|
+
// Uses exponential falloff so dark pixels contribute exponentially less
|
|
4
|
+
|
|
5
|
+
struct Uniforms {
|
|
6
|
+
threshold: f32, // Brightness threshold (e.g., 0.8)
|
|
7
|
+
softThreshold: f32, // Soft knee (0 = hard cutoff, 1 = very soft)
|
|
8
|
+
intensity: f32, // Overall bloom intensity
|
|
9
|
+
emissiveBoost: f32, // Extra boost for emissive pixels
|
|
10
|
+
maxBrightness: f32, // Clamp input brightness (prevents specular halos)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
14
|
+
@group(0) @binding(1) var inputTexture: texture_2d<f32>; // HDR lighting output
|
|
15
|
+
@group(0) @binding(2) var inputSampler: sampler;
|
|
16
|
+
|
|
17
|
+
struct VertexOutput {
|
|
18
|
+
@builtin(position) position: vec4f,
|
|
19
|
+
@location(0) uv: vec2f,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@vertex
|
|
23
|
+
fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
24
|
+
// Full-screen triangle
|
|
25
|
+
var positions = array<vec2f, 3>(
|
|
26
|
+
vec2f(-1.0, -1.0),
|
|
27
|
+
vec2f(3.0, -1.0),
|
|
28
|
+
vec2f(-1.0, 3.0)
|
|
29
|
+
);
|
|
30
|
+
var uvs = array<vec2f, 3>(
|
|
31
|
+
vec2f(0.0, 1.0),
|
|
32
|
+
vec2f(2.0, 1.0),
|
|
33
|
+
vec2f(0.0, -1.0)
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
var output: VertexOutput;
|
|
37
|
+
output.position = vec4f(positions[vertexIndex], 0.0, 1.0);
|
|
38
|
+
output.uv = uvs[vertexIndex];
|
|
39
|
+
return output;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Calculate brightness as max RGB (treats all colors equally for bloom threshold)
|
|
43
|
+
fn brightness(color: vec3f) -> f32 {
|
|
44
|
+
return max(max(color.r, color.g), color.b);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Soft threshold with knee curve
|
|
48
|
+
// Creates smooth transition around threshold instead of hard cutoff
|
|
49
|
+
fn softThresholdCurve(brightness: f32, threshold: f32, knee: f32) -> f32 {
|
|
50
|
+
let soft = threshold * knee;
|
|
51
|
+
let softMin = threshold - soft;
|
|
52
|
+
let softMax = threshold + soft;
|
|
53
|
+
|
|
54
|
+
if (brightness <= softMin) {
|
|
55
|
+
// Below threshold - exponential falloff
|
|
56
|
+
// Instead of 0, use exponential curve so bright-ish pixels still contribute slightly
|
|
57
|
+
let ratio = brightness / max(softMin, 0.001);
|
|
58
|
+
return pow(ratio, 4.0) * brightness; // Very aggressive falloff
|
|
59
|
+
} else if (brightness >= softMax) {
|
|
60
|
+
// Above threshold - full contribution
|
|
61
|
+
return brightness;
|
|
62
|
+
} else {
|
|
63
|
+
// In the soft knee region - smooth interpolation
|
|
64
|
+
let t = (brightness - softMin) / (softMax - softMin);
|
|
65
|
+
let smoothT = t * t * (3.0 - 2.0 * t); // Smoothstep
|
|
66
|
+
return mix(pow(brightness / threshold, 4.0) * brightness, brightness, smoothT);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@fragment
|
|
71
|
+
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
|
|
72
|
+
var color = textureSample(inputTexture, inputSampler, input.uv).rgb;
|
|
73
|
+
|
|
74
|
+
// Clamp extremely bright values (specular highlights) to prevent excessive bloom
|
|
75
|
+
let bright = brightness(color);
|
|
76
|
+
if (bright > uniforms.maxBrightness) {
|
|
77
|
+
color = color * (uniforms.maxBrightness / bright);
|
|
78
|
+
}
|
|
79
|
+
let clampedBright = min(bright, uniforms.maxBrightness);
|
|
80
|
+
|
|
81
|
+
// Apply soft threshold with exponential falloff
|
|
82
|
+
let contribution = softThresholdCurve(clampedBright, uniforms.threshold, uniforms.softThreshold);
|
|
83
|
+
|
|
84
|
+
// Calculate the extraction factor (how much of the original color to keep)
|
|
85
|
+
let factor = contribution / max(clampedBright, 0.001);
|
|
86
|
+
|
|
87
|
+
// Extract bloom color
|
|
88
|
+
var bloomColor = color * factor * uniforms.intensity;
|
|
89
|
+
|
|
90
|
+
// Note: Emissive is already included in the HDR color from lighting pass
|
|
91
|
+
// Very bright pixels (brightness > 1.0) are likely emissive or highly lit
|
|
92
|
+
// Give them extra boost based on how much they exceed 1.0
|
|
93
|
+
let emissiveFactor = max(0.0, clampedBright - 1.0);
|
|
94
|
+
bloomColor *= 1.0 + emissiveFactor * uniforms.emissiveBoost;
|
|
95
|
+
|
|
96
|
+
return vec4f(bloomColor, 1.0);
|
|
97
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Bloom blur shader - Separable Gaussian blur
|
|
2
|
+
// Run twice: once horizontal, once vertical
|
|
3
|
+
|
|
4
|
+
struct Uniforms {
|
|
5
|
+
direction: vec2f, // (1,0) for horizontal, (0,1) for vertical
|
|
6
|
+
texelSize: vec2f, // 1.0 / textureSize
|
|
7
|
+
blurRadius: f32, // Blur radius in pixels
|
|
8
|
+
padding: f32,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
12
|
+
@group(0) @binding(1) var inputTexture: texture_2d<f32>;
|
|
13
|
+
@group(0) @binding(2) var inputSampler: sampler;
|
|
14
|
+
|
|
15
|
+
struct VertexOutput {
|
|
16
|
+
@builtin(position) position: vec4f,
|
|
17
|
+
@location(0) uv: vec2f,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@vertex
|
|
21
|
+
fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
22
|
+
// Full-screen triangle
|
|
23
|
+
var positions = array<vec2f, 3>(
|
|
24
|
+
vec2f(-1.0, -1.0),
|
|
25
|
+
vec2f(3.0, -1.0),
|
|
26
|
+
vec2f(-1.0, 3.0)
|
|
27
|
+
);
|
|
28
|
+
var uvs = array<vec2f, 3>(
|
|
29
|
+
vec2f(0.0, 1.0),
|
|
30
|
+
vec2f(2.0, 1.0),
|
|
31
|
+
vec2f(0.0, -1.0)
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
var output: VertexOutput;
|
|
35
|
+
output.position = vec4f(positions[vertexIndex], 0.0, 1.0);
|
|
36
|
+
output.uv = uvs[vertexIndex];
|
|
37
|
+
return output;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Gaussian weight function
|
|
41
|
+
fn gaussian(x: f32, sigma: f32) -> f32 {
|
|
42
|
+
return exp(-(x * x) / (2.0 * sigma * sigma));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@fragment
|
|
46
|
+
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
|
|
47
|
+
let uv = input.uv;
|
|
48
|
+
let direction = uniforms.direction;
|
|
49
|
+
let texelSize = uniforms.texelSize;
|
|
50
|
+
let radius = uniforms.blurRadius;
|
|
51
|
+
|
|
52
|
+
// Sigma is radius / 3 for good coverage (99.7% of gaussian within 3 sigma)
|
|
53
|
+
let sigma = radius / 3.0;
|
|
54
|
+
|
|
55
|
+
// Sample center
|
|
56
|
+
var color = textureSample(inputTexture, inputSampler, uv).rgb;
|
|
57
|
+
var totalWeight = 1.0;
|
|
58
|
+
|
|
59
|
+
// Use incremental offsets for better cache coherency
|
|
60
|
+
// Sample in both directions from center
|
|
61
|
+
let stepSize = 1.5; // Sample every 1.5 pixels for quality/performance balance
|
|
62
|
+
let numSamples = i32(ceil(radius / stepSize));
|
|
63
|
+
|
|
64
|
+
for (var i = 1; i <= numSamples; i++) {
|
|
65
|
+
let offset = f32(i) * stepSize;
|
|
66
|
+
let weight = gaussian(offset, sigma);
|
|
67
|
+
|
|
68
|
+
// Positive direction
|
|
69
|
+
let uvPos = uv + direction * texelSize * offset;
|
|
70
|
+
color += textureSample(inputTexture, inputSampler, uvPos).rgb * weight;
|
|
71
|
+
|
|
72
|
+
// Negative direction
|
|
73
|
+
let uvNeg = uv - direction * texelSize * offset;
|
|
74
|
+
color += textureSample(inputTexture, inputSampler, uvNeg).rgb * weight;
|
|
75
|
+
|
|
76
|
+
totalWeight += weight * 2.0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return vec4f(color / totalWeight, 1.0);
|
|
80
|
+
}
|