base
This commit is contained in:
@@ -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
@@ -4,6 +4,7 @@
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
@@ -98,8 +99,9 @@ private:
|
||||
|
||||
// ============================================================================
|
||||
// Animator
|
||||
// blendWeight ramps from 0?1 over blendDuration when PlayAnimation is called.
|
||||
// CharacterBody::GetBlendedBoneMatrices() lerps all animators by blendWeight.
|
||||
// blendWeight ramps from 0?1 over blendDuration when PlayAnimation is called,
|
||||
// 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
|
||||
{
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
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:
|
||||
std::vector<glm::mat4> m_FinalBoneMatrices;
|
||||
Animation* m_CurrentAnimation = nullptr;
|
||||
@@ -186,6 +198,7 @@ public:
|
||||
|
||||
private:
|
||||
std::map<std::string, BoneInfo> m_BoneInfoMap;
|
||||
std::unordered_map<std::string, glm::mat4> m_LocalTransforms;
|
||||
float _blendTimer = 0.0f;
|
||||
|
||||
void RebuildBoneInfoMap()
|
||||
@@ -205,6 +218,7 @@ private:
|
||||
if (bone) {
|
||||
bone->Update(m_CurrentTime);
|
||||
nodeTransform = bone->GetLocalTransform();
|
||||
m_LocalTransforms[nodeName] = nodeTransform; // the Animation is shared, so keep this animator's own copy
|
||||
}
|
||||
|
||||
glm::mat4 globalTransform = parentTransform * nodeTransform;
|
||||
|
||||
@@ -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
@@ -1,111 +1,236 @@
|
||||
#include "Character.h"
|
||||
#include "Animator.h"
|
||||
#include "CharacterMind.h"
|
||||
#include "Ragdoll.h"
|
||||
#include "Physics.h"
|
||||
#include "tFunctions.h"
|
||||
#include <cctype>
|
||||
#include <cfloat>
|
||||
#include <cmath>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
// -------------------------------------------------------
|
||||
// BodyComponent
|
||||
// CharacterBody
|
||||
// -------------------------------------------------------
|
||||
|
||||
BodyComponent* CharacterBody::GetComponentOfName(std::string name) {
|
||||
for (BodyComponent* C : components) {
|
||||
if (C->name == name)
|
||||
return C;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
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;
|
||||
void CharacterBody::Perform(const CharacterAction& action) {
|
||||
switch (action.type) {
|
||||
case CharacterActionType::Move: {
|
||||
wishDirection = glm::vec3(action.direction.x, 0.0f, action.direction.z);
|
||||
if (glm::length(wishDirection) > 0.0001f) {
|
||||
wishDirection = glm::normalize(wishDirection);
|
||||
turnDirection = wishDirection;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// CharacterMind
|
||||
// -------------------------------------------------------
|
||||
void CharacterBody::ApplyMovement(bool grounded, float speedFactor, float dt, float referenceDt) {
|
||||
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() {
|
||||
// TODO: implement
|
||||
return nullptr;
|
||||
// With time stopped there's nothing to apply, and the rescaling below would divide zero by zero
|
||||
if (!physics || dt <= 0.0f)
|
||||
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
|
||||
// -------------------------------------------------------
|
||||
|
||||
void Character::PressInput(char letter) {
|
||||
letter = (char)std::toupper((unsigned char)letter);
|
||||
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)
|
||||
void Character::Think() {
|
||||
if (!mind)
|
||||
return;
|
||||
|
||||
const glm::quat target = LookAt(-turnDirection);
|
||||
const glm::quat current = body->t.GetRotationQuaternion();
|
||||
const float blend = 1.0f - std::exp(-turnSpeed * dt);
|
||||
body->t.RotateToQuaternion(glm::normalize(glm::slerp(current, target, blend)));
|
||||
pendingActions.clear();
|
||||
mind->ProduceActions(*this, pendingActions);
|
||||
for (const CharacterAction& action : pendingActions)
|
||||
body->Perform(action);
|
||||
}
|
||||
|
||||
void Character::SetWalking(bool walking) {
|
||||
if (body && body->animator)
|
||||
body->animator->playbackSpeed = walking ? 1.0f : 0.0f;
|
||||
int Character::GetCollisionGroup() {
|
||||
// Kept clear of the ragdoll's groups, which count up from 1
|
||||
static int nextGroup = 1000;
|
||||
if (collisionGroup == 0)
|
||||
collisionGroup = nextGroup++;
|
||||
return collisionGroup;
|
||||
}
|
||||
|
||||
glm::vec3 Character::GetForwardDirection() const {
|
||||
if (!body)
|
||||
return glm::vec3(0.0f, 0.0f, -1.0f);
|
||||
|
||||
glm::vec3 forward = -body->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;
|
||||
void Character::CreatePhysicsBody(Physics& physics, float massKg) {
|
||||
if (body->physics)
|
||||
return;
|
||||
p* physicsBody = new p(body);
|
||||
physicsBody->mass = massKg;
|
||||
physicsBody->lockRotation = true; // it turns by facing where it walks, never by tipping over
|
||||
physicsBody->friction = 0.0f; // walking does its own stopping
|
||||
physicsBody->collisionGroup = GetCollisionGroup();
|
||||
physics.AddObject(physicsBody);
|
||||
body->physics = physicsBody;
|
||||
}
|
||||
|
||||
void Character::EnableRagdoll(Physics& physics) {
|
||||
|
||||
+81
-80
@@ -2,49 +2,37 @@
|
||||
#define CHARACTER_CLASS
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <algorithm>
|
||||
#include <set>
|
||||
#include "t.h"
|
||||
#include "Rig.h"
|
||||
#include "Object.h"
|
||||
#include "Animator.h"
|
||||
#include "CharacterAction.h"
|
||||
|
||||
class Character;
|
||||
class CharacterManager;
|
||||
class CharacterBody;
|
||||
class BodyComponent;
|
||||
class CharacterMind;
|
||||
class Ragdoll;
|
||||
class Physics;
|
||||
class p;
|
||||
|
||||
class Action {
|
||||
public:
|
||||
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;
|
||||
};
|
||||
|
||||
// The physical side of a character: where it is, its physics body, its animation,
|
||||
// and carrying out whatever actions its mind sends it.
|
||||
class CharacterBody : public t_package {
|
||||
public:
|
||||
Rig* rig = nullptr;
|
||||
p* physics = nullptr; // made by Character::CreatePhysicsBody, its box is t's scale
|
||||
|
||||
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;
|
||||
BodyComponent* RootComponent = nullptr;
|
||||
float walkSpeed = 2.34f; // m/s on flat ground
|
||||
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() {
|
||||
animator = AddAnimator(0);
|
||||
@@ -61,31 +49,11 @@ public:
|
||||
return a;
|
||||
}
|
||||
|
||||
// Returns weighted blend of all playing animators.
|
||||
// Base (weight 0) is always the foundation; higher-weight animators
|
||||
// blend in on top based on their blendWeight (0?1).
|
||||
std::vector<glm::mat4> GetBlendedBoneMatrices() const {
|
||||
// Start from the base animator
|
||||
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;
|
||||
// The skinning matrices for this frame, from every animator layered together. Worked out in Step.
|
||||
// Base (weight 0) is the foundation; each higher-weight animator blends in by its blendWeight,
|
||||
// but only on the bones its animation has tracks for, so an arm-only clip leaves the legs walking.
|
||||
const std::vector<glm::mat4>& GetBlendedBoneMatrices() const {
|
||||
return blendedBones.empty() ? animator->GetFinalBoneMatrices() : blendedBones;
|
||||
}
|
||||
|
||||
void SetRig(Rig* r) {
|
||||
@@ -97,64 +65,97 @@ public:
|
||||
void SetAnimation(Animation* anim) {
|
||||
animator->SetAnimation(anim);
|
||||
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); }
|
||||
|
||||
void Step(float dt) {
|
||||
for (Animator* a : animators)
|
||||
a->Step(dt);
|
||||
// Plays an animation once on top of everything else. Starts over if it's already playing.
|
||||
void PlayAction(Animation* anim) {
|
||||
if (!anim)
|
||||
return;
|
||||
if (!actionAnimator) {
|
||||
actionAnimator = AddAnimator(2);
|
||||
actionAnimator->blendDuration = 0.1f;
|
||||
}
|
||||
actionAnimator->loops = false;
|
||||
actionAnimator->SetAnimation(anim);
|
||||
actionAnimator->PlayAnimation();
|
||||
}
|
||||
|
||||
BodyComponent* GetComponentOfName(std::string name);
|
||||
void ExecuteAction(InputAction* action);
|
||||
};
|
||||
// The loop played while walking. It keeps running underneath so it picks up mid-stride.
|
||||
void SetWalkAnimation(Animation* anim) {
|
||||
if (!walkAnimator)
|
||||
walkAnimator = AddAnimator(1);
|
||||
walkAnimator->loops = true;
|
||||
walkAnimator->SetAnimation(anim);
|
||||
walkAnimator->PlayAnimation();
|
||||
walkAnimator->blendWeight = walkBlend;
|
||||
}
|
||||
|
||||
class CharacterMind {
|
||||
public:
|
||||
InputAction* ProduceAction();
|
||||
// Carries out one action. Movement actions only count for the frame they arrive in.
|
||||
void Perform(const CharacterAction& action);
|
||||
|
||||
// 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 {
|
||||
public:
|
||||
CharacterMind* mind = nullptr;
|
||||
CharacterMind* mind = nullptr; // what decides its actions, nullptr does nothing
|
||||
CharacterBody* body = nullptr;
|
||||
Ragdoll* ragdoll = nullptr; // when set, physics drives the rig instead of the animators
|
||||
SurfaceTexture surface;
|
||||
glm::vec4 colorOverride = glm::vec4(0.0f); // rgb + strength (0 = keep the rig's own texture)
|
||||
float visualScale = 1.4f; // rig is ~1.8 tall natively; hitbox is 2.5
|
||||
float visualScale = 1.4f; // rig is ~1.8 tall natively; its physics box is 2.5
|
||||
float metallic = 0.0f;
|
||||
float roughness = 0.7f;
|
||||
float ao = 1.0f;
|
||||
float turnSpeed = 12.0f;
|
||||
|
||||
void PressInput(char letter);
|
||||
void ReleaseInput(char letter);
|
||||
void ClearInput();
|
||||
glm::vec3 GetMovementDirection(const glm::vec3& forward, const glm::vec3& right) const;
|
||||
void SetTurnDirection(const glm::vec3& direction);
|
||||
const glm::vec3& GetTurnDirection() const { return turnDirection; }
|
||||
void SetWalking(bool walking);
|
||||
glm::vec3 GetForwardDirection() const;
|
||||
// Asks the mind for this frame's actions and hands them to the body
|
||||
void Think();
|
||||
|
||||
glm::vec3 GetForwardDirection() const { return body->GetForwardDirection(); }
|
||||
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();
|
||||
void Render(Shader& shader) override;
|
||||
bool GetWorldBounds(BoundingBox& bounds) const override;
|
||||
|
||||
void Step(float dt) {
|
||||
if (ragdoll) return;
|
||||
TurnToDirection(dt);
|
||||
body->Step(dt);
|
||||
}
|
||||
|
||||
private:
|
||||
void TurnToDirection(float dt);
|
||||
glm::mat4 GetRigMatrix() const;
|
||||
|
||||
std::set<char> inputLetters;
|
||||
glm::vec3 turnDirection = glm::vec3(0.0f);
|
||||
int collisionGroup = 0;
|
||||
std::vector<CharacterAction> pendingActions;
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -64,23 +64,9 @@ Character* CharacterManager::GetCharacter(const std::string& name) const {
|
||||
return it != characters.end() ? it->second.get() : nullptr;
|
||||
}
|
||||
|
||||
void CharacterManager::SetControlledCharacter(const std::string& name) {
|
||||
controlledCharacter = GetCharacter(name);
|
||||
}
|
||||
|
||||
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::Think() {
|
||||
for (Character* character : characterOrder)
|
||||
character->Think();
|
||||
}
|
||||
|
||||
void CharacterManager::Step(float dt) {
|
||||
|
||||
+2
-6
@@ -26,11 +26,8 @@ public:
|
||||
Animation* GetAnimation(const std::string& name) const;
|
||||
Character* GetCharacter(const std::string& name) const;
|
||||
|
||||
void SetControlledCharacter(const std::string& name);
|
||||
void PressInput(char letter);
|
||||
void ReleaseInput(char letter);
|
||||
void ClearInput();
|
||||
|
||||
// Every character's mind decides its actions for this frame
|
||||
void Think();
|
||||
void Step(float dt);
|
||||
|
||||
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<Character>> characters;
|
||||
std::vector<Character*> characterOrder;
|
||||
Character* controlledCharacter = nullptr;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
@@ -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 ? "_" : "");
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
@@ -10,6 +10,7 @@
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
#include <algorithm>
|
||||
#include <cfloat>
|
||||
#include <cmath>
|
||||
|
||||
// ============================================================================
|
||||
@@ -68,7 +69,6 @@
|
||||
#include "Sound.h"
|
||||
#include "Gui.h"
|
||||
#include "font.h"
|
||||
#include "TexturePainter.h"
|
||||
|
||||
// Character / animation
|
||||
#include "Rig.h"
|
||||
@@ -76,8 +76,11 @@
|
||||
#include "Character.h"
|
||||
#include "CharacterManager.h"
|
||||
#include "Ragdoll.h"
|
||||
#include "DefaultBodyComponents.h"
|
||||
#include "BodyComponentActions.h"
|
||||
#include "Console.h"
|
||||
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include "CharacterMind.h"
|
||||
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#include "stb_image.h"
|
||||
@@ -93,6 +96,7 @@ Engine::Engine()
|
||||
glfwSetCursorPosCallback(window->handle, mouse_callback);
|
||||
glfwSetScrollCallback(window->handle, scroll_callback);
|
||||
glfwSetKeyCallback(window->handle, key_callback);
|
||||
glfwSetCharCallback(window->handle, char_callback);
|
||||
glfwSetMouseButtonCallback(window->handle, mouse_button_callback);
|
||||
glfwSwapInterval(1);
|
||||
|
||||
@@ -138,12 +142,14 @@ void Engine::Initiate()
|
||||
charactermanager = new CharacterManager();
|
||||
charactermanager->LoadRig("humanoid", "assets/rig.glb");
|
||||
charactermanager->LoadAnimation("walk", "assets/walkanim.glb", "humanoid");
|
||||
charactermanager->LoadAnimation("idle", "assets/idle.glb", "humanoid");
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Player character
|
||||
// ------------------------------------------------------------------------
|
||||
Character& testcharacter = *charactermanager->CreateCharacter("character", "humanoid", "walk");
|
||||
charactermanager->SetControlledCharacter("character");
|
||||
// Idle is the base pose, walking fades in on top of it while moving
|
||||
Character& testcharacter = *charactermanager->CreateCharacter("character", "humanoid", "idle");
|
||||
testcharacter.body->SetWalkAnimation(charactermanager->GetAnimation("walk"));
|
||||
testcharacter.castshadow = true;
|
||||
testcharacter.colorOverride = glm::vec4(0.62f, 0.64f, 0.68f, 1.0f);
|
||||
testcharacter.roughness = 0.65f;
|
||||
@@ -151,17 +157,13 @@ void Engine::Initiate()
|
||||
testcharacter.body->t.TranslateTo({ 0.0f, 1.5f, 0.0f });
|
||||
testcharacter.body->animator->loops = true;
|
||||
testcharacter.BindToRenderSystem();
|
||||
|
||||
p* characterBody = new p(testcharacter.body);
|
||||
characterBody->mass = 80.0f;
|
||||
characterBody->lockRotation = true;
|
||||
characterBody->friction = 0.0f;
|
||||
physicsengine->AddObject(characterBody);
|
||||
testcharacter.CreatePhysicsBody(*physicsengine, 80.0f);
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 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.colorOverride = glm::vec4(0.10f, 0.20f, 0.48f, 1.0f);
|
||||
npc.metallic = 0.1f;
|
||||
@@ -169,10 +171,96 @@ void Engine::Initiate()
|
||||
npc.body->t.ScaleBy({ 0.7f, 2.5f, 0.7f });
|
||||
npc.body->t.TranslateTo({ 3.0f, 1.25f, 3.0f });
|
||||
npc.BindToRenderSystem();
|
||||
npc.CreatePhysicsBody(*physicsengine, 80.0f);
|
||||
|
||||
testcharacter.SetWalking(false);
|
||||
npc.SetWalking(false);
|
||||
npc.EnableRagdoll(*physicsengine);
|
||||
testcharacter.body->SetWalking(false);
|
||||
npc.body->SetWalking(false);
|
||||
|
||||
// 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
|
||||
@@ -211,7 +299,7 @@ void Engine::Initiate()
|
||||
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 settingsReturn = MenuScreen::Main;
|
||||
bool menuActive = true;
|
||||
@@ -277,7 +365,7 @@ void Engine::Initiate()
|
||||
box->t2d.position = { 0.0f, 0.5f, x, y };
|
||||
box->t2d.center = { 0.0f, 0.5f };
|
||||
box->t2d.size = { 0.0f, 0.0f, width, height };
|
||||
box->rounding = 0.15f;
|
||||
box->rounding = 0.05f;
|
||||
box->BindToRenderSystem();
|
||||
menuItems.push_back(box);
|
||||
|
||||
@@ -301,11 +389,9 @@ void Engine::Initiate()
|
||||
const float mainButtonHeight = 60.0f;
|
||||
MenuButton playButton = MakeMenuButton("PLAY", menuMargin, -60.0f, menuContentWidth, mainButtonHeight, 0.75f,
|
||||
glm::vec3(0.82f, 0.45f, 0.10f), menuAccent, 0.95f);
|
||||
MenuButton drawerButton = MakeMenuButton("TEXTURE DRAWER", menuMargin, 12.0f, menuContentWidth, mainButtonHeight, 0.75f,
|
||||
MenuButton settingsButton = MakeMenuButton("SETTINGS", menuMargin, 12.0f, menuContentWidth, mainButtonHeight, 0.75f,
|
||||
menuButtonColor, menuButtonHover);
|
||||
MenuButton settingsButton = MakeMenuButton("SETTINGS", menuMargin, 84.0f, menuContentWidth, mainButtonHeight, 0.75f,
|
||||
menuButtonColor, menuButtonHover);
|
||||
MenuButton quitButton = MakeMenuButton("QUIT", menuMargin, 156.0f, menuContentWidth, mainButtonHeight, 0.75f,
|
||||
MenuButton quitButton = MakeMenuButton("QUIT", menuMargin, 84.0f, menuContentWidth, mainButtonHeight, 0.75f,
|
||||
menuButtonColor, glm::vec3(0.42f, 0.14f, 0.16f));
|
||||
|
||||
// Settings rows: LABEL [-] VALUE [+]
|
||||
@@ -349,117 +435,12 @@ void Engine::Initiate()
|
||||
MenuButton backButton = MakeMenuButton("BACK", menuMargin, 120.0f, menuContentWidth, 50.0f, 0.6f,
|
||||
glm::vec3(0.82f, 0.45f, 0.10f), menuAccent, 0.95f);
|
||||
|
||||
// Texture drawer. Filled in further down, once the things it paints on exist.
|
||||
struct DrawableSurface {
|
||||
const char* name;
|
||||
SurfaceTexture* surface;
|
||||
glm::vec4* colorOverride;
|
||||
bool hasCanvas = false;
|
||||
};
|
||||
std::vector<DrawableSurface> drawableSurfaces;
|
||||
int selectedSurface = 0;
|
||||
int selectedSwatch = 0;
|
||||
TexturePainter painter;
|
||||
bool painting = false;
|
||||
|
||||
// The first edit swaps in a private copy, so painting never touches the shared image file.
|
||||
// A colour override would hide the paint, so that goes too.
|
||||
auto EnsureCanvas = [&](DrawableSurface& drawable) {
|
||||
if (drawable.hasCanvas)
|
||||
return;
|
||||
|
||||
SurfaceTexture& surface = *drawable.surface;
|
||||
surface.texture = surface.texture ? resourcemanager->CopyTexture(*surface.texture)
|
||||
: resourcemanager->CreateTexture(256, 256);
|
||||
drawable.colorOverride->w = 0.0f;
|
||||
drawable.hasCanvas = true;
|
||||
};
|
||||
|
||||
const float surfaceRowY = -120.0f;
|
||||
const float swatchRowY = -64.0f;
|
||||
const float brushRowY = -8.0f;
|
||||
|
||||
MenuButton surfacePrevious = MakeMenuButton("<", menuMargin, surfaceRowY, 44.0f, 44.0f, 0.6f,
|
||||
menuButtonColor, menuButtonHover, 0.6f, true);
|
||||
MenuButton surfaceNext = MakeMenuButton(">", stepperUpX, surfaceRowY, 44.0f, 44.0f, 0.6f,
|
||||
menuButtonColor, menuButtonHover, 0.6f, true);
|
||||
TextBox* surfaceName = MakeMenuText("", menuMargin + 44.0f, surfaceRowY, menuContentWidth - 88.0f, 44.0f, 0.5f, menuText, 0.5f);
|
||||
|
||||
const glm::vec3 swatchColors[] = {
|
||||
{ 0.0f, 0.0f, 0.0f }, { 1.0f, 1.0f, 1.0f }, { 0.90f, 0.15f, 0.15f },
|
||||
{ 0.96f, 0.58f, 0.16f }, { 0.20f, 0.75f, 0.30f }, { 0.20f, 0.45f, 0.95f }
|
||||
};
|
||||
const float swatchSize = 52.0f;
|
||||
const float swatchSpacing = 60.0f;
|
||||
|
||||
std::vector<MenuButton> swatches;
|
||||
swatches.reserve(std::size(swatchColors));
|
||||
for (int i = 0; i < (int)std::size(swatchColors); i++) {
|
||||
swatches.push_back(MakeMenuButton("", menuMargin + swatchSpacing * i, swatchRowY, swatchSize, 44.0f, 0.5f,
|
||||
swatchColors[i], glm::mix(swatchColors[i], glm::vec3(1.0f), 0.2f), 1.0f, true));
|
||||
}
|
||||
|
||||
Box* swatchMarker = new Box();
|
||||
swatchMarker->Color = menuAccent;
|
||||
swatchMarker->t2d.center = { 0.0f, 0.5f };
|
||||
swatchMarker->t2d.size = { 0.0f, 0.0f, swatchSize, 4.0f };
|
||||
swatchMarker->BindToRenderSystem();
|
||||
menuItems.push_back(swatchMarker);
|
||||
|
||||
TextBox* brushLabel = MakeMenuText("BRUSH SIZE", menuMargin, brushRowY, 172.0f, 44.0f, 0.5f, menuMutedText);
|
||||
TextBox* brushValue = MakeMenuText("", stepperValueX, brushRowY, stepperValueWidth, 44.0f, 0.5f, menuText, 0.5f);
|
||||
MenuButton brushDown = MakeMenuButton("-", stepperDownX, brushRowY, 44.0f, 44.0f, 0.7f,
|
||||
menuButtonColor, menuButtonHover, 0.6f, true);
|
||||
MenuButton brushUp = MakeMenuButton("+", stepperUpX, brushRowY, 44.0f, 44.0f, 0.7f,
|
||||
menuButtonColor, menuButtonHover, 0.6f, true);
|
||||
MenuButton clearButton = MakeMenuButton("CLEAR", menuMargin, 52.0f, menuContentWidth, 50.0f, 0.6f,
|
||||
menuButtonColor, menuButtonHover);
|
||||
|
||||
// The canvas sits in the middle of the space right of the panel
|
||||
const float canvasSize = 640.0f;
|
||||
|
||||
Box* canvasBorder = new Box();
|
||||
canvasBorder->Color = menuAccent;
|
||||
canvasBorder->t2d.position = { 0.5f, 0.5f, menuPanelWidth * 0.5f, 0.0f };
|
||||
canvasBorder->t2d.center = { 0.5f, 0.5f };
|
||||
canvasBorder->t2d.size = { 0.0f, 0.0f, canvasSize + 8.0f, canvasSize + 8.0f };
|
||||
canvasBorder->BindToRenderSystem();
|
||||
menuItems.push_back(canvasBorder);
|
||||
|
||||
Box* canvasBlank = new Box(); // shows through until the surface has a texture
|
||||
canvasBlank->t2d.position = canvasBorder->t2d.position;
|
||||
canvasBlank->t2d.center = { 0.5f, 0.5f };
|
||||
canvasBlank->t2d.size = { 0.0f, 0.0f, canvasSize, canvasSize };
|
||||
canvasBlank->BindToRenderSystem();
|
||||
menuItems.push_back(canvasBlank);
|
||||
|
||||
ImageBox* canvasImage = new ImageBox();
|
||||
canvasImage->t2d.position = canvasBorder->t2d.position;
|
||||
canvasImage->t2d.center = { 0.5f, 0.5f };
|
||||
canvasImage->t2d.size = { 0.0f, 0.0f, canvasSize, canvasSize };
|
||||
canvasImage->BindToRenderSystem();
|
||||
menuItems.push_back(canvasImage);
|
||||
|
||||
std::vector<MenuButton*> menuButtons = {
|
||||
&playButton, &drawerButton, &settingsButton, &quitButton,
|
||||
&playButton, &settingsButton, &quitButton,
|
||||
&sensitivityDown, &sensitivityUp, &fovDown, &fovUp,
|
||||
&hitboxToggle, &graphicsButton, &resetButton, &backButton,
|
||||
&bloomToggle, &shadowToggle, &lightShadowToggle, &vsyncToggle, &cullingToggle,
|
||||
&surfacePrevious, &surfaceNext, &brushDown, &brushUp, &clearButton
|
||||
&bloomToggle, &shadowToggle, &lightShadowToggle, &vsyncToggle, &cullingToggle
|
||||
};
|
||||
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) {
|
||||
button.box->Opacity = visible ? button.opacity : 0.0f;
|
||||
@@ -472,7 +453,6 @@ void Engine::Initiate()
|
||||
const bool paused = menuScreen == MenuScreen::Paused;
|
||||
const bool settings = menuScreen == MenuScreen::Settings;
|
||||
const bool graphics = menuScreen == MenuScreen::Graphics;
|
||||
const bool drawer = menuScreen == MenuScreen::Drawer;
|
||||
const bool mainButtons = main || paused;
|
||||
|
||||
menuVeil->Opacity = 0.22f;
|
||||
@@ -480,39 +460,13 @@ void Engine::Initiate()
|
||||
menuPanelEdge->Opacity = 1.0f;
|
||||
menuTitle->Opacity = 1.0f;
|
||||
menuSubtitle->Opacity = 1.0f;
|
||||
menuHint->Opacity = 1.0f;
|
||||
menuSubtitle->text = main ? "MAIN MENU" : (paused ? "PAUSED"
|
||||
: (drawer ? "TEXTURE DRAWER" : (graphics ? "GRAPHICS" : "SETTINGS")));
|
||||
menuSubtitle->text = main ? "MAIN MENU" : (paused ? "PAUSED" : (graphics ? "GRAPHICS" : "SETTINGS"));
|
||||
playButton.label->text = paused ? "RESUME" : "PLAY";
|
||||
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(drawerButton, mainButtons);
|
||||
SetButtonVisible(settingsButton, mainButtons);
|
||||
SetButtonVisible(quitButton, mainButtons);
|
||||
SetButtonVisible(surfacePrevious, drawer);
|
||||
SetButtonVisible(surfaceNext, drawer);
|
||||
SetButtonVisible(brushDown, drawer);
|
||||
SetButtonVisible(brushUp, drawer);
|
||||
SetButtonVisible(clearButton, drawer);
|
||||
for (MenuButton& swatch : swatches)
|
||||
SetButtonVisible(swatch, drawer);
|
||||
|
||||
Texture* canvasTexture = drawableSurfaces[selectedSurface].surface->texture;
|
||||
canvasImage->surface = SurfaceTexture::Stretched(canvasTexture);
|
||||
canvasImage->Opacity = drawer && canvasTexture ? 1.0f : 0.0f;
|
||||
canvasBlank->Opacity = drawer ? 1.0f : 0.0f;
|
||||
canvasBorder->Opacity = drawer ? 1.0f : 0.0f;
|
||||
surfaceName->Opacity = drawer ? 1.0f : 0.0f;
|
||||
surfaceName->text = drawableSurfaces[selectedSurface].name;
|
||||
brushLabel->Opacity = drawer ? 1.0f : 0.0f;
|
||||
brushValue->Opacity = drawer ? 1.0f : 0.0f;
|
||||
brushValue->text = std::to_string(painter.brushSize);
|
||||
swatchMarker->Opacity = drawer ? 1.0f : 0.0f;
|
||||
swatchMarker->t2d.position = { 0.0f, 0.5f, menuMargin + swatchSpacing * selectedSwatch, swatchRowY + 28.0f };
|
||||
|
||||
SetButtonVisible(sensitivityDown, settings);
|
||||
SetButtonVisible(sensitivityUp, settings);
|
||||
@@ -521,7 +475,7 @@ void Engine::Initiate()
|
||||
SetButtonVisible(hitboxToggle, settings);
|
||||
SetButtonVisible(graphicsButton, settings);
|
||||
SetButtonVisible(resetButton, settings);
|
||||
SetButtonVisible(backButton, settings || drawer || graphics);
|
||||
SetButtonVisible(backButton, settings || graphics);
|
||||
|
||||
auto ShowToggle = [&](MenuButton& button, const char* name, bool enabled) {
|
||||
SetButtonVisible(button, graphics);
|
||||
@@ -549,9 +503,8 @@ void Engine::Initiate()
|
||||
auto OpenMenu = [&](MenuScreen screen) {
|
||||
menuActive = true;
|
||||
menuScreen = screen;
|
||||
charactermanager->ClearInput();
|
||||
testcharacter.SetWalking(false);
|
||||
npc.SetWalking(false);
|
||||
testcharacter.body->SetWalking(false);
|
||||
npc.body->SetWalking(false);
|
||||
fpsCounter->Opacity = 0.0f;
|
||||
camera->lockedcursor = false;
|
||||
glfwSetInputMode(window->handle, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
|
||||
@@ -560,7 +513,6 @@ void Engine::Initiate()
|
||||
|
||||
auto ResumeGame = [&]() {
|
||||
menuActive = false;
|
||||
charactermanager->ClearInput();
|
||||
for (Box* item : menuItems) item->Opacity = 0.0f;
|
||||
fpsCounter->Opacity = 1.0f;
|
||||
camera->lockedcursor = true;
|
||||
@@ -575,21 +527,21 @@ void Engine::Initiate()
|
||||
glfwSetInputMode(window->handle, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Baseplate
|
||||
// Baseplate, a flat slab with its top at y = 0
|
||||
// ------------------------------------------------------------------------
|
||||
Mesh* floor = CreateCubeMesh();
|
||||
floor->name = "baseplate";
|
||||
floor->surface = SurfaceTexture::Tiled(blanktex, 8.0f);
|
||||
floor->roughness = 0.9f;
|
||||
floor->t.ScaleTo({ 200.0f, 2.0f, 200.0f });
|
||||
floor->t.TranslateTo({ 0.0f, -1.0f, 0.0f });
|
||||
floor->BindToRenderSystem();
|
||||
mainf->AddChild(floor);
|
||||
Mesh* baseplate = CreateCubeMesh();
|
||||
baseplate->name = "baseplate";
|
||||
baseplate->t.ScaleTo({ 200.0f, 2.0f, 200.0f });
|
||||
baseplate->t.TranslateTo({ 0.0f, -1.0f, 0.0f });
|
||||
baseplate->t.RotateToQuaternion(glm::quat(1.0f, 0.0f, 0.0f, 0.0f));
|
||||
baseplate->surface = SurfaceTexture::Tiled(blanktex, 8.0f);
|
||||
baseplate->BindToRenderSystem();
|
||||
mainf->AddChild(baseplate);
|
||||
|
||||
p* floorBody = new p(floor);
|
||||
floorBody->velocity = false;
|
||||
physicsengine->AddObject(floorBody);
|
||||
const BoundingBox floorBounds = floor->t.GetRotationlessAABB();
|
||||
p* baseplateBody = new p(baseplate);
|
||||
baseplateBody->velocity = false;
|
||||
baseplateBody->friction = 0.7f;
|
||||
physicsengine->AddObject(baseplateBody);
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Light cubes
|
||||
@@ -616,22 +568,19 @@ void Engine::Initiate()
|
||||
Mesh* pointLightCube = AddLightCube("point_light_cube", pointLightPosition, 0.8f, pointLightColor);
|
||||
rendersystem->lighting->AddPointLight(pointLightPosition, pointLightColor, 300.0f, 30.0f);
|
||||
|
||||
drawableSurfaces.push_back({ "BASEPLATE", &floor->surface, &floor->colorOverride });
|
||||
drawableSurfaces.push_back({ "PLAYER", &testcharacter.surface, &testcharacter.colorOverride });
|
||||
drawableSurfaces.push_back({ "DUMMY", &npc.surface, &npc.colorOverride });
|
||||
drawableSurfaces.push_back({ "SKY LIGHT CUBE", &skyLightCube->surface, &skyLightCube->colorOverride });
|
||||
drawableSurfaces.push_back({ "POINT LIGHT CUBE", &pointLightCube->surface, &pointLightCube->colorOverride });
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Ground check + respawn
|
||||
// ------------------------------------------------------------------------
|
||||
auto IsGrounded = [&]() -> bool {
|
||||
auto IsGrounded = [&](Character& character) -> bool {
|
||||
Ray downRay;
|
||||
downRay.origin = testcharacter.body->t.GetTranslation();
|
||||
downRay.origin = character.body->t.GetTranslation();
|
||||
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()) {
|
||||
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;
|
||||
auto hit = IsRayInT(downRay, obj->pointer->t);
|
||||
if (hit.has_value()) {
|
||||
@@ -677,6 +626,16 @@ void Engine::Initiate()
|
||||
prevFrameTime = frameNow;
|
||||
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;
|
||||
const bool escapeDown = keyboard->IsKeyDown(GLFW_KEY_ESCAPE);
|
||||
const bool escapePressed = escapeDown && !prevEscapeDown;
|
||||
@@ -685,7 +644,7 @@ void Engine::Initiate()
|
||||
if (escapePressed) {
|
||||
if (!menuActive)
|
||||
OpenMenu(MenuScreen::Paused);
|
||||
else if (menuScreen == MenuScreen::Settings || menuScreen == MenuScreen::Drawer)
|
||||
else if (menuScreen == MenuScreen::Settings)
|
||||
menuScreen = settingsReturn;
|
||||
else if (menuScreen == MenuScreen::Graphics)
|
||||
menuScreen = MenuScreen::Settings;
|
||||
@@ -696,32 +655,75 @@ void Engine::Initiate()
|
||||
// --------------------------------------------------------------------
|
||||
// Mouse look
|
||||
// --------------------------------------------------------------------
|
||||
int wheelSteps = 0; // up is positive
|
||||
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) {
|
||||
float xoffset = mouse->GetX() - mouse->GetLastX();
|
||||
float yoffset = mouse->GetLastY() - mouse->GetY();
|
||||
camera->ProcessMouseMovement(xoffset, yoffset);
|
||||
}
|
||||
else if (buffer.GetType() == Mouse::Event::Type::RPress) {
|
||||
camera->lockedcursor = !camera->lockedcursor;
|
||||
if (camera->lockedcursor) {
|
||||
glfwSetInputMode(window->handle, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
|
||||
}
|
||||
|
||||
const glm::vec2 mousePos((float)mouse->GetX(), (float)mouse->GetY());
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// 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;
|
||||
glfwGetWindowSize(window->handle, &ww, &wh);
|
||||
glfwSetCursorPos(window->handle, ww * 0.5, wh * 0.5);
|
||||
}
|
||||
else {
|
||||
glfwSetInputMode(window->handle, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
|
||||
|
||||
// Otherwise the jump back to the middle reads as one big mouse movement and the camera snaps
|
||||
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) {
|
||||
ApplyMenuLayout();
|
||||
|
||||
const bool liveBackdrop = menuScreen == MenuScreen::Main ||
|
||||
((menuScreen == MenuScreen::Settings || menuScreen == MenuScreen::Graphics || menuScreen == MenuScreen::Drawer)
|
||||
((menuScreen == MenuScreen::Settings || menuScreen == MenuScreen::Graphics)
|
||||
&& settingsReturn == MenuScreen::Main);
|
||||
if (liveBackdrop) {
|
||||
static float menuAngle = 0.6f;
|
||||
@@ -817,42 +819,9 @@ void Engine::Initiate()
|
||||
menuScreen = MenuScreen::Settings;
|
||||
}
|
||||
}
|
||||
else if (menuScreen == MenuScreen::Drawer) {
|
||||
const int surfaceCount = (int)drawableSurfaces.size();
|
||||
if (surfacePrevious.hovered) {
|
||||
selectedSurface = (selectedSurface + surfaceCount - 1) % surfaceCount;
|
||||
}
|
||||
else if (surfaceNext.hovered) {
|
||||
selectedSurface = (selectedSurface + 1) % surfaceCount;
|
||||
}
|
||||
else if (brushDown.hovered) {
|
||||
painter.brushSize = glm::max(painter.brushSize - 2, 2);
|
||||
}
|
||||
else if (brushUp.hovered) {
|
||||
painter.brushSize = glm::min(painter.brushSize + 2, 64);
|
||||
}
|
||||
else if (clearButton.hovered) {
|
||||
DrawableSurface& drawable = drawableSurfaces[selectedSurface];
|
||||
EnsureCanvas(drawable);
|
||||
painter.Clear(*drawable.surface->texture);
|
||||
}
|
||||
else if (backButton.hovered) {
|
||||
menuScreen = settingsReturn;
|
||||
}
|
||||
for (int i = 0; i < (int)swatches.size(); i++) {
|
||||
if (swatches[i].hovered) {
|
||||
selectedSwatch = i;
|
||||
painter.color = swatchColors[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (playButton.hovered) {
|
||||
ResumeGame();
|
||||
}
|
||||
else if (drawerButton.hovered) {
|
||||
settingsReturn = menuScreen;
|
||||
menuScreen = MenuScreen::Drawer;
|
||||
}
|
||||
else if (settingsButton.hovered) {
|
||||
settingsReturn = menuScreen;
|
||||
menuScreen = MenuScreen::Settings;
|
||||
@@ -864,25 +833,6 @@ void Engine::Initiate()
|
||||
menuScreen = MenuScreen::Main;
|
||||
}
|
||||
}
|
||||
// A stroke has to start on the canvas, but carries on if the mouse wanders off it
|
||||
canvasImage->t2d.Recalculate();
|
||||
const glm::vec2 canvasUV = (mousePos - canvasImage->t2d.pixelposition) / canvasImage->t2d.pixelsize;
|
||||
if (!mouseDown || menuScreen != MenuScreen::Drawer) {
|
||||
painting = false;
|
||||
}
|
||||
else if (!prevMouseDown) {
|
||||
painting = canvasUV.x >= 0.0f && canvasUV.x <= 1.0f && canvasUV.y >= 0.0f && canvasUV.y <= 1.0f;
|
||||
}
|
||||
|
||||
if (painting) {
|
||||
DrawableSurface& drawable = drawableSurfaces[selectedSurface];
|
||||
EnsureCanvas(drawable);
|
||||
painter.Stroke(*drawable.surface->texture, glm::clamp(canvasUV, glm::vec2(0.0f), glm::vec2(1.0f)));
|
||||
}
|
||||
else {
|
||||
painter.EndStroke();
|
||||
}
|
||||
|
||||
prevMouseDown = mouseDown;
|
||||
|
||||
UpdateHitboxDebug();
|
||||
@@ -891,74 +841,84 @@ void Engine::Initiate()
|
||||
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);
|
||||
const bool hasMoveInput = glm::length(inputDirection) > 0.0f;
|
||||
keyboardMind.enabled = !console->IsOpen(); // typing isn't walking
|
||||
charactermanager->Think();
|
||||
|
||||
if (hasMoveInput)
|
||||
testcharacter.SetTurnDirection(inputDirection);
|
||||
testcharacter.SetWalking(hasMoveInput);
|
||||
for (Character* character : charactermanager->GetCharacters()) {
|
||||
if (character->ragdoll) continue;
|
||||
character->body->ApplyMovement(IsGrounded(*character), 1.0f, deltatime, refDt);
|
||||
}
|
||||
|
||||
charactermanager->Step(deltatime);
|
||||
|
||||
const glm::vec3 inputVel = hasMoveInput
|
||||
? 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;
|
||||
}
|
||||
|
||||
float physicsAlpha = 0.0f; // how far this frame is between the last tick and the next
|
||||
{
|
||||
const int substeps = std::max(1, (int)std::ceil(deltatime * 120.0f));
|
||||
const float subDt = deltatime / float(substeps);
|
||||
for (int i = 0; i < substeps; i++) physicsengine->Step(subDt);
|
||||
}
|
||||
npc.ragdoll->Update(deltatime);
|
||||
// Physics ticks a fixed 30 times a second of real time, whatever the frame rate.
|
||||
// deltatime is real time sped up by timeScale, so a tick covers that much more game time.
|
||||
const float tickRate = 30.0f;
|
||||
const float tickSeconds = 1.0f / tickRate;
|
||||
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) {
|
||||
testcharacter.body->t.TranslateTo(RandomSpawn());
|
||||
characterBody->linearvelocity = glm::vec3(0.0f);
|
||||
characterBody->WakeUp();
|
||||
static float physicsAccumulator = 0.0f;
|
||||
physicsAccumulator += deltatime / timeScale;
|
||||
int ticks = 0;
|
||||
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
|
||||
// --------------------------------------------------------------------
|
||||
{
|
||||
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);
|
||||
glm::vec3 cameraPos = target - camera->t.GetFrontVector() * 5.0f;
|
||||
|
||||
const bool playerOffFloor =
|
||||
playerPos.x < floorBounds.min.x || playerPos.x > floorBounds.max.x ||
|
||||
playerPos.z < floorBounds.min.z || playerPos.z > floorBounds.max.z;
|
||||
const bool ignoreFloorCollision = playerOffFloor || playerPos.y < floorBounds.max.y;
|
||||
// Scroll zooms, unless something on screen is using the wheel. Eases toward the new distance.
|
||||
static float zoomTarget = 5.0f;
|
||||
static float zoomDistance = 5.0f;
|
||||
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 float desiredDistance = glm::length(cameraRay);
|
||||
@@ -969,12 +929,10 @@ void Engine::Initiate()
|
||||
cameraSweep.direction = cameraDir;
|
||||
float nearestHit = desiredDistance;
|
||||
for (p* obj : physicsengine->GetObjects()) {
|
||||
if (obj->pointer == testcharacter.body || !obj->collision)
|
||||
if (obj->pointer == controlled->body || !obj->collision)
|
||||
continue;
|
||||
if (obj->collisionGroup != 0) // flailing ragdoll limbs would make the camera jump
|
||||
continue;
|
||||
if (obj == floorBody && ignoreFloorCollision)
|
||||
continue;
|
||||
auto hit = IsRayInT(cameraSweep, obj->pointer->t);
|
||||
if (!hit.has_value())
|
||||
continue;
|
||||
@@ -1024,6 +982,7 @@ void Engine::Initiate()
|
||||
// --------------------------------------------------------------------
|
||||
UpdateHitboxDebug();
|
||||
rendersystem->Render(*camera);
|
||||
physicsengine->RemoveInterpolation(); // back to the real positions before anything moves them
|
||||
|
||||
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)
|
||||
{
|
||||
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) {
|
||||
static int wx = 100, wy = 100, ww = 1920, wh = 1080;
|
||||
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);
|
||||
else if (action == GLFW_RELEASE) keyboard->KeyUp(key);
|
||||
}
|
||||
|
||||
if (key >= GLFW_KEY_A && key <= GLFW_KEY_Z && engine && engine->charactermanager) {
|
||||
const char letter = (char)('A' + (key - GLFW_KEY_A));
|
||||
if (action == GLFW_PRESS) engine->charactermanager->PressInput(letter);
|
||||
else if (action == GLFW_RELEASE) engine->charactermanager->ReleaseInput(letter);
|
||||
}
|
||||
void char_callback(GLFWwindow* window, unsigned int codepoint)
|
||||
{
|
||||
if (engine && engine->console && engine->console->IsOpen())
|
||||
engine->console->OnChar(codepoint);
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -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 char_callback(GLFWwindow* window, unsigned int codepoint);
|
||||
|
||||
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
|
||||
|
||||
void mouse_callback(GLFWwindow* windowe, double xposIn, double yposIn);
|
||||
@@ -34,6 +36,7 @@ class ParticleEmitter;
|
||||
class Camera;
|
||||
class Animator;
|
||||
class CharacterManager;
|
||||
class Console;
|
||||
|
||||
class Engine {
|
||||
public:
|
||||
@@ -41,6 +44,7 @@ public:
|
||||
Physics* physicsengine = nullptr;
|
||||
RenderSystem* rendersystem = nullptr;
|
||||
CharacterManager* charactermanager = nullptr;
|
||||
Console* console = nullptr;
|
||||
public:
|
||||
//METADATA
|
||||
int frame = 0;
|
||||
|
||||
+316
-101
@@ -64,8 +64,10 @@ void Physics::Step(float dt)
|
||||
|
||||
// Integrate forces ? velocities
|
||||
for (p* body : objects) {
|
||||
// Nothing rotates until the end of the step, so everything until then can share this
|
||||
body->invInertia = ComputeInvInertiaTensor(body);
|
||||
// Nothing rotates until the end of the step, so everything until then can share this.
|
||||
// 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;
|
||||
|
||||
@@ -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
|
||||
// ============================================================================
|
||||
void Physics::ResolveCollisions(float dt)
|
||||
{
|
||||
FindCollisions();
|
||||
|
||||
for (Collision& col : collisions)
|
||||
Resolve(&col);
|
||||
|
||||
if (joints.empty()) return;
|
||||
|
||||
// The first contact pass nudges positions, so these have to be worked out after it
|
||||
PrepareContact(col);
|
||||
for (BallJoint* joint : joints) {
|
||||
DampJoint(*joint, dt);
|
||||
PrepareJoint(*joint, dt);
|
||||
}
|
||||
|
||||
// Joints and contacts fight each other, so iterate them together or limbs get pulled through the floor
|
||||
for (int i = 0; i < PHYSICS_JOINT_ITERATIONS; i++) {
|
||||
// One pass over the contacts leaves things resting on several points spinning a little, since
|
||||
// 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)
|
||||
SolveJoint(*joint);
|
||||
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
|
||||
// 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()
|
||||
{
|
||||
collisions.clear();
|
||||
broadPhase.clear();
|
||||
|
||||
for (p* body : objects) {
|
||||
if (!body->collision) continue;
|
||||
BroadPhaseEntry entry;
|
||||
entry.body = body;
|
||||
entry.bounds = body->pointer->t.GetAABB();
|
||||
broadPhase.push_back(entry);
|
||||
// Everything goes in, collision or not, so the list only changes when bodies come or go
|
||||
const bool rebuilt = broadPhaseVersion != objectsVersion;
|
||||
if (rebuilt) {
|
||||
broadPhase.clear();
|
||||
for (p* body : objects) {
|
||||
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) {
|
||||
return first.bounds.min.x < second.bounds.min.x;
|
||||
});
|
||||
// Things barely move between substeps, so last step's order is nearly sorted already and insertion
|
||||
// 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++) {
|
||||
const BroadPhaseEntry& first = broadPhase[i];
|
||||
p* a = first.body;
|
||||
if (!a->collision) continue;
|
||||
|
||||
for (int j = i + 1; j < (int)broadPhase.size(); j++) {
|
||||
const BroadPhaseEntry& second = broadPhase[j];
|
||||
if (second.bounds.min.x >= first.bounds.max.x) break;
|
||||
|
||||
p* b = second.body;
|
||||
if (!b->collision) 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->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 (a->collisionMesh || b->collisionMesh) {
|
||||
@@ -172,18 +259,29 @@ void Physics::FindCollisions()
|
||||
continue;
|
||||
}
|
||||
|
||||
std::optional<TInTInfo> e = TInT(a->pointer->t, b->pointer->t);
|
||||
if (e.has_value()) {
|
||||
Collision tp(a, b);
|
||||
tp.POI = e.value().POI;
|
||||
tp.CN = e.value().CN;
|
||||
tp.overlap = e.value().overlap;
|
||||
collisions.push_back(tp);
|
||||
}
|
||||
std::optional<TInTInfo> e = TInT(a->pointer->t, b->pointer->t, false);
|
||||
if (e.has_value())
|
||||
CollideBoxes(a, b, e.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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 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.
|
||||
const bool surfaceIsA = !a->collisionMesh && b->collisionMesh;
|
||||
// Something that never moves, like terrain, is the surface. Otherwise box faces are the
|
||||
// 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;
|
||||
|
||||
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)
|
||||
{
|
||||
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;
|
||||
p* b = c->ObjB;
|
||||
if (!body->velocity)
|
||||
return;
|
||||
body->linearvelocity += body->GetInvMass() * impulse;
|
||||
if (!body->lockRotation)
|
||||
body->angularvelocity += body->invInertia * glm::cross(r, impulse);
|
||||
}
|
||||
|
||||
a->WakeUp();
|
||||
b->WakeUp();
|
||||
static glm::vec3 RelativeVelocity(const Collision& c)
|
||||
{
|
||||
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;
|
||||
const glm::vec3 rA = c->POI - a->pointer->t.GetTranslation();
|
||||
const glm::vec3 rB = c->POI - b->pointer->t.GetTranslation();
|
||||
|
||||
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) {
|
||||
// One over how hard it is to push the two points apart along direction
|
||||
static float InverseDenominator(const Collision& c, const glm::vec3& direction)
|
||||
{
|
||||
auto angular = [](const glm::mat3& invI, const glm::vec3& r, const glm::vec3& 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 ----
|
||||
const float denomN = invMassA + invMassB + angDenom(invIA, rA, n) + angDenom(invIB, rB, n);
|
||||
const float jN = -(1.0f + e) * relVelN / denomN;
|
||||
|
||||
const glm::vec3 impulseN = jN * n;
|
||||
if (a->velocity) {
|
||||
a->linearvelocity += invMassA * impulseN;
|
||||
if (!a->lockRotation)
|
||||
a->angularvelocity += invIA * glm::cross(rA, impulseN);
|
||||
}
|
||||
if (b->velocity) {
|
||||
b->linearvelocity -= invMassB * impulseN;
|
||||
if (!b->lockRotation)
|
||||
b->angularvelocity -= invIB * glm::cross(rB, impulseN);
|
||||
// Something moving into a sleeping body wakes it. Something merely resting against it doesn't, or the two would
|
||||
// keep each other awake forever, and waking resets the timer every body needs to run down before it can sleep.
|
||||
// A push that does get through to a sleeper wakes it anyway, in UpdateSleep.
|
||||
static void WakeIfDisturbed(p* sleeper, const p* other)
|
||||
{
|
||||
if (!sleeper->isSleeping)
|
||||
return;
|
||||
// Hitboxes get moved by the animation rather than by physics, so they have no velocity to go by
|
||||
if (other->isHitbox) {
|
||||
sleeper->WakeUp();
|
||||
return;
|
||||
}
|
||||
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 ----
|
||||
glm::vec3 tangent = relVel - relVelN * n;
|
||||
const float tangLen = glm::length(tangent);
|
||||
if (tangLen > 1e-6f) {
|
||||
tangent /= tangLen;
|
||||
void Physics::PrepareContact(Collision& c)
|
||||
{
|
||||
p* a = c.ObjA;
|
||||
p* b = c.ObjB;
|
||||
WakeIfDisturbed(a, b);
|
||||
WakeIfDisturbed(b, a);
|
||||
|
||||
const float denomT = invMassA + invMassB + angDenom(invIA, rA, tangent) + angDenom(invIB, rB, tangent);
|
||||
float jT = -glm::dot(relVel, tangent) / denomT;
|
||||
c.rA = c.POI - a->pointer->t.GetTranslation();
|
||||
c.rB = c.POI - b->pointer->t.GetTranslation();
|
||||
|
||||
const float mu = (a->friction + b->friction) * 0.5f * c->frictionScale;
|
||||
jT = glm::clamp(jT, -mu * jN, mu * jN);
|
||||
// Friction works along two fixed directions across the surface
|
||||
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;
|
||||
if (a->velocity) {
|
||||
a->linearvelocity += invMassA * impulseT;
|
||||
if (!a->lockRotation)
|
||||
a->angularvelocity += invIA * glm::cross(rA, impulseT);
|
||||
}
|
||||
if (b->velocity) {
|
||||
b->linearvelocity -= invMassB * impulseT;
|
||||
if (!b->lockRotation)
|
||||
b->angularvelocity -= invIB * glm::cross(rB, impulseT);
|
||||
}
|
||||
}
|
||||
c.inverseDenomN = InverseDenominator(c, n);
|
||||
c.inverseDenomT1 = InverseDenominator(c, c.tangent1);
|
||||
c.inverseDenomT2 = InverseDenominator(c, c.tangent2);
|
||||
c.mu = (a->friction + b->friction) * 0.5f * c.frictionScale;
|
||||
|
||||
// ---- Baumgarte positional correction ----
|
||||
if (!firstPass)
|
||||
// Bounce off at a share of the speed it came in at, but slow touches just stop, or resting things would hop
|
||||
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;
|
||||
|
||||
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)
|
||||
* PHYSICS_BAUMGARTE_FACTOR
|
||||
* c->correctionWeight;
|
||||
const glm::vec3 corrVec = correction * n;
|
||||
* c.correctionWeight;
|
||||
const glm::vec3 corrVec = correction * c.CN;
|
||||
if (a->velocity) a->pointer->t.TranslateBy(invMassA * corrVec);
|
||||
if (b->velocity) b->pointer->t.TranslateBy(-invMassB * corrVec);
|
||||
}
|
||||
|
||||
@@ -27,10 +27,11 @@ static constexpr float PHYSICS_LINEAR_DAMPING = 0.995f;
|
||||
static constexpr float PHYSICS_ANGULAR_DAMPING = 0.990f;
|
||||
static constexpr float PHYSICS_BAUMGARTE_FACTOR = 0.1f;
|
||||
static constexpr float PHYSICS_BAUMGARTE_SLOP = 0.02f;
|
||||
static constexpr float PHYSICS_SLEEP_LINEAR_SQ = 0.01f * 0.01f;
|
||||
static constexpr float PHYSICS_SLEEP_ANGULAR_SQ = 0.01f * 0.01f;
|
||||
static constexpr float PHYSICS_SLEEP_LINEAR_SQ = 0.05f * 0.05f; // m/s
|
||||
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_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 float PHYSICS_JOINT_BIAS = 0.2f;
|
||||
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 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_MIN_IMPACT_IMPULSE = 1.0f; // newton seconds, softer contacts like resting aren't recorded
|
||||
|
||||
// ============================================================================
|
||||
// Physics body
|
||||
@@ -75,6 +77,7 @@ public:
|
||||
bool isSleeping = false;
|
||||
bool lockRotation = false;
|
||||
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
|
||||
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
|
||||
|
||||
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* 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) {}
|
||||
};
|
||||
|
||||
@@ -154,6 +184,14 @@ struct BallJoint {
|
||||
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 {
|
||||
p* body = nullptr;
|
||||
BoundingBox bounds;
|
||||
@@ -168,18 +206,25 @@ private:
|
||||
std::vector<BallJoint*> joints;
|
||||
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;
|
||||
unsigned int objectsVersion = 1;
|
||||
unsigned int broadPhaseVersion = 0;
|
||||
std::vector<Collision> collisions;
|
||||
std::vector<Collision> meshContacts;
|
||||
std::vector<int> meshCandidates;
|
||||
|
||||
static glm::mat3 ComputeInvInertiaTensor(const p* body);
|
||||
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 CollideTriangles(p* a, p* b);
|
||||
void CollideBoxes(p* a, p* b, const TInTInfo& info);
|
||||
void WakeGroup(int group);
|
||||
void WakeAround(p* removed);
|
||||
void PrepareJoint(BallJoint& joint, float dt);
|
||||
void SolveJoint(BallJoint& joint);
|
||||
void DampJoint(BallJoint& joint, float dt);
|
||||
@@ -191,7 +236,7 @@ public:
|
||||
void SetGravity(const glm::vec3& g) noexcept { gravity = g; }
|
||||
|
||||
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); }
|
||||
const std::vector<p*>& GetObjects() const { return objects; }
|
||||
|
||||
@@ -203,13 +248,18 @@ public:
|
||||
|
||||
void RemoveP(p* obj) {
|
||||
objects.erase(std::find(objects.begin(), objects.end(), obj));
|
||||
objectsVersion++;
|
||||
WakeAround(obj);
|
||||
}
|
||||
|
||||
void RemoveObject(t_package* pointer) {
|
||||
for (int i = 0; i < (int)objects.size(); i++) {
|
||||
if (objects[i]->pointer == pointer) {
|
||||
delete objects[i];
|
||||
p* removed = objects[i];
|
||||
objects.erase(objects.begin() + i);
|
||||
objectsVersion++;
|
||||
WakeAround(removed);
|
||||
delete removed;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -217,7 +267,24 @@ public:
|
||||
|
||||
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);
|
||||
|
||||
// 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
|
||||
+15
-13
@@ -480,23 +480,25 @@ void RenderSystem::Render(Camera& camera)
|
||||
}
|
||||
|
||||
// ---- 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);
|
||||
PrepareBoxShader(camera);
|
||||
int activeGuiShader = -1;
|
||||
for (Renderable* r : renderables) {
|
||||
if (r->shadertype == Renderable::BoxShader)
|
||||
r->Render(*BoxShader);
|
||||
}
|
||||
const int type = r->shadertype;
|
||||
if (type != Renderable::BoxShader && type != Renderable::ImageBoxShader && type != Renderable::TextShader)
|
||||
continue;
|
||||
|
||||
PrepareImageBoxShader(camera);
|
||||
for (Renderable* r : renderables) {
|
||||
if (r->shadertype == Renderable::ImageBoxShader)
|
||||
r->Render(*ImageBoxShader);
|
||||
}
|
||||
if (type != activeGuiShader) {
|
||||
if (type == Renderable::BoxShader) PrepareBoxShader(camera);
|
||||
else if (type == Renderable::ImageBoxShader) PrepareImageBoxShader(camera);
|
||||
else PrepareTextShader(camera);
|
||||
activeGuiShader = type;
|
||||
}
|
||||
|
||||
PrepareTextShader(camera);
|
||||
for (Renderable* r : renderables) {
|
||||
if (r->shadertype == Renderable::TextShader)
|
||||
r->Render(*Text2DShader);
|
||||
if (type == Renderable::BoxShader) r->Render(*BoxShader);
|
||||
else if (type == Renderable::ImageBoxShader) r->Render(*ImageBoxShader);
|
||||
else r->Render(*Text2DShader);
|
||||
}
|
||||
glEnable(GL_CULL_FACE);
|
||||
|
||||
|
||||
@@ -3,8 +3,13 @@
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <assimp/Importer.hpp>
|
||||
#include <assimp/postprocess.h>
|
||||
#include <assimp/scene.h>
|
||||
|
||||
#include "texture.h"
|
||||
#include "font.h"
|
||||
#include "MeshData.h"
|
||||
|
||||
ResourceManager::~ResourceManager()
|
||||
{
|
||||
@@ -19,6 +24,49 @@ ResourceManager::~ResourceManager()
|
||||
}
|
||||
for (auto& [path, font] : fonts)
|
||||
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)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
class Texture;
|
||||
class Font;
|
||||
class MeshData;
|
||||
|
||||
// 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.
|
||||
@@ -20,6 +21,9 @@ public:
|
||||
Texture* GetTexture(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
|
||||
Texture* CreateTexture(int width, int height);
|
||||
Texture* CopyTexture(const Texture& source);
|
||||
@@ -27,6 +31,7 @@ public:
|
||||
private:
|
||||
std::unordered_map<std::string, Texture*> textures;
|
||||
std::unordered_map<std::string, Font*> fonts;
|
||||
std::unordered_map<std::string, MeshData*> meshes;
|
||||
std::vector<Texture*> createdTextures;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
+6
-4
@@ -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">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|Win32">
|
||||
@@ -166,6 +166,7 @@
|
||||
<ItemGroup>
|
||||
<ClCompile Include="Character.cpp" />
|
||||
<ClCompile Include="CharacterManager.cpp" />
|
||||
<ClCompile Include="CharacterMind.cpp" />
|
||||
<ClCompile Include="CollisionMesh.cpp" />
|
||||
<ClCompile Include="Cubemap.h" />
|
||||
<ClCompile Include="EBO.cpp" />
|
||||
@@ -175,6 +176,7 @@
|
||||
<ClCompile Include="Mouse.cpp" />
|
||||
<ClCompile Include="Physics.cpp" />
|
||||
<ClCompile Include="Ragdoll.cpp" />
|
||||
<ClCompile Include="Console.cpp" />
|
||||
<ClCompile Include="RenderSystem.cpp" />
|
||||
<ClCompile Include="ResourceManager.cpp" />
|
||||
<ClCompile Include="shaderClass.cpp" />
|
||||
@@ -187,12 +189,12 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="Animator.h" />
|
||||
<ClInclude Include="BodyComponentActions.h" />
|
||||
<ClInclude Include="CharacterAction.h" />
|
||||
<ClInclude Include="Character.h" />
|
||||
<ClInclude Include="CharacterManager.h" />
|
||||
<ClInclude Include="CollisionMesh.h" />
|
||||
<ClInclude Include="common.h" />
|
||||
<ClInclude Include="DefaultBodyComponents.h" />
|
||||
<ClInclude Include="CharacterMind.h" />
|
||||
<ClInclude Include="Engine.h" />
|
||||
<ClInclude Include="Folder.h" />
|
||||
<ClInclude Include="font.h" />
|
||||
@@ -204,6 +206,7 @@
|
||||
<ClInclude Include="Physics.h" />
|
||||
<ClInclude Include="QuadVertices.h" />
|
||||
<ClInclude Include="Ragdoll.h" />
|
||||
<ClInclude Include="Console.h" />
|
||||
<ClInclude Include="RenderSystem.h" />
|
||||
<ClInclude Include="ResourceManager.h" />
|
||||
<ClInclude Include="Rig.h" />
|
||||
@@ -223,7 +226,6 @@
|
||||
<ClInclude Include="shaderClass.h" />
|
||||
<ClInclude Include="SoundSystem.h" />
|
||||
<ClInclude Include="SurfaceTexture.h" />
|
||||
<ClInclude Include="TexturePainter.h" />
|
||||
<ClInclude Include="stb_image.h" />
|
||||
<ClInclude Include="t.h" />
|
||||
<ClInclude Include="t2d.h" />
|
||||
|
||||
@@ -49,6 +49,9 @@
|
||||
<ClCompile Include="Ragdoll.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Console.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ResourceManager.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
@@ -61,6 +64,9 @@
|
||||
<ClCompile Include="CharacterManager.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="CharacterMind.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="Camera.h">
|
||||
@@ -135,10 +141,10 @@
|
||||
<ClInclude Include="Ragdoll.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="SurfaceTexture.h">
|
||||
<ClInclude Include="Console.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="TexturePainter.h">
|
||||
<ClInclude Include="SurfaceTexture.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Gui.h">
|
||||
@@ -195,10 +201,10 @@
|
||||
<ClInclude Include="CharacterManager.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="BodyComponentActions.h">
|
||||
<ClInclude Include="CharacterAction.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="DefaultBodyComponents.h">
|
||||
<ClInclude Include="CharacterMind.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Animator.h">
|
||||
|
||||
+23
-67
@@ -63,65 +63,23 @@ uniform float envIntensity = 0.8;
|
||||
|
||||
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;
|
||||
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);
|
||||
return c * c;
|
||||
}
|
||||
|
||||
float GeometrySchlickGGX(float NdotV, float rough)
|
||||
{
|
||||
float r = rough + 1.0;
|
||||
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)
|
||||
// Light from one direction, L points from the surface toward the light.
|
||||
// diffuse and specular are already scaled for this pixel, see main.
|
||||
vec3 ShadeLight(vec3 L, vec3 radiance, vec3 N, vec3 V, vec3 diffuse, vec3 specular, float power)
|
||||
{
|
||||
float NdotL = max(dot(N, L), 0.0);
|
||||
if (NdotL <= 0.0)
|
||||
return vec3(0.0);
|
||||
|
||||
vec3 H = normalize(V + L);
|
||||
vec3 F = FresnelSchlick(max(dot(H, V), 0.0), F0);
|
||||
float NDF = DistributionGGX(N, H, rough);
|
||||
float G = GeometrySmith(N, V, L, rough);
|
||||
vec3 specular = (NDF * G * F) / max(4.0 * NdotV * NdotL, 1e-4);
|
||||
vec3 kD = (vec3(1.0) - F) * (1.0 - metal);
|
||||
return (kD * albedo / PI + specular) * radiance * NdotL;
|
||||
float highlight = pow(max(dot(N, H), 0.0), power);
|
||||
return (diffuse + specular * highlight) * radiance * NdotL;
|
||||
}
|
||||
|
||||
// Inverse square falloff, windowed so it reaches exactly zero at the radius
|
||||
@@ -196,7 +154,7 @@ void main()
|
||||
albedoSample = texture(tex, texCoord);
|
||||
if (albedoSample.a < 0.1)
|
||||
discard;
|
||||
albedoSample.rgb = pow(albedoSample.rgb, vec3(2.2));
|
||||
albedoSample.rgb = ToLinear(albedoSample.rgb);
|
||||
}
|
||||
|
||||
albedoSample.rgb = mix(albedoSample.rgb, colorOverride.rgb, colorOverride.w);
|
||||
@@ -207,22 +165,25 @@ void main()
|
||||
|
||||
vec3 N = normalize(normal);
|
||||
vec3 V = normalize(viewPos - FragPos);
|
||||
float NdotV = max(dot(N, V), 1e-4);
|
||||
|
||||
vec3 F0 = mix(vec3(0.04), albedo, metal);
|
||||
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);
|
||||
|
||||
// Facing away from the sun has no direct light, so skip the BRDF and the 9 shadow samples
|
||||
vec3 sunL = normalize(-dirLight.direction);
|
||||
if (dot(N, sunL) > 0.0) {
|
||||
float shadow = ShadowCalculation(fragposlightspace, N, sunL);
|
||||
direct += ShadeLight(sunL, dirLight.color * dirLight.intensity, N, V, albedo, metal, rough, F0, NdotV) * shadow;
|
||||
direct += ShadeLight(sunL, dirLight.color * dirLight.intensity, N, V, diffuseBRDF, specularBRDF, power) * shadow;
|
||||
}
|
||||
|
||||
for (int i = 0; i < directionalLightCount; i++) {
|
||||
vec3 L = normalize(-directionalLights[i].direction);
|
||||
vec3 radiance = directionalLights[i].color * directionalLights[i].intensity;
|
||||
direct += ShadeLight(L, radiance, N, V, albedo, metal, rough, F0, NdotV);
|
||||
direct += ShadeLight(L, radiance, N, V, diffuseBRDF, specularBRDF, power);
|
||||
}
|
||||
|
||||
for (int i = 0; i < pointLightCount; i++) {
|
||||
@@ -237,7 +198,7 @@ void main()
|
||||
if (dot(N, L) <= 0.0)
|
||||
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)
|
||||
lit *= LocalShadow(pointLights[i].shadowTile + PointShadowFace(-toLight), FragPos, N);
|
||||
direct += lit;
|
||||
@@ -259,19 +220,14 @@ void main()
|
||||
continue;
|
||||
|
||||
vec3 radiance = spotLights[i].color * spotLights[i].intensity * PointAttenuation(distanceSquared, radius) * cone;
|
||||
vec3 lit = ShadeLight(L, radiance, N, V, albedo, metal, rough, F0, NdotV);
|
||||
vec3 lit = ShadeLight(L, radiance, N, V, diffuseBRDF, specularBRDF, power);
|
||||
if (spotLights[i].shadowTile >= 0)
|
||||
lit *= LocalShadow(spotLights[i].shadowTile, FragPos, N);
|
||||
direct += lit;
|
||||
}
|
||||
|
||||
vec3 ambientF = FresnelSchlickRoughness(NdotV, F0, rough);
|
||||
vec3 ambientKD = (vec3(1.0) - ambientF) * (1.0 - metal);
|
||||
vec3 irradiance = pow(textureLod(environmentMap, N, 7.0).rgb, vec3(2.2)) * envIntensity;
|
||||
vec3 R = reflect(-V, N);
|
||||
vec3 prefiltered = pow(textureLod(environmentMap, R, rough * 7.0).rgb, vec3(2.2)) * envIntensity;
|
||||
vec3 specularIBL = prefiltered * EnvironmentBRDF(F0, rough, NdotV);
|
||||
vec3 ambient = (ambientKD * irradiance * albedo + specularIBL) * ao;
|
||||
// Ambient is the sky's blurred colour in the direction the surface faces
|
||||
vec3 ambient = ToLinear(textureLod(environmentMap, N, 7.0).rgb) * envIntensity * albedo * ao;
|
||||
|
||||
FragColor = vec4(direct + ambient + albedo * emissive * 4.0, albedoSample.a);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@ vec2 SurfaceUV()
|
||||
{
|
||||
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;
|
||||
if (n.y >= n.x && n.y >= n.z) return p.xz + uvOffset;
|
||||
if (n.x >= n.z) return p.zy + uvOffset;
|
||||
|
||||
@@ -58,65 +58,23 @@ uniform float envIntensity = 0.8;
|
||||
|
||||
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;
|
||||
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);
|
||||
return c * c;
|
||||
}
|
||||
|
||||
float GeometrySchlickGGX(float NdotV, float rough)
|
||||
{
|
||||
float r = rough + 1.0;
|
||||
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)
|
||||
// Light from one direction, L points from the surface toward the light.
|
||||
// diffuse and specular are already scaled for this pixel, see main.
|
||||
vec3 ShadeLight(vec3 L, vec3 radiance, vec3 N, vec3 V, vec3 diffuse, vec3 specular, float power)
|
||||
{
|
||||
float NdotL = max(dot(N, L), 0.0);
|
||||
if (NdotL <= 0.0)
|
||||
return vec3(0.0);
|
||||
|
||||
vec3 H = normalize(V + L);
|
||||
vec3 F = FresnelSchlick(max(dot(H, V), 0.0), F0);
|
||||
float NDF = DistributionGGX(N, H, rough);
|
||||
float G = GeometrySmith(N, V, L, rough);
|
||||
vec3 specular = (NDF * G * F) / max(4.0 * NdotV * NdotL, 1e-4);
|
||||
vec3 kD = (vec3(1.0) - F) * (1.0 - metal);
|
||||
return (kD * albedo / PI + specular) * radiance * NdotL;
|
||||
float highlight = pow(max(dot(N, H), 0.0), power);
|
||||
return (diffuse + specular * highlight) * radiance * NdotL;
|
||||
}
|
||||
|
||||
// Inverse square falloff, windowed so it reaches exactly zero at the radius
|
||||
@@ -203,22 +161,25 @@ void main()
|
||||
|
||||
vec3 N = normalize(normal);
|
||||
vec3 V = normalize(viewPos - FragPos);
|
||||
float NdotV = max(dot(N, V), 1e-4);
|
||||
|
||||
vec3 F0 = mix(vec3(0.04), albedo, metal);
|
||||
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);
|
||||
|
||||
// Facing away from the sun has no direct light, so skip the BRDF and the 9 shadow samples
|
||||
vec3 sunL = normalize(-dirLight.direction);
|
||||
if (dot(N, sunL) > 0.0) {
|
||||
float shadow = ShadowCalculation(lightSpaceMatrix * vec4(FragPos, 1.0), N, sunL);
|
||||
direct += ShadeLight(sunL, dirLight.color * dirLight.intensity, N, V, albedo, metal, rough, F0, NdotV) * shadow;
|
||||
direct += ShadeLight(sunL, dirLight.color * dirLight.intensity, N, V, diffuseBRDF, specularBRDF, power) * shadow;
|
||||
}
|
||||
|
||||
for (int i = 0; i < directionalLightCount; i++) {
|
||||
vec3 L = normalize(-directionalLights[i].direction);
|
||||
vec3 radiance = directionalLights[i].color * directionalLights[i].intensity;
|
||||
direct += ShadeLight(L, radiance, N, V, albedo, metal, rough, F0, NdotV);
|
||||
direct += ShadeLight(L, radiance, N, V, diffuseBRDF, specularBRDF, power);
|
||||
}
|
||||
|
||||
for (int i = 0; i < pointLightCount; i++) {
|
||||
@@ -233,7 +194,7 @@ void main()
|
||||
if (dot(N, L) <= 0.0)
|
||||
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)
|
||||
lit *= LocalShadow(pointLights[i].shadowTile + PointShadowFace(-toLight), FragPos, N);
|
||||
direct += lit;
|
||||
@@ -255,19 +216,14 @@ void main()
|
||||
continue;
|
||||
|
||||
vec3 radiance = spotLights[i].color * spotLights[i].intensity * PointAttenuation(distanceSquared, radius) * cone;
|
||||
vec3 lit = ShadeLight(L, radiance, N, V, albedo, metal, rough, F0, NdotV);
|
||||
vec3 lit = ShadeLight(L, radiance, N, V, diffuseBRDF, specularBRDF, power);
|
||||
if (spotLights[i].shadowTile >= 0)
|
||||
lit *= LocalShadow(spotLights[i].shadowTile, FragPos, N);
|
||||
direct += lit;
|
||||
}
|
||||
|
||||
vec3 ambientF = FresnelSchlickRoughness(NdotV, F0, rough);
|
||||
vec3 ambientKD = (vec3(1.0) - ambientF) * (1.0 - metal);
|
||||
vec3 irradiance = pow(textureLod(environmentMap, N, 7.0).rgb, vec3(2.2)) * envIntensity;
|
||||
vec3 R = reflect(-V, N);
|
||||
vec3 prefiltered = pow(textureLod(environmentMap, R, rough * 7.0).rgb, vec3(2.2)) * envIntensity;
|
||||
vec3 specularIBL = prefiltered * EnvironmentBRDF(F0, rough, NdotV);
|
||||
vec3 ambient = (ambientKD * irradiance * albedo + specularIBL) * ao;
|
||||
// Ambient is the sky's blurred colour in the direction the surface faces
|
||||
vec3 ambient = ToLinear(textureLod(environmentMap, N, 7.0).rgb) * envIntensity * albedo * ao;
|
||||
|
||||
vec3 emission = albedo * material.a * 4.0;
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ void main()
|
||||
albedo = texture(tex, TexCoord);
|
||||
if (albedo.a < 0.1)
|
||||
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);
|
||||
|
||||
@@ -27,7 +27,8 @@ vec2 SurfaceUV()
|
||||
{
|
||||
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;
|
||||
if (n.y >= n.x && n.y >= n.z) return p.xz + uvOffset;
|
||||
if (n.x >= n.z) return p.zy + uvOffset;
|
||||
|
||||
@@ -80,65 +80,81 @@ public:
|
||||
void LookAt(glm::vec3 eye, glm::vec3 target) noexcept {
|
||||
TranslateTo(eye);
|
||||
orient = -orient * target;
|
||||
version++;
|
||||
}
|
||||
|
||||
void TranslateBy(glm::vec3 e) noexcept {
|
||||
pos += e;
|
||||
version++;
|
||||
}
|
||||
|
||||
void TranslateTo(glm::vec3 e) noexcept {
|
||||
pos = e;
|
||||
version++;
|
||||
}
|
||||
|
||||
void ScaleBy(glm::vec3 e) noexcept {
|
||||
scale *= e;
|
||||
version++;
|
||||
}
|
||||
|
||||
void ScaleBy(GLfloat e) noexcept {
|
||||
scale *= e;
|
||||
version++;
|
||||
}
|
||||
|
||||
void ScaleTo(glm::vec3 e) noexcept {
|
||||
scale = e;
|
||||
version++;
|
||||
}
|
||||
|
||||
void ScaleTo(GLfloat e) noexcept {
|
||||
scale = glm::vec3(e);
|
||||
version++;
|
||||
}
|
||||
|
||||
void ScaleToX(GLfloat e) noexcept {
|
||||
scale.x = e;
|
||||
version++;
|
||||
}
|
||||
|
||||
void ScaleToY(GLfloat e) noexcept {
|
||||
scale.y = e;
|
||||
version++;
|
||||
}
|
||||
|
||||
void ScaleToZ(GLfloat e) noexcept {
|
||||
scale.z = e;
|
||||
version++;
|
||||
}
|
||||
|
||||
void RotateToQuaternion(glm::quat q) noexcept {
|
||||
orient = q;
|
||||
version++;
|
||||
}
|
||||
|
||||
void RotateByQuaternion(glm::quat q) noexcept {
|
||||
orient = q * orient;
|
||||
version++;
|
||||
}
|
||||
|
||||
void NormalizeRotation() noexcept {
|
||||
orient = glm::normalize(orient);
|
||||
version++;
|
||||
}
|
||||
|
||||
void RotateByQuaternionCumulate(glm::quat q) noexcept {
|
||||
orient = orient * q;
|
||||
version++;
|
||||
}
|
||||
|
||||
void RotateByEulerAnglesCumulate(glm::vec3 e) noexcept {
|
||||
orient = orient * glm::quat(e);
|
||||
version++;
|
||||
}
|
||||
void RotateToEulerAngles(glm::vec3 e) noexcept {
|
||||
orient = glm::quat(e);
|
||||
version++;
|
||||
}
|
||||
void RotateByEulerAngles(glm::vec3 e) noexcept {
|
||||
RotateByQuaternion(glm::quat(e));
|
||||
@@ -185,6 +201,12 @@ public:
|
||||
target.scale = scale;
|
||||
target.orient = orient;
|
||||
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 {
|
||||
@@ -199,8 +221,19 @@ public:
|
||||
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 {
|
||||
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 {
|
||||
@@ -221,6 +254,10 @@ private:
|
||||
glm::vec3 pos;
|
||||
glm::vec3 scale;
|
||||
glm::quat orient;
|
||||
|
||||
unsigned int version = 1;
|
||||
mutable unsigned int cachedMatrixVersion = 0;
|
||||
mutable glm::mat4 cachedMatrix = glm::mat4(1.0f);
|
||||
};
|
||||
|
||||
class t_package {
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include<glm/gtc/matrix_transform.hpp>
|
||||
#include<glm/gtc/type_ptr.hpp>
|
||||
#include "Structures.h"
|
||||
#include "common.h"
|
||||
#include "Window.h" // Recalculate reads the window's size
|
||||
|
||||
class t2d_package {
|
||||
|
||||
|
||||
+110
-88
@@ -349,108 +349,130 @@ bool TInTNoInfo(t& t1, t& t2) {
|
||||
//FINAL
|
||||
return true;
|
||||
}
|
||||
std::optional<TInTInfo> TInT(t& t1, t& t2) {
|
||||
std::vector<glm::vec3> worldvertices1 = {
|
||||
// Separating axis test between two boxes: the 3 face directions of each, and the 9 crossings of those.
|
||||
// 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},
|
||||
};
|
||||
std::vector<glm::vec3> worldvertices2 = worldvertices1;
|
||||
glm::mat4 t1mat = t1.GetMatrix(), t2mat = t2.GetMatrix();
|
||||
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));
|
||||
const glm::mat4 t1mat = t1.GetMatrix(), t2mat = t2.GetMatrix();
|
||||
const glm::mat4 t1inv = glm::inverse(t1mat), t2inv = glm::inverse(t2mat);
|
||||
|
||||
glm::vec3 fv1=t1.GetFrontVector(), rv1=t1.GetRightVector(), uv1=t1.GetUpVector();
|
||||
glm::vec3 fv2=t2.GetFrontVector(), rv2=t2.GetRightVector(), uv2=t2.GetUpVector();
|
||||
glm::vec3 worldvertices1[8], worldvertices2[8];
|
||||
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 = {
|
||||
fv1, uv1, rv1, fv2, uv2, rv2,
|
||||
glm::cross(fv1,fv2), glm::cross(fv1,rv2), glm::cross(fv1,uv2),
|
||||
glm::cross(rv1,fv2), glm::cross(rv1,rv2), glm::cross(rv1,uv2),
|
||||
glm::cross(uv1,fv2), glm::cross(uv1,rv2), glm::cross(uv1,uv2)
|
||||
glm::vec3 summed(0.0f);
|
||||
int inside = 0;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
if (glm::all(glm::lessThanEqual(glm::abs(glm::vec3(t1inv * glm::vec4(worldvertices2[i], 1.0f))), glm::vec3(0.5f + 1e-4f)))) {
|
||||
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 lowestoverlap = FLT_MAX;
|
||||
bool bestInfront = false;
|
||||
int bestIndex = 0;
|
||||
float bestDist = FLT_MAX;
|
||||
glm::vec3 bestPt(0.0f);
|
||||
|
||||
for (int i = 0; i < (int)allAxes.size(); i++) {
|
||||
glm::vec3 axis = allAxes[i];
|
||||
if (i >= 6) {
|
||||
float len = glm::length(axis);
|
||||
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;
|
||||
}
|
||||
}
|
||||
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]];
|
||||
|
||||
tr.overlap = lowestoverlap;
|
||||
tr.CN = allAxes[bestIndex] * (bestInfront ? 1.0f : -1.0f);
|
||||
|
||||
// POI
|
||||
glm::mat4 t1inv = glm::inverse(t1mat), t2inv = glm::inverse(t2mat);
|
||||
std::vector<glm::vec3> contactPts;
|
||||
for (const glm::vec3& v : worldvertices2) {
|
||||
glm::vec3 local = glm::vec3(t1inv * glm::vec4(v, 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); }
|
||||
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 {
|
||||
float c=glm::dot(d1,r), b=glm::dot(d1,d2), denom=a*e-b*b;
|
||||
if (e < 1e-10f) { t=0.0f; s=glm::clamp(-c/a,0.0f,1.0f); }
|
||||
else {
|
||||
float c=glm::dot(d1,r), b=glm::dot(d1,d2), denom=a*e-b*b;
|
||||
if (e < 1e-10f) { t=0.0f; s=glm::clamp(-c/a,0.0f,1.0f); }
|
||||
else {
|
||||
s = denom > 1e-10f ? glm::clamp((b*f-c*e)/denom,0.0f,1.0f) : 0.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); }
|
||||
}
|
||||
s = denom > 1e-10f ? glm::clamp((b*f-c*e)/denom,0.0f,1.0f) : 0.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 tr;
|
||||
return bestPt;
|
||||
}
|
||||
glm::quat AngleAxis(glm::vec3 axis, float angle) {
|
||||
float halfangle = angle * 0.5f;
|
||||
|
||||
+3
-1
@@ -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);
|
||||
|
||||
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);
|
||||
|
||||
glm::quat AngleAxis(glm::vec3 axis, float angle);
|
||||
|
||||
Reference in New Issue
Block a user