littlejsengine 1.18.29 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FAQ.md +841 -815
- package/README.md +211 -199
- package/dist/littlejs.d.ts +4383 -812
- package/dist/littlejs.esm.js +31848 -16680
- package/dist/littlejs.esm.min.js +14 -1
- package/dist/littlejs.js +31230 -16166
- package/dist/littlejs.min.js +13 -1
- package/dist/littlejs.release.js +27419 -15457
- package/package.json +10 -5
- package/plugins/audioEffects.js +375 -0
- package/plugins/box2d.js +2516 -2073
- package/plugins/drawUtilities.js +206 -31
- package/plugins/gltf.js +740 -0
- package/plugins/jsconfig.json +19 -0
- package/plugins/lightSystem.js +719 -330
- package/plugins/math3d.js +911 -0
- package/plugins/medalSystem.js +118 -72
- package/plugins/newgrounds.js +400 -199
- package/plugins/pathFinder.js +208 -175
- package/plugins/pluginExport.js +196 -115
- package/plugins/postProcess.js +284 -166
- package/plugins/render3d.js +4072 -0
- package/plugins/render3dDebug.js +109 -0
- package/plugins/render3dExtras.js +1754 -0
- package/plugins/render3dVoxels.js +502 -0
- package/plugins/textureSheet.js +86 -48
- package/plugins/threejs.js +183 -158
- package/plugins/tweakables.js +411 -0
- package/plugins/tweenSystem.js +646 -526
- package/plugins/uiSystem.js +2024 -1687
- package/plugins/zzfxm.js +8 -15
- package/src/engine.js +345 -194
- package/src/engineAudio.js +1022 -777
- package/src/engineBuild.mjs +289 -253
- package/src/engineDebug.js +912 -735
- package/src/engineDraw.js +1774 -1510
- package/src/engineEditor.js +2431 -0
- package/src/engineExport.js +419 -396
- package/src/engineInput.js +1462 -1293
- package/src/engineLogo.js +4 -3
- package/src/engineMath.js +1274 -1183
- package/src/engineObject.js +674 -551
- package/src/engineParticles.js +583 -532
- package/src/engineRelease.js +67 -45
- package/src/engineSettings.js +808 -726
- package/src/engineTileLayer.js +995 -741
- package/src/engineUtilities.js +32 -13
- package/src/engineWebGL.js +1162 -941
- package/src/jsconfig.json +17 -13
- package/plugins/box2d.wasm.js +0 -630
- package/plugins/box2d.wasm.wasm +0 -0
package/src/engineWebGL.js
CHANGED
|
@@ -1,942 +1,1163 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LittleJS WebGL Interface
|
|
3
|
-
* - WebGL2 rendering engine for high-performance graphics
|
|
4
|
-
* - Batched sprite rendering for drawing thousands of sprites efficiently
|
|
5
|
-
* - Instanced rendering using vertex array objects (VAOs)
|
|
6
|
-
* - Polygon rendering with triangle strip support
|
|
7
|
-
* - Shader system with custom vertex and fragment shaders
|
|
8
|
-
* - Texture management with automatic atlas support
|
|
9
|
-
* - Post-processing effects via framebuffer and shader plugins
|
|
10
|
-
* - Automatic fallback to Canvas2D if WebGL is unavailable
|
|
11
|
-
* - Context loss and restoration handling
|
|
12
|
-
* - Can be disabled with glEnable setting
|
|
13
|
-
* - Advanced users can create custom shaders and render targets
|
|
14
|
-
* @namespace WebGL
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
'use strict';
|
|
18
|
-
|
|
19
|
-
/** The WebGL canvas which appears below the main canvas
|
|
20
|
-
* @type {HTMLCanvasElement}
|
|
21
|
-
* @memberof WebGL */
|
|
22
|
-
let glCanvas;
|
|
23
|
-
|
|
24
|
-
/** WebGL2 context for `glCanvas`
|
|
25
|
-
* @type {WebGL2RenderingContext}
|
|
26
|
-
* @memberof WebGL */
|
|
27
|
-
let glContext;
|
|
28
|
-
|
|
29
|
-
/** Should WebGL be setup with anti-aliasing? must be set before calling engineInit
|
|
30
|
-
* @type {boolean}
|
|
31
|
-
* @memberof WebGL */
|
|
32
|
-
let glAntialias = true;
|
|
33
|
-
|
|
34
|
-
// WebGL internal variables not exposed to documentation
|
|
35
|
-
let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos, glInstancedVAO, glPolyVAO, glFramebuffer, glRenderTarget, glCanBeEnabled = true;
|
|
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
|
-
glContext.
|
|
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
|
-
glContext
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
* @param {
|
|
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
|
-
function
|
|
394
|
-
{
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
glContext.
|
|
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
|
-
* @param {
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
{
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
if (
|
|
501
|
-
{
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
//
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
* @
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
* @param {
|
|
548
|
-
* @param {
|
|
549
|
-
* @
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
* @param {
|
|
587
|
-
* @
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
{
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
* @
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
{
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
* @
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
*
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
//
|
|
774
|
-
const
|
|
775
|
-
const
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
{
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
//
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1
|
+
/**
|
|
2
|
+
* LittleJS WebGL Interface
|
|
3
|
+
* - WebGL2 rendering engine for high-performance graphics
|
|
4
|
+
* - Batched sprite rendering for drawing thousands of sprites efficiently
|
|
5
|
+
* - Instanced rendering using vertex array objects (VAOs)
|
|
6
|
+
* - Polygon rendering with triangle strip support
|
|
7
|
+
* - Shader system with custom vertex and fragment shaders
|
|
8
|
+
* - Texture management with automatic atlas support
|
|
9
|
+
* - Post-processing effects via framebuffer and shader plugins
|
|
10
|
+
* - Automatic fallback to Canvas2D if WebGL is unavailable
|
|
11
|
+
* - Context loss and restoration handling
|
|
12
|
+
* - Can be disabled with glEnable setting
|
|
13
|
+
* - Advanced users can create custom shaders and render targets
|
|
14
|
+
* @namespace WebGL
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
/** The WebGL canvas which appears below the main canvas
|
|
20
|
+
* @type {HTMLCanvasElement}
|
|
21
|
+
* @memberof WebGL */
|
|
22
|
+
let glCanvas;
|
|
23
|
+
|
|
24
|
+
/** WebGL2 context for `glCanvas`
|
|
25
|
+
* @type {WebGL2RenderingContext}
|
|
26
|
+
* @memberof WebGL */
|
|
27
|
+
let glContext;
|
|
28
|
+
|
|
29
|
+
/** Should WebGL be setup with anti-aliasing? must be set before calling engineInit
|
|
30
|
+
* @type {boolean}
|
|
31
|
+
* @memberof WebGL */
|
|
32
|
+
let glAntialias = true;
|
|
33
|
+
|
|
34
|
+
// WebGL internal variables not exposed to documentation
|
|
35
|
+
let glMipmappedTextures = new WeakSet, glMipmapsUntilTarget = new WeakSet, glMipmapsStale = new Set, glPremultipliedTextures = new WeakSet, glShaderPremultiplied, glEnableBeforeLoss = true, glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos = new Set, glInstancedVAO, glPolyVAO, glFramebuffer, glRenderTarget, glShaderObjects = [], glCustomShader, glBatchShader, glProgramCustom, glTransform, glRenderTargetSaved, glUniformLocations = new WeakMap, glCanBeEnabled = true;
|
|
36
|
+
// ANDed onto every packed color as a draw is queued; the light system's shadow pass sets 0xff000000
|
|
37
|
+
// to draw everything black with its alpha kept (rgbaInt packs alpha in the top byte)
|
|
38
|
+
let glColorMask = -1;
|
|
39
|
+
// ORed onto the additive color of every quad and onto every poly point's color as a draw is queued; the light system's
|
|
40
|
+
// emissive pass sets a grey with the mask at 0xff000000, so a draw comes out that grey in its own shape
|
|
41
|
+
let glColorAdditive = 0;
|
|
42
|
+
// a texture drawn into with the canvas's own transform and its size, the light system's lightmap while its pass
|
|
43
|
+
// runs: a target set and ended inside it, like a tile layer redrawn in a renderLight, goes back to it, not the canvas
|
|
44
|
+
let glRenderTargetBase;
|
|
45
|
+
// set by the light system's shadow pass, which draws the world with its own camera, so a screen space
|
|
46
|
+
// WebGL draw in a render() is skipped instead of landing somewhere in the world
|
|
47
|
+
let glSkipScreenSpace = false;
|
|
48
|
+
|
|
49
|
+
// WebGL internal constants
|
|
50
|
+
const gl_ARRAY_BUFFER_SIZE = 5e5;
|
|
51
|
+
const gl_INDICES_PER_INSTANCE = 11;
|
|
52
|
+
const gl_INSTANCE_BYTE_STRIDE = gl_INDICES_PER_INSTANCE * 4;
|
|
53
|
+
const gl_MAX_INSTANCES = gl_ARRAY_BUFFER_SIZE / gl_INSTANCE_BYTE_STRIDE | 0;
|
|
54
|
+
const gl_INDICES_PER_POLY_VERTEX = 3;
|
|
55
|
+
const gl_POLY_VERTEX_BYTE_STRIDE = gl_INDICES_PER_POLY_VERTEX * 4;
|
|
56
|
+
const gl_MAX_POLY_VERTEXES = gl_ARRAY_BUFFER_SIZE / gl_POLY_VERTEX_BYTE_STRIDE | 0;
|
|
57
|
+
|
|
58
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
59
|
+
|
|
60
|
+
// the sprite vertex shader, shared by the engine's program and every Shader so one vertex layout fits all
|
|
61
|
+
const gl_VERTEX_SOURCE =
|
|
62
|
+
'#version 300 es\n' + // specify GLSL ES version
|
|
63
|
+
'precision highp float;'+ // use highp for accuracy
|
|
64
|
+
'uniform mat4 m;'+ // transform matrix
|
|
65
|
+
'layout(location=0) in vec2 g;'+ // in: geometry
|
|
66
|
+
'layout(location=1) in vec4 p;'+ // in: position/size
|
|
67
|
+
'layout(location=2) in vec4 u;'+ // in: uvs
|
|
68
|
+
'layout(location=3) in vec4 c;'+ // in: color
|
|
69
|
+
'layout(location=4) in vec4 a;'+ // in: additiveColor
|
|
70
|
+
'layout(location=5) in float r;'+// in: rotation
|
|
71
|
+
'out vec2 v,l;'+ // out: uv, and 0 to 1 across the sprite for a Shader's localUV
|
|
72
|
+
'out vec4 d,e;'+ // out: color, additiveColor
|
|
73
|
+
'void main(){'+ // shader entry point
|
|
74
|
+
'vec2 s=(g-.5)*p.zw;'+ // get size offset
|
|
75
|
+
'gl_Position=m*vec4(p.xy+s*cos(r)-vec2(-s.y,s)*sin(r),1,1);'+ // transform position
|
|
76
|
+
'v=mix(u.xw,u.zy,g);'+ // pass uv to fragment shader
|
|
77
|
+
'l=g;d=c;e=a;'+ // pass local uv and colors to fragment shader
|
|
78
|
+
'}'; // end of shader
|
|
79
|
+
|
|
80
|
+
// the end of every sprite fragment shader, the engine's and each Shader's: t is the surface color
|
|
81
|
+
// a render target and a smooth image hold premultiplied color, so their batches blend with ONE and premultiply here,
|
|
82
|
+
// giving what a straight texel gives, clamped as the blend clamps it: exactly when the additive alpha is 0 or
|
|
83
|
+
// the texel is clear or opaque, and close otherwise, since the straight color is not kept to be multiplied
|
|
84
|
+
const gl_FRAGMENT_TINT_SOURCE =
|
|
85
|
+
'c=t*d+e;'+ // modulate by color plus additive
|
|
86
|
+
'if(premultipliedTexture){'+ // a render target or a smooth image
|
|
87
|
+
'c.a=min(c.a,1.);'+ // the alpha the blend uses
|
|
88
|
+
'c.rgb=min(t.rgb*d.rgb*min(d.a+e.a,1.)+e.rgb*c.a,c.a);}';
|
|
89
|
+
|
|
90
|
+
// Initialize WebGL, called automatically by the engine
|
|
91
|
+
function glInit(rootElement)
|
|
92
|
+
{
|
|
93
|
+
if (!glEnable || headlessMode)
|
|
94
|
+
{
|
|
95
|
+
glCanBeEnabled = false;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// create the canvas and textures
|
|
100
|
+
glCanvas = document.createElement('canvas');
|
|
101
|
+
glContext = glCanvas.getContext('webgl2', {antialias:glAntialias});
|
|
102
|
+
|
|
103
|
+
if (!glContext)
|
|
104
|
+
{
|
|
105
|
+
console.warn('WebGL2 not supported, falling back to 2D canvas rendering!');
|
|
106
|
+
glCanvas = glContext = undefined;
|
|
107
|
+
glEnable = false;
|
|
108
|
+
glCanBeEnabled = false;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// attach the WebGL canvas;
|
|
113
|
+
rootElement.appendChild(glCanvas);
|
|
114
|
+
|
|
115
|
+
// startup webgl, and make the textures of any texture infos made before it
|
|
116
|
+
initWebGL();
|
|
117
|
+
for (const info of glTextureInfos)
|
|
118
|
+
info.glTexture ||= glCreateTexture(info.image, info.wrap);
|
|
119
|
+
|
|
120
|
+
// setup context lost and restore handlers
|
|
121
|
+
glCanvas.addEventListener('webglcontextlost', (e)=>
|
|
122
|
+
{
|
|
123
|
+
glEnableBeforeLoss = glEnable;
|
|
124
|
+
glEnable = false; // disable WebGL rendering
|
|
125
|
+
glCanvas.style.display = 'none'; // hide the gl canvas
|
|
126
|
+
e.preventDefault(); // prevent default to allow restoration
|
|
127
|
+
LOG('WebGL context lost! Switching to Canvas2d rendering.');
|
|
128
|
+
|
|
129
|
+
// remove WebGL textures
|
|
130
|
+
for (const info of glTextureInfos)
|
|
131
|
+
info.glTexture = undefined;
|
|
132
|
+
glActiveTexture = undefined;
|
|
133
|
+
// every Shader compiles again on its next draw, and the first flush after restore picks its program again
|
|
134
|
+
for (const shader of glShaderObjects)
|
|
135
|
+
shader.program = undefined;
|
|
136
|
+
glBatchShader = undefined;
|
|
137
|
+
glProgramCustom = true;
|
|
138
|
+
glUniformLocations = new WeakMap; // the programs those belonged to are gone
|
|
139
|
+
// drop any partially-filled batch so the next glFlush doesn't
|
|
140
|
+
// upload stale glBatchCount against fresh empty buffers on restore
|
|
141
|
+
glBatchCount = 0;
|
|
142
|
+
glPolyMode = false;
|
|
143
|
+
pluginList.forEach(plugin=>plugin.glContextLost?.());
|
|
144
|
+
});
|
|
145
|
+
glCanvas.addEventListener('webglcontextrestored', ()=>
|
|
146
|
+
{
|
|
147
|
+
glEnable = glEnableBeforeLoss; // WebGL rendering as it was before the loss
|
|
148
|
+
glCanvas.style.display = ''; // show the gl canvas
|
|
149
|
+
LOG('WebGL context restored, reinitializing...');
|
|
150
|
+
|
|
151
|
+
// reinit WebGL and restore textures
|
|
152
|
+
glMipmappedTextures = new WeakSet;
|
|
153
|
+
glMipmapsUntilTarget = new WeakSet;
|
|
154
|
+
glMipmapsStale.clear();
|
|
155
|
+
glPremultipliedTextures = new WeakSet; // the tile layers draw into their new textures again below
|
|
156
|
+
initWebGL();
|
|
157
|
+
for (const info of glTextureInfos)
|
|
158
|
+
info.glTexture = glCreateTexture(info.image, info.wrap);
|
|
159
|
+
pluginList.forEach(plugin=>plugin.glContextRestored?.());
|
|
160
|
+
|
|
161
|
+
// a tile layer drawn on the GPU only had its tiles in the lost texture, it draws them again
|
|
162
|
+
for (const o of engineObjects)
|
|
163
|
+
o instanceof TileLayer && o.isUsingWebGL && !o.destroyed && o.redraw();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
function initWebGL()
|
|
167
|
+
{
|
|
168
|
+
// setup instanced rendering shader program
|
|
169
|
+
glShader = glCreateProgram(gl_VERTEX_SOURCE,
|
|
170
|
+
'#version 300 es\n' + // specify GLSL ES version
|
|
171
|
+
'precision highp float;'+ // use highp for accuracy
|
|
172
|
+
'uniform sampler2D s;'+ // texture
|
|
173
|
+
'uniform bool premultipliedTexture;'+ // is the texture premultiplied, a render target or a smooth image
|
|
174
|
+
'in vec2 v;'+ // in: uv
|
|
175
|
+
'in vec4 d,e;'+ // in: color, additiveColor
|
|
176
|
+
'out vec4 c;'+ // out: color
|
|
177
|
+
'void main(){'+ // shader entry point
|
|
178
|
+
'vec4 t=texture(s,v);'+ // sample the texture
|
|
179
|
+
gl_FRAGMENT_TINT_SOURCE + // apply color and additive
|
|
180
|
+
'}' // end of shader
|
|
181
|
+
);
|
|
182
|
+
glShaderPremultiplied = false; // a new program starts with its uniforms at 0
|
|
183
|
+
|
|
184
|
+
// setup poly rendering shaders
|
|
185
|
+
glPolyShader = glCreateProgram(
|
|
186
|
+
'#version 300 es\n' + // specify GLSL ES version
|
|
187
|
+
'precision highp float;'+ // use highp for better accuracy
|
|
188
|
+
'uniform mat4 m;'+ // transform matrix
|
|
189
|
+
'in vec2 p;'+ // in: position
|
|
190
|
+
'in vec4 c;'+ // in: color
|
|
191
|
+
'out vec4 d;'+ // out: color
|
|
192
|
+
'void main(){'+ // shader entry point
|
|
193
|
+
'gl_Position=m*vec4(p,1,1);'+ // transform position
|
|
194
|
+
'd=c;'+ // pass color to fragment shader
|
|
195
|
+
'}' // end of shader
|
|
196
|
+
,
|
|
197
|
+
'#version 300 es\n' + // specify GLSL ES version
|
|
198
|
+
'precision highp float;'+ // use highp for better accuracy
|
|
199
|
+
'in vec4 d;'+ // in: color
|
|
200
|
+
'out vec4 c;'+ // out: color
|
|
201
|
+
'void main(){'+ // shader entry point
|
|
202
|
+
'c=d;'+ // set color
|
|
203
|
+
'}' // end of shader
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// init buffers
|
|
207
|
+
const glInstanceData = new ArrayBuffer(gl_ARRAY_BUFFER_SIZE);
|
|
208
|
+
glPositionData = new Float32Array(glInstanceData);
|
|
209
|
+
glColorData = new Uint32Array(glInstanceData);
|
|
210
|
+
glArrayBuffer = glContext.createBuffer();
|
|
211
|
+
glGeometryBuffer = glContext.createBuffer();
|
|
212
|
+
glFramebuffer = glContext.createFramebuffer();
|
|
213
|
+
glBatchCount = 0;
|
|
214
|
+
|
|
215
|
+
// create the geometry buffer, triangle strip square
|
|
216
|
+
const geometry = new Float32Array([0,0,1,0,0,1,1,1]);
|
|
217
|
+
glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
|
|
218
|
+
glContext.bufferData(glContext.ARRAY_BUFFER, geometry, glContext.STATIC_DRAW);
|
|
219
|
+
|
|
220
|
+
let offset, shader, stride;
|
|
221
|
+
const initVertexAttrib = (name, type, typeSize, size, divisor=0)=>
|
|
222
|
+
{
|
|
223
|
+
const location = glContext.getAttribLocation(shader, name);
|
|
224
|
+
const normalize = typeSize === 1;
|
|
225
|
+
const fixedStride = typeSize && stride;
|
|
226
|
+
glContext.enableVertexAttribArray(location);
|
|
227
|
+
glContext.vertexAttribPointer(location, size, type, normalize, fixedStride, offset);
|
|
228
|
+
glContext.vertexAttribDivisor(location, divisor);
|
|
229
|
+
offset += size*typeSize;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// setup VAO for instanced rendering
|
|
233
|
+
glInstancedVAO = glContext.createVertexArray();
|
|
234
|
+
glContext.bindVertexArray(glInstancedVAO);
|
|
235
|
+
|
|
236
|
+
// configure instanced vertex attributes
|
|
237
|
+
offset = 0, shader = glShader, stride = gl_INSTANCE_BYTE_STRIDE;
|
|
238
|
+
glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
|
|
239
|
+
// geometry, its own packed buffer: typeSize 0 gives stride 0 and does not advance the offset
|
|
240
|
+
initVertexAttrib('g', glContext.FLOAT, 0, 2);
|
|
241
|
+
glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
|
|
242
|
+
glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
|
|
243
|
+
initVertexAttrib('p', glContext.FLOAT, 4, 4, 1); // position & size
|
|
244
|
+
initVertexAttrib('u', glContext.FLOAT, 4, 4, 1); // texture coords
|
|
245
|
+
initVertexAttrib('c', glContext.UNSIGNED_BYTE, 1, 4, 1); // color
|
|
246
|
+
initVertexAttrib('a', glContext.UNSIGNED_BYTE, 1, 4, 1); // additiveColor
|
|
247
|
+
initVertexAttrib('r', glContext.FLOAT, 4, 1, 1); // rotation
|
|
248
|
+
|
|
249
|
+
// setup VAO for poly rendering
|
|
250
|
+
glPolyVAO = glContext.createVertexArray();
|
|
251
|
+
glContext.bindVertexArray(glPolyVAO);
|
|
252
|
+
|
|
253
|
+
// configure poly vertex attributes
|
|
254
|
+
offset = 0, shader = glPolyShader, stride = gl_POLY_VERTEX_BYTE_STRIDE;
|
|
255
|
+
initVertexAttrib('p', glContext.FLOAT, 4, 2); // position
|
|
256
|
+
initVertexAttrib('c', glContext.UNSIGNED_BYTE, 1, 4); // color
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function glSetInstancedMode(force=false)
|
|
261
|
+
{
|
|
262
|
+
if (!force && !glPolyMode) return;
|
|
263
|
+
|
|
264
|
+
// setup instanced mode
|
|
265
|
+
glFlush();
|
|
266
|
+
glPolyMode = false;
|
|
267
|
+
glContext.useProgram(glShader);
|
|
268
|
+
glContext.bindVertexArray(glInstancedVAO);
|
|
269
|
+
glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer); // a VAO does not keep this binding
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function glSetPolyMode()
|
|
273
|
+
{
|
|
274
|
+
if (glPolyMode) return;
|
|
275
|
+
|
|
276
|
+
// setup poly mode
|
|
277
|
+
glFlush();
|
|
278
|
+
glPolyMode = true;
|
|
279
|
+
glContext.useProgram(glPolyShader);
|
|
280
|
+
glContext.bindVertexArray(glPolyVAO);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Setup WebGL render each frame, called automatically by engine
|
|
284
|
+
// Also used by tile layer rendering when redrawing tiles
|
|
285
|
+
function glPreRender(clear=true)
|
|
286
|
+
{
|
|
287
|
+
if (!glEnable || !glContext) return;
|
|
288
|
+
|
|
289
|
+
ASSERT(!glBatchCount, 'glPreRender called with unflushed batch.');
|
|
290
|
+
|
|
291
|
+
// mainCanvasSize is css pixels, the backing store is scaled by the pixel
|
|
292
|
+
// ratio, render targets are offscreen so they are never scaled
|
|
293
|
+
const dpr = glRenderTarget ? 1 : getCanvasPixelRatio();
|
|
294
|
+
const bufferSizeX = mainCanvasSize.x * dpr | 0;
|
|
295
|
+
const bufferSizeY = mainCanvasSize.y * dpr | 0;
|
|
296
|
+
if (!glRenderTarget)
|
|
297
|
+
{
|
|
298
|
+
// set to same size as main canvas, only when it changes because
|
|
299
|
+
// setting it reallocates the drawing buffer and invalidates the frame
|
|
300
|
+
if (glCanvas.width !== bufferSizeX || glCanvas.height !== bufferSizeY)
|
|
301
|
+
{
|
|
302
|
+
glCanvas.width = bufferSizeX;
|
|
303
|
+
glCanvas.height = bufferSizeY;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
glContext.viewport(0, 0, bufferSizeX, bufferSizeY);
|
|
307
|
+
clear && glClearCanvas();
|
|
308
|
+
|
|
309
|
+
// build the transform matrix
|
|
310
|
+
const s = vec2(2*cameraScale).divide(mainCanvasSize);
|
|
311
|
+
if (glRenderTarget)
|
|
312
|
+
s.y = -s.y; // invert y when using render target
|
|
313
|
+
const rotatedCam = cameraPos.rotate(-cameraAngle);
|
|
314
|
+
const p = vec2(-1).subtract(rotatedCam.multiply(s));
|
|
315
|
+
const ca = cos(cameraAngle);
|
|
316
|
+
const sa = sin(cameraAngle);
|
|
317
|
+
// column major; the shaders send (x, y, 1, 1), so the third column adds 1 to x and y and p takes it back off:
|
|
318
|
+
// clip = s * (world turned by -cameraAngle - the camera turned the same way), z = 1
|
|
319
|
+
const transform = [
|
|
320
|
+
s.x * ca, s.y * sa, 0, 0,
|
|
321
|
+
-s.x * sa, s.y * ca, 0, 0,
|
|
322
|
+
1, 1, 1, 0,
|
|
323
|
+
p.x, p.y, 0, 1];
|
|
324
|
+
glTransform = transform;
|
|
325
|
+
|
|
326
|
+
// set the same transform matrix for both shaders
|
|
327
|
+
const initUniform = (program, uniform, value)=>
|
|
328
|
+
{
|
|
329
|
+
glContext.useProgram(program);
|
|
330
|
+
glContext.uniformMatrix4fv(glUniformLocation(program, uniform), false, value);
|
|
331
|
+
}
|
|
332
|
+
initUniform(glPolyShader, 'm', transform);
|
|
333
|
+
initUniform(glShader, 'm', transform);
|
|
334
|
+
|
|
335
|
+
// set the active texture
|
|
336
|
+
glContext.activeTexture(glContext.TEXTURE0);
|
|
337
|
+
if (textureInfos[0])
|
|
338
|
+
{
|
|
339
|
+
glActiveTexture = textureInfos[0].glTexture;
|
|
340
|
+
glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// rebind the array buffer
|
|
344
|
+
glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
|
|
345
|
+
|
|
346
|
+
// start with additive blending off
|
|
347
|
+
glAdditive = glBatchAdditive = false;
|
|
348
|
+
|
|
349
|
+
// force it to set instanced mode
|
|
350
|
+
glSetInstancedMode(true);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Clear the canvas or render target to canvasClearColor
|
|
354
|
+
* @memberof WebGL */
|
|
355
|
+
function glClearCanvas()
|
|
356
|
+
{
|
|
357
|
+
if (!glContext) return;
|
|
358
|
+
|
|
359
|
+
// clear using the canvasClearColor, premultiplied like everything the blend writes
|
|
360
|
+
const color = canvasClearColor;
|
|
361
|
+
glContext.clearColor(color.r*color.a, color.g*color.a, color.b*color.a, color.a);
|
|
362
|
+
glContext.clear(glContext.COLOR_BUFFER_BIT);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Set the WebGL texture, called automatically if using multiple textures
|
|
366
|
+
* - This may also flush the gl buffer resulting in more draw calls and worse performance
|
|
367
|
+
* @param {WebGLTexture} texture
|
|
368
|
+
* @memberof WebGL */
|
|
369
|
+
function glSetTexture(texture)
|
|
370
|
+
{
|
|
371
|
+
if (!glContext) return;
|
|
372
|
+
if (texture !== glActiveTexture)
|
|
373
|
+
{
|
|
374
|
+
// must flush cache with the old texture to set a new one
|
|
375
|
+
glFlush();
|
|
376
|
+
glActiveTexture = texture;
|
|
377
|
+
glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
|
|
378
|
+
}
|
|
379
|
+
glMipmapsStale.size && glUpdateMipmaps(texture); // bound already or not, drawn into since
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// make the smaller levels of a texture again if it was drawn into since, it must be the bound texture
|
|
383
|
+
function glUpdateMipmaps(texture)
|
|
384
|
+
{
|
|
385
|
+
if (!glMipmapsStale.has(texture)) return;
|
|
386
|
+
glMipmapsStale.delete(texture);
|
|
387
|
+
glContext.generateMipmap(glContext.TEXTURE_2D);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// a texture's wrap as its two WebGL modes, across and down: true repeats both and false clamps both, or a pair of
|
|
391
|
+
// modes as a glTF sampler gives them; the pairs are WebGL's fixed values, shared so a draw makes no array
|
|
392
|
+
const glWrapRepeat = Object.freeze([10497, 10497]), glWrapClamp = Object.freeze([33071, 33071]);
|
|
393
|
+
function glWrapModes(wrap)
|
|
394
|
+
{ return isArray(wrap) ? wrap : wrap ? glWrapRepeat : glWrapClamp; }
|
|
395
|
+
|
|
396
|
+
/** Set the wrap mode on an existing WebGL texture
|
|
397
|
+
* Flushes the current batch only if the texture is the active one
|
|
398
|
+
* @param {WebGLTexture} texture
|
|
399
|
+
* @param {boolean|Array<number>} [wrap] - true for REPEAT, false for CLAMP_TO_EDGE, or the WebGL modes across and
|
|
400
|
+
* down, like [gl.CLAMP_TO_EDGE, gl.MIRRORED_REPEAT]
|
|
401
|
+
* @memberof WebGL */
|
|
402
|
+
function glSetTextureWrap(texture, wrap=true)
|
|
403
|
+
{
|
|
404
|
+
if (!glContext || !texture) return;
|
|
405
|
+
|
|
406
|
+
// flush only if changing wrap on the currently bound texture
|
|
407
|
+
const isCurrent = texture === glActiveTexture;
|
|
408
|
+
if (isCurrent)
|
|
409
|
+
glFlush();
|
|
410
|
+
else
|
|
411
|
+
glContext.bindTexture(glContext.TEXTURE_2D, texture);
|
|
412
|
+
|
|
413
|
+
const [wrapS, wrapT] = glWrapModes(wrap);
|
|
414
|
+
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_S, wrapS);
|
|
415
|
+
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_T, wrapT);
|
|
416
|
+
|
|
417
|
+
if (!isCurrent && glActiveTexture)
|
|
418
|
+
glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** Compile WebGL shader of the given type, will throw errors if in debug mode
|
|
422
|
+
* @param {string} source
|
|
423
|
+
* @param {number} type
|
|
424
|
+
* @return {WebGLShader}
|
|
425
|
+
* @memberof WebGL */
|
|
426
|
+
function glCompileShader(source, type)
|
|
427
|
+
{
|
|
428
|
+
if (!glContext) return;
|
|
429
|
+
|
|
430
|
+
// build the shader
|
|
431
|
+
const shader = glContext.createShader(type);
|
|
432
|
+
glContext.shaderSource(shader, source);
|
|
433
|
+
glContext.compileShader(shader);
|
|
434
|
+
|
|
435
|
+
// check for errors
|
|
436
|
+
if (debug && !glContext.getShaderParameter(shader, glContext.COMPILE_STATUS))
|
|
437
|
+
throw glContext.getShaderInfoLog(shader);
|
|
438
|
+
return shader;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** Create WebGL program with given shaders
|
|
442
|
+
* @param {string} vsSource
|
|
443
|
+
* @param {string} fsSource
|
|
444
|
+
* @return {WebGLProgram}
|
|
445
|
+
* @memberof WebGL */
|
|
446
|
+
function glCreateProgram(vsSource, fsSource)
|
|
447
|
+
{
|
|
448
|
+
if (!glContext) return;
|
|
449
|
+
|
|
450
|
+
// build the program
|
|
451
|
+
const program = glContext.createProgram();
|
|
452
|
+
glContext.attachShader(program, glCompileShader(vsSource, glContext.VERTEX_SHADER));
|
|
453
|
+
glContext.attachShader(program, glCompileShader(fsSource, glContext.FRAGMENT_SHADER));
|
|
454
|
+
glContext.linkProgram(program);
|
|
455
|
+
|
|
456
|
+
// check for errors
|
|
457
|
+
if (debug && !glContext.getProgramParameter(program, glContext.LINK_STATUS))
|
|
458
|
+
throw glContext.getProgramInfoLog(program);
|
|
459
|
+
return program;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// a uniform location, looked up once per program
|
|
463
|
+
function glUniformLocation(program, name)
|
|
464
|
+
{
|
|
465
|
+
let cache = glUniformLocations.get(program);
|
|
466
|
+
cache || glUniformLocations.set(program, cache = {});
|
|
467
|
+
return cache[name] ??= glContext.getUniformLocation(program, name);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// a Shader's 2D program, compiled the first time a batch needs it: the snippet's mainImage gives the surface
|
|
471
|
+
// color, then the sprite's color and additive color apply as the engine's own fragment shader does
|
|
472
|
+
function glShaderProgram(shader)
|
|
473
|
+
{
|
|
474
|
+
return shader.program ||= glCreateProgram(gl_VERTEX_SOURCE,
|
|
475
|
+
'#version 300 es\n' +
|
|
476
|
+
'precision highp float;' +
|
|
477
|
+
'uniform sampler2D iChannel0;' + // the texture
|
|
478
|
+
'uniform vec3 iResolution;' + // canvas size in pixels
|
|
479
|
+
'uniform float iTime;' + // engine time
|
|
480
|
+
'uniform bool premultipliedTexture;' + // is the texture premultiplied, a render target or a smooth image
|
|
481
|
+
'in vec2 v,l;in vec4 d,e;out vec4 c;\n' + // a define needs its own line
|
|
482
|
+
'#define localUV l\n' +
|
|
483
|
+
shader.fragmentCode + '\n' +
|
|
484
|
+
'void main(){vec4 t;mainImage(t,v);' + gl_FRAGMENT_TINT_SOURCE + '}');
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** Create WebGL texture from an image and init the texture settings
|
|
488
|
+
* Restores the active texture when done
|
|
489
|
+
* @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap} [image]
|
|
490
|
+
* @param {boolean|Array<number>} [wrap] - true for REPEAT, false for CLAMP_TO_EDGE, or the WebGL modes across and down
|
|
491
|
+
* @return {WebGLTexture}
|
|
492
|
+
* @memberof WebGL */
|
|
493
|
+
function glCreateTexture(image, wrap=false)
|
|
494
|
+
{
|
|
495
|
+
if (!glContext) return;
|
|
496
|
+
|
|
497
|
+
// build the texture
|
|
498
|
+
const texture = glContext.createTexture();
|
|
499
|
+
let mipMap = false;
|
|
500
|
+
if (image?.width)
|
|
501
|
+
{
|
|
502
|
+
glSetTextureData(texture, image);
|
|
503
|
+
glContext.bindTexture(glContext.TEXTURE_2D, texture);
|
|
504
|
+
// WebGL2 makes mipmaps at any size, a texture that becomes a render target keeps them only at powers of two
|
|
505
|
+
mipMap = !tilesPixelated;
|
|
506
|
+
if (mipMap && !(isPowerOfTwo(image.width) && isPowerOfTwo(image.height)))
|
|
507
|
+
glMipmapsUntilTarget.add(texture);
|
|
508
|
+
}
|
|
509
|
+
else
|
|
510
|
+
{
|
|
511
|
+
// create a white texture
|
|
512
|
+
const whitePixel = new Uint8Array([255, 255, 255, 255]);
|
|
513
|
+
glContext.bindTexture(glContext.TEXTURE_2D, texture);
|
|
514
|
+
glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, 1, 1, 0, glContext.RGBA, glContext.UNSIGNED_BYTE, whitePixel);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// set texture filtering
|
|
518
|
+
const magFilter = tilesPixelated ? glContext.NEAREST : glContext.LINEAR;
|
|
519
|
+
const minFilter = mipMap ? glContext.LINEAR_MIPMAP_LINEAR : magFilter;
|
|
520
|
+
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MAG_FILTER, magFilter);
|
|
521
|
+
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, minFilter);
|
|
522
|
+
const [wrapS, wrapT] = glWrapModes(wrap);
|
|
523
|
+
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_S, wrapS);
|
|
524
|
+
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_T, wrapT);
|
|
525
|
+
if (mipMap)
|
|
526
|
+
{
|
|
527
|
+
glContext.generateMipmap(glContext.TEXTURE_2D);
|
|
528
|
+
glMipmappedTextures.add(texture);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// rebind active texture
|
|
532
|
+
glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
|
|
533
|
+
return texture;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** Deletes a WebGL texture
|
|
537
|
+
* @param {WebGLTexture} [texture]
|
|
538
|
+
* @memberof WebGL */
|
|
539
|
+
function glDeleteTexture(texture)
|
|
540
|
+
{
|
|
541
|
+
if (!glContext) return;
|
|
542
|
+
|
|
543
|
+
glContext.deleteTexture(texture);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** Set WebGL texture data from an image, restores the active texture when done
|
|
547
|
+
* @param {WebGLTexture} texture
|
|
548
|
+
* @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap} image
|
|
549
|
+
* @memberof WebGL */
|
|
550
|
+
function glSetTextureData(texture, image)
|
|
551
|
+
{
|
|
552
|
+
if (!glContext) return;
|
|
553
|
+
|
|
554
|
+
// build the texture, after drawing what was queued with the old image
|
|
555
|
+
ASSERT(image?.width > 0, 'Invalid image data.');
|
|
556
|
+
texture === glActiveTexture && glFlush();
|
|
557
|
+
glContext.bindTexture(glContext.TEXTURE_2D, texture);
|
|
558
|
+
if (typeof ImageBitmap != 'undefined' && image instanceof ImageBitmap)
|
|
559
|
+
{
|
|
560
|
+
// WebGL ignores the unpack flag below for a bitmap and uploads it as it was made, so it is drawn onto a
|
|
561
|
+
// canvas first, which reads either kind of bitmap right and uploads as the flag says
|
|
562
|
+
const canvas = new OffscreenCanvas(image.width, image.height);
|
|
563
|
+
canvas.getContext('2d').drawImage(image, 0, 0);
|
|
564
|
+
image = canvas;
|
|
565
|
+
}
|
|
566
|
+
// smooth filtering mixes a texel with its see through neighbors, right only for premultiplied color, or the
|
|
567
|
+
// edges go dark; pixel art is sampled a texel at a time, and uploads straight color as it always has
|
|
568
|
+
const premultiply = !tilesPixelated;
|
|
569
|
+
glContext.pixelStorei(glContext.UNPACK_PREMULTIPLY_ALPHA_WEBGL, premultiply);
|
|
570
|
+
glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, image);
|
|
571
|
+
glContext.pixelStorei(glContext.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
|
572
|
+
if (premultiply)
|
|
573
|
+
glPremultipliedTextures.add(texture); // drawn with the premultiplied blend, as a render target is
|
|
574
|
+
else
|
|
575
|
+
glPremultipliedTextures.delete(texture); // an image uploads straight color, even into a used render target
|
|
576
|
+
|
|
577
|
+
// keep mipmaps in sync with new level 0 data, for any texture that has them
|
|
578
|
+
if (glMipmappedTextures.has(texture))
|
|
579
|
+
glContext.generateMipmap(glContext.TEXTURE_2D);
|
|
580
|
+
|
|
581
|
+
// rebind active texture
|
|
582
|
+
glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** Internal: tells WebGL to create or update the glTexture and start tracking it, TextureInfo calls it
|
|
586
|
+
* @param {TextureInfo} textureInfo
|
|
587
|
+
* @ignore */
|
|
588
|
+
function glRegisterTextureInfo(textureInfo)
|
|
589
|
+
{
|
|
590
|
+
if (headlessMode) return;
|
|
591
|
+
|
|
592
|
+
// add texture info to tracking list even if gl is not enabled
|
|
593
|
+
glTextureInfos.add(textureInfo);
|
|
594
|
+
|
|
595
|
+
if (!glContext) return;
|
|
596
|
+
|
|
597
|
+
// create or set the texture data
|
|
598
|
+
if (textureInfo.glTexture)
|
|
599
|
+
glSetTextureData(textureInfo.glTexture, textureInfo.image);
|
|
600
|
+
else
|
|
601
|
+
textureInfo.glTexture = glCreateTexture(textureInfo.image, textureInfo.wrap);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/** Internal: tells WebGL to destroy the glTexture and stop tracking it, TextureInfo calls it
|
|
605
|
+
* @param {TextureInfo} textureInfo
|
|
606
|
+
* @ignore */
|
|
607
|
+
function glUnregisterTextureInfo(textureInfo)
|
|
608
|
+
{
|
|
609
|
+
if (headlessMode) return;
|
|
610
|
+
|
|
611
|
+
// delete texture info from tracking list even if gl is not enabled
|
|
612
|
+
glTextureInfos.delete(textureInfo);
|
|
613
|
+
|
|
614
|
+
// unset and destroy the texture, drawing what is batched with it first, since deleting unbinds it
|
|
615
|
+
const glTexture = textureInfo.glTexture;
|
|
616
|
+
textureInfo.glTexture = undefined;
|
|
617
|
+
if (glTexture && glTexture === glActiveTexture)
|
|
618
|
+
{
|
|
619
|
+
glFlush();
|
|
620
|
+
glActiveTexture = undefined; // so nothing binds the deleted texture again
|
|
621
|
+
}
|
|
622
|
+
glMipmapsStale.delete(glTexture);
|
|
623
|
+
glPremultipliedTextures.delete(glTexture);
|
|
624
|
+
glDeleteTexture(glTexture);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** Draw all sprites and clear out the buffer, called automatically by the system whenever necessary
|
|
628
|
+
* @memberof WebGL */
|
|
629
|
+
function glFlush()
|
|
630
|
+
{
|
|
631
|
+
if (glEnable && glContext && glBatchCount)
|
|
632
|
+
{
|
|
633
|
+
// a render target holds premultiplied color, a batch drawing one takes its color as it is
|
|
634
|
+
// glSetTexture flushes on every change, so the whole batch drew with the bound texture
|
|
635
|
+
const premultiplied = !glPolyMode && glPremultipliedTextures.has(glActiveTexture);
|
|
636
|
+
|
|
637
|
+
// set blend mode
|
|
638
|
+
const sourceBlend = premultiplied ? glContext.ONE : glContext.SRC_ALPHA;
|
|
639
|
+
const destBlend = glBatchAdditive ? glContext.ONE : glContext.ONE_MINUS_SRC_ALPHA;
|
|
640
|
+
glContext.blendFuncSeparate(sourceBlend, destBlend, glContext.ONE, destBlend);
|
|
641
|
+
glContext.enable(glContext.BLEND);
|
|
642
|
+
|
|
643
|
+
// a Shader's program for this batch, or the engine's own again after one
|
|
644
|
+
if (!glPolyMode && (glBatchShader || glProgramCustom))
|
|
645
|
+
{
|
|
646
|
+
const program = glBatchShader ? glShaderProgram(glBatchShader) : glShader;
|
|
647
|
+
glContext.useProgram(program);
|
|
648
|
+
glProgramCustom = !!glBatchShader;
|
|
649
|
+
if (glBatchShader)
|
|
650
|
+
{
|
|
651
|
+
const uniform = (name)=> glUniformLocation(program, name);
|
|
652
|
+
glContext.uniformMatrix4fv(uniform('m'), false, glTransform);
|
|
653
|
+
glContext.uniform1f(uniform('iTime'), time);
|
|
654
|
+
// a render target is the size glPreRender gave its viewport
|
|
655
|
+
const width = glRenderTarget ? mainCanvasSize.x : glCanvas.width;
|
|
656
|
+
const height = glRenderTarget ? mainCanvasSize.y : glCanvas.height;
|
|
657
|
+
glContext.uniform3f(uniform('iResolution'), width, height, 1);
|
|
658
|
+
glContext.uniform1i(uniform('premultipliedTexture'), +premultiplied);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
if (!glPolyMode && !glBatchShader && glShaderPremultiplied !== premultiplied)
|
|
662
|
+
{
|
|
663
|
+
// the engine's program is the one in use, and it keeps the flag until it changes
|
|
664
|
+
glContext.uniform1i(glUniformLocation(glShader, 'premultipliedTexture'), +premultiplied);
|
|
665
|
+
glShaderPremultiplied = premultiplied;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const byteLength = glBatchCount *
|
|
669
|
+
(glPolyMode ? gl_INDICES_PER_POLY_VERTEX : gl_INDICES_PER_INSTANCE);
|
|
670
|
+
glContext.bufferSubData(glContext.ARRAY_BUFFER, 0, glPositionData, 0, byteLength);
|
|
671
|
+
|
|
672
|
+
// draw the batch
|
|
673
|
+
if (glPolyMode)
|
|
674
|
+
glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, glBatchCount);
|
|
675
|
+
else
|
|
676
|
+
glContext.drawArraysInstanced(glContext.TRIANGLE_STRIP, 0, 4, glBatchCount);
|
|
677
|
+
++drawCount;
|
|
678
|
+
primitiveCount += glBatchCount;
|
|
679
|
+
glBatchCount = 0;
|
|
680
|
+
}
|
|
681
|
+
glBatchAdditive = glAdditive;
|
|
682
|
+
glBatchShader = glCustomShader;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/** Flush any sprites still in the buffer and copy the WebGL canvas to a 2D context
|
|
686
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
|
|
687
|
+
* @memberof WebGL */
|
|
688
|
+
function glCopyToContext(context)
|
|
689
|
+
{
|
|
690
|
+
if (!glEnable || !glContext) return;
|
|
691
|
+
|
|
692
|
+
glFlush();
|
|
693
|
+
context.drawImage(glCanvas, 0, 0);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/** Set anti-aliasing for WebGL canvas
|
|
697
|
+
* Must be called before engineInit
|
|
698
|
+
* @param {boolean} [antialias]
|
|
699
|
+
* @memberof WebGL */
|
|
700
|
+
function glSetAntialias(antialias=true)
|
|
701
|
+
{
|
|
702
|
+
ASSERT(!glCanvas, 'must be called before engineInit');
|
|
703
|
+
glAntialias = antialias;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/** Add a sprite to the gl draw list, used by all gl draw functions
|
|
707
|
+
* @param {number} x
|
|
708
|
+
* @param {number} y
|
|
709
|
+
* @param {number} sizeX
|
|
710
|
+
* @param {number} sizeY
|
|
711
|
+
* @param {number} [angle]
|
|
712
|
+
* @param {number} [uv0X]
|
|
713
|
+
* @param {number} [uv0Y]
|
|
714
|
+
* @param {number} [uv1X]
|
|
715
|
+
* @param {number} [uv1Y]
|
|
716
|
+
* @param {number} [rgba=-1] - white is -1
|
|
717
|
+
* @param {number} [rgbaAdditive=0] - black is 0
|
|
718
|
+
* @memberof WebGL */
|
|
719
|
+
function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgba=-1, rgbaAdditive=0)
|
|
720
|
+
{
|
|
721
|
+
// flush if there is not enough room or if different blend mode
|
|
722
|
+
if (glBatchCount >= gl_MAX_INSTANCES || glBatchAdditive !== glAdditive || glBatchShader !== glCustomShader)
|
|
723
|
+
glFlush();
|
|
724
|
+
glSetInstancedMode();
|
|
725
|
+
|
|
726
|
+
let offset = glBatchCount++ * gl_INDICES_PER_INSTANCE;
|
|
727
|
+
glPositionData[offset++] = x;
|
|
728
|
+
glPositionData[offset++] = y;
|
|
729
|
+
glPositionData[offset++] = sizeX;
|
|
730
|
+
glPositionData[offset++] = sizeY;
|
|
731
|
+
glPositionData[offset++] = uv0X;
|
|
732
|
+
glPositionData[offset++] = uv0Y;
|
|
733
|
+
glPositionData[offset++] = uv1X;
|
|
734
|
+
glPositionData[offset++] = uv1Y;
|
|
735
|
+
glColorData[offset++] = rgba & glColorMask;
|
|
736
|
+
glColorData[offset++] = rgbaAdditive & glColorMask | glColorAdditive;
|
|
737
|
+
glPositionData[offset++] = angle;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/** Add an untextured rect to the gl draw list
|
|
741
|
+
* Zeroes the uvs and rgba so the texture contribution multiplies to 0,
|
|
742
|
+
* then carries the real color in the additive slot. Works regardless of
|
|
743
|
+
* which texture is currently bound.
|
|
744
|
+
* @param {number} x
|
|
745
|
+
* @param {number} y
|
|
746
|
+
* @param {number} sizeX
|
|
747
|
+
* @param {number} sizeY
|
|
748
|
+
* @param {number} angle
|
|
749
|
+
* @param {number} rgba - color as 32-bit integer
|
|
750
|
+
* @memberof WebGL */
|
|
751
|
+
function glDrawUntextured(x, y, sizeX, sizeY, angle, rgba)
|
|
752
|
+
{
|
|
753
|
+
glDraw(x, y, sizeX, sizeY, angle, 0, 0, 0, 0, 0, rgba);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/** Transform and add a polygon to the gl draw list
|
|
757
|
+
* @param {Array<Vector2>} points - Array of Vector2 points
|
|
758
|
+
* @param {number} rgba - Color of the polygon as a 32-bit integer
|
|
759
|
+
* @param {number} x
|
|
760
|
+
* @param {number} y
|
|
761
|
+
* @param {number} sx
|
|
762
|
+
* @param {number} sy
|
|
763
|
+
* @param {number} angle
|
|
764
|
+
* @param {boolean} [tristrip] - should tristrip algorithm be used
|
|
765
|
+
* @memberof WebGL */
|
|
766
|
+
function glDrawPointsTransform(points, rgba, x, y, sx, sy, angle, tristrip=true)
|
|
767
|
+
{
|
|
768
|
+
const pointsOut = [];
|
|
769
|
+
const sa = sin(-angle);
|
|
770
|
+
const ca = cos(-angle);
|
|
771
|
+
for (const p of points)
|
|
772
|
+
{
|
|
773
|
+
// transform the point
|
|
774
|
+
const px = p.x*sx;
|
|
775
|
+
const py = p.y*sy;
|
|
776
|
+
pointsOut.push(vec2(x + ca*px - sa*py, y + sa*px + ca*py));
|
|
777
|
+
}
|
|
778
|
+
const drawPoints = tristrip ? glPolyStrip(pointsOut) : pointsOut;
|
|
779
|
+
glDrawPoints(drawPoints, rgba);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/** Transform and add a polygon's outline to the gl draw list
|
|
783
|
+
* @param {Array<Vector2>} points - Array of Vector2 points
|
|
784
|
+
* @param {number} rgba - Color of the polygon as a 32-bit integer
|
|
785
|
+
* @param {number} lineWidth - Width of the outline
|
|
786
|
+
* @param {number} x
|
|
787
|
+
* @param {number} y
|
|
788
|
+
* @param {number} sx
|
|
789
|
+
* @param {number} sy
|
|
790
|
+
* @param {number} angle
|
|
791
|
+
* @param {boolean} [wrap] - Should the outline connect the first and last points
|
|
792
|
+
* @memberof WebGL */
|
|
793
|
+
function glDrawOutlineTransform(points, rgba, lineWidth, x, y, sx, sy, angle, wrap=true)
|
|
794
|
+
{
|
|
795
|
+
const outlinePoints = glMakeOutline(points, lineWidth, wrap);
|
|
796
|
+
glDrawPointsTransform(outlinePoints, rgba, x, y, sx, sy, angle, false);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/** Add a list of points to the gl draw list
|
|
800
|
+
* @param {Array<Vector2>} points - Array of Vector2 points in tri strip order
|
|
801
|
+
* @param {number} rgba - Color as a 32-bit integer
|
|
802
|
+
* @memberof WebGL */
|
|
803
|
+
function glDrawPoints(points, rgba)
|
|
804
|
+
{ glDrawPolyStrip(points, rgba); }
|
|
805
|
+
|
|
806
|
+
/** Add a list of colored points to the gl draw list
|
|
807
|
+
* @param {Array<Vector2>} points - Array of Vector2 points in tri strip order
|
|
808
|
+
* @param {Array<number>} pointColors - Array of 32-bit integer colors
|
|
809
|
+
* @memberof WebGL */
|
|
810
|
+
function glDrawColoredPoints(points, pointColors)
|
|
811
|
+
{ glDrawPolyStrip(points, 0, pointColors); }
|
|
812
|
+
|
|
813
|
+
// queue a triangle strip in the poly batch with degenerate verts at start and end,
|
|
814
|
+
// one color for all its points or one each
|
|
815
|
+
function glDrawPolyStrip(points, rgba, pointColors)
|
|
816
|
+
{
|
|
817
|
+
if (!glEnable || points.length < 3)
|
|
818
|
+
return; // needs at least 3 points to have area
|
|
819
|
+
|
|
820
|
+
// flush if there is not enough room or if different blend mode
|
|
821
|
+
const vertCount = points.length + 2;
|
|
822
|
+
if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
|
|
823
|
+
glFlush();
|
|
824
|
+
ASSERT(vertCount < gl_MAX_POLY_VERTEXES, 'poly exceeds max batch size');
|
|
825
|
+
if (vertCount >= gl_MAX_POLY_VERTEXES) return; // release-build safety net
|
|
826
|
+
glSetPolyMode();
|
|
827
|
+
|
|
828
|
+
// setup triangle strip with degenerate verts at start and end
|
|
829
|
+
let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
|
|
830
|
+
for (let i = vertCount; i--;)
|
|
831
|
+
{
|
|
832
|
+
const j = clamp(i-1, 0, vertCount-3);
|
|
833
|
+
const point = points[j];
|
|
834
|
+
const color = pointColors ? pointColors[j] : rgba;
|
|
835
|
+
glPositionData[offset++] = point.x;
|
|
836
|
+
glPositionData[offset++] = point.y;
|
|
837
|
+
glColorData[offset++] = color & glColorMask | glColorAdditive;
|
|
838
|
+
}
|
|
839
|
+
glBatchCount += vertCount;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/** Set the WebGL render target to the given texture or back to the canvas
|
|
843
|
+
* - What is drawn into a texture is stored premultiplied, and draws of that texture blend it as such
|
|
844
|
+
* @param {WebGLTexture} [texture] - a texture or undefined to use normal glCanvas
|
|
845
|
+
* @param {boolean} [clear] - should the render target be cleared, to canvasClearColor, CLEAR_BLACK for a transparent one
|
|
846
|
+
* @memberof WebGL */
|
|
847
|
+
function glSetRenderTarget(texture, clear=false)
|
|
848
|
+
{
|
|
849
|
+
// what was batched so far draws where it was meant to, before the target changes
|
|
850
|
+
glFlush();
|
|
851
|
+
const previousTarget = glRenderTarget;
|
|
852
|
+
if (texture)
|
|
853
|
+
{
|
|
854
|
+
if (glMipmapsUntilTarget.has(texture))
|
|
855
|
+
{
|
|
856
|
+
// a layer at a size other than a power of two draws without mipmaps as it always has,
|
|
857
|
+
// so they are not made again after every redraw, the 3D renderer still makes its own
|
|
858
|
+
glMipmapsUntilTarget.delete(texture);
|
|
859
|
+
glMipmappedTextures.delete(texture);
|
|
860
|
+
glContext.bindTexture(glContext.TEXTURE_2D, texture);
|
|
861
|
+
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, glContext.LINEAR);
|
|
862
|
+
glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
|
|
863
|
+
}
|
|
864
|
+
glPremultipliedTextures.add(texture); // the blend writes premultiplied color into it
|
|
865
|
+
// coming from the canvas, keep its transform and blend mode to put back after
|
|
866
|
+
glRenderTarget || (glRenderTargetSaved = [glTransform, glAdditive]);
|
|
867
|
+
glRenderTarget = texture;
|
|
868
|
+
glContext.bindFramebuffer(glContext.FRAMEBUFFER, glFramebuffer);
|
|
869
|
+
glContext.framebufferTexture2D(glContext.FRAMEBUFFER,
|
|
870
|
+
glContext.COLOR_ATTACHMENT0, glContext.TEXTURE_2D, texture, 0);
|
|
871
|
+
glPreRender(clear);
|
|
872
|
+
}
|
|
873
|
+
else
|
|
874
|
+
{
|
|
875
|
+
glRenderTarget = undefined;
|
|
876
|
+
if (glRenderTargetBase)
|
|
877
|
+
{
|
|
878
|
+
// back to the texture the canvas transform was drawing into, at its own size
|
|
879
|
+
const [baseTexture, baseSize] = glRenderTargetBase;
|
|
880
|
+
glContext.bindFramebuffer(glContext.FRAMEBUFFER, glFramebuffer);
|
|
881
|
+
glContext.framebufferTexture2D(glContext.FRAMEBUFFER,
|
|
882
|
+
glContext.COLOR_ATTACHMENT0, glContext.TEXTURE_2D, baseTexture, 0);
|
|
883
|
+
glContext.viewport(0, 0, baseSize.x, baseSize.y);
|
|
884
|
+
}
|
|
885
|
+
else
|
|
886
|
+
{
|
|
887
|
+
glContext.bindFramebuffer(glContext.FRAMEBUFFER, null);
|
|
888
|
+
|
|
889
|
+
// use the backing store size, mainCanvasSize is css pixels and may
|
|
890
|
+
// still be the render target's size when unwinding a layer redraw
|
|
891
|
+
glContext.viewport(0, 0, glCanvas.width, glCanvas.height);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// the canvas's own transform and blend mode again, the target set its own
|
|
895
|
+
if (glRenderTargetSaved)
|
|
896
|
+
{
|
|
897
|
+
[glTransform, glAdditive] = glRenderTargetSaved;
|
|
898
|
+
glRenderTargetSaved = undefined;
|
|
899
|
+
for (const program of [glPolyShader, glShader])
|
|
900
|
+
{
|
|
901
|
+
glContext.useProgram(program);
|
|
902
|
+
glContext.uniformMatrix4fv(glUniformLocation(program, 'm'), false, glTransform);
|
|
903
|
+
}
|
|
904
|
+
glBatchAdditive = glAdditive;
|
|
905
|
+
glSetInstancedMode(true);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// a target with mipmaps drawn into has only its top level new, the smaller levels are made again from it
|
|
910
|
+
// the next time it is drawn from
|
|
911
|
+
if (previousTarget && previousTarget !== texture && glMipmappedTextures.has(previousTarget))
|
|
912
|
+
glMipmapsStale.add(previousTarget);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/** Clear out a rectangle area of the WebGL canvas or render target
|
|
916
|
+
* - In framebuffer pixels from the bottom left: backing store pixels on the canvas, texture pixels in a
|
|
917
|
+
* render target
|
|
918
|
+
* @param {number} x
|
|
919
|
+
* @param {number} y
|
|
920
|
+
* @param {number} width
|
|
921
|
+
* @param {number} height
|
|
922
|
+
* @memberof WebGL */
|
|
923
|
+
function glClearRect(x, y, width, height)
|
|
924
|
+
{
|
|
925
|
+
if (!glEnable) return;
|
|
926
|
+
glFlush(); // tiles batched before the clear go down before it, not over what replaces them
|
|
927
|
+
|
|
928
|
+
// Enable scissor test to clear only the specified area
|
|
929
|
+
glContext.enable(glContext.SCISSOR_TEST);
|
|
930
|
+
glContext.scissor(x, y, width, height);
|
|
931
|
+
glContext.clearColor(0, 0, 0, 0);
|
|
932
|
+
glContext.clear(glContext.COLOR_BUFFER_BIT);
|
|
933
|
+
glContext.disable(glContext.SCISSOR_TEST);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
937
|
+
|
|
938
|
+
// WebGL internal function to convert polygon to outline triangle strip
|
|
939
|
+
function glMakeOutline(points, width, wrap=true)
|
|
940
|
+
{
|
|
941
|
+
if (points.length < 2)
|
|
942
|
+
return [];
|
|
943
|
+
|
|
944
|
+
const halfWidth = width / 2;
|
|
945
|
+
const strip = [];
|
|
946
|
+
const n = points.length;
|
|
947
|
+
const e = 1e-6;
|
|
948
|
+
// miter ratio cap (dimensionless, matches SVG/Canvas2D convention)
|
|
949
|
+
const miterLimit = 10;
|
|
950
|
+
for (let i = 0; i < n; i++)
|
|
951
|
+
{
|
|
952
|
+
// for each vertex, calculate normal based on adjacent edges
|
|
953
|
+
const prev = points[wrap ? (i - 1 + n) % n : max(i - 1, 0)];
|
|
954
|
+
const curr = points[i];
|
|
955
|
+
const next = points[wrap ? (i + 1) % n : min(i + 1, n - 1)];
|
|
956
|
+
|
|
957
|
+
// direction from previous to current
|
|
958
|
+
const dx1 = curr.x - prev.x;
|
|
959
|
+
const dy1 = curr.y - prev.y;
|
|
960
|
+
const len1 = (dx1*dx1 + dy1*dy1)**.5;
|
|
961
|
+
|
|
962
|
+
// direction from current to next
|
|
963
|
+
const dx2 = next.x - curr.x;
|
|
964
|
+
const dy2 = next.y - curr.y;
|
|
965
|
+
const len2 = (dx2*dx2 + dy2*dy2)**.5;
|
|
966
|
+
|
|
967
|
+
if (len1 < e && len2 < e)
|
|
968
|
+
continue; // skip degenerate point
|
|
969
|
+
|
|
970
|
+
// calculate perpendicular normals for each edge
|
|
971
|
+
const nx1 = len1 > e ? -dy1 / len1 : 0;
|
|
972
|
+
const ny1 = len1 > e ? dx1 / len1 : 0;
|
|
973
|
+
const nx2 = len2 > e ? -dy2 / len2 : 0;
|
|
974
|
+
const ny2 = len2 > e ? dx2 / len2 : 0;
|
|
975
|
+
|
|
976
|
+
// average the normals for miter
|
|
977
|
+
let nx = nx1 + nx2;
|
|
978
|
+
let ny = ny1 + ny2;
|
|
979
|
+
const nlen = (nx*nx + ny*ny)**.5;
|
|
980
|
+
if (nlen < e)
|
|
981
|
+
{
|
|
982
|
+
// 180 degree turn - use perpendicular
|
|
983
|
+
nx = nx1;
|
|
984
|
+
ny = ny1;
|
|
985
|
+
}
|
|
986
|
+
else
|
|
987
|
+
{
|
|
988
|
+
// calculate miter length
|
|
989
|
+
nx /= nlen;
|
|
990
|
+
ny /= nlen;
|
|
991
|
+
const dot = nx1 * nx + ny1 * ny;
|
|
992
|
+
if (dot > e)
|
|
993
|
+
{
|
|
994
|
+
// scale normal by miter length, clamped to miterLimit
|
|
995
|
+
const miterLength = min(1 / dot, miterLimit);
|
|
996
|
+
nx *= miterLength;
|
|
997
|
+
ny *= miterLength;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// create inner and outer points along the normal
|
|
1002
|
+
const inner = vec2(curr.x - nx * halfWidth, curr.y - ny * halfWidth);
|
|
1003
|
+
const outer = vec2(curr.x + nx * halfWidth, curr.y + ny * halfWidth);
|
|
1004
|
+
strip.push(inner);
|
|
1005
|
+
strip.push(outer);
|
|
1006
|
+
}
|
|
1007
|
+
if (strip.length > 1 && wrap)
|
|
1008
|
+
{
|
|
1009
|
+
// close the loop
|
|
1010
|
+
strip.push(strip[0]);
|
|
1011
|
+
strip.push(strip[1]);
|
|
1012
|
+
}
|
|
1013
|
+
return strip;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// WebGL internal function to convert polys to tri strips
|
|
1017
|
+
function glPolyStrip(points)
|
|
1018
|
+
{
|
|
1019
|
+
// validate input
|
|
1020
|
+
if (points.length < 3)
|
|
1021
|
+
return [];
|
|
1022
|
+
|
|
1023
|
+
// cross product helper: (b-a) x (c-a)
|
|
1024
|
+
const cross = (a,b,c)=> (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
|
|
1025
|
+
|
|
1026
|
+
// calculate signed area of polygon
|
|
1027
|
+
const signedArea = (poly)=>
|
|
1028
|
+
{
|
|
1029
|
+
let area = 0;
|
|
1030
|
+
for (let i = poly.length; i--;)
|
|
1031
|
+
{
|
|
1032
|
+
const j = (i+1) % poly.length;
|
|
1033
|
+
area += poly[i].cross(poly[j]);
|
|
1034
|
+
}
|
|
1035
|
+
return area;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// ensure counter-clockwise winding (slice first so we don't mutate caller's array)
|
|
1039
|
+
if (signedArea(points) < 0)
|
|
1040
|
+
points = points.slice().reverse();
|
|
1041
|
+
|
|
1042
|
+
// a convex polygon, which every circle, ellipse and regular polygon is, zigzags between its two sides
|
|
1043
|
+
// into one strip with no bridges, so it skips the ear clipping below and draws with a fifth of the vertices
|
|
1044
|
+
// convex is every corner turning left, and the edges going right then left only once each way round, since
|
|
1045
|
+
// a star listed point by point also turns left at every corner but winds around twice
|
|
1046
|
+
const e = 1e-9, n = points.length;
|
|
1047
|
+
let convex = true, flips = 0, lastSide = 0;
|
|
1048
|
+
for (let i = 0; convex && i < n; ++i)
|
|
1049
|
+
{
|
|
1050
|
+
const a = points[(i + n - 1) % n], b = points[i], dx = points[(i + 1) % n].x - b.x;
|
|
1051
|
+
convex = cross(a, b, points[(i + 1) % n]) > -e;
|
|
1052
|
+
const side = dx > e ? 1 : dx < -e ? -1 : 0;
|
|
1053
|
+
if (side && side !== lastSide)
|
|
1054
|
+
lastSide && ++flips, lastSide = side;
|
|
1055
|
+
}
|
|
1056
|
+
if (convex && flips <= 2)
|
|
1057
|
+
{
|
|
1058
|
+
const strip = [points[0]];
|
|
1059
|
+
for (let i = 1, j = n - 1; i <= j; ++i, --j)
|
|
1060
|
+
{
|
|
1061
|
+
strip.push(points[i]);
|
|
1062
|
+
i === j || strip.push(points[j]);
|
|
1063
|
+
}
|
|
1064
|
+
return strip;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// check if point is inside triangle
|
|
1068
|
+
const pointInTriangle = (p, a, b, c)=>
|
|
1069
|
+
{
|
|
1070
|
+
const c1 = cross(a, b, p);
|
|
1071
|
+
const c2 = cross(b, c, p);
|
|
1072
|
+
const c3 = cross(c, a, p);
|
|
1073
|
+
const negative = (c1<-e?1:0) + (c2<-e?1:0) + (c3<-e?1:0);
|
|
1074
|
+
const positive = (c1> e?1:0) + (c2> e?1:0) + (c3> e?1:0);
|
|
1075
|
+
return !(negative && positive);
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
// ear clipping triangulation
|
|
1079
|
+
const indices = [];
|
|
1080
|
+
for (let i = 0; i < points.length; ++i)
|
|
1081
|
+
indices[i] = i;
|
|
1082
|
+
const triangles = [];
|
|
1083
|
+
let attempts = 0;
|
|
1084
|
+
const maxAttempts = points.length ** 2 + 100;
|
|
1085
|
+
while (indices.length > 3 && attempts++ < maxAttempts)
|
|
1086
|
+
{
|
|
1087
|
+
let foundEar = false;
|
|
1088
|
+
for (let i = 0; i < indices.length; i++)
|
|
1089
|
+
{
|
|
1090
|
+
const i0 = indices[(i + indices.length - 1) % indices.length];
|
|
1091
|
+
const i1 = indices[i];
|
|
1092
|
+
const i2 = indices[(i + 1) % indices.length];
|
|
1093
|
+
const a = points[i0], b = points[i1], c = points[i2];
|
|
1094
|
+
|
|
1095
|
+
// check if convex
|
|
1096
|
+
if (cross(a, b, c) < e) continue;
|
|
1097
|
+
|
|
1098
|
+
// check if any other point is inside
|
|
1099
|
+
let hasInside = false;
|
|
1100
|
+
for (let j = 0; j < indices.length; j++)
|
|
1101
|
+
{
|
|
1102
|
+
const k = indices[j];
|
|
1103
|
+
if (k === i0 || k === i1 || k === i2) continue;
|
|
1104
|
+
|
|
1105
|
+
const p = points[k];
|
|
1106
|
+
hasInside = pointInTriangle(p, a, b, c);
|
|
1107
|
+
if (hasInside) break;
|
|
1108
|
+
}
|
|
1109
|
+
if (hasInside) continue;
|
|
1110
|
+
|
|
1111
|
+
// found valid ear
|
|
1112
|
+
triangles.push([i0, i1, i2]);
|
|
1113
|
+
indices.splice(i, 1);
|
|
1114
|
+
foundEar = true;
|
|
1115
|
+
break;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// fallback for degenerate cases
|
|
1119
|
+
if (!foundEar)
|
|
1120
|
+
{
|
|
1121
|
+
let worstIndex = -1, worstValue = Infinity;
|
|
1122
|
+
for (let i = 0; i < indices.length; i++)
|
|
1123
|
+
{
|
|
1124
|
+
const i0 = indices[(i + indices.length - 1) % indices.length];
|
|
1125
|
+
const i1 = indices[i];
|
|
1126
|
+
const i2 = indices[(i + 1) % indices.length];
|
|
1127
|
+
const value = abs(cross(points[i0], points[i1], points[i2]));
|
|
1128
|
+
if (value < worstValue)
|
|
1129
|
+
{
|
|
1130
|
+
worstValue = value;
|
|
1131
|
+
worstIndex = i;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
if (worstIndex < 0) break;
|
|
1135
|
+
|
|
1136
|
+
const i0 = indices[(worstIndex + indices.length - 1) % indices.length];
|
|
1137
|
+
const i1 = indices[worstIndex];
|
|
1138
|
+
const i2 = indices[(worstIndex + 1) % indices.length];
|
|
1139
|
+
triangles.push([i0, i1, i2]);
|
|
1140
|
+
indices.splice(worstIndex, 1);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// add final triangle
|
|
1145
|
+
if (indices.length === 3)
|
|
1146
|
+
triangles.push([indices[0], indices[1], indices[2]]);
|
|
1147
|
+
if (!triangles.length)
|
|
1148
|
+
return [];
|
|
1149
|
+
|
|
1150
|
+
// convert triangles to triangle strip with degenerate connectors
|
|
1151
|
+
const strip = [];
|
|
1152
|
+
let [a0, b0, c0] = triangles[0];
|
|
1153
|
+
strip.push(points[a0], points[b0], points[c0]);
|
|
1154
|
+
for (let i = 1; i < triangles.length; i++)
|
|
1155
|
+
{
|
|
1156
|
+
// add degenerate bridge from last vertex to first of new triangle
|
|
1157
|
+
const [a, b, c] = triangles[i];
|
|
1158
|
+
strip.push(points[c0], points[a]);
|
|
1159
|
+
strip.push(points[a], points[b], points[c]);
|
|
1160
|
+
c0 = c;
|
|
1161
|
+
}
|
|
1162
|
+
return strip;
|
|
942
1163
|
}
|