This commit is contained in:
2026-11-02 19:35:36 -05:00
parent a6103e453d
commit 7b32ad674e
52 changed files with 3285 additions and 1488 deletions
+1
View File
@@ -0,0 +1 @@
.vs
+37 -70
View File
@@ -1,7 +1,9 @@
#include "Character.h" #include "Character.h"
#include "Animator.h" #include "Animator.h"
#include "Ragdoll.h"
#include "tFunctions.h" #include "tFunctions.h"
#include <cctype> #include <cctype>
#include <cfloat>
#include <cmath> #include <cmath>
#include <glm/gtc/quaternion.hpp> #include <glm/gtc/quaternion.hpp>
@@ -89,46 +91,6 @@ void Character::TurnToDirection(float dt) {
body->t.RotateToQuaternion(glm::normalize(glm::slerp(current, target, blend))); 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) { void Character::SetWalking(bool walking) {
if (body && body->animator) if (body && body->animator)
body->animator->playbackSpeed = walking ? 1.0f : 0.0f; body->animator->playbackSpeed = walking ? 1.0f : 0.0f;
@@ -146,24 +108,38 @@ glm::vec3 Character::GetForwardDirection() const {
return forward / length; 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) 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(); // The rig is about 2 units across natively in every direction it reaches, arms out included.
forward.y = 0.0f; // A cube that size around the body covers any pose and any turn.
if (glm::length(forward) <= 0.0001f) const glm::vec3 reach(visualScale);
return; bounds.min = body->t.GetTranslation() - reach;
forward = glm::normalize(forward); bounds.max = body->t.GetTranslation() + reach;
return true;
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;
} }
Character::Character() { Character::Character() {
@@ -174,24 +150,15 @@ Character::Character() {
void Character::Render(Shader& shader) { void Character::Render(Shader& shader) {
if (!body || !body->rig || !body->animator) return; if (!body || !body->rig || !body->animator) return;
if (body->animator->m_CurrentAnimation == nullptr) { const std::vector<glm::mat4> transforms = ragdoll ? ragdoll->GetBoneMatrices()
for (int i = 0; i < 100; i++) : body->animator->m_CurrentAnimation ? body->GetBlendedBoneMatrices()
shader.SetMat4(("finalBonesMatrices[" + std::to_string(i) + "]").c_str(), glm::mat4(1.0f)); : std::vector<glm::mat4>(100, glm::mat4(1.0f));
} shader.SetMat4Array("finalBonesMatrices", transforms.data(), glm::min((int)transforms.size(), 100));
else {
std::vector<glm::mat4> transforms = body->GetBlendedBoneMatrices();
for (int i = 0; i < transforms.size(); i++)
shader.SetMat4(("finalBonesMatrices[" + std::to_string(i) + "]").c_str(), transforms[i]);
}
shader.Set4F("colorOverride", colorOverride); shader.Set4F("colorOverride", colorOverride);
shader.SetInt("useTexture", 0); surface.Apply(shader, 1, glm::vec3(visualScale));
shader.SetInt("tex", 1);
shader.Set1F("metallic", metallic); shader.Set1F("metallic", metallic);
shader.Set1F("roughness", roughness); shader.Set1F("roughness", roughness);
shader.Set1F("ao", ao); 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, ragdoll ? glm::mat4(1.0f) : GetRigMatrix()); // ragdoll bones are already in world space
body->rig->Render(shader, body->t.GetTranslationMatrix() * body->t.GetRotationMatrix()
* glm::scale(glm::mat4(1.0f), glm::vec3(visualScale)));
shader.Set4F("colorOverride", glm::vec4(0.0f)); // don't leak into other rig draws shader.Set4F("colorOverride", glm::vec4(0.0f)); // don't leak into other rig draws
} }
+8 -36
View File
@@ -14,22 +14,8 @@ class Character;
class CharacterManager; class CharacterManager;
class CharacterBody; class CharacterBody;
class BodyComponent; class BodyComponent;
class Ragdoll;
struct Hitbox { class Physics;
t transform;
int damage = 0;
float knockback = 0.0f;
bool active = false;
std::set<Character*> hitTargets;
bool CanHit(Character* target) const {
return target && hitTargets.find(target) == hitTargets.end();
}
void RegisterHit(Character* target) {
if (target) hitTargets.insert(target);
}
};
class Action { class Action {
public: public:
@@ -52,7 +38,6 @@ public:
class CharacterBody : public t_package { class CharacterBody : public t_package {
public: public:
int Health = 100;
Rig* rig = nullptr; Rig* rig = nullptr;
std::vector<Animator*> animators; std::vector<Animator*> animators;
@@ -135,16 +120,14 @@ class Character : public Object, public Renderable {
public: public:
CharacterMind* mind = nullptr; CharacterMind* mind = nullptr;
CharacterBody* body = 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) 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 visualScale = 1.4f; // rig is ~1.8 tall natively; hitbox is 2.5
float metallic = 0.0f; float metallic = 0.0f;
float roughness = 0.7f; float roughness = 0.7f;
float ao = 1.0f; float ao = 1.0f;
float turnSpeed = 12.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 PressInput(char letter);
void ReleaseInput(char letter); void ReleaseInput(char letter);
@@ -152,37 +135,26 @@ public:
glm::vec3 GetMovementDirection(const glm::vec3& forward, const glm::vec3& right) const; glm::vec3 GetMovementDirection(const glm::vec3& forward, const glm::vec3& right) const;
void SetTurnDirection(const glm::vec3& direction); void SetTurnDirection(const glm::vec3& direction);
const glm::vec3& GetTurnDirection() const { return turnDirection; } 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); void SetWalking(bool walking);
glm::vec3 GetForwardDirection() const; glm::vec3 GetForwardDirection() const;
const Hitbox& GetPunchHitbox() const { return punchHitbox; } void EnableRagdoll(Physics& physics);
Character(); Character();
void Render(Shader& shader) override; void Render(Shader& shader) override;
bool GetWorldBounds(BoundingBox& bounds) const override;
void Step(float dt) { void Step(float dt) {
if (punchLookLocked && punchAnimator && punchAnimator->isPlaying) if (ragdoll) return;
turnDirection = punchLookDirection;
TurnToDirection(dt); TurnToDirection(dt);
punchHitbox.active = false;
body->Step(dt); body->Step(dt);
} }
private: private:
friend class CharacterManager;
void TurnToDirection(float dt); void TurnToDirection(float dt);
void OnPunchAnimationEnd(); glm::mat4 GetRigMatrix() const;
std::set<char> inputLetters; std::set<char> inputLetters;
glm::vec3 turnDirection = glm::vec3(0.0f); 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 #endif
-32
View File
@@ -1,9 +1,7 @@
#include "CharacterManager.h" #include "CharacterManager.h"
#include "Animator.h" #include "Animator.h"
#include "Character.h" #include "Character.h"
#include "Physics.h"
#include "Rig.h" #include "Rig.h"
#include "tFunctions.h"
CharacterManager::CharacterManager() = default; CharacterManager::CharacterManager() = default;
CharacterManager::~CharacterManager() = default; CharacterManager::~CharacterManager() = default;
@@ -89,33 +87,3 @@ void CharacterManager::Step(float dt) {
for (Character* character : characterOrder) for (Character* character : characterOrder)
character->Step(dt); 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();
}
}
}
}
-2
View File
@@ -7,7 +7,6 @@
#include <vector> #include <vector>
class Character; class Character;
class Physics;
class Rig; class Rig;
class Animation; class Animation;
@@ -33,7 +32,6 @@ public:
void ClearInput(); void ClearInput();
void Step(float dt); void Step(float dt);
void ResolveCombat(Physics& physics);
const std::vector<Character*>& GetCharacters() const { return characterOrder; } const std::vector<Character*>& GetCharacters() const { return characterOrder; }
+128
View File
@@ -0,0 +1,128 @@
#include "CollisionMesh.h"
#include <algorithm>
#include <cfloat>
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<Triangle>& 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<int>& 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<Triangle> 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;
}
+41
View File
@@ -0,0 +1,41 @@
#ifndef COLLISION_MESH_CLASS
#define COLLISION_MESH_CLASS
#include <vector>
#include <glm/glm.hpp>
#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<Triangle>& meshTriangles);
// The 12 triangles of a unit cube, used for any body without its own mesh
static const CollisionMesh& UnitBox();
const std::vector<Triangle>& 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<int>& 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<Triangle> triangles;
std::vector<Node> nodes;
};
#endif
+443 -262
View File
@@ -68,12 +68,14 @@
#include "Sound.h" #include "Sound.h"
#include "Gui.h" #include "Gui.h"
#include "font.h" #include "font.h"
#include "TexturePainter.h"
// Character / animation // Character / animation
#include "Rig.h" #include "Rig.h"
#include "Animator.h" #include "Animator.h"
#include "Character.h" #include "Character.h"
#include "CharacterManager.h" #include "CharacterManager.h"
#include "Ragdoll.h"
#include "DefaultBodyComponents.h" #include "DefaultBodyComponents.h"
#include "BodyComponentActions.h" #include "BodyComponentActions.h"
@@ -108,6 +110,12 @@ Engine::Engine()
rendersystem->RigShader = new Shader("shaders/rig.vert", "shaders/default.frag"); rendersystem->RigShader = new Shader("shaders/rig.vert", "shaders/default.frag");
rendersystem->MeshShader = new Shader("shaders/default.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->MeshShadowShader = new Shader("shaders/meshshadow.vert", "shaders/shadow.frag");
rendersystem->RigShadowShader = new Shader("shaders/rigshadow.vert", "shaders/shadow.frag"); rendersystem->RigShadowShader = new Shader("shaders/rigshadow.vert", "shaders/shadow.frag");
rendersystem->SkyboxShader = new Shader("shaders/skybox.vert", "shaders/skybox.frag"); rendersystem->SkyboxShader = new Shader("shaders/skybox.vert", "shaders/skybox.frag");
@@ -124,14 +132,12 @@ Engine::Engine()
// ============================================================================ // ============================================================================
void Engine::Initiate() void Engine::Initiate()
{ {
Font* arial = new Font("assets/ft/arial.ttf"); Font* arial = resourcemanager->GetFont("assets/ft/arial.ttf");
Texture* blanktex = new Texture("assets/blanktexture.png"); Texture* blanktex = resourcemanager->GetTexture("assets/blanktexture.png");
Texture* snowflake = new Texture("assets/snowflake.png");
charactermanager = new CharacterManager(); charactermanager = new CharacterManager();
charactermanager->LoadRig("humanoid", "assets/rig.glb"); charactermanager->LoadRig("humanoid", "assets/rig.glb");
charactermanager->LoadAnimation("walk", "assets/walkanim.glb", "humanoid"); charactermanager->LoadAnimation("walk", "assets/walkanim.glb", "humanoid");
charactermanager->LoadAnimation("punch", "assets/punchanim.glb", "humanoid");
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// Player character // Player character
@@ -144,7 +150,6 @@ void Engine::Initiate()
testcharacter.body->t.ScaleBy({ 0.7f, 2.5f, 0.7f }); testcharacter.body->t.ScaleBy({ 0.7f, 2.5f, 0.7f });
testcharacter.body->t.TranslateTo({ 0.0f, 1.5f, 0.0f }); testcharacter.body->t.TranslateTo({ 0.0f, 1.5f, 0.0f });
testcharacter.body->animator->loops = true; testcharacter.body->animator->loops = true;
testcharacter.SetPunchAnimation(charactermanager->GetAnimation("punch"));
testcharacter.BindToRenderSystem(); testcharacter.BindToRenderSystem();
p* characterBody = new p(testcharacter.body); p* characterBody = new p(testcharacter.body);
@@ -163,39 +168,11 @@ void Engine::Initiate()
npc.roughness = 0.5f; npc.roughness = 0.5f;
npc.body->t.ScaleBy({ 0.7f, 2.5f, 0.7f }); npc.body->t.ScaleBy({ 0.7f, 2.5f, 0.7f });
npc.body->t.TranslateTo({ 3.0f, 1.25f, 3.0f }); npc.body->t.TranslateTo({ 3.0f, 1.25f, 3.0f });
npc.SetPunchAnimation(charactermanager->GetAnimation("punch"));
npc.SetPunchLooping(true);
npc.Punch();
npc.BindToRenderSystem(); 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); testcharacter.SetWalking(false);
npc.SetWalking(false); npc.SetWalking(false);
npc.EnableRagdoll(*physicsengine);
// ------------------------------------------------------------------------
// 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);
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// 3D audio // 3D audio
@@ -221,149 +198,272 @@ void Engine::Initiate()
fpsCounter->t2d.size = { 0.1f, 0.1f, 0.0f, 0.0f }; fpsCounter->t2d.size = { 0.1f, 0.1f, 0.0f, 0.0f };
fpsCounter->BindToRenderSystem(); 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 { struct MenuButton {
Box* box; Box* box;
Box* accent;
TextBox* label; TextBox* label;
glm::vec3 color; 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 menuScreen = MenuScreen::Main;
MenuScreen settingsReturn = MenuScreen::Main; MenuScreen settingsReturn = MenuScreen::Main;
bool menuActive = true; bool menuActive = true;
bool prevMouseDown = false; bool prevMouseDown = false;
std::vector<Box*> menuItems; std::vector<Box*> 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(); Box* menuVeil = new Box();
menuVeil->Color = { 0.02f, 0.03f, 0.05f }; 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.position = { 0.0f, 0.0f, 0.0f, 0.0f };
menuVeil->t2d.size = { 1.0f, 1.0f, 0.0f, 0.0f }; menuVeil->t2d.size = { 1.0f, 1.0f, 0.0f, 0.0f };
menuVeil->BindToRenderSystem(); menuVeil->BindToRenderSystem();
menuItems.push_back(menuVeil); menuItems.push_back(menuVeil);
TextBox* menuTitle = new TextBox(); Box* menuPanel = new Box();
menuTitle->font = arial; menuPanel->Color = { 0.03f, 0.04f, 0.06f };
menuTitle->text = "EIIFWHRN"; menuPanel->t2d.position = { 0.0f, 0.0f, 0.0f, 0.0f };
menuTitle->fontsize = 3.0f; menuPanel->t2d.size = { 0.0f, 1.0f, menuPanelWidth, 0.0f };
menuTitle->Color = { 0.95f, 0.97f, 1.0f }; menuPanel->BindToRenderSystem();
menuTitle->t2d.position = { 0.5f, 0.17f, 0.0f, 0.0f }; menuItems.push_back(menuPanel);
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);
TextBox* menuSubtitle = new TextBox(); Box* menuPanelEdge = new Box();
menuSubtitle->font = arial; menuPanelEdge->Color = menuAccent;
menuSubtitle->text = "MAIN MENU"; menuPanelEdge->t2d.position = { 0.0f, 0.0f, menuPanelWidth, 0.0f };
menuSubtitle->fontsize = 0.65f; menuPanelEdge->t2d.size = { 0.0f, 1.0f, 3.0f, 0.0f };
menuSubtitle->Color = { 0.55f, 0.65f, 0.78f }; menuPanelEdge->BindToRenderSystem();
menuSubtitle->t2d.position = { 0.5f, 0.285f, 0.0f, 0.0f }; menuItems.push_back(menuPanelEdge);
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);
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(); TextBox* label = new TextBox();
label->font = arial; label->font = arial;
label->text = text; label->text = text;
label->fontsize = fontsize; label->fontsize = fontsize;
label->Color = { 0.95f, 0.97f, 1.0f }; label->Color = color;
label->t2d.position = { x, y, 0.0f, 0.0f }; label->t2d.position = { 0.0f, 0.5f, x, y };
label->t2d.center = { 0.5f, 0.5f }; label->t2d.center = { 0.0f, 0.5f };
label->t2d.size = { 0.0f, 0.0f, width, height }; label->t2d.size = { 0.0f, 0.0f, width, height };
label->textCenter = { 0.5f, 0.5f }; label->textCenter = { alignX, 0.5f };
label->BindToRenderSystem(); label->BindToRenderSystem();
menuItems.push_back(label); menuItems.push_back(label);
return label;
return MenuButton{ box, label, color };
}; };
MenuButton playButton = MakeMenuButton("PLAY", 0.5f, 0.46f, glm::vec3(0.12f, 0.30f, 0.22f)); TextBox* menuTitle = MakeMenuText("EIIFWHRN", menuMargin, -235.0f, 400.0f, 100.0f, 1.6f, menuText);
MenuButton settingsButton = MakeMenuButton("SETTINGS", 0.5f, 0.58f, glm::vec3(0.15f, 0.23f, 0.34f)); TextBox* menuSubtitle = MakeMenuText("MAIN MENU", menuMargin, -165.0f, menuContentWidth, 36.0f, 0.5f, menuAccent);
MenuButton quitButton = MakeMenuButton("QUIT", 0.5f, 0.70f, glm::vec3(0.17f, 0.18f, 0.22f));
MenuButton sensitivityDown = MakeMenuButton("- SENS", 0.40f, 0.47f, auto MakeMenuButton = [&](const char* text, float x, float y, float width, float height,
glm::vec3(0.15f, 0.23f, 0.34f), 170.0f, 50.0f, 0.65f); float fontsize, glm::vec3 color, glm::vec3 hoverColor,
MenuButton sensitivityUp = MakeMenuButton("+ SENS", 0.60f, 0.47f, float opacity = 0.6f, bool compact = false) {
glm::vec3(0.15f, 0.23f, 0.34f), 170.0f, 50.0f, 0.65f); Box* box = new Box();
MenuButton fovDown = MakeMenuButton("- FOV", 0.40f, 0.58f, box->Color = color;
glm::vec3(0.15f, 0.23f, 0.34f), 170.0f, 50.0f, 0.65f); box->t2d.position = { 0.0f, 0.5f, x, y };
MenuButton fovUp = MakeMenuButton("+ FOV", 0.60f, 0.58f, box->t2d.center = { 0.0f, 0.5f };
glm::vec3(0.15f, 0.23f, 0.34f), 170.0f, 50.0f, 0.65f); box->t2d.size = { 0.0f, 0.0f, width, height };
MenuButton hitboxToggle = MakeMenuButton("HITBOXES: OFF", 0.5f, 0.68f, box->rounding = 0.15f;
glm::vec3(0.20f, 0.22f, 0.28f), 340.0f, 50.0f, 0.65f); box->BindToRenderSystem();
MenuButton resetButton = MakeMenuButton("RESET DEFAULTS", 0.5f, 0.77f, menuItems.push_back(box);
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);
TextBox* settingsInfo = new TextBox(); Box* accent = new Box();
settingsInfo->font = arial; accent->Color = menuAccent;
settingsInfo->fontsize = 0.65f; accent->t2d.position = { 0.0f, 0.5f, x, y };
settingsInfo->Color = { 0.85f, 0.90f, 0.97f }; accent->t2d.center = { 0.0f, 0.5f };
settingsInfo->t2d.position = { 0.5f, 0.37f, 0.0f, 0.0f }; accent->t2d.size = { 0.0f, 0.0f, 0.0f, height };
settingsInfo->t2d.center = { 0.5f, 0.5f }; accent->BindToRenderSystem();
settingsInfo->t2d.size = { 0.8f, 0.0f, 0.0f, 90.0f }; menuItems.push_back(accent);
settingsInfo->textCenter = { 0.5f, 0.5f };
settingsInfo->BindToRenderSystem(); // Compact buttons hold a single centered symbol, the rest are left aligned and slide on hover
menuItems.push_back(settingsInfo); 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<DrawableSurface> 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<MenuButton> 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<MenuButton*> 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(); TextBox* menuHint = new TextBox();
menuHint->font = arial; menuHint->font = arial;
menuHint->text = "WASD TO MOVE | LEFT CLICK TO PUNCH | ESC FOR MENU"; menuHint->fontsize = 0.42f;
menuHint->fontsize = 0.5f; menuHint->Color = menuMutedText;
menuHint->Color = { 0.55f, 0.61f, 0.70f }; menuHint->t2d.position = { 0.0f, 1.0f, menuMargin, -28.0f };
menuHint->t2d.position = { 0.5f, 0.92f, 0.0f, 0.0f }; menuHint->t2d.center = { 0.0f, 1.0f };
menuHint->t2d.center = { 0.5f, 0.5f }; menuHint->t2d.size = { 0.0f, 0.0f, menuContentWidth, 110.0f };
menuHint->t2d.size = { 0.9f, 0.0f, 0.0f, 40.0f }; menuHint->textCenter = { 0.0f, 1.0f };
menuHint->textCenter = { 0.5f, 0.5f };
menuHint->BindToRenderSystem(); menuHint->BindToRenderSystem();
menuItems.push_back(menuHint); menuItems.push_back(menuHint);
auto SetButtonVisible = [](MenuButton& button, bool visible) { 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; button.label->Opacity = visible ? 1.0f : 0.0f;
}; };
@@ -371,38 +471,79 @@ void Engine::Initiate()
const bool main = menuScreen == MenuScreen::Main; const bool main = menuScreen == MenuScreen::Main;
const bool paused = menuScreen == MenuScreen::Paused; const bool paused = menuScreen == MenuScreen::Paused;
const bool settings = menuScreen == MenuScreen::Settings; 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; menuTitle->Opacity = 1.0f;
menuSubtitle->Opacity = 1.0f; menuSubtitle->Opacity = 1.0f;
menuHint->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"; playButton.label->text = paused ? "RESUME" : "PLAY";
quitButton.label->text = paused ? "QUIT TO MENU" : "QUIT"; quitButton.label->text = paused ? "QUIT TO MENU" : "QUIT";
menuHint->text = settings menuHint->text = drawer
? "ESC TO GO BACK | ADJUST MOUSE, FOV, AND HITBOXES" ? "DRAG ON THE CANVAS TO DRAW\nESC TO GO BACK"
: "WASD TO MOVE | LEFT CLICK TO PUNCH | ESC FOR MENU"; : (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(sensitivityDown, settings);
SetButtonVisible(sensitivityUp, settings); SetButtonVisible(sensitivityUp, settings);
SetButtonVisible(fovDown, settings); SetButtonVisible(fovDown, settings);
SetButtonVisible(fovUp, settings); SetButtonVisible(fovUp, settings);
SetButtonVisible(hitboxToggle, settings); SetButtonVisible(hitboxToggle, settings);
SetButtonVisible(graphicsButton, settings);
SetButtonVisible(resetButton, settings); SetButtonVisible(resetButton, settings);
SetButtonVisible(backButton, settings); SetButtonVisible(backButton, settings || drawer || graphics);
settingsInfo->Opacity = settings ? 1.0f : 0.0f;
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.label->text = showHitboxes ? "HITBOXES: ON" : "HITBOXES: OFF";
hitboxToggle.color = showHitboxes hitboxToggle.color = showHitboxes ? glm::vec3(0.82f, 0.45f, 0.10f) : menuButtonColor;
? glm::vec3(0.12f, 0.30f, 0.22f)
: glm::vec3(0.20f, 0.22f, 0.28f);
settingsInfo->text = "MOUSE SENSITIVITY: " + sensValue->text = std::to_string((int)std::lround(StartupCmPer360)) + " CM";
std::to_string((int)std::lround(StartupCmPer360)) + fovValue->text = std::to_string((int)std::lround(camera->FOV));
" CM / 360\nFIELD OF VIEW: " + std::to_string((int)std::lround(camera->FOV));
}; };
auto OpenMenu = [&](MenuScreen screen) { auto OpenMenu = [&](MenuScreen screen) {
@@ -412,9 +553,6 @@ void Engine::Initiate()
testcharacter.SetWalking(false); testcharacter.SetWalking(false);
npc.SetWalking(false); npc.SetWalking(false);
fpsCounter->Opacity = 0.0f; fpsCounter->Opacity = 0.0f;
healthText->Opacity = 0.0f;
healthBarBack->Opacity = 0.0f;
healthBarFill->Opacity = 0.0f;
camera->lockedcursor = false; camera->lockedcursor = false;
glfwSetInputMode(window->handle, GLFW_CURSOR, GLFW_CURSOR_NORMAL); glfwSetInputMode(window->handle, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
prevMouseDown = mouse->IsLeftDown(); prevMouseDown = mouse->IsLeftDown();
@@ -425,9 +563,6 @@ void Engine::Initiate()
charactermanager->ClearInput(); charactermanager->ClearInput();
for (Box* item : menuItems) item->Opacity = 0.0f; for (Box* item : menuItems) item->Opacity = 0.0f;
fpsCounter->Opacity = 1.0f; fpsCounter->Opacity = 1.0f;
healthText->Opacity = 1.0f;
healthBarBack->Opacity = 0.85f;
healthBarFill->Opacity = 1.0f;
camera->lockedcursor = true; camera->lockedcursor = true;
glfwSetInputMode(window->handle, GLFW_CURSOR, GLFW_CURSOR_DISABLED); glfwSetInputMode(window->handle, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
int ww, wh; int ww, wh;
@@ -436,9 +571,6 @@ void Engine::Initiate()
}; };
fpsCounter->Opacity = 0.0f; fpsCounter->Opacity = 0.0f;
healthText->Opacity = 0.0f;
healthBarBack->Opacity = 0.0f;
healthBarFill->Opacity = 0.0f;
camera->lockedcursor = false; camera->lockedcursor = false;
glfwSetInputMode(window->handle, GLFW_CURSOR, GLFW_CURSOR_NORMAL); glfwSetInputMode(window->handle, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
@@ -447,9 +579,9 @@ void Engine::Initiate()
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
Mesh* floor = CreateCubeMesh(); Mesh* floor = CreateCubeMesh();
floor->name = "baseplate"; floor->name = "baseplate";
floor->meshTexture = blanktex; floor->surface = SurfaceTexture::Tiled(blanktex, 8.0f);
floor->roughness = 0.9f; 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->t.TranslateTo({ 0.0f, -1.0f, 0.0f });
floor->BindToRenderSystem(); floor->BindToRenderSystem();
mainf->AddChild(floor); mainf->AddChild(floor);
@@ -459,30 +591,36 @@ void Engine::Initiate()
physicsengine->AddObject(floorBody); physicsengine->AddObject(floorBody);
const BoundingBox floorBounds = floor->t.GetRotationlessAABB(); const BoundingBox floorBounds = floor->t.GetRotationlessAABB();
const float wallHeight = 4.0f; // ------------------------------------------------------------------------
const float wallThickness = 1.0f; // Light cubes
const float wallOffset = 16.0f + wallThickness * 0.5f; // ------------------------------------------------------------------------
const float wallSpan = 32.0f + wallThickness; auto AddLightCube = [&](const char* name, const glm::vec3& position, float size, const glm::vec3& color) -> Mesh* {
Mesh* cube = CreateCubeMesh();
auto AddWall = [&](const char* name, const glm::vec3& position, const glm::vec3& scale) { cube->name = name;
Mesh* wall = CreateCubeMesh(); cube->colorOverride = glm::vec4(color, 1.0f);
wall->name = name; cube->emissive = 1.0f;
wall->meshTexture = blanktex; cube->t.ScaleTo(size);
wall->roughness = 0.9f; cube->t.TranslateTo(position);
wall->t.ScaleTo(scale); cube->BindToRenderSystem();
wall->t.TranslateTo(position); mainf->AddChild(cube);
wall->BindToRenderSystem(); return cube;
mainf->AddChild(wall);
p* wallBody = new p(wall);
wallBody->velocity = false;
physicsengine->AddObject(wallBody);
}; };
AddWall("wall_north", { 0.0f, wallHeight * 0.5f, -wallOffset }, { wallSpan, wallHeight, wallThickness }); const glm::vec3 skyLightColor(0.45f, 0.65f, 1.0f);
AddWall("wall_south", { 0.0f, wallHeight * 0.5f, wallOffset }, { wallSpan, wallHeight, wallThickness }); const glm::vec3 skyLightPosition(-6.0f, 7.0f, -6.0f);
AddWall("wall_west", { -wallOffset, wallHeight * 0.5f, 0.0f }, { wallThickness, wallHeight, 32.0f }); Mesh* skyLightCube = AddLightCube("spot_light_cube", skyLightPosition, 1.5f, skyLightColor);
AddWall("wall_east", { wallOffset, wallHeight * 0.5f, 0.0f }, { wallThickness, wallHeight, 32.0f }); 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 // Ground check + respawn
@@ -515,10 +653,12 @@ void Engine::Initiate()
if (showHitboxes) { if (showHitboxes) {
for (Character* character : charactermanager->GetCharacters()) { for (Character* character : charactermanager->GetCharacters()) {
if (!character->body) continue; 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) }); 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); rendersystem->SetHitboxDebugLines(lines);
@@ -545,8 +685,10 @@ void Engine::Initiate()
if (escapePressed) { if (escapePressed) {
if (!menuActive) if (!menuActive)
OpenMenu(MenuScreen::Paused); OpenMenu(MenuScreen::Paused);
else if (menuScreen == MenuScreen::Settings) else if (menuScreen == MenuScreen::Settings || menuScreen == MenuScreen::Drawer)
menuScreen = settingsReturn; menuScreen = settingsReturn;
else if (menuScreen == MenuScreen::Graphics)
menuScreen = MenuScreen::Settings;
else if (menuScreen == MenuScreen::Paused) else if (menuScreen == MenuScreen::Paused)
ResumeGame(); ResumeGame();
} }
@@ -561,9 +703,6 @@ void Engine::Initiate()
float yoffset = mouse->GetLastY() - mouse->GetY(); float yoffset = mouse->GetLastY() - mouse->GetY();
camera->ProcessMouseMovement(xoffset, yoffset); 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) { else if (buffer.GetType() == Mouse::Event::Type::RPress) {
camera->lockedcursor = !camera->lockedcursor; camera->lockedcursor = !camera->lockedcursor;
if (camera->lockedcursor) { if (camera->lockedcursor) {
@@ -582,7 +721,8 @@ void Engine::Initiate()
ApplyMenuLayout(); ApplyMenuLayout();
const bool liveBackdrop = menuScreen == MenuScreen::Main || 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) { if (liveBackdrop) {
static float menuAngle = 0.6f; static float menuAngle = 0.6f;
menuAngle += deltatime * 0.12f; menuAngle += deltatime * 0.12f;
@@ -593,11 +733,7 @@ void Engine::Initiate()
camera->t.RotateToQuaternion(LookAt(menuTarget - menuCamera)); camera->t.RotateToQuaternion(LookAt(menuTarget - menuCamera));
camera->Step(deltatime); camera->Step(deltatime);
npc.SetTurnDirection(testcharacter.body->t.GetTranslation() -
npc.body->t.GetTranslation());
charactermanager->Step(deltatime); charactermanager->Step(deltatime);
rain->t.TranslateTo(camera->t.GetTranslation() + glm::vec3(0.0f, 20.0f, 0.0f));
rain->Step(deltatime);
} }
soundsystem->SetListenerPosition( soundsystem->SetListenerPosition(
@@ -607,89 +743,146 @@ void Engine::Initiate()
); );
soundsystem->Update(); 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()); const glm::vec2 mousePos((float)mouse->GetX(), (float)mouse->GetY());
auto Hovered = [&](const MenuButton& button) { const float hoverEase = 1.0f - std::exp(-12.0f * deltatime);
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 bool playHovered = Hovered(playButton); for (MenuButton* button : menuButtons) {
const bool settingsHovered = Hovered(settingsButton); t2d_package& t2d = button->box->t2d;
const bool quitHovered = Hovered(quitButton); t2d.Recalculate();
const bool sensDownHovered = Hovered(sensitivityDown); button->hovered = button->box->Opacity > 0.0f &&
const bool sensUpHovered = Hovered(sensitivityUp); mousePos.x > t2d.pixelposition.x &&
const bool fovDownHovered = Hovered(fovDown); mousePos.y > t2d.pixelposition.y &&
const bool fovUpHovered = Hovered(fovUp); mousePos.x < t2d.pixelposition.x + t2d.pixelsize.x &&
const bool hitboxHovered = Hovered(hitboxToggle); mousePos.y < t2d.pixelposition.y + t2d.pixelsize.y;
const bool resetHovered = Hovered(resetButton);
const bool backHovered = Hovered(backButton);
playButton.box->Color = playHovered ? glm::vec3(0.18f, 0.44f, 0.32f) : playButton.color; button->hover += ((button->hovered ? 1.0f : 0.0f) - button->hover) * hoverEase;
settingsButton.box->Color = settingsHovered ? glm::vec3(0.22f, 0.34f, 0.50f) : settingsButton.color; button->box->Color = glm::mix(button->color, button->hoverColor, button->hover);
quitButton.box->Color = quitHovered ? glm::vec3(0.30f, 0.20f, 0.22f) : quitButton.color; button->accent->t2d.size.z = 5.0f * button->hover;
sensitivityDown.box->Color = sensDownHovered ? glm::vec3(0.22f, 0.34f, 0.50f) : sensitivityDown.color; button->label->t2d.position.z = button->labelX + button->labelSlide * button->hover;
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;
const bool mouseDown = mouse->IsLeftDown(); const bool mouseDown = mouse->IsLeftDown();
if (mouseDown && !prevMouseDown) { if (mouseDown && !prevMouseDown) {
if (menuScreen == MenuScreen::Settings) { if (menuScreen == MenuScreen::Settings) {
if (sensDownHovered) { if (sensitivityDown.hovered) {
StartupCmPer360 = glm::clamp(StartupCmPer360 + 2.0f, 4.0f, 80.0f); StartupCmPer360 = glm::clamp(StartupCmPer360 + 2.0f, 4.0f, 80.0f);
camera->SetSensitivityCm(StartupCmPer360, StartupMouseDpi); camera->SetSensitivityCm(StartupCmPer360, StartupMouseDpi);
} }
else if (sensUpHovered) { else if (sensitivityUp.hovered) {
StartupCmPer360 = glm::clamp(StartupCmPer360 - 2.0f, 4.0f, 80.0f); StartupCmPer360 = glm::clamp(StartupCmPer360 - 2.0f, 4.0f, 80.0f);
camera->SetSensitivityCm(StartupCmPer360, StartupMouseDpi); camera->SetSensitivityCm(StartupCmPer360, StartupMouseDpi);
} }
else if (fovDownHovered) { else if (fovDown.hovered) {
camera->FOV = glm::clamp(camera->FOV - 5.0f, 60.0f, 130.0f); 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); camera->FOV = glm::clamp(camera->FOV + 5.0f, 60.0f, 130.0f);
} }
else if (hitboxHovered) { else if (hitboxToggle.hovered) {
showHitboxes = !showHitboxes; showHitboxes = !showHitboxes;
} }
else if (resetHovered) { else if (graphicsButton.hovered) {
menuScreen = MenuScreen::Graphics;
}
else if (resetButton.hovered) {
StartupCmPer360 = 16.0f; StartupCmPer360 = 16.0f;
StartupMouseDpi = 800.0f; StartupMouseDpi = 800.0f;
camera->SetSensitivityCm(StartupCmPer360, StartupMouseDpi); camera->SetSensitivityCm(StartupCmPer360, StartupMouseDpi);
camera->FOV = 110.0f; camera->FOV = 110.0f;
showHitboxes = false; showHitboxes = false;
} }
else if (backHovered) { else if (backButton.hovered) {
menuScreen = settingsReturn; 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(); ResumeGame();
} }
else if (settingsHovered) { else if (drawerButton.hovered) {
settingsReturn = menuScreen;
menuScreen = MenuScreen::Drawer;
}
else if (settingsButton.hovered) {
settingsReturn = menuScreen; settingsReturn = menuScreen;
menuScreen = MenuScreen::Settings; menuScreen = MenuScreen::Settings;
} }
else if (quitHovered) { else if (quitButton.hovered) {
if (menuScreen == MenuScreen::Main) if (menuScreen == MenuScreen::Main)
glfwSetWindowShouldClose(window->handle, 1); glfwSetWindowShouldClose(window->handle, 1);
else else
menuScreen = MenuScreen::Main; 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; prevMouseDown = mouseDown;
UpdateHitboxDebug(); UpdateHitboxDebug();
@@ -712,11 +905,6 @@ void Engine::Initiate()
testcharacter.SetTurnDirection(inputDirection); testcharacter.SetTurnDirection(inputDirection);
testcharacter.SetWalking(hasMoveInput); 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); charactermanager->Step(deltatime);
const glm::vec3 inputVel = hasMoveInput const glm::vec3 inputVel = hasMoveInput
@@ -746,13 +934,12 @@ void Engine::Initiate()
characterBody->linearvelocity.z += wishDir.z * add; characterBody->linearvelocity.z += wishDir.z * add;
} }
charactermanager->ResolveCombat(*physicsengine);
{ {
const int substeps = std::max(1, (int)std::ceil(deltatime * 120.0f)); const int substeps = std::max(1, (int)std::ceil(deltatime * 120.0f));
const float subDt = deltatime / float(substeps); const float subDt = deltatime / float(substeps);
for (int i = 0; i < substeps; i++) physicsengine->Step(subDt); for (int i = 0; i < substeps; i++) physicsengine->Step(subDt);
} }
npc.ragdoll->Update(deltatime);
if (testcharacter.body->t.GetTranslation().y < -20.0f) { if (testcharacter.body->t.GetTranslation().y < -20.0f) {
testcharacter.body->t.TranslateTo(RandomSpawn()); testcharacter.body->t.TranslateTo(RandomSpawn());
@@ -784,6 +971,8 @@ void Engine::Initiate()
for (p* obj : physicsengine->GetObjects()) { for (p* obj : physicsengine->GetObjects()) {
if (obj->pointer == testcharacter.body || !obj->collision) if (obj->pointer == testcharacter.body || !obj->collision)
continue; continue;
if (obj->collisionGroup != 0) // flailing ragdoll limbs would make the camera jump
continue;
if (obj == floorBody && ignoreFloorCollision) if (obj == floorBody && ignoreFloorCollision)
continue; continue;
auto hit = IsRayInT(cameraSweep, obj->pointer->t); auto hit = IsRayInT(cameraSweep, obj->pointer->t);
@@ -800,9 +989,6 @@ void Engine::Initiate()
} }
camera->Step(deltatime); camera->Step(deltatime);
rain->t.TranslateTo(camera->t.GetTranslation() + glm::vec3(0.0f, 20.0f, 0.0f));
rain->Step(deltatime);
Sound* music = static_cast<Sound*>(mainf->GetFirstChildOfName("music")); Sound* music = static_cast<Sound*>(mainf->GetFirstChildOfName("music"));
if (music) music->Update3DPosition(0.0f, 0.0f, 0.0f); if (music) music->Update3DPosition(0.0f, 0.0f, 0.0f);
@@ -833,14 +1019,6 @@ void Engine::Initiate()
fpsCounter->text = std::to_string(measuredFps) + " FPS"; 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 // Render
// -------------------------------------------------------------------- // --------------------------------------------------------------------
@@ -878,9 +1056,12 @@ void key_callback(GLFWwindow* window, int key, int scancode, int action, int mod
const GLFWvidmode* mode = glfwGetVideoMode(monitor); const GLFWvidmode* mode = glfwGetVideoMode(monitor);
glfwSetWindowMonitor(window, monitor, 0, 0, mode->width, mode->height, mode->refreshRate); 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); if (action == GLFW_PRESS) keyboard->KeyDown(key);
else if (action == GLFW_RELEASE) keyboard->KeyUp(key); else if (action == GLFW_RELEASE) keyboard->KeyUp(key);
+3 -9
View File
@@ -8,6 +8,7 @@
#include "QuadVertices.h" #include "QuadVertices.h"
#include "shaderClass.h" #include "shaderClass.h"
#include "texture.h" #include "texture.h"
#include "SurfaceTexture.h"
#include "t2d.h" #include "t2d.h"
#include "font.h" #include "font.h"
#include "RenderSystem.h" #include "RenderSystem.h"
@@ -190,7 +191,7 @@ public:
class ImageBox : public Box { class ImageBox : public Box {
public: public:
Texture* tex = nullptr; SurfaceTexture surface;
ImageBox() { ImageBox() {
shadertype = ShaderType::ImageBoxShader; shadertype = ShaderType::ImageBoxShader;
@@ -208,14 +209,7 @@ public:
ShaderProgram.Set2F("pixelScale", t2d.pixelsize); ShaderProgram.Set2F("pixelScale", t2d.pixelsize);
ShaderProgram.Set2F("pixelPos", t2d.pixelposition); ShaderProgram.Set2F("pixelPos", t2d.pixelposition);
ShaderProgram.Set1F("rounding", rounding); ShaderProgram.Set1F("rounding", rounding);
glActiveTexture(GL_TEXTURE0); surface.Apply(ShaderProgram, 0, glm::vec3(t2d.pixelsize, 0.0f));
if (tex) {
glBindTexture(GL_TEXTURE_2D, tex->ID);
ShaderProgram.SetInt("tex", 0);
}
else {
glBindTexture(GL_TEXTURE_2D, 0);
}
boxVAO.Bind(); boxVAO.Bind();
glDrawArrays(GL_TRIANGLES, 0, 6); glDrawArrays(GL_TRIANGLES, 0, 6);
boxVAO.Unbind(); boxVAO.Unbind();
+48
View File
@@ -0,0 +1,48 @@
#ifndef LIGHT_CLASS
#define LIGHT_CLASS
#include <glm/glm.hpp>
// 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
+45 -7
View File
@@ -1,21 +1,59 @@
#ifndef LIGHTING_CLASS #ifndef LIGHTING_CLASS
#define LIGHTING_CLASS #define LIGHTING_CLASS
#include "t.h" #include <vector>
#include "tFunctions.h"
#include "Light.h"
// Every light in the scene. Directional lights are the only ones without shadows, apart from the sun.
class Lighting { class Lighting {
public: public:
t dirlighting; DirectionalLight sun;
std::vector<DirectionalLight*> directionalLights;
std::vector<PointLight*> pointLights;
std::vector<SpotLight*> spotLights;
std::vector<glm::mat4> shadowTileMatrices; // world to light clip space, one per used atlas tile
Lighting() { Lighting() {
dirlighting.RotateToQuaternion(glm::quat({ glm::radians(90.0f),0.0f,0.0f})); sun.color = glm::vec3(1.0f, 0.97f, 0.92f);
dirlighting.TranslateTo({0.0f,0.0f,0.0f}); 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: private:
}; };
#endif #endif
+11 -11
View File
@@ -25,6 +25,7 @@
#include "tFunctions.h" #include "tFunctions.h"
#include "Vertex.h" #include "Vertex.h"
#include "texture.h" #include "texture.h"
#include "SurfaceTexture.h"
#include "MeshData.h" #include "MeshData.h"
struct IntersectionData { struct IntersectionData {
@@ -36,11 +37,12 @@ class Mesh : public Object , public Renderable, public t_package , public MeshDa
friend class Model; friend class Model;
protected: protected:
public: public:
Texture* meshTexture = nullptr; SurfaceTexture surface;
glm::vec4 colorOverride = glm::vec4(0.0f); // rgb + strength; 0 = use the texture's own colour glm::vec4 colorOverride = glm::vec4(0.0f); // rgb + strength; 0 = use the texture's own colour
float metallic = 0.0f; float metallic = 0.0f;
float roughness = 0.8f; float roughness = 0.8f;
float ao = 1.0f; 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; VAO vao;
VBO vbo; 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 { void Render(Shader& ShaderProgram) override {
glm::mat4 topass = t.GetMatrix(); glm::mat4 topass = t.GetMatrix();
ShaderProgram.Activate(); ShaderProgram.Activate();
glActiveTexture(GL_TEXTURE1); surface.Apply(ShaderProgram, 1, t.GetScale());
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);
}
ShaderProgram.SetMat4("modl", topass); ShaderProgram.SetMat4("modl", topass);
ShaderProgram.Set4F("colorOverride", colorOverride); ShaderProgram.Set4F("colorOverride", colorOverride);
ShaderProgram.Set1F("metallic", metallic); ShaderProgram.Set1F("metallic", metallic);
ShaderProgram.Set1F("roughness", roughness); ShaderProgram.Set1F("roughness", roughness);
ShaderProgram.Set1F("ao", ao); ShaderProgram.Set1F("ao", ao);
ShaderProgram.Set1F("emissive", emissive);
vao.Bind(); vao.Bind();
glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0); glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
vao.Unbind(); vao.Unbind();
-626
View File
@@ -1,626 +0,0 @@
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
#include "Net.h"
#include <algorithm>
#include <chrono>
#include <cstring>
#include <iostream>
#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<double>(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<Peer> 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<PendingBlast> pendingBlasts;
uint32_t blastSeq = 0;
std::vector<uint64_t> seenBlasts; // (type, sender, id) of recent blasts/hits, for de-duplication
struct PendingSpawn { SpawnPacket pkt; int copiesLeft; };
std::vector<PendingSpawn> 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<PendingHit> 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<Entry>& 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<NetCube>& 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<Entry> 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<NetUpdate>& out, std::vector<NetBlast>* blasts, std::vector<NetHit>* hits,
std::vector<NetCubeRequest>* spawns, std::vector<NetCube>* 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;
}
-115
View File
@@ -1,115 +0,0 @@
#ifndef NET_CLASS
#define NET_CLASS
#include <cstdint>
#include <string>
#include <vector>
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
// 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<NetUpdate>& out, std::vector<NetBlast>* blasts = nullptr,
std::vector<NetHit>* hits = nullptr, std::vector<NetCubeRequest>* spawns = nullptr,
std::vector<NetCube>* 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<NetCube>& 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
+8 -11
View File
@@ -66,10 +66,12 @@ public:
public: public:
std::vector<Particle*> particles; std::vector<Particle*> particles;
Texture* tex; SurfaceTexture surface;
Mesh* mesh; Mesh* mesh;
unsigned int instanceBuffer = 0;
ParticleEmitter() { ParticleEmitter() {
shadertype = ParticleShader; shadertype = ParticleShader;
mesh = new Mesh(quadVertices3D, quadIndices3D); mesh = new Mesh(quadVertices3D, quadIndices3D);
@@ -81,6 +83,7 @@ public:
} }
~ParticleEmitter() { ~ParticleEmitter() {
if (instanceBuffer != 0) glDeleteBuffers(1, &instanceBuffer);
mesh->Delete(); mesh->Delete();
} }
@@ -131,13 +134,8 @@ public:
ShaderProgram.Set4F("color",color); 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<glm::mat4> matrices; std::vector<glm::mat4> matrices;
if (color.w != 1.0f) { if (color.w != 1.0f) {
@@ -167,9 +165,8 @@ public:
} }
} }
} }
unsigned int buffer; if (instanceBuffer == 0) glGenBuffers(1, &instanceBuffer);
glGenBuffers(1, &buffer); glBindBuffer(GL_ARRAY_BUFFER, instanceBuffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, particles.size() * sizeof(glm::mat4), &matrices[0], GL_STATIC_DRAW); glBufferData(GL_ARRAY_BUFFER, particles.size() * sizeof(glm::mat4), &matrices[0], GL_STATIC_DRAW);
mesh->vao.Bind(); mesh->vao.Bind();
+427 -15
View File
@@ -1,5 +1,8 @@
#include "Physics.h" #include "Physics.h"
#include <glm/gtc/constants.hpp>
#include <glm/gtc/quaternion.hpp>
// ============================================================================ // ============================================================================
// ComputeInvInertiaTensor // ComputeInvInertiaTensor
// Returns zero matrix for static or rotation-locked bodies. // 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) 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 linSq = glm::dot(body->linearvelocity, body->linearvelocity);
const float angSq = glm::dot(body->angularvelocity, body->angularvelocity); 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) 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 // Integrate forces ? velocities
for (p* body : objects) { 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->force += body->mass * gravity;
body->linearvelocity += (body->force / body->mass) * dt; body->linearvelocity += (body->force / body->mass) * dt;
if (!body->lockRotation) 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 body->linearvelocity *= linearDamping;
const float steps = dt * 60.0f; body->angularvelocity *= angularDamping;
body->linearvelocity *= std::pow(PHYSICS_LINEAR_DAMPING, steps);
body->angularvelocity *= std::pow(PHYSICS_ANGULAR_DAMPING, steps);
body->force = glm::vec3(0.0f); body->force = glm::vec3(0.0f);
body->torque = glm::vec3(0.0f); body->torque = glm::vec3(0.0f);
@@ -80,17 +90,415 @@ void Physics::Step(float dt)
if (!body->velocity) continue; if (!body->velocity) continue;
if (UpdateSleep(body, dt)) 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); body->pointer->t.TranslateBy(body->linearvelocity * dt);
if (!body->lockRotation) if (!body->lockRotation) {
body->pointer->t.RotateByQuaternion(glm::quat(body->angularvelocity * dt)); 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<TInTInfo> 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<Collision>& 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<Collision>& 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<float>();
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 // Resolve
// ============================================================================ // ============================================================================
void Physics::Resolve(Collision* c) void Physics::Resolve(Collision* c, bool firstPass)
{ {
p* a = c->ObjA; p* a = c->ObjA;
p* b = c->ObjB; 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 rA = c->POI - a->pointer->t.GetTranslation();
const glm::vec3 rB = c->POI - b->pointer->t.GetTranslation(); const glm::vec3 rB = c->POI - b->pointer->t.GetTranslation();
const glm::mat3 invIA = ComputeInvInertiaTensor(a); const glm::mat3& invIA = a->invInertia;
const glm::mat3 invIB = ComputeInvInertiaTensor(b); const glm::mat3& invIB = b->invInertia;
const float invMassA = a->GetInvMass(); const float invMassA = a->GetInvMass();
const float invMassB = b->GetInvMass(); const float invMassB = b->GetInvMass();
@@ -115,7 +523,7 @@ void Physics::Resolve(Collision* c)
const float relVelN = glm::dot(relVel, n); const float relVelN = glm::dot(relVel, n);
if (relVelN > 0.0f) return; 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 ? 0.0f
: glm::min(a->restitution, b->restitution); : 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); const float denomT = invMassA + invMassB + angDenom(invIA, rA, tangent) + angDenom(invIB, rB, tangent);
float jT = -glm::dot(relVel, tangent) / denomT; 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); jT = glm::clamp(jT, -mu * jN, mu * jN);
const glm::vec3 impulseT = jT * tangent; const glm::vec3 impulseT = jT * tangent;
@@ -165,10 +573,14 @@ void Physics::Resolve(Collision* c)
} }
// ---- Baumgarte positional correction ---- // ---- Baumgarte positional correction ----
if (!firstPass)
return;
const float correction = glm::max(c->overlap - PHYSICS_BAUMGARTE_SLOP, 0.0f) const float correction = glm::max(c->overlap - PHYSICS_BAUMGARTE_SLOP, 0.0f)
/ (invMassA + invMassB) / (invMassA + invMassB)
* PHYSICS_BAUMGARTE_FACTOR; * PHYSICS_BAUMGARTE_FACTOR
* c->correctionWeight;
const glm::vec3 corrVec = correction * n; const glm::vec3 corrVec = correction * n;
if (a->velocity) a->pointer->t.TranslateBy(invMassA * corrVec); if (a->velocity) a->pointer->t.TranslateBy(invMassA * corrVec);
if (b->velocity) b->pointer->t.TranslateBy(-invMassB * corrVec); if (b->velocity) b->pointer->t.TranslateBy(-invMassB * corrVec);
} }
+84 -32
View File
@@ -12,11 +12,13 @@
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp> #include <glm/gtc/type_ptr.hpp>
#include <glm/gtc/quaternion.hpp>
#include "common.h" #include "common.h"
#include "Engine.h" #include "Engine.h"
#include "Camera.h" #include "Camera.h"
#include "t.h" #include "t.h"
#include "tFunctions.h" #include "tFunctions.h"
#include "CollisionMesh.h"
// ============================================================================ // ============================================================================
// Tuneable constants // 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_ANGULAR_SQ = 0.01f * 0.01f;
static constexpr float PHYSICS_SLEEP_TIME_THRESHOLD = 0.5f; static constexpr float PHYSICS_SLEEP_TIME_THRESHOLD = 0.5f;
static constexpr float PHYSICS_RESTITUTION_THRESHOLD = 1.0f; 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 // Physics body
@@ -65,15 +74,23 @@ public:
bool velocity = true; bool velocity = true;
bool isSleeping = false; bool isSleeping = false;
bool lockRotation = 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 mass = 1.0f;
float restitution = 0.2f; float restitution = 0.2f;
float friction = 0.5f; 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 linearvelocity = { 0.0f, 0.0f, 0.0f };
glm::vec3 angularvelocity = { 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 force = { 0.0f, 0.0f, 0.0f };
glm::vec3 torque = { 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; float sleepTimer = 0.0f;
}; };
@@ -85,23 +102,87 @@ struct Collision {
glm::vec3 POI; glm::vec3 POI;
glm::vec3 CN; glm::vec3 CN;
float overlap = 0.0f; float overlap = 0.0f;
float correctionWeight = 1.0f; // shares positional correction between a pair's contacts
float frictionScale = 1.0f;
p* ObjA = nullptr; p* ObjA = nullptr;
p* ObjB = nullptr; p* ObjB = nullptr;
Collision(p* a, p* b) : ObjA(a), ObjB(b) {} 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 // Physics world
// ============================================================================ // ============================================================================
class Physics { class Physics {
private: private:
std::vector<p*> objects; std::vector<p*> objects;
std::vector<BallJoint*> joints;
glm::vec3 gravity = glm::vec3(0.0f, -9.81f, 0.0f); glm::vec3 gravity = glm::vec3(0.0f, -9.81f, 0.0f);
// Kept between steps so they don't get reallocated
std::vector<BroadPhaseEntry> broadPhase;
std::vector<Collision> collisions;
std::vector<Collision> meshContacts;
std::vector<int> meshCandidates;
static glm::mat3 ComputeInvInertiaTensor(const p* body); static glm::mat3 ComputeInvInertiaTensor(const p* body);
static bool UpdateSleep(p* body, float dt); 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: public:
Physics() {} Physics() {}
@@ -111,6 +192,7 @@ public:
int GetObjectAmount() const noexcept { return (int)objects.size(); } int GetObjectAmount() const noexcept { return (int)objects.size(); }
void AddObject(p* obj) { objects.push_back(obj); } void AddObject(p* obj) { objects.push_back(obj); }
void AddJoint(BallJoint* joint) { joints.push_back(joint); }
const std::vector<p*>& GetObjects() const { return objects; } const std::vector<p*>& GetObjects() const { return objects; }
p* PPointerOfObject(t_package* pointer) const { p* PPointerOfObject(t_package* pointer) const {
@@ -135,37 +217,7 @@ public:
void Step(float dt); void Step(float dt);
void SolveCollisions(std::vector<Collision>& collisions) { void ResolveCollisions(float dt);
for (Collision& col : collisions)
Resolve(&col);
}
void ResolveCollisions(float dt) {
std::vector<Collision> 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<TInTInfo> 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);
}
}; };
#endif #endif
+329
View File
@@ -0,0 +1,329 @@
#include "Ragdoll.h"
#include <cfloat>
#include <iostream>
#include <string>
#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<int>& 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<glm::mat4>& 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<glm::mat4> 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<Triangle> 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<Triangle> 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<glm::mat4> Ragdoll::GetBoneMatrices() const
{
std::vector<glm::mat4> bones = restBones;
if (parts.empty())
return bones;
std::vector<glm::mat4> 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;
}
+48
View File
@@ -0,0 +1,48 @@
#ifndef RAGDOLL_CLASS
#define RAGDOLL_CLASS
#include <vector>
#include <glm/glm.hpp>
#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<glm::mat4> GetBoneMatrices() const;
const std::vector<Part*>& GetParts() const { return parts; }
private:
void Reset();
std::vector<Part*> parts;
std::vector<int> boneParts; // part index of each bone
std::vector<glm::mat4> restBones; // world space bone matrices at the moment of going limp
std::vector<glm::mat4> restInverses; // inverse of each part's starting transform
int group = 0;
float settleTimer = 0.0f;
};
#endif
+520 -57
View File
@@ -30,11 +30,54 @@ static const float hitboxLineVertices[] = {
static void ConfigurePBR(Shader* shader, Lighting* lighting) static void ConfigurePBR(Shader* shader, Lighting* lighting)
{ {
shader->Set3F("dirLight.direction", lighting->dirlighting.GetFrontVector()); shader->Set3F("dirLight.direction", lighting->sun.direction);
shader->Set3F("dirLight.color", glm::vec3(1.0f, 0.97f, 0.92f)); shader->Set3F("dirLight.color", lighting->sun.color);
shader->Set1F("dirLight.intensity", 3.0f); shader->Set1F("dirLight.intensity", lighting->sun.intensity);
shader->Set1F("envIntensity", 0.8f); shader->Set1F("envIntensity", 0.8f);
shader->SetInt("environmentMap", 2); 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(); screenfbo->Bind();
glGenTextures(1, &textureColorbuffer); glGenTextures(1, &textureColorbuffer);
glBindTexture(GL_TEXTURE_2D, 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_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0); glBindTexture(GL_TEXTURE_2D, 0);
@@ -112,6 +155,50 @@ void RenderSystem::Initialize()
std::cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!\n"; std::cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!\n";
screenfbo->Unbind(); 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 // Shadow framebuffer
shadowdepthmapfbo = new FBO(); shadowdepthmapfbo = new FBO();
SHADOW_RESOLUTION = 4096; SHADOW_RESOLUTION = 4096;
@@ -134,16 +221,88 @@ void RenderSystem::Initialize()
glReadBuffer(GL_NONE); glReadBuffer(GL_NONE);
glBindFramebuffer(GL_FRAMEBUFFER, 0); 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(); 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) 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; 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); 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); glBindTexture(GL_TEXTURE_2D, 0);
glBindRenderbuffer(GL_RENDERBUFFER, rbo); glBindRenderbuffer(GL_RENDERBUFFER, rbo);
@@ -226,7 +385,7 @@ void RenderSystem::RenderTranslucent(Camera& camera, const glm::mat4& lightSpace
// ============================================================================ // ============================================================================
void RenderSystem::Render(Camera& camera) 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 proj = camera.GetProjectionMatrix(0.05f, 2000.0f);
const glm::mat4 view = camera.GetViewMatrix(); 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 float near_plane = 0.1f, far_plane = 100.0f;
const glm::mat4 lightSpaceMatrix = const glm::mat4 lightSpaceMatrix =
glm::ortho(-32.0f, 32.0f, -32.0f, 32.0f, near_plane, far_plane) 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()); camPos, camera.t.GetUpVector());
// ---- Shadow pass ---- // ---- Shadow pass ----
@@ -245,32 +404,29 @@ void RenderSystem::Render(Camera& camera)
shadowdepthmapfbo->Bind(); shadowdepthmapfbo->Bind();
glClear(GL_DEPTH_BUFFER_BIT); glClear(GL_DEPTH_BUFFER_BIT);
MeshShadowShader->Activate(); if (graphics.shadows)
MeshShadowShader->SetMat4("lightSpaceMatrix", lightSpaceMatrix); RenderShadowCasters(lightSpaceMatrix); // with nothing drawn, the cleared map leaves everything lit
for (Renderable* r : renderables) { RenderLocalShadows();
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);
}
glCullFace(GL_BACK); glCullFace(GL_BACK);
// ---- Main scene ---- // ---- Geometry pass ----
screenfbo->Bind(); RenderGeometryPass(proj, view, camPos);
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);
// 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); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glDisable(GL_DEPTH_TEST); glDepthFunc(GL_LEQUAL);
glDepthMask(GL_FALSE);
SkyboxShader->Activate(); SkyboxShader->Activate();
SkyboxShader->SetMat4("proj", proj); SkyboxShader->SetMat4("proj", proj);
SkyboxShader->SetMat4("view", glm::mat4(glm::mat3(view))); SkyboxShader->SetMat4("view", glm::mat4(glm::mat3(view)));
@@ -279,28 +435,14 @@ void RenderSystem::Render(Camera& camera)
skybox->Bind(); skybox->Bind();
glDrawArrays(GL_TRIANGLES, 0, 36); glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0); glBindVertexArray(0);
glDepthMask(GL_TRUE);
glDepthFunc(GL_LESS);
// ---- Lighting pass ----
glDisable(GL_DEPTH_TEST);
RenderLightingPass(camPos, lightSpaceMatrix);
glEnable(GL_DEPTH_TEST); 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) ---- // ---- Translucent pass (meshes + particles + rigs, sorted back-to-front) ----
// Disable face culling so thin translucent quads render both sides // Disable face culling so thin translucent quads render both sides
glDisable(GL_CULL_FACE); glDisable(GL_CULL_FACE);
@@ -309,17 +451,33 @@ void RenderSystem::Render(Camera& camera)
RenderHitboxDebug(camera); RenderHitboxDebug(camera);
// ---- Framebuffer blit to screen ---- // ---- Bloom ----
screenfbo->Unbind();
glDisable(GL_DEPTH_TEST); 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); glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT); glClear(GL_COLOR_BUFFER_BIT);
ScreenShader->Activate(); if (debugView == 0) {
ScreenShader->Set1F("exposure", 1.0f); ScreenShader->Activate();
quadVAO->Bind(); ScreenShader->Set1F("exposure", 1.0f);
glActiveTexture(GL_TEXTURE0); ScreenShader->Set1F("bloomStrength", graphics.bloom ? 0.6f : 0.0f);
glBindTexture(GL_TEXTURE_2D, textureColorbuffer); ScreenShader->SetInt("screenTexture", 0);
glDrawArrays(GL_TRIANGLES, 0, 6); 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) ---- // ---- GUI (always on top, no depth) ----
glDisable(GL_CULL_FACE); glDisable(GL_CULL_FACE);
@@ -347,6 +505,309 @@ void RenderSystem::Render(Camera& camera)
glfwSwapBuffers(window->handle); 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<glm::mat4>& 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 // Shader preparation helpers
// ============================================================================ // ============================================================================
@@ -438,6 +899,8 @@ Renderable::Renderable() {}
Renderable::~Renderable() Renderable::~Renderable()
{ {
if (occlusionQuery != 0)
glDeleteQueries(1, &occlusionQuery);
engine->rendersystem->RemoveRenderable(this); engine->rendersystem->RemoveRenderable(this);
} }
+51
View File
@@ -4,6 +4,7 @@
#include <vector> #include <vector>
#include <map> #include <map>
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include "Structures.h"
const float skyboxVertices[] = { 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,
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(); void BindToRenderSystem();
virtual void Render(Shader& ShaderProgram) {} virtual void Render(Shader& ShaderProgram) {}
virtual glm::vec3 GetWorldPosition() const { return glm::vec3(0.0f); } 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 { struct HitboxDebugLine {
glm::mat4 transform; glm::mat4 transform;
glm::vec3 color; glm::vec3 color;
@@ -52,8 +69,23 @@ struct HitboxDebugLine {
class RenderSystem { class RenderSystem {
public: public:
Lighting* lighting = nullptr; Lighting* lighting = nullptr;
GraphicsSettings graphics;
FBO* screenfbo = nullptr; FBO* screenfbo = nullptr;
FBO* shadowdepthmapfbo = 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 depthMaptexture = 0;
unsigned int textureColorbuffer = 0; unsigned int textureColorbuffer = 0;
unsigned int rbo = 0; unsigned int rbo = 0;
@@ -66,6 +98,12 @@ public:
Shader* ScreenShader = nullptr; Shader* ScreenShader = nullptr;
Shader* RigShader = nullptr; Shader* RigShader = nullptr;
Shader* MeshShader = 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* ImageBoxShader = nullptr;
Shader* BoxShader = nullptr; Shader* BoxShader = nullptr;
Shader* Text2DShader = nullptr; Shader* Text2DShader = nullptr;
@@ -79,6 +117,7 @@ public:
void Initialize(); void Initialize();
void Resize(int width, int height); void Resize(int width, int height);
void Render(Camera& camera); void Render(Camera& camera);
void CycleDebugView();
void SetHitboxDebugLines(const std::vector<HitboxDebugLine>& lines) { hitboxDebugLines = lines; } void SetHitboxDebugLines(const std::vector<HitboxDebugLine>& lines) { hitboxDebugLines = lines; }
void AddRenderable(Renderable* r) { renderables.push_back(r); } void AddRenderable(Renderable* r) { renderables.push_back(r); }
void RemoveRenderable(Renderable* r) { void RemoveRenderable(Renderable* r) {
@@ -98,5 +137,17 @@ private:
void PrepareTextShader(Camera& camera); void PrepareTextShader(Camera& camera);
void RenderTranslucent(Camera& camera, const glm::mat4& lightSpaceMatrix, const glm::vec3& camPos); void RenderTranslucent(Camera& camera, const glm::mat4& lightSpaceMatrix, const glm::vec3& camPos);
void RenderHitboxDebug(Camera& camera); 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<Renderable*> occlusionCandidates; // drawn into the queries after the geometry pass
void RenderLightingPass(const glm::vec3& camPos, const glm::mat4& lightSpaceMatrix);
void RenderDebugView();
}; };
#endif #endif
+69
View File
@@ -0,0 +1,69 @@
#include "ResourceManager.h"
#include <iostream>
#include <stdexcept>
#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;
}
+22 -9
View File
@@ -1,20 +1,33 @@
#ifndef RESOURCE_MANAGER_CLASS #ifndef RESOURCE_MANAGER_CLASS
#define RESOURCE_MANAGER_CLASS #define RESOURCE_MANAGER_CLASS
#include <string>
#include <unordered_map>
#include <vector> #include <vector>
#include "Folder.h" class Texture;
//#include "font.h" class Font;
#include "Model.h"
#include "SoundSystem.h"
// 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 { class ResourceManager {
public: public:
//std::vector<Font*> fonts; ResourceManager() {}
std::vector<Model*> models; ~ResourceManager(); // needs the GL context, delete this before the window
std::vector<SoundData*> sounds; ResourceManager(const ResourceManager&) = delete;
public:
// 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<std::string, Texture*> textures;
std::unordered_map<std::string, Font*> fonts;
std::vector<Texture*> createdTextures;
}; };
#endif #endif
+57
View File
@@ -0,0 +1,57 @@
#ifndef SURFACE_TEXTURE_CLASS
#define SURFACE_TEXTURE_CLASS
#include <glm/glm.hpp>
#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
+71
View File
@@ -0,0 +1,71 @@
#ifndef TEXTURE_PAINTER_CLASS
#define TEXTURE_PAINTER_CLASS
#include <cmath>
#include <glm/glm.hpp>
#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
+12 -2
View File
@@ -166,15 +166,17 @@
<ItemGroup> <ItemGroup>
<ClCompile Include="Character.cpp" /> <ClCompile Include="Character.cpp" />
<ClCompile Include="CharacterManager.cpp" /> <ClCompile Include="CharacterManager.cpp" />
<ClCompile Include="CollisionMesh.cpp" />
<ClCompile Include="Cubemap.h" /> <ClCompile Include="Cubemap.h" />
<ClCompile Include="EBO.cpp" /> <ClCompile Include="EBO.cpp" />
<ClCompile Include="Engine.cpp" /> <ClCompile Include="Engine.cpp" />
<ClCompile Include="glad.c" /> <ClCompile Include="glad.c" />
<ClCompile Include="main.cpp" /> <ClCompile Include="main.cpp" />
<ClCompile Include="Mouse.cpp" /> <ClCompile Include="Mouse.cpp" />
<ClCompile Include="Net.cpp" />
<ClCompile Include="Physics.cpp" /> <ClCompile Include="Physics.cpp" />
<ClCompile Include="Ragdoll.cpp" />
<ClCompile Include="RenderSystem.cpp" /> <ClCompile Include="RenderSystem.cpp" />
<ClCompile Include="ResourceManager.cpp" />
<ClCompile Include="shaderClass.cpp" /> <ClCompile Include="shaderClass.cpp" />
<ClCompile Include="Sound.cpp" /> <ClCompile Include="Sound.cpp" />
<ClCompile Include="SoundSystem.cpp" /> <ClCompile Include="SoundSystem.cpp" />
@@ -188,18 +190,20 @@
<ClInclude Include="BodyComponentActions.h" /> <ClInclude Include="BodyComponentActions.h" />
<ClInclude Include="Character.h" /> <ClInclude Include="Character.h" />
<ClInclude Include="CharacterManager.h" /> <ClInclude Include="CharacterManager.h" />
<ClInclude Include="CollisionMesh.h" />
<ClInclude Include="common.h" /> <ClInclude Include="common.h" />
<ClInclude Include="DefaultBodyComponents.h" /> <ClInclude Include="DefaultBodyComponents.h" />
<ClInclude Include="Engine.h" /> <ClInclude Include="Engine.h" />
<ClInclude Include="Folder.h" /> <ClInclude Include="Folder.h" />
<ClInclude Include="font.h" /> <ClInclude Include="font.h" />
<ClInclude Include="Gui.h" /> <ClInclude Include="Gui.h" />
<ClInclude Include="Light.h" />
<ClInclude Include="Lighting.h" /> <ClInclude Include="Lighting.h" />
<ClInclude Include="MeshData.h" /> <ClInclude Include="MeshData.h" />
<ClInclude Include="Net.h" />
<ClInclude Include="Particle.h" /> <ClInclude Include="Particle.h" />
<ClInclude Include="Physics.h" /> <ClInclude Include="Physics.h" />
<ClInclude Include="QuadVertices.h" /> <ClInclude Include="QuadVertices.h" />
<ClInclude Include="Ragdoll.h" />
<ClInclude Include="RenderSystem.h" /> <ClInclude Include="RenderSystem.h" />
<ClInclude Include="ResourceManager.h" /> <ClInclude Include="ResourceManager.h" />
<ClInclude Include="Rig.h" /> <ClInclude Include="Rig.h" />
@@ -218,6 +222,8 @@
<ClInclude Include="Object.h" /> <ClInclude Include="Object.h" />
<ClInclude Include="shaderClass.h" /> <ClInclude Include="shaderClass.h" />
<ClInclude Include="SoundSystem.h" /> <ClInclude Include="SoundSystem.h" />
<ClInclude Include="SurfaceTexture.h" />
<ClInclude Include="TexturePainter.h" />
<ClInclude Include="stb_image.h" /> <ClInclude Include="stb_image.h" />
<ClInclude Include="t.h" /> <ClInclude Include="t.h" />
<ClInclude Include="t2d.h" /> <ClInclude Include="t2d.h" />
@@ -231,12 +237,16 @@
<ItemGroup> <ItemGroup>
<None Include="shaders\2DText.frag" /> <None Include="shaders\2DText.frag" />
<None Include="shaders\2DText.vert" /> <None Include="shaders\2DText.vert" />
<None Include="shaders\bloom_blur.frag" />
<None Include="shaders\bloom_extract.frag" />
<None Include="shaders\Box.frag" /> <None Include="shaders\Box.frag" />
<None Include="shaders\Box.vert" /> <None Include="shaders\Box.vert" />
<None Include="shaders\default.frag" /> <None Include="shaders\default.frag" />
<None Include="shaders\default.vert" /> <None Include="shaders\default.vert" />
<None Include="shaders\deferred_lighting.frag" /> <None Include="shaders\deferred_lighting.frag" />
<None Include="shaders\deferred_lighting.vert" /> <None Include="shaders\deferred_lighting.vert" />
<None Include="shaders\gbuffer_debug.frag" />
<None Include="shaders\gbuffer_debug.vert" />
<None Include="shaders\geometry.frag" /> <None Include="shaders\geometry.frag" />
<None Include="shaders\geometry.vert" /> <None Include="shaders\geometry.vert" />
<None Include="shaders\geometry_rig.vert" /> <None Include="shaders\geometry_rig.vert" />
+36
View File
@@ -43,6 +43,15 @@
<ClCompile Include="Physics.cpp"> <ClCompile Include="Physics.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="CollisionMesh.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Ragdoll.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="ResourceManager.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Sound.cpp"> <ClCompile Include="Sound.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
@@ -120,6 +129,18 @@
<ClInclude Include="Physics.h"> <ClInclude Include="Physics.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="CollisionMesh.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Ragdoll.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="SurfaceTexture.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="TexturePainter.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Gui.h"> <ClInclude Include="Gui.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
@@ -162,6 +183,9 @@
<ClInclude Include="MeshData.h"> <ClInclude Include="MeshData.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="Light.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Lighting.h"> <ClInclude Include="Lighting.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
@@ -205,6 +229,18 @@
<None Include="shaders\deferred_lighting.frag"> <None Include="shaders\deferred_lighting.frag">
<Filter>Shaders</Filter> <Filter>Shaders</Filter>
</None> </None>
<None Include="shaders\gbuffer_debug.frag">
<Filter>Shaders</Filter>
</None>
<None Include="shaders\bloom_blur.frag">
<Filter>Shaders</Filter>
</None>
<None Include="shaders\bloom_extract.frag">
<Filter>Shaders</Filter>
</None>
<None Include="shaders\gbuffer_debug.vert">
<Filter>Shaders</Filter>
</None>
<None Include="shaders\deferred_lighting.vert"> <None Include="shaders\deferred_lighting.vert">
<Filter>Shaders</Filter> <Filter>Shaders</Filter>
</None> </None>
+1
View File
@@ -105,6 +105,7 @@ int main() {
engine->Initiate(); engine->Initiate();
std::cout << "engine stopped\n"; std::cout << "engine stopped\n";
delete resourcemanager;
delete window; delete window;
delete mouse; delete mouse;
delete keyboard; delete keyboard;
View File
View File
+27 -12
View File
@@ -6,6 +6,7 @@
#include<glm/glm.hpp> #include<glm/glm.hpp>
#include<string> #include<string>
#include<unordered_map>
#include<fstream> #include<fstream>
#include <sstream> #include <sstream>
#include <iostream> #include <iostream>
@@ -19,35 +20,49 @@ public:
Shader(const char* vertexFile, const char* fragFile); Shader(const char* vertexFile, const char* fragFile);
void Set1F(const char* uniform, float value) { void Set1F(const char* uniform, float value) {
GLint location = glGetUniformLocation(Shader::ID, uniform); glUniform1f(GetLocation(uniform), value);
glUniform1f(location, value);
}; };
void Set2F(const char* uniform, glm::vec2 value) { void Set2F(const char* uniform, glm::vec2 value) {
GLint location = glGetUniformLocation(Shader::ID, uniform); glUniform2f(GetLocation(uniform), value.x,value.y);
glUniform2f(location, value.x,value.y);
}; };
void Set3F(const char* uniform, glm::vec3 value) { void Set3F(const char* uniform, glm::vec3 value) {
GLint location = glGetUniformLocation(Shader::ID, uniform); glUniform3f(GetLocation(uniform), value.x,value.y,value.z);
glUniform3f(location, value.x,value.y,value.z);
}; };
void Set4F(const char* uniform, glm::vec4 value) { void Set4F(const char* uniform, glm::vec4 value) {
GLint location = glGetUniformLocation(Shader::ID, uniform); glUniform4f(GetLocation(uniform), value.x, value.y, value.z, value.w);
glUniform4f(location, value.x, value.y, value.z, value.w);
}; };
void SetMat4(const char* uniform, glm::mat4 value) { void SetMat4(const char* uniform, glm::mat4 value) {
GLint location = glGetUniformLocation(Shader::ID, uniform); glUniformMatrix4fv(GetLocation(uniform),1,GL_FALSE,&value[0][0]);
glUniformMatrix4fv(location,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) { void SetInt(const char* uniform, GLuint value) {
GLint location = glGetUniformLocation(Shader::ID, uniform); glUniform1i(GetLocation(uniform), value);
glUniform1i(location, value);
}; };
void Activate(); void Activate();
void Delete(); 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<std::string, GLint> uniformLocations;
}; };
#endif #endif
+3 -3
View File
@@ -12,7 +12,6 @@ uniform vec2 pixelScale;
uniform vec2 pixelPos; uniform vec2 pixelPos;
uniform vec4 Color = {0.0f,0.0f,0.0f,1.0f}; uniform vec4 Color = {0.0f,0.0f,0.0f,1.0f};
uniform vec4 BackgroundColor = {0.0f,0.0f,0.0f,1.0f};
void main() void main()
{ {
@@ -27,10 +26,11 @@ void main()
vec2 absrelcoord = abs(fragPosTopLeft - pixelScale/2 - pixelPos); vec2 absrelcoord = abs(fragPosTopLeft - pixelScale/2 - pixelPos);
if (!(absrelcoord.x < r1.x || absrelcoord.y < r1.y)) { 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; discard;
}
} }
} }
}
} }
+3 -3
View File
@@ -12,7 +12,6 @@ uniform vec2 pixelScale;
uniform vec2 pixelPos; uniform vec2 pixelPos;
uniform vec4 Color = {0.0f,0.0f,0.0f,1.0f}; 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; uniform sampler2D tex;
@@ -29,10 +28,11 @@ void main()
vec2 absrelcoord = abs(fragPosTopLeft - pixelScale/2 - pixelPos); vec2 absrelcoord = abs(fragPosTopLeft - pixelScale/2 - pixelPos);
if (!(absrelcoord.x < r1.x || absrelcoord.y < r1.y)) { 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; discard;
}
} }
} }
}
} }
+7 -1
View File
@@ -11,6 +11,10 @@ uniform vec2 pixelScale;
uniform float z; uniform float z;
uniform float rotation; 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() void main()
{ {
vec2 unitPos = aPos * 0.5 + 0.5; vec2 unitPos = aPos * 0.5 + 0.5;
@@ -20,5 +24,7 @@ void main()
gl_Position = vec4(ndc, 0.0f, 1.0f); 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;
} }
+4 -2
View File
@@ -11,12 +11,14 @@ out vec3 FragPos;
uniform mat4 view; uniform mat4 view;
uniform mat4 proj; 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() void main()
{ {
FragPos = vec3(modl * vec4(aPos, 1.0)); FragPos = vec3(modl * vec4(aPos, 1.0));
normal = aNormal; normal = aNormal;
texCoord = aTex; texCoord = (uvMode == 1 ? aPos.xy * tileScale.xy : aTex) + uvOffset;
gl_Position = proj* view * modl * vec4(aPos,1.0f); gl_Position = proj* view * modl * vec4(aPos,1.0f);
} }
+6 -6
View File
@@ -4,6 +4,8 @@ out vec4 FragColor;
in vec2 TexCoords; in vec2 TexCoords;
uniform sampler2D screenTexture; uniform sampler2D screenTexture;
uniform sampler2D bloomTexture;
uniform float bloomStrength = 0.0f;
uniform float brightness = 0.0f; uniform float brightness = 0.0f;
uniform float colorContrast = 0.0f; uniform float colorContrast = 0.0f;
uniform float saturation = 1.0f; uniform float saturation = 1.0f;
@@ -21,21 +23,19 @@ vec3 ACESFilm(vec3 x)
void main() 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); vec3 finalcolor = ACESFilm(originalcolor * exposure);
const float gamma = 2.2; const float gamma = 2.2;
finalcolor = pow(finalcolor, vec3(1.0 / gamma)); finalcolor = pow(finalcolor, vec3(1.0 / gamma));
vec3 contrast = vec3(1.0f) - finalcolor;
finalcolor = mix(finalcolor,vec3(1.0f) - finalcolor,colorContrast); finalcolor = mix(finalcolor,vec3(1.0f) - finalcolor,colorContrast);
float average = (finalcolor[0]+finalcolor[1]+finalcolor[2])/3.0f; float average = (finalcolor[0]+finalcolor[1]+finalcolor[2])/3.0f;
for (int i = 0; i < 3; i++) { finalcolor = mix(vec3(average), finalcolor, saturation);
finalcolor[i] = average + saturation * (finalcolor[i] - average);
}
FragColor = vec4(finalcolor,1.0f); FragColor = vec4(finalcolor,1.0f);
} }
+23
View File
@@ -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);
}
+26
View File
@@ -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);
}
+150 -13
View File
@@ -12,7 +12,42 @@ struct DirLight {
float intensity; 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 vec3 viewPos;
uniform sampler2DShadow shadowMap; uniform sampler2DShadow shadowMap;
@@ -23,6 +58,7 @@ uniform int useTexture = 0;
uniform float metallic = 0.0; uniform float metallic = 0.0;
uniform float roughness = 0.7; uniform float roughness = 0.7;
uniform float ao = 1.0; uniform float ao = 1.0;
uniform float emissive = 0.0;
uniform float envIntensity = 0.8; uniform float envIntensity = 0.8;
const float PI = 3.14159265359; const float PI = 3.14159265359;
@@ -72,6 +108,65 @@ vec3 EnvironmentBRDF(vec3 F0, float rough, float NdotV)
return F0 * AB.x + AB.y; 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) float ShadowCalculation(vec4 fragPosLightSpace, vec3 N, vec3 L)
{ {
vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w; vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
@@ -112,21 +207,63 @@ void main()
vec3 N = normalize(normal); vec3 N = normalize(normal);
vec3 V = normalize(viewPos - FragPos); vec3 V = normalize(viewPos - FragPos);
vec3 L = normalize(-dirLight.direction);
vec3 H = normalize(V + L);
float NdotV = max(dot(N, V), 1e-4); 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 F0 = mix(vec3(0.04), albedo, metal);
vec3 F = FresnelSchlick(max(dot(H, V), 0.0), F0); vec3 direct = vec3(0.0);
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;
float shadow = ShadowCalculation(fragposlightspace, N, L); // Facing away from the sun has no direct light, so skip the BRDF and the 9 shadow samples
vec3 direct = (kD * albedo / PI + specular) * radiance * NdotL * shadow; 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 ambientF = FresnelSchlickRoughness(NdotV, F0, rough);
vec3 ambientKD = (vec3(1.0) - ambientF) * (1.0 - metal); vec3 ambientKD = (vec3(1.0) - ambientF) * (1.0 - metal);
@@ -136,5 +273,5 @@ void main()
vec3 specularIBL = prefiltered * EnvironmentBRDF(F0, rough, NdotV); vec3 specularIBL = prefiltered * EnvironmentBRDF(F0, rough, NdotV);
vec3 ambient = (ambientKD * irradiance * albedo + specularIBL) * ao; vec3 ambient = (ambientKD * irradiance * albedo + specularIBL) * ao;
FragColor = vec4(direct + ambient, albedoSample.a); FragColor = vec4(direct + ambient + albedo * emissive * 4.0, albedoSample.a);
} }
+29 -5
View File
@@ -13,11 +13,35 @@ uniform mat4 proj;
uniform mat4 lightSpaceMatrix; 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() void main()
{ {
FragPos = vec3(modl * vec4(aPos, 1.0)); vec4 worldPos = modl * vec4(aPos, 1.0);
normal = transpose(inverse(mat3(modl))) * aNormal; FragPos = worldPos.xyz;
texCoord = aTex; normal = CofactorMatrix(mat3(modl)) * aNormal;
fragposlightspace = lightSpaceMatrix * vec4(FragPos, 1.0); texCoord = SurfaceUV();
gl_Position = proj* view * modl * vec4(aPos,1.0f); fragposlightspace = lightSpaceMatrix * worldPos;
gl_Position = proj * view * worldPos;
} }
+251 -37
View File
@@ -3,59 +3,273 @@ out vec4 FragColor;
in vec2 TexCoord; in vec2 TexCoord;
uniform sampler2D gPosition;
uniform sampler2D gNormal;
uniform sampler2D gAlbedo;
uniform sampler2D gLightSpacePos;
uniform sampler2DShadow shadowMap;
struct DirLight { struct DirLight {
vec3 direction; vec3 direction;
vec3 ambient; vec3 color;
vec3 diffuse; float intensity;
vec3 specular;
}; };
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; float a = rough * rough;
projCoords = projCoords * 0.5 + 0.5; float a2 = a * a;
if (projCoords.z > 1.0) return 0.0; float NdotH = max(dot(N, H), 0.0);
return texture(shadowMap, vec3(projCoords.xy, projCoords.z)); 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 r = rough + 1.0;
float diff = max(dot(normal, lightDir), 0.0); float k = (r * r) / 8.0;
vec3 halfwayDir = normalize(lightDir + viewDir); return NdotV / (NdotV * (1.0 - k) + k);
float spec = pow(max(dot(normal, halfwayDir), 0.0), 32.0); }
vec3 ambient = light.ambient * albedo; float GeometrySmith(vec3 N, vec3 V, vec3 L, float rough)
vec3 diffuse = light.diffuse * diff * albedo; {
vec3 specular = light.specular * spec; 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); vec3 FresnelSchlick(float cosTheta, vec3 F0)
return ambient + (diffuse + specular) * shadow; {
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() void main()
{ {
vec3 fragPos = texture(gPosition, TexCoord).rgb; vec3 normal = texture(gNormal, TexCoord).rgb;
vec3 normal = normalize(texture(gNormal, TexCoord).rgb);
vec4 albedaSample = texture(gAlbedo, TexCoord);
vec4 fragPosLS = texture(gLightSpacePos, TexCoord);
// Sky (no geometry written) — output clear color // Nothing was written here, keep the skybox underneath
if (length(normal) < 0.1) { if (dot(normal, normal) < 0.01)
FragColor = vec4(0.0, 0.0, 0.0, 0.0); discard;
return;
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); for (int i = 0; i < directionalLightCount; i++) {
vec3 lit = CalcDirLight(dirLight, normal, viewDir, fragPosLS, albedaSample.rgb); 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);
} }
+28 -18
View File
@@ -6,36 +6,46 @@ in vec2 TexCoord;
uniform sampler2D gPosition; uniform sampler2D gPosition;
uniform sampler2D gNormal; uniform sampler2D gNormal;
uniform sampler2D gAlbedo; uniform sampler2D gAlbedo;
uniform sampler2D gLightSpacePos; uniform sampler2D gMaterial;
uniform sampler2DShadow shadowMap;
uniform sampler2D depthTex; 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() void main()
{ {
FragColor = vec4(0.0, 0.0, 0.0, 1.0);
if (debugMode == 1) { if (debugMode == 1) {
vec3 v = texture(gPosition, TexCoord).rgb; vec3 v = texture(gPosition, TexCoord).rgb;
FragColor = vec4(v * 0.01 + 0.5, 1.0); FragColor = vec4(v * 0.01 + 0.5, 1.0);
return;
} }
if (debugMode == 2) { else if (debugMode == 2) {
vec3 n = normalize(texture(gNormal, TexCoord).rgb); vec3 n = texture(gNormal, TexCoord).rgb;
FragColor = vec4(n * 0.5 + 0.5, 1.0); if (length(n) >= 0.1)
return; FragColor = vec4(normalize(n) * 0.5 + 0.5, 1.0);
} }
if (debugMode == 3) { else if (debugMode == 3) {
FragColor = texture(gAlbedo, TexCoord); FragColor = vec4(pow(texture(gAlbedo, TexCoord).rgb, vec3(1.0 / 2.2)), 1.0);
return;
} }
if (debugMode == 4) { else if (debugMode == 4) {
FragColor = texture(gLightSpacePos, TexCoord); FragColor = vec4(texture(gMaterial, TexCoord).rgb, 1.0);
return;
} }
if (debugMode == 5) { else if (debugMode == 5) {
float d = texture(depthTex, TexCoord).r; // 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); 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);
}
} }
+21 -9
View File
@@ -1,23 +1,35 @@
#version 330 core #version 330 core
layout (location = 0) out vec3 gPosition; layout (location = 0) out vec3 gPosition;
layout (location = 1) out vec3 gNormal; layout (location = 1) out vec3 gNormal;
layout (location = 2) out vec4 gAlbedo; layout (location = 2) out vec4 gAlbedo; // linear rgb + alpha
layout (location = 3) out vec4 gLightSpacePos; layout (location = 3) out vec4 gMaterial; // r = metallic, g = roughness, b = ao, a = emissive
in vec3 FragPos; in vec3 FragPos;
in vec3 Normal; in vec3 Normal;
in vec2 TexCoord; in vec2 TexCoord;
in vec4 FragPosLightSpace;
uniform sampler2D tex; 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() void main()
{ {
vec4 albedo = texture(tex, TexCoord); vec4 albedo = vec4(0.8, 0.8, 0.8, 1.0);
if (albedo.a < 0.1) discard; if (useTexture != 0) {
albedo = texture(tex, TexCoord);
if (albedo.a < 0.1)
discard;
albedo.rgb = pow(albedo.rgb, vec3(2.2));
}
gPosition = FragPos; albedo.rgb = mix(albedo.rgb, colorOverride.rgb, colorOverride.w);
gNormal = normalize(Normal);
gAlbedo = albedo; gPosition = FragPos;
gLightSpacePos = FragPosLightSpace; 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));
} }
+25 -5
View File
@@ -6,19 +6,39 @@ layout (location = 2) in vec2 aTexCoord;
out vec3 FragPos; out vec3 FragPos;
out vec3 Normal; out vec3 Normal;
out vec2 TexCoord; out vec2 TexCoord;
out vec4 FragPosLightSpace;
uniform mat4 proj; uniform mat4 proj;
uniform mat4 view; uniform mat4 view;
uniform mat4 modl; 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() void main()
{ {
vec4 worldPos = modl * vec4(aPos, 1.0); vec4 worldPos = modl * vec4(aPos, 1.0);
FragPos = worldPos.xyz; FragPos = worldPos.xyz;
Normal = normalize(mat3(transpose(inverse(modl))) * aNormal); Normal = normalize(CofactorMatrix(mat3(modl)) * aNormal);
TexCoord = aTexCoord; TexCoord = SurfaceUV();
FragPosLightSpace = lightSpaceMatrix * worldPos;
gl_Position = proj * view * worldPos; gl_Position = proj * view * worldPos;
} }
+48 -15
View File
@@ -8,27 +8,60 @@ layout (location = 4) in vec4 weights;
out vec3 FragPos; out vec3 FragPos;
out vec3 Normal; out vec3 Normal;
out vec2 TexCoord; out vec2 TexCoord;
out vec4 FragPosLightSpace;
uniform mat4 proj; uniform mat4 proj;
uniform mat4 view; uniform mat4 view;
uniform mat4 modl; 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() void main()
{ {
mat4 boneTransform = finalBonesMatrices[boneIds[0]] * weights[0]; vec4 totalPosition = vec4(0.0);
boneTransform += finalBonesMatrices[boneIds[1]] * weights[1]; vec3 totalNormal = vec3(0.0);
boneTransform += finalBonesMatrices[boneIds[2]] * weights[2];
boneTransform += finalBonesMatrices[boneIds[3]] * weights[3];
vec4 localPos = boneTransform * vec4(aPos, 1.0); for (int i = 0; i < MAX_BONE_INFLUENCE; i++)
vec4 worldPos = modl * localPos; {
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; vec4 worldPos = modl * totalPosition;
Normal = normalize(mat3(transpose(inverse(modl * boneTransform))) * aNormal);
TexCoord = aTexCoord; FragPos = worldPos.xyz;
FragPosLightSpace = lightSpaceMatrix * worldPos; Normal = normalize(CofactorMatrix(mat3(modl)) * totalNormal);
gl_Position = proj * view * worldPos; TexCoord = SurfaceUV();
} gl_Position = proj * view * worldPos;
}
+30 -6
View File
@@ -20,7 +20,30 @@ out vec3 normal;
out vec2 texCoord; out vec2 texCoord;
out vec3 FragPos; out vec3 FragPos;
out vec4 fragposlightspace; 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() void main()
{ {
vec4 totalPosition = vec4(0.0); vec4 totalPosition = vec4(0.0);
@@ -40,9 +63,10 @@ void main()
totalPosition += (finalBonesMatrices[boneIds[i]] * vec4(pos, 1.0))* weights[i]; totalPosition += (finalBonesMatrices[boneIds[i]] * vec4(pos, 1.0))* weights[i];
totalNormal += mat3(finalBonesMatrices[boneIds[i]]) * norm * weights[i]; totalNormal += mat3(finalBonesMatrices[boneIds[i]]) * norm * weights[i];
} }
FragPos = vec3(modl * totalPosition); vec4 worldPos = modl * totalPosition;
fragposlightspace = lightSpaceMatrix * vec4(FragPos, 1.0); FragPos = worldPos.xyz;
gl_Position = proj * view * modl * totalPosition; fragposlightspace = lightSpaceMatrix * worldPos;
normal = normalize(mat3(transpose(inverse(modl))) * totalNormal); gl_Position = proj * view * worldPos;
texCoord = tex; normal = normalize(CofactorMatrix(mat3(modl)) * totalNormal);
texCoord = SurfaceUV();
} }
+1 -2
View File
@@ -10,6 +10,5 @@ void main()
{ {
TexCoords = aPos; TexCoords = aPos;
vec4 pos = proj * view * vec4(aPos, 1.0); vec4 pos = proj * view * vec4(aPos, 1.0);
//gl_Position = pos.xyww gl_Position = pos.xyww; // sits on the far plane, so it only draws where no geometry is
gl_Position = pos;
} }
+10 -23
View File
@@ -30,31 +30,18 @@ public:
return tr; return tr;
} }
BoundingBox GetAABB() { BoundingBox GetAABB() const {
std::vector<glm::vec3> corners = { // Each world axis reaches as far as the rotated box's axes add up to, no need to visit the corners
{0.5f,0.5f,0.5f}, const glm::mat3 rotation = glm::mat3_cast(orient);
{0.5f,-0.5f,0.5f}, const glm::vec3 half = scale * 0.5f;
{0.5f,-0.5f,-0.5f}, const glm::vec3 extent =
{0.5f,0.5f,-0.5f}, glm::abs(rotation[0]) * half.x +
{-0.5f,0.5f,0.5f}, glm::abs(rotation[1]) * half.y +
{-0.5f,-0.5f,0.5f}, glm::abs(rotation[2]) * half.z;
{-0.5f,0.5f,-0.5f},
{-0.5f,-0.5f,-0.5f},
};
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; BoundingBox tr;
tr.min = min; tr.min = pos - extent;
tr.max = max; tr.max = pos + extent;
//std::cout << tr.min.x << ' ' << tr.min.y << ' ' << tr.min.z << ' ' << tr.max.x << ' ' << tr.max.y << ' ' << tr.max.z << '\n';
return tr; return tr;
} }
+39
View File
@@ -5,6 +5,45 @@ glm::vec3 CalculateTriangleNormal(Triangle t) {
return glm::normalize(glm::cross(t.b - t.a, t.c - t.a)); 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) { float Magnitude2(const glm::vec3& v) {
return v.x*v.x + v.y*v.y + v.z*v.z; return v.x*v.x + v.y*v.y + v.z*v.z;
} }
+1
View File
@@ -28,6 +28,7 @@ struct AxisCollisionCN {
}; };
glm::vec3 CalculateTriangleNormal(Triangle t); glm::vec3 CalculateTriangleNormal(Triangle t);
bool TriangleInTriangle(const Triangle& t1, const Triangle& t2);
float Magnitude2(const glm::vec3& v); float Magnitude2(const glm::vec3& v);
+36 -26
View File
@@ -1,51 +1,61 @@
#include "texture.h" #include "texture.h"
#include <stdexcept>
#include "stb_image.h"
Texture::Texture(const char* filename) : path(filename) Texture::Texture(const char* filename) : path(filename)
{ {
int widthImg, heightImg, nrComponents; int nrComponents;
stbi_set_flip_vertically_on_load(true); 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) if (not bytes)
{ throw std::runtime_error("Failed to load texture: " + path);
std::cout << "Failed to load texture" << std::endl;
throw; 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); glGenTextures(1, &ID);
glBindTexture(GL_TEXTURE_2D, 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_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
GLenum format; glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
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);
glGenerateMipmap(GL_TEXTURE_2D); glGenerateMipmap(GL_TEXTURE_2D);
stbi_image_free(bytes);
glBindTexture(GL_TEXTURE_2D, 0); glBindTexture(GL_TEXTURE_2D, 0);
} }
void Texture::texUnit(Shader shader, const char* uniform, GLuint unit) { void Texture::Upload()
shader.SetInt(uniform,unit); {
}
void Texture::Bind() {
glBindTexture(GL_TEXTURE_2D, ID); glBindTexture(GL_TEXTURE_2D, ID);
} glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
glGenerateMipmap(GL_TEXTURE_2D);
void Texture::Unbind() {
glBindTexture(GL_TEXTURE_2D, 0); glBindTexture(GL_TEXTURE_2D, 0);
} }
void Texture::Delete() { void Texture::Delete() {
glDeleteTextures(1, &ID); glDeleteTextures(1, &ID);
} }
+17 -6
View File
@@ -2,20 +2,31 @@
#ifndef TEXTURE_CLASS_H #ifndef TEXTURE_CLASS_H
#define TEXTURE_CLASS_H #define TEXTURE_CLASS_H
#include "stb_image.h" #include <string>
#include <vector>
#include <glad/glad.h> #include <glad/glad.h>
#include "shaderClass.h" // A loaded image and nothing else. Game elements wear one through a SurfaceTexture,
// get these from the ResourceManager.
class Texture { class Texture {
public: public:
GLuint ID; GLuint ID;
std::string path; std::string path;
int width = 0;
int height = 0;
std::vector<unsigned char> pixels; // RGBA, row 0 is the bottom of the image
Texture(const char* filename); Texture(const char* filename);
void texUnit(Shader shader, const char* uniform, GLuint unit); Texture(int newWidth, int newHeight); // blank white
void Bind(); Texture(const Texture& source); // an independent copy with its own GL texture
void Unbind();
// Sends pixels to the GPU after they've been edited
void Upload();
void Delete(); void Delete();
private:
void CreateGLTexture();
}; };
#endif #endif