-
Notifications
You must be signed in to change notification settings - Fork 904
/
runtimescene-pixi-renderer.ts
439 lines (376 loc) · 17.4 KB
/
runtimescene-pixi-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
namespace gdjs {
/**
* The renderer for a gdjs.RuntimeScene using Pixi.js.
*/
export class RuntimeScenePixiRenderer
implements gdjs.RuntimeInstanceContainerPixiRenderer {
private _runtimeGameRenderer: gdjs.RuntimeGamePixiRenderer | null;
private _runtimeScene: gdjs.RuntimeScene;
private _pixiContainer: PIXI.Container;
private _profilerText: PIXI.Text | null = null;
private _showCursorAtNextRender: boolean = false;
private _threeRenderer: THREE.WebGLRenderer | null = null;
private _layerRenderingMetrics: {
rendered2DLayersCount: number;
rendered3DLayersCount: number;
} = {
rendered2DLayersCount: 0,
rendered3DLayersCount: 0,
};
constructor(
runtimeScene: gdjs.RuntimeScene,
runtimeGameRenderer: gdjs.RuntimeGamePixiRenderer | null
) {
this._runtimeGameRenderer = runtimeGameRenderer;
this._runtimeScene = runtimeScene;
this._pixiContainer = new PIXI.Container();
// Contains the layers of the scene (and, optionally, debug PIXI objects).
this._pixiContainer.sortableChildren = true;
this._threeRenderer = this._runtimeGameRenderer
? this._runtimeGameRenderer.getThreeRenderer()
: null;
}
onGameResolutionResized() {
const pixiRenderer = this._runtimeGameRenderer
? this._runtimeGameRenderer.getPIXIRenderer()
: null;
if (!pixiRenderer) {
return;
}
const runtimeGame = this._runtimeScene.getGame();
// TODO (3D): should this be done for each individual layer?
// Especially if we remove _pixiContainer entirely.
this._pixiContainer.scale.x =
pixiRenderer.width / runtimeGame.getGameResolutionWidth();
this._pixiContainer.scale.y =
pixiRenderer.height / runtimeGame.getGameResolutionHeight();
for (const runtimeLayer of this._runtimeScene._orderedLayers) {
runtimeLayer.getRenderer().onGameResolutionResized();
}
}
onSceneUnloaded() {
// TODO (3D): call the method with the same name on RuntimeLayers so they can dispose?
}
render() {
const runtimeGameRenderer = this._runtimeGameRenderer;
if (!runtimeGameRenderer) return;
const pixiRenderer = runtimeGameRenderer.getPIXIRenderer();
if (!pixiRenderer) return;
const threeRenderer = this._threeRenderer;
// If we are in VR, we cannot render like this: we must use the special VR
// rendering method to not display a black screen.
//
// We cannot call it here either - the headset will request a frame to be rendered
// whenever it wants and we must oblige, however this will be called whenever
// we do a step - and we may step multiple times or none at all depending on the
// min/max FPS and possibly other factors, and the headset will not allow that.
//
// It is therefore left to the VR extension to call the VR rendering method whenever
// the headset require a new image, we'll just disable rendering when stepping to
// not interfere with the headset's rendering.
if (threeRenderer && threeRenderer.xr.isPresenting) return;
this._layerRenderingMetrics.rendered2DLayersCount = 0;
this._layerRenderingMetrics.rendered3DLayersCount = 0;
if (threeRenderer) {
// Layered 2D, 3D or 2D+3D rendering.
threeRenderer.info.autoReset = false;
threeRenderer.info.reset();
/** Useful to render the background color. */
let isFirstRender = true;
/**
* true if the last layer rendered 3D objects using Three.js, false otherwise.
* Useful to avoid needlessly resetting the WebGL states between layers (which can be expensive).
*/
let lastRenderWas3D = true;
// Even if no rendering at all has been made already, setting up the Three.js/PixiJS renderers
// might have changed some WebGL states already. Reset the state for the very first frame.
// And, out of caution, keep doing it for every frame.
// TODO (3D): optimization - check if this can be done only on the very first frame.
threeRenderer.resetState();
// Render each layer one by one.
for (let i = 0; i < this._runtimeScene._orderedLayers.length; ++i) {
const runtimeLayer = this._runtimeScene._orderedLayers[i];
if (!runtimeLayer.isVisible()) continue;
const runtimeLayerRenderer = runtimeLayer.getRenderer();
const runtimeLayerRenderingType = runtimeLayer.getRenderingType();
const layerHas3DObjectsToRender = runtimeLayerRenderer.has3DObjects();
if (
runtimeLayerRenderingType ===
gdjs.RuntimeLayerRenderingType.TWO_D ||
!layerHas3DObjectsToRender
) {
// Render a layer with 2D rendering (PixiJS) only if layer is configured as is
// or if there is no 3D object to render.
if (lastRenderWas3D) {
// Ensure the state is clean for PixiJS to render.
threeRenderer.resetState();
pixiRenderer.reset();
}
if (isFirstRender) {
// Render the background color.
pixiRenderer.background.color = this._runtimeScene.getBackgroundColor();
pixiRenderer.background.alpha = 1;
if (this._runtimeScene.getClearCanvas()) pixiRenderer.clear();
isFirstRender = false;
}
if (runtimeLayer.isLightingLayer()) {
// Render the lights on the render texture used then by the lighting Sprite.
runtimeLayerRenderer.renderOnPixiRenderTexture(pixiRenderer);
}
// TODO (2d lights): refactor to remove the need for `getLightingSprite`.
const pixiContainer =
(runtimeLayer.isLightingLayer() &&
runtimeLayerRenderer.getLightingSprite()) ||
runtimeLayerRenderer.getRendererObject();
pixiRenderer.render(pixiContainer, { clear: false });
this._layerRenderingMetrics.rendered2DLayersCount++;
lastRenderWas3D = false;
} else {
// Render a layer with 3D rendering, and possibly some 2D rendering too.
const threeScene = runtimeLayerRenderer.getThreeScene();
const threeCamera = runtimeLayerRenderer.getThreeCamera();
const threeEffectComposer = runtimeLayerRenderer.getThreeEffectComposer();
// Render the 3D objects of this layer.
if (threeScene && threeCamera && threeEffectComposer) {
// TODO (3D) - optimization: do this at the beginning for all layers that are 2d+3d?
// So the second pass is clearer (just rendering 2d or 3d layers without doing PixiJS renders in between).
if (
runtimeLayerRenderingType ===
gdjs.RuntimeLayerRenderingType.TWO_D_PLUS_THREE_D
) {
const layerHas2DObjectsToRender = runtimeLayerRenderer.has2DObjects();
if (layerHas2DObjectsToRender) {
if (lastRenderWas3D) {
// Ensure the state is clean for PixiJS to render.
threeRenderer.resetState();
pixiRenderer.reset();
}
// Do the rendering of the PixiJS objects of the layer on the render texture.
// Then, update the texture of the plane showing the PixiJS rendering,
// so that the 2D rendering made by PixiJS can be shown in the 3D world.
runtimeLayerRenderer.renderOnPixiRenderTexture(pixiRenderer);
runtimeLayerRenderer.updateThreePlaneTextureFromPixiRenderTexture(
// The renderers are needed to find the internal WebGL texture.
threeRenderer,
pixiRenderer
);
this._layerRenderingMetrics.rendered2DLayersCount++;
lastRenderWas3D = false;
}
runtimeLayerRenderer.show2DRenderingPlane(
layerHas2DObjectsToRender
);
}
if (!lastRenderWas3D) {
// It's important to reset the internal WebGL state of PixiJS, then Three.js
// to ensure the 3D rendering is made properly by Three.js
pixiRenderer.reset();
threeRenderer.resetState();
}
if (isFirstRender) {
// Render the background color.
threeRenderer.setClearColor(
this._runtimeScene.getBackgroundColor()
);
threeRenderer.resetState();
if (this._runtimeScene.getClearCanvas()) threeRenderer.clear();
threeScene.background = new THREE.Color(
this._runtimeScene.getBackgroundColor()
);
isFirstRender = false;
} else {
// It's important to set the background to null, as maybe the first rendered
// layer has changed and so the Three.js scene background must be reset.
threeScene.background = null;
}
// Clear the depth as each layer is independent and display on top of the previous one,
// even 3D objects.
threeRenderer.clearDepth();
if (runtimeLayerRenderer.hasPostProcessingPass()) {
threeEffectComposer.render();
} else {
threeRenderer.render(threeScene, threeCamera);
}
this._layerRenderingMetrics.rendered3DLayersCount++;
lastRenderWas3D = true;
}
}
}
const debugContainer = this._runtimeScene
.getDebuggerRenderer()
.getRendererObject();
if (debugContainer) {
threeRenderer.resetState();
pixiRenderer.reset();
pixiRenderer.render(debugContainer);
lastRenderWas3D = false;
}
if (!lastRenderWas3D) {
// Out of caution, reset the WebGL states from PixiJS to start again
// with a 3D rendering on the next frame.
pixiRenderer.reset();
}
// Uncomment to display some debug metrics from Three.js.
// console.log(threeRenderer.info);
} else {
// 2D only rendering.
// Render lights in render textures first.
for (const runtimeLayer of this._runtimeScene._orderedLayers) {
if (runtimeLayer.isLightingLayer()) {
// Render the lights on the render texture used then by the lighting Sprite.
const runtimeLayerRenderer = runtimeLayer.getRenderer();
runtimeLayerRenderer.renderOnPixiRenderTexture(pixiRenderer);
}
}
// this._renderProfileText(); //Uncomment to display profiling times
// Render all the layers then.
// TODO: replace by a loop like in 3D?
pixiRenderer.background.color = this._runtimeScene.getBackgroundColor();
pixiRenderer.render(this._pixiContainer, {
clear: this._runtimeScene.getClearCanvas(),
});
this._layerRenderingMetrics.rendered2DLayersCount++;
}
// synchronize showing the cursor with rendering (useful to reduce
// blinking while switching from in-game cursor)
if (this._showCursorAtNextRender) {
const canvas = runtimeGameRenderer.getCanvas();
if (canvas) canvas.style.cursor = '';
this._showCursorAtNextRender = false;
}
// Uncomment to check the number of 2D&3D rendering done
// console.log(this._layerRenderingMetrics);
}
/**
* Unless you know what you are doing, use the VR extension instead of this function directly.
*
* In VR, only 3D elements can be rendered, 2D cannot.
* This rendering method skips over all 2D layers and elements, and simply renders the 3D content.
* This method is to be called by the XRSession's `requestAnimationFrame` for rendering to
* the headset whenever the headset requests the screen to be drawn. Note that while an XRSession
* is in progress, the regular `requestAnimationFrame` will be disabled. Make sure that whenever you
* enter an XRSession, you:
* - Call this function first and foremost when the XRSession's requestAnimationFrame fires,
* as it is necessary to draw asap as the headset will eventually stop waiting and just draw the
* framebuffer as it is to maintain a constant screen refresh rate, which can be in the middle of
* or even before rendering if we aren't fast enough, leading to screen flashes and bugs.
* - Call GDevelop's step function to give the scene a chance to step after having drawn to the screen
* to allow the game to actually progress, since GDevelop will no longer step by itself with
* `requestAnimationFrame` disabled.
*
* Note to engine developers: `threeRenderer.resetState()` may NOT be called in this function,
* as WebXR modifies the WebGL state in a way that resetting it will cause an improper render
* that will lead to a black screen being displayed in VR mode.
*/
renderForVR() {
const runtimeGameRenderer = this._runtimeGameRenderer;
if (!runtimeGameRenderer) return;
const threeRenderer = this._threeRenderer;
// VR rendering relies on ThreeJS
if (!threeRenderer)
throw new Error('Cannot render a scene with no 3D elements in VR!');
// Render each layer one by one.
let isFirstRender = true;
for (let i = 0; i < this._runtimeScene._orderedLayers.length; ++i) {
const runtimeLayer = this._runtimeScene._orderedLayers[i];
if (!runtimeLayer.isVisible()) continue;
const runtimeLayerRenderer = runtimeLayer.getRenderer();
const runtimeLayerRenderingType = runtimeLayer.getRenderingType();
if (
runtimeLayerRenderingType === gdjs.RuntimeLayerRenderingType.TWO_D ||
!runtimeLayerRenderer.has3DObjects()
)
continue;
// Render a layer with 3D rendering, and no 2D rendering at all for VR.
const threeScene = runtimeLayerRenderer.getThreeScene();
const threeCamera = runtimeLayerRenderer.getThreeCamera();
if (!threeScene || !threeCamera) continue;
if (isFirstRender) {
// Render the background color.
threeRenderer.setClearColor(this._runtimeScene.getBackgroundColor());
if (this._runtimeScene.getClearCanvas()) threeRenderer.clear();
threeScene.background = new THREE.Color(
this._runtimeScene.getBackgroundColor()
);
isFirstRender = false;
} else {
// It's important to set the background to null, as maybe the first rendered
// layer has changed and so the Three.js scene background must be reset.
threeScene.background = null;
}
// Clear the depth as each layer is independent and display on top of the previous one,
// even 3D objects.
threeRenderer.clearDepth();
threeRenderer.render(threeScene, threeCamera);
}
}
_renderProfileText() {
const profiler = this._runtimeScene.getProfiler();
if (!profiler) {
return;
}
if (!this._profilerText) {
this._profilerText = new PIXI.Text(' ', {
align: 'left',
stroke: '#FFF',
strokeThickness: 1,
});
// Add on top of all layers:
this._pixiContainer.addChild(this._profilerText);
}
const average = profiler.getFramesAverageMeasures();
const outputs = [];
gdjs.Profiler.getProfilerSectionTexts('All', average, outputs);
this._profilerText.text = outputs.join('\n');
}
hideCursor(): void {
this._showCursorAtNextRender = false;
const canvas = this._runtimeGameRenderer
? this._runtimeGameRenderer.getCanvas()
: null;
if (canvas) canvas.style.cursor = 'none';
}
showCursor(): void {
this._showCursorAtNextRender = true;
}
getPIXIContainer() {
return this._pixiContainer;
}
getRendererObject() {
return this._pixiContainer;
}
get3DRendererObject() {
// There is no notion of a container for all 3D objects. Each 3D object is
// added to their layer container.
return null;
}
/** @deprecated use `runtimeGame.getRenderer().getPIXIRenderer()` instead */
getPIXIRenderer() {
return this._runtimeGameRenderer
? this._runtimeGameRenderer.getPIXIRenderer()
: null;
}
setLayerIndex(layer: gdjs.RuntimeLayer, index: float): void {
const layerPixiRenderer: gdjs.LayerPixiRenderer = layer.getRenderer();
let layerPixiObject:
| PIXI.Container
| PIXI.Sprite
| null = layerPixiRenderer.getRendererObject();
if (layer.isLightingLayer()) {
// TODO (2d lights): refactor to remove the need for `getLightingSprite`.
layerPixiObject = layerPixiRenderer.getLightingSprite();
}
if (!layerPixiObject) {
return;
}
if (this._pixiContainer.children.indexOf(layerPixiObject) === index) {
return;
}
this._pixiContainer.removeChild(layerPixiObject);
this._pixiContainer.addChildAt(layerPixiObject, index);
}
}
// Register the class to let the engine use it.
export type RuntimeSceneRenderer = gdjs.RuntimeScenePixiRenderer;
export const RuntimeSceneRenderer = gdjs.RuntimeScenePixiRenderer;
}