vgpu-render 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +605 -2
- package/dist/assets/fira-mono-medium-subset.d.ts +4 -0
- package/dist/assets/fira-mono-medium-subset.js +889 -0
- package/dist/assets/fira-mono-msdf-atlas.d.ts +13 -0
- package/dist/assets/fira-mono-msdf-atlas.js +6222 -0
- package/dist/camera-2d.d.ts +43 -0
- package/dist/camera-2d.js +75 -0
- package/dist/color-space.d.ts +32 -0
- package/dist/color-space.js +45 -0
- package/dist/curve.d.ts +16 -0
- package/dist/curve.js +46 -0
- package/dist/debug-draw-3d.d.ts +14 -0
- package/dist/debug-draw-3d.js +36 -0
- package/dist/frame-draw-list-2d.d.ts +177 -0
- package/dist/frame-draw-list-2d.js +378 -0
- package/dist/frame-draw-list.d.ts +413 -0
- package/dist/frame-draw-list.js +556 -0
- package/dist/geometry-generators.d.ts +101 -0
- package/dist/geometry-generators.js +481 -0
- package/dist/gpu-debug-draw-2d.d.ts +48 -0
- package/dist/gpu-debug-draw-2d.js +450 -0
- package/dist/gpu-debug-draw-3d.d.ts +52 -0
- package/dist/gpu-debug-draw-3d.js +892 -0
- package/dist/gpu-instancing.d.ts +34 -0
- package/dist/gpu-instancing.js +93 -0
- package/dist/gpu-text-3d.d.ts +98 -0
- package/dist/gpu-text-3d.js +514 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +20 -0
- package/dist/infinite-grid-2d.d.ts +73 -0
- package/dist/infinite-grid-2d.js +253 -0
- package/dist/infinite-grid-3d.d.ts +134 -0
- package/dist/infinite-grid-3d.js +407 -0
- package/dist/mesh-batch-3d.d.ts +10 -0
- package/dist/mesh-batch-3d.js +67 -0
- package/dist/shape2d-shader.d.ts +14 -0
- package/dist/shape2d-shader.js +288 -0
- package/dist/text-atlas.d.ts +176 -0
- package/dist/text-atlas.js +498 -0
- package/dist/text-font.d.ts +190 -0
- package/dist/text-font.js +261 -0
- package/dist/text-layout.d.ts +175 -0
- package/dist/text-layout.js +390 -0
- package/dist/text-msdf-baked.d.ts +41 -0
- package/dist/text-msdf-baked.js +117 -0
- package/dist/text-msdf-font.d.ts +85 -0
- package/dist/text-msdf-font.js +159 -0
- package/dist/text-msdf.d.ts +70 -0
- package/dist/text-msdf.js +242 -0
- package/dist/text-raster.d.ts +40 -0
- package/dist/text-raster.js +60 -0
- package/dist/text-sdf.d.ts +28 -0
- package/dist/text-sdf.js +119 -0
- package/dist/vector-math.d.ts +19 -0
- package/dist/vector-math.js +37 -0
- package/package.json +80 -4
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import { crossVector, normalizeVector, rotateAroundAxis } from "./vector-math.js";
|
|
2
|
+
const add = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
|
3
|
+
const scale = (a, n) => [a[0] * n, a[1] * n, a[2] * n];
|
|
4
|
+
const rotate = (p, rotation) => {
|
|
5
|
+
const [sx, cx] = [Math.sin(rotation[0]), Math.cos(rotation[0])];
|
|
6
|
+
const [sy, cy] = [Math.sin(rotation[1]), Math.cos(rotation[1])];
|
|
7
|
+
const [sz, cz] = [Math.sin(rotation[2]), Math.cos(rotation[2])];
|
|
8
|
+
const x1 = p[0] * cy + p[2] * sy;
|
|
9
|
+
const z1 = -p[0] * sy + p[2] * cy;
|
|
10
|
+
const y1 = p[1] * cx - z1 * sx;
|
|
11
|
+
const z2 = p[1] * sx + z1 * cx;
|
|
12
|
+
return [x1 * cz - y1 * sz, x1 * sz + y1 * cz, z2];
|
|
13
|
+
};
|
|
14
|
+
const transformMesh = (mesh, rotation, center) => ({
|
|
15
|
+
triangles: mesh.triangles.map(([a, b, c]) => [add(rotate(a, rotation), center), add(rotate(b, rotation), center), add(rotate(c, rotation), center)]),
|
|
16
|
+
segments: mesh.segments.map(([a, b]) => [add(rotate(a, rotation), center), add(rotate(b, rotation), center)]),
|
|
17
|
+
});
|
|
18
|
+
export function rect(size, center = [0, 0, 0], rotation = [0, 0, 0]) {
|
|
19
|
+
const [w, h] = size;
|
|
20
|
+
const p = [[-w / 2, 0, -h / 2], [w / 2, 0, -h / 2], [w / 2, 0, h / 2], [-w / 2, 0, h / 2]];
|
|
21
|
+
return transformMesh({ triangles: [[p[0], p[1], p[2]], [p[0], p[2], p[3]]], segments: [[p[0], p[1]], [p[1], p[2]], [p[2], p[3]], [p[3], p[0]]] }, rotation, center);
|
|
22
|
+
}
|
|
23
|
+
export function box(size, center = [0, 0, 0], rotation = [0, 0, 0]) {
|
|
24
|
+
const [x, y, z] = scale(size, 0.5);
|
|
25
|
+
const p = [[-x, -y, -z], [x, -y, -z], [x, -y, z], [-x, -y, z], [-x, y, -z], [x, y, -z], [x, y, z], [-x, y, z]];
|
|
26
|
+
const faces = [[p[0], p[2], p[1]], [p[0], p[3], p[2]], [p[4], p[5], p[6]], [p[4], p[6], p[7]], [p[0], p[1], p[5]], [p[0], p[5], p[4]], [p[1], p[2], p[6]], [p[1], p[6], p[5]], [p[2], p[3], p[7]], [p[2], p[7], p[6]], [p[3], p[0], p[4]], [p[3], p[4], p[7]]];
|
|
27
|
+
const edges = [[p[0], p[1]], [p[1], p[2]], [p[2], p[3]], [p[3], p[0]], [p[4], p[5]], [p[5], p[6]], [p[6], p[7]], [p[7], p[4]], [p[0], p[4]], [p[1], p[5]], [p[2], p[6]], [p[3], p[7]]];
|
|
28
|
+
return transformMesh({ triangles: faces, segments: edges }, rotation, center);
|
|
29
|
+
}
|
|
30
|
+
export function disc(radius, segments = 32, center = [0, 0, 0], rotation = [0, 0, 0]) {
|
|
31
|
+
const triangles = [];
|
|
32
|
+
const segmentsOut = [];
|
|
33
|
+
const origin = [0, 0, 0];
|
|
34
|
+
for (let i = 0; i < segments; i++) {
|
|
35
|
+
const a = i / segments * Math.PI * 2;
|
|
36
|
+
const b = (i + 1) / segments * Math.PI * 2;
|
|
37
|
+
const p = [Math.cos(a) * radius, 0, Math.sin(a) * radius];
|
|
38
|
+
const q = [Math.cos(b) * radius, 0, Math.sin(b) * radius];
|
|
39
|
+
triangles.push([origin, p, q]);
|
|
40
|
+
segmentsOut.push([p, q]);
|
|
41
|
+
}
|
|
42
|
+
return transformMesh({ triangles, segments: segmentsOut }, rotation, center);
|
|
43
|
+
}
|
|
44
|
+
export function arc(radius, angle, segments = 32, center = [0, 0, 0], rotation = [0, 0, 0]) {
|
|
45
|
+
const segmentsOut = [];
|
|
46
|
+
const steps = Math.max(1, segments);
|
|
47
|
+
for (let i = 0; i < steps; i++) {
|
|
48
|
+
const a = i / steps * angle;
|
|
49
|
+
const b = (i + 1) / steps * angle;
|
|
50
|
+
segmentsOut.push([[Math.cos(a) * radius, 0, Math.sin(a) * radius], [Math.cos(b) * radius, 0, Math.sin(b) * radius]]);
|
|
51
|
+
}
|
|
52
|
+
return transformMesh({ triangles: [], segments: segmentsOut }, rotation, center);
|
|
53
|
+
}
|
|
54
|
+
export function cylinder(radius, halfHeight, segments = 32, center = [0, 0, 0], rotation = [0, 0, 0]) {
|
|
55
|
+
const triangles = [];
|
|
56
|
+
const edges = [];
|
|
57
|
+
const top = [];
|
|
58
|
+
const bottom = [];
|
|
59
|
+
for (let i = 0; i < segments; i++) {
|
|
60
|
+
const a = i / segments * Math.PI * 2;
|
|
61
|
+
bottom.push([Math.cos(a) * radius, -halfHeight, Math.sin(a) * radius]);
|
|
62
|
+
top.push([Math.cos(a) * radius, halfHeight, Math.sin(a) * radius]);
|
|
63
|
+
}
|
|
64
|
+
for (let i = 0; i < segments; i++) {
|
|
65
|
+
const n = (i + 1) % segments;
|
|
66
|
+
triangles.push([bottom[i], bottom[n], top[n]], [bottom[i], top[n], top[i]], [[0, -halfHeight, 0], bottom[n], bottom[i]], [[0, halfHeight, 0], top[i], top[n]]);
|
|
67
|
+
edges.push([bottom[i], bottom[n]], [top[i], top[n]], [bottom[i], top[i]]);
|
|
68
|
+
}
|
|
69
|
+
return transformMesh({ triangles, segments: edges }, rotation, center);
|
|
70
|
+
}
|
|
71
|
+
export function cone(radius, height, segments = 32, center = [0, 0, 0], rotation = [0, 0, 0]) {
|
|
72
|
+
const triangles = [];
|
|
73
|
+
const edges = [];
|
|
74
|
+
const apex = [0, height / 2, 0];
|
|
75
|
+
const base = [];
|
|
76
|
+
for (let i = 0; i < segments; i++) {
|
|
77
|
+
const a = i / segments * Math.PI * 2;
|
|
78
|
+
base.push([Math.cos(a) * radius, -height / 2, Math.sin(a) * radius]);
|
|
79
|
+
}
|
|
80
|
+
for (let i = 0; i < segments; i++) {
|
|
81
|
+
const n = (i + 1) % segments;
|
|
82
|
+
triangles.push([base[i], base[n], apex], [[0, -height / 2, 0], base[n], base[i]]);
|
|
83
|
+
edges.push([base[i], base[n]], [base[i], apex]);
|
|
84
|
+
}
|
|
85
|
+
return transformMesh({ triangles, segments: edges }, rotation, center);
|
|
86
|
+
}
|
|
87
|
+
export function sphere(radius, segments = 24, rings = 12, center = [0, 0, 0], rotation = [0, 0, 0]) {
|
|
88
|
+
const triangles = [];
|
|
89
|
+
const edges = [];
|
|
90
|
+
const rows = [];
|
|
91
|
+
for (let y = 0; y <= rings; y++) {
|
|
92
|
+
const v = y / rings;
|
|
93
|
+
const phi = v * Math.PI;
|
|
94
|
+
const row = [];
|
|
95
|
+
for (let x = 0; x < segments; x++) {
|
|
96
|
+
const a = x / segments * Math.PI * 2;
|
|
97
|
+
row.push([Math.sin(phi) * Math.cos(a) * radius, Math.cos(phi) * radius, Math.sin(phi) * Math.sin(a) * radius]);
|
|
98
|
+
}
|
|
99
|
+
rows.push(row);
|
|
100
|
+
}
|
|
101
|
+
for (let y = 0; y < rings; y++)
|
|
102
|
+
for (let x = 0; x < segments; x++) {
|
|
103
|
+
const n = (x + 1) % segments;
|
|
104
|
+
const a = rows[y][x];
|
|
105
|
+
const b = rows[y][n];
|
|
106
|
+
const c = rows[y + 1][n];
|
|
107
|
+
const d = rows[y + 1][x];
|
|
108
|
+
triangles.push([a, b, c], [a, c, d]);
|
|
109
|
+
edges.push([a, b], [a, d]);
|
|
110
|
+
}
|
|
111
|
+
return transformMesh({ triangles, segments: edges }, rotation, center);
|
|
112
|
+
}
|
|
113
|
+
export function capsule(radius, halfHeight, segments = 24, rings = 8, center = [0, 0, 0], rotation = [0, 0, 0]) {
|
|
114
|
+
const body = cylinder(radius, halfHeight, segments, center, rotation);
|
|
115
|
+
const top = sphere(radius, segments, rings, add(center, [0, halfHeight, 0]), rotation);
|
|
116
|
+
const bottom = sphere(radius, segments, rings, add(center, [0, -halfHeight, 0]), rotation);
|
|
117
|
+
return { triangles: [...body.triangles, ...top.triangles, ...bottom.triangles], segments: [...body.segments, ...top.segments, ...bottom.segments] };
|
|
118
|
+
}
|
|
119
|
+
/** Reference `CIRCLE_SEGMENT_COUNT_MIN`; below it the reference emits nothing. */
|
|
120
|
+
const CIRCLE_SEGMENT_COUNT_MIN = 3;
|
|
121
|
+
/** Reference `SPHERE_AXIS_SEGMENT_COUNT_MIN`. */
|
|
122
|
+
const SPHERE_AXIS_SEGMENT_COUNT_MIN = 1;
|
|
123
|
+
/** Reference `is_zero_approx` (reference `CMP_EPSILON`). */
|
|
124
|
+
const isZeroApprox = (value) => Math.abs(value) < 0.00001;
|
|
125
|
+
const addVec = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
|
126
|
+
const subVec = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
127
|
+
const scaleVec = (v, factor) => [v[0] * factor, v[1] * factor, v[2] * factor];
|
|
128
|
+
const lengthSquared = (v) => v[0] * v[0] + v[1] * v[1] + v[2] * v[2];
|
|
129
|
+
/** `_get_cone_base_rotation`: `Basis(width_axis, axis.cross(width_axis), axis)`. */
|
|
130
|
+
export function coneBaseBasis(basis) {
|
|
131
|
+
const axis = normalizeVector(basis.y);
|
|
132
|
+
const widthAxis = normalizeVector(basis.x);
|
|
133
|
+
const heightAxis = crossVector(axis, widthAxis);
|
|
134
|
+
const height = lengthSquared(heightAxis) <= 0 ? normalizeVector(basis.z) : normalizeVector(heightAxis);
|
|
135
|
+
return { x: widthAxis, y: height, z: axis };
|
|
136
|
+
}
|
|
137
|
+
/** `_get_circle_points`: `center + (basis.x * cos + basis.y * sin) * radius`. */
|
|
138
|
+
export function circlePoints(center, radius, segments, basis) {
|
|
139
|
+
if (radius <= 0 || segments < CIRCLE_SEGMENT_COUNT_MIN)
|
|
140
|
+
return [];
|
|
141
|
+
const points = [];
|
|
142
|
+
for (let i = 0; i < segments; i++) {
|
|
143
|
+
const angle = (Math.PI * 2 * i) / segments;
|
|
144
|
+
const cos = Math.cos(angle) * radius;
|
|
145
|
+
const sin = Math.sin(angle) * radius;
|
|
146
|
+
points.push([
|
|
147
|
+
center[0] + basis.x[0] * cos + basis.y[0] * sin,
|
|
148
|
+
center[1] + basis.x[1] * cos + basis.y[1] * sin,
|
|
149
|
+
center[2] + basis.x[2] * cos + basis.y[2] * sin,
|
|
150
|
+
]);
|
|
151
|
+
}
|
|
152
|
+
return points;
|
|
153
|
+
}
|
|
154
|
+
/** `Basis(axis, angle) * basis` in column language: rotate every column around `axis`. */
|
|
155
|
+
function rotateCircleBasis(basis, axis, angle) {
|
|
156
|
+
return {
|
|
157
|
+
x: rotateAroundAxis(basis.x, axis, angle),
|
|
158
|
+
y: rotateAroundAxis(basis.y, axis, angle),
|
|
159
|
+
z: rotateAroundAxis(basis.z, axis, angle),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/** `_should_flip_sphere_ring_normal`: canonicalise a ring normal for de-duplication. */
|
|
163
|
+
function shouldFlipRingNormal(normal) {
|
|
164
|
+
if (!isZeroApprox(normal[0]))
|
|
165
|
+
return normal[0] < 0;
|
|
166
|
+
if (!isZeroApprox(normal[1]))
|
|
167
|
+
return normal[1] < 0;
|
|
168
|
+
return normal[2] < 0;
|
|
169
|
+
}
|
|
170
|
+
/** `_get_sphere_ring_key`: quantise center/normal/radius so duplicates collapse. */
|
|
171
|
+
function sphereRingKey(center, normal, radius) {
|
|
172
|
+
let canonical = normalizeVector(normal);
|
|
173
|
+
if (shouldFlipRingNormal(canonical))
|
|
174
|
+
canonical = scaleVec(canonical, -1);
|
|
175
|
+
const quantize = (value) => Math.round(value * 100000);
|
|
176
|
+
return [
|
|
177
|
+
quantize(center[0]), quantize(center[1]), quantize(center[2]),
|
|
178
|
+
quantize(canonical[0]), quantize(canonical[1]), quantize(canonical[2]),
|
|
179
|
+
quantize(radius),
|
|
180
|
+
].join(":");
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* `_get_circle_normals`: outward normals of a circle, sampled half a segment ahead of each
|
|
184
|
+
* point so every segment gets the radial direction at its own midpoint.
|
|
185
|
+
*/
|
|
186
|
+
export function circleNormals(segments, basis) {
|
|
187
|
+
if (segments < CIRCLE_SEGMENT_COUNT_MIN)
|
|
188
|
+
return [];
|
|
189
|
+
const normals = [];
|
|
190
|
+
for (let i = 0; i < segments; i++) {
|
|
191
|
+
const angle = (Math.PI * 2 * (i + 0.5)) / segments;
|
|
192
|
+
const cos = Math.cos(angle);
|
|
193
|
+
const sin = Math.sin(angle);
|
|
194
|
+
normals.push([
|
|
195
|
+
basis.x[0] * cos + basis.y[0] * sin,
|
|
196
|
+
basis.x[1] * cos + basis.y[1] * sin,
|
|
197
|
+
basis.x[2] * cos + basis.y[2] * sin,
|
|
198
|
+
]);
|
|
199
|
+
}
|
|
200
|
+
return normals;
|
|
201
|
+
}
|
|
202
|
+
/** `_get_sphere_ring_normals`: each normal points from the sphere centre through the midpoint. */
|
|
203
|
+
export function sphereRingNormals(sphereCenter, points) {
|
|
204
|
+
const normals = [];
|
|
205
|
+
for (let i = 0; i < points.length; i++) {
|
|
206
|
+
const next = points[(i + 1) % points.length];
|
|
207
|
+
const point = points[i];
|
|
208
|
+
normals.push(normalizeVector([
|
|
209
|
+
(point[0] + next[0]) * 0.5 - sphereCenter[0],
|
|
210
|
+
(point[1] + next[1]) * 0.5 - sphereCenter[1],
|
|
211
|
+
(point[2] + next[2]) * 0.5 - sphereCenter[2],
|
|
212
|
+
]));
|
|
213
|
+
}
|
|
214
|
+
return normals;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* `_get_capsule_side_outline_normals`: project each segment midpoint onto the capsule axis
|
|
218
|
+
* and use the direction from that axis point as the normal.
|
|
219
|
+
*
|
|
220
|
+
* Applied to a cap ring this reproduces `_get_circle_normals` exactly, because the ring
|
|
221
|
+
* midpoints project onto the cap centre and the resulting directions are the ring radials.
|
|
222
|
+
*/
|
|
223
|
+
export function capsuleOutlineNormals(bottomCenter, topCenter, axis, points) {
|
|
224
|
+
const midHeight = Math.hypot(topCenter[0] - bottomCenter[0], topCenter[1] - bottomCenter[1], topCenter[2] - bottomCenter[2]);
|
|
225
|
+
const normals = [];
|
|
226
|
+
for (let i = 0; i < points.length; i++) {
|
|
227
|
+
const next = points[(i + 1) % points.length];
|
|
228
|
+
const point = points[i];
|
|
229
|
+
const midpoint = [(point[0] + next[0]) * 0.5, (point[1] + next[1]) * 0.5, (point[2] + next[2]) * 0.5];
|
|
230
|
+
const fromBottom = subVec(midpoint, bottomCenter);
|
|
231
|
+
const axisDistance = Math.min(Math.max(fromBottom[0] * axis[0] + fromBottom[1] * axis[1] + fromBottom[2] * axis[2], 0), midHeight);
|
|
232
|
+
const normalCenter = addVec(bottomCenter, scaleVec(axis, axisDistance));
|
|
233
|
+
normals.push(normalizeVector(subVec(midpoint, normalCenter)));
|
|
234
|
+
}
|
|
235
|
+
return normals;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* `_get_arc_normals`: radial normals at each segment midpoint of an arc sweep. An arc is a
|
|
239
|
+
* strip, so it takes one normal per segment rather than per vertex.
|
|
240
|
+
*/
|
|
241
|
+
export function arcNormals(angle, segments, basis) {
|
|
242
|
+
if (angle === 0 || segments < CIRCLE_SEGMENT_COUNT_MIN)
|
|
243
|
+
return [];
|
|
244
|
+
const normals = [];
|
|
245
|
+
for (let i = 0; i < segments; i++) {
|
|
246
|
+
const arcAngle = (angle * (i + 0.5)) / segments;
|
|
247
|
+
const cos = Math.cos(arcAngle);
|
|
248
|
+
const sin = Math.sin(arcAngle);
|
|
249
|
+
normals.push([
|
|
250
|
+
basis.x[0] * cos + basis.y[0] * sin,
|
|
251
|
+
basis.x[1] * cos + basis.y[1] * sin,
|
|
252
|
+
basis.x[2] * cos + basis.y[2] * sin,
|
|
253
|
+
]);
|
|
254
|
+
}
|
|
255
|
+
return normals;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* `_get_rect_normals`: one normal per corner, in the `_get_rect_points` order `rectLoop` uses
|
|
259
|
+
* (`-w-h`, `+w-h`, `+w+h`, `-w+h`).
|
|
260
|
+
*/
|
|
261
|
+
export function rectNormals(basis) {
|
|
262
|
+
return [scaleVec(basis.y, -1), basis.x, basis.y, scaleVec(basis.x, -1)];
|
|
263
|
+
}
|
|
264
|
+
/** Coordinates of `point` in the orthonormal frame `basis`, relative to `origin`. */
|
|
265
|
+
function localCoordinates(point, origin, basis) {
|
|
266
|
+
const offset = [point[0] - origin[0], point[1] - origin[1], point[2] - origin[2]];
|
|
267
|
+
return [
|
|
268
|
+
offset[0] * basis.x[0] + offset[1] * basis.x[1] + offset[2] * basis.x[2],
|
|
269
|
+
offset[0] * basis.y[0] + offset[1] * basis.y[1] + offset[2] * basis.y[2],
|
|
270
|
+
offset[0] * basis.z[0] + offset[1] * basis.z[1] + offset[2] * basis.z[2],
|
|
271
|
+
];
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* `_get_box_line_normals`, derived from the edges themselves rather than from a fixed table.
|
|
275
|
+
*
|
|
276
|
+
* A box edge runs along one basis axis and joins the two faces whose normals are the other two
|
|
277
|
+
* axes, so its normal is the sum of those two unit axes with the signs of the edge's position.
|
|
278
|
+
* Deriving it keeps the normals aligned with whatever order the mesh generator emits edges in,
|
|
279
|
+
* instead of depending on the reference's own point order.
|
|
280
|
+
*/
|
|
281
|
+
export function boxEdgeNormals(segments, center, basis) {
|
|
282
|
+
const axes = [basis.x, basis.y, basis.z];
|
|
283
|
+
const normals = [];
|
|
284
|
+
for (const [from, to] of segments) {
|
|
285
|
+
const fromLocal = localCoordinates(from, center, basis);
|
|
286
|
+
const toLocal = localCoordinates(to, center, basis);
|
|
287
|
+
// The axis the edge runs along is the one whose coordinate differs between its endpoints.
|
|
288
|
+
const along = [0, 1, 2].find((index) => Math.abs(fromLocal[index] - toLocal[index]) > 1e-9) ?? 0;
|
|
289
|
+
let normal = [0, 0, 0];
|
|
290
|
+
for (const index of [0, 1, 2]) {
|
|
291
|
+
if (index === along)
|
|
292
|
+
continue;
|
|
293
|
+
const sign = fromLocal[index] + toLocal[index] >= 0 ? 1 : -1;
|
|
294
|
+
normal = addVec(normal, scaleVec(axes[index], sign));
|
|
295
|
+
}
|
|
296
|
+
normals.push(normalizeVector(normal));
|
|
297
|
+
}
|
|
298
|
+
return normals;
|
|
299
|
+
}
|
|
300
|
+
/** `_get_cone_side_line_normals`: one normal per side line, tilted along the cone slope. */
|
|
301
|
+
export function coneSideLineNormals(radius, height, sideSegmentCount, basis) {
|
|
302
|
+
if (radius <= 0 || height <= 0 || sideSegmentCount <= 0)
|
|
303
|
+
return [];
|
|
304
|
+
const axis = normalizeVector(basis.y);
|
|
305
|
+
const baseBasis = coneBaseBasis(basis);
|
|
306
|
+
const normals = [];
|
|
307
|
+
for (let i = 0; i < sideSegmentCount; i++) {
|
|
308
|
+
const angle = (Math.PI * 2 * i) / sideSegmentCount;
|
|
309
|
+
const radial = addVec(scaleVec(baseBasis.x, Math.cos(angle)), scaleVec(baseBasis.y, Math.sin(angle)));
|
|
310
|
+
normals.push(normalizeVector(addVec(scaleVec(radial, height), scaleVec(axis, radius))));
|
|
311
|
+
}
|
|
312
|
+
return normals;
|
|
313
|
+
}
|
|
314
|
+
/** `_get_cylinder_side_line_normals`: one radial normal per wall line. */
|
|
315
|
+
export function cylinderSideLineNormals(radius, halfHeight, sideSegmentCount, basis) {
|
|
316
|
+
if (radius <= 0 || halfHeight <= 0 || sideSegmentCount <= 0)
|
|
317
|
+
return [];
|
|
318
|
+
const ringBasis = coneBaseBasis(basis);
|
|
319
|
+
const normals = [];
|
|
320
|
+
for (let i = 0; i < sideSegmentCount; i++) {
|
|
321
|
+
const angle = (Math.PI * 2 * i) / sideSegmentCount;
|
|
322
|
+
normals.push(normalizeVector(addVec(scaleVec(ringBasis.x, Math.cos(angle)), scaleVec(ringBasis.y, Math.sin(angle)))));
|
|
323
|
+
}
|
|
324
|
+
return normals;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* `_emit_sphere_axis_rings`: `y_axis_segment_count` latitude rings plus
|
|
328
|
+
* `z_axis_segment_count` meridian great circles, de-duplicated by ring key.
|
|
329
|
+
*
|
|
330
|
+
* - Latitude rings sit at `radius * (-1 + 2i/n)` for `i in 1..n-1` with ring radius
|
|
331
|
+
* `sqrt(r^2 - offset^2)`, so `n < 2` draws none and `n = 2` draws the equator only.
|
|
332
|
+
* - Great circles all have radius `radius` and advance by `PI / z` around `basis.y`, so
|
|
333
|
+
* `z < 1` draws none. A count of `z` yields `ceil(z / 2)` distinct circles, because the
|
|
334
|
+
* ring key folds opposite normals together.
|
|
335
|
+
*/
|
|
336
|
+
export function sphereWire(radius, segments, yAxisSegmentCount, zAxisSegmentCount, center, basis) {
|
|
337
|
+
if (radius <= 0 || segments < CIRCLE_SEGMENT_COUNT_MIN)
|
|
338
|
+
return { loops: [], lines: [] };
|
|
339
|
+
const loops = [];
|
|
340
|
+
const seen = new Set();
|
|
341
|
+
if (yAxisSegmentCount >= 2) {
|
|
342
|
+
// `Basis(basis.x, basis.z, -basis.y)`: ring plane spanned by basis.x/basis.z.
|
|
343
|
+
const ringBasis = { x: basis.x, y: basis.z, z: scaleVec(basis.y, -1) };
|
|
344
|
+
for (let i = 1; i < yAxisSegmentCount; i++) {
|
|
345
|
+
const offset = radius * (-1 + (2 * i) / yAxisSegmentCount);
|
|
346
|
+
const ringRadius = Math.sqrt(Math.max(radius * radius - offset * offset, 0));
|
|
347
|
+
if (ringRadius <= 0)
|
|
348
|
+
continue;
|
|
349
|
+
const ringCenter = addVec(center, scaleVec(basis.y, offset));
|
|
350
|
+
const key = sphereRingKey(ringCenter, basis.y, ringRadius);
|
|
351
|
+
if (seen.has(key))
|
|
352
|
+
continue;
|
|
353
|
+
seen.add(key);
|
|
354
|
+
loops.push(circlePoints(ringCenter, ringRadius, segments, ringBasis));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (zAxisSegmentCount >= SPHERE_AXIS_SEGMENT_COUNT_MIN) {
|
|
358
|
+
// `Basis(basis.y, basis.z, basis.x)` rotated in `PI / z` steps around basis.y.
|
|
359
|
+
const baseCircleBasis = { x: basis.y, y: basis.z, z: basis.x };
|
|
360
|
+
const axis = normalizeVector(basis.y);
|
|
361
|
+
const angleStep = Math.PI / zAxisSegmentCount;
|
|
362
|
+
for (let i = 0; i < zAxisSegmentCount; i++) {
|
|
363
|
+
const circleBasis = rotateCircleBasis(baseCircleBasis, axis, angleStep * i);
|
|
364
|
+
const key = sphereRingKey(center, circleBasis.z, radius);
|
|
365
|
+
if (seen.has(key))
|
|
366
|
+
continue;
|
|
367
|
+
seen.add(key);
|
|
368
|
+
loops.push(circlePoints(center, radius, segments, circleBasis));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return { loops, lines: [] };
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* `_get_capsule_side_outline_points`: half arc over the top cap, half arc under the
|
|
375
|
+
* bottom cap, forming the capsule silhouette in the `radial` plane.
|
|
376
|
+
*/
|
|
377
|
+
function capsuleSideOutline(bottomCenter, topCenter, axis, radial, radius, arcSegmentCount) {
|
|
378
|
+
const points = [];
|
|
379
|
+
for (let i = 0; i <= arcSegmentCount; i++) {
|
|
380
|
+
const angle = (Math.PI * i) / arcSegmentCount;
|
|
381
|
+
const offset = addVec(scaleVec(radial, Math.cos(angle) * radius), scaleVec(axis, Math.sin(angle) * radius));
|
|
382
|
+
points.push(addVec(topCenter, offset));
|
|
383
|
+
}
|
|
384
|
+
for (let i = 0; i <= arcSegmentCount; i++) {
|
|
385
|
+
const angle = Math.PI + (Math.PI * i) / arcSegmentCount;
|
|
386
|
+
const offset = addVec(scaleVec(radial, Math.cos(angle) * radius), scaleVec(axis, Math.sin(angle) * radius));
|
|
387
|
+
points.push(addVec(bottomCenter, offset));
|
|
388
|
+
}
|
|
389
|
+
return points;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* `_emit_capsule`: bottom/top cylinder rings plus `side_segment_count` closed side
|
|
393
|
+
* outlines. The reference has no solid capsule variant, so this is the whole wireframe.
|
|
394
|
+
*/
|
|
395
|
+
export function capsuleWire(radius, halfHeight, segments, sideSegmentCount, center, basis) {
|
|
396
|
+
if (radius <= 0 || halfHeight < 0 || segments < CIRCLE_SEGMENT_COUNT_MIN)
|
|
397
|
+
return { loops: [], lines: [] };
|
|
398
|
+
const axis = normalizeVector(basis.y);
|
|
399
|
+
const bottomCenter = subVec(center, scaleVec(axis, halfHeight));
|
|
400
|
+
const topCenter = addVec(center, scaleVec(axis, halfHeight));
|
|
401
|
+
const ringBasis = coneBaseBasis(basis);
|
|
402
|
+
const loops = [circlePoints(bottomCenter, radius, segments, ringBasis)];
|
|
403
|
+
if (bottomCenter.some((value, index) => Math.abs(value - topCenter[index]) >= 0.00001)) {
|
|
404
|
+
loops.push(circlePoints(topCenter, radius, segments, ringBasis));
|
|
405
|
+
}
|
|
406
|
+
if (sideSegmentCount >= SPHERE_AXIS_SEGMENT_COUNT_MIN) {
|
|
407
|
+
const arcSegmentCount = Math.max(1, Math.ceil(segments * 0.5));
|
|
408
|
+
for (let i = 0; i < sideSegmentCount; i++) {
|
|
409
|
+
const angle = (Math.PI * i) / sideSegmentCount;
|
|
410
|
+
const radial = normalizeVector(addVec(scaleVec(ringBasis.x, Math.cos(angle)), scaleVec(ringBasis.y, Math.sin(angle))));
|
|
411
|
+
loops.push(capsuleSideOutline(bottomCenter, topCenter, axis, radial, radius, arcSegmentCount));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return { loops, lines: [] };
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* `_get_cone_base_points` + `_get_cone_side_line_points`: the base ring is a loop and
|
|
418
|
+
* the side lines are `apex -> base point` pairs emitted as a line list.
|
|
419
|
+
*/
|
|
420
|
+
export function coneWire(radius, height, segments, sideSegmentCount, center, basis) {
|
|
421
|
+
if (radius <= 0 || height <= 0 || segments < CIRCLE_SEGMENT_COUNT_MIN)
|
|
422
|
+
return { loops: [], lines: [] };
|
|
423
|
+
const baseCenter = subVec(center, scaleVec(basis.y, height * 0.5));
|
|
424
|
+
const apex = addVec(center, scaleVec(basis.y, height * 0.5));
|
|
425
|
+
const baseBasis = coneBaseBasis(basis);
|
|
426
|
+
const lines = [];
|
|
427
|
+
if (sideSegmentCount > 0) {
|
|
428
|
+
for (let i = 0; i < sideSegmentCount; i++) {
|
|
429
|
+
const angle = (Math.PI * 2 * i) / sideSegmentCount;
|
|
430
|
+
const offset = addVec(scaleVec(baseBasis.x, Math.cos(angle) * radius), scaleVec(baseBasis.y, Math.sin(angle) * radius));
|
|
431
|
+
lines.push([apex, addVec(baseCenter, offset)]);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return { loops: [circlePoints(baseCenter, radius, segments, baseBasis)], lines };
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* `_get_cylinder_ring_points` + `_get_cylinder_side_line_points`: two rings and
|
|
438
|
+
* `side_segment_count` vertical wall segments.
|
|
439
|
+
*/
|
|
440
|
+
export function cylinderWire(radius, halfHeight, segments, sideSegmentCount, center, basis) {
|
|
441
|
+
if (radius <= 0 || halfHeight <= 0 || segments < CIRCLE_SEGMENT_COUNT_MIN)
|
|
442
|
+
return { loops: [], lines: [] };
|
|
443
|
+
const ringBasis = coneBaseBasis(basis);
|
|
444
|
+
const bottomCenter = subVec(center, scaleVec(basis.y, halfHeight));
|
|
445
|
+
const topCenter = addVec(center, scaleVec(basis.y, halfHeight));
|
|
446
|
+
const lines = [];
|
|
447
|
+
for (let i = 0; i < sideSegmentCount; i++) {
|
|
448
|
+
const angle = (Math.PI * 2 * i) / sideSegmentCount;
|
|
449
|
+
const radial = addVec(scaleVec(ringBasis.x, Math.cos(angle) * radius), scaleVec(ringBasis.y, Math.sin(angle) * radius));
|
|
450
|
+
lines.push([addVec(bottomCenter, radial), addVec(topCenter, radial)]);
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
loops: [circlePoints(bottomCenter, radius, segments, ringBasis), circlePoints(topCenter, radius, segments, ringBasis)],
|
|
454
|
+
lines,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
export function gridLines(cells, spacing, center = [0, 0, 0], rotation = [0, 0, 0], skew = [0, 0], outer = "none") {
|
|
458
|
+
const [cellX, cellZ] = cells;
|
|
459
|
+
const [spacingX, spacingZ] = spacing;
|
|
460
|
+
if (cellX <= 0 || cellZ <= 0 || spacingX <= 0 || spacingZ <= 0)
|
|
461
|
+
return [];
|
|
462
|
+
const skewTan = [Math.tan(skew[0]), Math.tan(skew[1])];
|
|
463
|
+
const dx = [spacingX, 0, spacingX * skewTan[1]];
|
|
464
|
+
const dz = [spacingZ * skewTan[0], 0, spacingZ];
|
|
465
|
+
const start = [-dx[0] * cellX * 0.5 - dz[0] * cellZ * 0.5, 0, -dx[2] * cellX * 0.5 - dz[2] * cellZ * 0.5];
|
|
466
|
+
const place = (p) => add(rotate(p, rotation), center);
|
|
467
|
+
const scale = (v, n) => [v[0] * n, v[1] * n, v[2] * n];
|
|
468
|
+
const plus = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
|
469
|
+
const keepX = outer === "x" || outer === "xz";
|
|
470
|
+
const keepZ = outer === "z" || outer === "xz";
|
|
471
|
+
const segments = [];
|
|
472
|
+
for (let zIndex = keepX ? 0 : 1; zIndex < (keepX ? cellZ + 1 : cellZ); zIndex++) {
|
|
473
|
+
const from = plus(start, scale(dz, zIndex));
|
|
474
|
+
segments.push([place(from), place(plus(from, scale(dx, cellX)))]);
|
|
475
|
+
}
|
|
476
|
+
for (let xIndex = keepZ ? 0 : 1; xIndex < (keepZ ? cellX + 1 : cellX); xIndex++) {
|
|
477
|
+
const from = plus(start, scale(dx, xIndex));
|
|
478
|
+
segments.push([place(from), place(plus(from, scale(dz, cellZ)))]);
|
|
479
|
+
}
|
|
480
|
+
return segments;
|
|
481
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GPU renderer for the 2D debug-draw layer.
|
|
3
|
+
*
|
|
4
|
+
* The 2D primitives are the 3D ones under a different projection. `camera-2d.ts` produces a clip
|
|
5
|
+
* matrix that lands `world2d`/`screen` pixels on the near plane, and the line, point and joint
|
|
6
|
+
* shaders already do everything after the projection in screen space — pixel widths, dash phase,
|
|
7
|
+
* joint wedges, the near-plane clip. So this renderer reuses that WGSL verbatim (injected as
|
|
8
|
+
* `shaders`, because the 3D renderer owns the source and must not be imported back) and only
|
|
9
|
+
* supplies its own instance buffers, indirect draws and camera uniforms.
|
|
10
|
+
*
|
|
11
|
+
* `world2d` and `screen` commands need different matrices, and one draw binds one uniform, so each
|
|
12
|
+
* space gets its own bucket of primitives. The layer draws after every 3D primitive with depth
|
|
13
|
+
* write off, which is what makes the 2D overlay unconditionally cover the 3D world.
|
|
14
|
+
*/
|
|
15
|
+
import { type FrameComputePass, type FramePass, type Gpu } from "vgpu";
|
|
16
|
+
import { type Camera2D } from "./camera-2d.js";
|
|
17
|
+
import type { FrameCommand2D } from "./frame-draw-list-2d.js";
|
|
18
|
+
export type GpuDebugDraw2DOptions = {
|
|
19
|
+
/**
|
|
20
|
+
* WGSL for the shared primitives, generated by `GpuDebugDraw3D` so the two layers cannot drift
|
|
21
|
+
* apart on the instance layout. The vertex stages must place the primitive through
|
|
22
|
+
* `camera.viewProjection * vec4f(position, 1.0)` and read `camera.viewportSize`.
|
|
23
|
+
*/
|
|
24
|
+
readonly shaders: {
|
|
25
|
+
readonly point: string;
|
|
26
|
+
readonly line: string;
|
|
27
|
+
readonly joint: string;
|
|
28
|
+
};
|
|
29
|
+
/** Vertices the injected joint shader fans out per wedge; must match its `ROUND_RESOLUTION`. */
|
|
30
|
+
readonly jointVertexCount: number;
|
|
31
|
+
readonly points?: number;
|
|
32
|
+
readonly lines?: number;
|
|
33
|
+
readonly joints?: number;
|
|
34
|
+
readonly shapes?: number;
|
|
35
|
+
};
|
|
36
|
+
export declare class GpuDebugDraw2D {
|
|
37
|
+
#private;
|
|
38
|
+
constructor(gpu: Gpu, options: GpuDebugDraw2DOptions);
|
|
39
|
+
/** Camera the `world2d` bucket projects through. `screen` commands ignore it. */
|
|
40
|
+
setCamera(camera: Camera2D): void;
|
|
41
|
+
get camera(): Camera2D;
|
|
42
|
+
prepare(commands: readonly FrameCommand2D[]): void;
|
|
43
|
+
dispatchIndirect(pass: FrameComputePass): void;
|
|
44
|
+
draw(pass: FramePass): void;
|
|
45
|
+
destroy(): void;
|
|
46
|
+
/** Instances the last `prepare` left in both buckets, for tests and status readouts. */
|
|
47
|
+
get primitiveCount(): number;
|
|
48
|
+
}
|