Selaa lähdekoodia

Merge pull request #466 from molstar/cellpack-tweaks

Cellpack tweaks
Alexander Rose 2 vuotta sitten
vanhempi
commit
4dfbc3830f

+ 6 - 0
CHANGELOG.md

@@ -6,6 +6,12 @@ Note that since we don't clearly distinguish between a public and private interf
 
 ## [Unreleased]
 
+- Add ``instanceGranularity`` option for marker, transparency, clipping, overpaint, substance data to save memory
+- CellPack extension tweaks
+    - Use instancing to create DNA/RNA curves to save memory
+    - Enable ``instanceGranularity`` by default
+    - Add ``adjustStyle`` option to LoadCellPackModel action (stylized, no multi-sample, no far clipping, chain picking)
+
 ## [v3.10.2] - 2022-06-26
 
 - Fix superfluous shader varying

+ 43 - 118
src/extensions/cellpack/model.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Ludovic Autin <ludovic.autin@gmail.com>
@@ -12,7 +12,7 @@ import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { Ingredient, CellPacking, CompartmentPrimitives } from './data';
 import { getFromPdb, getFromCellPackDB, IngredientFiles, parseCif, parsePDBfile, getStructureMean, getFromOPM } from './util';
 import { Model, Structure, StructureSymmetry, StructureSelection, QueryContext, Unit, Trajectory } from '../../mol-model/structure';
-import { trajectoryFromMmCIF, MmcifFormat } from '../../mol-model-formats/structure/mmcif';
+import { trajectoryFromMmCIF } from '../../mol-model-formats/structure/mmcif';
 import { trajectoryFromPDB } from '../../mol-model-formats/structure/pdb';
 import { Mat4, Vec3, Quat } from '../../mol-math/linear-algebra';
 import { SymmetryOperator } from '../../mol-math/geometry';
@@ -22,17 +22,12 @@ import { ParseCellPack, StructureFromCellpack, DefaultCellPackBaseUrl, Structure
 import { MolScriptBuilder as MS } from '../../mol-script/language/builder';
 import { getMatFromResamplePoints } from './curve';
 import { compile } from '../../mol-script/runtime/query/compiler';
-import { CifCategory, CifField } from '../../mol-io/reader/cif';
-import { mmCIF_Schema } from '../../mol-io/reader/cif/schema/mmcif';
-import { Column } from '../../mol-data/db';
-import { createModels } from '../../mol-model-formats/structure/basic/parser';
 import { CellpackPackingPreset, CellpackMembranePreset } from './preset';
 import { Asset } from '../../mol-util/assets';
 import { Color } from '../../mol-util/color';
 import { objectForEach } from '../../mol-util/object';
 import { readFromFile } from '../../mol-util/data-source';
 import { ColorNames } from '../../mol-util/color/names';
-import { createBasic } from '../../mol-model-formats/structure/basic/schema';
 
 function getCellPackModelUrl(fileName: string, baseUrl: string) {
     return `${baseUrl}/results/${fileName}`;
@@ -142,7 +137,7 @@ async function getStructure(plugin: PluginContext, model: Model, source: Ingredi
 
 function getTransformLegacy(trans: Vec3, rot: Quat) {
     const q: Quat = Quat.create(-rot[3], rot[0], rot[1], rot[2]);
-    const m: Mat4 = Mat4.fromQuat(Mat4.zero(), q);
+    const m: Mat4 = Mat4.fromQuat(Mat4(), q);
     Mat4.transpose(m, m);
     Mat4.scale(m, m, Vec3.create(-1.0, 1.0, -1.0));
     Mat4.setTranslation(m, trans);
@@ -151,7 +146,7 @@ function getTransformLegacy(trans: Vec3, rot: Quat) {
 
 function getTransform(trans: Vec3, rot: Quat) {
     const q: Quat = Quat.create(-rot[0], rot[1], rot[2], -rot[3]);
-    const m: Mat4 = Mat4.fromQuat(Mat4.zero(), q);
+    const m: Mat4 = Mat4.fromQuat(Mat4(), q);
     const p: Vec3 = Vec3.create(-trans[0], trans[1], trans[2]);
     Mat4.setTranslation(m, p);
     return m;
@@ -214,111 +209,10 @@ function getAssembly(name: string, transforms: Mat4[], structure: Structure) {
     return builder.getStructure();
 }
 
-function getCifCurve(name: string, transforms: Mat4[], model: Model) {
-    if (!MmcifFormat.is(model.sourceData)) throw new Error('mmcif source data needed');
-
-    const { db } = model.sourceData.data;
-    const d = db.atom_site;
-    const n = d._rowCount;
-    const rowCount = n * transforms.length;
-
-    const { offsets, count } = model.atomicHierarchy.chainAtomSegments;
-
-    const x = d.Cartn_x.toArray();
-    const y = d.Cartn_y.toArray();
-    const z = d.Cartn_z.toArray();
-
-    const Cartn_x = new Float32Array(rowCount);
-    const Cartn_y = new Float32Array(rowCount);
-    const Cartn_z = new Float32Array(rowCount);
-    const map = new Uint32Array(rowCount);
-    const seq = new Int32Array(rowCount);
-    let offset = 0;
-    for (let c = 0; c < count; ++c) {
-        const cStart = offsets[c];
-        const cEnd = offsets[c + 1];
-        const cLength = cEnd - cStart;
-        for (let t = 0, tl = transforms.length; t < tl; ++t) {
-            const m = transforms[t];
-            for (let j = cStart; j < cEnd; ++j) {
-                const i = offset + j - cStart;
-                const xj = x[j], yj = y[j], zj = z[j];
-                Cartn_x[i] = m[0] * xj + m[4] * yj + m[8] * zj + m[12];
-                Cartn_y[i] = m[1] * xj + m[5] * yj + m[9] * zj + m[13];
-                Cartn_z[i] = m[2] * xj + m[6] * yj + m[10] * zj + m[14];
-                map[i] = j;
-                seq[i] = t + 1;
-            }
-            offset += cLength;
-        }
-    }
-
-    function multColumn<T>(column: Column<T>) {
-        const array = column.toArray();
-        return Column.ofLambda({
-            value: row => array[map[row]],
-            areValuesEqual: (rowA, rowB) => map[rowA] === map[rowB] || array[map[rowA]] === array[map[rowB]],
-            rowCount, schema: column.schema
-        });
-    }
-
-    const _atom_site: CifCategory.SomeFields<mmCIF_Schema['atom_site']> = {
-        auth_asym_id: CifField.ofColumn(multColumn(d.auth_asym_id)),
-        auth_atom_id: CifField.ofColumn(multColumn(d.auth_atom_id)),
-        auth_comp_id: CifField.ofColumn(multColumn(d.auth_comp_id)),
-        auth_seq_id: CifField.ofNumbers(seq),
-
-        B_iso_or_equiv: CifField.ofColumn(Column.ofConst(0, rowCount, Column.Schema.float)),
-        Cartn_x: CifField.ofNumbers(Cartn_x),
-        Cartn_y: CifField.ofNumbers(Cartn_y),
-        Cartn_z: CifField.ofNumbers(Cartn_z),
-        group_PDB: CifField.ofColumn(Column.ofConst('ATOM', rowCount, Column.Schema.str)),
-        id: CifField.ofColumn(Column.ofLambda({
-            value: row => row,
-            areValuesEqual: (rowA, rowB) => rowA === rowB,
-            rowCount, schema: d.id.schema,
-        })),
-
-        label_alt_id: CifField.ofColumn(multColumn(d.label_alt_id)),
-
-        label_asym_id: CifField.ofColumn(multColumn(d.label_asym_id)),
-        label_atom_id: CifField.ofColumn(multColumn(d.label_atom_id)),
-        label_comp_id: CifField.ofColumn(multColumn(d.label_comp_id)),
-        label_seq_id: CifField.ofNumbers(seq),
-        label_entity_id: CifField.ofColumn(Column.ofConst('1', rowCount, Column.Schema.str)),
-
-        occupancy: CifField.ofColumn(Column.ofConst(1, rowCount, Column.Schema.float)),
-        type_symbol: CifField.ofColumn(multColumn(d.type_symbol)),
-
-        pdbx_PDB_ins_code: CifField.ofColumn(Column.ofConst('', rowCount, Column.Schema.str)),
-        pdbx_PDB_model_num: CifField.ofColumn(Column.ofConst(1, rowCount, Column.Schema.int)),
-    };
-
-    const categories = {
-        entity: CifCategory.ofTable('entity', db.entity),
-        chem_comp: CifCategory.ofTable('chem_comp', db.chem_comp),
-        atom_site: CifCategory.ofFields('atom_site', _atom_site)
-    };
-
-    return {
-        header: name,
-        categoryNames: Object.keys(categories),
-        categories
-    };
-}
-
-async function getCurve(plugin: PluginContext, name: string, ingredient: Ingredient, transforms: Mat4[], model: Model) {
-    const cif = getCifCurve(name, transforms, model);
-    const curveModelTask = Task.create('Curve Model', async ctx => {
-        const format = MmcifFormat.fromFrame(cif);
-        const basic = createBasic(format.data.db, true);
-        const models = await createModels(basic, format, ctx);
-        return models.representative;
-    });
-
-    const curveModel = await plugin.runTask(curveModelTask);
-    // ingredient.source.selection = undefined;
-    return getStructure(plugin, curveModel, ingredient);
+async function getCurve(name: string, transforms: Mat4[], model: Model) {
+    const structure = Structure.ofModel(model);
+    const assembly = getAssembly(name, transforms, structure);
+    return assembly;
 }
 
 async function getIngredientStructure(plugin: PluginContext, ingredient: Ingredient, baseUrl: string, ingredientFiles: IngredientFiles, trajCache: TrajectoryCache, location: 'surface' | 'interior' | 'cytoplasme') {
@@ -339,7 +233,7 @@ async function getIngredientStructure(plugin: PluginContext, ingredient: Ingredi
     if (!model) return;
     let structure: Structure;
     if (nbCurve) {
-        structure = await getCurve(plugin, name, ingredient, getCurveTransforms(ingredient), model);
+        structure = await getCurve(name, getCurveTransforms(ingredient), model);
     } else {
         if ((!results || results.length === 0)) return;
         let bu: string|undefined = source.bu ? source.bu : undefined;
@@ -363,7 +257,7 @@ async function getIngredientStructure(plugin: PluginContext, ingredient: Ingredi
             structure = Structure.transform(structure, m1);
             if (ingredient.offset) {
                 const o: Vec3 = Vec3.create(ingredient.offset[0], ingredient.offset[1], ingredient.offset[2]);
-                if (!Vec3.exactEquals(o, Vec3.zero())) { // -1, 1, 4e-16 ??
+                if (!Vec3.exactEquals(o, Vec3())) { // -1, 1, 4e-16 ??
                     if (location !== 'surface') {
                         Vec3.negate(o, o);
                     }
@@ -377,7 +271,7 @@ async function getIngredientStructure(plugin: PluginContext, ingredient: Ingredi
                 if (!Vec3.exactEquals(p, Vec3.unitZ)) {
                     const q: Quat = Quat.identity();
                     Quat.rotationTo(q, p, Vec3.unitZ);
-                    const m: Mat4 = Mat4.fromQuat(Mat4.zero(), q);
+                    const m: Mat4 = Mat4.fromQuat(Mat4(), q);
                     structure = Structure.transform(structure, m);
                 }
             }
@@ -521,6 +415,7 @@ async function loadMembrane(plugin: PluginContext, name: string, state: State, p
             .apply(StructureFromAssemblies, undefined, { state: { isGhost: true } })
             .commit({ revertOnError: true });
         const membraneParams = {
+            ignoreLight: params.preset.adjustStyle,
             representation: params.preset.representation,
         };
         await CellpackMembranePreset.apply(membrane, membraneParams, plugin);
@@ -537,6 +432,7 @@ async function loadMembrane(plugin: PluginContext, name: string, state: State, p
             .apply(StateTransforms.Model.StructureFromModel, props, { state: { isGhost: true } })
             .commit({ revertOnError: true });
         const membraneParams = {
+            ignoreLight: params.preset.adjustStyle,
             representation: params.preset.representation,
         };
         await CellpackMembranePreset.apply(membrane, membraneParams, plugin);
@@ -620,6 +516,7 @@ async function loadPackings(plugin: PluginContext, runtime: RuntimeContext, stat
 
         const packingParams = {
             traceOnly: params.preset.traceOnly,
+            ignoreLight: params.preset.adjustStyle,
             representation: params.preset.representation,
         };
         await CellpackPackingPreset.apply(packing, packingParams, plugin);
@@ -671,7 +568,8 @@ const LoadCellPackModelParams = {
     ingredients: PD.FileList({ accept: '.cif,.bcif,.pdb', label: 'Ingredient files' }),
     preset: PD.Group({
         traceOnly: PD.Boolean(false),
-        representation: PD.Select('gaussian-surface', PD.arrayToOptions(['spacefill', 'gaussian-surface', 'point', 'orientation']))
+        adjustStyle: PD.Boolean(true),
+        representation: PD.Select('gaussian-surface', PD.arrayToOptions(['spacefill', 'gaussian-surface', 'point', 'orientation'] as const))
     }, { isExpanded: true })
 };
 type LoadCellPackModelParams = PD.Values<typeof LoadCellPackModelParams>
@@ -681,5 +579,32 @@ export const LoadCellPackModel = StateAction.build({
     params: LoadCellPackModelParams,
     from: PSO.Root
 })(({ state, params }, ctx: PluginContext) => Task.create('CellPack Loader', async taskCtx => {
+    if (params.preset.adjustStyle) {
+        ctx.managers.interactivity.setProps({ granularity: 'chain' });
+        ctx.canvas3d?.setProps({
+            multiSample: { mode: 'off' },
+            cameraClipping: { far: false },
+            postprocessing: {
+                occlusion: {
+                    name: 'on',
+                    params: {
+                        samples: 32,
+                        radius: 8,
+                        bias: 1,
+                        blurKernelSize: 15,
+                        resolutionScale: 1,
+                    }
+                },
+                outline: {
+                    name: 'on',
+                    params: {
+                        scale: 1,
+                        threshold: 0.33,
+                        color: ColorNames.black,
+                    }
+                }
+            }
+        });
+    }
     await loadPackings(ctx, taskCtx, state, params);
 }));

+ 13 - 7
src/extensions/cellpack/preset.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Ludovic Autin <ludovic.autin@gmail.com>
@@ -13,7 +13,8 @@ import { CellPackGenerateColorThemeProvider } from './color/generate';
 
 export const CellpackPackingPresetParams = {
     traceOnly: PD.Boolean(true),
-    representation: PD.Select('gaussian-surface', PD.arrayToOptions(['gaussian-surface', 'spacefill', 'point', 'orientation'])),
+    ignoreLight: PD.Boolean(false),
+    representation: PD.Select('gaussian-surface', PD.arrayToOptions(['gaussian-surface', 'spacefill', 'point', 'orientation'] as const)),
 };
 export type CellpackPackingPresetParams = PD.ValuesFor<typeof CellpackPackingPresetParams>
 
@@ -27,7 +28,9 @@ export const CellpackPackingPreset = StructureRepresentationPresetProvider({
 
         const reprProps = {
             ignoreHydrogens: true,
-            traceOnly: params.traceOnly
+            traceOnly: params.traceOnly,
+            instanceGranularity: true,
+            ignoreLight: params.ignoreLight,
         };
         const components = {
             polymer: await presetStaticComponent(plugin, structureCell, 'polymer')
@@ -37,8 +40,8 @@ export const CellpackPackingPreset = StructureRepresentationPresetProvider({
             Object.assign(reprProps, {
                 quality: 'custom', resolution: 10, radiusOffset: 2, doubleSided: false
             });
-        } else if (params.representation === 'spacefill' && params.traceOnly) {
-            Object.assign(reprProps, { sizeFactor: 2 });
+        } else if (params.representation === 'spacefill') {
+            Object.assign(reprProps, { sizeFactor: params.traceOnly ? 2 : 1 });
         }
 
         // default is generated
@@ -57,7 +60,8 @@ export const CellpackPackingPreset = StructureRepresentationPresetProvider({
 //
 
 export const CellpackMembranePresetParams = {
-    representation: PD.Select('gaussian-surface', PD.arrayToOptions(['gaussian-surface', 'spacefill', 'point', 'orientation'])),
+    ignoreLight: PD.Boolean(false),
+    representation: PD.Select('gaussian-surface', PD.arrayToOptions(['gaussian-surface', 'spacefill', 'point', 'orientation'] as const)),
 };
 export type CellpackMembranePresetParams = PD.ValuesFor<typeof CellpackMembranePresetParams>
 
@@ -71,6 +75,8 @@ export const CellpackMembranePreset = StructureRepresentationPresetProvider({
 
         const reprProps = {
             ignoreHydrogens: true,
+            instanceGranularity: true,
+            ignoreLight: params.ignoreLight,
         };
         const components = {
             membrane: await presetStaticComponent(plugin, structureCell, 'all', { label: 'Membrane' })
@@ -84,7 +90,7 @@ export const CellpackMembranePreset = StructureRepresentationPresetProvider({
 
         const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, {});
         const representations = {
-            membrane: builder.buildRepresentation(update, components.membrane, { type: 'gaussian-surface', typeParams: { ...typeParams, ...reprProps }, color: 'uniform', colorParams: { value: ColorNames.lightgrey } }, { tag: 'all' })
+            membrane: builder.buildRepresentation(update, components.membrane, { type: params.representation, typeParams: { ...typeParams, ...reprProps }, color: 'uniform', colorParams: { value: ColorNames.lightgrey } }, { tag: 'all' })
         };
 
         await update.commit({ revertOnError: true });

+ 6 - 1
src/mol-geo/geometry/base.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -83,6 +83,7 @@ export namespace BaseGeometry {
         quality: PD.Select<VisualQuality>('auto', VisualQualityOptions, { isEssential: true, description: 'Visual/rendering quality of the representation.' }),
         material: Material.getParam(),
         clip: PD.Group(Clip.Params),
+        instanceGranularity: PD.Boolean(false, { description: 'Use instance granularity for marker, transparency, clipping, overpaint, substance data to save memory.' }),
     };
     export type Params = typeof Params
 
@@ -118,6 +119,8 @@ export namespace BaseGeometry {
             uClipObjectPosition: ValueCell.create(clip.objects.position),
             uClipObjectRotation: ValueCell.create(clip.objects.rotation),
             uClipObjectScale: ValueCell.create(clip.objects.scale),
+
+            instanceGranularity: ValueCell.create(props.instanceGranularity),
         };
     }
 
@@ -135,6 +138,8 @@ export namespace BaseGeometry {
         ValueCell.update(values.uClipObjectPosition, clip.objects.position);
         ValueCell.update(values.uClipObjectRotation, clip.objects.rotation);
         ValueCell.update(values.uClipObjectScale, clip.objects.scale);
+
+        ValueCell.updateIfChanged(values.instanceGranularity, props.instanceGranularity);
     }
 
     export function createRenderableState(props: Partial<PD.Values<Params>> = {}): RenderableState {

+ 8 - 2
src/mol-geo/geometry/clipping-data.ts

@@ -9,10 +9,13 @@ import { Vec2 } from '../../mol-math/linear-algebra';
 import { TextureImage, createTextureImage } from '../../mol-gl/renderable/util';
 import { Clipping } from '../../mol-theme/clipping';
 
+export type ClippingType = 'instance' | 'groupInstance';
+
 export type ClippingData = {
     tClipping: ValueCell<TextureImage<Uint8Array>>
     uClippingTexDim: ValueCell<Vec2>
-    dClipping: ValueCell<boolean>,
+    dClipping: ValueCell<boolean>
+    dClippingType: ValueCell<string>
 }
 
 export function applyClippingGroups(array: Uint8Array, start: number, end: number, groups: Clipping.Groups) {
@@ -24,18 +27,20 @@ export function clearClipping(array: Uint8Array, start: number, end: number) {
     array.fill(0, start, end);
 }
 
-export function createClipping(count: number, clippingData?: ClippingData): ClippingData {
+export function createClipping(count: number, type: ClippingType, clippingData?: ClippingData): ClippingData {
     const clipping = createTextureImage(Math.max(1, count), 1, Uint8Array, clippingData && clippingData.tClipping.ref.value.array);
     if (clippingData) {
         ValueCell.update(clippingData.tClipping, clipping);
         ValueCell.update(clippingData.uClippingTexDim, Vec2.create(clipping.width, clipping.height));
         ValueCell.updateIfChanged(clippingData.dClipping, count > 0);
+        ValueCell.updateIfChanged(clippingData.dClippingType, type);
         return clippingData;
     } else {
         return {
             tClipping: ValueCell.create(clipping),
             uClippingTexDim: ValueCell.create(Vec2.create(clipping.width, clipping.height)),
             dClipping: ValueCell.create(count > 0),
+            dClippingType: ValueCell.create(type),
         };
     }
 }
@@ -52,6 +57,7 @@ export function createEmptyClipping(clippingData?: ClippingData): ClippingData {
             tClipping: ValueCell.create(emptyClippingTexture),
             uClippingTexDim: ValueCell.create(Vec2.create(1, 1)),
             dClipping: ValueCell.create(false),
+            dClippingType: ValueCell.create('groupInstance'),
         };
     }
 }

+ 3 - 1
src/mol-geo/geometry/cylinders/cylinders.ts

@@ -201,7 +201,9 @@ export namespace Cylinders {
 
         const color = createColors(locationIt, positionIt, theme.color);
         const size = createSizes(locationIt, theme.size);
-        const marker = createMarkers(instanceCount * groupCount);
+        const marker = props.instanceGranularity
+            ? createMarkers(instanceCount, 'instance')
+            : createMarkers(instanceCount * groupCount, 'groupInstance');
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
         const material = createEmptySubstance();

+ 3 - 1
src/mol-geo/geometry/direct-volume/direct-volume.ts

@@ -211,7 +211,9 @@ export namespace DirectVolume {
         const positionIt = Utils.createPositionIterator(directVolume, transform);
 
         const color = createColors(locationIt, positionIt, theme.color);
-        const marker = createMarkers(instanceCount * groupCount);
+        const marker = props.instanceGranularity
+            ? createMarkers(instanceCount, 'instance')
+            : createMarkers(instanceCount * groupCount, 'groupInstance');
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
         const material = createEmptySubstance();

+ 3 - 1
src/mol-geo/geometry/image/image.ts

@@ -143,7 +143,9 @@ namespace Image {
         const positionIt = Utils.createPositionIterator(image, transform);
 
         const color = createColors(locationIt, positionIt, theme.color);
-        const marker = createMarkers(instanceCount * groupCount);
+        const marker = props.instanceGranularity
+            ? createMarkers(instanceCount, 'instance')
+            : createMarkers(instanceCount * groupCount, 'groupInstance');
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
         const material = createEmptySubstance();

+ 3 - 1
src/mol-geo/geometry/lines/lines.ts

@@ -208,7 +208,9 @@ export namespace Lines {
 
         const color = createColors(locationIt, positionIt, theme.color);
         const size = createSizes(locationIt, theme.size);
-        const marker = createMarkers(instanceCount * groupCount);
+        const marker = props.instanceGranularity
+            ? createMarkers(instanceCount, 'instance')
+            : createMarkers(instanceCount * groupCount, 'groupInstance');
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
         const material = createEmptySubstance();

+ 9 - 3
src/mol-geo/geometry/marker-data.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -8,12 +8,15 @@ import { ValueCell } from '../../mol-util/value-cell';
 import { Vec2 } from '../../mol-math/linear-algebra';
 import { TextureImage, createTextureImage } from '../../mol-gl/renderable/util';
 
+export type MarkerType = 'instance' | 'groupInstance';
+
 export type MarkerData = {
-    uMarker: ValueCell<number>,
+    uMarker: ValueCell<number>
     tMarker: ValueCell<TextureImage<Uint8Array>>
     uMarkerTexDim: ValueCell<Vec2>
     markerAverage: ValueCell<number>
     markerStatus: ValueCell<number>
+    dMarkerType: ValueCell<string>
 }
 
 const MarkerCountLut = new Uint8Array(0x0303 + 1);
@@ -64,7 +67,7 @@ export function getMarkersAverage(array: Uint8Array, count: number): number {
     return sum / count;
 }
 
-export function createMarkers(count: number, markerData?: MarkerData): MarkerData {
+export function createMarkers(count: number, type: MarkerType, markerData?: MarkerData): MarkerData {
     const markers = createTextureImage(Math.max(1, count), 1, Uint8Array, markerData && markerData.tMarker.ref.value.array);
     const average = getMarkersAverage(markers.array, count);
     const status = average === 0 ? 0 : -1;
@@ -74,6 +77,7 @@ export function createMarkers(count: number, markerData?: MarkerData): MarkerDat
         ValueCell.update(markerData.uMarkerTexDim, Vec2.create(markers.width, markers.height));
         ValueCell.updateIfChanged(markerData.markerAverage, average);
         ValueCell.updateIfChanged(markerData.markerStatus, status);
+        ValueCell.updateIfChanged(markerData.dMarkerType, type);
         return markerData;
     } else {
         return {
@@ -82,6 +86,7 @@ export function createMarkers(count: number, markerData?: MarkerData): MarkerDat
             uMarkerTexDim: ValueCell.create(Vec2.create(markers.width, markers.height)),
             markerAverage: ValueCell.create(average),
             markerStatus: ValueCell.create(status),
+            dMarkerType: ValueCell.create(type),
         };
     }
 }
@@ -102,6 +107,7 @@ export function createEmptyMarkers(markerData?: MarkerData): MarkerData {
             uMarkerTexDim: ValueCell.create(Vec2.create(1, 1)),
             markerAverage: ValueCell.create(0),
             markerStatus: ValueCell.create(0),
+            dMarkerType: ValueCell.create('groupInstance'),
         };
     }
 }

+ 3 - 1
src/mol-geo/geometry/mesh/mesh.ts

@@ -666,7 +666,9 @@ export namespace Mesh {
         const positionIt = createPositionIterator(mesh, transform);
 
         const color = createColors(locationIt, positionIt, theme.color);
-        const marker = createMarkers(instanceCount * groupCount);
+        const marker = props.instanceGranularity
+            ? createMarkers(instanceCount, 'instance')
+            : createMarkers(instanceCount * groupCount, 'groupInstance');
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
         const material = createEmptySubstance();

+ 5 - 2
src/mol-geo/geometry/overpaint-data.ts

@@ -10,6 +10,8 @@ import { TextureImage, createTextureImage } from '../../mol-gl/renderable/util';
 import { Color } from '../../mol-util/color';
 import { createNullTexture, Texture } from '../../mol-gl/webgl/texture';
 
+export type OverpaintType = 'instance' | 'groupInstance' | 'volumeInstance';
+
 export type OverpaintData = {
     tOverpaint: ValueCell<TextureImage<Uint8Array>>
     uOverpaintTexDim: ValueCell<Vec2>
@@ -34,12 +36,13 @@ export function clearOverpaint(array: Uint8Array, start: number, end: number) {
     return true;
 }
 
-export function createOverpaint(count: number, overpaintData?: OverpaintData): OverpaintData {
+export function createOverpaint(count: number, type: OverpaintType, overpaintData?: OverpaintData): OverpaintData {
     const overpaint = createTextureImage(Math.max(1, count), 4, Uint8Array, overpaintData && overpaintData.tOverpaint.ref.value.array);
     if (overpaintData) {
         ValueCell.update(overpaintData.tOverpaint, overpaint);
         ValueCell.update(overpaintData.uOverpaintTexDim, Vec2.create(overpaint.width, overpaint.height));
         ValueCell.updateIfChanged(overpaintData.dOverpaint, count > 0);
+        ValueCell.updateIfChanged(overpaintData.dOverpaintType, type);
         return overpaintData;
     } else {
         return {
@@ -50,7 +53,7 @@ export function createOverpaint(count: number, overpaintData?: OverpaintData): O
             tOverpaintGrid: ValueCell.create(createNullTexture()),
             uOverpaintGridDim: ValueCell.create(Vec3.create(1, 1, 1)),
             uOverpaintGridTransform: ValueCell.create(Vec4.create(0, 0, 0, 1)),
-            dOverpaintType: ValueCell.create('groupInstance'),
+            dOverpaintType: ValueCell.create(type),
         };
     }
 }

+ 3 - 1
src/mol-geo/geometry/points/points.ts

@@ -170,7 +170,9 @@ export namespace Points {
 
         const color = createColors(locationIt, positionIt, theme.color);
         const size = createSizes(locationIt, theme.size);
-        const marker = createMarkers(instanceCount * groupCount);
+        const marker = props.instanceGranularity
+            ? createMarkers(instanceCount, 'instance')
+            : createMarkers(instanceCount * groupCount, 'groupInstance');
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
         const material = createEmptySubstance();

+ 3 - 1
src/mol-geo/geometry/spheres/spheres.ts

@@ -171,7 +171,9 @@ export namespace Spheres {
 
         const color = createColors(locationIt, positionIt, theme.color);
         const size = createSizes(locationIt, theme.size);
-        const marker = createMarkers(instanceCount * groupCount);
+        const marker = props.instanceGranularity
+            ? createMarkers(instanceCount, 'instance')
+            : createMarkers(instanceCount * groupCount, 'groupInstance');
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
         const material = createEmptySubstance();

+ 5 - 2
src/mol-geo/geometry/substance-data.ts

@@ -10,6 +10,8 @@ import { TextureImage, createTextureImage } from '../../mol-gl/renderable/util';
 import { createNullTexture, Texture } from '../../mol-gl/webgl/texture';
 import { Material } from '../../mol-util/material';
 
+export type SubstanceType = 'instance' | 'groupInstance' | 'volumeInstance';
+
 export type SubstanceData = {
     tSubstance: ValueCell<TextureImage<Uint8Array>>
     uSubstanceTexDim: ValueCell<Vec2>
@@ -34,12 +36,13 @@ export function clearSubstance(array: Uint8Array, start: number, end: number) {
     return true;
 }
 
-export function createSubstance(count: number, substanceData?: SubstanceData): SubstanceData {
+export function createSubstance(count: number, type: SubstanceType, substanceData?: SubstanceData): SubstanceData {
     const substance = createTextureImage(Math.max(1, count), 4, Uint8Array, substanceData && substanceData.tSubstance.ref.value.array);
     if (substanceData) {
         ValueCell.update(substanceData.tSubstance, substance);
         ValueCell.update(substanceData.uSubstanceTexDim, Vec2.create(substance.width, substance.height));
         ValueCell.updateIfChanged(substanceData.dSubstance, count > 0);
+        ValueCell.updateIfChanged(substanceData.dSubstanceType, type);
         return substanceData;
     } else {
         return {
@@ -50,7 +53,7 @@ export function createSubstance(count: number, substanceData?: SubstanceData): S
             tSubstanceGrid: ValueCell.create(createNullTexture()),
             uSubstanceGridDim: ValueCell.create(Vec3.create(1, 1, 1)),
             uSubstanceGridTransform: ValueCell.create(Vec4.create(0, 0, 0, 1)),
-            dSubstanceType: ValueCell.create('groupInstance'),
+            dSubstanceType: ValueCell.create(type),
         };
     }
 }

+ 3 - 1
src/mol-geo/geometry/text/text.ts

@@ -211,7 +211,9 @@ export namespace Text {
 
         const color = createColors(locationIt, positionIt, theme.color);
         const size = createSizes(locationIt, theme.size);
-        const marker = createMarkers(instanceCount * groupCount);
+        const marker = props.instanceGranularity
+            ? createMarkers(instanceCount, 'instance')
+            : createMarkers(instanceCount * groupCount, 'groupInstance');
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
         const substance = createEmptySubstance();

+ 3 - 1
src/mol-geo/geometry/texture-mesh/texture-mesh.ts

@@ -137,7 +137,9 @@ export namespace TextureMesh {
         const positionIt = Utils.createPositionIterator(textureMesh, transform);
 
         const color = createColors(locationIt, positionIt, theme.color);
-        const marker = createMarkers(instanceCount * groupCount);
+        const marker = props.instanceGranularity
+            ? createMarkers(instanceCount, 'instance')
+            : createMarkers(instanceCount * groupCount, 'groupInstance');
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
         const substance = createEmptySubstance();

+ 5 - 2
src/mol-geo/geometry/transparency-data.ts

@@ -9,6 +9,8 @@ import { Vec2, Vec3, Vec4 } from '../../mol-math/linear-algebra';
 import { TextureImage, createTextureImage } from '../../mol-gl/renderable/util';
 import { createNullTexture, Texture } from '../../mol-gl/webgl/texture';
 
+export type TransparencyType = 'instance' | 'groupInstance' | 'volumeInstance';
+
 export type TransparencyData = {
     tTransparency: ValueCell<TextureImage<Uint8Array>>
     uTransparencyTexDim: ValueCell<Vec2>
@@ -41,13 +43,14 @@ export function clearTransparency(array: Uint8Array, start: number, end: number)
     array.fill(0, start, end);
 }
 
-export function createTransparency(count: number, transparencyData?: TransparencyData): TransparencyData {
+export function createTransparency(count: number, type: TransparencyType, transparencyData?: TransparencyData): TransparencyData {
     const transparency = createTextureImage(Math.max(1, count), 1, Uint8Array, transparencyData && transparencyData.tTransparency.ref.value.array);
     if (transparencyData) {
         ValueCell.update(transparencyData.tTransparency, transparency);
         ValueCell.update(transparencyData.uTransparencyTexDim, Vec2.create(transparency.width, transparency.height));
         ValueCell.updateIfChanged(transparencyData.dTransparency, count > 0);
         ValueCell.updateIfChanged(transparencyData.transparencyAverage, getTransparencyAverage(transparency.array, count));
+        ValueCell.updateIfChanged(transparencyData.dTransparencyType, type);
         return transparencyData;
     } else {
         return {
@@ -59,7 +62,7 @@ export function createTransparency(count: number, transparencyData?: Transparenc
             tTransparencyGrid: ValueCell.create(createNullTexture()),
             uTransparencyGridDim: ValueCell.create(Vec3.create(1, 1, 1)),
             uTransparencyGridTransform: ValueCell.create(Vec4.create(0, 0, 0, 1)),
-            dTransparencyType: ValueCell.create('groupInstance'),
+            dTransparencyType: ValueCell.create(type),
         };
     }
 }

+ 7 - 3
src/mol-gl/renderable/schema.ts

@@ -205,6 +205,7 @@ export const MarkerSchema = {
     tMarker: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'),
     markerAverage: ValueSpec('number'),
     markerStatus: ValueSpec('number'),
+    dMarkerType: DefineSpec('string', ['instance', 'groupInstance']),
 } as const;
 export type MarkerSchema = typeof MarkerSchema
 export type MarkerValues = Values<MarkerSchema>
@@ -217,7 +218,7 @@ export const OverpaintSchema = {
     uOverpaintGridDim: UniformSpec('v3'),
     uOverpaintGridTransform: UniformSpec('v4'),
     tOverpaintGrid: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
-    dOverpaintType: DefineSpec('string', ['groupInstance', 'volumeInstance']),
+    dOverpaintType: DefineSpec('string', ['instance', 'groupInstance', 'volumeInstance']),
 } as const;
 export type OverpaintSchema = typeof OverpaintSchema
 export type OverpaintValues = Values<OverpaintSchema>
@@ -231,7 +232,7 @@ export const TransparencySchema = {
     uTransparencyGridDim: UniformSpec('v3'),
     uTransparencyGridTransform: UniformSpec('v4'),
     tTransparencyGrid: TextureSpec('texture', 'alpha', 'ubyte', 'linear'),
-    dTransparencyType: DefineSpec('string', ['groupInstance', 'volumeInstance']),
+    dTransparencyType: DefineSpec('string', ['instance', 'groupInstance', 'volumeInstance']),
 } as const;
 export type TransparencySchema = typeof TransparencySchema
 export type TransparencyValues = Values<TransparencySchema>
@@ -244,7 +245,7 @@ export const SubstanceSchema = {
     uSubstanceGridDim: UniformSpec('v3'),
     uSubstanceGridTransform: UniformSpec('v4'),
     tSubstanceGrid: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
-    dSubstanceType: DefineSpec('string', ['groupInstance', 'volumeInstance']),
+    dSubstanceType: DefineSpec('string', ['instance', 'groupInstance', 'volumeInstance']),
 } as const;
 export type SubstanceSchema = typeof SubstanceSchema
 export type SubstanceValues = Values<SubstanceSchema>
@@ -253,6 +254,7 @@ export const ClippingSchema = {
     uClippingTexDim: UniformSpec('v2'),
     tClipping: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'),
     dClipping: DefineSpec('boolean'),
+    dClippingType: DefineSpec('string', ['instance', 'groupInstance']),
 } as const;
 export type ClippingSchema = typeof ClippingSchema
 export type ClippingValues = Values<ClippingSchema>
@@ -311,6 +313,8 @@ export const BaseSchema = {
     extraTransform: ValueSpec('float32'),
     /** denotes reflection in transform */
     hasReflection: ValueSpec('boolean'),
+    /** use instance granularity for marker, transparency, clipping, overpaint, substance */
+    instanceGranularity: ValueSpec('boolean'),
 
     /** bounding sphere taking aTransform into account and encompases all instances */
     boundingSphere: ValueSpec('sphere'),

+ 5 - 1
src/mol-gl/shader/chunks/assign-clipping-varying.glsl.ts

@@ -1,5 +1,9 @@
 export const assign_clipping_varying = `
 #if dClipObjectCount != 0 && defined(dClipping)
-    vClipping = readFromTexture(tClipping, aInstance * float(uGroupCount) + group, uClippingTexDim).a;
+    #if defined(dClippingType_instance)
+        vClipping = readFromTexture(tClipping, aInstance, uClippingTexDim).a;
+    #elif defined(dMarkerType_groupInstance)
+        vClipping = readFromTexture(tClipping, aInstance * float(uGroupCount) + group, uClippingTexDim).a;
+    #endif
 #endif
 `;

+ 9 - 3
src/mol-gl/shader/chunks/assign-color-varying.glsl.ts

@@ -25,7 +25,9 @@ export const assign_color_varying = `
     #endif
 
     #ifdef dOverpaint
-        #if defined(dOverpaintType_groupInstance)
+        #if defined(dOverpaintType_instance)
+            vOverpaint = readFromTexture(tOverpaint, aInstance, uOverpaintTexDim);
+        #elif defined(dOverpaintType_groupInstance)
             vOverpaint = readFromTexture(tOverpaint, aInstance * float(uGroupCount) + group, uOverpaintTexDim);
         #elif defined(dOverpaintType_vertexInstance)
             vOverpaint = readFromTexture(tOverpaint, int(aInstance) * uVertexCount + VertexID, uOverpaintTexDim);
@@ -43,7 +45,9 @@ export const assign_color_varying = `
     #endif
 
     #ifdef dSubstance
-        #if defined(dSubstanceType_groupInstance)
+        #if defined(dSubstanceType_instance)
+            vSubstance = readFromTexture(tSubstance, aInstance, uSubstanceTexDim);
+        #elif defined(dSubstanceType_groupInstance)
             vSubstance = readFromTexture(tSubstance, aInstance * float(uGroupCount) + group, uSubstanceTexDim);
         #elif defined(dSubstanceType_vertexInstance)
             vSubstance = readFromTexture(tSubstance, int(aInstance) * uVertexCount + VertexID, uSubstanceTexDim);
@@ -72,7 +76,9 @@ export const assign_color_varying = `
 #endif
 
 #ifdef dTransparency
-    #if defined(dTransparencyType_groupInstance)
+    #if defined(dTransparencyType_instance)
+        vTransparency = readFromTexture(tTransparency, aInstance, uTransparencyTexDim).a;
+    #elif defined(dTransparencyType_groupInstance)
         vTransparency = readFromTexture(tTransparency, aInstance * float(uGroupCount) + group, uTransparencyTexDim).a;
     #elif defined(dTransparencyType_vertexInstance)
         vTransparency = readFromTexture(tTransparency, int(aInstance) * uVertexCount + VertexID, uTransparencyTexDim).a;

+ 5 - 1
src/mol-gl/shader/chunks/assign-marker-varying.glsl.ts

@@ -1,5 +1,9 @@
 export const assign_marker_varying = `
 #if defined(dRenderVariant_color) || defined(dRenderVariant_marking)
-    vMarker = readFromTexture(tMarker, aInstance * float(uGroupCount) + group, uMarkerTexDim).a;
+    #if defined(dMarkerType_instance)
+        vMarker = readFromTexture(tMarker, aInstance, uMarkerTexDim).a;
+    #elif defined(dMarkerType_groupInstance)
+        vMarker = readFromTexture(tMarker, aInstance * float(uGroupCount) + group, uMarkerTexDim).a;
+    #endif
 #endif
 `;

+ 3 - 3
src/mol-gl/shader/chunks/color-vert-params.glsl.ts

@@ -28,7 +28,7 @@ uniform float uBumpiness;
     #endif
 
     #ifdef dOverpaint
-        #if defined(dOverpaintType_groupInstance) || defined(dOverpaintType_vertexInstance)
+        #if defined(dOverpaintType_instance) || defined(dOverpaintType_groupInstance) || defined(dOverpaintType_vertexInstance)
             varying vec4 vOverpaint;
             uniform vec2 uOverpaintTexDim;
             uniform sampler2D tOverpaint;
@@ -42,7 +42,7 @@ uniform float uBumpiness;
     #endif
 
     #ifdef dSubstance
-        #if defined(dSubstanceType_groupInstance) || defined(dSubstanceType_vertexInstance)
+        #if defined(dSubstanceType_instance) || defined(dSubstanceType_groupInstance) || defined(dSubstanceType_vertexInstance)
             varying vec4 vSubstance;
             uniform vec2 uSubstanceTexDim;
             uniform sampler2D tSubstance;
@@ -75,7 +75,7 @@ uniform float uBumpiness;
 #endif
 
 #ifdef dTransparency
-    #if defined(dTransparencyType_groupInstance) || defined(dTransparencyType_vertexInstance)
+    #if defined(dTransparencyType_instance) || defined(dTransparencyType_groupInstance) || defined(dTransparencyType_vertexInstance)
         varying float vTransparency;
         uniform vec2 uTransparencyTexDim;
         uniform sampler2D tTransparency;

+ 30 - 3
src/mol-repr/shape/representation.ts

@@ -78,6 +78,10 @@ export function ShapeRepresentation<D, G extends Geometry, P extends Geometry.Pa
             console.warn('unexpected state');
         }
 
+        if (props.instanceGranularity !== currentProps.instanceGranularity) {
+            updateState.updateTransform = true;
+        }
+
         if (updateState.updateTransform) {
             updateState.updateColor = true;
             updateState.updateSize = true;
@@ -125,7 +129,11 @@ export function ShapeRepresentation<D, G extends Geometry, P extends Geometry.Pa
                     Shape.createTransform(_shape.transforms, _renderObject.values);
                     locationIt = Shape.groupIterator(_shape);
                     const { instanceCount, groupCount } = locationIt;
-                    createMarkers(instanceCount * groupCount, _renderObject.values);
+                    if (props.instanceGranularity) {
+                        createMarkers(instanceCount, 'instance', _renderObject.values);
+                    } else {
+                        createMarkers(instanceCount * groupCount, 'groupInstance', _renderObject.values);
+                    }
                 }
 
                 if (updateState.createGeometry) {
@@ -167,11 +175,30 @@ export function ShapeRepresentation<D, G extends Geometry, P extends Geometry.Pa
         });
     }
 
+    function eachInstance(loci: Loci, shape: Shape, apply: (interval: Interval) => boolean) {
+        let changed = false;
+        if (!ShapeGroup.isLoci(loci)) return false;
+        if (ShapeGroup.isLociEmpty(loci)) return false;
+        if (loci.shape !== shape) return false;
+        for (const g of loci.groups) {
+            if (apply(Interval.ofSingleton(g.instance))) changed = true;
+        }
+        return changed;
+    }
+
     function lociApply(loci: Loci, apply: (interval: Interval) => boolean) {
         if (isEveryLoci(loci) || (Shape.isLoci(loci) && loci.shape === _shape)) {
-            return apply(Interval.ofBounds(0, _shape.groupCount * _shape.transforms.length));
+            if (currentProps.instanceGranularity) {
+                return apply(Interval.ofBounds(0, _shape.transforms.length));
+            } else {
+                return apply(Interval.ofBounds(0, _shape.groupCount * _shape.transforms.length));
+            }
         } else {
-            return eachShapeGroup(loci, _shape, apply);
+            if (currentProps.instanceGranularity) {
+                return eachInstance(loci, _shape, apply);
+            } else {
+                return eachShapeGroup(loci, _shape, apply);
+            }
         }
     }
 

+ 28 - 4
src/mol-repr/structure/complex-visual.ts

@@ -6,7 +6,7 @@
 
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { Visual, VisualContext } from '../visual';
-import { Structure, StructureElement } from '../../mol-model/structure';
+import { Bond, Structure, StructureElement } from '../../mol-model/structure';
 import { Geometry, GeometryUtils } from '../../mol-geo/geometry/geometry';
 import { LocationIterator } from '../../mol-geo/util/location-iterator';
 import { Theme } from '../../mol-theme/theme';
@@ -126,6 +126,10 @@ export function ComplexVisual<G extends Geometry, P extends StructureParams & Ge
             updateState.createGeometry = true;
         }
 
+        if (newProps.instanceGranularity !== currentProps.instanceGranularity) {
+            updateState.updateTransform = true;
+        }
+
         if (updateState.updateSize && !('uSize' in renderObject.values)) {
             updateState.createGeometry = true;
         }
@@ -154,7 +158,11 @@ export function ComplexVisual<G extends Geometry, P extends StructureParams & Ge
                 // console.log('update transform')
                 locationIt = createLocationIterator(newStructure);
                 const { instanceCount, groupCount } = locationIt;
-                createMarkers(instanceCount * groupCount, renderObject.values);
+                if (newProps.instanceGranularity) {
+                    createMarkers(instanceCount, 'instance', renderObject.values);
+                } else {
+                    createMarkers(instanceCount * groupCount, 'groupInstance', renderObject.values);
+                }
             }
 
             if (updateState.createGeometry) {
@@ -205,11 +213,27 @@ export function ComplexVisual<G extends Geometry, P extends StructureParams & Ge
         return false;
     }
 
+    function eachInstance(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
+        let changed = false;
+        if (!StructureElement.Loci.is(loci) && !Bond.isLoci(loci)) return false;
+        if (!Structure.areEquivalent(loci.structure, structure)) return false;
+        if (apply(Interval.ofSingleton(0))) changed = true;
+        return changed;
+    }
+
     function lociApply(loci: Loci, apply: (interval: Interval) => boolean, isMarking: boolean) {
         if (lociIsSuperset(loci)) {
-            return apply(Interval.ofBounds(0, locationIt.groupCount * locationIt.instanceCount));
+            if (currentProps.instanceGranularity) {
+                return apply(Interval.ofBounds(0, locationIt.instanceCount));
+            } else {
+                return apply(Interval.ofBounds(0, locationIt.groupCount * locationIt.instanceCount));
+            }
         } else {
-            return eachLocation(loci, currentStructure, apply, isMarking);
+            if (currentProps.instanceGranularity) {
+                return eachInstance(loci, currentStructure, apply);
+            } else {
+                return eachLocation(loci, currentStructure, apply, isMarking);
+            }
         }
     }
 

+ 45 - 4
src/mol-repr/structure/units-visual.ts

@@ -5,7 +5,7 @@
  */
 
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
-import { Structure, Unit, StructureElement } from '../../mol-model/structure';
+import { Structure, Unit, StructureElement, Bond } from '../../mol-model/structure';
 import { RepresentationProps } from '../representation';
 import { Visual, VisualContext } from '../visual';
 import { Geometry, GeometryUtils } from '../../mol-geo/geometry/geometry';
@@ -126,6 +126,10 @@ export function UnitsVisual<G extends Geometry, P extends StructureParams & Geom
             updateState.createGeometry = true;
         }
 
+        if (newProps.instanceGranularity !== currentProps.instanceGranularity) {
+            updateState.updateTransform = true;
+        }
+
         if (!deepEqual(newProps.unitKinds, currentProps.unitKinds)) {
             // console.log('new unitKinds');
             updateState.createGeometry = true;
@@ -194,7 +198,11 @@ export function UnitsVisual<G extends Geometry, P extends StructureParams & Geom
                 // console.log('update transform');
                 locationIt = createLocationIterator(newStructureGroup);
                 const { instanceCount, groupCount } = locationIt;
-                createMarkers(instanceCount * groupCount, renderObject.values);
+                if (newProps.instanceGranularity) {
+                    createMarkers(instanceCount, 'instance', renderObject.values);
+                } else {
+                    createMarkers(instanceCount * groupCount, 'groupInstance', renderObject.values);
+                }
             }
 
             if (updateState.updateMatrix) {
@@ -259,11 +267,44 @@ export function UnitsVisual<G extends Geometry, P extends StructureParams & Geom
         return false;
     }
 
+    function eachInstance(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
+        let changed = false;
+        if (Bond.isLoci(loci)) {
+            const { structure, group } = structureGroup;
+            if (!Structure.areEquivalent(loci.structure, structure)) return false;
+            for (const b of loci.bonds) {
+                if (b.aUnit !== b.bUnit) continue;
+                const unitIdx = group.unitIndexMap.get(b.aUnit.id);
+                if (unitIdx !== undefined) {
+                    if (apply(Interval.ofSingleton(unitIdx))) changed = true;
+                }
+            }
+        } else if (StructureElement.Loci.is(loci)) {
+            const { structure, group } = structureGroup;
+            if (!Structure.areEquivalent(loci.structure, structure)) return false;
+            for (const e of loci.elements) {
+                const unitIdx = group.unitIndexMap.get(e.unit.id);
+                if (unitIdx !== undefined) {
+                    if (apply(Interval.ofSingleton(unitIdx))) changed = true;
+                }
+            }
+        }
+        return changed;
+    }
+
     function lociApply(loci: Loci, apply: (interval: Interval) => boolean, isMarking: boolean) {
         if (lociIsSuperset(loci)) {
-            return apply(Interval.ofBounds(0, locationIt.groupCount * locationIt.instanceCount));
+            if (currentProps.instanceGranularity) {
+                return apply(Interval.ofBounds(0, locationIt.instanceCount));
+            } else {
+                return apply(Interval.ofBounds(0, locationIt.groupCount * locationIt.instanceCount));
+            }
         } else {
-            return eachLocation(loci, currentStructureGroup, apply, isMarking);
+            if (currentProps.instanceGranularity) {
+                return eachInstance(loci, currentStructureGroup, apply);
+            } else {
+                return eachLocation(loci, currentStructureGroup, apply, isMarking);
+            }
         }
     }
 

+ 39 - 21
src/mol-repr/visual.ts

@@ -82,8 +82,10 @@ namespace Visual {
     export function mark(renderObject: GraphicsRenderObject | undefined, loci: Loci, action: MarkerAction, lociApply: LociApply, previous?: PreviousMark) {
         if (!renderObject || isEmptyLoci(loci)) return false;
 
-        const { tMarker, uMarker, markerAverage, markerStatus, uGroupCount, instanceCount } = renderObject.values;
-        const count = uGroupCount.ref.value * instanceCount.ref.value;
+        const { tMarker, uMarker, markerAverage, markerStatus, uGroupCount, instanceCount, instanceGranularity: instanceGranularity } = renderObject.values;
+        const count = instanceGranularity.ref.value
+            ? instanceCount.ref.value
+            : uGroupCount.ref.value * instanceCount.ref.value;
         const { array } = tMarker.ref.value;
         const currentStatus = markerStatus.ref.value as MarkerInfo['status'];
 
@@ -158,11 +160,14 @@ namespace Visual {
     export function setOverpaint(renderObject: GraphicsRenderObject | undefined, overpaint: Overpaint, lociApply: LociApply, clear: boolean, smoothing?: SmoothingContext) {
         if (!renderObject) return;
 
-        const { tOverpaint, dOverpaintType, dOverpaint, uGroupCount, instanceCount } = renderObject.values;
-        const count = uGroupCount.ref.value * instanceCount.ref.value;
+        const { tOverpaint, dOverpaintType, dOverpaint, uGroupCount, instanceCount, instanceGranularity: instanceGranularity } = renderObject.values;
+        const count = instanceGranularity.ref.value
+            ? instanceCount.ref.value
+            : uGroupCount.ref.value * instanceCount.ref.value;
 
-        // ensure texture has right size
-        createOverpaint(overpaint.layers.length ? count : 0, renderObject.values);
+        // ensure texture has right size and type
+        const type = instanceGranularity.ref.value ? 'instance' : 'groupInstance';
+        createOverpaint(overpaint.layers.length ? count : 0, type, renderObject.values);
         const { array } = tOverpaint.ref.value;
 
         // clear all if requested
@@ -180,10 +185,11 @@ namespace Visual {
             lociApply(loci, apply, false);
         }
         ValueCell.update(tOverpaint, tOverpaint.ref.value);
-        ValueCell.updateIfChanged(dOverpaintType, 'groupInstance');
+        ValueCell.updateIfChanged(dOverpaintType, type);
         ValueCell.updateIfChanged(dOverpaint, overpaint.layers.length > 0);
 
         if (overpaint.layers.length === 0) return;
+        if (type === 'instance') return;
 
         if (smoothing && hasColorSmoothingProp(smoothing.props)) {
             const { geometry, props, webgl } = smoothing;
@@ -208,11 +214,14 @@ namespace Visual {
     export function setTransparency(renderObject: GraphicsRenderObject | undefined, transparency: Transparency, lociApply: LociApply, clear: boolean, smoothing?: SmoothingContext) {
         if (!renderObject) return;
 
-        const { tTransparency, dTransparencyType, transparencyAverage, dTransparency, uGroupCount, instanceCount } = renderObject.values;
-        const count = uGroupCount.ref.value * instanceCount.ref.value;
+        const { tTransparency, dTransparencyType, transparencyAverage, dTransparency, uGroupCount, instanceCount, instanceGranularity: instanceGranularity } = renderObject.values;
+        const count = instanceGranularity.ref.value
+            ? instanceCount.ref.value
+            : uGroupCount.ref.value * instanceCount.ref.value;
 
-        // ensure texture has right size and variant
-        createTransparency(transparency.layers.length ? count : 0, renderObject.values);
+        // ensure texture has right size and type
+        const type = instanceGranularity.ref.value ? 'instance' : 'groupInstance';
+        createTransparency(transparency.layers.length ? count : 0, type, renderObject.values);
         const { array } = tTransparency.ref.value;
 
         // clear if requested
@@ -229,10 +238,11 @@ namespace Visual {
         }
         ValueCell.update(tTransparency, tTransparency.ref.value);
         ValueCell.updateIfChanged(transparencyAverage, getTransparencyAverage(array, count));
-        ValueCell.updateIfChanged(dTransparencyType, 'groupInstance');
+        ValueCell.updateIfChanged(dTransparencyType, type);
         ValueCell.updateIfChanged(dTransparency, transparency.layers.length > 0);
 
         if (transparency.layers.length === 0) return;
+        if (type === 'instance') return;
 
         if (smoothing && hasColorSmoothingProp(smoothing.props)) {
             const { geometry, props, webgl } = smoothing;
@@ -257,11 +267,14 @@ namespace Visual {
     export function setSubstance(renderObject: GraphicsRenderObject | undefined, substance: Substance, lociApply: LociApply, clear: boolean, smoothing?: SmoothingContext) {
         if (!renderObject) return;
 
-        const { tSubstance, dSubstanceType, dSubstance, uGroupCount, instanceCount } = renderObject.values;
-        const count = uGroupCount.ref.value * instanceCount.ref.value;
+        const { tSubstance, dSubstanceType, dSubstance, uGroupCount, instanceCount, instanceGranularity: instanceGranularity } = renderObject.values;
+        const count = instanceGranularity.ref.value
+            ? instanceCount.ref.value
+            : uGroupCount.ref.value * instanceCount.ref.value;
 
-        // ensure texture has right size
-        createSubstance(substance.layers.length ? count : 0, renderObject.values);
+        // ensure texture has right size and type
+        const type = instanceGranularity.ref.value ? 'instance' : 'groupInstance';
+        createSubstance(substance.layers.length ? count : 0, type, renderObject.values);
         const { array } = tSubstance.ref.value;
 
         // clear all if requested
@@ -279,10 +292,11 @@ namespace Visual {
             lociApply(loci, apply, false);
         }
         ValueCell.update(tSubstance, tSubstance.ref.value);
-        ValueCell.updateIfChanged(dSubstanceType, 'groupInstance');
+        ValueCell.updateIfChanged(dSubstanceType, type);
         ValueCell.updateIfChanged(dSubstance, substance.layers.length > 0);
 
         if (substance.layers.length === 0) return;
+        if (type === 'instance') return;
 
         if (smoothing && hasColorSmoothingProp(smoothing.props)) {
             const { geometry, props, webgl } = smoothing;
@@ -307,12 +321,15 @@ namespace Visual {
     export function setClipping(renderObject: GraphicsRenderObject | undefined, clipping: Clipping, lociApply: LociApply, clear: boolean) {
         if (!renderObject) return;
 
-        const { tClipping, dClipping, uGroupCount, instanceCount } = renderObject.values;
-        const count = uGroupCount.ref.value * instanceCount.ref.value;
+        const { tClipping, dClippingType, dClipping, uGroupCount, instanceCount, instanceGranularity: instanceGranularity } = renderObject.values;
+        const count = instanceGranularity.ref.value
+            ? instanceCount.ref.value
+            : uGroupCount.ref.value * instanceCount.ref.value;
         const { layers } = clipping;
 
-        // ensure texture has right size
-        createClipping(layers.length ? count : 0, renderObject.values);
+        // ensure texture has right size and type
+        const type = instanceGranularity.ref.value ? 'instance' : 'groupInstance';
+        createClipping(layers.length ? count : 0, type, renderObject.values);
         const { array } = tClipping.ref.value;
 
         // clear if requested
@@ -328,6 +345,7 @@ namespace Visual {
             lociApply(loci, apply, false);
         }
         ValueCell.update(tClipping, tClipping.ref.value);
+        ValueCell.updateIfChanged(dClippingType, type);
         ValueCell.updateIfChanged(dClipping, clipping.layers.length > 0);
     }
 

+ 36 - 3
src/mol-repr/volume/representation.ts

@@ -33,6 +33,7 @@ import { Clipping } from '../../mol-theme/clipping';
 import { WebGLContext } from '../../mol-gl/webgl/context';
 import { isPromiseLike } from '../../mol-util/type-helpers';
 import { Substance } from '../../mol-theme/substance';
+import { createMarkers } from '../../mol-geo/geometry/marker-data';
 
 export interface VolumeVisual<P extends VolumeParams> extends Visual<Volume, P> { }
 
@@ -108,6 +109,10 @@ export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geomet
         if (updateState.createGeometry) {
             updateState.updateColor = true;
         }
+
+        if (newProps.instanceGranularity !== currentProps.instanceGranularity) {
+            updateState.updateTransform = true;
+        }
     }
 
     function update(newGeometry?: G) {
@@ -124,7 +129,18 @@ export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geomet
                 throw new Error('expected renderObject to be available');
             }
 
-            locationIt.reset();
+            if (updateState.updateTransform) {
+                // console.log('update transform');
+                locationIt = createLocationIterator(newVolume);
+                const { instanceCount, groupCount } = locationIt;
+                if (newProps.instanceGranularity) {
+                    createMarkers(instanceCount, 'instance', renderObject.values);
+                } else {
+                    createMarkers(instanceCount * groupCount, 'groupInstance', renderObject.values);
+                }
+            } else {
+                locationIt.reset();
+            }
 
             if (updateState.createGeometry) {
                 if (newGeometry) {
@@ -165,11 +181,28 @@ export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geomet
         }
     }
 
+    function eachInstance(loci: Loci, volume: Volume, apply: (interval: Interval) => boolean) {
+        let changed = false;
+        if (!Volume.Cell.isLoci(loci)) return false;
+        if (Volume.Cell.isLociEmpty(loci)) return false;
+        if (!Volume.areEquivalent(loci.volume, volume)) return false;
+        if (apply(Interval.ofSingleton(0))) changed = true;
+        return changed;
+    }
+
     function lociApply(loci: Loci, apply: (interval: Interval) => boolean) {
         if (isEveryLoci(loci)) {
-            return apply(Interval.ofBounds(0, locationIt.groupCount * locationIt.instanceCount));
+            if (currentProps.instanceGranularity) {
+                return apply(Interval.ofBounds(0, locationIt.instanceCount));
+            } else {
+                return apply(Interval.ofBounds(0, locationIt.groupCount * locationIt.instanceCount));
+            }
         } else {
-            return eachLocation(loci, currentVolume, currentProps, apply);
+            if (currentProps.instanceGranularity) {
+                return eachInstance(loci, currentVolume, apply);
+            } else {
+                return eachLocation(loci, currentVolume, currentProps, apply);
+            }
         }
     }