3d-spinner 0.9.4 → 0.9.6

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 (54) hide show
  1. package/dist/animations/charged-orb.js +4 -2
  2. package/dist/animations/ghost-train.d.ts +1 -1
  3. package/dist/animations/ghost-train.js +3 -1
  4. package/dist/animations/grid-assembly.d.ts +1 -1
  5. package/dist/animations/spin.d.ts +3 -1
  6. package/dist/animations/spin.js +6 -1
  7. package/dist/cjs/animations/charged-orb.cjs +177 -30
  8. package/dist/cjs/animations/grid-assembly.cjs +171 -28
  9. package/dist/cjs/animations/object-motion.cjs +163 -26
  10. package/dist/cjs/animations/particles.cjs +181 -30
  11. package/dist/cjs/animations/spin.cjs +176 -29
  12. package/dist/cjs/engines/little-3d-engine/little-3d-engine.cjs +200 -47
  13. package/dist/cjs/engines/little-3d-engine/loaders/obj.cjs +51 -13
  14. package/dist/cjs/engines/little-3d-engine/renderers/canvas2d-textured.cjs +56 -6
  15. package/dist/cjs/engines/little-3d-engine/renderers/webgl-textured.cjs +69 -10
  16. package/dist/cjs/engines/little-3d-engine/renderers/webgpu-textured.cjs +64 -11
  17. package/dist/cjs/prefabs/prefabs.cjs +200 -44
  18. package/dist/engines/little-3d-engine/core/geometry.d.ts +15 -2
  19. package/dist/engines/little-3d-engine/core/geometry.js +25 -3
  20. package/dist/engines/little-3d-engine/core/light.d.ts +28 -4
  21. package/dist/engines/little-3d-engine/core/light.js +48 -7
  22. package/dist/engines/little-3d-engine/core/mesh.d.ts +33 -0
  23. package/dist/engines/little-3d-engine/core/mesh.js +12 -0
  24. package/dist/engines/little-3d-engine/little-3d-engine.d.ts +2 -2
  25. package/dist/engines/little-3d-engine/little-3d-engine.js +1 -1
  26. package/dist/engines/little-3d-engine/loaders/obj.d.ts +8 -7
  27. package/dist/engines/little-3d-engine/loaders/obj.js +74 -20
  28. package/dist/engines/little-3d-engine/renderers/canvas2d.js +23 -1
  29. package/dist/engines/little-3d-engine/renderers/webgl-textured.js +8 -3
  30. package/dist/engines/little-3d-engine/renderers/webgl.js +41 -7
  31. package/dist/engines/little-3d-engine/renderers/webgpu.js +43 -10
  32. package/dist/engines/little-3d-engine/shapes/complex/plane.d.ts +8 -3
  33. package/dist/engines/little-3d-engine/shapes/complex/plane.js +10 -4
  34. package/dist/engines/little-3d-engine/shapes/primitives/cube.d.ts +3 -2
  35. package/dist/engines/little-3d-engine/shapes/primitives/cube.js +4 -2
  36. package/dist/engines/little-3d-engine/shapes/primitives/octahedron.d.ts +3 -2
  37. package/dist/engines/little-3d-engine/shapes/primitives/octahedron.js +4 -2
  38. package/dist/engines/little-3d-engine/shapes/primitives/pyramid.d.ts +3 -2
  39. package/dist/engines/little-3d-engine/shapes/primitives/pyramid.js +4 -2
  40. package/dist/engines/little-3d-engine/shapes/primitives/quad.d.ts +3 -2
  41. package/dist/engines/little-3d-engine/shapes/primitives/quad.js +4 -2
  42. package/dist/engines/little-3d-engine/shapes/primitives/spheres/cube-sphere.d.ts +3 -2
  43. package/dist/engines/little-3d-engine/shapes/primitives/spheres/cube-sphere.js +4 -2
  44. package/dist/engines/little-3d-engine/shapes/primitives/spheres/icosphere.d.ts +3 -2
  45. package/dist/engines/little-3d-engine/shapes/primitives/spheres/icosphere.js +4 -2
  46. package/dist/engines/little-3d-engine/shapes/primitives/spheres/octa-sphere.d.ts +3 -2
  47. package/dist/engines/little-3d-engine/shapes/primitives/spheres/octa-sphere.js +4 -2
  48. package/dist/engines/little-3d-engine/shapes/primitives/spheres/uv-sphere.d.ts +3 -2
  49. package/dist/engines/little-3d-engine/shapes/primitives/spheres/uv-sphere.js +4 -2
  50. package/dist/engines/little-3d-engine/shapes/primitives/tetrahedron.d.ts +3 -2
  51. package/dist/engines/little-3d-engine/shapes/primitives/tetrahedron.js +4 -2
  52. package/dist/umd/spinner.global.js +271 -67
  53. package/dist/umd/spinner.global.min.js +58 -17
  54. package/package.json +1 -1
@@ -139,7 +139,10 @@ function expandToTriangles(mesh) {
139
139
  const positions = new Float32Array(triangles * 9);
140
140
  const normals = new Float32Array(triangles * 9);
141
141
  const colors = new Float32Array(triangles * 9);
142
+ const emissives = new Float32Array(triangles * 9);
143
+ const speculars = new Float32Array(triangles * 12);
142
144
  let o = 0;
145
+ let so = 0;
143
146
  for (const face of mesh.faces) {
144
147
  const v0 = mesh.vertices[face.indices[0]];
145
148
  const v1 = mesh.vertices[face.indices[1]];
@@ -149,6 +152,15 @@ function expandToTriangles(mesh) {
149
152
  const cr = r / 255;
150
153
  const cg = g / 255;
151
154
  const cb = b / 255;
155
+ const emissive = face.material?.emissive;
156
+ const er = emissive ? emissive[0] : 0;
157
+ const eg = emissive ? emissive[1] : 0;
158
+ const eb = emissive ? emissive[2] : 0;
159
+ const specular = face.material?.specular;
160
+ const sr = specular ? specular[0] : 0;
161
+ const sg = specular ? specular[1] : 0;
162
+ const sb = specular ? specular[2] : 0;
163
+ const sn = specular ? face.material?.shininess ?? 32 : 1;
152
164
  for (let k = 1; k < face.indices.length - 1; k++) {
153
165
  const tri = [face.indices[0], face.indices[k], face.indices[k + 1]];
154
166
  for (const index of tri) {
@@ -162,11 +174,19 @@ function expandToTriangles(mesh) {
162
174
  colors[o] = cr;
163
175
  colors[o + 1] = cg;
164
176
  colors[o + 2] = cb;
177
+ emissives[o] = er;
178
+ emissives[o + 1] = eg;
179
+ emissives[o + 2] = eb;
180
+ speculars[so] = sr;
181
+ speculars[so + 1] = sg;
182
+ speculars[so + 2] = sb;
183
+ speculars[so + 3] = sn;
165
184
  o += 3;
185
+ so += 4;
166
186
  }
167
187
  }
168
188
  }
169
- return { positions, normals, colors, count: positions.length / 3 };
189
+ return { positions, normals, colors, emissives, speculars, count: positions.length / 3 };
170
190
  }
171
191
  function midpoint(a, b) {
172
192
  return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, z: (a.z + b.z) / 2 };
@@ -204,13 +224,42 @@ var init_geometry = __esm({
204
224
  function clamp01(value) {
205
225
  return Math.min(1, Math.max(0, value));
206
226
  }
207
- function shadeColor(normal, color, light) {
227
+ function clamp255(value) {
228
+ return Math.round(Math.min(255, Math.max(0, value)));
229
+ }
230
+ function shade(normal, color, light, surface) {
208
231
  const lambert = Math.max(0, dot(normal, light.toLight));
209
232
  const brightness = clamp01(light.ambient + light.intensity * lambert);
210
- const [r, g, b] = parseColor(color);
211
- return `rgb(${Math.round(r * brightness)}, ${Math.round(g * brightness)}, ${Math.round(
212
- b * brightness
213
- )})`;
233
+ const [baseR, baseG, baseB] = parseColor(color);
234
+ let r = baseR * brightness;
235
+ let g = baseG * brightness;
236
+ let b = baseB * brightness;
237
+ const material = surface?.material;
238
+ const specular = material?.specular;
239
+ const viewDir = surface?.viewDir;
240
+ if (specular && viewDir && lambert > 0) {
241
+ const half = normalize({
242
+ x: light.toLight.x + viewDir.x,
243
+ y: light.toLight.y + viewDir.y,
244
+ z: light.toLight.z + viewDir.z
245
+ });
246
+ const shininess = material?.shininess ?? 32;
247
+ const highlight = Math.pow(Math.max(0, dot(normal, half)), shininess) * light.intensity * 255;
248
+ r += highlight * specular[0];
249
+ g += highlight * specular[1];
250
+ b += highlight * specular[2];
251
+ }
252
+ const emissive = material?.emissive;
253
+ if (emissive) {
254
+ r += emissive[0] * 255;
255
+ g += emissive[1] * 255;
256
+ b += emissive[2] * 255;
257
+ }
258
+ return [clamp255(r), clamp255(g), clamp255(b)];
259
+ }
260
+ function shadeColor(normal, color, light, surface) {
261
+ const [r, g, b] = shade(normal, color, light, surface);
262
+ return `rgb(${r}, ${g}, ${b})`;
214
263
  }
215
264
  var DEFAULTS2, Light;
216
265
  var init_light = __esm({
@@ -233,8 +282,8 @@ var init_light = __esm({
233
282
  };
234
283
  }
235
284
  /** Convenience wrapper around {@link shadeColor} using this light. */
236
- shade(normal, color) {
237
- return shadeColor(normal, color, this.params);
285
+ shade(normal, color, surface) {
286
+ return shadeColor(normal, color, this.params, surface);
238
287
  }
239
288
  };
240
289
  }
@@ -274,28 +323,51 @@ var init_webgl = __esm({
274
323
  in vec3 aPos;
275
324
  in vec3 aNormal;
276
325
  in vec3 aColor;
326
+ in vec3 aEmissive;
327
+ in vec4 aSpecular;
277
328
  uniform mat4 uViewProj;
278
329
  uniform mat4 uModel;
279
330
  out vec3 vNormal;
280
331
  out vec3 vColor;
332
+ out vec3 vEmissive;
333
+ out vec4 vSpecular;
334
+ out vec3 vWorldPos;
281
335
  void main() {
282
336
  vNormal = mat3(uModel) * aNormal;
283
337
  vColor = aColor;
284
- gl_Position = uViewProj * uModel * vec4(aPos, 1.0);
338
+ vEmissive = aEmissive;
339
+ vSpecular = aSpecular;
340
+ vec4 world = uModel * vec4(aPos, 1.0);
341
+ vWorldPos = world.xyz;
342
+ gl_Position = uViewProj * world;
285
343
  }`;
286
344
  FRAGMENT_SHADER = `#version 300 es
287
345
  precision mediump float;
288
346
  in vec3 vNormal;
289
347
  in vec3 vColor;
348
+ in vec3 vEmissive;
349
+ in vec4 vSpecular;
350
+ in vec3 vWorldPos;
290
351
  uniform vec3 uToLight;
352
+ uniform vec3 uEye;
291
353
  uniform float uIntensity;
292
354
  uniform float uAmbient;
293
355
  uniform float uOpacity;
294
356
  out vec4 fragColor;
295
357
  void main() {
296
- float lambert = max(dot(normalize(vNormal), normalize(uToLight)), 0.0);
358
+ vec3 normal = normalize(vNormal);
359
+ vec3 toLight = normalize(uToLight);
360
+ float lambert = max(dot(normal, toLight), 0.0);
297
361
  float brightness = clamp(uAmbient + uIntensity * lambert, 0.0, 1.0);
298
- fragColor = vec4(vColor * brightness, uOpacity);
362
+ vec3 lit = vColor * brightness;
363
+ if (lambert > 0.0) {
364
+ vec3 viewDir = normalize(uEye - vWorldPos);
365
+ vec3 halfVec = normalize(toLight + viewDir);
366
+ float highlight = pow(max(dot(normal, halfVec), 0.0), vSpecular.w) * uIntensity;
367
+ lit += highlight * vSpecular.xyz;
368
+ }
369
+ lit += vEmissive;
370
+ fragColor = vec4(lit, uOpacity);
299
371
  }`;
300
372
  WebGLRenderer = class {
301
373
  constructor(options = {}) {
@@ -316,9 +388,12 @@ void main() {
316
388
  aPos: gl.getAttribLocation(this.program, "aPos"),
317
389
  aNormal: gl.getAttribLocation(this.program, "aNormal"),
318
390
  aColor: gl.getAttribLocation(this.program, "aColor"),
391
+ aEmissive: gl.getAttribLocation(this.program, "aEmissive"),
392
+ aSpecular: gl.getAttribLocation(this.program, "aSpecular"),
319
393
  uViewProj: gl.getUniformLocation(this.program, "uViewProj"),
320
394
  uModel: gl.getUniformLocation(this.program, "uModel"),
321
395
  uToLight: gl.getUniformLocation(this.program, "uToLight"),
396
+ uEye: gl.getUniformLocation(this.program, "uEye"),
322
397
  uIntensity: gl.getUniformLocation(this.program, "uIntensity"),
323
398
  uAmbient: gl.getUniformLocation(this.program, "uAmbient"),
324
399
  uOpacity: gl.getUniformLocation(this.program, "uOpacity")
@@ -342,19 +417,23 @@ void main() {
342
417
  const data = expandToTriangles(mesh);
343
418
  const vao = gl.createVertexArray();
344
419
  gl.bindVertexArray(vao);
345
- const attribute = (location, array) => {
420
+ const buffers = [];
421
+ const attribute = (location, array, size = 3) => {
346
422
  if (location < 0) return;
347
423
  const buffer = gl.createBuffer();
424
+ buffers.push(buffer);
348
425
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
349
426
  gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW);
350
427
  gl.enableVertexAttribArray(location);
351
- gl.vertexAttribPointer(location, 3, gl.FLOAT, false, 0, 0);
428
+ gl.vertexAttribPointer(location, size, gl.FLOAT, false, 0, 0);
352
429
  };
353
430
  attribute(loc.aPos, data.positions);
354
431
  attribute(loc.aNormal, data.normals);
355
432
  attribute(loc.aColor, data.colors);
433
+ attribute(loc.aEmissive, data.emissives);
434
+ attribute(loc.aSpecular, data.speculars, 4);
356
435
  gl.bindVertexArray(null);
357
- const result = { vao, count: data.count };
436
+ const result = { vao, buffers, count: data.count };
358
437
  this.cache.set(mesh, result);
359
438
  return result;
360
439
  }
@@ -367,6 +446,7 @@ void main() {
367
446
  gl.useProgram(this.program);
368
447
  gl.uniformMatrix4fv(loc.uViewProj, false, new Float32Array(frame.viewProjection));
369
448
  gl.uniform3f(loc.uToLight, frame.light.toLight.x, frame.light.toLight.y, frame.light.toLight.z);
449
+ gl.uniform3f(loc.uEye, frame.eye.x, frame.eye.y, frame.eye.z);
370
450
  gl.uniform1f(loc.uIntensity, frame.light.intensity);
371
451
  gl.uniform1f(loc.uAmbient, frame.light.ambient);
372
452
  gl.disable(gl.BLEND);
@@ -411,7 +491,10 @@ void main() {
411
491
  destroy() {
412
492
  const gl = this.gl;
413
493
  if (gl) {
414
- for (const mesh of this.cache.values()) gl.deleteVertexArray(mesh.vao);
494
+ for (const mesh of this.cache.values()) {
495
+ gl.deleteVertexArray(mesh.vao);
496
+ for (const buffer of mesh.buffers) gl.deleteBuffer(buffer);
497
+ }
415
498
  if (this.program) gl.deleteProgram(this.program);
416
499
  }
417
500
  this.cache.clear();
@@ -441,6 +524,7 @@ struct Uniforms {
441
524
  model: mat4x4<f32>,
442
525
  toLight: vec4<f32>,
443
526
  params: vec4<f32>,
527
+ eye: vec4<f32>,
444
528
  };
445
529
  @group(0) @binding(0) var<uniform> u: Uniforms;
446
530
 
@@ -448,23 +532,40 @@ struct VSOut {
448
532
  @builtin(position) position: vec4<f32>,
449
533
  @location(0) normal: vec3<f32>,
450
534
  @location(1) color: vec3<f32>,
535
+ @location(2) emissive: vec3<f32>,
536
+ @location(3) specular: vec4<f32>,
537
+ @location(4) worldPos: vec3<f32>,
451
538
  };
452
539
 
453
540
  @vertex
454
- fn vs(@location(0) pos: vec3<f32>, @location(1) normal: vec3<f32>, @location(2) color: vec3<f32>) -> VSOut {
541
+ fn vs(@location(0) pos: vec3<f32>, @location(1) normal: vec3<f32>, @location(2) color: vec3<f32>, @location(3) emissive: vec3<f32>, @location(4) specular: vec4<f32>) -> VSOut {
455
542
  var out: VSOut;
456
543
  let m = mat3x3<f32>(u.model[0].xyz, u.model[1].xyz, u.model[2].xyz);
457
544
  out.normal = m * normal;
458
545
  out.color = color;
459
- out.position = u.viewProj * u.model * vec4<f32>(pos, 1.0);
546
+ out.emissive = emissive;
547
+ out.specular = specular;
548
+ let world = u.model * vec4<f32>(pos, 1.0);
549
+ out.worldPos = world.xyz;
550
+ out.position = u.viewProj * world;
460
551
  return out;
461
552
  }
462
553
 
463
554
  @fragment
464
555
  fn fs(in: VSOut) -> @location(0) vec4<f32> {
465
- let lambert = max(dot(normalize(in.normal), normalize(u.toLight.xyz)), 0.0);
556
+ let normal = normalize(in.normal);
557
+ let toLight = normalize(u.toLight.xyz);
558
+ let lambert = max(dot(normal, toLight), 0.0);
466
559
  let brightness = clamp(u.params.y + u.params.x * lambert, 0.0, 1.0);
467
- return vec4<f32>(in.color * brightness, u.params.z);
560
+ var lit = in.color * brightness;
561
+ if (lambert > 0.0) {
562
+ let viewDir = normalize(u.eye.xyz - in.worldPos);
563
+ let halfVec = normalize(toLight + viewDir);
564
+ let highlight = pow(max(dot(normal, halfVec), 0.0), in.specular.w) * u.params.x;
565
+ lit = lit + highlight * in.specular.xyz;
566
+ }
567
+ lit = lit + in.emissive;
568
+ return vec4<f32>(lit, u.params.z);
468
569
  }
469
570
  `;
470
571
  CLIP_Z_FIX = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 1];
@@ -505,13 +606,15 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
505
606
  {
506
607
  binding: 0,
507
608
  visibility: stage.VERTEX | stage.FRAGMENT,
508
- buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: 160 }
609
+ buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: 176 }
509
610
  }
510
611
  ]
511
612
  });
512
- const vertexBuffer = (location) => ({
513
- arrayStride: 12,
514
- attributes: [{ shaderLocation: location, offset: 0, format: "float32x3" }]
613
+ const vertexBuffer = (location, components = 3) => ({
614
+ arrayStride: components * 4,
615
+ attributes: [
616
+ { shaderLocation: location, offset: 0, format: `float32x${components}` }
617
+ ]
515
618
  });
516
619
  const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [layout] });
517
620
  const blend = {
@@ -527,7 +630,13 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
527
630
  vertex: {
528
631
  module: module2,
529
632
  entryPoint: "vs",
530
- buffers: [vertexBuffer(0), vertexBuffer(1), vertexBuffer(2)]
633
+ buffers: [
634
+ vertexBuffer(0),
635
+ vertexBuffer(1),
636
+ vertexBuffer(2),
637
+ vertexBuffer(3),
638
+ vertexBuffer(4, 4)
639
+ ]
531
640
  },
532
641
  fragment: {
533
642
  module: module2,
@@ -580,6 +689,8 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
580
689
  position: upload(data.positions),
581
690
  normal: upload(data.normals),
582
691
  color: upload(data.colors),
692
+ emissive: upload(data.emissives),
693
+ specular: upload(data.speculars),
583
694
  count: data.count
584
695
  };
585
696
  this.cache.set(mesh, result);
@@ -631,7 +742,7 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
631
742
  const bindGroup = this.device.createBindGroup({
632
743
  layout,
633
744
  entries: [
634
- { binding: 0, resource: { buffer: this.uniformBuffer, offset: 0, size: 160 } }
745
+ { binding: 0, resource: { buffer: this.uniformBuffer, offset: 0, size: 176 } }
635
746
  ]
636
747
  });
637
748
  draws.forEach((draw, i) => {
@@ -640,6 +751,7 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
640
751
  data.set(draw.item.model, 16);
641
752
  data.set([frame.light.toLight.x, frame.light.toLight.y, frame.light.toLight.z, 0], 32);
642
753
  data.set([frame.light.intensity, frame.light.ambient, draw.opacity, 0], 36);
754
+ data.set([frame.eye.x, frame.eye.y, frame.eye.z, 0], 40);
643
755
  this.device.queue.writeBuffer(this.uniformBuffer, i * UNIFORM_STRIDE, data);
644
756
  });
645
757
  const encoder = this.device.createCommandEncoder();
@@ -666,6 +778,8 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
666
778
  pass.setVertexBuffer(0, mesh.position);
667
779
  pass.setVertexBuffer(1, mesh.normal);
668
780
  pass.setVertexBuffer(2, mesh.color);
781
+ pass.setVertexBuffer(3, mesh.emissive);
782
+ pass.setVertexBuffer(4, mesh.specular);
669
783
  pass.draw(mesh.count);
670
784
  });
671
785
  pass.end();
@@ -677,6 +791,8 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
677
791
  mesh.position.destroy?.();
678
792
  mesh.normal.destroy?.();
679
793
  mesh.color.destroy?.();
794
+ mesh.emissive.destroy?.();
795
+ mesh.specular.destroy?.();
680
796
  }
681
797
  this.cache.clear();
682
798
  this.uniformBuffer?.destroy?.();
@@ -759,9 +875,30 @@ var init_canvas2d = __esm({
759
875
  depth += dot(d, d);
760
876
  }
761
877
  depth /= face.indices.length;
878
+ let surface;
879
+ const material = face.material;
880
+ if (material) {
881
+ if (material.specular) {
882
+ let cx = 0;
883
+ let cy = 0;
884
+ let cz = 0;
885
+ for (const i of face.indices) {
886
+ cx += world[i].x;
887
+ cy += world[i].y;
888
+ cz += world[i].z;
889
+ }
890
+ const inv = 1 / face.indices.length;
891
+ const viewDir = normalize(
892
+ subtract(frame.eye, { x: cx * inv, y: cy * inv, z: cz * inv })
893
+ );
894
+ surface = { material, viewDir };
895
+ } else {
896
+ surface = { material };
897
+ }
898
+ }
762
899
  polygons.push({
763
900
  points,
764
- color: shadeColor(normal, face.color, frame.light),
901
+ color: shadeColor(normal, face.color, frame.light, surface),
765
902
  depth,
766
903
  opacity: faceOpacity
767
904
  });
@@ -852,6 +989,7 @@ __export(little_3d_engine_exports, {
852
989
  Camera: () => Camera,
853
990
  Light: () => Light,
854
991
  Little3dEngine: () => Little3dEngine,
992
+ attachMaterial: () => attachMaterial,
855
993
  cross: () => cross,
856
994
  cube: () => cube,
857
995
  cubeSphere: () => cubeSphere,
@@ -915,6 +1053,12 @@ init_light();
915
1053
  init_math();
916
1054
 
917
1055
  // src/engines/little-3d-engine/core/mesh.ts
1056
+ function attachMaterial(mesh, material) {
1057
+ if (material) {
1058
+ for (const face of mesh.faces) face.material = material;
1059
+ }
1060
+ return mesh;
1061
+ }
918
1062
  function transform(init) {
919
1063
  return {
920
1064
  position: init?.position ?? { x: 0, y: 0, z: 0 },
@@ -929,7 +1073,7 @@ init_light();
929
1073
 
930
1074
  // src/engines/little-3d-engine/shapes/primitives/cube.ts
931
1075
  var DEFAULT_COLORS = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
932
- function cube(size = 1, colors = DEFAULT_COLORS) {
1076
+ function cube(size = 1, colors = DEFAULT_COLORS, material) {
933
1077
  const h = size / 2;
934
1078
  const vertices = [
935
1079
  { x: -h, y: -h, z: h },
@@ -949,12 +1093,12 @@ function cube(size = 1, colors = DEFAULT_COLORS) {
949
1093
  { indices: [1, 5, 6, 2], color: colors[4 % colors.length] },
950
1094
  { indices: [4, 0, 3, 7], color: colors[5 % colors.length] }
951
1095
  ];
952
- return { vertices, faces };
1096
+ return attachMaterial({ vertices, faces }, material);
953
1097
  }
954
1098
 
955
1099
  // src/engines/little-3d-engine/shapes/primitives/quad.ts
956
1100
  var DEFAULT_COLORS2 = ["#3b82f6"];
957
- function quad(size = 1, colors = DEFAULT_COLORS2) {
1101
+ function quad(size = 1, colors = DEFAULT_COLORS2, material) {
958
1102
  const s = size / 2;
959
1103
  const vertices = [
960
1104
  { x: -s, y: -s, z: 0 },
@@ -962,12 +1106,15 @@ function quad(size = 1, colors = DEFAULT_COLORS2) {
962
1106
  { x: s, y: s, z: 0 },
963
1107
  { x: -s, y: s, z: 0 }
964
1108
  ];
965
- return { vertices, faces: [{ indices: [0, 1, 2, 3], color: colors[0] }] };
1109
+ return attachMaterial(
1110
+ { vertices, faces: [{ indices: [0, 1, 2, 3], color: colors[0] }] },
1111
+ material
1112
+ );
966
1113
  }
967
1114
 
968
1115
  // src/engines/little-3d-engine/shapes/primitives/tetrahedron.ts
969
1116
  var DEFAULT_COLORS3 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b"];
970
- function tetrahedron(size = 1, colors = DEFAULT_COLORS3) {
1117
+ function tetrahedron(size = 1, colors = DEFAULT_COLORS3, material) {
971
1118
  const s = size / 2;
972
1119
  const vertices = [
973
1120
  { x: s, y: s, z: s },
@@ -981,7 +1128,7 @@ function tetrahedron(size = 1, colors = DEFAULT_COLORS3) {
981
1128
  { indices: [0, 2, 3], color: colors[2 % colors.length] },
982
1129
  { indices: [1, 3, 2], color: colors[3 % colors.length] }
983
1130
  ];
984
- return { vertices, faces };
1131
+ return attachMaterial({ vertices, faces }, material);
985
1132
  }
986
1133
 
987
1134
  // src/engines/little-3d-engine/shapes/primitives/octahedron.ts
@@ -995,7 +1142,7 @@ var DEFAULT_COLORS4 = [
995
1142
  "#06b6d4",
996
1143
  "#eab308"
997
1144
  ];
998
- function octahedron(size = 1, colors = DEFAULT_COLORS4) {
1145
+ function octahedron(size = 1, colors = DEFAULT_COLORS4, material) {
999
1146
  const r = size / 2;
1000
1147
  const vertices = [
1001
1148
  { x: r, y: 0, z: 0 },
@@ -1015,12 +1162,12 @@ function octahedron(size = 1, colors = DEFAULT_COLORS4) {
1015
1162
  { indices: [5, 3, 1], color: colors[6 % colors.length] },
1016
1163
  { indices: [5, 0, 3], color: colors[7 % colors.length] }
1017
1164
  ];
1018
- return { vertices, faces };
1165
+ return attachMaterial({ vertices, faces }, material);
1019
1166
  }
1020
1167
 
1021
1168
  // src/engines/little-3d-engine/shapes/primitives/pyramid.ts
1022
1169
  var DEFAULT_COLORS5 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981"];
1023
- function pyramid(size = 1, colors = DEFAULT_COLORS5) {
1170
+ function pyramid(size = 1, colors = DEFAULT_COLORS5, material) {
1024
1171
  const h = size / 2;
1025
1172
  const vertices = [
1026
1173
  { x: -h, y: -h, z: h },
@@ -1036,12 +1183,12 @@ function pyramid(size = 1, colors = DEFAULT_COLORS5) {
1036
1183
  { indices: [4, 2, 3], color: colors[3 % colors.length] },
1037
1184
  { indices: [4, 3, 0], color: colors[4 % colors.length] }
1038
1185
  ];
1039
- return { vertices, faces };
1186
+ return attachMaterial({ vertices, faces }, material);
1040
1187
  }
1041
1188
 
1042
1189
  // src/engines/little-3d-engine/shapes/primitives/spheres/uv-sphere.ts
1043
1190
  var DEFAULT_COLORS6 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
1044
- function uvSphere(size = 1, detail = 1, colors = DEFAULT_COLORS6) {
1191
+ function uvSphere(size = 1, detail = 1, colors = DEFAULT_COLORS6, material) {
1045
1192
  const r = size / 2;
1046
1193
  const d = Math.max(1, Math.floor(detail));
1047
1194
  const slices = Math.max(4, d * 4);
@@ -1080,7 +1227,7 @@ function uvSphere(size = 1, detail = 1, colors = DEFAULT_COLORS6) {
1080
1227
  color: color()
1081
1228
  });
1082
1229
  }
1083
- return { vertices, faces };
1230
+ return attachMaterial({ vertices, faces }, material);
1084
1231
  }
1085
1232
 
1086
1233
  // src/engines/little-3d-engine/shapes/primitives/spheres/icosphere.ts
@@ -1123,8 +1270,11 @@ var SEED_FACES = [
1123
1270
  [8, 6, 7],
1124
1271
  [9, 8, 1]
1125
1272
  ];
1126
- function icosphere(size = 1, detail = 1, colors = DEFAULT_COLORS7) {
1127
- return sphereFromTriangles(SEED_VERTICES, SEED_FACES, size, detail, colors);
1273
+ function icosphere(size = 1, detail = 1, colors = DEFAULT_COLORS7, material) {
1274
+ return attachMaterial(
1275
+ sphereFromTriangles(SEED_VERTICES, SEED_FACES, size, detail, colors),
1276
+ material
1277
+ );
1128
1278
  }
1129
1279
 
1130
1280
  // src/engines/little-3d-engine/shapes/primitives/spheres/octa-sphere.ts
@@ -1148,8 +1298,11 @@ var SEED_FACES2 = [
1148
1298
  [5, 3, 1],
1149
1299
  [5, 0, 3]
1150
1300
  ];
1151
- function octaSphere(size = 1, detail = 1, colors = DEFAULT_COLORS8) {
1152
- return sphereFromTriangles(SEED_VERTICES2, SEED_FACES2, size, detail, colors);
1301
+ function octaSphere(size = 1, detail = 1, colors = DEFAULT_COLORS8, material) {
1302
+ return attachMaterial(
1303
+ sphereFromTriangles(SEED_VERTICES2, SEED_FACES2, size, detail, colors),
1304
+ material
1305
+ );
1153
1306
  }
1154
1307
 
1155
1308
  // src/engines/little-3d-engine/shapes/primitives/spheres/cube-sphere.ts
@@ -1162,7 +1315,7 @@ var CUBE_FACES = [
1162
1315
  { normal: [0, 1, 0], right: [1, 0, 0], up: [0, 0, -1] },
1163
1316
  { normal: [0, -1, 0], right: [1, 0, 0], up: [0, 0, 1] }
1164
1317
  ];
1165
- function cubeSphere(size = 1, detail = 1, colors = DEFAULT_COLORS9) {
1318
+ function cubeSphere(size = 1, detail = 1, colors = DEFAULT_COLORS9, material) {
1166
1319
  const r = size / 2;
1167
1320
  const n = Math.max(1, Math.floor(detail));
1168
1321
  const vertices = [];
@@ -1191,13 +1344,13 @@ function cubeSphere(size = 1, detail = 1, colors = DEFAULT_COLORS9) {
1191
1344
  }
1192
1345
  }
1193
1346
  }
1194
- return { vertices, faces };
1347
+ return attachMaterial({ vertices, faces }, material);
1195
1348
  }
1196
1349
 
1197
1350
  // src/engines/little-3d-engine/shapes/complex/plane.ts
1198
1351
  var DEFAULT_COLORS10 = ["#e0f2fe", "#7dd3fc", "#38bdf8", "#f8fafc"];
1199
- function planeMesh(colors = DEFAULT_COLORS10) {
1200
- return {
1352
+ function planeMesh(colors = DEFAULT_COLORS10, material) {
1353
+ return attachMaterial({
1201
1354
  vertices: [
1202
1355
  { x: 0.9, y: 0, z: 0 },
1203
1356
  { x: -0.2, y: 0, z: 0.82 },
@@ -1225,7 +1378,7 @@ function planeMesh(colors = DEFAULT_COLORS10) {
1225
1378
  { indices: [3, 6, 8], color: colors[0] ?? DEFAULT_COLORS10[0] },
1226
1379
  { indices: [3, 8, 6], color: colors[1] ?? DEFAULT_COLORS10[1] }
1227
1380
  ]
1228
- };
1381
+ }, material);
1229
1382
  }
1230
1383
 
1231
1384
  // src/engines/little-3d-engine/textures/dynamic/canvas-texture.ts
@@ -24,28 +24,64 @@ __export(obj_exports, {
24
24
  });
25
25
  module.exports = __toCommonJS(obj_exports);
26
26
  var DEFAULT_COLORS = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
27
+ function clamp01(value) {
28
+ return Math.min(1, Math.max(0, value));
29
+ }
27
30
  function channelToHex(value) {
28
31
  const channel = Number.parseFloat(value);
29
32
  if (!Number.isFinite(channel)) return void 0;
30
- return Math.round(Math.min(1, Math.max(0, channel)) * 255).toString(16).padStart(2, "0");
33
+ return Math.round(clamp01(channel) * 255).toString(16).padStart(2, "0");
31
34
  }
32
- function parseMtlColors(text) {
33
- const colors = /* @__PURE__ */ new Map();
34
- let material;
35
+ function parseRgb(parts) {
36
+ const channels = parts.slice(1, 4).map(Number.parseFloat);
37
+ if (channels.length !== 3 || !channels.every(Number.isFinite)) return void 0;
38
+ return [clamp01(channels[0]), clamp01(channels[1]), clamp01(channels[2])];
39
+ }
40
+ function toMaterial(surface) {
41
+ const material = {};
42
+ if (surface.specular) material.specular = surface.specular;
43
+ if (surface.shininess !== void 0) material.shininess = surface.shininess;
44
+ if (surface.emissive) material.emissive = surface.emissive;
45
+ return Object.keys(material).length > 0 ? material : void 0;
46
+ }
47
+ function parseMtl(text) {
48
+ const materials = /* @__PURE__ */ new Map();
49
+ const surfaces = /* @__PURE__ */ new Map();
50
+ let name;
35
51
  for (const line of text.split("\n")) {
36
52
  const trimmed = line.trim();
37
53
  if (trimmed === "" || trimmed.startsWith("#")) continue;
38
54
  const parts = trimmed.split(/\s+/);
39
- if (parts[0] === "newmtl") {
40
- material = parts.slice(1).join(" ");
41
- } else if (parts[0] === "Kd" && material) {
55
+ const keyword = parts[0];
56
+ if (keyword === "newmtl") {
57
+ name = parts.slice(1).join(" ");
58
+ if (name && !materials.has(name)) {
59
+ materials.set(name, {});
60
+ surfaces.set(name, {});
61
+ }
62
+ continue;
63
+ }
64
+ if (!name) continue;
65
+ const entry = materials.get(name);
66
+ const surface = surfaces.get(name);
67
+ if (keyword === "Kd") {
42
68
  const channels = parts.slice(1, 4).map(channelToHex);
43
69
  if (channels.length === 3 && channels.every((channel) => channel !== void 0)) {
44
- colors.set(material, `#${channels.join("")}`);
70
+ entry.color = `#${channels.join("")}`;
45
71
  }
72
+ } else if (keyword === "Ks") {
73
+ surface.specular = parseRgb(parts);
74
+ } else if (keyword === "Ns") {
75
+ const ns = Number.parseFloat(parts[1]);
76
+ if (Number.isFinite(ns)) surface.shininess = Math.max(0, ns);
77
+ } else if (keyword === "Ke") {
78
+ surface.emissive = parseRgb(parts);
46
79
  }
47
80
  }
48
- return colors;
81
+ for (const [key, surface] of surfaces) {
82
+ materials.get(key).material = toMaterial(surface);
83
+ }
84
+ return materials;
49
85
  }
50
86
  function resolveIndex(token, vertexCount) {
51
87
  const n = parseInt(token, 10);
@@ -53,7 +89,7 @@ function resolveIndex(token, vertexCount) {
53
89
  }
54
90
  function parseObj(text, options = {}) {
55
91
  const colors = options.colors ?? DEFAULT_COLORS;
56
- const materialColors = options.useMtlColors && options.mtl ? parseMtlColors(options.mtl) : void 0;
92
+ const materials = options.useMtlColors && options.mtl ? parseMtl(options.mtl) : void 0;
57
93
  const vertices = [];
58
94
  const faces = [];
59
95
  let material;
@@ -77,9 +113,11 @@ function parseObj(text, options = {}) {
77
113
  indices.push(resolveIndex(vertexToken, vertices.length));
78
114
  }
79
115
  if (indices.length >= 3) {
80
- const mtlColor = material ? materialColors?.get(material) : void 0;
81
- const color = mtlColor ?? (materialColors ? colors[0] ?? "#888888" : colors[faces.length % colors.length]);
82
- faces.push({ indices, color });
116
+ const entry = material ? materials?.get(material) : void 0;
117
+ const color = entry?.color ?? (materials ? colors[0] ?? "#888888" : colors[faces.length % colors.length]);
118
+ const face = { indices, color };
119
+ if (entry?.material) face.material = entry.material;
120
+ faces.push(face);
83
121
  }
84
122
  }
85
123
  }