mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Ironforge Great Forge lava, magma water rendering, LavaSteam particle effects
- Add magma/slime rendering path to water shader (fbm noise, crust/molten/core coloring) - Fix WMO liquid height filter rejecting high-altitude zones like Ironforge (Z>300) - Allow interior WMO magma/slime MLIQ groups to load (skip only water/ocean) - Mark LAVASTEAM.m2 as spell effect for proper additive blend, hide emission mesh - Add isLavaModel flag for M2 ForgeLava/LavaPots UV scroll fallback - Add isLava material detection in WMO renderer for lava texture UV animation - Fix WMO material UBO colors for magma (was blue, now orange-red)
This commit is contained in:
parent
2c5b7cd368
commit
a24fe4cc45
10 changed files with 158 additions and 12 deletions
|
|
@ -155,6 +155,52 @@ void main() {
|
|||
float time = fogParams.z;
|
||||
float basicType = push.liquidBasicType;
|
||||
|
||||
// ============================================================
|
||||
// Magma / Slime — self-luminous flowing surfaces, skip water path
|
||||
// ============================================================
|
||||
if (basicType > 1.5) {
|
||||
float dist = length(viewPos.xyz - FragPos);
|
||||
vec2 flowUV = FragPos.xy;
|
||||
|
||||
bool isMagma = basicType < 2.5;
|
||||
|
||||
// Multi-octave flowing noise for organic lava look
|
||||
float n1 = fbmNoise(flowUV * 0.06 + vec2(time * 0.02, time * 0.03), time * 0.4);
|
||||
float n2 = fbmNoise(flowUV * 0.10 + vec2(-time * 0.015, time * 0.025), time * 0.3);
|
||||
float n3 = noiseValue(flowUV * 0.25 + vec2(time * 0.04, -time * 0.02));
|
||||
float flow = n1 * 0.45 + n2 * 0.35 + n3 * 0.20;
|
||||
|
||||
// Dark crust vs bright molten core
|
||||
vec3 crustColor, hotColor, coreColor;
|
||||
if (isMagma) {
|
||||
crustColor = vec3(0.15, 0.04, 0.01); // dark cooled rock
|
||||
hotColor = vec3(1.0, 0.45, 0.05); // orange molten
|
||||
coreColor = vec3(1.0, 0.85, 0.3); // bright yellow-white core
|
||||
} else {
|
||||
crustColor = vec3(0.05, 0.15, 0.02);
|
||||
hotColor = vec3(0.3, 0.8, 0.15);
|
||||
coreColor = vec3(0.5, 1.0, 0.3);
|
||||
}
|
||||
|
||||
// Three-tier color: crust → molten → hot core
|
||||
float crustMask = smoothstep(0.25, 0.50, flow);
|
||||
float coreMask = smoothstep(0.60, 0.80, flow);
|
||||
vec3 color = mix(crustColor, hotColor, crustMask);
|
||||
color = mix(color, coreColor, coreMask);
|
||||
|
||||
// Subtle pulsing emissive glow
|
||||
float pulse = 1.0 + 0.15 * sin(time * 1.5 + flow * 6.0);
|
||||
color *= pulse;
|
||||
|
||||
// Emissive brightening for hot areas
|
||||
color *= 1.0 + coreMask * 0.6;
|
||||
|
||||
float fogFactor = clamp((fogParams.y - dist) / (fogParams.y - fogParams.x), 0.0, 1.0);
|
||||
color = mix(fogColor.rgb, color, fogFactor);
|
||||
outColor = vec4(color, 0.97);
|
||||
return;
|
||||
}
|
||||
|
||||
vec2 screenUV = gl_FragCoord.xy / vec2(textureSize(SceneColor, 0));
|
||||
|
||||
// --- Normal computation ---
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -28,6 +28,7 @@ layout(set = 1, binding = 1) uniform WMOMaterial {
|
|||
int pomMaxSamples;
|
||||
float heightMapVariance;
|
||||
float normalMapStrength;
|
||||
int isLava;
|
||||
};
|
||||
|
||||
layout(set = 1, binding = 2) uniform sampler2D uNormalHeightMap;
|
||||
|
|
@ -120,6 +121,14 @@ void main() {
|
|||
// Compute final UV (with POM if enabled)
|
||||
vec2 finalUV = TexCoord;
|
||||
|
||||
// Lava/magma: scroll UVs for flowing effect
|
||||
if (isLava != 0) {
|
||||
float time = fogParams.z;
|
||||
// Scroll both axes — pools get horizontal flow, waterfalls get vertical flow
|
||||
// (UV orientation depends on mesh, so animate both)
|
||||
finalUV += vec2(time * 0.04, time * 0.06);
|
||||
}
|
||||
|
||||
// Build TBN matrix
|
||||
vec3 T = normalize(Tangent);
|
||||
vec3 B = normalize(Bitangent);
|
||||
|
|
@ -170,7 +179,10 @@ void main() {
|
|||
shadow = mix(1.0, shadow, shadowParams.y);
|
||||
}
|
||||
|
||||
if (unlit != 0) {
|
||||
if (isLava != 0) {
|
||||
// Lava is self-luminous — bright emissive, no shadows
|
||||
result = texColor.rgb * 1.5;
|
||||
} else if (unlit != 0) {
|
||||
result = texColor.rgb * shadow;
|
||||
} else if (isInterior != 0) {
|
||||
vec3 mocv = max(VertColor.rgb, vec3(0.5));
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -119,6 +119,7 @@ struct M2ModelGPU {
|
|||
bool isElvenLike = false; // Model name matches elf/elven/quel (precomputed)
|
||||
bool isLanternLike = false; // Model name matches lantern/lamp/light (precomputed)
|
||||
bool isKoboldFlame = false; // Model name matches kobold+(candle/torch/mine) (precomputed)
|
||||
bool isLavaModel = false; // Model name contains lava/molten/magma (UV scroll fallback)
|
||||
bool hasTextureAnimation = false; // True if any batch has UV animation
|
||||
|
||||
// Particle emitter data (kept from M2Model)
|
||||
|
|
|
|||
|
|
@ -340,7 +340,9 @@ private:
|
|||
int32_t pomMaxSamples; // 36 (max ray-march steps)
|
||||
float heightMapVariance; // 40 (low variance = skip POM)
|
||||
float normalMapStrength; // 44 (0=flat, 1=full, 2=exaggerated)
|
||||
}; // 48 bytes total
|
||||
int32_t isLava; // 48 (1=lava/magma UV scroll)
|
||||
float pad[3]; // 52-60 padding to 64 bytes
|
||||
}; // 64 bytes total
|
||||
|
||||
/**
|
||||
* WMO group GPU resources
|
||||
|
|
@ -380,6 +382,7 @@ private:
|
|||
bool unlit = false;
|
||||
bool isTransparent = false; // blendMode >= 2
|
||||
bool isWindow = false; // F_SIDN or F_WINDOW material
|
||||
bool isLava = false; // lava/magma texture (UV scroll)
|
||||
// For multi-draw: store index ranges
|
||||
struct DrawRange { uint32_t firstIndex; uint32_t indexCount; };
|
||||
std::vector<DrawRange> draws;
|
||||
|
|
|
|||
|
|
@ -1131,10 +1131,32 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
|
|||
(lowerName.find("hazardlight") != std::string::npos) ||
|
||||
(lowerName.find("lavasplash") != std::string::npos) ||
|
||||
(lowerName.find("lavabubble") != std::string::npos) ||
|
||||
(lowerName.find("lavasteam") != std::string::npos) ||
|
||||
(lowerName.find("wisps") != std::string::npos);
|
||||
gpuModel.isSpellEffect = effectByName ||
|
||||
(hasParticles && model.vertices.size() <= 200 &&
|
||||
model.particleEmitters.size() >= 3);
|
||||
gpuModel.isLavaModel =
|
||||
(lowerName.find("forgelava") != std::string::npos) ||
|
||||
(lowerName.find("lavapot") != std::string::npos) ||
|
||||
(lowerName.find("lavaflow") != std::string::npos);
|
||||
if (lowerName.find("lava") != std::string::npos || lowerName.find("steam") != std::string::npos) {
|
||||
LOG_WARNING("M2 LAVA/STEAM: '", model.name, "' isSpellEffect=", gpuModel.isSpellEffect ? "Y" : "N",
|
||||
" effectByName=", effectByName ? "Y" : "N",
|
||||
" particles=", model.particleEmitters.size(),
|
||||
" verts=", model.vertices.size(),
|
||||
" batches=", model.batches.size(),
|
||||
" texTransforms=", model.textureTransforms.size(),
|
||||
" texTransformLookup=", model.textureTransformLookup.size(),
|
||||
" isLavaModel=", gpuModel.isLavaModel ? "Y" : "N");
|
||||
for (size_t bi = 0; bi < model.batches.size(); bi++) {
|
||||
const auto& b = model.batches[bi];
|
||||
uint8_t bm = (b.materialIndex < model.materials.size()) ? model.materials[b.materialIndex].blendMode : 255;
|
||||
uint16_t mf = (b.materialIndex < model.materials.size()) ? model.materials[b.materialIndex].flags : 0;
|
||||
LOG_WARNING(" batch[", bi, "]: blend=", (int)bm, " matFlags=0x", std::hex, mf, std::dec,
|
||||
" texAnimIdx=", b.textureAnimIndex, " idxCount=", b.indexCount);
|
||||
}
|
||||
}
|
||||
gpuModel.isInstancePortal =
|
||||
(lowerName.find("instanceportal") != std::string::npos) ||
|
||||
(lowerName.find("instancenewportal") != std::string::npos) ||
|
||||
|
|
@ -2357,6 +2379,9 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
|
|||
}
|
||||
|
||||
const bool foliageLikeModel = model.isFoliageLike;
|
||||
// Particle-dominant spell effects: mesh is emission geometry, render dim
|
||||
const bool particleDominantEffect = model.isSpellEffect &&
|
||||
!model.particleEmitters.empty() && model.batches.size() <= 2;
|
||||
for (const auto& batch : model.batches) {
|
||||
if (batch.indexCount == 0) continue;
|
||||
if (!model.isGroundDetail && batch.submeshLevel != targetLOD) continue;
|
||||
|
|
@ -2421,6 +2446,12 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
|
|||
}
|
||||
}
|
||||
}
|
||||
// Lava M2 models: fallback UV scroll if no texture animation
|
||||
if (model.isLavaModel && uvOffset == glm::vec2(0.0f)) {
|
||||
static auto startTime = std::chrono::steady_clock::now();
|
||||
float t = std::chrono::duration<float>(std::chrono::steady_clock::now() - startTime).count();
|
||||
uvOffset = glm::vec2(t * 0.03f, -t * 0.08f);
|
||||
}
|
||||
|
||||
// Foliage/card-like batches render more stably as cutout (depth-write on)
|
||||
// instead of alpha-blended sorting.
|
||||
|
|
@ -2498,6 +2529,10 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
|
|||
pc.useBones = useBones ? 1 : 0;
|
||||
pc.isFoliage = model.shadowWindFoliage ? 1 : 0;
|
||||
pc.fadeAlpha = instanceFadeAlpha;
|
||||
// Particle-dominant effects: mesh is emission geometry, don't render
|
||||
if (particleDominantEffect && batch.blendMode <= 1) {
|
||||
continue;
|
||||
}
|
||||
vkCmdPushConstants(cmd, pipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(pc), &pc);
|
||||
|
||||
vkCmdDrawIndexed(cmd, batch.indexCount, 1, batch.indexStart, 0, 0);
|
||||
|
|
@ -2948,8 +2983,23 @@ void M2Renderer::emitParticles(M2Instance& inst, const M2ModelGPU& gpu, float dt
|
|||
std::uniform_real_distribution<float> distN(-1.0f, 1.0f);
|
||||
std::uniform_int_distribution<int> distTile;
|
||||
|
||||
static uint32_t steamDiagCounter = 0;
|
||||
bool steamDiag = (gpu.isSpellEffect && gpu.particleEmitters.size() >= 6 && steamDiagCounter < 3);
|
||||
|
||||
for (size_t ei = 0; ei < gpu.particleEmitters.size(); ei++) {
|
||||
const auto& em = gpu.particleEmitters[ei];
|
||||
if (steamDiag) {
|
||||
float rate = interpFloat(em.emissionRate, inst.animTime, inst.currentSequenceIndex,
|
||||
gpu.sequences, gpu.globalSequenceDurations);
|
||||
float life = interpFloat(em.lifespan, inst.animTime, inst.currentSequenceIndex,
|
||||
gpu.sequences, gpu.globalSequenceDurations);
|
||||
LOG_WARNING("STEAM PARTICLE DIAG emitter[", ei, "]: enabled=", em.enabled ? "Y" : "N",
|
||||
" rate=", rate, " life=", life,
|
||||
" animTime=", inst.animTime, " seq=", inst.currentSequenceIndex,
|
||||
" bone=", em.bone, " blendType=", (int)em.blendingType,
|
||||
" globalSeq=", em.emissionRate.globalSequence,
|
||||
" rateSeqs=", em.emissionRate.sequences.size());
|
||||
}
|
||||
if (!em.enabled) continue;
|
||||
|
||||
float rate = interpFloat(em.emissionRate, inst.animTime, inst.currentSequenceIndex,
|
||||
|
|
@ -3038,6 +3088,12 @@ void M2Renderer::emitParticles(M2Instance& inst, const M2ModelGPU& gpu, float dt
|
|||
inst.emitterAccumulators[ei] = 0.0f;
|
||||
}
|
||||
}
|
||||
if (steamDiag) {
|
||||
LOG_WARNING("STEAM PARTICLE DIAG: totalParticles=", inst.particles.size(),
|
||||
" sequences=", gpu.sequences.size(),
|
||||
" globalSeqDurations=", gpu.globalSequenceDurations.size());
|
||||
steamDiagCounter++;
|
||||
}
|
||||
}
|
||||
|
||||
void M2Renderer::updateParticles(M2Instance& inst, float dt) {
|
||||
|
|
|
|||
|
|
@ -835,10 +835,24 @@ bool TerrainManager::advanceFinalization(FinalizingTile& ft) {
|
|||
modelMatrix = glm::rotate(modelMatrix, wmoReady.rotation.z, glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
modelMatrix = glm::rotate(modelMatrix, wmoReady.rotation.y, glm::vec3(0.0f, 1.0f, 0.0f));
|
||||
modelMatrix = glm::rotate(modelMatrix, wmoReady.rotation.x, glm::vec3(1.0f, 0.0f, 0.0f));
|
||||
for (const auto& group : wmoReady.model.groups) {
|
||||
for (size_t gi = 0; gi < wmoReady.model.groups.size(); gi++) {
|
||||
const auto& group = wmoReady.model.groups[gi];
|
||||
if (!group.liquid.hasLiquid()) continue;
|
||||
// Skip interior groups — their liquid is for indoor areas
|
||||
if (group.flags & 0x2000) continue;
|
||||
uint16_t lt = group.liquid.materialId;
|
||||
uint8_t basicType = (lt == 0) ? 0 : ((lt - 1) % 4);
|
||||
bool isInterior = (group.flags & 0x2000) != 0;
|
||||
LOG_WARNING("WMO MLIQ group", gi, ": flags=0x", std::hex, group.flags, std::dec,
|
||||
" materialId=", lt, " basicType=", (int)basicType,
|
||||
" interior=", isInterior ? "Y" : "N",
|
||||
" xVerts=", group.liquid.xVerts, " yVerts=", group.liquid.yVerts);
|
||||
// Skip interior water/ocean but keep magma/slime (e.g. Ironforge lava)
|
||||
if (isInterior) {
|
||||
if (basicType < 2) {
|
||||
LOG_WARNING(" -> SKIPPED (interior water/ocean)");
|
||||
continue;
|
||||
}
|
||||
LOG_WARNING(" -> LOADING (interior magma/slime)");
|
||||
}
|
||||
waterRenderer->loadFromWMO(group.liquid, modelMatrix, wmoInstId);
|
||||
loadedLiquids++;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -544,9 +544,14 @@ void WaterRenderer::updateMaterialUBO(WaterSurface& surface) {
|
|||
// WMO liquid material override
|
||||
if (surface.wmoId != 0) {
|
||||
const uint8_t basicType = (surface.liquidType == 0) ? 0 : ((surface.liquidType - 1) % 4);
|
||||
if (basicType == 2 || basicType == 3) {
|
||||
color = glm::vec4(0.2f, 0.4f, 0.6f, 1.0f);
|
||||
alpha = 0.45f;
|
||||
if (basicType == 2) {
|
||||
// Magma — bright orange-red, opaque
|
||||
color = glm::vec4(1.0f, 0.35f, 0.05f, 1.0f);
|
||||
alpha = 0.95f;
|
||||
} else if (basicType == 3) {
|
||||
// Slime — green, semi-opaque
|
||||
color = glm::vec4(0.2f, 0.6f, 0.1f, 1.0f);
|
||||
alpha = 0.85f;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -935,7 +940,7 @@ void WaterRenderer::loadFromWMO([[maybe_unused]] const pipeline::WMOLiquid& liqu
|
|||
surface.origin.z = adjustedZ;
|
||||
surface.position.z = adjustedZ;
|
||||
|
||||
if (surface.origin.z > 300.0f || surface.origin.z < -100.0f) return;
|
||||
if (surface.origin.z > 2000.0f || surface.origin.z < -500.0f) return;
|
||||
|
||||
// Build tile mask from MLIQ flags and per-vertex heights
|
||||
size_t tileCount = static_cast<size_t>(surface.width) * static_cast<size_t>(surface.height);
|
||||
|
|
|
|||
|
|
@ -596,20 +596,26 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
|||
// so we additionally check for "window" or "glass" in the texture path to
|
||||
// distinguish actual glass from lamp post geometry.
|
||||
bool isWindow = false;
|
||||
bool isLava = false;
|
||||
if (batch.materialId < modelData.materialTextureIndices.size()) {
|
||||
uint32_t ti = modelData.materialTextureIndices[batch.materialId];
|
||||
if (ti < modelData.textureNames.size()) {
|
||||
const auto& texName = modelData.textureNames[ti];
|
||||
// Case-insensitive search for "window" or "glass"
|
||||
// Case-insensitive search for material types
|
||||
std::string texNameLower = texName;
|
||||
std::transform(texNameLower.begin(), texNameLower.end(), texNameLower.begin(), ::tolower);
|
||||
isWindow = (texNameLower.find("window") != std::string::npos ||
|
||||
texNameLower.find("glass") != std::string::npos);
|
||||
isLava = (texNameLower.find("lava") != std::string::npos ||
|
||||
texNameLower.find("molten") != std::string::npos ||
|
||||
texNameLower.find("magma") != std::string::npos);
|
||||
if (isLava) {
|
||||
LOG_WARNING("WMO LAVA BATCH: tex='", texName, "' matId=", batch.materialId,
|
||||
" blend=", blendMode, " flags=0x", std::hex, matFlags, std::dec);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
BatchKey key{ reinterpret_cast<uintptr_t>(tex), alphaTest, unlit, isWindow };
|
||||
auto& mb = batchMap[key];
|
||||
if (mb.draws.empty()) {
|
||||
|
|
@ -619,6 +625,7 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
|||
mb.unlit = unlit;
|
||||
mb.isTransparent = (blendMode >= 2);
|
||||
mb.isWindow = isWindow;
|
||||
mb.isLava = isLava;
|
||||
// Look up normal/height map from texture cache
|
||||
if (hasTexture && tex != whiteTexture_.get()) {
|
||||
for (const auto& [cacheKey, cacheEntry] : textureCache) {
|
||||
|
|
@ -668,6 +675,7 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
|||
}
|
||||
matData.heightMapVariance = mb.heightMapVariance;
|
||||
matData.normalMapStrength = normalMapStrength_;
|
||||
matData.isLava = mb.isLava ? 1 : 0;
|
||||
if (matBuf.info.pMappedData) {
|
||||
memcpy(matBuf.info.pMappedData, &matData, sizeof(matData));
|
||||
}
|
||||
|
|
@ -789,6 +797,7 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
|||
doodadTemplate.m2Path = m2Path;
|
||||
doodadTemplate.localTransform = localTransform;
|
||||
modelData.doodadTemplates.push_back(doodadTemplate);
|
||||
|
||||
}
|
||||
|
||||
if (!modelData.doodadTemplates.empty()) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue