Added sparks
This commit is contained in:
parent
2bb7da2e37
commit
393ebcd831
BIN
resources/w/spark.png
(Stored with Git LFS)
Normal file
BIN
resources/w/spark.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
|
||||
@ -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__);
|
||||
|
||||
|
||||
@ -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,17 +177,16 @@ 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()) {
|
||||
sparkQuad.data.PositionData = drawPositions;
|
||||
@ -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{
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user