Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 4 additions & 12 deletions Sources/Rendering/Core/Camera/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -498,12 +498,6 @@ function vtkCamera(publicAPI, model) {
publicAPI.computeViewParametersFromViewMatrix(tmpMatrix);
};

publicAPI.setModelTransformMatrix = (mat) => {
model.modelTransformMatrix = mat;
};

publicAPI.getModelTransformMatrix = () => model.modelTransformMatrix;

publicAPI.setViewMatrix = (mat) => {
model.viewMatrix = mat;
if (model.viewMatrix) {
Expand All @@ -516,7 +510,7 @@ function vtkCamera(publicAPI, model) {
publicAPI.getViewMatrix = () => {
if (model.viewMatrix) {
if (model.modelTransformMatrix) {
mat4.multiply(tmpMatrix, model.viewMatrix, model.modelTransformMatrix);
mat4.multiply(tmpMatrix, model.modelTransformMatrix, model.viewMatrix);
return tmpMatrix;
}
return model.viewMatrix;
Expand All @@ -531,13 +525,10 @@ function vtkCamera(publicAPI, model) {

mat4.transpose(tmpMatrix, tmpMatrix);

const result = new Float64Array(16);
if (model.modelTransformMatrix) {
mat4.multiply(result, tmpMatrix, model.modelTransformMatrix);
} else {
mat4.copy(result, tmpMatrix);
mat4.multiply(tmpMatrix, model.modelTransformMatrix, tmpMatrix);
}
return result;
return tmpMatrix;
};

publicAPI.setProjectionMatrix = (mat) => {
Expand Down Expand Up @@ -809,6 +800,7 @@ export function extend(publicAPI, model, initialValues = {}) {
'useOffAxisProjection',
'freezeFocalPoint',
'physicalScale',
'modelTransformMatrix',
]);

macro.getArray(publicAPI, model, [
Expand Down
321 changes: 321 additions & 0 deletions Sources/Rendering/Core/Camera/test/testModelTransformMatrix.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { mat4, vec3 } from 'gl-matrix';
import vtkCamera from 'vtk.js/Sources/Rendering/Core/Camera';
import vtkTransform from 'vtk.js/Sources/Common/Transform/Transform';

describe('Camera Model Transform Matrix', () => {
let camera;

beforeEach(() => {
camera = vtkCamera.newInstance();
});

describe('Basic Model Transform API', () => {
it('should support setting and getting model transform matrix', () => {
const transform = vtkTransform.newInstance();
transform.scale(2, 3, 4);
const matrix = transform.getMatrix();

camera.setModelTransformMatrix(matrix);
const retrieved = camera.getModelTransformMatrix();

expect(retrieved).toBeDefined();
expect(retrieved).toEqual(matrix);

transform.delete();
});

it('should handle null model transform matrix', () => {
expect(camera.getModelTransformMatrix()).toBeNull();
});

it('should allow clearing model transform matrix', () => {
const transform = vtkTransform.newInstance();
transform.scale(2, 2, 2);
camera.setModelTransformMatrix(transform.getMatrix());

camera.setModelTransformMatrix(null);
expect(camera.getModelTransformMatrix()).toBeNull();

transform.delete();
});
});

describe('View Matrix Composition with Model Transform', () => {
it('should compose model transform with view matrix for camera-computed view matrix', () => {
// Setup camera position
camera.setPosition(0, 0, 10);
camera.setFocalPoint(0, 0, 0);
camera.setViewUp(0, 1, 0);

// Create a simple scale transform
const transform = vtkTransform.newInstance();
transform.scale(1, 1, 2); // Scale Z by 2x

const viewMatrixWithoutTransform = camera.getViewMatrix();
camera.setModelTransformMatrix(transform.getMatrix());
const viewMatrixWithTransform = camera.getViewMatrix();

// Matrices should be different after applying transform
expect(viewMatrixWithTransform).not.toEqual(viewMatrixWithoutTransform);

transform.delete();
});

it('should compose model transform with manual view matrix', () => {
// Set a manual view matrix
const manualViewMatrix = mat4.create();
mat4.lookAt(
manualViewMatrix,
[0, 0, 10], // eye
[0, 0, 0], // center
[0, 1, 0] // up
);
mat4.transpose(manualViewMatrix, manualViewMatrix);

camera.setViewMatrix(manualViewMatrix);

// Apply model transform
const transform = vtkTransform.newInstance();
transform.scale(2, 2, 2);
camera.setModelTransformMatrix(transform.getMatrix());

const result = camera.getViewMatrix();

// Should be a new array (composition result)
expect(result).toBeDefined();
expect(result.length).toBe(16);

transform.delete();
});
});

describe('World-Space Transform Application (Fix Verification)', () => {
it('should apply model transform in world space (pre-multiplication)', () => {
// Setup camera
camera.setPosition(10, 10, 10);
camera.setFocalPoint(0, 0, 0);
camera.setViewUp(0, 1, 0);

// Create vertical exaggeration transform (scale Z)
const scaleZ = 2;
const transform = vtkTransform.newInstance();
transform.scale(1, 1, scaleZ);
const modelTransformMatrix = transform.getMatrix();

// Get view matrix with transform applied
camera.setModelTransformMatrix(modelTransformMatrix);
const viewMatrixWithTransform = camera.getViewMatrix();

// Get view matrix without transform for comparison
camera.setModelTransformMatrix(null);
const viewMatrixWithoutTransform = camera.getViewMatrix();

// The transform should affect the view matrix
expect(viewMatrixWithTransform).not.toEqual(viewMatrixWithoutTransform);

// Verify the transform is in world space by testing point transformation
// A point in world space that would be affected by Z-scaling
const testPoint = vec3.fromValues(0, 0, 1);
const transformedPoint = vec3.create();

// Apply view matrix to test point
vec3.transformMat4(transformedPoint, testPoint, viewMatrixWithTransform);

// The Z-scale should be reflected in the transformation
// This verifies world-space application (not camera-space)
expect(transformedPoint).toBeDefined();

transform.delete();
});

it('should maintain vertical exaggeration alignment regardless of camera rotation', () => {
// This test verifies the fix: transforms should be world-space invariant
// to camera orientation

// Create a Z-scale transform (vertical exaggeration)
const transform = vtkTransform.newInstance();
transform.scale(1, 1, 2); // 2x vertical exaggeration
const modelTransformMatrix = transform.getMatrix();

// Get view matrix at initial orientation
camera.setPosition(0, 0, 10);
camera.setFocalPoint(0, 0, 0);
camera.setViewUp(0, 1, 0);
camera.setModelTransformMatrix(modelTransformMatrix);
const viewMatrix1 = camera.getViewMatrix();

// Get view matrix after camera rotation
// Rotate camera around focal point (elevation)
camera.elevation(45);
const viewMatrix2 = camera.getViewMatrix();

// Both should incorporate the same model transform
// The model transform should affect both view matrices
expect(viewMatrix1).toBeDefined();
expect(viewMatrix2).toBeDefined();

// Verify both matrices are different (due to camera rotation)
// but both contain the model transform
let matricesDiffer = false;
for (let i = 0; i < 16; i++) {
if (Math.abs(viewMatrix1[i] - viewMatrix2[i]) > 1e-6) {
matricesDiffer = true;
break;
}
}
expect(matricesDiffer).toBe(true); // Camera rotation should change view matrix

// The key test: model transform should be pre-multiplied
// This is verified by checking that the Z-scaling is preserved
// in world space regardless of camera orientation
camera.elevation(-45); // Reset
expect(camera.getModelTransformMatrix()).toEqual(modelTransformMatrix);

transform.delete();
});

it('should apply scale transform correctly for vertical exaggeration use case', () => {
// Replicate the actual vertical exaggeration use case from the MRE
camera.setPosition(5, 5, 10);
camera.setFocalPoint(0, 0, 0);
camera.setViewUp(0, 0, 1); // Z-up scene (like in the MRE)

// Apply 2x vertical exaggeration
const exaggeration = 2;
const transform = vtkTransform.newInstance();
transform.scale(1, 1, exaggeration); // Scale world Z-axis

camera.setModelTransformMatrix(transform.getMatrix());
const viewMatrix = camera.getViewMatrix();

// Verify view matrix is created successfully
expect(viewMatrix).toBeDefined();
expect(viewMatrix.length).toBe(16);

// Test with different exaggerations
const exaggerations = [0.5, 1, 1.5, 2, 3, 5];
exaggerations.forEach((scale) => {
const t = vtkTransform.newInstance();
t.scale(1, 1, scale);
camera.setModelTransformMatrix(t.getMatrix());

const vm = camera.getViewMatrix();
expect(vm).toBeDefined();
expect(vm.length).toBe(16);

t.delete();
});

transform.delete();
});
});

describe('Matrix Multiplication Order Verification', () => {
it('should use pre-multiplication (modelTransform * viewMatrix)', () => {
// This test specifically verifies that the fix uses pre-multiplication
// (world-space) rather than post-multiplication (camera-space)

camera.setPosition(5, 5, 5);
camera.setFocalPoint(0, 0, 0);
camera.setViewUp(0, 1, 0);

// Create a translation in the model transform
const transform = vtkTransform.newInstance();
transform.translate(1, 0, 0); // Translate along X
const modelTransformMatrix = transform.getMatrix();

camera.setModelTransformMatrix(modelTransformMatrix);
const viewMatrix = camera.getViewMatrix();

// Get the view matrix without transform
camera.setModelTransformMatrix(null);
const viewMatrixOnly = camera.getViewMatrix();

// Manually compute pre-multiplication (world space)
const preMultiplied = mat4.create();
mat4.multiply(preMultiplied, modelTransformMatrix, viewMatrixOnly);

// Manually compute post-multiplication (camera space)
const postMultiplied = mat4.create();
mat4.multiply(postMultiplied, viewMatrixOnly, modelTransformMatrix);

// The actual result should match pre-multiplication (world space)
let preMultipliedMatches = true;
for (let i = 0; i < 16; i++) {
if (Math.abs(viewMatrix[i] - preMultiplied[i]) > 1e-6) {
preMultipliedMatches = false;
break;
}
}
expect(preMultipliedMatches).toBe(true);

// And should NOT match post-multiplication
let postMultipliedMatches = true;
for (let i = 0; i < 16; i++) {
if (Math.abs(viewMatrix[i] - postMultiplied[i]) > 1e-6) {
postMultipliedMatches = false;
break;
}
}
expect(postMultipliedMatches).toBe(false);

transform.delete();
});
});

describe('Edge Cases and Robustness', () => {
it('should handle identity transform', () => {
const transform = vtkTransform.newInstance();
// No operations on identity transform
camera.setModelTransformMatrix(transform.getMatrix());

const viewMatrix = camera.getViewMatrix();
expect(viewMatrix).toBeDefined();

transform.delete();
});

it('should handle multiple transform applications', () => {
const transform = vtkTransform.newInstance();
transform.scale(2, 2, 2);
camera.setModelTransformMatrix(transform.getMatrix());

// Update multiple times
for (let i = 0; i < 5; i++) {
const vm = camera.getViewMatrix();
expect(vm).toBeDefined();

// Change scale
const t = vtkTransform.newInstance();
t.scale(1, 1, i + 1);
camera.setModelTransformMatrix(t.getMatrix());
t.delete();
}

transform.delete();
});

it('should work with various camera parameters', () => {
const positions = [
[0, 0, 10],
[10, 10, 10],
[-5, 5, 20],
[0, 0, -10],
];

const transform = vtkTransform.newInstance();
transform.scale(1, 1, 3);
camera.setModelTransformMatrix(transform.getMatrix());

positions.forEach((pos) => {
camera.setPosition(...pos);
const vm = camera.getViewMatrix();
expect(vm).toBeDefined();
expect(vm.length).toBe(16);
});

transform.delete();
});
});
});
Loading