main.cpp 16 KB

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