Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ export function getDefaultObjectTypeConfigs(): ObjectTypeConfig[] {
},
{
typeName: 'CaloCells',
getObject: PhoenixObjects.getCaloCell,
getObject: PhoenixObjects.getCaloCellsInstanced,
concatonateObjs: true,
cuts: [
new Cut('phi', -pi, pi, 0.01),
new Cut('eta', -5.0, 5.0, 0.1),
Expand All @@ -98,7 +99,7 @@ export function getDefaultObjectTypeConfigs(): ObjectTypeConfig[] {
scaleConfig: {
key: 'caloCellsScale',
label: 'CaloCells Scale',
scaleMethod: 'scaleChildObjects',
scaleMethod: 'scaleInstancedObjects',
scaleAxis: 'z',
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
CanvasTexture,
ShaderMaterial,
DoubleSide,
InstancedMesh,
Matrix4,
} from 'three';
import { ConvexGeometry } from 'three/examples/jsm/geometries/ConvexGeometry.js';
import { EVENT_DATA_TYPE_COLORS } from '../../helpers/constants';
Expand Down Expand Up @@ -773,6 +775,102 @@ export class PhoenixObjects {
return cube;
}

/**
* Create all CaloCells as a single InstancedMesh for performance.
* Receives the entire collection array and returns one object with
* per-instance transforms and colors, reducing 187K draw calls to 1.
* @param cellsParams Array of all cell parameters in the collection.
* @returns InstancedMesh containing all cells.
*/
public static getCaloCellsInstanced(cellsParams: any[]): Object3D {
const defaultRadius = 1700;
const defaultZ = 2000;
const defaultSide = 30;
const defaultLength = 30;

const count = cellsParams.length;
const unitBox = new BoxGeometry(1, 1, 1);
const material = new MeshPhongMaterial({
color: cellsParams[0]?.color ?? EVENT_DATA_TYPE_COLORS.CaloClusters,
transparent: true,
opacity: 0.7,
});

const mesh = new InstancedMesh(unitBox, material, count);

// Reusable temporaries to avoid per-iteration allocation
const tempMatrix = new Matrix4();
const tempPosition = new Vector3();
const tempScale = new Vector3();
const tempColor = new Color();
const tempObj = new Object3D();

for (let i = 0; i < count; i++) {
const cell = cellsParams[i];

// Position (reuse existing helper)
const position = PhoenixObjects.getCaloPosition(
cell,
defaultRadius,
defaultZ,
);

// Cell dimensions — scale encodes variable width/length
const cellWidth = cell.side ?? defaultSide;
let cellLength = cell.length ?? defaultLength;
if (cellLength < cellWidth) {
cellLength = cellWidth;
}

// Orientation via lookAt (same 3-branch logic as getCaloCell)
tempObj.position.copy(position);
tempObj.rotation.set(0, 0, 0);
tempObj.updateMatrix();
if (!cell.radius && !cell.z) {
tempObj.lookAt(0, 0, 0);
} else if (cell.z && !cell.radius) {
tempObj.lookAt(position.x, position.y, 0);
}
if (cell.radius) {
tempObj.lookAt(0, 0, position.z);
}

// Compose transform: position + orientation + scale
tempScale.set(cellWidth, cellWidth, cellLength);
tempMatrix.compose(position, tempObj.quaternion, tempScale);
mesh.setMatrixAt(i, tempMatrix);

// Per-instance color
tempColor.set(cell.color ?? EVENT_DATA_TYPE_COLORS.CaloClusters);
mesh.setColorAt(i, tempColor);

// Write back identifiers for filtering and collection lookups
cell._instanceId = i;
cell.uuid = mesh.uuid;
}

mesh.instanceMatrix.needsUpdate = true;
if (mesh.instanceColor) {
mesh.instanceColor.needsUpdate = true;
}

// Compute bounding sphere across all instances for correct frustum culling.
// Without this, Three.js uses the unit-box bounding sphere and cells
// disappear when panning.
mesh.computeBoundingSphere();

mesh.name = 'CaloCell';
mesh.userData = {
_isInstancedCaloCells: true,
_instanceData: cellsParams,
_originalMatrices: null, // lazily populated on first filter
_scaleValue: 1, // current scale factor (for filter↔scale coordination)
_scaleAxis: null as string | null,
};

return mesh;
}

/**
* Get the planar calo cells from parameters.
* @param caloCells Parameters to build planar calo cells.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Group,
Scene,
Mesh,
InstancedMesh,
TubeGeometry,
MathUtils,
} from 'three';
Expand Down Expand Up @@ -704,6 +705,10 @@ export class ControlsManager {
}
}
});
} else if (object instanceof InstancedMesh) {
// InstancedMesh: compute bounding sphere across all instances
object.computeBoundingSphere();
objectPosition = object.boundingSphere?.center ?? new Vector3();
} else if (object.position.equals(origin)) {
// Get the center of bounding sphere of objects with no position
objectPosition = object.geometry?.boundingSphere?.center;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import {
TubeGeometry,
MeshToonMaterial,
Line,
InstancedMesh,
Matrix4,
type Object3DEventMap,
} from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
Expand Down Expand Up @@ -298,6 +300,15 @@ export class SceneManager {
const collection = eventData.getObjectByName(collectionName);
if (collection) {
for (const child of Object.values(collection.children)) {
// InstancedMesh: filter by scaling hidden instances to zero
if (
child.userData?._isInstancedCaloCells &&
child instanceof InstancedMesh
) {
this.filterInstancedMesh(child, filters);
continue;
}

if (child.userData) {
for (const filter of filters) {
const value = child.userData[filter.field];
Expand Down Expand Up @@ -915,6 +926,143 @@ export class SceneManager {
});
}

/**
* Filter an InstancedMesh by setting hidden instances to zero-scale.
* Respects current scale factor so filter and scale don't conflict.
* @param mesh The InstancedMesh to filter.
* @param filters Cuts used to determine visibility of each instance.
*/
private filterInstancedMesh(mesh: InstancedMesh, filters: Cut[]) {
const instanceData: any[] = mesh.userData._instanceData;
if (!instanceData) return;

this.ensureOriginalMatrices(mesh);

const originalMatrices: Float32Array = mesh.userData._originalMatrices;
const scaleValue: number = mesh.userData._scaleValue ?? 1;
const scaleAxis: string | null = mesh.userData._scaleAxis ?? null;
const zeroMatrix = new Matrix4().makeScale(0, 0, 0);
const tempMatrix = new Matrix4();
const pos = new Vector3();
const quat = new Quaternion();
const scl = new Vector3();

for (let i = 0; i < instanceData.length; i++) {
const cell = instanceData[i];
let visible = true;

for (const filter of filters) {
const value = cell[filter.field];
if (value !== undefined && !filter.cutPassed(value)) {
visible = false;
break;
}
}

if (visible) {
tempMatrix.fromArray(originalMatrices, i * 16);
// Re-apply current scale factor so filtering doesn't reset scale
if (scaleValue !== 1) {
tempMatrix.decompose(pos, quat, scl);
this.applyAxisScale(scl, scaleValue, scaleAxis);
tempMatrix.compose(pos, quat, scl);
}
mesh.setMatrixAt(i, tempMatrix);
} else {
mesh.setMatrixAt(i, zeroMatrix);
}
}

mesh.instanceMatrix.needsUpdate = true;
}

/**
* Scale instances in an InstancedMesh along an axis.
* Stores scale state so filtering preserves it. Skips zero-scaled (filtered) instances.
* Falls back to scaleChildObjects for non-instanced groups.
* @param groupName Name of the group containing the InstancedMesh.
* @param value Scale factor (1 = original size).
* @param axis Optional axis to scale along ('x', 'y', 'z').
*/
public scaleInstancedObjects(
groupName: string,
value: number,
axis?: string,
) {
const object = this.scene.getObjectByName(groupName);
if (!object) return;

let handled = false;
object.traverse((child: Object3D) => {
if (
child.userData?._isInstancedCaloCells &&
child instanceof InstancedMesh
) {
handled = true;
const instanceData: any[] = child.userData._instanceData;
if (!instanceData) return;

this.ensureOriginalMatrices(child);

// Store scale state for filter coordination
child.userData._scaleValue = value;
child.userData._scaleAxis = axis ?? null;

const originalMatrices: Float32Array = child.userData._originalMatrices;
const tempMatrix = new Matrix4();
const currentMatrix = new Matrix4();
const pos = new Vector3();
const quat = new Quaternion();
const scl = new Vector3();

for (let i = 0; i < instanceData.length; i++) {
// Skip instances that are currently filtered out (zero-scale)
child.getMatrixAt(i, currentMatrix);
const e = currentMatrix.elements;
if (e[0] === 0 && e[5] === 0 && e[10] === 0) continue;

tempMatrix.fromArray(originalMatrices, i * 16);
tempMatrix.decompose(pos, quat, scl);
this.applyAxisScale(scl, value, axis);
tempMatrix.compose(pos, quat, scl);
child.setMatrixAt(i, tempMatrix);
}

child.instanceMatrix.needsUpdate = true;
}
});

// Fallback for non-instanced objects in the same group
if (!handled) {
this.scaleChildObjects(groupName, value, axis);
}
}

/** Snapshot the original instance matrices on first use. */
private ensureOriginalMatrices(mesh: InstancedMesh) {
if (!mesh.userData._originalMatrices) {
mesh.userData._originalMatrices =
mesh.instanceMatrix.array.slice() as Float32Array;
}
}

/** Apply a scale factor to a Vector3 along a specific axis or uniformly. */
private applyAxisScale(scl: Vector3, value: number, axis?: string | null) {
switch (axis) {
case 'x':
scl.x *= value;
break;
case 'y':
scl.y *= value;
break;
case 'z':
scl.z *= value;
break;
default:
scl.multiplyScalar(value);
}
}

/**
* Add label to the three.js object.
* @param label Label to add to the event object.
Expand Down
Loading
Loading