#include "NifFile.h" #include #include #include #include #include #include #include "json.hpp" namespace { constexpr uint32_t Version(uint32_t a, uint32_t b, uint32_t c, uint32_t d) { return (a << 24) | (b << 16) | (c << 8) | d; } constexpr uint32_t MIN_VERSION = Version(20, 2, 0, 5); // strings in a table and block sizes in the header constexpr uint32_t MAX_VERSION = Version(20, 3, 0, 9); constexpr uint32_t MAX_BLOCKS = 200000; constexpr uint32_t MAX_DEPTH = 64; constexpr uint16_t APP_CULLED = 1; // NiAVObject flag: hidden // Little-endian reads that fail (and stay failed) instead of running past the end class Reader { public: explicit Reader(std::string_view data) : m_Data(data) {} template T Read() { T value{}; if (!m_Ok || sizeof(T) > m_Data.size() - m_Pos) { m_Ok = false; return value; } std::memcpy(&value, m_Data.data() + m_Pos, sizeof(T)); m_Pos += sizeof(T); return value; } float Float() { return Read(); } uint8_t U8() { return Read(); } uint16_t U16() { return Read(); } uint32_t U32() { return Read(); } int32_t I32() { return Read(); } void Skip(uint64_t bytes) { if (!m_Ok || bytes > m_Data.size() - m_Pos) { m_Ok = false; return; } m_Pos += static_cast(bytes); } // `count` values of T, or empty (and failed) if there aren't that many template std::vector Array(uint64_t count) { std::vector values; if (!m_Ok || count > (m_Data.size() - m_Pos) / sizeof(T)) { m_Ok = false; return values; } values.resize(static_cast(count)); std::memcpy(values.data(), m_Data.data() + m_Pos, static_cast(count) * sizeof(T)); m_Pos += static_cast(count) * sizeof(T); return values; } std::string SizedString() { const auto length = U32(); if (!m_Ok || length > m_Data.size() - m_Pos) { m_Ok = false; return {}; } std::string value(m_Data.substr(m_Pos, length)); m_Pos += length; return value; } bool Ok() const { return m_Ok; } size_t Position() const { return m_Pos; } private: std::string_view m_Data; size_t m_Pos = 0; bool m_Ok = true; }; // Rotation (row-major, for column vectors), translation and uniform scale: p' = t + s * R p struct Transform { std::array r{ 1, 0, 0, 0, 1, 0, 0, 0, 1 }; std::array t{}; float s{ 1.0f }; std::array Rotate(const std::array& v) const { return { r[0] * v[0] + r[1] * v[1] + r[2] * v[2], r[3] * v[0] + r[4] * v[1] + r[5] * v[2], r[6] * v[0] + r[7] * v[1] + r[8] * v[2] }; } std::array Apply(const std::array& v) const { const auto rotated = Rotate(v); return { t[0] + s * rotated[0], t[1] + s * rotated[1], t[2] + s * rotated[2] }; } // This transform, then `local` inside it (parent * child) Transform Then(const Transform& local) const { Transform out; for (int row = 0; row < 3; row++) { for (int col = 0; col < 3; col++) { out.r[row * 3 + col] = r[row * 3] * local.r[col] + r[row * 3 + 1] * local.r[3 + col] + r[row * 3 + 2] * local.r[6 + col]; } } out.t = Apply(local.t); out.s = s * local.s; return out; } }; // The properties in effect at a point of the tree: a child's own property of a type replaces its parent's struct Properties { int32_t material = -1; int32_t alpha = -1; int32_t texturing = -1; int32_t vertexColor = -1; int32_t stencil = -1; int32_t shaderTag = -1; // the nearest multishader tag ("S05__...") on the way down the tree }; struct NetHeader { std::string name; }; struct AvHeader { std::string name; uint16_t flags{}; Transform transform; std::vector properties; }; class Parser { public: Parser(std::string_view data, uint32_t lod) : m_Data(data), m_Lod(lod) {} std::optional Run(std::string& error) { if (!ReadHeader(error)) return std::nullopt; m_Model.version = m_Version; // The footer lists the roots; the first block is the root when it can't be read std::vector roots; Reader footer(m_Data.substr(m_FooterStart)); const auto count = footer.U32(); if (footer.Ok() && count <= m_Blocks.size()) roots = footer.Array(count); if (roots.empty() && !m_Blocks.empty()) roots.push_back(0); for (const auto root : roots) Visit(root, Transform{}, Properties{}, 0); for (size_t i = 0; i < m_Blocks.size(); i++) { if (!m_Used.contains(static_cast(i))) { const auto& type = m_Types[m_Blocks[i].type]; if (!IsDrawnType(type)) m_Model.skipped[type]++; } } bool first = true; for (const auto& mesh : m_Model.meshes) { for (size_t i = 0; i + 2 < mesh.positions.size(); i += 3) { for (int axis = 0; axis < 3; axis++) { const auto value = mesh.positions[i + axis]; m_Model.min[axis] = first ? value : std::min(m_Model.min[axis], value); m_Model.max[axis] = first ? value : std::max(m_Model.max[axis], value); } first = false; } } return std::move(m_Model); } // The DDS file for pixel data block `index` (see NifFile::EmbeddedTexture) std::optional Dds(int32_t index, std::string& error) { if (!ReadHeader(error)) return std::nullopt; const auto* type = TypeOf(index); if (!type || (*type != "NiPixelData" && *type != "NiPersistentSrcTextureRendererData")) return std::nullopt; auto reader = BlockReader(index); const auto format = reader.U32(); reader.U8(); // bits per pixel reader.U32(); // renderer hint reader.U32(); // extra data reader.U8(); // flags const auto tiling = reader.U32(); if (m_Version >= Version(20, 3, 0, 4)) reader.U8(); // sRGB reader.Skip(4 * 10); // channels reader.I32(); // palette const auto mipCount = reader.U32(); const auto bytesPerPixel = reader.U32(); if (!reader.Ok() || mipCount == 0 || mipCount > 16 || tiling != 0) return std::nullopt; std::vector> mips; // width, height, offset for (uint32_t i = 0; i < mipCount; i++) mips.push_back({ reader.U32(), reader.U32(), reader.U32() }); const auto pixelCount = reader.U32(); if (*type == "NiPersistentSrcTextureRendererData") { reader.U32(); // padded pixel count reader.U32(); // faces reader.U32(); // platform } else { reader.U32(); // faces } std::string_view pixels; if (reader.Ok() && pixelCount <= m_Blocks[index].size - reader.Position()) pixels = m_Data.substr(m_Blocks[index].offset + reader.Position(), pixelCount); const auto width = mips[0][0], height = mips[0][1]; if (pixels.empty() || width == 0 || height == 0 || width > 8192 || height > 8192 || mips[0][2] != 0) return std::nullopt; // DDS header (Microsoft's DDS_HEADER and DDS_PIXELFORMAT) std::array header{}; header[0] = 124; header[1] = 0x1 | 0x2 | 0x4 | 0x1000 | 0x20000; // caps, height, width, pixel format, mipmap count header[2] = height; header[3] = width; header[6] = mipCount; header[18] = 32; // pixel format size if (format >= 4 && format <= 6) { header[19] = 0x4; // four CC const char* fourCc = format == 4 ? "DXT1" : format == 5 ? "DXT3" : "DXT5"; std::memcpy(&header[20], fourCc, 4); } else if ((format == 0 && bytesPerPixel == 3) || (format == 1 && bytesPerPixel == 4)) { header[19] = format == 1 ? 0x41 : 0x40; // RGB, with alpha header[21] = bytesPerPixel * 8; header[22] = 0x000000FF; // red first in memory header[23] = 0x0000FF00; header[24] = 0x00FF0000; header[25] = format == 1 ? 0xFF000000 : 0; } else { return std::nullopt; } header[26] = 0x1000 | (mipCount > 1 ? 0x400008 : 0); // texture, mipmaps std::string out = "DDS "; out.append(reinterpret_cast(header.data()), header.size() * 4); out.append(pixels); return out; } private: struct Block { uint16_t type{}; size_t offset{}; uint32_t size{}; }; std::string_view m_Data; uint32_t m_Lod; uint32_t m_Version{}; std::vector m_Types; std::vector m_Strings; std::vector m_Blocks; size_t m_FooterStart{}; std::set m_Used; std::set m_Visiting; NifFile::Model m_Model; static bool IsDrawnType(const std::string& type) { return type == "NiNode" || type == "NiLODNode" || type == "NiBillboardNode" || type == "NiSwitchNode" || type == "NiTriShape" || type == "NiTriStrips" || type == "NiTriShapeData" || type == "NiTriStripsData" || type == "NiMaterialProperty" || type == "NiAlphaProperty" || type == "NiTexturingProperty" || type == "NiSourceTexture" || type == "NiVertexColorProperty" || type == "NiStencilProperty" || type == "NiRangeLODData" || // Read and ignored: they change nothing a still picture shows type == "NiSpecularProperty" || type == "NiZBufferProperty" || type == "NiShadeProperty" || type == "NiStringExtraData"; } bool ReadHeader(std::string& error) { const auto newline = m_Data.substr(0, 128).find('\n'); if (newline == std::string_view::npos || !(m_Data.starts_with("Gamebryo File Format") || m_Data.starts_with("NetImmerse File Format"))) { error = "not a Gamebryo file"; return false; } Reader header(m_Data.substr(newline + 1)); m_Version = header.U32(); if (m_Version < MIN_VERSION || m_Version > MAX_VERSION) { error = "unsupported version"; return false; } const auto endian = header.U8(); const auto userVersion = header.U32(); const auto blockCount = header.U32(); if (!header.Ok() || endian != 1 || userVersion != 0 || blockCount > MAX_BLOCKS) { error = "unsupported header (big-endian or another game's user version)"; return false; } const auto typeCount = header.U16(); for (uint32_t i = 0; i < typeCount && header.Ok(); i++) m_Types.push_back(header.SizedString()); const auto typeIndex = header.Array(blockCount); const auto sizes = header.Array(blockCount); const auto stringCount = header.U32(); header.U32(); // longest string for (uint32_t i = 0; i < stringCount && header.Ok(); i++) m_Strings.push_back(header.SizedString()); const auto groupCount = header.U32(); header.Skip(static_cast(groupCount) * 4); if (!header.Ok()) { error = "truncated header"; return false; } size_t offset = newline + 1 + header.Position(); m_Blocks.reserve(blockCount); for (uint32_t i = 0; i < blockCount; i++) { const uint16_t type = typeIndex[i] & 0x7FFF; // the high bit marks PhysX blocks if (type >= m_Types.size() || sizes[i] > m_Data.size() - offset) { error = "block " + std::to_string(i) + " is out of range"; return false; } m_Blocks.push_back({ type, offset, sizes[i] }); offset += sizes[i]; } m_FooterStart = offset; return true; } const std::string* TypeOf(int32_t index) const { if (index < 0 || static_cast(index) >= m_Blocks.size()) return nullptr; return &m_Types[m_Blocks[index].type]; } Reader BlockReader(int32_t index) const { const auto& block = m_Blocks[index]; return Reader(m_Data.substr(block.offset, block.size)); } std::string String(uint32_t index) const { return index < m_Strings.size() ? m_Strings[index] : std::string{}; } NetHeader ReadNet(Reader& reader) { NetHeader net; net.name = String(reader.U32()); const auto extra = reader.U32(); reader.Skip(static_cast(extra) * 4); reader.I32(); // controller return net; } AvHeader ReadAv(Reader& reader) { AvHeader av; av.name = ReadNet(reader).name; av.flags = reader.U16(); for (auto& value : av.transform.t) value = reader.Float(); // Matrix33 is stored m11, m21, m31, m12, ... (nif.xml): column by column std::array stored{}; for (auto& value : stored) value = reader.Float(); for (int row = 0; row < 3; row++) { for (int col = 0; col < 3; col++) av.transform.r[row * 3 + col] = stored[col * 3 + row]; } av.transform.s = reader.Float(); const auto count = reader.U32(); av.properties = reader.Array(count); reader.I32(); // collision object return av; } Properties Inherit(Properties properties, const std::vector& own) { for (const auto ref : own) { const auto* type = TypeOf(ref); if (!type) continue; if (*type == "NiMaterialProperty") properties.material = ref; else if (*type == "NiAlphaProperty") properties.alpha = ref; else if (*type == "NiTexturingProperty") properties.texturing = ref; else if (*type == "NiVertexColorProperty") properties.vertexColor = ref; else if (*type == "NiStencilProperty") properties.stencil = ref; m_Used.insert(ref); } return properties; } void Visit(int32_t index, const Transform& parent, Properties properties, uint32_t depth) { const auto* type = TypeOf(index); if (!type || depth > MAX_DEPTH || m_Visiting.contains(index)) return; m_Visiting.insert(index); if (*type == "NiNode" || *type == "NiLODNode" || *type == "NiBillboardNode" || *type == "NiSwitchNode") { m_Used.insert(index); VisitNode(index, *type, parent, properties, depth); } else if (*type == "NiTriShape" || *type == "NiTriStrips") { m_Used.insert(index); VisitGeometry(index, parent, properties); } m_Visiting.erase(index); } void VisitNode(int32_t index, const std::string& type, const Transform& parent, Properties properties, uint32_t depth) { auto reader = BlockReader(index); const auto av = ReadAv(reader); const auto childCount = reader.U32(); const auto children = reader.Array(childCount); const auto effectCount = reader.U32(); reader.Skip(static_cast(effectCount) * 4); if (!reader.Ok()) return; const auto world = parent.Then(av.transform); // Recorded even when hidden: attach points often are if (!av.name.empty() && !m_Model.nodes.contains(av.name)) { NifFile::NodeTransform node; for (int i = 0; i < 9; i++) node.rotation[i] = world.r[i] * world.s; node.translation = world.t; m_Model.nodes.emplace(av.name, node); } if (av.flags & APP_CULLED) return; properties = Inherit(properties, av.properties); if (const auto tag = NifFile::ShaderTag(av.name); tag >= 0) properties.shaderTag = tag; std::vector drawn = children; if (type == "NiSwitchNode" || type == "NiLODNode") { reader.U16(); // switch flags const auto active = reader.U32(); drawn.clear(); if (type == "NiSwitchNode") { if (reader.Ok() && active < children.size()) drawn.push_back(children[active]); } else if (const auto chosen = ChooseLod(reader.I32(), children)) { drawn.push_back(*chosen); } } for (const auto child : drawn) Visit(child, world, properties, depth + 1); } // The child of an NiLODNode for m_Lod: children ordered nearest range first (the most detailed) std::optional ChooseLod(int32_t dataRef, const std::vector& children) { if (children.empty()) return std::nullopt; std::vector> order; const auto* dataType = TypeOf(dataRef); if (dataType && *dataType == "NiRangeLODData") { m_Used.insert(dataRef); auto data = BlockReader(dataRef); data.Skip(12); // center const auto levels = data.U32(); for (uint32_t i = 0; i < levels && data.Ok() && i < children.size(); i++) { const auto nearExtent = data.Float(); data.Float(); // far extent if (data.Ok()) order.emplace_back(nearExtent, children[i]); } } if (order.size() != children.size()) { order.clear(); for (size_t i = 0; i < children.size(); i++) order.emplace_back(static_cast(i), children[i]); } std::stable_sort(order.begin(), order.end(), [](const auto& a, const auto& b) { return a.first < b.first; }); return order[std::min(m_Lod, order.size() - 1)].second; } void VisitGeometry(int32_t index, const Transform& parent, Properties properties) { auto reader = BlockReader(index); const auto av = ReadAv(reader); const auto dataRef = reader.I32(); const auto skin = reader.I32(); if (!reader.Ok() || (av.flags & APP_CULLED)) return; properties = Inherit(properties, av.properties); if (const auto tag = NifFile::ShaderTag(av.name); tag >= 0) properties.shaderTag = tag; const auto* dataType = TypeOf(dataRef); if (!dataType || (*dataType != "NiTriShapeData" && *dataType != "NiTriStripsData")) return; m_Used.insert(dataRef); NifFile::Mesh mesh; if (!ReadGeometryData(dataRef, *dataType == "NiTriStripsData", parent.Then(av.transform), mesh) || mesh.indices.empty()) return; mesh.material = ReadMaterial(properties); mesh.material.shaderTag = properties.shaderTag; if (skin >= 0) { m_Model.skinned++; m_Used.insert(skin); } m_Model.meshes.push_back(std::move(mesh)); } bool ReadGeometryData(int32_t index, bool strips, const Transform& transform, NifFile::Mesh& mesh) { auto reader = BlockReader(index); reader.I32(); // group ID const auto count = reader.U16(); reader.U8(); // keep flags reader.U8(); // compress flags if (reader.U8()) { const auto vertices = reader.Array(static_cast(count) * 3); mesh.positions.reserve(vertices.size()); for (size_t i = 0; i + 2 < vertices.size(); i += 3) { const auto p = transform.Apply({ vertices[i], vertices[i + 1], vertices[i + 2] }); mesh.positions.insert(mesh.positions.end(), p.begin(), p.end()); } } const auto dataFlags = reader.U16(); if (reader.U8()) { const auto normals = reader.Array(static_cast(count) * 3); mesh.normals.reserve(normals.size()); for (size_t i = 0; i + 2 < normals.size(); i += 3) { auto n = transform.Rotate({ normals[i], normals[i + 1], normals[i + 2] }); const auto length = std::sqrt(n[0] * n[0] + n[1] * n[1] + n[2] * n[2]); if (length > 0.0f) for (auto& value : n) value /= length; mesh.normals.insert(mesh.normals.end(), n.begin(), n.end()); } if (dataFlags & 4096) reader.Skip(static_cast(count) * 24); // tangents and bitangents } reader.Skip(16); // bounding sphere if (reader.U8()) { const auto colors = reader.Array(static_cast(count) * 4); mesh.colors.reserve(colors.size()); for (const auto value : colors) mesh.colors.push_back(static_cast(std::lround(std::clamp(value, 0.0f, 1.0f) * 255.0f))); } const auto uvSets = dataFlags & 63; if (uvSets > 0) { mesh.uvs = reader.Array(static_cast(count) * 2); reader.Skip(static_cast(uvSets - 1) * count * 8); } reader.U16(); // consistency flags reader.I32(); // additional data const auto triangles = reader.U16(); if (!strips) { reader.U32(); // triangle points if (reader.U8()) mesh.indices = reader.Array(static_cast(triangles) * 3); } else { const auto stripCount = reader.U16(); const auto lengths = reader.Array(stripCount); if (reader.U8()) { for (const auto length : lengths) { const auto points = reader.Array(length); for (size_t i = 2; i < points.size(); i++) { const auto a = points[i - 2], b = points[i - 1], c = points[i]; if (a == b || b == c || a == c) continue; if (i % 2 == 0) mesh.indices.insert(mesh.indices.end(), { a, b, c }); else mesh.indices.insert(mesh.indices.end(), { a, c, b }); } } } } if (!reader.Ok() || mesh.positions.size() != static_cast(count) * 3) return false; if (mesh.normals.size() != mesh.positions.size()) mesh.normals.clear(); if (mesh.colors.size() != static_cast(count) * 4) mesh.colors.clear(); if (mesh.uvs.size() != static_cast(count) * 2) mesh.uvs.clear(); // Drop triangles pointing past the vertices std::vector valid; valid.reserve(mesh.indices.size()); for (size_t i = 0; i + 2 < mesh.indices.size(); i += 3) { if (mesh.indices[i] < count && mesh.indices[i + 1] < count && mesh.indices[i + 2] < count) { valid.insert(valid.end(), { mesh.indices[i], mesh.indices[i + 1], mesh.indices[i + 2] }); } } mesh.indices = std::move(valid); return true; } NifFile::Material ReadMaterial(const Properties& properties) { NifFile::Material material; if (properties.material >= 0) { auto reader = BlockReader(properties.material); ReadNet(reader); reader.Skip(12); // ambient std::array diffuse{}, emissive{}; for (auto& value : diffuse) value = reader.Float(); reader.Skip(12); // specular for (auto& value : emissive) value = reader.Float(); reader.Float(); // glossiness const auto alpha = reader.Float(); if (reader.Ok()) { material.diffuse = diffuse; material.emissive = emissive; material.alpha = std::clamp(alpha, 0.0f, 1.0f); } } if (properties.alpha >= 0) { auto reader = BlockReader(properties.alpha); ReadNet(reader); const auto flags = reader.U16(); const auto threshold = reader.U8(); if (reader.Ok()) { material.alphaBlend = flags & 1; material.alphaTest = flags & 0x200; material.alphaThreshold = threshold; } } if (properties.vertexColor >= 0) { auto reader = BlockReader(properties.vertexColor); ReadNet(reader); const auto flags = reader.U16(); if (reader.Ok()) material.vertexColorMode = static_cast((flags >> 4) & 3); } if (properties.stencil >= 0) { auto reader = BlockReader(properties.stencil); ReadNet(reader); const auto flags = reader.U16(); if (reader.Ok()) material.doubleSided = ((flags >> 10) & 3) == 3; // DRAW_BOTH } if (properties.texturing >= 0) { auto reader = BlockReader(properties.texturing); ReadNet(reader); reader.U16(); // flags reader.U32(); // texture count if (reader.U8()) { // has base texture const auto source = reader.I32(); const auto flags = reader.U16(); const auto* type = TypeOf(source); if (reader.Ok() && type && *type == "NiSourceTexture") { m_Used.insert(source); auto texture = BlockReader(source); ReadNet(texture); const auto external = texture.U8(); const auto file = String(texture.U32()); const auto pixels = texture.I32(); const auto* pixelType = TypeOf(pixels); if (texture.Ok() && external == 1) material.texture = file; else if (texture.Ok() && pixelType && (*pixelType == "NiPixelData" || *pixelType == "NiPersistentSrcTextureRendererData")) { material.embeddedTexture = pixels; m_Used.insert(pixels); } const auto clamp = (flags >> 12) & 0xF; material.clampU = clamp == 0 || clamp == 1; material.clampV = clamp == 0 || clamp == 2; } } } return material; } }; void Append(std::string& out, const void* data, size_t bytes) { out.append(static_cast(data), bytes); } void Pad(std::string& out) { while (out.size() % 4) out.push_back('\0'); } nlohmann::json Color(const std::array& color) { return { std::round(color[0] * 1000.0f) / 1000.0f, std::round(color[1] * 1000.0f) / 1000.0f, std::round(color[2] * 1000.0f) / 1000.0f }; } } namespace NifFile { int32_t ShaderTag(std::string_view name) { // The client reads the tag with sscanf: "S%d" at the start of the name, else "_S%d" after the first "_S" const auto number = [](std::string_view digits) -> int32_t { size_t i = 0; while (i < digits.size() && (digits[i] == ' ' || digits[i] == '\t')) i++; // A signed number names no mapShaders row, like no number at all if (i >= digits.size() || digits[i] < '0' || digits[i] > '9') return -1; int32_t value = 0; for (; i < digits.size() && digits[i] >= '0' && digits[i] <= '9' && value < 100000; i++) value = value * 10 + (digits[i] - '0'); return value; }; if (name.starts_with('S')) { const auto tag = number(name.substr(1)); if (tag >= 0) return tag; } const auto at = name.find("_S"); if (at == std::string_view::npos || at + 3 >= name.size()) return -1; return number(name.substr(at + 2)); } int32_t MultishaderPart(std::optional tagShader) { return tagShader && *tagShader >= 3 && *tagShader <= 0x6C ? *tagShader : LEGO_SHADER; } eTextureAlpha TextureAlphaFor(int32_t shader) { switch (shader) { // LEGOPPLighting: textured alone the texture's alpha is forced to 1; with vertex colors it only lays the // texture over them (lerp by its alpha) and the vertex alpha is what shows through case 4: case 5: case 12: case 25: case 27: case 28: case 29: case 30: case 50: case 72: case 88: // Darkling: the same lay-over; alpha from lighting or the fade case 75: case 76: case 77: case 102: case 103: case 104: return eTextureAlpha::DECAL; // LEGOPPLighting_Item: texture alpha forced to 1, multiplied by the vertex colors case 31: case 48: // TerrainMeshLighting_Rim: texture times vertex colors, alpha only the fade case 3: return eTextureAlpha::IGNORED; default: return eTextureAlpha::OPACITY; } } uint8_t ShaderLookFor(int32_t shader) { // By the techniques of each mapShaders row (its label names them: "NL" no lighting, "NT" no texture, "VC" // vertex colors), e.g. 38 "Basic VC" is Technique_Basic_Lighting_VertColor, 33 "Basic NL VC NT" // Technique_Basic_NoLighting_VertColor_NoTexture, 32 "Basic NL Material" Technique_Basic_Material_NoLighting switch (shader) { // Basic NL Material, Over Everything Material Unlit case 32: case 108: return UNLIT | NO_VERTEX_COLORS | MATERIAL_COLOR; // Basic NL, Basic NL UVAnim, OneSidedAlpha NL (and skinned), Opaque NL NoFog case 34: case 36: case 56: case 61: case 83: return UNLIT | NO_VERTEX_COLORS; // Basic, the lit one without vertex colors case 94: return NO_VERTEX_COLORS; // VertColor_NoLight_NoTex_AnimAlpha, VC_NL_NoTex_2D, Basic NL VC NT, OneSidedAlpha NL VC NT (and skinned), // Basic NL NT, Opaque NL VC NT NoFog case 11: case 16: case 33: case 58: case 63: case 80: case 82: return UNLIT | NO_TEXTURE; // Basic VC NT, Opaque VC NT NoFog case 37: case 85: return NO_TEXTURE; // VertColor_NoLighting_Alpha, VertColorTex_NoLight_AlphaBlend and _AlphaTest, VC_NoLighting_2D, Over // Everything (Unlit), Basic NL VC, LEGO-No Light, OneSidedAlpha NL VC (and skinned), OneSidedAlpha NL // AnimAlpha, the NoLight scrolling UVs, Opaque NL VC NoFog, Additive NoLight VertColor, Two Textures Added NL // VC AnimUV, Distortion (Ocean) Unlit, Two Layers Blended NL VC AnimUV case 8: case 10: case 54: case 15: case 23: case 35: case 52: case 57: case 62: case 68: case 70: case 73: case 81: case 84: case 87: case 93: case 101: case 105: return UNLIT; default: return 0; } } std::optional Parse(std::string_view data, uint32_t lod, std::string& error) { return Parser(data, lod).Run(error); } std::optional EmbeddedTexture(std::string_view data, int32_t block) { std::string error; return Parser(data, 0).Dds(block, error); } std::string Encode(const Model& model, const std::vector& textures) { std::string body; nlohmann::json meshes = nlohmann::json::array(); std::vector names; for (size_t m = 0; m < model.meshes.size(); m++) { const auto& mesh = model.meshes[m]; const auto& material = mesh.material; const auto vertices = mesh.positions.size() / 3; const std::string texture = m < textures.size() ? textures[m] : std::string{}; int32_t textureIndex = -1; if (!texture.empty()) { const auto it = std::find(names.begin(), names.end(), texture); textureIndex = static_cast(it - names.begin()); if (it == names.end()) names.push_back(texture); } nlohmann::json entry{ {"offset", body.size()}, {"vertices", vertices}, {"indices", mesh.indices.size()}, {"normals", !mesh.normals.empty()}, {"uv", !mesh.uvs.empty() && textureIndex >= 0}, {"colors", !mesh.colors.empty()}, {"diffuse", Color(material.diffuse)}, {"emissive", Color(material.emissive)}, {"alpha", std::round(material.alpha * 1000.0f) / 1000.0f}, {"blend", material.alphaBlend}, {"test", material.alphaTest ? material.alphaThreshold : -1}, {"doubleSided", material.doubleSided}, {"vertexColors", material.vertexColorMode}, {"texture", textureIndex}, {"clampU", material.clampU}, {"clampV", material.clampV}, {"shaderTag", material.shaderTag} }; Append(body, mesh.positions.data(), mesh.positions.size() * sizeof(float)); if (!mesh.normals.empty()) { std::vector packed(mesh.normals.size()); for (size_t i = 0; i < packed.size(); i++) packed[i] = static_cast(std::lround(std::clamp(mesh.normals[i], -1.0f, 1.0f) * 127.0f)); Append(body, packed.data(), packed.size()); Pad(body); } if (entry["uv"].get()) Append(body, mesh.uvs.data(), mesh.uvs.size() * sizeof(float)); if (!mesh.colors.empty()) Append(body, mesh.colors.data(), mesh.colors.size()); Append(body, mesh.indices.data(), mesh.indices.size() * sizeof(uint16_t)); Pad(body); meshes.push_back(std::move(entry)); } nlohmann::json header{ {"version", 1}, {"meshes", meshes}, {"textures", names}, {"min", { model.min[0], model.min[1], model.min[2] }}, {"max", { model.max[0], model.max[1], model.max[2] }} }; auto text = header.dump(); while (text.size() % 4) text.push_back(' '); std::string out; const auto length = static_cast(text.size()); Append(out, &length, sizeof(length)); out += text; out += body; return out; } std::optional KfmModelPath(std::string_view data) { const auto newline = data.substr(0, 128).find('\n'); if (newline == std::string_view::npos || data.substr(0, newline).find("KFM") == std::string_view::npos) return std::nullopt; Reader reader(data.substr(newline + 1)); reader.U8(); // little endian auto path = reader.SizedString(); if (!reader.Ok() || path.empty()) return std::nullopt; return path; } }