Skip to content
Snippets Groups Projects
Commit 79dba8ea authored by Alexander Rose's avatar Alexander Rose
Browse files

wip, gpu gaussian density

parent 9f9c633a
No related branches found
No related tags found
No related merge requests found
......@@ -69,7 +69,6 @@ export function GaussianDensityPointVisual(): UnitsVisual<GaussianDensityPointPr
if (newProps.radiusOffset !== currentProps.radiusOffset) state.createGeometry = true
if (newProps.smoothness !== currentProps.smoothness) state.createGeometry = true
if (newProps.useGpu !== currentProps.useGpu) state.createGeometry = true
if (newProps.readSlices !== currentProps.readSlices) state.createGeometry = true
if (newProps.ignoreCache !== currentProps.ignoreCache) state.createGeometry = true
}
})
......
......@@ -68,7 +68,6 @@ export function GaussianDensityVolumeVisual(): UnitsVisual<GaussianDensityVolume
if (newProps.radiusOffset !== currentProps.radiusOffset) state.createGeometry = true
if (newProps.smoothness !== currentProps.smoothness) state.createGeometry = true
if (newProps.useGpu !== currentProps.useGpu) state.createGeometry = true
if (newProps.readSlices !== currentProps.readSlices) state.createGeometry = true
if (newProps.ignoreCache !== currentProps.ignoreCache) state.createGeometry = true
}
})
......
......@@ -93,7 +93,6 @@ export function GaussianSurfaceVisual(): UnitsVisual<GaussianSurfaceProps> {
if (newProps.radiusOffset !== currentProps.radiusOffset) state.createGeometry = true
if (newProps.smoothness !== currentProps.smoothness) state.createGeometry = true
if (newProps.useGpu !== currentProps.useGpu) state.createGeometry = true
if (newProps.readSlices !== currentProps.readSlices) state.createGeometry = true
if (newProps.ignoreCache !== currentProps.ignoreCache) state.createGeometry = true
}
})
......
......@@ -53,7 +53,6 @@ export function GaussianWireframeVisual(): UnitsVisual<GaussianWireframeProps> {
if (newProps.radiusOffset !== currentProps.radiusOffset) state.createGeometry = true
if (newProps.smoothness !== currentProps.smoothness) state.createGeometry = true
if (newProps.useGpu !== currentProps.useGpu) state.createGeometry = true
if (newProps.readSlices !== currentProps.readSlices) state.createGeometry = true
if (newProps.ignoreCache !== currentProps.ignoreCache) state.createGeometry = true
}
})
......
......@@ -114,7 +114,11 @@ export interface Texture {
readonly internalFormat: number
readonly type: number
define: (x: number, y: number, z: number) => void
readonly width: number
readonly height: number
readonly depth: number
define: (width: number, height: number, depth?: number) => void
load: (image: TextureImage<any>) => void
bind: (id: TextureId) => void
unbind: (id: TextureId) => void
......@@ -151,6 +155,8 @@ export function createTexture(ctx: Context, kind: TextureKind, _format: TextureF
gl.texParameteri(target, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.bindTexture(target, null)
let width = 0, height = 0, depth = 0
let destroyed = false
ctx.textureCount += 1
......@@ -161,7 +167,12 @@ export function createTexture(ctx: Context, kind: TextureKind, _format: TextureF
internalFormat,
type,
define: (width: number, height: number, depth?: number) => {
get width () { return width },
get height () { return height },
get depth () { return depth },
define: (_width: number, _height: number, _depth?: number) => {
width = _width, height = _height, depth = _depth || 0
gl.bindTexture(target, texture)
if (target === gl.TEXTURE_2D) {
// TODO remove cast when webgl2 types are fixed
......@@ -178,11 +189,13 @@ export function createTexture(ctx: Context, kind: TextureKind, _format: TextureF
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE);
if (target === gl.TEXTURE_2D) {
const { array, width, height } = data as TextureImage<any>;
const { array, width: _width, height: _height } = data as TextureImage<any>
width = _width, height = _height;
// TODO remove cast when webgl2 types are fixed
(gl as WebGLRenderingContext).texImage2D(target, 0, internalFormat, width, height, 0, format, type, array)
} else if (target === (gl as WebGL2RenderingContext).TEXTURE_3D) {
const { array, width, height, depth } = data as TextureVolume<any>;
const { array, width: _width, height: _height, depth: _depth } = data as TextureVolume<any>
width = _width, height = _height, depth = _depth;
(gl as WebGL2RenderingContext).texImage3D(target, 0, internalFormat, width, height, depth, 0, format, type, array)
} else {
throw new Error('unknown texture target')
......
......@@ -19,7 +19,6 @@ export const DefaultGaussianDensityProps = {
resolution: 1,
radiusOffset: 0,
smoothness: 1.5,
readSlices: false,
useGpu: true,
}
export type GaussianDensityProps = typeof DefaultGaussianDensityProps
......
......@@ -21,143 +21,48 @@ import { createFramebuffer } from 'mol-gl/webgl/framebuffer';
import { createTexture, Texture, TextureAttachment } from 'mol-gl/webgl/texture';
import { GLRenderingContext } from 'mol-gl/webgl/compat';
let webglContext: Context
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
}
export async function GaussianDensityGPU(ctx: RuntimeContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityProps): Promise<DensityData> {
// TODO allow passing a context via props
const webgl = getWebGLContext()
if (webgl.maxDrawBuffers > 0) {
console.log('GaussianDensityMultiDrawBuffer')
const { texture, scale, bbox, dim } = await GaussianDensityMultiDrawBuffer(ctx, webgl, position, box, radius, props)
console.time('gpu gaussian density 3d texture read')
const field = fieldFromTexture3d(webgl, texture, dim)
console.timeEnd('gpu gaussian density 3d texture read')
const idData = field.space.create()
const idField = Tensor.create(field.space, idData)
const transform = Mat4.identity()
Mat4.fromScaling(transform, scale)
Mat4.setTranslation(transform, bbox.min)
const renderTarget = createRenderTarget(webgl, dim[0], dim[1])
return { field, idField, transform, renderTarget, bbox, gridDimension: dim }
} else {
console.log('GaussianDensitySingleDrawBuffer')
return GaussianDensitySingleDrawBuffer(ctx, webgl, position, box, radius, props)
}
}
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)
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
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, delta, expandedBox, dim }
}
function getGaussianDensityRenderObject(webgl: Context, drawCount: number, positions: Float32Array, radii: Float32Array, box: Box3D, dimensions: Vec3, smoothness: number) {
const extent = Vec3.sub(Vec3.zero(), box.max, box.min)
const values: GaussianDensityValues = {
drawCount: ValueCell.create(drawCount),
instanceCount: ValueCell.create(1),
aRadius: ValueCell.create(radii),
aPosition: ValueCell.create(positions),
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),
uAlpha: ValueCell.create(smoothness),
const useMultiDraw = webgl.maxDrawBuffers > 0
dDrawBuffers: ValueCell.create(Math.min(8, webgl.maxDrawBuffers)),
}
const state: RenderableState = {
visible: true,
depthMask: false
}
console.time('gpu gaussian density render')
const { texture, scale, bbox, dim } = useMultiDraw ?
await GaussianDensityMultiDrawBuffer(ctx, webgl, position, box, radius, props) :
await GaussianDensitySingleDrawBuffer(ctx, webgl, position, box, radius, props)
console.timeEnd('gpu gaussian density render')
const renderObject = createGaussianDensityRenderObject(values, state)
console.time('gpu gaussian density read')
const field = useMultiDraw ?
fieldFromTexture3d(webgl, texture, dim) :
fieldFromTexture2d(webgl, texture, dim)
console.timeEnd('gpu gaussian density read')
return renderObject
}
const idData = field.space.create()
const idField = Tensor.create(field.space, idData)
//
const transform = Mat4.identity()
Mat4.fromScaling(transform, scale)
Mat4.setTranslation(transform, bbox.min)
function setRenderingDefaults(gl: GLRenderingContext) {
gl.disable(gl.CULL_FACE)
gl.frontFace(gl.CCW)
gl.cullFace(gl.BACK)
const renderTarget = createRenderTarget(webgl, dim[0], dim[1])
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.blendEquation(gl.FUNC_ADD)
gl.enable(gl.BLEND)
return { field, idField, transform, renderTarget, bbox, gridDimension: dim }
}
//
async function GaussianDensitySingleDrawBuffer(ctx: RuntimeContext, webgl: Context, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityProps): Promise<DensityData> {
const { readSlices, smoothness } = props
async function GaussianDensitySingleDrawBuffer(ctx: RuntimeContext, webgl: Context, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityProps) {
const { smoothness } = props
const { drawCount, positions, radii, delta, expandedBox, dim } = await prepareGaussianDensityData(ctx, position, box, radius, props)
const [ dx, dy, dz ] = dim
const renderObject = getGaussianDensityRenderObject(webgl, drawCount, positions, radii, expandedBox, dim, smoothness)
const renderable = createRenderable(webgl, renderObject)
//
// TODO fallback to lower resolution when texture size is not large enough
const maxTexSize = webgl.maxTextureSize
let fboTexDimX = 0
let fboTexDimY = dim[1]
......@@ -172,93 +77,43 @@ async function GaussianDensitySingleDrawBuffer(ctx: RuntimeContext, webgl: Conte
fboTexDimX = dim[0] * dim[2]
}
console.log('dim', dim, 'cols', fboTexCols, 'rows', fboTexRows)
//
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 { gl } = webgl
const { uCurrentSlice, uCurrentX, uCurrentY } = renderObject.values
const program = renderable.getProgram('draw')
const renderTarget = createRenderTarget(webgl, fboTexDimX, fboTexDimY)
const framebuffer = createFramebuffer(webgl)
framebuffer.bind()
const texture = createTexture(webgl, 'image-uint8', 'rgba', 'ubyte', 'linear')
texture.define(fboTexDimX, fboTexDimY)
const program = renderable.getProgram('draw')
program.use()
renderTarget.bind()
setRenderingDefaults(gl)
texture.attachFramebuffer(framebuffer, 0)
const slice = new Uint8Array(dim[0] * dim[1] * 4)
console.time('gpu gaussian density slices')
let currCol = 0
let currY = 0
let currX = 0
let j = 0
for (let i = 0; i < dim[2]; ++i) {
for (let i = 0; i < dz; ++i) {
if (currCol >= fboTexCols) {
currCol -= fboTexCols
currY += dim[1]
currY += dy
currX = 0
}
gl.viewport(currX, currY, dim[0], dim[1])
gl.viewport(currX, currY, dx, dy)
ValueCell.update(uCurrentSlice, i)
ValueCell.update(uCurrentX, currX)
ValueCell.update(uCurrentY, currY)
renderable.render('draw')
if (readSlices) {
renderTarget.readBuffer(currX, currY, dim[0], dim[1], slice)
for (let iy = 0; iy < dim[1]; ++iy) {
for (let ix = 0; ix < dim[0]; ++ix) {
data[j] = slice[4 * (iy * dim[0] + ix)] / 255
++j
}
}
}
++currCol
currX += dim[0]
}
console.timeEnd('gpu gaussian density slices')
//
if (!readSlices) {
console.time('gpu gaussian density full')
renderTarget.getBuffer()
const { array } = renderTarget.image
let idx = 0
let tmpCol = 0
let tmpRow = 0
for (let iz = 0; iz < dim[2]; ++iz) {
if (tmpCol >= fboTexCols ) {
tmpCol = 0
tmpRow += dim[1]
}
for (let iy = 0; iy < dim[1]; ++iy) {
for (let ix = 0; ix < dim[0]; ++ix) {
data[idx] = array[4 * (tmpCol * dim[0] + (iy + tmpRow) * fboTexDimX + ix)] / 255
idx++
}
}
tmpCol++
}
console.timeEnd('gpu gaussian density full')
currX += dx
}
//
const transform = Mat4.identity()
Mat4.fromScaling(transform, Vec3.inverse(Vec3.zero(), delta))
Mat4.setTranslation(transform, expandedBox.min)
framebuffer.destroy() // clean up
return { field, idField, transform, renderTarget, bbox: expandedBox, gridDimension: dim }
return { texture, scale: Vec3.inverse(Vec3.zero(), delta), bbox: expandedBox, dim }
}
async function GaussianDensityMultiDrawBuffer(ctx: RuntimeContext, webgl: Context, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityProps) {
......@@ -299,14 +154,12 @@ async function GaussianDensityMultiDrawBuffer(ctx: RuntimeContext, webgl: Contex
gl.viewport(0, 0, dx, dy)
setRenderingDefaults(gl)
//
// z-slices to be render with multi render targets
const dzMulti = Math.floor(dz / drawBuffers) * drawBuffers
// render multi target
const programMulti = renderable.getProgram('draw')
programMulti.use()
console.time('gpu gaussian density 3d texture multi')
for (let i = 0; i < dzMulti; i += drawBuffers) {
ValueCell.update(uCurrentSlice, i)
for (let k = 0; k < drawBuffers; ++k) {
......@@ -314,54 +167,185 @@ async function GaussianDensityMultiDrawBuffer(ctx: RuntimeContext, webgl: Contex
}
renderable.render('draw')
}
console.timeEnd('gpu gaussian density 3d texture multi')
// render single target
ValueCell.updateIfChanged(renderable.values.dDrawBuffers, 1)
renderable.update()
const programSingle = renderable.getProgram('draw')
programSingle.use()
console.time('gpu gaussian density 3d texture single')
for (let i = dzMulti; i < dz; ++i) {
ValueCell.update(uCurrentSlice, i)
texture.attachFramebuffer(framebuffer, 0, i)
renderable.render('draw')
}
console.timeEnd('gpu gaussian density 3d texture single')
// must detach framebuffer attachments before reading is possible
for (let k = 0; k < drawBuffers; ++k) {
texture.detachFramebuffer(framebuffer, k as TextureAttachment)
}
framebuffer.destroy() // clean up
// throw new Error('foo')
framebuffer.destroy() // clean up
return { texture, scale: Vec3.inverse(Vec3.zero(), delta), bbox: expandedBox, dim }
}
//
function fieldFromTexture3d(ctx: Context, texture: Texture, dim: Vec3) {
let webglContext: Context
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)
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
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, delta, expandedBox, dim }
}
function getGaussianDensityRenderObject(webgl: Context, drawCount: number, positions: Float32Array, radii: Float32Array, box: Box3D, dimensions: Vec3, smoothness: number) {
const extent = Vec3.sub(Vec3.zero(), box.max, box.min)
const values: GaussianDensityValues = {
drawCount: ValueCell.create(drawCount),
instanceCount: ValueCell.create(1),
aRadius: ValueCell.create(radii),
aPosition: ValueCell.create(positions),
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),
uAlpha: ValueCell.create(smoothness),
dDrawBuffers: ValueCell.create(Math.min(8, webgl.maxDrawBuffers)),
}
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.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.blendEquation(gl.FUNC_ADD)
gl.enable(gl.BLEND)
}
function fieldFromTexture2d(ctx: Context, texture: Texture, dim: Vec3) {
const { gl } = 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 image = new Uint8Array(width * height * 4)
const framebuffer = createFramebuffer(ctx)
framebuffer.bind()
texture.attachFramebuffer(framebuffer, 0)
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, image)
let idx = 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) {
data[idx] = image[4 * (tmpCol * dx + (iy + tmpRow) * width + ix)] / 255
idx++
}
}
tmpCol++
}
framebuffer.destroy()
return field
}
function fieldFromTexture3d(ctx: Context, texture: Texture, dim: Vec3) {
const { gl } = ctx
const { width, height, depth } = texture
const space = Tensor.Space(dim, [2, 1, 0], Float32Array)
const data = space.create()
const field = Tensor.create(space, data)
const slice = new Uint8Array(dx * dy * 4)
const slice = new Uint8Array(width * height * 4)
const framebuffer = createFramebuffer(ctx)
framebuffer.bind()
let j = 0
for (let i = 0; i < dz; ++i) {
for (let i = 0; i < depth; ++i) {
texture.attachFramebuffer(framebuffer, 0, i)
gl.readPixels(0, 0, dx, dy, gl.RGBA, gl.UNSIGNED_BYTE, slice)
for (let iy = 0; iy < dim[1]; ++iy) {
for (let ix = 0; ix < dim[0]; ++ix) {
data[j] = slice[4 * (iy * dim[0] + ix)] / 255
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, slice)
for (let iy = 0; iy < height; ++iy) {
for (let ix = 0; ix < width; ++ix) {
data[j] = slice[4 * (iy * width + ix)] / 255
++j
}
}
......
......@@ -16,7 +16,6 @@ export const GaussianDensityParams = {
radiusOffset: NumberParam('Radius Offset', '', 0, 0, 10, 0.1),
smoothness: NumberParam('Smoothness', '', 1.5, 0.5, 2.5, 0.1),
useGpu: BooleanParam('Use GPU', '', true),
readSlices: BooleanParam('Read Slices', '', false),
ignoreCache: BooleanParam('Ignore Cache', '', false),
}
export const DefaultGaussianDensityProps = paramDefaultValues(GaussianDensityParams)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment