Ver Fonte

add cylinders geometry and shader

Alexander Rose há 4 anos atrás
pai
commit
d58e90d93f

+ 102 - 0
src/mol-geo/geometry/cylinders/cylinders-builder.ts

@@ -0,0 +1,102 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ChunkedArray } from '../../../mol-data/util';
+import { Cylinders } from './cylinders';
+import { Vec3 } from '../../../mol-math/linear-algebra';
+
+export interface CylindersBuilder {
+    add(startX: number, startY: number, startZ: number, endX: number, endY: number, endZ: number, radiusScale: number, topCap: boolean, bottomCap: boolean, group: number): void
+    addFixedCountDashes(start: Vec3, end: Vec3, segmentCount: number, radiusScale: number, topCap: boolean, bottomCap: boolean, group: number): void
+    addFixedLengthDashes(start: Vec3, end: Vec3, segmentLength: number, radiusScale: number, topCap: boolean, bottomCap: boolean, group: number): void
+    getCylinders(): Cylinders
+}
+
+const tmpVecA = Vec3();
+const tmpVecB = Vec3();
+const tmpDir = Vec3();
+
+// avoiding namespace lookup improved performance in Chrome (Aug 2020)
+const caAdd = ChunkedArray.add;
+const caAdd3 = ChunkedArray.add3;
+
+export namespace CylindersBuilder {
+    export function create(initialCount = 2048, chunkSize = 1024, cylinders?: Cylinders): CylindersBuilder {
+        const groups = ChunkedArray.create(Float32Array, 1, chunkSize, cylinders ? cylinders.groupBuffer.ref.value : initialCount);
+        const starts = ChunkedArray.create(Float32Array, 3, chunkSize, cylinders ? cylinders.startBuffer.ref.value : initialCount);
+        const ends = ChunkedArray.create(Float32Array, 3, chunkSize, cylinders ? cylinders.endBuffer.ref.value : initialCount);
+        const scales = ChunkedArray.create(Float32Array, 1, chunkSize, cylinders ? cylinders.scaleBuffer.ref.value : initialCount);
+        const caps = ChunkedArray.create(Float32Array, 1, chunkSize, cylinders ? cylinders.capBuffer.ref.value : initialCount);
+
+        const add = (startX: number, startY: number, startZ: number, endX: number, endY: number, endZ: number, radiusScale: number, topCap: boolean, bottomCap: boolean, group: number) => {
+            for (let i = 0; i < 6; ++i) {
+                caAdd3(starts, startX, startY, startZ);
+                caAdd3(ends, endX, endY, endZ);
+                caAdd(groups, group);
+                caAdd(scales, radiusScale);
+                caAdd(caps, (topCap ? 1 : 0) + (bottomCap ? 2 : 0));
+            }
+        };
+
+        const addFixedCountDashes = (start: Vec3, end: Vec3, segmentCount: number, radiusScale: number, topCap: boolean, bottomCap: boolean, group: number) => {
+            const d = Vec3.distance(start, end);
+            const s = Math.floor(segmentCount / 2);
+            const step = 1 / segmentCount;
+
+            Vec3.sub(tmpDir, end, start);
+            for (let j = 0; j < s; ++j) {
+                const f = step * (j * 2 + 1);
+                Vec3.setMagnitude(tmpDir, tmpDir, d * f);
+                Vec3.add(tmpVecA, start, tmpDir);
+                Vec3.setMagnitude(tmpDir, tmpDir, d * step * ((j + 1) * 2));
+                Vec3.add(tmpVecB, start, tmpDir);
+                add(tmpVecA[0], tmpVecA[1], tmpVecA[2], tmpVecB[0], tmpVecB[1], tmpVecB[2], radiusScale, topCap, bottomCap, group);
+            }
+        };
+
+        return {
+            add,
+            addFixedCountDashes,
+            addFixedLengthDashes: (start: Vec3, end: Vec3, segmentLength: number, radiusScale: number, topCap: boolean, bottomCap: boolean, group: number) => {
+                const d = Vec3.distance(start, end);
+                addFixedCountDashes(start, end, d / segmentLength, radiusScale, topCap, bottomCap, group);
+            },
+            getCylinders: () => {
+                const cylinderCount = groups.elementCount / 6;
+                const gb = ChunkedArray.compact(groups, true) as Float32Array;
+                const sb = ChunkedArray.compact(starts, true) as Float32Array;
+                const eb = ChunkedArray.compact(ends, true) as Float32Array;
+                const ab = ChunkedArray.compact(scales, true) as Float32Array;
+                const cb = ChunkedArray.compact(caps, true) as Float32Array;
+                const mb = cylinders && cylinderCount <= cylinders.cylinderCount ? cylinders.mappingBuffer.ref.value : new Float32Array(cylinderCount * 18);
+                const ib = cylinders && cylinderCount <= cylinders.cylinderCount ? cylinders.indexBuffer.ref.value : new Uint32Array(cylinderCount * 12);
+                if (!cylinders || cylinderCount > cylinders.cylinderCount) fillMappingAndIndices(cylinderCount, mb, ib);
+                return Cylinders.create(mb, ib, gb, sb, eb, ab, cb, cylinderCount, cylinders);
+            }
+        };
+    }
+}
+
+function fillMappingAndIndices(n: number, mb: Float32Array, ib: Uint32Array) {
+    for (let i = 0; i < n; ++i) {
+        const mo = i * 18;
+        mb[mo] = -1; mb[mo + 1] = 1; mb[mo + 2] = -1;
+        mb[mo + 3] = -1; mb[mo + 4] = -1; mb[mo + 5] = -1;
+        mb[mo + 6] = 1; mb[mo + 7] = 1; mb[mo + 8] = -1;
+        mb[mo + 9] = 1; mb[mo + 10] = 1; mb[mo + 11] = 1;
+        mb[mo + 12] = 1; mb[mo + 13] = -1; mb[mo + 14] = -1;
+        mb[mo + 15] = 1; mb[mo + 16] = -1; mb[mo + 17] = 1;
+    }
+
+    for (let i = 0; i < n; ++i) {
+        const o = i * 6;
+        const io = i * 12;
+        ib[io] = o; ib[io + 1] = o + 1; ib[io + 2] = o + 2;
+        ib[io + 3] = o + 1; ib[io + 4] = o + 4; ib[io + 5] = o + 2;
+        ib[io + 6] = o + 2; ib[io + 7] = o + 4; ib[io + 8] = o + 3;
+        ib[io + 9] = o + 4; ib[io + 10] = o + 5; ib[io + 11] = o + 3;
+    }
+}

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

@@ -0,0 +1,278 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ValueCell } from '../../../mol-util';
+import { Mat4, Vec3, Vec4 } from '../../../mol-math/linear-algebra';
+import { transformPositionArray, GroupMapping, createGroupMapping} from '../../util';
+import { GeometryUtils } from '../geometry';
+import { createColors } from '../color-data';
+import { createMarkers } from '../marker-data';
+import { createSizes, getMaxSize } from '../size-data';
+import { TransformData } from '../transform-data';
+import { LocationIterator, PositionLocation } from '../../util/location-iterator';
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+import { calculateInvariantBoundingSphere, calculateTransformBoundingSphere } from '../../../mol-gl/renderable/util';
+import { Sphere3D } from '../../../mol-math/geometry';
+import { Theme } from '../../../mol-theme/theme';
+import { Color } from '../../../mol-util/color';
+import { BaseGeometry } from '../base';
+import { createEmptyOverpaint } from '../overpaint-data';
+import { createEmptyTransparency } from '../transparency-data';
+import { hashFnv32a } from '../../../mol-data/util';
+import { createEmptyClipping } from '../clipping-data';
+import { CylindersValues } from '../../../mol-gl/renderable/cylinders';
+import { RenderableState } from '../../../mol-gl/renderable';
+
+export interface Cylinders {
+    readonly kind: 'cylinders',
+
+    /** Number of cylinders */
+    cylinderCount: number,
+
+    /** Mapping buffer as array of uvw values wrapped in a value cell */
+    readonly mappingBuffer: ValueCell<Float32Array>,
+    /** Index buffer as array of vertex index triplets wrapped in a value cell */
+    readonly indexBuffer: ValueCell<Uint32Array>,
+    /** Group buffer as array of group ids for each vertex wrapped in a value cell */
+    readonly groupBuffer: ValueCell<Float32Array>,
+    /** Cylinder start buffer as array of xyz values wrapped in a value cell */
+    readonly startBuffer: ValueCell<Float32Array>,
+    /** Cylinder end buffer as array of xyz values wrapped in a value cell */
+    readonly endBuffer: ValueCell<Float32Array>,
+    /** Cylinder scale buffer as array of scaling factors wrapped in a value cell */
+    readonly scaleBuffer: ValueCell<Float32Array>,
+    /** Cylinder cap buffer as array of cap flags wrapped in a value cell */
+    readonly capBuffer: ValueCell<Float32Array>,
+
+    /** Bounding sphere of the cylinders */
+    readonly boundingSphere: Sphere3D
+    /** Maps group ids to cylinder indices */
+    readonly groupMapping: GroupMapping
+
+    setBoundingSphere(boundingSphere: Sphere3D): void
+}
+
+export namespace Cylinders {
+    export function create(mappings: Float32Array, indices: Uint32Array, groups: Float32Array, starts: Float32Array, ends: Float32Array, scales: Float32Array, caps: Float32Array, cylinderCount: number, cylinders?: Cylinders): Cylinders {
+        return cylinders ?
+            update(mappings, indices, groups, starts, ends, scales, caps, cylinderCount, cylinders) :
+            fromArrays(mappings, indices, groups, starts, ends, scales, caps, cylinderCount);
+    }
+
+    export function createEmpty(cylinders?: Cylinders): Cylinders {
+        const mb = cylinders ? cylinders.mappingBuffer.ref.value : new Float32Array(0);
+        const ib = cylinders ? cylinders.indexBuffer.ref.value : new Uint32Array(0);
+        const gb = cylinders ? cylinders.groupBuffer.ref.value : new Float32Array(0);
+        const sb = cylinders ? cylinders.startBuffer.ref.value : new Float32Array(0);
+        const eb = cylinders ? cylinders.endBuffer.ref.value : new Float32Array(0);
+        const ab = cylinders ? cylinders.scaleBuffer.ref.value : new Float32Array(0);
+        const cb = cylinders ? cylinders.capBuffer.ref.value : new Float32Array(0);
+        return create(mb, ib, gb, sb, eb, ab, cb, 0, cylinders);
+    }
+
+    function hashCode(cylinders: Cylinders) {
+        return hashFnv32a([
+            cylinders.cylinderCount, cylinders.mappingBuffer.ref.version, cylinders.indexBuffer.ref.version,
+            cylinders.groupBuffer.ref.version, cylinders.startBuffer.ref.version, cylinders.endBuffer.ref.version, cylinders.scaleBuffer.ref.version, cylinders.capBuffer.ref.version
+        ]);
+    }
+
+    function fromArrays(mappings: Float32Array, indices: Uint32Array, groups: Float32Array, starts: Float32Array, ends: Float32Array, scales: Float32Array, caps: Float32Array, cylinderCount: number): Cylinders {
+
+        const boundingSphere = Sphere3D();
+        let groupMapping: GroupMapping;
+
+        let currentHash = -1;
+        let currentGroup = -1;
+
+        const cylinders = {
+            kind: 'cylinders' as const,
+            cylinderCount,
+            mappingBuffer: ValueCell.create(mappings),
+            indexBuffer: ValueCell.create(indices),
+            groupBuffer: ValueCell.create(groups),
+            startBuffer: ValueCell.create(starts),
+            endBuffer: ValueCell.create(ends),
+            scaleBuffer: ValueCell.create(scales),
+            capBuffer: ValueCell.create(caps),
+            get boundingSphere() {
+                const newHash = hashCode(cylinders);
+                if (newHash !== currentHash) {
+                    const s = calculateInvariantBoundingSphere(cylinders.startBuffer.ref.value, cylinders.cylinderCount * 6, 6);
+                    const e = calculateInvariantBoundingSphere(cylinders.endBuffer.ref.value, cylinders.cylinderCount * 6, 6);
+
+                    Sphere3D.expandBySphere(boundingSphere, s, e);
+                    currentHash = newHash;
+                }
+                return boundingSphere;
+            },
+            get groupMapping() {
+                if (cylinders.groupBuffer.ref.version !== currentGroup) {
+                    groupMapping = createGroupMapping(cylinders.groupBuffer.ref.value, cylinders.cylinderCount, 6);
+                    currentGroup = cylinders.groupBuffer.ref.version;
+                }
+                return groupMapping;
+            },
+            setBoundingSphere(sphere: Sphere3D) {
+                Sphere3D.copy(boundingSphere, sphere);
+                currentHash = hashCode(cylinders);
+            }
+        };
+        return cylinders;
+    }
+
+    function update(mappings: Float32Array, indices: Uint32Array, groups: Float32Array, starts: Float32Array, ends: Float32Array, scales: Float32Array, caps: Float32Array, cylinderCount: number, cylinders: Cylinders) {
+        if (cylinderCount > cylinders.cylinderCount) {
+            ValueCell.update(cylinders.mappingBuffer, mappings);
+            ValueCell.update(cylinders.indexBuffer, indices);
+        }
+        cylinders.cylinderCount = cylinderCount;
+        ValueCell.update(cylinders.groupBuffer, groups);
+        ValueCell.update(cylinders.startBuffer, starts);
+        ValueCell.update(cylinders.endBuffer, ends);
+        ValueCell.update(cylinders.scaleBuffer, scales);
+        ValueCell.update(cylinders.capBuffer, caps);
+        return cylinders;
+    }
+
+    export function transform(cylinders: Cylinders, t: Mat4) {
+        const start = cylinders.startBuffer.ref.value;
+        transformPositionArray(t, start, 0, cylinders.cylinderCount * 6);
+        ValueCell.update(cylinders.startBuffer, start);
+        const end = cylinders.endBuffer.ref.value;
+        transformPositionArray(t, end, 0, cylinders.cylinderCount * 6);
+        ValueCell.update(cylinders.endBuffer, end);
+    }
+
+    //
+
+    export const Params = {
+        ...BaseGeometry.Params,
+        sizeFactor: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }),
+        sizeAspectRatio: PD.Numeric(1, { min: 0, max: 3, step: 0.01 }),
+        doubleSided: PD.Boolean(false, BaseGeometry.CustomQualityParamInfo),
+        ignoreLight: PD.Boolean(false, BaseGeometry.ShadingCategory),
+        xrayShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
+    };
+    export type Params = typeof Params
+
+    export const Utils: GeometryUtils<Cylinders, Params> = {
+        Params,
+        createEmpty,
+        createValues,
+        createValuesSimple,
+        updateValues,
+        updateBoundingSphere,
+        createRenderableState,
+        updateRenderableState,
+        createPositionIterator
+    };
+
+    function createPositionIterator(cylinders: Cylinders, transform: TransformData): LocationIterator {
+        const groupCount = cylinders.cylinderCount * 6;
+        const instanceCount = transform.instanceCount.ref.value;
+        const location = PositionLocation();
+        const p = location.position;
+        const s = cylinders.startBuffer.ref.value;
+        const e = cylinders.endBuffer.ref.value;
+        const m = transform.aTransform.ref.value;
+        const getLocation = (groupIndex: number, instanceIndex: number) => {
+            const v = groupIndex % 6 === 0 ? s : e;
+            if (instanceIndex < 0) {
+                Vec3.fromArray(p, v, groupIndex * 3);
+            } else {
+                Vec3.transformMat4Offset(p, v, m, 0, groupIndex * 3, instanceIndex * 16);
+            }
+            return location;
+        };
+        return LocationIterator(groupCount, instanceCount, 2, getLocation);
+    }
+
+    function createValues(cylinders: Cylinders, transform: TransformData, locationIt: LocationIterator, theme: Theme, props: PD.Values<Params>): CylindersValues {
+        const { instanceCount, groupCount } = locationIt;
+        const positionIt = createPositionIterator(cylinders, transform);
+
+        const color = createColors(locationIt, positionIt, theme.color);
+        const size = createSizes(locationIt, theme.size);
+        const marker = createMarkers(instanceCount * groupCount);
+        const overpaint = createEmptyOverpaint();
+        const transparency = createEmptyTransparency();
+        const clipping = createEmptyClipping();
+
+        const counts = { drawCount: cylinders.cylinderCount * 4 * 3, vertexCount: cylinders.cylinderCount * 6, groupCount, instanceCount };
+
+        const padding = getMaxSize(size) * props.sizeFactor;
+        const invariantBoundingSphere = Sphere3D.clone(cylinders.boundingSphere);
+        const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, transform.aTransform.ref.value, instanceCount);
+
+        return {
+            aMapping: cylinders.mappingBuffer,
+            aGroup: cylinders.groupBuffer,
+            aStart: cylinders.startBuffer,
+            aEnd: cylinders.endBuffer,
+            aScale: cylinders.scaleBuffer,
+            aCap: cylinders.capBuffer,
+            elements: cylinders.indexBuffer,
+            boundingSphere: ValueCell.create(boundingSphere),
+            invariantBoundingSphere: ValueCell.create(invariantBoundingSphere),
+            uInvariantBoundingSphere: ValueCell.create(Vec4.ofSphere(invariantBoundingSphere)),
+            ...color,
+            ...size,
+            ...marker,
+            ...overpaint,
+            ...transparency,
+            ...clipping,
+            ...transform,
+
+            padding: ValueCell.create(padding),
+
+            ...BaseGeometry.createValues(props, counts),
+            uSizeFactor: ValueCell.create(props.sizeFactor * props.sizeAspectRatio),
+            dDoubleSided: ValueCell.create(props.doubleSided),
+            dIgnoreLight: ValueCell.create(props.ignoreLight),
+            dXrayShaded: ValueCell.create(props.xrayShaded),
+        };
+    }
+
+    function createValuesSimple(cylinders: Cylinders, props: Partial<PD.Values<Params>>, colorValue: Color, sizeValue: number, transform?: TransformData) {
+        const s = BaseGeometry.createSimple(colorValue, sizeValue, transform);
+        const p = { ...PD.getDefaultValues(Params), ...props };
+        return createValues(cylinders, s.transform, s.locationIterator, s.theme, p);
+    }
+
+    function updateValues(values: CylindersValues, props: PD.Values<Params>) {
+        BaseGeometry.updateValues(values, props);
+        ValueCell.updateIfChanged(values.uSizeFactor, props.sizeFactor * props.sizeAspectRatio);
+        ValueCell.updateIfChanged(values.dDoubleSided, props.doubleSided);
+        ValueCell.updateIfChanged(values.dIgnoreLight, props.ignoreLight);
+        ValueCell.updateIfChanged(values.dXrayShaded, props.xrayShaded);
+    }
+
+    function updateBoundingSphere(values: CylindersValues, cylinders: Cylinders) {
+        const invariantBoundingSphere = Sphere3D.clone(cylinders.boundingSphere);
+        const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, values.aTransform.ref.value, values.instanceCount.ref.value);
+
+        if (!Sphere3D.equals(boundingSphere, values.boundingSphere.ref.value)) {
+            ValueCell.update(values.boundingSphere, boundingSphere);
+        }
+        if (!Sphere3D.equals(invariantBoundingSphere, values.invariantBoundingSphere.ref.value)) {
+            ValueCell.update(values.invariantBoundingSphere, invariantBoundingSphere);
+            ValueCell.update(values.uInvariantBoundingSphere, Vec4.fromSphere(values.uInvariantBoundingSphere.ref.value, invariantBoundingSphere));
+        }
+    }
+
+    function createRenderableState(props: PD.Values<Params>): RenderableState {
+        const state = BaseGeometry.createRenderableState(props);
+        updateRenderableState(state, props);
+        return state;
+    }
+
+    function updateRenderableState(state: RenderableState, props: PD.Values<Params>) {
+        BaseGeometry.updateRenderableState(state, props);
+        state.opaque = state.opaque && !props.xrayShaded;
+        state.writeDepth = state.opaque;
+    }
+}

+ 18 - 11
src/mol-geo/geometry/geometry.ts

@@ -22,28 +22,31 @@ import { Theme } from '../../mol-theme/theme';
 import { RenderObjectValues } from '../../mol-gl/render-object';
 import { TextureMesh } from './texture-mesh/texture-mesh';
 import { Image } from './image/image';
+import { Cylinders } from './cylinders/cylinders';
 
-export type GeometryKind = 'mesh' | 'points' | 'spheres' | 'text' | 'lines' | 'direct-volume' | 'image' | 'texture-mesh'
+export type GeometryKind = 'mesh' | 'points' | 'spheres' | 'cylinders' | 'text' | 'lines' | 'direct-volume' | 'image' | 'texture-mesh'
 
 export type Geometry<T extends GeometryKind = GeometryKind> =
     T extends 'mesh' ? Mesh :
         T extends 'points' ? Points :
             T extends 'spheres' ? Spheres :
-                T extends 'text' ? Text :
-                    T extends 'lines' ? Lines :
-                        T extends 'direct-volume' ? DirectVolume :
-                            T extends 'image' ? Image :
-                                T extends 'texture-mesh' ? TextureMesh : never
+                T extends 'cylinders' ? Cylinders :
+                    T extends 'text' ? Text :
+                        T extends 'lines' ? Lines :
+                            T extends 'direct-volume' ? DirectVolume :
+                                T extends 'image' ? Image :
+                                    T extends 'texture-mesh' ? TextureMesh : never
 
 type GeometryParams<T extends GeometryKind> =
     T extends 'mesh' ? Mesh.Params :
         T extends 'points' ? Points.Params :
             T extends 'spheres' ? Spheres.Params :
-                T extends 'text' ? Text.Params :
-                    T extends 'lines' ? Lines.Params :
-                        T extends 'direct-volume' ? DirectVolume.Params :
-                            T extends 'image' ? Image.Params :
-                                T extends 'texture-mesh' ? TextureMesh.Params : never
+                T extends 'cylinders' ? Cylinders.Params :
+                    T extends 'text' ? Text.Params :
+                        T extends 'lines' ? Lines.Params :
+                            T extends 'direct-volume' ? DirectVolume.Params :
+                                T extends 'image' ? Image.Params :
+                                    T extends 'texture-mesh' ? TextureMesh.Params : never
 
 export interface GeometryUtils<G extends Geometry, P extends PD.Params = GeometryParams<G['kind']>, V = RenderObjectValues<G['kind']>> {
     Params: P
@@ -65,6 +68,7 @@ export namespace Geometry {
             case 'mesh': return geometry.triangleCount * 3;
             case 'points': return geometry.pointCount;
             case 'spheres': return geometry.sphereCount * 2 * 3;
+            case 'cylinders': return geometry.cylinderCount * 4 * 3;
             case 'text': return geometry.charCount * 2 * 3;
             case 'lines': return geometry.lineCount * 2 * 3;
             case 'direct-volume': return 12 * 3;
@@ -78,6 +82,7 @@ export namespace Geometry {
             case 'mesh': return geometry.vertexCount;
             case 'points': return geometry.pointCount;
             case 'spheres': return geometry.sphereCount * 4;
+            case 'cylinders': return geometry.cylinderCount * 6;
             case 'text': return geometry.charCount * 4;
             case 'lines': return geometry.lineCount * 4;
             case 'direct-volume':
@@ -93,6 +98,7 @@ export namespace Geometry {
             case 'mesh':
             case 'points':
             case 'spheres':
+            case 'cylinders':
             case 'text':
             case 'lines':
                 return getDrawCount(geometry) === 0 ? 0 : (arrayMax(geometry.groupBuffer.ref.value) + 1);
@@ -111,6 +117,7 @@ export namespace Geometry {
             case 'mesh': return Mesh.Utils as any;
             case 'points': return Points.Utils as any;
             case 'spheres': return Spheres.Utils as any;
+            case 'cylinders': return Cylinders.Utils as any;
             case 'text': return Text.Utils as any;
             case 'lines': return Lines.Utils as any;
             case 'direct-volume': return DirectVolume.Utils as any;

+ 9 - 6
src/mol-gl/render-object.ts

@@ -15,6 +15,7 @@ import { SpheresValues, SpheresRenderable } from './renderable/spheres';
 import { TextValues, TextRenderable } from './renderable/text';
 import { TextureMeshValues, TextureMeshRenderable } from './renderable/texture-mesh';
 import { ImageValues, ImageRenderable } from './renderable/image';
+import { CylindersRenderable, CylindersValues } from './renderable/cylinders';
 
 const getNextId = idFactory(0, 0x7FFFFFFF);
 
@@ -28,17 +29,18 @@ export interface GraphicsRenderObject<T extends RenderObjectType = RenderObjectT
     readonly materialId: number
 }
 
-export type RenderObjectType = 'mesh' | 'points' | 'spheres' | 'text' | 'lines' | 'direct-volume' | 'image' | 'texture-mesh'
+export type RenderObjectType = 'mesh' | 'points' | 'spheres' | 'cylinders' | 'text' | 'lines' | 'direct-volume' | 'image' | 'texture-mesh'
 
 export type RenderObjectValues<T extends RenderObjectType> =
     T extends 'mesh' ? MeshValues :
         T extends 'points' ? PointsValues :
             T extends 'spheres' ? SpheresValues :
-                T extends 'text' ? TextValues :
-                    T extends 'lines' ? LinesValues :
-                        T extends 'direct-volume' ? DirectVolumeValues :
-                            T extends 'image' ? ImageValues :
-                                T extends 'texture-mesh' ? TextureMeshValues : never
+                T extends 'cylinders' ? CylindersValues :
+                    T extends 'text' ? TextValues :
+                        T extends 'lines' ? LinesValues :
+                            T extends 'direct-volume' ? DirectVolumeValues :
+                                T extends 'image' ? ImageValues :
+                                    T extends 'texture-mesh' ? TextureMeshValues : never
 
 //
 
@@ -51,6 +53,7 @@ export function createRenderable<T extends RenderObjectType>(ctx: WebGLContext,
         case 'mesh': return MeshRenderable(ctx, o.id, o.values as MeshValues, o.state, o.materialId);
         case 'points': return PointsRenderable(ctx, o.id, o.values as PointsValues, o.state, o.materialId);
         case 'spheres': return SpheresRenderable(ctx, o.id, o.values as SpheresValues, o.state, o.materialId);
+        case 'cylinders': return CylindersRenderable(ctx, o.id, o.values as CylindersValues, o.state, o.materialId);
         case 'text': return TextRenderable(ctx, o.id, o.values as TextValues, o.state, o.materialId);
         case 'lines': return LinesRenderable(ctx, o.id, o.values as LinesValues, o.state, o.materialId);
         case 'direct-volume': return DirectVolumeRenderable(ctx, o.id, o.values as DirectVolumeValues, o.state, o.materialId);

+ 40 - 0
src/mol-gl/renderable/cylinders.ts

@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Renderable, RenderableState, createRenderable } from '../renderable';
+import { WebGLContext } from '../webgl/context';
+import { createGraphicsRenderItem } from '../webgl/render-item';
+import { GlobalUniformSchema, BaseSchema, AttributeSpec, Values, InternalSchema, SizeSchema, InternalValues, ElementsSpec, ValueSpec, DefineSpec, GlobalTextureSchema } from './schema';
+import { CylindersShaderCode } from '../shader-code';
+import { ValueCell } from '../../mol-util';
+
+export const CylindersSchema = {
+    ...BaseSchema,
+    ...SizeSchema,
+    aStart: AttributeSpec('float32', 3, 0),
+    aEnd: AttributeSpec('float32', 3, 0),
+    aMapping: AttributeSpec('float32', 3, 0),
+    aScale: AttributeSpec('float32', 1, 0),
+    aCap: AttributeSpec('float32', 1, 0),
+    elements: ElementsSpec('uint32'),
+
+    padding: ValueSpec('number'),
+    dDoubleSided: DefineSpec('boolean'),
+    dIgnoreLight: DefineSpec('boolean'),
+    dXrayShaded: DefineSpec('boolean'),
+};
+export type CylindersSchema = typeof CylindersSchema
+export type CylindersValues = Values<CylindersSchema>
+
+export function CylindersRenderable(ctx: WebGLContext, id: number, values: CylindersValues, state: RenderableState, materialId: number): Renderable<CylindersValues> {
+    const schema = { ...GlobalUniformSchema, ...GlobalTextureSchema, ...InternalSchema, ...CylindersSchema };
+    const internalValues: InternalValues = {
+        uObjectId: ValueCell.create(id),
+    };
+    const shaderCode = CylindersShaderCode;
+    const renderItem = createGraphicsRenderItem(ctx, 'triangles', shaderCode, schema, { ...values, ...internalValues }, materialId);
+    return createRenderable(renderItem, values, state);
+}

+ 6 - 0
src/mol-gl/shader-code.ts

@@ -117,6 +117,8 @@ export function ShaderCode(name: string, vert: string, frag: string, extensions:
     return { id: shaderCodeId(), name, vert: addIncludes(vert), frag: addIncludes(frag), extensions };
 }
 
+// Note: `drawBuffers` need to be 'optional' for wboit
+
 import points_vert from './shader/points.vert';
 import points_frag from './shader/points.frag';
 export const PointsShaderCode = ShaderCode('points', points_vert, points_frag, { drawBuffers: 'optional' });
@@ -125,6 +127,10 @@ import spheres_vert from './shader/spheres.vert';
 import spheres_frag from './shader/spheres.frag';
 export const SpheresShaderCode = ShaderCode('spheres', spheres_vert, spheres_frag, { fragDepth: 'required', drawBuffers: 'optional' });
 
+import cylinders_vert from './shader/cylinders.vert';
+import cylinders_frag from './shader/cylinders.frag';
+export const CylindersShaderCode = ShaderCode('cylinders', cylinders_vert, cylinders_frag, { fragDepth: 'required', drawBuffers: 'optional' });
+
 import text_vert from './shader/text.vert';
 import text_frag from './shader/text.frag';
 export const TextShaderCode = ShaderCode('text', text_vert, text_frag, { standardDerivatives: 'required', drawBuffers: 'optional' });

+ 141 - 0
src/mol-gl/shader/cylinders.frag.ts

@@ -0,0 +1,141 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+export default `
+precision highp float;
+precision highp int;
+
+uniform mat4 uView;
+
+varying mat4 vTransform;
+varying vec3 vStart;
+varying vec3 vEnd;
+varying float vSize;
+varying float vCap;
+
+uniform vec3 uCameraDir;
+uniform float uIsOrtho;
+uniform vec3 uCameraPosition;
+
+#include common
+#include common_frag_params
+#include color_frag_params
+#include light_frag_params
+#include common_clip
+#include wboit_params
+
+// adapted from https://www.shadertoy.com/view/4lcSRn
+// The MIT License, Copyright 2016 Inigo Quilez
+bool CylinderImpostor(
+    in vec3 rayOrigin, in vec3 rayDir,
+    in vec3 start, in vec3 end, in float radius,
+    out vec4 intersection, out bool interior
+){
+    vec3 ba = end - start;
+    vec3 oc = rayOrigin - start;
+
+    float baba = dot(ba, ba);
+    float bard = dot(ba, rayDir);
+    float baoc = dot(ba, oc);
+
+    float k2 = baba - bard*bard;
+    float k1 = baba * dot(oc, rayDir) - baoc * bard;
+    float k0 = baba * dot(oc, oc) - baoc * baoc - radius * radius * baba;
+
+    float h = k1 * k1 - k2 * k0;
+    if (h < 0.0) return false;
+
+    bool topCap = (vCap > 0.9 && vCap < 1.1) || vCap >= 2.9;
+    bool bottomCap = (vCap > 1.9 && vCap < 2.1) || vCap >= 2.9;
+
+    // body outside
+    h = sqrt(h);
+    float t = (-k1 - h) / k2;
+    float y = baoc + t * bard;
+    if (y > 0.0 && y < baba) {
+        interior = false;
+        intersection = vec4(t, (oc + t * rayDir - ba * y / baba) / radius);
+        return true;
+    }
+
+    if (topCap && y < 0.0) {
+        // top cap
+        t = -baoc / bard;
+        if (abs(k1 + k2 * t) < h) {
+            interior = false;
+            intersection = vec4(t, ba * sign(y) / baba);
+            return true;
+        }
+    } else if(bottomCap && y >= 0.0) {
+        // bottom cap
+        t = (baba - baoc) / bard;
+        if (abs(k1 + k2 * t) < h) {
+            interior = false;
+            intersection = vec4(t, ba * sign(y) / baba);
+            return true;
+        }
+    }
+
+    #ifdef dDoubleSided
+        // body inside
+        h = -h;
+        t = (-k1 - h) / k2;
+        y = baoc + t * bard;
+        if (y > 0.0 && y < baba) {
+            interior = true;
+            intersection = vec4(t, (oc + t * rayDir - ba * y / baba) / radius);
+            return true;
+        }
+
+        // TODO: handle inside caps???
+    #endif
+
+    return false;
+}
+
+void main() {
+    #include clip_pixel
+
+    vec3 rayDir = mix(normalize(vModelPosition - uCameraPosition), uCameraDir, uIsOrtho);
+
+    vec4 intersection;
+    bool interior;
+    bool hit = CylinderImpostor(vModelPosition, rayDir, vStart, vEnd, vSize, intersection, interior);
+    if (!hit) discard;
+
+    vec3 vViewPosition = vModelPosition + intersection.x * rayDir;
+    vViewPosition = (uView * vec4(vViewPosition, 1.0)).xyz;
+    gl_FragDepthEXT = calcDepth(vViewPosition);
+
+    // bugfix (mac only?)
+    if (gl_FragDepthEXT < 0.0) discard;
+    if (gl_FragDepthEXT > 1.0) discard;
+
+    #include assign_material_color
+
+    #if defined(dRenderVariant_pick)
+        #include check_picking_alpha
+        gl_FragColor = material;
+    #elif defined(dRenderVariant_depth)
+        gl_FragColor = material;
+    #elif defined(dRenderVariant_color)
+        #ifdef dIgnoreLight
+            gl_FragColor = material;
+        #else
+            mat3 normalMatrix = transpose3(inverse3(mat3(uView)));
+            vec3 normal = normalize(normalMatrix * -normalize(intersection.yzw));
+            #include apply_light_color
+        #endif
+
+        #include apply_interior_color
+        #include apply_marker_color
+        #include apply_fog
+
+        float fragmentDepth = gl_FragDepthEXT;
+        #include wboit_write
+    #endif
+}
+`;

+ 74 - 0
src/mol-gl/shader/cylinders.vert.ts

@@ -0,0 +1,74 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+export default `
+precision highp float;
+precision highp int;
+
+#include common
+#include read_from_texture
+#include common_vert_params
+#include color_vert_params
+#include size_vert_params
+#include common_clip
+
+uniform mat4 uModelView;
+
+attribute mat4 aTransform;
+attribute float aInstance;
+attribute float aGroup;
+
+attribute vec3 aMapping;
+attribute vec3 aStart;
+attribute vec3 aEnd;
+attribute float aScale;
+attribute float aCap;
+
+varying mat4 vTransform;
+varying vec3 vStart;
+varying vec3 vEnd;
+varying float vSize;
+varying float vCap;
+
+uniform float uIsOrtho;
+uniform vec3 uCameraDir;
+
+void main() {
+    #include assign_group
+    #include assign_color_varying
+    #include assign_marker_varying
+    #include assign_clipping_varying
+    #include assign_size
+
+    mat4 modelTransform = uModel * aTransform;
+
+    vTransform = aTransform;
+    vStart = (modelTransform * vec4(aStart, 1.0)).xyz;
+    vEnd = (modelTransform * vec4(aEnd, 1.0)).xyz;
+    vSize = size * aScale;
+    vCap = aCap;
+
+    vModelPosition = (vStart + vEnd) * 0.5;
+    vec3 camDir = -mix(normalize(vModelPosition - uCameraPosition), uCameraDir, uIsOrtho);
+    vec3 dir = vEnd - vStart;
+    // ensure cylinder 'dir' is pointing towards the camera
+    if(dot(camDir, dir) < 0.0) dir = -dir;
+
+    vec3 left = cross(camDir, dir);
+    vec3 up = cross(left, dir);
+    left = vSize * normalize(left);
+    up = vSize * normalize(up);
+
+    // move vertex in object-space from center to corner
+    vModelPosition += aMapping.x * dir + aMapping.y * left + aMapping.z * up;
+
+    vec4 mvPosition = uView * vec4(vModelPosition, 1.0);
+    vViewPosition = mvPosition.xyz;
+    gl_Position = uProjection * mvPosition;
+
+    #include clip_instance
+}
+`;