diff --git a/Sources/Rendering/Core/Follower/api.md b/Sources/Rendering/Core/Follower/api.md new file mode 100644 index 00000000000..e32ae12745f --- /dev/null +++ b/Sources/Rendering/Core/Follower/api.md @@ -0,0 +1,25 @@ +vtkFollower is a subclass of Actor that always faces the camera. + +You must set the camera before use. This class will update the matrix such +that the follower always faces the camera. Sepcifically the y axis will up +up, the Z axes will point to the camera and the x axis will point to the +right. You may need to rotate, scale, position the follower to get your data +oriented propoerly for this convention. + +If useViewUp is set then instea dof using the camera's view up the follow's +vieUp will be used. This is usefull in cases where you want up to be locked +independent of the camera. This is typically the case for VR or AR +annotations where the headset may tilt but text should continue to be +relative to a constant view up vector. + +## See Also + +[vtkActor](./Rendering_Core_Actor.html) + +## setCamera() + +Set the camera that this follower should face + +## getMTime() + +Get the newest "modification time" of the actor, its properties, and texture (if set). diff --git a/Sources/Rendering/Core/Follower/index.js b/Sources/Rendering/Core/Follower/index.js new file mode 100644 index 00000000000..375c4383f35 --- /dev/null +++ b/Sources/Rendering/Core/Follower/index.js @@ -0,0 +1,130 @@ +import { vec3, mat4 } from 'gl-matrix'; +import macro from 'vtk.js/Sources/macro'; +import vtkActor from 'vtk.js/Sources/Rendering/Core/Actor'; + +// ---------------------------------------------------------------------------- +// vtkFollower methods +// ---------------------------------------------------------------------------- + +function vtkFollower(publicAPI, model) { + // Set our className + model.classHierarchy.push('vtkFollower'); + + // Capture 'parentClass' api for internal use + const superClass = { ...publicAPI }; + + publicAPI.getMTime = () => { + let mt = superClass.getMTime(); + if (model.camera !== null) { + const time = model.camera.getMTime(); + mt = time > mt ? time : mt; + } + + return mt; + }; + + publicAPI.computeMatrix = () => { + // check whether or not need to rebuild the matrix + if (publicAPI.getMTime() > model.matrixMTime.getMTime()) { + mat4.identity(model.matrix); + if (model.userMatrix) { + mat4.multiply(model.matrix, model.matrix, model.userMatrix); + } + mat4.translate(model.matrix, model.matrix, model.origin); + mat4.translate(model.matrix, model.matrix, model.position); + mat4.multiply(model.matrix, model.matrix, model.rotation); + mat4.scale(model.matrix, model.matrix, model.scale); + + if (model.camera) { + // first compute our target viewUp + const vup = vec3.fromValues(model.viewUp); + if (!model.useViewUp) { + vec3.set(vup, ...model.camera.getViewUp()); + } + + // compute a vpn + const vpn = vec3.create(); + if (model.camera.getParallelProjection()) { + vec3.set(vpn, model.camera.getViewPlaneNormal()); + } else { + vec3.set(vpn, ...model.position); + const tmpv3 = vec3.fromValues(...model.camera.getPosition()); + vec3.subtract(vpn, tmpv3, vpn); + vec3.normalize(vpn, vpn); + } + + // compute vright + const vright = vec3.create(); + vec3.cross(vright, vup, vpn); + vec3.normalize(vright, vright); + + // now recompute the vpn so that it is orthogonal to vup + vec3.cross(vpn, vright, vup); + vec3.normalize(vpn, vpn); + + model.followerMatrix[0] = vright[0]; + model.followerMatrix[1] = vright[1]; + model.followerMatrix[2] = vright[2]; + + model.followerMatrix[4] = vup[0]; + model.followerMatrix[5] = vup[1]; + model.followerMatrix[6] = vup[2]; + + model.followerMatrix[8] = vpn[0]; + model.followerMatrix[9] = vpn[1]; + model.followerMatrix[10] = vpn[2]; + + mat4.multiply(model.matrix, model.followerMatrix, model.matrix); + } + + mat4.translate(model.matrix, model.matrix, [ + -model.origin[0], + -model.origin[1], + -model.origin[2], + ]); + mat4.transpose(model.matrix, model.matrix); + + // check for identity + model.isIdentity = false; + model.matrixMTime.modified(); + } + }; +} + +// ---------------------------------------------------------------------------- +// Object factory +// ---------------------------------------------------------------------------- + +const DEFAULT_VALUES = { + viewUp: [0, 1, 0], + useViewUp: false, + camera: null, +}; + +// ---------------------------------------------------------------------------- + +export function extend(publicAPI, model, initialValues = {}) { + Object.assign(model, DEFAULT_VALUES, initialValues); + + // Inheritance + vtkActor.extend(publicAPI, model, initialValues); + + model.followerMatrix = mat4.create(); + mat4.identity(model.followerMatrix); + + // Build VTK API + macro.setGet(publicAPI, model, ['useViewUp', 'camera']); + + macro.setGetArray(publicAPI, model, ['viewUp'], 3); + + // Object methods + vtkFollower(publicAPI, model); +} + +// ---------------------------------------------------------------------------- + +export const newInstance = macro.newInstance(extend, 'vtkFollower'); + +// ---------------------------------------------------------------------------- + +export default { newInstance, extend }; diff --git a/Sources/Rendering/Core/Follower/test/testFollower.js b/Sources/Rendering/Core/Follower/test/testFollower.js new file mode 100644 index 00000000000..8fe82f2488a --- /dev/null +++ b/Sources/Rendering/Core/Follower/test/testFollower.js @@ -0,0 +1,73 @@ +import test from 'tape-catch'; +import testUtils from 'vtk.js/Sources/Testing/testUtils'; + +import vtkFollower from 'vtk.js/Sources/Rendering/Core/Follower'; +import vtkMapper from 'vtk.js/Sources/Rendering/Core/Mapper'; +import vtkOBJReader from 'vtk.js/Sources/IO/Misc/OBJReader'; +import vtkOpenGLRenderWindow from 'vtk.js/Sources/Rendering/OpenGL/RenderWindow'; +import vtkRenderWindow from 'vtk.js/Sources/Rendering/Core/RenderWindow'; +import vtkRenderer from 'vtk.js/Sources/Rendering/Core/Renderer'; + +import baseline from './testFollower.png'; + +test.onlyIfWebGL('Test Follower class', (t) => { + const gc = testUtils.createGarbageCollector(t); + t.ok('rendering', 'vtkFollower'); + + // 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.1, 0.2, 0.4); + + // ---------------------------------------------------------------------------- + // Test code + // ---------------------------------------------------------------------------- + const reader = gc.registerResource( + vtkOBJReader.newInstance({ splitMode: 'usemtl' }) + ); + + const mapper = gc.registerResource(vtkMapper.newInstance()); + mapper.setInputConnection(reader.getOutputPort()); + + const actor = gc.registerResource(vtkFollower.newInstance()); + actor.setMapper(mapper); + actor.setCamera(renderer.getActiveCamera()); + renderer.addActor(actor); + + reader + .setUrl( + `${__BASE_PATH__}/Data/obj/space-shuttle-orbiter/space-shuttle-orbiter.obj` + ) + .then(() => { + renderer.resetCamera(); + renderWindow.render(); + + const glwindow = gc.registerResource(vtkOpenGLRenderWindow.newInstance()); + glwindow.setContainer(renderWindowContainer); + renderWindow.addView(glwindow); + glwindow.setSize(400, 400); + + renderer.getActiveCamera().azimuth(10); + renderer.getActiveCamera().elevation(10); + renderer.getActiveCamera().orthogonalizeViewUp(); + glwindow.captureNextImage().then((image) => { + testUtils.compareImages( + image, + [baseline], + 'Rendering/Core/Follower/testFollower', + t, + 1.5, + gc.releaseResources + ); + }); + renderWindow.render(); + }); +}); diff --git a/Sources/Rendering/Core/Follower/test/testFollower.png b/Sources/Rendering/Core/Follower/test/testFollower.png new file mode 100644 index 00000000000..eba1b4c7762 Binary files /dev/null and b/Sources/Rendering/Core/Follower/test/testFollower.png differ diff --git a/Sources/tests.js b/Sources/tests.js index acaa66424b2..a1f92d7d5eb 100644 --- a/Sources/tests.js +++ b/Sources/tests.js @@ -35,6 +35,7 @@ import './Rendering/Core/ColorTransferFunction/test/testColorTransferFunction'; import './Rendering/Core/ColorTransferFunction/test/testColorTransferFunctionInterpolation'; import './Rendering/Core/ColorTransferFunction/test/testColorTransferFunctionPresets'; import './Rendering/Core/Coordinate/test/testCoordinate'; +import './Rendering/Core/Follower/test/testFollower'; import './Rendering/Core/Glyph3DMapper/test/testGlyph3DMapper'; import './Rendering/Core/Mapper/test/testEdgeVisibility'; import './Rendering/Core/Mapper/test/testVectorComponent';