Order DOT spheres by Morton index

Add DOT sphere impostors in Morton order so sphere LOD stride sampling remains spatially distributed.
This commit is contained in:
Ludovic Autin
2026-05-02 10:48:31 -07:00
parent c9734d83a2
commit 19fec3bbc1
2 changed files with 88 additions and 25 deletions

View File

@@ -0,0 +1,42 @@
/**
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Ludovic Autin <autin@scripps.edu>
*/
import { CustomProperties } from '../../../mol-model/custom-property';
import { Grid, Volume } from '../../../mol-model/volume';
import { Mat4, Tensor } from '../../../mol-math/linear-algebra';
import { createVolumeSphereImpostor } from '../dot';
function createTestVolume(dimensions: [number, number, number], data: number[]): Volume {
return {
grid: {
transform: { kind: 'matrix', matrix: Mat4.identity() },
cells: Tensor.create(Tensor.Space(dimensions, [2, 1, 0]), Tensor.Data1(data)),
stats: { min: 0, max: 1, mean: 0.5, sigma: 0.5 },
} satisfies Grid,
instances: [{ transform: Mat4.identity() }],
sourceData: { kind: 'test', name: 'test', data: {} } as any,
customProperties: new CustomProperties(),
_propertyData: Object.create(null),
_localPropertyData: Object.create(null),
};
}
describe('volume dot representation', () => {
it('adds sphere impostor dots in Morton order for LOD sampling', () => {
const volume = createTestVolume([2, 2, 2], [
1, 1,
1, 1,
1, 1,
1, 1,
]);
const spheres = createVolumeSphereImpostor(undefined as any, volume, 0, undefined as any, {
isoValue: Volume.IsoValue.absolute(0.5),
perturbPositions: false,
} as any);
expect(Array.from(spheres.groupBuffer.ref.value)).toEqual([0, 4, 2, 6, 1, 5, 3, 7]);
});
});

View File

@@ -29,7 +29,7 @@ import { PointsBuilder } from '../../mol-geo/geometry/points/points-builder';
import { Mat4 } from '../../mol-math/linear-algebra';
import { Interval } from '../../mol-data/int/interval';
import { OrderedSet } from '../../mol-data/int/ordered-set';
import { PCG } from '../../mol-data/util/hash-functions';
import { mortonOrder3d, PCG } from '../../mol-data/util/hash-functions';
import { VolumeKey, VolumeVisual } from './visual';
export const VolumeDotParams = {
@@ -121,21 +121,16 @@ function getRandomOffsetFromBasis({ x, y, z, maxScale }: Basis): Vec3 {
return offset;
}
export function createVolumeSphereImpostor(ctx: VisualContext, volume: Volume, key: number, theme: Theme, props: VolumeSphereProps, spheres?: Spheres): Spheres {
const { cells: { space, data }, stats } = volume.grid;
const gridToCartn = Grid.getGridToCartesianTransform(volume.grid);
const isoVal = Volume.IsoValue.toAbsolute(props.isoValue, stats).absoluteValue;
type OrderedDotCell = {
readonly order: number
readonly offset: number
}
const p = Vec3();
function getOrderedDotCells(volume: Volume, isoVal: number): OrderedDotCell[] {
const { cells: { space, data } } = volume.grid;
const [xn, yn, zn] = space.dimensions;
const count = Math.ceil((xn * yn * zn) / 10);
const builder = SpheresBuilder.create(count, Math.ceil(count / 2), spheres);
const invert = isoVal < 0;
// Precompute basis vectors and largest cell axis length
const basis = props.perturbPositions ? getBasis(gridToCartn) : undefined;
const cells: OrderedDotCell[] = [];
for (let z = 0; z < zn; ++z) {
for (let y = 0; y < yn; ++y) {
@@ -143,21 +138,47 @@ export function createVolumeSphereImpostor(ctx: VisualContext, volume: Volume, k
const value = space.get(data, x, y, z);
if (!invert && value < isoVal || invert && value > isoVal) continue;
const cellIdx = space.dataOffset(x, y, z);
if (basis) {
Vec3.set(p, x, y, z);
Vec3.transformMat4(p, p, gridToCartn);
const offset = getRandomOffsetFromBasis(basis);
Vec3.add(p, p, offset);
} else {
Vec3.set(p, x, y, z);
Vec3.transformMat4(p, p, gridToCartn);
}
builder.add(p[0], p[1], p[2], cellIdx);
cells.push({
order: mortonOrder3d(x, y, z) >>> 0,
offset: space.dataOffset(x, y, z)
});
}
}
}
cells.sort((a, b) => a.order - b.order || a.offset - b.offset);
return cells;
}
export function createVolumeSphereImpostor(ctx: VisualContext, volume: Volume, key: number, theme: Theme, props: VolumeSphereProps, spheres?: Spheres): Spheres {
const { cells: { space }, stats } = volume.grid;
const gridToCartn = Grid.getGridToCartesianTransform(volume.grid);
const isoVal = Volume.IsoValue.toAbsolute(props.isoValue, stats).absoluteValue;
const p = Vec3();
const coords = [0, 0, 0];
const orderedCells = getOrderedDotCells(volume, isoVal);
const count = orderedCells.length;
const builder = SpheresBuilder.create(count, Math.ceil(count / 2), spheres);
// Precompute basis vectors and largest cell axis length
const basis = props.perturbPositions ? getBasis(gridToCartn) : undefined;
for (const cell of orderedCells) {
space.getCoords(cell.offset, coords);
if (basis) {
Vec3.set(p, coords[0], coords[1], coords[2]);
Vec3.transformMat4(p, p, gridToCartn);
const offset = getRandomOffsetFromBasis(basis);
Vec3.add(p, p, offset);
} else {
Vec3.set(p, coords[0], coords[1], coords[2]);
Vec3.transformMat4(p, p, gridToCartn);
}
builder.add(p[0], p[1], p[2], cell.offset);
}
const s = builder.getSpheres();
s.setBoundingSphere(Volume.Isosurface.getBoundingSphere(volume, props.isoValue));
return s;
@@ -346,4 +367,4 @@ export const DotRepresentationProvider = VolumeRepresentationProvider({
defaultSizeTheme: { name: 'uniform' },
locationKinds: ['cell-location', 'position-location'],
isApplicable: (volume: Volume) => !Volume.isEmpty(volume) && !Volume.Segmentation.get(volume)
});
});