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
@@ -136,7 +136,10 @@ function expandToTriangles(mesh) {
136
136
  const positions = new Float32Array(triangles * 9);
137
137
  const normals = new Float32Array(triangles * 9);
138
138
  const colors = new Float32Array(triangles * 9);
139
+ const emissives = new Float32Array(triangles * 9);
140
+ const speculars = new Float32Array(triangles * 12);
139
141
  let o = 0;
142
+ let so = 0;
140
143
  for (const face of mesh.faces) {
141
144
  const v0 = mesh.vertices[face.indices[0]];
142
145
  const v1 = mesh.vertices[face.indices[1]];
@@ -146,6 +149,15 @@ function expandToTriangles(mesh) {
146
149
  const cr = r / 255;
147
150
  const cg = g / 255;
148
151
  const cb = b / 255;
152
+ const emissive = face.material?.emissive;
153
+ const er = emissive ? emissive[0] : 0;
154
+ const eg = emissive ? emissive[1] : 0;
155
+ const eb = emissive ? emissive[2] : 0;
156
+ const specular = face.material?.specular;
157
+ const sr = specular ? specular[0] : 0;
158
+ const sg = specular ? specular[1] : 0;
159
+ const sb = specular ? specular[2] : 0;
160
+ const sn = specular ? face.material?.shininess ?? 32 : 1;
149
161
  for (let k = 1; k < face.indices.length - 1; k++) {
150
162
  const tri = [face.indices[0], face.indices[k], face.indices[k + 1]];
151
163
  for (const index of tri) {
@@ -159,11 +171,19 @@ function expandToTriangles(mesh) {
159
171
  colors[o] = cr;
160
172
  colors[o + 1] = cg;
161
173
  colors[o + 2] = cb;
174
+ emissives[o] = er;
175
+ emissives[o + 1] = eg;
176
+ emissives[o + 2] = eb;
177
+ speculars[so] = sr;
178
+ speculars[so + 1] = sg;
179
+ speculars[so + 2] = sb;
180
+ speculars[so + 3] = sn;
162
181
  o += 3;
182
+ so += 4;
163
183
  }
164
184
  }
165
185
  }
166
- return { positions, normals, colors, count: positions.length / 3 };
186
+ return { positions, normals, colors, emissives, speculars, count: positions.length / 3 };
167
187
  }
168
188
  function midpoint(a, b) {
169
189
  return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, z: (a.z + b.z) / 2 };
@@ -201,13 +221,42 @@ var init_geometry = __esm({
201
221
  function clamp01(value) {
202
222
  return Math.min(1, Math.max(0, value));
203
223
  }
204
- function shadeColor(normal, color, light) {
224
+ function clamp255(value) {
225
+ return Math.round(Math.min(255, Math.max(0, value)));
226
+ }
227
+ function shade(normal, color, light, surface) {
205
228
  const lambert = Math.max(0, dot(normal, light.toLight));
206
229
  const brightness = clamp01(light.ambient + light.intensity * lambert);
207
- const [r, g, b] = parseColor(color);
208
- return `rgb(${Math.round(r * brightness)}, ${Math.round(g * brightness)}, ${Math.round(
209
- b * brightness
210
- )})`;
230
+ const [baseR, baseG, baseB] = parseColor(color);
231
+ let r = baseR * brightness;
232
+ let g = baseG * brightness;
233
+ let b = baseB * brightness;
234
+ const material = surface?.material;
235
+ const specular = material?.specular;
236
+ const viewDir = surface?.viewDir;
237
+ if (specular && viewDir && lambert > 0) {
238
+ const half = normalize({
239
+ x: light.toLight.x + viewDir.x,
240
+ y: light.toLight.y + viewDir.y,
241
+ z: light.toLight.z + viewDir.z
242
+ });
243
+ const shininess = material?.shininess ?? 32;
244
+ const highlight = Math.pow(Math.max(0, dot(normal, half)), shininess) * light.intensity * 255;
245
+ r += highlight * specular[0];
246
+ g += highlight * specular[1];
247
+ b += highlight * specular[2];
248
+ }
249
+ const emissive = material?.emissive;
250
+ if (emissive) {
251
+ r += emissive[0] * 255;
252
+ g += emissive[1] * 255;
253
+ b += emissive[2] * 255;
254
+ }
255
+ return [clamp255(r), clamp255(g), clamp255(b)];
256
+ }
257
+ function shadeColor(normal, color, light, surface) {
258
+ const [r, g, b] = shade(normal, color, light, surface);
259
+ return `rgb(${r}, ${g}, ${b})`;
211
260
  }
212
261
  var DEFAULTS2, Light;
213
262
  var init_light = __esm({
@@ -230,8 +279,8 @@ var init_light = __esm({
230
279
  };
231
280
  }
232
281
  /** Convenience wrapper around {@link shadeColor} using this light. */
233
- shade(normal, color) {
234
- return shadeColor(normal, color, this.params);
282
+ shade(normal, color, surface) {
283
+ return shadeColor(normal, color, this.params, surface);
235
284
  }
236
285
  };
237
286
  }
@@ -271,28 +320,51 @@ var init_webgl = __esm({
271
320
  in vec3 aPos;
272
321
  in vec3 aNormal;
273
322
  in vec3 aColor;
323
+ in vec3 aEmissive;
324
+ in vec4 aSpecular;
274
325
  uniform mat4 uViewProj;
275
326
  uniform mat4 uModel;
276
327
  out vec3 vNormal;
277
328
  out vec3 vColor;
329
+ out vec3 vEmissive;
330
+ out vec4 vSpecular;
331
+ out vec3 vWorldPos;
278
332
  void main() {
279
333
  vNormal = mat3(uModel) * aNormal;
280
334
  vColor = aColor;
281
- gl_Position = uViewProj * uModel * vec4(aPos, 1.0);
335
+ vEmissive = aEmissive;
336
+ vSpecular = aSpecular;
337
+ vec4 world = uModel * vec4(aPos, 1.0);
338
+ vWorldPos = world.xyz;
339
+ gl_Position = uViewProj * world;
282
340
  }`;
283
341
  FRAGMENT_SHADER = `#version 300 es
284
342
  precision mediump float;
285
343
  in vec3 vNormal;
286
344
  in vec3 vColor;
345
+ in vec3 vEmissive;
346
+ in vec4 vSpecular;
347
+ in vec3 vWorldPos;
287
348
  uniform vec3 uToLight;
349
+ uniform vec3 uEye;
288
350
  uniform float uIntensity;
289
351
  uniform float uAmbient;
290
352
  uniform float uOpacity;
291
353
  out vec4 fragColor;
292
354
  void main() {
293
- float lambert = max(dot(normalize(vNormal), normalize(uToLight)), 0.0);
355
+ vec3 normal = normalize(vNormal);
356
+ vec3 toLight = normalize(uToLight);
357
+ float lambert = max(dot(normal, toLight), 0.0);
294
358
  float brightness = clamp(uAmbient + uIntensity * lambert, 0.0, 1.0);
295
- fragColor = vec4(vColor * brightness, uOpacity);
359
+ vec3 lit = vColor * brightness;
360
+ if (lambert > 0.0) {
361
+ vec3 viewDir = normalize(uEye - vWorldPos);
362
+ vec3 halfVec = normalize(toLight + viewDir);
363
+ float highlight = pow(max(dot(normal, halfVec), 0.0), vSpecular.w) * uIntensity;
364
+ lit += highlight * vSpecular.xyz;
365
+ }
366
+ lit += vEmissive;
367
+ fragColor = vec4(lit, uOpacity);
296
368
  }`;
297
369
  WebGLRenderer = class {
298
370
  constructor(options = {}) {
@@ -313,9 +385,12 @@ void main() {
313
385
  aPos: gl.getAttribLocation(this.program, "aPos"),
314
386
  aNormal: gl.getAttribLocation(this.program, "aNormal"),
315
387
  aColor: gl.getAttribLocation(this.program, "aColor"),
388
+ aEmissive: gl.getAttribLocation(this.program, "aEmissive"),
389
+ aSpecular: gl.getAttribLocation(this.program, "aSpecular"),
316
390
  uViewProj: gl.getUniformLocation(this.program, "uViewProj"),
317
391
  uModel: gl.getUniformLocation(this.program, "uModel"),
318
392
  uToLight: gl.getUniformLocation(this.program, "uToLight"),
393
+ uEye: gl.getUniformLocation(this.program, "uEye"),
319
394
  uIntensity: gl.getUniformLocation(this.program, "uIntensity"),
320
395
  uAmbient: gl.getUniformLocation(this.program, "uAmbient"),
321
396
  uOpacity: gl.getUniformLocation(this.program, "uOpacity")
@@ -339,19 +414,23 @@ void main() {
339
414
  const data = expandToTriangles(mesh);
340
415
  const vao = gl.createVertexArray();
341
416
  gl.bindVertexArray(vao);
342
- const attribute = (location, array) => {
417
+ const buffers = [];
418
+ const attribute = (location, array, size = 3) => {
343
419
  if (location < 0) return;
344
420
  const buffer = gl.createBuffer();
421
+ buffers.push(buffer);
345
422
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
346
423
  gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW);
347
424
  gl.enableVertexAttribArray(location);
348
- gl.vertexAttribPointer(location, 3, gl.FLOAT, false, 0, 0);
425
+ gl.vertexAttribPointer(location, size, gl.FLOAT, false, 0, 0);
349
426
  };
350
427
  attribute(loc.aPos, data.positions);
351
428
  attribute(loc.aNormal, data.normals);
352
429
  attribute(loc.aColor, data.colors);
430
+ attribute(loc.aEmissive, data.emissives);
431
+ attribute(loc.aSpecular, data.speculars, 4);
353
432
  gl.bindVertexArray(null);
354
- const result = { vao, count: data.count };
433
+ const result = { vao, buffers, count: data.count };
355
434
  this.cache.set(mesh, result);
356
435
  return result;
357
436
  }
@@ -364,6 +443,7 @@ void main() {
364
443
  gl.useProgram(this.program);
365
444
  gl.uniformMatrix4fv(loc.uViewProj, false, new Float32Array(frame.viewProjection));
366
445
  gl.uniform3f(loc.uToLight, frame.light.toLight.x, frame.light.toLight.y, frame.light.toLight.z);
446
+ gl.uniform3f(loc.uEye, frame.eye.x, frame.eye.y, frame.eye.z);
367
447
  gl.uniform1f(loc.uIntensity, frame.light.intensity);
368
448
  gl.uniform1f(loc.uAmbient, frame.light.ambient);
369
449
  gl.disable(gl.BLEND);
@@ -408,7 +488,10 @@ void main() {
408
488
  destroy() {
409
489
  const gl = this.gl;
410
490
  if (gl) {
411
- for (const mesh of this.cache.values()) gl.deleteVertexArray(mesh.vao);
491
+ for (const mesh of this.cache.values()) {
492
+ gl.deleteVertexArray(mesh.vao);
493
+ for (const buffer of mesh.buffers) gl.deleteBuffer(buffer);
494
+ }
412
495
  if (this.program) gl.deleteProgram(this.program);
413
496
  }
414
497
  this.cache.clear();
@@ -438,6 +521,7 @@ struct Uniforms {
438
521
  model: mat4x4<f32>,
439
522
  toLight: vec4<f32>,
440
523
  params: vec4<f32>,
524
+ eye: vec4<f32>,
441
525
  };
442
526
  @group(0) @binding(0) var<uniform> u: Uniforms;
443
527
 
@@ -445,23 +529,40 @@ struct VSOut {
445
529
  @builtin(position) position: vec4<f32>,
446
530
  @location(0) normal: vec3<f32>,
447
531
  @location(1) color: vec3<f32>,
532
+ @location(2) emissive: vec3<f32>,
533
+ @location(3) specular: vec4<f32>,
534
+ @location(4) worldPos: vec3<f32>,
448
535
  };
449
536
 
450
537
  @vertex
451
- fn vs(@location(0) pos: vec3<f32>, @location(1) normal: vec3<f32>, @location(2) color: vec3<f32>) -> VSOut {
538
+ 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 {
452
539
  var out: VSOut;
453
540
  let m = mat3x3<f32>(u.model[0].xyz, u.model[1].xyz, u.model[2].xyz);
454
541
  out.normal = m * normal;
455
542
  out.color = color;
456
- out.position = u.viewProj * u.model * vec4<f32>(pos, 1.0);
543
+ out.emissive = emissive;
544
+ out.specular = specular;
545
+ let world = u.model * vec4<f32>(pos, 1.0);
546
+ out.worldPos = world.xyz;
547
+ out.position = u.viewProj * world;
457
548
  return out;
458
549
  }
459
550
 
460
551
  @fragment
461
552
  fn fs(in: VSOut) -> @location(0) vec4<f32> {
462
- let lambert = max(dot(normalize(in.normal), normalize(u.toLight.xyz)), 0.0);
553
+ let normal = normalize(in.normal);
554
+ let toLight = normalize(u.toLight.xyz);
555
+ let lambert = max(dot(normal, toLight), 0.0);
463
556
  let brightness = clamp(u.params.y + u.params.x * lambert, 0.0, 1.0);
464
- return vec4<f32>(in.color * brightness, u.params.z);
557
+ var lit = in.color * brightness;
558
+ if (lambert > 0.0) {
559
+ let viewDir = normalize(u.eye.xyz - in.worldPos);
560
+ let halfVec = normalize(toLight + viewDir);
561
+ let highlight = pow(max(dot(normal, halfVec), 0.0), in.specular.w) * u.params.x;
562
+ lit = lit + highlight * in.specular.xyz;
563
+ }
564
+ lit = lit + in.emissive;
565
+ return vec4<f32>(lit, u.params.z);
465
566
  }
466
567
  `;
467
568
  CLIP_Z_FIX = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 1];
@@ -502,13 +603,15 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
502
603
  {
503
604
  binding: 0,
504
605
  visibility: stage.VERTEX | stage.FRAGMENT,
505
- buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: 160 }
606
+ buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: 176 }
506
607
  }
507
608
  ]
508
609
  });
509
- const vertexBuffer = (location) => ({
510
- arrayStride: 12,
511
- attributes: [{ shaderLocation: location, offset: 0, format: "float32x3" }]
610
+ const vertexBuffer = (location, components = 3) => ({
611
+ arrayStride: components * 4,
612
+ attributes: [
613
+ { shaderLocation: location, offset: 0, format: `float32x${components}` }
614
+ ]
512
615
  });
513
616
  const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [layout] });
514
617
  const blend = {
@@ -524,7 +627,13 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
524
627
  vertex: {
525
628
  module: module2,
526
629
  entryPoint: "vs",
527
- buffers: [vertexBuffer(0), vertexBuffer(1), vertexBuffer(2)]
630
+ buffers: [
631
+ vertexBuffer(0),
632
+ vertexBuffer(1),
633
+ vertexBuffer(2),
634
+ vertexBuffer(3),
635
+ vertexBuffer(4, 4)
636
+ ]
528
637
  },
529
638
  fragment: {
530
639
  module: module2,
@@ -577,6 +686,8 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
577
686
  position: upload(data.positions),
578
687
  normal: upload(data.normals),
579
688
  color: upload(data.colors),
689
+ emissive: upload(data.emissives),
690
+ specular: upload(data.speculars),
580
691
  count: data.count
581
692
  };
582
693
  this.cache.set(mesh, result);
@@ -628,7 +739,7 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
628
739
  const bindGroup = this.device.createBindGroup({
629
740
  layout,
630
741
  entries: [
631
- { binding: 0, resource: { buffer: this.uniformBuffer, offset: 0, size: 160 } }
742
+ { binding: 0, resource: { buffer: this.uniformBuffer, offset: 0, size: 176 } }
632
743
  ]
633
744
  });
634
745
  draws.forEach((draw, i) => {
@@ -637,6 +748,7 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
637
748
  data.set(draw.item.model, 16);
638
749
  data.set([frame.light.toLight.x, frame.light.toLight.y, frame.light.toLight.z, 0], 32);
639
750
  data.set([frame.light.intensity, frame.light.ambient, draw.opacity, 0], 36);
751
+ data.set([frame.eye.x, frame.eye.y, frame.eye.z, 0], 40);
640
752
  this.device.queue.writeBuffer(this.uniformBuffer, i * UNIFORM_STRIDE, data);
641
753
  });
642
754
  const encoder = this.device.createCommandEncoder();
@@ -663,6 +775,8 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
663
775
  pass.setVertexBuffer(0, mesh.position);
664
776
  pass.setVertexBuffer(1, mesh.normal);
665
777
  pass.setVertexBuffer(2, mesh.color);
778
+ pass.setVertexBuffer(3, mesh.emissive);
779
+ pass.setVertexBuffer(4, mesh.specular);
666
780
  pass.draw(mesh.count);
667
781
  });
668
782
  pass.end();
@@ -674,6 +788,8 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
674
788
  mesh.position.destroy?.();
675
789
  mesh.normal.destroy?.();
676
790
  mesh.color.destroy?.();
791
+ mesh.emissive.destroy?.();
792
+ mesh.specular.destroy?.();
677
793
  }
678
794
  this.cache.clear();
679
795
  this.uniformBuffer?.destroy?.();
@@ -756,9 +872,30 @@ var init_canvas2d = __esm({
756
872
  depth += dot(d, d);
757
873
  }
758
874
  depth /= face.indices.length;
875
+ let surface;
876
+ const material = face.material;
877
+ if (material) {
878
+ if (material.specular) {
879
+ let cx = 0;
880
+ let cy = 0;
881
+ let cz = 0;
882
+ for (const i of face.indices) {
883
+ cx += world[i].x;
884
+ cy += world[i].y;
885
+ cz += world[i].z;
886
+ }
887
+ const inv = 1 / face.indices.length;
888
+ const viewDir = normalize(
889
+ subtract(frame.eye, { x: cx * inv, y: cy * inv, z: cz * inv })
890
+ );
891
+ surface = { material, viewDir };
892
+ } else {
893
+ surface = { material };
894
+ }
895
+ }
759
896
  polygons.push({
760
897
  points,
761
- color: shadeColor(normal, face.color, frame.light),
898
+ color: shadeColor(normal, face.color, frame.light, surface),
762
899
  depth,
763
900
  opacity: faceOpacity
764
901
  });
@@ -1296,9 +1433,11 @@ void main() {
1296
1433
  const data = expandToTriangles(mesh);
1297
1434
  const vao = gl.createVertexArray();
1298
1435
  gl.bindVertexArray(vao);
1436
+ const buffers = [];
1299
1437
  const attribute = (location, array, size) => {
1300
1438
  if (location < 0) return;
1301
1439
  const buffer = gl.createBuffer();
1440
+ buffers.push(buffer);
1302
1441
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
1303
1442
  gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW);
1304
1443
  gl.enableVertexAttribArray(location);
@@ -1308,7 +1447,7 @@ void main() {
1308
1447
  attribute(loc.aColor, data.colors, 3);
1309
1448
  attribute(loc.aUV, planarUVs(mesh), 2);
1310
1449
  gl.bindVertexArray(null);
1311
- const result = { vao, count: data.count };
1450
+ const result = { vao, buffers, count: data.count };
1312
1451
  this.buffers.set(mesh, result);
1313
1452
  return result;
1314
1453
  }
@@ -1346,7 +1485,10 @@ void main() {
1346
1485
  const gl = this.gl;
1347
1486
  if (gl) {
1348
1487
  for (const texture of this.textures.values()) gl.deleteTexture(texture);
1349
- for (const buffers of this.buffers.values()) gl.deleteVertexArray(buffers.vao);
1488
+ for (const cached of this.buffers.values()) {
1489
+ gl.deleteVertexArray(cached.vao);
1490
+ for (const buffer of cached.buffers) gl.deleteBuffer(buffer);
1491
+ }
1350
1492
  if (this.program) gl.deleteProgram(this.program);
1351
1493
  }
1352
1494
  this.textures.clear();
@@ -1624,6 +1766,12 @@ init_light();
1624
1766
  init_math();
1625
1767
 
1626
1768
  // src/engines/little-3d-engine/core/mesh.ts
1769
+ function attachMaterial(mesh, material) {
1770
+ if (material) {
1771
+ for (const face of mesh.faces) face.material = material;
1772
+ }
1773
+ return mesh;
1774
+ }
1627
1775
  function transform(init) {
1628
1776
  return {
1629
1777
  position: init?.position ?? { x: 0, y: 0, z: 0 },
@@ -1638,7 +1786,7 @@ init_light();
1638
1786
 
1639
1787
  // src/engines/little-3d-engine/shapes/primitives/cube.ts
1640
1788
  var DEFAULT_COLORS = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
1641
- function cube(size = 1, colors = DEFAULT_COLORS) {
1789
+ function cube(size = 1, colors = DEFAULT_COLORS, material) {
1642
1790
  const h = size / 2;
1643
1791
  const vertices = [
1644
1792
  { x: -h, y: -h, z: h },
@@ -1658,12 +1806,12 @@ function cube(size = 1, colors = DEFAULT_COLORS) {
1658
1806
  { indices: [1, 5, 6, 2], color: colors[4 % colors.length] },
1659
1807
  { indices: [4, 0, 3, 7], color: colors[5 % colors.length] }
1660
1808
  ];
1661
- return { vertices, faces };
1809
+ return attachMaterial({ vertices, faces }, material);
1662
1810
  }
1663
1811
 
1664
1812
  // src/engines/little-3d-engine/shapes/primitives/quad.ts
1665
1813
  var DEFAULT_COLORS2 = ["#3b82f6"];
1666
- function quad(size = 1, colors = DEFAULT_COLORS2) {
1814
+ function quad(size = 1, colors = DEFAULT_COLORS2, material) {
1667
1815
  const s = size / 2;
1668
1816
  const vertices = [
1669
1817
  { x: -s, y: -s, z: 0 },
@@ -1671,12 +1819,15 @@ function quad(size = 1, colors = DEFAULT_COLORS2) {
1671
1819
  { x: s, y: s, z: 0 },
1672
1820
  { x: -s, y: s, z: 0 }
1673
1821
  ];
1674
- return { vertices, faces: [{ indices: [0, 1, 2, 3], color: colors[0] }] };
1822
+ return attachMaterial(
1823
+ { vertices, faces: [{ indices: [0, 1, 2, 3], color: colors[0] }] },
1824
+ material
1825
+ );
1675
1826
  }
1676
1827
 
1677
1828
  // src/engines/little-3d-engine/shapes/primitives/tetrahedron.ts
1678
1829
  var DEFAULT_COLORS3 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b"];
1679
- function tetrahedron(size = 1, colors = DEFAULT_COLORS3) {
1830
+ function tetrahedron(size = 1, colors = DEFAULT_COLORS3, material) {
1680
1831
  const s = size / 2;
1681
1832
  const vertices = [
1682
1833
  { x: s, y: s, z: s },
@@ -1690,12 +1841,12 @@ function tetrahedron(size = 1, colors = DEFAULT_COLORS3) {
1690
1841
  { indices: [0, 2, 3], color: colors[2 % colors.length] },
1691
1842
  { indices: [1, 3, 2], color: colors[3 % colors.length] }
1692
1843
  ];
1693
- return { vertices, faces };
1844
+ return attachMaterial({ vertices, faces }, material);
1694
1845
  }
1695
1846
 
1696
1847
  // src/engines/little-3d-engine/shapes/primitives/pyramid.ts
1697
1848
  var DEFAULT_COLORS4 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981"];
1698
- function pyramid(size = 1, colors = DEFAULT_COLORS4) {
1849
+ function pyramid(size = 1, colors = DEFAULT_COLORS4, material) {
1699
1850
  const h = size / 2;
1700
1851
  const vertices = [
1701
1852
  { x: -h, y: -h, z: h },
@@ -1711,7 +1862,7 @@ function pyramid(size = 1, colors = DEFAULT_COLORS4) {
1711
1862
  { indices: [4, 2, 3], color: colors[3 % colors.length] },
1712
1863
  { indices: [4, 3, 0], color: colors[4 % colors.length] }
1713
1864
  ];
1714
- return { vertices, faces };
1865
+ return attachMaterial({ vertices, faces }, material);
1715
1866
  }
1716
1867
 
1717
1868
  // src/engines/little-3d-engine/shapes/primitives/spheres/icosphere.ts
@@ -1754,8 +1905,11 @@ var SEED_FACES = [
1754
1905
  [8, 6, 7],
1755
1906
  [9, 8, 1]
1756
1907
  ];
1757
- function icosphere(size = 1, detail = 1, colors = DEFAULT_COLORS5) {
1758
- return sphereFromTriangles(SEED_VERTICES, SEED_FACES, size, detail, colors);
1908
+ function icosphere(size = 1, detail = 1, colors = DEFAULT_COLORS5, material) {
1909
+ return attachMaterial(
1910
+ sphereFromTriangles(SEED_VERTICES, SEED_FACES, size, detail, colors),
1911
+ material
1912
+ );
1759
1913
  }
1760
1914
 
1761
1915
  // src/engines/little-3d-engine/shapes/primitives/spheres/octa-sphere.ts
@@ -1763,8 +1917,8 @@ init_geometry();
1763
1917
 
1764
1918
  // src/engines/little-3d-engine/shapes/complex/plane.ts
1765
1919
  var DEFAULT_COLORS6 = ["#e0f2fe", "#7dd3fc", "#38bdf8", "#f8fafc"];
1766
- function planeMesh(colors = DEFAULT_COLORS6) {
1767
- return {
1920
+ function planeMesh(colors = DEFAULT_COLORS6, material) {
1921
+ return attachMaterial({
1768
1922
  vertices: [
1769
1923
  { x: 0.9, y: 0, z: 0 },
1770
1924
  { x: -0.2, y: 0, z: 0.82 },
@@ -1792,7 +1946,7 @@ function planeMesh(colors = DEFAULT_COLORS6) {
1792
1946
  { indices: [3, 6, 8], color: colors[0] ?? DEFAULT_COLORS6[0] },
1793
1947
  { indices: [3, 8, 6], color: colors[1] ?? DEFAULT_COLORS6[1] }
1794
1948
  ]
1795
- };
1949
+ }, material);
1796
1950
  }
1797
1951
 
1798
1952
  // src/engines/little-3d-engine/textures/dynamic/canvas-texture.ts
@@ -2252,6 +2406,7 @@ var REENTER_STAGGER_MS = 45;
2252
2406
  var CENTER_POP_OUT_AT = REENTER_MS + (MINIS - 1) * REENTER_STAGGER_MS + 150;
2253
2407
  var CENTER_POP_OUT_MS = 420;
2254
2408
  var PARKED = { x: 0, y: 0, z: 50 };
2409
+ var ORB_MATERIAL = { specular: [1, 1, 1], shininess: 28 };
2255
2410
  var CENTER_COLORS = ["#67e8f9", "#22d3ee", "#0ea5e9", "#38bdf8", "#7dd3fc"];
2256
2411
  var MINI_COLORS = [
2257
2412
  ["#e0f2fe", "#bae6fd", "#7dd3fc"],
@@ -2280,9 +2435,9 @@ var ChargedOrbAnimation = class {
2280
2435
  backend: this.backend,
2281
2436
  camera: { position: { x: 0, y: 0, z: CAMERA_Z } }
2282
2437
  });
2283
- this.center = engine.add(icosphere(1, 2, CENTER_COLORS), { scale: 0 });
2438
+ this.center = engine.add(icosphere(1, 2, CENTER_COLORS, ORB_MATERIAL), { scale: 0 });
2284
2439
  for (let i = 0; i < MINIS; i++) {
2285
- const mesh = icosphere(1, 1, MINI_COLORS[i % MINI_COLORS.length]);
2440
+ const mesh = icosphere(1, 1, MINI_COLORS[i % MINI_COLORS.length], ORB_MATERIAL);
2286
2441
  this.minis.push(engine.add(mesh, { scale: 0, transparency: { ...MINI_TRANSPARENCY } }));
2287
2442
  }
2288
2443
  this.engine = engine;
@@ -2944,6 +3099,7 @@ var WARP_ACCEL = 1e3;
2944
3099
  var TRAIL_OUTRO_MS = 1200;
2945
3100
  var TRANSPARENCY = { mode: "two-sided", opacity: 0.275 };
2946
3101
  var CAR_COLORS = ["#bae6fd", "#7dd3fc", "#38bdf8", "#0ea5e9", "#a5f3fc", "#e0f2fe"];
3102
+ var CAR_MATERIAL = { emissive: [0.1, 0.2, 0.3] };
2947
3103
  var WORLD_UP2 = { x: 0, y: 1, z: 0 };
2948
3104
  function clamp014(value) {
2949
3105
  return Math.max(0, Math.min(1, value));
@@ -3004,7 +3160,7 @@ var GhostTrainAnimation = class {
3004
3160
  backend: this.backend,
3005
3161
  camera: { position: { x: 0, y: 0, z: CAMERA_Z2 }, fov: FOV }
3006
3162
  });
3007
- const mesh = cube(1, CAR_COLORS);
3163
+ const mesh = cube(1, CAR_COLORS, CAR_MATERIAL);
3008
3164
  for (let i = 0; i < MAX_CARS; i++) {
3009
3165
  this.cars.push(engine.add(mesh, { scale: 0, transparency: { ...TRANSPARENCY } }));
3010
3166
  }
@@ -7,13 +7,26 @@ export interface TriangleData {
7
7
  positions: Float32Array;
8
8
  normals: Float32Array;
9
9
  colors: Float32Array;
10
+ /**
11
+ * Per-vertex emissive (`Ke`) as linear `0..1` RGB, duplicated across each
12
+ * face's vertices. Faces with no material emit `(0,0,0)`, so a GPU shader can
13
+ * add this term unconditionally and unlit meshes stay identical.
14
+ */
15
+ emissives: Float32Array;
16
+ /**
17
+ * Per-vertex specular packed as 4 floats: `Ks` linear RGB in `xyz` and the
18
+ * `Ns` shininess exponent in `w`. Faces with no specular emit `(0,0,0,1)`, so
19
+ * the highlight term multiplies to zero and unlit meshes stay identical.
20
+ * Note the stride is 4 floats here, not 3 like the other arrays.
21
+ */
22
+ speculars: Float32Array;
10
23
  /** Number of vertices (positions.length / 3). */
11
24
  count: number;
12
25
  }
13
26
  /**
14
27
  * Triangulate a mesh into a non-indexed soup where every vertex carries its
15
- * face's normal and color. This is what GPU backends upload to render flat
16
- * shading without per-primitive state.
28
+ * face's normal, color, and emissive. This is what GPU backends upload to
29
+ * render flat shading without per-primitive state.
17
30
  */
18
31
  export declare function expandToTriangles(mesh: Mesh): TriangleData;
19
32
  /**
@@ -13,8 +13,8 @@ export function parseColor(color) {
13
13
  }
14
14
  /**
15
15
  * Triangulate a mesh into a non-indexed soup where every vertex carries its
16
- * face's normal and color. This is what GPU backends upload to render flat
17
- * shading without per-primitive state.
16
+ * face's normal, color, and emissive. This is what GPU backends upload to
17
+ * render flat shading without per-primitive state.
18
18
  */
19
19
  export function expandToTriangles(mesh) {
20
20
  let triangles = 0;
@@ -23,7 +23,10 @@ export function expandToTriangles(mesh) {
23
23
  const positions = new Float32Array(triangles * 9);
24
24
  const normals = new Float32Array(triangles * 9);
25
25
  const colors = new Float32Array(triangles * 9);
26
+ const emissives = new Float32Array(triangles * 9);
27
+ const speculars = new Float32Array(triangles * 12);
26
28
  let o = 0;
29
+ let so = 0;
27
30
  for (const face of mesh.faces) {
28
31
  const v0 = mesh.vertices[face.indices[0]];
29
32
  const v1 = mesh.vertices[face.indices[1]];
@@ -33,6 +36,17 @@ export function expandToTriangles(mesh) {
33
36
  const cr = r / 255;
34
37
  const cg = g / 255;
35
38
  const cb = b / 255;
39
+ const emissive = face.material?.emissive;
40
+ const er = emissive ? emissive[0] : 0;
41
+ const eg = emissive ? emissive[1] : 0;
42
+ const eb = emissive ? emissive[2] : 0;
43
+ const specular = face.material?.specular;
44
+ const sr = specular ? specular[0] : 0;
45
+ const sg = specular ? specular[1] : 0;
46
+ const sb = specular ? specular[2] : 0;
47
+ // Ns only matters where Ks is non-zero; default the exponent to 1 (32 is
48
+ // the flat-shading default when a specular is present but Ns is omitted).
49
+ const sn = specular ? face.material?.shininess ?? 32 : 1;
36
50
  for (let k = 1; k < face.indices.length - 1; k++) {
37
51
  const tri = [face.indices[0], face.indices[k], face.indices[k + 1]];
38
52
  for (const index of tri) {
@@ -46,11 +60,19 @@ export function expandToTriangles(mesh) {
46
60
  colors[o] = cr;
47
61
  colors[o + 1] = cg;
48
62
  colors[o + 2] = cb;
63
+ emissives[o] = er;
64
+ emissives[o + 1] = eg;
65
+ emissives[o + 2] = eb;
66
+ speculars[so] = sr;
67
+ speculars[so + 1] = sg;
68
+ speculars[so + 2] = sb;
69
+ speculars[so + 3] = sn;
49
70
  o += 3;
71
+ so += 4;
50
72
  }
51
73
  }
52
74
  }
53
- return { positions, normals, colors, count: positions.length / 3 };
75
+ return { positions, normals, colors, emissives, speculars, count: positions.length / 3 };
54
76
  }
55
77
  function midpoint(a, b) {
56
78
  return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, z: (a.z + b.z) / 2 };