Skip to content

Commit

Permalink
feat(Rendering): Add follower class that always faces the camera
Browse files Browse the repository at this point in the history
Subclass of Actor that always faces the camera.
  • Loading branch information
Ken Martin committed Jun 22, 2020
1 parent bb7eab2 commit e8d8325
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 0 deletions.
25 changes: 25 additions & 0 deletions Sources/Rendering/Core/Follower/api.md
Original file line number Diff line number Diff line change
@@ -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).
130 changes: 130 additions & 0 deletions Sources/Rendering/Core/Follower/index.js
Original file line number Diff line number Diff line change
@@ -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 };
73 changes: 73 additions & 0 deletions Sources/Rendering/Core/Follower/test/testFollower.js
Original file line number Diff line number Diff line change
@@ -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();
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions Sources/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down

0 comments on commit e8d8325

Please sign in to comment.