@forgeax/engine-shader 0.1.20 → 0.1.23
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/ShaderRegistry.d.ts.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +33070 -8
- package/dist/index.mjs.map +1 -1
- package/dist/ltc/provenance.d.ts +17 -0
- package/dist/ltc/provenance.d.ts.map +1 -0
- package/dist/ltc/tables.d.ts +22 -0
- package/dist/ltc/tables.d.ts.map +1 -0
- package/dist/material/artifact-types.d.ts +52 -0
- package/dist/material/artifact-types.d.ts.map +1 -1
- package/dist/material-schemas.d.ts +5 -0
- package/dist/material-schemas.d.ts.map +1 -1
- package/dist/register-default-sprite-lit.d.ts +3 -6
- package/dist/register-default-sprite-lit.d.ts.map +1 -1
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.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 +138 -30
- package/src/__tests__/direct-light-layout.unit.test.ts +40 -0
- package/src/__tests__/ibl-irradiance.unit.test.ts +7 -4
- package/src/__tests__/ibl-sampling.unit.test.ts +3 -4
- package/src/__tests__/lighting-point-spot-volume.unit.test.ts +16 -2
- package/src/__tests__/lighting-punctual.unit.test.ts +61 -8
- package/src/__tests__/lighting-spot-volume-attenuation.unit.test.ts +1 -1
- package/src/__tests__/ltc-provenance.unit.test.ts +33 -0
- package/src/__tests__/manifest.fixture.json +33 -0
- package/src/__tests__/material-contract.unit.test.ts +61 -1
- package/src/__tests__/material-derived-builtins.integration.test.ts +27 -0
- package/src/__tests__/probe-lighting-composition.unit.test.ts +38 -0
- package/src/__tests__/rect-area-brdf.unit.test.ts +36 -0
- package/src/__tests__/reflection-probe-sampling.unit.test.ts +45 -0
- package/src/__tests__/scene-temporal.unit.test.ts +20 -0
- package/src/__tests__/shader.unit.test.ts +40 -15
- package/src/__tests__/shadow-pcss.unit.test.ts +131 -0
- package/src/__tests__/spot-cookie-composition.unit.test.ts +57 -0
- package/src/__tests__/spot-modifier-composition.unit.test.ts +19 -0
- package/src/__tests__/sprite-lit-shader.test.ts +24 -11
- package/src/__tests__/sprite-variants.unit.test.ts +5 -1
- package/src/__tests__/transmission-thickness-scale.unit.test.ts +7 -1
- package/src/__tests__/transparent-pbr.unit.test.ts +7 -0
- package/src/brdf.wgsl +115 -1
- package/src/common.wgsl +95 -91
- package/src/default-standard-pbr-skin.wgsl +108 -92
- package/src/default-standard-pbr.wgsl +189 -191
- package/src/fxaa.wgsl +27 -6
- package/src/ibl-irradiance.wgsl +4 -1
- package/src/ibl-sampling.wgsl +56 -2
- package/src/index.ts +23 -0
- package/src/lighting-directional.wgsl +161 -132
- package/src/lighting-probe.wgsl +51 -0
- package/src/lighting-punctual.wgsl +69 -29
- package/src/lighting-rect-area.wgsl +162 -0
- package/src/lighting-spot-modifiers.wgsl +93 -0
- package/src/ltc/provenance.ts +19 -0
- package/src/ltc/tables.ts +2831 -0
- package/src/material/artifact-types.ts +137 -0
- package/src/material-schemas.ts +12 -0
- package/src/register-default-sprite-lit.ts +3 -6
- package/src/scene-temporal.wgsl +14 -8
- package/src/shadow-pcf.wgsl +44 -1
- package/src/sprite-lit.wgsl +37 -53
- package/src/sprite.wgsl +1 -1
- package/src/standard-cluster.wgsl +274 -0
- package/src/tonemap.wgsl +8 -3
- package/src/types.ts +2 -1
- package/src/volume/volume-inject.wgsl +48 -38
- package/src/volume/volume-integrate.wgsl +47 -42
- package/dist/.tsbuildinfo +0 -1
- package/src/hdrp-cluster-forward.wgsl +0 -224
|
@@ -1,14 +1,151 @@
|
|
|
1
1
|
import type { BindGroupLayoutDescriptor } from '@forgeax/engine-types';
|
|
2
2
|
|
|
3
|
+
export interface MaterialShaderResourceSlot {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly parameter: string;
|
|
6
|
+
readonly kind: 'sampler' | 'texture';
|
|
7
|
+
readonly group: number;
|
|
8
|
+
readonly binding: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface MaterialShaderVertexInput {
|
|
12
|
+
readonly semantic: string;
|
|
13
|
+
readonly location: number;
|
|
14
|
+
readonly format: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface MaterialShaderArtifactReceipt {
|
|
18
|
+
readonly directEntry: string;
|
|
19
|
+
readonly sceneIndexEntry: string;
|
|
20
|
+
readonly materialRow: {
|
|
21
|
+
readonly byteLength: number;
|
|
22
|
+
readonly fields: readonly string[];
|
|
23
|
+
};
|
|
24
|
+
readonly resourceSlots: readonly MaterialShaderResourceSlot[];
|
|
25
|
+
readonly uvSets: readonly { readonly parameter: string; readonly set: number }[];
|
|
26
|
+
readonly vertexInputs: readonly MaterialShaderVertexInput[];
|
|
27
|
+
readonly alphaMask: { readonly cutoff: string; readonly source: string };
|
|
28
|
+
readonly skinPaletteAddress?: {
|
|
29
|
+
readonly group: number;
|
|
30
|
+
readonly binding: number;
|
|
31
|
+
readonly stride: number;
|
|
32
|
+
};
|
|
33
|
+
readonly reflection: {
|
|
34
|
+
readonly layoutIdentity: string;
|
|
35
|
+
readonly resourceSlots: readonly MaterialShaderResourceSlot[];
|
|
36
|
+
readonly vertexInputs: readonly MaterialShaderVertexInput[];
|
|
37
|
+
};
|
|
38
|
+
readonly receiptIdentity: string;
|
|
39
|
+
readonly generation: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
3
42
|
export interface MaterialShaderArtifact {
|
|
4
43
|
readonly material: string;
|
|
5
44
|
readonly pass: string;
|
|
6
45
|
readonly wgsl: string;
|
|
7
46
|
readonly layoutIdentity: string;
|
|
47
|
+
/** Naga-reflected UV set count used to derive clamp-to-last vertex aliases. */
|
|
48
|
+
readonly uvSetCount?: number;
|
|
8
49
|
readonly bindings: readonly BindGroupLayoutDescriptor[];
|
|
9
50
|
readonly deps: readonly string[];
|
|
10
51
|
readonly vertexInputs: readonly Readonly<Record<string, unknown>>[];
|
|
11
52
|
readonly specializationKey?: string;
|
|
53
|
+
/** Producer-owned ABI facts shared by direct and scene-index consumers. */
|
|
54
|
+
readonly receipt?: MaterialShaderArtifactReceipt;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const STANDARD_PBR_TEXTURES = [
|
|
58
|
+
'baseColorTexture',
|
|
59
|
+
'metallicRoughnessTexture',
|
|
60
|
+
'normalTexture',
|
|
61
|
+
'specularTintTexture',
|
|
62
|
+
'emissiveTexture',
|
|
63
|
+
'occlusionTexture',
|
|
64
|
+
'transmissionTexture',
|
|
65
|
+
'thicknessTexture',
|
|
66
|
+
] as const;
|
|
67
|
+
|
|
68
|
+
const STANDARD_PBR_NUMERIC_FIELDS = [
|
|
69
|
+
'baseColor',
|
|
70
|
+
'metallic',
|
|
71
|
+
'roughness',
|
|
72
|
+
'metallicChannel',
|
|
73
|
+
'roughnessChannel',
|
|
74
|
+
'aoChannel',
|
|
75
|
+
'extraChannel',
|
|
76
|
+
'emissive',
|
|
77
|
+
'emissiveIntensity',
|
|
78
|
+
'occlusionStrength',
|
|
79
|
+
'alphaCutoff',
|
|
80
|
+
'clearcoat',
|
|
81
|
+
'clearcoatRoughness',
|
|
82
|
+
'specularTint',
|
|
83
|
+
'normalScale',
|
|
84
|
+
'transmission',
|
|
85
|
+
'ior',
|
|
86
|
+
'thickness',
|
|
87
|
+
'attenuationColor',
|
|
88
|
+
'attenuationDistance',
|
|
89
|
+
] as const;
|
|
90
|
+
|
|
91
|
+
const STANDARD_PBR_VERTEX_INPUTS: readonly MaterialShaderVertexInput[] = [
|
|
92
|
+
{ semantic: 'position', location: 0, format: 'float32x3' },
|
|
93
|
+
{ semantic: 'normal', location: 1, format: 'float32x3' },
|
|
94
|
+
{ semantic: 'uv', location: 2, format: 'float32x2' },
|
|
95
|
+
{ semantic: 'tangent', location: 3, format: 'float32x4' },
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
function standardPbrResourceSlots(): MaterialShaderResourceSlot[] {
|
|
99
|
+
return STANDARD_PBR_TEXTURES.flatMap((parameter, index) => [
|
|
100
|
+
{
|
|
101
|
+
name: `${parameter}_sampler`,
|
|
102
|
+
parameter,
|
|
103
|
+
kind: 'sampler' as const,
|
|
104
|
+
group: 1,
|
|
105
|
+
binding: index * 2 + 1,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: parameter,
|
|
109
|
+
parameter,
|
|
110
|
+
kind: 'texture' as const,
|
|
111
|
+
group: 1,
|
|
112
|
+
binding: index * 2 + 2,
|
|
113
|
+
},
|
|
114
|
+
]);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Builds the one Standard PBR ABI receipt consumed by both material lanes.
|
|
119
|
+
* The skinned module uses the same material producer and only adds its palette
|
|
120
|
+
* address; it must retain the same receipt identity for material compatibility.
|
|
121
|
+
*/
|
|
122
|
+
export function createStandardPbrArtifactReceipt(skinned = false): MaterialShaderArtifactReceipt {
|
|
123
|
+
const resourceSlots = standardPbrResourceSlots();
|
|
124
|
+
const uvSets = STANDARD_PBR_TEXTURES.map((parameter) => ({ parameter, set: 0 }));
|
|
125
|
+
const vertexInputs = skinned
|
|
126
|
+
? [
|
|
127
|
+
...STANDARD_PBR_VERTEX_INPUTS,
|
|
128
|
+
{ semantic: 'skinIndex', location: 4, format: 'uint16x4' },
|
|
129
|
+
{ semantic: 'skinWeight', location: 5, format: 'float32x4' },
|
|
130
|
+
]
|
|
131
|
+
: [...STANDARD_PBR_VERTEX_INPUTS];
|
|
132
|
+
return {
|
|
133
|
+
directEntry: 'vs_main',
|
|
134
|
+
sceneIndexEntry: 'vs_scene_index',
|
|
135
|
+
materialRow: { byteLength: 384, fields: [...STANDARD_PBR_NUMERIC_FIELDS] },
|
|
136
|
+
resourceSlots,
|
|
137
|
+
uvSets,
|
|
138
|
+
vertexInputs,
|
|
139
|
+
alphaMask: { cutoff: 'alphaCutoff', source: 'baseColor.a' },
|
|
140
|
+
...(skinned ? { skinPaletteAddress: { group: 2, binding: 1, stride: 64 } } : {}),
|
|
141
|
+
reflection: {
|
|
142
|
+
layoutIdentity: 'standard-pbr/material-row-v1',
|
|
143
|
+
resourceSlots,
|
|
144
|
+
vertexInputs,
|
|
145
|
+
},
|
|
146
|
+
receiptIdentity: 'standard-pbr/material-row-v1',
|
|
147
|
+
generation: 1,
|
|
148
|
+
};
|
|
12
149
|
}
|
|
13
150
|
|
|
14
151
|
export function isMaterialShaderArtifact(value: unknown): value is MaterialShaderArtifact {
|
package/src/material-schemas.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { ParamSchemaEntry } from '@forgeax/engine-types';
|
|
2
|
+
import {
|
|
3
|
+
createStandardPbrArtifactReceipt,
|
|
4
|
+
type MaterialShaderArtifactReceipt,
|
|
5
|
+
} from './material/artifact-types.js';
|
|
2
6
|
|
|
3
7
|
export const STANDARD_PBR_ALPHA_CUTOFF_DEFAULT = 0;
|
|
4
8
|
|
|
@@ -34,6 +38,14 @@ export const DEFAULT_STANDARD_PBR_PARAM_SCHEMA: readonly ParamSchemaEntry[] = [
|
|
|
34
38
|
{ name: 'thicknessTexture', type: 'texture2d' },
|
|
35
39
|
];
|
|
36
40
|
|
|
41
|
+
/** The single producer receipt shared by direct and scene-index Standard PBR. */
|
|
42
|
+
export const STANDARD_PBR_ARTIFACT_RECEIPT: MaterialShaderArtifactReceipt =
|
|
43
|
+
createStandardPbrArtifactReceipt();
|
|
44
|
+
|
|
45
|
+
/** Skinned Standard PBR keeps the material receipt identity and adds palette ABI. */
|
|
46
|
+
export const STANDARD_PBR_SKIN_ARTIFACT_RECEIPT: MaterialShaderArtifactReceipt =
|
|
47
|
+
createStandardPbrArtifactReceipt(true);
|
|
48
|
+
|
|
37
49
|
export const DEFAULT_UNLIT_PARAM_SCHEMA: readonly ParamSchemaEntry[] = [
|
|
38
50
|
{ name: 'baseColor', type: 'color', default: [1, 1, 1, 1] },
|
|
39
51
|
{ name: 'alphaCutoff', type: 'f32', default: 0 },
|
|
@@ -56,12 +56,9 @@ export interface SpriteLitCaps {
|
|
|
56
56
|
* compiled by @forgeax/engine-vite-plugin-shader at build time and surfaced
|
|
57
57
|
* in manifest.json. The caller reads it from the manifest entries.
|
|
58
58
|
*
|
|
59
|
-
* `caps.storageBuffer`
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* The BGL itself is built downstream in runtime/pbr-pipeline.ts (sprite-lit
|
|
63
|
-
* reuses pbr-view / pbr-mesh-array / pbr-instances unchanged; AC-07
|
|
64
|
-
* BGL byte-identical).
|
|
59
|
+
* `caps.storageBuffer` is retained for the shared mesh/instance layout
|
|
60
|
+
* selection. Sprite-lit local lighting reads the same Standard Cluster group
|
|
61
|
+
* as PBR; the view group has no direct point/spot arrays.
|
|
65
62
|
*
|
|
66
63
|
* @param registry ShaderRegistry instance (engine boot path).
|
|
67
64
|
* @param composedWgsl Post-naga_oil composed WGSL source string.
|
package/src/scene-temporal.wgsl
CHANGED
|
@@ -27,17 +27,23 @@ fn sceneTemporalUv(clip : vec4<f32>) -> vec2<f32> {
|
|
|
27
27
|
return vec2<f32>(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
// Signed view-space Z shared by temporal reconstruction and Standard Cluster
|
|
31
|
+
// lookup. Both CPU and WGSL binners consume the negative camera-space depth:
|
|
32
|
+
// perspective uses -clip.w, while orthographic depth is reconstructed from
|
|
33
|
+
// the projection's near/far payload rather than Euclidean world distance.
|
|
34
|
+
fn sceneViewZ(clip : vec4<f32>, temporalProjection : vec4<f32>) -> f32 {
|
|
32
35
|
let ndcDepth = clip.z / max(abs(clip.w), 1e-6);
|
|
33
|
-
let
|
|
34
|
-
ndcDepth * (temporalProjection.y - temporalProjection.x);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
let orthographicViewZ = -(temporalProjection.x +
|
|
37
|
+
ndcDepth * (temporalProjection.y - temporalProjection.x));
|
|
38
|
+
return select(
|
|
39
|
+
-clip.w,
|
|
40
|
+
orthographicViewZ,
|
|
38
41
|
temporalProjection.z >= 0.5,
|
|
39
42
|
);
|
|
40
|
-
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fn sceneTemporalViewDepth(clip : vec4<f32>, temporalProjection : vec4<f32>) -> f32 {
|
|
46
|
+
return log2(1.0 + max(-sceneViewZ(clip, temporalProjection), 0.0));
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
fn packSceneTemporalV1(
|
package/src/shadow-pcf.wgsl
CHANGED
|
@@ -20,6 +20,49 @@
|
|
|
20
20
|
//
|
|
21
21
|
// Return: 1.0 = fully lit, 0.0 = fully shadowed.
|
|
22
22
|
|
|
23
|
+
// === Directional PCSS sampling primitives ====================================
|
|
24
|
+
//
|
|
25
|
+
// These functions deliberately accept tile bounds from their caller. They own
|
|
26
|
+
// only integer addressing, raw depth, comparison sampling, and the shared
|
|
27
|
+
// receiver-bias equation; cascade selection, profile selection, blocker
|
|
28
|
+
// averaging, penumbra sizing, and cascade blending remain Directional policy.
|
|
29
|
+
|
|
30
|
+
fn shadow_clamp_texel_to_tile(
|
|
31
|
+
texel : vec2<i32>,
|
|
32
|
+
tileOrigin : vec2<i32>,
|
|
33
|
+
tileSize : vec2<i32>,
|
|
34
|
+
inset : i32,
|
|
35
|
+
) -> vec2<i32> {
|
|
36
|
+
let tileMin = tileOrigin + vec2<i32>(inset);
|
|
37
|
+
let tileMax = tileOrigin + max(vec2<i32>(inset), tileSize - vec2<i32>(1 + inset));
|
|
38
|
+
return min(tileMax, max(tileMin, texel));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fn shadow_load_raw_depth(
|
|
42
|
+
shadowMap : texture_depth_2d,
|
|
43
|
+
texel : vec2<i32>,
|
|
44
|
+
) -> f32 {
|
|
45
|
+
return textureLoad(shadowMap, texel, 0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fn shadow_sample_compare(
|
|
49
|
+
shadowMap : texture_depth_2d,
|
|
50
|
+
shadowSampler: sampler_comparison,
|
|
51
|
+
uv : vec2<f32>,
|
|
52
|
+
depthRef : f32,
|
|
53
|
+
) -> f32 {
|
|
54
|
+
return textureSampleCompareLevel(shadowMap, shadowSampler, uv, depthRef);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn shadow_biased_receiver_depth(
|
|
58
|
+
receiverDepth : f32,
|
|
59
|
+
normalBias : f32,
|
|
60
|
+
depthBias : f32,
|
|
61
|
+
nDotL : f32,
|
|
62
|
+
) -> f32 {
|
|
63
|
+
return receiverDepth - max(normalBias * (1.0 - nDotL), depthBias);
|
|
64
|
+
}
|
|
65
|
+
|
|
23
66
|
// 3x3 integer offset table for PCF kernel (9 taps).
|
|
24
67
|
// Extracted from lighting-directional.wgsl inline loop (T-M2-4);
|
|
25
68
|
// `sample_shadow_2d` walks all 9 taps; `sample_shadow_cube_hw2x2` does NOT
|
|
@@ -99,7 +142,7 @@ fn sample_shadow_2d_kernel(
|
|
|
99
142
|
// `shadowAtlas` is @group(0) @binding(5) texture_depth_cube_array.
|
|
100
143
|
// `shadowSampler` is @group(0) @binding(4) sampler_comparison (shared with
|
|
101
144
|
// directional shadows — same sampler type, no dimension distinction).
|
|
102
|
-
// `lightLocal` is the
|
|
145
|
+
// `lightLocal` is the light-to-fragment direction vector in the cubemap's
|
|
103
146
|
// coordinate system (Bevy convention: left-handed cubemap; caller applies
|
|
104
147
|
// flip_z = vec3(1,1,-1) to convert right-hand world to left-hand cubemap).
|
|
105
148
|
// `layer` is the cube_array layer index (i32; 0..3 for 4 shadow-casting
|
package/src/sprite-lit.wgsl
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
#pragma variant_axis STORAGE_BUFFER_AVAILABLE
|
|
2
|
+
#pragma variant_axis CLUSTER_FORWARD_AVAILABLE
|
|
2
3
|
#define_import_path forgeax_material::sprite-lit
|
|
3
4
|
|
|
4
|
-
#import forgeax_view::common::{View, Mesh, InstanceData,
|
|
5
|
-
#import forgeax_scene_temporal::{packSceneTemporalV1}
|
|
5
|
+
#import forgeax_view::common::{View, Mesh, InstanceData, view, meshes, instances, sampleMaterialTexture, packSceneTemporal}
|
|
6
|
+
#import forgeax_scene_temporal::{packSceneTemporalV1, sceneViewZ}
|
|
7
|
+
#ifdef CLUSTER_FORWARD_AVAILABLE
|
|
8
|
+
#import forgeax_standard::cluster::{evaluateStandardClusterLights}
|
|
9
|
+
#endif
|
|
6
10
|
|
|
7
11
|
// @forgeax/engine-shader - sprite-lit.wgsl
|
|
8
12
|
// (tweak-20260701-sprite-lit-flat-default-drop-ndotl-for-2d).
|
|
@@ -26,7 +30,8 @@
|
|
|
26
30
|
// - no shadow receiver / caster
|
|
27
31
|
// - no Godot-style light_mode blends
|
|
28
32
|
// - no IBL ambient
|
|
29
|
-
// -
|
|
33
|
+
// - Standard clustered punctual lights are enabled by the
|
|
34
|
+
// CLUSTER_FORWARD_AVAILABLE variant.
|
|
30
35
|
//
|
|
31
36
|
// >>> AI user error self-recovery hint:
|
|
32
37
|
// sprite-lit needs at least one light in scene; for unlit sprites use
|
|
@@ -34,12 +39,11 @@
|
|
|
34
39
|
// 1 string change in MaterialAsset.passes[0].shader).
|
|
35
40
|
//
|
|
36
41
|
// Bindings (byte-identical to sprite.wgsl so the 4 BindGroupLayout chain is
|
|
37
|
-
// reused without a per-pipeline BGL; the
|
|
42
|
+
// reused without a per-pipeline BGL; the PBR-unused entries 3..6 bind
|
|
38
43
|
// pipelineState.defaultSampler + .defaultWhiteTextureView on the host side):
|
|
39
44
|
//
|
|
40
45
|
// @group(0) @binding(0) view uniform
|
|
41
|
-
// @group(
|
|
42
|
-
// @group(0) @binding(2) spotLightsBuffer storage / uniform (variant)
|
|
46
|
+
// @group(2) @binding(3..6) Standard Cluster storage (selected variant)
|
|
43
47
|
// @group(1) @binding(0) material uniform (sprite layout
|
|
44
48
|
// colorTint /
|
|
45
49
|
// region /
|
|
@@ -80,16 +84,17 @@ struct VsIn {
|
|
|
80
84
|
@location(3) tangent : vec4<f32>,
|
|
81
85
|
};
|
|
82
86
|
|
|
83
|
-
// VsOut carries the atlas UV plus the per-fragment world-space position
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
// multi-instance draws and could not handle the 9-slice quad layout.
|
|
87
|
+
// VsOut carries the atlas UV plus the per-fragment world-space position and
|
|
88
|
+
// the Standard cluster coordinates produced by the vertex stage. Fragment
|
|
89
|
+
// `@builtin(position)` is framebuffer-space after rasterization, so it is not
|
|
90
|
+
// a valid source for NDC cluster lookup; keep the perspective divide here and
|
|
91
|
+
// interpolate the result alongside worldPos.
|
|
89
92
|
struct VsOut {
|
|
90
93
|
@builtin(position) clip : vec4<f32>,
|
|
91
94
|
@location(0) uv_atlas : vec2<f32>,
|
|
92
95
|
@location(1) worldPos : vec3<f32>,
|
|
96
|
+
@location(2) ndc : vec3<f32>,
|
|
97
|
+
@location(3) viewZ : f32,
|
|
93
98
|
};
|
|
94
99
|
|
|
95
100
|
struct SpriteVertex {
|
|
@@ -155,6 +160,11 @@ fn vs_main(in : VsIn, @builtin(instance_index) idx : u32, @builtin(vertex_index)
|
|
|
155
160
|
out.clip = view.worldViewProj * world;
|
|
156
161
|
out.uv_atlas = vertex.uvAtlas;
|
|
157
162
|
out.worldPos = world.xyz;
|
|
163
|
+
let clipPos = out.clip;
|
|
164
|
+
out.ndc = clipPos.xyz / clipPos.w;
|
|
165
|
+
// Keep the cluster depth exactly aligned with the CPU binner for both
|
|
166
|
+
// perspective and off-axis orthographic projections.
|
|
167
|
+
out.viewZ = sceneViewZ(clipPos, view.temporalProjection);
|
|
158
168
|
return out;
|
|
159
169
|
}
|
|
160
170
|
|
|
@@ -174,50 +184,24 @@ fn spriteLitDirectional(albedo : vec3<f32>) -> vec3<f32> {
|
|
|
174
184
|
return albedo * view.lightColor;
|
|
175
185
|
}
|
|
176
186
|
|
|
177
|
-
// Flat point-light contribution. Applies the KHR-lights-punctual smooth-
|
|
178
|
-
// window range attenuation only. `attenuation` is 0 outside the light's
|
|
179
|
-
// range (`invRangeSquared`) and 1 at the light center; the reciprocal-square
|
|
180
|
-
// term gives the physical falloff. URP cap = 4 point lights per pass.
|
|
181
|
-
fn spriteLitPoint(p : PointLight, worldPos : vec3<f32>, albedo : vec3<f32>) -> vec3<f32> {
|
|
182
|
-
let toLight = p.position - worldPos;
|
|
183
|
-
let dSquared = max(dot(toLight, toLight), 1e-4);
|
|
184
|
-
let factor = 1.0 - (dSquared * p.invRangeSquared) * (dSquared * p.invRangeSquared);
|
|
185
|
-
let attenuation = max(min(factor, 1.0), 0.0) / dSquared;
|
|
186
|
-
return albedo * p.colorTimesIntensity * attenuation;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Flat spot-light contribution. Same range attenuation as spriteLitPoint,
|
|
190
|
-
// modulated by a smoothstep cone factor (host-side degree -> cosine
|
|
191
|
-
// conversion). Spot direction only shapes the cone; it does not project
|
|
192
|
-
// onto a surface normal. URP cap = 4 spot lights per pass.
|
|
193
|
-
fn spriteLitSpot(s : SpotLight, worldPos : vec3<f32>, albedo : vec3<f32>) -> vec3<f32> {
|
|
194
|
-
let toLight = s.position - worldPos;
|
|
195
|
-
let dSquared = max(dot(toLight, toLight), 1e-4);
|
|
196
|
-
let l = toLight / sqrt(dSquared);
|
|
197
|
-
let factor = 1.0 - (dSquared * s.invRangeSquared) * (dSquared * s.invRangeSquared);
|
|
198
|
-
let attenuation = max(min(factor, 1.0), 0.0) / dSquared;
|
|
199
|
-
let cone = smoothstep(s.cosOuter, s.cosInner, dot(l, -s.direction));
|
|
200
|
-
return albedo * s.colorTimesIntensity * attenuation * cone;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
187
|
// Shared shading body used by both fs_main (LDR) and fs_main_hdr (HDR).
|
|
204
188
|
// The two entry points differ only in their tail-end clamp + srgb encode
|
|
205
189
|
// step (LDR clamp / HDR pass-through boundary).
|
|
206
|
-
fn spriteLitShadeAccum(
|
|
190
|
+
fn spriteLitShadeAccum(
|
|
191
|
+
albedo : vec3<f32>,
|
|
192
|
+
worldPos : vec3<f32>,
|
|
193
|
+
ndc : vec3<f32>,
|
|
194
|
+
viewZ : f32,
|
|
195
|
+
) -> vec3<f32> {
|
|
207
196
|
// Directional contribution (1 light, View UBO).
|
|
208
197
|
var lit = spriteLitDirectional(albedo);
|
|
209
|
-
|
|
210
|
-
let
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
let spotCount = spotLightsBuffer.count;
|
|
217
|
-
for (var i : u32 = 0u; i < spotCount; i = i + 1u) {
|
|
218
|
-
let s = spotLightsBuffer.slots[i];
|
|
219
|
-
lit = lit + spriteLitSpot(s, worldPos, albedo);
|
|
220
|
-
}
|
|
198
|
+
#ifdef CLUSTER_FORWARD_AVAILABLE
|
|
199
|
+
let viewDir = normalize(view.cameraPos - worldPos);
|
|
200
|
+
lit = lit + evaluateStandardClusterLights(
|
|
201
|
+
ndc, viewZ, worldPos, vec3<f32>(0.0, 0.0, 1.0),
|
|
202
|
+
viewDir, albedo, 0.0, 1.0, vec3<f32>(0.04), true,
|
|
203
|
+
);
|
|
204
|
+
#endif
|
|
221
205
|
return lit;
|
|
222
206
|
}
|
|
223
207
|
|
|
@@ -225,7 +209,7 @@ fn spriteLitShadeAccum(albedo : vec3<f32>, worldPos : vec3<f32>) -> vec3<f32> {
|
|
|
225
209
|
fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
|
226
210
|
let texel = sampleMaterialTexture(baseColorTexture, baseColorSampler, in.uv_atlas, material.baseColorTextureCoordinatesMetadata.zw);
|
|
227
211
|
let albedo4 = texel * material.colorTint;
|
|
228
|
-
let lit = spriteLitShadeAccum(albedo4.rgb, in.worldPos);
|
|
212
|
+
let lit = spriteLitShadeAccum(albedo4.rgb, in.worldPos, in.ndc, in.viewZ);
|
|
229
213
|
// Strict clamp 0..1 before premultiplied alpha multiply keeps the LDR
|
|
230
214
|
// path bounded even when a scene stacks many lights.
|
|
231
215
|
let lit_rgba = clamp(vec4<f32>(lit, albedo4.a), vec4<f32>(0.0), vec4<f32>(1.0));
|
|
@@ -243,7 +227,7 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
|
|
243
227
|
fn fs_main_hdr(in : VsOut) -> @location(0) vec4<f32> {
|
|
244
228
|
let texel = sampleMaterialTexture(baseColorTexture, baseColorSampler, in.uv_atlas, material.baseColorTextureCoordinatesMetadata.zw);
|
|
245
229
|
let albedo4 = texel * material.colorTint;
|
|
246
|
-
let lit = spriteLitShadeAccum(albedo4.rgb, in.worldPos);
|
|
230
|
+
let lit = spriteLitShadeAccum(albedo4.rgb, in.worldPos, in.ndc, in.viewZ);
|
|
247
231
|
// HDR variant: do NOT clamp the lit output to [0, 1]; let the tonemap
|
|
248
232
|
// pass absorb HDR values > 1. Alpha stays clamped because the premult
|
|
249
233
|
// math requires alpha in [0, 1].
|
package/src/sprite.wgsl
CHANGED
|
@@ -152,7 +152,7 @@ fn resolveSpriteVertex(in : VsIn, idx : u32, vertex_index : u32) -> SpriteVertex
|
|
|
152
152
|
// pivotAndSize.zw scaled the local quad AND the world matrix also scaled
|
|
153
153
|
// it, producing the scale^2 visual size debt that research F-4 flagged.
|
|
154
154
|
// Post-w11 there is one and only one scale source: the entity's
|
|
155
|
-
//
|
|
155
|
+
// GlobalTransform.world. The mesh's built-in vertex.pos is NOT consumed (sprite
|
|
156
156
|
// geometry is fully re-derived from uv + pivot) so HANDLE_QUAD's mesh.pos
|
|
157
157
|
// remains a placeholder driving vertex count (4) and topology only.
|
|
158
158
|
//
|