@gridspace/raster-path 1.0.2
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 +20 -0
- package/README.md +292 -0
- package/build/app.js +1254 -0
- package/build/index.html +92 -0
- package/build/parse-stl.js +114 -0
- package/build/raster-path.js +688 -0
- package/build/serve.json +12 -0
- package/build/style.css +158 -0
- package/build/webgpu-worker.js +3011 -0
- package/package.json +58 -0
- package/scripts/build-shaders.js +65 -0
- package/src/index.js +688 -0
- package/src/shaders/planar-rasterize.wgsl +213 -0
- package/src/shaders/planar-toolpath.wgsl +83 -0
- package/src/shaders/radial-raster-v2.wgsl +195 -0
- package/src/web/app.js +1254 -0
- package/src/web/parse-stl.js +114 -0
- package/src/web/webgpu-worker.js +2520 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// Planar rasterization with spatial partitioning
|
|
2
|
+
// Sentinel value for empty cells (far below any real geometry)
|
|
3
|
+
const EMPTY_CELL: f32 = -1e10;
|
|
4
|
+
|
|
5
|
+
struct Uniforms {
|
|
6
|
+
bounds_min_x: f32,
|
|
7
|
+
bounds_min_y: f32,
|
|
8
|
+
bounds_min_z: f32,
|
|
9
|
+
bounds_max_x: f32,
|
|
10
|
+
bounds_max_y: f32,
|
|
11
|
+
bounds_max_z: f32,
|
|
12
|
+
step_size: f32,
|
|
13
|
+
grid_width: u32,
|
|
14
|
+
grid_height: u32,
|
|
15
|
+
triangle_count: u32,
|
|
16
|
+
filter_mode: u32, // 0 = UPWARD (terrain, keep highest), 1 = DOWNWARD (tool, keep lowest)
|
|
17
|
+
spatial_grid_width: u32,
|
|
18
|
+
spatial_grid_height: u32,
|
|
19
|
+
spatial_cell_size: f32,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@group(0) @binding(0) var<storage, read> triangles: array<f32>;
|
|
23
|
+
@group(0) @binding(1) var<storage, read_write> output_points: array<f32>;
|
|
24
|
+
@group(0) @binding(2) var<storage, read_write> valid_mask: array<u32>;
|
|
25
|
+
@group(0) @binding(3) var<uniform> uniforms: Uniforms;
|
|
26
|
+
@group(0) @binding(4) var<storage, read> spatial_cell_offsets: array<u32>;
|
|
27
|
+
@group(0) @binding(5) var<storage, read> spatial_triangle_indices: array<u32>;
|
|
28
|
+
|
|
29
|
+
// Fast 2D bounding box check for XY plane
|
|
30
|
+
fn ray_hits_triangle_bbox_2d(ray_x: f32, ray_y: f32, v0: vec3<f32>, v1: vec3<f32>, v2: vec3<f32>) -> bool {
|
|
31
|
+
// Add small epsilon to catch near-misses (mesh vertex gaps, FP rounding)
|
|
32
|
+
let epsilon = 0.001; // 1 micron tolerance
|
|
33
|
+
let min_x = min(min(v0.x, v1.x), v2.x) - epsilon;
|
|
34
|
+
let max_x = max(max(v0.x, v1.x), v2.x) + epsilon;
|
|
35
|
+
let min_y = min(min(v0.y, v1.y), v2.y) - epsilon;
|
|
36
|
+
let max_y = max(max(v0.y, v1.y), v2.y) + epsilon;
|
|
37
|
+
|
|
38
|
+
return ray_x >= min_x && ray_x <= max_x && ray_y >= min_y && ray_y <= max_y;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Ray-triangle intersection using Möller-Trumbore algorithm
|
|
42
|
+
fn ray_triangle_intersect(
|
|
43
|
+
ray_origin: vec3<f32>,
|
|
44
|
+
ray_dir: vec3<f32>,
|
|
45
|
+
v0: vec3<f32>,
|
|
46
|
+
v1: vec3<f32>,
|
|
47
|
+
v2: vec3<f32>
|
|
48
|
+
) -> vec2<f32> { // Returns (hit: 0.0 or 1.0, z: intersection_z)
|
|
49
|
+
// Larger epsilon needed because near-parallel triangles (small 'a') amplify errors via f=1/a
|
|
50
|
+
let EPSILON = 0.0001;
|
|
51
|
+
|
|
52
|
+
// Early rejection using 2D bounding box (very cheap!)
|
|
53
|
+
if (!ray_hits_triangle_bbox_2d(ray_origin.x, ray_origin.y, v0, v1, v2)) {
|
|
54
|
+
return vec2<f32>(0.0, 0.0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Calculate edges
|
|
58
|
+
let edge1 = v1 - v0;
|
|
59
|
+
let edge2 = v2 - v0;
|
|
60
|
+
|
|
61
|
+
// Cross product: ray_dir × edge2
|
|
62
|
+
let h = cross(ray_dir, edge2);
|
|
63
|
+
|
|
64
|
+
// Dot product: edge1 · h
|
|
65
|
+
let a = dot(edge1, h);
|
|
66
|
+
|
|
67
|
+
if (a > -EPSILON && a < EPSILON) {
|
|
68
|
+
return vec2<f32>(0.0, 0.0); // Ray parallel to triangle
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let f = 1.0 / a;
|
|
72
|
+
|
|
73
|
+
// s = ray_origin - v0
|
|
74
|
+
let s = ray_origin - v0;
|
|
75
|
+
|
|
76
|
+
// u = f * (s · h)
|
|
77
|
+
let u = f * dot(s, h);
|
|
78
|
+
|
|
79
|
+
// Allow tolerance for edges/vertices to ensure watertight coverage
|
|
80
|
+
if (u < -EPSILON || u > 1.0 + EPSILON) {
|
|
81
|
+
return vec2<f32>(0.0, 0.0);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Cross product: s × edge1
|
|
85
|
+
let q = cross(s, edge1);
|
|
86
|
+
|
|
87
|
+
// v = f * (ray_dir · q)
|
|
88
|
+
let v = f * dot(ray_dir, q);
|
|
89
|
+
|
|
90
|
+
// Allow tolerance for edges/vertices to ensure watertight coverage
|
|
91
|
+
if (v < -EPSILON || u + v > 1.0 + EPSILON) {
|
|
92
|
+
return vec2<f32>(0.0, 0.0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// t = f * (edge2 · q)
|
|
96
|
+
let t = f * dot(edge2, q);
|
|
97
|
+
|
|
98
|
+
if (t > EPSILON) {
|
|
99
|
+
// Intersection found - calculate Z coordinate
|
|
100
|
+
let intersection_z = ray_origin.z + ray_dir.z * t;
|
|
101
|
+
return vec2<f32>(1.0, intersection_z);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return vec2<f32>(0.0, 0.0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@compute @workgroup_size(16, 16)
|
|
108
|
+
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
|
109
|
+
let grid_x = global_id.x;
|
|
110
|
+
let grid_y = global_id.y;
|
|
111
|
+
|
|
112
|
+
if (grid_x >= uniforms.grid_width || grid_y >= uniforms.grid_height) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Calculate world position for this grid point (center of cell)
|
|
117
|
+
let world_x = uniforms.bounds_min_x + f32(grid_x) * uniforms.step_size;
|
|
118
|
+
let world_y = uniforms.bounds_min_y + f32(grid_y) * uniforms.step_size;
|
|
119
|
+
|
|
120
|
+
// Initialize best_z based on filter mode
|
|
121
|
+
var best_z: f32;
|
|
122
|
+
if (uniforms.filter_mode == 0u) {
|
|
123
|
+
best_z = -1e10; // Terrain: keep highest Z
|
|
124
|
+
} else {
|
|
125
|
+
best_z = 1e10; // Tool: keep lowest Z
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
var found = false;
|
|
129
|
+
|
|
130
|
+
// Ray from below mesh pointing up (+Z direction)
|
|
131
|
+
let ray_origin = vec3<f32>(world_x, world_y, uniforms.bounds_min_z - 1.0);
|
|
132
|
+
let ray_dir = vec3<f32>(0.0, 0.0, 1.0);
|
|
133
|
+
|
|
134
|
+
// Find which spatial grid cell this ray belongs to
|
|
135
|
+
let spatial_cell_x = u32((world_x - uniforms.bounds_min_x) / uniforms.spatial_cell_size);
|
|
136
|
+
let spatial_cell_y = u32((world_y - uniforms.bounds_min_y) / uniforms.spatial_cell_size);
|
|
137
|
+
|
|
138
|
+
// Clamp to spatial grid bounds
|
|
139
|
+
let clamped_cx = min(spatial_cell_x, uniforms.spatial_grid_width - 1u);
|
|
140
|
+
let clamped_cy = min(spatial_cell_y, uniforms.spatial_grid_height - 1u);
|
|
141
|
+
|
|
142
|
+
let spatial_cell_idx = clamped_cy * uniforms.spatial_grid_width + clamped_cx;
|
|
143
|
+
|
|
144
|
+
// Get triangle range for this cell
|
|
145
|
+
let start_idx = spatial_cell_offsets[spatial_cell_idx];
|
|
146
|
+
let end_idx = spatial_cell_offsets[spatial_cell_idx + 1u];
|
|
147
|
+
|
|
148
|
+
// Test only triangles in this spatial cell
|
|
149
|
+
for (var idx = start_idx; idx < end_idx; idx++) {
|
|
150
|
+
let tri_idx = spatial_triangle_indices[idx];
|
|
151
|
+
let tri_base = tri_idx * 9u;
|
|
152
|
+
|
|
153
|
+
// Read triangle vertices (already in local space and rotated if needed)
|
|
154
|
+
let v0 = vec3<f32>(
|
|
155
|
+
triangles[tri_base],
|
|
156
|
+
triangles[tri_base + 1u],
|
|
157
|
+
triangles[tri_base + 2u]
|
|
158
|
+
);
|
|
159
|
+
let v1 = vec3<f32>(
|
|
160
|
+
triangles[tri_base + 3u],
|
|
161
|
+
triangles[tri_base + 4u],
|
|
162
|
+
triangles[tri_base + 5u]
|
|
163
|
+
);
|
|
164
|
+
let v2 = vec3<f32>(
|
|
165
|
+
triangles[tri_base + 6u],
|
|
166
|
+
triangles[tri_base + 7u],
|
|
167
|
+
triangles[tri_base + 8u]
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
let result = ray_triangle_intersect(ray_origin, ray_dir, v0, v1, v2);
|
|
171
|
+
let hit = result.x;
|
|
172
|
+
let intersection_z = result.y;
|
|
173
|
+
|
|
174
|
+
if (hit > 0.5) {
|
|
175
|
+
if (uniforms.filter_mode == 0u) {
|
|
176
|
+
// Terrain: keep highest
|
|
177
|
+
if (intersection_z > best_z) {
|
|
178
|
+
best_z = intersection_z;
|
|
179
|
+
found = true;
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
// Tool: keep lowest
|
|
183
|
+
if (intersection_z < best_z) {
|
|
184
|
+
best_z = intersection_z;
|
|
185
|
+
found = true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Write output based on filter mode
|
|
192
|
+
let output_idx = grid_y * uniforms.grid_width + grid_x;
|
|
193
|
+
|
|
194
|
+
if (uniforms.filter_mode == 0u) {
|
|
195
|
+
// Terrain: Dense output (Z-only, sentinel value for empty cells)
|
|
196
|
+
if (found) {
|
|
197
|
+
output_points[output_idx] = best_z;
|
|
198
|
+
} else {
|
|
199
|
+
output_points[output_idx] = EMPTY_CELL;
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
// Tool: Sparse output (X, Y, Z triplets with valid mask)
|
|
203
|
+
output_points[output_idx * 3u] = f32(grid_x);
|
|
204
|
+
output_points[output_idx * 3u + 1u] = f32(grid_y);
|
|
205
|
+
output_points[output_idx * 3u + 2u] = best_z;
|
|
206
|
+
|
|
207
|
+
if (found) {
|
|
208
|
+
valid_mask[output_idx] = 1u;
|
|
209
|
+
} else {
|
|
210
|
+
valid_mask[output_idx] = 0u;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Planar toolpath generation
|
|
2
|
+
// Sentinel value for empty terrain cells (must match rasterize shader)
|
|
3
|
+
const EMPTY_CELL: f32 = -1e10;
|
|
4
|
+
const MAX_F32: f32 = 3.402823466e+38;
|
|
5
|
+
|
|
6
|
+
struct SparseToolPoint {
|
|
7
|
+
x_offset: i32,
|
|
8
|
+
y_offset: i32,
|
|
9
|
+
z_value: f32,
|
|
10
|
+
padding: f32,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
struct Uniforms {
|
|
14
|
+
terrain_width: u32,
|
|
15
|
+
terrain_height: u32,
|
|
16
|
+
tool_count: u32,
|
|
17
|
+
x_step: u32,
|
|
18
|
+
y_step: u32,
|
|
19
|
+
oob_z: f32,
|
|
20
|
+
points_per_line: u32,
|
|
21
|
+
num_scanlines: u32,
|
|
22
|
+
y_offset: u32, // Offset to center Y position (for single-scanline radial mode)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@group(0) @binding(0) var<storage, read> terrain_map: array<f32>;
|
|
26
|
+
@group(0) @binding(1) var<storage, read> sparse_tool: array<SparseToolPoint>;
|
|
27
|
+
@group(0) @binding(2) var<storage, read_write> output_path: array<f32>;
|
|
28
|
+
@group(0) @binding(3) var<uniform> uniforms: Uniforms;
|
|
29
|
+
|
|
30
|
+
@compute @workgroup_size(16, 16)
|
|
31
|
+
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
|
32
|
+
let scanline = global_id.y;
|
|
33
|
+
let point_idx = global_id.x;
|
|
34
|
+
|
|
35
|
+
if (scanline >= uniforms.num_scanlines || point_idx >= uniforms.points_per_line) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let tool_center_x = i32(point_idx * uniforms.x_step);
|
|
40
|
+
let tool_center_y = i32(scanline * uniforms.y_step) + i32(uniforms.y_offset);
|
|
41
|
+
|
|
42
|
+
// var max_collision_z = -MAX_F32; // Track maximum collision height
|
|
43
|
+
var max_collision_z = uniforms.oob_z; // Track maximum collision height
|
|
44
|
+
var found_collision = false;
|
|
45
|
+
|
|
46
|
+
for (var i = 0u; i < uniforms.tool_count; i++) {
|
|
47
|
+
let tool_point = sparse_tool[i];
|
|
48
|
+
let terrain_x = tool_center_x + tool_point.x_offset;
|
|
49
|
+
let terrain_y = tool_center_y + tool_point.y_offset;
|
|
50
|
+
|
|
51
|
+
// Bounds check: X must always be in bounds
|
|
52
|
+
if (terrain_x < 0 || terrain_x >= i32(uniforms.terrain_width)) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Bounds check: Y must be within terrain strip bounds
|
|
57
|
+
// For single-scanline OUTPUT mode, the tool center is at Y=0 but the terrain
|
|
58
|
+
// strip contains the full Y range (tool width), so tool offsets access different Y rows
|
|
59
|
+
if (terrain_y < 0 || terrain_y >= i32(uniforms.terrain_height)) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let terrain_idx = u32(terrain_y) * uniforms.terrain_width + u32(terrain_x);
|
|
64
|
+
let terrain_z = terrain_map[terrain_idx];
|
|
65
|
+
|
|
66
|
+
// Check if terrain cell has geometry (not empty sentinel value)
|
|
67
|
+
if (terrain_z > EMPTY_CELL + 1.0) {
|
|
68
|
+
// Tool z_value is positive offset from tip (tip=0, shaft=+50)
|
|
69
|
+
// Subtract from terrain to find where tool center needs to be
|
|
70
|
+
let collision_z = terrain_z + tool_point.z_value;
|
|
71
|
+
max_collision_z = max(max_collision_z, collision_z);
|
|
72
|
+
found_collision = true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
var output_z = uniforms.oob_z;
|
|
77
|
+
if (found_collision) {
|
|
78
|
+
output_z = max_collision_z;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let output_idx = scanline * uniforms.points_per_line + point_idx;
|
|
82
|
+
output_path[output_idx] = output_z;
|
|
83
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// Radial V2 rasterization with X-bucketing and rotating ray planes
|
|
2
|
+
// Sentinel value for empty cells (far below any real geometry)
|
|
3
|
+
const EMPTY_CELL: f32 = -1e10;
|
|
4
|
+
const PI: f32 = 3.14159265359;
|
|
5
|
+
|
|
6
|
+
struct Uniforms {
|
|
7
|
+
resolution: f32, // Grid step size (mm)
|
|
8
|
+
angle_step: f32, // Radians between angles
|
|
9
|
+
num_angles: u32, // Total number of angular strips
|
|
10
|
+
max_radius: f32, // Ray origin distance from X-axis (maxHypot * 1.01)
|
|
11
|
+
tool_width: f32, // Tool width in mm
|
|
12
|
+
grid_y_height: u32, // Tool width in pixels (toolWidth / resolution)
|
|
13
|
+
bucket_width: f32, // Width of each X-bucket (mm)
|
|
14
|
+
bucket_grid_width: u32, // Bucket width in pixels
|
|
15
|
+
global_min_x: f32, // Global minimum X coordinate
|
|
16
|
+
z_floor: f32, // Z value for empty cells
|
|
17
|
+
filter_mode: u32, // 0 = max Z (terrain), 1 = min Z (tool)
|
|
18
|
+
num_buckets: u32, // Total number of X-buckets
|
|
19
|
+
start_angle: f32, // Starting angle offset in radians (for batching)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
struct BucketInfo {
|
|
23
|
+
min_x: f32, // Bucket X range start (mm)
|
|
24
|
+
max_x: f32, // Bucket X range end (mm)
|
|
25
|
+
start_index: u32, // Index into triangle_indices array
|
|
26
|
+
count: u32 // Number of triangles in this bucket
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@group(0) @binding(0) var<storage, read> triangles: array<f32>;
|
|
30
|
+
@group(0) @binding(1) var<storage, read_write> output: array<f32>;
|
|
31
|
+
@group(0) @binding(2) var<uniform> uniforms: Uniforms;
|
|
32
|
+
@group(0) @binding(3) var<storage, read> bucket_info: array<BucketInfo>;
|
|
33
|
+
@group(0) @binding(4) var<storage, read> triangle_indices: array<u32>;
|
|
34
|
+
|
|
35
|
+
// Note: AABB early rejection removed - X-bucketing already provides spatial filtering
|
|
36
|
+
// A proper ray-AABB intersection test would be needed if we wanted bounding box culling,
|
|
37
|
+
// but checking if ray_origin is inside AABB was incorrect and rejected valid triangles
|
|
38
|
+
|
|
39
|
+
// Ray-triangle intersection using Möller-Trumbore algorithm
|
|
40
|
+
fn ray_triangle_intersect(
|
|
41
|
+
ray_origin: vec3<f32>,
|
|
42
|
+
ray_dir: vec3<f32>,
|
|
43
|
+
v0: vec3<f32>,
|
|
44
|
+
v1: vec3<f32>,
|
|
45
|
+
v2: vec3<f32>
|
|
46
|
+
) -> vec2<f32> { // Returns (hit: 0.0 or 1.0, z: intersection_z)
|
|
47
|
+
let EPSILON = 0.0001;
|
|
48
|
+
|
|
49
|
+
// Calculate edges
|
|
50
|
+
let edge1 = v1 - v0;
|
|
51
|
+
let edge2 = v2 - v0;
|
|
52
|
+
|
|
53
|
+
// Cross product: ray_dir × edge2
|
|
54
|
+
let h = cross(ray_dir, edge2);
|
|
55
|
+
|
|
56
|
+
// Dot product: edge1 · h
|
|
57
|
+
let a = dot(edge1, h);
|
|
58
|
+
|
|
59
|
+
if (a > -EPSILON && a < EPSILON) {
|
|
60
|
+
return vec2<f32>(0.0, 0.0); // Ray parallel to triangle
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let f = 1.0 / a;
|
|
64
|
+
|
|
65
|
+
// s = ray_origin - v0
|
|
66
|
+
let s = ray_origin - v0;
|
|
67
|
+
|
|
68
|
+
// u = f * (s · h)
|
|
69
|
+
let u = f * dot(s, h);
|
|
70
|
+
|
|
71
|
+
if (u < -EPSILON || u > 1.0 + EPSILON) {
|
|
72
|
+
return vec2<f32>(0.0, 0.0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Cross product: s × edge1
|
|
76
|
+
let q = cross(s, edge1);
|
|
77
|
+
|
|
78
|
+
// v = f * (ray_dir · q)
|
|
79
|
+
let v = f * dot(ray_dir, q);
|
|
80
|
+
|
|
81
|
+
if (v < -EPSILON || u + v > 1.0 + EPSILON) {
|
|
82
|
+
return vec2<f32>(0.0, 0.0);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// t = f * (edge2 · q)
|
|
86
|
+
let t = f * dot(edge2, q);
|
|
87
|
+
|
|
88
|
+
if (t > EPSILON) {
|
|
89
|
+
// Intersection found - return distance along ray (t parameter)
|
|
90
|
+
return vec2<f32>(1.0, t);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return vec2<f32>(0.0, 0.0);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@compute @workgroup_size(8, 8, 1)
|
|
97
|
+
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
|
98
|
+
let bucket_idx = global_id.z;
|
|
99
|
+
let grid_y = global_id.y;
|
|
100
|
+
let angle_idx = global_id.x;
|
|
101
|
+
|
|
102
|
+
// Bounds check
|
|
103
|
+
if (angle_idx >= uniforms.num_angles ||
|
|
104
|
+
grid_y >= uniforms.grid_y_height ||
|
|
105
|
+
bucket_idx >= uniforms.num_buckets) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let bucket = bucket_info[bucket_idx];
|
|
110
|
+
let angle = uniforms.start_angle + (f32(angle_idx) * uniforms.angle_step);
|
|
111
|
+
|
|
112
|
+
// Calculate bucket min grid X
|
|
113
|
+
let bucket_min_grid_x = u32((bucket.min_x - uniforms.global_min_x) / uniforms.resolution);
|
|
114
|
+
|
|
115
|
+
// Loop over X within this bucket
|
|
116
|
+
for (var local_x = 0u; local_x < uniforms.bucket_grid_width; local_x++) {
|
|
117
|
+
let grid_x = bucket_min_grid_x + local_x;
|
|
118
|
+
let world_x = uniforms.global_min_x + f32(grid_x) * uniforms.resolution;
|
|
119
|
+
|
|
120
|
+
// Rotating top-down scan: normal XY planar scan, rotated around X-axis
|
|
121
|
+
// Step 1: Define scan position in planar frame (X, Y, Z_above)
|
|
122
|
+
let scan_x = world_x;
|
|
123
|
+
let scan_y = f32(grid_y) * uniforms.resolution - uniforms.tool_width / 2.0;
|
|
124
|
+
let scan_z = uniforms.max_radius; // Start above the model
|
|
125
|
+
|
|
126
|
+
// Step 2: Rotate position (scan_x, scan_y, scan_z) around X-axis by 'angle'
|
|
127
|
+
// X stays the same, rotate YZ plane: y' = y*cos - z*sin, z' = y*sin + z*cos
|
|
128
|
+
let ray_origin_x = scan_x;
|
|
129
|
+
let ray_origin_y = scan_y * cos(angle) - scan_z * sin(angle);
|
|
130
|
+
let ray_origin_z = scan_y * sin(angle) + scan_z * cos(angle);
|
|
131
|
+
let ray_origin = vec3<f32>(ray_origin_x, ray_origin_y, ray_origin_z);
|
|
132
|
+
|
|
133
|
+
// Step 3: Rotate ray direction (0, 0, -1) around X-axis by 'angle'
|
|
134
|
+
// X component stays 0, rotate YZ: dy = 0*cos - (-1)*sin = sin, dz = 0*sin + (-1)*cos = -cos
|
|
135
|
+
let ray_dir = vec3<f32>(0.0, sin(angle), -cos(angle));
|
|
136
|
+
|
|
137
|
+
// Initialize best distance (closest hit)
|
|
138
|
+
var best_t: f32 = 1e10; // Start with very large distance
|
|
139
|
+
var found = false;
|
|
140
|
+
|
|
141
|
+
// Ray-cast against triangles in this bucket
|
|
142
|
+
for (var i = 0u; i < bucket.count; i++) {
|
|
143
|
+
let tri_idx = triangle_indices[bucket.start_index + i];
|
|
144
|
+
let tri_base = tri_idx * 9u;
|
|
145
|
+
|
|
146
|
+
// Read triangle vertices
|
|
147
|
+
let v0 = vec3<f32>(
|
|
148
|
+
triangles[tri_base],
|
|
149
|
+
triangles[tri_base + 1u],
|
|
150
|
+
triangles[tri_base + 2u]
|
|
151
|
+
);
|
|
152
|
+
let v1 = vec3<f32>(
|
|
153
|
+
triangles[tri_base + 3u],
|
|
154
|
+
triangles[tri_base + 4u],
|
|
155
|
+
triangles[tri_base + 5u]
|
|
156
|
+
);
|
|
157
|
+
let v2 = vec3<f32>(
|
|
158
|
+
triangles[tri_base + 6u],
|
|
159
|
+
triangles[tri_base + 7u],
|
|
160
|
+
triangles[tri_base + 8u]
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
let result = ray_triangle_intersect(ray_origin, ray_dir, v0, v1, v2);
|
|
164
|
+
let hit = result.x;
|
|
165
|
+
let t = result.y; // Distance along ray
|
|
166
|
+
|
|
167
|
+
if (hit > 0.5) {
|
|
168
|
+
// Keep closest hit (minimum t)
|
|
169
|
+
if (t < best_t) {
|
|
170
|
+
best_t = t;
|
|
171
|
+
found = true;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Write output
|
|
177
|
+
// Layout: bucket_idx * numAngles * bucketWidth * gridHeight
|
|
178
|
+
// + angle_idx * bucketWidth * gridHeight
|
|
179
|
+
// + grid_y * bucketWidth
|
|
180
|
+
// + local_x
|
|
181
|
+
let output_idx = bucket_idx * uniforms.num_angles * uniforms.bucket_grid_width * uniforms.grid_y_height
|
|
182
|
+
+ angle_idx * uniforms.bucket_grid_width * uniforms.grid_y_height
|
|
183
|
+
+ grid_y * uniforms.bucket_grid_width
|
|
184
|
+
+ local_x;
|
|
185
|
+
|
|
186
|
+
if (found) {
|
|
187
|
+
// Terrain height = distance from scan origin minus ray travel distance
|
|
188
|
+
// Ray started at max_radius from X-axis, traveled best_t distance to hit
|
|
189
|
+
let terrain_height = uniforms.max_radius - best_t;
|
|
190
|
+
output[output_idx] = terrain_height;
|
|
191
|
+
} else {
|
|
192
|
+
output[output_idx] = uniforms.z_floor;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|