Parse M2 render flags and apply per-batch blend modes

Water/lava batches in fountain and Ironforge M2 models use non-opaque
blend modes (alpha, additive) defined in the M2 material table. Without
parsing these, they rendered as solid surfaces extending visibly beyond
their containers. Now each batch looks up its blend mode from the
material array and sets the appropriate GL blend function.
This commit is contained in:
Kelsi 2026-02-06 01:54:25 -08:00
parent ad04da31c3
commit 4d80b92c39
4 changed files with 76 additions and 1 deletions

View file

@ -109,6 +109,12 @@ struct M2Batch {
uint16_t submeshLevel = 0; // Submesh level (0=base, 1+=LOD/alternate mesh)
};
// Material / render flags (per-batch blend mode)
struct M2Material {
uint16_t flags; // Render flags (unlit, unfogged, two-sided, etc.)
uint16_t blendMode; // 0=Opaque, 1=AlphaKey, 2=Alpha, 3=Add, 4=Mod, 5=Mod2x, 6=BlendAdd, 7=Screen
};
// Texture transform (UV animation) data
struct M2TextureTransform {
M2AnimationTrack translation; // UV translation keyframes
@ -145,6 +151,7 @@ struct M2Model {
std::vector<M2Batch> batches;
std::vector<M2Texture> textures;
std::vector<uint16_t> textureLookup; // Batch texture index lookup
std::vector<M2Material> materials; // Render flags / blend modes
// Texture transforms (UV animation)
std::vector<M2TextureTransform> textureTransforms;

View file

@ -32,6 +32,7 @@ struct M2ModelGPU {
uint32_t indexCount = 0;
bool hasAlpha = false;
uint16_t textureAnimIndex = 0xFFFF; // 0xFFFF = no texture animation
uint16_t blendMode = 0; // 0=Opaque, 1=AlphaKey, 2=Alpha, 3=Add, etc.
};
GLuint vao = 0;

View file

@ -518,6 +518,20 @@ M2Model M2Loader::load(const std::vector<uint8_t>& m2Data) {
model.textureLookup = readArray<uint16_t>(m2Data, header.ofsTexLookup, header.nTexLookup);
}
// Read render flags / materials (blend modes)
if (header.nRenderFlags > 0 && header.ofsRenderFlags > 0) {
struct M2MaterialDisk { uint16_t flags; uint16_t blendMode; };
auto diskMats = readArray<M2MaterialDisk>(m2Data, header.ofsRenderFlags, header.nRenderFlags);
model.materials.reserve(diskMats.size());
for (const auto& dm : diskMats) {
M2Material mat;
mat.flags = dm.flags;
mat.blendMode = dm.blendMode;
model.materials.push_back(mat);
}
core::Logger::getInstance().debug(" Materials: ", model.materials.size());
}
// Read texture transforms (UV animation data)
if (header.nUVAnimation > 0 && header.ofsUVAnimation > 0) {
// Build per-sequence flags for skipping external .anim data

View file

@ -729,6 +729,11 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
gpuModel.hasTextureAnimation = true;
}
// Store blend mode from material
if (batch.materialIndex < model.materials.size()) {
bgpu.blendMode = model.materials[batch.materialIndex].blendMode;
}
// Resolve texture: batch.textureIndex → textureLookup → allTextures
GLuint tex = whiteTexture;
if (batch.textureIndex < model.textureLookup.size()) {
@ -1253,9 +1258,49 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
}
shader->setUniform("uUVOffset", uvOffset);
// Apply per-batch blend mode from M2 material
// 0=Opaque, 1=AlphaKey, 2=Alpha, 3=Add, 4=Mod, 5=Mod2x, 6=BlendAdd, 7=Screen
bool batchTransparent = false;
switch (batch.blendMode) {
case 0: // Opaque
glBlendFunc(GL_ONE, GL_ZERO);
break;
case 1: // Alpha Key (alpha test, handled by uAlphaTest)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
break;
case 2: // Alpha
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
batchTransparent = true;
break;
case 3: // Additive
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
batchTransparent = true;
break;
case 4: // Mod
glBlendFunc(GL_DST_COLOR, GL_ZERO);
batchTransparent = true;
break;
case 5: // Mod2x
glBlendFunc(GL_DST_COLOR, GL_SRC_COLOR);
batchTransparent = true;
break;
case 6: // BlendAdd
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
batchTransparent = true;
break;
default: // Fallback
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
break;
}
// Disable depth writes for transparent/additive batches
if (batchTransparent && fadeAlpha >= 1.0f) {
glDepthMask(GL_FALSE);
}
bool hasTexture = (batch.texture != 0);
shader->setUniform("uHasTexture", hasTexture);
shader->setUniform("uAlphaTest", batch.hasAlpha);
shader->setUniform("uAlphaTest", batch.blendMode == 1);
if (hasTexture) {
glActiveTexture(GL_TEXTURE0);
@ -1266,6 +1311,14 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
glDrawElements(GL_TRIANGLES, batch.indexCount, GL_UNSIGNED_SHORT,
(void*)(batch.indexStart * sizeof(uint16_t)));
// Restore depth writes and blend func after transparent batch
if (batchTransparent && fadeAlpha >= 1.0f) {
glDepthMask(GL_TRUE);
}
if (batch.blendMode != 0 && batch.blendMode != 2) {
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
}
lastDrawCallCount++;
}