Forráskód Böngészése

multi-sample pass improvements & fixes

- use float rt when possible
- test color-float-buffer support
- fix, limit samples per frame
- include camera-helper scene
Alexander Rose 4 éve
szülő
commit
cfee9d86c0

+ 2 - 2
src/mol-canvas3d/canvas3d.ts

@@ -248,9 +248,9 @@ namespace Canvas3D {
             controls.update(currentTime);
             Viewport.set(camera.viewport, 0, 0, width, height);
             const cameraChanged = camera.update();
-            multiSample.update(force || cameraChanged, currentTime);
+            const multiSampleChanged = multiSample.update(force || cameraChanged);
 
-            if (force || cameraChanged || multiSample.enabled) {
+            if (force || cameraChanged || multiSampleChanged) {
                 renderer.setViewport(0, 0, width, height);
                 if (multiSample.enabled) {
                     multiSample.render(true, p.transparentBackground);

+ 19 - 10
src/mol-canvas3d/helper/camera-helper.ts

@@ -89,7 +89,7 @@ export class CameraHelper {
     update(camera: Camera) {
         if (!this.renderObject) return;
 
-        updateCamera(this.camera, camera.viewport);
+        updateCamera(this.camera, camera.viewport, camera.viewOffset);
         Mat4.extractRotation(this.scene.view, camera.view);
 
         const r = this.renderObject.values.boundingSphere.ref.value.radius;
@@ -101,23 +101,32 @@ export class CameraHelper {
     }
 }
 
-function updateCamera(camera: Camera, viewport: Viewport) {
+function updateCamera(camera: Camera, viewport: Viewport, viewOffset: Camera.ViewOffset) {
     const { near, far } = camera;
 
-    const fullLeft = -(viewport.width - viewport.x) / 2;
-    const fullRight = (viewport.width - viewport.x) / 2;
-    const fullTop = (viewport.height - viewport.y) / 2;
-    const fullBottom = -(viewport.height - viewport.y) / 2;
+    const fullLeft = -viewport.width / 2;
+    const fullRight = viewport.width / 2;
+    const fullTop = viewport.height / 2;
+    const fullBottom = -viewport.height / 2;
 
     const dx = (fullRight - fullLeft) / 2;
     const dy = (fullTop - fullBottom) / 2;
     const cx = (fullRight + fullLeft) / 2;
     const cy = (fullTop + fullBottom) / 2;
 
-    const left = cx - dx;
-    const right = cx + dx;
-    const top = cy + dy;
-    const bottom = cy - dy;
+    let left = cx - dx;
+    let right = cx + dx;
+    let top = cy + dy;
+    let bottom = cy - dy;
+
+    if (viewOffset.enabled) {
+        const scaleW = (fullRight - fullLeft) / viewOffset.width;
+        const scaleH = (fullTop - fullBottom) / viewOffset.height;
+        left += scaleW * viewOffset.offsetX;
+        right = left + scaleW * viewOffset.width;
+        top -= scaleH * viewOffset.offsetY;
+        bottom = top - scaleH * viewOffset.height;
+    }
 
     Mat4.ortho(camera.projection, left, right, top, bottom, near, far);
 }

+ 7 - 3
src/mol-canvas3d/passes/image.ts

@@ -16,6 +16,7 @@ import { MultiSamplePass, MultiSampleParams } from './multi-sample';
 import { Camera } from '../camera';
 import { Viewport } from '../camera/util';
 import { HandleHelper } from '../helper/handle-helper';
+import { PixelData } from '../../mol-util/image';
 
 export const ImageParams = {
     transparentBackground: PD.Boolean(false),
@@ -41,7 +42,7 @@ export class ImagePass {
     get width() { return this._width; }
     get height() { return this._height; }
 
-    constructor(webgl: WebGLContext, private renderer: Renderer, scene: Scene, private camera: Camera, debugHelper: BoundingSphereHelper, handleHelper: HandleHelper, props: Partial<ImageProps>) {
+    constructor(private webgl: WebGLContext, private renderer: Renderer, scene: Scene, private camera: Camera, debugHelper: BoundingSphereHelper, handleHelper: HandleHelper, props: Partial<ImageProps>) {
         const p = { ...PD.getDefaultValues(ImageParams), ...props };
 
         this._transparentBackground = p.transparentBackground;
@@ -104,7 +105,10 @@ export class ImagePass {
     getImageData(width: number, height: number) {
         this.setSize(width, height);
         this.render();
-        const pd = this.colorTarget.getPixelData();
-        return new ImageData(new Uint8ClampedArray(pd.array), pd.width, pd.height);
+        this.colorTarget.bind();
+        const array = new Uint8Array(width * height * 4);
+        this.webgl.readPixels(0, 0, width, height, array);
+        PixelData.flipY({ array, width, height });
+        return new ImageData(new Uint8ClampedArray(array), width, height);
     }
 }

+ 59 - 77
src/mol-canvas3d/passes/multi-sample.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -60,37 +60,24 @@ export class MultiSamplePass {
     private holdTarget: RenderTarget
     private compose: ComposeRenderable
 
-    private sampleIndex = -1
-    private currentTime = 0
-    private lastRenderTime = 0
+    private sampleIndex = -2
 
     constructor(private webgl: WebGLContext, private camera: Camera, private drawPass: DrawPass, private postprocessing: PostprocessingPass, props: Partial<MultiSampleProps>) {
-        const { gl } = webgl;
-        this.colorTarget = webgl.createRenderTarget(gl.drawingBufferWidth, gl.drawingBufferHeight);
-        this.composeTarget = webgl.createRenderTarget(gl.drawingBufferWidth, gl.drawingBufferHeight);
-        this.holdTarget = webgl.createRenderTarget(gl.drawingBufferWidth, gl.drawingBufferHeight);
+        const { gl, extensions } = webgl;
+        this.colorTarget = webgl.createRenderTarget(gl.drawingBufferWidth, gl.drawingBufferHeight, false);
+        this.composeTarget = webgl.createRenderTarget(gl.drawingBufferWidth, gl.drawingBufferHeight, false, extensions.colorBufferFloat ? 'float32' : 'uint8');
+        this.holdTarget = webgl.createRenderTarget(gl.drawingBufferWidth, gl.drawingBufferHeight, false);
         this.compose = getComposeRenderable(webgl, drawPass.colorTarget.texture);
         this.props = { ...PD.getDefaultValues(MultiSampleParams), ...props };
     }
 
     get enabled() {
-        if (this.props.mode === 'temporal') {
-            if (this.currentTime - this.lastRenderTime > 200) {
-                return this.sampleIndex !== -1;
-            } else {
-                this.sampleIndex = 0;
-                return false;
-            }
-        } else if (this.props.mode === 'on') {
-            return true;
-        } else {
-            return false;
-        }
+        return this.props.mode !== 'off';
     }
 
-    update(changed: boolean, currentTime: number) {
-        if (changed) this.lastRenderTime = currentTime;
-        this.currentTime = currentTime;
+    update(changed: boolean) {
+        if (changed) this.sampleIndex = -1;
+        return this.props.mode === 'temporal' ? this.sampleIndex !== -2 : false;
     }
 
     setSize(width: number, height: number) {
@@ -140,6 +127,7 @@ export class MultiSamplePass {
             const offset = offsetList[i];
             Camera.setViewOffset(camera.viewOffset, width, height, offset[0], offset[1], width, height);
             camera.update();
+            this.drawPass.cameraHelper.update(camera);
 
             // the theory is that equal weights for each sample lead to an accumulation of rounding
             // errors. The following equation varies the sampleWeight per sample so that it is uniformly
@@ -196,15 +184,17 @@ export class MultiSamplePass {
         // each sample with camera jitter and accumulates the results.
         const offsetList = JitterVectors[ Math.max(0, Math.min(this.props.sampleLevel, 5)) ];
 
-        if (this.sampleIndex === -1) return;
+        if (this.sampleIndex === -2) return;
         if (this.sampleIndex >= offsetList.length) {
-            this.sampleIndex = -1;
+            this.sampleIndex = -2;
             return;
         }
 
-        const i = this.sampleIndex;
+        const width = drawPass.colorTarget.getWidth();
+        const height = drawPass.colorTarget.getHeight();
+        const sampleWeight = 1.0 / offsetList.length;
 
-        if (i === 0) {
+        if (this.sampleIndex === -1) {
             drawPass.render(false, transparentBackground);
             if (postprocessing.enabled) postprocessing.render(false);
             ValueCell.update(compose.values.uWeight, 1.0);
@@ -214,59 +204,57 @@ export class MultiSamplePass {
             holdTarget.bind();
             state.disable(gl.BLEND);
             compose.render();
-        }
-
-        const sampleWeight = 1.0 / offsetList.length;
-
-        camera.viewOffset.enabled = true;
-        ValueCell.update(compose.values.tColor, postprocessing.enabled ? postprocessing.target.texture : drawPass.colorTarget.texture);
-        ValueCell.update(compose.values.uWeight, sampleWeight);
-        compose.update();
-
-        const width = drawPass.colorTarget.getWidth();
-        const height = drawPass.colorTarget.getHeight();
-
-        // render the scene multiple times, each slightly jitter offset
-        // from the last and accumulate the results.
-        const numSamplesPerFrame = Math.pow(2, this.props.sampleLevel);
-        for (let i = 0; i < numSamplesPerFrame; ++i) {
-            const offset = offsetList[this.sampleIndex];
-            Camera.setViewOffset(camera.viewOffset, width, height, offset[0], offset[1], width, height);
-            camera.update();
-
-            // render scene and optionally postprocess
-            drawPass.render(false, transparentBackground);
-            if (postprocessing.enabled) postprocessing.render(false);
+            this.sampleIndex += 1;
+        } else {
+            camera.viewOffset.enabled = true;
+            ValueCell.update(compose.values.tColor, postprocessing.enabled ? postprocessing.target.texture : drawPass.colorTarget.texture);
+            ValueCell.update(compose.values.uWeight, sampleWeight);
+            compose.update();
 
-            // compose rendered scene with compose target
-            composeTarget.bind();
-            state.enable(gl.BLEND);
-            state.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD);
-            state.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
-            state.disable(gl.DEPTH_TEST);
-            state.disable(gl.SCISSOR_TEST);
-            state.depthMask(false);
-            if (this.sampleIndex === 0) {
-                state.clearColor(0, 0, 0, 0);
-                gl.clear(gl.COLOR_BUFFER_BIT);
+            // render the scene multiple times, each slightly jitter offset
+            // from the last and accumulate the results.
+            const numSamplesPerFrame = Math.pow(2, Math.max(0, this.props.sampleLevel - 2));
+            for (let i = 0; i < numSamplesPerFrame; ++i) {
+                const offset = offsetList[this.sampleIndex];
+                Camera.setViewOffset(camera.viewOffset, width, height, offset[0], offset[1], width, height);
+                camera.update();
+                this.drawPass.cameraHelper.update(camera);
+
+                // render scene and optionally postprocess
+                drawPass.render(false, transparentBackground);
+                if (postprocessing.enabled) postprocessing.render(false);
+
+                // compose rendered scene with compose target
+                composeTarget.bind();
+                state.enable(gl.BLEND);
+                state.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD);
+                state.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
+                state.disable(gl.DEPTH_TEST);
+                state.disable(gl.SCISSOR_TEST);
+                state.depthMask(false);
+                if (this.sampleIndex === 0) {
+                    state.clearColor(0, 0, 0, 0);
+                    gl.clear(gl.COLOR_BUFFER_BIT);
+                }
+                compose.render();
+
+                this.sampleIndex += 1;
+                if (this.sampleIndex >= offsetList.length ) break;
             }
-            compose.render();
+        }
 
-            this.sampleIndex += 1;
-            if (this.sampleIndex >= offsetList.length ) break;
+        if (toDrawingBuffer) {
+            webgl.unbindFramebuffer();
+        } else {
+            this.colorTarget.bind();
         }
+        gl.viewport(0, 0, width, height);
 
         const accumulationWeight = this.sampleIndex * sampleWeight;
         if (accumulationWeight > 0) {
             ValueCell.update(compose.values.uWeight, 1.0);
             ValueCell.update(compose.values.tColor, composeTarget.texture);
             compose.update();
-            if (toDrawingBuffer) {
-                webgl.unbindFramebuffer();
-            } else {
-                this.colorTarget.bind();
-            }
-            gl.viewport(0, 0, width, height);
             state.disable(gl.BLEND);
             compose.render();
         }
@@ -274,12 +262,6 @@ export class MultiSamplePass {
             ValueCell.update(compose.values.uWeight, 1.0 - accumulationWeight);
             ValueCell.update(compose.values.tColor, holdTarget.texture);
             compose.update();
-            if (toDrawingBuffer) {
-                webgl.unbindFramebuffer();
-            } else {
-                this.colorTarget.bind();
-            }
-            gl.viewport(0, 0, width, height);
             if (accumulationWeight === 0) state.disable(gl.BLEND);
             else state.enable(gl.BLEND);
             compose.render();
@@ -287,7 +269,7 @@ export class MultiSamplePass {
 
         camera.viewOffset.enabled = false;
         camera.update();
-        if (this.sampleIndex >= offsetList.length) this.sampleIndex = -1;
+        if (this.sampleIndex >= offsetList.length) this.sampleIndex = -2;
     }
 }
 

+ 1 - 1
src/mol-canvas3d/passes/postprocessing.ts

@@ -105,7 +105,7 @@ export class PostprocessingPass {
 
     constructor(private webgl: WebGLContext, private camera: Camera, drawPass: DrawPass, props: Partial<PostprocessingProps>) {
         const { gl } = webgl;
-        this.target = webgl.createRenderTarget(gl.drawingBufferWidth, gl.drawingBufferHeight);
+        this.target = webgl.createRenderTarget(gl.drawingBufferWidth, gl.drawingBufferHeight, false);
         this.props = { ...PD.getDefaultValues(PostprocessingParams), ...props };
         const { colorTarget, depthTexture, packedDepth } = drawPass;
         this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTexture, packedDepth, this.props);

+ 138 - 3
src/mol-gl/webgl/compat.ts

@@ -1,9 +1,14 @@
+import { isDebugMode } from '../../mol-util/debug';
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
+import { getErrorDescription, getGLContext } from './context';
+import { getProgram } from './program';
+import { getShader } from './shader';
+
 export type GLRenderingContext = WebGLRenderingContext | WebGL2RenderingContext
 
 export function isWebGL(gl: any): gl is WebGLRenderingContext {
@@ -139,7 +144,10 @@ export function getColorBufferFloat(gl: GLRenderingContext): COMPAT_color_buffer
         return { RGBA32F: gl.RGBA32F };
     } else {
         const ext = gl.getExtension('WEBGL_color_buffer_float');
-        if (ext === null) return null;
+        if (ext === null) {
+            // test as support may not be advertised by browsers
+            return testColorBufferFloat() ? { RGBA32F: 0x8814 } : null;
+        }
         gl.getExtension('EXT_float_blend');
         return { RGBA32F: ext.RGBA32F_EXT };
     }
@@ -267,4 +275,131 @@ export function getSRGB(gl: GLRenderingContext): COMPAT_sRGB | null {
             SRGB: ext.SRGB_EXT
         };
     }
-}
+}
+
+//
+
+const TextureTestVertShader = `
+attribute vec4 aPosition;
+
+void main() {
+  gl_Position = aPosition;
+}`;
+
+const TextureTestFragShader = `
+precision mediump float;
+uniform vec4 uColor;
+uniform sampler2D uTexture;
+
+void main() {
+  gl_FragColor = texture2D(uTexture, vec2(0.5, 0.5)) * uColor;
+}`;
+
+const TextureTestTexCoords = new Float32Array([
+    -1.0, -1.0, 1.0, -1.0, -1.0,  1.0, -1.0,  1.0, 1.0, -1.0, 1.0,  1.0
+]);
+
+export function testColorBufferFloat() {
+    // adapted from https://stackoverflow.com/questions/28827511/
+
+    // Get A WebGL context
+    const canvas = document.createElement('canvas');
+    canvas.width = 16;
+    canvas.height = 16;
+    canvas.style.width = `${16}px`;
+    canvas.style.height = `${16}px`;
+    const gl = getGLContext(canvas);
+    if (gl === null) throw new Error('Unable to get WebGL context');
+
+    const type = gl.FLOAT;
+    gl.getExtension('OES_texture_float');
+
+    // setup shaders
+    const vertShader = getShader(gl, { type: 'vert', source: TextureTestVertShader });
+    const fragShader = getShader(gl, { type: 'frag', source: TextureTestFragShader });
+    if (!vertShader || !fragShader) return false;
+
+    // setup program
+    const program = getProgram(gl);
+    gl.attachShader(program, vertShader);
+    gl.attachShader(program, fragShader);
+    gl.linkProgram(program);
+    gl.useProgram(program);
+
+    // look up where the vertex data needs to go.
+    const positionLocation = gl.getAttribLocation(program, 'aPosition');
+    const colorLoc = gl.getUniformLocation(program, 'uColor');
+    if (!colorLoc) {
+        if (isDebugMode) {
+            console.log(`error getting 'uColor' uniform location`);
+        }
+        return false;
+    }
+
+    // provide texture coordinates for the rectangle.
+    const positionBuffer = gl.createBuffer();
+    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
+    gl.bufferData(gl.ARRAY_BUFFER, TextureTestTexCoords, gl.STATIC_DRAW);
+    gl.enableVertexAttribArray(positionLocation);
+    gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
+
+    const whiteTex = gl.createTexture();
+    const whiteData = new Uint8Array([255, 255, 255, 255]);
+    gl.bindTexture(gl.TEXTURE_2D, whiteTex);
+    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, whiteData);
+
+    const tex = gl.createTexture();
+    gl.bindTexture(gl.TEXTURE_2D, tex);
+    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, type, null);
+    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
+    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
+
+    const fb = gl.createFramebuffer();
+    gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
+    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
+    const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
+    if (status !== gl.FRAMEBUFFER_COMPLETE) {
+        if (isDebugMode) {
+            console.log(`error creating framebuffer for '${type}'`);
+        }
+        return false;
+    }
+
+    // Draw the rectangle.
+    gl.bindTexture(gl.TEXTURE_2D, whiteTex);
+    gl.uniform4fv(colorLoc, [0, 10, 20, 1]);
+    gl.drawArrays(gl.TRIANGLES, 0, 6);
+
+    gl.bindTexture(gl.TEXTURE_2D, tex);
+    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
+    gl.clearColor(1, 0, 0, 1);
+    gl.clear(gl.COLOR_BUFFER_BIT);
+    gl.uniform4fv(colorLoc, [0, 1 / 10, 1 / 20, 1]);
+    gl.drawArrays(gl.TRIANGLES, 0, 6);
+
+    // Check if rendered correctly
+    const pixel = new Uint8Array(4);
+    gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel);
+    if (pixel[0] !== 0 || pixel[1] < 248 || pixel[2] < 248 || pixel[3] < 254) {
+        if (isDebugMode) {
+            console.log(`not able to actually render to '${type}' texture`);
+        }
+        return false;
+    }
+
+    // Check reading from float texture
+    if (type === gl.FLOAT) {
+        gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
+        const floatPixel = new Float32Array(4);
+        gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.FLOAT, floatPixel);
+        const error = gl.getError();
+        if (error) {
+            if (isDebugMode) {
+                console.log(`error reading float pixels: '${getErrorDescription(gl, error)}'`);
+            }
+            return false;
+        }
+    }
+
+    return true;
+}

+ 5 - 4
src/mol-gl/webgl/context.ts

@@ -15,6 +15,7 @@ import { WebGLResources, createResources } from './resources';
 import { RenderTarget, createRenderTarget } from './render-target';
 import { BehaviorSubject } from 'rxjs';
 import { now } from '../../mol-util/now';
+import { TextureFilter } from './texture';
 
 export function getGLContext(canvas: HTMLCanvasElement, contextAttributes?: WebGLContextAttributes): GLRenderingContext | null {
     function getContext(contextId: 'webgl' | 'experimental-webgl' | 'webgl2') {
@@ -31,7 +32,7 @@ function getPixelRatio() {
     return (typeof window !== 'undefined') ? window.devicePixelRatio : 1;
 }
 
-function getErrorDescription(gl: GLRenderingContext, error: number) {
+export function getErrorDescription(gl: GLRenderingContext, error: number) {
     switch (error) {
         case gl.NO_ERROR: return 'no error';
         case gl.INVALID_ENUM: return 'invalid enum';
@@ -194,7 +195,7 @@ export interface WebGLContext {
     setContextLost: () => void
     handleContextRestored: () => void
 
-    createRenderTarget: (width: number, height: number) => RenderTarget
+    createRenderTarget: (width: number, height: number, depth?: boolean, type?: 'uint8' | 'float32', filter?: TextureFilter) => RenderTarget
     unbindFramebuffer: () => void
     readPixels: (x: number, y: number, width: number, height: number, buffer: Uint8Array | Float32Array) => void
     readPixelsAsync: (x: number, y: number, width: number, height: number, buffer: Uint8Array) => Promise<void>
@@ -302,8 +303,8 @@ export function createContext(gl: GLRenderingContext): WebGLContext {
             contextRestored.next(now());
         },
 
-        createRenderTarget: (width: number, height: number) => {
-            const renderTarget = createRenderTarget(gl, resources, width, height);
+        createRenderTarget: (width: number, height: number, depth?: boolean, type?: 'uint8' | 'float32', filter?: TextureFilter) => {
+            const renderTarget = createRenderTarget(gl, resources, width, height, depth, type, filter);
             renderTargets.add(renderTarget);
             return {
                 ...renderTarget,

+ 6 - 5
src/mol-gl/webgl/extensions.ts

@@ -9,10 +9,10 @@ import { isDebugMode } from '../../mol-util/debug';
 
 export type WebGLExtensions = {
     instancedArrays: COMPAT_instanced_arrays
-    textureFloat: COMPAT_texture_float
     elementIndexUint: COMPAT_element_index_uint
 
     standardDerivatives: COMPAT_standard_derivatives | null
+    textureFloat: COMPAT_texture_float | null
     textureFloatLinear: COMPAT_texture_float_linear | null
     depthTexture: COMPAT_depth_texture | null
     blendMinMax: COMPAT_blend_minmax | null
@@ -29,10 +29,6 @@ export function createExtensions(gl: GLRenderingContext): WebGLExtensions {
     if (instancedArrays === null) {
         throw new Error('Could not find support for "instanced_arrays"');
     }
-    const textureFloat = getTextureFloat(gl);
-    if (textureFloat === null) {
-        throw new Error('Could not find support for "texture_float"');
-    }
     const elementIndexUint = getElementIndexUint(gl);
     if (elementIndexUint === null) {
         throw new Error('Could not find support for "element_index_uint"');
@@ -44,6 +40,11 @@ export function createExtensions(gl: GLRenderingContext): WebGLExtensions {
         // - can't be a required extension because it is not supported by `headless-gl`
         console.log('Could not find support for "standard_derivatives"');
     }
+    const textureFloat = getTextureFloat(gl);
+    if (isDebugMode && textureFloat === null) {
+        // TODO make sure non-support is handled downstream
+        console.log('Could not find support for "texture_float"');
+    }
     const textureFloatLinear = getTextureFloatLinear(gl);
     if (isDebugMode && textureFloatLinear === null) {
         // TODO handle non-support downstream (no gpu gaussian calc, no gpu mc???)

+ 1 - 1
src/mol-gl/webgl/program.ts

@@ -131,7 +131,7 @@ export interface ProgramProps {
     schema: RenderableSchema
 }
 
-function getProgram(gl: GLRenderingContext) {
+export function getProgram(gl: GLRenderingContext) {
     const program = gl.createProgram();
     if (program === null) {
         throw new Error('Could not create WebGL program');

+ 14 - 41
src/mol-gl/webgl/render-target.ts

@@ -4,13 +4,9 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { readPixels } from './context';
 import { idFactory } from '../../mol-util/id-factory';
-import { Texture } from './texture';
+import { Texture, TextureFilter } from './texture';
 import { Framebuffer } from './framebuffer';
-import { TextureImage } from '../renderable/util';
-import { Mutable } from '../../mol-util/type-helpers';
-import { PixelData } from '../../mol-util/image';
 import { WebGLResources } from './resources';
 import { GLRenderingContext } from './compat';
 
@@ -18,7 +14,6 @@ const getNextRenderTargetId = idFactory();
 
 export interface RenderTarget {
     readonly id: number
-    readonly image: TextureImage<any>
     readonly texture: Texture
     readonly framebuffer: Framebuffer
 
@@ -27,48 +22,32 @@ export interface RenderTarget {
     /** binds framebuffer and sets viewport to rendertarget's width and height */
     bind: () => void
     setSize: (width: number, height: number) => void
-    readBuffer: (x: number, y: number, width: number, height: number, dst: Uint8Array) => void
-    getBuffer: () => Uint8Array
-    getPixelData: () => PixelData
     reset: () => void
     destroy: () => void
 }
 
-export function createRenderTarget(gl: GLRenderingContext, resources: WebGLResources, _width: number, _height: number): RenderTarget {
-    const image: Mutable<TextureImage<Uint8Array>> = {
-        array: new Uint8Array(_width * _height * 4),
-        width: _width,
-        height: _height
-    };
+export function createRenderTarget(gl: GLRenderingContext, resources: WebGLResources, _width: number, _height: number, depth = true, type: 'uint8' | 'float32' = 'uint8', filter: TextureFilter = 'nearest'): RenderTarget {
 
     const framebuffer = resources.framebuffer();
-    const targetTexture = resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
+    const targetTexture = type === 'float32'
+        ? resources.texture('image-float32', 'rgba', 'float', filter)
+        : resources.texture('image-uint8', 'rgba', 'ubyte', filter);
     // make a depth renderbuffer of the same size as the targetTexture
-    const depthRenderbuffer = resources.renderbuffer('depth16', 'depth', _width, _height);
+    const depthRenderbuffer = depth
+        ? resources.renderbuffer('depth16', 'depth', _width, _height)
+        : null;
 
     function init() {
-        targetTexture.load(image);
+        targetTexture.define(_width, _height);
         targetTexture.attachFramebuffer(framebuffer, 'color0');
-        depthRenderbuffer.attachFramebuffer(framebuffer);
+        if (depthRenderbuffer) depthRenderbuffer.attachFramebuffer(framebuffer);
     }
     init();
 
     let destroyed = false;
 
-    function readBuffer(x: number, y: number, width: number, height: number, dst: Uint8Array) {
-        framebuffer.bind();
-        gl.viewport(0, 0, _width, _height);
-        readPixels(gl, x, y, width, height, dst);
-    }
-
-    function getBuffer() {
-        readBuffer(0, 0, _width, _height, image.array);
-        return image.array;
-    }
-
     return {
         id: getNextRenderTargetId(),
-        image,
         texture: targetTexture,
         framebuffer,
 
@@ -81,15 +60,9 @@ export function createRenderTarget(gl: GLRenderingContext, resources: WebGLResou
         setSize: (width: number, height: number) => {
             _width = width;
             _height = height;
-            image.array = new Uint8Array(_width * _height * 4);
-            image.width = _width;
-            image.height = _height;
-            targetTexture.load(image);
-            depthRenderbuffer.setSize(_width, _height);
+            targetTexture.define(_width, _height);
+            if (depthRenderbuffer) depthRenderbuffer.setSize(_width, _height);
         },
-        readBuffer,
-        getBuffer,
-        getPixelData: () => PixelData.flipY(PixelData.create(getBuffer(), _width, _height)),
         reset: () => {
             init();
         },
@@ -97,8 +70,8 @@ export function createRenderTarget(gl: GLRenderingContext, resources: WebGLResou
             if (destroyed) return;
             targetTexture.destroy();
             framebuffer.destroy();
-            depthRenderbuffer.destroy();
+            if (depthRenderbuffer) depthRenderbuffer.destroy();
             destroyed = true;
         }
     };
-}
+}

+ 1 - 1
src/mol-gl/webgl/shader.ts

@@ -27,7 +27,7 @@ export interface Shader {
     destroy: () => void
 }
 
-function getShader(gl: GLRenderingContext, props: ShaderProps) {
+export function getShader(gl: GLRenderingContext, props: ShaderProps) {
     const { type, source } = props;
     const shader = gl.createShader(type === 'vert' ? gl.VERTEX_SHADER : gl.FRAGMENT_SHADER);
     if (shader === null) {