Toast Engine
Loading...
Searching...
No Matches
voxel_query.hpp
Go to the documentation of this file.
1
7
8#pragma once
9
10#include <algorithm>
11#include <array>
12#include <cmath>
13#include <cstdint>
14#include <glm/glm.hpp>
15#include <limits>
16#include <span>
21#include <type_traits>
22
23namespace physics {
24
26 const voxel::Volume& volume;
27 const voxel::VolumeSurface& surface;
28 const voxel::Palette& palette;
29 const voxel::MaterialLibrary& materials;
30};
31
33 glm::ivec3 voxel;
34 glm::ivec3 brick;
35 uint32_t brick_slot;
36 uint16_t local_index;
37 glm::vec3 min;
38 glm::vec3 max;
39 uint8_t normal_index;
40 voxel::VoxelClass classification;
41 uint8_t palette_index;
42 const voxel::PhysicalMaterial* material;
43 uint8_t exposure = voxel::k_all_faces_exposed;
44};
45
46namespace _detail {
47
48template<typename Callback>
49[[nodiscard]]
50auto invokeVoxelCallback(Callback& callback, const VoxelCandidate& candidate) -> bool {
51 if constexpr (std::is_same_v<std::invoke_result_t<Callback&, const VoxelCandidate&>, bool>) {
52 return callback(candidate);
53 } else {
54 callback(candidate);
55 return true;
56 }
57}
58
59}
60
61template<typename Callback>
62void queryVoxelSurface(const VoxelQueryContext& context, const AABB& aabb, Callback&& callback) {
63 // floor and ceil before clamping or a negative corner loses the info needed to reject it
64 glm::ivec3 first = glm::ivec3(glm::floor(aabb.min / voxel::k_voxel_size));
65 glm::ivec3 last = glm::ivec3(glm::ceil(aabb.max / voxel::k_voxel_size)) - 1;
66
67 const glm::ivec3 voxel_dims = glm::ivec3(context.volume.voxelDims());
68 if (glm::any(glm::lessThan(last, glm::ivec3(0))) || glm::any(glm::greaterThanEqual(first, voxel_dims))) {
69 return;
70 }
71 first = glm::clamp(first, glm::ivec3(0), voxel_dims - 1);
72 last = glm::clamp(last, glm::ivec3(0), voxel_dims - 1);
73 if (first.x > last.x || first.y > last.y || first.z > last.z) {
74 return;
75 }
76
77 const glm::ivec3 first_brick = first >> 3;
78 const glm::ivec3 last_brick = last >> 3;
79 const glm::ivec3 brick_dims = glm::ivec3(context.volume.brickDims());
80 const auto brick_dim = static_cast<int32_t>(voxel::k_brick_dim);
81
82 bool stopped = false;
83
84 const auto process_brick = [&](glm::ivec3 brick) {
85 const voxel::BrickEntry entry = context.volume.entryAt(brick);
86 if (entry.tag() == voxel::BrickTag::empty) {
87 return;
88 }
89
90 const std::span<const voxel::SurfaceVoxel> surface = context.surface.brickSurface(brick);
91 if (surface.empty()) {
92 return;
93 }
94
95 // private in Volume and VolumeSurface so recompute it the same way here
96 const uint32_t brick_slot = static_cast<uint32_t>(brick.x) + (static_cast<uint32_t>(brick.y) * brick_dims.x) +
97 (static_cast<uint32_t>(brick.z) * brick_dims.x * brick_dims.y);
98
99 const glm::ivec3 brick_base = brick * brick_dim;
100 const auto emit = [&](const voxel::SurfaceVoxel& surface_voxel, glm::ivec3 global_voxel) -> bool {
101 const uint8_t palette_index = context.volume.materialAt(global_voxel);
102 const uint32_t material_index = voxel::resolveMaterialIndex(context.palette, context.materials, palette_index);
103 const voxel::PhysicalMaterial& material = context.materials.materials[material_index];
104 if (not material.collides()) {
105 return true;
106 }
107
108 const glm::vec3 min = glm::vec3(global_voxel) * voxel::k_voxel_size;
109 const glm::vec3 max = min + glm::vec3(voxel::k_voxel_size);
110
111 return _detail::invokeVoxelCallback(
112 callback,
114 .voxel = global_voxel,
115 .brick = brick,
116 .brick_slot = brick_slot,
117 .local_index = surface_voxel.local_index,
118 .min = min,
119 .max = max,
120 .normal_index = voxel::normalOf(surface_voxel.classification),
121 .classification = voxel::classOf(surface_voxel.classification),
122 .palette_index = palette_index,
123 .material = &material,
124 .exposure = surface_voxel.exposure,
125 }
126 );
127 };
128
129 const glm::ivec3 clipped_min = glm::max(first, brick_base);
130 const glm::ivec3 clipped_max = glm::min(last, brick_base + (brick_dim - 1));
131 const glm::ivec3 clipped_dims = clipped_max - clipped_min + 1;
132 const auto clipped_voxels =
133 static_cast<size_t>(clipped_dims.x) * static_cast<size_t>(clipped_dims.y) * static_cast<size_t>(clipped_dims.z);
134
135 if (clipped_voxels >= surface.size()) {
136 for (const voxel::SurfaceVoxel& surface_voxel : surface) {
137 const voxel::BrickCoord local = voxel::localFromIndex(surface_voxel.local_index);
138 const glm::ivec3 global_voxel =
139 brick_base + glm::ivec3 {static_cast<int32_t>(local.x), static_cast<int32_t>(local.y), static_cast<int32_t>(local.z)};
140
141 if (glm::any(glm::lessThan(global_voxel, first)) || glm::any(glm::greaterThan(global_voxel, last))) {
142 continue;
143 }
144 if (not emit(surface_voxel, global_voxel)) {
145 stopped = true;
146 return;
147 }
148 }
149 return;
150 }
151
152 for (int32_t vz = clipped_min.z; vz <= clipped_max.z; ++vz) {
153 for (int32_t vy = clipped_min.y; vy <= clipped_max.y; ++vy) {
154 for (int32_t vx = clipped_min.x; vx <= clipped_max.x; ++vx) {
155 const auto local_index = static_cast<uint16_t>(
156 (static_cast<uint32_t>(vz - brick_base.z) << 6u) | (static_cast<uint32_t>(vy - brick_base.y) << 3u) |
157 static_cast<uint32_t>(vx - brick_base.x)
158 );
159
160 const auto found = std::lower_bound(
161 surface.begin(), surface.end(), local_index, [](const voxel::SurfaceVoxel& candidate, uint16_t index) {
162 return candidate.local_index < index;
163 }
164 );
165 if (found == surface.end() || found->local_index != local_index) {
166 continue;
167 }
168 if (not emit(*found, glm::ivec3 {vx, vy, vz})) {
169 stopped = true;
170 return;
171 }
172 }
173 }
174 }
175 };
176
177 const uint64_t box_bricks = static_cast<uint64_t>(last_brick.x - first_brick.x + 1) *
178 static_cast<uint64_t>(last_brick.y - first_brick.y + 1) *
179 static_cast<uint64_t>(last_brick.z - first_brick.z + 1);
180 const uint64_t populated_bricks = static_cast<uint64_t>(context.surface.populatedBrickCount());
181
182 if (populated_bricks < box_bricks) {
183 context.surface.forEachPopulatedBrick([&](glm::ivec3 brick) {
184 if (stopped) {
185 return;
186 }
187 if (glm::any(glm::lessThan(brick, first_brick)) || glm::any(glm::greaterThan(brick, last_brick))) {
188 return;
189 }
190 process_brick(brick);
191 });
192 } else {
193 for (int32_t z = first_brick.z; z <= last_brick.z && not stopped; ++z) {
194 for (int32_t y = first_brick.y; y <= last_brick.y && not stopped; ++y) {
195 for (int32_t x = first_brick.x; x <= last_brick.x && not stopped; ++x) {
196 process_brick({x, y, z});
197 }
198 }
199 }
200 }
201}
202
204 float distance = 0.0f;
205 glm::vec3 normal = {};
206 glm::ivec3 voxel = {};
207 uint8_t palette_index = voxel::k_empty_palette_index;
208};
209
210namespace _detail {
211
212inline constexpr float k_ray_tie_epsilon = 1.0e-6f;
213
214[[nodiscard]]
215inline auto rayTieSlack(float t) -> float {
216 return k_ray_tie_epsilon * std::max(1.0f, std::abs(t));
217}
218
219}
220
226template<typename OnEntry>
228 const VoxelQueryContext& context, const AABB& bounds, glm::vec3 origin, glm::vec3 direction, float max_distance,
229 bool hit_passable, OnEntry&& on_entry
230) {
231 constexpr float k_infinity = std::numeric_limits<float>::infinity();
232 constexpr auto k_brick = static_cast<int32_t>(voxel::k_brick_dim);
233 constexpr float k_voxel = voxel::k_voxel_size;
234
235 const voxel::Volume& volume = context.volume;
236 const glm::ivec3 dims = glm::ivec3(volume.voxelDims());
237
238 // from here everything is in voxel size
239 const glm::vec3 start = origin / k_voxel;
240 const glm::vec3 box_min = glm::max(glm::floor(bounds.min / k_voxel - 1.0e-3f), glm::vec3(0.0f));
241 const glm::vec3 box_max = glm::min(glm::ceil(bounds.max / k_voxel + 1.0e-3f), glm::vec3(dims));
242 float limit = max_distance / k_voxel;
243
244 std::array<float, 3> entry_times {-k_infinity, -k_infinity, -k_infinity};
245 glm::vec3 inverse(0.0f);
246 glm::vec3 sign(0.0f);
247 float t_exit = k_infinity;
248
249 for (int axis = 0; axis < 3; ++axis) {
250 if (direction[axis] == 0.0f) {
251 if (start[axis] < box_min[axis] || start[axis] > box_max[axis]) {
252 return;
253 }
254 continue;
255 }
256
257 inverse[axis] = 1.0f / direction[axis];
258 sign[axis] = direction[axis] > 0.0f ? 1.0f : -1.0f;
259 const float t0 = (box_min[axis] - start[axis]) * inverse[axis];
260 const float t1 = (box_max[axis] - start[axis]) * inverse[axis];
261 entry_times[axis] = std::min(t0, t1);
262 t_exit = std::min(t_exit, std::max(t0, t1));
263 }
264
265 // the face it enters the box through
266 float t = -k_infinity;
267 int axis = -1;
268 for (int candidate = 0; candidate < 3; ++candidate) {
269 if (entry_times[candidate] > t) {
270 t = entry_times[candidate];
271 axis = candidate;
272 }
273 }
274
275 // axes the ray crosses a plane of at t
276 uint32_t crossed = 0;
277 if (t < 0.0f) {
278 t = 0.0f;
279 axis = -1;
280 } else {
281 for (int candidate = 0; candidate < 3; ++candidate) {
282 if (sign[candidate] != 0.0f && entry_times[candidate] >= t - _detail::rayTieSlack(t)) {
283 crossed |= 1u << candidate;
284 }
285 }
286 }
287
288 // moves t to where the ray leaves the cell
289 const auto leave = [&](glm::ivec3 cell_min, int32_t size) -> bool {
290 std::array<float, 3> plane_times {k_infinity, k_infinity, k_infinity};
291 float next = k_infinity;
292 int next_axis = axis;
293 for (int candidate = 0; candidate < 3; ++candidate) {
294 if (sign[candidate] == 0.0f) {
295 continue;
296 }
297 const float plane = static_cast<float>(sign[candidate] > 0.0f ? cell_min[candidate] + size : cell_min[candidate]);
298 plane_times[candidate] = (plane - start[candidate]) * inverse[candidate];
299 if (plane_times[candidate] < next) {
300 next = plane_times[candidate];
301 next_axis = candidate;
302 }
303 }
304 if (!std::isfinite(next)) {
305 return false;
306 }
307
308 crossed = 0;
309 for (int candidate = 0; candidate < 3; ++candidate) {
310 if (plane_times[candidate] <= next + _detail::rayTieSlack(next)) {
311 crossed |= 1u << candidate;
312 }
313 }
314 t = std::max(next, t);
315 axis = next_axis;
316 return true;
317 };
318
319 const auto collides = [&](uint8_t palette_index) {
320 const uint32_t material = voxel::resolveMaterialIndex(context.palette, context.materials, palette_index);
321 return context.materials.materials[material].collides();
322 };
323
324 // whatever happens a ray cannot cross more cells than these
325 const int32_t max_steps = (3 * (dims.x + dims.y + dims.z)) + 16;
326 bool in_solid = false;
327
328 for (int32_t step = 0; step < max_steps; ++step) {
329 if (t > std::min(t_exit, limit)) {
330 return;
331 }
332
333 const glm::vec3 position = start + (direction * t);
334 glm::ivec3 cell;
335 for (int candidate = 0; candidate < 3; ++candidate) {
336 if ((crossed & (1u << candidate)) != 0) {
337 const auto plane = static_cast<int32_t>(std::lround(position[candidate]));
338 cell[candidate] = sign[candidate] > 0.0f ? plane : plane - 1;
339 } else if (sign[candidate] < 0.0f) {
340 // a ray that is going down is in the cell below it
341 cell[candidate] = static_cast<int32_t>(std::ceil(position[candidate])) - 1;
342 } else {
343 cell[candidate] = static_cast<int32_t>(std::floor(position[candidate]));
344 }
345 }
346 if (glm::any(glm::lessThan(cell, glm::ivec3(0))) || glm::any(glm::greaterThanEqual(cell, dims))) {
347 return;
348 }
349
350 const glm::ivec3 brick = cell >> 3;
351 const glm::ivec3 brick_min = brick * k_brick;
352 const voxel::BrickEntry entry = volume.entryAt(brick);
353
354 if (entry.tag() == voxel::BrickTag::empty) {
355 in_solid = false;
356 if (!leave(brick_min, k_brick)) {
357 return;
358 }
359 continue;
360 }
361
362 // A uniform brick is one material all through
363 const bool uniform = entry.tag() == voxel::BrickTag::uniform;
364 uint8_t palette_index = voxel::k_empty_palette_index;
365 if (uniform) {
366 palette_index = static_cast<uint8_t>(entry.payload());
367 } else {
368 const voxel::BrickOccupancy* occupancy = volume.occupancyPointer(brick);
369 const glm::uvec3 local = glm::uvec3(cell - brick_min);
370 if (occupancy != nullptr && voxel::isSolid(*occupancy, local.x, local.y, local.z)) {
371 palette_index = volume.materialAt(cell);
372 }
373 }
374
375 const bool solid = palette_index != voxel::k_empty_palette_index && (hit_passable || collides(palette_index));
376 if (solid && !in_solid) {
377 glm::vec3 normal = -direction;
378 if (axis >= 0) {
379 normal = glm::vec3(0.0f);
380 normal[axis] = -sign[axis];
381 }
382 const float reach =
383 on_entry(VoxelRayEntry {.distance = t * k_voxel, .normal = normal, .voxel = cell, .palette_index = palette_index});
384 limit = std::min(limit, reach / k_voxel);
385 }
386 in_solid = solid;
387
388 if (!(uniform ? leave(brick_min, k_brick) : leave(cell, 1))) {
389 return;
390 }
391 }
392}
393
394}
Axis-aligned bounds node.
Keyed by brick slot not pool id since uniform bricks have no pool id and shared bricks differ per ins...
Definition surface.hpp:174
Definition voxel_volume.hpp:21
auto occupancyPointer(glm::ivec3 brick) const noexcept -> const BrickOccupancy *
Null when the brick holds nothing.
Definition voxel_volume.cpp:169
auto resolveMaterialIndex(const Palette &palette, const MaterialLibrary &library, uint8_t palette_index) noexcept -> uint32_t
The library must not be empty.
Definition palette.hpp:149
Definition aabb.hpp:16
Definition voxel_query.hpp:32
Definition voxel_query.hpp:25
Definition voxel_query.hpp:203
Definition voxel_constants.hpp:44
constexpr auto payload() const noexcept -> uint32_t
Definition voxel_constants.hpp:59
One 64-bit word per z-slice with bit y * 8 + x.
Definition brick.hpp:18
Definition palette.hpp:143
Entry 0 is the empty voxel and must stay all zero.
Definition palette.hpp:59
Definition palette.hpp:94
uint16_t local_index
localIndex which is also the occupancy bit index
Definition surface.hpp:121
@ uniform
The payload is a palette index not a brick id.
Definition voxel_constants.hpp:34
void queryVoxelRay(const VoxelQueryContext &context, const AABB &bounds, glm::vec3 origin, glm::vec3 direction, float max_distance, bool hit_passable, OnEntry &&on_entry)
Definition voxel_query.hpp:227