@forgeax/engine-shader 0.1.2
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 +202 -0
- package/README.md +99 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/ShaderRegistry.d.ts +208 -0
- package/dist/ShaderRegistry.d.ts.map +1 -0
- package/dist/__tests__/atmosphere-background.test.d.ts +2 -0
- package/dist/__tests__/atmosphere-background.test.d.ts.map +1 -0
- package/dist/__tests__/atmosphere-cubemap-orientation.test.d.ts +2 -0
- package/dist/__tests__/atmosphere-cubemap-orientation.test.d.ts.map +1 -0
- package/dist/__tests__/atmosphere-preetham-numeric.test.d.ts +2 -0
- package/dist/__tests__/atmosphere-preetham-numeric.test.d.ts.map +1 -0
- package/dist/__tests__/atmosphere-preetham-red.test.d.ts +2 -0
- package/dist/__tests__/atmosphere-preetham-red.test.d.ts.map +1 -0
- package/dist/__tests__/atmosphere-preetham.test.d.ts +2 -0
- package/dist/__tests__/atmosphere-preetham.test.d.ts.map +1 -0
- package/dist/__tests__/atmosphere-three-r184-reference.test.d.ts +2 -0
- package/dist/__tests__/atmosphere-three-r184-reference.test.d.ts.map +1 -0
- package/dist/__tests__/bloom-fxaa-tonemap.unit.test.d.ts +2 -0
- package/dist/__tests__/bloom-fxaa-tonemap.unit.test.d.ts.map +1 -0
- package/dist/__tests__/builtin-texture-sampling-contract.test.d.ts +2 -0
- package/dist/__tests__/builtin-texture-sampling-contract.test.d.ts.map +1 -0
- package/dist/__tests__/default-standard-pbr-alpha.unit.test.d.ts +2 -0
- package/dist/__tests__/default-standard-pbr-alpha.unit.test.d.ts.map +1 -0
- package/dist/__tests__/deferred-lighting-ssao.test.d.ts +2 -0
- package/dist/__tests__/deferred-lighting-ssao.test.d.ts.map +1 -0
- package/dist/__tests__/fog-oracle.unit.test.d.ts +2 -0
- package/dist/__tests__/fog-oracle.unit.test.d.ts.map +1 -0
- package/dist/__tests__/fog-orthographic-ray.unit.test.d.ts +2 -0
- package/dist/__tests__/fog-orthographic-ray.unit.test.d.ts.map +1 -0
- package/dist/__tests__/fog-producer-matrix.integration.test.d.ts +2 -0
- package/dist/__tests__/fog-producer-matrix.integration.test.d.ts.map +1 -0
- package/dist/__tests__/hdrp-ssao-compile.test.d.ts +2 -0
- package/dist/__tests__/hdrp-ssao-compile.test.d.ts.map +1 -0
- package/dist/__tests__/ibl-irradiance.unit.test.d.ts +2 -0
- package/dist/__tests__/ibl-irradiance.unit.test.d.ts.map +1 -0
- package/dist/__tests__/ibl-sampling.unit.test.d.ts +2 -0
- package/dist/__tests__/ibl-sampling.unit.test.d.ts.map +1 -0
- package/dist/__tests__/lighting-punctual.unit.test.d.ts +2 -0
- package/dist/__tests__/lighting-punctual.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-artifact-dedup.unit.test.d.ts +2 -0
- package/dist/__tests__/material-artifact-dedup.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-builtins.unit.test.d.ts +2 -0
- package/dist/__tests__/material-builtins.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-contract.unit.test.d.ts +2 -0
- package/dist/__tests__/material-contract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-derived-builtins.integration.test.d.ts +2 -0
- package/dist/__tests__/material-derived-builtins.integration.test.d.ts.map +1 -0
- package/dist/__tests__/public-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/public-surface.unit.test.d.ts.map +1 -0
- package/dist/__tests__/scene-temporal.unit.test.d.ts +2 -0
- package/dist/__tests__/scene-temporal.unit.test.d.ts.map +1 -0
- package/dist/__tests__/shader-registry-transaction.unit.test.d.ts +2 -0
- package/dist/__tests__/shader-registry-transaction.unit.test.d.ts.map +1 -0
- package/dist/__tests__/shader.unit.test.d.ts +2 -0
- package/dist/__tests__/shader.unit.test.d.ts.map +1 -0
- package/dist/__tests__/sprite-lit-shader.test.d.ts +2 -0
- package/dist/__tests__/sprite-lit-shader.test.d.ts.map +1 -0
- package/dist/__tests__/sprite-variants.unit.test.d.ts +2 -0
- package/dist/__tests__/sprite-variants.unit.test.d.ts.map +1 -0
- package/dist/__tests__/taa-resolve.unit.test.d.ts +2 -0
- package/dist/__tests__/taa-resolve.unit.test.d.ts.map +1 -0
- package/dist/__tests__/transparent-pbr.unit.test.d.ts +2 -0
- package/dist/__tests__/transparent-pbr.unit.test.d.ts.map +1 -0
- package/dist/__tests__/vertex-color-variant.unit.test.d.ts +2 -0
- package/dist/__tests__/vertex-color-variant.unit.test.d.ts.map +1 -0
- package/dist/errors.d.ts +96 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +560 -0
- package/dist/index.mjs.map +1 -0
- package/dist/material/artifact-registry.d.ts +31 -0
- package/dist/material/artifact-registry.d.ts.map +1 -0
- package/dist/material/artifact-types.d.ts +13 -0
- package/dist/material/artifact-types.d.ts.map +1 -0
- package/dist/material-schemas.d.ts +8 -0
- package/dist/material-schemas.d.ts.map +1 -0
- package/dist/register-default-sprite-lit.d.ts +40 -0
- package/dist/register-default-sprite-lit.d.ts.map +1 -0
- package/dist/register-default-standard-pbr-skin.d.ts +34 -0
- package/dist/register-default-standard-pbr-skin.d.ts.map +1 -0
- package/dist/tonemap.d.ts +13 -0
- package/dist/tonemap.d.ts.map +1 -0
- package/dist/types.d.ts +47 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +58 -0
- package/src/ShaderRegistry.ts +519 -0
- package/src/__tests__/atmosphere-background.test.ts +33 -0
- package/src/__tests__/atmosphere-cubemap-orientation.test.ts +68 -0
- package/src/__tests__/atmosphere-preetham-numeric.test.ts +122 -0
- package/src/__tests__/atmosphere-preetham-red.test.ts +52 -0
- package/src/__tests__/atmosphere-preetham.test.ts +132 -0
- package/src/__tests__/atmosphere-three-r184-reference.test.ts +122 -0
- package/src/__tests__/bloom-fxaa-tonemap.unit.test.ts +31 -0
- package/src/__tests__/builtin-texture-sampling-contract.test.ts +56 -0
- package/src/__tests__/default-standard-pbr-alpha.unit.test.ts +19 -0
- package/src/__tests__/deferred-lighting-ssao.test.ts +114 -0
- package/src/__tests__/fog-oracle.unit.test.ts +132 -0
- package/src/__tests__/fog-orthographic-ray.unit.test.ts +62 -0
- package/src/__tests__/fog-producer-matrix.integration.test.ts +77 -0
- package/src/__tests__/hdrp-ssao-compile.test.ts +185 -0
- package/src/__tests__/ibl-irradiance.unit.test.ts +43 -0
- package/src/__tests__/ibl-sampling.unit.test.ts +40 -0
- package/src/__tests__/lighting-punctual.unit.test.ts +66 -0
- package/src/__tests__/manifest.fixture.json +22 -0
- package/src/__tests__/material-artifact-dedup.unit.test.ts +21 -0
- package/src/__tests__/material-builtins.unit.test.ts +111 -0
- package/src/__tests__/material-contract.unit.test.ts +79 -0
- package/src/__tests__/material-derived-builtins.integration.test.ts +30 -0
- package/src/__tests__/public-surface.unit.test.ts +9 -0
- package/src/__tests__/scene-temporal.unit.test.ts +32 -0
- package/src/__tests__/shader-registry-transaction.unit.test.ts +116 -0
- package/src/__tests__/shader.unit.test.ts +1909 -0
- package/src/__tests__/sprite-lit-shader.test.ts +240 -0
- package/src/__tests__/sprite-variants.unit.test.ts +209 -0
- package/src/__tests__/taa-resolve.unit.test.ts +32 -0
- package/src/__tests__/transparent-pbr.unit.test.ts +21 -0
- package/src/__tests__/vertex-color-variant.unit.test.ts +52 -0
- package/src/atmosphere-background.wgsl +80 -0
- package/src/atmosphere-cubemap.wgsl +72 -0
- package/src/atmosphere-ibl.wgsl +127 -0
- package/src/atmosphere-preetham.wgsl +214 -0
- package/src/bloom-blur.wgsl +89 -0
- package/src/bloom-bright.wgsl +50 -0
- package/src/bloom-composite.wgsl +54 -0
- package/src/brdf.wgsl +25 -0
- package/src/common.wgsl +523 -0
- package/src/default-standard-pbr-skin.wgsl +538 -0
- package/src/default-standard-pbr.wgsl +794 -0
- package/src/errors.ts +169 -0
- package/src/fog.wgsl +27 -0
- package/src/fxaa.wgsl +259 -0
- package/src/hdrp-cluster-forward.wgsl +186 -0
- package/src/hdrp-deferred-lighting.wgsl +69 -0
- package/src/hdrp-ssao.wgsl +174 -0
- package/src/ibl-brdf-lut.wgsl +92 -0
- package/src/ibl-equirect-to-cube.wgsl +56 -0
- package/src/ibl-irradiance.wgsl +89 -0
- package/src/ibl-prefilter.wgsl +101 -0
- package/src/ibl-sampling.wgsl +58 -0
- package/src/ibl-shared.wgsl +118 -0
- package/src/index.ts +166 -0
- package/src/lighting-directional.wgsl +471 -0
- package/src/lighting-punctual.wgsl +274 -0
- package/src/material/artifact-registry.ts +78 -0
- package/src/material/artifact-types.ts +26 -0
- package/src/material-schemas.ts +48 -0
- package/src/msdf-text.wgsl +208 -0
- package/src/pbr-temporal.wgsl +73 -0
- package/src/register-default-sprite-lit.ts +87 -0
- package/src/register-default-standard-pbr-skin.ts +74 -0
- package/src/shadow-pcf.wgsl +120 -0
- package/src/shadow_caster.wgsl +54 -0
- package/src/skybox.wgsl +75 -0
- package/src/sprite-lit.wgsl +307 -0
- package/src/sprite.wgsl +365 -0
- package/src/taa-resolve.wgsl +129 -0
- package/src/tbn.wgsl +54 -0
- package/src/tonemap.ts +13 -0
- package/src/tonemap.wgsl +177 -0
- package/src/types.ts +59 -0
- package/src/unlit.wgsl +186 -0
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
#define_import_path forgeax_material::standard
|
|
2
|
+
#import forgeax_view::common::{View, FogViewParams, FogRay, Mesh, InstanceData, view, meshes, instances, PointLight, SpotLight, pointLightsBuffer, spotLightsBuffer, shadowMap, shadowSampler, sampleMaterialTexture}
|
|
3
|
+
#import forgeax_pbr::temporal::{projectPbrSceneTemporal}
|
|
4
|
+
#import forgeax_pbr::brdf::{f_schlick, v_smith, d_ggx}
|
|
5
|
+
#import forgeax_pbr::ibl_sampling::{sampleIblDiffuse, sampleIblSpecular}
|
|
6
|
+
#import forgeax_pbr::tbn::{decodeTangentSpaceNormalRg, scaleTangentSpaceNormal, applyTBN}
|
|
7
|
+
#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
|
+
#ifdef CLUSTER_FORWARD_AVAILABLE
|
|
15
|
+
#import forgeax_hdrp::cluster_forward::{evaluate_cluster_lights, get_ssao_intensity}
|
|
16
|
+
#endif
|
|
17
|
+
|
|
18
|
+
#pragma variant_axis STORAGE_BUFFER_AVAILABLE
|
|
19
|
+
#pragma variant_axis CLUSTER_FORWARD_AVAILABLE
|
|
20
|
+
#pragma variant_axis VERTEX_COLOR_AVAILABLE
|
|
21
|
+
|
|
22
|
+
// @forgeax/engine-shader - default-standard-pbr.wgsl
|
|
23
|
+
// (feat-20260523-shader-template-instance-split M5 / T04).
|
|
24
|
+
//
|
|
25
|
+
// Engine-shipped default standard PBR material shader, registered under the
|
|
26
|
+
// reserved path identifier `forgeax::default-standard-pbr` (plan-strategy
|
|
27
|
+
// D-DefaultStandardPbr-Identifier + plan-strategy section 8.2). This file is
|
|
28
|
+
// the M5 successor to the monolithic `pbr.wgsl`; the BRDF / IBL / TBN /
|
|
29
|
+
// lighting helpers all live in independent ShaderModules and are pulled in
|
|
30
|
+
// via naga_oil #import (charter F1 grep gate -- AI users grep the #import
|
|
31
|
+
// header to enumerate every helper dependency in one shot).
|
|
32
|
+
//
|
|
33
|
+
// Bindings (4 BG layout slots; View + Mesh bindings inherited from
|
|
34
|
+
// forgeax_view::common; shadow map + comparison sampler at view BG
|
|
35
|
+
// @binding(3..4) per shadow-mapping feat; Skylight 7 bindings merged into
|
|
36
|
+
// the PBR material BGL at @binding(7..13) per D-5 round-4):
|
|
37
|
+
//
|
|
38
|
+
// @group(0) @binding(0) view uniform (see common.wgsl)
|
|
39
|
+
// @group(1) @binding(0) material uniform (baseColor vec4
|
|
40
|
+
// + metallic + roughness
|
|
41
|
+
// + 4 channel selectors f32
|
|
42
|
+
// + emissive vec3 + emissiveIntensity
|
|
43
|
+
// + uv/alpha/clearcoat/specularTint = 112 B)
|
|
44
|
+
// @group(1) @binding(1) baseColorSampler sampler
|
|
45
|
+
// @group(1) @binding(2) baseColorTexture texture_2d<f32>
|
|
46
|
+
// @group(1) @binding(3) metallicRoughnessSampler sampler
|
|
47
|
+
// @group(1) @binding(4) metallicRoughnessTexture texture_2d<f32>
|
|
48
|
+
// @group(1) @binding(5) normalSampler sampler
|
|
49
|
+
// @group(1) @binding(6) normalTexture texture_2d<f32>
|
|
50
|
+
// @group(1) @binding(7) specularTintSampler + @binding(8) specularTintTexture
|
|
51
|
+
// @group(1) @binding(9..15) Skylight (irradiance / prefilter / brdfLut +
|
|
52
|
+
// samplers) + skylight uniform (intensity)
|
|
53
|
+
// @group(2) @binding(0) meshes storage (worldFromLocal mat4
|
|
54
|
+
// + normalMatrix mat3,
|
|
55
|
+
// see common.wgsl)
|
|
56
|
+
// @group(3) @binding(0) instances storage (per-instance
|
|
57
|
+
// localFromInstance mat4;
|
|
58
|
+
// indexed by @builtin
|
|
59
|
+
// (instance_index);
|
|
60
|
+
// see common.wgsl)
|
|
61
|
+
//
|
|
62
|
+
// 20 entries fit within `device.limits.maxBindingsPerBindGroup` (default
|
|
63
|
+
// 1000 across all known WebGPU devices; chrome-beta + dawn confirmed via
|
|
64
|
+
// the runtime probe in createRenderer.ts -- requirements R-E acceptance
|
|
65
|
+
// gate). Adding more bindings requires raising the entry-count fixture in
|
|
66
|
+
// the M5-T04 acceptanceCheck readback assertion.
|
|
67
|
+
//
|
|
68
|
+
// Two-layer fail-fast for roughness=0 NaN avoidance (plan-strategy section
|
|
69
|
+
// 5.3 + AC-02 b/c):
|
|
70
|
+
// layer 1: AssetRegistry.register fail-fast (M1 + M4 paramValues 3-tier)
|
|
71
|
+
// returns 'asset-invalid-value' / 'material-param-type-mismatch'
|
|
72
|
+
// before the payload reaches the GPU.
|
|
73
|
+
// layer 2: shader internal `let a = max(material.roughness, 0.04); a = a * a;`
|
|
74
|
+
// keeps D_GGX finite even if a producer somehow bypasses layer 1.
|
|
75
|
+
//
|
|
76
|
+
// Normal mapping (AC-05 + plan-strategy D-4): TBN basis built from
|
|
77
|
+
// per-vertex tangent (vec4 with handedness sign in .w) + interpolated
|
|
78
|
+
// world-space normal (consumed via mesh.normalMatrix from common.wgsl,
|
|
79
|
+
// plan-strategy D-5). RG-only tangent-space normal: sample.rg encodes
|
|
80
|
+
// (x,y) of the unit-length tangent normal, z is reconstructed via
|
|
81
|
+
// z = sqrt(1 - x^2 - y^2). Default 1x1 normal fallback texture is
|
|
82
|
+
// RG=(128,128) which decodes to tangent (0,0,1) -- zero perturbation when
|
|
83
|
+
// normalTexture is absent (host-side pipelineState.defaultNormalTextureView,
|
|
84
|
+
// distinct from the white fallback used by baseColor / metallicRoughness
|
|
85
|
+
// slots so a missing normal does not pollute the white-on-missing semantics
|
|
86
|
+
// of the other two slots). RG encoding also matches BC5 / RG normal maps
|
|
87
|
+
// and tolerates RGB normal maps (b is dropped, z is recomputed --
|
|
88
|
+
// equivalent for unit vectors).
|
|
89
|
+
|
|
90
|
+
struct Material {
|
|
91
|
+
baseColor : vec4<f32>,
|
|
92
|
+
metallic : f32,
|
|
93
|
+
roughness : f32,
|
|
94
|
+
// Channel selectors (D-8): 4 independent f32 entries split out of the
|
|
95
|
+
// legacy `channelMap : vec4<u32>`. Each value is a small integer encoded
|
|
96
|
+
// as f32 in {0.0, 1.0, 2.0, 3.0} indexing into {r,g,b,a}. Default glTF 2.0
|
|
97
|
+
// packing = (metallicChannel=2, roughnessChannel=1, aoChannel=0,
|
|
98
|
+
// extraChannel=0). The fragment casts to u32 at the pick site -- f32 is
|
|
99
|
+
// chosen so the schema entry type aligns with the 14-tuple
|
|
100
|
+
// MaterialParamType numeric-run packing rule (4-byte stride, 16 B span).
|
|
101
|
+
metallicChannel : f32,
|
|
102
|
+
roughnessChannel : f32,
|
|
103
|
+
aoChannel : f32,
|
|
104
|
+
extraChannel : f32,
|
|
105
|
+
// vec3 align=16 inserts 8 implicit padding bytes after extraChannel
|
|
106
|
+
// (offsets 40..48) so emissive lands at offset 48.
|
|
107
|
+
emissive : vec3<f32>,
|
|
108
|
+
emissiveIntensity : f32,
|
|
109
|
+
occlusionStrength : f32,
|
|
110
|
+
alphaCutoff : f32,
|
|
111
|
+
clearcoat : f32,
|
|
112
|
+
clearcoatRoughness : f32,
|
|
113
|
+
specularTint : vec3<f32>,
|
|
114
|
+
normalScale : f32,
|
|
115
|
+
baseColorTextureCoordinatesTransform : vec4<f32>,
|
|
116
|
+
baseColorTextureCoordinatesMetadata : vec4<f32>,
|
|
117
|
+
metallicRoughnessTextureCoordinatesTransform : vec4<f32>,
|
|
118
|
+
metallicRoughnessTextureCoordinatesMetadata : vec4<f32>,
|
|
119
|
+
normalTextureCoordinatesTransform : vec4<f32>,
|
|
120
|
+
normalTextureCoordinatesMetadata : vec4<f32>,
|
|
121
|
+
specularTintTextureCoordinatesTransform : vec4<f32>,
|
|
122
|
+
specularTintTextureCoordinatesMetadata : vec4<f32>,
|
|
123
|
+
emissiveTextureCoordinatesTransform : vec4<f32>,
|
|
124
|
+
emissiveTextureCoordinatesMetadata : vec4<f32>,
|
|
125
|
+
occlusionTextureCoordinatesTransform : vec4<f32>,
|
|
126
|
+
occlusionTextureCoordinatesMetadata : vec4<f32>,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
@group(1) @binding(0) var<uniform> material : Material;
|
|
130
|
+
@group(1) @binding(1) var baseColorSampler : sampler;
|
|
131
|
+
@group(1) @binding(2) var baseColorTexture : texture_2d<f32>;
|
|
132
|
+
@group(1) @binding(3) var metallicRoughnessSampler : sampler;
|
|
133
|
+
@group(1) @binding(4) var metallicRoughnessTexture : texture_2d<f32>;
|
|
134
|
+
@group(1) @binding(5) var normalSampler : sampler;
|
|
135
|
+
@group(1) @binding(6) var normalTexture : texture_2d<f32>;
|
|
136
|
+
@group(1) @binding(7) var specularTintSampler : sampler;
|
|
137
|
+
@group(1) @binding(8) var specularTintTexture : texture_2d<f32>;
|
|
138
|
+
@group(1) @binding(9) var emissiveSampler : sampler;
|
|
139
|
+
@group(1) @binding(10) var emissiveTexture : texture_2d<f32>;
|
|
140
|
+
@group(1) @binding(11) var occlusionSampler : sampler;
|
|
141
|
+
@group(1) @binding(12) var occlusionTexture : texture_2d<f32>;
|
|
142
|
+
|
|
143
|
+
// Naga reflection does not retain filtering usage through the generic shared
|
|
144
|
+
// helper. These compile-time-only witnesses retain the binding contract while
|
|
145
|
+
// every runtime material sample still goes through sampleMaterialTexture.
|
|
146
|
+
fn materialTextureFilteringWitness() {
|
|
147
|
+
let base = baseColorTexture;
|
|
148
|
+
let metallicRoughness = metallicRoughnessTexture;
|
|
149
|
+
let normal = normalTexture;
|
|
150
|
+
let specularTint = specularTintTexture;
|
|
151
|
+
let emissive = emissiveTexture;
|
|
152
|
+
let occlusion = occlusionTexture;
|
|
153
|
+
let baseWitness = textureSample(base, baseColorSampler, vec2<f32>(0.0));
|
|
154
|
+
let metallicRoughnessWitness = textureSample(metallicRoughness, metallicRoughnessSampler, vec2<f32>(0.0));
|
|
155
|
+
let normalWitness = textureSample(normal, normalSampler, vec2<f32>(0.0));
|
|
156
|
+
let specularTintWitness = textureSample(specularTint, specularTintSampler, vec2<f32>(0.0));
|
|
157
|
+
let emissiveWitness = textureSample(emissive, emissiveSampler, vec2<f32>(0.0));
|
|
158
|
+
let occlusionWitness = textureSample(occlusion, occlusionSampler, vec2<f32>(0.0));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Skylight bindings merged into the PBR material BGL
|
|
162
|
+
// (feat-20260520-skylight-ibl-cubemap M3 / t48 round-4 amend per D-5
|
|
163
|
+
// round-4 REVISED). The round-2 stand-alone group 4 Skylight BGL collided
|
|
164
|
+
// with WebGPU's default maxBindGroups=4 in chrome-beta and blocked pbr-pl
|
|
165
|
+
// pipeline-layout creation; round-4 appends the 7 Skylight entries to the
|
|
166
|
+
// PBR material BindGroupLayout at binding 7..13. The material BG factory
|
|
167
|
+
// (mergeSkylightIntoMaterialBgl in
|
|
168
|
+
// packages/runtime/src/ibl/skylight-bind-group.ts) extends the layout to
|
|
169
|
+
// 14 entries; render-system-record assembles a single merged material
|
|
170
|
+
// BindGroup (no extra setBindGroup(4) call). Identity (default) resources
|
|
171
|
+
// produce ambient = 0; with a real Skylight, ibl_sampling helpers project
|
|
172
|
+
// the IBL irradiance + split-sum specular into `ambient` below.
|
|
173
|
+
struct SkylightUniforms {
|
|
174
|
+
intensity : f32,
|
|
175
|
+
// The former pad0/1/2 lanes now carry the linear-space ambient `color` tint
|
|
176
|
+
// (downstream integration #4). Kept as three scalars (NOT vec3<f32>) so the
|
|
177
|
+
// struct stays exactly 16 B: a vec3 has 16-byte alignment in std140 and
|
|
178
|
+
// would push `color` to offset 16. The rotation vec4 follows at offset 16,
|
|
179
|
+
// so the host writes one 32 B payload.
|
|
180
|
+
colorR : f32,
|
|
181
|
+
colorG : f32,
|
|
182
|
+
colorB : f32,
|
|
183
|
+
rotation : vec4<f32>,
|
|
184
|
+
};
|
|
185
|
+
@group(1) @binding(13) var irradianceMap : texture_cube<f32>;
|
|
186
|
+
@group(1) @binding(14) var irradianceSampler : sampler;
|
|
187
|
+
@group(1) @binding(15) var prefilterMap : texture_cube<f32>;
|
|
188
|
+
@group(1) @binding(16) var prefilterSampler : sampler;
|
|
189
|
+
@group(1) @binding(17) var brdfLut : texture_2d<f32>;
|
|
190
|
+
@group(1) @binding(18) var brdfLutSampler : sampler;
|
|
191
|
+
@group(1) @binding(19) var<uniform> skylight : SkylightUniforms;
|
|
192
|
+
|
|
193
|
+
// feat-20260612-hdrp-ssao M7 (round 2) D-B + D-C, scope-amend-webgl2-ubo:
|
|
194
|
+
// SSAO sampling lives on the HDRP unified BGL @group(2) alongside the
|
|
195
|
+
// cluster bindings (binding 7 = ssao texture, binding 8 = sampler). The
|
|
196
|
+
// intensity scalar is folded into `cluster_uniform.near_far_log.w` (the
|
|
197
|
+
// previously-unused std140 pad lane on @binding(6)) — declaring a
|
|
198
|
+
// dedicated UBO at @binding(9) overflows WebGL2's
|
|
199
|
+
// `max_uniform_buffers_per_shader_stage = 11` budget on rhi-wgpu's
|
|
200
|
+
// fallback path. Disabled SSAO path binds 1x1 white at @binding(7) + the
|
|
201
|
+
// host writes intensity=0 into the cluster pad lane, so the synthesis
|
|
202
|
+
// collapses identically.
|
|
203
|
+
#ifdef CLUSTER_FORWARD_AVAILABLE
|
|
204
|
+
@group(2) @binding(7) var ssaoBlurredTexture : texture_2d<f32>;
|
|
205
|
+
@group(2) @binding(8) var ssaoBlurredSampler : sampler;
|
|
206
|
+
#endif
|
|
207
|
+
|
|
208
|
+
struct VsIn {
|
|
209
|
+
@location(0) pos : vec3<f32>,
|
|
210
|
+
@location(1) normal : vec3<f32>,
|
|
211
|
+
@location(2) uv : vec2<f32>,
|
|
212
|
+
@location(3) tangent : vec4<f32>,
|
|
213
|
+
// MaterialAsset per-slot texCoord: reserve the canonical UV0-UV7 inputs.
|
|
214
|
+
// Missing mesh sets are supplied by the pipeline's clamp-to-last aliases.
|
|
215
|
+
@location(6) uv1 : vec2<f32>,
|
|
216
|
+
@location(7) uv2 : vec2<f32>,
|
|
217
|
+
@location(8) uv3 : vec2<f32>,
|
|
218
|
+
@location(9) uv4 : vec2<f32>,
|
|
219
|
+
@location(10) uv5 : vec2<f32>,
|
|
220
|
+
@location(11) uv6 : vec2<f32>,
|
|
221
|
+
@location(12) uv7 : vec2<f32>,
|
|
222
|
+
#ifdef VERTEX_COLOR_AVAILABLE
|
|
223
|
+
@location(13) color : vec4<f32>,
|
|
224
|
+
#endif
|
|
225
|
+
};
|
|
226
|
+
struct VsOut {
|
|
227
|
+
@builtin(position) clip : vec4<f32>,
|
|
228
|
+
@location(0) worldPos : vec3<f32>,
|
|
229
|
+
@location(1) worldNormal : vec3<f32>,
|
|
230
|
+
@location(2) uv : vec2<f32>,
|
|
231
|
+
@location(3) worldTangent : vec4<f32>,
|
|
232
|
+
@location(4) @interpolate(flat) instanceIdx : u32,
|
|
233
|
+
// feat-city-glb multi-UV tiling: second UV set inter-stage varying. Uses the
|
|
234
|
+
// previously-vacant @location(5) so @location(6)/(7) stay byte-stable with
|
|
235
|
+
// the prior layout (CSM M5/w19).
|
|
236
|
+
@location(5) uv1 : vec2<f32>,
|
|
237
|
+
@location(8) uv2 : vec2<f32>,
|
|
238
|
+
@location(9) uv3 : vec2<f32>,
|
|
239
|
+
@location(10) uv4 : vec2<f32>,
|
|
240
|
+
@location(11) uv5 : vec2<f32>,
|
|
241
|
+
@location(12) uv6 : vec2<f32>,
|
|
242
|
+
@location(13) uv7 : vec2<f32>,
|
|
243
|
+
#ifdef VERTEX_COLOR_AVAILABLE
|
|
244
|
+
@location(14) color : vec4<f32>,
|
|
245
|
+
#endif
|
|
246
|
+
@location(6) ndc : vec3<f32>, // NDC for HDRP cluster lookup (w10)
|
|
247
|
+
// feat-20260609-hdrp-cluster-fragment-ggx M4.5-followup: view-space z is
|
|
248
|
+
// needed by ndc_position_to_cluster (slice index uses log-z mapping that
|
|
249
|
+
// takes negative view_z, NOT NDC z which is [0,1]). Earlier `in.ndc.z`
|
|
250
|
+
// pass through view_z slot collapsed every fragment to slice 0, so cube
|
|
251
|
+
// surfaces -- whose cluster cells were unrelated to the floor's slice 0
|
|
252
|
+
// hot zone -- received zero light. Forward view_z explicitly. M5 / w19:
|
|
253
|
+
// also feeds CSM cascade selection in evalDirectional.
|
|
254
|
+
@location(7) viewZ : f32,
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
fn pick_channel(rgba : vec4<f32>, channelIndex : u32) -> f32 {
|
|
258
|
+
// Branch-free channel pick. WGSL has no array<f32, 4>(rgba) addressable
|
|
259
|
+
// helper; manual switch keeps the path uniform.
|
|
260
|
+
switch (channelIndex) {
|
|
261
|
+
case 0u: { return rgba.r; }
|
|
262
|
+
case 1u: { return rgba.g; }
|
|
263
|
+
case 2u: { return rgba.b; }
|
|
264
|
+
default: { return rgba.a; }
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
fn applySceneFog(viewParams : View, color : vec3<f32>, alpha : f32, worldPos : vec3<f32>) -> vec4<f32> {
|
|
269
|
+
var origin = viewParams.cameraPos;
|
|
270
|
+
var direction = normalize(worldPos - origin);
|
|
271
|
+
var rayDistance = length(worldPos - origin);
|
|
272
|
+
if (viewParams.temporalProjection.z >= 0.5) {
|
|
273
|
+
let nearH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 0.0, 1.0);
|
|
274
|
+
let farH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 1.0, 1.0);
|
|
275
|
+
let nearPoint = nearH.xyz / nearH.w;
|
|
276
|
+
let farPoint = farH.xyz / farH.w;
|
|
277
|
+
direction = normalize(farPoint - nearPoint);
|
|
278
|
+
// Orthographic rays are parallel, but their origins differ per fragment.
|
|
279
|
+
// Project the current fragment onto the camera plane instead of reusing
|
|
280
|
+
// the center NDC ray origin for every pixel.
|
|
281
|
+
origin = worldPos - direction * dot(worldPos - viewParams.cameraPos, direction);
|
|
282
|
+
rayDistance = max(dot(worldPos - origin, direction), 0.0);
|
|
283
|
+
}
|
|
284
|
+
return apply_fog(viewParams.fog, FogRay(origin, direction, rayDistance), vec4<f32>(color, alpha));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Light evaluators live in forgeax_pbr::lighting_directional +
|
|
288
|
+
// forgeax_pbr::lighting_punctual (M5 / T02). evalDirectional consumes the
|
|
289
|
+
// host's view UBO + shadowMap (LO 3.1.3 slope-scaled-bias 3x3 PCF;
|
|
290
|
+
// feat-20260520-directional-light-shadow-mapping byte-equivalent). evalPoint
|
|
291
|
+
// / evalSpot share evalPunctualBody (GGX specular + Lambertian diffuse +
|
|
292
|
+
// KHR_lights_punctual quartic range attenuation;
|
|
293
|
+
// feat-20260519-light-casters-point-spot-pbr M4 / w22 byte-equivalent).
|
|
294
|
+
// Charter P4: spot light is a thin cone-multiplier on top of the punctual
|
|
295
|
+
// body, point light is the body unchanged -- no magic-value collapse.
|
|
296
|
+
|
|
297
|
+
@vertex
|
|
298
|
+
fn vs_main(in : VsIn, @builtin(instance_index) idx : u32) -> VsOut {
|
|
299
|
+
// feat-20260604-instances-per-instance-transform-shader-group3-bin M1 / w4:
|
|
300
|
+
// Entity world from meshes[0] — the @group(2) dynamic-offset window has
|
|
301
|
+
// already been aimed at this entity's slot (render-system-record.ts:2996
|
|
302
|
+
// setBindGroup(2, meshBindGroup, [i * MESH_PER_ENTITY_STRIDE])). Per-instance
|
|
303
|
+
// local from instances[idx] — @group(3) is a flat per-instance buffer indexed
|
|
304
|
+
// directly by instance_index (firstInstance=0, render-system-record.ts:2898).
|
|
305
|
+
// Combine: entity_world * per_instance_local.
|
|
306
|
+
let instanceLocal = instances[idx].localFromInstance;
|
|
307
|
+
let entityWorld = meshes[0].worldFromLocal;
|
|
308
|
+
let world = entityWorld * instanceLocal * vec4<f32>(in.pos, 1.0);
|
|
309
|
+
var out : VsOut;
|
|
310
|
+
out.clip = view.worldViewProj * world;
|
|
311
|
+
out.worldPos = world.xyz;
|
|
312
|
+
// Normal: derive the inverse-transpose from the world matrix columns. This
|
|
313
|
+
// keeps non-uniform entity scale correct while avoiding the Mesh.normalMatrix
|
|
314
|
+
// storage field whose mat3x3 layout is not consumed consistently by the GPU
|
|
315
|
+
// path. The per-instance local transform remains uniform-scale-only, as it
|
|
316
|
+
// was before this owner fix.
|
|
317
|
+
let a = entityWorld[0].xyz;
|
|
318
|
+
let b = entityWorld[1].xyz;
|
|
319
|
+
let c = entityWorld[2].xyz;
|
|
320
|
+
let cof0 = cross(b, c);
|
|
321
|
+
let cof1 = cross(c, a);
|
|
322
|
+
let cof2 = cross(a, b);
|
|
323
|
+
let det = dot(a, cof0);
|
|
324
|
+
let entityNormal = select(
|
|
325
|
+
in.normal,
|
|
326
|
+
(cof0 * in.normal.x + cof1 * in.normal.y + cof2 * in.normal.z) / det,
|
|
327
|
+
abs(det) >= 1e-6,
|
|
328
|
+
);
|
|
329
|
+
out.worldNormal = normalize(entityNormal);
|
|
330
|
+
// Tangent transformed by the combined entity*instance chain as a direction
|
|
331
|
+
// (w=0); .w handedness preserved for bitangent reconstruction in fragment.
|
|
332
|
+
let worldTangentXyz = normalize((entityWorld * instanceLocal * vec4<f32>(in.tangent.xyz, 0.0)).xyz);
|
|
333
|
+
out.worldTangent = vec4<f32>(worldTangentXyz, in.tangent.w);
|
|
334
|
+
out.uv = in.uv;
|
|
335
|
+
out.uv1 = in.uv1;
|
|
336
|
+
out.uv2 = in.uv2;
|
|
337
|
+
out.uv3 = in.uv3;
|
|
338
|
+
out.uv4 = in.uv4;
|
|
339
|
+
out.uv5 = in.uv5;
|
|
340
|
+
out.uv6 = in.uv6;
|
|
341
|
+
out.uv7 = in.uv7;
|
|
342
|
+
#ifdef VERTEX_COLOR_AVAILABLE
|
|
343
|
+
out.color = in.color;
|
|
344
|
+
#endif
|
|
345
|
+
out.instanceIdx = idx;
|
|
346
|
+
// feat-20260613-csm-cascaded-shadow-maps M5 / w19: the per-fragment
|
|
347
|
+
// light-space position varying is gone; evalDirectional computes
|
|
348
|
+
// per-cascade lightViewProj * worldPos in the fragment stage from
|
|
349
|
+
// viewZ + worldPos.
|
|
350
|
+
// NDC for HDRP cluster lookup (feat-20260609-hdrp-cluster-fragment-ggx M2 / w10).
|
|
351
|
+
// Perspective divide on clip-space position; ndc.z retains depth-buffer value.
|
|
352
|
+
let clipPos = out.clip;
|
|
353
|
+
out.ndc = vec3(clipPos.xy / clipPos.w, clipPos.z / clipPos.w);
|
|
354
|
+
// Standard WebGPU/GL perspective projection has clip.w = -view.z, so
|
|
355
|
+
// view_z = -clip.w (negative in front of the camera). M4.5-followup:
|
|
356
|
+
// ndc_position_to_cluster needs this for log-z slice mapping; passing
|
|
357
|
+
// ndc.z (in [0,1]) instead made every fragment land in slice 0.
|
|
358
|
+
out.viewZ = -clipPos.w;
|
|
359
|
+
return out;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
fn transformedMaterialUv(transform : vec4<f32>, metadata : vec4<f32>, in : VsOut) -> vec2<f32> {
|
|
363
|
+
var source = in.uv;
|
|
364
|
+
if (metadata.x >= 1.0) { source = in.uv1; }
|
|
365
|
+
if (metadata.x >= 2.0) { source = in.uv2; }
|
|
366
|
+
if (metadata.x >= 3.0) { source = in.uv3; }
|
|
367
|
+
if (metadata.x >= 4.0) { source = in.uv4; }
|
|
368
|
+
if (metadata.x >= 5.0) { source = in.uv5; }
|
|
369
|
+
if (metadata.x >= 6.0) { source = in.uv6; }
|
|
370
|
+
if (metadata.x >= 7.0) { source = in.uv7; }
|
|
371
|
+
let scaled = source * transform.zw;
|
|
372
|
+
let angle = metadata.y;
|
|
373
|
+
let c = cos(angle);
|
|
374
|
+
let s = sin(angle);
|
|
375
|
+
return vec2<f32>(scaled.x * c - scaled.y * s, scaled.x * s + scaled.y * c) + transform.xy;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
fn materialAlpha(baseSample : vec4<f32>) -> f32 {
|
|
379
|
+
return material.baseColor.a * baseSample.a;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
fn materialVertexColor(in : VsOut) -> vec4<f32> {
|
|
383
|
+
#ifdef VERTEX_COLOR_AVAILABLE
|
|
384
|
+
return in.color;
|
|
385
|
+
#else
|
|
386
|
+
return vec4<f32>(1.0);
|
|
387
|
+
#endif
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// linearColorDomain: transparent source and destination values are blended
|
|
391
|
+
// before any display encoding. The fixed-function material blend state uses
|
|
392
|
+
// this same equation for the render target's declared linear domain.
|
|
393
|
+
fn blendLinearTransparent(
|
|
394
|
+
source : vec3<f32>,
|
|
395
|
+
destination : vec3<f32>,
|
|
396
|
+
alpha : f32,
|
|
397
|
+
) -> vec3<f32> {
|
|
398
|
+
return source * alpha + destination * (1.0 - alpha);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// linearHdrColorDomain: fs_main writes linear HDR when its target is HDR.
|
|
402
|
+
// toneStageInput: the value remains linear until the fullscreen tone stage.
|
|
403
|
+
|
|
404
|
+
fn alphaTest(alpha : f32) {
|
|
405
|
+
if (material.alphaCutoff > 0.0 && alpha <= material.alphaCutoff) {
|
|
406
|
+
discard;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
@fragment
|
|
411
|
+
fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
|
412
|
+
let baseUv = transformedMaterialUv(material.baseColorTextureCoordinatesTransform, material.baseColorTextureCoordinatesMetadata, in);
|
|
413
|
+
let baseSample = sampleMaterialTexture(baseColorTexture, baseColorSampler, baseUv, material.baseColorTextureCoordinatesMetadata.zw);
|
|
414
|
+
let vertexColor = materialVertexColor(in);
|
|
415
|
+
alphaTest(material.baseColor.a * baseSample.a * vertexColor.a);
|
|
416
|
+
let alpha = materialAlpha(baseSample) * vertexColor.a;
|
|
417
|
+
let albedo = material.baseColor.rgb * baseSample.rgb * vertexColor.rgb;
|
|
418
|
+
|
|
419
|
+
// Metallic-roughness texture sampling with per-field channel selectors
|
|
420
|
+
// (D-8): glTF 2.0 default layout B=metallic, G=roughness, R=occlusion is
|
|
421
|
+
// encoded by the host as 4 independent f32 selectors in the merged UBO
|
|
422
|
+
// (metallicChannel/roughnessChannel/aoChannel/extraChannel). Cast to u32
|
|
423
|
+
// at the pick_channel call site; values stay in {0,1,2,3}.
|
|
424
|
+
let mrUv = transformedMaterialUv(material.metallicRoughnessTextureCoordinatesTransform, material.metallicRoughnessTextureCoordinatesMetadata, in);
|
|
425
|
+
let mrSample = sampleMaterialTexture(metallicRoughnessTexture, metallicRoughnessSampler, mrUv, material.metallicRoughnessTextureCoordinatesMetadata.zw);
|
|
426
|
+
let metallic = material.metallic * pick_channel(mrSample, u32(material.metallicChannel));
|
|
427
|
+
let roughnessTex = pick_channel(mrSample, u32(material.roughnessChannel));
|
|
428
|
+
|
|
429
|
+
// Layer-2 fail-fast: shader internal clamp keeps D_GGX finite for
|
|
430
|
+
// roughness=0 even if the asset somehow bypasses layer-1 register
|
|
431
|
+
// fail-fast (plan-strategy section 5.3 + AC-02 (b)+(c)). Apply to the
|
|
432
|
+
// material scalar prior to the channelMap-extracted texture multiplier
|
|
433
|
+
// so the final roughness >= 0.04 floor is preserved (texture sample is
|
|
434
|
+
// a 1.0-default white when metallicRoughnessTexture is absent, so
|
|
435
|
+
// multiplying after the clamp keeps the floor intact).
|
|
436
|
+
var a = max(material.roughness, 0.04);
|
|
437
|
+
a = a * roughnessTex;
|
|
438
|
+
a = a * a;
|
|
439
|
+
|
|
440
|
+
// TBN basis composed via forgeax_pbr::tbn helpers; default fallback
|
|
441
|
+
// (defaultNormalTextureView) RG=(128,128) -> tangent (0,0,1) -> world n
|
|
442
|
+
// unchanged.
|
|
443
|
+
let normalUv = transformedMaterialUv(material.normalTextureCoordinatesTransform, material.normalTextureCoordinatesMetadata, in);
|
|
444
|
+
let normSampleRg = sampleMaterialTexture(normalTexture, normalSampler, normalUv, material.normalTextureCoordinatesMetadata.zw).rg;
|
|
445
|
+
let normTangent = scaleTangentSpaceNormal(
|
|
446
|
+
decodeTangentSpaceNormalRg(normSampleRg), material.normalScale,
|
|
447
|
+
);
|
|
448
|
+
let n = applyTBN(in.worldNormal, in.worldTangent, normTangent);
|
|
449
|
+
|
|
450
|
+
let v = normalize(view.cameraPos - in.worldPos);
|
|
451
|
+
let specularUv = transformedMaterialUv(material.specularTintTextureCoordinatesTransform, material.specularTintTextureCoordinatesMetadata, in);
|
|
452
|
+
let specularTint = material.specularTint * sampleMaterialTexture(
|
|
453
|
+
specularTintTexture, specularTintSampler, specularUv, material.specularTintTextureCoordinatesMetadata.zw,
|
|
454
|
+
).rgb;
|
|
455
|
+
let f0 = mix(vec3<f32>(0.04) * specularTint, albedo, metallic);
|
|
456
|
+
let coatRoughness = max(material.clearcoatRoughness, 0.04);
|
|
457
|
+
let coatAlpha = coatRoughness * coatRoughness;
|
|
458
|
+
let coatF = f_schlick(max(dot(n, v), 0.0), vec3<f32>(0.04)) * material.clearcoat;
|
|
459
|
+
|
|
460
|
+
// Ambient (IBL) + 1 + N + N accumulation
|
|
461
|
+
// (feat-20260520-skylight-ibl-cubemap M3 / t48 +
|
|
462
|
+
// feat-20260520-directional-light-shadow-mapping +
|
|
463
|
+
// feat-20260518-pbr-direct-lighting-mvp Finding 4):
|
|
464
|
+
//
|
|
465
|
+
// color = ambient(IBL) + directional(shadowed) + sum(point) + sum(spot)
|
|
466
|
+
//
|
|
467
|
+
// When the host Skylight bind group provides default-zero resources the IBL
|
|
468
|
+
// helpers sample to vec3(0) and ambient = 0, so the shader falls through to
|
|
469
|
+
// direct lighting naturally (zero contribution, no branch, no #if guard).
|
|
470
|
+
//
|
|
471
|
+
// sampleIblDiffuse / sampleIblSpecular are imported from
|
|
472
|
+
// forgeax_pbr::ibl_sampling (ibl-sampling.wgsl) and take the
|
|
473
|
+
// @group(1) @binding(7..13) Skylight resources as function arguments
|
|
474
|
+
// -- the runtime helper module is zero-binding so the host owns the
|
|
475
|
+
// binding layout (round-4 amend: Skylight merged into material BGL).
|
|
476
|
+
// `material.roughness` is the unsquared scalar; `a` above is the
|
|
477
|
+
// alpha*alpha form used by direct-light D_GGX, which is wrong for the
|
|
478
|
+
// split-sum mip lookup, so we re-derive the post-shader-clamp roughness
|
|
479
|
+
// here (matches sampleIblSpecular's `mip = roughness * 4.0` expectation).
|
|
480
|
+
//
|
|
481
|
+
// Direct lights (1 + N + N): one directional + N point + N spot, summed
|
|
482
|
+
// sequentially. LIGHT_ARRAY_MAX_SLOTS = 4 host-side bounds the loop trip
|
|
483
|
+
// counts. GGX BRDF helpers (d_ggx / v_smith / f_schlick from
|
|
484
|
+
// forgeax_pbr::brdf) run inside each evalDirectional / evalPoint / evalSpot
|
|
485
|
+
// so all three light types share the same microfacet specular + Lambertian
|
|
486
|
+
// diffuse form. The directional path additionally projects worldPos
|
|
487
|
+
// through the per-cascade light-space matrix in the fragment stage for
|
|
488
|
+
// shadow-map PCF lookup (CSM, feat-20260613).
|
|
489
|
+
let kD = (vec3<f32>(1.0) - f_schlick(max(dot(n, v), 0.0), f0)) * (1.0 - metallic);
|
|
490
|
+
let iblRoughness = max(material.roughness, 0.04) * roughnessTex;
|
|
491
|
+
let irradiance = sampleIblDiffuse(n, skylight.rotation, irradianceMap, irradianceSampler);
|
|
492
|
+
let specularIbl = sampleIblSpecular(
|
|
493
|
+
n, v, iblRoughness, f0,
|
|
494
|
+
skylight.rotation,
|
|
495
|
+
prefilterMap, prefilterSampler, brdfLut, brdfLutSampler,
|
|
496
|
+
);
|
|
497
|
+
let occlusionUv = transformedMaterialUv(material.occlusionTextureCoordinatesTransform, material.occlusionTextureCoordinatesMetadata, in);
|
|
498
|
+
let aoSample = sampleMaterialTexture(occlusionTexture, occlusionSampler, occlusionUv, material.occlusionTextureCoordinatesMetadata.zw);
|
|
499
|
+
let ao = mix(1.0, aoSample.r, material.occlusionStrength);
|
|
500
|
+
// feat-20260612-hdrp-ssao M7 round-2: `var` (mutable) so the
|
|
501
|
+
// CLUSTER_FORWARD_AVAILABLE branch below can `ambient *=` the SSAO
|
|
502
|
+
// factor. The non-HDRP path leaves ambient untouched.
|
|
503
|
+
let skyColor = vec3<f32>(skylight.colorR, skylight.colorG, skylight.colorB);
|
|
504
|
+
var ambient = (kD * irradiance * albedo + specularIbl) * (vec3<f32>(1.0) - coatF);
|
|
505
|
+
if (material.clearcoat != 0.0) {
|
|
506
|
+
let clearcoatIbl = sampleIblSpecular(
|
|
507
|
+
n, v, coatRoughness, vec3<f32>(0.04),
|
|
508
|
+
skylight.rotation,
|
|
509
|
+
prefilterMap, prefilterSampler, brdfLut, brdfLutSampler,
|
|
510
|
+
);
|
|
511
|
+
ambient = ambient + clearcoatIbl * material.clearcoat;
|
|
512
|
+
}
|
|
513
|
+
ambient = ambient * skyColor * skylight.intensity * ao;
|
|
514
|
+
#ifdef CLUSTER_FORWARD_AVAILABLE
|
|
515
|
+
// feat-20260612-hdrp-ssao M2 round-1 + M7 round-2 (plan-strategy D-7 + D-B + D-C):
|
|
516
|
+
// SSAO ambient synthesis. Reads the half-res R8 `ssaoBlurredTexture` from
|
|
517
|
+
// the ssao-blur pass (HDRP unified BGL @group(2) @binding(7..9)). The host
|
|
518
|
+
// always binds those slots: when SSAO is disabled, binding 7 receives a
|
|
519
|
+
// 1x1 white fallback (AO=1.0) and binding 9 receives a zero-intensity
|
|
520
|
+
// uniform — `mix(1.0, ssao*ao, 0.0) = 1.0` so ambient collapses to the
|
|
521
|
+
// round-1 baseline (no PSO recompile across the toggle).
|
|
522
|
+
//
|
|
523
|
+
// Sampling uses the screen-space NDC -> [0,1] uv; we recover it from the
|
|
524
|
+
// clip-space xy that the vertex stage already emits via in.ndc.xy.
|
|
525
|
+
let ssaoUv = in.ndc.xy * vec2<f32>(0.5, -0.5) + vec2<f32>(0.5, 0.5);
|
|
526
|
+
let ssaoFactor = textureSample(ssaoBlurredTexture, ssaoBlurredSampler, ssaoUv).r;
|
|
527
|
+
// scope-amend-webgl2-ubo: intensity packed into cluster_uniform.near_far_log.w.
|
|
528
|
+
let ssaoIntensity = get_ssao_intensity();
|
|
529
|
+
ambient *= mix(1.0, ssaoFactor * ao, ssaoIntensity);
|
|
530
|
+
#endif
|
|
531
|
+
var color = ambient;
|
|
532
|
+
let directionalShadow = evalDirectionalShadowFactor(n, in.worldPos, in.viewZ);
|
|
533
|
+
let directionalBase = evalDirectionalNoShadow(n, v, albedo, metallic, a, f0);
|
|
534
|
+
color = color + directionalShadow * directionalBase;
|
|
535
|
+
if (material.clearcoat != 0.0) {
|
|
536
|
+
let directionalClearcoat = evalDirectionalNoShadow(
|
|
537
|
+
n, v, vec3<f32>(0.0), 1.0, coatAlpha, vec3<f32>(0.04),
|
|
538
|
+
);
|
|
539
|
+
color = color + directionalShadow * material.clearcoat * directionalClearcoat;
|
|
540
|
+
}
|
|
541
|
+
#ifdef CLUSTER_FORWARD_AVAILABLE
|
|
542
|
+
// NDC from vertex shader (perspective-divided clip-space, interpolated).
|
|
543
|
+
// view_z: NDC depth for cluster Z-slice lookup.
|
|
544
|
+
color = color + evaluate_cluster_lights(in.ndc, in.viewZ, in.worldPos, n, v, albedo, metallic, a, f0);
|
|
545
|
+
if (material.clearcoat != 0.0) {
|
|
546
|
+
color = color + material.clearcoat * evaluate_cluster_lights(
|
|
547
|
+
in.ndc, in.viewZ, in.worldPos, n, v, vec3<f32>(0.0), 1.0, coatAlpha, vec3<f32>(0.04),
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
#else
|
|
551
|
+
let pointCount = pointLightsBuffer.count;
|
|
552
|
+
for (var i: u32 = 0u; i < pointCount; i = i + 1u) {
|
|
553
|
+
let p = pointLightsBuffer.slots[i];
|
|
554
|
+
#ifdef POINT_SHADOW_AVAILABLE
|
|
555
|
+
// feat-20260612-point-light-shadows-urp-hdrp Round-2 F-1: gate between
|
|
556
|
+
// shadowed and unshadowed evaluation on the host-assigned
|
|
557
|
+
// shadowAtlasLayer (>= 0 means PointLightShadow companion exists; -1
|
|
558
|
+
// sentinel means no shadow). shadowParams[layer] = (near, far,
|
|
559
|
+
// 1/(far-near), 0) is bound at @group(0) @binding(6) (shadowParams
|
|
560
|
+
// declared in common.wgsl).
|
|
561
|
+
if (p.shadowAtlasLayer >= 0) {
|
|
562
|
+
let lane = shadowParams[p.shadowAtlasLayer];
|
|
563
|
+
color = color + evalPointShadowed(
|
|
564
|
+
p.position, p.colorTimesIntensity, p.invRangeSquared,
|
|
565
|
+
in.worldPos, n, v, albedo, metallic, a, f0,
|
|
566
|
+
p.shadowAtlasLayer, lane.x, lane.y, 0.005, 0.05,
|
|
567
|
+
);
|
|
568
|
+
if (material.clearcoat != 0.0) {
|
|
569
|
+
color = color + material.clearcoat * evalPointShadowed(
|
|
570
|
+
p.position, p.colorTimesIntensity, p.invRangeSquared,
|
|
571
|
+
in.worldPos, n, v, vec3<f32>(0.0), 1.0, coatAlpha, vec3<f32>(0.04),
|
|
572
|
+
p.shadowAtlasLayer, lane.x, lane.y, 0.005, 0.05,
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
} else {
|
|
576
|
+
color = color + evalPoint(
|
|
577
|
+
p.position, p.colorTimesIntensity, p.invRangeSquared,
|
|
578
|
+
in.worldPos, n, v, albedo, metallic, a, f0,
|
|
579
|
+
);
|
|
580
|
+
if (material.clearcoat != 0.0) {
|
|
581
|
+
color = color + material.clearcoat * evalPoint(
|
|
582
|
+
p.position, p.colorTimesIntensity, p.invRangeSquared,
|
|
583
|
+
in.worldPos, n, v, vec3<f32>(0.0), 1.0, coatAlpha, vec3<f32>(0.04),
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
#else
|
|
588
|
+
color = color + evalPoint(
|
|
589
|
+
p.position, p.colorTimesIntensity, p.invRangeSquared,
|
|
590
|
+
in.worldPos, n, v, albedo, metallic, a, f0,
|
|
591
|
+
);
|
|
592
|
+
if (material.clearcoat != 0.0) {
|
|
593
|
+
color = color + material.clearcoat * evalPoint(
|
|
594
|
+
p.position, p.colorTimesIntensity, p.invRangeSquared,
|
|
595
|
+
in.worldPos, n, v, vec3<f32>(0.0), 1.0, coatAlpha, vec3<f32>(0.04),
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
#endif
|
|
599
|
+
}
|
|
600
|
+
let spotCount = spotLightsBuffer.count;
|
|
601
|
+
for (var i: u32 = 0u; i < spotCount; i = i + 1u) {
|
|
602
|
+
let s = spotLightsBuffer.slots[i];
|
|
603
|
+
// feat-20260625-spot-light-shadow-mapping M3 / w15 (plan-strategy D-1
|
|
604
|
+
// fragment side + D-3 + D-4): gate on the host-assigned shadowAtlasTile
|
|
605
|
+
// (>= 0 means a spot atlas tile was allocated; -1 sentinel means no
|
|
606
|
+
// shadow / clipped / direction-degenerate). The shadowed path reads the
|
|
607
|
+
// per-spot perspective matrix from the View UBO `view.spotLightViewProj`
|
|
608
|
+
// array (lane N = shadowAtlasTile N; folded from the standalone binding 9
|
|
609
|
+
// into the View UBO in feat-20260625 w25 for WebGL2 uniform-buffer budget);
|
|
610
|
+
// the unshadowed path stays on evalSpot. bias hardcoded 0.005 / 0.05 aligns
|
|
611
|
+
// with the DirectionalLight defaults (D-6); the OOB/NaN gate lives inside
|
|
612
|
+
// evalSpotShadowed, not here.
|
|
613
|
+
if (s.shadowAtlasTile >= 0) {
|
|
614
|
+
color = color + evalSpotShadowed(
|
|
615
|
+
s.position, s.direction, s.colorTimesIntensity,
|
|
616
|
+
s.cosInner, s.cosOuter, s.invRangeSquared,
|
|
617
|
+
in.worldPos, n, v, albedo, metallic, a, f0,
|
|
618
|
+
view.spotLightViewProj[s.shadowAtlasTile], s.shadowAtlasTile, 0.005, 0.05,
|
|
619
|
+
);
|
|
620
|
+
if (material.clearcoat != 0.0) {
|
|
621
|
+
color = color + material.clearcoat * evalSpotShadowed(
|
|
622
|
+
s.position, s.direction, s.colorTimesIntensity,
|
|
623
|
+
s.cosInner, s.cosOuter, s.invRangeSquared,
|
|
624
|
+
in.worldPos, n, v, vec3<f32>(0.0), 1.0, coatAlpha, vec3<f32>(0.04),
|
|
625
|
+
view.spotLightViewProj[s.shadowAtlasTile], s.shadowAtlasTile, 0.005, 0.05,
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
} else {
|
|
629
|
+
color = color + evalSpot(
|
|
630
|
+
s.position, s.direction, s.colorTimesIntensity,
|
|
631
|
+
s.cosInner, s.cosOuter, s.invRangeSquared,
|
|
632
|
+
in.worldPos, n, v, albedo, metallic, a, f0,
|
|
633
|
+
);
|
|
634
|
+
if (material.clearcoat != 0.0) {
|
|
635
|
+
color = color + material.clearcoat * evalSpot(
|
|
636
|
+
s.position, s.direction, s.colorTimesIntensity,
|
|
637
|
+
s.cosInner, s.cosOuter, s.invRangeSquared,
|
|
638
|
+
in.worldPos, n, v, vec3<f32>(0.0), 1.0, coatAlpha, vec3<f32>(0.04),
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
#endif // CLUSTER_FORWARD_AVAILABLE
|
|
644
|
+
let emissiveUv = transformedMaterialUv(material.emissiveTextureCoordinatesTransform, material.emissiveTextureCoordinatesMetadata, in);
|
|
645
|
+
let emissiveSample = sampleMaterialTexture(emissiveTexture, emissiveSampler, emissiveUv, material.emissiveTextureCoordinatesMetadata.zw).rgb;
|
|
646
|
+
color = color + material.emissive * material.emissiveIntensity * emissiveSample;
|
|
647
|
+
return applySceneFog(view, color, alpha, in.worldPos);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// ── G-buffer output struct (feat-20260612-hdrp-deferred-shading M2 / w12) ──
|
|
651
|
+
//
|
|
652
|
+
// D-8: g-buffer fragment lives in default-standard-pbr as an additional entry
|
|
653
|
+
// point (`fs_gbuffer`), NOT a separate MaterialShader. This aligns with D-1
|
|
654
|
+
// (concept count compression — no new shader id for the same material).
|
|
655
|
+
//
|
|
656
|
+
// D-2 / requirements §3.2 g-buffer schema:
|
|
657
|
+
// @location(0) RT0 = normal.rgb + roughness.a → rgba16f
|
|
658
|
+
// @location(1) RT1 = albedo.rgb + metallic.a → rgba8unorm
|
|
659
|
+
// @location(2) RT2 = emissive.rgb + ao.a → rgba16f
|
|
660
|
+
|
|
661
|
+
struct GBufferOutput {
|
|
662
|
+
@location(0) normal_roughness : vec4<f32>,
|
|
663
|
+
@location(1) albedo_metallic : vec4<f32>,
|
|
664
|
+
@location(2) emissive_ao : vec4<f32>,
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
/// Deferred g-buffer fragment entry: writes material properties (normal,
|
|
668
|
+
/// albedo, roughness, metallic, emissive, ao) to a 3-RT g-buffer for the
|
|
669
|
+
/// deferred lighting pass to decode. Lighting evaluation is deferred — this
|
|
670
|
+
/// entry does NOT compute GGX / directional / cluster lights.
|
|
671
|
+
///
|
|
672
|
+
/// Shares the same vertex shader `vs_main` and the same material UBO / texture
|
|
673
|
+
/// bindings as `fs_main`; only the fragment output differs. The HDRP
|
|
674
|
+
/// pipeline's g-buffer render pass binds this entry point via the shader's
|
|
675
|
+
/// multi-entry support (passKind='deferred' selects `fs_gbuffer`).
|
|
676
|
+
@fragment
|
|
677
|
+
fn fs_gbuffer(in : VsOut) -> GBufferOutput {
|
|
678
|
+
let baseUv = transformedMaterialUv(
|
|
679
|
+
material.baseColorTextureCoordinatesTransform,
|
|
680
|
+
material.baseColorTextureCoordinatesMetadata,
|
|
681
|
+
in,
|
|
682
|
+
);
|
|
683
|
+
let baseSample = sampleMaterialTexture(baseColorTexture, baseColorSampler, baseUv, material.baseColorTextureCoordinatesMetadata.zw);
|
|
684
|
+
let vertexColor = materialVertexColor(in);
|
|
685
|
+
alphaTest(material.baseColor.a * baseSample.a * vertexColor.a);
|
|
686
|
+
let alpha = materialAlpha(baseSample) * vertexColor.a;
|
|
687
|
+
let albedo = material.baseColor.rgb * baseSample.rgb * vertexColor.rgb;
|
|
688
|
+
|
|
689
|
+
let mrUv = transformedMaterialUv(
|
|
690
|
+
material.metallicRoughnessTextureCoordinatesTransform,
|
|
691
|
+
material.metallicRoughnessTextureCoordinatesMetadata,
|
|
692
|
+
in,
|
|
693
|
+
);
|
|
694
|
+
let mrSample = sampleMaterialTexture(metallicRoughnessTexture, metallicRoughnessSampler, mrUv, material.metallicRoughnessTextureCoordinatesMetadata.zw);
|
|
695
|
+
let metallic = material.metallic * pick_channel(mrSample, u32(material.metallicChannel));
|
|
696
|
+
let roughnessTex = pick_channel(mrSample, u32(material.roughnessChannel));
|
|
697
|
+
|
|
698
|
+
var a = max(material.roughness, 0.04);
|
|
699
|
+
a = a * roughnessTex;
|
|
700
|
+
|
|
701
|
+
let normalUv = transformedMaterialUv(material.normalTextureCoordinatesTransform, material.normalTextureCoordinatesMetadata, in);
|
|
702
|
+
let normSampleRg = sampleMaterialTexture(normalTexture, normalSampler, normalUv, material.normalTextureCoordinatesMetadata.zw).rg;
|
|
703
|
+
let normTangent = scaleTangentSpaceNormal(
|
|
704
|
+
decodeTangentSpaceNormalRg(normSampleRg), material.normalScale,
|
|
705
|
+
);
|
|
706
|
+
let n = applyTBN(in.worldNormal, in.worldTangent, normTangent);
|
|
707
|
+
|
|
708
|
+
let emissiveUv = transformedMaterialUv(material.emissiveTextureCoordinatesTransform, material.emissiveTextureCoordinatesMetadata, in);
|
|
709
|
+
let emissiveSample = sampleMaterialTexture(emissiveTexture, emissiveSampler, emissiveUv, material.emissiveTextureCoordinatesMetadata.zw).rgb;
|
|
710
|
+
let emissive = material.emissive * material.emissiveIntensity * emissiveSample;
|
|
711
|
+
|
|
712
|
+
let occlusionUv = transformedMaterialUv(material.occlusionTextureCoordinatesTransform, material.occlusionTextureCoordinatesMetadata, in);
|
|
713
|
+
let aoSample = sampleMaterialTexture(occlusionTexture, occlusionSampler, occlusionUv, material.occlusionTextureCoordinatesMetadata.zw);
|
|
714
|
+
let ao = mix(1.0, aoSample.r, material.occlusionStrength);
|
|
715
|
+
|
|
716
|
+
var out : GBufferOutput;
|
|
717
|
+
out.normal_roughness = vec4<f32>(n * 0.5 + 0.5, a);
|
|
718
|
+
out.albedo_metallic = vec4<f32>(albedo, metallic);
|
|
719
|
+
out.emissive_ao = vec4<f32>(emissive, ao);
|
|
720
|
+
return out;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
struct TemporalVsOut {
|
|
724
|
+
@builtin(position) clip : vec4<f32>,
|
|
725
|
+
@location(0) uv : vec2<f32>,
|
|
726
|
+
@location(1) uv1 : vec2<f32>,
|
|
727
|
+
@location(2) uv2 : vec2<f32>,
|
|
728
|
+
@location(3) uv3 : vec2<f32>,
|
|
729
|
+
@location(4) uv4 : vec2<f32>,
|
|
730
|
+
@location(5) uv5 : vec2<f32>,
|
|
731
|
+
@location(6) uv6 : vec2<f32>,
|
|
732
|
+
@location(7) uv7 : vec2<f32>,
|
|
733
|
+
@location(8) @interpolate(linear) currentClip : vec4<f32>,
|
|
734
|
+
@location(9) @interpolate(linear) previousClip : vec4<f32>,
|
|
735
|
+
#ifdef VERTEX_COLOR_AVAILABLE
|
|
736
|
+
@location(14) color : vec4<f32>,
|
|
737
|
+
#endif
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
@vertex
|
|
741
|
+
fn vs_temporal(in : VsIn, @builtin(instance_index) idx : u32) -> TemporalVsOut {
|
|
742
|
+
let currentWorld = meshes[0].worldFromLocal *
|
|
743
|
+
instances[idx].localFromInstance * vec4<f32>(in.pos, 1.0);
|
|
744
|
+
var previousWorld = currentWorld;
|
|
745
|
+
#if STORAGE_BUFFER_AVAILABLE == true
|
|
746
|
+
previousWorld = meshes[0].previousWorldFromLocal *
|
|
747
|
+
instances[idx].previousLocalFromInstance * vec4<f32>(in.pos, 1.0);
|
|
748
|
+
#endif
|
|
749
|
+
var out : TemporalVsOut;
|
|
750
|
+
out.currentClip = view.temporalCurrentViewProj * currentWorld;
|
|
751
|
+
out.clip = out.currentClip;
|
|
752
|
+
out.previousClip = view.temporalPreviousViewProj * previousWorld;
|
|
753
|
+
out.uv = in.uv;
|
|
754
|
+
out.uv1 = in.uv1;
|
|
755
|
+
out.uv2 = in.uv2;
|
|
756
|
+
out.uv3 = in.uv3;
|
|
757
|
+
out.uv4 = in.uv4;
|
|
758
|
+
out.uv5 = in.uv5;
|
|
759
|
+
out.uv6 = in.uv6;
|
|
760
|
+
out.uv7 = in.uv7;
|
|
761
|
+
#ifdef VERTEX_COLOR_AVAILABLE
|
|
762
|
+
out.color = in.color;
|
|
763
|
+
#endif
|
|
764
|
+
return out;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
fn temporalVertexAlpha(in : TemporalVsOut) -> f32 {
|
|
768
|
+
#ifdef VERTEX_COLOR_AVAILABLE
|
|
769
|
+
return in.color.a;
|
|
770
|
+
#else
|
|
771
|
+
return 1.0;
|
|
772
|
+
#endif
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
@fragment
|
|
776
|
+
fn fs_temporal(in : TemporalVsOut) -> @location(0) vec4<f32> {
|
|
777
|
+
var reactive = 1.0;
|
|
778
|
+
#if STORAGE_BUFFER_AVAILABLE == true
|
|
779
|
+
reactive = meshes[0].temporal.x;
|
|
780
|
+
#endif
|
|
781
|
+
return projectPbrSceneTemporal(
|
|
782
|
+
material.baseColor.a * temporalVertexAlpha(in),
|
|
783
|
+
material.alphaCutoff,
|
|
784
|
+
baseColorTexture,
|
|
785
|
+
baseColorSampler,
|
|
786
|
+
material.baseColorTextureCoordinatesTransform,
|
|
787
|
+
material.baseColorTextureCoordinatesMetadata,
|
|
788
|
+
in.currentClip,
|
|
789
|
+
in.previousClip,
|
|
790
|
+
reactive,
|
|
791
|
+
in.uv, in.uv1, in.uv2, in.uv3,
|
|
792
|
+
in.uv4, in.uv5, in.uv6, in.uv7,
|
|
793
|
+
);
|
|
794
|
+
}
|