// Copyright (c) 2008-2023 the Urho3D project // License: MIT #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Physics.h" #include URHO3D_DEFINE_APPLICATION_MAIN(Physics) Physics::Physics(Context* context) : Sample(context), drawDebug_(false) { } void Physics::Start() { // Execute base class startup Sample::Start(); // Create the scene content CreateScene(); // Create the UI content CreateInstructions(); // Setup the viewport for displaying the scene SetupViewport(); // Hook up to the frame update and render post-update events SubscribeToEvents(); // Set the mouse mode to use in the sample Sample::InitMouseMode(MM_RELATIVE); } void Physics::CreateScene() { auto* cache = GetSubsystem(); scene_ = new Scene(context_); // Create octree, use default volume (-1000, -1000, -1000) to (1000, 1000, 1000) // Create a physics simulation world with default parameters, which will update at 60fps. Like the Octree must // exist before creating drawable components, the PhysicsWorld must exist before creating physics components. // Finally, create a DebugRenderer component so that we can draw physics debug geometry scene_->CreateComponent(); scene_->CreateComponent(); scene_->CreateComponent(); // Create a Zone component for ambient lighting & fog control Node* zoneNode = scene_->CreateChild("Zone"); auto* zone = zoneNode->CreateComponent(); zone->SetBoundingBox(BoundingBox(-1000.0f, 1000.0f)); zone->SetAmbientColor(Color(0.15f, 0.15f, 0.15f)); zone->SetFogColor(Color(1.0f, 1.0f, 1.0f)); zone->SetFogStart(300.0f); zone->SetFogEnd(500.0f); // Create a directional light to the world. Enable cascaded shadows on it Node* lightNode = scene_->CreateChild("DirectionalLight"); lightNode->SetDirection(Vector3(0.6f, -1.0f, 0.8f)); auto* light = lightNode->CreateComponent(); light->SetLightType(LIGHT_DIRECTIONAL); light->SetCastShadows(true); light->SetShadowBias(BiasParameters(0.00025f, 0.5f)); // Set cascade splits at 10, 50 and 200 world units, fade shadows out at 80% of maximum shadow distance light->SetShadowCascade(CascadeParameters(10.0f, 50.0f, 200.0f, 0.0f, 0.8f)); // Create skybox. The Skybox component is used like StaticModel, but it will be always located at the camera, giving the // illusion of the box planes being far away. Use just the ordinary Box model and a suitable material, whose shader will // generate the necessary 3D texture coordinates for cube mapping Node* skyNode = scene_->CreateChild("Sky"); skyNode->SetScale(500.0f); // The scale actually does not matter auto* skybox = skyNode->CreateComponent(); skybox->SetModel(cache->GetResource("Models/Box.mdl")); skybox->SetMaterial(cache->GetResource("Materials/Skybox.xml")); { // Create a floor object, 1000 x 1000 world units. Adjust position so that the ground is at zero Y Node* floorNode = scene_->CreateChild("Floor"); floorNode->SetPosition(Vector3(0.0f, -0.5f, 0.0f)); floorNode->SetScale(Vector3(1000.0f, 1.0f, 1000.0f)); auto* floorObject = floorNode->CreateComponent(); floorObject->SetModel(cache->GetResource("Models/Box.mdl")); floorObject->SetMaterial(cache->GetResource("Materials/StoneTiled.xml")); // Make the floor physical by adding RigidBody and CollisionShape components. The RigidBody's default // parameters make the object static (zero mass.) Note that a CollisionShape by itself will not participate // in the physics simulation /*RigidBody* body = */floorNode->CreateComponent(); auto* shape = floorNode->CreateComponent(); // Set a box shape of size 1 x 1 x 1 for collision. The shape will be scaled with the scene node scale, so the // rendering and physics representation sizes should match (the box model is also 1 x 1 x 1.) shape->SetBox(Vector3::ONE); } { // Create a pyramid of movable physics objects for (int y = 0; y < 8; ++y) { for (int x = -y; x <= y; ++x) { Node* boxNode = scene_->CreateChild("Box"); boxNode->SetPosition(Vector3((float)x, -(float)y + 8.0f, 0.0f)); auto* boxObject = boxNode->CreateComponent(); boxObject->SetModel(cache->GetResource("Models/Box.mdl")); boxObject->SetMaterial(cache->GetResource("Materials/StoneEnvMapSmall.xml")); boxObject->SetCastShadows(true); // Create RigidBody and CollisionShape components like above. Give the RigidBody mass to make it movable // and also adjust friction. The actual mass is not important; only the mass ratios between colliding // objects are significant auto* body = boxNode->CreateComponent(); body->SetMass(1.0f); body->SetFriction(0.75f); auto* shape = boxNode->CreateComponent(); shape->SetBox(Vector3::ONE); } } } // Create the camera. Set far clip to match the fog. Note: now we actually create the camera node outside the scene, because // we want it to be unaffected by scene load / save cameraNode_ = new Node(context_); auto* camera = cameraNode_->CreateComponent(); camera->SetFarClip(500.0f); // Set an initial position for the camera scene node above the floor cameraNode_->SetPosition(Vector3(0.0f, 5.0f, -20.0f)); } void Physics::CreateInstructions() { auto* cache = GetSubsystem(); auto* ui = GetSubsystem(); // Construct new Text object, set string to display and font to use auto* instructionText = ui->GetRoot()->CreateChild(); instructionText->SetText( "Use WASD keys and mouse/touch to move\n" "LMB to spawn physics objects\n" "F5 to save scene, F7 to load\n" "Space to toggle physics debug geometry" ); instructionText->SetFont(cache->GetResource("Fonts/Anonymous Pro.ttf"), 15); // The text has multiple rows. Center them in relation to each other instructionText->SetTextAlignment(HA_CENTER); // Position the text relative to the screen center instructionText->SetHorizontalAlignment(HA_CENTER); instructionText->SetVerticalAlignment(VA_CENTER); instructionText->SetPosition(0, ui->GetRoot()->GetHeight() / 4); } void Physics::SetupViewport() { auto* renderer = GetSubsystem(); // Set up a viewport to the Renderer subsystem so that the 3D scene can be seen SharedPtr viewport(new Viewport(context_, scene_, cameraNode_->GetComponent())); renderer->SetViewport(0, viewport); } void Physics::SubscribeToEvents() { // Subscribe HandleUpdate() function for processing update events SubscribeToEvent(E_UPDATE, URHO3D_HANDLER(Physics, HandleUpdate)); // Subscribe HandlePostRenderUpdate() function for processing the post-render update event, during which we request // debug geometry SubscribeToEvent(E_POSTRENDERUPDATE, URHO3D_HANDLER(Physics, HandlePostRenderUpdate)); } void Physics::MoveCamera(float timeStep) { // Do not move if the UI has a focused element (the console) if (GetSubsystem()->GetFocusElement()) return; auto* input = GetSubsystem(); // Movement speed as world units per second const float MOVE_SPEED = 20.0f; // Mouse sensitivity as degrees per pixel const float MOUSE_SENSITIVITY = 0.1f; // Use this frame's mouse motion to adjust camera node yaw and pitch. Clamp the pitch between -90 and 90 degrees IntVector2 mouseMove = input->GetMouseMove(); yaw_ += MOUSE_SENSITIVITY * mouseMove.x_; pitch_ += MOUSE_SENSITIVITY * mouseMove.y_; pitch_ = Clamp(pitch_, -90.0f, 90.0f); // Construct new orientation for the camera scene node from yaw and pitch. Roll is fixed to zero cameraNode_->SetRotation(Quaternion(pitch_, yaw_, 0.0f)); // Read WASD keys and move the camera scene node to the corresponding direction if they are pressed if (input->GetKeyDown(KEY_W)) cameraNode_->Translate(Vector3::FORWARD * MOVE_SPEED * timeStep); if (input->GetKeyDown(KEY_S)) cameraNode_->Translate(Vector3::BACK * MOVE_SPEED * timeStep); if (input->GetKeyDown(KEY_A)) cameraNode_->Translate(Vector3::LEFT * MOVE_SPEED * timeStep); if (input->GetKeyDown(KEY_D)) cameraNode_->Translate(Vector3::RIGHT * MOVE_SPEED * timeStep); // "Shoot" a physics object with left mousebutton if (input->GetMouseButtonPress(MOUSEB_LEFT)) SpawnObject(); // Check for loading/saving the scene. Save the scene to the file Data/Scenes/Physics.xml relative to the executable // directory if (input->GetKeyPress(KEY_F5)) { File saveFile(context_, GetSubsystem()->GetProgramDir() + "Data/Scenes/Physics.xml", FILE_WRITE); scene_->SaveXML(saveFile); } if (input->GetKeyPress(KEY_F7)) { File loadFile(context_, GetSubsystem()->GetProgramDir() + "Data/Scenes/Physics.xml", FILE_READ); scene_->LoadXML(loadFile); } // Toggle physics debug geometry with space if (input->GetKeyPress(KEY_SPACE)) drawDebug_ = !drawDebug_; } void Physics::SpawnObject() { auto* cache = GetSubsystem(); // Create a smaller box at camera position Node* boxNode = scene_->CreateChild("SmallBox"); boxNode->SetPosition(cameraNode_->GetPosition()); boxNode->SetRotation(cameraNode_->GetRotation()); boxNode->SetScale(0.25f); auto* boxObject = boxNode->CreateComponent(); boxObject->SetModel(cache->GetResource("Models/Box.mdl")); boxObject->SetMaterial(cache->GetResource("Materials/StoneEnvMapSmall.xml")); boxObject->SetCastShadows(true); // Create physics components, use a smaller mass also auto* body = boxNode->CreateComponent(); body->SetMass(0.25f); body->SetFriction(0.75f); auto* shape = boxNode->CreateComponent(); shape->SetBox(Vector3::ONE); const float OBJECT_VELOCITY = 10.0f; // Set initial velocity for the RigidBody based on camera forward vector. Add also a slight up component // to overcome gravity better body->SetLinearVelocity(cameraNode_->GetRotation() * Vector3(0.0f, 0.25f, 1.0f) * OBJECT_VELOCITY); } void Physics::HandleUpdate(StringHash eventType, VariantMap& eventData) { using namespace Update; // Take the frame time step, which is stored as a float float timeStep = eventData[P_TIMESTEP].GetFloat(); // Move the camera, scale movement with time step MoveCamera(timeStep); } void Physics::HandlePostRenderUpdate(StringHash eventType, VariantMap& eventData) { // If draw debug mode is enabled, draw physics debug geometry. Use depth test to make the result easier to interpret if (drawDebug_) scene_->GetComponent()->DrawDebugGeometry(true); }