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 "Character.h"
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <random>
|
||||||
#include "GameConstants.h"
|
#include "GameConstants.h"
|
||||||
#include "Environment.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);
|
auto it = animations.find(currentState);
|
||||||
if (it == animations.end()) return;
|
if (it == animations.end()) return;
|
||||||
|
|
||||||
@ -304,13 +309,22 @@ void Character::update(int64_t deltaMs) {
|
|||||||
showWeapon = false;
|
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 (isPlayer)
|
||||||
{
|
{
|
||||||
if (prevFrame == 18 && static_cast<int>(anim.currentFrame) != 18 && (currentState == AnimationState::ACTION_ATTACK || currentState == AnimationState::ACTION_ATTACK_2))
|
if (prevFrame == 18 && static_cast<int>(anim.currentFrame) != 18 && (currentState == AnimationState::ACTION_ATTACK || currentState == AnimationState::ACTION_ATTACK_2))
|
||||||
{
|
{
|
||||||
if (attackTarget != nullptr)
|
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)
|
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) {
|
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)
|
if (!isPlayer && hp <= 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@ -624,6 +643,11 @@ void Character::drawShadowDepthGpuSkinning(Renderer& renderer) {
|
|||||||
// ==================== Main pass with shadows ====================
|
// ==================== Main pass with shadows ====================
|
||||||
|
|
||||||
void Character::drawWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera) {
|
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)
|
if (!isPlayer && hp <= 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@ -755,10 +779,45 @@ void Character::drawGpuSkinningWithShadow(Renderer& renderer, const Eigen::Matri
|
|||||||
CheckGlError(__FILE__, __LINE__);
|
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;
|
hp = hp - damageAmount;
|
||||||
if (hp < 0) hp = 0;
|
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
|
} // namespace ZL
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
#include "render/Renderer.h"
|
#include "render/Renderer.h"
|
||||||
#include "render/TextureManager.h"
|
#include "render/TextureManager.h"
|
||||||
#include "items/Item.h"
|
#include "items/Item.h"
|
||||||
|
#include "SparkEmitter.h"
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <map>
|
#include <map>
|
||||||
@ -51,7 +52,19 @@ public:
|
|||||||
void drawWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera);
|
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
|
// Public: read by Game for camera tracking and ray-cast origin
|
||||||
Eigen::Vector3f position = Eigen::Vector3f(0.f, 0.f, 0.f);
|
Eigen::Vector3f position = Eigen::Vector3f(0.f, 0.f, 0.f);
|
||||||
@ -92,6 +105,8 @@ public:
|
|||||||
std::string weaponAttachBoneName = "RightHand";
|
std::string weaponAttachBoneName = "RightHand";
|
||||||
bool showWeapon = true;
|
bool showWeapon = true;
|
||||||
|
|
||||||
|
SparkEmitter hitSparkEmitter;
|
||||||
|
|
||||||
|
|
||||||
private:
|
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/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 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 = std::make_unique<Character>();
|
||||||
/*
|
/*
|
||||||
player->loadBinaryAnimation(AnimationState::STAND, "resources/w/gg/gg_stand_idle001.anim", CONST_ZIP_FILE);
|
player->loadBinaryAnimation(AnimationState::STAND, "resources/w/gg/gg_stand_idle001.anim", CONST_ZIP_FILE);
|
||||||
@ -93,10 +95,14 @@ namespace ZL
|
|||||||
|
|
||||||
player->canAttack = true;
|
player->canAttack = true;
|
||||||
player->isPlayer = true;
|
player->isPlayer = true;
|
||||||
|
player->setupHitSparks(sparkTexture);
|
||||||
std::cout << "Load resurces step 9" << std::endl;
|
std::cout << "Load resurces step 9" << std::endl;
|
||||||
|
|
||||||
// Load NPCs from JSON
|
// Load NPCs from JSON
|
||||||
npcs = GameObjectLoader::loadAndCreate_Npcs("resources/config2/npcs.json", CONST_ZIP_FILE);
|
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));
|
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->setTarget(npc02->position);
|
||||||
npc02->canAttack = true;
|
npc02->canAttack = true;
|
||||||
npc02->attackTarget = player.get();
|
npc02->attackTarget = player.get();
|
||||||
|
npc02->setupHitSparks(sparkTexture);
|
||||||
|
|
||||||
npcs.push_back(std::move(npc02));
|
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);
|
if (player) player->draw(renderer);
|
||||||
for (auto& npc : npcs) npc->draw(renderer);
|
for (auto& npc : npcs) npc->draw(renderer);
|
||||||
|
|
||||||
@ -499,12 +510,20 @@ namespace ZL
|
|||||||
#ifdef DEBUG_LIGHT
|
#ifdef DEBUG_LIGHT
|
||||||
// In debug-light mode characters use the plain shaders (draw normally
|
// In debug-light mode characters use the plain shaders (draw normally
|
||||||
// but from the light's viewpoint — projection/view already on stack).
|
// 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);
|
if (player) player->draw(renderer);
|
||||||
for (auto& npc : npcs) npc->draw(renderer);
|
for (auto& npc : npcs) npc->draw(renderer);
|
||||||
#else
|
#else
|
||||||
// Characters use their own shadow-aware shaders
|
// Characters use their own shadow-aware shaders
|
||||||
CheckGlError(__FILE__, __LINE__);
|
CheckGlError(__FILE__, __LINE__);
|
||||||
|
|
||||||
|
if (player) player->prepareHitSparksForDraw(cameraViewMatrix);
|
||||||
|
for (auto& npc : npcs) npc->prepareHitSparksForDraw(cameraViewMatrix);
|
||||||
|
|
||||||
if (player) player->drawWithShadow(renderer, lightFromCamera, shadowMap->getDepthTexture(), lightDirCamera);
|
if (player) player->drawWithShadow(renderer, lightFromCamera, shadowMap->getDepthTexture(), lightDirCamera);
|
||||||
CheckGlError(__FILE__, __LINE__);
|
CheckGlError(__FILE__, __LINE__);
|
||||||
|
|
||||||
|
|||||||
@ -38,7 +38,8 @@ namespace ZL {
|
|||||||
scaleRange(copyFrom.scaleRange),
|
scaleRange(copyFrom.scaleRange),
|
||||||
lifeTimeRange(copyFrom.lifeTimeRange),
|
lifeTimeRange(copyFrom.lifeTimeRange),
|
||||||
shaderProgramName(copyFrom.shaderProgramName),
|
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
|
// Each copy gets its own GPU buffers; only copy CPU-side data
|
||||||
sparkQuad.data = copyFrom.sparkQuad.data;
|
sparkQuad.data = copyFrom.sparkQuad.data;
|
||||||
@ -77,8 +78,12 @@ namespace ZL {
|
|||||||
texture = tex;
|
texture = tex;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SparkEmitter::prepareDrawData(bool withRotation) {
|
void SparkEmitter::prepareDrawData(const Eigen::Matrix4f& cameraViewMatrix) {
|
||||||
if (!drawDataDirty) return;
|
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();
|
drawPositions.clear();
|
||||||
drawTexCoords.clear();
|
drawTexCoords.clear();
|
||||||
@ -88,17 +93,32 @@ namespace ZL {
|
|||||||
return;
|
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;
|
std::vector<std::pair<const SparkParticle*, float>> sortedParticles;
|
||||||
sortedParticles.reserve(getActiveParticleCount());
|
sortedParticles.reserve(getActiveParticleCount());
|
||||||
|
|
||||||
for (const auto& particle : particles) {
|
for (const auto& particle : particles) {
|
||||||
if (particle.active) {
|
if (!particle.active) continue;
|
||||||
Vector3f posCam;
|
float depthKey;
|
||||||
|
if (billboard) {
|
||||||
posCam = particle.position;
|
depthKey = cameraViewMatrix(2, 0) * particle.position(0)
|
||||||
|
+ cameraViewMatrix(2, 1) * particle.position(1)
|
||||||
sortedParticles.push_back({ &particle, posCam(2) });
|
+ 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(),
|
std::sort(sortedParticles.begin(), sortedParticles.end(),
|
||||||
@ -108,13 +128,38 @@ namespace ZL {
|
|||||||
|
|
||||||
for (const auto& [particlePtr, depth] : sortedParticles) {
|
for (const auto& [particlePtr, depth] : sortedParticles) {
|
||||||
const auto& particle = *particlePtr;
|
const auto& particle = *particlePtr;
|
||||||
Vector3f posCam;
|
|
||||||
posCam = particle.position;
|
|
||||||
|
|
||||||
|
|
||||||
float size = particleSize * particle.scale;
|
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) });
|
drawPositions.push_back({ posCam(0) - size, posCam(1) - size, posCam(2) });
|
||||||
drawTexCoords.push_back({ 0.0f, 0.0f });
|
drawTexCoords.push_back({ 0.0f, 0.0f });
|
||||||
|
|
||||||
@ -132,19 +177,18 @@ namespace ZL {
|
|||||||
|
|
||||||
drawPositions.push_back({ posCam(0) + size, posCam(1) - size, posCam(2) });
|
drawPositions.push_back({ posCam(0) + size, posCam(1) - size, posCam(2) });
|
||||||
drawTexCoords.push_back({ 1.0f, 0.0f });
|
drawTexCoords.push_back({ 1.0f, 0.0f });
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
drawDataDirty = false;
|
drawDataDirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SparkEmitter::prepareForDraw(bool withRotation) {
|
void SparkEmitter::prepareForDraw(const Eigen::Matrix4f& cameraViewMatrix) {
|
||||||
if (!configured) return;
|
if (!configured) return;
|
||||||
|
|
||||||
prepareDrawData(withRotation);
|
prepareDrawData(cameraViewMatrix);
|
||||||
|
|
||||||
if (!drawPositions.empty()) {
|
if (!drawPositions.empty()) {
|
||||||
sparkQuad.data.PositionData = drawPositions;
|
sparkQuad.data.PositionData = drawPositions;
|
||||||
sparkQuad.data.TexCoordData = drawTexCoords;
|
sparkQuad.data.TexCoordData = drawTexCoords;
|
||||||
sparkQuad.RefreshVBO();
|
sparkQuad.RefreshVBO();
|
||||||
@ -368,6 +412,19 @@ namespace ZL {
|
|||||||
float speed = speedDist(gen);
|
float speed = speedDist(gen);
|
||||||
float zSpeed = zSpeedDist(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
|
// Теперь biasX берется из JSON
|
||||||
if (emitterIndex == 0) {
|
if (emitterIndex == 0) {
|
||||||
return Vector3f{
|
return Vector3f{
|
||||||
|
|||||||
@ -51,9 +51,18 @@ namespace ZL {
|
|||||||
std::string shaderProgramName;
|
std::string shaderProgramName;
|
||||||
|
|
||||||
bool configured;
|
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;
|
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:
|
public:
|
||||||
SparkEmitter();
|
SparkEmitter();
|
||||||
SparkEmitter(const SparkEmitter& copyFrom);
|
SparkEmitter(const SparkEmitter& copyFrom);
|
||||||
@ -74,11 +83,24 @@ namespace ZL {
|
|||||||
void setUseWorldSpace(bool use) { useWorldSpace = use; }
|
void setUseWorldSpace(bool use) { useWorldSpace = use; }
|
||||||
bool loadFromJsonFile(const std::string& path, Renderer& renderer, const std::string& zipFile = "");
|
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 update(float deltaTimeMs);
|
||||||
void emit(float ageMs = 0.0f, float lerpT = 0.0f);
|
void emit(float ageMs = 0.0f, float lerpT = 0.0f);
|
||||||
|
|
||||||
// Вызывать ДО draw() в начале кадра: готовит данные и загружает в VBO.
|
// Вызывать ДО 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);
|
||||||
void draw(Renderer& renderer, float zoom, int screenWidth, int screenHeight, bool withRotation);
|
void draw(Renderer& renderer, float zoom, int screenWidth, int screenHeight, bool withRotation);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user