matrix-engine-wgpu 1.0.2 → 1.0.5

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.
Files changed (51) hide show
  1. package/REFERENCE.md +3 -0
  2. package/app-worker.js +45 -0
  3. package/empty.js +12 -0
  4. package/examples/load-obj-file.js +48 -0
  5. package/examples/unlit-textures.js +27 -0
  6. package/examples.js +7 -0
  7. package/index.js +18 -5
  8. package/main.js +49 -23
  9. package/package.json +11 -2
  10. package/public/ammojs/ammo.js +957 -0
  11. package/public/ammojs/ammo.wasm.js +921 -0
  12. package/public/ammojs/ammo.wasm.wasm +0 -0
  13. package/public/app-worker.js +47 -0
  14. package/public/app.js +2640 -305
  15. package/public/css/style.css +1 -2
  16. package/public/empty.html +25 -0
  17. package/public/empty.js +9107 -0
  18. package/public/examples.html +25 -0
  19. package/public/examples.js +9180 -0
  20. package/public/index.html +1 -1
  21. package/public/res/meshes/blender/lopta.mtl +10 -0
  22. package/public/res/meshes/blender/lopta.obj +3402 -0
  23. package/public/res/meshes/blender/piramyd.blend +0 -0
  24. package/public/res/meshes/blender/piramyd.blend1 +0 -0
  25. package/public/res/meshes/blender/piramyd.js +42 -0
  26. package/public/res/meshes/blender/piramyd.mtl +10 -0
  27. package/public/res/meshes/blender/piramyd.obj +18696 -0
  28. package/public/res/meshes/blender/piramyd1.js +42 -0
  29. package/public/res/meshes/blender/welcomeTextblend.blend +0 -0
  30. package/public/res/meshes/dragon/stanfordDragonData.js +5 -0
  31. package/public/res/meshes/obj/armor.obj +319 -0
  32. package/public/res/meshes/obj/armor.png +0 -0
  33. package/public/worker.html +25 -0
  34. package/readme.md +130 -48
  35. package/src/engine/ball.js +26 -10
  36. package/src/engine/cube.js +100 -81
  37. package/src/engine/engine.js +466 -4
  38. package/src/engine/final/adaptJSON1.js +53 -0
  39. package/src/engine/final/utils2.js +63 -0
  40. package/src/engine/loader-obj.js +469 -0
  41. package/src/engine/matrix-class.js +5 -4
  42. package/src/engine/matrix-mesh.js +49 -0
  43. package/src/engine/mesh-obj.js +534 -0
  44. package/src/engine/utils.js +300 -45
  45. package/src/physics/matrix-ammo.js +119 -0
  46. package/src/shaders/fragment.wgsl.js +48 -0
  47. package/src/shaders/shaders.js +4 -124
  48. package/src/shaders/vertex.wgsl.js +49 -0
  49. package/src/shaders/vertexShadow.wgsl.js +20 -0
  50. package/src/world.js +246 -0
  51. package/src/meWGPU.js +0 -173
@@ -1,8 +1,470 @@
1
+ // Note: The code in this file does not use the 'dst' output parameter of functions in the
2
+ // 'wgpu-matrix' library, so produces many temporary vectors and matrices.
3
+ // This is intentional, as this sample prefers readability over performance.
4
+ import { Mat4, Vec3, Vec4, mat4, vec3 } from 'wgpu-matrix';
5
+ // import Input from './input';
1
6
 
7
+ // // Common interface for camera implementations
8
+ // export default interface Camera {
9
+ // // update updates the camera using the user-input and returns the view matrix.
10
+ // update(delta_time: number, input: Input): Mat4;
2
11
 
3
- export class Engine {
12
+ // // The camera matrix.
13
+ // // This is the inverse of the view matrix.
14
+ // matrix: Mat4;
15
+ // // Alias to column vector 0 of the camera matrix.
16
+ // right: Vec4;
17
+ // // Alias to column vector 1 of the camera matrix.
18
+ // up: Vec4;
19
+ // // Alias to column vector 2 of the camera matrix.
20
+ // back: Vec4;
21
+ // // Alias to column vector 3 of the camera matrix.
22
+ // position: Vec4;
23
+ // }
4
24
 
5
- constructor() {
6
-
25
+ // The common functionality between camera implementations
26
+ class CameraBase {
27
+ // The camera matrix
28
+ matrix_ = new Float32Array([
29
+ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1,
30
+ ]);
31
+
32
+ // The calculated view matrix readonly
33
+ view_ = mat4.create();
34
+
35
+ // Aliases to column vectors of the matrix
36
+ right_ = new Float32Array(this.matrix_.buffer, 4 * 0, 4);
37
+ up_ = new Float32Array(this.matrix_.buffer, 4 * 4, 4);
38
+ back_ = new Float32Array(this.matrix_.buffer, 4 * 8, 4);
39
+ position_ = new Float32Array(this.matrix_.buffer, 4 * 12, 4);
40
+
41
+ // Returns the camera matrix
42
+ get matrix() {
43
+ return this.matrix_;
44
+ }
45
+ // Assigns `mat` to the camera matrix
46
+ set matrix(mat) {
47
+ mat4.copy(mat, this.matrix_);
48
+ }
49
+
50
+ // Returns the camera view matrix
51
+ get view() {
52
+ return this.view_;
53
+ }
54
+ // Assigns `mat` to the camera view
55
+ set view(mat) {
56
+ mat4.copy(mat, this.view_);
57
+ }
58
+
59
+ // Returns column vector 0 of the camera matrix
60
+ get right() {
61
+ return this.right_;
62
+ }
63
+ // Assigns `vec` to the first 3 elements of column vector 0 of the camera matrix
64
+ set right(vec) {
65
+ vec3.copy(vec, this.right_);
66
+ }
67
+
68
+ // Returns column vector 1 of the camera matrix
69
+ get up() {
70
+ return this.up_;
71
+ }
72
+ // Assigns `vec` to the first 3 elements of column vector 1 of the camera matrix \ Vec3
73
+ set up(vec) {
74
+ vec3.copy(vec, this.up_);
75
+ }
76
+
77
+ // Returns column vector 2 of the camera matrix
78
+ get back() {
79
+ return this.back_;
80
+ }
81
+ // Assigns `vec` to the first 3 elements of column vector 2 of the camera matrix
82
+ set back(vec) {
83
+ vec3.copy(vec, this.back_);
84
+ }
85
+
86
+ // Returns column vector 3 of the camera matrix
87
+ get position() {
88
+ return this.position_;
89
+ }
90
+ // Assigns `vec` to the first 3 elements of column vector 3 of the camera matrix
91
+ set position(vec) {
92
+ vec3.copy(vec, this.position_);
93
+ }
94
+ }
95
+
96
+ // WASDCamera is a camera implementation that behaves similar to first-person-shooter PC games.
97
+ export class WASDCamera extends CameraBase {
98
+ // The camera absolute pitch angle
99
+ pitch = 0;
100
+ // The camera absolute yaw angle
101
+ yaw = 0;
102
+
103
+ // The movement veloicty readonly
104
+ velocity_ = vec3.create();
105
+
106
+ // Speed multiplier for camera movement
107
+ movementSpeed = 10;
108
+
109
+ // Speed multiplier for camera rotation
110
+ rotationSpeed = 1;
111
+
112
+ // Movement velocity drag coeffient [0 .. 1]
113
+ // 0: Continues forever
114
+ // 1: Instantly stops moving
115
+ frictionCoefficient = 0.99;
116
+
117
+ // Returns velocity vector
118
+ get velocity() {
119
+ return this.velocity_;
120
+ }
121
+ // Assigns `vec` to the velocity vector
122
+ set velocity(vec) {
123
+ vec3.copy(vec, this.velocity_);
124
+ }
125
+
126
+ // Construtor
127
+ constructor(options) {
128
+ super();
129
+ if (options && (options.position || options.target)) {
130
+ const position = options.position ?? vec3.create(0, 0, 0);
131
+ const target = options.target ?? vec3.create(0, 0, 0);
132
+
133
+ const forward = vec3.normalize(vec3.sub(target, position));
134
+ this.recalculateAngles(forward);
135
+ this.position = position;
136
+ console.log('camera postion:', position);
137
+ }
138
+ }
139
+
140
+ // Returns the camera matrix
141
+ get matrix() {
142
+ return super.matrix;
143
+ }
144
+
145
+ // Assigns `mat` to the camera matrix, and recalcuates the camera angles
146
+ set matrix(mat) {
147
+ super.matrix = mat;
148
+ this.recalculateAngles(this.back);
149
+ }
150
+
151
+ update(deltaTime, input) {
152
+ const sign = (positive, negative) =>
153
+ (positive ? 1 : 0) - (negative ? 1 : 0);
154
+
155
+ // Apply the delta rotation to the pitch and yaw angles
156
+ this.yaw -= input.analog.x * deltaTime * this.rotationSpeed;
157
+ this.pitch -= input.analog.y * deltaTime * this.rotationSpeed;
158
+
159
+ // Wrap yaw between [0° .. 360°], just to prevent large accumulation.
160
+ this.yaw = mod(this.yaw, Math.PI * 2);
161
+ // Clamp pitch between [-90° .. +90°] to prevent somersaults.
162
+ this.pitch = clamp(this.pitch, -Math.PI / 2, Math.PI / 2);
163
+
164
+ // Save the current position, as we're about to rebuild the camera matrix.
165
+ const position = vec3.copy(this.position);
166
+
167
+ // Reconstruct the camera's rotation, and store into the camera matrix.
168
+ super.matrix = mat4.rotateX(mat4.rotationY(this.yaw), this.pitch);
169
+
170
+ // Calculate the new target velocity
171
+ const digital = input.digital;
172
+ const deltaRight = sign(digital.right, digital.left);
173
+ const deltaUp = sign(digital.up, digital.down);
174
+ const targetVelocity = vec3.create();
175
+ const deltaBack = sign(digital.backward, digital.forward);
176
+ vec3.addScaled(targetVelocity, this.right, deltaRight, targetVelocity);
177
+ vec3.addScaled(targetVelocity, this.up, deltaUp, targetVelocity);
178
+ vec3.addScaled(targetVelocity, this.back, deltaBack, targetVelocity);
179
+ vec3.normalize(targetVelocity, targetVelocity);
180
+ vec3.mulScalar(targetVelocity, this.movementSpeed, targetVelocity);
181
+
182
+ // Mix new target velocity
183
+ this.velocity = lerp(
184
+ targetVelocity,
185
+ this.velocity,
186
+ Math.pow(1 - this.frictionCoefficient, deltaTime)
187
+ );
188
+
189
+ // Integrate velocity to calculate new position
190
+ this.position = vec3.addScaled(position, this.velocity, deltaTime);
191
+
192
+ // Invert the camera matrix to build the view matrix
193
+ this.view = mat4.invert(this.matrix);
194
+ return this.view;
195
+ }
196
+
197
+ // Recalculates the yaw and pitch values from a directional vector
198
+ recalculateAngles(dir) {
199
+ this.yaw = Math.atan2(dir[0], dir[2]);
200
+ this.pitch = -Math.asin(dir[1]);
201
+ }
202
+ }
203
+
204
+ // ArcballCamera implements a basic orbiting camera around the world origin
205
+ export class ArcballCamera extends CameraBase {
206
+ // The camera distance from the target
207
+ distance = 0;
208
+
209
+ // The current angular velocity
210
+ angularVelocity = 0;
211
+
212
+ // The current rotation axis
213
+ axis_ = vec3.create();
214
+
215
+ // Returns the rotation axis
216
+ get axis() {
217
+ return this.axis_;
218
+ }
219
+ // Assigns `vec` to the rotation axis
220
+ set axis(vec) {
221
+ vec3.copy(vec, this.axis_);
222
+ }
223
+
224
+ // Speed multiplier for camera rotation
225
+ rotationSpeed = 1;
226
+
227
+ // Speed multiplier for camera zoom
228
+ zoomSpeed = 0.1;
229
+
230
+ // Rotation velocity drag coeffient [0 .. 1]
231
+ // 0: Spins forever
232
+ // 1: Instantly stops spinning
233
+ frictionCoefficient = 0.999;
234
+
235
+ // Construtor
236
+ constructor(options) {
237
+ super();
238
+ if (options && options.position) {
239
+ this.position = options.position;
240
+ this.distance = vec3.len(this.position);
241
+ this.back = vec3.normalize(this.position);
242
+ this.recalcuateRight();
243
+ this.recalcuateUp();
244
+ }
245
+ }
246
+
247
+ // Returns the camera matrix
248
+ get matrix() {
249
+ return super.matrix;
250
+ }
251
+
252
+ // Assigns `mat` to the camera matrix, and recalcuates the distance
253
+ set matrix(mat) {
254
+ super.matrix = mat;
255
+ this.distance = vec3.len(this.position);
256
+ }
257
+
258
+ update(deltaTime, input) {
259
+ const epsilon = 0.0000001;
260
+
261
+ if (input.analog.touching) {
262
+ // Currently being dragged.
263
+ this.angularVelocity = 0;
264
+ } else {
265
+ // Dampen any existing angular velocity
266
+ this.angularVelocity *= Math.pow(1 - this.frictionCoefficient, deltaTime);
267
+ }
268
+
269
+ // Calculate the movement vector
270
+ const movement = vec3.create();
271
+ vec3.addScaled(movement, this.right, input.analog.x, movement);
272
+ vec3.addScaled(movement, this.up, -input.analog.y, movement);
273
+
274
+ // Cross the movement vector with the view direction to calculate the rotation axis x magnitude
275
+ const crossProduct = vec3.cross(movement, this.back);
276
+
277
+ // Calculate the magnitude of the drag
278
+ const magnitude = vec3.len(crossProduct);
279
+
280
+ if (magnitude > epsilon) {
281
+ // Normalize the crossProduct to get the rotation axis
282
+ this.axis = vec3.scale(crossProduct, 1 / magnitude);
283
+
284
+ // Remember the current angular velocity. This is used when the touch is released for a fling.
285
+ this.angularVelocity = magnitude * this.rotationSpeed;
286
+ }
287
+
288
+ // The rotation around this.axis to apply to the camera matrix this update
289
+ const rotationAngle = this.angularVelocity * deltaTime;
290
+ if (rotationAngle > epsilon) {
291
+ // Rotate the matrix around axis
292
+ // Note: The rotation is not done as a matrix-matrix multiply as the repeated multiplications
293
+ // will quickly introduce substantial error into the matrix.
294
+ this.back = vec3.normalize(rotate(this.back, this.axis, rotationAngle));
295
+ this.recalcuateRight();
296
+ this.recalcuateUp();
297
+ }
298
+
299
+ // recalculate `this.position` from `this.back` considering zoom
300
+ if (input.analog.zoom !== 0) {
301
+ this.distance *= 1 + input.analog.zoom * this.zoomSpeed;
302
+ }
303
+ this.position = vec3.scale(this.back, this.distance);
304
+
305
+ // Invert the camera matrix to build the view matrix
306
+ this.view = mat4.invert(this.matrix);
307
+ return this.view;
7
308
  }
8
- }
309
+
310
+ // Assigns `this.right` with the cross product of `this.up` and `this.back`
311
+ recalcuateRight() {
312
+ this.right = vec3.normalize(vec3.cross(this.up, this.back));
313
+ }
314
+
315
+ // Assigns `this.up` with the cross product of `this.back` and `this.right`
316
+ recalcuateUp() {
317
+ this.up = vec3.normalize(vec3.cross(this.back, this.right));
318
+ }
319
+ }
320
+
321
+ // Returns `x` clamped between [`min` .. `max`]
322
+ function clamp(x, min, max) {
323
+ return Math.min(Math.max(x, min), max);
324
+ }
325
+
326
+ // Returns `x` float-modulo `div`
327
+ function mod(x, div) {
328
+ return x - Math.floor(Math.abs(x) / div) * div * Math.sign(x);
329
+ }
330
+
331
+ // Returns `vec` rotated `angle` radians around `axis`
332
+ function rotate(vec, axis, angle) {
333
+ return vec3.transformMat4Upper3x3(vec, mat4.rotation(axis, angle));
334
+ }
335
+
336
+ // Returns the linear interpolation between 'a' and 'b' using 's'
337
+ function lerp(a, b, s) {
338
+ return vec3.addScaled(a, vec3.sub(b, a), s);
339
+ }
340
+
341
+ // IMPUT
342
+
343
+ // // Input holds as snapshot of input state
344
+ // export default interface Input {
345
+ // // Digital input (e.g keyboard state)
346
+ // readonly digital: {
347
+ // readonly forward: boolean;
348
+ // readonly backward: boolean;
349
+ // readonly left: boolean;
350
+ // readonly right: boolean;
351
+ // readonly up: boolean;
352
+ // readonly down: boolean;
353
+ // };
354
+ // // Analog input (e.g mouse, touchscreen)
355
+ // readonly analog: {
356
+ // readonly x: number;
357
+ // readonly y: number;
358
+ // readonly zoom: number;
359
+ // readonly touching: boolean;
360
+ // };
361
+ // }
362
+
363
+ // InputHandler is a function that when called, returns the current Input state.
364
+ // export type InputHandler = () => Input;
365
+
366
+ // createInputHandler returns an InputHandler by attaching event handlers to the window and canvas.
367
+ export function createInputHandler(window, canvas) {
368
+ let digital = {
369
+ forward: false,
370
+ backward: false,
371
+ left: false,
372
+ right: false,
373
+ up: false,
374
+ down: false,
375
+ };
376
+ let analog = {
377
+ x: 0,
378
+ y: 0,
379
+ zoom: 0,
380
+ };
381
+ let mouseDown = false;
382
+
383
+ const setDigital = (e, value) => {
384
+ switch (e.code) {
385
+ case 'KeyW':
386
+ digital.forward = value;
387
+ e.preventDefault();
388
+ e.stopPropagation();
389
+ break;
390
+ case 'KeyS':
391
+ digital.backward = value;
392
+ e.preventDefault();
393
+ e.stopPropagation();
394
+ break;
395
+ case 'KeyA':
396
+ digital.left = value;
397
+ e.preventDefault();
398
+ e.stopPropagation();
399
+ break;
400
+ case 'KeyD':
401
+ digital.right = value;
402
+ e.preventDefault();
403
+ e.stopPropagation();
404
+ break;
405
+ case 'Space':
406
+ digital.up = value;
407
+ e.preventDefault();
408
+ e.stopPropagation();
409
+ break;
410
+ case 'ShiftLeft':
411
+ case 'ControlLeft':
412
+ case 'KeyC':
413
+ digital.down = value;
414
+ e.preventDefault();
415
+ e.stopPropagation();
416
+ break;
417
+ }
418
+ };
419
+
420
+ window.addEventListener('keydown', (e) => setDigital(e, true));
421
+ window.addEventListener('keyup', (e) => setDigital(e, false));
422
+
423
+ canvas.style.touchAction = 'pinch-zoom';
424
+ canvas.addEventListener('pointerdown', () => {
425
+ mouseDown = true;
426
+ });
427
+ canvas.addEventListener('pointerup', () => {
428
+ mouseDown = false;
429
+ });
430
+ canvas.addEventListener('pointermove', (e) => {
431
+ mouseDown = e.pointerType == 'mouse' ? (e.buttons & 1) !== 0 : true;
432
+ if (mouseDown) {
433
+ // console.log('TEST ', analog)
434
+ analog.x += e.movementX / 10;
435
+ analog.y += e.movementY / 10;
436
+ }
437
+ });
438
+ canvas.addEventListener(
439
+ 'wheel',
440
+ (e) => {
441
+ mouseDown = (e.buttons & 1) !== 0;
442
+ if (mouseDown) {
443
+ // The scroll value varies substantially between user agents / browsers.
444
+ // Just use the sign.
445
+ analog.zoom += Math.sign(e.deltaY);
446
+ e.preventDefault();
447
+ e.stopPropagation();
448
+ }
449
+ },
450
+ { passive: false }
451
+ );
452
+
453
+ return () => {
454
+ const out = {
455
+ digital,
456
+ analog: {
457
+ x: analog.x,
458
+ y: analog.y,
459
+ zoom: analog.zoom,
460
+ touching: mouseDown,
461
+ },
462
+ };
463
+ // Clear the analog values, as these accumulate.
464
+ analog.x = 0;
465
+ analog.y = 0;
466
+ analog.zoom = 0;
467
+ return out;
468
+ };
469
+ }
470
+
@@ -0,0 +1,53 @@
1
+ import {computeSurfaceNormals, computeProjectedPlaneUVs} from './utils2.js';
2
+
3
+ export function adaptJSON1(dragonRawData) {
4
+
5
+ let mesh = {
6
+ positions: dragonRawData.positions,
7
+ triangles: dragonRawData.cells,
8
+ normals: [],
9
+ uvs: []
10
+ };
11
+
12
+ // Compute surface normals
13
+ mesh.normals = computeSurfaceNormals(mesh.positions, mesh.triangles);
14
+
15
+ // Compute some easy uvs for testing
16
+ mesh.uvs = computeProjectedPlaneUVs(mesh.positions, 'xy');
17
+
18
+ return mesh;
19
+ }
20
+
21
+ export function addVerticesNormalUvs(mesh) {
22
+
23
+ var meshAdapted = {
24
+ positions: [],
25
+ cells: [],
26
+ uvs: mesh.textures,
27
+ vertices: mesh.vertices
28
+ // normals: mesh.vertexNormals
29
+ };
30
+ // force syntese
31
+ for (var x = 0; x < mesh.vertices.length; x=x+3) {
32
+ var sub = [];
33
+ sub.push(mesh.vertices[x])
34
+ sub.push(mesh.vertices[x+1])
35
+ sub.push(mesh.vertices[x+2])
36
+ meshAdapted.positions.push(sub)
37
+ sub = [];
38
+ sub.push(mesh.indices[x])
39
+ sub.push(mesh.indices[x+1])
40
+ sub.push(mesh.indices[x+2])
41
+ meshAdapted.cells.push(sub)
42
+ }
43
+
44
+ // Compute surface normals
45
+ meshAdapted.normals = computeSurfaceNormals(meshAdapted.positions, meshAdapted.cells);
46
+ // Compute some easy uvs for testing
47
+ meshAdapted.uvs = computeProjectedPlaneUVs(meshAdapted.positions, 'xy');
48
+
49
+ meshAdapted.triangles = meshAdapted.cells
50
+ return meshAdapted;
51
+ }
52
+
53
+
@@ -0,0 +1,63 @@
1
+ import { vec3 } from 'wgpu-matrix';
2
+
3
+ export function computeSurfaceNormals(positions,triangles){
4
+ const normals = positions.map(() => {
5
+ // Initialize to zero.
6
+ return [0, 0, 0];
7
+ });
8
+ triangles.forEach(([i0, i1, i2]) => {
9
+ const p0 = positions[i0];
10
+ const p1 = positions[i1];
11
+ const p2 = positions[i2];
12
+
13
+ const v0 = vec3.subtract(p1, p0);
14
+ const v1 = vec3.subtract(p2, p0);
15
+
16
+ vec3.normalize(v0, v0);
17
+ vec3.normalize(v1, v1);
18
+ const norm = vec3.cross(v0, v1);
19
+
20
+ // Accumulate the normals.
21
+ vec3.add(normals[i0], norm, normals[i0]);
22
+ vec3.add(normals[i1], norm, normals[i1]);
23
+ vec3.add(normals[i2], norm, normals[i2]);
24
+ });
25
+ normals.forEach((n) => {
26
+ // Normalize accumulated normals.
27
+ vec3.normalize(n, n);
28
+ });
29
+
30
+ return normals;
31
+ }
32
+
33
+ // type ProjectedPlane = 'xy' | 'xz' | 'yz';
34
+ const projectedPlane2Ids = {
35
+ xy: [0, 1],
36
+ xz: [0, 2],
37
+ yz: [1, 2],
38
+ };
39
+
40
+ export function computeProjectedPlaneUVs(positions, projectedPlane) {
41
+ const idxs = projectedPlane2Ids[projectedPlane];
42
+ const uvs = positions.map(() => {
43
+ // Initialize to zero.
44
+ return [0, 0];
45
+ });
46
+ const extentMin = [Infinity, Infinity];
47
+ const extentMax = [-Infinity, -Infinity];
48
+ positions.forEach((pos, i) => {
49
+ // Simply project to the selected plane
50
+ uvs[i][0] = pos[idxs[0]];
51
+ uvs[i][1] = pos[idxs[1]];
52
+
53
+ extentMin[0] = Math.min(pos[idxs[0]], extentMin[0]);
54
+ extentMin[1] = Math.min(pos[idxs[1]], extentMin[1]);
55
+ extentMax[0] = Math.max(pos[idxs[0]], extentMax[0]);
56
+ extentMax[1] = Math.max(pos[idxs[1]], extentMax[1]);
57
+ });
58
+ uvs.forEach((uv) => {
59
+ uv[0] = (uv[0] - extentMin[0]) / (extentMax[0] - extentMin[0]);
60
+ uv[1] = (uv[1] - extentMin[1]) / (extentMax[1] - extentMin[1]);
61
+ });
62
+ return uvs;
63
+ }