mirror of
https://github.com/RGBCube/serenity
synced 2025-07-28 10:57:36 +00:00
3DFileViewer: Move Demos/GLTeapot
to Applications/3DFileViewer
Also changes the category to `Graphics`
This commit is contained in:
parent
132ecfc47b
commit
585e7890cd
15 changed files with 18 additions and 18 deletions
8
Userland/Applications/3DFileViewer/CMakeLists.txt
Normal file
8
Userland/Applications/3DFileViewer/CMakeLists.txt
Normal file
|
@ -0,0 +1,8 @@
|
|||
set(SOURCES
|
||||
Mesh.cpp
|
||||
WavefrontOBJLoader.cpp
|
||||
main.cpp
|
||||
)
|
||||
|
||||
serenity_app(3DFileViewer ICON app-3d-file-viewer)
|
||||
target_link_libraries(3DFileViewer LibGUI LibGL)
|
24
Userland/Applications/3DFileViewer/Common.h
Normal file
24
Userland/Applications/3DFileViewer/Common.h
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright (c) 2021, Jesse Buhagiar <jooster669@gmail.com>
|
||||
* Copyright (c) 2021, Mathieu Gaillard <gaillard.mathieu.39@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibGL/GL/gl.h>
|
||||
|
||||
// Point in 3D space
|
||||
struct Vertex {
|
||||
GLfloat x;
|
||||
GLfloat y;
|
||||
GLfloat z;
|
||||
};
|
||||
|
||||
// A triangle defines a single "face" of a mesh
|
||||
struct Triangle {
|
||||
GLuint a;
|
||||
GLuint b;
|
||||
GLuint c;
|
||||
};
|
91
Userland/Applications/3DFileViewer/Mesh.cpp
Normal file
91
Userland/Applications/3DFileViewer/Mesh.cpp
Normal file
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Copyright (c) 2021, Jesse Buhagiar <jooster669@gmail.com>
|
||||
* Copyright (c) 2021, Mathieu Gaillard <gaillard.mathieu.39@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibGL/GL/gl.h>
|
||||
#include <LibGfx/Color.h>
|
||||
#include <LibGfx/Vector3.h>
|
||||
#include <LibGfx/Vector4.h>
|
||||
|
||||
#include "Mesh.h"
|
||||
|
||||
const Color colors[] {
|
||||
Color::Red,
|
||||
Color::Green,
|
||||
Color::Blue,
|
||||
Color::Magenta,
|
||||
Color::Yellow,
|
||||
Color::Cyan,
|
||||
Color::White
|
||||
};
|
||||
|
||||
Mesh::Mesh(Vector<Vertex> vertices, Vector<Triangle> triangles)
|
||||
: m_vertex_list(move(vertices))
|
||||
, m_triangle_list(move(triangles))
|
||||
{
|
||||
}
|
||||
|
||||
void Mesh::draw()
|
||||
{
|
||||
// Light direction
|
||||
const FloatVector3 light_direction(1.f, 1.f, 1.f);
|
||||
|
||||
// Mesh color
|
||||
const FloatVector4 mesh_ambient_color(0.2f, 0.2f, 0.2f, 1.f);
|
||||
const FloatVector4 mesh_diffuse_color(0.6f, 0.6f, 0.6f, 1.f);
|
||||
|
||||
for (u32 i = 0; i < m_triangle_list.size(); i++) {
|
||||
const auto& triangle = m_triangle_list[i];
|
||||
|
||||
const FloatVector3 vertex_a(
|
||||
m_vertex_list.at(triangle.a).x,
|
||||
m_vertex_list.at(triangle.a).y,
|
||||
m_vertex_list.at(triangle.a).z);
|
||||
|
||||
const FloatVector3 vertex_b(
|
||||
m_vertex_list.at(triangle.b).x,
|
||||
m_vertex_list.at(triangle.b).y,
|
||||
m_vertex_list.at(triangle.b).z);
|
||||
|
||||
const FloatVector3 vertex_c(
|
||||
m_vertex_list.at(triangle.c).x,
|
||||
m_vertex_list.at(triangle.c).y,
|
||||
m_vertex_list.at(triangle.c).z);
|
||||
|
||||
// Compute the triangle normal
|
||||
const FloatVector3 vec_ab = vertex_b - vertex_a;
|
||||
const FloatVector3 vec_ac = vertex_c - vertex_a;
|
||||
const FloatVector3 normal = vec_ab.cross(vec_ac).normalized();
|
||||
|
||||
// Compute lighting with a Lambertian color model
|
||||
const auto light_intensity = max(light_direction.dot(normal), 0.f);
|
||||
const FloatVector4 color = mesh_ambient_color
|
||||
+ mesh_diffuse_color * light_intensity;
|
||||
|
||||
glBegin(GL_TRIANGLES);
|
||||
glColor4f(color.x(), color.y(), color.z(), color.w());
|
||||
|
||||
// Vertex 1
|
||||
glVertex3f(
|
||||
m_vertex_list.at(m_triangle_list[i].a).x,
|
||||
m_vertex_list.at(m_triangle_list[i].a).y,
|
||||
m_vertex_list.at(m_triangle_list[i].a).z);
|
||||
|
||||
// Vertex 2
|
||||
glVertex3f(
|
||||
m_vertex_list.at(m_triangle_list[i].b).x,
|
||||
m_vertex_list.at(m_triangle_list[i].b).y,
|
||||
m_vertex_list.at(m_triangle_list[i].b).z);
|
||||
|
||||
// Vertex 3
|
||||
glVertex3f(
|
||||
m_vertex_list.at(m_triangle_list[i].c).x,
|
||||
m_vertex_list.at(m_triangle_list[i].c).y,
|
||||
m_vertex_list.at(m_triangle_list[i].c).z);
|
||||
|
||||
glEnd();
|
||||
}
|
||||
}
|
30
Userland/Applications/3DFileViewer/Mesh.h
Normal file
30
Userland/Applications/3DFileViewer/Mesh.h
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright (c) 2021, Jesse Buhagiar <jooster669@gmail.com>
|
||||
* Copyright (c) 2021, Mathieu Gaillard <gaillard.mathieu.39@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/RefCounted.h>
|
||||
#include <AK/Vector.h>
|
||||
|
||||
#include "Common.h"
|
||||
|
||||
class Mesh : public RefCounted<Mesh> {
|
||||
public:
|
||||
Mesh() = delete;
|
||||
|
||||
Mesh(Vector<Vertex> vertices, Vector<Triangle> triangles);
|
||||
|
||||
size_t vertex_count() const { return m_vertex_list.size(); }
|
||||
|
||||
size_t triangle_count() const { return m_triangle_list.size(); }
|
||||
|
||||
void draw();
|
||||
|
||||
private:
|
||||
Vector<Vertex> m_vertex_list;
|
||||
Vector<Triangle> m_triangle_list;
|
||||
};
|
20
Userland/Applications/3DFileViewer/MeshLoader.h
Normal file
20
Userland/Applications/3DFileViewer/MeshLoader.h
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright (c) 2021, Jesse Buhagiar <jooster669@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/String.h>
|
||||
|
||||
#include "Common.h"
|
||||
#include "Mesh.h"
|
||||
|
||||
class MeshLoader {
|
||||
public:
|
||||
MeshLoader() { }
|
||||
virtual ~MeshLoader() { }
|
||||
|
||||
virtual RefPtr<Mesh> load(const String& fname) = 0;
|
||||
};
|
76
Userland/Applications/3DFileViewer/WavefrontOBJLoader.cpp
Normal file
76
Userland/Applications/3DFileViewer/WavefrontOBJLoader.cpp
Normal file
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright (c) 2021, Jesse Buhagiar <jooster669@gmail.com>
|
||||
* Copyright (c) 2021, Mathieu Gaillard <gaillard.mathieu.39@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "WavefrontOBJLoader.h"
|
||||
#include <LibCore/File.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
RefPtr<Mesh> WavefrontOBJLoader::load(const String& fname)
|
||||
{
|
||||
auto obj_file_or_error = Core::File::open(fname, Core::OpenMode::ReadOnly);
|
||||
Vector<Vertex> vertices;
|
||||
Vector<Triangle> triangles;
|
||||
|
||||
dbgln("Wavefront: Loading {}...", fname);
|
||||
|
||||
if (obj_file_or_error.is_error())
|
||||
return nullptr;
|
||||
|
||||
// Start reading file line by line
|
||||
for (auto line = obj_file_or_error.value()->line_begin(); !line.at_end(); ++line) {
|
||||
auto object_line = *line;
|
||||
|
||||
// FIXME: Parse texture coordinates and vertex normals
|
||||
if (object_line.starts_with("vt") || object_line.starts_with("vn")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// This line describes a vertex (a position in 3D space)
|
||||
if (object_line.starts_with("v")) {
|
||||
auto vertex_line = object_line.split_view(' ');
|
||||
if (vertex_line.size() != 4) {
|
||||
dbgln("Wavefront: Malformed vertex line. Aborting.");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
vertices.append(
|
||||
{ static_cast<GLfloat>(atof(String(vertex_line.at(1)).characters())),
|
||||
static_cast<GLfloat>(atof(String(vertex_line.at(2)).characters())),
|
||||
static_cast<GLfloat>(atof(String(vertex_line.at(3)).characters())) });
|
||||
}
|
||||
// This line describes a face (a collection of 3 vertices, aka a triangle)
|
||||
else if (object_line.starts_with("f")) {
|
||||
auto face_line = object_line.split_view(' ');
|
||||
if (face_line.size() != 4) {
|
||||
dbgln("Wavefront: Malformed face line. Aborting.");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (object_line.contains("/")) {
|
||||
for (int i = 1; i <= 3; ++i) {
|
||||
face_line.at(i) = face_line.at(i).split_view("/").at(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new triangle
|
||||
triangles.append(
|
||||
{
|
||||
face_line.at(1).to_uint().value() - 1,
|
||||
face_line.at(2).to_uint().value() - 1,
|
||||
face_line.at(3).to_uint().value() - 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (vertices.is_empty()) {
|
||||
dbgln("Wavefront: Failed to read any data from 3D file: {}", fname);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
dbgln("Wavefront: Done.");
|
||||
return adopt_ref(*new Mesh(vertices, triangles));
|
||||
}
|
21
Userland/Applications/3DFileViewer/WavefrontOBJLoader.h
Normal file
21
Userland/Applications/3DFileViewer/WavefrontOBJLoader.h
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright (c) 2021, Jesse Buhagiar <jooster669@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/RefCounted.h>
|
||||
#include <AK/RefPtr.h>
|
||||
|
||||
#include "Mesh.h"
|
||||
#include "MeshLoader.h"
|
||||
|
||||
class WavefrontOBJLoader : public MeshLoader {
|
||||
public:
|
||||
WavefrontOBJLoader() { }
|
||||
~WavefrontOBJLoader() override { }
|
||||
|
||||
RefPtr<Mesh> load(const String& fname) override;
|
||||
};
|
204
Userland/Applications/3DFileViewer/main.cpp
Normal file
204
Userland/Applications/3DFileViewer/main.cpp
Normal file
|
@ -0,0 +1,204 @@
|
|||
/*
|
||||
* Copyright (c) 2021, Jesse Buhagiar <jooster669@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibCore/ElapsedTimer.h>
|
||||
#include <LibCore/File.h>
|
||||
#include <LibGL/GL/gl.h>
|
||||
#include <LibGL/GLContext.h>
|
||||
#include <LibGUI/Application.h>
|
||||
#include <LibGUI/FilePicker.h>
|
||||
#include <LibGUI/Icon.h>
|
||||
#include <LibGUI/Menu.h>
|
||||
#include <LibGUI/Menubar.h>
|
||||
#include <LibGUI/MessageBox.h>
|
||||
#include <LibGUI/Painter.h>
|
||||
#include <LibGUI/Widget.h>
|
||||
#include <LibGUI/Window.h>
|
||||
#include <LibGfx/Bitmap.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "Mesh.h"
|
||||
#include "MeshLoader.h"
|
||||
#include "WavefrontOBJLoader.h"
|
||||
|
||||
static constexpr u16 RENDER_WIDTH = 640;
|
||||
static constexpr u16 RENDER_HEIGHT = 480;
|
||||
|
||||
class GLContextWidget final : public GUI::Widget {
|
||||
C_OBJECT(GLContextWidget)
|
||||
public:
|
||||
bool load(const String& fname);
|
||||
|
||||
private:
|
||||
GLContextWidget()
|
||||
: m_mesh_loader(adopt_own(*new WavefrontOBJLoader()))
|
||||
{
|
||||
m_bitmap = Gfx::Bitmap::create(Gfx::BitmapFormat::BGRx8888, { RENDER_WIDTH, RENDER_HEIGHT });
|
||||
m_context = GL::create_context(*m_bitmap);
|
||||
|
||||
start_timer(20);
|
||||
|
||||
GL::make_context_current(m_context);
|
||||
glFrontFace(GL_CW);
|
||||
glEnable(GL_CULL_FACE);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
|
||||
// Set projection matrix
|
||||
glMatrixMode(GL_PROJECTION);
|
||||
glLoadIdentity();
|
||||
glFrustum(-0.5, 0.5, -0.5, 0.5, 1, 1500);
|
||||
|
||||
m_init_list = glGenLists(1);
|
||||
glNewList(m_init_list, GL_COMPILE);
|
||||
{
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClearDepth(1.0);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
}
|
||||
glEndList();
|
||||
|
||||
// Load the teapot
|
||||
m_mesh = m_mesh_loader->load("/res/gl/teapot.obj");
|
||||
|
||||
dbgln("3DFileViewer: teapot mesh has {} triangles.", m_mesh->triangle_count());
|
||||
}
|
||||
|
||||
virtual void paint_event(GUI::PaintEvent&) override;
|
||||
virtual void timer_event(Core::TimerEvent&) override;
|
||||
|
||||
private:
|
||||
RefPtr<Mesh> m_mesh;
|
||||
RefPtr<Gfx::Bitmap> m_bitmap;
|
||||
OwnPtr<GL::GLContext> m_context;
|
||||
NonnullOwnPtr<WavefrontOBJLoader> m_mesh_loader;
|
||||
GLuint m_init_list { 0 };
|
||||
};
|
||||
|
||||
void GLContextWidget::paint_event(GUI::PaintEvent& event)
|
||||
{
|
||||
GUI::Painter painter(*this);
|
||||
painter.add_clip_rect(event.rect());
|
||||
painter.draw_scaled_bitmap(rect(), *m_bitmap, m_bitmap->rect());
|
||||
}
|
||||
|
||||
void GLContextWidget::timer_event(Core::TimerEvent&)
|
||||
{
|
||||
static float angle = 0.0f;
|
||||
glCallList(m_init_list);
|
||||
|
||||
angle -= 0.01f;
|
||||
auto matrix = Gfx::translation_matrix(FloatVector3(0, 0, -8.5))
|
||||
* Gfx::rotation_matrix(FloatVector3(1, 0, 0), angle)
|
||||
* Gfx::rotation_matrix(FloatVector3(0, 1, 0), 0.0f)
|
||||
* Gfx::rotation_matrix(FloatVector3(0, 0, 1), angle);
|
||||
|
||||
// We need to transpose here because OpenGL expects matrices in column major order
|
||||
// but our matrix class stores elements in row major order.
|
||||
matrix = matrix.transpose();
|
||||
|
||||
glMatrixMode(GL_MODELVIEW);
|
||||
glLoadMatrixf((float*)matrix.elements());
|
||||
|
||||
if (!m_mesh.is_null())
|
||||
m_mesh->draw();
|
||||
|
||||
m_context->present();
|
||||
update();
|
||||
}
|
||||
|
||||
bool GLContextWidget::load(const String& fname)
|
||||
{
|
||||
m_mesh = m_mesh_loader->load(fname);
|
||||
if (!m_mesh.is_null()) {
|
||||
dbgln("3DFileViewer: mesh has {} triangles.", m_mesh->triangle_count());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
auto app = GUI::Application::construct(argc, argv);
|
||||
|
||||
if (pledge("stdio recvfd sendfd rpath", nullptr) < 0) {
|
||||
perror("pledge");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (unveil("/res", "r") < 0) {
|
||||
perror("unveil");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (unveil("/home", "r") < 0) {
|
||||
perror("unveil");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (unveil(nullptr, nullptr) < 0) {
|
||||
perror("unveil");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Construct the main window
|
||||
auto window = GUI::Window::construct();
|
||||
auto app_icon = GUI::Icon::default_icon("app-3d-file-viewer");
|
||||
|
||||
window->set_icon(app_icon.bitmap_for_size(16));
|
||||
window->set_title("3D File Viewer");
|
||||
window->resize(640, 480);
|
||||
window->set_resizable(false);
|
||||
window->set_double_buffering_enabled(true);
|
||||
auto& widget = window->set_main_widget<GLContextWidget>();
|
||||
|
||||
auto menubar = GUI::Menubar::construct();
|
||||
auto& file_menu = menubar->add_menu("&File");
|
||||
|
||||
file_menu.add_action(GUI::CommonActions::make_open_action([&](auto&) {
|
||||
Optional<String> open_path = GUI::FilePicker::get_open_filepath(window);
|
||||
|
||||
if (!open_path.has_value())
|
||||
return;
|
||||
|
||||
auto file = Core::File::construct(open_path.value());
|
||||
|
||||
if (!file->filename().ends_with(".obj")) {
|
||||
GUI::MessageBox::show(window, String::formatted("Opening \"{}\" failed: invalid file type", open_path.value()), "Error", GUI::MessageBox::Type::Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file->open(Core::OpenMode::ReadOnly) && file->error() != ENOENT) {
|
||||
GUI::MessageBox::show(window, String::formatted("Opening \"{}\" failed: {}", open_path.value(), strerror(errno)), "Error", GUI::MessageBox::Type::Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (file->is_device()) {
|
||||
GUI::MessageBox::show(window, String::formatted("Opening \"{}\" failed: Can't open device files", open_path.value()), "Error", GUI::MessageBox::Type::Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (file->is_directory()) {
|
||||
GUI::MessageBox::show(window, String::formatted("Opening \"{}\" failed: Can't open directories", open_path.value()), "Error", GUI::MessageBox::Type::Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!widget.load(file->filename()))
|
||||
GUI::MessageBox::show(window, String::formatted("Reading \"{}\" failed.", open_path.value()), "Error", GUI::MessageBox::Type::Error);
|
||||
}));
|
||||
|
||||
file_menu.add_action(GUI::CommonActions::make_quit_action([&](auto&) {
|
||||
app->quit();
|
||||
}));
|
||||
|
||||
auto& help_menu = menubar->add_menu("&Help");
|
||||
help_menu.add_action(GUI::CommonActions::make_about_action("3D File Viewer", app_icon, window));
|
||||
|
||||
window->set_menubar(move(menubar));
|
||||
window->show();
|
||||
|
||||
return app->exec();
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue