This commit is contained in:
2026-11-03 20:05:02 -05:00
parent 7b32ad674e
commit 95c79efe83
38 changed files with 1636 additions and 2561 deletions
-105
View File
@@ -1,105 +0,0 @@
#ifndef ANIMATOR_CLASS
#define ANIMATOR_CLASS
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <map>
#include <vector>
#include <string>
#include <assimp/scene.h>
#include <assimp/Importer.hpp>
#include "Rig.h"
// Scale of the "Head" bone (and what hangs off it), around the neck. 1 = as authored.
inline float HeadScale = 1.0f;
struct AssimpNodeData
{
glm::mat4 transformation;
std::string name;
int childrenCount;
std::vector<AssimpNodeData> children;
};
// ============================================================================
// Animation
// ============================================================================
class Animation
{
public:
Animation() = default;
Animation(const std::string& animationPath, Rig* rig = nullptr)
{
Assimp::Importer importer;
const aiScene* scene = importer.ReadFile(animationPath, aiProcess_Triangulate);
assert(scene && scene->mRootNode);
const aiAnimation* anim = scene->mAnimations[0];
m_Duration = (float)anim->mDuration;
m_TicksPerSecond = (int)(anim->mTicksPerSecond != 0 ? anim->mTicksPerSecond : 24);
ReadHierarchyData(m_RootNode, scene->mRootNode);
for (unsigned int i = 0; i < anim->mNumChannels; i++) {
const aiNodeAnim* channel = anim->mChannels[i];
m_Bones.emplace_back(channel->mNodeName.data, 0, channel);
m_BoneNames.push_back(channel->mNodeName.data);
}
if (rig) RegisterBonesWithRig(*rig);
}
void RegisterBonesWithRig(Rig& rig)
{
for (const std::string& name : m_BoneNames) {
if (rig.m_BoneInfoMap.find(name) == rig.m_BoneInfoMap.end()) {
BoneInfo info;
info.id = rig.m_BoneCounter++;
info.offset = glm::mat4(1.0f);
rig.m_BoneInfoMap[name] = info;
}
}
}
Bone* FindBone(const std::string& name)
{
for (Bone& b : m_Bones)
if (b.GetBoneName() == name) return &b;
return nullptr;
}
float GetTicksPerSecond() const { return (float)m_TicksPerSecond; }
float GetDuration() const { return m_Duration; }
const AssimpNodeData& GetRootNode() const { return m_RootNode; }
public:
float m_Duration = 0.0f;
int m_TicksPerSecond = 24;
std::vector<Bone> m_Bones;
std::vector<std::string> m_BoneNames;
AssimpNodeData m_RootNode;
private:
void ReadHierarchyData(AssimpNodeData& dest, const aiNode* src)
{
assert(src);
dest.name = src->mName.data;
dest.transformation = AssimpGLMHelpers::ConvertMatrixToGLMFormat(src->mTransformation);
dest.childrenCount = (int)src->mNumChildren;
for (unsigned int i = 0; i < src->mNumChildren; i++) {
AssimpNodeData child;
ReadHierarchyData(child, src->mChildren[i]);
dest.children.push_back(child);
}
}
};
// ============================================================================
// Animator
// blendWeight ramps from 0?1 over blendDuration when PlayAnimation is called.
// CharacterBody::GetBlendedBoneMatrices() lerps all animators by blendWeight.
// ============================================================================
class Animator
{
public:
+16 -2
View File
@@ -4,6 +4,7 @@
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/matrix_transform.hpp>
#include <map> #include <map>
#include <unordered_map>
#include <vector> #include <vector>
#include <string> #include <string>
#include <functional> #include <functional>
@@ -98,8 +99,9 @@ private:
// ============================================================================ // ============================================================================
// Animator // Animator
// blendWeight ramps from 0?1 over blendDuration when PlayAnimation is called. // blendWeight ramps from 0?1 over blendDuration when PlayAnimation is called,
// CharacterBody::GetBlendedBoneMatrices() lerps all animators by blendWeight. // and back down over the end of an animation that doesn't loop.
// CharacterBody layers animators bone by bone: each one only moves the bones its animation has tracks for.
// ============================================================================ // ============================================================================
class Animator class Animator
{ {
@@ -172,11 +174,21 @@ public:
} }
} }
// A one-shot fades out over its last blendDuration instead of snapping back
if (!loops && blendDuration > 0.0f) {
const float remaining = (m_CurrentAnimation->GetDuration() - m_CurrentTime)
/ glm::max(m_CurrentAnimation->GetTicksPerSecond() * playbackSpeed, 1e-4f);
blendWeight = glm::min(blendWeight, glm::clamp(remaining / blendDuration, 0.0f, 1.0f));
}
CalculateBoneTransform(&m_CurrentAnimation->GetRootNode(), glm::mat4(1.0f)); CalculateBoneTransform(&m_CurrentAnimation->GetRootNode(), glm::mat4(1.0f));
} }
const std::vector<glm::mat4>& GetFinalBoneMatrices() const { return m_FinalBoneMatrices; } const std::vector<glm::mat4>& GetFinalBoneMatrices() const { return m_FinalBoneMatrices; }
// This frame's transform of each bone relative to its parent, only for bones the animation has a track for
const std::unordered_map<std::string, glm::mat4>& GetLocalTransforms() const { return m_LocalTransforms; }
public: public:
std::vector<glm::mat4> m_FinalBoneMatrices; std::vector<glm::mat4> m_FinalBoneMatrices;
Animation* m_CurrentAnimation = nullptr; Animation* m_CurrentAnimation = nullptr;
@@ -186,6 +198,7 @@ public:
private: private:
std::map<std::string, BoneInfo> m_BoneInfoMap; std::map<std::string, BoneInfo> m_BoneInfoMap;
std::unordered_map<std::string, glm::mat4> m_LocalTransforms;
float _blendTimer = 0.0f; float _blendTimer = 0.0f;
void RebuildBoneInfoMap() void RebuildBoneInfoMap()
@@ -205,6 +218,7 @@ private:
if (bone) { if (bone) {
bone->Update(m_CurrentTime); bone->Update(m_CurrentTime);
nodeTransform = bone->GetLocalTransform(); nodeTransform = bone->GetLocalTransform();
m_LocalTransforms[nodeName] = nodeTransform; // the Animation is shared, so keep this animator's own copy
} }
glm::mat4 globalTransform = parentTransform * nodeTransform; glm::mat4 globalTransform = parentTransform * nodeTransform;
-7
View File
@@ -1,7 +0,0 @@
#pragma once
#include "Character.h"
void Footstep(CharacterBody* body, BodyComponent* bc) {
body->t.TranslateBy(body->t.GetFrontVector() * 0.1f);
}
+206 -81
View File
@@ -1,111 +1,236 @@
#include "Character.h" #include "Character.h"
#include "Animator.h" #include "Animator.h"
#include "CharacterMind.h"
#include "Ragdoll.h" #include "Ragdoll.h"
#include "Physics.h"
#include "tFunctions.h" #include "tFunctions.h"
#include <cctype>
#include <cfloat> #include <cfloat>
#include <cmath> #include <cmath>
#include <glm/gtc/quaternion.hpp> #include <glm/gtc/quaternion.hpp>
// ------------------------------------------------------- // -------------------------------------------------------
// BodyComponent // CharacterBody
// ------------------------------------------------------- // -------------------------------------------------------
BodyComponent* CharacterBody::GetComponentOfName(std::string name) { void CharacterBody::Perform(const CharacterAction& action) {
for (BodyComponent* C : components) { switch (action.type) {
if (C->name == name) case CharacterActionType::Move: {
return C; wishDirection = glm::vec3(action.direction.x, 0.0f, action.direction.z);
} if (glm::length(wishDirection) > 0.0001f) {
return nullptr; wishDirection = glm::normalize(wishDirection);
} turnDirection = wishDirection;
void CharacterBody::ExecuteAction(InputAction* action) {
for (BodyComponent* C : components) {
if (C->name == action->BodyComponentName) {
for (Action* a : C->Actions) {
if (a->Name == action->ActionName) {
a->FunctionPointer(this, C);
break;
}
}
break;
} }
break;
}
} }
} }
// ------------------------------------------------------- void CharacterBody::ApplyMovement(bool grounded, float speedFactor, float dt, float referenceDt) {
// CharacterMind const bool moving = glm::length(wishDirection) > 0.0f && speedFactor > 0.0f;
// ------------------------------------------------------- SetWalking(moving);
wishDirection = glm::vec3(0.0f); // used up, the next Move action has to ask again
InputAction* CharacterMind::ProduceAction() { // With time stopped there's nothing to apply, and the rescaling below would divide zero by zero
// TODO: implement if (!physics || dt <= 0.0f)
return nullptr; return;
// Walks the way it faces rather than straight along the input, so it turns into the direction first
const float movespeed = walkSpeed * speedFactor;
const glm::vec3 inputVel = moving ? GetForwardDirection() * movespeed : glm::vec3(0.0f);
const float accel = 40.0f;
const float damp = 0.75f;
const float airControl = 0.5f;
const float groundTopSpeed = movespeed * accel * referenceDt * damp / (1.0f - damp);
if (grounded) {
// Damping and acceleration worked out per reference step, then rescaled so frame rate doesn't change them
const float dampStep = std::pow(damp, dt / referenceDt);
const float accelScale = referenceDt * damp * (1.0f - dampStep) / (dt * dampStep * (1.0f - damp));
physics->linearvelocity.x *= dampStep;
physics->linearvelocity.z *= dampStep;
physics->linearvelocity.x += inputVel.x * accel * dt * accelScale;
physics->linearvelocity.z += inputVel.z * accel * dt * accelScale;
}
else if (moving) {
// In the air it can only steer, up to walking speed
const glm::vec3 wishDir = glm::normalize(inputVel);
const float current = glm::dot(glm::vec3(physics->linearvelocity.x, 0.0f, physics->linearvelocity.z), wishDir);
const float add = glm::clamp(groundTopSpeed - current, 0.0f, accel * airControl * movespeed * dt);
physics->linearvelocity.x += wishDir.x * add;
physics->linearvelocity.z += wishDir.z * add;
}
}
void CharacterBody::Step(float dt) {
if (glm::length(turnDirection) > 0.0001f) {
const glm::quat target = LookAt(-turnDirection);
const glm::quat current = t.GetRotationQuaternion();
const float blend = 1.0f - std::exp(-turnSpeed * dt);
t.RotateToQuaternion(glm::normalize(glm::slerp(current, target, blend)));
}
const float target = walking ? 1.0f : 0.0f;
walkBlend += (target - walkBlend) * (1.0f - std::exp(-walkFadeSpeed * dt));
for (Animator* a : animators)
a->Step(dt);
// After stepping, since the animator ramps its own weight up to full when it plays
if (walkAnimator)
walkAnimator->blendWeight = walkBlend;
if (rig && animator->m_CurrentAnimation) {
blendedBones = animator->GetFinalBoneMatrices();
BlendBones(animator->m_CurrentAnimation->GetRootNode(), glm::mat4(1.0f));
}
}
// Blends two parent-relative transforms, position and scale linearly and rotation along the shortest arc
static glm::mat4 BlendLocalTransforms(const glm::mat4& from, const glm::mat4& to, float t) {
const glm::vec3 fromScale(glm::length(glm::vec3(from[0])), glm::length(glm::vec3(from[1])), glm::length(glm::vec3(from[2])));
const glm::vec3 toScale(glm::length(glm::vec3(to[0])), glm::length(glm::vec3(to[1])), glm::length(glm::vec3(to[2])));
const glm::mat3 fromRotation(glm::vec3(from[0]) / fromScale.x, glm::vec3(from[1]) / fromScale.y, glm::vec3(from[2]) / fromScale.z);
const glm::mat3 toRotation(glm::vec3(to[0]) / toScale.x, glm::vec3(to[1]) / toScale.y, glm::vec3(to[2]) / toScale.z);
const glm::quat rotation = glm::normalize(glm::slerp(glm::quat_cast(fromRotation), glm::quat_cast(toRotation), t));
glm::mat4 result = glm::mat4_cast(rotation) * glm::scale(glm::mat4(1.0f), glm::mix(fromScale, toScale, t));
result[3] = glm::vec4(glm::mix(glm::vec3(from[3]), glm::vec3(to[3]), t), 1.0f);
return result;
}
// Every node's rest transform relative to the model, and its parent's name, from one clip's hierarchy
static void CollectRestPose(const AssimpNodeData& node, const glm::mat4& parentTransform, const std::string& parentName,
std::unordered_map<std::string, glm::mat4>& globals, std::unordered_map<std::string, std::string>& parents) {
const glm::mat4 global = parentTransform * node.transformation;
globals[node.name] = global;
parents[node.name] = parentName;
for (const AssimpNodeData& child : node.children)
CollectRestPose(child, global, node.name, globals, parents);
}
// A clip exported with only some bones, like an arm on its own, has them hung off whatever ancestor made it into
// the file. Its tracks are then relative to that ancestor instead of the bone's real parent. Going through the rest
// pose of both files turns one into the other, so the arm still rides on the torso.
const std::unordered_map<std::string, glm::mat4>& CharacterBody::GetParentCorrections(const Animation* layer) {
const Animation* base = animator->m_CurrentAnimation;
const auto key = std::make_pair(base, layer);
const auto cached = parentCorrections.find(key);
if (cached != parentCorrections.end())
return cached->second;
std::unordered_map<std::string, glm::mat4>& corrections = parentCorrections[key];
if (!base || !layer || base == layer)
return corrections;
std::unordered_map<std::string, glm::mat4> baseGlobals, layerGlobals;
std::unordered_map<std::string, std::string> baseParents, layerParents;
CollectRestPose(base->GetRootNode(), glm::mat4(1.0f), "", baseGlobals, baseParents);
CollectRestPose(layer->GetRootNode(), glm::mat4(1.0f), "", layerGlobals, layerParents);
for (const std::string& bone : layer->m_BoneNames) {
const auto baseParent = baseParents.find(bone);
const auto layerParent = layerParents.find(bone);
if (baseParent == baseParents.end() || layerParent == layerParents.end())
continue;
if (baseParent->second == layerParent->second)
continue;
const auto baseParentGlobal = baseGlobals.find(baseParent->second);
const auto layerParentGlobal = layerGlobals.find(layerParent->second);
const glm::mat4 from = layerParentGlobal != layerGlobals.end() ? layerParentGlobal->second : glm::mat4(1.0f);
const glm::mat4 to = baseParentGlobal != baseGlobals.end() ? baseParentGlobal->second : glm::mat4(1.0f);
corrections[bone] = glm::inverse(to) * from;
}
return corrections;
}
// Walks the base animation's hierarchy. Each bone starts from its rest pose, then every layer with a track for it
// blends in on top in weight order, before it's passed down to its children.
void CharacterBody::BlendBones(const AssimpNodeData& node, const glm::mat4& parentTransform) {
glm::mat4 local = node.transformation;
for (Animator* a : animators) {
if (!a->m_CurrentAnimation || a->blendWeight <= 0.0f)
continue;
if (a != animator && !a->isPlaying)
continue;
const auto& transforms = a->GetLocalTransforms();
const auto found = transforms.find(node.name);
if (found == transforms.end())
continue;
glm::mat4 layerLocal = found->second;
if (a != animator) {
const auto& corrections = GetParentCorrections(a->m_CurrentAnimation);
const auto correction = corrections.find(node.name);
if (correction != corrections.end())
layerLocal = correction->second * layerLocal;
}
local = a == animator || a->blendWeight >= 1.0f
? layerLocal
: BlendLocalTransforms(local, layerLocal, a->blendWeight);
}
glm::mat4 global = parentTransform * local;
if (node.name == "Head")
global = glm::scale(global, glm::vec3(HeadScale));
const auto info = rig->m_BoneInfoMap.find(node.name);
if (info != rig->m_BoneInfoMap.end()) {
const int index = info->second.id;
if (index >= 0 && index < (int)blendedBones.size())
blendedBones[index] = global * info->second.offset;
}
for (const AssimpNodeData& child : node.children)
BlendBones(child, global);
}
void CharacterBody::SetWalking(bool isWalking) {
walking = isWalking;
}
glm::vec3 CharacterBody::GetForwardDirection() const {
glm::vec3 forward = -t.GetFrontVector();
forward.y = 0.0f;
const float length = glm::length(forward);
if (length <= 0.0001f)
return glm::vec3(0.0f, 0.0f, -1.0f);
return forward / length;
} }
// ------------------------------------------------------- // -------------------------------------------------------
// Character // Character
// ------------------------------------------------------- // -------------------------------------------------------
void Character::PressInput(char letter) { void Character::Think() {
letter = (char)std::toupper((unsigned char)letter); if (!mind)
if (letter == 'W' || letter == 'A' || letter == 'S' || letter == 'D')
inputLetters.insert(letter);
}
void Character::ReleaseInput(char letter) {
letter = (char)std::toupper((unsigned char)letter);
inputLetters.erase(letter);
}
void Character::ClearInput() {
inputLetters.clear();
}
glm::vec3 Character::GetMovementDirection(const glm::vec3& forward, const glm::vec3& right) const {
const glm::vec3 flatForward = glm::normalize(glm::vec3(forward.x, 0.0f, forward.z));
const glm::vec3 flatRight = glm::normalize(glm::vec3(right.x, 0.0f, right.z));
glm::vec3 direction(0.0f);
if (inputLetters.count('W')) direction += flatForward;
if (inputLetters.count('S')) direction -= flatForward;
if (inputLetters.count('A')) direction += flatRight;
if (inputLetters.count('D')) direction -= flatRight;
if (glm::length(direction) > 0.0f)
direction = glm::normalize(direction);
return direction;
}
void Character::SetTurnDirection(const glm::vec3& direction) {
const glm::vec3 flatDirection(direction.x, 0.0f, direction.z);
if (glm::length(flatDirection) > 0.0001f)
turnDirection = glm::normalize(flatDirection);
}
void Character::TurnToDirection(float dt) {
if (glm::length(turnDirection) <= 0.0001f)
return; return;
pendingActions.clear();
const glm::quat target = LookAt(-turnDirection); mind->ProduceActions(*this, pendingActions);
const glm::quat current = body->t.GetRotationQuaternion(); for (const CharacterAction& action : pendingActions)
const float blend = 1.0f - std::exp(-turnSpeed * dt); body->Perform(action);
body->t.RotateToQuaternion(glm::normalize(glm::slerp(current, target, blend)));
} }
void Character::SetWalking(bool walking) { int Character::GetCollisionGroup() {
if (body && body->animator) // Kept clear of the ragdoll's groups, which count up from 1
body->animator->playbackSpeed = walking ? 1.0f : 0.0f; static int nextGroup = 1000;
if (collisionGroup == 0)
collisionGroup = nextGroup++;
return collisionGroup;
} }
glm::vec3 Character::GetForwardDirection() const { void Character::CreatePhysicsBody(Physics& physics, float massKg) {
if (!body) if (body->physics)
return glm::vec3(0.0f, 0.0f, -1.0f); return;
p* physicsBody = new p(body);
glm::vec3 forward = -body->t.GetFrontVector(); physicsBody->mass = massKg;
forward.y = 0.0f; physicsBody->lockRotation = true; // it turns by facing where it walks, never by tipping over
const float length = glm::length(forward); physicsBody->friction = 0.0f; // walking does its own stopping
if (length <= 0.0001f) physicsBody->collisionGroup = GetCollisionGroup();
return glm::vec3(0.0f, 0.0f, -1.0f); physics.AddObject(physicsBody);
return forward / length; body->physics = physicsBody;
} }
void Character::EnableRagdoll(Physics& physics) { void Character::EnableRagdoll(Physics& physics) {
+81 -80
View File
@@ -2,49 +2,37 @@
#define CHARACTER_CLASS #define CHARACTER_CLASS
#include <vector> #include <vector>
#include <string> #include <string>
#include <functional> #include <map>
#include <unordered_map>
#include <algorithm> #include <algorithm>
#include <set>
#include "t.h" #include "t.h"
#include "Rig.h" #include "Rig.h"
#include "Object.h" #include "Object.h"
#include "Animator.h" #include "Animator.h"
#include "CharacterAction.h"
class Character; class Character;
class CharacterManager; class CharacterManager;
class CharacterBody; class CharacterMind;
class BodyComponent;
class Ragdoll; class Ragdoll;
class Physics; class Physics;
class p;
class Action { // The physical side of a character: where it is, its physics body, its animation,
public: // and carrying out whatever actions its mind sends it.
std::string Name;
std::function<void(CharacterBody*, BodyComponent*)> FunctionPointer;
};
class InputAction {
public:
std::string BodyComponentName;
std::string ActionName;
};
class BodyComponent {
public:
std::string name;
BodyComponent* ParentComponent = nullptr;
std::vector<Action*> Actions;
};
class CharacterBody : public t_package { class CharacterBody : public t_package {
public: public:
Rig* rig = nullptr; Rig* rig = nullptr;
p* physics = nullptr; // made by Character::CreatePhysicsBody, its box is t's scale
std::vector<Animator*> animators; std::vector<Animator*> animators;
Animator* animator = nullptr; // back-compat: base animator at weight 0 Animator* animator = nullptr; // the base layer, always fully on: standing idle
Animator* walkAnimator = nullptr; // on top of the idle, faded in while walking
Animator* actionAnimator = nullptr; // one-shots on top of both, see PlayAction
std::vector<BodyComponent*> components; float walkSpeed = 2.34f; // m/s on flat ground
BodyComponent* RootComponent = nullptr; float turnSpeed = 12.0f; // how quickly it turns to face where it's walking
float walkFadeSpeed = 8.0f; // how quickly it fades between idle and walking
CharacterBody() { CharacterBody() {
animator = AddAnimator(0); animator = AddAnimator(0);
@@ -61,31 +49,11 @@ public:
return a; return a;
} }
// Returns weighted blend of all playing animators. // The skinning matrices for this frame, from every animator layered together. Worked out in Step.
// Base (weight 0) is always the foundation; higher-weight animators // Base (weight 0) is the foundation; each higher-weight animator blends in by its blendWeight,
// blend in on top based on their blendWeight (0?1). // but only on the bones its animation has tracks for, so an arm-only clip leaves the legs walking.
std::vector<glm::mat4> GetBlendedBoneMatrices() const { const std::vector<glm::mat4>& GetBlendedBoneMatrices() const {
// Start from the base animator return blendedBones.empty() ? animator->GetFinalBoneMatrices() : blendedBones;
std::vector<glm::mat4> result = animator->GetFinalBoneMatrices();
const int count = (int)result.size();
// Blend each higher-priority playing animator on top
for (int i = 1; i < (int)animators.size(); i++) {
Animator* a = animators[i];
if (!a->isPlaying || !a->m_CurrentAnimation || a->blendWeight <= 0.0f) continue;
const auto& matrices = a->GetFinalBoneMatrices();
for (int j = 0; j < count && j < (int)matrices.size(); j++)
result[j] = glm::mix(result[j], matrices[j], a->blendWeight);
}
return result;
}
// Returns the highest-weight playing animator (for back-compat)
Animator* GetActiveAnimator() const {
for (int i = (int)animators.size() - 1; i >= 0; i--)
if (animators[i]->isPlaying && animators[i]->m_CurrentAnimation)
return animators[i];
return animator;
} }
void SetRig(Rig* r) { void SetRig(Rig* r) {
@@ -97,64 +65,97 @@ public:
void SetAnimation(Animation* anim) { void SetAnimation(Animation* anim) {
animator->SetAnimation(anim); animator->SetAnimation(anim);
animator->PlayAnimation(); animator->PlayAnimation();
animator->blendWeight = 1.0f; // base never blends � always instant animator->blendWeight = 1.0f; // base never blends, always instant
} }
void LoadAnimation(Animation* anim) { SetAnimation(anim); } // Plays an animation once on top of everything else. Starts over if it's already playing.
void PlayAction(Animation* anim) {
void Step(float dt) { if (!anim)
for (Animator* a : animators) return;
a->Step(dt); if (!actionAnimator) {
actionAnimator = AddAnimator(2);
actionAnimator->blendDuration = 0.1f;
}
actionAnimator->loops = false;
actionAnimator->SetAnimation(anim);
actionAnimator->PlayAnimation();
} }
BodyComponent* GetComponentOfName(std::string name); // The loop played while walking. It keeps running underneath so it picks up mid-stride.
void ExecuteAction(InputAction* action); void SetWalkAnimation(Animation* anim) {
}; if (!walkAnimator)
walkAnimator = AddAnimator(1);
walkAnimator->loops = true;
walkAnimator->SetAnimation(anim);
walkAnimator->PlayAnimation();
walkAnimator->blendWeight = walkBlend;
}
class CharacterMind { // Carries out one action. Movement actions only count for the frame they arrive in.
public: void Perform(const CharacterAction& action);
InputAction* ProduceAction();
// Pushes the physics body toward walking speed along wherever it faces, from this frame's Move action.
// speedFactor scales it, 0 stops it.
void ApplyMovement(bool grounded, float speedFactor, float dt, float referenceDt);
// Turning and animation
void Step(float dt);
void SetWalking(bool walking);
glm::vec3 GetForwardDirection() const;
private:
glm::vec3 wishDirection = glm::vec3(0.0f); // from this frame's Move action
glm::vec3 turnDirection = glm::vec3(0.0f); // where it's turning to face, kept after it stops
bool walking = false;
float walkBlend = 0.0f; // 0 = idle, 1 = walking
std::vector<glm::mat4> blendedBones;
// Per (base clip, layer clip): bone name -> what turns the layer's track into one relative to the bone's
// real parent. Only bones whose parent differs between the two files have an entry.
std::map<std::pair<const Animation*, const Animation*>, std::unordered_map<std::string, glm::mat4>> parentCorrections;
void BlendBones(const AssimpNodeData& node, const glm::mat4& parentTransform);
const std::unordered_map<std::string, glm::mat4>& GetParentCorrections(const Animation* layer);
}; };
class Character : public Object, public Renderable { class Character : public Object, public Renderable {
public: public:
CharacterMind* mind = nullptr; CharacterMind* mind = nullptr; // what decides its actions, nullptr does nothing
CharacterBody* body = nullptr; CharacterBody* body = nullptr;
Ragdoll* ragdoll = nullptr; // when set, physics drives the rig instead of the animators Ragdoll* ragdoll = nullptr; // when set, physics drives the rig instead of the animators
SurfaceTexture surface; 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; its physics box 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;
void PressInput(char letter); // Asks the mind for this frame's actions and hands them to the body
void ReleaseInput(char letter); void Think();
void ClearInput();
glm::vec3 GetMovementDirection(const glm::vec3& forward, const glm::vec3& right) const; glm::vec3 GetForwardDirection() const { return body->GetForwardDirection(); }
void SetTurnDirection(const glm::vec3& direction);
const glm::vec3& GetTurnDirection() const { return turnDirection; }
void SetWalking(bool walking);
glm::vec3 GetForwardDirection() const;
void EnableRagdoll(Physics& physics); void EnableRagdoll(Physics& physics);
// The box physics moves the character around with, sized by body->t's scale
void CreatePhysicsBody(Physics& physics, float massKg);
// Its physics body's group, so it never collides with anything else of its own
int GetCollisionGroup();
Character(); Character();
void Render(Shader& shader) override; void Render(Shader& shader) override;
bool GetWorldBounds(BoundingBox& bounds) const override; bool GetWorldBounds(BoundingBox& bounds) const override;
void Step(float dt) { void Step(float dt) {
if (ragdoll) return; if (ragdoll) return;
TurnToDirection(dt);
body->Step(dt); body->Step(dt);
} }
private: private:
void TurnToDirection(float dt);
glm::mat4 GetRigMatrix() const; glm::mat4 GetRigMatrix() const;
std::set<char> inputLetters; int collisionGroup = 0;
glm::vec3 turnDirection = glm::vec3(0.0f); std::vector<CharacterAction> pendingActions;
}; };
#endif
#endif
+23
View File
@@ -0,0 +1,23 @@
#ifndef CHARACTER_ACTION_CLASS
#define CHARACTER_ACTION_CLASS
#include <glm/glm.hpp>
enum class CharacterActionType {
Move // walk along direction, or stand still if it's zero
};
// Something a character's mind wants it to do this frame. Minds produce these, the body carries them out.
struct CharacterAction {
CharacterActionType type = CharacterActionType::Move;
glm::vec3 direction = glm::vec3(0.0f); // world space, only x and z are used
static CharacterAction Move(const glm::vec3& direction) {
CharacterAction action;
action.type = CharacterActionType::Move;
action.direction = direction;
return action;
}
};
#endif
-15
View File
@@ -1,15 +0,0 @@
#ifndef CHARACTERACTIONS_CLASS
#define CHARACTERACTIONS_CLASS
enum CharacterAction {
MOVE_FORWARD,
MOVE_BACKWARD,
MOVE_LEFT,
MOVE_RIGHT,
JUMP,
CROUCH,
SPRINT
};
#endif
+3 -17
View File
@@ -64,23 +64,9 @@ Character* CharacterManager::GetCharacter(const std::string& name) const {
return it != characters.end() ? it->second.get() : nullptr; return it != characters.end() ? it->second.get() : nullptr;
} }
void CharacterManager::SetControlledCharacter(const std::string& name) { void CharacterManager::Think() {
controlledCharacter = GetCharacter(name); for (Character* character : characterOrder)
} character->Think();
void CharacterManager::PressInput(char letter) {
if (controlledCharacter)
controlledCharacter->PressInput(letter);
}
void CharacterManager::ReleaseInput(char letter) {
if (controlledCharacter)
controlledCharacter->ReleaseInput(letter);
}
void CharacterManager::ClearInput() {
if (controlledCharacter)
controlledCharacter->ClearInput();
} }
void CharacterManager::Step(float dt) { void CharacterManager::Step(float dt) {
+2 -6
View File
@@ -26,11 +26,8 @@ public:
Animation* GetAnimation(const std::string& name) const; Animation* GetAnimation(const std::string& name) const;
Character* GetCharacter(const std::string& name) const; Character* GetCharacter(const std::string& name) const;
void SetControlledCharacter(const std::string& name); // Every character's mind decides its actions for this frame
void PressInput(char letter); void Think();
void ReleaseInput(char letter);
void ClearInput();
void Step(float dt); void Step(float dt);
const std::vector<Character*>& GetCharacters() const { return characterOrder; } const std::vector<Character*>& GetCharacters() const { return characterOrder; }
@@ -40,7 +37,6 @@ private:
std::unordered_map<std::string, std::unique_ptr<Animation>> animations; std::unordered_map<std::string, std::unique_ptr<Animation>> animations;
std::unordered_map<std::string, std::unique_ptr<Character>> characters; std::unordered_map<std::string, std::unique_ptr<Character>> characters;
std::vector<Character*> characterOrder; std::vector<Character*> characterOrder;
Character* controlledCharacter = nullptr;
}; };
#endif #endif
+29
View File
@@ -0,0 +1,29 @@
#include "CharacterMind.h"
#include "Camera.h"
#include "common.h"
#include "Keyboard.h"
void KeyboardMind::ProduceActions(const Character& character, std::vector<CharacterAction>& actions)
{
glm::vec3 direction(0.0f);
if (enabled) {
// Flattened, so looking up or down doesn't slow walking
const glm::vec3 forward = camera.t.GetFrontVector();
const glm::vec3 right = camera.t.GetRightVector();
const glm::vec3 flatForward = glm::normalize(glm::vec3(forward.x, 0.0f, forward.z));
const glm::vec3 flatRight = glm::normalize(glm::vec3(right.x, 0.0f, right.z));
// The camera's right vector points to the left of the screen, so A adds it
if (keyboard->IsKeyDown(GLFW_KEY_W)) direction += flatForward;
if (keyboard->IsKeyDown(GLFW_KEY_S)) direction -= flatForward;
if (keyboard->IsKeyDown(GLFW_KEY_A)) direction += flatRight;
if (keyboard->IsKeyDown(GLFW_KEY_D)) direction -= flatRight;
if (glm::length(direction) > 0.0f)
direction = glm::normalize(direction);
}
// Always sent, so letting go of the keys stops the character
actions.push_back(CharacterAction::Move(direction));
}
+35
View File
@@ -0,0 +1,35 @@
#ifndef CHARACTER_MIND_CLASS
#define CHARACTER_MIND_CLASS
#include <vector>
#include "CharacterAction.h"
class Character;
class Camera;
// Decides what a character does, as actions. A character without a mind does nothing.
// AI goes here later, as another kind of mind.
class CharacterMind {
public:
virtual ~CharacterMind() = default;
// Called once a frame, adds whatever the character should do this frame
virtual void ProduceActions(const Character& character, std::vector<CharacterAction>& actions) = 0;
};
// The player: WASD, relative to where the camera faces
class KeyboardMind : public CharacterMind {
public:
explicit KeyboardMind(const Camera& camera) : camera(camera) {}
// Off while the keys are busy with something else, like typing in the console
bool enabled = true;
void ProduceActions(const Character& character, std::vector<CharacterAction>& actions) override;
private:
const Camera& camera;
};
#endif
+212
View File
@@ -0,0 +1,212 @@
#include "Console.h"
#include <algorithm>
#include <cctype>
#include <cmath>
#include <sstream>
#include "Gui.h"
static constexpr float PANEL_HEIGHT = 360.0f;
static constexpr float INPUT_HEIGHT = 36.0f;
static constexpr float MARGIN = 16.0f;
static constexpr int VISIBLE_LINES = 13;
static constexpr int MAX_LOG_LINES = 300;
static constexpr int MAX_INPUT_LENGTH = 200;
static constexpr int SCROLL_STEP = 5;
static std::string Lowercase(std::string text)
{
std::transform(text.begin(), text.end(), text.begin(), [](unsigned char c) { return (char)std::tolower(c); });
return text;
}
Console::Console(Font* newFont) : font(newFont)
{
Register("help", "help", "lists every command", [this](const Arguments&) {
for (const auto& [name, entry] : commands)
Print(" " + entry.usage + " - " + entry.description);
});
Register("clear", "clear", "empties the console", [this](const Arguments&) {
log.clear();
scroll = 0;
});
}
// GUI draws in the order things were made, so this waits until the first Update to land on top of everything
void Console::CreateDisplay()
{
background = new Box();
background->Color = { 0.02f, 0.03f, 0.05f };
background->Opacity = 0.0f;
background->t2d.position = { 0.0f, 0.0f, 0.0f, 0.0f };
background->t2d.size = { 1.0f, 0.0f, 0.0f, PANEL_HEIGHT };
background->BindToRenderSystem();
inputBackground = new Box();
inputBackground->Color = { 0.10f, 0.12f, 0.16f };
inputBackground->Opacity = 0.0f;
inputBackground->t2d.position = { 0.0f, 0.0f, 0.0f, PANEL_HEIGHT - INPUT_HEIGHT };
inputBackground->t2d.size = { 1.0f, 0.0f, 0.0f, INPUT_HEIGHT };
inputBackground->BindToRenderSystem();
// Bottom aligned, so the newest line sits just above the input and old ones get cut off at the top
logText = new TextBox();
logText->font = font;
logText->fontsize = 0.42f;
logText->Color = { 0.85f, 0.88f, 0.93f };
logText->Opacity = 0.0f;
logText->t2d.position = { 0.0f, 0.0f, MARGIN, 8.0f };
logText->t2d.size = { 1.0f, 0.0f, -2.0f * MARGIN, PANEL_HEIGHT - INPUT_HEIGHT - 12.0f };
logText->textCenter = { 0.0f, 1.0f };
logText->BindToRenderSystem();
inputText = new TextBox();
inputText->font = font;
inputText->fontsize = 0.45f;
inputText->Color = { 0.96f, 0.58f, 0.16f };
inputText->Opacity = 0.0f;
inputText->t2d.position = { 0.0f, 0.0f, MARGIN, PANEL_HEIGHT - INPUT_HEIGHT };
inputText->t2d.size = { 1.0f, 0.0f, -2.0f * MARGIN, INPUT_HEIGHT };
inputText->textCenter = { 0.0f, 0.5f };
inputText->BindToRenderSystem();
}
void Console::Register(const std::string& name, const std::string& usage, const std::string& description, Command run)
{
commands[Lowercase(name)] = { usage, description, std::move(run) };
}
void Console::Execute(const std::string& line)
{
std::istringstream words(line);
std::string name;
if (!(words >> name))
return;
Arguments arguments;
for (std::string word; words >> word; )
arguments.push_back(word);
auto command = commands.find(Lowercase(name));
if (command == commands.end()) {
Print("Unknown command '" + name + "', type help for a list");
return;
}
command->second.run(arguments);
}
void Console::Print(const std::string& message)
{
std::istringstream lines(message);
for (std::string line; std::getline(lines, line); ) {
log.push_back(line);
if ((int)log.size() > MAX_LOG_LINES)
log.pop_front();
}
}
void Console::Open()
{
open = true;
historyIndex = (int)history.size();
caretTimer = 0.0f;
}
void Console::Close()
{
open = false;
}
void Console::OnChar(unsigned int codepoint)
{
// The font only has printable ASCII, and the toggle key shouldn't end up in the line
if (codepoint < 32 || codepoint > 126 || codepoint == '`' || codepoint == '~')
return;
if ((int)input.size() < MAX_INPUT_LENGTH)
input += (char)codepoint;
caretTimer = 0.0f;
}
void Console::OnKey(int key)
{
caretTimer = 0.0f;
switch (key) {
case GLFW_KEY_ENTER:
case GLFW_KEY_KP_ENTER:
if (input.empty())
break;
Print("> " + input);
if (history.empty() || history.back() != input)
history.push_back(input);
historyIndex = (int)history.size();
scroll = 0;
{
// Copied first, a command is allowed to touch the console's own state
const std::string line = input;
input.clear();
Execute(line);
}
break;
case GLFW_KEY_BACKSPACE:
if (!input.empty())
input.pop_back();
break;
case GLFW_KEY_UP:
if (historyIndex > 0) {
historyIndex--;
input = history[historyIndex];
}
break;
case GLFW_KEY_DOWN:
if (historyIndex < (int)history.size()) {
historyIndex++;
input = historyIndex < (int)history.size() ? history[historyIndex] : std::string();
}
break;
case GLFW_KEY_PAGE_UP:
Scroll(SCROLL_STEP);
break;
case GLFW_KEY_PAGE_DOWN:
Scroll(-SCROLL_STEP);
break;
case GLFW_KEY_ESCAPE:
Close();
break;
default:
break;
}
}
void Console::Scroll(int lines)
{
scroll = glm::clamp(scroll + lines, 0, std::max((int)log.size() - VISIBLE_LINES, 0));
}
void Console::Update(float dt)
{
if (!background)
CreateDisplay();
const float opacity = open ? 1.0f : 0.0f;
background->Opacity = open ? 0.88f : 0.0f;
inputBackground->Opacity = open ? 0.9f : 0.0f;
logText->Opacity = opacity;
inputText->Opacity = opacity;
if (!open)
return;
const int newest = (int)log.size() - scroll;
const int oldest = std::max(newest - VISIBLE_LINES, 0);
std::string visible;
for (int i = oldest; i < newest; i++) {
visible += log[i];
if (i + 1 < newest)
visible += '\n';
}
logText->text = visible;
caretTimer += dt;
const bool caretOn = std::fmod(caretTimer, 1.0f) < 0.5f;
inputText->text = "> " + input + (caretOn ? "_" : "");
}
+67
View File
@@ -0,0 +1,67 @@
#ifndef CONSOLE_CLASS
#define CONSOLE_CLASS
#include <deque>
#include <functional>
#include <map>
#include <string>
#include <vector>
class Font;
class Box;
class TextBox;
// The in-game console. Commands get registered by name and run with whatever words follow them.
class Console {
public:
using Arguments = std::vector<std::string>; // the words after the command name
using Command = std::function<void(const Arguments&)>;
explicit Console(Font* newFont);
// usage is shown by help, like "give <item> [count]". Names aren't case sensitive.
void Register(const std::string& name, const std::string& usage, const std::string& description, Command run);
void Execute(const std::string& line);
void Print(const std::string& message);
void Open();
void Close();
void Toggle() { open ? Close() : Open(); }
bool IsOpen() const { return open; }
// Feed these from the GLFW callbacks while it's open
void OnChar(unsigned int codepoint);
void OnKey(int key);
// Positive goes back toward older lines
void Scroll(int lines);
// Once a frame, refreshes what's on screen
void Update(float dt);
private:
struct Entry {
std::string usage;
std::string description;
Command run;
};
void CreateDisplay();
Font* font = nullptr;
std::map<std::string, Entry> commands; // sorted, so help lists them alphabetically
std::deque<std::string> log;
std::vector<std::string> history;
int historyIndex = 0; // history.size() means the line being typed
std::string input;
int scroll = 0; // how many lines up from the newest
float caretTimer = 0.0f;
bool open = false;
Box* background = nullptr;
Box* inputBackground = nullptr;
TextBox* logText = nullptr;
TextBox* inputText = nullptr;
};
#endif
-36
View File
@@ -1,36 +0,0 @@
#ifndef DEFAULT_BODY_COMPONENTS
#define DEFAULT_BODY_COMPONENTS
#include <vector>
#include "Character.h"
#include "BodyComponentActions.h"
std::vector<BodyComponent*> GenerateDefaultBody() {
std::vector<BodyComponent*> TR;
BodyComponent* Head = new BodyComponent();
BodyComponent* Torso = new BodyComponent();
BodyComponent* RightArm = new BodyComponent();
BodyComponent* LeftArm = new BodyComponent();
BodyComponent* RightLeg = new BodyComponent();
BodyComponent* LeftLeg = new BodyComponent();
Head->name = "Head";
Torso->name = "Torso";
RightArm->name = "RightArm";
LeftArm->name = "LeftArm";
RightLeg->name = "RightLeg";
LeftLeg->name = "LeftLeg";
TR.push_back(Head);
TR.push_back(Torso);
TR.push_back(RightArm);
TR.push_back(LeftArm);
TR.push_back(RightLeg);
TR.push_back(LeftLeg);
return TR;
}
#endif
+282 -305
View File
@@ -10,6 +10,7 @@
#include <memory> #include <memory>
#include <type_traits> #include <type_traits>
#include <algorithm> #include <algorithm>
#include <cfloat>
#include <cmath> #include <cmath>
// ============================================================================ // ============================================================================
@@ -68,7 +69,6 @@
#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"
@@ -76,8 +76,11 @@
#include "Character.h" #include "Character.h"
#include "CharacterManager.h" #include "CharacterManager.h"
#include "Ragdoll.h" #include "Ragdoll.h"
#include "DefaultBodyComponents.h" #include "Console.h"
#include "BodyComponentActions.h"
#include <iomanip>
#include <sstream>
#include "CharacterMind.h"
#define STB_IMAGE_IMPLEMENTATION #define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h" #include "stb_image.h"
@@ -93,6 +96,7 @@ Engine::Engine()
glfwSetCursorPosCallback(window->handle, mouse_callback); glfwSetCursorPosCallback(window->handle, mouse_callback);
glfwSetScrollCallback(window->handle, scroll_callback); glfwSetScrollCallback(window->handle, scroll_callback);
glfwSetKeyCallback(window->handle, key_callback); glfwSetKeyCallback(window->handle, key_callback);
glfwSetCharCallback(window->handle, char_callback);
glfwSetMouseButtonCallback(window->handle, mouse_button_callback); glfwSetMouseButtonCallback(window->handle, mouse_button_callback);
glfwSwapInterval(1); glfwSwapInterval(1);
@@ -138,12 +142,14 @@ void Engine::Initiate()
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("idle", "assets/idle.glb", "humanoid");
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// Player character // Player character
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
Character& testcharacter = *charactermanager->CreateCharacter("character", "humanoid", "walk"); // Idle is the base pose, walking fades in on top of it while moving
charactermanager->SetControlledCharacter("character"); Character& testcharacter = *charactermanager->CreateCharacter("character", "humanoid", "idle");
testcharacter.body->SetWalkAnimation(charactermanager->GetAnimation("walk"));
testcharacter.castshadow = true; testcharacter.castshadow = true;
testcharacter.colorOverride = glm::vec4(0.62f, 0.64f, 0.68f, 1.0f); testcharacter.colorOverride = glm::vec4(0.62f, 0.64f, 0.68f, 1.0f);
testcharacter.roughness = 0.65f; testcharacter.roughness = 0.65f;
@@ -151,17 +157,13 @@ void Engine::Initiate()
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.BindToRenderSystem(); testcharacter.BindToRenderSystem();
testcharacter.CreatePhysicsBody(*physicsengine, 80.0f);
p* characterBody = new p(testcharacter.body);
characterBody->mass = 80.0f;
characterBody->lockRotation = true;
characterBody->friction = 0.0f;
physicsengine->AddObject(characterBody);
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// NPC // NPC
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
Character& npc = *charactermanager->CreateCharacter("npc", "humanoid", "walk"); Character& npc = *charactermanager->CreateCharacter("npc", "humanoid", "idle");
npc.body->SetWalkAnimation(charactermanager->GetAnimation("walk"));
npc.castshadow = true; npc.castshadow = true;
npc.colorOverride = glm::vec4(0.10f, 0.20f, 0.48f, 1.0f); npc.colorOverride = glm::vec4(0.10f, 0.20f, 0.48f, 1.0f);
npc.metallic = 0.1f; npc.metallic = 0.1f;
@@ -169,10 +171,96 @@ void Engine::Initiate()
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.BindToRenderSystem(); npc.BindToRenderSystem();
npc.CreatePhysicsBody(*physicsengine, 80.0f);
testcharacter.SetWalking(false); testcharacter.body->SetWalking(false);
npc.SetWalking(false); npc.body->SetWalking(false);
npc.EnableRagdoll(*physicsengine);
// The keyboard is the player's mind: it turns WASD into actions for whichever character it's in.
// The camera follows that character too, C moves both to the other one.
KeyboardMind keyboardMind(*camera);
Character* controlled = &testcharacter;
controlled->mind = &keyboardMind;
// ------------------------------------------------------------------------
// Console commands. help and clear are built in.
// ------------------------------------------------------------------------
console = new Console(arial);
// From the camera, out through a point on the screen in pixels
auto MouseRay = [&](const glm::vec2& mousePos) {
const glm::mat4 inverseViewProjection = glm::inverse(camera->GetProjectionMatrix(0.05f, 2000.0f) * camera->GetViewMatrix());
const glm::vec2 ndc(mousePos.x / window->width * 2.0f - 1.0f, 1.0f - mousePos.y / window->height * 2.0f);
glm::vec4 nearPoint = inverseViewProjection * glm::vec4(ndc, -1.0f, 1.0f);
glm::vec4 farPoint = inverseViewProjection * glm::vec4(ndc, 1.0f, 1.0f);
nearPoint /= nearPoint.w;
farPoint /= farPoint.w;
Ray ray;
ray.origin = glm::vec3(nearPoint);
ray.direction = glm::normalize(glm::vec3(farPoint - nearPoint));
return ray;
};
// Debug cubes G throws, each a mesh and the physics body moving it. Cleared once they fall off the world.
struct DebugCube {
Mesh* mesh = nullptr;
p* body = nullptr;
};
std::vector<DebugCube> debugCubes;
auto SpawnDebugCube = [&](const glm::vec3& position, const glm::vec3& velocity) {
DebugCube cube;
cube.mesh = CreateCubeMesh();
cube.mesh->name = "debug_cube";
cube.mesh->colorOverride = glm::vec4(1.0f, 0.4f, 0.1f, 1.0f);
cube.mesh->roughness = 0.7f;
cube.mesh->castshadow = true;
cube.mesh->t.ScaleTo(0.3f);
cube.mesh->t.RotateToQuaternion(glm::quat(1.0f, 0.0f, 0.0f, 0.0f));
cube.mesh->t.TranslateTo(position);
cube.mesh->BindToRenderSystem();
cube.body = new p(cube.mesh);
cube.body->mass = 2.0f;
cube.body->restitution = 0.1f;
cube.body->friction = 0.7f;
cube.body->maxSpeed = 40.0f;
cube.body->linearvelocity = velocity;
physicsengine->AddObject(cube.body);
debugCubes.push_back(cube);
};
auto ParseFloat = [](const std::string& text, float& value) {
try { size_t used = 0; value = std::stof(text, &used); return used == text.size(); }
catch (...) { return false; }
};
console->Register("tp", "tp <x> <y> <z>", "teleports you", [&](const Console::Arguments& args) {
glm::vec3 position;
if (args.size() < 3 || !ParseFloat(args[0], position.x) || !ParseFloat(args[1], position.y) || !ParseFloat(args[2], position.z)) {
console->Print("Usage: tp <x> <y> <z>");
return;
}
controlled->body->t.TranslateTo(position);
controlled->body->physics->linearvelocity = glm::vec3(0.0f);
controlled->body->physics->WakeUp();
console->Print("Teleported");
});
console->Register("gravity", "gravity [strength]", "shows or sets how hard things fall, in m/s^2", [&](const Console::Arguments& args) {
float strength = 0.0f;
if (!args.empty()) {
if (!ParseFloat(args[0], strength)) { console->Print("Usage: gravity [strength]"); return; }
physicsengine->SetGravity(glm::vec3(0.0f, -strength, 0.0f));
}
std::ostringstream text;
text << "Gravity is " << std::fixed << std::setprecision(2) << -physicsengine->GetGravity().y << " m/s^2";
console->Print(text.str());
});
console->Register("quit", "quit", "closes the game", [&](const Console::Arguments&) {
glfwSetWindowShouldClose(window->handle, 1);
});
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// 3D audio // 3D audio
@@ -211,7 +299,7 @@ void Engine::Initiate()
float hover = 0.0f; // eased 0-1 float hover = 0.0f; // eased 0-1
}; };
enum class MenuScreen { Main, Paused, Settings, Graphics, Drawer }; enum class MenuScreen { Main, Paused, Settings, Graphics };
MenuScreen menuScreen = MenuScreen::Main; MenuScreen menuScreen = MenuScreen::Main;
MenuScreen settingsReturn = MenuScreen::Main; MenuScreen settingsReturn = MenuScreen::Main;
bool menuActive = true; bool menuActive = true;
@@ -277,7 +365,7 @@ void Engine::Initiate()
box->t2d.position = { 0.0f, 0.5f, x, y }; box->t2d.position = { 0.0f, 0.5f, x, y };
box->t2d.center = { 0.0f, 0.5f }; box->t2d.center = { 0.0f, 0.5f };
box->t2d.size = { 0.0f, 0.0f, width, height }; box->t2d.size = { 0.0f, 0.0f, width, height };
box->rounding = 0.15f; box->rounding = 0.05f;
box->BindToRenderSystem(); box->BindToRenderSystem();
menuItems.push_back(box); menuItems.push_back(box);
@@ -301,11 +389,9 @@ void Engine::Initiate()
const float mainButtonHeight = 60.0f; const float mainButtonHeight = 60.0f;
MenuButton playButton = MakeMenuButton("PLAY", menuMargin, -60.0f, menuContentWidth, mainButtonHeight, 0.75f, MenuButton playButton = MakeMenuButton("PLAY", menuMargin, -60.0f, menuContentWidth, mainButtonHeight, 0.75f,
glm::vec3(0.82f, 0.45f, 0.10f), menuAccent, 0.95f); glm::vec3(0.82f, 0.45f, 0.10f), menuAccent, 0.95f);
MenuButton drawerButton = MakeMenuButton("TEXTURE DRAWER", menuMargin, 12.0f, menuContentWidth, mainButtonHeight, 0.75f, MenuButton settingsButton = MakeMenuButton("SETTINGS", menuMargin, 12.0f, menuContentWidth, mainButtonHeight, 0.75f,
menuButtonColor, menuButtonHover); menuButtonColor, menuButtonHover);
MenuButton settingsButton = MakeMenuButton("SETTINGS", menuMargin, 84.0f, menuContentWidth, mainButtonHeight, 0.75f, MenuButton quitButton = MakeMenuButton("QUIT", 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)); menuButtonColor, glm::vec3(0.42f, 0.14f, 0.16f));
// Settings rows: LABEL [-] VALUE [+] // Settings rows: LABEL [-] VALUE [+]
@@ -349,117 +435,12 @@ void Engine::Initiate()
MenuButton backButton = MakeMenuButton("BACK", menuMargin, 120.0f, menuContentWidth, 50.0f, 0.6f, MenuButton backButton = MakeMenuButton("BACK", menuMargin, 120.0f, menuContentWidth, 50.0f, 0.6f,
glm::vec3(0.82f, 0.45f, 0.10f), menuAccent, 0.95f); 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 = { std::vector<MenuButton*> menuButtons = {
&playButton, &drawerButton, &settingsButton, &quitButton, &playButton, &settingsButton, &quitButton,
&sensitivityDown, &sensitivityUp, &fovDown, &fovUp, &sensitivityDown, &sensitivityUp, &fovDown, &fovUp,
&hitboxToggle, &graphicsButton, &resetButton, &backButton, &hitboxToggle, &graphicsButton, &resetButton, &backButton,
&bloomToggle, &shadowToggle, &lightShadowToggle, &vsyncToggle, &cullingToggle, &bloomToggle, &shadowToggle, &lightShadowToggle, &vsyncToggle, &cullingToggle
&surfacePrevious, &surfaceNext, &brushDown, &brushUp, &clearButton
}; };
for (MenuButton& swatch : swatches)
menuButtons.push_back(&swatch);
TextBox* menuHint = new TextBox();
menuHint->font = arial;
menuHint->fontsize = 0.42f;
menuHint->Color = menuMutedText;
menuHint->t2d.position = { 0.0f, 1.0f, menuMargin, -28.0f };
menuHint->t2d.center = { 0.0f, 1.0f };
menuHint->t2d.size = { 0.0f, 0.0f, menuContentWidth, 110.0f };
menuHint->textCenter = { 0.0f, 1.0f };
menuHint->BindToRenderSystem();
menuItems.push_back(menuHint);
auto SetButtonVisible = [](MenuButton& button, bool visible) { auto SetButtonVisible = [](MenuButton& button, bool visible) {
button.box->Opacity = visible ? button.opacity : 0.0f; button.box->Opacity = visible ? button.opacity : 0.0f;
@@ -472,7 +453,6 @@ void Engine::Initiate()
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 graphics = menuScreen == MenuScreen::Graphics;
const bool drawer = menuScreen == MenuScreen::Drawer;
const bool mainButtons = main || paused; const bool mainButtons = main || paused;
menuVeil->Opacity = 0.22f; menuVeil->Opacity = 0.22f;
@@ -480,39 +460,13 @@ void Engine::Initiate()
menuPanelEdge->Opacity = 1.0f; menuPanelEdge->Opacity = 1.0f;
menuTitle->Opacity = 1.0f; menuTitle->Opacity = 1.0f;
menuSubtitle->Opacity = 1.0f; menuSubtitle->Opacity = 1.0f;
menuHint->Opacity = 1.0f; menuSubtitle->text = main ? "MAIN MENU" : (paused ? "PAUSED" : (graphics ? "GRAPHICS" : "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 = drawer
? "DRAG ON THE CANVAS TO DRAW\nESC TO GO BACK"
: (settings || graphics ? "ESC TO GO BACK" : "WASD TO MOVE\nESC FOR MENU");
SetButtonVisible(playButton, mainButtons); SetButtonVisible(playButton, mainButtons);
SetButtonVisible(drawerButton, mainButtons);
SetButtonVisible(settingsButton, mainButtons); SetButtonVisible(settingsButton, mainButtons);
SetButtonVisible(quitButton, 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(sensitivityDown, settings); SetButtonVisible(sensitivityDown, settings);
SetButtonVisible(sensitivityUp, settings); SetButtonVisible(sensitivityUp, settings);
@@ -521,7 +475,7 @@ void Engine::Initiate()
SetButtonVisible(hitboxToggle, settings); SetButtonVisible(hitboxToggle, settings);
SetButtonVisible(graphicsButton, settings); SetButtonVisible(graphicsButton, settings);
SetButtonVisible(resetButton, settings); SetButtonVisible(resetButton, settings);
SetButtonVisible(backButton, settings || drawer || graphics); SetButtonVisible(backButton, settings || graphics);
auto ShowToggle = [&](MenuButton& button, const char* name, bool enabled) { auto ShowToggle = [&](MenuButton& button, const char* name, bool enabled) {
SetButtonVisible(button, graphics); SetButtonVisible(button, graphics);
@@ -549,9 +503,8 @@ void Engine::Initiate()
auto OpenMenu = [&](MenuScreen screen) { auto OpenMenu = [&](MenuScreen screen) {
menuActive = true; menuActive = true;
menuScreen = screen; menuScreen = screen;
charactermanager->ClearInput(); testcharacter.body->SetWalking(false);
testcharacter.SetWalking(false); npc.body->SetWalking(false);
npc.SetWalking(false);
fpsCounter->Opacity = 0.0f; fpsCounter->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);
@@ -560,7 +513,6 @@ void Engine::Initiate()
auto ResumeGame = [&]() { auto ResumeGame = [&]() {
menuActive = false; menuActive = false;
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;
camera->lockedcursor = true; camera->lockedcursor = true;
@@ -575,21 +527,21 @@ void Engine::Initiate()
glfwSetInputMode(window->handle, GLFW_CURSOR, GLFW_CURSOR_NORMAL); glfwSetInputMode(window->handle, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// Baseplate // Baseplate, a flat slab with its top at y = 0
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
Mesh* floor = CreateCubeMesh(); Mesh* baseplate = CreateCubeMesh();
floor->name = "baseplate"; baseplate->name = "baseplate";
floor->surface = SurfaceTexture::Tiled(blanktex, 8.0f); baseplate->t.ScaleTo({ 200.0f, 2.0f, 200.0f });
floor->roughness = 0.9f; baseplate->t.TranslateTo({ 0.0f, -1.0f, 0.0f });
floor->t.ScaleTo({ 200.0f, 2.0f, 200.0f }); baseplate->t.RotateToQuaternion(glm::quat(1.0f, 0.0f, 0.0f, 0.0f));
floor->t.TranslateTo({ 0.0f, -1.0f, 0.0f }); baseplate->surface = SurfaceTexture::Tiled(blanktex, 8.0f);
floor->BindToRenderSystem(); baseplate->BindToRenderSystem();
mainf->AddChild(floor); mainf->AddChild(baseplate);
p* floorBody = new p(floor); p* baseplateBody = new p(baseplate);
floorBody->velocity = false; baseplateBody->velocity = false;
physicsengine->AddObject(floorBody); baseplateBody->friction = 0.7f;
const BoundingBox floorBounds = floor->t.GetRotationlessAABB(); physicsengine->AddObject(baseplateBody);
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// Light cubes // Light cubes
@@ -616,22 +568,19 @@ void Engine::Initiate()
Mesh* pointLightCube = AddLightCube("point_light_cube", pointLightPosition, 0.8f, pointLightColor); Mesh* pointLightCube = AddLightCube("point_light_cube", pointLightPosition, 0.8f, pointLightColor);
rendersystem->lighting->AddPointLight(pointLightPosition, pointLightColor, 300.0f, 30.0f); 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
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
auto IsGrounded = [&]() -> bool { auto IsGrounded = [&](Character& character) -> bool {
Ray downRay; Ray downRay;
downRay.origin = testcharacter.body->t.GetTranslation(); downRay.origin = character.body->t.GetTranslation();
downRay.direction = glm::vec3(0.0f, -1.0f, 0.0f); downRay.direction = glm::vec3(0.0f, -1.0f, 0.0f);
const float halfHeight = testcharacter.body->t.GetScale().y * 0.5f; const float halfHeight = character.body->t.GetScale().y * 0.5f;
for (p* obj : physicsengine->GetObjects()) { for (p* obj : physicsengine->GetObjects()) {
if (obj->pointer == testcharacter.body) continue; if (obj->pointer == character.body) continue;
if (obj->collisionGroup != 0 && obj->collisionGroup == character.GetCollisionGroup()) continue; // anything else of its own
if (!obj->collision) continue; if (!obj->collision) continue;
auto hit = IsRayInT(downRay, obj->pointer->t); auto hit = IsRayInT(downRay, obj->pointer->t);
if (hit.has_value()) { if (hit.has_value()) {
@@ -677,6 +626,16 @@ void Engine::Initiate()
prevFrameTime = frameNow; prevFrameTime = frameNow;
const float refDt = 1.0f / float(fps); const float refDt = 1.0f / float(fps);
static bool prevSwapDown = false;
const bool swapDown = keyboard->IsKeyDown(GLFW_KEY_C);
if (swapDown && !prevSwapDown && !menuActive) {
// The keyboard moves over, the one left behind has no mind and stands still
controlled->mind = nullptr;
controlled = controlled == &testcharacter ? &npc : &testcharacter;
controlled->mind = &keyboardMind;
}
prevSwapDown = swapDown;
static bool prevEscapeDown = false; static bool prevEscapeDown = false;
const bool escapeDown = keyboard->IsKeyDown(GLFW_KEY_ESCAPE); const bool escapeDown = keyboard->IsKeyDown(GLFW_KEY_ESCAPE);
const bool escapePressed = escapeDown && !prevEscapeDown; const bool escapePressed = escapeDown && !prevEscapeDown;
@@ -685,7 +644,7 @@ void Engine::Initiate()
if (escapePressed) { if (escapePressed) {
if (!menuActive) if (!menuActive)
OpenMenu(MenuScreen::Paused); OpenMenu(MenuScreen::Paused);
else if (menuScreen == MenuScreen::Settings || menuScreen == MenuScreen::Drawer) else if (menuScreen == MenuScreen::Settings)
menuScreen = settingsReturn; menuScreen = settingsReturn;
else if (menuScreen == MenuScreen::Graphics) else if (menuScreen == MenuScreen::Graphics)
menuScreen = MenuScreen::Settings; menuScreen = MenuScreen::Settings;
@@ -696,32 +655,75 @@ void Engine::Initiate()
// -------------------------------------------------------------------- // --------------------------------------------------------------------
// Mouse look // Mouse look
// -------------------------------------------------------------------- // --------------------------------------------------------------------
int wheelSteps = 0; // up is positive
while (Mouse::Event buffer = mouse->Read()) { while (Mouse::Event buffer = mouse->Read()) {
if (menuActive) continue; if (buffer.GetType() == Mouse::Event::Type::WheelUp) wheelSteps++;
else if (buffer.GetType() == Mouse::Event::Type::WheelDown) wheelSteps--;
if (menuActive || console->IsOpen()) continue;
if (buffer.GetType() == Mouse::Event::Type::Move && camera->lockedcursor) { if (buffer.GetType() == Mouse::Event::Type::Move && camera->lockedcursor) {
float xoffset = mouse->GetX() - mouse->GetLastX(); float xoffset = mouse->GetX() - mouse->GetLastX();
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::RPress) { }
camera->lockedcursor = !camera->lockedcursor;
if (camera->lockedcursor) { const glm::vec2 mousePos((float)mouse->GetX(), (float)mouse->GetY());
glfwSetInputMode(window->handle, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
// --------------------------------------------------------------------
// Debug: G throws a cube at the cursor, or straight ahead with the cursor locked
// --------------------------------------------------------------------
static bool prevLaunchDown = false;
const bool launchDown = keyboard->IsKeyDown(GLFW_KEY_G);
if (launchDown && !prevLaunchDown && !menuActive) {
const float launchSpeed = 40.0f;
const glm::vec2 aim = camera->lockedcursor ? glm::vec2(window->width * 0.5f, window->height * 0.5f) : mousePos;
const Ray ray = MouseRay(aim);
// The camera sits behind the player and looks over them, so start past them rather than hitting them on the way out
const float pastPlayer = glm::dot(controlled->body->t.GetTranslation() - ray.origin, ray.direction) + 1.0f;
const glm::vec3 start = ray.origin + ray.direction * std::max(pastPlayer, 1.0f);
SpawnDebugCube(start, ray.direction * launchSpeed);
}
prevLaunchDown = launchDown;
// --------------------------------------------------------------------
// Cursor. Locked for looking around, unless Alt has freed it or the console needs the mouse.
// The menus handle it themselves.
// --------------------------------------------------------------------
static bool mouseFreed = false;
static bool prevAltDown = false;
const bool altDown = keyboard->IsKeyDown(GLFW_KEY_LEFT_ALT) || keyboard->IsKeyDown(GLFW_KEY_RIGHT_ALT);
if (altDown && !prevAltDown && !menuActive)
mouseFreed = !mouseFreed;
prevAltDown = altDown;
if (!menuActive) {
const bool wantLocked = !mouseFreed && !console->IsOpen();
if (camera->lockedcursor != wantLocked) {
camera->lockedcursor = wantLocked;
glfwSetInputMode(window->handle, GLFW_CURSOR, wantLocked ? GLFW_CURSOR_DISABLED : GLFW_CURSOR_NORMAL);
if (wantLocked) {
int ww, wh; int ww, wh;
glfwGetWindowSize(window->handle, &ww, &wh); glfwGetWindowSize(window->handle, &ww, &wh);
glfwSetCursorPos(window->handle, ww * 0.5, wh * 0.5); glfwSetCursorPos(window->handle, ww * 0.5, wh * 0.5);
}
else { // Otherwise the jump back to the middle reads as one big mouse movement and the camera snaps
glfwSetInputMode(window->handle, GLFW_CURSOR, GLFW_CURSOR_NORMAL); mouse->OnMouseMove((unsigned int)(ww / 2), (unsigned int)(wh / 2));
mouse->OnMouseMove((unsigned int)(ww / 2), (unsigned int)(wh / 2));
} }
} }
} }
if (console->IsOpen() && wheelSteps != 0)
console->Scroll(wheelSteps * 3);
console->Update(deltatime);
if (menuActive) { if (menuActive) {
ApplyMenuLayout(); ApplyMenuLayout();
const bool liveBackdrop = menuScreen == MenuScreen::Main || const bool liveBackdrop = menuScreen == MenuScreen::Main ||
((menuScreen == MenuScreen::Settings || menuScreen == MenuScreen::Graphics || menuScreen == MenuScreen::Drawer) ((menuScreen == MenuScreen::Settings || menuScreen == MenuScreen::Graphics)
&& settingsReturn == MenuScreen::Main); && settingsReturn == MenuScreen::Main);
if (liveBackdrop) { if (liveBackdrop) {
static float menuAngle = 0.6f; static float menuAngle = 0.6f;
@@ -817,42 +819,9 @@ void Engine::Initiate()
menuScreen = MenuScreen::Settings; 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) { else if (playButton.hovered) {
ResumeGame(); ResumeGame();
} }
else if (drawerButton.hovered) {
settingsReturn = menuScreen;
menuScreen = MenuScreen::Drawer;
}
else if (settingsButton.hovered) { else if (settingsButton.hovered) {
settingsReturn = menuScreen; settingsReturn = menuScreen;
menuScreen = MenuScreen::Settings; menuScreen = MenuScreen::Settings;
@@ -864,25 +833,6 @@ void Engine::Initiate()
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();
@@ -891,74 +841,84 @@ void Engine::Initiate()
continue; continue;
} }
const glm::vec3 camFront = camera->t.GetFrontVector();
const glm::vec3 camRight = camera->t.GetRightVector();
const float movespeed = 3.0f * 1.3f * 0.6f;
// -------------------------------------------------------------------- // --------------------------------------------------------------------
// WASD movement // Characters: minds decide actions, bodies carry them out
// -------------------------------------------------------------------- // --------------------------------------------------------------------
const glm::vec3 inputDirection = testcharacter.GetMovementDirection(camFront, camRight); keyboardMind.enabled = !console->IsOpen(); // typing isn't walking
const bool hasMoveInput = glm::length(inputDirection) > 0.0f; charactermanager->Think();
if (hasMoveInput) for (Character* character : charactermanager->GetCharacters()) {
testcharacter.SetTurnDirection(inputDirection); if (character->ragdoll) continue;
testcharacter.SetWalking(hasMoveInput); character->body->ApplyMovement(IsGrounded(*character), 1.0f, deltatime, refDt);
}
charactermanager->Step(deltatime); charactermanager->Step(deltatime);
const glm::vec3 inputVel = hasMoveInput float physicsAlpha = 0.0f; // how far this frame is between the last tick and the next
? testcharacter.GetForwardDirection() * movespeed
: glm::vec3(0.0f);
const float accel = 40.0f;
const float damp = 0.75f;
const float airControl = 0.5f;
const float groundTopSpeed = movespeed * accel * refDt * damp / (1.0f - damp);
if (IsGrounded()) {
const float dampStep = std::pow(damp, deltatime / refDt);
const float accelScale = refDt * damp * (1.0f - dampStep) / (deltatime * dampStep * (1.0f - damp));
characterBody->linearvelocity.x *= dampStep;
characterBody->linearvelocity.z *= dampStep;
characterBody->linearvelocity.x += inputVel.x * accel * deltatime * accelScale;
characterBody->linearvelocity.z += inputVel.z * accel * deltatime * accelScale;
}
else if (glm::length(inputVel) > 0.0f) {
const glm::vec3 wishDir = glm::normalize(inputVel);
const float current = glm::dot(glm::vec3(characterBody->linearvelocity.x, 0.0f,
characterBody->linearvelocity.z), wishDir);
const float add = glm::clamp(groundTopSpeed - current, 0.0f,
accel * airControl * movespeed * deltatime);
characterBody->linearvelocity.x += wishDir.x * add;
characterBody->linearvelocity.z += wishDir.z * add;
}
{ {
const int substeps = std::max(1, (int)std::ceil(deltatime * 120.0f)); // Physics ticks a fixed 30 times a second of real time, whatever the frame rate.
const float subDt = deltatime / float(substeps); // deltatime is real time sped up by timeScale, so a tick covers that much more game time.
for (int i = 0; i < substeps; i++) physicsengine->Step(subDt); const float tickRate = 30.0f;
} const float tickSeconds = 1.0f / tickRate;
npc.ragdoll->Update(deltatime); const float timeScale = 144.0f / float(fps);
const float tickDt = tickSeconds * timeScale;
const int substepsPerTick = std::max(1, (int)std::ceil(tickDt * 120.0f)); // keeps fast things from passing through
const int maxTicksPerFrame = 4;
if (testcharacter.body->t.GetTranslation().y < -20.0f) { static float physicsAccumulator = 0.0f;
testcharacter.body->t.TranslateTo(RandomSpawn()); physicsAccumulator += deltatime / timeScale;
characterBody->linearvelocity = glm::vec3(0.0f); int ticks = 0;
characterBody->WakeUp(); while (physicsAccumulator >= tickSeconds && ticks < maxTicksPerFrame) {
physicsengine->Tick(tickDt, substepsPerTick);
physicsAccumulator -= tickSeconds;
ticks++;
}
// Fell too far behind, a long hitch. Drop the backlog rather than spending frames catching up.
if (physicsAccumulator >= tickSeconds)
physicsAccumulator = 0.0f;
physicsAlpha = physicsAccumulator / tickSeconds;
} }
// Nothing reads the contacts physics records yet, so they're let go every frame
physicsengine->ClearImpacts();
// Debug cubes that fell off the world go
for (int i = (int)debugCubes.size() - 1; i >= 0; i--) {
if (debugCubes[i].mesh->t.GetTranslationY() > -50.0f)
continue;
physicsengine->RemoveP(debugCubes[i].body);
delete debugCubes[i].body;
delete debugCubes[i].mesh; // takes itself out of the render system
debugCubes.erase(debugCubes.begin() + i);
}
for (Character* character : charactermanager->GetCharacters()) {
p* body = character->body->physics;
if (body && body->pointer->t.GetTranslation().y < -20.0f) {
body->pointer->t.TranslateTo(RandomSpawn());
body->linearvelocity = glm::vec3(0.0f);
body->WakeUp();
}
}
// Drawn partway to the next tick from here until after Render, so nothing may move bodies in between
physicsengine->ApplyInterpolation(physicsAlpha);
// -------------------------------------------------------------------- // --------------------------------------------------------------------
// Third person camera // Third person camera
// -------------------------------------------------------------------- // --------------------------------------------------------------------
{ {
const glm::vec3 playerPos = testcharacter.body->t.GetTranslation(); const glm::vec3 playerPos = controlled->body->t.GetTranslation();
const glm::vec3 target = playerPos + glm::vec3(0.0f, 0.8f, 0.0f); const glm::vec3 target = playerPos + glm::vec3(0.0f, 0.8f, 0.0f);
glm::vec3 cameraPos = target - camera->t.GetFrontVector() * 5.0f;
const bool playerOffFloor = // Scroll zooms, unless something on screen is using the wheel. Eases toward the new distance.
playerPos.x < floorBounds.min.x || playerPos.x > floorBounds.max.x || static float zoomTarget = 5.0f;
playerPos.z < floorBounds.min.z || playerPos.z > floorBounds.max.z; static float zoomDistance = 5.0f;
const bool ignoreFloorCollision = playerOffFloor || playerPos.y < floorBounds.max.y; if (!console->IsOpen() && wheelSteps != 0)
zoomTarget = glm::clamp(zoomTarget * std::pow(0.85f, (float)wheelSteps), 1.5f, 15.0f);
zoomDistance += (zoomTarget - zoomDistance) * (1.0f - std::exp(-12.0f * deltatime));
glm::vec3 cameraPos = target - camera->t.GetFrontVector() * zoomDistance;
const glm::vec3 cameraRay = cameraPos - target; const glm::vec3 cameraRay = cameraPos - target;
const float desiredDistance = glm::length(cameraRay); const float desiredDistance = glm::length(cameraRay);
@@ -969,12 +929,10 @@ void Engine::Initiate()
cameraSweep.direction = cameraDir; cameraSweep.direction = cameraDir;
float nearestHit = desiredDistance; float nearestHit = desiredDistance;
for (p* obj : physicsengine->GetObjects()) { for (p* obj : physicsengine->GetObjects()) {
if (obj->pointer == testcharacter.body || !obj->collision) if (obj->pointer == controlled->body || !obj->collision)
continue; continue;
if (obj->collisionGroup != 0) // flailing ragdoll limbs would make the camera jump if (obj->collisionGroup != 0) // flailing ragdoll limbs would make the camera jump
continue; continue;
if (obj == floorBody && ignoreFloorCollision)
continue;
auto hit = IsRayInT(cameraSweep, obj->pointer->t); auto hit = IsRayInT(cameraSweep, obj->pointer->t);
if (!hit.has_value()) if (!hit.has_value())
continue; continue;
@@ -1024,6 +982,7 @@ void Engine::Initiate()
// -------------------------------------------------------------------- // --------------------------------------------------------------------
UpdateHitboxDebug(); UpdateHitboxDebug();
rendersystem->Render(*camera); rendersystem->Render(*camera);
physicsengine->RemoveInterpolation(); // back to the real positions before anything moves them
glfwPollEvents(); glfwPollEvents();
} }
@@ -1044,6 +1003,22 @@ void framebuffer_size_callback(GLFWwindow* window, int w, int h)
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods)
{ {
Console* console = engine ? engine->console : nullptr;
if (console && key == GLFW_KEY_GRAVE_ACCENT && action == GLFW_PRESS) {
console->Toggle();
return;
}
// While typing, keys belong to the console. Releases still go through, so nothing held stays stuck down.
if (console && console->IsOpen()) {
if (action == GLFW_PRESS || action == GLFW_REPEAT)
console->OnKey(key);
else if (action == GLFW_RELEASE)
keyboard->KeyUp(key);
return;
}
if (key == GLFW_KEY_F11 && action == GLFW_PRESS) { if (key == GLFW_KEY_F11 && action == GLFW_PRESS) {
static int wx = 100, wy = 100, ww = 1920, wh = 1080; static int wx = 100, wy = 100, ww = 1920, wh = 1080;
if (glfwGetWindowMonitor(window)) { if (glfwGetWindowMonitor(window)) {
@@ -1064,16 +1039,18 @@ void key_callback(GLFWwindow* window, int key, int scancode, int action, int mod
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);
}
if (key >= GLFW_KEY_A && key <= GLFW_KEY_Z && engine && engine->charactermanager) { void char_callback(GLFWwindow* window, unsigned int codepoint)
const char letter = (char)('A' + (key - GLFW_KEY_A)); {
if (action == GLFW_PRESS) engine->charactermanager->PressInput(letter); if (engine && engine->console && engine->console->IsOpen())
else if (action == GLFW_RELEASE) engine->charactermanager->ReleaseInput(letter); engine->console->OnChar(codepoint);
}
} }
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset) void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{ {
// Mouse counts wheel notches in Windows units, 120 to a notch, and carries the remainder for touchpads
mouse->OnWheelDelta(mouse->GetX(), mouse->GetY(), (int)std::lround(yoffset * 120.0));
} }
void mouse_callback(GLFWwindow* window, double xposIn, double yposIn) void mouse_callback(GLFWwindow* window, double xposIn, double yposIn)
+4
View File
@@ -20,6 +20,8 @@ void framebuffer_size_callback(GLFWwindow* windowe, int w, int h);
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods); void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods);
void char_callback(GLFWwindow* window, unsigned int codepoint);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset); void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void mouse_callback(GLFWwindow* windowe, double xposIn, double yposIn); void mouse_callback(GLFWwindow* windowe, double xposIn, double yposIn);
@@ -34,6 +36,7 @@ class ParticleEmitter;
class Camera; class Camera;
class Animator; class Animator;
class CharacterManager; class CharacterManager;
class Console;
class Engine { class Engine {
public: public:
@@ -41,6 +44,7 @@ public:
Physics* physicsengine = nullptr; Physics* physicsengine = nullptr;
RenderSystem* rendersystem = nullptr; RenderSystem* rendersystem = nullptr;
CharacterManager* charactermanager = nullptr; CharacterManager* charactermanager = nullptr;
Console* console = nullptr;
public: public:
//METADATA //METADATA
int frame = 0; int frame = 0;
+316 -101
View File
@@ -64,8 +64,10 @@ void Physics::Step(float dt)
// Integrate forces ? velocities // Integrate forces ? velocities
for (p* body : objects) { for (p* body : objects) {
// Nothing rotates until the end of the step, so everything until then can share this // Nothing rotates until the end of the step, so everything until then can share this.
body->invInertia = ComputeInvInertiaTensor(body); // A sleeping body hasn't turned since it was last worked out.
if (!body->isSleeping)
body->invInertia = ComputeInvInertiaTensor(body);
if (!body->velocity || body->isSleeping || body->frozen) continue; if (!body->velocity || body->isSleeping || body->frozen) continue;
@@ -104,29 +106,80 @@ void Physics::Step(float dt)
} }
} }
// ============================================================================
// Tick and interpolation
// ============================================================================
void Physics::Tick(float dt, int substeps)
{
for (p* body : objects) {
if (!body->velocity) continue;
body->previousPosition = body->pointer->t.GetTranslation();
body->previousOrientation = body->pointer->t.GetRotationQuaternion();
body->hasPrevious = true;
}
substeps = std::max(substeps, 1);
for (int i = 0; i < substeps; i++)
Step(dt / float(substeps));
}
void Physics::ApplyInterpolation(float alpha)
{
alpha = glm::clamp(alpha, 0.0f, 1.0f);
for (p* body : objects) {
// Asleep it hasn't moved, so there's nothing to blend
if (!body->velocity || !body->hasPrevious || body->interpolated || body->isSleeping) continue;
t& transform = body->pointer->t;
body->simulatedPosition = transform.GetTranslation();
body->simulatedOrientation = transform.GetRotationQuaternion();
transform.TranslateTo(glm::mix(body->previousPosition, body->simulatedPosition, alpha));
if (!body->lockRotation)
transform.RotateToQuaternion(glm::slerp(body->previousOrientation, body->simulatedOrientation, alpha));
body->interpolated = true;
}
}
void Physics::RemoveInterpolation()
{
for (p* body : objects) {
if (!body->interpolated) continue;
body->pointer->t.TranslateTo(body->simulatedPosition);
body->pointer->t.RotateToQuaternion(body->simulatedOrientation);
body->interpolated = false;
}
}
// ============================================================================ // ============================================================================
// ResolveCollisions // ResolveCollisions
// ============================================================================ // ============================================================================
void Physics::ResolveCollisions(float dt) void Physics::ResolveCollisions(float dt)
{ {
FindCollisions(); FindCollisions();
for (Collision& col : collisions) for (Collision& col : collisions)
Resolve(&col); PrepareContact(col);
if (joints.empty()) return;
// The first contact pass nudges positions, so these have to be worked out after it
for (BallJoint* joint : joints) { for (BallJoint* joint : joints) {
DampJoint(*joint, dt); DampJoint(*joint, dt);
PrepareJoint(*joint, dt); PrepareJoint(*joint, dt);
} }
// Joints and contacts fight each other, so iterate them together or limbs get pulled through the floor // One pass over the contacts leaves things resting on several points spinning a little, since
for (int i = 0; i < PHYSICS_JOINT_ITERATIONS; i++) { // each push partly undoes the last. Going round several times settles them. Joints and contacts
// fight each other too, so they get iterated together.
const int iterations = joints.empty() ? PHYSICS_CONTACT_ITERATIONS : PHYSICS_JOINT_ITERATIONS;
for (int i = 0; i < iterations; i++) {
for (BallJoint* joint : joints) for (BallJoint* joint : joints)
SolveJoint(*joint); SolveJoint(*joint);
for (Collision& col : collisions) for (Collision& col : collisions)
Resolve(&col, false); SolveContact(col);
}
// Positions last, so nothing above saw them half moved
for (Collision& col : collisions) {
CorrectContactPosition(col);
if (col.normalImpulse > PHYSICS_MIN_IMPACT_IMPULSE)
impacts.push_back({ col.ObjA, col.ObjB, col.POI, col.normalImpulse });
} }
} }
@@ -135,36 +188,70 @@ void Physics::ResolveCollisions(float dt)
// Sort and sweep: once sorted along x, a body can only touch the ones after it // Sort and sweep: once sorted along x, a body can only touch the ones after it
// that start before it ends. // that start before it ends.
// ============================================================================ // ============================================================================
// A body's world bounds, only worked out again once its transform has changed
static const BoundingBox& GetBounds(p* body)
{
const t& transform = body->pointer->t;
if (body->boundsVersion != transform.GetVersion()) {
body->bounds = transform.GetAABB();
body->boundsVersion = transform.GetVersion();
}
return body->bounds;
}
void Physics::FindCollisions() void Physics::FindCollisions()
{ {
collisions.clear(); collisions.clear();
broadPhase.clear();
for (p* body : objects) { // Everything goes in, collision or not, so the list only changes when bodies come or go
if (!body->collision) continue; const bool rebuilt = broadPhaseVersion != objectsVersion;
BroadPhaseEntry entry; if (rebuilt) {
entry.body = body; broadPhase.clear();
entry.bounds = body->pointer->t.GetAABB(); for (p* body : objects) {
broadPhase.push_back(entry); BroadPhaseEntry entry;
entry.body = body;
broadPhase.push_back(entry);
}
broadPhaseVersion = objectsVersion;
} }
for (BroadPhaseEntry& entry : broadPhase)
entry.bounds = GetBounds(entry.body);
std::sort(broadPhase.begin(), broadPhase.end(), [](const BroadPhaseEntry& first, const BroadPhaseEntry& second) { // Things barely move between substeps, so last step's order is nearly sorted already and insertion
return first.bounds.min.x < second.bounds.min.x; // sort gets through it in about one pass. A fresh list gets a proper sort.
}); if (rebuilt) {
std::sort(broadPhase.begin(), broadPhase.end(), [](const BroadPhaseEntry& first, const BroadPhaseEntry& second) {
return first.bounds.min.x < second.bounds.min.x;
});
}
else {
for (int i = 1; i < (int)broadPhase.size(); i++) {
const BroadPhaseEntry entry = broadPhase[i];
int j = i - 1;
while (j >= 0 && broadPhase[j].bounds.min.x > entry.bounds.min.x) {
broadPhase[j + 1] = broadPhase[j];
j--;
}
broadPhase[j + 1] = entry;
}
}
for (int i = 0; i < (int)broadPhase.size(); i++) { for (int i = 0; i < (int)broadPhase.size(); i++) {
const BroadPhaseEntry& first = broadPhase[i]; const BroadPhaseEntry& first = broadPhase[i];
p* a = first.body; p* a = first.body;
if (!a->collision) continue;
for (int j = i + 1; j < (int)broadPhase.size(); j++) { for (int j = i + 1; j < (int)broadPhase.size(); j++) {
const BroadPhaseEntry& second = broadPhase[j]; const BroadPhaseEntry& second = broadPhase[j];
if (second.bounds.min.x >= first.bounds.max.x) break; if (second.bounds.min.x >= first.bounds.max.x) break;
p* b = second.body; p* b = second.body;
if (!b->collision) continue;
if (!a->velocity && !b->velocity) continue; if (!a->velocity && !b->velocity) continue;
if (a->isSleeping && b->isSleeping) continue; // Two things that can't move into each other, like an item asleep on the ground
if ((a->isSleeping || !a->velocity) && (b->isSleeping || !b->velocity) && !a->isHitbox && !b->isHitbox) continue;
if ((a->frozen || !a->velocity) && (b->frozen || !b->velocity)) continue; if ((a->frozen || !a->velocity) && (b->frozen || !b->velocity)) continue;
if (a->collisionGroup != 0 && a->collisionGroup == b->collisionGroup) continue; if (a->collisionGroup != 0 && a->collisionGroup == b->collisionGroup) continue; if ((a->isHitbox && b->collisionGroup != 0) || (b->isHitbox && a->collisionGroup != 0)) continue;
if (!BoundingBoxInBoundingBox(first.bounds, second.bounds)) continue; if (!BoundingBoxInBoundingBox(first.bounds, second.bounds)) continue;
if (a->collisionMesh || b->collisionMesh) { if (a->collisionMesh || b->collisionMesh) {
@@ -172,18 +259,29 @@ void Physics::FindCollisions()
continue; continue;
} }
std::optional<TInTInfo> e = TInT(a->pointer->t, b->pointer->t); std::optional<TInTInfo> e = TInT(a->pointer->t, b->pointer->t, false);
if (e.has_value()) { if (e.has_value())
Collision tp(a, b); CollideBoxes(a, b, e.value());
tp.POI = e.value().POI;
tp.CN = e.value().CN;
tp.overlap = e.value().overlap;
collisions.push_back(tp);
}
} }
} }
} }
// The inverse of a transform made of a position, rotation and scale: undo the move, turn back, then unscale.
// Much cheaper than a general 4x4 inverse.
static glm::mat4 InverseTransform(const t& transform)
{
const glm::mat3 rotation = glm::mat3_cast(transform.GetRotationQuaternion());
const glm::vec3 inverseScale = 1.0f / glm::max(glm::abs(transform.GetScale()), glm::vec3(1e-6f));
glm::mat3 inverse = glm::transpose(rotation);
for (int column = 0; column < 3; column++)
inverse[column] *= inverseScale; // scales each row, the unscale comes after the turn back
glm::mat4 result(inverse);
result[3] = glm::vec4(-(inverse * transform.GetTranslation()), 1.0f);
return result;
}
// ============================================================================ // ============================================================================
// CollideTriangles // CollideTriangles
// Tests every triangle of one body against the other's BVH. Triangles have no // Tests every triangle of one body against the other's BVH. Triangles have no
@@ -273,10 +371,11 @@ void Physics::CollideTriangles(p* a, p* b)
const glm::mat4 queryToWorld = (queryIsA ? a : b)->pointer->t.GetMatrix(); const glm::mat4 queryToWorld = (queryIsA ? a : b)->pointer->t.GetMatrix();
const glm::mat4 targetToWorld = (queryIsA ? b : a)->pointer->t.GetMatrix(); const glm::mat4 targetToWorld = (queryIsA ? b : a)->pointer->t.GetMatrix();
const glm::mat4 queryToTarget = glm::inverse(targetToWorld) * queryToWorld; const glm::mat4 queryToTarget = InverseTransform((queryIsA ? b : a)->pointer->t) * queryToWorld;
// Box faces are the reliable surface. Between two meshes, b's faces are used. // Something that never moves, like terrain, is the surface. Otherwise box faces are the
const bool surfaceIsA = !a->collisionMesh && b->collisionMesh; // reliable surface, and between two meshes, b's faces are used.
const bool surfaceIsA = a->velocity != b->velocity ? !a->velocity : (!a->collisionMesh && b->collisionMesh);
const bool surfaceIsQuery = surfaceIsA == queryIsA; const bool surfaceIsQuery = surfaceIsA == queryIsA;
meshContacts.clear(); meshContacts.clear();
@@ -340,6 +439,83 @@ void Physics::CollideTriangles(p* a, p* b)
} }
} }
// ============================================================================
// CollideBoxes
// One contact point can't hold a box flat: it tips about it every step and never settles.
// So every corner of either box that's inside the other becomes a contact, pushing along
// the separating axis TInT found. Edge on edge touches have no such corners and keep TInT's point.
// ============================================================================
void Physics::CollideBoxes(p* a, p* b, const TInTInfo& info)
{
static const glm::vec3 corners[8] = {
{ 0.5f, 0.5f, 0.5f }, { 0.5f, -0.5f, 0.5f }, { 0.5f, 0.5f, -0.5f }, { 0.5f, -0.5f, -0.5f },
{ -0.5f, 0.5f, 0.5f }, { -0.5f, -0.5f, 0.5f }, { -0.5f, 0.5f, -0.5f }, { -0.5f, -0.5f, -0.5f },
};
const float tolerance = 0.01f; // world units, so a corner resting right on the surface still counts
const t& ta = a->pointer->t;
const t& tb = b->pointer->t;
const glm::mat4 aToWorld = ta.GetMatrix();
const glm::mat4 bToWorld = tb.GetMatrix();
const glm::mat4 worldToA = InverseTransform(ta);
const glm::mat4 worldToB = InverseTransform(tb);
const glm::vec3 insideA = glm::vec3(0.5f) + tolerance / glm::max(ta.GetScale(), glm::vec3(1e-4f));
const glm::vec3 insideB = glm::vec3(0.5f) + tolerance / glm::max(tb.GetScale(), glm::vec3(1e-4f));
// CN points from b to a, so b's surface is its furthest point along it and a's is its nearest
glm::vec3 worldA[8], worldB[8];
float surfaceB = -FLT_MAX;
float surfaceA = FLT_MAX;
for (int i = 0; i < 8; i++) {
worldA[i] = glm::vec3(aToWorld * glm::vec4(corners[i], 1.0f));
worldB[i] = glm::vec3(bToWorld * glm::vec4(corners[i], 1.0f));
surfaceA = glm::min(surfaceA, glm::dot(worldA[i], info.CN));
surfaceB = glm::max(surfaceB, glm::dot(worldB[i], info.CN));
}
meshContacts.clear();
auto AddCorner = [&](const glm::vec3& point, float depth) {
Collision contact(a, b);
contact.POI = point;
contact.CN = info.CN;
contact.overlap = glm::clamp(depth, 0.0f, info.overlap);
meshContacts.push_back(contact);
};
for (int i = 0; i < 8; i++) {
if (glm::all(glm::lessThanEqual(glm::abs(glm::vec3(worldToB * glm::vec4(worldA[i], 1.0f))), insideB)))
AddCorner(worldA[i], surfaceB - glm::dot(worldA[i], info.CN));
if (glm::all(glm::lessThanEqual(glm::abs(glm::vec3(worldToA * glm::vec4(worldB[i], 1.0f))), insideA)))
AddCorner(worldB[i], glm::dot(worldB[i], info.CN) - surfaceA);
}
if (meshContacts.empty()) {
Collision contact(a, b);
contact.POI = BoxContactPoint(ta, tb);
contact.CN = info.CN;
contact.overlap = info.overlap;
collisions.push_back(contact);
return;
}
KeepSpreadContacts(meshContacts);
for (Collision& contact : meshContacts) {
contact.correctionWeight = 1.0f / (float)meshContacts.size();
collisions.push_back(contact);
}
}
// Whatever was asleep on or against a body that's going away would otherwise hang there in the air
void Physics::WakeAround(p* removed)
{
BoundingBox around = GetBounds(removed);
around.min -= glm::vec3(0.05f);
around.max += glm::vec3(0.05f);
for (p* body : objects) {
if (body->isSleeping && BoundingBoxInBoundingBox(GetBounds(body), around))
body->WakeUp();
}
}
void Physics::WakeGroup(int group) void Physics::WakeGroup(int group)
{ {
for (p* body : objects) { for (p* body : objects) {
@@ -496,91 +672,130 @@ void Physics::DampJoint(BallJoint& joint, float dt)
} }
// ============================================================================ // ============================================================================
// Resolve // Contacts
// Solved the same way as the joints. Prepare works out everything that only
// depends on where things are, then Solve runs many times a step. The impulse
// is kept as a running total that can't go below zero, so a later pass can
// take back an earlier pass's overshoot, but a contact can never pull.
// ============================================================================ // ============================================================================
void Physics::Resolve(Collision* c, bool firstPass) static void ApplyImpulse(p* body, const glm::vec3& impulse, const glm::vec3& r)
{ {
p* a = c->ObjA; if (!body->velocity)
p* b = c->ObjB; return;
body->linearvelocity += body->GetInvMass() * impulse;
if (!body->lockRotation)
body->angularvelocity += body->invInertia * glm::cross(r, impulse);
}
a->WakeUp(); static glm::vec3 RelativeVelocity(const Collision& c)
b->WakeUp(); {
const glm::vec3 velA = c.ObjA->linearvelocity + glm::cross(c.ObjA->angularvelocity, c.rA);
const glm::vec3 velB = c.ObjB->linearvelocity + glm::cross(c.ObjB->angularvelocity, c.rB);
return velA - velB;
}
const glm::vec3 n = c->CN; // One over how hard it is to push the two points apart along direction
const glm::vec3 rA = c->POI - a->pointer->t.GetTranslation(); static float InverseDenominator(const Collision& c, const glm::vec3& direction)
const glm::vec3 rB = c->POI - b->pointer->t.GetTranslation(); {
auto angular = [](const glm::mat3& invI, const glm::vec3& r, const glm::vec3& d) {
const glm::mat3& invIA = a->invInertia;
const glm::mat3& invIB = b->invInertia;
const float invMassA = a->GetInvMass();
const float invMassB = b->GetInvMass();
const glm::vec3 velA = a->linearvelocity + glm::cross(a->angularvelocity, rA);
const glm::vec3 velB = b->linearvelocity + glm::cross(b->angularvelocity, rB);
const glm::vec3 relVel = velA - velB;
const float relVelN = glm::dot(relVel, n);
if (relVelN > 0.0f) return;
const float e = (!firstPass || glm::abs(relVelN) < PHYSICS_RESTITUTION_THRESHOLD)
? 0.0f
: glm::min(a->restitution, b->restitution);
auto angDenom = [](const glm::mat3& invI, const glm::vec3& r, const glm::vec3& d) {
return glm::dot(glm::cross(invI * glm::cross(r, d), r), d); return glm::dot(glm::cross(invI * glm::cross(r, d), r), d);
}; };
const float denom = c.ObjA->GetInvMass() + c.ObjB->GetInvMass()
+ angular(c.ObjA->invInertia, c.rA, direction) + angular(c.ObjB->invInertia, c.rB, direction);
return denom > 1e-8f ? 1.0f / denom : 0.0f;
}
// ---- Normal impulse ---- // Something moving into a sleeping body wakes it. Something merely resting against it doesn't, or the two would
const float denomN = invMassA + invMassB + angDenom(invIA, rA, n) + angDenom(invIB, rB, n); // keep each other awake forever, and waking resets the timer every body needs to run down before it can sleep.
const float jN = -(1.0f + e) * relVelN / denomN; // A push that does get through to a sleeper wakes it anyway, in UpdateSleep.
static void WakeIfDisturbed(p* sleeper, const p* other)
const glm::vec3 impulseN = jN * n; {
if (a->velocity) { if (!sleeper->isSleeping)
a->linearvelocity += invMassA * impulseN; return;
if (!a->lockRotation) // Hitboxes get moved by the animation rather than by physics, so they have no velocity to go by
a->angularvelocity += invIA * glm::cross(rA, impulseN); if (other->isHitbox) {
} sleeper->WakeUp();
if (b->velocity) { return;
b->linearvelocity -= invMassB * impulseN;
if (!b->lockRotation)
b->angularvelocity -= invIB * glm::cross(rB, impulseN);
} }
const float linSq = glm::dot(other->linearvelocity, other->linearvelocity);
const float angSq = glm::dot(other->angularvelocity, other->angularvelocity);
if (linSq > PHYSICS_SLEEP_LINEAR_SQ * 4.0f || angSq > PHYSICS_SLEEP_ANGULAR_SQ * 4.0f)
sleeper->WakeUp();
}
// ---- Friction impulse ---- void Physics::PrepareContact(Collision& c)
glm::vec3 tangent = relVel - relVelN * n; {
const float tangLen = glm::length(tangent); p* a = c.ObjA;
if (tangLen > 1e-6f) { p* b = c.ObjB;
tangent /= tangLen; WakeIfDisturbed(a, b);
WakeIfDisturbed(b, a);
const float denomT = invMassA + invMassB + angDenom(invIA, rA, tangent) + angDenom(invIB, rB, tangent); c.rA = c.POI - a->pointer->t.GetTranslation();
float jT = -glm::dot(relVel, tangent) / denomT; c.rB = c.POI - b->pointer->t.GetTranslation();
const float mu = (a->friction + b->friction) * 0.5f * c->frictionScale; // Friction works along two fixed directions across the surface
jT = glm::clamp(jT, -mu * jN, mu * jN); const glm::vec3& n = c.CN;
c.tangent1 = glm::abs(n.x) > 0.57f ? glm::vec3(n.y, -n.x, 0.0f) : glm::vec3(0.0f, n.z, -n.y);
c.tangent1 = glm::normalize(c.tangent1);
c.tangent2 = glm::cross(n, c.tangent1);
const glm::vec3 impulseT = jT * tangent; c.inverseDenomN = InverseDenominator(c, n);
if (a->velocity) { c.inverseDenomT1 = InverseDenominator(c, c.tangent1);
a->linearvelocity += invMassA * impulseT; c.inverseDenomT2 = InverseDenominator(c, c.tangent2);
if (!a->lockRotation) c.mu = (a->friction + b->friction) * 0.5f * c.frictionScale;
a->angularvelocity += invIA * glm::cross(rA, impulseT);
}
if (b->velocity) {
b->linearvelocity -= invMassB * impulseT;
if (!b->lockRotation)
b->angularvelocity -= invIB * glm::cross(rB, impulseT);
}
}
// ---- Baumgarte positional correction ---- // Bounce off at a share of the speed it came in at, but slow touches just stop, or resting things would hop
if (!firstPass) const float approach = glm::dot(RelativeVelocity(c), n);
const float restitution = approach < -PHYSICS_RESTITUTION_THRESHOLD ? glm::min(a->restitution, b->restitution) : 0.0f;
c.targetSpeed = -restitution * approach;
c.normalImpulse = 0.0f;
c.tangentImpulse = glm::vec2(0.0f);
}
void Physics::SolveContact(Collision& c)
{
const glm::vec3& n = c.CN;
// ---- Normal ----
const float speed = glm::dot(RelativeVelocity(c), n);
const float total = glm::max(c.normalImpulse + (c.targetSpeed - speed) * c.inverseDenomN, 0.0f);
const float normalStep = total - c.normalImpulse;
c.normalImpulse = total;
ApplyImpulse(c.ObjA, n * normalStep, c.rA);
ApplyImpulse(c.ObjB, -n * normalStep, c.rB);
// ---- Friction, never more than mu times how hard the surfaces press together ----
const glm::vec3 relVel = RelativeVelocity(c);
glm::vec2 friction = c.tangentImpulse + glm::vec2(
-glm::dot(relVel, c.tangent1) * c.inverseDenomT1,
-glm::dot(relVel, c.tangent2) * c.inverseDenomT2);
const float maxFriction = c.mu * c.normalImpulse;
if (glm::length(friction) > maxFriction)
friction = glm::length(friction) > 1e-8f ? glm::normalize(friction) * maxFriction : glm::vec2(0.0f);
const glm::vec2 frictionStep = friction - c.tangentImpulse;
c.tangentImpulse = friction;
const glm::vec3 frictionImpulse = c.tangent1 * frictionStep.x + c.tangent2 * frictionStep.y;
ApplyImpulse(c.ObjA, frictionImpulse, c.rA);
ApplyImpulse(c.ObjB, -frictionImpulse, c.rB);
}
// Baumgarte: nudges overlapping bodies apart a little each step
void Physics::CorrectContactPosition(Collision& c)
{
p* a = c.ObjA;
p* b = c.ObjB;
const float invMassA = a->GetInvMass();
const float invMassB = b->GetInvMass();
if (invMassA + invMassB <= 0.0f)
return; 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; * c.correctionWeight;
const glm::vec3 corrVec = correction * n; const glm::vec3 corrVec = correction * c.CN;
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);
} }
+73 -6
View File
@@ -27,10 +27,11 @@ static constexpr float PHYSICS_LINEAR_DAMPING = 0.995f;
static constexpr float PHYSICS_ANGULAR_DAMPING = 0.990f; static constexpr float PHYSICS_ANGULAR_DAMPING = 0.990f;
static constexpr float PHYSICS_BAUMGARTE_FACTOR = 0.1f; static constexpr float PHYSICS_BAUMGARTE_FACTOR = 0.1f;
static constexpr float PHYSICS_BAUMGARTE_SLOP = 0.02f; static constexpr float PHYSICS_BAUMGARTE_SLOP = 0.02f;
static constexpr float PHYSICS_SLEEP_LINEAR_SQ = 0.01f * 0.01f; static constexpr float PHYSICS_SLEEP_LINEAR_SQ = 0.05f * 0.05f; // m/s
static constexpr float PHYSICS_SLEEP_ANGULAR_SQ = 0.01f * 0.01f; static constexpr float PHYSICS_SLEEP_ANGULAR_SQ = 0.08f * 0.08f; // rad/s
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_CONTACT_ITERATIONS = 8; // per step with no joints around
static constexpr int PHYSICS_JOINT_ITERATIONS = 12; static constexpr int PHYSICS_JOINT_ITERATIONS = 12;
static constexpr float PHYSICS_JOINT_BIAS = 0.2f; static constexpr float PHYSICS_JOINT_BIAS = 0.2f;
static constexpr float PHYSICS_JOINT_MAX_BIAS_SPEED = 5.0f; static constexpr float PHYSICS_JOINT_MAX_BIAS_SPEED = 5.0f;
@@ -38,6 +39,7 @@ static constexpr float PHYSICS_JOINT_DAMPING = 5.0f;
static constexpr int PHYSICS_MAX_MESH_CONTACTS = 4; static constexpr int PHYSICS_MAX_MESH_CONTACTS = 4;
static constexpr float PHYSICS_MAX_MESH_DEPTH = 0.1f; 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 static constexpr float PHYSICS_MESH_PAIR_FRICTION = 0.25f; // two meshes catch on each other, so they slide easier
static constexpr float PHYSICS_MIN_IMPACT_IMPULSE = 1.0f; // newton seconds, softer contacts like resting aren't recorded
// ============================================================================ // ============================================================================
// Physics body // Physics body
@@ -75,6 +77,7 @@ public:
bool isSleeping = false; bool isSleeping = false;
bool lockRotation = false; bool lockRotation = false;
bool frozen = false; // parked until something touches its group bool frozen = false; // parked until something touches its group
bool isHitbox = false; // only collides with loose objects, never with other characters
// Set this to collide triangle by triangle, otherwise the body is its t's box // Set this to collide triangle by triangle, otherwise the body is its t's box
const CollisionMesh* collisionMesh = nullptr; const CollisionMesh* collisionMesh = nullptr;
@@ -93,6 +96,18 @@ public:
glm::mat3 invInertia = glm::mat3(0.0f); // world space, refreshed at the start of each step glm::mat3 invInertia = glm::mat3(0.0f); // world space, refreshed at the start of each step
float sleepTimer = 0.0f; float sleepTimer = 0.0f;
// For drawing it smoothly between physics ticks, see Physics::ApplyInterpolation
glm::vec3 previousPosition = glm::vec3(0.0f);
glm::quat previousOrientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f);
glm::vec3 simulatedPosition = glm::vec3(0.0f);
glm::quat simulatedOrientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f);
bool hasPrevious = false; // false until its first tick, so new bodies don't slide in from nowhere
bool interpolated = false; // t currently holds the blended pose, not the real one
// World bounds, kept until t changes. Most bodies sit still most of the time.
BoundingBox bounds;
unsigned int boundsVersion = 0;
}; };
// ============================================================================ // ============================================================================
@@ -107,6 +122,21 @@ struct Collision {
p* ObjA = nullptr; p* ObjA = nullptr;
p* ObjB = nullptr; p* ObjB = nullptr;
// Filled in by PrepareContact, these can't change during a step's iterations
glm::vec3 rA = glm::vec3(0.0f);
glm::vec3 rB = glm::vec3(0.0f);
glm::vec3 tangent1 = glm::vec3(0.0f);
glm::vec3 tangent2 = glm::vec3(0.0f);
float inverseDenomN = 0.0f;
float inverseDenomT1 = 0.0f;
float inverseDenomT2 = 0.0f;
float targetSpeed = 0.0f; // how fast it should be separating afterwards, the bounce
float mu = 0.0f;
// Running totals over the iterations
float normalImpulse = 0.0f;
glm::vec2 tangentImpulse = glm::vec2(0.0f);
Collision(p* a, p* b) : ObjA(a), ObjB(b) {} Collision(p* a, p* b) : ObjA(a), ObjB(b) {}
}; };
@@ -154,6 +184,14 @@ struct BallJoint {
int rowCount = 0; int rowCount = 0;
}; };
// One contact pushing two bodies apart during a step. impulse is in newton seconds.
struct Impact {
p* a = nullptr;
p* b = nullptr;
glm::vec3 point = glm::vec3(0.0f);
float impulse = 0.0f;
};
struct BroadPhaseEntry { struct BroadPhaseEntry {
p* body = nullptr; p* body = nullptr;
BoundingBox bounds; BoundingBox bounds;
@@ -168,18 +206,25 @@ private:
std::vector<BallJoint*> joints; 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 // Kept between steps so they don't get reallocated. The broad phase also keeps its order,
// and is only rebuilt when bodies come or go.
std::vector<BroadPhaseEntry> broadPhase; std::vector<BroadPhaseEntry> broadPhase;
unsigned int objectsVersion = 1;
unsigned int broadPhaseVersion = 0;
std::vector<Collision> collisions; std::vector<Collision> collisions;
std::vector<Collision> meshContacts; std::vector<Collision> meshContacts;
std::vector<int> meshCandidates; 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, bool firstPass = true); void PrepareContact(Collision& c);
void SolveContact(Collision& c);
void CorrectContactPosition(Collision& c);
void FindCollisions(); void FindCollisions();
void CollideTriangles(p* a, p* b); void CollideTriangles(p* a, p* b);
void CollideBoxes(p* a, p* b, const TInTInfo& info);
void WakeGroup(int group); void WakeGroup(int group);
void WakeAround(p* removed);
void PrepareJoint(BallJoint& joint, float dt); void PrepareJoint(BallJoint& joint, float dt);
void SolveJoint(BallJoint& joint); void SolveJoint(BallJoint& joint);
void DampJoint(BallJoint& joint, float dt); void DampJoint(BallJoint& joint, float dt);
@@ -191,7 +236,7 @@ public:
void SetGravity(const glm::vec3& g) noexcept { gravity = g; } void SetGravity(const glm::vec3& g) noexcept { gravity = g; }
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); objectsVersion++; }
void AddJoint(BallJoint* joint) { joints.push_back(joint); } void AddJoint(BallJoint* joint) { joints.push_back(joint); }
const std::vector<p*>& GetObjects() const { return objects; } const std::vector<p*>& GetObjects() const { return objects; }
@@ -203,13 +248,18 @@ public:
void RemoveP(p* obj) { void RemoveP(p* obj) {
objects.erase(std::find(objects.begin(), objects.end(), obj)); objects.erase(std::find(objects.begin(), objects.end(), obj));
objectsVersion++;
WakeAround(obj);
} }
void RemoveObject(t_package* pointer) { void RemoveObject(t_package* pointer) {
for (int i = 0; i < (int)objects.size(); i++) { for (int i = 0; i < (int)objects.size(); i++) {
if (objects[i]->pointer == pointer) { if (objects[i]->pointer == pointer) {
delete objects[i]; p* removed = objects[i];
objects.erase(objects.begin() + i); objects.erase(objects.begin() + i);
objectsVersion++;
WakeAround(removed);
delete removed;
return; return;
} }
} }
@@ -217,7 +267,24 @@ public:
void Step(float dt); void Step(float dt);
// One fixed physics tick, split into substeps so fast things don't pass through each other.
// Remembers where everything was beforehand, for ApplyInterpolation.
void Tick(float dt, int substeps);
// Moves every moving body's t partway from where it was before the last tick (0) to where it is now (1),
// so things drawn between ticks don't step along at the tick rate. Nothing may move bodies until
// RemoveInterpolation puts the real positions back.
void ApplyInterpolation(float alpha);
void RemoveInterpolation();
void ResolveCollisions(float dt); void ResolveCollisions(float dt);
// Every contact since the last ClearImpacts, for working out what hit what and how hard
const std::vector<Impact>& GetImpacts() const { return impacts; }
void ClearImpacts() { impacts.clear(); }
private:
std::vector<Impact> impacts;
}; };
#endif #endif
+15 -13
View File
@@ -480,23 +480,25 @@ void RenderSystem::Render(Camera& camera)
} }
// ---- GUI (always on top, no depth) ---- // ---- GUI (always on top, no depth) ----
// In the order things were made, boxes and text mixed, so a menu made later covers the text of one under it.
// The shader only gets switched when the kind of element changes.
glDisable(GL_CULL_FACE); glDisable(GL_CULL_FACE);
PrepareBoxShader(camera); int activeGuiShader = -1;
for (Renderable* r : renderables) { for (Renderable* r : renderables) {
if (r->shadertype == Renderable::BoxShader) const int type = r->shadertype;
r->Render(*BoxShader); if (type != Renderable::BoxShader && type != Renderable::ImageBoxShader && type != Renderable::TextShader)
} continue;
PrepareImageBoxShader(camera); if (type != activeGuiShader) {
for (Renderable* r : renderables) { if (type == Renderable::BoxShader) PrepareBoxShader(camera);
if (r->shadertype == Renderable::ImageBoxShader) else if (type == Renderable::ImageBoxShader) PrepareImageBoxShader(camera);
r->Render(*ImageBoxShader); else PrepareTextShader(camera);
} activeGuiShader = type;
}
PrepareTextShader(camera); if (type == Renderable::BoxShader) r->Render(*BoxShader);
for (Renderable* r : renderables) { else if (type == Renderable::ImageBoxShader) r->Render(*ImageBoxShader);
if (r->shadertype == Renderable::TextShader) else r->Render(*Text2DShader);
r->Render(*Text2DShader);
} }
glEnable(GL_CULL_FACE); glEnable(GL_CULL_FACE);
+48
View File
@@ -3,8 +3,13 @@
#include <iostream> #include <iostream>
#include <stdexcept> #include <stdexcept>
#include <assimp/Importer.hpp>
#include <assimp/postprocess.h>
#include <assimp/scene.h>
#include "texture.h" #include "texture.h"
#include "font.h" #include "font.h"
#include "MeshData.h"
ResourceManager::~ResourceManager() ResourceManager::~ResourceManager()
{ {
@@ -19,6 +24,49 @@ ResourceManager::~ResourceManager()
} }
for (auto& [path, font] : fonts) for (auto& [path, font] : fonts)
delete font; delete font;
for (auto& [path, mesh] : meshes)
delete mesh;
}
const MeshData* ResourceManager::GetMeshData(const std::string& path)
{
auto existing = meshes.find(path);
if (existing != meshes.end())
return existing->second;
// Pre-transforming bakes each node's placement in, so the merged meshes keep their layout
Assimp::Importer importer;
const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_GenSmoothNormals |
aiProcess_JoinIdenticalVertices | aiProcess_PreTransformVertices);
MeshData* data = nullptr;
if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || scene->mNumMeshes == 0) {
std::cout << "RESOURCE MANAGER: couldn't load mesh " << path << ": " << importer.GetErrorString() << "\n";
}
else {
data = new MeshData();
for (unsigned int m = 0; m < scene->mNumMeshes; m++) {
const aiMesh* mesh = scene->mMeshes[m];
const GLuint firstVertex = (GLuint)data->vertices.size();
for (unsigned int i = 0; i < mesh->mNumVertices; i++) {
Vertex vertex;
vertex.Position = glm::vec3(mesh->mVertices[i].x, mesh->mVertices[i].y, mesh->mVertices[i].z);
vertex.Normal = mesh->HasNormals()
? glm::vec3(mesh->mNormals[i].x, mesh->mNormals[i].y, mesh->mNormals[i].z) : glm::vec3(0.0f, 1.0f, 0.0f);
vertex.TexCoords = mesh->mTextureCoords[0]
? glm::vec2(mesh->mTextureCoords[0][i].x, mesh->mTextureCoords[0][i].y) : glm::vec2(0.0f);
data->vertices.push_back(vertex);
}
for (unsigned int f = 0; f < mesh->mNumFaces; f++) {
for (unsigned int j = 0; j < mesh->mFaces[f].mNumIndices; j++)
data->indices.push_back(firstVertex + mesh->mFaces[f].mIndices[j]);
}
}
}
meshes[path] = data;
return data;
} }
Texture* ResourceManager::CreateTexture(int width, int height) Texture* ResourceManager::CreateTexture(int width, int height)
+5
View File
@@ -7,6 +7,7 @@
class Texture; class Texture;
class Font; class Font;
class MeshData;
// Owns every texture and font, so each file only loads once however many things use it. // 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. // Sounds are cached by SoundSystem, rigs and animations by CharacterManager.
@@ -20,6 +21,9 @@ public:
Texture* GetTexture(const std::string& path); Texture* GetTexture(const std::string& path);
Font* GetFont(const std::string& path); Font* GetFont(const std::string& path);
// Every mesh in a model file merged into one, in the file's own units. nullptr if it can't be read.
const MeshData* GetMeshData(const std::string& path);
// Not cached, every call makes a new texture that's free to be edited // Not cached, every call makes a new texture that's free to be edited
Texture* CreateTexture(int width, int height); Texture* CreateTexture(int width, int height);
Texture* CopyTexture(const Texture& source); Texture* CopyTexture(const Texture& source);
@@ -27,6 +31,7 @@ public:
private: private:
std::unordered_map<std::string, Texture*> textures; std::unordered_map<std::string, Texture*> textures;
std::unordered_map<std::string, Font*> fonts; std::unordered_map<std::string, Font*> fonts;
std::unordered_map<std::string, MeshData*> meshes;
std::vector<Texture*> createdTextures; std::vector<Texture*> createdTextures;
}; };
-71
View File
@@ -1,71 +0,0 @@
#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
BIN
View File
Binary file not shown.
Binary file not shown.
-741
View File
File diff suppressed because one or more lines are too long
-741
View File
File diff suppressed because one or more lines are too long
Binary file not shown.
+6 -4
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations"> <ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32"> <ProjectConfiguration Include="Debug|Win32">
@@ -166,6 +166,7 @@
<ItemGroup> <ItemGroup>
<ClCompile Include="Character.cpp" /> <ClCompile Include="Character.cpp" />
<ClCompile Include="CharacterManager.cpp" /> <ClCompile Include="CharacterManager.cpp" />
<ClCompile Include="CharacterMind.cpp" />
<ClCompile Include="CollisionMesh.cpp" /> <ClCompile Include="CollisionMesh.cpp" />
<ClCompile Include="Cubemap.h" /> <ClCompile Include="Cubemap.h" />
<ClCompile Include="EBO.cpp" /> <ClCompile Include="EBO.cpp" />
@@ -175,6 +176,7 @@
<ClCompile Include="Mouse.cpp" /> <ClCompile Include="Mouse.cpp" />
<ClCompile Include="Physics.cpp" /> <ClCompile Include="Physics.cpp" />
<ClCompile Include="Ragdoll.cpp" /> <ClCompile Include="Ragdoll.cpp" />
<ClCompile Include="Console.cpp" />
<ClCompile Include="RenderSystem.cpp" /> <ClCompile Include="RenderSystem.cpp" />
<ClCompile Include="ResourceManager.cpp" /> <ClCompile Include="ResourceManager.cpp" />
<ClCompile Include="shaderClass.cpp" /> <ClCompile Include="shaderClass.cpp" />
@@ -187,12 +189,12 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="Animator.h" /> <ClInclude Include="Animator.h" />
<ClInclude Include="BodyComponentActions.h" /> <ClInclude Include="CharacterAction.h" />
<ClInclude Include="Character.h" /> <ClInclude Include="Character.h" />
<ClInclude Include="CharacterManager.h" /> <ClInclude Include="CharacterManager.h" />
<ClInclude Include="CollisionMesh.h" /> <ClInclude Include="CollisionMesh.h" />
<ClInclude Include="common.h" /> <ClInclude Include="common.h" />
<ClInclude Include="DefaultBodyComponents.h" /> <ClInclude Include="CharacterMind.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" />
@@ -204,6 +206,7 @@
<ClInclude Include="Physics.h" /> <ClInclude Include="Physics.h" />
<ClInclude Include="QuadVertices.h" /> <ClInclude Include="QuadVertices.h" />
<ClInclude Include="Ragdoll.h" /> <ClInclude Include="Ragdoll.h" />
<ClInclude Include="Console.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" />
@@ -223,7 +226,6 @@
<ClInclude Include="shaderClass.h" /> <ClInclude Include="shaderClass.h" />
<ClInclude Include="SoundSystem.h" /> <ClInclude Include="SoundSystem.h" />
<ClInclude Include="SurfaceTexture.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" />
+10 -4
View File
@@ -49,6 +49,9 @@
<ClCompile Include="Ragdoll.cpp"> <ClCompile Include="Ragdoll.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="Console.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="ResourceManager.cpp"> <ClCompile Include="ResourceManager.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
@@ -61,6 +64,9 @@
<ClCompile Include="CharacterManager.cpp"> <ClCompile Include="CharacterManager.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="CharacterMind.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="Camera.h"> <ClInclude Include="Camera.h">
@@ -135,10 +141,10 @@
<ClInclude Include="Ragdoll.h"> <ClInclude Include="Ragdoll.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="SurfaceTexture.h"> <ClInclude Include="Console.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="TexturePainter.h"> <ClInclude Include="SurfaceTexture.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="Gui.h"> <ClInclude Include="Gui.h">
@@ -195,10 +201,10 @@
<ClInclude Include="CharacterManager.h"> <ClInclude Include="CharacterManager.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="BodyComponentActions.h"> <ClInclude Include="CharacterAction.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="DefaultBodyComponents.h"> <ClInclude Include="CharacterMind.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="Animator.h"> <ClInclude Include="Animator.h">
+23 -67
View File
@@ -63,65 +63,23 @@ uniform float envIntensity = 0.8;
const float PI = 3.14159265359; const float PI = 3.14159265359;
float DistributionGGX(vec3 N, vec3 H, float rough) // Plain Blinn-Phong: ambient, Lambert diffuse and a half vector highlight.
// Roughness sets how tight and strong the highlight is, metallic tints it and takes away the diffuse.
// Close enough to the 2.2 curve and much cheaper than pow
vec3 ToLinear(vec3 c)
{ {
float a = rough * rough; return c * c;
float a2 = a * a;
float NdotH = max(dot(N, H), 0.0);
float NdotH2 = NdotH * NdotH;
float denom = NdotH2 * (a2 - 1.0) + 1.0;
return a2 / max(PI * denom * denom, 1e-5);
} }
float GeometrySchlickGGX(float NdotV, float rough) // Light from one direction, L points from the surface toward the light.
{ // diffuse and specular are already scaled for this pixel, see main.
float r = rough + 1.0; vec3 ShadeLight(vec3 L, vec3 radiance, vec3 N, vec3 V, vec3 diffuse, vec3 specular, float power)
float k = (r * r) / 8.0;
return NdotV / (NdotV * (1.0 - k) + k);
}
float GeometrySmith(vec3 N, vec3 V, vec3 L, float rough)
{
float NdotV = max(dot(N, V), 0.0);
float NdotL = max(dot(N, L), 0.0);
return GeometrySchlickGGX(NdotV, rough) * GeometrySchlickGGX(NdotL, rough);
}
vec3 FresnelSchlick(float cosTheta, vec3 F0)
{
return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}
vec3 FresnelSchlickRoughness(float cosTheta, vec3 F0, float rough)
{
vec3 f = max(vec3(1.0 - rough), F0);
return F0 + (f - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}
vec3 EnvironmentBRDF(vec3 F0, float rough, float NdotV)
{
const vec4 c0 = vec4(-1.0, -0.0275, -0.572, 0.022);
const vec4 c1 = vec4(1.0, 0.0425, 1.04, -0.04);
vec4 r = rough * c0 + c1;
float a004 = min(r.x * r.x, exp2(-9.28 * NdotV)) * r.x + r.y;
vec2 AB = vec2(-1.04, 1.04) * a004 + r.zw;
return F0 * AB.x + AB.y;
}
// Light from one direction, L points from the surface toward the light
vec3 ShadeLight(vec3 L, vec3 radiance, vec3 N, vec3 V, vec3 albedo, float metal, float rough, vec3 F0, float NdotV)
{ {
float NdotL = max(dot(N, L), 0.0); float NdotL = max(dot(N, L), 0.0);
if (NdotL <= 0.0)
return vec3(0.0);
vec3 H = normalize(V + L); vec3 H = normalize(V + L);
vec3 F = FresnelSchlick(max(dot(H, V), 0.0), F0); float highlight = pow(max(dot(N, H), 0.0), power);
float NDF = DistributionGGX(N, H, rough); return (diffuse + specular * highlight) * radiance * NdotL;
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 // Inverse square falloff, windowed so it reaches exactly zero at the radius
@@ -196,7 +154,7 @@ void main()
albedoSample = texture(tex, texCoord); albedoSample = texture(tex, texCoord);
if (albedoSample.a < 0.1) if (albedoSample.a < 0.1)
discard; discard;
albedoSample.rgb = pow(albedoSample.rgb, vec3(2.2)); albedoSample.rgb = ToLinear(albedoSample.rgb);
} }
albedoSample.rgb = mix(albedoSample.rgb, colorOverride.rgb, colorOverride.w); albedoSample.rgb = mix(albedoSample.rgb, colorOverride.rgb, colorOverride.w);
@@ -207,22 +165,25 @@ void main()
vec3 N = normalize(normal); vec3 N = normalize(normal);
vec3 V = normalize(viewPos - FragPos); vec3 V = normalize(viewPos - FragPos);
float NdotV = max(dot(N, V), 1e-4);
vec3 F0 = mix(vec3(0.04), albedo, metal); float smoothness = 1.0 - clamp(rough, 0.0, 1.0);
float power = exp2(10.0 * smoothness + 1.0); // 2 when fully rough, 2048 when mirror smooth
vec3 specularColor = mix(vec3(0.5 * smoothness * smoothness), albedo * (0.25 + 0.75 * smoothness), metal);
vec3 diffuseBRDF = albedo * (1.0 - metal) / PI; // the light intensities were tuned with the 1/PI in
vec3 specularBRDF = specularColor / PI;
vec3 direct = vec3(0.0); vec3 direct = vec3(0.0);
// Facing away from the sun has no direct light, so skip the BRDF and the 9 shadow samples // Facing away from the sun has no direct light, so skip the BRDF and the 9 shadow samples
vec3 sunL = normalize(-dirLight.direction); vec3 sunL = normalize(-dirLight.direction);
if (dot(N, sunL) > 0.0) { if (dot(N, sunL) > 0.0) {
float shadow = ShadowCalculation(fragposlightspace, N, sunL); float shadow = ShadowCalculation(fragposlightspace, N, sunL);
direct += ShadeLight(sunL, dirLight.color * dirLight.intensity, N, V, albedo, metal, rough, F0, NdotV) * shadow; direct += ShadeLight(sunL, dirLight.color * dirLight.intensity, N, V, diffuseBRDF, specularBRDF, power) * shadow;
} }
for (int i = 0; i < directionalLightCount; i++) { for (int i = 0; i < directionalLightCount; i++) {
vec3 L = normalize(-directionalLights[i].direction); vec3 L = normalize(-directionalLights[i].direction);
vec3 radiance = directionalLights[i].color * directionalLights[i].intensity; vec3 radiance = directionalLights[i].color * directionalLights[i].intensity;
direct += ShadeLight(L, radiance, N, V, albedo, metal, rough, F0, NdotV); direct += ShadeLight(L, radiance, N, V, diffuseBRDF, specularBRDF, power);
} }
for (int i = 0; i < pointLightCount; i++) { for (int i = 0; i < pointLightCount; i++) {
@@ -237,7 +198,7 @@ void main()
if (dot(N, L) <= 0.0) if (dot(N, L) <= 0.0)
continue; continue;
vec3 lit = ShadeLight(L, radiance, N, V, albedo, metal, rough, F0, NdotV); vec3 lit = ShadeLight(L, radiance, N, V, diffuseBRDF, specularBRDF, power);
if (pointLights[i].shadowTile >= 0) if (pointLights[i].shadowTile >= 0)
lit *= LocalShadow(pointLights[i].shadowTile + PointShadowFace(-toLight), FragPos, N); lit *= LocalShadow(pointLights[i].shadowTile + PointShadowFace(-toLight), FragPos, N);
direct += lit; direct += lit;
@@ -259,19 +220,14 @@ void main()
continue; continue;
vec3 radiance = spotLights[i].color * spotLights[i].intensity * PointAttenuation(distanceSquared, radius) * cone; vec3 radiance = spotLights[i].color * spotLights[i].intensity * PointAttenuation(distanceSquared, radius) * cone;
vec3 lit = ShadeLight(L, radiance, N, V, albedo, metal, rough, F0, NdotV); vec3 lit = ShadeLight(L, radiance, N, V, diffuseBRDF, specularBRDF, power);
if (spotLights[i].shadowTile >= 0) if (spotLights[i].shadowTile >= 0)
lit *= LocalShadow(spotLights[i].shadowTile, FragPos, N); lit *= LocalShadow(spotLights[i].shadowTile, FragPos, N);
direct += lit; direct += lit;
} }
vec3 ambientF = FresnelSchlickRoughness(NdotV, F0, rough); // Ambient is the sky's blurred colour in the direction the surface faces
vec3 ambientKD = (vec3(1.0) - ambientF) * (1.0 - metal); vec3 ambient = ToLinear(textureLod(environmentMap, N, 7.0).rgb) * envIntensity * albedo * ao;
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;
FragColor = vec4(direct + ambient + albedo * emissive * 4.0, albedoSample.a); FragColor = vec4(direct + ambient + albedo * emissive * 4.0, albedoSample.a);
} }
+2 -1
View File
@@ -29,7 +29,8 @@ vec2 SurfaceUV()
{ {
if (uvMode == 0) return aTex + uvOffset; if (uvMode == 0) return aTex + uvOffset;
vec3 n = abs(aNormal); // Unscale the normal first, so a stretched mesh picks the axis it faces in the world
vec3 n = abs(aNormal / max(tileScale, vec3(1e-4)));
vec3 p = aPos * tileScale; vec3 p = aPos * tileScale;
if (n.y >= n.x && n.y >= n.z) return p.xz + uvOffset; if (n.y >= n.x && n.y >= n.z) return p.xz + uvOffset;
if (n.x >= n.z) return p.zy + uvOffset; if (n.x >= n.z) return p.zy + uvOffset;
+22 -66
View File
@@ -58,65 +58,23 @@ uniform float envIntensity = 0.8;
const float PI = 3.14159265359; const float PI = 3.14159265359;
float DistributionGGX(vec3 N, vec3 H, float rough) // Plain Blinn-Phong: ambient, Lambert diffuse and a half vector highlight.
// Roughness sets how tight and strong the highlight is, metallic tints it and takes away the diffuse.
// Close enough to the 2.2 curve and much cheaper than pow
vec3 ToLinear(vec3 c)
{ {
float a = rough * rough; return c * c;
float a2 = a * a;
float NdotH = max(dot(N, H), 0.0);
float NdotH2 = NdotH * NdotH;
float denom = NdotH2 * (a2 - 1.0) + 1.0;
return a2 / max(PI * denom * denom, 1e-5);
} }
float GeometrySchlickGGX(float NdotV, float rough) // Light from one direction, L points from the surface toward the light.
{ // diffuse and specular are already scaled for this pixel, see main.
float r = rough + 1.0; vec3 ShadeLight(vec3 L, vec3 radiance, vec3 N, vec3 V, vec3 diffuse, vec3 specular, float power)
float k = (r * r) / 8.0;
return NdotV / (NdotV * (1.0 - k) + k);
}
float GeometrySmith(vec3 N, vec3 V, vec3 L, float rough)
{
float NdotV = max(dot(N, V), 0.0);
float NdotL = max(dot(N, L), 0.0);
return GeometrySchlickGGX(NdotV, rough) * GeometrySchlickGGX(NdotL, rough);
}
vec3 FresnelSchlick(float cosTheta, vec3 F0)
{
return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}
vec3 FresnelSchlickRoughness(float cosTheta, vec3 F0, float rough)
{
vec3 f = max(vec3(1.0 - rough), F0);
return F0 + (f - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}
vec3 EnvironmentBRDF(vec3 F0, float rough, float NdotV)
{
const vec4 c0 = vec4(-1.0, -0.0275, -0.572, 0.022);
const vec4 c1 = vec4(1.0, 0.0425, 1.04, -0.04);
vec4 r = rough * c0 + c1;
float a004 = min(r.x * r.x, exp2(-9.28 * NdotV)) * r.x + r.y;
vec2 AB = vec2(-1.04, 1.04) * a004 + r.zw;
return F0 * AB.x + AB.y;
}
// Light from one direction, L points from the surface toward the light
vec3 ShadeLight(vec3 L, vec3 radiance, vec3 N, vec3 V, vec3 albedo, float metal, float rough, vec3 F0, float NdotV)
{ {
float NdotL = max(dot(N, L), 0.0); float NdotL = max(dot(N, L), 0.0);
if (NdotL <= 0.0)
return vec3(0.0);
vec3 H = normalize(V + L); vec3 H = normalize(V + L);
vec3 F = FresnelSchlick(max(dot(H, V), 0.0), F0); float highlight = pow(max(dot(N, H), 0.0), power);
float NDF = DistributionGGX(N, H, rough); return (diffuse + specular * highlight) * radiance * NdotL;
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 // Inverse square falloff, windowed so it reaches exactly zero at the radius
@@ -203,22 +161,25 @@ void main()
vec3 N = normalize(normal); vec3 N = normalize(normal);
vec3 V = normalize(viewPos - FragPos); vec3 V = normalize(viewPos - FragPos);
float NdotV = max(dot(N, V), 1e-4);
vec3 F0 = mix(vec3(0.04), albedo, metal); float smoothness = 1.0 - clamp(rough, 0.0, 1.0);
float power = exp2(10.0 * smoothness + 1.0); // 2 when fully rough, 2048 when mirror smooth
vec3 specularColor = mix(vec3(0.5 * smoothness * smoothness), albedo * (0.25 + 0.75 * smoothness), metal);
vec3 diffuseBRDF = albedo * (1.0 - metal) / PI; // the light intensities were tuned with the 1/PI in
vec3 specularBRDF = specularColor / PI;
vec3 direct = vec3(0.0); vec3 direct = vec3(0.0);
// Facing away from the sun has no direct light, so skip the BRDF and the 9 shadow samples // Facing away from the sun has no direct light, so skip the BRDF and the 9 shadow samples
vec3 sunL = normalize(-dirLight.direction); vec3 sunL = normalize(-dirLight.direction);
if (dot(N, sunL) > 0.0) { if (dot(N, sunL) > 0.0) {
float shadow = ShadowCalculation(lightSpaceMatrix * vec4(FragPos, 1.0), N, sunL); 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; direct += ShadeLight(sunL, dirLight.color * dirLight.intensity, N, V, diffuseBRDF, specularBRDF, power) * shadow;
} }
for (int i = 0; i < directionalLightCount; i++) { for (int i = 0; i < directionalLightCount; i++) {
vec3 L = normalize(-directionalLights[i].direction); vec3 L = normalize(-directionalLights[i].direction);
vec3 radiance = directionalLights[i].color * directionalLights[i].intensity; vec3 radiance = directionalLights[i].color * directionalLights[i].intensity;
direct += ShadeLight(L, radiance, N, V, albedo, metal, rough, F0, NdotV); direct += ShadeLight(L, radiance, N, V, diffuseBRDF, specularBRDF, power);
} }
for (int i = 0; i < pointLightCount; i++) { for (int i = 0; i < pointLightCount; i++) {
@@ -233,7 +194,7 @@ void main()
if (dot(N, L) <= 0.0) if (dot(N, L) <= 0.0)
continue; continue;
vec3 lit = ShadeLight(L, radiance, N, V, albedo, metal, rough, F0, NdotV); vec3 lit = ShadeLight(L, radiance, N, V, diffuseBRDF, specularBRDF, power);
if (pointLights[i].shadowTile >= 0) if (pointLights[i].shadowTile >= 0)
lit *= LocalShadow(pointLights[i].shadowTile + PointShadowFace(-toLight), FragPos, N); lit *= LocalShadow(pointLights[i].shadowTile + PointShadowFace(-toLight), FragPos, N);
direct += lit; direct += lit;
@@ -255,19 +216,14 @@ void main()
continue; continue;
vec3 radiance = spotLights[i].color * spotLights[i].intensity * PointAttenuation(distanceSquared, radius) * cone; vec3 radiance = spotLights[i].color * spotLights[i].intensity * PointAttenuation(distanceSquared, radius) * cone;
vec3 lit = ShadeLight(L, radiance, N, V, albedo, metal, rough, F0, NdotV); vec3 lit = ShadeLight(L, radiance, N, V, diffuseBRDF, specularBRDF, power);
if (spotLights[i].shadowTile >= 0) if (spotLights[i].shadowTile >= 0)
lit *= LocalShadow(spotLights[i].shadowTile, FragPos, N); lit *= LocalShadow(spotLights[i].shadowTile, FragPos, N);
direct += lit; direct += lit;
} }
vec3 ambientF = FresnelSchlickRoughness(NdotV, F0, rough); // Ambient is the sky's blurred colour in the direction the surface faces
vec3 ambientKD = (vec3(1.0) - ambientF) * (1.0 - metal); vec3 ambient = ToLinear(textureLod(environmentMap, N, 7.0).rgb) * envIntensity * albedo * ao;
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; vec3 emission = albedo * material.a * 4.0;
+1 -1
View File
@@ -23,7 +23,7 @@ void main()
albedo = texture(tex, TexCoord); albedo = texture(tex, TexCoord);
if (albedo.a < 0.1) if (albedo.a < 0.1)
discard; discard;
albedo.rgb = pow(albedo.rgb, vec3(2.2)); albedo.rgb = albedo.rgb * albedo.rgb; // same cheap 2.2 stand-in as the lighting shaders
} }
albedo.rgb = mix(albedo.rgb, colorOverride.rgb, colorOverride.w); albedo.rgb = mix(albedo.rgb, colorOverride.rgb, colorOverride.w);
+2 -1
View File
@@ -27,7 +27,8 @@ vec2 SurfaceUV()
{ {
if (uvMode == 0) return aTexCoord + uvOffset; if (uvMode == 0) return aTexCoord + uvOffset;
vec3 n = abs(aNormal); // Unscale the normal first, so a stretched mesh picks the axis it faces in the world
vec3 n = abs(aNormal / max(tileScale, vec3(1e-4)));
vec3 p = aPos * tileScale; vec3 p = aPos * tileScale;
if (n.y >= n.x && n.y >= n.z) return p.xz + uvOffset; if (n.y >= n.x && n.y >= n.z) return p.xz + uvOffset;
if (n.x >= n.z) return p.zy + uvOffset; if (n.x >= n.z) return p.zy + uvOffset;
+38 -1
View File
@@ -80,65 +80,81 @@ public:
void LookAt(glm::vec3 eye, glm::vec3 target) noexcept { void LookAt(glm::vec3 eye, glm::vec3 target) noexcept {
TranslateTo(eye); TranslateTo(eye);
orient = -orient * target; orient = -orient * target;
version++;
} }
void TranslateBy(glm::vec3 e) noexcept { void TranslateBy(glm::vec3 e) noexcept {
pos += e; pos += e;
version++;
} }
void TranslateTo(glm::vec3 e) noexcept { void TranslateTo(glm::vec3 e) noexcept {
pos = e; pos = e;
version++;
} }
void ScaleBy(glm::vec3 e) noexcept { void ScaleBy(glm::vec3 e) noexcept {
scale *= e; scale *= e;
version++;
} }
void ScaleBy(GLfloat e) noexcept { void ScaleBy(GLfloat e) noexcept {
scale *= e; scale *= e;
version++;
} }
void ScaleTo(glm::vec3 e) noexcept { void ScaleTo(glm::vec3 e) noexcept {
scale = e; scale = e;
version++;
} }
void ScaleTo(GLfloat e) noexcept { void ScaleTo(GLfloat e) noexcept {
scale = glm::vec3(e); scale = glm::vec3(e);
version++;
} }
void ScaleToX(GLfloat e) noexcept { void ScaleToX(GLfloat e) noexcept {
scale.x = e; scale.x = e;
version++;
} }
void ScaleToY(GLfloat e) noexcept { void ScaleToY(GLfloat e) noexcept {
scale.y = e; scale.y = e;
version++;
} }
void ScaleToZ(GLfloat e) noexcept { void ScaleToZ(GLfloat e) noexcept {
scale.z = e; scale.z = e;
version++;
} }
void RotateToQuaternion(glm::quat q) noexcept { void RotateToQuaternion(glm::quat q) noexcept {
orient = q; orient = q;
version++;
} }
void RotateByQuaternion(glm::quat q) noexcept { void RotateByQuaternion(glm::quat q) noexcept {
orient = q * orient; orient = q * orient;
version++;
} }
void NormalizeRotation() noexcept { void NormalizeRotation() noexcept {
orient = glm::normalize(orient); orient = glm::normalize(orient);
version++;
} }
void RotateByQuaternionCumulate(glm::quat q) noexcept { void RotateByQuaternionCumulate(glm::quat q) noexcept {
orient = orient * q; orient = orient * q;
version++;
} }
void RotateByEulerAnglesCumulate(glm::vec3 e) noexcept { void RotateByEulerAnglesCumulate(glm::vec3 e) noexcept {
orient = orient * glm::quat(e); orient = orient * glm::quat(e);
version++;
} }
void RotateToEulerAngles(glm::vec3 e) noexcept { void RotateToEulerAngles(glm::vec3 e) noexcept {
orient = glm::quat(e); orient = glm::quat(e);
version++;
} }
void RotateByEulerAngles(glm::vec3 e) noexcept { void RotateByEulerAngles(glm::vec3 e) noexcept {
RotateByQuaternion(glm::quat(e)); RotateByQuaternion(glm::quat(e));
@@ -185,6 +201,12 @@ public:
target.scale = scale; target.scale = scale;
target.orient = orient; target.orient = orient;
target.pos = pos; target.pos = pos;
target.version++;
}
// Goes up every time it moves, turns or is resized, so other code can tell when something it worked out is stale
unsigned int GetVersion() const noexcept {
return version;
} }
glm::mat4 GetScaleMatrix() const noexcept { glm::mat4 GetScaleMatrix() const noexcept {
@@ -199,8 +221,19 @@ public:
return glm::mat4_cast(orient); return glm::mat4_cast(orient);
} }
// Kept until the transform changes. Physics asks for the same ones many times a step.
glm::mat4 GetMatrix() const noexcept { glm::mat4 GetMatrix() const noexcept {
return GetTranslationMatrix() * GetRotationMatrix() * GetScaleMatrix(); if (cachedMatrixVersion != version) {
// The rotation with each axis scaled and the translation dropped in, rather than three full matrices multiplied
glm::mat4 matrix = glm::mat4_cast(orient);
matrix[0] *= scale.x;
matrix[1] *= scale.y;
matrix[2] *= scale.z;
matrix[3] = glm::vec4(pos, 1.0f);
cachedMatrix = matrix;
cachedMatrixVersion = version;
}
return cachedMatrix;
} }
glm::mat4 GetMatrixRotationless() const noexcept { glm::mat4 GetMatrixRotationless() const noexcept {
@@ -221,6 +254,10 @@ private:
glm::vec3 pos; glm::vec3 pos;
glm::vec3 scale; glm::vec3 scale;
glm::quat orient; glm::quat orient;
unsigned int version = 1;
mutable unsigned int cachedMatrixVersion = 0;
mutable glm::mat4 cachedMatrix = glm::mat4(1.0f);
}; };
class t_package { class t_package {
+2
View File
@@ -11,6 +11,8 @@
#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 "Structures.h" #include "Structures.h"
#include "common.h"
#include "Window.h" // Recalculate reads the window's size
class t2d_package { class t2d_package {
+110 -88
View File
@@ -349,108 +349,130 @@ bool TInTNoInfo(t& t1, t& t2) {
//FINAL //FINAL
return true; return true;
} }
std::optional<TInTInfo> TInT(t& t1, t& t2) { // Separating axis test between two boxes: the 3 face directions of each, and the 9 crossings of those.
std::vector<glm::vec3> worldvertices1 = { // The direction they overlap least along is the way to push them apart, and that overlap is how deep they are.
// Each box's reach along a direction comes straight from its half sizes, no corners needed.
std::optional<TInTInfo> TInT(const t& t1, const t& t2, bool withPoint) {
const glm::vec3 axes1[3] = { t1.GetRightVector(), t1.GetUpVector(), t1.GetFrontVector() };
const glm::vec3 axes2[3] = { t2.GetRightVector(), t2.GetUpVector(), t2.GetFrontVector() };
const glm::vec3 half1 = t1.GetScale() * 0.5f;
const glm::vec3 half2 = t2.GetScale() * 0.5f;
const glm::vec3 center1 = t1.GetTranslation();
const glm::vec3 center2 = t2.GetTranslation();
glm::vec3 axes[15];
for (int i = 0; i < 3; i++) {
axes[i] = axes1[i];
axes[3 + i] = axes2[i];
for (int j = 0; j < 3; j++)
axes[6 + i * 3 + j] = glm::cross(axes1[i], axes2[j]);
}
float lowestoverlap = FLT_MAX;
glm::vec3 bestAxis(0.0f);
bool bestInfront = false;
for (int i = 0; i < 15; i++) {
glm::vec3 axis = axes[i];
if (i >= 6) {
const float len = glm::length(axis);
if (len < 1e-6f) continue; // parallel edges, already covered by the face directions
axis /= len;
}
const float c1 = glm::dot(center1, axis);
const float c2 = glm::dot(center2, axis);
const float r1 = glm::abs(glm::dot(axes1[0], axis)) * half1.x + glm::abs(glm::dot(axes1[1], axis)) * half1.y
+ glm::abs(glm::dot(axes1[2], axis)) * half1.z;
const float r2 = glm::abs(glm::dot(axes2[0], axis)) * half2.x + glm::abs(glm::dot(axes2[1], axis)) * half2.y
+ glm::abs(glm::dot(axes2[2], axis)) * half2.z;
const float min1 = c1 - r1, max1 = c1 + r1;
const float min2 = c2 - r2, max2 = c2 + r2;
if (!(min1 < max2 && min2 < max1))
return {};
const float overlap = glm::min(max1, max2) - glm::max(min1, min2);
if (overlap < lowestoverlap) {
lowestoverlap = overlap;
bestAxis = axis;
bestInfront = c1 > c2;
}
}
TInTInfo tr;
tr.overlap = lowestoverlap;
tr.CN = bestAxis * (bestInfront ? 1.0f : -1.0f); // from t2 toward t1
tr.POI = withPoint ? BoxContactPoint(t1, t2) : glm::vec3(0.0f);
return tr;
}
// Where two overlapping boxes touch: the middle of whichever corners are inside the other box,
// or with none, the closest point between their edges
glm::vec3 BoxContactPoint(const t& t1, const t& t2) {
static const glm::vec3 corners[8] = {
{0.5f,0.5f,0.5f},{0.5f,-0.5f,0.5f},{0.5f,0.5f,-0.5f},{0.5f,-0.5f,-0.5f}, {0.5f,0.5f,0.5f},{0.5f,-0.5f,0.5f},{0.5f,0.5f,-0.5f},{0.5f,-0.5f,-0.5f},
{-0.5f,0.5f,0.5f},{-0.5f,-0.5f,0.5f},{-0.5f,0.5f,-0.5f},{-0.5f,-0.5f,-0.5f}, {-0.5f,0.5f,0.5f},{-0.5f,-0.5f,0.5f},{-0.5f,0.5f,-0.5f},{-0.5f,-0.5f,-0.5f},
}; };
std::vector<glm::vec3> worldvertices2 = worldvertices1; const glm::mat4 t1mat = t1.GetMatrix(), t2mat = t2.GetMatrix();
glm::mat4 t1mat = t1.GetMatrix(), t2mat = t2.GetMatrix(); const glm::mat4 t1inv = glm::inverse(t1mat), t2inv = glm::inverse(t2mat);
for (glm::vec3& v : worldvertices1) v = glm::vec3(t1mat * glm::vec4(v, 1.0f));
for (glm::vec3& v : worldvertices2) v = glm::vec3(t2mat * glm::vec4(v, 1.0f));
glm::vec3 fv1=t1.GetFrontVector(), rv1=t1.GetRightVector(), uv1=t1.GetUpVector(); glm::vec3 worldvertices1[8], worldvertices2[8];
glm::vec3 fv2=t2.GetFrontVector(), rv2=t2.GetRightVector(), uv2=t2.GetUpVector(); for (int i = 0; i < 8; i++) {
worldvertices1[i] = glm::vec3(t1mat * glm::vec4(corners[i], 1.0f));
worldvertices2[i] = glm::vec3(t2mat * glm::vec4(corners[i], 1.0f));
}
std::vector<glm::vec3> allAxes = { glm::vec3 summed(0.0f);
fv1, uv1, rv1, fv2, uv2, rv2, int inside = 0;
glm::cross(fv1,fv2), glm::cross(fv1,rv2), glm::cross(fv1,uv2), for (int i = 0; i < 8; i++) {
glm::cross(rv1,fv2), glm::cross(rv1,rv2), glm::cross(rv1,uv2), if (glm::all(glm::lessThanEqual(glm::abs(glm::vec3(t1inv * glm::vec4(worldvertices2[i], 1.0f))), glm::vec3(0.5f + 1e-4f)))) {
glm::cross(uv1,fv2), glm::cross(uv1,rv2), glm::cross(uv1,uv2) summed += worldvertices2[i];
inside++;
}
if (glm::all(glm::lessThanEqual(glm::abs(glm::vec3(t2inv * glm::vec4(worldvertices1[i], 1.0f))), glm::vec3(0.5f + 1e-4f)))) {
summed += worldvertices1[i];
inside++;
}
}
if (inside > 0)
return summed / (float)inside;
static const int edgeIdx[][2] = {
{0,1},{0,2},{1,3},{2,3},
{4,5},{4,6},{5,7},{6,7},
{0,4},{1,5},{2,6},{3,7}
}; };
TInTInfo tr; float bestDist = FLT_MAX;
float lowestoverlap = FLT_MAX; glm::vec3 bestPt(0.0f);
bool bestInfront = false;
int bestIndex = 0;
for (int i = 0; i < (int)allAxes.size(); i++) { for (auto& e1 : edgeIdx) {
glm::vec3 axis = allAxes[i]; glm::vec3 p1 = worldvertices1[e1[0]], p2 = worldvertices1[e1[1]];
if (i >= 6) { for (auto& e2 : edgeIdx) {
float len = glm::length(axis); glm::vec3 p3 = worldvertices2[e2[0]], p4 = worldvertices2[e2[1]];
if (len < 1e-6f) continue;
axis = axis / len;
allAxes[i] = axis;
}
std::optional<AxisCollisionCN> b = TAxisCollidesTCN(worldvertices1, worldvertices2, axis);
if (!b.has_value()) return {};
if (b->overlap < lowestoverlap) {
lowestoverlap = b->overlap;
bestInfront = b->infront;
bestIndex = i;
}
}
tr.overlap = lowestoverlap; glm::vec3 d1=p2-p1, d2=p4-p3, r=p1-p3;
tr.CN = allAxes[bestIndex] * (bestInfront ? 1.0f : -1.0f); float a=glm::dot(d1,d1), e=glm::dot(d2,d2), f=glm::dot(d2,r);
float s, t;
// POI if (a < 1e-10f && e < 1e-10f) { s=0.0f; t=0.0f; }
glm::mat4 t1inv = glm::inverse(t1mat), t2inv = glm::inverse(t2mat); else if (a < 1e-10f) { s=0.0f; t=glm::clamp(f/e,0.0f,1.0f); }
std::vector<glm::vec3> contactPts; else {
for (const glm::vec3& v : worldvertices2) { float c=glm::dot(d1,r), b=glm::dot(d1,d2), denom=a*e-b*b;
glm::vec3 local = glm::vec3(t1inv * glm::vec4(v, 1.0f)); if (e < 1e-10f) { t=0.0f; s=glm::clamp(-c/a,0.0f,1.0f); }
if (glm::all(glm::lessThanEqual(glm::abs(local), glm::vec3(0.5f + 1e-4f))))
contactPts.push_back(v);
}
for (const glm::vec3& v : worldvertices1) {
glm::vec3 local = glm::vec3(t2inv * glm::vec4(v, 1.0f));
if (glm::all(glm::lessThanEqual(glm::abs(local), glm::vec3(0.5f + 1e-4f))))
contactPts.push_back(v);
}
if (contactPts.empty()) {
static const int edgeIdx[][2] = {
{0,1},{0,2},{1,3},{2,3},
{4,5},{4,6},{5,7},{6,7},
{0,4},{1,5},{2,6},{3,7}
};
float bestDist = FLT_MAX;
glm::vec3 bestPt(0.0f);
for (auto& e1 : edgeIdx) {
glm::vec3 p1 = worldvertices1[e1[0]], p2 = worldvertices1[e1[1]];
for (auto& e2 : edgeIdx) {
glm::vec3 p3 = worldvertices2[e2[0]], p4 = worldvertices2[e2[1]];
glm::vec3 d1=p2-p1, d2=p4-p3, r=p1-p3;
float a=glm::dot(d1,d1), e=glm::dot(d2,d2), f=glm::dot(d2,r);
float s, t;
if (a < 1e-10f && e < 1e-10f) { s=0.0f; t=0.0f; }
else if (a < 1e-10f) { s=0.0f; t=glm::clamp(f/e,0.0f,1.0f); }
else { else {
float c=glm::dot(d1,r), b=glm::dot(d1,d2), denom=a*e-b*b; s = denom > 1e-10f ? glm::clamp((b*f-c*e)/denom,0.0f,1.0f) : 0.0f;
if (e < 1e-10f) { t=0.0f; s=glm::clamp(-c/a,0.0f,1.0f); } t = (b*s+f)/e;
else { if (t < 0.0f) { t=0.0f; s=glm::clamp(-c/a,0.0f,1.0f); }
s = denom > 1e-10f ? glm::clamp((b*f-c*e)/denom,0.0f,1.0f) : 0.0f; else if (t > 1.0f) { t=1.0f; s=glm::clamp((b-c)/a,0.0f,1.0f); }
t = (b*s+f)/e;
if (t < 0.0f) { t=0.0f; s=glm::clamp(-c/a,0.0f,1.0f); }
else if (t > 1.0f) { t=1.0f; s=glm::clamp((b-c)/a,0.0f,1.0f); }
}
} }
glm::vec3 c1=p1+s*d1, c2=p3+t*d2;
float dist = glm::length(c1-c2);
if (dist < bestDist) { bestDist=dist; bestPt=(c1+c2)*0.5f; }
} }
glm::vec3 c1=p1+s*d1, c2=p3+t*d2;
float dist = glm::dot(c1-c2, c1-c2);
if (dist < bestDist) { bestDist=dist; bestPt=(c1+c2)*0.5f; }
} }
tr.POI = bestPt;
} else {
tr.POI = glm::vec3(0.0f);
for (const glm::vec3& p : contactPts) tr.POI += p;
tr.POI /= (float)contactPts.size();
} }
return bestPt;
return tr;
} }
glm::quat AngleAxis(glm::vec3 axis, float angle) { glm::quat AngleAxis(glm::vec3 axis, float angle) {
float halfangle = angle * 0.5f; float halfangle = angle * 0.5f;
+3 -1
View File
@@ -51,7 +51,9 @@ std::optional<std::pair<float, glm::vec3>> TAxisCollidesT(std::vector < glm::vec
std::optional<AxisCollisionCN> TAxisCollidesTCN(std::vector < glm::vec3 >& t1, std::vector < glm::vec3 >& t2, glm::vec3 axis); std::optional<AxisCollisionCN> TAxisCollidesTCN(std::vector < glm::vec3 >& t1, std::vector < glm::vec3 >& t2, glm::vec3 axis);
bool TNearT(t& t1, t& t22); bool TNearT(t& t1, t& t22);
std::optional<TInTInfo> TInT(t& t1, t& t2); // withPoint false leaves POI at zero, for callers that work out their own contact points
std::optional<TInTInfo> TInT(const t& t1, const t& t2, bool withPoint = true);
glm::vec3 BoxContactPoint(const t& t1, const t& t2);
bool TInTNoInfo(t& t1, t& t2); bool TInTNoInfo(t& t1, t& t2);
glm::quat AngleAxis(glm::vec3 axis, float angle); glm::quat AngleAxis(glm::vec3 axis, float angle);