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
@@ -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,17 +414,19 @@ 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 attribute = (location, array, size = 3) => {
343
418
  if (location < 0) return;
344
419
  const buffer = gl.createBuffer();
345
420
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
346
421
  gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW);
347
422
  gl.enableVertexAttribArray(location);
348
- gl.vertexAttribPointer(location, 3, gl.FLOAT, false, 0, 0);
423
+ gl.vertexAttribPointer(location, size, gl.FLOAT, false, 0, 0);
349
424
  };
350
425
  attribute(loc.aPos, data.positions);
351
426
  attribute(loc.aNormal, data.normals);
352
427
  attribute(loc.aColor, data.colors);
428
+ attribute(loc.aEmissive, data.emissives);
429
+ attribute(loc.aSpecular, data.speculars, 4);
353
430
  gl.bindVertexArray(null);
354
431
  const result = { vao, count: data.count };
355
432
  this.cache.set(mesh, result);
@@ -364,6 +441,7 @@ void main() {
364
441
  gl.useProgram(this.program);
365
442
  gl.uniformMatrix4fv(loc.uViewProj, false, new Float32Array(frame.viewProjection));
366
443
  gl.uniform3f(loc.uToLight, frame.light.toLight.x, frame.light.toLight.y, frame.light.toLight.z);
444
+ gl.uniform3f(loc.uEye, frame.eye.x, frame.eye.y, frame.eye.z);
367
445
  gl.uniform1f(loc.uIntensity, frame.light.intensity);
368
446
  gl.uniform1f(loc.uAmbient, frame.light.ambient);
369
447
  gl.disable(gl.BLEND);
@@ -438,6 +516,7 @@ struct Uniforms {
438
516
  model: mat4x4<f32>,
439
517
  toLight: vec4<f32>,
440
518
  params: vec4<f32>,
519
+ eye: vec4<f32>,
441
520
  };
442
521
  @group(0) @binding(0) var<uniform> u: Uniforms;
443
522
 
@@ -445,23 +524,40 @@ struct VSOut {
445
524
  @builtin(position) position: vec4<f32>,
446
525
  @location(0) normal: vec3<f32>,
447
526
  @location(1) color: vec3<f32>,
527
+ @location(2) emissive: vec3<f32>,
528
+ @location(3) specular: vec4<f32>,
529
+ @location(4) worldPos: vec3<f32>,
448
530
  };
449
531
 
450
532
  @vertex
451
- fn vs(@location(0) pos: vec3<f32>, @location(1) normal: vec3<f32>, @location(2) color: vec3<f32>) -> VSOut {
533
+ 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
534
  var out: VSOut;
453
535
  let m = mat3x3<f32>(u.model[0].xyz, u.model[1].xyz, u.model[2].xyz);
454
536
  out.normal = m * normal;
455
537
  out.color = color;
456
- out.position = u.viewProj * u.model * vec4<f32>(pos, 1.0);
538
+ out.emissive = emissive;
539
+ out.specular = specular;
540
+ let world = u.model * vec4<f32>(pos, 1.0);
541
+ out.worldPos = world.xyz;
542
+ out.position = u.viewProj * world;
457
543
  return out;
458
544
  }
459
545
 
460
546
  @fragment
461
547
  fn fs(in: VSOut) -> @location(0) vec4<f32> {
462
- let lambert = max(dot(normalize(in.normal), normalize(u.toLight.xyz)), 0.0);
548
+ let normal = normalize(in.normal);
549
+ let toLight = normalize(u.toLight.xyz);
550
+ let lambert = max(dot(normal, toLight), 0.0);
463
551
  let brightness = clamp(u.params.y + u.params.x * lambert, 0.0, 1.0);
464
- return vec4<f32>(in.color * brightness, u.params.z);
552
+ var lit = in.color * brightness;
553
+ if (lambert > 0.0) {
554
+ let viewDir = normalize(u.eye.xyz - in.worldPos);
555
+ let halfVec = normalize(toLight + viewDir);
556
+ let highlight = pow(max(dot(normal, halfVec), 0.0), in.specular.w) * u.params.x;
557
+ lit = lit + highlight * in.specular.xyz;
558
+ }
559
+ lit = lit + in.emissive;
560
+ return vec4<f32>(lit, u.params.z);
465
561
  }
466
562
  `;
467
563
  CLIP_Z_FIX = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 1];
@@ -502,13 +598,15 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
502
598
  {
503
599
  binding: 0,
504
600
  visibility: stage.VERTEX | stage.FRAGMENT,
505
- buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: 160 }
601
+ buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: 176 }
506
602
  }
507
603
  ]
508
604
  });
509
- const vertexBuffer = (location) => ({
510
- arrayStride: 12,
511
- attributes: [{ shaderLocation: location, offset: 0, format: "float32x3" }]
605
+ const vertexBuffer = (location, components = 3) => ({
606
+ arrayStride: components * 4,
607
+ attributes: [
608
+ { shaderLocation: location, offset: 0, format: `float32x${components}` }
609
+ ]
512
610
  });
513
611
  const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [layout] });
514
612
  const blend = {
@@ -524,7 +622,13 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
524
622
  vertex: {
525
623
  module: module2,
526
624
  entryPoint: "vs",
527
- buffers: [vertexBuffer(0), vertexBuffer(1), vertexBuffer(2)]
625
+ buffers: [
626
+ vertexBuffer(0),
627
+ vertexBuffer(1),
628
+ vertexBuffer(2),
629
+ vertexBuffer(3),
630
+ vertexBuffer(4, 4)
631
+ ]
528
632
  },
529
633
  fragment: {
530
634
  module: module2,
@@ -577,6 +681,8 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
577
681
  position: upload(data.positions),
578
682
  normal: upload(data.normals),
579
683
  color: upload(data.colors),
684
+ emissive: upload(data.emissives),
685
+ specular: upload(data.speculars),
580
686
  count: data.count
581
687
  };
582
688
  this.cache.set(mesh, result);
@@ -628,7 +734,7 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
628
734
  const bindGroup = this.device.createBindGroup({
629
735
  layout,
630
736
  entries: [
631
- { binding: 0, resource: { buffer: this.uniformBuffer, offset: 0, size: 160 } }
737
+ { binding: 0, resource: { buffer: this.uniformBuffer, offset: 0, size: 176 } }
632
738
  ]
633
739
  });
634
740
  draws.forEach((draw, i) => {
@@ -637,6 +743,7 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
637
743
  data.set(draw.item.model, 16);
638
744
  data.set([frame.light.toLight.x, frame.light.toLight.y, frame.light.toLight.z, 0], 32);
639
745
  data.set([frame.light.intensity, frame.light.ambient, draw.opacity, 0], 36);
746
+ data.set([frame.eye.x, frame.eye.y, frame.eye.z, 0], 40);
640
747
  this.device.queue.writeBuffer(this.uniformBuffer, i * UNIFORM_STRIDE, data);
641
748
  });
642
749
  const encoder = this.device.createCommandEncoder();
@@ -663,6 +770,8 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
663
770
  pass.setVertexBuffer(0, mesh.position);
664
771
  pass.setVertexBuffer(1, mesh.normal);
665
772
  pass.setVertexBuffer(2, mesh.color);
773
+ pass.setVertexBuffer(3, mesh.emissive);
774
+ pass.setVertexBuffer(4, mesh.specular);
666
775
  pass.draw(mesh.count);
667
776
  });
668
777
  pass.end();
@@ -674,6 +783,8 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
674
783
  mesh.position.destroy?.();
675
784
  mesh.normal.destroy?.();
676
785
  mesh.color.destroy?.();
786
+ mesh.emissive.destroy?.();
787
+ mesh.specular.destroy?.();
677
788
  }
678
789
  this.cache.clear();
679
790
  this.uniformBuffer?.destroy?.();
@@ -756,9 +867,30 @@ var init_canvas2d = __esm({
756
867
  depth += dot(d, d);
757
868
  }
758
869
  depth /= face.indices.length;
870
+ let surface;
871
+ const material = face.material;
872
+ if (material) {
873
+ if (material.specular) {
874
+ let cx = 0;
875
+ let cy = 0;
876
+ let cz = 0;
877
+ for (const i of face.indices) {
878
+ cx += world[i].x;
879
+ cy += world[i].y;
880
+ cz += world[i].z;
881
+ }
882
+ const inv = 1 / face.indices.length;
883
+ const viewDir = normalize(
884
+ subtract(frame.eye, { x: cx * inv, y: cy * inv, z: cz * inv })
885
+ );
886
+ surface = { material, viewDir };
887
+ } else {
888
+ surface = { material };
889
+ }
890
+ }
759
891
  polygons.push({
760
892
  points,
761
- color: shadeColor(normal, face.color, frame.light),
893
+ color: shadeColor(normal, face.color, frame.light, surface),
762
894
  depth,
763
895
  opacity: faceOpacity
764
896
  });
@@ -1624,6 +1756,12 @@ init_light();
1624
1756
  init_math();
1625
1757
 
1626
1758
  // src/engines/little-3d-engine/core/mesh.ts
1759
+ function attachMaterial(mesh, material) {
1760
+ if (material) {
1761
+ for (const face of mesh.faces) face.material = material;
1762
+ }
1763
+ return mesh;
1764
+ }
1627
1765
  function transform(init) {
1628
1766
  return {
1629
1767
  position: init?.position ?? { x: 0, y: 0, z: 0 },
@@ -1638,7 +1776,7 @@ init_light();
1638
1776
 
1639
1777
  // src/engines/little-3d-engine/shapes/primitives/cube.ts
1640
1778
  var DEFAULT_COLORS = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
1641
- function cube(size = 1, colors = DEFAULT_COLORS) {
1779
+ function cube(size = 1, colors = DEFAULT_COLORS, material) {
1642
1780
  const h = size / 2;
1643
1781
  const vertices = [
1644
1782
  { x: -h, y: -h, z: h },
@@ -1658,12 +1796,12 @@ function cube(size = 1, colors = DEFAULT_COLORS) {
1658
1796
  { indices: [1, 5, 6, 2], color: colors[4 % colors.length] },
1659
1797
  { indices: [4, 0, 3, 7], color: colors[5 % colors.length] }
1660
1798
  ];
1661
- return { vertices, faces };
1799
+ return attachMaterial({ vertices, faces }, material);
1662
1800
  }
1663
1801
 
1664
1802
  // src/engines/little-3d-engine/shapes/primitives/quad.ts
1665
1803
  var DEFAULT_COLORS2 = ["#3b82f6"];
1666
- function quad(size = 1, colors = DEFAULT_COLORS2) {
1804
+ function quad(size = 1, colors = DEFAULT_COLORS2, material) {
1667
1805
  const s = size / 2;
1668
1806
  const vertices = [
1669
1807
  { x: -s, y: -s, z: 0 },
@@ -1671,12 +1809,15 @@ function quad(size = 1, colors = DEFAULT_COLORS2) {
1671
1809
  { x: s, y: s, z: 0 },
1672
1810
  { x: -s, y: s, z: 0 }
1673
1811
  ];
1674
- return { vertices, faces: [{ indices: [0, 1, 2, 3], color: colors[0] }] };
1812
+ return attachMaterial(
1813
+ { vertices, faces: [{ indices: [0, 1, 2, 3], color: colors[0] }] },
1814
+ material
1815
+ );
1675
1816
  }
1676
1817
 
1677
1818
  // src/engines/little-3d-engine/shapes/primitives/tetrahedron.ts
1678
1819
  var DEFAULT_COLORS3 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b"];
1679
- function tetrahedron(size = 1, colors = DEFAULT_COLORS3) {
1820
+ function tetrahedron(size = 1, colors = DEFAULT_COLORS3, material) {
1680
1821
  const s = size / 2;
1681
1822
  const vertices = [
1682
1823
  { x: s, y: s, z: s },
@@ -1690,12 +1831,12 @@ function tetrahedron(size = 1, colors = DEFAULT_COLORS3) {
1690
1831
  { indices: [0, 2, 3], color: colors[2 % colors.length] },
1691
1832
  { indices: [1, 3, 2], color: colors[3 % colors.length] }
1692
1833
  ];
1693
- return { vertices, faces };
1834
+ return attachMaterial({ vertices, faces }, material);
1694
1835
  }
1695
1836
 
1696
1837
  // src/engines/little-3d-engine/shapes/primitives/pyramid.ts
1697
1838
  var DEFAULT_COLORS4 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981"];
1698
- function pyramid(size = 1, colors = DEFAULT_COLORS4) {
1839
+ function pyramid(size = 1, colors = DEFAULT_COLORS4, material) {
1699
1840
  const h = size / 2;
1700
1841
  const vertices = [
1701
1842
  { x: -h, y: -h, z: h },
@@ -1711,7 +1852,7 @@ function pyramid(size = 1, colors = DEFAULT_COLORS4) {
1711
1852
  { indices: [4, 2, 3], color: colors[3 % colors.length] },
1712
1853
  { indices: [4, 3, 0], color: colors[4 % colors.length] }
1713
1854
  ];
1714
- return { vertices, faces };
1855
+ return attachMaterial({ vertices, faces }, material);
1715
1856
  }
1716
1857
 
1717
1858
  // src/engines/little-3d-engine/shapes/primitives/spheres/icosphere.ts
@@ -1754,8 +1895,11 @@ var SEED_FACES = [
1754
1895
  [8, 6, 7],
1755
1896
  [9, 8, 1]
1756
1897
  ];
1757
- function icosphere(size = 1, detail = 1, colors = DEFAULT_COLORS5) {
1758
- return sphereFromTriangles(SEED_VERTICES, SEED_FACES, size, detail, colors);
1898
+ function icosphere(size = 1, detail = 1, colors = DEFAULT_COLORS5, material) {
1899
+ return attachMaterial(
1900
+ sphereFromTriangles(SEED_VERTICES, SEED_FACES, size, detail, colors),
1901
+ material
1902
+ );
1759
1903
  }
1760
1904
 
1761
1905
  // src/engines/little-3d-engine/shapes/primitives/spheres/octa-sphere.ts
@@ -1763,8 +1907,8 @@ init_geometry();
1763
1907
 
1764
1908
  // src/engines/little-3d-engine/shapes/complex/plane.ts
1765
1909
  var DEFAULT_COLORS6 = ["#e0f2fe", "#7dd3fc", "#38bdf8", "#f8fafc"];
1766
- function planeMesh(colors = DEFAULT_COLORS6) {
1767
- return {
1910
+ function planeMesh(colors = DEFAULT_COLORS6, material) {
1911
+ return attachMaterial({
1768
1912
  vertices: [
1769
1913
  { x: 0.9, y: 0, z: 0 },
1770
1914
  { x: -0.2, y: 0, z: 0.82 },
@@ -1792,7 +1936,7 @@ function planeMesh(colors = DEFAULT_COLORS6) {
1792
1936
  { indices: [3, 6, 8], color: colors[0] ?? DEFAULT_COLORS6[0] },
1793
1937
  { indices: [3, 8, 6], color: colors[1] ?? DEFAULT_COLORS6[1] }
1794
1938
  ]
1795
- };
1939
+ }, material);
1796
1940
  }
1797
1941
 
1798
1942
  // src/engines/little-3d-engine/textures/dynamic/canvas-texture.ts
@@ -2252,6 +2396,7 @@ var REENTER_STAGGER_MS = 45;
2252
2396
  var CENTER_POP_OUT_AT = REENTER_MS + (MINIS - 1) * REENTER_STAGGER_MS + 150;
2253
2397
  var CENTER_POP_OUT_MS = 420;
2254
2398
  var PARKED = { x: 0, y: 0, z: 50 };
2399
+ var ORB_MATERIAL = { specular: [1, 1, 1], shininess: 28 };
2255
2400
  var CENTER_COLORS = ["#67e8f9", "#22d3ee", "#0ea5e9", "#38bdf8", "#7dd3fc"];
2256
2401
  var MINI_COLORS = [
2257
2402
  ["#e0f2fe", "#bae6fd", "#7dd3fc"],
@@ -2280,9 +2425,9 @@ var ChargedOrbAnimation = class {
2280
2425
  backend: this.backend,
2281
2426
  camera: { position: { x: 0, y: 0, z: CAMERA_Z } }
2282
2427
  });
2283
- this.center = engine.add(icosphere(1, 2, CENTER_COLORS), { scale: 0 });
2428
+ this.center = engine.add(icosphere(1, 2, CENTER_COLORS, ORB_MATERIAL), { scale: 0 });
2284
2429
  for (let i = 0; i < MINIS; i++) {
2285
- const mesh = icosphere(1, 1, MINI_COLORS[i % MINI_COLORS.length]);
2430
+ const mesh = icosphere(1, 1, MINI_COLORS[i % MINI_COLORS.length], ORB_MATERIAL);
2286
2431
  this.minis.push(engine.add(mesh, { scale: 0, transparency: { ...MINI_TRANSPARENCY } }));
2287
2432
  }
2288
2433
  this.engine = engine;
@@ -2944,6 +3089,7 @@ var WARP_ACCEL = 1e3;
2944
3089
  var TRAIL_OUTRO_MS = 1200;
2945
3090
  var TRANSPARENCY = { mode: "two-sided", opacity: 0.275 };
2946
3091
  var CAR_COLORS = ["#bae6fd", "#7dd3fc", "#38bdf8", "#0ea5e9", "#a5f3fc", "#e0f2fe"];
3092
+ var CAR_MATERIAL = { emissive: [0.1, 0.2, 0.3] };
2947
3093
  var WORLD_UP2 = { x: 0, y: 1, z: 0 };
2948
3094
  function clamp014(value) {
2949
3095
  return Math.max(0, Math.min(1, value));
@@ -3004,7 +3150,7 @@ var GhostTrainAnimation = class {
3004
3150
  backend: this.backend,
3005
3151
  camera: { position: { x: 0, y: 0, z: CAMERA_Z2 }, fov: FOV }
3006
3152
  });
3007
- const mesh = cube(1, CAR_COLORS);
3153
+ const mesh = cube(1, CAR_COLORS, CAR_MATERIAL);
3008
3154
  for (let i = 0; i < MAX_CARS; i++) {
3009
3155
  this.cars.push(engine.add(mesh, { scale: 0, transparency: { ...TRANSPARENCY } }));
3010
3156
  }
@@ -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 };
@@ -1,4 +1,5 @@
1
1
  import { type Vec3 } from "./math.js";
2
+ import type { Material } from "./mesh.js";
2
3
  /** Directional light configuration. */
3
4
  export interface LightOptions {
4
5
  /** Direction the light travels. Default points down-forward into the scene. */
@@ -16,10 +17,33 @@ export interface LightParams {
16
17
  ambient: number;
17
18
  }
18
19
  /**
19
- * Flat-shade a face: brighten its base `color` by how directly its `normal`
20
- * faces the light, floored at the ambient level. Returns an `rgb(...)` string.
20
+ * Per-face surface inputs for {@link shade}/{@link shadeColor} beyond the base
21
+ * color: its MTL {@link Material} and the direction from the face toward the
22
+ * camera. Both are needed for the specular highlight; without them shading
23
+ * falls back to flat Lambert plus any emissive term.
21
24
  */
22
- export declare function shadeColor(normal: Vec3, color: string, light: LightParams): string;
25
+ export interface Surface {
26
+ /** MTL-derived specular/shininess/emissive for this face. */
27
+ material?: Material;
28
+ /** Unit vector from the face toward the eye, for the specular highlight. */
29
+ viewDir?: Vec3;
30
+ }
31
+ /**
32
+ * Shade a face and return `0..255` RGB channels.
33
+ *
34
+ * The diffuse term is flat Lambert: the base `color` brightened by how directly
35
+ * `normal` faces the light, floored at the ambient level. When a {@link Surface}
36
+ * supplies a specular material and a `viewDir`, a Blinn-Phong highlight (`Ks`
37
+ * tinted, tightened by `Ns`) is added; an emissive material (`Ke`) is always
38
+ * added on top. With no material this reduces exactly to the previous flat
39
+ * shading.
40
+ */
41
+ export declare function shade(normal: Vec3, color: string, light: LightParams, surface?: Surface): [number, number, number];
42
+ /**
43
+ * Shade a face and return an `rgb(...)` string. Thin wrapper over {@link shade};
44
+ * see it for the shading model. Passing no `surface` gives flat Lambert shading.
45
+ */
46
+ export declare function shadeColor(normal: Vec3, color: string, light: LightParams, surface?: Surface): string;
23
47
  /** A single directional light with an ambient term, used for flat shading. */
24
48
  export declare class Light {
25
49
  readonly options: LightOptions;
@@ -27,5 +51,5 @@ export declare class Light {
27
51
  readonly params: LightParams;
28
52
  constructor(options?: Partial<LightOptions>);
29
53
  /** Convenience wrapper around {@link shadeColor} using this light. */
30
- shade(normal: Vec3, color: string): string;
54
+ shade(normal: Vec3, color: string, surface?: Surface): string;
31
55
  }