vgpu-render 0.0.0-stage → 0.1.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 +555 -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-recorder-2d.d.ts +177 -0
- package/dist/frame-recorder-2d.js +378 -0
- package/dist/frame-recorder.d.ts +361 -0
- package/dist/frame-recorder.js +515 -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 +890 -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 +467 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +21 -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/layout-3d.d.ts +227 -0
- package/dist/layout-3d.js +226 -0
- package/dist/mesh-batch-3d.d.ts +10 -0
- package/dist/mesh-batch-3d.js +67 -0
- package/dist/scene-examples.d.ts +39 -0
- package/dist/scene-examples.js +200 -0
- package/dist/scenes-2d.d.ts +18 -0
- package/dist/scenes-2d.js +160 -0
- package/dist/scenes-3d.d.ts +46 -0
- package/dist/scenes-3d.js +1994 -0
- package/dist/shape2d-shader.d.ts +14 -0
- package/dist/shape2d-shader.js +287 -0
- package/dist/text-atlas.d.ts +174 -0
- package/dist/text-atlas.js +495 -0
- package/dist/text-font.d.ts +149 -0
- package/dist/text-font.js +209 -0
- package/dist/text-layout.d.ts +116 -0
- package/dist/text-layout.js +340 -0
- package/dist/text-msdf-baked.d.ts +36 -0
- package/dist/text-msdf-baked.js +112 -0
- package/dist/text-msdf-font.d.ts +79 -0
- package/dist/text-msdf-font.js +153 -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-scenes.d.ts +85 -0
- package/dist/text-scenes.js +338 -0
- package/dist/text-sdf.d.ts +28 -0
- package/dist/text-sdf.js +119 -0
- package/package.json +80 -4
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
import { compute, draw, geometry, storage } from "vgpu";
|
|
2
|
+
import { prepareShader } from "@vgpu/wgsl/prepare";
|
|
3
|
+
import { instanceGeometry } from "vgpu/scene/gpu";
|
|
4
|
+
import { instances } from "vgpu/scene";
|
|
5
|
+
import { GpuDebugDraw2D } from "./gpu-debug-draw-2d.js";
|
|
6
|
+
import { depthBiasHelpers, identity, indirectArgs, INDIRECT_SHADER, trim, world, zeroGeometry } from "./gpu-instancing.js";
|
|
7
|
+
import { GpuText3D } from "./gpu-text-3d.js";
|
|
8
|
+
/**
|
|
9
|
+
* Reference scenes set `depth_bias = -0.001` on the reference before drawing. Keeping it as an
|
|
10
|
+
* option (and baking it into the WGSL) means the shader can match the reference exactly
|
|
11
|
+
* without adding a fourth uniform field that would break the shared camera layout.
|
|
12
|
+
*/
|
|
13
|
+
export const DEFAULT_DEPTH_BIAS = -0.001;
|
|
14
|
+
const normalFadeConstants = `
|
|
15
|
+
const BACK_FADE_START: f32 = 1.74532925199;
|
|
16
|
+
const BACK_FADE_TRANSITION: f32 = 0.34906585040;
|
|
17
|
+
const NORMAL_BACK_FADE_ANGLE_MAX: f32 = 3.14159265359;
|
|
18
|
+
const NORMAL_BACK_FADE_ANGLE_EPSILON: f32 = 0.0001;`;
|
|
19
|
+
/**
|
|
20
|
+
* Normal-aware shading shared by `debug_normal_line_3d.gdshader` and
|
|
21
|
+
* `debug_normal_line_joint_3d_common.gdshaderinc`.
|
|
22
|
+
*
|
|
23
|
+
* `normal_fade` is the whole difference between the plain and `_n` pipelines: a stroke whose
|
|
24
|
+
* surface normal points away from the camera keeps only 20% of its brightness, fading back
|
|
25
|
+
* to full between `back_fade_start` (100°) and `back_fade_start + back_fade_transition` (20°)
|
|
26
|
+
* from the view direction. Both the angle constants and the `0.2` floor are the reference
|
|
27
|
+
* defaults, which every example scene keeps.
|
|
28
|
+
*
|
|
29
|
+
* `safe_normalize3` mirrors reference `Vector3.normalized()`, which returns a zero vector for
|
|
30
|
+
* a zero input instead of NaN; a zero normal then yields `facing_dot = 0`, i.e. full
|
|
31
|
+
* brightness, which is also what the reference does at the camera origin.
|
|
32
|
+
*/
|
|
33
|
+
const normalFadeHelpers = `
|
|
34
|
+
${normalFadeConstants}
|
|
35
|
+
|
|
36
|
+
fn safe_normalize3(value: vec3f) -> vec3f {
|
|
37
|
+
let lengthSquared = dot(value, value);
|
|
38
|
+
if (lengthSquared == 0.0) { return vec3f(0.0); }
|
|
39
|
+
return value * inverseSqrt(lengthSquared);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fn normal_fade(normal: vec3f, position: vec3f) -> f32 {
|
|
43
|
+
let viewDirection = safe_normalize3(-position);
|
|
44
|
+
let facingDot = dot(normal, viewDirection);
|
|
45
|
+
let fadeStartAngle = clamp(BACK_FADE_START, 0.0, NORMAL_BACK_FADE_ANGLE_MAX - NORMAL_BACK_FADE_ANGLE_EPSILON);
|
|
46
|
+
let fadeEndAngle = min(
|
|
47
|
+
fadeStartAngle + max(BACK_FADE_TRANSITION, NORMAL_BACK_FADE_ANGLE_EPSILON),
|
|
48
|
+
NORMAL_BACK_FADE_ANGLE_MAX
|
|
49
|
+
);
|
|
50
|
+
let fadeStartDot = cos(fadeStartAngle);
|
|
51
|
+
let fadeEndDot = cos(fadeEndAngle);
|
|
52
|
+
return mix(0.2, 1.0, smoothstep(fadeEndDot, fadeStartDot, facingDot));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Reference _resolve_normal: a missing normal falls back to +Z. */
|
|
56
|
+
fn resolve_normal(normal: vec3f) -> vec3f {
|
|
57
|
+
if (dot(normal, normal) == 0.0) { return vec3f(0.0, 0.0, 1.0); }
|
|
58
|
+
return normalize(normal);
|
|
59
|
+
}
|
|
60
|
+
`;
|
|
61
|
+
function pointShader(depthBias) {
|
|
62
|
+
return `
|
|
63
|
+
struct Camera { viewProjection: mat4x4f, viewportSize: vec2f }
|
|
64
|
+
@group(0) @binding(0) var<uniform> camera: Camera;
|
|
65
|
+
${depthBiasHelpers(depthBias)}
|
|
66
|
+
struct In {
|
|
67
|
+
@location(0) position: vec3f,
|
|
68
|
+
@location(1) world0: vec4f, @location(2) world1: vec4f,
|
|
69
|
+
@location(3) world2: vec4f, @location(4) world3: vec4f,
|
|
70
|
+
@location(5) point: vec3f, @location(6) color: vec4f,
|
|
71
|
+
@location(7) size: f32, @location(8) flags: u32,
|
|
72
|
+
}
|
|
73
|
+
struct Out { @builtin(position) clip: vec4f, @location(0) color: vec4f, @location(1) uv: vec2f, @location(2) roundShape: f32 }
|
|
74
|
+
@vertex fn vs_main(input: In, @builtin(vertex_index) vertex: u32) -> Out {
|
|
75
|
+
let corners = array<vec2f, 6>(vec2f(-1,-1), vec2f(1,-1), vec2f(-1,1), vec2f(-1,1), vec2f(1,-1), vec2f(1,1));
|
|
76
|
+
let clip = camera.viewProjection * vec4f(input.point, 1.0);
|
|
77
|
+
var size = input.size;
|
|
78
|
+
if ((input.flags & 1u) != 0u) {
|
|
79
|
+
// World-space size uses the vertical projection term, not the combined axis length.
|
|
80
|
+
size *= camera.viewProjection[1][1] * camera.viewportSize.y * 0.5 / max(clip.w, 0.0001);
|
|
81
|
+
}
|
|
82
|
+
let offset = corners[vertex] * size / camera.viewportSize * clip.w;
|
|
83
|
+
var out: Out;
|
|
84
|
+
out.clip = vec4f(clip.xy + offset, apply_depth_bias(clip), clip.w);
|
|
85
|
+
out.color = input.color;
|
|
86
|
+
out.uv = corners[vertex];
|
|
87
|
+
out.roundShape = f32(input.flags & 2u);
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
@fragment fn fs_main(input: Out) -> @location(0) vec4f {
|
|
91
|
+
if (input.roundShape > 0.5 && dot(input.uv, input.uv) > 1.0) { discard; }
|
|
92
|
+
return input.color;
|
|
93
|
+
}
|
|
94
|
+
`;
|
|
95
|
+
}
|
|
96
|
+
function lineShader(depthBias) {
|
|
97
|
+
return `
|
|
98
|
+
struct Camera { viewProjection: mat4x4f, view: mat4x4f, viewportSize: vec2f }
|
|
99
|
+
@group(0) @binding(0) var<uniform> camera: Camera;
|
|
100
|
+
${depthBiasHelpers(depthBias)}
|
|
101
|
+
${normalFadeHelpers}
|
|
102
|
+
const CLIP_W_EPSILON: f32 = 0.0001;
|
|
103
|
+
const SCREEN_EPSILON: f32 = 0.0001;
|
|
104
|
+
|
|
105
|
+
/** Floored modulo, matching GLSL. WGSL's % truncates and would shift negative dash phases. */
|
|
106
|
+
fn floored_mod(value: f32, period: f32) -> f32 {
|
|
107
|
+
return value - period * floor(value / period);
|
|
108
|
+
}
|
|
109
|
+
struct In {
|
|
110
|
+
@location(0) position: vec3f,
|
|
111
|
+
@location(1) world0: vec4f, @location(2) world1: vec4f,
|
|
112
|
+
@location(3) world2: vec4f, @location(4) world3: vec4f,
|
|
113
|
+
@location(5) start: vec3f, @location(6) end: vec3f,
|
|
114
|
+
@location(7) color: vec4f, @location(8) colorEnd: vec4f,
|
|
115
|
+
@location(9) width: f32, @location(10) flags: u32,
|
|
116
|
+
@location(11) dash: vec3f,
|
|
117
|
+
@location(12) normal: vec3f, @location(13) joint: u32,
|
|
118
|
+
}
|
|
119
|
+
struct Out { @builtin(position) clip: vec4f, @location(0) color: vec4f, @location(1) dash: vec3f, @location(2) distance: f32 }
|
|
120
|
+
|
|
121
|
+
@vertex fn vs_main(input: In, @builtin(vertex_index) vertex: u32) -> Out {
|
|
122
|
+
let corners = array<vec2f, 6>(vec2f(-0.5,0), vec2f(0.5,0), vec2f(-0.5,1), vec2f(-0.5,1), vec2f(0.5,0), vec2f(0.5,1));
|
|
123
|
+
let side = corners[vertex].x;
|
|
124
|
+
let along = corners[vertex].y;
|
|
125
|
+
let pathLength = length(input.end - input.start);
|
|
126
|
+
|
|
127
|
+
var out: Out;
|
|
128
|
+
out.color = mix(input.color, input.colorEnd, along);
|
|
129
|
+
// Normal-aware strokes fade when their surface normal faces away from the camera. Plain
|
|
130
|
+
// lines carry a zero normal and skip the fade, matching the two reference pipelines.
|
|
131
|
+
if (dot(input.normal, input.normal) > 0.0) {
|
|
132
|
+
let viewNormal = safe_normalize3((camera.view * vec4f(input.normal, 0.0)).xyz);
|
|
133
|
+
let viewCenter = (camera.view * vec4f(mix(input.start, input.end, 0.5), 1.0)).xyz;
|
|
134
|
+
out.color = vec4f(out.color.rgb * normal_fade(viewNormal, viewCenter), out.color.a);
|
|
135
|
+
}
|
|
136
|
+
out.dash = input.dash;
|
|
137
|
+
out.distance = along * pathLength + input.dash.z;
|
|
138
|
+
|
|
139
|
+
let clipARaw = camera.viewProjection * vec4f(input.start, 1.0);
|
|
140
|
+
let clipBRaw = camera.viewProjection * vec4f(input.end, 1.0);
|
|
141
|
+
if (clipARaw.w < CLIP_W_EPSILON && clipBRaw.w < CLIP_W_EPSILON) {
|
|
142
|
+
out.clip = vec4f(0.0, 0.0, 2.0, 1.0);
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Manual near-plane clipping, matching the reference and reference: the perspective divide
|
|
147
|
+
// below would otherwise mirror geometry that crosses behind the camera.
|
|
148
|
+
var clipA = clipARaw;
|
|
149
|
+
var clipB = clipBRaw;
|
|
150
|
+
var distanceA = 0.0;
|
|
151
|
+
var distanceB = pathLength;
|
|
152
|
+
var t = clip_near_plane_t(clipA, clipB);
|
|
153
|
+
if (t > 0.0) {
|
|
154
|
+
clipA = mix(clipA, clipB, t);
|
|
155
|
+
distanceA = mix(distanceA, distanceB, t);
|
|
156
|
+
}
|
|
157
|
+
t = clip_near_plane_t(clipB, clipA);
|
|
158
|
+
if (t > 0.0) {
|
|
159
|
+
clipB = mix(clipB, clipA, t);
|
|
160
|
+
distanceB = mix(distanceB, distanceA, t);
|
|
161
|
+
}
|
|
162
|
+
out.distance = mix(distanceA, distanceB, along) + input.dash.z;
|
|
163
|
+
|
|
164
|
+
let screenA = camera.viewportSize * (0.5 * clipA.xy / clipA.w + 0.5);
|
|
165
|
+
let screenB = camera.viewportSize * (0.5 * clipB.xy / clipB.w + 0.5);
|
|
166
|
+
let screenDelta = screenB - screenA;
|
|
167
|
+
if (dot(screenDelta, screenDelta) < SCREEN_EPSILON) {
|
|
168
|
+
out.clip = vec4f(0.0, 0.0, 2.0, 1.0);
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
var pixelWidth = max(input.width, 0.0);
|
|
173
|
+
if (input.flags == 1u) {
|
|
174
|
+
// World-space width scales by the vertical projection term at this clip depth.
|
|
175
|
+
pixelWidth = input.width * camera.viewProjection[1][1] * camera.viewportSize.y * 0.5
|
|
176
|
+
/ max(mix(clipA, clipB, along).w, 0.0001);
|
|
177
|
+
}
|
|
178
|
+
// Reference thinness fade: a sub-pixel line loses alpha instead of collapsing to a sliver.
|
|
179
|
+
if (pixelWidth > 0.0 && pixelWidth < 1.0) {
|
|
180
|
+
out.color.a *= pixelWidth;
|
|
181
|
+
pixelWidth = 1.0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let yBasis = normalize(screenDelta);
|
|
185
|
+
let xBasis = vec2f(-yBasis.y, yBasis.x);
|
|
186
|
+
let clip = mix(clipA, clipB, along);
|
|
187
|
+
let screen = mix(screenA, screenB, along) + xBasis * side * pixelWidth;
|
|
188
|
+
let ndc = screen / camera.viewportSize * 2.0 - 1.0;
|
|
189
|
+
out.clip = vec4f(ndc * clip.w, apply_depth_bias(clip), clip.w);
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@fragment fn fs_main(input: Out) -> @location(0) vec4f {
|
|
194
|
+
// The CPU side accumulates the dash offset and the arc length of earlier segments
|
|
195
|
+
// into dash.z, so distance is already the absolute on-path phase and must not have
|
|
196
|
+
// dash.z subtracted again here.
|
|
197
|
+
let visible = max(input.dash.x, 0.0);
|
|
198
|
+
let period = max(visible + max(input.dash.y, 0.0), 0.0001);
|
|
199
|
+
if (visible > 0.0 && floored_mod(input.distance, period) > visible) { discard; }
|
|
200
|
+
return input.color;
|
|
201
|
+
}
|
|
202
|
+
`;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Reference `round_joint_resolution` (3..8, default 8) and `miter_limit` (default 4.0) from
|
|
206
|
+
* `debug_draw_3d.gd`. Every reference scene keeps both at their defaults, so they are baked
|
|
207
|
+
* into the WGSL like `depth_bias` instead of turning into extra uniform fields.
|
|
208
|
+
*/
|
|
209
|
+
const ROUND_JOINT_RESOLUTION = 8;
|
|
210
|
+
const MITER_LIMIT = 4;
|
|
211
|
+
/** Round joints fan out one triangle per resolution step; bevel/miter need fewer vertices. */
|
|
212
|
+
const JOINT_VERTEX_COUNT = ROUND_JOINT_RESOLUTION * 3;
|
|
213
|
+
/**
|
|
214
|
+
* Port of `debug_line_joint_geometry_3d.gdshaderinc` plus the joint vertex/fragment stages.
|
|
215
|
+
*
|
|
216
|
+
* The reference renders a joint as its own instance per path vertex: it uploads
|
|
217
|
+
* `(previous, center, next)`, unrolls a tiny fan mesh whose `VERTEX.x` is the vertex index,
|
|
218
|
+
* and rebuilds the round/bevel/miter wedge in the vertex stage. Out-of-range vertices are
|
|
219
|
+
* parked behind the far plane, which is also how a single 24-vertex pipeline can draw all
|
|
220
|
+
* three joint kinds.
|
|
221
|
+
*/
|
|
222
|
+
function jointShader(depthBias) {
|
|
223
|
+
return `
|
|
224
|
+
struct Camera { viewProjection: mat4x4f, view: mat4x4f, viewportSize: vec2f }
|
|
225
|
+
@group(0) @binding(0) var<uniform> camera: Camera;
|
|
226
|
+
${depthBiasHelpers(depthBias)}
|
|
227
|
+
${normalFadeHelpers}
|
|
228
|
+
const CLIP_W_EPSILON: f32 = 0.0001;
|
|
229
|
+
const SCREEN_EPSILON: f32 = 0.0001;
|
|
230
|
+
const ROUND_RESOLUTION: i32 = ${ROUND_JOINT_RESOLUTION};
|
|
231
|
+
const MITER_LIMIT_VALUE: f32 = ${MITER_LIMIT.toFixed(1)};
|
|
232
|
+
const JOINT_ROUND: u32 = 1u;
|
|
233
|
+
const JOINT_BEVEL: u32 = 2u;
|
|
234
|
+
const JOINT_MITER: u32 = 3u;
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Normal blend factors across a wedge, ported from round_normal_t / bevel_normal_t /
|
|
238
|
+
* miter_normal_t. The wedge interpolates the two path normals in the same order as the
|
|
239
|
+
* reference picks its wedge vertices, so a normal-aware joint shades like the segments it
|
|
240
|
+
* joins instead of like a flat patch.
|
|
241
|
+
*/
|
|
242
|
+
fn round_normal_t(vertexIndex: i32, resolution: i32) -> f32 {
|
|
243
|
+
let triangleIndex = vertexIndex / 3;
|
|
244
|
+
let inTriangleIndex = vertexIndex - triangleIndex * 3;
|
|
245
|
+
if (inTriangleIndex == 0) {
|
|
246
|
+
return (f32(triangleIndex) + 0.5) / f32(max(resolution, 1));
|
|
247
|
+
}
|
|
248
|
+
return (f32(triangleIndex) + f32(inTriangleIndex) - 1.0) / f32(max(resolution, 1));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
fn bevel_normal_t(vertexIndex: i32) -> f32 {
|
|
252
|
+
let localIndex = vertexIndex % 3;
|
|
253
|
+
if (localIndex == 1) { return 1.0; }
|
|
254
|
+
if (localIndex == 2) { return 0.0; }
|
|
255
|
+
return 0.5;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
fn miter_normal_t(vertexIndex: i32) -> f32 {
|
|
259
|
+
let localIndex = vertexIndex % 6;
|
|
260
|
+
if (localIndex == 1) { return 0.0; }
|
|
261
|
+
if (localIndex == 2 || localIndex == 4) { return 0.5; }
|
|
262
|
+
if (localIndex == 5) { return 1.0; }
|
|
263
|
+
if (localIndex == 3) { return 0.75; }
|
|
264
|
+
return 0.25;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Reference joint-stage near-plane clip: unlike the line stage it keeps the raw
|
|
268
|
+
// intersection parameter instead of clamping it into [0, 1].
|
|
269
|
+
fn clip_near_plane(a: vec4f, b: vec4f) -> vec4f {
|
|
270
|
+
if (a.z > a.w && b.z <= b.w) {
|
|
271
|
+
let distance_a = a.z - a.w;
|
|
272
|
+
let distance_b = b.z - b.w;
|
|
273
|
+
let t = distance_a / (distance_a - distance_b) + NEAR_CLIP_EPSILON;
|
|
274
|
+
return mix(a, b, t);
|
|
275
|
+
}
|
|
276
|
+
return a;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
fn floored_mod(value: f32, period: f32) -> f32 {
|
|
280
|
+
return value - period * floor(value / period);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
fn screen_from_clip(clip: vec4f) -> vec2f {
|
|
284
|
+
return camera.viewportSize * (0.5 * clip.xy / clip.w + 0.5);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Reference safe_normalize: a degenerate screen direction collapses to zero.
|
|
288
|
+
fn safe_normalize(value: vec2f) -> vec2f {
|
|
289
|
+
let length_sq = dot(value, value);
|
|
290
|
+
if (length_sq < SCREEN_EPSILON) {
|
|
291
|
+
return vec2f(0.0, 0.0);
|
|
292
|
+
}
|
|
293
|
+
return value * inverseSqrt(length_sq);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Reference limit_vector: clamps the miter tip to max_length.
|
|
297
|
+
fn limit_vector(value: vec2f, max_length: f32) -> vec2f {
|
|
298
|
+
let length_sq = dot(value, value);
|
|
299
|
+
let max_length_sq = max_length * max_length;
|
|
300
|
+
if (length_sq > max_length_sq && length_sq > SCREEN_EPSILON) {
|
|
301
|
+
return value * (max_length * inverseSqrt(length_sq));
|
|
302
|
+
}
|
|
303
|
+
return value;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Reference round_offset: alternating centre/rim triangles covering the ab to cb wedge.
|
|
307
|
+
fn round_offset(index: i32, width: f32, ab: vec2f, ab_norm: vec2f, cb_norm: vec2f, cb: vec2f) -> vec2f {
|
|
308
|
+
let triangle_index = index / 3;
|
|
309
|
+
let in_triangle_index = index - triangle_index * 3;
|
|
310
|
+
let radius = sign(f32(in_triangle_index)) * 0.5 * width;
|
|
311
|
+
let theta = acos(clamp(dot(ab_norm, cb_norm), -1.0, 1.0));
|
|
312
|
+
var sigma = sign(dot(ab_norm, cb));
|
|
313
|
+
if (sigma == 0.0) {
|
|
314
|
+
sigma = 1.0;
|
|
315
|
+
}
|
|
316
|
+
let angle = theta * f32(triangle_index + in_triangle_index - 1) / f32(max(ROUND_RESOLUTION, 1));
|
|
317
|
+
let position_x = sigma * radius * cos(angle);
|
|
318
|
+
let position_y = radius * sin(angle);
|
|
319
|
+
return position_x * ab_norm + position_y * ab;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Reference bevel_offset: one triangle across the outer corners of both segments.
|
|
323
|
+
fn bevel_offset(index: i32, width: f32, ab: vec2f, cb: vec2f, ab_norm: vec2f, cb_norm: vec2f) -> vec2f {
|
|
324
|
+
let local_index = index % 3;
|
|
325
|
+
let tangent = safe_normalize(ab - cb);
|
|
326
|
+
let normal = vec2f(-tangent.y, tangent.x);
|
|
327
|
+
var sigma = sign(dot(ab + cb, normal));
|
|
328
|
+
if (sigma == 0.0) {
|
|
329
|
+
sigma = 1.0;
|
|
330
|
+
}
|
|
331
|
+
let p0 = width * sigma * ab_norm;
|
|
332
|
+
let p1 = width * sigma * cb_norm;
|
|
333
|
+
if (local_index == 1) {
|
|
334
|
+
return 0.5 * p1;
|
|
335
|
+
}
|
|
336
|
+
if (local_index == 2) {
|
|
337
|
+
return 0.5 * p0;
|
|
338
|
+
}
|
|
339
|
+
return vec2f(0.0, 0.0);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Reference miter_offset: two triangles reaching the clamped miter tip.
|
|
343
|
+
fn miter_offset(index: i32, width: f32, ab: vec2f, cb: vec2f, ab_norm: vec2f, cb_norm: vec2f) -> vec2f {
|
|
344
|
+
let local_index = index % 6;
|
|
345
|
+
let tangent = safe_normalize(ab - cb);
|
|
346
|
+
let normal = vec2f(-tangent.y, tangent.x);
|
|
347
|
+
var sigma = sign(dot(ab + cb, normal));
|
|
348
|
+
if (sigma == 0.0) {
|
|
349
|
+
sigma = 1.0;
|
|
350
|
+
}
|
|
351
|
+
let denominator = dot(normal, ab_norm);
|
|
352
|
+
if (abs(denominator) < SCREEN_EPSILON) {
|
|
353
|
+
return bevel_offset(index, width, ab, cb, ab_norm, cb_norm);
|
|
354
|
+
}
|
|
355
|
+
let p0 = width * sigma * ab_norm;
|
|
356
|
+
let p1 = limit_vector(width * sigma * normal / denominator, width * max(MITER_LIMIT_VALUE, 1.0));
|
|
357
|
+
let p2 = width * sigma * cb_norm;
|
|
358
|
+
if (local_index == 1) {
|
|
359
|
+
return 0.5 * p0;
|
|
360
|
+
}
|
|
361
|
+
if (local_index == 2 || local_index == 4) {
|
|
362
|
+
return 0.5 * p1;
|
|
363
|
+
}
|
|
364
|
+
if (local_index == 5) {
|
|
365
|
+
return 0.5 * p2;
|
|
366
|
+
}
|
|
367
|
+
return vec2f(0.0, 0.0);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
struct In {
|
|
371
|
+
@location(0) position: vec3f,
|
|
372
|
+
@location(1) world0: vec4f, @location(2) world1: vec4f,
|
|
373
|
+
@location(3) world2: vec4f, @location(4) world3: vec4f,
|
|
374
|
+
@location(5) previous: vec3f, @location(6) center: vec3f, @location(7) next: vec3f,
|
|
375
|
+
@location(8) color: vec4f, @location(9) width: f32, @location(10) flags: u32,
|
|
376
|
+
@location(11) dash: vec3f, @location(12) joint: u32,
|
|
377
|
+
@location(13) previousNormal: vec3f, @location(14) nextNormal: vec3f,
|
|
378
|
+
}
|
|
379
|
+
struct Out { @builtin(position) clip: vec4f, @location(0) color: vec4f, @location(1) dash: vec3f }
|
|
380
|
+
|
|
381
|
+
@vertex fn vs_main(input: In, @builtin(vertex_index) vertex: u32) -> Out {
|
|
382
|
+
var out: Out;
|
|
383
|
+
out.color = input.color;
|
|
384
|
+
out.dash = input.dash;
|
|
385
|
+
|
|
386
|
+
let clip_a_raw = camera.viewProjection * vec4f(input.previous, 1.0);
|
|
387
|
+
let clip_b_raw = camera.viewProjection * vec4f(input.center, 1.0);
|
|
388
|
+
let clip_c_raw = camera.viewProjection * vec4f(input.next, 1.0);
|
|
389
|
+
if (clip_a_raw.w < CLIP_W_EPSILON && clip_b_raw.w < CLIP_W_EPSILON && clip_c_raw.w < CLIP_W_EPSILON) {
|
|
390
|
+
out.clip = vec4f(0.0, 0.0, 2.0, 1.0);
|
|
391
|
+
return out;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Manual near-plane clipping, matching the reference: the wedge is rebuilt from the
|
|
395
|
+
// clipped neighbours so joints stay anchored when a segment crosses the near plane.
|
|
396
|
+
var clip_a = clip_near_plane(clip_a_raw, clip_c_raw);
|
|
397
|
+
var clip_b = clip_near_plane(clip_b_raw, clip_a);
|
|
398
|
+
var clip_c = clip_near_plane(clip_c_raw, clip_b);
|
|
399
|
+
clip_a = clip_near_plane(clip_a, clip_c);
|
|
400
|
+
|
|
401
|
+
let screen_a = screen_from_clip(clip_a);
|
|
402
|
+
let screen_b = screen_from_clip(clip_b);
|
|
403
|
+
let screen_c = screen_from_clip(clip_c);
|
|
404
|
+
let ab = safe_normalize(screen_b - screen_a);
|
|
405
|
+
let cb = safe_normalize(screen_b - screen_c);
|
|
406
|
+
if (dot(ab, ab) < SCREEN_EPSILON || dot(cb, cb) < SCREEN_EPSILON) {
|
|
407
|
+
out.clip = vec4f(0.0, 0.0, 2.0, 1.0);
|
|
408
|
+
return out;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
var width = max(input.width, 0.0);
|
|
412
|
+
if (input.flags == 1u) {
|
|
413
|
+
// Reference resolve_joint_width measures one world unit along the camera's right axis
|
|
414
|
+
// and converts it to pixels: that equals the vertical projection term used by the line
|
|
415
|
+
// shader, so joint and segment widths stay identical.
|
|
416
|
+
width = width * camera.viewProjection[1][1] * camera.viewportSize.y * 0.5 / max(clip_b.w, 0.0001);
|
|
417
|
+
} else if (width > 0.0 && width < 1.0) {
|
|
418
|
+
// Reference thinness fade: sub-pixel strokes lose alpha instead of collapsing to a sliver.
|
|
419
|
+
out.color.a *= width;
|
|
420
|
+
width = 1.0;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
let index = i32(vertex);
|
|
424
|
+
let ab_norm = vec2f(-ab.y, ab.x);
|
|
425
|
+
let cb_norm = vec2f(cb.y, -cb.x);
|
|
426
|
+
var offset = vec2f(0.0, 0.0);
|
|
427
|
+
var normalT = 0.0;
|
|
428
|
+
var rendered = false;
|
|
429
|
+
if (input.joint == JOINT_ROUND) {
|
|
430
|
+
if (index < ROUND_RESOLUTION * 3) {
|
|
431
|
+
offset = round_offset(index, width, ab, ab_norm, cb_norm, cb);
|
|
432
|
+
normalT = round_normal_t(index, ROUND_RESOLUTION);
|
|
433
|
+
rendered = true;
|
|
434
|
+
}
|
|
435
|
+
} else if (input.joint == JOINT_BEVEL) {
|
|
436
|
+
if (index < 3) {
|
|
437
|
+
offset = bevel_offset(index, width, ab, cb, ab_norm, cb_norm);
|
|
438
|
+
normalT = bevel_normal_t(index);
|
|
439
|
+
rendered = true;
|
|
440
|
+
}
|
|
441
|
+
} else if (input.joint == JOINT_MITER) {
|
|
442
|
+
if (index < 6) {
|
|
443
|
+
offset = miter_offset(index, width, ab, cb, ab_norm, cb_norm);
|
|
444
|
+
normalT = miter_normal_t(index);
|
|
445
|
+
rendered = true;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (!rendered) {
|
|
449
|
+
// Unused vertices of the shared fan mesh are parked behind the far plane.
|
|
450
|
+
out.clip = vec4f(0.0, 0.0, 2.0, 1.0);
|
|
451
|
+
return out;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Normal-aware joints (_n(joint)): blend the two path normals across the wedge and apply
|
|
455
|
+
// the same back-face fade as the segments, which is the only difference from a plain joint.
|
|
456
|
+
if (dot(input.previousNormal, input.previousNormal) > 0.0 || dot(input.nextNormal, input.nextNormal) > 0.0) {
|
|
457
|
+
let blended = safe_normalize3(mix(resolve_normal(input.previousNormal), resolve_normal(input.nextNormal), clamp(normalT, 0.0, 1.0)));
|
|
458
|
+
let viewNormal = safe_normalize3((camera.view * vec4f(blended, 0.0)).xyz);
|
|
459
|
+
let viewPosition = (camera.view * vec4f(input.center, 1.0)).xyz;
|
|
460
|
+
out.color = vec4f(out.color.rgb * normal_fade(viewNormal, viewPosition), out.color.a);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
let screen = screen_b + offset;
|
|
464
|
+
let ndc = screen / camera.viewportSize * 2.0 - 1.0;
|
|
465
|
+
out.clip = vec4f(ndc * clip_b.w, apply_depth_bias(clip_b), clip_b.w);
|
|
466
|
+
return out;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
@fragment fn fs_main(input: Out) -> @location(0) vec4f {
|
|
470
|
+
// CPU folds the accumulated path distance and the dash offset into dash.z, so this is the
|
|
471
|
+
// on-path phase at the vertex, matching the phase the adjoining segments render with.
|
|
472
|
+
let visible = max(input.dash.x, 0.0);
|
|
473
|
+
let period = max(visible + max(input.dash.y, 0.0), 0.0001);
|
|
474
|
+
if (visible > 0.0 && floored_mod(input.dash.z, period) > visible) { discard; }
|
|
475
|
+
return input.color;
|
|
476
|
+
}
|
|
477
|
+
`;
|
|
478
|
+
}
|
|
479
|
+
function triangleShader(depthBias) {
|
|
480
|
+
return `
|
|
481
|
+
struct Camera { viewProjection: mat4x4f }
|
|
482
|
+
@group(0) @binding(0) var<uniform> camera: Camera;
|
|
483
|
+
${depthBiasHelpers(depthBias)}
|
|
484
|
+
struct In {
|
|
485
|
+
@location(0) position: vec3f,
|
|
486
|
+
@location(1) world0: vec4f, @location(2) world1: vec4f,
|
|
487
|
+
@location(3) world2: vec4f, @location(4) world3: vec4f,
|
|
488
|
+
@location(5) a: vec3f, @location(6) b: vec3f, @location(7) c: vec3f,
|
|
489
|
+
@location(8) color: vec4f,
|
|
490
|
+
}
|
|
491
|
+
struct Out { @builtin(position) clip: vec4f, @location(0) color: vec4f }
|
|
492
|
+
@vertex fn vs_main(input: In, @builtin(vertex_index) vertex: u32) -> Out {
|
|
493
|
+
var p = input.a;
|
|
494
|
+
if (vertex == 1u) { p = input.b; } else if (vertex == 2u) { p = input.c; }
|
|
495
|
+
let clip = camera.viewProjection * vec4f(p, 1.0);
|
|
496
|
+
var out: Out;
|
|
497
|
+
out.clip = vec4f(clip.xy, apply_depth_bias(clip), clip.w);
|
|
498
|
+
out.color = input.color;
|
|
499
|
+
return out;
|
|
500
|
+
}
|
|
501
|
+
@fragment fn fs_main(input: Out) -> @location(0) vec4f { return input.color; }
|
|
502
|
+
`;
|
|
503
|
+
}
|
|
504
|
+
function sameVec3(a, b) {
|
|
505
|
+
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
|
|
506
|
+
}
|
|
507
|
+
function sameColor(a, b) {
|
|
508
|
+
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3];
|
|
509
|
+
}
|
|
510
|
+
function samePoint(a, b) {
|
|
511
|
+
return sameVec3(a.position, b.position) && sameColor(a.color, b.color) && a.size === b.size && a.sizeMode === b.sizeMode && a.round === b.round;
|
|
512
|
+
}
|
|
513
|
+
function sameLine(a, b) {
|
|
514
|
+
return sameVec3(a.start, b.start) && sameVec3(a.end, b.end) && sameColor(a.color, b.color) && sameColor(a.colorEnd, b.colorEnd)
|
|
515
|
+
&& a.width === b.width && a.widthMode === b.widthMode && a.dash?.[0] === b.dash?.[0] && a.dash?.[1] === b.dash?.[1] && a.dash?.[2] === b.dash?.[2]
|
|
516
|
+
&& (a.normal === undefined ? b.normal === undefined : b.normal !== undefined && sameVec3(a.normal, b.normal)) && a.joint === b.joint;
|
|
517
|
+
}
|
|
518
|
+
function sameTriangle(a, b) {
|
|
519
|
+
return sameVec3(a.a, b.a) && sameVec3(a.b, b.b) && sameVec3(a.c, b.c) && sameColor(a.color, b.color);
|
|
520
|
+
}
|
|
521
|
+
function sameJoint(a, b) {
|
|
522
|
+
return sameVec3(a.previous, b.previous) && sameVec3(a.center, b.center) && sameVec3(a.next, b.next)
|
|
523
|
+
&& sameColor(a.color, b.color) && a.width === b.width && a.widthMode === b.widthMode && a.joint === b.joint
|
|
524
|
+
&& a.dash?.[0] === b.dash?.[0] && a.dash?.[1] === b.dash?.[1] && a.dash?.[2] === b.dash?.[2]
|
|
525
|
+
&& (a.previousNormal === undefined ? b.previousNormal === undefined : b.previousNormal !== undefined && sameVec3(a.previousNormal, b.previousNormal))
|
|
526
|
+
&& (a.nextNormal === undefined ? b.nextNormal === undefined : b.nextNormal !== undefined && sameVec3(a.nextNormal, b.nextNormal));
|
|
527
|
+
}
|
|
528
|
+
function triangleGeometry(gpu) {
|
|
529
|
+
return geometry(gpu, {
|
|
530
|
+
buffers: [{ attributes: { position: "float32x3" }, data: new Float32Array(9) }],
|
|
531
|
+
vertexCount: 3,
|
|
532
|
+
topology: "triangle-list",
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Joints are placed entirely from `@builtin(vertex_index)`, so the mesh only has to supply a
|
|
537
|
+
* vertex count: the reference does the same with a fan mesh whose `VERTEX.x` is the index.
|
|
538
|
+
*/
|
|
539
|
+
function jointGeometry(gpu) {
|
|
540
|
+
return geometry(gpu, {
|
|
541
|
+
buffers: [{ attributes: { position: "float32x3" }, data: new Float32Array(JOINT_VERTEX_COUNT * 3) }],
|
|
542
|
+
vertexCount: JOINT_VERTEX_COUNT,
|
|
543
|
+
topology: "triangle-list",
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
export class GpuDebugDraw3D {
|
|
547
|
+
#gpu;
|
|
548
|
+
#depthBias;
|
|
549
|
+
#textOptions;
|
|
550
|
+
#defaultTextSource;
|
|
551
|
+
#texts = new Map();
|
|
552
|
+
#points;
|
|
553
|
+
#lines;
|
|
554
|
+
#triangles;
|
|
555
|
+
#pointIds = [];
|
|
556
|
+
#lineIds = [];
|
|
557
|
+
#triangleIds = [];
|
|
558
|
+
#pointCache = [];
|
|
559
|
+
#lineCache = [];
|
|
560
|
+
#triangleCache = [];
|
|
561
|
+
#pointBridge;
|
|
562
|
+
#lineBridge;
|
|
563
|
+
#triangleBridge;
|
|
564
|
+
#pointDraw;
|
|
565
|
+
#lineDraw;
|
|
566
|
+
#triangleDraw;
|
|
567
|
+
#pointIndirect;
|
|
568
|
+
#lineIndirect;
|
|
569
|
+
#triangleIndirect;
|
|
570
|
+
#indirectCompute;
|
|
571
|
+
#d2;
|
|
572
|
+
#viewProjection = identity();
|
|
573
|
+
/** World-to-view matrix, needed by the reference back-face fade on normal-aware strokes. */
|
|
574
|
+
#view = identity();
|
|
575
|
+
/**
|
|
576
|
+
* Last target size seen in `draw`, in canvas pixels. Text preparation runs a pass earlier and
|
|
577
|
+
* needs it: a world block's screen size — and with it the raster rung its glyphs land on — depends
|
|
578
|
+
* on the viewport, and on the first frame that rung is only worth picking conservatively.
|
|
579
|
+
*/
|
|
580
|
+
#viewportSize = [0, 0];
|
|
581
|
+
#pointCount = 0;
|
|
582
|
+
#lineCount = 0;
|
|
583
|
+
#triangleCount = 0;
|
|
584
|
+
#pointDirty = true;
|
|
585
|
+
#lineDirty = true;
|
|
586
|
+
#triangleDirty = true;
|
|
587
|
+
#joints;
|
|
588
|
+
#jointIds = [];
|
|
589
|
+
#jointCache = [];
|
|
590
|
+
#jointBridge;
|
|
591
|
+
#jointDraw;
|
|
592
|
+
#jointIndirect;
|
|
593
|
+
#jointCount = 0;
|
|
594
|
+
#jointDirty = true;
|
|
595
|
+
constructor(gpu, capacities = {}) {
|
|
596
|
+
const points = capacities.points ?? 4096;
|
|
597
|
+
const lines = capacities.lines ?? 16384;
|
|
598
|
+
const triangles = capacities.triangles ?? 8192;
|
|
599
|
+
const joints = capacities.joints ?? 8192;
|
|
600
|
+
const depthBias = capacities.depthBias ?? DEFAULT_DEPTH_BIAS;
|
|
601
|
+
this.#gpu = gpu;
|
|
602
|
+
this.#depthBias = depthBias;
|
|
603
|
+
this.#textOptions = capacities.text;
|
|
604
|
+
if (this.#textOptions !== undefined) {
|
|
605
|
+
const names = Object.keys(this.#textOptions.sources);
|
|
606
|
+
if (names.length === 0) {
|
|
607
|
+
throw new Error("text.sources must register at least one glyph source");
|
|
608
|
+
}
|
|
609
|
+
this.#defaultTextSource = names[0];
|
|
610
|
+
}
|
|
611
|
+
this.#points = instances({ capacity: points, attributes: { point: "float32x3", color: "float32x4", size: "float32", flags: "uint32" } });
|
|
612
|
+
this.#lines = instances({ capacity: lines, attributes: { start: "float32x3", end: "float32x3", color: "float32x4", colorEnd: "float32x4", width: "float32", flags: "uint32", dash: "float32x3", normal: "float32x3", joint: "uint32" } });
|
|
613
|
+
this.#triangles = instances({ capacity: triangles, attributes: { a: "float32x3", b: "float32x3", c: "float32x3", color: "float32x4" } });
|
|
614
|
+
this.#joints = instances({ capacity: joints, attributes: { previous: "float32x3", center: "float32x3", next: "float32x3", color: "float32x4", width: "float32", flags: "uint32", dash: "float32x3", joint: "uint32", previousNormal: "float32x3", nextNormal: "float32x3" } });
|
|
615
|
+
const pointMesh = zeroGeometry(gpu);
|
|
616
|
+
const lineMesh = zeroGeometry(gpu);
|
|
617
|
+
const triangleMesh = triangleGeometry(gpu);
|
|
618
|
+
const jointMesh = jointGeometry(gpu);
|
|
619
|
+
this.#pointBridge = instanceGeometry(gpu, this.#points, { mesh: pointMesh });
|
|
620
|
+
this.#lineBridge = instanceGeometry(gpu, this.#lines, { mesh: lineMesh });
|
|
621
|
+
this.#triangleBridge = instanceGeometry(gpu, this.#triangles, { mesh: triangleMesh });
|
|
622
|
+
this.#jointBridge = instanceGeometry(gpu, this.#joints, { mesh: jointMesh });
|
|
623
|
+
// Reference render modes: every line-like primitive uses `depth_draw_always` and rects
|
|
624
|
+
// disable culling (`debug_rect_3d.gdshader`), so strokes and filled planes both test and
|
|
625
|
+
// write depth and are two-sided. Closed solids use `cull_back` in the reference, but that
|
|
626
|
+
// needs a separate pipeline per cull mode; classifying them is tracked as follow-up work.
|
|
627
|
+
this.#pointDraw = draw(gpu, { shader: prepareShader(pointShader(depthBias)), geometry: this.#pointBridge.geometry, instances: 0, blend: "alpha", cull: "none", depth: { write: true, compare: "less-equal" } });
|
|
628
|
+
this.#lineDraw = draw(gpu, { shader: prepareShader(lineShader(depthBias)), geometry: this.#lineBridge.geometry, instances: 0, blend: "alpha", cull: "none", depth: { write: true, compare: "less-equal" } });
|
|
629
|
+
this.#triangleDraw = draw(gpu, { shader: prepareShader(triangleShader(depthBias)), geometry: this.#triangleBridge.geometry, instances: 0, blend: "alpha", cull: "none", depth: { write: true, compare: "less-equal" } });
|
|
630
|
+
this.#jointDraw = draw(gpu, { shader: prepareShader(jointShader(depthBias)), geometry: this.#jointBridge.geometry, instances: 0, blend: "alpha", cull: "none", depth: { write: true, compare: "less-equal" } });
|
|
631
|
+
this.#pointIndirect = storage(gpu, 16, { access: "read-write", indirect: true });
|
|
632
|
+
this.#lineIndirect = storage(gpu, 16, { access: "read-write", indirect: true });
|
|
633
|
+
this.#triangleIndirect = storage(gpu, 16, { access: "read-write", indirect: true });
|
|
634
|
+
this.#jointIndirect = storage(gpu, 16, { access: "read-write", indirect: true });
|
|
635
|
+
this.#indirectCompute = compute(gpu, INDIRECT_SHADER);
|
|
636
|
+
indirectArgs(this.#pointIndirect, 6, 0);
|
|
637
|
+
indirectArgs(this.#lineIndirect, 6, 0);
|
|
638
|
+
indirectArgs(this.#triangleIndirect, 3, 0);
|
|
639
|
+
indirectArgs(this.#jointIndirect, JOINT_VERTEX_COUNT, 0);
|
|
640
|
+
// The 2D layer draws the same primitives under another projection, so its shader sources are
|
|
641
|
+
// handed over here rather than imported back from this module.
|
|
642
|
+
this.#d2 = new GpuDebugDraw2D(gpu, {
|
|
643
|
+
shaders: { point: pointShader(depthBias), line: lineShader(depthBias), joint: jointShader(depthBias) },
|
|
644
|
+
jointVertexCount: JOINT_VERTEX_COUNT,
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
prepare(commands, camera, camera2d) {
|
|
648
|
+
let pointCount = 0;
|
|
649
|
+
let lineCount = 0;
|
|
650
|
+
let triangleCount = 0;
|
|
651
|
+
let jointCount = 0;
|
|
652
|
+
const textCommands = [];
|
|
653
|
+
const commands2d = [];
|
|
654
|
+
this.#pointDirty = false;
|
|
655
|
+
this.#lineDirty = false;
|
|
656
|
+
this.#triangleDirty = false;
|
|
657
|
+
this.#jointDirty = false;
|
|
658
|
+
if (camera2d !== undefined)
|
|
659
|
+
this.#d2.setCamera(camera2d);
|
|
660
|
+
for (const command of commands) {
|
|
661
|
+
if (command.kind === "point")
|
|
662
|
+
this.addPoint(command, pointCount++);
|
|
663
|
+
else if (command.kind === "triangle")
|
|
664
|
+
this.addTriangle(command, triangleCount++);
|
|
665
|
+
else if (command.kind === "joint")
|
|
666
|
+
this.addJoint(command, jointCount++);
|
|
667
|
+
else if (command.kind === "text")
|
|
668
|
+
textCommands.push(command);
|
|
669
|
+
else if (command.kind === "line")
|
|
670
|
+
this.addLine(command, lineCount++);
|
|
671
|
+
else
|
|
672
|
+
commands2d.push(command);
|
|
673
|
+
}
|
|
674
|
+
if (trim(this.#points, this.#pointIds, pointCount))
|
|
675
|
+
this.#pointDirty = true;
|
|
676
|
+
if (trim(this.#lines, this.#lineIds, lineCount))
|
|
677
|
+
this.#lineDirty = true;
|
|
678
|
+
if (trim(this.#triangles, this.#triangleIds, triangleCount))
|
|
679
|
+
this.#triangleDirty = true;
|
|
680
|
+
if (trim(this.#joints, this.#jointIds, jointCount))
|
|
681
|
+
this.#jointDirty = true;
|
|
682
|
+
if (this.#pointCache.length !== pointCount)
|
|
683
|
+
this.#pointDirty = true;
|
|
684
|
+
if (this.#lineCache.length !== lineCount)
|
|
685
|
+
this.#lineDirty = true;
|
|
686
|
+
if (this.#triangleCache.length !== triangleCount)
|
|
687
|
+
this.#triangleDirty = true;
|
|
688
|
+
if (this.#jointCache.length !== jointCount)
|
|
689
|
+
this.#jointDirty = true;
|
|
690
|
+
this.#pointCache.length = pointCount;
|
|
691
|
+
this.#lineCache.length = lineCount;
|
|
692
|
+
this.#triangleCache.length = triangleCount;
|
|
693
|
+
this.#jointCache.length = jointCount;
|
|
694
|
+
if (this.#pointDirty) {
|
|
695
|
+
this.#pointCount = this.#pointBridge.publish();
|
|
696
|
+
indirectArgs(this.#pointIndirect, 6, this.#pointCount);
|
|
697
|
+
}
|
|
698
|
+
if (this.#lineDirty) {
|
|
699
|
+
this.#lineCount = this.#lineBridge.publish();
|
|
700
|
+
indirectArgs(this.#lineIndirect, 6, this.#lineCount);
|
|
701
|
+
}
|
|
702
|
+
if (this.#triangleDirty) {
|
|
703
|
+
this.#triangleCount = this.#triangleBridge.publish();
|
|
704
|
+
indirectArgs(this.#triangleIndirect, 3, this.#triangleCount);
|
|
705
|
+
}
|
|
706
|
+
if (this.#jointDirty) {
|
|
707
|
+
this.#jointCount = this.#jointBridge.publish();
|
|
708
|
+
indirectArgs(this.#jointIndirect, JOINT_VERTEX_COUNT, this.#jointCount);
|
|
709
|
+
}
|
|
710
|
+
this.#viewProjection = camera.viewProjection;
|
|
711
|
+
this.#view = camera.view;
|
|
712
|
+
// Every renderer that exists is prepared, even with an empty list: that is what drops the
|
|
713
|
+
// previous frame's glyphs instead of leaving them on screen.
|
|
714
|
+
if (textCommands.length > 0 || this.#texts.size > 0) {
|
|
715
|
+
this.#prepareText(textCommands, camera);
|
|
716
|
+
}
|
|
717
|
+
// Always prepared, even with an empty list: that is what drops the previous frame's 2D
|
|
718
|
+
// primitives instead of leaving them on screen.
|
|
719
|
+
this.#d2.prepare(commands2d);
|
|
720
|
+
}
|
|
721
|
+
/** Splits the frame's text by source name and hands each group to its renderer. */
|
|
722
|
+
#prepareText(commands, camera) {
|
|
723
|
+
const groups = new Map();
|
|
724
|
+
for (const command of commands) {
|
|
725
|
+
const name = command.source ?? this.#defaultTextSource;
|
|
726
|
+
if (name === undefined) {
|
|
727
|
+
throw new Error("text commands need a glyph source: pass `text: { sources }` to the GpuDebugDraw3D constructor");
|
|
728
|
+
}
|
|
729
|
+
const group = groups.get(name);
|
|
730
|
+
if (group === undefined)
|
|
731
|
+
groups.set(name, [command]);
|
|
732
|
+
else
|
|
733
|
+
group.push(command);
|
|
734
|
+
}
|
|
735
|
+
for (const name of new Set([...this.#texts.keys(), ...groups.keys()])) {
|
|
736
|
+
this.#textRenderer(name).prepare(groups.get(name) ?? [], { ...camera, viewportSize: this.#viewportSize });
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
#textRenderer(name) {
|
|
740
|
+
const existing = this.#texts.get(name);
|
|
741
|
+
if (existing !== undefined)
|
|
742
|
+
return existing;
|
|
743
|
+
const source = this.#textOptions?.sources[name];
|
|
744
|
+
if (source === undefined) {
|
|
745
|
+
throw new Error(`text source "${name}" is not registered`);
|
|
746
|
+
}
|
|
747
|
+
const options = this.#textOptions;
|
|
748
|
+
const renderer = new GpuText3D(this.#gpu, {
|
|
749
|
+
font: source,
|
|
750
|
+
depthBias: this.#depthBias,
|
|
751
|
+
...(options.capacity === undefined ? {} : { capacity: options.capacity }),
|
|
752
|
+
...(options.atlasSize === undefined ? {} : { atlasSize: options.atlasSize }),
|
|
753
|
+
...(options.mipLevels === undefined ? {} : { mipLevels: options.mipLevels }),
|
|
754
|
+
...(options.pages === undefined ? {} : { pages: options.pages }),
|
|
755
|
+
...(options.onFull === undefined ? {} : { onFull: options.onFull }),
|
|
756
|
+
});
|
|
757
|
+
this.#texts.set(name, renderer);
|
|
758
|
+
return renderer;
|
|
759
|
+
}
|
|
760
|
+
updateIndirect(frame) {
|
|
761
|
+
frame.computePass((pass) => {
|
|
762
|
+
this.#indirectCompute.set({ params: { vertexCount: 6, instanceCount: this.#pointCount }, args: this.#pointIndirect });
|
|
763
|
+
pass.dispatch(this.#indirectCompute, 1);
|
|
764
|
+
this.#indirectCompute.set({ params: { vertexCount: 6, instanceCount: this.#lineCount }, args: this.#lineIndirect });
|
|
765
|
+
pass.dispatch(this.#indirectCompute, 1);
|
|
766
|
+
this.#indirectCompute.set({ params: { vertexCount: 3, instanceCount: this.#triangleCount }, args: this.#triangleIndirect });
|
|
767
|
+
pass.dispatch(this.#indirectCompute, 1);
|
|
768
|
+
this.#indirectCompute.set({ params: { vertexCount: JOINT_VERTEX_COUNT, instanceCount: this.#jointCount }, args: this.#jointIndirect });
|
|
769
|
+
pass.dispatch(this.#indirectCompute, 1);
|
|
770
|
+
this.#d2.dispatchIndirect(pass);
|
|
771
|
+
for (const renderer of this.#texts.values())
|
|
772
|
+
renderer.dispatchIndirect(pass);
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
draw(pass) {
|
|
776
|
+
this.#viewportSize = pass.target.size;
|
|
777
|
+
const camera = { viewProjection: this.#viewProjection, viewportSize: pass.target.size };
|
|
778
|
+
const normalCamera = { viewProjection: this.#viewProjection, view: this.#view, viewportSize: pass.target.size };
|
|
779
|
+
this.#pointDraw.set({ camera });
|
|
780
|
+
this.#lineDraw.set({ camera: normalCamera });
|
|
781
|
+
this.#jointDraw.set({ camera: normalCamera });
|
|
782
|
+
this.#triangleDraw.set({ camera: { viewProjection: this.#viewProjection } });
|
|
783
|
+
// Reference render order is line (priority 0) then joints (priority 1) then solids (2);
|
|
784
|
+
// joints must blend after the segments they join. Solids go first here so they occupy
|
|
785
|
+
// their final depth slot before the strokes draw.
|
|
786
|
+
if (this.#triangleCount > 0)
|
|
787
|
+
pass.draw(this.#triangleDraw, { indirect: this.#triangleIndirect });
|
|
788
|
+
if (this.#pointCount > 0)
|
|
789
|
+
pass.draw(this.#pointDraw, { indirect: this.#pointIndirect });
|
|
790
|
+
if (this.#lineCount > 0)
|
|
791
|
+
pass.draw(this.#lineDraw, { indirect: this.#lineIndirect });
|
|
792
|
+
if (this.#jointCount > 0)
|
|
793
|
+
pass.draw(this.#jointDraw, { indirect: this.#jointIndirect });
|
|
794
|
+
// The 2D layer is an overlay: it draws after every 3D primitive and before text, so a 2D
|
|
795
|
+
// HUD covers the 3D world while screen-space labels stay on top of both.
|
|
796
|
+
this.#d2.draw(pass);
|
|
797
|
+
// Text is last: its alpha-blended quads read the strokes and solids as they were left behind.
|
|
798
|
+
for (const renderer of this.#texts.values())
|
|
799
|
+
renderer.draw(pass);
|
|
800
|
+
}
|
|
801
|
+
destroy() {
|
|
802
|
+
this.#pointDraw.dispose();
|
|
803
|
+
this.#lineDraw.dispose();
|
|
804
|
+
this.#triangleDraw.dispose();
|
|
805
|
+
this.#jointDraw.dispose();
|
|
806
|
+
this.#indirectCompute.dispose();
|
|
807
|
+
this.#d2.destroy();
|
|
808
|
+
this.#pointBridge.destroy();
|
|
809
|
+
this.#lineBridge.destroy();
|
|
810
|
+
this.#triangleBridge.destroy();
|
|
811
|
+
this.#jointBridge.destroy();
|
|
812
|
+
for (const renderer of this.#texts.values())
|
|
813
|
+
renderer.destroy();
|
|
814
|
+
this.#texts.clear();
|
|
815
|
+
}
|
|
816
|
+
addTriangle(command, index) {
|
|
817
|
+
const previous = this.#triangleCache[index];
|
|
818
|
+
this.#triangleCache[index] = command;
|
|
819
|
+
if (previous !== undefined && sameTriangle(previous, command))
|
|
820
|
+
return;
|
|
821
|
+
this.#triangleDirty = true;
|
|
822
|
+
const id = this.#triangleIds[index] ?? this.#addTriangleId(index);
|
|
823
|
+
this.#triangles.set(id, { a: command.a, b: command.b, c: command.c, color: command.color });
|
|
824
|
+
}
|
|
825
|
+
#addTriangleId(index) {
|
|
826
|
+
const id = this.#triangles.add({ a: [0, 0, 0], b: [0, 0, 0], c: [0, 0, 0], color: [0, 0, 0, 0] });
|
|
827
|
+
world(this.#triangles, id);
|
|
828
|
+
this.#triangleIds[index] = id;
|
|
829
|
+
return id;
|
|
830
|
+
}
|
|
831
|
+
addPoint(command, index) {
|
|
832
|
+
const previous = this.#pointCache[index];
|
|
833
|
+
this.#pointCache[index] = command;
|
|
834
|
+
if (previous !== undefined && samePoint(previous, command))
|
|
835
|
+
return;
|
|
836
|
+
this.#pointDirty = true;
|
|
837
|
+
const id = this.#pointIds[index] ?? this.#addPointId(index);
|
|
838
|
+
this.#points.set(id, { point: command.position, color: command.color, size: command.size, flags: (command.sizeMode === "world" ? 1 : 0) | (command.round ? 2 : 0) });
|
|
839
|
+
}
|
|
840
|
+
#addPointId(index) {
|
|
841
|
+
const id = this.#points.add({ point: [0, 0, 0], color: [0, 0, 0, 0], size: 0, flags: 0 });
|
|
842
|
+
world(this.#points, id);
|
|
843
|
+
this.#pointIds[index] = id;
|
|
844
|
+
return id;
|
|
845
|
+
}
|
|
846
|
+
addLine(command, index) {
|
|
847
|
+
const previous = this.#lineCache[index];
|
|
848
|
+
this.#lineCache[index] = command;
|
|
849
|
+
if (previous !== undefined && sameLine(previous, command))
|
|
850
|
+
return;
|
|
851
|
+
this.#lineDirty = true;
|
|
852
|
+
const id = this.#lineIds[index] ?? this.#addLineId(index);
|
|
853
|
+
const dash = command.dash ?? [0, 0, 0];
|
|
854
|
+
const normal = command.normal ?? [0, 0, 0];
|
|
855
|
+
const joint = command.joint === "round" ? 1 : command.joint === "bevel" ? 2 : command.joint === "miter" ? 3 : 0;
|
|
856
|
+
this.#lines.set(id, { start: command.start, end: command.end, color: command.color, colorEnd: command.colorEnd, width: command.width, flags: command.widthMode === "world" ? 1 : 0, dash, normal, joint });
|
|
857
|
+
}
|
|
858
|
+
#addLineId(index) {
|
|
859
|
+
const id = this.#lines.add({ start: [0, 0, 0], end: [0, 0, 0], color: [0, 0, 0, 0], colorEnd: [0, 0, 0, 0], width: 0, flags: 0, dash: [0, 0, 0], normal: [0, 0, 0], joint: 0 });
|
|
860
|
+
world(this.#lines, id);
|
|
861
|
+
this.#lineIds[index] = id;
|
|
862
|
+
return id;
|
|
863
|
+
}
|
|
864
|
+
addJoint(command, index) {
|
|
865
|
+
const previous = this.#jointCache[index];
|
|
866
|
+
this.#jointCache[index] = command;
|
|
867
|
+
if (previous !== undefined && sameJoint(previous, command))
|
|
868
|
+
return;
|
|
869
|
+
this.#jointDirty = true;
|
|
870
|
+
const id = this.#jointIds[index] ?? this.#addJointId(index);
|
|
871
|
+
this.#joints.set(id, {
|
|
872
|
+
previous: command.previous,
|
|
873
|
+
center: command.center,
|
|
874
|
+
next: command.next,
|
|
875
|
+
color: command.color,
|
|
876
|
+
width: command.width,
|
|
877
|
+
flags: command.widthMode === "world" ? 1 : 0,
|
|
878
|
+
dash: command.dash ?? [0, 0, 0],
|
|
879
|
+
joint: command.joint === "round" ? 1 : command.joint === "bevel" ? 2 : 3,
|
|
880
|
+
previousNormal: command.previousNormal ?? [0, 0, 0],
|
|
881
|
+
nextNormal: command.nextNormal ?? [0, 0, 0],
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
#addJointId(index) {
|
|
885
|
+
const id = this.#joints.add({ previous: [0, 0, 0], center: [0, 0, 0], next: [0, 0, 0], color: [0, 0, 0, 0], width: 0, flags: 0, dash: [0, 0, 0], joint: 0, previousNormal: [0, 0, 0], nextNormal: [0, 0, 0] });
|
|
886
|
+
world(this.#joints, id);
|
|
887
|
+
this.#jointIds[index] = id;
|
|
888
|
+
return id;
|
|
889
|
+
}
|
|
890
|
+
}
|