@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,307 @@
|
|
|
1
|
+
#pragma variant_axis STORAGE_BUFFER_AVAILABLE
|
|
2
|
+
#define_import_path forgeax_material::sprite-lit
|
|
3
|
+
|
|
4
|
+
#import forgeax_view::common::{View, FogViewParams, FogRay, Mesh, InstanceData, PointLight, SpotLight, view, meshes, instances, pointLightsBuffer, spotLightsBuffer, sampleMaterialTexture, packSceneTemporal}
|
|
5
|
+
#import forgeax_view::fog::{apply_fog}
|
|
6
|
+
|
|
7
|
+
// @forgeax/engine-shader - sprite-lit.wgsl
|
|
8
|
+
// (tweak-20260701-sprite-lit-flat-default-drop-ndotl-for-2d).
|
|
9
|
+
//
|
|
10
|
+
// Parallel 4th sprite shading model (alongside forgeax::sprite / unlit /
|
|
11
|
+
// pbr-standard). Lights a 2D sprite quad with the 3 punctual light kinds
|
|
12
|
+
// (DirectionalLight + PointLight + SpotLight) using flat 2D lighting:
|
|
13
|
+
// each fragment sums `albedo * lightColor * attenuation * cone` across all
|
|
14
|
+
// active lights. Sprites are treated as omnidirectional receivers so light
|
|
15
|
+
// direction and camera orientation do not affect shading, matching the
|
|
16
|
+
// Godot Light2D and Unity URP 2D Renderer mental model.
|
|
17
|
+
//
|
|
18
|
+
// SSOTs:
|
|
19
|
+
// - requirements section 2 (in-scope items)
|
|
20
|
+
// - requirements section 4 AC-01 .. AC-07
|
|
21
|
+
// - plan-strategy D-P1 (VsOut carries worldPos from vertex stage)
|
|
22
|
+
// - plan-strategy D-P2 (light functions drop normal parameter; flat SSOT)
|
|
23
|
+
//
|
|
24
|
+
// What this shader is NOT:
|
|
25
|
+
// - no normal-map sampling
|
|
26
|
+
// - no shadow receiver / caster
|
|
27
|
+
// - no Godot-style light_mode blends
|
|
28
|
+
// - no IBL ambient
|
|
29
|
+
// - no HDRP cluster-forward path
|
|
30
|
+
//
|
|
31
|
+
// >>> AI user error self-recovery hint:
|
|
32
|
+
// sprite-lit needs at least one light in scene; for unlit sprites use
|
|
33
|
+
// forgeax::sprite (the engine ships them as parallel 4th shading model id;
|
|
34
|
+
// 1 string change in MaterialAsset.passes[0].shader).
|
|
35
|
+
//
|
|
36
|
+
// Bindings (byte-identical to sprite.wgsl so the 4 BindGroupLayout chain is
|
|
37
|
+
// reused without a per-pipeline BGL; the 4 PBR-unused entries 3..6 bind
|
|
38
|
+
// pipelineState.defaultSampler + .defaultWhiteTextureView on the host side):
|
|
39
|
+
//
|
|
40
|
+
// @group(0) @binding(0) view uniform
|
|
41
|
+
// @group(0) @binding(1) pointLightsBuffer storage / uniform (variant)
|
|
42
|
+
// @group(0) @binding(2) spotLightsBuffer storage / uniform (variant)
|
|
43
|
+
// @group(1) @binding(0) material uniform (sprite layout
|
|
44
|
+
// colorTint /
|
|
45
|
+
// region /
|
|
46
|
+
// pivotAndSize /
|
|
47
|
+
// slicesAndMode)
|
|
48
|
+
// @group(1) @binding(1) baseColorSampler sampler
|
|
49
|
+
// @group(1) @binding(2) baseColorTexture texture_2d<f32>
|
|
50
|
+
// @group(2) @binding(0) meshes storage
|
|
51
|
+
// @group(3) @binding(0) instances storage (per-instance
|
|
52
|
+
// localFromInstance
|
|
53
|
+
// mat4)
|
|
54
|
+
|
|
55
|
+
struct Material {
|
|
56
|
+
// .xyz multiplies textureSample.rgb; .w multiplies textureSample.a.
|
|
57
|
+
// Mirrors sprite.wgsl Material layout byte-for-byte (sprite-lit reuses
|
|
58
|
+
// the same UBO writer + paramSchema entries).
|
|
59
|
+
colorTint : vec4<f32>,
|
|
60
|
+
region : vec4<f32>,
|
|
61
|
+
pivotAndSize : vec4<f32>,
|
|
62
|
+
slicesAndMode : vec4<f32>,
|
|
63
|
+
baseColorTextureCoordinatesTransform : vec4<f32>,
|
|
64
|
+
baseColorTextureCoordinatesMetadata : vec4<f32>,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
@group(1) @binding(0) var<uniform> material : Material;
|
|
68
|
+
@group(1) @binding(1) var baseColorSampler : sampler;
|
|
69
|
+
@group(1) @binding(2) var baseColorTexture : texture_2d<f32>;
|
|
70
|
+
// Preserve filtering reflection for the bound texture passed to the helper.
|
|
71
|
+
fn materialTextureFilteringWitness() {
|
|
72
|
+
let base = baseColorTexture;
|
|
73
|
+
let baseWitness = textureSample(base, baseColorSampler, vec2<f32>(0.0));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
struct VsIn {
|
|
77
|
+
@location(0) pos : vec3<f32>,
|
|
78
|
+
@location(1) normal : vec3<f32>,
|
|
79
|
+
@location(2) uv : vec2<f32>,
|
|
80
|
+
@location(3) tangent : vec4<f32>,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// VsOut carries the atlas UV plus the per-fragment world-space position.
|
|
84
|
+
// The world position is emitted from the vertex stage (already computed
|
|
85
|
+
// there for `out.clip`), so PointLight / SpotLight attenuation gets an
|
|
86
|
+
// exact per-fragment worldPos through interpolation. This replaces the
|
|
87
|
+
// earlier fragment-side reconstruction path, which collapsed under
|
|
88
|
+
// multi-instance draws and could not handle the 9-slice quad layout.
|
|
89
|
+
struct VsOut {
|
|
90
|
+
@builtin(position) clip : vec4<f32>,
|
|
91
|
+
@location(0) uv_atlas : vec2<f32>,
|
|
92
|
+
@location(1) worldPos : vec3<f32>,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
struct SpriteVertex {
|
|
96
|
+
posLocal : vec3<f32>,
|
|
97
|
+
uvAtlas : vec2<f32>,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
fn resolveSpriteVertex(in : VsIn, vertex_index : u32) -> SpriteVertex {
|
|
101
|
+
// Body is the slice-aware sprite vertex stage byte-for-byte ported from
|
|
102
|
+
// sprite.wgsl (feat-20260520 M-3 / w19 + feat-20260527 M3 / w15). The
|
|
103
|
+
// 9-slice early-out via slicesAndMode == 0 sentinel keeps the legacy
|
|
104
|
+
// single-quad path byte-identical so sprite-lit demos that do not enable
|
|
105
|
+
// slicing pay zero perf for the slicing branch.
|
|
106
|
+
let pivot = material.pivotAndSize.xy;
|
|
107
|
+
let size = material.pivotAndSize.zw;
|
|
108
|
+
let useSlices = any(material.slicesAndMode != vec4<f32>(0.0));
|
|
109
|
+
var pos_local : vec3<f32>;
|
|
110
|
+
var uv_atlas : vec2<f32>;
|
|
111
|
+
if (useSlices) {
|
|
112
|
+
let abs_slices = abs(material.slicesAndMode);
|
|
113
|
+
let is_tile = material.slicesAndMode.w < 0.0;
|
|
114
|
+
let i = vertex_index % 4u;
|
|
115
|
+
let j = vertex_index / 4u;
|
|
116
|
+
let u_pos_arr = array<f32, 4>(0.0, abs_slices.x, 1.0 - abs_slices.z, 1.0);
|
|
117
|
+
let v_pos_arr = array<f32, 4>(0.0, abs_slices.y, 1.0 - abs_slices.w, 1.0);
|
|
118
|
+
let u_pos = u_pos_arr[i];
|
|
119
|
+
let v_pos_top = v_pos_arr[j];
|
|
120
|
+
let v_pos_eff = 1.0 - v_pos_top;
|
|
121
|
+
pos_local = vec3<f32>((u_pos - pivot.x) * size.x, (v_pos_eff - pivot.y) * size.y, 0.0);
|
|
122
|
+
var u_uv_arr = array<f32, 4>(0.0, abs_slices.x, 1.0 - abs_slices.z, 1.0);
|
|
123
|
+
var v_uv_arr = array<f32, 4>(0.0, abs_slices.y, 1.0 - abs_slices.w, 1.0);
|
|
124
|
+
if (is_tile) {
|
|
125
|
+
let mid_u = 1.0 - abs_slices.x - abs_slices.z;
|
|
126
|
+
let mid_v = 1.0 - abs_slices.y - abs_slices.w;
|
|
127
|
+
u_uv_arr[2] = abs_slices.x + 2.0 * mid_u;
|
|
128
|
+
u_uv_arr[3] = u_uv_arr[2] + abs_slices.z;
|
|
129
|
+
v_uv_arr[2] = abs_slices.y + 2.0 * mid_v;
|
|
130
|
+
v_uv_arr[3] = v_uv_arr[2] + abs_slices.w;
|
|
131
|
+
}
|
|
132
|
+
let uv_u = u_uv_arr[i];
|
|
133
|
+
let uv_v_top = v_uv_arr[j];
|
|
134
|
+
let uv_v_eff = 1.0 - uv_v_top;
|
|
135
|
+
uv_atlas = vec2<f32>(uv_u, uv_v_eff) * material.region.zw + material.region.xy;
|
|
136
|
+
} else {
|
|
137
|
+
let uv_eff = vec2<f32>(in.uv.x, 1.0 - in.uv.y);
|
|
138
|
+
pos_local = vec3<f32>((uv_eff - pivot) * size, 0.0);
|
|
139
|
+
uv_atlas = uv_eff * material.region.zw + material.region.xy;
|
|
140
|
+
}
|
|
141
|
+
var out : SpriteVertex;
|
|
142
|
+
out.posLocal = pos_local;
|
|
143
|
+
out.uvAtlas = uv_atlas;
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@vertex
|
|
148
|
+
fn vs_main(in : VsIn, @builtin(instance_index) idx : u32, @builtin(vertex_index) vertex_index : u32) -> VsOut {
|
|
149
|
+
let vertex = resolveSpriteVertex(in, vertex_index);
|
|
150
|
+
// AC-11 instances path day-1: the world transform is meshes[0].worldFromLocal
|
|
151
|
+
// * instances[idx].localFromInstance * vec4(pos_local, 1.0) -- same chain
|
|
152
|
+
// sprite.wgsl + default-standard-pbr.wgsl use.
|
|
153
|
+
let world = meshes[0].worldFromLocal * instances[idx].localFromInstance * vec4<f32>(vertex.posLocal, 1.0);
|
|
154
|
+
var out : VsOut;
|
|
155
|
+
out.clip = view.worldViewProj * world;
|
|
156
|
+
out.uv_atlas = vertex.uvAtlas;
|
|
157
|
+
out.worldPos = world.xyz;
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// linear_to_srgb mirrors sprite.wgsl L251-254: per-channel IEC 61966-2-1
|
|
162
|
+
// transfer used by the LDR fragment entry to encode the bgra8unorm swap
|
|
163
|
+
// chain output (the format is not hardware-sRGB-encoded; the shader does it).
|
|
164
|
+
fn linear_to_srgb(linear : f32) -> f32 {
|
|
165
|
+
let c = clamp(linear, 0.0, 1.0);
|
|
166
|
+
return select(c * 12.92, pow(c, 1.0 / 2.4) * 1.055 - 0.055, c > 0.0031308);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Flat directional contribution. Every fragment receives the full
|
|
170
|
+
// `view.lightColor` modulated by `albedo`. Direction of the light is
|
|
171
|
+
// intentionally ignored -- 2D sprites lie in the XY plane facing the camera
|
|
172
|
+
// and are treated as omnidirectional receivers.
|
|
173
|
+
fn spriteLitDirectional(albedo : vec3<f32>) -> vec3<f32> {
|
|
174
|
+
return albedo * view.lightColor;
|
|
175
|
+
}
|
|
176
|
+
|
|
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
|
+
// Shared shading body used by both fs_main (LDR) and fs_main_hdr (HDR).
|
|
204
|
+
// The two entry points differ only in their tail-end clamp + srgb encode
|
|
205
|
+
// step (LDR clamp / HDR pass-through boundary).
|
|
206
|
+
fn spriteLitShadeAccum(albedo : vec3<f32>, worldPos : vec3<f32>) -> vec3<f32> {
|
|
207
|
+
// Directional contribution (1 light, View UBO).
|
|
208
|
+
var lit = spriteLitDirectional(albedo);
|
|
209
|
+
// Point-light contribution (URP cap = 4; PointLightsArray.count).
|
|
210
|
+
let pointCount = pointLightsBuffer.count;
|
|
211
|
+
for (var i : u32 = 0u; i < pointCount; i = i + 1u) {
|
|
212
|
+
let p = pointLightsBuffer.slots[i];
|
|
213
|
+
lit = lit + spriteLitPoint(p, worldPos, albedo);
|
|
214
|
+
}
|
|
215
|
+
// Spot-light contribution (URP cap = 4).
|
|
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
|
+
}
|
|
221
|
+
return lit;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
fn applySceneFog(viewParams : View, color : vec3<f32>, alpha : f32, worldPos : vec3<f32>) -> vec4<f32> {
|
|
225
|
+
var origin = viewParams.cameraPos;
|
|
226
|
+
var direction = normalize(worldPos - origin);
|
|
227
|
+
var rayDistance = length(worldPos - origin);
|
|
228
|
+
if (viewParams.temporalProjection.z >= 0.5) {
|
|
229
|
+
let nearH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 0.0, 1.0);
|
|
230
|
+
let farH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 1.0, 1.0);
|
|
231
|
+
let nearPoint = nearH.xyz / nearH.w;
|
|
232
|
+
let farPoint = farH.xyz / farH.w;
|
|
233
|
+
direction = normalize(farPoint - nearPoint);
|
|
234
|
+
origin = worldPos - direction * dot(worldPos - viewParams.cameraPos, direction);
|
|
235
|
+
rayDistance = max(dot(worldPos - origin, direction), 0.0);
|
|
236
|
+
}
|
|
237
|
+
return apply_fog(viewParams.fog, FogRay(origin, direction, rayDistance), vec4<f32>(color, alpha));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
@fragment
|
|
241
|
+
fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
|
242
|
+
let texel = sampleMaterialTexture(baseColorTexture, baseColorSampler, in.uv_atlas, material.baseColorTextureCoordinatesMetadata.zw);
|
|
243
|
+
let albedo4 = texel * material.colorTint;
|
|
244
|
+
let lit = spriteLitShadeAccum(albedo4.rgb, in.worldPos);
|
|
245
|
+
// Strict clamp 0..1 before premultiplied alpha multiply keeps the LDR
|
|
246
|
+
// path bounded even when a scene stacks many lights.
|
|
247
|
+
let lit_rgba = clamp(vec4<f32>(lit, albedo4.a), vec4<f32>(0.0), vec4<f32>(1.0));
|
|
248
|
+
let fogged = applySceneFog(view, lit_rgba.rgb, lit_rgba.a, in.worldPos);
|
|
249
|
+
let premult = vec4<f32>(fogged.rgb * fogged.a, fogged.a);
|
|
250
|
+
// LDR target is bgra8unorm; encode rgb via the sRGB transfer in-shader.
|
|
251
|
+
return vec4<f32>(
|
|
252
|
+
linear_to_srgb(premult.r),
|
|
253
|
+
linear_to_srgb(premult.g),
|
|
254
|
+
linear_to_srgb(premult.b),
|
|
255
|
+
premult.a,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
@fragment
|
|
260
|
+
fn fs_main_hdr(in : VsOut) -> @location(0) vec4<f32> {
|
|
261
|
+
let texel = sampleMaterialTexture(baseColorTexture, baseColorSampler, in.uv_atlas, material.baseColorTextureCoordinatesMetadata.zw);
|
|
262
|
+
let albedo4 = texel * material.colorTint;
|
|
263
|
+
let lit = spriteLitShadeAccum(albedo4.rgb, in.worldPos);
|
|
264
|
+
// HDR variant: do NOT clamp the lit output to [0, 1]; let the tonemap
|
|
265
|
+
// pass absorb HDR values > 1. Alpha stays clamped because the premult
|
|
266
|
+
// math requires alpha in [0, 1].
|
|
267
|
+
let alpha = clamp(albedo4.a, 0.0, 1.0);
|
|
268
|
+
let fogged = applySceneFog(view, lit, alpha, in.worldPos);
|
|
269
|
+
return vec4<f32>(fogged.rgb * fogged.a, fogged.a);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
struct TemporalVsOut {
|
|
273
|
+
@builtin(position) clip : vec4<f32>,
|
|
274
|
+
@location(0) uvAtlas : vec2<f32>,
|
|
275
|
+
@location(1) @interpolate(linear) currentClip : vec4<f32>,
|
|
276
|
+
@location(2) @interpolate(linear) previousClip : vec4<f32>,
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
@vertex
|
|
280
|
+
fn vs_temporal(in : VsIn, @builtin(instance_index) idx : u32, @builtin(vertex_index) vertex_index : u32) -> TemporalVsOut {
|
|
281
|
+
let vertex = resolveSpriteVertex(in, vertex_index);
|
|
282
|
+
let currentWorld = meshes[0].worldFromLocal *
|
|
283
|
+
instances[idx].localFromInstance * vec4<f32>(vertex.posLocal, 1.0);
|
|
284
|
+
var previousWorld = currentWorld;
|
|
285
|
+
#if STORAGE_BUFFER_AVAILABLE == true
|
|
286
|
+
previousWorld = meshes[0].previousWorldFromLocal *
|
|
287
|
+
instances[idx].previousLocalFromInstance * vec4<f32>(vertex.posLocal, 1.0);
|
|
288
|
+
#endif
|
|
289
|
+
var out : TemporalVsOut;
|
|
290
|
+
out.currentClip = view.temporalCurrentViewProj * currentWorld;
|
|
291
|
+
out.clip = out.currentClip;
|
|
292
|
+
out.previousClip = view.temporalPreviousViewProj * previousWorld;
|
|
293
|
+
out.uvAtlas = vertex.uvAtlas;
|
|
294
|
+
return out;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
@fragment
|
|
298
|
+
fn fs_temporal(in : TemporalVsOut) -> @location(0) vec4<f32> {
|
|
299
|
+
let texel = sampleMaterialTexture(baseColorTexture, baseColorSampler, in.uvAtlas, material.baseColorTextureCoordinatesMetadata.zw);
|
|
300
|
+
let alpha = clamp(texel.a * material.colorTint.a, 0.0, 1.0);
|
|
301
|
+
if (alpha <= 0.0) {
|
|
302
|
+
discard;
|
|
303
|
+
}
|
|
304
|
+
return packSceneTemporal(in.currentClip, in.previousClip, 1.0);
|
|
305
|
+
}
|
|
306
|
+
// sprite-lit needs at least one light in the scene to be visible; for unlit
|
|
307
|
+
// sprites use forgeax::sprite (1 string change in MaterialAsset).
|
package/src/sprite.wgsl
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
#define_import_path forgeax_material::sprite
|
|
2
|
+
#pragma variant_axis STORAGE_BUFFER_AVAILABLE
|
|
3
|
+
#pragma variant_axis PER_INSTANCE_REGION
|
|
4
|
+
|
|
5
|
+
#import forgeax_view::common::{View, FogViewParams, FogRay, Mesh, InstanceData, view, meshes, instances, sampleMaterialTexture, packSceneTemporal}
|
|
6
|
+
#import forgeax_view::fog::{apply_fog}
|
|
7
|
+
|
|
8
|
+
// @forgeax/engine-shader - sprite.wgsl (feat-20260520-2d-sprite-layer-mvp /
|
|
9
|
+
// M-3 / w19). 2D sprite material — third variant of the MaterialAsset
|
|
10
|
+
// closed union (after 'unlit' and 'standard'). Pairs with the sprite
|
|
11
|
+
// alpha-blend pipeline (LDR + HDR variants; w24 createRenderer registration).
|
|
12
|
+
//
|
|
13
|
+
// Anchors:
|
|
14
|
+
// - requirements §3 AC-04 (vertex pos_local = (uv - pivot) * size +
|
|
15
|
+
// uv_atlas single-mad Q7 origin+size; fragment textureSample *
|
|
16
|
+
// colorTint; sprite is unlit per Q14; HDR clamp 0..1 R6 mitigation)
|
|
17
|
+
// - requirements §2.1.D shader table
|
|
18
|
+
// - requirements §6 R5 mitigation (sprite shader must NOT read the
|
|
19
|
+
// directional-light fields of the View UBO — sprite is unlit per Q14,
|
|
20
|
+
// 9/9 industry-default; charter P4 consistent abstraction)
|
|
21
|
+
// - requirements §6 R6 mitigation (HDR alpha blend boundary; sprite
|
|
22
|
+
// fragment output strictly clamped to [0, 1] before premultiplied
|
|
23
|
+
// alpha multiply)
|
|
24
|
+
// - plan-strategy §2 D-1 candidate (b) (sprite material reuses the PBR
|
|
25
|
+
// 7-entry BindGroup layout; 4 unused entries 3..6 default to
|
|
26
|
+
// `pipelineState.defaultSampler` + `pipelineState.defaultWhiteTextureView`;
|
|
27
|
+
// 0 lines new code on the BindGroup placeholders, 4 lines binding ref)
|
|
28
|
+
// - plan-strategy §2 D-9 (sprite shares the 112 B std140 View UBO with
|
|
29
|
+
// PBR; do NOT split off a sprite-specific View — charter P4 abstraction
|
|
30
|
+
// uniformity; static reads of the directional-light fields simply
|
|
31
|
+
// never happen, GPU pays zero perf for the unused fields)
|
|
32
|
+
// - research §Finding C-1 (`common.wgsl:17-22` View struct + naga_oil
|
|
33
|
+
// 0.22 Composer #import resolves View / Mesh / view / meshes at compile
|
|
34
|
+
// time; ShaderRegistry manifest path loadEngineShaderEntries adds a
|
|
35
|
+
// 4th entry in w20)
|
|
36
|
+
//
|
|
37
|
+
// Material bindings are derived from the sprite paramSchema; view, mesh and
|
|
38
|
+
// instance groups remain shared with the ordinary material pipeline:
|
|
39
|
+
//
|
|
40
|
+
// @group(0) @binding(0) view uniform (sprite reads
|
|
41
|
+
// view.worldViewProj
|
|
42
|
+
// only)
|
|
43
|
+
// @group(1) @binding(0) material uniform (sprite layout:
|
|
44
|
+
// colorTint vec4
|
|
45
|
+
// + region vec4
|
|
46
|
+
// + pivotAndSize
|
|
47
|
+
// vec4; 48 B)
|
|
48
|
+
// @group(1) @binding(1) baseColorSampler sampler
|
|
49
|
+
// @group(1) @binding(2) baseColorTexture texture_2d<f32>
|
|
50
|
+
// @group(2) @binding(0) meshes storage (sprite reads
|
|
51
|
+
// meshes[0]
|
|
52
|
+
// .worldFromLocal
|
|
53
|
+
// only — normal
|
|
54
|
+
// matrix unused)
|
|
55
|
+
// @group(3) @binding(0) instances storage (per-instance
|
|
56
|
+
// localFromInstance
|
|
57
|
+
// mat4; see
|
|
58
|
+
// common.wgsl)
|
|
59
|
+
//
|
|
60
|
+
// SSOT:
|
|
61
|
+
// sprite is unlit; do NOT read the directional-light fields of the
|
|
62
|
+
// View UBO — sprite.wgsl 1+1 line hard rule above and below; R5
|
|
63
|
+
// mitigation.
|
|
64
|
+
//
|
|
65
|
+
// @reuses forgeax_view::common View (112 B std140) — w19 imports the same
|
|
66
|
+
// View struct PBR / unlit consume so the 4-BindGroupLayout chain is
|
|
67
|
+
// pipeline-pipeline shareable; w24 sprite alpha-blend pipeline reuses
|
|
68
|
+
// pipelineState.viewBindGroupLayout / .materialBindGroupLayout /
|
|
69
|
+
// .meshBindGroupLayout / .instancesBindGroupLayout without a new BGL.
|
|
70
|
+
// @reuses MaterialBindGroup 7-entry PBR layout (4 unused entries 3..6 bind
|
|
71
|
+
// pipelineState.defaultSampler + pipelineState.defaultWhiteTextureView —
|
|
72
|
+
// plan-strategy D-1 candidate (b); 0 lines new GPU resource, 4 lines new
|
|
73
|
+
// binding ref).
|
|
74
|
+
// @new-surface sprite fragment output strictly clamped to [0, 1] before
|
|
75
|
+
// premultiplied-alpha multiply (R6 HDR alpha blend boundary mitigation;
|
|
76
|
+
// the sprite path runs through the same HDR rgba16float target as PBR
|
|
77
|
+
// when the active camera carries `tonemap !== 'none'`, so unclamped
|
|
78
|
+
// output would otherwise feed NaN / Inf into the tonemap fullscreen pass).
|
|
79
|
+
|
|
80
|
+
struct Material {
|
|
81
|
+
// .xyz multiplies textureSample.rgb; .w multiplies textureSample.a.
|
|
82
|
+
// AI users supply via SpriteMaterialAsset.colorTint (default [1,1,1,1]).
|
|
83
|
+
colorTint : vec4<f32>,
|
|
84
|
+
// Atlas region: .xy = (uMin, vMin) origin, .zw = (uW, vH) size. Single
|
|
85
|
+
// mad uv_atlas = uv * region.zw + region.xy. Host folds flipX / flipY
|
|
86
|
+
// into region (flipX -> region.x += region.z; region.z = -region.z;
|
|
87
|
+
// analog for Y) so the shader does not need an extra flip uniform.
|
|
88
|
+
region : vec4<f32>,
|
|
89
|
+
// feat-20260625-refactor-sprite-as-transparent-mesh M3 / w11 (D-6):
|
|
90
|
+
// pivotAndSize.xy = pivot (0..1 normalised; (0,0)=top-left, (1,1)=
|
|
91
|
+
// bottom-left of texture UV space). The legacy .zw=size pair is now
|
|
92
|
+
// a DEAD SLOT — the sprite quad is a local-space unit quad (1x1) and
|
|
93
|
+
// world scale flows entirely through meshes[i].worldFromLocal, no
|
|
94
|
+
// longer double-applied by both the UBO size factor and the world
|
|
95
|
+
// matrix (research F-4 "scale^2 -> scale^1" correction). The .zw
|
|
96
|
+
// half stays in the struct so the std140 layout slot 2 is byte-stable
|
|
97
|
+
// for the generic UBO writer (plan-strategy D-2 derive(uboLayout)
|
|
98
|
+
// offsets); shaders MUST NOT read material.pivotAndSize.zw post-w11.
|
|
99
|
+
pivotAndSize : vec4<f32>,
|
|
100
|
+
// 9-slice placeholder (feat-20260527-sprite-nineslice M1 / w3): .xyz =
|
|
101
|
+
// L/T/R/B inset values folded into a single vec4 by the schema-driven
|
|
102
|
+
// UBO writer (M2), .w = mode signed-encoded (positive = stretch / 0,
|
|
103
|
+
// negative = tile per plan-strategy section D-3 sliceMode-on-vec4.w
|
|
104
|
+
// sentinel). M1 is placeholder-only — vs_main / fs_main do NOT read
|
|
105
|
+
// this field; the field exists so the std140 layout has slot 3 reserved
|
|
106
|
+
// at the wgsl side, letting the M2 record-stage writer hit a byte-stable
|
|
107
|
+
// offset (plan-strategy D-7 byte-stable UBO write contract).
|
|
108
|
+
// sprite UBO grows 48 B -> 64 B; legacy sprite renders byte-identically
|
|
109
|
+
// because slicesAndMode is never sampled in the vertex / fragment paths
|
|
110
|
+
// when slices=[0,0,0,0] (zero-branch GPU path).
|
|
111
|
+
slicesAndMode : vec4<f32>,
|
|
112
|
+
baseColorTextureCoordinatesTransform : vec4<f32>,
|
|
113
|
+
baseColorTextureCoordinatesMetadata : vec4<f32>,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
@group(1) @binding(0) var<uniform> material : Material;
|
|
117
|
+
@group(1) @binding(1) var baseColorSampler : sampler;
|
|
118
|
+
@group(1) @binding(2) var baseColorTexture : texture_2d<f32>;
|
|
119
|
+
// Preserve filtering reflection for the bound texture passed to the helper.
|
|
120
|
+
fn materialTextureFilteringWitness() {
|
|
121
|
+
let base = baseColorTexture;
|
|
122
|
+
let baseWitness = textureSample(base, baseColorSampler, vec2<f32>(0.0));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
struct VsIn {
|
|
126
|
+
@location(0) pos : vec3<f32>,
|
|
127
|
+
@location(1) normal : vec3<f32>,
|
|
128
|
+
@location(2) uv : vec2<f32>,
|
|
129
|
+
@location(3) tangent : vec4<f32>,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
struct VsOut {
|
|
133
|
+
@builtin(position) clip : vec4<f32>,
|
|
134
|
+
// Atlas-space UV: already folded through region origin + size by the
|
|
135
|
+
// vertex stage so the fragment can sample the bound material texture at
|
|
136
|
+
// `in.uv_atlas` without any host-side region
|
|
137
|
+
// re-write per draw.
|
|
138
|
+
@location(0) uv_atlas : vec2<f32>,
|
|
139
|
+
@location(1) worldPos : vec3<f32>,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
struct SpriteVertex {
|
|
143
|
+
posLocal : vec3<f32>,
|
|
144
|
+
uvAtlas : vec2<f32>,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
fn resolveSpriteVertex(in : VsIn, idx : u32, vertex_index : u32) -> SpriteVertex {
|
|
148
|
+
// feat-20260625-refactor-sprite-as-transparent-mesh M3 / w11 (D-6):
|
|
149
|
+
// pos_local = (uv - pivot) * 1, NOT (uv - pivot) * size. The sprite quad
|
|
150
|
+
// is now a UNIT QUAD in local space ([-pivot, 1-pivot]) and world scale
|
|
151
|
+
// flows entirely through meshes[i].worldFromLocal. Pre-w11 the UBO's
|
|
152
|
+
// pivotAndSize.zw scaled the local quad AND the world matrix also scaled
|
|
153
|
+
// it, producing the scale^2 visual size debt that research F-4 flagged.
|
|
154
|
+
// Post-w11 there is one and only one scale source: the entity's
|
|
155
|
+
// Transform.world. The mesh's built-in vertex.pos is NOT consumed (sprite
|
|
156
|
+
// geometry is fully re-derived from uv + pivot) so HANDLE_QUAD's mesh.pos
|
|
157
|
+
// remains a placeholder driving vertex count (4) and topology only.
|
|
158
|
+
//
|
|
159
|
+
// Compensation: since bug-20260601-procedural-geometry-uv-v-axis-vs-webgpu-sampler-ori
|
|
160
|
+
// M1 flipped procedural HANDLE_QUAD from bottom-left V (1-t) to top-left
|
|
161
|
+
// V (t) to match the WebGPU sampler convention, the sprite's uv-derived
|
|
162
|
+
// geometry must be restored to the pre-M1 bottom-left convention so that
|
|
163
|
+
// position, triangle winding (cull:back, frontFace:ccw), and atlas sampling
|
|
164
|
+
// are byte-identical. We define uv_eff = (in.uv.x, 1 - in.uv.y) and consume
|
|
165
|
+
// it in BOTH pos_local and uv_atlas below.
|
|
166
|
+
let pivot = material.pivotAndSize.xy;
|
|
167
|
+
|
|
168
|
+
// feat-20260527-sprite-nineslice M3 / w15 (plan-strategy section D-3 + D-4):
|
|
169
|
+
// 9-region map. Early-out when slicesAndMode is the all-zero sentinel so
|
|
170
|
+
// the legacy single-quad path stays byte-identical for sprites without
|
|
171
|
+
// slices (zero-slice sprite users pay zero perf for the new feature). When
|
|
172
|
+
// useSlices fires, the 16-vertex HANDLE_NINESLICE_QUAD mesh is bound by
|
|
173
|
+
// the record stage (D-2); we reinterpret vertex_index as a 4x4 grid (i =
|
|
174
|
+
// column 0..3, j = row 0..3) and route each grid point through 4 anchor
|
|
175
|
+
// lanes:
|
|
176
|
+
// u_pos_arr / v_pos_arr -> position anchors (atlas-UV space, mapped to
|
|
177
|
+
// world via legacy `(u - pivot) * 1`)
|
|
178
|
+
// u_uv_arr / v_uv_arr -> atlas UV anchors (region-local, then folded
|
|
179
|
+
// through region.zw / region.xy)
|
|
180
|
+
// Stretch mode (slicesAndMode.w >= 0): u/v_uv_arr == u/v_pos_arr, the
|
|
181
|
+
// middle band UV stays in [slice, 1-slice]. Tile mode (slicesAndMode.w
|
|
182
|
+
// < 0): the middle UV anchors are pushed past 1.0 so the sampler with
|
|
183
|
+
// addressMode='repeat' (D-4 sampler.repeat path; D-9 register-time
|
|
184
|
+
// soft-warn flags missing repeat) wraps the middle band and re-emits
|
|
185
|
+
// the texture. Vertex shader does NOT call wgsl `fract` — the wrap is
|
|
186
|
+
// entirely sampler-driven (charter P3 + plan-strategy D-4 fract veto).
|
|
187
|
+
let useSlices = any(material.slicesAndMode != vec4<f32>(0.0));
|
|
188
|
+
var pos_local : vec3<f32>;
|
|
189
|
+
var uv_atlas : vec2<f32>;
|
|
190
|
+
if (useSlices) {
|
|
191
|
+
let abs_slices = abs(material.slicesAndMode);
|
|
192
|
+
let is_tile = material.slicesAndMode.w < 0.0;
|
|
193
|
+
let i = vertex_index % 4u;
|
|
194
|
+
let j = vertex_index / 4u;
|
|
195
|
+
|
|
196
|
+
// 4 anchors in atlas-UV space (top-left convention).
|
|
197
|
+
let u_pos_arr = array<f32, 4>(0.0, abs_slices.x, 1.0 - abs_slices.z, 1.0);
|
|
198
|
+
let v_pos_arr = array<f32, 4>(0.0, abs_slices.y, 1.0 - abs_slices.w, 1.0);
|
|
199
|
+
let u_pos = u_pos_arr[i];
|
|
200
|
+
let v_pos_top = v_pos_arr[j];
|
|
201
|
+
// Flip V to bottom-left convention before the legacy `(u - pivot) * 1`.
|
|
202
|
+
let v_pos_eff = 1.0 - v_pos_top;
|
|
203
|
+
pos_local = vec3<f32>(u_pos - pivot.x, v_pos_eff - pivot.y, 0.0);
|
|
204
|
+
|
|
205
|
+
// UV anchors. Stretch == position anchors. Tile pushes middle past 1.
|
|
206
|
+
var u_uv_arr = array<f32, 4>(0.0, abs_slices.x, 1.0 - abs_slices.z, 1.0);
|
|
207
|
+
var v_uv_arr = array<f32, 4>(0.0, abs_slices.y, 1.0 - abs_slices.w, 1.0);
|
|
208
|
+
if (is_tile) {
|
|
209
|
+
let mid_u = 1.0 - abs_slices.x - abs_slices.z;
|
|
210
|
+
let mid_v = 1.0 - abs_slices.y - abs_slices.w;
|
|
211
|
+
// One full retile across the middle band: anchor[2] = corner_left +
|
|
212
|
+
// 2 * mid; anchor[3] keeps the right-corner UV span attached.
|
|
213
|
+
u_uv_arr[2] = abs_slices.x + 2.0 * mid_u;
|
|
214
|
+
u_uv_arr[3] = u_uv_arr[2] + abs_slices.z;
|
|
215
|
+
v_uv_arr[2] = abs_slices.y + 2.0 * mid_v;
|
|
216
|
+
v_uv_arr[3] = v_uv_arr[2] + abs_slices.w;
|
|
217
|
+
}
|
|
218
|
+
let uv_u = u_uv_arr[i];
|
|
219
|
+
let uv_v_top = v_uv_arr[j];
|
|
220
|
+
let uv_v_eff = 1.0 - uv_v_top;
|
|
221
|
+
uv_atlas = vec2<f32>(uv_u, uv_v_eff) * material.region.zw + material.region.xy;
|
|
222
|
+
} else {
|
|
223
|
+
let uv_eff = vec2<f32>(in.uv.x, 1.0 - in.uv.y);
|
|
224
|
+
pos_local = vec3<f32>(uv_eff - pivot, 0.0);
|
|
225
|
+
// Q7 single-mad uv_atlas = uv * region.zw + region.xy. host folds
|
|
226
|
+
// flipX / flipY by negating region.zw + offsetting region.xy so this
|
|
227
|
+
// formula handles every flip case without a per-fragment branch.
|
|
228
|
+
//
|
|
229
|
+
// M2 compensation note: uv_eff restores the pre-M1 bottom-left V
|
|
230
|
+
// convention for the vertex stage. The host fold of flipY into region
|
|
231
|
+
// operates on atlas-space coordinates independently — no double-flip
|
|
232
|
+
// risk, because the region adjustment is a linear transform applied
|
|
233
|
+
// after uv_eff and neither path depends on the other's sign convention.
|
|
234
|
+
//
|
|
235
|
+
// feat-20260625-sprite-instances-and-tilemap-terrain-static-batch M2 /
|
|
236
|
+
// w7 (plan-strategy §2 D-5 + research §Q-R-4.4): when the sprite
|
|
237
|
+
// pipeline composes this module with PER_INSTANCE_REGION=true the
|
|
238
|
+
// region source is the per-instance value carried in
|
|
239
|
+
// `instances[idx].region` (host-uploaded 80 B-per-instance interleaved
|
|
240
|
+
// buffer; D-1 + D-9). When PER_INSTANCE_REGION is unset (legacy
|
|
241
|
+
// sprite-atlas / non-SpriteInstances entities) the region still comes
|
|
242
|
+
// from the material UBO. The 9-slice branch above keeps reading
|
|
243
|
+
// `material.region` regardless — 9-slice config is material-level by
|
|
244
|
+
// construction (OOS-3 + D-5) and the two region semantics do not mix.
|
|
245
|
+
var region_src = material.region;
|
|
246
|
+
#if PER_INSTANCE_REGION == true
|
|
247
|
+
region_src = instances[idx].region;
|
|
248
|
+
#endif
|
|
249
|
+
uv_atlas = uv_eff * region_src.zw + region_src.xy;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// feat-20260604-instances-per-instance-transform-shader-group3-bin M2 / w8:
|
|
253
|
+
// entity world from meshes[0] (dynamic-offset window already aimed at
|
|
254
|
+
// this entity), per-instance local from instances[idx].localFromInstance.
|
|
255
|
+
// sprite is unlit — normal matrix unused.
|
|
256
|
+
var out : SpriteVertex;
|
|
257
|
+
out.posLocal = pos_local;
|
|
258
|
+
out.uvAtlas = uv_atlas;
|
|
259
|
+
return out;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
@vertex
|
|
263
|
+
fn vs_main(in : VsIn, @builtin(instance_index) idx : u32, @builtin(vertex_index) vertex_index : u32) -> VsOut {
|
|
264
|
+
let vertex = resolveSpriteVertex(in, idx, vertex_index);
|
|
265
|
+
let world = meshes[0].worldFromLocal * instances[idx].localFromInstance * vec4<f32>(vertex.posLocal, 1.0);
|
|
266
|
+
var out : VsOut;
|
|
267
|
+
out.clip = view.worldViewProj * world;
|
|
268
|
+
out.uv_atlas = vertex.uvAtlas;
|
|
269
|
+
out.worldPos = world.xyz;
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
fn applySceneFog(viewParams : View, color : vec3<f32>, alpha : f32, worldPos : vec3<f32>) -> vec4<f32> {
|
|
274
|
+
var origin = viewParams.cameraPos;
|
|
275
|
+
var direction = normalize(worldPos - origin);
|
|
276
|
+
var rayDistance = length(worldPos - origin);
|
|
277
|
+
if (viewParams.temporalProjection.z >= 0.5) {
|
|
278
|
+
let nearH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 0.0, 1.0);
|
|
279
|
+
let farH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 1.0, 1.0);
|
|
280
|
+
let nearPoint = nearH.xyz / nearH.w;
|
|
281
|
+
let farPoint = farH.xyz / farH.w;
|
|
282
|
+
direction = normalize(farPoint - nearPoint);
|
|
283
|
+
origin = worldPos - direction * dot(worldPos - viewParams.cameraPos, direction);
|
|
284
|
+
rayDistance = max(dot(worldPos - origin, direction), 0.0);
|
|
285
|
+
}
|
|
286
|
+
return apply_fog(viewParams.fog, FogRay(origin, direction, rayDistance), vec4<f32>(color, alpha));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// linear_to_srgb: per-channel IEC 61966-2-1 transfer function used by the
|
|
290
|
+
// LDR fragment entry to encode linear premultiplied RGB into the bgra8unorm
|
|
291
|
+
// swap-chain storage. The bgra8unorm target is not hardware-sRGB-encoded
|
|
292
|
+
// (unlike bgra8unorm-srgb), so the shader must supply the encoding.
|
|
293
|
+
// Alpha is NOT encoded -- the blend equation operates on raw alpha.
|
|
294
|
+
fn linear_to_srgb(linear : f32) -> f32 {
|
|
295
|
+
let c = clamp(linear, 0.0, 1.0);
|
|
296
|
+
return select(c * 12.92, pow(c, 1.0 / 2.4) * 1.055 - 0.055, c > 0.0031308);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
@fragment
|
|
300
|
+
fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
|
301
|
+
let texel = sampleMaterialTexture(baseColorTexture, baseColorSampler, in.uv_atlas, material.baseColorTextureCoordinatesMetadata.zw);
|
|
302
|
+
// R6 mitigation: strict clamp 0..1 before premultiplied alpha multiply.
|
|
303
|
+
let rgba = clamp(texel * material.colorTint, vec4<f32>(0.0), vec4<f32>(1.0));
|
|
304
|
+
// Premultiplied alpha: rgb pre-multiplied by alpha for srcFactor='one' /
|
|
305
|
+
// dstFactor='one-minus-src-alpha' blend.
|
|
306
|
+
let fogged = applySceneFog(view, rgba.rgb, rgba.a, in.worldPos);
|
|
307
|
+
let premult = vec4<f32>(fogged.rgb * fogged.a, fogged.a);
|
|
308
|
+
// LDR target is bgra8unorm (blendable per WebGPU spec); hardware does not
|
|
309
|
+
// sRGB-encode bgra8unorm, so apply the transfer function in the shader.
|
|
310
|
+
// Only RGB channels are encoded; alpha is linear throughout the blend.
|
|
311
|
+
return vec4<f32>(
|
|
312
|
+
linear_to_srgb(premult.r),
|
|
313
|
+
linear_to_srgb(premult.g),
|
|
314
|
+
linear_to_srgb(premult.b),
|
|
315
|
+
premult.a,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// HDR variant: outputs linear premultiplied alpha for the rgba16float
|
|
320
|
+
// offscreen target; the tonemap fullscreen pass handles sRGB encoding.
|
|
321
|
+
@fragment
|
|
322
|
+
fn fs_main_hdr(in : VsOut) -> @location(0) vec4<f32> {
|
|
323
|
+
let texel = sampleMaterialTexture(baseColorTexture, baseColorSampler, in.uv_atlas, material.baseColorTextureCoordinatesMetadata.zw);
|
|
324
|
+
// R6 mitigation: strict clamp 0..1 before premultiplied alpha multiply.
|
|
325
|
+
let rgba = clamp(texel * material.colorTint, vec4<f32>(0.0), vec4<f32>(1.0));
|
|
326
|
+
let fogged = applySceneFog(view, rgba.rgb, rgba.a, in.worldPos);
|
|
327
|
+
return vec4<f32>(fogged.rgb * fogged.a, fogged.a);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
struct TemporalVsOut {
|
|
331
|
+
@builtin(position) clip : vec4<f32>,
|
|
332
|
+
@location(0) uvAtlas : vec2<f32>,
|
|
333
|
+
@location(1) @interpolate(linear) currentClip : vec4<f32>,
|
|
334
|
+
@location(2) @interpolate(linear) previousClip : vec4<f32>,
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
@vertex
|
|
338
|
+
fn vs_temporal(in : VsIn, @builtin(instance_index) idx : u32, @builtin(vertex_index) vertex_index : u32) -> TemporalVsOut {
|
|
339
|
+
let vertex = resolveSpriteVertex(in, idx, vertex_index);
|
|
340
|
+
let currentWorld = meshes[0].worldFromLocal *
|
|
341
|
+
instances[idx].localFromInstance * vec4<f32>(vertex.posLocal, 1.0);
|
|
342
|
+
var previousWorld = currentWorld;
|
|
343
|
+
#if STORAGE_BUFFER_AVAILABLE == true
|
|
344
|
+
previousWorld = meshes[0].previousWorldFromLocal *
|
|
345
|
+
instances[idx].previousLocalFromInstance * vec4<f32>(vertex.posLocal, 1.0);
|
|
346
|
+
#endif
|
|
347
|
+
var out : TemporalVsOut;
|
|
348
|
+
out.currentClip = view.temporalCurrentViewProj * currentWorld;
|
|
349
|
+
out.clip = out.currentClip;
|
|
350
|
+
out.previousClip = view.temporalPreviousViewProj * previousWorld;
|
|
351
|
+
out.uvAtlas = vertex.uvAtlas;
|
|
352
|
+
return out;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
@fragment
|
|
356
|
+
fn fs_temporal(in : TemporalVsOut) -> @location(0) vec4<f32> {
|
|
357
|
+
let texel = sampleMaterialTexture(baseColorTexture, baseColorSampler, in.uvAtlas, material.baseColorTextureCoordinatesMetadata.zw);
|
|
358
|
+
let alpha = clamp(texel.a * material.colorTint.a, 0.0, 1.0);
|
|
359
|
+
if (alpha <= 0.0) {
|
|
360
|
+
discard;
|
|
361
|
+
}
|
|
362
|
+
return packSceneTemporal(in.currentClip, in.previousClip, 1.0);
|
|
363
|
+
}
|
|
364
|
+
// sprite is unlit; do NOT read the directional-light fields of the View
|
|
365
|
+
// UBO -- same SSOT hard rule as the header comment above; R5 mitigation.
|