From 4d78ca9d945bea8a8b526590b117d5f336db9fa8 Mon Sep 17 00:00:00 2001 From: MrLetsplay2003 Date: Thu, 10 Nov 2022 22:00:34 +0100 Subject: [PATCH] Physics (WIP) --- src/kekengine/cpp/common/defaults.cpp | 41 ++++++++++++++++----- src/kekengine/cpp/object/gameobject.cpp | 29 +++++++++++++-- src/kekengine/cpp/object/object.cpp | 5 ++- src/kekengine/cpp/object/player.cpp | 27 ++++++++++++++ src/kekengine/cpp/physics/physics.cpp | 44 ++++++++++++++++++----- src/kekengine/cpp/util/utils.cpp | 12 ++++++- src/kekengine/include/constants.h | 10 ++++-- src/kekengine/include/gameobject.h | 25 +++++++++++-- src/kekengine/include/internal.h | 3 ++ src/kekengine/include/internal/physics.h | 14 ++++++++ src/kekengine/include/object.h | 6 +++- src/kekengine/include/physics.h | 8 ++++- src/kekengine/include/player.h | 35 ++++++++++++++++++ src/kekengine/include/utils.h | 2 ++ src/kekgame/cpp/kekgame.cpp | 40 ++++++++++++++++----- src/kekgame/res/image/white.png | Bin 0 -> 5219 bytes 16 files changed, 266 insertions(+), 35 deletions(-) create mode 100644 src/kekengine/cpp/object/player.cpp create mode 100644 src/kekengine/include/player.h create mode 100644 src/kekgame/res/image/white.png diff --git a/src/kekengine/cpp/common/defaults.cpp b/src/kekengine/cpp/common/defaults.cpp index 844f59f..d49f7cb 100644 --- a/src/kekengine/cpp/common/defaults.cpp +++ b/src/kekengine/cpp/common/defaults.cpp @@ -3,6 +3,7 @@ #include "uielements.h" #include "internal.h" #include "internal/ui.h" +#include "internal/physics.h" namespace kek::Defaults { @@ -15,6 +16,7 @@ static KeyBinding keyDown, keyOptions, keyToggleCursorMode, + keyToggleNoclip, keyExit; static ButtonElement *options; @@ -22,30 +24,39 @@ static ButtonElement *options; static void defaultInput(GLFWwindow *window, void *data) { if(Input::isKeyboardCaptured()) return; + glm::vec3 direction = glm::vec3(0); + if(Input::getKeyState(keyForward) == GLFW_PRESS) { - kekData.activeCamera->translate(kekData.activeCamera->direction * KEK_NOCLIP_SPEED * kekData.lastFrameTime); + direction += kekData.activeCamera->direction; } if(Input::getKeyState(keyBackward) == GLFW_PRESS) { - kekData.activeCamera->translate(kekData.activeCamera->direction * -KEK_NOCLIP_SPEED * kekData.lastFrameTime); + direction += -kekData.activeCamera->direction; } if(Input::getKeyState(keyLeft) == GLFW_PRESS) { - glm::vec3 camRight = glm::normalize(glm::cross(kekData.activeCamera->direction, glm::vec3(0.0f, 1.0f, 0.0f))); - kekData.activeCamera->translate(-camRight * KEK_NOCLIP_SPEED * kekData.lastFrameTime); + direction += -glm::normalize(glm::cross(kekData.activeCamera->direction, glm::vec3(0.0f, 1.0f, 0.0f))); } if(Input::getKeyState(keyRight) == GLFW_PRESS) { - glm::vec3 camRight = glm::normalize(glm::cross(kekData.activeCamera->direction, glm::vec3(0.0f, 1.0f, 0.0f))); - kekData.activeCamera->translate(camRight * KEK_NOCLIP_SPEED * kekData.lastFrameTime); + direction += glm::normalize(glm::cross(kekData.activeCamera->direction, glm::vec3(0.0f, 1.0f, 0.0f))); } if(Input::getKeyState(keyUp) == GLFW_PRESS) { - kekData.activeCamera->translateY(KEK_NOCLIP_SPEED * kekData.lastFrameTime); + direction += glm::vec3(0,1,0); } if(Input::getKeyState(keyDown) == GLFW_PRESS) { - kekData.activeCamera->translateY(-KEK_NOCLIP_SPEED * kekData.lastFrameTime); + direction += glm::vec3(0,-1,0); + } + + direction = glm::normalize(direction); + if(glm::length2(direction) > 0) { + if(kekData.player->noclip) { + kekData.activeCamera->translate(direction * KEK_NOCLIP_SPEED * kekData.lastFrameTime); + }else { + kekData.player->physics->body->applyCentralImpulse(Physics::fromGLM(glm::normalize(direction) * (kekData.lastFrameTime * 20))); + } } } @@ -65,6 +76,10 @@ static void defaultKeyCallback(GLFWwindow *window, int key, int scancode, int ac Input::setCursorMode(GLFWCursorMode::CAPTURE); } } + + if(key == Input::getKeyBinding(keyToggleNoclip).key && action == GLFW_PRESS) { + kekData.player->noclip = !kekData.player->noclip; + } } static void defaultMouseCallback(GLFWwindow *window, double x, double y, void *data) { @@ -136,6 +151,7 @@ void init() { keyDown = Input::createKeyBinding("Down", GLFW_KEY_LEFT_CONTROL); keyOptions = Input::createKeyBinding("Options", GLFW_KEY_Q); keyToggleCursorMode = Input::createKeyBinding("Toggle Cursor Mode", GLFW_KEY_TAB); + keyToggleNoclip = Input::createKeyBinding("Toggle Noclip", GLFW_KEY_N); keyExit = Input::createKeyBinding("Exit", GLFW_KEY_ESCAPE); Input::addPeriodicCallback(PeriodicCallback(defaultInput, nullptr)); @@ -145,6 +161,15 @@ void init() { options = new ButtonElement(uiPx(0), uiPx(100), uiPx(100), uiPx(50)); //UI::addElement(options); + + kekData.player = new Player(); + btCollisionShape *shape = new btBoxShape(btVector3(1,1,1)); + kekData.player->physics = new PhysicsObjectData(); + btRigidBody *body = new btRigidBody(1, nullptr, shape); + body->setActivationState(DISABLE_DEACTIVATION); + kekData.physics->world->addRigidBody(body); + kekData.player->physics->body = body; + kekData.player->moveTo(glm::vec3(0,10,0)); } void destroy() { diff --git a/src/kekengine/cpp/object/gameobject.cpp b/src/kekengine/cpp/object/gameobject.cpp index cee0603..1017b66 100644 --- a/src/kekengine/cpp/object/gameobject.cpp +++ b/src/kekengine/cpp/object/gameobject.cpp @@ -4,11 +4,12 @@ #include #include "internal.h" +#include "internal/physics.h" namespace kek { GameObject::GameObject() { - + this->physics = nullptr; } GameObject::~GameObject() { @@ -23,7 +24,7 @@ void GameObject::addMesh(Mesh *mesh) { void GameObject::draw(Shader *shader) { glm::mat4 model = glm::mat4(1.0f); - model = glm::translate(model, position) * glm::mat4_cast(rotation); + model = glm::translate(model, getPosition()) * glm::mat4_cast(getRotation()); glUniformMatrix4fv(glGetUniformLocation(kekData.shader->id, "model"), 1, GL_FALSE, glm::value_ptr(model)); for(Mesh *mesh : meshes) { @@ -31,4 +32,28 @@ void GameObject::draw(Shader *shader) { } } +void GameObject::addPhysics(btCollisionShape *shape, float mass, int collisionFlags) { + this->physics = new PhysicsObjectData(); + btRigidBody *body = new btRigidBody(mass, new btDefaultMotionState(), shape); + kekData.physics->world->addRigidBody(body); + body->setCollisionFlags(collisionFlags); + this->physics->body = body; +} + +void GameObject::rotateTo(glm::quat rotation) { + this->physics->body->getWorldTransform().setRotation(Physics::fromGLM(rotation)); +} + +glm::quat GameObject::getRotation() { + return Physics::toGLM(this->physics->body->getWorldTransform().getRotation()); +} + +void GameObject::moveTo(glm::vec3 position) { + this->physics->body->getWorldTransform().setOrigin(Physics::fromGLM(position)); +} + +glm::vec3 GameObject::getPosition() { + return Physics::toGLM(this->physics->body->getWorldTransform().getOrigin()); +} + } diff --git a/src/kekengine/cpp/object/object.cpp b/src/kekengine/cpp/object/object.cpp index ae64966..379d9a5 100644 --- a/src/kekengine/cpp/object/object.cpp +++ b/src/kekengine/cpp/object/object.cpp @@ -23,7 +23,6 @@ void DefaultObject::translate(glm::vec3 delta) { } void DefaultObject::moveTo(glm::vec3 position) { - //this->position = glm::vec3(position.x, position.y, position.z); this->position.x = position.x; this->position.y = position.y; this->position.z = position.z; @@ -45,6 +44,10 @@ void DefaultRotateableObject::rotate(float angle, glm::vec3 axis) { this->rotation = glm::rotate(rotation, glm::radians(angle), axis); } +void DefaultRotateableObject::rotateTo(glm::quat rotation) { + this->rotation = rotation; +} + void DefaultRotateableObject::lookAt(glm::vec3 direction) { this->rotation = glm::quatLookAt(glm::normalize(direction), glm::vec3(0.0f, 1.0f, 0.0f)); } diff --git a/src/kekengine/cpp/object/player.cpp b/src/kekengine/cpp/object/player.cpp new file mode 100644 index 0000000..bf5daa0 --- /dev/null +++ b/src/kekengine/cpp/object/player.cpp @@ -0,0 +1,27 @@ +#include "player.h" + +#include "internal/physics.h" + +namespace kek { + +Player::Player(): RotateableObject() { + this->noclip = false; +} + +void Player::rotateTo(glm::quat rotation) { + this->physics->body->getWorldTransform().setRotation(Physics::fromGLM(rotation)); +} + +glm::quat Player::getRotation() { + return Physics::toGLM(this->physics->body->getWorldTransform().getRotation()); +} + +void Player::moveTo(glm::vec3 position) { + this->physics->body->getWorldTransform().setOrigin(Physics::fromGLM(position)); +} + +glm::vec3 Player::getPosition() { + return Physics::toGLM(this->physics->body->getWorldTransform().getOrigin()); +} + +} diff --git a/src/kekengine/cpp/physics/physics.cpp b/src/kekengine/cpp/physics/physics.cpp index 149e207..51ff1c3 100644 --- a/src/kekengine/cpp/physics/physics.cpp +++ b/src/kekengine/cpp/physics/physics.cpp @@ -3,6 +3,8 @@ #include "internal.h" #include "internal/physics.h" +#include + namespace kek::Physics { void init() { @@ -14,23 +16,49 @@ void init() { btSequentialImpulseConstraintSolver *solver = new btSequentialImpulseConstraintSolver(); kekData.physics->world = new btDiscreteDynamicsWorld(dispatcher, overlappingPairCache, solver, collisionConf); kekData.physics->world->setGravity(btVector3(0, -10, 0)); - - btCollisionShape *shape = new btBoxShape(btVector3(1, 1, 1)); - btDefaultMotionState *state = new btDefaultMotionState(); - btRigidBody *body = new btRigidBody(1, state, shape); - kekData.physics->world->addRigidBody(body); } void destroy() { + for(int i = kekData.physics->world->getNumCollisionObjects() - 1; i >= 0; i--) { + btCollisionObject *obj = kekData.physics->world->getCollisionObjectArray()[i]; + btRigidBody *rigid = btRigidBody::upcast(obj); + if(obj->getCollisionShape()) delete obj->getCollisionShape(); + if(rigid && rigid->getMotionState()) delete rigid->getMotionState(); + kekData.physics->world->removeCollisionObject(obj); + delete obj; + } delete kekData.physics; } void step(float deltaT) { + for(GameObject *obj : kekData.activeScene->objects) { + if(obj->physics) { + obj->physics->body->getWorldTransform().setOrigin(fromGLM(obj->getPosition())); + + } + } + kekData.physics->world->stepSimulation(deltaT, 100); - //btRigidBody *body = btRigidBody::upcast(kekData.physics->world->getCollisionObjectArray()[0]); - //btTransform &t = body->getWorldTransform(); - //btVector3 &vec = t.getOrigin(); + if(!kekData.player->noclip) { + kekData.activeCamera->moveTo(kekData.player->getEyePosition()); + } +} + +glm::vec3 toGLM(btVector3 vec) { + return glm::vec3(vec.x(), vec.y(), vec.z()); +} + +btVector3 fromGLM(glm::vec3 vec) { + return btVector3(vec.x, vec.y, vec.z); +} + +glm::quat toGLM(btQuaternion quat) { + return glm::quat(quat.w(), quat.x(), quat.y(), quat.z()); +} + +btQuaternion fromGLM(glm::quat quat) { + return btQuaternion(quat.x, quat.y, quat.z, quat.w); } } diff --git a/src/kekengine/cpp/util/utils.cpp b/src/kekengine/cpp/util/utils.cpp index 3aaeeac..d43e439 100644 --- a/src/kekengine/cpp/util/utils.cpp +++ b/src/kekengine/cpp/util/utils.cpp @@ -167,8 +167,18 @@ std::vector genCubeVertices(float w, float h, float d, bool texCoor return vertices; } +Mesh *genCubeMesh(float w, float h, float d, std::shared_ptr ambient, std::shared_ptr diffuse, std::shared_ptr specular) { + std::vector vertices = genCubeVertices(w, h, d, true); + std::vector indices; + for(size_t i = 0; i < vertices.size(); i++) { + indices.push_back(i); + } + Material *material = new Material(ambient, diffuse, specular, 80.0f); // FIXME: hard-coded shininess + return new Mesh(vertices, indices, material); +} + std::string toString(glm::vec3 vector) { return std::to_string(vector.x) + " " + std::to_string(vector.y) + " " + std::to_string(vector.z); } -} \ No newline at end of file +} diff --git a/src/kekengine/include/constants.h b/src/kekengine/include/constants.h index 8b82345..b6904e3 100644 --- a/src/kekengine/include/constants.h +++ b/src/kekengine/include/constants.h @@ -15,6 +15,9 @@ #define KEK_LIGHT_LIMIT 64 // Also in shader/include/constants.glsl +#define KEK_LIGHT_MAX_DISTANCE 50 +#define KEK_LIGHT_MAX_DISTANCE_SQUARED (KEK_LIGHT_MAX_DISTANCE * KEK_LIGHT_MAX_DISTANCE) + #define KEK_NOCLIP_SPEED 10.0f #define KEK_CAMERA_NEAR 0.1f @@ -23,9 +26,6 @@ #define KEK_LIGHT_DEFAULT_AMBIENT_STRENGTH 0.05f #define KEK_LIGHT_DEFAULT_SPECULAR_STRENGTH 0.1f -#define KEK_LIGHT_MAX_DISTANCE 30 -#define KEK_LIGHT_MAX_DISTANCE_SQUARED (KEK_LIGHT_MAX_DISTANCE * KEK_LIGHT_MAX_DISTANCE) - #define KEK_INVALID_KEY_BINDING_NAME "INVALID" #define KEK_INVALID_ID -1u @@ -42,3 +42,7 @@ #define KEK_DEFAULT_FONT_SIZE_PIXELS 24 #define KEK_INPUT_DELETE -1u + +#define KEK_PLAYER_HEIGHT 2 +#define KEK_PLAYER_RADIUS 0.5f +#define KEK_PLAYER_EYE_OFFSET (KEK_PLAYER_HEIGHT / 2 - KEK_PLAYER_RADIUS) diff --git a/src/kekengine/include/gameobject.h b/src/kekengine/include/gameobject.h index 3dafe1b..4c0d908 100644 --- a/src/kekengine/include/gameobject.h +++ b/src/kekengine/include/gameobject.h @@ -2,24 +2,45 @@ #include "object.h" #include "mesh.h" +#include "physics.h" + +#include namespace kek { -class GameObject: public DefaultRotateableObject { +class GameObject: public RotateableObject { protected: std::vector meshes; public: + PhysicsObjectData *physics; + GameObject(); - ~GameObject(); + virtual ~GameObject(); // Adds a mesh to the GameObject. The GameObject takes ownership of the Mesh, so don't use Meshes in multiple GameObjects void addMesh(Mesh *mesh); void draw(Shader *shader); + void addPhysics(btCollisionShape *shape, float mass = 1, int collisionFlags = 0); + + virtual void rotate(float angle, glm::vec3 axis) { rotateTo(glm::rotate(getRotation(), angle, axis)); }; + + virtual void rotateTo(glm::quat rotation); + + virtual void lookAt(glm::vec3 direction) { rotateTo(glm::quatLookAt(glm::normalize(direction), glm::vec3(0,1,0))); }; + + virtual glm::quat getRotation(); + + virtual void translate(glm::vec3 delta) { moveTo(getPosition() + delta); }; + + virtual void moveTo(glm::vec3 position); + + virtual glm::vec3 getPosition(); + }; } diff --git a/src/kekengine/include/internal.h b/src/kekengine/include/internal.h index 5078f20..7c5ba31 100644 --- a/src/kekengine/include/internal.h +++ b/src/kekengine/include/internal.h @@ -10,6 +10,7 @@ #include "texture.h" #include "fonts.h" #include "ui.h" +#include "player.h" namespace kek { @@ -24,6 +25,8 @@ struct KekData { Camera *activeCamera; Scene *activeScene; + Player *player; + int screenWidth; int screenHeight; diff --git a/src/kekengine/include/internal/physics.h b/src/kekengine/include/internal/physics.h index 8820cab..eb7a4c2 100644 --- a/src/kekengine/include/internal/physics.h +++ b/src/kekengine/include/internal/physics.h @@ -8,4 +8,18 @@ struct PhysicsData { btDynamicsWorld *world; }; +struct PhysicsObjectData { + btRigidBody *body; +}; + +namespace Physics { + +glm::vec3 toGLM(btVector3 vec); +btVector3 fromGLM(glm::vec3 vec); + +glm::quat toGLM(btQuaternion quat); +btQuaternion fromGLM(glm::quat quat); + +} + } diff --git a/src/kekengine/include/object.h b/src/kekengine/include/object.h index 46e1687..222f1bd 100644 --- a/src/kekengine/include/object.h +++ b/src/kekengine/include/object.h @@ -49,6 +49,8 @@ public: virtual void rotate(float angle, glm::vec3 axis) = 0; + virtual void rotateTo(glm::quat rotation) = 0; + virtual void lookAt(glm::vec3 direction) = 0; virtual void lookAtPos(glm::vec3 position); @@ -69,6 +71,8 @@ public: virtual void rotate(float angle, glm::vec3 axis); + virtual void rotateTo(glm::quat rotation); + virtual void lookAt(glm::vec3 direction); virtual glm::quat getRotation(); @@ -81,4 +85,4 @@ public: }; -} \ No newline at end of file +} diff --git a/src/kekengine/include/physics.h b/src/kekengine/include/physics.h index a4a8bf1..0479b33 100644 --- a/src/kekengine/include/physics.h +++ b/src/kekengine/include/physics.h @@ -1,6 +1,10 @@ #pragma once -namespace kek::Physics { +namespace kek { + +struct PhysicsObjectData; + +namespace Physics { void init(); void destroy(); @@ -8,3 +12,5 @@ void destroy(); void step(float deltaT); } + +} diff --git a/src/kekengine/include/player.h b/src/kekengine/include/player.h new file mode 100644 index 0000000..aaed436 --- /dev/null +++ b/src/kekengine/include/player.h @@ -0,0 +1,35 @@ +#pragma once + +#include "gameobject.h" + +namespace kek { + +class Player: public RotateableObject { + +public: + PhysicsObjectData *physics; + bool noclip; + + Player(); + + virtual void rotate(float angle, glm::vec3 axis) { rotateTo(glm::rotate(getRotation(), angle, axis)); }; + + virtual void rotateTo(glm::quat rotation); + + virtual void lookAt(glm::vec3 direction) { rotateTo(glm::quatLookAt(glm::normalize(direction), glm::vec3(0,1,0))); }; + + virtual glm::quat getRotation(); + + virtual void translate(glm::vec3 delta) { moveTo(getPosition() + delta); }; + + virtual void moveTo(glm::vec3 position); + + virtual glm::vec3 getPosition(); + + virtual glm::vec3 getEyePosition() { return getPosition() + glm::vec3(0, KEK_PLAYER_EYE_OFFSET, 0); } + + virtual glm::vec3 getFootPosition() { return getPosition() - glm::vec3(0, KEK_PLAYER_HEIGHT / 2, 0); }; + +}; + +} diff --git a/src/kekengine/include/utils.h b/src/kekengine/include/utils.h index ea3562d..cd41868 100644 --- a/src/kekengine/include/utils.h +++ b/src/kekengine/include/utils.h @@ -46,6 +46,8 @@ void genCubeVertices(float w, float h, float d, bool texCoords, float **outVerts std::vector genCubeVertices(float w, float h, float d, bool texCoords); +Mesh *genCubeMesh(float w, float h, float d, std::shared_ptr ambient, std::shared_ptr diffuse, std::shared_ptr specular); + std::string toString(glm::vec3 vector); } diff --git a/src/kekgame/cpp/kekgame.cpp b/src/kekgame/cpp/kekgame.cpp index 6186299..21ae21b 100644 --- a/src/kekgame/cpp/kekgame.cpp +++ b/src/kekgame/cpp/kekgame.cpp @@ -4,6 +4,9 @@ #include #include +#include +#include + using namespace kek; static ButtonElement *button; @@ -43,24 +46,45 @@ void onButtonClick(void *data) { int main(int argc, char **argv) { if(Engine::init() != KEK_SUCCESS) return 1; - MemoryBuffer *buf = Resource::loadResource("object/sphere/Sphere.obj"); - Mesh *mesh = ObjParser::parseMesh(buf, "object/sphere/"); - delete buf; - - GameObject *test = new GameObject(); - test->addMesh(mesh); - Scene *scene = new Scene(); + + GameObject *floor = new GameObject(); + { + std::shared_ptr tex = Texture::load("image/white.png"); + floor->addMesh(genCubeMesh(100, 1, 100, tex, tex, tex)); + btCollisionShape *shape = new btBoxShape(btVector3(50,0.5,50)); + floor->addPhysics(shape, 0, btCollisionObject::CF_STATIC_OBJECT); + scene->addObject(floor); + } + + Mesh *mesh = ObjParser::loadMesh("object/sphere/Sphere.obj"); + GameObject *test = new GameObject(); + btCollisionShape *shape = new btSphereShape(1); + test->addPhysics(shape); + test->addMesh(mesh); + test->moveTo(glm::vec3(0,5,0)); scene->addObject(test); + Mesh *mesh2 = ObjParser::loadMesh("object/cube_colored/Cube.obj"); + GameObject *test3 = new GameObject(); + btCollisionShape *shape2 = new btBoxShape(btVector3(1,1,1)); + test3->addPhysics(shape2, 0, btCollisionObject::CF_STATIC_OBJECT); + test3->addMesh(mesh2); + test3->moveTo(glm::vec3(2, 1, 2)); + scene->addObject(test3); + + for(int i = 0; i < 10; i++) { GameObject *test2 = new GameObject(); + btCollisionShape *shape = new btBoxShape(btVector3(1,1,1)); + test2->addPhysics(shape); test2->addMesh(ObjParser::loadMesh("object/cube_colored/Cube.obj")); - test2->moveTo(glm::vec3(0.0f, 5.0f, 3 * i)); + test2->moveTo(glm::vec3(1.0f, 5.0f, 3 * i)); scene->addObject(test2); } PointLight *light = new PointLight(glm::vec3(1), 1, 0, 1); + light->moveTo(glm::vec3(0,1,0)); scene->lights->add(light); //DirectionalLight *l = new DirectionalLight(glm::vec3(1), glm::vec3(1, -1, 1)); diff --git a/src/kekgame/res/image/white.png b/src/kekgame/res/image/white.png new file mode 100644 index 0000000000000000000000000000000000000000..5bcb905439bb0e194669afe773f02a925a9cba07 GIT binary patch literal 5219 zcmeHLc~}$I79T)GsUWhr@c3ki8wyD#D`X@@gea*7!lEEr>tr&4QL;E0NI+>>3KbNQ zr-D)|4+V=>MYKZIK2gysprS=ZTX#`#L#-Qb?Yjwx_`Uw)_vwEoU%uSA_nhB3=XcJz zxi>kA&?)wIZgvm^*~>zLBOu5M46UI4*5IqzQNn>B8;_Kz7&-zo!g@-lCKCvlPSz7J zVItKKWcvMH(bPiku>-T)wlJnnbeg0Txwy2cY-fJ;mDgNT1qxG z27EK(j!wF8qU*@iuM~F*Ju3giy;WaF|HO3M6S^!_cP{sv8tJX#8cjG$%Z|RIDOp>6 zv?R>w`j$05rE>S<)PZwXXJ<-Y@@YP6YQY?b|Mty)j*qJw6Oc7(Js~Sv#q}u6U3YRv z$Q9Aj)yw^2SKjFCzY?)3EKF78)$nRx-1Oe1YrMSstiuY|epf56UC%#Qce}80^w;AX zjDKf5@f@4FqA_(~ZA@#^iswH{$2=SS@cY}tE*|k~xwd>>!_S6GMJcn1yzLKGUyA*M zyZLJPQq7r++f~12pSu+iGY;w-t=+n1GFTUU@3F{ENDl`r1trFE{+=!0^X^L~mabH{{?_ znYKEnFU+qF3*`F@ZNArDNo2S>p1z?z(s#Z;%k|sec-1Ou-TBIjadXbg&(>J~+&|8H zsi#%?YS~Jz6c;i&PaXK-ATi z#$B+-lA?%thf7_Hy_2H1R6My7w2W>q7&PIP*Az|vlEUKZ3tN)Cw&#`P|!bO=N2M)6kBlO{X^g`q|1p{G`atb9YNK zZr%wV%X7RQ3~N_F*2QCYwy$&wqilDKxw6o9X6Zoh#Dll^Iq7rJoWzsIJ7gWRw4Tqy z7F;({*!q|3qggo}c^C38>?=LnRunkv(2OD3H%@znNPITm?W}9LE4g3Sq71zH-<(thV)7y=qL?UiTHv|j3)hUw2N-m*Jt#)GF<@-2%yhOz zoEUlWu&K~^xx{q_HO6L<^+TU#`z5AKQ%du1qWjKMV!|U9H{~1|xCeeXvV|zR>^Ic? z>7!$d3qp@OPg$Hb>0gbSW4YO%`%B%YN9FzbdU%oRiCJsUP^$#@>%Sbzb0a`W2HF z{O8f(YRTlZE3A_h=bt{S()fm^?dR#scfDGLB9CM#Tg#>SX_qutuI(wWU%3B<1JYSG zHM=#$D)z5MBTSMFx(4glH4~ePC)C^&g?gm%kB?8bad)@g(6?dEGm|m;<%PfQktgok z+R3QYp_{V)278(t3HGiS`7{*QX;_#_rzBV=jUMc65acg0=`nmhLBmQSj?{`7_xIK^ zU{WP!L<{7ITpvWllOZV-5t$Mig{RENMJk3Qz|P-<0ssv`W3Wk+pf#W-F~fq3g0Xp; z&44W;^n5WRMy`N^bQA&eS$r14l$yvSE+fDW_NP>8G$MF%Hw1VRGvaAlkFwcDqmgCg zv2;`%nf-vY3DV&fd5n6g= zZwM9Mqpwe-5-j1Ua5j-ZXn?2zSmnH9a*9l@=+Q7!5JzhC7A-*bJCZc1eoNLnzL{4n z;q-O{sQ2K$BmE|Ji!hLq%h6yRo@jPY7A$6%?V~CkPO8wZp+F!I`uYi&h)5)0@-ZA^ zDn(os6Y&+P1Zu8`;0Xo2pk!JDjcIYh3OZ?rpiyGV)6+bVfyh9J`*8?zCy&0%S8xZ z3zP~+C+jE;2GU7tusDLP*Tz{E%!H$X3YnO}Wg%~86bTru1`1-vG*X*rdb+)AAT3KUY$;n5!@GV0!_(6}xA<>gLQ$<41gT=i(z;0MMU|ycfTp==wz0 zdol1{!k?<^6J77cz6!x%9bObw@{F?5hlV?HHAmctvNf{i2{51S8f(diCD!5#Re zEI2R<{4dlTXb(aC43*NFA)Cwe%h6Zn&G&kdaSGPji literal 0 HcmV?d00001