1 | class Vec {
|
2 | constructor(...values) {
|
3 | this.values = values;
|
4 | }
|
5 | get dim() {
|
6 | return this.values.length;
|
7 | }
|
8 | get x() {
|
9 | return this.values[0];
|
10 | }
|
11 | get y() {
|
12 | return this.values[1];
|
13 | }
|
14 | get z() {
|
15 | return this.values[2];
|
16 | }
|
17 | get w() {
|
18 | return this.values[3];
|
19 | }
|
20 | get xy() {
|
21 | return new Vec(this.values[0], this.values[1]);
|
22 | }
|
23 | get xz() {
|
24 | return new Vec(this.values[0], this.values[2]);
|
25 | }
|
26 | get yz() {
|
27 | return new Vec(this.values[1], this.values[2]);
|
28 | }
|
29 | get xyz() {
|
30 | return new Vec(this.values[0], this.values[1], this.values[2]);
|
31 | }
|
32 | |
33 |
|
34 |
|
35 |
|
36 | static fromArray(arr) {
|
37 | return new Vec(...arr);
|
38 | }
|
39 | |
40 |
|
41 |
|
42 |
|
43 |
|
44 | static fromNumber(n, dim) {
|
45 | return new Vec(...Array(dim).fill(n));
|
46 | }
|
47 | |
48 |
|
49 |
|
50 | clone() {
|
51 | return new Vec(...this.values);
|
52 | }
|
53 | |
54 |
|
55 |
|
56 |
|
57 |
|
58 | add(otherVec) {
|
59 | return new Vec(...this.values.map((v, idx) => v + otherVec.values[idx]));
|
60 | }
|
61 | |
62 |
|
63 |
|
64 |
|
65 |
|
66 | sub(otherVec) {
|
67 | return new Vec(...this.values.map((v, idx) => v - otherVec.values[idx]));
|
68 | }
|
69 | |
70 |
|
71 |
|
72 |
|
73 |
|
74 | mul(value) {
|
75 | return new Vec(...this.values.map((v) => v * value));
|
76 | }
|
77 | |
78 |
|
79 |
|
80 |
|
81 |
|
82 | div(value) {
|
83 | return new Vec(...this.values.map((x) => x / value));
|
84 | }
|
85 | |
86 |
|
87 |
|
88 |
|
89 | dot(otherVec) {
|
90 | return this.values
|
91 | .map((x, idx) => x * otherVec.values[idx])
|
92 | .reduce((a, b) => a + b);
|
93 | }
|
94 | |
95 |
|
96 |
|
97 |
|
98 | equals(otherVec) {
|
99 | return this.values
|
100 | .map((v, idx) => v === otherVec.values[idx])
|
101 | .reduce((a, b) => a === b);
|
102 | }
|
103 | |
104 |
|
105 |
|
106 | get length() {
|
107 | return Math.sqrt(this.values.map((v) => v ** 2).reduce((a, b) => a + b));
|
108 | }
|
109 | |
110 |
|
111 |
|
112 | toArray() {
|
113 | return this.values.slice(0);
|
114 | }
|
115 | |
116 |
|
117 |
|
118 | toString() {
|
119 | return `(${this.values.join(', ')})`;
|
120 | }
|
121 | |
122 |
|
123 |
|
124 |
|
125 |
|
126 | cross(otherVec) {
|
127 | if (this.dim !== 3 || otherVec.dim !== 3) {
|
128 | throw Error('dimension not supported');
|
129 | }
|
130 | return new Vec(this.y * otherVec.z - this.z * otherVec.y, this.z * otherVec.x - this.x * otherVec.z, this.x * otherVec.y - this.y * otherVec.x);
|
131 | }
|
132 | |
133 |
|
134 |
|
135 |
|
136 | get normalized() {
|
137 | return this.div(this.length);
|
138 | }
|
139 | }
|
140 |
|
141 |
|
142 | class Mat {
|
143 | constructor(values, options) {
|
144 | this.values = values;
|
145 | if (options) {
|
146 | this.numRows = options.numRows;
|
147 | this.numCols = options.numCols;
|
148 | }
|
149 | else {
|
150 | const dimension = Math.sqrt(values.length);
|
151 | if (Number.isInteger(dimension)) {
|
152 | this.numCols = this.numRows = dimension;
|
153 | return;
|
154 | }
|
155 | throw Error('ArgumentError');
|
156 | }
|
157 | }
|
158 | static identity(dimension) {
|
159 | if (dimension <= 0 || !Number.isInteger(dimension)) {
|
160 | throw Error('ArgumentError');
|
161 | }
|
162 | return new Mat(Array(dimension ** 2)
|
163 | .fill(0)
|
164 | .map((_, i) => (i % dimension === ((i / dimension) | 0) ? 1 : 0)));
|
165 | }
|
166 | |
167 |
|
168 |
|
169 |
|
170 |
|
171 | static fromVector(value) {
|
172 | if (value instanceof Vec) {
|
173 | return new Mat(value.toArray(), { numRows: value.dim, numCols: 1 });
|
174 | }
|
175 | throw Error('unsupported type');
|
176 | }
|
177 | |
178 |
|
179 |
|
180 | static fromVectors(vectors) {
|
181 | if (!vectors || vectors.length === 0) {
|
182 | throw Error('Argument error.');
|
183 | }
|
184 | const dimensions = vectors.map((v) => v.dim);
|
185 | const dimensionsMatch = dimensions.every((x) => x === dimensions[0]);
|
186 | const dimension = dimensions[0];
|
187 | if (!dimensionsMatch) {
|
188 | throw Error('Dimensions mismatch.');
|
189 | }
|
190 | const matrix = Array(dimension * vectors.length);
|
191 | for (let i = 0; i < vectors.length; i++) {
|
192 | for (let j = 0; j < dimension; j++) {
|
193 | matrix[i + j * vectors.length] = vectors[i].values[j];
|
194 | }
|
195 | }
|
196 | return new Mat(matrix, { numRows: dimension, numCols: vectors.length });
|
197 | }
|
198 | |
199 |
|
200 |
|
201 | toArray() {
|
202 | return this.values;
|
203 | }
|
204 | valueAt(row, column) {
|
205 | return this.values[column * this.numRows + row];
|
206 | }
|
207 | colAt(column) {
|
208 | const { numRows } = this;
|
209 | return this.values.slice(column * numRows, column * numRows + numRows);
|
210 | }
|
211 | rowAt(row) {
|
212 | const { numRows, numCols } = this;
|
213 | return Array(numCols)
|
214 | .fill(0)
|
215 | .map((_, column) => this.values[column * numRows + row]);
|
216 | }
|
217 | |
218 |
|
219 |
|
220 | transpose() {
|
221 | const transposedValues = [];
|
222 | Array(this.numRows)
|
223 | .fill(0)
|
224 | .map((_, i) => {
|
225 | transposedValues.push(...this.rowAt(i));
|
226 | });
|
227 | return new Mat(transposedValues, {
|
228 | numRows: this.numCols,
|
229 | numCols: this.numRows,
|
230 | });
|
231 | }
|
232 | equals(otherMatrix) {
|
233 | if (this.values.length !== otherMatrix.values.length ||
|
234 | this.numCols !== otherMatrix.numCols ||
|
235 | this.numRows !== otherMatrix.numRows) {
|
236 | return false;
|
237 | }
|
238 | for (let i = 0; i < this.values.length; i++) {
|
239 | if (this.values[i] !== otherMatrix.values[i]) {
|
240 | return false;
|
241 | }
|
242 | }
|
243 | return true;
|
244 | }
|
245 | add(otherMatrix) {
|
246 | if (this.numCols === otherMatrix.numCols &&
|
247 | this.numRows === otherMatrix.numRows &&
|
248 | this.values.length === otherMatrix.values.length) {
|
249 | const newValues = this.values.map((value, i) => value + otherMatrix.values[i]);
|
250 | return new Mat(newValues, {
|
251 | numRows: this.numRows,
|
252 | numCols: this.numCols,
|
253 | });
|
254 | }
|
255 | throw Error('ArgumentError');
|
256 | }
|
257 | sub(otherMatrix) {
|
258 | if (this.numCols === otherMatrix.numCols &&
|
259 | this.numRows === otherMatrix.numRows &&
|
260 | this.values.length === otherMatrix.values.length) {
|
261 | const newValues = this.values.map((value, i) => value - otherMatrix.values[i]);
|
262 | return new Mat(newValues, {
|
263 | numRows: this.numRows,
|
264 | numCols: this.numCols,
|
265 | });
|
266 | }
|
267 | throw Error('ArgumentError');
|
268 | }
|
269 | mul(param) {
|
270 | if (typeof param === 'number') {
|
271 | const multipliedValues = this.values.map((value) => value * param);
|
272 | return new Mat(multipliedValues, {
|
273 | numRows: this.numRows,
|
274 | numCols: this.numCols,
|
275 | });
|
276 | }
|
277 | if (param instanceof Vec) {
|
278 | if (param.dim !== this.numCols) {
|
279 | throw Error('dimension mismatch');
|
280 | }
|
281 | const m = this.mul(Mat.fromVector(param));
|
282 | return new Vec(...m.values);
|
283 | }
|
284 | if (param instanceof Mat) {
|
285 | const mat = param;
|
286 | const { numRows } = this;
|
287 | const { numCols } = mat;
|
288 | const multipliedValues = Array(numRows * numCols)
|
289 | .fill(0)
|
290 | .map((_, idx) => {
|
291 | const y = idx % numRows;
|
292 | const x = (idx / numRows) | 0;
|
293 | const row = this.rowAt(y);
|
294 | const col = mat.colAt(x);
|
295 | return row.map((value, i) => value * col[i]).reduce((a, b) => a + b);
|
296 | });
|
297 | return new Mat(multipliedValues, { numRows, numCols });
|
298 | }
|
299 | throw Error('ArgumentError');
|
300 | }
|
301 | determinant() {
|
302 | const { numRows, numCols } = this;
|
303 | const v = this.values;
|
304 | if (numRows !== numCols) {
|
305 | throw Error('ArgumentError');
|
306 | }
|
307 | if (numRows === 2) {
|
308 | return v[0] * v[3] - v[1] * v[2];
|
309 | }
|
310 | if (numRows === 3) {
|
311 |
|
312 |
|
313 |
|
314 |
|
315 |
|
316 | return (v[0] * v[4] * v[8] +
|
317 | v[3] * v[7] * v[2] +
|
318 | v[6] * v[1] * v[5] -
|
319 | v[2] * v[4] * v[6] -
|
320 | v[5] * v[7] * v[0] -
|
321 | v[8] * v[1] * v[3]);
|
322 | }
|
323 | throw Error('NotImplementedYet');
|
324 | }
|
325 | toString() {
|
326 | const { numRows, numCols, values } = this;
|
327 | return `mat${numRows}x${numCols}(${values.join(', ')})`;
|
328 | }
|
329 | }
|
330 | const Mat2 = {
|
331 | |
332 |
|
333 |
|
334 |
|
335 | rotation(angle) {
|
336 | const S = Math.sin(angle);
|
337 | const C = Math.cos(angle);
|
338 |
|
339 | return new Mat([
|
340 | C, S,
|
341 | -S, C
|
342 | ]);
|
343 | },
|
344 | scaling(sx, sy) {
|
345 |
|
346 | return new Mat([
|
347 | sx, 0,
|
348 | 0, sy
|
349 | ]);
|
350 | },
|
351 | };
|
352 | const Mat3 = {
|
353 | |
354 |
|
355 |
|
356 |
|
357 |
|
358 |
|
359 | translation(x, y) {
|
360 |
|
361 | return new Mat([
|
362 | 1, 0, 0,
|
363 | 0, 1, 0,
|
364 | x, y, 1
|
365 | ]);
|
366 | },
|
367 | |
368 |
|
369 |
|
370 |
|
371 |
|
372 |
|
373 |
|
374 | scaling(sx, sy, sz) {
|
375 |
|
376 | return new Mat([
|
377 | sx, 0, 0,
|
378 | 0, sy, 0,
|
379 | 0, 0, sz
|
380 | ]);
|
381 | },
|
382 | rotX(angle) {
|
383 | const { sin, cos } = Math;
|
384 | const S = sin(angle);
|
385 | const C = cos(angle);
|
386 |
|
387 | return new Mat([
|
388 | 1, 0, 0,
|
389 | 0, C, S,
|
390 | 0, -S, C
|
391 | ]);
|
392 | },
|
393 | rotY(angle) {
|
394 | const { sin, cos } = Math;
|
395 | const S = sin(angle);
|
396 | const C = cos(angle);
|
397 |
|
398 | return new Mat([
|
399 | C, 0, -S,
|
400 | 0, 1, 0,
|
401 | S, 0, C
|
402 | ]);
|
403 | },
|
404 | rotZ(angle) {
|
405 | const { sin, cos } = Math;
|
406 | const S = sin(angle);
|
407 | const C = cos(angle);
|
408 |
|
409 | return new Mat([
|
410 | C, S, 0,
|
411 | -S, C, 0,
|
412 | 0, 0, 1
|
413 | ]);
|
414 | },
|
415 | };
|
416 | const Mat4 = {
|
417 | identity() {
|
418 |
|
419 | return new Mat([
|
420 | 1, 0, 0, 0,
|
421 | 0, 1, 0, 0,
|
422 | 0, 0, 1, 0,
|
423 | 0, 0, 0, 1
|
424 | ]);
|
425 | },
|
426 | translation(x, y, z) {
|
427 |
|
428 | return new Mat([
|
429 | 1, 0, 0, 0,
|
430 | 0, 1, 0, 0,
|
431 | 0, 0, 1, 0,
|
432 | x, y, z, 1
|
433 | ]);
|
434 | },
|
435 | scaling(sx, sy, sz) {
|
436 |
|
437 | return new Mat([
|
438 | sx, 0, 0, 0,
|
439 | 0, sy, 0, 0,
|
440 | 0, 0, sz, 0,
|
441 | 0, 0, 0, 1
|
442 | ]);
|
443 | },
|
444 | rotX(angle) {
|
445 | const { sin, cos } = Math;
|
446 | const S = sin(angle);
|
447 | const C = cos(angle);
|
448 |
|
449 | return new Mat([
|
450 | 1, 0, 0, 0,
|
451 | 0, C, S, 0,
|
452 | 0, -S, C, 0,
|
453 | 0, 0, 0, 1
|
454 | ]);
|
455 | },
|
456 | rotY(angle) {
|
457 | const { sin, cos } = Math;
|
458 | const S = sin(angle);
|
459 | const C = cos(angle);
|
460 |
|
461 | return new Mat([
|
462 | C, 0, -S, 0,
|
463 | 0, 1, 0, 0,
|
464 | S, 0, C, 0,
|
465 | 0, 0, 0, 1
|
466 | ]);
|
467 | },
|
468 | rotZ(angle) {
|
469 | const { sin, cos } = Math;
|
470 | const S = sin(angle);
|
471 | const C = cos(angle);
|
472 |
|
473 | return new Mat([
|
474 | C, S, 0, 0,
|
475 | -S, C, 0, 0,
|
476 | 0, 0, 1, 0,
|
477 | 0, 0, 0, 1
|
478 | ]);
|
479 | },
|
480 | };
|
481 |
|
482 |
|
483 |
|
484 |
|
485 |
|
486 |
|
487 |
|
488 |
|
489 |
|
490 | function lookAt(eye, center, up) {
|
491 | const forward = eye.sub(center).normalized;
|
492 | const side = forward.cross(up).normalized;
|
493 | const up2 = side.cross(forward);
|
494 |
|
495 | return new Mat([
|
496 | side.x, side.y, side.z, 0,
|
497 | up2.x, up2.y, up2.z, 0,
|
498 | -forward.x, -forward.y, -forward.z, 0,
|
499 | 0, 0, 0, 1
|
500 | ]);
|
501 | }
|
502 |
|
503 |
|
504 |
|
505 |
|
506 |
|
507 |
|
508 |
|
509 |
|
510 |
|
511 |
|
512 |
|
513 |
|
514 | function ortho(left, right, bottom, top, zNear, zFar) {
|
515 | const tx = -(right + left) / (right - left);
|
516 | const ty = -(top + bottom) / (top - bottom);
|
517 | const tz = -(zFar + zNear) / (zFar - zNear);
|
518 |
|
519 | return new Mat([
|
520 | 2 / (right - left), 0, 0,
|
521 | 0, 0, 2 / (top - bottom), 0,
|
522 | 0, 0, 0, -2 / (zFar - zNear),
|
523 | 0, tx, ty, tz, 1
|
524 | ]);
|
525 | }
|
526 |
|
527 |
|
528 |
|
529 |
|
530 |
|
531 |
|
532 |
|
533 |
|
534 |
|
535 |
|
536 |
|
537 |
|
538 | function frustum(left, right, bottom, top, zNear, zFar) {
|
539 | const t1 = 2 * zNear;
|
540 | const t2 = right - left;
|
541 | const t3 = top - bottom;
|
542 | const t4 = zFar - zNear;
|
543 |
|
544 | return new Mat([t1 / t2, 0, 0, 0,
|
545 | 0, t1 / t3, 0, 0,
|
546 | (right + left) / t2, (top + bottom) / t3, (-zFar - zNear) / t4, -1,
|
547 | 0, 0, (-t1 * zFar) / t4, 0]);
|
548 | }
|
549 |
|
550 |
|
551 |
|
552 |
|
553 |
|
554 |
|
555 |
|
556 |
|
557 | function perspective(fieldOfView, aspectRatio, zNear, zFar) {
|
558 | const y = zNear * Math.tan((fieldOfView * Math.PI) / 360);
|
559 | const x = y * aspectRatio;
|
560 | return frustum(-x, x, -y, y, zNear, zFar);
|
561 | }
|
562 |
|
563 |
|
564 | class Geometry {
|
565 | constructor(vertices, faces, normals, texCoords) {
|
566 | this.vertices = vertices;
|
567 | this.faces = faces;
|
568 | this.normals = normals;
|
569 | this.texCoords = texCoords;
|
570 | }
|
571 | |
572 |
|
573 |
|
574 | toTriangles() {
|
575 | const { faces, vertices } = this;
|
576 | return faces
|
577 | .map((face) => {
|
578 | if (face.length === 3) {
|
579 | return face.map((vertexIndex) => vertices[vertexIndex]);
|
580 | }
|
581 | if (face.length === 4) {
|
582 | const q = face.map((vertexIndex) => vertices[vertexIndex]);
|
583 | return [q[0], q[1], q[3], q[3], q[1], q[2]];
|
584 | }
|
585 | throw Error('not supported');
|
586 | })
|
587 | .flat()
|
588 | .map((v) => v.toArray())
|
589 | .flat();
|
590 | }
|
591 | |
592 |
|
593 |
|
594 |
|
595 |
|
596 |
|
597 | static calculateSurfaceNormal(p1, p2, p3) {
|
598 | const u = p2.sub(p1);
|
599 | const v = p3.sub(p1);
|
600 | return new Vec(u.x * v.z - u.z * v.y, u.z * v.x - u.x * v.z, u.x * v.y - u.y * v.x);
|
601 | }
|
602 | |
603 |
|
604 |
|
605 |
|
606 |
|
607 |
|
608 |
|
609 |
|
610 |
|
611 | static box(sizeA = 1.0, sizeB = 1.0, sizeC = 1.0) {
|
612 | const a = sizeA * 0.5;
|
613 | const b = sizeB * 0.5;
|
614 | const c = sizeC * 0.5;
|
615 | const vertices = [
|
616 | [-a, -b, -c],
|
617 | [a, -b, -c],
|
618 | [-a, b, -c],
|
619 | [a, b, -c],
|
620 | [-a, -b, c],
|
621 | [a, -b, c],
|
622 | [-a, b, c],
|
623 | [a, b, c],
|
624 | ].map((v) => new Vec(...v));
|
625 |
|
626 |
|
627 |
|
628 |
|
629 |
|
630 | const faces = [
|
631 |
|
632 | [0, 1, 2],
|
633 | [2, 1, 3],
|
634 |
|
635 | [5, 4, 7],
|
636 | [7, 4, 6],
|
637 |
|
638 | [4, 0, 6],
|
639 | [6, 0, 2],
|
640 |
|
641 | [7, 5, 1],
|
642 | [1, 7, 3],
|
643 |
|
644 | [1, 0, 5],
|
645 | [5, 0, 4],
|
646 |
|
647 | [2, 3, 6],
|
648 | [6, 3, 7],
|
649 | ];
|
650 | const normals = faces.map((f) => Geometry.calculateSurfaceNormal(vertices[f[0]], vertices[f[1]], vertices[f[2]]));
|
651 | return new Geometry(vertices, faces, normals, []);
|
652 | }
|
653 | |
654 |
|
655 |
|
656 |
|
657 | static cube(size = 1.0) {
|
658 | return Geometry.box(size, size, size);
|
659 | }
|
660 | |
661 |
|
662 |
|
663 |
|
664 |
|
665 |
|
666 |
|
667 |
|
668 |
|
669 | static grid(x, y, width, height, rows, cols) {
|
670 | const deltaX = width / cols;
|
671 | const deltaY = height / rows;
|
672 | const vertices = Array((cols + 1) * (rows + 1))
|
673 | .fill(0)
|
674 | .map((_, i) => {
|
675 | const ix = i % cols;
|
676 | const iy = (i / cols) | 0;
|
677 | return new Vec(x + ix * deltaX, y + iy * deltaY, 0);
|
678 | });
|
679 | const faces = Array(rows * cols)
|
680 | .fill(0)
|
681 | .map((_, i) => {
|
682 | const ix = i % cols;
|
683 | const iy = (i / cols) | 0;
|
684 | const idx = iy * rows + ix;
|
685 | return [
|
686 | [idx, idx + 1, idx + rows],
|
687 | [idx + 1, idx + rows + 1, idx + rows],
|
688 | ];
|
689 | })
|
690 | .flat(1);
|
691 | const normals = faces.map((f) => Geometry.calculateSurfaceNormal(vertices[f[0]], vertices[f[1]], vertices[f[2]]));
|
692 | return new Geometry(vertices, faces, normals, []);
|
693 | }
|
694 | |
695 |
|
696 |
|
697 |
|
698 |
|
699 |
|
700 |
|
701 | static sphere(r = 0.5, sides = 36, segments = 18) {
|
702 | const vertices = [];
|
703 | const texCoords = [];
|
704 | const faces = [];
|
705 | const dphi = 360 / sides;
|
706 | const dtheta = 180 / segments;
|
707 | const evalPos = (theta, phi) => {
|
708 | const deg = Math.PI / 180.0;
|
709 | var pos = new Vec(r * Math.sin(theta * deg) * Math.sin(phi * deg), r * Math.cos(theta * deg), r * Math.sin(theta * deg) * Math.cos(phi * deg));
|
710 | return pos;
|
711 | };
|
712 | for (let segment = 0; segment <= segments; segment++) {
|
713 | const theta = segment * dtheta;
|
714 | for (let side = 0; side <= sides; side++) {
|
715 | const phi = side * dphi;
|
716 | const pos = evalPos(theta, phi);
|
717 | const texCoord = new Vec(phi / 360.0, theta / 180.0);
|
718 | vertices.push(pos);
|
719 | texCoords.push(texCoord);
|
720 | if (segment === segments)
|
721 | continue;
|
722 | if (side === sides)
|
723 | continue;
|
724 | if (segment == 0) {
|
725 |
|
726 | faces.push([
|
727 | segment * (sides + 1) + side,
|
728 | (segment + 1) * (sides + 1) + side,
|
729 | (segment + 1) * (sides + 1) + side + 1,
|
730 | ]);
|
731 | }
|
732 | else if (segment == segments - 1) {
|
733 |
|
734 | faces.push([
|
735 | segment * (sides + 1) + side,
|
736 | (segment + 1) * (sides + 1) + side + 1,
|
737 | segment * (sides + 1) + side + 1,
|
738 | ]);
|
739 | }
|
740 | else {
|
741 |
|
742 |
|
743 | const A = segment * (sides + 1) + side;
|
744 | const B = (segment + 1) * (sides + 1) + side;
|
745 | const C = (segment + 1) * (sides + 1) + side + 1;
|
746 | const D = segment * (sides + 1) + side + 1;
|
747 | faces.push([A, B, D]);
|
748 | faces.push([B, C, D]);
|
749 | }
|
750 | }
|
751 | }
|
752 | const normals = faces.map((f) => Geometry.calculateSurfaceNormal(vertices[f[0]], vertices[f[1]], vertices[f[2]]));
|
753 | return new Geometry(vertices, faces, normals, texCoords);
|
754 | }
|
755 | }
|
756 |
|
757 | export { Geometry, Mat, Mat2, Mat3, Mat4, Vec, frustum, lookAt, ortho, perspective };
|