@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,519 @@
|
|
|
1
|
+
// @forgeax/engine-shader/ShaderRegistry — runtime shader registry, instance-per-engine shape.
|
|
2
|
+
//
|
|
3
|
+
// Shape rules (plan-strategy §S-10 / D-R10 / OQ-5 close + AC-03):
|
|
4
|
+
// - instance-per-engine — must be created via
|
|
5
|
+
// `new ShaderRegistry({device, manifestUrl})`; module-level singletons
|
|
6
|
+
// (`export const registry = ...`) and static methods (`static get`) are
|
|
7
|
+
// forbidden — aligned with the instance-based style of
|
|
8
|
+
// `Engine.create({ rhi })`.
|
|
9
|
+
// - Physical isolation — this file does **not** import @forgeax/engine-shader-compiler /
|
|
10
|
+
// @forgeax/engine-naga / @forgeax/engine-wgpu-wasm (guarded by the AC-06 triple-grep gate;
|
|
11
|
+
// feat-20260511-naga-rhi-wgpu-merge M4 replaced the legacy single-shim ban
|
|
12
|
+
// with the merged ban list).
|
|
13
|
+
// - Result model — `loadManifest()` / `get()` always go through Result.ok /
|
|
14
|
+
// Result.err and **never throw** (AGENTS.md "Errors are structured" / charter
|
|
15
|
+
// proposition 4: explicit failure).
|
|
16
|
+
// - 9-member error union — `get()` returns
|
|
17
|
+
// `Result<ShaderModule, RhiError | ShaderError>` (AGENTS.md "RHI / Shader /
|
|
18
|
+
// error-model contract").
|
|
19
|
+
|
|
20
|
+
import type { Result, RhiError, ShaderModule } from '@forgeax/engine-rhi';
|
|
21
|
+
import type {
|
|
22
|
+
ImmutableParamSchemaProjection,
|
|
23
|
+
ManifestEntry,
|
|
24
|
+
ParamSchemaEntry,
|
|
25
|
+
ParamSchemaProjectionOwnerStats,
|
|
26
|
+
} from '@forgeax/engine-types';
|
|
27
|
+
import { findUndeclaredSampledTextures, ParamSchemaProjectionOwner } from '@forgeax/engine-types';
|
|
28
|
+
import {
|
|
29
|
+
err,
|
|
30
|
+
manifestMalformed,
|
|
31
|
+
materialShaderNotFound,
|
|
32
|
+
ok,
|
|
33
|
+
type ShaderError,
|
|
34
|
+
type Result as ShaderResult,
|
|
35
|
+
shaderNotFound,
|
|
36
|
+
} from './errors.js';
|
|
37
|
+
import type { MaterialShaderManifestEntry } from './types.js';
|
|
38
|
+
|
|
39
|
+
// ─── Device dependency-injection interface ──────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Device dependency-injection interface — ShaderRegistry does not directly
|
|
43
|
+
* depend on `@forgeax/engine-rhi-webgpu` (physical isolation); the caller (engine or
|
|
44
|
+
* tests) supplies a device-like object that implements
|
|
45
|
+
* `createShaderModule(desc) → Result<ShaderModule, RhiError>`.
|
|
46
|
+
*
|
|
47
|
+
* Difference from the top-level async `createShaderModule(device, desc)` in
|
|
48
|
+
* `@forgeax/engine-rhi-webgpu`:
|
|
49
|
+
* - This interface is synchronous — so `registry.get()` can return a cached
|
|
50
|
+
* module synchronously.
|
|
51
|
+
* - The caller is responsible for wrapping the async real path into a sync one
|
|
52
|
+
* (e.g. the engine pre-compiles and caches modules during `loadManifest`).
|
|
53
|
+
*
|
|
54
|
+
* Mock test shape: a direct sync implementation (see `createMockDevice` in
|
|
55
|
+
* src/__tests__/registry.test.ts).
|
|
56
|
+
*/
|
|
57
|
+
export interface ShaderRegistryDevice {
|
|
58
|
+
createShaderModule(desc: {
|
|
59
|
+
readonly code: string;
|
|
60
|
+
readonly label?: string | undefined;
|
|
61
|
+
}): Result<ShaderModule, RhiError>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── MaterialShader registry types (feat-20260523-shader-template-instance-split M5) ──
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The `forgeax::` prefix reserves engine-shipped material shader identifiers
|
|
68
|
+
* (plan-strategy D-DefaultStandardPbr-Identifier + §8 charter F1 grep gate).
|
|
69
|
+
* AI users grep `forgeax::` to enumerate every reserved shader identifier in
|
|
70
|
+
* one shot. Custom user-side shaders pick their own identifier path
|
|
71
|
+
* (typically `<package>::<id>`) — anything that is not a `<guid>`
|
|
72
|
+
* (UUIDv5/v7) shape and not `forgeax::*`.
|
|
73
|
+
*/
|
|
74
|
+
export const FORGEAX_RESERVED_PATH_PREFIX = 'forgeax::' as const;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Material shader admission input accepted by
|
|
78
|
+
* `installMaterialArtifact(identifier, entry)`. `findMaterialArtifact`
|
|
79
|
+
* returns a {@link RegisteredMaterialShaderEntry} with the immutable schema
|
|
80
|
+
* projection attached.
|
|
81
|
+
*
|
|
82
|
+
* - `source`: composed WGSL source (post-naga_oil); the final form fed to
|
|
83
|
+
* `device.createShaderModule({ code })` at pipeline-build time.
|
|
84
|
+
* - `paramSchema`: closed list of `{name, type, default?}` triples that the
|
|
85
|
+
* `MaterialAsset.payload.values` is validated against at register-time
|
|
86
|
+
* (feat-20260523-shader-template-instance-split M4 3-tier validation). Per
|
|
87
|
+
* feat-20260613-material-paramschema-driven-binding M3 / w12-w13, the
|
|
88
|
+
* paramSchema is also the SSOT for the pipeline BGL via
|
|
89
|
+
* `derive(paramSchema).bglEntries` — there is no longer a separate
|
|
90
|
+
* `bindingLayout` field on this entry (D-1 / D-2).
|
|
91
|
+
*
|
|
92
|
+
* The shape is symmetric across engine-default + user-custom paths
|
|
93
|
+
* (charter P4 consistent abstraction): both go through the same
|
|
94
|
+
* `installMaterialArtifact` API regardless of whether the host wires the
|
|
95
|
+
* default-standard-pbr triple at boot or the user registers a custom
|
|
96
|
+
* shader after asset import.
|
|
97
|
+
*/
|
|
98
|
+
export interface MaterialShaderEntry {
|
|
99
|
+
readonly source: string;
|
|
100
|
+
readonly paramSchema: readonly ParamSchemaEntry[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Registry-owned runtime shape published after immutable schema admission. */
|
|
104
|
+
export interface RegisteredMaterialShaderEntry extends MaterialShaderEntry {
|
|
105
|
+
readonly paramSchemaProjection: ImmutableParamSchemaProjection;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── Public registry types ──────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
export interface ShaderRegistryOptions {
|
|
111
|
+
readonly device: ShaderRegistryDevice;
|
|
112
|
+
/**
|
|
113
|
+
* URL the registry fetches `manifest.json` from. `undefined` puts the
|
|
114
|
+
* registry into the **zero-entry** mode: `loadManifest()` resolves
|
|
115
|
+
* `Result.ok(undefined)` without issuing a fetch and `entries()` returns
|
|
116
|
+
* an empty iterator. This is the bug-20260519 clear-pass-only path
|
|
117
|
+
* where AI users build a `Camera`-only world (no PBR / unlit pipeline
|
|
118
|
+
* is ever needed). `createRenderer.ts` Step 2 narrows on
|
|
119
|
+
* `manifestEntries.length > 0` to skip the dual `createShaderModule`
|
|
120
|
+
* compile in the same path (charter P3 + plan-strategy D-1 + D-2).
|
|
121
|
+
*/
|
|
122
|
+
readonly manifestUrl: string | undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ─── ShaderRegistry main class (instance-per-engine) ============================
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Runtime shader registry — content-addressable manifest lookup →
|
|
129
|
+
* `device.createShaderModule`.
|
|
130
|
+
*
|
|
131
|
+
* Usage:
|
|
132
|
+
* ```
|
|
133
|
+
* const registry = new ShaderRegistry({ device, manifestUrl: '/shaders/manifest.json' });
|
|
134
|
+
* const loaded = await registry.loadManifest();
|
|
135
|
+
* if (!loaded.ok) handleError(loaded.error); // ShaderError.manifest-malformed
|
|
136
|
+
*
|
|
137
|
+
* const result = registry.get('abc12345');
|
|
138
|
+
* if (!result.ok) handleError(result.error); // RhiError | ShaderError.shader-not-found
|
|
139
|
+
* else useModule(result.value);
|
|
140
|
+
* ```
|
|
141
|
+
*
|
|
142
|
+
* `static get` / module-level singletons are **forbidden** (plan-strategy
|
|
143
|
+
* §S-10).
|
|
144
|
+
*/
|
|
145
|
+
export class ShaderRegistry {
|
|
146
|
+
readonly #device: ShaderRegistryDevice;
|
|
147
|
+
readonly #manifestUrl: string | undefined;
|
|
148
|
+
// hash → ManifestEntry index (populated after loadManifest).
|
|
149
|
+
readonly #entries = new Map<string, ManifestEntry>();
|
|
150
|
+
// hash → ShaderModule cache (populated lazily on first get()).
|
|
151
|
+
readonly #moduleCache = new Map<string, ShaderModule>();
|
|
152
|
+
// identifier → MaterialShaderEntry index (populated by
|
|
153
|
+
// installMaterialArtifact; feat-20260523-shader-template-instance-split M5 / T05).
|
|
154
|
+
readonly #materialShaders = new Map<string, RegisteredMaterialShaderEntry>();
|
|
155
|
+
readonly #paramSchemaProjectionOwner = new ParamSchemaProjectionOwner();
|
|
156
|
+
readonly #materialShaderManifestEntries: MaterialShaderManifestEntry[] = [];
|
|
157
|
+
#manifestLoaded = false;
|
|
158
|
+
|
|
159
|
+
constructor(opts: ShaderRegistryOptions) {
|
|
160
|
+
this.#device = opts.device;
|
|
161
|
+
this.#manifestUrl = opts.manifestUrl;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Loads manifest.json and populates the hash → ManifestEntry index.
|
|
166
|
+
*
|
|
167
|
+
* Failure paths (charter proposition 4: explicit failure):
|
|
168
|
+
* - fetch / data: URL parse failed → `Result.err(ShaderError.manifest-malformed)`
|
|
169
|
+
* - JSON.parse failed → ditto
|
|
170
|
+
* - schema missing the `entries` field / an entries element missing
|
|
171
|
+
* hash/wgsl/bindings → ditto
|
|
172
|
+
* - a material shader row is incomplete → ditto
|
|
173
|
+
*
|
|
174
|
+
* Validation is atomic: the registry does not publish any entry or
|
|
175
|
+
* material shader row until the complete document has been validated.
|
|
176
|
+
*
|
|
177
|
+
* Idempotent: subsequent calls reuse the first result (never re-fetch;
|
|
178
|
+
* charter proposition 6: idempotency).
|
|
179
|
+
*/
|
|
180
|
+
async loadManifest(): Promise<ShaderResult<void, ShaderError>> {
|
|
181
|
+
if (this.#manifestLoaded) return ok(undefined);
|
|
182
|
+
|
|
183
|
+
// bug-20260519 D-2: zero-entry mode. `manifestUrl === undefined` means
|
|
184
|
+
// the host opted out of shipping a shader manifest (Camera-only /
|
|
185
|
+
// clear-pass-only LO 1.1 path); resolve `Result.ok` without issuing a
|
|
186
|
+
// fetch. `entries()` then yields nothing and createRenderer Step 2
|
|
187
|
+
// skips the dual `createShaderModule` compile (D-1 + D-3 nullable
|
|
188
|
+
// PipelineState). Subsequent `get(hash)` calls fall through to the
|
|
189
|
+
// shader-not-found arm via the empty entries map (charter P3
|
|
190
|
+
// explicit failure on misuse).
|
|
191
|
+
if (this.#manifestUrl === undefined) {
|
|
192
|
+
this.#manifestLoaded = true;
|
|
193
|
+
return ok(undefined);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let raw: string;
|
|
197
|
+
try {
|
|
198
|
+
const response = await fetch(this.#manifestUrl);
|
|
199
|
+
raw = await response.text();
|
|
200
|
+
} catch (e) {
|
|
201
|
+
return err(
|
|
202
|
+
manifestMalformed({
|
|
203
|
+
message: `ShaderRegistry: failed to fetch manifest at ${this.#manifestUrl}`,
|
|
204
|
+
hint: 'verify manifest URL is reachable; check bundler emitFile output path',
|
|
205
|
+
reason: e instanceof Error ? e.message : String(e),
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let parsed: unknown;
|
|
211
|
+
try {
|
|
212
|
+
parsed = JSON.parse(raw);
|
|
213
|
+
} catch (e) {
|
|
214
|
+
return err(
|
|
215
|
+
manifestMalformed({
|
|
216
|
+
message: 'ShaderRegistry: manifest JSON parse failed',
|
|
217
|
+
hint: 'manifest.json must be valid JSON; rebuild via @forgeax/engine-vite-plugin-shader generateBundle',
|
|
218
|
+
reason: e instanceof Error ? e.message : String(e),
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (typeof parsed !== 'object' || parsed === null || !('entries' in parsed)) {
|
|
224
|
+
return err(
|
|
225
|
+
manifestMalformed({
|
|
226
|
+
message: 'ShaderRegistry: manifest missing required `entries` field',
|
|
227
|
+
hint: 'manifest schema requires top-level `entries: ManifestEntry[]`',
|
|
228
|
+
}),
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const entries = (parsed as { entries: unknown }).entries;
|
|
233
|
+
if (!Array.isArray(entries)) {
|
|
234
|
+
return err(
|
|
235
|
+
manifestMalformed({
|
|
236
|
+
message: 'ShaderRegistry: manifest.entries is not an array',
|
|
237
|
+
hint: 'manifest schema requires `entries: ManifestEntry[]`',
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const validatedEntries: ManifestEntry[] = [];
|
|
243
|
+
for (const entry of entries) {
|
|
244
|
+
if (!isValidManifestEntry(entry)) {
|
|
245
|
+
return err(
|
|
246
|
+
manifestMalformed({
|
|
247
|
+
message: 'ShaderRegistry: manifest entry missing required fields',
|
|
248
|
+
hint: 'every entry needs {hash, wgsl, glsl, bindings} per @forgeax/engine-types.ManifestEntry',
|
|
249
|
+
reason: `bad entry: ${JSON.stringify(entry)}`,
|
|
250
|
+
}),
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
validatedEntries.push(entry);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// feat-20260526-pbr-uniform-fallback-no-storage-buffer M3 / w12:
|
|
257
|
+
// parse and store materialShaders[] from the manifest for
|
|
258
|
+
// createRenderer variant resolution. Non-existent / non-array
|
|
259
|
+
// materialShaders is treated as empty (backward-compatible with
|
|
260
|
+
// manifests from pre-M2 build runs).
|
|
261
|
+
const parsedSlim = parsed as { materialShaders?: unknown };
|
|
262
|
+
const validatedMaterialShaderEntries: MaterialShaderManifestEntry[] = [];
|
|
263
|
+
if (Array.isArray(parsedSlim.materialShaders)) {
|
|
264
|
+
for (const ms of parsedSlim.materialShaders) {
|
|
265
|
+
if (!isValidMaterialShaderManifestEntry(ms)) {
|
|
266
|
+
return err(
|
|
267
|
+
manifestMalformed({
|
|
268
|
+
message: 'ShaderRegistry: manifest material shader entry missing required fields',
|
|
269
|
+
hint: 'every material shader entry needs {identifier, sourcePath, composedWgsl, paramSchema, variants}',
|
|
270
|
+
reason: `bad material shader entry: ${JSON.stringify(ms)}`,
|
|
271
|
+
}),
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
validatedMaterialShaderEntries.push(ms);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
this.#entries.clear();
|
|
279
|
+
for (const entry of validatedEntries) {
|
|
280
|
+
this.#entries.set(entry.hash, entry);
|
|
281
|
+
}
|
|
282
|
+
this.#materialShaderManifestEntries.length = 0;
|
|
283
|
+
this.#materialShaderManifestEntries.push(...validatedMaterialShaderEntries);
|
|
284
|
+
this.#manifestLoaded = true;
|
|
285
|
+
return ok(undefined);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Iterate every `ManifestEntry` populated by the most recent successful
|
|
290
|
+
* `loadManifest()` call. Order matches the manifest JSON `entries` array
|
|
291
|
+
* (insertion order; charter P2 structured-over-prose: AI users walk this
|
|
292
|
+
* to discover engine-shipped entries by content marker without
|
|
293
|
+
* re-fetching the manifest).
|
|
294
|
+
*
|
|
295
|
+
* Returns an empty iterator when no manifest has been loaded yet
|
|
296
|
+
* (charter P3 explicit failure: the empty case is not an error — the
|
|
297
|
+
* absent SSOT is the absent contract).
|
|
298
|
+
*/
|
|
299
|
+
entries(): IterableIterator<ManifestEntry> {
|
|
300
|
+
return this.#entries.values();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Looks up a hash and forwards to `device.createShaderModule` (creates lazily
|
|
305
|
+
* on first hit, then caches).
|
|
306
|
+
*
|
|
307
|
+
* Error paths:
|
|
308
|
+
* - hash miss → `Result.err(ShaderError.shader-not-found)`
|
|
309
|
+
* - device.createShaderModule failed → `Result.err(RhiError)` passes through
|
|
310
|
+
* (it is **not** wrapped as a ShaderError).
|
|
311
|
+
*
|
|
312
|
+
* Sync shape: on cache miss, the underlying `device.createShaderModule` is
|
|
313
|
+
* called synchronously; the caller is responsible for handling the async
|
|
314
|
+
* real path at the device-wrapper layer (e.g. the engine pre-compiles
|
|
315
|
+
* during `loadManifest`).
|
|
316
|
+
*/
|
|
317
|
+
get(hash: string): Result<ShaderModule, RhiError | ShaderError> {
|
|
318
|
+
const cachedModule = this.#moduleCache.get(hash);
|
|
319
|
+
if (cachedModule !== undefined) {
|
|
320
|
+
return ok(cachedModule);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const entry = this.#entries.get(hash);
|
|
324
|
+
if (entry === undefined) {
|
|
325
|
+
return err(
|
|
326
|
+
shaderNotFound({
|
|
327
|
+
hash,
|
|
328
|
+
hint: 'verify manifest.json contains the expected hash; rerun build to regenerate manifest',
|
|
329
|
+
}),
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const result = this.#device.createShaderModule({ code: entry.wgsl, label: hash });
|
|
334
|
+
if (!result.ok) {
|
|
335
|
+
return result;
|
|
336
|
+
}
|
|
337
|
+
this.#moduleCache.set(hash, result.value);
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ─── MaterialShader registry surface ───────────────────────────────────────
|
|
342
|
+
// (feat-20260523-shader-template-instance-split M5 / T05 — plan-strategy
|
|
343
|
+
// D-DefaultStandardPbr-Identifier).
|
|
344
|
+
//
|
|
345
|
+
// Two-tier identifier namespace:
|
|
346
|
+
// - `forgeax::*` — engine-shipped reserved prefix; the host wires
|
|
347
|
+
// `forgeax::default-standard-pbr` at engine boot from the
|
|
348
|
+
// vite-plugin-shader manifest's `materialShaders[]` row + the
|
|
349
|
+
// `default-standard-pbr.schema.json` SSOT (M5 / T08 + M6).
|
|
350
|
+
// - any other identifier — user-side custom material shader, registered
|
|
351
|
+
// after asset import.
|
|
352
|
+
//
|
|
353
|
+
// The registry is the runtime SSOT consumed by:
|
|
354
|
+
// - the material decoder's `'::' two-way dispatch` (M4 / T01) — the path
|
|
355
|
+
// branch validates against `paramSchema` and feeds `MaterialSnapshot.
|
|
356
|
+
// materialShaderId` (M4 / T05).
|
|
357
|
+
// - `render-system-record`'s pipeline cache key (M4 / T06) — same
|
|
358
|
+
// identifier feeds the per-pipeline `(materialShaderId, stateHash)` key.
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Register a material shader entry under a stable identifier.
|
|
362
|
+
*
|
|
363
|
+
* **Throws** on:
|
|
364
|
+
* - duplicate registration of the same identifier (programmer error per
|
|
365
|
+
* AGENTS.md "explicit registration" + Inspector "fail-fast no overwrite"
|
|
366
|
+
* pattern; charter P3 explicit failure on misuse).
|
|
367
|
+
*
|
|
368
|
+
* Successful path returns void — the registry mutates in-place. Use
|
|
369
|
+
* {@link findMaterialArtifact} to read back.
|
|
370
|
+
*
|
|
371
|
+
* Identifier conventions (charter F1 / plan-strategy §8 §2):
|
|
372
|
+
* - `forgeax::<kebab-case>` — engine-shipped reserved prefix; the host
|
|
373
|
+
* registers `forgeax::default-standard-pbr` at boot.
|
|
374
|
+
* - `<package>::<id>` or `<guid>` — user-side custom shaders.
|
|
375
|
+
*
|
|
376
|
+
* @example
|
|
377
|
+
* ```ts
|
|
378
|
+
* const registry = new ShaderRegistry({ device, manifestUrl });
|
|
379
|
+
* registry.installMaterialArtifact('forgeax::default-standard-pbr', {
|
|
380
|
+
* source: composedWgsl,
|
|
381
|
+
* paramSchema: defaultStandardPbrSchema,
|
|
382
|
+
* });
|
|
383
|
+
* ```
|
|
384
|
+
*/
|
|
385
|
+
installMaterialArtifact(identifier: string, entry: MaterialShaderEntry): void {
|
|
386
|
+
if (this.#materialShaders.has(identifier)) {
|
|
387
|
+
throw new Error(
|
|
388
|
+
`ShaderRegistry: material shader identifier '${identifier}' already registered; same-name re-register is forbidden (AGENTS.md "explicit registration" + Inspector "fail-fast no overwrite" pattern). Use findMaterialArtifact to read back the existing entry.`,
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
// bug-20260619: user shaders registered directly here bypass the build-time
|
|
392
|
+
// superset gate (vite-plugin-shader's WGSL reflection). A schema that omits
|
|
393
|
+
// a texture field the WGSL actually samples would let the extract stage's
|
|
394
|
+
// `validateTextureHandle` silently drop the handle and fall back to the
|
|
395
|
+
// default white texture (opaque-white grass/windows in the LO 4.3 blending
|
|
396
|
+
// demo). Fail fast at register time instead (charter P3 explicit failure).
|
|
397
|
+
// Scoped to user shaders: engine `forgeax::*` shaders go through the
|
|
398
|
+
// build-time gate and may sample engine-injected textures (emissive /
|
|
399
|
+
// occlusion) absent from their schema by design.
|
|
400
|
+
const paramSchemaProjection = this.#paramSchemaProjectionOwner.admit({
|
|
401
|
+
ownerId: identifier,
|
|
402
|
+
revision: 1,
|
|
403
|
+
schema: entry.paramSchema,
|
|
404
|
+
});
|
|
405
|
+
if (!identifier.startsWith(FORGEAX_RESERVED_PATH_PREFIX)) {
|
|
406
|
+
const undeclared = findUndeclaredSampledTextures(entry.source, paramSchemaProjection.schema);
|
|
407
|
+
if (undeclared.length > 0) {
|
|
408
|
+
throw new Error(
|
|
409
|
+
`ShaderRegistry: material shader '${identifier}' samples texture(s) [${undeclared.join(', ')}] in its WGSL but its paramSchema does not declare them as texture entries. Add { name: '${undeclared[0]}', type: 'texture2d' } (and any others listed) to the paramSchema, or the engine would silently bind the default white texture (charter P3 explicit failure; see docs/handover/2026-06-19-blending-transparency-regression-bisect.md).`,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
this.#materialShaders.set(
|
|
414
|
+
identifier,
|
|
415
|
+
Object.freeze({
|
|
416
|
+
source: entry.source,
|
|
417
|
+
paramSchema: paramSchemaProjection.schema,
|
|
418
|
+
paramSchemaProjection,
|
|
419
|
+
}),
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Lookup a previously-registered material shader entry by identifier.
|
|
425
|
+
*
|
|
426
|
+
* Returns `Result.ok(entry)` on hit, `Result.err(material-shader-not-found)`
|
|
427
|
+
* on miss. The `Result` shape mirrors `get(hash)` for consistent
|
|
428
|
+
* abstraction across the runtime registry surface (charter P4).
|
|
429
|
+
*
|
|
430
|
+
* @example
|
|
431
|
+
* ```ts
|
|
432
|
+
* const r = registry.findMaterialArtifact('forgeax::default-standard-pbr');
|
|
433
|
+
* if (!r.ok) return handleError(r.error);
|
|
434
|
+
* const { source, paramSchema } = r.value;
|
|
435
|
+
* ```
|
|
436
|
+
*/
|
|
437
|
+
findMaterialArtifact(
|
|
438
|
+
identifier: string,
|
|
439
|
+
): ShaderResult<RegisteredMaterialShaderEntry, ShaderError> {
|
|
440
|
+
const entry = this.#materialShaders.get(identifier);
|
|
441
|
+
if (entry === undefined) {
|
|
442
|
+
return err(
|
|
443
|
+
materialShaderNotFound({
|
|
444
|
+
identifier,
|
|
445
|
+
expected: Array.from(this.#materialShaders.keys()),
|
|
446
|
+
hint: `register the shader via ShaderRegistry.installMaterialArtifact('${identifier}', ...) at engine boot, or grep '${FORGEAX_RESERVED_PATH_PREFIX}' to enumerate engine-shipped reserved identifiers`,
|
|
447
|
+
}),
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
return ok(entry);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Iterate every registered material shader identifier in registration
|
|
455
|
+
* order (charter F1 grep gate: `for (const id of registry.materialShaderIdentifiers())`
|
|
456
|
+
* lists every `forgeax::*` + user shader without a private-field reach-in).
|
|
457
|
+
*/
|
|
458
|
+
materialShaderIdentifiers(): IterableIterator<string> {
|
|
459
|
+
return this.#materialShaders.keys();
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Runtime diagnostic counters for the immutable ParamSchema owner. */
|
|
463
|
+
paramSchemaProjectionStats(): ParamSchemaProjectionOwnerStats {
|
|
464
|
+
return this.#paramSchemaProjectionOwner.stats();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Iterate every MaterialShaderManifestEntry parsed from manifest.json's
|
|
469
|
+
* `materialShaders[]` field during `loadManifest()`.
|
|
470
|
+
*
|
|
471
|
+
* feat-20260526-pbr-uniform-fallback-no-storage-buffer M3 / w12:
|
|
472
|
+
* createRenderer variant resolution consumes this list to select the
|
|
473
|
+
* correct variant (by `caps.storageBuffer`) and register the resolved
|
|
474
|
+
* WGSL via `installMaterialArtifact`.
|
|
475
|
+
*
|
|
476
|
+
* Returns an empty iterator when no manifest has been loaded yet or
|
|
477
|
+
* the manifest lacks `materialShaders` (backward-compatible).
|
|
478
|
+
*/
|
|
479
|
+
materialShaderManifestEntries(): IterableIterator<MaterialShaderManifestEntry> {
|
|
480
|
+
return this.#materialShaderManifestEntries.values();
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ─── helpers ────────────────────────────────────────────────────────────────────
|
|
485
|
+
|
|
486
|
+
function isValidManifestEntry(value: unknown): value is ManifestEntry {
|
|
487
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
488
|
+
const v = value as Record<string, unknown>;
|
|
489
|
+
if (typeof v.hash !== 'string') return false;
|
|
490
|
+
if (typeof v.wgsl !== 'string') return false;
|
|
491
|
+
// glsl allows string | undefined | null (manifest.json may serialize null as
|
|
492
|
+
// 'glsl: null').
|
|
493
|
+
if (v.glsl !== undefined && v.glsl !== null && typeof v.glsl !== 'string') return false;
|
|
494
|
+
if (typeof v.bindings !== 'string') return false;
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function isValidMaterialShaderManifestEntry(value: unknown): value is MaterialShaderManifestEntry {
|
|
499
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
500
|
+
const v = value as Record<string, unknown>;
|
|
501
|
+
if (typeof v.identifier !== 'string') return false;
|
|
502
|
+
if (typeof v.sourcePath !== 'string') return false;
|
|
503
|
+
if (typeof v.composedWgsl !== 'string') return false;
|
|
504
|
+
if (typeof v.paramSchema !== 'string') return false;
|
|
505
|
+
if (!Array.isArray(v.variants)) return false;
|
|
506
|
+
if (!v.variants.every(isValidMaterialShaderManifestVariant)) return false;
|
|
507
|
+
return v.uvSetCount === undefined || typeof v.uvSetCount === 'number';
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function isValidMaterialShaderManifestVariant(value: unknown): boolean {
|
|
511
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
512
|
+
const v = value as Record<string, unknown>;
|
|
513
|
+
if (typeof v.definesKey !== 'string') return false;
|
|
514
|
+
if (typeof v.composedWgsl !== 'string') return false;
|
|
515
|
+
if (typeof v.defines !== 'object' || v.defines === null || Array.isArray(v.defines)) {
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
return Object.values(v.defines).every((define) => typeof define === 'boolean');
|
|
519
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
let source = '';
|
|
4
|
+
|
|
5
|
+
beforeAll(async () => {
|
|
6
|
+
const fs = await import(/* @vite-ignore */ 'node:fs');
|
|
7
|
+
const path = await import(/* @vite-ignore */ 'node:path');
|
|
8
|
+
const url = await import(/* @vite-ignore */ 'node:url');
|
|
9
|
+
source = fs.readFileSync(
|
|
10
|
+
path.resolve(
|
|
11
|
+
path.dirname(url.fileURLToPath(import.meta.url)),
|
|
12
|
+
'..',
|
|
13
|
+
'atmosphere-background.wgsl',
|
|
14
|
+
),
|
|
15
|
+
'utf8',
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('analytic atmosphere background ray and disc', () => {
|
|
20
|
+
it('reconstructs a camera-relative world ray before comparing the Sun direction', () => {
|
|
21
|
+
expect(source).toMatch(/world\.xyz\s*\/\s*world\.w\s*-\s*view\.cameraPos/);
|
|
22
|
+
expect(source).toContain('atmosphere_sun_disc_radiance');
|
|
23
|
+
expect(source).toMatch(/dot\(normalize\(viewDirection\),\s*normalize\(sunDirection\)\)/);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('keeps the Sun disc out of the generated cube and IBL path', () => {
|
|
27
|
+
expect(source).toContain('sunDiscEnabled');
|
|
28
|
+
expect(source).toContain('atmosphere.sunDirection.y > 0.0');
|
|
29
|
+
expect(source).toContain('solidAngle');
|
|
30
|
+
expect(source).toContain('textureSample(sky, skySampler, cubeDirection)');
|
|
31
|
+
expect(source).toContain('vec3<f32>(direction.x, -direction.y, direction.z)');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
type Direction = readonly [number, number, number];
|
|
4
|
+
|
|
5
|
+
let cubeSource = '';
|
|
6
|
+
let iblSource = '';
|
|
7
|
+
|
|
8
|
+
beforeAll(async () => {
|
|
9
|
+
const fs = await import(/* @vite-ignore */ 'node:fs');
|
|
10
|
+
const path = await import(/* @vite-ignore */ 'node:path');
|
|
11
|
+
const url = await import(/* @vite-ignore */ 'node:url');
|
|
12
|
+
const shaderRoot = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), '..');
|
|
13
|
+
cubeSource = fs.readFileSync(path.resolve(shaderRoot, 'atmosphere-cubemap.wgsl'), 'utf8');
|
|
14
|
+
iblSource = fs.readFileSync(path.resolve(shaderRoot, 'atmosphere-ibl.wgsl'), 'utf8');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function worldDirection(face: number, x: number, y: number): Direction {
|
|
18
|
+
switch (face) {
|
|
19
|
+
case 0:
|
|
20
|
+
return [1, -y, -x];
|
|
21
|
+
case 1:
|
|
22
|
+
return [-1, -y, x];
|
|
23
|
+
case 2:
|
|
24
|
+
return [x, -1, -y];
|
|
25
|
+
case 3:
|
|
26
|
+
return [x, 1, y];
|
|
27
|
+
case 4:
|
|
28
|
+
return [x, -y, 1];
|
|
29
|
+
default:
|
|
30
|
+
return [-x, -y, -1];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function sameDirection(left: Direction, right: Direction): boolean {
|
|
35
|
+
return left.every((value, index) => Math.abs(value - (right[index] ?? 0)) < 1e-8);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('analytic cubemap world-direction convention', () => {
|
|
39
|
+
it('keeps every sampled face edge continuous with another face', () => {
|
|
40
|
+
const samples = [-1, -0.5, 0, 0.5, 1];
|
|
41
|
+
const edges: { readonly face: number; readonly direction: Direction }[] = [];
|
|
42
|
+
for (let face = 0; face < 6; face += 1) {
|
|
43
|
+
for (const sample of samples) {
|
|
44
|
+
edges.push({ face, direction: worldDirection(face, -1, sample) });
|
|
45
|
+
edges.push({ face, direction: worldDirection(face, 1, sample) });
|
|
46
|
+
edges.push({ face, direction: worldDirection(face, sample, -1) });
|
|
47
|
+
edges.push({ face, direction: worldDirection(face, sample, 1) });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
for (const edge of edges) {
|
|
51
|
+
expect(
|
|
52
|
+
edges.some(
|
|
53
|
+
(candidate) =>
|
|
54
|
+
candidate.face !== edge.face && sameDirection(candidate.direction, edge.direction),
|
|
55
|
+
),
|
|
56
|
+
).toBe(true);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('pins the corrected Y faces and applies the same convention to IBL', () => {
|
|
61
|
+
for (const source of [cubeSource, iblSource]) {
|
|
62
|
+
expect(source).toContain('case 2u: { output.direction = vec3<f32>(x, -1.0, -y); }');
|
|
63
|
+
expect(source).toContain('case 3u: { output.direction = vec3<f32>(x, 1.0, y); }');
|
|
64
|
+
}
|
|
65
|
+
expect(iblSource).toContain('-sampleDirection.y');
|
|
66
|
+
expect(iblSource).toContain('-lightDirection.y');
|
|
67
|
+
});
|
|
68
|
+
});
|