Added sparks

This commit is contained in:
Vladislav Khorev 2026-04-24 19:53:18 +03:00
parent 2bb7da2e37
commit 393ebcd831
6 changed files with 202 additions and 27 deletions

BIN
resources/w/spark.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -1,6 +1,7 @@
#include "Character.h"
#include <cmath>
#include <iostream>
#include <random>
#include "GameConstants.h"
#include "Environment.h"
@ -268,6 +269,10 @@ void Character::update(int64_t deltaMs) {
if (hitSparkEmitter.isConfigured()) {
hitSparkEmitter.update(static_cast<float>(deltaMs));
}
auto it = animations.find(currentState);
if (it == animations.end()) return;
@ -304,13 +309,22 @@ void Character::update(int64_t deltaMs) {
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 == 18 && static_cast<int>(anim.currentFrame) != 18 && (currentState == AnimationState::ACTION_ATTACK || currentState == AnimationState::ACTION_ATTACK_2))
{
if (attackTarget != nullptr)
{
attackTarget->applyDamage(10.f);
attackTarget->applyDamage(10.f, attackDirectionToTarget());
}
}
}
@ -320,7 +334,7 @@ void Character::update(int64_t deltaMs) {
{
if (attackTarget != nullptr)
{
attackTarget->applyDamage(10.f);
attackTarget->applyDamage(10.f, attackDirectionToTarget());
}
}
}
@ -365,6 +379,11 @@ void Character::update(int64_t deltaMs) {
}
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;
@ -624,6 +643,11 @@ void Character::drawShadowDepthGpuSkinning(Renderer& renderer) {
// ==================== 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;
@ -755,10 +779,45 @@ void Character::drawGpuSkinningWithShadow(Renderer& renderer, const Eigen::Matri
CheckGlError(__FILE__, __LINE__);
}
void Character::applyDamage(float damageAmount)
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);
}
} // namespace ZL

View File

@ -3,6 +3,7 @@
#include "render/Renderer.h"
#include "render/TextureManager.h"
#include "items/Item.h"
#include "SparkEmitter.h"
#include <functional>
#include <memory>
#include <map>
@ -51,7 +52,19 @@ public:
void drawWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera);
void applyDamage(float damageAmount);
// attackDirection is a world-space horizontal vector pointing from the
// attacker toward this character — i.e. the direction the hit pushes
// them. A zero vector disables directional sparks (isotropic burst).
void applyDamage(float damageAmount, const Eigen::Vector3f& attackDirection = Eigen::Vector3f::Zero());
// Configures the per-character hit-spark emitter with the shared spark
// texture. Safe to call once per character after construction.
void setupHitSparks(std::shared_ptr<Texture> sparkTexture);
// Call per-frame between update() and draw(), with the active camera
// view matrix (world → eye). The emitter billboards and sorts its
// particles against that camera before its VBO is uploaded.
void prepareHitSparksForDraw(const Eigen::Matrix4f& cameraViewMatrix);
// Public: read by Game for camera tracking and ray-cast origin
Eigen::Vector3f position = Eigen::Vector3f(0.f, 0.f, 0.f);
@ -92,6 +105,8 @@ public:
std::string weaponAttachBoneName = "RightHand";
bool showWeapon = true;
SparkEmitter hitSparkEmitter;
private:

View File

@ -44,6 +44,8 @@ namespace ZL
//auto playerTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/w/gg/IMG_20260413_182354_992.png", CONST_ZIP_FILE));
auto playerTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/w/gg/UniV_Grid_2K_Base_color.png", CONST_ZIP_FILE));
auto sparkTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/w/spark.png", CONST_ZIP_FILE));
player = std::make_unique<Character>();
/*
player->loadBinaryAnimation(AnimationState::STAND, "resources/w/gg/gg_stand_idle001.anim", CONST_ZIP_FILE);
@ -93,10 +95,14 @@ namespace ZL
player->canAttack = true;
player->isPlayer = true;
player->setupHitSparks(sparkTexture);
std::cout << "Load resurces step 9" << std::endl;
// Load NPCs from JSON
npcs = GameObjectLoader::loadAndCreate_Npcs("resources/config2/npcs.json", CONST_ZIP_FILE);
for (auto& npc : npcs) {
if (npc) npc->setupHitSparks(sparkTexture);
}
auto ghostTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/w/ghost_skin001.png", CONST_ZIP_FILE));
@ -125,6 +131,7 @@ namespace ZL
npc02->setTarget(npc02->position);
npc02->canAttack = true;
npc02->attackTarget = player.get();
npc02->setupHitSparks(sparkTexture);
npcs.push_back(std::move(npc02));
@ -338,6 +345,10 @@ namespace ZL
}
}
const Eigen::Matrix4f currentView = renderer.GetCurrentModelViewMatrix();
if (player) player->prepareHitSparksForDraw(currentView);
for (auto& npc : npcs) npc->prepareHitSparksForDraw(currentView);
if (player) player->draw(renderer);
for (auto& npc : npcs) npc->draw(renderer);
@ -499,12 +510,20 @@ namespace ZL
#ifdef DEBUG_LIGHT
// In debug-light mode characters use the plain shaders (draw normally
// but from the light's viewpoint — projection/view already on stack).
{
const Eigen::Matrix4f currentView = renderer.GetCurrentModelViewMatrix();
if (player) player->prepareHitSparksForDraw(currentView);
for (auto& npc : npcs) npc->prepareHitSparksForDraw(currentView);
}
if (player) player->draw(renderer);
for (auto& npc : npcs) npc->draw(renderer);
#else
// Characters use their own shadow-aware shaders
CheckGlError(__FILE__, __LINE__);
if (player) player->prepareHitSparksForDraw(cameraViewMatrix);
for (auto& npc : npcs) npc->prepareHitSparksForDraw(cameraViewMatrix);
if (player) player->drawWithShadow(renderer, lightFromCamera, shadowMap->getDepthTexture(), lightDirCamera);
CheckGlError(__FILE__, __LINE__);

View File

@ -38,7 +38,8 @@ namespace ZL {
scaleRange(copyFrom.scaleRange),
lifeTimeRange(copyFrom.lifeTimeRange),
shaderProgramName(copyFrom.shaderProgramName),
configured(copyFrom.configured), useWorldSpace(copyFrom.useWorldSpace)
configured(copyFrom.configured), useWorldSpace(copyFrom.useWorldSpace),
emissionDirection(copyFrom.emissionDirection)
{
// Each copy gets its own GPU buffers; only copy CPU-side data
sparkQuad.data = copyFrom.sparkQuad.data;
@ -77,8 +78,12 @@ namespace ZL {
texture = tex;
}
void SparkEmitter::prepareDrawData(bool withRotation) {
if (!drawDataDirty) return;
void SparkEmitter::prepareDrawData(const Eigen::Matrix4f& cameraViewMatrix) {
const bool billboard = !cameraViewMatrix.isApprox(Eigen::Matrix4f::Identity());
// Billboarding depends on camera orientation every frame, so we must
// rebuild even when particle state hasn't changed.
if (!drawDataDirty && !billboard) return;
drawPositions.clear();
drawTexCoords.clear();
@ -88,17 +93,32 @@ namespace ZL {
return;
}
// When billboarding, the camera's world-space right/up axes are the
// first two rows of the view matrix's rotation part. Sort key is
// eye-space Z (camera looks down -Z, so smaller z = farther; sorting
// ascending puts far particles first = back-to-front).
Vector3f camRight(1.f, 0.f, 0.f);
Vector3f camUp(0.f, 1.f, 0.f);
if (billboard) {
camRight = Vector3f(cameraViewMatrix(0, 0), cameraViewMatrix(0, 1), cameraViewMatrix(0, 2));
camUp = Vector3f(cameraViewMatrix(1, 0), cameraViewMatrix(1, 1), cameraViewMatrix(1, 2));
}
std::vector<std::pair<const SparkParticle*, float>> sortedParticles;
sortedParticles.reserve(getActiveParticleCount());
for (const auto& particle : particles) {
if (particle.active) {
Vector3f posCam;
posCam = particle.position;
sortedParticles.push_back({ &particle, posCam(2) });
if (!particle.active) continue;
float depthKey;
if (billboard) {
depthKey = cameraViewMatrix(2, 0) * particle.position(0)
+ cameraViewMatrix(2, 1) * particle.position(1)
+ cameraViewMatrix(2, 2) * particle.position(2)
+ cameraViewMatrix(2, 3);
} else {
depthKey = particle.position(2);
}
sortedParticles.push_back({ &particle, depthKey });
}
std::sort(sortedParticles.begin(), sortedParticles.end(),
@ -108,12 +128,37 @@ namespace ZL {
for (const auto& [particlePtr, depth] : sortedParticles) {
const auto& particle = *particlePtr;
Vector3f posCam;
posCam = particle.position;
float size = particleSize * particle.scale;
if (billboard) {
Vector3f r = camRight * size;
Vector3f u = camUp * size;
const Vector3f& c = particle.position;
Vector3f bl = c - r - u;
Vector3f tl = c - r + u;
Vector3f tr = c + r + u;
Vector3f br = c + r - u;
drawPositions.push_back({ bl(0), bl(1), bl(2) });
drawTexCoords.push_back({ 0.0f, 0.0f });
drawPositions.push_back({ tl(0), tl(1), tl(2) });
drawTexCoords.push_back({ 0.0f, 1.0f });
drawPositions.push_back({ tr(0), tr(1), tr(2) });
drawTexCoords.push_back({ 1.0f, 1.0f });
drawPositions.push_back({ bl(0), bl(1), bl(2) });
drawTexCoords.push_back({ 0.0f, 0.0f });
drawPositions.push_back({ tr(0), tr(1), tr(2) });
drawTexCoords.push_back({ 1.0f, 1.0f });
drawPositions.push_back({ br(0), br(1), br(2) });
drawTexCoords.push_back({ 1.0f, 0.0f });
} else {
const Vector3f& posCam = particle.position;
drawPositions.push_back({ posCam(0) - size, posCam(1) - size, posCam(2) });
drawTexCoords.push_back({ 0.0f, 0.0f });
@ -132,19 +177,18 @@ namespace ZL {
drawPositions.push_back({ posCam(0) + size, posCam(1) - size, posCam(2) });
drawTexCoords.push_back({ 1.0f, 0.0f });
}
}
drawDataDirty = false;
}
void SparkEmitter::prepareForDraw(bool withRotation) {
void SparkEmitter::prepareForDraw(const Eigen::Matrix4f& cameraViewMatrix) {
if (!configured) return;
prepareDrawData(withRotation);
prepareDrawData(cameraViewMatrix);
if (!drawPositions.empty()) {
if (!drawPositions.empty()) {
sparkQuad.data.PositionData = drawPositions;
sparkQuad.data.TexCoordData = drawTexCoords;
sparkQuad.RefreshVBO();
@ -368,6 +412,19 @@ namespace ZL {
float speed = speedDist(gen);
float zSpeed = zSpeedDist(gen);
if (emissionDirection.squaredNorm() > 1e-6f) {
// Directional mode: spray in a cone around emissionDirection.
Vector3f dir = emissionDirection.normalized();
Vector3f up(0.f, 1.f, 0.f);
if (std::fabs(dir.dot(up)) > 0.95f) up = Vector3f(1.f, 0.f, 0.f);
Vector3f right = dir.cross(up).normalized();
Vector3f forward = right.cross(dir).normalized();
std::uniform_real_distribution<float> spreadDist(-0.6f, 0.6f);
Vector3f v = (dir + right * spreadDist(gen) + forward * spreadDist(gen)).normalized() * speed;
v(1) += zSpeed;
return v;
}
// Теперь biasX берется из JSON
if (emitterIndex == 0) {
return Vector3f{

View File

@ -51,9 +51,18 @@ namespace ZL {
std::string shaderProgramName;
bool configured;
void prepareDrawData(bool withRotation);
// Rebuilds the CPU-side quad/UV arrays. If cameraViewMatrix is identity,
// particles are rendered as axis-aligned quads in world XY and sorted by
// world Z (legacy behavior). Otherwise quads are billboarded toward the
// camera using its world-space right/up axes, and particles are sorted
// back-to-front in eye space for correct alpha blending.
void prepareDrawData(const Eigen::Matrix4f& cameraViewMatrix);
bool useWorldSpace;
// When non-zero, velocities spray in a cone around this direction
// instead of the default planar XY + Z spread used by JSON emitters.
Vector3f emissionDirection = Vector3f{ 0.f, 0.f, 0.f };
public:
SparkEmitter();
SparkEmitter(const SparkEmitter& copyFrom);
@ -74,11 +83,24 @@ namespace ZL {
void setUseWorldSpace(bool use) { useWorldSpace = use; }
bool loadFromJsonFile(const std::string& path, Renderer& renderer, const std::string& zipFile = "");
// Programmatic configuration (alternative to loadFromJsonFile).
// Ranges follow the same semantics as the JSON fields.
void setSpeedRange(float minVal, float maxVal) { speedRange.min = minVal; speedRange.max = maxVal; }
void setZSpeedRange(float minVal, float maxVal) { zSpeedRange.min = minVal; zSpeedRange.max = maxVal; }
void setScaleRange(float minVal, float maxVal) { scaleRange.min = minVal; scaleRange.max = maxVal; }
void setLifeTimeRange(float minVal, float maxVal) { lifeTimeRange.min = minVal; lifeTimeRange.max = maxVal; }
void setEmissionDirection(const Vector3f& dir) { emissionDirection = dir; }
void markConfigured() { configured = true; }
bool isConfigured() const { return configured; }
void update(float deltaTimeMs);
void emit(float ageMs = 0.0f, float lerpT = 0.0f);
// Вызывать ДО draw() в начале кадра: готовит данные и загружает в VBO.
void prepareForDraw(bool withRotation = true);
// Pass the current camera view matrix (world → eye) to billboard sparks
// toward the camera and sort them back-to-front. Identity (default)
// keeps the legacy non-billboarded behavior.
void prepareForDraw(const Eigen::Matrix4f& cameraViewMatrix = Eigen::Matrix4f::Identity());
void draw(Renderer& renderer, float zoom, int screenWidth, int screenHeight);
void draw(Renderer& renderer, float zoom, int screenWidth, int screenHeight, bool withRotation);