Bladeren bron

wip, spheres renderable

Alexander Rose 6 jaren geleden
bovenliggende
commit
93cc8221d9

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

@@ -17,6 +17,7 @@ import { ParamDefinition as PD } from 'mol-util/param-definition'
 import { DirectVolume } from './direct-volume/direct-volume';
 import { Color } from 'mol-util/color';
 import { Vec3 } from 'mol-math/linear-algebra';
+import { Spheres } from './spheres/spheres';
 
 //
 
@@ -40,6 +41,7 @@ export const VisualQualityOptions = VisualQualityNames.map(n => [n, n] as [Visua
 export type GeometryKindType = {
     'mesh': Mesh,
     'points': Points,
+    'spheres': Spheres,
     'lines': Lines,
     'direct-volume': DirectVolume,
 }
@@ -51,6 +53,7 @@ export namespace Geometry {
         switch (geometry.kind) {
             case 'mesh': return geometry.triangleCount * 3
             case 'points': return geometry.pointCount
+            case 'spheres': return geometry.sphereCount * 2 * 3
             case 'lines': return geometry.lineCount * 2 * 3
             case 'direct-volume': return 12 * 3
         }

+ 64 - 0
src/mol-geo/geometry/spheres/spheres-builder.ts

@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2019 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 { ChunkedArray } from 'mol-data/util';
+import { Spheres } from './spheres';
+
+const quadMapping = new Float32Array([
+    -1.0, 1.0,
+    -1.0, -1.0,
+    1.0, 1.0,
+    1.0, -1.0
+])
+
+const quadMappingIndices = new Uint16Array([
+    0, 1, 2,
+    1, 3, 2
+])
+
+export interface SpheresBuilder {
+    add(x: number, y: number, z: number, group: number): void
+    getSpheres(): Spheres
+}
+
+export namespace SpheresBuilder {
+    export function create(initialCount = 2048, chunkSize = 1024, spheres?: Spheres): SpheresBuilder {
+        initialCount *= 4
+        chunkSize *= 4
+        const centers = ChunkedArray.create(Float32Array, 3, chunkSize, spheres ? spheres.centerBuffer.ref.value : initialCount);
+        const mappings = ChunkedArray.create(Float32Array, 2, chunkSize, spheres ? spheres.mappingBuffer.ref.value : initialCount);
+        const indices = ChunkedArray.create(Uint32Array, 3, chunkSize / 2, spheres ? spheres.indexBuffer.ref.value : initialCount / 2);
+        const groups = ChunkedArray.create(Float32Array, 1, chunkSize, spheres ? spheres.groupBuffer.ref.value : initialCount);
+
+        return {
+            add: (x: number, y: number, z: number, group: number) => {
+                const offset = centers.elementCount
+                for (let i = 0; i < 4; ++i) {
+                    ChunkedArray.add3(centers, x, y, z)
+                    ChunkedArray.add2(mappings, quadMapping[i * 2], quadMapping[i * 2 + 1])
+                    ChunkedArray.add(groups, group)
+                }
+                ChunkedArray.add3(indices, offset + quadMappingIndices[0], offset + quadMappingIndices[1], offset + quadMappingIndices[2])
+                ChunkedArray.add3(indices, offset + quadMappingIndices[3], offset + quadMappingIndices[4], offset + quadMappingIndices[5])
+            },
+            getSpheres: () => {
+                const cb = ChunkedArray.compact(centers, true) as Float32Array
+                const mb = ChunkedArray.compact(mappings, true) as Float32Array
+                const ib = ChunkedArray.compact(indices, true) as Uint32Array
+                const gb = ChunkedArray.compact(groups, true) as Float32Array
+                return {
+                    kind: 'spheres',
+                    sphereCount: centers.elementCount / 4,
+                    centerBuffer: spheres ? ValueCell.update(spheres.centerBuffer, cb) : ValueCell.create(cb),
+                    mappingBuffer: spheres ? ValueCell.update(spheres.centerBuffer, mb) : ValueCell.create(mb),
+                    indexBuffer: spheres ? ValueCell.update(spheres.indexBuffer, ib) : ValueCell.create(ib),
+                    groupBuffer: spheres ? ValueCell.update(spheres.groupBuffer, gb) : ValueCell.create(gb),
+                }
+            }
+        }
+    }
+}

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

@@ -0,0 +1,148 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ValueCell } from 'mol-util';
+import { Geometry } from '../geometry';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { TransformData, createIdentityTransform } from '../transform-data';
+import { LocationIterator } from 'mol-geo/util/location-iterator';
+import { Theme } from 'mol-theme/theme';
+import { SpheresValues } from 'mol-gl/renderable/spheres';
+import { createColors, createValueColor } from '../color-data';
+import { createMarkers } from '../marker-data';
+import { calculateBoundingSphere } from 'mol-gl/renderable/util';
+import { ColorNames } from 'mol-util/color/tables';
+import { Sphere3D } from 'mol-math/geometry';
+import { createSizes, createValueSize } from '../size-data';
+
+/** Spheres */
+export interface Spheres {
+    readonly kind: 'spheres',
+
+    /** Number of spheres */
+    sphereCount: number,
+
+    /** Center buffer as array of xyz values wrapped in a value cell */
+    readonly centerBuffer: ValueCell<Float32Array>,
+    /** Mapping buffer as array of xy values wrapped in a value cell */
+    readonly mappingBuffer: ValueCell<Float32Array>,
+    /** Index buffer as array of center 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>,
+}
+
+export namespace Spheres {
+    export function createEmpty(spheres?: Spheres): Spheres {
+        const cb = spheres ? spheres.centerBuffer.ref.value : new Float32Array(0)
+        const mb = spheres ? spheres.mappingBuffer.ref.value : new Float32Array(0)
+        const ib = spheres ? spheres.indexBuffer.ref.value : new Uint32Array(0)
+        const gb = spheres ? spheres.groupBuffer.ref.value : new Float32Array(0)
+        return {
+            kind: 'spheres',
+            sphereCount: 0,
+            centerBuffer: spheres ? ValueCell.update(spheres.centerBuffer, cb) : ValueCell.create(cb),
+            mappingBuffer: spheres ? ValueCell.update(spheres.mappingBuffer, mb) : ValueCell.create(mb),
+            indexBuffer: spheres ? ValueCell.update(spheres.indexBuffer, ib) : ValueCell.create(ib),
+            groupBuffer: spheres ? ValueCell.update(spheres.groupBuffer, gb) : ValueCell.create(gb)
+        }
+    }
+
+    export const Params = {
+        ...Geometry.Params,
+        doubleSided: PD.Boolean(false),
+    }
+    export type Params = typeof Params
+
+    export function createValues(spheres: Spheres, transform: TransformData, locationIt: LocationIterator, theme: Theme, props: PD.Values<Params>): SpheresValues {
+        const { instanceCount, groupCount } = locationIt
+        if (instanceCount !== transform.instanceCount.ref.value) {
+            throw new Error('instanceCount values in TransformData and LocationIterator differ')
+        }
+
+        const color = createColors(locationIt, theme.color)
+        const size = createSizes(locationIt, theme.size)
+        const marker = createMarkers(instanceCount * groupCount)
+
+        const counts = { drawCount: spheres.sphereCount * 2 * 3, groupCount, instanceCount }
+
+        const padding = 5 // TODO get max sphere size
+        const { boundingSphere, invariantBoundingSphere } = calculateBoundingSphere(
+            spheres.centerBuffer.ref.value, spheres.sphereCount * 4,
+            transform.aTransform.ref.value, instanceCount, padding
+        )
+
+        return {
+            aPosition: spheres.centerBuffer,
+            aMapping: spheres.mappingBuffer,
+            aGroup: spheres.groupBuffer,
+            elements: spheres.indexBuffer,
+            boundingSphere: ValueCell.create(boundingSphere),
+            invariantBoundingSphere: ValueCell.create(invariantBoundingSphere),
+            ...color,
+            ...size,
+            ...marker,
+            ...transform,
+
+            padding: ValueCell.create(padding),
+
+            ...Geometry.createValues(props, counts),
+            dDoubleSided: ValueCell.create(props.doubleSided),
+        }
+    }
+
+    export function createValuesSimple(spheres: Spheres, props: Partial<PD.Values<Params>>, colorValue = ColorNames.grey, sizeValue = 1, transform?: TransformData): SpheresValues {
+        const p = { ...PD.getDefaultValues(Params), ...props }
+        if (!transform) transform = createIdentityTransform()
+        const instanceCount = transform.instanceCount.ref.value
+        const groupCount = 1
+        const color = createValueColor(colorValue)
+        const size = createValueSize(sizeValue)
+        const marker = createMarkers(instanceCount * groupCount)
+
+        const counts = { drawCount: spheres.sphereCount * 2 * 3, groupCount, instanceCount }
+
+        const { boundingSphere, invariantBoundingSphere } = calculateBoundingSphere(
+            spheres.centerBuffer.ref.value, spheres.sphereCount * 4,
+            transform.aTransform.ref.value, instanceCount, sizeValue
+        )
+
+        return {
+            aPosition: spheres.centerBuffer,
+            aMapping: spheres.mappingBuffer,
+            aGroup: spheres.groupBuffer,
+            elements: spheres.indexBuffer,
+            boundingSphere: ValueCell.create(boundingSphere),
+            invariantBoundingSphere: ValueCell.create(invariantBoundingSphere),
+            ...color,
+            ...size,
+            ...marker,
+            ...transform,
+
+            padding: ValueCell.create(sizeValue),
+
+            ...Geometry.createValues(p, counts),
+            dDoubleSided: ValueCell.create(p.doubleSided),
+        }
+    }
+
+    export function updateValues(values: SpheresValues, props: PD.Values<Params>) {
+        Geometry.updateValues(values, props)
+    }
+
+    export function updateBoundingSphere(values: SpheresValues, spheres: Spheres) {
+        const { boundingSphere, invariantBoundingSphere } = calculateBoundingSphere(
+            values.aPosition.ref.value, spheres.sphereCount * 4,
+            values.aTransform.ref.value, values.instanceCount.ref.value, values.padding.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)
+        }
+    }
+}

+ 22 - 7
src/mol-gl/render-object.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -13,17 +13,26 @@ import { DirectVolumeValues, DirectVolumeRenderable } from './renderable/direct-
 import { MeshValues, MeshRenderable } from './renderable/mesh';
 import { PointsValues, PointsRenderable } from './renderable/points';
 import { LinesValues, LinesRenderable } from './renderable/lines';
+import { SpheresValues, SpheresRenderable } from './renderable/spheres';
 
 const getNextId = idFactory(0, 0x7FFFFFFF)
 
 export interface BaseRenderObject { id: number, type: string, values: RenderableValues, state: RenderableState }
 export interface MeshRenderObject extends BaseRenderObject { type: 'mesh', values: MeshValues }
 export interface PointsRenderObject extends BaseRenderObject { type: 'points', values: PointsValues }
+export interface SpheresRenderObject extends BaseRenderObject { type: 'spheres', values: SpheresValues }
 export interface LinesRenderObject extends BaseRenderObject { type: 'lines', values: LinesValues }
-export interface GaussianDensityRenderObject extends BaseRenderObject { type: 'gaussian-density', values: GaussianDensityValues }
 export interface DirectVolumeRenderObject extends BaseRenderObject { type: 'direct-volume', values: DirectVolumeValues }
 
-export type RenderObject = MeshRenderObject | PointsRenderObject | LinesRenderObject | GaussianDensityRenderObject | DirectVolumeRenderObject
+export interface GaussianDensityRenderObject extends BaseRenderObject { type: 'gaussian-density', values: GaussianDensityValues }
+
+//
+
+export type GraphicsRenderObject = MeshRenderObject | PointsRenderObject | SpheresRenderObject | LinesRenderObject | DirectVolumeRenderObject
+
+export type ComputeRenderObject = GaussianDensityRenderObject
+
+export type RenderObject = GraphicsRenderObject | ComputeRenderObject
 
 //
 
@@ -33,22 +42,28 @@ export function createMeshRenderObject(values: MeshValues, state: RenderableStat
 export function createPointsRenderObject(values: PointsValues, state: RenderableState): PointsRenderObject {
     return { id: getNextId(), type: 'points', values, state }
 }
+export function createSpheresRenderObject(values: SpheresValues, state: RenderableState): SpheresRenderObject {
+    return { id: getNextId(), type: 'spheres', values, state }
+}
 export function createLinesRenderObject(values: LinesValues, state: RenderableState): LinesRenderObject {
     return { id: getNextId(), type: 'lines', values, state }
 }
-export function createGaussianDensityRenderObject(values: GaussianDensityValues, state: RenderableState): GaussianDensityRenderObject {
-    return { id: getNextId(), type: 'gaussian-density', values, state }
-}
 export function createDirectVolumeRenderObject(values: DirectVolumeValues, state: RenderableState): DirectVolumeRenderObject {
     return { id: getNextId(), type: 'direct-volume', values, state }
 }
 
+export function createGaussianDensityRenderObject(values: GaussianDensityValues, state: RenderableState): GaussianDensityRenderObject {
+    return { id: getNextId(), type: 'gaussian-density', values, state }
+}
+
 export function createRenderable(ctx: WebGLContext, o: RenderObject): Renderable<any> {
     switch (o.type) {
         case 'mesh': return MeshRenderable(ctx, o.id, o.values, o.state)
         case 'points': return PointsRenderable(ctx, o.id, o.values, o.state)
+        case 'spheres': return SpheresRenderable(ctx, o.id, o.values, o.state)
         case 'lines': return LinesRenderable(ctx, o.id, o.values, o.state)
-        case 'gaussian-density': return GaussianDensityRenderable(ctx, o.id, o.values, o.state)
         case 'direct-volume': return DirectVolumeRenderable(ctx, o.id, o.values, o.state)
+
+        case 'gaussian-density': return GaussianDensityRenderable(ctx, o.id, o.values, o.state)
     }
 }

+ 38 - 0
src/mol-gl/renderable/spheres.ts

@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2019 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 { createRenderItem } from '../webgl/render-item';
+import { GlobalUniformSchema, BaseSchema, AttributeSpec, Values, InternalSchema, SizeSchema, InternalValues, ElementsSpec, ValueSpec, DefineSpec } from './schema';
+import { SpheresShaderCode } from '../shader-code';
+import { ValueCell } from 'mol-util';
+
+export const SpheresSchema = {
+    ...BaseSchema,
+    ...SizeSchema,
+    aPosition: AttributeSpec('float32', 3, 0),
+    aMapping: AttributeSpec('float32', 2, 0),
+    elements: ElementsSpec('uint32'),
+
+    padding: ValueSpec('number'),
+    dDoubleSided: DefineSpec('boolean'),
+}
+export type SpheresSchema = typeof SpheresSchema
+export type SpheresValues = Values<SpheresSchema>
+
+export function SpheresRenderable(ctx: WebGLContext, id: number, values: SpheresValues, state: RenderableState): Renderable<SpheresValues> {
+    const schema = { ...GlobalUniformSchema, ...InternalSchema, ...SpheresSchema }
+    const internalValues: InternalValues = {
+        uObjectId: ValueCell.create(id),
+        uPickable: ValueCell.create(state.pickable ? 1 : 0)
+    }
+    const shaderCode = SpheresShaderCode
+    const renderItem = createRenderItem(ctx, 'triangles', shaderCode, schema, { ...values, ...internalValues })
+    const renderable = createRenderable(renderItem, values, state);
+
+    return renderable
+}

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

@@ -89,8 +89,10 @@ export function calculateTransformBoundingSphere(invariantBoundingSphere: Sphere
     return boundaryHelper.getSphere()
 }
 
-export function calculateBoundingSphere(position: Float32Array, positionCount: number, transform: Float32Array, transformCount: number): { boundingSphere: Sphere3D, invariantBoundingSphere: Sphere3D } {
+export function calculateBoundingSphere(position: Float32Array, positionCount: number, transform: Float32Array, transformCount: number, padding = 0): { boundingSphere: Sphere3D, invariantBoundingSphere: Sphere3D } {
     const invariantBoundingSphere = calculateInvariantBoundingSphere(position, positionCount)
     const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, transform, transformCount)
+    Sphere3D.expand(boundingSphere, boundingSphere, padding)
+    Sphere3D.expand(invariantBoundingSphere, invariantBoundingSphere, padding)
     return { boundingSphere, invariantBoundingSphere }
 }

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

@@ -36,6 +36,12 @@ export const PointsShaderCode = ShaderCode(
     { standardDerivatives: false, fragDepth: false }
 )
 
+export const SpheresShaderCode = ShaderCode(
+    require('mol-gl/shader/spheres.vert'),
+    require('mol-gl/shader/spheres.frag'),
+    { standardDerivatives: false, fragDepth: true }
+)
+
 export const LinesShaderCode = ShaderCode(
     require('mol-gl/shader/lines.vert'),
     require('mol-gl/shader/lines.frag'),

+ 187 - 0
src/mol-gl/shader/spheres.frag

@@ -0,0 +1,187 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+precision highp float;
+precision highp int;
+
+#pragma glslify: import('./chunks/common-frag-params.glsl')
+#pragma glslify: import('./chunks/color-frag-params.glsl')
+
+// uniform vec3 uLightPosition;
+uniform vec3 uLightColor;
+uniform vec3 uLightAmbient;
+uniform mat4 uView;
+
+uniform mat4 uProjection;
+// uniform vec3 interiorColor;
+// uniform float interiorDarkening;
+vec3 interiorColor = vec3(1.0, 0.5, 0.5);
+float interiorDarkening = 0.0;
+
+uniform float clipNear;
+// uniform float ortho;
+float ortho = 0.0;
+
+varying float vRadius;
+varying float vRadiusSq;
+varying vec3 vPoint;
+varying vec3 vPointViewPosition;
+
+#pragma glslify: attenuation = require(./utils/attenuation.glsl)
+#pragma glslify: calculateSpecular = require(./utils/phong-specular.glsl)
+#pragma glslify: calculateDiffuse = require(./utils/oren-nayar-diffuse.glsl)
+
+const float specularScale = 0.15;
+const float shininess = 200.0;
+const float roughness = 100.0;
+const float albedo = 0.95;
+
+bool flag2 = false;
+bool interior = false;
+vec3 cameraPos;
+vec3 cameraNormal;
+
+// Calculate depth based on the given camera position.
+float calcDepth(in vec3 cameraPos){
+    vec2 clipZW = cameraPos.z * uProjection[2].zw + uProjection[3].zw;
+    return 0.5 + 0.5 * clipZW.x / clipZW.y;
+}
+
+float calcClip(in vec3 cameraPos) {
+    return dot(vec4(cameraPos, 1.0), vec4(0.0, 0.0, 1.0, clipNear - 0.5));
+}
+
+bool Impostor(out vec3 cameraPos, out vec3 cameraNormal){
+    vec3 cameraSpherePos = -vPointViewPosition;
+    cameraSpherePos.z += vRadius;
+
+    vec3 rayOrigin = mix(vec3(0.0, 0.0, 0.0), vPoint, ortho);
+    vec3 rayDirection = mix(normalize(vPoint), vec3(0.0, 0.0, 1.0), ortho);
+    vec3 cameraSphereDir = mix(cameraSpherePos, rayOrigin - cameraSpherePos, ortho);
+
+    float B = dot(rayDirection, cameraSphereDir);
+    float det = B * B + vRadiusSq - dot(cameraSphereDir, cameraSphereDir);
+
+    if(det < 0.0){
+        discard;
+        return false;
+    }
+
+    float sqrtDet = sqrt(det);
+    float posT = mix(B + sqrtDet, B + sqrtDet, ortho);
+    float negT = mix(B - sqrtDet, sqrtDet - B, ortho);
+
+    cameraPos = rayDirection * negT + rayOrigin;
+
+    #ifdef NEAR_CLIP
+        if(calcDepth(cameraPos) <= 0.0){
+            cameraPos = rayDirection * posT + rayOrigin;
+            interior = true;
+        }else if(calcClip(cameraPos) > 0.0){
+            cameraPos = rayDirection * posT + rayOrigin;
+            interior = true;
+            flag2 = true;
+        }
+    #else
+        if(calcDepth(cameraPos) <= 0.0){
+            cameraPos = rayDirection * posT + rayOrigin;
+            interior = true;
+        }
+    #endif
+
+    cameraNormal = normalize(cameraPos - cameraSpherePos);
+    cameraNormal *= float(!interior) * 2.0 - 1.0;
+
+    return !interior;
+}
+
+void main2(void){
+    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
+}
+
+void main(void){
+    bool flag = Impostor(cameraPos, cameraNormal);
+
+    #ifdef NEAR_CLIP
+        if(calcClip(cameraPos) > 0.0)
+            discard;
+    #endif
+
+    // FIXME not compatible with custom clipping plane
+    // Set the depth based on the new cameraPos.
+    gl_FragDepthEXT = calcDepth(cameraPos);
+    if(!flag){
+        // clamp to near clipping plane and add a tiny value to
+        // make spheres with a greater radius occlude smaller ones
+        #ifdef NEAR_CLIP
+            if( flag2 ){
+                gl_FragDepthEXT = max(0.0, calcDepth(vec3(-(clipNear - 0.5))) + (0.0000001 / vRadius));
+            }else if(gl_FragDepthEXT >= 0.0){
+                gl_FragDepthEXT = 0.0 + (0.0000001 / vRadius);
+            }
+        #else
+            if(gl_FragDepthEXT >= 0.0){
+                gl_FragDepthEXT = 0.0 + (0.0000001 / vRadius);
+            }
+        #endif
+    }
+
+    // bugfix (mac only?)
+    if (gl_FragDepthEXT < 0.0)
+        discard;
+    if (gl_FragDepthEXT > 1.0)
+        discard;
+
+    // material color
+    #pragma glslify: import('./chunks/assign-material-color.glsl')
+
+    #if defined(dColorType_objectPicking) || defined(dColorType_instancePicking) || defined(dColorType_groupPicking)
+        if (uAlpha < uPickingAlphaThreshold)
+            discard; // ignore so the element below can be picked
+        gl_FragColor = material;
+    #else
+
+        vec3 vNormal = cameraNormal;
+        vec3 vViewPosition = -cameraPos;
+
+        // determine surface to light direction
+        // vec4 viewLightPosition = view * vec4(lightPosition, 1.0);
+        // vec3 lightVector = viewLightPosition.xyz - vViewPosition;
+        vec3 lightVector = vViewPosition;
+
+        vec3 L = normalize(lightVector); // light direction
+        vec3 V = normalize(vViewPosition); // eye direction
+
+        vec3 N = normalize(vNormal);
+        #ifdef dDoubleSided
+            N = N * (float(gl_FrontFacing) * 2.0 - 1.0);
+        #endif
+
+        // compute our diffuse & specular terms
+        float specular = calculateSpecular(L, V, N, shininess) * specularScale;
+        vec3 diffuse = uLightColor * calculateDiffuse(L, V, N, roughness, albedo);
+        vec3 ambient = uLightAmbient;
+
+        // add the lighting
+        vec3 finalColor = material.rgb * (diffuse + ambient) + specular;
+
+        // gl_FragColor.rgb = N;
+        // gl_FragColor.a = 1.0;
+        // gl_FragColor.rgb = vec3(1.0, 0.0, 0.0);
+        gl_FragColor.rgb = finalColor;
+        gl_FragColor.a = material.a;
+
+        if(interior){
+            #ifdef USE_INTERIOR_COLOR
+                gl_FragColor.rgb = interiorColor;
+            #endif
+            gl_FragColor.rgb *= 1.0 - interiorDarkening;
+        }
+
+        #pragma glslify: import('./chunks/apply-marker-color.glsl')
+        #pragma glslify: import('./chunks/apply-fog.glsl')
+    #endif
+}

+ 104 - 0
src/mol-gl/shader/spheres.vert

@@ -0,0 +1,104 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+precision highp float;
+precision highp int;
+
+#pragma glslify: import('./chunks/common-vert-params.glsl')
+#pragma glslify: import('./chunks/color-vert-params.glsl')
+#pragma glslify: import('./chunks/size-vert-params.glsl')
+
+uniform mat4 uModelView;
+uniform mat4 uInvProjection;
+
+attribute vec3 aPosition;
+attribute vec2 aMapping;
+attribute mat4 aTransform;
+attribute float aInstance;
+attribute float aGroup;
+
+varying float vRadius;
+varying float vRadiusSq;
+varying vec3 vPoint;
+varying vec3 vPointViewPosition;
+
+#pragma glslify: matrixScale = require(./utils/matrix-scale.glsl)
+
+const mat4 D = mat4(
+    1.0, 0.0, 0.0, 0.0,
+    0.0, 1.0, 0.0, 0.0,
+    0.0, 0.0, 1.0, 0.0,
+    0.0, 0.0, 0.0, -1.0
+);
+
+mat4 transpose2(in mat4 inMatrix) {
+    vec4 i0 = inMatrix[0];
+    vec4 i1 = inMatrix[1];
+    vec4 i2 = inMatrix[2];
+    vec4 i3 = inMatrix[3];
+
+    mat4 outMatrix = mat4(
+        vec4(i0.x, i1.x, i2.x, i3.x),
+        vec4(i0.y, i1.y, i2.y, i3.y),
+        vec4(i0.z, i1.z, i2.z, i3.z),
+        vec4(i0.w, i1.w, i2.w, i3.w)
+    );
+    return outMatrix;
+}
+
+/**
+ * Compute point size and center using the technique described in:
+ * "GPU-Based Ray-Casting of Quadratic Surfaces" http://dl.acm.org/citation.cfm?id=2386396
+ * by Christian Sigg, Tim Weyrich, Mario Botsch, Markus Gross.
+ */
+void quadraticProjection(const in float radius, const in vec3 position){
+    vec2 xbc, ybc;
+
+    mat4 T = mat4(
+        radius, 0.0, 0.0, 0.0,
+        0.0, radius, 0.0, 0.0,
+        0.0, 0.0, radius, 0.0,
+        position.x, position.y, position.z, 1.0
+    );
+
+    mat4 R = transpose2(uProjection * uModelView * aTransform * T);
+    float A = dot(R[3], D * R[3]);
+    float B = -2.0 * dot(R[0], D * R[3]);
+    float C = dot(R[0], D * R[0]);
+    xbc[0] = (-B - sqrt(B * B - 4.0 * A * C)) / (2.0 * A);
+    xbc[1] = (-B + sqrt(B * B - 4.0 * A * C)) / (2.0 * A);
+    float sx = abs(xbc[0] - xbc[1]) * 0.5;
+
+    A = dot(R[3], D * R[3]);
+    B = -2.0 * dot(R[1], D * R[3]);
+    C = dot(R[1], D * R[1]);
+    ybc[0] = (-B - sqrt(B * B - 4.0 * A * C)) / (2.0 * A);
+    ybc[1] = (-B + sqrt(B * B - 4.0 * A * C)) / (2.0 * A);
+    float sy = abs(ybc[0] - ybc[1]) * 0.5;
+
+    gl_Position.xy = vec2(0.5 * (xbc.x + xbc.y), 0.5 * (ybc.x + ybc.y));
+    gl_Position.xy -= aMapping * vec2(sx, sy);
+    gl_Position.xy *= gl_Position.w;
+}
+
+
+void main(void){
+    #pragma glslify: import('./chunks/assign-color-varying.glsl')
+    #pragma glslify: import('./chunks/assign-size.glsl')
+
+    vRadius = size * matrixScale(uModelView);
+
+    vec4 mvPosition = uModelView * aTransform * vec4(aPosition, 1.0);
+    mvPosition.z -= vRadius; // avoid clipping, added again in fragment shader
+
+    gl_Position = uProjection * vec4(mvPosition.xyz, 1.0);
+    quadraticProjection(size, aPosition);
+
+    vRadiusSq = vRadius * vRadius;
+    vec4 vPoint4 = uInvProjection * gl_Position;
+    vPoint = vPoint4.xyz / vPoint4.w;
+    vPointViewPosition = -mvPosition.xyz / mvPosition.w;
+}

+ 6 - 0
src/mol-math/geometry/primitives/sphere3d.ts

@@ -80,6 +80,12 @@ namespace Sphere3D {
         return out
     }
 
+    /** Expand sphere by delta */
+    export function expand(out: Sphere3D, sphere: Sphere3D, delta: number): Sphere3D {
+        out.radius = sphere.radius + delta
+        return out
+    }
+
     /**
      * Returns whether or not the spheres have exactly the same center and radius (when compared with ===)
      */