Browse Source

volume representation, surface

Alexander Rose 7 years ago
parent
commit
82f4e9646d

+ 7 - 7
src/apps/render-test/components/detail.tsx → src/apps/render-test/components/sphere-detail.tsx

@@ -14,25 +14,25 @@ import Select from 'material-ui/Select';
 import State from '../state'
 import Observer from './observer';
 
-interface DetailState {
+interface SphereDetailState {
     loading: boolean
     value: number
 }
 
-export default class Detail extends Observer<{ state: State } & WithStyles, DetailState> {
-    state: DetailState = { loading: false, value: 2 }
+export default class SphereDetail extends Observer<{ state: State } & WithStyles, SphereDetailState> {
+    state: SphereDetailState = { loading: false, value: 2 }
 
     componentDidMount() {
         this.subscribe(this.props.state.loading, value => {
            this.setState({ loading: value });
         });
-        this.subscribe(this.props.state.detail, value => {
+        this.subscribe(this.props.state.sphereDetail, value => {
             this.setState({ value });
          });
     }
 
     handleValueChange = (event: React.ChangeEvent<any>) => {
-        this.props.state.detail.next(event.target.value)
+        this.props.state.sphereDetail.next(event.target.value)
     }
 
     render() {
@@ -43,14 +43,14 @@ export default class Detail extends Observer<{ state: State } & WithStyles, Deta
         })
 
         return <FormControl className={classes.formControl}>
-            <InputLabel htmlFor='detail-value'>Detail</InputLabel>
+            <InputLabel htmlFor='sphere-detail-value'>Sphere Detail</InputLabel>
             <Select
                 className={classes.selectField}
                 value={this.state.value}
                 onChange={this.handleValueChange}
                 inputProps={{
                     name: 'value',
-                    id: 'detail-value',
+                    id: 'sphere-detail-value',
                 }}
             >
                 {items}

+ 47 - 12
src/apps/render-test/state.ts

@@ -20,9 +20,12 @@ import { Run } from 'mol-task'
 import { Symmetry, Structure, Model } from 'mol-model/structure'
 
 // import mcubes from './utils/mcubes'
-import { getModelFromPdbId, getModelFromFile, log } from './utils'
+import { getModelFromPdbId, getModelFromFile, log, Volume, getVolumeFromEmdId } from './utils'
 import { StructureRepresentation } from 'mol-geo/representation/structure';
 import { Color } from 'mol-util/color';
+import Surface, { SurfaceProps } from 'mol-geo/representation/volume/surface';
+import { VolumeIsoValue } from 'mol-model/volume';
+import { VolumeRepresentation } from 'mol-geo/representation/volume';
 // import Cylinder from 'mol-geo/primitive/cylinder';
 
 
@@ -37,14 +40,16 @@ export type ColorTheme = keyof typeof ColorTheme
 
 export default class State {
     viewer: Viewer
-    pdbId = '4cup'
+    pdbId = ''
+    emdId = '8689'
     model = new BehaviorSubject<Model | undefined>(undefined)
+    volume = new BehaviorSubject<Volume | undefined>(undefined)
     initialized = new BehaviorSubject<boolean>(false)
     loading = new BehaviorSubject<boolean>(false)
 
     colorTheme = new BehaviorSubject<ColorTheme>('element-symbol')
     colorValue = new BehaviorSubject<Color>(0xFF4411)
-    detail = new BehaviorSubject<number>(0)
+    sphereDetail = new BehaviorSubject<number>(0)
     assembly = new BehaviorSubject<string>('')
 
     pointVisibility = new BehaviorSubject<boolean>(true)
@@ -52,11 +57,12 @@ export default class State {
 
     pointRepr: StructureRepresentation<PointProps>
     spacefillRepr: StructureRepresentation<SpacefillProps>
+    surfaceRepr: VolumeRepresentation<SurfaceProps>
 
     constructor() {
         this.colorTheme.subscribe(() => this.update())
         this.colorValue.subscribe(() => this.update())
-        this.detail.subscribe(() => this.update())
+        this.sphereDetail.subscribe(() => this.update())
         this.assembly.subscribe(() => this.initStructure())
 
         this.pointVisibility.subscribe(() => this.updateVisibility())
@@ -66,7 +72,7 @@ export default class State {
     getSpacefillProps (): SpacefillProps {
         const colorThemeName = this.colorTheme.getValue()
         return {
-            detail: this.detail.getValue(),
+            detail: this.sphereDetail.getValue(),
             colorTheme: colorThemeName === 'uniform' ?
                 { name: colorThemeName, value: this.colorValue.getValue() } :
                 { name: colorThemeName }
@@ -87,6 +93,7 @@ export default class State {
         this.viewer = Viewer.create(canvas, container)
         this.initialized.next(true)
         this.loadPdbId()
+        this.loadEmdId()
         this.viewer.animate()
     }
 
@@ -97,7 +104,7 @@ export default class State {
         let structure: Structure
         const assemblies = model.symmetry.assemblies
         if (assemblies.length) {
-            structure = await Run(Symmetry.buildAssembly(Structure.ofModel(model), assembly || '1'), log, 100)
+            structure = await Run(Symmetry.buildAssembly(Structure.ofModel(model), assembly || '1'), log, 500)
         } else {
             structure = Structure.ofModel(model)
         }
@@ -105,8 +112,8 @@ export default class State {
     }
 
     async initStructure () {
-        const { viewer, model } = this
-        if (!viewer || !model) return
+        const { viewer } = this
+        if (!viewer || !this.model.getValue()) return
 
         viewer.clear()
 
@@ -114,11 +121,11 @@ export default class State {
         if (!structure) return
 
         this.pointRepr = StructureRepresentation(Point)
-        await Run(this.pointRepr.create(structure, this.getPointProps()), log, 100)
+        await Run(this.pointRepr.create(structure, this.getPointProps()), log, 500)
         viewer.add(this.pointRepr)
 
         this.spacefillRepr = StructureRepresentation(Spacefill)
-        await Run(this.spacefillRepr.create(structure, this.getSpacefillProps()), log, 100)
+        await Run(this.spacefillRepr.create(structure, this.getSpacefillProps()), log, 500)
         viewer.add(this.spacefillRepr)
 
         this.updateVisibility()
@@ -138,6 +145,21 @@ export default class State {
         this.setModel((await getModelFromFile(file))[0])
     }
 
+    async initVolume () {
+        const { viewer } = this
+        const v = this.volume.getValue()
+        if (!viewer || !v) return
+
+        viewer.clear()
+
+        this.surfaceRepr = VolumeRepresentation(Surface)
+        await Run(this.surfaceRepr.create(v.volume, { isoValue: VolumeIsoValue.relative(v.volume.dataStats, 1.5) }), log, 500)
+        viewer.add(this.surfaceRepr)
+
+        viewer.requestDraw()
+        console.log(viewer.stats)
+    }
+
     async loadPdbId () {
         this.viewer.clear()
         if (this.pdbId.length !== 4) return
@@ -145,10 +167,23 @@ export default class State {
         this.setModel((await getModelFromPdbId(this.pdbId))[0])
     }
 
+    setVolume(volume: Volume) {
+        this.volume.next(volume)
+        this.initVolume()
+        this.loading.next(false)
+    }
+
+    async loadEmdId () {
+        this.viewer.clear()
+        if (this.emdId.length !== 4) return
+        this.loading.next(true)
+        this.setVolume(await getVolumeFromEmdId(this.emdId))
+    }
+
     async update () {
         if (!this.spacefillRepr) return
-        await Run(this.spacefillRepr.update(this.getSpacefillProps()), log, 100)
-        await Run(this.pointRepr.update(this.getPointProps()), log, 100)
+        await Run(this.spacefillRepr.update(this.getSpacefillProps()), log, 500)
+        await Run(this.pointRepr.update(this.getPointProps()), log, 500)
         this.viewer.add(this.spacefillRepr)
         this.viewer.add(this.pointRepr)
         this.viewer.update()

+ 2 - 2
src/apps/render-test/ui.tsx

@@ -16,7 +16,7 @@ import State from './state'
 import Viewport from './components/viewport'
 import FileInput from './components/file-input'
 import ColorTheme from './components/color-theme'
-import Detail from './components/detail'
+import SphereDetail from './components/sphere-detail'
 import Visibility from './components/visibility'
 import Assemblies from './components/assemblies'
 
@@ -86,7 +86,7 @@ class UI extends React.Component<{ state: State } & WithStyles, {  }> {
                         <div>
                             <Assemblies state={state} classes={classes}></Assemblies>
                             <ColorTheme state={state} classes={classes}></ColorTheme>
-                            <Detail state={state} classes={classes}></Detail>
+                            <SphereDetail state={state} classes={classes}></SphereDetail>
                             <Visibility state={state} classes={classes}></Visibility>
                         </div>
                     </form>

+ 21 - 7
src/apps/render-test/utils/index.ts

@@ -7,23 +7,29 @@
 import CIF from 'mol-io/reader/cif'
 import { Run, Progress } from 'mol-task'
 import { Model } from 'mol-model/structure'
+import { VolumeData, parseDensityServerData } from 'mol-model/volume'
+import { DensityServer_Data_Database } from 'mol-io/reader/cif/schema/density-server';
 
 export function log(progress: Progress) {
     const p = progress.root.progress
     console.log(`${p.message} ${(p.current/p.max*100).toFixed(2)}%`)
 }
 
+export async function downloadCif(url: string, isBinary: boolean) {
+    const data = await fetch(url);
+    return parseCif(isBinary ? new Uint8Array(await data.arrayBuffer()) : await data.text());
+}
+
 export async function parseCif(data: string|Uint8Array) {
     const comp = CIF.parse(data)
-    const parsed = await Run(comp, log, 100);
+    const parsed = await Run(comp, log, 500);
     if (parsed.isError) throw parsed;
-    return parsed
+    return parsed.result
 }
 
 export async function getModelFromPdbId(pdbid: string) {
-    const data = await fetch(`https://files.rcsb.org/download/${pdbid}.cif`)
-    const parsed = await parseCif(await data.text())
-    return Model.create({ kind: 'mmCIF', data: CIF.schema.mmCIF(parsed.result.blocks[0]) })
+    const cif = await downloadCif(`https://files.rcsb.org/download/${pdbid}.cif`, false)
+    return Model.create({ kind: 'mmCIF', data: CIF.schema.mmCIF(cif.blocks[0]) })
 }
 
 const readFileAsText = (file: File) => {
@@ -39,6 +45,14 @@ const readFileAsText = (file: File) => {
 }
 
 export async function getModelFromFile(file: File) {
-    const parsed = await parseCif(await readFileAsText(file))
-    return Model.create({ kind: 'mmCIF', data: CIF.schema.mmCIF(parsed.result.blocks[0]) })
+    const cif = await parseCif(await readFileAsText(file))
+    return Model.create({ kind: 'mmCIF', data: CIF.schema.mmCIF(cif.blocks[0]) })
+}
+
+export type Volume = { source: DensityServer_Data_Database, volume: VolumeData }
+
+export async function getVolumeFromEmdId(emdid: string): Promise<Volume> {
+    const cif = await downloadCif(`https://webchem.ncbr.muni.cz/DensityServer/em/emd-${emdid}/cell?detail=4`, true)
+    const data = CIF.schema.densityServer(cif.blocks[1])
+    return { source: data, volume: await Run(parseDensityServerData(data)) }
 }

+ 2 - 2
src/apps/structure-info/volume.ts

@@ -14,7 +14,7 @@ import CIF from 'mol-io/reader/cif'
 import { DensityServer_Data_Database } from 'mol-io/reader/cif/schema/density-server';
 import { Run } from 'mol-task';
 import { Table } from 'mol-data/db';
-import { computeVolumeMesh } from 'mol-geo/representation/volume/Mesh';
+import { computeVolumeSurface } from 'mol-geo/representation/volume/surface';
 import { StringBuilder } from 'mol-util';
 
 require('util.promisify').shim();
@@ -38,7 +38,7 @@ function print(data: Volume) {
 }
 
 async function doMesh(data: Volume, filename: string) {
-    const mesh = await Run(computeVolumeMesh(data.volume, VolumeIsoValue.relative(data.volume.dataStats, 1.5)));
+    const mesh = await Run(computeVolumeSurface(data.volume, VolumeIsoValue.relative(data.volume.dataStats, 1.5)));
     console.log({ vc: mesh.vertexCount, tc: mesh.triangleCount });
 
     // Export the mesh in OBJ format.

+ 16 - 0
src/mol-geo/representation/index.ts

@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Task } from 'mol-task'
+import { RenderObject } from 'mol-gl/scene';
+
+export interface RepresentationProps {}
+
+export interface Representation<D, P extends RepresentationProps = {}> {
+    renderObjects: ReadonlyArray<RenderObject>
+    create: (data: D, props?: P) => Task<void>
+    update: (props: P) => Task<void>
+}

+ 13 - 15
src/mol-geo/representation/structure/index.ts

@@ -9,22 +9,20 @@ import { EquivalenceClasses } from 'mol-data/util';
 import { OrderedSet } from 'mol-data/int'
 import { Task } from 'mol-task'
 import { RenderObject } from 'mol-gl/scene';
+import { Representation, RepresentationProps } from '..';
 // import { Mat4, EPSILON } from 'mol-math/linear-algebra';
 
-export interface RepresentationProps {
 
-}
-
-export interface UnitsRepresentation<Props = {}> {
+export interface UnitsRepresentation<P> {
     renderObjects: ReadonlyArray<RenderObject>
-    create: (units: ReadonlyArray<Unit>, elementGroup: ElementGroup, props: Props) => Task<void>
-    update: (props: RepresentationProps) => Task<boolean>
+    create: (units: ReadonlyArray<Unit>, elementGroup: ElementGroup, props: P) => Task<void>
+    update: (props: P) => Task<boolean>
 }
 
-export interface StructureRepresentation<Props = {}> {
+export interface StructureRepresentation<P extends RepresentationProps = {}> extends Representation<Structure, P> {
     renderObjects: ReadonlyArray<RenderObject>
-    create: (structure: Structure, props?: Props) => Task<void>
-    update: (props: Props) => Task<void>
+    create: (structure: Structure, props?: P) => Task<void>
+    update: (props: P) => Task<void>
 }
 
 interface GroupRepresentation<T> {
@@ -33,13 +31,13 @@ interface GroupRepresentation<T> {
     elementGroup: ElementGroup
 }
 
-export function StructureRepresentation<Props>(reprCtor: () => UnitsRepresentation<Props>): StructureRepresentation<Props> {
+export function StructureRepresentation<P>(reprCtor: () => UnitsRepresentation<P>): StructureRepresentation<P> {
     const renderObjects: RenderObject[] = []
-    const groupReprs: GroupRepresentation<Props>[] = []
+    const groupReprs: GroupRepresentation<P>[] = []
 
     return {
         renderObjects,
-        create(structure: Structure, props: Props = {} as Props) {
+        create(structure: Structure, props: P = {} as P) {
             return Task.create('StructureRepresentation.create', async ctx => {
                 const { elements, units } = structure;
                 const uniqueGroups = EquivalenceClasses<number, { unit: Unit, group: ElementGroup }>(
@@ -73,20 +71,20 @@ export function StructureRepresentation<Props>(reprCtor: () => UnitsRepresentati
                     const elementGroup = ElementSet.groupFromUnitIndex(elements, group[0])
                     const repr = reprCtor()
                     groupReprs.push({ repr, units: groupUnits, elementGroup })
-                    await ctx.update({ message: 'Building units...', current: i, max: il });
+                    await ctx.update({ message: 'Building structure unit representations...', current: i, max: il });
                     await ctx.runChild(repr.create(groupUnits, elementGroup, props));
                     renderObjects.push(...repr.renderObjects)
                 }
             });
         },
-        update(props: Props) {
+        update(props: P) {
             return Task.create('StructureRepresentation.update', async ctx => {
                 // TODO check model.id, conformation.id, unit.id, elementGroup(.hashCode/.areEqual)
                 renderObjects.length = 0 // clear
                 for (let i = 0, il = groupReprs.length; i < il; ++i) {
                     const groupRepr = groupReprs[i]
                     const { repr, units, elementGroup } = groupRepr
-                    await ctx.update({ message: 'Updating units...', current: i, max: il });
+                    await ctx.update({ message: 'Updating structure unit representations...', current: i, max: il });
                     if (!await ctx.runChild(repr.update(props))) {
                         await ctx.runChild(repr.create(units, elementGroup, props))
                     }

+ 2 - 2
src/mol-geo/representation/structure/point.ts

@@ -11,7 +11,7 @@ import { Unit, ElementGroup, Element } from 'mol-model/structure';
 import { Task } from 'mol-task'
 import { fillSerial } from 'mol-gl/renderable/util';
 
-import { RepresentationProps, UnitsRepresentation } from './index';
+import { UnitsRepresentation } from './index';
 import VertexMap from '../../shape/vertex-map';
 import { ColorTheme, SizeTheme } from '../../theme';
 import { createTransforms, createColors, createSizes } from './utils';
@@ -100,7 +100,7 @@ export default function Point(): UnitsRepresentation<PointProps> {
                 renderObjects.push(points)
             })
         },
-        update(props: RepresentationProps) {
+        update(props: PointProps) {
             return Task.create('Point.update', async ctx => {
                 if (!points || !_units || !_elementGroup) return false
 

+ 2 - 2
src/mol-geo/representation/structure/spacefill.ts

@@ -11,7 +11,7 @@ import { RenderObject, createMeshRenderObject, MeshRenderObject } from 'mol-gl/s
 import { Vec3, Mat4 } from 'mol-math/linear-algebra'
 import { OrderedSet } from 'mol-data/int'
 import { Unit, ElementGroup, Element, Queries } from 'mol-model/structure';
-import { RepresentationProps, UnitsRepresentation } from './index';
+import { UnitsRepresentation } from './index';
 import { Task } from 'mol-task'
 import { MeshBuilder } from '../../shape/mesh-builder';
 import { createTransforms, createColors } from './utils';
@@ -110,7 +110,7 @@ export default function Spacefill(): UnitsRepresentation<SpacefillProps> {
                 renderObjects.push(spheres)
             })
         },
-        update(props: RepresentationProps) {
+        update(props: SpacefillProps) {
             return Task.create('Spacefill.update', async ctx => {
                 if (!spheres) return false
 

+ 0 - 29
src/mol-geo/representation/volume/Mesh.ts

@@ -1,29 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-import { VolumeData, VolumeIsoValue } from 'mol-model/volume'
-import { Task } from 'mol-task'
-import { Mesh } from '../../shape/mesh';
-import { computeMarchingCubes } from '../../util/marching-cubes/algorithm';
-
-function computeVolumeMesh(volume: VolumeData, isoValue: VolumeIsoValue) {
-    return Task.create<Mesh>('Volume Surface', async ctx => {
-        ctx.update({ message: 'Marching cubes...' });
-
-        const mesh = await ctx.runChild(computeMarchingCubes({
-            isoLevel: VolumeIsoValue.toAbsolute(isoValue).absoluteValue,
-            scalarField: volume.data
-        }));
-
-        const transform = VolumeData.getGridToCartesianTransform(volume);
-        ctx.update({ message: 'Transforming mesh...' });
-        Mesh.transformImmediate(mesh, transform);
-
-        return mesh;
-    });
-}
-
-export { computeVolumeMesh }

+ 41 - 0
src/mol-geo/representation/volume/index.ts

@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Task } from 'mol-task'
+import { RenderObject } from 'mol-gl/scene';
+import { RepresentationProps, Representation } from '..';
+import { VolumeData } from 'mol-model/volume';
+
+export interface VolumeElementRepresentation<P> {
+    renderObjects: ReadonlyArray<RenderObject>
+    create: (volumeData: VolumeData, props: P) => Task<void>
+    update: (props: P) => Task<boolean>
+}
+
+export interface VolumeRepresentation<P extends RepresentationProps = {}> extends Representation<VolumeData, P> {
+    renderObjects: ReadonlyArray<RenderObject>
+    create: (volumeData: VolumeData, props?: P) => Task<void>
+    update: (props: P) => Task<void>
+}
+
+export function VolumeRepresentation<P>(reprCtor: () => VolumeElementRepresentation<P>): VolumeRepresentation<P> {
+    const renderObjects: RenderObject[] = []
+
+    return {
+        renderObjects,
+        create(volumeData: VolumeData, props: P = {} as P) {
+            return Task.create('VolumeRepresentation.create', async ctx => {
+                const repr = reprCtor()
+                await ctx.update({ message: 'Building volume representation...', current: 0, max: 1 });
+                await ctx.runChild(repr.create(volumeData, props));
+                renderObjects.push(...repr.renderObjects)
+            });
+        },
+        update(props: P) {
+            return Task.create('VolumeRepresentation.update', async ctx => {})
+        }
+    }
+}

+ 82 - 0
src/mol-geo/representation/volume/surface.ts

@@ -0,0 +1,82 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { VolumeData, VolumeIsoValue } from 'mol-model/volume'
+import { Task } from 'mol-task'
+import { computeMarchingCubes } from '../../util/marching-cubes/algorithm';
+import { Mesh } from '../../shape/mesh';
+import { VolumeElementRepresentation } from '.';
+import { RenderObject, createMeshRenderObject, MeshRenderObject } from 'mol-gl/scene';
+import { fillSerial } from 'mol-gl/renderable/util';
+import { ValueCell } from 'mol-util';
+import { Mat4 } from 'mol-math/linear-algebra';
+import { createUniformColor } from '../../util/color-data';
+
+export function computeVolumeSurface(volume: VolumeData, isoValue: VolumeIsoValue) {
+    return Task.create<Mesh>('Volume Surface', async ctx => {
+        ctx.update({ message: 'Marching cubes...' });
+
+        const mesh = await ctx.runChild(computeMarchingCubes({
+            isoLevel: VolumeIsoValue.toAbsolute(isoValue).absoluteValue,
+            scalarField: volume.data
+        }));
+
+        const transform = VolumeData.getGridToCartesianTransform(volume);
+        ctx.update({ message: 'Transforming mesh...' });
+        Mesh.transformImmediate(mesh, transform);
+
+        return mesh;
+    });
+}
+
+export const DefaultSurfaceProps = {
+    isoValue: VolumeIsoValue.relative({ min: 0, max: 0, mean: 0, sigma: 0 }, 0)
+}
+export type SurfaceProps = Partial<typeof DefaultSurfaceProps>
+
+export default function Surface(): VolumeElementRepresentation<SurfaceProps> {
+    const renderObjects: RenderObject[] = []
+    let surface: MeshRenderObject
+    let curProps = DefaultSurfaceProps
+
+    return {
+        renderObjects,
+        create(volume: VolumeData, props: SurfaceProps = {}) {
+            return Task.create('Point.create', async ctx => {
+                renderObjects.length = 0 // clear
+                curProps = { ...DefaultSurfaceProps, ...props }
+
+                const mesh = await ctx.runChild(computeVolumeSurface(volume, curProps.isoValue))
+
+                surface = createMeshRenderObject({
+                    objectId: 0,
+
+                    position: mesh.vertexBuffer,
+                    normal: mesh.normalBuffer,
+                    id: ValueCell.create(fillSerial(new Float32Array(mesh.vertexCount / 3))),
+                    color: createUniformColor({ value: 0x999999 }),
+                    transform: ValueCell.create(new Float32Array(Mat4.identity())),
+                    index: mesh.indexBuffer,
+
+                    instanceCount: 1,
+                    indexCount: mesh.triangleCount,
+                    elementCount: mesh.triangleCount,
+                    positionCount: mesh.vertexCount / 3,
+
+                    flatShaded: true
+                })
+                renderObjects.push(surface)
+            })
+        },
+        update(props: SurfaceProps) {
+            return Task.create('Surface.update', async ctx => {
+                // TODO
+                return false
+            })
+        }
+    }
+}

+ 9 - 2
src/mol-gl/renderable/mesh.ts

@@ -20,7 +20,7 @@ namespace Mesh {
         objectId: number
 
         position: ValueCell<Float32Array>
-        normal?: ValueCell<Float32Array>
+        normal: ValueCell<Float32Array | undefined>
         id: ValueCell<Float32Array>
 
         color: ColorData
@@ -31,12 +31,19 @@ namespace Mesh {
         instanceCount: number
         elementCount: number
         positionCount: number
+
+        flatShaded?: boolean
+        doubleSided?: boolean
     }
 
     export function create(ctx: Context, props: Props): Renderable<Props> {
+        const defines = getBaseDefines(props)
+        if (props.flatShaded) defines.FLAT_SHADED = ''
+        if (props.doubleSided) defines.DOUBLE_SIDED = ''
+
         const defs: RenderItemProps = {
             ...getBaseDefs(props),
-            shaderCode: addShaderDefines(getBaseDefines(props), MeshShaderCode),
+            shaderCode: addShaderDefines(defines, MeshShaderCode),
             drawMode: 'triangles',
             elementsKind: 'uint32'
         }

+ 3 - 3
src/mol-gl/renderable/util.ts

@@ -67,7 +67,7 @@ interface BaseProps {
     positionCount: number,
 
     position: ValueCell<Float32Array>
-    normal?: ValueCell<Float32Array>
+    normal?: ValueCell<Float32Array | undefined>
     id: ValueCell<Float32Array>
     transform: ValueCell<Float32Array>
 
@@ -133,7 +133,7 @@ export function getBaseAttributeDefs(props: BaseProps) {
         elementId: { kind: 'float32', itemSize: 1, divisor: 0 },
         transform: { kind: 'float32', itemSize: 16, divisor: 1 },
     }
-    if (props.normal) {
+    if (props.normal && props.normal.ref.value) {
         attributeDefs.normal = { kind: 'float32', itemSize: 3, divisor: 0 }
     }
     const color = props.color
@@ -156,7 +156,7 @@ export function getBaseAttributeValues(props: BaseProps) {
         elementId: id.ref.value,
         transform: transform.ref.value
     }
-    if (normal) {
+    if (normal && normal.ref.value) {
         attributeValues.normal = normal.ref.value
     }
     const color = props.color

+ 2 - 1
src/mol-gl/shader-code.ts

@@ -23,7 +23,8 @@ export const MeshShaderCode: ShaderCode = {
 type ShaderDefine = (
     'UNIFORM_COLOR' | 'ATTRIBUTE_COLOR' | 'INSTANCE_COLOR' | 'ELEMENT_COLOR' | 'ELEMENT_INSTANCE_COLOR' |
     'UNIFORM_SIZE' | 'ATTRIBUTE_SIZE' |
-    'POINT_SIZE_ATTENUATION'
+    'POINT_SIZE_ATTENUATION' |
+    'FLAT_SHADED' | 'DOUBLE_SIDED'
 )
 export type ShaderDefines = {
     [k in ShaderDefine]?: number|string

+ 20 - 2
src/mol-gl/shader/mesh.frag

@@ -4,6 +4,10 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
+#ifdef FLAT_SHADED
+    #extension GL_OES_standard_derivatives : enable
+#endif
+
 precision highp float;
 
 // uniform vec3 light_position;
@@ -11,7 +15,10 @@ uniform vec3 light_color;
 uniform vec3 light_ambient;
 uniform mat4 view;
 
-varying vec3 vNormal, vViewPosition;
+#ifndef FLAT_SHADED
+    varying vec3 vNormal;
+#endif
+varying vec3 vViewPosition;
 
 #pragma glslify: import('./chunks/color-frag-params.glsl')
 
@@ -35,7 +42,18 @@ void main() {
 
     vec3 L = normalize(lightVector); // light direction
     vec3 V = normalize(vViewPosition); // eye direction
-    vec3 N = normalize(-vNormal); // surface normal
+
+    // surface normal
+    #ifdef FLAT_SHADED
+        vec3 fdx = dFdx(vViewPosition);
+        vec3 fdy = dFdy(vViewPosition);
+        vec3 N = -normalize(cross(fdx, fdy));
+    #else
+        vec3 N = -normalize(vNormal);
+        #ifdef DOUBLE_SIDED
+            N = N * (float(gl_FrontFacing) * 2.0 - 1.0);
+        #endif
+    #endif
 
     // compute our diffuse & specular terms
     float specular = calculateSpecular(L, V, N, shininess) * specularScale;

+ 8 - 4
src/mol-gl/shader/mesh.vert

@@ -19,9 +19,11 @@ attribute mat4 transform;
 attribute float instanceId;
 attribute float elementId;
 
-attribute vec3 normal;
+#ifndef FLAT_SHADED
+    attribute vec3 normal;
+    varying vec3 vNormal;
+#endif
 
-varying vec3 vNormal;
 varying vec3 vViewPosition;
 
 #pragma glslify: inverse = require(./utils/inverse.glsl)
@@ -35,6 +37,8 @@ void main(){
     vViewPosition = mvPosition.xyz;
     gl_Position = projection * mvPosition;
 
-    mat3 normalMatrix = transpose(inverse(mat3(modelView)));
-    vNormal = normalize(normalMatrix * normal);
+    #ifndef FLAT_SHADED
+        mat3 normalMatrix = transpose(inverse(mat3(modelView)));
+        vNormal = normalize(normalMatrix * normal);
+    #endif
 }

+ 6 - 1
src/mol-gl/webgl/context.ts

@@ -33,6 +33,7 @@ function unbindResources (gl: WebGLRenderingContext) {
 
 type Extensions = {
     angleInstancedArrays: ANGLE_instanced_arrays
+    standardDerivatives: OES_standard_derivatives
     oesElementIndexUint: OES_element_index_uint | null
     oesVertexArrayObject: OES_vertex_array_object | null
 }
@@ -53,6 +54,10 @@ export function createContext(gl: WebGLRenderingContext): Context {
     if (angleInstancedArrays === null) {
         throw new Error('Could not get "ANGLE_instanced_arrays" extension')
     }
+    const standardDerivatives = gl.getExtension('OES_standard_derivatives')
+    if (standardDerivatives === null) {
+        throw new Error('Could not get "OES_standard_derivatives" extension')
+    }
     const oesElementIndexUint = gl.getExtension('OES_element_index_uint')
     if (oesElementIndexUint === null) {
         console.warn('Could not get "OES_element_index_uint" extension')
@@ -67,7 +72,7 @@ export function createContext(gl: WebGLRenderingContext): Context {
 
     return {
         gl,
-        extensions: { angleInstancedArrays, oesElementIndexUint, oesVertexArrayObject },
+        extensions: { angleInstancedArrays, standardDerivatives, oesElementIndexUint, oesVertexArrayObject },
         shaderCache,
         programCache,
         bufferCount: 0,

+ 10 - 10
src/mol-view/viewer.ts

@@ -9,20 +9,20 @@ import InputObserver from 'mol-util/input/input-observer'
 import * as SetUtils from 'mol-util/set'
 import Renderer, { RendererStats } from 'mol-gl/renderer'
 import { RenderObject } from 'mol-gl/scene'
-import { StructureRepresentation } from 'mol-geo/representation/structure';
 
 import TrackballControls from './controls/trackball'
 import { Viewport } from './camera/util'
 import { PerspectiveCamera } from './camera/perspective'
 import { resizeCanvas } from './util';
 import { createContext } from 'mol-gl/webgl/context';
+import { Representation } from 'mol-geo/representation';
 
 interface Viewer {
-    hide: (repr: StructureRepresentation) => void
-    show: (repr: StructureRepresentation) => void
+    hide: (repr: Representation<any>) => void
+    show: (repr: Representation<any>) => void
 
-    add: (repr: StructureRepresentation) => void
-    remove: (repr: StructureRepresentation) => void
+    add: (repr: Representation<any>) => void
+    remove: (repr: Representation<any>) => void
     update: () => void
     clear: () => void
 
@@ -49,7 +49,7 @@ function getWebGLContext(canvas: HTMLCanvasElement, contextAttributes?: WebGLCon
 
 namespace Viewer {
     export function create(canvas: HTMLCanvasElement, container: Element): Viewer {
-        const reprMap = new Map<StructureRepresentation, Set<RenderObject>>()
+        const reprMap = new Map<Representation<any>, Set<RenderObject>>()
 
         const input = InputObserver.create(canvas)
         input.resize.subscribe(handleResize)
@@ -99,16 +99,16 @@ namespace Viewer {
         handleResize()
 
         return {
-            hide: (repr: StructureRepresentation) => {
+            hide: (repr: Representation<any>) => {
                 const renderObjectSet = reprMap.get(repr)
                 if (renderObjectSet) renderObjectSet.forEach(o => o.visible = false)
             },
-            show: (repr: StructureRepresentation) => {
+            show: (repr: Representation<any>) => {
                 const renderObjectSet = reprMap.get(repr)
                 if (renderObjectSet) renderObjectSet.forEach(o => o.visible = true)
             },
 
-            add: (repr: StructureRepresentation) => {
+            add: (repr: Representation<any>) => {
                 const oldRO = reprMap.get(repr)
                 const newRO = new Set<RenderObject>()
                 repr.renderObjects.forEach(o => newRO.add(o))
@@ -120,7 +120,7 @@ namespace Viewer {
                 }
                 reprMap.set(repr, newRO)
             },
-            remove: (repr: StructureRepresentation) => {
+            remove: (repr: Representation<any>) => {
                 const renderObjectSet = reprMap.get(repr)
                 if (renderObjectSet) renderObjectSet.forEach(o => renderer.remove(o))
             },