MXVK Vulkan Framework 0.24.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
room.cpp
Go to the documentation of this file.
1#include <SDL3/SDL.h>
2#include <algorithm>
3#include <array>
4#include <charconv>
5#include <chrono>
6#include <cmath>
7#include <cctype>
8#include <cstdint>
9#include <cstdlib>
10#include <cstring>
11#include <filesystem>
12#include <format>
13#include <fstream>
14#include <glm/ext/matrix_clip_space.hpp>
15#include <glm/ext/matrix_transform.hpp>
16#include <glm/glm.hpp>
17#include <iostream>
18#include <random>
19#include <string>
20#include <string_view>
21#include <vector>
22
23#include "mxvk/argz.hpp"
24#include "mxvk/mxvk.hpp"
28#include "mxvk/mxvk_png.hpp"
30
31namespace walk {
32
33 struct WallSegment {
34 glm::vec3 start{0.0f};
35 glm::vec3 end{0.0f};
36 float height = 5.0f;
37 };
38
39 struct PillarInstance {
40 glm::vec3 position{0.0f};
41 float radius = 1.0f;
42 float height = 4.0f;
43 };
44
45 struct Collectible {
46 enum class Type {
49 };
50
52 glm::vec3 position{0.0f};
53 glm::vec3 hitCenterOffset{0.0f};
54 glm::vec3 rotation{0.0f};
55 glm::vec3 scale{1.0f};
56 float rotationSpeed = 12.0f;
57 float radius = 1.0f;
58 bool active = true;
59 };
60
61 struct Projectile {
62 struct TrailPoint {
63 glm::vec3 position{0.0f};
64 float lifetime = 0.0f;
65 float maxLifetime = 0.5f;
66 };
67
68 glm::vec3 position{0.0f};
69 glm::vec3 direction{0.0f, 0.0f, -1.0f};
70 float speed = 100.0f;
71 float lifetime = 0.0f;
72 float maxLifetime = 10.0f;
73 float distanceTraveled = 0.0f;
74 float maxDistance = 160.0f;
75 bool active = true;
76 std::vector<TrailPoint> trail{};
77 float trailTimer = 0.0f;
78 };
79
80 struct ExplosionParticle {
81 glm::vec3 position{0.0f};
82 glm::vec3 velocity{0.0f};
83 glm::vec3 color{1.0f, 0.5f, 0.2f};
84 float lifetime = 0.0f;
85 float maxLifetime = 0.55f;
86 float size = 0.08f;
87 bool active = true;
88 };
89
90 struct ParticlePointVertex {
91 glm::vec3 pos{0.0f};
92 glm::vec4 color{1.0f};
93 float size = 8.0f;
94 };
95
96 class MazeWorld {
97 public:
98 [[nodiscard]] glm::vec3 startPosition() const noexcept { return startPositionValue; }
99
100 [[nodiscard]] const std::vector<WallSegment> &walls() const noexcept { return wallSegments; }
101
102 [[nodiscard]] const std::vector<PillarInstance> &pillars() const noexcept { return pillarInstances; }
103
104 [[nodiscard]] std::vector<Collectible> &collectibles() noexcept { return collectibleItems; }
105
106 [[nodiscard]] const std::vector<Collectible> &collectibles() const noexcept { return collectibleItems; }
107
108 [[nodiscard]] int activeCollectibles() const {
109 int count = 0;
110 for (const Collectible &obj : collectibleItems) {
111 if (obj.active) {
112 ++count;
113 }
114 }
115 return count;
116 }
117
118 void generate(uint32_t seed) {
119 std::mt19937 rng(seed);
120 generateMaze(rng);
121 generatePillars(rng);
122 // Rescue: if the player start happens to land inside a pillar, push the
123 // start point to the safest unoccupied spot in the same cell.
124 if (checkPillarCollision(startPositionValue, 0.6f)) {
125 for (int attempt = 0; attempt < 64; ++attempt) {
126 const glm::vec3 candidate = randomPointInCell(startCellX, startCellZ, 0.6f, eyeHeight, rng, 0.5f);
127 if (!checkWallCollision(candidate, 0.5f) && !checkPillarCollision(candidate, 0.6f)) {
128 startPositionValue = candidate;
129 break;
130 }
131 }
132 }
133 generateCollectibles(rng);
134 }
135
136 [[nodiscard]] bool checkWallCollision(const glm::vec3 &position, float radius) const {
137 const float halfThickness = wallThicknessValue * 0.5f;
138 const float hitRadius = radius + halfThickness;
139 for (const WallSegment &wall : wallSegments) {
140 const glm::vec3 segment = wall.end - wall.start;
141 const float segmentLength = glm::length(segment);
142 if (segmentLength < 0.0001f) {
143 continue;
144 }
145
146 const glm::vec3 segmentDir = segment / segmentLength;
147 const glm::vec3 toPoint = position - wall.start;
148 const float projection = glm::clamp(glm::dot(toPoint, segmentDir), 0.0f, segmentLength);
149 glm::vec3 closest = wall.start + (segmentDir * projection);
150 closest.y = position.y;
151 if (glm::length(position - closest) < hitRadius) {
152 return true;
153 }
154 }
155 return false;
156 }
157
158 [[nodiscard]] bool checkPillarCollision(const glm::vec3 &position, float playerRadius) const {
159 for (const PillarInstance &pillar : pillarInstances) {
160 const glm::vec2 player2d(position.x, position.z);
161 const glm::vec2 pillar2d(pillar.position.x, pillar.position.z);
162 if (glm::length(player2d - pillar2d) < (playerRadius + pillar.radius)) {
163 return true;
164 }
165 }
166 return false;
167 }
168
169 [[nodiscard]] bool checkCollectibleCollision(const glm::vec3 &point, size_t &indexOut) const {
170 for (size_t i = 0; i < collectibleItems.size(); ++i) {
171 if (!collectibleItems[i].active) {
172 continue;
173 }
174 const Collectible &collectible = collectibleItems[i];
175 const glm::vec3 center = collectible.position + collectible.hitCenterOffset;
176 if (collectible.type == Collectible::Type::Bird) {
177 const glm::vec3 delta = point - center;
178 const float halfSide = collectible.radius;
179 if (std::abs(delta.x) <= halfSide &&
180 std::abs(delta.y) <= halfSide &&
181 std::abs(delta.z) <= halfSide) {
182 indexOut = i;
183 return true;
184 }
185 continue;
186 }
187
188 if (glm::length(center - point) < collectible.radius) {
189 indexOut = i;
190 return true;
191 }
192 }
193 return false;
194 }
195
196 [[nodiscard]] glm::vec3 randomPointInCell(int cellX, int cellZ, float objectRadius, float y, std::mt19937 &rng, float margin) const {
197 const float pad = objectRadius + wallThicknessValue * 0.5f + margin;
198 const float x0 = -size + static_cast<float>(cellX) * cellSize;
199 const float z0 = -size + static_cast<float>(cellZ) * cellSize;
200 const float x1 = x0 + cellSize;
201 const float z1 = z0 + cellSize;
202
203 float minX = x0 + pad;
204 float maxX = x1 - pad;
205 float minZ = z0 + pad;
206 float maxZ = z1 - pad;
207 if (minX > maxX) {
208 minX = maxX = (x0 + x1) * 0.5f;
209 }
210 if (minZ > maxZ) {
211 minZ = maxZ = (z0 + z1) * 0.5f;
212 }
213
214 std::uniform_real_distribution<float> distX(minX, maxX);
215 std::uniform_real_distribution<float> distZ(minZ, maxZ);
216 return glm::vec3(distX(rng), y, distZ(rng));
217 }
218
219 [[nodiscard]] float wallThickness() const noexcept { return wallThicknessValue; }
220
221 private:
222 struct Cell {
223 bool visited = false;
224 std::array<bool, 4> walls{true, true, true, true};
225 };
226
227 void generateMaze(std::mt19937 &rng) {
228 const int gridX = mazeGridX;
229 const int gridZ = mazeGridZ;
230 cellSize = (size * 2.0f) / static_cast<float>(gridX);
231
232 auto indexFor = [gridX](int x, int z) {
233 return z * gridX + x;
234 };
235
236 std::vector<Cell> grid(static_cast<size_t>(gridX * gridZ));
237 std::vector<std::pair<int, int>> stack;
238 stack.emplace_back(0, 0);
239 grid[static_cast<size_t>(indexFor(0, 0))].visited = true;
240
241 while (!stack.empty()) {
242 const auto [x, z] = stack.back();
243 std::vector<int> dirs;
244 if (z > 0 && !grid[static_cast<size_t>(indexFor(x, z - 1))].visited) {
245 dirs.push_back(0);
246 }
247 if (x < (gridX - 1) && !grid[static_cast<size_t>(indexFor(x + 1, z))].visited) {
248 dirs.push_back(1);
249 }
250 if (z < (gridZ - 1) && !grid[static_cast<size_t>(indexFor(x, z + 1))].visited) {
251 dirs.push_back(2);
252 }
253 if (x > 0 && !grid[static_cast<size_t>(indexFor(x - 1, z))].visited) {
254 dirs.push_back(3);
255 }
256
257 if (dirs.empty()) {
258 stack.pop_back();
259 continue;
260 }
261
262 std::uniform_int_distribution<size_t> pick(0, dirs.size() - 1U);
263 const int d = dirs[pick(rng)];
264 int nx = x;
265 int nz = z;
266 if (d == 0) {
267 nz = z - 1;
268 } else if (d == 1) {
269 nx = x + 1;
270 } else if (d == 2) {
271 nz = z + 1;
272 } else {
273 nx = x - 1;
274 }
275
276 grid[static_cast<size_t>(indexFor(x, z))].walls[static_cast<size_t>(d)] = false;
277 grid[static_cast<size_t>(indexFor(nx, nz))].walls[static_cast<size_t>((d + 2) % 4)] = false;
278 grid[static_cast<size_t>(indexFor(nx, nz))].visited = true;
279 stack.emplace_back(nx, nz);
280 }
281
282 wallSegments.clear();
283 for (int z = 0; z < gridZ; ++z) {
284 for (int x = 0; x < gridX; ++x) {
285 const float cx = -size + static_cast<float>(x) * cellSize;
286 const float cz = -size + static_cast<float>(z) * cellSize;
287 const float x0 = cx;
288 const float z0 = cz;
289 const float x1 = cx + cellSize;
290 const float z1 = cz + cellSize;
291 const Cell &cell = grid[static_cast<size_t>(indexFor(x, z))];
292
293 if (cell.walls[0]) {
294 wallSegments.push_back({glm::vec3(x0, 0.0f, z0), glm::vec3(x1, 0.0f, z0), wallHeight});
295 }
296 if (cell.walls[3]) {
297 wallSegments.push_back({glm::vec3(x0, 0.0f, z1), glm::vec3(x0, 0.0f, z0), wallHeight});
298 }
299 if (x == (gridX - 1) && cell.walls[1]) {
300 wallSegments.push_back({glm::vec3(x1, 0.0f, z0), glm::vec3(x1, 0.0f, z1), wallHeight});
301 }
302 if (z == (gridZ - 1) && cell.walls[2]) {
303 wallSegments.push_back({glm::vec3(x1, 0.0f, z1), glm::vec3(x0, 0.0f, z1), wallHeight});
304 }
305 }
306 }
307 mergeContiguousWalls();
308
309 const float playerRadius = 0.5f;
310 startCellX = 0;
311 startCellZ = 0;
312 startPositionValue = randomPointInCell(0, 0, playerRadius, eyeHeight, rng, 0.5f);
313 if (checkWallCollision(startPositionValue, playerRadius)) {
314 for (int z = 0; z < mazeGridZ; ++z) {
315 for (int x = 0; x < mazeGridX; ++x) {
316 startPositionValue = randomPointInCell(x, z, playerRadius, eyeHeight, rng, 0.5f);
317 if (!checkWallCollision(startPositionValue, playerRadius)) {
318 startCellX = x;
319 startCellZ = z;
320 return;
321 }
322 }
323 }
324 }
325 }
326
327 void mergeContiguousWalls() {
328 if (wallSegments.empty()) {
329 return;
330 }
331
332 struct NormalizedWall {
333 bool horizontal = false;
334 float constantAxis = 0.0f;
335 float startAxis = 0.0f;
336 float endAxis = 0.0f;
337 float height = 0.0f;
338 };
339
340 constexpr float epsilon = 0.0001f;
341 constexpr float adjacencyEpsilon = 0.001f;
342 constexpr float quantizeScale = 1000.0f;
343
344 const auto quantize = [](float value) {
345 return static_cast<int>(std::lround(value * quantizeScale));
346 };
347
348 std::vector<NormalizedWall> normalized;
349 normalized.reserve(wallSegments.size());
350 for (const WallSegment &wall : wallSegments) {
351 const bool horizontal = std::abs(wall.start.z - wall.end.z) <= epsilon;
352 if (horizontal) {
353 const float x0 = std::min(wall.start.x, wall.end.x);
354 const float x1 = std::max(wall.start.x, wall.end.x);
355 normalized.push_back({true, wall.start.z, x0, x1, wall.height});
356 } else {
357 const float z0 = std::min(wall.start.z, wall.end.z);
358 const float z1 = std::max(wall.start.z, wall.end.z);
359 normalized.push_back({false, wall.start.x, z0, z1, wall.height});
360 }
361 }
362
363 std::sort(normalized.begin(), normalized.end(), [&quantize](const NormalizedWall &a, const NormalizedWall &b) {
364 const auto keyA = std::array<int, 3>{a.horizontal ? 1 : 0, quantize(a.constantAxis), quantize(a.height)};
365 const auto keyB = std::array<int, 3>{b.horizontal ? 1 : 0, quantize(b.constantAxis), quantize(b.height)};
366 if (keyA != keyB) {
367 return keyA < keyB;
368 }
369 if (a.startAxis != b.startAxis) {
370 return a.startAxis < b.startAxis;
371 }
372 return a.endAxis < b.endAxis;
373 });
374
375 std::vector<WallSegment> merged;
376 merged.reserve(normalized.size());
377 size_t index = 0;
378 while (index < normalized.size()) {
379 const NormalizedWall first = normalized[index];
380 float runStart = first.startAxis;
381 float runEnd = first.endAxis;
382
383 size_t next = index + 1;
384 while (next < normalized.size()) {
385 const NormalizedWall &candidate = normalized[next];
386 if (candidate.horizontal != first.horizontal || quantize(candidate.constantAxis) != quantize(first.constantAxis) || quantize(candidate.height) != quantize(first.height)) {
387 break;
388 }
389
390 if (candidate.startAxis <= (runEnd + adjacencyEpsilon)) {
391 runEnd = std::max(runEnd, candidate.endAxis);
392 ++next;
393 continue;
394 }
395
396 if (first.horizontal) {
397 merged.push_back({glm::vec3(runStart, 0.0f, first.constantAxis), glm::vec3(runEnd, 0.0f, first.constantAxis), first.height});
398 } else {
399 merged.push_back({glm::vec3(first.constantAxis, 0.0f, runStart), glm::vec3(first.constantAxis, 0.0f, runEnd), first.height});
400 }
401 runStart = candidate.startAxis;
402 runEnd = candidate.endAxis;
403 ++next;
404 }
405
406 if (first.horizontal) {
407 merged.push_back({glm::vec3(runStart, 0.0f, first.constantAxis), glm::vec3(runEnd, 0.0f, first.constantAxis), first.height});
408 } else {
409 merged.push_back({glm::vec3(first.constantAxis, 0.0f, runStart), glm::vec3(first.constantAxis, 0.0f, runEnd), first.height});
410 }
411 index = next;
412 }
413
414 wallSegments.swap(merged);
415 }
416
417 void generatePillars(std::mt19937 &rng) {
418 pillarInstances.clear();
419 std::uniform_real_distribution<float> radiusDist(0.5f, 1.5f);
420 std::uniform_real_distribution<float> heightDist(3.0f, 6.0f);
421
422 constexpr int targetPillars = 15;
423 constexpr int maxAttempts = targetPillars * 8;
424 int created = 0;
425 for (int attempt = 0; attempt < maxAttempts && created < targetPillars; ++attempt) {
426 const int cellX = static_cast<int>(rng() % static_cast<uint32_t>(mazeGridX));
427 const int cellZ = static_cast<int>(rng() % static_cast<uint32_t>(mazeGridZ));
428 // Don't drop pillars on top of where the player spawns.
429 if (cellX == startCellX && cellZ == startCellZ) {
430 continue;
431 }
432 PillarInstance pillar{};
433 pillar.radius = radiusDist(rng);
434 pillar.height = heightDist(rng);
435 pillar.position = randomPointInCell(cellX, cellZ, pillar.radius, 0.0f, rng, 0.3f);
436 if (!checkWallCollision(pillar.position, pillar.radius)) {
437 pillarInstances.push_back(pillar);
438 ++created;
439 }
440 }
441 }
442
443 void generateCollectibles(std::mt19937 &rng) {
444 collectibleItems.clear();
445 const int usableCells = std::max(1, (mazeGridX * mazeGridZ) - 1);
446 const int targetCollectibles = usableCells * collectiblesPerCell;
447 collectibleItems.reserve(static_cast<size_t>(targetCollectibles));
448
449 std::vector<Collectible::Type> types;
450 types.reserve(static_cast<size_t>(targetCollectibles));
451 const int saturnCount = targetCollectibles / 2;
452 for (int i = 0; i < saturnCount; ++i) {
453 types.push_back(Collectible::Type::Saturn);
454 }
455 for (int i = saturnCount; i < targetCollectibles; ++i) {
456 types.push_back(Collectible::Type::Bird);
457 }
458 std::shuffle(types.begin(), types.end(), rng);
459
460 std::uniform_real_distribution<float> saturnScale(0.4f, 0.8f);
461 std::uniform_real_distribution<float> saturnRotSpeed(5.0f, 15.0f);
462 std::uniform_real_distribution<float> birdScale(0.3f, 0.5f);
463 std::uniform_real_distribution<float> birdRotSpeed(20.0f, 60.0f);
464
465 int typeIndex = 0;
466 for (int cellZ = 0; cellZ < mazeGridZ; ++cellZ) {
467 for (int cellX = 0; cellX < mazeGridX; ++cellX) {
468 if (cellX == startCellX && cellZ == startCellZ) {
469 continue;
470 }
471 for (int slot = 0; slot < collectiblesPerCell; ++slot) {
472 if (typeIndex >= targetCollectibles) {
473 break;
474 }
475 Collectible obj{};
476 obj.type = types[static_cast<size_t>(typeIndex)];
477 if (obj.type == Collectible::Type::Saturn) {
478 const float scale = saturnScale(rng);
479 obj.scale = glm::vec3(scale);
480 obj.rotationSpeed = saturnRotSpeed(rng);
481 obj.radius = 2.0f * scale;
482 } else {
483 const float scale = birdScale(rng);
484 obj.scale = glm::vec3(scale);
485 obj.rotationSpeed = birdRotSpeed(rng);
486 obj.radius = 0.5f * scale;
487 }
488
489 bool foundSpot = false;
490 glm::vec3 fallback = glm::vec3(0.0f, (obj.type == Collectible::Type::Bird) ? obj.radius : 2.5f, 0.0f);
491 for (int attempt = 0; attempt < 24 && !foundSpot; ++attempt) {
492 const float y = (obj.type == Collectible::Type::Bird) ? obj.radius : 2.5f;
493 const float margin = (attempt < 18) ? 0.45f : 0.10f;
494 const glm::vec3 candidate = randomPointInCell(cellX, cellZ, obj.radius, y, rng, margin);
495 fallback = candidate;
496
497 bool overlapsOtherCollectible = false;
498 for (const Collectible &placed : collectibleItems) {
499 const float separation = std::max(5.0f, placed.radius + obj.radius + 0.2f);
500 if (glm::length(placed.position - candidate) < separation) {
501 overlapsOtherCollectible = true;
502 break;
503 }
504 }
505
506 if (!overlapsOtherCollectible && !checkWallCollision(candidate, obj.radius) && !checkPillarCollision(candidate, obj.radius)) {
507 obj.position = candidate;
508 foundSpot = true;
509 }
510 }
511
512 if (!foundSpot) {
513 // Keep spawn count fixed: use the last in-cell candidate as a fallback.
514 obj.position = fallback;
515 }
516 collectibleItems.push_back(obj);
517 ++typeIndex;
518 }
519 }
520 }
521 }
522
523 std::vector<WallSegment> wallSegments{};
524 std::vector<PillarInstance> pillarInstances{};
525 std::vector<Collectible> collectibleItems{};
526
527 float size = 50.0f;
528 float wallHeight = 5.0f;
529 float wallThicknessValue = 0.5f;
530 int mazeGridX = 6;
531 int mazeGridZ = 6;
532 int collectiblesPerCell = 1;
533 float cellSize = 0.0f;
534 float eyeHeight = 1.7f;
535 int startCellX = 0;
536 int startCellZ = 0;
537 glm::vec3 startPositionValue{0.0f, 1.7f, 0.0f};
538 };
539
540 class RawPillarRenderer {
541 public:
542 struct PillarVertex {
543 glm::vec3 position{0.0f};
544 glm::vec2 texCoord{0.0f};
545 glm::vec3 normal{0.0f};
546 };
547
548 struct PillarUniforms {
549 glm::mat4 view{1.0f};
550 glm::mat4 proj{1.0f};
551 glm::vec4 fx{0.0f};
552 };
553
554 void load(mxvk::VK_Window *targetWindow,
555 const std::string &textureManifestPath,
556 const std::string &textureBasePath,
557 const std::vector<char> &vertSpv,
558 const std::vector<char> &fragSpv) {
559 if (targetWindow == nullptr) {
560 throw mxvk::Exception("walk: raw pillar renderer requires a valid window");
561 }
562 window = targetWindow;
563 vertexSpv = vertSpv;
564 fragmentSpv = fragSpv;
565
566 if (!window->ensureRenderResources()) {
567 throw mxvk::Exception("walk: raw pillar renderer requires render resources");
568 }
569
570 buildGeometry();
571 loadTexture(textureManifestPath, textureBasePath);
572 createTextureSampler();
573 createDescriptorSetLayout();
574 createUniformBuffers();
575 createDescriptorPool();
576 createDescriptorSets();
577 createPipeline();
578 }
579
580 void resize(mxvk::VK_Window *targetWindow) {
581 if (targetWindow == nullptr || targetWindow->getDevice() == VK_NULL_HANDLE) {
582 return;
583 }
584
585 window = targetWindow;
586 destroyPipeline();
587 destroyDescriptors();
588 createDescriptorSetLayout();
589 createUniformBuffers();
590 createDescriptorPool();
591 createDescriptorSets();
592 createPipeline();
593 }
594
595 /// @brief Hot-swap the fragment shader without rebuilding geometry or descriptors.
596 /// @param newFragSpv Compiled SPIR-V bytecode for the new fragment shader.
597 void reloadFragShader(const std::vector<char> &newFragSpv) {
598 if (window == nullptr || window->getDevice() == VK_NULL_HANDLE || newFragSpv.empty()) {
599 return;
600 }
601 vkDeviceWaitIdle(window->getDevice());
602 fragmentSpv = newFragSpv;
603 destroyPipeline();
604 createPipeline();
605 }
606
607 void cleanup(mxvk::VK_Window *targetWindow) {
608 if (targetWindow == nullptr || targetWindow->getDevice() == VK_NULL_HANDLE) {
609 return;
610 }
611
612 window = targetWindow;
613 destroyPipeline();
614 destroyDescriptors();
615 destroyTexture();
616 destroyBuffers();
617 window = nullptr;
618 }
619
620 void render(VkCommandBuffer cmd,
621 uint32_t imageIndex,
622 const std::vector<PillarInstance> &pillars,
623 const glm::mat4 &view,
624 const glm::mat4 &proj,
625 const glm::vec4 &fx) {
626 if (cmd == VK_NULL_HANDLE || pipeline == VK_NULL_HANDLE || pipelineLayout == VK_NULL_HANDLE) {
627 return;
628 }
629 if (imageIndex >= uniformBuffersMapped.size() || descriptorSets.empty() || vertexBuffer == VK_NULL_HANDLE || indexBuffer == VK_NULL_HANDLE) {
630 return;
631 }
632
633 PillarUniforms uniforms{};
634 uniforms.view = view;
635 uniforms.proj = proj;
636 uniforms.fx = fx;
637 std::memcpy(uniformBuffersMapped[imageIndex], &uniforms, sizeof(PillarUniforms));
638
639 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
640 vkCmdBindDescriptorSets(cmd,
641 VK_PIPELINE_BIND_POINT_GRAPHICS,
642 pipelineLayout,
643 0,
644 1,
645 &descriptorSets[imageIndex],
646 0,
647 nullptr);
648
649 const VkDeviceSize offset = 0;
650 vkCmdBindVertexBuffers(cmd, 0, 1, &vertexBuffer, &offset);
651 vkCmdBindIndexBuffer(cmd, indexBuffer, 0, VK_INDEX_TYPE_UINT32);
652
653 for (const PillarInstance &pillar : pillars) {
654 // Vertex data is defined with Y in [0..1] (base at 0.0, top at 1.0).
655 // To avoid z-fighting with the floor, sink the base slightly into the floor
656 // and place the translation at the pillar base Y.
657 constexpr float baseSink = 0.02f;
658 glm::mat4 model = glm::translate(glm::mat4(1.0f), glm::vec3(pillar.position.x, pillar.position.y - baseSink, pillar.position.z));
659 model = glm::scale(model, glm::vec3(pillar.radius, pillar.height, pillar.radius));
660 vkCmdPushConstants(cmd, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(glm::mat4), &model);
661 vkCmdDrawIndexed(cmd, indexCount, 1, 0, 0, 0);
662 }
663 }
664
665 private:
666 [[nodiscard]] uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) const {
667 VkPhysicalDeviceMemoryProperties memProperties{};
668 vkGetPhysicalDeviceMemoryProperties(window->getPhysicalDevice(), &memProperties);
669 for (uint32_t i = 0; i < memProperties.memoryTypeCount; ++i) {
670 if ((typeFilter & (1u << i)) != 0u && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) {
671 return i;
672 }
673 }
674 throw mxvk::Exception("walk: failed to find suitable memory type for raw pillar renderer");
675 }
676
677 void createBuffer(VkDeviceSize size,
678 VkBufferUsageFlags usage,
679 VkMemoryPropertyFlags properties,
680 VkBuffer &buffer,
681 VkDeviceMemory &bufferMemory) const {
682 VkBufferCreateInfo bufferInfo{};
683 bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
684 bufferInfo.size = size;
685 bufferInfo.usage = usage;
686 bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
687
688 if (vkCreateBuffer(window->getDevice(), &bufferInfo, nullptr, &buffer) != VK_SUCCESS) {
689 throw mxvk::Exception("walk: failed to create raw pillar buffer");
690 }
691
692 VkMemoryRequirements requirements{};
693 vkGetBufferMemoryRequirements(window->getDevice(), buffer, &requirements);
694
695 VkMemoryAllocateInfo allocInfo{};
696 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
697 allocInfo.allocationSize = requirements.size;
698
699 try {
700 allocInfo.memoryTypeIndex = findMemoryType(requirements.memoryTypeBits, properties);
701 if (vkAllocateMemory(window->getDevice(), &allocInfo, nullptr, &bufferMemory) != VK_SUCCESS) {
702 throw mxvk::Exception("walk: failed to allocate raw pillar buffer memory");
703 }
704
705 if (vkBindBufferMemory(window->getDevice(), buffer, bufferMemory, 0) != VK_SUCCESS) {
706 throw mxvk::Exception("walk: failed to bind raw pillar buffer memory");
707 }
708 } catch (...) {
709 if (bufferMemory != VK_NULL_HANDLE) {
710 vkFreeMemory(window->getDevice(), bufferMemory, nullptr);
711 bufferMemory = VK_NULL_HANDLE;
712 }
713 if (buffer != VK_NULL_HANDLE) {
714 vkDestroyBuffer(window->getDevice(), buffer, nullptr);
715 buffer = VK_NULL_HANDLE;
716 }
717 throw;
718 }
719 }
720
721 void createImage(uint32_t width,
722 uint32_t height,
723 VkFormat format,
724 VkImageTiling tiling,
725 VkImageUsageFlags usage,
726 VkMemoryPropertyFlags properties,
727 VkImage &image,
728 VkDeviceMemory &memory) const {
729 VkImageCreateInfo imageInfo{};
730 imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
731 imageInfo.imageType = VK_IMAGE_TYPE_2D;
732 imageInfo.extent.width = width;
733 imageInfo.extent.height = height;
734 imageInfo.extent.depth = 1;
735 imageInfo.mipLevels = 1;
736 imageInfo.arrayLayers = 1;
737 imageInfo.format = format;
738 imageInfo.tiling = tiling;
739 imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
740 imageInfo.usage = usage;
741 imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
742 imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
743
744 if (vkCreateImage(window->getDevice(), &imageInfo, nullptr, &image) != VK_SUCCESS) {
745 throw mxvk::Exception("walk: failed to create raw pillar image");
746 }
747
748 VkMemoryRequirements requirements{};
749 vkGetImageMemoryRequirements(window->getDevice(), image, &requirements);
750
751 VkMemoryAllocateInfo allocInfo{};
752 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
753 allocInfo.allocationSize = requirements.size;
754
755 try {
756 allocInfo.memoryTypeIndex = findMemoryType(requirements.memoryTypeBits, properties);
757 if (vkAllocateMemory(window->getDevice(), &allocInfo, nullptr, &memory) != VK_SUCCESS) {
758 throw mxvk::Exception("walk: failed to allocate raw pillar image memory");
759 }
760
761 if (vkBindImageMemory(window->getDevice(), image, memory, 0) != VK_SUCCESS) {
762 throw mxvk::Exception("walk: failed to bind raw pillar image memory");
763 }
764 } catch (...) {
765 if (memory != VK_NULL_HANDLE) {
766 vkFreeMemory(window->getDevice(), memory, nullptr);
767 memory = VK_NULL_HANDLE;
768 }
769 if (image != VK_NULL_HANDLE) {
770 vkDestroyImage(window->getDevice(), image, nullptr);
771 image = VK_NULL_HANDLE;
772 }
773 throw;
774 }
775 }
776
777 VkImageView createImageView(VkImage image, VkFormat format, VkImageAspectFlags aspectFlags) const {
778 VkImageViewCreateInfo viewInfo{};
779 viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
780 viewInfo.image = image;
781 viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
782 viewInfo.format = format;
783 viewInfo.subresourceRange.aspectMask = aspectFlags;
784 viewInfo.subresourceRange.baseMipLevel = 0;
785 viewInfo.subresourceRange.levelCount = 1;
786 viewInfo.subresourceRange.baseArrayLayer = 0;
787 viewInfo.subresourceRange.layerCount = 1;
788
789 VkImageView imageView = VK_NULL_HANDLE;
790 if (vkCreateImageView(window->getDevice(), &viewInfo, nullptr, &imageView) != VK_SUCCESS) {
791 throw mxvk::Exception("walk: failed to create raw pillar image view");
792 }
793 return imageView;
794 }
795
796 [[nodiscard]] VkCommandBuffer beginSingleTimeCommands() const {
797 VkCommandBufferAllocateInfo allocInfo{};
798 allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
799 allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
800 allocInfo.commandPool = window->getCommandPool();
801 allocInfo.commandBufferCount = 1;
802
803 VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
804 if (vkAllocateCommandBuffers(window->getDevice(), &allocInfo, &commandBuffer) != VK_SUCCESS) {
805 throw mxvk::Exception("walk: failed to allocate raw pillar command buffer");
806 }
807
808 VkCommandBufferBeginInfo beginInfo{};
809 beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
810 beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
811 if (vkBeginCommandBuffer(commandBuffer, &beginInfo) != VK_SUCCESS) {
812 vkFreeCommandBuffers(window->getDevice(), window->getCommandPool(), 1, &commandBuffer);
813 throw mxvk::Exception("walk: failed to begin raw pillar command buffer");
814 }
815
816 return commandBuffer;
817 }
818
819 void endSingleTimeCommands(VkCommandBuffer commandBuffer) const {
820 if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) {
821 vkFreeCommandBuffers(window->getDevice(), window->getCommandPool(), 1, &commandBuffer);
822 throw mxvk::Exception("walk: failed to end raw pillar command buffer");
823 }
824
825 VkSubmitInfo submitInfo{};
826 submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
827 submitInfo.commandBufferCount = 1;
828 submitInfo.pCommandBuffers = &commandBuffer;
829
830 if (vkQueueSubmit(window->getGraphicsQueue(), 1, &submitInfo, VK_NULL_HANDLE) != VK_SUCCESS) {
831 vkFreeCommandBuffers(window->getDevice(), window->getCommandPool(), 1, &commandBuffer);
832 throw mxvk::Exception("walk: failed to submit raw pillar command buffer");
833 }
834 if (vkQueueWaitIdle(window->getGraphicsQueue()) != VK_SUCCESS) {
835 vkFreeCommandBuffers(window->getDevice(), window->getCommandPool(), 1, &commandBuffer);
836 throw mxvk::Exception("walk: failed to wait for raw pillar upload queue");
837 }
838
839 vkFreeCommandBuffers(window->getDevice(), window->getCommandPool(), 1, &commandBuffer);
840 }
841
842 void transitionImageLayout(VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout) const {
843 VkCommandBuffer cmd = beginSingleTimeCommands();
844
845 VkImageMemoryBarrier barrier{};
846 barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
847 barrier.oldLayout = oldLayout;
848 barrier.newLayout = newLayout;
849 barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
850 barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
851 barrier.image = image;
852 barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
853 barrier.subresourceRange.baseMipLevel = 0;
854 barrier.subresourceRange.levelCount = 1;
855 barrier.subresourceRange.baseArrayLayer = 0;
856 barrier.subresourceRange.layerCount = 1;
857
858 VkPipelineStageFlags sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
859 VkPipelineStageFlags destinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
860 if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
861 barrier.srcAccessMask = 0;
862 barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
863 } else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
864 barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
865 barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
866 sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
867 destinationStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
868 }
869
870 vkCmdPipelineBarrier(cmd, sourceStage, destinationStage, 0, 0, nullptr, 0, nullptr, 1, &barrier);
871 endSingleTimeCommands(cmd);
872 }
873
874 void copyBufferToImage(VkBuffer buffer, VkImage image, uint32_t width, uint32_t height) const {
875 VkCommandBuffer cmd = beginSingleTimeCommands();
876 VkBufferImageCopy region{};
877 region.bufferOffset = 0;
878 region.bufferRowLength = 0;
879 region.bufferImageHeight = 0;
880 region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
881 region.imageSubresource.mipLevel = 0;
882 region.imageSubresource.baseArrayLayer = 0;
883 region.imageSubresource.layerCount = 1;
884 region.imageOffset = {0, 0, 0};
885 region.imageExtent = {width, height, 1};
886
887 vkCmdCopyBufferToImage(cmd, buffer, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
888 endSingleTimeCommands(cmd);
889 }
890
891 void createTextureSampler() {
892 if (textureSampler != VK_NULL_HANDLE) {
893 return;
894 }
895
896 VkPhysicalDeviceFeatures deviceFeatures{};
897 vkGetPhysicalDeviceFeatures(window->getPhysicalDevice(), &deviceFeatures);
898 VkPhysicalDeviceProperties deviceProperties{};
899 vkGetPhysicalDeviceProperties(window->getPhysicalDevice(), &deviceProperties);
900 const bool anisotropySupported = deviceFeatures.samplerAnisotropy == VK_TRUE;
901 const float anisotropyLevel = anisotropySupported
902 ? std::min(8.0f, deviceProperties.limits.maxSamplerAnisotropy)
903 : 1.0f;
904
905 VkSamplerCreateInfo samplerInfo{};
906 samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
907 samplerInfo.magFilter = VK_FILTER_LINEAR;
908 samplerInfo.minFilter = VK_FILTER_LINEAR;
909 samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
910 samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
911 samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
912 samplerInfo.anisotropyEnable = anisotropySupported ? VK_TRUE : VK_FALSE;
913 samplerInfo.maxAnisotropy = anisotropyLevel;
914 samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
915 samplerInfo.unnormalizedCoordinates = VK_FALSE;
916 samplerInfo.compareEnable = VK_FALSE;
917 samplerInfo.compareOp = VK_COMPARE_OP_ALWAYS;
918 samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
919
920 if (vkCreateSampler(window->getDevice(), &samplerInfo, nullptr, &textureSampler) != VK_SUCCESS) {
921 throw mxvk::Exception("walk: failed to create raw pillar texture sampler");
922 }
923 }
924
925 void createDescriptorSetLayout() {
926 if (descriptorSetLayout != VK_NULL_HANDLE) {
927 return;
928 }
929
930 VkDescriptorSetLayoutBinding samplerBinding{};
931 samplerBinding.binding = 0;
932 samplerBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
933 samplerBinding.descriptorCount = 1;
934 samplerBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
935
936 VkDescriptorSetLayoutBinding uboBinding{};
937 uboBinding.binding = 1;
938 uboBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
939 uboBinding.descriptorCount = 1;
940 uboBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
941
942 const std::array<VkDescriptorSetLayoutBinding, 2> bindings = {samplerBinding, uboBinding};
943
944 VkDescriptorSetLayoutCreateInfo layoutInfo{};
945 layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
946 layoutInfo.bindingCount = static_cast<uint32_t>(bindings.size());
947 layoutInfo.pBindings = bindings.data();
948
949 if (vkCreateDescriptorSetLayout(window->getDevice(), &layoutInfo, nullptr, &descriptorSetLayout) != VK_SUCCESS) {
950 throw mxvk::Exception("walk: failed to create raw pillar descriptor set layout");
951 }
952 }
953
954 void createUniformBuffers() {
955 destroyUniformBuffers();
956
957 const size_t frameCount = window->getSwapchainImageCount();
958 if (frameCount == 0) {
959 return;
960 }
961
962 uniformBuffers.resize(frameCount, VK_NULL_HANDLE);
963 uniformBufferMemory.resize(frameCount, VK_NULL_HANDLE);
964 uniformBuffersMapped.resize(frameCount, nullptr);
965
966 for (size_t i = 0; i < frameCount; ++i) {
967 createBuffer(sizeof(PillarUniforms),
968 VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
969 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
970 uniformBuffers[i],
971 uniformBufferMemory[i]);
972 vkMapMemory(window->getDevice(), uniformBufferMemory[i], 0, sizeof(PillarUniforms), 0, &uniformBuffersMapped[i]);
973 }
974 }
975
976 void createDescriptorPool() {
977 const uint32_t frameCount = static_cast<uint32_t>(window->getSwapchainImageCount());
978 std::array<VkDescriptorPoolSize, 2> poolSizes{};
979 poolSizes[0].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
980 poolSizes[0].descriptorCount = frameCount;
981 poolSizes[1].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
982 poolSizes[1].descriptorCount = frameCount;
983
984 VkDescriptorPoolCreateInfo poolInfo{};
985 poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
986 poolInfo.poolSizeCount = static_cast<uint32_t>(poolSizes.size());
987 poolInfo.pPoolSizes = poolSizes.data();
988 poolInfo.maxSets = frameCount;
989
990 if (vkCreateDescriptorPool(window->getDevice(), &poolInfo, nullptr, &descriptorPool) != VK_SUCCESS) {
991 throw mxvk::Exception("walk: failed to create raw pillar descriptor pool");
992 }
993 }
994
995 void createDescriptorSets() {
996 const size_t frameCount = window->getSwapchainImageCount();
997 std::vector<VkDescriptorSetLayout> layouts(frameCount, descriptorSetLayout);
998
999 VkDescriptorSetAllocateInfo allocInfo{};
1000 allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
1001 allocInfo.descriptorPool = descriptorPool;
1002 allocInfo.descriptorSetCount = static_cast<uint32_t>(frameCount);
1003 allocInfo.pSetLayouts = layouts.data();
1004
1005 descriptorSets.resize(frameCount, VK_NULL_HANDLE);
1006 if (vkAllocateDescriptorSets(window->getDevice(), &allocInfo, descriptorSets.data()) != VK_SUCCESS) {
1007 throw mxvk::Exception("walk: failed to allocate raw pillar descriptor sets");
1008 }
1009
1010 VkDescriptorImageInfo imageInfo{};
1011 imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
1012 imageInfo.imageView = textureView;
1013 imageInfo.sampler = textureSampler;
1014
1015 for (size_t i = 0; i < frameCount; ++i) {
1016 VkDescriptorBufferInfo bufferInfo{};
1017 bufferInfo.buffer = uniformBuffers[i];
1018 bufferInfo.offset = 0;
1019 bufferInfo.range = sizeof(PillarUniforms);
1020
1021 std::array<VkWriteDescriptorSet, 2> writes{};
1022 writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
1023 writes[0].dstSet = descriptorSets[i];
1024 writes[0].dstBinding = 0;
1025 writes[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
1026 writes[0].descriptorCount = 1;
1027 writes[0].pImageInfo = &imageInfo;
1028
1029 writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
1030 writes[1].dstSet = descriptorSets[i];
1031 writes[1].dstBinding = 1;
1032 writes[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
1033 writes[1].descriptorCount = 1;
1034 writes[1].pBufferInfo = &bufferInfo;
1035
1036 vkUpdateDescriptorSets(window->getDevice(), static_cast<uint32_t>(writes.size()), writes.data(), 0, nullptr);
1037 }
1038 }
1039
1040 void createPipeline() {
1041 if (descriptorSetLayout == VK_NULL_HANDLE || vertexSpv.empty() || fragmentSpv.empty() || window->getSwapchainFormat() == VK_FORMAT_UNDEFINED) {
1042 return;
1043 }
1044
1045 const VkShaderModule vertModule = mxvk::create_shader_module(window->getDevice(), vertexSpv);
1046 const VkShaderModule fragModule = mxvk::create_shader_module(window->getDevice(), fragmentSpv);
1047
1048 VkPipelineShaderStageCreateInfo vertStage{};
1049 vertStage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
1050 vertStage.stage = VK_SHADER_STAGE_VERTEX_BIT;
1051 vertStage.module = vertModule;
1052 vertStage.pName = "main";
1053
1054 VkPipelineShaderStageCreateInfo fragStage{};
1055 fragStage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
1056 fragStage.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
1057 fragStage.module = fragModule;
1058 fragStage.pName = "main";
1059 const std::array<VkPipelineShaderStageCreateInfo, 2> stages = {vertStage, fragStage};
1060
1061 VkVertexInputBindingDescription binding{};
1062 binding.binding = 0;
1063 binding.stride = sizeof(PillarVertex);
1064 binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
1065
1066 std::array<VkVertexInputAttributeDescription, 3> attrs{};
1067 attrs[0] = {0, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(PillarVertex, position)};
1068 attrs[1] = {1, 0, VK_FORMAT_R32G32_SFLOAT, offsetof(PillarVertex, texCoord)};
1069 attrs[2] = {2, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(PillarVertex, normal)};
1070
1071 VkPipelineVertexInputStateCreateInfo vertexInput{};
1072 vertexInput.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
1073 vertexInput.vertexBindingDescriptionCount = 1;
1074 vertexInput.pVertexBindingDescriptions = &binding;
1075 vertexInput.vertexAttributeDescriptionCount = static_cast<uint32_t>(attrs.size());
1076 vertexInput.pVertexAttributeDescriptions = attrs.data();
1077
1078 VkPipelineInputAssemblyStateCreateInfo inputAssembly{};
1079 inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
1080 inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
1081
1082 const std::array<VkDynamicState, 2> dynamicStates = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
1083 VkPipelineDynamicStateCreateInfo dynamicInfo{};
1084 dynamicInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
1085 dynamicInfo.dynamicStateCount = static_cast<uint32_t>(dynamicStates.size());
1086 dynamicInfo.pDynamicStates = dynamicStates.data();
1087
1088 VkPipelineViewportStateCreateInfo viewportState{};
1089 viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
1090 viewportState.viewportCount = 1;
1091 viewportState.scissorCount = 1;
1092
1093 VkPipelineRasterizationStateCreateInfo rasterizer{};
1094 rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
1095 rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
1096 // Disable face culling for the procedural pillar geometry. Winding
1097 // may differ and disabling culling prevents missing faces and flicker.
1098 rasterizer.cullMode = VK_CULL_MODE_NONE;
1099 rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;
1100 rasterizer.lineWidth = 1.0f;
1101 rasterizer.depthBiasEnable = VK_FALSE;
1102
1103 VkPipelineMultisampleStateCreateInfo multisample{};
1104 multisample.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
1105 multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
1106
1107 VkPipelineDepthStencilStateCreateInfo depthStencil{};
1108 depthStencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
1109 depthStencil.depthTestEnable = VK_TRUE;
1110 depthStencil.depthWriteEnable = VK_TRUE;
1111 depthStencil.depthCompareOp = VK_COMPARE_OP_LESS;
1112
1113 VkPipelineColorBlendAttachmentState blendAttachment{};
1114 blendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
1115 blendAttachment.blendEnable = VK_FALSE;
1116
1117 VkPipelineColorBlendStateCreateInfo colorBlend{};
1118 colorBlend.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
1119 colorBlend.attachmentCount = 1;
1120 colorBlend.pAttachments = &blendAttachment;
1121
1122 VkPushConstantRange pushRange{};
1123 pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
1124 pushRange.offset = 0;
1125 pushRange.size = sizeof(glm::mat4);
1126
1127 VkPipelineLayoutCreateInfo layoutInfo{};
1128 layoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
1129 layoutInfo.setLayoutCount = 1;
1130 layoutInfo.pSetLayouts = &descriptorSetLayout;
1131 layoutInfo.pushConstantRangeCount = 1;
1132 layoutInfo.pPushConstantRanges = &pushRange;
1133
1134 if (vkCreatePipelineLayout(window->getDevice(), &layoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) {
1135 vkDestroyShaderModule(window->getDevice(), fragModule, nullptr);
1136 vkDestroyShaderModule(window->getDevice(), vertModule, nullptr);
1137 throw mxvk::Exception("walk: failed to create raw pillar pipeline layout");
1138 }
1139
1140 const VkFormat colorFormat = window->getSwapchainFormat();
1141 const VkFormat depthFormat = window->getDepthFormat();
1142 VkPipelineRenderingCreateInfo renderingInfo{};
1143 renderingInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO;
1144 renderingInfo.colorAttachmentCount = 1;
1145 renderingInfo.pColorAttachmentFormats = &colorFormat;
1146 if (depthFormat != VK_FORMAT_UNDEFINED) {
1147 renderingInfo.depthAttachmentFormat = depthFormat;
1148 }
1149
1150 VkGraphicsPipelineCreateInfo pipelineInfo{};
1151 pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
1152 pipelineInfo.pNext = &renderingInfo;
1153 pipelineInfo.stageCount = static_cast<uint32_t>(stages.size());
1154 pipelineInfo.pStages = stages.data();
1155 pipelineInfo.pVertexInputState = &vertexInput;
1156 pipelineInfo.pInputAssemblyState = &inputAssembly;
1157 pipelineInfo.pViewportState = &viewportState;
1158 pipelineInfo.pRasterizationState = &rasterizer;
1159 pipelineInfo.pMultisampleState = &multisample;
1160 pipelineInfo.pDepthStencilState = &depthStencil;
1161 pipelineInfo.pColorBlendState = &colorBlend;
1162 pipelineInfo.pDynamicState = &dynamicInfo;
1163 pipelineInfo.layout = pipelineLayout;
1164 pipelineInfo.renderPass = VK_NULL_HANDLE;
1165
1166 if (vkCreateGraphicsPipelines(window->getDevice(), VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipeline) != VK_SUCCESS) {
1167 vkDestroyPipelineLayout(window->getDevice(), pipelineLayout, nullptr);
1168 pipelineLayout = VK_NULL_HANDLE;
1169 vkDestroyShaderModule(window->getDevice(), fragModule, nullptr);
1170 vkDestroyShaderModule(window->getDevice(), vertModule, nullptr);
1171 throw mxvk::Exception("walk: failed to create raw pillar graphics pipeline");
1172 }
1173
1174 vkDestroyShaderModule(window->getDevice(), fragModule, nullptr);
1175 vkDestroyShaderModule(window->getDevice(), vertModule, nullptr);
1176 }
1177
1178 void destroyPipeline() {
1179 if (window == nullptr || window->getDevice() == VK_NULL_HANDLE) {
1180 pipeline = VK_NULL_HANDLE;
1181 pipelineLayout = VK_NULL_HANDLE;
1182 return;
1183 }
1184
1185 if (pipeline != VK_NULL_HANDLE) {
1186 vkDestroyPipeline(window->getDevice(), pipeline, nullptr);
1187 pipeline = VK_NULL_HANDLE;
1188 }
1189 if (pipelineLayout != VK_NULL_HANDLE) {
1190 vkDestroyPipelineLayout(window->getDevice(), pipelineLayout, nullptr);
1191 pipelineLayout = VK_NULL_HANDLE;
1192 }
1193 }
1194
1195 void destroyDescriptors() {
1196 if (window == nullptr || window->getDevice() == VK_NULL_HANDLE) {
1197 descriptorSets.clear();
1198 descriptorPool = VK_NULL_HANDLE;
1199 descriptorSetLayout = VK_NULL_HANDLE;
1200 destroyUniformBuffers();
1201 return;
1202 }
1203
1204 descriptorSets.clear();
1205 if (descriptorPool != VK_NULL_HANDLE) {
1206 vkDestroyDescriptorPool(window->getDevice(), descriptorPool, nullptr);
1207 descriptorPool = VK_NULL_HANDLE;
1208 }
1209 if (descriptorSetLayout != VK_NULL_HANDLE) {
1210 vkDestroyDescriptorSetLayout(window->getDevice(), descriptorSetLayout, nullptr);
1211 descriptorSetLayout = VK_NULL_HANDLE;
1212 }
1213 destroyUniformBuffers();
1214 }
1215
1216 void destroyUniformBuffers() {
1217 if (window == nullptr || window->getDevice() == VK_NULL_HANDLE) {
1218 uniformBuffers.clear();
1219 uniformBufferMemory.clear();
1220 uniformBuffersMapped.clear();
1221 return;
1222 }
1223
1224 for (size_t i = 0; i < uniformBuffers.size(); ++i) {
1225 if (uniformBuffersMapped[i] != nullptr) {
1226 vkUnmapMemory(window->getDevice(), uniformBufferMemory[i]);
1227 uniformBuffersMapped[i] = nullptr;
1228 }
1229 if (uniformBuffers[i] != VK_NULL_HANDLE) {
1230 vkDestroyBuffer(window->getDevice(), uniformBuffers[i], nullptr);
1231 }
1232 if (uniformBufferMemory[i] != VK_NULL_HANDLE) {
1233 vkFreeMemory(window->getDevice(), uniformBufferMemory[i], nullptr);
1234 }
1235 }
1236
1237 uniformBuffers.clear();
1238 uniformBufferMemory.clear();
1239 uniformBuffersMapped.clear();
1240 }
1241
1242 void destroyTexture() {
1243 if (window == nullptr || window->getDevice() == VK_NULL_HANDLE) {
1244 textureView = VK_NULL_HANDLE;
1245 textureImage = VK_NULL_HANDLE;
1246 textureMemory = VK_NULL_HANDLE;
1247 textureSampler = VK_NULL_HANDLE;
1248 return;
1249 }
1250
1251 if (textureView != VK_NULL_HANDLE) {
1252 vkDestroyImageView(window->getDevice(), textureView, nullptr);
1253 textureView = VK_NULL_HANDLE;
1254 }
1255 if (textureImage != VK_NULL_HANDLE) {
1256 vkDestroyImage(window->getDevice(), textureImage, nullptr);
1257 textureImage = VK_NULL_HANDLE;
1258 }
1259 if (textureMemory != VK_NULL_HANDLE) {
1260 vkFreeMemory(window->getDevice(), textureMemory, nullptr);
1261 textureMemory = VK_NULL_HANDLE;
1262 }
1263 if (textureSampler != VK_NULL_HANDLE) {
1264 vkDestroySampler(window->getDevice(), textureSampler, nullptr);
1265 textureSampler = VK_NULL_HANDLE;
1266 }
1267 }
1268
1269 void destroyBuffers() {
1270 if (window == nullptr || window->getDevice() == VK_NULL_HANDLE) {
1271 vertexBuffer = VK_NULL_HANDLE;
1272 vertexMemory = VK_NULL_HANDLE;
1273 indexBuffer = VK_NULL_HANDLE;
1274 indexMemory = VK_NULL_HANDLE;
1275 return;
1276 }
1277
1278 if (vertexBuffer != VK_NULL_HANDLE) {
1279 vkDestroyBuffer(window->getDevice(), vertexBuffer, nullptr);
1280 vertexBuffer = VK_NULL_HANDLE;
1281 }
1282 if (vertexMemory != VK_NULL_HANDLE) {
1283 vkFreeMemory(window->getDevice(), vertexMemory, nullptr);
1284 vertexMemory = VK_NULL_HANDLE;
1285 }
1286 if (indexBuffer != VK_NULL_HANDLE) {
1287 vkDestroyBuffer(window->getDevice(), indexBuffer, nullptr);
1288 indexBuffer = VK_NULL_HANDLE;
1289 }
1290 if (indexMemory != VK_NULL_HANDLE) {
1291 vkFreeMemory(window->getDevice(), indexMemory, nullptr);
1292 indexMemory = VK_NULL_HANDLE;
1293 }
1294 }
1295
1296 void buildGeometry() {
1297 constexpr int segments = 16;
1298 constexpr float bottomCapScale = 1.5f;
1299 constexpr float baseDepth = -0.05f;
1300
1301 std::vector<float> vertices;
1302 std::vector<uint32_t> indices;
1303 vertices.reserve(128 * 8);
1304 indices.reserve(192);
1305
1306 for (int i = 0; i <= segments; ++i) {
1307 const float angle = static_cast<float>(i) / static_cast<float>(segments) * 2.0f * 3.14159265358979323846f;
1308 const float xBottom = std::cos(angle) * bottomCapScale;
1309 const float zBottom = std::sin(angle) * bottomCapScale;
1310 const float xTop = std::cos(angle);
1311 const float zTop = std::sin(angle);
1312 const float u = static_cast<float>(i) / static_cast<float>(segments);
1313 vertices.insert(vertices.end(), {
1314 xBottom,
1315 0.0f,
1316 zBottom,
1317 u,
1318 0.0f,
1319 xBottom,
1320 0.0f,
1321 zBottom,
1322 });
1323 vertices.insert(vertices.end(), {
1324 xTop,
1325 1.0f,
1326 zTop,
1327 u,
1328 1.0f,
1329 xTop,
1330 0.0f,
1331 zTop,
1332 });
1333 }
1334
1335 for (int i = 0; i < segments; ++i) {
1336 const int current = i * 2;
1337 const int next = (i + 1) * 2;
1338 indices.insert(indices.end(), {
1339 static_cast<uint32_t>(current),
1340 static_cast<uint32_t>(current + 1),
1341 static_cast<uint32_t>(next),
1342 static_cast<uint32_t>(next),
1343 static_cast<uint32_t>(current + 1),
1344 static_cast<uint32_t>(next + 1),
1345 });
1346 }
1347
1348 const uint32_t bottomCenterIndex = static_cast<uint32_t>(vertices.size() / 8);
1349 vertices.insert(vertices.end(), {
1350 0.0f,
1351 baseDepth,
1352 0.0f,
1353 0.5f,
1354 0.5f,
1355 0.0f,
1356 -1.0f,
1357 0.0f,
1358 });
1359
1360 const uint32_t bottomCapStart = static_cast<uint32_t>(vertices.size() / 8);
1361 for (int i = 0; i <= segments; ++i) {
1362 const float angle = static_cast<float>(i) / static_cast<float>(segments) * 2.0f * 3.14159265358979323846f;
1363 const float x = std::cos(angle) * bottomCapScale;
1364 const float z = std::sin(angle) * bottomCapScale;
1365 vertices.insert(vertices.end(), {
1366 x,
1367 0.0f,
1368 z,
1369 0.5f + x * 0.5f / bottomCapScale,
1370 0.5f + z * 0.5f / bottomCapScale,
1371 0.0f,
1372 -1.0f,
1373 0.0f,
1374 });
1375 }
1376 for (int i = 0; i < segments; ++i) {
1377 indices.insert(indices.end(), {
1378 bottomCenterIndex,
1379 bottomCapStart + static_cast<uint32_t>(i + 1),
1380 bottomCapStart + static_cast<uint32_t>(i),
1381 });
1382 }
1383
1384 const uint32_t topCenterIndex = static_cast<uint32_t>(vertices.size() / 8);
1385 vertices.insert(vertices.end(), {
1386 0.0f,
1387 1.0f,
1388 0.0f,
1389 0.5f,
1390 0.5f,
1391 0.0f,
1392 1.0f,
1393 0.0f,
1394 });
1395
1396 const uint32_t topCapStart = static_cast<uint32_t>(vertices.size() / 8);
1397 for (int i = 0; i <= segments; ++i) {
1398 const float angle = static_cast<float>(i) / static_cast<float>(segments) * 2.0f * 3.14159265358979323846f;
1399 const float x = std::cos(angle);
1400 const float z = std::sin(angle);
1401 vertices.insert(vertices.end(), {
1402 x,
1403 1.0f,
1404 z,
1405 0.5f + x * 0.5f,
1406 0.5f + z * 0.5f,
1407 0.0f,
1408 1.0f,
1409 0.0f,
1410 });
1411 }
1412 for (int i = 0; i < segments; ++i) {
1413 indices.insert(indices.end(), {
1414 topCenterIndex,
1415 topCapStart + static_cast<uint32_t>(i),
1416 topCapStart + static_cast<uint32_t>(i + 1),
1417 });
1418 }
1419
1420 vertexCount = static_cast<uint32_t>(vertices.size() / 8);
1421 indexCount = static_cast<uint32_t>(indices.size());
1422
1423 std::vector<PillarVertex> pillarVertices(vertexCount);
1424 for (uint32_t i = 0; i < vertexCount; ++i) {
1425 const size_t base = static_cast<size_t>(i) * 8;
1426 pillarVertices[i].position = glm::vec3(vertices[base + 0], vertices[base + 1], vertices[base + 2]);
1427 pillarVertices[i].texCoord = glm::vec2(vertices[base + 3], vertices[base + 4]);
1428 pillarVertices[i].normal = glm::vec3(vertices[base + 5], vertices[base + 6], vertices[base + 7]);
1429 }
1430
1431 createBuffer(static_cast<VkDeviceSize>(pillarVertices.size() * sizeof(PillarVertex)),
1432 VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
1433 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
1434 vertexBuffer,
1435 vertexMemory);
1436 void *mapped = nullptr;
1437 vkMapMemory(window->getDevice(), vertexMemory, 0, VK_WHOLE_SIZE, 0, &mapped);
1438 std::memcpy(mapped, pillarVertices.data(), pillarVertices.size() * sizeof(PillarVertex));
1439 vkUnmapMemory(window->getDevice(), vertexMemory);
1440
1441 createBuffer(static_cast<VkDeviceSize>(indices.size() * sizeof(uint32_t)),
1442 VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
1443 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
1444 indexBuffer,
1445 indexMemory);
1446 vkMapMemory(window->getDevice(), indexMemory, 0, VK_WHOLE_SIZE, 0, &mapped);
1447 std::memcpy(mapped, indices.data(), indices.size() * sizeof(uint32_t));
1448 vkUnmapMemory(window->getDevice(), indexMemory);
1449 }
1450
1451 void loadTexture([[maybe_unused]] const std::string &textureManifestPath, const std::string &textureBasePath) {
1452 SDL_Surface *surface = mxvk::LoadPNG((textureBasePath + "/ground.png").c_str());
1453 if (surface == nullptr) {
1454 throw mxvk::Exception("walk: failed to load raw pillar texture");
1455 }
1456
1457 const uint32_t width = static_cast<uint32_t>(surface->w);
1458 const uint32_t height = static_cast<uint32_t>(surface->h);
1459 const VkDeviceSize imageSize = static_cast<VkDeviceSize>(width) * static_cast<VkDeviceSize>(height) * 4U;
1460
1461 VkBuffer stagingBuffer = VK_NULL_HANDLE;
1462 VkDeviceMemory stagingMemory = VK_NULL_HANDLE;
1463 createBuffer(imageSize,
1464 VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
1465 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
1466 stagingBuffer,
1467 stagingMemory);
1468
1469 void *mapped = nullptr;
1470 vkMapMemory(window->getDevice(), stagingMemory, 0, imageSize, 0, &mapped);
1471 std::memcpy(mapped, surface->pixels, static_cast<size_t>(imageSize));
1472 vkUnmapMemory(window->getDevice(), stagingMemory);
1473
1474 createImage(width, height,
1475 VK_FORMAT_R8G8B8A8_UNORM,
1476 VK_IMAGE_TILING_OPTIMAL,
1477 VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
1478 VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
1479 textureImage,
1480 textureMemory);
1481
1482 transitionImageLayout(textureImage, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
1483 copyBufferToImage(stagingBuffer, textureImage, width, height);
1484 transitionImageLayout(textureImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
1485
1486 textureView = createImageView(textureImage, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_ASPECT_COLOR_BIT);
1487
1488 vkDestroyBuffer(window->getDevice(), stagingBuffer, nullptr);
1489 vkFreeMemory(window->getDevice(), stagingMemory, nullptr);
1490 SDL_DestroySurface(surface);
1491 }
1492
1493 mxvk::VK_Window *window = nullptr;
1494 std::vector<char> vertexSpv{};
1495 std::vector<char> fragmentSpv{};
1496
1497 uint32_t vertexCount = 0;
1498 uint32_t indexCount = 0;
1499 VkBuffer vertexBuffer = VK_NULL_HANDLE;
1500 VkDeviceMemory vertexMemory = VK_NULL_HANDLE;
1501 VkBuffer indexBuffer = VK_NULL_HANDLE;
1502 VkDeviceMemory indexMemory = VK_NULL_HANDLE;
1503
1504 VkImage textureImage = VK_NULL_HANDLE;
1505 VkDeviceMemory textureMemory = VK_NULL_HANDLE;
1506 VkImageView textureView = VK_NULL_HANDLE;
1507 VkSampler textureSampler = VK_NULL_HANDLE;
1508
1509 VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
1510 VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
1511 std::vector<VkDescriptorSet> descriptorSets{};
1512
1513 VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
1514 VkPipeline pipeline = VK_NULL_HANDLE;
1515
1516 std::vector<VkBuffer> uniformBuffers{};
1517 std::vector<VkDeviceMemory> uniformBufferMemory{};
1518 std::vector<void *> uniformBuffersMapped{};
1519 };
1520
1521 class RawWallRenderer {
1522 public:
1523 struct WallVertex {
1524 glm::vec3 position{0.0f};
1525 glm::vec2 texCoord{0.0f};
1526 glm::vec3 normal{0.0f};
1527 };
1528
1529 struct WallUniforms {
1530 glm::mat4 view{1.0f};
1531 glm::mat4 proj{1.0f};
1532 glm::vec4 fx{0.0f};
1533 };
1534
1535 void load(mxvk::VK_Window *targetWindow,
1536 const std::string &textureManifestPath,
1537 const std::string &textureBasePath,
1538 const std::vector<char> &vertexShaderSpv,
1539 const std::vector<char> &fragmentShaderSpv) {
1540 if (targetWindow == nullptr) {
1541 throw mxvk::Exception("walk: raw wall renderer requires a valid window");
1542 }
1543 window = targetWindow;
1544 vertSpv = vertexShaderSpv;
1545 fragSpv = fragmentShaderSpv;
1546
1547 if (!window->ensureRenderResources()) {
1548 throw mxvk::Exception("walk: raw wall renderer requires render resources");
1549 }
1550
1551 buildGeometry();
1552 loadTexture(textureManifestPath, textureBasePath);
1553 createTextureSampler();
1554 createDescriptorSetLayout();
1555 createUniformBuffers();
1556 createDescriptorPool();
1557 createDescriptorSets();
1558 createPipeline();
1559 }
1560
1561 void resize(mxvk::VK_Window *targetWindow) {
1562 if (targetWindow == nullptr || targetWindow->getDevice() == VK_NULL_HANDLE) {
1563 return;
1564 }
1565
1566 window = targetWindow;
1567 destroyPipeline();
1568 destroyDescriptors();
1569 createDescriptorSetLayout();
1570 createUniformBuffers();
1571 createDescriptorPool();
1572 createDescriptorSets();
1573 createPipeline();
1574 }
1575
1576 /// @brief Hot-swap the fragment shader without rebuilding geometry or descriptors.
1577 /// @param newFragSpv Compiled SPIR-V bytecode for the new fragment shader.
1578 void reloadFragShader(const std::vector<char> &newFragSpv) {
1579 if (window == nullptr || window->getDevice() == VK_NULL_HANDLE || newFragSpv.empty()) {
1580 return;
1581 }
1582 vkDeviceWaitIdle(window->getDevice());
1583 fragSpv = newFragSpv;
1584 destroyPipeline();
1585 createPipeline();
1586 }
1587
1588 void cleanup(mxvk::VK_Window *targetWindow) {
1589 if (targetWindow == nullptr || targetWindow->getDevice() == VK_NULL_HANDLE) {
1590 return;
1591 }
1592
1593 window = targetWindow;
1594 destroyPipeline();
1595 destroyDescriptors();
1596 destroyTexture();
1597 destroyBuffers();
1598 window = nullptr;
1599 }
1600
1601 void render(VkCommandBuffer cmd,
1602 uint32_t imageIndex,
1603 const std::vector<WallSegment> &walls,
1604 float wallThickness,
1605 const glm::mat4 &view,
1606 const glm::mat4 &proj,
1607 const glm::vec4 &fx) {
1608 if (cmd == VK_NULL_HANDLE || pipeline == VK_NULL_HANDLE || pipelineLayout == VK_NULL_HANDLE) {
1609 return;
1610 }
1611 if (imageIndex >= uniformBuffersMapped.size() || descriptorSets.empty() || vertexBuffer == VK_NULL_HANDLE || indexBuffer == VK_NULL_HANDLE) {
1612 return;
1613 }
1614
1615 WallUniforms uniforms{};
1616 uniforms.view = view;
1617 uniforms.proj = proj;
1618 uniforms.fx = fx;
1619 std::memcpy(uniformBuffersMapped[imageIndex], &uniforms, sizeof(WallUniforms));
1620
1621 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
1622 vkCmdBindDescriptorSets(cmd,
1623 VK_PIPELINE_BIND_POINT_GRAPHICS,
1624 pipelineLayout,
1625 0,
1626 1,
1627 &descriptorSets[imageIndex],
1628 0,
1629 nullptr);
1630
1631 const VkDeviceSize offset = 0;
1632 vkCmdBindVertexBuffers(cmd, 0, 1, &vertexBuffer, &offset);
1633 vkCmdBindIndexBuffer(cmd, indexBuffer, 0, VK_INDEX_TYPE_UINT32);
1634
1635 const float thickness = std::max(0.02f, wallThickness);
1636 // Extend each wall by about half its thickness on both ends so adjoining
1637 // runs meet cleanly without visible seam slivers.
1638 const float wallOverlap = thickness * 0.55f;
1639 for (const WallSegment &segment : walls) {
1640 const glm::vec3 center = (segment.start + segment.end) * 0.5f;
1641 const glm::vec3 span = segment.end - segment.start;
1642 const float length = glm::length(span);
1643 if (length < 0.0001f) {
1644 continue;
1645 }
1646 // Unit wall geometry has Y in [0..1]. Sink slightly to avoid floor/wall
1647 // depth fighting where the floor top plane is also at y=0.
1648 constexpr float baseSink = 0.01f;
1649 glm::mat4 model = glm::translate(glm::mat4(1.0f), glm::vec3(center.x, -baseSink, center.z));
1650 model = glm::rotate(model, std::atan2(span.z, span.x), glm::vec3(0.0f, 1.0f, 0.0f));
1651 model = glm::scale(model, glm::vec3(length + wallOverlap * 2.0f, segment.height, thickness));
1652 vkCmdPushConstants(cmd, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(glm::mat4), &model);
1653 vkCmdDrawIndexed(cmd, indexCount, 1, 0, 0, 0);
1654 }
1655 }
1656
1657 private:
1658 [[nodiscard]] uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) const {
1659 VkPhysicalDeviceMemoryProperties memProperties{};
1660 vkGetPhysicalDeviceMemoryProperties(window->getPhysicalDevice(), &memProperties);
1661 for (uint32_t i = 0; i < memProperties.memoryTypeCount; ++i) {
1662 if ((typeFilter & (1u << i)) != 0u && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) {
1663 return i;
1664 }
1665 }
1666 throw mxvk::Exception("walk: failed to find suitable memory type for raw wall renderer");
1667 }
1668
1669 void createBuffer(VkDeviceSize size,
1670 VkBufferUsageFlags usage,
1671 VkMemoryPropertyFlags properties,
1672 VkBuffer &buffer,
1673 VkDeviceMemory &bufferMemory) const {
1674 VkBufferCreateInfo bufferInfo{};
1675 bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
1676 bufferInfo.size = size;
1677 bufferInfo.usage = usage;
1678 bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
1679
1680 if (vkCreateBuffer(window->getDevice(), &bufferInfo, nullptr, &buffer) != VK_SUCCESS) {
1681 throw mxvk::Exception("walk: failed to create raw wall buffer");
1682 }
1683
1684 VkMemoryRequirements requirements{};
1685 vkGetBufferMemoryRequirements(window->getDevice(), buffer, &requirements);
1686
1687 VkMemoryAllocateInfo allocInfo{};
1688 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
1689 allocInfo.allocationSize = requirements.size;
1690
1691 try {
1692 allocInfo.memoryTypeIndex = findMemoryType(requirements.memoryTypeBits, properties);
1693 if (vkAllocateMemory(window->getDevice(), &allocInfo, nullptr, &bufferMemory) != VK_SUCCESS) {
1694 throw mxvk::Exception("walk: failed to allocate raw wall buffer memory");
1695 }
1696
1697 if (vkBindBufferMemory(window->getDevice(), buffer, bufferMemory, 0) != VK_SUCCESS) {
1698 throw mxvk::Exception("walk: failed to bind raw wall buffer memory");
1699 }
1700 } catch (...) {
1701 if (bufferMemory != VK_NULL_HANDLE) {
1702 vkFreeMemory(window->getDevice(), bufferMemory, nullptr);
1703 bufferMemory = VK_NULL_HANDLE;
1704 }
1705 if (buffer != VK_NULL_HANDLE) {
1706 vkDestroyBuffer(window->getDevice(), buffer, nullptr);
1707 buffer = VK_NULL_HANDLE;
1708 }
1709 throw;
1710 }
1711 }
1712
1713 void buildGeometry() {
1714 // Unit wall prism: X in [-0.5..0.5], Y in [0..1], Z in [-0.5..0.5].
1715 // Per-instance scaling in render() controls segment length/height/thickness.
1716 std::vector<WallVertex> verts;
1717 std::vector<uint32_t> inds;
1718 verts.reserve(24);
1719 inds.reserve(36);
1720
1721 const auto addFace = [&verts, &inds](const glm::vec3 &v0,
1722 const glm::vec3 &v1,
1723 const glm::vec3 &v2,
1724 const glm::vec3 &v3,
1725 const glm::vec3 &normal) {
1726 const uint32_t base = static_cast<uint32_t>(verts.size());
1727 verts.push_back({v0, glm::vec2(0.0f, 0.0f), normal});
1728 verts.push_back({v1, glm::vec2(1.0f, 0.0f), normal});
1729 verts.push_back({v2, glm::vec2(1.0f, 1.0f), normal});
1730 verts.push_back({v3, glm::vec2(0.0f, 1.0f), normal});
1731 inds.insert(inds.end(), {base + 0, base + 1, base + 2, base + 2, base + 3, base + 0});
1732 };
1733
1734 constexpr float x = 0.5f;
1735 constexpr float z = 0.5f;
1736 constexpr float y0 = 0.0f;
1737 constexpr float y1 = 1.0f;
1738
1739 addFace(glm::vec3(-x, y0, z), glm::vec3(x, y0, z), glm::vec3(x, y1, z), glm::vec3(-x, y1, z), glm::vec3(0.0f, 0.0f, 1.0f));
1740 addFace(glm::vec3(x, y0, -z), glm::vec3(-x, y0, -z), glm::vec3(-x, y1, -z), glm::vec3(x, y1, -z), glm::vec3(0.0f, 0.0f, -1.0f));
1741 addFace(glm::vec3(x, y0, z), glm::vec3(x, y0, -z), glm::vec3(x, y1, -z), glm::vec3(x, y1, z), glm::vec3(1.0f, 0.0f, 0.0f));
1742 addFace(glm::vec3(-x, y0, -z), glm::vec3(-x, y0, z), glm::vec3(-x, y1, z), glm::vec3(-x, y1, -z), glm::vec3(-1.0f, 0.0f, 0.0f));
1743 addFace(glm::vec3(-x, y1, z), glm::vec3(x, y1, z), glm::vec3(x, y1, -z), glm::vec3(-x, y1, -z), glm::vec3(0.0f, 1.0f, 0.0f));
1744 addFace(glm::vec3(-x, y0, -z), glm::vec3(x, y0, -z), glm::vec3(x, y0, z), glm::vec3(-x, y0, z), glm::vec3(0.0f, -1.0f, 0.0f));
1745
1746 vertexCount = static_cast<uint32_t>(verts.size());
1747 indexCount = static_cast<uint32_t>(inds.size());
1748
1749 createBuffer(static_cast<VkDeviceSize>(verts.size() * sizeof(WallVertex)),
1750 VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
1751 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
1752 vertexBuffer,
1753 vertexMemory);
1754 void *mapped = nullptr;
1755 vkMapMemory(window->getDevice(), vertexMemory, 0, VK_WHOLE_SIZE, 0, &mapped);
1756 std::memcpy(mapped, verts.data(), verts.size() * sizeof(WallVertex));
1757 vkUnmapMemory(window->getDevice(), vertexMemory);
1758
1759 createBuffer(static_cast<VkDeviceSize>(inds.size() * sizeof(uint32_t)),
1760 VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
1761 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
1762 indexBuffer,
1763 indexMemory);
1764 vkMapMemory(window->getDevice(), indexMemory, 0, VK_WHOLE_SIZE, 0, &mapped);
1765 std::memcpy(mapped, inds.data(), inds.size() * sizeof(uint32_t));
1766 vkUnmapMemory(window->getDevice(), indexMemory);
1767 }
1768
1769 void loadTexture([[maybe_unused]] const std::string &textureManifestPath, const std::string &textureBasePath) {
1770 SDL_Surface *surface = mxvk::LoadPNG((textureBasePath + "/wall_bricks.png").c_str());
1771 if (surface == nullptr) {
1772 throw mxvk::Exception("walk: failed to load raw wall texture");
1773 }
1774
1775 const uint32_t width = static_cast<uint32_t>(surface->w);
1776 const uint32_t height = static_cast<uint32_t>(surface->h);
1777 const VkDeviceSize imageSize = static_cast<VkDeviceSize>(width) * static_cast<VkDeviceSize>(height) * 4U;
1778
1779 VkBuffer stagingBuffer = VK_NULL_HANDLE;
1780 VkDeviceMemory stagingMemory = VK_NULL_HANDLE;
1781 createBuffer(imageSize,
1782 VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
1783 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
1784 stagingBuffer,
1785 stagingMemory);
1786
1787 void *mapped = nullptr;
1788 vkMapMemory(window->getDevice(), stagingMemory, 0, imageSize, 0, &mapped);
1789 std::memcpy(mapped, surface->pixels, static_cast<size_t>(imageSize));
1790 vkUnmapMemory(window->getDevice(), stagingMemory);
1791
1792 createImage(width, height,
1793 VK_FORMAT_R8G8B8A8_UNORM,
1794 VK_IMAGE_TILING_OPTIMAL,
1795 VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
1796 VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
1797 textureImage,
1798 textureMemory);
1799
1800 transitionImageLayout(textureImage, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
1801 copyBufferToImage(stagingBuffer, textureImage, width, height);
1802 transitionImageLayout(textureImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
1803
1804 textureView = createImageView(textureImage, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_ASPECT_COLOR_BIT);
1805
1806 vkDestroyBuffer(window->getDevice(), stagingBuffer, nullptr);
1807 vkFreeMemory(window->getDevice(), stagingMemory, nullptr);
1808 SDL_DestroySurface(surface);
1809 }
1810
1811 void createTextureSampler() {
1812 if (textureSampler != VK_NULL_HANDLE) {
1813 return;
1814 }
1815
1816 VkPhysicalDeviceFeatures deviceFeatures{};
1817 vkGetPhysicalDeviceFeatures(window->getPhysicalDevice(), &deviceFeatures);
1818 VkPhysicalDeviceProperties deviceProperties{};
1819 vkGetPhysicalDeviceProperties(window->getPhysicalDevice(), &deviceProperties);
1820 const bool anisotropySupported = deviceFeatures.samplerAnisotropy == VK_TRUE;
1821 const float anisotropyLevel = anisotropySupported
1822 ? std::min(8.0f, deviceProperties.limits.maxSamplerAnisotropy)
1823 : 1.0f;
1824
1825 VkSamplerCreateInfo samplerInfo{};
1826 samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
1827 samplerInfo.magFilter = VK_FILTER_LINEAR;
1828 samplerInfo.minFilter = VK_FILTER_LINEAR;
1829 samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT;
1830 samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT;
1831 samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT;
1832 samplerInfo.anisotropyEnable = anisotropySupported ? VK_TRUE : VK_FALSE;
1833 samplerInfo.maxAnisotropy = anisotropyLevel;
1834 samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
1835 samplerInfo.unnormalizedCoordinates = VK_FALSE;
1836 samplerInfo.compareEnable = VK_FALSE;
1837 samplerInfo.compareOp = VK_COMPARE_OP_ALWAYS;
1838 samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
1839
1840 if (vkCreateSampler(window->getDevice(), &samplerInfo, nullptr, &textureSampler) != VK_SUCCESS) {
1841 throw mxvk::Exception("walk: failed to create raw wall texture sampler");
1842 }
1843 }
1844
1845 void createDescriptorSetLayout() {
1846 if (descriptorSetLayout != VK_NULL_HANDLE) {
1847 return;
1848 }
1849
1850 VkDescriptorSetLayoutBinding samplerBinding{};
1851 samplerBinding.binding = 0;
1852 samplerBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
1853 samplerBinding.descriptorCount = 1;
1854 samplerBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
1855
1856 VkDescriptorSetLayoutBinding uboBinding{};
1857 uboBinding.binding = 1;
1858 uboBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
1859 uboBinding.descriptorCount = 1;
1860 uboBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
1861
1862 const std::array<VkDescriptorSetLayoutBinding, 2> bindings = {samplerBinding, uboBinding};
1863
1864 VkDescriptorSetLayoutCreateInfo layoutInfo{};
1865 layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
1866 layoutInfo.bindingCount = static_cast<uint32_t>(bindings.size());
1867 layoutInfo.pBindings = bindings.data();
1868
1869 if (vkCreateDescriptorSetLayout(window->getDevice(), &layoutInfo, nullptr, &descriptorSetLayout) != VK_SUCCESS) {
1870 throw mxvk::Exception("walk: failed to create raw wall descriptor set layout");
1871 }
1872 }
1873
1874 void createUniformBuffers() {
1875 destroyUniformBuffers();
1876
1877 const size_t frameCount = window->getSwapchainImageCount();
1878 if (frameCount == 0) {
1879 return;
1880 }
1881
1882 uniformBuffers.resize(frameCount, VK_NULL_HANDLE);
1883 uniformBufferMemory.resize(frameCount, VK_NULL_HANDLE);
1884 uniformBuffersMapped.resize(frameCount, nullptr);
1885
1886 for (size_t i = 0; i < frameCount; ++i) {
1887 createBuffer(sizeof(WallUniforms),
1888 VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
1889 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
1890 uniformBuffers[i],
1891 uniformBufferMemory[i]);
1892 vkMapMemory(window->getDevice(), uniformBufferMemory[i], 0, sizeof(WallUniforms), 0, &uniformBuffersMapped[i]);
1893 }
1894 }
1895
1896 void createDescriptorPool() {
1897 const uint32_t frameCount = static_cast<uint32_t>(window->getSwapchainImageCount());
1898 std::array<VkDescriptorPoolSize, 2> poolSizes{};
1899 poolSizes[0].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
1900 poolSizes[0].descriptorCount = frameCount;
1901 poolSizes[1].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
1902 poolSizes[1].descriptorCount = frameCount;
1903
1904 VkDescriptorPoolCreateInfo poolInfo{};
1905 poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
1906 poolInfo.poolSizeCount = static_cast<uint32_t>(poolSizes.size());
1907 poolInfo.pPoolSizes = poolSizes.data();
1908 poolInfo.maxSets = frameCount;
1909
1910 if (vkCreateDescriptorPool(window->getDevice(), &poolInfo, nullptr, &descriptorPool) != VK_SUCCESS) {
1911 throw mxvk::Exception("walk: failed to create raw wall descriptor pool");
1912 }
1913 }
1914
1915 void createDescriptorSets() {
1916 const size_t frameCount = window->getSwapchainImageCount();
1917 std::vector<VkDescriptorSetLayout> layouts(frameCount, descriptorSetLayout);
1918
1919 VkDescriptorSetAllocateInfo allocInfo{};
1920 allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
1921 allocInfo.descriptorPool = descriptorPool;
1922 allocInfo.descriptorSetCount = static_cast<uint32_t>(frameCount);
1923 allocInfo.pSetLayouts = layouts.data();
1924
1925 descriptorSets.resize(frameCount, VK_NULL_HANDLE);
1926 if (vkAllocateDescriptorSets(window->getDevice(), &allocInfo, descriptorSets.data()) != VK_SUCCESS) {
1927 throw mxvk::Exception("walk: failed to allocate raw wall descriptor sets");
1928 }
1929
1930 VkDescriptorImageInfo imageInfo{};
1931 imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
1932 imageInfo.imageView = textureView;
1933 imageInfo.sampler = textureSampler;
1934
1935 for (size_t i = 0; i < frameCount; ++i) {
1936 VkDescriptorBufferInfo bufferInfo{};
1937 bufferInfo.buffer = uniformBuffers[i];
1938 bufferInfo.offset = 0;
1939 bufferInfo.range = sizeof(WallUniforms);
1940
1941 std::array<VkWriteDescriptorSet, 2> writes{};
1942 writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
1943 writes[0].dstSet = descriptorSets[i];
1944 writes[0].dstBinding = 0;
1945 writes[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
1946 writes[0].descriptorCount = 1;
1947 writes[0].pImageInfo = &imageInfo;
1948
1949 writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
1950 writes[1].dstSet = descriptorSets[i];
1951 writes[1].dstBinding = 1;
1952 writes[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
1953 writes[1].descriptorCount = 1;
1954 writes[1].pBufferInfo = &bufferInfo;
1955
1956 vkUpdateDescriptorSets(window->getDevice(), static_cast<uint32_t>(writes.size()), writes.data(), 0, nullptr);
1957 }
1958 }
1959
1960 void createPipeline() {
1961 if (descriptorSetLayout == VK_NULL_HANDLE || vertSpv.empty() || fragSpv.empty() || window->getSwapchainFormat() == VK_FORMAT_UNDEFINED) {
1962 return;
1963 }
1964
1965 const VkShaderModule vertModule = mxvk::create_shader_module(window->getDevice(), vertSpv);
1966 const VkShaderModule fragModule = mxvk::create_shader_module(window->getDevice(), fragSpv);
1967
1968 VkPipelineShaderStageCreateInfo vertStage{};
1969 vertStage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
1970 vertStage.stage = VK_SHADER_STAGE_VERTEX_BIT;
1971 vertStage.module = vertModule;
1972 vertStage.pName = "main";
1973
1974 VkPipelineShaderStageCreateInfo fragStage{};
1975 fragStage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
1976 fragStage.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
1977 fragStage.module = fragModule;
1978 fragStage.pName = "main";
1979 const std::array<VkPipelineShaderStageCreateInfo, 2> stages = {vertStage, fragStage};
1980
1981 VkVertexInputBindingDescription binding{};
1982 binding.binding = 0;
1983 binding.stride = sizeof(WallVertex);
1984 binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
1985
1986 std::array<VkVertexInputAttributeDescription, 3> attrs{};
1987 attrs[0] = {0, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(WallVertex, position)};
1988 attrs[1] = {1, 0, VK_FORMAT_R32G32_SFLOAT, offsetof(WallVertex, texCoord)};
1989 attrs[2] = {2, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(WallVertex, normal)};
1990
1991 VkPipelineVertexInputStateCreateInfo vertexInput{};
1992 vertexInput.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
1993 vertexInput.vertexBindingDescriptionCount = 1;
1994 vertexInput.pVertexBindingDescriptions = &binding;
1995 vertexInput.vertexAttributeDescriptionCount = static_cast<uint32_t>(attrs.size());
1996 vertexInput.pVertexAttributeDescriptions = attrs.data();
1997
1998 VkPipelineInputAssemblyStateCreateInfo inputAssembly{};
1999 inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
2000 inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
2001
2002 const std::array<VkDynamicState, 2> dynamicStates = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
2003 VkPipelineDynamicStateCreateInfo dynamicInfo{};
2004 dynamicInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
2005 dynamicInfo.dynamicStateCount = static_cast<uint32_t>(dynamicStates.size());
2006 dynamicInfo.pDynamicStates = dynamicStates.data();
2007
2008 VkPipelineViewportStateCreateInfo viewportState{};
2009 viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
2010 viewportState.viewportCount = 1;
2011 viewportState.scissorCount = 1;
2012
2013 VkPipelineRasterizationStateCreateInfo rasterizer{};
2014 rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
2015 rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
2016 rasterizer.cullMode = VK_CULL_MODE_NONE;
2017 rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;
2018 rasterizer.lineWidth = 1.0f;
2019
2020 VkPipelineMultisampleStateCreateInfo multisample{};
2021 multisample.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
2022 multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
2023
2024 VkPipelineDepthStencilStateCreateInfo depthStencil{};
2025 depthStencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
2026 depthStencil.depthTestEnable = VK_TRUE;
2027 depthStencil.depthWriteEnable = VK_TRUE;
2028 depthStencil.depthCompareOp = VK_COMPARE_OP_LESS;
2029
2030 VkPipelineColorBlendAttachmentState blendAttachment{};
2031 blendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
2032 blendAttachment.blendEnable = VK_FALSE;
2033
2034 VkPipelineColorBlendStateCreateInfo colorBlend{};
2035 colorBlend.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
2036 colorBlend.attachmentCount = 1;
2037 colorBlend.pAttachments = &blendAttachment;
2038
2039 VkPushConstantRange pushRange{};
2040 pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
2041 pushRange.offset = 0;
2042 pushRange.size = sizeof(glm::mat4);
2043
2044 VkPipelineLayoutCreateInfo layoutInfo{};
2045 layoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
2046 layoutInfo.setLayoutCount = 1;
2047 layoutInfo.pSetLayouts = &descriptorSetLayout;
2048 layoutInfo.pushConstantRangeCount = 1;
2049 layoutInfo.pPushConstantRanges = &pushRange;
2050
2051 if (vkCreatePipelineLayout(window->getDevice(), &layoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) {
2052 vkDestroyShaderModule(window->getDevice(), fragModule, nullptr);
2053 vkDestroyShaderModule(window->getDevice(), vertModule, nullptr);
2054 throw mxvk::Exception("walk: failed to create raw wall pipeline layout");
2055 }
2056
2057 const VkFormat colorFormat = window->getSwapchainFormat();
2058 const VkFormat depthFormat = window->getDepthFormat();
2059 VkPipelineRenderingCreateInfo renderingInfo{};
2060 renderingInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO;
2061 renderingInfo.colorAttachmentCount = 1;
2062 renderingInfo.pColorAttachmentFormats = &colorFormat;
2063 if (depthFormat != VK_FORMAT_UNDEFINED) {
2064 renderingInfo.depthAttachmentFormat = depthFormat;
2065 }
2066
2067 VkGraphicsPipelineCreateInfo pipelineInfo{};
2068 pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
2069 pipelineInfo.pNext = &renderingInfo;
2070 pipelineInfo.stageCount = static_cast<uint32_t>(stages.size());
2071 pipelineInfo.pStages = stages.data();
2072 pipelineInfo.pVertexInputState = &vertexInput;
2073 pipelineInfo.pInputAssemblyState = &inputAssembly;
2074 pipelineInfo.pViewportState = &viewportState;
2075 pipelineInfo.pRasterizationState = &rasterizer;
2076 pipelineInfo.pMultisampleState = &multisample;
2077 pipelineInfo.pDepthStencilState = &depthStencil;
2078 pipelineInfo.pColorBlendState = &colorBlend;
2079 pipelineInfo.pDynamicState = &dynamicInfo;
2080 pipelineInfo.layout = pipelineLayout;
2081 pipelineInfo.renderPass = VK_NULL_HANDLE;
2082
2083 if (vkCreateGraphicsPipelines(window->getDevice(), VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipeline) != VK_SUCCESS) {
2084 vkDestroyPipelineLayout(window->getDevice(), pipelineLayout, nullptr);
2085 pipelineLayout = VK_NULL_HANDLE;
2086 vkDestroyShaderModule(window->getDevice(), fragModule, nullptr);
2087 vkDestroyShaderModule(window->getDevice(), vertModule, nullptr);
2088 throw mxvk::Exception("walk: failed to create raw wall graphics pipeline");
2089 }
2090
2091 vkDestroyShaderModule(window->getDevice(), fragModule, nullptr);
2092 vkDestroyShaderModule(window->getDevice(), vertModule, nullptr);
2093 }
2094
2095 void destroyPipeline() {
2096 if (window == nullptr || window->getDevice() == VK_NULL_HANDLE) {
2097 pipeline = VK_NULL_HANDLE;
2098 pipelineLayout = VK_NULL_HANDLE;
2099 return;
2100 }
2101
2102 if (pipeline != VK_NULL_HANDLE) {
2103 vkDestroyPipeline(window->getDevice(), pipeline, nullptr);
2104 pipeline = VK_NULL_HANDLE;
2105 }
2106 if (pipelineLayout != VK_NULL_HANDLE) {
2107 vkDestroyPipelineLayout(window->getDevice(), pipelineLayout, nullptr);
2108 pipelineLayout = VK_NULL_HANDLE;
2109 }
2110 }
2111
2112 void destroyDescriptors() {
2113 if (window == nullptr || window->getDevice() == VK_NULL_HANDLE) {
2114 descriptorSets.clear();
2115 descriptorPool = VK_NULL_HANDLE;
2116 descriptorSetLayout = VK_NULL_HANDLE;
2117 destroyUniformBuffers();
2118 return;
2119 }
2120
2121 descriptorSets.clear();
2122 if (descriptorPool != VK_NULL_HANDLE) {
2123 vkDestroyDescriptorPool(window->getDevice(), descriptorPool, nullptr);
2124 descriptorPool = VK_NULL_HANDLE;
2125 }
2126 if (descriptorSetLayout != VK_NULL_HANDLE) {
2127 vkDestroyDescriptorSetLayout(window->getDevice(), descriptorSetLayout, nullptr);
2128 descriptorSetLayout = VK_NULL_HANDLE;
2129 }
2130 destroyUniformBuffers();
2131 }
2132
2133 void destroyUniformBuffers() {
2134 if (window == nullptr || window->getDevice() == VK_NULL_HANDLE) {
2135 uniformBuffers.clear();
2136 uniformBufferMemory.clear();
2137 uniformBuffersMapped.clear();
2138 return;
2139 }
2140
2141 for (size_t i = 0; i < uniformBuffers.size(); ++i) {
2142 if (uniformBuffersMapped[i] != nullptr) {
2143 vkUnmapMemory(window->getDevice(), uniformBufferMemory[i]);
2144 uniformBuffersMapped[i] = nullptr;
2145 }
2146 if (uniformBuffers[i] != VK_NULL_HANDLE) {
2147 vkDestroyBuffer(window->getDevice(), uniformBuffers[i], nullptr);
2148 }
2149 if (uniformBufferMemory[i] != VK_NULL_HANDLE) {
2150 vkFreeMemory(window->getDevice(), uniformBufferMemory[i], nullptr);
2151 }
2152 }
2153
2154 uniformBuffers.clear();
2155 uniformBufferMemory.clear();
2156 uniformBuffersMapped.clear();
2157 }
2158
2159 void destroyTexture() {
2160 if (window == nullptr || window->getDevice() == VK_NULL_HANDLE) {
2161 textureView = VK_NULL_HANDLE;
2162 textureImage = VK_NULL_HANDLE;
2163 textureMemory = VK_NULL_HANDLE;
2164 textureSampler = VK_NULL_HANDLE;
2165 return;
2166 }
2167
2168 if (textureView != VK_NULL_HANDLE) {
2169 vkDestroyImageView(window->getDevice(), textureView, nullptr);
2170 textureView = VK_NULL_HANDLE;
2171 }
2172 if (textureImage != VK_NULL_HANDLE) {
2173 vkDestroyImage(window->getDevice(), textureImage, nullptr);
2174 textureImage = VK_NULL_HANDLE;
2175 }
2176 if (textureMemory != VK_NULL_HANDLE) {
2177 vkFreeMemory(window->getDevice(), textureMemory, nullptr);
2178 textureMemory = VK_NULL_HANDLE;
2179 }
2180 if (textureSampler != VK_NULL_HANDLE) {
2181 vkDestroySampler(window->getDevice(), textureSampler, nullptr);
2182 textureSampler = VK_NULL_HANDLE;
2183 }
2184 }
2185
2186 void destroyBuffers() {
2187 if (window == nullptr || window->getDevice() == VK_NULL_HANDLE) {
2188 vertexBuffer = VK_NULL_HANDLE;
2189 vertexMemory = VK_NULL_HANDLE;
2190 indexBuffer = VK_NULL_HANDLE;
2191 indexMemory = VK_NULL_HANDLE;
2192 return;
2193 }
2194
2195 if (vertexBuffer != VK_NULL_HANDLE) {
2196 vkDestroyBuffer(window->getDevice(), vertexBuffer, nullptr);
2197 vertexBuffer = VK_NULL_HANDLE;
2198 }
2199 if (vertexMemory != VK_NULL_HANDLE) {
2200 vkFreeMemory(window->getDevice(), vertexMemory, nullptr);
2201 vertexMemory = VK_NULL_HANDLE;
2202 }
2203 if (indexBuffer != VK_NULL_HANDLE) {
2204 vkDestroyBuffer(window->getDevice(), indexBuffer, nullptr);
2205 indexBuffer = VK_NULL_HANDLE;
2206 }
2207 if (indexMemory != VK_NULL_HANDLE) {
2208 vkFreeMemory(window->getDevice(), indexMemory, nullptr);
2209 indexMemory = VK_NULL_HANDLE;
2210 }
2211 }
2212
2213 void createImage(uint32_t width,
2214 uint32_t height,
2215 VkFormat format,
2216 VkImageTiling tiling,
2217 VkImageUsageFlags usage,
2218 VkMemoryPropertyFlags properties,
2219 VkImage &image,
2220 VkDeviceMemory &memory) const {
2221 VkImageCreateInfo imageInfo{};
2222 imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
2223 imageInfo.imageType = VK_IMAGE_TYPE_2D;
2224 imageInfo.extent.width = width;
2225 imageInfo.extent.height = height;
2226 imageInfo.extent.depth = 1;
2227 imageInfo.mipLevels = 1;
2228 imageInfo.arrayLayers = 1;
2229 imageInfo.format = format;
2230 imageInfo.tiling = tiling;
2231 imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
2232 imageInfo.usage = usage;
2233 imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
2234 imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
2235
2236 if (vkCreateImage(window->getDevice(), &imageInfo, nullptr, &image) != VK_SUCCESS) {
2237 throw mxvk::Exception("walk: failed to create raw wall image");
2238 }
2239
2240 VkMemoryRequirements requirements{};
2241 vkGetImageMemoryRequirements(window->getDevice(), image, &requirements);
2242
2243 VkMemoryAllocateInfo allocInfo{};
2244 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
2245 allocInfo.allocationSize = requirements.size;
2246
2247 try {
2248 allocInfo.memoryTypeIndex = findMemoryType(requirements.memoryTypeBits, properties);
2249 if (vkAllocateMemory(window->getDevice(), &allocInfo, nullptr, &memory) != VK_SUCCESS) {
2250 throw mxvk::Exception("walk: failed to allocate raw wall image memory");
2251 }
2252
2253 if (vkBindImageMemory(window->getDevice(), image, memory, 0) != VK_SUCCESS) {
2254 throw mxvk::Exception("walk: failed to bind raw wall image memory");
2255 }
2256 } catch (...) {
2257 if (memory != VK_NULL_HANDLE) {
2258 vkFreeMemory(window->getDevice(), memory, nullptr);
2259 memory = VK_NULL_HANDLE;
2260 }
2261 if (image != VK_NULL_HANDLE) {
2262 vkDestroyImage(window->getDevice(), image, nullptr);
2263 image = VK_NULL_HANDLE;
2264 }
2265 throw;
2266 }
2267 }
2268
2269 VkImageView createImageView(VkImage image, VkFormat format, VkImageAspectFlags aspectFlags) const {
2270 VkImageViewCreateInfo viewInfo{};
2271 viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
2272 viewInfo.image = image;
2273 viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
2274 viewInfo.format = format;
2275 viewInfo.subresourceRange.aspectMask = aspectFlags;
2276 viewInfo.subresourceRange.baseMipLevel = 0;
2277 viewInfo.subresourceRange.levelCount = 1;
2278 viewInfo.subresourceRange.baseArrayLayer = 0;
2279 viewInfo.subresourceRange.layerCount = 1;
2280
2281 VkImageView imageView = VK_NULL_HANDLE;
2282 if (vkCreateImageView(window->getDevice(), &viewInfo, nullptr, &imageView) != VK_SUCCESS) {
2283 throw mxvk::Exception("walk: failed to create raw wall image view");
2284 }
2285 return imageView;
2286 }
2287
2288 [[nodiscard]] VkCommandBuffer beginSingleTimeCommands() const {
2289 VkCommandBufferAllocateInfo allocInfo{};
2290 allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
2291 allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
2292 allocInfo.commandPool = window->getCommandPool();
2293 allocInfo.commandBufferCount = 1;
2294
2295 VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
2296 if (vkAllocateCommandBuffers(window->getDevice(), &allocInfo, &commandBuffer) != VK_SUCCESS) {
2297 throw mxvk::Exception("walk: failed to allocate raw wall command buffer");
2298 }
2299
2300 VkCommandBufferBeginInfo beginInfo{};
2301 beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
2302 beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
2303 if (vkBeginCommandBuffer(commandBuffer, &beginInfo) != VK_SUCCESS) {
2304 vkFreeCommandBuffers(window->getDevice(), window->getCommandPool(), 1, &commandBuffer);
2305 throw mxvk::Exception("walk: failed to begin raw wall command buffer");
2306 }
2307
2308 return commandBuffer;
2309 }
2310
2311 void endSingleTimeCommands(VkCommandBuffer commandBuffer) const {
2312 if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) {
2313 vkFreeCommandBuffers(window->getDevice(), window->getCommandPool(), 1, &commandBuffer);
2314 throw mxvk::Exception("walk: failed to end raw wall command buffer");
2315 }
2316
2317 VkSubmitInfo submitInfo{};
2318 submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
2319 submitInfo.commandBufferCount = 1;
2320 submitInfo.pCommandBuffers = &commandBuffer;
2321
2322 if (vkQueueSubmit(window->getGraphicsQueue(), 1, &submitInfo, VK_NULL_HANDLE) != VK_SUCCESS) {
2323 vkFreeCommandBuffers(window->getDevice(), window->getCommandPool(), 1, &commandBuffer);
2324 throw mxvk::Exception("walk: failed to submit raw wall command buffer");
2325 }
2326 if (vkQueueWaitIdle(window->getGraphicsQueue()) != VK_SUCCESS) {
2327 vkFreeCommandBuffers(window->getDevice(), window->getCommandPool(), 1, &commandBuffer);
2328 throw mxvk::Exception("walk: failed to wait for raw wall upload queue");
2329 }
2330
2331 vkFreeCommandBuffers(window->getDevice(), window->getCommandPool(), 1, &commandBuffer);
2332 }
2333
2334 void transitionImageLayout(VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout) const {
2335 VkCommandBuffer cmd = beginSingleTimeCommands();
2336
2337 VkImageMemoryBarrier barrier{};
2338 barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
2339 barrier.oldLayout = oldLayout;
2340 barrier.newLayout = newLayout;
2341 barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
2342 barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
2343 barrier.image = image;
2344 barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
2345 barrier.subresourceRange.baseMipLevel = 0;
2346 barrier.subresourceRange.levelCount = 1;
2347 barrier.subresourceRange.baseArrayLayer = 0;
2348 barrier.subresourceRange.layerCount = 1;
2349
2350 VkPipelineStageFlags sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
2351 VkPipelineStageFlags destinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
2352 if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
2353 barrier.srcAccessMask = 0;
2354 barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
2355 } else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
2356 barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
2357 barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
2358 sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
2359 destinationStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
2360 }
2361
2362 vkCmdPipelineBarrier(cmd, sourceStage, destinationStage, 0, 0, nullptr, 0, nullptr, 1, &barrier);
2363 endSingleTimeCommands(cmd);
2364 }
2365
2366 void copyBufferToImage(VkBuffer buffer, VkImage image, uint32_t width, uint32_t height) const {
2367 VkCommandBuffer cmd = beginSingleTimeCommands();
2368 VkBufferImageCopy region{};
2369 region.bufferOffset = 0;
2370 region.bufferRowLength = 0;
2371 region.bufferImageHeight = 0;
2372 region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
2373 region.imageSubresource.mipLevel = 0;
2374 region.imageSubresource.baseArrayLayer = 0;
2375 region.imageSubresource.layerCount = 1;
2376 region.imageOffset = {0, 0, 0};
2377 region.imageExtent = {width, height, 1};
2378
2379 vkCmdCopyBufferToImage(cmd, buffer, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
2380 endSingleTimeCommands(cmd);
2381 }
2382
2383 std::vector<char> vertSpv{};
2384 std::vector<char> fragSpv{};
2385
2386 uint32_t vertexCount = 0;
2387 uint32_t indexCount = 0;
2388 VkBuffer vertexBuffer = VK_NULL_HANDLE;
2389 VkDeviceMemory vertexMemory = VK_NULL_HANDLE;
2390 VkBuffer indexBuffer = VK_NULL_HANDLE;
2391 VkDeviceMemory indexMemory = VK_NULL_HANDLE;
2392
2393 VkImage textureImage = VK_NULL_HANDLE;
2394 VkDeviceMemory textureMemory = VK_NULL_HANDLE;
2395 VkImageView textureView = VK_NULL_HANDLE;
2396 VkSampler textureSampler = VK_NULL_HANDLE;
2397
2398 VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
2399 VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
2400 std::vector<VkDescriptorSet> descriptorSets{};
2401
2402 VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
2403 VkPipeline pipeline = VK_NULL_HANDLE;
2404
2405 std::vector<VkBuffer> uniformBuffers{};
2406 std::vector<VkDeviceMemory> uniformBufferMemory{};
2407 std::vector<void *> uniformBuffersMapped{};
2408
2409 VkDevice device [[maybe_unused]] = VK_NULL_HANDLE;
2410 mxvk::VK_Window *window = nullptr;
2411 };
2412
2413 class WalkWindow final : public mxvk::VK_IOWindow {
2414 public:
2416 : mxvk::VK_IOWindow(args.path, "FPS Maze Room - MXVK", args.width, args.height, args.fullscreen, args.enable_vsync),
2417 assetRoot((args.path.empty() || args.path == ".") ? std::string(WALK_ASSET_DIR) : args.path),
2418 shaderRoot(assetRoot + "/data"),
2419 modelRoot(assetRoot + "/data") {
2420 logEnv(std::format("initializing window {}x{} (fullscreen={})", args.width, args.height, args.fullscreen ? "true" : "false"));
2421 logEnv(std::format("asset root: {}", assetRoot));
2422 logEnv(std::format("model root: {}", modelRoot));
2423
2424 std::mt19937 rng(static_cast<uint32_t>(std::chrono::high_resolution_clock::now().time_since_epoch().count()));
2425 world.generate(rng());
2426 logEnv(std::format("world generated (walls={}, pillars={}, collectibles={})",
2427 world.walls().size(),
2428 world.pillars().size(),
2429 world.collectibles().size()));
2430 setClearColor(100.0f / 255.0f, 181.0f / 255.0f, 246.0f / 255.0f, 1.0f);
2431
2432 cameraPos = world.startPosition();
2433 yaw = chooseBestSpawnYaw(cameraPos);
2434 pitch = 0.0f;
2435 updateCameraVectors();
2436
2437 setFont(assetRoot + "/data/font.ttf", 22);
2438
2439 const std::string vertPath = shaderRoot + "/model.vert.spv";
2440 const std::string wallFragPath = shaderRoot + "/wall.frag.spv";
2441 const std::string floorFragPath = shaderRoot + "/floor.frag.spv";
2442 const std::string pillarVertPath = shaderRoot + "/pillar.vert.spv";
2443 const std::string pillarFragPath = shaderRoot + "/pillar.frag.spv";
2444 const std::string objectFragPath = shaderRoot + "/object.frag.spv";
2445 const std::string bulletFragPath = shaderRoot + "/bullet.frag.spv";
2446 const std::string particleFragPath = shaderRoot + "/particle.frag.spv";
2447 const std::string groundTexManifest = assetRoot + "/data/ground.tex";
2448
2449 modelVertSpv = vertPath;
2450 pillarVertSpv = pillarVertPath;
2451 wallFragSpv = wallFragPath;
2452 floorFragSpv = floorFragPath;
2453 pillarFragSpv = pillarFragPath;
2454 objectFragSpv = objectFragPath;
2455 bulletFragSpv = bulletFragPath;
2456
2457 loadModel(floorModel, modelRoot + "/cube.mxmod.z", groundTexManifest, assetRoot + "/data", vertPath, floorFragPath);
2458 loadModel(bulletModel, modelRoot + "/sphere.mxmod.z", "", "", vertPath, bulletFragPath);
2459
2460 logEnv("loading wall renderer assets");
2461 rawWallRenderer.load(this,
2462 groundTexManifest,
2463 assetRoot + "/data",
2464 loadSpv(pillarVertPath),
2465 loadSpv(wallFragPath));
2466 logEnv("wall renderer ready");
2467
2468 logEnv("loading pillar renderer assets");
2469 rawPillarRenderer.load(this,
2470 groundTexManifest,
2471 assetRoot + "/data",
2472 loadSpv(pillarVertPath),
2473 loadSpv(pillarFragPath));
2474 logEnv("pillar renderer ready");
2475
2476 loadModel(saturnModel, assetRoot + "/data/saturn.mxmod.z",
2477 assetRoot + "/data/planet.tex", assetRoot + "/data", vertPath, objectFragPath);
2478 loadModel(birdModel, assetRoot + "/data/tux.obj",
2479 assetRoot + "/data/tux.mtl", assetRoot + "/data", vertPath, objectFragPath);
2480 loadModel(blasterModel, assetRoot + "/data/blaster.obj",
2481 assetRoot + "/data/blaster.mtl", assetRoot + "/data", vertPath, objectFragPath);
2482 normalizeCollectiblesToModel();
2483
2484 pointParticleVertSpv = shaderRoot + "/particle_points.vert.spv";
2485 pointParticleFragSpv = shaderRoot + "/particle_points.frag.spv";
2486 initializePointParticles();
2487 logEnv("point-particle pipeline initialized");
2488
2489 loadPostProcessingShaderIndex(args.shaderPath);
2490 setPostProcessingShaderIndex(args.shader_index);
2491 applyPostProcessingShaderSelection();
2492
2493 tryOpenFirstGamepad();
2494 SDL_SetWindowRelativeMouseMode(getSDLWindow(), true);
2495 logEnv("mouse capture enabled");
2496 }
2497
2498 ~WalkWindow() override {
2499 logEnv("shutting down walk window");
2500 if (gamepad != nullptr) {
2501 SDL_CloseGamepad(gamepad);
2502 gamepad = nullptr;
2503 gamepadId = 0;
2504 }
2505 if (device != VK_NULL_HANDLE) {
2506 vkDeviceWaitIdle(device);
2507 destroyPointParticles();
2508 cleanupModels();
2509 }
2510 }
2511
2512 void event(SDL_Event &e) override {
2513 const bool is_left_double_click =
2514 (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN &&
2515 e.button.button == SDL_BUTTON_LEFT &&
2516 e.button.clicks >= 2);
2517
2518 if (is_left_double_click) {
2519 if (SDL_Window *const sdlWindow = getSDLWindow(); sdlWindow != nullptr) {
2520 SDL_RaiseWindow(sdlWindow);
2521 SDL_SetWindowMouseGrab(sdlWindow, true);
2522 SDL_SetWindowRelativeMouseMode(sdlWindow, true);
2523 }
2524
2525 mouseCapture = true;
2526 firstMouse = true;
2527 if (!visible()) {
2528 suppressProjectileOnNextLeftDown = true;
2529 }
2530 logEnv("mouse capture enabled (double-click)");
2531 }
2532
2534 }
2535
2536 void console_event(SDL_Event &e) override {
2537 if (e.type == SDL_EVENT_QUIT) {
2538 logEnv("received quit event");
2539 exit();
2540 return;
2541 }
2542
2543 if (e.type == SDL_EVENT_KEY_DOWN) {
2544 if (e.key.key == SDLK_ESCAPE) {
2545 if (mouseCapture) {
2546 mouseCapture = false;
2547 SDL_SetWindowRelativeMouseMode(getSDLWindow(), false);
2548 suppressProjectileOnNextLeftDown = false;
2549 logEnv("mouse capture disabled (ESC)");
2550 } else {
2551 logEnv("exit requested by ESC");
2552 exit();
2553 return;
2554 }
2555 } else if (e.key.key == SDLK_F) {
2556 showFps = !showFps;
2557 logEnv(std::format("FPS overlay {}", showFps ? "enabled" : "disabled"));
2558 } else if (e.key.key == SDLK_R && !e.key.repeat) {
2559 selectPostProcessingShader(-1);
2560 } else if (e.key.key == SDLK_T && !e.key.repeat) {
2561 selectPostProcessingShader(1);
2562 }
2563 }
2564
2565 if (e.type == SDL_EVENT_GAMEPAD_ADDED) {
2566 logEnv(std::format("gamepad added (id={})", static_cast<int>(e.gdevice.which)));
2567 openGamepad(e.gdevice.which);
2568 }
2569
2570 if (e.type == SDL_EVENT_GAMEPAD_REMOVED) {
2571 if (gamepad != nullptr && e.gdevice.which == gamepadId) {
2572 logEnv(std::format("gamepad removed (id={})", static_cast<int>(e.gdevice.which)));
2573 SDL_CloseGamepad(gamepad);
2574 gamepad = nullptr;
2575 gamepadId = 0;
2576 }
2577 }
2578
2579 if (e.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
2580 if (e.gbutton.button == SDL_GAMEPAD_BUTTON_BACK || e.gbutton.button == SDL_GAMEPAD_BUTTON_START) {
2581 logEnv("exit requested by gamepad back/start");
2582 exit();
2583 } else if (e.gbutton.button == SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER) {
2584 fireProjectile();
2585 } else if (e.gbutton.button == SDL_GAMEPAD_BUTTON_SOUTH && cameraPos.y <= 1.71f) {
2586 jumpVelocity = 0.3f;
2587 logEnv("jump triggered by gamepad");
2588 }
2589 }
2590
2591 if (e.type == SDL_EVENT_MOUSE_MOTION && mouseCapture) {
2592 if (firstMouse) {
2593 firstMouse = false;
2594 return;
2595 }
2596 yaw += static_cast<float>(e.motion.xrel) * mouseSensitivity;
2597 pitch -= static_cast<float>(e.motion.yrel) * mouseSensitivity;
2598 pitch = glm::clamp(pitch, -89.0f, 89.0f);
2599 updateCameraVectors();
2600 }
2601
2602 if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT && mouseCapture) {
2603 if (suppressProjectileOnNextLeftDown) {
2604 suppressProjectileOnNextLeftDown = false;
2605 return;
2606 }
2607 fireProjectile();
2608 }
2609 }
2610
2611 void console_proc() override {
2612 tryOpenFirstGamepad();
2613 const auto now = std::chrono::steady_clock::now();
2614 float deltaTime = std::chrono::duration<float>(now - lastTick).count();
2615 lastTick = now;
2616 deltaTime = std::clamp(deltaTime, 0.0f, 0.05f);
2617 updatePostProcessingShaderUniforms(deltaTime);
2618
2619 if (!visible()) {
2620 updatePlayer(deltaTime);
2621 }
2622 updateProjectiles(deltaTime);
2623 updateExplosions(deltaTime);
2624 updateCollectibles(deltaTime);
2625
2626 const int aliveObjects = world.activeCollectibles();
2627 if (!visible()) {
2628 printText(std::format("Objects left: {}", aliveObjects), 20, 20, {255, 255, 255, 255});
2629 printText(std::format("Active Bullets: {}", bullets.size()), 20, 48, {255, 220, 120, 255});
2630 if (showFps && deltaTime > 0.0001f) {
2631 const int fps = static_cast<int>(1.0f / deltaTime);
2632 printText(std::format("FPS: {}", fps), 20, 76, {120, 255, 120, 255});
2633 }
2634
2635 const VkExtent2D extent = getSwapchainExtent();
2636 const int cx = static_cast<int>(extent.width / 2U);
2637 const int cy = static_cast<int>(extent.height / 2U);
2638 printText("+", cx - 6, cy - 12, {255, 64, 64, 255});
2639 printText("3D Room - WASD/Left Stick move, Mouse/Right Stick look, Click/RB shoot, Back/Start quit", 20, static_cast<int>(extent.height) - 36, {210, 210, 210, 255});
2640 if (!postProcessingShaders.empty()) {
2641 printText(std::format("Post FX: {} / {} R/T", postProcessingShaderIndex + 1, postProcessingShaders.size()), 20, 104, {180, 220, 255, 255});
2642 }
2643 }
2644 }
2645
2646 void onSwapchainRecreated() override {
2647 logEnv("swapchain recreated; resizing render resources");
2648 floorModel.resize(this);
2649 rawWallRenderer.resize(this);
2650 rawPillarRenderer.resize(this);
2651 saturnModel.resize(this);
2652 birdModel.resize(this);
2653 blasterModel.resize(this);
2654 bulletModel.resize(this);
2655 rebuildPointParticlePipeline();
2656 }
2657
2658 void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t imageIndex) override {
2659 const VkExtent2D extent = getSwapchainExtent();
2660 const float aspect = (extent.height > 0U)
2661 ? static_cast<float>(extent.width) / static_cast<float>(extent.height)
2662 : 1.0f;
2663
2664 const glm::mat4 view = glm::lookAt(cameraPos, cameraPos + cameraFront, glm::vec3(0.0f, 1.0f, 0.0f));
2665 glm::mat4 proj = glm::perspective(glm::radians(45.0f), aspect, 0.1f, 1000.0f);
2666 proj[1][1] *= -1.0f;
2667
2668 const float t = static_cast<float>(SDL_GetTicks()) * 0.001f;
2669
2670 // Floor: a thin slab sized to cover the maze footprint.
2671 {
2672 constexpr float floorHalfSize = 100.0f;
2673 constexpr float floorThickness = 0.04f;
2674 const glm::vec3 extent = floorModel.modelAxisExtent();
2675 const glm::vec3 srcScale(
2676 (floorHalfSize * 2.0f) / std::max(extent.x, 1e-4f),
2677 floorThickness / std::max(extent.y, 1e-4f),
2678 (floorHalfSize * 2.0f) / std::max(extent.z, 1e-4f));
2679 glm::mat4 floorWorld = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, -0.02f, 0.0f));
2680 floorWorld = glm::scale(floorWorld, srcScale);
2681 renderModel(cmd, imageIndex, floorModel, floorWorld, view, proj,
2682 glm::vec4(0.0f, 0.0f, 0.0f, t), false);
2683 }
2684
2685 rawWallRenderer.render(cmd,
2686 imageIndex,
2687 world.walls(),
2688 world.wallThickness(),
2689 view,
2690 proj,
2691 glm::vec4(0.58f, 0.58f, 0.65f, t));
2692
2693 rawPillarRenderer.render(cmd, imageIndex, world.pillars(), view, proj, glm::vec4(0.0f, 0.0f, 0.0f, t));
2694
2695 for (const Collectible &obj : world.collectibles()) {
2696 if (!obj.active) {
2697 continue;
2698 }
2699 glm::mat4 world = glm::translate(glm::mat4(1.0f), obj.position);
2700 world = glm::rotate(world, glm::radians(obj.rotation.y), glm::vec3(0.0f, 1.0f, 0.0f));
2701 world = glm::scale(world, obj.scale);
2702 if (obj.type == Collectible::Type::Saturn) {
2703 renderRawModel(cmd, imageIndex, saturnModel, world, view, proj, glm::vec4(cameraPos, 0.0f));
2704 } else {
2705 renderRawModel(cmd, imageIndex, birdModel, world, view, proj, glm::vec4(cameraPos, 0.0f));
2706 }
2707 }
2708
2709 if (!visible()) {
2710 renderRawModel(cmd,
2711 imageIndex,
2712 blasterModel,
2713 blasterWorldTransform(),
2714 view,
2715 proj,
2716 glm::vec4(cameraPos, 0.0f));
2717 }
2718
2719 for (const Projectile &bullet : bullets) {
2720 if (!bullet.active) {
2721 continue;
2722 }
2723 if (bullet.lifetime < 0.05f) {
2724 continue;
2725 }
2726 glm::mat4 world = glm::translate(glm::mat4(1.0f), bullet.position);
2727 world = glm::scale(world, glm::vec3(0.07f, 0.07f, 0.20f));
2728 const float fadeProgress = glm::clamp(bullet.lifetime / bullet.maxLifetime, 0.0f, 1.0f);
2729 const float distanceProgress = glm::clamp(bullet.distanceTraveled / bullet.maxDistance, 0.0f, 1.0f);
2730 const float alpha = std::min(1.0f - fadeProgress, 1.0f - distanceProgress);
2731 renderRawModel(cmd, imageIndex, bulletModel, world, view, proj, glm::vec4(alpha, 0.0f, 0.0f, 0.0f));
2732 }
2733 renderPointParticles(cmd, view, proj);
2734 }
2735
2736 private:
2737 enum class ProjectileHitType {
2738 None,
2739 Floor,
2740 Wall,
2741 Pillar,
2742 };
2743
2744 struct ProjectileTraceHit {
2745 ProjectileHitType type = ProjectileHitType::None;
2746 glm::vec3 impact{0.0f};
2747 size_t collectibleIndex = 0;
2748 };
2749
2750 [[nodiscard]] ProjectileTraceHit traceProjectileSegment(const glm::vec3 &from, const glm::vec3 &to) const {
2751 const glm::vec3 dir = to - from;
2752 const float travel = glm::length(dir);
2753 if (travel <= 1e-8f) {
2754 return {};
2755 }
2756
2757 constexpr float sampleStride = 0.03f;
2758 constexpr float projectileRadius = 0.015f;
2759 const int steps = std::max(1, static_cast<int>(std::ceil(travel / sampleStride)));
2760 for (int i = 0; i <= steps; ++i) {
2761 const float t = static_cast<float>(i) / static_cast<float>(steps);
2762 const glm::vec3 point = from + (dir * t);
2763 if (pointHitsWall3D(point, projectileRadius)) {
2764 return {ProjectileHitType::Wall, point, 0};
2765 }
2766 if (pointHitsPillar3D(point, projectileRadius)) {
2767 return {ProjectileHitType::Pillar, point, 0};
2768 }
2769 if (point.y <= 0.0f) {
2770 return {ProjectileHitType::Floor, point, 0};
2771 }
2772 }
2773
2774 return {};
2775 }
2776
2777 bool handleConsoleCommand(const std::vector<std::string> &args, std::ostream &out) override {
2778 if (args.empty()) {
2779 return true;
2780 }
2781
2782 const std::string &cmd = args[0];
2783
2784 if (cmd == "spawn_random" || (cmd == "spawn" && args.size() >= 2 && args[1] == "random")) {
2785 const int attempts = (args.size() >= 3 && cmd == "spawn") ? parseIntOrDefault(args[2], 128)
2786 : ((args.size() >= 2 && cmd == "spawn_random") ? parseIntOrDefault(args[1], 128) : 128);
2787 glm::vec3 candidate = cameraPos;
2788 if (!sampleNavigablePoint(1.7f, 0.68f, candidate, std::max(1, attempts))) {
2789 candidate = world.startPosition();
2790 }
2791
2792 cameraPos = candidate;
2793 yaw = chooseBestSpawnYaw(cameraPos);
2794 pitch = 0.0f;
2795 updateCameraVectors();
2796
2797 out << std::format("Spawned at random location ({:.2f}, {:.2f}, {:.2f})", cameraPos.x, cameraPos.y, cameraPos.z);
2798 logEnv("command: spawn_random");
2799 return true;
2800 }
2801
2802 if (cmd == "reset" || cmd == "reset_collectibles") {
2803 std::vector<Collectible> &collectibles = world.collectibles();
2804 for (size_t i = 0; i < collectibles.size(); ++i) {
2805 Collectible &obj = collectibles[i];
2806 obj.active = true;
2807 obj.rotation = glm::vec3(0.0f);
2808 relocateCollectible(i, 2.0f, 128);
2809 }
2810 resolveCollectibleClusters(2.0f, 4);
2811 destroyedCount = 0;
2812 out << std::format("Collectibles reset. Active collectibles: {}", world.activeCollectibles());
2813 logEnv("command: reset collectibles");
2814 return true;
2815 }
2816
2817 if (cmd == "add_collectibles" || cmd == "add_collectables") {
2818 const int requested = (args.size() >= 2) ? parseIntOrDefault(args[1], 10) : 10;
2819 const int toAdd = std::clamp(requested, 1, 200);
2820 std::uniform_int_distribution<int> typeDist(0, 1);
2821 std::uniform_real_distribution<float> saturnScale(0.4f, 0.8f);
2822 std::uniform_real_distribution<float> saturnRotSpeed(5.0f, 15.0f);
2823 std::uniform_real_distribution<float> birdScale(0.3f, 0.5f);
2824 std::uniform_real_distribution<float> birdRotSpeed(20.0f, 60.0f);
2825
2826 int added = 0;
2827 for (int i = 0; i < toAdd; ++i) {
2828 Collectible obj{};
2829 obj.type = (typeDist(rng) == 0) ? Collectible::Type::Saturn : Collectible::Type::Bird;
2830 if (obj.type == Collectible::Type::Saturn) {
2831 const float scale = saturnScale(rng);
2832 obj.scale = glm::vec3(scale);
2833 obj.rotationSpeed = saturnRotSpeed(rng);
2834 obj.radius = saturnHitRadiusForScale(scale);
2835 obj.hitCenterOffset = saturnHitCenterOffsetForScale(scale);
2836 } else {
2837 const float scale = birdScale(rng);
2838 obj.scale = glm::vec3(scale);
2839 obj.rotationSpeed = birdRotSpeed(rng);
2840 obj.radius = birdHitHalfSideForScale(scale);
2841 obj.hitCenterOffset = birdHitCenterOffsetForScale(scale);
2842 }
2843
2844 bool placed = false;
2845 for (int attempt = 0; attempt < 96; ++attempt) {
2846 const float y = (obj.type == Collectible::Type::Bird) ? birdGroundYForScale(obj.scale.x) : 2.5f;
2847 const float placementRadius = placementRadiusForCollectible(obj);
2848 glm::vec3 candidate{};
2849 if (!sampleNavigablePoint(y, placementRadius, candidate, 1)) {
2850 continue;
2851 }
2852
2853 bool overlaps = false;
2854 for (const Collectible &existing : world.collectibles()) {
2855 if (!existing.active) {
2856 continue;
2857 }
2858 const float separation = std::max(5.0f, existing.radius + obj.radius + 0.2f);
2859 if (glm::length(existing.position - candidate) < separation) {
2860 overlaps = true;
2861 break;
2862 }
2863 }
2864
2865 if (!overlaps) {
2866 obj.position = candidate;
2867 placed = true;
2868 break;
2869 }
2870 }
2871
2872 if (placed) {
2873 world.collectibles().push_back(obj);
2874 ++added;
2875 }
2876 }
2877
2878 out << std::format("Added {} collectible(s). Active collectibles: {}",
2879 added,
2880 world.activeCollectibles());
2881 resolveCollectibleClusters(2.0f, 4);
2882 logEnv(std::format("command: add_collectibles requested={} added={}", toAdd, added));
2883 return true;
2884 }
2885
2886 if (cmd == "status") {
2887 out << std::format("pos=({:.2f}, {:.2f}, {:.2f}) yaw={:.2f} pitch={:.2f}\n"
2888 "walls={} pillars={} collectibles(active/total)={}/{} bullets={} particles={} destroyed={}",
2889 cameraPos.x,
2890 cameraPos.y,
2891 cameraPos.z,
2892 yaw,
2893 pitch,
2894 world.walls().size(),
2895 world.pillars().size(),
2896 world.activeCollectibles(),
2897 world.collectibles().size(),
2898 bullets.size(),
2899 explosionParticles.size(),
2900 destroyedCount);
2901 return true;
2902 }
2903
2904 if (cmd == "teleport") {
2905 if (args.size() < 4) {
2906 out << "Usage: teleport <x> <y> <z>";
2907 return true;
2908 }
2909
2910 float x = 0.0f;
2911 float y = 0.0f;
2912 float z = 0.0f;
2913 if (!tryParseFloat(args[1], x) || !tryParseFloat(args[2], y) || !tryParseFloat(args[3], z)) {
2914 out << "teleport: invalid numeric argument(s)";
2915 return true;
2916 }
2917
2918 const glm::vec3 candidate{x, y, z};
2919 if (world.checkWallCollision(candidate, 0.68f) || world.checkPillarCollision(candidate, 0.68f)) {
2920 out << "teleport blocked: target intersects wall/pillar";
2921 return true;
2922 }
2923
2924 cameraPos = candidate;
2925 out << std::format("Teleported to ({:.2f}, {:.2f}, {:.2f})", x, y, z);
2926 logEnv("command: teleport");
2927 return true;
2928 }
2929
2930 if (cmd == "clear_bullets") {
2931 const std::size_t removed = bullets.size();
2932 bullets.clear();
2933 out << std::format("Cleared {} bullet(s)", removed);
2934 return true;
2935 }
2936
2937 if (cmd == "clear_fx") {
2938 const std::size_t removed = explosionParticles.size();
2939 explosionParticles.clear();
2940 out << std::format("Cleared {} particle effect(s)", removed);
2941 return true;
2942 }
2943
2944 if (cmd == "set_fps") {
2945 if (args.size() < 2) {
2946 out << std::format("FPS overlay is currently {}. Usage: set_fps <on|off>", showFps ? "on" : "off");
2947 return true;
2948 }
2949 const std::string value = toLowerCopy(args[1]);
2950 if (value == "on" || value == "1" || value == "true") {
2951 showFps = true;
2952 out << "FPS overlay enabled";
2953 return true;
2954 }
2955 if (value == "off" || value == "0" || value == "false") {
2956 showFps = false;
2957 out << "FPS overlay disabled";
2958 return true;
2959 }
2960
2961 out << "Usage: set_fps <on|off>";
2962 return true;
2963 }
2964
2965 if (cmd == "regen_world") {
2966 const uint32_t seed = (args.size() >= 2) ? static_cast<uint32_t>(parseIntOrDefault(args[1], static_cast<int>(rng())))
2967 : rng();
2968 world.generate(seed);
2969 normalizeCollectiblesToModel();
2970 cameraPos = world.startPosition();
2971 yaw = chooseBestSpawnYaw(cameraPos);
2972 pitch = 0.0f;
2973 updateCameraVectors();
2974 bullets.clear();
2975 explosionParticles.clear();
2976 destroyedCount = 0;
2977
2978 out << std::format("Regenerated world with seed {} (walls={}, pillars={}, collectibles={})",
2979 seed,
2980 world.walls().size(),
2981 world.pillars().size(),
2982 world.collectibles().size());
2983 logEnv(std::format("command: regen_world seed={}", seed));
2984 return true;
2985 }
2986
2987 if (cmd == "set_wall" || cmd == "set_floor" || cmd == "set_pillar" || cmd == "set_object" || cmd == "set_bullet") {
2988 if (args.size() < 2) {
2989 out << std::format("Usage: {} <shader.spv|full/path/to/shader.spv>", cmd);
2990 return true;
2991 }
2992
2993 const std::string shaderPath = resolveShaderPath(args[1]);
2994 std::vector<char> shaderBytes;
2995 try {
2996 shaderBytes = loadSpv(shaderPath);
2997 } catch (const mxvk::Exception &e) {
2998 out << std::format("{}: failed to load shader '{}': {}", cmd, shaderPath, e.text());
2999 return true;
3000 }
3001
3002 if (shaderBytes.empty()) {
3003 out << std::format("{}: shader '{}' is empty", cmd, shaderPath);
3004 return true;
3005 }
3006
3007 if (cmd == "set_wall") {
3008 wallFragSpv = shaderPath;
3009 rawWallRenderer.reloadFragShader(shaderBytes);
3010 out << std::format("Wall shader reloaded from {}", shaderPath);
3011 } else if (cmd == "set_floor") {
3012 floorFragSpv = shaderPath;
3013 floorModel.setShaders(this, modelVertSpv, shaderPath);
3014 out << std::format("Floor shader reloaded from {}", shaderPath);
3015 } else if (cmd == "set_pillar") {
3016 pillarFragSpv = shaderPath;
3017 rawPillarRenderer.reloadFragShader(shaderBytes);
3018 out << std::format("Pillar shader reloaded from {}", shaderPath);
3019 } else if (cmd == "set_object") {
3020 objectFragSpv = shaderPath;
3021 saturnModel.setShaders(this, modelVertSpv, shaderPath);
3022 birdModel.setShaders(this, modelVertSpv, shaderPath);
3023 out << std::format("Object shader reloaded from {}", shaderPath);
3024 } else if (cmd == "set_bullet") {
3025 bulletFragSpv = shaderPath;
3026 bulletModel.setShaders(this, modelVertSpv, shaderPath);
3027 out << std::format("Bullet shader reloaded from {}", shaderPath);
3028 }
3029
3030 logEnv(std::format("command: {} shader={}", cmd, shaderPath));
3031 return true;
3032 }
3033
3034 if (cmd == "list_shaders") {
3035 const std::array<std::pair<std::string_view, std::string>, 11> shaders{{
3036 {"wall.frag", resolveShaderPath("wall.frag.spv")},
3037 {"floor.frag", resolveShaderPath("floor.frag.spv")},
3038 {"pillar.frag", resolveShaderPath("pillar.frag.spv")},
3039 {"object.frag", resolveShaderPath("object.frag.spv")},
3040 {"bullet.frag", resolveShaderPath("bullet.frag.spv")},
3041 {"particle.frag", resolveShaderPath("particle.frag.spv")},
3042 {"particle_points.frag", resolveShaderPath("particle_points.frag.spv")},
3043 {"bubble.frag", resolveShaderPath("bubble.frag.spv")},
3044 {"floor_kale.frag", resolveShaderPath("floor_kale.frag.spv")},
3045 {"floor_swirl.frag", resolveShaderPath("floor_swirl.frag.spv")},
3046 {"floor_twist.frag", resolveShaderPath("floor_twist.frag.spv")},
3047 }};
3048
3049 out << "Available shaders:\n";
3050 for (const auto &[name, path] : shaders) {
3051 out << std::format(" {:<20} {}\n", name, path);
3052 }
3053 out << std::format("Current bindings:\n"
3054 " wall {}\n"
3055 " floor {}\n"
3056 " pillar {}\n"
3057 " object {}\n"
3058 " bullet {}\n",
3059 wallFragSpv,
3060 floorFragSpv,
3061 pillarFragSpv,
3062 objectFragSpv,
3063 bulletFragSpv);
3064 if (!postProcessingShaders.empty()) {
3065 out << std::format(" post_fx {} of {} {}\n",
3066 postProcessingShaderIndex + 1,
3067 postProcessingShaders.size(),
3068 currentPostProcessingShader());
3069 }
3070 return true;
3071 }
3072
3073 return false;
3074 }
3075
3076 void appendConsoleHelp(std::ostream &out) const override {
3077 out << "\nWalk debug commands:\n"
3078 << " spawn_random [attempts] Spawn player at random valid location\n"
3079 << " spawn random [attempts] Alias for spawn_random\n"
3080 << " reset Reset all collectibles to active\n"
3081 << " add_collectibles [count] Add random collectibles (alias: add_collectables)\n"
3082 << " status Print camera/world/debug state\n"
3083 << " teleport <x> <y> <z> Teleport player if destination is valid\n"
3084 << " clear_bullets Remove all active bullets\n"
3085 << " clear_fx Remove all active explosion particles\n"
3086 << " set_fps <on|off> Toggle FPS overlay\n"
3087 << " set_wall <shader.spv> Reload wall fragment shader\n"
3088 << " set_floor <shader.spv> Reload floor fragment shader\n"
3089 << " set_pillar <shader.spv> Reload pillar fragment shader\n"
3090 << " set_object <shader.spv> Reload object fragment shader\n"
3091 << " set_bullet <shader.spv> Reload bullet fragment shader\n"
3092 << " list_shaders Print available shaders and current bindings\n"
3093 << " R/T Select previous/next post-processing shader\n"
3094 << " regen_world [seed] Regenerate maze, pillars, and collectibles";
3095 }
3096
3097 void logEnv(const std::string &message) {
3098 print(std::format("[walk] {}", message), {255, 100, 255, 255});
3099 }
3100
3101 [[nodiscard]] static std::string trimLine(std::string value) {
3102 auto begin = value.begin();
3103 while (begin != value.end() && std::isspace(static_cast<unsigned char>(*begin)) != 0) {
3104 ++begin;
3105 }
3106
3107 auto end = value.end();
3108 while (end != begin && std::isspace(static_cast<unsigned char>(*(end - 1))) != 0) {
3109 --end;
3110 }
3111
3112 return std::string(begin, end);
3113 }
3114
3115 [[nodiscard]] static std::string joinPath(const std::string &base, const std::string &file) {
3116 const std::filesystem::path filePath(file);
3117 if (base.empty() || filePath.is_absolute()) {
3118 return filePath.string();
3119 }
3120 return (std::filesystem::path(base) / filePath).string();
3121 }
3122
3123 [[nodiscard]] static std::string resolvePostProcessingShaderEntry(const std::string &shaderPath, const std::string &entry) {
3124 const std::filesystem::path entryPath(entry);
3125 if (entryPath.is_absolute() || entryPath.extension() == ".spv") {
3126 return joinPath(shaderPath, entry);
3127 }
3128
3129 std::filesystem::path spvEntry = entryPath.parent_path() / "spv" / entryPath.stem();
3130 spvEntry.replace_extension(".spv");
3131 const std::filesystem::path spvPath = std::filesystem::path(shaderPath) / spvEntry;
3132 if (std::filesystem::exists(spvPath)) {
3133 return spvPath.string();
3134 }
3135
3136 std::filesystem::path siblingEntry = entryPath;
3137 siblingEntry.replace_extension(".spv");
3138 const std::filesystem::path siblingSpvPath = std::filesystem::path(shaderPath) / siblingEntry;
3139 if (std::filesystem::exists(siblingSpvPath)) {
3140 return siblingSpvPath.string();
3141 }
3142
3143 return joinPath(shaderPath, entry);
3144 }
3145
3146 void loadPostProcessingShaderIndex(const std::string &shaderPath) {
3147 postProcessingShaderPath = shaderPath;
3148 postProcessingShaders.clear();
3149 postProcessingShaderIndex = 0;
3150
3151 if (postProcessingShaderPath.empty()) {
3153 return;
3154 }
3155
3156 const std::string indexPath = joinPath(postProcessingShaderPath, "index.txt");
3157 std::ifstream input(indexPath);
3158 if (!input.is_open()) {
3159 throw mxvk::Exception("walk_post: failed to open post-processing shader index: " + indexPath);
3160 }
3161
3162 std::string line;
3163 while (std::getline(input, line)) {
3164 const size_t comment = line.find('#');
3165 if (comment != std::string::npos) {
3166 line.resize(comment);
3167 }
3168
3169 const std::string entry = trimLine(line);
3170 if (entry.empty()) {
3171 continue;
3172 }
3173
3174 const std::string shaderFile = resolvePostProcessingShaderEntry(postProcessingShaderPath, entry);
3175 if (std::filesystem::path(shaderFile).extension() != ".spv") {
3176 throw mxvk::Exception("walk_post: post-processing shader entry is not SPIR-V: " + entry);
3177 }
3178 if (!std::filesystem::exists(shaderFile)) {
3179 throw mxvk::Exception("walk_post: post-processing shader listed in index.txt was not found: " + shaderFile);
3180 }
3181
3182 postProcessingShaders.push_back(shaderFile);
3183 }
3184
3185 if (postProcessingShaders.empty()) {
3186 throw mxvk::Exception("walk_post: post-processing shader index did not list any shaders: " + indexPath);
3187 }
3188
3189 logEnv(std::format("loaded {} post-processing shader(s) from {}", postProcessingShaders.size(), indexPath));
3190 }
3191
3192 void setPostProcessingShaderIndex(const int index) {
3193 if (postProcessingShaders.empty()) {
3194 postProcessingShaderIndex = 0;
3195 return;
3196 }
3197
3198 const int shaderCount = static_cast<int>(postProcessingShaders.size());
3199 postProcessingShaderIndex = index % shaderCount;
3200 if (postProcessingShaderIndex < 0) {
3201 postProcessingShaderIndex += shaderCount;
3202 }
3203 }
3204
3205 [[nodiscard]] const std::string &currentPostProcessingShader() const {
3206 return postProcessingShaders[static_cast<std::size_t>(postProcessingShaderIndex)];
3207 }
3208
3209 void applyPostProcessingShaderSelection() {
3210 if (postProcessingShaders.empty()) {
3211 return;
3212 }
3213
3214 if (postProcessingSprite != nullptr) {
3215 postProcessingSprite->setFragmentShaderPath(currentPostProcessingShader());
3216 postProcessingFrameCount = 0;
3217 postProcessingStartTime = std::chrono::steady_clock::now();
3218 previousPostProcessingTime = postProcessingStartTime;
3220 logEnv(std::format("post-processing shader {} of {}: {}",
3221 postProcessingShaderIndex + 1,
3222 postProcessingShaders.size(),
3223 currentPostProcessingShader()));
3224 return;
3225 }
3226
3227 postProcessingSprite = attachPostProcessingShader(currentPostProcessingShader(), 1.0f, 1.0f, 1.0f, 0.0f);
3228 if (postProcessingSprite != nullptr) {
3229 postProcessingSprite->enableExtendedUBO();
3230 postProcessingFrameCount = 0;
3231 postProcessingStartTime = std::chrono::steady_clock::now();
3232 previousPostProcessingTime = postProcessingStartTime;
3233 }
3235 logEnv(std::format("post-processing shader {} of {}: {}",
3236 postProcessingShaderIndex + 1,
3237 postProcessingShaders.size(),
3238 currentPostProcessingShader()));
3239 }
3240
3241 void selectPostProcessingShader(const int direction) {
3242 if (postProcessingShaders.empty()) {
3243 return;
3244 }
3245
3246 setPostProcessingShaderIndex(postProcessingShaderIndex + direction);
3247 if (getDevice() != VK_NULL_HANDLE) {
3248 vkDeviceWaitIdle(getDevice());
3249 }
3250 applyPostProcessingShaderSelection();
3251 }
3252
3253 void updatePostProcessingShaderUniforms(const float deltaTime) {
3254 if (postProcessingSprite == nullptr || postProcessingShaders.empty()) {
3255 return;
3256 }
3257
3258 const auto now = std::chrono::steady_clock::now();
3259 const float elapsed = std::chrono::duration<float>(now - postProcessingStartTime).count();
3260 const float frameDelta = std::max(deltaTime, std::chrono::duration<float>(now - previousPostProcessingTime).count());
3261 previousPostProcessingTime = now;
3262 ++postProcessingFrameCount;
3263
3264 float mouseX = 0.0f;
3265 float mouseY = 0.0f;
3266 const SDL_MouseButtonFlags mouseButtons = SDL_GetMouseState(&mouseX, &mouseY);
3267 const float mousePressed = (mouseButtons & SDL_BUTTON_LMASK) != 0U ? 1.0f : 0.0f;
3268 const float frameRate = frameDelta > 0.0001f ? (1.0f / frameDelta) : 0.0f;
3269
3270 setPostProcessingShaderParams(1.0f, 1.0f, 1.0f, elapsed);
3271 postProcessingSprite->setMouseState(mouseX, mouseY, mousePressed, mousePressed);
3272 postProcessingSprite->setUniform0(1.0f, 1.0f, 1.0f, 0.0f);
3273 postProcessingSprite->setUniform1(frameDelta, 0.0f, 0.0f, frameRate);
3274 postProcessingSprite->setUniform2(static_cast<float>(postProcessingFrameCount), elapsed, 48000.0f, 0.0f);
3275 postProcessingSprite->setUniform3(0.0f, 0.0f, 0.0f, 0.0f);
3276 }
3277
3278 /// @brief Resolve a shader SPV name to a full path.
3279 ///
3280 /// Looks in the runtime shader directory first; if the file is not
3281 /// found there, the provided @p name is returned as-is so callers can pass
3282 /// absolute paths directly.
3283 [[nodiscard]] std::string resolveShaderPath(const std::string &name) const {
3284 const std::string runtimePath = shaderRoot + "/" + name;
3285 if (std::filesystem::exists(runtimePath)) {
3286 return runtimePath;
3287 }
3288 return name;
3289 }
3290
3291 [[nodiscard]] static std::string toLowerCopy(std::string value) {
3292 std::transform(value.begin(), value.end(), value.begin(), [](const unsigned char ch) {
3293 return static_cast<char>(std::tolower(ch));
3294 });
3295 return value;
3296 }
3297
3298 [[nodiscard]] static int parseIntOrDefault(const std::string &text, const int fallback) {
3299 int value = fallback;
3300 const auto begin = text.data();
3301 const auto end = text.data() + text.size();
3302 const auto [ptr, ec] = std::from_chars(begin, end, value);
3303 if (ec != std::errc{} || ptr != end) {
3304 return fallback;
3305 }
3306 return value;
3307 }
3308
3309 [[nodiscard]] static bool tryParseFloat(const std::string &text, float &outValue) {
3310 try {
3311 size_t parsed = 0;
3312 const float value = std::stof(text, &parsed);
3313 if (parsed != text.size()) {
3314 return false;
3315 }
3316 outValue = value;
3317 return true;
3318 } catch (...) {
3319 return false;
3320 }
3321 }
3322
3323 bool sampleNavigablePoint(const float y, const float radius, glm::vec3 &outPoint, const int maxAttempts) {
3324 float minX = -50.0f;
3325 float maxX = 50.0f;
3326 float minZ = -50.0f;
3327 float maxZ = 50.0f;
3328
3329 bool haveBounds = false;
3330 for (const WallSegment &wall : world.walls()) {
3331 if (!haveBounds) {
3332 minX = std::min(wall.start.x, wall.end.x);
3333 maxX = std::max(wall.start.x, wall.end.x);
3334 minZ = std::min(wall.start.z, wall.end.z);
3335 maxZ = std::max(wall.start.z, wall.end.z);
3336 haveBounds = true;
3337 } else {
3338 minX = std::min(minX, std::min(wall.start.x, wall.end.x));
3339 maxX = std::max(maxX, std::max(wall.start.x, wall.end.x));
3340 minZ = std::min(minZ, std::min(wall.start.z, wall.end.z));
3341 maxZ = std::max(maxZ, std::max(wall.start.z, wall.end.z));
3342 }
3343 }
3344
3345 if (!haveBounds) {
3346 outPoint = world.startPosition();
3347 outPoint.y = y;
3348 return true;
3349 }
3350
3351 const float margin = std::max(0.8f, radius + 0.5f);
3352 minX += margin;
3353 maxX -= margin;
3354 minZ += margin;
3355 maxZ -= margin;
3356
3357 if (minX > maxX || minZ > maxZ) {
3358 outPoint = world.startPosition();
3359 outPoint.y = y;
3360 return true;
3361 }
3362
3363 std::uniform_real_distribution<float> distX(minX, maxX);
3364 std::uniform_real_distribution<float> distZ(minZ, maxZ);
3365 for (int i = 0; i < maxAttempts; ++i) {
3366 const glm::vec3 candidate{distX(rng), y, distZ(rng)};
3367 if (!world.checkWallCollision(candidate, radius) && !world.checkPillarCollision(candidate, radius)) {
3368 outPoint = candidate;
3369 return true;
3370 }
3371 }
3372
3373 return false;
3374 }
3375
3376 [[nodiscard]] static const char *collectibleTypeName(Collectible::Type type) noexcept {
3377 return type == Collectible::Type::Saturn ? "saturn" : "bird";
3378 }
3379
3380 void loadModel(mxvk::VKAbstractModel &model,
3381 const std::string &modelPath,
3382 const std::string &textureManifest,
3383 const std::string &textureBase,
3384 const std::string &vertSpv,
3385 const std::string &fragSpv,
3386 bool backfaceCulling = false) {
3387 logEnv(std::format("loading model '{}'", modelPath));
3388 model.load(this, modelPath, textureManifest, textureBase, 1.0f);
3389 model.setBackfaceCulling(backfaceCulling);
3390 model.setShaders(this, vertSpv, fragSpv);
3391 logEnv(std::format("model ready '{}'", modelPath));
3392 }
3393
3394 void cleanupModels() {
3395 floorModel.cleanup(this);
3396 rawWallRenderer.cleanup(this);
3397 rawPillarRenderer.cleanup(this);
3398
3399 saturnModel.cleanup(this);
3400 birdModel.cleanup(this);
3401 blasterModel.cleanup(this);
3402 bulletModel.cleanup(this);
3403 }
3404
3405 [[nodiscard]] uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) const {
3406 VkPhysicalDeviceMemoryProperties memProperties{};
3407 vkGetPhysicalDeviceMemoryProperties(getPhysicalDevice(), &memProperties);
3408 for (uint32_t i = 0; i < memProperties.memoryTypeCount; ++i) {
3409 if ((typeFilter & (1u << i)) != 0u && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) {
3410 return i;
3411 }
3412 }
3413 throw mxvk::Exception("walk: failed to find suitable Vulkan memory type for point particles");
3414 }
3415
3416 void initializePointParticles() {
3417 if (!ensureRenderResources()) {
3418 throw mxvk::Exception("walk: render resources unavailable for point particles");
3419 }
3420
3421 destroyPointParticles();
3422 try {
3423 VkBufferCreateInfo bufferInfo{};
3424 bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
3425 bufferInfo.size = maxPointVertices * sizeof(ParticlePointVertex);
3426 bufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
3427 bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
3428 if (vkCreateBuffer(getDevice(), &bufferInfo, nullptr, &pointVertexBuffer) != VK_SUCCESS) {
3429 throw mxvk::Exception("walk: failed to create point particle vertex buffer");
3430 }
3431
3432 VkMemoryRequirements memReq{};
3433 vkGetBufferMemoryRequirements(getDevice(), pointVertexBuffer, &memReq);
3434 VkMemoryAllocateInfo allocInfo{};
3435 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
3436 allocInfo.allocationSize = memReq.size;
3437 allocInfo.memoryTypeIndex = findMemoryType(memReq.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
3438 if (vkAllocateMemory(getDevice(), &allocInfo, nullptr, &pointVertexMemory) != VK_SUCCESS) {
3439 throw mxvk::Exception("walk: failed to allocate point particle vertex memory");
3440 }
3441 if (vkBindBufferMemory(getDevice(), pointVertexBuffer, pointVertexMemory, 0) != VK_SUCCESS) {
3442 throw mxvk::Exception("walk: failed to bind point particle vertex memory");
3443 }
3444 if (vkMapMemory(getDevice(), pointVertexMemory, 0, bufferInfo.size, 0, &pointVertexMapped) != VK_SUCCESS) {
3445 throw mxvk::Exception("walk: failed to map point particle vertex memory");
3446 }
3447
3448 rebuildPointParticlePipeline();
3449 } catch (...) {
3450 destroyPointParticles();
3451 throw;
3452 }
3453 }
3454
3455 void rebuildPointParticlePipeline() {
3456 if (pointPipeline != VK_NULL_HANDLE) {
3457 vkDestroyPipeline(getDevice(), pointPipeline, nullptr);
3458 pointPipeline = VK_NULL_HANDLE;
3459 }
3460 if (pointPipelineLayout != VK_NULL_HANDLE) {
3461 vkDestroyPipelineLayout(getDevice(), pointPipelineLayout, nullptr);
3462 pointPipelineLayout = VK_NULL_HANDLE;
3463 }
3464
3465 if (getSwapchainFormat() == VK_FORMAT_UNDEFINED) {
3466 return;
3467 }
3468
3469 const std::vector<char> vertBytes = loadSpv(pointParticleVertSpv);
3470 const std::vector<char> fragBytes = loadSpv(pointParticleFragSpv);
3471 const VkShaderModule vertModule = mxvk::create_shader_module(getDevice(), vertBytes);
3472 const VkShaderModule fragModule = mxvk::create_shader_module(getDevice(), fragBytes);
3473
3474 VkPipelineShaderStageCreateInfo vertStage{};
3475 vertStage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
3476 vertStage.stage = VK_SHADER_STAGE_VERTEX_BIT;
3477 vertStage.module = vertModule;
3478 vertStage.pName = "main";
3479
3480 VkPipelineShaderStageCreateInfo fragStage{};
3481 fragStage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
3482 fragStage.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
3483 fragStage.module = fragModule;
3484 fragStage.pName = "main";
3485 const std::array<VkPipelineShaderStageCreateInfo, 2> stages = {vertStage, fragStage};
3486
3487 VkVertexInputBindingDescription binding{};
3488 binding.binding = 0;
3489 binding.stride = sizeof(ParticlePointVertex);
3490 binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
3491
3492 std::array<VkVertexInputAttributeDescription, 3> attrs{};
3493 attrs[0] = {0, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(ParticlePointVertex, pos)};
3494 attrs[1] = {1, 0, VK_FORMAT_R32G32B32A32_SFLOAT, offsetof(ParticlePointVertex, color)};
3495 attrs[2] = {2, 0, VK_FORMAT_R32_SFLOAT, offsetof(ParticlePointVertex, size)};
3496
3497 VkPipelineVertexInputStateCreateInfo vertexInput{};
3498 vertexInput.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
3499 vertexInput.vertexBindingDescriptionCount = 1;
3500 vertexInput.pVertexBindingDescriptions = &binding;
3501 vertexInput.vertexAttributeDescriptionCount = static_cast<uint32_t>(attrs.size());
3502 vertexInput.pVertexAttributeDescriptions = attrs.data();
3503
3504 VkPipelineInputAssemblyStateCreateInfo inputAssembly{};
3505 inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
3506 inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_POINT_LIST;
3507
3508 const std::array<VkDynamicState, 2> dynamicStates = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
3509 VkPipelineDynamicStateCreateInfo dynamicInfo{};
3510 dynamicInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
3511 dynamicInfo.dynamicStateCount = static_cast<uint32_t>(dynamicStates.size());
3512 dynamicInfo.pDynamicStates = dynamicStates.data();
3513
3514 VkPipelineViewportStateCreateInfo viewportState{};
3515 viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
3516 viewportState.viewportCount = 1;
3517 viewportState.scissorCount = 1;
3518
3519 VkPipelineRasterizationStateCreateInfo rasterizer{};
3520 rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
3521 rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
3522 rasterizer.cullMode = VK_CULL_MODE_NONE;
3523 rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;
3524 rasterizer.lineWidth = 1.0f;
3525
3526 VkPipelineMultisampleStateCreateInfo multisample{};
3527 multisample.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
3528 multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
3529
3530 VkPipelineDepthStencilStateCreateInfo depthStencil{};
3531 depthStencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
3532 depthStencil.depthTestEnable = VK_FALSE;
3533 depthStencil.depthWriteEnable = VK_FALSE;
3534 depthStencil.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL;
3535
3536 VkPipelineColorBlendAttachmentState blendAttachment{};
3537 blendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
3538 blendAttachment.blendEnable = VK_TRUE;
3539 blendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
3540 blendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE;
3541 blendAttachment.colorBlendOp = VK_BLEND_OP_ADD;
3542 blendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
3543 blendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
3544 blendAttachment.alphaBlendOp = VK_BLEND_OP_ADD;
3545
3546 VkPipelineColorBlendStateCreateInfo colorBlend{};
3547 colorBlend.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
3548 colorBlend.attachmentCount = 1;
3549 colorBlend.pAttachments = &blendAttachment;
3550
3551 VkPushConstantRange pushRange{};
3552 pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
3553 pushRange.offset = 0;
3554 pushRange.size = sizeof(glm::mat4);
3555
3556 VkPipelineLayoutCreateInfo layoutInfo{};
3557 layoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
3558 layoutInfo.pushConstantRangeCount = 1;
3559 layoutInfo.pPushConstantRanges = &pushRange;
3560 if (vkCreatePipelineLayout(getDevice(), &layoutInfo, nullptr, &pointPipelineLayout) != VK_SUCCESS) {
3561 vkDestroyShaderModule(getDevice(), fragModule, nullptr);
3562 vkDestroyShaderModule(getDevice(), vertModule, nullptr);
3563 throw mxvk::Exception("walk: failed to create point particle pipeline layout");
3564 }
3565
3566 const VkFormat colorFormat = getSwapchainFormat();
3567 const VkFormat depthFormat = getDepthFormat();
3568 VkPipelineRenderingCreateInfo renderingInfo{};
3569 renderingInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO;
3570 renderingInfo.colorAttachmentCount = 1;
3571 renderingInfo.pColorAttachmentFormats = &colorFormat;
3572 if (depthFormat != VK_FORMAT_UNDEFINED) {
3573 renderingInfo.depthAttachmentFormat = depthFormat;
3574 }
3575
3576 VkGraphicsPipelineCreateInfo pipelineInfo{};
3577 pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
3578 pipelineInfo.pNext = &renderingInfo;
3579 pipelineInfo.stageCount = static_cast<uint32_t>(stages.size());
3580 pipelineInfo.pStages = stages.data();
3581 pipelineInfo.pVertexInputState = &vertexInput;
3582 pipelineInfo.pInputAssemblyState = &inputAssembly;
3583 pipelineInfo.pViewportState = &viewportState;
3584 pipelineInfo.pRasterizationState = &rasterizer;
3585 pipelineInfo.pMultisampleState = &multisample;
3586 pipelineInfo.pDepthStencilState = &depthStencil;
3587 pipelineInfo.pColorBlendState = &colorBlend;
3588 pipelineInfo.pDynamicState = &dynamicInfo;
3589 pipelineInfo.layout = pointPipelineLayout;
3590 pipelineInfo.renderPass = VK_NULL_HANDLE;
3591
3592 if (vkCreateGraphicsPipelines(getDevice(), VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pointPipeline) != VK_SUCCESS) {
3593 vkDestroyShaderModule(getDevice(), fragModule, nullptr);
3594 vkDestroyShaderModule(getDevice(), vertModule, nullptr);
3595 throw mxvk::Exception("walk: failed to create point particle graphics pipeline");
3596 }
3597
3598 vkDestroyShaderModule(getDevice(), fragModule, nullptr);
3599 vkDestroyShaderModule(getDevice(), vertModule, nullptr);
3600 }
3601
3602 void destroyPointParticles() {
3603 if (pointPipeline != VK_NULL_HANDLE) {
3604 vkDestroyPipeline(getDevice(), pointPipeline, nullptr);
3605 pointPipeline = VK_NULL_HANDLE;
3606 }
3607 if (pointPipelineLayout != VK_NULL_HANDLE) {
3608 vkDestroyPipelineLayout(getDevice(), pointPipelineLayout, nullptr);
3609 pointPipelineLayout = VK_NULL_HANDLE;
3610 }
3611 if (pointVertexMapped != nullptr) {
3612 vkUnmapMemory(getDevice(), pointVertexMemory);
3613 pointVertexMapped = nullptr;
3614 }
3615 if (pointVertexBuffer != VK_NULL_HANDLE) {
3616 vkDestroyBuffer(getDevice(), pointVertexBuffer, nullptr);
3617 pointVertexBuffer = VK_NULL_HANDLE;
3618 }
3619 if (pointVertexMemory != VK_NULL_HANDLE) {
3620 vkFreeMemory(getDevice(), pointVertexMemory, nullptr);
3621 pointVertexMemory = VK_NULL_HANDLE;
3622 }
3623 }
3624
3625 void renderPointParticles(VkCommandBuffer cmd, const glm::mat4 &view, const glm::mat4 &proj) {
3626 if (pointPipeline == VK_NULL_HANDLE || pointPipelineLayout == VK_NULL_HANDLE || pointVertexMapped == nullptr) {
3627 return;
3628 }
3629
3630 std::vector<ParticlePointVertex> vertices{};
3631 vertices.reserve(2048);
3632
3633 for (const Projectile &bullet : bullets) {
3634 if (!bullet.active) {
3635 continue;
3636 }
3637 for (const Projectile::TrailPoint &point : bullet.trail) {
3638 const float life = glm::clamp(point.lifetime / point.maxLifetime, 0.0f, 1.0f);
3639 const float fade = 1.0f - life;
3640 vertices.push_back({point.position, glm::vec4(1.0f, 0.2f, 0.0f, fade * 0.8f), 12.0f});
3641 }
3642 }
3643
3644 for (const ExplosionParticle &particle : explosionParticles) {
3645 if (!particle.active) {
3646 continue;
3647 }
3648 const float life = glm::clamp(particle.lifetime / particle.maxLifetime, 0.0f, 1.0f);
3649 const float fade = 1.0f - life;
3650 const float sizePx = glm::clamp(particle.size * 320.0f, 12.0f, 160.0f);
3651 vertices.push_back({particle.position, glm::vec4(particle.color, fade), sizePx});
3652 }
3653
3654 if (vertices.empty()) {
3655 return;
3656 }
3657
3658 if (vertices.size() > maxPointVertices) {
3659 vertices.resize(maxPointVertices);
3660 }
3661 std::memcpy(pointVertexMapped, vertices.data(), vertices.size() * sizeof(ParticlePointVertex));
3662
3663 const VkBuffer vb = pointVertexBuffer;
3664 const VkDeviceSize offset = 0;
3665 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pointPipeline);
3666 vkCmdBindVertexBuffers(cmd, 0, 1, &vb, &offset);
3667 const glm::mat4 vp = proj * view;
3668 vkCmdPushConstants(cmd, pointPipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(glm::mat4), &vp);
3669 vkCmdDraw(cmd, static_cast<uint32_t>(vertices.size()), 1, 0, 0);
3670 }
3671
3672 bool openGamepad(SDL_JoystickID id) {
3673 if (gamepad != nullptr && gamepadId == id) {
3674 return true;
3675 }
3676 if (gamepad != nullptr) {
3677 SDL_CloseGamepad(gamepad);
3678 gamepad = nullptr;
3679 gamepadId = 0;
3680 }
3681 gamepad = SDL_OpenGamepad(id);
3682 if (gamepad == nullptr) {
3683 logEnv(std::format("failed to open gamepad id={}", static_cast<int>(id)));
3684 return false;
3685 }
3686 gamepadId = id;
3687 const char *padName = SDL_GetGamepadName(gamepad);
3688 logEnv(std::format("gamepad connected: id={} name='{}'",
3689 static_cast<int>(id),
3690 padName != nullptr ? padName : "unknown"));
3691 return true;
3692 }
3693
3694 void tryOpenFirstGamepad() {
3695 if (gamepad != nullptr) {
3696 return;
3697 }
3698 int count = 0;
3699 SDL_JoystickID *ids = SDL_GetGamepads(&count);
3700 if (ids == nullptr || count <= 0) {
3701 if (ids != nullptr) {
3702 SDL_free(ids);
3703 }
3704 return;
3705 }
3706 openGamepad(ids[0]);
3707 SDL_free(ids);
3708 }
3709
3710 [[nodiscard]] static glm::mat4 composeNormalizedModel(const mxvk::VKAbstractModel &model, const glm::mat4 &world) {
3711 glm::mat4 transform = world;
3712 transform = transform * glm::scale(glm::mat4(1.0f), glm::vec3(model.modelRenderScale()));
3713 transform = transform * glm::translate(glm::mat4(1.0f), model.modelCenterOffset());
3714 return transform;
3715 }
3716
3717 // For meshes whose final world-space dimensions are already baked into `world`
3718 // (walls/pillars/floor) we still want to recenter the source mesh on its
3719 // bounding-box center, but we must NOT compound the renderScale on top.
3720 [[nodiscard]] static glm::mat4 composeRecenteredModel(const mxvk::VKAbstractModel &model, const glm::mat4 &world) {
3721 return world * glm::translate(glm::mat4(1.0f), model.modelCenterOffset());
3722 }
3723
3724 void renderModel(VkCommandBuffer cmd,
3725 uint32_t imageIndex,
3726 mxvk::VKAbstractModel &model,
3727 const glm::mat4 &world,
3728 const glm::mat4 &view,
3729 const glm::mat4 &proj,
3730 const glm::vec4 &fx,
3731 bool autoNormalize = true) {
3732 mxvk::UniformBufferObject ubo{};
3733 ubo.model = autoNormalize ? composeNormalizedModel(model, world)
3734 : composeRecenteredModel(model, world);
3735 ubo.view = view;
3736 ubo.proj = proj;
3737 ubo.fx = fx;
3738 model.updateUBO(imageIndex, ubo);
3739 model.render(cmd, imageIndex, false);
3740 }
3741
3742 void renderRawModel(VkCommandBuffer cmd,
3743 uint32_t imageIndex,
3744 mxvk::VKAbstractModel &model,
3745 const glm::mat4 &world,
3746 const glm::mat4 &view,
3747 const glm::mat4 &proj,
3748 const glm::vec4 &fx) {
3749 mxvk::UniformBufferObject ubo{};
3750 ubo.model = world;
3751 ubo.view = view;
3752 ubo.proj = proj;
3753 ubo.fx = fx;
3754 model.updateUBO(imageIndex, ubo);
3755 model.render(cmd, imageIndex, false);
3756 }
3757
3758 void updateCameraVectors() {
3759 glm::vec3 front(0.0f);
3760 front.x = std::cos(glm::radians(yaw)) * std::cos(glm::radians(pitch));
3761 front.y = std::sin(glm::radians(pitch));
3762 front.z = std::sin(glm::radians(yaw)) * std::cos(glm::radians(pitch));
3763 cameraFront = glm::normalize(front);
3764 }
3765
3766 void buildCameraBasis(glm::vec3 &forward, glm::vec3 &right, glm::vec3 &up) const {
3767 forward = cameraFront;
3768 if (glm::length(forward) <= 1e-5f) {
3769 forward = glm::vec3(0.0f, 0.0f, -1.0f);
3770 } else {
3771 forward = glm::normalize(forward);
3772 }
3773
3774 right = glm::cross(forward, glm::vec3(0.0f, 1.0f, 0.0f));
3775 if (glm::length(right) <= 1e-5f) {
3776 right = glm::vec3(1.0f, 0.0f, 0.0f);
3777 } else {
3778 right = glm::normalize(right);
3779 }
3780
3781 up = glm::cross(right, forward);
3782 if (glm::length(up) <= 1e-5f) {
3783 up = glm::vec3(0.0f, 1.0f, 0.0f);
3784 } else {
3785 up = glm::normalize(up);
3786 }
3787 }
3788
3789 [[nodiscard]] glm::vec3 blasterMuzzleTipPosition() const {
3790 glm::vec3 forward(0.0f);
3791 glm::vec3 right(0.0f);
3792 glm::vec3 up(0.0f);
3793 buildCameraBasis(forward, right, up);
3794 return cameraPos + (forward * 0.55f) + (right * 0.18f) - (up * 0.12f);
3795 }
3796
3797 [[nodiscard]] glm::vec3 projectileSpawnPosition() const {
3798 glm::vec3 forward(0.0f);
3799 glm::vec3 right(0.0f);
3800 glm::vec3 up(0.0f);
3801 buildCameraBasis(forward, right, up);
3802 constexpr float projectileForwardOffset = 0.015f;
3803 return blasterMuzzleTipPosition() + (forward * projectileForwardOffset);
3804 }
3805
3806 [[nodiscard]] glm::mat4 blasterWorldTransform() const {
3807 glm::vec3 forward(0.0f);
3808 glm::vec3 right(0.0f);
3809 glm::vec3 up(0.0f);
3810 buildCameraBasis(forward, right, up);
3811
3812 constexpr float blasterScale = 0.45f;
3813 constexpr glm::vec3 localMuzzle(0.95f, 0.09f, 0.0f);
3814 const glm::vec3 desiredMuzzle = blasterMuzzleTipPosition();
3815 const glm::vec3 origin = desiredMuzzle - (forward * (localMuzzle.x * blasterScale)) - (up * (localMuzzle.y * blasterScale)) - (right * (localMuzzle.z * blasterScale));
3816
3817 glm::mat4 world(1.0f);
3818 world[0] = glm::vec4(forward * blasterScale, 0.0f);
3819 world[1] = glm::vec4(up * blasterScale, 0.0f);
3820 world[2] = glm::vec4(right * blasterScale, 0.0f);
3821 world[3] = glm::vec4(origin, 1.0f);
3822 return world;
3823 }
3824
3825 [[nodiscard]] float viewDistanceInDirection(const glm::vec3 &origin, const glm::vec3 &direction) const {
3826 const glm::vec3 dir = glm::normalize(glm::vec3(direction.x, 0.0f, direction.z));
3827 constexpr float maxDistance = 14.0f;
3828 constexpr float step = 0.35f;
3829 constexpr float probeRadius = 0.30f;
3830 for (float d = step; d <= maxDistance; d += step) {
3831 const glm::vec3 point = origin + (dir * d);
3832 if (world.checkWallCollision(point, probeRadius) || world.checkPillarCollision(point, probeRadius)) {
3833 return d - step;
3834 }
3835 }
3836 return maxDistance;
3837 }
3838
3839 [[nodiscard]] float chooseBestSpawnYaw(const glm::vec3 &origin) const {
3840 constexpr float pi = 3.14159265358979323846f;
3841 constexpr int sampleCount = 48;
3842 float bestDistance = -1.0f;
3843 float bestYaw = yaw;
3844 for (int i = 0; i < sampleCount; ++i) {
3845 const float angle = (-pi) + (2.0f * pi * static_cast<float>(i) / static_cast<float>(sampleCount));
3846 const glm::vec3 dir(std::cos(angle), 0.0f, std::sin(angle));
3847 const float dist = viewDistanceInDirection(origin, dir);
3848 if (dist > bestDistance) {
3849 bestDistance = dist;
3850 bestYaw = glm::degrees(angle);
3851 }
3852 }
3853 return bestYaw;
3854 }
3855
3856 void updatePlayer(float deltaTime) {
3857 const bool *keys = SDL_GetKeyboardState(nullptr);
3858 glm::vec3 horizontalFront = glm::normalize(glm::vec3(cameraFront.x, 0.0f, cameraFront.z));
3859 if (glm::length(horizontalFront) < 0.0001f) {
3860 horizontalFront = glm::vec3(0.0f, 0.0f, -1.0f);
3861 }
3862 const glm::vec3 right = glm::normalize(glm::cross(horizontalFront, glm::vec3(0.0f, 1.0f, 0.0f)));
3863
3864 glm::vec3 desired = cameraPos;
3865 bool sprint = keys[SDL_SCANCODE_LSHIFT] != 0;
3866 if (gamepad != nullptr && SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_LEFT_STICK)) {
3867 sprint = true;
3868 }
3869 const float cameraSpeed = 0.2f;
3870 const float speed = sprint ? cameraSpeed * 2.0f : cameraSpeed;
3871 const float frameScale = deltaTime * 60.0f;
3872 const float moveStep = speed * frameScale;
3873
3874 if (keys[SDL_SCANCODE_W]) {
3875 desired += horizontalFront * moveStep;
3876 }
3877 if (keys[SDL_SCANCODE_S]) {
3878 desired -= horizontalFront * moveStep;
3879 }
3880 if (keys[SDL_SCANCODE_A]) {
3881 desired -= right * moveStep;
3882 }
3883 if (keys[SDL_SCANCODE_D]) {
3884 desired += right * moveStep;
3885 }
3886
3887 if (gamepad != nullptr) {
3888 const Sint16 leftX = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX);
3889 const Sint16 leftY = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY);
3890 if (std::abs(leftX) > stickDeadZone) {
3891 desired += moveStep * (static_cast<float>(leftX) / 32768.0f) * right;
3892 }
3893 if (std::abs(leftY) > stickDeadZone) {
3894 desired -= moveStep * (static_cast<float>(leftY) / 32768.0f) * horizontalFront;
3895 }
3896
3897 const Sint16 rightX = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTX);
3898 const Sint16 rightY = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTY);
3899 if (std::abs(rightX) > stickDeadZone || std::abs(rightY) > stickDeadZone) {
3900 yaw += (static_cast<float>(rightX) / 32768.0f) * controllerLookSensitivity;
3901 pitch -= (static_cast<float>(rightY) / 32768.0f) * controllerLookSensitivity;
3902 pitch = glm::clamp(pitch, -89.0f, 89.0f);
3903 updateCameraVectors();
3904 }
3905 }
3906
3907 constexpr float playerRadius = 0.5f;
3908 constexpr float cameraStandOff = 0.18f;
3909 const float collisionRadius = playerRadius + cameraStandOff;
3910 const auto isBlocked = [this, collisionRadius](const glm::vec3 &position) {
3911 return world.checkWallCollision(position, collisionRadius) || world.checkPillarCollision(position, collisionRadius);
3912 };
3913
3914 if (!isBlocked(desired)) {
3915 cameraPos = desired;
3916 } else {
3917 // Resolve per-axis so the player slides along obstacles instead of
3918 // clipping into them or fully stopping on diagonal movement.
3919 glm::vec3 tryX = cameraPos;
3920 tryX.x = desired.x;
3921 if (!isBlocked(tryX)) {
3922 cameraPos.x = tryX.x;
3923 }
3924
3925 glm::vec3 tryZ = cameraPos;
3926 tryZ.z = desired.z;
3927 if (!isBlocked(tryZ)) {
3928 cameraPos.z = tryZ.z;
3929 }
3930 }
3931
3932 const bool crouch = keys[SDL_SCANCODE_LCTRL] != 0;
3933 const float minHeight = crouch ? 0.8f : 1.7f;
3934 if (keys[SDL_SCANCODE_SPACE] && cameraPos.y <= minHeight + 0.01f) {
3935 jumpVelocity = 0.3f;
3936 }
3937
3938 cameraPos.y += jumpVelocity * deltaTime * 60.0f;
3939 jumpVelocity -= gravity * deltaTime * 60.0f;
3940 if (cameraPos.y < minHeight) {
3941 cameraPos.y = minHeight;
3942 jumpVelocity = 0.0f;
3943 }
3944 }
3945
3946 void fireProjectile() {
3947 emitMuzzleParticles();
3948
3949 Projectile bullet{};
3950 bullet.position = projectileSpawnPosition();
3951 bullet.direction = glm::normalize(cameraFront);
3952 bullets.push_back(bullet);
3953 logEnv(std::format("projectile fired from ({:.2f}, {:.2f}, {:.2f}) dir=({:.2f}, {:.2f}, {:.2f}) active_bullets={}",
3954 bullet.position.x,
3955 bullet.position.y,
3956 bullet.position.z,
3957 bullet.direction.x,
3958 bullet.direction.y,
3959 bullet.direction.z,
3960 bullets.size()));
3961 }
3962
3963 void emitMuzzleParticles() {
3964 glm::vec3 forward(0.0f);
3965 glm::vec3 right(0.0f);
3966 glm::vec3 up(0.0f);
3967 buildCameraBasis(forward, right, up);
3968
3969 const glm::vec3 muzzle = blasterMuzzleTipPosition();
3970 std::uniform_real_distribution<float> lateralJitter(-0.20f, 0.20f);
3971 std::uniform_real_distribution<float> verticalJitter(-0.12f, 0.12f);
3972 std::uniform_real_distribution<float> speedDist(8.0f, 26.0f);
3973 std::uniform_real_distribution<float> lifeDist(0.06f, 0.16f);
3974 std::uniform_real_distribution<float> warmDist(0.75f, 1.0f);
3975
3976 constexpr int particleCount = 24;
3977 for (int i = 0; i < particleCount; ++i) {
3978 ExplosionParticle p{};
3979 p.position = muzzle + (forward * 0.01f);
3980
3981 glm::vec3 dir = forward + (right * lateralJitter(rng)) + (up * verticalJitter(rng));
3982 if (glm::length(dir) <= 1e-5f) {
3983 dir = forward;
3984 } else {
3985 dir = glm::normalize(dir);
3986 }
3987
3988 const float speed = speedDist(rng);
3989 p.velocity = dir * speed;
3990 p.color = glm::vec3(warmDist(rng), warmDist(rng) * 0.7f, warmDist(rng) * 0.18f);
3991 p.maxLifetime = lifeDist(rng);
3992 p.size = 0.035f + (speed * 0.003f);
3993 explosionParticles.push_back(p);
3994 }
3995 }
3996
3997 void updateProjectiles(float deltaTime) {
3998 for (size_t bulletIndex = 0; bulletIndex < bullets.size(); ++bulletIndex) {
3999 Projectile &bullet = bullets[bulletIndex];
4000 if (!bullet.active) {
4001 continue;
4002 }
4003
4004 const glm::vec3 previous = bullet.position;
4005 const glm::vec3 displacement = bullet.direction * bullet.speed * deltaTime;
4006 bullet.position += displacement;
4007 bullet.lifetime += deltaTime;
4008 bullet.distanceTraveled += glm::length(displacement);
4009 bullet.trailTimer += deltaTime;
4010 if (bullet.trailTimer >= 0.02f) {
4011 Projectile::TrailPoint point{};
4012 point.position = bullet.position;
4013 bullet.trail.push_back(point);
4014 bullet.trailTimer = 0.0f;
4015 }
4016 for (Projectile::TrailPoint &point : bullet.trail) {
4017 point.lifetime += deltaTime;
4018 }
4019 bullet.trail.erase(
4020 std::remove_if(bullet.trail.begin(), bullet.trail.end(), [](const Projectile::TrailPoint &point) {
4021 return point.lifetime >= point.maxLifetime;
4022 }),
4023 bullet.trail.end());
4024
4025 size_t collectibleIndex = 0;
4026 glm::vec3 collectibleImpact{0.0f};
4027 if (lineHitCollectible(previous, bullet.position, collectibleIndex, collectibleImpact)) {
4028 createExplosion(collectibleImpact, 5000, false);
4029 const Collectible::Type hitType = world.collectibles()[collectibleIndex].type;
4030 const bool removed = deactivateCollectibleAt(collectibleIndex);
4031 resolveCollectibleClusters(2.0f, 3);
4032 bullet.active = false;
4033 if (removed) {
4034 ++destroyedCount;
4035 }
4036 logEnv(std::format("bullet {} hit {} collectible {} at ({:.2f}, {:.2f}, {:.2f}); destroyed={}",
4037 bulletIndex,
4038 collectibleTypeName(hitType),
4039 collectibleIndex,
4040 collectibleImpact.x,
4041 collectibleImpact.y,
4042 collectibleImpact.z,
4043 destroyedCount));
4044 continue;
4045 }
4046
4047 const ProjectileTraceHit segmentHit = traceProjectileSegment(previous, bullet.position);
4048 if (segmentHit.type != ProjectileHitType::None) {
4049
4050 if (segmentHit.type == ProjectileHitType::Floor) {
4051 createExplosion(glm::vec3(segmentHit.impact.x, 0.0f, segmentHit.impact.z), 1500, true);
4052 bullet.active = false;
4053 logEnv(std::format("bullet {} hit floor at ({:.2f}, {:.2f}, {:.2f})",
4054 bulletIndex,
4055 segmentHit.impact.x,
4056 0.0f,
4057 segmentHit.impact.z));
4058 continue;
4059 }
4060
4061 createExplosion(segmentHit.impact, 1500, true);
4062 bullet.active = false;
4063 logEnv(std::format("bullet {} hit {} at ({:.2f}, {:.2f}, {:.2f})",
4064 bulletIndex,
4065 (segmentHit.type == ProjectileHitType::Pillar) ? "pillar" : "wall",
4066 segmentHit.impact.x,
4067 segmentHit.impact.y,
4068 segmentHit.impact.z));
4069 continue;
4070 }
4071
4072 if (bullet.lifetime >= bullet.maxLifetime) {
4073 bullet.active = false;
4074 logEnv(std::format("bullet {} expired after {:.2f}s", bulletIndex, bullet.lifetime));
4075 continue;
4076 }
4077
4078 if (bullet.distanceTraveled >= bullet.maxDistance) {
4079 bullet.active = false;
4080 logEnv(std::format("bullet {} faded after traveling {:.2f} units", bulletIndex, bullet.distanceTraveled));
4081 }
4082 }
4083
4084 bullets.erase(std::remove_if(bullets.begin(), bullets.end(), [](const Projectile &b) { return !b.active; }), bullets.end());
4085 }
4086
4087 void updateCollectibles(float deltaTime) {
4088 for (Collectible &obj : world.collectibles()) {
4089 if (!obj.active) {
4090 continue;
4091 }
4092 obj.rotation.y += obj.rotationSpeed * deltaTime;
4093 if (obj.rotation.y > 360.0f) {
4094 obj.rotation.y -= 360.0f;
4095 }
4096 }
4097
4098 collectibleClusterResolveTimer += deltaTime;
4099 if (collectibleClusterResolveTimer >= 0.75f) {
4100 collectibleClusterResolveTimer = 0.0f;
4101 resolveCollectibleClusters(2.0f, 2);
4102 }
4103 }
4104
4105 void createExplosion(const glm::vec3 &position, int requestedCount, bool isRed) {
4106 if (requestedCount <= 0) {
4107 return;
4108 }
4109
4110 constexpr float pi = 3.14159265358979323846f;
4111 std::uniform_real_distribution<float> speedDist(3.0f, 15.0f);
4112 std::uniform_real_distribution<float> angleDist(0.0f, 2.0f * pi);
4113 std::uniform_real_distribution<float> elevationDist(-(pi / 6.0f), pi / 3.0f);
4114 std::uniform_real_distribution<float> colorDist(0.7f, 1.0f);
4115
4116 const int count = std::min(requestedCount * 2, 800);
4117 logEnv(std::format("explosion at ({:.2f}, {:.2f}, {:.2f}) particles={} style={}",
4118 position.x,
4119 position.y,
4120 position.z,
4121 count,
4122 isRed ? "impact" : "collectible"));
4123 for (int i = 0; i < count; ++i) {
4124 ExplosionParticle p{};
4125 p.position = position;
4126 const float theta = angleDist(rng);
4127 const float phi = elevationDist(rng);
4128 const float v = speedDist(rng);
4129 p.velocity = glm::vec3(v * std::cos(phi) * std::cos(theta), v * std::sin(phi), v * std::cos(phi) * std::sin(theta));
4130 if (isRed) {
4131 p.color = glm::vec3(colorDist(rng), colorDist(rng) * 0.3f, colorDist(rng) * 0.1f);
4132 } else {
4133 p.color = glm::vec3(colorDist(rng), colorDist(rng) * 0.7f, colorDist(rng) * 0.2f);
4134 }
4135 p.maxLifetime = 0.55f;
4136 p.size = 0.08f + (v * 0.010f);
4137 explosionParticles.push_back(p);
4138 }
4139 }
4140
4141 void updateExplosions(float deltaTime) {
4142 for (ExplosionParticle &particle : explosionParticles) {
4143 if (!particle.active) {
4144 continue;
4145 }
4146 particle.position += particle.velocity * deltaTime;
4147 particle.velocity.y -= 9.8f * deltaTime;
4148
4149 for (const PillarInstance &pillar : world.pillars()) {
4150 const glm::vec2 particle2d(particle.position.x, particle.position.z);
4151 const glm::vec2 pillar2d(pillar.position.x, pillar.position.z);
4152 const float distance = glm::length(particle2d - pillar2d);
4153 if (distance < pillar.radius && particle.position.y > 0.0f && particle.position.y < pillar.height) {
4154 glm::vec2 normal(1.0f, 0.0f);
4155 if (distance > 0.00001f) {
4156 normal = glm::normalize(particle2d - pillar2d);
4157 }
4158 const glm::vec2 vel2d(particle.velocity.x, particle.velocity.z);
4159 const glm::vec2 reflected = vel2d - 2.0f * glm::dot(vel2d, normal) * normal;
4160 particle.velocity.x = reflected.x * 0.5f;
4161 particle.velocity.z = reflected.y * 0.5f;
4162 const glm::vec2 correction = normal * (pillar.radius - distance + 0.1f);
4163 particle.position.x += correction.x;
4164 particle.position.z += correction.y;
4165 }
4166 }
4167
4168 for (const WallSegment &wall : world.walls()) {
4169 glm::vec3 wallDir = wall.end - wall.start;
4170 const float wallLength = glm::length(wallDir);
4171 if (wallLength < 0.0001f) {
4172 continue;
4173 }
4174 wallDir = glm::normalize(wallDir);
4175 const glm::vec3 toStart = particle.position - wall.start;
4176 float projection = glm::dot(toStart, wallDir);
4177 projection = glm::clamp(projection, 0.0f, wallLength);
4178 glm::vec3 closest = wall.start + wallDir * projection;
4179 closest.y = particle.position.y;
4180 const float distance = glm::length(particle.position - closest);
4181 if (distance < 0.5f && particle.position.y >= 0.0f && particle.position.y <= wall.height) {
4182 glm::vec3 normal(1.0f, 0.0f, 0.0f);
4183 if (distance > 0.0001f) {
4184 normal = glm::normalize(particle.position - closest);
4185 }
4186 particle.velocity = glm::reflect(particle.velocity, normal) * 0.5f;
4187 particle.position += normal * 0.2f;
4188 }
4189 }
4190
4191 if (particle.position.y < 0.0f) {
4192 particle.position.y = 0.0f;
4193 particle.velocity.y = -particle.velocity.y * 0.3f;
4194 particle.velocity.x *= 0.8f;
4195 particle.velocity.z *= 0.8f;
4196 }
4197
4198 particle.lifetime += deltaTime;
4199 particle.size *= 0.98f;
4200 if (particle.lifetime >= particle.maxLifetime) {
4201 particle.active = false;
4202 }
4203 }
4204
4205 explosionParticles.erase(
4206 std::remove_if(explosionParticles.begin(), explosionParticles.end(), [](const ExplosionParticle &p) {
4207 return !p.active;
4208 }),
4209 explosionParticles.end());
4210 }
4211
4212 [[nodiscard]] bool lineHitWall(const glm::vec3 &from, const glm::vec3 &to, glm::vec3 &impactOut) const {
4213 const glm::vec3 dir = to - from;
4214 constexpr float bulletRadius = 0.015f;
4215 const float travel = glm::length(dir);
4216 if (travel <= 1e-8f) {
4217 return false;
4218 }
4219
4220 constexpr float sampleStride = 0.05f;
4221 const int steps = std::max(1, static_cast<int>(std::ceil(travel / sampleStride)));
4222 float previousT = 0.0f;
4223 for (int i = 0; i <= steps; ++i) {
4224 const float t = static_cast<float>(i) / static_cast<float>(steps);
4225 const glm::vec3 point = from + (dir * t);
4226 if (pointHitsWall3D(point, bulletRadius)) {
4227 float lo = previousT;
4228 float hi = t;
4229 for (int iter = 0; iter < 10; ++iter) {
4230 const float mid = 0.5f * (lo + hi);
4231 const glm::vec3 midPoint = from + (dir * mid);
4232 if (pointHitsWall3D(midPoint, bulletRadius)) {
4233 hi = mid;
4234 } else {
4235 lo = mid;
4236 }
4237 }
4238 impactOut = from + (dir * hi);
4239 return true;
4240 }
4241 previousT = t;
4242 }
4243 return false;
4244 }
4245
4246 [[nodiscard]] bool lineHitPillar(const glm::vec3 &from, const glm::vec3 &to, glm::vec3 &impactOut) const {
4247 const glm::vec3 dir = to - from;
4248 constexpr float bulletRadius = 0.015f;
4249 const float travel = glm::length(dir);
4250 if (travel <= 1e-8f) {
4251 return false;
4252 }
4253
4254 constexpr float sampleStride = 0.05f;
4255 const int steps = std::max(1, static_cast<int>(std::ceil(travel / sampleStride)));
4256 float previousT = 0.0f;
4257 for (int i = 0; i <= steps; ++i) {
4258 const float t = static_cast<float>(i) / static_cast<float>(steps);
4259 const glm::vec3 point = from + (dir * t);
4260 if (pointHitsPillar3D(point, bulletRadius)) {
4261 float lo = previousT;
4262 float hi = t;
4263 for (int iter = 0; iter < 10; ++iter) {
4264 const float mid = 0.5f * (lo + hi);
4265 const glm::vec3 midPoint = from + (dir * mid);
4266 if (pointHitsPillar3D(midPoint, bulletRadius)) {
4267 hi = mid;
4268 } else {
4269 lo = mid;
4270 }
4271 }
4272 impactOut = from + (dir * hi);
4273 return true;
4274 }
4275 previousT = t;
4276 }
4277 return false;
4278 }
4279
4280 [[nodiscard]] bool lineHitCollectible(const glm::vec3 &from, const glm::vec3 &to, size_t &indexOut, glm::vec3 &impactOut) const {
4281 const glm::vec3 dir = to - from;
4282 const float dirLen2 = glm::dot(dir, dir);
4283 constexpr float bulletRadius = 0.015f;
4284 if (dirLen2 <= 1e-8f) {
4285 return false;
4286 }
4287
4288 bool found = false;
4289 float bestT = 2.0f;
4290 size_t bestIndex = 0;
4291
4292 const std::vector<Collectible> &collectibles = world.collectibles();
4293 for (size_t i = 0; i < collectibles.size(); ++i) {
4294 const Collectible &obj = collectibles[i];
4295 if (!obj.active) {
4296 continue;
4297 }
4298
4299 float tHit = 2.0f;
4300 bool hit = false;
4301
4302 if (obj.type == Collectible::Type::Bird) {
4303 const glm::vec3 halfExtents(obj.radius + bulletRadius);
4304 const glm::vec3 center = obj.position + obj.hitCenterOffset;
4305 const glm::vec3 boxMin = center - halfExtents;
4306 const glm::vec3 boxMax = center + halfExtents;
4307
4308 float tMin = 0.0f;
4309 float tMax = 1.0f;
4310 bool slabMiss = false;
4311
4312 for (int axis = 0; axis < 3; ++axis) {
4313 const float origin = from[axis];
4314 const float delta = dir[axis];
4315 const float minB = boxMin[axis];
4316 const float maxB = boxMax[axis];
4317
4318 if (std::abs(delta) <= 1e-8f) {
4319 if (origin < minB || origin > maxB) {
4320 slabMiss = true;
4321 break;
4322 }
4323 continue;
4324 }
4325
4326 float t0 = (minB - origin) / delta;
4327 float t1 = (maxB - origin) / delta;
4328 if (t0 > t1) {
4329 std::swap(t0, t1);
4330 }
4331
4332 tMin = std::max(tMin, t0);
4333 tMax = std::min(tMax, t1);
4334 if (tMin > tMax) {
4335 slabMiss = true;
4336 break;
4337 }
4338 }
4339
4340 if (!slabMiss) {
4341 hit = true;
4342 tHit = tMin;
4343 }
4344 } else {
4345 const glm::vec3 center = obj.position + obj.hitCenterOffset;
4346 const glm::vec3 m = from - center;
4347 const float a = dirLen2;
4348 const float b = 2.0f * glm::dot(m, dir);
4349 const float hitRadius = obj.radius + bulletRadius;
4350 const float c = glm::dot(m, m) - (hitRadius * hitRadius);
4351 const float discriminant = (b * b) - (4.0f * a * c);
4352 if (discriminant >= 0.0f) {
4353 const float sqrtD = std::sqrt(discriminant);
4354 const float invDen = 1.0f / (2.0f * a);
4355 const float t0 = (-b - sqrtD) * invDen;
4356 const float t1 = (-b + sqrtD) * invDen;
4357 if (t0 >= 0.0f && t0 <= 1.0f) {
4358 hit = true;
4359 tHit = t0;
4360 } else if (t1 >= 0.0f && t1 <= 1.0f) {
4361 hit = true;
4362 tHit = t1;
4363 }
4364 }
4365 }
4366
4367 if (hit && tHit >= 0.0f && tHit <= 1.0f && tHit < bestT) {
4368 found = true;
4369 bestT = tHit;
4370 bestIndex = i;
4371 }
4372 }
4373
4374 if (!found) {
4375 return false;
4376 }
4377
4378 indexOut = bestIndex;
4379 impactOut = from + (dir * bestT);
4380 return true;
4381 }
4382
4383 [[nodiscard]] bool pointHitsWall3D(const glm::vec3 &point, float radius) const {
4384 const float halfThickness = (world.wallThickness() * 0.5f) + radius;
4385 const float halfThicknessSq = halfThickness * halfThickness;
4386 for (const WallSegment &wall : world.walls()) {
4387 if (point.y < 0.0f || point.y > wall.height) {
4388 continue;
4389 }
4390
4391 const glm::vec2 start(wall.start.x, wall.start.z);
4392 const glm::vec2 end(wall.end.x, wall.end.z);
4393 const glm::vec2 seg = end - start;
4394 const float segLen2 = glm::dot(seg, seg);
4395 if (segLen2 <= 1e-8f) {
4396 continue;
4397 }
4398
4399 const glm::vec2 p(point.x, point.z);
4400 const glm::vec2 toPoint = p - start;
4401 const float t = glm::clamp(glm::dot(toPoint, seg) / segLen2, 0.0f, 1.0f);
4402 const glm::vec2 closest = start + (seg * t);
4403 const glm::vec2 d = p - closest;
4404 if (glm::dot(d, d) <= halfThicknessSq) {
4405 return true;
4406 }
4407 }
4408 return false;
4409 }
4410
4411 [[nodiscard]] bool pointHitsPillar3D(const glm::vec3 &point, float radius) const {
4412 for (const PillarInstance &pillar : world.pillars()) {
4413 if (point.y < 0.0f || point.y > pillar.height) {
4414 continue;
4415 }
4416
4417 const glm::vec2 p(point.x, point.z);
4418 const glm::vec2 c(pillar.position.x, pillar.position.z);
4419 const float hitRadius = pillar.radius + radius;
4420 const glm::vec2 d = p - c;
4421 if (glm::dot(d, d) <= (hitRadius * hitRadius)) {
4422 return true;
4423 }
4424 }
4425 return false;
4426 }
4427
4428 [[nodiscard]] float birdGroundYForScale(float scale) const {
4429 const glm::vec3 extent = birdModel.modelAxisExtent();
4430 const glm::vec3 centerOffset = birdModel.modelCenterOffset();
4431 const float modelMinY = -centerOffset.y - (extent.y * 0.5f);
4432 const float clampedScale = std::max(scale, 0.0001f);
4433 return std::max(0.0f, -modelMinY * clampedScale);
4434 }
4435
4436 [[nodiscard]] float birdHitHalfSideForScale(float scale) const {
4437 const glm::vec3 extent = birdModel.modelAxisExtent();
4438 const float modelSide = std::max({extent.x, extent.y, extent.z, 0.0001f});
4439 const float clampedScale = std::max(scale, 0.0001f);
4440 return 0.5f * modelSide * clampedScale;
4441 }
4442
4443 [[nodiscard]] float saturnHitRadiusForScale(float scale) const {
4444 const glm::vec3 extent = saturnModel.modelAxisExtent();
4445 const float modelDiameter = std::max({extent.x, extent.y, extent.z, 0.0001f});
4446 const float clampedScale = std::max(scale, 0.0001f);
4447 return 0.5f * modelDiameter * clampedScale;
4448 }
4449
4450 [[nodiscard]] glm::vec3 saturnHitCenterOffsetForScale(float scale) const {
4451 const glm::vec3 centerOffset = saturnModel.modelCenterOffset();
4452 const float clampedScale = std::max(scale, 0.0001f);
4453 return glm::vec3(-centerOffset.x * clampedScale,
4454 -centerOffset.y * clampedScale,
4455 -centerOffset.z * clampedScale);
4456 }
4457
4458 [[nodiscard]] glm::vec3 birdHitCenterOffsetForScale(float scale) const {
4459 const glm::vec3 centerOffset = birdModel.modelCenterOffset();
4460 const float clampedScale = std::max(scale, 0.0001f);
4461 return glm::vec3(0.0f, -centerOffset.y * clampedScale, 0.0f);
4462 }
4463
4464 [[nodiscard]] float birdSpawnClearanceRadiusForScale(float scale) const {
4465 const glm::vec3 extent = birdModel.modelAxisExtent();
4466 const glm::vec3 centerOffset = birdModel.modelCenterOffset();
4467 const float clampedScale = std::max(scale, 0.0001f);
4468
4469 const float halfXFromOrigin = (extent.x * 0.5f) + std::abs(centerOffset.x);
4470 const float halfZFromOrigin = (extent.z * 0.5f) + std::abs(centerOffset.z);
4471 const float horizontalRadius = std::max(halfXFromOrigin, halfZFromOrigin) * clampedScale;
4472 return horizontalRadius + 0.05f;
4473 }
4474
4475 [[nodiscard]] float placementRadiusForCollectible(const Collectible &obj) const {
4476 if (obj.type == Collectible::Type::Bird) {
4477 return std::max(obj.radius, birdSpawnClearanceRadiusForScale(obj.scale.x));
4478 }
4479 return obj.radius;
4480 }
4481
4482 void normalizeCollectiblesToModel() {
4483 for (Collectible &obj : world.collectibles()) {
4484 if (obj.type == Collectible::Type::Bird) {
4485 obj.radius = birdHitHalfSideForScale(obj.scale.x);
4486 obj.hitCenterOffset = birdHitCenterOffsetForScale(obj.scale.x);
4487 obj.position.y = birdGroundYForScale(obj.scale.x);
4488 } else {
4489 obj.radius = saturnHitRadiusForScale(obj.scale.x);
4490 obj.hitCenterOffset = saturnHitCenterOffsetForScale(obj.scale.x);
4491 }
4492 }
4493
4494 resolveCollectibleEnvironmentCollisions();
4495 resolveCollectibleOverlaps();
4496 resolveCollectibleClusters(2.0f, 5);
4497 }
4498
4499 [[nodiscard]] bool overlapsCollectibleAt(const glm::vec3 &candidate,
4500 float radius,
4501 size_t ignoreIndex,
4502 bool includeInactive) const {
4503 const std::vector<Collectible> &collectibles = world.collectibles();
4504 for (size_t i = 0; i < collectibles.size(); ++i) {
4505 if (i == ignoreIndex) {
4506 continue;
4507 }
4508 const Collectible &other = collectibles[i];
4509 if (!includeInactive && !other.active) {
4510 continue;
4511 }
4512
4513 const float separation = std::max(5.0f, other.radius + radius + 0.2f);
4514 if (glm::length(other.position - candidate) < separation) {
4515 return true;
4516 }
4517 }
4518 return false;
4519 }
4520
4521 bool relocateCollectible(size_t index, float minMoveDistance, int maxAttempts) {
4522 std::vector<Collectible> &collectibles = world.collectibles();
4523 if (index >= collectibles.size()) {
4524 return false;
4525 }
4526
4527 Collectible &obj = collectibles[index];
4528 const glm::vec3 oldPosition = obj.position;
4529 const float y = (obj.type == Collectible::Type::Bird) ? birdGroundYForScale(obj.scale.x) : 2.5f;
4530 const float placementRadius = placementRadiusForCollectible(obj);
4531
4532 for (int attempt = 0; attempt < maxAttempts; ++attempt) {
4533 glm::vec3 candidate{};
4534 if (!sampleNavigablePoint(y, placementRadius, candidate, 4)) {
4535 continue;
4536 }
4537 if (glm::length(candidate - oldPosition) < minMoveDistance) {
4538 continue;
4539 }
4540 if (overlapsCollectibleAt(candidate, obj.radius, index, false)) {
4541 continue;
4542 }
4543
4544 obj.position = candidate;
4545 return true;
4546 }
4547
4548 return false;
4549 }
4550
4551 void resolveCollectibleEnvironmentCollisions() {
4552 std::vector<Collectible> &collectibles = world.collectibles();
4553 for (size_t i = 0; i < collectibles.size(); ++i) {
4554 if (!collectibles[i].active) {
4555 continue;
4556 }
4557
4558 const float placementRadius = placementRadiusForCollectible(collectibles[i]);
4559 if (!world.checkWallCollision(collectibles[i].position, placementRadius) &&
4560 !world.checkPillarCollision(collectibles[i].position, placementRadius)) {
4561 continue;
4562 }
4563
4564 relocateCollectible(i, 2.0f, 512);
4565 }
4566 }
4567
4568 void resolveCollectibleOverlaps() {
4569 std::vector<Collectible> &collectibles = world.collectibles();
4570 for (size_t i = 0; i < collectibles.size(); ++i) {
4571 if (!collectibles[i].active) {
4572 continue;
4573 }
4574 if (!overlapsCollectibleAt(collectibles[i].position, collectibles[i].radius, i, false)) {
4575 continue;
4576 }
4577 relocateCollectible(i, 1.5f, 320);
4578 }
4579 }
4580
4581 void resolveCollectibleClusters(float minVisualSeparation, int passes) {
4582 if (minVisualSeparation <= 0.0f || passes <= 0) {
4583 return;
4584 }
4585
4586 std::vector<Collectible> &collectibles = world.collectibles();
4587 const float minVisualSeparationSq = minVisualSeparation * minVisualSeparation;
4588 for (int pass = 0; pass < passes; ++pass) {
4589 bool movedAny = false;
4590 for (size_t i = 0; i < collectibles.size(); ++i) {
4591 if (!collectibles[i].active) {
4592 continue;
4593 }
4594
4595 for (size_t j = i + 1; j < collectibles.size(); ++j) {
4596 if (!collectibles[j].active) {
4597 continue;
4598 }
4599
4600 const glm::vec3 delta = collectibles[j].position - collectibles[i].position;
4601 if (glm::dot(delta, delta) >= minVisualSeparationSq) {
4602 continue;
4603 }
4604
4605 if (relocateCollectible(j, minVisualSeparation, 512)) {
4606 movedAny = true;
4607 }
4608 }
4609 }
4610
4611 if (!movedAny) {
4612 break;
4613 }
4614 }
4615 }
4616
4617 void disperseNearbyCollectibles(const glm::vec3 &center, float radius, size_t ignoreIndex) {
4618 std::vector<Collectible> &collectibles = world.collectibles();
4619 for (size_t i = 0; i < collectibles.size(); ++i) {
4620 if (i == ignoreIndex) {
4621 continue;
4622 }
4623 if (!collectibles[i].active) {
4624 continue;
4625 }
4626 if (glm::length(collectibles[i].position - center) > radius) {
4627 continue;
4628 }
4629
4630 relocateCollectible(i, std::max(3.0f, radius), 256);
4631 }
4632 }
4633
4634 [[nodiscard]] bool deactivateCollectibleAt(size_t index) {
4635 std::vector<Collectible> &collectibles = world.collectibles();
4636 if (index >= collectibles.size()) {
4637 return false;
4638 }
4639
4640 Collectible &obj = collectibles[index];
4641 if (!obj.active) {
4642 return false;
4643 }
4644
4645 obj.active = false;
4646 return true;
4647 }
4648
4649 std::string assetRoot;
4650 std::string shaderRoot;
4651 std::string modelRoot;
4652
4653 MazeWorld world{};
4654 mxvk::VKAbstractModel floorModel{};
4655 RawWallRenderer rawWallRenderer{};
4656 RawPillarRenderer rawPillarRenderer{};
4657 mxvk::VKAbstractModel saturnModel{};
4658 mxvk::VKAbstractModel birdModel{};
4659 mxvk::VKAbstractModel blasterModel{};
4660 mxvk::VKAbstractModel bulletModel{};
4661
4662 VkPipelineLayout pointPipelineLayout = VK_NULL_HANDLE;
4663 VkPipeline pointPipeline = VK_NULL_HANDLE;
4664 VkBuffer pointVertexBuffer = VK_NULL_HANDLE;
4665 VkDeviceMemory pointVertexMemory = VK_NULL_HANDLE;
4666 void *pointVertexMapped = nullptr;
4667 size_t maxPointVertices = 200000;
4668 std::string pointParticleVertSpv{};
4669 std::string pointParticleFragSpv{};
4670 std::string modelVertSpv{};
4671 std::string pillarVertSpv{};
4672 std::string wallFragSpv{};
4673 std::string floorFragSpv{};
4674 std::string pillarFragSpv{};
4675 std::string objectFragSpv{};
4676 std::string bulletFragSpv{};
4677 std::string postProcessingShaderPath{};
4678 std::vector<std::string> postProcessingShaders{};
4679 mxvk::VK_Sprite *postProcessingSprite = nullptr;
4680 int postProcessingShaderIndex = 0;
4681 uint32_t postProcessingFrameCount = 0;
4682 std::chrono::steady_clock::time_point postProcessingStartTime{std::chrono::steady_clock::now()};
4683 std::chrono::steady_clock::time_point previousPostProcessingTime{postProcessingStartTime};
4684
4685 std::vector<Projectile> bullets{};
4686 std::vector<ExplosionParticle> explosionParticles{};
4687 std::mt19937 rng{std::random_device{}()};
4688
4689 glm::vec3 cameraPos{0.0f, 1.7f, 0.0f};
4690 glm::vec3 cameraFront{0.0f, 0.0f, -1.0f};
4691 float yaw = -90.0f;
4692 float pitch = 0.0f;
4693 bool mouseCapture = true;
4694 bool firstMouse = true;
4695 bool suppressProjectileOnNextLeftDown = false;
4696 bool showFps = true;
4697 float mouseSensitivity = 0.15f;
4698
4699 float jumpVelocity = 0.0f;
4700 float gravity = 0.015f;
4701 float collectibleClusterResolveTimer = 0.0f;
4702 uint32_t destroyedCount = 0;
4703
4704 SDL_Gamepad *gamepad = nullptr;
4705 SDL_JoystickID gamepadId = 0;
4706 int stickDeadZone = 8000;
4707 float controllerLookSensitivity = 2.0f;
4708
4709 std::chrono::steady_clock::time_point lastTick{std::chrono::steady_clock::now()};
4710 };
4711
4712} // namespace walk
4713
4714int main(int argc, char **argv) {
4715 try {
4716 const Arguments args = proc_args(argc, argv);
4717 walk::WalkWindow window(args);
4718 window.loop();
4719 } catch (mxvk::Exception &e) {
4720 std::cerr << std::format("mxvk: Exception: {}\n", e.text());
4721 return EXIT_FAILURE;
4722 } catch (ArgException<std::string> &e) {
4723 std::cerr << std::format("mxvk: Argument Exception: {}\n", e.text());
4724 return EXIT_FAILURE;
4725 }
4726
4727 return EXIT_SUCCESS;
4728}
Lightweight, header-only, template command-line argument parser.
Arguments proc_args(int &argc, char **argv)
Parse standard libmx2 command-line options from main()'s argv.
Definition argz.hpp:872
Exception thrown by Argz::proc() on unrecognised or malformed options.
Definition argz.hpp:178
std::string text() const
void setBackfaceCulling(bool enabled)
Enable or disable backface culling for this model pipeline.
void updateUBO(uint32_t imageIndex, const UniformBufferObject &ubo)
Update one per-frame UBO payload.
float modelRenderScale() const
Access the computed render scale used for normalization.
void load(VK_Window *window, const std::string &modelPath, const std::string &textureManifestPath, const std::string &textureBasePath, float scale=1.0f)
Load mesh/texture resources and build Vulkan state.
void setShaders(VK_Window *window, const std::string &vertSpv, const std::string &fragSpv)
Configure custom shader paths and rebuild pipelines.
glm::vec3 modelCenterOffset() const
Access the computed center offset used for normalization.
void render(VkCommandBuffer cmd, uint32_t imageIndex, bool wireframe=false) const
Record draw commands for this model.
virtual void appendConsoleHelp(std::ostream &out) const
Append app-specific help lines to the console help output.
virtual bool handleConsoleCommand(const std::vector< std::string > &args, std::ostream &out)
Handle app-specific console commands.
void event(SDL_Event &e) override
Handle one SDL event.
void print(const std::string &text, SDL_Color col={255, 255, 255, 255})
VK_IOWindow(const std::string &path, const std::string &title, const int width, const int height, const bool fullscreen, const bool enableVsync=false)
Main Vulkan window wrapper for MXVK.
Definition mxvk.hpp:37
VkExtent2D getSwapchainExtent() const noexcept
Get the current swapchain extent.
Definition mxvk.hpp:186
void loop()
Run the main event/render loop.
Definition mxvk.cpp:600
VkDevice getDevice() const noexcept
Get the Vulkan logical device handle.
Definition mxvk.hpp:168
VkDevice device
Definition mxvk.hpp:485
SDL_Window * getSDLWindow() const noexcept
Get the underlying SDL window handle.
Definition mxvk.hpp:165
void setClearColor(float r, float g, float b, float a=1.0f)
Set the per-frame color attachment clear color.
Definition mxvk.cpp:593
void exit()
Request loop termination.
Definition mxvk.cpp:1126
VK_Sprite * attachPostProcessingShader(const std::string &fragmentShaderPath, float p1=0.0f, float p2=0.0f, float p3=0.0f, float p4=0.0f)
Attach a full-screen post-processing fragment shader.
Definition mxvk.cpp:1154
bool ensureRenderResources()
Ensure deferred render resources are initialized.
Definition mxvk.cpp:1406
VkFormat getDepthFormat() const noexcept
Get the depth format used for dynamic rendering attachments.
Definition mxvk.hpp:189
void setFont(const std::string &fontPath, int fontSize=24)
Set the active text-render font.
Definition mxvk.cpp:2991
void setPostProcessingEnabled(bool enabled)
Definition mxvk.hpp:284
void printText(const std::string &text, int x, int y, const SDL_Color &col)
Queue a text string for rendering during the current frame.
Definition mxvk.cpp:3018
static std::vector< char > loadSpv(const std::string &path)
Load a SPIR-V file from disk.
Definition mxvk.cpp:141
void setPostProcessingShaderParams(float p1=0.0f, float p2=0.0f, float p3=0.0f, float p4=0.0f)
Set the post-processing shader params passed as a vec4 push constant.
Definition mxvk.cpp:1227
VkPhysicalDevice getPhysicalDevice() const noexcept
Get the Vulkan physical device handle.
Definition mxvk.hpp:171
VkFormat getSwapchainFormat() const noexcept
Get the swapchain color format.
Definition mxvk.hpp:183
const std::vector< PillarInstance > & pillars() const noexcept
Definition room.cpp:102
bool checkCollectibleCollision(const glm::vec3 &point, size_t &indexOut) const
Definition room.cpp:169
std::vector< Collectible > & collectibles() noexcept
Definition room.cpp:104
glm::vec3 startPosition() const noexcept
Definition room.cpp:98
bool checkWallCollision(const glm::vec3 &position, float radius) const
Definition room.cpp:134
float wallThickness() const noexcept
Definition room.cpp:219
bool checkPillarCollision(const glm::vec3 &position, float playerRadius) const
Definition room.cpp:156
const std::vector< WallSegment > & walls() const noexcept
Definition room.cpp:100
const std::vector< Collectible > & collectibles() const noexcept
Definition room.cpp:106
int activeCollectibles() const
Definition room.cpp:108
void generate(uint32_t seed)
Definition room.cpp:118
glm::vec3 randomPointInCell(int cellX, int cellZ, float objectRadius, float y, std::mt19937 &rng, float margin) const
Definition room.cpp:194
void load(mxvk::VK_Window *targetWindow, const std::string &textureManifestPath, const std::string &textureBasePath, const std::vector< char > &vertSpv, const std::vector< char > &fragSpv)
Definition room.cpp:554
void resize(mxvk::VK_Window *targetWindow)
Definition room.cpp:580
void reloadFragShader(const std::vector< char > &newFragSpv)
Hot-swap the fragment shader without rebuilding geometry or descriptors.
Definition room.cpp:597
void render(VkCommandBuffer cmd, uint32_t imageIndex, const std::vector< PillarInstance > &pillars, const glm::mat4 &view, const glm::mat4 &proj, const glm::vec4 &fx)
Definition room.cpp:620
void cleanup(mxvk::VK_Window *targetWindow)
Definition room.cpp:607
void reloadFragShader(const std::vector< char > &newFragSpv)
Hot-swap the fragment shader without rebuilding geometry or descriptors.
Definition room.cpp:1578
void resize(mxvk::VK_Window *targetWindow)
Definition room.cpp:1561
void load(mxvk::VK_Window *targetWindow, const std::string &textureManifestPath, const std::string &textureBasePath, const std::vector< char > &vertexShaderSpv, const std::vector< char > &fragmentShaderSpv)
Definition room.cpp:1535
void render(VkCommandBuffer cmd, uint32_t imageIndex, const std::vector< WallSegment > &walls, float wallThickness, const glm::mat4 &view, const glm::mat4 &proj, const glm::vec4 &fx)
Definition room.cpp:1601
void cleanup(mxvk::VK_Window *targetWindow)
Definition room.cpp:1588
~WalkWindow() override
Definition room.cpp:2498
void event(SDL_Event &e) override
Handle one SDL event.
Definition room.cpp:2512
void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t imageIndex) override
Optional hook for derived classes to record extra draw commands.
Definition room.cpp:2658
void console_proc() override
Definition room.cpp:2611
WalkWindow(const Arguments &args)
Definition room.cpp:2415
void onSwapchainRecreated() override
Called after swapchain and render resources are recreated.
Definition room.cpp:2646
void console_event(SDL_Event &e) override
Definition room.cpp:2536
int main(void)
Definition main.cpp:7
High-level model wrapper integrated with MXVK dynamic rendering.
PNG image loading and saving utilities via SDL3.
std::string trimLine(const std::string &text)
Definition shaders.cpp:37
std::string joinPath(const std::string &base, const std::string &file)
Definition shaders.cpp:51
Utilities for loading and saving PNG images.
Definition mxvk.hpp:30
VkShaderModule create_shader_module(VkDevice device, const std::vector< char > &spv_bytes)
Create a shader module from SPIR-V bytecode.
SDL_Surface * LoadPNG(const char *file)
Load a PNG file into an SDL_Surface.
Definition mxvk_png.cpp:103
std::default_random_engine & rng()
Returns the thread-local random number engine used by simulation helpers.
Definition room.cpp:29
std::atomic< bool > active
Definition relay.cpp:12
Plain data structure returned by proc_args() with all common libmx2 CLI options.
Definition argz.hpp:730
std::string shaderPath
Optional SPV shader folder path (-S / --shader-path).
Definition argz.hpp:765
bool fullscreen
Whether fullscreen mode was requested.
Definition argz.hpp:736
int height
Viewport height in pixels (default: 720).
Definition argz.hpp:733
int width
Viewport width in pixels (default: 1280).
Definition argz.hpp:732
int shader_index
Optional initial shader entry index.
Definition argz.hpp:769
float y
Definition space.cpp:59
float x
Definition space.cpp:59
bool active
Definition space.cpp:62
float lifetime
Definition space.cpp:61
float rotationSpeed
Definition room.cpp:54
glm::vec3 hitCenterOffset
Definition room.cpp:51
glm::vec3 scale
Definition room.cpp:53
glm::vec3 rotation
Definition room.cpp:52
glm::vec3 position
Definition room.cpp:50
glm::vec3 position
Definition room.cpp:38
glm::vec3 direction
Definition room.cpp:67
float distanceTraveled
Definition room.cpp:71
glm::vec3 position
Definition room.cpp:66
std::vector< TrailPoint > trail
Definition room.cpp:74
float maxDistance
Definition room.cpp:72
float lifetime
Definition room.cpp:69
float trailTimer
Definition room.cpp:75
float maxLifetime
Definition room.cpp:70
glm::vec3 start
Definition room.cpp:32
glm::vec3 end
Definition room.cpp:33