poppygl 0.0.17 → 0.0.19
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 +5 -0
- package/dist/index.d.ts +14 -1
- package/dist/index.js +307 -67
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -50,8 +50,13 @@ const png = await renderGLTFToPNGBufferFromGLBBuffer(glb, {
|
|
|
50
50
|
`renderGLTFToPNGBufferFromURL` accepts the same render options as the lower-level APIs:
|
|
51
51
|
|
|
52
52
|
- `width`/`height` (default `512`): output resolution in pixels.
|
|
53
|
+
- `supersampling`: render at `width * supersampling` / `height * supersampling`, then downsample (default `1`).
|
|
53
54
|
- `fov`: vertical field of view in degrees (defaults to `35`).
|
|
54
55
|
- `camPos` and `lookAt`: override the auto-framed camera position and target.
|
|
56
|
+
- `debugPoints`: optional `{ label, position }[]` overlay rendered on top of the final PNG for world-space debugging markers.
|
|
57
|
+
- `debugFontSize`: optional pixel size for `debugPoints` labels; when omitted the renderer derives a size from the image dimensions.
|
|
58
|
+
- `debugPointColor`: optional RGB tuple for debug point markers.
|
|
59
|
+
- `debugLabelColor`: optional RGB tuple for debug point labels.
|
|
55
60
|
- `lightDir`: normalized directional light vector (defaults to a top-right key light).
|
|
56
61
|
- `ambient`: ambient lighting contribution (0–1, defaults to `0.2`).
|
|
57
62
|
- `gamma`: gamma correction applied to the output (defaults to `2.2`).
|
package/dist/index.d.ts
CHANGED
|
@@ -53,6 +53,14 @@ interface GLTFScene {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
declare const DEFAULT_LIGHT_DIR: readonly [-0.4, -0.9, -0.2];
|
|
56
|
+
interface DebugPoint {
|
|
57
|
+
label: string;
|
|
58
|
+
position: {
|
|
59
|
+
x: number;
|
|
60
|
+
y: number;
|
|
61
|
+
z: number;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
56
64
|
/**
|
|
57
65
|
* Convert a hex color string to RGB array with values 0-1
|
|
58
66
|
* @param hex - Hex color string like "#ffffff" or "ffffff"
|
|
@@ -62,6 +70,7 @@ declare function hexToRgb(hex: string): [number, number, number] | null;
|
|
|
62
70
|
interface RenderOptions {
|
|
63
71
|
width: number;
|
|
64
72
|
height: number;
|
|
73
|
+
supersampling: number;
|
|
65
74
|
fov: number;
|
|
66
75
|
cull: boolean;
|
|
67
76
|
gamma: boolean;
|
|
@@ -71,6 +80,10 @@ interface RenderOptions {
|
|
|
71
80
|
lookAt?: readonly [number, number, number] | null;
|
|
72
81
|
backgroundColor?: readonly [number, number, number] | string | null;
|
|
73
82
|
grid?: boolean | GridOptions;
|
|
83
|
+
debugPoints?: DebugPoint[] | null;
|
|
84
|
+
debugFontSize?: number | null;
|
|
85
|
+
debugPointColor?: readonly [number, number, number] | null;
|
|
86
|
+
debugLabelColor?: readonly [number, number, number] | null;
|
|
74
87
|
}
|
|
75
88
|
type RenderOptionsInput = Partial<RenderOptions>;
|
|
76
89
|
declare const DEFAULT_RENDER_OPTIONS: RenderOptions;
|
|
@@ -187,4 +200,4 @@ declare function renderGLTFToPNGBuffer(gltfPathOrString: string, options?: Rende
|
|
|
187
200
|
|
|
188
201
|
declare function renderSceneFromGLTF(scene: GLTFScene, options?: RenderOptionsInput, imageFactory?: ImageFactory): RenderResult;
|
|
189
202
|
|
|
190
|
-
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, createInfiniteGrid, createSceneFromGLTF, createUint8Bitmap, decodeImageFromBuffer, drawInfiniteGrid, encodePNGToBuffer, getDefaultRenderOptions, hexToRgb, isJPG, isPNG, loadGLTFWithResourcesFromURL, pureImageFactory, renderDrawCalls, renderGLTFToPNGBuffer, renderGLTFToPNGBufferFromGLBBuffer, renderGLTFToPNGBufferFromURL, renderSceneFromGLTF, resolveRenderOptions };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -132,35 +132,56 @@ var createUint8Bitmap = (width, height) => ({
|
|
|
132
132
|
data: new Uint8ClampedArray(width * height * 4)
|
|
133
133
|
});
|
|
134
134
|
|
|
135
|
-
// lib/render/
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
135
|
+
// lib/render/downsampleBitmap.ts
|
|
136
|
+
function downsampleBitmap(source, targetWidth, targetHeight, imageFactory) {
|
|
137
|
+
const target = imageFactory(targetWidth, targetHeight);
|
|
138
|
+
const scaleX = source.width / targetWidth;
|
|
139
|
+
const scaleY = source.height / targetHeight;
|
|
140
|
+
for (let y = 0; y < targetHeight; y++) {
|
|
141
|
+
const srcY0 = Math.floor(y * scaleY);
|
|
142
|
+
const srcY1 = Math.max(
|
|
143
|
+
srcY0 + 1,
|
|
144
|
+
Math.min(source.height, Math.floor((y + 1) * scaleY))
|
|
145
|
+
);
|
|
146
|
+
for (let x = 0; x < targetWidth; x++) {
|
|
147
|
+
const srcX0 = Math.floor(x * scaleX);
|
|
148
|
+
const srcX1 = Math.max(
|
|
149
|
+
srcX0 + 1,
|
|
150
|
+
Math.min(source.width, Math.floor((x + 1) * scaleX))
|
|
151
|
+
);
|
|
152
|
+
let r = 0;
|
|
153
|
+
let g = 0;
|
|
154
|
+
let b = 0;
|
|
155
|
+
let a = 0;
|
|
156
|
+
let count = 0;
|
|
157
|
+
for (let sy = srcY0; sy < srcY1; sy++) {
|
|
158
|
+
for (let sx = srcX0; sx < srcX1; sx++) {
|
|
159
|
+
const srcIdx = (sy * source.width + sx) * 4;
|
|
160
|
+
r += source.data[srcIdx + 0];
|
|
161
|
+
g += source.data[srcIdx + 1];
|
|
162
|
+
b += source.data[srcIdx + 2];
|
|
163
|
+
a += source.data[srcIdx + 3];
|
|
164
|
+
count++;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const dstIdx = (y * targetWidth + x) * 4;
|
|
168
|
+
target.data[dstIdx + 0] = Math.round(r / count);
|
|
169
|
+
target.data[dstIdx + 1] = Math.round(g / count);
|
|
170
|
+
target.data[dstIdx + 2] = Math.round(b / count);
|
|
171
|
+
target.data[dstIdx + 3] = Math.round(a / count);
|
|
172
|
+
}
|
|
141
173
|
}
|
|
142
|
-
|
|
143
|
-
const g = Number.parseInt(cleanHex.substring(2, 4), 16) / 255;
|
|
144
|
-
const b = Number.parseInt(cleanHex.substring(4, 6), 16) / 255;
|
|
145
|
-
return [r, g, b];
|
|
146
|
-
}
|
|
147
|
-
var DEFAULT_RENDER_OPTIONS = {
|
|
148
|
-
width: 800,
|
|
149
|
-
height: 600,
|
|
150
|
-
fov: 60,
|
|
151
|
-
cull: true,
|
|
152
|
-
gamma: true,
|
|
153
|
-
ambient: 0.15,
|
|
154
|
-
lightDir: DEFAULT_LIGHT_DIR,
|
|
155
|
-
camPos: null,
|
|
156
|
-
lookAt: null,
|
|
157
|
-
backgroundColor: null,
|
|
158
|
-
grid: false
|
|
159
|
-
};
|
|
160
|
-
function getDefaultRenderOptions() {
|
|
161
|
-
return { ...DEFAULT_RENDER_OPTIONS };
|
|
174
|
+
return target;
|
|
162
175
|
}
|
|
163
176
|
|
|
177
|
+
// lib/render/drawDebugPoints.ts
|
|
178
|
+
import {
|
|
179
|
+
glyphAdvanceRatio,
|
|
180
|
+
glyphLineAlphabet,
|
|
181
|
+
textMetrics
|
|
182
|
+
} from "@tscircuit/alphabet";
|
|
183
|
+
import { mat4 as mat44, vec4 as vec43 } from "gl-matrix";
|
|
184
|
+
|
|
164
185
|
// lib/render/SoftwareRenderer.ts
|
|
165
186
|
import { mat3, mat4 as mat43, vec3 as vec32, vec4 as vec42 } from "gl-matrix";
|
|
166
187
|
|
|
@@ -230,6 +251,40 @@ function computeSmoothNormals(positions, indices) {
|
|
|
230
251
|
return normals;
|
|
231
252
|
}
|
|
232
253
|
|
|
254
|
+
// lib/render/getDefaultRenderOptions.ts
|
|
255
|
+
var DEFAULT_LIGHT_DIR = [-0.4, -0.9, -0.2];
|
|
256
|
+
function hexToRgb(hex) {
|
|
257
|
+
const cleanHex = hex.replace(/^#/, "");
|
|
258
|
+
if (!/^[0-9A-Fa-f]{6}$/.test(cleanHex)) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
const r = Number.parseInt(cleanHex.substring(0, 2), 16) / 255;
|
|
262
|
+
const g = Number.parseInt(cleanHex.substring(2, 4), 16) / 255;
|
|
263
|
+
const b = Number.parseInt(cleanHex.substring(4, 6), 16) / 255;
|
|
264
|
+
return [r, g, b];
|
|
265
|
+
}
|
|
266
|
+
var DEFAULT_RENDER_OPTIONS = {
|
|
267
|
+
width: 800,
|
|
268
|
+
height: 600,
|
|
269
|
+
supersampling: 1,
|
|
270
|
+
fov: 60,
|
|
271
|
+
cull: true,
|
|
272
|
+
gamma: true,
|
|
273
|
+
ambient: 0.15,
|
|
274
|
+
lightDir: DEFAULT_LIGHT_DIR,
|
|
275
|
+
camPos: null,
|
|
276
|
+
lookAt: null,
|
|
277
|
+
backgroundColor: null,
|
|
278
|
+
grid: false,
|
|
279
|
+
debugPoints: null,
|
|
280
|
+
debugFontSize: null,
|
|
281
|
+
debugPointColor: null,
|
|
282
|
+
debugLabelColor: null
|
|
283
|
+
};
|
|
284
|
+
function getDefaultRenderOptions() {
|
|
285
|
+
return { ...DEFAULT_RENDER_OPTIONS };
|
|
286
|
+
}
|
|
287
|
+
|
|
233
288
|
// lib/utils/mulColor.ts
|
|
234
289
|
function mulColor(a, b) {
|
|
235
290
|
return [a[0] * b[0], a[1] * b[1], a[2] * b[2], a[3] * b[3]];
|
|
@@ -603,19 +658,177 @@ function edge(a, b, p) {
|
|
|
603
658
|
return (p[0] - a[0]) * (b[1] - a[1]) - (p[1] - a[1]) * (b[0] - a[0]);
|
|
604
659
|
}
|
|
605
660
|
|
|
606
|
-
// lib/render/
|
|
607
|
-
|
|
661
|
+
// lib/render/drawDebugPoints.ts
|
|
662
|
+
var DEFAULT_MARKER_COLOR = [
|
|
663
|
+
255,
|
|
664
|
+
0,
|
|
665
|
+
170,
|
|
666
|
+
255
|
|
667
|
+
];
|
|
668
|
+
var DEFAULT_TEXT_COLOR = [
|
|
669
|
+
255,
|
|
670
|
+
24,
|
|
671
|
+
170,
|
|
672
|
+
255
|
|
673
|
+
];
|
|
674
|
+
var CHARACTER_WIDTH_RATIO = 0.62;
|
|
675
|
+
function drawDebugPoints(renderer, camera, debugPoints, debugFontSize, debugPointColor, debugLabelColor) {
|
|
676
|
+
if (debugPoints.length === 0) return;
|
|
677
|
+
const viewProj = mat44.create();
|
|
678
|
+
mat44.multiply(viewProj, camera.proj, camera.view);
|
|
679
|
+
const markerColor = toColorRGBA(debugPointColor, DEFAULT_MARKER_COLOR);
|
|
680
|
+
const textColor = toColorRGBA(debugLabelColor, DEFAULT_TEXT_COLOR);
|
|
681
|
+
const labelScale = typeof debugFontSize === "number" && Number.isFinite(debugFontSize) ? Math.max(1, Math.round(debugFontSize)) : Math.max(
|
|
682
|
+
12,
|
|
683
|
+
Math.round(Math.min(renderer.width, renderer.height) * 0.028)
|
|
684
|
+
);
|
|
685
|
+
const textThickness = Math.max(
|
|
686
|
+
2,
|
|
687
|
+
Math.round(labelScale * textMetrics.strokeWidthRatio)
|
|
688
|
+
);
|
|
689
|
+
for (const debugPoint of debugPoints) {
|
|
690
|
+
const projected = projectWorldToScreen(
|
|
691
|
+
debugPoint.position,
|
|
692
|
+
viewProj,
|
|
693
|
+
renderer
|
|
694
|
+
);
|
|
695
|
+
if (!projected) continue;
|
|
696
|
+
drawMarker(renderer, projected, markerColor);
|
|
697
|
+
drawLabel(
|
|
698
|
+
renderer,
|
|
699
|
+
projected,
|
|
700
|
+
debugPoint.label,
|
|
701
|
+
textColor,
|
|
702
|
+
labelScale,
|
|
703
|
+
textThickness
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
function projectWorldToScreen(position, viewProj, renderer) {
|
|
708
|
+
const clip = vec43.fromValues(position.x, position.y, position.z, 1);
|
|
709
|
+
vec43.transformMat4(clip, clip, viewProj);
|
|
710
|
+
if (!Number.isFinite(clip[3]) || clip[3] <= 0) return null;
|
|
711
|
+
const invW = 1 / clip[3];
|
|
712
|
+
const ndcX = clip[0] * invW;
|
|
713
|
+
const ndcY = clip[1] * invW;
|
|
714
|
+
const ndcZ = clip[2] * invW;
|
|
715
|
+
if (!Number.isFinite(ndcX) || !Number.isFinite(ndcY) || !Number.isFinite(ndcZ) || ndcZ < -1 || ndcZ > 1) {
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
608
718
|
return {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
lightDir: options.lightDir != null ? options.lightDir : DEFAULT_RENDER_OPTIONS.lightDir,
|
|
612
|
-
camPos: options.camPos !== void 0 ? options.camPos : DEFAULT_RENDER_OPTIONS.camPos,
|
|
613
|
-
lookAt: options.lookAt !== void 0 ? options.lookAt : DEFAULT_RENDER_OPTIONS.lookAt
|
|
719
|
+
x: (ndcX * 0.5 + 0.5) * (renderer.width - 1),
|
|
720
|
+
y: (1 - (ndcY * 0.5 + 0.5)) * (renderer.height - 1)
|
|
614
721
|
};
|
|
615
722
|
}
|
|
723
|
+
function drawMarker(renderer, projected, color) {
|
|
724
|
+
const centerX = Math.round(projected.x);
|
|
725
|
+
const centerY = Math.round(projected.y);
|
|
726
|
+
for (let offset = -5; offset <= 4; offset += 1) {
|
|
727
|
+
renderer.setPixel(
|
|
728
|
+
centerX + offset,
|
|
729
|
+
centerY,
|
|
730
|
+
color[0],
|
|
731
|
+
color[1],
|
|
732
|
+
color[2],
|
|
733
|
+
color[3]
|
|
734
|
+
);
|
|
735
|
+
renderer.setPixel(
|
|
736
|
+
centerX,
|
|
737
|
+
centerY + offset,
|
|
738
|
+
color[0],
|
|
739
|
+
color[1],
|
|
740
|
+
color[2],
|
|
741
|
+
color[3]
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
function drawLabel(renderer, projected, label, color, scale, textThickness) {
|
|
746
|
+
if (label.length === 0) return;
|
|
747
|
+
const scaleX = scale * CHARACTER_WIDTH_RATIO;
|
|
748
|
+
const labelWidth = measureLabelWidth(label, scaleX);
|
|
749
|
+
let x = projected.x + scale * 0.6;
|
|
750
|
+
let y = projected.y - scale * 1.1;
|
|
751
|
+
if (x + labelWidth > renderer.width - 2) {
|
|
752
|
+
x = projected.x - scale * 0.6 - labelWidth;
|
|
753
|
+
}
|
|
754
|
+
if (y < 2) {
|
|
755
|
+
y = projected.y + scale * 0.2;
|
|
756
|
+
}
|
|
757
|
+
drawStrokeText(renderer, label, x, y, scaleX, scale, color, textThickness);
|
|
758
|
+
}
|
|
759
|
+
function drawStrokeText(renderer, text, x, y, scaleX, scaleY, color, thickness) {
|
|
760
|
+
let cursorX = x;
|
|
761
|
+
for (const rawChar of text) {
|
|
762
|
+
const char = resolveGlyph(rawChar);
|
|
763
|
+
const glyph = glyphLineAlphabet[char];
|
|
764
|
+
const advanceRatio = glyphAdvanceRatio[char] ?? (char === " " ? textMetrics.spaceWidthRatio : textMetrics.glyphWidthRatio);
|
|
765
|
+
if (glyph) {
|
|
766
|
+
for (const segment of glyph) {
|
|
767
|
+
drawLine(
|
|
768
|
+
renderer,
|
|
769
|
+
cursorX + segment.x1 * scaleX,
|
|
770
|
+
y + (1 - segment.y1) * scaleY,
|
|
771
|
+
cursorX + segment.x2 * scaleX,
|
|
772
|
+
y + (1 - segment.y2) * scaleY,
|
|
773
|
+
color,
|
|
774
|
+
thickness
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
cursorX += advanceRatio * scaleX;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
function resolveGlyph(char) {
|
|
782
|
+
if (glyphLineAlphabet[char]) return char;
|
|
783
|
+
const uppercase = char.toUpperCase();
|
|
784
|
+
if (glyphLineAlphabet[uppercase]) return uppercase;
|
|
785
|
+
return "?";
|
|
786
|
+
}
|
|
787
|
+
function measureLabelWidth(text, scaleX) {
|
|
788
|
+
let width = 0;
|
|
789
|
+
for (const rawChar of text) {
|
|
790
|
+
const char = resolveGlyph(rawChar);
|
|
791
|
+
width += glyphAdvanceRatio[char] ?? (char === " " ? textMetrics.spaceWidthRatio : textMetrics.glyphWidthRatio);
|
|
792
|
+
}
|
|
793
|
+
return width * scaleX;
|
|
794
|
+
}
|
|
795
|
+
function drawLine(renderer, x0, y0, x1, y1, color, thickness) {
|
|
796
|
+
const dx = x1 - x0;
|
|
797
|
+
const dy = y1 - y0;
|
|
798
|
+
const steps = Math.max(Math.abs(dx), Math.abs(dy));
|
|
799
|
+
if (steps === 0) {
|
|
800
|
+
drawBrush(renderer, x0, y0, color, thickness);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
for (let step = 0; step <= steps; step += 1) {
|
|
804
|
+
const t = step / steps;
|
|
805
|
+
drawBrush(renderer, x0 + dx * t, y0 + dy * t, color, thickness);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
function drawBrush(renderer, x, y, color, thickness) {
|
|
809
|
+
const radius = Math.max(0, Math.floor((thickness - 1) / 2));
|
|
810
|
+
const centerX = Math.round(x);
|
|
811
|
+
const centerY = Math.round(y);
|
|
812
|
+
for (let offsetY = -radius; offsetY <= radius; offsetY += 1) {
|
|
813
|
+
for (let offsetX = -radius; offsetX <= radius; offsetX += 1) {
|
|
814
|
+
renderer.setPixel(
|
|
815
|
+
centerX + offsetX,
|
|
816
|
+
centerY + offsetY,
|
|
817
|
+
color[0],
|
|
818
|
+
color[1],
|
|
819
|
+
color[2],
|
|
820
|
+
color[3]
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
function toColorRGBA(color, fallback) {
|
|
826
|
+
if (!color) return fallback;
|
|
827
|
+
return [color[0], color[1], color[2], 255];
|
|
828
|
+
}
|
|
616
829
|
|
|
617
830
|
// lib/render/drawInfiniteGrid.ts
|
|
618
|
-
import { mat4 as
|
|
831
|
+
import { mat4 as mat45, vec4 as vec44 } from "gl-matrix";
|
|
619
832
|
function computeGridLineIntensity(params) {
|
|
620
833
|
const rx = params.world_x / params.cell_size;
|
|
621
834
|
const rz = params.world_z / params.cell_size;
|
|
@@ -651,23 +864,23 @@ function drawInfiniteGrid(software_renderer, params) {
|
|
|
651
864
|
const camera = params.camera;
|
|
652
865
|
const view = camera.view;
|
|
653
866
|
const proj = camera.proj;
|
|
654
|
-
const inv_view =
|
|
655
|
-
|
|
867
|
+
const inv_view = mat45.create();
|
|
868
|
+
mat45.invert(inv_view, view);
|
|
656
869
|
const cam_pos_x = inv_view[12];
|
|
657
870
|
const cam_pos_y = inv_view[13];
|
|
658
871
|
const cam_pos_z = inv_view[14];
|
|
659
|
-
const vp =
|
|
660
|
-
|
|
661
|
-
const inv_vp =
|
|
662
|
-
|
|
872
|
+
const vp = mat45.create();
|
|
873
|
+
mat45.multiply(vp, proj, view);
|
|
874
|
+
const inv_vp = mat45.create();
|
|
875
|
+
mat45.invert(inv_vp, vp);
|
|
663
876
|
for (let y = 0; y < software_renderer.height; y++) {
|
|
664
877
|
for (let x = 0; x < software_renderer.width; x++) {
|
|
665
878
|
const ndc_x = x / (software_renderer.width - 1) * 2 - 1;
|
|
666
879
|
const ndc_y = 1 - y / (software_renderer.height - 1) * 2;
|
|
667
|
-
const near_point =
|
|
668
|
-
const far_point =
|
|
669
|
-
|
|
670
|
-
|
|
880
|
+
const near_point = vec44.fromValues(ndc_x, ndc_y, -1, 1);
|
|
881
|
+
const far_point = vec44.fromValues(ndc_x, ndc_y, 1, 1);
|
|
882
|
+
vec44.transformMat4(near_point, near_point, inv_vp);
|
|
883
|
+
vec44.transformMat4(far_point, far_point, inv_vp);
|
|
671
884
|
near_point[0] /= near_point[3];
|
|
672
885
|
near_point[1] /= near_point[3];
|
|
673
886
|
near_point[2] /= near_point[3];
|
|
@@ -682,9 +895,9 @@ function drawInfiniteGrid(software_renderer, params) {
|
|
|
682
895
|
if (t < 0 || t > 1) continue;
|
|
683
896
|
const world_x = near_point[0] + t * ray_dir_x;
|
|
684
897
|
const world_z = near_point[2] + t * ray_dir_z;
|
|
685
|
-
const world_pt =
|
|
686
|
-
const clip_pt =
|
|
687
|
-
|
|
898
|
+
const world_pt = vec44.fromValues(world_x, grid_y, world_z, 1);
|
|
899
|
+
const clip_pt = vec44.create();
|
|
900
|
+
vec44.transformMat4(clip_pt, world_pt, vp);
|
|
688
901
|
const ndc_z = clip_pt[2] / clip_pt[3];
|
|
689
902
|
const depth_01 = ndc_z * 0.5 + 0.5;
|
|
690
903
|
const di = y * software_renderer.width + x;
|
|
@@ -759,22 +972,33 @@ function drawInfiniteGrid(software_renderer, params) {
|
|
|
759
972
|
}
|
|
760
973
|
}
|
|
761
974
|
|
|
975
|
+
// lib/render/resolveRenderOptions.ts
|
|
976
|
+
function resolveRenderOptions(options = {}) {
|
|
977
|
+
const supersampling = typeof options.supersampling === "number" && Number.isFinite(options.supersampling) ? Math.max(1, Math.floor(options.supersampling)) : DEFAULT_RENDER_OPTIONS.supersampling;
|
|
978
|
+
return {
|
|
979
|
+
...DEFAULT_RENDER_OPTIONS,
|
|
980
|
+
...options,
|
|
981
|
+
supersampling,
|
|
982
|
+
lightDir: options.lightDir != null ? options.lightDir : DEFAULT_RENDER_OPTIONS.lightDir,
|
|
983
|
+
camPos: options.camPos !== void 0 ? options.camPos : DEFAULT_RENDER_OPTIONS.camPos,
|
|
984
|
+
lookAt: options.lookAt !== void 0 ? options.lookAt : DEFAULT_RENDER_OPTIONS.lookAt
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
|
|
762
988
|
// lib/render/renderDrawCalls.ts
|
|
763
989
|
function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint8Bitmap) {
|
|
764
990
|
const options = resolveRenderOptions(optionsInput);
|
|
991
|
+
const renderWidth = options.width * options.supersampling;
|
|
992
|
+
const renderHeight = options.height * options.supersampling;
|
|
765
993
|
const camera = buildCamera(
|
|
766
994
|
drawCalls,
|
|
767
|
-
|
|
768
|
-
|
|
995
|
+
renderWidth,
|
|
996
|
+
renderHeight,
|
|
769
997
|
options.fov,
|
|
770
998
|
options.camPos ?? null,
|
|
771
999
|
options.lookAt ?? null
|
|
772
1000
|
);
|
|
773
|
-
const renderer = new SoftwareRenderer(
|
|
774
|
-
options.width,
|
|
775
|
-
options.height,
|
|
776
|
-
imageFactory
|
|
777
|
-
);
|
|
1001
|
+
const renderer = new SoftwareRenderer(renderWidth, renderHeight, imageFactory);
|
|
778
1002
|
let rgb = null;
|
|
779
1003
|
if (options.backgroundColor) {
|
|
780
1004
|
if (typeof options.backgroundColor === "string") {
|
|
@@ -870,11 +1094,27 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
|
|
|
870
1094
|
});
|
|
871
1095
|
}
|
|
872
1096
|
renderGroup(blendDraws);
|
|
873
|
-
|
|
1097
|
+
if (options.debugPoints?.length) {
|
|
1098
|
+
drawDebugPoints(
|
|
1099
|
+
renderer,
|
|
1100
|
+
camera,
|
|
1101
|
+
options.debugPoints,
|
|
1102
|
+
options.debugFontSize,
|
|
1103
|
+
options.debugPointColor,
|
|
1104
|
+
options.debugLabelColor
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
const bitmap = options.supersampling > 1 ? downsampleBitmap(
|
|
1108
|
+
renderer.bitmap,
|
|
1109
|
+
options.width,
|
|
1110
|
+
options.height,
|
|
1111
|
+
imageFactory
|
|
1112
|
+
) : renderer.bitmap;
|
|
1113
|
+
return { bitmap, camera, options };
|
|
874
1114
|
}
|
|
875
1115
|
|
|
876
1116
|
// lib/gltf/createInfiniteGrid.ts
|
|
877
|
-
import { mat4 as
|
|
1117
|
+
import { mat4 as mat46 } from "gl-matrix";
|
|
878
1118
|
function createInfiniteGrid(options = {}) {
|
|
879
1119
|
const cellSize = options.cellSize ?? 1;
|
|
880
1120
|
const sectionSize = options.sectionSize ?? 10;
|
|
@@ -898,9 +1138,9 @@ function createInfiniteGrid(options = {}) {
|
|
|
898
1138
|
size
|
|
899
1139
|
]);
|
|
900
1140
|
const indices = new Uint32Array([0, 1, 2, 0, 2, 3]);
|
|
901
|
-
const model =
|
|
1141
|
+
const model = mat46.create();
|
|
902
1142
|
const offset = options.offset ?? {};
|
|
903
|
-
|
|
1143
|
+
mat46.fromTranslation(model, [offset.x ?? 0, offset.y ?? 0, offset.z ?? 0]);
|
|
904
1144
|
return {
|
|
905
1145
|
positions,
|
|
906
1146
|
indices,
|
|
@@ -925,7 +1165,7 @@ function createInfiniteGrid(options = {}) {
|
|
|
925
1165
|
}
|
|
926
1166
|
|
|
927
1167
|
// lib/gltf/createSceneFromGLTF.ts
|
|
928
|
-
import { mat4 as
|
|
1168
|
+
import { mat4 as mat47 } from "gl-matrix";
|
|
929
1169
|
var COMPONENT_INFO = {
|
|
930
1170
|
5120: {
|
|
931
1171
|
name: "BYTE",
|
|
@@ -1123,17 +1363,17 @@ function readIndices(ctx, accessorIndex) {
|
|
|
1123
1363
|
}
|
|
1124
1364
|
function nodeLocalMatrix(node) {
|
|
1125
1365
|
if (node.matrix) {
|
|
1126
|
-
const m2 =
|
|
1366
|
+
const m2 = mat47.create();
|
|
1127
1367
|
for (let i = 0; i < 16; i++) m2[i] = node.matrix[i];
|
|
1128
1368
|
return m2;
|
|
1129
1369
|
}
|
|
1130
1370
|
const translation = node.translation || [0, 0, 0];
|
|
1131
1371
|
const rotation = node.rotation || [0, 0, 0, 1];
|
|
1132
1372
|
const scale = node.scale || [1, 1, 1];
|
|
1133
|
-
const m =
|
|
1134
|
-
const tmp =
|
|
1135
|
-
|
|
1136
|
-
|
|
1373
|
+
const m = mat47.create();
|
|
1374
|
+
const tmp = mat47.create();
|
|
1375
|
+
mat47.fromRotationTranslationScale(tmp, rotation, translation, scale);
|
|
1376
|
+
mat47.copy(m, tmp);
|
|
1137
1377
|
return m;
|
|
1138
1378
|
}
|
|
1139
1379
|
function getMaterial(gltf, textures, images, materialIndex) {
|
|
@@ -1171,8 +1411,8 @@ function createSceneFromGLTF(gltf, resources) {
|
|
|
1171
1411
|
const node = nodes[nodeIndex];
|
|
1172
1412
|
if (!node) return;
|
|
1173
1413
|
const local = nodeLocalMatrix(node);
|
|
1174
|
-
const world =
|
|
1175
|
-
|
|
1414
|
+
const world = mat47.create();
|
|
1415
|
+
mat47.multiply(world, parentMatrix, local);
|
|
1176
1416
|
if (Number.isInteger(node.mesh)) {
|
|
1177
1417
|
const mesh = meshes[node.mesh];
|
|
1178
1418
|
if (mesh) {
|
|
@@ -1210,7 +1450,7 @@ function createSceneFromGLTF(gltf, resources) {
|
|
|
1210
1450
|
traverse(childIndex, world);
|
|
1211
1451
|
}
|
|
1212
1452
|
}
|
|
1213
|
-
const identity =
|
|
1453
|
+
const identity = mat47.create();
|
|
1214
1454
|
for (const rootIndex of scene.nodes || []) {
|
|
1215
1455
|
traverse(rootIndex, identity);
|
|
1216
1456
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "poppygl",
|
|
3
3
|
"main": "dist/index.js",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.19",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/tscircuit/poppygl.git"
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"type": "module",
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@biomejs/biome": "^2.2.4",
|
|
23
|
+
"@tscircuit/alphabet": "^0.0.22",
|
|
23
24
|
"@types/bun": "^1.2.23",
|
|
24
25
|
"@types/readable-stream": "^4.0.21",
|
|
25
26
|
"looks-same": "^10.0.1",
|