├── .gitattributes ├── video └── multi-camera.mp4 ├── tests ├── 3dstreet │ ├── assets │ │ └── images │ │ │ └── skybox │ │ │ ├── negx.jpg │ │ │ ├── negy.jpg │ │ │ ├── negz.jpg │ │ │ ├── posx.jpg │ │ │ ├── posy.jpg │ │ │ └── posz.jpg │ └── lib │ │ └── aframe-cubemap-component.js ├── mirror-color-management.html ├── camera-texture-geometries.html ├── camera-texture-add-remove.html ├── multi-screen-with-cursor-plus-cube-env-map.html └── multi-screen-with-cursor-add-remove.html ├── LICENSE ├── examples ├── mirror-example.html ├── monitor-user-view.html ├── viewpoint-selector-basic.html ├── mouse-rotate-controls.js ├── viewpoint-selector-alternate.html ├── camera-texture.html ├── multi-screen.html ├── multi-screen-with-cursor.html └── embedded-views.html ├── src ├── mirror.js ├── multi-camera.js └── viewpoint-selector.js ├── aframe └── cursor.js └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /video/multi-camera.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diarmidmackenzie/aframe-multi-camera/HEAD/video/multi-camera.mp4 -------------------------------------------------------------------------------- /tests/3dstreet/assets/images/skybox/negx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diarmidmackenzie/aframe-multi-camera/HEAD/tests/3dstreet/assets/images/skybox/negx.jpg -------------------------------------------------------------------------------- /tests/3dstreet/assets/images/skybox/negy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diarmidmackenzie/aframe-multi-camera/HEAD/tests/3dstreet/assets/images/skybox/negy.jpg -------------------------------------------------------------------------------- /tests/3dstreet/assets/images/skybox/negz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diarmidmackenzie/aframe-multi-camera/HEAD/tests/3dstreet/assets/images/skybox/negz.jpg -------------------------------------------------------------------------------- /tests/3dstreet/assets/images/skybox/posx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diarmidmackenzie/aframe-multi-camera/HEAD/tests/3dstreet/assets/images/skybox/posx.jpg -------------------------------------------------------------------------------- /tests/3dstreet/assets/images/skybox/posy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diarmidmackenzie/aframe-multi-camera/HEAD/tests/3dstreet/assets/images/skybox/posy.jpg -------------------------------------------------------------------------------- /tests/3dstreet/assets/images/skybox/posz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diarmidmackenzie/aframe-multi-camera/HEAD/tests/3dstreet/assets/images/skybox/posz.jpg -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 diarmidmackenzie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/mirror-example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 17 | 18 | 19 | 21 | 23 | 25 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/mirror-color-management.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 17 | 18 | 19 | 21 | 23 | 25 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /examples/monitor-user-view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 21 | 22 | 23 | 27 | 29 | 30 | 31 | 32 | 33 |
34 | Drag with the mouse to look around, and move with WASD.
35 | The screen down in front of you shows your current camera view.
36 | Look at it and you will see an infinite tunnel of camera views. 37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /examples/viewpoint-selector-basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/mouse-rotate-controls.js: -------------------------------------------------------------------------------- 1 | /* Basic mouse cvontrols to rotate. Derived from 2 | A-Frame Example: https://aframe.io/examples/showcase/modelviewer/ 3 | But with some significant changes to improve rotation stability */ 4 | AFRAME.registerComponent('mouse-rotate-controls', { 5 | 6 | init: function () { 7 | 8 | this.xQuaternion = new THREE.Quaternion(); 9 | this.yQuaternion = new THREE.Quaternion(); 10 | this.yAxis = new THREE.Vector3(); 11 | this.xAxis = new THREE.Vector3(1, 0, 0); 12 | this.unusedVector = new THREE.Vector3(); 13 | 14 | // Mouse 2D controls. 15 | this.onMouseUp = this.onMouseUp.bind(this); 16 | this.onMouseMove = this.onMouseMove.bind(this); 17 | this.onMouseDown = this.onMouseDown.bind(this); 18 | document.addEventListener('mouseup', this.onMouseUp); 19 | document.addEventListener('mousemove', this.onMouseMove); 20 | document.addEventListener('mousedown', this.onMouseDown); 21 | }, 22 | 23 | onMouseDown: function (evt) { 24 | this.oldClientX = evt.clientX; 25 | this.oldClientY = evt.clientY; 26 | }, 27 | 28 | onMouseUp: function (evt) { 29 | if (evt.buttons === undefined || evt.buttons !== 0) { return; } 30 | this.oldClientX = undefined; 31 | this.oldClientY = undefined; 32 | }, 33 | 34 | onMouseMove: function (evt) { 35 | this.rotateModel(evt); 36 | }, 37 | 38 | rotateModel: function (evt) { 39 | var dX; 40 | var dY; 41 | 42 | if (!this.oldClientX) { return; } 43 | dX = this.oldClientX - evt.clientX; 44 | dY = this.oldClientY - evt.clientY; 45 | 46 | // xAxis for rotation is fixed. 47 | // yAxis comes from target object. 48 | this.el.object3D.matrix.extractBasis(this.unusedVector, this.yAxis, this.unusedVector); 49 | this.xQuaternion.setFromAxisAngle(this.yAxis, -dX / 400) 50 | this.yQuaternion.setFromAxisAngle(this.xAxis, -dY / 400) 51 | 52 | this.el.object3D.quaternion.premultiply(this.xQuaternion); 53 | this.el.object3D.quaternion.premultiply(this.yQuaternion); 54 | 55 | this.oldClientX = evt.clientX; 56 | this.oldClientY = evt.clientY; 57 | } 58 | }); 59 | 60 | AFRAME.registerComponent('hover', { 61 | 62 | init() { 63 | this.el.addEventListener("mouseenter", this.hover.bind(this)); 64 | this.el.addEventListener("mouseleave", this.unhover.bind(this)); 65 | }, 66 | 67 | hover() { 68 | this.el.setAttribute("material", "emissive: #fff; emissiveIntensity: 0.2") 69 | }, 70 | 71 | unhover() { 72 | this.el.setAttribute("material", "emissive: #000; emissiveIntensity: 0") 73 | } 74 | }); 75 | -------------------------------------------------------------------------------- /examples/viewpoint-selector-alternate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
44 |
45 | 46 | 47 | -------------------------------------------------------------------------------- /examples/camera-texture.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 16 | 17 | 18 | 20 | 22 | 24 | 26 | 27 | 29 | 30 | 32 | 33 | 35 | 36 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
45 | Drag with the mouse to look around, and move with WASD.
46 | The planes in the scene in front of you show various different camera angles on the shapes.
47 | All rendered onto planes within the A-Frame scene itself. 48 |
49 | 50 | 51 | -------------------------------------------------------------------------------- /tests/camera-texture-geometries.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 16 | 17 | 18 | 20 | 22 | 24 | 26 | 27 | 29 | 30 | 32 | 33 | 35 | 36 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
45 | Drag with the mouse to look around, and move with WASD.
46 | The geometries in the scene in front of you show various different camera angles on the shapes.
47 | All rendered onto planes within the A-Frame scene itself. 48 |
49 | 50 | 51 | -------------------------------------------------------------------------------- /examples/multi-screen.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 | These cameras are fixed relative to the objects in the scene. 43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | These cameras are in fixed world positions. 52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | 60 |
61 | Drag with the mouse to rotate the scene 62 |
63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /examples/multi-screen-with-cursor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | 24 | 26 | 27 | 29 | 30 | 31 | 32 | 33 | 34 | 36 | 37 | 39 | 40 | 42 | 43 | 44 | 45 | 46 |
47 | 48 | 49 |
50 | These cameras are fixed relative to the objects in the scene. 51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | These cameras are in fixed world positions. 60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | 68 |
69 | Drag with the mouse to rotate the scene
70 | Hover over an object on any camera to highlight it. 71 |
72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /tests/camera-texture-add-remove.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 60 | 61 | 62 | 63 | 64 | 65 | 67 | 68 | 70 | 71 | 72 | 74 | 76 | 78 | 80 | 81 | 82 | 83 | 84 | 85 |
86 | Drag with the mouse to look around, and move with WASD.
87 | The planes in the scene in front of you show various different camera angles on the shapes.
88 | All rendered onto planes within the A-Frame scene itself.
89 | Cameras are added & removed at random every second. 90 |
91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /tests/multi-screen-with-cursor-plus-cube-env-map.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 28 | 29 | 31 | 32 | 34 | 35 | 36 | 37 | 38 | 39 | 41 | 42 | 44 | 45 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
54 | 55 | 56 |
57 | These cameras are fixed relative to the objects in the scene. 58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | These cameras are in fixed world positions. 67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | 75 |
76 | Drag with the mouse to rotate the scene
77 | Hover over an object on any camera to highlight it. 78 |
79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /tests/multi-screen-with-cursor-add-remove.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 64 | 65 | 66 | 67 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 |
82 | These cameras are fixed relative to the objects in the scene.
83 | Camera views are added/removed at random every second. 84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | These cameras are in fixed world positions.
93 | Camera views are added/removed at random every second. 94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | 102 |
103 | Drag with the mouse to rotate the scene
104 | Hover over an object on any camera to highlight it. 105 |
106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /tests/3dstreet/lib/aframe-cubemap-component.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | if (typeof AFRAME === "undefined") { 3 | throw new Error( 4 | "Component attempted to register before AFRAME was available." 5 | ); 6 | } 7 | 8 | /** 9 | * Cubemap component for A-Frame. 10 | * 11 | * Adapted from "Skybox and environment map in Three.js" by Roman Liutikov 12 | * https://web.archive.org/web/20160206163422/https://blog.romanliutikov.com/post/58705840698/skybox-and-environment-map-in-threejs 13 | * 14 | */ 15 | AFRAME.registerComponent("cubemap", { 16 | schema: { 17 | folder: { 18 | type: "string", 19 | }, 20 | edgeLength: { 21 | type: "int", 22 | default: 5000, 23 | }, 24 | ext: { 25 | type: "string", 26 | default: "jpg", 27 | }, 28 | }, 29 | 30 | /** 31 | * Called once when the component is initialized. 32 | * Used to set up initial state and instantiate variables. 33 | */ 34 | init: function () { 35 | // entity data 36 | const el = this.el; 37 | const data = this.data; 38 | 39 | // A Cubemap can be rendered as a mesh composed of a BoxBufferGeometry and 40 | // ShaderMaterial. EdgeLength will scale the mesh 41 | this.geometry = new THREE.BoxBufferGeometry(1, 1, 1); 42 | 43 | // Now for the ShaderMaterial. 44 | const shader = THREE.ShaderLib["cube"]; 45 | // Note: cloning the material is necessary to prevent the cube shader's 46 | // uniforms from being mutated. If the material was not cloned, all cubemaps 47 | // in the scene would share the same uniforms (and look identical). 48 | this.material = new THREE.ShaderMaterial({ 49 | fragmentShader: shader.fragmentShader, 50 | vertexShader: shader.vertexShader, 51 | uniforms: shader.uniforms, 52 | depthWrite: false, 53 | side: THREE.BackSide, 54 | }).clone(); 55 | // Threejs seems to have removed the 'tCube' uniform. 56 | // Workaround from: https://stackoverflow.com/a/59454999/6591491 57 | Object.defineProperty(this.material, "envMap", { 58 | get: function () { 59 | return this.uniforms.envMap.value; 60 | }, 61 | }); 62 | // A dummy texture is needed (otherwise the shader will be invalid and spew 63 | // a million errors) 64 | this.material.uniforms["envMap"].value = new THREE.Texture(); 65 | this.loader = new THREE.CubeTextureLoader(); 66 | 67 | // We can create the mesh now and update the material with a texture later on 68 | // in the update lifecycle handler. 69 | this.mesh = new THREE.Mesh(this.geometry, this.material); 70 | this.mesh.scale.set(data.edgeLength, data.edgeLength, data.edgeLength); 71 | el.setObject3D("cubemap", this.mesh); 72 | }, 73 | 74 | /** 75 | * Called when component is attached and when component data changes. 76 | * Generally modifies the entity based on the data. 77 | */ 78 | update: function (oldData) { 79 | // entity data 80 | const el = this.el; 81 | const data = this.data; 82 | const rendererSystem = el.sceneEl.systems.renderer; 83 | 84 | if (data.edgeLength !== oldData.edgeLength) { 85 | // Update the size of the skybox. 86 | this.mesh.scale.set(data.edgeLength, data.edgeLength, data.edgeLength); 87 | } 88 | 89 | if (data.ext !== oldData.ext || data.folder !== oldData.folder) { 90 | // Load textures. 91 | // Path to the folder containing the 6 cubemap images 92 | const srcPath = data.folder; 93 | // Cubemap image files must follow this naming scheme 94 | // from: http://threejs.org/docs/index.html#Reference/Textures/CubeTexture 95 | var urls = ["posx", "negx", "posy", "negy", "posz", "negz"]; 96 | // Apply extension 97 | urls = urls.map(function (val) { 98 | return val + "." + data.ext; 99 | }); 100 | 101 | // Set folder path, and load cubemap textures 102 | this.loader.setPath(srcPath); 103 | this.loader.load(urls, onTextureLoad.bind(this)); 104 | 105 | function onTextureLoad(texture) { 106 | if (srcPath !== this.data.folder) { 107 | // The texture that just finished loading no longer matches the folder 108 | // set on this component. This can happen when the user calls setAttribute() 109 | // to change folders multiple times in quick succession. 110 | texture.dispose(); 111 | return; 112 | } 113 | // Have the renderer system set texture encoding as in A-Frame core. 114 | // https://github.com/bryik/aframe-cubemap-component/issues/13#issuecomment-626238202 115 | rendererSystem.applyColorCorrection(texture); 116 | 117 | // Apply cubemap texture to shader uniforms and dispose of the old texture. 118 | const oldTexture = this.material.uniforms["envMap"].value; 119 | this.material.uniforms["envMap"].value = texture; 120 | if (oldTexture) { 121 | oldTexture.dispose(); 122 | } 123 | 124 | // Tell the world that the cubemap texture has loaded. 125 | el.emit("cubemapLoaded"); 126 | } 127 | } 128 | }, 129 | 130 | /** 131 | * Called when a component is removed (e.g., via removeAttribute). 132 | * Generally undoes all modifications to the entity. 133 | */ 134 | remove: function () { 135 | this.geometry.dispose(); 136 | this.material.uniforms["envMap"].value.dispose(); 137 | this.material.dispose(); 138 | this.el.removeObject3D("cubemap"); 139 | }, 140 | }); -------------------------------------------------------------------------------- /examples/embedded-views.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 15 | 16 | 17 | 18 | 19 | 21 | 22 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 | 38 |
39 |
40 |
41 |
42 |

43 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 44 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 45 | veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 46 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate 47 | velit esse cillum dolore eu fugiat nulla pariatur. 48 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia 49 | deserunt mollit anim id est laborum. 50 |

51 |

52 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 53 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 54 | veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 55 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate 56 | velit esse cillum dolore eu fugiat nulla pariatur. 57 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia 58 | deserunt mollit anim id est laborum. 59 |

60 |
61 | 62 |
63 |
64 |
65 |

66 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 67 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 68 | veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 69 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate 70 | velit esse cillum dolore eu fugiat nulla pariatur. 71 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia 72 | deserunt mollit anim id est laborum. 73 |

74 |

75 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 76 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 77 | veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 78 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate 79 | velit esse cillum dolore eu fugiat nulla pariatur. 80 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia 81 | deserunt mollit anim id est laborum. 82 |

83 |
84 | 85 |
86 |
87 |
88 |

89 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 90 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 91 | veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 92 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate 93 | velit esse cillum dolore eu fugiat nulla pariatur. 94 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia 95 | deserunt mollit anim id est laborum. 96 |

97 |

98 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 99 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 100 | veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 101 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate 102 | velit esse cillum dolore eu fugiat nulla pariatur. 103 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia 104 | deserunt mollit anim id est laborum. 105 |

106 |
107 |
108 | 109 | 110 | -------------------------------------------------------------------------------- /src/mirror.js: -------------------------------------------------------------------------------- 1 | /* This code comes from THREE.js 2 | https://github.com/mrdoob/three.js/blob/dev/examples/jsm/objects/Reflector.js 3 | */ 4 | 5 | const Color = THREE.Color; 6 | const LinearFilter = THREE.LinearFilter; 7 | const MathUtils = THREE.MathUtils; 8 | const Matrix4 = THREE.Matrix4; 9 | const Mesh = THREE.Mesh; 10 | const PerspectiveCamera = THREE.PerspectiveCamera; 11 | const Plane = THREE.Plane; 12 | const RGBFormat = THREE.RGBFormat; 13 | const ShaderMaterial = THREE.ShaderMaterial; 14 | const UniformsUtils = THREE.UniformsUtils; 15 | const Vector3 = THREE.Vector3; 16 | const Vector4 = THREE.Vector4; 17 | const WebGLRenderTarget = THREE.WebGLRenderTarget; 18 | 19 | class Reflector extends Mesh { 20 | 21 | constructor( geometry, options = {} ) { 22 | 23 | super( geometry ); 24 | 25 | this.type = 'Reflector'; 26 | 27 | const scope = this; 28 | 29 | const color = ( options.color !== undefined ) ? new Color( options.color ) : new Color( 0x7F7F7F ); 30 | const textureWidth = options.textureWidth || 512; 31 | const textureHeight = options.textureHeight || 512; 32 | const clipBias = options.clipBias || 0; 33 | const shader = options.shader || Reflector.ReflectorShader; 34 | 35 | // 36 | 37 | const reflectorPlane = new Plane(); 38 | const normal = new Vector3(); 39 | const reflectorWorldPosition = new Vector3(); 40 | const cameraWorldPosition = new Vector3(); 41 | const rotationMatrix = new Matrix4(); 42 | const lookAtPosition = new Vector3( 0, 0, - 1 ); 43 | const clipPlane = new Vector4(); 44 | 45 | const view = new Vector3(); 46 | const target = new Vector3(); 47 | const q = new Vector4(); 48 | 49 | const textureMatrix = new Matrix4(); 50 | const virtualCamera = new PerspectiveCamera(); 51 | 52 | const parameters = { 53 | minFilter: LinearFilter, 54 | magFilter: LinearFilter, 55 | format: RGBFormat 56 | }; 57 | 58 | const renderTarget = new WebGLRenderTarget( textureWidth, textureHeight, parameters ); 59 | 60 | if ( ! MathUtils.isPowerOfTwo( textureWidth ) || ! MathUtils.isPowerOfTwo( textureHeight ) ) { 61 | 62 | renderTarget.texture.generateMipmaps = false; 63 | 64 | } 65 | 66 | const material = new ShaderMaterial( { 67 | uniforms: UniformsUtils.clone( shader.uniforms ), 68 | fragmentShader: shader.fragmentShader, 69 | vertexShader: shader.vertexShader 70 | } ); 71 | 72 | material.uniforms[ 'tDiffuse' ].value = renderTarget.texture; 73 | material.uniforms[ 'color' ].value = color; 74 | material.uniforms[ 'textureMatrix' ].value = textureMatrix; 75 | 76 | this.material = material; 77 | 78 | this.onBeforeRender = function ( renderer, scene, camera ) { 79 | 80 | reflectorWorldPosition.setFromMatrixPosition( scope.matrixWorld ); 81 | cameraWorldPosition.setFromMatrixPosition( camera.matrixWorld ); 82 | 83 | rotationMatrix.extractRotation( scope.matrixWorld ); 84 | 85 | normal.set( 0, 0, 1 ); 86 | normal.applyMatrix4( rotationMatrix ); 87 | 88 | view.subVectors( reflectorWorldPosition, cameraWorldPosition ); 89 | 90 | // Avoid rendering when reflector is facing away 91 | 92 | if ( view.dot( normal ) > 0 ) return; 93 | 94 | view.reflect( normal ).negate(); 95 | view.add( reflectorWorldPosition ); 96 | 97 | rotationMatrix.extractRotation( camera.matrixWorld ); 98 | 99 | lookAtPosition.set( 0, 0, - 1 ); 100 | lookAtPosition.applyMatrix4( rotationMatrix ); 101 | lookAtPosition.add( cameraWorldPosition ); 102 | 103 | target.subVectors( reflectorWorldPosition, lookAtPosition ); 104 | target.reflect( normal ).negate(); 105 | target.add( reflectorWorldPosition ); 106 | 107 | virtualCamera.position.copy( view ); 108 | virtualCamera.up.set( 0, 1, 0 ); 109 | virtualCamera.up.applyMatrix4( rotationMatrix ); 110 | virtualCamera.up.reflect( normal ); 111 | virtualCamera.lookAt( target ); 112 | 113 | virtualCamera.far = camera.far; // Used in WebGLBackground 114 | 115 | virtualCamera.updateMatrixWorld(); 116 | virtualCamera.projectionMatrix.copy( camera.projectionMatrix ); 117 | 118 | // Update the texture matrix 119 | textureMatrix.set( 120 | 0.5, 0.0, 0.0, 0.5, 121 | 0.0, 0.5, 0.0, 0.5, 122 | 0.0, 0.0, 0.5, 0.5, 123 | 0.0, 0.0, 0.0, 1.0 124 | ); 125 | textureMatrix.multiply( virtualCamera.projectionMatrix ); 126 | textureMatrix.multiply( virtualCamera.matrixWorldInverse ); 127 | textureMatrix.multiply( scope.matrixWorld ); 128 | 129 | // Now update projection matrix with new clip plane, implementing code from: http://www.terathon.com/code/oblique.html 130 | // Paper explaining this technique: http://www.terathon.com/lengyel/Lengyel-Oblique.pdf 131 | reflectorPlane.setFromNormalAndCoplanarPoint( normal, reflectorWorldPosition ); 132 | reflectorPlane.applyMatrix4( virtualCamera.matrixWorldInverse ); 133 | 134 | clipPlane.set( reflectorPlane.normal.x, reflectorPlane.normal.y, reflectorPlane.normal.z, reflectorPlane.constant ); 135 | 136 | const projectionMatrix = virtualCamera.projectionMatrix; 137 | 138 | q.x = ( Math.sign( clipPlane.x ) + projectionMatrix.elements[ 8 ] ) / projectionMatrix.elements[ 0 ]; 139 | q.y = ( Math.sign( clipPlane.y ) + projectionMatrix.elements[ 9 ] ) / projectionMatrix.elements[ 5 ]; 140 | q.z = - 1.0; 141 | q.w = ( 1.0 + projectionMatrix.elements[ 10 ] ) / projectionMatrix.elements[ 14 ]; 142 | 143 | // Calculate the scaled plane vector 144 | clipPlane.multiplyScalar( 2.0 / clipPlane.dot( q ) ); 145 | 146 | // Replacing the third row of the projection matrix 147 | projectionMatrix.elements[ 2 ] = clipPlane.x; 148 | projectionMatrix.elements[ 6 ] = clipPlane.y; 149 | projectionMatrix.elements[ 10 ] = clipPlane.z + 1.0 - clipBias; 150 | projectionMatrix.elements[ 14 ] = clipPlane.w; 151 | 152 | // Render 153 | 154 | renderTarget.texture.encoding = renderer.outputEncoding; 155 | 156 | scope.visible = false; 157 | 158 | const currentRenderTarget = renderer.getRenderTarget(); 159 | 160 | const currentXrEnabled = renderer.xr.enabled; 161 | const currentShadowAutoUpdate = renderer.shadowMap.autoUpdate; 162 | 163 | renderer.xr.enabled = false; // Avoid camera modification 164 | renderer.shadowMap.autoUpdate = false; // Avoid re-computing shadows 165 | 166 | renderer.setRenderTarget( renderTarget ); 167 | 168 | renderer.state.buffers.depth.setMask( true ); // make sure the depth buffer is writable so it can be properly cleared, see #18897 169 | 170 | if ( renderer.autoClear === false ) renderer.clear(); 171 | renderer.render( scene, virtualCamera ); 172 | 173 | renderer.xr.enabled = currentXrEnabled; 174 | renderer.shadowMap.autoUpdate = currentShadowAutoUpdate; 175 | 176 | renderer.setRenderTarget( currentRenderTarget ); 177 | 178 | // Restore viewport 179 | 180 | const viewport = camera.viewport; 181 | 182 | if ( viewport !== undefined ) { 183 | 184 | renderer.state.viewport( viewport ); 185 | 186 | } 187 | 188 | scope.visible = true; 189 | 190 | }; 191 | 192 | this.getRenderTarget = function () { 193 | 194 | return renderTarget; 195 | 196 | }; 197 | 198 | this.dispose = function () { 199 | 200 | renderTarget.dispose(); 201 | scope.material.dispose(); 202 | 203 | }; 204 | 205 | } 206 | 207 | } 208 | 209 | Reflector.prototype.isReflector = true; 210 | 211 | Reflector.ReflectorShader = { 212 | 213 | uniforms: { 214 | 215 | 'color': { 216 | value: null 217 | }, 218 | 219 | 'tDiffuse': { 220 | value: null 221 | }, 222 | 223 | 'textureMatrix': { 224 | value: null 225 | } 226 | 227 | }, 228 | 229 | vertexShader: /* glsl */` 230 | uniform mat4 textureMatrix; 231 | varying vec4 vUv; 232 | 233 | #include 234 | #include 235 | 236 | void main() { 237 | 238 | vUv = textureMatrix * vec4( position, 1.0 ); 239 | 240 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 241 | 242 | #include 243 | 244 | }`, 245 | 246 | fragmentShader: /* glsl */` 247 | uniform vec3 color; 248 | uniform sampler2D tDiffuse; 249 | varying vec4 vUv; 250 | 251 | #include 252 | 253 | float blendOverlay( float base, float blend ) { 254 | 255 | return( base < 0.5 ? ( 2.0 * base * blend ) : ( 1.0 - 2.0 * ( 1.0 - base ) * ( 1.0 - blend ) ) ); 256 | 257 | } 258 | 259 | vec3 blendOverlay( vec3 base, vec3 blend ) { 260 | 261 | return vec3( blendOverlay( base.r, blend.r ), blendOverlay( base.g, blend.g ), blendOverlay( base.b, blend.b ) ); 262 | 263 | } 264 | 265 | void main() { 266 | 267 | #include 268 | 269 | vec4 base = texture2DProj( tDiffuse, vUv ); 270 | gl_FragColor = vec4( blendOverlay( base.rgb, color ), 1.0 ); 271 | 272 | }` 273 | }; 274 | 275 | 276 | /* Add this component to an to turn it into a mirror 277 | */ 278 | 279 | AFRAME.registerComponent('mirror', { 280 | schema: { 281 | quality: {type: 'string', oneOf: ['high, low'], default: 'high'} 282 | }, 283 | 284 | init() { 285 | 286 | this.el.addEventListener("loaded", () => { 287 | 288 | function nearestPowerOf2(n) { 289 | return 1 << 31 - Math.clz32(n); 290 | } 291 | 292 | // 2 * nearest power of 2 gives a nice look, but at a perf cost. 293 | const factor = (this.data.quality === 'high') ? 2 : 1; 294 | const geometry = this.el.getObject3D('mesh').geometry; 295 | 296 | const mirror = new Reflector( geometry, { 297 | textureWidth: factor * nearestPowerOf2(window.innerWidth * window.devicePixelRatio), 298 | textureHeight: factor * nearestPowerOf2(window.innerHeight * window.devicePixelRatio) 299 | } ); 300 | 301 | // make sure base object material does not interfere with mirror. 302 | // this.el.getObject3D('mesh').material.side = THREE.FrontSide; 303 | // needs work - how to get mirror back to show? 304 | //this.el.getObject3D('mesh').material.opacity = 0; 305 | 306 | mirror.position.z = 0.01; 307 | this.el.object3D.add(mirror); 308 | //this.el.object3D.getWorldPosition(mirror.position); 309 | //this.el.sceneEl.object3D.add(mirror); 310 | }); 311 | } 312 | 313 | }); 314 | -------------------------------------------------------------------------------- /src/multi-camera.js: -------------------------------------------------------------------------------- 1 | /* System that supports capture of the the main A-Frame render() call 2 | by add-render-call */ 3 | AFRAME.registerSystem('add-render-call', { 4 | 5 | init() { 6 | 7 | this.render = this.render.bind(this); 8 | this.originalRender = this.el.sceneEl.renderer.render; 9 | this.el.sceneEl.renderer.render = this.render; 10 | this.el.sceneEl.renderer.autoClear = false; 11 | 12 | this.preRenderCalls = []; 13 | this.postRenderCalls = []; 14 | this.suppresssDefaultRenderCount = 0; 15 | }, 16 | 17 | addPreRenderCall(render) { 18 | this.preRenderCalls.push(render) 19 | }, 20 | 21 | removePreRenderCall(render) { 22 | const index = this.preRenderCalls.indexOf(render); 23 | if (index > -1) { 24 | this.preRenderCalls.splice(index, 1); 25 | } 26 | }, 27 | 28 | addPostRenderCall(render) { 29 | this.postRenderCalls.push(render) 30 | }, 31 | 32 | removePostRenderCall(render) { 33 | const index = this.postRenderCalls.indexOf(render); 34 | if (index > -1) { 35 | this.postRenderCalls.splice(index, 1); 36 | } 37 | else { 38 | console.warn("Unexpected failure to remove render call") 39 | } 40 | }, 41 | 42 | suppressOriginalRender() { 43 | this.suppresssDefaultRenderCount++; 44 | }, 45 | 46 | unsuppressOriginalRender() { 47 | this.suppresssDefaultRenderCount--; 48 | 49 | if (this.suppresssDefaultRenderCount < 0) { 50 | console.warn("Unexpected unsuppression of original render") 51 | this.suppresssDefaultRenderCount = 0; 52 | } 53 | }, 54 | 55 | render(scene, camera) { 56 | 57 | const renderer = this.el.sceneEl.renderer 58 | 59 | if (scene !== this.el.sceneEl.object3D || 60 | camera != this.el.sceneEl.camera) { 61 | // Render call is for a different scene (e.g. generating a texture from a cubemap) 62 | // or not the main camera. 63 | // Don't apply any pre- or post-render calls, just let the rendere function as 64 | // normal. 65 | this.originalRender.call(renderer, scene, camera) 66 | return 67 | } 68 | 69 | // set up THREE.js stats to correctly count across all render calls. 70 | renderer.info.autoReset = false; 71 | renderer.info.reset(); 72 | 73 | this.preRenderCalls.forEach((f) => f()); 74 | 75 | if (this.suppresssDefaultRenderCount <= 0) { 76 | this.originalRender.call(renderer, scene, camera) 77 | } 78 | 79 | this.postRenderCalls.forEach((f) => f()); 80 | } 81 | }); 82 | 83 | /* Component that captures the main A-Frame render() call 84 | and adds an additional render call. 85 | Must specify an entity and component that expose a function call render(). */ 86 | AFRAME.registerComponent('add-render-call', { 87 | 88 | multiple: true, 89 | 90 | schema: { 91 | entity: {type: 'selector'}, 92 | componentName: {type: 'string'}, 93 | sequence: {type: 'string', oneOf: ['before', 'after', 'replace'], default: 'after'} 94 | }, 95 | 96 | init() { 97 | 98 | this.invokeRender = this.invokeRender.bind(this); 99 | 100 | }, 101 | 102 | update(oldData) { 103 | 104 | // first clean up any old settings. 105 | this.removeSettings(oldData) 106 | 107 | // now add new settings. 108 | if (this.data.sequence === "before") { 109 | this.system.addPreRenderCall(this.invokeRender) 110 | } 111 | 112 | if (this.data.sequence === "replace") { 113 | this.system.suppressOriginalRender() 114 | } 115 | 116 | if (this.data.sequence === "after" || 117 | this.data.sequence === "replace") 118 | { 119 | this.system.addPostRenderCall(this.invokeRender) 120 | } 121 | }, 122 | 123 | remove() { 124 | this.removeSettings(this.data) 125 | }, 126 | 127 | removeSettings(data) { 128 | if (data.sequence === "before") { 129 | this.system.removePreRenderCall(this.invokeRender) 130 | } 131 | 132 | if (data.sequence === "replace") { 133 | this.system.unsuppressOriginalRender() 134 | } 135 | 136 | if (data.sequence === "after" || 137 | data.sequence === "replace") 138 | { 139 | this.system.removePostRenderCall(this.invokeRender) 140 | } 141 | }, 142 | 143 | invokeRender() 144 | { 145 | const componentName = this.data.componentName; 146 | if ((this.data.entity) && 147 | (this.data.entity.components[componentName])) { 148 | this.data.entity.components[componentName].render(this.el.sceneEl.renderer, this.system.originalRender); 149 | } 150 | } 151 | }); 152 | 153 | /* Component to set layers via HTML attribute. */ 154 | AFRAME.registerComponent('layers', { 155 | schema : {type: 'number', default: 0}, 156 | 157 | init: function() { 158 | 159 | setObjectLayer = function(object, layer) { 160 | if (!object.el || 161 | !object.el.hasAttribute('keep-default-layer')) { 162 | object.layers.set(layer); 163 | } 164 | object.children.forEach(o => setObjectLayer(o, layer)); 165 | } 166 | 167 | this.el.addEventListener("loaded", () => { 168 | setObjectLayer(this.el.object3D, this.data); 169 | }); 170 | 171 | if (this.el.hasAttribute('text')) { 172 | this.el.addEventListener("textfontset", () => { 173 | setObjectLayer(this.el.object3D, this.data); 174 | }); 175 | } 176 | } 177 | }); 178 | 179 | /* This component has code in common with viewpoint-selector-renderer 180 | However it's a completely generic stripped-down version, which 181 | just delivers the 2nd camera function. 182 | i.e. it is missing: 183 | - The positioning of the viewpoint-selector entity. 184 | - The cursor / raycaster elements. 185 | */ 186 | 187 | AFRAME.registerComponent('secondary-camera', { 188 | schema: { 189 | output: {type: 'string', oneOf: ['screen', 'scene', 'plane'], default: 'screen'}, //'plane' is there for backwards compatibility 190 | outputElement: {type: 'selector'}, 191 | cameraType: {type: 'string', oneOf: ['perspective, orthographic'], default: 'perspective'}, 192 | sequence: {type: 'string', oneOf: ['before', 'after', 'replace'], default: 'after'}, 193 | quality: {type: 'string', oneOf: ['high, low'], default: 'high'}, 194 | aspectRatio: {type: 'string', default: 'auto'} 195 | }, 196 | 197 | init() { 198 | 199 | if (!this.el.id) { 200 | console.error("No id specified on entity. secondary-camera only works on entities with an id") 201 | } 202 | 203 | this.savedViewport = new THREE.Vector4(); 204 | this.savedScissorTest = false; 205 | this.savedScissor = new THREE.Vector4(); 206 | this.outputRectangle = new THREE.Vector4(); 207 | this.sceneInfo = this.prepareScene(); 208 | this.activeRenderTarget = 0; 209 | if (this.data.aspectRatio !== 'auto' && isNaN(parseFloat(this.data.aspectRatio))) { 210 | console.error("aspectRatio must be a number or 'auto'"); 211 | } 212 | this.aspectRatio = (this.data.aspectRatio === 'auto') ? 'auto' : parseFloat(this.data.aspectRatio); 213 | 214 | // add the render call to the scene 215 | this.el.sceneEl.setAttribute(`add-render-call__${this.el.id}`, 216 | {entity: `#${this.el.id}`, 217 | componentName: "secondary-camera", 218 | sequence: this.data.sequence}); 219 | 220 | // if there is a cursor on this entity, set it up to read this camera. 221 | if (this.el.hasAttribute('cursor')) { 222 | this.el.setAttribute("cursor", "canvas: user; camera: user"); 223 | 224 | this.el.addEventListener('loaded', () => { 225 | this.el.components['raycaster'].raycaster.layers.mask = this.el.object3D.layers.mask; 226 | 227 | const cursor = this.el.components['cursor']; 228 | cursor.removeEventListeners(); 229 | cursor.camera = this.camera; 230 | cursor.canvas = this.data.outputElement; 231 | cursor.canvasBounds = cursor.canvas.getBoundingClientRect(); 232 | cursor.addEventListeners(); 233 | cursor.updateMouseEventListeners(); 234 | }); 235 | } 236 | 237 | if (this.data.output !== 'screen') { 238 | 239 | if (this.data.output === 'plane') { 240 | console.warn("schema warning for secondary-camera component: output:scene is deprecated. Please use output:scene instead.") 241 | } 242 | 243 | if (!this.data.outputElement.hasLoaded) { 244 | this.data.outputElement.addEventListener("loaded", () => { 245 | this.configureCamera() 246 | }); 247 | } else { 248 | this.configureCamera() 249 | } 250 | } 251 | }, 252 | 253 | configureCamera() { 254 | const object = this.data.outputElement.getObject3D('mesh'); 255 | function nearestPowerOf2(n) { 256 | return 1 << 31 - Math.clz32(n); 257 | } 258 | // 2 * nearest power of 2 gives a nice look, but at a perf cost. 259 | const factor = (this.data.quality === 'high') ? 2 : 1; 260 | 261 | const width = factor * nearestPowerOf2(window.innerWidth * window.devicePixelRatio); 262 | const height = factor * nearestPowerOf2(window.innerHeight * window.devicePixelRatio); 263 | 264 | function newRenderTarget() { 265 | const target = new THREE.WebGLRenderTarget(width, 266 | height, 267 | { 268 | minFilter: THREE.LinearFilter, 269 | magFilter: THREE.LinearFilter, 270 | stencilBuffer: false, 271 | generateMipmaps: false 272 | }); 273 | 274 | return target; 275 | } 276 | // We use 2 render targets, and alternate each frame, so that we are 277 | // never rendering to a target that is actually in front of the camera. 278 | this.renderTargets = [newRenderTarget(), 279 | newRenderTarget()] 280 | 281 | if (this.aspectRatio === 'auto') { 282 | if (object.geometry.parameters.width && object.geometry.parameters.height) { 283 | this.camera.aspect = object.geometry.parameters.width / 284 | object.geometry.parameters.height; 285 | } 286 | else { 287 | this.camera.aspect = 1; 288 | } 289 | } else { 290 | this.camera.aspect = this.aspectRatio; 291 | } 292 | }, 293 | 294 | remove() { 295 | 296 | this.el.sceneEl.removeAttribute(`add-render-call__${this.el.id}`); 297 | if (this.renderTargets) { 298 | this.renderTargets[0].dispose(); 299 | this.renderTargets[1].dispose(); 300 | } 301 | 302 | // "Remove" code does not tidy up adjustments made to cursor component. 303 | // rarely necessary as cursor is typically put in place at the same time 304 | // as the secondary camera, and so will be disposed of at the same time. 305 | }, 306 | 307 | prepareScene() { 308 | this.scene = this.el.sceneEl.object3D; 309 | 310 | const width = 2; 311 | const height = 2; 312 | 313 | if (this.data.cameraType === "orthographic") { 314 | this.camera = new THREE.OrthographicCamera( width / - 2, width / 2, height / 2, height / - 2, 1, 1000 ); 315 | } 316 | else { 317 | this.camera = new THREE.PerspectiveCamera( 45, width / height, 1, 1000); 318 | } 319 | 320 | this.scene.add(this.camera); 321 | return; 322 | }, 323 | 324 | render(renderer, renderFunction) { 325 | 326 | // don't bother rendering to screen in VR mode. 327 | if (this.data.output === "screen" && this.el.sceneEl.is('vr-mode')) return; 328 | 329 | var elemRect; 330 | 331 | if (this.data.output === "screen") { 332 | const elem = this.data.outputElement; 333 | 334 | // get the viewport relative position of this element 335 | elemRect = elem.getBoundingClientRect(); 336 | this.camera.aspect = elemRect.width / elemRect.height; 337 | } 338 | 339 | // Camera position & layers match this entity. 340 | this.el.object3D.getWorldPosition(this.camera.position); 341 | this.el.object3D.getWorldQuaternion(this.camera.quaternion); 342 | this.camera.layers.mask = this.el.object3D.layers.mask; 343 | 344 | this.camera.updateProjectionMatrix(); 345 | 346 | if (this.data.output === "screen") { 347 | // "bottom" position is relative to the whole viewport, not just the canvas. 348 | // We need to turn this into a distance from the bottom of the canvas. 349 | // We need to consider the header bar above the canvas, and the size of the canvas. 350 | const mainRect = renderer.domElement.getBoundingClientRect(); 351 | 352 | renderer.getViewport(this.savedViewport); 353 | this.savedScissorTest = renderer.getScissorTest(); 354 | renderer.getScissor(this.savedScissor); 355 | 356 | this.outputRectangle.set(elemRect.left - mainRect.left, 357 | mainRect.bottom - elemRect.bottom, 358 | elemRect.width, 359 | elemRect.height); 360 | renderer.setViewport(this.outputRectangle); 361 | renderer.setScissorTest(true); 362 | renderer.setScissor(this.outputRectangle); 363 | 364 | renderFunction.call(renderer, this.scene, this.camera); 365 | renderer.setViewport(this.savedViewport); 366 | renderer.setScissorTest(this.savedScissorTest); 367 | renderer.setScissor(this.savedScissor); 368 | } 369 | else { 370 | // target === "plane" 371 | 372 | // store off current renderer properties so that they can be restored. 373 | const currentRenderTarget = renderer.getRenderTarget(); 374 | const currentXrEnabled = renderer.xr.enabled; 375 | const currentShadowAutoUpdate = renderer.shadowMap.autoUpdate; 376 | 377 | // temporarily override renderer properties for rendering to a texture. 378 | renderer.xr.enabled = false; // Avoid camera modification 379 | renderer.shadowMap.autoUpdate = false; // Avoid re-computing shadows 380 | 381 | const renderTarget = this.renderTargets[this.activeRenderTarget]; 382 | renderTarget.texture.colorSpace = renderer.outputColorSpace; 383 | renderer.setRenderTarget(renderTarget); 384 | renderer.state.buffers.depth.setMask( true ); // make sure the depth buffer is writable so it can be properly cleared, see #18897 385 | renderer.clear(); 386 | 387 | renderFunction.call(renderer, this.scene, this.camera); 388 | 389 | this.data.outputElement.getObject3D('mesh').material.map = renderTarget.texture; 390 | 391 | // restore original renderer settings. 392 | renderer.setRenderTarget(currentRenderTarget); 393 | renderer.xr.enabled = currentXrEnabled; 394 | renderer.shadowMap.autoUpdate = currentShadowAutoUpdate; 395 | 396 | this.activeRenderTarget = 1 - this.activeRenderTarget; 397 | } 398 | } 399 | }); 400 | -------------------------------------------------------------------------------- /aframe/cursor.js: -------------------------------------------------------------------------------- 1 | /* Replacement cursor.js component 2 | For use with A-Frame until PR-4983 is released: https://github.com/aframevr/aframe/pull/4983 3 | Based on the following version of A-Frame cursor.js 4 | https://github.com/aframevr/aframe/blob/25f09c11f84a60f5c078b112154688ccbadf20ff/src/components/cursor.js 5 | 6 | Use as follows: 7 | 8 | 9 | */ 10 | 11 | const utils = AFRAME.utils; 12 | const bind = utils.bind; 13 | 14 | var EVENTS = { 15 | CLICK: 'click', 16 | FUSING: 'fusing', 17 | MOUSEENTER: 'mouseenter', 18 | MOUSEDOWN: 'mousedown', 19 | MOUSELEAVE: 'mouseleave', 20 | MOUSEUP: 'mouseup' 21 | }; 22 | 23 | var STATES = { 24 | FUSING: 'cursor-fusing', 25 | HOVERING: 'cursor-hovering', 26 | HOVERED: 'cursor-hovered' 27 | }; 28 | 29 | var CANVAS_EVENTS = { 30 | DOWN: ['mousedown', 'touchstart'], 31 | UP: ['mouseup', 'touchend'] 32 | }; 33 | 34 | var WEBXR_EVENTS = { 35 | DOWN: ['selectstart'], 36 | UP: ['selectend'] 37 | }; 38 | 39 | var CANVAS_HOVER_CLASS = 'a-mouse-cursor-hover'; 40 | 41 | /** 42 | * Cursor component. Applies the raycaster component specifically for starting the raycaster 43 | * from the camera and pointing from camera's facing direction, and then only returning the 44 | * closest intersection. Cursor can be fine-tuned by setting raycaster properties. 45 | * 46 | * @member {object} fuseTimeout - Timeout to trigger fuse-click. 47 | * @member {Element} cursorDownEl - Entity that was last mousedowned during current click. 48 | * @member {object} intersection - Attributes of the current intersection event, including 49 | * 3D- and 2D-space coordinates. See: http://threejs.org/docs/api/core/Raycaster.html 50 | * @member {Element} intersectedEl - Currently-intersected entity. Used to keep track to 51 | * emit events when unintersecting. 52 | */ 53 | delete AFRAME.components['cursor'] 54 | AFRAME.registerComponent('cursor', { 55 | dependencies: ['raycaster'], 56 | 57 | schema: { 58 | downEvents: {default: []}, 59 | fuse: {default: utils.device.isMobile()}, 60 | fuseTimeout: {default: 1500, min: 0}, 61 | mouseCursorStylesEnabled: {default: true}, 62 | upEvents: {default: []}, 63 | rayOrigin: {default: 'entity', oneOf: ['mouse', 'entity']}, 64 | canvas: {default: 'auto', oneOf: ['auto', 'user']}, 65 | camera: {default: 'auto', oneOf: ['auto', 'user']} 66 | }, 67 | 68 | init: function () { 69 | var self = this; 70 | 71 | this.fuseTimeout = undefined; 72 | this.cursorDownEl = null; 73 | this.intersectedEl = null; 74 | this.canvasBounds = document.body.getBoundingClientRect(); 75 | this.isCursorDown = false; 76 | 77 | // expose camera and cursor to user if required. 78 | if (this.data.camera === 'user') { 79 | this.camera = this.el.sceneEl.camera; 80 | } 81 | if (this.data.canvas === 'user') { 82 | this.canvas = this.el.sceneEl.canvas; 83 | this.canvasBounds = this.canvas.getBoundingClientRect(); 84 | } 85 | 86 | // Debounce. 87 | this.updateCanvasBounds = utils.debounce(function updateCanvasBounds () { 88 | self.canvasBounds = self.getCanvas().getBoundingClientRect(); 89 | }, 500); 90 | 91 | this.eventDetail = {}; 92 | this.intersectedEventDetail = {cursorEl: this.el}; 93 | 94 | // Bind methods. 95 | this.onCursorDown = bind(this.onCursorDown, this); 96 | this.onCursorUp = bind(this.onCursorUp, this); 97 | this.onIntersection = bind(this.onIntersection, this); 98 | this.onIntersectionCleared = bind(this.onIntersectionCleared, this); 99 | this.onMouseMove = bind(this.onMouseMove, this); 100 | this.onEnterVR = bind(this.onEnterVR, this); 101 | 102 | // Variables used in raycasting. One set of these needed per-cursor 103 | // instance. 104 | this.direction = new THREE.Vector3(); 105 | this.origin = new THREE.Vector3(); 106 | this.rayCasterConfig = {origin: this.origin, direction: this.direction}; 107 | }, 108 | 109 | update: function (oldData) { 110 | if (this.data.rayOrigin === oldData.rayOrigin) { return; } 111 | this.updateMouseEventListeners(); 112 | }, 113 | 114 | play: function () { 115 | this.addEventListeners(); 116 | }, 117 | 118 | pause: function () { 119 | this.removeEventListeners(); 120 | }, 121 | 122 | remove: function () { 123 | var el = this.el; 124 | el.removeState(STATES.HOVERING); 125 | el.removeState(STATES.FUSING); 126 | clearTimeout(this.fuseTimeout); 127 | if (this.intersectedEl) { this.intersectedEl.removeState(STATES.HOVERED); } 128 | this.removeEventListeners(); 129 | }, 130 | 131 | addEventListeners: function () { 132 | var canvas; 133 | var data = this.data; 134 | var el = this.el; 135 | var self = this; 136 | 137 | const addCanvasListeners = () => { 138 | canvas = this.getCanvas(); 139 | if (data.downEvents.length || data.upEvents.length) { return; } 140 | CANVAS_EVENTS.DOWN.forEach(function (downEvent) { 141 | canvas.addEventListener(downEvent, self.onCursorDown); 142 | }); 143 | CANVAS_EVENTS.UP.forEach(function (upEvent) { 144 | canvas.addEventListener(upEvent, self.onCursorUp); 145 | }); 146 | }; 147 | 148 | canvas = this.getCanvas(); 149 | if (canvas) { 150 | addCanvasListeners(); 151 | } else { 152 | el.sceneEl.addEventListener('render-target-loaded', addCanvasListeners); 153 | } 154 | 155 | data.downEvents.forEach(function (downEvent) { 156 | el.addEventListener(downEvent, self.onCursorDown); 157 | }); 158 | data.upEvents.forEach(function (upEvent) { 159 | el.addEventListener(upEvent, self.onCursorUp); 160 | }); 161 | el.addEventListener('raycaster-intersection', this.onIntersection); 162 | el.addEventListener('raycaster-closest-entity-changed', this.onIntersection); 163 | 164 | el.addEventListener('raycaster-intersection-cleared', this.onIntersectionCleared); 165 | 166 | el.sceneEl.addEventListener('rendererresize', this.updateCanvasBounds); 167 | el.sceneEl.addEventListener('enter-vr', this.onEnterVR); 168 | window.addEventListener('resize', this.updateCanvasBounds); 169 | window.addEventListener('scroll', this.updateCanvasBounds); 170 | 171 | this.updateMouseEventListeners(); 172 | }, 173 | 174 | removeEventListeners: function () { 175 | var canvas; 176 | var data = this.data; 177 | var el = this.el; 178 | var self = this; 179 | 180 | canvas = this.getCanvas(); 181 | if (canvas && !data.downEvents.length && !data.upEvents.length) { 182 | CANVAS_EVENTS.DOWN.forEach(function (downEvent) { 183 | canvas.removeEventListener(downEvent, self.onCursorDown); 184 | }); 185 | CANVAS_EVENTS.UP.forEach(function (upEvent) { 186 | canvas.removeEventListener(upEvent, self.onCursorUp); 187 | }); 188 | } 189 | 190 | data.downEvents.forEach(function (downEvent) { 191 | el.removeEventListener(downEvent, self.onCursorDown); 192 | }); 193 | data.upEvents.forEach(function (upEvent) { 194 | el.removeEventListener(upEvent, self.onCursorUp); 195 | }); 196 | el.removeEventListener('raycaster-intersection', this.onIntersection); 197 | el.removeEventListener('raycaster-intersection-cleared', this.onIntersectionCleared); 198 | canvas.removeEventListener('mousemove', this.onMouseMove); 199 | canvas.removeEventListener('touchstart', this.onMouseMove); 200 | canvas.removeEventListener('touchmove', this.onMouseMove); 201 | 202 | el.sceneEl.removeEventListener('rendererresize', this.updateCanvasBounds); 203 | el.sceneEl.removeEventListener('enter-vr', this.onEnterVR); 204 | window.removeEventListener('resize', this.updateCanvasBounds); 205 | window.removeEventListener('scroll', this.updateCanvasBounds); 206 | }, 207 | 208 | updateMouseEventListeners: function () { 209 | var canvas; 210 | var el = this.el; 211 | 212 | canvas = this.getCanvas(); 213 | canvas.removeEventListener('mousemove', this.onMouseMove); 214 | canvas.removeEventListener('touchmove', this.onMouseMove); 215 | el.setAttribute('raycaster', 'useWorldCoordinates', false); 216 | if (this.data.rayOrigin !== 'mouse') { return; } 217 | canvas.addEventListener('mousemove', this.onMouseMove, false); 218 | canvas.addEventListener('touchmove', this.onMouseMove, false); 219 | el.setAttribute('raycaster', 'useWorldCoordinates', true); 220 | this.updateCanvasBounds(); 221 | }, 222 | 223 | onMouseMove: (function () { 224 | var mouse = new THREE.Vector2(); 225 | 226 | return function (evt) { 227 | var bounds = this.canvasBounds; 228 | var camera = this.getCamera(); 229 | var left; 230 | var point; 231 | var top; 232 | 233 | camera.parent.updateMatrixWorld(); 234 | 235 | // Calculate mouse position based on the canvas element 236 | if (evt.type === 'touchmove' || evt.type === 'touchstart') { 237 | // Track the first touch for simplicity. 238 | point = evt.touches.item(0); 239 | } else { 240 | point = evt; 241 | } 242 | 243 | left = point.clientX - bounds.left; 244 | top = point.clientY - bounds.top; 245 | mouse.x = (left / bounds.width) * 2 - 1; 246 | mouse.y = -(top / bounds.height) * 2 + 1; 247 | 248 | if (camera && camera.isPerspectiveCamera) { 249 | this.origin.setFromMatrixPosition(camera.matrixWorld); 250 | this.direction.set(mouse.x, mouse.y, 0.5).unproject(camera).sub(this.origin).normalize(); 251 | } else if (camera && camera.isOrthographicCamera) { 252 | this.origin.set(mouse.x, mouse.y, (camera.near + camera.far) / (camera.near - camera.far)).unproject(camera); // set origin in plane of camera 253 | this.direction.set(0, 0, -1).transformDirection(camera.matrixWorld); 254 | } else { 255 | console.error('AFRAME.Raycaster: Unsupported camera type: ' + camera.type); 256 | } 257 | 258 | this.el.setAttribute('raycaster', this.rayCasterConfig); 259 | if (evt.type === 'touchmove') { evt.preventDefault(); } 260 | }; 261 | })(), 262 | 263 | /** 264 | * Trigger mousedown and keep track of the mousedowned entity. 265 | */ 266 | onCursorDown: function (evt) { 267 | this.isCursorDown = true; 268 | // Raycast again for touch. 269 | if (this.data.rayOrigin === 'mouse' && evt.type === 'touchstart') { 270 | this.onMouseMove(evt); 271 | this.el.components.raycaster.checkIntersections(); 272 | evt.preventDefault(); 273 | } 274 | 275 | this.twoWayEmit(EVENTS.MOUSEDOWN); 276 | this.cursorDownEl = this.intersectedEl; 277 | }, 278 | 279 | /** 280 | * Trigger mouseup if: 281 | * - Not fusing (mobile has no mouse). 282 | * - Currently intersecting an entity. 283 | * - Currently-intersected entity is the same as the one when mousedown was triggered, 284 | * in case user mousedowned one entity, dragged to another, and mouseupped. 285 | */ 286 | onCursorUp: function (evt) { 287 | if (!this.isCursorDown) { return; } 288 | 289 | this.isCursorDown = false; 290 | 291 | var data = this.data; 292 | this.twoWayEmit(EVENTS.MOUSEUP); 293 | 294 | // If intersected entity has changed since the cursorDown, still emit mouseUp on the 295 | // previously cursorUp entity. 296 | if (this.cursorDownEl && this.cursorDownEl !== this.intersectedEl) { 297 | this.intersectedEventDetail.intersection = null; 298 | this.cursorDownEl.emit(EVENTS.MOUSEUP, this.intersectedEventDetail); 299 | } 300 | 301 | if ((!data.fuse || data.rayOrigin === 'mouse') && 302 | this.intersectedEl && this.cursorDownEl === this.intersectedEl) { 303 | this.twoWayEmit(EVENTS.CLICK); 304 | } 305 | 306 | this.cursorDownEl = null; 307 | if (evt.type === 'touchend') { evt.preventDefault(); } 308 | }, 309 | 310 | /** 311 | * Handle intersection. 312 | */ 313 | onIntersection: function (evt) { 314 | var currentIntersection; 315 | var cursorEl = this.el; 316 | var index; 317 | var intersectedEl; 318 | var intersection; 319 | 320 | // Select closest object, excluding the cursor. 321 | index = evt.detail.els[0] === cursorEl ? 1 : 0; 322 | intersection = evt.detail.intersections[index]; 323 | intersectedEl = evt.detail.els[index]; 324 | 325 | // If cursor is the only intersected object, ignore the event. 326 | if (!intersectedEl) { return; } 327 | 328 | // Already intersecting this entity. 329 | if (this.intersectedEl === intersectedEl) { return; } 330 | 331 | // Ignore events further away than active intersection. 332 | if (this.intersectedEl) { 333 | currentIntersection = this.el.components.raycaster.getIntersection(this.intersectedEl); 334 | if (currentIntersection && currentIntersection.distance <= intersection.distance) { return; } 335 | } 336 | 337 | // Unset current intersection. 338 | this.clearCurrentIntersection(true); 339 | 340 | this.setIntersection(intersectedEl, intersection); 341 | }, 342 | 343 | /** 344 | * Handle intersection cleared. 345 | */ 346 | onIntersectionCleared: function (evt) { 347 | var clearedEls = evt.detail.clearedEls; 348 | // Check if the current intersection has ended 349 | if (clearedEls.indexOf(this.intersectedEl) === -1) { return; } 350 | this.clearCurrentIntersection(); 351 | }, 352 | 353 | onEnterVR: function () { 354 | this.clearCurrentIntersection(true); 355 | var xrSession = this.el.sceneEl.xrSession; 356 | var self = this; 357 | if (!xrSession) { return; } 358 | if (this.data.rayOrigin === 'mouse') { return; } 359 | WEBXR_EVENTS.DOWN.forEach(function (downEvent) { 360 | xrSession.addEventListener(downEvent, self.onCursorDown); 361 | }); 362 | WEBXR_EVENTS.UP.forEach(function (upEvent) { 363 | xrSession.addEventListener(upEvent, self.onCursorUp); 364 | }); 365 | }, 366 | 367 | setIntersection: function (intersectedEl, intersection) { 368 | var cursorEl = this.el; 369 | var data = this.data; 370 | var self = this; 371 | 372 | // Already intersecting. 373 | if (this.intersectedEl === intersectedEl) { return; } 374 | 375 | // Set new intersection. 376 | this.intersectedEl = intersectedEl; 377 | 378 | // Hovering. 379 | cursorEl.addState(STATES.HOVERING); 380 | intersectedEl.addState(STATES.HOVERED); 381 | this.twoWayEmit(EVENTS.MOUSEENTER); 382 | 383 | if (this.data.mouseCursorStylesEnabled && this.data.rayOrigin === 'mouse') { 384 | this.getCanvas().classList.add(CANVAS_HOVER_CLASS); 385 | } 386 | 387 | // Begin fuse if necessary. 388 | if (data.fuseTimeout === 0 || !data.fuse) { return; } 389 | cursorEl.addState(STATES.FUSING); 390 | this.twoWayEmit(EVENTS.FUSING); 391 | this.fuseTimeout = setTimeout(function fuse () { 392 | cursorEl.removeState(STATES.FUSING); 393 | self.twoWayEmit(EVENTS.CLICK); 394 | }, data.fuseTimeout); 395 | }, 396 | 397 | clearCurrentIntersection: function (ignoreRemaining) { 398 | var index; 399 | var intersection; 400 | var intersections; 401 | var cursorEl = this.el; 402 | 403 | // Nothing to be cleared. 404 | if (!this.intersectedEl) { return; } 405 | 406 | // No longer hovering (or fusing). 407 | this.intersectedEl.removeState(STATES.HOVERED); 408 | cursorEl.removeState(STATES.HOVERING); 409 | cursorEl.removeState(STATES.FUSING); 410 | this.twoWayEmit(EVENTS.MOUSELEAVE); 411 | 412 | if (this.data.mouseCursorStylesEnabled && this.data.rayOrigin === 'mouse') { 413 | this.getCanvas().classList.remove(CANVAS_HOVER_CLASS); 414 | } 415 | 416 | // Unset intersected entity (after emitting the event). 417 | this.intersectedEl = null; 418 | 419 | // Clear fuseTimeout. 420 | clearTimeout(this.fuseTimeout); 421 | 422 | // Set intersection to another raycasted element if any. 423 | if (ignoreRemaining === true) { return; } 424 | intersections = this.el.components.raycaster.intersections; 425 | if (intersections.length === 0) { return; } 426 | // Exclude the cursor. 427 | index = intersections[0].object.el === cursorEl ? 1 : 0; 428 | intersection = intersections[index]; 429 | if (!intersection) { return; } 430 | this.setIntersection(intersection.object.el, intersection); 431 | }, 432 | 433 | /** 434 | * Helper to emit on both the cursor and the intersected entity (if exists). 435 | */ 436 | twoWayEmit: function (evtName) { 437 | var el = this.el; 438 | var intersectedEl = this.intersectedEl; 439 | var intersection; 440 | 441 | intersection = this.el.components.raycaster.getIntersection(intersectedEl); 442 | this.eventDetail.intersectedEl = intersectedEl; 443 | this.eventDetail.intersection = intersection; 444 | el.emit(evtName, this.eventDetail); 445 | 446 | if (!intersectedEl) { return; } 447 | 448 | this.intersectedEventDetail.intersection = intersection; 449 | intersectedEl.emit(evtName, this.intersectedEventDetail); 450 | }, 451 | 452 | getCanvas: function () { 453 | if (this.data.canvas === 'user') { 454 | return this.canvas; 455 | } else { 456 | return this.el.sceneEl.canvas; 457 | } 458 | }, 459 | 460 | getCamera: function () { 461 | if (this.data.camera === 'user') { 462 | return this.camera; 463 | } else { 464 | return this.el.sceneEl.camera; 465 | } 466 | } 467 | }); 468 | -------------------------------------------------------------------------------- /src/viewpoint-selector.js: -------------------------------------------------------------------------------- 1 | /* Component that handles the rendering of the viewpoint selector overlay 2 | When initialized, this exposes the following properties and functions: 3 | 4 | render(renderer) - call this during the render cycle to render the overlay. 5 | 6 | This component actually implements 3 separate functions: 7 | - Putting in place a secondary camera (see also: secondary-camera component) 8 | - Putting in place raycaster and cursor for that secondary camera. 9 | - Setting up the viewpoint-selectore entity in front of the secondary camera. 10 | 11 | It might be helpful to split out these functions into 3 separate components 12 | (as I have started to do with secondary-camera), so that they can each be 13 | used independently. Under consideration for future evolution of this 14 | codebase... 15 | */ 16 | AFRAME.registerComponent('viewpoint-selector-renderer', { 17 | schema: { 18 | displayBox: {type: 'selector', default: '#viewpoint-selector-box'}, 19 | cameraType: {type: 'string', oneOf: ['perspective, orthographic'], default: 'orthographic'}, 20 | syncTarget: {type: 'selector'}, 21 | layer: {type: 'number', default: 0}, 22 | angleOffset: {type: 'vec3'}, 23 | outer: {type: 'number', default: 1}, 24 | inner: {type: 'number', default: 0.6}, 25 | text: {type: 'color', default: '#000000'}, 26 | color: {type: 'color', default: '#FFFFFF'}, 27 | hoverColor: {type: 'color', default: '#BBBBBB'}, 28 | animationTimer: {type: 'number', default: 500} 29 | }, 30 | 31 | init() { 32 | 33 | this.savedViewport = new THREE.Vector4(); 34 | this.sceneInfo = this.prepareScene(); 35 | this.displayBox = this.data.displayBox; 36 | this.vr = this.el.sceneEl.is('vr-mode'); 37 | 38 | // Add Viewpoint Selector to the scene, and ensure it is lit. 39 | this.viewpointSelector = document.createElement('a-entity'); 40 | this.viewpointSelector.setAttribute('id', 'viewpoint-selector') 41 | this.viewpointSelector.setAttribute('viewpoint-selector', 42 | {syncTarget: `#${this.data.syncTarget.id}`, 43 | angleOffset: this.data.angleOffset, 44 | layer: this.data.layer, 45 | outer: this.data.outer, 46 | inner: this.data.inner, 47 | text: this.data.text, 48 | color: this.data.color, 49 | hoverColor: this.data.hoverColor, 50 | animationTimer: this.data.animationTimer 51 | }); 52 | this.viewpointSelector.object3D.position.set(0 , 0, -2) 53 | this.el.sceneEl.appendChild(this.viewpointSelector); 54 | 55 | this.viewpointSelectorControls = document.createElement('a-entity'); 56 | this.viewpointSelectorControls.setAttribute('id', 'viewpoint-selector-controls') 57 | this.viewpointSelectorControls.setAttribute("raycaster", "objects: .viewpointSelectorElement; interval: 100"); 58 | //fuseTimeout: 0: workaround for aframe bug on mobile device: device gets selected on scene loading (https://github.com/aframevr/aframe/issues/4141) 59 | this.viewpointSelectorControls.setAttribute("cursor", "rayOrigin: mouse; fuseTimeout: 0; canvas: user; camera: user"); 60 | this.el.sceneEl.appendChild(this.viewpointSelectorControls); 61 | 62 | this.viewpointSelectorControls.addEventListener('loaded', () => { 63 | this.viewpointSelectorControls.components['raycaster'].raycaster.layers.set(this.data.layer); 64 | const cursor = this.viewpointSelectorControls.components['cursor']; 65 | cursor.removeEventListeners(); 66 | cursor.camera = this.camera; 67 | cursor.canvas = this.displayBox; 68 | cursor.canvasBounds = cursor.canvas.getBoundingClientRect(); 69 | cursor.addEventListeners(); 70 | cursor.updateMouseEventListeners(); 71 | 72 | // a resize event also helps get everything aligned. 73 | window.dispatchEvent(new Event('resize')); 74 | }); 75 | 76 | // re-use main lighting - just ensure additional layer is set. 77 | enableObjectLayer = function(object, layer) { 78 | object.layers.enable(layer); 79 | object.children.forEach(o => enableObjectLayer(o, layer)); 80 | } 81 | document.querySelectorAll('[light]').forEach(el => { 82 | enableObjectLayer(el.object3D, this.data.layer); 83 | }); 84 | 85 | // add the render call to the scene 86 | // Since moving to add-render-call System (needed to support add/remove 87 | // of cameras), it seems this only works if I specify an identifier. 88 | // I don't really understand why, and haven't looked closely at this yet... 89 | // Added a note to the README. 90 | this.el.sceneEl.setAttribute('add-render-call__1', 91 | {entity: `#${this.el.id}`, 92 | componentName: "viewpoint-selector-renderer"}); 93 | 94 | // listen for movement into / out of VR. 95 | this.el.sceneEl.addEventListener('enter-vr', this.enterVR.bind(this), false); 96 | this.el.sceneEl.addEventListener('exit-vr', this.exitVR.bind(this), false); 97 | }, 98 | 99 | enterVR() { 100 | this.vr = true; 101 | 102 | // turn off viewpoint selector raycaster. Rndering will also stop. 103 | this.viewpointSelectorControls.setAttribute('raycaster', 'enabled: false'); 104 | }, 105 | 106 | exitVR() { 107 | this.vr = false; 108 | 109 | // turn on viewpoint selector raycaster. Rendering will also restart. 110 | this.viewpointSelectorControls.setAttribute('raycaster', 'enabled: true'); 111 | }, 112 | 113 | prepareScene() { 114 | this.scene = this.el.sceneEl.object3D; 115 | 116 | const width = 2; 117 | const height = 2; 118 | 119 | if (this.data.cameraType === "orthographic") { 120 | this.camera = new THREE.OrthographicCamera( width / - 2, width / 2, height / 2, height / - 2, 1, 1000 ); 121 | } 122 | else { 123 | this.camera = new THREE.PerspectiveCamera( 45, width / height, 1, 1000); 124 | } 125 | 126 | this.scene.add(this.camera); 127 | 128 | this.camera.position.set(0, 0, 0); 129 | this.camera.lookAt(0, 0, 0); 130 | this.camera.layers.disableAll(); 131 | this.camera.layers.set(this.data.layer); 132 | 133 | return; 134 | }, 135 | 136 | render(renderer, renderFunction) { 137 | 138 | if (this.vr) return; // no Viewpoint Selector in VR. 139 | 140 | const elem = this.displayBox; 141 | 142 | // get the viewport relative position of this element 143 | const elemRect = elem.getBoundingClientRect(); 144 | 145 | this.camera.aspect = elemRect.width / elemRect.height; 146 | this.camera.updateProjectionMatrix(); 147 | 148 | // "bottom" position is relative to the whole viewport, not just the canvas. 149 | // We need to turn this into a distance from the bottom of the canvas. 150 | // We need to consider the header bar above the canvas, and the size of the canvas. 151 | const mainRect = renderer.domElement.getBoundingClientRect(); 152 | 153 | renderer.getViewport(this.savedViewport); 154 | 155 | renderer.setViewport(elemRect.left - mainRect.left, 156 | mainRect.bottom - elemRect.bottom, 157 | elemRect.width, 158 | elemRect.height); 159 | 160 | renderFunction.call(renderer, this.scene, this.camera); 161 | renderer.setViewport(this.savedViewport); 162 | } 163 | }); 164 | 165 | AFRAME.registerComponent('viewpoint-selector', { 166 | 167 | schema: { 168 | outer: {type: 'number', default: 1}, 169 | inner: {type: 'number', default: 0.6}, 170 | text: {type: 'color', default: '#000000'}, 171 | color: {type: 'color', default: '#FFFFFF'}, 172 | hoverColor: {type: 'color', default: '#BBBBBB'}, 173 | layer: {type: 'number', default: 0}, 174 | syncTarget: {type: 'selector'}, 175 | angleOffset: {type: 'vec3'}, 176 | animationTimer: {type: 'number', default: 500} 177 | }, 178 | 179 | init: function () { 180 | 181 | // quaternions to use when translating to/from groups. 182 | this.selectorToGroupQuaternion = new THREE.Quaternion(); 183 | this.tempEuler = new THREE.Euler(); 184 | this.tempEuler.set(THREE.MathUtils.degToRad(this.data.angleOffset.x), 185 | THREE.MathUtils.degToRad(this.data.angleOffset.y), 186 | THREE.MathUtils.degToRad(this.data.angleOffset.z)); 187 | this.selectorToGroupQuaternion.setFromEuler(this.tempEuler); 188 | this.groupToSelectorQuaternion = this.selectorToGroupQuaternion.clone().invert(); 189 | 190 | this.animationTimer = 0; 191 | this.targetQuaternion = new THREE.Quaternion(); 192 | 193 | this.hoverStart = this.hoverStart.bind(this); 194 | this.hoverEnd = this.hoverEnd.bind(this); 195 | this.click = this.click.bind(this); 196 | 197 | this.quaternion = new THREE.Quaternion(); 198 | this.vector = new THREE.Vector3(); 199 | this.defaultOrientation = new THREE.Vector3( 0, 0, 1 ); 200 | 201 | const big = this.data.outer / 2; 202 | const small = this.data.inner / 2; 203 | frameCenter = (this.data.outer + this.data.inner) / 4; 204 | 205 | // Plane faces with text. 206 | this.front = this.createPlane("FRONT", `0 0 ${big}`, " 0 0 0"); 207 | this.back = this.createPlane("BACK", `0 0 -${big}`, " 0 180 0"); 208 | this.left = this.createPlane("LEFT", `-${big} 0 0`, " 0 -90 0"); 209 | this.right = this.createPlane("RIGHT", ` ${big} 0 0`, " 0 90 0"); 210 | this.top = this.createPlane("TOP", `0 ${big} 0`, "-90 0 0"); 211 | this.bottom = this.createPlane("BOTTOM", `0 -${big} 0`, " 90 0 0"); 212 | 213 | // cube corners. "RTF" = Right, Top, Front etc. (matches x/y/z order) 214 | // R = Right, L = Left, B = Bottom, T = Top, F = Front, K = Back (avoids ambiguity with bottom"). 215 | this.createCorner("RTF", ` ${frameCenter} ${frameCenter} ${frameCenter}`); 216 | this.createCorner("RTK", ` ${frameCenter} ${frameCenter} -${frameCenter}`); 217 | this.createCorner("RBF", ` ${frameCenter} -${frameCenter} ${frameCenter}`); 218 | this.createCorner("RBK", ` ${frameCenter} -${frameCenter} -${frameCenter}`); 219 | this.createCorner("LTF", `-${frameCenter} ${frameCenter} ${frameCenter}`); 220 | this.createCorner("LTK", `-${frameCenter} ${frameCenter} -${frameCenter}`); 221 | this.createCorner("LBF", `-${frameCenter} -${frameCenter} ${frameCenter}`); 222 | this.createCorner("LBK", `-${frameCenter} -${frameCenter} -${frameCenter}`); 223 | 224 | // edges - front 225 | this.createEdge("FT", ` 0 ${frameCenter} ${frameCenter}`, " 0 0 0"); 226 | this.createEdge("FB", ` 0 -${frameCenter} ${frameCenter}`, " 0 0 0"); 227 | this.createEdge("FR", ` ${frameCenter} 0 ${frameCenter}`, " 0 0 90"); 228 | this.createEdge("FL", `-${frameCenter} 0 ${frameCenter}`, " 0 0 90"); 229 | 230 | // edges - middle 231 | this.createEdge("TR", ` ${frameCenter} ${frameCenter} 0`, " 0 90 0"); 232 | this.createEdge("BR", ` ${frameCenter} -${frameCenter} 0`, " 0 90 0"); 233 | this.createEdge("TL", `-${frameCenter} ${frameCenter} 0`, " 0 90 0"); 234 | this.createEdge("BL", `-${frameCenter} -${frameCenter} 0`, " 0 90 0"); 235 | 236 | // edges - back 237 | this.createEdge("KT", ` 0 ${frameCenter} -${frameCenter}`, " 0 0 0"); 238 | this.createEdge("KB", ` 0 -${frameCenter} -${frameCenter}`, " 0 0 0"); 239 | this.createEdge("KL", ` ${frameCenter} 0 -${frameCenter}`, " 0 0 90"); 240 | this.createEdge("KR", `-${frameCenter} 0 -${frameCenter}`, " 0 0 90"); 241 | 242 | 243 | // Initially tried automatically calculating rotations using 244 | // quaternion.setFromUnitVectors(this.vector, this.defaultOrientation); 245 | // But this didn't give enough fine control in terms of final orientation 246 | // (as "up"/"down" don't hve any special status vs. other directions) 247 | // so it's simpler (and maximum control) to hard code 248 | // the angles for each position. 249 | this.angles = { 250 | "FRONT" : {x: 0, y: 0, z: 0}, 251 | "BACK" : {x: 0, y: 180, z: 0}, 252 | "LEFT" : {x: 0, y: 90, z: 0}, 253 | "RIGHT" : {x: 0, y: -90, z: 0}, 254 | "TOP" : {x: 90, y: 0, z: 0}, 255 | "BOTTOM": {x: -90, y: 0, z: 0}, 256 | "FT" : {x: 45, y: 0, z: 0}, 257 | "FB" : {x: -45, y: 0, z: 0}, 258 | "FR" : {x: 0, y: -45, z: 0}, 259 | "FL" : {x: 0, y: 45, z: 0}, 260 | "KT" : {x: 45, y: 180, z: 0}, 261 | "KB" : {x: -45, y: 180, z: 0}, 262 | "KR" : {x: 0, y: 135, z: 0}, 263 | "KL" : {x: 0, y: 225, z: 0}, 264 | "TR" : {x: 45, y: -90, z: 0}, 265 | "BR" : {x: -45, y: -90, z: 0}, 266 | "TL" : {x: 45, y: 90, z: 0}, 267 | "BL" : {x: -45, y: 90, z: 0}, 268 | "RTF" : {x: 45, y: -45, z: 0}, 269 | "RTK" : {x: 45, y: 225, z: 0}, 270 | "RBF" : {x: -45, y: -45, z: 0}, 271 | "RBK" : {x: -45, y: 225, z: 0}, 272 | "LTF" : {x: 45, y: 45, z: 0}, 273 | "LTK" : {x: 45, y: 135, z: 0}, 274 | "LBF" : {x: -45, y: 45, z: 0}, 275 | "LBK" : {x: -45, y: 135, z: 0}, 276 | } 277 | }, 278 | 279 | createPlane(text, position, rotation) { 280 | 281 | const plane = document.createElement('a-plane'); 282 | plane.setAttribute('text', {value: text, color: this.data.text, wrapCount: 7, width: this.data.inner * 1.4, align: 'center'}); 283 | plane.setAttribute('id', text); 284 | plane.setAttribute('class', 'viewpointSelectorElement'); 285 | plane.setAttribute('position', position); 286 | plane.setAttribute('rotation', rotation); 287 | plane.setAttribute('width', this.data.inner); 288 | plane.setAttribute('height', this.data.inner); 289 | plane.setAttribute('material', {color: this.data.color}); 290 | plane.setAttribute('layers', this.data.layer); 291 | 292 | this.el.appendChild(plane); 293 | this.addListenersToElement(plane); 294 | 295 | return plane; 296 | }, 297 | 298 | createCorner(id, position) { 299 | 300 | const size = (this.data.outer - this.data.inner) / 2; 301 | 302 | const box = document.createElement('a-box'); 303 | box.setAttribute('position', position); 304 | box.setAttribute('id', id); 305 | box.setAttribute('class', 'viewpointSelectorElement'); 306 | box.setAttribute('width', size); 307 | box.setAttribute('height', size); 308 | box.setAttribute('depth', size); 309 | box.setAttribute('material', {color: this.data.color}); 310 | box.setAttribute('layers', this.data.layer); 311 | 312 | this.el.appendChild(box); 313 | this.addListenersToElement(box); 314 | 315 | return box; 316 | }, 317 | 318 | createEdge(id, position, rotation) { 319 | 320 | const size = (this.data.outer - this.data.inner) / 2; 321 | 322 | const box = document.createElement('a-box'); 323 | box.setAttribute('position', position); 324 | box.setAttribute('id', id); 325 | box.setAttribute('class', 'viewpointSelectorElement'); 326 | box.setAttribute('rotation', rotation); 327 | box.setAttribute('width', this.data.inner); 328 | box.setAttribute('height', size); 329 | box.setAttribute('depth', size); 330 | box.setAttribute('material', {color: this.data.color}); 331 | box.setAttribute('layers', this.data.layer); 332 | 333 | this.el.appendChild(box); 334 | this.addListenersToElement(box); 335 | 336 | return box; 337 | }, 338 | 339 | addListenersToElement: function(el) { 340 | 341 | el.addEventListener('mouseenter', this.hoverStart); 342 | el.addEventListener('mouseleave', this.hoverEnd); 343 | el.addEventListener('click', this.click); 344 | 345 | }, 346 | 347 | hoverStart: function(event) { 348 | 349 | event.target.setAttribute("material", {color: this.data.hoverColor}); 350 | 351 | }, 352 | 353 | hoverEnd: function(event) { 354 | 355 | event.target.setAttribute("material", {color: this.data.color}); 356 | 357 | }, 358 | 359 | click: function(event) { 360 | 361 | /* Rather than rotating the viewpoint selector and syncing teh scen to it, 362 | Simpler to rotate the scene itself and rely on tick synchronization to rotate the viewpoint 363 | selector itself. */ 364 | const id = event.target.id; 365 | 366 | angle = this.angles[id]; 367 | 368 | this.tempEuler.set(THREE.MathUtils.degToRad(angle.x), 369 | THREE.MathUtils.degToRad(angle.y), 370 | THREE.MathUtils.degToRad(angle.z)) 371 | this.targetQuaternion.setFromEuler(this.tempEuler); 372 | this.targetQuaternion.multiply(this.selectorToGroupQuaternion); 373 | 374 | // Kick off the animation by setting a timer - animation handled in tick() 375 | // Don't use A-Frame animaton, because this forces us to use Euler's which 376 | // are unusable for this purpose. 377 | this.animationTimer = this.data.animationTimer; 378 | }, 379 | 380 | tick(time, timeDelta) { 381 | 382 | // First part of tick is to apply any updates necessary to the target element. 383 | if (this.animationTimer > 0) { 384 | 385 | const factor = Math.min(timeDelta / this.animationTimer, 1); 386 | this.data.syncTarget.object3D.quaternion.slerp(this.targetQuaternion, factor) 387 | 388 | this.animationTimer -= timeDelta; 389 | } 390 | 391 | // Second part of tick processing is to sync the viewpoint selector to the target. 392 | if (this.data.syncTarget) { 393 | this.el.object3D.quaternion.copy(this.data.syncTarget.object3D.quaternion); 394 | this.el.object3D.quaternion.multiply(this.groupToSelectorQuaternion); 395 | } 396 | } 397 | }); 398 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aframe-multi-camera 2 | 3 | 5 | 6 | 7 | 8 | ## Overview 9 | 10 | This repository contains 11 | 12 | - components that can be used to create multi-camera scenes in A-Frame. Cameras can render to the canvas, or a to plane surface within the scene. 13 | - components to create a viewpoint selector widget for a A-Frame scene being displayed in 2D. This widget provides an intuitive interface that allows a user to jump to any viewing angle of an object, in 45 degree increments. This is one example of how the multi-camera function can be used. 14 | 15 | ## Examples 16 | 17 | We have the following examples: 18 | 19 | #### Multiple Cameras 20 | 21 | [Multiple cameras and canvases](https://diarmidmackenzie.github.io/aframe-multi-camera/examples/multi-screen.html) 22 | 23 | [Cursor use across multiple secondary cameras](https://diarmidmackenzie.github.io/aframe-multi-camera/examples/multi-screen-with-cursor.html) 24 | 25 | [Multiple camera views embedded in an HTML page without any background full-screen A-Frame scene](https://diarmidmackenzie.github.io/aframe-multi-camera/examples/embedded-views.html) 26 | 27 | [Secondary cameras rendering to planes in the scene, to create a CCTV monitor effect](https://diarmidmackenzie.github.io/aframe-multi-camera/examples/camera-texture.html) 28 | 29 | [Primary camera rendering to a plane in the scene, to create a CCTV monitor effect](https://diarmidmackenzie.github.io/aframe-multi-camera/examples/monitor-user-view.html) 30 | 31 | [Mirrors rendered to planes in the scene](https://diarmidmackenzie.github.io/aframe-multi-camera/examples/mirror-example.html) 32 | 33 | ### Viewpoint Selector 34 | 35 | A [basic example](https://diarmidmackenzie.github.io/aframe-multi-camera/examples/viewpoint-selector-basic.html) of a scene where the central objects can be manipulated using the viewpoint-selector: 36 | 37 | A [second example](https://diarmidmackenzie.github.io/aframe-multi-camera/examples/viewpoint-selector-alternate.html), demonstrating various different configuration options for the viewpoint selector. 38 | 39 | 40 | ## Components 41 | 42 | The components in this repository are as follows: 43 | 44 | - add-render-call - This is a generic component that supports extensions to the main A-Frame render cycle, allowing for rendering from multiple cameras, and to multiple canvases. 45 | - secondary-camera - This is a generic component that uses add-render-call to support one or more secondary cameras, optionally also using a cursor. 46 | - layers - This allows configuration of layers on objects. Layers control whether an object can be seen by a camera, whether it is intersected with a raycaster etc. They are used to allow the viewpoint selector to exist in the main A-Frame scene, without interfering with it. 47 | - viewpoint-selector - This implements the cube-shaped selector itself. 48 | - viewpoint-selector-renderer - This configures the viewpoint-selector, and the accompanying cursor, camera, lighting and renderer 49 | 50 | The configuration options for each of these components are detailed below. 51 | 52 | The following components are used to support examples. 53 | 54 | - mouse-rotate-controls - A very simple utility component that allows an entity in a scene to be rotated by dragging the mouse (no configuration parameters). 55 | 56 | - hover. A very simple hove effect, used to demonstrate cursor controls in some of the examples. (no configuration parameters) 57 | 58 | 59 | 60 | ## Installation 61 | 62 | There are 4 modules that you may need. 63 | 64 | - To use secondary cameras at all, you need the `multi-camera.js` module from the `src` directory. 65 | - To use mouse cursor on a secondary camera, you will also need the `cursor.js` module from the `src` directory (see note below). 66 | - To use the viewpoint selector, you will need both of the above, and also the `viewpoint-selector`.js module from the `src` directory. 67 | - To use mirrors, you just need the `mirror.js` module from the `src` directory 68 | 69 | You can either download them and include them like this: 70 | 71 | ``` 72 | 73 | 74 | 75 | 76 | ``` 77 | 78 | Or via JSDelivr CDN (check the releases in the repo for the best version number to use) 79 | 80 | ``` 81 | 82 | 83 | 84 | 85 | ``` 86 | 87 | Theses components are not yet available in npm. If that would be useful to you, please raise an issue against the repo and I'll work on it... 88 | 89 | #### A note on cursor.js 90 | 91 | Using the mouse cursor on a canvas or camera that is not the main A-Frame canvas/camera depends upon a fix that is not yet available in any main A-Frame release (see [this PR](https://github.com/aframevr/aframe/pull/4983) for latest status on getting this into A-Frame.) 92 | 93 | Therefore you will need to include the patched version of A-Frame `cursor.js` from the aframe directory of this project. This module can be included in an HTML file after including A-Frame to patch `cursor.js`. 94 | 95 | 96 | 97 | ## Limitations 98 | 99 | Here are some current limitations with these components. 100 | 101 | - Various components have limited support for updates (i.e. changes to properties after initial creation). I'm happy to fix issues like this up on a case by case basis - please raise an issue on the repository. PRs for these fixes also welcome too!. 102 | - Rendering in-scene (i.e. mirror and secondary cameras that are not rendering to screen) currently only render to planes. Rendering to other flat shapes (circle, triangle, ring) should be straightforward - if someone raises an issue or PR I'll happily take a look. Not sure about rendering to 3D surfaces - getting some texture in place is probably not too hard. Getting physically accurate mirror reflections is probably *very* difficult! 103 | - For rendering in-scene, there is no frustrum culling. Textures are rendered whether or not they are actually in view of camera. 104 | - Rendering to screen is intended for desktop only, and is not intended to be used in VR. When in VR, rendering is skipped for secondary-cameras that render to screen (for rendering to a VR HUD, you can use rendering in-scene to a plane fixed directly in front of the camera). 105 | - In VR (tested on Oculus Quest 2) , mirror textures seem to be extremely pixelated, regardless of "quality" settings. I haven't yet understood why this is. 106 | - Cursor controls can only be used on secondary cameras rendered to screen, not on mirror textures, or secondary cameras rendered to a plane or other geometry within the scene. If you have specific use cases that need cursor on either of these, raise an issue and I will take a look at how hard this is. 107 | 108 | 109 | 110 | ## Components 111 | 112 | #### add-render-call 113 | 114 | This is a generic base component that supports extensions to the main A-Frame render cycle, allowing for rendering from multiple cameras, and to multiple canvases. 115 | 116 | If you use `secondary-camera` or `viewpoint-selector-renderer`, they will set this up for you, and you don't need to worry about this component. 117 | 118 | This could be added anywhere in the scene and should have the same effect, but typically it is added to the scene. Multiple instances of this component can be added, in which case they will compound with each other, in the order that they are initialized in. 119 | 120 | NOTE: for reasons unknown, it appears that this *doesn't* work unless it has an identifier - so add specify as e.g. `add-render-call___1` rather than `add-render-call` 121 | 122 | | Property | Default value | Description | 123 | | ------------- | ------------- | ------------------------------------------------------------ | 124 | | entity | none | A DOM selector for an entity that has a component configured on it with a suitable render extension function. | 125 | | componentName | none | The name of the component on the entity that has a suitable render extension function. | 126 | | sequence | 'after' | 'before', 'after' or 'replace'. Determines whether the camera view is rendered before or after the main scene is rendered, or whether this camera replaces the main camera completely.
If 'replace' is set on any one secondary camera, this will mean that the main camera is not rendered. | 127 | 128 | This component expects the specified entity / component to provide a suitable render extension function. 129 | 130 | This takes the following form: 131 | 132 | - It is available on the component as `this.render(renderer, renderFunction)` 133 | - It is written to be called after the main A-Frame rendering is completed, to perform additional rendering steps. It can and should assume that [`renderer.autoClear`](https://threejs.org/docs/index.html?q=webGLRen#api/en/renderers/WebGLRenderer.autoClear) has been set to `false`. 134 | - It expects to be passed the following parameters: 135 | - `renderer`: The [THREE.WebGLRenderer](https://threejs.org/docs/index.html?q=webGLRen#api/en/renderers/WebGLRenderer) object being used to render the scene 136 | - `renderFunction`: The original [render()](https://threejs.org/docs/index.html?q=webGLRen#api/en/renderers/WebGLRenderer.render) function for this renderer, which can be used to perform any additional rendering required. This should be used in place of `renderer.render()`, which has been co-opted by this component (and calling it would lead to infinite recursion) 137 | 138 | For an example of a suitable render extension function, see `render()` in `viewpoint-selector-renderer`. 139 | 140 | WARNING: removal of this component is currently bugged: see "Limitations". 141 | 142 | #### secondary-camera 143 | 144 | This is a generic component that uses add-render-call to support one or more secondary cameras, optionally also using a cursor. 145 | 146 | | Property | Default value | Description | 147 | | ------------- | ------------- | ------------------------------------------------------------ | 148 | | output | 'screen' | 'screen' or 'scene' (or 'plane' for backward compatibility). Determines whether the camera output goes to screen or to an entity within the scene. | 149 | | outputElement | none | The DOM element that camera output should be rendered to. For output = 'screen', this is typically a HTML div outside the A-Frame scene, with suitable styling to set its position.
For output='scene' (or 'plane') this should be an A-Frame entity, for example an ``. If you want to render on something other than a plane (like a circle for example), you may need to play with the aspectRatio property (see below). It also needs to have the src parameter specified with a texture (any texture will do) - see examples. (I should be able to write some code to lift this restriction, but didn't do it yet!) | 150 | | cameraType | orthographic | Either "orthographic" or "perspective". Determines the type of camera used to render the viewpoint selector cube. Other camera parameters (e.g. position, fov etc.) are not yet configurable. | 151 | | sequence | 'after' | 'before', 'after' or 'replace'. Determines whether the camera view is rendered before or after the main scene is rendered, or whether this camera replaces the main camera completely.
Typically set to 'after' when rendering to screen, and 'before' when rendering to a plane within the scene (so that the rendered plane is included in the final scene render).
If 'replace' is set on any one secondary camera, this will mean that the main camera is not rendered. | 152 | | quality | "high" | Either "high" or "low". Only applicable when output='scene' (or 'plane'). This determines the resolution used for the texture. "low" quality results in a lower-resolution texture, where there will be more noticeable pixelation, but will support a higher frame-rate. | 153 | | aspectRatio | "auto" | Either "auto" or a number. "auto" will set the aspect ratio based on the geometry's "width" and "height" properties if they exist, or will set the aspect ratio to 1 if they don't. If you specify a numeric value, it will be used as the aspect ratio (for example if the outputElement is a circle, specify a ratio of 1). | 154 | 155 | This component provides generic second camera functionality in the scene. 156 | 157 | In addition to the properties explicitly configured on the secondary-camera component, the secondary camera will also respond to the following components configured on the same entity. 158 | 159 | | **Component** | Effect | 160 | | ------------- | ------------------------------------------------------------ | 161 | | position | Used to control the position of the camera | 162 | | rotation | Used to to configure the orientation of the camera | 163 | | cursor | If this is configured, it should be configured with the properties "camera: user; canvas: user". The secondary camera will reconfigure the cursor and underlying raycaster to work with this camera and canvas. Note that this functionality depends on an enhancement to cursor.js that is not in A-Frame yet. See "Installation" above. | 164 | | layer | Used to set which layer the camera views. If a cursor component is configured on this entity, the underlying raycaster will also be configured to use the same layer. (WARNING: I didn't test this function yet. Hopefully it works!) | 165 | 166 | See the examples for how secondary-camera can be used. 167 | 168 | 169 | 170 | #### layers 171 | 172 | This allows configuration of layers on objects. Layers control whether an object can be seen by a camera, whether it is intersected with a raycaster etc. They are used to allow the viewpoint selector to exist in the main A-Frame scene, without interfering with it. 173 | 174 | It takes a single un-named property, 175 | 176 | See [the THREE.js docs](https://threejs.org/docs/index.html?q=layers#api/en/core/Layers ) for more background on Layers. 177 | 178 | NOTE: Layer 0 is the default layer. In VR, layers 1 & 2 are reserved for the left & right eyes respectively. So if you want to use custom layers, it's best to use layer 3+. 179 | 180 | #### viewpoint-selector 181 | 182 | This implements the cube-shaped selector itself. If you use viwpoint-selector-renderer, this sets up viewpoint-selector, and you won't need to use this component directly. 183 | 184 | | Property | Default value | Description | 185 | | -------------- | -------------------- | ------------------------------------------------------------ | 186 | | outer | 1 | The dimension (in metres) of the rotating cube. | 187 | | inner | 0.6 | The dimension (in metres) of the inner faces of the cube. | 188 | | text | #000000 (black) | The color used for the text on the cube | 189 | | color | #FFFFFF (white) | The color used for the cube itself | 190 | | hoverColor | #BBBBBB (light grey) | The color used for the cube segments when the mouse hovers over them | 191 | | layer | 0 | The layer used to render the viewpoint selector. Lighting, camera and raycaster must all also use this layer. | 192 | | syncTarget | None | A DOM selector for a target that should have its rotation synchronized with the viewpoint-selector. Typically this target will be a wrapper for all the objects you want to control with the viewpoint selector. When this target moves, the viewpoint selector moves to match it. When segments of the viewpoint selector are clicked, the viewpoint selector rotates and the target rotates with it. | 193 | | angleOffset | 0 0 0 | An angle offset between the viewpoint selector and the syncTarget. Specify this when the default (zero) rotation for the target is not "0 0 0". The viewpoint selector will maintain this offset between itself and the target. | 194 | | animationTimer | 500 | The time in msecs taken to rotate to a new fixed position after the user clicks on a segment of the viewpoint selector. | 195 | 196 | 197 | 198 | #### viewpoint-selector-renderer 199 | 200 | This configures the viewpoint-selector, and the accompanying cursor, camera, lighting and renderer. 201 | 202 | Much of the code here is similar to secondary-camera, and this component could probably be rewritten to use secondary-camera (in which case it could become a lot simpler). 203 | 204 | | Property | Default value | Description | 205 | | ------------------- | ----------------------- | ------------------------------------------------------------ | 206 | | displayBox | #viewpoint-selector-box | A DOM selector for an HTML element which should be used as a canvas to render the viewpoint-selector. | 207 | | cameraType | orthographic | Either "orthographic" or "perspective". Determines the type of camera used to render the viewpoint selector cube. Other camera parameters (e.g. position, fov etc.) are not yet configurable. | 208 | | other parameters... | various, see above. | These parameters are used to configure the viewpoint-selector itself. See above for details. | 209 | 210 | 211 | 212 | #### mirror 213 | 214 | This can be used to configure one side of an ``entity to act as a mirror. This is not currently supported on any other primitive (though it should also work on an `` that is set up with a plane geometry) 215 | 216 | | Property | Default value | Description | 217 | | -------- | ------------- | ------------------------------------------------------------ | 218 | | quality | "high" | Either "high" or "low". This determines the resolution used for the texture. "low" quality results in a lower-resolution texture, where there will be more noticeable pixelation, but will support a higher frame-rate. | 219 | 220 | 221 | 222 | In addition to the properties explicitly configured on the mirror component, the plane should be configured as follows: 223 | 224 | - For an opaque-backed mirror: `side` or `material.side` should be set to `back`. Other `material` parameters should be set to whatever material effect is desired for the mirror back. 225 | - For a mirror that is invisible when viewed from behind, set `opacity` or `material.opacity` to 0. 226 | 227 | 228 | 229 | ## Acknowledgments 230 | 231 | The `viewpoint selector` and `viewpoint-selector-renderer` components were originally developed in collaboration with Virtonomy (https://virtonomy.io), and formed the foundation for several other components in this repository. 232 | 233 | I am grateful to them for giving me permission to release these components as Open Source under the MIT License. 234 | 235 | The `mirror` component is just a thin wrapper around the THREE.js `Reflector` class, as can be seen in [this example](https://threejs.org/examples/webgl_mirror.html). 236 | 237 | 238 | 239 | ## Implementation Notes 240 | 241 | #### Single vs. multiple WebGL Contexts 242 | 243 | Some solutions for secondary cameras that I have seen elsewhere use multiple WebGL contexts (i.e. multiple canvases) to achieve the necessary rendering. 244 | 245 | E.g. https://jgbarah.github.io/aframe-playground/camrender-01/ 246 | 247 | (based on: https://wirewhiz.com/how-to-use-a-cameras-output-as-a-texture-in-aframe/...) 248 | 249 | In these components, I've avoided using multiple WebGL Contexts, instead using a single WebGL context, but invoking the render() method multiple times. 250 | 251 | With multiple WebGL contexts, the scene, geometry, textures etc. all need to be uploaded to the GPU for each context, which certainly increases memory usage, and I suspect impacts performance as well. 252 | 253 | I don't honestly know how great the benefits of using a single WebGL Context are, but I think there ought to be at least some benefit. Maybe I'll try to do some performance comparisons at some point. 254 | 255 | Note that even using a single WebGL context, there's still a draw call required for every object, on every camera, so secondary cameras do have a significant performance hit even when using just a single WebGL context - but hopefully less than it would have been otherwise. 256 | 257 | 258 | #### add-render-call vs. onBeforeRender() 259 | 260 | The mirror component uses THREE.js `object3D.onBeforeRender()` callbacks, rather than using the `add-render-call` component. 261 | 262 | I've experimented with using `object3DonBeforeRender()` for `secondary-camera` but hit issues with recursive `render()` calls that I couldn't easily solve. I haven't yet understood why the mirror component doesn't hit the same problems. 263 | 264 | However I'm interested to see whether moving mirror to use `add-render-call` might help with the highly pixelated textures in VR (see "Limitations"). 265 | 266 | 267 | #### add-render-call vs. tick() 268 | 269 | The [aframe-playground](https://jgbarah.github.io/aframe-playground/camrender-01/) example mentioned above uses the tick() function to perform the pre-rendering to textures. 270 | 271 | I've been wondering whether add-render-call is over-complicated, and instead of using this for rendering "before" and "after" the main render step, we could just use the tick(0 and tock() methods already offered by A-Frame - which would be somewhat simpler. 272 | 273 | So far I'm not inclined t make that change. Key reasons being: 274 | 275 | - If we used tick() and tock() processing for the additional render steps, this would get mixed up in terms of scheduling with any other tick() and tock() processing going on in the scene. By inserting the additional processing into the main render() call for the scene, we avoid any issues that might arise when other components' tick() or tock() method acts before we have finished rendering. 276 | - By keeping all render calls together, the add-render-call simplifies tracking rendering statistics (as used in the A-Frame stats component), including correctly counting draw calls.. Given that additional cameras do have a significant performance impact, I think it's useful to be able to track accurate statistics for the render processing. 277 | 278 | 279 | ## Tests 280 | 281 | As well as the examples folder, there is also a tests folder. 282 | 283 | This contains modified versions of examples, in order to test specific functions (so far I've focussed on cleanly adding / removing secondary cameras). 284 | 285 | These are not in the main examples folder, as they don't offer any particular additional "how to" insight. 286 | 287 | --------------------------------------------------------------------------------