Skip to content
Snippets Groups Projects
gpu.ts 13.07 KiB
/**
 * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
 *
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 * @author Michael Krone <michael.krone@uni-tuebingen.de>
 */

import { RuntimeContext } from 'mol-task'
import { PositionData, DensityData, DensityTextureData } from '../common'
import { Box3D } from '../../geometry'
import { GaussianDensityProps, getDelta } from '../gaussian-density'
import { OrderedSet } from 'mol-data/int'
import { Vec3, Tensor, Mat4 } from '../../linear-algebra'
import { GaussianDensityValues } from 'mol-gl/renderable/gaussian-density'
import { ValueCell, defaults } from 'mol-util'
import { RenderableState, Renderable } from 'mol-gl/renderable'
import { createRenderable, createGaussianDensityRenderObject } from 'mol-gl/render-object'
import { WebGLContext, createContext, getGLContext } from 'mol-gl/webgl/context';
import { createTexture, Texture } from 'mol-gl/webgl/texture';
import { GLRenderingContext, isWebGL2 } from 'mol-gl/webgl/compat';
import { decodeIdRGB } from 'mol-geo/geometry/picking';

/** name for shared framebuffer used for gpu gaussian surface operations */
const FramebufferName = 'gaussian-density-gpu'

export async function GaussianDensityGPU(ctx: RuntimeContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityProps): Promise<DensityData> {
    const webgl = defaults(props.webgl, getWebGLContext())
    // always use texture2d when the gaussian density needs to be downloaded from the GPU,
    // it's faster than texture3d
    console.time('GaussianDensityTexture2d')
    const { scale, bbox, texture, dim } = await GaussianDensityTexture2d(ctx, webgl, position, box, radius, props)
    console.timeEnd('GaussianDensityTexture2d')
    const { field, idField } = await fieldFromTexture2d(webgl, texture, dim)

    const transform = Mat4.identity()
    Mat4.fromScaling(transform, scale)
    Mat4.setTranslation(transform, bbox.min)

    return { field, idField, transform }
}

export async function GaussianDensityTexture(ctx: RuntimeContext, webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityProps, oldTexture?: Texture): Promise<DensityTextureData> {
    console.time(`GaussianDensityTexture, ${webgl.isWebGL2 ? '3d' : '2d'}`)
    const { texture, scale, bbox, dim } = webgl.isWebGL2 ?
        await GaussianDensityTexture3d(ctx, webgl, position, box, radius, props, oldTexture) :
        await GaussianDensityTexture2d(ctx, webgl, position, box, radius, props, oldTexture)
    console.timeEnd(`GaussianDensityTexture, ${webgl.isWebGL2 ? '3d' : '2d'}`)

    const transform = Mat4.identity()
    Mat4.fromScaling(transform, scale)
    Mat4.setTranslation(transform, bbox.min)

    return { transform, texture, bbox, gridDimension: dim }
}

//

async function GaussianDensityTexture2d(ctx: RuntimeContext, webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityProps, texture?: Texture) {
    const { smoothness } = props

    const { drawCount, positions, radii, groups, delta, expandedBox, dim } = await prepareGaussianDensityData(ctx, position, box, radius, props)
    const [ dx, dy, dz ] = dim
    const { texDimX, texDimY, texCols } = getTexture2dSize(webgl.maxTextureSize, dim)

    const minDistanceTexture = createTexture(webgl, 'image-uint8', 'rgba', 'ubyte', 'nearest')
    minDistanceTexture.define(texDimX, texDimY)

    const renderObject = getGaussianDensityRenderObject(webgl, drawCount, positions, radii, groups, minDistanceTexture, expandedBox, dim, smoothness)
    const renderable = createRenderable(webgl, renderObject)
    //

    const { gl, framebufferCache } = webgl
    const { uCurrentSlice, uCurrentX, uCurrentY } = renderObject.values

    const framebuffer = framebufferCache.get(webgl, FramebufferName).value
    framebuffer.bind()
    setRenderingDefaults(gl)

    if (!texture) texture = createTexture(webgl, 'image-uint8', 'rgba', 'ubyte', 'linear')
    texture.define(texDimX, texDimY)

    function render(fbTex: Texture) {
        fbTex.attachFramebuffer(framebuffer, 0)
        let currCol = 0
        let currY = 0
        let currX = 0
        for (let i = 0; i < dz; ++i) {
            if (currCol >= texCols) {
                currCol -= texCols
                currY += dy
                currX = 0
            }
            gl.viewport(currX, currY, dx, dy)
            ValueCell.update(uCurrentSlice, i)
            ValueCell.update(uCurrentX, currX)
            ValueCell.update(uCurrentY, currY)
            renderable.render('draw')
            ++currCol
            currX += dx
        }
    }

    setupMinDistanceRendering(webgl, renderable)
    render(minDistanceTexture)

    setupDensityRendering(webgl, renderable)
    render(texture)

    setupGroupIdRendering(webgl, renderable)
    render(texture)

    await ctx.update({ message: 'gpu gaussian density calculation' });
    await webgl.waitForGpuCommandsComplete()

    return { texture, scale: Vec3.inverse(Vec3.zero(), delta), bbox: expandedBox, dim }
}

async function GaussianDensityTexture3d(ctx: RuntimeContext, webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityProps, texture?: Texture) {
    const { smoothness } = props

    const { drawCount, positions, radii, groups, delta, expandedBox, dim } = await prepareGaussianDensityData(ctx, position, box, radius, props)
    const [ dx, dy, dz ] = dim
    const minDistanceTexture = createTexture(webgl, 'volume-uint8', 'rgba', 'ubyte', 'nearest')
    minDistanceTexture.define(dx, dy, dz)

    const renderObject = getGaussianDensityRenderObject(webgl, drawCount, positions, radii, groups, minDistanceTexture, expandedBox, dim, smoothness)
    const renderable = createRenderable(webgl, renderObject)

    //

    const { gl, framebufferCache } = webgl
    const { uCurrentSlice } = renderObject.values

    const framebuffer = framebufferCache.get(webgl, FramebufferName).value
    framebuffer.bind()
    setRenderingDefaults(gl)
    gl.viewport(0, 0, dx, dy)

    if (!texture) texture = createTexture(webgl, 'volume-uint8', 'rgba', 'ubyte', 'linear')
    texture.define(dx, dy, dz)

    function render(fbTex: Texture) {
        for (let i = 0; i < dz; ++i) {
            ValueCell.update(uCurrentSlice, i)
            fbTex.attachFramebuffer(framebuffer, 0, i)
            renderable.render('draw')
        }
    }

    setupMinDistanceRendering(webgl, renderable)
    render(minDistanceTexture)

    setupDensityRendering(webgl, renderable)
    render(texture)

    setupGroupIdRendering(webgl, renderable)
    render(texture)

    await ctx.update({ message: 'gpu gaussian density calculation' });
    await webgl.waitForGpuCommandsComplete()

    return { texture, scale: Vec3.inverse(Vec3.zero(), delta), bbox: expandedBox, dim }
}

//

let webglContext: WebGLContext
function getWebGLContext() {
    if (webglContext) return webglContext
    const canvas = document.createElement('canvas')
    const gl = getGLContext(canvas, {
        alpha: true,
        antialias: false,
        depth: false,
        preserveDrawingBuffer: true
    })
    if (!gl) throw new Error('Could not create a WebGL rendering context')
    webglContext = createContext(gl)
    return webglContext
}

async function prepareGaussianDensityData(ctx: RuntimeContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityProps) {
    const { resolution, radiusOffset } = props

    const { indices, x, y, z } = position
    const n = OrderedSet.size(indices)

    const positions = new Float32Array(n * 3)
    const radii = new Float32Array(n)
    const groups = new Float32Array(n)

    let maxRadius = 0

    for (let i = 0; i < n; ++i) {
        const j = OrderedSet.getAt(indices, i);

        positions[i * 3] = x[j]
        positions[i * 3 + 1] = y[j]
        positions[i * 3 + 2] = z[j]
        const r = radius(j) + radiusOffset
        if (maxRadius < r) maxRadius = r
        radii[i] = r
        groups[i] = i

        if (i % 10000 === 0 && ctx.shouldUpdate) {
            await ctx.update({ message: 'preparing density data', current: i, max: n })
        }
    }
    const pad = maxRadius * 2 + resolution
    const expandedBox = Box3D.expand(Box3D.empty(), box, Vec3.create(pad, pad, pad));
    const extent = Vec3.sub(Vec3.zero(), expandedBox.max, expandedBox.min)

    const delta = getDelta(expandedBox, resolution)
    const dim = Vec3.zero()
    Vec3.ceil(dim, Vec3.mul(dim, extent, delta))
    console.log('grid dim gpu', dim)

    return { drawCount: n, positions, radii, groups, delta, expandedBox, dim }
}

function getGaussianDensityRenderObject(webgl: WebGLContext, drawCount: number, positions: Float32Array, radii: Float32Array, groups: Float32Array, minDistanceTexture: Texture, box: Box3D, dimensions: Vec3, smoothness: number) {
    const extent = Vec3.sub(Vec3.zero(), box.max, box.min)
    const { texDimX, texDimY } = getTexture2dSize(webgl.maxTextureSize, dimensions)

    const values: GaussianDensityValues = {
        drawCount: ValueCell.create(drawCount),
        instanceCount: ValueCell.create(1),

        aRadius: ValueCell.create(radii),
        aPosition: ValueCell.create(positions),
        aGroup: ValueCell.create(groups),

        uCurrentSlice: ValueCell.create(0),
        uCurrentX: ValueCell.create(0),
        uCurrentY: ValueCell.create(0),
        uBboxMin: ValueCell.create(box.min),
        uBboxMax: ValueCell.create(box.max),
        uBboxSize: ValueCell.create(extent),
        uGridDim: ValueCell.create(dimensions),
        uGridTexDim: ValueCell.create(Vec3.create(texDimX, texDimY, 0)),
        uAlpha: ValueCell.create(smoothness),
        tMinDistanceTex: ValueCell.create(minDistanceTexture),

        dGridTexType: ValueCell.create(minDistanceTexture.depth > 0 ? '3d' : '2d'),
        dCalcType: ValueCell.create('density'),
    }
    const state: RenderableState = {
        visible: true,
        depthMask: false
    }

    const renderObject = createGaussianDensityRenderObject(values, state)

    return renderObject
}

function setRenderingDefaults(gl: GLRenderingContext) {
    gl.disable(gl.CULL_FACE)
    gl.frontFace(gl.CCW)
    gl.cullFace(gl.BACK)
    gl.enable(gl.BLEND)
}

function setupMinDistanceRendering(webgl: WebGLContext, renderable: Renderable<any>) {
    const { gl } = webgl
    ValueCell.update(renderable.values.dCalcType, 'minDistance')
    renderable.update()
    renderable.getProgram('draw').use()
    gl.blendFunc(gl.ONE, gl.ONE)
    // the shader writes 1 - dist so we set blending to MAX
    gl.blendEquation(webgl.extensions.blendMinMax.MAX)
}

function setupDensityRendering(webgl: WebGLContext, renderable: Renderable<any>) {
    const { gl } = webgl
    ValueCell.update(renderable.values.dCalcType, 'density')
    renderable.update()
    renderable.getProgram('draw').use()
    gl.blendFunc(gl.ONE, gl.ONE)
    gl.blendEquation(gl.FUNC_ADD)
}

function setupGroupIdRendering(webgl: WebGLContext, renderable: Renderable<any>) {
    const { gl } = webgl
    ValueCell.update(renderable.values.dCalcType, 'groupId')
    renderable.update()
    renderable.getProgram('draw').use()
    // overwrite color, don't change alpha
    gl.blendFuncSeparate(gl.ONE, gl.ZERO, gl.ZERO, gl.ONE)
    gl.blendEquation(gl.FUNC_ADD)
}

function getTexture2dSize(maxTexSize: number, gridDim: Vec3) {
    let texDimX = 0
    let texDimY = gridDim[1]
    let texRows = 1
    let texCols = gridDim[0]
    if (maxTexSize < gridDim[0] * gridDim[2]) {
        texCols =  Math.floor(maxTexSize / gridDim[0])
        texRows = Math.ceil(gridDim[2] / texCols)
        texDimX = texCols * gridDim[0]
        texDimY *= texRows
    } else {
        texDimX = gridDim[0] * gridDim[2]
    }
    return { texDimX, texDimY, texRows, texCols }
}

async function fieldFromTexture2d(ctx: WebGLContext, texture: Texture, dim: Vec3) {
    console.log('isWebGL2', isWebGL2(ctx.gl))
    console.time('fieldFromTexture2d')
    const { framebufferCache } = ctx
    const [ dx, dy, dz ] = dim
    const { width, height } = texture
    const fboTexCols = Math.floor(width / dx)

    const space = Tensor.Space(dim, [2, 1, 0], Float32Array)
    const data = space.create()
    const field = Tensor.create(space, data)
    const idData = space.create()
    const idField = Tensor.create(space, idData)

    const image = new Uint8Array(width * height * 4)

    const framebuffer = framebufferCache.get(ctx, FramebufferName).value
    framebuffer.bind()
    texture.attachFramebuffer(framebuffer, 0)
    await ctx.readPixelsAsync(0, 0, width, height, image)

    let j = 0
    let tmpCol = 0
    let tmpRow = 0
    for (let iz = 0; iz < dz; ++iz) {
        if (tmpCol >= fboTexCols ) {
            tmpCol = 0
            tmpRow += dy
        }
        for (let iy = 0; iy < dy; ++iy) {
            for (let ix = 0; ix < dx; ++ix) {
                const idx = 4 * (tmpCol * dx + (iy + tmpRow) * width + ix)
                data[j] = image[idx + 3] / 255
                idData[j] = decodeIdRGB(image[idx], image[idx + 1], image[idx + 2])
                j++
            }
        }
        tmpCol++
    }
    console.timeEnd('fieldFromTexture2d')

    return { field, idField }
}