mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Fix M2 white shell artifact from missing textures, add opacity track support
Batches whose named texture fails to load now render invisible instead of white (the swampreeds01a.blp case causing a white shell around aquatic plants). Also implements proper M2 opacity plumbing: - Parse texture weight tracks (M2Track<fixed16>) and color animation alpha tracks (M2Color.alpha) to resolve per-batch opacity at load time - Skip batches with batchOpacity < 0.01 in the render loop - Apply M2Texture.flags (bit0=WrapS, bit1=WrapT) to GL sampler wrap mode - Upload both UV sets (texCoords[0] and texCoords[1]) and select via textureUnit uniform, so batches referencing UV set 1 render correctly
This commit is contained in:
parent
4ba10e772b
commit
9a950ce09f
4 changed files with 141 additions and 15 deletions
|
|
@ -196,6 +196,16 @@ struct M2Model {
|
|||
std::vector<M2TextureTransform> textureTransforms;
|
||||
std::vector<uint16_t> textureTransformLookup;
|
||||
|
||||
// Texture weights (per-batch opacity, from M2Track<fixed16>)
|
||||
// Each entry is the "at-rest" opacity value (0=transparent, 1=opaque).
|
||||
// batch.transparencyIndex → textureTransformLookup[idx] → textureWeights[trackIdx]
|
||||
std::vector<float> textureWeights;
|
||||
|
||||
// Color animation alpha values (from M2Color.alpha M2Track<fixed16>)
|
||||
// One entry per color animation slot; batch.colorIndex indexes directly into this.
|
||||
// Value 0=transparent, 1=opaque. Independent from textureWeights.
|
||||
std::vector<float> colorAlphas;
|
||||
|
||||
// Attachment points (for weapon/effect anchoring)
|
||||
std::vector<M2Attachment> attachments;
|
||||
std::vector<uint16_t> attachmentLookup; // attachment ID → index
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@ struct M2ModelGPU {
|
|||
uint16_t blendMode = 0; // 0=Opaque, 1=AlphaKey, 2=Alpha, 3=Add, etc.
|
||||
uint16_t materialFlags = 0; // M2 material flags (0x01=Unlit, 0x04=TwoSided, 0x10=NoDepthWrite)
|
||||
uint16_t submeshLevel = 0; // LOD level: 0=base, 1=LOD1, 2=LOD2, 3=LOD3
|
||||
uint8_t textureUnit = 0; // UV set index (0=texCoords[0], 1=texCoords[1])
|
||||
uint8_t texFlags = 0; // M2Texture.flags (bit0=WrapS, bit1=WrapT)
|
||||
float batchOpacity = 1.0f; // Resolved texture weight opacity (0=transparent, skip batch)
|
||||
glm::vec3 center = glm::vec3(0.0f); // Center of batch geometry (model space)
|
||||
float glowSize = 1.0f; // Approx radius of batch geometry
|
||||
};
|
||||
|
|
@ -355,7 +358,7 @@ private:
|
|||
uint32_t nextInstanceId = 1;
|
||||
uint32_t lastDrawCallCount = 0;
|
||||
|
||||
GLuint loadTexture(const std::string& path);
|
||||
GLuint loadTexture(const std::string& path, uint32_t texFlags = 0);
|
||||
struct TextureCacheEntry {
|
||||
GLuint id = 0;
|
||||
size_t approxBytes = 0;
|
||||
|
|
|
|||
|
|
@ -965,6 +965,37 @@ M2Model M2Loader::load(const std::vector<uint8_t>& m2Data) {
|
|||
model.textureLookup = readArray<uint16_t>(m2Data, header.ofsTexLookup, header.nTexLookup);
|
||||
}
|
||||
|
||||
// Parse color animation alpha values (M2Color: vec3 color track + fixed16 alpha track).
|
||||
// Each M2Color is two M2TrackDisk headers (20+20 = 40 bytes).
|
||||
// We only need the alpha track (at offset 20) — controls per-batch opacity.
|
||||
if (header.nColors > 0 && header.ofsColors > 0 && header.nColors < 4096) {
|
||||
static constexpr uint32_t M2COLOR_SIZE = 40; // 20-byte color track + 20-byte alpha track
|
||||
model.colorAlphas.reserve(header.nColors);
|
||||
for (uint32_t ci = 0; ci < header.nColors; ci++) {
|
||||
uint32_t alphaTrackOfs = header.ofsColors + ci * M2COLOR_SIZE + 20; // skip vec3 track
|
||||
if (alphaTrackOfs + sizeof(M2TrackDisk) > m2Data.size()) {
|
||||
model.colorAlphas.push_back(1.0f);
|
||||
continue;
|
||||
}
|
||||
M2TrackDisk td = readValue<M2TrackDisk>(m2Data, alphaTrackOfs);
|
||||
float alpha = 1.0f;
|
||||
if (td.nKeys > 0 && td.ofsKeys > 0 && td.nKeys < 4096) {
|
||||
for (uint32_t si = 0; si < td.nKeys; si++) {
|
||||
uint32_t hdOfs = td.ofsKeys + si * 8;
|
||||
if (hdOfs + 8 > m2Data.size()) break;
|
||||
uint32_t count = readValue<uint32_t>(m2Data, hdOfs);
|
||||
uint32_t offset = readValue<uint32_t>(m2Data, hdOfs + 4);
|
||||
if (count == 0 || offset == 0) continue;
|
||||
if (offset + sizeof(uint16_t) > m2Data.size()) continue;
|
||||
uint16_t rawVal = readValue<uint16_t>(m2Data, offset);
|
||||
alpha = std::min(1.0f, rawVal / 32767.0f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
model.colorAlphas.push_back(alpha);
|
||||
}
|
||||
}
|
||||
|
||||
// Read bone lookup table (vertex bone indices reference this to get actual bone index)
|
||||
if (header.nBoneLookupTable > 0 && header.ofsBoneLookupTable > 0) {
|
||||
model.boneLookupTable = readArray<uint16_t>(m2Data, header.ofsBoneLookupTable, header.nBoneLookupTable);
|
||||
|
|
@ -1021,10 +1052,43 @@ M2Model M2Loader::load(const std::vector<uint8_t>& m2Data) {
|
|||
}
|
||||
|
||||
// Read texture transform lookup (nTransLookup)
|
||||
// Note: ofsTransLookup holds the transparency track lookup table (indexed by batch.transparencyIndex).
|
||||
if (header.nTransLookup > 0 && header.ofsTransLookup > 0) {
|
||||
model.textureTransformLookup = readArray<uint16_t>(m2Data, header.ofsTransLookup, header.nTransLookup);
|
||||
}
|
||||
|
||||
// Parse transparency tracks (M2Track<fixed16>) — controls per-batch opacity.
|
||||
// fixed16 = uint16_t / 32767.0f, range 0 (transparent) to 1 (opaque).
|
||||
// We extract the "at-rest" value from the first available keyframe.
|
||||
if (header.nTransparency > 0 && header.ofsTransparency > 0 &&
|
||||
header.nTransparency < 4096) {
|
||||
model.textureWeights.reserve(header.nTransparency);
|
||||
for (uint32_t ti = 0; ti < header.nTransparency; ti++) {
|
||||
uint32_t trackOfs = header.ofsTransparency + ti * sizeof(M2TrackDisk);
|
||||
if (trackOfs + sizeof(M2TrackDisk) > m2Data.size()) {
|
||||
model.textureWeights.push_back(1.0f);
|
||||
continue;
|
||||
}
|
||||
M2TrackDisk td = readValue<M2TrackDisk>(m2Data, trackOfs);
|
||||
float opacity = 1.0f;
|
||||
// Scan sub-arrays until we find one with keyframe data
|
||||
if (td.nKeys > 0 && td.ofsKeys > 0 && td.nKeys < 4096) {
|
||||
for (uint32_t si = 0; si < td.nKeys; si++) {
|
||||
uint32_t hdOfs = td.ofsKeys + si * 8;
|
||||
if (hdOfs + 8 > m2Data.size()) break;
|
||||
uint32_t count = readValue<uint32_t>(m2Data, hdOfs);
|
||||
uint32_t offset = readValue<uint32_t>(m2Data, hdOfs + 4);
|
||||
if (count == 0 || offset == 0) continue;
|
||||
if (offset + sizeof(uint16_t) > m2Data.size()) continue;
|
||||
uint16_t rawVal = readValue<uint16_t>(m2Data, offset);
|
||||
opacity = std::min(1.0f, rawVal / 32767.0f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
model.textureWeights.push_back(opacity);
|
||||
}
|
||||
}
|
||||
|
||||
// Read attachment points (vanilla uses 48-byte struct, WotLK uses 40-byte)
|
||||
if (header.nAttachments > 0 && header.ofsAttachments > 0) {
|
||||
model.attachments.reserve(header.nAttachments);
|
||||
|
|
|
|||
|
|
@ -274,6 +274,7 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) {
|
|||
layout (location = 2) in vec2 aTexCoord;
|
||||
layout (location = 3) in vec4 aBoneWeights;
|
||||
layout (location = 4) in vec4 aBoneIndicesF;
|
||||
layout (location = 5) in vec2 aTexCoord2;
|
||||
|
||||
uniform mat4 uModel;
|
||||
uniform mat4 uView;
|
||||
|
|
@ -281,6 +282,7 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) {
|
|||
uniform bool uUseBones;
|
||||
uniform mat4 uBones[128];
|
||||
uniform vec2 uUVOffset;
|
||||
uniform int uTexCoordSet; // 0 = UV set 0, 1 = UV set 1
|
||||
out vec3 FragPos;
|
||||
out vec3 Normal;
|
||||
out vec2 TexCoord;
|
||||
|
|
@ -302,7 +304,7 @@ bool M2Renderer::initialize(pipeline::AssetManager* assets) {
|
|||
vec4 worldPos = uModel * vec4(pos, 1.0);
|
||||
FragPos = worldPos.xyz;
|
||||
Normal = mat3(uModel) * norm;
|
||||
TexCoord = aTexCoord + uUVOffset;
|
||||
TexCoord = (uTexCoordSet == 1 ? aTexCoord2 : aTexCoord) + uUVOffset;
|
||||
|
||||
gl_Position = uProjection * uView * worldPos;
|
||||
}
|
||||
|
|
@ -1017,8 +1019,8 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
|
|||
}
|
||||
|
||||
// Create VBO with interleaved vertex data
|
||||
// Format: position (3), normal (3), texcoord (2), boneWeights (4), boneIndices (4 as float)
|
||||
const size_t floatsPerVertex = 16;
|
||||
// Format: position (3), normal (3), texcoord0 (2), texcoord1 (2), boneWeights (4), boneIndices (4 as float)
|
||||
const size_t floatsPerVertex = 18;
|
||||
std::vector<float> vertexData;
|
||||
vertexData.reserve(model.vertices.size() * floatsPerVertex);
|
||||
|
||||
|
|
@ -1031,6 +1033,8 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
|
|||
vertexData.push_back(v.normal.z);
|
||||
vertexData.push_back(v.texCoords[0].x);
|
||||
vertexData.push_back(v.texCoords[0].y);
|
||||
vertexData.push_back(v.texCoords[1].x);
|
||||
vertexData.push_back(v.texCoords[1].y);
|
||||
// Bone weights (normalized 0-1)
|
||||
float w0 = v.boneWeights[0] / 255.0f;
|
||||
float w1 = v.boneWeights[1] / 255.0f;
|
||||
|
|
@ -1069,40 +1073,49 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
|
|||
glEnableVertexAttribArray(1);
|
||||
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, stride, (void*)(3 * sizeof(float)));
|
||||
|
||||
// TexCoord
|
||||
// TexCoord0
|
||||
glEnableVertexAttribArray(2);
|
||||
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, stride, (void*)(6 * sizeof(float)));
|
||||
|
||||
// TexCoord1
|
||||
glEnableVertexAttribArray(5);
|
||||
glVertexAttribPointer(5, 2, GL_FLOAT, GL_FALSE, stride, (void*)(8 * sizeof(float)));
|
||||
|
||||
// Bone Weights
|
||||
glEnableVertexAttribArray(3);
|
||||
glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, stride, (void*)(8 * sizeof(float)));
|
||||
glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, stride, (void*)(10 * sizeof(float)));
|
||||
|
||||
// Bone Indices (as integer attribute)
|
||||
glEnableVertexAttribArray(4);
|
||||
glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, stride, (void*)(12 * sizeof(float)));
|
||||
glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, stride, (void*)(14 * sizeof(float)));
|
||||
|
||||
glBindVertexArray(0);
|
||||
|
||||
// Load ALL textures from the model into a local vector
|
||||
// Load ALL textures from the model into a local vector.
|
||||
// textureLoadFailed[i] is true if texture[i] had a named path that failed to load.
|
||||
// Such batches are hidden (batchOpacity=0) rather than rendered white.
|
||||
std::vector<GLuint> allTextures;
|
||||
std::vector<bool> textureLoadFailed;
|
||||
if (assetManager) {
|
||||
for (size_t ti = 0; ti < model.textures.size(); ti++) {
|
||||
const auto& tex = model.textures[ti];
|
||||
if (!tex.filename.empty()) {
|
||||
GLuint texId = loadTexture(tex.filename);
|
||||
if (texId == whiteTexture) {
|
||||
GLuint texId = loadTexture(tex.filename, tex.flags);
|
||||
bool failed = (texId == whiteTexture);
|
||||
if (failed) {
|
||||
LOG_WARNING("M2 model ", model.name, " texture[", ti, "] failed to load: ", tex.filename);
|
||||
}
|
||||
if (isInvisibleTrap) {
|
||||
LOG_INFO(" InvisibleTrap texture[", ti, "]: ", tex.filename, " -> ", (texId == whiteTexture ? "WHITE" : "OK"));
|
||||
LOG_INFO(" InvisibleTrap texture[", ti, "]: ", tex.filename, " -> ", (failed ? "WHITE" : "OK"));
|
||||
}
|
||||
allTextures.push_back(texId);
|
||||
textureLoadFailed.push_back(failed);
|
||||
} else {
|
||||
LOG_WARNING("M2 model ", model.name, " texture[", ti, "] has empty filename (using white fallback)");
|
||||
if (isInvisibleTrap) {
|
||||
LOG_INFO(" InvisibleTrap texture[", ti, "]: EMPTY (using white fallback)");
|
||||
}
|
||||
allTextures.push_back(whiteTexture);
|
||||
textureLoadFailed.push_back(false); // Empty filename = intentional white (type!=0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1146,16 +1159,38 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
|
|||
|
||||
// Resolve texture: batch.textureIndex → textureLookup → allTextures
|
||||
GLuint tex = whiteTexture;
|
||||
bool texFailed = false;
|
||||
if (batch.textureIndex < model.textureLookup.size()) {
|
||||
uint16_t texIdx = model.textureLookup[batch.textureIndex];
|
||||
if (texIdx < allTextures.size()) {
|
||||
tex = allTextures[texIdx];
|
||||
texFailed = (texIdx < textureLoadFailed.size()) && textureLoadFailed[texIdx];
|
||||
}
|
||||
if (texIdx < model.textures.size()) {
|
||||
bgpu.texFlags = static_cast<uint8_t>(model.textures[texIdx].flags & 0x3);
|
||||
}
|
||||
} else if (!allTextures.empty()) {
|
||||
tex = allTextures[0];
|
||||
texFailed = !textureLoadFailed.empty() && textureLoadFailed[0];
|
||||
}
|
||||
bgpu.texture = tex;
|
||||
bgpu.hasAlpha = (tex != 0 && tex != whiteTexture);
|
||||
bgpu.textureUnit = static_cast<uint8_t>(batch.textureUnit & 0x1);
|
||||
|
||||
// Resolve opacity: texture weight track × color animation alpha
|
||||
// Batches whose texture failed to load are hidden (avoid white shell artifacts)
|
||||
bgpu.batchOpacity = texFailed ? 0.0f : 1.0f;
|
||||
// Texture weight track (via transparency lookup)
|
||||
if (batch.transparencyIndex < model.textureTransformLookup.size()) {
|
||||
uint16_t trackIdx = model.textureTransformLookup[batch.transparencyIndex];
|
||||
if (trackIdx < model.textureWeights.size()) {
|
||||
bgpu.batchOpacity *= model.textureWeights[trackIdx];
|
||||
}
|
||||
}
|
||||
// Color animation alpha (M2Color.alpha, indexed directly by colorIndex)
|
||||
if (batch.colorIndex < model.colorAlphas.size()) {
|
||||
bgpu.batchOpacity *= model.colorAlphas[batch.colorIndex];
|
||||
}
|
||||
|
||||
// Compute batch center and radius for glow sprite positioning
|
||||
if (bgpu.blendMode >= 3 && batch.indexCount > 0) {
|
||||
|
|
@ -1217,6 +1252,7 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
|
|||
LOG_DEBUG("Loaded M2 model: ", model.name, " (", models[modelId].vertexCount, " vertices, ",
|
||||
models[modelId].indexCount / 3, " triangles, ", models[modelId].batches.size(), " batches)");
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1807,6 +1843,7 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
|
|||
static uint8_t lastBlendMode = 255; // Invalid initial value
|
||||
static bool depthMaskState = true; // Track current depth mask state
|
||||
static glm::vec2 lastUVOffset = glm::vec2(-999.0f); // Track UV offset state
|
||||
static int lastTexCoordSet = -1; // Track active UV set (0 or 1)
|
||||
|
||||
// Reset state tracking at start of frame to handle shader rebinds
|
||||
lastBoundTexture = 0;
|
||||
|
|
@ -1818,6 +1855,7 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
|
|||
lastBlendMode = 255;
|
||||
depthMaskState = true;
|
||||
lastUVOffset = glm::vec2(-999.0f);
|
||||
lastTexCoordSet = -1;
|
||||
|
||||
// Set texture unit once per frame instead of per-batch
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
|
|
@ -1912,6 +1950,9 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
|
|||
// Skip batches that don't match target LOD level
|
||||
if (batch.submeshLevel != targetLOD) continue;
|
||||
|
||||
// Skip batches with zero opacity from texture weight tracks (should be invisible)
|
||||
if (batch.batchOpacity < 0.01f) continue;
|
||||
|
||||
// Additive/mod batches (glow halos, light effects): collect as glow sprites
|
||||
// instead of rendering the mesh geometry which appears as flat orange disks.
|
||||
if (batch.blendMode >= 3) {
|
||||
|
|
@ -2022,6 +2063,13 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
|
|||
lastBoundTexture = batch.texture;
|
||||
}
|
||||
|
||||
// UV set selector (textureUnit: 0=UV0, 1=UV1)
|
||||
int texCoordSet = static_cast<int>(batch.textureUnit);
|
||||
if (texCoordSet != lastTexCoordSet) {
|
||||
shader->setUniform("uTexCoordSet", texCoordSet);
|
||||
lastTexCoordSet = texCoordSet;
|
||||
}
|
||||
|
||||
glDrawElements(GL_TRIANGLES, batch.indexCount, GL_UNSIGNED_SHORT,
|
||||
(void*)(batch.indexStart * sizeof(uint16_t)));
|
||||
|
||||
|
|
@ -2760,7 +2808,7 @@ void M2Renderer::cleanupUnusedModels() {
|
|||
}
|
||||
}
|
||||
|
||||
GLuint M2Renderer::loadTexture(const std::string& path) {
|
||||
GLuint M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) {
|
||||
auto normalizeKey = [](std::string key) {
|
||||
std::replace(key.begin(), key.end(), '/', '\\');
|
||||
std::transform(key.begin(), key.end(), key.begin(),
|
||||
|
|
@ -2804,8 +2852,9 @@ GLuint M2Renderer::loadTexture(const std::string& path) {
|
|||
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
|
||||
// M2Texture flags: bit 0 = WrapS (1=repeat, 0=clamp), bit 1 = WrapT
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, (texFlags & 0x1) ? GL_REPEAT : GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, (texFlags & 0x2) ? GL_REPEAT : GL_CLAMP_TO_EDGE);
|
||||
glGenerateMipmap(GL_TEXTURE_2D);
|
||||
applyAnisotropicFiltering();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue