poppygl 0.0.19 → 0.0.20

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/README.md CHANGED
@@ -53,6 +53,8 @@ const png = await renderGLTFToPNGBufferFromGLBBuffer(glb, {
53
53
  - `supersampling`: render at `width * supersampling` / `height * supersampling`, then downsample (default `1`).
54
54
  - `fov`: vertical field of view in degrees (defaults to `35`).
55
55
  - `camPos` and `lookAt`: override the auto-framed camera position and target.
56
+ - `up`: choose the world-up axis for the camera with `"y+" | "y-" | "x+" | "x-" | "z+" | "z-"`.
57
+ - `cameraRotation`: apply Euler degrees `{ x, y, z }` to define camera orientation directly. When set, it takes precedence over `lookAt`.
56
58
  - `debugPoints`: optional `{ label, position }[]` overlay rendered on top of the final PNG for world-space debugging markers.
57
59
  - `debugFontSize`: optional pixel size for `debugPoints` labels; when omitted the renderer derives a size from the image dimensions.
58
60
  - `debugPointColor`: optional RGB tuple for debug point markers.
package/dist/index.d.ts CHANGED
@@ -61,6 +61,12 @@ interface DebugPoint {
61
61
  z: number;
62
62
  };
63
63
  }
64
+ type CameraUp = "y+" | "y-" | "x+" | "x-" | "z+" | "z-";
65
+ interface CameraRotation {
66
+ x: number;
67
+ y: number;
68
+ z: number;
69
+ }
64
70
  /**
65
71
  * Convert a hex color string to RGB array with values 0-1
66
72
  * @param hex - Hex color string like "#ffffff" or "ffffff"
@@ -78,6 +84,8 @@ interface RenderOptions {
78
84
  lightDir: readonly [number, number, number];
79
85
  camPos?: readonly [number, number, number] | null;
80
86
  lookAt?: readonly [number, number, number] | null;
87
+ up: CameraUp;
88
+ cameraRotation: CameraRotation | null;
81
89
  backgroundColor?: readonly [number, number, number] | string | null;
82
90
  grid?: boolean | GridOptions;
83
91
  debugPoints?: DebugPoint[] | null;
@@ -93,7 +101,7 @@ interface Camera {
93
101
  view: mat4;
94
102
  proj: mat4;
95
103
  }
96
- declare function buildCamera(drawCalls: DrawCall[], width: number, height: number, fovDeg: number, camPos: readonly [number, number, number] | null | undefined, lookAt: readonly [number, number, number] | null | undefined): Camera;
104
+ declare function buildCamera(drawCalls: DrawCall[], width: number, height: number, fovDeg: number, camPos: readonly [number, number, number] | null | undefined, lookAt: readonly [number, number, number] | null | undefined, up?: CameraUp | null | undefined, cameraRotation?: CameraRotation | null | undefined): Camera;
97
105
 
98
106
  interface RenderResult {
99
107
  bitmap: BitmapLike;
@@ -200,4 +208,4 @@ declare function renderGLTFToPNGBuffer(gltfPathOrString: string, options?: Rende
200
208
 
201
209
  declare function renderSceneFromGLTF(scene: GLTFScene, options?: RenderOptionsInput, imageFactory?: ImageFactory): RenderResult;
202
210
 
203
- export { type BitmapLike, type Camera, DEFAULT_LIGHT_DIR, DEFAULT_RENDER_OPTIONS, type DebugPoint, 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, createInfiniteGrid, createSceneFromGLTF, createUint8Bitmap, decodeImageFromBuffer, drawInfiniteGrid, encodePNGToBuffer, getDefaultRenderOptions, hexToRgb, isJPG, isPNG, loadGLTFWithResourcesFromURL, pureImageFactory, renderDrawCalls, renderGLTFToPNGBuffer, renderGLTFToPNGBufferFromGLBBuffer, renderGLTFToPNGBufferFromURL, renderSceneFromGLTF, resolveRenderOptions };
211
+ export { type BitmapLike, type Camera, type CameraRotation, type CameraUp, DEFAULT_LIGHT_DIR, DEFAULT_RENDER_OPTIONS, type DebugPoint, 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, createInfiniteGrid, createSceneFromGLTF, createUint8Bitmap, decodeImageFromBuffer, drawInfiniteGrid, encodePNGToBuffer, getDefaultRenderOptions, hexToRgb, isJPG, isPNG, loadGLTFWithResourcesFromURL, pureImageFactory, renderDrawCalls, renderGLTFToPNGBuffer, renderGLTFToPNGBufferFromGLBBuffer, renderGLTFToPNGBufferFromURL, renderSceneFromGLTF, resolveRenderOptions };
package/dist/index.js CHANGED
@@ -36,49 +36,173 @@ function computeWorldAABB(drawCalls) {
36
36
  var toRad = (degrees) => degrees * Math.PI / 180;
37
37
 
38
38
  // lib/camera/buildCamera.ts
39
- function buildCamera(drawCalls, width, height, fovDeg, camPos, lookAt) {
40
- const aspect = width / height;
41
- const near = 0.01;
42
- const far = 1e3;
43
- const proj = mat4.create();
44
- mat4.perspective(proj, toRad(fovDeg), aspect, near, far);
45
- let eye;
46
- let center;
47
- if (camPos) {
48
- eye = vec3.fromValues(camPos[0], camPos[1], camPos[2]);
49
- if (lookAt) {
50
- center = vec3.fromValues(lookAt[0], lookAt[1], lookAt[2]);
51
- } else {
52
- const aabb = computeWorldAABB(drawCalls);
53
- center = vec3.fromValues(
54
- 0.5 * (aabb.min[0] + aabb.max[0]),
55
- 0.5 * (aabb.min[1] + aabb.max[1]),
56
- 0.5 * (aabb.min[2] + aabb.max[2])
57
- );
39
+ function getWorldUpVector(up) {
40
+ switch (up) {
41
+ case "y-":
42
+ return vec3.fromValues(0, -1, 0);
43
+ case "x+":
44
+ return vec3.fromValues(1, 0, 0);
45
+ case "x-":
46
+ return vec3.fromValues(-1, 0, 0);
47
+ case "z+":
48
+ return vec3.fromValues(0, 0, 1);
49
+ case "z-":
50
+ return vec3.fromValues(0, 0, -1);
51
+ case "y+":
52
+ default:
53
+ return vec3.fromValues(0, 1, 0);
54
+ }
55
+ }
56
+ function buildCameraWorldMatrix(eye, forward, up) {
57
+ const normalizedForward = vec3.normalize(vec3.create(), forward);
58
+ const right = vec3.cross(vec3.create(), normalizedForward, up);
59
+ if (vec3.squaredLength(right) === 0) {
60
+ return null;
61
+ }
62
+ vec3.normalize(right, right);
63
+ const correctedUp = vec3.cross(vec3.create(), right, normalizedForward);
64
+ if (vec3.squaredLength(correctedUp) === 0) {
65
+ return null;
66
+ }
67
+ vec3.normalize(correctedUp, correctedUp);
68
+ return mat4.fromValues(
69
+ right[0],
70
+ right[1],
71
+ right[2],
72
+ 0,
73
+ correctedUp[0],
74
+ correctedUp[1],
75
+ correctedUp[2],
76
+ 0,
77
+ -normalizedForward[0],
78
+ -normalizedForward[1],
79
+ -normalizedForward[2],
80
+ 0,
81
+ eye[0],
82
+ eye[1],
83
+ eye[2],
84
+ 1
85
+ );
86
+ }
87
+ function getDefaultForwardForUp(up) {
88
+ const zForward = vec3.fromValues(0, 0, -1);
89
+ if (Math.abs(vec3.dot(up, zForward)) < 0.999) {
90
+ return zForward;
91
+ }
92
+ return vec3.fromValues(1, 0, 0);
93
+ }
94
+ function buildViewFromRotation(eye, up, rotation) {
95
+ const cameraWorld = buildCameraWorldMatrix(
96
+ eye,
97
+ getDefaultForwardForUp(up),
98
+ up
99
+ );
100
+ if (!cameraWorld) {
101
+ return mat4.create();
102
+ }
103
+ if (rotation.x !== 0) {
104
+ mat4.rotateX(cameraWorld, cameraWorld, toRad(rotation.x));
105
+ }
106
+ if (rotation.y !== 0) {
107
+ mat4.rotateY(cameraWorld, cameraWorld, toRad(rotation.y));
108
+ }
109
+ if (rotation.z !== 0) {
110
+ mat4.rotateZ(cameraWorld, cameraWorld, toRad(rotation.z));
111
+ }
112
+ const view = mat4.create();
113
+ mat4.invert(view, cameraWorld);
114
+ return view;
115
+ }
116
+ function buildViewFromLookAt(eye, center, up) {
117
+ const forward = vec3.subtract(vec3.create(), center, eye);
118
+ if (vec3.squaredLength(forward) === 0) {
119
+ return mat4.create();
120
+ }
121
+ const cameraWorld = buildCameraWorldMatrix(eye, forward, up);
122
+ if (!cameraWorld) {
123
+ return mat4.create();
124
+ }
125
+ const view = mat4.create();
126
+ mat4.invert(view, cameraWorld);
127
+ return view;
128
+ }
129
+ function getAutoDistance(drawCalls, fovDeg) {
130
+ const aabb = computeWorldAABB(drawCalls);
131
+ const diag = vec3.distance(
132
+ vec3.fromValues(aabb.min[0], aabb.min[1], aabb.min[2]),
133
+ vec3.fromValues(aabb.max[0], aabb.max[1], aabb.max[2])
134
+ );
135
+ const radius = diag * 0.5;
136
+ const fov = toRad(fovDeg);
137
+ return radius / Math.tan(fov * 0.5) + radius * 0.5;
138
+ }
139
+ function getAABBCornerDistances(aabb, eye) {
140
+ const xs = [aabb.min[0], aabb.max[0]];
141
+ const ys = [aabb.min[1], aabb.max[1]];
142
+ const zs = [aabb.min[2], aabb.max[2]];
143
+ let nearest = Infinity;
144
+ let farthest = 0;
145
+ for (const x of xs) {
146
+ for (const y of ys) {
147
+ for (const z of zs) {
148
+ const distance = vec3.distance(eye, vec3.fromValues(x, y, z));
149
+ nearest = Math.min(nearest, distance);
150
+ farthest = Math.max(farthest, distance);
151
+ }
58
152
  }
59
- } else {
60
- const aabb = computeWorldAABB(drawCalls);
61
- center = vec3.fromValues(
62
- 0.5 * (aabb.min[0] + aabb.max[0]),
63
- 0.5 * (aabb.min[1] + aabb.max[1]),
64
- 0.5 * (aabb.min[2] + aabb.max[2])
65
- );
66
- const diag = vec3.distance(
67
- vec3.fromValues(aabb.min[0], aabb.min[1], aabb.min[2]),
68
- vec3.fromValues(aabb.max[0], aabb.max[1], aabb.max[2])
153
+ }
154
+ return { nearest, farthest };
155
+ }
156
+ function getCameraForwardFromView(view) {
157
+ const cameraWorld = mat4.create();
158
+ mat4.invert(cameraWorld, view);
159
+ return vec3.fromValues(-cameraWorld[8], -cameraWorld[9], -cameraWorld[10]);
160
+ }
161
+ function buildCamera(drawCalls, width, height, fovDeg, camPos, lookAt, up = "y+", cameraRotation = null) {
162
+ const aspect = width / height;
163
+ const worldUp = getWorldUpVector(up);
164
+ const aabb = computeWorldAABB(drawCalls);
165
+ const center = vec3.fromValues(
166
+ 0.5 * (aabb.min[0] + aabb.max[0]),
167
+ 0.5 * (aabb.min[1] + aabb.max[1]),
168
+ 0.5 * (aabb.min[2] + aabb.max[2])
169
+ );
170
+ const autoDistance = getAutoDistance(drawCalls, fovDeg);
171
+ if (cameraRotation) {
172
+ const provisionalEye = camPos ? vec3.fromValues(camPos[0], camPos[1], camPos[2]) : vec3.clone(center);
173
+ const provisionalView = buildViewFromRotation(
174
+ provisionalEye,
175
+ worldUp,
176
+ cameraRotation
69
177
  );
70
- const radius = diag * 0.5;
71
- const fov = toRad(fovDeg);
72
- const dist = radius / Math.tan(fov * 0.5) + radius * 0.5;
73
- eye = vec3.fromValues(
74
- center[0] + dist,
75
- center[1] + dist * 0.3,
76
- center[2] + dist
178
+ const eye2 = camPos ? provisionalEye : vec3.scaleAndAdd(
179
+ vec3.create(),
180
+ center,
181
+ getCameraForwardFromView(provisionalView),
182
+ -autoDistance
77
183
  );
184
+ const { nearest: nearest2, farthest: farthest2 } = getAABBCornerDistances(aabb, eye2);
185
+ const near2 = Math.max(0.01, nearest2 * 0.5);
186
+ const far2 = Math.max(1e3, farthest2 * 1.1);
187
+ const proj2 = mat4.create();
188
+ mat4.perspective(proj2, toRad(fovDeg), aspect, near2, far2);
189
+ return {
190
+ view: buildViewFromRotation(eye2, worldUp, cameraRotation),
191
+ proj: proj2
192
+ };
78
193
  }
79
- const up = vec3.fromValues(0, 1, 0);
80
- const view = mat4.create();
81
- mat4.lookAt(view, eye, center, up);
194
+ const eye = camPos ? vec3.fromValues(camPos[0], camPos[1], camPos[2]) : vec3.fromValues(
195
+ center[0] + autoDistance,
196
+ center[1] + autoDistance * 0.3,
197
+ center[2] + autoDistance
198
+ );
199
+ const target = lookAt ? vec3.fromValues(lookAt[0], lookAt[1], lookAt[2]) : center;
200
+ const { nearest, farthest } = getAABBCornerDistances(aabb, eye);
201
+ const near = Math.max(0.01, nearest * 0.5);
202
+ const far = Math.max(1e3, farthest * 1.1);
203
+ const proj = mat4.create();
204
+ mat4.perspective(proj, toRad(fovDeg), aspect, near, far);
205
+ const view = buildViewFromLookAt(eye, target, worldUp);
82
206
  return { view, proj };
83
207
  }
84
208
 
@@ -274,6 +398,8 @@ var DEFAULT_RENDER_OPTIONS = {
274
398
  lightDir: DEFAULT_LIGHT_DIR,
275
399
  camPos: null,
276
400
  lookAt: null,
401
+ up: "y+",
402
+ cameraRotation: null,
277
403
  backgroundColor: null,
278
404
  grid: false,
279
405
  debugPoints: null,
@@ -484,8 +610,8 @@ var SoftwareRenderer = class {
484
610
  const ndcX = c[0] * invW;
485
611
  const ndcY = c[1] * invW;
486
612
  const ndcZ = c[2] * invW;
487
- const sx = Math.round((ndcX * 0.5 + 0.5) * (this.width - 1));
488
- const sy = Math.round((1 - (ndcY * 0.5 + 0.5)) * (this.height - 1));
613
+ const sx = (ndcX * 0.5 + 0.5) * (this.width - 1);
614
+ const sy = (1 - (ndcY * 0.5 + 0.5)) * (this.height - 1);
489
615
  vScreen[i] = [sx, sy];
490
616
  vInvW[i] = invW;
491
617
  vNDCz[i] = ndcZ;
@@ -515,10 +641,16 @@ var SoftwareRenderer = class {
515
641
  const area = edge(v0, v1, v2);
516
642
  if (area === 0) continue;
517
643
  if (cullBackFaces && area < 0) continue;
518
- let minX = Math.max(0, Math.min(v0[0], v1[0], v2[0]) | 0);
519
- let maxX = Math.min(this.width - 1, Math.max(v0[0], v1[0], v2[0]) | 0);
520
- let minY = Math.max(0, Math.min(v0[1], v1[1], v2[1]) | 0);
521
- let maxY = Math.min(this.height - 1, Math.max(v0[1], v1[1], v2[1]) | 0);
644
+ const minX = Math.max(0, Math.floor(Math.min(v0[0], v1[0], v2[0])));
645
+ const maxX = Math.min(
646
+ this.width - 1,
647
+ Math.ceil(Math.max(v0[0], v1[0], v2[0]))
648
+ );
649
+ const minY = Math.max(0, Math.floor(Math.min(v0[1], v1[1], v2[1])));
650
+ const maxY = Math.min(
651
+ this.height - 1,
652
+ Math.ceil(Math.max(v0[1], v1[1], v2[1]))
653
+ );
522
654
  const invW = [
523
655
  vInvW[i0],
524
656
  vInvW[i1],
@@ -557,6 +689,7 @@ var SoftwareRenderer = class {
557
689
  const l2 = w2 * invArea;
558
690
  const zndc = l0 * ndcZ[0] + l1 * ndcZ[1] + l2 * ndcZ[2];
559
691
  const z01 = zndc * 0.5 + 0.5;
692
+ if (z01 < 0 || z01 > 1) continue;
560
693
  const di = y * this.width + x;
561
694
  const depth = this.depth;
562
695
  if (z01 >= depth[di]) continue;
@@ -973,6 +1106,21 @@ function drawInfiniteGrid(software_renderer, params) {
973
1106
  }
974
1107
 
975
1108
  // lib/render/resolveRenderOptions.ts
1109
+ var CAMERA_UP_VALUES = /* @__PURE__ */ new Set(["y+", "y-", "x+", "x-", "z+", "z-"]);
1110
+ function resolveCameraUp(up) {
1111
+ return up != null && CAMERA_UP_VALUES.has(up) ? up : DEFAULT_RENDER_OPTIONS.up;
1112
+ }
1113
+ function resolveCameraRotation(cameraRotation) {
1114
+ if (cameraRotation == null) {
1115
+ return DEFAULT_RENDER_OPTIONS.cameraRotation;
1116
+ }
1117
+ const resolveAxis = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
1118
+ return {
1119
+ x: resolveAxis(cameraRotation.x),
1120
+ y: resolveAxis(cameraRotation.y),
1121
+ z: resolveAxis(cameraRotation.z)
1122
+ };
1123
+ }
976
1124
  function resolveRenderOptions(options = {}) {
977
1125
  const supersampling = typeof options.supersampling === "number" && Number.isFinite(options.supersampling) ? Math.max(1, Math.floor(options.supersampling)) : DEFAULT_RENDER_OPTIONS.supersampling;
978
1126
  return {
@@ -981,7 +1129,9 @@ function resolveRenderOptions(options = {}) {
981
1129
  supersampling,
982
1130
  lightDir: options.lightDir != null ? options.lightDir : DEFAULT_RENDER_OPTIONS.lightDir,
983
1131
  camPos: options.camPos !== void 0 ? options.camPos : DEFAULT_RENDER_OPTIONS.camPos,
984
- lookAt: options.lookAt !== void 0 ? options.lookAt : DEFAULT_RENDER_OPTIONS.lookAt
1132
+ lookAt: options.lookAt !== void 0 ? options.lookAt : DEFAULT_RENDER_OPTIONS.lookAt,
1133
+ up: resolveCameraUp(options.up),
1134
+ cameraRotation: resolveCameraRotation(options.cameraRotation)
985
1135
  };
986
1136
  }
987
1137
 
@@ -996,7 +1146,9 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
996
1146
  renderHeight,
997
1147
  options.fov,
998
1148
  options.camPos ?? null,
999
- options.lookAt ?? null
1149
+ options.lookAt ?? null,
1150
+ options.up,
1151
+ options.cameraRotation ?? null
1000
1152
  );
1001
1153
  const renderer = new SoftwareRenderer(renderWidth, renderHeight, imageFactory);
1002
1154
  let rgb = null;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "poppygl",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.19",
4
+ "version": "0.0.20",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/tscircuit/poppygl.git"
@@ -20,7 +20,7 @@
20
20
  "type": "module",
21
21
  "devDependencies": {
22
22
  "@biomejs/biome": "^2.2.4",
23
- "@tscircuit/alphabet": "^0.0.22",
23
+ "@tscircuit/alphabet": "0.0.23",
24
24
  "@types/bun": "^1.2.23",
25
25
  "@types/readable-stream": "^4.0.21",
26
26
  "looks-same": "^10.0.1",