первые наработки по рендеру

This commit is contained in:
2026-05-23 00:08:32 +04:00
parent 1a56b22e38
commit bba203aa53
25 changed files with 1841 additions and 110 deletions
+19 -11
View File
@@ -270,16 +270,17 @@ inline void adt_placement_pos_to_godot(float px, float pz, float py,
gz = py;
}
// MDDF/MODF Euler angles are stored in degrees using the same axis layout as
// placement positions: Y-up, so rot[1] is yaw. Map each axis straight through
// to Godot's Y-up Euler space — the position conversion is identity, so the
// rotation conversion is identity too. rot[1] gets +180° later for WMOs.
// MDDF/MODF Euler angles are stored in degrees in WoW render space (Y-up):
// rot[0] — pitch around X, rot[1] yaw around Y, rot[2] — roll around Z.
// WoW's "0 yaw" faces a different direction than Godot's, so rot[1] needs
// a fixed +90° offset (matches Blender wow.export importer). Other axes
// map straight through.
inline void adt_placement_rot_to_godot(float rx, float ry_yaw, float rz,
float &gx, float &gy, float &gz) {
constexpr float D2R = (float)(M_PI / 180.0);
gx = rx * D2R;
gy = ry_yaw * D2R;
gz = rz * D2R;
gx = rx * D2R;
gy = (ry_yaw - 90.0f) * D2R;
gz = rz * D2R;
}
} // namespace
@@ -382,12 +383,19 @@ void ADTLoader::_parse_adt(const uint8_t *raw, size_t len, Dictionary &result) {
float gx, gy, gz, rx, ry, rz;
adt_placement_pos_to_godot(od[i].pos[0], od[i].pos[1], od[i].pos[2], gx, gy, gz);
adt_placement_rot_to_godot(od[i].rot[0], od[i].rot[1], od[i].rot[2], rx, ry, rz);
// MODF (WMO) needs an additional +90° yaw on top of the M2 offset
// (so total WMO yaw offset = 0°). WMO local-forward differs from M2.
ry -= (float)(M_PI * 0.5);
Dictionary p;
p["name_id"] = (int)od[i].nameId;
p["pos"] = Vector3(gx, gy, gz);
p["rot"] = Vector3(rx, ry, rz);
p["scale"] = 1.0f;
p["name_id"] = (int)od[i].nameId;
p["unique_id"] = (int)od[i].uniqueId;
p["pos"] = Vector3(gx, gy, gz);
p["rot"] = Vector3(rx, ry, rz);
p["scale"] = 1.0f;
p["flags"] = (int)od[i].flags;
p["doodad_set"] = (int)od[i].doodadSet;
p["name_set"] = (int)od[i].nameSet;
wmo_placements.push_back(p);
}
}
+8 -4
View File
@@ -37,10 +37,14 @@ namespace godot {
//
// Placement Dictionary (both M2 and WMO):
// {
// "name_id": int, # index into m2_names / wmo_names
// "pos": Vector3, # Godot world coords
// "rot": Vector3, # Euler angles (radians)
// "scale": float, # M2 only (WMO always 1.0)
// "name_id": int, # index into m2_names / wmo_names
// "unique_id": int, # WMO only — MODF uniqueId (dedupe key across tiles)
// "pos": Vector3, # Godot world coords
// "rot": Vector3, # Euler angles (radians)
// "scale": float, # M2 only (WMO always 1.0)
// "flags": int, # WMO only
// "doodad_set": int, # WMO only
// "name_set": int, # WMO only
// }
//
// Chunk Dictionary:
+4 -2
View File
@@ -190,8 +190,10 @@ Dictionary M2Loader::parse_m2(const std::vector<uint8_t> &buf, const std::string
// WoW model space (X right, Y forward, Z up) → Godot (X right, Y up, Z back)
vertices[i] = Vector3( v.pos[0], v.pos[2], -v.pos[1]);
normals[i] = Vector3( v.normal[0], v.normal[2], -v.normal[1]);
// WoW V origin is top-left; Godot expects bottom-left → flip V
uvs[i] = Vector2(v.texCoords[0], 1.0f - v.texCoords[1]);
// WoW BLP textures and Godot 4 sampling both use V=0 at top
// (D3D/Vulkan convention) — pass UVs through unchanged. Same as
// wmo_loader's MOTV handling.
uvs[i] = Vector2(v.texCoords[0], v.texCoords[1]);
}
}
+2
View File
@@ -4,6 +4,7 @@
#include "adt_loader.h"
#include "blp_loader.h"
#include "m2_loader.h"
#include "wdt_loader.h"
#include <godot_cpp/core/defs.hpp>
#include <godot_cpp/godot.hpp>
@@ -17,6 +18,7 @@ void initialize_mpq_extractor_module(ModuleInitializationLevel p_level) {
ClassDB::register_class<ADTLoader>();
ClassDB::register_class<BLPLoader>();
ClassDB::register_class<M2Loader>();
ClassDB::register_class<WDTLoader>();
}
void uninitialize_mpq_extractor_module(ModuleInitializationLevel p_level) {
+156
View File
@@ -0,0 +1,156 @@
#include "wdt_loader.h"
#include "wow_chunk_reader.h"
#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
#include <godot_cpp/variant/vector3.hpp>
#include <fstream>
#include <cstring>
using namespace godot;
#pragma pack(push, 1)
struct MPHD {
uint32_t flags;
uint32_t something;
uint32_t unused[6];
};
struct MAINEntry {
uint32_t flags; // bit 0 = has_adt
uint32_t async_id;
};
struct WDTMODFEntry {
uint32_t nameId;
uint32_t uniqueId;
float pos[3];
float rot[3];
float bboxMin[3];
float bboxMax[3];
uint16_t flags;
uint16_t doodadSet;
uint16_t nameSet;
uint16_t padding;
};
#pragma pack(pop)
std::vector<uint8_t> WDTLoader::read_file(const std::string &path) {
std::ifstream f(path, std::ios::binary | std::ios::ate);
if (!f) return {};
auto sz = f.tellg(); f.seekg(0);
std::vector<uint8_t> buf(sz);
f.read(reinterpret_cast<char *>(buf.data()), sz);
return buf;
}
std::string WDTLoader::to_std(const String &s) {
return std::string(s.utf8().get_data());
}
String WDTLoader::to_godot(const std::string &s) {
return String(s.c_str());
}
Dictionary WDTLoader::load_wdt(const String &path) {
Dictionary result;
const std::string p = to_std(path);
std::vector<uint8_t> buf = read_file(p);
if (buf.empty()) {
UtilityFunctions::push_warning("WDTLoader: cannot open file: ", path);
return result;
}
_parse_wdt(buf.data(), buf.size(), result);
return result;
}
Dictionary WDTLoader::load_wdt_from_bytes(const PackedByteArray &bytes) {
Dictionary result;
if (bytes.size() <= 0) return result;
_parse_wdt(bytes.ptr(), (size_t)bytes.size(), result);
return result;
}
void WDTLoader::_parse_wdt(const uint8_t *raw, size_t len, Dictionary &result) {
ChunkReader reader(raw, len);
uint32_t version = 0;
uint32_t flags = 0;
bool found_mphd = false;
Array tiles;
WoWChunk c;
while (reader.next(c)) {
if (c.is("MVER") && c.size >= 4) {
std::memcpy(&version, c.data, 4);
} else if (c.is("MPHD") && c.size >= sizeof(MPHD)) {
const MPHD &mphd = c.as<MPHD>();
flags = mphd.flags;
found_mphd = true;
} else if (c.is("MAIN")) {
const uint32_t expected = 64 * 64 * (uint32_t)sizeof(MAINEntry);
if (c.size < expected) {
UtilityFunctions::push_warning("WDTLoader: MAIN chunk too small: ", (int)c.size);
continue;
}
const MAINEntry *entries = c.array<MAINEntry>();
for (int y = 0; y < 64; ++y) {
for (int x = 0; x < 64; ++x) {
const MAINEntry &e = entries[y * 64 + x];
if (!(e.flags & 0x1)) continue;
Dictionary td;
td["x"] = (int)x;
td["y"] = (int)y;
td["flags"] = (int)e.flags;
tiles.push_back(td);
}
}
} else if (c.is("MWMO") && c.size > 0) {
// Global WMO filename (null-terminated).
const char *s = reinterpret_cast<const char *>(c.data);
size_t n = c.size;
while (n > 0 && s[n - 1] == '\0') --n;
result["global_wmo_name"] = String(std::string(s, n).c_str());
} else if (c.is("MODF") && c.size >= sizeof(WDTMODFEntry)) {
const WDTMODFEntry &e = c.as<WDTMODFEntry>();
float gx, gy, gz;
wow_to_godot(e.pos[0], e.pos[1], e.pos[2], gx, gy, gz);
float rx, ry, rz;
wow_rot_to_godot(e.rot[0], e.rot[1], e.rot[2], rx, ry, rz);
// Global WMOs (MODF) get the same extra yaw as tile WMOs — see
// the matching comment in adt_loader.cpp MODF loop.
ry -= (float)(M_PI * 0.5);
Dictionary placement;
placement["name_id"] = (int)e.nameId;
placement["unique_id"] = (int)e.uniqueId;
placement["pos"] = Vector3(gx, gy, gz);
placement["rot"] = Vector3(rx, ry, rz);
placement["flags"] = (int)e.flags;
placement["doodad_set"] = (int)e.doodadSet;
placement["name_set"] = (int)e.nameSet;
result["global_wmo_placement"] = placement;
}
}
if (!found_mphd) {
UtilityFunctions::push_warning("WDTLoader: MPHD chunk missing");
}
result["version"] = (int)version;
result["flags"] = (int)flags;
result["has_global_wmo"] = (flags & 0x1) != 0;
result["big_alpha"] = (flags & 0x4) != 0;
result["sort_by_size_class"] = (flags & 0x8) != 0;
result["tiles"] = tiles;
}
void WDTLoader::_bind_methods() {
ClassDB::bind_method(D_METHOD("load_wdt", "path"), &WDTLoader::load_wdt);
ClassDB::bind_method(D_METHOD("load_wdt_from_bytes", "bytes"), &WDTLoader::load_wdt_from_bytes);
}
+55
View File
@@ -0,0 +1,55 @@
#pragma once
#include <vector>
#include <string>
#include <cstdint>
#include <godot_cpp/classes/ref_counted.hpp>
#include <godot_cpp/variant/dictionary.hpp>
#include <godot_cpp/variant/array.hpp>
#include <godot_cpp/variant/string.hpp>
#include <godot_cpp/variant/packed_byte_array.hpp>
namespace godot {
// ─────────────────────────────────────────────────────────────────────────────
// WDTLoader
//
// Loads WoW 3.3.5a WDT map descriptors.
//
// Usage (GDScript):
// var wdt = WDTLoader.new()
// var data = wdt.load_wdt("C:/wow/Data/World/Maps/Azeroth/Azeroth.wdt")
//
// Return Dictionary:
// {
// "version": int, # MVER (expected 18)
// "flags": int, # MPHD.flags
// "has_global_wmo": bool, # flags & 0x1
// "big_alpha": bool, # flags & 0x4
// "sort_by_size_class": bool, # flags & 0x8
// "tiles": Array[Dictionary], # one entry per ADT present (flag 0x1 in MAIN)
// # { "x": int, "y": int, "flags": int }
// # optional, only when has_global_wmo:
// "global_wmo_name": String,
// "global_wmo_placement": Dictionary, # MODF-like entry
// }
// ─────────────────────────────────────────────────────────────────────────────
class WDTLoader : public RefCounted {
GDCLASS(WDTLoader, RefCounted)
public:
Dictionary load_wdt(const String &path);
Dictionary load_wdt_from_bytes(const PackedByteArray &bytes);
protected:
static void _bind_methods();
private:
static std::vector<uint8_t> read_file(const std::string &path);
static std::string to_std(const String &s);
static String to_godot(const std::string &s);
static void _parse_wdt(const uint8_t *raw, size_t len, Dictionary &result);
};
} // namespace godot
+139 -1
View File
@@ -4,6 +4,7 @@
#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
#include <godot_cpp/variant/packed_string_array.hpp>
#include <godot_cpp/variant/quaternion.hpp>
#include <fstream>
#include <filesystem>
@@ -85,8 +86,40 @@ struct MONREntry { float x, y, z; };
struct MOTVEntry { float u, v; };
struct MOCVEntry { uint8_t b, g, r, a; };
struct MLIQHeader { // Liquid header inside MLIQ chunk
uint32_t xverts; // = xtiles + 1
uint32_t yverts; // = ytiles + 1
uint32_t xtiles;
uint32_t ytiles;
float corner[3]; // base position (WMO local, Z-up)
uint16_t materialId;
};
struct MLIQVertex { // 8 bytes per vertex (interpretation depends on liquid type)
uint32_t data; // flow info or s,t for magma — we ignore
float height;
};
struct MODSEntry { // Doodad set: 32 bytes
char name[20];
uint32_t startDoodad; // first MODD index
uint32_t nDoodads;
uint32_t unused;
};
struct MODDEntry { // Doodad placement: 40 bytes
uint32_t nameOfsAndFlags; // low 24 = MODN byte offset, high 8 = flags
float pos[3]; // WMO local (Z-up)
float rot[4]; // quaternion (qx, qy, qz, qw) in WMO local
float scale;
uint8_t color[4]; // BGRA tint
};
#pragma pack(pop)
// MLIQ tile size in WoW yards (same constant as ADT MCNK unit cells).
static constexpr float WMO_LIQUID_UNIT_SIZE = 4.1666666f;
// ─────────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────────
@@ -122,6 +155,14 @@ Dictionary WMOLoader::parse_root(const std::vector<uint8_t> &buf) {
Array materials;
std::vector<std::string> tex_strings;
// Two-pass: first collect raw chunks, then build (MODD/MODN/MODS may appear in any order).
const uint8_t *modn_data = nullptr;
size_t modn_size = 0;
const MODSEntry *mods = nullptr;
uint32_t mods_count = 0;
const MODDEntry *modd = nullptr;
uint32_t modd_count = 0;
while (reader.next(chunk)) {
if (chunk.is("MOTX")) {
@@ -152,11 +193,69 @@ Dictionary WMOLoader::parse_root(const std::vector<uint8_t> &buf) {
mat["texture1"] = tex_idx(mt[i].texUnit1);
materials.push_back(mat);
}
} else if (chunk.is("MODS")) {
mods = chunk.array<MODSEntry>();
mods_count = chunk.count_of(sizeof(MODSEntry));
} else if (chunk.is("MODN")) {
modn_data = chunk.data;
modn_size = chunk.size;
} else if (chunk.is("MODD")) {
modd = chunk.array<MODDEntry>();
modd_count = chunk.count_of(sizeof(MODDEntry));
}
}
root["textures"] = textures;
root["materials"] = materials;
// ── Doodad sets ─────────────────────────────────────────────────────────
Array doodad_sets;
for (uint32_t i = 0; i < mods_count; ++i) {
Dictionary s;
char buf[21]; std::memcpy(buf, mods[i].name, 20); buf[20] = 0;
s["name"] = String(buf);
s["start"] = (int)mods[i].startDoodad;
s["count"] = (int)mods[i].nDoodads;
doodad_sets.push_back(s);
}
root["doodad_sets"] = doodad_sets;
// ── Doodad placements ───────────────────────────────────────────────────
// Position/rotation come in WMO local (Z-up). Convert to Godot Y-up the
// same way as MOVT vertices: (lx, ly, lz) → (-ly, lz, -lx). Quaternion
// vector part transforms as a vector under that basis change; the scalar
// part is kept (signed `qw` → `qw`). The basis change has det = -1 which
// reverses winding; for rotations of M2 prototypes that already live in
// Godot-space (built via M2Builder), this gives the visually correct
// orientation.
Array doodad_placements;
for (uint32_t i = 0; i < modd_count; ++i) {
const MODDEntry &d = modd[i];
// Resolve doodad name from MODN by byte offset.
uint32_t name_ofs = d.nameOfsAndFlags & 0x00FFFFFFu;
String name;
if (modn_data && name_ofs < modn_size) {
const char *s = reinterpret_cast<const char *>(modn_data + name_ofs);
// Bound by remaining MODN bytes.
size_t max_len = modn_size - name_ofs;
size_t len = 0;
while (len < max_len && s[len] != 0) ++len;
name = String(std::string(s, len).c_str());
}
Dictionary p;
p["name"] = name;
p["flags"] = (int)((d.nameOfsAndFlags >> 24) & 0xFFu);
p["pos"] = Vector3(-d.pos[1], d.pos[2], -d.pos[0]);
p["rot"] = Quaternion(-d.rot[1], d.rot[2], -d.rot[0], d.rot[3]);
p["scale"] = d.scale;
p["color"] = Color(d.color[2] / 255.f, d.color[1] / 255.f,
d.color[0] / 255.f, d.color[3] / 255.f);
doodad_placements.push_back(p);
}
root["doodad_placements"] = doodad_placements;
return root;
}
@@ -214,7 +313,7 @@ Dictionary WMOLoader::parse_group(const std::vector<uint8_t> &buf) {
auto *v = sc.array<MOTVEntry>();
uvs.resize(n);
for (uint32_t i = 0; i < n; ++i)
uvs[i] = Vector2(v[i].u, v[i].v);
uvs[i] = Vector2(v[i].u, 1.0f - v[i].v);
} else if (sc.is("MOVI")) {
uint32_t n = sc.size / 2;
@@ -244,6 +343,45 @@ Dictionary WMOLoader::parse_group(const std::vector<uint8_t> &buf) {
b["flags"] = (int)ba[i].flags;
batches.push_back(b);
}
} else if (sc.is("MLIQ") && sc.size >= sizeof(MLIQHeader)) {
const auto &h = *reinterpret_cast<const MLIQHeader *>(sc.data);
size_t need = sizeof(MLIQHeader)
+ (size_t)h.xverts * h.yverts * sizeof(MLIQVertex)
+ (size_t)h.xtiles * h.ytiles;
if (need <= sc.size && h.xverts > 0 && h.yverts > 0
&& h.xtiles > 0 && h.ytiles > 0) {
auto *verts = reinterpret_cast<const MLIQVertex *>(
sc.data + sizeof(MLIQHeader));
auto *tiles = sc.data + sizeof(MLIQHeader)
+ (size_t)h.xverts * h.yverts * sizeof(MLIQVertex);
PackedFloat32Array heights;
heights.resize((int)(h.xverts * h.yverts));
{
float *dst = heights.ptrw();
for (uint32_t i = 0; i < h.xverts * h.yverts; ++i)
dst[i] = verts[i].height;
}
PackedByteArray tile_flags;
tile_flags.resize((int)(h.xtiles * h.ytiles));
std::memcpy(tile_flags.ptrw(), tiles,
(size_t)h.xtiles * h.ytiles);
Dictionary liquid;
liquid["xverts"] = (int)h.xverts;
liquid["yverts"] = (int)h.yverts;
liquid["xtiles"] = (int)h.xtiles;
liquid["ytiles"] = (int)h.ytiles;
// Corner in WMO local (Z-up). Builder converts to Godot.
liquid["corner"] = Vector3(h.corner[0], h.corner[1], h.corner[2]);
liquid["material_id"] = (int)h.materialId;
liquid["unit_size"] = WMO_LIQUID_UNIT_SIZE;
liquid["heights"] = heights;
liquid["tiles"] = tile_flags;
group["liquid"] = liquid;
}
}
}
+6 -4
View File
@@ -86,7 +86,9 @@ inline void wow_to_godot(float wx, float wy, float wz,
gz = -(wx - 17066.666f);
}
// WoW rotation (degrees, ZXY order) → Godot Euler (radians, XYZ)
// WoW MDDF/MODF rotation (degrees, render-space Y-up: rx=pitch, ry=yaw, rz=roll)
// → Godot Euler (radians). WoW's "0 yaw" differs from Godot's, so ry needs
// a fixed +90° offset (matches Blender wow.export importer).
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
@@ -94,9 +96,9 @@ inline void wow_rot_to_godot(float rx, float ry, float rz,
float &gx, float &gy, float &gz)
{
constexpr float D2R = (float)(M_PI / 180.0);
gx = rx * D2R;
gy = (rz - 90.0f) * D2R; // WoW Z-rot maps to Godot Y
gz = -ry * D2R;
gx = rx * D2R;
gy = (ry - 90.0f) * D2R;
gz = rz * D2R;
}
// Collect null-separated strings from a chunk into a vector