@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
JavaScript
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