From 4c34ebed2b3508301b1ba0db4f88fdf2186e675f Mon Sep 17 00:00:00 2001 From: Matt McCormick Date: Wed, 14 Feb 2018 10:22:42 -0500 Subject: [PATCH 1/6] perf(VolumeMapper): Compute gradients in web workers This cuts initial volume load time in half. Gradient computation occurs asynchronously in up to four web workers. These worker are built into the final bundle inline with worker-loader. VolumeMapper does not try to use the lightingTexture until after the gradients have been computed. --- .../OpenGL/Texture/ComputeGradients.worker.js | 77 ++ Sources/Rendering/OpenGL/Texture/index.js | 277 ++--- .../Rendering/OpenGL/VolumeMapper/index.js | 32 +- .../OpenGL/VolumeMapper/test/testLighting.js | 3 +- .../VolumeMapper/test/testLighting_3.png | Bin 0 -> 48876 bytes Sources/macro.js | 4 +- Utilities/config/rules-tests.js | 6 + Utilities/config/rules-vtk.js | 6 + package-lock.json | 974 +++++++++++++++++- package.json | 162 +-- 10 files changed, 1285 insertions(+), 256 deletions(-) create mode 100644 Sources/Rendering/OpenGL/Texture/ComputeGradients.worker.js create mode 100644 Sources/Rendering/OpenGL/VolumeMapper/test/testLighting_3.png diff --git a/Sources/Rendering/OpenGL/Texture/ComputeGradients.worker.js b/Sources/Rendering/OpenGL/Texture/ComputeGradients.worker.js new file mode 100644 index 00000000000..eea5f407d1c --- /dev/null +++ b/Sources/Rendering/OpenGL/Texture/ComputeGradients.worker.js @@ -0,0 +1,77 @@ +import { vec3 } from 'gl-matrix'; +import registerWebworker from 'webworker-promise/lib/register'; + +import vtkMath from 'vtk.js/Sources/Common/Core/Math'; + +registerWebworker( + async ( + { width, height, depth, spacing, data, haveWebgl2, depthStart, depthEnd }, + emit + ) => { + // have to compute the gradient to get the normal + // and magnitude + const depthLength = depthEnd - depthStart + 1; + const gradients = new Float32Array(width * height * depthLength * 4); + const gradientMagnitudes = new Float32Array(width * height * depthLength); + + const sliceSize = width * height; + let inPtr = 0; + let outPtr = 0; + const grad = vec3.create(); + vec3.set( + grad, + (data[inPtr + 1] - data[inPtr]) / spacing[0], + (data[inPtr + width] - data[inPtr]) / spacing[1], + (data[inPtr + sliceSize] - data[inPtr]) / spacing[2] + ); + let minMag = vec3.length(grad); + let maxMag = -1.0; + for (let z = depthStart; z < depthEnd + 1; ++z) { + let zedge = 0; + if (z === depth - 1) { + zedge = -sliceSize; + } + for (let y = 0; y < height; ++y) { + let yedge = 0; + if (y === height - 1) { + yedge = -width; + } + for (let x = 0; x < width; ++x) { + let edge = inPtr + zedge + yedge; + if (x === width - 1) { + edge--; + } + vec3.set( + grad, + (data[edge + 1] - data[edge]) / spacing[0], + (data[edge + width] - data[edge]) / spacing[1], + (data[edge + sliceSize] - data[edge]) / spacing[2] + ); + + const mag = vec3.length(grad); + vec3.normalize(grad, grad); + gradients[outPtr++] = grad[0]; + gradients[outPtr++] = grad[1]; + gradients[outPtr++] = grad[2]; + gradients[outPtr++] = mag; + gradientMagnitudes[inPtr] = mag; + inPtr++; + } + } + } + const arrayMinMag = vtkMath.arrayMin(gradientMagnitudes); + const arrayMaxMag = vtkMath.arrayMax(gradientMagnitudes); + minMag = Math.min(arrayMinMag, minMag); + maxMag = Math.max(arrayMaxMag, maxMag); + + const result = { + subGradients: gradients, + subMinMag: minMag, + subMaxMag: maxMag, + subDepthStart: depthStart, + }; + return new registerWebworker.TransferableResponse(result, [ + gradients.buffer, + ]); + } +); diff --git a/Sources/Rendering/OpenGL/Texture/index.js b/Sources/Rendering/OpenGL/Texture/index.js index cf85dc03d57..60acfe510ae 100644 --- a/Sources/Rendering/OpenGL/Texture/index.js +++ b/Sources/Rendering/OpenGL/Texture/index.js @@ -1,9 +1,12 @@ +import WebworkerPromise from 'webworker-promise'; + import Constants from 'vtk.js/Sources/Rendering/OpenGL/Texture/Constants'; import macro from 'vtk.js/Sources/macro'; import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray'; import vtkMath from 'vtk.js/Sources/Common/Core/Math'; import vtkViewNode from 'vtk.js/Sources/Rendering/SceneGraph/ViewNode'; -import { vec3 } from 'gl-matrix'; + +import ComputeGradientsWorker from './ComputeGradients.worker'; const { Wrap, Filter } = Constants; const { VtkDataTypes } = vtkDataArray; @@ -1102,161 +1105,163 @@ function vtkOpenGLTexture(publicAPI, model) { // This method creates a normal/gradient texture for 3D volume // rendering publicAPI.create3DLighting = (scalarTexture, data, spacing) => { + model.computedGradients = false; const vinfo = scalarTexture.getVolumeInfo(); const width = vinfo.width; const height = vinfo.height; const depth = vinfo.depth; - // have to compute the gradient to get the normal - // and magnitude - const tmpArray = new Float32Array(width * height * depth * 4); - const tmpMagArray = new Float32Array(width * height * depth); - - let inPtr = 0; - let outPtr = 0; - const sliceSize = width * height; - const grad = vec3.create(); - vec3.set( - grad, - (data[inPtr + 1] - data[inPtr]) / spacing[0], - (data[inPtr + width] - data[inPtr]) / spacing[1], - (data[inPtr + sliceSize] - data[inPtr]) / spacing[2] - ); - let minMag = vec3.length(grad); - let maxMag = -1.0; - for (let z = 0; z < depth; ++z) { - let zedge = 0; - if (z === depth - 1) { - zedge = -sliceSize; - } - for (let y = 0; y < height; ++y) { - let yedge = 0; - if (y === height - 1) { - yedge = -width; + const haveWebgl2 = model.openGLRenderWindow.getWebgl2(); + + const maxNumberOfWorkers = 4; + const depthStride = Math.floor(depth / maxNumberOfWorkers) || 1; + const workers = []; + let depthIndex = 0; + while (depthIndex < depth) { + const worker = new ComputeGradientsWorker(); + const workerPromise = new WebworkerPromise(worker); + const depthStart = depthIndex; + const depthEnd = Math.min(depthIndex + depthStride, depth - 1); + const subData = new data.constructor( + data.slice(depthStart * width * height, (depthEnd + 1) * width * height) + ); + workers.push( + workerPromise.postMessage( + { + width, + height, + depth, + spacing, + data: subData, + haveWebgl2, + depthStart, + depthEnd, + }, + [subData.buffer] + ) + ); + depthIndex += depthStride; + } + Promise.all(workers).then((workerResults) => { + const gradients = new Float32Array(width * height * depth * 4); + let minMag = Infinity; + let maxMag = -Infinity; + + workerResults.forEach( + ({ subGradients, subMinMag, subMaxMag, subDepthStart }) => { + const start = subDepthStart * width * height * 4; + gradients.set(subGradients, start); + minMag = Math.min(subMinMag, minMag); + maxMag = Math.max(subMaxMag, maxMag); } - for (let x = 0; x < width; ++x) { - let edge = inPtr + zedge + yedge; - if (x === width - 1) { - edge--; - } - vec3.set( - grad, - (data[edge + 1] - data[edge]) / spacing[0], - (data[edge + width] - data[edge]) / spacing[1], - (data[edge + sliceSize] - data[edge]) / spacing[2] - ); + ); - const mag = vec3.length(grad); - vec3.normalize(grad, grad); - tmpArray[outPtr++] = grad[0]; - tmpArray[outPtr++] = grad[1]; - tmpArray[outPtr++] = grad[2]; - tmpArray[outPtr++] = mag; - tmpMagArray[inPtr] = mag; - inPtr++; + const numPixelsIn = width * height * depth; + const reformattedGradients = new Uint8Array(numPixelsIn * 4); + if (haveWebgl2) { + let outIdx = 0; + for (let p = 0; p < numPixelsIn; ++p) { + const pp = p * 4; + reformattedGradients[outIdx++] = 127.5 + 127.5 * gradients[pp]; + reformattedGradients[outIdx++] = 127.5 + 127.5 * gradients[pp + 1]; + reformattedGradients[outIdx++] = 127.5 + 127.5 * gradients[pp + 2]; + // we encode gradient magnitude using sqrt so that + // we have nonlinear resolution + reformattedGradients[outIdx++] = + 255.0 * Math.sqrt(gradients[pp + 3] / maxMag); } } - } - const arrayMinMag = vtkMath.arrayMin(tmpMagArray); - const arrayMaxMag = vtkMath.arrayMax(tmpMagArray); - minMag = Math.min(arrayMinMag, minMag); - maxMag = Math.max(arrayMaxMag, maxMag); - // store the information, we will need it later - model.volumeInfo = { min: minMag, max: maxMag }; - let outIdx = 0; + // store the information, we will need it later + model.volumeInfo = { min: minMag, max: maxMag }; - if (model.openGLRenderWindow.getWebgl2()) { - const numPixelsIn = width * height * depth; - const newArray = new Uint8Array(numPixelsIn * 4); - for (let p = 0; p < numPixelsIn; ++p) { - const pp = p * 4; - newArray[outIdx++] = 127.5 + 127.5 * tmpArray[pp]; - newArray[outIdx++] = 127.5 + 127.5 * tmpArray[pp + 1]; - newArray[outIdx++] = 127.5 + 127.5 * tmpArray[pp + 2]; - // we encode gradient magnitude using sqrt so that - // we have nonlinear resolution - newArray[outIdx++] = 255.0 * Math.sqrt(tmpArray[pp + 3] / maxMag); + if (haveWebgl2) { + const create3DFromRawReturn = publicAPI.create3DFromRaw( + width, + height, + depth, + 4, + VtkDataTypes.UNSIGNED_CHAR, + reformattedGradients + ); + model.computedGradients = true; + model.gradientsBuildTime.modified(); + return create3DFromRawReturn; } - return publicAPI.create3DFromRaw( - width, - height, - depth, - 4, - VtkDataTypes.UNSIGNED_CHAR, - newArray - ); - } - - // Now determine the texture parameters using the arguments. - publicAPI.getOpenGLDataType(VtkDataTypes.UNSIGNED_CHAR); - publicAPI.getInternalFormat(VtkDataTypes.UNSIGNED_CHAR, 4); - publicAPI.getFormat(VtkDataTypes.UNSIGNED_CHAR, 4); - - if (!model.internalFormat || !model.format || !model.openGLDataType) { - vtkErrorMacro('Failed to determine texture parameters.'); - return false; - } - model.target = model.context.TEXTURE_2D; - model.components = 4; - model.depth = 1; - model.numberOfDimensions = 2; + // Now determine the texture parameters using the arguments. + publicAPI.getOpenGLDataType(VtkDataTypes.UNSIGNED_CHAR); + publicAPI.getInternalFormat(VtkDataTypes.UNSIGNED_CHAR, 4); + publicAPI.getFormat(VtkDataTypes.UNSIGNED_CHAR, 4); - // now store the computed values into the packed 2D - // texture using the same packing as volumeInfo - model.width = scalarTexture.getWidth(); - model.height = scalarTexture.getHeight(); - const newArray = new Uint8Array(model.width * model.height * 4); + if (!model.internalFormat || !model.format || !model.openGLDataType) { + vtkErrorMacro('Failed to determine texture parameters.'); + return false; + } - for (let yRep = 0; yRep < vinfo.yreps; yRep++) { - const xrepsThisRow = Math.min(vinfo.xreps, depth - yRep * vinfo.xreps); - const outXContIncr = - model.width - xrepsThisRow * Math.floor(width / vinfo.xstride); - for (let inY = 0; inY < height; inY += vinfo.ystride) { - for (let xRep = 0; xRep < xrepsThisRow; xRep++) { - const inOffset = - 4 * ((yRep * vinfo.xreps + xRep) * width * height + inY * width); - for (let inX = 0; inX < width; inX += vinfo.xstride) { - // copy value - newArray[outIdx++] = 127.5 + 127.5 * tmpArray[inOffset + inX * 4]; - newArray[outIdx++] = - 127.5 + 127.5 * tmpArray[inOffset + inX * 4 + 1]; - newArray[outIdx++] = - 127.5 + 127.5 * tmpArray[inOffset + inX * 4 + 2]; - // we encode gradient magnitude using sqrt so that - // we have nonlinear resolution - newArray[outIdx++] = - 255.0 * Math.sqrt(tmpArray[inOffset + inX * 4 + 3] / maxMag); + model.target = model.context.TEXTURE_2D; + model.components = 4; + model.depth = 1; + model.numberOfDimensions = 2; + + // now store the computed values into the packed 2D + // texture using the same packing as volumeInfo + model.width = scalarTexture.getWidth(); + model.height = scalarTexture.getHeight(); + + let outIdx = 0; + for (let yRep = 0; yRep < vinfo.yreps; yRep++) { + const xrepsThisRow = Math.min(vinfo.xreps, depth - yRep * vinfo.xreps); + const outXContIncr = + model.width - xrepsThisRow * Math.floor(width / vinfo.xstride); + for (let inY = 0; inY < height; inY += vinfo.ystride) { + for (let xRep = 0; xRep < xrepsThisRow; xRep++) { + const inOffset = + 4 * ((yRep * vinfo.xreps + xRep) * width * height + inY * width); + for (let inX = 0; inX < width; inX += vinfo.xstride) { + // copy value + reformattedGradients[outIdx++] = + 127.5 + 127.5 * gradients[inOffset + inX * 4]; + reformattedGradients[outIdx++] = + 127.5 + 127.5 * gradients[inOffset + inX * 4 + 1]; + reformattedGradients[outIdx++] = + 127.5 + 127.5 * gradients[inOffset + inX * 4 + 2]; + // we encode gradient magnitude using sqrt so that + // we have nonlinear resolution + reformattedGradients[outIdx++] = + 255.0 * Math.sqrt(gradients[inOffset + inX * 4 + 3] / maxMag); + } } + outIdx += outXContIncr * 4; } - outIdx += outXContIncr * 4; } - } - model.openGLRenderWindow.activateTexture(publicAPI); - publicAPI.createTexture(); - publicAPI.bind(); + model.openGLRenderWindow.activateTexture(publicAPI); + publicAPI.createTexture(); + publicAPI.bind(); - // Source texture data from the PBO. - // model.context.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); - model.context.pixelStorei(model.context.UNPACK_ALIGNMENT, 1); + // Source texture data from the PBO. + // model.context.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); + model.context.pixelStorei(model.context.UNPACK_ALIGNMENT, 1); - model.context.texImage2D( - model.target, - 0, - model.internalFormat, - model.width, - model.height, - 0, - model.format, - model.openGLDataType, - newArray - ); + model.context.texImage2D( + model.target, + 0, + model.internalFormat, + model.width, + model.height, + 0, + model.format, + model.openGLDataType, + reformattedGradients + ); - publicAPI.deactivate(); - return true; + publicAPI.deactivate(); + model.computedGradients = true; + model.gradientsBuildTime.modified(); + return true; + }); }; publicAPI.setOpenGLRenderWindow = (rw) => { @@ -1310,6 +1315,7 @@ const DEFAULT_VALUES = { baseLevel: 0, maxLevel: 0, generateMipmap: false, + computedGradients: false, }; // ---------------------------------------------------------------------------- @@ -1326,6 +1332,9 @@ export function extend(publicAPI, model, initialValues = {}) { model.textureBuildTime = {}; macro.obj(model.textureBuildTime, { mtime: 0 }); + model.gradientsBuildTime = {}; + macro.obj(model.gradientsBuildTime, { mtime: 0 }); + // Build VTK API macro.set(publicAPI, model, ['format', 'openGLDataType']); @@ -1346,6 +1355,8 @@ export function extend(publicAPI, model, initialValues = {}) { 'components', 'handle', 'target', + 'computedGradients', + 'gradientsBuildTime', ]); // Object methods diff --git a/Sources/Rendering/OpenGL/VolumeMapper/index.js b/Sources/Rendering/OpenGL/VolumeMapper/index.js index 25a4c7eb54d..0d8929ba7c2 100644 --- a/Sources/Rendering/OpenGL/VolumeMapper/index.js +++ b/Sources/Rendering/OpenGL/VolumeMapper/index.js @@ -93,7 +93,9 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { let FSSource = shaders.Fragment; const iType = actor.getProperty().getInterpolationType(); - const gopacity = actor.getProperty().getUseGradientOpacity(0); + const gopacity = + actor.getProperty().getUseGradientOpacity(0) && + model.lightingTexture.getComputedGradients(); const volInfo = model.scalarTexture.getVolumeInfo(); // WebGL2 @@ -391,7 +393,9 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { !!model.lastZBufferTexture !== !!model.zBufferTexture || cellBO.getShaderSourceTime().getMTime() < publicAPI.getMTime() || cellBO.getShaderSourceTime().getMTime() < actor.getMTime() || - cellBO.getShaderSourceTime().getMTime() < model.currentInput.getMTime() + cellBO.getShaderSourceTime().getMTime() < model.currentInput.getMTime() || + cellBO.getShaderSourceTime().getMTime() < + model.lightingTexture.getGradientsBuildTime().getMTime() ) { model.lastZBufferTexture = model.zBufferTexture; return true; @@ -709,7 +713,8 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { ); program.setUniformf('cscale', sscale / (cRange[1] - cRange[0])); - if (vprop.getUseGradientOpacity(0)) { + const computedGradients = model.lightingTexture.getComputedGradients(); + if (vprop.getUseGradientOpacity(0) && computedGradients) { const lightingInfo = model.lightingTexture.getVolumeInfo(); const gomin = vprop.getGradientOpacityMinimumOpacity(0); const gomax = vprop.getGradientOpacityMaximumOpacity(0); @@ -729,7 +734,10 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { ); } - if (model.lastLightComplexity > 0 || vprop.getUseGradientOpacity(0)) { + if ( + (model.lastLightComplexity > 0 || vprop.getUseGradientOpacity(0)) && + computedGradients + ) { program.setUniformi( 'normalTexture', model.lightingTexture.getTextureUnit() @@ -892,6 +900,7 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { } }; + let computedGradientsRenderTimeout = null; publicAPI.renderPieceDraw = (ren, actor) => { const gl = model.context; @@ -904,7 +913,20 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { actor.getProperty().getShade() || actor.getProperty().getUseGradientOpacity(0) ) { - model.lightingTexture.activate(); + // Only activate once volumeInfo has been populated. + if (model.lightingTexture.getComputedGradients()) { + model.lightingTexture.activate(); + } else { + // We wanted to render, but the gradients have not finished computing. + // So, re-render later. + if (computedGradientsRenderTimeout !== null) { + clearTimeout(computedGradientsRenderTimeout); + } + computedGradientsRenderTimeout = setTimeout( + model.openGLRenderWindow.modified, + 20 + ); + } } publicAPI.updateShaders(model.tris, ren, actor); diff --git a/Sources/Rendering/OpenGL/VolumeMapper/test/testLighting.js b/Sources/Rendering/OpenGL/VolumeMapper/test/testLighting.js index 89d9049303a..5ed9f80f58f 100644 --- a/Sources/Rendering/OpenGL/VolumeMapper/test/testLighting.js +++ b/Sources/Rendering/OpenGL/VolumeMapper/test/testLighting.js @@ -13,6 +13,7 @@ import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper'; import baseline1 from './testLighting.png'; import baseline2 from './testLighting_2.png'; +import baseline3 from './testLighting_3.png'; test.onlyIfWebGL('Test Lighted Volume Rendering', (t) => { const gc = testUtils.createGarbageCollector(t); @@ -94,7 +95,7 @@ test.onlyIfWebGL('Test Lighted Volume Rendering', (t) => { const image = glwindow.captureImage(); testUtils.compareImages( image, - [baseline1, baseline2], + [baseline1, baseline2, baseline3], 'Rendering/OpenGL/VolumeMapper/testLighting', t, 1.5, diff --git a/Sources/Rendering/OpenGL/VolumeMapper/test/testLighting_3.png b/Sources/Rendering/OpenGL/VolumeMapper/test/testLighting_3.png new file mode 100644 index 0000000000000000000000000000000000000000..a01976af026cf9e7339535ebe56447f9873fb584 GIT binary patch literal 48876 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