Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create configurable camera #16

Merged
merged 4 commits into from
May 2, 2018
Merged
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
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ module.exports = {
"json",
"node"
],
testRegex: '(/tests/.*|(\\.|/)(test|spec))\\.(ts|js)x?$',
testRegex: '/tests/.*\\.(test|spec)\\.(ts|js)x?$',
coverageDirectory: 'coverage',
collectCoverageFrom: [
'src/**/*.{ts,tsx,js,jsx}',
Expand Down
7 changes: 5 additions & 2 deletions src/examples/render.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Renderer } from '../renderer/Renderer';

import { mat4 } from 'gl-matrix';
import { mat4, vec3 } from 'gl-matrix';
import { flatMap, range } from 'lodash';

const renderer = new Renderer(800, 600);

const transform = mat4.fromTranslation(mat4.create(), [0, 0, -8]);
const transform = mat4.fromTranslation(mat4.create(), [0, 0, -4]);
const vertices: number[] = [];
const normals: number[] = [];
const indices: number[] = [];
Expand Down Expand Up @@ -42,6 +42,9 @@ range(numLat - 1).forEach((lat: number) => {
colors.push(...flatMap(vertices, () => [1, 0, 0]));

document.body.appendChild(renderer.stage);

renderer.camera.moveTo(vec3.fromValues(0, 0, 4));
renderer.camera.lookAt(vec3.fromValues(2, 0, -4));
renderer.draw(
[
{
Expand Down
102 changes: 102 additions & 0 deletions src/renderer/Camera.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { mat4, quat, vec3 } from 'gl-matrix';

/**
* This represents the orientation of the camera in a scene, defined by a position for the camera and
* a target for the camera to look at.
*/
export class Camera {
private static readonly up: vec3 = vec3.fromValues(0, 1, 0);
private static readonly defaultDirection: vec3 = vec3.fromValues(0, 0, -1);

private position: vec3 = vec3.fromValues(0, 0, 0);
private target: vec3 = vec3.copy(vec3.create(), Camera.defaultDirection);

// `transform` is only updated when getTransform is called. The `dirty` flag is used to signal
// that an update has occurred and the cached transform value needs to be recomputed.
private transform: mat4 = mat4.create();
private dirty: boolean = true;

/**
* @param {vec3} position The world-space coordinate to move the camera to, preserving rotation.
*/
public moveTo(position: vec3) {
const direction = vec3.subtract(vec3.create(), position, this.position);
vec3.add(this.target, this.target, direction);
vec3.copy(this.position, position);
this.dirty = true;
}

/**
* @param {vec3} position The world-space coordinate to move the camera to, keeping the camera
* rotated towards its previous target.
*/
public moveToWithFixedTarget(position: vec3) {
vec3.copy(this.position, position);
this.dirty = true;
}

/**
* @param {vec3} direction A world-space direction vector that will be added to the camera's
* position, preserving its existing rotation.
*/
public moveBy(direction: vec3) {
vec3.add(this.position, this.position, direction);
vec3.add(this.target, this.target, direction);
this.dirty = true;
}

/**
* @param {vec3} direction A world-space direction vector that will be added to the camera's
* position, keeping the camera pointed towards its previous target.
*/
public moveByWithFixedTarget(direction: vec3) {
vec3.add(this.position, this.position, direction);
this.dirty = true;
}

/**
* @param {vec3} target A world-space coordinate that the camera will rotate to face.
*/
public lookAt(target: vec3) {
vec3.copy(this.target, target);
this.dirty = true;
}

/**
* @param {vec3} target A quaternion that will be applied to the camera's current rotation.
*/
public rotate(rotation: quat) {
const direction = vec3.subtract(vec3.create(), this.target, this.position);
vec3.normalize(direction, direction);
vec3.transformQuat(direction, direction, rotation);
vec3.add(this.target, this.position, direction);
this.dirty = true;
}

/**
* @param {vec3} target A quaternion that will replace the camera's current rotation.
*/
public setRotation(rotation: quat) {
const direction = vec3.copy(vec3.create(), Camera.defaultDirection);
vec3.transformQuat(direction, direction, rotation);
vec3.add(this.target, this.position, direction);
this.dirty = true;
}

/**
* Using the current position and target of the camera, produces a transformation matrix to
* bring world space coordinates into camera space.
*/
public getTransform(): mat4 {
if (this.dirty) {
this.updateTransform();
}

return this.transform;
}

private updateTransform() {
mat4.lookAt(this.transform, this.position, this.target, Camera.up);
this.dirty = false;
}
}
8 changes: 5 additions & 3 deletions src/renderer/Renderer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Camera } from './Camera';
import { drawAxes, DrawAxesProps } from './commands/drawAxes';
import { drawObject, DrawObjectProps } from './commands/drawObject';

Expand Down Expand Up @@ -27,12 +28,13 @@ export class Renderer {
public readonly height: number;
public readonly stage: HTMLDivElement;

public camera: Camera = new Camera();

private clearAll: () => void;
private clearDepth: () => void;
private drawObject: REGL.DrawCommand<REGL.DefaultContext, DrawObjectProps>;
private drawAxes: REGL.DrawCommand<REGL.DefaultContext, DrawAxesProps>;

private cameraTransform: mat4 = mat4.create();
private projectionMatrix: mat4 = mat4.create();
private ctx2D: CanvasRenderingContext2D;

Expand Down Expand Up @@ -98,7 +100,7 @@ export class Renderer {
objects.forEach((o: RenderObject) =>
this.drawObject({
model: o.transform,
cameraTransform: this.cameraTransform,
cameraTransform: this.camera.getTransform(),
projectionMatrix: this.projectionMatrix,
positions: o.vertices,
normals: o.normals,
Expand All @@ -122,7 +124,7 @@ export class Renderer {
const vector = vec4.fromValues(point[0], point[1], point[2], 0);

// Bring them into camera space
vec4.transformMat4(vector, vector, this.cameraTransform);
vec4.transformMat4(vector, vector, this.camera.getTransform());

// Scale them and place them in the lower left corner of the screen
vec4.scale(vector, vector, Math.min(this.width, this.height) * 0.05);
Expand Down
67 changes: 67 additions & 0 deletions tests/Renderer/Camera.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Camera } from '../../src/renderer/Camera';
import '../glMatrix';

import { mat4, quat, vec3 } from 'gl-matrix';

describe('Camera', () => {
it('defaults to an identity transformation', () => {
expect(new Camera().getTransform()).toEqualMat4(mat4.create());
});

it('can be moved', () => {
const camera = new Camera();
camera.lookAt(vec3.fromValues(0, 0, -1));
camera.moveTo(vec3.fromValues(0, 1, 1));
expect(mat4.getTranslation(vec3.create(), camera.getTransform())).toEqualVec3(
vec3.fromValues(0, -1, -1)
);
expect(mat4.getRotation(quat.create(), camera.getTransform())).toEqualQuat(quat.create());
});

it('can be moved without changing the target', () => {
const camera = new Camera();
camera.lookAt(vec3.fromValues(0, 0, -1));
camera.moveToWithFixedTarget(vec3.fromValues(0, 2, 1));
expect(
vec3.transformMat4(vec3.create(), vec3.fromValues(0, 0, -1), camera.getTransform())
).toEqualVec3(vec3.fromValues(0, 0, -Math.sqrt(2) * 2));
});

it('can be moved incrementally', () => {
const camera = new Camera();
camera.lookAt(vec3.fromValues(0, 0, -1));
camera.moveTo(vec3.fromValues(0, 1, 0));
camera.moveBy(vec3.fromValues(0, 0, 1));
expect(mat4.getTranslation(vec3.create(), camera.getTransform())).toEqualVec3(
vec3.fromValues(0, -1, -1)
);
expect(mat4.getRotation(quat.create(), camera.getTransform())).toEqualQuat(quat.create());
});

it('can be moved incrementally without changing the target', () => {
const camera = new Camera();
camera.lookAt(vec3.fromValues(0, 0, -1));
camera.moveToWithFixedTarget(vec3.fromValues(0, 1, 1));
camera.moveByWithFixedTarget(vec3.fromValues(0, 1, 0));
expect(
vec3.transformMat4(vec3.create(), vec3.fromValues(0, 0, -1), camera.getTransform())
).toEqualVec3(vec3.fromValues(0, 0, -Math.sqrt(2) * 2));
});

it('can be rotated', () => {
const camera = new Camera();
camera.setRotation(quat.fromEuler(quat.create(), 0, 90, 0));
expect(
vec3.transformMat4(vec3.create(), vec3.fromValues(1, 0, 0), camera.getTransform())
).toEqualVec3(vec3.fromValues(0, 0, 1));
});

it('can be rotated incrementally', () => {
const camera = new Camera();
camera.setRotation(quat.fromEuler(quat.create(), 0, 90, 0));
camera.rotate(quat.fromEuler(quat.create(), 0, 90, 0));
expect(
vec3.transformMat4(vec3.create(), vec3.fromValues(0, 0, -1), camera.getTransform())
).toEqualVec3(vec3.fromValues(0, 0, 1));
});
});
59 changes: 59 additions & 0 deletions tests/glMatrix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { mat4, quat, vec3 } from 'gl-matrix';

declare global {
namespace jest {
interface Matchers<R> {
toEqualMat4(argument: mat4): { pass: boolean; message(): string };
toEqualVec3(argument: vec3): { pass: boolean; message(): string };
toEqualQuat(argument: quat): { pass: boolean; message(): string };
}
}
}

expect.extend({
toEqualMat4(received: mat4, argument: mat4) {
if (mat4.equals(received, argument)) {
return {
message: () =>
`expected ${mat4.str(received)} to not be equal to ${mat4.str(argument)}`,
pass: true
};
} else {
return {
message: () =>
`expected ${mat4.str(received)} to be equal to ${mat4.str(argument)}`,
pass: false
};
}
},
toEqualVec3(received: vec3, argument: vec3) {
if (vec3.equals(received, argument)) {
return {
message: () =>
`expected ${vec3.str(received)} to not be equal to ${vec3.str(argument)}`,
pass: true
};
} else {
return {
message: () =>
`expected ${vec3.str(received)} to be equal to ${vec3.str(argument)}`,
pass: false
};
}
},
toEqualQuat(received: quat, argument: quat) {
if (quat.equals(received, argument)) {
return {
message: () =>
`expected ${quat.str(received)} to not be equal to ${quat.str(argument)}`,
pass: true
};
} else {
return {
message: () =>
`expected ${quat.str(received)} to be equal to ${quat.str(argument)}`,
pass: false
};
}
}
});
1 change: 1 addition & 0 deletions tslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"no-multiline-string": false,
"no-require-imports": false,
"no-suspicious-comment": false,
"no-import-side-effect": false,
"typedef": [
true,
"parameter",
Expand Down