소스 검색

add Substance theming (per-group material)

Alexander Rose 3 년 전
부모
커밋
c704b7505c
35개의 변경된 파일696개의 추가작업 그리고 35개의 파일을 삭제
  1. 3 0
      src/mol-geo/geometry/cylinders/cylinders.ts
  2. 3 0
      src/mol-geo/geometry/direct-volume/direct-volume.ts
  3. 3 0
      src/mol-geo/geometry/image/image.ts
  4. 3 0
      src/mol-geo/geometry/lines/lines.ts
  5. 34 0
      src/mol-geo/geometry/mesh/color-smoothing.ts
  6. 3 0
      src/mol-geo/geometry/mesh/mesh.ts
  7. 3 0
      src/mol-geo/geometry/points/points.ts
  8. 3 0
      src/mol-geo/geometry/spheres/spheres.ts
  9. 76 0
      src/mol-geo/geometry/substance-data.ts
  10. 3 0
      src/mol-geo/geometry/text/text.ts
  11. 36 0
      src/mol-geo/geometry/texture-mesh/color-smoothing.ts
  12. 3 0
      src/mol-geo/geometry/texture-mesh/texture-mesh.ts
  13. 1 2
      src/mol-gl/renderable/points.ts
  14. 17 8
      src/mol-gl/renderable/schema.ts
  15. 6 10
      src/mol-gl/shader/chunks/apply-light-color.glsl.ts
  16. 14 0
      src/mol-gl/shader/chunks/assign-color-varying.glsl.ts
  17. 7 0
      src/mol-gl/shader/chunks/assign-material-color.glsl.ts
  18. 7 0
      src/mol-gl/shader/chunks/color-frag-params.glsl.ts
  19. 17 0
      src/mol-gl/shader/chunks/color-vert-params.glsl.ts
  20. 0 4
      src/mol-gl/shader/chunks/light-frag-params.glsl.ts
  21. 23 1
      src/mol-gl/shader/direct-volume.frag.ts
  22. 76 0
      src/mol-plugin-state/helpers/structure-substance.ts
  23. 14 4
      src/mol-plugin-state/manager/structure/component.ts
  24. 119 0
      src/mol-plugin-state/transforms/representation.ts
  25. 1 0
      src/mol-plugin/spec.ts
  26. 8 1
      src/mol-repr/representation.ts
  27. 3 0
      src/mol-repr/shape/representation.ts
  28. 6 0
      src/mol-repr/structure/complex-representation.ts
  29. 5 0
      src/mol-repr/structure/complex-visual.ts
  30. 7 2
      src/mol-repr/structure/units-representation.ts
  31. 5 0
      src/mol-repr/structure/units-visual.ts
  32. 55 2
      src/mol-repr/visual.ts
  33. 4 0
      src/mol-repr/volume/representation.ts
  34. 1 1
      src/mol-theme/overpaint.ts
  35. 127 0
      src/mol-theme/substance.ts

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

@@ -25,6 +25,7 @@ import { hashFnv32a } from '../../../mol-data/util';
 import { createEmptyClipping } from '../clipping-data';
 import { CylindersValues } from '../../../mol-gl/renderable/cylinders';
 import { RenderableState } from '../../../mol-gl/renderable';
+import { createEmptySubstance } from '../substance-data';
 
 export interface Cylinders {
     readonly kind: 'cylinders',
@@ -200,6 +201,7 @@ export namespace Cylinders {
         const marker = createMarkers(instanceCount * groupCount);
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
+        const material = createEmptySubstance();
         const clipping = createEmptyClipping();
 
         const counts = { drawCount: cylinders.cylinderCount * 4 * 3, vertexCount: cylinders.cylinderCount * 6, groupCount, instanceCount };
@@ -224,6 +226,7 @@ export namespace Cylinders {
             ...marker,
             ...overpaint,
             ...transparency,
+            ...material,
             ...clipping,
             ...transform,
 

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

@@ -28,6 +28,7 @@ import { createTransferFunctionTexture, getControlPointsFromVec2Array } from './
 import { createEmptyClipping } from '../clipping-data';
 import { Grid, Volume } from '../../../mol-model/volume';
 import { ColorNames } from '../../../mol-util/color/names';
+import { createEmptySubstance } from '../substance-data';
 
 const VolumeBox = Box();
 
@@ -246,6 +247,7 @@ export namespace DirectVolume {
         const marker = createMarkers(instanceCount * groupCount);
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
+        const material = createEmptySubstance();
         const clipping = createEmptyClipping();
 
         const [x, y, z] = gridDimension.ref.value;
@@ -270,6 +272,7 @@ export namespace DirectVolume {
             ...marker,
             ...overpaint,
             ...transparency,
+            ...material,
             ...clipping,
             ...transform,
             ...BaseGeometry.createValues(props, counts),

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

@@ -26,6 +26,7 @@ import { fillSerial } from '../../../mol-util/array';
 import { createEmptyClipping } from '../clipping-data';
 import { NullLocation } from '../../../mol-model/location';
 import { QuadPositions } from '../../../mol-gl/compute/util';
+import { createEmptySubstance } from '../substance-data';
 
 const QuadIndices = new Uint32Array([
     0, 1, 2,
@@ -145,6 +146,7 @@ namespace Image {
         const marker = createMarkers(instanceCount * groupCount);
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
+        const material = createEmptySubstance();
         const clipping = createEmptyClipping();
 
         const counts = { drawCount: QuadIndices.length, vertexCount: QuadPositions.length / 3, groupCount, instanceCount };
@@ -157,6 +159,7 @@ namespace Image {
             ...marker,
             ...overpaint,
             ...transparency,
+            ...material,
             ...clipping,
             ...transform,
             ...BaseGeometry.createValues(props, counts),

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

@@ -26,6 +26,7 @@ import { createEmptyOverpaint } from '../overpaint-data';
 import { createEmptyTransparency } from '../transparency-data';
 import { hashFnv32a } from '../../../mol-data/util';
 import { createEmptyClipping } from '../clipping-data';
+import { createEmptySubstance } from '../substance-data';
 
 /** Wide line */
 export interface Lines {
@@ -210,6 +211,7 @@ export namespace Lines {
         const marker = createMarkers(instanceCount * groupCount);
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
+        const material = createEmptySubstance();
         const clipping = createEmptyClipping();
 
         const counts = { drawCount: lines.lineCount * 2 * 3, vertexCount: lines.lineCount * 4, groupCount, instanceCount };
@@ -231,6 +233,7 @@ export namespace Lines {
             ...marker,
             ...overpaint,
             ...transparency,
+            ...material,
             ...clipping,
             ...transform,
 

+ 34 - 0
src/mol-geo/geometry/mesh/color-smoothing.ts

@@ -356,3 +356,37 @@ export function applyMeshTransparencySmoothing(values: MeshValues, resolution: n
         ValueCell.update(values.uTransparencyTexDim, smoothingData.texDim);
     }
 }
+
+function isSupportedSubstanceType(x: string): x is 'groupInstance' {
+    return x === 'groupInstance';
+}
+
+export function applyMeshSubstanceSmoothing(values: MeshValues, resolution: number, stride: number, webgl?: WebGLContext, colorTexture?: Texture) {
+    if (!isSupportedSubstanceType(values.dSubstanceType.ref.value)) return;
+
+    const smoothingData = calcMeshColorSmoothing({
+        vertexCount: values.uVertexCount.ref.value,
+        instanceCount: values.uInstanceCount.ref.value,
+        groupCount: values.uGroupCount.ref.value,
+        transformBuffer: values.aTransform.ref.value,
+        instanceBuffer: values.aInstance.ref.value,
+        positionBuffer: values.aPosition.ref.value,
+        groupBuffer: values.aGroup.ref.value,
+        colorData: values.tSubstance.ref.value,
+        colorType: values.dSubstanceType.ref.value,
+        boundingSphere: values.boundingSphere.ref.value,
+        invariantBoundingSphere: values.invariantBoundingSphere.ref.value,
+        itemSize: 3
+    }, resolution, stride, webgl, colorTexture);
+    if (smoothingData.kind === 'volume') {
+        ValueCell.updateIfChanged(values.dSubstanceType, smoothingData.type);
+        ValueCell.update(values.tSubstanceGrid, smoothingData.texture);
+        ValueCell.update(values.uSubstanceTexDim, smoothingData.gridTexDim);
+        ValueCell.update(values.uSubstanceGridDim, smoothingData.gridDim);
+        ValueCell.update(values.uSubstanceGridTransform, smoothingData.gridTransform);
+    } else if (smoothingData.kind === 'vertex') {
+        ValueCell.updateIfChanged(values.dSubstanceType, smoothingData.type);
+        ValueCell.update(values.tSubstance, smoothingData.texture);
+        ValueCell.update(values.uSubstanceTexDim, smoothingData.texDim);
+    }
+}

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

@@ -27,6 +27,7 @@ import { createEmptyClipping } from '../clipping-data';
 import { RenderableState } from '../../../mol-gl/renderable';
 import { arraySetAdd } from '../../../mol-util/array';
 import { degToRad } from '../../../mol-math/misc';
+import { createEmptySubstance } from '../substance-data';
 
 export interface Mesh {
     readonly kind: 'mesh',
@@ -665,6 +666,7 @@ export namespace Mesh {
         const marker = createMarkers(instanceCount * groupCount);
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
+        const material = createEmptySubstance();
         const clipping = createEmptyClipping();
 
         const counts = { drawCount: mesh.triangleCount * 3, vertexCount: mesh.vertexCount, groupCount, instanceCount };
@@ -684,6 +686,7 @@ export namespace Mesh {
             ...marker,
             ...overpaint,
             ...transparency,
+            ...material,
             ...clipping,
             ...transform,
 

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

@@ -25,6 +25,7 @@ import { createEmptyOverpaint } from '../overpaint-data';
 import { createEmptyTransparency } from '../transparency-data';
 import { hashFnv32a } from '../../../mol-data/util';
 import { createEmptyClipping } from '../clipping-data';
+import { createEmptySubstance } from '../substance-data';
 
 /** Point cloud */
 export interface Points {
@@ -172,6 +173,7 @@ export namespace Points {
         const marker = createMarkers(instanceCount * groupCount);
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
+        const material = createEmptySubstance();
         const clipping = createEmptyClipping();
 
         const counts = { drawCount: points.pointCount, vertexCount: points.pointCount, groupCount, instanceCount };
@@ -190,6 +192,7 @@ export namespace Points {
             ...marker,
             ...overpaint,
             ...transparency,
+            ...material,
             ...clipping,
             ...transform,
 

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

@@ -25,6 +25,7 @@ import { GroupMapping, createGroupMapping } from '../../util';
 import { createEmptyClipping } from '../clipping-data';
 import { Vec3, Vec4 } from '../../../mol-math/linear-algebra';
 import { RenderableState } from '../../../mol-gl/renderable';
+import { createEmptySubstance } from '../substance-data';
 
 export interface Spheres {
     readonly kind: 'spheres',
@@ -170,6 +171,7 @@ export namespace Spheres {
         const marker = createMarkers(instanceCount * groupCount);
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
+        const material = createEmptySubstance();
         const clipping = createEmptyClipping();
 
         const counts = { drawCount: spheres.sphereCount * 2 * 3, vertexCount: spheres.sphereCount * 4, groupCount, instanceCount };
@@ -191,6 +193,7 @@ export namespace Spheres {
             ...marker,
             ...overpaint,
             ...transparency,
+            ...material,
             ...clipping,
             ...transform,
 

+ 76 - 0
src/mol-geo/geometry/substance-data.ts

@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ValueCell } from '../../mol-util/value-cell';
+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';
+import { Material } from '../../mol-util/material';
+
+export type SubstanceData = {
+    tSubstance: ValueCell<TextureImage<Uint8Array>>
+    uSubstanceTexDim: ValueCell<Vec2>
+    dSubstance: ValueCell<boolean>,
+
+    tSubstanceGrid: ValueCell<Texture>,
+    uSubstanceGridDim: ValueCell<Vec3>,
+    uSubstanceGridTransform: ValueCell<Vec4>,
+    dSubstanceType: ValueCell<string>,
+}
+
+export function applySubstanceMaterial(array: Uint8Array, start: number, end: number, material: Material) {
+    for (let i = start; i < end; ++i) {
+        Material.toArray(material, array, i * 3);
+        array[i * 3 + 2] = 255;
+    }
+    return true;
+}
+
+export function clearSubstance(array: Uint8Array, start: number, end: number) {
+    array.fill(0, start * 3, end * 3);
+    return true;
+}
+
+export function createSubstance(count: number, 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);
+        return substanceData;
+    } else {
+        return {
+            tSubstance: ValueCell.create(substance),
+            uSubstanceTexDim: ValueCell.create(Vec2.create(substance.width, substance.height)),
+            dSubstance: ValueCell.create(count > 0),
+
+            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'),
+        };
+    }
+}
+
+const emptySubstanceTexture = { array: new Uint8Array(4), width: 1, height: 1 };
+export function createEmptySubstance(substanceData?: SubstanceData): SubstanceData {
+    if (substanceData) {
+        ValueCell.update(substanceData.tSubstance, emptySubstanceTexture);
+        ValueCell.update(substanceData.uSubstanceTexDim, Vec2.create(1, 1));
+        return substanceData;
+    } else {
+        return {
+            tSubstance: ValueCell.create(emptySubstanceTexture),
+            uSubstanceTexDim: ValueCell.create(Vec2.create(1, 1)),
+            dSubstance: ValueCell.create(false),
+
+            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'),
+        };
+    }
+}

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

@@ -29,6 +29,7 @@ import { createEmptyTransparency } from '../transparency-data';
 import { hashFnv32a } from '../../../mol-data/util';
 import { GroupMapping, createGroupMapping } from '../../util';
 import { createEmptyClipping } from '../clipping-data';
+import { createEmptySubstance } from '../substance-data';
 
 type TextAttachment = (
     'bottom-left' | 'bottom-center' | 'bottom-right' |
@@ -213,6 +214,7 @@ export namespace Text {
         const marker = createMarkers(instanceCount * groupCount);
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
+        const substance = createEmptySubstance();
         const clipping = createEmptyClipping();
 
         const counts = { drawCount: text.charCount * 2 * 3, vertexCount: text.charCount * 4, groupCount, instanceCount };
@@ -235,6 +237,7 @@ export namespace Text {
             ...marker,
             ...overpaint,
             ...transparency,
+            ...substance,
             ...clipping,
             ...transform,
 

+ 36 - 0
src/mol-geo/geometry/texture-mesh/color-smoothing.ts

@@ -500,3 +500,39 @@ export function applyTextureMeshTransparencySmoothing(values: TextureMeshValues,
     ValueCell.update(values.uTransparencyGridDim, smoothingData.gridDim);
     ValueCell.update(values.uTransparencyGridTransform, smoothingData.gridTransform);
 }
+
+function isSupportedSubstanceType(x: string): x is 'groupInstance' {
+    return x === 'groupInstance';
+}
+
+export function applyTextureMeshSubstanceSmoothing(values: TextureMeshValues, resolution: number, stride: number, webgl: WebGLContext, colorTexture?: Texture) {
+    if (!isSupportedSubstanceType(values.dSubstanceType.ref.value)) return;
+
+    stride *= 3; // triple because TextureMesh is never indexed (no elements buffer)
+
+    if (!webgl.namedTextures[ColorSmoothingRgbName]) {
+        webgl.namedTextures[ColorSmoothingRgbName] = webgl.resources.texture('image-uint8', 'rgb', 'ubyte', 'nearest');
+    }
+    const colorData = webgl.namedTextures[ColorSmoothingRgbName];
+    colorData.load(values.tSubstance.ref.value);
+
+    const smoothingData = calcTextureMeshColorSmoothing({
+        vertexCount: values.uVertexCount.ref.value,
+        instanceCount: values.uInstanceCount.ref.value,
+        groupCount: values.uGroupCount.ref.value,
+        transformBuffer: values.aTransform.ref.value,
+        instanceBuffer: values.aInstance.ref.value,
+        positionTexture: values.tPosition.ref.value,
+        groupTexture: values.tGroup.ref.value,
+        colorData,
+        colorType: values.dSubstanceType.ref.value,
+        boundingSphere: values.boundingSphere.ref.value,
+        invariantBoundingSphere: values.invariantBoundingSphere.ref.value,
+    }, resolution, stride, webgl, colorTexture);
+
+    ValueCell.updateIfChanged(values.dSubstanceType, smoothingData.type);
+    ValueCell.update(values.tSubstanceGrid, smoothingData.texture);
+    ValueCell.update(values.uSubstanceTexDim, smoothingData.gridTexDim);
+    ValueCell.update(values.uSubstanceGridDim, smoothingData.gridDim);
+    ValueCell.update(values.uSubstanceGridTransform, smoothingData.gridTransform);
+}

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

@@ -23,6 +23,7 @@ import { createNullTexture, Texture } from '../../../mol-gl/webgl/texture';
 import { Vec2, Vec4 } from '../../../mol-math/linear-algebra';
 import { createEmptyClipping } from '../clipping-data';
 import { NullLocation } from '../../../mol-model/location';
+import { createEmptySubstance } from '../substance-data';
 
 export interface TextureMesh {
     readonly kind: 'texture-mesh',
@@ -135,6 +136,7 @@ export namespace TextureMesh {
         const marker = createMarkers(instanceCount * groupCount);
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
+        const substance = createEmptySubstance();
         const clipping = createEmptyClipping();
 
         const counts = { drawCount: textureMesh.vertexCount, vertexCount: textureMesh.vertexCount, groupCount, instanceCount };
@@ -156,6 +158,7 @@ export namespace TextureMesh {
             ...marker,
             ...overpaint,
             ...transparency,
+            ...substance,
             ...clipping,
             ...transform,
 

+ 1 - 2
src/mol-gl/renderable/points.ts

@@ -10,7 +10,6 @@ import { createGraphicsRenderItem } from '../webgl/render-item';
 import { GlobalUniformSchema, BaseSchema, AttributeSpec, DefineSpec, Values, InternalSchema, SizeSchema, InternalValues, GlobalTextureSchema } from './schema';
 import { PointsShaderCode } from '../shader-code';
 import { ValueCell } from '../../mol-util';
-import { Points } from '../../mol-geo/geometry/points/points';
 
 export const PointsSchema = {
     ...BaseSchema,
@@ -18,7 +17,7 @@ export const PointsSchema = {
     aGroup: AttributeSpec('float32', 1, 0),
     aPosition: AttributeSpec('float32', 3, 0),
     dPointSizeAttenuation: DefineSpec('boolean'),
-    dPointStyle: DefineSpec('string', Points.StyleTypeNames),
+    dPointStyle: DefineSpec('string', ['square', 'circle', 'fuzzy']),
 };
 export type PointsSchema = typeof PointsSchema
 export type PointsValues = Values<PointsSchema>

+ 17 - 8
src/mol-gl/renderable/schema.ts

@@ -241,6 +241,19 @@ export const TransparencySchema = {
 export type TransparencySchema = typeof TransparencySchema
 export type TransparencyValues = Values<TransparencySchema>
 
+export const SubstanceSchema = {
+    uSubstanceTexDim: UniformSpec('v2'),
+    tSubstance: TextureSpec('image-uint8', 'rgb', 'ubyte', 'nearest'),
+    dSubstance: DefineSpec('boolean'),
+
+    uSubstanceGridDim: UniformSpec('v3'),
+    uSubstanceGridTransform: UniformSpec('v4'),
+    tSubstanceGrid: TextureSpec('texture', 'rgb', 'ubyte', 'linear'),
+    dSubstanceType: DefineSpec('string', ['groupInstance', 'volumeInstance']),
+} as const;
+export type SubstanceSchema = typeof SubstanceSchema
+export type SubstanceValues = Values<SubstanceSchema>
+
 export const ClippingSchema = {
     dClipObjectCount: DefineSpec('number'),
     dClipVariant: DefineSpec('string', ['instance', 'pixel']),
@@ -252,20 +265,13 @@ export const ClippingSchema = {
 export type ClippingSchema = typeof ClippingSchema
 export type ClippingValues = Values<ClippingSchema>
 
-export const MaterialSchema = {
-    uMetalness: UniformSpec('f'),
-    uRoughness: UniformSpec('f'),
-} as const;
-export type MaterialSchema = typeof MaterialSchema
-export type MaterialValues = Values<MaterialSchema>
-
 export const BaseSchema = {
     ...ColorSchema,
     ...MarkerSchema,
     ...OverpaintSchema,
     ...TransparencySchema,
+    ...SubstanceSchema,
     ...ClippingSchema,
-    ...MaterialSchema,
 
     dLightCount: DefineSpec('number'),
 
@@ -280,6 +286,9 @@ export const BaseSchema = {
      * final alpha, calculated as `values.alpha * state.alpha`
      */
     uAlpha: UniformSpec('f', 'material'),
+    uMetalness: UniformSpec('f', 'material'),
+    uRoughness: UniformSpec('f', 'material'),
+
     uVertexCount: UniformSpec('i'),
     uInstanceCount: UniformSpec('i'),
     uGroupCount: UniformSpec('i'),

+ 6 - 10
src/mol-gl/shader/chunks/apply-light-color.glsl.ts

@@ -25,13 +25,9 @@ vec4 color = material;
 ReflectedLight reflectedLight = ReflectedLight(vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0));
 
 PhysicalMaterial physicalMaterial;
-physicalMaterial.diffuseColor = color.rgb * (1.0 - uMetalness);
-vec3 dxy = max(abs(dFdx(normal)), abs(dFdy(normal)));
-float geometryRoughness = max(max(dxy.x, dxy.y), dxy.z);
-physicalMaterial.roughness = max(uRoughness, 0.0525);
-physicalMaterial.roughness += geometryRoughness;
-physicalMaterial.roughness = min(physicalMaterial.roughness, 1.0);
-physicalMaterial.specularColor = mix(vec3( 0.04 ), color.rgb, uMetalness);
+physicalMaterial.diffuseColor = color.rgb * (1.0 - metalness);
+physicalMaterial.roughness = max(roughness, 0.0525);
+physicalMaterial.specularColor = mix(vec3(0.04), color.rgb, metalness);
 physicalMaterial.specularF90 = 1.0;
 
 GeometricContext geometry;
@@ -52,8 +48,8 @@ vec3 irradiance = uAmbientColor * PI; // * PI for punctual light
 RE_IndirectDiffuse_Physical(irradiance, geometry, physicalMaterial, reflectedLight);
 
 // indirect specular only metals
-vec3 radiance = uAmbientColor * uMetalness;
-vec3 iblIrradiance = uAmbientColor * uMetalness;
+vec3 radiance = uAmbientColor * metalness;
+vec3 iblIrradiance = uAmbientColor * metalness;
 vec3 clearcoatRadiance = vec3(0.0);
 RE_IndirectSpecular_Physical(radiance, iblIrradiance, clearcoatRadiance, geometry, physicalMaterial, reflectedLight);
 
@@ -62,6 +58,6 @@ vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffu
 gl_FragColor = vec4(outgoingLight, color.a);
 
 #ifdef dXrayShaded
-    gl_FragColor.a *= 1.0 - pow(abs(dot(normal, vec3(0, 0, 1))), uXrayEdgeFalloff);
+    gl_FragColor.a *= 1.0 - pow(abs(dot(normal, vec3(0.0, 0.0, 1.0))), uXrayEdgeFalloff);
 #endif
 `;

+ 14 - 0
src/mol-gl/shader/chunks/assign-color-varying.glsl.ts

@@ -41,6 +41,20 @@ export const assign_color_varying = `
             vOverpaint.rgb = mix(vColor.rgb, vOverpaint.rgb, vOverpaint.a);
         #endif
     #endif
+
+    #ifdef dSubstance
+        #if defined(dSubstanceType_groupInstance)
+            vSubstance = readFromTexture(tSubstance, aInstance * float(uGroupCount) + group, uSubstanceTexDim).rgb;
+        #elif defined(dSubstanceType_vertexInstance)
+            vSubstance = readFromTexture(tSubstance, int(aInstance) * uVertexCount + VertexID, uSubstanceTexDim).rgb;
+        #elif defined(dSubstanceType_volumeInstance)
+            vec3 sgridPos = (uSubstanceGridTransform.w * (vModelPosition - uSubstanceGridTransform.xyz)) / uSubstanceGridDim;
+            vSubstance = texture3dFrom2dLinear(tSubstanceGrid, sgridPos, uSubstanceGridDim, uSubstanceTexDim).rgb;
+        #endif
+
+        // pre-mix to avoid artifacts due to empty substance
+        vSubstance.rg = mix(vec2(uMetalness, uRoughness), vSubstance.rg, vSubstance.b);
+    #endif
 #elif defined(dRenderVariant_pick)
     #if defined(dRenderVariant_pickObject)
         vColor = vec4(encodeFloatRGB(float(uObjectId)), 1.0);

+ 7 - 0
src/mol-gl/shader/chunks/assign-material-color.glsl.ts

@@ -20,6 +20,13 @@ export const assign_material_color = `
     #if defined(dOverpaint)
         material.rgb = mix(material.rgb, vOverpaint.rgb, vOverpaint.a);
     #endif
+
+    float metalness = uMetalness;
+    float roughness = uRoughness;
+    #ifdef dSubstance
+        metalness = mix(metalness, vSubstance.r, vSubstance.b);
+        roughness = mix(roughness, vSubstance.g, vSubstance.b);
+    #endif
 #elif defined(dRenderVariant_pick)
     vec4 material = vColor;
 #elif defined(dRenderVariant_depth)

+ 7 - 0
src/mol-gl/shader/chunks/color-frag-params.glsl.ts

@@ -1,4 +1,7 @@
 export const color_frag_params = `
+uniform float uMetalness;
+uniform float uRoughness;
+
 #if defined(dRenderVariant_color)
     #if defined(dColorType_uniform)
         uniform vec3 uColor;
@@ -9,6 +12,10 @@ export const color_frag_params = `
     #ifdef dOverpaint
         varying vec4 vOverpaint;
     #endif
+
+    #ifdef dSubstance
+        varying vec3 vSubstance;
+    #endif
 #elif defined(dRenderVariant_pick)
     #if __VERSION__ == 100
         varying vec4 vColor;

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

@@ -1,4 +1,7 @@
 export const color_vert_params = `
+uniform float uMetalness;
+uniform float uRoughness;
+
 #if defined(dRenderVariant_color)
     #if defined(dColorType_uniform)
         uniform vec3 uColor;
@@ -30,6 +33,20 @@ export const color_vert_params = `
             uniform sampler2D tOverpaintGrid;
         #endif
     #endif
+
+    #ifdef dSubstance
+        #if defined(dSubstanceType_groupInstance) || defined(dSubstanceType_vertexInstance)
+            varying vec3 vSubstance;
+            uniform vec2 uSubstanceTexDim;
+            uniform sampler2D tSubstance;
+        #elif defined(dSubstanceType_volumeInstance)
+            varying vec3 vSubstance;
+            uniform vec2 uSubstanceTexDim;
+            uniform vec3 uSubstanceGridDim;
+            uniform vec4 uSubstanceGridTransform;
+            uniform sampler2D tSubstanceGrid;
+        #endif
+    #endif
 #elif defined(dRenderVariant_pick)
     #if __VERSION__ == 100
         varying vec4 vColor;

+ 0 - 4
src/mol-gl/shader/chunks/light-frag-params.glsl.ts

@@ -12,10 +12,6 @@ uniform vec3 uLightDirection[dLightCount];
 uniform vec3 uLightColor[dLightCount];
 uniform vec3 uAmbientColor;
 
-uniform float uReflectivity;
-uniform float uMetalness;
-uniform float uRoughness;
-
 struct PhysicalMaterial {
     vec3 diffuseColor;
     float roughness;

+ 23 - 1
src/mol-gl/shader/direct-volume.frag.ts

@@ -64,6 +64,9 @@ uniform int uMarkerPriority;
     uniform sampler2D tMarker;
 #endif
 
+uniform float uMetalness;
+uniform float uRoughness;
+
 uniform float uFogNear;
 uniform float uFogFar;
 uniform vec3 uFogColor;
@@ -115,6 +118,13 @@ uniform mat4 uCartnToUnit;
             uniform sampler2D tOverpaint;
         #endif
     #endif
+
+    #ifdef dSubstance
+        #if defined(dSubstanceType_groupInstance) || defined(dSubstanceType_vertexInstance)
+            uniform vec2 uSubstanceTexDim;
+            uniform sampler2D tSubstance;
+        #endif
+    #endif
 #endif
 
 #if defined(dGridTexType_2d)
@@ -194,6 +204,9 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 rayDir) {
 
     vec3 color = vec3(0.45, 0.55, 0.8);
     vec4 overpaint = vec4(0.0);
+    vec3 substance = vec3(0.0);
+    float metalness = uMetalness;
+    float roughness = uRoughness;
 
     vec3 gradient = vec3(1.0);
     vec3 dx = vec3(gradOffset * scaleVol.x, 0.0, 0.0);
@@ -304,7 +317,7 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 rayDir) {
                         #if defined(dOverpaintType_groupInstance)
                             overpaint = readFromTexture(tOverpaint, vInstance * float(uGroupCount) + group, uOverpaintTexDim);
                         #elif defined(dOverpaintType_vertexInstance)
-                            overpaint = texture3dFrom1dTrilinear(tOverpaint, isoPos, uGridDim, uOverpaintTexDim, vInstance * float(uVertexCount)).rgb;
+                            overpaint = texture3dFrom1dTrilinear(tOverpaint, isoPos, uGridDim, uOverpaintTexDim, vInstance * float(uVertexCount));
                         #endif
 
                         color = mix(color, overpaint.rgb, overpaint.a);
@@ -345,6 +358,15 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 rayDir) {
                         vec3 normal = -normalize(normalMatrix * normalize(gradient));
                         normal = normal * (float(flipped) * 2.0 - 1.0);
                         normal = normal * -(float(interior) * 2.0 - 1.0);
+                        #ifdef dSubstance
+                            #if defined(dSubstanceType_groupInstance)
+                                substance = readFromTexture(tSubstance, vInstance * float(uGroupCount) + group, uSubstanceTexDim).rgb;
+                            #elif defined(dSubstanceType_vertexInstance)
+                                substance = texture3dFrom1dTrilinear(tSubstance, isoPos, uGridDim, uSubstanceTexDim, vInstance * float(uVertexCount)).rgb;
+                            #endif
+                            metalness = mix(metalness, substance.r, substance.b);
+                            roughness = mix(roughness, substance.g, substance.b);
+                        #endif
                         #include apply_light_color
                     #endif
 

+ 76 - 0
src/mol-plugin-state/helpers/structure-substance.ts

@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Structure, StructureElement } from '../../mol-model/structure';
+import { PluginStateObject } from '../../mol-plugin-state/objects';
+import { StateTransforms } from '../../mol-plugin-state/transforms';
+import { PluginContext } from '../../mol-plugin/context';
+import { StateBuilder, StateObjectCell, StateSelection, StateTransform } from '../../mol-state';
+import { Substance } from '../../mol-theme/substance';
+import { StructureComponentRef } from '../manager/structure/hierarchy-state';
+import { EmptyLoci, isEmptyLoci, Loci } from '../../mol-model/loci';
+import { Material } from '../../mol-util/material';
+
+type SubstanceEachReprCallback = (update: StateBuilder.Root, repr: StateObjectCell<PluginStateObject.Molecule.Structure.Representation3D, StateTransform<typeof StateTransforms.Representation.StructureRepresentation3D>>, substance?: StateObjectCell<any, StateTransform<typeof StateTransforms.Representation.SubstanceStructureRepresentation3DFromBundle>>) => Promise<void>
+const SubstanceManagerTag = 'substance-controls';
+
+export async function setStructureSubstance(plugin: PluginContext, components: StructureComponentRef[], material: Material | -1, lociGetter: (structure: Structure) => Promise<StructureElement.Loci | EmptyLoci>, types?: string[]) {
+    await eachRepr(plugin, components, async (update, repr, substanceCell) => {
+        if (types && types.length > 0 && !types.includes(repr.params!.values.type.name)) return;
+
+        const structure = repr.obj!.data.sourceData;
+        // always use the root structure to get the loci so the substance
+        // stays applicable as long as the root structure does not change
+        const loci = await lociGetter(structure.root);
+        if (Loci.isEmpty(loci) || isEmptyLoci(loci)) return;
+
+        const layer = {
+            bundle: StructureElement.Bundle.fromLoci(loci),
+            material: material === -1 ? Material(0) : material,
+            clear: material === -1
+        };
+
+        if (substanceCell) {
+            const bundleLayers = [...substanceCell.params!.values.layers, layer];
+            const filtered = getFilteredBundle(bundleLayers, structure);
+            update.to(substanceCell).update(Substance.toBundle(filtered));
+        } else {
+            const filtered = getFilteredBundle([layer], structure);
+            update.to(repr.transform.ref)
+                .apply(StateTransforms.Representation.SubstanceStructureRepresentation3DFromBundle, Substance.toBundle(filtered), { tags: SubstanceManagerTag });
+        }
+    });
+}
+
+export async function clearStructureSubstance(plugin: PluginContext, components: StructureComponentRef[], types?: string[]) {
+    await eachRepr(plugin, components, async (update, repr, substanceCell) => {
+        if (types && types.length > 0 && !types.includes(repr.params!.values.type.name)) return;
+        if (substanceCell) {
+            update.delete(substanceCell.transform.ref);
+        }
+    });
+}
+
+async function eachRepr(plugin: PluginContext, components: StructureComponentRef[], callback: SubstanceEachReprCallback) {
+    const state = plugin.state.data;
+    const update = state.build();
+    for (const c of components) {
+        for (const r of c.representations) {
+            const substance = state.select(StateSelection.Generators.ofTransformer(StateTransforms.Representation.SubstanceStructureRepresentation3DFromBundle, r.cell.transform.ref).withTag(SubstanceManagerTag));
+            await callback(update, r.cell, substance[0]);
+        }
+    }
+
+    return update.commit({ doNotUpdateCurrent: true });
+}
+
+/** filter substance layers for given structure */
+function getFilteredBundle(layers: Substance.BundleLayer[], structure: Structure) {
+    const substance = Substance.ofBundle(layers, structure.root);
+    const merged = Substance.merge(substance);
+    return Substance.filter(merged, structure);
+}

+ 14 - 4
src/mol-plugin-state/manager/structure/component.ts

@@ -30,6 +30,7 @@ import { Clipping } from '../../../mol-theme/clipping';
 import { setStructureClipping } from '../../helpers/structure-clipping';
 import { setStructureTransparency } from '../../helpers/structure-transparency';
 import { StructureFocusRepresentation } from '../../../mol-plugin/behavior/dynamic/selection/structure-focus-representation';
+import { setStructureSubstance } from '../../helpers/structure-substance';
 import { Material } from '../../../mol-util/material';
 
 export { StructureComponentManager };
@@ -375,14 +376,19 @@ class StructureComponentManager extends StatefulPluginComponent<StructureCompone
 
             const getLoci = async (s: Structure) => StructureSelection.toLociWithSourceUnits(await params.selection.getSelection(this.plugin, ctx, s));
             for (const s of xs) {
-                if (params.action.name === 'reset') {
-                    await setStructureOverpaint(this.plugin, s.components, -1, getLoci, params.representations);
-                } else if (params.action.name === 'color') {
+                if (params.action.name === 'color') {
                     const p = params.action.params;
                     await setStructureOverpaint(this.plugin, s.components, p.color, getLoci, params.representations);
+                } else if (params.action.name === 'resetColor') {
+                    await setStructureOverpaint(this.plugin, s.components, -1, getLoci, params.representations);
                 } else if (params.action.name === 'transparency') {
                     const p = params.action.params;
                     await setStructureTransparency(this.plugin, s.components, p.value, getLoci, params.representations);
+                } else if (params.action.name === 'material') {
+                    const p = params.action.params;
+                    await setStructureSubstance(this.plugin, s.components, p.material, getLoci, params.representations);
+                } else if (params.action.name === 'resetMaterial') {
+                    await setStructureSubstance(this.plugin, s.components, -1, getLoci, params.representations);
                 } else if (params.action.name === 'clipping') {
                     const p = params.action.params;
                     await setStructureClipping(this.plugin, s.components, Clipping.Groups.fromNames(p.excludeGroups), getLoci, params.representations);
@@ -481,10 +487,14 @@ namespace StructureComponentManager {
                 color: PD.Group({
                     color: PD.Color(ColorNames.blue, { isExpanded: true }),
                 }, { isFlat: true }),
-                reset: PD.EmptyGroup({ label: 'Reset Color' }),
+                resetColor: PD.EmptyGroup({ label: 'Reset Color' }),
                 transparency: PD.Group({
                     value: PD.Numeric(0.5, { min: 0, max: 1, step: 0.01 }),
                 }, { isFlat: true }),
+                material: PD.Group({
+                    material: Material.getParam({ isFlat: true }),
+                }, { isFlat: true }),
+                resetMaterial: PD.EmptyGroup({ label: 'Reset Material' }),
                 clipping: PD.Group({
                     excludeGroups: PD.MultiSelect([] as Clipping.Groups.Names[], PD.objectToOptions(Clipping.Groups.Names)),
                 }, { isFlat: true }),

+ 119 - 0
src/mol-plugin-state/transforms/representation.ts

@@ -41,6 +41,8 @@ import { getBoxMesh } from './shape';
 import { Shape } from '../../mol-model/shape';
 import { Box3D } from '../../mol-math/geometry';
 import { PlaneParams, PlaneRepresentation } from '../../mol-repr/shape/loci/plane';
+import { Substance } from '../../mol-theme/substance';
+import { Material } from '../../mol-util/material';
 
 export { StructureRepresentation3D };
 export { ExplodeStructureRepresentation3D };
@@ -50,6 +52,8 @@ export { OverpaintStructureRepresentation3DFromScript };
 export { OverpaintStructureRepresentation3DFromBundle };
 export { TransparencyStructureRepresentation3DFromScript };
 export { TransparencyStructureRepresentation3DFromBundle };
+export { SubstanceStructureRepresentation3DFromScript };
+export { SubstanceStructureRepresentation3DFromBundle };
 export { ClippingStructureRepresentation3DFromScript };
 export { ClippingStructureRepresentation3DFromBundle };
 export { VolumeRepresentation3D };
@@ -529,6 +533,121 @@ const TransparencyStructureRepresentation3DFromBundle = PluginStateTransform.Bui
     }
 });
 
+type SubstanceStructureRepresentation3DFromScript = typeof SubstanceStructureRepresentation3DFromScript
+const SubstanceStructureRepresentation3DFromScript = PluginStateTransform.BuiltIn({
+    name: 'substance-structure-representation-3d-from-script',
+    display: 'Substance 3D Representation',
+    from: SO.Molecule.Structure.Representation3D,
+    to: SO.Molecule.Structure.Representation3DState,
+    params: () => ({
+        layers: PD.ObjectList({
+            script: PD.Script(Script('(sel.atom.all)', 'mol-script')),
+            material: Material.getParam(),
+            clear: PD.Boolean(false)
+        }, e => `${e.clear ? 'Clear' : Material.toString(e.material)}`, {
+            defaultValue: [{
+                script: Script('(sel.atom.all)', 'mol-script'),
+                material: Material.fromNormalized(0, 1),
+                clear: false
+            }]
+        }),
+    })
+})({
+    canAutoUpdate() {
+        return true;
+    },
+    apply({ a, params }) {
+        const structure = a.data.sourceData;
+        const geometryVersion = a.data.repr.geometryVersion;
+        const substance = Substance.ofScript(params.layers, structure);
+
+        return new SO.Molecule.Structure.Representation3DState({
+            state: { substance },
+            initialState: { substance: Substance.Empty },
+            info: { structure, geometryVersion },
+            repr: a.data.repr
+        }, { label: `Substance (${substance.layers.length} Layers)` });
+    },
+    update({ a, b, newParams, oldParams }) {
+        const info = b.data.info as { structure: Structure, geometryVersion: number };
+        const newStructure = a.data.sourceData;
+        if (newStructure !== info.structure) return StateTransformer.UpdateResult.Recreate;
+        if (a.data.repr !== b.data.repr) return StateTransformer.UpdateResult.Recreate;
+
+        const newGeometryVersion = a.data.repr.geometryVersion;
+        // smoothing needs to be re-calculated when geometry changes
+        if (newGeometryVersion !== info.geometryVersion && hasColorSmoothingProp(a.data.repr.props)) return StateTransformer.UpdateResult.Unchanged;
+
+        const oldSubstance = b.data.state.substance!;
+        const newSubstance = Substance.ofScript(newParams.layers, newStructure);
+        if (Substance.areEqual(oldSubstance, newSubstance)) return StateTransformer.UpdateResult.Unchanged;
+
+        info.geometryVersion = newGeometryVersion;
+        b.data.state.substance = newSubstance;
+        b.data.repr = a.data.repr;
+        b.label = `Substance (${newSubstance.layers.length} Layers)`;
+        return StateTransformer.UpdateResult.Updated;
+    }
+});
+
+type SubstanceStructureRepresentation3DFromBundle = typeof SubstanceStructureRepresentation3DFromBundle
+const SubstanceStructureRepresentation3DFromBundle = PluginStateTransform.BuiltIn({
+    name: 'substance-structure-representation-3d-from-bundle',
+    display: 'Substance 3D Representation',
+    from: SO.Molecule.Structure.Representation3D,
+    to: SO.Molecule.Structure.Representation3DState,
+    params: () => ({
+        layers: PD.ObjectList({
+            bundle: PD.Value<StructureElement.Bundle>(StructureElement.Bundle.Empty),
+            material: Material.getParam(),
+            clear: PD.Boolean(false)
+        }, e => `${e.clear ? 'Clear' : Material.toString(e.material)}`, {
+            defaultValue: [{
+                bundle: StructureElement.Bundle.Empty,
+                material: Material.fromNormalized(0, 1),
+                clear: false
+            }],
+            isHidden: true
+        }),
+    })
+})({
+    canAutoUpdate() {
+        return true;
+    },
+    apply({ a, params }) {
+        const structure = a.data.sourceData;
+        const geometryVersion = a.data.repr.geometryVersion;
+        const substance = Substance.ofBundle(params.layers, structure);
+
+        return new SO.Molecule.Structure.Representation3DState({
+            state: { substance },
+            initialState: { substance: Substance.Empty },
+            info: { structure, geometryVersion },
+            repr: a.data.repr
+        }, { label: `Substance (${substance.layers.length} Layers)` });
+    },
+    update({ a, b, newParams, oldParams }) {
+        const info = b.data.info as { structure: Structure, geometryVersion: number };
+        const newStructure = a.data.sourceData;
+        if (newStructure !== info.structure) return StateTransformer.UpdateResult.Recreate;
+        if (a.data.repr !== b.data.repr) return StateTransformer.UpdateResult.Recreate;
+
+        const newGeometryVersion = a.data.repr.geometryVersion;
+        // smoothing needs to be re-calculated when geometry changes
+        if (newGeometryVersion !== info.geometryVersion && hasColorSmoothingProp(a.data.repr.props)) return StateTransformer.UpdateResult.Unchanged;
+
+        const oldSubstance = b.data.state.substance!;
+        const newSubstance = Substance.ofBundle(newParams.layers, newStructure);
+        if (Substance.areEqual(oldSubstance, newSubstance)) return StateTransformer.UpdateResult.Unchanged;
+
+        info.geometryVersion = newGeometryVersion;
+        b.data.state.substance = newSubstance;
+        b.data.repr = a.data.repr;
+        b.label = `Substance (${newSubstance.layers.length} Layers)`;
+        return StateTransformer.UpdateResult.Updated;
+    }
+});
+
 type ClippingStructureRepresentation3DFromScript = typeof ClippingStructureRepresentation3DFromScript
 const ClippingStructureRepresentation3DFromScript = PluginStateTransform.BuiltIn({
     name: 'clipping-structure-representation-3d-from-script',

+ 1 - 0
src/mol-plugin/spec.ts

@@ -101,6 +101,7 @@ export const DefaultPluginSpec = (): PluginSpec => ({
         PluginSpec.Action(StateTransforms.Representation.UnwindStructureAssemblyRepresentation3D),
         PluginSpec.Action(StateTransforms.Representation.OverpaintStructureRepresentation3DFromScript),
         PluginSpec.Action(StateTransforms.Representation.TransparencyStructureRepresentation3DFromScript),
+        PluginSpec.Action(StateTransforms.Representation.SubstanceStructureRepresentation3DFromScript),
 
         PluginSpec.Action(AssignColorVolume),
         PluginSpec.Action(StateTransforms.Volume.VolumeFromCcp4),

+ 8 - 1
src/mol-repr/representation.ts

@@ -25,6 +25,7 @@ import { CustomProperty } from '../mol-model-props/common/custom-property';
 import { Clipping } from '../mol-theme/clipping';
 import { SetUtils } from '../mol-util/set';
 import { cantorPairing } from '../mol-data/util';
+import { Substance } from '../mol-theme/substance';
 
 export type RepresentationProps = { [k: string]: any }
 
@@ -186,6 +187,8 @@ namespace Representation {
         overpaint: Overpaint
         /** Per group transparency applied to the representation's renderobjects */
         transparency: Transparency
+        /** Per group material applied to the representation's renderobjects */
+        substance: Substance
         /** Bit mask of per group clipping applied to the representation's renderobjects */
         clipping: Clipping
         /** Controls if the representation's renderobjects are synced automatically with GPU or not */
@@ -196,7 +199,7 @@ namespace Representation {
         markerActions: MarkerActions
     }
     export function createState(): State {
-        return { visible: true, alphaFactor: 1, pickable: true, colorOnly: false, syncManually: false, transform: Mat4.identity(), overpaint: Overpaint.Empty, transparency: Transparency.Empty, clipping: Clipping.Empty, markerActions: MarkerActions.All };
+        return { visible: true, alphaFactor: 1, pickable: true, colorOnly: false, syncManually: false, transform: Mat4.identity(), overpaint: Overpaint.Empty, transparency: Transparency.Empty, substance: Substance.Empty, clipping: Clipping.Empty, markerActions: MarkerActions.All };
     }
     export function updateState(state: State, update: Partial<State>) {
         if (update.visible !== undefined) state.visible = update.visible;
@@ -205,6 +208,7 @@ namespace Representation {
         if (update.colorOnly !== undefined) state.colorOnly = update.colorOnly;
         if (update.overpaint !== undefined) state.overpaint = update.overpaint;
         if (update.transparency !== undefined) state.transparency = update.transparency;
+        if (update.substance !== undefined) state.substance = update.substance;
         if (update.clipping !== undefined) state.clipping = update.clipping;
         if (update.syncManually !== undefined) state.syncManually = update.syncManually;
         if (update.transform !== undefined) Mat4.copy(state.transform, update.transform);
@@ -410,6 +414,9 @@ namespace Representation {
                 if (state.transparency !== undefined) {
                     // TODO
                 }
+                if (state.substance !== undefined) {
+                    // TODO
+                }
                 if (state.transform !== undefined) Visual.setTransform(renderObject, state.transform);
 
                 Representation.updateState(currentState, state);

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

@@ -216,6 +216,9 @@ export function ShapeRepresentation<D, G extends Geometry, P extends Geometry.Pa
                 if (state.transparency !== undefined) {
                     Visual.setTransparency(_renderObject, state.transparency, lociApply, true);
                 }
+                if (state.substance !== undefined) {
+                    Visual.setSubstance(_renderObject, state.substance, lociApply, true);
+                }
                 if (state.transform !== undefined) Visual.setTransform(_renderObject, state.transform);
             }
 

+ 6 - 0
src/mol-repr/structure/complex-representation.ts

@@ -21,6 +21,7 @@ import { StructureParams } from './params';
 import { Clipping } from '../../mol-theme/clipping';
 import { Transparency } from '../../mol-theme/transparency';
 import { WebGLContext } from '../../mol-gl/webgl/context';
+import { Substance } from '../../mol-theme/substance';
 
 export function ComplexRepresentation<P extends StructureParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: (materialId: number, structure: Structure, props: PD.Values<P>, webgl?: WebGLContext) => ComplexVisual<P>): StructureRepresentation<P> {
     let version = 0;
@@ -113,6 +114,11 @@ export function ComplexRepresentation<P extends StructureParams>(label: string,
             const remappedTransparency = Transparency.remap(state.transparency, _structure);
             visual.setTransparency(remappedTransparency, webgl);
         }
+        if (state.substance !== undefined && visual) {
+            // Remap loci from equivalent structure to the current structure
+            const remappedSubstance = Substance.remap(state.substance, _structure);
+            visual.setSubstance(remappedSubstance, webgl);
+        }
         if (state.clipping !== undefined && visual) {
             // Remap loci from equivalent structure to the current structure
             const remappedClipping = Clipping.remap(state.clipping, _structure);

+ 5 - 0
src/mol-repr/structure/complex-visual.ts

@@ -36,6 +36,7 @@ import { Clipping } from '../../mol-theme/clipping';
 import { TextureMesh } from '../../mol-geo/geometry/texture-mesh/texture-mesh';
 import { WebGLContext } from '../../mol-gl/webgl/context';
 import { isPromiseLike } from '../../mol-util/type-helpers';
+import { Substance } from '../../mol-theme/substance';
 
 export interface ComplexVisual<P extends StructureParams> extends Visual<Structure, P> { }
 
@@ -266,6 +267,10 @@ export function ComplexVisual<G extends Geometry, P extends StructureParams & Ge
             const smoothing = { geometry, props: currentProps, webgl };
             Visual.setTransparency(renderObject, transparency, lociApply, true, smoothing);
         },
+        setSubstance(substance: Substance, webgl?: WebGLContext) {
+            const smoothing = { geometry, props: currentProps, webgl };
+            Visual.setSubstance(renderObject, substance, lociApply, true, smoothing);
+        },
         setClipping(clipping: Clipping) {
             Visual.setClipping(renderObject, clipping, lociApply, true);
         },

+ 7 - 2
src/mol-repr/structure/units-representation.ts

@@ -25,6 +25,7 @@ import { StructureParams } from './params';
 import { Clipping } from '../../mol-theme/clipping';
 import { WebGLContext } from '../../mol-gl/webgl/context';
 import { StructureGroup } from './visual/util/common';
+import { Substance } from '../../mol-theme/substance';
 
 export interface UnitsVisual<P extends StructureParams> extends Visual<StructureGroup, P> { }
 
@@ -218,13 +219,14 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
     }
 
     function setVisualState(visual: UnitsVisual<P>, group: Unit.SymmetryGroup, state: Partial<StructureRepresentationState>) {
-        const { visible, alphaFactor, pickable, overpaint, transparency, clipping, transform, unitTransforms } = state;
+        const { visible, alphaFactor, pickable, overpaint, transparency, substance, clipping, transform, unitTransforms } = state;
 
         if (visible !== undefined) visual.setVisibility(visible);
         if (alphaFactor !== undefined) visual.setAlphaFactor(alphaFactor);
         if (pickable !== undefined) visual.setPickable(pickable);
         if (overpaint !== undefined) visual.setOverpaint(overpaint, webgl);
         if (transparency !== undefined) visual.setTransparency(transparency, webgl);
+        if (substance !== undefined) visual.setSubstance(substance, webgl);
         if (clipping !== undefined) visual.setClipping(clipping);
         if (transform !== undefined) visual.setTransform(transform);
         if (unitTransforms !== undefined) {
@@ -238,7 +240,7 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
     }
 
     function setState(state: Partial<StructureRepresentationState>) {
-        const { visible, alphaFactor, pickable, overpaint, transparency, clipping, transform, unitTransforms, syncManually, markerActions } = state;
+        const { visible, alphaFactor, pickable, overpaint, transparency, substance, clipping, transform, unitTransforms, syncManually, markerActions } = state;
         const newState: Partial<StructureRepresentationState> = {};
 
         if (visible !== _state.visible) newState.visible = visible;
@@ -250,6 +252,9 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
         if (transparency !== undefined && _structure) {
             newState.transparency = Transparency.remap(transparency, _structure);
         }
+        if (substance !== undefined && _structure) {
+            newState.substance = Substance.remap(substance, _structure);
+        }
         if (clipping !== undefined && _structure) {
             newState.clipping = Clipping.remap(clipping, _structure);
         }

+ 5 - 0
src/mol-repr/structure/units-visual.ts

@@ -40,6 +40,7 @@ import { StructureParams, StructureMeshParams, StructureSpheresParams, Structure
 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';
 
 export interface UnitsVisual<P extends RepresentationProps = {}> extends Visual<StructureGroup, P> { }
 
@@ -331,6 +332,10 @@ export function UnitsVisual<G extends Geometry, P extends StructureParams & Geom
             const smoothing = { geometry, props: currentProps, webgl };
             Visual.setTransparency(renderObject, transparency, lociApply, true, smoothing);
         },
+        setSubstance(substance: Substance, webgl?: WebGLContext) {
+            const smoothing = { geometry, props: currentProps, webgl };
+            Visual.setSubstance(renderObject, substance, lociApply, true, smoothing);
+        },
         setClipping(clipping: Clipping) {
             Visual.setClipping(renderObject, clipping, lociApply, true);
         },

+ 55 - 2
src/mol-repr/visual.ts

@@ -27,8 +27,10 @@ import { getMarkersAverage } from '../mol-geo/geometry/marker-data';
 import { Texture } from '../mol-gl/webgl/texture';
 import { Geometry } from '../mol-geo/geometry/geometry';
 import { getColorSmoothingProps, hasColorSmoothingProp } from '../mol-geo/geometry/base';
-import { applyMeshOverpaintSmoothing, applyMeshTransparencySmoothing } from '../mol-geo/geometry/mesh/color-smoothing';
-import { applyTextureMeshOverpaintSmoothing, applyTextureMeshTransparencySmoothing } from '../mol-geo/geometry/texture-mesh/color-smoothing';
+import { applyMeshOverpaintSmoothing, applyMeshSubstanceSmoothing, applyMeshTransparencySmoothing } from '../mol-geo/geometry/mesh/color-smoothing';
+import { applyTextureMeshOverpaintSmoothing, applyTextureMeshSubstanceSmoothing, applyTextureMeshTransparencySmoothing } from '../mol-geo/geometry/texture-mesh/color-smoothing';
+import { Substance } from '../mol-theme/substance';
+import { applySubstanceMaterial, clearSubstance, createSubstance } from '../mol-geo/geometry/substance-data';
 
 export interface VisualContext {
     readonly runtime: RuntimeContext
@@ -51,6 +53,7 @@ interface Visual<D, P extends PD.Params> {
     setTransform: (matrix?: Mat4, instanceMatrices?: Float32Array | null) => void
     setOverpaint: (overpaint: Overpaint, webgl?: WebGLContext) => void
     setTransparency: (transparency: Transparency, webgl?: WebGLContext) => void
+    setSubstance: (substance: Substance, webgl?: WebGLContext) => void
     setClipping: (clipping: Clipping) => void
     destroy: () => void
     mustRecreate?: (data: D, props: PD.Values<P>, webgl?: WebGLContext) => boolean
@@ -143,6 +146,7 @@ namespace Visual {
         resolution?: number
         overpaintTexture?: Texture
         transparencyTexture?: Texture
+        substanceTexture?: Texture
     }
 
     type SmoothingContext = {
@@ -248,6 +252,55 @@ namespace Visual {
         }
     }
 
+    export function setSubstance(renderObject: GraphicsRenderObject | undefined, substance: Substance, lociApply: LociApply, clear: boolean, smoothing?: SmoothingContext) {
+        if (!renderObject) return;
+
+        const { tSubstance, dSubstanceType, uGroupCount, instanceCount } = renderObject.values;
+        const count = uGroupCount.ref.value * instanceCount.ref.value;
+
+        // ensure texture has right size
+        createSubstance(substance.layers.length ? count : 0, renderObject.values);
+        const { array } = tSubstance.ref.value;
+
+        // clear all if requested
+        if (clear) clearSubstance(array, 0, count);
+
+        for (let i = 0, il = substance.layers.length; i < il; ++i) {
+            const { loci, material, clear } = substance.layers[i];
+            const apply = (interval: Interval) => {
+                const start = Interval.start(interval);
+                const end = Interval.end(interval);
+                return clear
+                    ? clearSubstance(array, start, end)
+                    : applySubstanceMaterial(array, start, end, material);
+            };
+            lociApply(loci, apply, false);
+        }
+        ValueCell.update(tSubstance, tSubstance.ref.value);
+        ValueCell.updateIfChanged(dSubstanceType, 'groupInstance');
+
+        if (substance.layers.length === 0) return;
+
+        if (smoothing && hasColorSmoothingProp(smoothing.props)) {
+            const { geometry, props, webgl } = smoothing;
+            if (geometry.kind === 'mesh') {
+                const { resolution, substanceTexture } = geometry.meta as SurfaceMeta;
+                const csp = getColorSmoothingProps(props.smoothColors, true, resolution);
+                if (csp) {
+                    applyMeshSubstanceSmoothing(renderObject.values as any, csp.resolution, csp.stride, webgl, substanceTexture);
+                    (geometry.meta as SurfaceMeta).substanceTexture = renderObject.values.tSubstanceGrid.ref.value;
+                }
+            } else if (webgl && geometry.kind === 'texture-mesh') {
+                const { resolution, substanceTexture } = geometry.meta as SurfaceMeta;
+                const csp = getColorSmoothingProps(props.smoothColors, true, resolution);
+                if (csp) {
+                    applyTextureMeshSubstanceSmoothing(renderObject.values as any, csp.resolution, csp.stride, webgl, substanceTexture);
+                    (geometry.meta as SurfaceMeta).substanceTexture = renderObject.values.tSubstanceGrid.ref.value;
+                }
+            }
+        }
+    }
+
     export function setClipping(renderObject: GraphicsRenderObject | undefined, clipping: Clipping, lociApply: LociApply, clear: boolean) {
         if (!renderObject) return;
 

+ 4 - 0
src/mol-repr/volume/representation.ts

@@ -32,6 +32,7 @@ import { SizeValues } from '../../mol-gl/renderable/schema';
 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';
 
 export interface VolumeVisual<P extends VolumeParams> extends Visual<Volume, P> { }
 
@@ -211,6 +212,9 @@ export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geomet
         setTransparency(transparency: Transparency) {
             return Visual.setTransparency(renderObject, transparency, lociApply, true);
         },
+        setSubstance(substance: Substance) {
+            return Visual.setSubstance(renderObject, substance, lociApply, true);
+        },
         setClipping(clipping: Clipping) {
             return Visual.setClipping(renderObject, clipping, lociApply, true);
         },

+ 1 - 1
src/mol-theme/overpaint.ts

@@ -68,7 +68,7 @@ namespace Overpaint {
         const layers: Overpaint.Layer[] = [];
         map.forEach((loci, colorOrClear) => {
             const clear = colorOrClear === -1;
-            const color = colorOrClear === -1 ? Color(0) : colorOrClear;
+            const color = clear ? Color(0) : colorOrClear;
             layers.push({ loci, color, clear });
         });
         return { layers };

+ 127 - 0
src/mol-theme/substance.ts

@@ -0,0 +1,127 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Loci } from '../mol-model/loci';
+import { Structure, StructureElement } from '../mol-model/structure';
+import { Script } from '../mol-script/script';
+import { Material } from '../mol-util/material';
+
+export { Substance };
+
+type Substance = { readonly layers: ReadonlyArray<Substance.Layer> }
+
+function Substance(layers: ReadonlyArray<Substance.Layer>): Substance {
+    return { layers };
+}
+
+namespace Substance {
+    export type Layer = { readonly loci: StructureElement.Loci, readonly material: Material, readonly clear: boolean }
+    export const Empty: Substance = { layers: [] };
+
+    export function areEqual(sA: Substance, sB: Substance) {
+        if (sA.layers.length === 0 && sB.layers.length === 0) return true;
+        if (sA.layers.length !== sB.layers.length) return false;
+        for (let i = 0, il = sA.layers.length; i < il; ++i) {
+            if (sA.layers[i].clear !== sB.layers[i].clear) return false;
+            if (sA.layers[i].material !== sB.layers[i].material) return false;
+            if (!Loci.areEqual(sA.layers[i].loci, sB.layers[i].loci)) return false;
+        }
+        return true;
+    }
+
+    export function isEmpty(overpaint: Substance) {
+        return overpaint.layers.length === 0;
+    }
+
+    export function remap(substance: Substance, structure: Structure) {
+        const layers: Substance.Layer[] = [];
+        for (const layer of substance.layers) {
+            let { loci, material, clear } = layer;
+            loci = StructureElement.Loci.remap(loci, structure);
+            if (!StructureElement.Loci.isEmpty(loci)) {
+                layers.push({ loci, material, clear });
+            }
+        }
+        return { layers };
+    }
+
+    export function merge(substance: Substance): Substance {
+        if (isEmpty(substance)) return substance;
+        const { structure } = substance.layers[0].loci;
+        const map = new Map<Material | -1, StructureElement.Loci>();
+        let shadowed = StructureElement.Loci.none(structure);
+        for (let i = 0, il = substance.layers.length; i < il; ++i) {
+            let { loci, material, clear } = substance.layers[il - i - 1]; // process from end
+            loci = StructureElement.Loci.subtract(loci, shadowed);
+            shadowed = StructureElement.Loci.union(loci, shadowed);
+            if (!StructureElement.Loci.isEmpty(loci)) {
+                const materialOrClear = clear ? -1 : material;
+                if (map.has(materialOrClear)) {
+                    loci = StructureElement.Loci.union(loci, map.get(materialOrClear)!);
+                }
+                map.set(materialOrClear, loci);
+            }
+        }
+        const layers: Substance.Layer[] = [];
+        map.forEach((loci, materialOrClear) => {
+            const clear = materialOrClear === -1;
+            const material = clear ? Material(0) : materialOrClear;
+            layers.push({ loci, material, clear });
+        });
+        return { layers };
+    }
+
+    export function filter(substance: Substance, filter: Structure): Substance {
+        if (isEmpty(substance)) return substance;
+        const { structure } = substance.layers[0].loci;
+        const layers: Substance.Layer[] = [];
+        for (const layer of substance.layers) {
+            let { loci, material, clear } = layer;
+            // filter by first map to the `filter` structure and
+            // then map back to the original structure of the substance loci
+            const filtered = StructureElement.Loci.remap(loci, filter);
+            loci = StructureElement.Loci.remap(filtered, structure);
+            if (!StructureElement.Loci.isEmpty(loci)) {
+                layers.push({ loci, material, clear });
+            }
+        }
+        return { layers };
+    }
+
+    export type ScriptLayer = { script: Script, material: Material, clear: boolean }
+    export function ofScript(scriptLayers: ScriptLayer[], structure: Structure): Substance {
+        const layers: Substance.Layer[] = [];
+        for (let i = 0, il = scriptLayers.length; i < il; ++i) {
+            const { script, material, clear } = scriptLayers[i];
+            const loci = Script.toLoci(script, structure);
+            if (!StructureElement.Loci.isEmpty(loci)) {
+                layers.push({ loci, material, clear });
+            }
+        }
+        return { layers };
+    }
+
+    export type BundleLayer = { bundle: StructureElement.Bundle, material: Material, clear: boolean }
+    export function ofBundle(bundleLayers: BundleLayer[], structure: Structure): Substance {
+        const layers: Substance.Layer[] = [];
+        for (let i = 0, il = bundleLayers.length; i < il; ++i) {
+            const { bundle, material, clear } = bundleLayers[i];
+            const loci = StructureElement.Bundle.toLoci(bundle, structure.root);
+            layers.push({ loci, material, clear });
+        }
+        return { layers };
+    }
+
+    export function toBundle(overpaint: Substance) {
+        const layers: BundleLayer[] = [];
+        for (let i = 0, il = overpaint.layers.length; i < il; ++i) {
+            const { loci, material, clear } = overpaint.layers[i];
+            const bundle = StructureElement.Bundle.fromLoci(loci);
+            layers.push({ bundle, material, clear });
+        }
+        return { layers };
+    }
+}