poppygl 0.0.33 → 0.0.35
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 +39 -4
- package/dist/index.d.ts +24 -2
- package/dist/index.js +438 -129
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -86,10 +86,12 @@ Realistic mode supports base-color textures and scalar glTF metallic/roughness
|
|
|
86
86
|
values. Normal maps, metallic/roughness textures, and custom HDR environments
|
|
87
87
|
are not yet supported. Transparent and alpha-cutout surfaces do not cast shadows.
|
|
88
88
|
|
|
89
|
-
Use realistic mode for saved images. At 600×600 with 2× supersampling,
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
Use realistic mode for saved images. At 600×600 with 2× supersampling, the RP2040
|
|
90
|
+
motor-controller benchmark takes 2.26–5.49 seconds per warm render across three
|
|
91
|
+
views, a 2.11–2.76× speedup over the previous realistic renderer on the same
|
|
92
|
+
machine. Blender comparison error increases by at most 0.47%. See
|
|
93
|
+
[benchmark results and reproduction steps](BENCHMARK.md#rp2040-motor-controller-500-sample-blender-baseline-and-2-gate).
|
|
94
|
+
Render times vary with model complexity and hardware.
|
|
93
95
|
|
|
94
96
|
### Camera and background
|
|
95
97
|
|
|
@@ -131,10 +133,43 @@ const png = await renderGLTFToPNGFromGLB(glb, {
|
|
|
131
133
|
- `cull`: enable back-face culling (default `true`).
|
|
132
134
|
- `backgroundColor`: a hex string such as `"#e5e7eb"`, or an RGB tuple with values from 0 to 1.
|
|
133
135
|
- `grid`: show a ground grid (default `false`), or provide grid options.
|
|
136
|
+
- `showEdges`: draw visible boundary and crease edges on all triangle meshes (default `false`).
|
|
137
|
+
- `showHiddenEdges`: draw visible edges plus faint dashed occluded edges on all meshes. Omit it or use `null` to preserve each model's flag; `false` disables those flags.
|
|
138
|
+
- `edgeWidth`: width of both visible and hidden mesh edges in output pixels (default `1`).
|
|
139
|
+
- `lineWidth`: width of glTF `LINES` and finite grid lines in output pixels (default `1`).
|
|
140
|
+
- `hiddenEdgeOpacity`: opacity of dashed occluded mesh edges, from `0` to `1` (default `0.35`).
|
|
134
141
|
- `fetchImpl`: optional override for resource loading (must match the `fetch` signature).
|
|
135
142
|
|
|
136
143
|
You can inspect the defaults via `getDefaultRenderOptions()` or reuse the internal merge logic with `resolveRenderOptions()`.
|
|
137
144
|
|
|
145
|
+
### Thicker edges and hidden geometry
|
|
146
|
+
|
|
147
|
+
Enable edges directly when rendering an ordinary glTF or GLB, without editing its metadata:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
const png = await renderGLTFToPNGFromGLB(glb, {
|
|
151
|
+
showHiddenEdges: true,
|
|
152
|
+
edgeWidth: 2.5,
|
|
153
|
+
hiddenEdgeOpacity: 0.35,
|
|
154
|
+
supersampling: 2,
|
|
155
|
+
backgroundColor: "#ffffff",
|
|
156
|
+
})
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Use `showEdges: true` for solid visible edges alone. Hidden segments use the
|
|
160
|
+
scene depth buffer and appear dashed; edge overlays never replace geometry depth.
|
|
161
|
+
Boundary and crease edges are extracted from the original triangle geometry,
|
|
162
|
+
excluding coplanar triangulation diagonals. Smooth curved silhouettes are not
|
|
163
|
+
additional extracted edges.
|
|
164
|
+
|
|
165
|
+
Widths stay constant in the final image when supersampling changes. Positive,
|
|
166
|
+
finite fractional widths are supported; invalid widths fall back to one pixel.
|
|
167
|
+
Wider strokes use antialiased screen-space coverage with round caps in the
|
|
168
|
+
software renderer. They are clipped before the perspective divide and remain
|
|
169
|
+
depth tested across their full width. No GPU or shader runtime is required.
|
|
170
|
+
`lineWidth` also applies to ordinary glTF line primitives and finite grid lines;
|
|
171
|
+
the procedural infinite grid keeps its existing independent styling.
|
|
172
|
+
|
|
138
173
|
### Hidden edges for selected sub-models
|
|
139
174
|
|
|
140
175
|
Set `showHiddenEdges` in a glTF node's `extras` to render that node with solid
|
package/dist/index.d.ts
CHANGED
|
@@ -31,6 +31,8 @@ interface DrawCall {
|
|
|
31
31
|
colors?: Float32Array | null;
|
|
32
32
|
mode?: number;
|
|
33
33
|
showHiddenEdges?: boolean;
|
|
34
|
+
/** Source triangle third vertices for each extracted edge segment. */
|
|
35
|
+
edgeAdjacentVertices?: number[][];
|
|
34
36
|
}
|
|
35
37
|
interface GridOptions {
|
|
36
38
|
size?: number | readonly [number, number, number];
|
|
@@ -100,6 +102,16 @@ interface RenderOptions {
|
|
|
100
102
|
debugFontSize?: number | null;
|
|
101
103
|
debugPointColor?: readonly [number, number, number] | null;
|
|
102
104
|
debugLabelColor?: readonly [number, number, number] | null;
|
|
105
|
+
/** Draw visible boundary and crease edges on every triangle mesh. */
|
|
106
|
+
showEdges: boolean;
|
|
107
|
+
/** Override model showHiddenEdges flags; null preserves each model's flag. */
|
|
108
|
+
showHiddenEdges: boolean | null;
|
|
109
|
+
/** Width of visible and hidden mesh edges in output pixels. */
|
|
110
|
+
edgeWidth: number;
|
|
111
|
+
/** Width of glTF LINES and finite grid lines in output pixels. */
|
|
112
|
+
lineWidth: number;
|
|
113
|
+
/** Opacity of dashed, occluded mesh edges, from 0 to 1. */
|
|
114
|
+
hiddenEdgeOpacity: number;
|
|
103
115
|
}
|
|
104
116
|
type RenderOptionsInput = Partial<RenderOptions>;
|
|
105
117
|
declare const DEFAULT_RENDER_OPTIONS: RenderOptions;
|
|
@@ -181,9 +193,13 @@ declare class StudioLighting {
|
|
|
181
193
|
private eye;
|
|
182
194
|
private shadowDirections;
|
|
183
195
|
private diffuseCache;
|
|
196
|
+
private reflectionSamples;
|
|
197
|
+
private normalDiffuseCache;
|
|
198
|
+
private specularCache;
|
|
199
|
+
private blendedSpecular;
|
|
184
200
|
constructor(drawCalls: DrawCall[], camera: Camera);
|
|
185
201
|
private bounceRadiance;
|
|
186
|
-
shade(position: V3, n: V3, base: V3, material: Material,
|
|
202
|
+
shade(position: V3, n: V3, base: V3, material: Material, pixelX?: number, pixelY?: number): V3;
|
|
187
203
|
}
|
|
188
204
|
|
|
189
205
|
interface LightSettings {
|
|
@@ -191,6 +207,8 @@ interface LightSettings {
|
|
|
191
207
|
ambient: number;
|
|
192
208
|
}
|
|
193
209
|
interface LineRenderOptions {
|
|
210
|
+
/** Stroke width in this renderer's raster pixels. Defaults to one. */
|
|
211
|
+
width?: number;
|
|
194
212
|
depthCompare?: "less" | "less-equal" | "greater";
|
|
195
213
|
depthWrite?: boolean;
|
|
196
214
|
depthBias?: number;
|
|
@@ -212,7 +230,11 @@ declare class SoftwareRenderer {
|
|
|
212
230
|
drawLines(mesh: DrawCall, camera: Camera, gammaOut?: boolean, options?: LineRenderOptions): void;
|
|
213
231
|
sampleTextureNearest(img: BitmapLike | null, u: number, v: number): MutableRGBA;
|
|
214
232
|
perspInterp(attrs: number[][], invWs: number[], lambdas: number[]): number[];
|
|
215
|
-
drawMesh(mesh: DrawCall, camera: Camera, light: LightSettings, material: Material, cullBackFaces?: boolean, gammaOut?: boolean, studio?: StudioLighting
|
|
233
|
+
drawMesh(mesh: DrawCall, camera: Camera, light: LightSettings, material: Material, cullBackFaces?: boolean, gammaOut?: boolean, studio?: StudioLighting, visibility?: {
|
|
234
|
+
winners: Int32Array;
|
|
235
|
+
triangleOffset: number;
|
|
236
|
+
depthOnly: boolean;
|
|
237
|
+
}): number;
|
|
216
238
|
}
|
|
217
239
|
|
|
218
240
|
/**
|
package/dist/index.js
CHANGED
|
@@ -271,7 +271,7 @@ function triangleNormal(positions, i0, i1, i2) {
|
|
|
271
271
|
if (length === 0) return null;
|
|
272
272
|
return [nx / length, ny / length, nz / length];
|
|
273
273
|
}
|
|
274
|
-
function addEdge(edges, positions, i0, i1, normal) {
|
|
274
|
+
function addEdge(edges, positions, i0, i1, thirdVertex, normal) {
|
|
275
275
|
const key0 = positionKey(positions, i0);
|
|
276
276
|
const key1 = positionKey(positions, i1);
|
|
277
277
|
const isForward = key0 < key1;
|
|
@@ -280,8 +280,13 @@ function addEdge(edges, positions, i0, i1, normal) {
|
|
|
280
280
|
const existing = edges.get(key);
|
|
281
281
|
if (existing) {
|
|
282
282
|
if (normal) existing.normals.push(normal);
|
|
283
|
+
existing.adjacentVertices.push(thirdVertex);
|
|
283
284
|
} else {
|
|
284
|
-
edges.set(key, {
|
|
285
|
+
edges.set(key, {
|
|
286
|
+
indices,
|
|
287
|
+
normals: normal ? [normal] : [],
|
|
288
|
+
adjacentVertices: [thirdVertex]
|
|
289
|
+
});
|
|
285
290
|
}
|
|
286
291
|
}
|
|
287
292
|
function hasCrease(normals, minimumDot) {
|
|
@@ -307,15 +312,17 @@ function createEdgeDrawCall(mesh, creaseAngleDegrees = DEFAULT_CREASE_ANGLE_DEGR
|
|
|
307
312
|
const i1 = triangleIndices[i + 1];
|
|
308
313
|
const i2 = triangleIndices[i + 2];
|
|
309
314
|
const normal = triangleNormal(mesh.positions, i0, i1, i2);
|
|
310
|
-
addEdge(edges, mesh.positions, i0, i1, normal);
|
|
311
|
-
addEdge(edges, mesh.positions, i1, i2, normal);
|
|
312
|
-
addEdge(edges, mesh.positions, i2, i0, normal);
|
|
315
|
+
addEdge(edges, mesh.positions, i0, i1, i2, normal);
|
|
316
|
+
addEdge(edges, mesh.positions, i1, i2, i0, normal);
|
|
317
|
+
addEdge(edges, mesh.positions, i2, i0, i1, normal);
|
|
313
318
|
}
|
|
314
319
|
const minimumDot = Math.cos(creaseAngleDegrees * Math.PI / 180);
|
|
315
320
|
const edgeIndices = [];
|
|
321
|
+
const edgeAdjacentVertices = [];
|
|
316
322
|
for (const edge2 of edges.values()) {
|
|
317
323
|
if (!hasCrease(edge2.normals, minimumDot)) continue;
|
|
318
324
|
edgeIndices.push(edge2.indices[0], edge2.indices[1]);
|
|
325
|
+
edgeAdjacentVertices.push(edge2.adjacentVertices);
|
|
319
326
|
}
|
|
320
327
|
if (edgeIndices.length === 0) return null;
|
|
321
328
|
return {
|
|
@@ -329,7 +336,8 @@ function createEdgeDrawCall(mesh, creaseAngleDegrees = DEFAULT_CREASE_ANGLE_DEGR
|
|
|
329
336
|
baseColorTexture: null,
|
|
330
337
|
alphaMode: "BLEND"
|
|
331
338
|
},
|
|
332
|
-
mode: 1
|
|
339
|
+
mode: 1,
|
|
340
|
+
edgeAdjacentVertices
|
|
333
341
|
};
|
|
334
342
|
}
|
|
335
343
|
|
|
@@ -490,7 +498,12 @@ var DEFAULT_RENDER_OPTIONS = {
|
|
|
490
498
|
debugPoints: null,
|
|
491
499
|
debugFontSize: null,
|
|
492
500
|
debugPointColor: null,
|
|
493
|
-
debugLabelColor: null
|
|
501
|
+
debugLabelColor: null,
|
|
502
|
+
showEdges: false,
|
|
503
|
+
showHiddenEdges: null,
|
|
504
|
+
edgeWidth: 1,
|
|
505
|
+
lineWidth: 1,
|
|
506
|
+
hiddenEdgeOpacity: 0.35
|
|
494
507
|
};
|
|
495
508
|
function getDefaultRenderOptions() {
|
|
496
509
|
return { ...DEFAULT_RENDER_OPTIONS };
|
|
@@ -598,14 +611,18 @@ var GeometryDenoiser = class {
|
|
|
598
611
|
this.albedo = new Float32Array(count * 3);
|
|
599
612
|
this.radiance = new Float32Array(count * 3);
|
|
600
613
|
this.distance = new Float32Array(count);
|
|
614
|
+
this.horizontal = new Float32Array(count * 3);
|
|
601
615
|
}
|
|
602
616
|
ids;
|
|
603
617
|
normals;
|
|
604
618
|
albedo;
|
|
605
619
|
radiance;
|
|
606
620
|
distance;
|
|
621
|
+
horizontal;
|
|
607
622
|
materials = /* @__PURE__ */ new WeakMap();
|
|
608
623
|
nextId = 1;
|
|
624
|
+
first = Infinity;
|
|
625
|
+
last = -1;
|
|
609
626
|
materialId(material) {
|
|
610
627
|
let id = this.materials.get(material);
|
|
611
628
|
if (!id) {
|
|
@@ -615,6 +632,8 @@ var GeometryDenoiser = class {
|
|
|
615
632
|
return id;
|
|
616
633
|
}
|
|
617
634
|
record(index, id, n, albedo, radiance, distance) {
|
|
635
|
+
if (index < this.first) this.first = index;
|
|
636
|
+
if (index > this.last) this.last = index;
|
|
618
637
|
this.ids[index] = id;
|
|
619
638
|
this.normals.set(n, index * 3);
|
|
620
639
|
this.albedo.set(albedo, index * 3);
|
|
@@ -622,43 +641,149 @@ var GeometryDenoiser = class {
|
|
|
622
641
|
this.distance[index] = distance;
|
|
623
642
|
}
|
|
624
643
|
apply(output, gamma) {
|
|
625
|
-
const kernel = [1,
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
644
|
+
const kernel = [1, 8, 28, 56, 70, 56, 28, 8, 1];
|
|
645
|
+
const radius = 4;
|
|
646
|
+
const n = this.normals, a = this.albedo, ids = this.ids;
|
|
647
|
+
const firstRow = Math.floor(this.first / this.width);
|
|
648
|
+
const lastRow = Math.floor(this.last / this.width);
|
|
649
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
650
|
+
const source = pass === 0 ? this.radiance : this.horizontal;
|
|
651
|
+
for (let y = firstRow; y <= lastRow; y++)
|
|
652
|
+
for (let x = 0; x < this.width; x++) {
|
|
653
|
+
const index = y * this.width + x, id = ids[index];
|
|
654
|
+
if (id === 0) continue;
|
|
655
|
+
const k = index * 3, distance = this.distance[index];
|
|
656
|
+
const nx = n[k], ny = n[k + 1], nz = n[k + 2];
|
|
657
|
+
const ar = a[k], ag = a[k + 1], ab = a[k + 2];
|
|
658
|
+
let r = 0, g = 0, b = 0, total = 0;
|
|
659
|
+
for (let delta = -radius; delta <= radius; delta++) {
|
|
660
|
+
const px = pass === 0 ? x + delta : x, py = pass === 0 ? y : y + delta;
|
|
635
661
|
if (px < 0 || py < 0 || px >= this.width || py >= this.height)
|
|
636
662
|
continue;
|
|
637
663
|
const neighbor = py * this.width + px, p = neighbor * 3;
|
|
638
|
-
if (
|
|
639
|
-
if (Math.abs(
|
|
640
|
-
continue;
|
|
641
|
-
if (n[k] * n[p] + n[k + 1] * n[p + 1] + n[k + 2] * n[p + 2] < 0.98)
|
|
664
|
+
if (ids[neighbor] !== id) continue;
|
|
665
|
+
if (Math.abs(distance - this.distance[neighbor]) > distance * 0.02)
|
|
642
666
|
continue;
|
|
643
|
-
if (
|
|
667
|
+
if (nx * n[p] + ny * n[p + 1] + nz * n[p + 2] < 0.98) continue;
|
|
668
|
+
if (Math.abs(ar - a[p]) + Math.abs(ag - a[p + 1]) + Math.abs(ab - a[p + 2]) > 0.03)
|
|
644
669
|
continue;
|
|
645
|
-
const weight = kernel[
|
|
670
|
+
const weight = kernel[delta + radius];
|
|
646
671
|
total += weight;
|
|
647
|
-
r +=
|
|
648
|
-
g +=
|
|
649
|
-
b +=
|
|
672
|
+
r += source[p] * weight;
|
|
673
|
+
g += source[p + 1] * weight;
|
|
674
|
+
b += source[p + 2] * weight;
|
|
650
675
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
676
|
+
if (pass === 0) {
|
|
677
|
+
this.horizontal[k] = r / total;
|
|
678
|
+
this.horizontal[k + 1] = g / total;
|
|
679
|
+
this.horizontal[k + 2] = b / total;
|
|
680
|
+
} else {
|
|
681
|
+
const encode2 = (value) => Math.floor(
|
|
682
|
+
255 * (gamma ? srgbEncodeLinear01(
|
|
683
|
+
Math.max(0, Math.min(1, value / total))
|
|
684
|
+
) : Math.max(0, Math.min(1, value / total)))
|
|
685
|
+
);
|
|
686
|
+
output[index * 4] = encode2(r);
|
|
687
|
+
output[index * 4 + 1] = encode2(g);
|
|
688
|
+
output[index * 4 + 2] = encode2(b);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
659
692
|
}
|
|
660
693
|
};
|
|
661
694
|
|
|
695
|
+
// lib/render/rasterizeWideLine.ts
|
|
696
|
+
function clipLine(start, end, marginX, marginY) {
|
|
697
|
+
if (![...start, ...end].every(Number.isFinite)) return null;
|
|
698
|
+
const distances = (p) => [
|
|
699
|
+
p[3] * (1 + marginX) + p[0],
|
|
700
|
+
p[3] * (1 + marginX) - p[0],
|
|
701
|
+
p[3] * (1 + marginY) + p[1],
|
|
702
|
+
p[3] * (1 + marginY) - p[1],
|
|
703
|
+
p[3] + p[2],
|
|
704
|
+
p[3] - p[2]
|
|
705
|
+
];
|
|
706
|
+
const a = distances(start);
|
|
707
|
+
const b = distances(end);
|
|
708
|
+
let from = 0;
|
|
709
|
+
let to = 1;
|
|
710
|
+
for (let plane = 0; plane < 6; plane++) {
|
|
711
|
+
if (a[plane] < 0 && b[plane] < 0) return null;
|
|
712
|
+
if (a[plane] < 0 !== b[plane] < 0) {
|
|
713
|
+
const t = a[plane] / (a[plane] - b[plane]);
|
|
714
|
+
if (a[plane] < 0) from = Math.max(from, t);
|
|
715
|
+
else to = Math.min(to, t);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
if (from > to) return null;
|
|
719
|
+
return [from, to].map(
|
|
720
|
+
(t) => Array.from(start, (value, axis) => value + t * (end[axis] - value))
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
function rasterizeWideLine(start, end, width, height, strokeWidth, dashed, dashLength, fragment) {
|
|
724
|
+
const radius = strokeWidth / 2;
|
|
725
|
+
const padding = radius + 0.5;
|
|
726
|
+
const clipped = clipLine(
|
|
727
|
+
start,
|
|
728
|
+
end,
|
|
729
|
+
2 * padding / Math.max(1, width - 1),
|
|
730
|
+
2 * padding / Math.max(1, height - 1)
|
|
731
|
+
);
|
|
732
|
+
if (!clipped) return;
|
|
733
|
+
const project = (p) => [
|
|
734
|
+
(p[0] / p[3] * 0.5 + 0.5) * (width - 1),
|
|
735
|
+
(0.5 - p[1] / p[3] * 0.5) * (height - 1),
|
|
736
|
+
// Clipping guarantees this range; absorb near/far-plane roundoff so an
|
|
737
|
+
// endpoint's round cap is not discarded at z = -epsilon.
|
|
738
|
+
Math.max(0, Math.min(1, p[2] / p[3] * 0.5 + 0.5))
|
|
739
|
+
];
|
|
740
|
+
const a = project(clipped[0]);
|
|
741
|
+
const b = project(clipped[1]);
|
|
742
|
+
if (![...a, ...b].every(Number.isFinite)) return;
|
|
743
|
+
const [x0, y0, z0] = a;
|
|
744
|
+
const [x1, y1, z1] = b;
|
|
745
|
+
const dx = x1 - x0;
|
|
746
|
+
const dy = y1 - y0;
|
|
747
|
+
const lengthSquared = dx * dx + dy * dy;
|
|
748
|
+
const length = Math.sqrt(lengthSquared);
|
|
749
|
+
const original = start[3] > 0 ? project(start) : a;
|
|
750
|
+
const phase = Math.hypot(x0 - original[0], y0 - original[1]);
|
|
751
|
+
const drawPixel = (x, y) => {
|
|
752
|
+
const t = lengthSquared === 0 ? 0 : Math.max(
|
|
753
|
+
0,
|
|
754
|
+
Math.min(1, ((x - x0) * dx + (y - y0) * dy) / lengthSquared)
|
|
755
|
+
);
|
|
756
|
+
if (dashed && Math.floor((phase + t * length) / dashLength) % 2 === 1)
|
|
757
|
+
return;
|
|
758
|
+
const distance = Math.hypot(x - (x0 + t * dx), y - (y0 + t * dy));
|
|
759
|
+
const coverage = Math.min(1, padding - distance);
|
|
760
|
+
if (coverage > 0) fragment(x, y, z0 + t * (z1 - z0), coverage);
|
|
761
|
+
};
|
|
762
|
+
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
763
|
+
const band = length === 0 ? padding : padding * length / Math.abs(dx);
|
|
764
|
+
const first = Math.max(0, Math.ceil(Math.min(x0, x1) - padding));
|
|
765
|
+
const last = Math.min(width - 1, Math.floor(Math.max(x0, x1) + padding));
|
|
766
|
+
for (let x = first; x <= last; x++) {
|
|
767
|
+
const t = dx === 0 ? 0 : Math.max(0, Math.min(1, (x - x0) / dx));
|
|
768
|
+
const centerY = y0 + t * dy;
|
|
769
|
+
const top = Math.max(0, Math.ceil(centerY - band));
|
|
770
|
+
const bottom = Math.min(height - 1, Math.floor(centerY + band));
|
|
771
|
+
for (let y = top; y <= bottom; y++) drawPixel(x, y);
|
|
772
|
+
}
|
|
773
|
+
} else {
|
|
774
|
+
const band = padding * length / Math.abs(dy);
|
|
775
|
+
const first = Math.max(0, Math.ceil(Math.min(y0, y1) - padding));
|
|
776
|
+
const last = Math.min(height - 1, Math.floor(Math.max(y0, y1) + padding));
|
|
777
|
+
for (let y = first; y <= last; y++) {
|
|
778
|
+
const t = Math.max(0, Math.min(1, (y - y0) / dy));
|
|
779
|
+
const centerX = x0 + t * dx;
|
|
780
|
+
const left = Math.max(0, Math.ceil(centerX - band));
|
|
781
|
+
const right = Math.min(width - 1, Math.floor(centerX + band));
|
|
782
|
+
for (let x = left; x <= right; x++) drawPixel(x, y);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
662
787
|
// lib/render/SoftwareRenderer.ts
|
|
663
788
|
var SoftwareRenderer = class {
|
|
664
789
|
width;
|
|
@@ -711,6 +836,7 @@ var SoftwareRenderer = class {
|
|
|
711
836
|
const vScreen = new Array(vertexCount);
|
|
712
837
|
const vInvW = new Float32Array(vertexCount);
|
|
713
838
|
const vNDCz = new Float32Array(vertexCount);
|
|
839
|
+
const clipVertices = [];
|
|
714
840
|
for (let i = 0; i < vertexCount; i++) {
|
|
715
841
|
const p = vec42.fromValues(
|
|
716
842
|
positions[i * 3 + 0],
|
|
@@ -720,6 +846,7 @@ var SoftwareRenderer = class {
|
|
|
720
846
|
);
|
|
721
847
|
const c = vec42.create();
|
|
722
848
|
vec42.transformMat4(c, p, mvp);
|
|
849
|
+
clipVertices.push(c);
|
|
723
850
|
const invW = 1 / c[3];
|
|
724
851
|
vInvW[i] = invW;
|
|
725
852
|
if (!isFinite(invW)) {
|
|
@@ -754,9 +881,76 @@ var SoftwareRenderer = class {
|
|
|
754
881
|
const depthWrite = options.depthWrite ?? true;
|
|
755
882
|
const depthBias = options.depthBias ?? 0;
|
|
756
883
|
const dashLength = Math.max(1, options.dashLength ?? 4);
|
|
884
|
+
const strokeWidth = typeof options.width === "number" && Number.isFinite(options.width) && options.width > 0 ? options.width : 1;
|
|
885
|
+
const drawFragment = (xi, yi, z, coverage = 1) => {
|
|
886
|
+
if (z < 0 || z > 1) return;
|
|
887
|
+
const di = yi * this.width + xi;
|
|
888
|
+
const currentDepth = this.depth[di];
|
|
889
|
+
const passesDepth = depthCompare === "greater" ? z > currentDepth + depthBias : depthCompare === "less-equal" ? z <= currentDepth + depthBias : z < currentDepth - depthBias;
|
|
890
|
+
if (!passesDepth) return;
|
|
891
|
+
if (depthWrite) this.depth[di] = z;
|
|
892
|
+
const pixelIndex = di * 4;
|
|
893
|
+
const destinationAlpha = this.buffer[pixelIndex + 3] / 255;
|
|
894
|
+
const alpha = sourceAlpha * coverage;
|
|
895
|
+
const oneMinusSourceAlpha = 1 - alpha;
|
|
896
|
+
const outAlpha = alpha + destinationAlpha * oneMinusSourceAlpha;
|
|
897
|
+
const blend = (channel, offset) => outAlpha === 0 ? 0 : (channel * alpha + this.buffer[pixelIndex + offset] * destinationAlpha * oneMinusSourceAlpha) / outAlpha;
|
|
898
|
+
this.setPixel(
|
|
899
|
+
xi,
|
|
900
|
+
yi,
|
|
901
|
+
blend(r255, 0),
|
|
902
|
+
blend(g255, 1),
|
|
903
|
+
blend(b255, 2),
|
|
904
|
+
outAlpha * 255
|
|
905
|
+
);
|
|
906
|
+
};
|
|
757
907
|
for (let i = 0; i < indices.length; i += 2) {
|
|
758
908
|
const i0 = indices[i + 0];
|
|
759
909
|
const i1 = indices[i + 1];
|
|
910
|
+
if (strokeWidth !== 1) {
|
|
911
|
+
const sourceFaces = (mesh.edgeAdjacentVertices?.[i / 2] ?? []).flatMap(
|
|
912
|
+
(third) => {
|
|
913
|
+
if (clipVertices[i0][3] <= 0 || clipVertices[i1][3] <= 0 || clipVertices[third][3] <= 0)
|
|
914
|
+
return [];
|
|
915
|
+
const a2 = vScreen[i0];
|
|
916
|
+
const b2 = vScreen[i1];
|
|
917
|
+
const c = vScreen[third];
|
|
918
|
+
const area = edge(a2, b2, c);
|
|
919
|
+
if (!Number.isFinite(area) || area === 0) return [];
|
|
920
|
+
return [{ a: a2, b: b2, c, inverseArea: 1 / area, third }];
|
|
921
|
+
}
|
|
922
|
+
);
|
|
923
|
+
rasterizeWideLine(
|
|
924
|
+
clipVertices[i0],
|
|
925
|
+
clipVertices[i1],
|
|
926
|
+
this.width,
|
|
927
|
+
this.height,
|
|
928
|
+
strokeWidth,
|
|
929
|
+
options.dashed ?? false,
|
|
930
|
+
dashLength,
|
|
931
|
+
sourceFaces.length === 0 ? drawFragment : (x2, y2, z2, coverage) => {
|
|
932
|
+
let surfaceDepth = Infinity;
|
|
933
|
+
const sample = [x2 + 0.5, y2 + 0.5];
|
|
934
|
+
for (const face of sourceFaces) {
|
|
935
|
+
const l0 = edge(face.b, face.c, sample) * face.inverseArea;
|
|
936
|
+
const l1 = edge(face.c, face.a, sample) * face.inverseArea;
|
|
937
|
+
const l2 = 1 - l0 - l1;
|
|
938
|
+
if (l0 < -1e-6 || l1 < -1e-6 || l2 < -1e-6) continue;
|
|
939
|
+
const depth = (l0 * vNDCz[i0] + l1 * vNDCz[i1] + l2 * vNDCz[face.third]) * 0.5 + 0.5;
|
|
940
|
+
if (depth >= 0 && depth <= 1) {
|
|
941
|
+
surfaceDepth = Math.min(surfaceDepth, depth);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
drawFragment(
|
|
945
|
+
x2,
|
|
946
|
+
y2,
|
|
947
|
+
surfaceDepth === Infinity ? z2 : surfaceDepth,
|
|
948
|
+
coverage
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
);
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
760
954
|
const v0s = vScreen[i0];
|
|
761
955
|
const v1s = vScreen[i1];
|
|
762
956
|
const z0_ndc = vNDCz[i0];
|
|
@@ -789,20 +983,7 @@ var SoftwareRenderer = class {
|
|
|
789
983
|
const xi = Math.round(x);
|
|
790
984
|
const yi = Math.round(y);
|
|
791
985
|
if (xi >= 0 && xi < this.width && yi >= 0 && yi < this.height && z >= 0 && z <= 1) {
|
|
792
|
-
|
|
793
|
-
const currentDepth = this.depth[di];
|
|
794
|
-
const passesDepth = depthCompare === "greater" ? z > currentDepth + depthBias : depthCompare === "less-equal" ? z <= currentDepth + depthBias : z < currentDepth - depthBias;
|
|
795
|
-
if (passesDepth) {
|
|
796
|
-
if (depthWrite) this.depth[di] = z;
|
|
797
|
-
const pixelIndex = di * 4;
|
|
798
|
-
const destinationAlpha = this.buffer[pixelIndex + 3] / 255;
|
|
799
|
-
const oneMinusSourceAlpha = 1 - sourceAlpha;
|
|
800
|
-
const outAlpha = sourceAlpha + destinationAlpha * oneMinusSourceAlpha;
|
|
801
|
-
const outR = outAlpha === 0 ? 0 : (r255 * sourceAlpha + this.buffer[pixelIndex + 0] * destinationAlpha * oneMinusSourceAlpha) / outAlpha;
|
|
802
|
-
const outG = outAlpha === 0 ? 0 : (g255 * sourceAlpha + this.buffer[pixelIndex + 1] * destinationAlpha * oneMinusSourceAlpha) / outAlpha;
|
|
803
|
-
const outB = outAlpha === 0 ? 0 : (b255 * sourceAlpha + this.buffer[pixelIndex + 2] * destinationAlpha * oneMinusSourceAlpha) / outAlpha;
|
|
804
|
-
this.setPixel(xi, yi, outR, outG, outB, outAlpha * 255);
|
|
805
|
-
}
|
|
986
|
+
drawFragment(xi, yi, z);
|
|
806
987
|
}
|
|
807
988
|
x += xinc;
|
|
808
989
|
y += yinc;
|
|
@@ -835,9 +1016,15 @@ var SoftwareRenderer = class {
|
|
|
835
1016
|
}
|
|
836
1017
|
return out;
|
|
837
1018
|
}
|
|
838
|
-
drawMesh(mesh, camera, light, material, cullBackFaces = true, gammaOut = true, studio) {
|
|
1019
|
+
drawMesh(mesh, camera, light, material, cullBackFaces = true, gammaOut = true, studio, visibility) {
|
|
839
1020
|
let { positions, normals, uvs, indices, model, colors } = mesh;
|
|
840
1021
|
const materialId = this.denoiser?.materialId(material) ?? 0;
|
|
1022
|
+
const lightDir = light.dir ?? DEFAULT_LIGHT_DIR;
|
|
1023
|
+
const ambient = clamp(light.ambient ?? DEFAULT_RENDER_OPTIONS.ambient, 0, 1);
|
|
1024
|
+
const L = studio ? null : vec32.normalize(
|
|
1025
|
+
vec32.create(),
|
|
1026
|
+
vec32.fromValues(lightDir[0], lightDir[1], lightDir[2])
|
|
1027
|
+
);
|
|
841
1028
|
const view = camera.view;
|
|
842
1029
|
const proj = camera.proj;
|
|
843
1030
|
const mvp = mat43.create();
|
|
@@ -864,12 +1051,17 @@ var SoftwareRenderer = class {
|
|
|
864
1051
|
colors = clipped.colors;
|
|
865
1052
|
idx = clipped.indices;
|
|
866
1053
|
vertexCount = positions.length / 3;
|
|
1054
|
+
const depthOnly = visibility?.depthOnly ?? false;
|
|
867
1055
|
const vScreen = new Array(vertexCount);
|
|
868
1056
|
const vInvW = new Float32Array(vertexCount);
|
|
869
1057
|
const vNDCz = new Float32Array(vertexCount);
|
|
870
|
-
const vWorldN = new Array(
|
|
871
|
-
|
|
872
|
-
|
|
1058
|
+
const vWorldN = new Array(
|
|
1059
|
+
depthOnly ? 0 : vertexCount
|
|
1060
|
+
);
|
|
1061
|
+
const vWorldP = studio && !depthOnly ? new Array(vertexCount) : null;
|
|
1062
|
+
const vColor = new Array(
|
|
1063
|
+
depthOnly ? 0 : vertexCount
|
|
1064
|
+
);
|
|
873
1065
|
for (let i = 0; i < vertexCount; i++) {
|
|
874
1066
|
const p = vec42.fromValues(
|
|
875
1067
|
positions[i * 3 + 0],
|
|
@@ -892,6 +1084,7 @@ var SoftwareRenderer = class {
|
|
|
892
1084
|
vScreen[i] = [sx, sy];
|
|
893
1085
|
vInvW[i] = invW;
|
|
894
1086
|
vNDCz[i] = ndcZ;
|
|
1087
|
+
if (depthOnly) continue;
|
|
895
1088
|
const n = vec32.fromValues(
|
|
896
1089
|
useNormals[i * 3 + 0],
|
|
897
1090
|
useNormals[i * 3 + 1],
|
|
@@ -969,9 +1162,15 @@ var SoftwareRenderer = class {
|
|
|
969
1162
|
const z01 = zndc * 0.5 + 0.5;
|
|
970
1163
|
if (z01 < 0 || z01 > 1) continue;
|
|
971
1164
|
const di = y * this.width + x;
|
|
1165
|
+
if (visibility && !visibility.depthOnly && visibility.winners[di] !== visibility.triangleOffset + i / 3)
|
|
1166
|
+
continue;
|
|
972
1167
|
const depth = this.depth;
|
|
973
1168
|
if (z01 >= depth[di]) continue;
|
|
974
1169
|
depth[di] = z01;
|
|
1170
|
+
if (visibility?.depthOnly) {
|
|
1171
|
+
visibility.winners[di] = visibility.triangleOffset + i / 3;
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
975
1174
|
let baseColor = [
|
|
976
1175
|
material.baseColorFactor[0],
|
|
977
1176
|
material.baseColorFactor[1],
|
|
@@ -1006,24 +1205,7 @@ var SoftwareRenderer = class {
|
|
|
1006
1205
|
np1 / nlen,
|
|
1007
1206
|
np2 / nlen
|
|
1008
1207
|
];
|
|
1009
|
-
|
|
1010
|
-
const ambient = clamp(
|
|
1011
|
-
light.ambient ?? DEFAULT_RENDER_OPTIONS.ambient,
|
|
1012
|
-
0,
|
|
1013
|
-
1
|
|
1014
|
-
);
|
|
1015
|
-
const L = vec32.normalize(
|
|
1016
|
-
vec32.create(),
|
|
1017
|
-
vec32.fromValues(lightDir[0], lightDir[1], lightDir[2])
|
|
1018
|
-
);
|
|
1019
|
-
const ndotl = Math.max(
|
|
1020
|
-
0,
|
|
1021
|
-
nrm[0] * -L[0] + nrm[1] * -L[1] + nrm[2] * -L[2]
|
|
1022
|
-
);
|
|
1023
|
-
const lit = ambient + (1 - ambient) * ndotl;
|
|
1024
|
-
let r = baseColor[0] * lit;
|
|
1025
|
-
let g = baseColor[1] * lit;
|
|
1026
|
-
let b = baseColor[2] * lit;
|
|
1208
|
+
let r = 0, g = 0, b = 0;
|
|
1027
1209
|
if (studio && worldPositions) {
|
|
1028
1210
|
const position = this.perspInterp(worldPositions, invW, [
|
|
1029
1211
|
l0,
|
|
@@ -1035,8 +1217,18 @@ var SoftwareRenderer = class {
|
|
|
1035
1217
|
nrm,
|
|
1036
1218
|
[baseColor[0], baseColor[1], baseColor[2]],
|
|
1037
1219
|
material,
|
|
1038
|
-
|
|
1220
|
+
x,
|
|
1221
|
+
y
|
|
1039
1222
|
);
|
|
1223
|
+
} else {
|
|
1224
|
+
const ndotl = Math.max(
|
|
1225
|
+
0,
|
|
1226
|
+
nrm[0] * -L[0] + nrm[1] * -L[1] + nrm[2] * -L[2]
|
|
1227
|
+
);
|
|
1228
|
+
const lit = ambient + (1 - ambient) * ndotl;
|
|
1229
|
+
r = baseColor[0] * lit;
|
|
1230
|
+
g = baseColor[1] * lit;
|
|
1231
|
+
b = baseColor[2] * lit;
|
|
1040
1232
|
}
|
|
1041
1233
|
const emission = material.emissiveFactor ?? [0, 0, 0];
|
|
1042
1234
|
const strength = material.emissiveStrength ?? 1;
|
|
@@ -1117,6 +1309,7 @@ var SoftwareRenderer = class {
|
|
|
1117
1309
|
}
|
|
1118
1310
|
}
|
|
1119
1311
|
}
|
|
1312
|
+
return idx.length / 3;
|
|
1120
1313
|
}
|
|
1121
1314
|
};
|
|
1122
1315
|
function edge(a, b, p) {
|
|
@@ -1463,9 +1656,17 @@ function resolveRenderOptions(options = {}) {
|
|
|
1463
1656
|
camPos: options.camPos !== void 0 ? options.camPos : DEFAULT_RENDER_OPTIONS.camPos,
|
|
1464
1657
|
lookAt: options.lookAt !== void 0 ? options.lookAt : DEFAULT_RENDER_OPTIONS.lookAt,
|
|
1465
1658
|
up: resolveCameraUp(options.up),
|
|
1466
|
-
cameraRotation: resolveCameraRotation(options.cameraRotation)
|
|
1659
|
+
cameraRotation: resolveCameraRotation(options.cameraRotation),
|
|
1660
|
+
showEdges: options.showEdges === true,
|
|
1661
|
+
showHiddenEdges: typeof options.showHiddenEdges === "boolean" ? options.showHiddenEdges : null,
|
|
1662
|
+
edgeWidth: resolveWidth(options.edgeWidth),
|
|
1663
|
+
lineWidth: resolveWidth(options.lineWidth),
|
|
1664
|
+
hiddenEdgeOpacity: typeof options.hiddenEdgeOpacity === "number" && Number.isFinite(options.hiddenEdgeOpacity) ? Math.max(0, Math.min(1, options.hiddenEdgeOpacity)) : DEFAULT_RENDER_OPTIONS.hiddenEdgeOpacity
|
|
1467
1665
|
};
|
|
1468
1666
|
}
|
|
1667
|
+
function resolveWidth(width) {
|
|
1668
|
+
return typeof width === "number" && Number.isFinite(width) && width > 0 ? width : 1;
|
|
1669
|
+
}
|
|
1469
1670
|
|
|
1470
1671
|
// lib/render/StudioLighting.ts
|
|
1471
1672
|
import { mat4 as mat46 } from "gl-matrix";
|
|
@@ -1502,15 +1703,16 @@ var RayOcclusion = class {
|
|
|
1502
1703
|
if ((dc.mode ?? 4) !== 4 || dc.material.alphaMode === "BLEND") continue;
|
|
1503
1704
|
if (dc.material.alphaMode === "MASK") continue;
|
|
1504
1705
|
const count = dc.indices?.length ?? dc.positions.length / 3;
|
|
1505
|
-
const
|
|
1506
|
-
|
|
1706
|
+
const worldVertices = [];
|
|
1707
|
+
for (let i = 0; i < dc.positions.length; i += 3) {
|
|
1507
1708
|
const p = vec33.transformMat4(
|
|
1508
1709
|
vec33.create(),
|
|
1509
1710
|
[dc.positions[i], dc.positions[i + 1], dc.positions[i + 2]],
|
|
1510
1711
|
dc.model
|
|
1511
1712
|
);
|
|
1512
|
-
|
|
1513
|
-
}
|
|
1713
|
+
worldVertices.push([p[0], p[1], p[2]]);
|
|
1714
|
+
}
|
|
1715
|
+
const vertex = (index) => worldVertices[dc.indices?.[index] ?? index];
|
|
1514
1716
|
for (let i = 0; i < count; i += 3) {
|
|
1515
1717
|
const p = vertex(i), q = vertex(i + 1), r = vertex(i + 2);
|
|
1516
1718
|
const min = [0, 1, 2].map(
|
|
@@ -1586,52 +1788,97 @@ var RayOcclusion = class {
|
|
|
1586
1788
|
}
|
|
1587
1789
|
}
|
|
1588
1790
|
build(triangles) {
|
|
1589
|
-
const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
|
|
1791
|
+
const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity], centerMin = [Infinity, Infinity, Infinity], centerMax = [-Infinity, -Infinity, -Infinity];
|
|
1590
1792
|
for (const t of triangles)
|
|
1591
1793
|
for (let a = 0; a < 3; a++) {
|
|
1592
1794
|
min[a] = Math.min(min[a], t.min[a]);
|
|
1593
1795
|
max[a] = Math.max(max[a], t.max[a]);
|
|
1796
|
+
centerMin[a] = Math.min(centerMin[a], t.center[a]);
|
|
1797
|
+
centerMax[a] = Math.max(centerMax[a], t.center[a]);
|
|
1594
1798
|
}
|
|
1595
1799
|
if (triangles.length <= 8) return { min, max, triangles };
|
|
1800
|
+
const binCount = 16;
|
|
1596
1801
|
const area = (lo, hi) => {
|
|
1597
1802
|
const x = hi[0] - lo[0], y = hi[1] - lo[1], z = hi[2] - lo[2];
|
|
1598
1803
|
return 2 * (x * y + y * z + z * x);
|
|
1599
1804
|
};
|
|
1600
|
-
let bestCost = Infinity,
|
|
1805
|
+
let bestCost = Infinity, bestAxis = -1, bestSplit = 0, bestMin = 0, bestScale = 0;
|
|
1806
|
+
let longestAxis = 0;
|
|
1807
|
+
for (let a = 1; a < 3; a++)
|
|
1808
|
+
if (centerMax[a] - centerMin[a] > centerMax[longestAxis] - centerMin[longestAxis])
|
|
1809
|
+
longestAxis = a;
|
|
1601
1810
|
for (let axis = 0; axis < 3; axis++) {
|
|
1602
|
-
|
|
1603
|
-
const
|
|
1604
|
-
|
|
1605
|
-
|
|
1811
|
+
if (triangles.length <= 4096 && axis !== longestAxis) continue;
|
|
1812
|
+
const loCenter = centerMin[axis], hiCenter = centerMax[axis];
|
|
1813
|
+
if (loCenter === hiCenter) continue;
|
|
1814
|
+
const scale = binCount / (hiCenter - loCenter);
|
|
1815
|
+
const counts = new Uint32Array(binCount);
|
|
1816
|
+
const bounds = new Float64Array(binCount * 6);
|
|
1817
|
+
for (let i = 0; i < binCount; i++) {
|
|
1818
|
+
bounds.fill(Infinity, i * 6, i * 6 + 3);
|
|
1819
|
+
bounds.fill(-Infinity, i * 6 + 3, i * 6 + 6);
|
|
1820
|
+
}
|
|
1821
|
+
for (const t of triangles) {
|
|
1822
|
+
const bin = Math.min(
|
|
1823
|
+
binCount - 1,
|
|
1824
|
+
Math.floor((t.center[axis] - loCenter) * scale)
|
|
1825
|
+
);
|
|
1826
|
+
counts[bin]++;
|
|
1827
|
+
const offset = bin * 6;
|
|
1606
1828
|
for (let a = 0; a < 3; a++) {
|
|
1607
|
-
|
|
1608
|
-
|
|
1829
|
+
bounds[offset + a] = Math.min(bounds[offset + a], t.min[a]);
|
|
1830
|
+
bounds[offset + a + 3] = Math.max(bounds[offset + a + 3], t.max[a]);
|
|
1609
1831
|
}
|
|
1610
|
-
|
|
1832
|
+
}
|
|
1833
|
+
const suffixCost = new Float64Array(binCount);
|
|
1834
|
+
let lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity], count = 0;
|
|
1835
|
+
for (let i = binCount - 1; i >= 0; i--) {
|
|
1836
|
+
count += counts[i];
|
|
1837
|
+
if (counts[i])
|
|
1838
|
+
for (let a = 0; a < 3; a++) {
|
|
1839
|
+
lo[a] = Math.min(lo[a], bounds[i * 6 + a]);
|
|
1840
|
+
hi[a] = Math.max(hi[a], bounds[i * 6 + a + 3]);
|
|
1841
|
+
}
|
|
1842
|
+
suffixCost[i] = count ? area(lo, hi) * count : 0;
|
|
1611
1843
|
}
|
|
1612
1844
|
lo = [Infinity, Infinity, Infinity];
|
|
1613
1845
|
hi = [-Infinity, -Infinity, -Infinity];
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1846
|
+
count = 0;
|
|
1847
|
+
for (let i = 0; i < binCount - 1; i++) {
|
|
1848
|
+
count += counts[i];
|
|
1849
|
+
if (counts[i])
|
|
1850
|
+
for (let a = 0; a < 3; a++) {
|
|
1851
|
+
lo[a] = Math.min(lo[a], bounds[i * 6 + a]);
|
|
1852
|
+
hi[a] = Math.max(hi[a], bounds[i * 6 + a + 3]);
|
|
1853
|
+
}
|
|
1854
|
+
if (!count || count === triangles.length) continue;
|
|
1855
|
+
const cost = area(lo, hi) * count + suffixCost[i + 1];
|
|
1622
1856
|
if (cost < bestCost) {
|
|
1623
1857
|
bestCost = cost;
|
|
1624
|
-
|
|
1625
|
-
|
|
1858
|
+
bestAxis = axis;
|
|
1859
|
+
bestSplit = i;
|
|
1860
|
+
bestMin = loCenter;
|
|
1861
|
+
bestScale = scale;
|
|
1626
1862
|
}
|
|
1627
1863
|
}
|
|
1628
1864
|
}
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
left
|
|
1633
|
-
right
|
|
1634
|
-
}
|
|
1865
|
+
let left, right;
|
|
1866
|
+
if (bestAxis < 0) {
|
|
1867
|
+
const middle = triangles.length >> 1;
|
|
1868
|
+
left = triangles.slice(0, middle);
|
|
1869
|
+
right = triangles.slice(middle);
|
|
1870
|
+
} else {
|
|
1871
|
+
left = [];
|
|
1872
|
+
right = [];
|
|
1873
|
+
for (const t of triangles) {
|
|
1874
|
+
const bin = Math.min(
|
|
1875
|
+
binCount - 1,
|
|
1876
|
+
Math.floor((t.center[bestAxis] - bestMin) * bestScale)
|
|
1877
|
+
);
|
|
1878
|
+
(bin <= bestSplit ? left : right).push(t);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
return { min, max, left: this.build(left), right: this.build(right) };
|
|
1635
1882
|
}
|
|
1636
1883
|
prepareRay(direction) {
|
|
1637
1884
|
this.parallelAxes = 0;
|
|
@@ -1766,6 +2013,9 @@ var W = 96;
|
|
|
1766
2013
|
var H = 48;
|
|
1767
2014
|
var SAMPLES = 128;
|
|
1768
2015
|
var ROUGHNESSES = [0.04, 0.1, 0.18, 0.28, 0.4, 0.55, 0.7, 0.85, 1];
|
|
2016
|
+
var SHADOW_SAMPLES = 4;
|
|
2017
|
+
var REFLECTION_SAMPLES = 4;
|
|
2018
|
+
var DIFFUSE_SAMPLES = 2;
|
|
1769
2019
|
var lobes = studio_environment_default.lobes.map((l) => ({
|
|
1770
2020
|
...l,
|
|
1771
2021
|
direction: normalize(l.direction),
|
|
@@ -1898,6 +2148,22 @@ var StudioLighting = class {
|
|
|
1898
2148
|
eye;
|
|
1899
2149
|
shadowDirections;
|
|
1900
2150
|
diffuseCache = /* @__PURE__ */ new WeakMap();
|
|
2151
|
+
reflectionSamples = /* @__PURE__ */ new Map();
|
|
2152
|
+
normalDiffuseCache = /* @__PURE__ */ new WeakMap();
|
|
2153
|
+
specularCache = /* @__PURE__ */ new Map();
|
|
2154
|
+
blendedSpecular(rough, level, blend) {
|
|
2155
|
+
const cached = this.specularCache.get(rough);
|
|
2156
|
+
if (cached) return cached;
|
|
2157
|
+
if (this.specularCache.size >= 16) return null;
|
|
2158
|
+
const tables = lobes.map((_, j) => {
|
|
2159
|
+
const lo = this.tables.specular[level][j], hi = this.tables.specular[level + 1][j], table = new Float64Array(W * H);
|
|
2160
|
+
for (let i = 0; i < table.length; i++)
|
|
2161
|
+
table[i] = lo[i] * (1 - blend) + hi[i] * blend;
|
|
2162
|
+
return table;
|
|
2163
|
+
});
|
|
2164
|
+
this.specularCache.set(rough, tables);
|
|
2165
|
+
return tables;
|
|
2166
|
+
}
|
|
1901
2167
|
constructor(drawCalls, camera) {
|
|
1902
2168
|
this.occlusion = new RayOcclusion(drawCalls);
|
|
1903
2169
|
const world = mat46.invert(mat46.create(), camera.view);
|
|
@@ -1906,8 +2172,8 @@ var StudioLighting = class {
|
|
|
1906
2172
|
this.eye = [world[12], world[13], world[14]];
|
|
1907
2173
|
this.shadowDirections = lobes.map((l) => {
|
|
1908
2174
|
const [t, b] = basis(l.direction);
|
|
1909
|
-
return Array.from({ length:
|
|
1910
|
-
const u = (i + 0.5) /
|
|
2175
|
+
return Array.from({ length: SHADOW_SAMPLES }, (_, i) => {
|
|
2176
|
+
const u = (i + 0.5) / SHADOW_SAMPLES, cos = 1 + Math.log(1 - u) / l.sharpness, sin = Math.sqrt(Math.max(0, 1 - cos * cos)), phi = 2 * Math.PI * radicalInverse(i);
|
|
1911
2177
|
const direction = orient(
|
|
1912
2178
|
sin * Math.cos(phi),
|
|
1913
2179
|
sin * Math.sin(phi),
|
|
@@ -1965,15 +2231,16 @@ var StudioLighting = class {
|
|
|
1965
2231
|
);
|
|
1966
2232
|
const pref = [...studio_environment_default.ambient];
|
|
1967
2233
|
const reflectedLookup = tableLookup(reflected);
|
|
2234
|
+
const specularTables = this.blendedSpecular(rough, level, blend);
|
|
1968
2235
|
for (let j = 0; j < lobes.length; j++) {
|
|
1969
|
-
const value = sampleTable(this.tables.specular[level][j], reflectedLookup) * (1 - blend) + sampleTable(this.tables.specular[level + 1][j], reflectedLookup) * blend;
|
|
2236
|
+
const value = specularTables ? sampleTable(specularTables[j], reflectedLookup) : sampleTable(this.tables.specular[level][j], reflectedLookup) * (1 - blend) + sampleTable(this.tables.specular[level + 1][j], reflectedLookup) * blend;
|
|
1970
2237
|
for (let k = 0; k < 3; k++) pref[k] += value * lobes[j].color[k];
|
|
1971
2238
|
}
|
|
1972
2239
|
for (let k = 0; k < 3; k++)
|
|
1973
2240
|
radiance[k] += pref[k] * ((0.04 * (1 - metal) + base[k] * metal) * A + B);
|
|
1974
2241
|
return radiance;
|
|
1975
2242
|
}
|
|
1976
|
-
shade(position, n, base, material,
|
|
2243
|
+
shade(position, n, base, material, pixelX = 0, pixelY = 0) {
|
|
1977
2244
|
const v = normalize([
|
|
1978
2245
|
this.eye[0] - position[0],
|
|
1979
2246
|
this.eye[1] - position[1],
|
|
@@ -2005,12 +2272,20 @@ var StudioLighting = class {
|
|
|
2005
2272
|
const specColor = f0.map(
|
|
2006
2273
|
(c, j) => studio_environment_default.ambient[j] * (c * A + B)
|
|
2007
2274
|
);
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
bits = Math.imul(bits ^ bits >>> 15, 2221713035);
|
|
2011
|
-
const hash = ((bits ^ bits >>> 16) >>> 0) / 4294967296;
|
|
2275
|
+
const gradient = (0.06711056 * pixelX + 583715e-8 * pixelY) % 1;
|
|
2276
|
+
const hash = 52.9829189 * gradient % 1;
|
|
2012
2277
|
const phase = hash * 2 * Math.PI, cosPhase = Math.cos(phase), sinPhase = Math.sin(phase);
|
|
2013
|
-
|
|
2278
|
+
let normalDiffuse = this.normalDiffuseCache.get(material);
|
|
2279
|
+
if (!normalDiffuse || normalDiffuse.normal.some((value, k) => !Object.is(value, n[k]))) {
|
|
2280
|
+
const lookup = tableLookup(n);
|
|
2281
|
+
normalDiffuse = {
|
|
2282
|
+
normal: [...n],
|
|
2283
|
+
values: this.tables.diffuse.map((table) => sampleTable(table, lookup))
|
|
2284
|
+
};
|
|
2285
|
+
this.normalDiffuseCache.set(material, normalDiffuse);
|
|
2286
|
+
}
|
|
2287
|
+
const reflectedLookup = tableLookup(reflected);
|
|
2288
|
+
const specularTables = this.blendedSpecular(rough, level, blend);
|
|
2014
2289
|
for (let j = 0; j < lobes.length; j++) {
|
|
2015
2290
|
const dirs = this.shadowDirections[j];
|
|
2016
2291
|
let visible = 0, total = 0;
|
|
@@ -2025,8 +2300,8 @@ var StudioLighting = class {
|
|
|
2025
2300
|
if (weight > 0 && !this.occlusion.occluded(origin, l)) visible += weight;
|
|
2026
2301
|
}
|
|
2027
2302
|
const visibility = total > 0 ? visible / total : 1;
|
|
2028
|
-
const diffuse =
|
|
2029
|
-
const specular = (sampleTable(this.tables.specular[level][j], reflectedLookup) * (1 - blend) + sampleTable(this.tables.specular[level + 1][j], reflectedLookup) * blend) * visibility;
|
|
2303
|
+
const diffuse = normalDiffuse.values[j] * visibility;
|
|
2304
|
+
const specular = (specularTables ? sampleTable(specularTables[j], reflectedLookup) : sampleTable(this.tables.specular[level][j], reflectedLookup) * (1 - blend) + sampleTable(this.tables.specular[level + 1][j], reflectedLookup) * blend) * visibility;
|
|
2030
2305
|
for (let k = 0; k < 3; k++) {
|
|
2031
2306
|
color[k] += lobes[j].color[k] * base[k] * (1 - metal) * 0.96 * diffuse;
|
|
2032
2307
|
specColor[k] += lobes[j].color[k] * (f0[k] * A + B) * specular;
|
|
@@ -2034,8 +2309,8 @@ var StudioLighting = class {
|
|
|
2034
2309
|
}
|
|
2035
2310
|
const [t, b] = basis(n);
|
|
2036
2311
|
let blocked = 0;
|
|
2037
|
-
for (let i = 0; metal < 1 && i <
|
|
2038
|
-
const phi = i * Math.PI /
|
|
2312
|
+
for (let i = 0; metal < 1 && i < DIFFUSE_SAMPLES; i++) {
|
|
2313
|
+
const phi = i * 2 * Math.PI / DIFFUSE_SAMPLES + phase, l = orient(
|
|
2039
2314
|
0.7 * Math.cos(phi),
|
|
2040
2315
|
0.7 * Math.sin(phi),
|
|
2041
2316
|
Math.sqrt(0.51),
|
|
@@ -2048,20 +2323,26 @@ var StudioLighting = class {
|
|
|
2048
2323
|
blocked++;
|
|
2049
2324
|
const bounce2 = this.bounceRadiance(hit, l);
|
|
2050
2325
|
for (let k = 0; k < 3; k++)
|
|
2051
|
-
color[k] += bounce2[k] * base[k] * (1 - metal) * 0.96 /
|
|
2326
|
+
color[k] += bounce2[k] * base[k] * (1 - metal) * 0.96 / DIFFUSE_SAMPLES;
|
|
2052
2327
|
}
|
|
2053
2328
|
}
|
|
2054
|
-
const ao = 1 - blocked /
|
|
2329
|
+
const ao = 1 - blocked / DIFFUSE_SAMPLES;
|
|
2055
2330
|
for (let k = 0; k < 3; k++)
|
|
2056
2331
|
color[k] -= studio_environment_default.ambient[k] * (1 - ao) * base[k] * (1 - metal) * 0.96;
|
|
2057
2332
|
const [rt, rb] = basis(reflected), bounce = [0, 0, 0];
|
|
2058
2333
|
let hitWeight = 0, totalWeight = 0;
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2334
|
+
let samples = this.reflectionSamples.get(rough);
|
|
2335
|
+
if (!samples) {
|
|
2336
|
+
samples = Array.from({ length: REFLECTION_SAMPLES }, (_, i) => {
|
|
2337
|
+
const u = (i + 0.5) / REFLECTION_SAMPLES, phi = 2 * Math.PI * radicalInverse(i), a = rough * rough, nh = Math.sqrt((1 - u) / (1 + (a * a - 1) * u)), sin = Math.sqrt(1 - nh * nh);
|
|
2338
|
+
return [sin * Math.cos(phi), sin * Math.sin(phi), nh];
|
|
2339
|
+
});
|
|
2340
|
+
this.reflectionSamples.set(rough, samples);
|
|
2341
|
+
}
|
|
2342
|
+
for (const [sx, sy, nh] of samples) {
|
|
2062
2343
|
const h = orient(
|
|
2063
|
-
|
|
2064
|
-
|
|
2344
|
+
sx * cosPhase - sy * sinPhase,
|
|
2345
|
+
sx * sinPhase + sy * cosPhase,
|
|
2065
2346
|
nh,
|
|
2066
2347
|
reflected,
|
|
2067
2348
|
rt,
|
|
@@ -2170,10 +2451,33 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
|
|
|
2170
2451
|
(dc) => (dc.material.alphaMode ?? "OPAQUE") === "BLEND"
|
|
2171
2452
|
);
|
|
2172
2453
|
const studio = options.realistic ? new StudioLighting(allDrawCalls, camera) : void 0;
|
|
2173
|
-
|
|
2174
|
-
|
|
2454
|
+
let winners;
|
|
2455
|
+
const triangleOffsets = [];
|
|
2456
|
+
if (studio && opaqueDraws.length > 1 && opaqueDraws.every((dc) => dc.mode !== 1)) {
|
|
2457
|
+
winners = new Int32Array(renderWidth * renderHeight).fill(-1);
|
|
2458
|
+
let triangleOffset = 0;
|
|
2459
|
+
for (const dc of opaqueDraws) {
|
|
2460
|
+
triangleOffsets.push(triangleOffset);
|
|
2461
|
+
triangleOffset += renderer.drawMesh(
|
|
2462
|
+
dc,
|
|
2463
|
+
camera,
|
|
2464
|
+
{ dir: options.lightDir, ambient: options.ambient },
|
|
2465
|
+
dc.material,
|
|
2466
|
+
options.cull,
|
|
2467
|
+
options.gamma,
|
|
2468
|
+
studio,
|
|
2469
|
+
{ winners, triangleOffset, depthOnly: true }
|
|
2470
|
+
);
|
|
2471
|
+
}
|
|
2472
|
+
renderer.depth.fill(Infinity);
|
|
2473
|
+
}
|
|
2474
|
+
const renderGroup = (dcs, opaque = false) => {
|
|
2475
|
+
for (let i = 0; i < dcs.length; i++) {
|
|
2476
|
+
const dc = dcs[i];
|
|
2175
2477
|
if (dc.mode === 1) {
|
|
2176
|
-
renderer.drawLines(dc, camera, options.gamma
|
|
2478
|
+
renderer.drawLines(dc, camera, options.gamma, {
|
|
2479
|
+
width: options.lineWidth * options.supersampling
|
|
2480
|
+
});
|
|
2177
2481
|
} else {
|
|
2178
2482
|
renderer.drawMesh(
|
|
2179
2483
|
dc,
|
|
@@ -2182,12 +2486,13 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
|
|
|
2182
2486
|
dc.material,
|
|
2183
2487
|
options.cull,
|
|
2184
2488
|
options.gamma,
|
|
2185
|
-
studio
|
|
2489
|
+
studio,
|
|
2490
|
+
opaque && winners ? { winners, triangleOffset: triangleOffsets[i], depthOnly: false } : void 0
|
|
2186
2491
|
);
|
|
2187
2492
|
}
|
|
2188
2493
|
}
|
|
2189
2494
|
};
|
|
2190
|
-
renderGroup(opaqueDraws);
|
|
2495
|
+
renderGroup(opaqueDraws, true);
|
|
2191
2496
|
renderGroup(maskDraws);
|
|
2192
2497
|
if (infiniteGridOptions) {
|
|
2193
2498
|
const aabb = computeWorldAABB(drawCalls);
|
|
@@ -2208,21 +2513,25 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
|
|
|
2208
2513
|
renderGroup(blendDraws);
|
|
2209
2514
|
renderer.denoise(options.gamma);
|
|
2210
2515
|
for (const dc of drawCalls) {
|
|
2211
|
-
|
|
2516
|
+
const showHiddenEdges = options.showHiddenEdges ?? dc.showHiddenEdges;
|
|
2517
|
+
if (!options.showEdges && !showHiddenEdges) continue;
|
|
2212
2518
|
const edgeDrawCall = createEdgeDrawCall(dc);
|
|
2213
2519
|
if (!edgeDrawCall) continue;
|
|
2214
2520
|
renderer.drawLines(edgeDrawCall, camera, options.gamma, {
|
|
2215
2521
|
depthCompare: "less-equal",
|
|
2216
2522
|
depthWrite: false,
|
|
2217
|
-
depthBias: 1e-4
|
|
2523
|
+
depthBias: 1e-4,
|
|
2524
|
+
width: options.edgeWidth * options.supersampling
|
|
2218
2525
|
});
|
|
2526
|
+
if (!showHiddenEdges) continue;
|
|
2219
2527
|
renderer.drawLines(edgeDrawCall, camera, options.gamma, {
|
|
2220
2528
|
depthCompare: "greater",
|
|
2221
2529
|
depthWrite: false,
|
|
2222
2530
|
depthBias: 1e-4,
|
|
2223
2531
|
dashed: true,
|
|
2224
2532
|
dashLength: 4 * options.supersampling,
|
|
2225
|
-
opacity:
|
|
2533
|
+
opacity: options.hiddenEdgeOpacity,
|
|
2534
|
+
width: options.edgeWidth * options.supersampling
|
|
2226
2535
|
});
|
|
2227
2536
|
}
|
|
2228
2537
|
if (options.debugPoints?.length) {
|