poppygl 0.0.29 → 0.0.31
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 +78 -10
- package/dist/index.d.ts +20 -3
- package/dist/index.js +722 -25
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -2,11 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
Render GLTF files to PNG images in completely native JavaScript without WebGL/OpenGL.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
Left to right: regular PoppyGL, PoppyGL with `realistic: true`, and a Blender
|
|
8
|
+
Cycles reference. All three views use the same model, camera, and materials.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm install poppygl
|
|
14
|
+
```
|
|
6
15
|
|
|
7
16
|
## Quick start
|
|
8
17
|
|
|
9
|
-
Give poppygl a `.gltf` or `.glb` URL and it will fetch every referenced buffer/texture, rasterize the scene, and
|
|
18
|
+
Give poppygl a `.gltf` or `.glb` URL and it will fetch every referenced buffer/texture, rasterize the scene, and return the PNG as a `Uint8Array`.
|
|
10
19
|
|
|
11
20
|
```ts
|
|
12
21
|
import { renderGLTFToPNGFromURL } from "poppygl"
|
|
@@ -28,7 +37,7 @@ await writeFile("DamagedHelmet.png", png)
|
|
|
28
37
|
|
|
29
38
|
## Render from an in-memory GLB buffer
|
|
30
39
|
|
|
31
|
-
Already have the `.glb` bytes (for example, uploaded by a user or read from disk)? Send the buffer straight to `renderGLTFToPNGFromGLB` and receive the PNG
|
|
40
|
+
Already have the `.glb` bytes (for example, uploaded by a user or read from disk)? Send the buffer straight to `renderGLTFToPNGFromGLB` and receive the PNG as a `Uint8Array`.
|
|
32
41
|
|
|
33
42
|
```ts
|
|
34
43
|
import { readFile } from "node:fs/promises"
|
|
@@ -40,18 +49,75 @@ const png = await renderGLTFToPNGFromGLB(glb, {
|
|
|
40
49
|
height: 768,
|
|
41
50
|
})
|
|
42
51
|
|
|
43
|
-
|
|
52
|
+
await writeFile("DamagedHelmet.png", png)
|
|
44
53
|
```
|
|
45
54
|
|
|
46
55
|
> This helper expects every referenced buffer/image to be embedded in the GLB (the usual single-file package). If the asset links out to external resources, load it with `renderGLTFToPNGFromURL` instead so poppygl can fetch the extras.
|
|
47
56
|
|
|
48
57
|
## Render options
|
|
49
58
|
|
|
59
|
+
### Realistic studio rendering
|
|
60
|
+
|
|
61
|
+
Pass `realistic: true` for studio lighting, metal reflections, and soft shadows.
|
|
62
|
+
The default is `false`, which uses the faster diffuse renderer.
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { readFile, writeFile } from "node:fs/promises"
|
|
66
|
+
import { renderGLTFToPNGFromGLB } from "poppygl"
|
|
67
|
+
|
|
68
|
+
const glb = await readFile("motor.glb")
|
|
69
|
+
const png = await renderGLTFToPNGFromGLB(glb, {
|
|
70
|
+
realistic: true,
|
|
71
|
+
width: 900,
|
|
72
|
+
height: 900,
|
|
73
|
+
supersampling: 2,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
await writeFile("motor.png", png)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The same flag works with `renderGLTFToPNGFromURL`, `renderDrawCalls`, and
|
|
80
|
+
`renderSceneFromGLTF`. Realistic mode uses a fixed studio lighting preset;
|
|
81
|
+
`ambient` and `lightDir` apply to regular rendering. Camera, background, and
|
|
82
|
+
output settings work in both modes. A floor must be included in your model
|
|
83
|
+
if you want it to receive shadows.
|
|
84
|
+
|
|
85
|
+
Realistic mode supports base-color textures and scalar glTF metallic/roughness
|
|
86
|
+
values. Normal maps, metallic/roughness textures, and custom HDR environments
|
|
87
|
+
are not yet supported. Transparent and alpha-cutout surfaces do not cast shadows.
|
|
88
|
+
|
|
89
|
+
Use realistic mode for saved images. At 600×600 with 2× supersampling, example
|
|
90
|
+
models took 21–39 seconds to render, compared with 0.6–1.6 seconds in regular
|
|
91
|
+
mode on the same machine. Reduce resolution or supersampling for faster output;
|
|
92
|
+
render times vary with model complexity and hardware.
|
|
93
|
+
|
|
94
|
+
### Camera and background
|
|
95
|
+
|
|
96
|
+
The camera automatically frames the model unless you specify `camPos` and
|
|
97
|
+
`lookAt`. Set `up` to match your model's coordinate system:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
const png = await renderGLTFToPNGFromGLB(glb, {
|
|
101
|
+
realistic: true,
|
|
102
|
+
width: 800,
|
|
103
|
+
height: 600,
|
|
104
|
+
camPos: [80, 60, 80],
|
|
105
|
+
lookAt: [0, 0, 0],
|
|
106
|
+
up: "y+",
|
|
107
|
+
fov: 45,
|
|
108
|
+
backgroundColor: "#e5e7eb",
|
|
109
|
+
grid: false,
|
|
110
|
+
})
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Options
|
|
114
|
+
|
|
50
115
|
`renderGLTFToPNGFromURL` accepts the same render options as the lower-level APIs:
|
|
51
116
|
|
|
52
|
-
- `
|
|
117
|
+
- `realistic`: enable studio lighting, reflections, and soft shadows (default `false`).
|
|
118
|
+
- `width`/`height`: output resolution in pixels (defaults `800`/`600`).
|
|
53
119
|
- `supersampling`: render at `width * supersampling` / `height * supersampling`, then downsample (default `1`).
|
|
54
|
-
- `fov`: vertical field of view in degrees (
|
|
120
|
+
- `fov`: vertical field of view in degrees (default `60`).
|
|
55
121
|
- `camPos` and `lookAt`: override the auto-framed camera position and target.
|
|
56
122
|
- `up`: choose the world-up axis for the camera with `"y+" | "y-" | "x+" | "x-" | "z+" | "z-"`.
|
|
57
123
|
- `cameraRotation`: apply Euler degrees `{ x, y, z }` to define camera orientation directly. When set, it takes precedence over `lookAt`.
|
|
@@ -59,10 +125,12 @@ const png = await renderGLTFToPNGFromGLB(glb, {
|
|
|
59
125
|
- `debugFontSize`: optional pixel size for `debugPoints` labels; when omitted the renderer derives a size from the image dimensions.
|
|
60
126
|
- `debugPointColor`: optional RGB tuple for debug point markers.
|
|
61
127
|
- `debugLabelColor`: optional RGB tuple for debug point labels.
|
|
62
|
-
- `lightDir`:
|
|
63
|
-
- `ambient`: ambient lighting contribution
|
|
64
|
-
- `gamma`: gamma correction
|
|
65
|
-
- `cull`: back-face culling
|
|
128
|
+
- `lightDir`: directional light vector for regular mode (default `[-0.4, -0.9, -0.2]`).
|
|
129
|
+
- `ambient`: ambient lighting contribution from 0 to 1 (default `0.15`; regular mode only).
|
|
130
|
+
- `gamma`: enable gamma correction (default `true`).
|
|
131
|
+
- `cull`: enable back-face culling (default `true`).
|
|
132
|
+
- `backgroundColor`: a hex string such as `"#e5e7eb"`, or an RGB tuple with values from 0 to 1.
|
|
133
|
+
- `grid`: show a ground grid (default `false`), or provide grid options.
|
|
66
134
|
- `fetchImpl`: optional override for resource loading (must match the `fetch` signature).
|
|
67
135
|
|
|
68
136
|
You can inspect the defaults via `getDefaultRenderOptions()` or reuse the internal merge logic with `resolveRenderOptions()`.
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ declare const createUint8Bitmap: ImageFactory;
|
|
|
13
13
|
interface Material {
|
|
14
14
|
baseColorFactor: [number, number, number, number];
|
|
15
15
|
baseColorTexture: BitmapLike | null;
|
|
16
|
+
metallicFactor?: number;
|
|
17
|
+
roughnessFactor?: number;
|
|
16
18
|
alphaMode?: "OPAQUE" | "MASK" | "BLEND";
|
|
17
19
|
alphaCutoff?: number;
|
|
18
20
|
}
|
|
@@ -75,6 +77,8 @@ interface CameraRotation {
|
|
|
75
77
|
*/
|
|
76
78
|
declare function hexToRgb(hex: string): [number, number, number] | null;
|
|
77
79
|
interface RenderOptions {
|
|
80
|
+
/** Opt into metallic/roughness studio lighting and geometry-based soft shadows. */
|
|
81
|
+
realistic: boolean;
|
|
78
82
|
width: number;
|
|
79
83
|
height: number;
|
|
80
84
|
supersampling: number;
|
|
@@ -167,6 +171,17 @@ declare function isPNG(filenameOrUri: string): boolean;
|
|
|
167
171
|
declare function isJPG(filenameOrUri: string): boolean;
|
|
168
172
|
declare function decodeImageFromBuffer(buf: Uint8Array, mimeType?: string | null): Promise<BitmapLike>;
|
|
169
173
|
|
|
174
|
+
type V3 = [number, number, number];
|
|
175
|
+
declare class StudioLighting {
|
|
176
|
+
private tables;
|
|
177
|
+
private occlusion;
|
|
178
|
+
private eye;
|
|
179
|
+
private shadowDirections;
|
|
180
|
+
constructor(drawCalls: DrawCall[], camera: Camera);
|
|
181
|
+
private bounceRadiance;
|
|
182
|
+
shade(position: V3, n: V3, base: V3, material: Material, sampleIndex?: number): V3;
|
|
183
|
+
}
|
|
184
|
+
|
|
170
185
|
interface LightSettings {
|
|
171
186
|
dir: readonly [number, number, number];
|
|
172
187
|
ambient: number;
|
|
@@ -184,14 +199,16 @@ declare class SoftwareRenderer {
|
|
|
184
199
|
readonly height: number;
|
|
185
200
|
readonly bitmap: BitmapLike;
|
|
186
201
|
readonly depth: Float32Array;
|
|
187
|
-
|
|
202
|
+
private readonly denoiser;
|
|
203
|
+
constructor(width: number, height: number, imageFactory?: ImageFactory, denoise?: boolean);
|
|
188
204
|
get buffer(): Uint8Array<ArrayBufferLike> | Uint8ClampedArray<ArrayBufferLike>;
|
|
189
205
|
clear(colorRGBA?: [number, number, number, number]): void;
|
|
190
|
-
setPixel(x: number, y: number, r: number, g: number, b: number, a: number): void;
|
|
206
|
+
setPixel(x: number, y: number, r: number, g: number, b: number, a: number, keepGuide?: boolean): void;
|
|
207
|
+
denoise(gammaOut?: boolean): void;
|
|
191
208
|
drawLines(mesh: DrawCall, camera: Camera, gammaOut?: boolean, options?: LineRenderOptions): void;
|
|
192
209
|
sampleTextureNearest(img: BitmapLike | null, u: number, v: number): MutableRGBA;
|
|
193
210
|
perspInterp(attrs: number[][], invWs: number[], lambdas: number[]): number[];
|
|
194
|
-
drawMesh(mesh: DrawCall, camera: Camera, light: LightSettings, material: Material, cullBackFaces?: boolean, gammaOut?: boolean): void;
|
|
211
|
+
drawMesh(mesh: DrawCall, camera: Camera, light: LightSettings, material: Material, cullBackFaces?: boolean, gammaOut?: boolean, studio?: StudioLighting): void;
|
|
195
212
|
}
|
|
196
213
|
|
|
197
214
|
/**
|
package/dist/index.js
CHANGED
|
@@ -140,13 +140,16 @@ function getAABBCornerDistances(aabb, eye) {
|
|
|
140
140
|
const xs = [aabb.min[0], aabb.max[0]];
|
|
141
141
|
const ys = [aabb.min[1], aabb.max[1]];
|
|
142
142
|
const zs = [aabb.min[2], aabb.max[2]];
|
|
143
|
-
|
|
143
|
+
const nearest = Math.hypot(
|
|
144
|
+
...[0, 1, 2].map(
|
|
145
|
+
(axis) => Math.max(aabb.min[axis] - eye[axis], 0, eye[axis] - aabb.max[axis])
|
|
146
|
+
)
|
|
147
|
+
);
|
|
144
148
|
let farthest = 0;
|
|
145
149
|
for (const x of xs) {
|
|
146
150
|
for (const y of ys) {
|
|
147
151
|
for (const z of zs) {
|
|
148
152
|
const distance = vec3.distance(eye, vec3.fromValues(x, y, z));
|
|
149
|
-
nearest = Math.min(nearest, distance);
|
|
150
153
|
farthest = Math.max(farthest, distance);
|
|
151
154
|
}
|
|
152
155
|
}
|
|
@@ -287,8 +290,8 @@ function hasCrease(normals, minimumDot) {
|
|
|
287
290
|
for (let j = i + 1; j < normals.length; j++) {
|
|
288
291
|
const a = normals[i];
|
|
289
292
|
const b = normals[j];
|
|
290
|
-
const
|
|
291
|
-
if (
|
|
293
|
+
const dot2 = a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
294
|
+
if (dot2 < minimumDot) return true;
|
|
292
295
|
}
|
|
293
296
|
}
|
|
294
297
|
return false;
|
|
@@ -469,6 +472,7 @@ function hexToRgb(hex) {
|
|
|
469
472
|
return [r, g, b];
|
|
470
473
|
}
|
|
471
474
|
var DEFAULT_RENDER_OPTIONS = {
|
|
475
|
+
realistic: false,
|
|
472
476
|
width: 800,
|
|
473
477
|
height: 600,
|
|
474
478
|
supersampling: 1,
|
|
@@ -512,22 +516,168 @@ function srgbDecodeToLinear01(srgb) {
|
|
|
512
516
|
// lib/utils/clamp.ts
|
|
513
517
|
var clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
514
518
|
|
|
519
|
+
// lib/render/clipMeshToNearPlane.ts
|
|
520
|
+
function clipMeshToNearPlane(mesh, mvp) {
|
|
521
|
+
const count = mesh.positions.length / 3;
|
|
522
|
+
const distances = new Float64Array(count);
|
|
523
|
+
let needsClipping = false;
|
|
524
|
+
for (let i = 0; i < count; i++) {
|
|
525
|
+
const distance = (mvp[2] + mvp[3]) * mesh.positions[i * 3] + (mvp[6] + mvp[7]) * mesh.positions[i * 3 + 1] + (mvp[10] + mvp[11]) * mesh.positions[i * 3 + 2] + mvp[14] + mvp[15];
|
|
526
|
+
distances[i] = distance;
|
|
527
|
+
if (!(distance >= 0)) needsClipping = true;
|
|
528
|
+
}
|
|
529
|
+
if (!needsClipping) return mesh;
|
|
530
|
+
const positions = Array.from(mesh.positions);
|
|
531
|
+
const normals = mesh.normals ? Array.from(mesh.normals) : null;
|
|
532
|
+
const uvs = mesh.uvs ? Array.from(mesh.uvs) : null;
|
|
533
|
+
const colors = mesh.colors ? Array.from(mesh.colors) : null;
|
|
534
|
+
const indices = [];
|
|
535
|
+
const sourceIndices = mesh.indices ?? Uint32Array.from({ length: count }, (_, i) => i);
|
|
536
|
+
const intersections = /* @__PURE__ */ new Map();
|
|
537
|
+
const intersect = (a, b) => {
|
|
538
|
+
if (a > b) [a, b] = [b, a];
|
|
539
|
+
if (distances[a] === 0) return a;
|
|
540
|
+
if (distances[b] === 0) return b;
|
|
541
|
+
const key = `${a}:${b}`;
|
|
542
|
+
const cached = intersections.get(key);
|
|
543
|
+
if (cached !== void 0) return cached;
|
|
544
|
+
const t = distances[a] / (distances[a] - distances[b]);
|
|
545
|
+
const index = positions.length / 3;
|
|
546
|
+
const interpolate = (values, size) => {
|
|
547
|
+
if (!values) return;
|
|
548
|
+
for (let k = 0; k < size; k++) {
|
|
549
|
+
const start = values[a * size + k];
|
|
550
|
+
values.push(start + t * (values[b * size + k] - start));
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
interpolate(positions, 3);
|
|
554
|
+
interpolate(normals, 3);
|
|
555
|
+
interpolate(uvs, 2);
|
|
556
|
+
interpolate(colors, 3);
|
|
557
|
+
intersections.set(key, index);
|
|
558
|
+
return index;
|
|
559
|
+
};
|
|
560
|
+
for (let i = 0; i < sourceIndices.length; i += 3) {
|
|
561
|
+
const triangle = [
|
|
562
|
+
sourceIndices[i],
|
|
563
|
+
sourceIndices[i + 1],
|
|
564
|
+
sourceIndices[i + 2]
|
|
565
|
+
];
|
|
566
|
+
if (triangle.some((vertex) => !Number.isFinite(distances[vertex]))) continue;
|
|
567
|
+
const polygon = [];
|
|
568
|
+
for (let j = 0; j < 3; j++) {
|
|
569
|
+
const a = triangle[j];
|
|
570
|
+
const b = triangle[(j + 1) % 3];
|
|
571
|
+
const aInside = distances[a] >= 0;
|
|
572
|
+
const bInside = distances[b] >= 0;
|
|
573
|
+
if (aInside) polygon.push(a);
|
|
574
|
+
if (aInside !== bInside) polygon.push(intersect(a, b));
|
|
575
|
+
}
|
|
576
|
+
for (let j = 1; j + 1 < polygon.length; j++) {
|
|
577
|
+
indices.push(polygon[0], polygon[j], polygon[j + 1]);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return {
|
|
581
|
+
...mesh,
|
|
582
|
+
positions: new Float32Array(positions),
|
|
583
|
+
normals: normals ? new Float32Array(normals) : null,
|
|
584
|
+
uvs: uvs ? new Float32Array(uvs) : null,
|
|
585
|
+
colors: colors ? new Float32Array(colors) : null,
|
|
586
|
+
indices: new Uint32Array(indices)
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// lib/render/GeometryDenoiser.ts
|
|
591
|
+
var GeometryDenoiser = class {
|
|
592
|
+
constructor(width, height) {
|
|
593
|
+
this.width = width;
|
|
594
|
+
this.height = height;
|
|
595
|
+
const count = width * height;
|
|
596
|
+
this.ids = new Int32Array(count);
|
|
597
|
+
this.normals = new Float32Array(count * 3);
|
|
598
|
+
this.albedo = new Float32Array(count * 3);
|
|
599
|
+
this.radiance = new Float32Array(count * 3);
|
|
600
|
+
this.distance = new Float32Array(count);
|
|
601
|
+
}
|
|
602
|
+
ids;
|
|
603
|
+
normals;
|
|
604
|
+
albedo;
|
|
605
|
+
radiance;
|
|
606
|
+
distance;
|
|
607
|
+
materials = /* @__PURE__ */ new WeakMap();
|
|
608
|
+
nextId = 1;
|
|
609
|
+
materialId(material) {
|
|
610
|
+
let id = this.materials.get(material);
|
|
611
|
+
if (!id) {
|
|
612
|
+
id = this.nextId++;
|
|
613
|
+
this.materials.set(material, id);
|
|
614
|
+
}
|
|
615
|
+
return id;
|
|
616
|
+
}
|
|
617
|
+
record(index, id, n, albedo, radiance, distance) {
|
|
618
|
+
this.ids[index] = id;
|
|
619
|
+
this.normals.set(n, index * 3);
|
|
620
|
+
this.albedo.set(albedo, index * 3);
|
|
621
|
+
this.radiance.set(radiance, index * 3);
|
|
622
|
+
this.distance[index] = distance;
|
|
623
|
+
}
|
|
624
|
+
apply(output, gamma) {
|
|
625
|
+
const kernel = [1, 4, 6, 4, 1];
|
|
626
|
+
for (let y = 0; y < this.height; y++)
|
|
627
|
+
for (let x = 0; x < this.width; x++) {
|
|
628
|
+
const index = y * this.width + x, id = this.ids[index];
|
|
629
|
+
if (id === 0) continue;
|
|
630
|
+
const k = index * 3, n = this.normals, a = this.albedo;
|
|
631
|
+
let r = 0, g = 0, b = 0, total = 0;
|
|
632
|
+
for (let dy = -2; dy <= 2; dy++)
|
|
633
|
+
for (let dx = -2; dx <= 2; dx++) {
|
|
634
|
+
const px = x + dx, py = y + dy;
|
|
635
|
+
if (px < 0 || py < 0 || px >= this.width || py >= this.height)
|
|
636
|
+
continue;
|
|
637
|
+
const neighbor = py * this.width + px, p = neighbor * 3;
|
|
638
|
+
if (this.ids[neighbor] !== id) continue;
|
|
639
|
+
if (Math.abs(this.distance[index] - this.distance[neighbor]) > this.distance[index] * 0.02)
|
|
640
|
+
continue;
|
|
641
|
+
if (n[k] * n[p] + n[k + 1] * n[p + 1] + n[k + 2] * n[p + 2] < 0.98)
|
|
642
|
+
continue;
|
|
643
|
+
if (Math.abs(a[k] - a[p]) + Math.abs(a[k + 1] - a[p + 1]) + Math.abs(a[k + 2] - a[p + 2]) > 0.03)
|
|
644
|
+
continue;
|
|
645
|
+
const weight = kernel[dy + 2] * kernel[dx + 2];
|
|
646
|
+
total += weight;
|
|
647
|
+
r += this.radiance[p] * weight;
|
|
648
|
+
g += this.radiance[p + 1] * weight;
|
|
649
|
+
b += this.radiance[p + 2] * weight;
|
|
650
|
+
}
|
|
651
|
+
if (!total) continue;
|
|
652
|
+
const encode2 = (value) => Math.floor(
|
|
653
|
+
255 * (gamma ? srgbEncodeLinear01(Math.max(0, Math.min(1, value / total))) : Math.max(0, Math.min(1, value / total)))
|
|
654
|
+
);
|
|
655
|
+
output[index * 4] = encode2(r);
|
|
656
|
+
output[index * 4 + 1] = encode2(g);
|
|
657
|
+
output[index * 4 + 2] = encode2(b);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
|
|
515
662
|
// lib/render/SoftwareRenderer.ts
|
|
516
663
|
var SoftwareRenderer = class {
|
|
517
664
|
width;
|
|
518
665
|
height;
|
|
519
666
|
bitmap;
|
|
520
667
|
depth;
|
|
521
|
-
|
|
668
|
+
denoiser;
|
|
669
|
+
constructor(width, height, imageFactory = createUint8Bitmap, denoise = false) {
|
|
522
670
|
this.width = width;
|
|
523
671
|
this.height = height;
|
|
524
672
|
this.bitmap = imageFactory(width, height);
|
|
525
673
|
this.depth = new Float32Array(width * height);
|
|
674
|
+
this.denoiser = denoise ? new GeometryDenoiser(width, height) : null;
|
|
526
675
|
}
|
|
527
676
|
get buffer() {
|
|
528
677
|
return this.bitmap.data;
|
|
529
678
|
}
|
|
530
679
|
clear(colorRGBA = [0, 0, 0, 255]) {
|
|
680
|
+
this.denoiser?.ids.fill(0);
|
|
531
681
|
const [r, g, b, a] = colorRGBA;
|
|
532
682
|
for (let i = 0; i < this.width * this.height; i++) {
|
|
533
683
|
const j = i * 4;
|
|
@@ -538,14 +688,18 @@ var SoftwareRenderer = class {
|
|
|
538
688
|
this.depth[i] = Infinity;
|
|
539
689
|
}
|
|
540
690
|
}
|
|
541
|
-
setPixel(x, y, r, g, b, a) {
|
|
691
|
+
setPixel(x, y, r, g, b, a, keepGuide = false) {
|
|
542
692
|
if (x < 0 || y < 0 || x >= this.width || y >= this.height) return;
|
|
543
693
|
const idx = (y * this.width + x) * 4;
|
|
694
|
+
if (this.denoiser && !keepGuide) this.denoiser.ids[y * this.width + x] = 0;
|
|
544
695
|
this.buffer[idx + 0] = r;
|
|
545
696
|
this.buffer[idx + 1] = g;
|
|
546
697
|
this.buffer[idx + 2] = b;
|
|
547
698
|
this.buffer[idx + 3] = a;
|
|
548
699
|
}
|
|
700
|
+
denoise(gammaOut = true) {
|
|
701
|
+
this.denoiser?.apply(this.buffer, gammaOut);
|
|
702
|
+
}
|
|
549
703
|
drawLines(mesh, camera, gammaOut = true, options = {}) {
|
|
550
704
|
const { positions, indices, model, material } = mesh;
|
|
551
705
|
if (!indices) return;
|
|
@@ -681,16 +835,17 @@ var SoftwareRenderer = class {
|
|
|
681
835
|
}
|
|
682
836
|
return out;
|
|
683
837
|
}
|
|
684
|
-
drawMesh(mesh, camera, light, material, cullBackFaces = true, gammaOut = true) {
|
|
685
|
-
|
|
838
|
+
drawMesh(mesh, camera, light, material, cullBackFaces = true, gammaOut = true, studio) {
|
|
839
|
+
let { positions, normals, uvs, indices, model, colors } = mesh;
|
|
840
|
+
const materialId = this.denoiser?.materialId(material) ?? 0;
|
|
686
841
|
const view = camera.view;
|
|
687
842
|
const proj = camera.proj;
|
|
688
843
|
const mvp = mat43.create();
|
|
689
844
|
mat43.multiply(mvp, proj, mat43.multiply(mat43.create(), view, model));
|
|
690
845
|
const normalMat = mat3.create();
|
|
691
846
|
mat3.normalFromMat4(normalMat, model);
|
|
692
|
-
|
|
693
|
-
|
|
847
|
+
let vertexCount = positions.length / 3 | 0;
|
|
848
|
+
let idx = indices ?? (() => {
|
|
694
849
|
const a = new Uint32Array(vertexCount);
|
|
695
850
|
for (let i = 0; i < vertexCount; i++) a[i] = i;
|
|
696
851
|
return a;
|
|
@@ -699,10 +854,21 @@ var SoftwareRenderer = class {
|
|
|
699
854
|
if (!useNormals) {
|
|
700
855
|
useNormals = computeSmoothNormals(positions, idx);
|
|
701
856
|
}
|
|
857
|
+
const clipped = clipMeshToNearPlane(
|
|
858
|
+
{ ...mesh, normals: useNormals, indices: idx },
|
|
859
|
+
mvp
|
|
860
|
+
);
|
|
861
|
+
positions = clipped.positions;
|
|
862
|
+
useNormals = clipped.normals;
|
|
863
|
+
uvs = clipped.uvs;
|
|
864
|
+
colors = clipped.colors;
|
|
865
|
+
idx = clipped.indices;
|
|
866
|
+
vertexCount = positions.length / 3;
|
|
702
867
|
const vScreen = new Array(vertexCount);
|
|
703
868
|
const vInvW = new Float32Array(vertexCount);
|
|
704
869
|
const vNDCz = new Float32Array(vertexCount);
|
|
705
870
|
const vWorldN = new Array(vertexCount);
|
|
871
|
+
const vWorldP = studio ? new Array(vertexCount) : null;
|
|
706
872
|
const vColor = new Array(vertexCount);
|
|
707
873
|
for (let i = 0; i < vertexCount; i++) {
|
|
708
874
|
const p = vec42.fromValues(
|
|
@@ -713,6 +879,10 @@ var SoftwareRenderer = class {
|
|
|
713
879
|
);
|
|
714
880
|
const c = vec42.create();
|
|
715
881
|
vec42.transformMat4(c, p, mvp);
|
|
882
|
+
if (vWorldP) {
|
|
883
|
+
const world = vec42.transformMat4(vec42.create(), p, model);
|
|
884
|
+
vWorldP[i] = [world[0], world[1], world[2]];
|
|
885
|
+
}
|
|
716
886
|
const invW = 1 / c[3];
|
|
717
887
|
const ndcX = c[0] * invW;
|
|
718
888
|
const ndcY = c[1] * invW;
|
|
@@ -773,6 +943,7 @@ var SoftwareRenderer = class {
|
|
|
773
943
|
vWorldN[i1],
|
|
774
944
|
vWorldN[i2]
|
|
775
945
|
];
|
|
946
|
+
const worldPositions = vWorldP ? [vWorldP[i0], vWorldP[i1], vWorldP[i2]] : null;
|
|
776
947
|
const uv = uvs ? [
|
|
777
948
|
[uvs[i0 * 2 + 0], uvs[i0 * 2 + 1]],
|
|
778
949
|
[uvs[i1 * 2 + 0], uvs[i1 * 2 + 1]],
|
|
@@ -853,7 +1024,22 @@ var SoftwareRenderer = class {
|
|
|
853
1024
|
let r = baseColor[0] * lit;
|
|
854
1025
|
let g = baseColor[1] * lit;
|
|
855
1026
|
let b = baseColor[2] * lit;
|
|
1027
|
+
if (studio && worldPositions) {
|
|
1028
|
+
const position = this.perspInterp(worldPositions, invW, [
|
|
1029
|
+
l0,
|
|
1030
|
+
l1,
|
|
1031
|
+
l2
|
|
1032
|
+
]);
|
|
1033
|
+
[r, g, b] = studio.shade(
|
|
1034
|
+
position,
|
|
1035
|
+
nrm,
|
|
1036
|
+
[baseColor[0], baseColor[1], baseColor[2]],
|
|
1037
|
+
material,
|
|
1038
|
+
di
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
856
1041
|
let a = baseColor[3];
|
|
1042
|
+
const linearR = r, linearG = g, linearB = b;
|
|
857
1043
|
const alphaMode = material.alphaMode ?? "OPAQUE";
|
|
858
1044
|
const alphaCutoff = material.alphaCutoff ?? 0.5;
|
|
859
1045
|
if (alphaMode === "MASK" && a < alphaCutoff) continue;
|
|
@@ -895,8 +1081,22 @@ var SoftwareRenderer = class {
|
|
|
895
1081
|
clamp(outR, 0, 1) * 255 | 0,
|
|
896
1082
|
clamp(outG, 0, 1) * 255 | 0,
|
|
897
1083
|
clamp(outB, 0, 1) * 255 | 0,
|
|
898
|
-
clamp(outA, 0, 1) * 255 | 0
|
|
1084
|
+
clamp(outA, 0, 1) * 255 | 0,
|
|
1085
|
+
true
|
|
899
1086
|
);
|
|
1087
|
+
if (this.denoiser) {
|
|
1088
|
+
if (alphaMode !== "BLEND") {
|
|
1089
|
+
const distance = 1 / (l0 * invW[0] + l1 * invW[1] + l2 * invW[2]);
|
|
1090
|
+
this.denoiser.record(
|
|
1091
|
+
di,
|
|
1092
|
+
materialId,
|
|
1093
|
+
nrm,
|
|
1094
|
+
[baseColor[0], baseColor[1], baseColor[2]],
|
|
1095
|
+
[linearR, linearG, linearB],
|
|
1096
|
+
distance
|
|
1097
|
+
);
|
|
1098
|
+
} else this.denoiser.ids[di] = 0;
|
|
1099
|
+
}
|
|
900
1100
|
}
|
|
901
1101
|
}
|
|
902
1102
|
}
|
|
@@ -1250,6 +1450,493 @@ function resolveRenderOptions(options = {}) {
|
|
|
1250
1450
|
};
|
|
1251
1451
|
}
|
|
1252
1452
|
|
|
1453
|
+
// lib/render/StudioLighting.ts
|
|
1454
|
+
import { mat4 as mat46 } from "gl-matrix";
|
|
1455
|
+
|
|
1456
|
+
// lib/render/studio-environment.json
|
|
1457
|
+
var studio_environment_default = {
|
|
1458
|
+
ambient: [0.055, 0.065, 0.085],
|
|
1459
|
+
lobes: [
|
|
1460
|
+
{ direction: [-0.45, 0.8, 0.4], sharpness: 18, color: [5, 4.8, 4.4] },
|
|
1461
|
+
{ direction: [0.8, 0.3, 0.3], sharpness: 30, color: [2.6, 3.1, 4] },
|
|
1462
|
+
{ direction: [-0.2, 0.3, -0.95], sharpness: 50, color: [4, 4.3, 5] }
|
|
1463
|
+
]
|
|
1464
|
+
};
|
|
1465
|
+
|
|
1466
|
+
// lib/render/RayOcclusion.ts
|
|
1467
|
+
import { vec3 as vec33 } from "gl-matrix";
|
|
1468
|
+
var RayOcclusion = class {
|
|
1469
|
+
root;
|
|
1470
|
+
bias;
|
|
1471
|
+
constructor(drawCalls) {
|
|
1472
|
+
const triangles = [];
|
|
1473
|
+
for (const dc of drawCalls) {
|
|
1474
|
+
if ((dc.mode ?? 4) !== 4 || dc.material.alphaMode === "BLEND") continue;
|
|
1475
|
+
if (dc.material.alphaMode === "MASK") continue;
|
|
1476
|
+
const count = dc.indices?.length ?? dc.positions.length / 3;
|
|
1477
|
+
const vertex = (index) => {
|
|
1478
|
+
const i = (dc.indices?.[index] ?? index) * 3;
|
|
1479
|
+
const p = vec33.transformMat4(
|
|
1480
|
+
vec33.create(),
|
|
1481
|
+
[dc.positions[i], dc.positions[i + 1], dc.positions[i + 2]],
|
|
1482
|
+
dc.model
|
|
1483
|
+
);
|
|
1484
|
+
return [p[0], p[1], p[2]];
|
|
1485
|
+
};
|
|
1486
|
+
for (let i = 0; i < count; i += 3) {
|
|
1487
|
+
const p = vertex(i), q = vertex(i + 1), r = vertex(i + 2);
|
|
1488
|
+
const min = [0, 1, 2].map(
|
|
1489
|
+
(a) => Math.min(p[a], q[a], r[a])
|
|
1490
|
+
);
|
|
1491
|
+
const max = [0, 1, 2].map(
|
|
1492
|
+
(a) => Math.max(p[a], q[a], r[a])
|
|
1493
|
+
);
|
|
1494
|
+
triangles.push({
|
|
1495
|
+
p,
|
|
1496
|
+
e1: q.map((v, a) => v - p[a]),
|
|
1497
|
+
e2: r.map((v, a) => v - p[a]),
|
|
1498
|
+
min,
|
|
1499
|
+
max,
|
|
1500
|
+
center: min.map((v, a) => (v + max[a]) / 2),
|
|
1501
|
+
material: dc.material
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
this.root = triangles.length ? this.build(triangles) : null;
|
|
1506
|
+
const extent = this.root ? Math.hypot(...this.root.max.map((v, a) => v - this.root.min[a])) : 1;
|
|
1507
|
+
this.bias = Math.max(1e-7, extent * 1e-5);
|
|
1508
|
+
}
|
|
1509
|
+
build(triangles) {
|
|
1510
|
+
const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
|
|
1511
|
+
for (const t of triangles)
|
|
1512
|
+
for (let a = 0; a < 3; a++) {
|
|
1513
|
+
min[a] = Math.min(min[a], t.min[a]);
|
|
1514
|
+
max[a] = Math.max(max[a], t.max[a]);
|
|
1515
|
+
}
|
|
1516
|
+
if (triangles.length <= 8) return { min, max, triangles };
|
|
1517
|
+
const area = (lo, hi) => {
|
|
1518
|
+
const x = hi[0] - lo[0], y = hi[1] - lo[1], z = hi[2] - lo[2];
|
|
1519
|
+
return 2 * (x * y + y * z + z * x);
|
|
1520
|
+
};
|
|
1521
|
+
let bestCost = Infinity, middle = triangles.length >> 1, best = triangles;
|
|
1522
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
1523
|
+
const sorted = triangles.slice().sort((a, b) => a.center[axis] - b.center[axis]);
|
|
1524
|
+
const suffix = new Float64Array(sorted.length);
|
|
1525
|
+
let lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
|
|
1526
|
+
for (let i = sorted.length - 1; i >= 0; i--) {
|
|
1527
|
+
for (let a = 0; a < 3; a++) {
|
|
1528
|
+
lo[a] = Math.min(lo[a], sorted[i].min[a]);
|
|
1529
|
+
hi[a] = Math.max(hi[a], sorted[i].max[a]);
|
|
1530
|
+
}
|
|
1531
|
+
suffix[i] = area(lo, hi);
|
|
1532
|
+
}
|
|
1533
|
+
lo = [Infinity, Infinity, Infinity];
|
|
1534
|
+
hi = [-Infinity, -Infinity, -Infinity];
|
|
1535
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
1536
|
+
const triangle = sorted[i - 1];
|
|
1537
|
+
for (let a = 0; a < 3; a++) {
|
|
1538
|
+
lo[a] = Math.min(lo[a], triangle.min[a]);
|
|
1539
|
+
hi[a] = Math.max(hi[a], triangle.max[a]);
|
|
1540
|
+
}
|
|
1541
|
+
if (triangle.center[axis] === sorted[i].center[axis]) continue;
|
|
1542
|
+
const cost = area(lo, hi) * i + suffix[i] * (sorted.length - i);
|
|
1543
|
+
if (cost < bestCost) {
|
|
1544
|
+
bestCost = cost;
|
|
1545
|
+
middle = i;
|
|
1546
|
+
best = sorted;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
return {
|
|
1551
|
+
min,
|
|
1552
|
+
max,
|
|
1553
|
+
left: this.build(best.slice(0, middle)),
|
|
1554
|
+
right: this.build(best.slice(middle))
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
occluded(origin, direction, maxDistance = Infinity) {
|
|
1558
|
+
return this.root ? this.intersect(this.root, origin, direction, maxDistance) : false;
|
|
1559
|
+
}
|
|
1560
|
+
/** Closest opaque triangle for one-bounce reflections and diffuse fill. */
|
|
1561
|
+
trace(origin, direction, maxDistance = Infinity) {
|
|
1562
|
+
const state = {
|
|
1563
|
+
hit: null,
|
|
1564
|
+
limit: maxDistance
|
|
1565
|
+
};
|
|
1566
|
+
if (this.root)
|
|
1567
|
+
this.intersect(this.root, origin, direction, maxDistance, state);
|
|
1568
|
+
return state.hit;
|
|
1569
|
+
}
|
|
1570
|
+
intersect(node, o, d, limit, state) {
|
|
1571
|
+
if (state) limit = state.limit;
|
|
1572
|
+
let near = 0, far = limit;
|
|
1573
|
+
for (let a = 0; a < 3; a++) {
|
|
1574
|
+
if (Math.abs(d[a]) < 1e-12) {
|
|
1575
|
+
if (o[a] < node.min[a] || o[a] > node.max[a]) return false;
|
|
1576
|
+
continue;
|
|
1577
|
+
}
|
|
1578
|
+
const t0 = (node.min[a] - o[a]) / d[a], t1 = (node.max[a] - o[a]) / d[a];
|
|
1579
|
+
near = Math.max(near, Math.min(t0, t1));
|
|
1580
|
+
far = Math.min(far, Math.max(t0, t1));
|
|
1581
|
+
if (far < near) return false;
|
|
1582
|
+
}
|
|
1583
|
+
if (node.triangles) {
|
|
1584
|
+
for (const { p, e1, e2, material } of node.triangles) {
|
|
1585
|
+
const px = d[1] * e2[2] - d[2] * e2[1], py = d[2] * e2[0] - d[0] * e2[2], pz = d[0] * e2[1] - d[1] * e2[0];
|
|
1586
|
+
const det = e1[0] * px + e1[1] * py + e1[2] * pz;
|
|
1587
|
+
if (Math.abs(det) < 1e-12) continue;
|
|
1588
|
+
const inv = 1 / det, tx = o[0] - p[0], ty = o[1] - p[1], tz = o[2] - p[2];
|
|
1589
|
+
const u = (tx * px + ty * py + tz * pz) * inv;
|
|
1590
|
+
if (u < 0 || u > 1) continue;
|
|
1591
|
+
const qx = ty * e1[2] - tz * e1[1], qy = tz * e1[0] - tx * e1[2], qz = tx * e1[1] - ty * e1[0];
|
|
1592
|
+
const v = (d[0] * qx + d[1] * qy + d[2] * qz) * inv;
|
|
1593
|
+
if (v < 0 || u + v > 1) continue;
|
|
1594
|
+
const distance = (e2[0] * qx + e2[1] * qy + e2[2] * qz) * inv;
|
|
1595
|
+
if (distance > this.bias * 0.25 && distance < limit) {
|
|
1596
|
+
if (!state) return true;
|
|
1597
|
+
const normal = [
|
|
1598
|
+
e1[1] * e2[2] - e1[2] * e2[1],
|
|
1599
|
+
e1[2] * e2[0] - e1[0] * e2[2],
|
|
1600
|
+
e1[0] * e2[1] - e1[1] * e2[0]
|
|
1601
|
+
];
|
|
1602
|
+
const length = Math.hypot(...normal) || 1;
|
|
1603
|
+
for (let a = 0; a < 3; a++) normal[a] /= length;
|
|
1604
|
+
if (normal[0] * d[0] + normal[1] * d[1] + normal[2] * d[2] > 0)
|
|
1605
|
+
for (let a = 0; a < 3; a++) normal[a] *= -1;
|
|
1606
|
+
state.hit = {
|
|
1607
|
+
position: [
|
|
1608
|
+
o[0] + d[0] * distance,
|
|
1609
|
+
o[1] + d[1] * distance,
|
|
1610
|
+
o[2] + d[2] * distance
|
|
1611
|
+
],
|
|
1612
|
+
normal,
|
|
1613
|
+
material,
|
|
1614
|
+
distance
|
|
1615
|
+
};
|
|
1616
|
+
state.limit = limit = distance;
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
return false;
|
|
1620
|
+
}
|
|
1621
|
+
if (state) {
|
|
1622
|
+
this.intersect(node.left, o, d, limit, state);
|
|
1623
|
+
this.intersect(node.right, o, d, state.limit, state);
|
|
1624
|
+
return state.hit !== null;
|
|
1625
|
+
}
|
|
1626
|
+
return this.intersect(node.left, o, d, limit) || this.intersect(node.right, o, d, limit);
|
|
1627
|
+
}
|
|
1628
|
+
};
|
|
1629
|
+
|
|
1630
|
+
// lib/render/StudioLighting.ts
|
|
1631
|
+
var dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
1632
|
+
var normalize = (v) => {
|
|
1633
|
+
const l = Math.hypot(...v) || 1;
|
|
1634
|
+
return [v[0] / l, v[1] / l, v[2] / l];
|
|
1635
|
+
};
|
|
1636
|
+
var W = 96;
|
|
1637
|
+
var H = 48;
|
|
1638
|
+
var SAMPLES = 128;
|
|
1639
|
+
var ROUGHNESSES = [0.04, 0.1, 0.18, 0.28, 0.4, 0.55, 0.7, 0.85, 1];
|
|
1640
|
+
var lobes = studio_environment_default.lobes.map((l) => ({
|
|
1641
|
+
...l,
|
|
1642
|
+
direction: normalize(l.direction),
|
|
1643
|
+
color: l.color
|
|
1644
|
+
}));
|
|
1645
|
+
function radicalInverse(value) {
|
|
1646
|
+
let result = 0, factor = 0.5;
|
|
1647
|
+
while (value) {
|
|
1648
|
+
result += (value & 1) * factor;
|
|
1649
|
+
value >>>= 1;
|
|
1650
|
+
factor *= 0.5;
|
|
1651
|
+
}
|
|
1652
|
+
return result;
|
|
1653
|
+
}
|
|
1654
|
+
function basis(n) {
|
|
1655
|
+
const t = normalize(Math.abs(n[1]) < 0.999 ? [n[2], 0, -n[0]] : [1, 0, 0]);
|
|
1656
|
+
return [
|
|
1657
|
+
t,
|
|
1658
|
+
[
|
|
1659
|
+
n[1] * t[2] - n[2] * t[1],
|
|
1660
|
+
n[2] * t[0] - n[0] * t[2],
|
|
1661
|
+
n[0] * t[1] - n[1] * t[0]
|
|
1662
|
+
]
|
|
1663
|
+
];
|
|
1664
|
+
}
|
|
1665
|
+
function orient(x, y, z, n, t, b) {
|
|
1666
|
+
return [
|
|
1667
|
+
t[0] * x + b[0] * y + n[0] * z,
|
|
1668
|
+
t[1] * x + b[1] * y + n[1] * z,
|
|
1669
|
+
t[2] * x + b[2] * y + n[2] * z
|
|
1670
|
+
];
|
|
1671
|
+
}
|
|
1672
|
+
function sampleTable(table, n) {
|
|
1673
|
+
const x = (Math.atan2(n[2], n[0]) / (2 * Math.PI) + 0.5) * W - 0.5;
|
|
1674
|
+
const y = Math.max(
|
|
1675
|
+
0,
|
|
1676
|
+
Math.min(
|
|
1677
|
+
H - 1,
|
|
1678
|
+
Math.acos(Math.max(-1, Math.min(1, n[1]))) / Math.PI * H - 0.5
|
|
1679
|
+
)
|
|
1680
|
+
);
|
|
1681
|
+
const ix = Math.floor(x), iy = Math.floor(y), fx = x - ix, fy = y - iy;
|
|
1682
|
+
const a = (ix % W + W) % W, b = (a + 1) % W, c = Math.min(H - 1, iy + 1);
|
|
1683
|
+
return (table[iy * W + a] * (1 - fx) + table[iy * W + b] * fx) * (1 - fy) + (table[c * W + a] * (1 - fx) + table[c * W + b] * fx) * fy;
|
|
1684
|
+
}
|
|
1685
|
+
var cachedTables;
|
|
1686
|
+
function getTables() {
|
|
1687
|
+
if (cachedTables) return cachedTables;
|
|
1688
|
+
const diffuse = lobes.map(() => new Float32Array(W * H));
|
|
1689
|
+
const specular = ROUGHNESSES.map(
|
|
1690
|
+
() => lobes.map(() => new Float32Array(W * H))
|
|
1691
|
+
);
|
|
1692
|
+
const diffuseSamples = Array.from({ length: SAMPLES }, (_, i) => {
|
|
1693
|
+
const phi = 2 * Math.PI * radicalInverse(i), r = Math.sqrt((i + 0.5) / SAMPLES);
|
|
1694
|
+
return [r * Math.cos(phi), r * Math.sin(phi), Math.sqrt(1 - r * r)];
|
|
1695
|
+
});
|
|
1696
|
+
const halfSamples = ROUGHNESSES.map(
|
|
1697
|
+
(rough) => Array.from({ length: SAMPLES }, (_, i) => {
|
|
1698
|
+
const phi = 2 * Math.PI * radicalInverse(i), u = (i + 0.5) / SAMPLES, a = rough * rough;
|
|
1699
|
+
const cos = Math.sqrt((1 - u) / (1 + (a * a - 1) * u)), sin = Math.sqrt(1 - cos * cos);
|
|
1700
|
+
return [sin * Math.cos(phi), sin * Math.sin(phi), cos];
|
|
1701
|
+
})
|
|
1702
|
+
);
|
|
1703
|
+
for (let y = 0; y < H; y++)
|
|
1704
|
+
for (let x = 0; x < W; x++) {
|
|
1705
|
+
const theta = (y + 0.5) / H * Math.PI, phi = ((x + 0.5) / W - 0.5) * 2 * Math.PI;
|
|
1706
|
+
const n = [
|
|
1707
|
+
Math.sin(theta) * Math.cos(phi),
|
|
1708
|
+
Math.cos(theta),
|
|
1709
|
+
Math.sin(theta) * Math.sin(phi)
|
|
1710
|
+
];
|
|
1711
|
+
const [t, b] = basis(n), index = y * W + x;
|
|
1712
|
+
for (const s of diffuseSamples) {
|
|
1713
|
+
const l = orient(...s, n, t, b);
|
|
1714
|
+
for (let j = 0; j < lobes.length; j++)
|
|
1715
|
+
diffuse[j][index] += Math.exp(lobes[j].sharpness * (dot(l, lobes[j].direction) - 1)) / SAMPLES;
|
|
1716
|
+
}
|
|
1717
|
+
for (let r = 0; r < ROUGHNESSES.length; r++) {
|
|
1718
|
+
let weight = 0;
|
|
1719
|
+
for (const s of halfSamples[r]) {
|
|
1720
|
+
const h = orient(...s, n, t, b), nh = dot(n, h);
|
|
1721
|
+
const l = [
|
|
1722
|
+
2 * nh * h[0] - n[0],
|
|
1723
|
+
2 * nh * h[1] - n[1],
|
|
1724
|
+
2 * nh * h[2] - n[2]
|
|
1725
|
+
];
|
|
1726
|
+
const nl = Math.max(0, dot(n, l));
|
|
1727
|
+
weight += nl;
|
|
1728
|
+
for (let j = 0; j < lobes.length; j++)
|
|
1729
|
+
specular[r][j][index] += Math.exp(
|
|
1730
|
+
lobes[j].sharpness * (dot(l, lobes[j].direction) - 1)
|
|
1731
|
+
) * nl;
|
|
1732
|
+
}
|
|
1733
|
+
for (let j = 0; j < lobes.length; j++)
|
|
1734
|
+
specular[r][j][index] /= Math.max(weight, 1e-8);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
const brdf = new Float32Array(64 * 64 * 2);
|
|
1738
|
+
for (let y = 0; y < 64; y++)
|
|
1739
|
+
for (let x = 0; x < 64; x++) {
|
|
1740
|
+
const rough = (y + 0.5) / 64, nv = (x + 0.5) / 64, a = rough * rough, a2 = a * a;
|
|
1741
|
+
const v = [Math.sqrt(1 - nv * nv), 0, nv];
|
|
1742
|
+
let A = 0, B = 0;
|
|
1743
|
+
for (let i = 0; i < SAMPLES; i++) {
|
|
1744
|
+
const u = (i + 0.5) / SAMPLES, phi = 2 * Math.PI * radicalInverse(i);
|
|
1745
|
+
const nh = Math.sqrt((1 - u) / (1 + (a2 - 1) * u)), sin = Math.sqrt(1 - nh * nh);
|
|
1746
|
+
const h = [sin * Math.cos(phi), sin * Math.sin(phi), nh], vh = Math.max(0, dot(v, h));
|
|
1747
|
+
const nl = 2 * vh * nh - nv;
|
|
1748
|
+
if (nl <= 0) continue;
|
|
1749
|
+
const gv = 2 * nv / (nv + Math.sqrt(a2 + (1 - a2) * nv * nv));
|
|
1750
|
+
const gl = 2 * nl / (nl + Math.sqrt(a2 + (1 - a2) * nl * nl));
|
|
1751
|
+
const vis = gv * gl * vh / Math.max(nh * nv, 1e-8), f = (1 - vh) ** 5;
|
|
1752
|
+
A += (1 - f) * vis;
|
|
1753
|
+
B += f * vis;
|
|
1754
|
+
}
|
|
1755
|
+
const index = (y * 64 + x) * 2;
|
|
1756
|
+
brdf[index] = A / SAMPLES;
|
|
1757
|
+
brdf[index + 1] = B / SAMPLES;
|
|
1758
|
+
}
|
|
1759
|
+
cachedTables = { diffuse, specular, brdf };
|
|
1760
|
+
return cachedTables;
|
|
1761
|
+
}
|
|
1762
|
+
var StudioLighting = class {
|
|
1763
|
+
tables = getTables();
|
|
1764
|
+
occlusion;
|
|
1765
|
+
eye;
|
|
1766
|
+
shadowDirections;
|
|
1767
|
+
constructor(drawCalls, camera) {
|
|
1768
|
+
this.occlusion = new RayOcclusion(drawCalls);
|
|
1769
|
+
const world = mat46.invert(mat46.create(), camera.view);
|
|
1770
|
+
if (!world)
|
|
1771
|
+
throw new Error("Realistic rendering requires an invertible camera view.");
|
|
1772
|
+
this.eye = [world[12], world[13], world[14]];
|
|
1773
|
+
this.shadowDirections = lobes.map((l) => {
|
|
1774
|
+
const [t, b] = basis(l.direction);
|
|
1775
|
+
return Array.from({ length: 8 }, (_, i) => {
|
|
1776
|
+
const u = (i + 0.5) / 8, cos = 1 + Math.log(1 - u) / l.sharpness, sin = Math.sqrt(Math.max(0, 1 - cos * cos)), phi = 2 * Math.PI * radicalInverse(i);
|
|
1777
|
+
return orient(
|
|
1778
|
+
sin * Math.cos(phi),
|
|
1779
|
+
sin * Math.sin(phi),
|
|
1780
|
+
cos,
|
|
1781
|
+
l.direction,
|
|
1782
|
+
t,
|
|
1783
|
+
b
|
|
1784
|
+
);
|
|
1785
|
+
});
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
bounceRadiance(hit, incoming) {
|
|
1789
|
+
const metal = Math.max(0, Math.min(1, hit.material.metallicFactor ?? 1));
|
|
1790
|
+
const irradiance = [...studio_environment_default.ambient];
|
|
1791
|
+
for (let j = 0; j < lobes.length; j++) {
|
|
1792
|
+
const value = sampleTable(this.tables.diffuse[j], hit.normal);
|
|
1793
|
+
for (let k = 0; k < 3; k++) irradiance[k] += value * lobes[j].color[k];
|
|
1794
|
+
}
|
|
1795
|
+
const base = hit.material.baseColorFactor;
|
|
1796
|
+
const rough = Math.max(0.04, Math.min(1, hit.material.roughnessFactor ?? 1));
|
|
1797
|
+
const nv = Math.max(1e-3, -dot(hit.normal, incoming));
|
|
1798
|
+
const reflected = [
|
|
1799
|
+
2 * nv * hit.normal[0] + incoming[0],
|
|
1800
|
+
2 * nv * hit.normal[1] + incoming[1],
|
|
1801
|
+
2 * nv * hit.normal[2] + incoming[2]
|
|
1802
|
+
];
|
|
1803
|
+
let level = 0;
|
|
1804
|
+
while (level < ROUGHNESSES.length - 2 && ROUGHNESSES[level + 1] < rough)
|
|
1805
|
+
level++;
|
|
1806
|
+
const blend = (rough - ROUGHNESSES[level]) / (ROUGHNESSES[level + 1] - ROUGHNESSES[level]);
|
|
1807
|
+
const index = (Math.min(63, Math.floor(rough * 64)) * 64 + Math.min(63, Math.floor(nv * 64))) * 2;
|
|
1808
|
+
const A = this.tables.brdf[index], B = this.tables.brdf[index + 1];
|
|
1809
|
+
const radiance = irradiance.map(
|
|
1810
|
+
(c, k) => c * base[k] * (1 - metal) * 0.96
|
|
1811
|
+
);
|
|
1812
|
+
const pref = [...studio_environment_default.ambient];
|
|
1813
|
+
for (let j = 0; j < lobes.length; j++) {
|
|
1814
|
+
const value = sampleTable(this.tables.specular[level][j], reflected) * (1 - blend) + sampleTable(this.tables.specular[level + 1][j], reflected) * blend;
|
|
1815
|
+
for (let k = 0; k < 3; k++) pref[k] += value * lobes[j].color[k];
|
|
1816
|
+
}
|
|
1817
|
+
for (let k = 0; k < 3; k++)
|
|
1818
|
+
radiance[k] += pref[k] * ((0.04 * (1 - metal) + base[k] * metal) * A + B);
|
|
1819
|
+
return radiance;
|
|
1820
|
+
}
|
|
1821
|
+
shade(position, n, base, material, sampleIndex = 0) {
|
|
1822
|
+
const v = normalize([
|
|
1823
|
+
this.eye[0] - position[0],
|
|
1824
|
+
this.eye[1] - position[1],
|
|
1825
|
+
this.eye[2] - position[2]
|
|
1826
|
+
]);
|
|
1827
|
+
const nv = Math.max(1e-3, dot(n, v));
|
|
1828
|
+
const reflected = [
|
|
1829
|
+
2 * nv * n[0] - v[0],
|
|
1830
|
+
2 * nv * n[1] - v[1],
|
|
1831
|
+
2 * nv * n[2] - v[2]
|
|
1832
|
+
];
|
|
1833
|
+
const origin = [
|
|
1834
|
+
position[0] + n[0] * this.occlusion.bias,
|
|
1835
|
+
position[1] + n[1] * this.occlusion.bias,
|
|
1836
|
+
position[2] + n[2] * this.occlusion.bias
|
|
1837
|
+
];
|
|
1838
|
+
const metal = Math.max(0, Math.min(1, material.metallicFactor ?? 1));
|
|
1839
|
+
const rough = Math.max(0.04, Math.min(1, material.roughnessFactor ?? 1));
|
|
1840
|
+
let level = 0;
|
|
1841
|
+
while (level < ROUGHNESSES.length - 2 && ROUGHNESSES[level + 1] < rough)
|
|
1842
|
+
level++;
|
|
1843
|
+
const blend = (rough - ROUGHNESSES[level]) / (ROUGHNESSES[level + 1] - ROUGHNESSES[level]);
|
|
1844
|
+
const bx = Math.min(63, Math.floor(nv * 64)), by = Math.min(63, Math.floor(rough * 64)), bi = (by * 64 + bx) * 2;
|
|
1845
|
+
const A = this.tables.brdf[bi], B = this.tables.brdf[bi + 1];
|
|
1846
|
+
const f0 = base.map((c) => 0.04 * (1 - metal) + c * metal);
|
|
1847
|
+
const color = base.map(
|
|
1848
|
+
(c, j) => studio_environment_default.ambient[j] * c * (1 - metal) * 0.96
|
|
1849
|
+
);
|
|
1850
|
+
const specColor = f0.map(
|
|
1851
|
+
(c, j) => studio_environment_default.ambient[j] * (c * A + B)
|
|
1852
|
+
);
|
|
1853
|
+
let bits = sampleIndex + 1;
|
|
1854
|
+
bits = Math.imul(bits ^ bits >>> 16, 2146121005);
|
|
1855
|
+
bits = Math.imul(bits ^ bits >>> 15, 2221713035);
|
|
1856
|
+
const hash = ((bits ^ bits >>> 16) >>> 0) / 4294967296;
|
|
1857
|
+
const phase = hash * 2 * Math.PI, cosPhase = Math.cos(phase), sinPhase = Math.sin(phase);
|
|
1858
|
+
for (let j = 0; j < lobes.length; j++) {
|
|
1859
|
+
const dirs = this.shadowDirections[j];
|
|
1860
|
+
let visible = 0, total = 0;
|
|
1861
|
+
const axis = lobes[j].direction;
|
|
1862
|
+
for (const direction of dirs) {
|
|
1863
|
+
const parallel = dot(direction, axis);
|
|
1864
|
+
const cross = [
|
|
1865
|
+
axis[1] * direction[2] - axis[2] * direction[1],
|
|
1866
|
+
axis[2] * direction[0] - axis[0] * direction[2],
|
|
1867
|
+
axis[0] * direction[1] - axis[1] * direction[0]
|
|
1868
|
+
];
|
|
1869
|
+
const l = direction.map(
|
|
1870
|
+
(value, k) => axis[k] * parallel + (value - axis[k] * parallel) * cosPhase + cross[k] * sinPhase
|
|
1871
|
+
);
|
|
1872
|
+
const weight = Math.max(0, dot(n, l));
|
|
1873
|
+
total += weight;
|
|
1874
|
+
if (weight > 0 && !this.occlusion.occluded(origin, l)) visible += weight;
|
|
1875
|
+
}
|
|
1876
|
+
const visibility = total > 0 ? visible / total : 1;
|
|
1877
|
+
const diffuse = sampleTable(this.tables.diffuse[j], n) * visibility;
|
|
1878
|
+
const specular = (sampleTable(this.tables.specular[level][j], reflected) * (1 - blend) + sampleTable(this.tables.specular[level + 1][j], reflected) * blend) * visibility;
|
|
1879
|
+
for (let k = 0; k < 3; k++) {
|
|
1880
|
+
color[k] += lobes[j].color[k] * base[k] * (1 - metal) * 0.96 * diffuse;
|
|
1881
|
+
specColor[k] += lobes[j].color[k] * (f0[k] * A + B) * specular;
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
const [t, b] = basis(n);
|
|
1885
|
+
let blocked = 0;
|
|
1886
|
+
for (let i = 0; i < 4; i++) {
|
|
1887
|
+
const phi = i * Math.PI / 2 + phase, l = orient(
|
|
1888
|
+
0.7 * Math.cos(phi),
|
|
1889
|
+
0.7 * Math.sin(phi),
|
|
1890
|
+
Math.sqrt(0.51),
|
|
1891
|
+
n,
|
|
1892
|
+
t,
|
|
1893
|
+
b
|
|
1894
|
+
);
|
|
1895
|
+
const hit = this.occlusion.trace(origin, l);
|
|
1896
|
+
if (hit) {
|
|
1897
|
+
blocked++;
|
|
1898
|
+
const bounce2 = this.bounceRadiance(hit, l);
|
|
1899
|
+
for (let k = 0; k < 3; k++)
|
|
1900
|
+
color[k] += bounce2[k] * base[k] * (1 - metal) * 0.96 / 4;
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
const ao = 1 - blocked / 4;
|
|
1904
|
+
for (let k = 0; k < 3; k++)
|
|
1905
|
+
color[k] -= studio_environment_default.ambient[k] * (1 - ao) * base[k] * (1 - metal) * 0.96;
|
|
1906
|
+
const [rt, rb] = basis(reflected), bounce = [0, 0, 0];
|
|
1907
|
+
let hitWeight = 0, totalWeight = 0;
|
|
1908
|
+
for (let i = 0; i < 8; i++) {
|
|
1909
|
+
const u = (i + 0.5) / 8, phi = 2 * Math.PI * radicalInverse(i) + phase, a = rough * rough;
|
|
1910
|
+
const nh = Math.sqrt((1 - u) / (1 + (a * a - 1) * u)), sin = Math.sqrt(1 - nh * nh);
|
|
1911
|
+
const h = orient(
|
|
1912
|
+
sin * Math.cos(phi),
|
|
1913
|
+
sin * Math.sin(phi),
|
|
1914
|
+
nh,
|
|
1915
|
+
reflected,
|
|
1916
|
+
rt,
|
|
1917
|
+
rb
|
|
1918
|
+
);
|
|
1919
|
+
const l = [
|
|
1920
|
+
2 * nh * h[0] - reflected[0],
|
|
1921
|
+
2 * nh * h[1] - reflected[1],
|
|
1922
|
+
2 * nh * h[2] - reflected[2]
|
|
1923
|
+
];
|
|
1924
|
+
const weight = Math.max(0, dot(reflected, l));
|
|
1925
|
+
totalWeight += weight;
|
|
1926
|
+
if (weight === 0 || dot(n, l) <= 0) continue;
|
|
1927
|
+
const hit = this.occlusion.trace(origin, l);
|
|
1928
|
+
if (hit) {
|
|
1929
|
+
hitWeight += weight;
|
|
1930
|
+
const radiance = this.bounceRadiance(hit, l);
|
|
1931
|
+
for (let k = 0; k < 3; k++) bounce[k] += radiance[k] * weight;
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
for (let k = 0; k < 3; k++)
|
|
1935
|
+
color[k] += specColor[k] * (1 - hitWeight / Math.max(totalWeight, 1e-8)) + bounce[k] / Math.max(totalWeight, 1e-8) * (f0[k] * A + B);
|
|
1936
|
+
return color;
|
|
1937
|
+
}
|
|
1938
|
+
};
|
|
1939
|
+
|
|
1253
1940
|
// lib/render/renderDrawCalls.ts
|
|
1254
1941
|
function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint8Bitmap) {
|
|
1255
1942
|
const options = resolveRenderOptions(optionsInput);
|
|
@@ -1265,7 +1952,12 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
|
|
|
1265
1952
|
options.up,
|
|
1266
1953
|
options.cameraRotation ?? null
|
|
1267
1954
|
);
|
|
1268
|
-
const renderer = new SoftwareRenderer(
|
|
1955
|
+
const renderer = new SoftwareRenderer(
|
|
1956
|
+
renderWidth,
|
|
1957
|
+
renderHeight,
|
|
1958
|
+
imageFactory,
|
|
1959
|
+
options.realistic
|
|
1960
|
+
);
|
|
1269
1961
|
let rgb = null;
|
|
1270
1962
|
if (options.backgroundColor) {
|
|
1271
1963
|
if (typeof options.backgroundColor === "string") {
|
|
@@ -1326,6 +2018,7 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
|
|
|
1326
2018
|
const blendDraws = allDrawCalls.filter(
|
|
1327
2019
|
(dc) => (dc.material.alphaMode ?? "OPAQUE") === "BLEND"
|
|
1328
2020
|
);
|
|
2021
|
+
const studio = options.realistic ? new StudioLighting(allDrawCalls, camera) : void 0;
|
|
1329
2022
|
const renderGroup = (dcs) => {
|
|
1330
2023
|
for (const dc of dcs) {
|
|
1331
2024
|
if (dc.mode === 1) {
|
|
@@ -1337,7 +2030,8 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
|
|
|
1337
2030
|
{ dir: options.lightDir, ambient: options.ambient },
|
|
1338
2031
|
dc.material,
|
|
1339
2032
|
options.cull,
|
|
1340
|
-
options.gamma
|
|
2033
|
+
options.gamma,
|
|
2034
|
+
studio
|
|
1341
2035
|
);
|
|
1342
2036
|
}
|
|
1343
2037
|
}
|
|
@@ -1361,6 +2055,7 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
|
|
|
1361
2055
|
});
|
|
1362
2056
|
}
|
|
1363
2057
|
renderGroup(blendDraws);
|
|
2058
|
+
renderer.denoise(options.gamma);
|
|
1364
2059
|
for (const dc of drawCalls) {
|
|
1365
2060
|
if (!dc.showHiddenEdges) continue;
|
|
1366
2061
|
const edgeDrawCall = createEdgeDrawCall(dc);
|
|
@@ -1399,7 +2094,7 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
|
|
|
1399
2094
|
}
|
|
1400
2095
|
|
|
1401
2096
|
// lib/gltf/createInfiniteGrid.ts
|
|
1402
|
-
import { mat4 as
|
|
2097
|
+
import { mat4 as mat47 } from "gl-matrix";
|
|
1403
2098
|
function createInfiniteGrid(options = {}) {
|
|
1404
2099
|
const cellSize = options.cellSize ?? 1;
|
|
1405
2100
|
const sectionSize = options.sectionSize ?? 10;
|
|
@@ -1423,9 +2118,9 @@ function createInfiniteGrid(options = {}) {
|
|
|
1423
2118
|
size
|
|
1424
2119
|
]);
|
|
1425
2120
|
const indices = new Uint32Array([0, 1, 2, 0, 2, 3]);
|
|
1426
|
-
const model =
|
|
2121
|
+
const model = mat47.create();
|
|
1427
2122
|
const offset = options.offset ?? {};
|
|
1428
|
-
|
|
2123
|
+
mat47.fromTranslation(model, [offset.x ?? 0, offset.y ?? 0, offset.z ?? 0]);
|
|
1429
2124
|
return {
|
|
1430
2125
|
positions,
|
|
1431
2126
|
indices,
|
|
@@ -1450,7 +2145,7 @@ function createInfiniteGrid(options = {}) {
|
|
|
1450
2145
|
}
|
|
1451
2146
|
|
|
1452
2147
|
// lib/gltf/createSceneFromGLTF.ts
|
|
1453
|
-
import { mat4 as
|
|
2148
|
+
import { mat4 as mat48 } from "gl-matrix";
|
|
1454
2149
|
var COMPONENT_INFO = {
|
|
1455
2150
|
5120: {
|
|
1456
2151
|
name: "BYTE",
|
|
@@ -1657,17 +2352,17 @@ function readIndices(ctx, accessorIndex) {
|
|
|
1657
2352
|
}
|
|
1658
2353
|
function nodeLocalMatrix(node) {
|
|
1659
2354
|
if (node.matrix) {
|
|
1660
|
-
const m2 =
|
|
2355
|
+
const m2 = mat48.create();
|
|
1661
2356
|
for (let i = 0; i < 16; i++) m2[i] = node.matrix[i];
|
|
1662
2357
|
return m2;
|
|
1663
2358
|
}
|
|
1664
2359
|
const translation = node.translation || [0, 0, 0];
|
|
1665
2360
|
const rotation = node.rotation || [0, 0, 0, 1];
|
|
1666
2361
|
const scale = node.scale || [1, 1, 1];
|
|
1667
|
-
const m =
|
|
1668
|
-
const tmp =
|
|
1669
|
-
|
|
1670
|
-
|
|
2362
|
+
const m = mat48.create();
|
|
2363
|
+
const tmp = mat48.create();
|
|
2364
|
+
mat48.fromRotationTranslationScale(tmp, rotation, translation, scale);
|
|
2365
|
+
mat48.copy(m, tmp);
|
|
1671
2366
|
return m;
|
|
1672
2367
|
}
|
|
1673
2368
|
function getMaterial(gltf, textures, images, materialIndex) {
|
|
@@ -1688,6 +2383,8 @@ function getMaterial(gltf, textures, images, materialIndex) {
|
|
|
1688
2383
|
return {
|
|
1689
2384
|
baseColorFactor: factor,
|
|
1690
2385
|
baseColorTexture: texImg,
|
|
2386
|
+
metallicFactor: pbr.metallicFactor ?? 1,
|
|
2387
|
+
roughnessFactor: pbr.roughnessFactor ?? 1,
|
|
1691
2388
|
alphaMode,
|
|
1692
2389
|
alphaCutoff
|
|
1693
2390
|
};
|
|
@@ -1705,8 +2402,8 @@ function createSceneFromGLTF(gltf, resources) {
|
|
|
1705
2402
|
const node = nodes[nodeIndex];
|
|
1706
2403
|
if (!node) return;
|
|
1707
2404
|
const local = nodeLocalMatrix(node);
|
|
1708
|
-
const world =
|
|
1709
|
-
|
|
2405
|
+
const world = mat48.create();
|
|
2406
|
+
mat48.multiply(world, parentMatrix, local);
|
|
1710
2407
|
if (Number.isInteger(node.mesh)) {
|
|
1711
2408
|
const mesh = meshes[node.mesh];
|
|
1712
2409
|
if (mesh) {
|
|
@@ -1745,7 +2442,7 @@ function createSceneFromGLTF(gltf, resources) {
|
|
|
1745
2442
|
traverse(childIndex, world);
|
|
1746
2443
|
}
|
|
1747
2444
|
}
|
|
1748
|
-
const identity =
|
|
2445
|
+
const identity = mat48.create();
|
|
1749
2446
|
for (const rootIndex of scene.nodes || []) {
|
|
1750
2447
|
traverse(rootIndex, identity);
|
|
1751
2448
|
}
|
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.31",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/tscircuit/poppygl.git"
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"scripts": {
|
|
13
13
|
"start": "cosmos",
|
|
14
14
|
"build:site": "cosmos-export",
|
|
15
|
-
"build": "tsup-node ./lib/index.ts --format esm --dts",
|
|
15
|
+
"build": "tsup-node ./lib/index.ts --format esm --dts --external @jscad/modeling",
|
|
16
16
|
"format": "biome format --write .",
|
|
17
17
|
"format:check": "biome format .",
|
|
18
18
|
"test": "bun test",
|
|
@@ -21,10 +21,12 @@
|
|
|
21
21
|
"type": "module",
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@biomejs/biome": "^2.2.4",
|
|
24
|
+
"@jscad/modeling": "2.13.0",
|
|
24
25
|
"@playwright/test": "^1.60.0",
|
|
25
26
|
"@tscircuit/alphabet": "0.0.23",
|
|
26
27
|
"@types/bun": "^1.2.23",
|
|
27
28
|
"@types/readable-stream": "^4.0.21",
|
|
29
|
+
"jscad-electronics": "0.0.187",
|
|
28
30
|
"looks-same": "^10.0.1",
|
|
29
31
|
"react": "^19.1.1",
|
|
30
32
|
"react-cosmos": "^7.0.0",
|