littlejsengine 1.18.28 → 1.19.3
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 +5 -0
- package/README.md +207 -199
- package/dist/littlejs.d.ts +2099 -48
- package/dist/littlejs.esm.js +16251 -10315
- package/dist/littlejs.esm.min.js +13 -1
- package/dist/littlejs.js +15747 -9888
- package/dist/littlejs.min.js +13 -1
- package/dist/littlejs.release.js +15719 -9867
- package/package.json +1 -1
- package/plugins/audioEffects.js +371 -0
- package/plugins/lightSystem.js +5 -3
- package/plugins/math3d.js +870 -0
- package/plugins/medalSystem.js +280 -280
- package/plugins/pluginExport.js +181 -115
- package/plugins/postProcess.js +241 -166
- package/plugins/render3d.js +4006 -0
- package/plugins/textureSheet.js +458 -458
- package/plugins/threejs.js +6 -1
- package/plugins/tweenSystem.js +528 -526
- package/plugins/zzfxm.js +159 -166
- package/src/engine.js +172 -137
- package/src/engineAudio.js +918 -775
- package/src/engineBuild.mjs +3 -0
- package/src/engineDebug.js +15 -8
- package/src/engineDraw.js +1672 -1510
- package/src/engineExport.js +407 -396
- package/src/engineInput.js +1 -1
- package/src/engineLogo.js +4 -3
- package/src/engineMath.js +69 -0
- package/src/engineObject.js +560 -551
- package/src/engineSettings.js +759 -725
- package/src/engineTileLayer.js +3 -3
- package/src/engineUtilities.js +13 -0
- package/src/engineWebGL.js +1005 -941
package/src/engineWebGL.js
CHANGED
|
@@ -1,942 +1,1006 @@
|
|
|
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
|
-
// WebGL internal constants
|
|
38
|
-
const gl_ARRAY_BUFFER_SIZE = 5e5;
|
|
39
|
-
const gl_INDICES_PER_INSTANCE = 11;
|
|
40
|
-
const gl_INSTANCE_BYTE_STRIDE = gl_INDICES_PER_INSTANCE * 4;
|
|
41
|
-
const gl_MAX_INSTANCES = gl_ARRAY_BUFFER_SIZE / gl_INSTANCE_BYTE_STRIDE | 0;
|
|
42
|
-
const gl_INDICES_PER_POLY_VERTEX = 3;
|
|
43
|
-
const gl_POLY_VERTEX_BYTE_STRIDE = gl_INDICES_PER_POLY_VERTEX * 4;
|
|
44
|
-
const gl_MAX_POLY_VERTEXES = gl_ARRAY_BUFFER_SIZE / gl_POLY_VERTEX_BYTE_STRIDE | 0;
|
|
45
|
-
|
|
46
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
47
|
-
|
|
48
|
-
// Initialize WebGL, called automatically by the engine
|
|
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
|
-
'#version 300 es\n' + // specify GLSL ES version
|
|
154
|
-
'precision highp float;'+ // use highp for better accuracy
|
|
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
|
-
glContext.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
initVertexAttrib('
|
|
211
|
-
initVertexAttrib('c', glContext.UNSIGNED_BYTE, 1, 4); // color
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function
|
|
227
|
-
{
|
|
228
|
-
if (glPolyMode) return;
|
|
229
|
-
|
|
230
|
-
// setup
|
|
231
|
-
glFlush();
|
|
232
|
-
glPolyMode =
|
|
233
|
-
glContext.useProgram(
|
|
234
|
-
glContext.bindVertexArray(
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
const
|
|
260
|
-
const
|
|
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
|
-
glContext.
|
|
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
|
-
* @param {string}
|
|
369
|
-
* @
|
|
370
|
-
* @
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
glContext.
|
|
379
|
-
glContext.
|
|
380
|
-
|
|
381
|
-
// check for errors
|
|
382
|
-
if (debug && !glContext.
|
|
383
|
-
throw glContext.
|
|
384
|
-
return
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/** Create WebGL
|
|
388
|
-
*
|
|
389
|
-
* @param {
|
|
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
|
-
glContext.
|
|
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
|
-
|
|
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
|
-
|
|
548
|
-
|
|
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
|
-
* @param {
|
|
586
|
-
* @
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
*
|
|
597
|
-
* @param {
|
|
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
|
-
const
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
/**
|
|
699
|
-
* @param {
|
|
700
|
-
* @param {
|
|
701
|
-
* @memberof WebGL */
|
|
702
|
-
function
|
|
703
|
-
{
|
|
704
|
-
if (
|
|
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
|
-
|
|
775
|
-
|
|
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 glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos, glInstancedVAO, glPolyVAO, glFramebuffer, glRenderTarget, glShaderObjects = [], glCustomShader, glBatchShader, glProgramCustom, glTransform, glUniformLocations = new Map, glCanBeEnabled = true;
|
|
36
|
+
|
|
37
|
+
// WebGL internal constants
|
|
38
|
+
const gl_ARRAY_BUFFER_SIZE = 5e5;
|
|
39
|
+
const gl_INDICES_PER_INSTANCE = 11;
|
|
40
|
+
const gl_INSTANCE_BYTE_STRIDE = gl_INDICES_PER_INSTANCE * 4;
|
|
41
|
+
const gl_MAX_INSTANCES = gl_ARRAY_BUFFER_SIZE / gl_INSTANCE_BYTE_STRIDE | 0;
|
|
42
|
+
const gl_INDICES_PER_POLY_VERTEX = 3;
|
|
43
|
+
const gl_POLY_VERTEX_BYTE_STRIDE = gl_INDICES_PER_POLY_VERTEX * 4;
|
|
44
|
+
const gl_MAX_POLY_VERTEXES = gl_ARRAY_BUFFER_SIZE / gl_POLY_VERTEX_BYTE_STRIDE | 0;
|
|
45
|
+
|
|
46
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
47
|
+
|
|
48
|
+
// Initialize WebGL, called automatically by the engine
|
|
49
|
+
// the sprite vertex shader, shared by the engine's program and every Shader so one vertex layout fits all
|
|
50
|
+
const gl_VERTEX_SOURCE =
|
|
51
|
+
'#version 300 es\n' + // specify GLSL ES version
|
|
52
|
+
'precision highp float;'+ // use highp for accuracy
|
|
53
|
+
'uniform mat4 m;'+ // transform matrix
|
|
54
|
+
'layout(location=0) in vec2 g;'+ // in: geometry
|
|
55
|
+
'layout(location=1) in vec4 p;'+ // in: position/size
|
|
56
|
+
'layout(location=2) in vec4 u;'+ // in: uvs
|
|
57
|
+
'layout(location=3) in vec4 c;'+ // in: color
|
|
58
|
+
'layout(location=4) in vec4 a;'+ // in: additiveColor
|
|
59
|
+
'layout(location=5) in float r;'+// in: rotation
|
|
60
|
+
'out vec2 v,l;'+ // out: uv, and 0 to 1 across the sprite for a Shader's localUV
|
|
61
|
+
'out vec4 d,e;'+ // out: color, additiveColor
|
|
62
|
+
'void main(){'+ // shader entry point
|
|
63
|
+
'vec2 s=(g-.5)*p.zw;'+ // get size offset
|
|
64
|
+
'gl_Position=m*vec4(p.xy+s*cos(r)-vec2(-s.y,s)*sin(r),1,1);'+ // transform position
|
|
65
|
+
'v=mix(u.xw,u.zy,g);'+ // pass uv to fragment shader
|
|
66
|
+
'l=g;d=c;e=a;'+ // pass local uv and colors to fragment shader
|
|
67
|
+
'}'; // end of shader
|
|
68
|
+
|
|
69
|
+
function glInit(rootElement)
|
|
70
|
+
{
|
|
71
|
+
// keep set of texture infos so they can be restored if context is lost
|
|
72
|
+
glTextureInfos = new Set;
|
|
73
|
+
|
|
74
|
+
if (!glEnable || headlessMode)
|
|
75
|
+
{
|
|
76
|
+
glCanBeEnabled = false;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// create the canvas and textures
|
|
81
|
+
glCanvas = document.createElement('canvas');
|
|
82
|
+
glContext = glCanvas.getContext('webgl2', {antialias:glAntialias});
|
|
83
|
+
|
|
84
|
+
if (!glContext)
|
|
85
|
+
{
|
|
86
|
+
console.warn('WebGL2 not supported, falling back to 2D canvas rendering!');
|
|
87
|
+
glCanvas = glContext = undefined;
|
|
88
|
+
glEnable = false;
|
|
89
|
+
glCanBeEnabled = false;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// attach the WebGL canvas;
|
|
94
|
+
rootElement.appendChild(glCanvas);
|
|
95
|
+
|
|
96
|
+
// startup webgl
|
|
97
|
+
initWebGL();
|
|
98
|
+
|
|
99
|
+
// setup context lost and restore handlers
|
|
100
|
+
glCanvas.addEventListener('webglcontextlost', (e)=>
|
|
101
|
+
{
|
|
102
|
+
glEnable = false; // disable WebGL rendering
|
|
103
|
+
glCanvas.style.display = 'none'; // hide the gl canvas
|
|
104
|
+
e.preventDefault(); // prevent default to allow restoration
|
|
105
|
+
LOG('WebGL context lost! Switching to Canvas2d rendering.');
|
|
106
|
+
|
|
107
|
+
// remove WebGL textures
|
|
108
|
+
for (const info of glTextureInfos)
|
|
109
|
+
info.glTexture = undefined;
|
|
110
|
+
glActiveTexture = undefined;
|
|
111
|
+
// every Shader compiles again on its next draw, and the first flush after restore picks its program again
|
|
112
|
+
for (const shader of glShaderObjects)
|
|
113
|
+
shader.program = undefined;
|
|
114
|
+
glBatchShader = undefined;
|
|
115
|
+
glProgramCustom = true;
|
|
116
|
+
glUniformLocations = new Map; // the programs those belonged to are gone
|
|
117
|
+
// drop any partially-filled batch so the next glFlush doesn't
|
|
118
|
+
// upload stale glBatchCount against fresh empty buffers on restore
|
|
119
|
+
glBatchCount = 0;
|
|
120
|
+
glPolyMode = false;
|
|
121
|
+
pluginList.forEach(plugin=>plugin.glContextLost?.());
|
|
122
|
+
});
|
|
123
|
+
glCanvas.addEventListener('webglcontextrestored', ()=>
|
|
124
|
+
{
|
|
125
|
+
glEnable = true; // re-enable WebGL rendering
|
|
126
|
+
glCanvas.style.display = ''; // show the gl canvas
|
|
127
|
+
LOG('WebGL context restored, reinitializing...');
|
|
128
|
+
|
|
129
|
+
// reinit WebGL and restore textures
|
|
130
|
+
initWebGL();
|
|
131
|
+
for (const info of glTextureInfos)
|
|
132
|
+
info.glTexture = glCreateTexture(info.image, info.wrap);
|
|
133
|
+
pluginList.forEach(plugin=>plugin.glContextRestored?.());
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
function initWebGL()
|
|
137
|
+
{
|
|
138
|
+
// setup instanced rendering shader program
|
|
139
|
+
glShader = glCreateProgram(gl_VERTEX_SOURCE,
|
|
140
|
+
'#version 300 es\n' + // specify GLSL ES version
|
|
141
|
+
'precision highp float;'+ // use highp for accuracy
|
|
142
|
+
'uniform sampler2D s;'+ // texture
|
|
143
|
+
'in vec2 v;'+ // in: uv
|
|
144
|
+
'in vec4 d,e;'+ // in: color, additiveColor
|
|
145
|
+
'out vec4 c;'+ // out: color
|
|
146
|
+
'void main(){'+ // shader entry point
|
|
147
|
+
'c=texture(s,v)*d+e;'+ // modulate texture by color plus additive
|
|
148
|
+
'}' // end of shader
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// setup poly rendering shaders
|
|
152
|
+
glPolyShader = glCreateProgram(
|
|
153
|
+
'#version 300 es\n' + // specify GLSL ES version
|
|
154
|
+
'precision highp float;'+ // use highp for better accuracy
|
|
155
|
+
'uniform mat4 m;'+ // transform matrix
|
|
156
|
+
'in vec2 p;'+ // in: position
|
|
157
|
+
'in vec4 c;'+ // in: color
|
|
158
|
+
'out vec4 d;'+ // out: color
|
|
159
|
+
'void main(){'+ // shader entry point
|
|
160
|
+
'gl_Position=m*vec4(p,1,1);'+ // transform position
|
|
161
|
+
'd=c;'+ // pass color to fragment shader
|
|
162
|
+
'}' // end of shader
|
|
163
|
+
,
|
|
164
|
+
'#version 300 es\n' + // specify GLSL ES version
|
|
165
|
+
'precision highp float;'+ // use highp for better accuracy
|
|
166
|
+
'in vec4 d;'+ // in: color
|
|
167
|
+
'out vec4 c;'+ // out: color
|
|
168
|
+
'void main(){'+ // shader entry point
|
|
169
|
+
'c=d;'+ // set color
|
|
170
|
+
'}' // end of shader
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// init buffers
|
|
174
|
+
const glInstanceData = new ArrayBuffer(gl_ARRAY_BUFFER_SIZE);
|
|
175
|
+
glPositionData = new Float32Array(glInstanceData);
|
|
176
|
+
glColorData = new Uint32Array(glInstanceData);
|
|
177
|
+
glArrayBuffer = glContext.createBuffer();
|
|
178
|
+
glGeometryBuffer = glContext.createBuffer();
|
|
179
|
+
glFramebuffer = glContext.createFramebuffer();
|
|
180
|
+
glBatchCount = 0;
|
|
181
|
+
|
|
182
|
+
// create the geometry buffer, triangle strip square
|
|
183
|
+
const geometry = new Float32Array([0,0,1,0,0,1,1,1]);
|
|
184
|
+
glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
|
|
185
|
+
glContext.bufferData(glContext.ARRAY_BUFFER, geometry, glContext.STATIC_DRAW);
|
|
186
|
+
|
|
187
|
+
let offset, shader, stride;
|
|
188
|
+
const initVertexAttrib = (name, type, typeSize, size, divisor=0)=>
|
|
189
|
+
{
|
|
190
|
+
const location = glContext.getAttribLocation(shader, name);
|
|
191
|
+
const normalize = typeSize === 1;
|
|
192
|
+
const fixedStride = typeSize && stride;
|
|
193
|
+
glContext.enableVertexAttribArray(location);
|
|
194
|
+
glContext.vertexAttribPointer(location, size, type, normalize, fixedStride, offset);
|
|
195
|
+
glContext.vertexAttribDivisor(location, divisor);
|
|
196
|
+
offset += size*typeSize;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// setup VAO for instanced rendering
|
|
200
|
+
glInstancedVAO = glContext.createVertexArray();
|
|
201
|
+
glContext.bindVertexArray(glInstancedVAO);
|
|
202
|
+
|
|
203
|
+
// configure instanced vertex attributes
|
|
204
|
+
offset = 0, shader = glShader, stride = gl_INSTANCE_BYTE_STRIDE;
|
|
205
|
+
glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
|
|
206
|
+
initVertexAttrib('g', glContext.FLOAT, 0, 2); // geometry
|
|
207
|
+
glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
|
|
208
|
+
glContext.bufferData(glContext.ARRAY_BUFFER, gl_ARRAY_BUFFER_SIZE, glContext.DYNAMIC_DRAW);
|
|
209
|
+
initVertexAttrib('p', glContext.FLOAT, 4, 4, 1); // position & size
|
|
210
|
+
initVertexAttrib('u', glContext.FLOAT, 4, 4, 1); // texture coords
|
|
211
|
+
initVertexAttrib('c', glContext.UNSIGNED_BYTE, 1, 4, 1); // color
|
|
212
|
+
initVertexAttrib('a', glContext.UNSIGNED_BYTE, 1, 4, 1); // additiveColor
|
|
213
|
+
initVertexAttrib('r', glContext.FLOAT, 4, 1, 1); // rotation
|
|
214
|
+
|
|
215
|
+
// setup VAO for poly rendering
|
|
216
|
+
glPolyVAO = glContext.createVertexArray();
|
|
217
|
+
glContext.bindVertexArray(glPolyVAO);
|
|
218
|
+
|
|
219
|
+
// configure poly vertex attributes
|
|
220
|
+
offset = 0, shader = glPolyShader, stride = gl_POLY_VERTEX_BYTE_STRIDE;
|
|
221
|
+
initVertexAttrib('p', glContext.FLOAT, 4, 2); // position
|
|
222
|
+
initVertexAttrib('c', glContext.UNSIGNED_BYTE, 1, 4); // color
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function glSetInstancedMode(force=false)
|
|
227
|
+
{
|
|
228
|
+
if (!force && !glPolyMode) return;
|
|
229
|
+
|
|
230
|
+
// setup instanced mode
|
|
231
|
+
glFlush();
|
|
232
|
+
glPolyMode = false;
|
|
233
|
+
glContext.useProgram(glShader);
|
|
234
|
+
glContext.bindVertexArray(glInstancedVAO);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function glSetPolyMode()
|
|
238
|
+
{
|
|
239
|
+
if (glPolyMode) return;
|
|
240
|
+
|
|
241
|
+
// setup poly mode
|
|
242
|
+
glFlush();
|
|
243
|
+
glPolyMode = true;
|
|
244
|
+
glContext.useProgram(glPolyShader);
|
|
245
|
+
glContext.bindVertexArray(glPolyVAO);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Setup WebGL render each frame, called automatically by engine
|
|
249
|
+
// Also used by tile layer rendering when redrawing tiles
|
|
250
|
+
function glPreRender(clear=true)
|
|
251
|
+
{
|
|
252
|
+
if (!glEnable || !glContext) return;
|
|
253
|
+
|
|
254
|
+
ASSERT(!glBatchCount, 'glPreRender called with unflushed batch.');
|
|
255
|
+
|
|
256
|
+
// mainCanvasSize is css pixels, the backing store is scaled by the pixel
|
|
257
|
+
// ratio, render targets are offscreen so they are never scaled
|
|
258
|
+
const dpr = glRenderTarget ? 1 : getCanvasPixelRatio();
|
|
259
|
+
const bufferSizeX = mainCanvasSize.x * dpr | 0;
|
|
260
|
+
const bufferSizeY = mainCanvasSize.y * dpr | 0;
|
|
261
|
+
if (!glRenderTarget)
|
|
262
|
+
{
|
|
263
|
+
// set to same size as main canvas, only when it changes because
|
|
264
|
+
// setting it reallocates the drawing buffer and invalidates the frame
|
|
265
|
+
if (glCanvas.width !== bufferSizeX || glCanvas.height !== bufferSizeY)
|
|
266
|
+
{
|
|
267
|
+
glCanvas.width = bufferSizeX;
|
|
268
|
+
glCanvas.height = bufferSizeY;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
glContext.viewport(0, 0, bufferSizeX, bufferSizeY);
|
|
272
|
+
clear && glClearCanvas();
|
|
273
|
+
|
|
274
|
+
// build the transform matrix
|
|
275
|
+
const s = vec2(2*cameraScale).divide(mainCanvasSize);
|
|
276
|
+
if (glRenderTarget)
|
|
277
|
+
s.y = -s.y; // invert y when using render target
|
|
278
|
+
const rotatedCam = cameraPos.rotate(-cameraAngle);
|
|
279
|
+
const p = vec2(-1).subtract(rotatedCam.multiply(s));
|
|
280
|
+
const ca = cos(cameraAngle);
|
|
281
|
+
const sa = sin(cameraAngle);
|
|
282
|
+
const transform = [
|
|
283
|
+
s.x * ca, s.y * sa, 0, 0,
|
|
284
|
+
-s.x * sa, s.y * ca, 0, 0,
|
|
285
|
+
1, 1, 1, 0,
|
|
286
|
+
p.x, p.y, 0, 1];
|
|
287
|
+
glTransform = transform;
|
|
288
|
+
|
|
289
|
+
// set the same transform matrix for both shaders
|
|
290
|
+
const initUniform = (program, uniform, value)=>
|
|
291
|
+
{
|
|
292
|
+
glContext.useProgram(program);
|
|
293
|
+
const location = glContext.getUniformLocation(program, uniform);
|
|
294
|
+
glContext.uniformMatrix4fv(location, false, value);
|
|
295
|
+
}
|
|
296
|
+
initUniform(glPolyShader, 'm', transform);
|
|
297
|
+
initUniform(glShader, 'm', transform);
|
|
298
|
+
|
|
299
|
+
// set the active texture
|
|
300
|
+
glContext.activeTexture(glContext.TEXTURE0);
|
|
301
|
+
if (textureInfos[0])
|
|
302
|
+
{
|
|
303
|
+
glActiveTexture = textureInfos[0].glTexture;
|
|
304
|
+
glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// rebind the array buffer
|
|
308
|
+
glContext.bindBuffer(glContext.ARRAY_BUFFER, glArrayBuffer);
|
|
309
|
+
|
|
310
|
+
// start with additive blending off
|
|
311
|
+
glAdditive = glBatchAdditive = false;
|
|
312
|
+
|
|
313
|
+
// force it to set instanced mode
|
|
314
|
+
glSetInstancedMode(true);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Clear the canvas and setup the viewport
|
|
318
|
+
* @memberof WebGL */
|
|
319
|
+
function glClearCanvas()
|
|
320
|
+
{
|
|
321
|
+
if (!glContext) return;
|
|
322
|
+
|
|
323
|
+
// clear using the canvasClearColor
|
|
324
|
+
const color = canvasClearColor;
|
|
325
|
+
glContext.clearColor(color.r, color.g, color.b, color.a);
|
|
326
|
+
glContext.clear(glContext.COLOR_BUFFER_BIT);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Set the WebGL texture, called automatically if using multiple textures
|
|
330
|
+
* - This may also flush the gl buffer resulting in more draw calls and worse performance
|
|
331
|
+
* @param {WebGLTexture} texture
|
|
332
|
+
* @memberof WebGL */
|
|
333
|
+
function glSetTexture(texture)
|
|
334
|
+
{
|
|
335
|
+
// must flush cache with the old texture to set a new one
|
|
336
|
+
if (!glContext || texture === glActiveTexture) return;
|
|
337
|
+
|
|
338
|
+
glFlush();
|
|
339
|
+
glActiveTexture = texture;
|
|
340
|
+
glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Set the wrap mode (REPEAT or CLAMP_TO_EDGE) on an existing WebGL texture
|
|
344
|
+
* Flushes the current batch only if the texture is the active one
|
|
345
|
+
* @param {WebGLTexture} texture
|
|
346
|
+
* @param {boolean} [wrap] - true for REPEAT, false for CLAMP_TO_EDGE
|
|
347
|
+
* @memberof WebGL */
|
|
348
|
+
function glSetTextureWrap(texture, wrap=true)
|
|
349
|
+
{
|
|
350
|
+
if (!glContext || !texture) return;
|
|
351
|
+
|
|
352
|
+
// flush only if changing wrap on the currently bound texture
|
|
353
|
+
const isCurrent = texture === glActiveTexture;
|
|
354
|
+
if (isCurrent)
|
|
355
|
+
glFlush();
|
|
356
|
+
else
|
|
357
|
+
glContext.bindTexture(glContext.TEXTURE_2D, texture);
|
|
358
|
+
|
|
359
|
+
const wrapMode = wrap ? glContext.REPEAT : glContext.CLAMP_TO_EDGE;
|
|
360
|
+
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_S, wrapMode);
|
|
361
|
+
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_T, wrapMode);
|
|
362
|
+
|
|
363
|
+
if (!isCurrent && glActiveTexture)
|
|
364
|
+
glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Compile WebGL shader of the given type, will throw errors if in debug mode
|
|
368
|
+
* @param {string} source
|
|
369
|
+
* @param {number} type
|
|
370
|
+
* @return {WebGLShader}
|
|
371
|
+
* @memberof WebGL */
|
|
372
|
+
function glCompileShader(source, type)
|
|
373
|
+
{
|
|
374
|
+
if (!glContext) return;
|
|
375
|
+
|
|
376
|
+
// build the shader
|
|
377
|
+
const shader = glContext.createShader(type);
|
|
378
|
+
glContext.shaderSource(shader, source);
|
|
379
|
+
glContext.compileShader(shader);
|
|
380
|
+
|
|
381
|
+
// check for errors
|
|
382
|
+
if (debug && !glContext.getShaderParameter(shader, glContext.COMPILE_STATUS))
|
|
383
|
+
throw glContext.getShaderInfoLog(shader);
|
|
384
|
+
return shader;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Create WebGL program with given shaders
|
|
388
|
+
* @param {string} vsSource
|
|
389
|
+
* @param {string} fsSource
|
|
390
|
+
* @return {WebGLProgram}
|
|
391
|
+
* @memberof WebGL */
|
|
392
|
+
function glCreateProgram(vsSource, fsSource)
|
|
393
|
+
{
|
|
394
|
+
if (!glContext) return;
|
|
395
|
+
|
|
396
|
+
// build the program
|
|
397
|
+
const program = glContext.createProgram();
|
|
398
|
+
glContext.attachShader(program, glCompileShader(vsSource, glContext.VERTEX_SHADER));
|
|
399
|
+
glContext.attachShader(program, glCompileShader(fsSource, glContext.FRAGMENT_SHADER));
|
|
400
|
+
glContext.linkProgram(program);
|
|
401
|
+
|
|
402
|
+
// check for errors
|
|
403
|
+
if (debug && !glContext.getProgramParameter(program, glContext.LINK_STATUS))
|
|
404
|
+
throw glContext.getProgramInfoLog(program);
|
|
405
|
+
return program;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// a uniform location, looked up once per program
|
|
409
|
+
function glUniformLocation(program, name)
|
|
410
|
+
{
|
|
411
|
+
let cache = glUniformLocations.get(program);
|
|
412
|
+
cache || glUniformLocations.set(program, cache = {});
|
|
413
|
+
return cache[name] ??= glContext.getUniformLocation(program, name);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// a Shader's 2D program, compiled the first time a batch needs it: the snippet's mainImage gives the surface
|
|
417
|
+
// color, then the sprite's color and additive color apply as the engine's own fragment shader does
|
|
418
|
+
function glShaderProgram(shader)
|
|
419
|
+
{
|
|
420
|
+
return shader.program ||= glCreateProgram(gl_VERTEX_SOURCE,
|
|
421
|
+
'#version 300 es\n' +
|
|
422
|
+
'precision highp float;' +
|
|
423
|
+
'uniform sampler2D iChannel0;' + // the texture
|
|
424
|
+
'uniform vec3 iResolution;' + // canvas size in pixels
|
|
425
|
+
'uniform float iTime;' + // engine time
|
|
426
|
+
'in vec2 v,l;in vec4 d,e;out vec4 c;\n' + // a define needs its own line
|
|
427
|
+
'#define localUV l\n' +
|
|
428
|
+
shader.fragmentCode + '\n' +
|
|
429
|
+
'void main(){vec4 t;mainImage(t,v);c=t*d+e;}');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Create WebGL texture from an image and init the texture settings
|
|
433
|
+
* Restores the active texture when done
|
|
434
|
+
* @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas} [image]
|
|
435
|
+
* @param {boolean} [wrap] - true for REPEAT, false for CLAMP_TO_EDGE
|
|
436
|
+
* @return {WebGLTexture}
|
|
437
|
+
* @memberof WebGL */
|
|
438
|
+
function glCreateTexture(image, wrap=false)
|
|
439
|
+
{
|
|
440
|
+
if (!glContext) return;
|
|
441
|
+
|
|
442
|
+
// build the texture
|
|
443
|
+
const texture = glContext.createTexture();
|
|
444
|
+
let mipMap = false;
|
|
445
|
+
if (image?.width)
|
|
446
|
+
{
|
|
447
|
+
glSetTextureData(texture, image);
|
|
448
|
+
glContext.bindTexture(glContext.TEXTURE_2D, texture);
|
|
449
|
+
mipMap = !tilesPixelated && isPowerOfTwo(image.width) && isPowerOfTwo(image.height);
|
|
450
|
+
}
|
|
451
|
+
else
|
|
452
|
+
{
|
|
453
|
+
// create a white texture
|
|
454
|
+
const whitePixel = new Uint8Array([255, 255, 255, 255]);
|
|
455
|
+
glContext.bindTexture(glContext.TEXTURE_2D, texture);
|
|
456
|
+
glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, 1, 1, 0, glContext.RGBA, glContext.UNSIGNED_BYTE, whitePixel);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// set texture filtering
|
|
460
|
+
const magFilter = tilesPixelated ? glContext.NEAREST : glContext.LINEAR;
|
|
461
|
+
const minFilter = mipMap ? glContext.LINEAR_MIPMAP_LINEAR : magFilter;
|
|
462
|
+
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MAG_FILTER, magFilter);
|
|
463
|
+
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, minFilter);
|
|
464
|
+
const wrapMode = wrap ? glContext.REPEAT : glContext.CLAMP_TO_EDGE;
|
|
465
|
+
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_S, wrapMode);
|
|
466
|
+
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_T, wrapMode);
|
|
467
|
+
if (mipMap)
|
|
468
|
+
glContext.generateMipmap(glContext.TEXTURE_2D);
|
|
469
|
+
|
|
470
|
+
// rebind active texture
|
|
471
|
+
glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
|
|
472
|
+
return texture;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Deletes a WebGL texture
|
|
476
|
+
* @param {WebGLTexture} [texture]
|
|
477
|
+
* @memberof WebGL */
|
|
478
|
+
function glDeleteTexture(texture)
|
|
479
|
+
{
|
|
480
|
+
if (!glContext) return;
|
|
481
|
+
|
|
482
|
+
glContext.deleteTexture(texture);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** Set WebGL texture data from an image, restores the active texture when done
|
|
486
|
+
* @param {WebGLTexture} texture
|
|
487
|
+
* @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas} image
|
|
488
|
+
* @memberof WebGL */
|
|
489
|
+
function glSetTextureData(texture, image)
|
|
490
|
+
{
|
|
491
|
+
if (!glContext) return;
|
|
492
|
+
|
|
493
|
+
// build the texture
|
|
494
|
+
ASSERT(image?.width > 0, 'Invalid image data.');
|
|
495
|
+
glContext.bindTexture(glContext.TEXTURE_2D, texture);
|
|
496
|
+
glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, image);
|
|
497
|
+
|
|
498
|
+
// keep mipmaps in sync with new level 0 data (same condition as glCreateTexture)
|
|
499
|
+
if (!tilesPixelated && isPowerOfTwo(image.width) && isPowerOfTwo(image.height))
|
|
500
|
+
glContext.generateMipmap(glContext.TEXTURE_2D);
|
|
501
|
+
|
|
502
|
+
// rebind active texture
|
|
503
|
+
glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** Tells WebGL to create or update the glTexture and start tracking it
|
|
507
|
+
* @param {TextureInfo} textureInfo
|
|
508
|
+
* @memberof WebGL */
|
|
509
|
+
function glRegisterTextureInfo(textureInfo)
|
|
510
|
+
{
|
|
511
|
+
if (headlessMode) return;
|
|
512
|
+
|
|
513
|
+
// add texture info to tracking list even if gl is not enabled
|
|
514
|
+
glTextureInfos.add(textureInfo);
|
|
515
|
+
|
|
516
|
+
if (!glContext) return;
|
|
517
|
+
|
|
518
|
+
// create or set the texture data
|
|
519
|
+
if (textureInfo.glTexture)
|
|
520
|
+
glSetTextureData(textureInfo.glTexture, textureInfo.image);
|
|
521
|
+
else
|
|
522
|
+
textureInfo.glTexture = glCreateTexture(textureInfo.image, textureInfo.wrap);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** Tells WebGL to destroy the glTexture and stop tracking it
|
|
526
|
+
* @param {TextureInfo} textureInfo
|
|
527
|
+
* @memberof WebGL */
|
|
528
|
+
function glUnregisterTextureInfo(textureInfo)
|
|
529
|
+
{
|
|
530
|
+
if (headlessMode) return;
|
|
531
|
+
|
|
532
|
+
// delete texture info from tracking list even if gl is not enabled
|
|
533
|
+
glTextureInfos.delete(textureInfo);
|
|
534
|
+
|
|
535
|
+
// unset and destroy the texture
|
|
536
|
+
const glTexture = textureInfo.glTexture;
|
|
537
|
+
textureInfo.glTexture = undefined;
|
|
538
|
+
glDeleteTexture(glTexture);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** Draw all sprites and clear out the buffer, called automatically by the system whenever necessary
|
|
542
|
+
* @memberof WebGL */
|
|
543
|
+
function glFlush()
|
|
544
|
+
{
|
|
545
|
+
if (glEnable && glContext && glBatchCount)
|
|
546
|
+
{
|
|
547
|
+
// set blend mode
|
|
548
|
+
const destBlend = glBatchAdditive ? glContext.ONE : glContext.ONE_MINUS_SRC_ALPHA;
|
|
549
|
+
glContext.blendFuncSeparate(glContext.SRC_ALPHA, destBlend, glContext.ONE, destBlend);
|
|
550
|
+
glContext.enable(glContext.BLEND);
|
|
551
|
+
|
|
552
|
+
// a Shader's program for this batch, or the engine's own again after one
|
|
553
|
+
if (!glPolyMode && (glBatchShader || glProgramCustom))
|
|
554
|
+
{
|
|
555
|
+
const program = glBatchShader ? glShaderProgram(glBatchShader) : glShader;
|
|
556
|
+
glContext.useProgram(program);
|
|
557
|
+
glProgramCustom = !!glBatchShader;
|
|
558
|
+
if (glBatchShader)
|
|
559
|
+
{
|
|
560
|
+
const uniform = (name)=> glUniformLocation(program, name);
|
|
561
|
+
glContext.uniformMatrix4fv(uniform('m'), false, glTransform);
|
|
562
|
+
glContext.uniform1f(uniform('iTime'), time);
|
|
563
|
+
glContext.uniform3f(uniform('iResolution'), glCanvas.width, glCanvas.height, 1);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const byteLength = glBatchCount *
|
|
568
|
+
(glPolyMode ? gl_INDICES_PER_POLY_VERTEX : gl_INDICES_PER_INSTANCE);
|
|
569
|
+
glContext.bufferSubData(glContext.ARRAY_BUFFER, 0, glPositionData, 0, byteLength);
|
|
570
|
+
|
|
571
|
+
// draw the batch
|
|
572
|
+
if (glPolyMode)
|
|
573
|
+
glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, glBatchCount);
|
|
574
|
+
else
|
|
575
|
+
glContext.drawArraysInstanced(glContext.TRIANGLE_STRIP, 0, 4, glBatchCount);
|
|
576
|
+
++drawCount;
|
|
577
|
+
primitiveCount += glBatchCount;
|
|
578
|
+
glBatchCount = 0;
|
|
579
|
+
}
|
|
580
|
+
glBatchAdditive = glAdditive;
|
|
581
|
+
glBatchShader = glCustomShader;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** Flush any sprites still in the buffer and copy to main canvas
|
|
585
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
|
|
586
|
+
* @memberof WebGL */
|
|
587
|
+
function glCopyToContext(context)
|
|
588
|
+
{
|
|
589
|
+
if (!glEnable || !glContext) return;
|
|
590
|
+
|
|
591
|
+
glFlush();
|
|
592
|
+
context.drawImage(glCanvas, 0, 0);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** Set anti-aliasing for WebGL canvas
|
|
596
|
+
* Must be called before engineInit
|
|
597
|
+
* @param {boolean} [antialias]
|
|
598
|
+
* @memberof WebGL */
|
|
599
|
+
function glSetAntialias(antialias=true)
|
|
600
|
+
{
|
|
601
|
+
ASSERT(!glCanvas, 'must be called before engineInit');
|
|
602
|
+
glAntialias = antialias;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/** Add a sprite to the gl draw list, used by all gl draw functions
|
|
606
|
+
* @param {number} x
|
|
607
|
+
* @param {number} y
|
|
608
|
+
* @param {number} sizeX
|
|
609
|
+
* @param {number} sizeY
|
|
610
|
+
* @param {number} [angle]
|
|
611
|
+
* @param {number} [uv0X]
|
|
612
|
+
* @param {number} [uv0Y]
|
|
613
|
+
* @param {number} [uv1X]
|
|
614
|
+
* @param {number} [uv1Y]
|
|
615
|
+
* @param {number} [rgba=-1] - white is -1
|
|
616
|
+
* @param {number} [rgbaAdditive=0] - black is 0
|
|
617
|
+
* @memberof WebGL */
|
|
618
|
+
function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgba=-1, rgbaAdditive=0)
|
|
619
|
+
{
|
|
620
|
+
// flush if there is not enough room or if different blend mode
|
|
621
|
+
if (glBatchCount >= gl_MAX_INSTANCES || glBatchAdditive !== glAdditive || glBatchShader !== glCustomShader)
|
|
622
|
+
glFlush();
|
|
623
|
+
glSetInstancedMode();
|
|
624
|
+
|
|
625
|
+
let offset = glBatchCount++ * gl_INDICES_PER_INSTANCE;
|
|
626
|
+
glPositionData[offset++] = x;
|
|
627
|
+
glPositionData[offset++] = y;
|
|
628
|
+
glPositionData[offset++] = sizeX;
|
|
629
|
+
glPositionData[offset++] = sizeY;
|
|
630
|
+
glPositionData[offset++] = uv0X;
|
|
631
|
+
glPositionData[offset++] = uv0Y;
|
|
632
|
+
glPositionData[offset++] = uv1X;
|
|
633
|
+
glPositionData[offset++] = uv1Y;
|
|
634
|
+
glColorData[offset++] = rgba;
|
|
635
|
+
glColorData[offset++] = rgbaAdditive;
|
|
636
|
+
glPositionData[offset++] = angle;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/** Add an untextured rect to the gl draw list
|
|
640
|
+
* Zeroes the uvs and rgba so the texture contribution multiplies to 0,
|
|
641
|
+
* then carries the real color in the additive slot. Works regardless of
|
|
642
|
+
* which texture is currently bound.
|
|
643
|
+
* @param {number} x
|
|
644
|
+
* @param {number} y
|
|
645
|
+
* @param {number} sizeX
|
|
646
|
+
* @param {number} sizeY
|
|
647
|
+
* @param {number} angle
|
|
648
|
+
* @param {number} rgba - color as 32-bit integer
|
|
649
|
+
* @memberof WebGL */
|
|
650
|
+
function glDrawUntextured(x, y, sizeX, sizeY, angle, rgba)
|
|
651
|
+
{
|
|
652
|
+
glDraw(x, y, sizeX, sizeY, angle, 0, 0, 0, 0, 0, rgba);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** Transform and add a polygon to the gl draw list
|
|
656
|
+
* @param {Array<Vector2>} points - Array of Vector2 points
|
|
657
|
+
* @param {number} rgba - Color of the polygon as a 32-bit integer
|
|
658
|
+
* @param {number} x
|
|
659
|
+
* @param {number} y
|
|
660
|
+
* @param {number} sx
|
|
661
|
+
* @param {number} sy
|
|
662
|
+
* @param {number} angle
|
|
663
|
+
* @param {boolean} [tristrip] - should tristrip algorithm be used
|
|
664
|
+
* @memberof WebGL */
|
|
665
|
+
function glDrawPointsTransform(points, rgba, x, y, sx, sy, angle, tristrip=true)
|
|
666
|
+
{
|
|
667
|
+
const pointsOut = [];
|
|
668
|
+
const sa = sin(-angle);
|
|
669
|
+
const ca = cos(-angle);
|
|
670
|
+
for (const p of points)
|
|
671
|
+
{
|
|
672
|
+
// transform the point
|
|
673
|
+
const px = p.x*sx;
|
|
674
|
+
const py = p.y*sy;
|
|
675
|
+
pointsOut.push(vec2(x + ca*px - sa*py, y + sa*px + ca*py));
|
|
676
|
+
}
|
|
677
|
+
const drawPoints = tristrip ? glPolyStrip(pointsOut) : pointsOut;
|
|
678
|
+
glDrawPoints(drawPoints, rgba);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/** Transform and add a polygon to the gl draw list
|
|
682
|
+
* @param {Array<Vector2>} points - Array of Vector2 points
|
|
683
|
+
* @param {number} rgba - Color of the polygon as a 32-bit integer
|
|
684
|
+
* @param {number} lineWidth - Width of the outline
|
|
685
|
+
* @param {number} x
|
|
686
|
+
* @param {number} y
|
|
687
|
+
* @param {number} sx
|
|
688
|
+
* @param {number} sy
|
|
689
|
+
* @param {number} angle
|
|
690
|
+
* @param {boolean} [wrap] - Should the outline connect the first and last points
|
|
691
|
+
* @memberof WebGL */
|
|
692
|
+
function glDrawOutlineTransform(points, rgba, lineWidth, x, y, sx, sy, angle, wrap=true)
|
|
693
|
+
{
|
|
694
|
+
const outlinePoints = glMakeOutline(points, lineWidth, wrap);
|
|
695
|
+
glDrawPointsTransform(outlinePoints, rgba, x, y, sx, sy, angle, false);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/** Add a list of points to the gl draw list
|
|
699
|
+
* @param {Array<Vector2>} points - Array of Vector2 points in tri strip order
|
|
700
|
+
* @param {number} rgba - Color as a 32-bit integer
|
|
701
|
+
* @memberof WebGL */
|
|
702
|
+
function glDrawPoints(points, rgba)
|
|
703
|
+
{
|
|
704
|
+
if (!glEnable || points.length < 3)
|
|
705
|
+
return; // needs at least 3 points to have area
|
|
706
|
+
|
|
707
|
+
// flush if there is not enough room or if different blend mode
|
|
708
|
+
const vertCount = points.length + 2;
|
|
709
|
+
if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
|
|
710
|
+
glFlush();
|
|
711
|
+
ASSERT(vertCount < gl_MAX_POLY_VERTEXES, 'poly exceeds max batch size');
|
|
712
|
+
if (vertCount >= gl_MAX_POLY_VERTEXES) return; // release-build safety net
|
|
713
|
+
glSetPolyMode();
|
|
714
|
+
|
|
715
|
+
// setup triangle strip with degenerate verts at start and end
|
|
716
|
+
let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
|
|
717
|
+
for (let i = vertCount; i--;)
|
|
718
|
+
{
|
|
719
|
+
const j = clamp(i-1, 0, vertCount-3);
|
|
720
|
+
const point = points[j];
|
|
721
|
+
glPositionData[offset++] = point.x;
|
|
722
|
+
glPositionData[offset++] = point.y;
|
|
723
|
+
glColorData[offset++] = rgba;
|
|
724
|
+
}
|
|
725
|
+
glBatchCount += vertCount;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/** Add a list of colored points to the gl draw list
|
|
729
|
+
* @param {Array<Vector2>} points - Array of Vector2 points in tri strip order
|
|
730
|
+
* @param {Array<number>} pointColors - Array of 32-bit integer colors
|
|
731
|
+
* @memberof WebGL */
|
|
732
|
+
function glDrawColoredPoints(points, pointColors)
|
|
733
|
+
{
|
|
734
|
+
if (!glEnable || points.length < 3)
|
|
735
|
+
return; // needs at least 3 points to have area
|
|
736
|
+
|
|
737
|
+
// flush if there is not enough room or if different blend mode
|
|
738
|
+
const vertCount = points.length + 2;
|
|
739
|
+
if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
|
|
740
|
+
glFlush();
|
|
741
|
+
ASSERT(vertCount < gl_MAX_POLY_VERTEXES, 'poly exceeds max batch size');
|
|
742
|
+
if (vertCount >= gl_MAX_POLY_VERTEXES) return; // release-build safety net
|
|
743
|
+
glSetPolyMode();
|
|
744
|
+
|
|
745
|
+
// setup triangle strip with degenerate verts at start and end
|
|
746
|
+
let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
|
|
747
|
+
for (let i = vertCount; i--;)
|
|
748
|
+
{
|
|
749
|
+
const j = clamp(i-1, 0, vertCount-3);
|
|
750
|
+
const point = points[j];
|
|
751
|
+
const color = pointColors[j];
|
|
752
|
+
glPositionData[offset++] = point.x;
|
|
753
|
+
glPositionData[offset++] = point.y;
|
|
754
|
+
glColorData[offset++] = color;
|
|
755
|
+
}
|
|
756
|
+
glBatchCount += vertCount;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/** Set the WebGL render target to the given texture or back to the canvas
|
|
760
|
+
* @param {WebGLTexture} [texture] - a texture or undefined to use normal glCanvas
|
|
761
|
+
* @param {boolean} [clear] - should the render target be cleared
|
|
762
|
+
* @memberof WebGL */
|
|
763
|
+
function glSetRenderTarget(texture, clear=false)
|
|
764
|
+
{
|
|
765
|
+
if (texture)
|
|
766
|
+
{
|
|
767
|
+
glRenderTarget = texture;
|
|
768
|
+
glContext.bindFramebuffer(glContext.FRAMEBUFFER, glFramebuffer);
|
|
769
|
+
glContext.framebufferTexture2D(glContext.FRAMEBUFFER,
|
|
770
|
+
glContext.COLOR_ATTACHMENT0, glContext.TEXTURE_2D, texture, 0);
|
|
771
|
+
glPreRender(clear);
|
|
772
|
+
}
|
|
773
|
+
else
|
|
774
|
+
{
|
|
775
|
+
glFlush();
|
|
776
|
+
glRenderTarget = undefined;
|
|
777
|
+
glContext.bindFramebuffer(glContext.FRAMEBUFFER, null);
|
|
778
|
+
|
|
779
|
+
// use the backing store size, mainCanvasSize is css pixels and may
|
|
780
|
+
// still be the render target's size when unwinding a layer redraw
|
|
781
|
+
glContext.viewport(0, 0, glCanvas.width, glCanvas.height);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/** Clear out a rectangle area of the WebGL canvas or render target
|
|
786
|
+
* @param {number} x
|
|
787
|
+
* @param {number} y
|
|
788
|
+
* @param {number} width
|
|
789
|
+
* @param {number} height
|
|
790
|
+
* @memberof WebGL */
|
|
791
|
+
function glClearRect(x, y, width, height)
|
|
792
|
+
{
|
|
793
|
+
if (!glEnable) return;
|
|
794
|
+
|
|
795
|
+
// Enable scissor test to clear only the specified area
|
|
796
|
+
glContext.enable(glContext.SCISSOR_TEST);
|
|
797
|
+
glContext.scissor(x, y, width, height);
|
|
798
|
+
glContext.clearColor(0, 0, 0, 0);
|
|
799
|
+
glContext.clear(glContext.COLOR_BUFFER_BIT);
|
|
800
|
+
glContext.disable(glContext.SCISSOR_TEST);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
804
|
+
|
|
805
|
+
// WebGL internal function to convert polygon to outline triangle strip
|
|
806
|
+
function glMakeOutline(points, width, wrap=true)
|
|
807
|
+
{
|
|
808
|
+
if (points.length < 2)
|
|
809
|
+
return [];
|
|
810
|
+
|
|
811
|
+
const halfWidth = width / 2;
|
|
812
|
+
const strip = [];
|
|
813
|
+
const n = points.length;
|
|
814
|
+
const e = 1e-6;
|
|
815
|
+
// miter ratio cap (dimensionless, matches SVG/Canvas2D convention)
|
|
816
|
+
const miterLimit = 10;
|
|
817
|
+
for (let i = 0; i < n; i++)
|
|
818
|
+
{
|
|
819
|
+
// for each vertex, calculate normal based on adjacent edges
|
|
820
|
+
const prev = points[wrap ? (i - 1 + n) % n : max(i - 1, 0)];
|
|
821
|
+
const curr = points[i];
|
|
822
|
+
const next = points[wrap ? (i + 1) % n : min(i + 1, n - 1)];
|
|
823
|
+
|
|
824
|
+
// direction from previous to current
|
|
825
|
+
const dx1 = curr.x - prev.x;
|
|
826
|
+
const dy1 = curr.y - prev.y;
|
|
827
|
+
const len1 = (dx1*dx1 + dy1*dy1)**.5;
|
|
828
|
+
|
|
829
|
+
// direction from current to next
|
|
830
|
+
const dx2 = next.x - curr.x;
|
|
831
|
+
const dy2 = next.y - curr.y;
|
|
832
|
+
const len2 = (dx2*dx2 + dy2*dy2)**.5;
|
|
833
|
+
|
|
834
|
+
if (len1 < e && len2 < e)
|
|
835
|
+
continue; // skip degenerate point
|
|
836
|
+
|
|
837
|
+
// calculate perpendicular normals for each edge
|
|
838
|
+
const nx1 = len1 > e ? -dy1 / len1 : 0;
|
|
839
|
+
const ny1 = len1 > e ? dx1 / len1 : 0;
|
|
840
|
+
const nx2 = len2 > e ? -dy2 / len2 : 0;
|
|
841
|
+
const ny2 = len2 > e ? dx2 / len2 : 0;
|
|
842
|
+
|
|
843
|
+
// average the normals for miter
|
|
844
|
+
let nx = nx1 + nx2;
|
|
845
|
+
let ny = ny1 + ny2;
|
|
846
|
+
const nlen = (nx*nx + ny*ny)**.5;
|
|
847
|
+
if (nlen < e)
|
|
848
|
+
{
|
|
849
|
+
// 180 degree turn - use perpendicular
|
|
850
|
+
nx = nx1;
|
|
851
|
+
ny = ny1;
|
|
852
|
+
}
|
|
853
|
+
else
|
|
854
|
+
{
|
|
855
|
+
// calculate miter length
|
|
856
|
+
nx /= nlen;
|
|
857
|
+
ny /= nlen;
|
|
858
|
+
const dot = nx1 * nx + ny1 * ny;
|
|
859
|
+
if (dot > e)
|
|
860
|
+
{
|
|
861
|
+
// scale normal by miter length, clamped to miterLimit
|
|
862
|
+
const miterLength = min(1 / dot, miterLimit);
|
|
863
|
+
nx *= miterLength;
|
|
864
|
+
ny *= miterLength;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// create inner and outer points along the normal
|
|
869
|
+
const inner = vec2(curr.x - nx * halfWidth, curr.y - ny * halfWidth);
|
|
870
|
+
const outer = vec2(curr.x + nx * halfWidth, curr.y + ny * halfWidth);
|
|
871
|
+
strip.push(inner);
|
|
872
|
+
strip.push(outer);
|
|
873
|
+
}
|
|
874
|
+
if (strip.length > 1 && wrap)
|
|
875
|
+
{
|
|
876
|
+
// close the loop
|
|
877
|
+
strip.push(strip[0]);
|
|
878
|
+
strip.push(strip[1]);
|
|
879
|
+
}
|
|
880
|
+
return strip;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// WebGL internal function to convert polys to tri strips
|
|
884
|
+
function glPolyStrip(points)
|
|
885
|
+
{
|
|
886
|
+
// validate input
|
|
887
|
+
if (points.length < 3)
|
|
888
|
+
return [];
|
|
889
|
+
|
|
890
|
+
// cross product helper: (b-a) x (c-a)
|
|
891
|
+
const cross = (a,b,c)=> (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
|
|
892
|
+
|
|
893
|
+
// calculate signed area of polygon
|
|
894
|
+
const signedArea = (poly)=>
|
|
895
|
+
{
|
|
896
|
+
let area = 0;
|
|
897
|
+
for (let i = poly.length; i--;)
|
|
898
|
+
{
|
|
899
|
+
const j = (i+1) % poly.length;
|
|
900
|
+
area += poly[i].cross(poly[j]);
|
|
901
|
+
}
|
|
902
|
+
return area;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// ensure counter-clockwise winding (slice first so we don't mutate caller's array)
|
|
906
|
+
if (signedArea(points) < 0)
|
|
907
|
+
points = points.slice().reverse();
|
|
908
|
+
|
|
909
|
+
// check if point is inside triangle
|
|
910
|
+
const e = 1e-9;
|
|
911
|
+
const pointInTriangle = (p, a, b, c)=>
|
|
912
|
+
{
|
|
913
|
+
const c1 = cross(a, b, p);
|
|
914
|
+
const c2 = cross(b, c, p);
|
|
915
|
+
const c3 = cross(c, a, p);
|
|
916
|
+
const negative = (c1<-e?1:0) + (c2<-e?1:0) + (c3<-e?1:0);
|
|
917
|
+
const positive = (c1> e?1:0) + (c2> e?1:0) + (c3> e?1:0);
|
|
918
|
+
return !(negative && positive);
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
// ear clipping triangulation
|
|
922
|
+
const indices = [];
|
|
923
|
+
for (let i = 0; i < points.length; ++i)
|
|
924
|
+
indices[i] = i;
|
|
925
|
+
const triangles = [];
|
|
926
|
+
let attempts = 0;
|
|
927
|
+
const maxAttempts = points.length ** 2 + 100;
|
|
928
|
+
while (indices.length > 3 && attempts++ < maxAttempts)
|
|
929
|
+
{
|
|
930
|
+
let foundEar = false;
|
|
931
|
+
for (let i = 0; i < indices.length; i++)
|
|
932
|
+
{
|
|
933
|
+
const i0 = indices[(i + indices.length - 1) % indices.length];
|
|
934
|
+
const i1 = indices[i];
|
|
935
|
+
const i2 = indices[(i + 1) % indices.length];
|
|
936
|
+
const a = points[i0], b = points[i1], c = points[i2];
|
|
937
|
+
|
|
938
|
+
// check if convex
|
|
939
|
+
if (cross(a, b, c) < e) continue;
|
|
940
|
+
|
|
941
|
+
// check if any other point is inside
|
|
942
|
+
let hasInside = false;
|
|
943
|
+
for (let j = 0; j < indices.length; j++)
|
|
944
|
+
{
|
|
945
|
+
const k = indices[j];
|
|
946
|
+
if (k === i0 || k === i1 || k === i2) continue;
|
|
947
|
+
|
|
948
|
+
const p = points[k];
|
|
949
|
+
hasInside = pointInTriangle(p, a, b, c);
|
|
950
|
+
if (hasInside) break;
|
|
951
|
+
}
|
|
952
|
+
if (hasInside) continue;
|
|
953
|
+
|
|
954
|
+
// found valid ear
|
|
955
|
+
triangles.push([i0, i1, i2]);
|
|
956
|
+
indices.splice(i, 1);
|
|
957
|
+
foundEar = true;
|
|
958
|
+
break;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// fallback for degenerate cases
|
|
962
|
+
if (!foundEar)
|
|
963
|
+
{
|
|
964
|
+
let worstIndex = -1, worstValue = Infinity;
|
|
965
|
+
for (let i = 0; i < indices.length; i++)
|
|
966
|
+
{
|
|
967
|
+
const i0 = indices[(i + indices.length - 1) % indices.length];
|
|
968
|
+
const i1 = indices[i];
|
|
969
|
+
const i2 = indices[(i + 1) % indices.length];
|
|
970
|
+
const value = abs(cross(points[i0], points[i1], points[i2]));
|
|
971
|
+
if (value < worstValue)
|
|
972
|
+
{
|
|
973
|
+
worstValue = value;
|
|
974
|
+
worstIndex = i;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
if (worstIndex < 0) break;
|
|
978
|
+
|
|
979
|
+
const i0 = indices[(worstIndex + indices.length - 1) % indices.length];
|
|
980
|
+
const i1 = indices[worstIndex];
|
|
981
|
+
const i2 = indices[(worstIndex + 1) % indices.length];
|
|
982
|
+
triangles.push([i0, i1, i2]);
|
|
983
|
+
indices.splice(worstIndex, 1);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// add final triangle
|
|
988
|
+
if (indices.length === 3)
|
|
989
|
+
triangles.push([indices[0], indices[1], indices[2]]);
|
|
990
|
+
if (!triangles.length)
|
|
991
|
+
return [];
|
|
992
|
+
|
|
993
|
+
// convert triangles to triangle strip with degenerate connectors
|
|
994
|
+
const strip = [];
|
|
995
|
+
let [a0, b0, c0] = triangles[0];
|
|
996
|
+
strip.push(points[a0], points[b0], points[c0]);
|
|
997
|
+
for (let i = 1; i < triangles.length; i++)
|
|
998
|
+
{
|
|
999
|
+
// add degenerate bridge from last vertex to first of new triangle
|
|
1000
|
+
const [a, b, c] = triangles[i];
|
|
1001
|
+
strip.push(points[c0], points[a]);
|
|
1002
|
+
strip.push(points[a], points[b], points[c]);
|
|
1003
|
+
c0 = c;
|
|
1004
|
+
}
|
|
1005
|
+
return strip;
|
|
942
1006
|
}
|