main.cpp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. /*
  2. * Copyright (c) 2021, Jesse Buhagiar <jooster669@gmail.com>
  3. *
  4. * SPDX-License-Identifier: BSD-2-Clause
  5. */
  6. #include <LibCore/ElapsedTimer.h>
  7. #include <LibCore/File.h>
  8. #include <LibCore/System.h>
  9. #include <LibFileSystemAccessClient/Client.h>
  10. #include <LibGL/GL/gl.h>
  11. #include <LibGL/GLContext.h>
  12. #include <LibGUI/ActionGroup.h>
  13. #include <LibGUI/Application.h>
  14. #include <LibGUI/FilePicker.h>
  15. #include <LibGUI/Icon.h>
  16. #include <LibGUI/Label.h>
  17. #include <LibGUI/Menu.h>
  18. #include <LibGUI/Menubar.h>
  19. #include <LibGUI/MessageBox.h>
  20. #include <LibGUI/Painter.h>
  21. #include <LibGUI/Widget.h>
  22. #include <LibGUI/Window.h>
  23. #include <LibGfx/Bitmap.h>
  24. #include <LibGfx/Palette.h>
  25. #include <LibMain/Main.h>
  26. #include "Mesh.h"
  27. #include "WavefrontOBJLoader.h"
  28. static constexpr u16 RENDER_WIDTH = 640;
  29. static constexpr u16 RENDER_HEIGHT = 480;
  30. class GLContextWidget final : public GUI::Frame {
  31. C_OBJECT(GLContextWidget);
  32. public:
  33. bool load_path(String const& fname);
  34. bool load_fd_and_close(int fd, String const& fname);
  35. bool load_file(Core::File& file, String const& fname);
  36. void toggle_rotate_x() { m_rotate_x = !m_rotate_x; }
  37. void toggle_rotate_y() { m_rotate_y = !m_rotate_y; }
  38. void toggle_rotate_z() { m_rotate_z = !m_rotate_z; }
  39. void set_rotation_speed(float speed) { m_rotation_speed = speed; }
  40. void set_stat_label(RefPtr<GUI::Label> l) { m_stats = l; };
  41. void set_wrap_s_mode(GLint mode) { m_wrap_s_mode = mode; }
  42. void set_wrap_t_mode(GLint mode) { m_wrap_t_mode = mode; }
  43. void set_texture_scale(float scale) { m_texture_scale = scale; }
  44. void set_mag_filter(GLint filter) { m_mag_filter = filter; }
  45. void toggle_show_frame_rate()
  46. {
  47. m_show_frame_rate = !m_show_frame_rate;
  48. m_stats->set_visible(m_show_frame_rate);
  49. }
  50. private:
  51. GLContextWidget()
  52. : m_mesh_loader(adopt_own(*new WavefrontOBJLoader()))
  53. {
  54. m_bitmap = Gfx::Bitmap::try_create(Gfx::BitmapFormat::BGRx8888, { RENDER_WIDTH, RENDER_HEIGHT }).release_value_but_fixme_should_propagate_errors();
  55. m_context = GL::create_context(*m_bitmap);
  56. start_timer(20);
  57. GL::make_context_current(m_context);
  58. glFrontFace(GL_CCW);
  59. glEnable(GL_CULL_FACE);
  60. glEnable(GL_DEPTH_TEST);
  61. // Set projection matrix
  62. glMatrixMode(GL_PROJECTION);
  63. glLoadIdentity();
  64. glFrustum(-0.5, 0.5, -0.5, 0.5, 1, 1500);
  65. m_init_list = glGenLists(1);
  66. glNewList(m_init_list, GL_COMPILE);
  67. {
  68. glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
  69. glClearDepth(1.0);
  70. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  71. }
  72. glEndList();
  73. }
  74. virtual void paint_event(GUI::PaintEvent&) override;
  75. virtual void resize_event(GUI::ResizeEvent&) override;
  76. virtual void timer_event(Core::TimerEvent&) override;
  77. virtual void mousemove_event(GUI::MouseEvent&) override;
  78. virtual void mousewheel_event(GUI::MouseEvent&) override;
  79. private:
  80. RefPtr<Mesh> m_mesh;
  81. RefPtr<Gfx::Bitmap> m_bitmap;
  82. OwnPtr<GL::GLContext> m_context;
  83. NonnullOwnPtr<WavefrontOBJLoader> m_mesh_loader;
  84. GLuint m_init_list { 0 };
  85. bool m_rotate_x = true;
  86. bool m_rotate_y = false;
  87. bool m_rotate_z = true;
  88. float m_angle_x = 0.0;
  89. float m_angle_y = 0.0;
  90. float m_angle_z = 0.0;
  91. Gfx::IntPoint m_last_mouse;
  92. float m_rotation_speed = 60.f;
  93. bool m_show_frame_rate = false;
  94. int m_cycles = 0;
  95. int m_accumulated_time = 0;
  96. RefPtr<GUI::Label> m_stats;
  97. GLint m_wrap_s_mode = GL_REPEAT;
  98. GLint m_wrap_t_mode = GL_REPEAT;
  99. float m_texture_scale = 1.0f;
  100. GLint m_mag_filter = GL_NEAREST;
  101. float m_zoom = 1;
  102. };
  103. void GLContextWidget::paint_event(GUI::PaintEvent& event)
  104. {
  105. GUI::Frame::paint_event(event);
  106. GUI::Painter painter(*this);
  107. painter.add_clip_rect(event.rect());
  108. painter.draw_scaled_bitmap(frame_inner_rect(), *m_bitmap, m_bitmap->rect());
  109. }
  110. void GLContextWidget::resize_event(GUI::ResizeEvent& event)
  111. {
  112. GUI::Frame::resize_event(event);
  113. if (m_stats)
  114. m_stats->set_x(width() - m_stats->width());
  115. };
  116. void GLContextWidget::mousemove_event(GUI::MouseEvent& event)
  117. {
  118. if (event.buttons() == GUI::MouseButton::Primary) {
  119. int delta_x = m_last_mouse.x() - event.x();
  120. int delta_y = m_last_mouse.y() - event.y();
  121. m_angle_x -= delta_y / 2.0f;
  122. m_angle_y -= delta_x / 2.0f;
  123. }
  124. m_last_mouse = event.position();
  125. }
  126. void GLContextWidget::mousewheel_event(GUI::MouseEvent& event)
  127. {
  128. if (event.wheel_delta() > 0)
  129. m_zoom /= 1.1f;
  130. else
  131. m_zoom *= 1.1f;
  132. }
  133. void GLContextWidget::timer_event(Core::TimerEvent&)
  134. {
  135. auto timer = Core::ElapsedTimer::start_new();
  136. glCallList(m_init_list);
  137. if (m_rotate_x)
  138. m_angle_x -= m_rotation_speed * 0.01f;
  139. if (m_rotate_y)
  140. m_angle_y -= m_rotation_speed * 0.01f;
  141. if (m_rotate_z)
  142. m_angle_z -= m_rotation_speed * 0.01f;
  143. glMatrixMode(GL_MODELVIEW);
  144. glLoadIdentity();
  145. glTranslatef(0, 0, -8.5);
  146. glRotatef(m_angle_x, 1, 0, 0);
  147. glRotatef(m_angle_y, 0, 1, 0);
  148. glRotatef(m_angle_z, 0, 0, 1);
  149. glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, m_wrap_s_mode);
  150. glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, m_wrap_t_mode);
  151. glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, m_mag_filter);
  152. glScalef(m_zoom, m_zoom, m_zoom);
  153. if (!m_mesh.is_null())
  154. m_mesh->draw(m_texture_scale);
  155. m_context->present();
  156. if ((m_cycles % 30) == 0) {
  157. int render_time = m_accumulated_time / 30;
  158. int frame_rate = render_time > 0 ? 1000 / render_time : 0;
  159. m_stats->set_text(String::formatted("{} fps, {} ms", frame_rate, render_time));
  160. m_accumulated_time = 0;
  161. }
  162. update();
  163. m_accumulated_time += timer.elapsed();
  164. m_cycles++;
  165. }
  166. bool GLContextWidget::load_path(String const& filename)
  167. {
  168. auto file = Core::File::construct(filename);
  169. if (!file->open(Core::OpenMode::ReadOnly) && file->error() != ENOENT) {
  170. GUI::MessageBox::show(window(), String::formatted("Opening \"{}\" failed: {}", filename, strerror(errno)), "Error", GUI::MessageBox::Type::Error);
  171. return false;
  172. }
  173. return load_file(file, filename);
  174. }
  175. bool GLContextWidget::load_fd_and_close(int fd, String const& filename)
  176. {
  177. auto file = Core::File::construct();
  178. if (!file->open(fd, Core::OpenMode::ReadOnly, Core::File::ShouldCloseFileDescriptor::Yes) && file->error() != ENOENT) {
  179. GUI::MessageBox::show(window(), String::formatted("Opening \"{}\" failed: {}", filename, strerror(errno)), "Error", GUI::MessageBox::Type::Error);
  180. return false;
  181. }
  182. return load_file(file, filename);
  183. }
  184. bool GLContextWidget::load_file(Core::File& file, String const& filename)
  185. {
  186. if (!filename.ends_with(".obj")) {
  187. GUI::MessageBox::show(window(), String::formatted("Opening \"{}\" failed: invalid file type", filename), "Error", GUI::MessageBox::Type::Error);
  188. return false;
  189. }
  190. if (file.is_device()) {
  191. GUI::MessageBox::show(window(), String::formatted("Opening \"{}\" failed: Can't open device files", filename), "Error", GUI::MessageBox::Type::Error);
  192. return false;
  193. }
  194. if (file.is_directory()) {
  195. GUI::MessageBox::show(window(), String::formatted("Opening \"{}\" failed: Can't open directories", filename), "Error", GUI::MessageBox::Type::Error);
  196. return false;
  197. }
  198. auto new_mesh = m_mesh_loader->load(file);
  199. if (new_mesh.is_null()) {
  200. GUI::MessageBox::show(window(), String::formatted("Reading \"{}\" failed.", filename), "Error", GUI::MessageBox::Type::Error);
  201. return false;
  202. }
  203. // Determine whether or not a texture for this model resides within the same directory
  204. StringBuilder builder;
  205. builder.append(filename.split('.').at(0));
  206. builder.append(".bmp");
  207. String texture_path = Core::File::absolute_path(builder.string_view());
  208. // Attempt to open the texture file from disk
  209. RefPtr<Gfx::Bitmap> texture_image;
  210. if (Core::File::exists(texture_path)) {
  211. auto bitmap_or_error = Gfx::Bitmap::try_load_from_file(texture_path);
  212. if (!bitmap_or_error.is_error())
  213. texture_image = bitmap_or_error.release_value_but_fixme_should_propagate_errors();
  214. } else {
  215. auto result = FileSystemAccessClient::Client::the().request_file(window()->window_id(), builder.string_view(), Core::OpenMode::ReadOnly);
  216. if (result.error != 0) {
  217. return false;
  218. }
  219. auto bitmap_or_error = Gfx::Bitmap::try_load_from_fd_and_close(*result.fd, *result.chosen_file);
  220. if (!bitmap_or_error.is_error())
  221. texture_image = bitmap_or_error.release_value_but_fixme_should_propagate_errors();
  222. }
  223. GLuint tex;
  224. glGenTextures(1, &tex);
  225. if (texture_image) {
  226. // Upload texture data to the GL
  227. glBindTexture(GL_TEXTURE_2D, tex);
  228. glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, texture_image->width(), texture_image->height(), 0, GL_BGRA, GL_UNSIGNED_BYTE, texture_image->scanline(0));
  229. } else {
  230. dbgln("3DFileViewer: Couldn't load texture for {}", filename);
  231. }
  232. m_mesh = new_mesh;
  233. dbgln("3DFileViewer: mesh has {} triangles.", m_mesh->triangle_count());
  234. return true;
  235. }
  236. ErrorOr<int> serenity_main(Main::Arguments arguments)
  237. {
  238. auto app = GUI::Application::construct(arguments);
  239. TRY(Core::System::pledge("stdio thread recvfd sendfd rpath unix", nullptr));
  240. TRY(Core::System::unveil("/tmp/portal/filesystemaccess", "rw"));
  241. TRY(Core::System::unveil("/home/anon/Documents/3D Models/teapot.obj", "r"));
  242. TRY(Core::System::unveil("/home/anon/Documents/3D Models/teapot.bmp", "r"));
  243. TRY(Core::System::unveil("/res", "r"));
  244. TRY(Core::System::unveil(nullptr, nullptr));
  245. // Construct the main window
  246. auto window = GUI::Window::construct();
  247. auto app_icon = GUI::Icon::default_icon("app-3d-file-viewer");
  248. window->set_icon(app_icon.bitmap_for_size(16));
  249. window->set_title("3D File Viewer");
  250. window->resize(640 + 4, 480 + 4);
  251. window->set_resizable(false);
  252. window->set_double_buffering_enabled(true);
  253. auto& widget = window->set_main_widget<GLContextWidget>();
  254. auto& time = widget.add<GUI::Label>();
  255. time.set_visible(false);
  256. time.set_foreground_role(ColorRole::HoverHighlight);
  257. time.set_relative_rect({ 0, 8, 86, 10 });
  258. time.set_x(widget.width() - time.width());
  259. widget.set_stat_label(time);
  260. auto& file_menu = window->add_menu("&File");
  261. file_menu.add_action(GUI::CommonActions::make_open_action([&](auto&) {
  262. auto result = FileSystemAccessClient::Client::the().open_file(window->window_id());
  263. if (result.error != 0)
  264. return;
  265. if (widget.load_fd_and_close(*result.fd, *result.chosen_file)) {
  266. auto canonical_path = Core::File::absolute_path(*result.chosen_file);
  267. window->set_title(String::formatted("{} - 3D File Viewer", canonical_path));
  268. }
  269. }));
  270. file_menu.add_separator();
  271. file_menu.add_action(GUI::CommonActions::make_quit_action([&](auto&) {
  272. app->quit();
  273. }));
  274. auto& view_menu = window->add_menu("&View");
  275. view_menu.add_action(GUI::CommonActions::make_fullscreen_action([&](auto&) {
  276. window->set_fullscreen(!window->is_fullscreen());
  277. }));
  278. auto& rotation_axis_menu = view_menu.add_submenu("Rotation &Axis");
  279. auto rotation_x_action = GUI::Action::create_checkable("&X", [&widget](auto&) {
  280. widget.toggle_rotate_x();
  281. });
  282. auto rotation_y_action = GUI::Action::create_checkable("&Y", [&widget](auto&) {
  283. widget.toggle_rotate_y();
  284. });
  285. auto rotation_z_action = GUI::Action::create_checkable("&Z", [&widget](auto&) {
  286. widget.toggle_rotate_z();
  287. });
  288. rotation_axis_menu.add_action(*rotation_x_action);
  289. rotation_axis_menu.add_action(*rotation_y_action);
  290. rotation_axis_menu.add_action(*rotation_z_action);
  291. rotation_x_action->set_checked(true);
  292. rotation_z_action->set_checked(true);
  293. auto& rotation_speed_menu = view_menu.add_submenu("Rotation &Speed");
  294. GUI::ActionGroup rotation_speed_actions;
  295. rotation_speed_actions.set_exclusive(true);
  296. auto no_rotation_action = GUI::Action::create_checkable("N&o Rotation", [&widget](auto&) {
  297. widget.set_rotation_speed(0.f);
  298. });
  299. auto slow_rotation_action = GUI::Action::create_checkable("&Slow", [&widget](auto&) {
  300. widget.set_rotation_speed(30.f);
  301. });
  302. auto normal_rotation_action = GUI::Action::create_checkable("&Normal", [&widget](auto&) {
  303. widget.set_rotation_speed(60.f);
  304. });
  305. auto fast_rotation_action = GUI::Action::create_checkable("&Fast", [&widget](auto&) {
  306. widget.set_rotation_speed(90.f);
  307. });
  308. rotation_speed_actions.add_action(*no_rotation_action);
  309. rotation_speed_actions.add_action(*slow_rotation_action);
  310. rotation_speed_actions.add_action(*normal_rotation_action);
  311. rotation_speed_actions.add_action(*fast_rotation_action);
  312. rotation_speed_menu.add_action(*no_rotation_action);
  313. rotation_speed_menu.add_action(*slow_rotation_action);
  314. rotation_speed_menu.add_action(*normal_rotation_action);
  315. rotation_speed_menu.add_action(*fast_rotation_action);
  316. normal_rotation_action->set_checked(true);
  317. auto show_frame_rate_action = GUI::Action::create_checkable("Show Frame &Rate", [&widget](auto&) {
  318. widget.toggle_show_frame_rate();
  319. });
  320. view_menu.add_action(*show_frame_rate_action);
  321. auto& texture_menu = window->add_menu("&Texture");
  322. auto& wrap_u_menu = texture_menu.add_submenu("Wrap &S");
  323. GUI::ActionGroup wrap_s_actions;
  324. wrap_s_actions.set_exclusive(true);
  325. auto wrap_u_repeat_action = GUI::Action::create_checkable("&Repeat", [&widget](auto&) {
  326. widget.set_wrap_s_mode(GL_REPEAT);
  327. });
  328. auto wrap_u_mirrored_repeat_action = GUI::Action::create_checkable("&Mirrored Repeat", [&widget](auto&) {
  329. widget.set_wrap_s_mode(GL_MIRRORED_REPEAT);
  330. });
  331. auto wrap_u_clamp_action = GUI::Action::create_checkable("&Clamp", [&widget](auto&) {
  332. widget.set_wrap_s_mode(GL_CLAMP);
  333. });
  334. wrap_s_actions.add_action(*wrap_u_repeat_action);
  335. wrap_s_actions.add_action(*wrap_u_mirrored_repeat_action);
  336. wrap_s_actions.add_action(*wrap_u_clamp_action);
  337. wrap_u_menu.add_action(*wrap_u_repeat_action);
  338. wrap_u_menu.add_action(*wrap_u_mirrored_repeat_action);
  339. wrap_u_menu.add_action(*wrap_u_clamp_action);
  340. wrap_u_repeat_action->set_checked(true);
  341. auto& wrap_t_menu = texture_menu.add_submenu("Wrap &T");
  342. GUI::ActionGroup wrap_t_actions;
  343. wrap_t_actions.set_exclusive(true);
  344. auto wrap_t_repeat_action = GUI::Action::create_checkable("&Repeat", [&widget](auto&) {
  345. widget.set_wrap_t_mode(GL_REPEAT);
  346. });
  347. auto wrap_t_mirrored_repeat_action = GUI::Action::create_checkable("&Mirrored Repeat", [&widget](auto&) {
  348. widget.set_wrap_t_mode(GL_MIRRORED_REPEAT);
  349. });
  350. auto wrap_t_clamp_action = GUI::Action::create_checkable("&Clamp", [&widget](auto&) {
  351. widget.set_wrap_t_mode(GL_CLAMP);
  352. });
  353. wrap_t_actions.add_action(*wrap_t_repeat_action);
  354. wrap_t_actions.add_action(*wrap_t_mirrored_repeat_action);
  355. wrap_t_actions.add_action(*wrap_t_clamp_action);
  356. wrap_t_menu.add_action(*wrap_t_repeat_action);
  357. wrap_t_menu.add_action(*wrap_t_mirrored_repeat_action);
  358. wrap_t_menu.add_action(*wrap_t_clamp_action);
  359. wrap_t_repeat_action->set_checked(true);
  360. auto& texture_scale_menu = texture_menu.add_submenu("S&cale");
  361. GUI::ActionGroup texture_scale_actions;
  362. texture_scale_actions.set_exclusive(true);
  363. auto texture_scale_025_action = GUI::Action::create_checkable("0.25x", [&widget](auto&) {
  364. widget.set_texture_scale(0.25f);
  365. });
  366. auto texture_scale_05_action = GUI::Action::create_checkable("0.5x", [&widget](auto&) {
  367. widget.set_texture_scale(0.5f);
  368. });
  369. auto texture_scale_1_action = GUI::Action::create_checkable("1x", [&widget](auto&) {
  370. widget.set_texture_scale(1);
  371. });
  372. auto texture_scale_2_action = GUI::Action::create_checkable("2x", [&widget](auto&) {
  373. widget.set_texture_scale(2);
  374. });
  375. auto texture_scale_4_action = GUI::Action::create_checkable("4x", [&widget](auto&) {
  376. widget.set_texture_scale(4);
  377. });
  378. texture_scale_actions.add_action(*texture_scale_025_action);
  379. texture_scale_actions.add_action(*texture_scale_05_action);
  380. texture_scale_actions.add_action(*texture_scale_1_action);
  381. texture_scale_actions.add_action(*texture_scale_2_action);
  382. texture_scale_actions.add_action(*texture_scale_4_action);
  383. texture_scale_menu.add_action(*texture_scale_025_action);
  384. texture_scale_menu.add_action(*texture_scale_05_action);
  385. texture_scale_menu.add_action(*texture_scale_1_action);
  386. texture_scale_menu.add_action(*texture_scale_2_action);
  387. texture_scale_menu.add_action(*texture_scale_4_action);
  388. texture_scale_1_action->set_checked(true);
  389. auto& texture_mag_filter_menu = texture_menu.add_submenu("Mag Filter");
  390. GUI::ActionGroup texture_mag_filter_actions;
  391. texture_mag_filter_actions.set_exclusive(true);
  392. auto texture_mag_filter_nearest_action = GUI::Action::create_checkable("&Nearest", [&widget](auto&) {
  393. widget.set_mag_filter(GL_NEAREST);
  394. });
  395. auto texture_mag_filter_linear_action = GUI::Action::create_checkable("&Linear", [&widget](auto&) {
  396. widget.set_mag_filter(GL_LINEAR);
  397. });
  398. texture_mag_filter_actions.add_action(*texture_mag_filter_nearest_action);
  399. texture_mag_filter_actions.add_action(*texture_mag_filter_linear_action);
  400. texture_mag_filter_menu.add_action(*texture_mag_filter_nearest_action);
  401. texture_mag_filter_menu.add_action(*texture_mag_filter_linear_action);
  402. texture_mag_filter_nearest_action->set_checked(true);
  403. auto& help_menu = window->add_menu("&Help");
  404. help_menu.add_action(GUI::CommonActions::make_about_action("3D File Viewer", app_icon, window));
  405. window->show();
  406. auto filename = arguments.argc > 1 ? arguments.argv[1] : "/home/anon/Documents/3D Models/teapot.obj";
  407. if (widget.load_path(filename)) {
  408. auto canonical_path = Core::File::absolute_path(filename);
  409. window->set_title(String::formatted("{} - 3D File Viewer", canonical_path));
  410. }
  411. return app->exec();
  412. }