diff --git a/README.md b/README.md index f9530af..ea13e7f 100644 --- a/README.md +++ b/README.md @@ -124,3 +124,6 @@ public: 1. Add the source file to the [Scenes](Scenes) directory. 2. Run `cmake . -B build` again before building the project. +## Further reading: + +If you would like to know more, or work through a demo example look at our guide in this repository, which you can find in the [guide.md file](https://github.com/tum-pbs/game-physics-template/blob/main/guide.md). This guide works through input handling, extending scenes and further details like adaptive timestepping and will be updated over time too. \ No newline at end of file diff --git a/guide.md b/guide.md index 7e14c74..d7061f8 100644 --- a/guide.md +++ b/guide.md @@ -394,7 +394,7 @@ void Scene1::simulateStep(){ To remove particles after 1 second we add this call to the end of the simulateStep function where we remove particles if a conditional is true for the respective entry: ```cpp particles.erase(std::remove_if(particles.begin(), particles.end(), [](const Particle& particle){ - return particle.lifetime > 1.f; + return particle.lifetime > 1.5f; }), particles.end()); ``` All of this gives us this interactive physics simulation: @@ -406,7 +406,7 @@ All of this gives us this interactive physics simulation: If we want to add gravity we can simply add a gravitational field in the simulateStep function and update the velocity accordingly: ```cpp -glm::vec3 gravityAccel = glm::vec3(0, -9.81f, 0); +glm::vec3 gravityAccel = glm::vec3(0, 0, -9.81f); for (auto& particle : particles){ particle.position += 0.01f * particle.velocity; @@ -415,9 +415,11 @@ for (auto& particle : particles){ } ``` +In this case we set the gravity to be pointing down in the z-direction. Note that some physics engines assign the "down" direction to be the negative y-coordinate (this is an interesting legacy development, e.g., consider 2D games where the down direction is naturally y). Within this framework we always set the z-direction to be up and down and chose the gravity accordingly. + Which gives us our final physics system: -![alt text](https://github.com/user-attachments/assets/34273fe0-48a0-46e9-b9e3-f3da85563a85) +![alt text](https://github.com/user-attachments/assets/80a47bc6-8266-4144-9ec2-74f4447f74cf) ## Keyboard input @@ -483,7 +485,7 @@ The first two states are self-explanatory and describe the state of a key after A key being pressed and released is an _event_, i.e., it is a change of state of the keyboard and if we only look at the _state_ of a key then we cannot detect this single event, however, we can utilize our operating system or window system to send us a notification if this _event_ occured and we have to write functions that react to events instead of checking the state of keys. This has some other convenient advantages, e.g., if our game is running at 1 fps then we have to hold a key for an entire second to make sure the key is being pressed while _some_ function checks for input and there is no way to queue events, whereas for an event based system we only need to make sure that the event is sent at _some_ point which is then queued up for processing. While this may be an extreme example, there can be weird interactions or situations where two functions check for key state during a frame and one sees a key as being held and one sees a key as not being held, leading to very subtle and hard to reproduce bugs. On the other hand, for an event based system a common bug is someone _tabbing_ out of the game while holding a key such that the key _release_ event is never sent. Can you think of other issues? -If you want to use input during any submission __DO NOT USE THE GLFW VARIANT__. The GLFW variant requires changes to files outside of the Scene folder which are not part of the code you submit. (Note that this will only mean that the tutor grading your exercise wont be able to press buttons for any interactive element you chose to implement, and you will not be able to use this during the exam) +Implementing an event based input, e.g., using GLFW key callbacks or ImGUI events, requires changes to files outside of the Scene folder which are not part of the code you submit. (Note that this will mean that the tutor grading your exercise wont be able to press buttons for any interactive element you chose to implement, and you will not be able to use this during the exam so we stronlgy recommend using immediate inputs). ### ImGUI Immediate mode inputs @@ -496,7 +498,7 @@ void Scene1::simulateStep(){ // roll += roll_increment; // yaw += yaw_increment; - glm::vec3 gravityAccel = glm::vec3(0, -9.81f, 0); + glm::vec3 gravityAccel = glm::vec3(0, 0, -9.81f); for (auto& particle : particles){ particle.position += 0.01f * particle.velocity; @@ -505,7 +507,7 @@ void Scene1::simulateStep(){ } particles.erase(std::remove_if(particles.begin(), particles.end(), [](const Particle& particle){ - return particle.lifetime > 1.f; + return particle.lifetime > 1.5f; }), particles.end()); @@ -527,142 +529,121 @@ void Scene1::simulateStep(){ } ``` -Which are all necessary changes. You may still want to try the GLFW based variant as it shows some further C++ features and might be useful in general. +Which are all necessary changes. If you are very curious about input handling then implementing a GLFW event based version can be quite a challenge as it requires changes to multiple files, even outside of the Scenes folder, and potentially some manual tracking of inputs, however, this is not necessary as immediate mode inputs will suffice for all features for this course. ![Example image of the final result showing a spray of many spheres](https://github.com/user-attachments/assets/fb85a57d-fec7-451e-8d85-3baa0d8e38a3) +## Real Time Integration -### GLFW Event based Input +So far we only used a fixed hardcoded timestep, but this may not be the most appropriate solution for your game. A common way to do physics is to instead implement an adaptive timestepping that takes the framerate of the game into account, which you can simply get via ImGui using `ImGui::GetIO().DeltaTime`. Implementing this in our demo code then is straight forward: -Adding the GLFW event based input is significantly more involved as we first need to tell GLFW to send all key inputs to a custom function instead of letting ImGUI handle everything. To do this we need to make changes in the src folder (reminder: you do not submit these files!): ```cpp -// src/Simulator.cpp -void Simulator::init(){ - //... - glfwSetWindowUserPointer(renderer.getWindow(), this); +// Scene1.cpp - glfwSetKeyCallback(renderer.getWindow(), [](GLFWwindow *window, int key, int scancode, int action, int mods) { - auto simulator = static_cast(glfwGetWindowUserPointer(window)); - simulator->onKeyInput(window, key, scancode, action, mods); - }); +void Scene1::simulateStep(){ + float realtimeDt = ImGui::GetIO().DeltaTime; + // ... + for (auto& particle : particles){ + particle.position += realtimeDt * particle.velocity; + particle.lifetime += realtimeDt; + particle.velocity += gravityAccel * realtimeDt; + } + // ... } ``` -What we do here is to first set a _user_ pointer in the GLFW window state, which is useful as the event function cannot otherwise access the simulator instance. Note that we could also do this by using a Singleton design pattern for the simulator, which may be something you want to do in your own code. Also note that this requires some _C_ wizardry via void-pointers. Please try to avoid this as much as possible in your own code as casting things to and from void-pointers is very error prone with unpredictable and unexpected behavior, e.g., if things go wrong your code might order some pizza online. +## Mouse Input -We also need to actually add this onKeyInput function to the parent scene class +Something that may come in handy is having the ability to have a user click and drag on the screen to apply a force to the objects in the scene, even though it is not that useful for this demo example. + +This can be done as well in our framework but is a bit more involved as this requires camera information that is not readily available during the simulator (a simulation does not have the concept of a camera in most cases). Accordingly we need to save all relevant information during a part of the render process where it is available to make it available during the simulation. + +To do this we add a few members to our _Scene_ class: ```cpp -//Scene.h +// Scene.h class Scene{ //... - virtual void onKeyInput(GLFWwindow* window, int key, int scancode, int action, int mods){} + glm::mat4 cameraMatrix = glm::mat4(1); + glm::vec3 fwd = glm::vec3(1, 0, 0); + glm::vec3 right = glm::vec3(0, 1, 0); + glm::vec3 up = glm::vec3(0, 0, 1); } ``` -To do this we need to override the onKeyInput function of the actual scene and create a structure to keep track of the currently pressed inputs (more on this later): +Which are then used to store all relevant information. In these members we will store the camera matrix and from this also derive the forward (the direction _into_ the screen) and the right and up vectors (relative to the camera coordinate system). The forward direction could also be used to _shoot_ objects into the scene, whereas the right and up will be used for a dragging interaction. +To capture this information we need to modify the _onDraw_ function (make sure to do this in the child scenes if they override the parent function too!): ```cpp -// Scene1.h -class Scene1 : public Scene{ - //... - struct inputState{ - bool space = false; - bool w = false, a = false, s = false, d = false; - bool e = false, q = false; - }; - inputState keyState; - - virtual void onKeyInput(GLFWwindow *window, int key, int scancode, int action, int mods) override; +// Scene.cpp +void onDraw(Renderer& renderer){ //... + cameraMatrix = renderer.camera.viewMatrix; + fwd = inverse(cameraMatrix) * glm::vec4(0, 0, 1, 0); + right = inverse(cameraMatrix) * glm::vec4(1, 0, 0, 0); + up = inverse(cameraMatrix) * glm::vec4(0, 1, 0, 0); } ``` -Key input in GLFW works by sending events whenever a key is pressed. To find out which key is being pressed we can look at the _key_ argument, e.g., if we press spacebar then key is equal to `GLFW_KEY_SPACE`. _mods_ contains any modifiers, i.e., shift, alt and control, whereas scancode is an internal value that we do not need to worry about too much. The important part for us now is the action which can have three values: - -- GLFW_KEY_PRESS -- GLFW_KEY_HELD -- GLFW_KEY_REPEAT +To then add a dragging interaction for the right mouse button (the left is used for camera interactivity) we can check if the mouse button was released before the current frame using +```cpp +ImGui::IsMouseReleased(ImGuiMouseButton_Right) +``` -The first value is sent on the initial press of the button and the repeat value is sent once we release the button, note that if a user tabs out of the program and releases the key without the games window being in focus, no release event will be sent and the input is _stuck_. We could naively use the held event to shoot spheres but this would not be fun on most systems as there is a multiple second delay before this event is triggered. In most applications this is beneficial (try pressing a key on your keyboard in an editor and notice the delay), but for us this is not ideal. +Which returns true in this case (we could also add interactivity where the button is _held_ down instead of released but this is left to the reader as an exercise). We can then get the amount of _pixels_ the mouse was dragged by using +```cpp +auto drag = ImGui::GetMouseDragDelta(1); +``` +Note the 1 in the argument to get the drag delta for the _right_ mouse button. If the drag distance was zero we skip adding a force to an object, e.g., if this is done during a loop over all objects: +```cpp +if(ImGui::IsMouseReleased(ImGuiMouseButton_Right)){ + auto drag = ImGui::GetMouseDragDelta(1); + if(drag.x == 0 && drag.y == 0) + continue; + //... +} +``` -Instead we will key track of which key is currently held manually: +To now calculate the mouse force we multiply the drag in the x-direction with the _right_ vector of the camera and the drag in y-direction with the _up_ vector of the camera. Due to conventions and preferences some users may prefer inverting certain components, e.g., should the mouse being moved left move the object to the left (this makes a lot of sense for dragging interactions) or should we use a _spring_ like interaction where we move the mouse to the right and on releasing the button the distance we moved influences the force applied to the object in the opposite direction (this makes a lot of sense for on release interactions). In a full game engine it makes sense to make this configurable for the user but here we invert the y-coordinate (due to screen coordinate systems) and keep the x as is: ```cpp -// Scene1.cpp -void Scene1::onKeyInput(GLFWwindow* window, int key, int scancode, int action, int mods){ - if(action == GLFW_PRESS){ - if(key == GLFW_KEY_W) - keyState.w = true; - if(key == GLFW_KEY_A) - keyState.a = true; - if(key == GLFW_KEY_S) - keyState.s = true; - if(key == GLFW_KEY_D) - keyState.d = true; - if(key == GLFW_KEY_E) - keyState.e = true; - if(key == GLFW_KEY_Q) - keyState.q = true; - if(key == GLFW_KEY_SPACE) - keyState.space = true; - } - if(action == GLFW_RELEASE){ - if(key == GLFW_KEY_W) - keyState.w = false; - if(key == GLFW_KEY_A) - keyState.a = false; - if(key == GLFW_KEY_S) - keyState.s = false; - if(key == GLFW_KEY_D) - keyState.d = false; - if(key == GLFW_KEY_E) - keyState.e = false; - if(key == GLFW_KEY_Q) - keyState.q = false; - if(key == GLFW_KEY_SPACE) - keyState.space = false; - } +if(ImGui::IsMouseReleased(ImGuiMouseButton_Right)){ + auto drag = ImGui::GetMouseDragDelta(1); + if(drag.x == 0 && drag.y == 0) + continue; + auto dx = drag.x * right; + auto dy = -drag.y * up; + object.force += dx + dy; } ``` -Which is repetetive and error prone so try avoiding code like this if possible. We then modify our simulateStep function to no longer automatically rotate the cube and shoot a sphere when the spacebar is held down: +You might also want to scale this force or precompute the value once outside of looping over all objects to avoid recomputing the same values over and over again. Also make sure to apply this force in every substep of multistep integrators! + +Applying this to our particle shooter is straight forward by modifying the simulateStep function (after including the changes to the Scene1 class and the onDraw function): ```cpp -// Scene1.cpp void Scene1::simulateStep(){ - // pitch += pitch_increment; - // roll += roll_increment; - // yaw += yaw_increment; + // ... + if(ImGui::IsMouseReleased(ImGuiMouseButton_Right)){ + auto drag = ImGui::GetMouseDragDelta(1); + if(!(drag.x == 0 && drag.y == 0)){ + auto dx = drag.x * right; + auto dy = -drag.y * up; + for (auto& particle : particles){ + particle.velocity += (dx + dy) * 0.01f; + } + } + } +} +``` - glm::vec3 gravityAccel = glm::vec3(0, -9.81f, 0); +And an example of the behavior this leads to after dragging particles to the right: - for (auto& particle : particles){ - particle.position += 0.01f * particle.velocity; - particle.lifetime += 0.01f; - particle.velocity += gravityAccel * 0.01f; - } - - particles.erase(std::remove_if(particles.begin(), particles.end(), [](const Particle& particle){ - return particle.lifetime > 1.f; - }), particles.end()); +![Image showing mouse interaciton](https://github.com/user-attachments/assets/2f770c5c-f35d-4ff2-86c7-fcab37bfa1e0) - if(keyState.space) - launchSphere(); - if(keyState.w) - pitch += pitch_increment; - if(keyState.s) - pitch -= pitch_increment; - if(keyState.a) - roll += roll_increment; - if(keyState.d) - roll -= roll_increment; - if(keyState.q) - yaw += yaw_increment; - if(keyState.e) - yaw -= yaw_increment; - lastLaunch++; -} +While this interaction is not _physical_ it allows us to add some fun interactivity. If you make these changes you may also want to change how long particles exist as they vanish very quickly which you can do by updating +```cpp +particles.erase(std::remove_if(particles.begin(), particles.end(), [](const Particle& particle){ + return particle.lifetime > 1.5f; +}), particles.end()); ``` -Which gives us our final interactive program that covers all relevant aspects of the framework! - -![Example image of the final result showing a spray of many spheres](https://github.com/user-attachments/assets/fb85a57d-fec7-451e-8d85-3baa0d8e38a3) +You may also want to consider making the lifetime a GUI element or updating it based on the interactions, e.g., a particle that was dragged around has the lifetime counter reset. \ No newline at end of file