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