diff --git a/src/mol-geo/geometry/texture-mesh/texture-mesh.ts b/src/mol-geo/geometry/texture-mesh/texture-mesh.ts index 02ad38068ae315ae459a2611fb511e0b5bf1a21b..6e7c9ddc8e742a90d50ec6615b3238fc657203e4 100644 --- a/src/mol-geo/geometry/texture-mesh/texture-mesh.ts +++ b/src/mol-geo/geometry/texture-mesh/texture-mesh.ts @@ -104,7 +104,8 @@ export namespace TextureMesh { const counts = { drawCount: textureMesh.vertexCount, vertexCount: textureMesh.vertexCount / 3, groupCount, instanceCount }; - const transformBoundingSphere = calculateTransformBoundingSphere(textureMesh.boundingSphere, transform.aTransform.ref.value, transform.instanceCount.ref.value); + const invariantBoundingSphere = Sphere3D.clone(textureMesh.boundingSphere); + const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, transform.aTransform.ref.value, instanceCount); return { uGeoTexDim: textureMesh.geoTextureDim, @@ -113,9 +114,9 @@ export namespace TextureMesh { // aGroup is used as a vertex index here and the group id is retirieved from tPositionGroup aGroup: ValueCell.create(fillSerial(new Float32Array(textureMesh.vertexCount))), - boundingSphere: ValueCell.create(transformBoundingSphere), - invariantBoundingSphere: ValueCell.create(Sphere3D.clone(textureMesh.boundingSphere)), - uInvariantBoundingSphere: ValueCell.create(Vec4.ofSphere(textureMesh.boundingSphere)), + boundingSphere: ValueCell.create(boundingSphere), + invariantBoundingSphere: ValueCell.create(invariantBoundingSphere), + uInvariantBoundingSphere: ValueCell.create(Vec4.ofSphere(invariantBoundingSphere)), ...color, ...marker, @@ -141,8 +142,7 @@ export namespace TextureMesh { } function updateValues(values: TextureMeshValues, props: PD.Values<Params>) { - ValueCell.updateIfChanged(values.alpha, props.alpha); // `uAlpha` is set in renderable.render - + BaseGeometry.updateValues(values, props); ValueCell.updateIfChanged(values.dDoubleSided, props.doubleSided); ValueCell.updateIfChanged(values.dFlatShaded, props.flatShaded); ValueCell.updateIfChanged(values.dFlipSided, props.flipSided); @@ -156,8 +156,9 @@ export namespace TextureMesh { } function updateBoundingSphere(values: TextureMeshValues, textureMesh: TextureMesh) { - const invariantBoundingSphere = textureMesh.boundingSphere; + const invariantBoundingSphere = Sphere3D.clone(textureMesh.boundingSphere); const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, values.aTransform.ref.value, values.instanceCount.ref.value); + if (!Sphere3D.equals(boundingSphere, values.boundingSphere.ref.value)) { ValueCell.update(values.boundingSphere, boundingSphere); } diff --git a/src/mol-gl/compute/histogram-pyramid/reduction.ts b/src/mol-gl/compute/histogram-pyramid/reduction.ts index 96aef7dd705b4416f3efbfd732170f5ccb65a9f0..de5caa40f50b3a6f648f157f1d8f0815125061d5 100644 --- a/src/mol-gl/compute/histogram-pyramid/reduction.ts +++ b/src/mol-gl/compute/histogram-pyramid/reduction.ts @@ -124,11 +124,11 @@ export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture, const maxSize = Math.pow(2, levels); // console.log('levels', levels, 'maxSize', maxSize, 'input', w); - const pyramidTexture = getTexture('pyramid', ctx, 'image-float32', 'rgba', 'float', 'nearest'); - pyramidTexture.define(maxSize, maxSize); + const pyramidTex = getTexture('pyramid', ctx, 'image-float32', 'rgba', 'float', 'nearest'); + pyramidTex.define(maxSize, maxSize); const framebuffer = getFramebuffer('pyramid', ctx); - pyramidTexture.attachFramebuffer(framebuffer, 0); + pyramidTex.attachFramebuffer(framebuffer, 0); gl.viewport(0, 0, maxSize, maxSize); gl.clear(gl.COLOR_BUFFER_BIT); @@ -162,29 +162,24 @@ export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture, gl.scissor(0, 0, gridTexDim[0], gridTexDim[1]); renderable.render(); - pyramidTexture.bind(0); + pyramidTex.bind(0); gl.copyTexSubImage2D(gl.TEXTURE_2D, 0, offset, 0, 0, 0, size, size); - pyramidTexture.unbind(0); + pyramidTex.unbind(0); offset += size; } gl.finish(); - // printTexture(ctx, pyramidTexture, 2) + // printTexture(ctx, pyramidTex, 2) // - const finalCount = getHistopyramidSum(ctx, levelTexturesFramebuffers[0].texture); - const height = Math.ceil(finalCount / Math.pow(2, levels)); + // return at least a count of one to avoid issues downstram + const count = Math.max(1, getHistopyramidSum(ctx, levelTexturesFramebuffers[0].texture)); + const height = Math.ceil(count / Math.pow(2, levels)); // const scale = Vec2.create(maxSize / inputTexture.width, maxSize / inputTexture.height); // console.log('height', height, 'finalCount', finalCount, 'scale', scale); - return { - pyramidTex: pyramidTexture, - count: finalCount, - height, - levels, - scale - }; + return { pyramidTex, count, height, levels, scale }; } \ No newline at end of file diff --git a/src/mol-gl/compute/marching-cubes/active-voxels.ts b/src/mol-gl/compute/marching-cubes/active-voxels.ts index be2ada5b1455684f12d6dc524a1de249025b130d..c22119b447755a0a079b58f96530ecbcfda0edf0 100644 --- a/src/mol-gl/compute/marching-cubes/active-voxels.ts +++ b/src/mol-gl/compute/marching-cubes/active-voxels.ts @@ -109,9 +109,9 @@ export function calcActiveVoxels(ctx: WebGLContext, volumeData: Texture, gridDim gl.scissor(0, 0, gridTexDim[0], gridTexDim[1]); renderable.render(); - // console.log('gridScale', gridScale, 'gridTexDim', gridTexDim, 'gridDim', gridDim) - // console.log('volumeData', volumeData) - // console.log('at', readTexture(ctx, activeVoxelsTex)) + // console.log('gridScale', gridScale, 'gridTexDim', gridTexDim, 'gridDim', gridDim); + // console.log('volumeData', volumeData); + // console.log('at', readTexture(ctx, activeVoxelsTex)); gl.finish(); diff --git a/src/mol-gl/shader/marching-cubes/active-voxels.frag.ts b/src/mol-gl/shader/marching-cubes/active-voxels.frag.ts index 94e4f4042018a99fd9419b26e9df2ea7ead4ccaa..c9a9aebce07300c6e204076bcd4e9344e6cd35a3 100644 --- a/src/mol-gl/shader/marching-cubes/active-voxels.frag.ts +++ b/src/mol-gl/shader/marching-cubes/active-voxels.frag.ts @@ -11,7 +11,7 @@ uniform vec3 uGridTexDim; uniform vec2 uScale; // cube corners -const vec3 c0 = vec3(0., 0., 0.); +// const vec3 c0 = vec3(0., 0., 0.); const vec3 c1 = vec3(1., 0., 0.); const vec3 c2 = vec3(1., 1., 0.); const vec3 c3 = vec3(0., 1., 0.); @@ -25,8 +25,7 @@ vec3 index3dFrom2d(vec2 coord) { vec2 columnRow = floor(gridTexPos / uGridDim.xy); vec2 posXY = gridTexPos - columnRow * uGridDim.xy; float posZ = columnRow.y * floor(uGridTexDim.x / uGridDim.x) + columnRow.x; - vec3 posXYZ = vec3(posXY, posZ) / uGridDim; - return posXYZ; + return vec3(posXY, posZ); } float intDiv(float a, float b) { return float(int(a) / int(b)); } @@ -41,7 +40,7 @@ vec4 texture3dFrom2dNearest(sampler2D tex, vec3 pos, vec3 gridDim, vec2 texDim) } vec4 voxel(vec3 pos) { - return texture3dFrom2dNearest(tVolumeData, pos, uGridDim, uGridTexDim.xy); + return texture3dFrom2dNearest(tVolumeData, pos / uGridDim, uGridDim, uGridTexDim.xy); } void main(void) { @@ -50,20 +49,26 @@ void main(void) { // get MC case as the sum of corners that are below the given iso level float c = step(voxel(posXYZ).a, uIsoValue) - + 2. * step(voxel(posXYZ + c1 / uGridDim).a, uIsoValue) - + 4. * step(voxel(posXYZ + c2 / uGridDim).a, uIsoValue) - + 8. * step(voxel(posXYZ + c3 / uGridDim).a, uIsoValue) - + 16. * step(voxel(posXYZ + c4 / uGridDim).a, uIsoValue) - + 32. * step(voxel(posXYZ + c5 / uGridDim).a, uIsoValue) - + 64. * step(voxel(posXYZ + c6 / uGridDim).a, uIsoValue) - + 128. * step(voxel(posXYZ + c7 / uGridDim).a, uIsoValue); + + 2. * step(voxel(posXYZ + c1).a, uIsoValue) + + 4. * step(voxel(posXYZ + c2).a, uIsoValue) + + 8. * step(voxel(posXYZ + c3).a, uIsoValue) + + 16. * step(voxel(posXYZ + c4).a, uIsoValue) + + 32. * step(voxel(posXYZ + c5).a, uIsoValue) + + 64. * step(voxel(posXYZ + c6).a, uIsoValue) + + 128. * step(voxel(posXYZ + c7).a, uIsoValue); c *= step(c, 254.); + // handle out of bounds positions + posXYZ += 1.0; + posXYZ.xy += 1.0; // one pixel padding (usually ok even if the texture has no padding) + if (posXYZ.x >= uGridDim.x || posXYZ.y >= uGridDim.y || posXYZ.z >= uGridDim.z) + c = 0.0; + // get total triangles to generate for calculated MC case from triCount texture float totalTrianglesToGenerate = texture2D(tTriCount, vec2(intMod(c, 16.), floor(c / 16.)) / 16.).a; gl_FragColor = vec4(vec3(totalTrianglesToGenerate * 3.0), c / 255.0); - // gl_FragColor = vec4(255.0, 0.0, 0.0, voxel(posXYZ + c4 / uGridDim).a * 255.0); + // gl_FragColor = vec4(255.0, 0.0, 0.0, voxel(posXYZ + c4).a * 255.0); // gl_FragColor = vec4(255.0, 0.0, 0.0, voxel(posXYZ).a * 255.0); // vec2 uv = vCoordinate; diff --git a/src/mol-gl/shader/marching-cubes/isosurface.frag.ts b/src/mol-gl/shader/marching-cubes/isosurface.frag.ts index 3aa52a12177848e57ec84eefa4e0ee7de7accd7e..fbde2bd5fc401610502617671608d309f09b9553 100644 --- a/src/mol-gl/shader/marching-cubes/isosurface.frag.ts +++ b/src/mol-gl/shader/marching-cubes/isosurface.frag.ts @@ -31,15 +31,12 @@ const vec3 c5 = vec3(1., 0., 1.); const vec3 c6 = vec3(1., 1., 1.); const vec3 c7 = vec3(0., 1., 1.); -const float EPS = 0.00001; - vec3 index3dFrom2d(vec2 coord) { vec2 gridTexPos = coord * uGridTexDim.xy; vec2 columnRow = floor(gridTexPos / uGridDim.xy); vec2 posXY = gridTexPos - columnRow * uGridDim.xy; float posZ = columnRow.y * floor(uGridTexDim.x / uGridDim.x) + columnRow.x; - vec3 posXYZ = vec3(posXY, posZ); - return posXYZ; + return vec3(posXY, posZ); } vec4 texture3dFrom2dNearest(sampler2D tex, vec3 pos, vec3 gridDim, vec2 texDim) { @@ -51,7 +48,7 @@ vec4 texture3dFrom2dNearest(sampler2D tex, vec3 pos, vec3 gridDim, vec2 texDim) } vec4 voxel(vec3 pos) { - return texture3dFrom2dNearest(tVolumeData, pos, uGridDim, uGridTexDim.xy); + return texture3dFrom2dNearest(tVolumeData, pos / uGridDim, uGridDim, uGridTexDim.xy); } void main(void) { @@ -173,27 +170,34 @@ void main(void) { // b0 = floor(b0 + 0.5); // b1 = floor(b1 + 0.5); - vec4 d0 = voxel(b0 / uGridDim); - vec4 d1 = voxel(b1 / uGridDim); + vec4 d0 = voxel(b0); + vec4 d1 = voxel(b1); float v0 = d0.a; float v1 = d1.a; float t = (uIsoValue - v0) / (v0 - v1); - // t = -0.5; gl_FragData[0].xyz = (uGridTransform * vec4(b0 + t * (b0 - b1), 1.0)).xyz; - gl_FragData[0].w = decodeFloatRGB(d0.rgb); // group id + + // group id + #if __VERSION__ == 100 + // webgl1 does not support 'flat' interpolation (i.e. no interpolation) + // so we provide a constant that stands for 'unknown-group' + gl_FragData[0].w = 16777216.0; + #else + gl_FragData[0].w = t < 0.5 ? decodeFloatRGB(d0.rgb) : decodeFloatRGB(d1.rgb); + #endif // normals from gradients vec3 n0 = -normalize(vec3( - voxel((b0 - c1) / uGridDim).a - voxel((b0 + c1) / uGridDim).a, - voxel((b0 - c3) / uGridDim).a - voxel((b0 + c3) / uGridDim).a, - voxel((b0 - c4) / uGridDim).a - voxel((b0 + c4) / uGridDim).a + voxel(b0 - c1).a - voxel(b0 + c1).a, + voxel(b0 - c3).a - voxel(b0 + c3).a, + voxel(b0 - c4).a - voxel(b0 + c4).a )); vec3 n1 = -normalize(vec3( - voxel((b1 - c1) / uGridDim).a - voxel((b1 + c1) / uGridDim).a, - voxel((b1 - c3) / uGridDim).a - voxel((b1 + c3) / uGridDim).a, - voxel((b1 - c4) / uGridDim).a - voxel((b1 + c4) / uGridDim).a + voxel(b1 - c1).a - voxel(b1 + c1).a, + voxel(b1 - c3).a - voxel(b1 + c3).a, + voxel(b1 - c4).a - voxel(b1 + c4).a )); gl_FragData[1].xyz = -vec3( n0.x + t * (n0.x - n1.x), diff --git a/src/mol-gl/webgl/program.ts b/src/mol-gl/webgl/program.ts index fcf40dca8c20951fa830531758181285c44c0f57..d7ed7fd16cba7a69d07e4698f35f3b743b421a64 100644 --- a/src/mol-gl/webgl/program.ts +++ b/src/mol-gl/webgl/program.ts @@ -24,7 +24,7 @@ export interface Program { use: () => void setUniforms: (uniformValues: UniformsList) => void bindAttributes: (attribueBuffers: AttributeBuffers) => void - bindTextures: (textures: Textures, startingTargetUnit?: number) => void + bindTextures: (textures: Textures, startingTargetUnit: number) => void reset: () => void destroy: () => void @@ -198,9 +198,7 @@ export function createProgram(gl: GLRenderingContext, state: WebGLState, extensi if (l !== -1) buffer.bind(l); } }, - bindTextures: (textures: Textures, startingTargetUnit?: number) => { - startingTargetUnit = startingTargetUnit ?? 0; - + bindTextures: (textures: Textures, startingTargetUnit: number) => { for (let i = 0, il = textures.length; i < il; ++i) { const [k, texture] = textures[i]; const l = locations[k]; diff --git a/src/mol-gl/webgl/render-item.ts b/src/mol-gl/webgl/render-item.ts index 02eb1c15e21387bfb592b8281416b6431374906a..4b754bcede3df0074197914cefac6fea0e2f8914 100644 --- a/src/mol-gl/webgl/render-item.ts +++ b/src/mol-gl/webgl/render-item.ts @@ -173,7 +173,7 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode: program.bindTextures(sharedTexturesList, 0); program.bindTextures(textures, sharedTexturesList.length); } else { - program.bindTextures(textures); + program.bindTextures(textures, 0); } } else { const vertexArray = vertexArrays[variant]; @@ -191,7 +191,7 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode: program.bindTextures(sharedTexturesList, 0); program.bindTextures(textures, sharedTexturesList.length); } else { - program.bindTextures(textures); + program.bindTextures(textures, 0); } if (vertexArray) { vertexArray.bind(); diff --git a/src/mol-gl/webgl/texture.ts b/src/mol-gl/webgl/texture.ts index 3847d82d313e809843fad5f5b2da189458dc8509..a7972d3cc87513a742eaf57223c4688e2a322440 100644 --- a/src/mol-gl/webgl/texture.ts +++ b/src/mol-gl/webgl/texture.ts @@ -173,7 +173,11 @@ export interface Texture { getByteCount: () => number define: (width: number, height: number, depth?: number) => void - load: (image: TextureImage<any> | TextureVolume<any>) => void + /** + * The `sub` option requires an existing allocation on the GPU, that is, either + * `define` or `load` without `sub` must have been called before. + */ + load: (image: TextureImage<any> | TextureVolume<any>, sub?: boolean) => void bind: (id: TextureId) => void unbind: (id: TextureId) => void /** Use `layer` to attach a z-slice of a 3D texture */ @@ -246,7 +250,7 @@ export function createTexture(gl: GLRenderingContext, extensions: WebGLExtension } } - function load(data: TextureImage<any> | TextureVolume<any>) { + function load(data: TextureImage<any> | TextureVolume<any>, sub = false) { gl.bindTexture(target, texture); // unpack alignment of 1 since we use textures only for data gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1); @@ -254,12 +258,20 @@ export function createTexture(gl: GLRenderingContext, extensions: WebGLExtension gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 0); if (isTexture2d(data, target, gl)) { gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, !!data.flipY); - width = data.width, height = data.height; - gl.texImage2D(target, 0, internalFormat, width, height, 0, format, type, data.array); + if (sub) { + gl.texSubImage2D(target, 0, 0, 0, data.width, data.height, format, type, data.array); + } else { + width = data.width, height = data.height; + gl.texImage2D(target, 0, internalFormat, width, height, 0, format, type, data.array); + } } else if (isWebGL2(gl) && isTexture3d(data, target, gl)) { gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); - width = data.width, height = data.height, depth = data.depth; - gl.texImage3D(target, 0, internalFormat, width, height, depth, 0, format, type, data.array); + if (sub) { + gl.texSubImage3D(target, 0, 0, 0, 0, data.width, data.height, data.depth, format, type, data.array); + } else { + width = data.width, height = data.height, depth = data.depth; + gl.texImage3D(target, 0, internalFormat, width, height, depth, 0, format, type, data.array); + } } else { throw new Error('unknown texture target'); } @@ -317,11 +329,8 @@ export function createTexture(gl: GLRenderingContext, extensions: WebGLExtension texture = getTexture(gl); init(); - if (loadedData) { - load(loadedData); - } else { - define(width, height, depth); - } + define(width, height, depth); + if (loadedData) load(loadedData); }, destroy: () => { if (destroyed) return; diff --git a/src/mol-repr/structure/visual/gaussian-surface-mesh.ts b/src/mol-repr/structure/visual/gaussian-surface-mesh.ts index b56fde923f67f27c5d6a458ae2d4f3204f3458ba..da1a0f8566be136d7636dd8871726a0c2b5742be 100644 --- a/src/mol-repr/structure/visual/gaussian-surface-mesh.ts +++ b/src/mol-repr/structure/visual/gaussian-surface-mesh.ts @@ -157,7 +157,7 @@ async function createGaussianSurfaceTextureMesh(ctx: VisualContext, unit: Unit, // ctx.webgl.waitForGpuCommandsCompleteSync(); // console.timeEnd('createIsosurfaceBuffers'); - const boundingSphere = Sphere3D.expand(Sphere3D(), structure.boundary.sphere, props.radiusOffset + getStructureExtraRadius(structure)); + const boundingSphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, props.radiusOffset + getStructureExtraRadius(structure)); const surface = TextureMesh.create(gv.vertexCount, 1, gv.vertexGroupTexture, gv.normalTexture, boundingSphere, textureMesh); // console.log({ // renderables: ctx.webgl.namedComputeRenderables, diff --git a/src/mol-repr/volume/direct-volume.ts b/src/mol-repr/volume/direct-volume.ts index 4be7254e117f1fc2810dc11fda195a700b939d88..89af5d7bcd2f5e03394bbd75fb8c7e961d6c0530 100644 --- a/src/mol-repr/volume/direct-volume.ts +++ b/src/mol-repr/volume/direct-volume.ts @@ -21,15 +21,7 @@ import { RepresentationContext, RepresentationParamsGetter } from '../representa import { Interval } from '../../mol-data/int'; import { Loci, EmptyLoci } from '../../mol-model/loci'; import { PickingId } from '../../mol-geo/geometry/picking'; -import { eachVolumeLoci } from './util'; - -// avoiding namespace lookup improved performance in Chrome (Aug 2020) -const v3set = Vec3.set; -const v3normalize = Vec3.normalize; -const v3sub = Vec3.sub; -const v3addScalar = Vec3.addScalar; -const v3scale = Vec3.scale; -const v3toArray = Vec3.toArray; +import { createVolumeTexture2d, createVolumeTexture3d, eachVolumeLoci } from './util'; function getBoundingBox(gridDimension: Vec3, transform: Mat4) { const bbox = Box3D(); @@ -40,75 +32,9 @@ function getBoundingBox(gridDimension: Vec3, transform: Mat4) { // 2d volume texture -function getVolumeTexture2dLayout(dim: Vec3, maxTextureSize: number) { - let width = 0; - let height = dim[1]; - let rows = 1; - let columns = dim[0]; - if (maxTextureSize < dim[0] * dim[2]) { - columns = Math.floor(maxTextureSize / dim[0]); - rows = Math.ceil(dim[2] / columns); - width = columns * dim[0]; - height *= rows; - } else { - width = dim[0] * dim[2]; - } - return { width, height, columns, rows }; -} - -function createVolumeTexture2d(volume: Volume, maxTextureSize: number) { - const { cells: { space, data }, stats: { max, min } } = volume.grid; - const dim = space.dimensions as Vec3; - const { dataOffset: o } = space; - const { width, height } = getVolumeTexture2dLayout(dim, maxTextureSize); - - const array = new Uint8Array(width * height * 4); - const textureImage = { array, width, height }; - - const diff = max - min; - const [ xn, yn, zn ] = dim; - - const n0 = Vec3(); - const n1 = Vec3(); - - const xn1 = xn - 1; - const yn1 = yn - 1; - const zn1 = zn - 1; - - for (let z = 0; z < zn; ++z) { - for (let y = 0; y < yn; ++y) { - for (let x = 0; x < xn; ++x) { - const column = Math.floor(((z * xn) % width) / xn); - const row = Math.floor((z * xn) / width); - const px = column * xn + x; - const index = 4 * ((row * yn * width) + (y * width) + px); - const offset = o(x, y, z); - - v3set(n0, - data[o(Math.max(0, x - 1), y, z)], - data[o(x, Math.max(0, y - 1), z)], - data[o(x, y, Math.max(0, z - 1))] - ); - v3set(n1, - data[o(Math.min(xn1, x + 1), y, z)], - data[o(x, Math.min(yn1, y + 1), z)], - data[o(x, y, Math.min(zn1, z + 1))] - ); - v3normalize(n0, v3sub(n0, n0, n1)); - v3addScalar(n0, v3scale(n0, n0, 0.5), 0.5); - v3toArray(v3scale(n0, n0, 255), array, index); - - array[index + 3] = ((data[offset] - min) / diff) * 255; - } - } - } - - return textureImage; -} - export function createDirectVolume2d(ctx: RuntimeContext, webgl: WebGLContext, volume: Volume, directVolume?: DirectVolume) { const gridDimension = volume.grid.cells.space.dimensions as Vec3; - const textureImage = createVolumeTexture2d(volume, webgl.maxTextureSize); + const textureImage = createVolumeTexture2d(volume, 'normals'); // debugTexture(createImageData(textureImage.array, textureImage.width, textureImage.height), 1/3) const transform = Grid.getGridToCartesianTransform(volume.grid); const bbox = getBoundingBox(gridDimension, transform); @@ -123,51 +49,6 @@ export function createDirectVolume2d(ctx: RuntimeContext, webgl: WebGLContext, v // 3d volume texture -function createVolumeTexture3d(volume: Volume) { - const { cells: { space, data }, stats: { max, min } } = volume.grid; - const [ width, height, depth ] = space.dimensions as Vec3; - const { dataOffset: o } = space; - - const array = new Uint8Array(width * height * depth * 4); - const textureVolume = { array, width, height, depth }; - const diff = max - min; - - const n0 = Vec3(); - const n1 = Vec3(); - - const width1 = width - 1; - const height1 = height - 1; - const depth1 = depth - 1; - - let i = 0; - for (let z = 0; z < depth; ++z) { - for (let y = 0; y < height; ++y) { - for (let x = 0; x < width; ++x) { - const offset = o(x, y, z); - - v3set(n0, - data[o(Math.max(0, x - 1), y, z)], - data[o(x, Math.max(0, y - 1), z)], - data[o(x, y, Math.max(0, z - 1))] - ); - v3set(n1, - data[o(Math.min(width1, x + 1), y, z)], - data[o(x, Math.min(height1, y + 1), z)], - data[o(x, y, Math.min(depth1, z + 1))] - ); - v3normalize(n0, v3sub(n0, n0, n1)); - v3addScalar(n0, v3scale(n0, n0, 0.5), 0.5); - v3toArray(v3scale(n0, n0, 255), array, i); - - array[i + 3] = ((data[offset] - min) / diff) * 255; - i += 4; - } - } - } - - return textureVolume; -} - function getUnitToCartn(grid: Grid) { if (grid.transform.kind === 'matrix') { return { diff --git a/src/mol-repr/volume/isosurface.ts b/src/mol-repr/volume/isosurface.ts index 4b95e84e6b7c8fddefe6d77c2e50fb62efcc5d2b..cc2b6e450d6b7021de0150acb00be4b2d4897d93 100644 --- a/src/mol-repr/volume/isosurface.ts +++ b/src/mol-repr/volume/isosurface.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> * @author Alexander Rose <alexander.rose@weirdbyte.de> @@ -20,9 +20,13 @@ import { RepresentationContext, RepresentationParamsGetter, Representation } fro import { PickingId } from '../../mol-geo/geometry/picking'; import { EmptyLoci, Loci } from '../../mol-model/loci'; import { Interval } from '../../mol-data/int'; -import { Tensor } from '../../mol-math/linear-algebra'; +import { Tensor, Vec2, Vec3 } from '../../mol-math/linear-algebra'; import { fillSerial } from '../../mol-util/array'; -import { eachVolumeLoci } from './util'; +import { createVolumeTexture2d, eachVolumeLoci, getVolumeTexture2dLayout } from './util'; +import { TextureMesh } from '../../mol-geo/geometry/texture-mesh/texture-mesh'; +import { calcActiveVoxels } from '../../mol-gl/compute/marching-cubes/active-voxels'; +import { createHistogramPyramid } from '../../mol-gl/compute/histogram-pyramid/reduction'; +import { createIsosurfaceBuffers } from '../../mol-gl/compute/marching-cubes/isosurface'; export const VolumeIsosurfaceParams = { isoValue: Volume.IsoValueParam @@ -91,6 +95,80 @@ export function IsosurfaceMeshVisual(materialId: number): VolumeVisual<Isosurfac // +async function createVolumeIsosurfaceTextureMesh(ctx: VisualContext, volume: Volume, theme: Theme, props: VolumeIsosurfaceProps, textureMesh?: TextureMesh) { + if (!ctx.webgl) throw new Error('webgl context required to create volume isosurface texture-mesh'); + + const { resources } = ctx.webgl; + if (!volume._propertyData['texture2d']) { + // TODO: handle disposal + volume._propertyData['texture2d'] = resources.texture('image-uint8', 'rgba', 'ubyte', 'linear'); + } + const texture = volume._propertyData['texture2d']; + + const padding = 1; + const transform = Grid.getGridToCartesianTransform(volume.grid); + const gridDimension = Vec3.clone(volume.grid.cells.space.dimensions as Vec3); + const { width, height, powerOfTwoSize: texDim } = getVolumeTexture2dLayout(gridDimension, padding); + const gridTexDim = Vec3.create(width, height, 0); + const gridTexScale = Vec2.create(width / texDim, height / texDim); + // console.log({ texDim, width, height, gridDimension }); + + if (!textureMesh) { + // set to power-of-two size required for histopyramid calculation + texture.define(texDim, texDim); + // load volume into sub-section of texture + texture.load(createVolumeTexture2d(volume, 'groups', padding), true); + } + + const { max, min } = volume.grid.stats; + const diff = max - min; + const value = Volume.IsoValue.toAbsolute(props.isoValue, volume.grid.stats).absoluteValue; + const isoLevel = ((value - min) / diff); + + gridDimension[0] += padding; + gridDimension[1] += padding; + + // console.time('calcActiveVoxels'); + const activeVoxelsTex = calcActiveVoxels(ctx.webgl, texture, gridDimension, gridTexDim, isoLevel, gridTexScale); + // ctx.webgl.waitForGpuCommandsCompleteSync(); + // console.timeEnd('calcActiveVoxels'); + + // console.time('createHistogramPyramid'); + const compacted = createHistogramPyramid(ctx.webgl, activeVoxelsTex, gridTexScale, gridTexDim); + // ctx.webgl.waitForGpuCommandsCompleteSync(); + // console.timeEnd('createHistogramPyramid'); + + // console.time('createIsosurfaceBuffers'); + const gv = createIsosurfaceBuffers(ctx.webgl, activeVoxelsTex, texture, compacted, gridDimension, gridTexDim, transform, isoLevel, textureMesh ? textureMesh.vertexGroupTexture.ref.value : undefined, textureMesh ? textureMesh.normalTexture.ref.value : undefined); + // ctx.webgl.waitForGpuCommandsCompleteSync(); + // console.timeEnd('createIsosurfaceBuffers'); + + const surface = TextureMesh.create(gv.vertexCount, 1, gv.vertexGroupTexture, gv.normalTexture, Volume.getBoundingSphere(volume), textureMesh); + // console.log({ + // renderables: ctx.webgl.namedComputeRenderables, + // framebuffers: ctx.webgl.namedFramebuffers, + // textures: ctx.webgl.namedTextures, + // }); + // ctx.webgl.waitForGpuCommandsCompleteSync(); + return surface; +} + +export function IsosurfaceTextureMeshVisual(materialId: number): VolumeVisual<IsosurfaceMeshParams> { + return VolumeVisual<TextureMesh, IsosurfaceMeshParams>({ + defaultProps: PD.getDefaultValues(IsosurfaceMeshParams), + createGeometry: createVolumeIsosurfaceTextureMesh, + createLocationIterator: (volume: Volume) => LocationIterator(volume.grid.cells.data.length, 1, 1, () => NullLocation), + getLoci: getIsosurfaceLoci, + eachLocation: eachIsosurface, + setUpdateState: (state: VisualUpdateState, volume: Volume, newProps: PD.Values<IsosurfaceMeshParams>, currentProps: PD.Values<IsosurfaceMeshParams>) => { + if (!Volume.IsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.grid.stats)) state.createGeometry = true; + }, + geometryUtils: TextureMesh.Utils + }, materialId); +} + +// + export async function createVolumeIsosurfaceWireframe(ctx: VisualContext, volume: Volume, theme: Theme, props: VolumeIsosurfaceProps, lines?: Lines) { ctx.runtime.update({ message: 'Marching cubes...' }); @@ -136,6 +214,8 @@ export function IsosurfaceWireframeVisual(materialId: number): VolumeVisual<Isos const IsosurfaceVisuals = { 'solid': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Volume, IsosurfaceMeshParams>) => VolumeRepresentation('Isosurface mesh', ctx, getParams, IsosurfaceMeshVisual, getLoci), + // TODO: don't enable yet as it breaks state sessions + // 'solid-gpu': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Volume, IsosurfaceMeshParams>) => VolumeRepresentation('Isosurface texture-mesh', ctx, getParams, IsosurfaceTextureMeshVisual, getLoci), 'wireframe': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Volume, IsosurfaceWireframeParams>) => VolumeRepresentation('Isosurface wireframe', ctx, getParams, IsosurfaceWireframeVisual, getLoci), }; diff --git a/src/mol-repr/volume/util.ts b/src/mol-repr/volume/util.ts index 9ab2e583c581b6e68b12da42c6b26c487c657851..b35fd08e82ac015fbeca86c4a0971b7926d8a4fe 100644 --- a/src/mol-repr/volume/util.ts +++ b/src/mol-repr/volume/util.ts @@ -8,6 +8,16 @@ import { Volume } from '../../mol-model/volume'; import { Loci } from '../../mol-model/loci'; import { Interval, OrderedSet } from '../../mol-data/int'; import { equalEps } from '../../mol-math/linear-algebra/3d/common'; +import Vec3 from '../../mol-math/linear-algebra/3d/vec3'; +import { encodeFloatRGBtoArray } from '../../mol-util/float-packing'; + +// avoiding namespace lookup improved performance in Chrome (Aug 2020) +const v3set = Vec3.set; +const v3normalize = Vec3.normalize; +const v3sub = Vec3.sub; +const v3addScalar = Vec3.addScalar; +const v3scale = Vec3.scale; +const v3toArray = Vec3.toArray; export function eachVolumeLoci(loci: Loci, volume: Volume, isoValue: Volume.IsoValue | undefined, apply: (interval: Interval) => boolean) { let changed = false; @@ -41,4 +51,127 @@ export function eachVolumeLoci(loci: Loci, volume: Volume, isoValue: Volume.IsoV } } return changed; +} + +// + +export function getVolumeTexture2dLayout(dim: Vec3, padding = 0) { + const area = dim[0] * dim[1] * dim[2]; + const squareDim = Math.sqrt(area); + const powerOfTwoSize = Math.pow(2, Math.ceil(Math.log(squareDim) / Math.log(2))); + + let width = dim[0] + padding; + let height = dim[1] + padding; + let rows = 1; + let columns = width; + if (powerOfTwoSize < width * dim[2]) { + columns = Math.floor(powerOfTwoSize / width); + rows = Math.ceil(dim[2] / columns); + width *= columns; + height *= rows; + } else { + width *= dim[2]; + } + return { width, height, columns, rows, powerOfTwoSize: height < powerOfTwoSize ? powerOfTwoSize : powerOfTwoSize * 2 }; +} + +export function createVolumeTexture2d(volume: Volume, variant: 'normals' | 'groups', padding = 0) { + const { cells: { space, data }, stats: { max, min } } = volume.grid; + const dim = space.dimensions as Vec3; + const { dataOffset: o } = space; + const { width, height } = getVolumeTexture2dLayout(dim, padding); + + const array = new Uint8Array(width * height * 4); + const textureImage = { array, width, height }; + + const diff = max - min; + const [ xn, yn, zn ] = dim; + const xnp = xn + padding; + const ynp = yn + padding; + + const n0 = Vec3(); + const n1 = Vec3(); + + const xn1 = xn - 1; + const yn1 = yn - 1; + const zn1 = zn - 1; + + for (let z = 0; z < zn; ++z) { + for (let y = 0; y < yn; ++y) { + for (let x = 0; x < xn; ++x) { + const column = Math.floor(((z * xnp) % width) / xnp); + const row = Math.floor((z * xnp) / width); + const px = column * xnp + x; + const index = 4 * ((row * ynp * width) + (y * width) + px); + const offset = o(x, y, z); + + if (variant === 'groups') { + encodeFloatRGBtoArray(offset, array, index); + } else { + v3set(n0, + data[o(Math.max(0, x - 1), y, z)], + data[o(x, Math.max(0, y - 1), z)], + data[o(x, y, Math.max(0, z - 1))] + ); + v3set(n1, + data[o(Math.min(xn1, x + 1), y, z)], + data[o(x, Math.min(yn1, y + 1), z)], + data[o(x, y, Math.min(zn1, z + 1))] + ); + v3normalize(n0, v3sub(n0, n0, n1)); + v3addScalar(n0, v3scale(n0, n0, 0.5), 0.5); + v3toArray(v3scale(n0, n0, 255), array, index); + } + + array[index + 3] = ((data[offset] - min) / diff) * 255; + } + } + } + + return textureImage; +} + +export function createVolumeTexture3d(volume: Volume) { + const { cells: { space, data }, stats: { max, min } } = volume.grid; + const [ width, height, depth ] = space.dimensions as Vec3; + const { dataOffset: o } = space; + + const array = new Uint8Array(width * height * depth * 4); + const textureVolume = { array, width, height, depth }; + const diff = max - min; + + const n0 = Vec3(); + const n1 = Vec3(); + + const width1 = width - 1; + const height1 = height - 1; + const depth1 = depth - 1; + + let i = 0; + for (let z = 0; z < depth; ++z) { + for (let y = 0; y < height; ++y) { + for (let x = 0; x < width; ++x) { + const offset = o(x, y, z); + + v3set(n0, + data[o(Math.max(0, x - 1), y, z)], + data[o(x, Math.max(0, y - 1), z)], + data[o(x, y, Math.max(0, z - 1))] + ); + v3set(n1, + data[o(Math.min(width1, x + 1), y, z)], + data[o(x, Math.min(height1, y + 1), z)], + data[o(x, y, Math.min(depth1, z + 1))] + ); + v3normalize(n0, v3sub(n0, n0, n1)); + v3addScalar(n0, v3scale(n0, n0, 0.5), 0.5); + v3toArray(v3scale(n0, n0, 255), array, i); + + array[i + 3] = ((data[offset] - min) / diff) * 255; + i += 4; + } + } + } + + return textureVolume; } \ No newline at end of file