UNPKG

@acransac/vtk.js

Version:

Visualization Toolkit for the Web

699 lines (636 loc) 21.2 kB
import macro from 'vtk.js/Sources/macro'; import vtkOpenGLTexture from 'vtk.js/Sources/Rendering/OpenGL/Texture'; import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray'; import vtkHelper from 'vtk.js/Sources/Rendering/OpenGL/Helper'; import vtkProperty from 'vtk.js/Sources/Rendering/Core/Property'; import vtkVertexArrayObject from 'vtk.js/Sources/Rendering/OpenGL/VertexArrayObject'; import vtkFrameBuffer from 'vtk.js/Sources/Rendering/OpenGL/Framebuffer'; import vtkLineIntegralConvolution2D from 'vtk.js/Sources/Rendering/OpenGL/SurfaceLIC/LineIntegralConvolution2D'; /* eslint-disable camelcase */ import vtkLineIntegralConvolution2D_quadVS from 'vtk.js/Sources/Rendering/OpenGL/SurfaceLIC/glsl/vtkLineIntegralConvolution2D_quadVS.glsl'; import vtkLineIntegralConvolution2D_SC from 'vtk.js/Sources/Rendering/OpenGL/SurfaceLIC/glsl/vtkLineIntegralConvolution2D_SC.glsl'; import vtkSurfaceLICInterface_DCpy from 'vtk.js/Sources/Rendering/OpenGL/SurfaceLIC/glsl/vtkSurfaceLICInterface_DCpy.glsl'; import vtkSurfaceLICInterface_CE from 'vtk.js/Sources/Rendering/OpenGL/SurfaceLIC/glsl/vtkSurfaceLICInterface_CE.glsl'; /* eslint-enable camelcase */ import seedrandom from 'seedrandom'; import { ContrastEnhanceMode, NoiseType, } from 'vtk.js/Sources/Rendering/Core/SurfaceLICInterface/Constants'; import vtkSurfaceLICInterface from 'vtk.js/Sources/Rendering/Core/SurfaceLICInterface'; const { Representation } = vtkProperty; // ---------------------------------------------------------------------------- // vtkLICInterface methods // ---------------------------------------------------------------------------- function getQuadPoly(openGLRenderWindow) { const quad = vtkHelper.newInstance(); quad.setOpenGLRenderWindow(openGLRenderWindow); // build the CABO const ptsArray = new Float32Array(12); for (let i = 0; i < 4; i++) { ptsArray[i * 3] = (i % 2) * 2 - 1.0; ptsArray[i * 3 + 1] = i > 1 ? 1.0 : -1.0; ptsArray[i * 3 + 2] = 0.0; } const tCoord = new Float32Array([0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0]); const cellArray = new Uint16Array(8); cellArray[0] = 3; cellArray[1] = 0; cellArray[2] = 1; cellArray[3] = 3; cellArray[4] = 3; cellArray[5] = 0; cellArray[6] = 3; cellArray[7] = 2; const points = vtkDataArray.newInstance({ numberOfComponents: 3, values: ptsArray, }); points.setName('points'); const cells = vtkDataArray.newInstance({ numberOfComponents: 1, values: cellArray, }); const tArray = vtkDataArray.newInstance({ numberOfComponents: 2, values: tCoord, }); quad.getCABO().createVBO(cells, 'polys', Representation.SURFACE, { points, cellOffset: 0, tcoords: tArray, }); return quad; } function vtkOpenGLSurfaceLICInterface(publicAPI, model) { model.classHierarchy.push('vtkOpenGLSurfaceLICInterface'); publicAPI.renderQuad = (bounds, program) => { const poly = model.licQuad; const gl = model.context; let VAO = model.licQuadVAO; if (!VAO) { VAO = vtkVertexArrayObject.newInstance(); VAO.setOpenGLRenderWindow(model.openGLRenderWindow); model.licQuadVAO = VAO; } if (model.previousProgramHash !== program.getMd5Hash()) { VAO.shaderProgramChanged(); poly.getCABO().bind(); VAO.addAttributeArray( program, poly.getCABO(), 'vertexDC', poly.getCABO().getVertexOffset(), poly.getCABO().getStride(), model.context.FLOAT, 3, model.context.FALSE ); VAO.addAttributeArray( program, poly.getCABO(), 'tcoordDC', poly.getCABO().getTCoordOffset(), poly.getCABO().getStride(), model.context.FLOAT, 2, model.context.FALSE ); model.previousProgramHash = program.getMd5Hash(); } gl.drawArrays(gl.TRIANGLES, 0, poly.getCABO().getElementCount()); VAO.release(); }; function generateGaussianNoise( length, numberOfNoiseLevels, noiseImpulseProbability, noiseImpulseBackgroundValue, min, max ) { const N = 2048; const impulseProb = Math.max(0.0, Math.min(1.0, noiseImpulseProbability)); const noise = Float32Array.from({ length: length * length }, () => { let val = 0; if (impulseProb === 1.0 || Math.random() > 1.0 - impulseProb) { for (let i = 0; i < N; ++i) { val += Math.random(); } } return val; }); // Normalize let maxVal = 0.0; let minVal = N + 1; noise.forEach((val) => { // Don't count 0s for minVal if impulseProb < 1.0 if (impulseProb === 1.0) { minVal = val < minVal ? val : minVal; } else { minVal = val < minVal && val > 0.0 ? val : minVal; } maxVal = val > maxVal ? val : maxVal; }); let diff = maxVal - minVal; if (diff === 0.0) { minVal = 0.0; if (maxVal === 0.0) { diff = 1.0; } else { diff = maxVal; } } const maxLevel = numberOfNoiseLevels - 1; const delta = maxLevel !== 0 ? 1.0 / maxLevel : 0.0; const noiseRange = max - min; return noise.map((val) => { const normalized = val < minVal ? val : (val - minVal) / diff; const l = Math.floor(normalized * numberOfNoiseLevels); const quantized = l > maxLevel ? maxLevel : l; if (val >= minVal) { if (numberOfNoiseLevels === 1) { return max; } return min + quantized * delta * noiseRange; } return noiseImpulseBackgroundValue; }); } function generateUniformNoise( [width, height], numberOfNoiseLevels, min, max ) { const diff = max - min; return Float32Array.from({ length: width * height }, () => { let r = Math.random(); r = Math.floor(r * numberOfNoiseLevels) / numberOfNoiseLevels; r = r * diff + min; if (r > 1.0) { return 1.0; } if (r < 0.0) { return 0.0; } return r; }); } publicAPI.generateNoiseTexture = (length) => { if (!model.noiseTexture || model.licInterface.getRebuildNoiseTexture()) { model.licInterface.setRebuildNoiseTexture(false); if (model.noiseTexture) { model.noiseTexture.releaseGraphicsResources(); } // Reseed RNG seedrandom(model.noiseGeneratorSeed, { global: true }); let base = []; const { noiseTextureType, noiseGrainSize, numberOfNoiseLevels, noiseImpulseProbability, noiseImpulseBackgroundValue, minNoiseValue, maxNoiseValue, } = model.licInterface.get( 'noiseTextureType', 'noiseGrainSize', 'numberOfNoiseLevels', 'noiseImpulseProbability', 'noiseImpulseBackgroundValue', 'minNoiseValue', 'maxNoiseValue' ); switch (noiseTextureType) { case NoiseType.GAUSSIAN: base = generateGaussianNoise( Math.floor(length / noiseGrainSize), numberOfNoiseLevels, noiseImpulseProbability, noiseImpulseBackgroundValue, minNoiseValue, maxNoiseValue ); break; case NoiseType.UNIFORM: default: base = generateUniformNoise( [ Math.ceil(length / noiseGrainSize), Math.ceil(length / noiseGrainSize), ], numberOfNoiseLevels, minNoiseValue, maxNoiseValue ); } const invGrainSize = 1.0 / noiseGrainSize; const values = Float32Array.from( { length: length * length * 4 }, (val, index) => { const baseIndex = index / 4; if (index % 4 === 0) { const x = Math.floor((baseIndex % length) * invGrainSize); const y = Math.floor((baseIndex / length) * invGrainSize); return base[y * (length / noiseGrainSize) + x]; } if (index % 4 === 1 || index % 4 === 3) { return 1.0; } return 0.0; } ); const texture = vtkOpenGLTexture.newInstance({ wrapS: vtkOpenGLTexture.Wrap.REPEAT, wrapT: vtkOpenGLTexture.Wrap.REPEAT, minificationFilter: vtkOpenGLTexture.Filter.NEAREST, magnificationFilter: vtkOpenGLTexture.Filter.NEAREST, generateMipMap: false, openGLDataType: model.context.FLOAT, baseLevel: 0, maxLevel: 0, autoParameters: false, }); texture.setOpenGLRenderWindow(model.openGLRenderWindow); texture.create2DFromRaw(length, length, 4, 'Float32Array', values); texture.activate(); texture.sendParameters(); texture.deactivate(); model.noiseTexture = texture; } }; publicAPI.buildAShader = (fSource) => model.openGLRenderWindow .getShaderCache() .readyShaderProgramArray( vtkLineIntegralConvolution2D_quadVS, fSource, '' ); publicAPI.allocateTextures = () => { const nearest = vtkOpenGLTexture.Filter.NEAREST; const linear = vtkOpenGLTexture.Filter.LINEAR; const rw = model.openGLRenderWindow; if (!model.geometryImage) { model.geometryImage = publicAPI.allocateTexture(rw, nearest); } if (!model.vectorImage) { model.vectorImage = publicAPI.allocateTexture(rw, linear); } if (!model.maskVectorImage) { model.maskVectorImage = publicAPI.allocateTexture(rw, linear); } if (!model.LICImage) { model.LICImage = publicAPI.allocateTexture(rw, nearest); } if (!model.RGBColorImage) { model.RGBColorImage = publicAPI.allocateTexture(rw, nearest); } if (!model.HSLColorImage) { model.HSLColorImage = publicAPI.allocateTexture(rw, nearest); } if (!model.depthTexture) { model.depthTexture = publicAPI.allocateDepthTexture(rw); } }; publicAPI.allocateTexture = (openGLRenderWindow, filter) => { const gl = model.context; const texture = vtkOpenGLTexture.newInstance({ wrapS: vtkOpenGLTexture.Wrap.CLAMP_TO_EDGE, wrapT: vtkOpenGLTexture.Wrap.CLAMP_TO_EDGE, minificationFilter: filter, magnificationFilter: filter, generateMipmap: false, openGLDataType: gl.FLOAT, baseLevel: 0, maxLevel: 0, autoParameters: false, }); texture.setOpenGLRenderWindow(openGLRenderWindow); texture.setInternalFormat(gl.RGBA32F); texture.create2DFromRaw(...model.size, 4, 'Float32Array', null); texture.activate(); texture.sendParameters(); texture.deactivate(); return texture; }; publicAPI.allocateDepthTexture = (openGLRenderWindow) => { const gl = model.context; const texture = vtkOpenGLTexture.newInstance({ generateMipmap: false, openGLDataType: gl.FLOAT, autoParameters: false, }); texture.setOpenGLRenderWindow(openGLRenderWindow); texture.createDepthFromRaw(...model.size, 'Float32Array', null); texture.activate(); texture.sendParameters(); texture.deactivate(); return texture; }; publicAPI.createFBO = () => { if (!model.framebuffer) { model.licHelper = null; // All buffers need rebuilding const fb = vtkFrameBuffer.newInstance(); fb.setOpenGLRenderWindow(model.openGLRenderWindow); fb.saveCurrentBindingsAndBuffers(); fb.create(...model.size); fb.populateFramebuffer(); model.framebuffer = fb; fb.restorePreviousBindingsAndBuffers(); } }; publicAPI.completedGeometry = () => { const gl = model.context; const fb = model.framebuffer; fb.removeColorBuffer(0); fb.removeColorBuffer(1); fb.removeColorBuffer(2); fb.removeDepthBuffer(); gl.drawBuffers([gl.NONE]); fb.restorePreviousBindingsAndBuffers(); }; publicAPI.buildAllShaders = () => { if (model.shadersNeedBuilding) { model.licColorPass = publicAPI.buildAShader( vtkLineIntegralConvolution2D_SC ); model.licCopyPass = publicAPI.buildAShader(vtkSurfaceLICInterface_DCpy); model.enhanceContrastPass = publicAPI.buildAShader( vtkSurfaceLICInterface_CE ); model.shadersNeedBuilding = false; } }; publicAPI.initializeResources = () => { publicAPI.createFBO(); publicAPI.generateNoiseTexture(model.licInterface.getNoiseTextureSize()); publicAPI.allocateTextures(); publicAPI.buildAllShaders(); if (!model.licQuad) { model.licQuad = getQuadPoly(model.openGLRenderWindow); } if (!model.licHelper) { model.licHelper = vtkLineIntegralConvolution2D.newInstance(); } }; publicAPI.prepareForGeometry = () => { const fb = model.framebuffer; fb.saveCurrentBindingsAndBuffers(); fb.bind(); model.geometryImage.activate(); model.vectorImage.activate(); model.maskVectorImage.activate(); fb.removeColorBuffer(0); fb.removeColorBuffer(1); fb.removeColorBuffer(2); fb.setColorBuffer(model.geometryImage, 0); fb.setColorBuffer(model.vectorImage, 1); fb.setColorBuffer(model.maskVectorImage, 2); fb.setDepthBuffer(model.depthTexture); const gl = model.context; gl.drawBuffers([ gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1, gl.COLOR_ATTACHMENT2, ]); gl.viewport(0, 0, ...model.size); gl.scissor(0, 0, ...model.size); gl.disable(gl.BLEND); gl.disable(gl.DEPTH_TEST); gl.disable(gl.SCISSOR_TEST); gl.clearColor(0.0, 0.0, 0.0, 0.0); // eslint-disable-next-line no-bitwise gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT); }; // Copies the LIC image to the renderWindow. Will try to upscale the image to match the rw's size. publicAPI.copyToScreen = (windowSize) => { model.RGBColorImage.activate(); model.depthTexture.activate(); if (!model.licCopyPass) { publicAPI.initializeResources(); } const copyPass = model.licCopyPass; model.openGLRenderWindow.getShaderCache().readyShaderProgram(copyPass); const gl = model.context; gl.viewport(0, 0, ...windowSize); gl.scissor(0, 0, ...windowSize); gl.disable(gl.BLEND); gl.enable(gl.DEPTH_TEST); gl.disable(gl.SCISSOR_TEST); copyPass.setUniformi('texDepth', model.depthTexture.getTextureUnit()); copyPass.setUniformi('texRGBColors', model.RGBColorImage.getTextureUnit()); publicAPI.renderQuad(windowSize, copyPass); model.RGBColorImage.deactivate(); model.depthTexture.deactivate(); }; publicAPI.combineColorsAndLIC = () => { const gl = model.context; const fb = model.framebuffer; fb.saveCurrentBindingsAndBuffers(); fb.bind(); fb.create(...model.size); fb.removeColorBuffer(0); fb.removeColorBuffer(1); fb.setColorBuffer(model.RGBColorImage, 0); fb.setColorBuffer(model.HSLColorImage, 1); gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1]); gl.disable(gl.DEPTH_TEST); gl.clearColor(0.0, 0.0, 0.0, 0.0); gl.clear(gl.COLOR_BUFFER_BIT); model.vectorImage.activate(); model.geometryImage.activate(); model.LICImage.activate(); if (!model.licColorPass) { publicAPI.initializeResources(); } const colorPass = model.licColorPass; model.openGLRenderWindow.getShaderCache().readyShaderProgram(colorPass); colorPass.setUniformi('texVectors', model.vectorImage.getTextureUnit()); colorPass.setUniformi( 'texGeomColors', model.geometryImage.getTextureUnit() ); const { colorMode, LICIntensity, mapModeBias, maskIntensity, maskColor, enhanceContrast, lowColorContrastEnhancementFactor, highColorContrastEnhancementFactor, } = model.licInterface.get( 'colorMode', 'LICIntensity', 'mapModeBias', 'maskIntensity', 'maskColor', 'enhanceContrast', 'lowColorContrastEnhancementFactor', 'highColorContrastEnhancementFactor' ); colorPass.setUniformi('texLIC', model.LICImage.getTextureUnit()); colorPass.setUniformi('uScalarColorMode', colorMode); colorPass.setUniformf('uLICIntensity', LICIntensity); colorPass.setUniformf('uMapBias', mapModeBias); colorPass.setUniformf('uMaskIntensity', maskIntensity); colorPass.setUniform3f('uMaskColor', ...maskColor); publicAPI.renderQuad(model.size, colorPass); model.vectorImage.deactivate(); model.geometryImage.deactivate(); model.LICImage.deactivate(); fb.removeColorBuffer(0); fb.removeColorBuffer(1); gl.drawBuffers([gl.NONE]); if ( enhanceContrast === ContrastEnhanceMode.COLOR || enhanceContrast === ContrastEnhanceMode.BOTH ) { // min and max luminance values. Most of the time close to 0 and 1 let min = 0.0; let max = 1.0; let lDiff = max - min; min += lDiff * lowColorContrastEnhancementFactor; max -= lDiff * highColorContrastEnhancementFactor; lDiff = max - min; fb.setColorBuffer(model.RGBColorImage); gl.drawBuffers([gl.COLOR_ATTACHMENT0]); model.geometryImage.activate(); model.HSLColorImage.activate(); model.LICImage.activate(); if (!model.enhanceContrastPass) { publicAPI.initializeResources(); } const { enhanceContrastPass } = model; model.openGLRenderWindow .getShaderCache() .readyShaderProgram(enhanceContrastPass); enhanceContrastPass.setUniformi( 'texGeomColors', model.geometryImage.getTextureUnit() ); enhanceContrastPass.setUniformi( 'texHSLColors', model.HSLColorImage.getTextureUnit() ); enhanceContrastPass.setUniformi( 'texLIC', model.LICImage.getTextureUnit() ); enhanceContrastPass.setUniformf('uLMin', min); enhanceContrastPass.setUniformf('uLMaxMinDiff', lDiff); publicAPI.renderQuad(model.size, enhanceContrastPass); model.geometryImage.deactivate(); model.HSLColorImage.deactivate(); model.LICImage.deactivate(); fb.removeColorBuffer(0); gl.drawBuffers([gl.NONE]); } fb.restorePreviousBindingsAndBuffers(); }; publicAPI.applyLIC = () => { const options = model.licInterface.get( 'stepSize', 'numberOfSteps', 'enhancedLIC', 'enhanceContrast', 'lowLICContrastEnhancementFactor', 'highLICContrastEnhancementFactor', 'antiAlias', 'normalizeVectors', 'maskThreshold', 'transformVectors' ); const resultTexture = model.licHelper.executeLIC( model.size, model.vectorImage, model.maskVectorImage, model.noiseTexture, model.openGLRenderWindow, options ); if (!resultTexture) { console.error('Failed to compute image LIC'); model.LICImage = null; return; } model.LICImage = resultTexture; }; publicAPI.setSize = (size) => { // If size changed, reallocate fb and textures if (Array.isArray(size) && size.length === 2) { if ( !model.size || model.size[0] !== size[0] || model.size[1] !== size[1] ) { model.size = size; publicAPI.releaseGraphicsResources(); } } }; publicAPI.releaseGraphicsResources = () => { if (model.geometryImage) { model.geometryImage.releaseGraphicsResources(); model.geometryImage = null; } if (model.vectorImage) { model.vectorImage.releaseGraphicsResources(); model.vectorImage = null; } if (model.maskVectorImage) { model.maskVectorImage.releaseGraphicsResources(); model.maskVectorImage = null; } if (model.LICImage) { model.LICImage.releaseGraphicsResources(); model.LICImage = null; } if (model.RGBColorImage) { model.RGBColorImage.releaseGraphicsResources(); model.RGBColorImage = null; } if (model.HSLColorImage) { model.HSLColorImage.releaseGraphicsResources(); model.HSLColorImage = null; } if (model.depthTexture) { model.depthTexture.releaseGraphicsResources(); model.depthTexture = null; } if (model.framebuffer) { model.framebuffer.releaseGraphicsResources(); model.framebuffer = null; } }; } const DEFAULT_VALUES = { context: null, openGLRenderWindow: null, shadersNeedBuilding: true, reallocateTextures: true, size: null, licInterface: null, }; export function extend(publicAPI, model, initialValues = {}) { Object.assign(model, DEFAULT_VALUES, initialValues); // Inherit vtkSurfaceLICInterface.extend(publicAPI, model, initialValues); macro.obj(publicAPI, model); macro.setGet(publicAPI, model, [ 'context', 'openGLRenderWindow', 'reallocateTextures', 'licInterface', 'size', ]); // Object methods vtkOpenGLSurfaceLICInterface(publicAPI, model); } // ---------------------------------------------------------------------------- export const newInstance = macro.newInstance(extend, 'vtkSurfaceLICInterface'); // ---------------------------------------------------------------------------- export default { newInstance, extend };