mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 17:43:52 +00:00
Fix M2 particle rendering: color, gravity, transparency, and animation
- Fix FBlock color keys from 3-byte BGR to 4-byte RGBA (CImVector) to prevent garbled purple/red colors from byte misalignment - Add circular soft-edge falloff in particle fragment shader (GL_POINTS rendered as squares by default) - Apply default gravity (4.0 spray, 1.5 mist) when M2 gravity is 0 since bone animation from .anim files isn't loaded yet - Add drift velocity to speed=0 emitters so particles spread as mist instead of clustering at static bone positions - Run particle updates for all nearby instances, not just those in boneWorkIndices_, to prevent particles freezing when bone culled - Wrap animation time for particle models to keep emission tracks looping - Cap particle scale to 1.5 and reduce point size multiplier (800→400) - Desaturate FBlock colors 70% toward white for natural water appearance - Reduce additive blend alpha to 5% and volume particles to 2%
This commit is contained in:
parent
35034ca544
commit
bbcc18aa22
2 changed files with 76 additions and 20 deletions
|
|
@ -572,7 +572,7 @@ void parseAnimTrackVanilla(const std::vector<uint8_t>& data,
|
||||||
// FBlocks are like M2Track but WITHOUT the interpolationType/globalSequence prefix.
|
// FBlocks are like M2Track but WITHOUT the interpolationType/globalSequence prefix.
|
||||||
void parseFBlock(const std::vector<uint8_t>& data, uint32_t offset,
|
void parseFBlock(const std::vector<uint8_t>& data, uint32_t offset,
|
||||||
M2FBlock& fb, int valueType) {
|
M2FBlock& fb, int valueType) {
|
||||||
// valueType: 0 = color (3 bytes RGB), 1 = alpha (uint16), 2 = scale (float pair)
|
// valueType: 0 = color (CImVector, 4 bytes RGBA), 1 = alpha (uint16), 2 = scale (float pair)
|
||||||
if (offset + sizeof(FBlockDisk) > data.size()) return;
|
if (offset + sizeof(FBlockDisk) > data.size()) return;
|
||||||
|
|
||||||
FBlockDisk disk = readValue<FBlockDisk>(data, offset);
|
FBlockDisk disk = readValue<FBlockDisk>(data, offset);
|
||||||
|
|
@ -595,15 +595,14 @@ void parseFBlock(const std::vector<uint8_t>& data, uint32_t offset,
|
||||||
uint32_t ofsKeys = disk.ofsKeys;
|
uint32_t ofsKeys = disk.ofsKeys;
|
||||||
|
|
||||||
if (valueType == 0) {
|
if (valueType == 0) {
|
||||||
// Color: 3 bytes per key.
|
// Color: CImVector (4 bytes RGBA) per key. We extract RGB, ignore A.
|
||||||
// WotLK particle FBlock color keys are stored as BGR in practice for many assets
|
if (ofsKeys + nKeys * 4 > data.size()) return;
|
||||||
// (notably water/waterfall emitters). Decode to RGB explicitly.
|
|
||||||
if (ofsKeys + nKeys * 3 > data.size()) return;
|
|
||||||
fb.vec3Values.reserve(nKeys);
|
fb.vec3Values.reserve(nKeys);
|
||||||
for (uint32_t i = 0; i < nKeys; i++) {
|
for (uint32_t i = 0; i < nKeys; i++) {
|
||||||
uint8_t b = data[ofsKeys + i * 3 + 0];
|
uint8_t r = data[ofsKeys + i * 4 + 0];
|
||||||
uint8_t g = data[ofsKeys + i * 3 + 1];
|
uint8_t g = data[ofsKeys + i * 4 + 1];
|
||||||
uint8_t r = data[ofsKeys + i * 3 + 2];
|
uint8_t b = data[ofsKeys + i * 4 + 2];
|
||||||
|
// byte 3 is alpha, handled separately by the alpha FBlock
|
||||||
fb.vec3Values.emplace_back(r / 255.0f, g / 255.0f, b / 255.0f);
|
fb.vec3Values.emplace_back(r / 255.0f, g / 255.0f, b / 255.0f);
|
||||||
}
|
}
|
||||||
} else if (valueType == 1) {
|
} else if (valueType == 1) {
|
||||||
|
|
|
||||||
|
|
@ -527,7 +527,7 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) {
|
||||||
vec4 viewPos = uView * vec4(aPos, 1.0);
|
vec4 viewPos = uView * vec4(aPos, 1.0);
|
||||||
gl_Position = uProjection * viewPos;
|
gl_Position = uProjection * viewPos;
|
||||||
float dist = max(-viewPos.z, 1.0);
|
float dist = max(-viewPos.z, 1.0);
|
||||||
gl_PointSize = clamp(aSize * 800.0 / dist, 1.0, 256.0);
|
gl_PointSize = clamp(aSize * 400.0 / dist, 1.0, 64.0);
|
||||||
vColor = aColor;
|
vColor = aColor;
|
||||||
vTile = aTile;
|
vTile = aTile;
|
||||||
}
|
}
|
||||||
|
|
@ -542,6 +542,12 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) {
|
||||||
out vec4 FragColor;
|
out vec4 FragColor;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
// Circular soft-edge falloff (GL_POINTS are square by default)
|
||||||
|
vec2 center = gl_PointCoord - vec2(0.5);
|
||||||
|
float dist = length(center);
|
||||||
|
if (dist > 0.5) discard;
|
||||||
|
float edgeFade = smoothstep(0.5, 0.2, dist);
|
||||||
|
|
||||||
vec2 tileCount = max(uTileCount, vec2(1.0));
|
vec2 tileCount = max(uTileCount, vec2(1.0));
|
||||||
float tilesX = tileCount.x;
|
float tilesX = tileCount.x;
|
||||||
float tilesY = tileCount.y;
|
float tilesY = tileCount.y;
|
||||||
|
|
@ -553,6 +559,7 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) {
|
||||||
vec2 uv = gl_PointCoord * tileSize + vec2(col, row) * tileSize;
|
vec2 uv = gl_PointCoord * tileSize + vec2(col, row) * tileSize;
|
||||||
vec4 texColor = texture(uTexture, uv);
|
vec4 texColor = texture(uTexture, uv);
|
||||||
FragColor = texColor * vColor;
|
FragColor = texColor * vColor;
|
||||||
|
FragColor.a *= edgeFade;
|
||||||
if (FragColor.a < 0.01) discard;
|
if (FragColor.a < 0.01) discard;
|
||||||
}
|
}
|
||||||
)";
|
)";
|
||||||
|
|
@ -1520,6 +1527,11 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm::
|
||||||
|
|
||||||
if (!model.hasAnimation || model.disableAnimation) {
|
if (!model.hasAnimation || model.disableAnimation) {
|
||||||
instance.animTime += dtMs;
|
instance.animTime += dtMs;
|
||||||
|
// Wrap animation time for models with particle emitters so emission
|
||||||
|
// rate tracks keep looping instead of running past their keyframes.
|
||||||
|
if (!model.particleEmitters.empty() && instance.animTime > 3333.0f) {
|
||||||
|
instance.animTime = std::fmod(instance.animTime, 3333.0f);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1535,6 +1547,11 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm::
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle animation looping / variation transitions
|
// Handle animation looping / variation transitions
|
||||||
|
// When animDuration is 0 (e.g. "Stand" with infinite loop) but the model
|
||||||
|
// has particle emitters, wrap time so particle emission tracks keep looping.
|
||||||
|
if (instance.animDuration <= 0.0f && !model.particleEmitters.empty()) {
|
||||||
|
instance.animDuration = 3333.0f; // ~3.3s loop for continuous particle effects
|
||||||
|
}
|
||||||
if (instance.animDuration > 0.0f && instance.animTime >= instance.animDuration) {
|
if (instance.animDuration > 0.0f && instance.animTime >= instance.animDuration) {
|
||||||
if (instance.playingVariation) {
|
if (instance.playingVariation) {
|
||||||
// Variation finished — return to idle
|
// Variation finished — return to idle
|
||||||
|
|
@ -1637,16 +1654,20 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm::
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 3: Particle update (sequential — uses RNG, not thread-safe)
|
// Phase 3: Particle update (sequential — uses RNG, not thread-safe)
|
||||||
for (size_t idx : boneWorkIndices_) {
|
// Run for ALL nearby instances with particle emitters, not just those in
|
||||||
if (idx >= instances.size()) continue;
|
// boneWorkIndices_, so particles keep animating even when bone updates are culled.
|
||||||
|
for (size_t idx = 0; idx < instances.size(); ++idx) {
|
||||||
auto& instance = instances[idx];
|
auto& instance = instances[idx];
|
||||||
auto mdlIt = models.find(instance.modelId);
|
auto mdlIt = models.find(instance.modelId);
|
||||||
if (mdlIt == models.end()) continue;
|
if (mdlIt == models.end()) continue;
|
||||||
const auto& model = mdlIt->second;
|
const auto& model = mdlIt->second;
|
||||||
if (!model.particleEmitters.empty()) {
|
if (model.particleEmitters.empty()) continue;
|
||||||
emitParticles(instance, model, deltaTime);
|
// Distance cull: only update particles within visible range
|
||||||
updateParticles(instance, deltaTime);
|
glm::vec3 toCam = instance.position - cachedCamPos_;
|
||||||
}
|
float distSq = glm::dot(toCam, toCam);
|
||||||
|
if (distSq > cachedMaxRenderDistSq_) continue;
|
||||||
|
emitParticles(instance, model, deltaTime);
|
||||||
|
updateParticles(instance, deltaTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2254,6 +2275,17 @@ void M2Renderer::emitParticles(M2Instance& inst, const M2ModelGPU& gpu, float dt
|
||||||
glm::mat3 rotMat = glm::mat3(inst.modelMatrix * boneXform);
|
glm::mat3 rotMat = glm::mat3(inst.modelMatrix * boneXform);
|
||||||
p.velocity = rotMat * dir * speed;
|
p.velocity = rotMat * dir * speed;
|
||||||
|
|
||||||
|
// When emission speed is ~0 and bone animation isn't loaded (.anim files),
|
||||||
|
// particles pile up at the same position. Give them a drift so they
|
||||||
|
// spread outward like a mist/spray effect instead of clustering.
|
||||||
|
if (std::abs(speed) < 0.01f) {
|
||||||
|
p.velocity = rotMat * glm::vec3(
|
||||||
|
distN(particleRng_) * 1.0f,
|
||||||
|
distN(particleRng_) * 1.0f,
|
||||||
|
-dist01(particleRng_) * 0.5f
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const uint32_t tilesX = std::max<uint16_t>(em.textureCols, 1);
|
const uint32_t tilesX = std::max<uint16_t>(em.textureCols, 1);
|
||||||
const uint32_t tilesY = std::max<uint16_t>(em.textureRows, 1);
|
const uint32_t tilesY = std::max<uint16_t>(em.textureRows, 1);
|
||||||
const uint32_t totalTiles = tilesX * tilesY;
|
const uint32_t totalTiles = tilesX * tilesY;
|
||||||
|
|
@ -2291,9 +2323,22 @@ void M2Renderer::updateParticles(M2Instance& inst, float dt) {
|
||||||
}
|
}
|
||||||
// Apply gravity
|
// Apply gravity
|
||||||
if (p.emitterIndex >= 0 && p.emitterIndex < static_cast<int>(gpu.particleEmitters.size())) {
|
if (p.emitterIndex >= 0 && p.emitterIndex < static_cast<int>(gpu.particleEmitters.size())) {
|
||||||
float grav = interpFloat(gpu.particleEmitters[p.emitterIndex].gravity,
|
const auto& pem = gpu.particleEmitters[p.emitterIndex];
|
||||||
|
float grav = interpFloat(pem.gravity,
|
||||||
inst.animTime, inst.currentSequenceIndex,
|
inst.animTime, inst.currentSequenceIndex,
|
||||||
gpu.sequences, gpu.globalSequenceDurations);
|
gpu.sequences, gpu.globalSequenceDurations);
|
||||||
|
// When M2 gravity is 0, apply default gravity so particles arc downward.
|
||||||
|
// Many fountain M2s rely on bone animation (.anim files) we don't load yet.
|
||||||
|
if (grav == 0.0f) {
|
||||||
|
float emSpeed = interpFloat(pem.emissionSpeed,
|
||||||
|
inst.animTime, inst.currentSequenceIndex,
|
||||||
|
gpu.sequences, gpu.globalSequenceDurations);
|
||||||
|
if (std::abs(emSpeed) > 0.1f) {
|
||||||
|
grav = 4.0f; // spray particles
|
||||||
|
} else {
|
||||||
|
grav = 1.5f; // mist/drift particles - gentler fall
|
||||||
|
}
|
||||||
|
}
|
||||||
p.velocity.z -= grav * dt;
|
p.velocity.z -= grav * dt;
|
||||||
}
|
}
|
||||||
p.position += p.velocity * dt;
|
p.position += p.velocity * dt;
|
||||||
|
|
@ -2349,11 +2394,23 @@ void M2Renderer::renderM2Particles(const glm::mat4& view, const glm::mat4& proj)
|
||||||
|
|
||||||
float lifeRatio = p.life / std::max(p.maxLife, 0.001f);
|
float lifeRatio = p.life / std::max(p.maxLife, 0.001f);
|
||||||
glm::vec3 color = interpFBlockVec3(em.particleColor, lifeRatio);
|
glm::vec3 color = interpFBlockVec3(em.particleColor, lifeRatio);
|
||||||
float alpha = interpFBlockFloat(em.particleAlpha, lifeRatio);
|
float alpha = std::min(interpFBlockFloat(em.particleAlpha, lifeRatio), 1.0f);
|
||||||
float scale = interpFBlockFloat(em.particleScale, lifeRatio);
|
float rawScale = interpFBlockFloat(em.particleScale, lifeRatio);
|
||||||
|
|
||||||
// Note: blue-dominant color correction removed — it was over-brightening
|
// FBlock colors are tint values meant to multiply a bright texture.
|
||||||
// water/fountain particles, making them look like spell effects.
|
// Desaturate toward white so particles look like water spray, not neon.
|
||||||
|
color = glm::mix(color, glm::vec3(1.0f), 0.7f);
|
||||||
|
|
||||||
|
// Large-scale particles (>2.0) are volume/backdrop effects meant to be
|
||||||
|
// nearly invisible mist. Fade them heavily since we render as point sprites.
|
||||||
|
if (rawScale > 2.0f) {
|
||||||
|
alpha *= 0.02f;
|
||||||
|
}
|
||||||
|
// Reduce additive particle intensity to prevent blinding overlap
|
||||||
|
if (em.blendingType == 3 || em.blendingType == 4) {
|
||||||
|
alpha *= 0.05f;
|
||||||
|
}
|
||||||
|
float scale = std::min(rawScale, 1.5f);
|
||||||
|
|
||||||
GLuint tex = whiteTexture;
|
GLuint tex = whiteTexture;
|
||||||
if (p.emitterIndex < static_cast<int>(gpu.particleTextures.size())) {
|
if (p.emitterIndex < static_cast<int>(gpu.particleTextures.size())) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue