#include "Character.h" #include "render/TextRenderer.h" #include #include #include #include #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 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 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 tex) { meshTextures[meshName] = std::move(tex); } /* void Character::setTexture(std::shared_ptr 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(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(M_PI)) angleDiff -= 2.f * static_cast(M_PI); while (angleDiff < -static_cast(M_PI)) angleDiff += 2.f * static_cast(M_PI); float rotStep = rotationSpeed * static_cast(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(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(deltaMs) / 24.f; /* if (npcId == "ghost_01x") { std::cout << "Current animation frame: " << anim.currentFrame << " / " << anim.totalFrames << " -- " << anim.lastFrame << std::endl; }*/ if (static_cast(anim.currentFrame) >= 20 && currentState == AnimationState::STAND_TO_ACTION) { showWeapon = true; } else if (static_cast(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(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(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(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(anim.currentFrame) != anim.lastFrame) { anim.gpuSkinningShaderData.ComputeSkinningMatrices(anim.model.startBones, anim.model.animations[0].keyFrames, static_cast(anim.currentFrame)); if (!useGpuSkinning) { anim.model.Interpolate(static_cast(anim.currentFrame)); } anim.lastFrame = static_cast(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(Environment::width) / static_cast(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(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(Environment::width) / static_cast(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(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(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(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(Environment::width) / static_cast(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(Environment::width) / static_cast(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(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 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 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 color = canAttack ? std::array{ 1.f, 0.f, 0.f, 1.f } : std::array{ 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