UNPKG

13.4 kBJavaScriptView Raw
1'use strict';
2
3import * as THREE from 'three';
4import EdgesHelper from './EdgesHelper.js';
5import FluxCameras from './FluxCameras.js';
6import FluxHelpers from './helpers/FluxHelpers.js';
7import FluxRenderContext from './FluxRenderContext.js';
8import CameraControls from './controls/CameraControls.js';
9import SelectionControls from './controls/SelectionControls.js';
10import * as constants from './controls/constants.js';
11
12/**
13 * Class wrapping the three.js renderer with more build in functionality.
14 * Context swapping lets a single OpenGL context and canvas be used for multiple renderers.
15 * @class FluxRenderer
16 * @param {Element} domParent The div container for the canvas
17 * @param {Number} width The width of the canvas
18 * @param {Number} height The height of the canvas
19 * @param {Enumeration} selection Whether to enable user selection
20 */
21export default function FluxRenderer(domParent, width, height, selection) {
22 this.id = FluxRenderer.nextId++;
23
24 // Dom element that wraps the canvas
25 this._domParent = domParent;
26
27 // Current three.js geometry to render
28 this._model = null;
29
30 // The object containing the lights in the scene
31 this._lights = null;
32
33 // The context that contains the renderer and corresponds to a canvas
34 // Create renderer for the first time.
35 this._context = FluxRenderContext.getNewContext();
36
37 this._width = width;
38 this._height = height;
39
40 this._createCacheCanvas(width, height);
41
42 this.setClearColor(0xC5CDCC);
43
44 this._cameras = new FluxCameras(width, height);
45 this._helpers = new FluxHelpers();
46 this._helpersScene = new THREE.Scene();
47 this._helpersScene.add(this._helpers);
48
49 // Scene containing geometry to be rendered in this viewport THREE.Scene
50 this._scene = new THREE.Scene();
51
52 this._controls = [];
53 // Camera to be rendered with.Any instance of `THREE.Camera` can be set here.
54 var _this = this;
55 this._editorControls = this.addControls(CameraControls);
56 this._editorControls.addEventListener(constants.Events.CHANGE, function(event) {
57 _this._cameras.updateCamera(_this._width, _this._height);
58 _this.dispatchEvent(event);
59 });
60 this._selectionControls = this.addControls(SelectionControls);
61 this._selectionControls.setMode(selection);
62
63 // Fog object for this viewport constructed from color and density
64 this._fog = new THREE.FogExp2( this._clearColor, 0.0 );
65 this._scene.fog = this._fog;
66
67 // Scene containing edges geometry for hidden line rendering
68 this._edgesScene = new THREE.Scene();
69 this._edgesMode = EdgesHelper.EDGES_MODES.NONE;
70}
71
72FluxRenderer.prototype = Object.create( THREE.EventDispatcher.prototype );
73FluxRenderer.prototype.constructor = FluxRenderer;
74
75// Used for debugging issues with _setHost
76FluxRenderer.nextId = 0;
77
78/**
79 * Set the lights used to illuminate the scene.
80 * @param {THREE.Object3D} lights Object with lights as children
81 */
82FluxRenderer.prototype.setLights = function(lights) {
83 if (this._lights) {
84 this._scene.remove(this._lights);
85 }
86 this._lights = lights;
87 this._scene.add(this._lights);
88};
89
90/**
91 * Remove the geometry objects from the THREE registry so they can be garbage collected
92 * @param {THREE.Object3D} obj The object being removed
93 */
94function _removeGeometries(obj) {
95 if (obj.geometry) {
96 obj.geometry.dispose();
97 }
98}
99
100/**
101 * Remove an object from the scene and clean up memory
102 * @param {THREE.Scene} scene Scene containing the model
103 * @param {THREE.Object3D} model The geometry to remove
104 */
105function _deleteFromScene(scene, model) {
106 if (!model || !scene) return;
107 scene.remove(model);
108 model.traverse(_removeGeometries);
109}
110
111/**
112 * Add a new plugin for user interaction controls.
113 * See ViewportControls.js for more information.
114 * @param {ViewportControls} CustomControls A constructor that implements the controls interface.
115 * @return {CustomControls} The new instance
116 */
117FluxRenderer.prototype.addControls = function(CustomControls) {
118 var customControls = new CustomControls(this._cameras.getCamera(), this._scene, this._domParent, this._width, this._height);
119 var _this = this;
120 customControls.addEventListener(constants.Events.CHANGE, function (event) {
121 _this.dispatchEvent(event);
122 });
123 this._controls.push(customControls);
124 return customControls;
125};
126
127/**
128 * Define the material that is applied on selected objects
129 * @param {Object} data Flux json description of a material
130 */
131FluxRenderer.prototype.setSelectionMaterial = function(data) {
132 this._selectionControls.setMaterial(data);
133};
134
135/**
136 * Get the currently selected geometry
137 * @return {THREE.Object3D} Current selection
138 */
139FluxRenderer.prototype.getSelection = function() {
140 return this._selectionControls.getSelection();
141};
142
143/**
144 * set the currently selected geometry
145 * @param {THREE.Object3D} object New selection
146 */
147FluxRenderer.prototype.setSelection = function(object) {
148 this._selectionControls.setSelection(object);
149};
150
151/**
152 * Set the object to render
153 * Replaces old render contents
154 * @param {THREE.Object3D} model What to render
155 */
156FluxRenderer.prototype.setModel = function(model) {
157 if (this._model) {
158 _deleteFromScene(this._scene, this._model);
159 _deleteFromScene(this._edgesScene, this._model.edgesHelper);
160 }
161 this._model = model;
162 if (this._model) {
163 this._scene.add(this._model);
164 if (EdgesHelper.AddEdges(this._model, this._edgesMode)) {
165 this._edgesScene.add(this._model.edgesHelper);
166 }
167 }
168};
169
170/**
171 * Set the edges rendering mode for hidden line rendering
172 * @param {EdgesHelper.EDGES_MODES} mode Whether to render front, back, both or none
173 */
174FluxRenderer.prototype.setEdgesMode = function(mode) {
175 this._edgesMode = mode;
176};
177
178/**
179 * Restore the camera to a default location
180 */
181FluxRenderer.prototype.homeCamera = function() {
182 this._editorControls.focus(this._helpers);
183};
184
185/**
186* Focus the controls' current camera on an object.
187* This function will focus on the union of object and all of it's visible children.
188* @param {THREE.Object3D} [obj] The scene object to focus on.
189*/
190FluxRenderer.prototype.focus = function(obj) {
191 if (!this._model && !obj) return;
192 if (obj) {
193 this._editorControls.focus(obj);
194 } else {
195 var selection = this._selectionControls.getSelectionSphere();
196 if (selection) {
197 this._editorControls.focus(selection);
198 } else {
199 this._editorControls.focus(this._model);
200 }
201 }
202 // Changing the controls here triggers a render
203};
204
205/**
206 * Set the clear color (background) for WebGL canvas
207 * @param {String|Number} color Hexadecimal or a CSS-style string
208 * @param {Number} alpha Opacity
209 */
210FluxRenderer.prototype.setClearColor = function(color, alpha) {
211 this._clearColor = new THREE.Color(color);
212 this._clearAlpha = alpha;
213};
214
215/**
216 * Whether to draw helpers (axis and grid)
217 *
218 * @param {Boolean} visible False to hide them
219 */
220FluxRenderer.prototype.setHelpersVisible = function(visible) {
221 this._helpersScene.visible = !!visible;
222};
223
224/**
225 * Set up a new canvas used for storing a cached image.
226 * The cache image is populated when this renderer loses it's context.
227 * @private
228 * @param {Number} width The width of the canvas
229 * @param {Number} height The height of the canvas
230 */
231FluxRenderer.prototype._createCacheCanvas = function(width, height) {
232 if (this._cacheCanvas) return;
233 // The canvas used to store a cached image of the previous render when all the WebGL contexts are in use with other renderers
234 this._cacheCanvas = document.createElement('canvas');
235 this._cacheCanvas.width = width;
236 this._cacheCanvas.height = height;
237 this._cacheCanvas.style.position = 'absolute';
238 this._cacheCanvas.style['user-select'] = 'none';
239 this._cacheCanvas.style['-webkit-user-select'] = 'none';
240 this._domParent.appendChild(this._cacheCanvas);
241
242 // Canvas2D used to store framebuffer pixels after renderer.domElement migration.
243 this.ctx = this._cacheCanvas.getContext('2d');
244};
245
246/**
247 * Destructor to prevent future rendering after being unloaded
248 */
249FluxRenderer.prototype.detach = function() {
250 if (this._context && this._context.currentHost === this) {
251 this._context.currentHost = null;
252 }
253};
254
255/**
256 * Set which camera view to use (ex perspective, top etc.).
257 * @param {FluxCameras.VIEWS} view The new view mode
258 */
259FluxRenderer.prototype.setView = function(view) {
260 this._cameras.setView(view);
261 this._cameras.updateCamera(this._width, this._height);
262 // Update the camera for each controls object
263 for (var i=0;i<this._controls.length;i++) {
264 this._controls[i].setCamera(this._cameras.getCamera());
265 }
266 this._helpers.setView(view);
267};
268
269/**
270 * Creates depth, normal materials and depth, normal render targets.
271 * @private
272 */
273FluxRenderer.prototype._addRenderTargets = function() {
274 // depth render target (uses THREE.js depth shader)
275 this._depthTarget = new THREE.WebGLRenderTarget(
276 window.innerWidth, //TODO(kyle) Why does this use window!?
277 window.innerHeight,
278 {
279 minFilter: THREE.NearestFilter,
280 magFilter: THREE.NearestFilter,
281 format: THREE.RGBAFormat
282 }
283 );
284
285 // normal render target
286 this._normalTarget = this._depthTarget.clone();
287};
288
289/**
290* Render the scene with its geometry.
291*/
292FluxRenderer.prototype.doRender = function () {
293 this._setHost();
294 this._update();
295 this._context.renderer.clear();
296 this._context.renderer.render(this._scene, this._cameras.getCamera());
297 this._context.renderer.render(this._edgesScene, this._cameras.getCamera());
298 this._context.renderer.render(this._helpersScene, this._cameras.getCamera());
299};
300
301/**
302 * Say whether there are any objects to render in the model
303 * @return {Boolean} True if there are objects to render
304 */
305FluxRenderer.prototype.anyValidPrims = function() {
306 return this._model ? this._model.children.length > 0 : false;
307};
308
309/**
310 * Copy the image that is in the render canvas to this renderer's cache canvas.
311 * This allows the rendered image to persist even when the renderer is not available.
312 * This happens when the user moves the mouse away from this viewport to another one.
313 * @private
314 */
315FluxRenderer.prototype._cacheImageToCanvas = function () {
316 this.doRender();
317 this.ctx.drawImage(this._context.renderer.domElement, 0, 0, this._cacheCanvas.width, this._cacheCanvas.height);
318};
319
320/**
321 * Get the canvas for use in QA scripts
322 * @return {Canvas} WebGL canvas dom element
323 */
324FluxRenderer.prototype.getGlCanvas = function() {
325 return this._context.renderer.domElement;
326};
327
328/**
329* Migrate renderer.domElement to this host if necessary
330* and copy framebuffer into Canvas2D element of the previous host.
331 * @private
332*/
333FluxRenderer.prototype._setHost = function() {
334 if (this === this._context.currentHost) return;
335 if (this._context.currentHost) {
336 // Copy the image from domElement (THREE's interactive canvas)
337 // to the 2D context for this element's canvas
338 // This image will remain up until the user interacts with the old viewport again
339 this._context.currentHost._cacheImageToCanvas();
340 }
341 this._context.currentHost = this;
342 this.setSize(this._width, this._height);
343 // Move the THREE.WebGLRenderer's canvas under the new host
344 this._domParent.appendChild(this._context.renderer.domElement);
345};
346
347/**
348 * Set the WebGLRenderer parameters to match this renderer.
349 * @private
350 */
351FluxRenderer.prototype._update = function() {
352 this._context.renderer.autoClearColor = false;
353 this._context.renderer.autoClearDepth = false;
354 this._context.renderer.setSize(this._width, this._height);
355 this._context.renderer.setClearColor(this._clearColor, this._clearAlpha);
356};
357
358/**
359 * Set the size of things that are per viewport.
360 * @param {Number} width The canvas width in pixels
361 * @param {Number} height The canvas height in pixels
362 */
363FluxRenderer.prototype.setSize = function(width, height) {
364 if (width <= 0 || height <= 0 || (width === this._width && height === this.height)) return;
365 this._width = width;
366 this._height = height;
367
368 this._cameras.updateCamera(this._width, this._height);
369
370 for (var i=0;i<this._controls.length;i++) {
371 this._controls[i].setSize(this._width, this._height);
372 }
373
374 this._cacheCanvas.height = height;
375 this._cacheCanvas.width = width;
376};
377
378/**
379 * Make serializable by pruning all references and building an object property tree
380 * @return {Object} Data to stringify
381 */
382FluxRenderer.prototype.toJSON = function() {
383 var serializableState = {
384 cameras: this._cameras.toJSON(), // camera pos and view
385 controls: this._editorControls.toJSON() // center point
386 };
387 //TODO(Kyle): Transition to serializing all controls objets
388 return serializableState;
389};
390
391/**
392 * Take a data object and use it to update the internal state
393 * @param {Object} state The properties to set
394 */
395FluxRenderer.prototype.fromJSON = function(state) {
396 if (!state) return;
397 if (state.cameras != null) {
398 this.setView(state.cameras.view);
399 this._cameras.fromJSON(state.cameras);
400 }
401 if (state.controls) {
402 //TODO(Kyle): Transition to deserializing all controls objets
403 this._editorControls.fromJSON(state.controls);
404 }
405};