43 assetRoot((args.path.empty() || args.path ==
".") ? std::string(
VIEWER_ASSET_DIR) : args.path),
44 shaderRoot(args.shaderPath.empty() ? assetRoot +
"/data" : args.shaderPath),
45 benchmarkEnabled(args.benchmark) {
49 modelPath = resolveModelPath(args.
filename.empty() ? defaultModelName : args.
filename);
50 textureManifestPath = resolveOptionalPath(args.
texture);
51 textureBasePath = args.
resource_path.empty() ? resolveTextureBasePath(textureManifestPath) : resolveOptionalPath(args.
resource_path);
53 std::cout <<
"viewer: model='" << modelPath <<
"'\n";
54 if (!textureManifestPath.empty()) {
55 std::cout <<
"viewer: texture manifest='" << textureManifestPath <<
"' base='" << textureBasePath <<
"'\n";
58 model.load(
this, modelPath, textureManifestPath, textureBasePath, 1.0f);
59 model.setShaders(
this, shaderRoot +
"/model.vert.spv", shaderRoot +
"/model.frag.spv");
60 const std::string modelFormat =
61 std::filesystem::path(modelPath).extension() ==
".obj" ?
"OBJ" :
"model";
62 benchmarkName = std::format(
63 "{} geometry draw (Vulkan backend, {} frames)",
65 BENCHMARK_FRAME_COUNT);
69 if (
device != VK_NULL_HANDLE) {
75 void event(SDL_Event &e)
override {
76 if (e.type == SDL_EVENT_KEY_DOWN) {
77 if (e.key.key == SDLK_ESCAPE) {
87 wireframe = !wireframe;
88 std::cout <<
"viewer: wireframe " << (wireframe ?
"on" :
"off") <<
'\n';
92 autoRotate = !autoRotate;
93 std::cout <<
"viewer: auto-rotate " << (autoRotate ?
"on" :
"off") <<
'\n';
104 adjustCameraDistance(-0.5f);
107 adjustCameraDistance(0.5f);
114 if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) {
115 mouseDragging =
true;
116 lastMouseX =
static_cast<int>(e.button.x);
117 lastMouseY =
static_cast<int>(e.button.y);
121 if (e.type == SDL_EVENT_MOUSE_BUTTON_UP && e.button.button == SDL_BUTTON_LEFT) {
122 mouseDragging =
false;
126 if (e.type == SDL_EVENT_MOUSE_MOTION && mouseDragging) {
127 const int x =
static_cast<int>(e.motion.x);
128 const int y =
static_cast<int>(e.motion.y);
129 rotationYDegrees +=
static_cast<float>(x - lastMouseX) * mouseSensitivity;
130 rotationXDegrees +=
static_cast<float>(y - lastMouseY) * mouseSensitivity;
131 rotationXDegrees = std::clamp(rotationXDegrees, -89.0f, 89.0f);
137 if (e.type == SDL_EVENT_MOUSE_WHEEL) {
138 const float delta = (e.wheel.y != 0.0f) ? e.wheel.y :
static_cast<float>(e.wheel.integer_y);
139 adjustCameraDistance(-delta * 0.45f);
145 const bool *keys = SDL_GetKeyboardState(
nullptr);
146 if (keys ==
nullptr) {
150 const auto now = std::chrono::steady_clock::now();
151 const float deltaSeconds = std::clamp(std::chrono::duration<float>(now - lastUpdateTime).count(), 0.0f, 0.1f);
152 lastUpdateTime = now;
154 constexpr float ROTATE_SPEED = 120.0f;
155 if (keys[SDL_SCANCODE_LEFT]) {
156 rotationYDegrees -= ROTATE_SPEED * deltaSeconds;
158 if (keys[SDL_SCANCODE_RIGHT]) {
159 rotationYDegrees += ROTATE_SPEED * deltaSeconds;
161 if (keys[SDL_SCANCODE_UP]) {
162 rotationXDegrees -= ROTATE_SPEED * deltaSeconds;
164 if (keys[SDL_SCANCODE_DOWN]) {
165 rotationXDegrees += ROTATE_SPEED * deltaSeconds;
167 if (keys[SDL_SCANCODE_A]) {
168 adjustCameraDistance(-2.5f * deltaSeconds);
170 if (keys[SDL_SCANCODE_S]) {
171 adjustCameraDistance(2.5f * deltaSeconds);
173 rotationXDegrees = std::clamp(rotationXDegrees, -89.0f, 89.0f);
176 autoRotationRadians = wrapRadians(autoRotationRadians + 0.55f * deltaSeconds);
179 if (!benchmarkEnabled) {
185 if (benchmarkEnabled && benchmarkStopwatch ==
nullptr) {
187 std::make_unique<StopWatch<HighResolutionClockPolicy>>(benchmarkName);
192 if (benchmarkStopwatch ==
nullptr) {
195 ++benchmarkFrameCount;
196 if (benchmarkFrameCount == BENCHMARK_FRAME_COUNT) {
197 if (
device != VK_NULL_HANDLE) {
200 benchmarkStopwatch->Stop();
201 benchmarkStopwatch.reset();
212 const float aspect = (extent.height > 0U)
213 ?
static_cast<float>(extent.width) /
static_cast<float>(extent.height)
215 const float elapsedSeconds = std::chrono::duration<float>(std::chrono::steady_clock::now() - startTime).count();
218 ubo.
model = glm::rotate(glm::mat4(1.0f), glm::radians(rotationXDegrees), glm::vec3(1.0f, 0.0f, 0.0f));
219 ubo.
model = glm::rotate(ubo.
model, glm::radians(rotationYDegrees) + autoRotationRadians, glm::vec3(0.0f, 1.0f, 0.0f));
220 ubo.
model = glm::scale(ubo.
model, glm::vec3(model.modelRenderScale()));
221 ubo.
model = glm::translate(ubo.
model, model.modelCenterOffset());
222 ubo.
view = glm::lookAt(glm::vec3(0.0f, 0.0f, cameraDistance), glm::vec3(0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
223 ubo.
proj = glm::perspective(glm::radians(50.0f), aspect, 0.1f, 100.0f);
224 ubo.
proj[1][1] *= -1.0f;
225 ubo.
fx = glm::vec4(elapsedSeconds, wireframe ? 1.0f : 0.0f, 0.0f, 0.0f);
227 model.updateUBO(imageIndex, ubo);
228 model.render(cmd, imageIndex, wireframe);
232 [[nodiscard]] std::string resolveFontPath()
const {
233 const std::array<std::filesystem::path, 4> candidates = {
234 std::filesystem::path(assetRoot) /
"data/default.ttf",
235 std::filesystem::path(assetRoot) /
"data/font.ttf",
240 for (
const std::filesystem::path &candidate : candidates) {
241 if (std::filesystem::exists(candidate)) {
242 return candidate.string();
246 return candidates.front().string();
249 [[nodiscard]] std::string resolveModelPath(
const std::string &name)
const {
250 namespace fs = std::filesystem;
251 const fs::path requested(name);
252 std::error_code ec{};
254 if (fs::exists(requested, ec)) {
255 return fs::weakly_canonical(requested, ec).string();
259 const fs::path assetPath(assetRoot);
260 const fs::path sourceRoot = fs::path(VIEWER_SOURCE_DIR).parent_path().parent_path();
261 const fs::path runtimeRoot = fs::path(
VIEWER_ASSET_DIR).parent_path().parent_path();
262 const fs::path fileName = requested.filename();
263 const fs::path withoutExtension = fileName.stem();
265 const fs::path candidates[] = {
266 assetPath / requested,
267 assetPath /
"data" / requested,
268 sourceRoot / requested,
269 sourceRoot /
"models" / requested,
270 sourceRoot /
"models" / fileName,
271 sourceRoot /
"models" / (withoutExtension.string() +
".mxmod.z"),
272 runtimeRoot / requested,
273 runtimeRoot /
"models" / requested,
274 runtimeRoot /
"models" / fileName,
275 runtimeRoot /
"models" / (withoutExtension.string() +
".mxmod.z"),
278 for (
const fs::path &candidate : candidates) {
279 if (fs::exists(candidate, ec)) {
280 return fs::weakly_canonical(candidate, ec).string();
285 throw mxvk::Exception(
"viewer: failed to locate model: " + name);
288 [[nodiscard]] std::string resolveOptionalPath(
const std::string &name)
const {
293 namespace fs = std::filesystem;
294 const fs::path requested(name);
295 std::error_code ec{};
296 if (fs::exists(requested, ec)) {
297 return fs::weakly_canonical(requested, ec).string();
301 const fs::path assetPath(assetRoot);
302 const fs::path sourceRoot = fs::path(VIEWER_SOURCE_DIR).parent_path().parent_path();
303 const fs::path candidates[] = {
304 assetPath / requested,
305 assetPath /
"data" / requested,
306 sourceRoot / requested,
307 sourceRoot /
"models" / requested,
310 for (
const fs::path &candidate : candidates) {
311 if (fs::exists(candidate, ec)) {
312 return fs::weakly_canonical(candidate, ec).string();
320 [[nodiscard]]
static std::string resolveTextureBasePath(
const std::string &manifestPath) {
321 if (manifestPath.empty()) {
324 return std::filesystem::path(manifestPath).parent_path().string();
327 void adjustCameraDistance(
float delta) {
328 cameraDistance = std::clamp(cameraDistance + delta, 0.4f, 100.0f);
332 rotationXDegrees = 0.0f;
333 rotationYDegrees = 0.0f;
334 autoRotationRadians = 0.0f;
335 cameraDistance = 5.0f;
338 [[nodiscard]]
static float wrapRadians(
float radians) {
339 constexpr float TWO_PI = 6.28318530718f;
340 float wrapped = std::fmod(radians, TWO_PI);
341 if (wrapped < 0.0f) {
347 void updateOverlay() {
349 const auto now = std::chrono::steady_clock::now();
350 const double elapsed = std::chrono::duration<double>(now - fpsSampleTime).count();
351 if (elapsed >= 0.25) {
352 fpsText = std::format(
"FPS: {:.1f}",
static_cast<double>(frameCount) / elapsed);
357 printText(fpsText, 14, 12, SDL_Color{235, 240, 255, 255});
358 printText(std::format(
"Model: {}", std::filesystem::path(modelPath).filename().
string()), 14, 36, SDL_Color{210, 220, 235, 255});
359 printText(std::format(
"Mode: {} Auto: {} Distance: {:.1f}",
360 wireframe ?
"wire" :
"fill",
361 autoRotate ?
"on" :
"off",
365 SDL_Color{210, 220, 235, 255});
368 printText(
"Drag/arrows rotate Wheel/+/-/A/S zoom W wire R/P auto-rotate H/Space help Home reset Esc quit",
371 SDL_Color{185, 198, 215, 255});
375 static constexpr const char *defaultModelName =
"cube.mxmod.z";
376 static constexpr std::size_t BENCHMARK_FRAME_COUNT = 60 * 10;
378 std::string assetRoot{};
379 std::string shaderRoot{};
380 std::string modelPath{};
381 std::string textureManifestPath{};
382 std::string textureBasePath{};
383 mxvk::VKAbstractModel model{};
384 bool wireframe =
false;
385 bool autoRotate =
true;
386 bool showHelp =
true;
387 bool mouseDragging =
false;
390 float mouseSensitivity = 0.5f;
391 float rotationXDegrees = 0.0f;
392 float rotationYDegrees = 0.0f;
393 float autoRotationRadians = 0.0f;
394 float cameraDistance = 5.0f;
395 uint32_t frameCount = 0;
396 std::string fpsText =
"FPS: --";
397 std::chrono::steady_clock::time_point fpsSampleTime{std::chrono::steady_clock::now()};
398 std::chrono::steady_clock::time_point lastUpdateTime{std::chrono::steady_clock::now()};
399 std::chrono::steady_clock::time_point startTime{std::chrono::steady_clock::now()};
400 bool benchmarkEnabled =
false;
401 std::size_t benchmarkFrameCount = 0;
402 std::string benchmarkName;
403 std::unique_ptr<StopWatch<HighResolutionClockPolicy>> benchmarkStopwatch;