diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0a34381 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vs \ No newline at end of file diff --git a/Character.cpp b/Character.cpp index 3b35c5c..1d29b93 100644 --- a/Character.cpp +++ b/Character.cpp @@ -1,7 +1,9 @@ #include "Character.h" #include "Animator.h" +#include "Ragdoll.h" #include "tFunctions.h" #include +#include #include #include @@ -89,46 +91,6 @@ void Character::TurnToDirection(float dt) { body->t.RotateToQuaternion(glm::normalize(glm::slerp(current, target, blend))); } -void Character::SetPunchAnimation(Animation* animation) { - if (!body || !animation) - return; - - if (!punchAnimator) { - punchAnimator = body->AddAnimator(10); - punchAnimator->blendDuration = 0.08f; - } - - punchAnimation = animation; - punchAnimator->SetAnimation(animation); - punchAnimator->loops = false; - punchAnimator->playbackSpeed = punchAnimationSpeed; - punchAnimator->onAnimationEnd = [this]() { OnPunchAnimationEnd(); }; - punchAnimator->StopAnimation(); - punchHitbox.active = false; - punchHitbox.hitTargets.clear(); -} - -void Character::SetPunchLooping(bool looping) { - if (punchAnimator) - punchAnimator->loops = looping; -} - -void Character::Punch(const glm::vec3& lookDirection) { - if (!punchAnimation || !punchAnimator || punchAnimator->isPlaying) - return; - - const glm::vec3 flatLook(lookDirection.x, 0.0f, lookDirection.z); - punchLookLocked = glm::length(flatLook) > 0.0001f; - if (punchLookLocked) { - punchLookDirection = glm::normalize(flatLook); - SetTurnDirection(punchLookDirection); - } - - punchHitbox.hitTargets.clear(); - punchAnimator->playbackSpeed = punchAnimationSpeed; - punchAnimator->PlayAnimation(); -} - void Character::SetWalking(bool walking) { if (body && body->animator) body->animator->playbackSpeed = walking ? 1.0f : 0.0f; @@ -146,24 +108,38 @@ glm::vec3 Character::GetForwardDirection() const { return forward / length; } -void Character::OnPunchAnimationEnd() { +void Character::EnableRagdoll(Physics& physics) { + if (!ragdoll) + ragdoll = new Ragdoll(*body, GetRigMatrix(), physics); +} + +glm::mat4 Character::GetRigMatrix() const { + // body->t's scale is the hitbox only; the rig is drawn at visualScale so it fills that hitbox + return body->t.GetTranslationMatrix() * body->t.GetRotationMatrix() + * glm::scale(glm::mat4(1.0f), glm::vec3(visualScale)); +} + +bool Character::GetWorldBounds(BoundingBox& bounds) const { if (!body) - return; + return false; - punchHitbox.hitTargets.clear(); + if (ragdoll && !ragdoll->GetParts().empty()) { + bounds.min = glm::vec3(FLT_MAX); + bounds.max = glm::vec3(-FLT_MAX); + for (const Ragdoll::Part* part : ragdoll->GetParts()) { + const BoundingBox partBounds = part->transform.t.GetAABB(); + bounds.min = glm::min(bounds.min, partBounds.min); + bounds.max = glm::max(bounds.max, partBounds.max); + } + return true; + } - glm::vec3 forward = -body->t.GetFrontVector(); - forward.y = 0.0f; - if (glm::length(forward) <= 0.0001f) - return; - forward = glm::normalize(forward); - - punchHitbox.transform.TranslateTo(body->t.GetTranslation() + forward * punchReach); - punchHitbox.transform.RotateToQuaternion(LookAt(forward)); - punchHitbox.transform.ScaleTo(punchHitboxSize); - punchHitbox.damage = (int)punchDamage; - punchHitbox.knockback = 3.5f; - punchHitbox.active = true; + // The rig is about 2 units across natively in every direction it reaches, arms out included. + // A cube that size around the body covers any pose and any turn. + const glm::vec3 reach(visualScale); + bounds.min = body->t.GetTranslation() - reach; + bounds.max = body->t.GetTranslation() + reach; + return true; } Character::Character() { @@ -174,24 +150,15 @@ Character::Character() { void Character::Render(Shader& shader) { if (!body || !body->rig || !body->animator) return; - if (body->animator->m_CurrentAnimation == nullptr) { - for (int i = 0; i < 100; i++) - shader.SetMat4(("finalBonesMatrices[" + std::to_string(i) + "]").c_str(), glm::mat4(1.0f)); - } - else { - std::vector transforms = body->GetBlendedBoneMatrices(); - - for (int i = 0; i < transforms.size(); i++) - shader.SetMat4(("finalBonesMatrices[" + std::to_string(i) + "]").c_str(), transforms[i]); - } + const std::vector transforms = ragdoll ? ragdoll->GetBoneMatrices() + : body->animator->m_CurrentAnimation ? body->GetBlendedBoneMatrices() + : std::vector(100, glm::mat4(1.0f)); + shader.SetMat4Array("finalBonesMatrices", transforms.data(), glm::min((int)transforms.size(), 100)); shader.Set4F("colorOverride", colorOverride); - shader.SetInt("useTexture", 0); - shader.SetInt("tex", 1); + surface.Apply(shader, 1, glm::vec3(visualScale)); shader.Set1F("metallic", metallic); shader.Set1F("roughness", roughness); shader.Set1F("ao", ao); - // body->t's scale is the hitbox only; the rig is drawn at visualScale so it fills that hitbox - body->rig->Render(shader, body->t.GetTranslationMatrix() * body->t.GetRotationMatrix() - * glm::scale(glm::mat4(1.0f), glm::vec3(visualScale))); + body->rig->Render(shader, ragdoll ? glm::mat4(1.0f) : GetRigMatrix()); // ragdoll bones are already in world space shader.Set4F("colorOverride", glm::vec4(0.0f)); // don't leak into other rig draws } \ No newline at end of file diff --git a/Character.h b/Character.h index cf07c4f..3227384 100644 --- a/Character.h +++ b/Character.h @@ -14,22 +14,8 @@ class Character; class CharacterManager; class CharacterBody; class BodyComponent; - -struct Hitbox { - t transform; - int damage = 0; - float knockback = 0.0f; - bool active = false; - std::set hitTargets; - - bool CanHit(Character* target) const { - return target && hitTargets.find(target) == hitTargets.end(); - } - - void RegisterHit(Character* target) { - if (target) hitTargets.insert(target); - } -}; +class Ragdoll; +class Physics; class Action { public: @@ -52,7 +38,6 @@ public: class CharacterBody : public t_package { public: - int Health = 100; Rig* rig = nullptr; std::vector animators; @@ -135,16 +120,14 @@ class Character : public Object, public Renderable { public: CharacterMind* mind = nullptr; CharacterBody* body = nullptr; + Ragdoll* ragdoll = nullptr; // when set, physics drives the rig instead of the animators + SurfaceTexture surface; glm::vec4 colorOverride = glm::vec4(0.0f); // rgb + strength (0 = keep the rig's own texture) float visualScale = 1.4f; // rig is ~1.8 tall natively; hitbox is 2.5 float metallic = 0.0f; float roughness = 0.7f; float ao = 1.0f; float turnSpeed = 12.0f; - float punchDamage = 12.0f; - float punchReach = 1.15f; - float punchAnimationSpeed = 0.6f; - glm::vec3 punchHitboxSize = glm::vec3(1.1f, 1.5f, 1.4f); void PressInput(char letter); void ReleaseInput(char letter); @@ -152,37 +135,26 @@ public: glm::vec3 GetMovementDirection(const glm::vec3& forward, const glm::vec3& right) const; void SetTurnDirection(const glm::vec3& direction); const glm::vec3& GetTurnDirection() const { return turnDirection; } - void SetPunchAnimation(Animation* animation); - void SetPunchLooping(bool looping); - void Punch(const glm::vec3& lookDirection = glm::vec3(0.0f)); void SetWalking(bool walking); glm::vec3 GetForwardDirection() const; - const Hitbox& GetPunchHitbox() const { return punchHitbox; } + void EnableRagdoll(Physics& physics); Character(); void Render(Shader& shader) override; + bool GetWorldBounds(BoundingBox& bounds) const override; void Step(float dt) { - if (punchLookLocked && punchAnimator && punchAnimator->isPlaying) - turnDirection = punchLookDirection; + if (ragdoll) return; TurnToDirection(dt); - punchHitbox.active = false; body->Step(dt); } private: - friend class CharacterManager; - void TurnToDirection(float dt); - void OnPunchAnimationEnd(); + glm::mat4 GetRigMatrix() const; std::set inputLetters; glm::vec3 turnDirection = glm::vec3(0.0f); - Animation* punchAnimation = nullptr; - Animator* punchAnimator = nullptr; - Hitbox punchHitbox; - glm::vec3 punchLookDirection = glm::vec3(0.0f); - bool punchLookLocked = false; }; #endif \ No newline at end of file diff --git a/CharacterManager.cpp b/CharacterManager.cpp index 76a6fec..670dddc 100644 --- a/CharacterManager.cpp +++ b/CharacterManager.cpp @@ -1,9 +1,7 @@ #include "CharacterManager.h" #include "Animator.h" #include "Character.h" -#include "Physics.h" #include "Rig.h" -#include "tFunctions.h" CharacterManager::CharacterManager() = default; CharacterManager::~CharacterManager() = default; @@ -89,33 +87,3 @@ void CharacterManager::Step(float dt) { for (Character* character : characterOrder) character->Step(dt); } - -void CharacterManager::ResolveCombat(Physics& physics) { - for (Character* attacker : characterOrder) { - Hitbox& hitbox = attacker->punchHitbox; - if (!hitbox.active || hitbox.damage <= 0) - continue; - - for (Character* target : characterOrder) { - if (target == attacker || !hitbox.CanHit(target)) - continue; - - if (!BoundingBoxInBoundingBox(hitbox.transform.GetAABB(), target->body->t.GetAABB())) - continue; - - hitbox.RegisterHit(target); - target->body->Health = std::max(0, target->body->Health - hitbox.damage); - - glm::vec3 knockback = target->body->t.GetTranslation() - - attacker->body->t.GetTranslation(); - knockback.y = 0.0f; - if (glm::length(knockback) > 0.0001f) - knockback = glm::normalize(knockback) * hitbox.knockback; - knockback.y = 1.5f; - if (p* targetBody = physics.PPointerOfObject(target->body)) { - targetBody->linearvelocity += knockback; - targetBody->WakeUp(); - } - } - } -} diff --git a/CharacterManager.h b/CharacterManager.h index 64a48aa..3976bb0 100644 --- a/CharacterManager.h +++ b/CharacterManager.h @@ -7,7 +7,6 @@ #include class Character; -class Physics; class Rig; class Animation; @@ -33,7 +32,6 @@ public: void ClearInput(); void Step(float dt); - void ResolveCombat(Physics& physics); const std::vector& GetCharacters() const { return characterOrder; } diff --git a/CollisionMesh.cpp b/CollisionMesh.cpp new file mode 100644 index 0000000..edf7949 --- /dev/null +++ b/CollisionMesh.cpp @@ -0,0 +1,128 @@ +#include "CollisionMesh.h" + +#include +#include + +static constexpr int LEAF_TRIANGLES = 4; + +static glm::vec3 TriangleCentroid(const Triangle& tri) +{ + return (tri.a + tri.b + tri.c) / 3.0f; +} + +// Bounds overlap plus the triangle's plane, which is what prunes huge floor triangles +static bool TriangleOverlapsBox(const Triangle& tri, const glm::vec3& boxMin, const glm::vec3& boxMax) +{ + const glm::vec3 triMin = glm::min(tri.a, glm::min(tri.b, tri.c)); + const glm::vec3 triMax = glm::max(tri.a, glm::max(tri.b, tri.c)); + if (glm::any(glm::lessThan(triMax, boxMin)) || glm::any(glm::greaterThan(triMin, boxMax))) + return false; + + const glm::vec3 normal = glm::cross(tri.b - tri.a, tri.c - tri.a); + const glm::vec3 center = (boxMin + boxMax) * 0.5f; + const glm::vec3 halfSize = (boxMax - boxMin) * 0.5f; + const float distance = glm::dot(normal, center - tri.a); + const float radius = glm::dot(glm::abs(normal), halfSize); + return glm::abs(distance) <= radius; +} + +CollisionMesh::CollisionMesh(const std::vector& meshTriangles) + : triangles(meshTriangles) +{ + if (!triangles.empty()) + Build(0, (int)triangles.size()); +} + +int CollisionMesh::Build(int start, int count) +{ + const int index = (int)nodes.size(); + nodes.push_back(Node()); + + glm::vec3 boundsMin(FLT_MAX); + glm::vec3 boundsMax(-FLT_MAX); + glm::vec3 centroidMin(FLT_MAX); + glm::vec3 centroidMax(-FLT_MAX); + for (int i = start; i < start + count; i++) { + const Triangle& tri = triangles[i]; + boundsMin = glm::min(boundsMin, glm::min(tri.a, glm::min(tri.b, tri.c))); + boundsMax = glm::max(boundsMax, glm::max(tri.a, glm::max(tri.b, tri.c))); + const glm::vec3 centroid = TriangleCentroid(tri); + centroidMin = glm::min(centroidMin, centroid); + centroidMax = glm::max(centroidMax, centroid); + } + nodes[index].boundsMin = boundsMin; + nodes[index].boundsMax = boundsMax; + + if (count <= LEAF_TRIANGLES) { + nodes[index].start = start; + nodes[index].count = count; + return index; + } + + // Split at the median along the widest axis + const glm::vec3 extent = centroidMax - centroidMin; + const int axis = extent.x > extent.y ? (extent.x > extent.z ? 0 : 2) : (extent.y > extent.z ? 1 : 2); + const int half = count / 2; + std::nth_element(triangles.begin() + start, triangles.begin() + start + half, triangles.begin() + start + count, + [axis](const Triangle& a, const Triangle& b) { + return TriangleCentroid(a)[axis] < TriangleCentroid(b)[axis]; + }); + + // Build grows nodes, so index stays valid but references into it wouldn't + const int left = Build(start, half); + const int right = Build(start + half, count - half); + nodes[index].left = left; + nodes[index].right = right; + return index; +} + +void CollisionMesh::Query(const Triangle& tri, std::vector& hits) const +{ + if (nodes.empty()) + return; + + int stack[64]; + int stackSize = 0; + stack[stackSize++] = 0; + + while (stackSize > 0) { + const Node& node = nodes[stack[--stackSize]]; + if (!TriangleOverlapsBox(tri, node.boundsMin, node.boundsMax)) + continue; + + if (node.left == -1) { + for (int i = node.start; i < node.start + node.count; i++) + hits.push_back(i); + } + else { + stack[stackSize++] = node.left; + stack[stackSize++] = node.right; + } + } +} + +const CollisionMesh& CollisionMesh::UnitBox() +{ + static const CollisionMesh box = []() { + const glm::vec3 axes[3] = { {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f, 1.0f} }; + + std::vector faces; + for (int axis = 0; axis < 3; axis++) { + for (float side : { 1.0f, -1.0f }) { + const glm::vec3 normal = axes[axis] * side; + const glm::vec3 u = axes[(axis + 1) % 3]; + const glm::vec3 v = glm::cross(normal, u); // u x v = normal, so the winding faces outward + + const glm::vec3 center = normal * 0.5f; + const glm::vec3 p0 = center - u * 0.5f - v * 0.5f; + const glm::vec3 p1 = center + u * 0.5f - v * 0.5f; + const glm::vec3 p2 = center + u * 0.5f + v * 0.5f; + const glm::vec3 p3 = center - u * 0.5f + v * 0.5f; + faces.push_back({ p0, p1, p2 }); + faces.push_back({ p0, p2, p3 }); + } + } + return CollisionMesh(faces); + }(); + return box; +} diff --git a/CollisionMesh.h b/CollisionMesh.h new file mode 100644 index 0000000..947ee4c --- /dev/null +++ b/CollisionMesh.h @@ -0,0 +1,41 @@ +#ifndef COLLISION_MESH_CLASS +#define COLLISION_MESH_CLASS + +#include + +#include + +#include "Structures.h" + +// Triangle soup for triangle by triangle collision, wrapped in a BVH. +// Triangles live in the body's unit space, so t.GetMatrix() takes them to world space +// the same way it does for a Mesh. Winding must be counter clockwise seen from outside. +class CollisionMesh { +public: + CollisionMesh(const std::vector& meshTriangles); + + // The 12 triangles of a unit cube, used for any body without its own mesh + static const CollisionMesh& UnitBox(); + + const std::vector& GetTriangles() const { return triangles; } + + // Appends the index of every triangle whose BVH leaf could touch tri (same space as the mesh) + void Query(const Triangle& tri, std::vector& hits) const; + +private: + struct Node { + glm::vec3 boundsMin; + glm::vec3 boundsMax; + int left = -1; // -1 = leaf + int right = -1; + int start = 0; + int count = 0; + }; + + int Build(int start, int count); + + std::vector triangles; + std::vector nodes; +}; + +#endif diff --git a/Engine.cpp b/Engine.cpp index 24adb65..da79412 100644 --- a/Engine.cpp +++ b/Engine.cpp @@ -68,12 +68,14 @@ #include "Sound.h" #include "Gui.h" #include "font.h" +#include "TexturePainter.h" // Character / animation #include "Rig.h" #include "Animator.h" #include "Character.h" #include "CharacterManager.h" +#include "Ragdoll.h" #include "DefaultBodyComponents.h" #include "BodyComponentActions.h" @@ -108,6 +110,12 @@ Engine::Engine() rendersystem->RigShader = new Shader("shaders/rig.vert", "shaders/default.frag"); rendersystem->MeshShader = new Shader("shaders/default.vert", "shaders/default.frag"); + rendersystem->GeometryShader = new Shader("shaders/geometry.vert", "shaders/geometry.frag"); + rendersystem->GeometryRigShader = new Shader("shaders/geometry_rig.vert", "shaders/geometry.frag"); + rendersystem->DeferredLightingShader = new Shader("shaders/deferred_lighting.vert", "shaders/deferred_lighting.frag"); + rendersystem->GBufferDebugShader = new Shader("shaders/gbuffer_debug.vert", "shaders/gbuffer_debug.frag"); + rendersystem->BloomExtractShader = new Shader("shaders/ScreenShader.vert", "shaders/bloom_extract.frag"); + rendersystem->BloomBlurShader = new Shader("shaders/ScreenShader.vert", "shaders/bloom_blur.frag"); rendersystem->MeshShadowShader = new Shader("shaders/meshshadow.vert", "shaders/shadow.frag"); rendersystem->RigShadowShader = new Shader("shaders/rigshadow.vert", "shaders/shadow.frag"); rendersystem->SkyboxShader = new Shader("shaders/skybox.vert", "shaders/skybox.frag"); @@ -124,14 +132,12 @@ Engine::Engine() // ============================================================================ void Engine::Initiate() { - Font* arial = new Font("assets/ft/arial.ttf"); - Texture* blanktex = new Texture("assets/blanktexture.png"); - Texture* snowflake = new Texture("assets/snowflake.png"); + Font* arial = resourcemanager->GetFont("assets/ft/arial.ttf"); + Texture* blanktex = resourcemanager->GetTexture("assets/blanktexture.png"); charactermanager = new CharacterManager(); charactermanager->LoadRig("humanoid", "assets/rig.glb"); charactermanager->LoadAnimation("walk", "assets/walkanim.glb", "humanoid"); - charactermanager->LoadAnimation("punch", "assets/punchanim.glb", "humanoid"); // ------------------------------------------------------------------------ // Player character @@ -144,7 +150,6 @@ void Engine::Initiate() testcharacter.body->t.ScaleBy({ 0.7f, 2.5f, 0.7f }); testcharacter.body->t.TranslateTo({ 0.0f, 1.5f, 0.0f }); testcharacter.body->animator->loops = true; - testcharacter.SetPunchAnimation(charactermanager->GetAnimation("punch")); testcharacter.BindToRenderSystem(); p* characterBody = new p(testcharacter.body); @@ -163,39 +168,11 @@ void Engine::Initiate() npc.roughness = 0.5f; npc.body->t.ScaleBy({ 0.7f, 2.5f, 0.7f }); npc.body->t.TranslateTo({ 3.0f, 1.25f, 3.0f }); - npc.SetPunchAnimation(charactermanager->GetAnimation("punch")); - npc.SetPunchLooping(true); - npc.Punch(); npc.BindToRenderSystem(); - p* npcBody = new p(npc.body); - npcBody->mass = 80.0f; - npcBody->lockRotation = true; - npcBody->friction = 0.0f; - physicsengine->AddObject(npcBody); - testcharacter.SetWalking(false); npc.SetWalking(false); - - // ------------------------------------------------------------------------ - // Rain particles - // ------------------------------------------------------------------------ - ParticleEmitter* rain = new ParticleEmitter(); - rain->name = "rain"; - rain->tex = snowflake; - rain->opaque = false; - rain->speed = 35.0f; - rain->lifespan = 0.6f; - rain->angularvelocity = { 0.0f, 0.0f, glm::radians(10.0f) }; - rain->size = { 0.04f, 0.4f, 0.0f }; - rain->emitangle = { glm::radians(60.0f), glm::radians(360.0f), glm::radians(60.0f) }; - rain->facecamera = false; - rain->color = { 1.0f, 1.0f, 1.0f, 1.0f }; - rain->emitdirection = ParticleEmitter::EmitDirection::Perpendicular; - rain->t.TranslateTo({ 0.0f, 10.0f, 0.0f }); - rain->t.RotateToQuaternion(glm::quat(glm::vec3(glm::radians(85.0f), 0.0f, 0.0f))); - rain->BindToRenderSystem(); - mainf->AddChild(rain); + npc.EnableRagdoll(*physicsengine); // ------------------------------------------------------------------------ // 3D audio @@ -221,149 +198,272 @@ void Engine::Initiate() fpsCounter->t2d.size = { 0.1f, 0.1f, 0.0f, 0.0f }; fpsCounter->BindToRenderSystem(); - TextBox* healthText = new TextBox(); - healthText->font = arial; - healthText->text = ""; - healthText->fontsize = 0.5f; - healthText->Color = { 0.95f, 0.97f, 1.0f }; - healthText->t2d.position = { 0.02f, 0.02f, 0.0f, 0.0f }; - healthText->t2d.size = { 0.45f, 0.0f, 0.0f, 40.0f }; - healthText->textCenter = { 0.0f, 0.0f }; - healthText->BindToRenderSystem(); - - const float healthBarFillWidth = 314.0f; - - Box* healthBarBack = new Box(); - healthBarBack->Color = { 0.03f, 0.04f, 0.06f }; - healthBarBack->Opacity = 0.85f; - healthBarBack->t2d.position = { 0.02f, 1.0f, 0.0f, -30.0f }; - healthBarBack->t2d.center = { 0.0f, 1.0f }; - healthBarBack->t2d.size = { 0.0f, 0.0f, 320.0f, 26.0f }; - healthBarBack->BindToRenderSystem(); - - Box* healthBarFill = new Box(); - healthBarFill->Color = { 0.2f, 0.85f, 0.35f }; - healthBarFill->t2d.position = { 0.02f, 1.0f, 3.0f, -33.0f }; - healthBarFill->t2d.center = { 0.0f, 1.0f }; - healthBarFill->t2d.size = { 0.0f, 0.0f, healthBarFillWidth, 20.0f }; - healthBarFill->BindToRenderSystem(); - struct MenuButton { Box* box; + Box* accent; TextBox* label; glm::vec3 color; + glm::vec3 hoverColor; + float opacity; + float labelX; + float labelSlide; // how far the label moves right on hover + bool hovered = false; + float hover = 0.0f; // eased 0-1 }; - enum class MenuScreen { Main, Paused, Settings }; + enum class MenuScreen { Main, Paused, Settings, Graphics, Drawer }; MenuScreen menuScreen = MenuScreen::Main; MenuScreen settingsReturn = MenuScreen::Main; bool menuActive = true; bool prevMouseDown = false; std::vector menuItems; + // Everything hangs off a left side panel so the live scene stays visible. + // Items are positioned in pixels from the left edge and the vertical center of the window. + const float menuPanelWidth = 460.0f; + const float menuMargin = 48.0f; + const float menuContentWidth = 364.0f; + const glm::vec3 menuAccent(0.96f, 0.58f, 0.16f); + const glm::vec3 menuText(0.95f, 0.97f, 1.0f); + const glm::vec3 menuMutedText(0.62f, 0.68f, 0.77f); + const glm::vec3 menuButtonColor(0.11f, 0.14f, 0.19f); + const glm::vec3 menuButtonHover(0.20f, 0.25f, 0.33f); + Box* menuVeil = new Box(); menuVeil->Color = { 0.02f, 0.03f, 0.05f }; - menuVeil->Opacity = 0.55f; menuVeil->t2d.position = { 0.0f, 0.0f, 0.0f, 0.0f }; menuVeil->t2d.size = { 1.0f, 1.0f, 0.0f, 0.0f }; menuVeil->BindToRenderSystem(); menuItems.push_back(menuVeil); - TextBox* menuTitle = new TextBox(); - menuTitle->font = arial; - menuTitle->text = "EIIFWHRN"; - menuTitle->fontsize = 3.0f; - menuTitle->Color = { 0.95f, 0.97f, 1.0f }; - menuTitle->t2d.position = { 0.5f, 0.17f, 0.0f, 0.0f }; - menuTitle->t2d.center = { 0.5f, 0.5f }; - menuTitle->t2d.size = { 0.8f, 0.0f, 0.0f, 170.0f }; - menuTitle->textCenter = { 0.5f, 0.5f }; - menuTitle->BindToRenderSystem(); - menuItems.push_back(menuTitle); + Box* menuPanel = new Box(); + menuPanel->Color = { 0.03f, 0.04f, 0.06f }; + menuPanel->t2d.position = { 0.0f, 0.0f, 0.0f, 0.0f }; + menuPanel->t2d.size = { 0.0f, 1.0f, menuPanelWidth, 0.0f }; + menuPanel->BindToRenderSystem(); + menuItems.push_back(menuPanel); - TextBox* menuSubtitle = new TextBox(); - menuSubtitle->font = arial; - menuSubtitle->text = "MAIN MENU"; - menuSubtitle->fontsize = 0.65f; - menuSubtitle->Color = { 0.55f, 0.65f, 0.78f }; - menuSubtitle->t2d.position = { 0.5f, 0.285f, 0.0f, 0.0f }; - menuSubtitle->t2d.center = { 0.5f, 0.5f }; - menuSubtitle->t2d.size = { 0.4f, 0.0f, 0.0f, 50.0f }; - menuSubtitle->textCenter = { 0.5f, 0.5f }; - menuSubtitle->BindToRenderSystem(); - menuItems.push_back(menuSubtitle); - - auto MakeMenuButton = [&](const char* text, float x, float y, glm::vec3 color, - float width = 340.0f, float height = 62.0f, - float fontsize = 0.85f) { - Box* box = new Box(); - box->Color = color; - box->Opacity = 0.92f; - box->t2d.position = { x, y, 0.0f, 0.0f }; - box->t2d.center = { 0.5f, 0.5f }; - box->t2d.size = { 0.0f, 0.0f, width, height }; - box->BindToRenderSystem(); - menuItems.push_back(box); + Box* menuPanelEdge = new Box(); + menuPanelEdge->Color = menuAccent; + menuPanelEdge->t2d.position = { 0.0f, 0.0f, menuPanelWidth, 0.0f }; + menuPanelEdge->t2d.size = { 0.0f, 1.0f, 3.0f, 0.0f }; + menuPanelEdge->BindToRenderSystem(); + menuItems.push_back(menuPanelEdge); + auto MakeMenuText = [&](const char* text, float x, float y, float width, float height, + float fontsize, glm::vec3 color, float alignX = 0.0f) { TextBox* label = new TextBox(); label->font = arial; label->text = text; label->fontsize = fontsize; - label->Color = { 0.95f, 0.97f, 1.0f }; - label->t2d.position = { x, y, 0.0f, 0.0f }; - label->t2d.center = { 0.5f, 0.5f }; + label->Color = color; + label->t2d.position = { 0.0f, 0.5f, x, y }; + label->t2d.center = { 0.0f, 0.5f }; label->t2d.size = { 0.0f, 0.0f, width, height }; - label->textCenter = { 0.5f, 0.5f }; + label->textCenter = { alignX, 0.5f }; label->BindToRenderSystem(); menuItems.push_back(label); - - return MenuButton{ box, label, color }; + return label; }; - MenuButton playButton = MakeMenuButton("PLAY", 0.5f, 0.46f, glm::vec3(0.12f, 0.30f, 0.22f)); - MenuButton settingsButton = MakeMenuButton("SETTINGS", 0.5f, 0.58f, glm::vec3(0.15f, 0.23f, 0.34f)); - MenuButton quitButton = MakeMenuButton("QUIT", 0.5f, 0.70f, glm::vec3(0.17f, 0.18f, 0.22f)); + TextBox* menuTitle = MakeMenuText("EIIFWHRN", menuMargin, -235.0f, 400.0f, 100.0f, 1.6f, menuText); + TextBox* menuSubtitle = MakeMenuText("MAIN MENU", menuMargin, -165.0f, menuContentWidth, 36.0f, 0.5f, menuAccent); - MenuButton sensitivityDown = MakeMenuButton("- SENS", 0.40f, 0.47f, - glm::vec3(0.15f, 0.23f, 0.34f), 170.0f, 50.0f, 0.65f); - MenuButton sensitivityUp = MakeMenuButton("+ SENS", 0.60f, 0.47f, - glm::vec3(0.15f, 0.23f, 0.34f), 170.0f, 50.0f, 0.65f); - MenuButton fovDown = MakeMenuButton("- FOV", 0.40f, 0.58f, - glm::vec3(0.15f, 0.23f, 0.34f), 170.0f, 50.0f, 0.65f); - MenuButton fovUp = MakeMenuButton("+ FOV", 0.60f, 0.58f, - glm::vec3(0.15f, 0.23f, 0.34f), 170.0f, 50.0f, 0.65f); - MenuButton hitboxToggle = MakeMenuButton("HITBOXES: OFF", 0.5f, 0.68f, - glm::vec3(0.20f, 0.22f, 0.28f), 340.0f, 50.0f, 0.65f); - MenuButton resetButton = MakeMenuButton("RESET DEFAULTS", 0.5f, 0.77f, - glm::vec3(0.20f, 0.22f, 0.28f), 340.0f, 52.0f, 0.65f); - MenuButton backButton = MakeMenuButton("BACK", 0.5f, 0.86f, - glm::vec3(0.12f, 0.30f, 0.22f), 340.0f, 52.0f, 0.85f); + auto MakeMenuButton = [&](const char* text, float x, float y, float width, float height, + float fontsize, glm::vec3 color, glm::vec3 hoverColor, + float opacity = 0.6f, bool compact = false) { + Box* box = new Box(); + box->Color = color; + box->t2d.position = { 0.0f, 0.5f, x, y }; + box->t2d.center = { 0.0f, 0.5f }; + box->t2d.size = { 0.0f, 0.0f, width, height }; + box->rounding = 0.15f; + box->BindToRenderSystem(); + menuItems.push_back(box); - TextBox* settingsInfo = new TextBox(); - settingsInfo->font = arial; - settingsInfo->fontsize = 0.65f; - settingsInfo->Color = { 0.85f, 0.90f, 0.97f }; - settingsInfo->t2d.position = { 0.5f, 0.37f, 0.0f, 0.0f }; - settingsInfo->t2d.center = { 0.5f, 0.5f }; - settingsInfo->t2d.size = { 0.8f, 0.0f, 0.0f, 90.0f }; - settingsInfo->textCenter = { 0.5f, 0.5f }; - settingsInfo->BindToRenderSystem(); - menuItems.push_back(settingsInfo); + Box* accent = new Box(); + accent->Color = menuAccent; + accent->t2d.position = { 0.0f, 0.5f, x, y }; + accent->t2d.center = { 0.0f, 0.5f }; + accent->t2d.size = { 0.0f, 0.0f, 0.0f, height }; + accent->BindToRenderSystem(); + menuItems.push_back(accent); + + // Compact buttons hold a single centered symbol, the rest are left aligned and slide on hover + const float padding = compact ? 0.0f : 20.0f; + TextBox* label = MakeMenuText(text, x + padding, y, width - padding * 2.0f, height, + fontsize, menuText, compact ? 0.5f : 0.0f); + + return MenuButton{ box, accent, label, color, hoverColor, opacity, + x + padding, compact ? 0.0f : 8.0f }; + }; + + const float mainButtonHeight = 60.0f; + MenuButton playButton = MakeMenuButton("PLAY", menuMargin, -60.0f, menuContentWidth, mainButtonHeight, 0.75f, + glm::vec3(0.82f, 0.45f, 0.10f), menuAccent, 0.95f); + MenuButton drawerButton = MakeMenuButton("TEXTURE DRAWER", menuMargin, 12.0f, menuContentWidth, mainButtonHeight, 0.75f, + menuButtonColor, menuButtonHover); + MenuButton settingsButton = MakeMenuButton("SETTINGS", menuMargin, 84.0f, menuContentWidth, mainButtonHeight, 0.75f, + menuButtonColor, menuButtonHover); + MenuButton quitButton = MakeMenuButton("QUIT", menuMargin, 156.0f, menuContentWidth, mainButtonHeight, 0.75f, + menuButtonColor, glm::vec3(0.42f, 0.14f, 0.16f)); + + // Settings rows: LABEL [-] VALUE [+] + const float stepperDownX = menuMargin + 176.0f; + const float stepperValueX = stepperDownX + 44.0f; + const float stepperUpX = menuMargin + menuContentWidth - 44.0f; + const float stepperValueWidth = stepperUpX - stepperValueX; + const float sensRowY = -120.0f; + const float fovRowY = -64.0f; + + TextBox* sensLabel = MakeMenuText("SENSITIVITY", menuMargin, sensRowY, 172.0f, 44.0f, 0.5f, menuMutedText); + TextBox* sensValue = MakeMenuText("", stepperValueX, sensRowY, stepperValueWidth, 44.0f, 0.5f, menuText, 0.5f); + TextBox* fovLabel = MakeMenuText("FIELD OF VIEW", menuMargin, fovRowY, 172.0f, 44.0f, 0.5f, menuMutedText); + TextBox* fovValue = MakeMenuText("", stepperValueX, fovRowY, stepperValueWidth, 44.0f, 0.5f, menuText, 0.5f); + + MenuButton sensitivityDown = MakeMenuButton("-", stepperDownX, sensRowY, 44.0f, 44.0f, 0.7f, + menuButtonColor, menuButtonHover, 0.6f, true); + MenuButton sensitivityUp = MakeMenuButton("+", stepperUpX, sensRowY, 44.0f, 44.0f, 0.7f, + menuButtonColor, menuButtonHover, 0.6f, true); + MenuButton fovDown = MakeMenuButton("-", stepperDownX, fovRowY, 44.0f, 44.0f, 0.7f, + menuButtonColor, menuButtonHover, 0.6f, true); + MenuButton fovUp = MakeMenuButton("+", stepperUpX, fovRowY, 44.0f, 44.0f, 0.7f, + menuButtonColor, menuButtonHover, 0.6f, true); + MenuButton hitboxToggle = MakeMenuButton("HITBOXES: OFF", menuMargin, -18.0f, menuContentWidth, 40.0f, 0.6f, + menuButtonColor, menuButtonHover); + MenuButton graphicsButton = MakeMenuButton("GRAPHICS", menuMargin, 26.0f, menuContentWidth, 40.0f, 0.6f, + menuButtonColor, menuButtonHover); + MenuButton resetButton = MakeMenuButton("RESET DEFAULTS", menuMargin, 70.0f, menuContentWidth, 40.0f, 0.6f, + menuButtonColor, menuButtonHover); + + MenuButton bloomToggle = MakeMenuButton("", menuMargin, -128.0f, menuContentWidth, 44.0f, 0.6f, + menuButtonColor, menuButtonHover); + MenuButton shadowToggle = MakeMenuButton("", menuMargin, -78.0f, menuContentWidth, 44.0f, 0.6f, + menuButtonColor, menuButtonHover); + MenuButton lightShadowToggle = MakeMenuButton("", menuMargin, -28.0f, menuContentWidth, 44.0f, 0.6f, + menuButtonColor, menuButtonHover); + MenuButton vsyncToggle = MakeMenuButton("", menuMargin, 22.0f, menuContentWidth, 44.0f, 0.6f, + menuButtonColor, menuButtonHover); + MenuButton cullingToggle = MakeMenuButton("", menuMargin, 72.0f, menuContentWidth, 44.0f, 0.6f, + menuButtonColor, menuButtonHover); + MenuButton backButton = MakeMenuButton("BACK", menuMargin, 120.0f, menuContentWidth, 50.0f, 0.6f, + glm::vec3(0.82f, 0.45f, 0.10f), menuAccent, 0.95f); + + // Texture drawer. Filled in further down, once the things it paints on exist. + struct DrawableSurface { + const char* name; + SurfaceTexture* surface; + glm::vec4* colorOverride; + bool hasCanvas = false; + }; + std::vector drawableSurfaces; + int selectedSurface = 0; + int selectedSwatch = 0; + TexturePainter painter; + bool painting = false; + + // The first edit swaps in a private copy, so painting never touches the shared image file. + // A colour override would hide the paint, so that goes too. + auto EnsureCanvas = [&](DrawableSurface& drawable) { + if (drawable.hasCanvas) + return; + + SurfaceTexture& surface = *drawable.surface; + surface.texture = surface.texture ? resourcemanager->CopyTexture(*surface.texture) + : resourcemanager->CreateTexture(256, 256); + drawable.colorOverride->w = 0.0f; + drawable.hasCanvas = true; + }; + + const float surfaceRowY = -120.0f; + const float swatchRowY = -64.0f; + const float brushRowY = -8.0f; + + MenuButton surfacePrevious = MakeMenuButton("<", menuMargin, surfaceRowY, 44.0f, 44.0f, 0.6f, + menuButtonColor, menuButtonHover, 0.6f, true); + MenuButton surfaceNext = MakeMenuButton(">", stepperUpX, surfaceRowY, 44.0f, 44.0f, 0.6f, + menuButtonColor, menuButtonHover, 0.6f, true); + TextBox* surfaceName = MakeMenuText("", menuMargin + 44.0f, surfaceRowY, menuContentWidth - 88.0f, 44.0f, 0.5f, menuText, 0.5f); + + const glm::vec3 swatchColors[] = { + { 0.0f, 0.0f, 0.0f }, { 1.0f, 1.0f, 1.0f }, { 0.90f, 0.15f, 0.15f }, + { 0.96f, 0.58f, 0.16f }, { 0.20f, 0.75f, 0.30f }, { 0.20f, 0.45f, 0.95f } + }; + const float swatchSize = 52.0f; + const float swatchSpacing = 60.0f; + + std::vector swatches; + swatches.reserve(std::size(swatchColors)); + for (int i = 0; i < (int)std::size(swatchColors); i++) { + swatches.push_back(MakeMenuButton("", menuMargin + swatchSpacing * i, swatchRowY, swatchSize, 44.0f, 0.5f, + swatchColors[i], glm::mix(swatchColors[i], glm::vec3(1.0f), 0.2f), 1.0f, true)); + } + + Box* swatchMarker = new Box(); + swatchMarker->Color = menuAccent; + swatchMarker->t2d.center = { 0.0f, 0.5f }; + swatchMarker->t2d.size = { 0.0f, 0.0f, swatchSize, 4.0f }; + swatchMarker->BindToRenderSystem(); + menuItems.push_back(swatchMarker); + + TextBox* brushLabel = MakeMenuText("BRUSH SIZE", menuMargin, brushRowY, 172.0f, 44.0f, 0.5f, menuMutedText); + TextBox* brushValue = MakeMenuText("", stepperValueX, brushRowY, stepperValueWidth, 44.0f, 0.5f, menuText, 0.5f); + MenuButton brushDown = MakeMenuButton("-", stepperDownX, brushRowY, 44.0f, 44.0f, 0.7f, + menuButtonColor, menuButtonHover, 0.6f, true); + MenuButton brushUp = MakeMenuButton("+", stepperUpX, brushRowY, 44.0f, 44.0f, 0.7f, + menuButtonColor, menuButtonHover, 0.6f, true); + MenuButton clearButton = MakeMenuButton("CLEAR", menuMargin, 52.0f, menuContentWidth, 50.0f, 0.6f, + menuButtonColor, menuButtonHover); + + // The canvas sits in the middle of the space right of the panel + const float canvasSize = 640.0f; + + Box* canvasBorder = new Box(); + canvasBorder->Color = menuAccent; + canvasBorder->t2d.position = { 0.5f, 0.5f, menuPanelWidth * 0.5f, 0.0f }; + canvasBorder->t2d.center = { 0.5f, 0.5f }; + canvasBorder->t2d.size = { 0.0f, 0.0f, canvasSize + 8.0f, canvasSize + 8.0f }; + canvasBorder->BindToRenderSystem(); + menuItems.push_back(canvasBorder); + + Box* canvasBlank = new Box(); // shows through until the surface has a texture + canvasBlank->t2d.position = canvasBorder->t2d.position; + canvasBlank->t2d.center = { 0.5f, 0.5f }; + canvasBlank->t2d.size = { 0.0f, 0.0f, canvasSize, canvasSize }; + canvasBlank->BindToRenderSystem(); + menuItems.push_back(canvasBlank); + + ImageBox* canvasImage = new ImageBox(); + canvasImage->t2d.position = canvasBorder->t2d.position; + canvasImage->t2d.center = { 0.5f, 0.5f }; + canvasImage->t2d.size = { 0.0f, 0.0f, canvasSize, canvasSize }; + canvasImage->BindToRenderSystem(); + menuItems.push_back(canvasImage); + + std::vector menuButtons = { + &playButton, &drawerButton, &settingsButton, &quitButton, + &sensitivityDown, &sensitivityUp, &fovDown, &fovUp, + &hitboxToggle, &graphicsButton, &resetButton, &backButton, + &bloomToggle, &shadowToggle, &lightShadowToggle, &vsyncToggle, &cullingToggle, + &surfacePrevious, &surfaceNext, &brushDown, &brushUp, &clearButton + }; + for (MenuButton& swatch : swatches) + menuButtons.push_back(&swatch); TextBox* menuHint = new TextBox(); menuHint->font = arial; - menuHint->text = "WASD TO MOVE | LEFT CLICK TO PUNCH | ESC FOR MENU"; - menuHint->fontsize = 0.5f; - menuHint->Color = { 0.55f, 0.61f, 0.70f }; - menuHint->t2d.position = { 0.5f, 0.92f, 0.0f, 0.0f }; - menuHint->t2d.center = { 0.5f, 0.5f }; - menuHint->t2d.size = { 0.9f, 0.0f, 0.0f, 40.0f }; - menuHint->textCenter = { 0.5f, 0.5f }; + menuHint->fontsize = 0.42f; + menuHint->Color = menuMutedText; + menuHint->t2d.position = { 0.0f, 1.0f, menuMargin, -28.0f }; + menuHint->t2d.center = { 0.0f, 1.0f }; + menuHint->t2d.size = { 0.0f, 0.0f, menuContentWidth, 110.0f }; + menuHint->textCenter = { 0.0f, 1.0f }; menuHint->BindToRenderSystem(); menuItems.push_back(menuHint); auto SetButtonVisible = [](MenuButton& button, bool visible) { - button.box->Opacity = visible ? 0.92f : 0.0f; + button.box->Opacity = visible ? button.opacity : 0.0f; + button.accent->Opacity = visible ? 1.0f : 0.0f; button.label->Opacity = visible ? 1.0f : 0.0f; }; @@ -371,38 +471,79 @@ void Engine::Initiate() const bool main = menuScreen == MenuScreen::Main; const bool paused = menuScreen == MenuScreen::Paused; const bool settings = menuScreen == MenuScreen::Settings; + const bool graphics = menuScreen == MenuScreen::Graphics; + const bool drawer = menuScreen == MenuScreen::Drawer; + const bool mainButtons = main || paused; - menuVeil->Opacity = 0.55f; + menuVeil->Opacity = 0.22f; + menuPanel->Opacity = 0.82f; + menuPanelEdge->Opacity = 1.0f; menuTitle->Opacity = 1.0f; menuSubtitle->Opacity = 1.0f; menuHint->Opacity = 1.0f; - menuSubtitle->text = main ? "MAIN MENU" : (paused ? "PAUSED" : "SETTINGS"); + menuSubtitle->text = main ? "MAIN MENU" : (paused ? "PAUSED" + : (drawer ? "TEXTURE DRAWER" : (graphics ? "GRAPHICS" : "SETTINGS"))); playButton.label->text = paused ? "RESUME" : "PLAY"; quitButton.label->text = paused ? "QUIT TO MENU" : "QUIT"; - menuHint->text = settings - ? "ESC TO GO BACK | ADJUST MOUSE, FOV, AND HITBOXES" - : "WASD TO MOVE | LEFT CLICK TO PUNCH | ESC FOR MENU"; + menuHint->text = drawer + ? "DRAG ON THE CANVAS TO DRAW\nESC TO GO BACK" + : (settings || graphics ? "ESC TO GO BACK" : "WASD TO MOVE\nESC FOR MENU"); + + SetButtonVisible(playButton, mainButtons); + SetButtonVisible(drawerButton, mainButtons); + SetButtonVisible(settingsButton, mainButtons); + SetButtonVisible(quitButton, mainButtons); + SetButtonVisible(surfacePrevious, drawer); + SetButtonVisible(surfaceNext, drawer); + SetButtonVisible(brushDown, drawer); + SetButtonVisible(brushUp, drawer); + SetButtonVisible(clearButton, drawer); + for (MenuButton& swatch : swatches) + SetButtonVisible(swatch, drawer); + + Texture* canvasTexture = drawableSurfaces[selectedSurface].surface->texture; + canvasImage->surface = SurfaceTexture::Stretched(canvasTexture); + canvasImage->Opacity = drawer && canvasTexture ? 1.0f : 0.0f; + canvasBlank->Opacity = drawer ? 1.0f : 0.0f; + canvasBorder->Opacity = drawer ? 1.0f : 0.0f; + surfaceName->Opacity = drawer ? 1.0f : 0.0f; + surfaceName->text = drawableSurfaces[selectedSurface].name; + brushLabel->Opacity = drawer ? 1.0f : 0.0f; + brushValue->Opacity = drawer ? 1.0f : 0.0f; + brushValue->text = std::to_string(painter.brushSize); + swatchMarker->Opacity = drawer ? 1.0f : 0.0f; + swatchMarker->t2d.position = { 0.0f, 0.5f, menuMargin + swatchSpacing * selectedSwatch, swatchRowY + 28.0f }; - SetButtonVisible(playButton, !settings); - SetButtonVisible(settingsButton, !settings); - SetButtonVisible(quitButton, !settings); SetButtonVisible(sensitivityDown, settings); SetButtonVisible(sensitivityUp, settings); SetButtonVisible(fovDown, settings); SetButtonVisible(fovUp, settings); SetButtonVisible(hitboxToggle, settings); + SetButtonVisible(graphicsButton, settings); SetButtonVisible(resetButton, settings); - SetButtonVisible(backButton, settings); - settingsInfo->Opacity = settings ? 1.0f : 0.0f; + SetButtonVisible(backButton, settings || drawer || graphics); + + auto ShowToggle = [&](MenuButton& button, const char* name, bool enabled) { + SetButtonVisible(button, graphics); + button.label->text = std::string(name) + (enabled ? ": ON" : ": OFF"); + button.color = enabled ? glm::vec3(0.82f, 0.45f, 0.10f) : menuButtonColor; + }; + ShowToggle(bloomToggle, "BLOOM", rendersystem->graphics.bloom); + ShowToggle(shadowToggle, "SUN SHADOWS", rendersystem->graphics.shadows); + ShowToggle(lightShadowToggle, "LIGHT SHADOWS", rendersystem->graphics.lightShadows); + ShowToggle(vsyncToggle, "VSYNC", rendersystem->graphics.vsync); + ShowToggle(cullingToggle, "CULLING", rendersystem->graphics.culling); + + sensLabel->Opacity = settings ? 1.0f : 0.0f; + sensValue->Opacity = settings ? 1.0f : 0.0f; + fovLabel->Opacity = settings ? 1.0f : 0.0f; + fovValue->Opacity = settings ? 1.0f : 0.0f; hitboxToggle.label->text = showHitboxes ? "HITBOXES: ON" : "HITBOXES: OFF"; - hitboxToggle.color = showHitboxes - ? glm::vec3(0.12f, 0.30f, 0.22f) - : glm::vec3(0.20f, 0.22f, 0.28f); + hitboxToggle.color = showHitboxes ? glm::vec3(0.82f, 0.45f, 0.10f) : menuButtonColor; - settingsInfo->text = "MOUSE SENSITIVITY: " + - std::to_string((int)std::lround(StartupCmPer360)) + - " CM / 360\nFIELD OF VIEW: " + std::to_string((int)std::lround(camera->FOV)); + sensValue->text = std::to_string((int)std::lround(StartupCmPer360)) + " CM"; + fovValue->text = std::to_string((int)std::lround(camera->FOV)); }; auto OpenMenu = [&](MenuScreen screen) { @@ -412,9 +553,6 @@ void Engine::Initiate() testcharacter.SetWalking(false); npc.SetWalking(false); fpsCounter->Opacity = 0.0f; - healthText->Opacity = 0.0f; - healthBarBack->Opacity = 0.0f; - healthBarFill->Opacity = 0.0f; camera->lockedcursor = false; glfwSetInputMode(window->handle, GLFW_CURSOR, GLFW_CURSOR_NORMAL); prevMouseDown = mouse->IsLeftDown(); @@ -425,9 +563,6 @@ void Engine::Initiate() charactermanager->ClearInput(); for (Box* item : menuItems) item->Opacity = 0.0f; fpsCounter->Opacity = 1.0f; - healthText->Opacity = 1.0f; - healthBarBack->Opacity = 0.85f; - healthBarFill->Opacity = 1.0f; camera->lockedcursor = true; glfwSetInputMode(window->handle, GLFW_CURSOR, GLFW_CURSOR_DISABLED); int ww, wh; @@ -436,9 +571,6 @@ void Engine::Initiate() }; fpsCounter->Opacity = 0.0f; - healthText->Opacity = 0.0f; - healthBarBack->Opacity = 0.0f; - healthBarFill->Opacity = 0.0f; camera->lockedcursor = false; glfwSetInputMode(window->handle, GLFW_CURSOR, GLFW_CURSOR_NORMAL); @@ -447,9 +579,9 @@ void Engine::Initiate() // ------------------------------------------------------------------------ Mesh* floor = CreateCubeMesh(); floor->name = "baseplate"; - floor->meshTexture = blanktex; + floor->surface = SurfaceTexture::Tiled(blanktex, 8.0f); floor->roughness = 0.9f; - floor->t.ScaleTo({ 32.0f, 2.0f, 32.0f }); + floor->t.ScaleTo({ 200.0f, 2.0f, 200.0f }); floor->t.TranslateTo({ 0.0f, -1.0f, 0.0f }); floor->BindToRenderSystem(); mainf->AddChild(floor); @@ -459,30 +591,36 @@ void Engine::Initiate() physicsengine->AddObject(floorBody); const BoundingBox floorBounds = floor->t.GetRotationlessAABB(); - const float wallHeight = 4.0f; - const float wallThickness = 1.0f; - const float wallOffset = 16.0f + wallThickness * 0.5f; - const float wallSpan = 32.0f + wallThickness; - - auto AddWall = [&](const char* name, const glm::vec3& position, const glm::vec3& scale) { - Mesh* wall = CreateCubeMesh(); - wall->name = name; - wall->meshTexture = blanktex; - wall->roughness = 0.9f; - wall->t.ScaleTo(scale); - wall->t.TranslateTo(position); - wall->BindToRenderSystem(); - mainf->AddChild(wall); - - p* wallBody = new p(wall); - wallBody->velocity = false; - physicsengine->AddObject(wallBody); + // ------------------------------------------------------------------------ + // Light cubes + // ------------------------------------------------------------------------ + auto AddLightCube = [&](const char* name, const glm::vec3& position, float size, const glm::vec3& color) -> Mesh* { + Mesh* cube = CreateCubeMesh(); + cube->name = name; + cube->colorOverride = glm::vec4(color, 1.0f); + cube->emissive = 1.0f; + cube->t.ScaleTo(size); + cube->t.TranslateTo(position); + cube->BindToRenderSystem(); + mainf->AddChild(cube); + return cube; }; - AddWall("wall_north", { 0.0f, wallHeight * 0.5f, -wallOffset }, { wallSpan, wallHeight, wallThickness }); - AddWall("wall_south", { 0.0f, wallHeight * 0.5f, wallOffset }, { wallSpan, wallHeight, wallThickness }); - AddWall("wall_west", { -wallOffset, wallHeight * 0.5f, 0.0f }, { wallThickness, wallHeight, 32.0f }); - AddWall("wall_east", { wallOffset, wallHeight * 0.5f, 0.0f }, { wallThickness, wallHeight, 32.0f }); + const glm::vec3 skyLightColor(0.45f, 0.65f, 1.0f); + const glm::vec3 skyLightPosition(-6.0f, 7.0f, -6.0f); + Mesh* skyLightCube = AddLightCube("spot_light_cube", skyLightPosition, 1.5f, skyLightColor); + rendersystem->lighting->AddSpotLight(skyLightPosition, { 0.0f, -1.0f, 0.0f }, skyLightColor, 1200.0f, 25.0f, 18.0f, 26.0f); + + const glm::vec3 pointLightPosition(6.0f, 3.0f, -6.0f); + const glm::vec3 pointLightColor(1.0f, 0.35f, 0.05f); + Mesh* pointLightCube = AddLightCube("point_light_cube", pointLightPosition, 0.8f, pointLightColor); + rendersystem->lighting->AddPointLight(pointLightPosition, pointLightColor, 300.0f, 30.0f); + + drawableSurfaces.push_back({ "BASEPLATE", &floor->surface, &floor->colorOverride }); + drawableSurfaces.push_back({ "PLAYER", &testcharacter.surface, &testcharacter.colorOverride }); + drawableSurfaces.push_back({ "DUMMY", &npc.surface, &npc.colorOverride }); + drawableSurfaces.push_back({ "SKY LIGHT CUBE", &skyLightCube->surface, &skyLightCube->colorOverride }); + drawableSurfaces.push_back({ "POINT LIGHT CUBE", &pointLightCube->surface, &pointLightCube->colorOverride }); // ------------------------------------------------------------------------ // Ground check + respawn @@ -515,10 +653,12 @@ void Engine::Initiate() if (showHitboxes) { for (Character* character : charactermanager->GetCharacters()) { if (!character->body) continue; + if (character->ragdoll) { + for (Ragdoll::Part* part : character->ragdoll->GetParts()) + lines.push_back({ part->transform.t.GetMatrix(), glm::vec3(0.2f, 0.9f, 0.35f) }); + continue; + } lines.push_back({ character->body->t.GetMatrix(), glm::vec3(0.2f, 0.9f, 0.35f) }); - const Hitbox& punch = character->GetPunchHitbox(); - if (punch.active) - lines.push_back({ punch.transform.GetMatrix(), glm::vec3(1.0f, 0.25f, 0.1f) }); } } rendersystem->SetHitboxDebugLines(lines); @@ -545,8 +685,10 @@ void Engine::Initiate() if (escapePressed) { if (!menuActive) OpenMenu(MenuScreen::Paused); - else if (menuScreen == MenuScreen::Settings) + else if (menuScreen == MenuScreen::Settings || menuScreen == MenuScreen::Drawer) menuScreen = settingsReturn; + else if (menuScreen == MenuScreen::Graphics) + menuScreen = MenuScreen::Settings; else if (menuScreen == MenuScreen::Paused) ResumeGame(); } @@ -561,9 +703,6 @@ void Engine::Initiate() float yoffset = mouse->GetLastY() - mouse->GetY(); camera->ProcessMouseMovement(xoffset, yoffset); } - else if (buffer.GetType() == Mouse::Event::Type::LPress) { - testcharacter.Punch(camera->t.GetFrontVector()); - } else if (buffer.GetType() == Mouse::Event::Type::RPress) { camera->lockedcursor = !camera->lockedcursor; if (camera->lockedcursor) { @@ -582,7 +721,8 @@ void Engine::Initiate() ApplyMenuLayout(); const bool liveBackdrop = menuScreen == MenuScreen::Main || - (menuScreen == MenuScreen::Settings && settingsReturn == MenuScreen::Main); + ((menuScreen == MenuScreen::Settings || menuScreen == MenuScreen::Graphics || menuScreen == MenuScreen::Drawer) + && settingsReturn == MenuScreen::Main); if (liveBackdrop) { static float menuAngle = 0.6f; menuAngle += deltatime * 0.12f; @@ -593,11 +733,7 @@ void Engine::Initiate() camera->t.RotateToQuaternion(LookAt(menuTarget - menuCamera)); camera->Step(deltatime); - npc.SetTurnDirection(testcharacter.body->t.GetTranslation() - - npc.body->t.GetTranslation()); charactermanager->Step(deltatime); - rain->t.TranslateTo(camera->t.GetTranslation() + glm::vec3(0.0f, 20.0f, 0.0f)); - rain->Step(deltatime); } soundsystem->SetListenerPosition( @@ -607,89 +743,146 @@ void Engine::Initiate() ); soundsystem->Update(); - MenuButton* buttons[] = { - &playButton, &settingsButton, &quitButton, - &sensitivityDown, &sensitivityUp, &fovDown, &fovUp, - &hitboxToggle, &resetButton, &backButton - }; - for (MenuButton* button : buttons) - button->box->t2d.Recalculate(); - const glm::vec2 mousePos((float)mouse->GetX(), (float)mouse->GetY()); - auto Hovered = [&](const MenuButton& button) { - return mousePos.x > button.box->t2d.pixelposition.x && - mousePos.y > button.box->t2d.pixelposition.y && - mousePos.x < button.box->t2d.pixelposition.x + button.box->t2d.pixelsize.x && - mousePos.y < button.box->t2d.pixelposition.y + button.box->t2d.pixelsize.y; - }; + const float hoverEase = 1.0f - std::exp(-12.0f * deltatime); - const bool playHovered = Hovered(playButton); - const bool settingsHovered = Hovered(settingsButton); - const bool quitHovered = Hovered(quitButton); - const bool sensDownHovered = Hovered(sensitivityDown); - const bool sensUpHovered = Hovered(sensitivityUp); - const bool fovDownHovered = Hovered(fovDown); - const bool fovUpHovered = Hovered(fovUp); - const bool hitboxHovered = Hovered(hitboxToggle); - const bool resetHovered = Hovered(resetButton); - const bool backHovered = Hovered(backButton); + for (MenuButton* button : menuButtons) { + t2d_package& t2d = button->box->t2d; + t2d.Recalculate(); + button->hovered = button->box->Opacity > 0.0f && + mousePos.x > t2d.pixelposition.x && + mousePos.y > t2d.pixelposition.y && + mousePos.x < t2d.pixelposition.x + t2d.pixelsize.x && + mousePos.y < t2d.pixelposition.y + t2d.pixelsize.y; - playButton.box->Color = playHovered ? glm::vec3(0.18f, 0.44f, 0.32f) : playButton.color; - settingsButton.box->Color = settingsHovered ? glm::vec3(0.22f, 0.34f, 0.50f) : settingsButton.color; - quitButton.box->Color = quitHovered ? glm::vec3(0.30f, 0.20f, 0.22f) : quitButton.color; - sensitivityDown.box->Color = sensDownHovered ? glm::vec3(0.22f, 0.34f, 0.50f) : sensitivityDown.color; - sensitivityUp.box->Color = sensUpHovered ? glm::vec3(0.22f, 0.34f, 0.50f) : sensitivityUp.color; - fovDown.box->Color = fovDownHovered ? glm::vec3(0.22f, 0.34f, 0.50f) : fovDown.color; - fovUp.box->Color = fovUpHovered ? glm::vec3(0.22f, 0.34f, 0.50f) : fovUp.color; - hitboxToggle.box->Color = hitboxHovered ? glm::vec3(0.22f, 0.34f, 0.50f) : hitboxToggle.color; - resetButton.box->Color = resetHovered ? glm::vec3(0.30f, 0.32f, 0.40f) : resetButton.color; - backButton.box->Color = backHovered ? glm::vec3(0.18f, 0.44f, 0.32f) : backButton.color; + button->hover += ((button->hovered ? 1.0f : 0.0f) - button->hover) * hoverEase; + button->box->Color = glm::mix(button->color, button->hoverColor, button->hover); + button->accent->t2d.size.z = 5.0f * button->hover; + button->label->t2d.position.z = button->labelX + button->labelSlide * button->hover; + } const bool mouseDown = mouse->IsLeftDown(); if (mouseDown && !prevMouseDown) { if (menuScreen == MenuScreen::Settings) { - if (sensDownHovered) { + if (sensitivityDown.hovered) { StartupCmPer360 = glm::clamp(StartupCmPer360 + 2.0f, 4.0f, 80.0f); camera->SetSensitivityCm(StartupCmPer360, StartupMouseDpi); } - else if (sensUpHovered) { + else if (sensitivityUp.hovered) { StartupCmPer360 = glm::clamp(StartupCmPer360 - 2.0f, 4.0f, 80.0f); camera->SetSensitivityCm(StartupCmPer360, StartupMouseDpi); } - else if (fovDownHovered) { + else if (fovDown.hovered) { camera->FOV = glm::clamp(camera->FOV - 5.0f, 60.0f, 130.0f); } - else if (fovUpHovered) { + else if (fovUp.hovered) { camera->FOV = glm::clamp(camera->FOV + 5.0f, 60.0f, 130.0f); } - else if (hitboxHovered) { + else if (hitboxToggle.hovered) { showHitboxes = !showHitboxes; } - else if (resetHovered) { + else if (graphicsButton.hovered) { + menuScreen = MenuScreen::Graphics; + } + else if (resetButton.hovered) { StartupCmPer360 = 16.0f; StartupMouseDpi = 800.0f; camera->SetSensitivityCm(StartupCmPer360, StartupMouseDpi); camera->FOV = 110.0f; showHitboxes = false; } - else if (backHovered) { + else if (backButton.hovered) { menuScreen = settingsReturn; } } - else if (playHovered) { + else if (menuScreen == MenuScreen::Graphics) { + GraphicsSettings& graphics = rendersystem->graphics; + if (bloomToggle.hovered) { + graphics.bloom = !graphics.bloom; + } + else if (shadowToggle.hovered) { + graphics.shadows = !graphics.shadows; + } + else if (lightShadowToggle.hovered) { + graphics.lightShadows = !graphics.lightShadows; + } + else if (vsyncToggle.hovered) { + graphics.vsync = !graphics.vsync; + glfwSwapInterval(graphics.vsync ? 1 : 0); + } + else if (cullingToggle.hovered) { + graphics.culling = !graphics.culling; + } + else if (backButton.hovered) { + menuScreen = MenuScreen::Settings; + } + } + else if (menuScreen == MenuScreen::Drawer) { + const int surfaceCount = (int)drawableSurfaces.size(); + if (surfacePrevious.hovered) { + selectedSurface = (selectedSurface + surfaceCount - 1) % surfaceCount; + } + else if (surfaceNext.hovered) { + selectedSurface = (selectedSurface + 1) % surfaceCount; + } + else if (brushDown.hovered) { + painter.brushSize = glm::max(painter.brushSize - 2, 2); + } + else if (brushUp.hovered) { + painter.brushSize = glm::min(painter.brushSize + 2, 64); + } + else if (clearButton.hovered) { + DrawableSurface& drawable = drawableSurfaces[selectedSurface]; + EnsureCanvas(drawable); + painter.Clear(*drawable.surface->texture); + } + else if (backButton.hovered) { + menuScreen = settingsReturn; + } + for (int i = 0; i < (int)swatches.size(); i++) { + if (swatches[i].hovered) { + selectedSwatch = i; + painter.color = swatchColors[i]; + } + } + } + else if (playButton.hovered) { ResumeGame(); } - else if (settingsHovered) { + else if (drawerButton.hovered) { + settingsReturn = menuScreen; + menuScreen = MenuScreen::Drawer; + } + else if (settingsButton.hovered) { settingsReturn = menuScreen; menuScreen = MenuScreen::Settings; } - else if (quitHovered) { + else if (quitButton.hovered) { if (menuScreen == MenuScreen::Main) glfwSetWindowShouldClose(window->handle, 1); else menuScreen = MenuScreen::Main; } } + // A stroke has to start on the canvas, but carries on if the mouse wanders off it + canvasImage->t2d.Recalculate(); + const glm::vec2 canvasUV = (mousePos - canvasImage->t2d.pixelposition) / canvasImage->t2d.pixelsize; + if (!mouseDown || menuScreen != MenuScreen::Drawer) { + painting = false; + } + else if (!prevMouseDown) { + painting = canvasUV.x >= 0.0f && canvasUV.x <= 1.0f && canvasUV.y >= 0.0f && canvasUV.y <= 1.0f; + } + + if (painting) { + DrawableSurface& drawable = drawableSurfaces[selectedSurface]; + EnsureCanvas(drawable); + painter.Stroke(*drawable.surface->texture, glm::clamp(canvasUV, glm::vec2(0.0f), glm::vec2(1.0f))); + } + else { + painter.EndStroke(); + } + prevMouseDown = mouseDown; UpdateHitboxDebug(); @@ -712,11 +905,6 @@ void Engine::Initiate() testcharacter.SetTurnDirection(inputDirection); testcharacter.SetWalking(hasMoveInput); - const glm::vec3 npcVelocity(npcBody->linearvelocity.x, 0.0f, npcBody->linearvelocity.z); - npc.SetWalking(glm::length(npcVelocity) > 0.1f); - npc.SetTurnDirection(testcharacter.body->t.GetTranslation() - - npc.body->t.GetTranslation()); - charactermanager->Step(deltatime); const glm::vec3 inputVel = hasMoveInput @@ -746,13 +934,12 @@ void Engine::Initiate() characterBody->linearvelocity.z += wishDir.z * add; } - charactermanager->ResolveCombat(*physicsengine); - { const int substeps = std::max(1, (int)std::ceil(deltatime * 120.0f)); const float subDt = deltatime / float(substeps); for (int i = 0; i < substeps; i++) physicsengine->Step(subDt); } + npc.ragdoll->Update(deltatime); if (testcharacter.body->t.GetTranslation().y < -20.0f) { testcharacter.body->t.TranslateTo(RandomSpawn()); @@ -784,6 +971,8 @@ void Engine::Initiate() for (p* obj : physicsengine->GetObjects()) { if (obj->pointer == testcharacter.body || !obj->collision) continue; + if (obj->collisionGroup != 0) // flailing ragdoll limbs would make the camera jump + continue; if (obj == floorBody && ignoreFloorCollision) continue; auto hit = IsRayInT(cameraSweep, obj->pointer->t); @@ -800,9 +989,6 @@ void Engine::Initiate() } camera->Step(deltatime); - rain->t.TranslateTo(camera->t.GetTranslation() + glm::vec3(0.0f, 20.0f, 0.0f)); - rain->Step(deltatime); - Sound* music = static_cast(mainf->GetFirstChildOfName("music")); if (music) music->Update3DPosition(0.0f, 0.0f, 0.0f); @@ -833,14 +1019,6 @@ void Engine::Initiate() fpsCounter->text = std::to_string(measuredFps) + " FPS"; } - healthText->text = "PLAYER " + std::to_string((int)std::lround(testcharacter.body->Health)) + - " HP DUMMY " + std::to_string((int)std::lround(npc.body->Health)) + " HP"; - - const float healthRatio = glm::clamp((float)testcharacter.body->Health / 100.0f, 0.0f, 1.0f); - healthBarFill->t2d.size.z = healthBarFillWidth * healthRatio; - healthBarFill->Color = glm::mix(glm::vec3(0.85f, 0.20f, 0.12f), - glm::vec3(0.20f, 0.85f, 0.35f), healthRatio); - // -------------------------------------------------------------------- // Render // -------------------------------------------------------------------- @@ -878,9 +1056,12 @@ void key_callback(GLFWwindow* window, int key, int scancode, int action, int mod const GLFWvidmode* mode = glfwGetVideoMode(monitor); glfwSetWindowMonitor(window, monitor, 0, 0, mode->width, mode->height, mode->refreshRate); } - glfwSwapInterval(1); + glfwSwapInterval(engine && engine->rendersystem && !engine->rendersystem->graphics.vsync ? 0 : 1); } + if (key == GLFW_KEY_H && action == GLFW_PRESS && engine && engine->rendersystem) + engine->rendersystem->CycleDebugView(); + if (action == GLFW_PRESS) keyboard->KeyDown(key); else if (action == GLFW_RELEASE) keyboard->KeyUp(key); diff --git a/Gui.h b/Gui.h index 819edf9..4945bee 100644 --- a/Gui.h +++ b/Gui.h @@ -8,6 +8,7 @@ #include "QuadVertices.h" #include "shaderClass.h" #include "texture.h" +#include "SurfaceTexture.h" #include "t2d.h" #include "font.h" #include "RenderSystem.h" @@ -190,7 +191,7 @@ public: class ImageBox : public Box { public: - Texture* tex = nullptr; + SurfaceTexture surface; ImageBox() { shadertype = ShaderType::ImageBoxShader; @@ -208,14 +209,7 @@ public: ShaderProgram.Set2F("pixelScale", t2d.pixelsize); ShaderProgram.Set2F("pixelPos", t2d.pixelposition); ShaderProgram.Set1F("rounding", rounding); - glActiveTexture(GL_TEXTURE0); - if (tex) { - glBindTexture(GL_TEXTURE_2D, tex->ID); - ShaderProgram.SetInt("tex", 0); - } - else { - glBindTexture(GL_TEXTURE_2D, 0); - } + surface.Apply(ShaderProgram, 0, glm::vec3(t2d.pixelsize, 0.0f)); boxVAO.Bind(); glDrawArrays(GL_TRIANGLES, 0, 6); boxVAO.Unbind(); diff --git a/Light.h b/Light.h new file mode 100644 index 0000000..db84a81 --- /dev/null +++ b/Light.h @@ -0,0 +1,48 @@ +#ifndef LIGHT_CLASS +#define LIGHT_CLASS + +#include + +// These have to match the sizes in deferred_lighting.frag and default.frag +static constexpr int MAX_DIRECTIONAL_LIGHTS = 4; +static constexpr int MAX_POINT_LIGHTS = 16; +static constexpr int MAX_SPOT_LIGHTS = 8; + +// Spot and point shadows share one depth texture cut into tiles. +// A spot light takes 1 tile, a point light takes 6. Lights past the limit just don't cast shadows. +static constexpr int SHADOW_ATLAS_COLUMNS = 6; // has to match the shaders too +static constexpr int SHADOW_ATLAS_TILES = SHADOW_ATLAS_COLUMNS * SHADOW_ATLAS_COLUMNS; +static constexpr int SHADOW_ATLAS_TILE_SIZE = 640; + +class Light { +public: + glm::vec3 color = glm::vec3(1.0f); // linear rgb + float intensity = 1.0f; + bool castShadow = true; // spot and point lights, the sun always has its own shadow map + int shadowTile = -1; // first atlas tile, set each frame by the render system +}; + +// Parallel rays with no position, so it lights the whole scene from one direction +class DirectionalLight : public Light { +public: + glm::vec3 direction = glm::vec3(0.0f, -1.0f, 0.0f); // the way the light travels +}; + +// Fades with distance and reaches nothing past radius +class PointLight : public Light { +public: + glm::vec3 position = glm::vec3(0.0f); + float radius = 10.0f; +}; + +// A cone from a point. Full brightness inside innerAngle, fading to nothing at outerAngle. +class SpotLight : public Light { +public: + glm::vec3 position = glm::vec3(0.0f); + glm::vec3 direction = glm::vec3(0.0f, -1.0f, 0.0f); // the way the cone points + float radius = 20.0f; + float innerAngle = 20.0f; // degrees from the center of the cone + float outerAngle = 30.0f; +}; + +#endif diff --git a/Lighting.h b/Lighting.h index d2e1089..fff00f8 100644 --- a/Lighting.h +++ b/Lighting.h @@ -1,21 +1,59 @@ #ifndef LIGHTING_CLASS #define LIGHTING_CLASS -#include "t.h" -#include "tFunctions.h" +#include +#include "Light.h" + +// Every light in the scene. Directional lights are the only ones without shadows, apart from the sun. class Lighting { public: - t dirlighting; + DirectionalLight sun; + std::vector directionalLights; + std::vector pointLights; + std::vector spotLights; + std::vector shadowTileMatrices; // world to light clip space, one per used atlas tile Lighting() { - dirlighting.RotateToQuaternion(glm::quat({ glm::radians(90.0f),0.0f,0.0f})); - dirlighting.TranslateTo({0.0f,0.0f,0.0f}); + sun.color = glm::vec3(1.0f, 0.97f, 0.92f); + sun.intensity = 3.0f; } + DirectionalLight* AddDirectionalLight(const glm::vec3& direction, const glm::vec3& color, float intensity) { + DirectionalLight* light = new DirectionalLight(); + light->direction = glm::normalize(direction); + light->color = color; + light->intensity = intensity; + directionalLights.push_back(light); + return light; + } + + PointLight* AddPointLight(const glm::vec3& position, const glm::vec3& color, float intensity, float radius) { + PointLight* light = new PointLight(); + light->position = position; + light->color = color; + light->intensity = intensity; + light->radius = radius; + pointLights.push_back(light); + return light; + } + + SpotLight* AddSpotLight(const glm::vec3& position, const glm::vec3& direction, const glm::vec3& color, + float intensity, float radius, float innerAngle, float outerAngle) { + SpotLight* light = new SpotLight(); + light->position = position; + light->direction = glm::normalize(direction); + light->color = color; + light->intensity = intensity; + light->radius = radius; + light->innerAngle = innerAngle; + light->outerAngle = outerAngle; + spotLights.push_back(light); + return light; + } private: - + }; -#endif \ No newline at end of file +#endif diff --git a/Mesh.h b/Mesh.h index b9c6577..5d8f538 100644 --- a/Mesh.h +++ b/Mesh.h @@ -25,6 +25,7 @@ #include "tFunctions.h" #include "Vertex.h" #include "texture.h" +#include "SurfaceTexture.h" #include "MeshData.h" struct IntersectionData { @@ -36,11 +37,12 @@ class Mesh : public Object , public Renderable, public t_package , public MeshDa friend class Model; protected: public: - Texture* meshTexture = nullptr; + SurfaceTexture surface; glm::vec4 colorOverride = glm::vec4(0.0f); // rgb + strength; 0 = use the texture's own colour float metallic = 0.0f; float roughness = 0.8f; float ao = 1.0f; + float emissive = 0.0f; // 0-1, how much of its own colour it shows with no light on it VAO vao; VBO vbo; @@ -461,26 +463,24 @@ public: } } + // Vertices are normalized into the unit cube, so t's box already holds the whole mesh + bool GetWorldBounds(BoundingBox& bounds) const override { + bounds = t.GetAABB(); + return true; + } + void Render(Shader& ShaderProgram) override { glm::mat4 topass = t.GetMatrix(); ShaderProgram.Activate(); - glActiveTexture(GL_TEXTURE1); - if (meshTexture != nullptr) { - glBindTexture(GL_TEXTURE_2D, meshTexture->ID); - ShaderProgram.SetInt("tex", 1); - ShaderProgram.SetInt("useTexture", 1); - } - else { - glBindTexture(GL_TEXTURE_2D, 0); - ShaderProgram.SetInt("useTexture", 0); - } + surface.Apply(ShaderProgram, 1, t.GetScale()); ShaderProgram.SetMat4("modl", topass); ShaderProgram.Set4F("colorOverride", colorOverride); ShaderProgram.Set1F("metallic", metallic); ShaderProgram.Set1F("roughness", roughness); ShaderProgram.Set1F("ao", ao); + ShaderProgram.Set1F("emissive", emissive); vao.Bind(); glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0); vao.Unbind(); diff --git a/Net.cpp b/Net.cpp deleted file mode 100644 index 769e098..0000000 --- a/Net.cpp +++ /dev/null @@ -1,626 +0,0 @@ -#define WIN32_LEAN_AND_MEAN -#define NOMINMAX -#include -#include -#pragma comment(lib, "ws2_32.lib") - -#include "Net.h" -#include -#include -#include -#include - -#ifndef SIO_UDP_CONNRESET -#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR, 12) -#endif - -namespace { - constexpr uint32_t NET_MAGIC = 0x45494946; // "EIIF" - constexpr uint16_t NET_VERSION = 3; - constexpr unsigned short NET_PORT = 27015; - constexpr double NET_TIMEOUT = 2.0; // seconds of silence before a player is dropped - - // One datagram = Header followed by 'count' Entry records. - // Client -> host: count 1 (the client's own state, id ignored). - // Host -> client: one entry per other player, host included (id 0). -#pragma pack(push, 1) - struct Header { - uint32_t magic; - uint16_t version; - uint32_t seq; - uint8_t count; - }; - struct Entry { - uint8_t id; - float pos[3]; - float rot[4]; // x, y, z, w - uint8_t sliding; - }; -#pragma pack(pop) - - constexpr uint32_t NET_MAGIC_BLAST = 0x45494942; // "EIIB" - constexpr int BLAST_COPIES = 4; // each blast is sent this many times - -#pragma pack(push, 1) - struct BlastPacket { - uint32_t magic; - uint16_t version; - uint8_t sender; - uint32_t id; - float center[3]; - float radius; - float power; - }; -#pragma pack(pop) - - constexpr uint32_t NET_MAGIC_HIT = 0x45494948; // "EIIH" - -#pragma pack(push, 1) - struct HitPacket { - uint32_t magic; - uint16_t version; - uint8_t sender; - uint8_t target; - uint32_t id; - int16_t damage; - float knock[3]; - }; -#pragma pack(pop) - - constexpr uint32_t NET_MAGIC_SPAWN = 0x45494953; // "EIIS" - constexpr uint32_t NET_MAGIC_CUBES = 0x45494943; // "EIIC" - constexpr int CUBES_PER_PACKET = 36; - -#pragma pack(push, 1) - struct SpawnPacket { - uint32_t magic; - uint16_t version; - uint8_t sender; - uint32_t id; - float pos[3]; - }; - struct CubesHeader { - uint32_t magic; - uint16_t version; - uint8_t count; - }; - struct CubeEntry { - uint32_t id; - float pos[3]; - float rot[4]; // x, y, z, w - }; -#pragma pack(pop) - - constexpr int MAX_ENTRIES = Lan::MaxPlayers; - constexpr int MAX_DATAGRAM = (int)sizeof(Header) + MAX_ENTRIES * (int)sizeof(Entry); - - double Now() { - using namespace std::chrono; - return duration(steady_clock::now().time_since_epoch()).count(); - } - - Entry MakeEntry(uint8_t id, const NetState& s) { - Entry e; - e.id = id; - e.pos[0] = s.position.x; e.pos[1] = s.position.y; e.pos[2] = s.position.z; - e.rot[0] = s.rotation.x; e.rot[1] = s.rotation.y; e.rot[2] = s.rotation.z; e.rot[3] = s.rotation.w; - e.sliding = s.sliding ? 1 : 0; - return e; - } - - NetState ReadEntry(const Entry& e) { - NetState s; - s.position = glm::vec3(e.pos[0], e.pos[1], e.pos[2]); - s.rotation = glm::quat(e.rot[3], e.rot[0], e.rot[1], e.rot[2]); // glm ctor order is w, x, y, z - s.sliding = e.sliding != 0; - return s; - } - - bool SameAddress(const sockaddr_in& a, const sockaddr_in& b) { - return a.sin_addr.s_addr == b.sin_addr.s_addr && a.sin_port == b.sin_port; - } -} - -struct Lan::Impl { - struct Peer { - sockaddr_in addr{}; - uint8_t id = 0; - double lastRecv = 0.0; - uint32_t lastSeq = 0; - bool started = false; // has sent at least one datagram (so lastSeq is valid) - NetState state; - bool hasState = false; - }; - - SOCKET sock = INVALID_SOCKET; - bool wsaStarted = false; - uint32_t sendSeq = 0; - - // Host - std::vector peers; - - // Client - sockaddr_in hostAddr{}; - bool anyRecv = false; - uint32_t lastSeq = 0; - double lastRecvTime = 0.0; - - // Blasts waiting for their remaining copies to go out - struct PendingBlast { BlastPacket pkt; int copiesLeft; }; - std::vector pendingBlasts; - uint32_t blastSeq = 0; - std::vector seenBlasts; // (type, sender, id) of recent blasts/hits, for de-duplication - - struct PendingSpawn { SpawnPacket pkt; int copiesLeft; }; - std::vector pendingSpawns; - uint32_t spawnSeq = 0; - size_t cubeCursor = 0; // host: where the next slice of cubes starts - - struct PendingHit { HitPacket pkt; int copiesLeft; }; - std::vector pendingHits; - uint32_t hitSeq = 0; - - void SendHitPacket(const sockaddr_in& to, const HitPacket& pkt) { - sendto(sock, (const char*)&pkt, (int)sizeof(pkt), 0, (const sockaddr*)&to, (int)sizeof(to)); - } - - // True the first time a (type, sender, id) triple is seen. type: 0 = blast, 1 = hit. - bool FirstSighting(uint8_t type, uint8_t sender, uint32_t id) { - const uint64_t key = ((uint64_t)type << 40) | ((uint64_t)sender << 32) | id; - if (std::find(seenBlasts.begin(), seenBlasts.end(), key) != seenBlasts.end()) return false; - seenBlasts.push_back(key); - if (seenBlasts.size() > 64) seenBlasts.erase(seenBlasts.begin()); - return true; - } - - void SendBlastPacket(const sockaddr_in& to, const BlastPacket& pkt) { - sendto(sock, (const char*)&pkt, (int)sizeof(pkt), 0, (const sockaddr*)&to, (int)sizeof(to)); - } - - void SendDatagram(const sockaddr_in& to, const std::vector& entries) { - char buf[MAX_DATAGRAM]; - Header h; - h.magic = NET_MAGIC; - h.version = NET_VERSION; - h.seq = ++sendSeq; - h.count = (uint8_t)entries.size(); - std::memcpy(buf, &h, sizeof(h)); - std::memcpy(buf + sizeof(h), entries.data(), entries.size() * sizeof(Entry)); - sendto(sock, buf, (int)(sizeof(h) + entries.size() * sizeof(Entry)), 0, - (const sockaddr*)&to, (int)sizeof(to)); - } -}; - -Lan::Lan() : impl(new Impl()) {} - -Lan::~Lan() { - Stop(); - delete impl; -} - -bool Lan::Start(Mode m, const std::string& hostIp) { - if (m == Mode::Off) return false; - - WSADATA wsa; - if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) { - std::cout << "[net] WSAStartup failed\n"; - return false; - } - impl->wsaStarted = true; - - impl->sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if (impl->sock == INVALID_SOCKET) { - std::cout << "[net] socket() failed: " << WSAGetLastError() << "\n"; - Stop(); - return false; - } - - u_long nonBlocking = 1; - ioctlsocket(impl->sock, FIONBIO, &nonBlocking); - - // Don't let an ICMP "port unreachable" (other side not up yet) kill recvfrom with WSAECONNRESET - BOOL newBehavior = FALSE; - DWORD bytes = 0; - WSAIoctl(impl->sock, SIO_UDP_CONNRESET, &newBehavior, sizeof(newBehavior), NULL, 0, &bytes, NULL, NULL); - - sockaddr_in local{}; - local.sin_family = AF_INET; - local.sin_addr.s_addr = htonl(INADDR_ANY); - local.sin_port = htons(m == Mode::Host ? NET_PORT : 0); // clients use any free port - if (bind(impl->sock, (sockaddr*)&local, sizeof(local)) == SOCKET_ERROR) { - std::cout << "[net] bind() failed: " << WSAGetLastError() - << " (is another copy already hosting on this PC?)\n"; - Stop(); - return false; - } - - if (m == Mode::Client) { - impl->hostAddr.sin_family = AF_INET; - impl->hostAddr.sin_port = htons(NET_PORT); - if (inet_pton(AF_INET, hostIp.c_str(), &impl->hostAddr.sin_addr) != 1) { - std::cout << "[net] '" << hostIp << "' is not a valid IPv4 address\n"; - Stop(); - return false; - } - } - - mode = m; - active = true; - return true; -} - -void Lan::Stop() { - if (impl->sock != INVALID_SOCKET) { - closesocket(impl->sock); - impl->sock = INVALID_SOCKET; - } - if (impl->wsaStarted) { - WSACleanup(); - impl->wsaStarted = false; - } - impl->peers.clear(); - impl->pendingBlasts.clear(); - impl->pendingHits.clear(); - impl->pendingSpawns.clear(); - impl->seenBlasts.clear(); - impl->anyRecv = false; - active = false; - mode = Mode::Off; -} - -void Lan::SendBlast(const NetBlast& b) { - if (!active) return; - Impl::PendingBlast pb; - pb.pkt.magic = NET_MAGIC_BLAST; - pb.pkt.version = NET_VERSION; - pb.pkt.sender = 0; // the host stamps the real id when relaying a client's blast - pb.pkt.id = ++impl->blastSeq; - pb.pkt.center[0] = b.center.x; pb.pkt.center[1] = b.center.y; pb.pkt.center[2] = b.center.z; - pb.pkt.radius = b.radius; - pb.pkt.power = b.power; - pb.copiesLeft = BLAST_COPIES; - impl->pendingBlasts.push_back(pb); -} - -void Lan::SendHit(uint8_t target, const NetHit& h) { - if (!active) return; - Impl::PendingHit ph; - ph.pkt.magic = NET_MAGIC_HIT; - ph.pkt.version = NET_VERSION; - ph.pkt.sender = 0; // the host stamps the real id when relaying a client's hit - ph.pkt.target = target; - ph.pkt.id = ++impl->hitSeq; - ph.pkt.damage = (int16_t)std::clamp(h.damage, 0, 1000); - ph.pkt.knock[0] = h.knockback.x; ph.pkt.knock[1] = h.knockback.y; ph.pkt.knock[2] = h.knockback.z; - ph.copiesLeft = BLAST_COPIES; - impl->pendingHits.push_back(ph); -} - -void Lan::SendCubeSpawn(const glm::vec3& position) { - if (!active || mode != Mode::Client) return; - Impl::PendingSpawn ps; - ps.pkt.magic = NET_MAGIC_SPAWN; - ps.pkt.version = NET_VERSION; - ps.pkt.sender = 0; // the host stamps the real id - ps.pkt.id = ++impl->spawnSeq; - ps.pkt.pos[0] = position.x; ps.pkt.pos[1] = position.y; ps.pkt.pos[2] = position.z; - ps.copiesLeft = BLAST_COPIES; - impl->pendingSpawns.push_back(ps); -} - -void Lan::SendCubes(const std::vector& cubes) { - if (!active || mode != Mode::Host || cubes.empty() || impl->peers.empty()) return; - - const size_t n = cubes.size(); - const size_t count = std::min(n, (size_t)CUBES_PER_PACKET); - if (impl->cubeCursor >= n) impl->cubeCursor = 0; - - char buf[sizeof(CubesHeader) + CUBES_PER_PACKET * sizeof(CubeEntry)]; - CubesHeader h; - h.magic = NET_MAGIC_CUBES; - h.version = NET_VERSION; - h.count = (uint8_t)count; - std::memcpy(buf, &h, sizeof(h)); - for (size_t i = 0; i < count; i++) { - const NetCube& c = cubes[(impl->cubeCursor + i) % n]; - CubeEntry e; - e.id = c.id; - e.pos[0] = c.position.x; e.pos[1] = c.position.y; e.pos[2] = c.position.z; - e.rot[0] = c.rotation.x; e.rot[1] = c.rotation.y; e.rot[2] = c.rotation.z; e.rot[3] = c.rotation.w; - std::memcpy(buf + sizeof(h) + i * sizeof(e), &e, sizeof(e)); - } - impl->cubeCursor = (impl->cubeCursor + count) % n; - - const int len = (int)(sizeof(h) + count * sizeof(CubeEntry)); - for (const Impl::Peer& to : impl->peers) - sendto(impl->sock, buf, len, 0, (const sockaddr*)&to.addr, (int)sizeof(to.addr)); -} - -void Lan::Send(const NetState& s) { - if (!active) return; - - // One more copy of every pending cube spawn request (client -> host) - for (Impl::PendingSpawn& ps : impl->pendingSpawns) { - sendto(impl->sock, (const char*)&ps.pkt, (int)sizeof(ps.pkt), 0, - (const sockaddr*)&impl->hostAddr, (int)sizeof(impl->hostAddr)); - ps.copiesLeft--; - } - impl->pendingSpawns.erase( - std::remove_if(impl->pendingSpawns.begin(), impl->pendingSpawns.end(), - [](const Impl::PendingSpawn& ps) { return ps.copiesLeft <= 0; }), - impl->pendingSpawns.end()); - - // One more copy of every pending hit: clients send to the host, the host to the target only - for (Impl::PendingHit& ph : impl->pendingHits) { - if (mode == Mode::Client) impl->SendHitPacket(impl->hostAddr, ph.pkt); - else for (const Impl::Peer& to : impl->peers) - if (to.id == ph.pkt.target) impl->SendHitPacket(to.addr, ph.pkt); - ph.copiesLeft--; - } - impl->pendingHits.erase( - std::remove_if(impl->pendingHits.begin(), impl->pendingHits.end(), - [](const Impl::PendingHit& ph) { return ph.copiesLeft <= 0; }), - impl->pendingHits.end()); - - // One more copy of every pending blast (to the host, or from the host to everyone) - for (Impl::PendingBlast& pb : impl->pendingBlasts) { - if (mode == Mode::Client) impl->SendBlastPacket(impl->hostAddr, pb.pkt); - else for (const Impl::Peer& to : impl->peers) impl->SendBlastPacket(to.addr, pb.pkt); - pb.copiesLeft--; - } - impl->pendingBlasts.erase( - std::remove_if(impl->pendingBlasts.begin(), impl->pendingBlasts.end(), - [](const Impl::PendingBlast& pb) { return pb.copiesLeft <= 0; }), - impl->pendingBlasts.end()); - - if (mode == Mode::Client) { - impl->SendDatagram(impl->hostAddr, { MakeEntry(0, s) }); - return; - } - - // Host: to each player, send the host's state plus every other player's latest state - for (const Impl::Peer& to : impl->peers) { - std::vector entries; - entries.push_back(MakeEntry(0, s)); - for (const Impl::Peer& other : impl->peers) { - if (other.id != to.id && other.hasState && (int)entries.size() < MAX_ENTRIES) - entries.push_back(MakeEntry(other.id, other.state)); - } - impl->SendDatagram(to.addr, entries); - } -} - -void Lan::Receive(std::vector& out, std::vector* blasts, std::vector* hits, - std::vector* spawns, std::vector* cubes) { - if (!active) return; - - char buf[1500]; - for (int i = 0; i < 512; i++) { // bounded so a flood can't stall the frame - sockaddr_in from{}; - int fromLen = (int)sizeof(from); - int r = recvfrom(impl->sock, buf, (int)sizeof(buf), 0, (sockaddr*)&from, &fromLen); - if (r == SOCKET_ERROR) { - if (WSAGetLastError() == WSAEWOULDBLOCK) break; - continue; - } - if (r < (int)sizeof(Header)) continue; - - uint32_t magic; - std::memcpy(&magic, buf, sizeof(magic)); - if (magic == NET_MAGIC_BLAST) { - if (r != (int)sizeof(BlastPacket)) continue; - BlastPacket bp; - std::memcpy(&bp, buf, sizeof(bp)); - if (bp.version != NET_VERSION) continue; - - if (mode == Mode::Host) { - // Only known players may blast. Stamp their id and relay to everyone else. - const Impl::Peer* sender = nullptr; - for (const Impl::Peer& pe : impl->peers) - if (SameAddress(pe.addr, from)) { sender = &pe; break; } - if (!sender) continue; - bp.sender = sender->id; - for (const Impl::Peer& to : impl->peers) - if (to.id != sender->id) impl->SendBlastPacket(to.addr, bp); - } - else if (!SameAddress(from, impl->hostAddr)) continue; // clients only listen to the host - - if (blasts && impl->FirstSighting(0, bp.sender, bp.id)) { - NetBlast nb; - nb.sender = bp.sender; - nb.id = bp.id; - nb.center = glm::vec3(bp.center[0], bp.center[1], bp.center[2]); - nb.radius = bp.radius; - nb.power = bp.power; - blasts->push_back(nb); - } - continue; - } - - if (magic == NET_MAGIC_SPAWN) { - if (mode != Mode::Host || r != (int)sizeof(SpawnPacket)) continue; - SpawnPacket sp; - std::memcpy(&sp, buf, sizeof(sp)); - if (sp.version != NET_VERSION) continue; - - // Only known players may spawn cubes - const Impl::Peer* sender = nullptr; - for (const Impl::Peer& pe : impl->peers) - if (SameAddress(pe.addr, from)) { sender = &pe; break; } - if (!sender) continue; - - if (spawns && impl->FirstSighting(2, sender->id, sp.id)) { - NetCubeRequest req; - req.sender = sender->id; - req.id = sp.id; - req.position = glm::vec3(sp.pos[0], sp.pos[1], sp.pos[2]); - spawns->push_back(req); - } - continue; - } - - if (magic == NET_MAGIC_CUBES) { - if (mode != Mode::Client || !SameAddress(from, impl->hostAddr)) continue; - CubesHeader ch; - if (r < (int)sizeof(ch)) continue; - std::memcpy(&ch, buf, sizeof(ch)); - if (ch.version != NET_VERSION || ch.count > CUBES_PER_PACKET) continue; - if (r != (int)(sizeof(ch) + ch.count * sizeof(CubeEntry))) continue; - - if (cubes) { - for (int k = 0; k < ch.count; k++) { - CubeEntry e; - std::memcpy(&e, buf + sizeof(ch) + k * sizeof(e), sizeof(e)); - NetCube c; - c.id = e.id; - c.position = glm::vec3(e.pos[0], e.pos[1], e.pos[2]); - c.rotation = glm::quat(e.rot[3], e.rot[0], e.rot[1], e.rot[2]); - cubes->push_back(c); - } - } - continue; - } - - if (magic == NET_MAGIC_HIT) { - if (r != (int)sizeof(HitPacket)) continue; - HitPacket hp; - std::memcpy(&hp, buf, sizeof(hp)); - if (hp.version != NET_VERSION) continue; - - bool forUs = true; - if (mode == Mode::Host) { - // Only known players may shoot. Stamp their id; deliver to us or forward to the target. - const Impl::Peer* sender = nullptr; - for (const Impl::Peer& pe : impl->peers) - if (SameAddress(pe.addr, from)) { sender = &pe; break; } - if (!sender) continue; - hp.sender = sender->id; - if (hp.target != 0) { - forUs = false; - for (const Impl::Peer& to : impl->peers) - if (to.id == hp.target) impl->SendHitPacket(to.addr, hp); - } - } - else if (!SameAddress(from, impl->hostAddr)) continue; // clients only listen to the host - - if (forUs && hits && impl->FirstSighting(1, hp.sender, hp.id)) { - NetHit nh; - nh.sender = hp.sender; - nh.id = hp.id; - nh.damage = hp.damage; - nh.knockback = glm::vec3(hp.knock[0], hp.knock[1], hp.knock[2]); - hits->push_back(nh); - } - continue; - } - - Header h; - std::memcpy(&h, buf, sizeof(h)); - if (h.magic != NET_MAGIC || h.version != NET_VERSION) continue; - if (h.count == 0 || h.count > MAX_ENTRIES) continue; - if (r != (int)(sizeof(Header) + h.count * sizeof(Entry))) continue; - - const double now = Now(); - - if (mode == Mode::Host) { - // Find this sender, or admit them as a new player - Impl::Peer* peer = nullptr; - for (Impl::Peer& p : impl->peers) - if (SameAddress(p.addr, from)) { peer = &p; break; } - - if (!peer) { - if ((int)impl->peers.size() >= MaxPlayers - 1) continue; // pit is full - uint8_t id = 1; - while (std::any_of(impl->peers.begin(), impl->peers.end(), - [&](const Impl::Peer& p) { return p.id == id; })) - id++; - Impl::Peer np; - np.addr = from; - np.id = id; - impl->peers.push_back(np); - peer = &impl->peers.back(); - std::cout << "[net] player " << (int)id << " joined\n"; - } - - if (peer->started && (int32_t)(h.seq - peer->lastSeq) <= 0) continue; // stale / duplicate - peer->started = true; - peer->lastSeq = h.seq; - peer->lastRecv = now; - - Entry e; - std::memcpy(&e, buf + sizeof(Header), sizeof(Entry)); - peer->state = ReadEntry(e); - peer->hasState = true; - - NetUpdate u; - u.id = peer->id; - u.state = peer->state; - out.push_back(u); - } - else { - if (!SameAddress(from, impl->hostAddr)) continue; // only listen to the host - - // After a long silence the host may have restarted (sequence back to 1): accept anything - if (impl->anyRecv && now - impl->lastRecvTime >= NET_TIMEOUT) impl->anyRecv = false; - if (impl->anyRecv && (int32_t)(h.seq - impl->lastSeq) <= 0) continue; - impl->anyRecv = true; - impl->lastSeq = h.seq; - impl->lastRecvTime = now; - - for (int k = 0; k < h.count; k++) { - Entry e; - std::memcpy(&e, buf + sizeof(Header) + k * sizeof(Entry), sizeof(Entry)); - NetUpdate u; - u.id = e.id; - u.state = ReadEntry(e); - out.push_back(u); - } - } - } - - // Host: forget players who stopped sending - if (mode == Mode::Host) { - const double now = Now(); - for (size_t i = 0; i < impl->peers.size();) { - if (now - impl->peers[i].lastRecv > NET_TIMEOUT) { - std::cout << "[net] player " << (int)impl->peers[i].id << " left\n"; - impl->peers.erase(impl->peers.begin() + i); - } - else i++; - } - } -} - -bool Lan::IsConnected() const { - if (!active) return false; - if (mode == Mode::Host) return !impl->peers.empty(); - return impl->anyRecv && (Now() - impl->lastRecvTime) < NET_TIMEOUT; -} - -int Lan::PeerCount() const { - return (int)impl->peers.size(); -} - -std::string Lan::LocalAddresses() { - WSADATA wsa; - if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) return ""; - - std::string result; - char name[256]; - if (gethostname(name, sizeof(name)) == 0) { - addrinfo hints{}; - hints.ai_family = AF_INET; - addrinfo* list = nullptr; - if (getaddrinfo(name, nullptr, &hints, &list) == 0) { - for (addrinfo* a = list; a; a = a->ai_next) { - char ip[INET_ADDRSTRLEN]; - const sockaddr_in* sa = (const sockaddr_in*)a->ai_addr; - if (inet_ntop(AF_INET, &sa->sin_addr, ip, sizeof(ip))) - result += std::string(result.empty() ? "" : ", ") + ip; - } - freeaddrinfo(list); - } - } - WSACleanup(); - return result; -} diff --git a/Net.h b/Net.h deleted file mode 100644 index e8d96cb..0000000 --- a/Net.h +++ /dev/null @@ -1,115 +0,0 @@ -#ifndef NET_CLASS -#define NET_CLASS - -#include -#include -#include -#include -#include - -// Multiplayer UDP link for a LAN pit. One player hosts (UDP port 27015), everyone else joins by IP. -// Clients send their own transform to the host every frame; the host relays everyone's -// transform to everyone else. The host is player id 0, joiners get ids 1, 2, 3, ... -// All Winsock code lives in Net.cpp so windows.h never leaks into the rest of the engine. - -struct NetState { - glm::vec3 position = glm::vec3(0.0f); - glm::quat rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); - bool sliding = false; -}; - -// A state received for some other player. -struct NetUpdate { - uint8_t id = 0; - NetState state; -}; - -// A shockwave explosion. Every machine applies the launch to its own player, since remote -// players are only puppets here. Blasts are re-sent a few times (UDP) and de-duplicated on receive. -struct NetBlast { - uint8_t sender = 0; // filled in by the host; ignored on send - uint32_t id = 0; // filled in by SendBlast - glm::vec3 center = glm::vec3(0.0f); - float radius = 0.0f; - float power = 0.0f; -}; - -// Damage dealt to another player. Each machine owns its own player's health, so the shooter -// aims locally and sends this to the victim, who applies it. Re-sent a few times and de-duplicated. -struct NetHit { - uint8_t sender = 0; // filled in by the host - uint32_t id = 0; // filled in by SendHit - int damage = 0; - glm::vec3 knockback = glm::vec3(0.0f); // added to the victim's velocity -}; - -// A physics cube owned by the host. The host simulates every cube and streams their poses to the -// clients, which only draw them. The host also decides when a cube despawns. -struct NetCube { - uint32_t id = 0; - glm::vec3 position = glm::vec3(0.0f); - glm::quat rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); -}; - -// A client asking the host to spawn a cube. Re-sent a few times (UDP) and de-duplicated by the host. -struct NetCubeRequest { - uint8_t sender = 0; // filled in by the host - uint32_t id = 0; - glm::vec3 position = glm::vec3(0.0f); -}; - -class Lan { -public: - enum class Mode { Off, Host, Client }; - - // Total players in the pit, host included. - static constexpr int MaxPlayers = 8; - - Lan(); - ~Lan(); - - // Opens the socket. For Client, hostIp is the host's IPv4 address. Returns false on failure. - bool Start(Mode mode, const std::string& hostIp); - void Stop(); - - Mode GetMode() const { return mode; } - bool IsActive() const { return active; } - - // Call once per frame with our own state. - void Send(const NetState& state); - // Drains all waiting packets and appends the other players' newest states to 'out'. - // If 'blasts' is given, new (de-duplicated) shockwave blasts from other players are appended to it. - // 'hits' gets damage aimed at us (de-duplicated). - // 'spawns' (host only) gets cube spawn requests from clients; 'cubes' (client only) gets the - // host's cube poses. - void Receive(std::vector& out, std::vector* blasts = nullptr, - std::vector* hits = nullptr, std::vector* spawns = nullptr, - std::vector* cubes = nullptr); - // Client: ask the host to spawn a cube at 'position'. Sent over the next few Send() calls. - void SendCubeSpawn(const glm::vec3& position); - // Host: stream cube poses to every client. Call once per frame; large sets go out in rotating - // slices, so a cube is refreshed every few frames. - void SendCubes(const std::vector& cubes); - // Damage another player. 'target' is the id from their NetUpdate (the host is 0). - void SendHit(uint8_t target, const NetHit& hit); - // Tell the other players about a blast we caused. Sent over the next few Send() calls. - void SendBlast(const NetBlast& blast); - - // Host: at least one player has joined. Client: the host has been heard from in the last 2 s. - bool IsConnected() const; - // Host only: number of joined players, not counting the host. - int PeerCount() const; - - // This machine's IPv4 addresses (for the host to read out to the joiners). - static std::string LocalAddresses(); - -private: - struct Impl; - Impl* impl; - Mode mode = Mode::Off; - bool active = false; -}; - -inline Lan* lan = nullptr; - -#endif diff --git a/Particle.h b/Particle.h index 7d54228..d27dce4 100644 --- a/Particle.h +++ b/Particle.h @@ -66,10 +66,12 @@ public: public: std::vector particles; - Texture* tex; + SurfaceTexture surface; Mesh* mesh; - + + unsigned int instanceBuffer = 0; + ParticleEmitter() { shadertype = ParticleShader; mesh = new Mesh(quadVertices3D, quadIndices3D); @@ -81,6 +83,7 @@ public: } ~ParticleEmitter() { + if (instanceBuffer != 0) glDeleteBuffers(1, &instanceBuffer); mesh->Delete(); } @@ -131,13 +134,8 @@ public: ShaderProgram.Set4F("color",color); - glActiveTexture(0); + surface.Apply(ShaderProgram, 0, size); - if (tex != nullptr) { - glBindTexture(GL_TEXTURE_2D, tex->ID); - ShaderProgram.SetInt("tex", 0); - } - else glBindTexture(GL_TEXTURE_2D, 0); std::vector matrices; if (color.w != 1.0f) { @@ -167,9 +165,8 @@ public: } } } - unsigned int buffer; - glGenBuffers(1, &buffer); - glBindBuffer(GL_ARRAY_BUFFER, buffer); + if (instanceBuffer == 0) glGenBuffers(1, &instanceBuffer); + glBindBuffer(GL_ARRAY_BUFFER, instanceBuffer); glBufferData(GL_ARRAY_BUFFER, particles.size() * sizeof(glm::mat4), &matrices[0], GL_STATIC_DRAW); mesh->vao.Bind(); diff --git a/Physics.cpp b/Physics.cpp index b185766..befde52 100644 --- a/Physics.cpp +++ b/Physics.cpp @@ -1,5 +1,8 @@ #include "Physics.h" +#include +#include + // ============================================================================ // ComputeInvInertiaTensor // Returns zero matrix for static or rotation-locked bodies. @@ -27,7 +30,8 @@ glm::mat3 Physics::ComputeInvInertiaTensor(const p* body) // ============================================================================ bool Physics::UpdateSleep(p* body, float dt) { - if (!body->velocity) return true; + if (!body->velocity || body->frozen) return true; + if (body->collisionGroup != 0) return false; // groups settle as a whole, a joint can't hold a sleeping body const float linSq = glm::dot(body->linearvelocity, body->linearvelocity); const float angSq = glm::dot(body->angularvelocity, body->angularvelocity); @@ -53,21 +57,27 @@ bool Physics::UpdateSleep(p* body, float dt) // ============================================================================ void Physics::Step(float dt) { + // The damping constants are per 1/60 s step; rescale so drag doesn't depend on frame rate + const float steps = dt * 60.0f; + const float linearDamping = std::pow(PHYSICS_LINEAR_DAMPING, steps); + const float angularDamping = std::pow(PHYSICS_ANGULAR_DAMPING, steps); + // Integrate forces ? velocities for (p* body : objects) { - if (!body->velocity || body->isSleeping) continue; + // Nothing rotates until the end of the step, so everything until then can share this + body->invInertia = ComputeInvInertiaTensor(body); + + if (!body->velocity || body->isSleeping || body->frozen) continue; body->force += body->mass * gravity; body->linearvelocity += (body->force / body->mass) * dt; if (!body->lockRotation) - body->angularvelocity += (ComputeInvInertiaTensor(body) * body->torque) * dt; + body->angularvelocity += (body->invInertia * body->torque) * dt; - // The damping constants are per 1/60 s step; rescale so drag doesn't depend on frame rate - const float steps = dt * 60.0f; - body->linearvelocity *= std::pow(PHYSICS_LINEAR_DAMPING, steps); - body->angularvelocity *= std::pow(PHYSICS_ANGULAR_DAMPING, steps); + body->linearvelocity *= linearDamping; + body->angularvelocity *= angularDamping; body->force = glm::vec3(0.0f); body->torque = glm::vec3(0.0f); @@ -80,17 +90,415 @@ void Physics::Step(float dt) if (!body->velocity) continue; if (UpdateSleep(body, dt)) continue; + if (body->maxSpeed > 0.0f && glm::length(body->linearvelocity) > body->maxSpeed) + body->linearvelocity = glm::normalize(body->linearvelocity) * body->maxSpeed; + if (body->maxAngularSpeed > 0.0f && glm::length(body->angularvelocity) > body->maxAngularSpeed) + body->angularvelocity = glm::normalize(body->angularvelocity) * body->maxAngularSpeed; + body->pointer->t.TranslateBy(body->linearvelocity * dt); - if (!body->lockRotation) + if (!body->lockRotation) { body->pointer->t.RotateByQuaternion(glm::quat(body->angularvelocity * dt)); + if (body->collisionMesh) body->pointer->t.NormalizeRotation(); + } } } +// ============================================================================ +// ResolveCollisions +// ============================================================================ +void Physics::ResolveCollisions(float dt) +{ + FindCollisions(); + for (Collision& col : collisions) + Resolve(&col); + + if (joints.empty()) return; + + // The first contact pass nudges positions, so these have to be worked out after it + for (BallJoint* joint : joints) { + DampJoint(*joint, dt); + PrepareJoint(*joint, dt); + } + + // Joints and contacts fight each other, so iterate them together or limbs get pulled through the floor + for (int i = 0; i < PHYSICS_JOINT_ITERATIONS; i++) { + for (BallJoint* joint : joints) + SolveJoint(*joint); + for (Collision& col : collisions) + Resolve(&col, false); + } +} + +// ============================================================================ +// FindCollisions +// Sort and sweep: once sorted along x, a body can only touch the ones after it +// that start before it ends. +// ============================================================================ +void Physics::FindCollisions() +{ + collisions.clear(); + broadPhase.clear(); + + for (p* body : objects) { + if (!body->collision) continue; + BroadPhaseEntry entry; + entry.body = body; + entry.bounds = body->pointer->t.GetAABB(); + broadPhase.push_back(entry); + } + + std::sort(broadPhase.begin(), broadPhase.end(), [](const BroadPhaseEntry& first, const BroadPhaseEntry& second) { + return first.bounds.min.x < second.bounds.min.x; + }); + + for (int i = 0; i < (int)broadPhase.size(); i++) { + const BroadPhaseEntry& first = broadPhase[i]; + p* a = first.body; + + for (int j = i + 1; j < (int)broadPhase.size(); j++) { + const BroadPhaseEntry& second = broadPhase[j]; + if (second.bounds.min.x >= first.bounds.max.x) break; + + p* b = second.body; + if (!a->velocity && !b->velocity) continue; + if (a->isSleeping && b->isSleeping) continue; + if ((a->frozen || !a->velocity) && (b->frozen || !b->velocity)) continue; + if (a->collisionGroup != 0 && a->collisionGroup == b->collisionGroup) continue; + if (!BoundingBoxInBoundingBox(first.bounds, second.bounds)) continue; + + if (a->collisionMesh || b->collisionMesh) { + CollideTriangles(a, b); + continue; + } + + std::optional e = TInT(a->pointer->t, b->pointer->t); + if (e.has_value()) { + Collision tp(a, b); + tp.POI = e.value().POI; + tp.CN = e.value().CN; + tp.overlap = e.value().overlap; + collisions.push_back(tp); + } + } + } +} + +// ============================================================================ +// CollideTriangles +// Tests every triangle of one body against the other's BVH. Triangles have no +// volume, so depth comes from how far the intersecting triangle reaches behind +// the other triangle's outward facing plane. +// ============================================================================ +static Triangle TransformTriangle(const Triangle& tri, const glm::mat4& matrix) +{ + return { + glm::vec3(matrix * glm::vec4(tri.a, 1.0f)), + glm::vec3(matrix * glm::vec4(tri.b, 1.0f)), + glm::vec3(matrix * glm::vec4(tri.c, 1.0f)) + }; +} + +// Keeps the deepest contact, then whichever contacts spread the rest out the most +static void KeepSpreadContacts(std::vector& contacts) +{ + if ((int)contacts.size() <= PHYSICS_MAX_MESH_CONTACTS) + return; + + int deepest = 0; + for (int i = 1; i < (int)contacts.size(); i++) { + if (contacts[i].overlap > contacts[deepest].overlap) + deepest = i; + } + std::swap(contacts[0], contacts[deepest]); + + for (int kept = 1; kept < PHYSICS_MAX_MESH_CONTACTS; kept++) { + int farthest = kept; + float farthestDistance = -1.0f; + for (int i = kept; i < (int)contacts.size(); i++) { + float nearest = FLT_MAX; + for (int j = 0; j < kept; j++) { + const glm::vec3 offset = contacts[i].POI - contacts[j].POI; + nearest = glm::min(nearest, glm::dot(offset, offset)); + } + if (nearest > farthestDistance) { + farthestDistance = nearest; + farthest = i; + } + } + std::swap(contacts[kept], contacts[farthest]); + } + + contacts.erase(contacts.begin() + PHYSICS_MAX_MESH_CONTACTS, contacts.end()); +} + +// Two meshes can tangle: a hand between two faces gets contacts pushing opposite ways, which cancel +// and pin it in place. So the whole pair shares one normal that always pushes them apart. +static void UnifyContactNormals(const p* a, const p* b, std::vector& contacts) +{ + glm::vec3 summed(0.0f); + float totalDepth = 0.0f; + for (const Collision& contact : contacts) { + summed += contact.CN * contact.overlap; + totalDepth += contact.overlap; + } + + // Faces that mostly disagree say nothing useful, so fall back to the line between the two bodies + const glm::vec3 apart = a->pointer->t.GetTranslation() - b->pointer->t.GetTranslation(); + glm::vec3 normal = summed; + if (glm::length(summed) < totalDepth * 0.3f) + normal = apart; + if (glm::length(normal) < 1e-6f) + return; + + normal = glm::normalize(normal); + if (glm::dot(normal, apart) < 0.0f) + normal = -normal; + + for (Collision& contact : contacts) { + contact.CN = normal; + contact.frictionScale = PHYSICS_MESH_PAIR_FRICTION; + } +} + +void Physics::CollideTriangles(p* a, p* b) +{ + const CollisionMesh& meshA = a->collisionMesh ? *a->collisionMesh : CollisionMesh::UnitBox(); + const CollisionMesh& meshB = b->collisionMesh ? *b->collisionMesh : CollisionMesh::UnitBox(); + + // Loop over the smaller mesh and search the larger one + const bool queryIsA = meshA.GetTriangles().size() <= meshB.GetTriangles().size(); + const CollisionMesh& queryMesh = queryIsA ? meshA : meshB; + const CollisionMesh& targetMesh = queryIsA ? meshB : meshA; + + const glm::mat4 queryToWorld = (queryIsA ? a : b)->pointer->t.GetMatrix(); + const glm::mat4 targetToWorld = (queryIsA ? b : a)->pointer->t.GetMatrix(); + const glm::mat4 queryToTarget = glm::inverse(targetToWorld) * queryToWorld; + + // Box faces are the reliable surface. Between two meshes, b's faces are used. + const bool surfaceIsA = !a->collisionMesh && b->collisionMesh; + const bool surfaceIsQuery = surfaceIsA == queryIsA; + + meshContacts.clear(); + + for (const Triangle& queryTriangle : queryMesh.GetTriangles()) { + const Triangle queryInTarget = TransformTriangle(queryTriangle, queryToTarget); + + meshCandidates.clear(); + targetMesh.Query(queryInTarget, meshCandidates); + + for (int index : meshCandidates) { + const Triangle& targetTriangle = targetMesh.GetTriangles()[index]; + if (!TriangleInTriangle(queryInTarget, targetTriangle)) + continue; + + const Triangle queryWorld = TransformTriangle(queryTriangle, queryToWorld); + const Triangle targetWorld = TransformTriangle(targetTriangle, targetToWorld); + const Triangle& surface = surfaceIsQuery ? queryWorld : targetWorld; + const Triangle& other = surfaceIsQuery ? targetWorld : queryWorld; + + const glm::vec3 faceNormal = glm::cross(surface.b - surface.a, surface.c - surface.a); + const float faceLength = glm::length(faceNormal); + if (faceLength < 1e-8f) + continue; + const glm::vec3 normal = faceNormal / faceLength; + + glm::vec3 deepestPoint = other.a; + float deepestDistance = glm::dot(other.a - surface.a, normal); + for (const glm::vec3& corner : { other.b, other.c }) { + const float distance = glm::dot(corner - surface.a, normal); + if (distance < deepestDistance) { + deepestDistance = distance; + deepestPoint = corner; + } + } + if (deepestDistance >= 0.0f) + continue; + + // CN points from b to a, the same way TInT does + Collision contact(a, b); + contact.POI = deepestPoint; + contact.CN = surfaceIsA ? -normal : normal; + contact.overlap = glm::min(-deepestDistance, PHYSICS_MAX_MESH_DEPTH); + meshContacts.push_back(contact); + } + } + + if (meshContacts.empty()) + return; + + if (a->frozen) WakeGroup(a->collisionGroup); + if (b->frozen) WakeGroup(b->collisionGroup); + + if (a->collisionMesh && b->collisionMesh) + UnifyContactNormals(a, b, meshContacts); + + KeepSpreadContacts(meshContacts); + for (Collision& contact : meshContacts) { + contact.correctionWeight = 1.0f / (float)meshContacts.size(); + collisions.push_back(contact); + } +} + +void Physics::WakeGroup(int group) +{ + for (p* body : objects) { + if (body->collisionGroup == group) + body->frozen = false; + } +} + +// ============================================================================ +// Joints +// Everything that only depends on where the bodies are gets worked out once in +// Prepare. Solve then just pushes velocities around, and runs many times a step. +// ============================================================================ +static glm::mat3 SkewMatrix(const glm::vec3& v) +{ + // Skew * x = cross(v, x). glm matrices are built from columns. + return glm::mat3( + glm::vec3(0.0f, v.z, -v.y), + glm::vec3(-v.z, 0.0f, v.x), + glm::vec3(v.y, -v.x, 0.0f)); +} + +// Asks for the child's spin relative to the parent around axis to close error +static void AddAngularRow(BallJoint& joint, const glm::vec3& axis, float error, float dt, bool oneSided) +{ + AngularRow& row = joint.rows[joint.rowCount++]; + row.axis = axis; + row.oneSided = oneSided; + row.targetSpeed = glm::clamp(error * (PHYSICS_JOINT_BIAS / dt), + -PHYSICS_JOINT_MAX_BIAS_SPEED, PHYSICS_JOINT_MAX_BIAS_SPEED); + row.inverseDenom = 1.0f / (glm::dot(axis, joint.parent->invInertia * axis) + glm::dot(axis, joint.child->invInertia * axis)); +} + +void Physics::PrepareJoint(BallJoint& joint, float dt) +{ + p* a = joint.parent; + p* b = joint.child; + if (a->frozen && b->frozen) + return; + + const glm::quat orientA = a->pointer->t.GetRotationQuaternion(); + const glm::quat orientB = b->pointer->t.GetRotationQuaternion(); + + const glm::vec3 rA = orientA * joint.anchorParent; + const glm::vec3 rB = orientB * joint.anchorChild; + joint.worldAnchorParent = rA; + joint.worldAnchorChild = rB; + + const glm::mat3 skewA = SkewMatrix(rA); + const glm::mat3 skewB = SkewMatrix(rB); + const glm::mat3 K = glm::mat3(a->GetInvMass() + b->GetInvMass()) + - skewA * a->invInertia * skewA - skewB * b->invInertia * skewB; + joint.inverseK = glm::inverse(K); + + const glm::vec3 drift = (b->pointer->t.GetTranslation() + rB) - (a->pointer->t.GetTranslation() + rA); + joint.bias = drift * (PHYSICS_JOINT_BIAS / dt); + if (glm::length(joint.bias) > PHYSICS_JOINT_MAX_BIAS_SPEED) + joint.bias = glm::normalize(joint.bias) * PHYSICS_JOINT_MAX_BIAS_SPEED; + + joint.rowCount = 0; + + // ---- Hinge ---- + if (joint.isHinge) { + const glm::vec3 hingeA = orientA * joint.hingeAxisParent; + const glm::vec3 hingeB = orientB * joint.hingeAxisChild; + + // Keep both hinge axes lined up, leaving only spin around the hinge free + const glm::vec3 misalignment = glm::cross(hingeB, hingeA); + const glm::vec3 side = glm::normalize(orientA * joint.hingeRefParent); + const glm::vec3 otherSide = glm::cross(hingeA, side); + AddAngularRow(joint, side, glm::dot(misalignment, side), dt, false); + AddAngularRow(joint, otherSide, glm::dot(misalignment, otherSide), dt, false); + + const glm::vec3 refA = side; + const glm::vec3 refB = orientB * joint.hingeRefChild; + const float bend = std::atan2(glm::dot(glm::cross(refA, refB), hingeA), glm::dot(refA, refB)); + + if (bend < joint.minBend) + AddAngularRow(joint, hingeA, joint.minBend - bend, dt, true); + else if (bend > joint.maxBend) + AddAngularRow(joint, -hingeA, bend - joint.maxBend, dt, true); + return; + } + + // ---- Swing cone ---- + const glm::vec3 axisA = orientA * joint.axisParent; + const glm::vec3 axisB = orientB * joint.axisChild; + const float angle = glm::acos(glm::clamp(glm::dot(axisA, axisB), -1.0f, 1.0f)); + if (angle > joint.maxAngle) { + const glm::vec3 swingAxis = glm::cross(axisB, axisA); + const float swingLength = glm::length(swingAxis); + if (swingLength > 1e-5f) + AddAngularRow(joint, swingAxis / swingLength, angle - joint.maxAngle, dt, true); + } + + // ---- Twist limit ---- + const glm::quat relative = glm::conjugate(orientA) * orientB; + const float twistAlongAxis = glm::dot(glm::vec3(relative.x, relative.y, relative.z), joint.axisChild); + float twist = 2.0f * std::atan2(twistAlongAxis, relative.w); + const float pi = glm::pi(); + if (twist > pi) twist -= 2.0f * pi; + else if (twist < -pi) twist += 2.0f * pi; + + const glm::vec3 twistAxis = orientA * joint.axisChild; + if (twist > joint.maxTwist) + AddAngularRow(joint, -twistAxis, twist - joint.maxTwist, dt, true); + else if (twist < -joint.maxTwist) + AddAngularRow(joint, twistAxis, -joint.maxTwist - twist, dt, true); +} + +void Physics::SolveJoint(BallJoint& joint) +{ + p* a = joint.parent; + p* b = joint.child; + if (a->frozen && b->frozen) + return; + + const glm::vec3& rA = joint.worldAnchorParent; + const glm::vec3& rB = joint.worldAnchorChild; + + const glm::vec3 relVel = b->linearvelocity + glm::cross(b->angularvelocity, rB) + - a->linearvelocity - glm::cross(a->angularvelocity, rA); + + const glm::vec3 impulse = joint.inverseK * (-relVel - joint.bias); + a->linearvelocity -= a->GetInvMass() * impulse; + a->angularvelocity -= a->invInertia * glm::cross(rA, impulse); + b->linearvelocity += b->GetInvMass() * impulse; + b->angularvelocity += b->invInertia * glm::cross(rB, impulse); + + for (int i = 0; i < joint.rowCount; i++) { + const AngularRow& row = joint.rows[i]; + const float speed = glm::dot(b->angularvelocity - a->angularvelocity, row.axis); + const float rowImpulse = (row.targetSpeed - speed) * row.inverseDenom; + if (row.oneSided && rowImpulse <= 0.0f) + continue; + + a->angularvelocity -= a->invInertia * row.axis * rowImpulse; + b->angularvelocity += b->invInertia * row.axis * rowImpulse; + } +} + +// Joints have no friction of their own, so without this limbs flail for a long time +void Physics::DampJoint(BallJoint& joint, float dt) +{ + p* a = joint.parent; + p* b = joint.child; + if (a->frozen && b->frozen) + return; + + const glm::vec3 relative = b->angularvelocity - a->angularvelocity; + const glm::vec3 removed = relative * (1.0f - std::exp(-PHYSICS_JOINT_DAMPING * dt)) * 0.5f; + a->angularvelocity += removed; + b->angularvelocity -= removed; +} + // ============================================================================ // Resolve // ============================================================================ -void Physics::Resolve(Collision* c) +void Physics::Resolve(Collision* c, bool firstPass) { p* a = c->ObjA; p* b = c->ObjB; @@ -102,8 +510,8 @@ void Physics::Resolve(Collision* c) const glm::vec3 rA = c->POI - a->pointer->t.GetTranslation(); const glm::vec3 rB = c->POI - b->pointer->t.GetTranslation(); - const glm::mat3 invIA = ComputeInvInertiaTensor(a); - const glm::mat3 invIB = ComputeInvInertiaTensor(b); + const glm::mat3& invIA = a->invInertia; + const glm::mat3& invIB = b->invInertia; const float invMassA = a->GetInvMass(); const float invMassB = b->GetInvMass(); @@ -115,7 +523,7 @@ void Physics::Resolve(Collision* c) const float relVelN = glm::dot(relVel, n); if (relVelN > 0.0f) return; - const float e = (glm::abs(relVelN) < PHYSICS_RESTITUTION_THRESHOLD) + const float e = (!firstPass || glm::abs(relVelN) < PHYSICS_RESTITUTION_THRESHOLD) ? 0.0f : glm::min(a->restitution, b->restitution); @@ -148,7 +556,7 @@ void Physics::Resolve(Collision* c) const float denomT = invMassA + invMassB + angDenom(invIA, rA, tangent) + angDenom(invIB, rB, tangent); float jT = -glm::dot(relVel, tangent) / denomT; - const float mu = (a->friction + b->friction) * 0.5f; + const float mu = (a->friction + b->friction) * 0.5f * c->frictionScale; jT = glm::clamp(jT, -mu * jN, mu * jN); const glm::vec3 impulseT = jT * tangent; @@ -165,10 +573,14 @@ void Physics::Resolve(Collision* c) } // ---- Baumgarte positional correction ---- + if (!firstPass) + return; + const float correction = glm::max(c->overlap - PHYSICS_BAUMGARTE_SLOP, 0.0f) / (invMassA + invMassB) - * PHYSICS_BAUMGARTE_FACTOR; + * PHYSICS_BAUMGARTE_FACTOR + * c->correctionWeight; const glm::vec3 corrVec = correction * n; if (a->velocity) a->pointer->t.TranslateBy(invMassA * corrVec); if (b->velocity) b->pointer->t.TranslateBy(-invMassB * corrVec); -} \ No newline at end of file +} diff --git a/Physics.h b/Physics.h index 26590c6..0b0e1d2 100644 --- a/Physics.h +++ b/Physics.h @@ -12,11 +12,13 @@ #include #include #include +#include #include "common.h" #include "Engine.h" #include "Camera.h" #include "t.h" #include "tFunctions.h" +#include "CollisionMesh.h" // ============================================================================ // Tuneable constants @@ -29,6 +31,13 @@ static constexpr float PHYSICS_SLEEP_LINEAR_SQ = 0.01f * 0.01f; static constexpr float PHYSICS_SLEEP_ANGULAR_SQ = 0.01f * 0.01f; static constexpr float PHYSICS_SLEEP_TIME_THRESHOLD = 0.5f; static constexpr float PHYSICS_RESTITUTION_THRESHOLD = 1.0f; +static constexpr int PHYSICS_JOINT_ITERATIONS = 12; +static constexpr float PHYSICS_JOINT_BIAS = 0.2f; +static constexpr float PHYSICS_JOINT_MAX_BIAS_SPEED = 5.0f; +static constexpr float PHYSICS_JOINT_DAMPING = 5.0f; +static constexpr int PHYSICS_MAX_MESH_CONTACTS = 4; +static constexpr float PHYSICS_MAX_MESH_DEPTH = 0.1f; +static constexpr float PHYSICS_MESH_PAIR_FRICTION = 0.25f; // two meshes catch on each other, so they slide easier // ============================================================================ // Physics body @@ -65,15 +74,23 @@ public: bool velocity = true; bool isSleeping = false; bool lockRotation = false; + bool frozen = false; // parked until something touches its group + + // Set this to collide triangle by triangle, otherwise the body is its t's box + const CollisionMesh* collisionMesh = nullptr; + int collisionGroup = 0; // non zero groups don't collide with themselves float mass = 1.0f; float restitution = 0.2f; float friction = 0.5f; + float maxSpeed = 0.0f; // 0 = no limit + float maxAngularSpeed = 0.0f; glm::vec3 linearvelocity = { 0.0f, 0.0f, 0.0f }; glm::vec3 angularvelocity = { 0.0f, 0.0f, 0.0f }; glm::vec3 force = { 0.0f, 0.0f, 0.0f }; glm::vec3 torque = { 0.0f, 0.0f, 0.0f }; + glm::mat3 invInertia = glm::mat3(0.0f); // world space, refreshed at the start of each step float sleepTimer = 0.0f; }; @@ -85,23 +102,87 @@ struct Collision { glm::vec3 POI; glm::vec3 CN; float overlap = 0.0f; + float correctionWeight = 1.0f; // shares positional correction between a pair's contacts + float frictionScale = 1.0f; p* ObjA = nullptr; p* ObjB = nullptr; Collision(p* a, p* b) : ObjA(a), ObjB(b) {} }; +// ============================================================================ +// One angular constraint on a joint, worked out once per step +// ============================================================================ +struct AngularRow { + glm::vec3 axis = glm::vec3(0.0f); + float targetSpeed = 0.0f; + float inverseDenom = 0.0f; + bool oneSided = false; // a limit only pushes back, the rest also hold the bodies together +}; + +// ============================================================================ +// Ball joint with a swing cone and a twist limit, or a one axis hinge +// Anchors and axes are local to each body: rotated, but not scaled. +// The twist limit assumes both bodies started out unrotated. +// ============================================================================ +struct BallJoint { + p* parent = nullptr; + p* child = nullptr; + glm::vec3 anchorParent = glm::vec3(0.0f); + glm::vec3 anchorChild = glm::vec3(0.0f); + glm::vec3 axisParent = glm::vec3(0.0f, 1.0f, 0.0f); + glm::vec3 axisChild = glm::vec3(0.0f, 1.0f, 0.0f); + float maxAngle = 0.0f; // radians between the two axes + float maxTwist = 0.0f; // radians either way around axisChild + + // Hinges ignore the limits above and bend around one axis, like an elbow. + // The bend is the angle from hingeRefParent to hingeRefChild around the hinge axis. + bool isHinge = false; + glm::vec3 hingeAxisParent = glm::vec3(1.0f, 0.0f, 0.0f); + glm::vec3 hingeAxisChild = glm::vec3(1.0f, 0.0f, 0.0f); + glm::vec3 hingeRefParent = glm::vec3(0.0f, 1.0f, 0.0f); + glm::vec3 hingeRefChild = glm::vec3(0.0f, 1.0f, 0.0f); + float minBend = 0.0f; + float maxBend = 0.0f; + + // Filled in by PrepareJoint, these can't change during a step's iterations + glm::vec3 worldAnchorParent = glm::vec3(0.0f); + glm::vec3 worldAnchorChild = glm::vec3(0.0f); + glm::vec3 bias = glm::vec3(0.0f); + glm::mat3 inverseK = glm::mat3(1.0f); + AngularRow rows[3]; + int rowCount = 0; +}; + +struct BroadPhaseEntry { + p* body = nullptr; + BoundingBox bounds; +}; + // ============================================================================ // Physics world // ============================================================================ class Physics { private: std::vector objects; + std::vector joints; glm::vec3 gravity = glm::vec3(0.0f, -9.81f, 0.0f); + // Kept between steps so they don't get reallocated + std::vector broadPhase; + std::vector collisions; + std::vector meshContacts; + std::vector meshCandidates; + static glm::mat3 ComputeInvInertiaTensor(const p* body); static bool UpdateSleep(p* body, float dt); - void Resolve(Collision* c); + void Resolve(Collision* c, bool firstPass = true); + void FindCollisions(); + void CollideTriangles(p* a, p* b); + void WakeGroup(int group); + void PrepareJoint(BallJoint& joint, float dt); + void SolveJoint(BallJoint& joint); + void DampJoint(BallJoint& joint, float dt); public: Physics() {} @@ -111,6 +192,7 @@ public: int GetObjectAmount() const noexcept { return (int)objects.size(); } void AddObject(p* obj) { objects.push_back(obj); } + void AddJoint(BallJoint* joint) { joints.push_back(joint); } const std::vector& GetObjects() const { return objects; } p* PPointerOfObject(t_package* pointer) const { @@ -135,37 +217,7 @@ public: void Step(float dt); - void SolveCollisions(std::vector& collisions) { - for (Collision& col : collisions) - Resolve(&col); - } - - void ResolveCollisions(float dt) { - std::vector collisions; - - for (p* a : objects) { - if (!a->collision) continue; - for (p* b : objects) { - if (b <= a || !b->collision) continue; - if (!a->velocity && !b->velocity) continue; - if (a->isSleeping && b->isSleeping) continue; - - if (TNearT(a->pointer->t, b->pointer->t) && - BoundingBoxInBoundingBox(a->pointer->t.GetAABB(), b->pointer->t.GetAABB())) - { - std::optional e = TInT(a->pointer->t, b->pointer->t); - if (e.has_value()) { - Collision tp(a, b); - tp.POI = e.value().POI; - tp.CN = e.value().CN; - tp.overlap = e.value().overlap; - collisions.push_back(tp); - } - } - } - } - SolveCollisions(collisions); - } + void ResolveCollisions(float dt); }; #endif \ No newline at end of file diff --git a/Ragdoll.cpp b/Ragdoll.cpp new file mode 100644 index 0000000..fd27d97 --- /dev/null +++ b/Ragdoll.cpp @@ -0,0 +1,329 @@ +#include "Ragdoll.h" + +#include +#include +#include + +#include "Animator.h" +#include "Character.h" +#include "CollisionMesh.h" +#include "Physics.h" + +struct PartDefinition { + const char* bone; + const char* endBone; // set for hinges (elbows, knees): the far end of the part, used to find the bend + float mass; + float maxSwing; // degrees the part can swing around the joint, in any direction + float coneTilt; // degrees the swing cone leans forward, so hips reach further forward than back + float maxTwist; // degrees the part can twist along its own length, either way + float minBend; // hinge range in degrees, 0 = straight + float maxBend; + float bendDirection; // 1 = folds forward, -1 = folds backward. Only used if the pose starts straight +}; + +// The hand, foot, finger and neck bones ride along with the nearest part above them. +// The root part has to come first, and parents have to come before their children. +static const PartDefinition partDefinitions[] = { + // bone endBone mass swing tilt twist bend range direction + { "Hip", nullptr, 14.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f }, + { "UpperTorso", nullptr, 22.0f, 30.0f, 0.0f, 25.0f, 0.0f, 0.0f, 0.0f }, + { "Head", nullptr, 5.0f, 45.0f, 0.0f, 60.0f, 0.0f, 0.0f, 0.0f }, + { "L_UpperArm", nullptr, 3.0f, 110.0f, 0.0f, 60.0f, 0.0f, 0.0f, 0.0f }, + { "L_LowerArm", "L_Hand", 2.5f, 0.0f, 0.0f, 0.0f, 0.0f, 150.0f, 1.0f }, + { "R_UpperArm", nullptr, 3.0f, 110.0f, 0.0f, 60.0f, 0.0f, 0.0f, 0.0f }, + { "R_LowerArm", "R_Hand", 2.5f, 0.0f, 0.0f, 0.0f, 0.0f, 150.0f, 1.0f }, + { "L_UpperLeg", nullptr, 9.0f, 65.0f, 35.0f, 30.0f, 0.0f, 0.0f, 0.0f }, + { "L_LowerLeg", "L_Foot", 6.0f, 0.0f, 0.0f, 0.0f, 0.0f, 140.0f, -1.0f }, + { "R_UpperLeg", nullptr, 9.0f, 65.0f, 35.0f, 30.0f, 0.0f, 0.0f, 0.0f }, + { "R_LowerLeg", "R_Foot", 6.0f, 0.0f, 0.0f, 0.0f, 0.0f, 140.0f, -1.0f }, +}; +static constexpr int PART_COUNT = sizeof(partDefinitions) / sizeof(partDefinitions[0]); + +static constexpr float PART_MIN_SIZE = 0.05f; +static constexpr float PART_MAX_SPEED = 30.0f; +static constexpr float PART_MAX_ANGULAR_SPEED = 30.0f; +static constexpr float SETTLE_SPEED = 0.2f; +static constexpr float SETTLE_ANGULAR_SPEED = 0.4f; +static constexpr float SETTLE_TIME = 0.6f; +static constexpr float FALL_LIMIT = -20.0f; +static constexpr float HINGE_MIN_POSE_BEND = 0.17f; // sin of 10 degrees, below this the pose counts as straight + +static const glm::quat IDENTITY_ROTATION(1.0f, 0.0f, 0.0f, 0.0f); + +// Walks the skeleton, handing every bone to the part it hangs from and recording each part's parent +static void AssignBones(const AssimpNodeData& node, int currentPart, const Rig& rig, + std::vector& boneParts, int (&partBones)[PART_COUNT], int (&partParents)[PART_COUNT]) +{ + const auto info = rig.m_BoneInfoMap.find(node.name); + + for (int i = 0; i < PART_COUNT; i++) { + if (node.name != partDefinitions[i].bone || info == rig.m_BoneInfoMap.end()) + continue; + partBones[i] = info->second.id; + partParents[i] = i == 0 ? -1 : currentPart; + currentPart = i; + break; + } + + if (info != rig.m_BoneInfoMap.end() && info->second.id >= 0 && info->second.id < (int)boneParts.size()) + boneParts[info->second.id] = currentPart; + + for (const AssimpNodeData& child : node.children) + AssignBones(child, currentPart, rig, boneParts, partBones, partParents); +} + +static glm::vec3 SkinVertex(const RigVertex& vertex, const std::vector& bones) +{ + glm::vec4 position(0.0f); + float totalWeight = 0.0f; + for (int i = 0; i < MAX_BONE_INFLUENCE; i++) { + const int bone = vertex.m_BoneIDs[i]; + if (bone < 0 || bone >= (int)bones.size()) + continue; + position += bones[bone] * glm::vec4(vertex.Position, 1.0f) * vertex.m_Weights[i]; + totalWeight += vertex.m_Weights[i]; + } + return totalWeight > 0.0f ? glm::vec3(position) / totalWeight : vertex.Position; +} + +Ragdoll::Ragdoll(CharacterBody& characterBody, const glm::mat4& rigMatrix, Physics& physics) +{ + static int nextGroup = 1; + group = nextGroup++; + + Rig* rig = characterBody.rig; + Animator* animator = characterBody.animator; + if (!rig || !animator || !animator->m_CurrentAnimation) { + std::cout << "RAGDOLL NEEDS A RIG AND AN ANIMATION\n"; + return; + } + + // Bone matrices aren't filled in until the animator has stepped once + characterBody.Step(0.001f); + const std::vector poseBones = characterBody.GetBlendedBoneMatrices(); + + int partBones[PART_COUNT]; + int partParents[PART_COUNT]; + for (int i = 0; i < PART_COUNT; i++) { + partBones[i] = -1; + partParents[i] = -1; + } + boneParts.assign(poseBones.size(), 0); + AssignBones(animator->m_CurrentAnimation->GetRootNode(), 0, *rig, boneParts, partBones, partParents); + + restBones.resize(poseBones.size()); + for (int i = 0; i < (int)poseBones.size(); i++) + restBones[i] = rigMatrix * poseBones[i]; + + // Each triangle goes to whichever part most of its skin weight belongs to + std::vector partTriangles[PART_COUNT]; + for (RigMesh* mesh : rig->meshes) { + for (int i = 0; i + 2 < (int)mesh->indices.size(); i += 3) { + const RigVertex* corners[3] = { + &mesh->vertices[mesh->indices[i]], + &mesh->vertices[mesh->indices[i + 1]], + &mesh->vertices[mesh->indices[i + 2]] + }; + + float partWeights[PART_COUNT] = {}; + for (const RigVertex* corner : corners) { + for (int j = 0; j < MAX_BONE_INFLUENCE; j++) { + const int bone = corner->m_BoneIDs[j]; + if (bone >= 0 && bone < (int)boneParts.size()) + partWeights[boneParts[bone]] += corner->m_Weights[j]; + } + } + int bestPart = 0; + for (int j = 1; j < PART_COUNT; j++) { + if (partWeights[j] > partWeights[bestPart]) + bestPart = j; + } + + partTriangles[bestPart].push_back({ + glm::vec3(rigMatrix * glm::vec4(SkinVertex(*corners[0], poseBones), 1.0f)), + glm::vec3(rigMatrix * glm::vec4(SkinVertex(*corners[1], poseBones), 1.0f)), + glm::vec3(rigMatrix * glm::vec4(SkinVertex(*corners[2], poseBones), 1.0f)) + }); + } + } + + // Parts start axis aligned around their triangles, in unit space like every other body + for (int i = 0; i < PART_COUNT; i++) { + glm::vec3 boundsMin(FLT_MAX); + glm::vec3 boundsMax(-FLT_MAX); + for (const Triangle& tri : partTriangles[i]) { + boundsMin = glm::min(boundsMin, glm::min(tri.a, glm::min(tri.b, tri.c))); + boundsMax = glm::max(boundsMax, glm::max(tri.a, glm::max(tri.b, tri.c))); + } + if (partTriangles[i].empty()) { + const int bone = partBones[i] >= 0 ? partBones[i] : 0; + boundsMin = boundsMax = glm::vec3(restBones[bone][3]); + } + + const glm::vec3 center = (boundsMin + boundsMax) * 0.5f; + const glm::vec3 size = glm::max(boundsMax - boundsMin, glm::vec3(PART_MIN_SIZE)); + + std::vector unitTriangles; + unitTriangles.reserve(partTriangles[i].size()); + for (const Triangle& tri : partTriangles[i]) + unitTriangles.push_back({ (tri.a - center) / size, (tri.b - center) / size, (tri.c - center) / size }); + + Part* part = new Part(); + part->restPosition = center; + part->transform.t.TranslateTo(center); + part->transform.t.ScaleTo(size); + part->transform.t.RotateToQuaternion(IDENTITY_ROTATION); + part->mesh = new CollisionMesh(unitTriangles); + + part->body = new p(&part->transform); + part->body->mass = partDefinitions[i].mass; + part->body->restitution = 0.0f; + part->body->friction = 0.6f; + part->body->collisionMesh = part->mesh; + part->body->collisionGroup = group; + part->body->maxSpeed = PART_MAX_SPEED; + part->body->maxAngularSpeed = PART_MAX_ANGULAR_SPEED; + physics.AddObject(part->body); + + parts.push_back(part); + restInverses.push_back(glm::inverse(part->transform.t.GetTranslationMatrix())); + } + + auto BonePosition = [&](const char* name, const glm::vec3& fallback) { + const auto info = rig->m_BoneInfoMap.find(name); + if (info == rig->m_BoneInfoMap.end()) + return fallback; + return glm::vec3(restBones[info->second.id] * glm::inverse(info->second.offset) * glm::vec4(0.0f, 0.0f, 0.0f, 1.0f)); + }; + + // The rig faces its local -Z, same as Character::GetForwardDirection + const glm::vec3 forward = glm::normalize(glm::vec3(rigMatrix * glm::vec4(0.0f, 0.0f, -1.0f, 0.0f))); + + // Joints sit on the child bone's origin. Everything starts unrotated, so local = world here. + glm::vec3 jointAnchors[PART_COUNT]; + jointAnchors[0] = parts[0]->restPosition; + + for (int i = 1; i < PART_COUNT; i++) { + const PartDefinition& definition = partDefinitions[i]; + if (partBones[i] < 0 || partParents[i] < 0) { + std::cout << "RAGDOLL BONE NOT FOUND: " << definition.bone << "\n"; + jointAnchors[i] = parts[i]->restPosition; + continue; + } + + const glm::vec3 anchor = BonePosition(definition.bone, parts[i]->restPosition); + jointAnchors[i] = anchor; + + Part* parent = parts[partParents[i]]; + Part* child = parts[i]; + glm::vec3 axis = child->restPosition - anchor; + axis = glm::length(axis) > 0.0001f ? glm::normalize(axis) : glm::vec3(0.0f, -1.0f, 0.0f); + + // Leaning only the parent's axis moves the cone without moving the part + glm::vec3 coneAxis = axis; + const glm::vec3 forwardOfAxis = forward - axis * glm::dot(forward, axis); + if (glm::length(forwardOfAxis) > 0.001f) { + const float tilt = glm::radians(definition.coneTilt); + coneAxis = glm::normalize(axis * std::cos(tilt) + glm::normalize(forwardOfAxis) * std::sin(tilt)); + } + + BallJoint* limits = new BallJoint(); + limits->parent = parent->body; + limits->child = child->body; + limits->anchorParent = anchor - parent->restPosition; + limits->anchorChild = anchor - child->restPosition; + limits->axisParent = coneAxis; + limits->axisChild = axis; + limits->maxAngle = glm::radians(definition.maxSwing); + limits->maxTwist = glm::radians(definition.maxTwist); + + if (definition.endBone) { + const glm::vec3 upperDirection = glm::normalize(anchor - jointAnchors[partParents[i]]); + const glm::vec3 lowerDirection = glm::normalize(BonePosition(definition.endBone, child->restPosition) - anchor); + + // A bent pose shows the real hinge. A straight one has to go by which way the limb should fold. + glm::vec3 hinge = glm::cross(upperDirection, lowerDirection); + if (glm::length(hinge) < HINGE_MIN_POSE_BEND) + hinge = glm::cross(upperDirection, forward * definition.bendDirection); + hinge = glm::length(hinge) > 0.001f ? glm::normalize(hinge) : glm::vec3(1.0f, 0.0f, 0.0f); + + limits->isHinge = true; + limits->hingeAxisParent = hinge; + limits->hingeAxisChild = hinge; + limits->hingeRefParent = glm::normalize(upperDirection - hinge * glm::dot(upperDirection, hinge)); + limits->hingeRefChild = glm::normalize(lowerDirection - hinge * glm::dot(lowerDirection, hinge)); + limits->minBend = glm::radians(definition.minBend); + limits->maxBend = glm::radians(definition.maxBend); + } + + physics.AddJoint(limits); + } + + // A nudge so it topples instead of balancing perfectly on its feet + parts[1]->body->linearvelocity = glm::vec3(0.8f, 0.0f, 0.4f); +} + +void Ragdoll::Update(float dt) +{ + if (parts.empty()) + return; + + if (parts[0]->transform.t.GetTranslationY() < FALL_LIMIT) { + Reset(); + return; + } + + if (parts[0]->body->frozen) + return; + + bool settled = true; + for (Part* part : parts) { + if (glm::length(part->body->linearvelocity) > SETTLE_SPEED || + glm::length(part->body->angularvelocity) > SETTLE_ANGULAR_SPEED) { + settled = false; + break; + } + } + + settleTimer = settled ? settleTimer + dt : 0.0f; + if (settleTimer < SETTLE_TIME) + return; + + // Freezing saves the triangle tests and stops resting contacts jittering. Contact wakes it again. + settleTimer = 0.0f; + for (Part* part : parts) { + part->body->frozen = true; + part->body->linearvelocity = glm::vec3(0.0f); + part->body->angularvelocity = glm::vec3(0.0f); + } +} + +void Ragdoll::Reset() +{ + settleTimer = 0.0f; + for (Part* part : parts) { + part->transform.t.TranslateTo(part->restPosition); + part->transform.t.RotateToQuaternion(IDENTITY_ROTATION); + part->body->linearvelocity = glm::vec3(0.0f); + part->body->angularvelocity = glm::vec3(0.0f); + part->body->frozen = false; + } +} + +std::vector Ragdoll::GetBoneMatrices() const +{ + std::vector bones = restBones; + if (parts.empty()) + return bones; + + std::vector partMoves; + partMoves.reserve(parts.size()); + for (int i = 0; i < (int)parts.size(); i++) { + const t& transform = parts[i]->transform.t; + partMoves.push_back(transform.GetTranslationMatrix() * transform.GetRotationMatrix() * restInverses[i]); + } + + for (int i = 0; i < (int)bones.size(); i++) + bones[i] = partMoves[boneParts[i]] * restBones[i]; + return bones; +} diff --git a/Ragdoll.h b/Ragdoll.h new file mode 100644 index 0000000..9e03be3 --- /dev/null +++ b/Ragdoll.h @@ -0,0 +1,48 @@ +#ifndef RAGDOLL_CLASS +#define RAGDOLL_CLASS + +#include + +#include + +#include "t.h" + +class CharacterBody; +class CollisionMesh; +class Physics; +class p; + +// Splits a character's rig into rigid bodies joined by ball joints. +// Each part collides using the rig triangles skinned mostly to its bones. +class Ragdoll { +public: + struct Part { + t_package transform; // scale is the part's bounding box, orientation starts at identity + p* body = nullptr; + CollisionMesh* mesh = nullptr; + glm::vec3 restPosition = glm::vec3(0.0f); + }; + + // rigMatrix is the model matrix the rig was being drawn with + Ragdoll(CharacterBody& characterBody, const glm::mat4& rigMatrix, Physics& physics); + + // Call once a frame after the physics steps + void Update(float dt); + + // Bone matrices in world space, draw the rig with an identity model matrix + std::vector GetBoneMatrices() const; + + const std::vector& GetParts() const { return parts; } + +private: + void Reset(); + + std::vector parts; + std::vector boneParts; // part index of each bone + std::vector restBones; // world space bone matrices at the moment of going limp + std::vector restInverses; // inverse of each part's starting transform + int group = 0; + float settleTimer = 0.0f; +}; + +#endif diff --git a/RenderSystem.cpp b/RenderSystem.cpp index 29ca9ad..1533e68 100644 --- a/RenderSystem.cpp +++ b/RenderSystem.cpp @@ -30,11 +30,54 @@ static const float hitboxLineVertices[] = { static void ConfigurePBR(Shader* shader, Lighting* lighting) { - shader->Set3F("dirLight.direction", lighting->dirlighting.GetFrontVector()); - shader->Set3F("dirLight.color", glm::vec3(1.0f, 0.97f, 0.92f)); - shader->Set1F("dirLight.intensity", 3.0f); + shader->Set3F("dirLight.direction", lighting->sun.direction); + shader->Set3F("dirLight.color", lighting->sun.color); + shader->Set1F("dirLight.intensity", lighting->sun.intensity); shader->Set1F("envIntensity", 0.8f); shader->SetInt("environmentMap", 2); + + const int directionalCount = glm::min((int)lighting->directionalLights.size(), MAX_DIRECTIONAL_LIGHTS); + shader->SetInt("directionalLightCount", directionalCount); + for (int i = 0; i < directionalCount; i++) { + const DirectionalLight* light = lighting->directionalLights[i]; + const std::string name = "directionalLights[" + std::to_string(i) + "]."; + shader->Set3F((name + "direction").c_str(), light->direction); + shader->Set3F((name + "color").c_str(), light->color); + shader->Set1F((name + "intensity").c_str(), light->intensity); + } + + const int pointCount = glm::min((int)lighting->pointLights.size(), MAX_POINT_LIGHTS); + shader->SetInt("pointLightCount", pointCount); + for (int i = 0; i < pointCount; i++) { + const PointLight* light = lighting->pointLights[i]; + const std::string name = "pointLights[" + std::to_string(i) + "]."; + shader->Set3F((name + "position").c_str(), light->position); + shader->Set3F((name + "color").c_str(), light->color); + shader->Set1F((name + "intensity").c_str(), light->intensity); + shader->Set1F((name + "radius").c_str(), light->radius); + shader->SetInt((name + "shadowTile").c_str(), light->shadowTile); + } + + shader->SetInt("localShadowMap", 10); + if (!lighting->shadowTileMatrices.empty()) { + shader->SetMat4Array("shadowTileMatrices", lighting->shadowTileMatrices.data(), + (int)lighting->shadowTileMatrices.size()); + } + + const int spotCount = glm::min((int)lighting->spotLights.size(), MAX_SPOT_LIGHTS); + shader->SetInt("spotLightCount", spotCount); + for (int i = 0; i < spotCount; i++) { + const SpotLight* light = lighting->spotLights[i]; + const std::string name = "spotLights[" + std::to_string(i) + "]."; + shader->Set3F((name + "position").c_str(), light->position); + shader->Set3F((name + "direction").c_str(), light->direction); + shader->Set3F((name + "color").c_str(), light->color); + shader->Set1F((name + "intensity").c_str(), light->intensity); + shader->Set1F((name + "radius").c_str(), light->radius); + shader->Set1F((name + "innerCos").c_str(), std::cos(glm::radians(light->innerAngle))); + shader->Set1F((name + "outerCos").c_str(), std::cos(glm::radians(light->outerAngle))); + shader->SetInt((name + "shadowTile").c_str(), light->shadowTile); + } } // ============================================================================ @@ -97,7 +140,7 @@ void RenderSystem::Initialize() screenfbo->Bind(); glGenTextures(1, &textureColorbuffer); glBindTexture(GL_TEXTURE_2D, textureColorbuffer); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, window->width, window->height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, window->width, window->height, 0, GL_RGBA, GL_FLOAT, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glBindTexture(GL_TEXTURE_2D, 0); @@ -112,6 +155,50 @@ void RenderSystem::Initialize() std::cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!\n"; screenfbo->Unbind(); + // G-buffer + gBuffer = new FBO(); + gBuffer->Bind(); + unsigned int* gTextures[] = { &gPosition, &gNormal, &gAlbedo, &gMaterial, &gDepth }; + for (unsigned int* texture : gTextures) { + glGenTextures(1, texture); + glBindTexture(GL_TEXTURE_2D, *texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + } + AllocateGBuffer(window->width, window->height); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, gPosition, 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, gNormal, 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, GL_TEXTURE_2D, gAlbedo, 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT3, GL_TEXTURE_2D, gMaterial, 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, gDepth, 0); + const GLenum gDrawBuffers[] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2, GL_COLOR_ATTACHMENT3 }; + glDrawBuffers(4, gDrawBuffers); + + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) + std::cout << "ERROR::FRAMEBUFFER:: G-buffer is not complete!\n"; + gBuffer->Unbind(); + + // Bloom framebuffers, the blur bounces between these two + for (int i = 0; i < 2; i++) { + bloomFBO[i] = new FBO(); + glGenTextures(1, &bloomTextures[i]); + glBindTexture(GL_TEXTURE_2D, bloomTextures[i]); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + } + AllocateBloom(window->width, window->height); + for (int i = 0; i < 2; i++) { + bloomFBO[i]->Bind(); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, bloomTextures[i], 0); + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) + std::cout << "ERROR::FRAMEBUFFER:: Bloom framebuffer is not complete!\n"; + bloomFBO[i]->Unbind(); + } + // Shadow framebuffer shadowdepthmapfbo = new FBO(); SHADOW_RESOLUTION = 4096; @@ -134,16 +221,88 @@ void RenderSystem::Initialize() glReadBuffer(GL_NONE); glBindFramebuffer(GL_FRAMEBUFFER, 0); + // Spot and point light shadow atlas. Unit 10 stays reserved for it, so it's only bound here. + const int shadowAtlasSize = SHADOW_ATLAS_COLUMNS * SHADOW_ATLAS_TILE_SIZE; + localShadowFBO = new FBO(); + glGenTextures(1, &localShadowAtlas); + glActiveTexture(GL_TEXTURE10); + glBindTexture(GL_TEXTURE_2D, localShadowAtlas); + glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, shadowAtlasSize, shadowAtlasSize, 0, + GL_DEPTH_COMPONENT, GL_FLOAT, NULL); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL); + glActiveTexture(GL_TEXTURE0); + + localShadowFBO->Bind(); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, localShadowAtlas, 0); + glDrawBuffer(GL_NONE); + glReadBuffer(GL_NONE); + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) + std::cout << "ERROR::FRAMEBUFFER:: Shadow atlas is not complete!\n"; + localShadowFBO->Unbind(); + + // The shadow map uses compare mode, which can't be read as plain depth without this + glGenSamplers(1, &shadowDebugSampler); + glSamplerParameteri(shadowDebugSampler, GL_TEXTURE_COMPARE_MODE, GL_NONE); + glSamplerParameteri(shadowDebugSampler, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glSamplerParameteri(shadowDebugSampler, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + lighting = new Lighting(); } +void RenderSystem::AllocateGBuffer(int width, int height) +{ + glBindTexture(GL_TEXTURE_2D, gPosition); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, NULL); + glBindTexture(GL_TEXTURE_2D, gNormal); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, width, height, 0, GL_RGBA, GL_FLOAT, NULL); + glBindTexture(GL_TEXTURE_2D, gAlbedo); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, width, height, 0, GL_RGBA, GL_FLOAT, NULL); + glBindTexture(GL_TEXTURE_2D, gMaterial); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); + // Same format as the screen rbo, the depth blit between them requires it + glBindTexture(GL_TEXTURE_2D, gDepth); + glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, width, height, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, NULL); + glBindTexture(GL_TEXTURE_2D, 0); +} + +// Quarter resolution, the blur reaches much further for the same number of samples +void RenderSystem::AllocateBloom(int width, int height) +{ + bloomWidth = glm::max(width / 4, 1); + bloomHeight = glm::max(height / 4, 1); + for (unsigned int texture : bloomTextures) { + glBindTexture(GL_TEXTURE_2D, texture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, bloomWidth, bloomHeight, 0, GL_RGBA, GL_FLOAT, NULL); + } + glBindTexture(GL_TEXTURE_2D, 0); +} + +void RenderSystem::CycleDebugView() +{ + static const char* const viewNames[] = { "FINAL", "POSITION", "NORMAL", "ALBEDO", "MATERIAL", "DEPTH", "SHADOW MAP", "BLOOM", "SHADOW ATLAS" }; + debugView = (debugView + 1) % 9; + std::cout << "DEBUG VIEW: " << viewNames[debugView] << "\n"; +} + void RenderSystem::Resize(int width, int height) { - if (!screenfbo || textureColorbuffer == 0 || rbo == 0 || width <= 0 || height <= 0) + if (!screenfbo || !gBuffer || !bloomFBO[0] || textureColorbuffer == 0 || rbo == 0 || width <= 0 || height <= 0) return; + AllocateBloom(width, height); + AllocateGBuffer(width, height); + gBuffer->Bind(); + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) + std::cout << "ERROR::FRAMEBUFFER:: G-buffer is not complete after resize!\n"; + gBuffer->Unbind(); + glBindTexture(GL_TEXTURE_2D, textureColorbuffer); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, width, height, 0, GL_RGBA, GL_FLOAT, NULL); glBindTexture(GL_TEXTURE_2D, 0); glBindRenderbuffer(GL_RENDERBUFFER, rbo); @@ -226,7 +385,7 @@ void RenderSystem::RenderTranslucent(Camera& camera, const glm::mat4& lightSpace // ============================================================================ void RenderSystem::Render(Camera& camera) { - lighting->dirlighting.RotateByEulerAngles({ glm::radians(0.05f), 0.0f, 0.0f }); + lighting->sun.direction = glm::normalize(glm::quat(glm::vec3(glm::radians(0.05f), 0.0f, 0.0f)) * lighting->sun.direction); const glm::mat4 proj = camera.GetProjectionMatrix(0.05f, 2000.0f); const glm::mat4 view = camera.GetViewMatrix(); @@ -235,7 +394,7 @@ void RenderSystem::Render(Camera& camera) const float near_plane = 0.1f, far_plane = 100.0f; const glm::mat4 lightSpaceMatrix = glm::ortho(-32.0f, 32.0f, -32.0f, 32.0f, near_plane, far_plane) - * glm::lookAt(camPos - lighting->dirlighting.GetFrontVector() * 10.0f, + * glm::lookAt(camPos - lighting->sun.direction * 10.0f, camPos, camera.t.GetUpVector()); // ---- Shadow pass ---- @@ -245,32 +404,29 @@ void RenderSystem::Render(Camera& camera) shadowdepthmapfbo->Bind(); glClear(GL_DEPTH_BUFFER_BIT); - MeshShadowShader->Activate(); - MeshShadowShader->SetMat4("lightSpaceMatrix", lightSpaceMatrix); - for (Renderable* r : renderables) { - if (!r->castshadow || r->shadertype != Renderable::MeshShader) continue; - r->Render(*MeshShadowShader); - } - - RigShadowShader->Activate(); - RigShadowShader->SetMat4("lightSpaceMatrix", lightSpaceMatrix); - for (Renderable* r : renderables) { - if (!r->castshadow || r->shadertype != Renderable::RigShader) continue; - r->Render(*RigShadowShader); - } + if (graphics.shadows) + RenderShadowCasters(lightSpaceMatrix); // with nothing drawn, the cleared map leaves everything lit + RenderLocalShadows(); glCullFace(GL_BACK); - // ---- Main scene ---- - screenfbo->Bind(); - glViewport(0, 0, window->width, window->height); - glEnable(GL_DEPTH_TEST); - glClearColor(212.f / 255.f, 223.f / 255.f, 232.f / 255.f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + // ---- Geometry pass ---- + RenderGeometryPass(proj, view, camPos); - // Skybox + // ---- Main scene ---- + // Translucents and hitboxes are still forward, they depth test against the G-buffer's depth + glBindFramebuffer(GL_READ_FRAMEBUFFER, gBuffer->ID); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, screenfbo->ID); + glBlitFramebuffer(0, 0, window->width, window->height, 0, 0, window->width, window->height, + GL_DEPTH_BUFFER_BIT, GL_NEAREST); + screenfbo->Bind(); + glClearColor(212.f / 255.f, 223.f / 255.f, 232.f / 255.f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + + // Skybox, depth tested so nothing covered by geometry gets shaded glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); - glDisable(GL_DEPTH_TEST); + glDepthFunc(GL_LEQUAL); + glDepthMask(GL_FALSE); SkyboxShader->Activate(); SkyboxShader->SetMat4("proj", proj); SkyboxShader->SetMat4("view", glm::mat4(glm::mat3(view))); @@ -279,28 +435,14 @@ void RenderSystem::Render(Camera& camera) skybox->Bind(); glDrawArrays(GL_TRIANGLES, 0, 36); glBindVertexArray(0); + glDepthMask(GL_TRUE); + glDepthFunc(GL_LESS); + + // ---- Lighting pass ---- + glDisable(GL_DEPTH_TEST); + RenderLightingPass(camPos, lightSpaceMatrix); glEnable(GL_DEPTH_TEST); - // ---- Opaque meshes ---- - PrepareMeshShader(camera); - MeshShader->SetMat4("lightSpaceMatrix", lightSpaceMatrix); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, depthMaptexture); - for (Renderable* r : renderables) { - if (r->shadertype == Renderable::MeshShader && r->opaque) - r->Render(*MeshShader); - } - - // ---- Opaque rigs ---- - PrepareRigShader(camera); - RigShader->SetMat4("lightSpaceMatrix", lightSpaceMatrix); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, depthMaptexture); - for (Renderable* r : renderables) { - if (r->shadertype == Renderable::RigShader && r->opaque) - r->Render(*RigShader); - } - // ---- Translucent pass (meshes + particles + rigs, sorted back-to-front) ---- // Disable face culling so thin translucent quads render both sides glDisable(GL_CULL_FACE); @@ -309,17 +451,33 @@ void RenderSystem::Render(Camera& camera) RenderHitboxDebug(camera); - // ---- Framebuffer blit to screen ---- - screenfbo->Unbind(); + // ---- Bloom ---- glDisable(GL_DEPTH_TEST); + if (graphics.bloom) + RenderBloom(); + else + screenfbo->Unbind(); + + // ---- Framebuffer blit to screen ---- + glViewport(0, 0, window->width, window->height); glClearColor(0.0f, 0.0f, 0.0f, 0.0f); glClear(GL_COLOR_BUFFER_BIT); - ScreenShader->Activate(); - ScreenShader->Set1F("exposure", 1.0f); - quadVAO->Bind(); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, textureColorbuffer); - glDrawArrays(GL_TRIANGLES, 0, 6); + if (debugView == 0) { + ScreenShader->Activate(); + ScreenShader->Set1F("exposure", 1.0f); + ScreenShader->Set1F("bloomStrength", graphics.bloom ? 0.6f : 0.0f); + ScreenShader->SetInt("screenTexture", 0); + ScreenShader->SetInt("bloomTexture", 1); + quadVAO->Bind(); + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, bloomTextures[0]); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, textureColorbuffer); + glDrawArrays(GL_TRIANGLES, 0, 6); + } + else { + RenderDebugView(); + } // ---- GUI (always on top, no depth) ---- glDisable(GL_CULL_FACE); @@ -347,6 +505,309 @@ void RenderSystem::Render(Camera& camera) glfwSwapBuffers(window->handle); } +// ============================================================================ +// Deferred passes +// ============================================================================ +void RenderSystem::RenderGeometryPass(const glm::mat4& proj, const glm::mat4& view, const glm::vec3& camPos) +{ + gBuffer->Bind(); + glViewport(0, 0, window->width, window->height); + glEnable(GL_DEPTH_TEST); + glDisable(GL_BLEND); // the attachments store data in alpha, blending would corrupt them + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + // Frustum planes as (normal, distance), pulled straight out of the view projection matrix + const glm::mat4 viewProjection = proj * view; + glm::vec4 frustum[6]; + for (int axis = 0; axis < 3; axis++) { + const glm::vec4 row(viewProjection[0][axis], viewProjection[1][axis], viewProjection[2][axis], viewProjection[3][axis]); + const glm::vec4 last(viewProjection[0][3], viewProjection[1][3], viewProjection[2][3], viewProjection[3][3]); + frustum[axis * 2] = last + row; + frustum[axis * 2 + 1] = last - row; + } + occlusionCandidates.clear(); + + GeometryShader->Activate(); + GeometryShader->SetMat4("proj", proj); + GeometryShader->SetMat4("view", view); + for (Renderable* r : renderables) { + if (r->shadertype == Renderable::MeshShader && r->opaque && IsVisible(r, frustum, camPos)) + r->Render(*GeometryShader); + } + + GeometryRigShader->Activate(); + GeometryRigShader->SetMat4("proj", proj); + GeometryRigShader->SetMat4("view", view); + for (Renderable* r : renderables) { + if (r->shadertype == Renderable::RigShader && r->opaque && IsVisible(r, frustum, camPos)) + r->Render(*GeometryRigShader); + } + + IssueOcclusionQueries(proj, view); + glEnable(GL_BLEND); +} + +// Bounds padding, so a box sitting exactly on its object's surface can't fail its own depth test +static constexpr float OCCLUSION_PADDING = 0.05f; + +bool RenderSystem::IsVisible(Renderable* r, const glm::vec4 (&frustum)[6], const glm::vec3& camPos) +{ + BoundingBox bounds; + if (!graphics.culling || !r->GetWorldBounds(bounds)) { + r->occluded = false; + return true; + } + + // Frustum: outside if the corner furthest along a plane's normal is still behind it + for (const glm::vec4& plane : frustum) { + const glm::vec3 normal(plane); + const glm::vec3 furthest( + normal.x >= 0.0f ? bounds.max.x : bounds.min.x, + normal.y >= 0.0f ? bounds.max.y : bounds.min.y, + normal.z >= 0.0f ? bounds.max.z : bounds.min.z); + if (glm::dot(normal, furthest) + plane.w < 0.0f) { + r->occluded = false; // so it draws straight away when it comes back into view + return false; + } + } + + // Only use an answer the GPU already has, waiting for one would stall the frame + if (r->occlusionPending) { + GLuint available = 0; + glGetQueryObjectuiv(r->occlusionQuery, GL_QUERY_RESULT_AVAILABLE, &available); + if (available) { + GLuint samplesPassed = 0; + glGetQueryObjectuiv(r->occlusionQuery, GL_QUERY_RESULT, &samplesPassed); + r->occluded = samplesPassed == 0; + r->occlusionPending = false; + } + } + + // The query box can't be seen from inside it, so never trust it then + const glm::vec3 padding(OCCLUSION_PADDING * 2.0f); + if (glm::all(glm::greaterThan(camPos, bounds.min - padding)) && glm::all(glm::lessThan(camPos, bounds.max + padding))) + r->occluded = false; + + occlusionCandidates.push_back(r); + return !r->occluded; +} + +// Draws each candidate's box against this frame's depth, colour and depth writes off. +// The results get used next frame. +void RenderSystem::IssueOcclusionQueries(const glm::mat4& proj, const glm::mat4& view) +{ + if (occlusionCandidates.empty()) + return; + + glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + glDepthMask(GL_FALSE); + glDisable(GL_CULL_FACE); // the skybox cube faces inward + + HitboxShader->Activate(); + HitboxShader->SetMat4("proj", proj); + HitboxShader->SetMat4("view", view); + skyboxVAO->Bind(); + + for (Renderable* r : occlusionCandidates) { + if (r->occlusionPending) + continue; + + BoundingBox bounds; + r->GetWorldBounds(bounds); + const glm::vec3 center = (bounds.min + bounds.max) * 0.5f; + const glm::vec3 halfSize = (bounds.max - bounds.min) * 0.5f + glm::vec3(OCCLUSION_PADDING); + + if (r->occlusionQuery == 0) + glGenQueries(1, &r->occlusionQuery); + + // The skybox cube runs from -1 to 1, so half size is the scale + HitboxShader->SetMat4("modl", glm::scale(glm::translate(glm::mat4(1.0f), center), halfSize)); + glBeginQuery(GL_ANY_SAMPLES_PASSED, r->occlusionQuery); + glDrawArrays(GL_TRIANGLES, 0, 36); + glEndQuery(GL_ANY_SAMPLES_PASSED); + r->occlusionPending = true; + } + + glBindVertexArray(0); + glEnable(GL_CULL_FACE); + glDepthMask(GL_TRUE); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); +} + +void RenderSystem::RenderShadowCasters(const glm::mat4& lightSpaceMatrix) +{ + MeshShadowShader->Activate(); + MeshShadowShader->SetMat4("lightSpaceMatrix", lightSpaceMatrix); + for (Renderable* r : renderables) { + if (!r->castshadow || r->shadertype != Renderable::MeshShader) continue; + r->Render(*MeshShadowShader); + } + + RigShadowShader->Activate(); + RigShadowShader->SetMat4("lightSpaceMatrix", lightSpaceMatrix); + for (Renderable* r : renderables) { + if (!r->castshadow || r->shadertype != Renderable::RigShader) continue; + r->Render(*RigShadowShader); + } +} + +// Draws the casters from each spot and point light into its tiles of the atlas +void RenderSystem::RenderLocalShadows() +{ + static constexpr float nearPlane = 0.2f; + + std::vector& tiles = lighting->shadowTileMatrices; + tiles.clear(); + + if (!graphics.lightShadows) { + for (SpotLight* light : lighting->spotLights) + light->shadowTile = -1; + for (PointLight* light : lighting->pointLights) + light->shadowTile = -1; + return; + } + + localShadowFBO->Bind(); + glClear(GL_DEPTH_BUFFER_BIT); + glEnable(GL_SCISSOR_TEST); + + auto RenderTile = [&](const glm::mat4& matrix) { + const int index = (int)tiles.size(); + const int x = (index % SHADOW_ATLAS_COLUMNS) * SHADOW_ATLAS_TILE_SIZE; + const int y = (index / SHADOW_ATLAS_COLUMNS) * SHADOW_ATLAS_TILE_SIZE; + glViewport(x, y, SHADOW_ATLAS_TILE_SIZE, SHADOW_ATLAS_TILE_SIZE); + glScissor(x, y, SHADOW_ATLAS_TILE_SIZE, SHADOW_ATLAS_TILE_SIZE); + RenderShadowCasters(matrix); + tiles.push_back(matrix); + }; + + for (SpotLight* light : lighting->spotLights) { + light->shadowTile = -1; + if (!light->castShadow || (int)tiles.size() >= SHADOW_ATLAS_TILES) + continue; + + light->shadowTile = (int)tiles.size(); + const glm::vec3 up = glm::abs(light->direction.y) > 0.99f ? glm::vec3(0.0f, 0.0f, 1.0f) : glm::vec3(0.0f, 1.0f, 0.0f); + RenderTile(glm::perspective(glm::radians(light->outerAngle * 2.0f + 2.0f), 1.0f, nearPlane, light->radius) + * glm::lookAt(light->position, light->position + light->direction, up)); + } + + // Order has to match PointShadowFace in the shaders. Slightly over 90 degrees so the faces overlap at the seams. + static const glm::vec3 faceDirections[6] = { + { 1.0f, 0.0f, 0.0f }, { -1.0f, 0.0f, 0.0f }, { 0.0f, 1.0f, 0.0f }, + { 0.0f, -1.0f, 0.0f }, { 0.0f, 0.0f, 1.0f }, { 0.0f, 0.0f, -1.0f } + }; + static const glm::vec3 faceUps[6] = { + { 0.0f, 1.0f, 0.0f }, { 0.0f, 1.0f, 0.0f }, { 0.0f, 0.0f, 1.0f }, + { 0.0f, 0.0f, 1.0f }, { 0.0f, 1.0f, 0.0f }, { 0.0f, 1.0f, 0.0f } + }; + + for (PointLight* light : lighting->pointLights) { + light->shadowTile = -1; + if (!light->castShadow || (int)tiles.size() + 6 > SHADOW_ATLAS_TILES) + continue; + + light->shadowTile = (int)tiles.size(); + const glm::mat4 projection = glm::perspective(glm::radians(92.0f), 1.0f, nearPlane, light->radius); + for (int face = 0; face < 6; face++) + RenderTile(projection * glm::lookAt(light->position, light->position + faceDirections[face], faceUps[face])); + } + + glDisable(GL_SCISSOR_TEST); + localShadowFBO->Unbind(); +} + +// Leaves the finished glow in bloomTextures[0] and the default framebuffer bound +void RenderSystem::RenderBloom() +{ + static constexpr int blurPasses = 8; // has to stay even, or the result ends up in bloomTextures[1] + + glDisable(GL_BLEND); + glViewport(0, 0, bloomWidth, bloomHeight); + quadVAO->Bind(); + glActiveTexture(GL_TEXTURE0); + + bloomFBO[0]->Bind(); + BloomExtractShader->Activate(); + BloomExtractShader->SetInt("screenTexture", 0); + BloomExtractShader->Set1F("threshold", 1.8f); + glBindTexture(GL_TEXTURE_2D, textureColorbuffer); + glDrawArrays(GL_TRIANGLES, 0, 6); + + BloomBlurShader->Activate(); + BloomBlurShader->SetInt("image", 0); + for (int pass = 0; pass < blurPasses; pass++) { + const int source = pass % 2; + bloomFBO[1 - source]->Bind(); + BloomBlurShader->SetInt("horizontal", source == 0 ? 1 : 0); + glBindTexture(GL_TEXTURE_2D, bloomTextures[source]); + glDrawArrays(GL_TRIANGLES, 0, 6); + } + + bloomFBO[0]->Unbind(); + glEnable(GL_BLEND); +} + +// Units 0 and 2 stay the shadow map and environment map, same as the forward shaders +void RenderSystem::BindGBufferTextures(Shader* shader) +{ + shader->SetInt("gPosition", 3); + shader->SetInt("gNormal", 4); + shader->SetInt("gAlbedo", 5); + shader->SetInt("gMaterial", 6); + glActiveTexture(GL_TEXTURE3); + glBindTexture(GL_TEXTURE_2D, gPosition); + glActiveTexture(GL_TEXTURE4); + glBindTexture(GL_TEXTURE_2D, gNormal); + glActiveTexture(GL_TEXTURE5); + glBindTexture(GL_TEXTURE_2D, gAlbedo); + glActiveTexture(GL_TEXTURE6); + glBindTexture(GL_TEXTURE_2D, gMaterial); +} + +// Expects depth testing off, the fullscreen quad shouldn't touch the depth buffer +void RenderSystem::RenderLightingPass(const glm::vec3& camPos, const glm::mat4& lightSpaceMatrix) +{ + DeferredLightingShader->Activate(); + DeferredLightingShader->Set3F("viewPos", camPos); + DeferredLightingShader->SetMat4("lightSpaceMatrix", lightSpaceMatrix); + DeferredLightingShader->SetInt("shadowMap", 0); + ConfigurePBR(DeferredLightingShader, lighting); + BindGBufferTextures(DeferredLightingShader); + glActiveTexture(GL_TEXTURE2); + skybox->Bind(); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, depthMaptexture); + quadVAO->Bind(); + glDrawArrays(GL_TRIANGLES, 0, 6); + glBindVertexArray(0); +} + +void RenderSystem::RenderDebugView() +{ + GBufferDebugShader->Activate(); + const bool showAtlas = debugView == 8; // reuses the shadow map view + GBufferDebugShader->SetInt("debugMode", showAtlas ? 6 : debugView); + GBufferDebugShader->Set1F("nearPlane", 0.05f); + GBufferDebugShader->Set1F("farPlane", 2000.0f); + BindGBufferTextures(GBufferDebugShader); + GBufferDebugShader->SetInt("depthTex", 7); + GBufferDebugShader->SetInt("shadowTex", 8); + glActiveTexture(GL_TEXTURE7); + glBindTexture(GL_TEXTURE_2D, gDepth); + glActiveTexture(GL_TEXTURE8); + glBindTexture(GL_TEXTURE_2D, showAtlas ? localShadowAtlas : depthMaptexture); + glBindSampler(8, shadowDebugSampler); + GBufferDebugShader->SetInt("bloomTex", 9); + glActiveTexture(GL_TEXTURE9); + glBindTexture(GL_TEXTURE_2D, bloomTextures[0]); + quadVAO->Bind(); + glDrawArrays(GL_TRIANGLES, 0, 6); + glBindSampler(8, 0); + glActiveTexture(GL_TEXTURE0); +} + // ============================================================================ // Shader preparation helpers // ============================================================================ @@ -438,6 +899,8 @@ Renderable::Renderable() {} Renderable::~Renderable() { + if (occlusionQuery != 0) + glDeleteQueries(1, &occlusionQuery); engine->rendersystem->RemoveRenderable(this); } diff --git a/RenderSystem.h b/RenderSystem.h index a36dacc..7de573c 100644 --- a/RenderSystem.h +++ b/RenderSystem.h @@ -4,6 +4,7 @@ #include #include #include +#include "Structures.h" const float skyboxVertices[] = { -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, @@ -43,7 +44,23 @@ public: void BindToRenderSystem(); virtual void Render(Shader& ShaderProgram) {} virtual glm::vec3 GetWorldPosition() const { return glm::vec3(0.0f); } + + // A world space box around everything this draws. Without one it's never culled. + virtual bool GetWorldBounds(BoundingBox& bounds) const { return false; } + + // Occlusion culling state, only the render system touches these + unsigned int occlusionQuery = 0; + bool occlusionPending = false; // a query was sent and hasn't been read back yet + bool occluded = false; // the last answered query saw none of it }; +struct GraphicsSettings { + bool bloom = true; + bool shadows = true; // the sun's shadow map + bool lightShadows = true; // spot and point light shadows + bool vsync = true; + bool culling = true; // frustum and occlusion culling for opaque meshes and characters +}; + struct HitboxDebugLine { glm::mat4 transform; glm::vec3 color; @@ -52,8 +69,23 @@ struct HitboxDebugLine { class RenderSystem { public: Lighting* lighting = nullptr; + GraphicsSettings graphics; FBO* screenfbo = nullptr; FBO* shadowdepthmapfbo = nullptr; + FBO* gBuffer = nullptr; + unsigned int gPosition = 0; + unsigned int gNormal = 0; + unsigned int gAlbedo = 0; + unsigned int gMaterial = 0; + unsigned int gDepth = 0; + unsigned int shadowDebugSampler = 0; + FBO* localShadowFBO = nullptr; + unsigned int localShadowAtlas = 0; + FBO* bloomFBO[2] = { nullptr, nullptr }; + unsigned int bloomTextures[2] = { 0, 0 }; + int bloomWidth = 0; + int bloomHeight = 0; + int debugView = 0; // 0 = final image, otherwise a debugMode in gbuffer_debug.frag unsigned int depthMaptexture = 0; unsigned int textureColorbuffer = 0; unsigned int rbo = 0; @@ -66,6 +98,12 @@ public: Shader* ScreenShader = nullptr; Shader* RigShader = nullptr; Shader* MeshShader = nullptr; + Shader* GeometryShader = nullptr; + Shader* GeometryRigShader = nullptr; + Shader* DeferredLightingShader = nullptr; + Shader* GBufferDebugShader = nullptr; + Shader* BloomExtractShader = nullptr; + Shader* BloomBlurShader = nullptr; Shader* ImageBoxShader = nullptr; Shader* BoxShader = nullptr; Shader* Text2DShader = nullptr; @@ -79,6 +117,7 @@ public: void Initialize(); void Resize(int width, int height); void Render(Camera& camera); + void CycleDebugView(); void SetHitboxDebugLines(const std::vector& lines) { hitboxDebugLines = lines; } void AddRenderable(Renderable* r) { renderables.push_back(r); } void RemoveRenderable(Renderable* r) { @@ -98,5 +137,17 @@ private: void PrepareTextShader(Camera& camera); void RenderTranslucent(Camera& camera, const glm::mat4& lightSpaceMatrix, const glm::vec3& camPos); void RenderHitboxDebug(Camera& camera); + void AllocateGBuffer(int width, int height); + void AllocateBloom(int width, int height); + void RenderBloom(); + void RenderShadowCasters(const glm::mat4& lightSpaceMatrix); + void RenderLocalShadows(); + void BindGBufferTextures(Shader* shader); + void RenderGeometryPass(const glm::mat4& proj, const glm::mat4& view, const glm::vec3& camPos); + bool IsVisible(Renderable* r, const glm::vec4 (&frustum)[6], const glm::vec3& camPos); + void IssueOcclusionQueries(const glm::mat4& proj, const glm::mat4& view); + std::vector occlusionCandidates; // drawn into the queries after the geometry pass + void RenderLightingPass(const glm::vec3& camPos, const glm::mat4& lightSpaceMatrix); + void RenderDebugView(); }; #endif \ No newline at end of file diff --git a/ResourceManager.cpp b/ResourceManager.cpp new file mode 100644 index 0000000..c60aea6 --- /dev/null +++ b/ResourceManager.cpp @@ -0,0 +1,69 @@ +#include "ResourceManager.h" + +#include +#include + +#include "texture.h" +#include "font.h" + +ResourceManager::~ResourceManager() +{ + for (auto& [path, texture] : textures) { + if (!texture) continue; + texture->Delete(); + delete texture; + } + for (Texture* texture : createdTextures) { + texture->Delete(); + delete texture; + } + for (auto& [path, font] : fonts) + delete font; +} + +Texture* ResourceManager::CreateTexture(int width, int height) +{ + createdTextures.push_back(new Texture(width, height)); + return createdTextures.back(); +} + +Texture* ResourceManager::CopyTexture(const Texture& source) +{ + createdTextures.push_back(new Texture(source)); + return createdTextures.back(); +} + +// Failed loads are cached as nullptr too, so a missing file only gets reported once +Texture* ResourceManager::GetTexture(const std::string& path) +{ + auto existing = textures.find(path); + if (existing != textures.end()) + return existing->second; + + Texture* texture = nullptr; + try { + texture = new Texture(path.c_str()); + } + catch (const std::exception& error) { + std::cout << "RESOURCE MANAGER: " << error.what() << "\n"; + } + textures[path] = texture; + return texture; +} + +Font* ResourceManager::GetFont(const std::string& path) +{ + auto existing = fonts.find(path); + if (existing != fonts.end()) + return existing->second; + + Font* font = nullptr; + try { + font = new Font(path.c_str()); + } + catch (const std::exception& error) { + std::cout << "RESOURCE MANAGER: " << error.what() << ": " << path << "\n"; + } + fonts[path] = font; + return font; +} diff --git a/ResourceManager.h b/ResourceManager.h index 37bd620..413c3b4 100644 --- a/ResourceManager.h +++ b/ResourceManager.h @@ -1,20 +1,33 @@ #ifndef RESOURCE_MANAGER_CLASS #define RESOURCE_MANAGER_CLASS +#include +#include #include -#include "Folder.h" -//#include "font.h" -#include "Model.h" -#include "SoundSystem.h" +class Texture; +class Font; +// Owns every texture and font, so each file only loads once however many things use it. +// Sounds are cached by SoundSystem, rigs and animations by CharacterManager. class ResourceManager { public: - //std::vector fonts; - std::vector models; - std::vector sounds; -public: + ResourceManager() {} + ~ResourceManager(); // needs the GL context, delete this before the window + ResourceManager(const ResourceManager&) = delete; + // Returns nullptr and logs it if the file can't be loaded + Texture* GetTexture(const std::string& path); + Font* GetFont(const std::string& path); + + // Not cached, every call makes a new texture that's free to be edited + Texture* CreateTexture(int width, int height); + Texture* CopyTexture(const Texture& source); + +private: + std::unordered_map textures; + std::unordered_map fonts; + std::vector createdTextures; }; -#endif \ No newline at end of file +#endif diff --git a/SurfaceTexture.h b/SurfaceTexture.h new file mode 100644 index 0000000..6c098bb --- /dev/null +++ b/SurfaceTexture.h @@ -0,0 +1,57 @@ +#ifndef SURFACE_TEXTURE_CLASS +#define SURFACE_TEXTURE_CLASS + +#include + +#include "shaderClass.h" +#include "texture.h" + +// How one game element wears a texture. Texture is the shared image, this is per element, +// so the same image can be stretched on one thing and tiled on another. +class SurfaceTexture { +public: + enum class Mode { + Stretch, // the image fills each face once, whatever size it is + Tile // the image repeats every tileSize, so it keeps its size as the element grows + }; + + Texture* texture = nullptr; + Mode mode = Mode::Stretch; + float tileSize = 1.0f; // world units on meshes, pixels on gui + glm::vec2 offset = glm::vec2(0.0f); // slides the image, in repeats of it + + SurfaceTexture() {} + + static SurfaceTexture Stretched(Texture* texture) { + SurfaceTexture surface; + surface.texture = texture; + return surface; + } + + static SurfaceTexture Tiled(Texture* texture, float tileSize) { + SurfaceTexture surface; + surface.texture = texture; + surface.mode = Mode::Tile; + surface.tileSize = tileSize; + return surface; + } + + // Binds the image and sets the uv uniforms. size is the element's size, with the shader active. + void Apply(Shader& shader, GLuint unit, const glm::vec3& size) const { + glActiveTexture(GL_TEXTURE0 + unit); + shader.SetInt("tex", unit); + if (texture == nullptr) { + glBindTexture(GL_TEXTURE_2D, 0); + shader.SetInt("useTexture", 0); + return; + } + + glBindTexture(GL_TEXTURE_2D, texture->ID); + shader.SetInt("useTexture", 1); + shader.SetInt("uvMode", mode == Mode::Tile ? 1 : 0); + shader.Set3F("tileScale", size / glm::max(tileSize, 0.0001f)); + shader.Set2F("uvOffset", offset); + } +}; + +#endif diff --git a/TexturePainter.h b/TexturePainter.h new file mode 100644 index 0000000..91048e6 --- /dev/null +++ b/TexturePainter.h @@ -0,0 +1,71 @@ +#ifndef TEXTURE_PAINTER_CLASS +#define TEXTURE_PAINTER_CLASS + +#include + +#include + +#include "texture.h" + +// The brush behind the texture drawer. Edits a Texture's pixels and uploads them. +class TexturePainter { +public: + glm::vec3 color = glm::vec3(0.0f); + int brushSize = 8; // radius in pixels of a 256 wide texture, so it looks the same on any size + + // uv runs 0-1 from the top left of the image. Joins up with the last call until EndStroke. + void Stroke(Texture& texture, const glm::vec2& uv) { + const glm::vec2 point(uv.x * texture.width, (1.0f - uv.y) * texture.height); + const float radius = glm::max(1.0f, brushSize * texture.width / 256.0f); + + if (!stroking) { + Stamp(texture, point, radius); + } + else { + // Fill the gap, the mouse can cross a lot of pixels in one frame + const int steps = (int)std::ceil(glm::length(point - lastPoint) / (radius * 0.5f)); + for (int i = 1; i <= steps; i++) + Stamp(texture, glm::mix(lastPoint, point, (float)i / (float)steps), radius); + } + + stroking = true; + lastPoint = point; + texture.Upload(); + } + + void EndStroke() { + stroking = false; + } + + void Clear(Texture& texture) { + texture.pixels.assign(texture.pixels.size(), 255); + texture.Upload(); + } + +private: + bool stroking = false; + glm::vec2 lastPoint = glm::vec2(0.0f); + + void Stamp(Texture& texture, const glm::vec2& center, float radius) { + const int minX = glm::max((int)(center.x - radius), 0); + const int maxX = glm::min((int)(center.x + radius), texture.width - 1); + const int minY = glm::max((int)(center.y - radius), 0); + const int maxY = glm::min((int)(center.y + radius), texture.height - 1); + + for (int y = minY; y <= maxY; y++) { + for (int x = minX; x <= maxX; x++) { + const glm::vec2 offset = glm::vec2(x + 0.5f, y + 0.5f) - center; + if (glm::dot(offset, offset) > radius * radius) + continue; + + unsigned char* pixel = &texture.pixels[((size_t)y * texture.width + x) * 4]; + pixel[0] = (unsigned char)(color.r * 255.0f); + pixel[1] = (unsigned char)(color.g * 255.0f); + pixel[2] = (unsigned char)(color.b * 255.0f); + pixel[3] = 255; + } + } + } +}; + +#endif diff --git a/eiifwhrn.vcxproj b/eiifwhrn.vcxproj index 4d75347..276eb54 100644 --- a/eiifwhrn.vcxproj +++ b/eiifwhrn.vcxproj @@ -166,15 +166,17 @@ + - + + @@ -188,18 +190,20 @@ + + - + @@ -218,6 +222,8 @@ + + @@ -231,12 +237,16 @@ + + + + diff --git a/eiifwhrn.vcxproj.filters b/eiifwhrn.vcxproj.filters index 0bfea55..6833aa7 100644 --- a/eiifwhrn.vcxproj.filters +++ b/eiifwhrn.vcxproj.filters @@ -43,6 +43,15 @@ Source Files + + Source Files + + + Source Files + + + Source Files + Source Files @@ -120,6 +129,18 @@ Header Files + + Header Files + + + Header Files + + + Header Files + + + Header Files + Header Files @@ -162,6 +183,9 @@ Header Files + + Header Files + Header Files @@ -205,6 +229,18 @@ Shaders + + Shaders + + + Shaders + + + Shaders + + + Shaders + Shaders diff --git a/main.cpp b/main.cpp index 25bd910..247ecf2 100644 --- a/main.cpp +++ b/main.cpp @@ -105,6 +105,7 @@ int main() { engine->Initiate(); std::cout << "engine stopped\n"; + delete resourcemanager; delete window; delete mouse; delete keyboard; diff --git a/patch2.txt b/patch2.txt new file mode 100644 index 0000000..e69de29 diff --git a/patch_temp.txt b/patch_temp.txt new file mode 100644 index 0000000..e69de29 diff --git a/shaderClass.h b/shaderClass.h index 43dd60f..51d61c9 100644 --- a/shaderClass.h +++ b/shaderClass.h @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -19,35 +20,49 @@ public: Shader(const char* vertexFile, const char* fragFile); void Set1F(const char* uniform, float value) { - GLint location = glGetUniformLocation(Shader::ID, uniform); - glUniform1f(location, value); + glUniform1f(GetLocation(uniform), value); }; void Set2F(const char* uniform, glm::vec2 value) { - GLint location = glGetUniformLocation(Shader::ID, uniform); - glUniform2f(location, value.x,value.y); + glUniform2f(GetLocation(uniform), value.x,value.y); }; void Set3F(const char* uniform, glm::vec3 value) { - GLint location = glGetUniformLocation(Shader::ID, uniform); - glUniform3f(location, value.x,value.y,value.z); + glUniform3f(GetLocation(uniform), value.x,value.y,value.z); }; void Set4F(const char* uniform, glm::vec4 value) { - GLint location = glGetUniformLocation(Shader::ID, uniform); - glUniform4f(location, value.x, value.y, value.z, value.w); + glUniform4f(GetLocation(uniform), value.x, value.y, value.z, value.w); }; void SetMat4(const char* uniform, glm::mat4 value) { - GLint location = glGetUniformLocation(Shader::ID, uniform); - glUniformMatrix4fv(location,1,GL_FALSE,&value[0][0]); + glUniformMatrix4fv(GetLocation(uniform),1,GL_FALSE,&value[0][0]); + }; + + // Uploads a whole uniform array in one call + void SetMat4Array(const char* uniform, const glm::mat4* values, int count) { + glUniformMatrix4fv(GetLocation(uniform),count,GL_FALSE,&values[0][0][0]); }; void SetInt(const char* uniform, GLuint value) { - GLint location = glGetUniformLocation(Shader::ID, uniform); - glUniform1i(location, value); + glUniform1i(GetLocation(uniform), value); }; void Activate(); void Delete(); + +private: + // Asking the driver is slow and locations never change once linked. + // Uniforms the shader doesn't have cache as -1, which glUniform ignores. + GLint GetLocation(const char* uniform) { + auto existing = uniformLocations.find(uniform); + if (existing != uniformLocations.end()) + return existing->second; + + GLint location = glGetUniformLocation(ID, uniform); + uniformLocations.emplace(uniform, location); + return location; + } + + std::unordered_map uniformLocations; }; #endif \ No newline at end of file diff --git a/shaders/Box.frag b/shaders/Box.frag index 8c60a7c..90a3585 100644 --- a/shaders/Box.frag +++ b/shaders/Box.frag @@ -12,7 +12,6 @@ uniform vec2 pixelScale; uniform vec2 pixelPos; uniform vec4 Color = {0.0f,0.0f,0.0f,1.0f}; -uniform vec4 BackgroundColor = {0.0f,0.0f,0.0f,1.0f}; void main() { @@ -27,10 +26,11 @@ void main() vec2 absrelcoord = abs(fragPosTopLeft - pixelScale/2 - pixelPos); if (!(absrelcoord.x < r1.x || absrelcoord.y < r1.y)) { - if (pow(absrelcoord.y-r1.y,2) > pow(roundingfactor,2) - pow((absrelcoord.x-r1.x),2)) { + vec2 cornerOffset = absrelcoord - r1; + if (dot(cornerOffset, cornerOffset) > roundingfactor * roundingfactor) { discard; + } } } - } } diff --git a/shaders/ImageBox.frag b/shaders/ImageBox.frag index f78d43d..e614407 100644 --- a/shaders/ImageBox.frag +++ b/shaders/ImageBox.frag @@ -12,7 +12,6 @@ uniform vec2 pixelScale; uniform vec2 pixelPos; uniform vec4 Color = {0.0f,0.0f,0.0f,1.0f}; -uniform vec4 BackgroundColor = {0.0f,0.0f,0.0f,1.0f}; uniform sampler2D tex; @@ -29,10 +28,11 @@ void main() vec2 absrelcoord = abs(fragPosTopLeft - pixelScale/2 - pixelPos); if (!(absrelcoord.x < r1.x || absrelcoord.y < r1.y)) { - if (pow(absrelcoord.y-r1.y,2) > pow(roundingfactor,2) - pow((absrelcoord.x-r1.x),2)) { + vec2 cornerOffset = absrelcoord - r1; + if (dot(cornerOffset, cornerOffset) > roundingfactor * roundingfactor) { discard; + } } } - } } diff --git a/shaders/ImageBox.vert b/shaders/ImageBox.vert index 5467b40..f6e949d 100644 --- a/shaders/ImageBox.vert +++ b/shaders/ImageBox.vert @@ -11,6 +11,10 @@ uniform vec2 pixelScale; uniform float z; uniform float rotation; +uniform int uvMode = 0; // 0 = stretch, 1 = tile +uniform vec3 tileScale = vec3(1.0); +uniform vec2 uvOffset = vec2(0.0); + void main() { vec2 unitPos = aPos * 0.5 + 0.5; @@ -20,5 +24,7 @@ void main() gl_Position = vec4(ndc, 0.0f, 1.0f); - TexCoords = aTexCoords; + // Screen y runs down here, so flip v to keep images the right way up + vec2 uv = vec2(aTexCoords.x, 1.0 - aTexCoords.y); + TexCoords = (uvMode == 1 ? uv * tileScale.xy : uv) + uvOffset; } \ No newline at end of file diff --git a/shaders/Particle.vert b/shaders/Particle.vert index f884c17..6af3a28 100644 --- a/shaders/Particle.vert +++ b/shaders/Particle.vert @@ -11,12 +11,14 @@ out vec3 FragPos; uniform mat4 view; uniform mat4 proj; - +uniform int uvMode = 0; // 0 = stretch, 1 = tile +uniform vec3 tileScale = vec3(1.0); +uniform vec2 uvOffset = vec2(0.0); void main() { FragPos = vec3(modl * vec4(aPos, 1.0)); normal = aNormal; - texCoord = aTex; + texCoord = (uvMode == 1 ? aPos.xy * tileScale.xy : aTex) + uvOffset; gl_Position = proj* view * modl * vec4(aPos,1.0f); } \ No newline at end of file diff --git a/shaders/ScreenShader.frag b/shaders/ScreenShader.frag index 592b348..0e618e2 100644 --- a/shaders/ScreenShader.frag +++ b/shaders/ScreenShader.frag @@ -4,6 +4,8 @@ out vec4 FragColor; in vec2 TexCoords; uniform sampler2D screenTexture; +uniform sampler2D bloomTexture; +uniform float bloomStrength = 0.0f; uniform float brightness = 0.0f; uniform float colorContrast = 0.0f; uniform float saturation = 1.0f; @@ -21,21 +23,19 @@ vec3 ACESFilm(vec3 x) void main() { - vec3 originalcolor = texture(screenTexture, TexCoords).rgb; + // Added before tonemapping, while everything is still in HDR + vec3 originalcolor = texture(screenTexture, TexCoords).rgb + + texture(bloomTexture, TexCoords).rgb * bloomStrength; vec3 finalcolor = ACESFilm(originalcolor * exposure); const float gamma = 2.2; finalcolor = pow(finalcolor, vec3(1.0 / gamma)); - vec3 contrast = vec3(1.0f) - finalcolor; - finalcolor = mix(finalcolor,vec3(1.0f) - finalcolor,colorContrast); float average = (finalcolor[0]+finalcolor[1]+finalcolor[2])/3.0f; - for (int i = 0; i < 3; i++) { - finalcolor[i] = average + saturation * (finalcolor[i] - average); - } + finalcolor = mix(vec3(average), finalcolor, saturation); FragColor = vec4(finalcolor,1.0f); } diff --git a/shaders/bloom_blur.frag b/shaders/bloom_blur.frag new file mode 100644 index 0000000..cd708c6 --- /dev/null +++ b/shaders/bloom_blur.frag @@ -0,0 +1,23 @@ +#version 330 core +out vec4 FragColor; + +in vec2 TexCoords; + +uniform sampler2D image; +uniform int horizontal; + +const float weights[5] = float[](0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216); + +void main() +{ + vec2 texel = 1.0 / vec2(textureSize(image, 0)); + vec2 direction = horizontal != 0 ? vec2(texel.x, 0.0) : vec2(0.0, texel.y); + + vec3 color = texture(image, TexCoords).rgb * weights[0]; + for (int i = 1; i < 5; i++) { + color += texture(image, TexCoords + direction * float(i)).rgb * weights[i]; + color += texture(image, TexCoords - direction * float(i)).rgb * weights[i]; + } + + FragColor = vec4(color, 1.0); +} diff --git a/shaders/bloom_extract.frag b/shaders/bloom_extract.frag new file mode 100644 index 0000000..5e9e838 --- /dev/null +++ b/shaders/bloom_extract.frag @@ -0,0 +1,26 @@ +#version 330 core +out vec4 FragColor; + +in vec2 TexCoords; + +uniform sampler2D screenTexture; +uniform float threshold; + +void main() +{ + // Averaged, since a quarter size target would otherwise skip most of the screen's pixels + vec2 texel = 1.0 / vec2(textureSize(screenTexture, 0)); + vec3 color = (texture(screenTexture, TexCoords + texel * vec2(-1.0, -1.0)).rgb + + texture(screenTexture, TexCoords + texel * vec2(1.0, -1.0)).rgb + + texture(screenTexture, TexCoords + texel * vec2(-1.0, 1.0)).rgb + + texture(screenTexture, TexCoords + texel * vec2(1.0, 1.0)).rgb) * 0.25; + + // A single blown out specular pixel would otherwise flash a huge glow + color = min(color, vec3(20.0)); + + // Keeps the hue of whatever is over the threshold, and fades in instead of cutting off + float brightness = max(color.r, max(color.g, color.b)); + float contribution = max(brightness - threshold, 0.0) / max(brightness, 1e-4); + + FragColor = vec4(color * contribution, 1.0); +} diff --git a/shaders/default.frag b/shaders/default.frag index 84a2266..56fe314 100644 --- a/shaders/default.frag +++ b/shaders/default.frag @@ -12,7 +12,42 @@ struct DirLight { float intensity; }; -uniform DirLight dirLight; +struct PointLight { + vec3 position; + vec3 color; + float intensity; + float radius; + int shadowTile; +}; + +struct SpotLight { + vec3 position; + vec3 direction; + vec3 color; + float intensity; + float radius; + float innerCos; + float outerCos; + int shadowTile; +}; + +// The array sizes have to match Light.h +#define MAX_DIRECTIONAL_LIGHTS 4 +#define MAX_POINT_LIGHTS 16 +#define MAX_SPOT_LIGHTS 8 +#define SHADOW_ATLAS_COLUMNS 6 +#define SHADOW_ATLAS_TILES (SHADOW_ATLAS_COLUMNS * SHADOW_ATLAS_COLUMNS) + +uniform DirLight dirLight; // the sun, the only light with a shadow map +uniform DirLight directionalLights[MAX_DIRECTIONAL_LIGHTS]; +uniform int directionalLightCount = 0; +uniform PointLight pointLights[MAX_POINT_LIGHTS]; +uniform int pointLightCount = 0; +uniform SpotLight spotLights[MAX_SPOT_LIGHTS]; +uniform int spotLightCount = 0; +uniform sampler2DShadow localShadowMap; +uniform mat4 shadowTileMatrices[SHADOW_ATLAS_TILES]; + uniform vec3 viewPos; uniform sampler2DShadow shadowMap; @@ -23,6 +58,7 @@ uniform int useTexture = 0; uniform float metallic = 0.0; uniform float roughness = 0.7; uniform float ao = 1.0; +uniform float emissive = 0.0; uniform float envIntensity = 0.8; const float PI = 3.14159265359; @@ -72,6 +108,65 @@ vec3 EnvironmentBRDF(vec3 F0, float rough, float NdotV) return F0 * AB.x + AB.y; } +// Light from one direction, L points from the surface toward the light +vec3 ShadeLight(vec3 L, vec3 radiance, vec3 N, vec3 V, vec3 albedo, float metal, float rough, vec3 F0, float NdotV) +{ + float NdotL = max(dot(N, L), 0.0); + if (NdotL <= 0.0) + return vec3(0.0); + + vec3 H = normalize(V + L); + vec3 F = FresnelSchlick(max(dot(H, V), 0.0), F0); + float NDF = DistributionGGX(N, H, rough); + float G = GeometrySmith(N, V, L, rough); + vec3 specular = (NDF * G * F) / max(4.0 * NdotV * NdotL, 1e-4); + vec3 kD = (vec3(1.0) - F) * (1.0 - metal); + return (kD * albedo / PI + specular) * radiance * NdotL; +} + +// Inverse square falloff, windowed so it reaches exactly zero at the radius +float PointAttenuation(float distanceSquared, float radius) +{ + float ratio = distanceSquared / (radius * radius); + float window = clamp(1.0 - ratio * ratio, 0.0, 1.0); + return window * window / max(distanceSquared, 0.01); +} + +// Which of a point light's 6 atlas tiles covers offset, the order is set in RenderLocalShadows +int PointShadowFace(vec3 offset) +{ + vec3 size = abs(offset); + if (size.x >= size.y && size.x >= size.z) return offset.x > 0.0 ? 0 : 1; + if (size.y >= size.z) return offset.y > 0.0 ? 2 : 3; + return offset.z > 0.0 ? 4 : 5; +} + +// 1 = lit, 0 = shadowed. Stays inside the tile so the filter can't pick up its neighbours. +float LocalShadow(int tile, vec3 worldPos, vec3 N) +{ + vec4 clipPos = shadowTileMatrices[tile] * vec4(worldPos + N * 0.04, 1.0); + if (clipPos.w <= 0.0) + return 1.0; + + vec3 projCoords = clipPos.xyz / clipPos.w * 0.5 + 0.5; + if (any(lessThan(projCoords, vec3(0.0))) || any(greaterThan(projCoords, vec3(1.0)))) + return 1.0; + + float tileSize = 1.0 / float(SHADOW_ATLAS_COLUMNS); + vec2 tileOrigin = vec2(float(tile % SHADOW_ATLAS_COLUMNS), float(tile / SHADOW_ATLAS_COLUMNS)) * tileSize; + vec2 texelSize = 1.0 / vec2(textureSize(localShadowMap, 0)); + vec2 uv = tileOrigin + projCoords.xy * tileSize; + + float shadow = 0.0; + for (int x = -1; x <= 1; x++) { + for (int y = -1; y <= 1; y++) { + vec2 sampleUV = clamp(uv + vec2(x, y) * texelSize, tileOrigin + texelSize, tileOrigin + tileSize - texelSize); + shadow += texture(localShadowMap, vec3(sampleUV, projCoords.z - 0.0002)); + } + } + return shadow / 9.0; +} + float ShadowCalculation(vec4 fragPosLightSpace, vec3 N, vec3 L) { vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w; @@ -112,21 +207,63 @@ void main() vec3 N = normalize(normal); vec3 V = normalize(viewPos - FragPos); - vec3 L = normalize(-dirLight.direction); - vec3 H = normalize(V + L); float NdotV = max(dot(N, V), 1e-4); - float NdotL = max(dot(N, L), 0.0); vec3 F0 = mix(vec3(0.04), albedo, metal); - vec3 F = FresnelSchlick(max(dot(H, V), 0.0), F0); - float NDF = DistributionGGX(N, H, rough); - float G = GeometrySmith(N, V, L, rough); - vec3 specular = (NDF * G * F) / max(4.0 * NdotV * NdotL, 1e-4); - vec3 kD = (vec3(1.0) - F) * (1.0 - metal); - vec3 radiance = dirLight.color * dirLight.intensity; + vec3 direct = vec3(0.0); - float shadow = ShadowCalculation(fragposlightspace, N, L); - vec3 direct = (kD * albedo / PI + specular) * radiance * NdotL * shadow; + // Facing away from the sun has no direct light, so skip the BRDF and the 9 shadow samples + vec3 sunL = normalize(-dirLight.direction); + if (dot(N, sunL) > 0.0) { + float shadow = ShadowCalculation(fragposlightspace, N, sunL); + direct += ShadeLight(sunL, dirLight.color * dirLight.intensity, N, V, albedo, metal, rough, F0, NdotV) * shadow; + } + + for (int i = 0; i < directionalLightCount; i++) { + vec3 L = normalize(-directionalLights[i].direction); + vec3 radiance = directionalLights[i].color * directionalLights[i].intensity; + direct += ShadeLight(L, radiance, N, V, albedo, metal, rough, F0, NdotV); + } + + for (int i = 0; i < pointLightCount; i++) { + vec3 toLight = pointLights[i].position - FragPos; + float distanceSquared = dot(toLight, toLight); + float radius = pointLights[i].radius; + if (distanceSquared >= radius * radius) + continue; + + vec3 radiance = pointLights[i].color * pointLights[i].intensity * PointAttenuation(distanceSquared, radius); + vec3 L = toLight / sqrt(distanceSquared); + if (dot(N, L) <= 0.0) + continue; + + vec3 lit = ShadeLight(L, radiance, N, V, albedo, metal, rough, F0, NdotV); + if (pointLights[i].shadowTile >= 0) + lit *= LocalShadow(pointLights[i].shadowTile + PointShadowFace(-toLight), FragPos, N); + direct += lit; + } + + for (int i = 0; i < spotLightCount; i++) { + vec3 toLight = spotLights[i].position - FragPos; + float distanceSquared = dot(toLight, toLight); + float radius = spotLights[i].radius; + if (distanceSquared >= radius * radius) + continue; + + vec3 L = toLight / sqrt(distanceSquared); + float cone = smoothstep(spotLights[i].outerCos, spotLights[i].innerCos, dot(-L, spotLights[i].direction)); + if (cone <= 0.0) + continue; + + if (dot(N, L) <= 0.0) + continue; + + vec3 radiance = spotLights[i].color * spotLights[i].intensity * PointAttenuation(distanceSquared, radius) * cone; + vec3 lit = ShadeLight(L, radiance, N, V, albedo, metal, rough, F0, NdotV); + if (spotLights[i].shadowTile >= 0) + lit *= LocalShadow(spotLights[i].shadowTile, FragPos, N); + direct += lit; + } vec3 ambientF = FresnelSchlickRoughness(NdotV, F0, rough); vec3 ambientKD = (vec3(1.0) - ambientF) * (1.0 - metal); @@ -136,5 +273,5 @@ void main() vec3 specularIBL = prefiltered * EnvironmentBRDF(F0, rough, NdotV); vec3 ambient = (ambientKD * irradiance * albedo + specularIBL) * ao; - FragColor = vec4(direct + ambient, albedoSample.a); + FragColor = vec4(direct + ambient + albedo * emissive * 4.0, albedoSample.a); } diff --git a/shaders/default.vert b/shaders/default.vert index 46f89d6..7a04ec7 100644 --- a/shaders/default.vert +++ b/shaders/default.vert @@ -13,11 +13,35 @@ uniform mat4 proj; uniform mat4 lightSpaceMatrix; +uniform int uvMode = 0; // 0 = stretch, 1 = tile +uniform vec3 tileScale = vec3(1.0); +uniform vec2 uvOffset = vec2(0.0); + +// Same direction as transpose(inverse(m)) for normals, without the per vertex inverse +mat3 CofactorMatrix(mat3 m) +{ + return mat3(cross(m[1], m[2]), cross(m[2], m[0]), cross(m[0], m[1])); +} + +// Tiling ignores the mesh's uvs and projects from whichever axis the surface faces, +// so every face of a stretched cube repeats at the same size +vec2 SurfaceUV() +{ + if (uvMode == 0) return aTex + uvOffset; + + vec3 n = abs(aNormal); + vec3 p = aPos * tileScale; + if (n.y >= n.x && n.y >= n.z) return p.xz + uvOffset; + if (n.x >= n.z) return p.zy + uvOffset; + return p.xy + uvOffset; +} + void main() { - FragPos = vec3(modl * vec4(aPos, 1.0)); - normal = transpose(inverse(mat3(modl))) * aNormal; - texCoord = aTex; - fragposlightspace = lightSpaceMatrix * vec4(FragPos, 1.0); - gl_Position = proj* view * modl * vec4(aPos,1.0f); + vec4 worldPos = modl * vec4(aPos, 1.0); + FragPos = worldPos.xyz; + normal = CofactorMatrix(mat3(modl)) * aNormal; + texCoord = SurfaceUV(); + fragposlightspace = lightSpaceMatrix * worldPos; + gl_Position = proj * view * worldPos; } \ No newline at end of file diff --git a/shaders/deferred_lighting.frag b/shaders/deferred_lighting.frag index 9a9c001..2c9f1b0 100644 --- a/shaders/deferred_lighting.frag +++ b/shaders/deferred_lighting.frag @@ -3,59 +3,273 @@ out vec4 FragColor; in vec2 TexCoord; -uniform sampler2D gPosition; -uniform sampler2D gNormal; -uniform sampler2D gAlbedo; -uniform sampler2D gLightSpacePos; -uniform sampler2DShadow shadowMap; - struct DirLight { vec3 direction; - vec3 ambient; - vec3 diffuse; - vec3 specular; + vec3 color; + float intensity; }; -uniform DirLight dirLight; -uniform vec3 viewPos; -float ShadowCalculation(vec4 fragPosLightSpace) +struct PointLight { + vec3 position; + vec3 color; + float intensity; + float radius; + int shadowTile; +}; + +struct SpotLight { + vec3 position; + vec3 direction; + vec3 color; + float intensity; + float radius; + float innerCos; + float outerCos; + int shadowTile; +}; + +// The array sizes have to match Light.h +#define MAX_DIRECTIONAL_LIGHTS 4 +#define MAX_POINT_LIGHTS 16 +#define MAX_SPOT_LIGHTS 8 +#define SHADOW_ATLAS_COLUMNS 6 +#define SHADOW_ATLAS_TILES (SHADOW_ATLAS_COLUMNS * SHADOW_ATLAS_COLUMNS) + +uniform DirLight dirLight; // the sun, the only light with a shadow map +uniform DirLight directionalLights[MAX_DIRECTIONAL_LIGHTS]; +uniform int directionalLightCount = 0; +uniform PointLight pointLights[MAX_POINT_LIGHTS]; +uniform int pointLightCount = 0; +uniform SpotLight spotLights[MAX_SPOT_LIGHTS]; +uniform int spotLightCount = 0; +uniform sampler2DShadow localShadowMap; +uniform mat4 shadowTileMatrices[SHADOW_ATLAS_TILES]; + +uniform vec3 viewPos; +uniform mat4 lightSpaceMatrix; + +uniform sampler2D gPosition; +uniform sampler2D gNormal; +uniform sampler2D gAlbedo; +uniform sampler2D gMaterial; +uniform sampler2DShadow shadowMap; +uniform samplerCube environmentMap; +uniform float envIntensity = 0.8; + +const float PI = 3.14159265359; + +float DistributionGGX(vec3 N, vec3 H, float rough) { - vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w; - projCoords = projCoords * 0.5 + 0.5; - if (projCoords.z > 1.0) return 0.0; - return texture(shadowMap, vec3(projCoords.xy, projCoords.z)); + float a = rough * rough; + float a2 = a * a; + float NdotH = max(dot(N, H), 0.0); + float NdotH2 = NdotH * NdotH; + float denom = NdotH2 * (a2 - 1.0) + 1.0; + return a2 / max(PI * denom * denom, 1e-5); } -vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir, vec4 fragPosLS, vec3 albedo) +float GeometrySchlickGGX(float NdotV, float rough) { - vec3 lightDir = normalize(-light.direction); - float diff = max(dot(normal, lightDir), 0.0); - vec3 halfwayDir = normalize(lightDir + viewDir); - float spec = pow(max(dot(normal, halfwayDir), 0.0), 32.0); + float r = rough + 1.0; + float k = (r * r) / 8.0; + return NdotV / (NdotV * (1.0 - k) + k); +} - vec3 ambient = light.ambient * albedo; - vec3 diffuse = light.diffuse * diff * albedo; - vec3 specular = light.specular * spec; +float GeometrySmith(vec3 N, vec3 V, vec3 L, float rough) +{ + float NdotV = max(dot(N, V), 0.0); + float NdotL = max(dot(N, L), 0.0); + return GeometrySchlickGGX(NdotV, rough) * GeometrySchlickGGX(NdotL, rough); +} - float shadow = ShadowCalculation(fragPosLS); - return ambient + (diffuse + specular) * shadow; +vec3 FresnelSchlick(float cosTheta, vec3 F0) +{ + return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0); +} + +vec3 FresnelSchlickRoughness(float cosTheta, vec3 F0, float rough) +{ + vec3 f = max(vec3(1.0 - rough), F0); + return F0 + (f - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0); +} + +vec3 EnvironmentBRDF(vec3 F0, float rough, float NdotV) +{ + const vec4 c0 = vec4(-1.0, -0.0275, -0.572, 0.022); + const vec4 c1 = vec4(1.0, 0.0425, 1.04, -0.04); + vec4 r = rough * c0 + c1; + float a004 = min(r.x * r.x, exp2(-9.28 * NdotV)) * r.x + r.y; + vec2 AB = vec2(-1.04, 1.04) * a004 + r.zw; + return F0 * AB.x + AB.y; +} + +// Light from one direction, L points from the surface toward the light +vec3 ShadeLight(vec3 L, vec3 radiance, vec3 N, vec3 V, vec3 albedo, float metal, float rough, vec3 F0, float NdotV) +{ + float NdotL = max(dot(N, L), 0.0); + if (NdotL <= 0.0) + return vec3(0.0); + + vec3 H = normalize(V + L); + vec3 F = FresnelSchlick(max(dot(H, V), 0.0), F0); + float NDF = DistributionGGX(N, H, rough); + float G = GeometrySmith(N, V, L, rough); + vec3 specular = (NDF * G * F) / max(4.0 * NdotV * NdotL, 1e-4); + vec3 kD = (vec3(1.0) - F) * (1.0 - metal); + return (kD * albedo / PI + specular) * radiance * NdotL; +} + +// Inverse square falloff, windowed so it reaches exactly zero at the radius +float PointAttenuation(float distanceSquared, float radius) +{ + float ratio = distanceSquared / (radius * radius); + float window = clamp(1.0 - ratio * ratio, 0.0, 1.0); + return window * window / max(distanceSquared, 0.01); +} + +// Which of a point light's 6 atlas tiles covers offset, the order is set in RenderLocalShadows +int PointShadowFace(vec3 offset) +{ + vec3 size = abs(offset); + if (size.x >= size.y && size.x >= size.z) return offset.x > 0.0 ? 0 : 1; + if (size.y >= size.z) return offset.y > 0.0 ? 2 : 3; + return offset.z > 0.0 ? 4 : 5; +} + +// 1 = lit, 0 = shadowed. Stays inside the tile so the filter can't pick up its neighbours. +float LocalShadow(int tile, vec3 worldPos, vec3 N) +{ + vec4 clipPos = shadowTileMatrices[tile] * vec4(worldPos + N * 0.04, 1.0); + if (clipPos.w <= 0.0) + return 1.0; + + vec3 projCoords = clipPos.xyz / clipPos.w * 0.5 + 0.5; + if (any(lessThan(projCoords, vec3(0.0))) || any(greaterThan(projCoords, vec3(1.0)))) + return 1.0; + + float tileSize = 1.0 / float(SHADOW_ATLAS_COLUMNS); + vec2 tileOrigin = vec2(float(tile % SHADOW_ATLAS_COLUMNS), float(tile / SHADOW_ATLAS_COLUMNS)) * tileSize; + vec2 texelSize = 1.0 / vec2(textureSize(localShadowMap, 0)); + vec2 uv = tileOrigin + projCoords.xy * tileSize; + + float shadow = 0.0; + for (int x = -1; x <= 1; x++) { + for (int y = -1; y <= 1; y++) { + vec2 sampleUV = clamp(uv + vec2(x, y) * texelSize, tileOrigin + texelSize, tileOrigin + tileSize - texelSize); + shadow += texture(localShadowMap, vec3(sampleUV, projCoords.z - 0.0002)); + } + } + return shadow / 9.0; +} + +float ShadowCalculation(vec4 fragPosLightSpace, vec3 N, vec3 L) +{ + vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w; + projCoords = projCoords * 0.5 + 0.5; + + if (projCoords.z > 1.0 || any(lessThan(projCoords, vec3(0.0))) || + any(greaterThan(projCoords, vec3(1.0)))) + return 1.0; + + float bias = max(0.004 * (1.0 - dot(N, L)), 0.001); + vec2 texelSize = 1.0 / vec2(textureSize(shadowMap, 0)); + float shadow = 0.0; + + for (int x = -1; x <= 1; x++) { + for (int y = -1; y <= 1; y++) { + vec2 offset = vec2(x, y) * texelSize; + shadow += texture(shadowMap, vec3(projCoords.xy + offset, projCoords.z - bias)); + } + } + return shadow / 9.0; } void main() { - vec3 fragPos = texture(gPosition, TexCoord).rgb; - vec3 normal = normalize(texture(gNormal, TexCoord).rgb); - vec4 albedaSample = texture(gAlbedo, TexCoord); - vec4 fragPosLS = texture(gLightSpacePos, TexCoord); + vec3 normal = texture(gNormal, TexCoord).rgb; - // Sky (no geometry written) — output clear color - if (length(normal) < 0.1) { - FragColor = vec4(0.0, 0.0, 0.0, 0.0); - return; + // Nothing was written here, keep the skybox underneath + if (dot(normal, normal) < 0.01) + discard; + + vec3 FragPos = texture(gPosition, TexCoord).rgb; + vec4 albedoSample = texture(gAlbedo, TexCoord); + vec4 material = texture(gMaterial, TexCoord); + + vec3 albedo = albedoSample.rgb; + float metal = material.r; + float rough = material.g; + float ao = material.b; + + vec3 N = normalize(normal); + vec3 V = normalize(viewPos - FragPos); + float NdotV = max(dot(N, V), 1e-4); + + vec3 F0 = mix(vec3(0.04), albedo, metal); + vec3 direct = vec3(0.0); + + // Facing away from the sun has no direct light, so skip the BRDF and the 9 shadow samples + vec3 sunL = normalize(-dirLight.direction); + if (dot(N, sunL) > 0.0) { + float shadow = ShadowCalculation(lightSpaceMatrix * vec4(FragPos, 1.0), N, sunL); + direct += ShadeLight(sunL, dirLight.color * dirLight.intensity, N, V, albedo, metal, rough, F0, NdotV) * shadow; } - vec3 viewDir = normalize(viewPos - fragPos); - vec3 lit = CalcDirLight(dirLight, normal, viewDir, fragPosLS, albedaSample.rgb); + for (int i = 0; i < directionalLightCount; i++) { + vec3 L = normalize(-directionalLights[i].direction); + vec3 radiance = directionalLights[i].color * directionalLights[i].intensity; + direct += ShadeLight(L, radiance, N, V, albedo, metal, rough, F0, NdotV); + } - FragColor = vec4(lit, albedaSample.a); + for (int i = 0; i < pointLightCount; i++) { + vec3 toLight = pointLights[i].position - FragPos; + float distanceSquared = dot(toLight, toLight); + float radius = pointLights[i].radius; + if (distanceSquared >= radius * radius) + continue; + + vec3 radiance = pointLights[i].color * pointLights[i].intensity * PointAttenuation(distanceSquared, radius); + vec3 L = toLight / sqrt(distanceSquared); + if (dot(N, L) <= 0.0) + continue; + + vec3 lit = ShadeLight(L, radiance, N, V, albedo, metal, rough, F0, NdotV); + if (pointLights[i].shadowTile >= 0) + lit *= LocalShadow(pointLights[i].shadowTile + PointShadowFace(-toLight), FragPos, N); + direct += lit; + } + + for (int i = 0; i < spotLightCount; i++) { + vec3 toLight = spotLights[i].position - FragPos; + float distanceSquared = dot(toLight, toLight); + float radius = spotLights[i].radius; + if (distanceSquared >= radius * radius) + continue; + + vec3 L = toLight / sqrt(distanceSquared); + float cone = smoothstep(spotLights[i].outerCos, spotLights[i].innerCos, dot(-L, spotLights[i].direction)); + if (cone <= 0.0) + continue; + + if (dot(N, L) <= 0.0) + continue; + + vec3 radiance = spotLights[i].color * spotLights[i].intensity * PointAttenuation(distanceSquared, radius) * cone; + vec3 lit = ShadeLight(L, radiance, N, V, albedo, metal, rough, F0, NdotV); + if (spotLights[i].shadowTile >= 0) + lit *= LocalShadow(spotLights[i].shadowTile, FragPos, N); + direct += lit; + } + + vec3 ambientF = FresnelSchlickRoughness(NdotV, F0, rough); + vec3 ambientKD = (vec3(1.0) - ambientF) * (1.0 - metal); + vec3 irradiance = pow(textureLod(environmentMap, N, 7.0).rgb, vec3(2.2)) * envIntensity; + vec3 R = reflect(-V, N); + vec3 prefiltered = pow(textureLod(environmentMap, R, rough * 7.0).rgb, vec3(2.2)) * envIntensity; + vec3 specularIBL = prefiltered * EnvironmentBRDF(F0, rough, NdotV); + vec3 ambient = (ambientKD * irradiance * albedo + specularIBL) * ao; + + vec3 emission = albedo * material.a * 4.0; + + FragColor = vec4(direct + ambient + emission, albedoSample.a); } diff --git a/shaders/gbuffer_debug.frag b/shaders/gbuffer_debug.frag index 544e0e7..9ef3c89 100644 --- a/shaders/gbuffer_debug.frag +++ b/shaders/gbuffer_debug.frag @@ -6,36 +6,46 @@ in vec2 TexCoord; uniform sampler2D gPosition; uniform sampler2D gNormal; uniform sampler2D gAlbedo; -uniform sampler2D gLightSpacePos; -uniform sampler2DShadow shadowMap; +uniform sampler2D gMaterial; uniform sampler2D depthTex; +uniform sampler2D shadowTex; +uniform sampler2D bloomTex; -uniform int debugMode; // 0 = final, 1 = position, 2 = normal, 3 = albedo, 4 = lightspace, 5 = depth +uniform float nearPlane; +uniform float farPlane; + +uniform int debugMode; // 1 = position, 2 = normal, 3 = albedo, 4 = material, 5 = depth, 6 = shadow map, 7 = bloom void main() { + FragColor = vec4(0.0, 0.0, 0.0, 1.0); + if (debugMode == 1) { vec3 v = texture(gPosition, TexCoord).rgb; FragColor = vec4(v * 0.01 + 0.5, 1.0); - return; } - if (debugMode == 2) { - vec3 n = normalize(texture(gNormal, TexCoord).rgb); - FragColor = vec4(n * 0.5 + 0.5, 1.0); - return; + else if (debugMode == 2) { + vec3 n = texture(gNormal, TexCoord).rgb; + if (length(n) >= 0.1) + FragColor = vec4(normalize(n) * 0.5 + 0.5, 1.0); } - if (debugMode == 3) { - FragColor = texture(gAlbedo, TexCoord); - return; + else if (debugMode == 3) { + FragColor = vec4(pow(texture(gAlbedo, TexCoord).rgb, vec3(1.0 / 2.2)), 1.0); } - if (debugMode == 4) { - FragColor = texture(gLightSpacePos, TexCoord); - return; + else if (debugMode == 4) { + FragColor = vec4(texture(gMaterial, TexCoord).rgb, 1.0); } - if (debugMode == 5) { - float d = texture(depthTex, TexCoord).r; + else if (debugMode == 5) { + // Perspective depth is nearly 1.0 everywhere, so linearize it and spread it out + float z = texture(depthTex, TexCoord).r * 2.0 - 1.0; + float linearDepth = (2.0 * nearPlane * farPlane) / (farPlane + nearPlane - z * (farPlane - nearPlane)); + float d = log(1.0 + linearDepth) / log(1.0 + farPlane); FragColor = vec4(vec3(d), 1.0); - return; } - FragColor = vec4(0.0); + else if (debugMode == 6) { + FragColor = vec4(vec3(texture(shadowTex, TexCoord).r), 1.0); + } + else if (debugMode == 7) { + FragColor = vec4(texture(bloomTex, TexCoord).rgb, 1.0); + } } diff --git a/shaders/geometry.frag b/shaders/geometry.frag index 3d2e08c..93c2696 100644 --- a/shaders/geometry.frag +++ b/shaders/geometry.frag @@ -1,23 +1,35 @@ #version 330 core layout (location = 0) out vec3 gPosition; layout (location = 1) out vec3 gNormal; -layout (location = 2) out vec4 gAlbedo; -layout (location = 3) out vec4 gLightSpacePos; +layout (location = 2) out vec4 gAlbedo; // linear rgb + alpha +layout (location = 3) out vec4 gMaterial; // r = metallic, g = roughness, b = ao, a = emissive in vec3 FragPos; in vec3 Normal; in vec2 TexCoord; -in vec4 FragPosLightSpace; uniform sampler2D tex; +uniform vec4 colorOverride = vec4(0.0); +uniform int useTexture = 0; +uniform float metallic = 0.0; +uniform float roughness = 0.7; +uniform float ao = 1.0; +uniform float emissive = 0.0; void main() { - vec4 albedo = texture(tex, TexCoord); - if (albedo.a < 0.1) discard; + vec4 albedo = vec4(0.8, 0.8, 0.8, 1.0); + if (useTexture != 0) { + albedo = texture(tex, TexCoord); + if (albedo.a < 0.1) + discard; + albedo.rgb = pow(albedo.rgb, vec3(2.2)); + } - gPosition = FragPos; - gNormal = normalize(Normal); - gAlbedo = albedo; - gLightSpacePos = FragPosLightSpace; + albedo.rgb = mix(albedo.rgb, colorOverride.rgb, colorOverride.w); + + gPosition = FragPos; + gNormal = normalize(Normal); + gAlbedo = albedo; + gMaterial = vec4(clamp(metallic, 0.0, 1.0), clamp(roughness, 0.04, 1.0), ao, clamp(emissive, 0.0, 1.0)); } diff --git a/shaders/geometry.vert b/shaders/geometry.vert index d928fca..72d0283 100644 --- a/shaders/geometry.vert +++ b/shaders/geometry.vert @@ -6,19 +6,39 @@ layout (location = 2) in vec2 aTexCoord; out vec3 FragPos; out vec3 Normal; out vec2 TexCoord; -out vec4 FragPosLightSpace; uniform mat4 proj; uniform mat4 view; uniform mat4 modl; -uniform mat4 lightSpaceMatrix; + +uniform int uvMode = 0; // 0 = stretch, 1 = tile +uniform vec3 tileScale = vec3(1.0); +uniform vec2 uvOffset = vec2(0.0); + +// Same direction as transpose(inverse(m)) for normals, without the per vertex inverse +mat3 CofactorMatrix(mat3 m) +{ + return mat3(cross(m[1], m[2]), cross(m[2], m[0]), cross(m[0], m[1])); +} + +// Tiling ignores the mesh's uvs and projects from whichever axis the surface faces, +// so every face of a stretched cube repeats at the same size +vec2 SurfaceUV() +{ + if (uvMode == 0) return aTexCoord + uvOffset; + + vec3 n = abs(aNormal); + vec3 p = aPos * tileScale; + if (n.y >= n.x && n.y >= n.z) return p.xz + uvOffset; + if (n.x >= n.z) return p.zy + uvOffset; + return p.xy + uvOffset; +} void main() { vec4 worldPos = modl * vec4(aPos, 1.0); FragPos = worldPos.xyz; - Normal = normalize(mat3(transpose(inverse(modl))) * aNormal); - TexCoord = aTexCoord; - FragPosLightSpace = lightSpaceMatrix * worldPos; + Normal = normalize(CofactorMatrix(mat3(modl)) * aNormal); + TexCoord = SurfaceUV(); gl_Position = proj * view * worldPos; } diff --git a/shaders/geometry_rig.vert b/shaders/geometry_rig.vert index 1bc930d..8cc0e18 100644 --- a/shaders/geometry_rig.vert +++ b/shaders/geometry_rig.vert @@ -8,27 +8,60 @@ layout (location = 4) in vec4 weights; out vec3 FragPos; out vec3 Normal; out vec2 TexCoord; -out vec4 FragPosLightSpace; uniform mat4 proj; uniform mat4 view; uniform mat4 modl; -uniform mat4 lightSpaceMatrix; -uniform mat4 finalBonesMatrices[100]; + +const int MAX_BONES = 100; +const int MAX_BONE_INFLUENCE = 4; +uniform mat4 finalBonesMatrices[MAX_BONES]; + +uniform int uvMode = 0; // 0 = stretch, 1 = tile +uniform vec3 tileScale = vec3(1.0); +uniform vec2 uvOffset = vec2(0.0); + +// Same direction as transpose(inverse(m)) for normals, without the per vertex inverse +mat3 CofactorMatrix(mat3 m) +{ + return mat3(cross(m[1], m[2]), cross(m[2], m[0]), cross(m[0], m[1])); +} + +// Tiling ignores the mesh's uvs and projects from whichever axis the surface faces. +// Done on the bind pose, so the pattern stays stuck to the character as it moves. +vec2 SurfaceUV() +{ + if (uvMode == 0) return aTexCoord + uvOffset; + + vec3 n = abs(aNormal); + vec3 p = aPos * tileScale; + if (n.y >= n.x && n.y >= n.z) return p.xz + uvOffset; + if (n.x >= n.z) return p.zy + uvOffset; + return p.xy + uvOffset; +} void main() { - mat4 boneTransform = finalBonesMatrices[boneIds[0]] * weights[0]; - boneTransform += finalBonesMatrices[boneIds[1]] * weights[1]; - boneTransform += finalBonesMatrices[boneIds[2]] * weights[2]; - boneTransform += finalBonesMatrices[boneIds[3]] * weights[3]; + vec4 totalPosition = vec4(0.0); + vec3 totalNormal = vec3(0.0); - vec4 localPos = boneTransform * vec4(aPos, 1.0); - vec4 worldPos = modl * localPos; + for (int i = 0; i < MAX_BONE_INFLUENCE; i++) + { + if (boneIds[i] == -1) continue; + if (boneIds[i] >= MAX_BONES) + { + totalPosition = vec4(aPos, 1.0); + totalNormal = aNormal; + break; + } + totalPosition += (finalBonesMatrices[boneIds[i]] * vec4(aPos, 1.0)) * weights[i]; + totalNormal += mat3(finalBonesMatrices[boneIds[i]]) * aNormal * weights[i]; + } - FragPos = worldPos.xyz; - Normal = normalize(mat3(transpose(inverse(modl * boneTransform))) * aNormal); - TexCoord = aTexCoord; - FragPosLightSpace = lightSpaceMatrix * worldPos; - gl_Position = proj * view * worldPos; -} \ No newline at end of file + vec4 worldPos = modl * totalPosition; + + FragPos = worldPos.xyz; + Normal = normalize(CofactorMatrix(mat3(modl)) * totalNormal); + TexCoord = SurfaceUV(); + gl_Position = proj * view * worldPos; +} diff --git a/shaders/rig.vert b/shaders/rig.vert index 1df9cb4..bfa8beb 100644 --- a/shaders/rig.vert +++ b/shaders/rig.vert @@ -20,7 +20,30 @@ out vec3 normal; out vec2 texCoord; out vec3 FragPos; out vec4 fragposlightspace; - + +uniform int uvMode = 0; // 0 = stretch, 1 = tile +uniform vec3 tileScale = vec3(1.0); +uniform vec2 uvOffset = vec2(0.0); + +// Same direction as transpose(inverse(m)) for normals, without the per vertex inverse +mat3 CofactorMatrix(mat3 m) +{ + return mat3(cross(m[1], m[2]), cross(m[2], m[0]), cross(m[0], m[1])); +} + +// Tiling ignores the mesh's uvs and projects from whichever axis the surface faces. +// Done on the bind pose, so the pattern stays stuck to the character as it moves. +vec2 SurfaceUV() +{ + if (uvMode == 0) return tex + uvOffset; + + vec3 n = abs(norm); + vec3 p = pos * tileScale; + if (n.y >= n.x && n.y >= n.z) return p.xz + uvOffset; + if (n.x >= n.z) return p.zy + uvOffset; + return p.xy + uvOffset; +} + void main() { vec4 totalPosition = vec4(0.0); @@ -40,9 +63,10 @@ void main() totalPosition += (finalBonesMatrices[boneIds[i]] * vec4(pos, 1.0))* weights[i]; totalNormal += mat3(finalBonesMatrices[boneIds[i]]) * norm * weights[i]; } - FragPos = vec3(modl * totalPosition); - fragposlightspace = lightSpaceMatrix * vec4(FragPos, 1.0); - gl_Position = proj * view * modl * totalPosition; - normal = normalize(mat3(transpose(inverse(modl))) * totalNormal); - texCoord = tex; + vec4 worldPos = modl * totalPosition; + FragPos = worldPos.xyz; + fragposlightspace = lightSpaceMatrix * worldPos; + gl_Position = proj * view * worldPos; + normal = normalize(CofactorMatrix(mat3(modl)) * totalNormal); + texCoord = SurfaceUV(); } \ No newline at end of file diff --git a/shaders/skybox.vert b/shaders/skybox.vert index 8f8663a..fb2f7f4 100644 --- a/shaders/skybox.vert +++ b/shaders/skybox.vert @@ -10,6 +10,5 @@ void main() { TexCoords = aPos; vec4 pos = proj * view * vec4(aPos, 1.0); - //gl_Position = pos.xyww - gl_Position = pos; + gl_Position = pos.xyww; // sits on the far plane, so it only draws where no geometry is } \ No newline at end of file diff --git a/t.h b/t.h index 9eefb57..22e4979 100644 --- a/t.h +++ b/t.h @@ -30,31 +30,18 @@ public: return tr; } - BoundingBox GetAABB() { - std::vector corners = { - {0.5f,0.5f,0.5f}, - {0.5f,-0.5f,0.5f}, - {0.5f,-0.5f,-0.5f}, - {0.5f,0.5f,-0.5f}, - {-0.5f,0.5f,0.5f}, - {-0.5f,-0.5f,0.5f}, - {-0.5f,0.5f,-0.5f}, - {-0.5f,-0.5f,-0.5f}, - }; + BoundingBox GetAABB() const { + // Each world axis reaches as far as the rotated box's axes add up to, no need to visit the corners + const glm::mat3 rotation = glm::mat3_cast(orient); + const glm::vec3 half = scale * 0.5f; + const glm::vec3 extent = + glm::abs(rotation[0]) * half.x + + glm::abs(rotation[1]) * half.y + + glm::abs(rotation[2]) * half.z; - glm::vec3 min(FLT_MAX); - glm::vec3 max(-FLT_MAX); - for (glm::vec3& corner : corners) { - corner *= scale; - corner = orient * corner; - corner += pos; - min = { glm::min(min.x, corner.x) ,glm::min(min.y, corner.y), glm::min(min.z, corner.z) }; - max = { glm::max(max.x, corner.x) ,glm::max(max.y, corner.y), glm::max(max.z, corner.z) }; - } BoundingBox tr; - tr.min = min; - tr.max = max; - //std::cout << tr.min.x << ' ' << tr.min.y << ' ' << tr.min.z << ' ' << tr.max.x << ' ' << tr.max.y << ' ' << tr.max.z << '\n'; + tr.min = pos - extent; + tr.max = pos + extent; return tr; } diff --git a/tFunctions.cpp b/tFunctions.cpp index 6752f3f..fdb0613 100644 --- a/tFunctions.cpp +++ b/tFunctions.cpp @@ -5,6 +5,45 @@ glm::vec3 CalculateTriangleNormal(Triangle t) { return glm::normalize(glm::cross(t.b - t.a, t.c - t.a)); } +static bool TriangleAxisSeparates(const Triangle& t1, const Triangle& t2, const glm::vec3& axis) { + const float a1 = glm::dot(t1.a, axis), b1 = glm::dot(t1.b, axis), c1 = glm::dot(t1.c, axis); + const float a2 = glm::dot(t2.a, axis), b2 = glm::dot(t2.b, axis), c2 = glm::dot(t2.c, axis); + const float min1 = glm::min(a1, glm::min(b1, c1)), max1 = glm::max(a1, glm::max(b1, c1)); + const float min2 = glm::min(a2, glm::min(b2, c2)), max2 = glm::max(a2, glm::max(b2, c2)); + return max1 < min2 || max2 < min1; +} + +//SEPARATING AXIS TEST, BOTH NORMALS PLUS EVERY EDGE PAIR +bool TriangleInTriangle(const Triangle& t1, const Triangle& t2) { + const glm::vec3 edges1[3] = { t1.b - t1.a, t1.c - t1.b, t1.a - t1.c }; + const glm::vec3 edges2[3] = { t2.b - t2.a, t2.c - t2.b, t2.a - t2.c }; + const glm::vec3 normal1 = glm::cross(edges1[0], edges1[1]); + const glm::vec3 normal2 = glm::cross(edges2[0], edges2[1]); + + if (TriangleAxisSeparates(t1, t2, normal1) || TriangleAxisSeparates(t1, t2, normal2)) + return false; + + bool coplanar = true; + for (const glm::vec3& edge1 : edges1) { + for (const glm::vec3& edge2 : edges2) { + const glm::vec3 axis = glm::cross(edge1, edge2); + if (glm::dot(axis, axis) < 1e-14f) continue; + coplanar = false; + if (TriangleAxisSeparates(t1, t2, axis)) return false; + } + } + + //EDGE PAIRS GIVE NOTHING FOR COPLANAR TRIANGLES, SO TEST WITHIN THE PLANE + if (coplanar) { + for (int i = 0; i < 3; i++) { + if (TriangleAxisSeparates(t1, t2, glm::cross(normal1, edges1[i]))) return false; + if (TriangleAxisSeparates(t1, t2, glm::cross(normal1, edges2[i]))) return false; + } + } + + return true; +} + float Magnitude2(const glm::vec3& v) { return v.x*v.x + v.y*v.y + v.z*v.z; } diff --git a/tFunctions.h b/tFunctions.h index 54b2d13..da26f86 100644 --- a/tFunctions.h +++ b/tFunctions.h @@ -28,6 +28,7 @@ struct AxisCollisionCN { }; glm::vec3 CalculateTriangleNormal(Triangle t); +bool TriangleInTriangle(const Triangle& t1, const Triangle& t2); float Magnitude2(const glm::vec3& v); diff --git a/texture.cpp b/texture.cpp index 0a1b97f..8ccb59e 100644 --- a/texture.cpp +++ b/texture.cpp @@ -1,51 +1,61 @@ #include "texture.h" +#include + +#include "stb_image.h" + Texture::Texture(const char* filename) : path(filename) { - int widthImg, heightImg, nrComponents; + int nrComponents; stbi_set_flip_vertically_on_load(true); - unsigned char* bytes = stbi_load(filename, &widthImg, &heightImg, &nrComponents, 0); + unsigned char* bytes = stbi_load(filename, &width, &height, &nrComponents, 4); if (not bytes) - { - std::cout << "Failed to load texture" << std::endl; - throw; - } + throw std::runtime_error("Failed to load texture: " + path); + + pixels.assign(bytes, bytes + (size_t)width * height * 4); + stbi_image_free(bytes); + + CreateGLTexture(); +} + +Texture::Texture(int newWidth, int newHeight) : width(newWidth), height(newHeight) +{ + pixels.assign((size_t)width * height * 4, 255); + CreateGLTexture(); +} + +Texture::Texture(const Texture& source) + : path(source.path), width(source.width), height(source.height), pixels(source.pixels) +{ + CreateGLTexture(); +} + +void Texture::CreateGLTexture() +{ glGenTextures(1, &ID); glBindTexture(GL_TEXTURE_2D, ID); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + // Mipmaps when shrunk, otherwise tiled textures shimmer in the distance + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); - GLenum format; - if (nrComponents == 1) - format = GL_RED; - else if (nrComponents == 3) - format = GL_RGB; - else if (nrComponents == 4) - format = GL_RGBA; - - glTexImage2D(GL_TEXTURE_2D, 0, format, widthImg, heightImg, 0, format, GL_UNSIGNED_BYTE, bytes); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); glGenerateMipmap(GL_TEXTURE_2D); - stbi_image_free(bytes); glBindTexture(GL_TEXTURE_2D, 0); } -void Texture::texUnit(Shader shader, const char* uniform, GLuint unit) { - shader.SetInt(uniform,unit); -} - -void Texture::Bind() { +void Texture::Upload() +{ glBindTexture(GL_TEXTURE_2D, ID); -} - -void Texture::Unbind() { + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); + glGenerateMipmap(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, 0); } void Texture::Delete() { glDeleteTextures(1, &ID); -} \ No newline at end of file +} diff --git a/texture.h b/texture.h index 9c17033..3a60908 100644 --- a/texture.h +++ b/texture.h @@ -2,20 +2,31 @@ #ifndef TEXTURE_CLASS_H #define TEXTURE_CLASS_H -#include "stb_image.h" +#include +#include + #include -#include "shaderClass.h" - +// A loaded image and nothing else. Game elements wear one through a SurfaceTexture, +// get these from the ResourceManager. class Texture { public: GLuint ID; std::string path; + int width = 0; + int height = 0; + std::vector pixels; // RGBA, row 0 is the bottom of the image + Texture(const char* filename); - void texUnit(Shader shader, const char* uniform, GLuint unit); - void Bind(); - void Unbind(); + Texture(int newWidth, int newHeight); // blank white + Texture(const Texture& source); // an independent copy with its own GL texture + + // Sends pixels to the GPU after they've been edited + void Upload(); void Delete(); + +private: + void CreateGLTexture(); }; #endif