From 2e2d65cf8c1683dc00340c76b3be7a281672b35f Mon Sep 17 00:00:00 2001
From: Alexander Rose <alex.rose@rcsb.org>
Date: Thu, 16 May 2019 17:19:35 -0700
Subject: [PATCH] image/pixel data refactoring

---
 src/mol-canvas3d/canvas3d.ts      | 13 ++++++------
 src/mol-gl/renderable/util.ts     |  4 ++--
 src/mol-gl/renderer.ts            | 12 +----------
 src/mol-gl/webgl/context.ts       | 26 +++++++++++------------
 src/mol-gl/webgl/render-target.ts |  8 ++++---
 src/mol-gl/webgl/texture.ts       |  4 ++--
 src/mol-util/image.ts             | 35 +++++++++++++++++++++++++++++++
 7 files changed, 64 insertions(+), 38 deletions(-)
 create mode 100644 src/mol-util/image.ts

diff --git a/src/mol-canvas3d/canvas3d.ts b/src/mol-canvas3d/canvas3d.ts
index a4a5648a9..8f231b245 100644
--- a/src/mol-canvas3d/canvas3d.ts
+++ b/src/mol-canvas3d/canvas3d.ts
@@ -33,6 +33,7 @@ import { ValueCell } from 'mol-util';
 import { getPostprocessingRenderable, PostprocessingParams, setPostprocessingProps } from './helper/postprocessing';
 import { JitterVectors, getComposeRenderable } from './helper/multi-sample';
 import { GLRenderingContext } from 'mol-gl/webgl/compat';
+import { PixelData } from 'mol-util/image';
 
 export const Canvas3DParams = {
     // TODO: FPS cap?
@@ -77,7 +78,7 @@ interface Canvas3D {
     resetCamera: () => void
     readonly camera: Camera
     downloadScreenshot: () => void
-    getImageData: (variant: GraphicsRenderVariant) => ImageData
+    getPixelData: (variant: GraphicsRenderVariant) => PixelData
     setProps: (props: Partial<Canvas3DProps>) => void
 
     /** Returns a copy of the current Canvas3D instance props */
@@ -620,12 +621,12 @@ namespace Canvas3D {
             downloadScreenshot: () => {
                 // TODO
             },
-            getImageData: (variant: GraphicsRenderVariant) => {
+            getPixelData: (variant: GraphicsRenderVariant) => {
                 switch (variant) {
-                    case 'draw': return renderer.getImageData()
-                    case 'pickObject': return objectPickTarget.getImageData()
-                    case 'pickInstance': return instancePickTarget.getImageData()
-                    case 'pickGroup': return groupPickTarget.getImageData()
+                    case 'draw': return webgl.getDrawingBufferPixelData()
+                    case 'pickObject': return objectPickTarget.getPixelData()
+                    case 'pickInstance': return instancePickTarget.getPixelData()
+                    case 'pickGroup': return groupPickTarget.getPixelData()
                 }
             },
             didDraw,
diff --git a/src/mol-gl/renderable/util.ts b/src/mol-gl/renderable/util.ts
index 5c768690c..7a8400e54 100644
--- a/src/mol-gl/renderable/util.ts
+++ b/src/mol-gl/renderable/util.ts
@@ -53,7 +53,7 @@ export function printTextureImage(textureImage: TextureImage<any>, scale = 1) {
     return printImageData(new ImageData(data, width, height), scale)
 }
 
-export function printImageData(imageData: ImageData, scale = 1) {
+export function printImageData(imageData: ImageData, scale = 1, pixelated = false) {
     const canvas = document.createElement('canvas')
     canvas.width = imageData.width
     canvas.height = imageData.height
@@ -66,7 +66,7 @@ export function printImageData(imageData: ImageData, scale = 1) {
         img.src = objectURL
         img.style.width = imageData.width * scale + 'px'
         img.style.height = imageData.height * scale + 'px';
-        (img.style as any).imageRendering = 'pixelated' // works in Chrome
+        if (pixelated) (img.style as any).imageRendering = 'pixelated' // supported only in Chrome
         img.style.position = 'absolute'
         img.style.top = '0px'
         img.style.left = '0px'
diff --git a/src/mol-gl/renderer.ts b/src/mol-gl/renderer.ts
index 1561924f4..0944f5232 100644
--- a/src/mol-gl/renderer.ts
+++ b/src/mol-gl/renderer.ts
@@ -8,7 +8,7 @@ import { Viewport } from 'mol-canvas3d/camera/util';
 import { Camera } from 'mol-canvas3d/camera';
 
 import Scene from './scene';
-import { WebGLContext, createImageData } from './webgl/context';
+import { WebGLContext } from './webgl/context';
 import { Mat4, Vec3, Vec4 } from 'mol-math/linear-algebra';
 import { Renderable } from './renderable';
 import { Color } from 'mol-util/color';
@@ -41,7 +41,6 @@ interface Renderer {
     render: (scene: Scene, variant: GraphicsRenderVariant) => void
     setProps: (props: Partial<RendererProps>) => void
     setViewport: (x: number, y: number, width: number, height: number) => void
-    getImageData: () => ImageData
     dispose: () => void
 }
 
@@ -257,15 +256,6 @@ namespace Renderer {
                     ValueCell.update(globalUniforms.uViewport, Vec4.set(globalUniforms.uViewport.ref.value, x, y, width, height))
                 }
             },
-            getImageData: () => {
-                const { x, y, width, height } = viewport
-                const dw = width - x
-                const dh = height - y
-                const buffer = new Uint8Array(dw * dh * 4)
-                ctx.unbindFramebuffer()
-                ctx.readPixels(x, y, width, height, buffer)
-                return createImageData(buffer, dw, dh)
-            },
 
             get props() {
                 return p
diff --git a/src/mol-gl/webgl/context.ts b/src/mol-gl/webgl/context.ts
index fa3f2d19f..c8975a722 100644
--- a/src/mol-gl/webgl/context.ts
+++ b/src/mol-gl/webgl/context.ts
@@ -12,6 +12,7 @@ import { Scheduler } from 'mol-task';
 import { isDebugMode } from 'mol-util/debug';
 import { createExtensions, WebGLExtensions } from './extensions';
 import { WebGLState, createState } from './state';
+import { PixelData } from 'mol-util/image';
 
 export function getGLContext(canvas: HTMLCanvasElement, contextAttributes?: WebGLContextAttributes): GLRenderingContext | null {
     function getContext(contextId: 'webgl' | 'experimental-webgl' | 'webgl2') {
@@ -134,19 +135,14 @@ function readPixels(gl: GLRenderingContext, x: number, y: number, width: number,
     if (isDebugMode) checkError(gl)
 }
 
-export function createImageData(buffer: ArrayLike<number>, width: number, height: number) {
-    const w = width * 4
-    const h = height
-    const data = new Uint8ClampedArray(width * height * 4)
-    for (let i = 0, maxI = h / 2; i < maxI; ++i) {
-        for (let j = 0, maxJ = w; j < maxJ; ++j) {
-            const index1 = i * w + j;
-            const index2 = (h-i-1) * w + j;
-            data[index1] = buffer[index2];
-            data[index2] = buffer[index1];
-        }
-    }
-    return new ImageData(data, width, height);
+function getDrawingBufferPixelData(gl: GLRenderingContext) {
+    const w = gl.drawingBufferWidth
+    const h = gl.drawingBufferHeight
+    const buffer = new Uint8Array(w * h * 4)
+    unbindFramebuffer(gl)
+    gl.viewport(0, 0, w, h)
+    readPixels(gl, 0, 0, w, h, buffer)
+    return PixelData.flipY(PixelData.create(buffer, w, h))
 }
 
 //
@@ -177,7 +173,7 @@ function createStats(): WebGLStats {
     }
 }
 
-
+//
 
 /** A WebGL context object, including the rendering context, resource caches and counts */
 export interface WebGLContext {
@@ -201,6 +197,7 @@ export interface WebGLContext {
     readPixelsAsync: (x: number, y: number, width: number, height: number, buffer: Uint8Array) => Promise<void>
     waitForGpuCommandsComplete: () => Promise<void>
     waitForGpuCommandsCompleteSync: () => void
+    getDrawingBufferPixelData: () => PixelData
     destroy: () => void
 }
 
@@ -286,6 +283,7 @@ export function createContext(gl: GLRenderingContext): WebGLContext {
         readPixelsAsync,
         waitForGpuCommandsComplete: () => waitForGpuCommandsComplete(gl),
         waitForGpuCommandsCompleteSync: () => waitForGpuCommandsCompleteSync(gl),
+        getDrawingBufferPixelData: () => getDrawingBufferPixelData(gl),
 
         destroy: () => {
             unbindResources(gl)
diff --git a/src/mol-gl/webgl/render-target.ts b/src/mol-gl/webgl/render-target.ts
index f09a8c13a..d82a66b7b 100644
--- a/src/mol-gl/webgl/render-target.ts
+++ b/src/mol-gl/webgl/render-target.ts
@@ -4,13 +4,14 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { WebGLContext, createImageData } from './context'
+import { WebGLContext } from './context'
 import { idFactory } from 'mol-util/id-factory';
 import { createTexture, Texture } from './texture';
 import { createFramebuffer, Framebuffer } from './framebuffer';
 import { createRenderbuffer } from './renderbuffer';
 import { TextureImage } from '../renderable/util';
 import { Mutable } from 'mol-util/type-helpers';
+import { PixelData } from 'mol-util/image';
 
 const getNextRenderTargetId = idFactory()
 
@@ -27,7 +28,7 @@ export interface RenderTarget {
     setSize: (width: number, height: number) => void
     readBuffer: (x: number, y: number, width: number, height: number, dst: Uint8Array) => void
     getBuffer: () => Uint8Array
-    getImageData: () => ImageData
+    getPixelData: () => PixelData
     destroy: () => void
 }
 
@@ -55,6 +56,7 @@ export function createRenderTarget (ctx: WebGLContext, _width: number, _height:
 
     function readBuffer(x: number, y: number, width: number, height: number, dst: Uint8Array) {
         framebuffer.bind()
+        gl.viewport(0, 0, _width, _height)
         ctx.readPixels(x, y, width, height, dst)
     }
 
@@ -86,7 +88,7 @@ export function createRenderTarget (ctx: WebGLContext, _width: number, _height:
         },
         readBuffer,
         getBuffer,
-        getImageData: () => createImageData(getBuffer(), _width, _height),
+        getPixelData: () => PixelData.flipY(PixelData.create(getBuffer(), _width, _height)),
         destroy: () => {
             if (destroyed) return
             targetTexture.destroy()
diff --git a/src/mol-gl/webgl/texture.ts b/src/mol-gl/webgl/texture.ts
index f5b7dec52..3918eb5d1 100644
--- a/src/mol-gl/webgl/texture.ts
+++ b/src/mol-gl/webgl/texture.ts
@@ -159,8 +159,8 @@ export function createTexture(ctx: WebGLContext, kind: TextureKind, _format: Tex
 
     // check texture kind and type compatability
     if (
-        (kind.endsWith('float32') && _type !== 'float') || 
-        (kind.endsWith('uint8') && _type !== 'ubyte') || 
+        (kind.endsWith('float32') && _type !== 'float') ||
+        (kind.endsWith('uint8') && _type !== 'ubyte') ||
         (kind.endsWith('depth') && _type !== 'ushort')
     ) {
         throw new Error(`texture kind '${kind}' and type '${_type}' are incompatible`)
diff --git a/src/mol-util/image.ts b/src/mol-util/image.ts
new file mode 100644
index 000000000..dcbe39a8b
--- /dev/null
+++ b/src/mol-util/image.ts
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+export { PixelData }
+
+interface PixelData {
+    readonly array: Uint8Array
+    readonly width: number
+    readonly height: number
+}
+
+namespace PixelData {
+    export function create(array: Uint8Array, width: number, height: number): PixelData {
+        return { array, width, height }
+    }
+
+    /** horizontally flips the pixel data in-place */
+    export function flipY(pixelData: PixelData): PixelData {
+        const { array, width, height } = pixelData
+        const width4 = width * 4
+        for (let i = 0, maxI = height / 2; i < maxI; ++i) {
+            for (let j = 0, maxJ = width4; j < maxJ; ++j) {
+                const index1 = i * width4 + j;
+                const index2 = (height - i - 1) * width4 + j;
+                const tmp = array[index1]
+                array[index1] = array[index2]
+                array[index2] = tmp
+            }
+        }
+        return pixelData
+    }
+}
\ No newline at end of file
-- 
GitLab