36 explicit ModelViewerWindow(
const Arguments &args) :
mxvk::
VK_Window(
"MXModel Viewer - [ Vulkan ]", args.width, args.height, args.fullscreen,
MXVK_VALIDATION, args.enable_vsync), assetRoot((args.path.empty() || args.path ==
".") ? std::string(
VIEWER_ASSET_DIR) : args.path), shaderRoot(args.shaderPath.empty() ? assetRoot +
"/data" : args.shaderPath), benchmarkEnabled(args.benchmark) {
40 modelPath = resolveModelPath(args.
filename.empty() ? defaultModelName : args.
filename);
41 textureManifestPath = resolveOptionalPath(args.
texture);
42 textureBasePath = args.
resource_path.empty() ? resolveTextureBasePath(textureManifestPath) : resolveOptionalPath(args.
resource_path);
44 std::cout <<
"viewer: model='" << modelPath <<
"'\n";
45 if (!textureManifestPath.empty()) {
46 std::cout <<
"viewer: texture manifest='" << textureManifestPath <<
"' base='" << textureBasePath <<
"'\n";
49 model.load(
this, modelPath, textureManifestPath, textureBasePath, 1.0f);
50 model.setShaders(
this, shaderRoot +
"/model.vert.spv", shaderRoot +
"/model.frag.spv");
51 const std::string modelFormat = std::filesystem::path(modelPath).extension() ==
".obj" ?
"OBJ" :
"model";
52 benchmarkName = std::format(
"{} geometry draw (Vulkan backend, {} frames)", modelFormat, BENCHMARK_FRAME_COUNT);
56 if (
device != VK_NULL_HANDLE) {
62 void event(SDL_Event &e)
override {
63 if (e.type == SDL_EVENT_KEY_DOWN) {
64 if (e.key.key == SDLK_ESCAPE) {
74 wireframe = !wireframe;
75 std::cout <<
"viewer: wireframe " << (wireframe ?
"on" :
"off") <<
'\n';
79 autoRotate = !autoRotate;
80 std::cout <<
"viewer: auto-rotate " << (autoRotate ?
"on" :
"off") <<
'\n';
91 adjustCameraDistance(-0.5f);
94 adjustCameraDistance(0.5f);
101 if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) {
102 mouseDragging =
true;
103 lastMouseX =
static_cast<int>(e.button.x);
104 lastMouseY =
static_cast<int>(e.button.y);
108 if (e.type == SDL_EVENT_MOUSE_BUTTON_UP && e.button.button == SDL_BUTTON_LEFT) {
109 mouseDragging =
false;
113 if (e.type == SDL_EVENT_MOUSE_MOTION && mouseDragging) {
114 const int x =
static_cast<int>(e.motion.x);
115 const int y =
static_cast<int>(e.motion.y);
116 rotationYDegrees +=
static_cast<float>(x - lastMouseX) * mouseSensitivity;
117 rotationXDegrees +=
static_cast<float>(y - lastMouseY) * mouseSensitivity;
118 rotationXDegrees = std::clamp(rotationXDegrees, -89.0f, 89.0f);
124 if (e.type == SDL_EVENT_MOUSE_WHEEL) {
125 const float delta = (e.wheel.y != 0.0f) ? e.wheel.y :
static_cast<float>(e.wheel.integer_y);
126 adjustCameraDistance(-delta * 0.45f);
132 const bool *keys = SDL_GetKeyboardState(
nullptr);
133 if (keys ==
nullptr) {
137 const auto now = std::chrono::steady_clock::now();
138 const float deltaSeconds = std::clamp(std::chrono::duration<float>(now - lastUpdateTime).count(), 0.0f, 0.1f);
139 lastUpdateTime = now;
141 constexpr float ROTATE_SPEED = 120.0f;
142 if (keys[SDL_SCANCODE_LEFT]) {
143 rotationYDegrees -= ROTATE_SPEED * deltaSeconds;
145 if (keys[SDL_SCANCODE_RIGHT]) {
146 rotationYDegrees += ROTATE_SPEED * deltaSeconds;
148 if (keys[SDL_SCANCODE_UP]) {
149 rotationXDegrees -= ROTATE_SPEED * deltaSeconds;
151 if (keys[SDL_SCANCODE_DOWN]) {
152 rotationXDegrees += ROTATE_SPEED * deltaSeconds;
154 if (keys[SDL_SCANCODE_A]) {
155 adjustCameraDistance(-2.5f * deltaSeconds);
157 if (keys[SDL_SCANCODE_S]) {
158 adjustCameraDistance(2.5f * deltaSeconds);
160 rotationXDegrees = std::clamp(rotationXDegrees, -89.0f, 89.0f);
163 autoRotationRadians = wrapRadians(autoRotationRadians + 0.55f * deltaSeconds);
166 if (!benchmarkEnabled) {
172 if (benchmarkEnabled && benchmarkStopwatch ==
nullptr) {
173 benchmarkStopwatch = std::make_unique<StopWatch<HighResolutionClockPolicy>>(benchmarkName);
178 if (benchmarkStopwatch ==
nullptr) {
181 ++benchmarkFrameCount;
182 if (benchmarkFrameCount == BENCHMARK_FRAME_COUNT) {
183 if (
device != VK_NULL_HANDLE) {
186 benchmarkStopwatch->Stop();
187 benchmarkStopwatch.reset();
196 const float aspect = (extent.height > 0U) ?
static_cast<float>(extent.width) /
static_cast<float>(extent.height) : 1.0f;
197 const float elapsedSeconds = std::chrono::duration<float>(std::chrono::steady_clock::now() - startTime).count();
200 ubo.
model = glm::rotate(glm::mat4(1.0f), glm::radians(rotationXDegrees), glm::vec3(1.0f, 0.0f, 0.0f));
201 ubo.
model = glm::rotate(ubo.
model, glm::radians(rotationYDegrees) + autoRotationRadians, glm::vec3(0.0f, 1.0f, 0.0f));
202 ubo.
model = glm::scale(ubo.
model, glm::vec3(
model.modelRenderScale()));
204 ubo.
view = glm::lookAt(glm::vec3(0.0f, 0.0f, cameraDistance), glm::vec3(0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
205 ubo.
proj = glm::perspective(glm::radians(50.0f), aspect, 0.1f, 100.0f);
206 ubo.
proj[1][1] *= -1.0f;
207 ubo.
fx = glm::vec4(elapsedSeconds, wireframe ? 1.0f : 0.0f, 0.0f, 0.0f);
209 model.updateUBO(imageIndex, ubo);
210 model.render(cmd, imageIndex, wireframe);
214 [[nodiscard]] std::string resolveFontPath()
const {
215 const std::array<std::filesystem::path, 4> candidates = {
216 std::filesystem::path(assetRoot) /
"data/default.ttf",
217 std::filesystem::path(assetRoot) /
"data/font.ttf",
222 for (
const std::filesystem::path &candidate : candidates) {
223 if (std::filesystem::exists(candidate)) {
224 return candidate.string();
228 return candidates.front().string();
231 [[nodiscard]] std::string resolveModelPath(
const std::string &name)
const {
232 namespace fs = std::filesystem;
233 const fs::path requested(name);
234 std::error_code ec{};
236 if (fs::exists(requested, ec)) {
237 return fs::weakly_canonical(requested, ec).string();
241 const fs::path assetPath(assetRoot);
242 const fs::path sourceRoot = fs::path(VIEWER_SOURCE_DIR).parent_path().parent_path();
243 const fs::path runtimeRoot = fs::path(
VIEWER_ASSET_DIR).parent_path().parent_path();
244 const fs::path fileName = requested.filename();
245 const fs::path withoutExtension = fileName.stem();
247 const fs::path candidates[] = {
248 assetPath / requested,
249 assetPath /
"data" / requested,
250 sourceRoot / requested,
251 sourceRoot /
"models" / requested,
252 sourceRoot /
"models" / fileName,
253 sourceRoot /
"models" / (withoutExtension.string() +
".mxmod.z"),
254 runtimeRoot / requested,
255 runtimeRoot /
"models" / requested,
256 runtimeRoot /
"models" / fileName,
257 runtimeRoot /
"models" / (withoutExtension.string() +
".mxmod.z"),
260 for (
const fs::path &candidate : candidates) {
261 if (fs::exists(candidate, ec)) {
262 return fs::weakly_canonical(candidate, ec).string();
267 throw mxvk::Exception(
"viewer: failed to locate model: " + name);
270 [[nodiscard]] std::string resolveOptionalPath(
const std::string &name)
const {
275 namespace fs = std::filesystem;
276 const fs::path requested(name);
277 std::error_code ec{};
278 if (fs::exists(requested, ec)) {
279 return fs::weakly_canonical(requested, ec).string();
283 const fs::path assetPath(assetRoot);
284 const fs::path sourceRoot = fs::path(VIEWER_SOURCE_DIR).parent_path().parent_path();
285 const fs::path candidates[] = {
286 assetPath / requested,
287 assetPath /
"data" / requested,
288 sourceRoot / requested,
289 sourceRoot /
"models" / requested,
292 for (
const fs::path &candidate : candidates) {
293 if (fs::exists(candidate, ec)) {
294 return fs::weakly_canonical(candidate, ec).string();
302 [[nodiscard]]
static std::string resolveTextureBasePath(
const std::string &manifestPath) {
303 if (manifestPath.empty()) {
306 return std::filesystem::path(manifestPath).parent_path().string();
309 void adjustCameraDistance(
float delta) { cameraDistance = std::clamp(cameraDistance + delta, 0.4f, 100.0f); }
312 rotationXDegrees = 0.0f;
313 rotationYDegrees = 0.0f;
314 autoRotationRadians = 0.0f;
315 cameraDistance = 5.0f;
318 [[nodiscard]]
static float wrapRadians(
float radians) {
319 constexpr float TWO_PI = 6.28318530718f;
320 float wrapped = std::fmod(radians, TWO_PI);
321 if (wrapped < 0.0f) {
327 void updateOverlay() {
329 const auto now = std::chrono::steady_clock::now();
330 const double elapsed = std::chrono::duration<double>(now - fpsSampleTime).count();
331 if (elapsed >= 0.25) {
332 fpsText = std::format(
"FPS: {:.1f}",
static_cast<double>(frameCount) / elapsed);
337 printText(fpsText, 14, 12, SDL_Color{235, 240, 255, 255});
338 printText(std::format(
"Model: {}", std::filesystem::path(modelPath).filename().
string()), 14, 36, SDL_Color{210, 220, 235, 255});
339 printText(std::format(
"Mode: {} Auto: {} Distance: {:.1f}", wireframe ?
"wire" :
"fill", autoRotate ?
"on" :
"off", cameraDistance), 14, 60, SDL_Color{210, 220, 235, 255});
342 printText(
"Drag/arrows rotate Wheel/+/-/A/S zoom W wire R/P auto-rotate H/Space help Home reset Esc quit", 14,
static_cast<int>(
getSwapchainExtent().height) - 30, SDL_Color{185, 198, 215, 255});
346 static constexpr const char *defaultModelName =
"cube.mxmod.z";
347 static constexpr std::size_t BENCHMARK_FRAME_COUNT = 60 * 10;
349 std::string assetRoot{};
350 std::string shaderRoot{};
351 std::string modelPath{};
352 std::string textureManifestPath{};
353 std::string textureBasePath{};
354 mxvk::VKAbstractModel model{};
355 bool wireframe =
false;
356 bool autoRotate =
true;
357 bool showHelp =
true;
358 bool mouseDragging =
false;
361 float mouseSensitivity = 0.5f;
362 float rotationXDegrees = 0.0f;
363 float rotationYDegrees = 0.0f;
364 float autoRotationRadians = 0.0f;
365 float cameraDistance = 5.0f;
366 uint32_t frameCount = 0;
367 std::string fpsText =
"FPS: --";
368 std::chrono::steady_clock::time_point fpsSampleTime{std::chrono::steady_clock::now()};
369 std::chrono::steady_clock::time_point lastUpdateTime{std::chrono::steady_clock::now()};
370 std::chrono::steady_clock::time_point startTime{std::chrono::steady_clock::now()};
371 bool benchmarkEnabled =
false;
372 std::size_t benchmarkFrameCount = 0;
373 std::string benchmarkName;
374 std::unique_ptr<StopWatch<HighResolutionClockPolicy>> benchmarkStopwatch;