-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtriangle.ts
1862 lines (1649 loc) · 72.3 KB
/
triangle.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
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
586
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
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
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Helper constants for fixed-point math. TODO: switch to the Fx8 class?
const FP_BITS = 8
const FP_BITS_SQ = FP_BITS * 2
const FP_BITS_3 = FP_BITS * 3
const FP_ONE = 1 << FP_BITS
const FP_ONE_SQ = FP_ONE << FP_BITS
const FP_ONE_MASK = FP_ONE - 1
const FP_ONE_SQ_MASK = FP_ONE_SQ - 1
namespace simpleperf {
export let isEnabled: boolean = false
class Counter {
name: string
calls: number
microsAll: number
microsShallow: number
started: number
constructor(name: string) {
this.name = name
this.reset()
}
reset() {
this.calls = 0
this.microsAll = 0
this.microsShallow = 0
this.started = 0
}
start() {
if (!isEnabled) return
stack.push(this)
++this.calls
this.started = control.micros()
}
end() {
if (!isEnabled) return
// Ignore the sign/higher bits of the elapsed time to avoid
// glitches when the microsecond timer wraps. (Every ~18minutes.)
const elapsed = (control.micros() - this.started) & 0x1fffffff
this.microsAll += elapsed
this.microsShallow += elapsed
const popped = stack.pop()
if (popped != this) throw("bad perf stack nesting")
if (stack.length > 0) {
stack[stack.length - 1].microsShallow -= elapsed
}
}
}
const counters: Counter[] = []
let stack: Counter[] = []
let startTick: number = 0
let endTick: number = 0
export function getCounter(name: string) {
const counter: Counter = new Counter(name)
counters.push(counter)
return counter
}
const perfUntraced = getCounter("(untraced)")
export function enable() {
stack = []
for (let i = 0; i < counters.length; ++i) {
counters[i].reset()
}
startTick = control.micros()
isEnabled = true
}
export function getResultString(exclusive: boolean) {
let out = ""
let elapsed = endTick - startTick
let totalMicros = 0
perfUntraced.microsShallow = 0
for (let i = 0; i < counters.length; ++i) {
totalMicros += counters[i].microsShallow
}
perfUntraced.microsShallow = elapsed - totalMicros
perfUntraced.microsAll = perfUntraced.microsShallow
if (stack.length) throw("stack not empty")
out += "Trace results, sorted by " + (exclusive ? "exclusive" : "inclusive") + "%\n\xa0\n"
out += "Format: excl%/incl% (excl time/incl time, count)\n\xa0\n"
out += "elapsed time: " + Math.round(elapsed / 1000) + "ms\n"
out += "traced time: " + Math.round(totalMicros / 1000) + "ms\n\xa0\n"
if (exclusive) {
counters.sort((a, b) => b.microsShallow - a.microsShallow)
} else {
counters.sort((a, b) => b.microsAll - a.microsAll)
}
for (let i = 0; i < counters.length; ++i) {
const pctAll = Math.round(100 * counters[i].microsAll / elapsed)
const pctShallow = Math.round(100 * counters[i].microsShallow / elapsed)
const shallow = Math.round(counters[i].microsShallow / 1000)
const all = Math.round(counters[i].microsAll / 1000)
out += pctShallow + "%/" + pctAll + "% " + counters[i].name + " (" + shallow + "ms/" + all + "ms " + counters[i].calls + "x)\n"
}
return out
}
export function disableAndShowResults() {
isEnabled = false
endTick = control.micros()
// TODO: this pushScene appears to be necessary to avoid a 021 error (too many objects) on hardware
game.pushScene()
game.showLongText(getResultString(true), DialogLayout.Full)
game.showLongText(getResultString(false), DialogLayout.Full)
game.popScene()
}
}
const perfPrepareFrame = simpleperf.getCounter("prepareFrame")
const perfDrawFrame = simpleperf.getCounter("drawFrame")
const perfDrawFrameRead = simpleperf.getCounter("drawFrameRead")
const perfDrawFrameSort = simpleperf.getCounter("drawFrameSort")
const perfDrawFrameBuf = simpleperf.getCounter("drawFrameBuf")
const perfPreRender = simpleperf.getCounter("preRender")
const perfVertexTransform = simpleperf.getCounter("vertexXform")
const perfClipWorld = simpleperf.getCounter("clipWorld")
const perfPerspective = simpleperf.getCounter("perspective")
const perfaddTrapezoids = simpleperf.getCounter("addTrapezoids")
const perfOutTri = simpleperf.getCounter("outTri")
const perfOutTriSimple = simpleperf.getCounter("outTriSimple")
const perfClipPoly = simpleperf.getCounter("clipPoly")
const perfClipEdge = simpleperf.getCounter("clipEdge")
interface Fx16 {
_dummyFx16: string;
}
namespace Fx {
export function toIntFloor(a: Fx8) {
return (a as any as number) >> 8
}
export function frac(a: Fx8) {
return ((a as any as number) & 255) as any as Fx8
}
export function frac2(a: Fx8) {
return ((a as any as number) & 511) as any as Fx8
}
export function mul16(a: Fx8, b: Fx8): Fx16 {
return (Math.imul((a as any as number), (b as any as number))) as any as Fx16
}
export function add3(a: Fx16, b: Fx16, c: Fx16): Fx8 {
return ((a as any as number) + (b as any as number) + (c as any as number) >> 8) as any as Fx8
}
export function add4(a: Fx16, b: Fx16, c: Fx16, d: Fx8): Fx8 {
return (((a as any as number) + (b as any as number) + (c as any as number) >> 8) + (d as any as number)) as any as Fx8
}
export function multiplyAdd3(a0: Fx8, b0: Fx8, a1: Fx8, b1: Fx8, a2: Fx8, b2: Fx8): Fx8 {
return (Math.imul(a0 as any as number, b0 as any as number) +
Math.imul(a1 as any as number, b1 as any as number) +
Math.imul(a2 as any as number, b2 as any as number) >> 8) as any as Fx8
}
export function multiplyAdd4(a0: Fx8, b0: Fx8, a1: Fx8, b1: Fx8, a2: Fx8, b2: Fx8, c: Fx8): Fx8 {
return ((Math.imul(a0 as any as number, b0 as any as number) +
Math.imul(a1 as any as number, b1 as any as number) +
Math.imul(a2 as any as number, b2 as any as number) >> 8) + (c as any as number)) as any as Fx8
}
}
function Fx8_to_FP(a: Fx8) {
// assumes FP_BITS is 8
return (a as any as number)
}
function FP_to_Fx8(a: number): Fx8 {
// assumes FP_BITS is 8
return (a as any as number) as any as Fx8
}
namespace renderer3d {
/*
export function drawTriLineAlternatingSwapped(tri: ActiveTrapezoid, buf: Buffer, col0: number, col1: number) {
renderer3d.drawTriLineAlternating(tri, buf, col1, col0)
}
export function drawTriLineFlatCol1(tri: ActiveTrapezoid, buf: Buffer, col0: number, col1: number) {
renderer3d.drawTriLineFlat(tri, buf, col1)
}
*/
export function drawTriLineAlternating(tri: ActiveTrapezoid, buf: Buffer, col0: number, col1: number) {
const y0 = Fx.toIntFloor(tri.a_y)
const y1 = Fx.toIntFloor(tri.b_y)
let y = y0
if (y & 1) {
// Swap the colors so that col0 is consistently on even Y
const tmp = col0
col0 = col1
col1 = tmp
}
let y1end = y1 - 7
while (y <= y1end) {
buf.setUint8(y, col0)
buf.setUint8(y + 1, col1)
buf.setUint8(y + 2, col0)
buf.setUint8(y + 3, col1)
buf.setUint8(y + 4, col0)
buf.setUint8(y + 5, col1)
buf.setUint8(y + 6, col0)
buf.setUint8(y + 7, col1)
y += 8
}
if (y <= y1 - 3) {
buf.setUint8(y, col0)
buf.setUint8(y + 1, col1)
buf.setUint8(y + 2, col0)
buf.setUint8(y + 3, col1)
y += 4
}
if (y <= y1 - 1) {
buf.setUint8(y, col0)
buf.setUint8(y + 1, col1)
y += 2
}
if (y <= y1) {
buf.setUint8(y, col0)
//++y
}
tri.a_y = Fx.add(tri.a_y, tri.a_dydx)
tri.b_y = Fx.add(tri.b_y, tri.b_dydx)
}
export function drawTriLineFlat(tri: ActiveTrapezoid, buf: Buffer, col: number) {
//console.log("x=" + x + " tri.x0=" + tri.x0 + " tri.z=" + tri.z)
const y0 = Fx.toIntFloor(tri.a_y)
const y1 = Fx.toIntFloor(tri.b_y)
let y = y0
let y1end = y1 - 7
while (y <= y1end) {
buf.setUint8(y, col)
buf.setUint8(y + 1, col)
buf.setUint8(y + 2, col)
buf.setUint8(y + 3, col)
buf.setUint8(y + 4, col)
buf.setUint8(y + 5, col)
buf.setUint8(y + 6, col)
buf.setUint8(y + 7, col)
y += 8
}
if (y <= y1 - 3) {
buf.setUint8(y, col)
buf.setUint8(y + 1, col)
buf.setUint8(y + 2, col)
buf.setUint8(y + 3, col)
y += 4
}
if (y <= y1 - 1) {
buf.setUint8(y, col)
buf.setUint8(y + 1, col)
y += 2
}
if (y <= y1) {
buf.setUint8(y, col)
//++y
}
tri.a_y = Fx.add(tri.a_y, tri.a_dydx)
tri.b_y = Fx.add(tri.b_y, tri.b_dydx)
}
}
class Renderer3d {
// Storage for the Drawables, indexed by starting X coordinate
xstarts: Trapezoid[][]
columnBuffer: Buffer
lightDirection: number[]
viewerFromWorld: number[]
_nextOrderNum: number
// Configurable settings
useFlatShading: boolean
// dither pattern:
// 0 3
// 2 1
//static dither0 = [0, 2]
//static dither1 = [3, 1]
/*
// The indirect function call seems to be slower than conditionals
static ditherEven = [
renderer3d.drawTriLineFlat,
renderer3d.drawTriLineAlternating,
renderer3d.drawTriLineAlternating,
renderer3d.drawTriLineAlternating]
static ditherOdd = [
renderer3d.drawTriLineFlat,
renderer3d.drawTriLineFlat,
renderer3d.drawTriLineAlternatingSwapped,
renderer3d.drawTriLineFlatCol1]
*/
constructor() {
this.xstarts = []
this.columnBuffer = Buffer.create(120)
this.useFlatShading = true
this.lightDirection = [0, 0, -1]
this.viewerFromWorld = []
}
setPaletteGrayscale() {
const p = palette.defaultPalette();
for (let i = 0; i < p.length; ++i) {
p.setColor(i, color.rgb(i * 16, i * 16, i * 16));
}
p.setColor(0, 0)
//p.setColor(1, color.rgb(255, 0, 0))
palette.setColors(p)
}
setPaletteDefault() {
const p = palette.defaultPalette();
p.setColor(0, 0)
palette.setColors(p)
}
setLightAngles(alphaDegrees: number, betaDegrees: number) {
const alpha = alphaDegrees * Math.PI / 180
const beta = betaDegrees * Math.PI / 180
this.lightDirection[0] = Math.floor(Math.cos(alpha) * Math.cos(beta) * FP_ONE)
this.lightDirection[1] = Math.floor(Math.sin(beta) * FP_ONE)
this.lightDirection[2] = Math.floor(Math.sin(alpha) * Math.cos(beta) * FP_ONE)
//normalizeFP(lightDirection) - not needed, already normalized
}
setViewerPose(poseMatrix: number[]) {
setInverseTransformFP_from_Float(this.viewerFromWorld, poseMatrix)
}
prepareFrame() {
perfPrepareFrame.start()
Trapezoid.resetInstances()
if (!this.xstarts.length) {
for (let x = 0; x < 160; ++x) {
this.xstarts[x] = []
}
}
this._nextOrderNum = 1
perfPrepareFrame.end()
}
getOrderNum() {
return this._nextOrderNum++
}
freeResources() {
Trapezoid.eraseInstances()
this.xstarts = []
}
drawFrame(image: Image) {
perfDrawFrame.start()
//control.enablePerfCounter()
//console.logValue("trapezoids", Trapezoid.instNum)
const buf = this.columnBuffer
const xstarts = this.xstarts
if (!xstarts.length) {
perfDrawFrame.end()
return
}
//ActiveTrapezoid.resetInstances()
let active: ActiveTrapezoid[] = []
let activeOut: ActiveTrapezoid[] = []
let nextEnd = 999
for (let x = 0; x < 160; ++x) {
perfDrawFrameSort.start()
// Update the active trapezoid list. Need to remove the ones that
// have passed their end coordinate (x1), and add new ones from xstarts.
if (x >= nextEnd || xstarts[x].length) {
//console.log("x=" + x + " **********************")
nextEnd = 999
const toMerge: Trapezoid[] = xstarts[x]
//console.log(" active=" + active.map(v => "o=" + v.base.order + ",x1=" + v.base.x1).join(", "))
//console.log(" toMerge=" + toMerge.map(v => "o=" + v.order + ",x1=" + v.x1).join(", "))
// Do a merge sort, combining the already-sorted active array with the new entries.
let j = 0 // output index
if (toMerge.length) {
// sort the xstarts that need merging in ascending order
toMerge.sort((a, b) => a.order - b.order)
let aidx = 0 // for the active array
let midx = 0 // for the toMerge array
// While both arrays have elements, take the smaller then check again.
while (aidx < active.length && midx < toMerge.length) {
if (active[aidx].base.x1 < x) {
// This one is past its end, skip it
++aidx
} else {
let v: ActiveTrapezoid
if (active[aidx].base.order < toMerge[midx].order) {
v = active[aidx++]
} else {
v = new ActiveTrapezoid(toMerge[midx++])
}
const vx1 = v.base.x1
if (vx1 < nextEnd) nextEnd = vx1
activeOut[j++] = v
}
}
// Copy whichever array has leftovers (should be only one of them)
while (aidx < active.length) {
const v = active[aidx++]
const vx1 = v.base.x1
if (vx1 < x) continue
if (vx1 < nextEnd) nextEnd = vx1
activeOut[j++] = v
}
while (midx < toMerge.length) {
const v = new ActiveTrapezoid(toMerge[midx++])
const vx1 = v.base.x1
if (vx1 < nextEnd) nextEnd = vx1
activeOut[j++] = v
}
xstarts[x] = []
} else {
// Nothing to merge. Just delete trapezoids that have passed the end.
for (let i = 0; i < active.length; ++i) {
const v = active[i]
const vx1 = v.base.x1
if (vx1 < x) continue
if (vx1 < nextEnd) nextEnd = vx1
activeOut[j++] = v
}
}
//console.log("x=" + x + " nextEnd=" + nextEnd + " activeOut=" + activeOut.map(v => "ord=" + v.base.order + " x1=" + v.base.x1).join(", "))
// Delete any leftover entries in activeOut that weren't overwritten.
activeOut.splice(j, activeOut.length)
// Swap the arrays for next time
const tmp = active
active = activeOut
activeOut = tmp
}
/*
active = active.filter(v => v.base.x1 >= x)
if (xstarts[x].length) {
while (xstarts[x].length) {
let d : Drawable = xstarts[x].pop()
active.push(new ActiveTrapezoid(d as Trapezoid))
}
// Sort in ascending priority order, drawing lower numbers first.
// This isn't necessarily a Z value. When using a strict painter's algorithm,
// this is simply the object's sequence number.
active.sort((a, b) => a.base.order - b.base.order)
}
*/
perfDrawFrameSort.end()
if (!active.length) continue
perfDrawFrameRead.start()
image.getRows(x, buf)
perfDrawFrameRead.end()
//const dither = x & 1 ? Renderer3d.dither1 : Renderer3d.dither0
//const dither = x & 1 ? Renderer3d.ditherOdd : Renderer3d.ditherEven
perfDrawFrameBuf.start()
if (this.useFlatShading) {
for (let l = 0; l < active.length; ++l) {
const tri = active[l]
renderer3d.drawTriLineFlat(tri, buf, tri.base.color >> 2)
}
} else {
/*
for (let l = 0; l < active.length; ++l) {
let tri = active[l]
//console.log("x=" + x + " tri.x0=" + tri.x0 + " tri.z=" + tri.z)
const colBase = tri.base.color
const col0 = colBase >> 2
const col1 = col0 + 1
const colIncr = colBase & 3
dither[colIncr](tri, buf, col0, col1)
}
*/
if (x & 1) {
for (let l = 0; l < active.length; ++l) {
let tri = active[l]
//console.log("x=" + x + " tri.x0=" + tri.x0 + " tri.z=" + tri.z)
const colBase = tri.base.color
const col0 = colBase >> 2
const col1 = col0 + 1
const colIncr = colBase & 3
if (colIncr <= 1) {
renderer3d.drawTriLineFlat(tri, buf, col0)
} else if (colIncr == 3) {
renderer3d.drawTriLineFlat(tri, buf, col1)
} else { //if (colIncr == 2)
renderer3d.drawTriLineAlternating(tri, buf, col1, col0)
}
}
} else {
for (let l = 0; l < active.length; ++l) {
let tri = active[l]
//console.log("x=" + x + " tri.x0=" + tri.x0 + " tri.z=" + tri.z)
const colBase = tri.base.color
const col0 = colBase >> 2
const colIncr = colBase & 3
if (colIncr == 0) {
renderer3d.drawTriLineFlat(tri, buf, col0)
} else {
const col1 = col0 + 1
renderer3d.drawTriLineAlternating(tri, buf, col0, col1)
}
}
}
/*
for (let l = 0; l < active.length; ++l) {
let tri = active[l]
//console.log("x=" + x + " tri.x0=" + tri.x0 + " tri.z=" + tri.z)
const colBase = tri.base.color
const col0 = colBase >> 2
const col1 = col0 + 1
const colIncr = colBase & 3
if (xbit) {
if (colIncr <= 1) {
Renderer3d.drawTriLineFlat(tri, buf, col0)
} else if (colIncr == 3) {
Renderer3d.drawTriLineFlat(tri, buf, col1)
} else { //if (colIncr == 2)
Renderer3d.drawTriLineAlternating(tri, buf, col1, col0)
}
} else {
if (colIncr == 0) {
Renderer3d.drawTriLineFlat(tri, buf, col0)
} else {
Renderer3d.drawTriLineAlternating(tri, buf, col0, col1)
}
}
}
*/
/*
const col = tri.base.color
let ybit = y0 & 1
const y0 = Fx.toIntFloor(tri.a_y)
const y1 = Fx.toIntFloor(tri.b_y)
for (let y = y0; y <= y1; ++y) {
// Sanity check that color values are integers. Don't keep this enabled
// when deploying on hardware, it slows things down a lot.
//if (col < 0 || col > 63 || col != Math.floor(col)) console.log("col=" + col)
buf.setUint8(y, col + dither[ybit] >> 2)
ybit = 1 - ybit
}
tri.a_y = Fx.add(tri.a_y, tri.a_dydx)
tri.b_y = Fx.add(tri.b_y, tri.b_dydx)
*/
}
image.setRows(x, buf)
perfDrawFrameBuf.end()
}
perfDrawFrame.end()
}
}
// Matrices are 3 rows x 4 columns, following OpenGL conventions but
// omitting the fourth row which is always (0, 0, 0, 1).
//
// m0 m3 m6 m9
// m1 m4 m7 m10
// m2 m5 m8 m11
//
// This is stored as a plain array in column-major order:
// [m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11]
//
// Geometrically, this combines a rotation and position.
// Space B's origin in space A coordinates is at (ax, ay, az).
// Space B's X axis is in direction (ux, uy, uz) in space A coordinates.
// Space B's Y axis is in direction (vx, vy, vz) in space A coordinates.
// Space B's Z axis is in direction (wx, wy, wz) in space A coordinates.
//
// This matrix product transforms a point in space B coordinates (bx, by, bz)
// to space A coordinates (ax, ay, az):
//
// ux vx wx px * bx = ax = ux*bx + vx*by + wx*bz + ax
// uy vy wy py by ay uy*bx + vy*by + wy*bz + ay
// uz vz wz pz bz az uz*bx + vz*by + wz*bz + az
// 1 1 1
function setInverseTransformFP_from_Float(o: number[], m: number[]) {
// Inverse of a rotation + translation matrix, converted to fixed point.
// Transpose the translation part, and apply the transposed (inverse)
// rotation to the original translation followed by negating it.
o[0] = Math.floor(m[0] * FP_ONE)
o[1] = Math.floor(m[3] * FP_ONE)
o[2] = Math.floor(m[6] * FP_ONE)
o[3] = Math.floor(m[1] * FP_ONE)
o[4] = Math.floor(m[4] * FP_ONE)
o[5] = Math.floor(m[7] * FP_ONE)
o[6] = Math.floor(m[2] * FP_ONE)
o[7] = Math.floor(m[5] * FP_ONE)
o[8] = Math.floor(m[8] * FP_ONE)
o[9] = Math.floor(-o[0] * m[9] - o[3] * m[10] - o[6] * m[11])
o[10] = Math.floor(-o[1] * m[9] - o[4] * m[10] - o[7] * m[11])
o[11] = Math.floor(-o[2] * m[9] - o[5] * m[10] - o[8] * m[11])
//console.log('m=' + m.join(' '))
//console.log('o=' + o.join(' '))
}
function vec_applyInverseTransformToOriginFP(out: number[], m: number[]) {
out[0] = (-m[0] * m[9] - m[1] * m[10] - m[2] * m[11]) >> FP_BITS
out[1] = (-m[3] * m[9] - m[4] * m[10] - m[5] * m[11]) >> FP_BITS
out[2] = (-m[6] * m[9] - m[7] * m[10] - m[8] * m[11]) >> FP_BITS
}
function vec_convert_to_FP(out: number[], v: number[]) {
out[0] = Math.floor(v[0] * FP_ONE)
out[1] = Math.floor(v[1] * FP_ONE)
out[2] = Math.floor(v[2] * FP_ONE)
}
function vec_convert_to_Fx8(out: Fx8[], v: number[]) {
out[0] = Fx8(v[0])
out[1] = Fx8(v[1])
out[2] = Fx8(v[2])
}
function normalizeFP(v: number[]) {
let len = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2])
v[0] = Math.floor(v[0] * FP_ONE / len)
v[1] = Math.floor(v[1] * FP_ONE / len)
v[2] = Math.floor(v[2] * FP_ONE / len)
}
function dot_FP(aFP: number[], bFP: number[]) {
const a = (aFP as any as Fx8[])
const b = (bFP as any as Fx8[])
/*
return Math.imul(a[0], b[0]) + Math.imul(a[1], b[1]) + Math.imul(a[2], b[2]) >> FP_BITS
*/
return Fx8_to_FP(Fx.multiplyAdd3(a[0], b[0], a[1], b[1], a[2], b[2]))
}
function mat_setIdentity_FP(m: number[]) {
m[0] = m[4] = m[8] = FP_ONE
m[1] = m[2] = m[3] = 0
m[5] = m[6] = m[7] = 0
m[9] = m[10] = m[11] = 0
}
function mat33_copy(out: number[], m: number[]) {
out[0] = m[0]
out[1] = m[1]
out[2] = m[2]
out[3] = m[3]
out[4] = m[4]
out[5] = m[5]
out[6] = m[6]
out[7] = m[7]
out[8] = m[8]
}
// Multiply two 3D rotation matrices, leaving the non-rotation parts unchanged
function mul_mat33_mat33_partial_FP(outFP: number[], aFP: number[], bFP: number[]) {
/*
out[0] = (a[0] * b[0] + a[3] * b[1] + a[6] * b[2] >> FP_BITS)
out[1] = (a[1] * b[0] + a[4] * b[1] + a[7] * b[2] >> FP_BITS)
out[2] = (a[2] * b[0] + a[5] * b[1] + a[8] * b[2] >> FP_BITS)
out[3] = (a[0] * b[3] + a[3] * b[4] + a[6] * b[5] >> FP_BITS)
out[4] = (a[1] * b[3] + a[4] * b[4] + a[7] * b[5] >> FP_BITS)
out[5] = (a[2] * b[3] + a[5] * b[4] + a[8] * b[5] >> FP_BITS)
out[6] = (a[0] * b[6] + a[3] * b[7] + a[6] * b[8] >> FP_BITS)
out[7] = (a[1] * b[6] + a[4] * b[7] + a[7] * b[8] >> FP_BITS)
out[8] = (a[2] * b[6] + a[5] * b[7] + a[8] * b[8] >> FP_BITS)
*/
const out = (outFP as any as Fx8[])
const a = (aFP as any as Fx8[])
const b = (bFP as any as Fx8[])
out[0] = Fx.multiplyAdd3(a[0], b[0], a[3], b[1], a[6], b[2])
out[1] = Fx.multiplyAdd3(a[1], b[0], a[4], b[1], a[7], b[2])
out[2] = Fx.multiplyAdd3(a[2], b[0], a[5], b[1], a[8], b[2])
out[3] = Fx.multiplyAdd3(a[0], b[3], a[3], b[4], a[6], b[5])
out[4] = Fx.multiplyAdd3(a[1], b[3], a[4], b[4], a[7], b[5])
out[5] = Fx.multiplyAdd3(a[2], b[3], a[5], b[4], a[8], b[5])
out[6] = Fx.multiplyAdd3(a[0], b[6], a[3], b[7], a[6], b[8])
out[7] = Fx.multiplyAdd3(a[1], b[6], a[4], b[7], a[7], b[8])
out[8] = Fx.multiplyAdd3(a[2], b[6], a[5], b[7], a[8], b[8])
/*
out[1] = Fx.add3(Fx.mul16(a[1], b[0]), Fx.mul16(a[4], b[1]), Fx.mul16(a[7], b[2]))
out[2] = Fx.add3(Fx.mul16(a[2], b[0]), Fx.mul16(a[5], b[1]), Fx.mul16(a[8], b[2]))
out[3] = Fx.add3(Fx.mul16(a[0], b[3]), Fx.mul16(a[3], b[4]), Fx.mul16(a[6], b[5]))
out[4] = Fx.add3(Fx.mul16(a[1], b[3]), Fx.mul16(a[4], b[4]), Fx.mul16(a[7], b[5]))
out[5] = Fx.add3(Fx.mul16(a[2], b[3]), Fx.mul16(a[5], b[4]), Fx.mul16(a[8], b[5]))
out[6] = Fx.add3(Fx.mul16(a[0], b[6]), Fx.mul16(a[3], b[7]), Fx.mul16(a[6], b[8]))
out[7] = Fx.add3(Fx.mul16(a[1], b[6]), Fx.mul16(a[4], b[7]), Fx.mul16(a[7], b[8]))
out[8] = Fx.add3(Fx.mul16(a[2], b[6]), Fx.mul16(a[5], b[7]), Fx.mul16(a[8], b[8]))
*/
}
function mul_mat33_mat33_full_FP(out: number[], a: number[], b: number[]) {
mul_mat33_mat33_partial_FP(out, a, b)
out[9] = out[10] = out[11] = 0
}
function mul_mat43_mat43_FP(outFP: number[], aFP: number[], bFP: number[]) {
mul_mat33_mat33_partial_FP(outFP, aFP, bFP)
/*
out[9] = (a[0] * b[9] + a[3] * b[10] + a[6] * b[11] >> FP_BITS) + a[9]
out[10] = (a[1] * b[9] + a[4] * b[10] + a[7] * b[11] >> FP_BITS) + a[10]
out[11] = (a[2] * b[9] + a[5] * b[10] + a[8] * b[11] >> FP_BITS) + a[11]
*/
const out = (outFP as any as Fx8[])
const a = (aFP as any as Fx8[])
const b = (bFP as any as Fx8[])
out[ 9] = Fx.multiplyAdd4(a[0], b[9], a[3], b[10], a[6], b[11], a[ 9])
out[10] = Fx.multiplyAdd4(a[1], b[9], a[4], b[10], a[7], b[11], a[10])
out[11] = Fx.multiplyAdd4(a[2], b[9], a[5], b[10], a[8], b[11], a[11])
}
// Apply rotation and translation
function mul_mat43_vec_FP(outFP: number[], mFP: number[], vFP: number[]) {
const out = (outFP as any as Fx8[])
const m = (mFP as any as Fx8[])
const v = (vFP as any as Fx8[])
/*
out[0] = (Math.imul(m[0], v[0]) + Math.imul(m[3], v[1]) + Math.imul(m[6], v[2]) >> FP_BITS) + m[9]
out[1] = (Math.imul(m[1], v[0]) + Math.imul(m[4], v[1]) + Math.imul(m[7], v[2]) >> FP_BITS) + m[10]
out[2] = (Math.imul(m[2], v[0]) + Math.imul(m[5], v[1]) + Math.imul(m[8], v[2]) >> FP_BITS) + m[11]
*/
out[0] = Fx.multiplyAdd4(m[0], v[0], m[3], v[1], m[6], v[2], m[ 9])
out[1] = Fx.multiplyAdd4(m[1], v[0], m[4], v[1], m[7], v[2], m[10])
out[2] = Fx.multiplyAdd4(m[2], v[0], m[5], v[1], m[8], v[2], m[11])
}
// Apply rotation only, intended for normal vectors
function mul_mat33_vec_FP(outFP: number[], mFP: number[], vFP: number[]) {
const out = (outFP as any as Fx8[])
const m = (mFP as any as Fx8[])
const v = (vFP as any as Fx8[])
/*
out[0] = (Math.imul(m[0], v[0]) + Math.imul(m[3], v[1]) + Math.imul(m[6], v[2]) >> FP_BITS)
out[1] = (Math.imul(m[1], v[0]) + Math.imul(m[4], v[1]) + Math.imul(m[7], v[2]) >> FP_BITS)
out[2] = (Math.imul(m[2], v[0]) + Math.imul(m[5], v[1]) + Math.imul(m[8], v[2]) >> FP_BITS)
*/
out[0] = Fx.multiplyAdd3(m[0], v[0], m[3], v[1], m[6], v[2])
out[1] = Fx.multiplyAdd3(m[1], v[0], m[4], v[1], m[7], v[2])
out[2] = Fx.multiplyAdd3(m[2], v[0], m[5], v[1], m[8], v[2])
}
namespace Fx {
// For background on the sine approximation, see:
// https://github.com/microsoft/pxt-common-packages/pull/1178/files
const sineConstMul = 1 << 15
const sineApproxC = Math.round(0.0721435357258 * sineConstMul)
const sineApproxB = Math.round(-0.642443736562 * sineConstMul)
const sineApproxA = Math.round(1.57030020084 * sineConstMul)
export function sinFx8(angleTwos: Fx8): Fx8 {
const phase = (angleTwos as any as number) & 511
const isSecondHalf = phase & 256
let p = isSecondHalf ? phase - 256 : phase
if (p > 128) p = 256 - p
// Calculate using y = ((c * x^2 + b) * x^2 + a) * x
//
// The position p is x * 128, so after each multiply with p we need to
// shift right by 7 bits to keep the decimal point in the same place. (The
// approximation has a negative error near x=1 which helps avoid overflow.)
const p2 = Math.imul(p, p)
//if (Math.abs(Math.imul(sineApproxC, p2) + 0x6000) >= 0x40000000) throw("too big")
const u = (Math.imul(sineApproxC, p2) + 0x6000 >> 14) + sineApproxB
//if (Math.abs(Math.imul(u, p2) + 0x6000) >= 0x40000000) throw("too big")
const v = (Math.imul(u, p2) + 0x6000 >> 14) + sineApproxA
//if (Math.abs(Math.imul(v, p) + 0x2000) >= 0x40000000) throw("too big")
const w = Math.imul(v, p) + 0x2000 >> 14
// The result is exact within the 8-bit Fx8 precision.
return (isSecondHalf ? -w : w) as any as Fx8
}
export function cosFx8(angleTwos: Fx8) {
return sinFx8(((angleTwos as any as number) + 128) as any as Fx8)
}
/*
for (let i = 0; i <= 128; ++i) {
const v = sinFx8(i as any as Fx8) as any as number
const s = Math.round(256 * Math.sin(i * Math.PI / 256))
console.log("sinFx8(" + i + ")=" + sinFx8(i as any as Fx8) + " sin=" + s + " error=" + (v - s))
}
console.log("sin(45)=" + Fx.toFloat(sinFx8(64 as any as Fx8)))
console.log("cos(45)=" + Fx.toFloat(cosFx8(64 as any as Fx8)))
console.log("sin(90)=" + Fx.toFloat(sinFx8(128 as any as Fx8)))
console.log("cos(90)=" + Fx.toFloat(cosFx8(128 as any as Fx8)))
console.log("sin(0)=" + Fx.toFloat(sinFx8(0 as any as Fx8)))
console.log("cos(0)=" + Fx.toFloat(cosFx8(0 as any as Fx8)))
throw("stop")
*/
}
// Sets the 3x3 submatrix of the destination matrix to the product of a second
// matrix and a rotation angle. Leaves the positions in the destination
// matrix unchanged.
function mul_mat33_rotate33_columns_FP(out: number[], m: number[], angleTwos: Fx8, a: number, b: number, other: number) {
let s = Fx8_to_FP(Fx.sinFx8(angleTwos))
let c = Fx8_to_FP(Fx.cosFx8(angleTwos))
out[a] = Math.imul(m[a], c) + Math.imul(m[b], s) >> FP_BITS
out[a + 1] = Math.imul(m[a + 1], c) + Math.imul(m[b + 1], s) >> FP_BITS
out[a + 2] = Math.imul(m[a + 2], c) + Math.imul(m[b + 2], s) >> FP_BITS
out[b] = Math.imul(m[b], c) - Math.imul(m[a], s) >> FP_BITS
out[b + 1] = Math.imul(m[b + 1], c) - Math.imul(m[a + 1], s) >> FP_BITS
out[b + 2] = Math.imul(m[b + 2], c) - Math.imul(m[a + 2], s) >> FP_BITS
out[other] = m[other]
out[other + 1] = m[other + 1]
out[other + 2] = m[other + 2]
}
function mul_mat33_rotateX_partial_FP(out: number[], m: number[], angleTwos: Fx8) {
mul_mat33_rotate33_columns_FP(out, m, angleTwos, 3, 6, 0)
}
function mul_mat33_rotateY_partial_FP(out: number[], m: number[], angleTwos: Fx8) {
mul_mat33_rotate33_columns_FP(out, m, angleTwos, 6, 0, 3)
}
function mul_mat33_rotateZ_partial_FP(out: number[], m: number[], angleTwos: Fx8) {
mul_mat33_rotate33_columns_FP(out, m, angleTwos, 0, 3, 6)
}
// Apply a rotation in place to the specified matrix.
function rotate_mat33_columns_FP(m: number[], angleTwos: Fx8, a: number, b: number) {
let s = Fx8_to_FP(Fx.sinFx8(angleTwos))
let c = Fx8_to_FP(Fx.cosFx8(angleTwos))
let ox = m[a]
let oy = m[a + 1]
let oz = m[a + 2]
m[a] = Math.imul(m[a], c) + Math.imul(m[b], s) >> FP_BITS
m[a + 1] = Math.imul(m[a + 1], c) + Math.imul(m[b + 1], s) >> FP_BITS
m[a + 2] = Math.imul(m[a + 2], c) + Math.imul(m[b + 2], s) >> FP_BITS
m[b] = Math.imul(m[b], c) - Math.imul(ox, s) >> FP_BITS
m[b + 1] = Math.imul(m[b + 1], c) - Math.imul(oy, s) >> FP_BITS
m[b + 2] = Math.imul(m[b + 2], c) - Math.imul(oz, s) >> FP_BITS
}
function rotateX_mat33_FP(m: number[], angleTwos: Fx8) {
rotate_mat33_columns_FP(m, angleTwos, 3, 6)
}
function rotateY_mat33_FP(m: number[], angleTwos: Fx8) {
rotate_mat33_columns_FP(m, angleTwos, 6, 0)
}
function rotateZ_mat33_FP(m: number[], angleTwos: Fx8) {
rotate_mat33_columns_FP(m, angleTwos, 0, 3)
}
function applyScale_FP(m: number[], sx: number, sy: number, sz: number) {
m[0] = Math.imul(m[0], sx) >> FP_BITS
m[1] = Math.imul(m[1], sx) >> FP_BITS
m[2] = Math.imul(m[2], sx) >> FP_BITS
m[3] = Math.imul(m[3], sy) >> FP_BITS
m[4] = Math.imul(m[4], sy) >> FP_BITS
m[5] = Math.imul(m[5], sy) >> FP_BITS
m[6] = Math.imul(m[6], sz) >> FP_BITS
m[7] = Math.imul(m[7], sz) >> FP_BITS
m[8] = Math.imul(m[8], sz) >> FP_BITS
}
function scale_by_fraction(x: number, a: number, b: number) {
//return Math.idiv(Math.imul(x << 8, a | 0), b | 0) >> 8
return Fx.toIntFloor(Fx.idiv(Fx.imul(Fx8(x), a), b))
}
interface Drawable {
}
// Rendering is based on trapezoids that are screen-space slices of triangles or other
// polygons. Each is a quadrilateral with vertical left and right sides, while the top
// and bottom sides can be at any angle. Any triangle can be represented by at most
// two of these trapezoids, usually with two corners collapsed into a single location
// unless it's clipped at a screen edge.
//
// Examples trapezoids A, B, C:
//
// +-----____ ___+-_
// | | ____---- | -_
// | A | +___ B | -_
// | ___---+ ----____| C -_
// +--- ----____=
//
class Trapezoid implements Drawable {
buf: Buffer
/*
x0: number
y0a: number
y0b: number
x1: number
y1a: number
y1b: number
color: number
order: number
*/
static instances: Trapezoid[] = []
static instNum: number = 0
static addInstance(x0: number, y0a: number, y0b: number, x1: number, y1a: number, y1b: number, col: number, z: number = 0): Trapezoid {
if (Trapezoid.instNum < Trapezoid.instances.length) {
const ret = Trapezoid.instances[Trapezoid.instNum]
ret.set(x0, y0a, y0b, x1, y1a, y1b, col, z)
//console.log("reuse trapezoid " + Trapezoid.instNum)
++Trapezoid.instNum
return ret
} else {
const ret = new Trapezoid(x0, y0a, y0b, x1, y1a, y1b, col, z)
Trapezoid.instances.push(ret)
//console.log("ALLOCATED NEW trapezoid " + Trapezoid.instNum)
++Trapezoid.instNum
return ret
}
}
static resetInstances() {
Trapezoid.instNum = 0
}
static eraseInstances() {
Trapezoid.resetInstances()
Trapezoid.instances = []
}
constructor(x0: number, y0a: number, y0b: number, x1: number, y1a: number, y1b: number, col: number, z: number) {
this.buf = Buffer.create(8)
this.set(x0, y0a, y0b, x1, y1a, y1b, col, z)
}
/*
set(x0: number, y0a: number, y0b: number, x1: number, y1a: number, y1b: number, col: number, z: number) {
this.x0 = Math.floor(x0)
this.y0a = Math.floor(y0a)
this.y0b = Math.floor(y0b)
this.x1 = Math.floor(x1)
this.y1a = Math.floor(y1a)
this.y1b = Math.floor(y1b)
this.order = Math.floor(z)
this.color = Math.floor(col)
}
*/
set(x0: number, y0a: number, y0b: number, x1: number, y1a: number, y1b: number, col: number, z: number = 0) {
// For debugging, check that clipping resulted in valid coordinates. Don't leave
// this in the production version since it would slow things down unnecessarily.
/*
if (y0a < 0 || y0a > 119 || y0b < 0 || y0b > 119 || y1a < 0 || y1a > 119 || y1b < 0 || y1b > 119) {
console.log("clipping failed")
throw("clipping failed")
}
if (x0 != Math.floor(x0) || y0a != Math.floor(y0a) || y0b != Math.floor(y0b) || y1a != Math.floor(y1a) || y1b != Math.floor(y1b) || col != Math.floor(col) || z != Math.floor(z)) {
console.log("non-integer")
throw("non-integer")
}
if (x1 < x0) throw("bad x1=" + x1 + " x0=" + x0)
*/
this.buf.setUint8(0, x0)
this.buf.setUint8(1, y0a)
this.buf.setUint8(2, y0b)
this.buf.setUint8(3, x1)
this.buf.setUint8(4, y1a)
this.buf.setUint8(5, y1b)
this.buf.setUint8(6, z)
this.buf.setUint8(7, col)
}
get x0() { return this.buf.getUint8(0) }
get y0a() { return this.buf.getUint8(1) }
get y0b() { return this.buf.getUint8(2) }
get x1() { return this.buf.getUint8(3) }
get y1a() { return this.buf.getUint8(4) }
get y1b() { return this.buf.getUint8(5) }
get order() { return this.buf.getUint8(6) }
get color() { return this.buf.getUint8(7) }