@babylonjs/lite 1.5.0 → 1.6.0

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.
Files changed (41) hide show
  1. package/dist/index.js +440 -446
  2. package/dist/index.js.map +1 -1
  3. package/index.d.ts +90 -46
  4. package/lib/camera/geospatial-camera-controls.js +22 -0
  5. package/lib/camera/geospatial-camera-controls.js.map +1 -1
  6. package/lib/camera/geospatial-camera-fly.js +2 -1
  7. package/lib/camera/geospatial-camera-fly.js.map +1 -1
  8. package/lib/effect/effect-renderer.js +1 -1
  9. package/lib/effect/effect-renderer.js.map +1 -1
  10. package/lib/engine/engine.js +1 -1
  11. package/lib/index.js +4 -1
  12. package/lib/index.js.map +1 -1
  13. package/lib/post-process/taa.js +193 -0
  14. package/lib/post-process/taa.js.map +1 -0
  15. package/lib/sprite/billboard-custom-shader.js +32 -32
  16. package/lib/sprite/billboard-custom-shader.js.map +1 -1
  17. package/lib/sprite/billboard-pipeline.js +54 -56
  18. package/lib/sprite/billboard-pipeline.js.map +1 -1
  19. package/lib/sprite/custom-shader-core.js +1 -1
  20. package/lib/sprite/custom-shader-core.js.map +1 -1
  21. package/lib/sprite/shared/sprite-atlas.js +2 -2
  22. package/lib/sprite/shared/sprite-atlas.js.map +1 -1
  23. package/lib/sprite/sprite-2d-coverage-gamma.js +58 -0
  24. package/lib/sprite/sprite-2d-coverage-gamma.js.map +1 -0
  25. package/lib/sprite/sprite-2d-uvscroll.js +39 -0
  26. package/lib/sprite/sprite-2d-uvscroll.js.map +1 -0
  27. package/lib/sprite/sprite-2d.js +6 -40
  28. package/lib/sprite/sprite-2d.js.map +1 -1
  29. package/lib/sprite/sprite-coverage-gamma-hook.js +10 -0
  30. package/lib/sprite/sprite-coverage-gamma-hook.js.map +1 -0
  31. package/lib/sprite/sprite-custom-shader.js +2 -2
  32. package/lib/sprite/sprite-custom-shader.js.map +1 -1
  33. package/lib/sprite/sprite-pipeline.js +56 -71
  34. package/lib/sprite/sprite-pipeline.js.map +1 -1
  35. package/lib/sprite/sprite-renderable.js +5 -5
  36. package/lib/sprite/sprite-renderable.js.map +1 -1
  37. package/lib/sprite/sprite-renderer.js +4 -4
  38. package/lib/sprite/sprite-renderer.js.map +1 -1
  39. package/lib/sprite/sprite-scene.js +1 -1
  40. package/lib/sprite/sprite-scene.js.map +1 -1
  41. package/package.json +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"billboard-custom-shader.js","sources":["../../../src/sprite/billboard-custom-shader.ts"],"sourcesContent":["/**\n * Optional, tree-shakable custom-shader hook for `*BillboardSpriteSystem`.\n *\n * The default billboard pipeline bakes a fixed fragment (sample × tint, with an optional cutout\n * discard). Some scenes need a different per-fragment treatment — palette-indexed sampling,\n * COLORMAP light banding, toon shading, custom fog — and/or extra texture bindings beyond the\n * atlas. This module lets a caller supply a WGSL fragment body plus extra textures while the\n * billboard system keeps full ownership of geometry, instancing, sorting, and depth.\n *\n * Tree-shaking contract: the default billboard path never imports this module.\n * `billboard-pipeline.ts` only reaches the custom composer through the opaque object a caller\n * builds here via `createBillboardCustomShader`, so a scene that uses only stock billboards\n * pays zero bytes for this code.\n *\n * WGSL contract for the supplied `fragment` body:\n * - Receives `in: VOut` with: `uv: vec2<f32>`, `tint: vec4<f32>` (the per-sprite `color`),\n * `viewDist: f32` (distance from the camera to the sprite anchor in world units, constant\n * across the quad), `vWorldPos: vec3<f32>` (this fragment's world position).\n * - Has access to `atlasTex` / `atlasSamp` (the system atlas at group 1, bindings 1/2), each\n * extra texture as `<name>Tex` / `<name>Samp`, the `fx` UBO (`fx.time`, `fx.params`), and\n * the `billboards` system UBO (e.g. `billboards.opacityMul`).\n * - Must `return vec4<f32>(...)` (and may `discard`). No automatic cutout is injected — the\n * body owns all alpha handling.\n */\nimport { SCENE_UBO_WGSL } from \"../shader/scene-uniforms.js\";\nimport type { EngineContext } from \"../engine/engine.js\";\nimport type { BillboardDepthMode, BillboardOrientation } from \"./billboard-sprite.js\";\nimport { makeBillboardBasisWgsl } from \"./billboard-pipeline.js\";\nimport type { CustomShaderTexture, SpriteLayerFx } from \"./custom-shader-core.js\";\nimport {\n createSpriteLayerFx,\n EMPTY_PARAMS,\n makeCustomShaderLayoutEntries,\n makeExtraBindingsWgsl,\n makeFxStructWgsl,\n makeShaderModuleCache,\n nextCustomShaderKey,\n validateExtraTextureNames,\n} from \"./custom-shader-core.js\";\nimport type { BillboardFxHook } from \"./sprite-fx-hook.js\";\nimport { _registerBillboardFxHook } from \"./sprite-fx-hook.js\";\n\n/** One extra texture bound after the atlas (group 1, bindings 3, 5, 7, …). */\nexport type BillboardCustomTexture = CustomShaderTexture;\n\n/** Options for {@link createBillboardCustomShader}. */\nexport interface BillboardCustomShaderOptions {\n /** WGSL fragment body. See the module docs for the in-scope identifiers. */\n readonly fragment: string;\n /** Extra textures, in binding order. Each contributes a `texture_2d` + `sampler`. */\n readonly extraTextures?: readonly BillboardCustomTexture[];\n}\n\n/** Opaque custom-shader descriptor produced by {@link createBillboardCustomShader}. */\nexport interface BillboardCustomShader {\n /** @internal */\n readonly _entityType: \"billboard-custom-shader\";\n /** @internal Extra textures bound after the atlas. */\n readonly _extraTextures: readonly BillboardCustomTexture[];\n /** @internal Pipeline/shader-module cache discriminator. */\n readonly _key: string;\n /** @internal Builds the full WGSL for the given orientation (depth mode is irrelevant — the body owns alpha). */\n readonly _composeWgsl: (orientation: BillboardOrientation, depthMode: BillboardDepthMode) => string;\n /** @internal Compile + cache the `GPUShaderModule` for an orientation (owns its per-device cache). */\n readonly _getShaderModule: (engine: EngineContext, orientation: BillboardOrientation, depthMode: BillboardDepthMode) => GPUShaderModule;\n /** @internal Extra-texture + fx UBO bind-group **layout** entries, starting at `startBinding` (3). */\n readonly _layoutEntries: (startBinding: number) => GPUBindGroupLayoutEntry[];\n /** @internal Build the opaque per-system fx attachment (owns the `SpriteFx` UBO, scratch, and elapsed time). */\n readonly _createLayerFx: (engine: EngineContext, label: string) => SpriteLayerFx;\n}\n\nfunction makeCustomBillboardWgsl(orientation: BillboardOrientation, extraTextures: readonly BillboardCustomTexture[], fragment: string): string {\n const fxBinding = 3 + extraTextures.length * 2;\n return `${SCENE_UBO_WGSL}\nstruct BillboardSystem {\nopacityMul: vec4<f32>,\naxisAndCutoff: vec4<f32>,\n};\n@group(1) @binding(0) var<uniform> billboards: BillboardSystem;\n@group(1) @binding(1) var atlasTex: texture_2d<f32>;\n@group(1) @binding(2) var atlasSamp: sampler;\n${makeExtraBindingsWgsl(1, 3, extraTextures)}${makeFxStructWgsl(1, fxBinding)}\n${makeBillboardBasisWgsl(orientation)}\nstruct VIn {\n@builtin(vertex_index) vid: u32,\n@location(0) iPos: vec3<f32>,\n@location(1) iSize: vec2<f32>,\n@location(2) iUvMin: vec2<f32>,\n@location(3) iUvMax: vec2<f32>,\n@location(4) iRot: f32,\n@location(5) iPivot: vec2<f32>,\n@location(6) iColor: vec4<f32>,\n};\nstruct VOut {\n@builtin(position) pos: vec4<f32>,\n@location(0) uv: vec2<f32>,\n@location(1) tint: vec4<f32>,\n@location(2) viewDist: f32,\n@location(3) vWorldPos: vec3<f32>,\n};\n@vertex\nfn vs(in: VIn) -> VOut {\nlet corner = vec2<f32>(select(0.0, 1.0, in.vid == 1u || in.vid == 2u), select(0.0, 1.0, in.vid >= 2u));\nlet local = (corner - in.iPivot) * in.iSize;\nlet cosRot = cos(in.iRot);\nlet sinRot = sin(in.iRot);\nlet rotated = vec2<f32>(local.x * cosRot - local.y * sinRot, local.x * sinRot + local.y * cosRot);\nlet basis = getBillboardBasis(in.iPos);\nlet worldPos = in.iPos + basis.right * rotated.x + basis.up * rotated.y;\nvar out: VOut;\nout.pos = scene.viewProjection * vec4<f32>(worldPos, 1.0);\nout.uv = mix(in.iUvMin, in.iUvMax, corner);\nout.tint = in.iColor;\nlet viewCenter = scene.view * vec4<f32>(in.iPos, 1.0);\nout.viewDist = length(viewCenter.xyz);\nout.vWorldPos = worldPos;\nreturn out;\n}\n@fragment\nfn fs(in: VOut) -> @location(0) vec4<f32> {\n${fragment}\n}`;\n}\n\n/**\n * The billboard custom-shader hook implementation. Lives only in this (tree-shaken) module, so the\n * always-loaded billboard path never names `_customShader` / `shaderParams`. Reads both off the\n * opaque `system` and delegates to the descriptor's underscore-prefixed (mangled) methods.\n */\nconst BILLBOARD_FX_HOOK: BillboardFxHook = {\n initSystem(system, opts) {\n const customShader = opts.customShader;\n if (customShader) {\n (system as { _customShader?: BillboardCustomShader })._customShader = customShader;\n system.shaderParams = [0, 0, 0, 0];\n }\n },\n pipelineKeyPart(system) {\n return system._customShader?._key ?? \"\";\n },\n shaderModule(engine, system) {\n return system._customShader?._getShaderModule(engine, system._orientation, system._depthMode) ?? null;\n },\n layoutEntries(system, startBinding) {\n return system._customShader?._layoutEntries(startBinding) ?? null;\n },\n createLayerFx(engine, label, system) {\n return system._customShader?._createLayerFx(engine, label) ?? null;\n },\n updateFx(fx, system, deltaMs) {\n fx.update(system.shaderParams ?? EMPTY_PARAMS, deltaMs);\n },\n bindEntries(fx, startBinding) {\n return fx.bindEntries(startBinding);\n },\n disposeFx(fx) {\n fx.destroy();\n },\n};\n\n/**\n * Build a custom-shader descriptor to pass as `customShader` when creating a billboard system.\n * The descriptor is opaque; the pipeline consumes it lazily.\n */\nexport function createBillboardCustomShader(options: BillboardCustomShaderOptions): BillboardCustomShader {\n _registerBillboardFxHook(BILLBOARD_FX_HOOK);\n const fragment = options.fragment;\n if (typeof fragment !== \"string\" || fragment.trim().length === 0) {\n throw new Error(\"createBillboardCustomShader: `fragment` must be a non-empty WGSL string.\");\n }\n const extraTextures = options.extraTextures ?? [];\n validateExtraTextureNames(\"createBillboardCustomShader\", extraTextures);\n const moduleCache = makeShaderModuleCache();\n return {\n _entityType: \"billboard-custom-shader\",\n _extraTextures: extraTextures,\n _key: nextCustomShaderKey(\"c\"),\n _composeWgsl: (orientation) => makeCustomBillboardWgsl(orientation, extraTextures, fragment),\n _getShaderModule: (engine, orientation, depthMode) =>\n moduleCache(engine, `${orientation}:${depthMode}`, () => makeCustomBillboardWgsl(orientation, extraTextures, fragment)),\n _layoutEntries: (startBinding) => makeCustomShaderLayoutEntries(extraTextures, startBinding),\n _createLayerFx: (engine, label) => createSpriteLayerFx(engine, label, extraTextures),\n };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"billboard-custom-shader.js","sources":["../../../src/sprite/billboard-custom-shader.ts"],"sourcesContent":["/**\n * Optional, tree-shakable custom-shader hook for `*BillboardSpriteSystem`.\n *\n * The default billboard pipeline bakes a fixed fragment (sample × tint, with an optional cutout\n * discard). Some scenes need a different per-fragment treatment — palette-indexed sampling,\n * COLORMAP light banding, toon shading, custom fog — and/or extra texture bindings beyond the\n * atlas. This module lets a caller supply a WGSL fragment body plus extra textures while the\n * billboard system keeps full ownership of geometry, instancing, sorting, and depth.\n *\n * Tree-shaking contract: the default billboard path never imports this module.\n * `billboard-pipeline.ts` only reaches the custom composer through the opaque object a caller\n * builds here via `createBillboardCustomShader`, so a scene that uses only stock billboards\n * pays zero bytes for this code.\n *\n * WGSL contract for the supplied `fragment` body:\n * - Receives `in: VOut` with: `uv: vec2<f32>`, `tint: vec4<f32>` (the per-sprite `color`),\n * `viewDist: f32` (distance from the camera to the sprite anchor in world units, constant\n * across the quad), `vWorldPos: vec3<f32>` (this fragment's world position).\n * - Has access to `atlasTex` / `atlasSamp` (the system atlas at group 1, bindings 1/2), each\n * extra texture as `<name>Tex` / `<name>Samp`, the `fx` UBO (`fx.time`, `fx.params`), and\n * the `billboards` system UBO (e.g. `billboards.opacityMul`).\n * - Must `return vec4<f32>(...)` (and may `discard`). No automatic cutout is injected — the\n * body owns all alpha handling.\n */\nimport { SCENE_UBO_WGSL } from \"../shader/scene-uniforms.js\";\nimport type { EngineContext } from \"../engine/engine.js\";\nimport type { BillboardDepthMode, BillboardOrientation } from \"./billboard-sprite.js\";\nimport { makeBillboardBasisWgsl } from \"./billboard-pipeline.js\";\nimport type { CustomShaderTexture, SpriteLayerFx } from \"./custom-shader-core.js\";\nimport {\n createSpriteLayerFx,\n EMPTY_PARAMS,\n makeCustomShaderLayoutEntries,\n makeExtraBindingsWgsl,\n makeFxStructWgsl,\n makeShaderModuleCache,\n nextCustomShaderKey,\n validateExtraTextureNames,\n} from \"./custom-shader-core.js\";\nimport type { BillboardFxHook } from \"./sprite-fx-hook.js\";\nimport { _registerBillboardFxHook } from \"./sprite-fx-hook.js\";\n\n/** One extra texture bound after the atlas (group 1, bindings 3, 5, 7, …). */\nexport type BillboardCustomTexture = CustomShaderTexture;\n\n/** Options for {@link createBillboardCustomShader}. */\nexport interface BillboardCustomShaderOptions {\n /** WGSL fragment body. See the module docs for the in-scope identifiers. */\n readonly fragment: string;\n /** Extra textures, in binding order. Each contributes a `texture_2d` + `sampler`. */\n readonly extraTextures?: readonly BillboardCustomTexture[];\n}\n\n/** Opaque custom-shader descriptor produced by {@link createBillboardCustomShader}. */\nexport interface BillboardCustomShader {\n /** @internal */\n readonly _entityType: \"billboard-custom-shader\";\n /** @internal Extra textures bound after the atlas. */\n readonly _extraTextures: readonly BillboardCustomTexture[];\n /** @internal Pipeline/shader-module cache discriminator. */\n readonly _key: string;\n /** @internal Builds the full WGSL for the given orientation (depth mode is irrelevant — the body owns alpha). */\n readonly _composeWgsl: (orientation: BillboardOrientation, depthMode: BillboardDepthMode) => string;\n /** @internal Compile + cache the `GPUShaderModule` for an orientation (owns its per-device cache). */\n readonly _getShaderModule: (engine: EngineContext, orientation: BillboardOrientation, depthMode: BillboardDepthMode) => GPUShaderModule;\n /** @internal Extra-texture + fx UBO bind-group **layout** entries, starting at `startBinding` (3). */\n readonly _layoutEntries: (startBinding: number) => GPUBindGroupLayoutEntry[];\n /** @internal Build the opaque per-system fx attachment (owns the `SpriteFx` UBO, scratch, and elapsed time). */\n readonly _createLayerFx: (engine: EngineContext, label: string) => SpriteLayerFx;\n}\n\nfunction makeCustomBillboardWgsl(orientation: BillboardOrientation, extraTextures: readonly BillboardCustomTexture[], fragment: string): string {\n const fxBinding = 3 + extraTextures.length * 2;\n return `${SCENE_UBO_WGSL}\nstruct S {\nopacityMul: vec4f,\naxisAndCutoff: vec4f,\n};\n@group(1) @binding(0) var<uniform> billboards: S;\n@group(1) @binding(1) var atlasTex: texture_2d<f32>;\n@group(1) @binding(2) var atlasSamp: sampler;\n${makeExtraBindingsWgsl(1, 3, extraTextures)}${makeFxStructWgsl(1, fxBinding)}\n${makeBillboardBasisWgsl(orientation)}\nstruct I {\n@builtin(vertex_index) vid: u32,\n@location(0) p: vec3f,\n@location(1) s: vec2f,\n@location(2) a: vec2f,\n@location(3) b: vec2f,\n@location(4) r: f32,\n@location(5) o: vec2f,\n@location(6) c: vec4f,\n};\nstruct O {\n@builtin(position) p: vec4f,\n@location(0) uv: vec2f,\n@location(1) tint: vec4f,\n@location(2) viewDist: f32,\n@location(3) vWorldPos: vec3f,\n};\n@vertex\nfn vs(in: I) -> O {\nlet q = vec2f(select(0.0, 1.0, in.vid == 1u || in.vid == 2u), select(0.0, 1.0, in.vid >= 2u));\nlet l = (q - in.o) * in.s;\nlet cr = cos(in.r);\nlet sr = sin(in.r);\nlet r = vec2f(l.x * cr - l.y * sr, l.x * sr + l.y * cr);\nlet b = basis(in.p);\nlet wp = in.p + b.r * r.x + b.u * r.y;\nvar out: O;\nout.p = scene.viewProjection * vec4f(wp, 1);\nout.uv = mix(in.a, in.b, q);\nout.tint = in.c;\nlet viewCenter = scene.view * vec4f(in.p, 1);\nout.viewDist = length(viewCenter.xyz);\nout.vWorldPos = wp;\nreturn out;\n}\n@fragment\nfn fs(in: O) -> @location(0) vec4f {\n${fragment}\n}`;\n}\n\n/**\n * The billboard custom-shader hook implementation. Lives only in this (tree-shaken) module, so the\n * always-loaded billboard path never names `_customShader` / `shaderParams`. Reads both off the\n * opaque `system` and delegates to the descriptor's underscore-prefixed (mangled) methods.\n */\nconst BILLBOARD_FX_HOOK: BillboardFxHook = {\n initSystem(system, opts) {\n const customShader = opts.customShader;\n if (customShader) {\n (system as { _customShader?: BillboardCustomShader })._customShader = customShader;\n system.shaderParams = [0, 0, 0, 0];\n }\n },\n pipelineKeyPart(system) {\n return system._customShader?._key ?? \"\";\n },\n shaderModule(engine, system) {\n return system._customShader?._getShaderModule(engine, system._orientation, system._depthMode) ?? null;\n },\n layoutEntries(system, startBinding) {\n return system._customShader?._layoutEntries(startBinding) ?? null;\n },\n createLayerFx(engine, label, system) {\n return system._customShader?._createLayerFx(engine, label) ?? null;\n },\n updateFx(fx, system, deltaMs) {\n fx.update(system.shaderParams ?? EMPTY_PARAMS, deltaMs);\n },\n bindEntries(fx, startBinding) {\n return fx.bindEntries(startBinding);\n },\n disposeFx(fx) {\n fx.destroy();\n },\n};\n\n/**\n * Build a custom-shader descriptor to pass as `customShader` when creating a billboard system.\n * The descriptor is opaque; the pipeline consumes it lazily.\n */\nexport function createBillboardCustomShader(options: BillboardCustomShaderOptions): BillboardCustomShader {\n _registerBillboardFxHook(BILLBOARD_FX_HOOK);\n const fragment = options.fragment;\n if (typeof fragment !== \"string\" || fragment.trim().length === 0) {\n throw new Error(\"createBillboardCustomShader: `fragment` must be a non-empty WGSL string.\");\n }\n const extraTextures = options.extraTextures ?? [];\n validateExtraTextureNames(\"createBillboardCustomShader\", extraTextures);\n const moduleCache = makeShaderModuleCache();\n return {\n _entityType: \"billboard-custom-shader\",\n _extraTextures: extraTextures,\n _key: nextCustomShaderKey(\"c\"),\n _composeWgsl: (orientation) => makeCustomBillboardWgsl(orientation, extraTextures, fragment),\n _getShaderModule: (engine, orientation, depthMode) =>\n moduleCache(engine, `${orientation}:${depthMode}`, () => makeCustomBillboardWgsl(orientation, extraTextures, fragment)),\n _layoutEntries: (startBinding) => makeCustomShaderLayoutEntries(extraTextures, startBinding),\n _createLayerFx: (engine, label) => createSpriteLayerFx(engine, label, extraTextures),\n };\n}\n"],"names":[],"mappings":"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"}
@@ -24,89 +24,87 @@ function getDepthModeEntry(depthMode) {
24
24
  function makeBillboardBasisWgsl(orientation) {
25
25
  switch (orientation) {
26
26
  case "facing":
27
- return `struct BillboardBasis {
28
- right: vec3<f32>,
29
- up: vec3<f32>,
27
+ return `struct B {
28
+ r: vec3f,
29
+ u: vec3f,
30
30
  };
31
- fn getBillboardBasis(_anchor: vec3<f32>) -> BillboardBasis {
32
- let cameraRight = normalize(vec3<f32>(scene.view[0][0], scene.view[1][0], scene.view[2][0]));
33
- let cameraUp = normalize(vec3<f32>(scene.view[0][1], scene.view[1][1], scene.view[2][1]));
34
- return BillboardBasis(cameraRight, -cameraUp);
31
+ fn basis(_a: vec3f) -> B {
32
+ let r = normalize(vec3f(scene.view[0][0], scene.view[1][0], scene.view[2][0]));
33
+ let u = normalize(vec3f(scene.view[0][1], scene.view[1][1], scene.view[2][1]));
34
+ return B(r, -u);
35
35
  }`;
36
36
  case "axis-locked":
37
- return `struct BillboardBasis {
38
- right: vec3<f32>,
39
- up: vec3<f32>,
37
+ return `struct B {
38
+ r: vec3f,
39
+ u: vec3f,
40
40
  };
41
- fn getBillboardBasis(_anchor: vec3<f32>) -> BillboardBasis {
42
- let lockAxis = normalize(billboards.axisAndCutoff.xyz);
43
- let cameraRight = normalize(vec3<f32>(scene.view[0][0], scene.view[1][0], scene.view[2][0]));
44
- let projectedRight = cameraRight - lockAxis * dot(cameraRight, lockAxis);
45
- let projectedRightLen = length(projectedRight);
46
- let safeProjectedRightLen = max(projectedRightLen, 1e-4);
47
- let fallbackSeed = select(vec3<f32>(0.0, 0.0, 1.0), vec3<f32>(1.0, 0.0, 0.0), abs(lockAxis.z) > 0.999);
48
- let fallbackRightRaw = cross(lockAxis, fallbackSeed);
49
- let fallbackRight = fallbackRightRaw / max(length(fallbackRightRaw), 1e-4);
50
- let right = select(fallbackRight, projectedRight / safeProjectedRightLen, projectedRightLen > 1e-4);
51
- return BillboardBasis(right, -lockAxis);
41
+ fn basis(_a: vec3f) -> B {
42
+ let a = normalize(billboards.axisAndCutoff.xyz);
43
+ let cr = normalize(vec3f(scene.view[0][0], scene.view[1][0], scene.view[2][0]));
44
+ let pr = cr - a * dot(cr, a);
45
+ let pl = length(pr);
46
+ let f = select(vec3f(0, 0, 1), vec3f(1, 0, 0), abs(a.z) > 0.999);
47
+ let fr = cross(a, f);
48
+ let r = select(fr / max(length(fr), 1e-4), pr / max(pl, 1e-4), pl > 1e-4);
49
+ return B(r, -a);
52
50
  }`;
53
51
  }
54
52
  }
55
53
  function makeBillboardFragmentWgsl(depthMode) {
56
54
  if (depthMode === "cutout") {
57
55
  return `@fragment
58
- fn fs(in: VOut) -> @location(0) vec4<f32> {
59
- let sampleColor = textureSample(atlasTex, atlasSamp, in.uv);
60
- if (sampleColor.a < billboards.axisAndCutoff.w) {
56
+ fn fs(in: O) -> @location(0) vec4f {
57
+ let s = textureSample(atlasTex, atlasSamp, in.uv);
58
+ if (s.a < billboards.axisAndCutoff.w) {
61
59
  discard;
62
60
  }
63
- return sampleColor * in.tint * billboards.opacityMul;
61
+ return s * in.tint * billboards.opacityMul;
64
62
  }`;
65
63
  }
66
64
  return `@fragment
67
- fn fs(in: VOut) -> @location(0) vec4<f32> {
68
- let sampleColor = textureSample(atlasTex, atlasSamp, in.uv);
69
- return sampleColor * in.tint * billboards.opacityMul;
65
+ fn fs(in: O) -> @location(0) vec4f {
66
+ let s = textureSample(atlasTex, atlasSamp, in.uv);
67
+ return s * in.tint * billboards.opacityMul;
70
68
  }`;
71
69
  }
72
70
  function makeBillboardWgsl(orientation, depthMode) {
73
71
  return `${SCENE_UBO_WGSL}
74
- struct BillboardSystem {
75
- opacityMul: vec4<f32>,
76
- axisAndCutoff: vec4<f32>,
72
+ struct S {
73
+ opacityMul: vec4f,
74
+ axisAndCutoff: vec4f,
77
75
  };
78
- @group(1) @binding(0) var<uniform> billboards: BillboardSystem;
76
+ @group(1) @binding(0) var<uniform> billboards: S;
79
77
  @group(1) @binding(1) var atlasTex: texture_2d<f32>;
80
78
  @group(1) @binding(2) var atlasSamp: sampler;
81
79
  ${makeBillboardBasisWgsl(orientation)}
82
- struct VIn {
80
+ struct I {
83
81
  @builtin(vertex_index) vid: u32,
84
- @location(0) iPos: vec3<f32>,
85
- @location(1) iSize: vec2<f32>,
86
- @location(2) iUvMin: vec2<f32>,
87
- @location(3) iUvMax: vec2<f32>,
88
- @location(4) iRot: f32,
89
- @location(5) iPivot: vec2<f32>,
90
- @location(6) iColor: vec4<f32>,
82
+ @location(0) p: vec3f,
83
+ @location(1) s: vec2f,
84
+ @location(2) a: vec2f,
85
+ @location(3) b: vec2f,
86
+ @location(4) r: f32,
87
+ @location(5) o: vec2f,
88
+ @location(6) c: vec4f,
91
89
  };
92
- struct VOut {
93
- @builtin(position) pos: vec4<f32>,
94
- @location(0) uv: vec2<f32>,
95
- @location(1) tint: vec4<f32>,
90
+ struct O {
91
+ @builtin(position) p: vec4f,
92
+ @location(0) uv: vec2f,
93
+ @location(1) tint: vec4f,
96
94
  };
97
95
  @vertex
98
- fn vs(in: VIn) -> VOut {
99
- let corner = vec2<f32>(select(0.0, 1.0, in.vid == 1u || in.vid == 2u), select(0.0, 1.0, in.vid >= 2u));
100
- let local = (corner - in.iPivot) * in.iSize;
101
- let cosRot = cos(in.iRot);
102
- let sinRot = sin(in.iRot);
103
- let rotated = vec2<f32>(local.x * cosRot - local.y * sinRot, local.x * sinRot + local.y * cosRot);
104
- let basis = getBillboardBasis(in.iPos);
105
- let worldPos = in.iPos + basis.right * rotated.x + basis.up * rotated.y;
106
- var out: VOut;
107
- out.pos = scene.viewProjection * vec4<f32>(worldPos, 1.0);
108
- out.uv = mix(in.iUvMin, in.iUvMax, corner);
109
- out.tint = in.iColor;
96
+ fn vs(in: I) -> O {
97
+ let q = vec2f(select(0.0, 1.0, in.vid == 1u || in.vid == 2u), select(0.0, 1.0, in.vid >= 2u));
98
+ let l = (q - in.o) * in.s;
99
+ let cr = cos(in.r);
100
+ let sr = sin(in.r);
101
+ let r = vec2f(l.x * cr - l.y * sr, l.x * sr + l.y * cr);
102
+ let b = basis(in.p);
103
+ let wp = in.p + b.r * r.x + b.u * r.y;
104
+ var out: O;
105
+ out.p = scene.viewProjection * vec4f(wp, 1);
106
+ out.uv = mix(in.a, in.b, q);
107
+ out.tint = in.c;
110
108
  return out;
111
109
  }
112
110
  ${makeBillboardFragmentWgsl(depthMode)}`;
@@ -1 +1 @@
1
- {"version":3,"file":"billboard-pipeline.js","sources":["../../../src/sprite/billboard-pipeline.ts"],"sourcesContent":["import { F32, U32, U16 } from \"../engine/typed-arrays.js\";\nimport { BU, SS, CW } from \"../engine/gpu-flags.js\";\nimport type { EngineContext } from \"../engine/engine.js\";\nimport type { Mat4 } from \"../math/types.js\";\nimport { SCENE_UBO_WGSL } from \"../shader/scene-uniforms.js\";\nimport type { BillboardDepthMode, BillboardOrientation, BillboardSpriteSystem } from \"./billboard-sprite.js\";\nimport type { SpriteLayerFx } from \"./custom-shader-core.js\";\nimport { _getBillboardFxHook } from \"./sprite-fx-hook.js\";\nimport { BILLBOARD_INSTANCE_FLOATS_PER_SPRITE, BILLBOARD_INSTANCE_STRIDE_BYTES } from \"./billboard-sprite.js\";\n\nexport interface BillboardPipelineDeviceCache {\n /** @internal */\n _shaderModules: Map<string, GPUShaderModule>;\n /** @internal */\n _pipelines: Map<string, GPURenderPipeline>;\n}\n\nexport interface BillboardPipelineCache {\n /** @internal */\n _devices: WeakMap<GPUDevice, BillboardPipelineDeviceCache>;\n}\n\nconst DEPTH_MODE_TABLE: Readonly<Record<BillboardDepthMode, { index: number; writeEnabled: boolean }>> = {\n transparent: { index: 0, writeEnabled: false },\n cutout: { index: 1, writeEnabled: true },\n};\n\nconst BILLBOARD_POSITION_OFFSET_BYTES = 0;\nconst BILLBOARD_SIZE_OFFSET_BYTES = 12;\nconst BILLBOARD_UV_MIN_OFFSET_BYTES = 20;\nconst BILLBOARD_UV_MAX_OFFSET_BYTES = 28;\nconst BILLBOARD_ROTATION_OFFSET_BYTES = 36;\nconst BILLBOARD_PIVOT_OFFSET_BYTES = 40;\nconst BILLBOARD_COLOR_OFFSET_BYTES = 48;\n\nexport const BILLBOARD_SYSTEM_UBO_BYTES = 32;\nconst BILLBOARD_SYSTEM_UBO_FLOATS = BILLBOARD_SYSTEM_UBO_BYTES / 4;\nexport const BILLBOARD_INDEX_DATA: Readonly<Uint16Array> = new U16([0, 1, 2, 0, 2, 3]);\n\nexport interface BillboardInstanceSortScratch {\n /** @internal */\n _capacity: number;\n /** @internal */\n _sortedInstanceData: Float32Array;\n /** @internal */\n _sortIndices: Uint32Array;\n /** @internal */\n _sortDepths: Float32Array;\n}\n\nfunction getDepthModeEntry(depthMode: BillboardDepthMode): (typeof DEPTH_MODE_TABLE)[BillboardDepthMode] {\n return DEPTH_MODE_TABLE[depthMode];\n}\n\n/** @internal Shared by the optional billboard custom-shader composer. */\nexport function makeBillboardBasisWgsl(orientation: BillboardOrientation): string {\n switch (orientation) {\n case \"facing\":\n return `struct BillboardBasis {\nright: vec3<f32>,\nup: vec3<f32>,\n};\nfn getBillboardBasis(_anchor: vec3<f32>) -> BillboardBasis {\nlet cameraRight = normalize(vec3<f32>(scene.view[0][0], scene.view[1][0], scene.view[2][0]));\nlet cameraUp = normalize(vec3<f32>(scene.view[0][1], scene.view[1][1], scene.view[2][1]));\nreturn BillboardBasis(cameraRight, -cameraUp);\n}`;\n case \"axis-locked\":\n return `struct BillboardBasis {\nright: vec3<f32>,\nup: vec3<f32>,\n};\nfn getBillboardBasis(_anchor: vec3<f32>) -> BillboardBasis {\nlet lockAxis = normalize(billboards.axisAndCutoff.xyz);\nlet cameraRight = normalize(vec3<f32>(scene.view[0][0], scene.view[1][0], scene.view[2][0]));\nlet projectedRight = cameraRight - lockAxis * dot(cameraRight, lockAxis);\nlet projectedRightLen = length(projectedRight);\nlet safeProjectedRightLen = max(projectedRightLen, 1e-4);\nlet fallbackSeed = select(vec3<f32>(0.0, 0.0, 1.0), vec3<f32>(1.0, 0.0, 0.0), abs(lockAxis.z) > 0.999);\nlet fallbackRightRaw = cross(lockAxis, fallbackSeed);\nlet fallbackRight = fallbackRightRaw / max(length(fallbackRightRaw), 1e-4);\nlet right = select(fallbackRight, projectedRight / safeProjectedRightLen, projectedRightLen > 1e-4);\nreturn BillboardBasis(right, -lockAxis);\n}`;\n }\n}\n\nfunction makeBillboardFragmentWgsl(depthMode: BillboardDepthMode): string {\n if (depthMode === \"cutout\") {\n return `@fragment\nfn fs(in: VOut) -> @location(0) vec4<f32> {\nlet sampleColor = textureSample(atlasTex, atlasSamp, in.uv);\nif (sampleColor.a < billboards.axisAndCutoff.w) {\ndiscard;\n}\nreturn sampleColor * in.tint * billboards.opacityMul;\n}`;\n }\n return `@fragment\nfn fs(in: VOut) -> @location(0) vec4<f32> {\nlet sampleColor = textureSample(atlasTex, atlasSamp, in.uv);\nreturn sampleColor * in.tint * billboards.opacityMul;\n}`;\n}\n\nfunction makeBillboardWgsl(orientation: BillboardOrientation, depthMode: BillboardDepthMode): string {\n return `${SCENE_UBO_WGSL}\nstruct BillboardSystem {\nopacityMul: vec4<f32>,\naxisAndCutoff: vec4<f32>,\n};\n@group(1) @binding(0) var<uniform> billboards: BillboardSystem;\n@group(1) @binding(1) var atlasTex: texture_2d<f32>;\n@group(1) @binding(2) var atlasSamp: sampler;\n${makeBillboardBasisWgsl(orientation)}\nstruct VIn {\n@builtin(vertex_index) vid: u32,\n@location(0) iPos: vec3<f32>,\n@location(1) iSize: vec2<f32>,\n@location(2) iUvMin: vec2<f32>,\n@location(3) iUvMax: vec2<f32>,\n@location(4) iRot: f32,\n@location(5) iPivot: vec2<f32>,\n@location(6) iColor: vec4<f32>,\n};\nstruct VOut {\n@builtin(position) pos: vec4<f32>,\n@location(0) uv: vec2<f32>,\n@location(1) tint: vec4<f32>,\n};\n@vertex\nfn vs(in: VIn) -> VOut {\nlet corner = vec2<f32>(select(0.0, 1.0, in.vid == 1u || in.vid == 2u), select(0.0, 1.0, in.vid >= 2u));\nlet local = (corner - in.iPivot) * in.iSize;\nlet cosRot = cos(in.iRot);\nlet sinRot = sin(in.iRot);\nlet rotated = vec2<f32>(local.x * cosRot - local.y * sinRot, local.x * sinRot + local.y * cosRot);\nlet basis = getBillboardBasis(in.iPos);\nlet worldPos = in.iPos + basis.right * rotated.x + basis.up * rotated.y;\nvar out: VOut;\nout.pos = scene.viewProjection * vec4<f32>(worldPos, 1.0);\nout.uv = mix(in.iUvMin, in.iUvMax, corner);\nout.tint = in.iColor;\nreturn out;\n}\n${makeBillboardFragmentWgsl(depthMode)}`;\n}\n\nexport function createBillboardPipelineCache(): BillboardPipelineCache {\n return {\n _devices: new WeakMap(),\n };\n}\n\nexport function resetBillboardPipelineCache(cache: BillboardPipelineCache): void {\n cache._devices = new WeakMap();\n}\n\nexport function getOrCreateBillboardPipeline(\n engine: EngineContext,\n cache: BillboardPipelineCache,\n format: GPUTextureFormat,\n sampleCount: 1 | 4,\n system: BillboardSpriteSystem,\n depthStencilFormat: GPUTextureFormat,\n sceneBindGroupLayout: GPUBindGroupLayout\n): GPURenderPipeline {\n const deviceCache = getBillboardPipelineDeviceCache(engine, cache);\n const depthEntry = getDepthModeEntry(system._depthMode);\n const customKey = _getBillboardFxHook()?.pipelineKeyPart(system) ?? \"\";\n const key = `${format}:${sampleCount}:${system._orientation}:${system.blendMode._key}:${depthEntry.index}:${depthStencilFormat}:${customKey}`;\n const cached = deviceCache._pipelines.get(key);\n if (cached) {\n return cached;\n }\n const pipeline = buildBillboardPipeline(engine, deviceCache, format, sampleCount, system, depthStencilFormat, sceneBindGroupLayout);\n deviceCache._pipelines.set(key, pipeline);\n return pipeline;\n}\n\nexport function createBillboardInstanceBuffer(device: GPUDevice, system: BillboardSpriteSystem, label?: string): GPUBuffer {\n return device.createBuffer({\n label,\n size: system._capacity * BILLBOARD_INSTANCE_STRIDE_BYTES,\n usage: BU.VERTEX | BU.COPY_DST,\n });\n}\n\nexport function createBillboardInstanceSortScratch(): BillboardInstanceSortScratch {\n return {\n _capacity: 0,\n _sortedInstanceData: new F32(0),\n _sortIndices: new U32(0),\n _sortDepths: new F32(0),\n };\n}\n\nexport function uploadSortedBillboardInstances(\n device: GPUDevice,\n system: BillboardSpriteSystem,\n instanceBuffer: GPUBuffer,\n scratch: BillboardInstanceSortScratch,\n cameraViewMatrix: Mat4,\n foX = 0,\n foY = 0,\n foZ = 0\n): void {\n const count = system.count;\n if (count === 0) {\n system._dirtyMin = 0;\n system._dirtyMax = 0;\n return;\n }\n ensureBillboardInstanceSortScratch(scratch, count);\n const sourceData = system._instanceData;\n const sortedData = scratch._sortedInstanceData;\n const indices = scratch._sortIndices;\n const depths = scratch._sortDepths;\n // Under floating origin the camera offset (foX/foY/foZ) is subtracted from each\n // anchor so the GPU receives eye-relative positions that match the eye-relative\n // view-projection. The sort depth is computed from the same eye-relative anchor.\n // With a zero offset this is identical to the raw-anchor path.\n for (let index = 0; index < count; index++) {\n const base = index * BILLBOARD_INSTANCE_FLOATS_PER_SPRITE;\n const anchorX = sourceData[base]! - foX;\n const anchorY = sourceData[base + 1]! - foY;\n const anchorZ = sourceData[base + 2]! - foZ;\n indices[index] = index;\n depths[index] = cameraViewMatrix[2]! * anchorX + cameraViewMatrix[6]! * anchorY + cameraViewMatrix[10]! * anchorZ + cameraViewMatrix[14]!;\n }\n indices.subarray(0, count).sort((left, right) => depths[right]! - depths[left]! || left - right);\n for (let outIndex = 0; outIndex < count; outIndex++) {\n const sourceBase = indices[outIndex]! * BILLBOARD_INSTANCE_FLOATS_PER_SPRITE;\n const destBase = outIndex * BILLBOARD_INSTANCE_FLOATS_PER_SPRITE;\n sortedData[destBase] = sourceData[sourceBase]! - foX;\n sortedData[destBase + 1] = sourceData[sourceBase + 1]! - foY;\n sortedData[destBase + 2] = sourceData[sourceBase + 2]! - foZ;\n for (let field = 3; field < BILLBOARD_INSTANCE_FLOATS_PER_SPRITE; field++) {\n sortedData[destBase + field] = sourceData[sourceBase + field]!;\n }\n }\n device.queue.writeBuffer(instanceBuffer, 0, sortedData.buffer, sortedData.byteOffset, count * BILLBOARD_INSTANCE_STRIDE_BYTES);\n system._dirtyMin = 0;\n system._dirtyMax = 0;\n}\n\nexport function ensureBillboardInstanceBuffer(\n device: GPUDevice,\n system: BillboardSpriteSystem,\n currentBuffer: GPUBuffer,\n currentCapacity: number,\n label?: string\n): { buffer: GPUBuffer; capacity: number; reallocated: boolean } {\n if (currentCapacity >= system._capacity) {\n return { buffer: currentBuffer, capacity: currentCapacity, reallocated: false };\n }\n currentBuffer.destroy();\n return { buffer: createBillboardInstanceBuffer(device, system, label), capacity: system._capacity, reallocated: true };\n}\n\nexport function uploadBillboardInstances(\n device: GPUDevice,\n system: BillboardSpriteSystem,\n instanceBuffer: GPUBuffer,\n uploadedVersion: number,\n foX = 0,\n foY = 0,\n foZ = 0,\n foScratch: BillboardInstanceSortScratch | null = null\n): number {\n if (uploadedVersion === system._version) {\n return uploadedVersion;\n }\n if (system.count === 0) {\n system._dirtyMin = 0;\n system._dirtyMax = 0;\n return system._version;\n }\n // Floating-origin path: anchors live at world scale (~5e6), so they must be made\n // eye-relative (offset subtracted) before upload — otherwise the eye-relative\n // view-projection in the shader would cancel catastrophically in F32. The whole\n // range is re-uploaded because every anchor depends on the live camera offset.\n if ((foX !== 0 || foY !== 0 || foZ !== 0) && foScratch) {\n const count = system.count;\n ensureBillboardInstanceSortScratch(foScratch, count);\n const sourceData = system._instanceData;\n const dest = foScratch._sortedInstanceData;\n for (let index = 0; index < count; index++) {\n const base = index * BILLBOARD_INSTANCE_FLOATS_PER_SPRITE;\n dest[base] = sourceData[base]! - foX;\n dest[base + 1] = sourceData[base + 1]! - foY;\n dest[base + 2] = sourceData[base + 2]! - foZ;\n for (let field = 3; field < BILLBOARD_INSTANCE_FLOATS_PER_SPRITE; field++) {\n dest[base + field] = sourceData[base + field]!;\n }\n }\n device.queue.writeBuffer(instanceBuffer, 0, dest.buffer, dest.byteOffset, count * BILLBOARD_INSTANCE_STRIDE_BYTES);\n system._dirtyMin = 0;\n system._dirtyMax = 0;\n return system._version;\n }\n let lowIndex: number;\n let highIndex: number;\n if (uploadedVersion === -1) {\n lowIndex = 0;\n highIndex = system.count;\n } else {\n lowIndex = system._dirtyMin;\n highIndex = Math.min(system._dirtyMax, system.count);\n }\n if (highIndex > lowIndex) {\n const offsetBytes = lowIndex * BILLBOARD_INSTANCE_STRIDE_BYTES;\n const byteLength = (highIndex - lowIndex) * BILLBOARD_INSTANCE_STRIDE_BYTES;\n device.queue.writeBuffer(instanceBuffer, offsetBytes, system._instanceData.buffer, system._instanceData.byteOffset + offsetBytes, byteLength);\n }\n system._dirtyMin = 0;\n system._dirtyMax = 0;\n return system._version;\n}\n\nfunction ensureBillboardInstanceSortScratch(scratch: BillboardInstanceSortScratch, count: number): void {\n if (scratch._capacity >= count) {\n return;\n }\n scratch._capacity = count;\n scratch._sortedInstanceData = new F32(count * BILLBOARD_INSTANCE_FLOATS_PER_SPRITE);\n scratch._sortIndices = new U32(count);\n scratch._sortDepths = new F32(count);\n}\n\nexport function buildBillboardSystemUbo(system: BillboardSpriteSystem, ubo: Float32Array): void {\n const opacity = system.opacity;\n if (system.blendMode._premultipliedOpacity) {\n ubo[0] = opacity;\n ubo[1] = opacity;\n ubo[2] = opacity;\n ubo[3] = opacity;\n } else {\n ubo[0] = 1;\n ubo[1] = 1;\n ubo[2] = 1;\n ubo[3] = opacity;\n }\n ubo[4] = system._axis[0];\n ubo[5] = system._axis[1];\n ubo[6] = system._axis[2];\n ubo[7] = system.alphaCutoff;\n}\n\nexport function writeBillboardSystemUboIfDirty(device: GPUDevice, uniformBuffer: GPUBuffer, scratchUbo: Float32Array, lastUbo: Float32Array, forceWrite: boolean): void {\n let dirty = forceWrite;\n if (!dirty) {\n for (let index = 0; index < BILLBOARD_SYSTEM_UBO_FLOATS; index++) {\n if (lastUbo[index] !== scratchUbo[index]) {\n dirty = true;\n break;\n }\n }\n }\n if (dirty) {\n device.queue.writeBuffer(uniformBuffer, 0, scratchUbo.buffer, scratchUbo.byteOffset, BILLBOARD_SYSTEM_UBO_BYTES);\n lastUbo.set(scratchUbo);\n }\n}\n\nexport function createBillboardSystemBindGroup(\n engine: EngineContext,\n pipeline: GPURenderPipeline,\n system: BillboardSpriteSystem,\n uniformBuffer: GPUBuffer,\n fx?: SpriteLayerFx | null\n): GPUBindGroup {\n const texture = system.atlas.texture;\n const entries: GPUBindGroupEntry[] = [\n { binding: 0, resource: { buffer: uniformBuffer } },\n { binding: 1, resource: texture.view },\n { binding: 2, resource: texture.sampler },\n ];\n if (fx) {\n for (const entry of _getBillboardFxHook()!.bindEntries(fx, 3)) {\n entries.push(entry);\n }\n }\n return engine._device.createBindGroup({\n layout: pipeline.getBindGroupLayout(1),\n entries,\n });\n}\n\nfunction getBillboardPipelineDeviceCache(engine: EngineContext, cache: BillboardPipelineCache): BillboardPipelineDeviceCache {\n let deviceCache = cache._devices.get(engine._device);\n if (!deviceCache) {\n deviceCache = { _shaderModules: new Map(), _pipelines: new Map() };\n cache._devices.set(engine._device, deviceCache);\n }\n return deviceCache;\n}\n\nfunction getShaderModule(engine: EngineContext, cache: BillboardPipelineDeviceCache, system: BillboardSpriteSystem): GPUShaderModule {\n const orientation = system._orientation;\n const depthMode = system._depthMode;\n const customModule = _getBillboardFxHook()?.shaderModule(engine, system);\n if (customModule) {\n return customModule;\n }\n const key = `${orientation}:${getDepthModeEntry(depthMode).index}`;\n let module = cache._shaderModules.get(key);\n if (!module) {\n module = engine._device.createShaderModule({ code: makeBillboardWgsl(orientation, depthMode) });\n cache._shaderModules.set(key, module);\n }\n return module;\n}\n\nfunction buildBillboardPipeline(\n engine: EngineContext,\n cache: BillboardPipelineDeviceCache,\n format: GPUTextureFormat,\n sampleCount: 1 | 4,\n system: BillboardSpriteSystem,\n depthStencilFormat: GPUTextureFormat,\n sceneBindGroupLayout: GPUBindGroupLayout\n): GPURenderPipeline {\n const device = engine._device;\n const depthEntry = getDepthModeEntry(system._depthMode);\n const shaderModule = getShaderModule(engine, cache, system);\n const layoutEntries: GPUBindGroupLayoutEntry[] = [\n { binding: 0, visibility: SS.VERTEX | SS.FRAGMENT, buffer: { type: \"uniform\" } },\n { binding: 1, visibility: SS.FRAGMENT, texture: { sampleType: \"float\" } },\n { binding: 2, visibility: SS.FRAGMENT, sampler: { type: \"filtering\" } },\n ];\n const extraLayoutEntries = _getBillboardFxHook()?.layoutEntries(system, 3);\n if (extraLayoutEntries) {\n for (const entry of extraLayoutEntries) {\n layoutEntries.push(entry);\n }\n }\n const billboardBindGroupLayout = device.createBindGroupLayout({ entries: layoutEntries });\n return device.createRenderPipeline({\n label: `${system._orientation}-billboard-sprite-pipeline`,\n layout: device.createPipelineLayout({ bindGroupLayouts: [sceneBindGroupLayout, billboardBindGroupLayout] }),\n vertex: {\n module: shaderModule,\n entryPoint: \"vs\",\n buffers: [\n {\n arrayStride: BILLBOARD_INSTANCE_STRIDE_BYTES,\n stepMode: \"instance\",\n attributes: [\n { shaderLocation: 0, offset: BILLBOARD_POSITION_OFFSET_BYTES, format: \"float32x3\" },\n { shaderLocation: 1, offset: BILLBOARD_SIZE_OFFSET_BYTES, format: \"float32x2\" },\n { shaderLocation: 2, offset: BILLBOARD_UV_MIN_OFFSET_BYTES, format: \"float32x2\" },\n { shaderLocation: 3, offset: BILLBOARD_UV_MAX_OFFSET_BYTES, format: \"float32x2\" },\n { shaderLocation: 4, offset: BILLBOARD_ROTATION_OFFSET_BYTES, format: \"float32\" },\n { shaderLocation: 5, offset: BILLBOARD_PIVOT_OFFSET_BYTES, format: \"float32x2\" },\n { shaderLocation: 6, offset: BILLBOARD_COLOR_OFFSET_BYTES, format: \"float32x4\" },\n ],\n },\n ],\n },\n fragment: {\n module: shaderModule,\n entryPoint: \"fs\",\n targets: [system.blendMode._descriptor ? { format, blend: system.blendMode._descriptor, writeMask: CW.ALL } : { format, writeMask: CW.ALL }],\n },\n primitive: { topology: \"triangle-list\", cullMode: \"none\" },\n depthStencil: { format: depthStencilFormat, depthCompare: \"greater-equal\", depthWriteEnabled: depthEntry.writeEnabled },\n multisample: { count: sampleCount },\n });\n}\n"],"names":[],"mappings":";;;;;;AAsBA,MAAM,gBAAA,GAAmG;AAAA,EACrG,WAAA,EAAa,EAAE,KAAA,EAAO,CAAA,EAAG,cAAc,KAAA,EAAM;AAAA,EAC7C,MAAA,EAAQ,EAAE,KAAA,EAAO,CAAA,EAAG,cAAc,IAAA;AACtC,CAAA;AAEA,MAAM,+BAAA,GAAkC,CAAA;AACxC,MAAM,2BAAA,GAA8B,EAAA;AACpC,MAAM,6BAAA,GAAgC,EAAA;AACtC,MAAM,6BAAA,GAAgC,EAAA;AACtC,MAAM,+BAAA,GAAkC,EAAA;AACxC,MAAM,4BAAA,GAA+B,EAAA;AACrC,MAAM,4BAAA,GAA+B,EAAA;AAE9B,MAAM,0BAAA,GAA6B;AAC1C,MAAM,8BAA8B,0BAAA,GAA6B,CAAA;AAC1D,MAAM,oBAAA,GAA8C,IAAI,GAAA,CAAI,CAAC,CAAA,EAAG,GAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAC,CAAC;AAarF,SAAS,kBAAkB,SAAA,EAA8E;AACrG,EAAA,OAAO,iBAAiB,SAAS,CAAA;AACrC;AAGO,SAAS,uBAAuB,WAAA,EAA2C;AAC9E,EAAA,QAAQ,WAAA;AAAa,IACjB,KAAK,QAAA;AACD,MAAA,OAAO,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA,CAAA;AAAA,IASX,KAAK,aAAA;AACD,MAAA,OAAO,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA,CAAA;AAAA;AAiBnB;AAEA,SAAS,0BAA0B,SAAA,EAAuC;AACtE,EAAA,IAAI,cAAc,QAAA,EAAU;AACxB,IAAA,OAAO,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA,CAAA;AAAA,EAQX;AACA,EAAA,OAAO,CAAA;AAAA;AAAA;AAAA;AAAA,CAAA,CAAA;AAKX;AAEA,SAAS,iBAAA,CAAkB,aAAmC,SAAA,EAAuC;AACjG,EAAA,OAAO,GAAG,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQ1B,sBAAA,CAAuB,WAAW,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA+BnC,yBAAA,CAA0B,SAAS,CAAC,CAAA,CAAA;AACtC;AAEO,SAAS,4BAAA,GAAuD;AACnE,EAAA,OAAO;AAAA,IACH,QAAA,sBAAc,OAAA;AAAQ,GAC1B;AACJ;AAEO,SAAS,4BAA4B,KAAA,EAAqC;AAC7E,EAAA,KAAA,CAAM,QAAA,uBAAe,OAAA,EAAQ;AACjC;AAEO,SAAS,6BACZ,MAAA,EACA,KAAA,EACA,QACA,WAAA,EACA,MAAA,EACA,oBACA,oBAAA,EACiB;AACjB,EAAA,MAAM,WAAA,GAAc,+BAAA,CAAgC,MAAA,EAAQ,KAAK,CAAA;AACjE,EAAA,MAAM,UAAA,GAAa,iBAAA,CAAkB,MAAA,CAAO,UAAU,CAAA;AACtD,EAAA,MAAM,SAAA,GAAY,mBAAA,EAAoB,EAAG,eAAA,CAAgB,MAAM,CAAA,IAAK,EAAA;AACpE,EAAA,MAAM,MAAM,CAAA,EAAG,MAAM,IAAI,WAAW,CAAA,CAAA,EAAI,OAAO,YAAY,CAAA,CAAA,EAAI,MAAA,CAAO,SAAA,CAAU,IAAI,CAAA,CAAA,EAAI,UAAA,CAAW,KAAK,CAAA,CAAA,EAAI,kBAAkB,IAAI,SAAS,CAAA,CAAA;AAC3I,EAAA,MAAM,MAAA,GAAS,WAAA,CAAY,UAAA,CAAW,GAAA,CAAI,GAAG,CAAA;AAC7C,EAAA,IAAI,MAAA,EAAQ;AACR,IAAA,OAAO,MAAA;AAAA,EACX;AACA,EAAA,MAAM,QAAA,GAAW,uBAAuB,MAAA,EAAQ,WAAA,EAAa,QAAQ,WAAA,EAAa,MAAA,EAAQ,oBAAoB,oBAAoB,CAAA;AAClI,EAAA,WAAA,CAAY,UAAA,CAAW,GAAA,CAAI,GAAA,EAAK,QAAQ,CAAA;AACxC,EAAA,OAAO,QAAA;AACX;AAEO,SAAS,6BAAA,CAA8B,MAAA,EAAmB,MAAA,EAA+B,KAAA,EAA2B;AACvH,EAAA,OAAO,OAAO,YAAA,CAAa;AAAA,IACvB,KAAA;AAAA,IACA,IAAA,EAAM,OAAO,SAAA,GAAY,+BAAA;AAAA,IACzB,KAAA,EAAO,EAAA,CAAG,MAAA,GAAS,EAAA,CAAG;AAAA,GACzB,CAAA;AACL;AAEO,SAAS,kCAAA,GAAmE;AAC/E,EAAA,OAAO;AAAA,IACH,SAAA,EAAW,CAAA;AAAA,IACX,mBAAA,EAAqB,IAAI,GAAA,CAAI,CAAC,CAAA;AAAA,IAC9B,YAAA,EAAc,IAAI,GAAA,CAAI,CAAC,CAAA;AAAA,IACvB,WAAA,EAAa,IAAI,GAAA,CAAI,CAAC;AAAA,GAC1B;AACJ;AAEO,SAAS,8BAAA,CACZ,MAAA,EACA,MAAA,EACA,cAAA,EACA,OAAA,EACA,gBAAA,EACA,GAAA,GAAM,CAAA,EACN,GAAA,GAAM,CAAA,EACN,GAAA,GAAM,CAAA,EACF;AACJ,EAAA,MAAM,QAAQ,MAAA,CAAO,KAAA;AACrB,EAAA,IAAI,UAAU,CAAA,EAAG;AACb,IAAA,MAAA,CAAO,SAAA,GAAY,CAAA;AACnB,IAAA,MAAA,CAAO,SAAA,GAAY,CAAA;AACnB,IAAA;AAAA,EACJ;AACA,EAAA,kCAAA,CAAmC,SAAS,KAAK,CAAA;AACjD,EAAA,MAAM,aAAa,MAAA,CAAO,aAAA;AAC1B,EAAA,MAAM,aAAa,OAAA,CAAQ,mBAAA;AAC3B,EAAA,MAAM,UAAU,OAAA,CAAQ,YAAA;AACxB,EAAA,MAAM,SAAS,OAAA,CAAQ,WAAA;AAKvB,EAAA,KAAA,IAAS,KAAA,GAAQ,CAAA,EAAG,KAAA,GAAQ,KAAA,EAAO,KAAA,EAAA,EAAS;AACxC,IAAA,MAAM,OAAO,KAAA,GAAQ,oCAAA;AACrB,IAAA,MAAM,OAAA,GAAU,UAAA,CAAW,IAAI,CAAA,GAAK,GAAA;AACpC,IAAA,MAAM,OAAA,GAAU,UAAA,CAAW,IAAA,GAAO,CAAC,CAAA,GAAK,GAAA;AACxC,IAAA,MAAM,OAAA,GAAU,UAAA,CAAW,IAAA,GAAO,CAAC,CAAA,GAAK,GAAA;AACxC,IAAA,OAAA,CAAQ,KAAK,CAAA,GAAI,KAAA;AACjB,IAAA,MAAA,CAAO,KAAK,CAAA,GAAI,gBAAA,CAAiB,CAAC,IAAK,OAAA,GAAU,gBAAA,CAAiB,CAAC,CAAA,GAAK,UAAU,gBAAA,CAAiB,EAAE,CAAA,GAAK,OAAA,GAAU,iBAAiB,EAAE,CAAA;AAAA,EAC3I;AACA,EAAA,OAAA,CAAQ,QAAA,CAAS,CAAA,EAAG,KAAK,CAAA,CAAE,KAAK,CAAC,IAAA,EAAM,KAAA,KAAU,MAAA,CAAO,KAAK,CAAA,GAAK,MAAA,CAAO,IAAI,CAAA,IAAM,OAAO,KAAK,CAAA;AAC/F,EAAA,KAAA,IAAS,QAAA,GAAW,CAAA,EAAG,QAAA,GAAW,KAAA,EAAO,QAAA,EAAA,EAAY;AACjD,IAAA,MAAM,UAAA,GAAa,OAAA,CAAQ,QAAQ,CAAA,GAAK,oCAAA;AACxC,IAAA,MAAM,WAAW,QAAA,GAAW,oCAAA;AAC5B,IAAA,UAAA,CAAW,QAAQ,CAAA,GAAI,UAAA,CAAW,UAAU,CAAA,GAAK,GAAA;AACjD,IAAA,UAAA,CAAW,WAAW,CAAC,CAAA,GAAI,UAAA,CAAW,UAAA,GAAa,CAAC,CAAA,GAAK,GAAA;AACzD,IAAA,UAAA,CAAW,WAAW,CAAC,CAAA,GAAI,UAAA,CAAW,UAAA,GAAa,CAAC,CAAA,GAAK,GAAA;AACzD,IAAA,KAAA,IAAS,KAAA,GAAQ,CAAA,EAAG,KAAA,GAAQ,oCAAA,EAAsC,KAAA,EAAA,EAAS;AACvE,MAAA,UAAA,CAAW,QAAA,GAAW,KAAK,CAAA,GAAI,UAAA,CAAW,aAAa,KAAK,CAAA;AAAA,IAChE;AAAA,EACJ;AACA,EAAA,MAAA,CAAO,KAAA,CAAM,YAAY,cAAA,EAAgB,CAAA,EAAG,WAAW,MAAA,EAAQ,UAAA,CAAW,UAAA,EAAY,KAAA,GAAQ,+BAA+B,CAAA;AAC7H,EAAA,MAAA,CAAO,SAAA,GAAY,CAAA;AACnB,EAAA,MAAA,CAAO,SAAA,GAAY,CAAA;AACvB;AAEO,SAAS,6BAAA,CACZ,MAAA,EACA,MAAA,EACA,aAAA,EACA,iBACA,KAAA,EAC6D;AAC7D,EAAA,IAAI,eAAA,IAAmB,OAAO,SAAA,EAAW;AACrC,IAAA,OAAO,EAAE,MAAA,EAAQ,aAAA,EAAe,QAAA,EAAU,eAAA,EAAiB,aAAa,KAAA,EAAM;AAAA,EAClF;AACA,EAAA,aAAA,CAAc,OAAA,EAAQ;AACtB,EAAA,OAAO,EAAE,MAAA,EAAQ,6BAAA,CAA8B,MAAA,EAAQ,MAAA,EAAQ,KAAK,CAAA,EAAG,QAAA,EAAU,MAAA,CAAO,SAAA,EAAW,WAAA,EAAa,IAAA,EAAK;AACzH;AAEO,SAAS,wBAAA,CACZ,MAAA,EACA,MAAA,EACA,cAAA,EACA,eAAA,EACA,GAAA,GAAM,CAAA,EACN,GAAA,GAAM,CAAA,EACN,GAAA,GAAM,CAAA,EACN,SAAA,GAAiD,IAAA,EAC3C;AACN,EAAA,IAAI,eAAA,KAAoB,OAAO,QAAA,EAAU;AACrC,IAAA,OAAO,eAAA;AAAA,EACX;AACA,EAAA,IAAI,MAAA,CAAO,UAAU,CAAA,EAAG;AACpB,IAAA,MAAA,CAAO,SAAA,GAAY,CAAA;AACnB,IAAA,MAAA,CAAO,SAAA,GAAY,CAAA;AACnB,IAAA,OAAO,MAAA,CAAO,QAAA;AAAA,EAClB;AAKA,EAAA,IAAA,CAAK,QAAQ,CAAA,IAAK,GAAA,KAAQ,CAAA,IAAK,GAAA,KAAQ,MAAM,SAAA,EAAW;AACpD,IAAA,MAAM,QAAQ,MAAA,CAAO,KAAA;AACrB,IAAA,kCAAA,CAAmC,WAAW,KAAK,CAAA;AACnD,IAAA,MAAM,aAAa,MAAA,CAAO,aAAA;AAC1B,IAAA,MAAM,OAAO,SAAA,CAAU,mBAAA;AACvB,IAAA,KAAA,IAAS,KAAA,GAAQ,CAAA,EAAG,KAAA,GAAQ,KAAA,EAAO,KAAA,EAAA,EAAS;AACxC,MAAA,MAAM,OAAO,KAAA,GAAQ,oCAAA;AACrB,MAAA,IAAA,CAAK,IAAI,CAAA,GAAI,UAAA,CAAW,IAAI,CAAA,GAAK,GAAA;AACjC,MAAA,IAAA,CAAK,OAAO,CAAC,CAAA,GAAI,UAAA,CAAW,IAAA,GAAO,CAAC,CAAA,GAAK,GAAA;AACzC,MAAA,IAAA,CAAK,OAAO,CAAC,CAAA,GAAI,UAAA,CAAW,IAAA,GAAO,CAAC,CAAA,GAAK,GAAA;AACzC,MAAA,KAAA,IAAS,KAAA,GAAQ,CAAA,EAAG,KAAA,GAAQ,oCAAA,EAAsC,KAAA,EAAA,EAAS;AACvE,QAAA,IAAA,CAAK,IAAA,GAAO,KAAK,CAAA,GAAI,UAAA,CAAW,OAAO,KAAK,CAAA;AAAA,MAChD;AAAA,IACJ;AACA,IAAA,MAAA,CAAO,KAAA,CAAM,YAAY,cAAA,EAAgB,CAAA,EAAG,KAAK,MAAA,EAAQ,IAAA,CAAK,UAAA,EAAY,KAAA,GAAQ,+BAA+B,CAAA;AACjH,IAAA,MAAA,CAAO,SAAA,GAAY,CAAA;AACnB,IAAA,MAAA,CAAO,SAAA,GAAY,CAAA;AACnB,IAAA,OAAO,MAAA,CAAO,QAAA;AAAA,EAClB;AACA,EAAA,IAAI,QAAA;AACJ,EAAA,IAAI,SAAA;AACJ,EAAA,IAAI,oBAAoB,EAAA,EAAI;AACxB,IAAA,QAAA,GAAW,CAAA;AACX,IAAA,SAAA,GAAY,MAAA,CAAO,KAAA;AAAA,EACvB,CAAA,MAAO;AACH,IAAA,QAAA,GAAW,MAAA,CAAO,SAAA;AAClB,IAAA,SAAA,GAAY,IAAA,CAAK,GAAA,CAAI,MAAA,CAAO,SAAA,EAAW,OAAO,KAAK,CAAA;AAAA,EACvD;AACA,EAAA,IAAI,YAAY,QAAA,EAAU;AACtB,IAAA,MAAM,cAAc,QAAA,GAAW,+BAAA;AAC/B,IAAA,MAAM,UAAA,GAAA,CAAc,YAAY,QAAA,IAAY,+BAAA;AAC5C,IAAA,MAAA,CAAO,KAAA,CAAM,WAAA,CAAY,cAAA,EAAgB,WAAA,EAAa,MAAA,CAAO,aAAA,CAAc,MAAA,EAAQ,MAAA,CAAO,aAAA,CAAc,UAAA,GAAa,WAAA,EAAa,UAAU,CAAA;AAAA,EAChJ;AACA,EAAA,MAAA,CAAO,SAAA,GAAY,CAAA;AACnB,EAAA,MAAA,CAAO,SAAA,GAAY,CAAA;AACnB,EAAA,OAAO,MAAA,CAAO,QAAA;AAClB;AAEA,SAAS,kCAAA,CAAmC,SAAuC,KAAA,EAAqB;AACpG,EAAA,IAAI,OAAA,CAAQ,aAAa,KAAA,EAAO;AAC5B,IAAA;AAAA,EACJ;AACA,EAAA,OAAA,CAAQ,SAAA,GAAY,KAAA;AACpB,EAAA,OAAA,CAAQ,mBAAA,GAAsB,IAAI,GAAA,CAAI,KAAA,GAAQ,oCAAoC,CAAA;AAClF,EAAA,OAAA,CAAQ,YAAA,GAAe,IAAI,GAAA,CAAI,KAAK,CAAA;AACpC,EAAA,OAAA,CAAQ,WAAA,GAAc,IAAI,GAAA,CAAI,KAAK,CAAA;AACvC;AAEO,SAAS,uBAAA,CAAwB,QAA+B,GAAA,EAAyB;AAC5F,EAAA,MAAM,UAAU,MAAA,CAAO,OAAA;AACvB,EAAA,IAAI,MAAA,CAAO,UAAU,qBAAA,EAAuB;AACxC,IAAA,GAAA,CAAI,CAAC,CAAA,GAAI,OAAA;AACT,IAAA,GAAA,CAAI,CAAC,CAAA,GAAI,OAAA;AACT,IAAA,GAAA,CAAI,CAAC,CAAA,GAAI,OAAA;AACT,IAAA,GAAA,CAAI,CAAC,CAAA,GAAI,OAAA;AAAA,EACb,CAAA,MAAO;AACH,IAAA,GAAA,CAAI,CAAC,CAAA,GAAI,CAAA;AACT,IAAA,GAAA,CAAI,CAAC,CAAA,GAAI,CAAA;AACT,IAAA,GAAA,CAAI,CAAC,CAAA,GAAI,CAAA;AACT,IAAA,GAAA,CAAI,CAAC,CAAA,GAAI,OAAA;AAAA,EACb;AACA,EAAA,GAAA,CAAI,CAAC,CAAA,GAAI,MAAA,CAAO,KAAA,CAAM,CAAC,CAAA;AACvB,EAAA,GAAA,CAAI,CAAC,CAAA,GAAI,MAAA,CAAO,KAAA,CAAM,CAAC,CAAA;AACvB,EAAA,GAAA,CAAI,CAAC,CAAA,GAAI,MAAA,CAAO,KAAA,CAAM,CAAC,CAAA;AACvB,EAAA,GAAA,CAAI,CAAC,IAAI,MAAA,CAAO,WAAA;AACpB;AAEO,SAAS,8BAAA,CAA+B,MAAA,EAAmB,aAAA,EAA0B,UAAA,EAA0B,SAAuB,UAAA,EAA2B;AACpK,EAAA,IAAI,KAAA,GAAQ,UAAA;AACZ,EAAA,IAAI,CAAC,KAAA,EAAO;AACR,IAAA,KAAA,IAAS,KAAA,GAAQ,CAAA,EAAG,KAAA,GAAQ,2BAAA,EAA6B,KAAA,EAAA,EAAS;AAC9D,MAAA,IAAI,OAAA,CAAQ,KAAK,CAAA,KAAM,UAAA,CAAW,KAAK,CAAA,EAAG;AACtC,QAAA,KAAA,GAAQ,IAAA;AACR,QAAA;AAAA,MACJ;AAAA,IACJ;AAAA,EACJ;AACA,EAAA,IAAI,KAAA,EAAO;AACP,IAAA,MAAA,CAAO,KAAA,CAAM,YAAY,aAAA,EAAe,CAAA,EAAG,WAAW,MAAA,EAAQ,UAAA,CAAW,YAAY,0BAA0B,CAAA;AAC/G,IAAA,OAAA,CAAQ,IAAI,UAAU,CAAA;AAAA,EAC1B;AACJ;AAEO,SAAS,8BAAA,CACZ,MAAA,EACA,QAAA,EACA,MAAA,EACA,eACA,EAAA,EACY;AACZ,EAAA,MAAM,OAAA,GAAU,OAAO,KAAA,CAAM,OAAA;AAC7B,EAAA,MAAM,OAAA,GAA+B;AAAA,IACjC,EAAE,OAAA,EAAS,CAAA,EAAG,UAAU,EAAE,MAAA,EAAQ,eAAc,EAAE;AAAA,IAClD,EAAE,OAAA,EAAS,CAAA,EAAG,QAAA,EAAU,QAAQ,IAAA,EAAK;AAAA,IACrC,EAAE,OAAA,EAAS,CAAA,EAAG,QAAA,EAAU,QAAQ,OAAA;AAAQ,GAC5C;AACA,EAAA,IAAI,EAAA,EAAI;AACJ,IAAA,KAAA,MAAW,SAAS,mBAAA,EAAoB,CAAG,WAAA,CAAY,EAAA,EAAI,CAAC,CAAA,EAAG;AAC3D,MAAA,OAAA,CAAQ,KAAK,KAAK,CAAA;AAAA,IACtB;AAAA,EACJ;AACA,EAAA,OAAO,MAAA,CAAO,QAAQ,eAAA,CAAgB;AAAA,IAClC,MAAA,EAAQ,QAAA,CAAS,kBAAA,CAAmB,CAAC,CAAA;AAAA,IACrC;AAAA,GACH,CAAA;AACL;AAEA,SAAS,+BAAA,CAAgC,QAAuB,KAAA,EAA6D;AACzH,EAAA,IAAI,WAAA,GAAc,KAAA,CAAM,QAAA,CAAS,GAAA,CAAI,OAAO,OAAO,CAAA;AACnD,EAAA,IAAI,CAAC,WAAA,EAAa;AACd,IAAA,WAAA,GAAc,EAAE,gCAAgB,IAAI,GAAA,IAAO,UAAA,kBAAY,IAAI,KAAI,EAAE;AACjE,IAAA,KAAA,CAAM,QAAA,CAAS,GAAA,CAAI,MAAA,CAAO,OAAA,EAAS,WAAW,CAAA;AAAA,EAClD;AACA,EAAA,OAAO,WAAA;AACX;AAEA,SAAS,eAAA,CAAgB,MAAA,EAAuB,KAAA,EAAqC,MAAA,EAAgD;AACjI,EAAA,MAAM,cAAc,MAAA,CAAO,YAAA;AAC3B,EAAA,MAAM,YAAY,MAAA,CAAO,UAAA;AACzB,EAAA,MAAM,YAAA,GAAe,mBAAA,EAAoB,EAAG,YAAA,CAAa,QAAQ,MAAM,CAAA;AACvE,EAAA,IAAI,YAAA,EAAc;AACd,IAAA,OAAO,YAAA;AAAA,EACX;AACA,EAAA,MAAM,MAAM,CAAA,EAAG,WAAW,IAAI,iBAAA,CAAkB,SAAS,EAAE,KAAK,CAAA,CAAA;AAChE,EAAA,IAAI,MAAA,GAAS,KAAA,CAAM,cAAA,CAAe,GAAA,CAAI,GAAG,CAAA;AACzC,EAAA,IAAI,CAAC,MAAA,EAAQ;AACT,IAAA,MAAA,GAAS,MAAA,CAAO,QAAQ,kBAAA,CAAmB,EAAE,MAAM,iBAAA,CAAkB,WAAA,EAAa,SAAS,CAAA,EAAG,CAAA;AAC9F,IAAA,KAAA,CAAM,cAAA,CAAe,GAAA,CAAI,GAAA,EAAK,MAAM,CAAA;AAAA,EACxC;AACA,EAAA,OAAO,MAAA;AACX;AAEA,SAAS,uBACL,MAAA,EACA,KAAA,EACA,QACA,WAAA,EACA,MAAA,EACA,oBACA,oBAAA,EACiB;AACjB,EAAA,MAAM,SAAS,MAAA,CAAO,OAAA;AACtB,EAAA,MAAM,UAAA,GAAa,iBAAA,CAAkB,MAAA,CAAO,UAAU,CAAA;AACtD,EAAA,MAAM,YAAA,GAAe,eAAA,CAAgB,MAAA,EAAQ,KAAA,EAAO,MAAM,CAAA;AAC1D,EAAA,MAAM,aAAA,GAA2C;AAAA,IAC7C,EAAE,OAAA,EAAS,CAAA,EAAG,UAAA,EAAY,EAAA,CAAG,MAAA,GAAS,EAAA,CAAG,QAAA,EAAU,MAAA,EAAQ,EAAE,IAAA,EAAM,SAAA,EAAU,EAAE;AAAA,IAC/E,EAAE,OAAA,EAAS,CAAA,EAAG,UAAA,EAAY,EAAA,CAAG,UAAU,OAAA,EAAS,EAAE,UAAA,EAAY,OAAA,EAAQ,EAAE;AAAA,IACxE,EAAE,OAAA,EAAS,CAAA,EAAG,UAAA,EAAY,EAAA,CAAG,UAAU,OAAA,EAAS,EAAE,IAAA,EAAM,WAAA,EAAY;AAAE,GAC1E;AACA,EAAA,MAAM,kBAAA,GAAqB,mBAAA,EAAoB,EAAG,aAAA,CAAc,QAAQ,CAAC,CAAA;AACzE,EAAA,IAAI,kBAAA,EAAoB;AACpB,IAAA,KAAA,MAAW,SAAS,kBAAA,EAAoB;AACpC,MAAA,aAAA,CAAc,KAAK,KAAK,CAAA;AAAA,IAC5B;AAAA,EACJ;AACA,EAAA,MAAM,2BAA2B,MAAA,CAAO,qBAAA,CAAsB,EAAE,OAAA,EAAS,eAAe,CAAA;AACxF,EAAA,OAAO,OAAO,oBAAA,CAAqB;AAAA,IAC/B,KAAA,EAAO,CAAA,EAAG,MAAA,CAAO,YAAY,CAAA,0BAAA,CAAA;AAAA,IAC7B,MAAA,EAAQ,OAAO,oBAAA,CAAqB,EAAE,kBAAkB,CAAC,oBAAA,EAAsB,wBAAwB,CAAA,EAAG,CAAA;AAAA,IAC1G,MAAA,EAAQ;AAAA,MACJ,MAAA,EAAQ,YAAA;AAAA,MACR,UAAA,EAAY,IAAA;AAAA,MACZ,OAAA,EAAS;AAAA,QACL;AAAA,UACI,WAAA,EAAa,+BAAA;AAAA,UACb,QAAA,EAAU,UAAA;AAAA,UACV,UAAA,EAAY;AAAA,YACR,EAAE,cAAA,EAAgB,CAAA,EAAG,MAAA,EAAQ,+BAAA,EAAiC,QAAQ,WAAA,EAAY;AAAA,YAClF,EAAE,cAAA,EAAgB,CAAA,EAAG,MAAA,EAAQ,2BAAA,EAA6B,QAAQ,WAAA,EAAY;AAAA,YAC9E,EAAE,cAAA,EAAgB,CAAA,EAAG,MAAA,EAAQ,6BAAA,EAA+B,QAAQ,WAAA,EAAY;AAAA,YAChF,EAAE,cAAA,EAAgB,CAAA,EAAG,MAAA,EAAQ,6BAAA,EAA+B,QAAQ,WAAA,EAAY;AAAA,YAChF,EAAE,cAAA,EAAgB,CAAA,EAAG,MAAA,EAAQ,+BAAA,EAAiC,QAAQ,SAAA,EAAU;AAAA,YAChF,EAAE,cAAA,EAAgB,CAAA,EAAG,MAAA,EAAQ,4BAAA,EAA8B,QAAQ,WAAA,EAAY;AAAA,YAC/E,EAAE,cAAA,EAAgB,CAAA,EAAG,MAAA,EAAQ,4BAAA,EAA8B,QAAQ,WAAA;AAAY;AACnF;AACJ;AACJ,KACJ;AAAA,IACA,QAAA,EAAU;AAAA,MACN,MAAA,EAAQ,YAAA;AAAA,MACR,UAAA,EAAY,IAAA;AAAA,MACZ,OAAA,EAAS,CAAC,MAAA,CAAO,SAAA,CAAU,cAAc,EAAE,MAAA,EAAQ,OAAO,MAAA,CAAO,SAAA,CAAU,aAAa,SAAA,EAAW,EAAA,CAAG,KAAI,GAAI,EAAE,QAAQ,SAAA,EAAW,EAAA,CAAG,KAAK;AAAA,KAC/I;AAAA,IACA,SAAA,EAAW,EAAE,QAAA,EAAU,eAAA,EAAiB,UAAU,MAAA,EAAO;AAAA,IACzD,YAAA,EAAc,EAAE,MAAA,EAAQ,kBAAA,EAAoB,cAAc,eAAA,EAAiB,iBAAA,EAAmB,WAAW,YAAA,EAAa;AAAA,IACtH,WAAA,EAAa,EAAE,KAAA,EAAO,WAAA;AAAY,GACrC,CAAA;AACL;;;;"}
1
+ {"version":3,"file":"billboard-pipeline.js","sources":["../../../src/sprite/billboard-pipeline.ts"],"sourcesContent":["import { F32, U32, U16 } from \"../engine/typed-arrays.js\";\nimport { BU, SS, CW } from \"../engine/gpu-flags.js\";\nimport type { EngineContext } from \"../engine/engine.js\";\nimport type { Mat4 } from \"../math/types.js\";\nimport { SCENE_UBO_WGSL } from \"../shader/scene-uniforms.js\";\nimport type { BillboardDepthMode, BillboardOrientation, BillboardSpriteSystem } from \"./billboard-sprite.js\";\nimport type { SpriteLayerFx } from \"./custom-shader-core.js\";\nimport { _getBillboardFxHook } from \"./sprite-fx-hook.js\";\nimport { BILLBOARD_INSTANCE_FLOATS_PER_SPRITE, BILLBOARD_INSTANCE_STRIDE_BYTES } from \"./billboard-sprite.js\";\n\nexport interface BillboardPipelineDeviceCache {\n /** @internal */\n _shaderModules: Map<string, GPUShaderModule>;\n /** @internal */\n _pipelines: Map<string, GPURenderPipeline>;\n}\n\nexport interface BillboardPipelineCache {\n /** @internal */\n _devices: WeakMap<GPUDevice, BillboardPipelineDeviceCache>;\n}\n\nconst DEPTH_MODE_TABLE: Readonly<Record<BillboardDepthMode, { index: number; writeEnabled: boolean }>> = {\n transparent: { index: 0, writeEnabled: false },\n cutout: { index: 1, writeEnabled: true },\n};\n\nconst BILLBOARD_POSITION_OFFSET_BYTES = 0;\nconst BILLBOARD_SIZE_OFFSET_BYTES = 12;\nconst BILLBOARD_UV_MIN_OFFSET_BYTES = 20;\nconst BILLBOARD_UV_MAX_OFFSET_BYTES = 28;\nconst BILLBOARD_ROTATION_OFFSET_BYTES = 36;\nconst BILLBOARD_PIVOT_OFFSET_BYTES = 40;\nconst BILLBOARD_COLOR_OFFSET_BYTES = 48;\n\nexport const BILLBOARD_SYSTEM_UBO_BYTES = 32;\nconst BILLBOARD_SYSTEM_UBO_FLOATS = BILLBOARD_SYSTEM_UBO_BYTES / 4;\nexport const BILLBOARD_INDEX_DATA: Readonly<Uint16Array> = new U16([0, 1, 2, 0, 2, 3]);\n\nexport interface BillboardInstanceSortScratch {\n /** @internal */\n _capacity: number;\n /** @internal */\n _sortedInstanceData: Float32Array;\n /** @internal */\n _sortIndices: Uint32Array;\n /** @internal */\n _sortDepths: Float32Array;\n}\n\nfunction getDepthModeEntry(depthMode: BillboardDepthMode): (typeof DEPTH_MODE_TABLE)[BillboardDepthMode] {\n return DEPTH_MODE_TABLE[depthMode];\n}\n\n/** @internal Shared by the optional billboard custom-shader composer. */\nexport function makeBillboardBasisWgsl(orientation: BillboardOrientation): string {\n switch (orientation) {\n case \"facing\":\n return `struct B {\nr: vec3f,\nu: vec3f,\n};\nfn basis(_a: vec3f) -> B {\nlet r = normalize(vec3f(scene.view[0][0], scene.view[1][0], scene.view[2][0]));\nlet u = normalize(vec3f(scene.view[0][1], scene.view[1][1], scene.view[2][1]));\nreturn B(r, -u);\n}`;\n case \"axis-locked\":\n return `struct B {\nr: vec3f,\nu: vec3f,\n};\nfn basis(_a: vec3f) -> B {\nlet a = normalize(billboards.axisAndCutoff.xyz);\nlet cr = normalize(vec3f(scene.view[0][0], scene.view[1][0], scene.view[2][0]));\nlet pr = cr - a * dot(cr, a);\nlet pl = length(pr);\nlet f = select(vec3f(0, 0, 1), vec3f(1, 0, 0), abs(a.z) > 0.999);\nlet fr = cross(a, f);\nlet r = select(fr / max(length(fr), 1e-4), pr / max(pl, 1e-4), pl > 1e-4);\nreturn B(r, -a);\n}`;\n }\n}\n\nfunction makeBillboardFragmentWgsl(depthMode: BillboardDepthMode): string {\n if (depthMode === \"cutout\") {\n return `@fragment\nfn fs(in: O) -> @location(0) vec4f {\nlet s = textureSample(atlasTex, atlasSamp, in.uv);\nif (s.a < billboards.axisAndCutoff.w) {\ndiscard;\n}\nreturn s * in.tint * billboards.opacityMul;\n}`;\n }\n return `@fragment\nfn fs(in: O) -> @location(0) vec4f {\nlet s = textureSample(atlasTex, atlasSamp, in.uv);\nreturn s * in.tint * billboards.opacityMul;\n}`;\n}\n\nfunction makeBillboardWgsl(orientation: BillboardOrientation, depthMode: BillboardDepthMode): string {\n return `${SCENE_UBO_WGSL}\nstruct S {\nopacityMul: vec4f,\naxisAndCutoff: vec4f,\n};\n@group(1) @binding(0) var<uniform> billboards: S;\n@group(1) @binding(1) var atlasTex: texture_2d<f32>;\n@group(1) @binding(2) var atlasSamp: sampler;\n${makeBillboardBasisWgsl(orientation)}\nstruct I {\n@builtin(vertex_index) vid: u32,\n@location(0) p: vec3f,\n@location(1) s: vec2f,\n@location(2) a: vec2f,\n@location(3) b: vec2f,\n@location(4) r: f32,\n@location(5) o: vec2f,\n@location(6) c: vec4f,\n};\nstruct O {\n@builtin(position) p: vec4f,\n@location(0) uv: vec2f,\n@location(1) tint: vec4f,\n};\n@vertex\nfn vs(in: I) -> O {\nlet q = vec2f(select(0.0, 1.0, in.vid == 1u || in.vid == 2u), select(0.0, 1.0, in.vid >= 2u));\nlet l = (q - in.o) * in.s;\nlet cr = cos(in.r);\nlet sr = sin(in.r);\nlet r = vec2f(l.x * cr - l.y * sr, l.x * sr + l.y * cr);\nlet b = basis(in.p);\nlet wp = in.p + b.r * r.x + b.u * r.y;\nvar out: O;\nout.p = scene.viewProjection * vec4f(wp, 1);\nout.uv = mix(in.a, in.b, q);\nout.tint = in.c;\nreturn out;\n}\n${makeBillboardFragmentWgsl(depthMode)}`;\n}\n\nexport function createBillboardPipelineCache(): BillboardPipelineCache {\n return {\n _devices: new WeakMap(),\n };\n}\n\nexport function resetBillboardPipelineCache(cache: BillboardPipelineCache): void {\n cache._devices = new WeakMap();\n}\n\nexport function getOrCreateBillboardPipeline(\n engine: EngineContext,\n cache: BillboardPipelineCache,\n format: GPUTextureFormat,\n sampleCount: 1 | 4,\n system: BillboardSpriteSystem,\n depthStencilFormat: GPUTextureFormat,\n sceneBindGroupLayout: GPUBindGroupLayout\n): GPURenderPipeline {\n const deviceCache = getBillboardPipelineDeviceCache(engine, cache);\n const depthEntry = getDepthModeEntry(system._depthMode);\n const customKey = _getBillboardFxHook()?.pipelineKeyPart(system) ?? \"\";\n const key = `${format}:${sampleCount}:${system._orientation}:${system.blendMode._key}:${depthEntry.index}:${depthStencilFormat}:${customKey}`;\n const cached = deviceCache._pipelines.get(key);\n if (cached) {\n return cached;\n }\n const pipeline = buildBillboardPipeline(engine, deviceCache, format, sampleCount, system, depthStencilFormat, sceneBindGroupLayout);\n deviceCache._pipelines.set(key, pipeline);\n return pipeline;\n}\n\nexport function createBillboardInstanceBuffer(device: GPUDevice, system: BillboardSpriteSystem, label?: string): GPUBuffer {\n return device.createBuffer({\n label,\n size: system._capacity * BILLBOARD_INSTANCE_STRIDE_BYTES,\n usage: BU.VERTEX | BU.COPY_DST,\n });\n}\n\nexport function createBillboardInstanceSortScratch(): BillboardInstanceSortScratch {\n return {\n _capacity: 0,\n _sortedInstanceData: new F32(0),\n _sortIndices: new U32(0),\n _sortDepths: new F32(0),\n };\n}\n\nexport function uploadSortedBillboardInstances(\n device: GPUDevice,\n system: BillboardSpriteSystem,\n instanceBuffer: GPUBuffer,\n scratch: BillboardInstanceSortScratch,\n cameraViewMatrix: Mat4,\n foX = 0,\n foY = 0,\n foZ = 0\n): void {\n const count = system.count;\n if (count === 0) {\n system._dirtyMin = 0;\n system._dirtyMax = 0;\n return;\n }\n ensureBillboardInstanceSortScratch(scratch, count);\n const sourceData = system._instanceData;\n const sortedData = scratch._sortedInstanceData;\n const indices = scratch._sortIndices;\n const depths = scratch._sortDepths;\n // Under floating origin the camera offset (foX/foY/foZ) is subtracted from each\n // anchor so the GPU receives eye-relative positions that match the eye-relative\n // view-projection. The sort depth is computed from the same eye-relative anchor.\n // With a zero offset this is identical to the raw-anchor path.\n for (let index = 0; index < count; index++) {\n const base = index * BILLBOARD_INSTANCE_FLOATS_PER_SPRITE;\n const anchorX = sourceData[base]! - foX;\n const anchorY = sourceData[base + 1]! - foY;\n const anchorZ = sourceData[base + 2]! - foZ;\n indices[index] = index;\n depths[index] = cameraViewMatrix[2]! * anchorX + cameraViewMatrix[6]! * anchorY + cameraViewMatrix[10]! * anchorZ + cameraViewMatrix[14]!;\n }\n indices.subarray(0, count).sort((left, right) => depths[right]! - depths[left]! || left - right);\n for (let outIndex = 0; outIndex < count; outIndex++) {\n const sourceBase = indices[outIndex]! * BILLBOARD_INSTANCE_FLOATS_PER_SPRITE;\n const destBase = outIndex * BILLBOARD_INSTANCE_FLOATS_PER_SPRITE;\n sortedData[destBase] = sourceData[sourceBase]! - foX;\n sortedData[destBase + 1] = sourceData[sourceBase + 1]! - foY;\n sortedData[destBase + 2] = sourceData[sourceBase + 2]! - foZ;\n for (let field = 3; field < BILLBOARD_INSTANCE_FLOATS_PER_SPRITE; field++) {\n sortedData[destBase + field] = sourceData[sourceBase + field]!;\n }\n }\n device.queue.writeBuffer(instanceBuffer, 0, sortedData.buffer, sortedData.byteOffset, count * BILLBOARD_INSTANCE_STRIDE_BYTES);\n system._dirtyMin = 0;\n system._dirtyMax = 0;\n}\n\nexport function ensureBillboardInstanceBuffer(\n device: GPUDevice,\n system: BillboardSpriteSystem,\n currentBuffer: GPUBuffer,\n currentCapacity: number,\n label?: string\n): { buffer: GPUBuffer; capacity: number; reallocated: boolean } {\n if (currentCapacity >= system._capacity) {\n return { buffer: currentBuffer, capacity: currentCapacity, reallocated: false };\n }\n currentBuffer.destroy();\n return { buffer: createBillboardInstanceBuffer(device, system, label), capacity: system._capacity, reallocated: true };\n}\n\nexport function uploadBillboardInstances(\n device: GPUDevice,\n system: BillboardSpriteSystem,\n instanceBuffer: GPUBuffer,\n uploadedVersion: number,\n foX = 0,\n foY = 0,\n foZ = 0,\n foScratch: BillboardInstanceSortScratch | null = null\n): number {\n if (uploadedVersion === system._version) {\n return uploadedVersion;\n }\n if (system.count === 0) {\n system._dirtyMin = 0;\n system._dirtyMax = 0;\n return system._version;\n }\n // Floating-origin path: anchors live at world scale (~5e6), so they must be made\n // eye-relative (offset subtracted) before upload — otherwise the eye-relative\n // view-projection in the shader would cancel catastrophically in F32. The whole\n // range is re-uploaded because every anchor depends on the live camera offset.\n if ((foX !== 0 || foY !== 0 || foZ !== 0) && foScratch) {\n const count = system.count;\n ensureBillboardInstanceSortScratch(foScratch, count);\n const sourceData = system._instanceData;\n const dest = foScratch._sortedInstanceData;\n for (let index = 0; index < count; index++) {\n const base = index * BILLBOARD_INSTANCE_FLOATS_PER_SPRITE;\n dest[base] = sourceData[base]! - foX;\n dest[base + 1] = sourceData[base + 1]! - foY;\n dest[base + 2] = sourceData[base + 2]! - foZ;\n for (let field = 3; field < BILLBOARD_INSTANCE_FLOATS_PER_SPRITE; field++) {\n dest[base + field] = sourceData[base + field]!;\n }\n }\n device.queue.writeBuffer(instanceBuffer, 0, dest.buffer, dest.byteOffset, count * BILLBOARD_INSTANCE_STRIDE_BYTES);\n system._dirtyMin = 0;\n system._dirtyMax = 0;\n return system._version;\n }\n let lowIndex: number;\n let highIndex: number;\n if (uploadedVersion === -1) {\n lowIndex = 0;\n highIndex = system.count;\n } else {\n lowIndex = system._dirtyMin;\n highIndex = Math.min(system._dirtyMax, system.count);\n }\n if (highIndex > lowIndex) {\n const offsetBytes = lowIndex * BILLBOARD_INSTANCE_STRIDE_BYTES;\n const byteLength = (highIndex - lowIndex) * BILLBOARD_INSTANCE_STRIDE_BYTES;\n device.queue.writeBuffer(instanceBuffer, offsetBytes, system._instanceData.buffer, system._instanceData.byteOffset + offsetBytes, byteLength);\n }\n system._dirtyMin = 0;\n system._dirtyMax = 0;\n return system._version;\n}\n\nfunction ensureBillboardInstanceSortScratch(scratch: BillboardInstanceSortScratch, count: number): void {\n if (scratch._capacity >= count) {\n return;\n }\n scratch._capacity = count;\n scratch._sortedInstanceData = new F32(count * BILLBOARD_INSTANCE_FLOATS_PER_SPRITE);\n scratch._sortIndices = new U32(count);\n scratch._sortDepths = new F32(count);\n}\n\nexport function buildBillboardSystemUbo(system: BillboardSpriteSystem, ubo: Float32Array): void {\n const opacity = system.opacity;\n if (system.blendMode._premultipliedOpacity) {\n ubo[0] = opacity;\n ubo[1] = opacity;\n ubo[2] = opacity;\n ubo[3] = opacity;\n } else {\n ubo[0] = 1;\n ubo[1] = 1;\n ubo[2] = 1;\n ubo[3] = opacity;\n }\n ubo[4] = system._axis[0];\n ubo[5] = system._axis[1];\n ubo[6] = system._axis[2];\n ubo[7] = system.alphaCutoff;\n}\n\nexport function writeBillboardSystemUboIfDirty(device: GPUDevice, uniformBuffer: GPUBuffer, scratchUbo: Float32Array, lastUbo: Float32Array, forceWrite: boolean): void {\n let dirty = forceWrite;\n if (!dirty) {\n for (let index = 0; index < BILLBOARD_SYSTEM_UBO_FLOATS; index++) {\n if (lastUbo[index] !== scratchUbo[index]) {\n dirty = true;\n break;\n }\n }\n }\n if (dirty) {\n device.queue.writeBuffer(uniformBuffer, 0, scratchUbo.buffer, scratchUbo.byteOffset, BILLBOARD_SYSTEM_UBO_BYTES);\n lastUbo.set(scratchUbo);\n }\n}\n\nexport function createBillboardSystemBindGroup(\n engine: EngineContext,\n pipeline: GPURenderPipeline,\n system: BillboardSpriteSystem,\n uniformBuffer: GPUBuffer,\n fx?: SpriteLayerFx | null\n): GPUBindGroup {\n const texture = system.atlas.texture;\n const entries: GPUBindGroupEntry[] = [\n { binding: 0, resource: { buffer: uniformBuffer } },\n { binding: 1, resource: texture.view },\n { binding: 2, resource: texture.sampler },\n ];\n if (fx) {\n for (const entry of _getBillboardFxHook()!.bindEntries(fx, 3)) {\n entries.push(entry);\n }\n }\n return engine._device.createBindGroup({\n layout: pipeline.getBindGroupLayout(1),\n entries,\n });\n}\n\nfunction getBillboardPipelineDeviceCache(engine: EngineContext, cache: BillboardPipelineCache): BillboardPipelineDeviceCache {\n let deviceCache = cache._devices.get(engine._device);\n if (!deviceCache) {\n deviceCache = { _shaderModules: new Map(), _pipelines: new Map() };\n cache._devices.set(engine._device, deviceCache);\n }\n return deviceCache;\n}\n\nfunction getShaderModule(engine: EngineContext, cache: BillboardPipelineDeviceCache, system: BillboardSpriteSystem): GPUShaderModule {\n const orientation = system._orientation;\n const depthMode = system._depthMode;\n const customModule = _getBillboardFxHook()?.shaderModule(engine, system);\n if (customModule) {\n return customModule;\n }\n const key = `${orientation}:${getDepthModeEntry(depthMode).index}`;\n let module = cache._shaderModules.get(key);\n if (!module) {\n module = engine._device.createShaderModule({ code: makeBillboardWgsl(orientation, depthMode) });\n cache._shaderModules.set(key, module);\n }\n return module;\n}\n\nfunction buildBillboardPipeline(\n engine: EngineContext,\n cache: BillboardPipelineDeviceCache,\n format: GPUTextureFormat,\n sampleCount: 1 | 4,\n system: BillboardSpriteSystem,\n depthStencilFormat: GPUTextureFormat,\n sceneBindGroupLayout: GPUBindGroupLayout\n): GPURenderPipeline {\n const device = engine._device;\n const depthEntry = getDepthModeEntry(system._depthMode);\n const shaderModule = getShaderModule(engine, cache, system);\n const layoutEntries: GPUBindGroupLayoutEntry[] = [\n { binding: 0, visibility: SS.VERTEX | SS.FRAGMENT, buffer: { type: \"uniform\" } },\n { binding: 1, visibility: SS.FRAGMENT, texture: { sampleType: \"float\" } },\n { binding: 2, visibility: SS.FRAGMENT, sampler: { type: \"filtering\" } },\n ];\n const extraLayoutEntries = _getBillboardFxHook()?.layoutEntries(system, 3);\n if (extraLayoutEntries) {\n for (const entry of extraLayoutEntries) {\n layoutEntries.push(entry);\n }\n }\n const billboardBindGroupLayout = device.createBindGroupLayout({ entries: layoutEntries });\n return device.createRenderPipeline({\n label: `${system._orientation}-billboard-sprite-pipeline`,\n layout: device.createPipelineLayout({ bindGroupLayouts: [sceneBindGroupLayout, billboardBindGroupLayout] }),\n vertex: {\n module: shaderModule,\n entryPoint: \"vs\",\n buffers: [\n {\n arrayStride: BILLBOARD_INSTANCE_STRIDE_BYTES,\n stepMode: \"instance\",\n attributes: [\n { shaderLocation: 0, offset: BILLBOARD_POSITION_OFFSET_BYTES, format: \"float32x3\" },\n { shaderLocation: 1, offset: BILLBOARD_SIZE_OFFSET_BYTES, format: \"float32x2\" },\n { shaderLocation: 2, offset: BILLBOARD_UV_MIN_OFFSET_BYTES, format: \"float32x2\" },\n { shaderLocation: 3, offset: BILLBOARD_UV_MAX_OFFSET_BYTES, format: \"float32x2\" },\n { shaderLocation: 4, offset: BILLBOARD_ROTATION_OFFSET_BYTES, format: \"float32\" },\n { shaderLocation: 5, offset: BILLBOARD_PIVOT_OFFSET_BYTES, format: \"float32x2\" },\n { shaderLocation: 6, offset: BILLBOARD_COLOR_OFFSET_BYTES, format: \"float32x4\" },\n ],\n },\n ],\n },\n fragment: {\n module: shaderModule,\n entryPoint: \"fs\",\n targets: [system.blendMode._descriptor ? { format, blend: system.blendMode._descriptor, writeMask: CW.ALL } : { format, writeMask: CW.ALL }],\n },\n primitive: { topology: \"triangle-list\", cullMode: \"none\" },\n depthStencil: { format: depthStencilFormat, depthCompare: \"greater-equal\", depthWriteEnabled: depthEntry.writeEnabled },\n multisample: { count: sampleCount },\n });\n}\n"],"names":[],"mappings":"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"}
@@ -28,7 +28,7 @@ time: f32,
28
28
  _p0: f32,
29
29
  _p1: f32,
30
30
  _p2: f32,
31
- params: vec4<f32>,
31
+ params: vec4f,
32
32
  };
33
33
  @group(${group}) @binding(${binding}) var<uniform> fx: SpriteFx;`;
34
34
  }
@@ -1 +1 @@
1
- {"version":3,"file":"custom-shader-core.js","sources":["../../../src/sprite/custom-shader-core.ts"],"sourcesContent":["/**\n * Shared mechanics for the sprite-family custom-shader hooks (the engine owns the pipeline,\n * instancing, sorting, and vertex stage; the caller supplies only a WGSL **fragment body**\n * plus optional extra textures).\n *\n * This module holds **only** the parts that are identical across every sprite-family\n * custom-shader: extra-texture binding emission, WGSL-name validation, the always-present\n * `SpriteFx` UBO declaration, and cache-key allocation. Each system keeps its own composer\n * that owns its fixed vertex stage and varying contract — those genuinely differ (world-space\n * billboard facing vs. pixel-space 2D layer transform) and are not shared.\n *\n * Tree-shaking: a scene that never builds a custom shader never imports this module, so it\n * pays zero bytes for any of it. The GPU plumbing the custom-shader feature needs — the\n * `SpriteFx` UBO writer, the extra-texture / fx bind-group **layout** and **bind** entry\n * builders, and the per-device shader-module cache — also lives here (and is reached only\n * through a descriptor), so the always-loaded sprite/billboard pipeline + renderable modules\n * stay free of it.\n */\nimport { F32 } from \"../engine/typed-arrays.js\";\nimport { SS } from \"../engine/gpu-flags.js\";\nimport type { EngineContext } from \"../engine/engine.js\";\nimport { createEmptyUniformBuffer } from \"../resource/gpu-buffers.js\";\nimport type { Texture2D } from \"../texture/texture-2d.js\";\n\n/** One extra texture bound after the atlas. In WGSL it becomes `<name>Tex` + `<name>Samp`. */\nexport interface CustomShaderTexture {\n /** Identifier used in WGSL: becomes `<name>Tex` (texture) and `<name>Samp` (sampler). */\n readonly name: string;\n readonly texture: Texture2D;\n}\n\n/**\n * Opaque, per-layer / per-system **fx attachment**. Every piece of custom-shader runtime\n * state — the `SpriteFx` UBO buffer, its CPU scratch, and the accumulated elapsed time — lives\n * INSIDE this object, which is created (via a descriptor's `_createLayerFx` hook) only when a\n * layer/system actually has a custom shader.\n *\n * The always-loaded sprite/billboard renderer + renderable modules store a single nullable\n * `SpriteLayerFx | null` and reach the entire fx lifecycle (bind, per-frame update, dispose)\n * through it. A plain layer (no custom shader) allocates no fx fields, runs no fx branch, and\n * accumulates no time — the descriptor module that builds this object is never imported, so the\n * plain path pays zero bytes for the feature.\n */\nexport interface SpriteLayerFx {\n /** Append the extra-texture + fx UBO bind-group entries, starting at `startBinding` (always 3). */\n bindEntries(startBinding: number): GPUBindGroupEntry[];\n /** Accumulate `deltaMs` internally, then write the `SpriteFx` UBO (time + `params`). */\n update(params: readonly number[], deltaMs: number): void;\n /** Destroy the `SpriteFx` UBO buffer. */\n destroy(): void;\n}\n\n/** Valid WGSL identifier (used to validate extra-texture names before splicing them in). */\nconst WGSL_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n/** Shared all-zero `fx.params` fallback used when a layer/system has a custom shader but no params set. */\nexport const EMPTY_PARAMS: readonly number[] = [0, 0, 0, 0];\n\n/** Throw if any extra-texture name is not a legal WGSL identifier. `fnName` names the caller for the message. */\nexport function validateExtraTextureNames(fnName: string, extras: readonly CustomShaderTexture[]): void {\n for (const extra of extras) {\n if (!WGSL_NAME.test(extra.name)) {\n throw new Error(`${fnName}: extra texture name \"${extra.name}\" is not a valid WGSL identifier.`);\n }\n }\n}\n\n/**\n * Emit the `@group(group) @binding(n) var <name>Tex/<name>Samp` pairs for the extra textures,\n * starting at `startBinding` and stepping by 2 (texture, then sampler). The atlas occupies\n * bindings 1/2, so callers pass `startBinding = 3`.\n */\nexport function makeExtraBindingsWgsl(group: number, startBinding: number, extras: readonly CustomShaderTexture[]): string {\n let out = \"\";\n for (let i = 0; i < extras.length; i++) {\n const binding = startBinding + i * 2;\n const name = extras[i]!.name;\n out += `@group(${group}) @binding(${binding}) var ${name}Tex: texture_2d<f32>;\\n@group(${group}) @binding(${binding + 1}) var ${name}Samp: sampler;\\n`;\n }\n return out;\n}\n\n/**\n * Emit the always-present `SpriteFx` UBO declaration. The struct layout (32 bytes) matches the\n * CPU writer {@link writeSpriteFxUbo}:\n * [0] time (seconds since the renderable's first frame)\n * [1..3] padding (vec4 alignment)\n * [4..7] params.xyzw (user-set via `setSprite2DShaderParams` / `setBillboardShaderParams`)\n * `binding` is `3 + 2 * extraTextures.length` so the UBO always lands after the extra textures.\n */\nexport function makeFxStructWgsl(group: number, binding: number): string {\n return `struct SpriteFx {\ntime: f32,\n_p0: f32,\n_p1: f32,\n_p2: f32,\nparams: vec4<f32>,\n};\n@group(${group}) @binding(${binding}) var<uniform> fx: SpriteFx;`;\n}\n\nlet _nextKey = 0;\n\n/** Allocate a process-unique pipeline/shader-module cache key with the given prefix. */\nexport function nextCustomShaderKey(prefix: string): string {\n return `${prefix}${_nextKey++}`;\n}\n\n/**\n * Per-custom-shader `SpriteFx` UBO size in bytes. Bound at `@binding(3 + 2 * extraTextures.length)`.\n * Layout matches the WGSL `SpriteFx` struct emitted by {@link makeFxStructWgsl}:\n * [0] time (seconds since the renderable's first frame)\n * [1..3] padding (vec4 alignment)\n * [4..7] params.xyzw (user-set via `setSprite2DShaderParams` / `setBillboardShaderParams`)\n */\nexport const SPRITE_FX_UBO_BYTES = 32;\n/** Number of floats in the `SpriteFx` UBO scratch array. */\nexport const SPRITE_FX_UBO_FLOATS = SPRITE_FX_UBO_BYTES / 4;\n\n/** Write the `SpriteFx` UBO (time + user params) for a custom-shader layer/system. */\nexport function writeSpriteFxUbo(device: GPUDevice, fxBuffer: GPUBuffer, timeSeconds: number, params: readonly number[], scratch: Float32Array): void {\n scratch[0] = timeSeconds;\n scratch[1] = 0;\n scratch[2] = 0;\n scratch[3] = 0;\n scratch[4] = params[0] ?? 0;\n scratch[5] = params[1] ?? 0;\n scratch[6] = params[2] ?? 0;\n scratch[7] = params[3] ?? 0;\n device.queue.writeBuffer(fxBuffer, 0, scratch.buffer, scratch.byteOffset, SPRITE_FX_UBO_BYTES);\n}\n\n/**\n * Build the extra-texture + `SpriteFx` bind-group **layout** entries for a custom shader,\n * starting at `startBinding` (always 3 — the atlas occupies 0/1/2). Each extra texture\n * contributes a `texture` + `sampler` entry (stepping by 2); the fx UBO lands last at\n * `startBinding + 2 * extras.length`. Returned to the always-loaded pipeline builder, which\n * only appends them — keeping the custom-shader loop out of the plain path.\n */\nexport function makeCustomShaderLayoutEntries(extras: readonly CustomShaderTexture[], startBinding: number): GPUBindGroupLayoutEntry[] {\n const entries: GPUBindGroupLayoutEntry[] = [];\n let binding = startBinding;\n for (let i = 0; i < extras.length; i++) {\n entries.push({ binding, visibility: SS.FRAGMENT, texture: { sampleType: \"float\" } });\n entries.push({ binding: binding + 1, visibility: SS.FRAGMENT, sampler: { type: \"filtering\" } });\n binding += 2;\n }\n entries.push({ binding, visibility: SS.FRAGMENT, buffer: { type: \"uniform\" } });\n return entries;\n}\n\n/**\n * Build the extra-texture + `SpriteFx` bind-group entries for a custom shader, mirroring the\n * layout produced by {@link makeCustomShaderLayoutEntries}. The fx UBO entry is emitted only\n * when `fxBuffer` is present.\n */\nexport function makeCustomShaderBindEntries(extras: readonly CustomShaderTexture[], startBinding: number, fxBuffer: GPUBuffer | null | undefined): GPUBindGroupEntry[] {\n const entries: GPUBindGroupEntry[] = [];\n let binding = startBinding;\n for (let i = 0; i < extras.length; i++) {\n const texture = extras[i]!.texture;\n entries.push({ binding, resource: texture.view });\n entries.push({ binding: binding + 1, resource: texture.sampler });\n binding += 2;\n }\n if (fxBuffer) {\n entries.push({ binding, resource: { buffer: fxBuffer } });\n }\n return entries;\n}\n\n/**\n * Build a per-layer / per-system fx attachment for a custom-shader descriptor. Allocates the\n * `SpriteFx` UBO + scratch, captures the descriptor's extra textures, and owns the elapsed-time\n * accumulator — all inside the returned closure. Exposed only through the descriptor's\n * `_createLayerFx` hook, so the always-loaded renderer/renderable modules never see the fx\n * machinery and the plain path pays nothing.\n */\nexport function createSpriteLayerFx(engine: EngineContext, label: string, extras: readonly CustomShaderTexture[]): SpriteLayerFx {\n const device = engine._device;\n const buffer = createEmptyUniformBuffer(engine, SPRITE_FX_UBO_BYTES, label);\n const scratch = new F32(SPRITE_FX_UBO_FLOATS);\n let elapsedMs = 0;\n return {\n bindEntries(startBinding) {\n return makeCustomShaderBindEntries(extras, startBinding, buffer);\n },\n update(params, deltaMs) {\n elapsedMs += deltaMs;\n writeSpriteFxUbo(device, buffer, elapsedMs / 1000, params, scratch);\n },\n destroy() {\n buffer.destroy();\n },\n };\n}\n\n/**\n * Build a per-device shader-module cache for a single custom-shader descriptor. The returned\n * function compiles + caches one `GPUShaderModule` per `(device, key)` pair, auto-invalidating\n * when the engine's device changes (a new `WeakMap` entry). Lives on the descriptor so the\n * always-loaded pipeline cache no longer needs a custom-module `Map`.\n */\nexport function makeShaderModuleCache(): (engine: EngineContext, key: string, makeCode: () => string) => GPUShaderModule {\n let devices: WeakMap<GPUDevice, Map<string, GPUShaderModule>> | null = null;\n return (engine, key, makeCode) => {\n devices ??= new WeakMap();\n let byKey = devices.get(engine._device);\n if (!byKey) {\n byKey = new Map();\n devices.set(engine._device, byKey);\n }\n let module = byKey.get(key);\n if (!module) {\n module = engine._device.createShaderModule({ code: makeCode() });\n byKey.set(key, module);\n }\n return module;\n };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"custom-shader-core.js","sources":["../../../src/sprite/custom-shader-core.ts"],"sourcesContent":["/**\n * Shared mechanics for the sprite-family custom-shader hooks (the engine owns the pipeline,\n * instancing, sorting, and vertex stage; the caller supplies only a WGSL **fragment body**\n * plus optional extra textures).\n *\n * This module holds **only** the parts that are identical across every sprite-family\n * custom-shader: extra-texture binding emission, WGSL-name validation, the always-present\n * `SpriteFx` UBO declaration, and cache-key allocation. Each system keeps its own composer\n * that owns its fixed vertex stage and varying contract — those genuinely differ (world-space\n * billboard facing vs. pixel-space 2D layer transform) and are not shared.\n *\n * Tree-shaking: a scene that never builds a custom shader never imports this module, so it\n * pays zero bytes for any of it. The GPU plumbing the custom-shader feature needs — the\n * `SpriteFx` UBO writer, the extra-texture / fx bind-group **layout** and **bind** entry\n * builders, and the per-device shader-module cache — also lives here (and is reached only\n * through a descriptor), so the always-loaded sprite/billboard pipeline + renderable modules\n * stay free of it.\n */\nimport { F32 } from \"../engine/typed-arrays.js\";\nimport { SS } from \"../engine/gpu-flags.js\";\nimport type { EngineContext } from \"../engine/engine.js\";\nimport { createEmptyUniformBuffer } from \"../resource/gpu-buffers.js\";\nimport type { Texture2D } from \"../texture/texture-2d.js\";\n\n/** One extra texture bound after the atlas. In WGSL it becomes `<name>Tex` + `<name>Samp`. */\nexport interface CustomShaderTexture {\n /** Identifier used in WGSL: becomes `<name>Tex` (texture) and `<name>Samp` (sampler). */\n readonly name: string;\n readonly texture: Texture2D;\n}\n\n/**\n * Opaque, per-layer / per-system **fx attachment**. Every piece of custom-shader runtime\n * state — the `SpriteFx` UBO buffer, its CPU scratch, and the accumulated elapsed time — lives\n * INSIDE this object, which is created (via a descriptor's `_createLayerFx` hook) only when a\n * layer/system actually has a custom shader.\n *\n * The always-loaded sprite/billboard renderer + renderable modules store a single nullable\n * `SpriteLayerFx | null` and reach the entire fx lifecycle (bind, per-frame update, dispose)\n * through it. A plain layer (no custom shader) allocates no fx fields, runs no fx branch, and\n * accumulates no time — the descriptor module that builds this object is never imported, so the\n * plain path pays zero bytes for the feature.\n */\nexport interface SpriteLayerFx {\n /** Append the extra-texture + fx UBO bind-group entries, starting at `startBinding` (always 3). */\n bindEntries(startBinding: number): GPUBindGroupEntry[];\n /** Accumulate `deltaMs` internally, then write the `SpriteFx` UBO (time + `params`). */\n update(params: readonly number[], deltaMs: number): void;\n /** Destroy the `SpriteFx` UBO buffer. */\n destroy(): void;\n}\n\n/** Valid WGSL identifier (used to validate extra-texture names before splicing them in). */\nconst WGSL_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n/** Shared all-zero `fx.params` fallback used when a layer/system has a custom shader but no params set. */\nexport const EMPTY_PARAMS: readonly number[] = [0, 0, 0, 0];\n\n/** Throw if any extra-texture name is not a legal WGSL identifier. `fnName` names the caller for the message. */\nexport function validateExtraTextureNames(fnName: string, extras: readonly CustomShaderTexture[]): void {\n for (const extra of extras) {\n if (!WGSL_NAME.test(extra.name)) {\n throw new Error(`${fnName}: extra texture name \"${extra.name}\" is not a valid WGSL identifier.`);\n }\n }\n}\n\n/**\n * Emit the `@group(group) @binding(n) var <name>Tex/<name>Samp` pairs for the extra textures,\n * starting at `startBinding` and stepping by 2 (texture, then sampler). The atlas occupies\n * bindings 1/2, so callers pass `startBinding = 3`.\n */\nexport function makeExtraBindingsWgsl(group: number, startBinding: number, extras: readonly CustomShaderTexture[]): string {\n let out = \"\";\n for (let i = 0; i < extras.length; i++) {\n const binding = startBinding + i * 2;\n const name = extras[i]!.name;\n out += `@group(${group}) @binding(${binding}) var ${name}Tex: texture_2d<f32>;\\n@group(${group}) @binding(${binding + 1}) var ${name}Samp: sampler;\\n`;\n }\n return out;\n}\n\n/**\n * Emit the always-present `SpriteFx` UBO declaration. The struct layout (32 bytes) matches the\n * CPU writer {@link writeSpriteFxUbo}:\n * [0] time (seconds since the renderable's first frame)\n * [1..3] padding (vec4 alignment)\n * [4..7] params.xyzw (user-set via `setSprite2DShaderParams` / `setBillboardShaderParams`)\n * `binding` is `3 + 2 * extraTextures.length` so the UBO always lands after the extra textures.\n */\nexport function makeFxStructWgsl(group: number, binding: number): string {\n return `struct SpriteFx {\ntime: f32,\n_p0: f32,\n_p1: f32,\n_p2: f32,\nparams: vec4f,\n};\n@group(${group}) @binding(${binding}) var<uniform> fx: SpriteFx;`;\n}\n\nlet _nextKey = 0;\n\n/** Allocate a process-unique pipeline/shader-module cache key with the given prefix. */\nexport function nextCustomShaderKey(prefix: string): string {\n return `${prefix}${_nextKey++}`;\n}\n\n/**\n * Per-custom-shader `SpriteFx` UBO size in bytes. Bound at `@binding(3 + 2 * extraTextures.length)`.\n * Layout matches the WGSL `SpriteFx` struct emitted by {@link makeFxStructWgsl}:\n * [0] time (seconds since the renderable's first frame)\n * [1..3] padding (vec4 alignment)\n * [4..7] params.xyzw (user-set via `setSprite2DShaderParams` / `setBillboardShaderParams`)\n */\nexport const SPRITE_FX_UBO_BYTES = 32;\n/** Number of floats in the `SpriteFx` UBO scratch array. */\nexport const SPRITE_FX_UBO_FLOATS = SPRITE_FX_UBO_BYTES / 4;\n\n/** Write the `SpriteFx` UBO (time + user params) for a custom-shader layer/system. */\nexport function writeSpriteFxUbo(device: GPUDevice, fxBuffer: GPUBuffer, timeSeconds: number, params: readonly number[], scratch: Float32Array): void {\n scratch[0] = timeSeconds;\n scratch[1] = 0;\n scratch[2] = 0;\n scratch[3] = 0;\n scratch[4] = params[0] ?? 0;\n scratch[5] = params[1] ?? 0;\n scratch[6] = params[2] ?? 0;\n scratch[7] = params[3] ?? 0;\n device.queue.writeBuffer(fxBuffer, 0, scratch.buffer, scratch.byteOffset, SPRITE_FX_UBO_BYTES);\n}\n\n/**\n * Build the extra-texture + `SpriteFx` bind-group **layout** entries for a custom shader,\n * starting at `startBinding` (always 3 — the atlas occupies 0/1/2). Each extra texture\n * contributes a `texture` + `sampler` entry (stepping by 2); the fx UBO lands last at\n * `startBinding + 2 * extras.length`. Returned to the always-loaded pipeline builder, which\n * only appends them — keeping the custom-shader loop out of the plain path.\n */\nexport function makeCustomShaderLayoutEntries(extras: readonly CustomShaderTexture[], startBinding: number): GPUBindGroupLayoutEntry[] {\n const entries: GPUBindGroupLayoutEntry[] = [];\n let binding = startBinding;\n for (let i = 0; i < extras.length; i++) {\n entries.push({ binding, visibility: SS.FRAGMENT, texture: { sampleType: \"float\" } });\n entries.push({ binding: binding + 1, visibility: SS.FRAGMENT, sampler: { type: \"filtering\" } });\n binding += 2;\n }\n entries.push({ binding, visibility: SS.FRAGMENT, buffer: { type: \"uniform\" } });\n return entries;\n}\n\n/**\n * Build the extra-texture + `SpriteFx` bind-group entries for a custom shader, mirroring the\n * layout produced by {@link makeCustomShaderLayoutEntries}. The fx UBO entry is emitted only\n * when `fxBuffer` is present.\n */\nexport function makeCustomShaderBindEntries(extras: readonly CustomShaderTexture[], startBinding: number, fxBuffer: GPUBuffer | null | undefined): GPUBindGroupEntry[] {\n const entries: GPUBindGroupEntry[] = [];\n let binding = startBinding;\n for (let i = 0; i < extras.length; i++) {\n const texture = extras[i]!.texture;\n entries.push({ binding, resource: texture.view });\n entries.push({ binding: binding + 1, resource: texture.sampler });\n binding += 2;\n }\n if (fxBuffer) {\n entries.push({ binding, resource: { buffer: fxBuffer } });\n }\n return entries;\n}\n\n/**\n * Build a per-layer / per-system fx attachment for a custom-shader descriptor. Allocates the\n * `SpriteFx` UBO + scratch, captures the descriptor's extra textures, and owns the elapsed-time\n * accumulator — all inside the returned closure. Exposed only through the descriptor's\n * `_createLayerFx` hook, so the always-loaded renderer/renderable modules never see the fx\n * machinery and the plain path pays nothing.\n */\nexport function createSpriteLayerFx(engine: EngineContext, label: string, extras: readonly CustomShaderTexture[]): SpriteLayerFx {\n const device = engine._device;\n const buffer = createEmptyUniformBuffer(engine, SPRITE_FX_UBO_BYTES, label);\n const scratch = new F32(SPRITE_FX_UBO_FLOATS);\n let elapsedMs = 0;\n return {\n bindEntries(startBinding) {\n return makeCustomShaderBindEntries(extras, startBinding, buffer);\n },\n update(params, deltaMs) {\n elapsedMs += deltaMs;\n writeSpriteFxUbo(device, buffer, elapsedMs / 1000, params, scratch);\n },\n destroy() {\n buffer.destroy();\n },\n };\n}\n\n/**\n * Build a per-device shader-module cache for a single custom-shader descriptor. The returned\n * function compiles + caches one `GPUShaderModule` per `(device, key)` pair, auto-invalidating\n * when the engine's device changes (a new `WeakMap` entry). Lives on the descriptor so the\n * always-loaded pipeline cache no longer needs a custom-module `Map`.\n */\nexport function makeShaderModuleCache(): (engine: EngineContext, key: string, makeCode: () => string) => GPUShaderModule {\n let devices: WeakMap<GPUDevice, Map<string, GPUShaderModule>> | null = null;\n return (engine, key, makeCode) => {\n devices ??= new WeakMap();\n let byKey = devices.get(engine._device);\n if (!byKey) {\n byKey = new Map();\n devices.set(engine._device, byKey);\n }\n let module = byKey.get(key);\n if (!module) {\n module = engine._device.createShaderModule({ code: makeCode() });\n byKey.set(key, module);\n }\n return module;\n };\n}\n"],"names":[],"mappings":"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"}
@@ -32,10 +32,10 @@ function createGridSpriteAtlas(texture, options) {
32
32
  }
33
33
  async function loadSpriteAtlas(engine, textureUrl, options = {}) {
34
34
  if (options.metadataUrl !== void 0) {
35
- throw new Error("loadSpriteAtlas: metadataUrl is not implemented in PR 1.");
35
+ throw new Error("loadSpriteAtlas: metadataUrl unsupported.");
36
36
  }
37
37
  if (!options.gridSize) {
38
- throw new Error("loadSpriteAtlas: options.gridSize is required in PR 1.");
38
+ throw new Error("loadSpriteAtlas: gridSize required.");
39
39
  }
40
40
  const texOpts = {
41
41
  // Sprite UVs are top-down (origin at image top-left); do not flip.
@@ -1 +1 @@
1
- {"version":3,"file":"sprite-atlas.js","sources":["../../../../src/sprite/shared/sprite-atlas.ts"],"sourcesContent":["/**\n * Sprite atlas — shared foundation for `Sprite2DLayer` (and, in later PRs,\n * billboards). A `SpriteAtlas` is a pure data record: a `Texture2D` plus\n * an immutable list of `SpriteFrame`s. The same atlas may back multiple\n * layers / scenes; lifetime is governed by the underlying `Texture2D`.\n *\n * PR 1 ships only the grid-based atlas constructor and the loader. Frames\n * are addressed by **integer index only**; a string-keyed wrapper type\n * (analogous to a future `NamedSpriteAtlas`) will land alongside the\n * TexturePacker JSON loader in a later PR.\n *\n * NOT YET IMPLEMENTED (intentionally omitted, see\n * `docs/lite/sprites/pr1-pure-2d-sprites-scope.md`):\n * - `SpriteClip` / `clips` field on the atlas (animation playback). Will\n * land as an additive change to `SpriteAtlas` in a follow-up PR.\n */\nimport type { EngineContext } from \"../../engine/engine.js\";\nimport type { Texture2D, Texture2DOptions } from \"../../texture/texture-2d.js\";\nimport { loadTexture2D } from \"../../texture/texture-2d.js\";\n\n/** Texture sampling mode for a sprite atlas. */\nexport type SpriteSampling = \"linear\" | \"nearest\";\n\n/** A single frame in an atlas. UVs in [0,1]; pivot in [0,1] of the frame. */\nexport interface SpriteFrame {\n readonly name?: string;\n readonly uvMin: readonly [number, number];\n readonly uvMax: readonly [number, number];\n readonly sourceSizePx: readonly [number, number];\n readonly pivot: readonly [number, number];\n}\n\n/** A loaded sprite atlas — pure data, no methods. Frames are addressed by integer index. */\nexport interface SpriteAtlas {\n readonly texture: Texture2D;\n readonly textureSizePx: readonly [number, number];\n readonly frames: readonly SpriteFrame[];\n readonly premultipliedAlpha: boolean;\n /** @internal Mutable shelf-pack cursor + parameters carried by atlases built via the runtime\n * packer (`createSpriteAtlasFromFrames`). `appendSpriteAtlasFrames` resumes packing into the\n * remaining capacity using this state. Absent on atlases built via `createGridSpriteAtlas` /\n * `loadSpriteAtlas` — those cannot be appended to. */\n _packState?: SpriteAtlasPackState;\n /** @internal Mutable alias of `frames` (same underlying array) for the runtime packer to\n * push new entries into without casting away `readonly`. Always set together with\n * `_packState` (i.e. only on atlases built via `createSpriteAtlasFromFrames`). */\n _frames?: SpriteFrame[];\n}\n\n/** @internal Shelf-pack cursor + parameters for runtime atlas packing. Mutated by\n * `appendSpriteAtlasFrames` to track free space across calls. */\nexport interface SpriteAtlasPackState {\n /** Current shelf x-cursor (px). */\n penX: number;\n /** Current shelf y-cursor (px). */\n penY: number;\n /** Height of the current shelf (px); the next shelf will start at `penY + shelfHeight + padding`. */\n shelfHeight: number;\n /** Shelf wrap width (px); from `SpriteAtlasPackOptions.maxWidthPx`. */\n maxWidth: number;\n /** Gap between packed frames (px); from `SpriteAtlasPackOptions.paddingPx`. */\n padding: number;\n}\n\n/** Options for `createGridSpriteAtlas`. */\nexport interface GridAtlasOptions {\n cellWidthPx: number;\n cellHeightPx: number;\n /** Defaults to `floor(textureWidth / cellWidthPx)`. */\n columns?: number;\n /** Defaults to `floor(textureHeight / cellHeightPx)`. */\n rows?: number;\n marginPx?: number;\n spacingPx?: number;\n /** Default `[0.5, 0.5]`. */\n pivot?: readonly [number, number];\n premultipliedAlpha?: boolean;\n}\n\n/** Options for `loadSpriteAtlas`. PR 1 supports the `gridSize` path only. */\nexport interface LoadAtlasOptions {\n /** Grid cell size `[w, h]` in pixels. Required in PR 1. */\n gridSize?: readonly [number, number];\n /** Reserved for future PR — TexturePacker-style JSON. Throws if used in PR 1. */\n metadataUrl?: string;\n sampling?: SpriteSampling;\n /** Marks the atlas as carrying premultiplied RGBA so the renderer picks the\n * premultiplied blend pipeline (`srcFactor: ONE`). Default `false` — matches\n * the bits PNG decoding produces. Set together with `premultiplyOnLoad: true`\n * for mathematically correct soft edges. Setting this `true` without\n * `premultiplyOnLoad: true` is only correct if the source image is *already*\n * premultiplied on disk (e.g. produced by a build step). */\n premultipliedAlpha?: boolean;\n /** Tell the texture loader to premultiply alpha at decode time\n * (`createImageBitmap({ premultiplyAlpha: \"premultiply\" })`). Default `false`.\n * Pair with `premultipliedAlpha: true` for the premultiplied blend pipeline. */\n premultiplyOnLoad?: boolean;\n textureOptions?: Texture2DOptions;\n}\n\n/**\n * Build a `SpriteAtlas` from a uniform grid over an existing texture. All\n * cells share the supplied pivot. Frames are emitted row-major (top-left\n * first). No name lookup map is populated because grid cells have no names.\n */\nexport function createGridSpriteAtlas(texture: Texture2D, options: GridAtlasOptions): SpriteAtlas {\n const cellW = options.cellWidthPx;\n const cellH = options.cellHeightPx;\n const margin = options.marginPx ?? 0;\n const spacing = options.spacingPx ?? 0;\n const cols = options.columns ?? Math.max(1, Math.floor((texture.width - margin * 2 + spacing) / (cellW + spacing)));\n const rows = options.rows ?? Math.max(1, Math.floor((texture.height - margin * 2 + spacing) / (cellH + spacing)));\n const pivot = options.pivot ?? [0.5, 0.5];\n\n const tw = texture.width;\n const th = texture.height;\n const frames: SpriteFrame[] = [];\n for (let r = 0; r < rows; r++) {\n for (let c = 0; c < cols; c++) {\n const x = margin + c * (cellW + spacing);\n const y = margin + r * (cellH + spacing);\n frames.push({\n uvMin: [x / tw, y / th],\n uvMax: [(x + cellW) / tw, (y + cellH) / th],\n sourceSizePx: [cellW, cellH],\n pivot: [pivot[0], pivot[1]],\n });\n }\n }\n\n return {\n texture,\n textureSizePx: [tw, th],\n frames,\n premultipliedAlpha: options.premultipliedAlpha ?? false,\n };\n}\n\n/**\n * Load a sprite atlas from an image URL. PR 1 supports only the\n * `gridSize` path: the texture is fetched as a non-Y-flipped image\n * (so atlas UVs map top-down with `(0,0)` at the image top-left) and\n * partitioned into a grid via `createGridSpriteAtlas`.\n */\nexport async function loadSpriteAtlas(engine: EngineContext, textureUrl: string, options: LoadAtlasOptions = {}): Promise<SpriteAtlas> {\n if (options.metadataUrl !== undefined) {\n throw new Error(\"loadSpriteAtlas: metadataUrl is not implemented in PR 1.\");\n }\n if (!options.gridSize) {\n throw new Error(\"loadSpriteAtlas: options.gridSize is required in PR 1.\");\n }\n\n const texOpts: Texture2DOptions = {\n // Sprite UVs are top-down (origin at image top-left); do not flip.\n invertY: false,\n // Atlas frames typically tile cleanly; use clamp to avoid bleeding from neighbouring cells at edges.\n addressModeU: \"clamp-to-edge\",\n addressModeV: \"clamp-to-edge\",\n // Sprites usually look best with bilinear filtering and no mip chain — sharp pixel art still works in nearest.\n mipMaps: false,\n minFilter: options.sampling === \"nearest\" ? \"nearest\" : \"linear\",\n magFilter: options.sampling === \"nearest\" ? \"nearest\" : \"linear\",\n // Premultiply at decode if requested. Pair with `premultipliedAlpha: true` for\n // a mathematically honest premultiplied pipeline.\n premultiplyAlpha: options.premultiplyOnLoad ?? false,\n ...options.textureOptions,\n };\n\n const texture = await loadTexture2D(engine, textureUrl, texOpts);\n return createGridSpriteAtlas(texture, {\n cellWidthPx: options.gridSize[0],\n cellHeightPx: options.gridSize[1],\n // Default `false` — matches the straight RGBA bits the PNG decoder produces.\n // Callers wanting premultiplied blending should pass `premultiplyOnLoad: true`\n // *and* `premultipliedAlpha: true` together so storage and blend factors agree.\n premultipliedAlpha: options.premultipliedAlpha ?? false,\n });\n}\n\n/** @internal Resolve a frame index (just bounds-checks). Throws if out of range. */\nexport function resolveSpriteFrame(atlas: SpriteAtlas, frame: number): number {\n if (frame < 0 || frame >= atlas.frames.length) {\n throw new Error(`resolveSpriteFrame: index ${frame} out of range [0, ${atlas.frames.length})`);\n }\n return frame;\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sprite-atlas.js","sources":["../../../../src/sprite/shared/sprite-atlas.ts"],"sourcesContent":["/**\n * Sprite atlas — shared foundation for `Sprite2DLayer` (and, in later PRs,\n * billboards). A `SpriteAtlas` is a pure data record: a `Texture2D` plus\n * an immutable list of `SpriteFrame`s. The same atlas may back multiple\n * layers / scenes; lifetime is governed by the underlying `Texture2D`.\n *\n * PR 1 ships only the grid-based atlas constructor and the loader. Frames\n * are addressed by **integer index only**; a string-keyed wrapper type\n * (analogous to a future `NamedSpriteAtlas`) will land alongside the\n * TexturePacker JSON loader in a later PR.\n *\n * NOT YET IMPLEMENTED (intentionally omitted, see\n * `docs/lite/sprites/pr1-pure-2d-sprites-scope.md`):\n * - `SpriteClip` / `clips` field on the atlas (animation playback). Will\n * land as an additive change to `SpriteAtlas` in a follow-up PR.\n */\nimport type { EngineContext } from \"../../engine/engine.js\";\nimport type { Texture2D, Texture2DOptions } from \"../../texture/texture-2d.js\";\nimport { loadTexture2D } from \"../../texture/texture-2d.js\";\n\n/** Texture sampling mode for a sprite atlas. */\nexport type SpriteSampling = \"linear\" | \"nearest\";\n\n/** A single frame in an atlas. UVs in [0,1]; pivot in [0,1] of the frame. */\nexport interface SpriteFrame {\n readonly name?: string;\n readonly uvMin: readonly [number, number];\n readonly uvMax: readonly [number, number];\n readonly sourceSizePx: readonly [number, number];\n readonly pivot: readonly [number, number];\n}\n\n/** A loaded sprite atlas — pure data, no methods. Frames are addressed by integer index. */\nexport interface SpriteAtlas {\n readonly texture: Texture2D;\n readonly textureSizePx: readonly [number, number];\n readonly frames: readonly SpriteFrame[];\n readonly premultipliedAlpha: boolean;\n /** @internal Mutable shelf-pack cursor + parameters carried by atlases built via the runtime\n * packer (`createSpriteAtlasFromFrames`). `appendSpriteAtlasFrames` resumes packing into the\n * remaining capacity using this state. Absent on atlases built via `createGridSpriteAtlas` /\n * `loadSpriteAtlas` — those cannot be appended to. */\n _packState?: SpriteAtlasPackState;\n /** @internal Mutable alias of `frames` (same underlying array) for the runtime packer to\n * push new entries into without casting away `readonly`. Always set together with\n * `_packState` (i.e. only on atlases built via `createSpriteAtlasFromFrames`). */\n _frames?: SpriteFrame[];\n}\n\n/** @internal Shelf-pack cursor + parameters for runtime atlas packing. Mutated by\n * `appendSpriteAtlasFrames` to track free space across calls. */\nexport interface SpriteAtlasPackState {\n /** Current shelf x-cursor (px). */\n penX: number;\n /** Current shelf y-cursor (px). */\n penY: number;\n /** Height of the current shelf (px); the next shelf will start at `penY + shelfHeight + padding`. */\n shelfHeight: number;\n /** Shelf wrap width (px); from `SpriteAtlasPackOptions.maxWidthPx`. */\n maxWidth: number;\n /** Gap between packed frames (px); from `SpriteAtlasPackOptions.paddingPx`. */\n padding: number;\n}\n\n/** Options for `createGridSpriteAtlas`. */\nexport interface GridAtlasOptions {\n cellWidthPx: number;\n cellHeightPx: number;\n /** Defaults to `floor(textureWidth / cellWidthPx)`. */\n columns?: number;\n /** Defaults to `floor(textureHeight / cellHeightPx)`. */\n rows?: number;\n marginPx?: number;\n spacingPx?: number;\n /** Default `[0.5, 0.5]`. */\n pivot?: readonly [number, number];\n premultipliedAlpha?: boolean;\n}\n\n/** Options for `loadSpriteAtlas`. PR 1 supports the `gridSize` path only. */\nexport interface LoadAtlasOptions {\n /** Grid cell size `[w, h]` in pixels. Required in PR 1. */\n gridSize?: readonly [number, number];\n /** Reserved for future PR — TexturePacker-style JSON. Throws if used in PR 1. */\n metadataUrl?: string;\n sampling?: SpriteSampling;\n /** Marks the atlas as carrying premultiplied RGBA so the renderer picks the\n * premultiplied blend pipeline (`srcFactor: ONE`). Default `false` — matches\n * the bits PNG decoding produces. Set together with `premultiplyOnLoad: true`\n * for mathematically correct soft edges. Setting this `true` without\n * `premultiplyOnLoad: true` is only correct if the source image is *already*\n * premultiplied on disk (e.g. produced by a build step). */\n premultipliedAlpha?: boolean;\n /** Tell the texture loader to premultiply alpha at decode time\n * (`createImageBitmap({ premultiplyAlpha: \"premultiply\" })`). Default `false`.\n * Pair with `premultipliedAlpha: true` for the premultiplied blend pipeline. */\n premultiplyOnLoad?: boolean;\n textureOptions?: Texture2DOptions;\n}\n\n/**\n * Build a `SpriteAtlas` from a uniform grid over an existing texture. All\n * cells share the supplied pivot. Frames are emitted row-major (top-left\n * first). No name lookup map is populated because grid cells have no names.\n */\nexport function createGridSpriteAtlas(texture: Texture2D, options: GridAtlasOptions): SpriteAtlas {\n const cellW = options.cellWidthPx;\n const cellH = options.cellHeightPx;\n const margin = options.marginPx ?? 0;\n const spacing = options.spacingPx ?? 0;\n const cols = options.columns ?? Math.max(1, Math.floor((texture.width - margin * 2 + spacing) / (cellW + spacing)));\n const rows = options.rows ?? Math.max(1, Math.floor((texture.height - margin * 2 + spacing) / (cellH + spacing)));\n const pivot = options.pivot ?? [0.5, 0.5];\n\n const tw = texture.width;\n const th = texture.height;\n const frames: SpriteFrame[] = [];\n for (let r = 0; r < rows; r++) {\n for (let c = 0; c < cols; c++) {\n const x = margin + c * (cellW + spacing);\n const y = margin + r * (cellH + spacing);\n frames.push({\n uvMin: [x / tw, y / th],\n uvMax: [(x + cellW) / tw, (y + cellH) / th],\n sourceSizePx: [cellW, cellH],\n pivot: [pivot[0], pivot[1]],\n });\n }\n }\n\n return {\n texture,\n textureSizePx: [tw, th],\n frames,\n premultipliedAlpha: options.premultipliedAlpha ?? false,\n };\n}\n\n/**\n * Load a sprite atlas from an image URL. PR 1 supports only the\n * `gridSize` path: the texture is fetched as a non-Y-flipped image\n * (so atlas UVs map top-down with `(0,0)` at the image top-left) and\n * partitioned into a grid via `createGridSpriteAtlas`.\n */\nexport async function loadSpriteAtlas(engine: EngineContext, textureUrl: string, options: LoadAtlasOptions = {}): Promise<SpriteAtlas> {\n if (options.metadataUrl !== undefined) {\n throw new Error(\"loadSpriteAtlas: metadataUrl unsupported.\");\n }\n if (!options.gridSize) {\n throw new Error(\"loadSpriteAtlas: gridSize required.\");\n }\n\n const texOpts: Texture2DOptions = {\n // Sprite UVs are top-down (origin at image top-left); do not flip.\n invertY: false,\n // Atlas frames typically tile cleanly; use clamp to avoid bleeding from neighbouring cells at edges.\n addressModeU: \"clamp-to-edge\",\n addressModeV: \"clamp-to-edge\",\n // Sprites usually look best with bilinear filtering and no mip chain — sharp pixel art still works in nearest.\n mipMaps: false,\n minFilter: options.sampling === \"nearest\" ? \"nearest\" : \"linear\",\n magFilter: options.sampling === \"nearest\" ? \"nearest\" : \"linear\",\n // Premultiply at decode if requested. Pair with `premultipliedAlpha: true` for\n // a mathematically honest premultiplied pipeline.\n premultiplyAlpha: options.premultiplyOnLoad ?? false,\n ...options.textureOptions,\n };\n\n const texture = await loadTexture2D(engine, textureUrl, texOpts);\n return createGridSpriteAtlas(texture, {\n cellWidthPx: options.gridSize[0],\n cellHeightPx: options.gridSize[1],\n // Default `false` — matches the straight RGBA bits the PNG decoder produces.\n // Callers wanting premultiplied blending should pass `premultiplyOnLoad: true`\n // *and* `premultipliedAlpha: true` together so storage and blend factors agree.\n premultipliedAlpha: options.premultipliedAlpha ?? false,\n });\n}\n\n/** @internal Resolve a frame index (just bounds-checks). Throws if out of range. */\nexport function resolveSpriteFrame(atlas: SpriteAtlas, frame: number): number {\n if (frame < 0 || frame >= atlas.frames.length) {\n throw new Error(`resolveSpriteFrame: index ${frame} out of range [0, ${atlas.frames.length})`);\n }\n return frame;\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ import { makeSpritePrologueWgsl } from './sprite-pipeline.js';
2
+ import { _registerSpriteCoverageGammaHook } from './sprite-coverage-gamma-hook.js';
3
+
4
+ function makeCoverageGammaWgsl(hasDepth, spriteGroupIndex, uvScroll) {
5
+ return `${makeSpritePrologueWgsl(hasDepth, spriteGroupIndex, uvScroll)}
6
+ @fragment
7
+ fn fs(in: O) -> @location(0) vec4f {
8
+ let s = textureSample(atlasTex, atlasSamp, in.uv);
9
+ let a = pow(s.a, L.aa.x);
10
+ return vec4f(s.rgb, a) * in.tint * L.opacityMul;
11
+ }`;
12
+ }
13
+ let _shaderCache = null;
14
+ function getCoverageGammaShaderModule(engine, hasDepth, uvScroll) {
15
+ const device = engine._device;
16
+ const cache = _shaderCache ??= /* @__PURE__ */ new WeakMap();
17
+ let perDevice = cache.get(device);
18
+ if (!perDevice) {
19
+ perDevice = /* @__PURE__ */ new Map();
20
+ cache.set(device, perDevice);
21
+ }
22
+ const key = `${hasDepth ? 1 : 0}:${uvScroll ? 1 : 0}`;
23
+ let module = perDevice.get(key);
24
+ if (!module) {
25
+ module = device.createShaderModule({ code: makeCoverageGammaWgsl(hasDepth, hasDepth ? 1 : 0, uvScroll) });
26
+ perDevice.set(key, module);
27
+ }
28
+ return module;
29
+ }
30
+ function isGammaActive(layer) {
31
+ const g = layer._coverageGamma;
32
+ return g != null && Number.isFinite(g) && g > 0 && g !== 1;
33
+ }
34
+ const COVERAGE_GAMMA_HOOK = {
35
+ pipelineKeyPart(layer) {
36
+ return isGammaActive(layer) ? "1" : "0";
37
+ },
38
+ shaderModule(engine, hasDepth, layer) {
39
+ if (!isGammaActive(layer)) {
40
+ return null;
41
+ }
42
+ return getCoverageGammaShaderModule(engine, hasDepth, layer._uvScrollAttr != null);
43
+ },
44
+ writeUbo(layer, ubo) {
45
+ if (isGammaActive(layer)) {
46
+ ubo[12] = 1 / layer._coverageGamma;
47
+ } else {
48
+ ubo[12] = 0;
49
+ }
50
+ }
51
+ };
52
+ function setSprite2DCoverageGamma(layer, gamma) {
53
+ _registerSpriteCoverageGammaHook(COVERAGE_GAMMA_HOOK);
54
+ layer._coverageGamma = gamma;
55
+ }
56
+
57
+ export { setSprite2DCoverageGamma };
58
+ //# sourceMappingURL=sprite-2d-coverage-gamma.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sprite-2d-coverage-gamma.js","sources":["../../../src/sprite/sprite-2d-coverage-gamma.ts"],"sourcesContent":["/**\n * Opt-in coverage-gamma feature for `Sprite2DLayer` (glyph-atlas \"stem darkening\").\n *\n * Importing `setSprite2DCoverageGamma` is the trigger that pulls this module — and with it the\n * gamma fragment permutation, the `aa.x` UBO writer, and the pipeline-key part — into the bundle.\n * Sprite scenes that never call the setter keep the coverage-gamma hook `null`, so the\n * always-loaded sprite pipeline (`sprite-pipeline.ts`) carries zero gamma bytes.\n *\n * The hook methods take the layer **opaquely**; all `_coverageGamma` property access happens here,\n * in the tree-shaken module, so even the field-name string stays out of the always-loaded path —\n * mirroring the custom-shader hook (`sprite-fx-hook.ts`).\n */\nimport type { EngineContext } from \"../engine/engine.js\";\nimport type { Sprite2DLayer } from \"./sprite-2d.js\";\nimport { makeSpritePrologueWgsl } from \"./sprite-pipeline.js\";\nimport type { SpriteCoverageGammaHook } from \"./sprite-coverage-gamma-hook.js\";\nimport { _registerSpriteCoverageGammaHook } from \"./sprite-coverage-gamma-hook.js\";\n\n/**\n * Coverage-gamma sprite shader: the base prologue (vertex stage + `Layer` UBO with its `aa`\n * slot) plus a fragment that raises sampled alpha to `1/coverageGamma` (`L.aa.x`) so anti-aliased\n * glyph edges composite heavier, mimicking gamma-space stem darkening.\n */\nfunction makeCoverageGammaWgsl(hasDepth: boolean, spriteGroupIndex: 0 | 1, uvScroll: boolean): string {\n return `${makeSpritePrologueWgsl(hasDepth, spriteGroupIndex, uvScroll)}\n@fragment\nfn fs(in: O) -> @location(0) vec4f {\nlet s = textureSample(atlasTex, atlasSamp, in.uv);\nlet a = pow(s.a, L.aa.x);\nreturn vec4f(s.rgb, a) * in.tint * L.opacityMul;\n}`;\n}\n\nlet _shaderCache: WeakMap<GPUDevice, Map<string, GPUShaderModule>> | null = null;\n\nfunction getCoverageGammaShaderModule(engine: EngineContext, hasDepth: boolean, uvScroll: boolean): GPUShaderModule {\n const device = engine._device;\n const cache = (_shaderCache ??= new WeakMap());\n let perDevice = cache.get(device);\n if (!perDevice) {\n perDevice = new Map();\n cache.set(device, perDevice);\n }\n const key = `${hasDepth ? 1 : 0}:${uvScroll ? 1 : 0}`;\n let module = perDevice.get(key);\n if (!module) {\n module = device.createShaderModule({ code: makeCoverageGammaWgsl(hasDepth, hasDepth ? 1 : 0, uvScroll) });\n perDevice.set(key, module);\n }\n return module;\n}\n\n/**\n * True when `layer` has an active coverage gamma set via the opt-in setter. Active requires a\n * **finite, positive, non-identity** value: `1` is the identity no-op, and `0` / negative / `NaN` /\n * `Infinity` are rejected so `writeUbo` never produces a non-finite `1/gamma` exponent for `pow`.\n */\nfunction isGammaActive(layer: Sprite2DLayer): boolean {\n const g = layer._coverageGamma;\n return g != null && Number.isFinite(g) && g > 0 && g !== 1;\n}\n\nconst COVERAGE_GAMMA_HOOK: SpriteCoverageGammaHook = {\n pipelineKeyPart(layer: Sprite2DLayer): string {\n return isGammaActive(layer) ? \"1\" : \"0\";\n },\n shaderModule(engine: EngineContext, hasDepth: boolean, layer: Sprite2DLayer): GPUShaderModule | null {\n if (!isGammaActive(layer)) {\n return null;\n }\n return getCoverageGammaShaderModule(engine, hasDepth, layer._uvScrollAttr != null);\n },\n writeUbo(layer: Sprite2DLayer, ubo: Float32Array): void {\n // aa.x = 1/coverageGamma for active gamma layers; 0 otherwise (the base shader ignores aa,\n // but the reused scratch UBO must stay deterministic across mixed gamma / non-gamma layers).\n if (isGammaActive(layer)) {\n ubo[12] = 1 / layer._coverageGamma!;\n } else {\n ubo[12] = 0;\n }\n },\n};\n\n/**\n * Enable (or update) coverage gamma on a sprite layer for anti-aliased glyph \"stem darkening\".\n *\n * Coverage gamma raises the layer's sampled texture alpha to `1/coverageGamma` in the fragment\n * shader, thickening anti-aliased edges to mimic the gamma-space blending of native text\n * rasterizers (DirectWrite/CoreText). Intended for glyph-atlas (bitmap text) layers drawn into an\n * sRGB (linear-blended) surface, where correct linear AA otherwise makes text look lighter/thinner.\n * Values `> 1` thicken; `1` is a no-op (identity) and disables the effect. Non-finite or non-positive\n * values (`0`, negatives, `NaN`, `Infinity`) are also treated as disabled (the base shader is used).\n *\n * **Opt-in & tree-shakable:** importing this function is what pulls the coverage-gamma shader\n * permutation and UBO writer into the bundle. Sprite scenes that never call it ship zero gamma\n * bytes. The gamma value is stored internally on the layer and is *only* settable through this\n * function — there is no create-time option and no public field, so a value can never be written\n * that the renderer would silently ignore. Call it before `createSpriteRenderer` /\n * `addDepthHostedSpriteLayer` so the layer's first pipeline is built with the gamma permutation;\n * calling it later is also safe — the renderer re-fetches the pipeline each frame and rebuilds it\n * with the gamma permutation on the next frame.\n *\n * @param layer - The sprite layer to configure.\n * @param gamma - Coverage gamma. Typical text values are ~1.8–2.2; `1` disables the effect.\n */\nexport function setSprite2DCoverageGamma(layer: Sprite2DLayer, gamma: number): void {\n _registerSpriteCoverageGammaHook(COVERAGE_GAMMA_HOOK);\n (layer as { _coverageGamma?: number })._coverageGamma = gamma;\n}\n"],"names":[],"mappings":";;;AAuBA,SAAS,qBAAA,CAAsB,QAAA,EAAmB,gBAAA,EAAyB,QAAA,EAA2B;AAClG,EAAA,OAAO,CAAA,EAAG,sBAAA,CAAuB,QAAA,EAAU,gBAAA,EAAkB,QAAQ,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA,CAAA;AAO1E;AAEA,IAAI,YAAA,GAAwE,IAAA;AAE5E,SAAS,4BAAA,CAA6B,MAAA,EAAuB,QAAA,EAAmB,QAAA,EAAoC;AAChH,EAAA,MAAM,SAAS,MAAA,CAAO,OAAA;AACtB,EAAA,MAAM,KAAA,GAAS,YAAA,qBAAiB,IAAI,OAAA,EAAQ;AAC5C,EAAA,IAAI,SAAA,GAAY,KAAA,CAAM,GAAA,CAAI,MAAM,CAAA;AAChC,EAAA,IAAI,CAAC,SAAA,EAAW;AACZ,IAAA,SAAA,uBAAgB,GAAA,EAAI;AACpB,IAAA,KAAA,CAAM,GAAA,CAAI,QAAQ,SAAS,CAAA;AAAA,EAC/B;AACA,EAAA,MAAM,GAAA,GAAM,GAAG,QAAA,GAAW,CAAA,GAAI,CAAC,CAAA,CAAA,EAAI,QAAA,GAAW,IAAI,CAAC,CAAA,CAAA;AACnD,EAAA,IAAI,MAAA,GAAS,SAAA,CAAU,GAAA,CAAI,GAAG,CAAA;AAC9B,EAAA,IAAI,CAAC,MAAA,EAAQ;AACT,IAAA,MAAA,GAAS,MAAA,CAAO,kBAAA,CAAmB,EAAE,IAAA,EAAM,qBAAA,CAAsB,QAAA,EAAU,QAAA,GAAW,CAAA,GAAI,CAAA,EAAG,QAAQ,CAAA,EAAG,CAAA;AACxG,IAAA,SAAA,CAAU,GAAA,CAAI,KAAK,MAAM,CAAA;AAAA,EAC7B;AACA,EAAA,OAAO,MAAA;AACX;AAOA,SAAS,cAAc,KAAA,EAA+B;AAClD,EAAA,MAAM,IAAI,KAAA,CAAM,cAAA;AAChB,EAAA,OAAO,CAAA,IAAK,QAAQ,MAAA,CAAO,QAAA,CAAS,CAAC,CAAA,IAAK,CAAA,GAAI,KAAK,CAAA,KAAM,CAAA;AAC7D;AAEA,MAAM,mBAAA,GAA+C;AAAA,EACjD,gBAAgB,KAAA,EAA8B;AAC1C,IAAA,OAAO,aAAA,CAAc,KAAK,CAAA,GAAI,GAAA,GAAM,GAAA;AAAA,EACxC,CAAA;AAAA,EACA,YAAA,CAAa,MAAA,EAAuB,QAAA,EAAmB,KAAA,EAA8C;AACjG,IAAA,IAAI,CAAC,aAAA,CAAc,KAAK,CAAA,EAAG;AACvB,MAAA,OAAO,IAAA;AAAA,IACX;AACA,IAAA,OAAO,4BAAA,CAA6B,MAAA,EAAQ,QAAA,EAAU,KAAA,CAAM,iBAAiB,IAAI,CAAA;AAAA,EACrF,CAAA;AAAA,EACA,QAAA,CAAS,OAAsB,GAAA,EAAyB;AAGpD,IAAA,IAAI,aAAA,CAAc,KAAK,CAAA,EAAG;AACtB,MAAA,GAAA,CAAI,EAAE,CAAA,GAAI,CAAA,GAAI,KAAA,CAAM,cAAA;AAAA,IACxB,CAAA,MAAO;AACH,MAAA,GAAA,CAAI,EAAE,CAAA,GAAI,CAAA;AAAA,IACd;AAAA,EACJ;AACJ,CAAA;AAwBO,SAAS,wBAAA,CAAyB,OAAsB,KAAA,EAAqB;AAChF,EAAA,gCAAA,CAAiC,mBAAmB,CAAA;AACpD,EAAC,MAAsC,cAAA,GAAiB,KAAA;AAC5D;;;;"}