@babylonjs/lite 1.9.0 → 1.10.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 (128) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/index.js +596 -526
  3. package/dist/index.js.map +1 -1
  4. package/index.d.ts +107 -4
  5. package/lib/engine/engine.js +1 -1
  6. package/lib/engine/engine.js.map +1 -1
  7. package/lib/frame-graph/render-task.js +6 -4
  8. package/lib/frame-graph/render-task.js.map +1 -1
  9. package/lib/index.js +4 -1
  10. package/lib/index.js.map +1 -1
  11. package/lib/loader-gltf/gltf-ext-quantization.js +4 -1
  12. package/lib/loader-gltf/gltf-ext-quantization.js.map +1 -1
  13. package/lib/loader-gltf/gltf-feature-registry.js +11 -1
  14. package/lib/loader-gltf/gltf-feature-registry.js.map +1 -1
  15. package/lib/loader-gltf/gltf-feature-variants.js +11 -1
  16. package/lib/loader-gltf/gltf-feature-variants.js.map +1 -1
  17. package/lib/loader-gltf/gltf-json-asset.js +8 -1
  18. package/lib/loader-gltf/gltf-json-asset.js.map +1 -1
  19. package/lib/loader-gltf/gltf-material.js +4 -29
  20. package/lib/loader-gltf/gltf-material.js.map +1 -1
  21. package/lib/loader-gltf/gltf-parser.js +1 -8
  22. package/lib/loader-gltf/gltf-parser.js.map +1 -1
  23. package/lib/loader-gltf/gltf-pbr-builder.js +1 -15
  24. package/lib/loader-gltf/gltf-pbr-builder.js.map +1 -1
  25. package/lib/loader-gltf/gltf-variants.js +5 -10
  26. package/lib/loader-gltf/gltf-variants.js.map +1 -1
  27. package/lib/loader-gltf/load-gltf.js +19 -29
  28. package/lib/loader-gltf/load-gltf.js.map +1 -1
  29. package/lib/material/pbr/background-dds-skybox.js +35 -93
  30. package/lib/material/pbr/background-dds-skybox.js.map +1 -1
  31. package/lib/material/pbr/background-ground.js +13 -18
  32. package/lib/material/pbr/background-ground.js.map +1 -1
  33. package/lib/material/pbr/background-hdr-skybox.js +35 -93
  34. package/lib/material/pbr/background-hdr-skybox.js.map +1 -1
  35. package/lib/material/pbr/background-solid-skybox.js +35 -93
  36. package/lib/material/pbr/background-solid-skybox.js.map +1 -1
  37. package/lib/material/pbr/fragments/clearcoat-fragment.js +5 -1
  38. package/lib/material/pbr/fragments/clearcoat-fragment.js.map +1 -1
  39. package/lib/material/pbr/fragments/iridescence-fragment.js +4 -1
  40. package/lib/material/pbr/fragments/iridescence-fragment.js.map +1 -1
  41. package/lib/material/pbr/fragments/refraction-rtt-fragment.js +7 -2
  42. package/lib/material/pbr/fragments/refraction-rtt-fragment.js.map +1 -1
  43. package/lib/material/pbr/fragments/sheen-fragment.js +2 -1
  44. package/lib/material/pbr/fragments/sheen-fragment.js.map +1 -1
  45. package/lib/material/pbr/fragments/subsurface-fragment.js +2 -1
  46. package/lib/material/pbr/fragments/subsurface-fragment.js.map +1 -1
  47. package/lib/material/pbr/fragments/unlit-fragment.js +1 -2
  48. package/lib/material/pbr/fragments/unlit-fragment.js.map +1 -1
  49. package/lib/material/pbr/pbr-compose.js +2 -0
  50. package/lib/material/pbr/pbr-compose.js.map +1 -1
  51. package/lib/material/pbr/pbr-flag-bits.js +1 -16
  52. package/lib/material/pbr/pbr-flag-bits.js.map +1 -1
  53. package/lib/material/pbr/pbr-flags.js +1 -1
  54. package/lib/material/pbr/pbr-fog-wgsl.js +1 -1
  55. package/lib/material/pbr/pbr-fog-wgsl.js.map +1 -1
  56. package/lib/material/pbr/pbr-geometry-output-shader.js +2 -1
  57. package/lib/material/pbr/pbr-geometry-output-shader.js.map +1 -1
  58. package/lib/material/pbr/pbr-geometry-view.js +2 -1
  59. package/lib/material/pbr/pbr-geometry-view.js.map +1 -1
  60. package/lib/material/pbr/pbr-renderable.js +4 -0
  61. package/lib/material/pbr/pbr-renderable.js.map +1 -1
  62. package/lib/material/pbr/pbr-template-gamma.js +7 -0
  63. package/lib/material/pbr/pbr-template-gamma.js.map +1 -0
  64. package/lib/material/pbr/pbr-template.js +2 -2
  65. package/lib/material/pbr/pbr-template.js.map +1 -1
  66. package/lib/material/standard/skybox-cubemap.js +1 -1
  67. package/lib/material/standard/skybox-cubemap.js.map +1 -1
  68. package/lib/material/standard/standard-template.js +1 -1
  69. package/lib/material/standard/standard-template.js.map +1 -1
  70. package/lib/mesh/GaussianSplatting/gaussian-splatting-pipeline-sh.js +3 -5
  71. package/lib/mesh/GaussianSplatting/gaussian-splatting-pipeline-sh.js.map +1 -1
  72. package/lib/mesh/GaussianSplatting/gaussian-splatting-pipeline.js +3 -5
  73. package/lib/mesh/GaussianSplatting/gaussian-splatting-pipeline.js.map +1 -1
  74. package/lib/particle/node/blocks/particle-condition-block.js +55 -0
  75. package/lib/particle/node/blocks/particle-condition-block.js.map +1 -0
  76. package/lib/particle/node/blocks/particle-float-to-int-block.js +28 -0
  77. package/lib/particle/node/blocks/particle-float-to-int-block.js.map +1 -0
  78. package/lib/particle/node/blocks/particle-gradient-block.js +32 -0
  79. package/lib/particle/node/blocks/particle-gradient-block.js.map +1 -0
  80. package/lib/particle/node/blocks/particle-gradient-value-block.js +11 -0
  81. package/lib/particle/node/blocks/particle-gradient-value-block.js.map +1 -0
  82. package/lib/particle/node/blocks/particle-lerp-block.js +2 -24
  83. package/lib/particle/node/blocks/particle-lerp-block.js.map +1 -1
  84. package/lib/particle/node/blocks/particle-lerp.js +27 -0
  85. package/lib/particle/node/blocks/particle-lerp.js.map +1 -0
  86. package/lib/particle/node/blocks/particle-vector-length-block.js +30 -0
  87. package/lib/particle/node/blocks/particle-vector-length-block.js.map +1 -0
  88. package/lib/particle/node/blocks/system-block.js +13 -3
  89. package/lib/particle/node/blocks/system-block.js.map +1 -1
  90. package/lib/particle/node/blocks/texture-source-block.js +2 -1
  91. package/lib/particle/node/blocks/texture-source-block.js.map +1 -1
  92. package/lib/particle/node/blocks/update-size-block.js +19 -0
  93. package/lib/particle/node/blocks/update-size-block.js.map +1 -0
  94. package/lib/particle/node/npe-build-state.js +10 -0
  95. package/lib/particle/node/npe-build-state.js.map +1 -1
  96. package/lib/particle/node/npe-registry.js +12 -0
  97. package/lib/particle/node/npe-registry.js.map +1 -1
  98. package/lib/particle/particle-system.js +7 -2
  99. package/lib/particle/particle-system.js.map +1 -1
  100. package/lib/picking/billboard-pick-pipeline.js +269 -0
  101. package/lib/picking/billboard-pick-pipeline.js.map +1 -0
  102. package/lib/picking/gpu-picker.js +37 -37
  103. package/lib/picking/gpu-picker.js.map +1 -1
  104. package/lib/picking/gs-picking-pipeline.js +26 -1
  105. package/lib/picking/gs-picking-pipeline.js.map +1 -1
  106. package/lib/picking/pick-contributor.js +13 -0
  107. package/lib/picking/pick-contributor.js.map +1 -0
  108. package/lib/picking/picking-info.js.map +1 -1
  109. package/lib/scene/scene-core.js +2 -2
  110. package/lib/scene/scene-core.js.map +1 -1
  111. package/lib/shader/wgsl-fog.js +18 -0
  112. package/lib/shader/wgsl-fog.js.map +1 -0
  113. package/lib/shader/wgsl-helpers.js +1 -16
  114. package/lib/shader/wgsl-helpers.js.map +1 -1
  115. package/lib/sprite/billboard-scene.js +2 -0
  116. package/lib/sprite/billboard-scene.js.map +1 -1
  117. package/lib/sprite/picking/pick-billboard.js +16 -0
  118. package/lib/sprite/picking/pick-billboard.js.map +1 -0
  119. package/lib/sprite/picking/pick-sprite-2d.js +41 -0
  120. package/lib/sprite/picking/pick-sprite-2d.js.map +1 -0
  121. package/lib/sprite/shared/sprite-atlas.js +4 -1
  122. package/lib/sprite/shared/sprite-atlas.js.map +1 -1
  123. package/lib/sprite/sprite-2d.js.map +1 -1
  124. package/lib/sprite/sprite-pipeline.js +18 -1
  125. package/lib/sprite/sprite-pipeline.js.map +1 -1
  126. package/lib/sprite/sprite-renderer.js +12 -6
  127. package/lib/sprite/sprite-renderer.js.map +1 -1
  128. package/package.json +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"scene-core.js","sources":["../../../src/scene/scene-core.ts"],"sourcesContent":["import type { EngineContext, RenderingContext } from \"../engine/engine.js\";\nimport { _vis, isRenderingContextRegistered, registerRenderingContext, unregisterRenderingContext } from \"../engine/engine.js\";\nimport type { SurfaceContext } from \"../engine/surface.js\";\nimport type { Camera } from \"../camera/camera.js\";\nimport type { LightBase } from \"../light/types.js\";\nimport type { Mesh } from \"../mesh/mesh.js\";\nimport { disposeMeshGpu } from \"../mesh/mesh-dispose.js\";\nimport { registerMeshScene, unregisterMeshScene, enqueueMaterialSwap } from \"./mesh-scene-registry.js\";\nimport { processMaterialSwaps } from \"./scene-material-swap.js\";\nimport type { AnimationGroup } from \"../animation/animation-group.js\";\nimport { tickAnimation } from \"../animation/animation-tick.js\";\nimport type { ShadowGenerator } from \"../shadow/shadow-generator.js\";\nimport type { FogConfig } from \"../material/standard/standard-material.js\";\nimport type { Renderable, PrePassRenderable, SceneUniformUpdater, MeshGroupBuilder } from \"../render/renderable.js\";\nimport type { TransformNode } from \"./transform-node.js\";\nimport type { SceneNode } from \"./scene-node.js\";\nimport type { EnvironmentTextures } from \"../loader-env/load-env.js\";\nimport type { FrameGraph } from \"../frame-graph/frame-graph.js\";\nimport { createFrameGraph, _appendTask } from \"../frame-graph/frame-graph.js\";\nimport { createRenderTask } from \"../frame-graph/render-task.js\";\nimport { createRenderTarget } from \"../engine/render-target.js\";\nimport type { AssetContainer } from \"../asset-container.js\";\nimport type { SceneLightGpuState } from \"../render/lights-ubo.js\";\nimport type { ClusteredLightContainer } from \"../light/clustered.js\";\nimport type { GaussianSplattingMesh } from \"../mesh/GaussianSplatting/gaussian-splatting-mesh.js\";\nimport type { ToneMapping } from \"../material/pbr/tone-mapping.js\";\n\n/** Image processing configuration. */\nexport interface ImageProcessingConfig {\n exposure: number;\n contrast: number;\n toneMappingEnabled: boolean;\n /**\n * Tone mapping algorithm applied by PBR materials when `toneMappingEnabled` is true.\n * Undefined means the default {@link StandardToneMapping} (exponential). Assign a\n * built-in ({@link StandardToneMapping}, {@link AcesToneMapping}, {@link NeutralToneMapping})\n * or a custom {@link ToneMapping}.\n *\n * This is baked into the PBR shaders at `registerScene()` time. To change it after\n * registration, use `setSceneImageProcessing` so the affected pipelines are rebuilt.\n */\n toneMapping?: ToneMapping;\n}\n\n/** A clipping plane expressed as the coefficients `[a, b, c, d]` of `a·x + b·y + c·z + d`. */\nexport type ClipPlane = readonly [number, number, number, number];\n\n/** Top-level scene context — pure state, no attached methods. */\nexport interface SceneContext extends RenderingContext {\n /** Surface this scene renders into. Set at scene-creation time and immutable\n * afterwards — the default render task is sized and MSAA-matched to this surface,\n * and `registerScene` attaches the scene to it. For the engine's primary surface\n * (the common single-canvas case) this is the engine itself. The owning engine is\n * reachable via `scene.surface.engine`. */\n readonly surface: SurfaceContext;\n clearColor: GPUColorDict;\n camera: Camera | null;\n lights: LightBase[];\n imageProcessing: ImageProcessingConfig;\n\n /** All meshes added to the scene (standard + PBR). */\n meshes: Mesh[];\n\n /** Animation groups loaded from glTF or created manually. */\n animationGroups: AnimationGroup[];\n\n /** Fog configuration. Null = no fog. */\n fog: FogConfig | null;\n\n /** Scene clip plane as (normal.x, normal.y, normal.z, d). Matches Babylon.js Plane `dot(worldPosition, plane) > 0` discard semantics. */\n clipPlane: ClipPlane | null;\n\n /** Shadow generators registered on this scene. */\n shadowGenerators: ShadowGenerator[];\n\n /** Background material primaryColor (linear RGB). Default from Babylon createDefaultEnvironment. */\n environmentPrimaryColor?: [number, number, number];\n\n /** Environment cubemap Y rotation in radians. */\n envRotationY?: number;\n\n /** Fixed delta time in ms for deterministic animation. 0 = use real rAF delta. */\n fixedDeltaMs: number;\n\n /** All renderables in this scene. The active frame-graph tasks bucket them\n * (opaque / direct / transparent) at bind time based on `isTransparent`, `_direct`, and `_transmissive`. */\n /** @internal */\n _renderables: Renderable[];\n /** @internal Pre-pass work (shadow maps, compute, etc.). */\n _prePasses: PrePassRenderable[];\n /** GaussianSplatting meshes attached to this scene. Populated by\n * `attachGaussianSplattingMesh`. Scene-core stays GS-agnostic apart from\n * this opaque registry (used by `gpu-picker` to iterate GS meshes without\n * scanning `_renderables`). */\n /** @internal */\n _gsMeshes: GaussianSplattingMesh[];\n /** @internal Scene uniform updaters (one per shared UBO). */\n _uniformUpdaters: SceneUniformUpdater[];\n /** @internal Opt-in feature writers for the SceneUniforms UBO (fog, clip plane, env SH).\n * Populated lazily via the scene-ubo-extras seam; run by the render task. */\n _sceneUboContributors?: ((data: Float32Array, scene: SceneContext) => void)[];\n /** @internal Per-frame callbacks run before rendering (animation, physics, etc.). */\n _beforeRender: ((deltaMs: number) => void)[];\n /** @internal Deferred builders — registered by loaders/factories, run once at startEngine(). */\n _deferredBuilders: (() => void | Promise<void>)[];\n /** @internal Mesh group registry — maps builder to its mesh list (internal bookkeeping). */\n _groups: Map<MeshGroupBuilder, Mesh[]>;\n\n // ─── Dispose infrastructure ────────────────────────────────\n /** @internal Shared cleanup callbacks (scene UBOs, lights UBOs, etc.). Registered by builders. */\n _disposables: (() => void)[];\n /** @internal Per-mesh cleanup callbacks (mesh UBOs, bind groups). For material swap + dispose. */\n _meshDisposables: Map<Mesh, (() => void)[]>;\n /** @internal Per-mesh cleanup callbacks for AUX (material-OVERRIDE) view packets that an explicit render\n * task registered on a mesh it does not own — e.g. a depth-prepass / SSAO no-colour view of a wall. Kept\n * SEPARATE from `_meshDisposables` because a MAIN-material swap (`processMaterialSwaps`, which rebuilds\n * only the main renderable) must NOT tear these down: they belong to another task, whose cached bundle\n * would then replay a destroyed system UBO (\"used in submit while destroyed\"). Drained only on a real mesh\n * removal (`removeFromScene`) and scene dispose, exactly like `_meshDisposables` minus the swap path. */\n _meshAuxDisposables: Map<Mesh, (() => void)[]>;\n /** @internal Meshes whose material was changed via setter — drained before each render frame. */\n _materialSwapQueue: Mesh[];\n /** @internal Monotonic counter bumped when the renderable list changes (add/remove/rebuild). */\n _renderableVersion: number;\n /** @internal Monotonic counter bumped ONLY when a material's renderables are rebuilt/swapped (material\n * swap drain or `rebuildMaterial`) — NOT on a geometry resize (which bumps `_renderableVersion` alone).\n * Lets consumers that cache material-view-derived GPU state (e.g. the CSM shadow tasks' no-color material\n * views) cheaply re-record on a geometry-only edit and only fully rebuild when a caster's material UBOs\n * were actually destroyed/recreated (which would otherwise leave their cached views dangling). */\n _materialEpoch: number;\n /** True once the initial deferred build (buildScene) has run. Meshes added after\n * this point are materialized via the per-frame swap drain rather than the\n * boot-only deferred-builder path. */\n /** @internal */\n _built: boolean;\n /** Builders whose deferred group build has COMPLETED. A mesh that joins one of these groups after the fact\n * (post-boot, or a glTF prop whose async load resolves DURING buildScene's drain and joins an already-built\n * group) is materialized via the per-frame swap drain: its group builder has already run and won't see it,\n * so without this it would cast shadows (it's in ctx.meshes) but never be drawn in the color pass. */\n /** @internal */\n _builtGroups: Set<MeshGroupBuilder>;\n\n // ─── Stashed internal state (typed to avoid `as any` casts) ────\n /** @internal */\n _envTextures?: EnvironmentTextures;\n /** @internal Scene-owned shared LightsUniforms UBO state (group 0 binding 1). */\n _lightGpuState?: SceneLightGpuState;\n\n /** Frame graph driving this scene's rendering. Created eagerly by\n * `createSceneContext` with a default `RenderTask` that mirrors\n * `_renderables` into the swapchain. User code may add additional tasks\n * (offscreen RTTs, post-FX, UI overlays, etc.). */\n /** @internal */\n _frameGraph: FrameGraph;\n\n /** @internal Optional clustered point-light container. Only populated by the clustered-light extension API. */\n _clusteredLightContainer?: ClusteredLightContainer;\n /** @internal Updates clustered light cells for the camera used by the current render pass. */\n _clusteredLightUpdater?: (camera: Camera | null | undefined, targetWidth: number, targetHeight: number) => void;\n}\n\n/** Options passed to the scene-context factory. */\nexport interface SceneContextOptions {\n defaultRenderTask?: boolean;\n}\n\n/** Create an empty scene context bound to the given `surface`. The default render task\n * is built against the surface's format, MSAA configuration, and swapchain RT — the\n * scene is permanently bound to that surface. Pass `engine` directly (since\n * `EngineContext extends SurfaceContext`) for the common single-canvas case, or pass\n * an auxiliary surface created via `createSurface`. */\nexport function createSceneContext(surface: SurfaceContext, options?: SceneContextOptions): SceneContext {\n const eng = surface.engine;\n\n // Closures below capture `ctx` by-reference via this object.\n const ctxLocal: Omit<SceneContext, \"_frameGraph\"> = {\n surface,\n clearColor: { r: 0.2, g: 0.2, b: 0.3, a: 1.0 },\n camera: null,\n lights: [],\n meshes: [],\n animationGroups: [],\n fog: null,\n clipPlane: null,\n shadowGenerators: [],\n imageProcessing: { exposure: 1.0, contrast: 1.0, toneMappingEnabled: false },\n _renderables: [],\n _prePasses: [],\n _gsMeshes: [],\n _uniformUpdaters: [],\n fixedDeltaMs: 0,\n _beforeRender: [],\n _deferredBuilders: [],\n _groups: new Map(),\n _disposables: [],\n _meshDisposables: new Map(),\n _meshAuxDisposables: new Map(),\n _materialSwapQueue: [],\n _renderableVersion: 0,\n _materialEpoch: 0,\n _built: false,\n _builtGroups: new Set(),\n _drawCallsPre: 0,\n\n _update(): void {\n // When the engine was created with `useFloatingOrigin: true`, mark\n // the active camera so `getViewMatrix` knows to zero its\n // translation column (the GPU view × world product is then the\n // eye-relative result the LWR offset trick produces). For non-LWR\n // engines `eng.useFloatingOrigin` is false and this is a single\n // boolean check per frame — the inner branch is dead.\n if (eng.useFloatingOrigin && ctx.camera && !ctx.camera._useFloatingOrigin) {\n ctx.camera._useFloatingOrigin = true;\n ctx.camera._viewVer = -1;\n ctx.camera._vpVer = -1;\n }\n const d = ctx.fixedDeltaMs > 0 ? ctx.fixedDeltaMs : eng._currentDelta;\n const encoder = eng._currentEncoder;\n let draws = 0;\n for (const cb of ctx._beforeRender) {\n cb(d);\n }\n if (ctx._materialSwapQueue.length > 0) {\n processMaterialSwaps(ctx);\n }\n for (const pp of ctx._prePasses) {\n draws += pp.execute(encoder, eng);\n }\n for (const u of ctx._uniformUpdaters) {\n u.update(eng);\n }\n ctx._drawCallsPre = draws;\n },\n _record(): number {\n return ctx._frameGraph.execute();\n },\n _resize(): void {\n // Canvas backing-store changed: rebuild the frame graph so canvas-sized\n // render targets get re-allocated at the new pixel size before the next record.\n ctx._frameGraph.build();\n },\n };\n\n const ctx = ctxLocal as SceneContext;\n // Eagerly attach the frame graph + a default swapchain render-pass task. The\n // graph drives all GPU work for this scene; user code can add more tasks\n // (offscreen RTTs, post-FX, UI overlays) before/after.\n const fg = createFrameGraph(eng);\n ctx._frameGraph = fg;\n if (options?.defaultRenderTask !== false) {\n // MSAA: render into an MSAA colour RT (which owns depth) and resolve into the\n // single-sample scRT. No MSAA: render straight into the colour-only\n // scRT with a task-owned single-sample depth buffer it builds/clears/frees.\n // All three reads (format / msaaSamples / scRT) come from the bound `surface`.\n const msaa = surface.msaaSamples > 1;\n const rt = msaa\n ? createRenderTarget({ lbl: \"scene-color\", format: surface.format, dFormat: \"depth24plus-stencil8\", samples: surface.msaaSamples, size: surface })\n : surface.scRT;\n const depth = msaa ? undefined : createRenderTarget({ lbl: \"scene-depth\", dFormat: \"depth24plus-stencil8\", samples: 1, size: surface });\n _appendTask(fg, createRenderTask({ name: \"scene\", rt, rst: msaa ? surface.scRT : undefined, depth, clrColor: ctx.clearColor }, eng, ctx));\n }\n ctx._disposables.push(() => fg.dispose());\n return ctx;\n}\n\n/** Register a callback to run before each rendered frame. */\nexport function onBeforeRender(scene: SceneContext, cb: (deltaMs: number) => void): void {\n (scene as SceneContext)._beforeRender.unshift(cb);\n}\n\n/** Register a callback to run when `disposeScene` is called. Used to tie\n * user-owned GPU resources (e.g. a `SpriteRenderer`) to the scene's lifetime. */\nexport function onSceneDispose(scene: SceneContext, cb: () => void): void {\n (scene as SceneContext)._disposables.push(cb);\n}\n\n/** Get the scene's frame graph. Always non-null — created in `createSceneContext`. */\nexport function getFrameGraph(scene: SceneContext): FrameGraph {\n return (scene as SceneContext)._frameGraph;\n}\n\nexport interface DeferredSceneRenderables {\n renderables: readonly Renderable[];\n dispose?: () => void;\n}\n\n/** @internal Register optional scene-hosted render work without teaching `addToScene` about the feature. */\nexport function addDeferredSceneRenderables(\n scene: SceneContext,\n build: (engine: EngineContext, scene: SceneContext) => DeferredSceneRenderables | Promise<DeferredSceneRenderables>\n): void {\n const ctx = scene as SceneContext;\n ctx._deferredBuilders.push(async () => {\n const built = await build(ctx.surface.engine, ctx);\n ctx._renderables.push(...built.renderables);\n if (built.dispose) {\n ctx._disposables.push(built.dispose);\n }\n });\n}\n\n/**\n * Adds an entity (mesh, light, camera, transform node, shadow generator, or asset container)\n * to the scene, dispatching on its type. Asset containers are unpacked and each contained\n * entity added recursively. Optional scene-hosted systems such as depth-hosted sprites\n * expose their own opt-in add functions so mesh-only scenes do not pay feature-specific\n * routing bytes here.\n * @param scene - The owning scene (pillar 4b: entities never reference the scene themselves).\n * @param entity - The entity (or asset container) to add.\n */\nexport function addToScene(scene: SceneContext, entity: Mesh | LightBase | Camera | ShadowGenerator | TransformNode | AssetContainer): void {\n const ctx = scene as SceneContext;\n // AssetContainer from loadGltf / loadBabylon — process each field present\n if (\"entities\" in entity) {\n const result = entity as AssetContainer;\n for (const e of result.entities) {\n addToScene(scene, e);\n }\n if (result.clearColor) {\n ctx.clearColor = result.clearColor;\n }\n if (result.camera && !ctx.camera) {\n ctx.camera = result.camera;\n }\n if (result.animationGroups?.length) {\n const engine = ctx.surface.engine;\n const groups = result.animationGroups;\n ctx.animationGroups.push(...groups);\n ctx._beforeRender.push((deltaMs: number) => {\n for (const g of groups) {\n tickAnimation(g, deltaMs, engine);\n }\n });\n }\n return;\n }\n if (\"_gpu\" in entity && \"material\" in entity) {\n const mesh = entity as unknown as Mesh;\n ctx.meshes.push(mesh);\n registerMeshScene(ctx, mesh);\n const build = mesh.material ? (mesh.material as unknown as { _buildGroup?: MeshGroupBuilder })._buildGroup : undefined;\n if (build) {\n let group = ctx._groups.get(build);\n if (!group) {\n group = [];\n ctx._groups.set(build, group);\n ctx._deferredBuilders.push(async () => {\n const result = await build(ctx, group!);\n ctx._renderables.push(...result.renderables);\n if (result.updater) {\n ctx._uniformUpdaters.push(result.updater);\n }\n // This group's meshes now have renderables; a mesh that joins it LATER (post-boot or\n // mid-drain) won't be seen by this builder and must be materialized via a swap instead.\n ctx._builtGroups.add(build);\n });\n }\n group.push(mesh);\n // Materialize this mesh's renderable through the per-frame material-swap drain when the boot-only\n // deferred builder won't cover it: either after the initial build (`_built`), or when joining a group\n // whose builder has ALREADY completed (`_builtGroups`) — e.g. a glTF prop whose async load resolves\n // mid-drain and joins an already-built group. A mesh joining a group whose builder has NOT yet run (a\n // brand-new group, or one still pending in the drain) is built by that builder, so it must NOT enqueue\n // here — that would insert a SECOND renderable for it. buildScene drains the queue at the end.\n if (ctx._built || ctx._builtGroups.has(build)) {\n enqueueMaterialSwap(ctx, mesh);\n }\n }\n } else if (\"lightType\" in entity) {\n ctx.lights.push(entity as LightBase);\n }\n // Recurse into children of meshes, lights, cameras — set parent links\n const kids = (entity as unknown as SceneNode).children;\n if (kids?.length) {\n for (const child of kids) {\n (child as unknown as SceneNode).parent = entity as unknown as SceneNode;\n addToScene(scene, child);\n }\n }\n}\n\n/** Release all GPU resources owned by this scene. */\nexport function disposeScene(scene: SceneContext): void {\n const ctx = scene as SceneContext;\n unregisterRenderingContext(ctx.surface, ctx);\n for (const fn of ctx._disposables) {\n fn();\n }\n for (const fns of ctx._meshDisposables.values()) {\n for (const fn of fns) {\n fn();\n }\n }\n ctx._meshDisposables.clear();\n for (const fns of ctx._meshAuxDisposables.values()) {\n for (const fn of fns) {\n fn();\n }\n }\n ctx._meshAuxDisposables.clear();\n for (const mesh of ctx.meshes) {\n // Free the mesh's shared GPU buffers only when this was its LAST owning scene.\n if (unregisterMeshScene(ctx, mesh)) {\n disposeMeshGpu(mesh);\n }\n }\n ctx.meshes.length = 0;\n ctx._renderables.length = 0;\n ctx._prePasses.length = 0;\n ctx._gsMeshes.length = 0;\n ctx._uniformUpdaters.length = 0;\n ctx._beforeRender.length = 0;\n ctx._deferredBuilders.length = 0;\n ctx._disposables.length = 0;\n ctx._materialSwapQueue.length = 0;\n ctx.lights.length = 0;\n ctx.animationGroups.length = 0;\n ctx.shadowGenerators.length = 0;\n ctx.camera = null;\n}\n\n/** @internal Run all deferred builders (called by registerScene's boot step before the first frame). */\nexport async function buildScene(scene: SceneContext): Promise<void> {\n const ctx = scene as SceneContext;\n // Discard material-swap requests enqueued during scene SETUP — a mesh added, then re-materialed before boot\n // via the mesh.material setter (e.g. scene12 assigns each row's material AFTER addToScene). The deferred\n // builders below build every group's meshes fresh with their FINAL material, so those swaps are redundant;\n // processing them would insert a SECOND renderable per mesh (double-draw). Only swaps enqueued DURING the\n // drain below — an async mesh that joins an already-built group (see addToScene/_builtGroups) — must survive.\n ctx._materialSwapQueue.length = 0;\n while (ctx._deferredBuilders.length > 0) {\n const builders = [...ctx._deferredBuilders];\n ctx._deferredBuilders = [];\n await Promise.all(builders.map(async (b) => b()));\n }\n // Build the renderables for any meshes that joined an already-built group mid-drain (queued above) before\n // the first frame, instead of leaving them casting shadows but invisible in the color pass.\n processMaterialSwaps(ctx);\n ctx._renderableVersion++;\n ctx._built = true;\n}\n\n/**\n * Register a scene with the engine. Builds deferred work, sorts renderables by order,\n * and adds the scene to its bound surface's render list in overlay order. The scene is\n * always attached to `scene.surface` (which equals the engine itself in the\n * single-canvas case).\n */\nexport async function registerScene(scene: SceneContext): Promise<void> {\n const ctx = scene;\n const surface = ctx.surface;\n if (isRenderingContextRegistered(surface, ctx)) {\n return;\n }\n await buildScene(scene);\n ctx._renderables.sort(byOrder);\n await Promise.all(ctx._frameGraph._tasks.map((task) => task._preload?.()).filter((preload): preload is Promise<void> => preload !== undefined));\n ctx._frameGraph.build();\n if (surface._renderingContexts.length > 0) {\n const overlay = await import(\"./swapchain-overlay.js\");\n overlay.configureSwapchainOverlayScene(surface, ctx);\n }\n registerRenderingContext(surface, ctx);\n}\n\n/**\n * Register a scene with the engine and install the scene-owned shadow frame-graph task.\n * Use only for scenes that generate shadow maps. Like {@link registerScene}, the scene\n * is attached to `scene.surface` (and its owning engine is `scene.surface.engine`).\n */\nexport async function registerSceneWithShadowSupport(scene: SceneContext): Promise<void> {\n const ctx = scene as SceneContext;\n const surface = ctx.surface;\n if (isRenderingContextRegistered(surface, ctx)) {\n return;\n }\n await buildScene(scene);\n ctx._renderables.sort(byOrder);\n await ensureShadowTask(surface.engine, ctx);\n await Promise.all(ctx._frameGraph._tasks.map((task) => task._preload?.()).filter((preload): preload is Promise<void> => preload !== undefined));\n ctx._frameGraph.build();\n if (surface._renderingContexts.length > 0) {\n const overlay = await import(\"./swapchain-overlay.js\");\n overlay.configureSwapchainOverlayScene(surface, ctx);\n }\n registerRenderingContext(surface, ctx);\n}\n\nconst byOrder = (a: Renderable, b: Renderable): number => a.order - b.order;\n\nasync function ensureShadowTask(engine: EngineContext, scene: SceneContext): Promise<void> {\n const { createShadowTask } = await import(\"../frame-graph/shadow-task.js\");\n scene._frameGraph._tasks.unshift(createShadowTask(engine, scene));\n}\n\n/** Remove a previously-registered scene. Idempotent. Does not dispose scene resources.\n * The scene is always removed from `scene.surface`. */\nexport function unregisterScene(scene: SceneContext): void {\n unregisterRenderingContext(scene.surface, scene as SceneContext);\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"scene-core.js","sources":["../../../src/scene/scene-core.ts"],"sourcesContent":["import type { EngineContext, RenderingContext } from \"../engine/engine.js\";\nimport { _vis, isRenderingContextRegistered, registerRenderingContext, unregisterRenderingContext } from \"../engine/engine.js\";\nimport type { SurfaceContext } from \"../engine/surface.js\";\nimport type { Camera } from \"../camera/camera.js\";\nimport type { LightBase } from \"../light/types.js\";\nimport type { Mesh } from \"../mesh/mesh.js\";\nimport { disposeMeshGpu } from \"../mesh/mesh-dispose.js\";\nimport { registerMeshScene, unregisterMeshScene, enqueueMaterialSwap } from \"./mesh-scene-registry.js\";\nimport { processMaterialSwaps } from \"./scene-material-swap.js\";\nimport type { AnimationGroup } from \"../animation/animation-group.js\";\nimport { tickAnimation } from \"../animation/animation-tick.js\";\nimport type { ShadowGenerator } from \"../shadow/shadow-generator.js\";\nimport type { FogConfig } from \"../material/standard/standard-material.js\";\nimport type { Renderable, PrePassRenderable, SceneUniformUpdater, MeshGroupBuilder } from \"../render/renderable.js\";\nimport type { TransformNode } from \"./transform-node.js\";\nimport type { SceneNode } from \"./scene-node.js\";\nimport type { EnvironmentTextures } from \"../loader-env/load-env.js\";\nimport type { FrameGraph } from \"../frame-graph/frame-graph.js\";\nimport { createFrameGraph, _appendTask } from \"../frame-graph/frame-graph.js\";\nimport { createRenderTask } from \"../frame-graph/render-task.js\";\nimport { createRenderTarget } from \"../engine/render-target.js\";\nimport type { AssetContainer } from \"../asset-container.js\";\nimport type { SceneLightGpuState } from \"../render/lights-ubo.js\";\nimport type { ClusteredLightContainer } from \"../light/clustered.js\";\nimport type { PickSource } from \"../picking/pick-contributor.js\";\nimport type { ToneMapping } from \"../material/pbr/tone-mapping.js\";\n\n/** Image processing configuration. */\nexport interface ImageProcessingConfig {\n exposure: number;\n contrast: number;\n toneMappingEnabled: boolean;\n /**\n * Tone mapping algorithm applied by PBR materials when `toneMappingEnabled` is true.\n * Undefined means the default {@link StandardToneMapping} (exponential). Assign a\n * built-in ({@link StandardToneMapping}, {@link AcesToneMapping}, {@link NeutralToneMapping})\n * or a custom {@link ToneMapping}.\n *\n * This is baked into the PBR shaders at `registerScene()` time. To change it after\n * registration, use `setSceneImageProcessing` so the affected pipelines are rebuilt.\n */\n toneMapping?: ToneMapping;\n}\n\n/** A clipping plane expressed as the coefficients `[a, b, c, d]` of `a·x + b·y + c·z + d`. */\nexport type ClipPlane = readonly [number, number, number, number];\n\n/** Top-level scene context — pure state, no attached methods. */\nexport interface SceneContext extends RenderingContext {\n /** Surface this scene renders into. Set at scene-creation time and immutable\n * afterwards — the default render task is sized and MSAA-matched to this surface,\n * and `registerScene` attaches the scene to it. For the engine's primary surface\n * (the common single-canvas case) this is the engine itself. The owning engine is\n * reachable via `scene.surface.engine`. */\n readonly surface: SurfaceContext;\n clearColor: GPUColorDict;\n camera: Camera | null;\n lights: LightBase[];\n imageProcessing: ImageProcessingConfig;\n\n /** All meshes added to the scene (standard + PBR). */\n meshes: Mesh[];\n\n /** Animation groups loaded from glTF or created manually. */\n animationGroups: AnimationGroup[];\n\n /** Fog configuration. Null = no fog. */\n fog: FogConfig | null;\n\n /** Scene clip plane as (normal.x, normal.y, normal.z, d). Matches Babylon.js Plane `dot(worldPosition, plane) > 0` discard semantics. */\n clipPlane: ClipPlane | null;\n\n /** Shadow generators registered on this scene. */\n shadowGenerators: ShadowGenerator[];\n\n /** Background material primaryColor (linear RGB). Default from Babylon createDefaultEnvironment. */\n environmentPrimaryColor?: [number, number, number];\n\n /** Environment cubemap Y rotation in radians. */\n envRotationY?: number;\n\n /** Fixed delta time in ms for deterministic animation. 0 = use real rAF delta. */\n fixedDeltaMs: number;\n\n /** All renderables in this scene. The active frame-graph tasks bucket them\n * (opaque / direct / transparent) at bind time based on `isTransparent`, `_direct`, and `_transmissive`. */\n /** @internal */\n _renderables: Renderable[];\n /** @internal Pre-pass work (shadow maps, compute, etc.). */\n _prePasses: PrePassRenderable[];\n /** Pick sources — one per optional pickable entity (GS mesh, billboard system, …). Registered by\n * the entity module via `registerPickSource` when the entity is added; each is pure data + a\n * dynamic-import thunk the GPU picker resolves (once) on the first pick, so rendering the entity\n * pulls no pick-pipeline bytes. Scene-core stays pick-agnostic apart from this opaque list. */\n /** @internal */\n _pickSources: PickSource[];\n /** @internal Scene uniform updaters (one per shared UBO). */\n _uniformUpdaters: SceneUniformUpdater[];\n /** @internal Opt-in feature writers for the SceneUniforms UBO (fog, clip plane, env SH).\n * Populated lazily via the scene-ubo-extras seam; run by the render task. */\n _sceneUboContributors?: ((data: Float32Array, scene: SceneContext) => void)[];\n /** @internal Per-frame callbacks run before rendering (animation, physics, etc.). */\n _beforeRender: ((deltaMs: number) => void)[];\n /** @internal Deferred builders — registered by loaders/factories, run once at startEngine(). */\n _deferredBuilders: (() => void | Promise<void>)[];\n /** @internal Mesh group registry — maps builder to its mesh list (internal bookkeeping). */\n _groups: Map<MeshGroupBuilder, Mesh[]>;\n\n // ─── Dispose infrastructure ────────────────────────────────\n /** @internal Shared cleanup callbacks (scene UBOs, lights UBOs, etc.). Registered by builders. */\n _disposables: (() => void)[];\n /** @internal Per-mesh cleanup callbacks (mesh UBOs, bind groups). For material swap + dispose. */\n _meshDisposables: Map<Mesh, (() => void)[]>;\n /** @internal Per-mesh cleanup callbacks for AUX (material-OVERRIDE) view packets that an explicit render\n * task registered on a mesh it does not own — e.g. a depth-prepass / SSAO no-colour view of a wall. Kept\n * SEPARATE from `_meshDisposables` because a MAIN-material swap (`processMaterialSwaps`, which rebuilds\n * only the main renderable) must NOT tear these down: they belong to another task, whose cached bundle\n * would then replay a destroyed system UBO (\"used in submit while destroyed\"). Drained only on a real mesh\n * removal (`removeFromScene`) and scene dispose, exactly like `_meshDisposables` minus the swap path. */\n _meshAuxDisposables: Map<Mesh, (() => void)[]>;\n /** @internal Meshes whose material was changed via setter — drained before each render frame. */\n _materialSwapQueue: Mesh[];\n /** @internal Monotonic counter bumped when the renderable list changes (add/remove/rebuild). */\n _renderableVersion: number;\n /** @internal Monotonic counter bumped ONLY when a material's renderables are rebuilt/swapped (material\n * swap drain or `rebuildMaterial`) — NOT on a geometry resize (which bumps `_renderableVersion` alone).\n * Lets consumers that cache material-view-derived GPU state (e.g. the CSM shadow tasks' no-color material\n * views) cheaply re-record on a geometry-only edit and only fully rebuild when a caster's material UBOs\n * were actually destroyed/recreated (which would otherwise leave their cached views dangling). */\n _materialEpoch: number;\n /** True once the initial deferred build (buildScene) has run. Meshes added after\n * this point are materialized via the per-frame swap drain rather than the\n * boot-only deferred-builder path. */\n /** @internal */\n _built: boolean;\n /** Builders whose deferred group build has COMPLETED. A mesh that joins one of these groups after the fact\n * (post-boot, or a glTF prop whose async load resolves DURING buildScene's drain and joins an already-built\n * group) is materialized via the per-frame swap drain: its group builder has already run and won't see it,\n * so without this it would cast shadows (it's in ctx.meshes) but never be drawn in the color pass. */\n /** @internal */\n _builtGroups: Set<MeshGroupBuilder>;\n\n // ─── Stashed internal state (typed to avoid `as any` casts) ────\n /** @internal */\n _envTextures?: EnvironmentTextures;\n /** @internal Scene-owned shared LightsUniforms UBO state (group 0 binding 1). */\n _lightGpuState?: SceneLightGpuState;\n\n /** Frame graph driving this scene's rendering. Created eagerly by\n * `createSceneContext` with a default `RenderTask` that mirrors\n * `_renderables` into the swapchain. User code may add additional tasks\n * (offscreen RTTs, post-FX, UI overlays, etc.). */\n /** @internal */\n _frameGraph: FrameGraph;\n\n /** @internal Optional clustered point-light container. Only populated by the clustered-light extension API. */\n _clusteredLightContainer?: ClusteredLightContainer;\n /** @internal Updates clustered light cells for the camera used by the current render pass. */\n _clusteredLightUpdater?: (camera: Camera | null | undefined, targetWidth: number, targetHeight: number) => void;\n}\n\n/** Options passed to the scene-context factory. */\nexport interface SceneContextOptions {\n defaultRenderTask?: boolean;\n}\n\n/** Create an empty scene context bound to the given `surface`. The default render task\n * is built against the surface's format, MSAA configuration, and swapchain RT — the\n * scene is permanently bound to that surface. Pass `engine` directly (since\n * `EngineContext extends SurfaceContext`) for the common single-canvas case, or pass\n * an auxiliary surface created via `createSurface`. */\nexport function createSceneContext(surface: SurfaceContext, options?: SceneContextOptions): SceneContext {\n const eng = surface.engine;\n\n // Closures below capture `ctx` by-reference via this object.\n const ctxLocal: Omit<SceneContext, \"_frameGraph\"> = {\n surface,\n clearColor: { r: 0.2, g: 0.2, b: 0.3, a: 1.0 },\n camera: null,\n lights: [],\n meshes: [],\n animationGroups: [],\n fog: null,\n clipPlane: null,\n shadowGenerators: [],\n imageProcessing: { exposure: 1.0, contrast: 1.0, toneMappingEnabled: false },\n _renderables: [],\n _prePasses: [],\n _pickSources: [],\n _uniformUpdaters: [],\n fixedDeltaMs: 0,\n _beforeRender: [],\n _deferredBuilders: [],\n _groups: new Map(),\n _disposables: [],\n _meshDisposables: new Map(),\n _meshAuxDisposables: new Map(),\n _materialSwapQueue: [],\n _renderableVersion: 0,\n _materialEpoch: 0,\n _built: false,\n _builtGroups: new Set(),\n _drawCallsPre: 0,\n\n _update(): void {\n // When the engine was created with `useFloatingOrigin: true`, mark\n // the active camera so `getViewMatrix` knows to zero its\n // translation column (the GPU view × world product is then the\n // eye-relative result the LWR offset trick produces). For non-LWR\n // engines `eng.useFloatingOrigin` is false and this is a single\n // boolean check per frame — the inner branch is dead.\n if (eng.useFloatingOrigin && ctx.camera && !ctx.camera._useFloatingOrigin) {\n ctx.camera._useFloatingOrigin = true;\n ctx.camera._viewVer = -1;\n ctx.camera._vpVer = -1;\n }\n const d = ctx.fixedDeltaMs > 0 ? ctx.fixedDeltaMs : eng._currentDelta;\n const encoder = eng._currentEncoder;\n let draws = 0;\n for (const cb of ctx._beforeRender) {\n cb(d);\n }\n if (ctx._materialSwapQueue.length > 0) {\n processMaterialSwaps(ctx);\n }\n for (const pp of ctx._prePasses) {\n draws += pp.execute(encoder, eng);\n }\n for (const u of ctx._uniformUpdaters) {\n u.update(eng);\n }\n ctx._drawCallsPre = draws;\n },\n _record(): number {\n return ctx._frameGraph.execute();\n },\n _resize(): void {\n // Canvas backing-store changed: rebuild the frame graph so canvas-sized\n // render targets get re-allocated at the new pixel size before the next record.\n ctx._frameGraph.build();\n },\n };\n\n const ctx = ctxLocal as SceneContext;\n // Eagerly attach the frame graph + a default swapchain render-pass task. The\n // graph drives all GPU work for this scene; user code can add more tasks\n // (offscreen RTTs, post-FX, UI overlays) before/after.\n const fg = createFrameGraph(eng);\n ctx._frameGraph = fg;\n if (options?.defaultRenderTask !== false) {\n // MSAA: render into an MSAA colour RT (which owns depth) and resolve into the\n // single-sample scRT. No MSAA: render straight into the colour-only\n // scRT with a task-owned single-sample depth buffer it builds/clears/frees.\n // All three reads (format / msaaSamples / scRT) come from the bound `surface`.\n const msaa = surface.msaaSamples > 1;\n const rt = msaa\n ? createRenderTarget({ lbl: \"scene-color\", format: surface.format, dFormat: \"depth24plus-stencil8\", samples: surface.msaaSamples, size: surface })\n : surface.scRT;\n const depth = msaa ? undefined : createRenderTarget({ lbl: \"scene-depth\", dFormat: \"depth24plus-stencil8\", samples: 1, size: surface });\n _appendTask(fg, createRenderTask({ name: \"scene\", rt, rst: msaa ? surface.scRT : undefined, depth, clrColor: ctx.clearColor }, eng, ctx));\n }\n ctx._disposables.push(() => fg.dispose());\n return ctx;\n}\n\n/** Register a callback to run before each rendered frame. */\nexport function onBeforeRender(scene: SceneContext, cb: (deltaMs: number) => void): void {\n (scene as SceneContext)._beforeRender.unshift(cb);\n}\n\n/** Register a callback to run when `disposeScene` is called. Used to tie\n * user-owned GPU resources (e.g. a `SpriteRenderer`) to the scene's lifetime. */\nexport function onSceneDispose(scene: SceneContext, cb: () => void): void {\n (scene as SceneContext)._disposables.push(cb);\n}\n\n/** Get the scene's frame graph. Always non-null — created in `createSceneContext`. */\nexport function getFrameGraph(scene: SceneContext): FrameGraph {\n return (scene as SceneContext)._frameGraph;\n}\n\nexport interface DeferredSceneRenderables {\n renderables: readonly Renderable[];\n dispose?: () => void;\n}\n\n/** @internal Register optional scene-hosted render work without teaching `addToScene` about the feature. */\nexport function addDeferredSceneRenderables(\n scene: SceneContext,\n build: (engine: EngineContext, scene: SceneContext) => DeferredSceneRenderables | Promise<DeferredSceneRenderables>\n): void {\n const ctx = scene as SceneContext;\n ctx._deferredBuilders.push(async () => {\n const built = await build(ctx.surface.engine, ctx);\n ctx._renderables.push(...built.renderables);\n if (built.dispose) {\n ctx._disposables.push(built.dispose);\n }\n });\n}\n\n/**\n * Adds an entity (mesh, light, camera, transform node, shadow generator, or asset container)\n * to the scene, dispatching on its type. Asset containers are unpacked and each contained\n * entity added recursively. Optional scene-hosted systems such as depth-hosted sprites\n * expose their own opt-in add functions so mesh-only scenes do not pay feature-specific\n * routing bytes here.\n * @param scene - The owning scene (pillar 4b: entities never reference the scene themselves).\n * @param entity - The entity (or asset container) to add.\n */\nexport function addToScene(scene: SceneContext, entity: Mesh | LightBase | Camera | ShadowGenerator | TransformNode | AssetContainer): void {\n const ctx = scene as SceneContext;\n // AssetContainer from loadGltf / loadBabylon — process each field present\n if (\"entities\" in entity) {\n const result = entity as AssetContainer;\n for (const e of result.entities) {\n addToScene(scene, e);\n }\n if (result.clearColor) {\n ctx.clearColor = result.clearColor;\n }\n if (result.camera && !ctx.camera) {\n ctx.camera = result.camera;\n }\n if (result.animationGroups?.length) {\n const engine = ctx.surface.engine;\n const groups = result.animationGroups;\n ctx.animationGroups.push(...groups);\n ctx._beforeRender.push((deltaMs: number) => {\n for (const g of groups) {\n tickAnimation(g, deltaMs, engine);\n }\n });\n }\n return;\n }\n if (\"_gpu\" in entity && \"material\" in entity) {\n const mesh = entity as unknown as Mesh;\n ctx.meshes.push(mesh);\n registerMeshScene(ctx, mesh);\n const build = mesh.material ? (mesh.material as unknown as { _buildGroup?: MeshGroupBuilder })._buildGroup : undefined;\n if (build) {\n let group = ctx._groups.get(build);\n if (!group) {\n group = [];\n ctx._groups.set(build, group);\n ctx._deferredBuilders.push(async () => {\n const result = await build(ctx, group!);\n ctx._renderables.push(...result.renderables);\n if (result.updater) {\n ctx._uniformUpdaters.push(result.updater);\n }\n // This group's meshes now have renderables; a mesh that joins it LATER (post-boot or\n // mid-drain) won't be seen by this builder and must be materialized via a swap instead.\n ctx._builtGroups.add(build);\n });\n }\n group.push(mesh);\n // Materialize this mesh's renderable through the per-frame material-swap drain when the boot-only\n // deferred builder won't cover it: either after the initial build (`_built`), or when joining a group\n // whose builder has ALREADY completed (`_builtGroups`) — e.g. a glTF prop whose async load resolves\n // mid-drain and joins an already-built group. A mesh joining a group whose builder has NOT yet run (a\n // brand-new group, or one still pending in the drain) is built by that builder, so it must NOT enqueue\n // here — that would insert a SECOND renderable for it. buildScene drains the queue at the end.\n if (ctx._built || ctx._builtGroups.has(build)) {\n enqueueMaterialSwap(ctx, mesh);\n }\n }\n } else if (\"lightType\" in entity) {\n ctx.lights.push(entity as LightBase);\n }\n // Recurse into children of meshes, lights, cameras — set parent links\n const kids = (entity as unknown as SceneNode).children;\n if (kids?.length) {\n for (const child of kids) {\n (child as unknown as SceneNode).parent = entity as unknown as SceneNode;\n addToScene(scene, child);\n }\n }\n}\n\n/** Release all GPU resources owned by this scene. */\nexport function disposeScene(scene: SceneContext): void {\n const ctx = scene as SceneContext;\n unregisterRenderingContext(ctx.surface, ctx);\n for (const fn of ctx._disposables) {\n fn();\n }\n for (const fns of ctx._meshDisposables.values()) {\n for (const fn of fns) {\n fn();\n }\n }\n ctx._meshDisposables.clear();\n for (const fns of ctx._meshAuxDisposables.values()) {\n for (const fn of fns) {\n fn();\n }\n }\n ctx._meshAuxDisposables.clear();\n for (const mesh of ctx.meshes) {\n // Free the mesh's shared GPU buffers only when this was its LAST owning scene.\n if (unregisterMeshScene(ctx, mesh)) {\n disposeMeshGpu(mesh);\n }\n }\n ctx.meshes.length = 0;\n ctx._renderables.length = 0;\n ctx._prePasses.length = 0;\n ctx._pickSources.length = 0;\n ctx._uniformUpdaters.length = 0;\n ctx._beforeRender.length = 0;\n ctx._deferredBuilders.length = 0;\n ctx._disposables.length = 0;\n ctx._materialSwapQueue.length = 0;\n ctx.lights.length = 0;\n ctx.animationGroups.length = 0;\n ctx.shadowGenerators.length = 0;\n ctx.camera = null;\n}\n\n/** @internal Run all deferred builders (called by registerScene's boot step before the first frame). */\nexport async function buildScene(scene: SceneContext): Promise<void> {\n const ctx = scene as SceneContext;\n // Discard material-swap requests enqueued during scene SETUP — a mesh added, then re-materialed before boot\n // via the mesh.material setter (e.g. scene12 assigns each row's material AFTER addToScene). The deferred\n // builders below build every group's meshes fresh with their FINAL material, so those swaps are redundant;\n // processing them would insert a SECOND renderable per mesh (double-draw). Only swaps enqueued DURING the\n // drain below — an async mesh that joins an already-built group (see addToScene/_builtGroups) — must survive.\n ctx._materialSwapQueue.length = 0;\n while (ctx._deferredBuilders.length > 0) {\n const builders = [...ctx._deferredBuilders];\n ctx._deferredBuilders = [];\n await Promise.all(builders.map(async (b) => b()));\n }\n // Build the renderables for any meshes that joined an already-built group mid-drain (queued above) before\n // the first frame, instead of leaving them casting shadows but invisible in the color pass.\n processMaterialSwaps(ctx);\n ctx._renderableVersion++;\n ctx._built = true;\n}\n\n/**\n * Register a scene with the engine. Builds deferred work, sorts renderables by order,\n * and adds the scene to its bound surface's render list in overlay order. The scene is\n * always attached to `scene.surface` (which equals the engine itself in the\n * single-canvas case).\n */\nexport async function registerScene(scene: SceneContext): Promise<void> {\n const ctx = scene;\n const surface = ctx.surface;\n if (isRenderingContextRegistered(surface, ctx)) {\n return;\n }\n await buildScene(scene);\n ctx._renderables.sort(byOrder);\n await Promise.all(ctx._frameGraph._tasks.map((task) => task._preload?.()).filter((preload): preload is Promise<void> => preload !== undefined));\n ctx._frameGraph.build();\n if (surface._renderingContexts.length > 0) {\n const overlay = await import(\"./swapchain-overlay.js\");\n overlay.configureSwapchainOverlayScene(surface, ctx);\n }\n registerRenderingContext(surface, ctx);\n}\n\n/**\n * Register a scene with the engine and install the scene-owned shadow frame-graph task.\n * Use only for scenes that generate shadow maps. Like {@link registerScene}, the scene\n * is attached to `scene.surface` (and its owning engine is `scene.surface.engine`).\n */\nexport async function registerSceneWithShadowSupport(scene: SceneContext): Promise<void> {\n const ctx = scene as SceneContext;\n const surface = ctx.surface;\n if (isRenderingContextRegistered(surface, ctx)) {\n return;\n }\n await buildScene(scene);\n ctx._renderables.sort(byOrder);\n await ensureShadowTask(surface.engine, ctx);\n await Promise.all(ctx._frameGraph._tasks.map((task) => task._preload?.()).filter((preload): preload is Promise<void> => preload !== undefined));\n ctx._frameGraph.build();\n if (surface._renderingContexts.length > 0) {\n const overlay = await import(\"./swapchain-overlay.js\");\n overlay.configureSwapchainOverlayScene(surface, ctx);\n }\n registerRenderingContext(surface, ctx);\n}\n\nconst byOrder = (a: Renderable, b: Renderable): number => a.order - b.order;\n\nasync function ensureShadowTask(engine: EngineContext, scene: SceneContext): Promise<void> {\n const { createShadowTask } = await import(\"../frame-graph/shadow-task.js\");\n scene._frameGraph._tasks.unshift(createShadowTask(engine, scene));\n}\n\n/** Remove a previously-registered scene. Idempotent. Does not dispose scene resources.\n * The scene is always removed from `scene.surface`. */\nexport function unregisterScene(scene: SceneContext): void {\n unregisterRenderingContext(scene.surface, scene as SceneContext);\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ const WGSL_FOG = `
2
+ const E_FOG: f32 = 2.71828;
3
+ fn calcFogFactor(fogDistance: vec3<f32>) -> f32 {
4
+ var fogCoeff: f32 = 1.0;
5
+ let fogMode = scene.vFogInfos.x;
6
+ let fogStart = scene.vFogInfos.y;
7
+ let fogEnd = scene.vFogInfos.z;
8
+ let fogDensity = scene.vFogInfos.w;
9
+ let dist = length(fogDistance);
10
+ if (fogMode == 3.0) { fogCoeff = (fogEnd - dist) / (fogEnd - fogStart); }
11
+ else if (fogMode == 1.0) { fogCoeff = 1.0 / pow(E_FOG, dist * fogDensity); }
12
+ else if (fogMode == 2.0) { fogCoeff = 1.0 / pow(E_FOG, dist * dist * fogDensity * fogDensity); }
13
+ return clamp(fogCoeff, 0.0, 1.0);
14
+ }
15
+ `;
16
+
17
+ export { WGSL_FOG };
18
+ //# sourceMappingURL=wgsl-fog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wgsl-fog.js","sources":["../../../src/shader/wgsl-fog.ts"],"sourcesContent":["/** Fog calculation helper.\n * Requires: `scene.vFogInfos` (vec4) in scope. */\nexport const WGSL_FOG = `\nconst E_FOG: f32 = 2.71828;\nfn calcFogFactor(fogDistance: vec3<f32>) -> f32 {\nvar fogCoeff: f32 = 1.0;\nlet fogMode = scene.vFogInfos.x;\nlet fogStart = scene.vFogInfos.y;\nlet fogEnd = scene.vFogInfos.z;\nlet fogDensity = scene.vFogInfos.w;\nlet dist = length(fogDistance);\nif (fogMode == 3.0) { fogCoeff = (fogEnd - dist) / (fogEnd - fogStart); }\nelse if (fogMode == 1.0) { fogCoeff = 1.0 / pow(E_FOG, dist * fogDensity); }\nelse if (fogMode == 2.0) { fogCoeff = 1.0 / pow(E_FOG, dist * dist * fogDensity * fogDensity); }\nreturn clamp(fogCoeff, 0.0, 1.0);\n}\n`;\n"],"names":[],"mappings":"AAEO,MAAM,QAAA,GAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;;"}
@@ -31,21 +31,6 @@ let esm = 1.0 - clamp(exp(min(87.0, depthScale * shadowPixelDepth)) * shadowMapS
31
31
  return computeFallOff(esm, clipSpace.xy, frustumEdgeFalloff);
32
32
  }
33
33
  `;
34
- const WGSL_FOG = `
35
- const E_FOG: f32 = 2.71828;
36
- fn calcFogFactor(fogDistance: vec3<f32>) -> f32 {
37
- var fogCoeff: f32 = 1.0;
38
- let fogMode = scene.vFogInfos.x;
39
- let fogStart = scene.vFogInfos.y;
40
- let fogEnd = scene.vFogInfos.z;
41
- let fogDensity = scene.vFogInfos.w;
42
- let dist = length(fogDistance);
43
- if (fogMode == 3.0) { fogCoeff = (fogEnd - dist) / (fogEnd - fogStart); }
44
- else if (fogMode == 1.0) { fogCoeff = 1.0 / pow(E_FOG, dist * fogDensity); }
45
- else if (fogMode == 2.0) { fogCoeff = 1.0 / pow(E_FOG, dist * dist * fogDensity * fogDensity); }
46
- return clamp(fogCoeff, 0.0, 1.0);
47
- }
48
- `;
49
34
  const WGSL_DITHER = `
50
35
  fn dither(seed: vec2<f32>, varianceAmount: f32) -> f32 {
51
36
  let rand = fract(sin(dot(seed, vec2<f32>(12.9898, 78.233))) * 43758.5453);
@@ -55,5 +40,5 @@ return mix(-normVariance, normVariance, rand);
55
40
  `;
56
41
  const WGSL_NO_DITHER = "fn dither(a:vec2<f32>,b:f32)->f32{return 0.0;}";
57
42
 
58
- export { WGSL_DITHER, WGSL_FOG, WGSL_NO_DITHER, WGSL_PERTURB_NORMAL, WGSL_SHADOW_ESM };
43
+ export { WGSL_DITHER, WGSL_NO_DITHER, WGSL_PERTURB_NORMAL, WGSL_SHADOW_ESM };
59
44
  //# sourceMappingURL=wgsl-helpers.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"wgsl-helpers.js","sources":["../../../src/shader/wgsl-helpers.ts"],"sourcesContent":["/** Shared WGSL helper snippets used by both PBR and Standard material systems.\n *\n * These are pure WGSL function strings — no bindings, no UBO declarations.\n * Each material system wraps them with its own binding declarations. */\n\n/** Cotangent-frame bump mapping.\n * Requires: `bT` (texture_2d) and `bS` (sampler) in scope. */\nexport const WGSL_PERTURB_NORMAL = `\nfn perturbNormal(vNormalW: vec3<f32>, positionW: vec3<f32>, uv: vec2<f32>, bumpScale: f32) -> vec3<f32> {\nlet normalSample = textureSample(bT, bS, uv).rgb * 2.0 - 1.0;\nlet N = normalize(vNormalW) * bumpScale;\nlet dp1 = dpdx(positionW);\nlet dp2 = -dpdy(positionW);\nlet duv1 = dpdx(uv);\nlet duv2 = -dpdy(uv);\nlet dp2perp = cross(dp2, N);\nlet dp1perp = cross(N, dp1);\nvar tangent = dp2perp * duv1.x + dp1perp * duv2.x;\nvar bitangent = dp2perp * duv1.y + dp1perp * duv2.y;\nlet det = max(dot(tangent, tangent), dot(bitangent, bitangent));\nlet invmax = select(inverseSqrt(det), 0.0, det == 0.0);\nlet cotangentFrame = mat3x3<f32>(tangent * invmax, bitangent * invmax, N);\nreturn normalize(cotangentFrame * normalSample);\n}\n`;\n\n/** ESM shadow helper functions.\n * Requires: `shadowTex` (texture_2d), `shadowSampler` (sampler) in scope. */\nexport const WGSL_SHADOW_ESM = `\nfn computeFallOff(value: f32, clipSpace: vec2<f32>, frustumEdgeFalloff: f32) -> f32 {\nlet mask = smoothstep(1.0 - frustumEdgeFalloff, 1.00000012, clamp(dot(clipSpace, clipSpace), 0.0, 1.0));\nreturn mix(value, 1.0, mask);\n}\nfn computeShadowWithESM(posFromLight: vec4<f32>, depthMetric: f32, darkness: f32, depthScale: f32, frustumEdgeFalloff: f32) -> f32 {\nlet clipSpace = posFromLight.xyz / posFromLight.w;\nlet uv = vec2<f32>(0.5 * clipSpace.x + 0.5, 0.5 - 0.5 * clipSpace.y);\nif (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) { return 1.0; }\nlet shadowPixelDepth = clamp(depthMetric, 0.0, 1.0);\nlet shadowMapSample = textureSampleLevel(shadowTex, shadowSampler, uv, 0.0).x;\nlet esm = 1.0 - clamp(exp(min(87.0, depthScale * shadowPixelDepth)) * shadowMapSample, 0.0, 1.0 - darkness);\nreturn computeFallOff(esm, clipSpace.xy, frustumEdgeFalloff);\n}\n`;\n\n/** Fog calculation helper.\n * Requires: `scene.vFogInfos` (vec4) in scope. */\nexport const WGSL_FOG = `\nconst E_FOG: f32 = 2.71828;\nfn calcFogFactor(fogDistance: vec3<f32>) -> f32 {\nvar fogCoeff: f32 = 1.0;\nlet fogMode = scene.vFogInfos.x;\nlet fogStart = scene.vFogInfos.y;\nlet fogEnd = scene.vFogInfos.z;\nlet fogDensity = scene.vFogInfos.w;\nlet dist = length(fogDistance);\nif (fogMode == 3.0) { fogCoeff = (fogEnd - dist) / (fogEnd - fogStart); }\nelse if (fogMode == 1.0) { fogCoeff = 1.0 / pow(E_FOG, dist * fogDensity); }\nelse if (fogMode == 2.0) { fogCoeff = 1.0 / pow(E_FOG, dist * dist * fogDensity * fogDensity); }\nreturn clamp(fogCoeff, 0.0, 1.0);\n}\n`;\n\n/** Dither noise function.\n * Pure math — no UBO dependency. */\nexport const WGSL_DITHER = `\nfn dither(seed: vec2<f32>, varianceAmount: f32) -> f32 {\nlet rand = fract(sin(dot(seed, vec2<f32>(12.9898, 78.233))) * 43758.5453);\nlet normVariance = varianceAmount / 255.0;\nreturn mix(-normVariance, normVariance, rand);\n}\n`;\n\n/** Noise-disabled replacement for shaders that still call dither(). */\nexport const WGSL_NO_DITHER = \"fn dither(a:vec2<f32>,b:f32)->f32{return 0.0;}\";\n"],"names":[],"mappings":"AAOO,MAAM,mBAAA,GAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqB5B,MAAM,eAAA,GAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkBxB,MAAM,QAAA,GAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkBjB,MAAM,WAAA,GAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASpB,MAAM,cAAA,GAAiB;;;;"}
1
+ {"version":3,"file":"wgsl-helpers.js","sources":["../../../src/shader/wgsl-helpers.ts"],"sourcesContent":["/** Shared WGSL helper snippets used by both PBR and Standard material systems.\n *\n * These are pure WGSL function strings — no bindings, no UBO declarations.\n * Each material system wraps them with its own binding declarations. */\n\n/** Cotangent-frame bump mapping.\n * Requires: `bT` (texture_2d) and `bS` (sampler) in scope. */\nexport const WGSL_PERTURB_NORMAL = `\nfn perturbNormal(vNormalW: vec3<f32>, positionW: vec3<f32>, uv: vec2<f32>, bumpScale: f32) -> vec3<f32> {\nlet normalSample = textureSample(bT, bS, uv).rgb * 2.0 - 1.0;\nlet N = normalize(vNormalW) * bumpScale;\nlet dp1 = dpdx(positionW);\nlet dp2 = -dpdy(positionW);\nlet duv1 = dpdx(uv);\nlet duv2 = -dpdy(uv);\nlet dp2perp = cross(dp2, N);\nlet dp1perp = cross(N, dp1);\nvar tangent = dp2perp * duv1.x + dp1perp * duv2.x;\nvar bitangent = dp2perp * duv1.y + dp1perp * duv2.y;\nlet det = max(dot(tangent, tangent), dot(bitangent, bitangent));\nlet invmax = select(inverseSqrt(det), 0.0, det == 0.0);\nlet cotangentFrame = mat3x3<f32>(tangent * invmax, bitangent * invmax, N);\nreturn normalize(cotangentFrame * normalSample);\n}\n`;\n\n/** ESM shadow helper functions.\n * Requires: `shadowTex` (texture_2d), `shadowSampler` (sampler) in scope. */\nexport const WGSL_SHADOW_ESM = `\nfn computeFallOff(value: f32, clipSpace: vec2<f32>, frustumEdgeFalloff: f32) -> f32 {\nlet mask = smoothstep(1.0 - frustumEdgeFalloff, 1.00000012, clamp(dot(clipSpace, clipSpace), 0.0, 1.0));\nreturn mix(value, 1.0, mask);\n}\nfn computeShadowWithESM(posFromLight: vec4<f32>, depthMetric: f32, darkness: f32, depthScale: f32, frustumEdgeFalloff: f32) -> f32 {\nlet clipSpace = posFromLight.xyz / posFromLight.w;\nlet uv = vec2<f32>(0.5 * clipSpace.x + 0.5, 0.5 - 0.5 * clipSpace.y);\nif (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) { return 1.0; }\nlet shadowPixelDepth = clamp(depthMetric, 0.0, 1.0);\nlet shadowMapSample = textureSampleLevel(shadowTex, shadowSampler, uv, 0.0).x;\nlet esm = 1.0 - clamp(exp(min(87.0, depthScale * shadowPixelDepth)) * shadowMapSample, 0.0, 1.0 - darkness);\nreturn computeFallOff(esm, clipSpace.xy, frustumEdgeFalloff);\n}\n`;\n\n/** Dither noise function.\n * Pure math — no UBO dependency. */\nexport const WGSL_DITHER = `\nfn dither(seed: vec2<f32>, varianceAmount: f32) -> f32 {\nlet rand = fract(sin(dot(seed, vec2<f32>(12.9898, 78.233))) * 43758.5453);\nlet normVariance = varianceAmount / 255.0;\nreturn mix(-normVariance, normVariance, rand);\n}\n`;\n\n/** Noise-disabled replacement for shaders that still call dither(). */\nexport const WGSL_NO_DITHER = \"fn dither(a:vec2<f32>,b:f32)->f32{return 0.0;}\";\n"],"names":[],"mappings":"AAOO,MAAM,mBAAA,GAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqB5B,MAAM,eAAA,GAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkBxB,MAAM,WAAA,GAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASpB,MAAM,cAAA,GAAiB;;;;"}
@@ -1,6 +1,8 @@
1
1
  import { addDeferredSceneRenderables } from '../scene/scene-core.js';
2
+ import { registerPickSource } from '../picking/pick-contributor.js';
2
3
 
3
4
  function addBillboardSystem(scene, system) {
5
+ scene._disposables.push(registerPickSource(scene, system, () => import('../picking/billboard-pick-pipeline.js')));
4
6
  addDeferredSceneRenderables(scene, async (engine) => {
5
7
  const { buildBillboardRenderable } = await import('./billboard-renderable.js');
6
8
  const built = buildBillboardRenderable(engine, system);
@@ -1 +1 @@
1
- {"version":3,"file":"billboard-scene.js","sources":["../../../src/sprite/billboard-scene.ts"],"sourcesContent":["import type { SceneContext } from \"../scene/scene-core.js\";\nimport { addDeferredSceneRenderables } from \"../scene/scene-core.js\";\nimport type { AxisLockedBillboardSpriteSystem, BillboardSpriteSystem, FacingBillboardSpriteSystem } from \"./billboard-sprite.js\";\n\nfunction addBillboardSystem(scene: SceneContext, system: BillboardSpriteSystem): void {\n addDeferredSceneRenderables(scene, async (engine) => {\n const { buildBillboardRenderable } = await import(\"./billboard-renderable.js\");\n const built = buildBillboardRenderable(engine, system);\n return { renderables: [built.renderable], dispose: built.dispose };\n });\n}\n\n/**\n * Adds a camera-facing billboard sprite system to the scene so it is rendered each frame.\n * @param scene - Scene that will own and draw the system.\n * @param system - Facing billboard system to register.\n */\nexport function addFacingBillboardSystem(scene: SceneContext, system: FacingBillboardSpriteSystem): void {\n addBillboardSystem(scene, system);\n}\n\n/**\n * Adds an axis-locked billboard sprite system to the scene so it is rendered each frame.\n * @param scene - Scene that will own and draw the system.\n * @param system - Axis-locked billboard system to register.\n */\nexport function addAxisLockedBillboardSystem(scene: SceneContext, system: AxisLockedBillboardSpriteSystem): void {\n addBillboardSystem(scene, system);\n}\n"],"names":[],"mappings":";;AAIA,SAAS,kBAAA,CAAmB,OAAqB,MAAA,EAAqC;AAClF,EAAA,2BAAA,CAA4B,KAAA,EAAO,OAAO,MAAA,KAAW;AACjD,IAAA,MAAM,EAAE,wBAAA,EAAyB,GAAI,MAAM,OAAO,2BAA2B,CAAA;AAC7E,IAAA,MAAM,KAAA,GAAQ,wBAAA,CAAyB,MAAA,EAAQ,MAAM,CAAA;AACrD,IAAA,OAAO,EAAE,aAAa,CAAC,KAAA,CAAM,UAAU,CAAA,EAAG,OAAA,EAAS,MAAM,OAAA,EAAQ;AAAA,EACrE,CAAC,CAAA;AACL;AAOO,SAAS,wBAAA,CAAyB,OAAqB,MAAA,EAA2C;AACrG,EAAA,kBAAA,CAAmB,OAAO,MAAM,CAAA;AACpC;AAOO,SAAS,4BAAA,CAA6B,OAAqB,MAAA,EAA+C;AAC7G,EAAA,kBAAA,CAAmB,OAAO,MAAM,CAAA;AACpC;;;;"}
1
+ {"version":3,"file":"billboard-scene.js","sources":["../../../src/sprite/billboard-scene.ts"],"sourcesContent":["import type { SceneContext } from \"../scene/scene-core.js\";\nimport { addDeferredSceneRenderables } from \"../scene/scene-core.js\";\nimport type { AxisLockedBillboardSpriteSystem, BillboardSpriteSystem, FacingBillboardSpriteSystem } from \"./billboard-sprite.js\";\nimport { registerPickSource } from \"../picking/pick-contributor.js\";\n\nfunction addBillboardSystem(scene: SceneContext, system: BillboardSpriteSystem): void {\n // Make this system pickable by registering a pick source (the system + a dynamic-import thunk for\n // its pick pipeline). Pure data — billboard *rendering* pulls no pick-pipeline bytes; the picker\n // builds the contributor on the first pick via the pipeline's `createPickContributor`. The\n // unregister is disposed with the scene so the source doesn't outlive the system.\n scene._disposables.push(registerPickSource(scene, system, () => import(\"../picking/billboard-pick-pipeline.js\")));\n addDeferredSceneRenderables(scene, async (engine) => {\n const { buildBillboardRenderable } = await import(\"./billboard-renderable.js\");\n const built = buildBillboardRenderable(engine, system);\n return { renderables: [built.renderable], dispose: built.dispose };\n });\n}\n\n/**\n * Adds a camera-facing billboard sprite system to the scene so it is rendered each frame.\n * @param scene - Scene that will own and draw the system.\n * @param system - Facing billboard system to register.\n */\nexport function addFacingBillboardSystem(scene: SceneContext, system: FacingBillboardSpriteSystem): void {\n addBillboardSystem(scene, system);\n}\n\n/**\n * Adds an axis-locked billboard sprite system to the scene so it is rendered each frame.\n * @param scene - Scene that will own and draw the system.\n * @param system - Axis-locked billboard system to register.\n */\nexport function addAxisLockedBillboardSystem(scene: SceneContext, system: AxisLockedBillboardSpriteSystem): void {\n addBillboardSystem(scene, system);\n}\n"],"names":[],"mappings":";;;AAKA,SAAS,kBAAA,CAAmB,OAAqB,MAAA,EAAqC;AAKlF,EAAA,KAAA,CAAM,YAAA,CAAa,KAAK,kBAAA,CAAmB,KAAA,EAAO,QAAQ,MAAM,OAAO,uCAAuC,CAAC,CAAC,CAAA;AAChH,EAAA,2BAAA,CAA4B,KAAA,EAAO,OAAO,MAAA,KAAW;AACjD,IAAA,MAAM,EAAE,wBAAA,EAAyB,GAAI,MAAM,OAAO,2BAA2B,CAAA;AAC7E,IAAA,MAAM,KAAA,GAAQ,wBAAA,CAAyB,MAAA,EAAQ,MAAM,CAAA;AACrD,IAAA,OAAO,EAAE,aAAa,CAAC,KAAA,CAAM,UAAU,CAAA,EAAG,OAAA,EAAS,MAAM,OAAA,EAAQ;AAAA,EACrE,CAAC,CAAA;AACL;AAOO,SAAS,wBAAA,CAAyB,OAAqB,MAAA,EAA2C;AACrG,EAAA,kBAAA,CAAmB,OAAO,MAAM,CAAA;AACpC;AAOO,SAAS,4BAAA,CAA6B,OAAqB,MAAA,EAA+C;AAC7G,EAAA,kBAAA,CAAmB,OAAO,MAAM,CAAA;AACpC;;;;"}
@@ -0,0 +1,16 @@
1
+ import { pickAsync, disposePicker, createGpuPicker } from '../../picking/gpu-picker.js';
2
+
3
+ async function pickBillboardSprite(scene, x, y, picker) {
4
+ const owned = picker ?? createGpuPicker(scene);
5
+ try {
6
+ const info = await pickAsync(owned, x, y);
7
+ return info._spritePick ?? null;
8
+ } finally {
9
+ if (picker === void 0) {
10
+ disposePicker(owned);
11
+ }
12
+ }
13
+ }
14
+
15
+ export { pickBillboardSprite };
16
+ //# sourceMappingURL=pick-billboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pick-billboard.js","sources":["../../../../src/sprite/picking/pick-billboard.ts"],"sourcesContent":["/**\n * `pickBillboardSprite` — GPU hit-test for `*BillboardSpriteSystem` sprites.\n *\n * Billboards live in the scene's 3D pass, interleaved with meshes by depth, so picking one\n * requires the same depth-sorted GPU pass the mesh picker uses (a billboard behind a wall must\n * not be picked through it). This is a thin wrapper over the shared {@link pickAsync} pass: the\n * picker draws meshes, Gaussian-splatting meshes, and every billboard system into one 1×1\n * depth-sorted target; when a billboard sprite is the closest hit it attaches a `_spritePick`\n * payload, which this function extracts.\n *\n * Imported only when an app calls `pickBillboardSprite`, so scenes that never pick a billboard\n * pay zero bytes for it (and a billboard-free scene never even fetches the GPU pick pipeline).\n */\nimport type { SceneContext } from \"../../scene/scene-core.js\";\nimport { createGpuPicker, pickAsync, disposePicker, type GpuPicker } from \"../../picking/gpu-picker.js\";\nimport type { BillboardPickInfo } from \"../../picking/billboard-pick-pipeline.js\";\n\nexport type { BillboardPickInfo } from \"../../picking/billboard-pick-pipeline.js\";\n\n/**\n * Pick the topmost billboard sprite under a point, respecting occlusion by meshes and other\n * billboards (closest wins). Returns `null` if no billboard is the closest hit there — including\n * when a mesh is in front of the billboard, in which case the billboard is considered occluded.\n *\n * By default a throwaway {@link GpuPicker} is created and disposed per call, so a one-off pick\n * (e.g. click-to-select) retains no GPU state. For high-frequency picking (hover, drag) pass a\n * caller-owned `picker` from {@link createGpuPicker}: its 1×1 targets, scene UBO, and per-source\n * contributors (which compile the pick pipelines) are then reused across calls instead of rebuilt\n * each time, and the caller disposes it once via {@link disposePicker} when done. A supplied picker\n * must belong to `scene` — the same one passed to `createGpuPicker`.\n *\n * @param scene - Scene whose billboard systems (and meshes, for occlusion) are tested.\n * @param x - Query X in CSS pixels relative to the scene's canvas (same convention as `pickAsync`).\n * @param y - Query Y in CSS pixels relative to the scene's canvas.\n * @param picker - Optional caller-owned picker to reuse; when omitted, one is created and disposed per call.\n * @returns The billboard hit (`{ system, spriteIndex, pickedPoint, distance }`), or `null` for a miss.\n */\nexport async function pickBillboardSprite(scene: SceneContext, x: number, y: number, picker?: GpuPicker): Promise<BillboardPickInfo | null> {\n const owned = picker ?? createGpuPicker(scene);\n try {\n const info = await pickAsync(owned, x, y);\n return info._spritePick ?? null;\n } finally {\n // Only dispose a picker we created here; a caller-supplied picker is theirs to reuse and dispose.\n if (picker === undefined) {\n disposePicker(owned);\n }\n }\n}\n"],"names":[],"mappings":";;AAqCA,eAAsB,mBAAA,CAAoB,KAAA,EAAqB,CAAA,EAAW,CAAA,EAAW,MAAA,EAAuD;AACxI,EAAA,MAAM,KAAA,GAAQ,MAAA,IAAU,eAAA,CAAgB,KAAK,CAAA;AAC7C,EAAA,IAAI;AACA,IAAA,MAAM,IAAA,GAAO,MAAM,SAAA,CAAU,KAAA,EAAO,GAAG,CAAC,CAAA;AACxC,IAAA,OAAO,KAAK,WAAA,IAAe,IAAA;AAAA,EAC/B,CAAA,SAAE;AAEE,IAAA,IAAI,WAAW,MAAA,EAAW;AACtB,MAAA,aAAA,CAAc,KAAK,CAAA;AAAA,IACvB;AAAA,EACJ;AACJ;;;;"}
@@ -0,0 +1,41 @@
1
+ const POS_X = 0;
2
+ const POS_Y = 1;
3
+ const SIZE_X = 2;
4
+ const SIZE_Y = 3;
5
+ const ROTATION = 8;
6
+ function pickSprite2D(layers, xPx, yPx) {
7
+ for (let li = layers.length - 1; li >= 0; li--) {
8
+ const layer = layers[li];
9
+ if (!layer.visible) {
10
+ continue;
11
+ }
12
+ const data = layer._instanceData;
13
+ const stride = layer._instanceFloatsPerSprite;
14
+ const pivotX = layer.pivot[0];
15
+ const pivotY = layer.pivot[1];
16
+ for (let i = layer.count - 1; i >= 0; i--) {
17
+ const base = i * stride;
18
+ const sizeX = data[base + SIZE_X];
19
+ const sizeY = data[base + SIZE_Y];
20
+ if (sizeX <= 0 || sizeY <= 0) {
21
+ continue;
22
+ }
23
+ const dx = xPx - data[base + POS_X];
24
+ const dy = yPx - data[base + POS_Y];
25
+ const rotation = data[base + ROTATION];
26
+ const cos = Math.cos(rotation);
27
+ const sin = Math.sin(rotation);
28
+ const localX = dx * cos + dy * sin;
29
+ const localY = -dx * sin + dy * cos;
30
+ const u = localX / sizeX + pivotX;
31
+ const v = localY / sizeY + pivotY;
32
+ if (u >= 0 && u <= 1 && v >= 0 && v <= 1) {
33
+ return { layer, spriteIndex: i, u, v };
34
+ }
35
+ }
36
+ }
37
+ return null;
38
+ }
39
+
40
+ export { pickSprite2D };
41
+ //# sourceMappingURL=pick-sprite-2d.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pick-sprite-2d.js","sources":["../../../../src/sprite/picking/pick-sprite-2d.ts"],"sourcesContent":["/**\n * `pickSprite2D` — CPU hit-test for `Sprite2DLayer` sprites.\n *\n * A 2D sprite is a screen-space rectangle, so picking is closed-form: no GPU pass, no readback.\n * This module is imported only when an app actually calls `pickSprite2D`, so HUD / pure-2D\n * scenes that never pick pay zero bytes for it (it sits in its own `sprite/picking/` folder for\n * exactly that reason). The hit test inverts the same per-sprite transform the vertex shader\n * applies (pivot + rotation), so it reports the sprite the GPU actually drew under the point.\n */\nimport type { Sprite2DLayer } from \"../sprite-2d.js\";\n\n/** Result of a successful {@link pickSprite2D} hit. */\nexport interface SpritePickInfo {\n /** The layer that owns the hit sprite. */\n layer: Sprite2DLayer;\n /** Index-API slot of the hit sprite within `layer` (the value returned by `addSprite2DIndex`). */\n spriteIndex: number;\n /**\n * Sprite-local hit coordinate in `[0, 1]`: `(0, 0)` is the quad's first corner and `(1, 1)`\n * the opposite one, corrected for the layer's pivot and the sprite's rotation. It equals the\n * within-quad UV fraction at the hit point, so a caller can map it to a frame-relative texel.\n */\n u: number;\n v: number;\n}\n\n// Per-instance float slots (mirror sprite-2d.ts / sprite-pipeline.ts):\n// [0..1] positionPx [2..3] sizePx [8] rotation\nconst POS_X = 0;\nconst POS_Y = 1;\nconst SIZE_X = 2;\nconst SIZE_Y = 3;\nconst ROTATION = 8;\n\n/**\n * Hit-test a point against the sprites of one or more `Sprite2DLayer`s and return the topmost\n * sprite under it, or `null` for a miss.\n *\n * `xPx` / `yPx` are in the layers' **local** coordinate space — the same space as each sprite's\n * `positionPx`. For a layer rendered 1:1 to the canvas (identity `view`) that is simply the\n * backing-store pixel under the pointer (`(clientX - rect.left) * devicePixelRatio`). For a\n * panned / zoomed / rotated layer, map the pointer into layer space first via the inverse of\n * `layer.view` (the caller owns that transform). All `layers` are assumed to share one space.\n *\n * Layers are tested in reverse array order (later layers are drawn on top, so they win), and\n * within a layer the most-recently-added sprite wins — matching GPU draw order, so the returned\n * sprite is the one visually on top at that point. Layers with `visible: false` and hidden\n * sprites (`visible: false`, stored as a zero-size quad) are skipped, exactly as the renderer\n * skips them.\n *\n * @param layers - Sprite layers to test, in draw order (e.g. `spriteRenderer.layers`).\n * @param xPx - Query X in layer-local pixels.\n * @param yPx - Query Y in layer-local pixels.\n * @returns The topmost hit, or `null` if the point is over no sprite.\n */\nexport function pickSprite2D(layers: ReadonlyArray<Sprite2DLayer>, xPx: number, yPx: number): SpritePickInfo | null {\n for (let li = layers.length - 1; li >= 0; li--) {\n const layer = layers[li]!;\n if (!layer.visible) {\n continue;\n }\n const data = layer._instanceData;\n const stride = layer._instanceFloatsPerSprite;\n const pivotX = layer.pivot[0];\n const pivotY = layer.pivot[1];\n for (let i = layer.count - 1; i >= 0; i--) {\n const base = i * stride;\n const sizeX = data[base + SIZE_X]!;\n const sizeY = data[base + SIZE_Y]!;\n // A hidden sprite is stored as a degenerate (zero-size) quad; the renderer culls it,\n // so the picker must too. This also guards the divides below against a zero size.\n if (sizeX <= 0 || sizeY <= 0) {\n continue;\n }\n const dx = xPx - data[base + POS_X]!;\n const dy = yPx - data[base + POS_Y]!;\n // Invert the vertex shader's `rotate(localOffset, rotation)` by rotating the delta by\n // -rotation, then undo `(corner - pivot) * size` to recover the [0, 1] corner fraction.\n const rotation = data[base + ROTATION]!;\n const cos = Math.cos(rotation);\n const sin = Math.sin(rotation);\n const localX = dx * cos + dy * sin;\n const localY = -dx * sin + dy * cos;\n const u = localX / sizeX + pivotX;\n const v = localY / sizeY + pivotY;\n if (u >= 0 && u <= 1 && v >= 0 && v <= 1) {\n return { layer, spriteIndex: i, u, v };\n }\n }\n }\n return null;\n}\n"],"names":[],"mappings":"AA4BA,MAAM,KAAA,GAAQ,CAAA;AACd,MAAM,KAAA,GAAQ,CAAA;AACd,MAAM,MAAA,GAAS,CAAA;AACf,MAAM,MAAA,GAAS,CAAA;AACf,MAAM,QAAA,GAAW,CAAA;AAuBV,SAAS,YAAA,CAAa,MAAA,EAAsC,GAAA,EAAa,GAAA,EAAoC;AAChH,EAAA,KAAA,IAAS,KAAK,MAAA,CAAO,MAAA,GAAS,CAAA,EAAG,EAAA,IAAM,GAAG,EAAA,EAAA,EAAM;AAC5C,IAAA,MAAM,KAAA,GAAQ,OAAO,EAAE,CAAA;AACvB,IAAA,IAAI,CAAC,MAAM,OAAA,EAAS;AAChB,MAAA;AAAA,IACJ;AACA,IAAA,MAAM,OAAO,KAAA,CAAM,aAAA;AACnB,IAAA,MAAM,SAAS,KAAA,CAAM,wBAAA;AACrB,IAAA,MAAM,MAAA,GAAS,KAAA,CAAM,KAAA,CAAM,CAAC,CAAA;AAC5B,IAAA,MAAM,MAAA,GAAS,KAAA,CAAM,KAAA,CAAM,CAAC,CAAA;AAC5B,IAAA,KAAA,IAAS,IAAI,KAAA,CAAM,KAAA,GAAQ,CAAA,EAAG,CAAA,IAAK,GAAG,CAAA,EAAA,EAAK;AACvC,MAAA,MAAM,OAAO,CAAA,GAAI,MAAA;AACjB,MAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,IAAA,GAAO,MAAM,CAAA;AAChC,MAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,IAAA,GAAO,MAAM,CAAA;AAGhC,MAAA,IAAI,KAAA,IAAS,CAAA,IAAK,KAAA,IAAS,CAAA,EAAG;AAC1B,QAAA;AAAA,MACJ;AACA,MAAA,MAAM,EAAA,GAAK,GAAA,GAAM,IAAA,CAAK,IAAA,GAAO,KAAK,CAAA;AAClC,MAAA,MAAM,EAAA,GAAK,GAAA,GAAM,IAAA,CAAK,IAAA,GAAO,KAAK,CAAA;AAGlC,MAAA,MAAM,QAAA,GAAW,IAAA,CAAK,IAAA,GAAO,QAAQ,CAAA;AACrC,MAAA,MAAM,GAAA,GAAM,IAAA,CAAK,GAAA,CAAI,QAAQ,CAAA;AAC7B,MAAA,MAAM,GAAA,GAAM,IAAA,CAAK,GAAA,CAAI,QAAQ,CAAA;AAC7B,MAAA,MAAM,MAAA,GAAS,EAAA,GAAK,GAAA,GAAM,EAAA,GAAK,GAAA;AAC/B,MAAA,MAAM,MAAA,GAAS,CAAC,EAAA,GAAK,GAAA,GAAM,EAAA,GAAK,GAAA;AAChC,MAAA,MAAM,CAAA,GAAI,SAAS,KAAA,GAAQ,MAAA;AAC3B,MAAA,MAAM,CAAA,GAAI,SAAS,KAAA,GAAQ,MAAA;AAC3B,MAAA,IAAI,KAAK,CAAA,IAAK,CAAA,IAAK,KAAK,CAAA,IAAK,CAAA,IAAK,KAAK,CAAA,EAAG;AACtC,QAAA,OAAO,EAAE,KAAA,EAAO,WAAA,EAAa,CAAA,EAAG,GAAG,CAAA,EAAE;AAAA,MACzC;AAAA,IACJ;AAAA,EACJ;AACA,EAAA,OAAO,IAAA;AACX;;;;"}
@@ -68,6 +68,9 @@ function resolveSpriteFrame(atlas, frame) {
68
68
  }
69
69
  return frame;
70
70
  }
71
+ function disposeSpriteAtlas(atlas) {
72
+ atlas.texture.texture.destroy();
73
+ }
71
74
 
72
- export { createGridSpriteAtlas, loadSpriteAtlas, resolveSpriteFrame };
75
+ export { createGridSpriteAtlas, disposeSpriteAtlas, loadSpriteAtlas, resolveSpriteFrame };
73
76
  //# sourceMappingURL=sprite-atlas.js.map
@@ -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 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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"}
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\n/**\n * Release the GPU texture backing `atlas`.\n *\n * A `SpriteAtlas` is a pure data record whose lifetime is governed by its\n * `Texture2D`; it is deliberately NOT owned by any `Sprite2DLayer` or\n * `SpriteRenderer` (those hold it `readonly` and never free it), so a single\n * atlas can back many layers/renderers across many surfaces. That makes the\n * atlas the caller's to dispose: call this once, after every renderer/layer\n * referencing it has been disposed. Idempotence is NOT guaranteed — the\n * underlying `GPUTexture.destroy()` must be called exactly once.\n *\n * Note: atlases created via `createGridSpriteAtlas` from an externally-owned\n * `Texture2D` should be freed by whoever owns that texture, not via this call.\n */\nexport function disposeSpriteAtlas(atlas: SpriteAtlas): void {\n atlas.texture.texture.destroy();\n}\n"],"names":[],"mappings":";;AAyGO,SAAS,qBAAA,CAAsB,SAAoB,OAAA,EAAwC;AAC9F,EAAA,MAAM,QAAQ,OAAA,CAAQ,WAAA;AACtB,EAAA,MAAM,QAAQ,OAAA,CAAQ,YAAA;AACtB,EAAA,MAAM,MAAA,GAAS,QAAQ,QAAA,IAAY,CAAA;AACnC,EAAA,MAAM,OAAA,GAAU,QAAQ,SAAA,IAAa,CAAA;AACrC,EAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,OAAA,IAAW,IAAA,CAAK,IAAI,CAAA,EAAG,IAAA,CAAK,KAAA,CAAA,CAAO,OAAA,CAAQ,QAAQ,MAAA,GAAS,CAAA,GAAI,OAAA,KAAY,KAAA,GAAQ,QAAQ,CAAC,CAAA;AAClH,EAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,IAAA,IAAQ,IAAA,CAAK,IAAI,CAAA,EAAG,IAAA,CAAK,KAAA,CAAA,CAAO,OAAA,CAAQ,SAAS,MAAA,GAAS,CAAA,GAAI,OAAA,KAAY,KAAA,GAAQ,QAAQ,CAAC,CAAA;AAChH,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,KAAA,IAAS,CAAC,KAAK,GAAG,CAAA;AAExC,EAAA,MAAM,KAAK,OAAA,CAAQ,KAAA;AACnB,EAAA,MAAM,KAAK,OAAA,CAAQ,MAAA;AACnB,EAAA,MAAM,SAAwB,EAAC;AAC/B,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,IAAA,EAAM,CAAA,EAAA,EAAK;AAC3B,IAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,IAAA,EAAM,CAAA,EAAA,EAAK;AAC3B,MAAA,MAAM,CAAA,GAAI,MAAA,GAAS,CAAA,IAAK,KAAA,GAAQ,OAAA,CAAA;AAChC,MAAA,MAAM,CAAA,GAAI,MAAA,GAAS,CAAA,IAAK,KAAA,GAAQ,OAAA,CAAA;AAChC,MAAA,MAAA,CAAO,IAAA,CAAK;AAAA,QACR,KAAA,EAAO,CAAC,CAAA,GAAI,EAAA,EAAI,IAAI,EAAE,CAAA;AAAA,QACtB,OAAO,CAAA,CAAE,CAAA,GAAI,SAAS,EAAA,EAAA,CAAK,CAAA,GAAI,SAAS,EAAE,CAAA;AAAA,QAC1C,YAAA,EAAc,CAAC,KAAA,EAAO,KAAK,CAAA;AAAA,QAC3B,OAAO,CAAC,KAAA,CAAM,CAAC,CAAA,EAAG,KAAA,CAAM,CAAC,CAAC;AAAA,OAC7B,CAAA;AAAA,IACL;AAAA,EACJ;AAEA,EAAA,OAAO;AAAA,IACH,OAAA;AAAA,IACA,aAAA,EAAe,CAAC,EAAA,EAAI,EAAE,CAAA;AAAA,IACtB,MAAA;AAAA,IACA,kBAAA,EAAoB,QAAQ,kBAAA,IAAsB;AAAA,GACtD;AACJ;AAQA,eAAsB,eAAA,CAAgB,MAAA,EAAuB,UAAA,EAAoB,OAAA,GAA4B,EAAC,EAAyB;AACnI,EAAA,IAAI,OAAA,CAAQ,gBAAgB,MAAA,EAAW;AACnC,IAAA,MAAM,IAAI,MAAM,2CAA2C,CAAA;AAAA,EAC/D;AACA,EAAA,IAAI,CAAC,QAAQ,QAAA,EAAU;AACnB,IAAA,MAAM,IAAI,MAAM,qCAAqC,CAAA;AAAA,EACzD;AAEA,EAAA,MAAM,OAAA,GAA4B;AAAA;AAAA,IAE9B,OAAA,EAAS,KAAA;AAAA;AAAA,IAET,YAAA,EAAc,eAAA;AAAA,IACd,YAAA,EAAc,eAAA;AAAA;AAAA,IAEd,OAAA,EAAS,KAAA;AAAA,IACT,SAAA,EAAW,OAAA,CAAQ,QAAA,KAAa,SAAA,GAAY,SAAA,GAAY,QAAA;AAAA,IACxD,SAAA,EAAW,OAAA,CAAQ,QAAA,KAAa,SAAA,GAAY,SAAA,GAAY,QAAA;AAAA;AAAA;AAAA,IAGxD,gBAAA,EAAkB,QAAQ,iBAAA,IAAqB,KAAA;AAAA,IAC/C,GAAG,OAAA,CAAQ;AAAA,GACf;AAEA,EAAA,MAAM,OAAA,GAAU,MAAM,aAAA,CAAc,MAAA,EAAQ,YAAY,OAAO,CAAA;AAC/D,EAAA,OAAO,sBAAsB,OAAA,EAAS;AAAA,IAClC,WAAA,EAAa,OAAA,CAAQ,QAAA,CAAS,CAAC,CAAA;AAAA,IAC/B,YAAA,EAAc,OAAA,CAAQ,QAAA,CAAS,CAAC,CAAA;AAAA;AAAA;AAAA;AAAA,IAIhC,kBAAA,EAAoB,QAAQ,kBAAA,IAAsB;AAAA,GACrD,CAAA;AACL;AAGO,SAAS,kBAAA,CAAmB,OAAoB,KAAA,EAAuB;AAC1E,EAAA,IAAI,KAAA,GAAQ,CAAA,IAAK,KAAA,IAAS,KAAA,CAAM,OAAO,MAAA,EAAQ;AAC3C,IAAA,MAAM,IAAI,MAAM,CAAA,0BAAA,EAA6B,KAAK,qBAAqB,KAAA,CAAM,MAAA,CAAO,MAAM,CAAA,CAAA,CAAG,CAAA;AAAA,EACjG;AACA,EAAA,OAAO,KAAA;AACX;AAgBO,SAAS,mBAAmB,KAAA,EAA0B;AACzD,EAAA,KAAA,CAAM,OAAA,CAAQ,QAAQ,OAAA,EAAQ;AAClC;;;;"}