export module ra3.models; import std; export import ra3.core; export import ra3.render; export import ra3.fs; /** * The map's static art: the compiled W3D models that draw the buildings and * props a map places on the ground. * * Retail RA3 does not ship `.w3x`/`.w3d` files. BinaryAssetBuilder bakes every * model into a *binary asset stream* — a `.manifest` index plus a `.bin` of * relocatable instance data — and each map carries its own stream in * `Maps*.big` under `data\maps\official\\map.{manifest,bin}`. Those streams * hold the `W3DMesh` assets for the map's props (sidewalks, deck props, lights, * walls, civilian buildings, ...) and the `Texture` assets they sample. * * This module parses that stream, decodes the compiled `W3DMesh` vertex/index * buffers and the embedded DDS textures, and flattens the map's objects into a * single world-space triangle soup the renderers upload directly. * * Format references: ra3-headless `ra3tools/ra3_binary.py` (BAB * `ManifestHeader`/`AssetEntry`, `SageBinaryData/W3D.cs`) and OpenSAGE * `Data/Map/MapObject.cs`. Instance pointers are stored as offsets from the * start of the instance data, so no relocation pass is needed. */ export namespace ra3::models { using ra3::core::uint8; using ra3::core::uint16; using ra3::core::uint32; using ra3::core::int32; using ra3::core::usize; using ra3::render::argb; using ra3::render::image; /** Thrown when a compiled asset stream or a model payload is malformed. */ class model_error : public std::runtime_error { public: using std::runtime_error::runtime_error; }; namespace detail { [[nodiscard]] inline auto u16(std::span b, usize off) -> uint16 { return off + 2U <= b.size() ? static_cast(b[off]) | (static_cast(b[off + 1U]) << 8U) : 0U; } [[nodiscard]] inline auto u32(std::span b, usize off) -> uint32 { return off + 4U <= b.size() ? static_cast(b[off]) | (static_cast(b[off + 1U]) << 8U) | (static_cast(b[off + 2U]) << 16U) | (static_cast(b[off + 3U]) << 24U) : 0U; } [[nodiscard]] inline auto i32(std::span b, usize off) -> ra3::core::int32 { return static_cast(u32(b, off)); } [[nodiscard]] inline auto f32(std::span b, usize off) -> float { const auto bits = u32(b, off); float value = 0.0F; std::memcpy(&value, &bits, sizeof(value)); return value; } [[nodiscard]] inline auto cstr(std::span b, usize off, ra3::core::int32 length) -> std::string { if (off == 0U || length <= 0 || off + static_cast(length) > b.size()) return {}; return std::string{reinterpret_cast(b.data() + off), static_cast(length)}; } /** NUL-terminated string in the manifest name/source buffers. */ [[nodiscard]] inline auto nul_string(std::span b, usize off) -> std::string { if (off >= b.size()) return {}; usize end = off; while (end < b.size() && b[end] != 0U) ++end; return std::string{reinterpret_cast(b.data() + off), end - off}; } } /** * A parsed `BinaryAsset` stream: the manifest index plus the concatenated * instance data. Assets are addressed by `Type:Instance` or by their * `(typeId, instanceId)` pair (used by cross-asset references). */ class asset_stream { public: struct asset { std::string name; ///< `Type:Instance` std::string source; uint32 type_id = 0; uint32 instance_id = 0; usize instance_offset = 0; usize instance_size = 0; std::vector> references; ///< `(typeId, instanceId)` targets. [[nodiscard]] auto type_name() const -> std::string_view { return std::string_view{name}.substr(0, name.find(':')); } [[nodiscard]] auto instance_name() const -> std::string_view { const auto at = name.find(':'); return at == std::string::npos ? std::string_view{name} : std::string_view{name}.substr(at + 1U); } }; static constexpr uint32 header_size = 48U; static constexpr uint32 entry_size = 48U; /** Parse an in-memory `.manifest` + `.bin` pair. */ [[nodiscard]] static auto load(std::span manifest_raw, std::span data_raw) -> asset_stream { asset_stream stream; stream.data_ = fs::maybe_decompress(data_raw); stream.parse(fs::maybe_decompress(manifest_raw)); return stream; } /** * Parse a `.manifest` + `.bin` pair from disk. * * Retail's worldbuilder stream is ~1.1 GB and uncompressed, so a small * `.bin` is read into memory but a large one is read lazily per asset * (its pointers are offsets, so a slice needs no random-access decode). */ [[nodiscard]] static auto load_files(const std::filesystem::path &manifest_path, const std::filesystem::path &bin_path, usize inline_limit = 128U * 1024U * 1024U) -> asset_stream { const auto read_all = [](const std::filesystem::path &path) { std::ifstream in(path, std::ios::binary); if (!in) throw model_error("cannot open " + path.string()); return std::vector{std::istreambuf_iterator(in), std::istreambuf_iterator()}; }; std::error_code ec; const auto bin_size = std::filesystem::file_size(bin_path, ec); asset_stream stream; if (ec || bin_size <= inline_limit) { stream.data_ = fs::maybe_decompress(read_all(bin_path)); } else { stream.bin_path_ = bin_path; // The large retail streams (worldbuilder.bin) are stored raw; a // RefPack stream would not be randomly addressable anyway. const auto probe = [&] { std::ifstream in(bin_path, std::ios::binary); std::array head{}; in.read(reinterpret_cast(head.data()), static_cast(head.size())); return fs::is_refpack(head); }(); if (probe) throw model_error("compressed asset stream is too large to map"); } stream.parse(read_all(manifest_path)); return stream; } [[nodiscard]] auto assets() const -> const std::vector & { return assets_; } [[nodiscard]] auto find(uint32 type_id, uint32 instance_id) const -> const asset * { const auto it = index_.find((static_cast(type_id) << 32U) | instance_id); return it == index_.end() ? nullptr : &assets_[it->second]; } /** First asset whose `Type:Instance` name equals `name` (case-insensitive). */ [[nodiscard]] auto find_name(std::string_view name) const -> const asset * { const auto want = lower(name); for (const auto &a: assets_) { if (lower(a.name) == want) return &a; } return nullptr; } /** Every `W3DMesh` whose instance name equals `base` or starts with `base.`. */ [[nodiscard]] auto meshes_for(std::string_view base) const -> const std::vector & { static const std::vector none; const auto it = mesh_index_.find(lower(base)); return it == mesh_index_.end() ? none : it->second; } /** The asset's instance bytes (from memory, or lazily from disk). */ [[nodiscard]] auto read_instance(const asset &a) const -> std::vector { if (!data_.empty()) { if (a.instance_offset + a.instance_size > data_.size()) return {}; return {data_.begin() + static_cast(a.instance_offset), data_.begin() + static_cast(a.instance_offset + a.instance_size)}; } if (bin_path_.empty() || a.instance_size == 0U) return {}; std::ifstream in(bin_path_, std::ios::binary); if (!in) return {}; in.seekg(static_cast(a.instance_offset)); std::vector bytes(a.instance_size); in.read(reinterpret_cast(bytes.data()), static_cast(bytes.size())); return bytes; } [[nodiscard]] static auto lower(std::string_view text) -> std::string { std::string out{text}; std::transform(out.begin(), out.end(), out.begin(), [](unsigned char ch) { return static_cast(std::tolower(ch)); }); return out; } private: /** Parse the manifest into `assets_` + the lookup indexes. */ auto parse(std::span manifest) -> void { if (manifest.size() < header_size) throw model_error("asset manifest is too short"); if (manifest[0] != 0U) throw model_error("big-endian asset manifest is unsupported"); const auto count = detail::u32(manifest, 12U); const auto ref_buf_size = detail::u32(manifest, 32U); const auto ref_name_buf_size = detail::u32(manifest, 36U); const auto name_buf_size = detail::u32(manifest, 40U); const auto src_buf_size = detail::u32(manifest, 44U); (void) src_buf_size; if (header_size + static_cast(count) * entry_size > manifest.size()) throw model_error("asset manifest entry table is truncated"); const usize entries_off = header_size; const usize ref_off = entries_off + static_cast(count) * entry_size; const usize refname_off = ref_off + ref_buf_size; const usize name_off = refname_off + ref_name_buf_size; const usize src_off = name_off + name_buf_size; assets_.reserve(count); usize instance_offset = 4U; // the first four bytes of `.bin` are the stream checksum for (uint32 i = 0; i < count; ++i) { const usize e = entries_off + static_cast(i) * entry_size; asset a; a.type_id = detail::u32(manifest, e); a.instance_id = detail::u32(manifest, e + 4U); const auto aref_off = detail::i32(manifest, e + 16U); const auto aref_count = detail::i32(manifest, e + 20U); a.instance_size = detail::u32(manifest, e + 32U); a.instance_offset = instance_offset; a.name = detail::nul_string(manifest, name_off + static_cast(std::max(0, detail::i32(manifest, e + 24U)))); a.source = detail::nul_string(manifest, src_off + static_cast(std::max(0, detail::i32(manifest, e + 28U)))); if (aref_off >= 0 && aref_count > 0 && ref_off + static_cast(aref_off) + static_cast(aref_count) * 8U <= manifest.size()) { a.references.reserve(static_cast(aref_count)); for (int r = 0; r < aref_count; ++r) { const usize ro = ref_off + static_cast(aref_off) + static_cast(r) * 8U; a.references.emplace_back(detail::u32(manifest, ro), detail::u32(manifest, ro + 4U)); } } index_.try_emplace((static_cast(a.type_id) << 32U) | a.instance_id, assets_.size()); assets_.push_back(std::move(a)); instance_offset += assets_.back().instance_size; } // Index meshes by the base of their instance name (`A.B` -> `a` and // `a.b`) so a map object type resolves to its mesh parts in one look. for (const auto &a: assets_) { if (a.type_name() != "W3DMesh") continue; const auto instance = lower(a.instance_name()); mesh_index_[instance].push_back(&a); const auto dot = instance.find('.'); if (dot != std::string::npos) mesh_index_[instance.substr(0, dot)].push_back(&a); } } std::vector assets_; std::unordered_map index_; std::unordered_map> mesh_index_; std::vector data_; std::filesystem::path bin_path_; }; /** A decoded compiled `W3DMesh`: a vertex buffer plus a triangle list, in model space. */ struct mesh { std::vector positions; ///< 3 floats per vertex. std::vector normals; ///< 3 floats per vertex, or empty. std::vector uvs; ///< 2 floats per vertex, or empty. std::vector indices; std::string shader; ///< FX shader name (`BuildingsGeneric.fx`, ...). std::vector blend_indices; ///< 4 bone indices per vertex, or empty (static mesh). std::vector bone_remap; ///< Per-mesh blend index -> hierarchy bone map, or empty. std::vector blend_weights; ///< 4 weights per vertex, or empty. [[nodiscard]] auto skinned() const -> bool { return !blend_indices.empty(); } }; /** One bone of a `W3DHierarchy`: name hash, parent and the local (bind) transform. */ struct bone { uint32 name_hash = 0; int32 parent = -1; ///< -1 for the root. float tx = 0.0F; float ty = 0.0F; float tz = 0.0F; float qx = 0.0F; float qy = 0.0F; float qz = 0.0F; float qw = 1.0F; }; /** A decoded `W3DHierarchy` (the skeleton the meshes are bound to). */ struct hierarchy { std::vector bones; }; /** * Decode a compiled `W3DHierarchy`. * * Layout (from the retail data): `u32 pad, u32 boneCount, u32 headerBytes`, * then `boneCount` 100-byte records: `u32 nameHash, i32 parent, f32 t[3], * f32 quaternion[4] (x,y,z,w), f32 matrix[12]`. Meshes are bound to bones in * *bone space*, so a static render must rebuild each bone's world transform * from this default pose (see `bone_world_matrices`). */ [[nodiscard]] inline auto decode_hierarchy(std::span data) -> hierarchy { hierarchy out; const auto bone_count = detail::u32(data, 4U); const auto header = detail::u32(data, 8U); if (bone_count == 0U || bone_count > 4096U || header + static_cast(bone_count) * 100U > data.size()) return out; out.bones.reserve(bone_count); for (uint32 i = 0; i < bone_count; ++i) { const auto o = header + static_cast(i) * 100U; bone b; b.name_hash = detail::u32(data, o); b.parent = detail::i32(data, o + 4U); b.tx = detail::f32(data, o + 8U); b.ty = detail::f32(data, o + 12U); b.tz = detail::f32(data, o + 16U); b.qx = detail::f32(data, o + 20U); b.qy = detail::f32(data, o + 24U); b.qz = detail::f32(data, o + 28U); b.qw = detail::f32(data, o + 32U); out.bones.push_back(b); } return out; } /** A bone's world transform: 3x3 rotation (row-major) followed by a translation. */ struct bone_matrix { std::array rotation{1.0F, 0.0F, 0.0F, 0.0F, 1.0F, 0.0F, 0.0F, 0.0F, 1.0F}; std::array translation{0.0F, 0.0F, 0.0F}; }; namespace detail { /** Quaternion `(x, y, z, w)` to a row-major 3x3 rotation matrix. */ [[nodiscard]] inline auto quaternion_matrix(float x, float y, float z, float w) -> std::array { const auto n = std::sqrt(x * x + y * y + z * z + w * w); if (n <= 1.0e-8F) return {1.0F, 0.0F, 0.0F, 0.0F, 1.0F, 0.0F, 0.0F, 0.0F, 1.0F}; x /= n; y /= n; z /= n; w /= n; return {1.0F - 2.0F * (y * y + z * z), 2.0F * (x * y - z * w), 2.0F * (x * z + y * w), 2.0F * (x * y + z * w), 1.0F - 2.0F * (x * x + z * z), 2.0F * (y * z - x * w), 2.0F * (x * z - y * w), 2.0F * (y * z + x * w), 1.0F - 2.0F * (x * x + y * y)}; } [[nodiscard]] inline auto multiply(const std::array &a, const std::array &b) -> std::array { std::array out{}; for (int i = 0; i < 3; ++i) { for (int j = 0; j < 3; ++j) { out[static_cast(i) * 3U + static_cast(j)] = a[static_cast(i) * 3U] * b[static_cast(j)] + a[static_cast(i) * 3U + 1U] * b[3U + static_cast(j)] + a[static_cast(i) * 3U + 2U] * b[6U + static_cast(j)]; } } return out; } [[nodiscard]] inline auto rotate(const std::array &m, const std::array &v) -> std::array { return {m[0] * v[0] + m[1] * v[1] + m[2] * v[2], m[3] * v[0] + m[4] * v[1] + m[5] * v[2], m[6] * v[0] + m[7] * v[1] + m[8] * v[2]}; } } /** Each bone's world transform in the hierarchy's default (bind) pose. */ [[nodiscard]] inline auto bone_world_matrices(const hierarchy &h) -> std::vector { std::vector world(h.bones.size()); for (usize i = 0; i < h.bones.size(); ++i) { const auto &b = h.bones[i]; const auto local_rotation = detail::quaternion_matrix(b.qx, b.qy, b.qz, b.qw); const std::array local_translation{b.tx, b.ty, b.tz}; if (b.parent >= 0 && static_cast(b.parent) < i) { const auto &parent = world[static_cast(b.parent)]; world[i].rotation = detail::multiply(parent.rotation, local_rotation); const auto rotated = detail::rotate(parent.rotation, local_translation); world[i].translation = {parent.translation[0] + rotated[0], parent.translation[1] + rotated[1], parent.translation[2] + rotated[2]}; } else { world[i].rotation = local_rotation; world[i].translation = local_translation; } } return world; } namespace detail { /** One vertex element: which attribute, its byte offset and its type code. */ struct vertex_element { char usage = 0; uint32 index = 0; uint32 offset = 0; std::string type; }; [[nodiscard]] inline auto usage_of(char letter) -> bool { return letter == 'p' || letter == 'n' || letter == 't' || letter == 'c' || letter == 'g' || letter == 'b' || letter == 'i' || letter == 'w'; } /** Parse the D3D9 text vertex declaration `"p0:00:3f32 n0:0C:3f32 t0:1C:2f32"`. */ [[nodiscard]] inline auto parse_vertex_format(std::string_view decl) -> std::vector { std::vector elements; for (usize start = 0; start < decl.size();) { const auto end = decl.find(' ', start); const auto token = decl.substr(start, end == std::string_view::npos ? std::string_view::npos : end - start); start = end == std::string_view::npos ? decl.size() : end + 1U; const auto c1 = token.find(':'); if (c1 == std::string_view::npos) continue; const auto c2 = token.find(':', c1 + 1U); if (c2 == std::string_view::npos) continue; const auto name = token.substr(0, c1); if (name.empty() || !usage_of(name[0])) continue; vertex_element element; element.usage = name[0]; element.index = name.size() > 1U ? static_cast(std::strtoul(std::string{name.substr(1)}.c_str(), nullptr, 10)) : 0U; element.offset = static_cast(std::strtoul(std::string{token.substr(c1 + 1U, c2 - c1 - 1U)}.c_str(), nullptr, 16)); element.type = std::string{token.substr(c2 + 1U)}; elements.push_back(std::move(element)); } return elements; } /** Decode one vertex element (a small type grammar: ``). */ [[nodiscard]] inline auto decode_vertex_element(std::span buf, usize off, std::string_view type, float *out, uint32 wanted) -> uint32 { usize digits = 0; while (digits < type.size() && std::isdigit(static_cast(type[digits]))) ++digits; if (digits == 0U) return 0U; const auto count = static_cast(std::strtoul(std::string{type.substr(0, digits)}.c_str(), nullptr, 10)); const auto kind = type.substr(digits); const auto take = std::min(count, wanted); const auto bytes_per_element = (kind == "f32") ? 4U : ((kind == "u8n" || kind == "u8") ? 1U : 2U); const auto lerp = [&](uint32 i, float value) { out[i] = value; }; for (uint32 i = 0; i < take; ++i) { const auto at = off + static_cast(i) * bytes_per_element; if (kind == "f32") { lerp(i, f32(buf, off + static_cast(i) * 4U)); } else if (kind == "u8n") { lerp(i, at < buf.size() ? static_cast(buf[at]) / 255.0F : 0.0F); } else if (kind == "u8") { lerp(i, at < buf.size() ? static_cast(buf[at]) : 0.0F); } else if (kind == "s16n") { lerp(i, at + 2U <= buf.size() ? static_cast(static_cast(u16(buf, at))) / 32767.0F : 0.0F); } else if (kind == "u16n") { lerp(i, at + 2U <= buf.size() ? static_cast(u16(buf, at)) / 65535.0F : 0.0F); } else if (kind == "s16") { lerp(i, at + 2U <= buf.size() ? static_cast(static_cast(u16(buf, at))) : 0.0F); } else if (kind == "u16") { lerp(i, at + 2U <= buf.size() ? static_cast(u16(buf, at)) : 0.0F); } else if (kind == "f16") { const auto raw = at + 2U <= buf.size() ? u16(buf, at) : 0U; const auto sign = (raw & 0x8000U) != 0U ? -1.0F : 1.0F; const auto exp = (raw >> 10U) & 0x1FU; const auto mant = raw & 0x3FFU; float value = exp == 0U ? static_cast(mant) / 1024.0F * std::pow(2.0F, -14.0F) : (1.0F + static_cast(mant) / 1024.0F) * std::pow(2.0F, static_cast(exp) - 15.0F); lerp(i, sign * value); } } return take; } } /** * Decode a compiled `W3DMesh` instance. * * The compiled struct stores a GPU vertex buffer (declaration string + * packed vertices) and a triangle list; every pointer is an offset from the * instance start, so the payload is read in place. */ [[nodiscard]] inline auto decode_mesh(std::span data) -> mesh { mesh out; const auto vertex_off = detail::u32(data, 4U); const auto triangle_count = detail::u32(data, 52U); const auto triangle_items = detail::u32(data, 56U); if (vertex_off == 0U || vertex_off + 20U > data.size()) return out; const auto vertex_count = detail::u32(data, vertex_off); const auto stride = detail::u32(data, vertex_off + 4U); const auto element_items = detail::u32(data, vertex_off + 8U); const auto decl_bytes = detail::u32(data, vertex_off + 12U); const auto decl_items = detail::u32(data, vertex_off + 16U); if (vertex_count == 0U || stride == 0U || decl_items + decl_bytes > data.size()) return out; const std::string_view decl{reinterpret_cast(data.data() + decl_items), decl_bytes}; const auto elements = detail::parse_vertex_format(decl); const auto shader_len = detail::i32(data, 60U); const auto shader_ptr = detail::u32(data, 64U); if (shader_len > 0 && shader_ptr != 0U && shader_ptr + static_cast(shader_len) <= data.size()) { out.shader.assign(reinterpret_cast(data.data() + shader_ptr), static_cast(shader_len)); } // Per-mesh bone remap: a vertex's blend index selects an entry here, and // that entry is the hierarchy bone. Without it the wrong bones are used // and skinned models tear apart. const auto bone_count = detail::u32(data, vertex_off + 0x14U); const auto bone_ptr = detail::u32(data, vertex_off + 0x18U); if (bone_count > 0U && bone_count < 4096U && bone_ptr != 0U && bone_ptr + static_cast(bone_count) * 2U <= data.size()) { out.bone_remap.resize(bone_count); for (uint32 i = 0; i < bone_count; ++i) out.bone_remap[i] = detail::u16(data, bone_ptr + static_cast(i) * 2U); } out.positions.assign(static_cast(vertex_count) * 3U, 0.0F); std::vector normals(static_cast(vertex_count) * 3U, 0.0F); std::vector uvs(static_cast(vertex_count) * 2U, 0.0F); std::vector blend_indices(static_cast(vertex_count) * 4U, 0U); std::vector blend_weights(static_cast(vertex_count) * 4U, 0.0F); bool have_normals = false; bool have_uvs = false; bool have_blend = false; for (uint32 i = 0; i < vertex_count; ++i) { const auto base = static_cast(element_items) + static_cast(i) * stride; float normal[3] = {0.0F, 0.0F, 1.0F}; float uv[2] = {0.0F, 0.0F}; for (const auto &element: elements) { float value[4] = {0.0F, 0.0F, 0.0F, 0.0F}; if (element.usage == 'p' && element.index == 0U) { if (detail::decode_vertex_element(data, base + element.offset, element.type, value, 3U) >= 3U) { out.positions[static_cast(i) * 3U + 0U] = value[0]; out.positions[static_cast(i) * 3U + 1U] = value[1]; out.positions[static_cast(i) * 3U + 2U] = value[2]; } } else if (element.usage == 'n' && element.index == 0U) { if (detail::decode_vertex_element(data, base + element.offset, element.type, value, 3U) >= 3U) { normal[0] = value[0]; normal[1] = value[1]; normal[2] = value[2]; have_normals = true; } } else if (element.usage == 't' && element.index == 0U) { if (detail::decode_vertex_element(data, base + element.offset, element.type, value, 2U) >= 2U) { uv[0] = value[0]; uv[1] = value[1]; have_uvs = true; } } else if (element.usage == 'i' && element.index == 0U) { if (detail::decode_vertex_element(data, base + element.offset, element.type, value, 4U) >= 4U) { for (usize k = 0; k < 4U; ++k) blend_indices[static_cast(i) * 4U + k] = static_cast(std::clamp(value[k], 0.0F, 255.0F)); have_blend = true; } } else if (element.usage == 'w' && element.index == 0U) { if (detail::decode_vertex_element(data, base + element.offset, element.type, value, 4U) >= 4U) { for (usize k = 0; k < 4U; ++k) blend_weights[static_cast(i) * 4U + k] = value[k]; } } } normals[static_cast(i) * 3U + 0U] = normal[0]; normals[static_cast(i) * 3U + 1U] = normal[1]; normals[static_cast(i) * 3U + 2U] = normal[2]; uvs[static_cast(i) * 2U + 0U] = uv[0]; uvs[static_cast(i) * 2U + 1U] = uv[1]; } for (uint32 i = 0; i < triangle_count; ++i) { const auto base = static_cast(triangle_items) + static_cast(i) * 24U; const auto count = detail::u32(data, base); const auto ptr = detail::u32(data, base + 4U); if (count == 0U || ptr == 0U || ptr + static_cast(count) * 4U > data.size()) continue; std::vector ring(count); for (uint32 k = 0; k < count; ++k) ring[k] = detail::u32(data, ptr + static_cast(k) * 4U); for (uint32 k = 1; k + 1U < count; ++k) { // fan any n-gon out.indices.push_back(ring[0]); out.indices.push_back(ring[k]); out.indices.push_back(ring[k + 1U]); } } if (have_normals) out.normals = std::move(normals); if (have_uvs) out.uvs = std::move(uvs); if (have_blend) { out.blend_indices = std::move(blend_indices); out.blend_weights = std::move(blend_weights); } return out; } // ---- DDS -------------------------------------------------------------- namespace detail { inline auto decode_dxt_color(std::span block, usize offset, std::vector &out, uint32 w, uint32 h, uint32 x, uint32 y, bool force_four) -> void { const auto c0 = static_cast(block[offset]) | (static_cast(block[offset + 1U]) << 8U); const auto c1 = static_cast(block[offset + 2U]) | (static_cast(block[offset + 3U]) << 8U); const auto hilo = [](uint32 c) -> std::array { return {static_cast(((c >> 11U) & 0x1FU) * 255U / 31U), static_cast(((c >> 5U) & 0x3FU) * 255U / 63U), static_cast((c & 0x1FU) * 255U / 31U)}; }; const auto a = hilo(c0); const auto b = hilo(c1); std::array colors{}; colors[0] = argb(a[0], a[1], a[2]); colors[1] = argb(b[0], b[1], b[2]); if (c0 > c1 || force_four) { colors[2] = argb(static_cast((2U * a[0] + b[0]) / 3U), static_cast((2U * a[1] + b[1]) / 3U), static_cast((2U * a[2] + b[2]) / 3U)); colors[3] = argb(static_cast((a[0] + 2U * b[0]) / 3U), static_cast((a[1] + 2U * b[1]) / 3U), static_cast((a[2] + 2U * b[2]) / 3U)); } else { colors[2] = argb(static_cast((a[0] + b[0]) / 2U), static_cast((a[1] + b[1]) / 2U), static_cast((a[2] + b[2]) / 2U)); colors[3] = 0x00000000U; } uint32 indices = 0; for (uint32 k = 0; k < 4U; ++k) indices |= static_cast(block[offset + 4U + k]) << (8U * k); for (uint32 py = 0; py < 4U; ++py) { for (uint32 px = 0; px < 4U; ++px) { if (x + px >= w || y + py >= h) continue; out[static_cast(y + py) * w + (x + px)] = colors[(indices >> (2U * (py * 4U + px))) & 3U]; } } } } /** Decode a DDS (DXT1/3/5 or uncompressed RGB) to an ARGB image; empty when unsupported. */ [[nodiscard]] inline auto decode_dds(std::span data) -> image { if (data.size() < 128U || std::memcmp(data.data(), "DDS ", 4) != 0) return {}; const auto height = detail::u32(data, 12U); const auto width = detail::u32(data, 16U); const auto pfflags = detail::u32(data, 80U); const auto bits = detail::u32(data, 88U); const auto rmask = detail::u32(data, 92U); const auto gmask = detail::u32(data, 96U); const auto bmask = detail::u32(data, 100U); const auto amask = detail::u32(data, 104U); if (width == 0U || height == 0U || width > 8192U || height > 8192U) return {}; const auto fourcc = std::string_view{reinterpret_cast(data.data() + 84U), 4U}; std::vector pixels(static_cast(width) * height, 0xFF000000U); if (fourcc == "DXT1" || fourcc == "DXT3" || fourcc == "DXT5") { const auto stride = fourcc == "DXT1" ? 8U : 16U; usize pos = 128U; const auto blocks_x = (width + 3U) / 4U; const auto blocks_y = (height + 3U) / 4U; for (uint32 by = 0; by < blocks_y; ++by) { for (uint32 bx = 0; bx < blocks_x; ++bx) { if (pos + stride > data.size()) goto dds_done; // truncated: keep what we decoded const auto block = data.subspan(pos, stride); pos += stride; const uint32 x = bx * 4U; const uint32 y = by * 4U; if (fourcc == "DXT1") { detail::decode_dxt_color(block, 0U, pixels, width, height, x, y, false); } else { if (fourcc == "DXT3") { for (uint32 py = 0; py < 4U; ++py) { for (uint32 px = 0; px < 4U; ++px) { const auto i = py * 4U + px; const auto byte = block[i / 2U]; const auto alpha = static_cast((i % 2U == 0U) ? (byte & 0x0FU) : (byte >> 4U)); const auto at = static_cast(y + py) * width + (x + px); if (y + py < height && x + px < width) pixels[at] = (pixels[at] & 0x00FFFFFFU) | (static_cast(alpha) * 17U << 24U); } } } else { // DXT5 const auto a0 = block[0]; const auto a1 = block[1]; std::array palette{}; palette[0] = a0; palette[1] = a1; if (a0 > a1) { for (uint32 i = 1U; i < 7U; ++i) palette[i + 1U] = static_cast(((7U - i) * a0 + i * a1) / 7U); } else { for (uint32 i = 1U; i < 5U; ++i) palette[i + 1U] = static_cast(((5U - i) * a0 + i * a1) / 5U); palette[6] = 0U; palette[7] = 255U; } std::uint64_t abits = 0; for (uint32 k = 0; k < 6U; ++k) abits |= static_cast(block[2U + k]) << (8U * k); for (uint32 py = 0; py < 4U; ++py) { for (uint32 px = 0; px < 4U; ++px) { const auto i = py * 4U + px; const auto alpha = palette[(abits >> (3U * i)) & 7U]; const auto at = static_cast(y + py) * width + (x + px); if (y + py < height && x + px < width) pixels[at] = (pixels[at] & 0x00FFFFFFU) | (static_cast(alpha) << 24U); } } } detail::decode_dxt_color(block, 8U, pixels, width, height, x, y, true); } } } } else if ((pfflags & 0x40U) != 0U && (bits == 16U || bits == 24U || bits == 32U)) { const auto shift_of = [](uint32 mask) -> std::pair { if (mask == 0U) return {0U, 0U}; uint32 shift = 0; while (((mask >> shift) & 1U) == 0U) ++shift; uint32 size = 0; while (((mask >> (shift + size)) & 1U) != 0U) ++size; return {shift, size}; }; const auto channels = {shift_of(rmask), shift_of(gmask), shift_of(bmask), shift_of(amask)}; const auto bpp = bits / 8U; const auto *raw = data.data() + 128U; const auto available = data.size() > 128U ? (data.size() - 128U) / bpp : 0U; for (usize i = 0; i < static_cast(width) * height && i < available; ++i) { uint32 pixel = 0; for (uint32 b = 0; b < bpp; ++b) pixel |= static_cast(raw[i * bpp + b]) << (8U * b); uint32 out = 0xFF000000U; uint32 channel = 0; for (const auto [shift, size]: channels) { const auto maxv = size != 0U ? ((1U << size) - 1U) : 0U; const auto value = maxv != 0U ? (((pixel >> shift) & maxv) * 255U / maxv) : 255U; if (channel < 3U) { out |= value << (16U - channel * 8U); } else if (size != 0U) { out = (out & 0x00FFFFFFU) | (value << 24U); } ++channel; } pixels[i] = out; } } else { return {}; } dds_done:; image result(width, height); std::copy(pixels.begin(), pixels.end(), result.data()); return result; } /** Decode the DDS embedded in a compiled `Texture` instance; empty when none. */ [[nodiscard]] inline auto decode_texture(std::span instance) -> image { const auto off = detail::u32(instance, 4U); std::span dds{}; if (off > 0U && off < instance.size() && off + 4U <= instance.size() && std::memcmp(instance.data() + off, "DDS ", 4) == 0) { dds = instance.subspan(off); } else { for (usize i = 0; i + 4U <= instance.size(); ++i) { if (std::memcmp(instance.data() + i, "DDS ", 4) == 0) { dds = instance.subspan(i); break; } } } return dds.empty() ? image{} : decode_dds(dds); } /** * The diffuse texture a mesh samples, or `nullptr`. * * A `W3DMesh` stores `FXShaderConstant`s at `+76` (count) / `+80` (items); * a texture-valued constant (`0xA59096A6`) names its role and points at a * 1-based index into the mesh's cross-asset references. */ [[nodiscard]] inline auto mesh_diffuse_texture(const asset_stream &stream, const asset_stream::asset &mesh_asset) -> const asset_stream::asset * { const auto instance = stream.read_instance(mesh_asset); if (instance.empty()) return nullptr; const auto count = detail::u32(instance, 76U); const auto items = detail::u32(instance, 80U); for (uint32 i = 0; i < count; ++i) { const auto constant = detail::u32(instance, items + static_cast(i) * 4U); if (constant == 0U || detail::u32(instance, constant) != 0xA59096A6U) continue; const auto name = asset_stream::lower(detail::cstr(instance, detail::u32(instance, constant + 8U), detail::i32(instance, constant + 4U))); const bool is_diffuse = name.contains("diffuse") || name.contains("albedo") || name.contains("base") || name == "texture_0" || name == "texture0"; if (!is_diffuse) continue; const auto reference = detail::u32(instance, constant + 12U); if (reference >= 1U && reference <= mesh_asset.references.size()) { const auto [type_id, instance_id] = mesh_asset.references[reference - 1U]; if (const auto *texture = stream.find(type_id, instance_id); texture != nullptr) return texture; } } return nullptr; } // ---- scene ------------------------------------------------------------ /** A world-space triangle-soup vertex (position, normal, uv, texture layer). */ struct vertex { float x = 0.0F; float y = 0.0F; float z = 0.0F; float nx = 0.0F; float ny = 0.0F; float nz = 1.0F; float u = 0.0F; float v = 0.0F; float layer = 0.0F; }; /** Every model the map draws, flattened to world space and ready to upload. */ struct scene { std::vector vertices; std::vector indices; std::vector textures; ///< All `texture_size` square, ARGB. uint32 texture_size = 0; usize placed = 0; usize missing = 0; [[nodiscard]] auto empty() const -> bool { return indices.empty(); } [[nodiscard]] auto triangle_count() const -> usize { return indices.size() / 3U; } }; /** One placement request: an object type at a world position with a Z rotation. */ struct placement { std::string type; float x = 0.0F; float y = 0.0F; float z = 0.0F; float angle = 0.0F; float scale = 1.0F; }; namespace detail { /** Box-downscale `src` to a `size` square (averages, so atlas detail survives). */ [[nodiscard]] inline auto to_square(const image &src, uint32 size) -> image { image out(size, size); if (src.empty() || size == 0U) return out; for (uint32 y = 0; y < size; ++y) { const auto y0 = y * src.height() / size; const auto y1 = std::max(y0 + 1U, (y + 1U) * src.height() / size); for (uint32 x = 0; x < size; ++x) { const auto x0 = x * src.width() / size; const auto x1 = std::max(x0 + 1U, (x + 1U) * src.width() / size); uint32 r = 0; uint32 g = 0; uint32 b = 0; uint32 a = 0; uint32 count = 0; for (uint32 sy = y0; sy < y1; ++sy) { for (uint32 sx = x0; sx < x1; ++sx) { const auto texel = src.data()[static_cast(sy) * src.width() + sx]; r += (texel >> 16U) & 0xFFU; g += (texel >> 8U) & 0xFFU; b += texel & 0xFFU; a += (texel >> 24U) & 0xFFU; ++count; } } out.data()[static_cast(y) * size + x] = argb(static_cast(r / count), static_cast(g / count), static_cast(b / count), static_cast(a / count)); } } return out; } } /** * Flatten every placement into one world-space scene. * * @param stream The map's compiled art stream. * @param placements The objects to draw. * @param ground_height Terrain height (world Z) at a world `(x, y)`, so * objects sit on the relief instead of a flat plane. * @param texture_size Edge length of the shared texture array (0 = auto). */ [[nodiscard]] inline auto build_scene(const asset_stream &stream, std::span placements, const std::function &ground_height = {}, uint32 texture_size = 128U) -> scene { scene out; out.texture_size = texture_size == 0U ? 128U : texture_size; std::unordered_map texture_layers; const auto layer_for = [&](const asset_stream::asset *texture) -> uint32 { if (texture == nullptr) return 0xFFFFFFFFU; const auto key = asset_stream::lower(texture->name); if (const auto it = texture_layers.find(key); it != texture_layers.end()) return it->second; auto decoded = decode_texture(stream.read_instance(*texture)); if (decoded.empty()) return 0xFFFFFFFFU; const auto layer = static_cast(out.textures.size()); out.textures.push_back(detail::to_square(decoded, out.texture_size)); texture_layers.emplace(key, layer); return layer; }; // Bone world transforms per skeleton, cached by hierarchy name. Meshes are // skinned; their vertices are stored in *bone space*, so a static render // must rebuild the default pose and blend. std::unordered_map> skeletons; const auto skeleton_for = [&](std::string_view mesh_instance) -> const std::vector * { const auto dot = mesh_instance.find('.'); const auto base = std::string{mesh_instance.substr(0, dot)}; const auto key = asset_stream::lower(base); if (const auto it = skeletons.find(key); it != skeletons.end()) return it->second.empty() ? nullptr : &it->second; std::vector matrices; if (const auto *asset = stream.find_name("W3DHierarchy:" + base); asset != nullptr) { matrices = bone_world_matrices(decode_hierarchy(stream.read_instance(*asset))); } const auto it = skeletons.emplace(key, std::move(matrices)).first; return it->second.empty() ? nullptr : &it->second; }; for (const auto &item: placements) { const auto meshes = stream.meshes_for(item.type); if (meshes.empty()) { ++out.missing; continue; } const auto cos_a = std::cos(item.angle); const auto sin_a = std::sin(item.angle); const auto base_z = (ground_height ? ground_height(item.x, item.y) : 0.0F) + item.z; bool drawn = false; for (const auto *mesh_asset: meshes) { // Only opaque material parts are drawn; meshes with no diffuse // role are effects (FX-light billboards, `DefaultW3D.fx`) or // ambient helpers and would otherwise render as garbage. const auto *diffuse = mesh_diffuse_texture(stream, *mesh_asset); if (diffuse == nullptr) continue; const auto geometry = decode_mesh(stream.read_instance(*mesh_asset)); if (geometry.indices.empty()) continue; // Damage-fill shells (damaged interior/wreckage) are drawn on top // of the main shell in our opaque pass and would show its // interior texture (e.g. orange `CBBuilding_Wood`); skip them. if (asset_stream::lower(geometry.shader).contains("damagefill")) continue; drawn = true; const auto layer = layer_for(diffuse); const auto *skeleton = geometry.skinned() ? skeleton_for(mesh_asset->instance_name()) : nullptr; const auto base_vertex = static_cast(out.vertices.size()); const auto vertex_count = geometry.positions.size() / 3U; for (usize i = 0; i < vertex_count; ++i) { auto px = geometry.positions[i * 3U + 0U]; auto py = geometry.positions[i * 3U + 1U]; auto pz = geometry.positions[i * 3U + 2U]; auto nx = geometry.normals.empty() ? 0.0F : geometry.normals[i * 3U + 0U]; auto ny = geometry.normals.empty() ? 0.0F : geometry.normals[i * 3U + 1U]; auto nz = geometry.normals.empty() ? 1.0F : geometry.normals[i * 3U + 2U]; if (skeleton != nullptr) { // The compiled shader binds one joint per vertex // (`blendindices.x`), remapped through the mesh's bone // table, then transforms by the joint's world matrix. auto joint = static_cast(geometry.blend_indices[i * 4U + 0U]); if (joint < geometry.bone_remap.size()) joint = geometry.bone_remap[joint]; if (joint < skeleton->size()) { const auto &m = (*skeleton)[joint]; const std::array p{px, py, pz}; const auto rp = detail::rotate(m.rotation, p); const std::array n{nx, ny, nz}; const auto rn = detail::rotate(m.rotation, n); px = rp[0] + m.translation[0]; py = rp[1] + m.translation[1]; pz = rp[2] + m.translation[2]; nx = rn[0]; ny = rn[1]; nz = rn[2]; } } vertex v; v.x = item.x + (px * item.scale) * cos_a - (py * item.scale) * sin_a; v.y = item.y + (px * item.scale) * sin_a + (py * item.scale) * cos_a; v.z = base_z + pz * item.scale; v.nx = nx * cos_a - ny * sin_a; v.ny = nx * sin_a + ny * cos_a; v.nz = nz; if (!geometry.uvs.empty()) { v.u = geometry.uvs[i * 2U + 0U]; v.v = geometry.uvs[i * 2U + 1U]; } v.layer = layer == 0xFFFFFFFFU ? 0.0F : static_cast(layer); out.vertices.push_back(v); } for (const auto index: geometry.indices) out.indices.push_back(base_vertex + index); } if (drawn) { ++out.placed; } else { ++out.missing; } } return out; } }