987 lines
34 KiB
C++
987 lines
34 KiB
C++
#include "Character.h"
|
|
#include "render/TextRenderer.h"
|
|
#include <cmath>
|
|
#include <iostream>
|
|
#include <random>
|
|
#include <array>
|
|
#include "GameConstants.h"
|
|
#include "Environment.h"
|
|
|
|
namespace ZL {
|
|
|
|
const float ATTACK_COOLDOWN_TIME = 2.3f;
|
|
extern float x;
|
|
extern float y;
|
|
const float ATTACK_RANGE = 1.25f;
|
|
|
|
void Character::loadAnimation(AnimationState state, const std::string& filename, const std::string& zipFile) {
|
|
auto& data = animations[state];
|
|
data.model.LoadFromFile(filename, zipFile);
|
|
if (!data.model.animations.empty() && !data.model.animations[0].keyFrames.empty()) {
|
|
data.totalFrames = data.model.animations[0].keyFrames.back().frame + 1;
|
|
} else {
|
|
data.totalFrames = 1;
|
|
}
|
|
}
|
|
void Character::loadBinaryAnimation(AnimationState state, const std::string& filename, const std::string& zipFile) {
|
|
auto& data = animations[state];
|
|
data.model.LoadFromBinaryFile(filename, zipFile);
|
|
if (!data.model.animations.empty() && !data.model.animations[0].keyFrames.empty()) {
|
|
data.totalFrames = data.model.animations[0].keyFrames.back().frame + 1;
|
|
}
|
|
else {
|
|
data.totalFrames = 1;
|
|
}
|
|
}
|
|
|
|
void Character::setTarget(const Eigen::Vector3f& target,
|
|
std::function<void()> onArrived) {
|
|
Eigen::Vector3f normalizedTarget(target.x(), 0.f, target.z());
|
|
|
|
const bool sameRequestedTarget =
|
|
(normalizedTarget - requestedWalkTarget).norm() <= TARGET_REPLAN_THRESHOLD;
|
|
const bool alreadyMovingToTarget =
|
|
!pathWaypoints.empty() || (walkTarget - normalizedTarget).norm() <= TARGET_REPLAN_THRESHOLD;
|
|
const bool stoppedAfterFailedPath =
|
|
pathWaypoints.empty() && (walkTarget - position).norm() <= TARGET_REPLAN_THRESHOLD;
|
|
|
|
if (!onArrived && sameRequestedTarget && (alreadyMovingToTarget || stoppedAfterFailedPath)) {
|
|
return;
|
|
}
|
|
|
|
requestedWalkTarget = normalizedTarget;
|
|
onArrivedCallback = std::move(onArrived);
|
|
|
|
if (pathPlanner) {
|
|
pathWaypoints = pathPlanner(position, normalizedTarget);
|
|
currentWaypointIndex = 0;
|
|
|
|
if (!pathWaypoints.empty()) {
|
|
for (Eigen::Vector3f& waypoint : pathWaypoints) {
|
|
waypoint.y() = 0.f;
|
|
}
|
|
walkTarget = pathWaypoints.back();
|
|
return;
|
|
}
|
|
|
|
walkTarget = Eigen::Vector3f(position.x(), 0.f, position.z());
|
|
onArrivedCallback = nullptr;
|
|
return;
|
|
}
|
|
|
|
pathWaypoints.clear();
|
|
currentWaypointIndex = 0;
|
|
walkTarget = normalizedTarget;
|
|
}
|
|
|
|
void Character::setPathPlanner(PathPlanner planner) {
|
|
pathPlanner = std::move(planner);
|
|
}
|
|
|
|
bool Character::isMoving() const
|
|
{
|
|
Eigen::Vector3f toTarget = walkTarget - position;
|
|
toTarget.y() = 0.f;
|
|
return !pathWaypoints.empty() || toTarget.norm() > WALK_THRESHOLD;
|
|
}
|
|
|
|
Eigen::Vector3f Character::getCurrentNavigationTarget() const
|
|
{
|
|
if (!pathWaypoints.empty() && currentWaypointIndex < pathWaypoints.size()) {
|
|
return pathWaypoints[currentWaypointIndex];
|
|
}
|
|
return walkTarget;
|
|
}
|
|
|
|
void Character::forceReplan()
|
|
{
|
|
if (!pathPlanner) {
|
|
return;
|
|
}
|
|
|
|
const Eigen::Vector3f normalizedTarget(requestedWalkTarget.x(), 0.f, requestedWalkTarget.z());
|
|
pathWaypoints = pathPlanner(position, normalizedTarget);
|
|
currentWaypointIndex = 0;
|
|
|
|
if (!pathWaypoints.empty()) {
|
|
for (Eigen::Vector3f& waypoint : pathWaypoints) {
|
|
waypoint.y() = 0.f;
|
|
}
|
|
walkTarget = pathWaypoints.back();
|
|
return;
|
|
}
|
|
|
|
walkTarget = Eigen::Vector3f(position.x(), 0.f, position.z());
|
|
onArrivedCallback = nullptr;
|
|
}
|
|
|
|
void Character::setTexture(std::shared_ptr<ZL::Texture> texture)
|
|
{
|
|
for (auto& animEntry : animations) {
|
|
for (const auto& name : animEntry.second.model.meshNamesOrdered) {
|
|
meshTextures[name] = texture;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Character::setTexture(const std::string& meshName, std::shared_ptr<Texture> tex) {
|
|
meshTextures[meshName] = std::move(tex);
|
|
}
|
|
/*
|
|
void Character::setTexture(std::shared_ptr<Texture> tex) {
|
|
for (auto& animEntry : animations) {
|
|
for (const auto& name : animEntry.second.model.meshNamesOrdered) {
|
|
meshTextures[name] = tex;
|
|
}
|
|
}
|
|
}
|
|
*/
|
|
AnimationState Character::resolveActiveState() const {
|
|
|
|
if (animations.count(currentState)) return currentState;
|
|
return AnimationState::STAND;
|
|
}
|
|
|
|
void Character::update(int64_t deltaMs) {
|
|
if (initialHp <= 0.f) initialHp = hp;
|
|
|
|
|
|
//weaponInitialRotation = Eigen::AngleAxisf(x/180.0, Eigen::Vector3f::UnitZ()).toRotationMatrix();
|
|
//weaponInitialRotation = Eigen::AngleAxisf(-M_PI*0.5, Eigen::Vector3f::UnitZ()).toRotationMatrix();
|
|
//weaponInitialPosition = Eigen::Vector3f(0, 0.09, 0.016);
|
|
/*if (deltaMs > 200)
|
|
{
|
|
deltaMs = 200;
|
|
}*/
|
|
Eigen::Vector3f activeTarget;
|
|
Eigen::Vector3f lookTarget;
|
|
|
|
if (attackTarget == nullptr)
|
|
{
|
|
battle_state = 0;
|
|
attack = 0;
|
|
}
|
|
|
|
if (attackTarget != nullptr)
|
|
{
|
|
float distToGhost = (attackTarget->position - position).norm();
|
|
if (distToGhost >= 10.f)
|
|
{
|
|
if (isPlayer)
|
|
{
|
|
setTarget(attackTarget->position);
|
|
}
|
|
battle_state = 0;
|
|
|
|
}
|
|
else if (distToGhost < 10.0f && distToGhost >= ATTACK_RANGE) {
|
|
setTarget(attackTarget->position);
|
|
battle_state = 0;
|
|
}
|
|
else
|
|
{
|
|
battle_state = 1;
|
|
}
|
|
}
|
|
|
|
|
|
if (!pathWaypoints.empty() && currentWaypointIndex < pathWaypoints.size()) {
|
|
activeTarget = pathWaypoints[currentWaypointIndex];
|
|
}
|
|
else
|
|
{
|
|
|
|
activeTarget = walkTarget;
|
|
}
|
|
|
|
if (battle_state == 0)
|
|
{
|
|
lookTarget = activeTarget;
|
|
}
|
|
else
|
|
{
|
|
lookTarget = attackTarget->position;
|
|
}
|
|
|
|
targetFacingAngle = facingAngle;
|
|
|
|
if (currentState == AnimationState::WALK || currentState == AnimationState::STAND)
|
|
{
|
|
Eigen::Vector3f toTarget = activeTarget - position;
|
|
toTarget.y() = 0.f;
|
|
float dist = toTarget.norm();
|
|
|
|
if (dist > WALK_THRESHOLD) {
|
|
Eigen::Vector3f dir = toTarget / dist;
|
|
float moveAmount = walkSpeed * static_cast<float>(deltaMs) / 1000.f;
|
|
if (moveAmount >= dist) {
|
|
position = activeTarget;
|
|
position.y() = 0.f;
|
|
}
|
|
else {
|
|
position += dir * moveAmount;
|
|
}
|
|
targetFacingAngle = atan2(dir.x(), -dir.z());
|
|
currentState = AnimationState::WALK;
|
|
}
|
|
else {
|
|
currentState = AnimationState::STAND;
|
|
|
|
const bool hasNextWaypoint = !pathWaypoints.empty() && currentWaypointIndex + 1 < pathWaypoints.size();
|
|
if (hasNextWaypoint) {
|
|
++currentWaypointIndex;
|
|
}
|
|
else {
|
|
pathWaypoints.clear();
|
|
currentWaypointIndex = 0;
|
|
}
|
|
|
|
if (!hasNextWaypoint && onArrivedCallback) {
|
|
auto cb = std::move(onArrivedCallback);
|
|
onArrivedCallback = nullptr;
|
|
cb();
|
|
}
|
|
|
|
// While standing, optionally orient toward another character (e.g. the
|
|
// player when this NPC is being talked to). The smoothed rotation block
|
|
// below converges facingAngle to targetFacingAngle.
|
|
if (faceTarget) {
|
|
Eigen::Vector3f toFace = faceTarget->position - position;
|
|
toFace.y() = 0.f;
|
|
float d = toFace.norm();
|
|
if (d > 1e-3f) {
|
|
targetFacingAngle = atan2(toFace.x() / d, -toFace.z() / d);
|
|
}
|
|
else {
|
|
targetFacingAngle = facingAngle;
|
|
}
|
|
}
|
|
else {
|
|
targetFacingAngle = facingAngle;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hp <= 0)
|
|
{
|
|
if (currentState != AnimationState::ACTION_TO_DEATH && currentState != AnimationState::DEATH_IDLE)
|
|
{
|
|
currentState = AnimationState::ACTION_TO_DEATH;
|
|
resetAnim = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
|
|
if (battle_state == 1)
|
|
{
|
|
targetFacingAngle = atan2(lookTarget.x() - position.x(), -(lookTarget.z() - position.z()));
|
|
|
|
if (currentState == AnimationState::STAND || currentState == AnimationState::WALK) {
|
|
currentState = AnimationState::STAND_TO_ACTION;
|
|
resetAnim = true;
|
|
}
|
|
|
|
if (canAttack && attack == 0 && attack_cooldown < 0.f && currentState == AnimationState::ACTION_IDLE)
|
|
{
|
|
attack = 1;
|
|
attack_cooldown = ATTACK_COOLDOWN_TIME;
|
|
}
|
|
if (canAttack && attack_cooldown >= 0.f)
|
|
{
|
|
attack_cooldown = attack_cooldown - deltaMs / 1000.f;
|
|
}
|
|
|
|
if (attack == 1 && currentState == AnimationState::ACTION_IDLE)
|
|
{
|
|
if (attackTarget != nullptr && attackTarget->hp > 0)
|
|
{
|
|
currentState = AnimationState::ACTION_ATTACK;
|
|
resetAnim = true;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (currentState == AnimationState::STAND_TO_ACTION
|
|
|| currentState == AnimationState::ACTION_IDLE
|
|
|| currentState == AnimationState::ACTION_ATTACK
|
|
) {
|
|
currentState = AnimationState::ACTION_TO_STAND;
|
|
resetAnim = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Rotate toward target facing angle at constant angular speed
|
|
float angleDiff = targetFacingAngle - facingAngle;
|
|
while (angleDiff > static_cast<float>(M_PI)) angleDiff -= 2.f * static_cast<float>(M_PI);
|
|
while (angleDiff < -static_cast<float>(M_PI)) angleDiff += 2.f * static_cast<float>(M_PI);
|
|
float rotStep = rotationSpeed * static_cast<float>(deltaMs) / 1000.f;
|
|
if (std::fabs(angleDiff) <= rotStep) {
|
|
facingAngle = targetFacingAngle;
|
|
// One-shot face-target: once aligned, stop tracking. Otherwise the NPC
|
|
// would keep snapping to the player every tick after the conversation
|
|
// started, even as the player moved away.
|
|
faceTarget = nullptr;
|
|
} else {
|
|
facingAngle += (angleDiff > 0.f ? rotStep : -rotStep);
|
|
}
|
|
|
|
|
|
|
|
if (hitSparkEmitter.isConfigured()) {
|
|
hitSparkEmitter.update(static_cast<float>(deltaMs));
|
|
}
|
|
|
|
auto it = animations.find(currentState);
|
|
if (it == animations.end()) return;
|
|
|
|
auto& anim = it->second;
|
|
|
|
if (resetAnim)
|
|
{
|
|
resetAnim = false;
|
|
anim.currentFrame = 0;
|
|
}
|
|
//19
|
|
int prevFrame = anim.currentFrame;
|
|
anim.currentFrame += static_cast<float>(deltaMs) / 24.f;
|
|
/*
|
|
if (npcId == "ghost_01x")
|
|
{
|
|
std::cout << "Current animation frame: " << anim.currentFrame << " / " << anim.totalFrames << " -- " << anim.lastFrame << std::endl;
|
|
}*/
|
|
|
|
if (static_cast<int>(anim.currentFrame) >= 20 && currentState == AnimationState::STAND_TO_ACTION)
|
|
{
|
|
showWeapon = true;
|
|
}
|
|
else if (static_cast<int>(anim.currentFrame) <= 32 && currentState == AnimationState::ACTION_TO_STAND)
|
|
{
|
|
showWeapon = true;
|
|
}
|
|
else if (currentState == AnimationState::ACTION_ATTACK_2 || currentState == AnimationState::ACTION_ATTACK || currentState == AnimationState::ACTION_IDLE)
|
|
{
|
|
showWeapon = true;
|
|
}
|
|
else
|
|
{
|
|
showWeapon = false;
|
|
}
|
|
|
|
auto attackDirectionToTarget = [this]() {
|
|
Eigen::Vector3f dir = attackTarget->position - position;
|
|
dir.y() = 0.f;
|
|
float n = dir.norm();
|
|
if (n > 1e-3f) dir /= n;
|
|
else dir = Eigen::Vector3f::Zero();
|
|
return dir;
|
|
};
|
|
|
|
if (isPlayer)
|
|
{
|
|
if (prevFrame < 19 && static_cast<int>(anim.currentFrame) >= 19 && (currentState == AnimationState::ACTION_ATTACK || currentState == AnimationState::ACTION_ATTACK_2))
|
|
{
|
|
if (attackTarget != nullptr)
|
|
{
|
|
attackTarget->applyDamage(10.f, attackDirectionToTarget());
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (prevFrame < 50 && static_cast<int>(anim.currentFrame) >= 50 && (currentState == AnimationState::ACTION_ATTACK || currentState == AnimationState::ACTION_ATTACK_2))
|
|
{
|
|
if (attackTarget != nullptr)
|
|
{
|
|
attackTarget->applyDamage(10.f, attackDirectionToTarget());
|
|
}
|
|
}
|
|
}
|
|
|
|
int frms = anim.model.animations[0].keyFrames[anim.model.animations[0].keyFrames.size() - 1].frame;
|
|
if (static_cast<int>(anim.currentFrame) >= frms) {
|
|
anim.currentFrame = anim.model.startingFrame;
|
|
|
|
if (currentState == AnimationState::STAND_TO_ACTION)
|
|
{
|
|
currentState = AnimationState::ACTION_IDLE;
|
|
//resetAnim = true;
|
|
}
|
|
if (currentState == AnimationState::ACTION_TO_STAND)
|
|
{
|
|
currentState = AnimationState::STAND;
|
|
//resetAnim = true;
|
|
}
|
|
|
|
if (currentState == AnimationState::ACTION_ATTACK)
|
|
{
|
|
currentState = AnimationState::ACTION_IDLE;
|
|
//resetAnim = true;
|
|
attack = 0;
|
|
}
|
|
|
|
if (currentState == AnimationState::ACTION_TO_DEATH)
|
|
{
|
|
currentState = AnimationState::DEATH_IDLE;
|
|
//resetAnim = true;
|
|
}
|
|
}
|
|
|
|
|
|
if (static_cast<int>(anim.currentFrame) != anim.lastFrame) {
|
|
anim.gpuSkinningShaderData.ComputeSkinningMatrices(anim.model.startBones, anim.model.animations[0].keyFrames, static_cast<int>(anim.currentFrame));
|
|
if (!useGpuSkinning) {
|
|
anim.model.Interpolate(static_cast<int>(anim.currentFrame));
|
|
}
|
|
anim.lastFrame = static_cast<int>(anim.currentFrame);
|
|
}
|
|
}
|
|
|
|
void Character::draw(Renderer& renderer) {
|
|
// Sparks are world-space billboards — render under the caller's camera matrices.
|
|
if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) {
|
|
hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false);
|
|
}
|
|
|
|
if (!isPlayer && hp <= 0)
|
|
{
|
|
return;
|
|
}
|
|
if (useGpuSkinning) {
|
|
drawGpuSkinning(renderer);
|
|
return;
|
|
}
|
|
|
|
AnimationState drawState = resolveActiveState();
|
|
auto it = animations.find(drawState);
|
|
if (it == animations.end()) return;
|
|
|
|
renderer.shaderManager.PushShader("fog");
|
|
renderer.RenderUniform1i(textureUniformName, 0);
|
|
const float playerEyePos[3] = { 0.0f, 0.0f, -Environment::zoom };
|
|
renderer.RenderUniform3fv("uPlayerEyePos", playerEyePos);
|
|
|
|
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
|
|
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
|
|
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
|
|
|
renderer.PushMatrix();
|
|
renderer.TranslateMatrix({ position.x(), position.y(), position.z() });
|
|
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
|
renderer.ScaleMatrix(modelScale);
|
|
renderer.RotateMatrix(modelCorrectionRotation.toRotationMatrix());
|
|
|
|
auto& anim = it->second;
|
|
for (const auto& name : anim.model.meshNamesOrdered) {
|
|
auto mit = anim.model.meshes.find(name);
|
|
if (mit == anim.model.meshes.end()) continue;
|
|
auto tit = meshTextures.find(name);
|
|
if (tit == meshTextures.end() || !tit->second) continue;
|
|
glBindTexture(GL_TEXTURE_2D, tit->second->getTexID());
|
|
modelMutable.AssignFrom(mit->second.mesh);
|
|
modelMutable.RefreshVBO();
|
|
renderer.DrawVertexRenderStruct(modelMutable);
|
|
}
|
|
|
|
renderer.PopMatrix();
|
|
|
|
drawAttachedWeapon(renderer);
|
|
|
|
renderer.PopProjectionMatrix();
|
|
renderer.shaderManager.PopShader();
|
|
}
|
|
|
|
void Character::drawGpuSkinning(Renderer& renderer) {
|
|
AnimationState drawState = resolveActiveState();
|
|
auto it = animations.find(drawState);
|
|
if (it == animations.end())
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto& anim = it->second;
|
|
anim.gpuSkinningShaderData.prepareGpuSkinningVBOs(anim.model);
|
|
|
|
if (anim.gpuSkinningShaderData.skinningMatrices.empty())
|
|
{
|
|
if (anim.model.animations.empty() || anim.model.animations[0].keyFrames.empty()) return;
|
|
|
|
anim.gpuSkinningShaderData.ComputeSkinningMatrices(anim.model.startBones, anim.model.animations[0].keyFrames, static_cast<int>(anim.currentFrame));
|
|
|
|
if (anim.gpuSkinningShaderData.skinningMatrices.empty()) return;
|
|
}
|
|
static const std::string skinningShaderName = "fog_skinning";
|
|
static const std::string boneMatricesUniform = "uBoneMatrices[0]";
|
|
|
|
renderer.shaderManager.PushShader(skinningShaderName);
|
|
renderer.RenderUniform1i(textureUniformName, 0);
|
|
const float playerEyePosSkin[3] = { 0.0f, 0.0f, -Environment::zoom };
|
|
renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosSkin);
|
|
|
|
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
|
|
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
|
|
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
|
|
|
renderer.PushMatrix();
|
|
renderer.TranslateMatrix({ position.x(), position.y(), position.z() });
|
|
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
|
renderer.ScaleMatrix(modelScale);
|
|
renderer.RotateMatrix(modelCorrectionRotation.toRotationMatrix());
|
|
|
|
// Upload bone skinning matrices
|
|
renderer.RenderUniformMatrix4fvArray(boneMatricesUniform,
|
|
static_cast<int>(anim.gpuSkinningShaderData.skinningMatrices.size()), false,
|
|
anim.gpuSkinningShaderData.skinningMatrices[0].data());
|
|
|
|
for (const auto& name : anim.model.meshNamesOrdered) {
|
|
auto pit = anim.gpuSkinningShaderData.perMesh.find(name);
|
|
if (pit == anim.gpuSkinningShaderData.perMesh.end()) continue;
|
|
auto tit = meshTextures.find(name);
|
|
if (tit == meshTextures.end() || !tit->second) continue;
|
|
glBindTexture(GL_TEXTURE_2D, tit->second->getTexID());
|
|
pit->second.RenderVBO(renderer);
|
|
}
|
|
|
|
renderer.PopMatrix();
|
|
|
|
renderer.shaderManager.PushShader("fog");
|
|
renderer.RenderUniform1i(textureUniformName, 0);
|
|
renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosSkin);
|
|
drawAttachedWeapon(renderer);
|
|
renderer.shaderManager.PopShader();
|
|
|
|
renderer.PopProjectionMatrix();
|
|
renderer.shaderManager.PopShader();
|
|
}
|
|
|
|
bool Character::prepareGpuSkinning()
|
|
{
|
|
AnimationState drawState = resolveActiveState();
|
|
auto it = animations.find(drawState);
|
|
if (it == animations.end())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
auto& anim = it->second;
|
|
anim.gpuSkinningShaderData.prepareGpuSkinningVBOs(anim.model);
|
|
if (anim.gpuSkinningShaderData.skinningMatrices.empty()) {
|
|
if (anim.model.animations.empty() || anim.model.animations[0].keyFrames.empty())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
anim.gpuSkinningShaderData.ComputeSkinningMatrices(anim.model.startBones, anim.model.animations[0].keyFrames, static_cast<int>(anim.currentFrame));
|
|
|
|
if (anim.gpuSkinningShaderData.skinningMatrices.empty())
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void Character::drawAttachedWeapon(Renderer& renderer)
|
|
{
|
|
if (!showWeapon) return;
|
|
if (weaponMesh.data.PositionData.size() == 0 || !weaponTexture) return;
|
|
|
|
auto it = animations.find(resolveActiveState());
|
|
if (it == animations.end()) return;
|
|
|
|
auto& anim = it->second;
|
|
if (anim.gpuSkinningShaderData.skinningMatrices.empty()) return;
|
|
|
|
int boneIdx = anim.model.findBoneIndex(weaponAttachBoneName);
|
|
if (boneIdx < 0) return;
|
|
|
|
const Eigen::Matrix4f& bindBone = anim.model.animations[0].keyFrames[0].bones[boneIdx].boneMatrixWorld;
|
|
Eigen::Matrix4f currentBone = anim.gpuSkinningShaderData.skinningMatrices[boneIdx] * bindBone;
|
|
|
|
renderer.PushMatrix();
|
|
renderer.TranslateMatrix({ position.x(), position.y(), position.z() });
|
|
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
|
renderer.ScaleMatrix(modelScale);
|
|
renderer.RotateMatrix(modelCorrectionRotation.toRotationMatrix());
|
|
renderer.TranslateMatrix(Eigen::Vector3f(currentBone.block<3, 1>(0, 3)));
|
|
renderer.RotateMatrix(Eigen::Matrix3f(currentBone.block<3, 3>(0, 0)));
|
|
renderer.TranslateMatrix(weaponInitialPosition);
|
|
renderer.RotateMatrix(weaponInitialRotation);
|
|
|
|
glBindTexture(GL_TEXTURE_2D, weaponTexture->getTexID());
|
|
renderer.DrawVertexRenderStruct(weaponMesh);
|
|
renderer.PopMatrix();
|
|
}
|
|
|
|
// ==================== Shadow depth pass ====================
|
|
|
|
void Character::drawShadowDepth(Renderer& renderer) {
|
|
if (!isPlayer && hp <= 0)
|
|
{
|
|
return;
|
|
}
|
|
if (useGpuSkinning) {
|
|
drawShadowDepthGpuSkinning(renderer);
|
|
} else {
|
|
drawShadowDepthCpu(renderer);
|
|
}
|
|
}
|
|
|
|
void Character::drawShadowDepthCpu(Renderer& renderer) {
|
|
AnimationState drawState = resolveActiveState();
|
|
auto it = animations.find(drawState);
|
|
if (it == animations.end()) return;
|
|
|
|
// The caller has already pushed the shadow_depth shader and the
|
|
// light's projection + view onto the renderer stacks.
|
|
renderer.PushMatrix();
|
|
renderer.TranslateMatrix({ position.x(), position.y(), position.z() });
|
|
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
|
renderer.ScaleMatrix(modelScale);
|
|
renderer.RotateMatrix(modelCorrectionRotation.toRotationMatrix());
|
|
|
|
auto& anim = it->second;
|
|
for (const auto& name : anim.model.meshNamesOrdered) {
|
|
auto mit = anim.model.meshes.find(name);
|
|
if (mit == anim.model.meshes.end()) continue;
|
|
auto tit = meshTextures.find(name);
|
|
if (tit == meshTextures.end() || !tit->second) continue;
|
|
modelMutable.AssignFrom(mit->second.mesh);
|
|
modelMutable.RefreshVBO();
|
|
renderer.DrawVertexRenderStruct(modelMutable);
|
|
}
|
|
renderer.PopMatrix();
|
|
|
|
drawAttachedWeapon(renderer);
|
|
}
|
|
|
|
void Character::drawShadowDepthGpuSkinning(Renderer& renderer) {
|
|
|
|
CheckGlError(__FILE__, __LINE__);
|
|
AnimationState drawState = resolveActiveState();
|
|
auto it = animations.find(drawState);
|
|
if (it == animations.end()) return;
|
|
|
|
CheckGlError(__FILE__, __LINE__);
|
|
|
|
if (!prepareGpuSkinning()) return;
|
|
|
|
CheckGlError(__FILE__, __LINE__);
|
|
|
|
static const std::string shadowSkinningShader = "shadow_depth_skinning";
|
|
static const std::string boneMatricesUniform = "uBoneMatrices[0]";
|
|
|
|
// Switch to the skinning depth shader (caller has the static depth shader active).
|
|
renderer.shaderManager.PushShader(shadowSkinningShader);
|
|
|
|
renderer.PushMatrix();
|
|
renderer.TranslateMatrix({ position.x(), position.y(), position.z() });
|
|
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
|
renderer.ScaleMatrix(modelScale);
|
|
renderer.RotateMatrix(modelCorrectionRotation.toRotationMatrix());
|
|
|
|
CheckGlError(__FILE__, __LINE__);
|
|
renderer.RenderUniformMatrix4fvArray(boneMatricesUniform,
|
|
static_cast<int>(it->second.gpuSkinningShaderData.skinningMatrices.size()), false,
|
|
it->second.gpuSkinningShaderData.skinningMatrices[0].data());
|
|
|
|
CheckGlError(__FILE__, __LINE__);
|
|
for (const auto& name : it->second.model.meshNamesOrdered) {
|
|
auto pit = it->second.gpuSkinningShaderData.perMesh.find(name);
|
|
if (pit == it->second.gpuSkinningShaderData.perMesh.end()) continue;
|
|
auto tit = meshTextures.find(name);
|
|
if (tit == meshTextures.end() || !tit->second) continue;
|
|
pit->second.RenderVBO(renderer);
|
|
}
|
|
|
|
CheckGlError(__FILE__, __LINE__);
|
|
renderer.PopMatrix();
|
|
renderer.shaderManager.PopShader();
|
|
|
|
drawAttachedWeapon(renderer);
|
|
|
|
CheckGlError(__FILE__, __LINE__);
|
|
}
|
|
|
|
// ==================== Main pass with shadows ====================
|
|
|
|
void Character::drawWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera) {
|
|
// Sparks are world-space billboards — render under the caller's camera matrices.
|
|
if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) {
|
|
hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false);
|
|
}
|
|
|
|
if (!isPlayer && hp <= 0)
|
|
{
|
|
return;
|
|
}
|
|
if (useGpuSkinning) {
|
|
drawGpuSkinningWithShadow(renderer, lightFromCamera, shadowMapTex, lightDirCamera);
|
|
} else {
|
|
drawCpuWithShadow(renderer, lightFromCamera, shadowMapTex, lightDirCamera);
|
|
}
|
|
}
|
|
|
|
void Character::drawCpuWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera) {
|
|
AnimationState drawState = resolveActiveState();
|
|
auto it = animations.find(drawState);
|
|
if (it == animations.end()) return;
|
|
|
|
static const std::string shadowShader = "fog_shadow";
|
|
|
|
renderer.shaderManager.PushShader(shadowShader);
|
|
renderer.RenderUniform1i(textureUniformName, 0);
|
|
renderer.RenderUniform1i("uShadowMap", 1);
|
|
renderer.RenderUniformMatrix4fv("uLightFromCamera", false, lightFromCamera.data());
|
|
renderer.RenderUniform3fv("uLightDir", lightDirCamera.data());
|
|
const float playerEyePosShadow[3] = { 0.0f, 0.0f, -Environment::zoom };
|
|
renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosShadow);
|
|
|
|
glActiveTexture(GL_TEXTURE1);
|
|
glBindTexture(GL_TEXTURE_2D, shadowMapTex);
|
|
glActiveTexture(GL_TEXTURE0);
|
|
|
|
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
|
|
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
|
|
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
|
|
|
renderer.PushMatrix();
|
|
renderer.TranslateMatrix({ position.x(), position.y(), position.z() });
|
|
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
|
renderer.ScaleMatrix(modelScale);
|
|
renderer.RotateMatrix(modelCorrectionRotation.toRotationMatrix());
|
|
|
|
auto& anim = it->second;
|
|
for (const auto& name : anim.model.meshNamesOrdered) {
|
|
auto mit = anim.model.meshes.find(name);
|
|
if (mit == anim.model.meshes.end()) continue;
|
|
auto tit = meshTextures.find(name);
|
|
if (tit == meshTextures.end() || !tit->second) continue;
|
|
glBindTexture(GL_TEXTURE_2D, tit->second->getTexID());
|
|
modelMutable.AssignFrom(mit->second.mesh);
|
|
modelMutable.RefreshVBO();
|
|
renderer.DrawVertexRenderStruct(modelMutable);
|
|
}
|
|
|
|
renderer.PopMatrix();
|
|
|
|
renderer.shaderManager.PushShader("fog");
|
|
renderer.RenderUniform1i(textureUniformName, 0);
|
|
renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosShadow);
|
|
drawAttachedWeapon(renderer);
|
|
renderer.shaderManager.PopShader();
|
|
|
|
renderer.PopProjectionMatrix();
|
|
renderer.shaderManager.PopShader();
|
|
}
|
|
|
|
void Character::drawGpuSkinningWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera) {
|
|
AnimationState drawState = resolveActiveState();
|
|
auto it = animations.find(drawState);
|
|
if (it == animations.end()) return;
|
|
|
|
CheckGlError(__FILE__, __LINE__);
|
|
|
|
if (!prepareGpuSkinning()) return;
|
|
|
|
CheckGlError(__FILE__, __LINE__);
|
|
|
|
static const std::string skinningShadowShader = "fog_skinning_shadow";
|
|
static const std::string boneMatricesUniform = "uBoneMatrices[0]";
|
|
|
|
renderer.shaderManager.PushShader(skinningShadowShader);
|
|
renderer.RenderUniform1i(textureUniformName, 0);
|
|
renderer.RenderUniform1i("uShadowMap", 1);
|
|
renderer.RenderUniformMatrix4fv("uLightFromCamera", false, lightFromCamera.data());
|
|
renderer.RenderUniform3fv("uLightDir", lightDirCamera.data());
|
|
const float playerEyePosSkinShadow[3] = { 0.0f, 0.0f, -Environment::zoom };
|
|
renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosSkinShadow);
|
|
|
|
CheckGlError(__FILE__, __LINE__);
|
|
|
|
glActiveTexture(GL_TEXTURE1);
|
|
glBindTexture(GL_TEXTURE_2D, shadowMapTex);
|
|
glActiveTexture(GL_TEXTURE0);
|
|
|
|
CheckGlError(__FILE__, __LINE__);
|
|
|
|
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
|
|
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
|
|
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
|
|
|
renderer.PushMatrix();
|
|
renderer.TranslateMatrix({ position.x(), position.y(), position.z() });
|
|
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
|
renderer.ScaleMatrix(modelScale);
|
|
renderer.RotateMatrix(modelCorrectionRotation.toRotationMatrix());
|
|
|
|
CheckGlError(__FILE__, __LINE__);
|
|
renderer.RenderUniformMatrix4fvArray(boneMatricesUniform,
|
|
static_cast<int>(it->second.gpuSkinningShaderData.skinningMatrices.size()), false,
|
|
it->second.gpuSkinningShaderData.skinningMatrices[0].data());
|
|
|
|
CheckGlError(__FILE__, __LINE__);
|
|
|
|
for (const auto& name : it->second.model.meshNamesOrdered) {
|
|
auto pit = it->second.gpuSkinningShaderData.perMesh.find(name);
|
|
if (pit == it->second.gpuSkinningShaderData.perMesh.end()) continue;
|
|
auto tit = meshTextures.find(name);
|
|
if (tit == meshTextures.end() || !tit->second) continue;
|
|
glBindTexture(GL_TEXTURE_2D, tit->second->getTexID());
|
|
pit->second.RenderVBO(renderer);
|
|
}
|
|
|
|
renderer.PopMatrix();
|
|
|
|
renderer.shaderManager.PushShader("fog");
|
|
renderer.RenderUniform1i(textureUniformName, 0);
|
|
renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosSkinShadow);
|
|
drawAttachedWeapon(renderer);
|
|
renderer.shaderManager.PopShader();
|
|
|
|
CheckGlError(__FILE__, __LINE__);
|
|
|
|
|
|
renderer.PopProjectionMatrix();
|
|
renderer.shaderManager.PopShader();
|
|
|
|
CheckGlError(__FILE__, __LINE__);
|
|
}
|
|
|
|
void Character::setupHitSparks(std::shared_ptr<Texture> sparkTexture)
|
|
{
|
|
hitSparkEmitter.setTexture(sparkTexture);
|
|
hitSparkEmitter.setShaderProgramName("spark");
|
|
hitSparkEmitter.setMaxParticles(64);
|
|
hitSparkEmitter.setParticleSize(0.08f);
|
|
hitSparkEmitter.setBiasX(0.0f);
|
|
hitSparkEmitter.setEmissionRate(1.0e9f); // effectively disables auto-emission
|
|
hitSparkEmitter.setSpeedRange(2.0f, 4.0f);
|
|
hitSparkEmitter.setZSpeedRange(0.5f, 2.0f);
|
|
hitSparkEmitter.setScaleRange(0.8f, 1.2f);
|
|
hitSparkEmitter.setLifeTimeRange(300.0f, 600.0f);
|
|
hitSparkEmitter.setIsActive(false);
|
|
hitSparkEmitter.markConfigured();
|
|
}
|
|
|
|
void Character::applyDamage(float damageAmount, const Eigen::Vector3f& attackDirection)
|
|
{
|
|
hp = hp - damageAmount;
|
|
if (hp < 0) hp = 0;
|
|
|
|
if (!hitSparkEmitter.isConfigured()) return;
|
|
|
|
Eigen::Vector3f emitPos = position + Eigen::Vector3f(0.f, 1.f, 0.f);
|
|
hitSparkEmitter.resetEmissionPoints({ emitPos });
|
|
hitSparkEmitter.setEmissionDirection(attackDirection);
|
|
|
|
static std::mt19937 gen(std::random_device{}());
|
|
std::uniform_int_distribution<int> countDist(10, 15);
|
|
int count = countDist(gen);
|
|
for (int i = 0; i < count; ++i) {
|
|
hitSparkEmitter.emit();
|
|
}
|
|
}
|
|
|
|
void Character::prepareHitSparksForDraw(const Eigen::Matrix4f& cameraViewMatrix)
|
|
{
|
|
if (!hitSparkEmitter.isConfigured()) return;
|
|
hitSparkEmitter.prepareForDraw(cameraViewMatrix);
|
|
}
|
|
|
|
void Character::drawName(TextRenderer& textRenderer,
|
|
const Eigen::Matrix4f& cameraViewMatrix,
|
|
const Eigen::Matrix4f& projectionMatrix)
|
|
{
|
|
if (isPlayer) return;
|
|
if (hp <= 0.f) return;
|
|
if (npcName.empty()) return;
|
|
|
|
Eigen::Vector4f worldPos(position.x(), position.y() + 1.83f, position.z(), 1.f);
|
|
Eigen::Vector4f clip = projectionMatrix * cameraViewMatrix * worldPos;
|
|
if (clip.w() <= 0.f) return; // behind the camera
|
|
|
|
float ndcX = clip.x() / clip.w();
|
|
float ndcY = clip.y() / clip.w();
|
|
if (ndcX < -1.f || ndcX > 1.f || ndcY < -1.f || ndcY > 1.f) return; // off-screen
|
|
|
|
float screenX = (ndcX * 0.5f + 0.5f) * Environment::projectionWidth;
|
|
float screenY = (ndcY * 0.5f + 0.5f) * Environment::projectionHeight;
|
|
|
|
std::array<float, 4> color = canAttack
|
|
? std::array<float, 4>{ 1.f, 0.f, 0.f, 1.f }
|
|
: std::array<float, 4>{ 0.f, 1.f, 0.f, 1.f };
|
|
|
|
textRenderer.drawText(npcName, screenX, screenY, 1.0f, true, color);
|
|
}
|
|
|
|
void Character::drawHealthBar(Renderer& renderer,
|
|
const Eigen::Matrix4f& cameraViewMatrix,
|
|
const Eigen::Matrix4f& projectionMatrix)
|
|
{
|
|
if (!isPlayer && !canAttack) return;
|
|
if (hp <= 0.f) return;
|
|
if (initialHp <= 0.f) return;
|
|
|
|
Eigen::Vector4f worldPos(position.x(), position.y() + 1.75f, position.z(), 1.f);
|
|
Eigen::Vector4f clip = projectionMatrix * cameraViewMatrix * worldPos;
|
|
if (clip.w() <= 0.f) return;
|
|
float ndcX = clip.x() / clip.w();
|
|
float ndcY = clip.y() / clip.w();
|
|
if (ndcX < -1.2f || ndcX > 1.2f || ndcY < -1.2f || ndcY > 1.2f) return;
|
|
|
|
const float sx = (ndcX * 0.5f + 0.5f) * Environment::projectionWidth;
|
|
const float sy = (ndcY * 0.5f + 0.5f) * Environment::projectionHeight;
|
|
|
|
const float barW = 60.f;
|
|
const float barH = 6.f;
|
|
|
|
const float left = sx - barW * 0.5f;
|
|
const float right = sx + barW * 0.5f;
|
|
const float bottom = sy - barH * 0.5f;
|
|
const float top = sy + barH * 0.5f;
|
|
|
|
float frac = hp / initialHp;
|
|
if (frac < 0.f) frac = 0.f;
|
|
if (frac > 1.f) frac = 1.f;
|
|
const float split = left + barW * frac;
|
|
|
|
VertexDataStruct data;
|
|
const Eigen::Vector3f green(0.f, 1.f, 0.f);
|
|
const Eigen::Vector3f red (1.f, 0.f, 0.f);
|
|
|
|
auto pushQuad = [&](float x0, float x1, const Eigen::Vector3f& col) {
|
|
data.PositionData.push_back({ x0, bottom, 0.f });
|
|
data.PositionData.push_back({ x1, bottom, 0.f });
|
|
data.PositionData.push_back({ x1, top, 0.f });
|
|
data.PositionData.push_back({ x0, bottom, 0.f });
|
|
data.PositionData.push_back({ x1, top, 0.f });
|
|
data.PositionData.push_back({ x0, top, 0.f });
|
|
for (int i = 0; i < 6; ++i) data.ColorData.push_back(col);
|
|
};
|
|
|
|
if (frac > 0.f) pushQuad(left, split, green);
|
|
if (frac < 1.f) pushQuad(split, right, red);
|
|
|
|
healthBarMesh.AssignFrom(data);
|
|
healthBarMesh.RefreshVBO();
|
|
|
|
renderer.shaderManager.PushShader("defaultColor");
|
|
renderer.PushProjectionMatrix(0.f, Environment::projectionWidth,
|
|
0.f, Environment::projectionHeight,
|
|
-1.f, 1.f);
|
|
renderer.PushMatrix();
|
|
renderer.LoadIdentity();
|
|
|
|
renderer.DrawVertexRenderStruct(healthBarMesh);
|
|
|
|
renderer.PopMatrix();
|
|
renderer.PopProjectionMatrix();
|
|
renderer.shaderManager.PopShader();
|
|
}
|
|
|
|
} // namespace ZL
|