diff --git a/Sources/Rendering/Core/AbstractMapper/index.d.ts b/Sources/Rendering/Core/AbstractMapper/index.d.ts index 2e62de1c37e..4faf8f2fa46 100755 --- a/Sources/Rendering/Core/AbstractMapper/index.d.ts +++ b/Sources/Rendering/Core/AbstractMapper/index.d.ts @@ -49,6 +49,15 @@ export interface vtkAbstractMapper extends vtkAbstractMapperBase { */ setClippingPlanes(planes: vtkPlane[]): void; + /** + * Get the ith clipping plane as a homogeneous plane equation. + * Use getNumberOfClippingPlanes() to get the number of planes. + * @param {mat4} propMatrix + * @param {Number} i + * @param {Number[]} hnormal + */ + getClippingPlaneInDataCoords(propMatrix : mat4, i : number, hnormal : number[]): void; + /** * */ diff --git a/Sources/Rendering/Core/AbstractMapper/index.js b/Sources/Rendering/Core/AbstractMapper/index.js index a33c9c6e850..95de9107176 100644 --- a/Sources/Rendering/Core/AbstractMapper/index.js +++ b/Sources/Rendering/Core/AbstractMapper/index.js @@ -44,6 +44,36 @@ function vtkAbstractMapper(publicAPI, model) { } } }; + + publicAPI.getClippingPlaneInDataCoords = (propMatrix, i, hnormal) => { + const clipPlanes = model.clippingPlanes; + const mat = propMatrix; + + if (clipPlanes) { + const n = clipPlanes.length; + if (i >= 0 && i < n) { + // Get the plane + const plane = clipPlanes[i]; + const normal = plane.getNormal(); + const origin = plane.getOrigin(); + + // Compute the plane equation + const v1 = normal[0]; + const v2 = normal[1]; + const v3 = normal[2]; + const v4 = -(v1 * origin[0] + v2 * origin[1] + v3 * origin[2]); + + // Transform normal from world to data coords + hnormal[0] = v1 * mat[0] + v2 * mat[4] + v3 * mat[8] + v4 * mat[12]; + hnormal[1] = v1 * mat[1] + v2 * mat[5] + v3 * mat[9] + v4 * mat[13]; + hnormal[2] = v1 * mat[2] + v2 * mat[6] + v3 * mat[10] + v4 * mat[14]; + hnormal[3] = v1 * mat[3] + v2 * mat[7] + v3 * mat[11] + v4 * mat[15]; + + return; + } + } + macro.vtkErrorMacro(`Clipping plane index ${i} is out of range.`); + }; } // ---------------------------------------------------------------------------- diff --git a/Sources/Rendering/Core/AbstractMapper3D/index.d.ts b/Sources/Rendering/Core/AbstractMapper3D/index.d.ts index dcd7c909172..3526067de66 100755 --- a/Sources/Rendering/Core/AbstractMapper3D/index.d.ts +++ b/Sources/Rendering/Core/AbstractMapper3D/index.d.ts @@ -31,14 +31,6 @@ export interface vtkAbstractMapper3D extends vtkAbstractMapper { */ getLength(): number; - /** - * Get the ith clipping plane as a homogeneous plane equation. - * Use getNumberOfClippingPlanes() to get the number of planes. - * @param {mat4} propMatrix - * @param {Number} i - * @param {Number[]} hnormal - */ - getClippingPlaneInDataCoords(propMatrix : mat4, i : number, hnormal : number[]): void; } /** diff --git a/Sources/Rendering/Core/AbstractMapper3D/index.js b/Sources/Rendering/Core/AbstractMapper3D/index.js index 6a3234c1f88..7f0f71b99cd 100644 --- a/Sources/Rendering/Core/AbstractMapper3D/index.js +++ b/Sources/Rendering/Core/AbstractMapper3D/index.js @@ -1,5 +1,3 @@ -import macro from 'vtk.js/Sources/macro'; - import vtkAbstractMapper from 'vtk.js/Sources/Rendering/Core/AbstractMapper'; import * as vtkMath from 'vtk.js/Sources/Common/Core/Math'; // ---------------------------------------------------------------------------- @@ -35,36 +33,6 @@ function vtkAbstractMapper3D(publicAPI, model) { return Math.sqrt(l); }; - - publicAPI.getClippingPlaneInDataCoords = (propMatrix, i, hnormal) => { - const clipPlanes = model.clippingPlanes; - const mat = propMatrix; - - if (clipPlanes) { - const n = clipPlanes.length; - if (i >= 0 && i < n) { - // Get the plane - const plane = clipPlanes[i]; - const normal = plane.getNormal(); - const origin = plane.getOrigin(); - - // Compute the plane equation - const v1 = normal[0]; - const v2 = normal[1]; - const v3 = normal[2]; - const v4 = -(v1 * origin[0] + v2 * origin[1] + v3 * origin[2]); - - // Transform normal from world to data coords - hnormal[0] = v1 * mat[0] + v2 * mat[4] + v3 * mat[8] + v4 * mat[12]; - hnormal[1] = v1 * mat[1] + v2 * mat[5] + v3 * mat[9] + v4 * mat[13]; - hnormal[2] = v1 * mat[2] + v2 * mat[6] + v3 * mat[10] + v4 * mat[14]; - hnormal[3] = v1 * mat[3] + v2 * mat[7] + v3 * mat[11] + v4 * mat[15]; - - return; - } - } - macro.vtkErrorMacro(`Clipping plane index ${i} is out of range.`); - }; } // ---------------------------------------------------------------------------- diff --git a/Sources/Rendering/OpenGL/ImageMapper/index.js b/Sources/Rendering/OpenGL/ImageMapper/index.js index 31a3c61155d..1efeb533756 100644 --- a/Sources/Rendering/OpenGL/ImageMapper/index.js +++ b/Sources/Rendering/OpenGL/ImageMapper/index.js @@ -320,9 +320,56 @@ function vtkOpenGLImageMapper(publicAPI, model) { shaders.Vertex = VSSource; shaders.Fragment = FSSource; + publicAPI.replaceShaderClip(shaders, ren, actor); publicAPI.replaceShaderCoincidentOffset(shaders, ren, actor); }; + publicAPI.replaceShaderClip = (shaders, ren, actor) => { + let VSSource = shaders.Vertex; + let FSSource = shaders.Fragment; + + if (model.renderable.getNumberOfClippingPlanes()) { + let numClipPlanes = model.renderable.getNumberOfClippingPlanes(); + if (numClipPlanes > 6) { + macro.vtkErrorMacro('OpenGL has a limit of 6 clipping planes'); + numClipPlanes = 6; + } + VSSource = vtkShaderProgram.substitute(VSSource, '//VTK::Clip::Dec', [ + 'uniform int numClipPlanes;', + 'uniform vec4 clipPlanes[6];', + 'varying float clipDistancesVSOutput[6];', + ]).result; + + VSSource = vtkShaderProgram.substitute(VSSource, '//VTK::Clip::Impl', [ + 'for (int planeNum = 0; planeNum < 6; planeNum++)', + ' {', + ' if (planeNum >= numClipPlanes)', + ' {', + ' break;', + ' }', + ' clipDistancesVSOutput[planeNum] = dot(clipPlanes[planeNum], vertexMC);', + ' }', + ]).result; + FSSource = vtkShaderProgram.substitute(FSSource, '//VTK::Clip::Dec', [ + 'uniform int numClipPlanes;', + 'varying float clipDistancesVSOutput[6];', + ]).result; + + FSSource = vtkShaderProgram.substitute(FSSource, '//VTK::Clip::Impl', [ + 'for (int planeNum = 0; planeNum < 6; planeNum++)', + ' {', + ' if (planeNum >= numClipPlanes)', + ' {', + ' break;', + ' }', + ' if (clipDistancesVSOutput[planeNum] < 0.0) discard;', + ' }', + ]).result; + } + shaders.Vertex = VSSource; + shaders.Fragment = FSSource; + }; + publicAPI.getNeedToRebuildShaders = (cellBO, ren, actor) => { // has something changed that would require us to recreate the shader? // candidates are @@ -509,6 +556,33 @@ function vtkOpenGLImageMapper(publicAPI, model) { const texOpacityUnit = model.pwfTexture.getTextureUnit(); cellBO.getProgram().setUniformi('pwfTexture1', texOpacityUnit); + + if (model.renderable.getNumberOfClippingPlanes()) { + // add all the clipping planes + let numClipPlanes = model.renderable.getNumberOfClippingPlanes(); + if (numClipPlanes > 6) { + macro.vtkErrorMacro('OpenGL has a limit of 6 clipping planes'); + numClipPlanes = 6; + } + const image = model.currentInput; + const w2imat4 = image.getWorldToIndex(); + mat4.multiply(model.imagematinv, w2imat4, actor.getMatrix()); + const planeEquations = []; + for (let i = 0; i < numClipPlanes; i++) { + const planeEquation = []; + model.renderable.getClippingPlaneInDataCoords( + model.imagematinv, + i, + planeEquation + ); + + for (let j = 0; j < 4; j++) { + planeEquations.push(planeEquation[j]); + } + } + cellBO.getProgram().setUniformi('numClipPlanes', numClipPlanes); + cellBO.getProgram().setUniform4fv('clipPlanes', 6, planeEquations); + } }; publicAPI.setCameraShaderParameters = (cellBO, ren, actor) => { @@ -963,6 +1037,7 @@ const DEFAULT_VALUES = { openGLTexture: null, tris: null, imagemat: null, + imagematinv: null, colorTexture: null, pwfTexture: null, lastHaveSeenDepthRequest: false, @@ -989,6 +1064,7 @@ export function extend(publicAPI, model, initialValues = {}) { model.pwfTexture = vtkOpenGLTexture.newInstance(); model.imagemat = mat4.identity(new Float64Array(16)); + model.imagematinv = mat4.identity(new Float64Array(16)); // Build VTK API macro.setGet(publicAPI, model, []); diff --git a/Sources/Rendering/OpenGL/ImageMapper/test/testImageCroppingPlanes.js b/Sources/Rendering/OpenGL/ImageMapper/test/testImageCroppingPlanes.js new file mode 100644 index 00000000000..a4ef7ddb146 --- /dev/null +++ b/Sources/Rendering/OpenGL/ImageMapper/test/testImageCroppingPlanes.js @@ -0,0 +1,143 @@ +import test from 'tape-catch'; +import testUtils from 'vtk.js/Sources/Testing/testUtils'; + +import vtkImageGridSource from 'vtk.js/Sources/Filters/Sources/ImageGridSource'; +import vtkImageMapper from 'vtk.js/Sources/Rendering/Core/ImageMapper'; +import vtkImageSlice from 'vtk.js/Sources/Rendering/Core/ImageSlice'; +import vtkOpenGLRenderWindow from 'vtk.js/Sources/Rendering/OpenGL/RenderWindow'; +import vtkRenderer from 'vtk.js/Sources/Rendering/Core/Renderer'; +import vtkRenderWindow from 'vtk.js/Sources/Rendering/Core/RenderWindow'; +import vtkPlane from 'vtk.js/Sources/Common/DataModel/Plane'; +import vtkActor from 'vtk.js/Sources/Rendering/Core/Actor'; +import vtkPolyData from 'vtk.js/Sources/Common/DataModel/PolyData'; +import vtkMapper from 'vtk.js/Sources/Rendering/Core/Mapper'; + +import baseline from './testImageCroppingPlanes.png'; + +test.onlyIfWebGL('Test ImageMapper ClippingPlanes', (t) => { + const gc = testUtils.createGarbageCollector(t); + t.ok('rendering', 'vtkOpenGLImageMapper testImage'); + + // Create some control UI + const container = document.querySelector('body'); + const renderWindowContainer = gc.registerDOMElement( + document.createElement('div') + ); + container.appendChild(renderWindowContainer); + + // create what we will view + const renderWindow = gc.registerResource(vtkRenderWindow.newInstance()); + const renderer = gc.registerResource(vtkRenderer.newInstance()); + renderWindow.addRenderer(renderer); + renderer.setBackground(0.32, 0.34, 0.43); + + // ---------------------------------------------------------------------------- + // Test code + // ---------------------------------------------------------------------------- + + const gridSource = gc.registerResource(vtkImageGridSource.newInstance()); + const extent = 200; + const spacing = 16; + const origin = 8; + gridSource.setDataExtent(0, extent, 0, extent, 0, 0); + gridSource.setGridSpacing(spacing, spacing, 0); + gridSource.setGridOrigin(origin, origin, 0); + const direction = [0.866, 0.5, 0, -0.5, 0.866, 0, 0, 0, 1]; + gridSource.setDataDirection(...direction); + + const mapper = gc.registerResource(vtkImageMapper.newInstance()); + mapper.setInputConnection(gridSource.getOutputPort()); + + const clipPlane = vtkPlane.newInstance(); + clipPlane.setOrigin([0.0, 0.0, 0.0]); + clipPlane.setNormal([0.707, 0.707, 0.0]); + mapper.addClippingPlane(clipPlane); + + const actor = gc.registerResource(vtkImageSlice.newInstance()); + actor.getProperty().setColorWindow(255); + actor.getProperty().setColorLevel(127); + actor.getProperty().setOpacity(0.3); + actor.setMapper(mapper); + + const position = [0.3, 0.0, 0.0]; + actor.setPosition(position); + + const polyData = vtkPolyData.newInstance(); + const polyMapper = vtkMapper.newInstance(); + polyMapper.addClippingPlane(clipPlane); + polyMapper.setInputData(polyData); + + const polySideCount = parseInt(extent / spacing, 10); + const polyPoints = new Float32Array(polySideCount * polySideCount * 3); + let vertexIndex = 0; + for (let i = 0; i < polySideCount; i++) { + for (let j = 0; j < polySideCount; j++) { + polyPoints[vertexIndex] = origin + i * spacing; + polyPoints[vertexIndex + 1] = origin + j * spacing; + polyPoints[vertexIndex + 2] = origin; + vertexIndex += 3; + } + } + + function addLines(linesArray, offset, lineCount) { + for (let i = 0; i < lineCount - 1; i++) { + for (let j = 0; j < lineCount - 1; j++) { + const start = i * lineCount + j + offset; + linesArray.push( + 5, + start, + start + 1, + (i + 1) * lineCount + j + 1 + offset, + (i + 1) * lineCount + j + offset, + start + ); + } + } + } + + const polyActor = vtkActor.newInstance(); + polyActor.setMapper(polyMapper); + polyActor.getProperty().setOpacity(1.0); + polyActor.getProperty().setColor(1.0, 0.6, 0.6); + + polyActor.setPosition(position); + + const polyLines = []; + addLines(polyLines, 0, polySideCount); + + const verts = new Uint32Array(polyPoints.length); + verts.fill(1); + for (let i = 0; i < polyPoints.length; i++) { + verts[i * 2 + 1] = i; + } + polyData.getPoints().setData(polyPoints, 3); + polyData.getVerts().setData(verts); + polyData.getLines().setData(new Uint32Array(polyLines)); + + // Applied with DataDirection on the grid + polyActor.rotateZ(30); + + renderer.addActor(actor); + renderer.addActor(polyActor); + renderer.resetCamera(); + renderWindow.render(); + + // create something to view it, in this case webgl + const glwindow = gc.registerResource(vtkOpenGLRenderWindow.newInstance()); + glwindow.setContainer(renderWindowContainer); + renderWindow.addView(glwindow); + glwindow.setSize(400, 400); + + glwindow.captureNextImage().then((image) => { + testUtils.compareImages( + image, + [baseline], + 'Rendering/OpenGL/ImageMapper', + t, + 1, + gc.releaseResources + ); + }); + + renderWindow.render(); +}); diff --git a/Sources/Rendering/OpenGL/ImageMapper/test/testImageCroppingPlanes.png b/Sources/Rendering/OpenGL/ImageMapper/test/testImageCroppingPlanes.png new file mode 100644 index 00000000000..5ce339b9dd9 Binary files /dev/null and b/Sources/Rendering/OpenGL/ImageMapper/test/testImageCroppingPlanes.png differ diff --git a/Sources/tests.js b/Sources/tests.js index b36b6eb8bc0..00658227043 100644 --- a/Sources/tests.js +++ b/Sources/tests.js @@ -64,6 +64,7 @@ import './Rendering/Core/SphereMapper/test/testSphere'; import './Rendering/Core/StickMapper/test/testStick'; import './Rendering/Misc/GenericRenderWindow/test/testGenericRenderWindowCreateDelete'; import './Rendering/OpenGL/ImageMapper/test/testImage'; +import './Rendering/OpenGL/ImageMapper/test/testImageCroppingPlanes'; import './Rendering/OpenGL/ImageMapper/test/testImageNearestNeighbor'; import './Rendering/OpenGL/ImageMapper/test/testImageColorTransferFunction'; import './Rendering/OpenGL/PolyDataMapper/test/testAddShaderReplacements';