@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
|
@@ -1,24 +1,33 @@
|
|
|
1
1
|
#define_import_path forgeax_material::standard
|
|
2
|
-
#import forgeax_view::common::{View,
|
|
2
|
+
#import forgeax_view::common::{View, Mesh, InstanceData, view, meshes, instances, sampleMaterialTexture}
|
|
3
|
+
#import forgeax_scene_temporal::{sceneViewZ}
|
|
3
4
|
#import forgeax_pbr::temporal::{projectPbrSceneTemporal}
|
|
4
5
|
#import forgeax_pbr::brdf::{f_schlick, v_smith, d_ggx}
|
|
5
|
-
#import forgeax_pbr::ibl_sampling::{sampleIblDiffuse, sampleIblSpecular}
|
|
6
|
+
#import forgeax_pbr::ibl_sampling::{box_project, sampleIblDiffuse, sampleIblSpecular, sampleReflectionProbeSpecular}
|
|
6
7
|
#import forgeax_pbr::tbn::{decodeTangentSpaceNormalRg, scaleTangentSpaceNormal, applyTBN}
|
|
7
8
|
#import forgeax_pbr::lighting_directional::{evalDirectionalNoShadow, evalDirectionalShadowFactor}
|
|
8
|
-
#import forgeax_pbr::lighting_punctual::{evalPoint, evalSpot, evalSpotShadowed}
|
|
9
|
-
#import forgeax_view::fog::{apply_fog}
|
|
10
|
-
#ifdef POINT_SHADOW_AVAILABLE
|
|
11
|
-
#import forgeax_pbr::lighting_punctual::{evalPointShadowed}
|
|
12
|
-
#import forgeax_view::common::{shadowParams}
|
|
13
|
-
#endif
|
|
14
9
|
#ifdef CLUSTER_FORWARD_AVAILABLE
|
|
15
|
-
#import
|
|
10
|
+
#import forgeax_standard::cluster::{evaluateStandardClusterLights, get_ssao_intensity}
|
|
16
11
|
#endif
|
|
17
12
|
|
|
18
13
|
#pragma variant_axis STORAGE_BUFFER_AVAILABLE
|
|
19
14
|
#pragma variant_axis CLUSTER_FORWARD_AVAILABLE
|
|
20
15
|
#pragma variant_axis VERTEX_COLOR_AVAILABLE
|
|
21
16
|
#pragma variant_axis TRANSMISSION_AVAILABLE
|
|
17
|
+
#pragma variant_axis DIRECTIONAL_PCSS_AVAILABLE
|
|
18
|
+
#pragma variant_axis PROJECTOR_AVAILABLE
|
|
19
|
+
|
|
20
|
+
// Reflection probes remain a bounded scene-projected sampling lane. Their
|
|
21
|
+
// optional texture resources are the same Skylight inputs; when no probe is
|
|
22
|
+
// admitted, the explicit Skylight path below remains authoritative.
|
|
23
|
+
fn reflection_probe_sampling_contract(
|
|
24
|
+
worldPosition: vec3<f32>,
|
|
25
|
+
direction: vec3<f32>,
|
|
26
|
+
boxCenter: vec3<f32>,
|
|
27
|
+
boxExtents: vec3<f32>,
|
|
28
|
+
) -> vec3<f32> {
|
|
29
|
+
return box_project(worldPosition, direction, boxCenter, boxExtents);
|
|
30
|
+
}
|
|
22
31
|
|
|
23
32
|
// @forgeax/engine-shader - default-standard-pbr.wgsl
|
|
24
33
|
// (feat-20260523-shader-template-instance-split M5 / T04).
|
|
@@ -253,7 +262,11 @@ struct VsOut {
|
|
|
253
262
|
@location(1) worldNormal : vec3<f32>,
|
|
254
263
|
@location(2) uv : vec2<f32>,
|
|
255
264
|
@location(3) worldTangent : vec4<f32>,
|
|
256
|
-
|
|
265
|
+
#ifdef TRANSMISSION_AVAILABLE
|
|
266
|
+
// Keep mesh and instance storage vertex-only; transmission receives the
|
|
267
|
+
// transform basis from the vertex stage below.
|
|
268
|
+
@location(4) @interpolate(flat) transmissionBasis0 : vec4<f32>,
|
|
269
|
+
#endif
|
|
257
270
|
// feat-city-glb multi-UV tiling: second UV set inter-stage varying. Uses the
|
|
258
271
|
// previously-vacant @location(5) so @location(6)/(7) stay byte-stable with
|
|
259
272
|
// the prior layout (CSM M5/w19).
|
|
@@ -267,7 +280,7 @@ struct VsOut {
|
|
|
267
280
|
#ifdef VERTEX_COLOR_AVAILABLE
|
|
268
281
|
@location(14) color : vec4<f32>,
|
|
269
282
|
#endif
|
|
270
|
-
@location(6) ndc :
|
|
283
|
+
@location(6) ndc : vec4<f32>, // NDC for HDRP cluster lookup; .w = transmission basis tail
|
|
271
284
|
// feat-20260609-hdrp-cluster-fragment-ggx M4.5-followup: view-space z is
|
|
272
285
|
// needed by ndc_position_to_cluster (slice index uses log-z mapping that
|
|
273
286
|
// takes negative view_z, NOT NDC z which is [0,1]). Earlier `in.ndc.z`
|
|
@@ -276,6 +289,9 @@ struct VsOut {
|
|
|
276
289
|
// hot zone -- received zero light. Forward view_z explicitly. M5 / w19:
|
|
277
290
|
// also feeds CSM cascade selection in evalDirectional.
|
|
278
291
|
@location(7) viewZ : f32,
|
|
292
|
+
#ifdef TRANSMISSION_AVAILABLE
|
|
293
|
+
@location(15) @interpolate(flat) transmissionBasis1 : vec4<f32>,
|
|
294
|
+
#endif
|
|
279
295
|
};
|
|
280
296
|
|
|
281
297
|
fn pick_channel(rgba : vec4<f32>, channelIndex : u32) -> f32 {
|
|
@@ -289,25 +305,6 @@ fn pick_channel(rgba : vec4<f32>, channelIndex : u32) -> f32 {
|
|
|
289
305
|
}
|
|
290
306
|
}
|
|
291
307
|
|
|
292
|
-
fn applySceneFog(viewParams : View, color : vec3<f32>, alpha : f32, worldPos : vec3<f32>) -> vec4<f32> {
|
|
293
|
-
var origin = viewParams.cameraPos;
|
|
294
|
-
var direction = normalize(worldPos - origin);
|
|
295
|
-
var rayDistance = length(worldPos - origin);
|
|
296
|
-
if (viewParams.temporalProjection.z >= 0.5) {
|
|
297
|
-
let nearH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 0.0, 1.0);
|
|
298
|
-
let farH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 1.0, 1.0);
|
|
299
|
-
let nearPoint = nearH.xyz / nearH.w;
|
|
300
|
-
let farPoint = farH.xyz / farH.w;
|
|
301
|
-
direction = normalize(farPoint - nearPoint);
|
|
302
|
-
// Orthographic rays are parallel, but their origins differ per fragment.
|
|
303
|
-
// Project the current fragment onto the camera plane instead of reusing
|
|
304
|
-
// the center NDC ray origin for every pixel.
|
|
305
|
-
origin = worldPos - direction * dot(worldPos - viewParams.cameraPos, direction);
|
|
306
|
-
rayDistance = max(dot(worldPos - origin, direction), 0.0);
|
|
307
|
-
}
|
|
308
|
-
return apply_fog(viewParams.fog, FogRay(origin, direction, rayDistance), vec4<f32>(color, alpha));
|
|
309
|
-
}
|
|
310
|
-
|
|
311
308
|
// Light evaluators live in forgeax_pbr::lighting_directional +
|
|
312
309
|
// forgeax_pbr::lighting_punctual (M5 / T02). evalDirectional consumes the
|
|
313
310
|
// host's view UBO + shadowMap (LO 3.1.3 slope-scaled-bias 3x3 PCF;
|
|
@@ -356,6 +353,20 @@ fn vs_main(in : VsIn, @builtin(instance_index) idx : u32) -> VsOut {
|
|
|
356
353
|
// (w=0); .w handedness preserved for bitangent reconstruction in fragment.
|
|
357
354
|
let worldTangentXyz = normalize((entityWorld * instanceLocal * vec4<f32>(in.tangent.xyz, 0.0)).xyz);
|
|
358
355
|
out.worldTangent = vec4<f32>(worldTangentXyz, in.tangent.w);
|
|
356
|
+
#ifdef TRANSMISSION_AVAILABLE
|
|
357
|
+
out.transmissionBasis0 = vec4<f32>(
|
|
358
|
+
localToWorld[0].x,
|
|
359
|
+
localToWorld[0].y,
|
|
360
|
+
localToWorld[0].z,
|
|
361
|
+
localToWorld[1].x,
|
|
362
|
+
);
|
|
363
|
+
out.transmissionBasis1 = vec4<f32>(
|
|
364
|
+
localToWorld[1].y,
|
|
365
|
+
localToWorld[1].z,
|
|
366
|
+
localToWorld[2].x,
|
|
367
|
+
localToWorld[2].y,
|
|
368
|
+
);
|
|
369
|
+
#endif
|
|
359
370
|
out.uv = in.uv;
|
|
360
371
|
out.uv1 = in.uv1;
|
|
361
372
|
out.uv2 = in.uv2;
|
|
@@ -367,7 +378,6 @@ fn vs_main(in : VsIn, @builtin(instance_index) idx : u32) -> VsOut {
|
|
|
367
378
|
#ifdef VERTEX_COLOR_AVAILABLE
|
|
368
379
|
out.color = in.color;
|
|
369
380
|
#endif
|
|
370
|
-
out.instanceIdx = idx;
|
|
371
381
|
// feat-20260613-csm-cascaded-shadow-maps M5 / w19: the per-fragment
|
|
372
382
|
// light-space position varying is gone; evalDirectional computes
|
|
373
383
|
// per-cascade lightViewProj * worldPos in the fragment stage from
|
|
@@ -375,12 +385,14 @@ fn vs_main(in : VsIn, @builtin(instance_index) idx : u32) -> VsOut {
|
|
|
375
385
|
// NDC for HDRP cluster lookup (feat-20260609-hdrp-cluster-fragment-ggx M2 / w10).
|
|
376
386
|
// Perspective divide on clip-space position; ndc.z retains depth-buffer value.
|
|
377
387
|
let clipPos = out.clip;
|
|
378
|
-
out.ndc =
|
|
388
|
+
out.ndc = vec4(clipPos.xy / clipPos.w, clipPos.z / clipPos.w, localToWorld[2].z);
|
|
379
389
|
// Standard WebGPU/GL perspective projection has clip.w = -view.z, so
|
|
380
390
|
// view_z = -clip.w (negative in front of the camera). M4.5-followup:
|
|
381
391
|
// ndc_position_to_cluster needs this for log-z slice mapping; passing
|
|
382
392
|
// ndc.z (in [0,1]) instead made every fragment land in slice 0.
|
|
383
|
-
|
|
393
|
+
// Keep the cluster depth exactly aligned with the CPU binner for both
|
|
394
|
+
// perspective and off-axis orthographic projections.
|
|
395
|
+
out.viewZ = sceneViewZ(clipPos, view.temporalProjection);
|
|
384
396
|
return out;
|
|
385
397
|
}
|
|
386
398
|
|
|
@@ -585,12 +597,26 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
|
|
585
597
|
// shadow-map PCF lookup (CSM, feat-20260613).
|
|
586
598
|
let kD = (vec3<f32>(1.0) - f_schlick(max(dot(n, v), 0.0), f0)) * (1.0 - metallic);
|
|
587
599
|
let iblRoughness = clamp(max(finiteScalar(material.roughness, 0.5), 0.04) * roughnessTex, 0.04, 1.0);
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
600
|
+
var irradiance = vec3<f32>(0.0);
|
|
601
|
+
var specularIbl = vec3<f32>(0.0);
|
|
602
|
+
if (skylight.intensity < 0.0) {
|
|
603
|
+
// A negative intensity is the renderer-owned probe sentinel. The four
|
|
604
|
+
// color/rotation lanes carry probe center and half extents in this mode;
|
|
605
|
+
// Skylight resources remain unchanged and therefore preserve the ABI.
|
|
606
|
+
specularIbl = sampleReflectionProbeSpecular(
|
|
607
|
+
n, v, iblRoughness, f0, in.worldPos,
|
|
608
|
+
vec3<f32>(skylight.colorR, skylight.colorG, skylight.colorB),
|
|
609
|
+
skylight.rotation.xyz, vec4<f32>(0.0, 0.0, 0.0, 1.0),
|
|
610
|
+
prefilterMap, prefilterSampler, brdfLut, brdfLutSampler,
|
|
611
|
+
);
|
|
612
|
+
} else {
|
|
613
|
+
irradiance = sampleIblDiffuse(n, skylight.rotation, irradianceMap, irradianceSampler);
|
|
614
|
+
specularIbl = sampleIblSpecular(
|
|
615
|
+
n, v, iblRoughness, f0,
|
|
616
|
+
skylight.rotation,
|
|
617
|
+
prefilterMap, prefilterSampler, brdfLut, brdfLutSampler,
|
|
618
|
+
);
|
|
619
|
+
}
|
|
594
620
|
let occlusionUv = transformedMaterialUv(material.occlusionTextureCoordinatesTransform, material.occlusionTextureCoordinatesMetadata, in);
|
|
595
621
|
let aoSample = sampleMaterialTexture(occlusionTexture, occlusionSampler, occlusionUv, material.occlusionTextureCoordinatesMetadata.zw);
|
|
596
622
|
let ao = mix(1.0, aoSample.r, material.occlusionStrength);
|
|
@@ -607,7 +633,11 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
|
|
607
633
|
);
|
|
608
634
|
ambient = ambient + clearcoatIbl * material.clearcoat;
|
|
609
635
|
}
|
|
610
|
-
|
|
636
|
+
if (skylight.intensity < 0.0) {
|
|
637
|
+
ambient = ambient * (-skylight.intensity) * ao;
|
|
638
|
+
} else {
|
|
639
|
+
ambient = ambient * skyColor * skylight.intensity * ao;
|
|
640
|
+
}
|
|
611
641
|
#ifdef CLUSTER_FORWARD_AVAILABLE
|
|
612
642
|
// feat-20260612-hdrp-ssao M2 round-1 + M7 round-2 (plan-strategy D-7 + D-B + D-C):
|
|
613
643
|
// SSAO ambient synthesis. Reads the half-res R8 `ssaoBlurredTexture` from
|
|
@@ -634,10 +664,17 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
|
|
634
664
|
// the fragment stage preserves that contract and keeps thickness in the
|
|
635
665
|
// glTF unit-scale mesh space before measuring the refracted segment in world
|
|
636
666
|
// metres.
|
|
637
|
-
let
|
|
638
|
-
let
|
|
639
|
-
|
|
640
|
-
|
|
667
|
+
let localToWorld0 = in.transmissionBasis0.xyz;
|
|
668
|
+
let localToWorld1 = vec3<f32>(
|
|
669
|
+
in.transmissionBasis0.w,
|
|
670
|
+
in.transmissionBasis1.x,
|
|
671
|
+
in.transmissionBasis1.y,
|
|
672
|
+
);
|
|
673
|
+
let localToWorld2 = vec3<f32>(
|
|
674
|
+
in.transmissionBasis1.z,
|
|
675
|
+
in.transmissionBasis1.w,
|
|
676
|
+
in.ndc.w,
|
|
677
|
+
);
|
|
641
678
|
let inverseCofactor0 = cross(localToWorld1, localToWorld2);
|
|
642
679
|
let inverseCofactor1 = cross(localToWorld2, localToWorld0);
|
|
643
680
|
let inverseCofactor2 = cross(localToWorld0, localToWorld1);
|
|
@@ -721,110 +758,19 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
|
|
721
758
|
#ifdef CLUSTER_FORWARD_AVAILABLE
|
|
722
759
|
// NDC from vertex shader (perspective-divided clip-space, interpolated).
|
|
723
760
|
// view_z: NDC depth for cluster Z-slice lookup.
|
|
724
|
-
color = color +
|
|
761
|
+
color = color + evaluateStandardClusterLights(
|
|
762
|
+
in.ndc.xyz, in.viewZ, in.worldPos, n, v, diffuseAlbedo, metallic, a, f0, false,
|
|
763
|
+
);
|
|
725
764
|
if (material.clearcoat != 0.0) {
|
|
726
|
-
color = color + material.clearcoat *
|
|
727
|
-
in.ndc, in.viewZ, in.worldPos, n, v, vec3<f32>(0.0), 1.0, coatAlpha, vec3<f32>(0.04),
|
|
728
|
-
);
|
|
729
|
-
}
|
|
730
|
-
#else
|
|
731
|
-
let pointCount = pointLightsBuffer.count;
|
|
732
|
-
for (var i: u32 = 0u; i < pointCount; i = i + 1u) {
|
|
733
|
-
let p = pointLightsBuffer.slots[i];
|
|
734
|
-
#ifdef POINT_SHADOW_AVAILABLE
|
|
735
|
-
// feat-20260612-point-light-shadows-urp-hdrp Round-2 F-1: gate between
|
|
736
|
-
// shadowed and unshadowed evaluation on the host-assigned
|
|
737
|
-
// shadowAtlasLayer (>= 0 means PointLightShadow companion exists; -1
|
|
738
|
-
// sentinel means no shadow). shadowParams[layer] = (near, far,
|
|
739
|
-
// 1/(far-near), 0) is bound at @group(0) @binding(6) (shadowParams
|
|
740
|
-
// declared in common.wgsl).
|
|
741
|
-
if (p.shadowAtlasLayer >= 0) {
|
|
742
|
-
let lane = shadowParams[p.shadowAtlasLayer];
|
|
743
|
-
color = color + evalPointShadowed(
|
|
744
|
-
p.position, p.colorTimesIntensity, p.invRangeSquared,
|
|
745
|
-
in.worldPos, n, v, diffuseAlbedo, metallic, a, f0,
|
|
746
|
-
p.shadowAtlasLayer, lane.x, lane.y, 0.005, 0.05,
|
|
747
|
-
);
|
|
748
|
-
if (material.clearcoat != 0.0) {
|
|
749
|
-
color = color + material.clearcoat * evalPointShadowed(
|
|
750
|
-
p.position, p.colorTimesIntensity, p.invRangeSquared,
|
|
751
|
-
in.worldPos, n, v, vec3<f32>(0.0), 1.0, coatAlpha, vec3<f32>(0.04),
|
|
752
|
-
p.shadowAtlasLayer, lane.x, lane.y, 0.005, 0.05,
|
|
753
|
-
);
|
|
754
|
-
}
|
|
755
|
-
} else {
|
|
756
|
-
color = color + evalPoint(
|
|
757
|
-
p.position, p.colorTimesIntensity, p.invRangeSquared,
|
|
758
|
-
in.worldPos, n, v, diffuseAlbedo, metallic, a, f0,
|
|
759
|
-
);
|
|
760
|
-
if (material.clearcoat != 0.0) {
|
|
761
|
-
color = color + material.clearcoat * evalPoint(
|
|
762
|
-
p.position, p.colorTimesIntensity, p.invRangeSquared,
|
|
763
|
-
in.worldPos, n, v, vec3<f32>(0.0), 1.0, coatAlpha, vec3<f32>(0.04),
|
|
764
|
-
);
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
#else
|
|
768
|
-
color = color + evalPoint(
|
|
769
|
-
p.position, p.colorTimesIntensity, p.invRangeSquared,
|
|
770
|
-
in.worldPos, n, v, diffuseAlbedo, metallic, a, f0,
|
|
765
|
+
color = color + material.clearcoat * evaluateStandardClusterLights(
|
|
766
|
+
in.ndc.xyz, in.viewZ, in.worldPos, n, v, vec3<f32>(0.0), 1.0, coatAlpha, vec3<f32>(0.04), false,
|
|
771
767
|
);
|
|
772
|
-
if (material.clearcoat != 0.0) {
|
|
773
|
-
color = color + material.clearcoat * evalPoint(
|
|
774
|
-
p.position, p.colorTimesIntensity, p.invRangeSquared,
|
|
775
|
-
in.worldPos, n, v, vec3<f32>(0.0), 1.0, coatAlpha, vec3<f32>(0.04),
|
|
776
|
-
);
|
|
777
|
-
}
|
|
778
|
-
#endif
|
|
779
|
-
}
|
|
780
|
-
let spotCount = spotLightsBuffer.count;
|
|
781
|
-
for (var i: u32 = 0u; i < spotCount; i = i + 1u) {
|
|
782
|
-
let s = spotLightsBuffer.slots[i];
|
|
783
|
-
// feat-20260625-spot-light-shadow-mapping M3 / w15 (plan-strategy D-1
|
|
784
|
-
// fragment side + D-3 + D-4): gate on the host-assigned shadowAtlasTile
|
|
785
|
-
// (>= 0 means a spot atlas tile was allocated; -1 sentinel means no
|
|
786
|
-
// shadow / clipped / direction-degenerate). The shadowed path reads the
|
|
787
|
-
// per-spot perspective matrix from the View UBO `view.spotLightViewProj`
|
|
788
|
-
// array (lane N = shadowAtlasTile N; folded from the standalone binding 9
|
|
789
|
-
// into the View UBO in feat-20260625 w25 for WebGL2 uniform-buffer budget);
|
|
790
|
-
// the unshadowed path stays on evalSpot. bias hardcoded 0.005 / 0.05 aligns
|
|
791
|
-
// with the DirectionalLight defaults (D-6); the OOB/NaN gate lives inside
|
|
792
|
-
// evalSpotShadowed, not here.
|
|
793
|
-
if (s.shadowAtlasTile >= 0) {
|
|
794
|
-
color = color + evalSpotShadowed(
|
|
795
|
-
s.position, s.direction, s.colorTimesIntensity,
|
|
796
|
-
s.cosInner, s.cosOuter, s.invRangeSquared,
|
|
797
|
-
in.worldPos, n, v, diffuseAlbedo, metallic, a, f0,
|
|
798
|
-
view.spotLightViewProj[s.shadowAtlasTile], s.shadowAtlasTile, 0.005, 0.05,
|
|
799
|
-
);
|
|
800
|
-
if (material.clearcoat != 0.0) {
|
|
801
|
-
color = color + material.clearcoat * evalSpotShadowed(
|
|
802
|
-
s.position, s.direction, s.colorTimesIntensity,
|
|
803
|
-
s.cosInner, s.cosOuter, s.invRangeSquared,
|
|
804
|
-
in.worldPos, n, v, vec3<f32>(0.0), 1.0, coatAlpha, vec3<f32>(0.04),
|
|
805
|
-
view.spotLightViewProj[s.shadowAtlasTile], s.shadowAtlasTile, 0.005, 0.05,
|
|
806
|
-
);
|
|
807
|
-
}
|
|
808
|
-
} else {
|
|
809
|
-
color = color + evalSpot(
|
|
810
|
-
s.position, s.direction, s.colorTimesIntensity,
|
|
811
|
-
s.cosInner, s.cosOuter, s.invRangeSquared,
|
|
812
|
-
in.worldPos, n, v, diffuseAlbedo, metallic, a, f0,
|
|
813
|
-
);
|
|
814
|
-
if (material.clearcoat != 0.0) {
|
|
815
|
-
color = color + material.clearcoat * evalSpot(
|
|
816
|
-
s.position, s.direction, s.colorTimesIntensity,
|
|
817
|
-
s.cosInner, s.cosOuter, s.invRangeSquared,
|
|
818
|
-
in.worldPos, n, v, vec3<f32>(0.0), 1.0, coatAlpha, vec3<f32>(0.04),
|
|
819
|
-
);
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
768
|
}
|
|
823
769
|
#endif // CLUSTER_FORWARD_AVAILABLE
|
|
824
770
|
let emissiveUv = transformedMaterialUv(material.emissiveTextureCoordinatesTransform, material.emissiveTextureCoordinatesMetadata, in);
|
|
825
771
|
let emissiveSample = sampleMaterialTexture(emissiveTexture, emissiveSampler, emissiveUv, material.emissiveTextureCoordinatesMetadata.zw).rgb;
|
|
826
772
|
color = color + material.emissive * material.emissiveIntensity * emissiveSample;
|
|
827
|
-
return
|
|
773
|
+
return vec4<f32>(color, alpha);
|
|
828
774
|
}
|
|
829
775
|
|
|
830
776
|
// ── G-buffer output struct (feat-20260612-hdrp-deferred-shading M2 / w12) ──
|
|
@@ -912,6 +858,7 @@ struct TemporalVsOut {
|
|
|
912
858
|
@location(7) uv7 : vec2<f32>,
|
|
913
859
|
@location(8) @interpolate(perspective) currentClip : vec4<f32>,
|
|
914
860
|
@location(9) @interpolate(perspective) previousClip : vec4<f32>,
|
|
861
|
+
@location(10) @interpolate(flat) reactive : f32,
|
|
915
862
|
#ifdef VERTEX_COLOR_AVAILABLE
|
|
916
863
|
@location(14) color : vec4<f32>,
|
|
917
864
|
#endif
|
|
@@ -930,6 +877,11 @@ fn vs_temporal(in : VsIn, @builtin(instance_index) idx : u32) -> TemporalVsOut {
|
|
|
930
877
|
out.currentClip = view.temporalCurrentViewProj * currentWorld;
|
|
931
878
|
out.clip = out.currentClip;
|
|
932
879
|
out.previousClip = view.temporalPreviousViewProj * previousWorld;
|
|
880
|
+
#if STORAGE_BUFFER_AVAILABLE == true
|
|
881
|
+
out.reactive = meshes[0].temporal.x;
|
|
882
|
+
#else
|
|
883
|
+
out.reactive = 1.0;
|
|
884
|
+
#endif
|
|
933
885
|
out.uv = in.uv;
|
|
934
886
|
out.uv1 = in.uv1;
|
|
935
887
|
out.uv2 = in.uv2;
|
|
@@ -954,10 +906,6 @@ fn temporalVertexAlpha(in : TemporalVsOut) -> f32 {
|
|
|
954
906
|
|
|
955
907
|
@fragment
|
|
956
908
|
fn fs_temporal(in : TemporalVsOut) -> @location(0) vec4<f32> {
|
|
957
|
-
var reactive = 0.0;
|
|
958
|
-
#if STORAGE_BUFFER_AVAILABLE == true
|
|
959
|
-
reactive = meshes[0].temporal.x;
|
|
960
|
-
#endif
|
|
961
909
|
return projectPbrSceneTemporal(
|
|
962
910
|
material.baseColor.a * temporalVertexAlpha(in),
|
|
963
911
|
material.alphaCutoff,
|
|
@@ -968,7 +916,7 @@ fn fs_temporal(in : TemporalVsOut) -> @location(0) vec4<f32> {
|
|
|
968
916
|
in.currentClip,
|
|
969
917
|
in.previousClip,
|
|
970
918
|
view.temporalProjection,
|
|
971
|
-
reactive,
|
|
919
|
+
in.reactive,
|
|
972
920
|
in.uv, in.uv1, in.uv2, in.uv3,
|
|
973
921
|
in.uv4, in.uv5, in.uv6, in.uv7,
|
|
974
922
|
);
|
package/src/fxaa.wgsl
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
// FXAA 3.11 fragment shader -- faithful port of the canonical Simon
|
|
8
8
|
// Rodriguez reference (same lineage as Bevy's WGSL port), adapted to
|
|
9
9
|
// forgeax naga_oil conventions. Hardcoded "High" quality preset: 12-step
|
|
10
|
-
// edge search with compile-time step multipliers (
|
|
11
|
-
// branches).
|
|
10
|
+
// edge search with compile-time step multipliers (the only UBO field is the
|
|
11
|
+
// final-output dither switch; there are no quality #ifdef branches).
|
|
12
12
|
//
|
|
13
13
|
// Vertex stage: imports the SSOT fullscreen_triangle() from
|
|
14
14
|
// forgeax_view::common (single large triangle, research Finding 1).
|
|
@@ -35,10 +35,13 @@
|
|
|
35
35
|
// Bindings (group 0):
|
|
36
36
|
// @binding(0) screenTexture : texture_2d<f32> -- sampled display-encoded input
|
|
37
37
|
// @binding(1) samp : sampler -- linear filterable sampler
|
|
38
|
+
// @binding(2) params : FxaaParams -- final-output policy
|
|
38
39
|
// display-encoded: FXAA samples and writes display-encoded values; the Output
|
|
39
|
-
// Transform owns the sole linear-to-sRGB conversion before this pass.
|
|
40
|
+
// Transform owns the sole linear-to-sRGB conversion before this pass. FXAA is
|
|
41
|
+
// the final Standard surface writer, so the shared bounded dither is applied
|
|
42
|
+
// here, immediately before the surface's UNORM8 conversion.
|
|
40
43
|
|
|
41
|
-
#import forgeax_view::common::FullscreenOutput
|
|
44
|
+
#import forgeax_view::common::{FullscreenOutput, ditherUnorm8}
|
|
42
45
|
#import forgeax_view::common::fullscreen_triangle
|
|
43
46
|
|
|
44
47
|
const EDGE_THRESHOLD_MIN: f32 = 0.0312;
|
|
@@ -49,6 +52,18 @@ const ITERATIONS: i32 = 12;
|
|
|
49
52
|
@group(0) @binding(0) var screenTexture: texture_2d<f32>;
|
|
50
53
|
@group(0) @binding(1) var samp: sampler;
|
|
51
54
|
|
|
55
|
+
struct FxaaParams {
|
|
56
|
+
ditherEnabled: f32,
|
|
57
|
+
// Keep the uniform block at one 16-byte slot. WebGL2/GLES uniform-buffer
|
|
58
|
+
// validation uses the std140-aligned block size, while the host uploads a
|
|
59
|
+
// four-f32 (16-byte) payload.
|
|
60
|
+
_pad0: f32,
|
|
61
|
+
_pad1: f32,
|
|
62
|
+
_pad2: f32,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
@group(0) @binding(2) var<uniform> params: FxaaParams;
|
|
66
|
+
|
|
52
67
|
fn rgb2luma(rgb: vec3<f32>) -> f32 {
|
|
53
68
|
return sqrt(dot(rgb, vec3<f32>(0.299, 0.587, 0.114)));
|
|
54
69
|
}
|
|
@@ -105,7 +120,10 @@ fn fs_main(in: FullscreenOutput) -> @location(0) vec4<f32> {
|
|
|
105
120
|
|
|
106
121
|
// Not on an edge (or in a near-uniform region): pass the centre through.
|
|
107
122
|
if lumaRange < max(EDGE_THRESHOLD_MIN, lumaMax * EDGE_THRESHOLD_MAX) {
|
|
108
|
-
return vec4<f32>(
|
|
123
|
+
return vec4<f32>(
|
|
124
|
+
select(centerColor, ditherUnorm8(centerColor, in.position.xy), params.ditherEnabled > 0.5),
|
|
125
|
+
1.0,
|
|
126
|
+
);
|
|
109
127
|
}
|
|
110
128
|
|
|
111
129
|
// Lumas of the 4 corners.
|
|
@@ -254,5 +272,8 @@ fn fs_main(in: FullscreenOutput) -> @location(0) vec4<f32> {
|
|
|
254
272
|
}
|
|
255
273
|
|
|
256
274
|
let finalColor = sampleColor(finalUv);
|
|
257
|
-
return vec4<f32>(
|
|
275
|
+
return vec4<f32>(
|
|
276
|
+
select(finalColor, ditherUnorm8(finalColor, in.position.xy), params.ditherEnabled > 0.5),
|
|
277
|
+
1.0,
|
|
278
|
+
);
|
|
258
279
|
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
#define_import_path forgeax_view::hdrp_deferred_lighting
|
|
2
2
|
|
|
3
3
|
#import forgeax_view::common::FullscreenOutput
|
|
4
|
-
#import forgeax_view::common::
|
|
4
|
+
#import forgeax_view::common::View
|
|
5
5
|
#import forgeax_view::common::fullscreen_triangle
|
|
6
|
-
#import forgeax_view::fog::{apply_fog}
|
|
7
6
|
|
|
8
7
|
// HDRP deferred lighting producer. The lighting pass consumes the material
|
|
9
8
|
// G-buffer and current-frame depth, reconstructs the fragment world position,
|
|
10
|
-
// then
|
|
9
|
+
// then writes linear radiance for the shared volume composite pass.
|
|
11
10
|
|
|
12
11
|
@group(0) @binding(0) var<uniform> view : View;
|
|
13
12
|
|
|
@@ -24,28 +23,6 @@ fn vs_main(@builtin(vertex_index) vertex_index : u32) -> FullscreenOutput {
|
|
|
24
23
|
return fullscreen_triangle(vertex_index);
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
fn reconstructWorldPosition(uv : vec2<f32>, depth : f32) -> vec3<f32> {
|
|
28
|
-
let clip = vec4<f32>(uv * 2.0 - vec2<f32>(1.0), depth, 1.0);
|
|
29
|
-
let worldH = view.inverseViewProj * clip;
|
|
30
|
-
return worldH.xyz / worldH.w;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
fn fogDeferredRadiance(color : vec4<f32>, worldPos : vec3<f32>) -> vec4<f32> {
|
|
34
|
-
var origin = view.cameraPos;
|
|
35
|
-
var direction = normalize(worldPos - origin);
|
|
36
|
-
var rayDistance = length(worldPos - origin);
|
|
37
|
-
if (view.temporalProjection.z >= 0.5) {
|
|
38
|
-
let nearH = view.inverseViewProj * vec4<f32>(0.0, 0.0, 0.0, 1.0);
|
|
39
|
-
let farH = view.inverseViewProj * vec4<f32>(0.0, 0.0, 1.0, 1.0);
|
|
40
|
-
let nearPoint = nearH.xyz / nearH.w;
|
|
41
|
-
let farPoint = farH.xyz / farH.w;
|
|
42
|
-
direction = normalize(farPoint - nearPoint);
|
|
43
|
-
origin = worldPos - direction * dot(worldPos - view.cameraPos, direction);
|
|
44
|
-
rayDistance = max(dot(worldPos - origin, direction), 0.0);
|
|
45
|
-
}
|
|
46
|
-
return apply_fog(view.fog, FogRay(origin, direction, rayDistance), color);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
26
|
@fragment
|
|
50
27
|
fn fs_lighting(in : FullscreenOutput) -> @location(0) vec4<f32> {
|
|
51
28
|
// The lighting pass runs before the forward lane and shares its cleared
|
|
@@ -64,6 +41,5 @@ fn fs_lighting(in : FullscreenOutput) -> @location(0) vec4<f32> {
|
|
|
64
41
|
let light = max(dot(normal, normalize(-view.lightDir)), 0.0);
|
|
65
42
|
let direct = material.rgb * view.lightColor * light * clamp(emissiveSample.a, 0.0, 1.0);
|
|
66
43
|
let radiance = vec4<f32>(direct + emissiveSample.rgb, material.a);
|
|
67
|
-
|
|
68
|
-
return fogDeferredRadiance(radiance, worldPos);
|
|
44
|
+
return radiance;
|
|
69
45
|
}
|
package/src/ibl-sampling.wgsl
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
// - sampleIblDiffuse(N, irradianceMap, irradianceSampler)
|
|
17
17
|
// - sampleIblSpecular(N, V, roughness, F0, prefilterMap, prefilterSampler,
|
|
18
18
|
// brdfLut, brdfLutSampler)
|
|
19
|
+
// - box_project(worldPosition, direction, boxCenter, boxExtents)
|
|
20
|
+
// - sampleReflectionProbeSpecular(..., probeMap, probeSampler, ...)
|
|
19
21
|
|
|
20
22
|
#import forgeax_pbr::ibl_shared::{PI, fresnelSchlickRoughness, inverseRotateEnvironment}
|
|
21
23
|
|
|
@@ -56,3 +58,55 @@ fn sampleIblSpecular(
|
|
|
56
58
|
let F = fresnelSchlickRoughness(NdotV, F0, roughness);
|
|
57
59
|
return prefilteredColor * (F * envBRDF.r + envBRDF.g);
|
|
58
60
|
}
|
|
61
|
+
|
|
62
|
+
// Project a reflection ray from a point inside a probe box onto the box
|
|
63
|
+
// boundary. The helper is binding-free so the scene table and the material
|
|
64
|
+
// resource projection remain owned by the host record stage.
|
|
65
|
+
fn box_project(
|
|
66
|
+
worldPosition: vec3<f32>,
|
|
67
|
+
direction: vec3<f32>,
|
|
68
|
+
boxCenter: vec3<f32>,
|
|
69
|
+
boxExtents: vec3<f32>,
|
|
70
|
+
) -> vec3<f32> {
|
|
71
|
+
let safeExtents = max(boxExtents, vec3<f32>(0.0001));
|
|
72
|
+
let safeDirection = select(
|
|
73
|
+
vec3<f32>(0.0001),
|
|
74
|
+
direction,
|
|
75
|
+
abs(direction) >= vec3<f32>(0.0001),
|
|
76
|
+
);
|
|
77
|
+
let localPosition = worldPosition - boxCenter;
|
|
78
|
+
let edgeSign = select(vec3<f32>(-1.0), vec3<f32>(1.0), direction >= vec3<f32>(0.0));
|
|
79
|
+
let edge = edgeSign * safeExtents;
|
|
80
|
+
let distances = (edge - localPosition) / safeDirection;
|
|
81
|
+
let travel = min(distances.x, min(distances.y, distances.z));
|
|
82
|
+
return normalize(localPosition + direction * max(travel, 0.0));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Split-sum probe sample. The probe texture is supplied by the caller so a
|
|
86
|
+
// missing scene candidate can keep using sampleIblSpecular with the Skylight
|
|
87
|
+
// resources already present in the Standard material layout.
|
|
88
|
+
fn sampleReflectionProbeSpecular(
|
|
89
|
+
normal: vec3<f32>,
|
|
90
|
+
view: vec3<f32>,
|
|
91
|
+
roughness: f32,
|
|
92
|
+
F0: vec3<f32>,
|
|
93
|
+
worldPosition: vec3<f32>,
|
|
94
|
+
boxCenter: vec3<f32>,
|
|
95
|
+
boxExtents: vec3<f32>,
|
|
96
|
+
rotation: vec4<f32>,
|
|
97
|
+
probeMap: texture_cube<f32>,
|
|
98
|
+
probeSampler: sampler,
|
|
99
|
+
brdfLut: texture_2d<f32>,
|
|
100
|
+
brdfLutSampler: sampler,
|
|
101
|
+
) -> vec3<f32> {
|
|
102
|
+
let NdotV = max(dot(normal, view), 0.001);
|
|
103
|
+
let reflection = reflect(-view, normal);
|
|
104
|
+
let projected = box_project(worldPosition, reflection, boxCenter, boxExtents);
|
|
105
|
+
let rotated = inverseRotateEnvironment(projected, rotation);
|
|
106
|
+
let probeDirection = vec3<f32>(rotated.x, -rotated.y, rotated.z);
|
|
107
|
+
let mip = roughness * 4.0;
|
|
108
|
+
let prefilteredColor = textureSampleLevel(probeMap, probeSampler, probeDirection, mip).rgb;
|
|
109
|
+
let envBRDF = textureSample(brdfLut, brdfLutSampler, vec2<f32>(NdotV, roughness)).rg;
|
|
110
|
+
let F = fresnelSchlickRoughness(NdotV, F0, roughness);
|
|
111
|
+
return prefilteredColor * (F * envBRDF.r + envBRDF.g);
|
|
112
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -108,6 +108,12 @@ export const ENGINE_MATERIAL_MODULES = [
|
|
|
108
108
|
'forgeax_material::sprite-lit',
|
|
109
109
|
] as const;
|
|
110
110
|
|
|
111
|
+
/** Names of the zero-binding Standard reflection-probe shader helpers. */
|
|
112
|
+
export const REFLECTION_PROBE_SHADER_HELPERS = Object.freeze([
|
|
113
|
+
'box_project',
|
|
114
|
+
'sampleReflectionProbeSpecular',
|
|
115
|
+
] as const);
|
|
116
|
+
|
|
111
117
|
export function isEngineMaterialModule(module: string): boolean {
|
|
112
118
|
return (ENGINE_MATERIAL_MODULES as readonly string[]).includes(module);
|
|
113
119
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#define_import_path forgeax_pbr::lighting_attenuation
|
|
2
|
+
|
|
3
|
+
// Shared punctual light optics. Range is represented by the host-derived
|
|
4
|
+
// inverse squared range; the shader never reconstructs authoring units.
|
|
5
|
+
fn evalDistanceAttenuation(dSquared : f32, invRangeSquared : f32) -> f32 {
|
|
6
|
+
let safeDistance = max(dSquared, 1e-4);
|
|
7
|
+
let window = clamp(1.0 - (safeDistance * invRangeSquared) * (safeDistance * invRangeSquared), 0.0, 1.0);
|
|
8
|
+
return window * window / safeDistance;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
fn evalSpotAttenuation(
|
|
12
|
+
lightPos : vec3<f32>,
|
|
13
|
+
lightDir : vec3<f32>,
|
|
14
|
+
worldPos : vec3<f32>,
|
|
15
|
+
cosInner : f32,
|
|
16
|
+
cosOuter : f32,
|
|
17
|
+
invRangeSquared : f32,
|
|
18
|
+
) -> f32 {
|
|
19
|
+
let toLight = lightPos - worldPos;
|
|
20
|
+
let dSquared = dot(toLight, toLight);
|
|
21
|
+
let distance = evalDistanceAttenuation(dSquared, invRangeSquared);
|
|
22
|
+
let direction = normalize(select(vec3<f32>(0.0, 0.0, -1.0), toLight, dSquared > 1e-4));
|
|
23
|
+
let cone = smoothstep(cosOuter, cosInner, dot(direction, -normalize(lightDir)));
|
|
24
|
+
return distance * cone;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Isolated volume radiance helpers: this module has no resource bindings, so
|
|
28
|
+
// compute stages can reuse the exact surface attenuation and cone facts.
|
|
29
|
+
fn evalVolumePointRadiance(
|
|
30
|
+
lightPos : vec3<f32>,
|
|
31
|
+
colorTimesIntensity : vec3<f32>,
|
|
32
|
+
invRangeSquared : f32,
|
|
33
|
+
worldPos : vec3<f32>,
|
|
34
|
+
) -> vec3<f32> {
|
|
35
|
+
let toLight = lightPos - worldPos;
|
|
36
|
+
let dSquared = max(dot(toLight, toLight), 1e-4);
|
|
37
|
+
return colorTimesIntensity * evalDistanceAttenuation(dSquared, invRangeSquared);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fn evalVolumeSpotRadiance(
|
|
41
|
+
lightPos : vec3<f32>,
|
|
42
|
+
lightDir : vec3<f32>,
|
|
43
|
+
colorTimesIntensity : vec3<f32>,
|
|
44
|
+
cosInner : f32,
|
|
45
|
+
cosOuter : f32,
|
|
46
|
+
invRangeSquared : f32,
|
|
47
|
+
worldPos : vec3<f32>,
|
|
48
|
+
) -> vec3<f32> {
|
|
49
|
+
return colorTimesIntensity * evalSpotAttenuation(
|
|
50
|
+
lightPos, lightDir, worldPos, cosInner, cosOuter, invRangeSquared,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fn projectSpotUv(lightViewProj : mat4x4<f32>, worldPos : vec3<f32>) -> vec2<f32> {
|
|
55
|
+
let clip = lightViewProj * vec4<f32>(worldPos, 1.0);
|
|
56
|
+
let invW = select(1.0 / clip.w, 0.0, abs(clip.w) < 1e-6);
|
|
57
|
+
return vec2<f32>(clip.x * invW * 0.5 + 0.5, clip.y * invW * -0.5 + 0.5);
|
|
58
|
+
}
|