/
renderer.ts
469 lines (400 loc) · 18 KB
/
renderer.ts
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
/* spellchecker: disable */
import { Observable, ReplaySubject } from 'rxjs';
import { vec2, vec4 } from 'gl-matrix';
import { assert, logIf, LogLevel } from './auxiliaries';
import { clamp, v2 } from './gl-matrix-extensions';
import { ChangeLookup } from './changelookup';
import { Context } from './context';
import { Controllable } from './controller';
import { EventProvider } from './eventhandler'
import { Initializable } from './initializable';
import { GLclampf4, GLfloat2, GLsizei2, tuple2 } from './tuples';
import { Wizard } from './wizard';
/* spellchecker: enable */
/**
* The interface to a callback that is called if the renderer is invalidated.
*/
export interface Invalidate { (force: boolean): void; }
export enum LoadingStatus {
Started,
Finished,
}
/**
* Base class for hardware-accelerated processing and/or image-synthesis. It provides information such as the current
* canvas, the canvas's size (native resolution), and the multi-frame number (for progressive rendering). A renderer's
* properties are expected to be managed by its owning object or the canvas and should not be set directly/manually.
* Alterations to these properties can be tracked with the `_altered` property. This allows an inheritor to implement
* partial asset reallocation and, e.g., speed up dynamic multi-frame reconfiguration. The alterable object can be
* extended using `Object.assign(this._alterable, ... some structure of booleans)`.
*
* This base class further provides the invalidate method that invokes an invalidation callback also provided by the
* owning/controlling canvas.
*
* Since Initializable is extended, the initialization workflow applies to all specialized renderers (requires super
* calls in constructor as well as in initialize and uninitialize).
*
* Note that a renderer is currently intended to always render to the canvas it is bound to. Hence, there is no
* interface for setting a frame target.
*/
export abstract class Renderer extends Initializable implements Controllable {
/**
* The renderer's invalidation callback. This should usually be setup by the canvas and refer to a function in the
* canvas's controller, e.g., it should trigger an update within the controller.
*/
protected _invalidate: Invalidate;
/** @see {@link context} */
protected _context: Context;
/**
* Alterable auxiliary object for tracking changes on renderer input and lazy updates.
*/
protected readonly _altered = Object.assign(new ChangeLookup(), {
any: false, multiFrameNumber: false, frameSize: false, canvasSize: false, framePrecision: false,
clearColor: false, debugTexture: false,
});
/**
* This multi-frame number is for lazy reconfiguration and set on update. The inheritor can react to changes using
* this.altered.multiFrameNumber.
*/
protected _multiFrameNumber: number;
/**
* Targeted resolution for image synthesis. This might differ from the canvas resolution and should be used in
* frame calls of inheritors.
*/
protected _frameSize: GLsizei2 = [0, 0];
/**
* Actual, native resolution for the canvas currently in charge of controlling the renderer. This might differ from
* the targeted frame resolution but is required, e.g., for specific non-proportional ratios between frame size and
* canvas size.
*/
protected _canvasSize: GLsizei2 = [0, 0];
/**
* Targeted frame precision, e.g., used for frame accumulation. Note that any renderer is currently
* expected to take advantage of progressive rendering (e.g., multi-frame sampling) and accumulation as well as a
* blit pass (since main intend is multi-frame based rendering).
*/
protected _framePrecision: Wizard.Precision = Wizard.Precision.half;
/**
* The clear color, provided by the canvas the renderer is bound to. This is used in frame calls of inheritors.
*/
protected _clearColor: GLclampf4 = [0.0, 0.0, 0.0, 1.0];
/**
* List of textures for debugging purposes such as normals, ids, depth, masks, etc. that can be populated by the
* inheritor. The index of a texture identifier can then be for specifying a debug output of a render texture.
*/
protected _debugTextures = new Array<string>();
/**
* @see {@link debugTexture}
* This property can be observed, e.g., `aRenderer.debugTextureObservable.subscribe()`.
*/
protected _debugTexture: GLint;
protected _debugTextureSubject = new ReplaySubject<GLint>(1);
/**
* @see {@link isLoading}
*/
protected _isLoading: boolean;
/**
* This property can be observed via `aRenderer.loadingState$.observe()`. It is triggered when `finishLoading` or
* `startLoading` is called on this renderer.
*/
protected _loadingStatusSubscription: ReplaySubject<LoadingStatus>;
/** @callback Invalidate
* A callback intended to be invoked whenever the specialized renderer itself or one of its objects is invalid. This
* callback should be passed during initialization to all objects that might handle invalidation internally as well.
* As a result, rendering of a new frame will be triggered and enforced.
*/
@Initializable.assert_initialized()
protected invalidate(force: boolean = false): void {
this._invalidate(force);
}
/**
* Utility for communicating this._debugTexture changes to its associated subject.
*/
protected debugTextureNext(): void {
this._debugTextureSubject.next(this._debugTexture);
}
/**
* Context that can be used for processing and rendering as well as passed to rendering stages.
*/
protected get context(): Context {
this.assertInitialized();
return this._context;
}
protected get canvasSize(): GLsizei2 {
this.assertInitialized();
return this._canvasSize;
}
/**
* Whether or not any of the (relevant/monitored) rendering properties has been altered. This concept should be used
* by other classes (e.g., camera, rendering stages) for detecting modifications relevant for rendering output.
*/
protected get altered(): boolean {
return this._altered.any;
}
/**
* Actual initialize call specified by inheritor.
* @returns - whether initialization was successful
*/
protected abstract onInitialize(context: Context, callback: Invalidate,
eventProvider: EventProvider): boolean;
/**
* Actual uninitialize call specified by inheritor.
*/
protected abstract onUninitialize(): void;
/**
* Actual discard call specified by inheritor.
*/
protected abstract onDiscarded(): void;
/**
* Actual update call specified by inheritor. This is invoked in order to check if rendering of a frame is required
* by means of implementation specific evaluation (e.g., lazy non continuous rendering). Regardless of the return
* value a new frame (preparation, frame, swap) might be invoked anyway, e.g., when update is forced or canvas or
* context properties have changed or the renderer was invalidated @see{@link invalidate}.
* @returns - Whether to redraw
*/
protected abstract onUpdate(): boolean;
/**
* Actual prepare call specified by inheritor. This is invoked in order to prepare rendering of one or more frames.
* This should be used for rendering preparation, e.g., when using multi-frame rendering this might specify uniforms
* that do not change for every intermediate frame.
*/
protected abstract onPrepare(): void;
/**
* Actual frame call specified by inheritor. After (1) update and (2) preparation are invoked, a frame is invoked.
* This should be used for actual rendering implementation.
*/
protected abstract onFrame(frameNumber: number): void;
/**
* Actual swap call specified by inheritor. After (1) update, (2) preparation, and (3) frame are invoked, a swap
* might be invoked. In case of experimental batch rendering when using multi-frame a swap might be withhold for
* multiple frames. Any implementation is expected to ensure that contents of a frame to be on the OpenGL
* back buffer. The swap of front buffer and back buffer is scheduled after the invocation of this function by
* the browser.
*/
protected onSwap(): void { /* default empty impl. */ }
/**
* This method needs to be called by a renderer, when a loading process is started in order to notify listeners via
* the observable loadingState$.
*/
protected startLoading(): void {
this._isLoading = true;
this._loadingStatusSubscription.next(LoadingStatus.Started);
}
/**
* This method needs to be called when a loading process is finished in order to notify listeners via
* the observable loadingState$.
*/
protected finishLoading(): void {
this._isLoading = false;
this._loadingStatusSubscription.next(LoadingStatus.Finished);
}
/**
* When extending (specializing) this class, initialize should initialize all required stages and allocate assets
* that are shared between multiple stages. Note that `super.initialize()` should always be called first when
* 'overriding' this function.
*
* Note: the context handle is stored in a property, but should be passed to the stages by specializing
* renderer instead. The renderer itself should not allocate rendering resources directly, thus, it should not
* require a webgl context.
*
* @param context - Wrapped gl context for function resolution (passed to all stages).
* @param callback - Functions that is invoked when the renderer (or any stage) is invalidated.
* @param eventProvider - Provider for mouse events referring to the canvas element.
*/
@Initializable.initialize()
initialize(context: Context, callback: Invalidate,
eventProvider: EventProvider): boolean {
assert(context !== undefined, `valid webgl context required`);
this._context = context;
assert(callback !== undefined, `valid multi-frame update callback required`);
this._invalidate = callback;
this._isLoading = true;
this._loadingStatusSubscription = new ReplaySubject();
return this.onInitialize(context, callback, eventProvider);
}
/**
* Should release all assets and uninitialize all stages. `super.uninitialize()` should always be called first when
* overriding this function.
*/
@Initializable.uninitialize()
uninitialize(): void {
this.onUninitialize();
}
/**
* Should discard all assets and uninitialize all stages. `super.discarded()` should always be called first when
* overriding this function.
*/
@Initializable.discard()
public discard(): void {
this.onDiscarded();
}
/**
*
*/
@Initializable.assert_initialized()
update(multiFrameNumber: number): boolean {
if (this._canvasSize[0] !== this._context.gl.canvas.width ||
this._canvasSize[1] !== this._context.gl.canvas.height) {
this._canvasSize[0] = this._context.gl.canvas.width;
this._canvasSize[1] = this._context.gl.canvas.height;
this._altered.alter('canvasSize');
}
if (this._multiFrameNumber !== multiFrameNumber) {
this._multiFrameNumber = multiFrameNumber;
this._altered.alter('multiFrameNumber');
}
return this.onUpdate() || this._altered.any;
}
/**
* Prepares the rendering of the next frame (or subsequent frames when multi-frame rendering).
* This is part of the controllable interface. The renderer should reconfigure as lazy as possible.
* @param multiFrameNumber - The multi-frame number as requested by controller.
*/
@Initializable.assert_initialized()
prepare(): void {
this.onPrepare();
}
/**
* Controllable interface intended to trigger rendering of a full pass of the renderer that results in either an
* intermediate frame for accumulation to a full multi-frame or full frame for itself. The inheritor should invoke
* frames of relevant rendering and processing stages.
* @param frameNumber - The current frame number forwarded to onFrame.
*/
@Initializable.assert_initialized()
frame(frameNumber: number): void {
this.onFrame(frameNumber);
}
/**
* Interface intended to trigger swap (by controller).
*/
@Initializable.assert_initialized()
swap(): void {
this.onSwap();
}
/**
* Transforms local viewport coordinates into local intermediate frame coordinates.
* @param x - Horizontal coordinate for the upper left corner of the viewport origin.
* @param y - Vertical coordinate for the upper left corner of the viewport origin.
*/
frameCoords(x: GLint, y: GLint): GLfloat2 {
const position = vec2.divide(v2(), this._frameSize, this.canvasSize);
vec2.floor(position, vec2.multiply(position, [x + 0.5, y + 0.5], position));
vec2.add(position, position, [0.5, 0.5]);
return tuple2<GLfloat>(position);
}
// /**
// * @interface CoordsAccess
// * Look up a fragments coordinates by unprojecting the depth using the renderer's camera.
// * @param x - Horizontal coordinate for the upper left corner of the viewport origin.
// * @param y - Vertical coordinate for the upper left corner of the viewport origin.
// * @param zInNDC - optional depth parameter (e.g., from previous query).
// * @returns - 3D coordinate reprojected from NDC/depth to world space.
// */
// abstract coordsAt(x: GLint, y: GLint, zInNDC?: number, viewProjectionInverse?: mat4): vec3 | undefined;
// /**
// * @interface IDAccess
// * Look up an object id at a specific fragment.
// * @param x - Horizontal coordinate for the upper left corner of the viewport origin.
// * @param y - Vertical coordinate for the upper left corner of the viewport origin.
// * @returns - ID encoded of an object rendered/visible at given position.
// */
// abstract idAt(x: GLint, y: GLint): GLsizei | undefined;
/**
* Changes the frame size for rendering. This setter should only be used by the canvas this renderer is bound to.
* Changing the frame size invalidates the renderer.
*
* Note: the frame size is detached from the canvas size. When blitting the frame into the canvas, the frame is
* rescaled to fill or fit the canvas size.
*
* @param size - Resolution of the framebuffer.
*/
set frameSize(size: GLsizei2) {
this.assertInitialized();
if (vec2.equals(this._frameSize, size)) {
return;
}
Object.assign(this._frameSize, size);
this._altered.alter('frameSize');
this.invalidate();
}
/**
* Set the frame precision.
* @param format - The accumulation format. Expected values are one of 'byte', 'half', 'float', or 'auto'
*/
set framePrecision(precision: Wizard.Precision) {
this.assertInitialized();
if (this._framePrecision === precision) {
return;
}
this._framePrecision = precision;
this._altered.alter('framePrecision');
this.invalidate();
}
/**
* Sets the color used for clearing the background. This setter should only be used by the canvas this renderer is
* bound to. Changing the frame size invalidates the renderer.
* @param color - Red, green, blue, and alpha color components.
*/
set clearColor(color: GLclampf4) {
this.assertInitialized();
if (vec4.equals(this._clearColor, color)) {
return;
}
Object.assign(this._clearColor, color);
this._altered.alter('clearColor');
this.invalidate();
}
/**
* Read only access to the renderers registered render textures that can be blit to the back buffer for debugging.
* @returns - Array of render texture identifiers.
*/
get debugTextures(): Array<string> {
this.assertInitialized();
return this._debugTextures;
}
/**
* The render texture index for debug output. This is -1 when debug output is disabled. This should be used in
* the renderers swap implementation.
*/
get debugTexture(): GLint {
this.assertInitialized();
return this._debugTexture;
}
/**
* Enables to specify the index of a render texture to be blit to the back buffer for debugging. This invalidates
* but should result in a blit only if nothing else changed. When the requested debug texture was blit (and
* debugTexture was actually altered) `this.debugTextureNext()` should be called to inform observers.
* @param index - Render texture index based on debuggableTextures array. This should be in [-1, length of array].
*/
set debugTexture(index: GLint) {
this.assertInitialized();
if (this._debugTexture === index) {
return;
}
logIf(index >= this._debugTextures.length, LogLevel.Error, `invalid texture index, ` +
`debug texture disabled (index set to -1) | ${index} not in [-1,+${this._debugTextures.length - 1}]`);
this._debugTexture = index < this._debugTextures.length ?
clamp(index, -1, this._debugTextures.length - 1) : -1;
this._altered.alter('debugTexture');
this.invalidate();
}
/**
* Observable that can be used to subscribe to debug texture changes.
*/
get debugTexture$(): Observable<GLint> {
return this._debugTextureSubject.asObservable();
}
/**
* This property indicated whether a loading process is currently in progress.
* It can be changed by calling `startLoading` or `finishLoading` on this renderer.
*/
get isLoading(): boolean {
return this._isLoading;
}
/**
* Observable to subscribe to the current loading state of this renderer.
* Use `aRenderer.loadingStatus$.subscribe()` to register a new subscriber.
*/
get loadingStatus$(): Observable<LoadingStatus> {
return this._loadingStatusSubscription.asObservable();
}
}