// 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 "PhysicsStressTest.h" #include URHO3D_DEFINE_APPLICATION_MAIN(PhysicsStressTest) PhysicsStressTest::PhysicsStressTest(Context* context) : Sample(context), drawDebug_(false) { } void PhysicsStressTest::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 PhysicsStressTest::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(0.5f, 0.5f, 0.7f)); zone->SetFogStart(100.0f); zone->SetFogEnd(300.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 a floor object, 500 x 500 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(500.0f, 1.0f, 500.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 /*RigidBody* body = */floorNode->CreateComponent(); auto* shape = floorNode->CreateComponent(); shape->SetBox(Vector3::ONE); } { // Create static mushrooms with triangle mesh collision const unsigned NUM_MUSHROOMS = 50; for (unsigned i = 0; i < NUM_MUSHROOMS; ++i) { Node* mushroomNode = scene_->CreateChild("Mushroom"); mushroomNode->SetPosition(Vector3(Random(400.0f) - 200.0f, 0.0f, Random(400.0f) - 200.0f)); mushroomNode->SetRotation(Quaternion(0.0f, Random(360.0f), 0.0f)); mushroomNode->SetScale(5.0f + Random(5.0f)); auto* mushroomObject = mushroomNode->CreateComponent(); mushroomObject->SetModel(cache->GetResource("Models/Mushroom.mdl")); mushroomObject->SetMaterial(cache->GetResource("Materials/Mushroom.xml")); mushroomObject->SetCastShadows(true); /*RigidBody* body = */mushroomNode->CreateComponent(); auto* shape = mushroomNode->CreateComponent(); // By default the highest LOD level will be used, the LOD level can be passed as an optional parameter shape->SetTriangleMesh(mushroomObject->GetModel()); } } { // Create a large amount of falling physics objects const unsigned NUM_OBJECTS = 1000; for (unsigned i = 0; i < NUM_OBJECTS; ++i) { Node* boxNode = scene_->CreateChild("Box"); boxNode->SetPosition(Vector3(0.0f, i * 2.0f + 100.0f, 0.0f)); auto* boxObject = boxNode->CreateComponent(); boxObject->SetModel(cache->GetResource("Models/Box.mdl")); boxObject->SetMaterial(cache->GetResource("Materials/StoneSmall.xml")); boxObject->SetCastShadows(true); // Give the RigidBody mass to make it movable and also adjust friction auto* body = boxNode->CreateComponent(); body->SetMass(1.0f); body->SetFriction(1.0f); // Disable collision event signaling to reduce CPU load of the physics simulation body->SetCollisionEventMode(COLLISION_NEVER); auto* shape = boxNode->CreateComponent(); shape->SetBox(Vector3::ONE); } } // Create the camera. Limit far clip distance 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(300.0f); // Set an initial position for the camera scene node above the floor cameraNode_->SetPosition(Vector3(0.0f, 3.0f, -20.0f)); } void PhysicsStressTest::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 PhysicsStressTest::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 PhysicsStressTest::SubscribeToEvents() { // Subscribe HandleUpdate() function for processing update events SubscribeToEvent(E_UPDATE, URHO3D_HANDLER(PhysicsStressTest, HandleUpdate)); // Subscribe HandlePostRenderUpdate() function for processing the post-render update event, during which we request // debug geometry SubscribeToEvent(E_POSTRENDERUPDATE, URHO3D_HANDLER(PhysicsStressTest, HandlePostRenderUpdate)); } void PhysicsStressTest::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 if (input->GetKeyPress(KEY_F5)) { File saveFile(context_, GetSubsystem()->GetProgramDir() + "Data/Scenes/PhysicsStressTest.xml", FILE_WRITE); scene_->SaveXML(saveFile); } if (input->GetKeyPress(KEY_F7)) { File loadFile(context_, GetSubsystem()->GetProgramDir() + "Data/Scenes/PhysicsStressTest.xml", FILE_READ); scene_->LoadXML(loadFile); } // Toggle physics debug geometry with space if (input->GetKeyPress(KEY_SPACE)) drawDebug_ = !drawDebug_; } void PhysicsStressTest::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/StoneSmall.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 PhysicsStressTest::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 PhysicsStressTest::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); }