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,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shader-driven infinite ground grid, ported from the reference reference
|
|
3
|
+
* `the upstream reference`.
|
|
4
|
+
*
|
|
5
|
+
* The port keeps the reference algorithm and parameters:
|
|
6
|
+
* - a finite XZ quad is drawn once and faded near its bounds, so it reads as endless;
|
|
7
|
+
* - lines are computed in the fragment shader from world XZ coordinates;
|
|
8
|
+
* - three decimal LOD layers (finer / current / coarser) are combined, and their
|
|
9
|
+
* alphas are de-duplicated so overlapping layers do not stack brightness;
|
|
10
|
+
* - the LOD distance follows the reference camera-state rules from
|
|
11
|
+
* `infinite_grid_3d.gd::_update_camera_lod_distance`.
|
|
12
|
+
*
|
|
13
|
+
* Everything else (`grid_size`, `cell_size`, `line_width_pixels`, LOD ranges,
|
|
14
|
+
* center/grazing/edge/far fades, stipple discard and line colors) mirrors the
|
|
15
|
+
* reference uniforms one to one. `setOpacity` is the only demo-side extension:
|
|
16
|
+
* it scales the two reference line alphas instead of replacing them.
|
|
17
|
+
*/
|
|
18
|
+
import { draw, geometry } from "vgpu";
|
|
19
|
+
import { prepareShader } from "@vgpu/wgsl/prepare";
|
|
20
|
+
import { linearGridColor } from "./color-space.js";
|
|
21
|
+
const GRID_SHADER = prepareShader(`
|
|
22
|
+
struct GridUniforms {
|
|
23
|
+
viewProjection: mat4x4f,
|
|
24
|
+
cameraPosition: vec3f,
|
|
25
|
+
cellSize: f32,
|
|
26
|
+
viewGridCenter: vec2f,
|
|
27
|
+
gridCenter: vec2f,
|
|
28
|
+
lineWidthPixels: f32,
|
|
29
|
+
gridSize: f32,
|
|
30
|
+
edgeFadeStartRatio: f32,
|
|
31
|
+
edgeFadeEndRatio: f32,
|
|
32
|
+
minLodPower: f32,
|
|
33
|
+
maxLodPower: f32,
|
|
34
|
+
cameraLodDistance: f32,
|
|
35
|
+
cameraFarClip: f32,
|
|
36
|
+
grazingFadeStartFactor: f32,
|
|
37
|
+
grazingFadeEndFactor: f32,
|
|
38
|
+
lodCenterFadeLineCount: f32,
|
|
39
|
+
optionsA: vec4<u32>,
|
|
40
|
+
optionsB: vec4<u32>,
|
|
41
|
+
thinLineColor: vec4f,
|
|
42
|
+
thickLineColor: vec4f,
|
|
43
|
+
}
|
|
44
|
+
@group(0) @binding(0) var<uniform> grid: GridUniforms;
|
|
45
|
+
|
|
46
|
+
// optionsA: x = camera_is_orthogonal, y = debug_lod_colors, z = enable_grazing_opacity,
|
|
47
|
+
// w = enable_stipple_discard.
|
|
48
|
+
// optionsB: x = enable_lod_center_fade.
|
|
49
|
+
|
|
50
|
+
struct In { @location(0) position: vec3f }
|
|
51
|
+
struct Out {
|
|
52
|
+
@builtin(position) clip: vec4f,
|
|
53
|
+
@location(0) world: vec3f,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@vertex fn vs_main(input: In) -> Out {
|
|
57
|
+
var out: Out;
|
|
58
|
+
let world = input.position + vec3f(grid.gridCenter.x, 0.0, grid.gridCenter.y);
|
|
59
|
+
out.world = world;
|
|
60
|
+
out.clip = grid.viewProjection * vec4f(world, 1.0);
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Line coverage in pixel units so lines keep a stable screen-space width.
|
|
65
|
+
fn gridLineAlpha(uv: vec2f, currentCellSize: f32) -> f32 {
|
|
66
|
+
let coord = uv / currentCellSize;
|
|
67
|
+
let pixelSize = max(abs(dpdx(coord)) + abs(dpdy(coord)), vec2f(0.00001));
|
|
68
|
+
let distanceToLine = min(fract(coord), 1.0 - fract(coord));
|
|
69
|
+
let distancePixels = distanceToLine / pixelSize;
|
|
70
|
+
// 1 px AA transition avoids hard edges and flicker on thin lines.
|
|
71
|
+
let halfLineWidthPixels = max(grid.lineWidthPixels, 0.25) * 0.5;
|
|
72
|
+
let coverage = 1.0 - smoothstep(
|
|
73
|
+
vec2f(max(halfLineWidthPixels - 0.5, 0.0)),
|
|
74
|
+
vec2f(halfLineWidthPixels + 0.5),
|
|
75
|
+
distancePixels
|
|
76
|
+
);
|
|
77
|
+
return max(coverage.x, coverage.y);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fn lodCenterFade(uv: vec2f, currentCellSize: f32) -> f32 {
|
|
81
|
+
if (grid.optionsB.x == 0u) {
|
|
82
|
+
return 1.0;
|
|
83
|
+
}
|
|
84
|
+
let halfLineCount = max(grid.lodCenterFadeLineCount * 0.5, 1.0);
|
|
85
|
+
let snappedCenter = round(grid.viewGridCenter / currentCellSize) * currentCellSize;
|
|
86
|
+
let levelCoord = (uv - snappedCenter) / max(currentCellSize * halfLineCount, 0.00001);
|
|
87
|
+
if (grid.optionsA.x != 0u) {
|
|
88
|
+
let edge = 1.0 - min(1.0, dot(levelCoord, levelCoord));
|
|
89
|
+
return edge * edge;
|
|
90
|
+
}
|
|
91
|
+
return 1.0 - min(1.0, length(levelCoord));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@fragment fn fs_main(input: Out) -> @location(0) vec4f {
|
|
95
|
+
let uv = input.world.xz;
|
|
96
|
+
|
|
97
|
+
let safeCellSize = max(grid.cellSize, 0.00001);
|
|
98
|
+
let minPower = min(grid.minLodPower, grid.maxLodPower - 2.0);
|
|
99
|
+
let maxPower = max(grid.maxLodPower, minPower + 2.0);
|
|
100
|
+
let minLodCellSize = safeCellSize * pow(10.0, minPower);
|
|
101
|
+
let lodDistance = max(grid.cameraLodDistance, minLodCellSize);
|
|
102
|
+
let currentLodPower = clamp(
|
|
103
|
+
floor(log(lodDistance / safeCellSize) / log(10.0)),
|
|
104
|
+
minPower,
|
|
105
|
+
maxPower
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Select the current decimal LOD and fade toward the next coarser cell size.
|
|
109
|
+
let currentLodCellSize = safeCellSize * pow(10.0, currentLodPower);
|
|
110
|
+
let nextLodCellSize = currentLodCellSize * 10.0;
|
|
111
|
+
let lodFade = clamp(
|
|
112
|
+
(lodDistance - currentLodCellSize) / max(nextLodCellSize - currentLodCellSize, 0.00001),
|
|
113
|
+
0.0,
|
|
114
|
+
1.0
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Three neighboring LOD layers: fine lines fade out while major lines remain.
|
|
118
|
+
let lod0Power = clamp(currentLodPower - 1.0, minPower, maxPower);
|
|
119
|
+
let lod1Power = clamp(currentLodPower, minPower, maxPower);
|
|
120
|
+
let lod2Power = clamp(currentLodPower + 1.0, minPower, maxPower);
|
|
121
|
+
let lod0CellSize = safeCellSize * pow(10.0, lod0Power);
|
|
122
|
+
let lod1CellSize = safeCellSize * pow(10.0, lod1Power);
|
|
123
|
+
let lod2CellSize = safeCellSize * pow(10.0, lod2Power);
|
|
124
|
+
|
|
125
|
+
let lod0Alpha = gridLineAlpha(uv, lod0CellSize) * lodCenterFade(uv, lod0CellSize) * (1.0 - lodFade);
|
|
126
|
+
let lod1Alpha = gridLineAlpha(uv, lod1CellSize) * lodCenterFade(uv, lod1CellSize);
|
|
127
|
+
let lod2Alpha = gridLineAlpha(uv, lod2CellSize) * lodCenterFade(uv, lod2CellSize);
|
|
128
|
+
|
|
129
|
+
// Prevent overlapping LOD layers from stacking alpha and causing brightness jumps.
|
|
130
|
+
let lod2VisibleAlpha = lod2Alpha;
|
|
131
|
+
let lod1VisibleAlpha = lod1Alpha * (1.0 - lod2VisibleAlpha);
|
|
132
|
+
let lod0VisibleAlpha = lod0Alpha * (1.0 - lod1Alpha) * (1.0 - lod2VisibleAlpha);
|
|
133
|
+
|
|
134
|
+
// Blend minor/current/major line colors across the active LOD transition.
|
|
135
|
+
var lod0Color = grid.thinLineColor;
|
|
136
|
+
var lod1Color = mix(grid.thickLineColor, grid.thinLineColor, lodFade);
|
|
137
|
+
var lod2Color = grid.thickLineColor;
|
|
138
|
+
if (grid.optionsA.y != 0u) {
|
|
139
|
+
// Debug layers: blue = lower/finer, green = current, red = upper/coarser.
|
|
140
|
+
// Only RGB is replaced so alpha stays directly comparable with debug off.
|
|
141
|
+
lod0Color = vec4f(0.1, 0.35, 1.0, lod0Color.a);
|
|
142
|
+
lod1Color = vec4f(0.1, 1.0, 0.25, lod1Color.a);
|
|
143
|
+
lod2Color = vec4f(1.0, 0.15, 0.1, lod2Color.a);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let lod0LayerAlpha = lod0VisibleAlpha * lod0Color.a;
|
|
147
|
+
let lod1LayerAlpha = lod1VisibleAlpha * lod1Color.a;
|
|
148
|
+
let lod2LayerAlpha = lod2VisibleAlpha * lod2Color.a;
|
|
149
|
+
let lineAlpha = clamp(lod0LayerAlpha + lod1LayerAlpha + lod2LayerAlpha, 0.0, 1.0);
|
|
150
|
+
let lineColor = (
|
|
151
|
+
lod0Color.rgb * lod0LayerAlpha +
|
|
152
|
+
lod1Color.rgb * lod1LayerAlpha +
|
|
153
|
+
lod2Color.rgb * lod2LayerAlpha
|
|
154
|
+
) / max(lineAlpha, 0.00001);
|
|
155
|
+
|
|
156
|
+
// Fade out near the finite mesh bounds so the grid appears endless.
|
|
157
|
+
let halfGridSize = max(grid.gridSize * 0.5, 0.00001);
|
|
158
|
+
let normalizedGridCoord = (uv - grid.gridCenter) / halfGridSize;
|
|
159
|
+
let edgeStart = clamp(grid.edgeFadeStartRatio, 0.0, 1.0);
|
|
160
|
+
let edgeEnd = max(clamp(grid.edgeFadeEndRatio, 0.0, 1.0), edgeStart + 0.00001);
|
|
161
|
+
let edgeProgress = clamp(
|
|
162
|
+
(length(normalizedGridCoord) - edgeStart) / (edgeEnd - edgeStart),
|
|
163
|
+
0.0,
|
|
164
|
+
1.0
|
|
165
|
+
);
|
|
166
|
+
var edgeOpacity = 1.0 - edgeProgress;
|
|
167
|
+
if (grid.optionsA.x != 0u) {
|
|
168
|
+
edgeOpacity = pow(1.0 - edgeProgress * edgeProgress, 2.0);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Fade at grazing view angles to reduce distant transparent trails.
|
|
172
|
+
var grazingOpacity = 1.0;
|
|
173
|
+
if (grid.optionsA.z != 0u) {
|
|
174
|
+
let viewDir = normalize(grid.cameraPosition - input.world);
|
|
175
|
+
let viewFloorFactor = abs(dot(viewDir, vec3f(0.0, 1.0, 0.0)));
|
|
176
|
+
let grazingStart = min(grid.grazingFadeStartFactor, grid.grazingFadeEndFactor);
|
|
177
|
+
let grazingEnd = max(grid.grazingFadeStartFactor, grid.grazingFadeEndFactor);
|
|
178
|
+
var grazingProgress = step(grazingStart, viewFloorFactor);
|
|
179
|
+
if (grazingEnd - grazingStart > 0.00001) {
|
|
180
|
+
grazingProgress = clamp(
|
|
181
|
+
(viewFloorFactor - grazingStart) / (grazingEnd - grazingStart),
|
|
182
|
+
0.0,
|
|
183
|
+
1.0
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
grazingOpacity = smoothstep(0.0, 1.0, grazingProgress);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Fade near the camera far clip before the grid is clipped by depth.
|
|
190
|
+
let farClip = max(grid.cameraFarClip, 0.00001);
|
|
191
|
+
let cameraDistance = length(grid.cameraPosition - input.world);
|
|
192
|
+
let distanceOpacity = 1.0 - smoothstep(0.0, 0.5 * farClip, cameraDistance - 0.5 * farClip);
|
|
193
|
+
|
|
194
|
+
let alpha = lineAlpha * edgeOpacity * distanceOpacity * grazingOpacity;
|
|
195
|
+
|
|
196
|
+
// Stipple discard below 0.1 alpha avoids muddy trails from transparent depth blending.
|
|
197
|
+
if (grid.optionsA.w != 0u) {
|
|
198
|
+
let dashCoord = uv / lod1CellSize;
|
|
199
|
+
let dashDistanceToLine = min(fract(dashCoord), 1.0 - fract(dashCoord));
|
|
200
|
+
let dashAxis = select(input.clip.x, input.clip.y, dashDistanceToLine.x < dashDistanceToLine.y);
|
|
201
|
+
if (alpha < 0.1 && alpha / 0.1 < fract(dashAxis / 4.0)) {
|
|
202
|
+
discard;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (alpha <= 0.001) {
|
|
207
|
+
discard;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return vec4f(lineColor, alpha);
|
|
211
|
+
}
|
|
212
|
+
`);
|
|
213
|
+
const REFERENCE_THIN_LINE_COLOR = [0.42, 0.42, 0.42, 0.45];
|
|
214
|
+
const REFERENCE_THICK_LINE_COLOR = [0.62, 0.62, 0.62, 0.65];
|
|
215
|
+
const MIN_LOD_CELL_SIZE = 0.0001;
|
|
216
|
+
/** Mirrors the reference property setters, including their clamps. */
|
|
217
|
+
export function resolveGridOptions(options) {
|
|
218
|
+
return {
|
|
219
|
+
size: Math.max(options.size ?? 2000, 1),
|
|
220
|
+
edgeFadeStartRatio: clamp(options.edgeFadeStartRatio ?? 0, 0, 1),
|
|
221
|
+
edgeFadeEndRatio: clamp(options.edgeFadeEndRatio ?? 1, 0, 1),
|
|
222
|
+
cellSize: Math.max(options.cellSize ?? 1, 0.0001),
|
|
223
|
+
lineWidthPixels: Math.max(options.lineWidthPixels ?? 1, 0.25),
|
|
224
|
+
lodFinerLevels: Math.max(Math.trunc(options.lodFinerLevels ?? 3), 0),
|
|
225
|
+
lodTotalLevels: Math.max(Math.trunc(options.lodTotalLevels ?? 8), 3),
|
|
226
|
+
enableGrazingOpacity: options.enableGrazingOpacity ?? true,
|
|
227
|
+
grazingFadeStart: clamp(options.grazingFadeStart ?? 3, 0, 90),
|
|
228
|
+
grazingFadeEnd: clamp(options.grazingFadeEnd ?? 20, 0, 90),
|
|
229
|
+
enableStippleDiscard: options.enableStippleDiscard ?? false,
|
|
230
|
+
enableLodCenterFade: options.enableLodCenterFade ?? true,
|
|
231
|
+
lodCenterFadeLineCount: options.lodCenterFadeLineCount ?? 151,
|
|
232
|
+
thinLineColor: options.thinLineColor ?? REFERENCE_THIN_LINE_COLOR,
|
|
233
|
+
thickLineColor: options.thickLineColor ?? REFERENCE_THICK_LINE_COLOR,
|
|
234
|
+
debugLodColors: options.debugLodColors ?? false,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function clamp(value, min, max) {
|
|
238
|
+
return Math.min(Math.max(value, min), max);
|
|
239
|
+
}
|
|
240
|
+
/** Reference `_get_min_lod_cell_size()`. */
|
|
241
|
+
export function minLodCellSize(options) {
|
|
242
|
+
return Math.max(options.cellSize * 10 ** -options.lodFinerLevels, MIN_LOD_CELL_SIZE);
|
|
243
|
+
}
|
|
244
|
+
/** Reference `_apply_lod_range_parameters()`. */
|
|
245
|
+
export function lodPowers(options) {
|
|
246
|
+
const min = -options.lodFinerLevels;
|
|
247
|
+
return { min, max: min + options.lodTotalLevels - 1 };
|
|
248
|
+
}
|
|
249
|
+
/** Reference `_apply_grazing_fade_parameters()`. */
|
|
250
|
+
export function grazingFactors(options) {
|
|
251
|
+
const toRadians = (degrees) => (degrees * Math.PI) / 180;
|
|
252
|
+
return { start: Math.sin(toRadians(options.grazingFadeStart)), end: Math.sin(toRadians(options.grazingFadeEnd)) };
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Reference `_update_camera_lod_distance()` with the grid plane at y = 0: the
|
|
256
|
+
* distance to the view floor drives the LOD, falling back to camera height near
|
|
257
|
+
* grazing angles so the LOD distance cannot blow up.
|
|
258
|
+
*/
|
|
259
|
+
export function gridLodState(camera, options = {}) {
|
|
260
|
+
const resolved = resolveGridOptions(options);
|
|
261
|
+
const minimum = minLodCellSize(resolved);
|
|
262
|
+
const [forwardX, forwardY, forwardZ] = camera.cameraForward;
|
|
263
|
+
const [cameraX, cameraY, cameraZ] = camera.cameraPosition;
|
|
264
|
+
const gridY = 0;
|
|
265
|
+
if (camera.projection === "orthographic") {
|
|
266
|
+
const size = Math.max(camera.orthographicSize ?? 0, minimum);
|
|
267
|
+
let center = [cameraX, cameraZ];
|
|
268
|
+
if (Math.abs(forwardY) > 0.0001) {
|
|
269
|
+
const rayDistance = (gridY - cameraY) / forwardY;
|
|
270
|
+
center = [cameraX + forwardX * rayDistance, cameraZ + forwardZ * rayDistance];
|
|
271
|
+
}
|
|
272
|
+
return { lodDistance: size, viewGridCenter: center, cameraIsOrthogonal: true };
|
|
273
|
+
}
|
|
274
|
+
const height = Math.abs(cameraY - gridY);
|
|
275
|
+
const viewFloorFactor = Math.abs(forwardY);
|
|
276
|
+
const rayFloorDistance = height / Math.max(viewFloorFactor, 0.0001);
|
|
277
|
+
// Match Blender: blend view-to-ground distance toward camera height near grazing angles.
|
|
278
|
+
const lodDistance = Math.max(rayFloorDistance + (height - rayFloorDistance) * (1 - viewFloorFactor), minimum);
|
|
279
|
+
return {
|
|
280
|
+
lodDistance,
|
|
281
|
+
viewGridCenter: [cameraX + forwardX * lodDistance, cameraZ + forwardZ * lodDistance],
|
|
282
|
+
cameraIsOrthogonal: false,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
/** The quad is centered on the camera XZ, matching `follow_viewport_camera`. */
|
|
286
|
+
export class InfiniteGrid3D {
|
|
287
|
+
#gpu;
|
|
288
|
+
#draw;
|
|
289
|
+
#geometry;
|
|
290
|
+
#options;
|
|
291
|
+
#opacity = 1;
|
|
292
|
+
#camera = {
|
|
293
|
+
viewProjection: new Float32Array(16),
|
|
294
|
+
cameraPosition: [0, 5, 8],
|
|
295
|
+
cameraForward: [0, -1, 0],
|
|
296
|
+
projection: "perspective",
|
|
297
|
+
farClip: 100,
|
|
298
|
+
};
|
|
299
|
+
constructor(gpu, options = {}) {
|
|
300
|
+
this.#gpu = gpu;
|
|
301
|
+
this.#options = resolveGridOptions(options);
|
|
302
|
+
this.#geometry = buildGeometry(gpu, this.#options.size);
|
|
303
|
+
this.#draw = buildDraw(gpu, this.#geometry);
|
|
304
|
+
}
|
|
305
|
+
prepare(camera) {
|
|
306
|
+
this.#camera = camera;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Applies reference-style property updates. Only `size` rebuilds the quad;
|
|
310
|
+
* every other field is a per-frame uniform, exactly like the reference setters.
|
|
311
|
+
*/
|
|
312
|
+
setOptions(options) {
|
|
313
|
+
const previousSize = this.#options.size;
|
|
314
|
+
this.#options = resolveGridOptions({ ...this.#currentOptions(), ...options });
|
|
315
|
+
if (this.#options.size !== previousSize)
|
|
316
|
+
this.#rebuild();
|
|
317
|
+
}
|
|
318
|
+
/** Reference `set_grid_size()`: rebuilds the finite quad. */
|
|
319
|
+
setSize(size) {
|
|
320
|
+
this.#options.size = Math.max(size, 1);
|
|
321
|
+
this.#rebuild();
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Demo extension over the reference: scales `thin_line_color.a` and
|
|
325
|
+
* `thick_line_color.a` without discarding their reference ratio.
|
|
326
|
+
*/
|
|
327
|
+
setOpacity(opacity) {
|
|
328
|
+
this.#opacity = clamp(opacity, 0, 1);
|
|
329
|
+
}
|
|
330
|
+
draw(pass) {
|
|
331
|
+
const options = this.#options;
|
|
332
|
+
const lod = gridLodState(this.#camera, options);
|
|
333
|
+
const powers = lodPowers(options);
|
|
334
|
+
const grazing = grazingFactors(options);
|
|
335
|
+
const thinAlpha = options.thinLineColor[3] * this.#opacity;
|
|
336
|
+
const thickAlpha = options.thickLineColor[3] * this.#opacity;
|
|
337
|
+
this.#draw.set({
|
|
338
|
+
grid: {
|
|
339
|
+
viewProjection: this.#camera.viewProjection,
|
|
340
|
+
cameraPosition: this.#camera.cameraPosition,
|
|
341
|
+
cellSize: options.cellSize,
|
|
342
|
+
viewGridCenter: lod.viewGridCenter,
|
|
343
|
+
gridCenter: [this.#camera.cameraPosition[0], this.#camera.cameraPosition[2]],
|
|
344
|
+
lineWidthPixels: options.lineWidthPixels,
|
|
345
|
+
gridSize: options.size,
|
|
346
|
+
edgeFadeStartRatio: options.edgeFadeStartRatio,
|
|
347
|
+
edgeFadeEndRatio: options.edgeFadeEndRatio,
|
|
348
|
+
minLodPower: powers.min,
|
|
349
|
+
maxLodPower: powers.max,
|
|
350
|
+
cameraLodDistance: lod.lodDistance,
|
|
351
|
+
cameraFarClip: this.#camera.farClip,
|
|
352
|
+
grazingFadeStartFactor: grazing.start,
|
|
353
|
+
grazingFadeEndFactor: grazing.end,
|
|
354
|
+
lodCenterFadeLineCount: options.lodCenterFadeLineCount,
|
|
355
|
+
optionsA: [
|
|
356
|
+
lod.cameraIsOrthogonal ? 1 : 0,
|
|
357
|
+
options.debugLodColors ? 1 : 0,
|
|
358
|
+
options.enableGrazingOpacity ? 1 : 0,
|
|
359
|
+
options.enableStippleDiscard ? 1 : 0,
|
|
360
|
+
],
|
|
361
|
+
optionsB: [options.enableLodCenterFade ? 1 : 0, 0, 0, 0],
|
|
362
|
+
thinLineColor: linearGridColor(options.thinLineColor, thinAlpha),
|
|
363
|
+
thickLineColor: linearGridColor(options.thickLineColor, thickAlpha),
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
pass.draw(this.#draw);
|
|
367
|
+
}
|
|
368
|
+
destroy() {
|
|
369
|
+
this.#draw.dispose();
|
|
370
|
+
this.#geometry.destroy();
|
|
371
|
+
}
|
|
372
|
+
#rebuild() {
|
|
373
|
+
this.#draw.dispose();
|
|
374
|
+
this.#geometry.destroy();
|
|
375
|
+
this.#geometry = buildGeometry(this.#gpu, this.#options.size);
|
|
376
|
+
this.#draw = buildDraw(this.#gpu, this.#geometry);
|
|
377
|
+
}
|
|
378
|
+
#currentOptions() {
|
|
379
|
+
return { ...this.#options, thinLineColor: this.#options.thinLineColor, thickLineColor: this.#options.thickLineColor };
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
function buildGeometry(gpu, size) {
|
|
383
|
+
const half = size * 0.5;
|
|
384
|
+
return geometry(gpu, {
|
|
385
|
+
buffers: [{
|
|
386
|
+
attributes: { position: "float32x3" },
|
|
387
|
+
data: new Float32Array([
|
|
388
|
+
-half, 0, -half, half, 0, -half, half, 0, half,
|
|
389
|
+
-half, 0, -half, half, 0, half, -half, 0, half,
|
|
390
|
+
]),
|
|
391
|
+
}],
|
|
392
|
+
vertexCount: 6,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Reference shader uses `render_mode unshaded, blend_mix, depth_draw_always,
|
|
397
|
+
* cull_disabled`: unlit, straight-alpha blending, two-sided, and depth writes on.
|
|
398
|
+
*/
|
|
399
|
+
function buildDraw(gpu, quad) {
|
|
400
|
+
return draw(gpu, {
|
|
401
|
+
shader: GRID_SHADER,
|
|
402
|
+
geometry: quad,
|
|
403
|
+
blend: "alpha",
|
|
404
|
+
cull: "none",
|
|
405
|
+
depth: { write: true, compare: "less-equal" },
|
|
406
|
+
});
|
|
407
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type FramePass, type Gpu } from "vgpu";
|
|
2
|
+
import type { MeshData } from "./geometry-generators.js";
|
|
3
|
+
import type { Color } from "./frame-draw-list.js";
|
|
4
|
+
export declare class MeshBatch3D {
|
|
5
|
+
#private;
|
|
6
|
+
constructor(gpu: Gpu, mesh: MeshData, color: Color, solid: boolean);
|
|
7
|
+
draw(pass: FramePass, viewProjection: ArrayLike<number>): void;
|
|
8
|
+
destroy(): void;
|
|
9
|
+
get solid(): boolean;
|
|
10
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { draw, geometry } from "vgpu";
|
|
2
|
+
import { prepareShader } from "@vgpu/wgsl/prepare";
|
|
3
|
+
import { DEFAULT_DEPTH_BIAS } from "./gpu-debug-draw-3d.js";
|
|
4
|
+
/** Mirrors the reference mesh/rect shaders: `depth_draw_always` plus the same depth bias. */
|
|
5
|
+
const SHADER = prepareShader(`
|
|
6
|
+
struct Camera { viewProjection: mat4x4f }
|
|
7
|
+
@group(0) @binding(0) var<uniform> camera: Camera;
|
|
8
|
+
const DEPTH_BIAS: f32 = ${DEFAULT_DEPTH_BIAS};
|
|
9
|
+
const DEPTH_BIAS_EPSILON: f32 = 0.000488;
|
|
10
|
+
|
|
11
|
+
fn apply_depth_bias(clip: vec4f) -> f32 {
|
|
12
|
+
let bias = clamp(DEPTH_BIAS, -1.0, 1.0);
|
|
13
|
+
if (bias >= 0.0) {
|
|
14
|
+
return clip.z * (1.0 - bias);
|
|
15
|
+
}
|
|
16
|
+
let safe_depth = max(clip.z, DEPTH_BIAS_EPSILON);
|
|
17
|
+
let depth_ratio = max(clip.w / safe_depth - DEPTH_BIAS_EPSILON, DEPTH_BIAS_EPSILON);
|
|
18
|
+
return clip.z * exp2(-bias * log2(depth_ratio));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
struct In { @location(0) position: vec3f, @location(1) color: vec4f }
|
|
22
|
+
struct Out { @builtin(position) clip: vec4f, @location(0) color: vec4f }
|
|
23
|
+
@vertex fn vs_main(input: In) -> Out {
|
|
24
|
+
let clip = camera.viewProjection * vec4f(input.position, 1.0);
|
|
25
|
+
var out: Out;
|
|
26
|
+
out.clip = vec4f(clip.xy, apply_depth_bias(clip), clip.w);
|
|
27
|
+
out.color = input.color;
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
@fragment fn fs_main(input: Out) -> @location(0) vec4f { return input.color; }
|
|
31
|
+
`);
|
|
32
|
+
function flatten(mesh, color, solid) {
|
|
33
|
+
const points = solid ? mesh.triangles.flatMap((triangle) => triangle) : mesh.segments.flatMap((segment) => segment);
|
|
34
|
+
const data = new Float32Array(points.length * 7);
|
|
35
|
+
for (let i = 0; i < points.length; i++) {
|
|
36
|
+
const offset = i * 7;
|
|
37
|
+
const point = points[i];
|
|
38
|
+
data.set([point[0], point[1], point[2], color[0], color[1], color[2], color[3]], offset);
|
|
39
|
+
}
|
|
40
|
+
return data;
|
|
41
|
+
}
|
|
42
|
+
export class MeshBatch3D {
|
|
43
|
+
#draw;
|
|
44
|
+
#geometry;
|
|
45
|
+
#solid;
|
|
46
|
+
constructor(gpu, mesh, color, solid) {
|
|
47
|
+
this.#solid = solid;
|
|
48
|
+
const data = flatten(mesh, color, solid);
|
|
49
|
+
this.#geometry = geometry(gpu, {
|
|
50
|
+
buffers: [{ attributes: { position: "float32x3", color: "float32x4" }, data: data }],
|
|
51
|
+
vertexCount: data.length / 7,
|
|
52
|
+
topology: solid ? "triangle-list" : "line-list",
|
|
53
|
+
});
|
|
54
|
+
const options = { shader: SHADER, geometry: this.#geometry, depth: { write: true, compare: "less-equal" } };
|
|
55
|
+
// Filled rects disable culling in the reference; solids keep back-face culling.
|
|
56
|
+
this.#draw = draw(gpu, solid ? { ...options, blend: "alpha", cull: "back" } : { ...options, cull: "none" });
|
|
57
|
+
}
|
|
58
|
+
draw(pass, viewProjection) {
|
|
59
|
+
this.#draw.set({ camera: { viewProjection } });
|
|
60
|
+
pass.draw(this.#draw);
|
|
61
|
+
}
|
|
62
|
+
destroy() {
|
|
63
|
+
this.#draw.dispose();
|
|
64
|
+
this.#geometry.destroy();
|
|
65
|
+
}
|
|
66
|
+
get solid() { return this.#solid; }
|
|
67
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WGSL for the 2D shape pipeline.
|
|
3
|
+
*
|
|
4
|
+
* One instanced quad per shape; the fragment stage evaluates a signed distance field, so a
|
|
5
|
+
* rounded rectangle, an ellipse, an annulus or a regular polygon are all the same pipeline and
|
|
6
|
+
* none of them is expanded to triangles on the CPU. Coverage comes from the screen-space
|
|
7
|
+
* derivative of the distance, which keeps edges one pixel wide at any magnification.
|
|
8
|
+
*
|
|
9
|
+
* The distance is evaluated in the shape's own space (the space its coordinates were given in),
|
|
10
|
+
* then converted to canvas pixels, so a stroke width is always a pixel count. `pixelsPerUnit`
|
|
11
|
+
* and `yFlip` are what make that conversion exact for both 2D spaces: `world2d` has +y up and
|
|
12
|
+
* `zoom` pixels per unit, `screen` has +y down and one.
|
|
13
|
+
*/
|
|
14
|
+
export declare function shape2dShader(): string;
|