#include "Character.h" #include "render/TextRenderer.h" #include #include #include #include #include "GameConstants.h" #include "Environment.h" #include "render/TextureManager.h" #include "utils/Utils.h" #include "TextModel.h" #include "game_server/IGameServer.h" #include "navigation/PathFinder.h" #include "Location.h" namespace FRG { extern float x; extern float y; Dialogue::TranslationDatabase Character::nameTranslations; std::shared_ptr Character::stateSnapshot() const { auto snapshot = gameServer_.snapshot(); if (!snapshot) return {}; auto player = snapshot->players.find(entityId_); if (player != snapshot->players.end()) { return std::shared_ptr( std::move(snapshot), &player->second.state); } auto location = snapshot->locations.find(locationId_); if (location == snapshot->locations.end()) return {}; auto npc = location->second.npcs.find(entityId_); if (npc == location->second.npcs.end()) return {}; return std::shared_ptr( std::move(snapshot), &npc->second.state); } bool Character::isMoving() const { auto state = stateSnapshot(); return state && state->isMoving(); } Eigen::Vector3f Character::getRequestedWalkTarget() const { auto state = stateSnapshot(); return state ? state->getRequestedWalkTarget() : Eigen::Vector3f::Zero(); } Eigen::Vector3f Character::getCurrentNavigationTarget() const { auto state = stateSnapshot(); return state ? state->getCurrentNavigationTarget() : Eigen::Vector3f::Zero(); } float Character::getHp() const { auto state = stateSnapshot(); return state ? state->getHp() : 0.f; } void Character::loadAnimation(AnimationState animState, const std::string& filename, const std::string& zipFile) { auto& data = animations[animState]; 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 animState, const std::string& filename, const std::string& zipFile) { auto& data = animations[animState]; 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; } } std::unique_ptr Character::createFromState(const CharacterState& st, IGameServer& server, const std::string& locationId, const std::string& entityId, Renderer& renderer, const char* zipFile) { auto c = std::make_unique(server, locationId, entityId); const std::string zip = zipFile ? zipFile : ""; auto loadAnim = [&](AnimationState animState, const std::string& path) { if (path.empty()) return; try { if (path.size() >= 5 && path.substr(path.size() - 5) == ".anim") c->loadBinaryAnimation(animState, path, zip); else c->loadAnimation(animState, path, zip); } catch (const std::exception& e) { std::cerr << "Character::createFromState: failed to load animation '" << path << "': " << e.what() << std::endl; } }; for (const auto& [animState, path] : st.creationInfo.animationPaths) loadAnim(animState, path); auto loadTex = [&](const std::string& path) -> std::shared_ptr { if (path.empty()) return nullptr; try { //return std::make_shared(CreateTextureDataFromPng(path, zip)); return renderer.textureManager.LoadFromPng(path, zip); } catch (const std::exception& e) { std::cerr << "Character::createFromState: failed to load texture '" << path << "': " << e.what() << std::endl; return nullptr; } }; if (!st.creationInfo.singleTexturePath.empty()) { auto tex = loadTex(st.creationInfo.singleTexturePath); if (tex) c->setTexture(tex); } for (const auto& [meshName, texPath] : st.creationInfo.meshTexturePaths) { auto tex = loadTex(texPath); if (tex) c->setTexture(meshName, tex); } if (!st.creationInfo.weaponMeshPath.empty()) { const std::string& wp = st.creationInfo.weaponMeshPath; try { if (wp.size() >= 4 && wp.substr(wp.size() - 4) == ".bin") c->weaponMesh.data = LoadModelFromBinFile(wp, zip); else c->weaponMesh.data = LoadFromTextFile02(wp, zip); if (st.creationInfo.weaponMeshMove != Eigen::Vector3f::Zero()) c->weaponMesh.data.Move(st.creationInfo.weaponMeshMove); if (st.creationInfo.weaponMeshScale != 1.0f) c->weaponMesh.data.Scale(st.creationInfo.weaponMeshScale); c->weaponMesh.RefreshVBO(); } catch (const std::exception& e) { std::cerr << "Character::createFromState: failed to load weapon mesh '" << wp << "': " << e.what() << std::endl; } } c->weaponTexture = loadTex(st.creationInfo.weaponTexturePath); c->weaponAttachBoneName = st.creationInfo.weaponAttachBoneName; c->weaponInitialPosition = st.creationInfo.weaponInitialPosition; c->weaponInitialRotation = st.creationInfo.weaponInitialRotation.toRotationMatrix(); return c; } 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); } AnimationState Character::resolveActiveState(const CharacterState& state) const { if (animations.count(state.currentState)) return state.currentState; return AnimationState::STAND; } void Character::update(int64_t deltaMs, const CharacterUpdateEvents* events) { if (hitSparkEmitter.isConfigured()) { hitSparkEmitter.update(static_cast(deltaMs)); } auto stateSnapshot = this->stateSnapshot(); if (!stateSnapshot) return; const CharacterState& state = *stateSnapshot; if (events) { if (events->deathAnimationComplete && onDeathAnimComplete) onDeathAnimComplete(); if (events->attackImpact && audioPlayer) { audioPlayer->playSoundAsync(state.isPlayer ? "resources/audio/547041__cogfirestudios__hit-swing-sword-small-3.ogg" : "resources/audio/13925__adcbicycle__21.ogg"); } } if (!state.enabled) return; auto it = animations.find(state.currentState); if (it == animations.end()) return; auto& anim = it->second; anim.currentFrame = (std::min)( state.animationFrame, static_cast((std::max)(0, anim.totalFrames - 1))); if (static_cast(anim.currentFrame) != anim.lastFrame) { anim.gpuSkinningShaderData.ComputeSkinningMatrices( anim.model.startBones, anim.model.animations[0].keyFrames, static_cast(anim.currentFrame)); if (!state.useGpuSkinning) { anim.model.Interpolate(static_cast(anim.currentFrame)); } anim.lastFrame = static_cast(anim.currentFrame); } } void Character::draw(Renderer& renderer) { if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) { hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false); } renderer.RenderUniform1f("uAlpha", 1.0f); glDisable(GL_BLEND); auto stateSnapshot = this->stateSnapshot(); if (!stateSnapshot) return; const CharacterState& state = *stateSnapshot; if (!state.enabled) return; if (state.useGpuSkinning) { drawGpuSkinning(renderer, state); return; } AnimationState drawState = resolveActiveState(state); 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({ state.position.x(), state.position.y(), state.position.z() }); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.ScaleMatrix(state.modelScale); renderer.RotateMatrix(state.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, state); renderer.PopProjectionMatrix(); renderer.shaderManager.PopShader(); } void Character::drawGpuSkinning(Renderer& renderer, const CharacterState& state) { AnimationState drawState = resolveActiveState(state); 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({ state.position.x(), state.position.y(), state.position.z() }); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.ScaleMatrix(state.modelScale); renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix()); 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, state); renderer.shaderManager.PopShader(); renderer.PopProjectionMatrix(); renderer.shaderManager.PopShader(); } bool Character::prepareGpuSkinning(const CharacterState& state) { AnimationState drawState = resolveActiveState(state); 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, const CharacterState& state) { if (!state.showWeapon) return; if (weaponMesh.data.PositionData.size() == 0 || !weaponTexture) return; auto it = animations.find(resolveActiveState(state)); 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({ state.position.x(), state.position.y(), state.position.z() }); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.ScaleMatrix(state.modelScale); renderer.RotateMatrix(state.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) { auto stateSnapshot = this->stateSnapshot(); if (!stateSnapshot) return; const CharacterState& state = *stateSnapshot; if (!state.enabled) return; if (state.useGpuSkinning) { drawShadowDepthGpuSkinning(renderer, state); } else { drawShadowDepthCpu(renderer, state); } } void Character::drawShadowDepthCpu(Renderer& renderer, const CharacterState& state) { AnimationState drawState = resolveActiveState(state); auto it = animations.find(drawState); if (it == animations.end()) return; renderer.PushMatrix(); renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() }); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.ScaleMatrix(state.modelScale); renderer.RotateMatrix(state.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, state); } void Character::drawShadowDepthGpuSkinning(Renderer& renderer, const CharacterState& state) { CheckGlError(__FILE__, __LINE__); AnimationState drawState = resolveActiveState(state); auto it = animations.find(drawState); if (it == animations.end()) return; CheckGlError(__FILE__, __LINE__); if (!prepareGpuSkinning(state)) return; CheckGlError(__FILE__, __LINE__); static const std::string shadowSkinningShader = "shadow_depth_skinning"; static const std::string boneMatricesUniform = "uBoneMatrices[0]"; renderer.shaderManager.PushShader(shadowSkinningShader); renderer.PushMatrix(); renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() }); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.ScaleMatrix(state.modelScale); renderer.RotateMatrix(state.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, state); CheckGlError(__FILE__, __LINE__); } // ==================== Main pass with shadows ==================== void Character::drawWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera) { auto stateSnapshot = this->stateSnapshot(); if (!stateSnapshot) return; const CharacterState& state = *stateSnapshot; if (!state.enabled) return; if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) { hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false); } renderer.RenderUniform1f("uAlpha", 1.0f); glDisable(GL_BLEND); if (state.useGpuSkinning) { drawGpuSkinningWithShadow(renderer, state, lightFromCamera, shadowMapTex, lightDirCamera); } else { drawCpuWithShadow(renderer, state, lightFromCamera, shadowMapTex, lightDirCamera); } } void Character::drawCpuWithShadow(Renderer& renderer, const CharacterState& state, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera) { AnimationState drawState = resolveActiveState(state); 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({ state.position.x(), state.position.y(), state.position.z() }); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.ScaleMatrix(state.modelScale); renderer.RotateMatrix(state.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, state); renderer.shaderManager.PopShader(); renderer.PopProjectionMatrix(); renderer.shaderManager.PopShader(); } void Character::drawGpuSkinningWithShadow(Renderer& renderer, const CharacterState& state, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera) { AnimationState drawState = resolveActiveState(state); auto it = animations.find(drawState); if (it == animations.end()) return; CheckGlError(__FILE__, __LINE__); if (!prepareGpuSkinning(state)) 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({ state.position.x(), state.position.y(), state.position.z() }); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.ScaleMatrix(state.modelScale); renderer.RotateMatrix(state.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, state); renderer.shaderManager.PopShader(); CheckGlError(__FILE__, __LINE__); renderer.PopProjectionMatrix(); renderer.shaderManager.PopShader(); CheckGlError(__FILE__, __LINE__); } // ==================== Night pass with point lights ==================== void Character::drawNight(Renderer& renderer, const float* pointLightPosEye, const float* pointLightDirEye, const float* pointLightColors, int pointLightCount, const float* pointLightWorldXZ, const float* pointLightLimitXZ, const Eigen::Matrix4f& cameraViewInverse, const float* ambientColor, const float* fogColor) { auto stateSnapshot = this->stateSnapshot(); if (!stateSnapshot) return; const CharacterState& state = *stateSnapshot; if (!state.enabled) return; if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) { hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false); } renderer.RenderUniform1f("uAlpha", 1.0f); glDisable(GL_BLEND); if (state.useGpuSkinning) { drawNightGpuSkinning(renderer, state, pointLightPosEye, pointLightDirEye, pointLightColors, pointLightCount, pointLightWorldXZ, pointLightLimitXZ, cameraViewInverse, ambientColor, fogColor); } else { drawNightCpu(renderer, state, pointLightPosEye, pointLightDirEye, pointLightColors, pointLightCount, pointLightWorldXZ, pointLightLimitXZ, cameraViewInverse, ambientColor, fogColor); } } void Character::drawNightGpuSkinning(Renderer& renderer, const CharacterState& state, const float* plPosEye, const float* plDirEye, const float* plColors, int plCount, const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse, const float* ambientColor, const float* fogColor) { AnimationState drawState = resolveActiveState(state); auto it = animations.find(drawState); if (it == animations.end()) return; if (!prepareGpuSkinning(state)) return; static const std::string nightSkinningShader = "night_fog_skinning"; static const std::string boneMatricesUniform = "uBoneMatrices[0]"; renderer.shaderManager.PushShader(nightSkinningShader); renderer.RenderUniform1i(textureUniformName, 0); renderer.RenderUniform3fv("uAmbientColor", ambientColor); renderer.RenderUniform3fv("uFogColor", fogColor); const float playerEyePosNight[3] = { 0.0f, 0.0f, -Environment::zoom }; renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosNight); renderer.RenderUniform1i("uPointLightCount", plCount); if (plCount > 0) { renderer.RenderUniform3fvArray("uPointLightPos[0]", plCount, plPosEye); renderer.RenderUniform3fvArray("uPointLightDir[0]", plCount, plDirEye); renderer.RenderUniform3fvArray("uPointLightColor[0]", plCount, plColors); renderer.RenderUniform2fvArray("uPointLightWorldXZ[0]", plCount, plWorldXZ); renderer.RenderUniform2fvArray("uPointLightLimitXZ[0]", plCount, plLimitXZ); } renderer.RenderUniformMatrix4fv("uViewInverse", false, cameraViewInverse.data()); 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({ state.position.x(), state.position.y(), state.position.z() }); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.ScaleMatrix(state.modelScale); renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix()); renderer.RenderUniformMatrix4fvArray(boneMatricesUniform, static_cast(it->second.gpuSkinningShaderData.skinningMatrices.size()), false, it->second.gpuSkinningShaderData.skinningMatrices[0].data()); 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("night_fog"); renderer.RenderUniform1i(textureUniformName, 0); renderer.RenderUniform3fv("uAmbientColor", ambientColor); renderer.RenderUniform3fv("uFogColor", fogColor); renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosNight); renderer.RenderUniform1i("uPointLightCount", plCount); if (plCount > 0) { renderer.RenderUniform3fvArray("uPointLightPos[0]", plCount, plPosEye); renderer.RenderUniform3fvArray("uPointLightDir[0]", plCount, plDirEye); renderer.RenderUniform3fvArray("uPointLightColor[0]", plCount, plColors); renderer.RenderUniform2fvArray("uPointLightWorldXZ[0]", plCount, plWorldXZ); renderer.RenderUniform2fvArray("uPointLightLimitXZ[0]", plCount, plLimitXZ); } drawAttachedWeapon(renderer, state); renderer.shaderManager.PopShader(); renderer.PopProjectionMatrix(); renderer.shaderManager.PopShader(); } void Character::drawNightCpu(Renderer& renderer, const CharacterState& state, const float* plPosEye, const float* plDirEye, const float* plColors, int plCount, const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse, const float* ambientColor, const float* fogColor) { AnimationState drawState = resolveActiveState(state); auto it = animations.find(drawState); if (it == animations.end()) return; static const std::string nightShader = "night_fog"; renderer.shaderManager.PushShader(nightShader); renderer.RenderUniform1i(textureUniformName, 0); renderer.RenderUniform3fv("uAmbientColor", ambientColor); renderer.RenderUniform3fv("uFogColor", fogColor); const float playerEyePosNight[3] = { 0.0f, 0.0f, -Environment::zoom }; renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosNight); renderer.RenderUniform1i("uPointLightCount", plCount); if (plCount > 0) { renderer.RenderUniform3fvArray("uPointLightPos[0]", plCount, plPosEye); renderer.RenderUniform3fvArray("uPointLightDir[0]", plCount, plDirEye); renderer.RenderUniform3fvArray("uPointLightColor[0]", plCount, plColors); renderer.RenderUniform2fvArray("uPointLightWorldXZ[0]", plCount, plWorldXZ); renderer.RenderUniform2fvArray("uPointLightLimitXZ[0]", plCount, plLimitXZ); } 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({ state.position.x(), state.position.y(), state.position.z() }); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.ScaleMatrix(state.modelScale); renderer.RotateMatrix(state.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.RenderUniform3fv("uPlayerEyePos", playerEyePosNight); drawAttachedWeapon(renderer, state); renderer.PopProjectionMatrix(); renderer.shaderManager.PopShader(); } // ==================== Night pass with spot-light + shadow ==================== void Character::drawNightWithShadow(Renderer& renderer, const float* plPosEye, const float* plDirEye, const float* plColors, int plCount, const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const float* ambientColor, const float* fogColor) { auto stateSnapshot = this->stateSnapshot(); if (!stateSnapshot) return; const CharacterState& state = *stateSnapshot; if (!state.enabled) return; if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) { hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false); } renderer.RenderUniform1f("uAlpha", 1.0f); glDisable(GL_BLEND); if (state.useGpuSkinning) { drawNightGpuSkinningWithShadow(renderer, state, plPosEye, plDirEye, plColors, plCount, plWorldXZ, plLimitXZ, cameraViewInverse, lightFromCamera, shadowMapTex, ambientColor, fogColor); } else { drawNightCpuWithShadow(renderer, state, plPosEye, plDirEye, plColors, plCount, plWorldXZ, plLimitXZ, cameraViewInverse, lightFromCamera, shadowMapTex, ambientColor, fogColor); } } void Character::drawNightGpuSkinningWithShadow(Renderer& renderer, const CharacterState& state, const float* plPosEye, const float* plDirEye, const float* plColors, int plCount, const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const float* ambientColor, const float* fogColor) { AnimationState drawState = resolveActiveState(state); auto it = animations.find(drawState); if (it == animations.end()) return; if (!prepareGpuSkinning(state)) return; static const std::string nightShadowSkinningShader = "night_fog_skinning_shadow"; static const std::string boneMatricesUniform = "uBoneMatrices[0]"; renderer.shaderManager.PushShader(nightShadowSkinningShader); renderer.RenderUniform1i(textureUniformName, 0); renderer.RenderUniform1i("uShadowMap", 1); renderer.RenderUniform3fv("uAmbientColor", ambientColor); renderer.RenderUniform3fv("uFogColor", fogColor); renderer.RenderUniformMatrix4fv("uLightFromCamera", false, lightFromCamera.data()); if (plCount > 0) renderer.RenderUniform3fv("uLightDir", plDirEye); const float playerEyePosNightShadow[3] = { 0.0f, 0.0f, -Environment::zoom }; renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosNightShadow); renderer.RenderUniform1i("uPointLightCount", plCount); if (plCount > 0) { renderer.RenderUniform3fvArray("uPointLightPos[0]", plCount, plPosEye); renderer.RenderUniform3fvArray("uPointLightDir[0]", plCount, plDirEye); renderer.RenderUniform3fvArray("uPointLightColor[0]", plCount, plColors); } renderer.RenderUniform2fvArray("uPointLightWorldXZ[0]", plCount, plWorldXZ); renderer.RenderUniform2fvArray("uPointLightLimitXZ[0]", plCount, plLimitXZ); renderer.RenderUniformMatrix4fv("uViewInverse", false, cameraViewInverse.data()); 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({ state.position.x(), state.position.y(), state.position.z() }); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.ScaleMatrix(state.modelScale); renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix()); renderer.RenderUniformMatrix4fvArray(boneMatricesUniform, static_cast(it->second.gpuSkinningShaderData.skinningMatrices.size()), false, it->second.gpuSkinningShaderData.skinningMatrices[0].data()); 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("night_fog_shadow"); renderer.RenderUniform1i(textureUniformName, 0); renderer.RenderUniform1i("uShadowMap", 1); renderer.RenderUniform3fv("uAmbientColor", ambientColor); renderer.RenderUniform3fv("uFogColor", fogColor); renderer.RenderUniformMatrix4fv("uLightFromCamera", false, lightFromCamera.data()); if (plCount > 0) renderer.RenderUniform3fv("uLightDir", plDirEye); renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosNightShadow); renderer.RenderUniform1i("uPointLightCount", plCount); if (plCount > 0) { renderer.RenderUniform3fvArray("uPointLightPos[0]", plCount, plPosEye); renderer.RenderUniform3fvArray("uPointLightDir[0]", plCount, plDirEye); renderer.RenderUniform3fvArray("uPointLightColor[0]", plCount, plColors); } renderer.RenderUniform2fvArray("uPointLightWorldXZ[0]", plCount, plWorldXZ); renderer.RenderUniform2fvArray("uPointLightLimitXZ[0]", plCount, plLimitXZ); drawAttachedWeapon(renderer, state); renderer.shaderManager.PopShader(); renderer.PopProjectionMatrix(); renderer.shaderManager.PopShader(); } void Character::drawNightCpuWithShadow(Renderer& renderer, const CharacterState& state, const float* plPosEye, const float* plDirEye, const float* plColors, int plCount, const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const float* ambientColor, const float* fogColor) { AnimationState drawState = resolveActiveState(state); auto it = animations.find(drawState); if (it == animations.end()) return; static const std::string nightShadowShader = "night_fog_shadow"; renderer.shaderManager.PushShader(nightShadowShader); renderer.RenderUniform1i(textureUniformName, 0); renderer.RenderUniform1i("uShadowMap", 1); renderer.RenderUniform3fv("uAmbientColor", ambientColor); renderer.RenderUniform3fv("uFogColor", fogColor); renderer.RenderUniformMatrix4fv("uLightFromCamera", false, lightFromCamera.data()); if (plCount > 0) renderer.RenderUniform3fv("uLightDir", plDirEye); const float playerEyePosNightShadow[3] = { 0.0f, 0.0f, -Environment::zoom }; renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosNightShadow); renderer.RenderUniform1i("uPointLightCount", plCount); if (plCount > 0) { renderer.RenderUniform3fvArray("uPointLightPos[0]", plCount, plPosEye); renderer.RenderUniform3fvArray("uPointLightDir[0]", plCount, plDirEye); renderer.RenderUniform3fvArray("uPointLightColor[0]", plCount, plColors); } renderer.RenderUniform2fvArray("uPointLightWorldXZ[0]", plCount, plWorldXZ); renderer.RenderUniform2fvArray("uPointLightLimitXZ[0]", plCount, plLimitXZ); 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({ state.position.x(), state.position.y(), state.position.z() }); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.ScaleMatrix(state.modelScale); renderer.RotateMatrix(state.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.RenderUniform3fv("uPlayerEyePos", playerEyePosNightShadow); drawAttachedWeapon(renderer, state); renderer.PopProjectionMatrix(); renderer.shaderManager.PopShader(); } void Character::setupHitSparks(std::shared_ptr sparkTexture) { hitSparkEmitter.setTexture(sparkTexture); hitSparkEmitter.setShaderProgramName("spark"); hitSparkEmitter.setMaxParticles(128); hitSparkEmitter.setParticleSize(0.02f); hitSparkEmitter.setBiasX(0.0f); hitSparkEmitter.setEmissionRate(1.0e9f); 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::presentDamage(float hp, float maxHp, const Eigen::Vector3f& attackDirection, const std::string& attackerId) { if (onHpChanged) onHpChanged(hp, maxHp, attackerId); if (!hitSparkEmitter.isConfigured()) return; auto stateSnapshot = this->stateSnapshot(); if (!stateSnapshot) return; const CharacterState& state = *stateSnapshot; Eigen::Vector3f emitPos = state.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); } static void drawCharacterNameLabel( TextRenderer& textRenderer, const Eigen::Matrix4f& cameraViewMatrix, const Eigen::Matrix4f& projectionMatrix, const CharacterState& state, const std::string& displayName, const std::array& color) { if (!state.enabled || state.hp <= 0.f || displayName.empty()) return; Eigen::Vector4f worldPos( state.position.x(), state.position.y() + 1.83f, state.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.f || ndcX > 1.f || ndcY < -1.f || ndcY > 1.f) return; float screenX = (ndcX * 0.5f + 0.5f) * Environment::projectionWidth; float screenY = (ndcY * 0.5f + 0.5f) * Environment::projectionHeight; textRenderer.drawText(displayName, screenX, screenY, 1.0f, true, color); } void Character::drawName(TextRenderer& textRenderer, const Eigen::Matrix4f& cameraViewMatrix, const Eigen::Matrix4f& projectionMatrix) { auto stateSnapshot = this->stateSnapshot(); if (!stateSnapshot) return; const CharacterState& state = *stateSnapshot; if (state.isPlayer) return; if (state.npcName.empty()) return; std::array color = state.canAttack ? std::array{ 1.f, 0.f, 0.f, 1.f } : std::array{ 0.f, 1.f, 0.f, 1.f }; const std::string& translatedName = Character::nameTranslations.translateRef(state.npcName, g_currentLanguage); drawCharacterNameLabel( textRenderer, cameraViewMatrix, projectionMatrix, state, translatedName, color); } void Character::drawPlayerName( TextRenderer& textRenderer, const Eigen::Matrix4f& cameraViewMatrix, const Eigen::Matrix4f& projectionMatrix, const std::string& displayName) { auto snapshot = stateSnapshot(); if (!snapshot) return; static const std::array color = {0.f, 1.f, 0.f, 1.f}; drawCharacterNameLabel( textRenderer, cameraViewMatrix, projectionMatrix, *snapshot, displayName, color); } void Character::drawHealthBar(Renderer& renderer, const Eigen::Matrix4f& cameraViewMatrix, const Eigen::Matrix4f& projectionMatrix) { auto stateSnapshot = this->stateSnapshot(); if (!stateSnapshot) return; const CharacterState& state = *stateSnapshot; if (state.isPlayer || !state.canAttack) return; if (!state.enabled) return; if (state.hp <= 0.f) return; if (state.initialHp <= 0.f) return; Eigen::Vector4f worldPos(state.position.x(), state.position.y() + 1.75f, state.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 = state.hp / state.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 FRG