├── .gitignore ├── LICENSE ├── README.md ├── camera.js ├── package.json └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules/* 16 | *.DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2013 Mikola Lysenko 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **WORK IN PROGRESS** 2 | 3 | 3d-camera-core 4 | ============== 5 | A common interface for 3D cameras. This module is a caching layer for maintaining coordinate system transformations and computing camera properties from a set of generating matrices. This module is intended to be used as a common interface and should not be required directly. 6 | 7 | ### Notes on coordinates 8 | 9 | By convention, we will define 4 different 3 dimensional projective homogeneous coordinate systems: 10 | 11 | * **Data coordinates**: The coordinates used by models and 3D data 12 | * **World coordinates**: A common coordinate system for all objects within the scene 13 | * **Camera coordinates**: The coordinate system in the world where the camera is at the center 14 | * **Clip coordinates**: The device clip coordinate system 15 | 16 | These coordinates are related by a set of three homogeneous 4x4 matrices: 17 | 18 | * **Model matrix**: Maps data coordinates to world coordinates 19 | * **View matrix**: Maps world coordinates to camera coordinates 20 | * **Projection matrix**: Maps view coordinates to device clip coordinates 21 | 22 | The goal of this module is to maintain the relationships between these coordinate systems as matrices and to define a standard interface for renderable objects which need to consume camera information. Implementors should take this module and hook up whatever methods they want to compute the model/view/projection matrices, while users can then treat the resulting camera interface as a black box handling the various coordinate system conversions. 23 | 24 | # User side 25 | 26 | For most users of this module, you only need to worry about the stuff in this section. 27 | 28 | ## User example 29 | 30 | ```javascript 31 | //You should call some other module to create a camera controller 32 | var myCamera = createCameraType() 33 | 34 | //Once you have a camera, then you can access the coordinate conversions directly 35 | var dataToClip = myCamera.data.toClip 36 | 37 | //You can also access the origin of the camera in any coordinate system too 38 | var eyePosition = myCamera.world.origin 39 | ``` 40 | 41 | ## User API 42 | 43 | The overall goal of this module is to keep track of conversions between a number of different coordinate systems. Because multiplying and recalculating these conversions is expensive, this module caches this data for future use. After a camera object has been created, no further memory allocations are performed. 44 | 45 | ### Coordinate system conversions 46 | 47 | The most basic function of this module is to provide a convenient syntax for getting whatever camera transformations you need. 48 | 49 | #### `coords.toClip` 50 | A 4x4 matrix representing the conversion from `coords` into clip coordinates. 51 | 52 | #### `coords.toCamera` 53 | A 4x4 matrix representing the conversion from `coords` into camera coordinates. 54 | 55 | #### `coords.toWorld` 56 | A 4x4 matrix representing the conversion from `coords` into world coordinates 57 | 58 | #### `coords.toData` 59 | A 4x4 matrix representing the conversion from `coords` into data coordinates. 60 | 61 | ### Origin 62 | 63 | #### `coords.origin` 64 | 65 | The position of the camera in the coordinate system. 66 | 67 | # For implementors 68 | 69 | A camera implementation should provide one or more "controllers" for each of the model, view and projection matrices. Each controller is an object with two methods; one which tests if the controller has changed and one which reads out the state of the matrix for the controller. 70 | 71 | ## Implementation example 72 | 73 | ```javascript 74 | var createCamera = require('3d-camera-core') 75 | 76 | //A simple implementation of a camera controller 77 | function simpleController() { 78 | var data = [1,0,0,0, 79 | 0,1,0,0, 80 | 0,0,1,0, 81 | 0,0,0,1] 82 | var isDirty = false 83 | return { 84 | dirty: function() { 85 | return isDirty 86 | }, 87 | get: function(m) { 88 | isDirty = false 89 | for(var i=0; i<16; ++i) { 90 | m[i] = data[i] 91 | } 92 | }, 93 | set: function(m) { 94 | isDirty = true 95 | for(var i=0; i<16; ++i) { 96 | data[i] = m[i] 97 | } 98 | } 99 | } 100 | } 101 | 102 | //Create a set of controllers for the camera object 103 | var controllers = { 104 | model: simpleController(), 105 | view: simpleController(), 106 | projection: simpleController() 107 | } 108 | 109 | //Return camera 110 | var camera = createCamera(controllers) 111 | ``` 112 | 113 | ## Implementor API 114 | 115 | ### Constructor 116 | 117 | #### `var camera = createCamera(controllers)` 118 | 119 | This creates a new camera object with the given controllers. `controllers` is an object with the following properties: 120 | 121 | * `controllers.model` a controller for the model matrix 122 | * `controllers.view` a controller for view matrix 123 | * `controllers.projection` a controller for the projection matrix 124 | 125 | **Returns** A new camera object 126 | 127 | ### Controller interface 128 | 129 | Each controller is an object which provides two methods: 130 | 131 | #### `controller.dirty()` 132 | This method should test if the state of the controller has changed since the last time `controller.get()` was called. If it has, then the matrix value will be recomputed. 133 | 134 | **Returns** `true` if the camera matrix has changed, otherwise `false` 135 | 136 | #### `controller.get(matrix)` 137 | This retrieves the state of the controller's matrix. The result should be written into `matrix` 138 | 139 | ### Methods 140 | 141 | #### `camera.setController(matrix, controller)` 142 | 143 | Replaces the controller on the camera for `matrix` with `controller`. 144 | 145 | * `matrix` is the name of the matrix, which is either `model`, `view` or `projection` 146 | * `controller` is the new controller for the matrix 147 | 148 | # Legal 149 | 150 | (c) 2015 Mikola Lysenko. MIT License -------------------------------------------------------------------------------- /camera.js: -------------------------------------------------------------------------------- 1 | module.exports = createCamera 2 | 3 | var multiply = require('gl-mat4/multiply') 4 | var invert = require('gl-mat4/invert') 5 | var dup = require('dup') 6 | 7 | var MODEL = 'model' 8 | var VIEW = 'view' 9 | var PROJECTION = 'projection' 10 | 11 | var DATA = 'data' 12 | var WORLD = 'world' 13 | var CAMERA = 'camera' 14 | var CLIP = 'clip' 15 | 16 | var ORIGIN = 'origin' 17 | 18 | var COORD_SYSTEMS = [ DATA, WORLD, CAMERA, CLIP ] 19 | var MATRICES = [ MODEL, VIEW, PROJECTION ] 20 | 21 | function capitalizeFirst(str) { 22 | return str.charAt(0).toUpperCase() + str.slice(1) 23 | } 24 | 25 | function makeMatrix() { 26 | return [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] 27 | } 28 | 29 | function makeVector() { 30 | return [0,0,0] 31 | } 32 | 33 | function makeDefaultController() { 34 | return { 35 | dirty: function() { 36 | return false 37 | }, 38 | get: function(m) { 39 | for(var i=0; i<16; ++i) { 40 | m[i] = 0.0 41 | } 42 | for(var i=0; i<4; ++i) { 43 | m[i] = 1.0 44 | } 45 | } 46 | } 47 | } 48 | 49 | function createCamera(baseControllers) { 50 | var baseControllers = baseControllers || {} 51 | var controllers = {} 52 | var dirty = {} 53 | var storage = {} 54 | var counter = 1 55 | 56 | MATRICES.forEach(function(m) { 57 | controllers[m] = baseControllers[m] || makeDefaultController() 58 | storage[m] = makeMatrix() 59 | dirty[m] = ++counter 60 | controllers[m].get(storage[m]) 61 | }) 62 | 63 | function touch(m) { 64 | var c = controllers[m] 65 | var s = storage[m] 66 | if(c.dirty()) { 67 | c.get(s) 68 | return dirty[m] = ++counter 69 | } else { 70 | return dirty[m] 71 | } 72 | } 73 | 74 | function makeForwardSimple(propName) { 75 | return function() { 76 | touch(propName) 77 | return storage[propName] 78 | } 79 | } 80 | 81 | function makeForward(propName, dependencies) { 82 | var matrices = dependencies.map(function(m) { 83 | return storage[m] 84 | }) 85 | var data = storage[propName] = makeMatrix() 86 | dirty[propName] = 0 87 | return function() { 88 | var d = 0 89 | for(var i=0; idata') 68 | checkMatrix(camera.clip.toWorld, ['view', 'projection'], true, 'clip->world') 69 | checkMatrix(camera.clip.toCamera, ['projection'], true, 'clip->camera') 70 | checkMatrix(camera.clip.toClip, [], false, 'clip->clip') 71 | 72 | checkMatrix(camera.camera.toData, ['model', 'view'], true, 'camera->data') 73 | checkMatrix(camera.camera.toWorld, ['view'], true, 'camera->world') 74 | checkMatrix(camera.camera.toCamera, [], false, 'camera->camera') 75 | checkMatrix(camera.camera.toClip, ['projection'], false, 'camera-clip') 76 | 77 | checkMatrix(camera.world.toData, ['model'], true, 'world->data') 78 | checkMatrix(camera.world.toWorld, [], false, 'world->world') 79 | checkMatrix(camera.world.toCamera, ['view'], false, 'world->camera') 80 | checkMatrix(camera.world.toClip, ['view', 'projection'], false, 'world->clip') 81 | 82 | checkMatrix(camera.data.toData, [], false, 'data->data') 83 | checkMatrix(camera.data.toWorld, ['model'], false, 'data->world') 84 | checkMatrix(camera.data.toCamera, ['model', 'view'], false, 'data->camera') 85 | checkMatrix(camera.data.toClip, ['model', 'view', 'projection'], false, 'data->clip') 86 | } 87 | 88 | fullCheck() 89 | t.equals(controllers.model.numCalls, 1) 90 | t.equals(controllers.view.numCalls, 1) 91 | t.equals(controllers.projection.numCalls, 1) 92 | 93 | controllers.view.set([ 94 | 2, 0, 0, 0, 95 | 0, 1, 0, 0, 96 | 0, 0, 1, 0, 97 | 0, 0, 0, 1]) 98 | fullCheck() 99 | t.equals(controllers.model.numCalls, 1) 100 | t.equals(controllers.view.numCalls, 2) 101 | t.equals(controllers.projection.numCalls, 1) 102 | 103 | controllers.model.set([ 104 | 1, 0, 0, 0, 105 | 0, 2, 0, 0, 106 | 0, 0, 1, 0, 107 | 0, 0, 0, 1]) 108 | fullCheck() 109 | t.equals(controllers.model.numCalls, 2) 110 | t.equals(controllers.view.numCalls, 2) 111 | t.equals(controllers.projection.numCalls, 1) 112 | 113 | controllers.projection.set([ 114 | 1, 0, 0, 0, 115 | 0, 1, 0, 0, 116 | 0, 0, 2, 0, 117 | 0, 0, 0, 1]) 118 | fullCheck() 119 | t.equals(controllers.model.numCalls, 2) 120 | t.equals(controllers.view.numCalls, 2) 121 | t.equals(controllers.projection.numCalls, 2) 122 | 123 | controllers.view.set([ 124 | 1, 0, 0, 0, 125 | 0, 1, 0, 0, 126 | 0, 0, 1, 0, 127 | 1, 1, 1, 1]) 128 | fullCheck() 129 | t.same(camera.world.origin, [-1,-1,-1]) 130 | 131 | t.end() 132 | }) --------------------------------------------------------------------------------