poppygl 0.0.34 → 0.0.36
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 +81 -0
- package/dist/index.d.ts +31 -1
- package/dist/index.js +429 -39
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -133,10 +133,44 @@ const png = await renderGLTFToPNGFromGLB(glb, {
|
|
|
133
133
|
- `cull`: enable back-face culling (default `true`).
|
|
134
134
|
- `backgroundColor`: a hex string such as `"#e5e7eb"`, or an RGB tuple with values from 0 to 1.
|
|
135
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`, finite grid lines, and explosion guides in output pixels (default `1`).
|
|
140
|
+
- `hiddenEdgeOpacity`: opacity of dashed occluded mesh edges, from `0` to `1` (default `0.35`).
|
|
136
141
|
- `fetchImpl`: optional override for resource loading (must match the `fetch` signature).
|
|
137
142
|
|
|
138
143
|
You can inspect the defaults via `getDefaultRenderOptions()` or reuse the internal merge logic with `resolveRenderOptions()`.
|
|
139
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, finite grid lines,
|
|
171
|
+
and explosion guides, including arrowhead strokes. The procedural infinite grid
|
|
172
|
+
keeps its existing independent styling.
|
|
173
|
+
|
|
140
174
|
### Hidden edges for selected sub-models
|
|
141
175
|
|
|
142
176
|
Set `showHiddenEdges` in a glTF node's `extras` to render that node with solid
|
|
@@ -205,6 +239,53 @@ const png = await encodePNG(bitmap)
|
|
|
205
239
|
|
|
206
240
|
The only contract is that `buffers` is an array of `Uint8Array` instances and `images` is an array of `BitmapLike` textures (PNG and JPEG are supported out of the box via `decodeImageFromBuffer`).
|
|
207
241
|
|
|
242
|
+
## Explosion guide lines
|
|
243
|
+
|
|
244
|
+
Pass `explosionLines` to any render entrypoint to draw polylines between assembled
|
|
245
|
+
and displaced part positions. The caller moves the geometry and supplies the
|
|
246
|
+
points in the same glTF scene frame and units (normally right-handed, +Y up).
|
|
247
|
+
|
|
248
|
+
This lamp is adapted from [core's reference-surface lamp](https://github.com/tscircuit/core/blob/81bef4337570e3ac4d7dd6d30ab63481304e8147/tests/assembly/fixtures/reference-surface-lamp.tsx).
|
|
249
|
+
Its base, hollow blue stem, and bulb stay in place while the hollow shade,
|
|
250
|
+
mounting collar, and three spokes move together by **+60 mm on scene Y**. The
|
|
251
|
+
shade mount moves from Y=92 to Y=152. Two dashed guides join corresponding points
|
|
252
|
+
on its lower rim, from Y=68 to Y=128. Arrowheads at the displaced endpoints show
|
|
253
|
+
the upward movement:
|
|
254
|
+
|
|
255
|
+

|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
// explodedLampGlb already contains the shade translated +60 mm on scene Y.
|
|
259
|
+
const rimPoints = [[-36 / Math.SQRT2, 36 / Math.SQRT2], [36, 0]] as const
|
|
260
|
+
const png = await renderGLTFToPNGFromGLB(explodedLampGlb, {
|
|
261
|
+
explosionLines: rimPoints.map(([x, z]) => ({
|
|
262
|
+
points: [[x, 68, z], [x, 128, z]],
|
|
263
|
+
color: [0.015, 0.04, 0.08], // Linear RGB, 0..1; optional.
|
|
264
|
+
dashed: true, // Default; false gives a solid guide.
|
|
265
|
+
arrow: true, // Optional; false by default.
|
|
266
|
+
})),
|
|
267
|
+
})
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
The deterministic [lamp fixture](tests/fixtures/reference-lamp.ts) and
|
|
271
|
+
[visual test](tests/render/explosion-lamp.test.ts) generate the comparison without
|
|
272
|
+
network assets. Open the `explosion-lamp` Cosmos example to toggle shade movement,
|
|
273
|
+
guides, and arrows interactively. The separate synthetic explosion-lines snapshot
|
|
274
|
+
exercises elbows, geometry occlusion, and camera clipping.
|
|
275
|
+
|
|
276
|
+
Guides respect scene depth, do not write to the depth buffer, and render after
|
|
277
|
+
geometry denoising. Their points contribute to automatic camera framing. Lines
|
|
278
|
+
are clipped to the camera frustum before rasterization; dash length scales with
|
|
279
|
+
supersampling. Omitting the option or passing an empty list preserves the existing
|
|
280
|
+
rendering behavior. Each guide needs at least two finite XYZ points.
|
|
281
|
+
|
|
282
|
+
Set `arrow: true` to draw a solid open arrowhead at the final authored point,
|
|
283
|
+
following the final nonzero-length segment's direction. Arrowheads are at most
|
|
284
|
+
10 output pixels long, capped for short segments, and scale with supersampling.
|
|
285
|
+
They respect scene depth and remain solid when the shaft is dashed. `lineWidth`
|
|
286
|
+
controls both the shaft and arrowhead stroke width. Omit `arrow` or set it to
|
|
287
|
+
`false` for a guide without an arrow.
|
|
288
|
+
|
|
208
289
|
## Additional utilities
|
|
209
290
|
|
|
210
291
|
- `loadGLTFWithResourcesFromURL` returns `{ gltf, resources }` if you prefer to inspect or cache the parsed data before rendering.
|
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];
|
|
@@ -67,6 +69,21 @@ interface DebugPoint {
|
|
|
67
69
|
z: number;
|
|
68
70
|
};
|
|
69
71
|
}
|
|
72
|
+
/** Guide polyline in right-handed glTF scene coordinates (normally +Y up),
|
|
73
|
+
* using the model's scene units. Each vertex is a point, not a displacement.
|
|
74
|
+
*/
|
|
75
|
+
interface ExplosionLine {
|
|
76
|
+
points: readonly (readonly [number, number, number])[];
|
|
77
|
+
/** Linear RGB, 0..1; defaults to a muted blue. */
|
|
78
|
+
color?: readonly [number, number, number];
|
|
79
|
+
/** Defaults to true. Visible segments respect scene depth. */
|
|
80
|
+
dashed?: boolean;
|
|
81
|
+
/** Add a solid arrowhead at the last point, following the final segment.
|
|
82
|
+
* Defaults to false. The arrowhead is 10 output pixels long (or shorter for
|
|
83
|
+
* short segments), independent of camera distance and supersampling.
|
|
84
|
+
*/
|
|
85
|
+
arrow?: boolean;
|
|
86
|
+
}
|
|
70
87
|
type CameraUp = "y+" | "y-" | "x+" | "x-" | "z+" | "z-";
|
|
71
88
|
interface CameraRotation {
|
|
72
89
|
x: number;
|
|
@@ -100,6 +117,17 @@ interface RenderOptions {
|
|
|
100
117
|
debugFontSize?: number | null;
|
|
101
118
|
debugPointColor?: readonly [number, number, number] | null;
|
|
102
119
|
debugLabelColor?: readonly [number, number, number] | null;
|
|
120
|
+
explosionLines?: readonly ExplosionLine[] | null;
|
|
121
|
+
/** Draw visible boundary and crease edges on every triangle mesh. */
|
|
122
|
+
showEdges: boolean;
|
|
123
|
+
/** Override model showHiddenEdges flags; null preserves each model's flag. */
|
|
124
|
+
showHiddenEdges: boolean | null;
|
|
125
|
+
/** Width of visible and hidden mesh edges in output pixels. */
|
|
126
|
+
edgeWidth: number;
|
|
127
|
+
/** Width of glTF LINES, finite grid lines, and explosion guides in output pixels. */
|
|
128
|
+
lineWidth: number;
|
|
129
|
+
/** Opacity of dashed, occluded mesh edges, from 0 to 1. */
|
|
130
|
+
hiddenEdgeOpacity: number;
|
|
103
131
|
}
|
|
104
132
|
type RenderOptionsInput = Partial<RenderOptions>;
|
|
105
133
|
declare const DEFAULT_RENDER_OPTIONS: RenderOptions;
|
|
@@ -195,6 +223,8 @@ interface LightSettings {
|
|
|
195
223
|
ambient: number;
|
|
196
224
|
}
|
|
197
225
|
interface LineRenderOptions {
|
|
226
|
+
/** Stroke width in this renderer's raster pixels. Defaults to one. */
|
|
227
|
+
width?: number;
|
|
198
228
|
depthCompare?: "less" | "less-equal" | "greater";
|
|
199
229
|
depthWrite?: boolean;
|
|
200
230
|
depthBias?: number;
|
|
@@ -249,4 +279,4 @@ declare function renderGLTFToPNGFromGLB(glb: ArrayBuffer | Uint8Array, options?:
|
|
|
249
279
|
|
|
250
280
|
declare function renderSceneFromGLTF(scene: GLTFScene, options?: RenderOptionsInput, imageFactory?: ImageFactory): RenderResult;
|
|
251
281
|
|
|
252
|
-
export { type BitmapLike, type Camera, type CameraRotation, type CameraUp, DEFAULT_LIGHT_DIR, DEFAULT_RENDER_OPTIONS, type DebugPoint, type DrawCall, type FetchLike, type GLTFResources, type GLTFScene, type GridOptions, type ImageFactory, type LightSettings, type LineRenderOptions, type LoadGLTFWithResourcesFromURLOptions, type Material, type MutableRGBA, type RGBA, type RenderGLTFToPNGFromGLBOptions, type RenderGLTFToPNGFromURLOptions, type RenderOptions, type RenderOptionsInput, type RenderResult, SoftwareRenderer, bufferFromDataURI, buildCamera, computeSmoothNormals, computeWorldAABB, createEdgeDrawCall, createGrid, createInfiniteGrid, createSceneFromGLTF, createUint8Bitmap, decodeImageFromBuffer, drawInfiniteGrid, encodePNG, getDefaultRenderOptions, hexToRgb, isJPG, isPNG, loadGLTFWithResourcesFromURL, renderDrawCalls, renderGLTFToPNGFromGLB, renderGLTFToPNGFromURL, renderSceneFromGLTF, resolveRenderOptions };
|
|
282
|
+
export { type BitmapLike, type Camera, type CameraRotation, type CameraUp, DEFAULT_LIGHT_DIR, DEFAULT_RENDER_OPTIONS, type DebugPoint, type DrawCall, type ExplosionLine, type FetchLike, type GLTFResources, type GLTFScene, type GridOptions, type ImageFactory, type LightSettings, type LineRenderOptions, type LoadGLTFWithResourcesFromURLOptions, type Material, type MutableRGBA, type RGBA, type RenderGLTFToPNGFromGLBOptions, type RenderGLTFToPNGFromURLOptions, type RenderOptions, type RenderOptionsInput, type RenderResult, SoftwareRenderer, bufferFromDataURI, buildCamera, computeSmoothNormals, computeWorldAABB, createEdgeDrawCall, createGrid, createInfiniteGrid, createSceneFromGLTF, createUint8Bitmap, decodeImageFromBuffer, drawInfiniteGrid, encodePNG, getDefaultRenderOptions, hexToRgb, isJPG, isPNG, loadGLTFWithResourcesFromURL, renderDrawCalls, renderGLTFToPNGFromGLB, renderGLTFToPNGFromURL, renderSceneFromGLTF, resolveRenderOptions };
|
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,13 @@ var DEFAULT_RENDER_OPTIONS = {
|
|
|
490
498
|
debugPoints: null,
|
|
491
499
|
debugFontSize: null,
|
|
492
500
|
debugPointColor: null,
|
|
493
|
-
debugLabelColor: null
|
|
501
|
+
debugLabelColor: null,
|
|
502
|
+
explosionLines: null,
|
|
503
|
+
showEdges: false,
|
|
504
|
+
showHiddenEdges: null,
|
|
505
|
+
edgeWidth: 1,
|
|
506
|
+
lineWidth: 1,
|
|
507
|
+
hiddenEdgeOpacity: 0.35
|
|
494
508
|
};
|
|
495
509
|
function getDefaultRenderOptions() {
|
|
496
510
|
return { ...DEFAULT_RENDER_OPTIONS };
|
|
@@ -679,6 +693,98 @@ var GeometryDenoiser = class {
|
|
|
679
693
|
}
|
|
680
694
|
};
|
|
681
695
|
|
|
696
|
+
// lib/render/rasterizeWideLine.ts
|
|
697
|
+
function clipLine(start, end, marginX, marginY) {
|
|
698
|
+
if (![...start, ...end].every(Number.isFinite)) return null;
|
|
699
|
+
const distances = (p) => [
|
|
700
|
+
p[3] * (1 + marginX) + p[0],
|
|
701
|
+
p[3] * (1 + marginX) - p[0],
|
|
702
|
+
p[3] * (1 + marginY) + p[1],
|
|
703
|
+
p[3] * (1 + marginY) - p[1],
|
|
704
|
+
p[3] + p[2],
|
|
705
|
+
p[3] - p[2]
|
|
706
|
+
];
|
|
707
|
+
const a = distances(start);
|
|
708
|
+
const b = distances(end);
|
|
709
|
+
let from = 0;
|
|
710
|
+
let to = 1;
|
|
711
|
+
for (let plane = 0; plane < 6; plane++) {
|
|
712
|
+
if (a[plane] < 0 && b[plane] < 0) return null;
|
|
713
|
+
if (a[plane] < 0 !== b[plane] < 0) {
|
|
714
|
+
const t = a[plane] / (a[plane] - b[plane]);
|
|
715
|
+
if (a[plane] < 0) from = Math.max(from, t);
|
|
716
|
+
else to = Math.min(to, t);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
if (from > to) return null;
|
|
720
|
+
return [from, to].map(
|
|
721
|
+
(t) => Array.from(start, (value, axis) => value + t * (end[axis] - value))
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
function rasterizeWideLine(start, end, width, height, strokeWidth, dashed, dashLength, fragment) {
|
|
725
|
+
const radius = strokeWidth / 2;
|
|
726
|
+
const padding = radius + 0.5;
|
|
727
|
+
const clipped = clipLine(
|
|
728
|
+
start,
|
|
729
|
+
end,
|
|
730
|
+
2 * padding / Math.max(1, width - 1),
|
|
731
|
+
2 * padding / Math.max(1, height - 1)
|
|
732
|
+
);
|
|
733
|
+
if (!clipped) return;
|
|
734
|
+
const project = (p) => [
|
|
735
|
+
(p[0] / p[3] * 0.5 + 0.5) * (width - 1),
|
|
736
|
+
(0.5 - p[1] / p[3] * 0.5) * (height - 1),
|
|
737
|
+
// Clipping guarantees this range; absorb near/far-plane roundoff so an
|
|
738
|
+
// endpoint's round cap is not discarded at z = -epsilon.
|
|
739
|
+
Math.max(0, Math.min(1, p[2] / p[3] * 0.5 + 0.5))
|
|
740
|
+
];
|
|
741
|
+
const a = project(clipped[0]);
|
|
742
|
+
const b = project(clipped[1]);
|
|
743
|
+
if (![...a, ...b].every(Number.isFinite)) return;
|
|
744
|
+
const [x0, y0, z0] = a;
|
|
745
|
+
const [x1, y1, z1] = b;
|
|
746
|
+
const dx = x1 - x0;
|
|
747
|
+
const dy = y1 - y0;
|
|
748
|
+
const lengthSquared = dx * dx + dy * dy;
|
|
749
|
+
const length = Math.sqrt(lengthSquared);
|
|
750
|
+
const original = start[3] > 0 ? project(start) : a;
|
|
751
|
+
const phase = Math.hypot(x0 - original[0], y0 - original[1]);
|
|
752
|
+
const drawPixel = (x, y) => {
|
|
753
|
+
const t = lengthSquared === 0 ? 0 : Math.max(
|
|
754
|
+
0,
|
|
755
|
+
Math.min(1, ((x - x0) * dx + (y - y0) * dy) / lengthSquared)
|
|
756
|
+
);
|
|
757
|
+
if (dashed && Math.floor((phase + t * length) / dashLength) % 2 === 1)
|
|
758
|
+
return;
|
|
759
|
+
const distance = Math.hypot(x - (x0 + t * dx), y - (y0 + t * dy));
|
|
760
|
+
const coverage = Math.min(1, padding - distance);
|
|
761
|
+
if (coverage > 0) fragment(x, y, z0 + t * (z1 - z0), coverage);
|
|
762
|
+
};
|
|
763
|
+
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
764
|
+
const band = length === 0 ? padding : padding * length / Math.abs(dx);
|
|
765
|
+
const first = Math.max(0, Math.ceil(Math.min(x0, x1) - padding));
|
|
766
|
+
const last = Math.min(width - 1, Math.floor(Math.max(x0, x1) + padding));
|
|
767
|
+
for (let x = first; x <= last; x++) {
|
|
768
|
+
const t = dx === 0 ? 0 : Math.max(0, Math.min(1, (x - x0) / dx));
|
|
769
|
+
const centerY = y0 + t * dy;
|
|
770
|
+
const top = Math.max(0, Math.ceil(centerY - band));
|
|
771
|
+
const bottom = Math.min(height - 1, Math.floor(centerY + band));
|
|
772
|
+
for (let y = top; y <= bottom; y++) drawPixel(x, y);
|
|
773
|
+
}
|
|
774
|
+
} else {
|
|
775
|
+
const band = padding * length / Math.abs(dy);
|
|
776
|
+
const first = Math.max(0, Math.ceil(Math.min(y0, y1) - padding));
|
|
777
|
+
const last = Math.min(height - 1, Math.floor(Math.max(y0, y1) + padding));
|
|
778
|
+
for (let y = first; y <= last; y++) {
|
|
779
|
+
const t = Math.max(0, Math.min(1, (y - y0) / dy));
|
|
780
|
+
const centerX = x0 + t * dx;
|
|
781
|
+
const left = Math.max(0, Math.ceil(centerX - band));
|
|
782
|
+
const right = Math.min(width - 1, Math.floor(centerX + band));
|
|
783
|
+
for (let x = left; x <= right; x++) drawPixel(x, y);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
682
788
|
// lib/render/SoftwareRenderer.ts
|
|
683
789
|
var SoftwareRenderer = class {
|
|
684
790
|
width;
|
|
@@ -731,6 +837,7 @@ var SoftwareRenderer = class {
|
|
|
731
837
|
const vScreen = new Array(vertexCount);
|
|
732
838
|
const vInvW = new Float32Array(vertexCount);
|
|
733
839
|
const vNDCz = new Float32Array(vertexCount);
|
|
840
|
+
const clipVertices = [];
|
|
734
841
|
for (let i = 0; i < vertexCount; i++) {
|
|
735
842
|
const p = vec42.fromValues(
|
|
736
843
|
positions[i * 3 + 0],
|
|
@@ -740,6 +847,7 @@ var SoftwareRenderer = class {
|
|
|
740
847
|
);
|
|
741
848
|
const c = vec42.create();
|
|
742
849
|
vec42.transformMat4(c, p, mvp);
|
|
850
|
+
clipVertices.push(c);
|
|
743
851
|
const invW = 1 / c[3];
|
|
744
852
|
vInvW[i] = invW;
|
|
745
853
|
if (!isFinite(invW)) {
|
|
@@ -774,9 +882,76 @@ var SoftwareRenderer = class {
|
|
|
774
882
|
const depthWrite = options.depthWrite ?? true;
|
|
775
883
|
const depthBias = options.depthBias ?? 0;
|
|
776
884
|
const dashLength = Math.max(1, options.dashLength ?? 4);
|
|
885
|
+
const strokeWidth = typeof options.width === "number" && Number.isFinite(options.width) && options.width > 0 ? options.width : 1;
|
|
886
|
+
const drawFragment = (xi, yi, z, coverage = 1) => {
|
|
887
|
+
if (z < 0 || z > 1) return;
|
|
888
|
+
const di = yi * this.width + xi;
|
|
889
|
+
const currentDepth = this.depth[di];
|
|
890
|
+
const passesDepth = depthCompare === "greater" ? z > currentDepth + depthBias : depthCompare === "less-equal" ? z <= currentDepth + depthBias : z < currentDepth - depthBias;
|
|
891
|
+
if (!passesDepth) return;
|
|
892
|
+
if (depthWrite) this.depth[di] = z;
|
|
893
|
+
const pixelIndex = di * 4;
|
|
894
|
+
const destinationAlpha = this.buffer[pixelIndex + 3] / 255;
|
|
895
|
+
const alpha = sourceAlpha * coverage;
|
|
896
|
+
const oneMinusSourceAlpha = 1 - alpha;
|
|
897
|
+
const outAlpha = alpha + destinationAlpha * oneMinusSourceAlpha;
|
|
898
|
+
const blend = (channel, offset) => outAlpha === 0 ? 0 : (channel * alpha + this.buffer[pixelIndex + offset] * destinationAlpha * oneMinusSourceAlpha) / outAlpha;
|
|
899
|
+
this.setPixel(
|
|
900
|
+
xi,
|
|
901
|
+
yi,
|
|
902
|
+
blend(r255, 0),
|
|
903
|
+
blend(g255, 1),
|
|
904
|
+
blend(b255, 2),
|
|
905
|
+
outAlpha * 255
|
|
906
|
+
);
|
|
907
|
+
};
|
|
777
908
|
for (let i = 0; i < indices.length; i += 2) {
|
|
778
909
|
const i0 = indices[i + 0];
|
|
779
910
|
const i1 = indices[i + 1];
|
|
911
|
+
if (strokeWidth !== 1) {
|
|
912
|
+
const sourceFaces = (mesh.edgeAdjacentVertices?.[i / 2] ?? []).flatMap(
|
|
913
|
+
(third) => {
|
|
914
|
+
if (clipVertices[i0][3] <= 0 || clipVertices[i1][3] <= 0 || clipVertices[third][3] <= 0)
|
|
915
|
+
return [];
|
|
916
|
+
const a2 = vScreen[i0];
|
|
917
|
+
const b2 = vScreen[i1];
|
|
918
|
+
const c = vScreen[third];
|
|
919
|
+
const area = edge(a2, b2, c);
|
|
920
|
+
if (!Number.isFinite(area) || area === 0) return [];
|
|
921
|
+
return [{ a: a2, b: b2, c, inverseArea: 1 / area, third }];
|
|
922
|
+
}
|
|
923
|
+
);
|
|
924
|
+
rasterizeWideLine(
|
|
925
|
+
clipVertices[i0],
|
|
926
|
+
clipVertices[i1],
|
|
927
|
+
this.width,
|
|
928
|
+
this.height,
|
|
929
|
+
strokeWidth,
|
|
930
|
+
options.dashed ?? false,
|
|
931
|
+
dashLength,
|
|
932
|
+
sourceFaces.length === 0 ? drawFragment : (x2, y2, z2, coverage) => {
|
|
933
|
+
let surfaceDepth = Infinity;
|
|
934
|
+
const sample = [x2 + 0.5, y2 + 0.5];
|
|
935
|
+
for (const face of sourceFaces) {
|
|
936
|
+
const l0 = edge(face.b, face.c, sample) * face.inverseArea;
|
|
937
|
+
const l1 = edge(face.c, face.a, sample) * face.inverseArea;
|
|
938
|
+
const l2 = 1 - l0 - l1;
|
|
939
|
+
if (l0 < -1e-6 || l1 < -1e-6 || l2 < -1e-6) continue;
|
|
940
|
+
const depth = (l0 * vNDCz[i0] + l1 * vNDCz[i1] + l2 * vNDCz[face.third]) * 0.5 + 0.5;
|
|
941
|
+
if (depth >= 0 && depth <= 1) {
|
|
942
|
+
surfaceDepth = Math.min(surfaceDepth, depth);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
drawFragment(
|
|
946
|
+
x2,
|
|
947
|
+
y2,
|
|
948
|
+
surfaceDepth === Infinity ? z2 : surfaceDepth,
|
|
949
|
+
coverage
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
);
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
780
955
|
const v0s = vScreen[i0];
|
|
781
956
|
const v1s = vScreen[i1];
|
|
782
957
|
const z0_ndc = vNDCz[i0];
|
|
@@ -809,20 +984,7 @@ var SoftwareRenderer = class {
|
|
|
809
984
|
const xi = Math.round(x);
|
|
810
985
|
const yi = Math.round(y);
|
|
811
986
|
if (xi >= 0 && xi < this.width && yi >= 0 && yi < this.height && z >= 0 && z <= 1) {
|
|
812
|
-
|
|
813
|
-
const currentDepth = this.depth[di];
|
|
814
|
-
const passesDepth = depthCompare === "greater" ? z > currentDepth + depthBias : depthCompare === "less-equal" ? z <= currentDepth + depthBias : z < currentDepth - depthBias;
|
|
815
|
-
if (passesDepth) {
|
|
816
|
-
if (depthWrite) this.depth[di] = z;
|
|
817
|
-
const pixelIndex = di * 4;
|
|
818
|
-
const destinationAlpha = this.buffer[pixelIndex + 3] / 255;
|
|
819
|
-
const oneMinusSourceAlpha = 1 - sourceAlpha;
|
|
820
|
-
const outAlpha = sourceAlpha + destinationAlpha * oneMinusSourceAlpha;
|
|
821
|
-
const outR = outAlpha === 0 ? 0 : (r255 * sourceAlpha + this.buffer[pixelIndex + 0] * destinationAlpha * oneMinusSourceAlpha) / outAlpha;
|
|
822
|
-
const outG = outAlpha === 0 ? 0 : (g255 * sourceAlpha + this.buffer[pixelIndex + 1] * destinationAlpha * oneMinusSourceAlpha) / outAlpha;
|
|
823
|
-
const outB = outAlpha === 0 ? 0 : (b255 * sourceAlpha + this.buffer[pixelIndex + 2] * destinationAlpha * oneMinusSourceAlpha) / outAlpha;
|
|
824
|
-
this.setPixel(xi, yi, outR, outG, outB, outAlpha * 255);
|
|
825
|
-
}
|
|
987
|
+
drawFragment(xi, yi, z);
|
|
826
988
|
}
|
|
827
989
|
x += xinc;
|
|
828
990
|
y += yinc;
|
|
@@ -1495,9 +1657,17 @@ function resolveRenderOptions(options = {}) {
|
|
|
1495
1657
|
camPos: options.camPos !== void 0 ? options.camPos : DEFAULT_RENDER_OPTIONS.camPos,
|
|
1496
1658
|
lookAt: options.lookAt !== void 0 ? options.lookAt : DEFAULT_RENDER_OPTIONS.lookAt,
|
|
1497
1659
|
up: resolveCameraUp(options.up),
|
|
1498
|
-
cameraRotation: resolveCameraRotation(options.cameraRotation)
|
|
1660
|
+
cameraRotation: resolveCameraRotation(options.cameraRotation),
|
|
1661
|
+
showEdges: options.showEdges === true,
|
|
1662
|
+
showHiddenEdges: typeof options.showHiddenEdges === "boolean" ? options.showHiddenEdges : null,
|
|
1663
|
+
edgeWidth: resolveWidth(options.edgeWidth),
|
|
1664
|
+
lineWidth: resolveWidth(options.lineWidth),
|
|
1665
|
+
hiddenEdgeOpacity: typeof options.hiddenEdgeOpacity === "number" && Number.isFinite(options.hiddenEdgeOpacity) ? Math.max(0, Math.min(1, options.hiddenEdgeOpacity)) : DEFAULT_RENDER_OPTIONS.hiddenEdgeOpacity
|
|
1499
1666
|
};
|
|
1500
1667
|
}
|
|
1668
|
+
function resolveWidth(width) {
|
|
1669
|
+
return typeof width === "number" && Number.isFinite(width) && width > 0 ? width : 1;
|
|
1670
|
+
}
|
|
1501
1671
|
|
|
1502
1672
|
// lib/render/StudioLighting.ts
|
|
1503
1673
|
import { mat4 as mat46 } from "gl-matrix";
|
|
@@ -2200,13 +2370,190 @@ var StudioLighting = class {
|
|
|
2200
2370
|
}
|
|
2201
2371
|
};
|
|
2202
2372
|
|
|
2373
|
+
// lib/render/createExplosionLineDrawCall.ts
|
|
2374
|
+
import { mat4 as mat47, vec4 as vec45 } from "gl-matrix";
|
|
2375
|
+
function createExplosionLineDrawCall(line) {
|
|
2376
|
+
if (line.points.length < 2 || line.points.some(
|
|
2377
|
+
(point) => point.length !== 3 || !point.every(Number.isFinite)
|
|
2378
|
+
)) {
|
|
2379
|
+
throw new Error("Explosion lines need at least two finite XYZ points");
|
|
2380
|
+
}
|
|
2381
|
+
const color = line.color ?? [0.08, 0.2, 0.35];
|
|
2382
|
+
if (color.length !== 3 || color.some(
|
|
2383
|
+
(channel) => !Number.isFinite(channel) || channel < 0 || channel > 1
|
|
2384
|
+
)) {
|
|
2385
|
+
throw new Error(
|
|
2386
|
+
"Explosion line colors must be linear RGB values from 0 to 1"
|
|
2387
|
+
);
|
|
2388
|
+
}
|
|
2389
|
+
const indices = [];
|
|
2390
|
+
for (let vertex = 1; vertex < line.points.length; vertex++) {
|
|
2391
|
+
indices.push(vertex - 1, vertex);
|
|
2392
|
+
}
|
|
2393
|
+
return {
|
|
2394
|
+
positions: new Float32Array(line.points.flatMap((point) => [...point])),
|
|
2395
|
+
indices: new Uint32Array(indices),
|
|
2396
|
+
normals: null,
|
|
2397
|
+
uvs: null,
|
|
2398
|
+
model: mat47.create(),
|
|
2399
|
+
material: { baseColorFactor: [...color, 1], baseColorTexture: null },
|
|
2400
|
+
mode: 1
|
|
2401
|
+
};
|
|
2402
|
+
}
|
|
2403
|
+
function clipExplosionLineDrawCall(drawCall, camera) {
|
|
2404
|
+
const viewProjection = mat47.multiply(mat47.create(), camera.proj, camera.view);
|
|
2405
|
+
const positions = [];
|
|
2406
|
+
const indices = [];
|
|
2407
|
+
const distances = (point) => [
|
|
2408
|
+
point[3] + point[0],
|
|
2409
|
+
point[3] - point[0],
|
|
2410
|
+
point[3] + point[1],
|
|
2411
|
+
point[3] - point[1],
|
|
2412
|
+
point[3] + point[2],
|
|
2413
|
+
point[3] - point[2]
|
|
2414
|
+
];
|
|
2415
|
+
for (let segment = 0; segment < drawCall.indices.length; segment += 2) {
|
|
2416
|
+
const startIndex = drawCall.indices[segment] * 3;
|
|
2417
|
+
const endIndex = drawCall.indices[segment + 1] * 3;
|
|
2418
|
+
const start = vec45.fromValues(
|
|
2419
|
+
drawCall.positions[startIndex],
|
|
2420
|
+
drawCall.positions[startIndex + 1],
|
|
2421
|
+
drawCall.positions[startIndex + 2],
|
|
2422
|
+
1
|
|
2423
|
+
);
|
|
2424
|
+
const end = vec45.fromValues(
|
|
2425
|
+
drawCall.positions[endIndex],
|
|
2426
|
+
drawCall.positions[endIndex + 1],
|
|
2427
|
+
drawCall.positions[endIndex + 2],
|
|
2428
|
+
1
|
|
2429
|
+
);
|
|
2430
|
+
const startDistances = distances(
|
|
2431
|
+
vec45.transformMat4(vec45.create(), start, viewProjection)
|
|
2432
|
+
);
|
|
2433
|
+
const endDistances = distances(
|
|
2434
|
+
vec45.transformMat4(vec45.create(), end, viewProjection)
|
|
2435
|
+
);
|
|
2436
|
+
let from = 0;
|
|
2437
|
+
let to = 1;
|
|
2438
|
+
for (let plane = 0; plane < 6; plane++) {
|
|
2439
|
+
const a = startDistances[plane];
|
|
2440
|
+
const b = endDistances[plane];
|
|
2441
|
+
if (a < 0 && b < 0) {
|
|
2442
|
+
to = -1;
|
|
2443
|
+
break;
|
|
2444
|
+
}
|
|
2445
|
+
if (a < 0 !== b < 0) {
|
|
2446
|
+
const intersection = a / (a - b);
|
|
2447
|
+
if (a < 0) from = Math.max(from, intersection);
|
|
2448
|
+
else to = Math.min(to, intersection);
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
if (from > to) continue;
|
|
2452
|
+
const vertex = positions.length / 3;
|
|
2453
|
+
for (const progress of [from, to]) {
|
|
2454
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
2455
|
+
positions.push(start[axis] + progress * (end[axis] - start[axis]));
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
indices.push(vertex, vertex + 1);
|
|
2459
|
+
}
|
|
2460
|
+
return {
|
|
2461
|
+
...drawCall,
|
|
2462
|
+
positions: new Float32Array(positions),
|
|
2463
|
+
indices: new Uint32Array(indices)
|
|
2464
|
+
};
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
// lib/render/createExplosionArrowDrawCall.ts
|
|
2468
|
+
import { mat4 as mat48, vec4 as vec46 } from "gl-matrix";
|
|
2469
|
+
function createExplosionArrowDrawCall(drawCall, camera, width, height, supersampling) {
|
|
2470
|
+
if (width <= 1 || height <= 1) return null;
|
|
2471
|
+
const points = drawCall.positions;
|
|
2472
|
+
const end = points.length - 3;
|
|
2473
|
+
let start = end - 3;
|
|
2474
|
+
while (start >= 0 && points[start] === points[end] && points[start + 1] === points[end + 1] && points[start + 2] === points[end + 2]) {
|
|
2475
|
+
start -= 3;
|
|
2476
|
+
}
|
|
2477
|
+
if (start < 0) return null;
|
|
2478
|
+
const viewProjection = mat48.multiply(mat48.create(), camera.proj, camera.view);
|
|
2479
|
+
const tip = vec46.transformMat4(
|
|
2480
|
+
vec46.create(),
|
|
2481
|
+
vec46.fromValues(points[end], points[end + 1], points[end + 2], 1),
|
|
2482
|
+
viewProjection
|
|
2483
|
+
);
|
|
2484
|
+
if (!Array.from(tip).every(Number.isFinite) || tip[3] <= 0 || Math.abs(tip[0]) > tip[3] || Math.abs(tip[1]) > tip[3] || Math.abs(tip[2]) > tip[3]) {
|
|
2485
|
+
return null;
|
|
2486
|
+
}
|
|
2487
|
+
const clipped = clipExplosionLineDrawCall(
|
|
2488
|
+
{
|
|
2489
|
+
...drawCall,
|
|
2490
|
+
positions: new Float32Array([
|
|
2491
|
+
points[start],
|
|
2492
|
+
points[start + 1],
|
|
2493
|
+
points[start + 2],
|
|
2494
|
+
points[end],
|
|
2495
|
+
points[end + 1],
|
|
2496
|
+
points[end + 2]
|
|
2497
|
+
]),
|
|
2498
|
+
indices: new Uint32Array([0, 1])
|
|
2499
|
+
},
|
|
2500
|
+
camera
|
|
2501
|
+
);
|
|
2502
|
+
if (!clipped.indices?.length) return null;
|
|
2503
|
+
const tail = vec46.transformMat4(
|
|
2504
|
+
vec46.create(),
|
|
2505
|
+
vec46.fromValues(
|
|
2506
|
+
clipped.positions[0],
|
|
2507
|
+
clipped.positions[1],
|
|
2508
|
+
clipped.positions[2],
|
|
2509
|
+
1
|
|
2510
|
+
),
|
|
2511
|
+
viewProjection
|
|
2512
|
+
);
|
|
2513
|
+
if (tail[3] <= 0) return null;
|
|
2514
|
+
const dx = (tip[0] / tip[3] - tail[0] / tail[3]) * (width - 1) / 2;
|
|
2515
|
+
const dy = (tip[1] / tip[3] - tail[1] / tail[3]) * (height - 1) / 2;
|
|
2516
|
+
const length = Math.hypot(dx, dy);
|
|
2517
|
+
if (!Number.isFinite(length) || length < 1e-3) return null;
|
|
2518
|
+
const arrowLength = Math.min(10 * supersampling, length / 2);
|
|
2519
|
+
const ux = dx / length;
|
|
2520
|
+
const uy = dy / length;
|
|
2521
|
+
const inverse = mat48.invert(mat48.create(), viewProjection);
|
|
2522
|
+
if (!inverse) return null;
|
|
2523
|
+
const positions = [points[end], points[end + 1], points[end + 2]];
|
|
2524
|
+
for (const side of [-1, 1]) {
|
|
2525
|
+
const offsetX = -ux * arrowLength - side * uy * arrowLength * 0.45;
|
|
2526
|
+
const offsetY = -uy * arrowLength + side * ux * arrowLength * 0.45;
|
|
2527
|
+
const wing = vec46.transformMat4(
|
|
2528
|
+
vec46.create(),
|
|
2529
|
+
vec46.fromValues(
|
|
2530
|
+
tip[0] / tip[3] + 2 * offsetX / (width - 1),
|
|
2531
|
+
tip[1] / tip[3] + 2 * offsetY / (height - 1),
|
|
2532
|
+
tip[2] / tip[3],
|
|
2533
|
+
1
|
|
2534
|
+
),
|
|
2535
|
+
inverse
|
|
2536
|
+
);
|
|
2537
|
+
if (!Array.from(wing).every(Number.isFinite) || wing[3] === 0) return null;
|
|
2538
|
+
positions.push(wing[0] / wing[3], wing[1] / wing[3], wing[2] / wing[3]);
|
|
2539
|
+
}
|
|
2540
|
+
return {
|
|
2541
|
+
...drawCall,
|
|
2542
|
+
positions: new Float32Array(positions),
|
|
2543
|
+
indices: new Uint32Array([0, 1, 0, 2])
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2203
2547
|
// lib/render/renderDrawCalls.ts
|
|
2204
2548
|
function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint8Bitmap) {
|
|
2205
2549
|
const options = resolveRenderOptions(optionsInput);
|
|
2206
2550
|
const renderWidth = options.width * options.supersampling;
|
|
2207
2551
|
const renderHeight = options.height * options.supersampling;
|
|
2552
|
+
const explosionDrawCalls = (options.explosionLines ?? []).map(
|
|
2553
|
+
createExplosionLineDrawCall
|
|
2554
|
+
);
|
|
2208
2555
|
const camera = buildCamera(
|
|
2209
|
-
drawCalls,
|
|
2556
|
+
[...drawCalls, ...explosionDrawCalls],
|
|
2210
2557
|
renderWidth,
|
|
2211
2558
|
renderHeight,
|
|
2212
2559
|
options.fov,
|
|
@@ -2306,7 +2653,9 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
|
|
|
2306
2653
|
for (let i = 0; i < dcs.length; i++) {
|
|
2307
2654
|
const dc = dcs[i];
|
|
2308
2655
|
if (dc.mode === 1) {
|
|
2309
|
-
renderer.drawLines(dc, camera, options.gamma
|
|
2656
|
+
renderer.drawLines(dc, camera, options.gamma, {
|
|
2657
|
+
width: options.lineWidth * options.supersampling
|
|
2658
|
+
});
|
|
2310
2659
|
} else {
|
|
2311
2660
|
renderer.drawMesh(
|
|
2312
2661
|
dc,
|
|
@@ -2342,23 +2691,64 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
|
|
|
2342
2691
|
renderGroup(blendDraws);
|
|
2343
2692
|
renderer.denoise(options.gamma);
|
|
2344
2693
|
for (const dc of drawCalls) {
|
|
2345
|
-
|
|
2694
|
+
const showHiddenEdges = options.showHiddenEdges ?? dc.showHiddenEdges;
|
|
2695
|
+
if (!options.showEdges && !showHiddenEdges) continue;
|
|
2346
2696
|
const edgeDrawCall = createEdgeDrawCall(dc);
|
|
2347
2697
|
if (!edgeDrawCall) continue;
|
|
2348
2698
|
renderer.drawLines(edgeDrawCall, camera, options.gamma, {
|
|
2349
2699
|
depthCompare: "less-equal",
|
|
2350
2700
|
depthWrite: false,
|
|
2351
|
-
depthBias: 1e-4
|
|
2701
|
+
depthBias: 1e-4,
|
|
2702
|
+
width: options.edgeWidth * options.supersampling
|
|
2352
2703
|
});
|
|
2704
|
+
if (!showHiddenEdges) continue;
|
|
2353
2705
|
renderer.drawLines(edgeDrawCall, camera, options.gamma, {
|
|
2354
2706
|
depthCompare: "greater",
|
|
2355
2707
|
depthWrite: false,
|
|
2356
2708
|
depthBias: 1e-4,
|
|
2357
2709
|
dashed: true,
|
|
2358
2710
|
dashLength: 4 * options.supersampling,
|
|
2359
|
-
opacity:
|
|
2711
|
+
opacity: options.hiddenEdgeOpacity,
|
|
2712
|
+
width: options.edgeWidth * options.supersampling
|
|
2360
2713
|
});
|
|
2361
2714
|
}
|
|
2715
|
+
for (const [index, drawCall] of explosionDrawCalls.entries()) {
|
|
2716
|
+
renderer.drawLines(
|
|
2717
|
+
clipExplosionLineDrawCall(drawCall, camera),
|
|
2718
|
+
camera,
|
|
2719
|
+
options.gamma,
|
|
2720
|
+
{
|
|
2721
|
+
depthCompare: "less-equal",
|
|
2722
|
+
depthWrite: false,
|
|
2723
|
+
depthBias: 1e-4,
|
|
2724
|
+
dashed: options.explosionLines[index].dashed ?? true,
|
|
2725
|
+
dashLength: 4 * options.supersampling,
|
|
2726
|
+
width: options.lineWidth * options.supersampling
|
|
2727
|
+
}
|
|
2728
|
+
);
|
|
2729
|
+
if (options.explosionLines[index].arrow) {
|
|
2730
|
+
const arrow = createExplosionArrowDrawCall(
|
|
2731
|
+
drawCall,
|
|
2732
|
+
camera,
|
|
2733
|
+
renderWidth,
|
|
2734
|
+
renderHeight,
|
|
2735
|
+
options.supersampling
|
|
2736
|
+
);
|
|
2737
|
+
if (arrow) {
|
|
2738
|
+
renderer.drawLines(
|
|
2739
|
+
clipExplosionLineDrawCall(arrow, camera),
|
|
2740
|
+
camera,
|
|
2741
|
+
options.gamma,
|
|
2742
|
+
{
|
|
2743
|
+
depthCompare: "less-equal",
|
|
2744
|
+
depthWrite: false,
|
|
2745
|
+
depthBias: 1e-4,
|
|
2746
|
+
width: options.lineWidth * options.supersampling
|
|
2747
|
+
}
|
|
2748
|
+
);
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2362
2752
|
if (options.debugPoints?.length) {
|
|
2363
2753
|
drawDebugPoints(
|
|
2364
2754
|
renderer,
|
|
@@ -2379,7 +2769,7 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
|
|
|
2379
2769
|
}
|
|
2380
2770
|
|
|
2381
2771
|
// lib/gltf/createInfiniteGrid.ts
|
|
2382
|
-
import { mat4 as
|
|
2772
|
+
import { mat4 as mat49 } from "gl-matrix";
|
|
2383
2773
|
function createInfiniteGrid(options = {}) {
|
|
2384
2774
|
const cellSize = options.cellSize ?? 1;
|
|
2385
2775
|
const sectionSize = options.sectionSize ?? 10;
|
|
@@ -2403,9 +2793,9 @@ function createInfiniteGrid(options = {}) {
|
|
|
2403
2793
|
size
|
|
2404
2794
|
]);
|
|
2405
2795
|
const indices = new Uint32Array([0, 1, 2, 0, 2, 3]);
|
|
2406
|
-
const model =
|
|
2796
|
+
const model = mat49.create();
|
|
2407
2797
|
const offset = options.offset ?? {};
|
|
2408
|
-
|
|
2798
|
+
mat49.fromTranslation(model, [offset.x ?? 0, offset.y ?? 0, offset.z ?? 0]);
|
|
2409
2799
|
return {
|
|
2410
2800
|
positions,
|
|
2411
2801
|
indices,
|
|
@@ -2430,7 +2820,7 @@ function createInfiniteGrid(options = {}) {
|
|
|
2430
2820
|
}
|
|
2431
2821
|
|
|
2432
2822
|
// lib/gltf/createSceneFromGLTF.ts
|
|
2433
|
-
import { mat4 as
|
|
2823
|
+
import { mat4 as mat410 } from "gl-matrix";
|
|
2434
2824
|
var COMPONENT_INFO = {
|
|
2435
2825
|
5120: {
|
|
2436
2826
|
name: "BYTE",
|
|
@@ -2637,17 +3027,17 @@ function readIndices(ctx, accessorIndex) {
|
|
|
2637
3027
|
}
|
|
2638
3028
|
function nodeLocalMatrix(node) {
|
|
2639
3029
|
if (node.matrix) {
|
|
2640
|
-
const m2 =
|
|
3030
|
+
const m2 = mat410.create();
|
|
2641
3031
|
for (let i = 0; i < 16; i++) m2[i] = node.matrix[i];
|
|
2642
3032
|
return m2;
|
|
2643
3033
|
}
|
|
2644
3034
|
const translation = node.translation || [0, 0, 0];
|
|
2645
3035
|
const rotation = node.rotation || [0, 0, 0, 1];
|
|
2646
3036
|
const scale = node.scale || [1, 1, 1];
|
|
2647
|
-
const m =
|
|
2648
|
-
const tmp =
|
|
2649
|
-
|
|
2650
|
-
|
|
3037
|
+
const m = mat410.create();
|
|
3038
|
+
const tmp = mat410.create();
|
|
3039
|
+
mat410.fromRotationTranslationScale(tmp, rotation, translation, scale);
|
|
3040
|
+
mat410.copy(m, tmp);
|
|
2651
3041
|
return m;
|
|
2652
3042
|
}
|
|
2653
3043
|
function getMaterial(gltf, textures, images, materialIndex) {
|
|
@@ -2690,8 +3080,8 @@ function createSceneFromGLTF(gltf, resources) {
|
|
|
2690
3080
|
const node = nodes[nodeIndex];
|
|
2691
3081
|
if (!node) return;
|
|
2692
3082
|
const local = nodeLocalMatrix(node);
|
|
2693
|
-
const world =
|
|
2694
|
-
|
|
3083
|
+
const world = mat410.create();
|
|
3084
|
+
mat410.multiply(world, parentMatrix, local);
|
|
2695
3085
|
if (Number.isInteger(node.mesh)) {
|
|
2696
3086
|
const mesh = meshes[node.mesh];
|
|
2697
3087
|
if (mesh) {
|
|
@@ -2730,7 +3120,7 @@ function createSceneFromGLTF(gltf, resources) {
|
|
|
2730
3120
|
traverse(childIndex, world);
|
|
2731
3121
|
}
|
|
2732
3122
|
}
|
|
2733
|
-
const identity =
|
|
3123
|
+
const identity = mat410.create();
|
|
2734
3124
|
for (const rootIndex of scene.nodes || []) {
|
|
2735
3125
|
traverse(rootIndex, identity);
|
|
2736
3126
|
}
|