@forgeax/engine-render 0.1.24 → 0.1.26

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 (167) hide show
  1. package/README.md +118 -1
  2. package/dist/assembly/factory.d.ts +1 -1
  3. package/dist/assembly/factory.d.ts.map +1 -1
  4. package/dist/assembly/material/assembly.d.ts +4 -17
  5. package/dist/assembly/material/assembly.d.ts.map +1 -1
  6. package/dist/assembly/material/standard-layer-projection.d.ts.map +1 -1
  7. package/dist/assembly/material/surface-projection.d.ts +24 -0
  8. package/dist/assembly/material/surface-projection.d.ts.map +1 -0
  9. package/dist/assembly/material-shader-policy.d.ts +20 -0
  10. package/dist/assembly/material-shader-policy.d.ts.map +1 -1
  11. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  12. package/dist/assembly/webgpu-renderer.d.ts +1 -2
  13. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  14. package/dist/assembly/webgpu-vertex-layouts.d.ts +89 -0
  15. package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -0
  16. package/dist/assets/asset-decoders.d.ts.map +1 -1
  17. package/dist/authoring.mjs +1 -1
  18. package/dist/{chunk-Q752SHI5.mjs → chunk-ADHHVYLW.mjs} +3 -3
  19. package/dist/chunk-ADHHVYLW.mjs.map +1 -0
  20. package/dist/{chunk-TFH5I7JJ.mjs → chunk-E34VL5VI.mjs} +3 -3
  21. package/dist/{chunk-TFH5I7JJ.mjs.map → chunk-E34VL5VI.mjs.map} +1 -1
  22. package/dist/{chunk-YKB5DQV7.mjs → chunk-GF523LHF.mjs} +89 -21
  23. package/dist/chunk-GF523LHF.mjs.map +1 -0
  24. package/dist/{chunk-KZFTK6DO.mjs → chunk-HKXTW355.mjs} +7 -4
  25. package/dist/chunk-HKXTW355.mjs.map +1 -0
  26. package/dist/{chunk-UQYS7FRM.mjs → chunk-KM2NOX2I.mjs} +4 -4
  27. package/dist/{chunk-UQYS7FRM.mjs.map → chunk-KM2NOX2I.mjs.map} +1 -1
  28. package/dist/{chunk-YRUYGA4A.mjs → chunk-TMIASV2N.mjs} +944 -577
  29. package/dist/chunk-TMIASV2N.mjs.map +1 -0
  30. package/dist/{chunk-Z3YYH2MB.mjs → chunk-YUQXSEHG.mjs} +160 -73
  31. package/dist/chunk-YUQXSEHG.mjs.map +1 -0
  32. package/dist/{chunk-RR5L4SRS.mjs → chunk-ZZ4YQ474.mjs} +2 -2
  33. package/dist/{chunk-RR5L4SRS.mjs.map → chunk-ZZ4YQ474.mjs.map} +1 -1
  34. package/dist/components/instances.d.ts +13 -31
  35. package/dist/components/instances.d.ts.map +1 -1
  36. package/dist/construct-renderer.mjs +1012 -149
  37. package/dist/construct-renderer.mjs.map +1 -1
  38. package/dist/extract/contracts.d.ts +6 -6
  39. package/dist/extract/contracts.d.ts.map +1 -1
  40. package/dist/gpu-driven/production-raster.d.ts +6 -1
  41. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  42. package/dist/gpu-scene.d.ts +4 -0
  43. package/dist/gpu-scene.d.ts.map +1 -1
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.mjs +27 -9
  47. package/dist/index.mjs.map +1 -1
  48. package/dist/instance-buffer-cache.d.ts +7 -1
  49. package/dist/instance-buffer-cache.d.ts.map +1 -1
  50. package/dist/instances.d.ts +101 -0
  51. package/dist/instances.d.ts.map +1 -0
  52. package/dist/internal.mjs +5 -5
  53. package/dist/materials.d.ts +63 -2
  54. package/dist/materials.d.ts.map +1 -1
  55. package/dist/pipeline-builder.d.ts.map +1 -1
  56. package/dist/pipeline-spec-types.d.ts +2 -0
  57. package/dist/pipeline-spec-types.d.ts.map +1 -1
  58. package/dist/pipeline-spec.d.ts.map +1 -1
  59. package/dist/record/frame-snapshot.d.ts +13 -2
  60. package/dist/record/frame-snapshot.d.ts.map +1 -1
  61. package/dist/record/frame.d.ts.map +1 -1
  62. package/dist/record/main-pass-geometry.d.ts +23 -4
  63. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  64. package/dist/record/main-pass-material.d.ts +20 -2
  65. package/dist/record/main-pass-material.d.ts.map +1 -1
  66. package/dist/record/main-pass-sprite-draws.d.ts +3 -3
  67. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  68. package/dist/record/main-pass.d.ts.map +1 -1
  69. package/dist/record/material-uniforms.d.ts +4 -0
  70. package/dist/record/material-uniforms.d.ts.map +1 -0
  71. package/dist/record/render-context.d.ts +4 -3
  72. package/dist/record/render-context.d.ts.map +1 -1
  73. package/dist/record/shadow-pass.d.ts +10 -1
  74. package/dist/record/shadow-pass.d.ts.map +1 -1
  75. package/dist/recovery/render-system-candidate.d.ts +1 -1
  76. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  77. package/dist/render-contract.d.ts +4 -0
  78. package/dist/render-contract.d.ts.map +1 -1
  79. package/dist/render-system-extract-tail.d.ts +6 -0
  80. package/dist/render-system-extract-tail.d.ts.map +1 -1
  81. package/dist/render-system-extract.d.ts +44 -9
  82. package/dist/render-system-extract.d.ts.map +1 -1
  83. package/dist/render-system.d.ts +4 -1
  84. package/dist/render-system.d.ts.map +1 -1
  85. package/dist/scene/render-scene-types.d.ts +2 -0
  86. package/dist/scene/render-scene-types.d.ts.map +1 -1
  87. package/dist/scene/render-scene.d.ts +12 -1
  88. package/dist/scene/render-scene.d.ts.map +1 -1
  89. package/dist/systems/skin-palette-allocator.d.ts +6 -0
  90. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  91. package/dist/temporal/index.mjs +5 -5
  92. package/package.json +20 -20
  93. package/src/__tests__/fixtures/ai-weapon-spirit/README.md +20 -0
  94. package/src/__tests__/fixtures/ai-weapon-spirit/low-poly-toon.wgsl +178 -0
  95. package/src/__tests__/fixtures/ai-weapon-spirit/material.json +97 -0
  96. package/src/__tests__/fixtures/ai-weapon-spirit/painterly-surface.wgsl +55 -0
  97. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +3 -3
  98. package/src/__tests__/gpu-driven-view.browser.test.ts +11 -3
  99. package/src/__tests__/gpu-scene.unit.test.ts +38 -2
  100. package/src/__tests__/instances-store.unit.test.ts +46 -0
  101. package/src/__tests__/instances-world-ownership.integration.test.ts +62 -0
  102. package/src/__tests__/material-contract-inventory.unit.test.ts +48 -13
  103. package/src/__tests__/{material-static-dynamic-route.unit.test.ts → material-layer-projection.unit.test.ts} +30 -47
  104. package/src/__tests__/material-pass-policy.unit.test.ts +69 -0
  105. package/src/__tests__/material-program-dispatch.unit.test.ts +48 -0
  106. package/src/__tests__/material-snapshot-mutation.integration.test.ts +69 -0
  107. package/src/__tests__/material-surface-authoring.test-d.ts +62 -0
  108. package/src/__tests__/materials-transmission-validation.unit.test.ts +24 -0
  109. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +0 -4
  110. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +44 -2
  111. package/src/__tests__/skin-motion-regression.unit.test.ts +33 -0
  112. package/src/__tests__/skinned-shadow-caster.test.ts +30 -2
  113. package/src/__tests__/surface-adjacent-regressions.integration.test.ts +60 -0
  114. package/src/__tests__/surface-standard-pipeline.browser.test.ts +20 -0
  115. package/src/__tests__/surface-standard-pipeline.dawn.test.ts +20 -0
  116. package/src/__tests__/surface-standard-pipeline.fixture.ts +132 -0
  117. package/src/__tests__/surface-variant-provenance.integration.test.ts +33 -0
  118. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +1 -1
  119. package/src/assembly/factory.ts +2 -3
  120. package/src/assembly/material/__tests__/standard-layer-plan-projection.integration.test.ts +18 -0
  121. package/src/assembly/material/__tests__/surface-pass-projection.integration.test.ts +62 -0
  122. package/src/assembly/material/assembly.ts +8 -40
  123. package/src/assembly/material/standard-layer-projection.ts +3 -1
  124. package/src/assembly/material/surface-projection.ts +91 -0
  125. package/src/assembly/material-shader-policy.ts +88 -10
  126. package/src/assembly/recovery/renderer-recover.ts +1 -1
  127. package/src/assembly/webgpu-ready.ts +3 -16
  128. package/src/assembly/webgpu-renderer.ts +106 -166
  129. package/src/assembly/webgpu-vertex-layouts.ts +132 -0
  130. package/src/assets/asset-decoders.ts +27 -4
  131. package/src/components/instances.ts +14 -197
  132. package/src/extract/contracts.ts +6 -6
  133. package/src/gpu-driven/production-raster.ts +34 -1
  134. package/src/gpu-scene.ts +84 -29
  135. package/src/index.ts +1 -0
  136. package/src/instance-buffer-cache.ts +14 -1
  137. package/src/instances.ts +288 -0
  138. package/src/materials.ts +109 -14
  139. package/src/pipeline-builder.ts +14 -10
  140. package/src/pipeline-spec-types.ts +2 -0
  141. package/src/pipeline-spec.ts +13 -17
  142. package/src/record/frame-snapshot.ts +19 -2
  143. package/src/record/frame.ts +38 -3
  144. package/src/record/main-pass-geometry.ts +837 -337
  145. package/src/record/main-pass-material.ts +87 -2
  146. package/src/record/main-pass-sprite-draws.ts +66 -169
  147. package/src/record/main-pass.ts +24 -246
  148. package/src/record/material-uniforms.ts +186 -0
  149. package/src/record/render-context.ts +8 -0
  150. package/src/record/shadow-pass.ts +120 -115
  151. package/src/recovery/render-system-candidate.ts +46 -5
  152. package/src/render-contract.ts +4 -0
  153. package/src/render-system-extract-tail.ts +153 -57
  154. package/src/render-system-extract.ts +110 -73
  155. package/src/render-system.ts +59 -5
  156. package/src/scene/render-scene-types.ts +2 -0
  157. package/src/scene/render-scene.ts +579 -36
  158. package/src/systems/skin-palette-allocator.ts +129 -7
  159. package/src/transmission/__tests__/surface-compatibility.integration.test.ts +32 -0
  160. package/dist/assembly/material/pipeline-projection.d.ts +0 -30
  161. package/dist/assembly/material/pipeline-projection.d.ts.map +0 -1
  162. package/dist/chunk-KZFTK6DO.mjs.map +0 -1
  163. package/dist/chunk-Q752SHI5.mjs.map +0 -1
  164. package/dist/chunk-YKB5DQV7.mjs.map +0 -1
  165. package/dist/chunk-YRUYGA4A.mjs.map +0 -1
  166. package/dist/chunk-Z3YYH2MB.mjs.map +0 -1
  167. package/src/assembly/material/pipeline-projection.ts +0 -63
@@ -1,3 +1,5 @@
1
+ import { getOrCreateIblCache } from '../ibl/IblPipelineCache';
2
+ import type { _InternalRenderPipelineContext } from './render-context';
1
3
  // @forgeax/engine-runtime - RenderSystem record stage: main-pass-material.
2
4
  // Extracted from render-system-record.ts (feat-20260704 M3/w17, pure move).
3
5
 
@@ -976,6 +978,7 @@ export function buildPerSubmeshMaterialBg(
976
978
  submeshMaterial: MaterialSnapshot,
977
979
  entityKey: number,
978
980
  materialWorld: World = deps.world,
981
+ materialShaderId: string | undefined = submeshMaterial.materialShaderId,
979
982
  ): BindGroup {
980
983
  const {
981
984
  runtime,
@@ -996,9 +999,11 @@ export function buildPerSubmeshMaterialBg(
996
999
  resolveReflectionProbeResources?.(materialWorld, entityKey) ?? skylightResources;
997
1000
  const smMaterialHandle = submeshMaterial.materialHandle;
998
1001
  const materialCacheKey =
999
- smMaterialHandle === undefined ? undefined : `${materialWorld.identity}:${smMaterialHandle}`;
1002
+ smMaterialHandle === undefined
1003
+ ? undefined
1004
+ : `${materialWorld.identity}:${smMaterialHandle}:${materialShaderId ?? ''}`;
1000
1005
  const smHasVideoFields = (submeshMaterial.videoTextureFields?.size ?? 0) > 0;
1001
- const smShaderId = submeshMaterial.materialShaderId;
1006
+ const smShaderId = materialShaderId;
1002
1007
  // The cooked shader schema owns the resource ABI. A material snapshot may
1003
1008
  // carry only the authored values (for example a clearcoat-only root omits
1004
1009
  // the standard emissive/occlusion maps), while the Standard template still
@@ -1293,3 +1298,83 @@ export function buildPerSubmeshMaterialBg(
1293
1298
  }
1294
1299
  return smBindGroup;
1295
1300
  }
1301
+
1302
+ const ZERO_SKYLIGHT_PAYLOAD = new Float32Array([0, 0, 0, 0, 0, 0, 0, 1]);
1303
+
1304
+ /** The same scene resources back material bindings in depth and color passes. */
1305
+ export function prepareMaterialSkylight(c: _InternalRenderPipelineContext) {
1306
+ const { runtime, pipelineState, skylight, skylightCount } = c;
1307
+ const skylightFallback = pipelineState.skylightFallback;
1308
+ if (skylightFallback === null) {
1309
+ throw new RhiError({
1310
+ code: 'webgpu-runtime-error',
1311
+ expected: 'pipelineState.skylightFallback != null when PBR pipeline is active',
1312
+ hint: 'createRenderer must allocate skylightFallback alongside the PBR pipeline (D-5 round-4)',
1313
+ });
1314
+ }
1315
+ // feat-20260520-skylight-ibl-cubemap M4 round-4 / t60 (D-5 round-4):
1316
+ // select active vs fallback Skylight resources by `skylightCount` from
1317
+ // the extract stage. Active path reaches into the per-device
1318
+ // `IblPipelineCache` slots (irradianceView / prefilterView / brdfLutView)
1319
+ // populated by the internal equirect-to-cubemap projection; fallback uses the
1320
+ // 1x1-zero identity bundle that converges ambient to 0 (D-4 physical
1321
+ // convergence -- no `if (hasSkylight)` shader branch).
1322
+ // The samplers are reused from `skylightFallback.sampler` for both
1323
+ // paths (linear / clamp-to-edge is correct for IBL cube + 2D LUT
1324
+ // sampling either way). The intensity uniform is rewritten per-frame
1325
+ // when active so `sampleIblSpecular * intensity` carries the user's
1326
+ // Skylight.intensity value; fallback keeps intensity=0 (createSkylightFallback
1327
+ // seed) so ambient = 0 even when the same buffer is shared.
1328
+ let activeViews: { irr: TextureView; pref: TextureView; brdf: TextureView } | undefined;
1329
+ // Per-frame Skylight uniform: std140 32 B = [intensity, colorR, colorG,
1330
+ // colorB, rotation quaternion]. Default to all-zero so a transition from "has Skylight" ->
1331
+ // "no Skylight" does not leak the prior frame's ambient (intensity 0
1332
+ // muzzles everything, including the white fallback irradiance cube).
1333
+ runtime.device.queue.writeBuffer(skylightFallback.intensityBuffer, 0, ZERO_SKYLIGHT_PAYLOAD);
1334
+ if (skylight !== undefined && skylightCount >= 1) {
1335
+ // A Skylight exists. Write its intensity + color regardless of whether
1336
+ // a cubemap is bound: with a cubemap the IBL views below light the
1337
+ // ambient; WITHOUT one, the white fallback irradiance cube + this color
1338
+ // give an instant solid-color ambient (downstream integration #4) with
1339
+ // no async precompute. The white fallback only contributes when a
1340
+ // Skylight is present because the zero-payload above sets intensity 0
1341
+ // when no Skylight exists.
1342
+ const [cr, cg, cb] = skylight.color;
1343
+ const [qx, qy, qz, qw] = skylight.rotation;
1344
+ const uniformPayload = new Float32Array([skylight.intensity, cr, cg, cb, qx, qy, qz, qw]);
1345
+ runtime.device.queue.writeBuffer(skylightFallback.intensityBuffer, 0, uniformPayload);
1346
+ const cache = getOrCreateIblCache(runtime.deviceScope);
1347
+ if (
1348
+ cache.irradianceView !== undefined &&
1349
+ cache.prefilterView !== undefined &&
1350
+ cache.brdfLutView !== undefined
1351
+ ) {
1352
+ activeViews = {
1353
+ irr: cache.irradianceView,
1354
+ pref: cache.prefilterView,
1355
+ brdf: cache.brdfLutView,
1356
+ };
1357
+ }
1358
+ }
1359
+ const skylightResources =
1360
+ activeViews !== undefined
1361
+ ? {
1362
+ irradianceView: activeViews.irr,
1363
+ irradianceSampler: skylightFallback.sampler,
1364
+ prefilterView: activeViews.pref,
1365
+ prefilterSampler: skylightFallback.sampler,
1366
+ brdfLutView: activeViews.brdf,
1367
+ brdfLutSampler: skylightFallback.sampler,
1368
+ intensityBuffer: skylightFallback.intensityBuffer,
1369
+ }
1370
+ : {
1371
+ irradianceView: skylightFallback.irradianceView,
1372
+ irradianceSampler: skylightFallback.sampler,
1373
+ prefilterView: skylightFallback.prefilterView,
1374
+ prefilterSampler: skylightFallback.sampler,
1375
+ brdfLutView: skylightFallback.brdfLutView,
1376
+ brdfLutSampler: skylightFallback.sampler,
1377
+ intensityBuffer: skylightFallback.intensityBuffer,
1378
+ };
1379
+ return { skylightResources, activeViews };
1380
+ }
@@ -25,24 +25,19 @@ import {
25
25
  } from '../ibl/skylight-bind-group';
26
26
  import type { InstanceBufferCacheEntry } from '../instance-buffer-cache';
27
27
  import { SPRITE_PREMULTIPLIED_ALPHA_BLEND } from '../materials';
28
- import {
29
- isStandardPbrMaterialShader,
30
- SPRITE_PASS_PER_INSTANCE_REGION_VARIANT_SET,
31
- } from '../pbr-pipeline';
32
- import {
33
- buildBeginRenderPassDescriptor,
34
- standardTopologyVariantSet,
35
- variantSetFromVertexLayoutProjection,
36
- } from '../pipeline-spec';
37
- import type { MaterialSnapshot, SpriteInstancesSnapshot } from '../render-system-extract';
28
+ import { SPRITE_PASS_PER_INSTANCE_REGION_VARIANT_SET } from '../pbr-pipeline';
29
+ import { buildBeginRenderPassDescriptor, standardTopologyVariantSet } from '../pipeline-spec';
30
+ import type {
31
+ DispatchEntry,
32
+ MaterialSnapshot,
33
+ SpriteInstancesSnapshot,
34
+ } from '../render-system-extract';
38
35
  import { worldEntityKey } from './frame-snapshot';
39
- import { resolveGeometryInstancesBindGroup, resolveProbeBlendBuffer } from './main-pass-geometry';
36
+ import { recordGeometryDraws, resolveGeometryInstancesBindGroup } from './main-pass-geometry';
40
37
  import {
41
38
  BUILTIN_USER_REGION_TEXTURE_FIELDS,
42
39
  defaultViewForUserRegionField,
43
- entityHasTransparentSubmesh,
44
40
  residentTextureView,
45
- selectMaterialGroup2,
46
41
  } from './main-pass-material';
47
42
  import {
48
43
  extractEntryResourceHandle,
@@ -164,8 +159,14 @@ export function computeSplitLdrSprite(
164
159
  | undefined
165
160
  )[],
166
161
  tonemapActive: boolean,
162
+ dispatch?: readonly DispatchEntry[],
167
163
  ): boolean {
168
164
  if (tonemapActive) return false;
165
+ if (dispatch !== undefined && dispatch.length > 0) {
166
+ return dispatch.some(
167
+ (pass) => pass.tags.LightMode === 'Forward' && pass.renderState?.blend !== undefined,
168
+ );
169
+ }
169
170
  for (const entry of validatedOrdered) {
170
171
  if (entry === undefined) continue;
171
172
  const materials = entry.source.materials;
@@ -201,9 +202,11 @@ export function recordSpritePass(
201
202
  submeshMaterial: MaterialSnapshot,
202
203
  entityKey: number,
203
204
  materialWorld: World,
205
+ materialShaderId?: string,
204
206
  ) => BindGroup,
205
207
  skylightResources: SkylightBindGroupResources,
206
208
  graphPass?: RhiRenderPassEncoder,
209
+ selectedDispatch?: readonly DispatchEntry[],
207
210
  ): boolean {
208
211
  const {
209
212
  runtime,
@@ -409,22 +412,7 @@ export function recordSpritePass(
409
412
  reportSpriteUnavailable,
410
413
  );
411
414
 
412
- // feat-city-glb Bug 5 (per-submesh transparency): generic per-submesh PBR
413
- // draws in the LDR blend sub-pass. The sprite loop above handles only true
414
- // sprite / sprite-lit entities (whole-mesh sprite PSO). EVERY non-sprite
415
- // transparent material — built-in PBR, single- or multi-submesh, incl.
416
- // glTF alphaMode=BLEND (the crosswalk decal submesh AND the 4.3-blending
417
- // window quad) — is drawn here with its real shader + per-submesh
418
- // geometry, reusing the geometry pass's PBR machinery but rendering into
419
- // the resolved transparent attachment (colorFormatOverride =
420
- // transparentPassColorFormat(c)).
421
- // Opaque submeshes of a mixed mesh were already drawn in the geometry pass
422
- // (per-submesh skip there).
423
- //
424
- // Non-skinned, non-instanced (glTF static meshes): group 2 = the shared
425
- // frame meshBindGroup with the per-entity dynamic offset; group 3 = the
426
- // identity instance BG (drawIndexed instanceCount=1). Skinned / instanced
427
- // transparent submeshes are OOS here (no such content) and fall through.
415
+ // Keep program, geometry, bindings and dynamic offsets on the common draw path.
428
416
  recordSpriteTransparentPbrDraws(
429
417
  c,
430
418
  spritePass,
@@ -433,6 +421,7 @@ export function recordSpritePass(
433
421
  sampleCount,
434
422
  resolveMaterialBindGroup,
435
423
  meshGroup2,
424
+ selectedDispatch,
436
425
  );
437
426
 
438
427
  if (graphPass === undefined) spritePass.end();
@@ -440,159 +429,67 @@ export function recordSpritePass(
440
429
  return geometryPassEnded;
441
430
  }
442
431
 
443
- /**
444
- * feat-20260704 M3/w19: generic per-submesh transparent PBR draws in the LDR
445
- * blend sub-pass, extracted verbatim from the sprite split pass. Every
446
- * non-sprite transparent material (built-in PBR, single- or multi-submesh, incl.
447
- * glTF alphaMode=BLEND) draws here with its real shader + per-submesh geometry,
448
- * reusing the geometry pass's PBR machinery but rendering into the resolved
449
- * transparent attachment (colorFormatOverride = transparentPassColorFormat(c)). Opaque submeshes of a
450
- * mixed mesh were already drawn in the geometry pass (per-submesh skip there).
451
- * Sprites are handled by the sprite loop; skinned / instanced transparent
452
- * submeshes are OOS here and fall through.
453
- *
454
- * @internal
455
- */
432
+ /** Non-sprite transparent Passes use the same geometry and binding owner as opaque draws. */
456
433
  function recordSpriteTransparentPbrDraws(
457
434
  c: _InternalRenderPipelineContext,
458
- spritePass: RhiRenderPassEncoder,
435
+ pass: RhiRenderPassEncoder,
459
436
  matchedIndices: Set<number> | null,
460
437
  materialSlotIndices: readonly (readonly number[])[],
461
438
  sampleCount: number,
462
439
  resolveMaterialBindGroup: (
463
- materialSlot: number,
464
- submeshMaterial: MaterialSnapshot,
440
+ slot: number,
441
+ material: MaterialSnapshot,
465
442
  entityKey: number,
466
- materialWorld: World,
443
+ world: World,
444
+ shader?: string,
467
445
  ) => BindGroup,
468
446
  meshGroup2: BindGroup | null,
447
+ selectedDispatch?: readonly DispatchEntry[],
469
448
  ): void {
470
- const { runtime, pipelineState, validatedOrdered } = c;
471
- const isHdrTarget =
472
- c.tonemapActive || transparentPassColorFormat(c, pipelineState) === 'rgba16float';
473
- let lastPbrSubPipelineHandle: typeof pipelineState.unlitPipeline = null;
474
- let lastPbrSubVertexBuffer: GpuBuffer | null = null;
475
- let lastPbrSubIndexBuffer: GpuBuffer | null = null;
476
- for (let i = 0; i < validatedOrdered.length; i++) {
477
- const entry = validatedOrdered[i];
478
- if (entry === undefined) continue;
479
- if (matchedIndices !== null && !matchedIndices.has(entry.renderableIndex)) continue;
480
- // Sprites are handled by the sprite loop above; skip them here.
481
- const entShaderId = entry.source.material.materialShaderId;
482
- const entIsSprite = entShaderId === 'forgeax::sprite' || entShaderId === 'forgeax::sprite-lit';
483
- if (entIsSprite) continue;
484
- // Only entities that carry at least one transparent submesh reach a draw
485
- // (single-submesh transparent PBR and mixed opaque+transparent meshes).
486
- if (!entityHasTransparentSubmesh(entry.source)) continue;
487
- // Skinned / instanced transparent submeshes are not supported in this
488
- // sub-pass path (no such content today); skip to avoid mis-binding.
489
- if (entry.source.skin !== undefined || entry.source.instances !== undefined) continue;
490
-
491
- const matsForRebind = entry.source.materials;
492
- if (entry.mesh.vertexBuffer !== lastPbrSubVertexBuffer) {
493
- spritePass.setVertexBuffer(0, entry.mesh.vertexBuffer.handle);
494
- lastPbrSubVertexBuffer = entry.mesh.vertexBuffer;
495
- }
496
- if (
497
- entry.mesh.indexed &&
498
- entry.mesh.indexBuffer !== null &&
499
- entry.mesh.indexBuffer !== lastPbrSubIndexBuffer
500
- ) {
501
- spritePass.setIndexBuffer(entry.mesh.indexBuffer.handle, entry.mesh.indexFormat);
502
- lastPbrSubIndexBuffer = entry.mesh.indexBuffer;
449
+ const isSprite = (shader: string | undefined) =>
450
+ shader === 'forgeax::sprite' || shader === 'forgeax::sprite-lit';
451
+ const passes = selectedDispatch?.filter(
452
+ (entry) =>
453
+ entry.renderState?.blend !== undefined &&
454
+ !isSprite(entry.materialShaderId) &&
455
+ (matchedIndices === null || matchedIndices.has(entry.renderableIndex)),
456
+ );
457
+ const matched = new Map<number, Set<number>>();
458
+ const add = (index: number, handle: number) => {
459
+ let handles = matched.get(index);
460
+ if (handles === undefined) {
461
+ handles = new Set();
462
+ matched.set(index, handles);
503
463
  }
504
- for (let smIdx = 0; smIdx < entry.mesh.submeshes.length; smIdx++) {
505
- const sm = entry.mesh.submeshes[smIdx];
506
- if (sm === undefined) continue;
507
- const matSlotIdx = sm.materialSlot;
508
- const submeshMaterial = matsForRebind[matSlotIdx] ?? entry.source.material;
509
- // Only transparent submeshes belong in this blend sub-pass; opaque
510
- // submeshes were drawn in the geometry pass.
511
- if (submeshMaterial.transparent !== true) continue;
512
- const smShaderId = submeshMaterial.materialShaderId;
513
- if (smShaderId === undefined) continue;
514
- const probeBlendAvailable =
515
- runtime.device.caps.storageBuffer &&
516
- entry.source.probeBlendRecord !== undefined &&
517
- isStandardPbrMaterialShader(smShaderId);
518
- const probeBinding = probeBlendAvailable
519
- ? resolveProbeBlendBuffer(
520
- c,
521
- entry.source.probeBlendRecord,
522
- worldEntityKey(entry.source.worldId, entry.source.entityKey),
523
- )
524
- : undefined;
525
- const identityInstBg = resolveGeometryInstancesBindGroup(
526
- c,
527
- pipelineState.identityInstanceBuffer,
528
- probeBinding?.buffer,
529
- );
530
- if (probeBinding === undefined) spritePass.setBindGroup(3, identityInstBg);
531
- else spritePass.setBindGroup(3, identityInstBg, [probeBinding.offset]);
532
- const subVariantSet = standardTopologyVariantSet(
533
- c.standardLighting,
534
- runtime.device.caps.storageBuffer,
535
- entry.mesh.layoutProjection.attributes.some((attribute) => attribute.key === 'color'),
536
- probeBlendAvailable,
537
- );
538
- const projectionVariantSetResult = variantSetFromVertexLayoutProjection(
539
- entry.mesh.layoutProjection,
540
- subVariantSet,
541
- );
542
- if (!projectionVariantSetResult.ok) {
543
- runtime.errorRegistry.fire(projectionVariantSetResult.error);
544
- continue;
545
- }
546
- const projectionVariantSet = projectionVariantSetResult.value;
547
- const subPipelineEntry =
548
- c.runtime.getMaterialShaderPipelineEntry?.(
549
- smShaderId,
550
- isHdrTarget,
551
- submeshMaterial.renderState,
552
- sm.topology,
553
- entry.mesh.indexFormat,
554
- projectionVariantSet,
555
- 'forward',
556
- undefined,
557
- sampleCount,
558
- // Non-sRGB blend view (same override the sprite path uses).
559
- transparentPassColorFormat(c) as GPUTextureFormat,
560
- undefined,
561
- undefined,
562
- undefined,
563
- entry.mesh.layoutProjection,
564
- ) ?? null;
565
- if (subPipelineEntry === null) continue;
566
- const submeshContract = subPipelineEntry.group2Contract;
567
- const submeshGroup2 = selectMaterialGroup2(meshGroup2, c.meshBindGroup, submeshContract);
568
- if (submeshGroup2 === null) continue;
569
- spritePass.setBindGroup(2, submeshGroup2, [i * MESH_PER_ENTITY_STRIDE]);
570
-
571
- // Bind the per-submesh material BG first (mirrors the geometry pass
572
- // order), then resolve the PSO; a first-frame async-compile miss skips
573
- // only the draw, not the BG (one transient frame, PSO flows in next).
574
- const materialSlot = materialSlotIndices[i]?.[matSlotIdx] ?? materialSlotIndices[i]?.[0] ?? 0;
575
- const subBg = resolveMaterialBindGroup(
576
- materialSlot,
577
- submeshMaterial,
578
- entry.source.entityKey,
579
- entry.world ?? c.world,
580
- );
581
- spritePass.setBindGroup(1, subBg, [materialSlot * MATERIAL_PER_ENTITY_STRIDE]);
582
-
583
- const subPipeline = subPipelineEntry.pipeline;
584
-
585
- if (lastPbrSubPipelineHandle !== subPipeline) {
586
- spritePass.setPipeline(subPipeline);
587
- lastPbrSubPipelineHandle = subPipeline;
588
- }
589
- if (entry.mesh.indexed) {
590
- spritePass.drawIndexed(sm.indexCount, 1, sm.indexOffset, 0, 0);
591
- } else {
592
- spritePass.draw(sm.vertexCount, 1, 0, 0);
464
+ handles.add(handle);
465
+ };
466
+ if (passes !== undefined) {
467
+ for (const entry of passes) add(entry.renderableIndex, entry.materialHandle);
468
+ } else {
469
+ for (const entry of c.validatedOrdered) {
470
+ if (matchedIndices !== null && !matchedIndices.has(entry.renderableIndex)) continue;
471
+ if (isSprite(entry.source.material.materialShaderId)) continue;
472
+ for (const material of entry.source.materials) {
473
+ if (material.transparent === true) add(entry.renderableIndex, material.materialHandle ?? 0);
593
474
  }
594
475
  }
595
476
  }
477
+ recordGeometryDraws(
478
+ {
479
+ ...c,
480
+ splitLdrSprite: false,
481
+ transparentColorFormat: transparentPassColorFormat(c) as GPUTextureFormat,
482
+ },
483
+ pass,
484
+ matched,
485
+ materialSlotIndices,
486
+ sampleCount,
487
+ meshGroup2,
488
+ c.meshBindGroup,
489
+ resolveMaterialBindGroup,
490
+ 'forward',
491
+ passes,
492
+ );
596
493
  }
597
494
 
598
495
  /**
@@ -856,7 +753,7 @@ function recordSpriteEntityDraws(
856
753
  new RhiError({
857
754
  code: 'limit-exceeded',
858
755
  expected: `requestedBytes (${requestedBytes}) <= maxStorageBufferBindingSize (${cap})`,
859
- hint: 'reduce instance count to fit within device.limits.maxStorageBufferBindingSize, or split transforms across multiple Instances entries',
756
+ hint: 'reduce the sprite batch to fit within device.limits.maxStorageBufferBindingSize or use a storage-capable backend',
860
757
  detail: {
861
758
  maxStorageBufferBindingSize: cap,
862
759
  requestedBytes,