Commit 824caabd authored by Michal Petr's avatar Michal Petr
Browse files

feat: Collision tutorial

parent f1e24dfe
Loading
Loading
Loading
Loading
Compare dbf48ebc to e48cfb1e
Original line number Diff line number Diff line
Subproject commit dbf48ebcbba760f0c5469a7e26042c076b1ec3c2
Subproject commit e48cfb1e0e6fea04804a4bbc0de2794fa2afd1c7
+51 −17
Original line number Diff line number Diff line
@@ -11,6 +11,7 @@
#   include "osi/index.hpp"
#include "phx/body_system.hpp"
#   include "phx/collisions/collision_info.hpp"
#include "phx/collisions/collision_system.hpp"

namespace com {
    struct Frame;
@@ -28,6 +29,7 @@ namespace tut_collisions {
        ~Presenter() override;

        void next_round() override;
    	void next_round_tutorial();

        com::Folder* light_directional() const;
        com::Folder* light_ambient() const;
@@ -44,6 +46,8 @@ namespace tut_collisions {

        void handle_inputs();

    	void get_collisions();

        void draw_aabbs();
        void draw_debug_colliders();

@@ -84,7 +88,7 @@ namespace tut_collisions {
        com::Folder* m_world;

        // Debug drawing
        bool m_draw_objects = true;
        bool m_draw_objects_transparent = false;
        bool m_draw_aabbs = false;
        bool m_draw_debug_colliders = false;
        bool m_is_sim_running = false;
@@ -179,7 +183,7 @@ namespace tut_collisions {
    // Helper function to create a material for the crate
    inline void Presenter::create_gfx_materials() {
        // Crate material
        auto shader_crate = gfx::shader_system()->insert_shader<Graph>({name()}, "box-shader", Graph::LIT);
        auto shader_crate = gfx::shader_system()->insert_shader<Graph>({name()}, "box-shader", Graph::LIT, Graph::PHONG, Graph::PARTIAL);
        auto normal = shader_crate->insert<Graph::ConstantNode>(vec3{0.5, 0.5, 1.});

        shader_crate->connect(shader_crate->root(), Graph::MasterNodeLit::inputs::normal_ts, normal, 0);
@@ -192,6 +196,9 @@ namespace tut_collisions {
        shader_crate->connect(shader_crate->root(), Graph::MasterNodeLit::inputs::diffuse, container, 0);
        shader_crate->connect(shader_crate->root(), Graph::MasterNodeLit::inputs::specular, container, 0);

    	auto alpha = shader_crate->insert<Graph::ConstantNode>(0.5f);
    	shader_crate->connect(shader_crate->root(), Graph::MasterNodeLit::inputs::alpha, alpha, 0);

        auto emission = shader_crate->insert<Graph::ConstantNode>(vec3{0.});
        shader_crate->connect(shader_crate->root(), Graph::MasterNodeLit::inputs::emission, emission, 0);

@@ -299,7 +306,7 @@ namespace tut_collisions {
        // Debug shapes
        auto line_mesh = gfx::buffer_generators()->insert_procedural_box_wireframe(
            vec3{0.001, 0.001, 1}, "line_mesh", {"tutorial"});
        m_gfx_instances_lines = gfx::object_system()->insert_object({"tutorial", "lines"}, m_material_blue, line_mesh);
        m_gfx_instances_lines = gfx::object_system()->insert_object({"tutorial", "lines"}, m_material_cyan, line_mesh);

        auto sphere_ind_mesh = gfx::buffer_generators()->insert_procedural_sphere_solid(
            0.025f, 10, "sphere_ind_mesh", {"tutorial"});
@@ -334,22 +341,46 @@ namespace tut_collisions {
        const auto insert_text = [&](const std::string& text, size_t line, float offset_left = 0.0f) {
            auto* static_text = gfx::ui_generators()->create_ui_static_text(
                "static_text_" + std::to_string(line), vec2i{1000, 50}, nullptr, ui_scene, ui_layer, text, false);
            static_text->get_transform()->set_anchor(1.0f, offset_left, (static_cast<float>(line) * 0.1f), 1.0f);
            static_text->get_transform()->set_anchor(1.0f, offset_left, (static_cast<float>(line) * 0.025f) + 0.1f, 1.0f);
            static_text->register_handler(buffer_handler);
            static_text->request_fonts_to_load("OpenSans", 48U, true, true);
            static_text->request_fonts_to_load("OpenSans", 12U, true, true);
            static_text->set_horizontal_alignment(gfx::StaticText::LEFT);
            static_text->set_vertical_alignment(gfx::StaticText::TOP);

            gfx::ui_system()->insert_objects(static_text);
        };

        insert_text("1-6 - Select objects", 0);
        insert_text("F1  - Show/hide objects", 1);
        insert_text("F2  - Show/hide AABBs", 2);
        insert_text("F3  - Show/hide debug colliders", 3);
        insert_text("Arrow keys - Move selected object", 4);
        insert_text("Insert/Delete/Home/End - Rotate selected object", 5);
        insert_text("Comma/Period - Precision movement", 6);
        insert_text("1-6 - Select objects", 1);
        insert_text("F1  - Show/hide objects", 2);
        insert_text("F2  - Show/hide AABBs", 3);
        insert_text("F3  - Show/hide debug colliders", 4);
        insert_text("Arrow keys - Move selected object", 5);
        insert_text("Insert/Delete/Home/End - Rotate selected object", 6);
        insert_text("Comma/Period - Precision movement", 7);
    }

    inline void Presenter::next_round() {
    	// Clear the debug shapes
    	clear_lines();
    	clear_spheres();

    	handle_inputs();

    	next_round_tutorial();

    	// Debug drawing
    	get_collisions();

    	if (m_draw_aabbs) draw_aabbs();
    	if (m_draw_debug_colliders) draw_debug_colliders();

    	// Rendering
    	gfx::renderer()->clear_render_buffers();

    	gfx::camera_system()->activate_default_camera();
    	gfx::renderer()->present_collection(gfx::object_system()->objects()->find<com::Folder>("tutorial"), m_draw_objects_transparent ? gfx::Renderer::pipeline::TRANSPARENT : gfx::Renderer::pipeline::FORWARD);

    	gfx::ui_system()->present_current_scene(true, 2.0f);
    }

    inline void Presenter::handle_inputs() {
@@ -384,13 +415,9 @@ namespace tut_collisions {
            m_selected_object = &m_body_convex;
        }

    	if (osi::keyboard()->just_released().contains(SDL_GetKeyName(SDLK_SPACE))) {
			m_is_sim_running = !m_is_sim_running;
		}

        // Debug toggles
        if (osi::keyboard()->just_released().contains(SDL_GetKeyName(SDLK_F1))) {
            m_draw_objects = !m_draw_objects;
            m_draw_objects_transparent = !m_draw_objects_transparent;
        }

        if (osi::keyboard()->just_released().contains(SDL_GetKeyName(SDLK_F2))) {
@@ -464,6 +491,11 @@ namespace tut_collisions {
		}
    }

    inline void Presenter::get_collisions() {
    	m_broad_collisions = phx::collision_system()->get_broad_collisions(m_world);
    	m_narrow_collisions = phx::collision_system()->get_collisions(m_world);
    }

    inline void Presenter::draw_aabbs() {
        const auto objects = std::vector({m_body_box, m_body_sphere, m_body_cylinder, m_body_cone, m_body_capsule, m_body_convex});

@@ -537,6 +569,8 @@ namespace tut_collisions {
		const auto direction = normalize(destination - origin);
		const auto length = distance(origin, destination);

    	if (length < Num::epsilon) return;

		const auto direction_quat = look_at(direction, vec3{0, 0, 1}, vec3{0, 1, 0});

		const auto frame = m_lines_frame_folder->push_back<com::Folder>(m_lines_frame_folder->generate_unique_name("line"))->push_back<com::Frame>();
+141 −18
Original line number Diff line number Diff line
@@ -40,13 +40,29 @@ namespace tut_collisions {
        phx::body_system()->insert_sphere_collider(sphere_box, 0.5f);
        phx::body_system()->insert_cylinder_collider(cylinder_box, 0.5f, 2.0f);

        // We can also use a slightly different syntax. The following two lines are equivalent.
        phx::body_system()->insert_collider<phx::ConeCollider>(cone_box, 0.5f, 1.0f);
        phx::body_system()->insert_collider<phx::CapsuleCollider>(capsule_box, 0.5f, 2.0f);
        // To insert a collider into a different physics layer, we can pass an optional parameter to the insert_collider
        // function; by default, if no parameter is passed, the collider is inserted into the default layer.
        // Let's create a new layer and insert the rest of the colliders into it, we can do so by using the layer_system
        // module.
        auto layer = phx::layer_system()->create_layer("new_layer");

        // By default, the new layer interacts will all other layers, but you can supply an optional parameter to the
        // create_layer function to say, if that layer should interact with all other layers or not.
        // If you want to specifically exclude a layer from interacting with another layer, you'll have to modify the
        // collision_matrix, like so:
        phx::collision_matrix()->disable_collision(phx::layer_system()->default_layer(), layer);
        // We have now disabled the default layer from interacting with the new layer.

        // Let's create the rest of the colliders with the new layer.
        phx::body_system()->insert_cone_collider(cone_box, 0.5f, 1.0f, layer);

        // If you wish to set a layer to a collider later, you can do so in the body_system module.
        phx::body_system()->insert_capsule_collider(capsule_box, 0.5f, 2.0f);
        phx::body_system()->insert_layer(capsule_box, layer);

        // We can also explicitly declare a convex polyhedron as the collider. This requires the vertices and indices of
        // the polyhedron to be passed to the insert_collider function. The polyhedron must be convex.
        phx::body_system()->insert_collider<phx::ConvexHullCollider>(convex_box, m_rand_mesh_vertices, m_rand_mesh_indices);
        phx::body_system()->insert_convex_hull_collider(convex_box, m_rand_mesh_vertices, m_rand_mesh_indices, layer);

        // We'll create the world and insert the bodies into it.
        m_world = phx::world_system()->insert_world({"world"});
@@ -73,6 +89,15 @@ namespace tut_collisions {
        gfx::object_system()->push_frame_back(m_gfx_instances_capsule, capsule_frame);
        gfx::object_system()->push_frame_back(m_gfx_instances_convex, convex_frame);

        // If you wish to exclude two specific instances from colliding, you can exclude them in the collision_matrix.
        // For example, if you want to exclude the box and the sphere from colliding, you can do so like this:
        auto box_instance_collider = phx::body_system()->get_collider(m_body_box);
        auto sphere_instance_collider = phx::body_system()->get_collider(m_body_sphere);
        phx::collision_matrix()->override_colliders({box_instance_collider, sphere_instance_collider}, false);

        // To remove the exclusion, you can do so like this:
        // phx::collision_matrix()->remove_override({box_instance_collider, sphere_instance_collider});

        // Lastly we'll position the shapes around the origin of the world.
        box_frame->set_origin({5, 0, 0});
        sphere_frame->set_origin({2, 5, 0});
@@ -81,28 +106,126 @@ namespace tut_collisions {
        capsule_frame->set_origin({-2, -5, 0});
        convex_frame->set_origin({2, -5, 0});

        // Running the simulation now, using the debug features, you should see the shapes, their colliders and the
        // bounding boxes of the colliders in the world.
        // Running the simulation now, using the debug features, you'll be able to see the shapes in the world and if
        // you press F3, you'll get a wireframe view of the colliders.

        // The colliders, however, don't interact with each other yet. We'll implement that in the next step.
    }

    void Presenter::next_round_tutorial() {
        // # 2. DETECTING COLLISIONS #
        // Now that we have successfully set up the bodies and colliders, we can now detect the collisions between them.
        // We can now just simply call the detect_collisions function in the collision_system module. This will populate
        // a file, within the root folder of the world, with the appropriate collision information about colliding
        // pairs of colliders.
        // Let's set it up so that we can start and stop the collision detection by pressing the 'space' key. For this
        // there is a member boolean m_is_sim_running that we'll just toggle when the 'space' key is pressed.
        if (osi::keyboard()->just_released().contains(SDL_GetKeyName(SDLK_SPACE))) {
            m_is_sim_running = !m_is_sim_running;
        }

    void Presenter::next_round() {
        handle_inputs();
        clear_lines();
        clear_spheres();
        // Now, if the simulation is running, we'll simply call the detect_collisions function.
        // We can specify the broad and narrow phase algorithms to use in the function call. This is important to do
        // in the first call of the function, as the system creates the necessary data structures based on the types
        // specified.
        // You can however change the broad and narrow phase algorithms on the fly, but beware, as this will clear the
        // cache of the previous algorithm and will cause the system to rebuild some of the data structures.
        // For broad phase algorithms, you can choose between:
        //  - Bruteforce - phx::BroadPhaseType::Brute
        //      - The simplest algorithm, compares every pair of colliders, having a complexity of O(n^2). However, it
        //        can be useful and even faster for small scenes, as it doesn't require any preprocessing or data
        //        structures.
        //  - Sweep and Prune - phx::BroadPhaseType::SweepAndPrune
        //      - A more efficient algorithm, having a complexity of O(n+m), where n is the number of colliders and m is
        //        the number of overlapping pairs.
        //  - Dynamic AABB Tree - phx::BroadPhaseType::AABBTree
        //      - A more advanced algorithm, having a complexity of O(n log n), where n is the number of colliders.
        //
        // For narrow phase algorithms, you can choose between:
        //  - GJK - phx::NarrowPhaseType::GJK
        //      - A fast and robust algorithm for convex shapes; works with implicit shapes. The implementation uses
        //        the GJK algorithm with the EPA algorithm for calculating the deepest penetration depth.
        //  - V-Clip - phx::NarrowPhaseType::VClip
        //      - A more advanced algorithm for convex shapes; works with explicit shapes. It returns the specific
        //        two features which are either colliding or closest to each other.

        if (m_is_sim_running) {
            phx::collision_system()->detect_collisions(m_world, phx::BroadPhaseType::SweepAndPrune, phx::NarrowPhaseType::GJK);
            // Feel free to swap between the algorithms to see how they affect the performance of the simulation and
            // how they act in different scenarios.
            const auto collision_file = phx::collision_system()->detect_collisions(m_world, phx::BroadPhaseType::SweepAndPrune, phx::NarrowPhaseType::GJK);
        }

        if (m_draw_aabbs) draw_aabbs();
        if (m_draw_debug_colliders) draw_debug_colliders();

        gfx::renderer()->clear_render_buffers();
        // If you run the simulation now, you can enable the AABB's by pressing F2, where you'll see the AABB's changing
        // color based on the collision status of the colliders. Green means no collision, orange means they collided
        // in the broad phase and red means they collided in the narrow phase.

        // Let's move on to the next step where we'll use some of the information from the collisions.

        // # 3. VISUALISING COLLISIONS #
        // We'll now use the information from the collisions to draw points at the collision points. We will use the
        // debug functions to draw the points, which you can explore on your own in the header file.
        // First we'll iterate over the collisions and get the points of the collisions.
        // We can get the collisions by calling the get_collisions function in the collision_system module.
        auto collisions = phx::collision_system()->get_collisions(m_world);
        for (const auto& collision : collisions) {
            // The information in the collision info struct is:
            //  id - The id of the colliders that collided - i.e. the pair of colliders that collided.
            //  is_colliding - A boolean that tells if the colliders are colliding or not.
            //  is_valid - A boolean that tells if the rest of the information is valid or not. Sometimes, algorithms
            //             can't find the collision points, especially in the case of a very deep penetration.
            //  points - A pair of vec3's that represent the collision points.
            //  normal - The normal of the collision.
            //  distance - The penetration depth of the collision.
            //  features - A pair of geometry::Feature's that represent the features of the colliders that collided.
            //             This is only available if the narrow phase algorithm is V-Clip.

            // Let's use the draw_sphere function to draw the points of the collision.
            draw_sphere(collision.points.first);
            draw_sphere(collision.points.second);

            // If you run the simulation now, you'll see the points of the collisions as spheres in the world. It may
            // be hard to see the points, so if you press F1, you can toggle the bodies to be transparent, which
            // should make it easier to see the points.

            // Remember, the box and the sphere are excluded from colliding and the capsule, cone and convex hull are
            // in a separate layer, so they will collide only with each other.
        }

        gfx::camera_system()->activate_default_camera();
        gfx::renderer()->present_collection(gfx::object_system()->objects(), gfx::Renderer::pipeline::FORWARD);
        // # 4. DISTANCE QUERY #
        // Regardless of whether two colliders are colliding or not, we can query the distance between them. We'll set
        // it up so that we can toggle the distance query by pressing the 'v' key, which will draw lines between the
        // colliders and spheres at the closest points between them.
        if (osi::keyboard()->just_released().contains(SDL_GetKeyName(SDLK_v))) {
            // We'll call the distance query now. You can do so by calling the distance_query function in the
            // collision_system module. You'll need to supply, which narrow phase algorithm to use.
            // We'll call the distance function between the box and the sphere. The distance query ignores the collision
            // matrix and will always return the distance between the colliders.
            auto box_collider = phx::body_system()->get_collider(m_body_box);
            auto sphere_collider = phx::body_system()->get_collider(m_body_sphere);

            auto result = phx::collision_system()->distance_query(box_collider, sphere_collider, phx::NarrowPhaseType::VClip);

            // The result contains the following information:
            //  distance - The distance between the colliders, negative if they are colliding.
            //  closest_point_a - The closest point on the first collider.
            //  closest_point_b - The closest point on the second collider.
            // We'll print the distance result to the console.
            std::cout << "Distance: " << result.value().distance << std::endl;

            // We'll clear all the previous lines and spheres and draw the new ones, passing the optional parameter
            // to clear persistent debug objects.
            clear_lines(true);
            clear_spheres(true);

            // Let's draw the points and the line between them. We'll set the optional persistent parameter to true,
            // so that the points and the line are not cleared in the next frame.
            draw_sphere(result.value().closest_point_a, true);
            draw_sphere(result.value().closest_point_b, true);
            draw_line(result.value().closest_point_a, result.value().closest_point_b, true);
        }

        gfx::ui_system()->present_current_scene(true, 2.0f);
        // If you run the simulation now, you can press 'v' to see the closest points between the box and the sphere.
        // Feel free to swap the colliders for different objects and see how the distance query works between them.
    }

    void Presenter::release() {