@forgeax/engine-shader 0.1.19 → 0.1.21
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/README.md +10 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/ShaderRegistry.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +52 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/ShaderRegistry.ts +65 -0
- package/src/__tests__/bloom-fxaa-tonemap.unit.test.ts +30 -4
- package/src/__tests__/default-standard-pbr-alpha.unit.test.ts +7 -0
- package/src/__tests__/default-standard-pbr-transmission.unit.test.ts +71 -22
- package/src/__tests__/fog-oracle.unit.test.ts +110 -114
- package/src/__tests__/fog-orthographic-ray.unit.test.ts +12 -37
- package/src/__tests__/fog-producer-matrix.integration.test.ts +16 -51
- package/src/__tests__/lighting-point-spot-volume.unit.test.ts +56 -0
- package/src/__tests__/lighting-punctual.unit.test.ts +32 -6
- package/src/__tests__/lighting-spot-volume-attenuation.unit.test.ts +26 -0
- package/src/__tests__/lighting-spot-volume.unit.test.ts +25 -0
- package/src/__tests__/material-derived-builtins.integration.test.ts +27 -0
- package/src/__tests__/reflection-probe-sampling.unit.test.ts +23 -0
- package/src/__tests__/scene-temporal.unit.test.ts +20 -0
- package/src/__tests__/shader.unit.test.ts +35 -13
- package/src/__tests__/shadow-pcss.unit.test.ts +131 -0
- package/src/__tests__/sprite-lit-shader.test.ts +24 -11
- package/src/__tests__/sprite-variants.unit.test.ts +2 -0
- package/src/__tests__/transmission-thickness-scale.unit.test.ts +7 -1
- package/src/__tests__/transparent-pbr.unit.test.ts +7 -0
- package/src/common.wgsl +72 -73
- package/src/default-standard-pbr-skin.wgsl +31 -89
- package/src/default-standard-pbr.wgsl +94 -146
- package/src/fxaa.wgsl +27 -6
- package/src/hdrp-deferred-lighting.wgsl +3 -27
- package/src/ibl-sampling.wgsl +54 -0
- package/src/index.ts +6 -0
- package/src/lighting-attenuation.wgsl +58 -0
- package/src/lighting-directional.wgsl +188 -21
- package/src/lighting-punctual.wgsl +94 -19
- package/src/msdf-text.wgsl +4 -23
- package/src/scene-temporal.wgsl +14 -8
- package/src/shadow-pcf.wgsl +75 -11
- package/src/skybox.wgsl +2 -7
- package/src/sprite-lit.wgsl +38 -73
- package/src/sprite.wgsl +3 -22
- package/src/{hdrp-cluster-forward.wgsl → standard-cluster.wgsl} +74 -23
- package/src/tonemap.wgsl +8 -3
- package/src/unlit.wgsl +2 -24
- package/src/volume/volume-composite.wgsl +113 -0
- package/src/volume/volume-inject.wgsl +261 -0
- package/src/volume/volume-integrate.wgsl +337 -0
- package/src/volume/volume-temporal.wgsl +70 -0
package/src/skybox.wgsl
CHANGED
|
@@ -23,9 +23,8 @@
|
|
|
23
23
|
// @binding(3) rotation : SkyboxRotation (UBO)
|
|
24
24
|
|
|
25
25
|
#import forgeax_view::common::FullscreenOutput
|
|
26
|
-
#import forgeax_view::common::
|
|
26
|
+
#import forgeax_view::common::View
|
|
27
27
|
#import forgeax_view::common::fullscreen_triangle
|
|
28
|
-
#import forgeax_view::fog::{apply_fog}
|
|
29
28
|
#import forgeax_pbr::ibl_shared::{inverseRotateEnvironment}
|
|
30
29
|
|
|
31
30
|
@group(0) @binding(0) var cubemap : texture_cube<f32>;
|
|
@@ -67,9 +66,5 @@ fn skyboxDirection(uv : vec2<f32>) -> vec3<f32> {
|
|
|
67
66
|
fn skybox_fs(in : FullscreenOutput) -> @location(0) vec4<f32> {
|
|
68
67
|
let dir = skyboxDirection(in.uv);
|
|
69
68
|
let color = textureSample(cubemap, cubemapSampler, dir).rgb;
|
|
70
|
-
return
|
|
71
|
-
view.fog,
|
|
72
|
-
FogRay(view.cameraPos, dir, max(view.temporalProjection.y, 0.0)),
|
|
73
|
-
vec4<f32>(color, 1.0),
|
|
74
|
-
);
|
|
69
|
+
return vec4<f32>(color, 1.0);
|
|
75
70
|
}
|
package/src/sprite-lit.wgsl
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
#pragma variant_axis STORAGE_BUFFER_AVAILABLE
|
|
2
|
+
#pragma variant_axis CLUSTER_FORWARD_AVAILABLE
|
|
2
3
|
#define_import_path forgeax_material::sprite-lit
|
|
3
4
|
|
|
4
|
-
#import forgeax_view::common::{View,
|
|
5
|
-
#import forgeax_scene_temporal::{packSceneTemporalV1}
|
|
6
|
-
#
|
|
5
|
+
#import forgeax_view::common::{View, Mesh, InstanceData, view, meshes, instances, sampleMaterialTexture}
|
|
6
|
+
#import forgeax_scene_temporal::{packSceneTemporalV1, sceneViewZ}
|
|
7
|
+
#ifdef CLUSTER_FORWARD_AVAILABLE
|
|
8
|
+
#import forgeax_standard::cluster::{evaluateStandardClusterLights}
|
|
9
|
+
#endif
|
|
7
10
|
|
|
8
11
|
// @forgeax/engine-shader - sprite-lit.wgsl
|
|
9
12
|
// (tweak-20260701-sprite-lit-flat-default-drop-ndotl-for-2d).
|
|
@@ -27,7 +30,8 @@
|
|
|
27
30
|
// - no shadow receiver / caster
|
|
28
31
|
// - no Godot-style light_mode blends
|
|
29
32
|
// - no IBL ambient
|
|
30
|
-
// -
|
|
33
|
+
// - Standard clustered punctual lights are enabled by the
|
|
34
|
+
// CLUSTER_FORWARD_AVAILABLE variant.
|
|
31
35
|
//
|
|
32
36
|
// >>> AI user error self-recovery hint:
|
|
33
37
|
// sprite-lit needs at least one light in scene; for unlit sprites use
|
|
@@ -39,8 +43,7 @@
|
|
|
39
43
|
// pipelineState.defaultSampler + .defaultWhiteTextureView on the host side):
|
|
40
44
|
//
|
|
41
45
|
// @group(0) @binding(0) view uniform
|
|
42
|
-
// @group(0) @binding(1)
|
|
43
|
-
// @group(0) @binding(2) spotLightsBuffer storage / uniform (variant)
|
|
46
|
+
// @group(0) @binding(1..2) Standard Cluster storage (selected variant)
|
|
44
47
|
// @group(1) @binding(0) material uniform (sprite layout
|
|
45
48
|
// colorTint /
|
|
46
49
|
// region /
|
|
@@ -81,16 +84,17 @@ struct VsIn {
|
|
|
81
84
|
@location(3) tangent : vec4<f32>,
|
|
82
85
|
};
|
|
83
86
|
|
|
84
|
-
// VsOut carries the atlas UV plus the per-fragment world-space position
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
// multi-instance draws and could not handle the 9-slice quad layout.
|
|
87
|
+
// VsOut carries the atlas UV plus the per-fragment world-space position and
|
|
88
|
+
// the Standard cluster coordinates produced by the vertex stage. Fragment
|
|
89
|
+
// `@builtin(position)` is framebuffer-space after rasterization, so it is not
|
|
90
|
+
// a valid source for NDC cluster lookup; keep the perspective divide here and
|
|
91
|
+
// interpolate the result alongside worldPos.
|
|
90
92
|
struct VsOut {
|
|
91
93
|
@builtin(position) clip : vec4<f32>,
|
|
92
94
|
@location(0) uv_atlas : vec2<f32>,
|
|
93
95
|
@location(1) worldPos : vec3<f32>,
|
|
96
|
+
@location(2) ndc : vec3<f32>,
|
|
97
|
+
@location(3) viewZ : f32,
|
|
94
98
|
};
|
|
95
99
|
|
|
96
100
|
struct SpriteVertex {
|
|
@@ -156,6 +160,11 @@ fn vs_main(in : VsIn, @builtin(instance_index) idx : u32, @builtin(vertex_index)
|
|
|
156
160
|
out.clip = view.worldViewProj * world;
|
|
157
161
|
out.uv_atlas = vertex.uvAtlas;
|
|
158
162
|
out.worldPos = world.xyz;
|
|
163
|
+
let clipPos = out.clip;
|
|
164
|
+
out.ndc = clipPos.xyz / clipPos.w;
|
|
165
|
+
// Keep the cluster depth exactly aligned with the CPU binner for both
|
|
166
|
+
// perspective and off-axis orthographic projections.
|
|
167
|
+
out.viewZ = sceneViewZ(clipPos, view.temporalProjection);
|
|
159
168
|
return out;
|
|
160
169
|
}
|
|
161
170
|
|
|
@@ -175,79 +184,36 @@ fn spriteLitDirectional(albedo : vec3<f32>) -> vec3<f32> {
|
|
|
175
184
|
return albedo * view.lightColor;
|
|
176
185
|
}
|
|
177
186
|
|
|
178
|
-
// Flat point-light contribution. Applies the KHR-lights-punctual smooth-
|
|
179
|
-
// window range attenuation only. `attenuation` is 0 outside the light's
|
|
180
|
-
// range (`invRangeSquared`) and 1 at the light center; the reciprocal-square
|
|
181
|
-
// term gives the physical falloff. URP cap = 4 point lights per pass.
|
|
182
|
-
fn spriteLitPoint(p : PointLight, worldPos : vec3<f32>, albedo : vec3<f32>) -> vec3<f32> {
|
|
183
|
-
let toLight = p.position - worldPos;
|
|
184
|
-
let dSquared = max(dot(toLight, toLight), 1e-4);
|
|
185
|
-
let factor = 1.0 - (dSquared * p.invRangeSquared) * (dSquared * p.invRangeSquared);
|
|
186
|
-
let attenuation = max(min(factor, 1.0), 0.0) / dSquared;
|
|
187
|
-
return albedo * p.colorTimesIntensity * attenuation;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Flat spot-light contribution. Same range attenuation as spriteLitPoint,
|
|
191
|
-
// modulated by a smoothstep cone factor (host-side degree -> cosine
|
|
192
|
-
// conversion). Spot direction only shapes the cone; it does not project
|
|
193
|
-
// onto a surface normal. URP cap = 4 spot lights per pass.
|
|
194
|
-
fn spriteLitSpot(s : SpotLight, worldPos : vec3<f32>, albedo : vec3<f32>) -> vec3<f32> {
|
|
195
|
-
let toLight = s.position - worldPos;
|
|
196
|
-
let dSquared = max(dot(toLight, toLight), 1e-4);
|
|
197
|
-
let l = toLight / sqrt(dSquared);
|
|
198
|
-
let factor = 1.0 - (dSquared * s.invRangeSquared) * (dSquared * s.invRangeSquared);
|
|
199
|
-
let attenuation = max(min(factor, 1.0), 0.0) / dSquared;
|
|
200
|
-
let cone = smoothstep(s.cosOuter, s.cosInner, dot(l, -s.direction));
|
|
201
|
-
return albedo * s.colorTimesIntensity * attenuation * cone;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
187
|
// Shared shading body used by both fs_main (LDR) and fs_main_hdr (HDR).
|
|
205
188
|
// The two entry points differ only in their tail-end clamp + srgb encode
|
|
206
189
|
// step (LDR clamp / HDR pass-through boundary).
|
|
207
|
-
fn spriteLitShadeAccum(
|
|
190
|
+
fn spriteLitShadeAccum(
|
|
191
|
+
albedo : vec3<f32>,
|
|
192
|
+
worldPos : vec3<f32>,
|
|
193
|
+
ndc : vec3<f32>,
|
|
194
|
+
viewZ : f32,
|
|
195
|
+
) -> vec3<f32> {
|
|
208
196
|
// Directional contribution (1 light, View UBO).
|
|
209
197
|
var lit = spriteLitDirectional(albedo);
|
|
210
|
-
|
|
211
|
-
let
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
let spotCount = spotLightsBuffer.count;
|
|
218
|
-
for (var i : u32 = 0u; i < spotCount; i = i + 1u) {
|
|
219
|
-
let s = spotLightsBuffer.slots[i];
|
|
220
|
-
lit = lit + spriteLitSpot(s, worldPos, albedo);
|
|
221
|
-
}
|
|
198
|
+
#ifdef CLUSTER_FORWARD_AVAILABLE
|
|
199
|
+
let viewDir = normalize(view.cameraPos - worldPos);
|
|
200
|
+
lit = lit + evaluateStandardClusterLights(
|
|
201
|
+
ndc, viewZ, worldPos, vec3<f32>(0.0, 0.0, 1.0),
|
|
202
|
+
viewDir, albedo, 0.0, 1.0, vec3<f32>(0.04), true,
|
|
203
|
+
);
|
|
204
|
+
#endif
|
|
222
205
|
return lit;
|
|
223
206
|
}
|
|
224
207
|
|
|
225
|
-
fn applySceneFog(viewParams : View, color : vec3<f32>, alpha : f32, worldPos : vec3<f32>) -> vec4<f32> {
|
|
226
|
-
var origin = viewParams.cameraPos;
|
|
227
|
-
var direction = normalize(worldPos - origin);
|
|
228
|
-
var rayDistance = length(worldPos - origin);
|
|
229
|
-
if (viewParams.temporalProjection.z >= 0.5) {
|
|
230
|
-
let nearH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 0.0, 1.0);
|
|
231
|
-
let farH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 1.0, 1.0);
|
|
232
|
-
let nearPoint = nearH.xyz / nearH.w;
|
|
233
|
-
let farPoint = farH.xyz / farH.w;
|
|
234
|
-
direction = normalize(farPoint - nearPoint);
|
|
235
|
-
origin = worldPos - direction * dot(worldPos - viewParams.cameraPos, direction);
|
|
236
|
-
rayDistance = max(dot(worldPos - origin, direction), 0.0);
|
|
237
|
-
}
|
|
238
|
-
return apply_fog(viewParams.fog, FogRay(origin, direction, rayDistance), vec4<f32>(color, alpha));
|
|
239
|
-
}
|
|
240
|
-
|
|
241
208
|
@fragment
|
|
242
209
|
fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
|
243
210
|
let texel = sampleMaterialTexture(baseColorTexture, baseColorSampler, in.uv_atlas, material.baseColorTextureCoordinatesMetadata.zw);
|
|
244
211
|
let albedo4 = texel * material.colorTint;
|
|
245
|
-
let lit = spriteLitShadeAccum(albedo4.rgb, in.worldPos);
|
|
212
|
+
let lit = spriteLitShadeAccum(albedo4.rgb, in.worldPos, in.ndc, in.viewZ);
|
|
246
213
|
// Strict clamp 0..1 before premultiplied alpha multiply keeps the LDR
|
|
247
214
|
// path bounded even when a scene stacks many lights.
|
|
248
215
|
let lit_rgba = clamp(vec4<f32>(lit, albedo4.a), vec4<f32>(0.0), vec4<f32>(1.0));
|
|
249
|
-
let
|
|
250
|
-
let premult = vec4<f32>(fogged.rgb * fogged.a, fogged.a);
|
|
216
|
+
let premult = vec4<f32>(lit_rgba.rgb * lit_rgba.a, lit_rgba.a);
|
|
251
217
|
// LDR target is bgra8unorm; encode rgb via the sRGB transfer in-shader.
|
|
252
218
|
return vec4<f32>(
|
|
253
219
|
linear_to_srgb(premult.r),
|
|
@@ -261,13 +227,12 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
|
|
261
227
|
fn fs_main_hdr(in : VsOut) -> @location(0) vec4<f32> {
|
|
262
228
|
let texel = sampleMaterialTexture(baseColorTexture, baseColorSampler, in.uv_atlas, material.baseColorTextureCoordinatesMetadata.zw);
|
|
263
229
|
let albedo4 = texel * material.colorTint;
|
|
264
|
-
let lit = spriteLitShadeAccum(albedo4.rgb, in.worldPos);
|
|
230
|
+
let lit = spriteLitShadeAccum(albedo4.rgb, in.worldPos, in.ndc, in.viewZ);
|
|
265
231
|
// HDR variant: do NOT clamp the lit output to [0, 1]; let the tonemap
|
|
266
232
|
// pass absorb HDR values > 1. Alpha stays clamped because the premult
|
|
267
233
|
// math requires alpha in [0, 1].
|
|
268
234
|
let alpha = clamp(albedo4.a, 0.0, 1.0);
|
|
269
|
-
|
|
270
|
-
return vec4<f32>(fogged.rgb * fogged.a, fogged.a);
|
|
235
|
+
return vec4<f32>(lit * alpha, alpha);
|
|
271
236
|
}
|
|
272
237
|
|
|
273
238
|
struct TemporalVsOut {
|
package/src/sprite.wgsl
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
#pragma variant_axis STORAGE_BUFFER_AVAILABLE
|
|
3
3
|
#pragma variant_axis PER_INSTANCE_REGION
|
|
4
4
|
|
|
5
|
-
#import forgeax_view::common::{View,
|
|
5
|
+
#import forgeax_view::common::{View, Mesh, InstanceData, view, meshes, instances, sampleMaterialTexture}
|
|
6
6
|
#import forgeax_scene_temporal::{packSceneTemporalV1}
|
|
7
|
-
#import forgeax_view::fog::{apply_fog}
|
|
8
7
|
|
|
9
8
|
// @forgeax/engine-shader - sprite.wgsl (feat-20260520-2d-sprite-layer-mvp /
|
|
10
9
|
// M-3 / w19). 2D sprite material — third variant of the MaterialAsset
|
|
@@ -271,22 +270,6 @@ fn vs_main(in : VsIn, @builtin(instance_index) idx : u32, @builtin(vertex_index)
|
|
|
271
270
|
return out;
|
|
272
271
|
}
|
|
273
272
|
|
|
274
|
-
fn applySceneFog(viewParams : View, color : vec3<f32>, alpha : f32, worldPos : vec3<f32>) -> vec4<f32> {
|
|
275
|
-
var origin = viewParams.cameraPos;
|
|
276
|
-
var direction = normalize(worldPos - origin);
|
|
277
|
-
var rayDistance = length(worldPos - origin);
|
|
278
|
-
if (viewParams.temporalProjection.z >= 0.5) {
|
|
279
|
-
let nearH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 0.0, 1.0);
|
|
280
|
-
let farH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 1.0, 1.0);
|
|
281
|
-
let nearPoint = nearH.xyz / nearH.w;
|
|
282
|
-
let farPoint = farH.xyz / farH.w;
|
|
283
|
-
direction = normalize(farPoint - nearPoint);
|
|
284
|
-
origin = worldPos - direction * dot(worldPos - viewParams.cameraPos, direction);
|
|
285
|
-
rayDistance = max(dot(worldPos - origin, direction), 0.0);
|
|
286
|
-
}
|
|
287
|
-
return apply_fog(viewParams.fog, FogRay(origin, direction, rayDistance), vec4<f32>(color, alpha));
|
|
288
|
-
}
|
|
289
|
-
|
|
290
273
|
// linear_to_srgb: per-channel IEC 61966-2-1 transfer function used by the
|
|
291
274
|
// LDR fragment entry to encode linear premultiplied RGB into the bgra8unorm
|
|
292
275
|
// swap-chain storage. The bgra8unorm target is not hardware-sRGB-encoded
|
|
@@ -304,8 +287,7 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
|
|
304
287
|
let rgba = clamp(texel * material.colorTint, vec4<f32>(0.0), vec4<f32>(1.0));
|
|
305
288
|
// Premultiplied alpha: rgb pre-multiplied by alpha for srcFactor='one' /
|
|
306
289
|
// dstFactor='one-minus-src-alpha' blend.
|
|
307
|
-
let
|
|
308
|
-
let premult = vec4<f32>(fogged.rgb * fogged.a, fogged.a);
|
|
290
|
+
let premult = vec4<f32>(rgba.rgb * rgba.a, rgba.a);
|
|
309
291
|
// LDR target is bgra8unorm (blendable per WebGPU spec); hardware does not
|
|
310
292
|
// sRGB-encode bgra8unorm, so apply the transfer function in the shader.
|
|
311
293
|
// Only RGB channels are encoded; alpha is linear throughout the blend.
|
|
@@ -324,8 +306,7 @@ fn fs_main_hdr(in : VsOut) -> @location(0) vec4<f32> {
|
|
|
324
306
|
let texel = sampleMaterialTexture(baseColorTexture, baseColorSampler, in.uv_atlas, material.baseColorTextureCoordinatesMetadata.zw);
|
|
325
307
|
// R6 mitigation: strict clamp 0..1 before premultiplied alpha multiply.
|
|
326
308
|
let rgba = clamp(texel * material.colorTint, vec4<f32>(0.0), vec4<f32>(1.0));
|
|
327
|
-
|
|
328
|
-
return vec4<f32>(fogged.rgb * fogged.a, fogged.a);
|
|
309
|
+
return vec4<f32>(rgba.rgb * rgba.a, rgba.a);
|
|
329
310
|
}
|
|
330
311
|
|
|
331
312
|
struct TemporalVsOut {
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
//
|
|
1
|
+
// standard-cluster.wgsl — Standard clustered punctual light evaluation.
|
|
2
2
|
// feat-20260608-cluster-lighting M4 / w17.
|
|
3
3
|
//
|
|
4
4
|
// The cluster module owns membership and raw payload decoding only. Punctual
|
|
5
5
|
// BRDF, range, cone, PCF, and fail-open behavior live in lighting-punctual.
|
|
6
6
|
|
|
7
|
-
#define_import_path
|
|
7
|
+
#define_import_path forgeax_standard::cluster
|
|
8
8
|
|
|
9
|
-
#import forgeax_pbr::lighting_punctual::{evalPoint, evalSpot, evalSpotShadowed}
|
|
9
|
+
#import forgeax_pbr::lighting_punctual::{evalPoint, evalPointFlat, evalSpot, evalSpotFlat, evalSpotShadowed}
|
|
10
|
+
#import forgeax_pbr::lighting_attenuation::{projectSpotUv}
|
|
10
11
|
#import forgeax_view::common::{view}
|
|
12
|
+
#ifdef PROJECTOR_AVAILABLE
|
|
13
|
+
#import forgeax_view::common::{projectorTexture, projectorSampler}
|
|
14
|
+
#endif
|
|
11
15
|
#ifdef POINT_SHADOW_AVAILABLE
|
|
12
16
|
#import forgeax_pbr::lighting_punctual::{evalPointShadowed}
|
|
13
17
|
#endif
|
|
@@ -16,6 +20,11 @@
|
|
|
16
20
|
|
|
17
21
|
const KIND_POINT: u32 = 0u;
|
|
18
22
|
const KIND_SPOT: u32 = 1u;
|
|
23
|
+
// LightSlot.kind_and_shadow.y uses one high bit as the accepted projector
|
|
24
|
+
// marker. The remaining bits are the shadow-atlas tile (0..3), with zero as
|
|
25
|
+
// the explicit View-UBO lane for projector-only spots.
|
|
26
|
+
const PROJECTOR_FLAG: i32 = 0x40000000;
|
|
27
|
+
const TILE_MASK: i32 = 0x3fffffff;
|
|
19
28
|
|
|
20
29
|
// LightSlot is the byte-frozen 64B std430/std140 transport contract.
|
|
21
30
|
// [0..2] position, [3] invRangeSquared
|
|
@@ -37,13 +46,9 @@ struct ClusterUniform {
|
|
|
37
46
|
near_far_log : vec4<f32>,
|
|
38
47
|
};
|
|
39
48
|
|
|
40
|
-
#if STORAGE_BUFFER_AVAILABLE == true
|
|
41
49
|
@group(2) @binding(3) var<storage, read> light_data : array<LightSlot, 256>;
|
|
42
50
|
@group(2) @binding(4) var<storage, read> cluster_grid : array<u32>;
|
|
43
51
|
@group(2) @binding(5) var<storage, read> light_index_list : array<u32>;
|
|
44
|
-
#else
|
|
45
|
-
@group(2) @binding(3) var<uniform> light_data_uniform : array<LightSlot, 128>;
|
|
46
|
-
#endif
|
|
47
52
|
@group(2) @binding(6) var<uniform> cluster_uniform : ClusterUniform;
|
|
48
53
|
|
|
49
54
|
fn get_ssao_intensity() -> f32 {
|
|
@@ -84,6 +89,26 @@ fn ndc_position_to_cluster(
|
|
|
84
89
|
return vec3(cx, cy, cz);
|
|
85
90
|
}
|
|
86
91
|
|
|
92
|
+
// Surface SpotLight projector sampling is owned by the same clustered light
|
|
93
|
+
// evaluator as the analytic cone/range contribution. The host marks the
|
|
94
|
+
// selected projector in the LightSlot flag and writes its matrix to the
|
|
95
|
+
// corresponding View lane (lane 0 for projector-only); no second
|
|
96
|
+
// material-local sampling loop or resource owner is allowed.
|
|
97
|
+
fn sampleStandardSpotProjector(
|
|
98
|
+
lightViewProj : mat4x4<f32>,
|
|
99
|
+
world_pos : vec3<f32>,
|
|
100
|
+
) -> vec3<f32> {
|
|
101
|
+
#ifdef PROJECTOR_AVAILABLE
|
|
102
|
+
let uv = projectSpotUv(lightViewProj, world_pos);
|
|
103
|
+
if (!(uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0)) {
|
|
104
|
+
return vec3<f32>(1.0);
|
|
105
|
+
}
|
|
106
|
+
return textureSampleLevel(projectorTexture, projectorSampler, uv, 0.0).rgb;
|
|
107
|
+
#else
|
|
108
|
+
return vec3<f32>(1.0);
|
|
109
|
+
#endif
|
|
110
|
+
}
|
|
111
|
+
|
|
87
112
|
// Decode one kind first, then delegate the complete evaluation to the shared
|
|
88
113
|
// punctual owner. Unknown kinds are explicitly zero contribution.
|
|
89
114
|
fn evaluate_cluster_light(
|
|
@@ -95,9 +120,16 @@ fn evaluate_cluster_light(
|
|
|
95
120
|
metallic : f32,
|
|
96
121
|
alpha_sq : f32,
|
|
97
122
|
f0 : vec3<f32>,
|
|
123
|
+
flat_2d : bool,
|
|
98
124
|
) -> vec3<f32> {
|
|
99
125
|
let kind = light.kind_and_shadow.x;
|
|
100
126
|
if (kind == KIND_POINT) {
|
|
127
|
+
if (flat_2d) {
|
|
128
|
+
return evalPointFlat(
|
|
129
|
+
light.position.xyz, light.color.xyz, light.position.w,
|
|
130
|
+
world_pos, base_color,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
101
133
|
#ifdef POINT_SHADOW_AVAILABLE
|
|
102
134
|
let layer = bitcast<i32>(light.kind_and_shadow.y);
|
|
103
135
|
if (layer >= 0) {
|
|
@@ -117,8 +149,36 @@ fn evaluate_cluster_light(
|
|
|
117
149
|
);
|
|
118
150
|
}
|
|
119
151
|
if (kind == KIND_SPOT) {
|
|
120
|
-
|
|
121
|
-
|
|
152
|
+
if (flat_2d) {
|
|
153
|
+
return evalSpotFlat(
|
|
154
|
+
light.position.xyz, light.direction.xyz, light.color.xyz,
|
|
155
|
+
light.color.w, light.direction.w, light.position.w,
|
|
156
|
+
world_pos, base_color,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
let encoded_tile = bitcast<i32>(light.kind_and_shadow.y);
|
|
160
|
+
// The unshadowed sentinel is -1. A signed bitwise test against the
|
|
161
|
+
// projector flag would misclassify that sentinel because -1 has every bit
|
|
162
|
+
// set, so admit the flag only from the non-negative encoded domain.
|
|
163
|
+
if (encoded_tile >= 0 && (encoded_tile & PROJECTOR_FLAG) != 0) {
|
|
164
|
+
let tile = encoded_tile & TILE_MASK;
|
|
165
|
+
if (tile < 4) {
|
|
166
|
+
let projector = sampleStandardSpotProjector(view.spotLightViewProj[tile], world_pos);
|
|
167
|
+
return evalSpotShadowed(
|
|
168
|
+
light.position.xyz, light.direction.xyz, light.color.xyz,
|
|
169
|
+
// LightSlot packs cosInner in color.w and cosOuter in direction.w;
|
|
170
|
+
// keep the evaluator's named cone order intact at the decode boundary.
|
|
171
|
+
light.color.w, light.direction.w, light.position.w,
|
|
172
|
+
world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
|
|
173
|
+
view.spotLightViewProj[tile], tile, 0.005, 0.05,
|
|
174
|
+
bitcast<f32>(light.kind_and_shadow.w),
|
|
175
|
+
bitcast<f32>(light.kind_and_shadow.z),
|
|
176
|
+
) * projector;
|
|
177
|
+
}
|
|
178
|
+
return vec3<f32>(0.0);
|
|
179
|
+
}
|
|
180
|
+
if (encoded_tile >= 0 && encoded_tile < 4) {
|
|
181
|
+
let tile = encoded_tile;
|
|
122
182
|
return evalSpotShadowed(
|
|
123
183
|
light.position.xyz, light.direction.xyz, light.color.xyz,
|
|
124
184
|
// LightSlot packs cosInner in color.w and cosOuter in direction.w;
|
|
@@ -126,6 +186,8 @@ fn evaluate_cluster_light(
|
|
|
126
186
|
light.color.w, light.direction.w, light.position.w,
|
|
127
187
|
world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
|
|
128
188
|
view.spotLightViewProj[tile], tile, 0.005, 0.05,
|
|
189
|
+
bitcast<f32>(light.kind_and_shadow.w),
|
|
190
|
+
bitcast<f32>(light.kind_and_shadow.z),
|
|
129
191
|
);
|
|
130
192
|
}
|
|
131
193
|
return evalSpot(
|
|
@@ -137,7 +199,7 @@ fn evaluate_cluster_light(
|
|
|
137
199
|
return vec3<f32>(0.0);
|
|
138
200
|
}
|
|
139
201
|
|
|
140
|
-
fn
|
|
202
|
+
fn evaluateStandardClusterLights(
|
|
141
203
|
ndc : vec3<f32>,
|
|
142
204
|
view_z : f32,
|
|
143
205
|
world_pos : vec3<f32>,
|
|
@@ -147,6 +209,7 @@ fn evaluate_cluster_lights(
|
|
|
147
209
|
metallic : f32,
|
|
148
210
|
alpha_sq : f32,
|
|
149
211
|
f0 : vec3<f32>,
|
|
212
|
+
flat_2d : bool,
|
|
150
213
|
) -> vec3<f32> {
|
|
151
214
|
let gx = cluster_uniform.grid.x;
|
|
152
215
|
let gy = cluster_uniform.grid.y;
|
|
@@ -156,7 +219,6 @@ fn evaluate_cluster_lights(
|
|
|
156
219
|
let log_far = cluster_uniform.near_far_log.z;
|
|
157
220
|
var total_radiance = vec3<f32>(0.0);
|
|
158
221
|
|
|
159
|
-
#if STORAGE_BUFFER_AVAILABLE == true
|
|
160
222
|
let cluster_idx = ndc_position_to_cluster(ndc, view_z, gx, gy, gz, near, far, log_far);
|
|
161
223
|
let cluster_linear = cluster_idx.z * gy * gx + cluster_idx.y * gx + cluster_idx.x;
|
|
162
224
|
let grid_offset = cluster_linear * 2u;
|
|
@@ -166,20 +228,9 @@ fn evaluate_cluster_lights(
|
|
|
166
228
|
let light = light_data[light_index_list[list_offset + i]];
|
|
167
229
|
total_radiance += evaluate_cluster_light(
|
|
168
230
|
light, world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
|
|
231
|
+
flat_2d,
|
|
169
232
|
);
|
|
170
233
|
}
|
|
171
|
-
#else
|
|
172
|
-
let light_count = min(cluster_uniform.grid.w, 128u);
|
|
173
|
-
for (var i = 0u; i < 128u; i = i + 1u) {
|
|
174
|
-
if (i >= light_count) {
|
|
175
|
-
break;
|
|
176
|
-
}
|
|
177
|
-
total_radiance += evaluate_cluster_light(
|
|
178
|
-
light_data_uniform[i], world_pos, normal, view_dir,
|
|
179
|
-
base_color, metallic, alpha_sq, f0,
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
#endif
|
|
183
234
|
return total_radiance;
|
|
184
235
|
}
|
|
185
236
|
|
package/src/tonemap.wgsl
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
#import forgeax_view::common::FullscreenOutput
|
|
11
11
|
#import forgeax_view::common::fullscreen_triangle
|
|
12
|
-
#import forgeax_view::common::linearToSrgbOetf
|
|
12
|
+
#import forgeax_view::common::{linearToSrgbOetf, ditherUnorm8}
|
|
13
13
|
|
|
14
14
|
const TONEMAP_LUMINANCE_EPSILON : f32 = 1e-5;
|
|
15
15
|
|
|
@@ -17,7 +17,7 @@ struct TonemapParams {
|
|
|
17
17
|
exposure : f32,
|
|
18
18
|
whitePoint : f32,
|
|
19
19
|
mode : u32,
|
|
20
|
-
|
|
20
|
+
ditherEnabled : f32,
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
@group(1) @binding(0) var hdr : texture_2d<f32>;
|
|
@@ -171,5 +171,10 @@ fn fs_main(in : FullscreenOutput) -> @location(0) vec4<f32> {
|
|
|
171
171
|
}
|
|
172
172
|
// Every public mode reaches exactly one shared sRGB OETF.
|
|
173
173
|
let encoded = linearToSrgbOetf(mapped);
|
|
174
|
-
|
|
174
|
+
let output = select(
|
|
175
|
+
encoded,
|
|
176
|
+
ditherUnorm8(encoded, in.position.xy),
|
|
177
|
+
params.ditherEnabled > 0.5,
|
|
178
|
+
);
|
|
179
|
+
return vec4<f32>(output, source.a);
|
|
175
180
|
}
|
package/src/unlit.wgsl
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#define_import_path forgeax_material::unlit
|
|
2
|
-
#import forgeax_view::common::{View,
|
|
2
|
+
#import forgeax_view::common::{View, Mesh, InstanceData, view, meshes, instances, sampleMaterialTextureLinear}
|
|
3
3
|
#import forgeax_scene_temporal::{packSceneTemporalV1}
|
|
4
|
-
#import forgeax_view::fog::{apply_fog}
|
|
5
4
|
|
|
6
5
|
#pragma variant_axis STORAGE_BUFFER_AVAILABLE
|
|
7
6
|
#pragma variant_axis VERTEX_COLOR_AVAILABLE
|
|
@@ -101,22 +100,6 @@ fn materialVertexColor(in : VsOut) -> vec4<f32> {
|
|
|
101
100
|
#endif
|
|
102
101
|
}
|
|
103
102
|
|
|
104
|
-
fn applySceneFog(viewParams : View, color : vec3<f32>, alpha : f32, worldPos : vec3<f32>) -> vec4<f32> {
|
|
105
|
-
var origin = viewParams.cameraPos;
|
|
106
|
-
var direction = normalize(worldPos - origin);
|
|
107
|
-
var rayDistance = length(worldPos - origin);
|
|
108
|
-
if (viewParams.temporalProjection.z >= 0.5) {
|
|
109
|
-
let nearH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 0.0, 1.0);
|
|
110
|
-
let farH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 1.0, 1.0);
|
|
111
|
-
let nearPoint = nearH.xyz / nearH.w;
|
|
112
|
-
let farPoint = farH.xyz / farH.w;
|
|
113
|
-
direction = normalize(farPoint - nearPoint);
|
|
114
|
-
origin = worldPos - direction * dot(worldPos - viewParams.cameraPos, direction);
|
|
115
|
-
rayDistance = max(dot(worldPos - origin, direction), 0.0);
|
|
116
|
-
}
|
|
117
|
-
return apply_fog(viewParams.fog, FogRay(origin, direction, rayDistance), vec4<f32>(color, alpha));
|
|
118
|
-
}
|
|
119
|
-
|
|
120
103
|
@fragment
|
|
121
104
|
fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
|
122
105
|
let texSample = sampleMaterialTextureLinear(baseColorTexture, baseColorSampler, in.uv, material.baseColorTextureCoordinatesMetadata.zw);
|
|
@@ -125,12 +108,7 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
|
|
125
108
|
if (material.alphaCutoff > 0.0 && alpha < material.alphaCutoff) {
|
|
126
109
|
discard;
|
|
127
110
|
}
|
|
128
|
-
return
|
|
129
|
-
view,
|
|
130
|
-
material.baseColor.rgb * texSample.rgb * vertexColor.rgb,
|
|
131
|
-
alpha,
|
|
132
|
-
in.worldPos,
|
|
133
|
-
);
|
|
111
|
+
return vec4<f32>(material.baseColor.rgb * texSample.rgb * vertexColor.rgb, alpha);
|
|
134
112
|
}
|
|
135
113
|
|
|
136
114
|
struct TemporalVsOut {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#define_import_path forgeax_view::volume_composite
|
|
2
|
+
// This is a view-UBO projection, not a second matrix source. The fields and
|
|
3
|
+
// padding preserve common::View camera/inverse-projection byte offsets.
|
|
4
|
+
struct VolumeDepthView {
|
|
5
|
+
_prefix : array<vec4<f32>, 6>,
|
|
6
|
+
cameraPos : vec4<f32>,
|
|
7
|
+
_lightViewProjA : mat4x4<f32>,
|
|
8
|
+
inverseViewProj : mat4x4<f32>,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
struct VolumeOutput {
|
|
12
|
+
@builtin(position) position : vec4<f32>,
|
|
13
|
+
@location(0) uv : vec2<f32>,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
@group(0) @binding(0) var resolved_volume : texture_2d<f32>;
|
|
17
|
+
@group(0) @binding(1) var volume_sampler : sampler;
|
|
18
|
+
@group(0) @binding(2) var scene_depth : texture_depth_2d;
|
|
19
|
+
@group(0) @binding(3) var<uniform> volume_view : VolumeDepthView;
|
|
20
|
+
|
|
21
|
+
@vertex
|
|
22
|
+
fn volume_vs(@builtin(vertex_index) index : u32) -> VolumeOutput {
|
|
23
|
+
var positions = array<vec2<f32>, 3>(
|
|
24
|
+
vec2<f32>(-1.0, -1.0),
|
|
25
|
+
vec2<f32>(3.0, -1.0),
|
|
26
|
+
vec2<f32>(-1.0, 3.0),
|
|
27
|
+
);
|
|
28
|
+
var output : VolumeOutput;
|
|
29
|
+
output.position = vec4<f32>(positions[index], 0.0, 1.0);
|
|
30
|
+
output.uv = vec2<f32>(
|
|
31
|
+
positions[index].x * 0.5 + 0.5,
|
|
32
|
+
0.5 - positions[index].y * 0.5,
|
|
33
|
+
);
|
|
34
|
+
return output;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fn composite_resolved_volume(sample : vec4<f32>) -> vec4<f32> {
|
|
38
|
+
let transmittance = clamp(sample.a, 0.0, 1.0);
|
|
39
|
+
return vec4<f32>(sample.rgb, 1.0 - transmittance);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fn linear_scene_depth(uv : vec2<f32>) -> f32 {
|
|
43
|
+
let dimensions = max(vec2<f32>(textureDimensions(scene_depth)), vec2<f32>(1.0));
|
|
44
|
+
let pixel = vec2<i32>(clamp(uv * dimensions, vec2<f32>(0.0), dimensions - vec2<f32>(1.0)));
|
|
45
|
+
let device_depth = textureLoad(scene_depth, pixel, 0);
|
|
46
|
+
// WebGPU's framebuffer Y is opposite the view UBO's NDC Y.
|
|
47
|
+
let clip = vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, device_depth, 1.0);
|
|
48
|
+
let world = volume_view.inverseViewProj * clip;
|
|
49
|
+
let world_position = world.xyz / max(abs(world.w), 1e-5);
|
|
50
|
+
return distance(world_position, volume_view.cameraPos.xyz);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fn radiance_luma(sample : vec4<f32>) -> f32 {
|
|
54
|
+
return max(dot(sample.rgb, vec3<f32>(0.2126, 0.7152, 0.0722)), 0.0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn radiance_luma_weight(center : vec4<f32>, neighbor : vec4<f32>) -> f32 {
|
|
58
|
+
let center_luma = radiance_luma(center);
|
|
59
|
+
let neighbor_luma = radiance_luma(neighbor);
|
|
60
|
+
let relative_luma = abs(neighbor_luma - center_luma) /
|
|
61
|
+
max(max(neighbor_luma, center_luma), 1e-4);
|
|
62
|
+
return exp(-relative_luma * 8.0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fn edge_aware_resolved_volume(uv : vec2<f32>) -> vec4<f32> {
|
|
66
|
+
let dimensions = max(vec2<f32>(textureDimensions(resolved_volume)), vec2<f32>(1.0));
|
|
67
|
+
let texel = 1.0 / dimensions;
|
|
68
|
+
let center = textureSampleLevel(resolved_volume, volume_sampler, uv, 0.0);
|
|
69
|
+
let center_depth = linear_scene_depth(uv);
|
|
70
|
+
var accumulated = center * 4.0;
|
|
71
|
+
var total_weight = 4.0;
|
|
72
|
+
let offsets = array<vec2<f32>, 8>(
|
|
73
|
+
vec2<f32>(texel.x, 0.0),
|
|
74
|
+
vec2<f32>(-texel.x, 0.0),
|
|
75
|
+
vec2<f32>(0.0, texel.y),
|
|
76
|
+
vec2<f32>(0.0, -texel.y),
|
|
77
|
+
vec2<f32>(texel.x, texel.y),
|
|
78
|
+
vec2<f32>(-texel.x, texel.y),
|
|
79
|
+
vec2<f32>(texel.x, -texel.y),
|
|
80
|
+
vec2<f32>(-texel.x, -texel.y),
|
|
81
|
+
);
|
|
82
|
+
let spatial_weights = array<f32, 8>(2.0, 2.0, 2.0, 2.0, 1.0, 1.0, 1.0, 1.0);
|
|
83
|
+
for (var index = 0u; index < 8u; index += 1u) {
|
|
84
|
+
let neighbor_uv = clamp(uv + offsets[index], vec2<f32>(0.0), vec2<f32>(1.0));
|
|
85
|
+
let neighbor = textureSampleLevel(
|
|
86
|
+
resolved_volume,
|
|
87
|
+
volume_sampler,
|
|
88
|
+
neighbor_uv,
|
|
89
|
+
0.0,
|
|
90
|
+
);
|
|
91
|
+
let neighbor_depth = linear_scene_depth(neighbor_uv);
|
|
92
|
+
// Relative view-depth rejection keeps the silhouette transition local;
|
|
93
|
+
// a fixed world-unit threshold creates halos at far occluders.
|
|
94
|
+
let depth_weight = select(0.0, exp(-abs(neighbor_depth - center_depth) * 0.08),
|
|
95
|
+
abs(neighbor_depth - center_depth) <= max(center_depth * 0.08, 0.25));
|
|
96
|
+
// Transmittance is the available edge signal: retain hard depth/occluder
|
|
97
|
+
// boundaries while reconstructing smooth low-frequency scattering.
|
|
98
|
+
let edge_weight = spatial_weights[index] * depth_weight * exp(-abs(neighbor.a - center.a) * 6.0) *
|
|
99
|
+
radiance_luma_weight(center, neighbor);
|
|
100
|
+
accumulated += neighbor * edge_weight;
|
|
101
|
+
total_weight += edge_weight;
|
|
102
|
+
}
|
|
103
|
+
return accumulated / max(total_weight, 1e-5);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@fragment
|
|
107
|
+
fn volume_fs(input : VolumeOutput) -> @location(0) vec4<f32> {
|
|
108
|
+
// Injection zeroes samples behind the observed scene depth. Sampling the
|
|
109
|
+
// cumulative far slice therefore applies the full ray integral while the
|
|
110
|
+
// depth test remains owned by the injection path.
|
|
111
|
+
let sample = edge_aware_resolved_volume(input.uv);
|
|
112
|
+
return composite_resolved_volume(sample);
|
|
113
|
+
}
|