From 7b32ad674e075ff18bd9ccf6e2554788f3ce008f Mon Sep 17 00:00:00 2001 From: johnruina Date: Mon, 2 Nov 2026 19:35:36 -0500 Subject: [PATCH] stuff --- .gitignore | 1 + Character.cpp | 107 ++--- Character.h | 44 +- CharacterManager.cpp | 32 -- CharacterManager.h | 2 - CollisionMesh.cpp | 128 ++++++ CollisionMesh.h | 41 ++ Engine.cpp | 705 +++++++++++++++++++++------------ Gui.h | 12 +- Light.h | 48 +++ Lighting.h | 52 ++- Mesh.h | 22 +- Net.cpp | 626 ----------------------------- Net.h | 115 ------ Particle.h | 19 +- Physics.cpp | 442 ++++++++++++++++++++- Physics.h | 116 ++++-- Ragdoll.cpp | 329 +++++++++++++++ Ragdoll.h | 48 +++ RenderSystem.cpp | 577 ++++++++++++++++++++++++--- RenderSystem.h | 51 +++ ResourceManager.cpp | 69 ++++ ResourceManager.h | 31 +- SurfaceTexture.h | 57 +++ TexturePainter.h | 71 ++++ eiifwhrn.vcxproj | 14 +- eiifwhrn.vcxproj.filters | 36 ++ main.cpp | 1 + patch2.txt | 0 patch_temp.txt | 0 shaderClass.h | 39 +- shaders/Box.frag | 6 +- shaders/ImageBox.frag | 6 +- shaders/ImageBox.vert | 8 +- shaders/Particle.vert | 6 +- shaders/ScreenShader.frag | 12 +- shaders/bloom_blur.frag | 23 ++ shaders/bloom_extract.frag | 26 ++ shaders/default.frag | 163 +++++++- shaders/default.vert | 34 +- shaders/deferred_lighting.frag | 288 ++++++++++++-- shaders/gbuffer_debug.frag | 46 ++- shaders/geometry.frag | 30 +- shaders/geometry.vert | 30 +- shaders/geometry_rig.vert | 63 ++- shaders/rig.vert | 36 +- shaders/skybox.vert | 3 +- t.h | 33 +- tFunctions.cpp | 39 ++ tFunctions.h | 1 + texture.cpp | 62 +-- texture.h | 23 +- 52 files changed, 3285 insertions(+), 1488 deletions(-) create mode 100644 .gitignore create mode 100644 CollisionMesh.cpp create mode 100644 CollisionMesh.h create mode 100644 Light.h delete mode 100644 Net.cpp delete mode 100644 Net.h create mode 100644 Ragdoll.cpp create mode 100644 Ragdoll.h create mode 100644 ResourceManager.cpp create mode 100644 SurfaceTexture.h create mode 100644 TexturePainter.h create mode 100644 patch2.txt create mode 100644 patch_temp.txt create mode 100644 shaders/bloom_blur.frag create mode 100644 shaders/bloom_extract.frag 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