33 OpenCVModelWindow(
const Arguments &args,
const std::string &title) :
mxvk::
VK_Window(title, args.width, args.height, args.fullscreen,
MXVK_VALIDATION, args.enable_vsync), assetRoot((args.path.empty() || args.path ==
".") ? std::string(
opencv_model_ASSET_DIR) : args.path), shaderRoot(args.shaderPath.empty() ? assetRoot +
"/data" : args.shaderPath), cameraIndex(args.camera_index), fallbackWidth(args.width), fallbackHeight(args.height) {
35 modelPath = resolveModelPath(args.
filename, assetRoot);
36 if (modelPath.empty()) {
37 throw mxvk::Exception(
"opencv_model: args.filename must point to a model file (.obj/.mxmod/.mxmod.z)");
39 setFont(assetRoot +
"/data/font.ttf", 20);
41 if (!capture.open(cameraIndex)) {
42 throw mxvk::Exception(std::format(
"opencv_model: failed to open camera index {}", cameraIndex));
45 capture.set(cv::CAP_PROP_FRAME_WIDTH,
static_cast<double>(fallbackWidth));
46 capture.set(cv::CAP_PROP_FRAME_HEIGHT,
static_cast<double>(fallbackHeight));
47 fps = configureCameraFps();
49 fallbackWidth =
static_cast<int>(capture.get(cv::CAP_PROP_FRAME_WIDTH));
50 fallbackHeight =
static_cast<int>(capture.get(cv::CAP_PROP_FRAME_HEIGHT));
52 std::cout <<
"opencv_model: model='" << modelPath <<
"' capture=" << fallbackWidth <<
"x" << fallbackHeight <<
" @ " << fps <<
" fps\n";
54 const std::string vertPath = shaderRoot +
"/model.vert.spv";
55 const std::string fragPath = shaderRoot +
"/model.frag.spv";
56 model.load(
this, modelPath,
"",
"", 1.0f);
57 model.setShaders(
this, vertPath, fragPath);
59 if (!capture.readToModelTexture(
model,
true)) {
60 std::cerr <<
"opencv_model: failed to upload initial camera frame\n";
64 if (
device != VK_NULL_HANDLE) {
73 if (
device != VK_NULL_HANDLE) {
81 void event(SDL_Event &e)
override {
82 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_ESCAPE) {
87 if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) {
89 lastMouseX =
static_cast<int>(e.button.x);
90 lastMouseY =
static_cast<int>(e.button.y);
94 if (e.type == SDL_EVENT_MOUSE_BUTTON_UP && e.button.button == SDL_BUTTON_LEFT) {
95 mouseDragging =
false;
99 if (e.type == SDL_EVENT_MOUSE_MOTION && mouseDragging) {
100 const int x =
static_cast<int>(e.motion.x);
101 const int y =
static_cast<int>(e.motion.y);
102 const int deltaX = x - lastMouseX;
103 const int deltaY = y - lastMouseY;
105 mouseYawDegrees +=
static_cast<float>(deltaX) * mouseSensitivity;
106 mousePitchDegrees +=
static_cast<float>(deltaY) * mouseSensitivity;
107 mousePitchDegrees = std::clamp(mousePitchDegrees, -80.0f, 80.0f);
114 if (e.type == SDL_EVENT_MOUSE_WHEEL) {
115 const float delta = (e.wheel.y != 0.0f) ? e.wheel.y :
static_cast<float>(e.wheel.integer_y);
116 cameraDistance -= delta * 0.45f;
117 cameraDistance = std::clamp(cameraDistance, 1.8f, 12.0f);
125 const auto now = std::chrono::steady_clock::now();
126 const float deltaSeconds = std::clamp(std::chrono::duration<float>(now - lastUpdateTime).count(), 0.0f, 0.1f);
127 lastUpdateTime = now;
128 updateRotationFromKeyboard(deltaSeconds);
130 if (!capture.readToModelTexture(
model,
true)) {
132 if (!capture.open(cameraIndex)) {
135 capture.set(cv::CAP_PROP_FRAME_WIDTH,
static_cast<double>(fallbackWidth));
136 capture.set(cv::CAP_PROP_FRAME_HEIGHT,
static_cast<double>(fallbackHeight));
137 fps = configureCameraFps();
138 if (!capture.readToModelTexture(
model,
true)) {
147 const float elapsedSeconds = std::chrono::duration<float>(std::chrono::steady_clock::now() - startTime).count();
149 const float aspect = (extent.height > 0U) ?
static_cast<float>(extent.width) /
static_cast<float>(extent.height) : 1.0f;
152 ubo.
model = glm::mat4(1.0f);
153 ubo.
model = glm::rotate(ubo.
model, glm::radians(mousePitchDegrees), glm::vec3(1.0f, 0.0f, 0.0f));
154 ubo.
model = glm::rotate(ubo.
model, glm::radians(mouseYawDegrees), glm::vec3(0.0f, 1.0f, 0.0f));
155 ubo.
model = glm::rotate(ubo.
model, pitchRadians, glm::vec3(1.0f, 0.0f, 0.0f));
156 ubo.
model = glm::rotate(ubo.
model, yawRadians + autoSpinRadians, glm::vec3(0.0f, 1.0f, 0.0f));
157 ubo.
model = glm::scale(ubo.
model, glm::vec3(
model.modelRenderScale()));
159 ubo.
view = glm::lookAt(glm::vec3(0.0f, 0.15f, cameraDistance), glm::vec3(0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
160 ubo.
proj = glm::perspective(glm::radians(50.0f), aspect, 0.1f, 100.0f);
161 ubo.
proj[1][1] *= -1.0f;
162 ubo.
fx = glm::vec4(elapsedSeconds, 0.32f, 0.18f + 0.12f * std::sin(elapsedSeconds * 1.2f), 0.0f);
164 model.updateUBO(imageIndex, ubo);
165 model.render(cmd, imageIndex,
false);
169 [[nodiscard]]
double configureCameraFps() {
170 static constexpr std::array<double, 3> fpsChoices = {60.0, 30.0, 24.0};
172 for (
const double requestedFps : fpsChoices) {
173 capture.
set(cv::CAP_PROP_FPS, requestedFps);
174 const double reportedFps = capture.
get(cv::CAP_PROP_FPS);
175 if (reportedFps > 0.0 && reportedFps + 0.5 >= requestedFps) {
180 capture.set(cv::CAP_PROP_FPS, fpsChoices.back());
181 const double reportedFps = capture.get(cv::CAP_PROP_FPS);
182 return (reportedFps > 0.0) ? reportedFps : fpsChoices.back();
185 [[nodiscard]]
static std::string resolveModelPath(
const std::string &filename,
const std::string &assetRoot) {
186 if (filename.empty()) {
190 namespace fs = std::filesystem;
191 const fs::path requested(filename);
192 std::error_code ec{};
193 if (fs::exists(requested, ec)) {
194 return fs::weakly_canonical(requested, ec).string();
198 if (requested.is_absolute()) {
202 const fs::path assetPath(assetRoot);
203 const fs::path sourceRoot = assetPath.parent_path().parent_path();
204 const std::array<fs::path, 3> candidates = {
205 assetPath / requested,
206 sourceRoot / requested,
207 sourceRoot /
"models" / requested.filename(),
210 for (
const fs::path &candidate : candidates) {
211 if (fs::exists(candidate, ec)) {
212 return fs::weakly_canonical(candidate, ec).string();
220 [[nodiscard]]
static float wrapAngle(
float angleRadians) {
221 constexpr float TWO_PI = 6.28318530718f;
222 float wrapped = std::fmod(angleRadians, TWO_PI);
223 if (wrapped < 0.0f) {
229 void updateRotationFromKeyboard(
float deltaSeconds) {
230 const bool *keys = SDL_GetKeyboardState(
nullptr);
231 if (keys ==
nullptr) {
235 constexpr float MANUAL_SPEED = glm::radians(120.0f);
236 constexpr float AUTO_SPIN_SPEED = 0.55f;
238 bool usingArrowKeys =
false;
239 if (keys[SDL_SCANCODE_LEFT]) {
240 yawRadians -= MANUAL_SPEED * deltaSeconds;
241 usingArrowKeys =
true;
243 if (keys[SDL_SCANCODE_RIGHT]) {
244 yawRadians += MANUAL_SPEED * deltaSeconds;
245 usingArrowKeys =
true;
247 if (keys[SDL_SCANCODE_UP]) {
248 pitchRadians -= MANUAL_SPEED * deltaSeconds;
249 usingArrowKeys =
true;
251 if (keys[SDL_SCANCODE_DOWN]) {
252 pitchRadians += MANUAL_SPEED * deltaSeconds;
253 usingArrowKeys =
true;
256 if (!usingArrowKeys) {
257 autoSpinRadians += AUTO_SPIN_SPEED * deltaSeconds;
260 yawRadians = wrapAngle(yawRadians);
261 pitchRadians = wrapAngle(pitchRadians);
262 autoSpinRadians = wrapAngle(autoSpinRadians);
265 void updateFpsOverlay() {
267 const auto now = std::chrono::steady_clock::now();
268 const double elapsed = std::chrono::duration<double>(now - fpsSampleTime).count();
269 if (elapsed >= 0.25) {
270 fps =
static_cast<double>(fpsFrameCount) / elapsed;
273 fpsText = std::format(
"FPS: {:.1f}", fps);
276 printText(fpsText, 15, 15, SDL_Color{255, 255, 255, 255});
279 std::string assetRoot{};
280 std::string shaderRoot{};
281 std::string modelPath{};
283 int fallbackWidth = 1280;
284 int fallbackHeight = 720;
285 float yawRadians = 0.0f;
286 float pitchRadians = 0.0f;
287 float autoSpinRadians = 0.0f;
288 bool mouseDragging =
false;
291 float mouseYawDegrees = 0.0f;
292 float mousePitchDegrees = 0.0f;
293 float cameraDistance = 4.2f;
294 float mouseSensitivity = 0.35f;
296 uint32_t fpsFrameCount = 0;
297 std::chrono::steady_clock::time_point fpsSampleTime{std::chrono::steady_clock::now()};
298 std::string fpsText =
"FPS: --";
299 mxvk::VK_Capture capture{};
300 mxvk::VKAbstractModel model{};
301 std::chrono::steady_clock::time_point lastUpdateTime{std::chrono::steady_clock::now()};
302 std::chrono::steady_clock::time_point startTime{std::chrono::steady_clock::now()};
#define opencv_model_ASSET_DIR