@forgeax/engine-shader 0.1.23 → 0.1.24
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 +13 -4
- package/dist/ShaderRegistry.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +28 -52
- package/dist/index.mjs.map +1 -1
- package/dist/material-schemas.d.ts +31 -0
- package/dist/material-schemas.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/ShaderRegistry.ts +0 -17
- package/src/__tests__/default-standard-pbr-alpha.unit.test.ts +9 -3
- package/src/__tests__/default-standard-pbr-transmission.unit.test.ts +48 -166
- package/src/__tests__/deferred-lighting-ssao.test.ts +3 -3
- package/src/__tests__/ibl-irradiance.unit.test.ts +9 -0
- package/src/__tests__/lighting-directional.unit.test.ts +36 -0
- package/src/__tests__/lighting-punctual.unit.test.ts +12 -4
- package/src/__tests__/material-builtins.unit.test.ts +21 -3
- package/src/__tests__/material-contract.unit.test.ts +131 -17
- package/src/__tests__/physical-clearcoat.unit.test.ts +65 -0
- package/src/__tests__/probe-lighting-composition.unit.test.ts +3 -2
- package/src/__tests__/shader-manifest-transmission.unit.test.ts +8 -10
- package/src/__tests__/shader.unit.test.ts +52 -87
- package/src/__tests__/spot-cookie-composition.unit.test.ts +2 -1
- package/src/__tests__/standard-webgl2-varying-budget.unit.test.ts +17 -0
- package/src/__tests__/surface-v1-negative.unit.test.ts +37 -0
- package/src/__tests__/surface-v1.unit.test.ts +83 -0
- package/src/__tests__/transmission-thickness-scale.unit.test.ts +2 -6
- package/src/__tests__/vertex-color-variant.unit.test.ts +16 -5
- package/src/default-standard-pbr-skin.wgsl +678 -192
- package/src/default-standard-pbr.wgsl +563 -279
- package/src/default_standard_surface.wgsl +88 -0
- package/src/hdrp-cluster-forward.wgsl +225 -0
- package/src/ibl-irradiance.wgsl +43 -8
- package/src/ibl-prefilter.wgsl +13 -3
- package/src/ibl-shared.wgsl +9 -2
- package/src/index.ts +6 -0
- package/src/lighting-directional.wgsl +3 -1
- package/src/lighting-spot-modifiers.wgsl +8 -1
- package/src/lighting-spot-projector.wgsl +31 -0
- package/src/material/artifact-types.ts +2 -2
- package/src/material/physical/anisotropy.wgsl +26 -0
- package/src/material/physical/clearcoat.wgsl +24 -0
- package/src/material/physical/iridescence.wgsl +23 -0
- package/src/material/physical/sheen.wgsl +17 -0
- package/src/material-schemas.ts +65 -30
- package/src/register-default-standard-pbr-skin.ts +2 -2
- package/src/standard-cluster.wgsl +1 -36
- package/src/surface_v1.wgsl +26 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#define_import_path forgeax_material::default_standard_surface
|
|
2
|
+
|
|
3
|
+
#import forgeax_material::surface_v1::{SurfaceInput, SurfaceData}
|
|
4
|
+
|
|
5
|
+
// The default Surface owns only base facts. Standard's generated Material
|
|
6
|
+
// interface remains the source of the scalar and texture values; the
|
|
7
|
+
// Standard template owns physical-layer selection and lighting.
|
|
8
|
+
fn surfaceUv(input : SurfaceInput, transform : vec4<f32>, metadata : vec4<f32>) -> vec2<f32> {
|
|
9
|
+
let source = select(input.uv0, input.uv1, metadata.x >= 1.0);
|
|
10
|
+
let scaled = source * transform.zw;
|
|
11
|
+
let c = cos(metadata.y);
|
|
12
|
+
let s = sin(metadata.y);
|
|
13
|
+
return vec2<f32>(scaled.x * c - scaled.y * s, scaled.x * s + scaled.y * c) + transform.xy;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
fn surfaceChannel(value : vec4<f32>, channel : u32) -> f32 {
|
|
17
|
+
switch (channel) {
|
|
18
|
+
case 0u: { return value.r; }
|
|
19
|
+
case 1u: { return value.g; }
|
|
20
|
+
case 2u: { return value.b; }
|
|
21
|
+
default: { return value.a; }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fn surfaceNormal(input : SurfaceInput, encoded : vec4<f32>, normalScale : f32) -> vec3<f32> {
|
|
26
|
+
let tangentXY = (encoded.rg * 2.0 - vec2<f32>(1.0)) * normalScale;
|
|
27
|
+
let tangentZ = sqrt(max(1.0 - dot(tangentXY, tangentXY), 0.0));
|
|
28
|
+
let geometric = normalize(input.geometricNormalWS);
|
|
29
|
+
let tangent = normalize(input.tangentWS.xyz - geometric * dot(geometric, input.tangentWS.xyz));
|
|
30
|
+
let bitangent = normalize(cross(geometric, tangent)) * input.tangentWS.w;
|
|
31
|
+
return normalize(tangent * tangentXY.x + bitangent * tangentXY.y + geometric * tangentZ);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fn evaluate_surface(input : SurfaceInput) -> SurfaceData {
|
|
35
|
+
let baseSample = textureSample(
|
|
36
|
+
baseColorTexture,
|
|
37
|
+
baseColorSampler,
|
|
38
|
+
surfaceUv(input, material.baseColorTextureCoordinatesTransform, material.baseColorTextureCoordinatesMetadata) * material.baseColorTextureCoordinatesMetadata.zw,
|
|
39
|
+
);
|
|
40
|
+
let metallicRoughnessSample = textureSample(
|
|
41
|
+
metallicRoughnessTexture,
|
|
42
|
+
metallicRoughnessSampler,
|
|
43
|
+
surfaceUv(input, material.metallicRoughnessTextureCoordinatesTransform, material.metallicRoughnessTextureCoordinatesMetadata) * material.metallicRoughnessTextureCoordinatesMetadata.zw,
|
|
44
|
+
);
|
|
45
|
+
let normalSample = textureSample(
|
|
46
|
+
normalTexture,
|
|
47
|
+
normalSampler,
|
|
48
|
+
surfaceUv(input, material.normalTextureCoordinatesTransform, material.normalTextureCoordinatesMetadata) * material.normalTextureCoordinatesMetadata.zw,
|
|
49
|
+
);
|
|
50
|
+
let emissiveSample = textureSample(
|
|
51
|
+
emissiveTexture,
|
|
52
|
+
emissiveSampler,
|
|
53
|
+
surfaceUv(input, material.emissiveTextureCoordinatesTransform, material.emissiveTextureCoordinatesMetadata) * material.emissiveTextureCoordinatesMetadata.zw,
|
|
54
|
+
);
|
|
55
|
+
let occlusionSample = textureSample(
|
|
56
|
+
occlusionTexture,
|
|
57
|
+
occlusionSampler,
|
|
58
|
+
surfaceUv(input, material.occlusionTextureCoordinatesTransform, material.occlusionTextureCoordinatesMetadata) * material.occlusionTextureCoordinatesMetadata.zw,
|
|
59
|
+
);
|
|
60
|
+
let vertexColor = input.vertexColor;
|
|
61
|
+
let baseColor = material.baseColor.rgb * baseSample.rgb * vertexColor.rgb;
|
|
62
|
+
let metallic = clamp(
|
|
63
|
+
material.metallic * surfaceChannel(metallicRoughnessSample, u32(material.metallicChannel)),
|
|
64
|
+
0.0,
|
|
65
|
+
1.0,
|
|
66
|
+
);
|
|
67
|
+
let roughness = clamp(
|
|
68
|
+
material.roughness * surfaceChannel(metallicRoughnessSample, u32(material.roughnessChannel)),
|
|
69
|
+
0.04,
|
|
70
|
+
1.0,
|
|
71
|
+
);
|
|
72
|
+
let emissive = material.emissive * material.emissiveIntensity * emissiveSample.rgb;
|
|
73
|
+
let occlusion = clamp(
|
|
74
|
+
1.0 + (occlusionSample.r - 1.0) * material.occlusionStrength,
|
|
75
|
+
0.0,
|
|
76
|
+
1.0,
|
|
77
|
+
);
|
|
78
|
+
return SurfaceData(
|
|
79
|
+
baseColor,
|
|
80
|
+
surfaceNormal(input, normalSample, material.normalScale),
|
|
81
|
+
metallic,
|
|
82
|
+
roughness,
|
|
83
|
+
emissive,
|
|
84
|
+
occlusion,
|
|
85
|
+
clamp(material.baseColor.a * baseSample.a * vertexColor.a, 0.0, 1.0),
|
|
86
|
+
clamp(material.alphaCutoff, 0.0, 1.0),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// hdrp-cluster-forward.wgsl — HDRP cluster-forward punctual light evaluation.
|
|
2
|
+
// feat-20260608-cluster-lighting M4 / w17.
|
|
3
|
+
//
|
|
4
|
+
// The cluster module owns membership and raw payload decoding only. Punctual
|
|
5
|
+
// BRDF, range, cone, PCF, and fail-open behavior live in lighting-punctual.
|
|
6
|
+
|
|
7
|
+
#define_import_path forgeax_hdrp::cluster_forward
|
|
8
|
+
|
|
9
|
+
#import forgeax_pbr::lighting_punctual::{evalPoint, evalSpot, evalSpotShadowed}
|
|
10
|
+
#import forgeax_pbr::lighting_attenuation::{projectSpotUv}
|
|
11
|
+
#import forgeax_view::common::{view}
|
|
12
|
+
#ifdef PROJECTOR_AVAILABLE
|
|
13
|
+
#import forgeax_view::common::{projectorTexture, projectorSampler}
|
|
14
|
+
#endif
|
|
15
|
+
#ifdef POINT_SHADOW_AVAILABLE
|
|
16
|
+
#import forgeax_pbr::lighting_punctual::{evalPointShadowed}
|
|
17
|
+
#endif
|
|
18
|
+
|
|
19
|
+
#ifdef CLUSTER_FORWARD_AVAILABLE
|
|
20
|
+
|
|
21
|
+
const KIND_POINT: u32 = 0u;
|
|
22
|
+
const KIND_SPOT: u32 = 1u;
|
|
23
|
+
const PROJECTOR_ONLY_TILE: i32 = -2;
|
|
24
|
+
|
|
25
|
+
// LightSlot is the byte-frozen 64B std430/std140 transport contract.
|
|
26
|
+
// [0..2] position, [3] invRangeSquared
|
|
27
|
+
// [4..6] colorTimesIntensity, [7] cosInner
|
|
28
|
+
// [8..10] direction, [11] cosOuter
|
|
29
|
+
// [12] raw kind u32, [13] shadow identity i32
|
|
30
|
+
// [14] point near / spot shadow intensity f32 bits
|
|
31
|
+
// [15] point far / spot PCF kernel width f32 bits
|
|
32
|
+
struct LightSlot {
|
|
33
|
+
position : vec4<f32>,
|
|
34
|
+
color : vec4<f32>,
|
|
35
|
+
direction : vec4<f32>,
|
|
36
|
+
kind_and_shadow : vec4<u32>,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const LIGHTSLOT_BYTE_SIZE: u32 = 64u;
|
|
40
|
+
|
|
41
|
+
fn sampleClusterSpotProjector(lightViewProj : mat4x4<f32>, worldPos : vec3<f32>) -> vec3<f32> {
|
|
42
|
+
#ifdef PROJECTOR_AVAILABLE
|
|
43
|
+
let clip = lightViewProj * vec4<f32>(worldPos, 1.0);
|
|
44
|
+
// The projector is a cookie, not an extra cone. A fragment that cannot be
|
|
45
|
+
// projected (or lies outside the tile) keeps the analytic Spot contribution
|
|
46
|
+
// intact, matching Three's SpotLightNode fail-open map gate.
|
|
47
|
+
if (abs(clip.w) < 1e-6) { return vec3<f32>(1.0); }
|
|
48
|
+
let uv = projectSpotUv(lightViewProj, worldPos);
|
|
49
|
+
if (any(uv < vec2<f32>(0.0)) || any(uv > vec2<f32>(1.0))) {
|
|
50
|
+
return vec3<f32>(1.0);
|
|
51
|
+
}
|
|
52
|
+
return textureSampleLevel(projectorTexture, projectorSampler, uv, 0.0).rgb;
|
|
53
|
+
#else
|
|
54
|
+
return vec3<f32>(1.0);
|
|
55
|
+
#endif
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
struct ClusterUniform {
|
|
59
|
+
grid : vec4<u32>,
|
|
60
|
+
near_far_log : vec4<f32>,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
#if STORAGE_BUFFER_AVAILABLE == true
|
|
64
|
+
@group(2) @binding(3) var<storage, read> light_data : array<LightSlot, 256>;
|
|
65
|
+
@group(2) @binding(4) var<storage, read> cluster_grid : array<u32>;
|
|
66
|
+
@group(2) @binding(5) var<storage, read> light_index_list : array<u32>;
|
|
67
|
+
#else
|
|
68
|
+
@group(2) @binding(3) var<uniform> light_data_uniform : array<LightSlot, 128>;
|
|
69
|
+
#endif
|
|
70
|
+
@group(2) @binding(6) var<uniform> cluster_uniform : ClusterUniform;
|
|
71
|
+
|
|
72
|
+
fn get_ssao_intensity() -> f32 {
|
|
73
|
+
return cluster_uniform.near_far_log.w;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
fn view_z_to_z_slice(
|
|
77
|
+
view_z : f32,
|
|
78
|
+
grid_z : u32,
|
|
79
|
+
near : f32,
|
|
80
|
+
far : f32,
|
|
81
|
+
log_far_over_near : f32,
|
|
82
|
+
) -> u32 {
|
|
83
|
+
if (view_z >= -near) {
|
|
84
|
+
return 0u;
|
|
85
|
+
}
|
|
86
|
+
let slice = floor(log(-view_z / near) / log_far_over_near * f32(grid_z));
|
|
87
|
+
let u_slice = u32(slice);
|
|
88
|
+
if (u_slice >= grid_z) {
|
|
89
|
+
return grid_z - 1u;
|
|
90
|
+
}
|
|
91
|
+
return u_slice;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
fn ndc_position_to_cluster(
|
|
95
|
+
ndc : vec3<f32>,
|
|
96
|
+
view_z : f32,
|
|
97
|
+
grid_x : u32,
|
|
98
|
+
grid_y : u32,
|
|
99
|
+
grid_z : u32,
|
|
100
|
+
near : f32,
|
|
101
|
+
far : f32,
|
|
102
|
+
log_far : f32,
|
|
103
|
+
) -> vec3<u32> {
|
|
104
|
+
let cx = clamp(u32(floor((ndc.x * 0.5 + 0.5) * f32(grid_x))), 0u, grid_x - 1u);
|
|
105
|
+
let cy = clamp(u32(floor((ndc.y * 0.5 + 0.5) * f32(grid_y))), 0u, grid_y - 1u);
|
|
106
|
+
let cz = view_z_to_z_slice(view_z, grid_z, near, far, log_far);
|
|
107
|
+
return vec3(cx, cy, cz);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Decode one kind first, then delegate the complete evaluation to the shared
|
|
111
|
+
// punctual owner. Unknown kinds are explicitly zero contribution.
|
|
112
|
+
fn evaluate_cluster_light(
|
|
113
|
+
light : LightSlot,
|
|
114
|
+
world_pos : vec3<f32>,
|
|
115
|
+
normal : vec3<f32>,
|
|
116
|
+
view_dir : vec3<f32>,
|
|
117
|
+
base_color : vec3<f32>,
|
|
118
|
+
metallic : f32,
|
|
119
|
+
alpha_sq : f32,
|
|
120
|
+
f0 : vec3<f32>,
|
|
121
|
+
) -> vec3<f32> {
|
|
122
|
+
let kind = light.kind_and_shadow.x;
|
|
123
|
+
if (kind == KIND_POINT) {
|
|
124
|
+
#ifdef POINT_SHADOW_AVAILABLE
|
|
125
|
+
let layer = bitcast<i32>(light.kind_and_shadow.y);
|
|
126
|
+
if (layer >= 0) {
|
|
127
|
+
return evalPointShadowed(
|
|
128
|
+
light.position.xyz, light.color.xyz, light.position.w,
|
|
129
|
+
world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
|
|
130
|
+
layer,
|
|
131
|
+
bitcast<f32>(light.kind_and_shadow.z),
|
|
132
|
+
bitcast<f32>(light.kind_and_shadow.w),
|
|
133
|
+
0.005, 0.05,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
#endif
|
|
137
|
+
return evalPoint(
|
|
138
|
+
light.position.xyz, light.color.xyz, light.position.w,
|
|
139
|
+
world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (kind == KIND_SPOT) {
|
|
143
|
+
let tile = bitcast<i32>(light.kind_and_shadow.y);
|
|
144
|
+
if (tile >= 0) {
|
|
145
|
+
let projector = sampleClusterSpotProjector(view.spotLightViewProj[tile], world_pos);
|
|
146
|
+
return evalSpotShadowed(
|
|
147
|
+
light.position.xyz, light.direction.xyz, light.color.xyz,
|
|
148
|
+
// LightSlot packs cosInner in color.w and cosOuter in direction.w;
|
|
149
|
+
// keep the evaluator's named cone order intact at the decode boundary.
|
|
150
|
+
light.color.w, light.direction.w, light.position.w,
|
|
151
|
+
world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
|
|
152
|
+
view.spotLightViewProj[tile], tile, 0.005, 0.05,
|
|
153
|
+
clamp(
|
|
154
|
+
round(select(3.0, bitcast<f32>(light.kind_and_shadow.w),
|
|
155
|
+
bitcast<f32>(light.kind_and_shadow.w) > 0.5)),
|
|
156
|
+
1.0,
|
|
157
|
+
5.0,
|
|
158
|
+
),
|
|
159
|
+
clamp(bitcast<f32>(light.kind_and_shadow.z), 0.0, 1.0),
|
|
160
|
+
) * projector;
|
|
161
|
+
}
|
|
162
|
+
if (tile == PROJECTOR_ONLY_TILE) {
|
|
163
|
+
let projector = sampleClusterSpotProjector(view.spotLightViewProj[0], world_pos);
|
|
164
|
+
return evalSpot(
|
|
165
|
+
light.position.xyz, light.direction.xyz, light.color.xyz,
|
|
166
|
+
light.color.w, light.direction.w, light.position.w,
|
|
167
|
+
world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
|
|
168
|
+
) * projector;
|
|
169
|
+
}
|
|
170
|
+
return evalSpot(
|
|
171
|
+
light.position.xyz, light.direction.xyz, light.color.xyz,
|
|
172
|
+
light.color.w, light.direction.w, light.position.w,
|
|
173
|
+
world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
return vec3<f32>(0.0);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fn evaluate_cluster_lights(
|
|
180
|
+
ndc : vec3<f32>,
|
|
181
|
+
view_z : f32,
|
|
182
|
+
world_pos : vec3<f32>,
|
|
183
|
+
normal : vec3<f32>,
|
|
184
|
+
view_dir : vec3<f32>,
|
|
185
|
+
base_color : vec3<f32>,
|
|
186
|
+
metallic : f32,
|
|
187
|
+
alpha_sq : f32,
|
|
188
|
+
f0 : vec3<f32>,
|
|
189
|
+
) -> vec3<f32> {
|
|
190
|
+
let gx = cluster_uniform.grid.x;
|
|
191
|
+
let gy = cluster_uniform.grid.y;
|
|
192
|
+
let gz = cluster_uniform.grid.z;
|
|
193
|
+
let near = cluster_uniform.near_far_log.x;
|
|
194
|
+
let far = cluster_uniform.near_far_log.y;
|
|
195
|
+
let log_far = cluster_uniform.near_far_log.z;
|
|
196
|
+
var total_radiance = vec3<f32>(0.0);
|
|
197
|
+
|
|
198
|
+
#if STORAGE_BUFFER_AVAILABLE == true
|
|
199
|
+
let cluster_idx = ndc_position_to_cluster(ndc, view_z, gx, gy, gz, near, far, log_far);
|
|
200
|
+
let cluster_linear = cluster_idx.z * gy * gx + cluster_idx.y * gx + cluster_idx.x;
|
|
201
|
+
let grid_offset = cluster_linear * 2u;
|
|
202
|
+
let list_offset = cluster_grid[grid_offset];
|
|
203
|
+
let list_count = cluster_grid[grid_offset + 1u];
|
|
204
|
+
for (var i = 0u; i < list_count; i = i + 1u) {
|
|
205
|
+
let light = light_data[light_index_list[list_offset + i]];
|
|
206
|
+
total_radiance += evaluate_cluster_light(
|
|
207
|
+
light, world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
#else
|
|
211
|
+
let light_count = min(cluster_uniform.grid.w, 128u);
|
|
212
|
+
for (var i = 0u; i < 128u; i = i + 1u) {
|
|
213
|
+
if (i >= light_count) {
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
total_radiance += evaluate_cluster_light(
|
|
217
|
+
light_data_uniform[i], world_pos, normal, view_dir,
|
|
218
|
+
base_color, metallic, alpha_sq, f0,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
#endif
|
|
222
|
+
return total_radiance;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
#endif // CLUSTER_FORWARD_AVAILABLE
|
package/src/ibl-irradiance.wgsl
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
// @forgeax/engine-shader - ibl-irradiance.wgsl
|
|
4
4
|
// (feat-20260520-skylight-ibl-cubemap M3 / t44).
|
|
5
5
|
//
|
|
6
|
-
// Diffuse irradiance convolution. Per LearnOpenGL §6.2.2:
|
|
7
|
-
// Riemann sum (sampleDelta = 0.
|
|
8
|
-
// the convolved irradiance cubemap consumed by sampleIblDiffuse() at
|
|
9
|
-
// runtime.
|
|
6
|
+
// Diffuse irradiance convolution. Per LearnOpenGL §6.2.2: a bounded
|
|
7
|
+
// hemisphere Riemann sum (sampleDelta = 0.05) integrates the env cubemap to
|
|
8
|
+
// produce the convolved irradiance cubemap consumed by sampleIblDiffuse() at
|
|
9
|
+
// runtime. The budget is deliberately bounded for the rgba16float bake target
|
|
10
|
+
// so every backend completes the fragment without overflowing its shader
|
|
11
|
+
// work budget.
|
|
10
12
|
//
|
|
11
13
|
// @group(0) = per-face viewProj uniform.
|
|
12
14
|
// @group(1) = env cubemap (texture_cube<f32>) + sampler. This is the same
|
|
@@ -36,7 +38,25 @@ struct CubemapFaceUniforms {
|
|
|
36
38
|
@group(1) @binding(0) var envCube: texture_cube<f32>;
|
|
37
39
|
@group(1) @binding(1) var envSamplerS: sampler;
|
|
38
40
|
|
|
39
|
-
const IRRADIANCE_SAMPLE_DELTA: f32 = 0.
|
|
41
|
+
const IRRADIANCE_SAMPLE_DELTA: f32 = 0.05;
|
|
42
|
+
|
|
43
|
+
// The bake target is rgba16float, so a sample is admissible only when all of
|
|
44
|
+
// its lanes can be represented by that format. Equality catches NaN while
|
|
45
|
+
// the bound also rejects infinities before they enter the running sum.
|
|
46
|
+
fn irradianceFiniteScalar(value: f32) -> bool {
|
|
47
|
+
return value == value && abs(value) <= 65504.0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fn irradianceFiniteVec3(value: vec3<f32>) -> bool {
|
|
51
|
+
return irradianceFiniteScalar(value.x) &&
|
|
52
|
+
irradianceFiniteScalar(value.y) &&
|
|
53
|
+
irradianceFiniteScalar(value.z);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fn irradianceSanitizeScalar(value: f32) -> f32 {
|
|
57
|
+
let bounded = clamp(value, 0.0, 65504.0);
|
|
58
|
+
return select(0.0, bounded, irradianceFiniteScalar(value));
|
|
59
|
+
}
|
|
40
60
|
|
|
41
61
|
@vertex
|
|
42
62
|
fn cubemap_vs(in0: CubemapVsIn) -> CubemapVsOut {
|
|
@@ -76,8 +96,18 @@ fn irradianceConvolve_fs(in0: CubemapVsOut) -> @location(0) vec4<f32> {
|
|
|
76
96
|
let sampleColor = textureSampleLevel(
|
|
77
97
|
envCube, envSamplerS, sampleVec, 0.0,
|
|
78
98
|
).rgb;
|
|
79
|
-
|
|
80
|
-
|
|
99
|
+
// A malformed direction or backend sample must not poison the whole
|
|
100
|
+
// irradiance face. Skip only non-finite samples and keep the
|
|
101
|
+
// normalization count in lockstep with the accumulated radiance.
|
|
102
|
+
let sampleWeight = cos(theta) * sin(theta);
|
|
103
|
+
if (
|
|
104
|
+
irradianceFiniteVec3(sampleVec) &&
|
|
105
|
+
irradianceFiniteVec3(sampleColor) &&
|
|
106
|
+
irradianceFiniteScalar(sampleWeight)
|
|
107
|
+
) {
|
|
108
|
+
irradiance += sampleColor * sampleWeight;
|
|
109
|
+
nrSamples += 1.0;
|
|
110
|
+
}
|
|
81
111
|
theta += IRRADIANCE_SAMPLE_DELTA;
|
|
82
112
|
}
|
|
83
113
|
phi += IRRADIANCE_SAMPLE_DELTA;
|
|
@@ -88,5 +118,10 @@ fn irradianceConvolve_fs(in0: CubemapVsOut) -> @location(0) vec4<f32> {
|
|
|
88
118
|
// consumes this payload directly; applying another Lambert divide would
|
|
89
119
|
// darken diffuse IBL by PI.
|
|
90
120
|
irradiance = PI * irradiance / max(nrSamples, 1.0);
|
|
91
|
-
return vec4<f32>(
|
|
121
|
+
return vec4<f32>(
|
|
122
|
+
irradianceSanitizeScalar(irradiance.x),
|
|
123
|
+
irradianceSanitizeScalar(irradiance.y),
|
|
124
|
+
irradianceSanitizeScalar(irradiance.z),
|
|
125
|
+
1.0,
|
|
126
|
+
);
|
|
92
127
|
}
|
package/src/ibl-prefilter.wgsl
CHANGED
|
@@ -36,8 +36,10 @@ struct CubemapFaceUniforms {
|
|
|
36
36
|
struct PrefilterUniforms {
|
|
37
37
|
roughness: f32,
|
|
38
38
|
faceSize: f32,
|
|
39
|
+
// Actual mip count of the source cube bound at group(1). The source cube is
|
|
40
|
+
// currently base-level-only; the value prevents sampling an unallocated mip.
|
|
41
|
+
sourceMipLevelCount: f32,
|
|
39
42
|
// Keep the uniform block 16-byte aligned on WebGL2 downlevel backends.
|
|
40
|
-
_pad0: f32,
|
|
41
43
|
_pad1: f32,
|
|
42
44
|
};
|
|
43
45
|
|
|
@@ -48,6 +50,7 @@ struct PrefilterUniforms {
|
|
|
48
50
|
@group(1) @binding(1) var envSamplerS: sampler;
|
|
49
51
|
|
|
50
52
|
const PREFILTER_SAMPLE_COUNT: u32 = 1024u;
|
|
53
|
+
const PREFILTER_MIP_LEVEL_COUNT: f32 = 5.0;
|
|
51
54
|
|
|
52
55
|
@vertex
|
|
53
56
|
fn cubemap_vs(in0: CubemapVsIn) -> CubemapVsOut {
|
|
@@ -78,16 +81,23 @@ fn prefilterEnv_fs(in0: CubemapVsOut) -> @location(0) vec4<f32> {
|
|
|
78
81
|
let D0 = iblDGGX(max(dot(N, H), 0.0), roughness);
|
|
79
82
|
let NdotH0 = max(dot(N, H), 0.0);
|
|
80
83
|
let HdotV = max(dot(H, V), 0.0);
|
|
81
|
-
|
|
84
|
+
// A grazing half-vector can make HdotV exactly zero. Keep the PDF
|
|
85
|
+
// finite so the mip calculation cannot feed NaN into textureSampleLevel.
|
|
86
|
+
let pdf = D0 * NdotH0 / max(4.0 * HdotV, 0.0001) + 0.0001;
|
|
82
87
|
|
|
83
88
|
let resolution: f32 = 512.0;
|
|
84
89
|
let saTexel = 4.0 * PI / (6.0 * resolution * resolution);
|
|
85
90
|
let saSample = 1.0 / (f32(PREFILTER_SAMPLE_COUNT) * pdf + 0.0001);
|
|
86
91
|
|
|
87
|
-
let
|
|
92
|
+
let requestedMipLevel = min(select(
|
|
88
93
|
0.5 * log2(saSample / saTexel),
|
|
89
94
|
0.0,
|
|
90
95
|
roughness == 0.0,
|
|
96
|
+
), PREFILTER_MIP_LEVEL_COUNT - 1.0);
|
|
97
|
+
let mipLevel = clamp(
|
|
98
|
+
requestedMipLevel,
|
|
99
|
+
0.0,
|
|
100
|
+
max(prefUniforms.sourceMipLevelCount - 1.0, 0.0),
|
|
91
101
|
);
|
|
92
102
|
|
|
93
103
|
prefilteredColor += textureSampleLevel(
|
package/src/ibl-shared.wgsl
CHANGED
|
@@ -30,7 +30,10 @@ const PI: f32 = 3.14159265;
|
|
|
30
30
|
|
|
31
31
|
// Map a direction vector to equirectangular UV.
|
|
32
32
|
fn sampleSphericalMap(v: vec3<f32>) -> vec2<f32> {
|
|
33
|
-
|
|
33
|
+
// Cube-face rasterization can produce a direction whose normalized Y lane
|
|
34
|
+
// is a few ulps outside [-1, 1]. `asin` propagates that rounding error as
|
|
35
|
+
// NaN, poisoning the generated cubemap and every later IBL sample.
|
|
36
|
+
let uv = vec2<f32>(atan2(v.z, v.x), asin(clamp(v.y, -1.0, 1.0)));
|
|
34
37
|
return uv * INV_ATAN + 0.5;
|
|
35
38
|
}
|
|
36
39
|
|
|
@@ -87,7 +90,11 @@ fn importanceSampleGGX(Xi: vec2<f32>, N: vec3<f32>, roughness: f32) -> vec3<f32>
|
|
|
87
90
|
let a = roughness * roughness;
|
|
88
91
|
|
|
89
92
|
let phi = 2.0 * PI * Xi.x;
|
|
90
|
-
|
|
93
|
+
// The production Hammersley domain is i < N, so Xi.y stays below one.
|
|
94
|
+
// Retain a finite lower bound for callers that provide an endpoint sample
|
|
95
|
+
// directly; this is defensive input handling, not the production NaN fix.
|
|
96
|
+
let denominator = max(1.0 + (a * a - 1.0) * Xi.y, 1e-5);
|
|
97
|
+
let cosTheta = sqrt(max((1.0 - Xi.y) / denominator, 0.0));
|
|
91
98
|
let sinTheta = sqrt(max(1.0 - cosTheta * cosTheta, 0.0));
|
|
92
99
|
|
|
93
100
|
// GGX half-vector in tangent space.
|
package/src/index.ts
CHANGED
|
@@ -68,9 +68,15 @@ export {
|
|
|
68
68
|
DEFAULT_SPRITE_PARAM_SCHEMA,
|
|
69
69
|
DEFAULT_STANDARD_PBR_PARAM_SCHEMA,
|
|
70
70
|
DEFAULT_UNLIT_PARAM_SCHEMA,
|
|
71
|
+
STANDARD_BASE_PARAM_SCHEMA,
|
|
71
72
|
STANDARD_PBR_ALPHA_CUTOFF_DEFAULT,
|
|
72
73
|
STANDARD_PBR_ARTIFACT_RECEIPT,
|
|
73
74
|
STANDARD_PBR_SKIN_ARTIFACT_RECEIPT,
|
|
75
|
+
STANDARD_PHYSICAL_LAYER_PARAM_SCHEMA,
|
|
76
|
+
STANDARD_PHYSICAL_TEXTURE_FIELDS,
|
|
77
|
+
STANDARD_PIPELINE_PARAM_SCHEMA,
|
|
78
|
+
type StandardPhysicalTextureField,
|
|
79
|
+
standardPhysicalTextureFields,
|
|
74
80
|
} from './material-schemas.js';
|
|
75
81
|
export {
|
|
76
82
|
registerDefaultSpriteLit,
|
|
@@ -408,7 +408,9 @@ fn evalDirectionalNoShadow(
|
|
|
408
408
|
F0 : vec3<f32>,
|
|
409
409
|
) -> vec3<f32> {
|
|
410
410
|
let l = normalize(-view.lightDir);
|
|
411
|
-
let
|
|
411
|
+
let halfVector = viewDir + l;
|
|
412
|
+
let halfVectorLengthSquared = max(dot(halfVector, halfVector), 1e-8);
|
|
413
|
+
let h = halfVector * inverseSqrt(halfVectorLengthSquared);
|
|
412
414
|
let nDotL = max(dot(normal, l), 0.0);
|
|
413
415
|
let nDotV = max(dot(normal, viewDir), 1e-5);
|
|
414
416
|
let nDotH = max(dot(normal, h), 0.0);
|
|
@@ -67,7 +67,14 @@ fn spotModifierFactors(
|
|
|
67
67
|
ies = max(textureSampleLevel(iesProfileTexture, spotModifierSampler, iesUv, metadata.z, 0.0).r, 0.0);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
// The shadow lane uses 0xffffffff as its no-shadow sentinel. That value
|
|
71
|
+
// contains PROJECTOR_FLAG bits, so test the sentinel before interpreting
|
|
72
|
+
// the lane as a projector marker; otherwise every non-shadow Cookie spot
|
|
73
|
+
// silently takes the projector exclusion path.
|
|
74
|
+
if (
|
|
75
|
+
metadata.w != 0xffffffffu &&
|
|
76
|
+
(metadata.y == 0xffffffffu || (metadata.y & PROJECTOR_FLAG) == 0u)
|
|
77
|
+
) {
|
|
71
78
|
if (depth <= 0.0) {
|
|
72
79
|
cookie = vec3<f32>(0.0);
|
|
73
80
|
} else {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#define_import_path forgeax_pbr::lighting_spot_projector
|
|
2
|
+
|
|
3
|
+
// Clustered Standard owns the light membership/decode, while this helper
|
|
4
|
+
// keeps projector projection and resource sampling in one module. Keeping the
|
|
5
|
+
// projection/sample pair out of standard-cluster avoids a naga-oil scope edge
|
|
6
|
+
// when the low-resource projector path is composed with the shared cluster.
|
|
7
|
+
#import forgeax_pbr::lighting_attenuation::{projectSpotUv}
|
|
8
|
+
#ifdef EXTENDED_LIGHTING_AVAILABLE
|
|
9
|
+
#import forgeax_view::common::{spotModifierSampler, cookieTexture}
|
|
10
|
+
#else
|
|
11
|
+
#import forgeax_view::common::{projectorTexture, projectorSampler}
|
|
12
|
+
#endif
|
|
13
|
+
|
|
14
|
+
fn sampleStandardSpotProjector(
|
|
15
|
+
lightViewProj : mat4x4<f32>,
|
|
16
|
+
world_pos : vec3<f32>,
|
|
17
|
+
metadata : vec4<u32>,
|
|
18
|
+
) -> vec3<f32> {
|
|
19
|
+
let uv = projectSpotUv(lightViewProj, world_pos);
|
|
20
|
+
if (!(uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0)) {
|
|
21
|
+
return vec3<f32>(1.0);
|
|
22
|
+
}
|
|
23
|
+
#ifdef EXTENDED_LIGHTING_AVAILABLE
|
|
24
|
+
if (metadata.w == 0xffffffffu) {
|
|
25
|
+
return vec3<f32>(1.0);
|
|
26
|
+
}
|
|
27
|
+
return textureSampleLevel(cookieTexture, spotModifierSampler, uv, metadata.w, 0.0).rgb;
|
|
28
|
+
#else
|
|
29
|
+
return textureSampleLevel(projectorTexture, projectorSampler, uv, 0.0).rgb;
|
|
30
|
+
#endif
|
|
31
|
+
}
|
|
@@ -58,7 +58,7 @@ const STANDARD_PBR_TEXTURES = [
|
|
|
58
58
|
'baseColorTexture',
|
|
59
59
|
'metallicRoughnessTexture',
|
|
60
60
|
'normalTexture',
|
|
61
|
-
'
|
|
61
|
+
'specularColorTexture',
|
|
62
62
|
'emissiveTexture',
|
|
63
63
|
'occlusionTexture',
|
|
64
64
|
'transmissionTexture',
|
|
@@ -79,7 +79,7 @@ const STANDARD_PBR_NUMERIC_FIELDS = [
|
|
|
79
79
|
'alphaCutoff',
|
|
80
80
|
'clearcoat',
|
|
81
81
|
'clearcoatRoughness',
|
|
82
|
-
'
|
|
82
|
+
'specularColor',
|
|
83
83
|
'normalScale',
|
|
84
84
|
'transmission',
|
|
85
85
|
'ior',
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#define_import_path forgeax_pbr::anisotropy
|
|
2
|
+
|
|
3
|
+
// Engine-owned anisotropy helpers. The root contract supplies strength and
|
|
4
|
+
// rotation; the map (when present) supplies tangent direction in RG and a
|
|
5
|
+
// strength multiplier in B. Keeping the frame operation here means the
|
|
6
|
+
// Standard evaluator and custom Surface share one deterministic seam.
|
|
7
|
+
fn evaluateAnisotropicNormal(
|
|
8
|
+
normal : vec3<f32>,
|
|
9
|
+
tangent : vec4<f32>,
|
|
10
|
+
strength : f32,
|
|
11
|
+
rotation : f32,
|
|
12
|
+
direction : vec2<f32>,
|
|
13
|
+
) -> vec3<f32> {
|
|
14
|
+
let frameTangent = normalize(tangent.xyz);
|
|
15
|
+
let handedBitangent = normalize(cross(normal, frameTangent)) * select(-1.0, 1.0, tangent.w >= 0.0);
|
|
16
|
+
var mapDirection = vec2<f32>(1.0, 0.0);
|
|
17
|
+
if (dot(direction, direction) >= 1e-6) {
|
|
18
|
+
mapDirection = normalize(direction);
|
|
19
|
+
}
|
|
20
|
+
let mappedTangent = normalize(frameTangent * mapDirection.x + handedBitangent * mapDirection.y);
|
|
21
|
+
let mappedBitangent = normalize(cross(normal, mappedTangent)) * select(-1.0, 1.0, tangent.w >= 0.0);
|
|
22
|
+
let axis = normalize(mappedTangent * cos(rotation) + mappedBitangent * sin(rotation));
|
|
23
|
+
// A bounded bent normal preserves the zero-strength isotropic path while
|
|
24
|
+
// moving only the base specular highlight for non-zero anisotropy.
|
|
25
|
+
return normalize(normal + axis * clamp(strength, -0.999, 0.999) * 0.35);
|
|
26
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#define_import_path forgeax_pbr::clearcoat
|
|
2
|
+
|
|
3
|
+
// Clearcoat is the terminal physical layer. Its Fresnel term owns both the
|
|
4
|
+
// attenuation of lower layers and the weight of the coat radiance.
|
|
5
|
+
// The root supplies clearcoatRoughness and clearcoatNormalScale after their
|
|
6
|
+
// texture projections; this module deliberately owns only layer energy.
|
|
7
|
+
fn evaluateClearcoatFresnel(viewCosine : f32, clearcoatFactor : f32) -> f32 {
|
|
8
|
+
let cosine = clamp(viewCosine, 0.0, 1.0);
|
|
9
|
+
let oneMinusCosine = 1.0 - cosine;
|
|
10
|
+
let dielectricFresnel = 0.04 + 0.96 * oneMinusCosine * oneMinusCosine *
|
|
11
|
+
oneMinusCosine * oneMinusCosine * oneMinusCosine;
|
|
12
|
+
return clamp(clearcoatFactor, 0.0, 1.0) * dielectricFresnel;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
fn evaluateClearcoatLayer(
|
|
16
|
+
baseRadiance : vec3<f32>,
|
|
17
|
+
coatRadiance : vec3<f32>,
|
|
18
|
+
viewCosine : f32,
|
|
19
|
+
clearcoatFactor : f32,
|
|
20
|
+
) -> vec3<f32> {
|
|
21
|
+
let fresnel = evaluateClearcoatFresnel(viewCosine, clearcoatFactor);
|
|
22
|
+
let attenuatedBase = baseRadiance * (1.0 - fresnel);
|
|
23
|
+
return attenuatedBase + coatRadiance * fresnel;
|
|
24
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#define_import_path forgeax_pbr::iridescence
|
|
2
|
+
|
|
3
|
+
// Bounded RGB thin-film Fresnel approximation. Thickness is deliberately
|
|
4
|
+
// not clamped to min/max order: the glTF contract permits inverted bounds and
|
|
5
|
+
// the caller supplies the authored interval verbatim.
|
|
6
|
+
fn evaluateIridescenceFresnel(
|
|
7
|
+
baseF0 : vec3<f32>,
|
|
8
|
+
strength : f32,
|
|
9
|
+
filmIor : f32,
|
|
10
|
+
thicknessNanometres : f32,
|
|
11
|
+
) -> vec3<f32> {
|
|
12
|
+
let safeStrength = clamp(strength, 0.0, 1.0);
|
|
13
|
+
let safeIor = max(filmIor, 1.0);
|
|
14
|
+
let base = clamp((safeIor - 1.0) / (safeIor + 1.0), 0.0, 1.0);
|
|
15
|
+
let phase = thicknessNanometres * 0.018;
|
|
16
|
+
let spectral = vec3<f32>(
|
|
17
|
+
0.5 + 0.5 * cos(phase),
|
|
18
|
+
0.5 + 0.5 * cos(phase + 2.0943952),
|
|
19
|
+
0.5 + 0.5 * cos(phase + 4.1887903),
|
|
20
|
+
);
|
|
21
|
+
let film = clamp(vec3<f32>(base) * spectral, vec3<f32>(0.0), vec3<f32>(1.0));
|
|
22
|
+
return clamp(mix(baseF0, film, safeStrength), vec3<f32>(0.0), vec3<f32>(1.0));
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#define_import_path forgeax_pbr::sheen
|
|
2
|
+
|
|
3
|
+
// Charlie-like grazing lobe approximation with explicit energy compensation.
|
|
4
|
+
// It is evaluated below clearcoat, so the caller attenuates this result when
|
|
5
|
+
// the top coat Fresnel is applied.
|
|
6
|
+
fn evaluateSheenLayer(
|
|
7
|
+
baseRadiance : vec3<f32>,
|
|
8
|
+
sheenColor : vec3<f32>,
|
|
9
|
+
sheenRoughness : f32,
|
|
10
|
+
viewCosine : f32,
|
|
11
|
+
) -> vec3<f32> {
|
|
12
|
+
let roughness = clamp(sheenRoughness, 0.04, 1.0);
|
|
13
|
+
let grazing = pow(1.0 - clamp(viewCosine, 0.0, 1.0), 2.0);
|
|
14
|
+
let lobe = grazing * (1.0 - 0.5 * roughness);
|
|
15
|
+
let energy = clamp(max(max(sheenColor.r, sheenColor.g), sheenColor.b), 0.0, 1.0);
|
|
16
|
+
return baseRadiance * (1.0 - energy * lobe * 0.5) + sheenColor * lobe;
|
|
17
|
+
}
|