poppygl 0.0.12 → 0.0.14

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.
package/dist/index.d.ts CHANGED
@@ -10,26 +10,11 @@ interface BitmapLike {
10
10
  type ImageFactory = (width: number, height: number) => BitmapLike;
11
11
  declare const createUint8Bitmap: ImageFactory;
12
12
 
13
- declare const DEFAULT_LIGHT_DIR: readonly [-0.4, -0.9, -0.2];
14
- interface RenderOptions {
15
- width: number;
16
- height: number;
17
- fov: number;
18
- cull: boolean;
19
- gamma: boolean;
20
- ambient: number;
21
- lightDir: readonly [number, number, number];
22
- camPos?: readonly [number, number, number] | null;
23
- lookAt?: readonly [number, number, number] | null;
24
- backgroundColor?: readonly [number, number, number] | null;
25
- }
26
- type RenderOptionsInput = Partial<RenderOptions>;
27
- declare const DEFAULT_RENDER_OPTIONS: RenderOptions;
28
- declare function getDefaultRenderOptions(): RenderOptions;
29
-
30
13
  interface Material {
31
14
  baseColorFactor: [number, number, number, number];
32
15
  baseColorTexture: BitmapLike | null;
16
+ alphaMode?: "OPAQUE" | "MASK" | "BLEND";
17
+ alphaCutoff?: number;
33
18
  }
34
19
  interface DrawCall {
35
20
  positions: Float32Array;
@@ -39,6 +24,17 @@ interface DrawCall {
39
24
  model: mat4;
40
25
  material: Material;
41
26
  colors?: Float32Array | null;
27
+ mode?: number;
28
+ }
29
+ interface GridOptions {
30
+ size?: number | readonly [number, number, number];
31
+ divisions?: number;
32
+ color?: readonly [number, number, number];
33
+ offset?: Partial<{
34
+ x: number;
35
+ y: number;
36
+ z: number;
37
+ }>;
42
38
  }
43
39
  interface GLTFResources {
44
40
  buffers: Uint8Array[];
@@ -49,6 +45,24 @@ interface GLTFScene {
49
45
  gltf: any;
50
46
  }
51
47
 
48
+ declare const DEFAULT_LIGHT_DIR: readonly [-0.4, -0.9, -0.2];
49
+ interface RenderOptions {
50
+ width: number;
51
+ height: number;
52
+ fov: number;
53
+ cull: boolean;
54
+ gamma: boolean;
55
+ ambient: number;
56
+ lightDir: readonly [number, number, number];
57
+ camPos?: readonly [number, number, number] | null;
58
+ lookAt?: readonly [number, number, number] | null;
59
+ backgroundColor?: readonly [number, number, number] | null;
60
+ grid?: boolean | GridOptions;
61
+ }
62
+ type RenderOptionsInput = Partial<RenderOptions>;
63
+ declare const DEFAULT_RENDER_OPTIONS: RenderOptions;
64
+ declare function getDefaultRenderOptions(): RenderOptions;
65
+
52
66
  interface Camera {
53
67
  view: mat4;
54
68
  proj: mat4;
@@ -64,6 +78,8 @@ declare function renderDrawCalls(drawCalls: DrawCall[], optionsInput?: RenderOpt
64
78
 
65
79
  declare function resolveRenderOptions(options?: RenderOptionsInput): RenderOptions;
66
80
 
81
+ declare function createGrid(options?: GridOptions): DrawCall;
82
+
67
83
  type GLTF = any;
68
84
  declare function createSceneFromGLTF(gltf: GLTF, resources: GLTFResources): GLTFScene;
69
85
 
@@ -108,6 +124,7 @@ declare class SoftwareRenderer {
108
124
  get buffer(): Uint8Array<ArrayBufferLike> | Uint8ClampedArray<ArrayBufferLike>;
109
125
  clear(colorRGBA?: [number, number, number, number]): void;
110
126
  setPixel(x: number, y: number, r: number, g: number, b: number, a: number): void;
127
+ drawLines(mesh: DrawCall, camera: Camera, gammaOut?: boolean): void;
111
128
  sampleTextureNearest(img: BitmapLike | null, u: number, v: number): MutableRGBA;
112
129
  perspInterp(attrs: number[][], invWs: number[], lambdas: number[]): number[];
113
130
  drawMesh(mesh: DrawCall, camera: Camera, light: LightSettings, material: Material, cullBackFaces?: boolean, gammaOut?: boolean): void;
@@ -128,4 +145,4 @@ declare function renderGLTFToPNGBuffer(gltfPathOrString: string, options?: Rende
128
145
 
129
146
  declare function renderSceneFromGLTF(scene: GLTFScene, options?: RenderOptionsInput, imageFactory?: ImageFactory): RenderResult;
130
147
 
131
- export { type BitmapLike, type Camera, DEFAULT_LIGHT_DIR, DEFAULT_RENDER_OPTIONS, type DrawCall, type FetchLike, type GLTFResources, type GLTFScene, type ImageFactory, type LightSettings, type LoadGLTFWithResourcesFromURLOptions, type Material, type MutableRGBA, type RGBA, type RenderGLTFToPNGBufferFromGLBBufferOptions, type RenderGLTFToPNGBufferFromURLOptions, type RenderOptions, type RenderOptionsInput, type RenderResult, SoftwareRenderer, bufferFromDataURI, buildCamera, computeSmoothNormals, computeWorldAABB, createSceneFromGLTF, createUint8Bitmap, decodeImageFromBuffer, encodePNGToBuffer, getDefaultRenderOptions, isJPG, isPNG, loadGLTFWithResourcesFromURL, pureImageFactory, renderDrawCalls, renderGLTFToPNGBuffer, renderGLTFToPNGBufferFromGLBBuffer, renderGLTFToPNGBufferFromURL, renderSceneFromGLTF, resolveRenderOptions };
148
+ export { type BitmapLike, type Camera, DEFAULT_LIGHT_DIR, DEFAULT_RENDER_OPTIONS, type DrawCall, type FetchLike, type GLTFResources, type GLTFScene, type GridOptions, type ImageFactory, type LightSettings, type LoadGLTFWithResourcesFromURLOptions, type Material, type MutableRGBA, type RGBA, type RenderGLTFToPNGBufferFromGLBBufferOptions, type RenderGLTFToPNGBufferFromURLOptions, type RenderOptions, type RenderOptionsInput, type RenderResult, SoftwareRenderer, bufferFromDataURI, buildCamera, computeSmoothNormals, computeWorldAABB, createGrid, createSceneFromGLTF, createUint8Bitmap, decodeImageFromBuffer, encodePNGToBuffer, getDefaultRenderOptions, isJPG, isPNG, loadGLTFWithResourcesFromURL, pureImageFactory, renderDrawCalls, renderGLTFToPNGBuffer, renderGLTFToPNGBufferFromGLBBuffer, renderGLTFToPNGBufferFromURL, renderSceneFromGLTF, resolveRenderOptions };
package/dist/index.js CHANGED
@@ -23,6 +23,12 @@ function computeWorldAABB(drawCalls) {
23
23
  max[2] = Math.max(max[2], z);
24
24
  }
25
25
  }
26
+ if (!isFinite(min[0])) {
27
+ return {
28
+ min: [-1, -1, -1],
29
+ max: [1, 1, 1]
30
+ };
31
+ }
26
32
  return { min, max };
27
33
  }
28
34
 
@@ -76,6 +82,49 @@ function buildCamera(drawCalls, width, height, fovDeg, camPos, lookAt) {
76
82
  return { view, proj };
77
83
  }
78
84
 
85
+ // lib/gltf/createGrid.ts
86
+ import { mat4 as mat42 } from "gl-matrix";
87
+ function createGrid(options = {}) {
88
+ const size = options.size ?? 10;
89
+ const sizeX = typeof size === "number" ? size : size[0];
90
+ const sizeZ = typeof size === "number" ? size : size[2];
91
+ const divisions = options.divisions ?? 10;
92
+ const color = options.color ?? [0.5, 0.5, 0.5];
93
+ const offset = { x: 0, y: 0, z: 0, ...options.offset };
94
+ const positions = [];
95
+ const indices = [];
96
+ let vertexIndex = 0;
97
+ const halfSizeX = sizeX / 2;
98
+ const halfSizeZ = sizeZ / 2;
99
+ const stepX = sizeX / divisions;
100
+ const stepZ = sizeZ / divisions;
101
+ for (let i = 0; i <= divisions; i++) {
102
+ const pX = -halfSizeX + i * stepX;
103
+ const pZ = -halfSizeZ + i * stepZ;
104
+ positions.push(pX, 0, -halfSizeZ);
105
+ positions.push(pX, 0, halfSizeZ);
106
+ indices.push(vertexIndex++, vertexIndex++);
107
+ positions.push(-halfSizeX, 0, pZ);
108
+ positions.push(halfSizeX, 0, pZ);
109
+ indices.push(vertexIndex++, vertexIndex++);
110
+ }
111
+ const model = mat42.create();
112
+ mat42.fromTranslation(model, [offset.x, offset.y, offset.z]);
113
+ return {
114
+ positions: new Float32Array(positions),
115
+ indices: new Uint32Array(indices),
116
+ normals: null,
117
+ uvs: null,
118
+ model,
119
+ material: {
120
+ baseColorFactor: [...color, 1],
121
+ baseColorTexture: null
122
+ },
123
+ mode: 1
124
+ // 1 = LINES
125
+ };
126
+ }
127
+
79
128
  // lib/image/createUint8Bitmap.ts
80
129
  var createUint8Bitmap = (width, height) => ({
81
130
  width,
@@ -95,14 +144,15 @@ var DEFAULT_RENDER_OPTIONS = {
95
144
  lightDir: DEFAULT_LIGHT_DIR,
96
145
  camPos: null,
97
146
  lookAt: null,
98
- backgroundColor: null
147
+ backgroundColor: null,
148
+ grid: false
99
149
  };
100
150
  function getDefaultRenderOptions() {
101
151
  return { ...DEFAULT_RENDER_OPTIONS };
102
152
  }
103
153
 
104
154
  // lib/render/SoftwareRenderer.ts
105
- import { mat3, mat4 as mat42, vec3 as vec32, vec4 as vec42 } from "gl-matrix";
155
+ import { mat3, mat4 as mat43, vec3 as vec32, vec4 as vec42 } from "gl-matrix";
106
156
 
107
157
  // lib/gltf/computeSmoothNormals.ts
108
158
  function computeSmoothNormals(positions, indices) {
@@ -218,6 +268,96 @@ var SoftwareRenderer = class {
218
268
  this.buffer[idx + 2] = b;
219
269
  this.buffer[idx + 3] = a;
220
270
  }
271
+ drawLines(mesh, camera, gammaOut = true) {
272
+ const { positions, indices, model, material } = mesh;
273
+ if (!indices) return;
274
+ const view = camera.view;
275
+ const proj = camera.proj;
276
+ const mvp = mat43.create();
277
+ mat43.multiply(mvp, proj, mat43.multiply(mat43.create(), view, model));
278
+ const vertexCount = positions.length / 3 | 0;
279
+ const vScreen = new Array(vertexCount);
280
+ const vInvW = new Float32Array(vertexCount);
281
+ const vNDCz = new Float32Array(vertexCount);
282
+ for (let i = 0; i < vertexCount; i++) {
283
+ const p = vec42.fromValues(
284
+ positions[i * 3 + 0],
285
+ positions[i * 3 + 1],
286
+ positions[i * 3 + 2],
287
+ 1
288
+ );
289
+ const c = vec42.create();
290
+ vec42.transformMat4(c, p, mvp);
291
+ const invW = 1 / c[3];
292
+ vInvW[i] = invW;
293
+ if (!isFinite(invW)) {
294
+ vScreen[i] = [NaN, NaN];
295
+ vNDCz[i] = NaN;
296
+ continue;
297
+ }
298
+ const ndcX = c[0] * invW;
299
+ const ndcY = c[1] * invW;
300
+ const ndcZ = c[2] * invW;
301
+ const sx = (ndcX * 0.5 + 0.5) * (this.width - 1);
302
+ const sy = (1 - (ndcY * 0.5 + 0.5)) * (this.height - 1);
303
+ vScreen[i] = [sx, sy];
304
+ vNDCz[i] = ndcZ;
305
+ }
306
+ let [r, g, b, a] = material.baseColorFactor;
307
+ if (gammaOut) {
308
+ r = srgbEncodeLinear01(clamp(r, 0, 1));
309
+ g = srgbEncodeLinear01(clamp(g, 0, 1));
310
+ b = srgbEncodeLinear01(clamp(b, 0, 1));
311
+ } else {
312
+ r = clamp(r, 0, 1);
313
+ g = clamp(g, 0, 1);
314
+ b = clamp(b, 0, 1);
315
+ }
316
+ const r255 = r * 255 | 0;
317
+ const g255 = g * 255 | 0;
318
+ const b255 = b * 255 | 0;
319
+ const a255 = clamp(a, 0, 1) * 255 | 0;
320
+ for (let i = 0; i < indices.length; i += 2) {
321
+ const i0 = indices[i + 0];
322
+ const i1 = indices[i + 1];
323
+ const v0s = vScreen[i0];
324
+ const v1s = vScreen[i1];
325
+ const z0_ndc = vNDCz[i0];
326
+ const z1_ndc = vNDCz[i1];
327
+ if (isNaN(v0s[0]) || isNaN(v1s[0])) continue;
328
+ const z0_01 = z0_ndc * 0.5 + 0.5;
329
+ const z1_01 = z1_ndc * 0.5 + 0.5;
330
+ if (z0_01 < 0 && z1_01 < 0 || z0_01 > 1 && z1_01 > 1) continue;
331
+ const x0 = Math.round(v0s[0]);
332
+ const y0 = Math.round(v0s[1]);
333
+ const x1 = Math.round(v1s[0]);
334
+ const y1 = Math.round(v1s[1]);
335
+ const dx = x1 - x0;
336
+ const dy = y1 - y0;
337
+ const steps = Math.max(Math.abs(dx), Math.abs(dy));
338
+ if (steps === 0) continue;
339
+ const xinc = dx / steps;
340
+ const yinc = dy / steps;
341
+ const zinc = (z1_01 - z0_01) / steps;
342
+ let x = x0;
343
+ let y = y0;
344
+ let z = z0_01;
345
+ for (let k = 0; k <= steps; k++) {
346
+ const xi = Math.round(x);
347
+ const yi = Math.round(y);
348
+ if (xi >= 0 && xi < this.width && yi >= 0 && yi < this.height && z >= 0 && z <= 1) {
349
+ const di = yi * this.width + xi;
350
+ if (z < this.depth[di]) {
351
+ this.depth[di] = z;
352
+ this.setPixel(xi, yi, r255, g255, b255, a255);
353
+ }
354
+ }
355
+ x += xinc;
356
+ y += yinc;
357
+ z += zinc;
358
+ }
359
+ }
360
+ }
221
361
  sampleTextureNearest(img, u, v) {
222
362
  if (!img) return [1, 1, 1, 1];
223
363
  const x = clamp(Math.floor(u * (img.width - 1)), 0, img.width - 1);
@@ -247,8 +387,8 @@ var SoftwareRenderer = class {
247
387
  const { positions, normals, uvs, indices, model, colors } = mesh;
248
388
  const view = camera.view;
249
389
  const proj = camera.proj;
250
- const mvp = mat42.create();
251
- mat42.multiply(mvp, proj, mat42.multiply(mat42.create(), view, model));
390
+ const mvp = mat43.create();
391
+ mat43.multiply(mvp, proj, mat43.multiply(mat43.create(), view, model));
252
392
  const normalMat = mat3.create();
253
393
  mat3.normalFromMat4(normalMat, model);
254
394
  const vertexCount = positions.length / 3 | 0;
@@ -404,6 +544,13 @@ var SoftwareRenderer = class {
404
544
  let g = baseColor[1] * lit;
405
545
  let b = baseColor[2] * lit;
406
546
  let a = baseColor[3];
547
+ const alphaMode = material.alphaMode ?? "OPAQUE";
548
+ const alphaCutoff = material.alphaCutoff ?? 0.5;
549
+ if (alphaMode === "MASK" && a < alphaCutoff) continue;
550
+ const shouldWriteDepth = alphaMode !== "BLEND";
551
+ if (shouldWriteDepth) {
552
+ depth[di] = z01;
553
+ }
407
554
  if (gammaOut) {
408
555
  r = srgbEncodeLinear01(clamp(r, 0, 1));
409
556
  g = srgbEncodeLinear01(clamp(g, 0, 1));
@@ -413,13 +560,29 @@ var SoftwareRenderer = class {
413
560
  g = clamp(g, 0, 1);
414
561
  b = clamp(b, 0, 1);
415
562
  }
563
+ const dstIdx = (y * this.width + x) * 4;
564
+ const dstR = (this.buffer[dstIdx + 0] ?? 0) / 255;
565
+ const dstG = (this.buffer[dstIdx + 1] ?? 0) / 255;
566
+ const dstB = (this.buffer[dstIdx + 2] ?? 0) / 255;
567
+ const dstA = (this.buffer[dstIdx + 3] ?? 0) / 255;
568
+ let outR = r;
569
+ let outG = g;
570
+ let outB = b;
571
+ let outA = a;
572
+ if (alphaMode === "BLEND" && a < 1) {
573
+ const oneMinusA = 1 - a;
574
+ outR = r * a + dstR * oneMinusA;
575
+ outG = g * a + dstG * oneMinusA;
576
+ outB = b * a + dstB * oneMinusA;
577
+ outA = a + dstA * oneMinusA;
578
+ }
416
579
  this.setPixel(
417
580
  x,
418
581
  y,
419
- r * 255 | 0,
420
- g * 255 | 0,
421
- b * 255 | 0,
422
- clamp(a, 0, 1) * 255 | 0
582
+ clamp(outR, 0, 1) * 255 | 0,
583
+ clamp(outG, 0, 1) * 255 | 0,
584
+ clamp(outB, 0, 1) * 255 | 0,
585
+ clamp(outA, 0, 1) * 255 | 0
423
586
  );
424
587
  }
425
588
  }
@@ -468,21 +631,66 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
468
631
  } else {
469
632
  renderer.clear([0, 0, 0, 0]);
470
633
  }
471
- for (const dc of drawCalls) {
472
- renderer.drawMesh(
473
- dc,
474
- camera,
475
- { dir: options.lightDir, ambient: options.ambient },
476
- dc.material,
477
- options.cull,
478
- options.gamma
479
- );
634
+ const allDrawCalls = [...drawCalls];
635
+ if (options.grid) {
636
+ const userGridOptions = typeof options.grid === "boolean" ? {} : options.grid;
637
+ const aabb = computeWorldAABB(drawCalls);
638
+ const sizeX = aabb.max[0] - aabb.min[0];
639
+ const sizeZ = aabb.max[2] - aabb.min[2];
640
+ const maxSize = Math.max(sizeX, sizeZ);
641
+ const defaultSize = Math.ceil(maxSize * 1.2 / 2) * 2;
642
+ const defaultOffset = {
643
+ x: (aabb.min[0] + aabb.max[0]) / 2,
644
+ y: aabb.min[1],
645
+ z: (aabb.min[2] + aabb.max[2]) / 2
646
+ };
647
+ const defaultGridOptions = {
648
+ size: defaultSize > 0 ? defaultSize : 10,
649
+ offset: defaultOffset
650
+ };
651
+ const finalGridOptions = {
652
+ ...defaultGridOptions,
653
+ ...userGridOptions,
654
+ offset: {
655
+ ...defaultGridOptions.offset,
656
+ ...userGridOptions.offset
657
+ }
658
+ };
659
+ allDrawCalls.push(createGrid(finalGridOptions));
480
660
  }
661
+ const opaqueDraws = allDrawCalls.filter(
662
+ (dc) => (dc.material.alphaMode ?? "OPAQUE") === "OPAQUE"
663
+ );
664
+ const maskDraws = allDrawCalls.filter(
665
+ (dc) => (dc.material.alphaMode ?? "OPAQUE") === "MASK"
666
+ );
667
+ const blendDraws = allDrawCalls.filter(
668
+ (dc) => (dc.material.alphaMode ?? "OPAQUE") === "BLEND"
669
+ );
670
+ const renderGroup = (dcs) => {
671
+ for (const dc of dcs) {
672
+ if (dc.mode === 1) {
673
+ renderer.drawLines(dc, camera, options.gamma);
674
+ } else {
675
+ renderer.drawMesh(
676
+ dc,
677
+ camera,
678
+ { dir: options.lightDir, ambient: options.ambient },
679
+ dc.material,
680
+ options.cull,
681
+ options.gamma
682
+ );
683
+ }
684
+ }
685
+ };
686
+ renderGroup(opaqueDraws);
687
+ renderGroup(maskDraws);
688
+ renderGroup(blendDraws);
481
689
  return { bitmap: renderer.bitmap, camera, options };
482
690
  }
483
691
 
484
692
  // lib/gltf/createSceneFromGLTF.ts
485
- import { mat4 as mat43 } from "gl-matrix";
693
+ import { mat4 as mat44 } from "gl-matrix";
486
694
  var COMPONENT_INFO = {
487
695
  5120: {
488
696
  name: "BYTE",
@@ -680,17 +888,17 @@ function readIndices(ctx, accessorIndex) {
680
888
  }
681
889
  function nodeLocalMatrix(node) {
682
890
  if (node.matrix) {
683
- const m2 = mat43.create();
891
+ const m2 = mat44.create();
684
892
  for (let i = 0; i < 16; i++) m2[i] = node.matrix[i];
685
893
  return m2;
686
894
  }
687
895
  const translation = node.translation || [0, 0, 0];
688
896
  const rotation = node.rotation || [0, 0, 0, 1];
689
897
  const scale = node.scale || [1, 1, 1];
690
- const m = mat43.create();
691
- const tmp = mat43.create();
692
- mat43.fromRotationTranslationScale(tmp, rotation, translation, scale);
693
- mat43.copy(m, tmp);
898
+ const m = mat44.create();
899
+ const tmp = mat44.create();
900
+ mat44.fromRotationTranslationScale(tmp, rotation, translation, scale);
901
+ mat44.copy(m, tmp);
694
902
  return m;
695
903
  }
696
904
  function getMaterial(gltf, textures, images, materialIndex) {
@@ -706,7 +914,14 @@ function getMaterial(gltf, textures, images, materialIndex) {
706
914
  texImg = images[imageIndex] || null;
707
915
  }
708
916
  }
709
- return { baseColorFactor: factor, baseColorTexture: texImg };
917
+ const alphaMode = typeof material.alphaMode === "string" ? material.alphaMode : "OPAQUE";
918
+ const alphaCutoff = typeof material.alphaCutoff === "number" ? material.alphaCutoff : 0.5;
919
+ return {
920
+ baseColorFactor: factor,
921
+ baseColorTexture: texImg,
922
+ alphaMode,
923
+ alphaCutoff
924
+ };
710
925
  }
711
926
  function createSceneFromGLTF(gltf, resources) {
712
927
  const ctx = { gltf, buffers: resources.buffers };
@@ -721,8 +936,8 @@ function createSceneFromGLTF(gltf, resources) {
721
936
  const node = nodes[nodeIndex];
722
937
  if (!node) return;
723
938
  const local = nodeLocalMatrix(node);
724
- const world = mat43.create();
725
- mat43.multiply(world, parentMatrix, local);
939
+ const world = mat44.create();
940
+ mat44.multiply(world, parentMatrix, local);
726
941
  if (Number.isInteger(node.mesh)) {
727
942
  const mesh = meshes[node.mesh];
728
943
  if (mesh) {
@@ -760,7 +975,7 @@ function createSceneFromGLTF(gltf, resources) {
760
975
  traverse(childIndex, world);
761
976
  }
762
977
  }
763
- const identity = mat43.create();
978
+ const identity = mat44.create();
764
979
  for (const rootIndex of scene.nodes || []) {
765
980
  traverse(rootIndex, identity);
766
981
  }
@@ -1163,6 +1378,7 @@ export {
1163
1378
  buildCamera,
1164
1379
  computeSmoothNormals,
1165
1380
  computeWorldAABB,
1381
+ createGrid,
1166
1382
  createSceneFromGLTF,
1167
1383
  createUint8Bitmap,
1168
1384
  decodeImageFromBuffer,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "poppygl",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.12",
4
+ "version": "0.0.14",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/tscircuit/poppygl.git"