3d-spinner 0.9.4 → 0.9.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/dist/animations/charged-orb.js +4 -2
  2. package/dist/animations/ghost-train.js +3 -1
  3. package/dist/animations/spin.d.ts +3 -1
  4. package/dist/animations/spin.js +6 -1
  5. package/dist/cjs/animations/charged-orb.cjs +170 -28
  6. package/dist/cjs/animations/grid-assembly.cjs +164 -26
  7. package/dist/cjs/animations/object-motion.cjs +156 -24
  8. package/dist/cjs/animations/particles.cjs +167 -26
  9. package/dist/cjs/animations/spin.cjs +169 -27
  10. package/dist/cjs/engines/little-3d-engine/little-3d-engine.cjs +193 -45
  11. package/dist/cjs/engines/little-3d-engine/loaders/obj.cjs +51 -13
  12. package/dist/cjs/engines/little-3d-engine/renderers/canvas2d-textured.cjs +56 -6
  13. package/dist/cjs/engines/little-3d-engine/renderers/webgl-textured.cjs +55 -6
  14. package/dist/cjs/engines/little-3d-engine/renderers/webgpu-textured.cjs +64 -11
  15. package/dist/cjs/prefabs/prefabs.cjs +186 -40
  16. package/dist/engines/little-3d-engine/core/geometry.d.ts +15 -2
  17. package/dist/engines/little-3d-engine/core/geometry.js +25 -3
  18. package/dist/engines/little-3d-engine/core/light.d.ts +28 -4
  19. package/dist/engines/little-3d-engine/core/light.js +48 -7
  20. package/dist/engines/little-3d-engine/core/mesh.d.ts +33 -0
  21. package/dist/engines/little-3d-engine/core/mesh.js +12 -0
  22. package/dist/engines/little-3d-engine/little-3d-engine.d.ts +2 -2
  23. package/dist/engines/little-3d-engine/little-3d-engine.js +1 -1
  24. package/dist/engines/little-3d-engine/loaders/obj.d.ts +8 -7
  25. package/dist/engines/little-3d-engine/loaders/obj.js +74 -20
  26. package/dist/engines/little-3d-engine/renderers/canvas2d.js +23 -1
  27. package/dist/engines/little-3d-engine/renderers/webgl.js +34 -5
  28. package/dist/engines/little-3d-engine/renderers/webgpu.js +43 -10
  29. package/dist/engines/little-3d-engine/shapes/complex/plane.d.ts +8 -3
  30. package/dist/engines/little-3d-engine/shapes/complex/plane.js +10 -4
  31. package/dist/engines/little-3d-engine/shapes/primitives/cube.d.ts +3 -2
  32. package/dist/engines/little-3d-engine/shapes/primitives/cube.js +4 -2
  33. package/dist/engines/little-3d-engine/shapes/primitives/octahedron.d.ts +3 -2
  34. package/dist/engines/little-3d-engine/shapes/primitives/octahedron.js +4 -2
  35. package/dist/engines/little-3d-engine/shapes/primitives/pyramid.d.ts +3 -2
  36. package/dist/engines/little-3d-engine/shapes/primitives/pyramid.js +4 -2
  37. package/dist/engines/little-3d-engine/shapes/primitives/quad.d.ts +3 -2
  38. package/dist/engines/little-3d-engine/shapes/primitives/quad.js +4 -2
  39. package/dist/engines/little-3d-engine/shapes/primitives/spheres/cube-sphere.d.ts +3 -2
  40. package/dist/engines/little-3d-engine/shapes/primitives/spheres/cube-sphere.js +4 -2
  41. package/dist/engines/little-3d-engine/shapes/primitives/spheres/icosphere.d.ts +3 -2
  42. package/dist/engines/little-3d-engine/shapes/primitives/spheres/icosphere.js +4 -2
  43. package/dist/engines/little-3d-engine/shapes/primitives/spheres/octa-sphere.d.ts +3 -2
  44. package/dist/engines/little-3d-engine/shapes/primitives/spheres/octa-sphere.js +4 -2
  45. package/dist/engines/little-3d-engine/shapes/primitives/spheres/uv-sphere.d.ts +3 -2
  46. package/dist/engines/little-3d-engine/shapes/primitives/spheres/uv-sphere.js +4 -2
  47. package/dist/engines/little-3d-engine/shapes/primitives/tetrahedron.d.ts +3 -2
  48. package/dist/engines/little-3d-engine/shapes/primitives/tetrahedron.js +4 -2
  49. package/dist/umd/spinner.global.js +255 -63
  50. package/dist/umd/spinner.global.min.js +58 -17
  51. package/package.json +1 -1
@@ -139,7 +139,10 @@ var Spinner3D = (() => {
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 @@ var Spinner3D = (() => {
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 @@ var Spinner3D = (() => {
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 Spinner3D = (() => {
204
224
  function clamp012(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 = clamp012(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 Spinner3D = (() => {
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 Spinner3D = (() => {
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,17 +417,19 @@ 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 attribute = (location, array, size = 3) => {
346
421
  if (location < 0) return;
347
422
  const buffer = gl.createBuffer();
348
423
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
349
424
  gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW);
350
425
  gl.enableVertexAttribArray(location);
351
- gl.vertexAttribPointer(location, 3, gl.FLOAT, false, 0, 0);
426
+ gl.vertexAttribPointer(location, size, gl.FLOAT, false, 0, 0);
352
427
  };
353
428
  attribute(loc.aPos, data.positions);
354
429
  attribute(loc.aNormal, data.normals);
355
430
  attribute(loc.aColor, data.colors);
431
+ attribute(loc.aEmissive, data.emissives);
432
+ attribute(loc.aSpecular, data.speculars, 4);
356
433
  gl.bindVertexArray(null);
357
434
  const result = { vao, count: data.count };
358
435
  this.cache.set(mesh, result);
@@ -367,6 +444,7 @@ void main() {
367
444
  gl.useProgram(this.program);
368
445
  gl.uniformMatrix4fv(loc.uViewProj, false, new Float32Array(frame.viewProjection));
369
446
  gl.uniform3f(loc.uToLight, frame.light.toLight.x, frame.light.toLight.y, frame.light.toLight.z);
447
+ gl.uniform3f(loc.uEye, frame.eye.x, frame.eye.y, frame.eye.z);
370
448
  gl.uniform1f(loc.uIntensity, frame.light.intensity);
371
449
  gl.uniform1f(loc.uAmbient, frame.light.ambient);
372
450
  gl.disable(gl.BLEND);
@@ -441,6 +519,7 @@ struct Uniforms {
441
519
  model: mat4x4<f32>,
442
520
  toLight: vec4<f32>,
443
521
  params: vec4<f32>,
522
+ eye: vec4<f32>,
444
523
  };
445
524
  @group(0) @binding(0) var<uniform> u: Uniforms;
446
525
 
@@ -448,23 +527,40 @@ struct VSOut {
448
527
  @builtin(position) position: vec4<f32>,
449
528
  @location(0) normal: vec3<f32>,
450
529
  @location(1) color: vec3<f32>,
530
+ @location(2) emissive: vec3<f32>,
531
+ @location(3) specular: vec4<f32>,
532
+ @location(4) worldPos: vec3<f32>,
451
533
  };
452
534
 
453
535
  @vertex
454
- fn vs(@location(0) pos: vec3<f32>, @location(1) normal: vec3<f32>, @location(2) color: vec3<f32>) -> VSOut {
536
+ 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
537
  var out: VSOut;
456
538
  let m = mat3x3<f32>(u.model[0].xyz, u.model[1].xyz, u.model[2].xyz);
457
539
  out.normal = m * normal;
458
540
  out.color = color;
459
- out.position = u.viewProj * u.model * vec4<f32>(pos, 1.0);
541
+ out.emissive = emissive;
542
+ out.specular = specular;
543
+ let world = u.model * vec4<f32>(pos, 1.0);
544
+ out.worldPos = world.xyz;
545
+ out.position = u.viewProj * world;
460
546
  return out;
461
547
  }
462
548
 
463
549
  @fragment
464
550
  fn fs(in: VSOut) -> @location(0) vec4<f32> {
465
- let lambert = max(dot(normalize(in.normal), normalize(u.toLight.xyz)), 0.0);
551
+ let normal = normalize(in.normal);
552
+ let toLight = normalize(u.toLight.xyz);
553
+ let lambert = max(dot(normal, toLight), 0.0);
466
554
  let brightness = clamp(u.params.y + u.params.x * lambert, 0.0, 1.0);
467
- return vec4<f32>(in.color * brightness, u.params.z);
555
+ var lit = in.color * brightness;
556
+ if (lambert > 0.0) {
557
+ let viewDir = normalize(u.eye.xyz - in.worldPos);
558
+ let halfVec = normalize(toLight + viewDir);
559
+ let highlight = pow(max(dot(normal, halfVec), 0.0), in.specular.w) * u.params.x;
560
+ lit = lit + highlight * in.specular.xyz;
561
+ }
562
+ lit = lit + in.emissive;
563
+ return vec4<f32>(lit, u.params.z);
468
564
  }
469
565
  `;
470
566
  CLIP_Z_FIX = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 1];
@@ -505,13 +601,15 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
505
601
  {
506
602
  binding: 0,
507
603
  visibility: stage.VERTEX | stage.FRAGMENT,
508
- buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: 160 }
604
+ buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: 176 }
509
605
  }
510
606
  ]
511
607
  });
512
- const vertexBuffer = (location) => ({
513
- arrayStride: 12,
514
- attributes: [{ shaderLocation: location, offset: 0, format: "float32x3" }]
608
+ const vertexBuffer = (location, components = 3) => ({
609
+ arrayStride: components * 4,
610
+ attributes: [
611
+ { shaderLocation: location, offset: 0, format: `float32x${components}` }
612
+ ]
515
613
  });
516
614
  const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [layout] });
517
615
  const blend = {
@@ -527,7 +625,13 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
527
625
  vertex: {
528
626
  module,
529
627
  entryPoint: "vs",
530
- buffers: [vertexBuffer(0), vertexBuffer(1), vertexBuffer(2)]
628
+ buffers: [
629
+ vertexBuffer(0),
630
+ vertexBuffer(1),
631
+ vertexBuffer(2),
632
+ vertexBuffer(3),
633
+ vertexBuffer(4, 4)
634
+ ]
531
635
  },
532
636
  fragment: {
533
637
  module,
@@ -580,6 +684,8 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
580
684
  position: upload(data.positions),
581
685
  normal: upload(data.normals),
582
686
  color: upload(data.colors),
687
+ emissive: upload(data.emissives),
688
+ specular: upload(data.speculars),
583
689
  count: data.count
584
690
  };
585
691
  this.cache.set(mesh, result);
@@ -631,7 +737,7 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
631
737
  const bindGroup = this.device.createBindGroup({
632
738
  layout,
633
739
  entries: [
634
- { binding: 0, resource: { buffer: this.uniformBuffer, offset: 0, size: 160 } }
740
+ { binding: 0, resource: { buffer: this.uniformBuffer, offset: 0, size: 176 } }
635
741
  ]
636
742
  });
637
743
  draws.forEach((draw, i) => {
@@ -640,6 +746,7 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
640
746
  data.set(draw.item.model, 16);
641
747
  data.set([frame.light.toLight.x, frame.light.toLight.y, frame.light.toLight.z, 0], 32);
642
748
  data.set([frame.light.intensity, frame.light.ambient, draw.opacity, 0], 36);
749
+ data.set([frame.eye.x, frame.eye.y, frame.eye.z, 0], 40);
643
750
  this.device.queue.writeBuffer(this.uniformBuffer, i * UNIFORM_STRIDE, data);
644
751
  });
645
752
  const encoder = this.device.createCommandEncoder();
@@ -666,6 +773,8 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
666
773
  pass.setVertexBuffer(0, mesh.position);
667
774
  pass.setVertexBuffer(1, mesh.normal);
668
775
  pass.setVertexBuffer(2, mesh.color);
776
+ pass.setVertexBuffer(3, mesh.emissive);
777
+ pass.setVertexBuffer(4, mesh.specular);
669
778
  pass.draw(mesh.count);
670
779
  });
671
780
  pass.end();
@@ -677,6 +786,8 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
677
786
  mesh.position.destroy?.();
678
787
  mesh.normal.destroy?.();
679
788
  mesh.color.destroy?.();
789
+ mesh.emissive.destroy?.();
790
+ mesh.specular.destroy?.();
680
791
  }
681
792
  this.cache.clear();
682
793
  this.uniformBuffer?.destroy?.();
@@ -759,9 +870,30 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
759
870
  depth += dot(d, d);
760
871
  }
761
872
  depth /= face.indices.length;
873
+ let surface;
874
+ const material = face.material;
875
+ if (material) {
876
+ if (material.specular) {
877
+ let cx = 0;
878
+ let cy = 0;
879
+ let cz = 0;
880
+ for (const i of face.indices) {
881
+ cx += world[i].x;
882
+ cy += world[i].y;
883
+ cz += world[i].z;
884
+ }
885
+ const inv = 1 / face.indices.length;
886
+ const viewDir = normalize(
887
+ subtract(frame.eye, { x: cx * inv, y: cy * inv, z: cz * inv })
888
+ );
889
+ surface = { material, viewDir };
890
+ } else {
891
+ surface = { material };
892
+ }
893
+ }
762
894
  polygons.push({
763
895
  points,
764
- color: shadeColor(normal, face.color, frame.light),
896
+ color: shadeColor(normal, face.color, frame.light, surface),
765
897
  depth,
766
898
  opacity: faceOpacity
767
899
  });
@@ -1512,6 +1644,7 @@ void main() {
1512
1644
  SpinAnimation: () => SpinAnimation,
1513
1645
  WebGLTexturedRenderer: () => WebGLTexturedRenderer,
1514
1646
  WebGPUTexturedRenderer: () => WebGPUTexturedRenderer,
1647
+ attachMaterial: () => attachMaterial,
1515
1648
  centerAndScaleMesh: () => centerAndScaleMesh,
1516
1649
  chargedOrb: () => chargedOrb,
1517
1650
  circleMotion: () => circleMotion,
@@ -1729,6 +1862,12 @@ void main() {
1729
1862
  init_math();
1730
1863
 
1731
1864
  // src/engines/little-3d-engine/core/mesh.ts
1865
+ function attachMaterial(mesh, material) {
1866
+ if (material) {
1867
+ for (const face of mesh.faces) face.material = material;
1868
+ }
1869
+ return mesh;
1870
+ }
1732
1871
  function transform(init) {
1733
1872
  return {
1734
1873
  position: init?.position ?? { x: 0, y: 0, z: 0 },
@@ -1743,7 +1882,7 @@ void main() {
1743
1882
 
1744
1883
  // src/engines/little-3d-engine/shapes/primitives/cube.ts
1745
1884
  var DEFAULT_COLORS = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
1746
- function cube(size = 1, colors = DEFAULT_COLORS) {
1885
+ function cube(size = 1, colors = DEFAULT_COLORS, material) {
1747
1886
  const h = size / 2;
1748
1887
  const vertices = [
1749
1888
  { x: -h, y: -h, z: h },
@@ -1763,12 +1902,12 @@ void main() {
1763
1902
  { indices: [1, 5, 6, 2], color: colors[4 % colors.length] },
1764
1903
  { indices: [4, 0, 3, 7], color: colors[5 % colors.length] }
1765
1904
  ];
1766
- return { vertices, faces };
1905
+ return attachMaterial({ vertices, faces }, material);
1767
1906
  }
1768
1907
 
1769
1908
  // src/engines/little-3d-engine/shapes/primitives/quad.ts
1770
1909
  var DEFAULT_COLORS2 = ["#3b82f6"];
1771
- function quad(size = 1, colors = DEFAULT_COLORS2) {
1910
+ function quad(size = 1, colors = DEFAULT_COLORS2, material) {
1772
1911
  const s = size / 2;
1773
1912
  const vertices = [
1774
1913
  { x: -s, y: -s, z: 0 },
@@ -1776,12 +1915,15 @@ void main() {
1776
1915
  { x: s, y: s, z: 0 },
1777
1916
  { x: -s, y: s, z: 0 }
1778
1917
  ];
1779
- return { vertices, faces: [{ indices: [0, 1, 2, 3], color: colors[0] }] };
1918
+ return attachMaterial(
1919
+ { vertices, faces: [{ indices: [0, 1, 2, 3], color: colors[0] }] },
1920
+ material
1921
+ );
1780
1922
  }
1781
1923
 
1782
1924
  // src/engines/little-3d-engine/shapes/primitives/tetrahedron.ts
1783
1925
  var DEFAULT_COLORS3 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b"];
1784
- function tetrahedron(size = 1, colors = DEFAULT_COLORS3) {
1926
+ function tetrahedron(size = 1, colors = DEFAULT_COLORS3, material) {
1785
1927
  const s = size / 2;
1786
1928
  const vertices = [
1787
1929
  { x: s, y: s, z: s },
@@ -1795,7 +1937,7 @@ void main() {
1795
1937
  { indices: [0, 2, 3], color: colors[2 % colors.length] },
1796
1938
  { indices: [1, 3, 2], color: colors[3 % colors.length] }
1797
1939
  ];
1798
- return { vertices, faces };
1940
+ return attachMaterial({ vertices, faces }, material);
1799
1941
  }
1800
1942
 
1801
1943
  // src/engines/little-3d-engine/shapes/primitives/octahedron.ts
@@ -1809,7 +1951,7 @@ void main() {
1809
1951
  "#06b6d4",
1810
1952
  "#eab308"
1811
1953
  ];
1812
- function octahedron(size = 1, colors = DEFAULT_COLORS4) {
1954
+ function octahedron(size = 1, colors = DEFAULT_COLORS4, material) {
1813
1955
  const r = size / 2;
1814
1956
  const vertices = [
1815
1957
  { x: r, y: 0, z: 0 },
@@ -1829,12 +1971,12 @@ void main() {
1829
1971
  { indices: [5, 3, 1], color: colors[6 % colors.length] },
1830
1972
  { indices: [5, 0, 3], color: colors[7 % colors.length] }
1831
1973
  ];
1832
- return { vertices, faces };
1974
+ return attachMaterial({ vertices, faces }, material);
1833
1975
  }
1834
1976
 
1835
1977
  // src/engines/little-3d-engine/shapes/primitives/pyramid.ts
1836
1978
  var DEFAULT_COLORS5 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981"];
1837
- function pyramid(size = 1, colors = DEFAULT_COLORS5) {
1979
+ function pyramid(size = 1, colors = DEFAULT_COLORS5, material) {
1838
1980
  const h = size / 2;
1839
1981
  const vertices = [
1840
1982
  { x: -h, y: -h, z: h },
@@ -1850,12 +1992,12 @@ void main() {
1850
1992
  { indices: [4, 2, 3], color: colors[3 % colors.length] },
1851
1993
  { indices: [4, 3, 0], color: colors[4 % colors.length] }
1852
1994
  ];
1853
- return { vertices, faces };
1995
+ return attachMaterial({ vertices, faces }, material);
1854
1996
  }
1855
1997
 
1856
1998
  // src/engines/little-3d-engine/shapes/primitives/spheres/uv-sphere.ts
1857
1999
  var DEFAULT_COLORS6 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
1858
- function uvSphere(size = 1, detail = 1, colors = DEFAULT_COLORS6) {
2000
+ function uvSphere(size = 1, detail = 1, colors = DEFAULT_COLORS6, material) {
1859
2001
  const r = size / 2;
1860
2002
  const d = Math.max(1, Math.floor(detail));
1861
2003
  const slices = Math.max(4, d * 4);
@@ -1894,7 +2036,7 @@ void main() {
1894
2036
  color: color()
1895
2037
  });
1896
2038
  }
1897
- return { vertices, faces };
2039
+ return attachMaterial({ vertices, faces }, material);
1898
2040
  }
1899
2041
 
1900
2042
  // src/engines/little-3d-engine/shapes/primitives/spheres/icosphere.ts
@@ -1937,8 +2079,11 @@ void main() {
1937
2079
  [8, 6, 7],
1938
2080
  [9, 8, 1]
1939
2081
  ];
1940
- function icosphere(size = 1, detail = 1, colors = DEFAULT_COLORS7) {
1941
- return sphereFromTriangles(SEED_VERTICES, SEED_FACES, size, detail, colors);
2082
+ function icosphere(size = 1, detail = 1, colors = DEFAULT_COLORS7, material) {
2083
+ return attachMaterial(
2084
+ sphereFromTriangles(SEED_VERTICES, SEED_FACES, size, detail, colors),
2085
+ material
2086
+ );
1942
2087
  }
1943
2088
 
1944
2089
  // src/engines/little-3d-engine/shapes/primitives/spheres/octa-sphere.ts
@@ -1962,8 +2107,11 @@ void main() {
1962
2107
  [5, 3, 1],
1963
2108
  [5, 0, 3]
1964
2109
  ];
1965
- function octaSphere(size = 1, detail = 1, colors = DEFAULT_COLORS8) {
1966
- return sphereFromTriangles(SEED_VERTICES2, SEED_FACES2, size, detail, colors);
2110
+ function octaSphere(size = 1, detail = 1, colors = DEFAULT_COLORS8, material) {
2111
+ return attachMaterial(
2112
+ sphereFromTriangles(SEED_VERTICES2, SEED_FACES2, size, detail, colors),
2113
+ material
2114
+ );
1967
2115
  }
1968
2116
 
1969
2117
  // src/engines/little-3d-engine/shapes/primitives/spheres/cube-sphere.ts
@@ -1976,7 +2124,7 @@ void main() {
1976
2124
  { normal: [0, 1, 0], right: [1, 0, 0], up: [0, 0, -1] },
1977
2125
  { normal: [0, -1, 0], right: [1, 0, 0], up: [0, 0, 1] }
1978
2126
  ];
1979
- function cubeSphere(size = 1, detail = 1, colors = DEFAULT_COLORS9) {
2127
+ function cubeSphere(size = 1, detail = 1, colors = DEFAULT_COLORS9, material) {
1980
2128
  const r = size / 2;
1981
2129
  const n = Math.max(1, Math.floor(detail));
1982
2130
  const vertices = [];
@@ -2005,13 +2153,13 @@ void main() {
2005
2153
  }
2006
2154
  }
2007
2155
  }
2008
- return { vertices, faces };
2156
+ return attachMaterial({ vertices, faces }, material);
2009
2157
  }
2010
2158
 
2011
2159
  // src/engines/little-3d-engine/shapes/complex/plane.ts
2012
2160
  var DEFAULT_COLORS10 = ["#e0f2fe", "#7dd3fc", "#38bdf8", "#f8fafc"];
2013
- function planeMesh(colors = DEFAULT_COLORS10) {
2014
- return {
2161
+ function planeMesh(colors = DEFAULT_COLORS10, material) {
2162
+ return attachMaterial({
2015
2163
  vertices: [
2016
2164
  { x: 0.9, y: 0, z: 0 },
2017
2165
  { x: -0.2, y: 0, z: 0.82 },
@@ -2039,7 +2187,7 @@ void main() {
2039
2187
  { indices: [3, 6, 8], color: colors[0] ?? DEFAULT_COLORS10[0] },
2040
2188
  { indices: [3, 8, 6], color: colors[1] ?? DEFAULT_COLORS10[1] }
2041
2189
  ]
2042
- };
2190
+ }, material);
2043
2191
  }
2044
2192
 
2045
2193
  // src/engines/little-3d-engine/textures/dynamic/canvas-texture.ts
@@ -2596,10 +2744,14 @@ void main() {
2596
2744
  const pick = Array.isArray(color) ? (i) => color[i % color.length] : () => color;
2597
2745
  return { vertices: mesh.vertices, faces: mesh.faces.map((f, i) => ({ ...f, color: pick(i) })) };
2598
2746
  }
2747
+ function applyMaterial(mesh, material) {
2748
+ if (!material) return mesh;
2749
+ return { vertices: mesh.vertices, faces: mesh.faces.map((f) => ({ ...f, material })) };
2750
+ }
2599
2751
  var SpinAnimation = class {
2600
2752
  constructor(options = {}) {
2601
2753
  this.exited = false;
2602
- this.mesh = applyColor(resolveMesh(options.shape), options.color);
2754
+ this.mesh = applyMaterial(applyColor(resolveMesh(options.shape), options.color), options.material);
2603
2755
  this.spinX = options.spinX ?? 7e-4;
2604
2756
  this.spinY = options.spinY ?? 11e-4;
2605
2757
  this.backend = options.backend;
@@ -3292,6 +3444,7 @@ void main() {
3292
3444
  var CENTER_POP_OUT_AT = REENTER_MS + (MINIS - 1) * REENTER_STAGGER_MS + 150;
3293
3445
  var CENTER_POP_OUT_MS = 420;
3294
3446
  var PARKED = { x: 0, y: 0, z: 50 };
3447
+ var ORB_MATERIAL = { specular: [1, 1, 1], shininess: 28 };
3295
3448
  var CENTER_COLORS = ["#67e8f9", "#22d3ee", "#0ea5e9", "#38bdf8", "#7dd3fc"];
3296
3449
  var MINI_COLORS = [
3297
3450
  ["#e0f2fe", "#bae6fd", "#7dd3fc"],
@@ -3320,9 +3473,9 @@ void main() {
3320
3473
  backend: this.backend,
3321
3474
  camera: { position: { x: 0, y: 0, z: CAMERA_Z } }
3322
3475
  });
3323
- this.center = engine.add(icosphere(1, 2, CENTER_COLORS), { scale: 0 });
3476
+ this.center = engine.add(icosphere(1, 2, CENTER_COLORS, ORB_MATERIAL), { scale: 0 });
3324
3477
  for (let i = 0; i < MINIS; i++) {
3325
- const mesh = icosphere(1, 1, MINI_COLORS[i % MINI_COLORS.length]);
3478
+ const mesh = icosphere(1, 1, MINI_COLORS[i % MINI_COLORS.length], ORB_MATERIAL);
3326
3479
  this.minis.push(engine.add(mesh, { scale: 0, transparency: { ...MINI_TRANSPARENCY } }));
3327
3480
  }
3328
3481
  this.engine = engine;
@@ -3907,6 +4060,7 @@ void main() {
3907
4060
  var TRAIL_OUTRO_MS = 1200;
3908
4061
  var TRANSPARENCY = { mode: "two-sided", opacity: 0.275 };
3909
4062
  var CAR_COLORS = ["#bae6fd", "#7dd3fc", "#38bdf8", "#0ea5e9", "#a5f3fc", "#e0f2fe"];
4063
+ var CAR_MATERIAL = { emissive: [0.1, 0.2, 0.3] };
3910
4064
  var WORLD_UP2 = { x: 0, y: 1, z: 0 };
3911
4065
  function clamp016(value) {
3912
4066
  return Math.max(0, Math.min(1, value));
@@ -3967,7 +4121,7 @@ void main() {
3967
4121
  backend: this.backend,
3968
4122
  camera: { position: { x: 0, y: 0, z: CAMERA_Z3 }, fov: FOV2 }
3969
4123
  });
3970
- const mesh = cube(1, CAR_COLORS);
4124
+ const mesh = cube(1, CAR_COLORS, CAR_MATERIAL);
3971
4125
  for (let i = 0; i < MAX_CARS; i++) {
3972
4126
  this.cars.push(engine.add(mesh, { scale: 0, transparency: { ...TRANSPARENCY } }));
3973
4127
  }
@@ -4653,28 +4807,64 @@ void main() {
4653
4807
 
4654
4808
  // src/engines/little-3d-engine/loaders/obj.ts
4655
4809
  var DEFAULT_COLORS12 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
4810
+ function clamp018(value) {
4811
+ return Math.min(1, Math.max(0, value));
4812
+ }
4656
4813
  function channelToHex(value) {
4657
4814
  const channel = Number.parseFloat(value);
4658
4815
  if (!Number.isFinite(channel)) return void 0;
4659
- return Math.round(Math.min(1, Math.max(0, channel)) * 255).toString(16).padStart(2, "0");
4660
- }
4661
- function parseMtlColors(text) {
4662
- const colors = /* @__PURE__ */ new Map();
4663
- let material;
4816
+ return Math.round(clamp018(channel) * 255).toString(16).padStart(2, "0");
4817
+ }
4818
+ function parseRgb(parts) {
4819
+ const channels = parts.slice(1, 4).map(Number.parseFloat);
4820
+ if (channels.length !== 3 || !channels.every(Number.isFinite)) return void 0;
4821
+ return [clamp018(channels[0]), clamp018(channels[1]), clamp018(channels[2])];
4822
+ }
4823
+ function toMaterial(surface) {
4824
+ const material = {};
4825
+ if (surface.specular) material.specular = surface.specular;
4826
+ if (surface.shininess !== void 0) material.shininess = surface.shininess;
4827
+ if (surface.emissive) material.emissive = surface.emissive;
4828
+ return Object.keys(material).length > 0 ? material : void 0;
4829
+ }
4830
+ function parseMtl(text) {
4831
+ const materials = /* @__PURE__ */ new Map();
4832
+ const surfaces = /* @__PURE__ */ new Map();
4833
+ let name;
4664
4834
  for (const line of text.split("\n")) {
4665
4835
  const trimmed = line.trim();
4666
4836
  if (trimmed === "" || trimmed.startsWith("#")) continue;
4667
4837
  const parts = trimmed.split(/\s+/);
4668
- if (parts[0] === "newmtl") {
4669
- material = parts.slice(1).join(" ");
4670
- } else if (parts[0] === "Kd" && material) {
4838
+ const keyword = parts[0];
4839
+ if (keyword === "newmtl") {
4840
+ name = parts.slice(1).join(" ");
4841
+ if (name && !materials.has(name)) {
4842
+ materials.set(name, {});
4843
+ surfaces.set(name, {});
4844
+ }
4845
+ continue;
4846
+ }
4847
+ if (!name) continue;
4848
+ const entry = materials.get(name);
4849
+ const surface = surfaces.get(name);
4850
+ if (keyword === "Kd") {
4671
4851
  const channels = parts.slice(1, 4).map(channelToHex);
4672
4852
  if (channels.length === 3 && channels.every((channel) => channel !== void 0)) {
4673
- colors.set(material, `#${channels.join("")}`);
4853
+ entry.color = `#${channels.join("")}`;
4674
4854
  }
4855
+ } else if (keyword === "Ks") {
4856
+ surface.specular = parseRgb(parts);
4857
+ } else if (keyword === "Ns") {
4858
+ const ns = Number.parseFloat(parts[1]);
4859
+ if (Number.isFinite(ns)) surface.shininess = Math.max(0, ns);
4860
+ } else if (keyword === "Ke") {
4861
+ surface.emissive = parseRgb(parts);
4675
4862
  }
4676
4863
  }
4677
- return colors;
4864
+ for (const [key, surface] of surfaces) {
4865
+ materials.get(key).material = toMaterial(surface);
4866
+ }
4867
+ return materials;
4678
4868
  }
4679
4869
  function resolveIndex(token, vertexCount) {
4680
4870
  const n = parseInt(token, 10);
@@ -4682,7 +4872,7 @@ void main() {
4682
4872
  }
4683
4873
  function parseObj(text, options = {}) {
4684
4874
  const colors = options.colors ?? DEFAULT_COLORS12;
4685
- const materialColors = options.useMtlColors && options.mtl ? parseMtlColors(options.mtl) : void 0;
4875
+ const materials = options.useMtlColors && options.mtl ? parseMtl(options.mtl) : void 0;
4686
4876
  const vertices = [];
4687
4877
  const faces = [];
4688
4878
  let material;
@@ -4706,9 +4896,11 @@ void main() {
4706
4896
  indices.push(resolveIndex(vertexToken, vertices.length));
4707
4897
  }
4708
4898
  if (indices.length >= 3) {
4709
- const mtlColor = material ? materialColors?.get(material) : void 0;
4710
- const color = mtlColor ?? (materialColors ? colors[0] ?? "#888888" : colors[faces.length % colors.length]);
4711
- faces.push({ indices, color });
4899
+ const entry = material ? materials?.get(material) : void 0;
4900
+ const color = entry?.color ?? (materials ? colors[0] ?? "#888888" : colors[faces.length % colors.length]);
4901
+ const face = { indices, color };
4902
+ if (entry?.material) face.material = entry.material;
4903
+ faces.push(face);
4712
4904
  }
4713
4905
  }
4714
4906
  }