UNPKG

@babylonjs/core

Version:

Getting started? Play directly with the Babylon.js API using our [playground](https://playground.babylonjs.com/). It also contains a lot of samples to learn how to use it.

787 lines (786 loc) 36.7 kB
import { RenderTargetTexture } from "../Materials/Textures/renderTargetTexture.js"; import { ShaderMaterial } from "../Materials/shaderMaterial.js"; import { GaussianSplattingMaterial } from "../Materials/GaussianSplatting/gaussianSplattingMaterial.js"; import { GaussianSplattingGpuPickingMaterialPlugin } from "../Materials/GaussianSplatting/gaussianSplattingGpuPickingMaterialPlugin.js"; import { Color4 } from "../Maths/math.color.js"; import { VertexBuffer } from "../Meshes/buffer.js"; import { Logger } from "../Misc/logger.js"; /** * Class used to perform a picking operation using GPU * GPUPIcker can pick meshes, instances and thin instances */ export class GPUPicker { constructor() { this._pickingTexture = null; this._idMap = []; this._thinIdMap = []; this._meshUniqueIdToPickerId = []; this._idWarningIssued = false; this._cachedScene = null; this._engine = null; this._pickingMaterialCache = new Array(9).fill(null); this._pickableMeshes = []; this._meshMaterialMap = new Map(); this._readbuffer = null; this._meshRenderingCount = 0; this._renderWarningIssued = false; this._renderPickingTexture = false; this._sceneBeforeRenderObserver = null; this._pickingTextureAfterRenderObserver = null; this._nextFreeId = 1; this._gsPickingMaterials = []; this._gsCompoundRenderMeshes = []; /** Shader language used by the generator */ this._shaderLanguage = 0 /* ShaderLanguage.GLSL */; this._pickingInProgress = false; } /** * Gets the shader language used in this generator. */ get shaderLanguage() { return this._shaderLanguage; } /** * Gets a boolean indicating if the picking is in progress */ get pickingInProgress() { return this._pickingInProgress; } /** * Gets the default render materials used by the picker. * * index is Material filling mode */ get defaultRenderMaterials() { return this._pickingMaterialCache; } _getColorIdFromReadBuffer(offset) { const r = this._readbuffer[offset]; const g = this._readbuffer[offset + 1]; const b = this._readbuffer[offset + 2]; return (r << 16) + (g << 8) + b; } _createRenderTarget(scene, width, height) { if (this._cachedScene && this._pickingTexture) { const index = this._cachedScene.customRenderTargets.indexOf(this._pickingTexture); if (index > -1) { this._cachedScene.customRenderTargets.splice(index, 1); this._renderPickingTexture = false; } } if (this._pickingTexture) { this._pickingTexture.dispose(); } this._pickingTexture = new RenderTargetTexture("pickingTexure", { width: width, height: height }, scene, { generateMipMaps: false, type: 0, samplingMode: 1, }); } _clearPickingMaterials() { for (let i = 0; i < this._pickingMaterialCache.length; i++) { const material = this._pickingMaterialCache[i]; if (material !== null) { material.dispose(); this._pickingMaterialCache[i] = null; } } } _getPickingMaterial(scene, fillMode) { if (fillMode < 0 || 8 < fillMode) { fillMode = 0; } const cachedMaterial = this._pickingMaterialCache[fillMode]; if (cachedMaterial) { return cachedMaterial; } const engine = scene.getEngine(); if (engine.isWebGPU) { this._shaderLanguage = 1 /* ShaderLanguage.WGSL */; } const defines = []; const options = { attributes: [VertexBuffer.PositionKind, GPUPicker._AttributeName], uniforms: ["world", "viewProjection", "meshID"], needAlphaBlending: false, defines: defines, useClipPlane: null, shaderLanguage: this._shaderLanguage, extraInitializationsAsync: async () => { if (this.shaderLanguage === 1 /* ShaderLanguage.WGSL */) { await Promise.all([import("../ShadersWGSL/picking.fragment.js"), import("../ShadersWGSL/picking.vertex.js")]); } else { await Promise.all([import("../Shaders/picking.fragment.js"), import("../Shaders/picking.vertex.js")]); } }, }; const newMaterial = new ShaderMaterial("pickingShader", scene, "picking", options, false); newMaterial.fillMode = fillMode; newMaterial.onBindObservable.add(this._materialBindCallback, undefined, undefined, this); this._pickingMaterialCache[fillMode] = newMaterial; return newMaterial; } _materialBindCallback(mesh) { if (!mesh) { return; } const material = this._meshMaterialMap.get(mesh); if (!material) { if (!this._renderWarningIssued) { this._renderWarningIssued = true; Logger.Warn("GPUPicker issue: Mesh not found in the material map. This may happen when the root mesh of an instance is not in the picking list."); } return; } const effect = material.getEffect(); if (!effect) { return; } if (!mesh.hasInstances && !mesh.isAnInstance && !mesh.hasThinInstances && this._meshUniqueIdToPickerId[mesh.uniqueId] !== undefined) { effect.setFloat("meshID", this._meshUniqueIdToPickerId[mesh.uniqueId]); } this._meshRenderingCount++; } /** * Set the list of meshes to pick from * Set that value to null to clear the list (and avoid leaks) * The module will read and delete from the array provided by reference. Disposing the module or setting the value to null will clear the array. * @param list defines the list of meshes to pick from */ setPickingList(list) { this.clearPickingList(); if (!list || list.length === 0) { return; } // Prepare target const scene = ("mesh" in list[0] ? list[0].mesh : list[0]).getScene(); if (!this._cachedScene || this._cachedScene !== scene) { this._clearPickingMaterials(); } this.addPickingList(list); } /** * Clear the current picking list and free resources */ clearPickingList() { if (this._pickableMeshes) { // Cleanup for (let index = 0; index < this._pickableMeshes.length; index++) { const mesh = this._pickableMeshes[index]; const className = mesh.getClassName(); // Skip GS part proxies - they don't have instance buffers or render list entries if (className === "GaussianSplattingPartProxyMesh") { continue; } // Skip thin instance cleanup for GaussianSplattingMesh (thin instances are for batching, not picking) if (className !== "GaussianSplattingMesh") { if (mesh.hasInstances) { mesh.removeVerticesData(GPUPicker._AttributeName); } if (mesh.hasThinInstances) { mesh.thinInstanceSetBuffer(GPUPicker._AttributeName, null); } } if (this._pickingTexture) { this._pickingTexture.setMaterialForRendering(mesh, undefined); } const material = this._meshMaterialMap.get(mesh); if (material && !this._pickingMaterialCache.includes(material)) { material.onBindObservable.removeCallback(this._materialBindCallback); } } // Clean up GS compound meshes from render list for (const mesh of this._gsCompoundRenderMeshes) { if (this._pickingTexture) { this._pickingTexture.setMaterialForRendering(mesh, undefined); } } this._gsCompoundRenderMeshes.length = 0; // Dispose GS picking materials for (const material of this._gsPickingMaterials) { material.dispose(); } this._gsPickingMaterials.length = 0; this._pickableMeshes.length = 0; this._meshMaterialMap.clear(); this._idMap.length = 0; this._thinIdMap.length = 0; this._meshUniqueIdToPickerId.length = 0; if (this._pickingTexture) { this._pickingTexture.renderList = []; } } this._nextFreeId = 1; } /** * Add array of meshes to the current picking list * @param list defines the array of meshes to add to the current picking list */ addPickingList(list) { if (!list || list.length === 0) { return; } // Prepare target const scene = ("mesh" in list[0] ? list[0].mesh : list[0]).getScene(); const engine = scene.getEngine(); const rttSizeW = engine.getRenderWidth(); const rttSizeH = engine.getRenderHeight(); if (!this._pickingTexture) { this._createRenderTarget(scene, rttSizeW, rttSizeH); } else { const size = this._pickingTexture.getSize(); if (size.width !== rttSizeW || size.height !== rttSizeH || this._cachedScene !== scene) { this._createRenderTarget(scene, rttSizeW, rttSizeH); } } this._sceneBeforeRenderObserver?.remove(); this._sceneBeforeRenderObserver = scene.onBeforeRenderObservable.add(() => { if (scene.frameGraph && this._renderPickingTexture && this._cachedScene && this._pickingTexture) { this._cachedScene._renderRenderTarget(this._pickingTexture, this._cachedScene.cameras?.[0] ?? null); this._cachedScene.activeCamera = null; } }); this._cachedScene = scene; this._engine = scene.getEngine(); if (!this._pickingTexture.renderList) { this._pickingTexture.renderList = []; } const newPickableMeshes = new Array(list.length); const pickableMeshOffset = this._pickableMeshes?.length ?? 0; this._cachedScene = scene; this._engine = scene.getEngine(); for (let i = 0; i < list.length; i++) { const item = list[i]; if ("mesh" in item) { this._meshMaterialMap.set(item.mesh, item.material); newPickableMeshes[i] = item.mesh; } else { const className = item.getClassName(); if (className === "GaussianSplattingMesh" || className === "GaussianSplattingPartProxyMesh") { // GS meshes get special picking materials - handled in the ID assignment loop below newPickableMeshes[i] = item; } else { const material = this._getPickingMaterial(scene, item.material?.fillMode ?? 0); this._meshMaterialMap.set(item, material); newPickableMeshes[i] = item; } } } if (this._pickableMeshes !== null) { this._pickableMeshes = [...this._pickableMeshes, ...newPickableMeshes]; } else { this._pickableMeshes = newPickableMeshes; } // We will affect colors and create vertex color buffers let nextFreeId = this._nextFreeId; // Collect GaussianSplatting part proxy groups for compound picking const gsCompoundGroups = []; for (let index = 0; index < newPickableMeshes.length; index++) { const mesh = newPickableMeshes[index]; const className = mesh.getClassName(); // Handle GaussianSplatting part proxy meshes - collect by compound for processing after the loop if (className === "GaussianSplattingPartProxyMesh") { const proxy = mesh; // GaussianSplattingPartProxyMesh const compound = proxy.compoundSplatMesh; const globalIndex = index + pickableMeshOffset; let group = gsCompoundGroups[compound.uniqueId]; if (!group) { group = { compound, partEntries: [] }; gsCompoundGroups[compound.uniqueId] = group; } group.partEntries.push({ proxy, globalIndex }); continue; // Don't add to render list - the compound mesh will render for all parts } // Handle non-compound GaussianSplatting meshes if (className === "GaussianSplattingMesh") { const globalIndex = index + pickableMeshOffset; const pickId = nextFreeId; this._idMap[pickId] = globalIndex; this._meshUniqueIdToPickerId[mesh.uniqueId] = pickId; nextFreeId++; if (!mesh.isPickable || !mesh.isVisible) { continue; } // Create a GaussianSplattingMaterial with picking plugin for GPU picking const gsPickingMaterial = this._createGaussianSplattingPickingMaterial(scene, mesh); const plugin = gsPickingMaterial.pluginManager.getPlugin("GaussianSplatGpuPicking"); plugin.meshId = pickId; gsPickingMaterial.onBindObservable.add(() => { this._meshRenderingCount++; }); this._gsPickingMaterials.push(gsPickingMaterial); this._meshMaterialMap.set(mesh, gsPickingMaterial); this._pickingTexture.setMaterialForRendering(mesh, gsPickingMaterial); this._pickingTexture.renderList.push(mesh); continue; } // Standard mesh processing const material = this._meshMaterialMap.get(mesh); if (!this._pickingMaterialCache.includes(material)) { material.onBindObservable.add(this._materialBindCallback, undefined, undefined, this); } this._pickingTexture.setMaterialForRendering(mesh, material); this._pickingTexture.renderList.push(mesh); if (mesh.isAnInstance) { continue; // This will be handled by the source mesh } const globalIndex = index + pickableMeshOffset; if (mesh.hasThinInstances) { const thinInstanceCount = mesh.thinInstanceCount; const instanceIdData = new Float32Array(thinInstanceCount); for (let i = 0; i < thinInstanceCount; i++) { instanceIdData[i] = nextFreeId; this._thinIdMap[nextFreeId] = { meshId: globalIndex, thinId: i }; nextFreeId++; } mesh.thinInstanceSetBuffer(GPUPicker._AttributeName, instanceIdData, 1); } else { const currentMeshId = nextFreeId; this._idMap[currentMeshId] = globalIndex; nextFreeId++; if (mesh.hasInstances) { // find index of instances of that mesh const instancesForPick = []; for (let pickableMeshIndex = 0; pickableMeshIndex < newPickableMeshes.length; ++pickableMeshIndex) { const m = newPickableMeshes[pickableMeshIndex]; if (m.isAnInstance && m.sourceMesh === mesh) { instancesForPick.push(pickableMeshIndex); } } const instanceIdData = new Float32Array(instancesForPick.length + 1); // +1 for the source mesh instanceIdData[0] = currentMeshId; for (let i = 0; i < instancesForPick.length; i++) { instanceIdData[i + 1] = nextFreeId; const globalInstanceIndex = instancesForPick[i] + pickableMeshOffset; this._idMap[nextFreeId] = globalInstanceIndex; nextFreeId++; } const engine = mesh.getEngine(); const buffer = new VertexBuffer(engine, instanceIdData, GPUPicker._AttributeName, false, false, 1, true); mesh.setVerticesBuffer(buffer, true); } else { this._meshUniqueIdToPickerId[mesh.uniqueId] = currentMeshId; } } } // Process GaussianSplatting compound groups (part proxy meshes) for (const group of gsCompoundGroups) { if (!group) { continue; } const compound = group.compound; // Assign picking IDs for each part const partMeshIds = new Array(compound.partCount || 1).fill(0); for (const entry of group.partEntries) { const pickId = nextFreeId; this._idMap[pickId] = entry.globalIndex; const partIndex = entry.proxy.partIndex; if (partIndex < partMeshIds.length) { partMeshIds[partIndex] = pickId; } nextFreeId++; } // Create compound GS picking material with plugin const gsPickingMaterial = this._createGaussianSplattingPickingMaterial(scene, compound); const plugin = gsPickingMaterial.pluginManager.getPlugin("GaussianSplatGpuPicking"); plugin.isCompound = true; plugin.partMeshIds = partMeshIds; // Only active (included, visible, and pickable) parts should contribute to the depth buffer. const activeParts = group.partEntries .filter((e) => e.proxy.isPickable && e.proxy.isVisible) .map((e) => e.proxy.partIndex); plugin.setPartActive(activeParts); gsPickingMaterial.onBindObservable.add(() => { this._meshRenderingCount++; }); this._gsPickingMaterials.push(gsPickingMaterial); this._meshMaterialMap.set(compound, gsPickingMaterial); this._pickingTexture.setMaterialForRendering(compound, gsPickingMaterial); this._pickingTexture.renderList.push(compound); this._gsCompoundRenderMeshes.push(compound); } if (GPUPicker._MaxPickingId < nextFreeId - 1) { if (!this._idWarningIssued) { this._idWarningIssued = true; Logger.Warn(`GPUPicker maximum number of pickable meshes and instances is ${GPUPicker._MaxPickingId}. Some meshes or instances won't be pickable.`); } } this._nextFreeId = nextFreeId; } /** * Execute a picking operation * @param x defines the X coordinates where to run the pick * @param y defines the Y coordinates where to run the pick * @param disposeWhenDone defines a boolean indicating we do not want to keep resources alive (false by default) * @returns A promise with the picking results */ async pickAsync(x, y, disposeWhenDone = false) { if (this._pickingInProgress) { return null; } if (!this._pickableMeshes || this._pickableMeshes.length === 0) { return null; } const { rttSizeW, rttSizeH, devicePixelRatio } = this._getRenderInfo(); const { x: adjustedX, y: adjustedY } = this._prepareForPicking(x, y, devicePixelRatio); if (adjustedX < 0 || adjustedY < 0 || adjustedX >= rttSizeW || adjustedY >= rttSizeH) { return null; } this._pickingInProgress = true; // Invert Y const invertedY = rttSizeH - adjustedY - 1; this._preparePickingBuffer(this._engine, rttSizeW, rttSizeH, adjustedX, invertedY); return await this._executePickingAsync(adjustedX, invertedY, disposeWhenDone); } /** * Execute a picking operation on multiple coordinates * @param xy defines the X,Y coordinates where to run the pick * @param disposeWhenDone defines a boolean indicating we do not want to keep resources alive (false by default) * @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. */ async multiPickAsync(xy, disposeWhenDone = false) { if (this._pickingInProgress) { return null; } if (!this._pickableMeshes || this._pickableMeshes.length === 0 || xy.length === 0) { return null; } if (xy.length === 1) { const pi = await this.pickAsync(xy[0].x, xy[0].y, disposeWhenDone); return { meshes: [pi?.mesh ?? null], thinInstanceIndexes: pi?.thinInstanceIndex ? [pi.thinInstanceIndex] : undefined, }; } this._pickingInProgress = true; const processedXY = new Array(xy.length); let minX = Infinity; let maxX = -Infinity; let minY = Infinity; let maxY = -Infinity; const { rttSizeW, rttSizeH, devicePixelRatio } = this._getRenderInfo(); // Process screen coordinates adjust to dpr for (let i = 0; i < xy.length; i++) { const item = xy[i]; const { x, y } = item; const { x: adjustedX, y: adjustedY } = this._prepareForPicking(x, y, devicePixelRatio); processedXY[i] = { ...item, x: adjustedX, y: adjustedY, }; minX = Math.min(minX, adjustedX); maxX = Math.max(maxX, adjustedX); minY = Math.min(minY, adjustedY); maxY = Math.max(maxY, adjustedY); } const w = Math.max(maxX - minX, 1); const h = Math.max(maxY - minY, 1); const partialCutH = rttSizeH - maxY - 1; this._preparePickingBuffer(this._engine, rttSizeW, rttSizeH, minX, partialCutH, w, h); return await this._executeMultiPickingAsync(processedXY, minX, maxY, rttSizeH, w, h, disposeWhenDone); } /** * Execute a picking operation on box defined by two screen coordinates * @param x1 defines the X coordinate of the first corner of the box where to run the pick * @param y1 defines the Y coordinate of the first corner of the box where to run the pick * @param x2 defines the X coordinate of the opposite corner of the box where to run the pick * @param y2 defines the Y coordinate of the opposite corner of the box where to run the pick * @param disposeWhenDone defines a boolean indicating we do not want to keep resources alive (false by default) * @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. */ async boxPickAsync(x1, y1, x2, y2, disposeWhenDone = false) { if (this._pickingInProgress) { return null; } if (!this._pickableMeshes || this._pickableMeshes.length === 0) { return null; } this._pickingInProgress = true; const { rttSizeW, rttSizeH, devicePixelRatio } = this._getRenderInfo(); const { x: adjustedX1, y: adjustedY1 } = this._prepareForPicking(x1, y1, devicePixelRatio); const { x: adjustedX2, y: adjustedY2 } = this._prepareForPicking(x2, y2, devicePixelRatio); const minX = Math.max(Math.min(adjustedX1, adjustedX2), 0); const maxX = Math.min(Math.max(adjustedX1, adjustedX2), rttSizeW - 1); const minY = Math.max(Math.min(adjustedY1, adjustedY2), 0); const maxY = Math.min(Math.max(adjustedY1, adjustedY2), rttSizeH - 1); if (minX >= rttSizeW || minY >= rttSizeH || maxX < 0 || maxY < 0) { this._pickingInProgress = false; return null; } const w = Math.max(maxX - minX, 1); const h = Math.max(maxY - minY, 1); const partialCutH = rttSizeH - maxY - 1; this._preparePickingBuffer(this._engine, rttSizeW, rttSizeH, minX, partialCutH, w, h); return await this._executeBoxPickingAsync(minX, partialCutH, w, h, disposeWhenDone); } _getRenderInfo() { const engine = this._cachedScene.getEngine(); const rttSizeW = engine.getRenderWidth(); const rttSizeH = engine.getRenderHeight(); const devicePixelRatio = 1 / engine._hardwareScalingLevel; return { rttSizeW, rttSizeH, devicePixelRatio, }; } _prepareForPicking(x, y, devicePixelRatio) { return { x: (devicePixelRatio * x) >> 0, y: (devicePixelRatio * y) >> 0 }; } _preparePickingBuffer(engine, rttSizeW, rttSizeH, x, y, w = 1, h = 1) { this._meshRenderingCount = 0; const requiredBufferSize = engine.isWebGPU ? (4 * w * h + 255) & ~255 : 4 * w * h; if (!this._readbuffer || this._readbuffer.length < requiredBufferSize) { this._readbuffer = new Uint8Array(requiredBufferSize); } // Do we need to rebuild the RTT? const size = this._pickingTexture.getSize(); if (size.width !== rttSizeW || size.height !== rttSizeH) { this._createRenderTarget(this._cachedScene, rttSizeW, rttSizeH); this._updateRenderList(); } this._pickingTexture.clearColor = new Color4(0, 0, 0, 0); this._pickingTexture.onBeforeRender = () => { this._enableScissor(x, y, w, h); }; this._pickingTextureAfterRenderObserver?.remove(); this._pickingTextureAfterRenderObserver = this._pickingTexture.onAfterRenderObservable.add(() => { this._disableScissor(); }); this._cachedScene.customRenderTargets.push(this._pickingTexture); this._renderPickingTexture = true; } // pick one pixel async _executePickingAsync(x, y, disposeWhenDone) { return await new Promise((resolve, reject) => { if (!this._pickingTexture) { this._pickingInProgress = false; reject(new Error("Picking texture not created")); return; } // eslint-disable-next-line @typescript-eslint/no-misused-promises this._pickingTexture.onAfterRender = async () => { if (this._checkRenderStatus()) { this._pickingTexture.onAfterRender = null; let pickedMesh = null; let thinInstanceIndex = undefined; // Remove from the active RTTs const index = this._cachedScene.customRenderTargets.indexOf(this._pickingTexture); if (index > -1) { this._cachedScene.customRenderTargets.splice(index, 1); this._renderPickingTexture = false; } // Do the actual picking if (await this._readTexturePixelsAsync(x, y)) { const colorId = this._getColorIdFromReadBuffer(0); // Thin? if (this._thinIdMap[colorId]) { pickedMesh = this._pickableMeshes[this._thinIdMap[colorId].meshId]; thinInstanceIndex = this._thinIdMap[colorId].thinId; } else { pickedMesh = this._pickableMeshes[this._idMap[colorId]]; } } if (disposeWhenDone) { this.dispose(); } this._pickingInProgress = false; if (pickedMesh) { resolve({ mesh: pickedMesh, thinInstanceIndex: thinInstanceIndex }); } else { resolve(null); } } }; }); } // pick multiple pixels async _executeMultiPickingAsync(xy, minX, maxY, rttSizeH, w, h, disposeWhenDone) { return await new Promise((resolve, reject) => { if (!this._pickingTexture) { this._pickingInProgress = false; reject(new Error("Picking texture not created")); return; } // eslint-disable-next-line @typescript-eslint/no-misused-promises this._pickingTexture.onAfterRender = async () => { if (this._checkRenderStatus()) { this._pickingTexture.onAfterRender = null; const pickedMeshes = []; const thinInstanceIndexes = []; if (await this._readTexturePixelsAsync(minX, rttSizeH - maxY - 1, w, h)) { for (let i = 0; i < xy.length; i++) { const { pickedMesh, thinInstanceIndex } = this._getMeshFromMultiplePoints(xy[i].x, xy[i].y, minX, maxY, w); pickedMeshes.push(pickedMesh); thinInstanceIndexes.push(thinInstanceIndex ?? 0); } } if (disposeWhenDone) { this.dispose(); } this._pickingInProgress = false; resolve({ meshes: pickedMeshes, thinInstanceIndexes: thinInstanceIndexes }); } }; }); } // pick box area async _executeBoxPickingAsync(x, y, w, h, disposeWhenDone) { return await new Promise((resolve, reject) => { if (!this._pickingTexture) { this._pickingInProgress = false; reject(new Error("Picking texture not created")); return; } // eslint-disable-next-line @typescript-eslint/no-misused-promises this._pickingTexture.onAfterRender = async () => { if (this._checkRenderStatus()) { this._pickingTexture.onAfterRender = null; const pickedMeshes = []; const thinInstanceIndexes = []; if (await this._readTexturePixelsAsync(x, y, w, h)) { for (let offsetY = 0; offsetY < h; ++offsetY) { for (let offsetX = 0; offsetX < w; ++offsetX) { const colorId = this._getColorIdFromReadBuffer((offsetY * w + offsetX) * 4); if (colorId > 0) { // Thin? if (this._thinIdMap[colorId]) { const pickedMesh = this._pickableMeshes[this._thinIdMap[colorId].meshId]; const thinInstanceIndex = this._thinIdMap[colorId].thinId; pickedMeshes.push(pickedMesh); thinInstanceIndexes.push(thinInstanceIndex); } else { const pickedMesh = this._pickableMeshes[this._idMap[colorId]]; pickedMeshes.push(pickedMesh); thinInstanceIndexes.push(0); } } } } } if (disposeWhenDone) { this.dispose(); } this._pickingInProgress = false; resolve({ meshes: pickedMeshes, thinInstanceIndexes: thinInstanceIndexes }); } }; }); } _enableScissor(x, y, w = 1, h = 1) { if (this._engine.enableScissor) { this._engine.enableScissor(x, y, w, h); } } _disableScissor() { if (this._engine.disableScissor) { this._engine.disableScissor(); } } /** * @returns true if rendering if the picking texture has finished, otherwise false */ _checkRenderStatus() { const wasSuccessful = this._meshRenderingCount > 0; if (wasSuccessful) { // Remove from the active RTTs const index = this._cachedScene.customRenderTargets.indexOf(this._pickingTexture); if (index > -1) { this._cachedScene.customRenderTargets.splice(index, 1); this._renderPickingTexture = false; } return true; } this._meshRenderingCount = 0; return false; // Wait for shaders to be ready } _getMeshFromMultiplePoints(x, y, minX, maxY, w) { let offsetX = (x - minX - 1) * 4; let offsetY = (maxY - y - 1) * w * 4; offsetX = Math.max(offsetX, 0); offsetY = Math.max(offsetY, 0); const colorId = this._getColorIdFromReadBuffer(offsetX + offsetY); let pickedMesh = null; let thinInstanceIndex; if (colorId > 0) { if (this._thinIdMap[colorId]) { pickedMesh = this._pickableMeshes[this._thinIdMap[colorId].meshId]; thinInstanceIndex = this._thinIdMap[colorId].thinId; } else { pickedMesh = this._pickableMeshes[this._idMap[colorId]]; } } return { pickedMesh, thinInstanceIndex }; } /** * Updates the render list with the current pickable meshes. */ _updateRenderList() { this._pickingTexture.renderList = []; for (const mesh of this._pickableMeshes) { const className = mesh.getClassName(); // Part proxies don't render directly - their compound renders for them if (className === "GaussianSplattingPartProxyMesh") { continue; } this._pickingTexture.setMaterialForRendering(mesh, this._meshMaterialMap.get(mesh)); this._pickingTexture.renderList.push(mesh); } // Also add compound GS meshes that render on behalf of part proxies for (const mesh of this._gsCompoundRenderMeshes) { this._pickingTexture.setMaterialForRendering(mesh, this._meshMaterialMap.get(mesh)); this._pickingTexture.renderList.push(mesh); } } /** * Creates a GaussianSplattingMaterial configured for GPU picking by attaching * a GaussianSplattingGpuPickingMaterialPlugin. The plugin injects picking ID * encoding into the existing Gaussian Splatting shaders via material plugin hooks. * @param scene The scene * @param gsMesh The Gaussian Splatting mesh (used to set the source mesh on the material) * @returns A GaussianSplattingMaterial with the picking plugin attached */ _createGaussianSplattingPickingMaterial(scene, gsMesh) { const gsPickingMaterial = new GaussianSplattingMaterial("gaussianSplattingGpuPicking", scene); gsPickingMaterial.setSourceMesh(gsMesh); gsPickingMaterial.needAlphaBlending = () => false; gsPickingMaterial.backFaceCulling = false; // Attach the GPU picking plugin new GaussianSplattingGpuPickingMaterialPlugin(gsPickingMaterial); return gsPickingMaterial; } async _readTexturePixelsAsync(x, y, w = 1, h = 1) { if (!this._cachedScene || !this._pickingTexture?._texture) { return false; } const engine = this._cachedScene.getEngine(); await engine._readTexturePixels(this._pickingTexture._texture, w, h, -1, 0, this._readbuffer, true, true, x, y); return true; } /** Release the resources */ dispose() { this.setPickingList(null); this._cachedScene = null; // Cleaning up this._pickingTexture?.dispose(); this._pickingTexture = null; this._clearPickingMaterials(); this._sceneBeforeRenderObserver?.remove(); this._sceneBeforeRenderObserver = null; } } GPUPicker._AttributeName = "instanceMeshID"; GPUPicker._MaxPickingId = 0x00ffffff; // 24 bits unsigned integer max //# sourceMappingURL=gpuPicker.js.map