@babylonjs/core 9.12.1 → 9.14.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 (141) hide show
  1. package/Animations/runtimeAnimation.js +11 -0
  2. package/Animations/runtimeAnimation.js.map +1 -1
  3. package/AudioV2/abstractAudio/abstractSound.d.ts +2 -1
  4. package/AudioV2/abstractAudio/abstractSound.js +3 -2
  5. package/AudioV2/abstractAudio/abstractSound.js.map +1 -1
  6. package/AudioV2/abstractAudio/abstractSoundInstance.d.ts +1 -1
  7. package/AudioV2/abstractAudio/abstractSoundInstance.js.map +1 -1
  8. package/AudioV2/abstractAudio/staticSound.d.ts +6 -0
  9. package/AudioV2/abstractAudio/staticSound.js +9 -1
  10. package/AudioV2/abstractAudio/staticSound.js.map +1 -1
  11. package/AudioV2/abstractAudio/streamingSound.d.ts +6 -0
  12. package/AudioV2/abstractAudio/streamingSound.js +9 -1
  13. package/AudioV2/abstractAudio/streamingSound.js.map +1 -1
  14. package/AudioV2/webAudio/webAudioStaticSound.d.ts +1 -1
  15. package/AudioV2/webAudio/webAudioStaticSound.js +6 -3
  16. package/AudioV2/webAudio/webAudioStaticSound.js.map +1 -1
  17. package/AudioV2/webAudio/webAudioStreamingSound.d.ts +1 -1
  18. package/AudioV2/webAudio/webAudioStreamingSound.js +7 -4
  19. package/AudioV2/webAudio/webAudioStreamingSound.js.map +1 -1
  20. package/Cameras/arcRotateCamera.pure.js +9 -3
  21. package/Cameras/arcRotateCamera.pure.js.map +1 -1
  22. package/Cameras/targetCamera.pure.js +18 -18
  23. package/Cameras/targetCamera.pure.js.map +1 -1
  24. package/Collisions/gpuPicker.d.ts +111 -4
  25. package/Collisions/gpuPicker.js +696 -119
  26. package/Collisions/gpuPicker.js.map +1 -1
  27. package/Engines/Native/nativeInterfaces.d.ts +4 -2
  28. package/Engines/Native/nativeInterfaces.js.map +1 -1
  29. package/Engines/Native/nativeRenderTargetWrapper.d.ts +3 -0
  30. package/Engines/Native/nativeRenderTargetWrapper.js +24 -1
  31. package/Engines/Native/nativeRenderTargetWrapper.js.map +1 -1
  32. package/Engines/WebGPU/Extensions/engine.videoTexture.pure.js +16 -3
  33. package/Engines/WebGPU/Extensions/engine.videoTexture.pure.js.map +1 -1
  34. package/Engines/abstractEngine.pure.d.ts +8 -1
  35. package/Engines/abstractEngine.pure.js +4 -3
  36. package/Engines/abstractEngine.pure.js.map +1 -1
  37. package/Engines/engine.d.ts +113 -0
  38. package/Engines/nullEngine.pure.js +5 -1
  39. package/Engines/nullEngine.pure.js.map +1 -1
  40. package/Engines/thinNativeEngine.pure.d.ts +5 -0
  41. package/Engines/thinNativeEngine.pure.js +158 -13
  42. package/Engines/thinNativeEngine.pure.js.map +1 -1
  43. package/FlowGraph/flowGraph.js +9 -0
  44. package/FlowGraph/flowGraph.js.map +1 -1
  45. package/FlowGraph/flowGraphDataConnection.pure.d.ts +14 -0
  46. package/FlowGraph/flowGraphDataConnection.pure.js +38 -1
  47. package/FlowGraph/flowGraphDataConnection.pure.js.map +1 -1
  48. package/FlowGraph/serialization.d.ts +11 -0
  49. package/FlowGraph/serialization.js +11 -1
  50. package/FlowGraph/serialization.js.map +1 -1
  51. package/FrameGraph/Tasks/Rendering/iblShadows/iblShadowsAccumulationTask.d.ts +2 -2
  52. package/FrameGraph/Tasks/Rendering/iblShadows/iblShadowsAccumulationTask.js +43 -51
  53. package/FrameGraph/Tasks/Rendering/iblShadows/iblShadowsAccumulationTask.js.map +1 -1
  54. package/FrameGraph/Tasks/Rendering/iblShadowsRendererTask.pure.d.ts +0 -1
  55. package/FrameGraph/Tasks/Rendering/iblShadowsRendererTask.pure.js +63 -35
  56. package/FrameGraph/Tasks/Rendering/iblShadowsRendererTask.pure.js.map +1 -1
  57. package/Lights/Clustered/clusteredLightContainer.pure.d.ts +1 -1
  58. package/Lights/Clustered/clusteredLightContainer.pure.js +2 -2
  59. package/Lights/Clustered/clusteredLightContainer.pure.js.map +1 -1
  60. package/Lights/areaLight.pure.js +5 -0
  61. package/Lights/areaLight.pure.js.map +1 -1
  62. package/Lights/light.d.ts +4 -2
  63. package/Lights/light.js +10 -4
  64. package/Lights/light.js.map +1 -1
  65. package/Lights/rectAreaLight.pure.d.ts +17 -0
  66. package/Lights/rectAreaLight.pure.js +67 -0
  67. package/Lights/rectAreaLight.pure.js.map +1 -1
  68. package/Loading/Plugins/babylonFileLoader.pure.js +1 -1
  69. package/Loading/Plugins/babylonFileLoader.pure.js.map +1 -1
  70. package/Loading/sceneLoader.d.ts +5 -1
  71. package/Loading/sceneLoader.js +298 -401
  72. package/Loading/sceneLoader.js.map +1 -1
  73. package/Materials/GaussianSplatting/gaussianSplattingGpuPickingMaterialPlugin.pure.d.ts +15 -0
  74. package/Materials/GaussianSplatting/gaussianSplattingGpuPickingMaterialPlugin.pure.js +34 -1
  75. package/Materials/GaussianSplatting/gaussianSplattingGpuPickingMaterialPlugin.pure.js.map +1 -1
  76. package/Materials/GaussianSplatting/gaussianSplattingMaterial.pure.d.ts +9 -0
  77. package/Materials/GaussianSplatting/gaussianSplattingMaterial.pure.js +26 -2
  78. package/Materials/GaussianSplatting/gaussianSplattingMaterial.pure.js.map +1 -1
  79. package/Materials/Node/Blocks/GaussianSplatting/gaussianSplattingBlock.pure.js +3 -1
  80. package/Materials/Node/Blocks/GaussianSplatting/gaussianSplattingBlock.pure.js.map +1 -1
  81. package/Materials/Textures/HTML/htmlInCanvasPolyfill.d.ts +43 -0
  82. package/Materials/Textures/HTML/htmlInCanvasPolyfill.js +64 -0
  83. package/Materials/Textures/HTML/htmlInCanvasPolyfill.js.map +1 -0
  84. package/Materials/Textures/HTML/htmlInteractionManager.d.ts +69 -0
  85. package/Materials/Textures/HTML/htmlInteractionManager.js +130 -0
  86. package/Materials/Textures/HTML/htmlInteractionManager.js.map +1 -0
  87. package/Materials/Textures/HTML/htmlRaycastInteractionManager.d.ts +84 -0
  88. package/Materials/Textures/HTML/htmlRaycastInteractionManager.js +246 -0
  89. package/Materials/Textures/HTML/htmlRaycastInteractionManager.js.map +1 -0
  90. package/Materials/Textures/HTML/htmlTexture.d.ts +148 -0
  91. package/Materials/Textures/HTML/htmlTexture.js +455 -0
  92. package/Materials/Textures/HTML/htmlTexture.js.map +1 -0
  93. package/Materials/Textures/HTML/index.d.ts +4 -0
  94. package/Materials/Textures/HTML/index.js +5 -0
  95. package/Materials/Textures/HTML/index.js.map +1 -0
  96. package/Materials/Textures/HTML/pure.d.ts +5 -0
  97. package/Materials/Textures/HTML/pure.js +6 -0
  98. package/Materials/Textures/HTML/pure.js.map +1 -0
  99. package/Materials/Textures/index.d.ts +1 -0
  100. package/Materials/Textures/index.js +1 -1
  101. package/Materials/Textures/index.js.map +1 -1
  102. package/Materials/Textures/pure.d.ts +1 -0
  103. package/Materials/Textures/pure.js +1 -0
  104. package/Materials/Textures/pure.js.map +1 -1
  105. package/Materials/Textures/videoTexture.pure.d.ts +3 -0
  106. package/Materials/Textures/videoTexture.pure.js +30 -0
  107. package/Materials/Textures/videoTexture.pure.js.map +1 -1
  108. package/Meshes/GaussianSplatting/gaussianSplattingMeshBase.pure.d.ts +36 -0
  109. package/Meshes/GaussianSplatting/gaussianSplattingMeshBase.pure.js +56 -0
  110. package/Meshes/GaussianSplatting/gaussianSplattingMeshBase.pure.js.map +1 -1
  111. package/Meshes/GaussianSplatting/gaussianSplattingSortWorker.d.ts +3 -1
  112. package/Meshes/GaussianSplatting/gaussianSplattingSortWorker.js +210 -48
  113. package/Meshes/GaussianSplatting/gaussianSplattingSortWorker.js.map +1 -1
  114. package/Misc/tools.pure.js +1 -1
  115. package/Misc/tools.pure.js.map +1 -1
  116. package/Rendering/IBLShadows/iblShadowsVoxelRenderer.pure.d.ts +1 -0
  117. package/Rendering/IBLShadows/iblShadowsVoxelRenderer.pure.js +34 -0
  118. package/Rendering/IBLShadows/iblShadowsVoxelRenderer.pure.js.map +1 -1
  119. package/Shaders/ShadersInclude/gaussianSplatting.js +1 -0
  120. package/Shaders/ShadersInclude/gaussianSplatting.js.map +1 -1
  121. package/Shaders/gaussianSplatting.fragment.js +17 -1
  122. package/Shaders/gaussianSplatting.fragment.js.map +1 -1
  123. package/Shaders/gaussianSplatting.vertex.js +1 -1
  124. package/Shaders/gaussianSplatting.vertex.js.map +1 -1
  125. package/Shaders/gaussianSplattingDepth.vertex.js +1 -1
  126. package/Shaders/gaussianSplattingDepth.vertex.js.map +1 -1
  127. package/Shaders/picking.fragment.js +39 -3
  128. package/Shaders/picking.fragment.js.map +1 -1
  129. package/ShadersWGSL/ShadersInclude/gaussianSplatting.js +3 -1
  130. package/ShadersWGSL/ShadersInclude/gaussianSplatting.js.map +1 -1
  131. package/ShadersWGSL/ShadersInclude/openpbrIblFunctions.js +4 -3
  132. package/ShadersWGSL/ShadersInclude/openpbrIblFunctions.js.map +1 -1
  133. package/ShadersWGSL/gaussianSplatting.fragment.js +14 -1
  134. package/ShadersWGSL/gaussianSplatting.fragment.js.map +1 -1
  135. package/ShadersWGSL/gaussianSplatting.vertex.js +2 -2
  136. package/ShadersWGSL/gaussianSplatting.vertex.js.map +1 -1
  137. package/ShadersWGSL/gaussianSplattingDepth.vertex.js +2 -2
  138. package/ShadersWGSL/gaussianSplattingDepth.vertex.js.map +1 -1
  139. package/ShadersWGSL/picking.fragment.js +22 -1
  140. package/ShadersWGSL/picking.fragment.js.map +1 -1
  141. package/package.json +1 -1
@@ -1,14 +1,35 @@
1
1
 
2
+ import { MultiRenderTarget } from "../Materials/Textures/multiRenderTarget.pure.js";
2
3
  import { RenderTargetTexture } from "../Materials/Textures/renderTargetTexture.pure.js";
3
4
  import { ShaderMaterial } from "../Materials/shaderMaterial.pure.js";
4
5
  import { GaussianSplattingMaterial } from "../Materials/GaussianSplatting/gaussianSplattingMaterial.pure.js";
5
6
  import { GaussianSplattingGpuPickingMaterialPlugin } from "../Materials/GaussianSplatting/gaussianSplattingGpuPickingMaterialPlugin.pure.js";
6
7
  import { Color4 } from "../Maths/math.color.pure.js";
8
+ import { Epsilon } from "../Maths/math.constants.js";
9
+ import { Matrix, TmpVectors, Vector3 } from "../Maths/math.vector.pure.js";
7
10
  import { VertexBuffer } from "../Meshes/buffer.js";
8
11
  import { Logger } from "../Misc/logger.js";
12
+ /**
13
+ * Defines how multi pick texture readbacks should be performed.
14
+ */
15
+ export var GPUMultiPickReadbackStrategy;
16
+ (function (GPUMultiPickReadbackStrategy) {
17
+ /**
18
+ * Chooses between a single rectangle readback and small per-point readbacks using the thresholds in IGPUMultiPickOptions.
19
+ */
20
+ GPUMultiPickReadbackStrategy[GPUMultiPickReadbackStrategy["Auto"] = 0] = "Auto";
21
+ /**
22
+ * Always reads the full bounding rectangle of the picked points. This minimizes readback calls and is best for dense point sets.
23
+ */
24
+ GPUMultiPickReadbackStrategy[GPUMultiPickReadbackStrategy["Rectangle"] = 1] = "Rectangle";
25
+ /**
26
+ * Always reads each picked point independently. This minimizes transferred pixels for sparse point sets but can be slower when many points are picked.
27
+ */
28
+ GPUMultiPickReadbackStrategy[GPUMultiPickReadbackStrategy["Individual"] = 2] = "Individual";
29
+ })(GPUMultiPickReadbackStrategy || (GPUMultiPickReadbackStrategy = {}));
9
30
  /**
10
31
  * Class used to perform a picking operation using GPU
11
- * GPUPIcker can pick meshes, instances and thin instances
32
+ * GPUPicker can pick meshes, instances and thin instances
12
33
  */
13
34
  export class GPUPicker {
14
35
  constructor() {
@@ -23,10 +44,16 @@ export class GPUPicker {
23
44
  this._pickableMeshes = [];
24
45
  this._meshMaterialMap = new Map();
25
46
  this._readbuffer = null;
47
+ this._depthReadbuffer = null;
48
+ this._depthTextureType = 0;
49
+ this._isDepthTexturePacked = false;
50
+ this._useDepthPicking = false;
51
+ this._isUsingDepthPickingRenderTarget = false;
26
52
  this._meshRenderingCount = 0;
27
53
  this._renderWarningIssued = false;
28
54
  this._renderPickingTexture = false;
29
55
  this._sceneBeforeRenderObserver = null;
56
+ this._pickingTextureClearObserver = null;
30
57
  this._pickingTextureAfterRenderObserver = null;
31
58
  this._nextFreeId = 1;
32
59
  this._gsPickingMaterials = [];
@@ -55,12 +82,68 @@ export class GPUPicker {
55
82
  get defaultRenderMaterials() {
56
83
  return this._pickingMaterialCache;
57
84
  }
85
+ /**
86
+ * Gets or sets a boolean indicating if depth-based pickedPoint and normal reconstruction should be enabled.
87
+ *
88
+ * When disabled, GPUPicker uses the original single-color render target and shader path. When enabled, GPUPicker
89
+ * switches to a MultiRenderTarget and compiles the default picking shader with GPUPICKER_DEPTH to output both the
90
+ * picking id and the depth required to reconstruct the picked point and normal.
91
+ *
92
+ * Custom picking materials and special picking material plugins should also write the depth attachment. If they do
93
+ * not, GPUPicker will still try to reconstruct pickedPoint and normal from the depth target, but the returned values
94
+ * may be missing or incorrect.
95
+ */
96
+ get enableDepthPicking() {
97
+ return this._useDepthPicking;
98
+ }
99
+ set enableDepthPicking(value) {
100
+ if (this._useDepthPicking === value) {
101
+ return;
102
+ }
103
+ this._useDepthPicking = value;
104
+ this._isUsingDepthPickingRenderTarget = false;
105
+ this._depthReadbuffer = null;
106
+ let pickableMeshes = [];
107
+ if (this._cachedScene && this._pickingTexture) {
108
+ pickableMeshes = this._pickableMeshes.map((mesh) => {
109
+ const material = this._meshMaterialMap.get(mesh);
110
+ const className = mesh.getClassName();
111
+ if (material instanceof ShaderMaterial &&
112
+ !this._pickingMaterialCache.includes(material) &&
113
+ className !== "GaussianSplattingMesh" &&
114
+ className !== "GaussianSplattingPartProxyMesh") {
115
+ return { mesh, material };
116
+ }
117
+ return mesh;
118
+ });
119
+ }
120
+ this._clearPickingMaterials();
121
+ if (this._cachedScene && this._pickingTexture) {
122
+ this.clearPickingList();
123
+ this._pickingTexture.dispose();
124
+ this._pickingTexture = null;
125
+ if (pickableMeshes.length > 0) {
126
+ this.addPickingList(pickableMeshes);
127
+ }
128
+ }
129
+ }
58
130
  _getColorIdFromReadBuffer(offset) {
59
131
  const r = this._readbuffer[offset];
60
132
  const g = this._readbuffer[offset + 1];
61
133
  const b = this._readbuffer[offset + 2];
62
134
  return (r << 16) + (g << 8) + b;
63
135
  }
136
+ _getReadBufferOffset(x, y, width, height) {
137
+ const bufferY = this._cachedScene?.getEngine().isWebGPU ? height - y - 1 : y;
138
+ return (bufferY * width + x) * 4;
139
+ }
140
+ _createColorPickingRenderTarget(scene, width, height) {
141
+ return new RenderTargetTexture("pickingTexture", { width: width, height: height }, scene, {
142
+ generateMipMaps: false,
143
+ type: 0,
144
+ samplingMode: 1,
145
+ });
146
+ }
64
147
  _createRenderTarget(scene, width, height) {
65
148
  if (this._cachedScene && this._pickingTexture) {
66
149
  const index = this._cachedScene.customRenderTargets.indexOf(this._pickingTexture);
@@ -72,11 +155,53 @@ export class GPUPicker {
72
155
  if (this._pickingTexture) {
73
156
  this._pickingTexture.dispose();
74
157
  }
75
- this._pickingTexture = new RenderTargetTexture("pickingTexure", { width: width, height: height }, scene, {
76
- generateMipMaps: false,
77
- type: 0,
78
- samplingMode: 1,
79
- });
158
+ if (this._useDepthPicking) {
159
+ const engine = scene.getEngine();
160
+ const supportsDepthPickingRenderTarget = engine.isWebGPU || engine.webGLVersion !== 1;
161
+ if (!supportsDepthPickingRenderTarget) {
162
+ Logger.Warn("GPUPicker depth picking requires WebGL2, WebGPU, or Native engine support. Falling back to color-only GPU picking.");
163
+ this._useDepthPicking = false;
164
+ this._isUsingDepthPickingRenderTarget = false;
165
+ this._isDepthTexturePacked = false;
166
+ this._pickingTexture = this._createColorPickingRenderTarget(scene, width, height);
167
+ return;
168
+ }
169
+ if (engine.getCaps().textureFloatRender) {
170
+ this._depthTextureType = 1;
171
+ }
172
+ else if (engine.getCaps().textureHalfFloatRender) {
173
+ this._depthTextureType = 2;
174
+ }
175
+ else {
176
+ this._depthTextureType = 0;
177
+ }
178
+ this._isDepthTexturePacked = this._depthTextureType === 0;
179
+ const pickingTexture = new MultiRenderTarget("pickingTexture", { width: width, height: height }, 2, scene, {
180
+ generateMipMaps: false,
181
+ generateDepthBuffer: true,
182
+ generateStencilBuffer: false,
183
+ types: [0, this._depthTextureType],
184
+ samplingModes: [1, 1],
185
+ formats: [5, 5],
186
+ }, ["pickingTexture_id", "pickingTexture_depth"]);
187
+ if (pickingTexture.isSupported) {
188
+ this._pickingTexture = pickingTexture;
189
+ this._isUsingDepthPickingRenderTarget = true;
190
+ }
191
+ else {
192
+ Logger.Warn("GPUPicker depth picking requires MultiRenderTarget support. Falling back to color-only GPU picking.");
193
+ this._useDepthPicking = false;
194
+ this._isUsingDepthPickingRenderTarget = false;
195
+ this._isDepthTexturePacked = false;
196
+ pickingTexture.dispose();
197
+ this._pickingTexture = this._createColorPickingRenderTarget(scene, width, height);
198
+ }
199
+ }
200
+ else {
201
+ this._pickingTexture = this._createColorPickingRenderTarget(scene, width, height);
202
+ this._isUsingDepthPickingRenderTarget = false;
203
+ this._isDepthTexturePacked = false;
204
+ }
80
205
  }
81
206
  _clearPickingMaterials() {
82
207
  for (let i = 0; i < this._pickingMaterialCache.length; i++) {
@@ -100,6 +225,12 @@ export class GPUPicker {
100
225
  this._shaderLanguage = 1 /* ShaderLanguage.WGSL */;
101
226
  }
102
227
  const defines = [];
228
+ if (this._useDepthPicking) {
229
+ defines.push("#define GPUPICKER_DEPTH");
230
+ if (this._isDepthTexturePacked) {
231
+ defines.push("#define GPUPICKER_PACK_DEPTH");
232
+ }
233
+ }
103
234
  const options = {
104
235
  attributes: [VertexBuffer.PositionKind, GPUPicker._AttributeName],
105
236
  uniforms: ["world", "viewProjection", "meshID"],
@@ -429,25 +560,28 @@ export class GPUPicker {
429
560
  if (!this._pickableMeshes || this._pickableMeshes.length === 0) {
430
561
  return null;
431
562
  }
432
- const { rttSizeW, rttSizeH, devicePixelRatio } = this._getRenderInfo();
433
- const { x: adjustedX, y: adjustedY } = this._prepareForPicking(x, y, devicePixelRatio);
563
+ const { rttSizeW, rttSizeH, scaleX, scaleY } = this._getRenderInfo();
564
+ const { x: adjustedX, y: adjustedY } = this._prepareForPicking(x, y, scaleX, scaleY);
434
565
  if (adjustedX < 0 || adjustedY < 0 || adjustedX >= rttSizeW || adjustedY >= rttSizeH) {
435
566
  return null;
436
567
  }
437
568
  this._pickingInProgress = true;
438
569
  // Invert Y
439
570
  const invertedY = rttSizeH - adjustedY - 1;
440
- this._preparePickingBuffer(this._engine, rttSizeW, rttSizeH, adjustedX, invertedY);
571
+ const pickingRegion = this._getPickingRenderRegion(adjustedX, invertedY, rttSizeW, rttSizeH);
572
+ this._preparePickingBuffer(this._engine, rttSizeW, rttSizeH, pickingRegion.x, pickingRegion.y, pickingRegion.width, pickingRegion.height);
441
573
  await this._waitForPickingMaterialsReadyAsync();
574
+ this._addPickingTextureToRenderTargets();
442
575
  return await this._executePickingAsync(adjustedX, invertedY, disposeWhenDone);
443
576
  }
444
577
  /**
445
578
  * Execute a picking operation on multiple coordinates
446
579
  * @param xy defines the X,Y coordinates where to run the pick
447
580
  * @param disposeWhenDone defines a boolean indicating we do not want to keep resources alive (false by default)
581
+ * @param options defines options used to tune the multi pick readback strategy
448
582
  * @returns A promise with the picking results. Always returns an array with the same length as the number of coordinates. The mesh or null at the index where no mesh was picked.
449
583
  */
450
- async multiPickAsync(xy, disposeWhenDone = false) {
584
+ async multiPickAsync(xy, disposeWhenDone = false, options) {
451
585
  if (this._pickingInProgress) {
452
586
  return null;
453
587
  }
@@ -456,10 +590,15 @@ export class GPUPicker {
456
590
  }
457
591
  if (xy.length === 1) {
458
592
  const pi = await this.pickAsync(xy[0].x, xy[0].y, disposeWhenDone);
459
- return {
593
+ const result = {
460
594
  meshes: [pi?.mesh ?? null],
461
- thinInstanceIndexes: pi?.thinInstanceIndex ? [pi.thinInstanceIndex] : undefined,
595
+ thinInstanceIndexes: pi?.thinInstanceIndex !== undefined ? [pi.thinInstanceIndex] : undefined,
462
596
  };
597
+ if (this._useDepthPicking) {
598
+ result.pickedPoints = [pi?.pickedPoint ?? null];
599
+ result.normals = [pi?.normal ?? null];
600
+ }
601
+ return result;
463
602
  }
464
603
  this._pickingInProgress = true;
465
604
  const processedXY = new Array(xy.length);
@@ -467,28 +606,50 @@ export class GPUPicker {
467
606
  let maxX = -Infinity;
468
607
  let minY = Infinity;
469
608
  let maxY = -Infinity;
470
- const { rttSizeW, rttSizeH, devicePixelRatio } = this._getRenderInfo();
609
+ const { rttSizeW, rttSizeH, scaleX, scaleY } = this._getRenderInfo();
610
+ let hasInBoundsPoint = false;
611
+ let inBoundsPointCount = 0;
471
612
  // Process screen coordinates adjust to dpr
472
613
  for (let i = 0; i < xy.length; i++) {
473
614
  const item = xy[i];
474
615
  const { x, y } = item;
475
- const { x: adjustedX, y: adjustedY } = this._prepareForPicking(x, y, devicePixelRatio);
616
+ const { x: adjustedX, y: adjustedY } = this._prepareForPicking(x, y, scaleX, scaleY);
476
617
  processedXY[i] = {
477
618
  ...item,
478
619
  x: adjustedX,
479
620
  y: adjustedY,
480
621
  };
622
+ if (adjustedX < 0 || adjustedY < 0 || adjustedX >= rttSizeW || adjustedY >= rttSizeH) {
623
+ continue;
624
+ }
625
+ hasInBoundsPoint = true;
626
+ inBoundsPointCount++;
481
627
  minX = Math.min(minX, adjustedX);
482
628
  maxX = Math.max(maxX, adjustedX);
483
629
  minY = Math.min(minY, adjustedY);
484
630
  maxY = Math.max(maxY, adjustedY);
485
631
  }
486
- const w = Math.max(maxX - minX, 1);
487
- const h = Math.max(maxY - minY, 1);
488
- const partialCutH = rttSizeH - maxY - 1;
489
- this._preparePickingBuffer(this._engine, rttSizeW, rttSizeH, minX, partialCutH, w, h);
632
+ if (!hasInBoundsPoint) {
633
+ this._pickingInProgress = false;
634
+ return null;
635
+ }
636
+ const depthPadding = this._useDepthPicking ? GPUPicker._DepthPixelRadius : 0;
637
+ const regionLeft = Math.max(minX - depthPadding, 0);
638
+ const regionRight = Math.min(maxX + depthPadding, rttSizeW - 1);
639
+ const regionTop = Math.max(minY - depthPadding, 0);
640
+ const regionBottom = Math.min(maxY + depthPadding, rttSizeH - 1);
641
+ if (regionLeft >= rttSizeW || regionTop >= rttSizeH || regionRight < 0 || regionBottom < 0) {
642
+ this._pickingInProgress = false;
643
+ return null;
644
+ }
645
+ const w = regionRight - regionLeft + 1;
646
+ const h = regionBottom - regionTop + 1;
647
+ const partialCutH = rttSizeH - regionBottom - 1;
648
+ const useIndividualReadback = this._shouldUseIndividualMultiPickReadback(inBoundsPointCount, w * h, options);
649
+ this._preparePickingBuffer(this._engine, rttSizeW, rttSizeH, regionLeft, partialCutH, w, h, useIndividualReadback ? 1 : w, useIndividualReadback ? 1 : h);
490
650
  await this._waitForPickingMaterialsReadyAsync();
491
- return await this._executeMultiPickingAsync(processedXY, minX, maxY, rttSizeH, w, h, disposeWhenDone);
651
+ this._addPickingTextureToRenderTargets();
652
+ return await this._executeMultiPickingAsync(processedXY, regionLeft, partialCutH, rttSizeW, rttSizeH, w, h, useIndividualReadback, disposeWhenDone);
492
653
  }
493
654
  /**
494
655
  * Execute a picking operation on box defined by two screen coordinates
@@ -497,7 +658,7 @@ export class GPUPicker {
497
658
  * @param x2 defines the X coordinate of the opposite corner of the box where to run the pick
498
659
  * @param y2 defines the Y coordinate of the opposite corner of the box where to run the pick
499
660
  * @param disposeWhenDone defines a boolean indicating we do not want to keep resources alive (false by default)
500
- * @returns A promise with the picking results. Always returns an array with the same length as the number of coordinates. The mesh or null at the index where no mesh was picked.
661
+ * @returns A promise with the picking results. Contains one entry for each picked pixel in the box.
501
662
  */
502
663
  async boxPickAsync(x1, y1, x2, y2, disposeWhenDone = false) {
503
664
  if (this._pickingInProgress) {
@@ -507,9 +668,9 @@ export class GPUPicker {
507
668
  return null;
508
669
  }
509
670
  this._pickingInProgress = true;
510
- const { rttSizeW, rttSizeH, devicePixelRatio } = this._getRenderInfo();
511
- const { x: adjustedX1, y: adjustedY1 } = this._prepareForPicking(x1, y1, devicePixelRatio);
512
- const { x: adjustedX2, y: adjustedY2 } = this._prepareForPicking(x2, y2, devicePixelRatio);
671
+ const { rttSizeW, rttSizeH, scaleX, scaleY } = this._getRenderInfo();
672
+ const { x: adjustedX1, y: adjustedY1 } = this._prepareForPicking(x1, y1, scaleX, scaleY);
673
+ const { x: adjustedX2, y: adjustedY2 } = this._prepareForPicking(x2, y2, scaleX, scaleY);
513
674
  const minX = Math.max(Math.min(adjustedX1, adjustedX2), 0);
514
675
  const maxX = Math.min(Math.max(adjustedX1, adjustedX2), rttSizeW - 1);
515
676
  const minY = Math.max(Math.min(adjustedY1, adjustedY2), 0);
@@ -518,30 +679,84 @@ export class GPUPicker {
518
679
  this._pickingInProgress = false;
519
680
  return null;
520
681
  }
521
- const w = Math.max(maxX - minX, 1);
522
- const h = Math.max(maxY - minY, 1);
523
- const partialCutH = rttSizeH - maxY - 1;
524
- this._preparePickingBuffer(this._engine, rttSizeW, rttSizeH, minX, partialCutH, w, h);
682
+ const depthPadding = this._useDepthPicking ? GPUPicker._DepthPixelRadius : 0;
683
+ const regionLeft = Math.max(minX - depthPadding, 0);
684
+ const regionRight = Math.min(maxX + depthPadding, rttSizeW - 1);
685
+ const regionTop = Math.max(minY - depthPadding, 0);
686
+ const regionBottom = Math.min(maxY + depthPadding, rttSizeH - 1);
687
+ const w = regionRight - regionLeft + 1;
688
+ const h = regionBottom - regionTop + 1;
689
+ const partialCutH = rttSizeH - regionBottom - 1;
690
+ this._preparePickingBuffer(this._engine, rttSizeW, rttSizeH, regionLeft, partialCutH, w, h);
525
691
  await this._waitForPickingMaterialsReadyAsync();
526
- return await this._executeBoxPickingAsync(minX, partialCutH, w, h, disposeWhenDone);
692
+ this._addPickingTextureToRenderTargets();
693
+ return await this._executeBoxPickingAsync(minX, maxY, Math.max(maxX - minX, 1), Math.max(maxY - minY, 1), regionLeft, partialCutH, rttSizeW, rttSizeH, w, h, disposeWhenDone);
527
694
  }
528
695
  _getRenderInfo() {
529
696
  const engine = this._cachedScene.getEngine();
530
697
  const rttSizeW = engine.getRenderWidth();
531
698
  const rttSizeH = engine.getRenderHeight();
532
- const devicePixelRatio = 1 / engine._hardwareScalingLevel;
699
+ // Picking coordinates are expected in input/canvas CSS pixels, matching Babylon pointer coordinates.
700
+ // Use the actual client rect so CSS scaling/stretching is handled, and fall back to hardware scaling
701
+ // for engines without a DOM input element.
702
+ let inputElementClientRect = null;
703
+ try {
704
+ inputElementClientRect = engine.getInputElementClientRect();
705
+ }
706
+ catch {
707
+ // Non-DOM or pure engine builds may not register the DOM side effect. Fall back below.
708
+ }
709
+ const scaleX = inputElementClientRect?.width ? rttSizeW / inputElementClientRect.width : 1 / engine._hardwareScalingLevel;
710
+ const scaleY = inputElementClientRect?.height ? rttSizeH / inputElementClientRect.height : 1 / engine._hardwareScalingLevel;
533
711
  return {
534
712
  rttSizeW,
535
713
  rttSizeH,
536
- devicePixelRatio,
714
+ scaleX,
715
+ scaleY,
537
716
  };
538
717
  }
539
- _prepareForPicking(x, y, devicePixelRatio) {
540
- return { x: (devicePixelRatio * x) >> 0, y: (devicePixelRatio * y) >> 0 };
718
+ _prepareForPicking(x, y, scaleX, scaleY) {
719
+ return { x: (scaleX * x) >> 0, y: (scaleY * y) >> 0 };
720
+ }
721
+ _getPickingRenderRegion(x, y, renderWidth, renderHeight) {
722
+ if (!this._useDepthPicking) {
723
+ return { x, y, width: 1, height: 1 };
724
+ }
725
+ const radius = GPUPicker._DepthPixelRadius;
726
+ const left = Math.max(x - radius, 0);
727
+ const bottom = Math.max(y - radius, 0);
728
+ const right = Math.min(x + radius, renderWidth - 1);
729
+ const top = Math.min(y + radius, renderHeight - 1);
730
+ return {
731
+ x: left,
732
+ y: bottom,
733
+ width: right - left + 1,
734
+ height: top - bottom + 1,
735
+ };
736
+ }
737
+ _shouldUseIndividualMultiPickReadback(inBoundsPointCount, readArea, options) {
738
+ const readbackStrategy = options?.readbackStrategy ?? 0 /* GPUMultiPickReadbackStrategy.Auto */;
739
+ if (inBoundsPointCount === 0 || readbackStrategy === 1 /* GPUMultiPickReadbackStrategy.Rectangle */) {
740
+ return false;
741
+ }
742
+ if (readbackStrategy === 2 /* GPUMultiPickReadbackStrategy.Individual */) {
743
+ return true;
744
+ }
745
+ const maxIndividualReadbackCount = options?.maxIndividualReadbackCount !== undefined && Number.isFinite(options.maxIndividualReadbackCount) && options.maxIndividualReadbackCount >= 0
746
+ ? options.maxIndividualReadbackCount
747
+ : GPUPicker._MaxMultiPickIndividualReadbackCount;
748
+ if (inBoundsPointCount > maxIndividualReadbackCount) {
749
+ return false;
750
+ }
751
+ const individualReadbackAreaRatio = options?.individualReadbackAreaRatio !== undefined && Number.isFinite(options.individualReadbackAreaRatio) && options.individualReadbackAreaRatio > 0
752
+ ? options.individualReadbackAreaRatio
753
+ : GPUPicker._MultiPickIndividualReadbackAreaRatio;
754
+ const pointReadArea = inBoundsPointCount * (this._useDepthPicking ? 1 + (GPUPicker._DepthPixelRadius * 2 + 1) ** 2 : 1);
755
+ return readArea > pointReadArea * individualReadbackAreaRatio;
541
756
  }
542
- _preparePickingBuffer(engine, rttSizeW, rttSizeH, x, y, w = 1, h = 1) {
757
+ _preparePickingBuffer(engine, rttSizeW, rttSizeH, x, y, w = 1, h = 1, readBufferW = w, readBufferH = h) {
543
758
  this._meshRenderingCount = 0;
544
- const requiredBufferSize = engine.isWebGPU ? (4 * w * h + 255) & ~255 : 4 * w * h;
759
+ const requiredBufferSize = engine.isWebGPU ? (4 * readBufferW * readBufferH + 255) & ~255 : 4 * readBufferW * readBufferH;
545
760
  if (!this._readbuffer || this._readbuffer.length < requiredBufferSize) {
546
761
  this._readbuffer = new Uint8Array(requiredBufferSize);
547
762
  }
@@ -552,6 +767,16 @@ export class GPUPicker {
552
767
  this._updateRenderList();
553
768
  }
554
769
  this._pickingTexture.clearColor = new Color4(0, 0, 0, 0);
770
+ this._pickingTextureClearObserver?.remove();
771
+ this._pickingTextureClearObserver = this._pickingTexture.onClearObservable.add((engine) => {
772
+ if (this._isUsingDepthPickingRenderTarget) {
773
+ engine.bindAttachments(engine.buildTextureLayout([true, false]));
774
+ }
775
+ engine.clear(this._pickingTexture.clearColor, true, true, true);
776
+ if (this._isUsingDepthPickingRenderTarget) {
777
+ engine.bindAttachments(engine.buildTextureLayout([true, true]));
778
+ }
779
+ });
555
780
  this._pickingTexture.onBeforeRender = () => {
556
781
  this._enableScissor(x, y, w, h);
557
782
  };
@@ -559,9 +784,22 @@ export class GPUPicker {
559
784
  this._pickingTextureAfterRenderObserver = this._pickingTexture.onAfterRenderObservable.add(() => {
560
785
  this._disableScissor();
561
786
  });
787
+ }
788
+ _addPickingTextureToRenderTargets() {
789
+ this._removePickingTextureFromRenderTargets();
562
790
  this._cachedScene.customRenderTargets.push(this._pickingTexture);
563
791
  this._renderPickingTexture = true;
564
792
  }
793
+ _removePickingTextureFromRenderTargets() {
794
+ if (!this._cachedScene || !this._pickingTexture) {
795
+ return;
796
+ }
797
+ const index = this._cachedScene.customRenderTargets.indexOf(this._pickingTexture);
798
+ if (index > -1) {
799
+ this._cachedScene.customRenderTargets.splice(index, 1);
800
+ this._renderPickingTexture = false;
801
+ }
802
+ }
565
803
  // pick one pixel
566
804
  async _executePickingAsync(x, y, disposeWhenDone) {
567
805
  return await new Promise((resolve, reject) => {
@@ -573,43 +811,60 @@ export class GPUPicker {
573
811
  // eslint-disable-next-line @typescript-eslint/no-misused-promises
574
812
  this._pickingTexture.onAfterRender = async () => {
575
813
  if (this._checkRenderStatus()) {
576
- this._pickingTexture.onAfterRender = null;
577
- let pickedMesh = null;
578
- let thinInstanceIndex = undefined;
579
- // Remove from the active RTTs
580
- const index = this._cachedScene.customRenderTargets.indexOf(this._pickingTexture);
581
- if (index > -1) {
582
- this._cachedScene.customRenderTargets.splice(index, 1);
583
- this._renderPickingTexture = false;
584
- }
585
- // Do the actual picking
586
- if (await this._readTexturePixelsAsync(x, y)) {
587
- const colorId = this._getColorIdFromReadBuffer(0);
588
- // Thin?
589
- if (this._thinIdMap[colorId]) {
590
- pickedMesh = this._pickableMeshes[this._thinIdMap[colorId].meshId];
591
- thinInstanceIndex = this._thinIdMap[colorId].thinId;
814
+ try {
815
+ this._pickingTexture.onAfterRender = null;
816
+ let pickedMesh = null;
817
+ let thinInstanceIndex = undefined;
818
+ this._removePickingTextureFromRenderTargets();
819
+ // Do the actual picking
820
+ if (await this._readTexturePixelsAsync(x, y)) {
821
+ const colorId = this._getColorIdFromReadBuffer(0);
822
+ // Thin?
823
+ if (this._thinIdMap[colorId]) {
824
+ pickedMesh = this._pickableMeshes[this._thinIdMap[colorId].meshId];
825
+ thinInstanceIndex = this._thinIdMap[colorId].thinId;
826
+ }
827
+ else {
828
+ pickedMesh = this._pickableMeshes[this._idMap[colorId]];
829
+ }
830
+ }
831
+ // Depth reconstruction is guaranteed for GPUPicker's default picking shader. Custom picking
832
+ // materials and special material plugins can also work if they write the second MRT attachment;
833
+ // otherwise the reconstructed pickedPoint/normal may be missing or incorrect.
834
+ let depthPickingInfo = null;
835
+ if (this._useDepthPicking && pickedMesh) {
836
+ const camera = this._cachedScene.activeCamera;
837
+ if (camera) {
838
+ const { rttSizeW, rttSizeH } = this._getRenderInfo();
839
+ const viewport = camera.viewport.toGlobal(rttSizeW, rttSizeH);
840
+ const view = this._cachedScene.getViewMatrix().clone();
841
+ const projection = this._cachedScene.getProjectionMatrix().clone();
842
+ const cameraPosition = camera.globalPosition.clone();
843
+ depthPickingInfo = await this._getDepthPickingInfoAsync(x, y, rttSizeW, rttSizeH, view, projection, cameraPosition, viewport);
844
+ }
845
+ }
846
+ if (disposeWhenDone) {
847
+ this.dispose();
848
+ }
849
+ this._pickingInProgress = false;
850
+ if (pickedMesh) {
851
+ resolve({ mesh: pickedMesh, thinInstanceIndex: thinInstanceIndex, ...depthPickingInfo });
592
852
  }
593
853
  else {
594
- pickedMesh = this._pickableMeshes[this._idMap[colorId]];
854
+ resolve(null);
595
855
  }
596
856
  }
597
- if (disposeWhenDone) {
598
- this.dispose();
599
- }
600
- this._pickingInProgress = false;
601
- if (pickedMesh) {
602
- resolve({ mesh: pickedMesh, thinInstanceIndex: thinInstanceIndex });
603
- }
604
- else {
605
- resolve(null);
857
+ catch (error) {
858
+ this._removePickingTextureFromRenderTargets();
859
+ this._pickingInProgress = false;
860
+ reject(error instanceof Error ? error : new Error(`${error}`));
606
861
  }
607
862
  }
608
863
  };
609
864
  });
610
865
  }
611
866
  // pick multiple pixels
612
- async _executeMultiPickingAsync(xy, minX, maxY, rttSizeH, w, h, disposeWhenDone) {
867
+ async _executeMultiPickingAsync(xy, readX, readY, rttSizeW, rttSizeH, w, h, useIndividualReadback, disposeWhenDone) {
613
868
  return await new Promise((resolve, reject) => {
614
869
  if (!this._pickingTexture) {
615
870
  this._pickingInProgress = false;
@@ -619,27 +874,91 @@ export class GPUPicker {
619
874
  // eslint-disable-next-line @typescript-eslint/no-misused-promises
620
875
  this._pickingTexture.onAfterRender = async () => {
621
876
  if (this._checkRenderStatus()) {
622
- this._pickingTexture.onAfterRender = null;
623
- const pickedMeshes = [];
624
- const thinInstanceIndexes = [];
625
- if (await this._readTexturePixelsAsync(minX, rttSizeH - maxY - 1, w, h)) {
626
- for (let i = 0; i < xy.length; i++) {
627
- const { pickedMesh, thinInstanceIndex } = this._getMeshFromMultiplePoints(xy[i].x, xy[i].y, minX, maxY, w);
628
- pickedMeshes.push(pickedMesh);
629
- thinInstanceIndexes.push(thinInstanceIndex ?? 0);
877
+ try {
878
+ this._pickingTexture.onAfterRender = null;
879
+ const pickedMeshes = new Array(xy.length).fill(null);
880
+ const thinInstanceIndexes = new Array(xy.length).fill(0);
881
+ if (useIndividualReadback) {
882
+ for (let i = 0; i < xy.length; i++) {
883
+ const pointBottomY = rttSizeH - xy[i].y - 1;
884
+ if (xy[i].x < 0 || pointBottomY < 0 || xy[i].x >= rttSizeW || pointBottomY >= rttSizeH) {
885
+ continue;
886
+ }
887
+ // eslint-disable-next-line no-await-in-loop
888
+ if (await this._readTexturePixelsAsync(xy[i].x, pointBottomY, 1, 1)) {
889
+ const { pickedMesh, thinInstanceIndex } = this._getMeshFromReadBuffer(0);
890
+ pickedMeshes[i] = pickedMesh;
891
+ thinInstanceIndexes[i] = thinInstanceIndex ?? 0;
892
+ }
893
+ }
894
+ }
895
+ else {
896
+ if (await this._readTexturePixelsAsync(readX, readY, w, h)) {
897
+ for (let i = 0; i < xy.length; i++) {
898
+ const { pickedMesh, thinInstanceIndex } = this._getMeshFromMultiplePoints(xy[i].x, xy[i].y, readX, readY, rttSizeH, w, h);
899
+ pickedMeshes[i] = pickedMesh;
900
+ thinInstanceIndexes[i] = thinInstanceIndex ?? 0;
901
+ }
902
+ }
630
903
  }
904
+ let pickedPoints;
905
+ let normals;
906
+ if (this._useDepthPicking) {
907
+ pickedPoints = new Array(xy.length).fill(null);
908
+ normals = new Array(xy.length).fill(null);
909
+ }
910
+ if (pickedPoints && normals && pickedMeshes.some((mesh) => !!mesh)) {
911
+ const camera = this._cachedScene.activeCamera;
912
+ if (camera) {
913
+ const viewport = camera.viewport.toGlobal(rttSizeW, rttSizeH);
914
+ const view = this._cachedScene.getViewMatrix().clone();
915
+ const projection = this._cachedScene.getProjectionMatrix().clone();
916
+ const cameraPosition = camera.globalPosition.clone();
917
+ if (useIndividualReadback) {
918
+ for (let i = 0; i < xy.length; i++) {
919
+ if (!pickedMeshes[i]) {
920
+ continue;
921
+ }
922
+ const pointY = rttSizeH - xy[i].y - 1;
923
+ // eslint-disable-next-line no-await-in-loop
924
+ const depthPickingInfo = await this._getDepthPickingInfoAsync(xy[i].x, pointY, rttSizeW, rttSizeH, view, projection, cameraPosition, viewport);
925
+ pickedPoints[i] = depthPickingInfo?.pickedPoint ?? null;
926
+ normals[i] = depthPickingInfo?.normal ?? null;
927
+ }
928
+ }
929
+ else {
930
+ const depthPixels = await this._readDepthTexturePixelsAsync(readX, readY, w, h);
931
+ if (depthPixels) {
932
+ for (let i = 0; i < xy.length; i++) {
933
+ if (!pickedMeshes[i]) {
934
+ continue;
935
+ }
936
+ const pointY = rttSizeH - xy[i].y - 1;
937
+ const depthPickingInfo = this._getDepthPickingInfoFromBuffer(depthPixels, xy[i].x, pointY, readX, readY, w, h, rttSizeH, view, projection, cameraPosition, viewport);
938
+ pickedPoints[i] = depthPickingInfo?.pickedPoint ?? null;
939
+ normals[i] = depthPickingInfo?.normal ?? null;
940
+ }
941
+ }
942
+ }
943
+ }
944
+ }
945
+ if (disposeWhenDone) {
946
+ this.dispose();
947
+ }
948
+ this._pickingInProgress = false;
949
+ resolve({ meshes: pickedMeshes, thinInstanceIndexes: thinInstanceIndexes, pickedPoints, normals });
631
950
  }
632
- if (disposeWhenDone) {
633
- this.dispose();
951
+ catch (error) {
952
+ this._removePickingTextureFromRenderTargets();
953
+ this._pickingInProgress = false;
954
+ reject(error instanceof Error ? error : new Error(`${error}`));
634
955
  }
635
- this._pickingInProgress = false;
636
- resolve({ meshes: pickedMeshes, thinInstanceIndexes: thinInstanceIndexes });
637
956
  }
638
957
  };
639
958
  });
640
959
  }
641
960
  // pick box area
642
- async _executeBoxPickingAsync(x, y, w, h, disposeWhenDone) {
961
+ async _executeBoxPickingAsync(scanX, scanMaxY, scanW, scanH, readX, readY, rttSizeW, rttSizeH, readW, readH, disposeWhenDone) {
643
962
  return await new Promise((resolve, reject) => {
644
963
  if (!this._pickingTexture) {
645
964
  this._pickingInProgress = false;
@@ -649,42 +968,83 @@ export class GPUPicker {
649
968
  // eslint-disable-next-line @typescript-eslint/no-misused-promises
650
969
  this._pickingTexture.onAfterRender = async () => {
651
970
  if (this._checkRenderStatus()) {
652
- this._pickingTexture.onAfterRender = null;
653
- const pickedMeshes = [];
654
- const thinInstanceIndexes = [];
655
- if (await this._readTexturePixelsAsync(x, y, w, h)) {
656
- for (let offsetY = 0; offsetY < h; ++offsetY) {
657
- for (let offsetX = 0; offsetX < w; ++offsetX) {
658
- const colorId = this._getColorIdFromReadBuffer((offsetY * w + offsetX) * 4);
659
- if (colorId > 0) {
660
- // Thin?
661
- if (this._thinIdMap[colorId]) {
662
- const pickedMesh = this._pickableMeshes[this._thinIdMap[colorId].meshId];
663
- const thinInstanceIndex = this._thinIdMap[colorId].thinId;
664
- pickedMeshes.push(pickedMesh);
665
- thinInstanceIndexes.push(thinInstanceIndex);
971
+ try {
972
+ this._pickingTexture.onAfterRender = null;
973
+ const pickedMeshes = [];
974
+ const thinInstanceIndexes = [];
975
+ const pickedPixelCoordinates = [];
976
+ const scanBufferX = scanX - readX;
977
+ const scanBufferY = rttSizeH - scanMaxY - 1 - readY;
978
+ if (await this._readTexturePixelsAsync(readX, readY, readW, readH)) {
979
+ for (let offsetY = 0; offsetY < scanH; ++offsetY) {
980
+ for (let offsetX = 0; offsetX < scanW; ++offsetX) {
981
+ const bufferX = scanBufferX + offsetX;
982
+ const bufferY = scanBufferY + offsetY;
983
+ if (bufferX < 0 || bufferY < 0 || bufferX >= readW || bufferY >= readH) {
984
+ continue;
666
985
  }
667
- else {
668
- const pickedMesh = this._pickableMeshes[this._idMap[colorId]];
669
- pickedMeshes.push(pickedMesh);
670
- thinInstanceIndexes.push(0);
986
+ const colorId = this._getColorIdFromReadBuffer(this._getReadBufferOffset(bufferX, bufferY, readW, readH));
987
+ if (colorId > 0) {
988
+ // Thin?
989
+ if (this._thinIdMap[colorId]) {
990
+ const pickedMesh = this._pickableMeshes[this._thinIdMap[colorId].meshId];
991
+ const thinInstanceIndex = this._thinIdMap[colorId].thinId;
992
+ pickedMeshes.push(pickedMesh);
993
+ thinInstanceIndexes.push(thinInstanceIndex);
994
+ pickedPixelCoordinates.push({ x: scanX + offsetX, y: scanMaxY - offsetY });
995
+ }
996
+ else {
997
+ const pickedMesh = this._pickableMeshes[this._idMap[colorId]];
998
+ pickedMeshes.push(pickedMesh);
999
+ thinInstanceIndexes.push(0);
1000
+ pickedPixelCoordinates.push({ x: scanX + offsetX, y: scanMaxY - offsetY });
1001
+ }
671
1002
  }
672
1003
  }
673
1004
  }
674
1005
  }
1006
+ let pickedPoints;
1007
+ let normals;
1008
+ if (this._useDepthPicking) {
1009
+ pickedPoints = new Array(pickedMeshes.length).fill(null);
1010
+ normals = new Array(pickedMeshes.length).fill(null);
1011
+ }
1012
+ if (pickedPoints && normals && pickedMeshes.length > 0) {
1013
+ const camera = this._cachedScene.activeCamera;
1014
+ const depthPixels = await this._readDepthTexturePixelsAsync(readX, readY, readW, readH);
1015
+ if (camera && depthPixels) {
1016
+ const viewport = camera.viewport.toGlobal(rttSizeW, rttSizeH);
1017
+ const view = this._cachedScene.getViewMatrix().clone();
1018
+ const projection = this._cachedScene.getProjectionMatrix().clone();
1019
+ const cameraPosition = camera.globalPosition.clone();
1020
+ for (let i = 0; i < pickedPixelCoordinates.length; i++) {
1021
+ const point = pickedPixelCoordinates[i];
1022
+ const pointY = rttSizeH - point.y - 1;
1023
+ const depthPickingInfo = this._getDepthPickingInfoFromBuffer(depthPixels, point.x, pointY, readX, readY, readW, readH, rttSizeH, view, projection, cameraPosition, viewport);
1024
+ pickedPoints[i] = depthPickingInfo?.pickedPoint ?? null;
1025
+ normals[i] = depthPickingInfo?.normal ?? null;
1026
+ }
1027
+ }
1028
+ }
1029
+ if (disposeWhenDone) {
1030
+ this.dispose();
1031
+ }
1032
+ this._pickingInProgress = false;
1033
+ resolve({ meshes: pickedMeshes, thinInstanceIndexes: thinInstanceIndexes, pickedPoints, normals });
675
1034
  }
676
- if (disposeWhenDone) {
677
- this.dispose();
1035
+ catch (error) {
1036
+ this._removePickingTextureFromRenderTargets();
1037
+ this._pickingInProgress = false;
1038
+ reject(error instanceof Error ? error : new Error(`${error}`));
678
1039
  }
679
- this._pickingInProgress = false;
680
- resolve({ meshes: pickedMeshes, thinInstanceIndexes: thinInstanceIndexes });
681
1040
  }
682
1041
  };
683
1042
  });
684
1043
  }
685
1044
  _enableScissor(x, y, w = 1, h = 1) {
686
1045
  if (this._engine.enableScissor) {
687
- this._engine.enableScissor(x, y, w, h);
1046
+ const scissorY = this._engine?.isWebGPU && this._pickingTexture ? this._pickingTexture.getSize().height - y - h : y;
1047
+ this._engine.enableScissor(x, scissorY, w, h);
688
1048
  }
689
1049
  }
690
1050
  _disableScissor() {
@@ -725,25 +1085,35 @@ export class GPUPicker {
725
1085
  if (!renderList || renderList.length === 0) {
726
1086
  return;
727
1087
  }
1088
+ const engine = this._cachedScene.getEngine();
728
1089
  // Cap the number of polling attempts to avoid hanging forever if a shader fails to compile.
729
1090
  const maxAttempts = 200;
730
1091
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
731
1092
  let allReady = true;
732
- // Iterate every mesh (do not break early): each isReady call kicks off compilation
733
- // of that mesh's effect variant if not yet started, so visiting them all on the first
734
- // attempt lets the compiles run in parallel (KHR_parallel_shader_compile) instead of
735
- // being serialized one variant per render frame.
736
- for (let i = 0; i < renderList.length; i++) {
737
- const mesh = renderList[i];
738
- const material = this._meshMaterialMap.get(mesh);
739
- // Match the canonical "uses instanced shader variant" check used elsewhere in this file
740
- // (see addPickingList) InstancedMesh entries report isAnInstance=true while
741
- // hasInstances=false, so omitting isAnInstance would validate the wrong shader variant.
742
- const useInstances = mesh.hasInstances || mesh.isAnInstance || mesh.hasThinInstances;
743
- if (material && !material.isReady(mesh, useInstances)) {
744
- allReady = false;
1093
+ // Compile picking effects under the picking texture's render pass id so per-pass effect caches
1094
+ // (e.g. GaussianSplattingMaterial) don't stamp them onto the main pass draw wrapper.
1095
+ const previousRenderPassId = engine.currentRenderPassId;
1096
+ engine.currentRenderPassId = this._pickingTexture.renderPassId;
1097
+ try {
1098
+ // Iterate every mesh (do not break early): each isReady call kicks off compilation
1099
+ // of that mesh's effect variant if not yet started, so visiting them all on the first
1100
+ // attempt lets the compiles run in parallel (KHR_parallel_shader_compile) instead of
1101
+ // being serialized one variant per render frame.
1102
+ for (let i = 0; i < renderList.length; i++) {
1103
+ const mesh = renderList[i];
1104
+ const material = this._meshMaterialMap.get(mesh);
1105
+ // Match the canonical "uses instanced shader variant" check used elsewhere in this file
1106
+ // (see addPickingList) — InstancedMesh entries report isAnInstance=true while
1107
+ // hasInstances=false, so omitting isAnInstance would validate the wrong shader variant.
1108
+ const useInstances = mesh.hasInstances || mesh.isAnInstance || mesh.hasThinInstances;
1109
+ if (material && !material.isReady(mesh, useInstances)) {
1110
+ allReady = false;
1111
+ }
745
1112
  }
746
1113
  }
1114
+ finally {
1115
+ engine.currentRenderPassId = previousRenderPassId;
1116
+ }
747
1117
  if (allReady) {
748
1118
  return;
749
1119
  }
@@ -757,12 +1127,28 @@ export class GPUPicker {
757
1127
  }
758
1128
  Logger.Warn(`GPUPicker: gave up waiting for picking materials to compile after ${maxAttempts} attempts; picking results may be incorrect.`);
759
1129
  }
760
- _getMeshFromMultiplePoints(x, y, minX, maxY, w) {
761
- let offsetX = (x - minX - 1) * 4;
762
- let offsetY = (maxY - y - 1) * w * 4;
763
- offsetX = Math.max(offsetX, 0);
764
- offsetY = Math.max(offsetY, 0);
765
- const colorId = this._getColorIdFromReadBuffer(offsetX + offsetY);
1130
+ _getMeshFromMultiplePoints(x, y, readX, readY, renderHeight, readWidth, readHeight) {
1131
+ const bufferX = x - readX;
1132
+ const bufferY = renderHeight - y - 1 - readY;
1133
+ let pickedMesh = null;
1134
+ let thinInstanceIndex;
1135
+ if (bufferX < 0 || bufferY < 0 || bufferX >= readWidth || bufferY >= readHeight) {
1136
+ return { pickedMesh, thinInstanceIndex };
1137
+ }
1138
+ const colorId = this._getColorIdFromReadBuffer(this._getReadBufferOffset(bufferX, bufferY, readWidth, readHeight));
1139
+ if (colorId > 0) {
1140
+ if (this._thinIdMap[colorId]) {
1141
+ pickedMesh = this._pickableMeshes[this._thinIdMap[colorId].meshId];
1142
+ thinInstanceIndex = this._thinIdMap[colorId].thinId;
1143
+ }
1144
+ else {
1145
+ pickedMesh = this._pickableMeshes[this._idMap[colorId]];
1146
+ }
1147
+ }
1148
+ return { pickedMesh, thinInstanceIndex };
1149
+ }
1150
+ _getMeshFromReadBuffer(offset) {
1151
+ const colorId = this._getColorIdFromReadBuffer(offset);
766
1152
  let pickedMesh = null;
767
1153
  let thinInstanceIndex;
768
1154
  if (colorId > 0) {
@@ -810,17 +1196,191 @@ export class GPUPicker {
810
1196
  gsPickingMaterial.needAlphaBlending = () => false;
811
1197
  gsPickingMaterial.backFaceCulling = false;
812
1198
  // Attach the GPU picking plugin
813
- new GaussianSplattingGpuPickingMaterialPlugin(gsPickingMaterial);
1199
+ const plugin = new GaussianSplattingGpuPickingMaterialPlugin(gsPickingMaterial);
1200
+ plugin.enableDepthPicking = this._useDepthPicking;
1201
+ plugin.enablePackedDepthPicking = this._isDepthTexturePacked;
814
1202
  return gsPickingMaterial;
815
1203
  }
816
1204
  async _readTexturePixelsAsync(x, y, w = 1, h = 1) {
817
- if (!this._cachedScene || !this._pickingTexture?._texture) {
1205
+ if (!this._cachedScene || !this._pickingTexture) {
1206
+ return false;
1207
+ }
1208
+ const engine = this._cachedScene.getEngine();
1209
+ const texture = this._isUsingDepthPickingRenderTarget ? this._pickingTexture.textures[0]._texture : this._pickingTexture._texture;
1210
+ if (!texture) {
818
1211
  return false;
819
1212
  }
1213
+ const readY = engine.isWebGPU ? texture.height - y - h : y;
1214
+ await engine._readTexturePixels(texture, w, h, -1, 0, this._readbuffer, true, true, x, readY);
1215
+ return true;
1216
+ }
1217
+ async _readDepthTexturePixelsAsync(x, y, w, h) {
1218
+ if (!this._cachedScene || !this._isUsingDepthPickingRenderTarget || !(this._pickingTexture instanceof MultiRenderTarget)) {
1219
+ return null;
1220
+ }
1221
+ const texture = this._pickingTexture.textures[1]._texture;
1222
+ if (!texture) {
1223
+ return null;
1224
+ }
820
1225
  const engine = this._cachedScene.getEngine();
821
- await engine._readTexturePixels(this._pickingTexture._texture, w, h, -1, 0, this._readbuffer, true, true, x, y);
1226
+ const requiredByteLength = engine.isWebGPU ? (4 * 4 * w * h + 255) & ~255 : 4 * 4 * w * h;
1227
+ if (!this._depthReadbuffer || this._depthReadbuffer.byteLength < requiredByteLength) {
1228
+ this._depthReadbuffer = this._depthTextureType === 0 ? new Uint8Array(requiredByteLength) : new Float32Array(requiredByteLength / 4);
1229
+ }
1230
+ const readY = engine.isWebGPU ? texture.height - y - h : y;
1231
+ return await engine._readTexturePixels(texture, w, h, -1, 0, this._depthReadbuffer, true, false, x, readY);
1232
+ }
1233
+ async _getDepthPickingInfoAsync(x, y, renderWidth, renderHeight, view, projection, cameraPosition, viewport) {
1234
+ const radius = GPUPicker._DepthPixelRadius;
1235
+ const left = Math.max(x - radius, 0);
1236
+ const bottom = Math.max(y - radius, 0);
1237
+ const right = Math.min(x + radius, renderWidth - 1);
1238
+ const top = Math.min(y + radius, renderHeight - 1);
1239
+ const width = right - left + 1;
1240
+ const height = top - bottom + 1;
1241
+ const pixels = await this._readDepthTexturePixelsAsync(left, bottom, width, height);
1242
+ if (!pixels) {
1243
+ return null;
1244
+ }
1245
+ return this._getDepthPickingInfoFromBuffer(pixels, x, y, left, bottom, width, height, renderHeight, view, projection, cameraPosition, viewport);
1246
+ }
1247
+ _getDepthPickingInfoFromBuffer(pixels, x, y, bufferLeft, bufferBottom, bufferWidth, bufferHeight, renderHeight, view, projection, cameraPosition, viewport) {
1248
+ let centerBufferX = x - bufferLeft;
1249
+ let centerBufferY = y - bufferBottom;
1250
+ let centerDepth = this._getDepthFromBuffer(pixels, centerBufferX, centerBufferY, bufferWidth, bufferHeight);
1251
+ if (centerDepth === null) {
1252
+ let closestDistanceSquared = Infinity;
1253
+ for (let yy = 0; yy < bufferHeight; yy++) {
1254
+ for (let xx = 0; xx < bufferWidth; xx++) {
1255
+ const depth = this._getDepthFromBuffer(pixels, xx, yy, bufferWidth, bufferHeight);
1256
+ if (depth === null) {
1257
+ continue;
1258
+ }
1259
+ const dx = xx - centerBufferX;
1260
+ const dy = yy - centerBufferY;
1261
+ const distanceSquared = dx * dx + dy * dy;
1262
+ if (distanceSquared < closestDistanceSquared) {
1263
+ closestDistanceSquared = distanceSquared;
1264
+ centerBufferX = xx;
1265
+ centerBufferY = yy;
1266
+ centerDepth = depth;
1267
+ }
1268
+ }
1269
+ }
1270
+ }
1271
+ if (centerDepth === null) {
1272
+ return null;
1273
+ }
1274
+ x = bufferLeft + centerBufferX;
1275
+ y = bufferBottom + centerBufferY;
1276
+ const viewportTop = renderHeight - viewport.y - viewport.height;
1277
+ const centerViewportX = x - viewport.x;
1278
+ const centerViewportY = renderHeight - y - 1 - viewportTop;
1279
+ if (centerViewportX < 0 || centerViewportY < 0 || centerViewportX >= viewport.width || centerViewportY >= viewport.height) {
1280
+ return null;
1281
+ }
1282
+ const pickedPoint = Vector3.UnprojectFloatsToRef(centerViewportX, centerViewportY, centerDepth, viewport.width, viewport.height, Matrix.IdentityReadOnly, view, projection, new Vector3());
1283
+ const fallbackNormal = () => cameraPosition.subtractToRef(pickedPoint, new Vector3()).normalize();
1284
+ let bestOffsetA = null;
1285
+ let bestOffsetB = null;
1286
+ let bestDepthDelta = Infinity;
1287
+ const offsets = GPUPicker._DepthNeighborOffsets;
1288
+ const depthNeighbors = [];
1289
+ const epsilonSquared = Epsilon * Epsilon;
1290
+ for (let i = 0; i < offsets.length; i++) {
1291
+ const offset = offsets[i];
1292
+ const depth = this._getDepthFromBuffer(pixels, centerBufferX + offset[0], centerBufferY + offset[1], bufferWidth, bufferHeight);
1293
+ if (depth === null ||
1294
+ !this._getDepthPointFromBufferToRef(pixels, x, y, offset[0], offset[1], bufferLeft, bufferBottom, bufferWidth, bufferHeight, renderHeight, view, projection, viewport, TmpVectors.Vector3[3])) {
1295
+ continue;
1296
+ }
1297
+ depthNeighbors.push({ offset, depth, depthDelta: Math.abs(centerDepth - depth) });
1298
+ }
1299
+ depthNeighbors.sort((a, b) => a.depthDelta - b.depthDelta);
1300
+ for (let i = 0; i < depthNeighbors.length; i++) {
1301
+ const neighborA = depthNeighbors[i];
1302
+ this._getDepthPointFromBufferToRef(pixels, x, y, neighborA.offset[0], neighborA.offset[1], bufferLeft, bufferBottom, bufferWidth, bufferHeight, renderHeight, view, projection, viewport, TmpVectors.Vector3[3]);
1303
+ for (let j = i + 1; j < depthNeighbors.length; j++) {
1304
+ const neighborB = depthNeighbors[j];
1305
+ this._getDepthPointFromBufferToRef(pixels, x, y, neighborB.offset[0], neighborB.offset[1], bufferLeft, bufferBottom, bufferWidth, bufferHeight, renderHeight, view, projection, viewport, TmpVectors.Vector3[4]);
1306
+ const toA = TmpVectors.Vector3[3].subtractToRef(pickedPoint, TmpVectors.Vector3[0]);
1307
+ const toB = TmpVectors.Vector3[4].subtractToRef(pickedPoint, TmpVectors.Vector3[1]);
1308
+ if (toA.lengthSquared() < epsilonSquared || toB.lengthSquared() < epsilonSquared) {
1309
+ continue;
1310
+ }
1311
+ Vector3.CrossToRef(toB, toA, TmpVectors.Vector3[5]);
1312
+ if (TmpVectors.Vector3[5].lengthSquared() < epsilonSquared) {
1313
+ continue;
1314
+ }
1315
+ const depthDelta = neighborA.depthDelta + neighborB.depthDelta + Math.abs(neighborA.depth - neighborB.depth);
1316
+ if (depthDelta < bestDepthDelta) {
1317
+ bestDepthDelta = depthDelta;
1318
+ bestOffsetA = neighborA.offset;
1319
+ bestOffsetB = neighborB.offset;
1320
+ }
1321
+ }
1322
+ }
1323
+ if (!bestOffsetA || !bestOffsetB) {
1324
+ return { pickedPoint, normal: fallbackNormal() };
1325
+ }
1326
+ this._getDepthPointFromBufferToRef(pixels, x, y, bestOffsetA[0], bestOffsetA[1], bufferLeft, bufferBottom, bufferWidth, bufferHeight, renderHeight, view, projection, viewport, TmpVectors.Vector3[3]);
1327
+ this._getDepthPointFromBufferToRef(pixels, x, y, bestOffsetB[0], bestOffsetB[1], bufferLeft, bufferBottom, bufferWidth, bufferHeight, renderHeight, view, projection, viewport, TmpVectors.Vector3[4]);
1328
+ const toA = TmpVectors.Vector3[3].subtractToRef(pickedPoint, TmpVectors.Vector3[0]);
1329
+ const toB = TmpVectors.Vector3[4].subtractToRef(pickedPoint, TmpVectors.Vector3[1]);
1330
+ if (toA.lengthSquared() < epsilonSquared || toB.lengthSquared() < epsilonSquared) {
1331
+ return { pickedPoint, normal: fallbackNormal() };
1332
+ }
1333
+ toA.normalize();
1334
+ toB.normalize();
1335
+ const normal = Vector3.CrossToRef(toB, toA, new Vector3());
1336
+ if (normal.lengthSquared() < epsilonSquared) {
1337
+ return { pickedPoint, normal: fallbackNormal() };
1338
+ }
1339
+ normal.normalize();
1340
+ const cameraDirection = pickedPoint.subtractToRef(cameraPosition, TmpVectors.Vector3[2]).normalize();
1341
+ if (Vector3.Dot(normal, cameraDirection) > 0) {
1342
+ normal.negateInPlace();
1343
+ }
1344
+ return { pickedPoint, normal };
1345
+ }
1346
+ _getDepthPointFromBufferToRef(pixels, x, y, offsetX, offsetY, left, bottom, bufferWidth, bufferHeight, renderHeight, view, projection, viewport, result) {
1347
+ const bufferX = x + offsetX - left;
1348
+ const bufferY = y + offsetY - bottom;
1349
+ const depth = this._getDepthFromBuffer(pixels, bufferX, bufferY, bufferWidth, bufferHeight);
1350
+ if (depth === null) {
1351
+ return false;
1352
+ }
1353
+ const viewportTop = renderHeight - viewport.y - viewport.height;
1354
+ const viewportX = x + offsetX - viewport.x;
1355
+ const viewportY = renderHeight - (y + offsetY) - 1 - viewportTop;
1356
+ if (viewportX < 0 || viewportY < 0 || viewportX >= viewport.width || viewportY >= viewport.height) {
1357
+ return false;
1358
+ }
1359
+ Vector3.UnprojectFloatsToRef(viewportX, viewportY, depth, viewport.width, viewport.height, Matrix.IdentityReadOnly, view, projection, result);
822
1360
  return true;
823
1361
  }
1362
+ _getDepthFromBuffer(pixels, x, y, width, height) {
1363
+ if (x < 0 || y < 0 || x >= width || y >= height) {
1364
+ return null;
1365
+ }
1366
+ const index = this._getReadBufferOffset(x, y, width, height);
1367
+ let depth;
1368
+ if (pixels instanceof Uint8Array || pixels instanceof Uint8ClampedArray) {
1369
+ if (this._isDepthTexturePacked) {
1370
+ depth = pixels[index] / (255 * 255 * 255 * 255) + pixels[index + 1] / (255 * 255 * 255) + pixels[index + 2] / (255 * 255) + pixels[index + 3] / 255;
1371
+ }
1372
+ else {
1373
+ depth = pixels[index] / 255;
1374
+ }
1375
+ }
1376
+ else {
1377
+ depth = pixels[index];
1378
+ }
1379
+ if (!Number.isFinite(depth) || depth <= 0 || depth >= 1) {
1380
+ return null;
1381
+ }
1382
+ return depth;
1383
+ }
824
1384
  /** Release the resources */
825
1385
  dispose() {
826
1386
  this.setPickingList(null);
@@ -831,8 +1391,25 @@ export class GPUPicker {
831
1391
  this._clearPickingMaterials();
832
1392
  this._sceneBeforeRenderObserver?.remove();
833
1393
  this._sceneBeforeRenderObserver = null;
1394
+ this._pickingTextureClearObserver?.remove();
1395
+ this._pickingTextureClearObserver = null;
1396
+ this._pickingTextureAfterRenderObserver?.remove();
1397
+ this._pickingTextureAfterRenderObserver = null;
834
1398
  }
835
1399
  }
836
1400
  GPUPicker._AttributeName = "instanceMeshID";
837
1401
  GPUPicker._MaxPickingId = 0x00ffffff; // 24 bits unsigned integer max
1402
+ GPUPicker._DepthPixelRadius = 1;
1403
+ GPUPicker._MaxMultiPickIndividualReadbackCount = 32;
1404
+ GPUPicker._MultiPickIndividualReadbackAreaRatio = 16;
1405
+ GPUPicker._DepthNeighborOffsets = [
1406
+ [-1, 1],
1407
+ [0, 1],
1408
+ [1, 1],
1409
+ [1, 0],
1410
+ [1, -1],
1411
+ [0, -1],
1412
+ [-1, -1],
1413
+ [-1, 0],
1414
+ ];
838
1415
  //# sourceMappingURL=gpuPicker.js.map