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

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
@@ -19,10 +19,9 @@ update_interval = 0.1
max_concurrent_tile_tasks = 4
chunk_ops_per_tick = 64
cached_tile_mesh_limit = 48
use_baked_tile_cache = false
editor_follow_view_camera = false
enable_water = true
terrain_cast_shadows = true
m2_cast_shadows = null
[node name="Camera3D" type="Camera3D" parent="." unique_id=502573687]
transform = Transform3D(1, 0, 0, 0, 0.707107, 0.707107, 0, -0.707107, 0.707107, 0, 300, 300)
@@ -36,6 +35,9 @@ fast_mult = 8.0
transform = Transform3D(0.866025, -0.353553, 0.353553, 0, 0.707107, 0.707107, -0.5, -0.612372, 0.612372, 0, 0, 0)
light_energy = 1.5
shadow_enabled = true
directional_shadow_mode = 1
directional_shadow_fade_start = 0.85
directional_shadow_max_distance = 2200.0
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=12906896]
environment = SubResource("Environment_1")
+311 -59
View File
@@ -10,6 +10,7 @@ const REQUIRED_BAKED_TILE_FORMAT_VERSION := 4
const TILE_SIZE := 533.33333
const CHUNK_SIZE := TILE_SIZE / 16.0
const M2_RIGHT_YAW_OFFSET := 0.0
@export var extracted_dir: String = "res://data/extracted"
@export var map_name: String = "Azeroth"
@@ -51,6 +52,14 @@ const CHUNK_SIZE := TILE_SIZE / 16.0
@export var enable_m2_assets: bool = true
@export var enable_m2_placeholders: bool = false
@export var m2_placeholder_only_trees: bool = true
## Dynamic doodad render radius in ADT tiles. Negative keeps all loaded M2s active.
@export var m2_tile_radius: int = 2
## Dynamic WMO render radius in ADT tiles. Negative keeps all loaded WMOs active.
@export var wmo_tile_radius: int = 4
## Godot visibility range applied to instantiated M2 MultiMesh nodes. <= 0 disables it.
@export var m2_visibility_range: float = TILE_SIZE * 3.0
## Godot visibility range applied to instantiated WMO trees. <= 0 disables it.
@export var wmo_visibility_range: float = TILE_SIZE * 5.0
@export var m2_cache_dir: String = "res://data/cache/m2_glb"
@export var wmo_cache_dir: String = "res://data/cache/wmo_tscn"
@export var terrain_cast_shadows: bool = false
@@ -61,6 +70,7 @@ const CHUNK_SIZE := TILE_SIZE / 16.0
var _builder
var _map_dir := ""
var _abs_map_dir := ""
var _wdt_info: Dictionary = {}
var _available_tiles: Dictionary = {}
var _tile_states: Dictionary = {}
var _tile_load_queue: Array = []
@@ -91,6 +101,8 @@ var _wmo_prototype_cache: Dictionary = {}
var _wmo_missing_cache: Dictionary = {}
var _m2_scene_cache: Dictionary = {}
var _m2_missing_cache: Dictionary = {}
var _world_wmo_root: Node3D
var _wmo_registry: Dictionary = {}
func _ready() -> void:
@@ -110,10 +122,11 @@ func _ready() -> void:
_terrain_root.name = "Terrain"
add_child(_terrain_root)
_reset_terrain_root_children()
_ensure_world_wmo_root()
_set_editor_owner_recursive(_terrain_root)
_scan_available_tiles()
if _available_tiles.is_empty():
if _available_tiles.is_empty() and not _wdt_has_global_wmo():
push_error("No ADT tiles found in %s" % _map_dir)
return
if _can_use_baked_tile_cache():
@@ -121,6 +134,9 @@ func _ready() -> void:
if not DirAccess.dir_exists_absolute(baked_dir):
push_warning("Baked terrain cache not found: %s. Runtime will fall back to raw ADT builds." % baked_dir)
if enable_wmo:
_place_global_wmo()
if Engine.is_editor_hint():
_tick_editor_streaming()
elif auto_position_camera:
@@ -185,6 +201,64 @@ func _tick_editor_streaming() -> void:
func _scan_available_tiles() -> void:
_available_tiles.clear()
_wdt_info = {}
if _load_tiles_from_wdt():
print("StreamingWorld: loaded %d ADT tiles from WDT for %s" % [_available_tiles.size(), map_name])
return
push_warning("StreamingWorld: falling back to directory scan for %s (no WDT)" % map_name)
_load_tiles_from_directory()
print("StreamingWorld: found %d ADT tiles by directory scan for %s" % [_available_tiles.size(), map_name])
func _load_tiles_from_wdt() -> bool:
if not ClassDB.class_exists("WDTLoader"):
return false
var wdt_path_abs := _abs_map_dir.path_join("%s.wdt" % map_name)
if not FileAccess.file_exists(wdt_path_abs):
return false
var loader = ClassDB.instantiate("WDTLoader")
if loader == null:
return false
var data: Dictionary = loader.call("load_wdt", wdt_path_abs)
if data.is_empty():
push_warning("StreamingWorld: WDTLoader returned empty data for %s" % wdt_path_abs)
return false
var tiles: Array = data.get("tiles", [])
for tile_variant in tiles:
if not (tile_variant is Dictionary):
continue
var entry: Dictionary = tile_variant
var tx: int = int(entry.get("x", -1))
var ty: int = int(entry.get("y", -1))
if tx < 0 or tx > 63 or ty < 0 or ty > 63:
continue
var adt_file := "%s_%d_%d.adt" % [map_name, tx, ty]
var adt_abs := _abs_map_dir.path_join(adt_file)
# WDT flags the tile as present, but the ADT may still be missing in
# a partial extraction. Skip so downstream code never chases a ghost.
if not FileAccess.file_exists(adt_abs):
continue
var key := _tile_key(tx, ty)
_available_tiles[key] = adt_abs
_tile_min.x = min(_tile_min.x, tx)
_tile_min.y = min(_tile_min.y, ty)
_tile_max.x = max(_tile_max.x, tx)
_tile_max.y = max(_tile_max.y, ty)
_wdt_info = data
return not _available_tiles.is_empty()
func _load_tiles_from_directory() -> void:
var dir := DirAccess.open(_map_dir)
if dir == null:
push_error("Cannot open map directory: %s" % _map_dir)
@@ -210,8 +284,6 @@ func _scan_available_tiles() -> void:
_tile_max.x = max(_tile_max.x, tx)
_tile_max.y = max(_tile_max.y, ty)
print("StreamingWorld: found %d ADT tiles for %s" % [_available_tiles.size(), map_name])
func _refresh_streaming_targets(force: bool) -> void:
var camera := _get_stream_camera()
@@ -312,6 +384,7 @@ func _apply_streaming_target(wanted_tiles: Dictionary, focus_pos: Vector3) -> vo
else:
state["desired_lods"] = {}
state["desired_tile_lod"] = -1
state = _sync_detail_assets(state, focus_pos)
_tile_states[key] = state
_rebuild_chunk_queues()
@@ -592,25 +665,12 @@ func _finalize_loaded_tile(request: Dictionary, data: Dictionary) -> void:
if water_root:
tile_root.add_child(water_root)
_set_editor_owner_recursive(water_root)
if enable_m2_assets:
_build_tile_m2_assets(
tile_root,
tile_origin,
data.get("m2_names", PackedStringArray()),
data.get("m2_placements", []))
if enable_m2_placeholders:
_build_tile_m2_placeholders(
tile_root,
tile_origin,
data.get("m2_names", PackedStringArray()),
data.get("m2_placements", []))
if enable_wmo:
_build_tile_wmos(
tile_root,
tile_origin,
data.get("wmo_names", PackedStringArray()),
data.get("wmo_placements", []))
var state := {
"key": key,
"tx": request["tx"],
@@ -629,11 +689,18 @@ func _finalize_loaded_tile(request: Dictionary, data: Dictionary) -> void:
"desired_tile_lod": -1,
"wanted": true,
"root": tile_root,
"m2_names": data.get("m2_names", PackedStringArray()),
"m2_placements": data.get("m2_placements", []),
"m2_built": false,
"wmo_names": data.get("wmo_names", PackedStringArray()),
"wmo_placements": data.get("wmo_placements", []),
"wmo_refs": [],
}
# Use _last_focus_pos — already set correctly for both editor and game.
state["desired_lods"] = _compute_desired_lods(state, _last_focus_pos)
state["desired_tile_lod"] = -1 if not state["desired_lods"].is_empty() else _compute_desired_tile_lod(state, _last_focus_pos)
state = _sync_detail_assets(state, _last_focus_pos)
_tile_states[key] = state
_rebuild_chunk_queues()
@@ -663,27 +730,14 @@ func _finalize_loaded_baked_tile(request: Dictionary, baked_tile: Resource) -> v
var tile_origin: Vector3 = baked_tile.get("tile_origin")
tile_root.position = tile_origin
if enable_m2_assets:
var baked_m2_names_value: Variant = baked_tile.get("m2_names")
var baked_m2_placements_value: Variant = baked_tile.get("m2_placements")
var baked_m2_names: PackedStringArray = baked_m2_names_value if baked_m2_names_value is PackedStringArray else PackedStringArray()
var baked_m2_placements: Array = baked_m2_placements_value if baked_m2_placements_value is Array else []
_build_tile_m2_assets(
tile_root,
tile_origin,
baked_m2_names,
baked_m2_placements)
if enable_wmo:
var baked_wmo_names_value: Variant = baked_tile.get("wmo_names")
var baked_wmo_placements_value: Variant = baked_tile.get("wmo_placements")
var baked_wmo_names: PackedStringArray = baked_wmo_names_value if baked_wmo_names_value is PackedStringArray else PackedStringArray()
var baked_wmo_placements: Array = baked_wmo_placements_value if baked_wmo_placements_value is Array else []
_build_tile_wmos(
tile_root,
tile_origin,
baked_wmo_names,
baked_wmo_placements)
var baked_m2_names_value: Variant = baked_tile.get("m2_names")
var baked_m2_placements_value: Variant = baked_tile.get("m2_placements")
var baked_m2_names: PackedStringArray = baked_m2_names_value if baked_m2_names_value is PackedStringArray else PackedStringArray()
var baked_m2_placements: Array = baked_m2_placements_value if baked_m2_placements_value is Array else []
var baked_wmo_names_value: Variant = baked_tile.get("wmo_names")
var baked_wmo_placements_value: Variant = baked_tile.get("wmo_placements")
var baked_wmo_names: PackedStringArray = baked_wmo_names_value if baked_wmo_names_value is PackedStringArray else PackedStringArray()
var baked_wmo_placements: Array = baked_wmo_placements_value if baked_wmo_placements_value is Array else []
var state := {
"key": key,
"tx": request["tx"],
@@ -702,9 +756,16 @@ func _finalize_loaded_baked_tile(request: Dictionary, baked_tile: Resource) -> v
"desired_tile_lod": -1,
"wanted": true,
"root": tile_root,
"m2_names": baked_m2_names,
"m2_placements": baked_m2_placements,
"m2_built": false,
"wmo_names": baked_wmo_names,
"wmo_placements": baked_wmo_placements,
"wmo_refs": [],
}
state["desired_tile_lod"] = _compute_desired_tile_lod(state, _last_focus_pos)
state = _sync_detail_assets(state, _last_focus_pos)
_tile_states[key] = state
_rebuild_chunk_queues()
if debug_streaming:
@@ -910,6 +971,7 @@ func _release_tile(key: String) -> void:
return
var state: Dictionary = _tile_states[key]
_unregister_tile_wmos(state)
var tile_lod_rid: RID = state.get("tile_lod_rid", RID())
if tile_lod_rid.is_valid():
_free_render_instance(tile_lod_rid)
@@ -949,6 +1011,10 @@ func _clear_streamed_world() -> void:
_tile_states.clear()
_clear_tile_mesh_cache()
_clear_wmo_registry()
_ensure_world_wmo_root()
if enable_wmo:
_place_global_wmo()
func _reset_terrain_root_children() -> void:
@@ -1121,16 +1187,110 @@ func _tile_mesh_cache_key(tile_key: String, lod: int) -> String:
return "%s|%d" % [tile_key, lod]
func _build_tile_wmos(tile_root: Node3D, tile_origin: Vector3, wmo_names: PackedStringArray, wmo_placements: Array) -> void:
if wmo_names.is_empty() or wmo_placements.is_empty():
func _ensure_world_wmo_root() -> void:
if _world_wmo_root != null and is_instance_valid(_world_wmo_root):
return
_world_wmo_root = Node3D.new()
_world_wmo_root.name = "WMOs"
_terrain_root.add_child(_world_wmo_root)
_set_editor_owner_recursive(_world_wmo_root)
func _wdt_has_global_wmo() -> bool:
return bool(_wdt_info.get("has_global_wmo", false)) and _wdt_info.has("global_wmo_name")
func _place_global_wmo() -> void:
if not _wdt_has_global_wmo():
return
if _wmo_registry.has("global"):
return
var wmo_root := Node3D.new()
wmo_root.name = "WMOs"
tile_root.add_child(wmo_root)
_set_editor_owner_recursive(wmo_root)
_ensure_world_wmo_root()
for placement_variant in wmo_placements:
var rel_path := String(_wdt_info.get("global_wmo_name", "")).replace("\\", "/")
if rel_path.is_empty():
return
var placement: Dictionary = _wdt_info.get("global_wmo_placement", {})
if placement.is_empty():
placement = {
"pos": Vector3.ZERO,
"rot": Vector3.ZERO,
"scale": 1.0,
}
var instance := _instantiate_wmo_world(rel_path, placement)
if instance == null:
push_warning("StreamingWorld: failed to load global WMO: %s" % rel_path)
return
_world_wmo_root.add_child(instance)
_apply_visibility_range_recursive(instance, wmo_visibility_range)
_set_editor_owner_recursive(instance)
_wmo_registry["global"] = {
"node": instance,
"refs": {"__global__": true},
}
func _sync_detail_assets(state: Dictionary, focus_pos: Vector3) -> Dictionary:
if not bool(state.get("wanted", false)):
_disable_tile_detail_assets(state)
return state
var tile_root: Node = state.get("root", null)
if tile_root == null or not is_instance_valid(tile_root):
return state
if enable_wmo and _tile_within_detail_radius(state, focus_pos, wmo_tile_radius):
var refs: Array = state.get("wmo_refs", [])
if refs.is_empty():
_register_tile_wmos(
state,
state.get("wmo_names", PackedStringArray()),
state.get("wmo_placements", []))
else:
_unregister_tile_wmos(state)
if enable_m2_assets and _tile_within_detail_radius(state, focus_pos, m2_tile_radius):
if not bool(state.get("m2_built", false)):
state["m2_built"] = _build_tile_m2_assets(
tile_root as Node3D,
state.get("origin", Vector3.ZERO),
state.get("m2_names", PackedStringArray()),
state.get("m2_placements", []))
else:
_remove_tile_m2_assets(state)
state["m2_built"] = false
return state
func _disable_tile_detail_assets(state: Dictionary) -> void:
_unregister_tile_wmos(state)
_remove_tile_m2_assets(state)
state["m2_built"] = false
func _tile_within_detail_radius(state: Dictionary, focus_pos: Vector3, radius_tiles: int) -> bool:
if radius_tiles < 0:
return true
var radius_sq := (float(radius_tiles) * TILE_SIZE) ** 2
return _tile_dist_sq(state, focus_pos) <= radius_sq
func _register_tile_wmos(state: Dictionary, wmo_names: PackedStringArray, wmo_placements: Array) -> void:
var refs: Array = state.get("wmo_refs", [])
if wmo_names.is_empty() or wmo_placements.is_empty():
state["wmo_refs"] = refs
return
_ensure_world_wmo_root()
var tile_key: String = state["key"]
for i in wmo_placements.size():
var placement_variant = wmo_placements[i]
if not (placement_variant is Dictionary):
continue
var placement: Dictionary = placement_variant
@@ -1138,17 +1298,83 @@ func _build_tile_wmos(tile_root: Node3D, tile_origin: Vector3, wmo_names: Packed
if name_id < 0 or name_id >= wmo_names.size():
continue
var unique_key := _wmo_unique_key(placement, tile_key, i)
if _wmo_registry.has(unique_key):
var entry: Dictionary = _wmo_registry[unique_key]
var entry_refs: Dictionary = entry["refs"]
entry_refs[tile_key] = true
refs.append(unique_key)
continue
var rel_path: String = str(wmo_names[name_id]).replace("\\", "/")
var instance := _instantiate_wmo(rel_path, placement, tile_origin)
var instance := _instantiate_wmo_world(rel_path, placement)
if instance == null:
continue
wmo_root.add_child(instance)
_world_wmo_root.add_child(instance)
_apply_visibility_range_recursive(instance, wmo_visibility_range)
_set_editor_owner_recursive(instance)
_wmo_registry[unique_key] = {
"node": instance,
"refs": {tile_key: true},
}
refs.append(unique_key)
state["wmo_refs"] = refs
func _build_tile_m2_assets(tile_root: Node3D, tile_origin: Vector3, m2_names: PackedStringArray, m2_placements: Array) -> void:
if m2_names.is_empty() or m2_placements.is_empty():
func _unregister_tile_wmos(state: Dictionary) -> void:
var refs: Array = state.get("wmo_refs", [])
if refs.is_empty():
return
var tile_key: String = state["key"]
for unique_key in refs:
if not _wmo_registry.has(unique_key):
continue
var entry: Dictionary = _wmo_registry[unique_key]
var entry_refs: Dictionary = entry["refs"]
entry_refs.erase(tile_key)
if entry_refs.is_empty():
var node: Node = entry.get("node", null)
if node:
node.queue_free()
_wmo_registry.erase(unique_key)
state["wmo_refs"] = []
func _wmo_unique_key(placement: Dictionary, tile_key: String, idx: int) -> String:
var uid: int = int(placement.get("unique_id", -1))
# uniqueId is always set in vanilla/3.3.5a MODF data, but older baked caches
# predating the ADTLoader unique_id field will miss it. Fall back to a
# per-tile synthetic key — no cross-tile dedup, but also no collisions.
if uid > 0:
return "uid:%d" % uid
return "tile:%s:%d" % [tile_key, idx]
func _clear_wmo_registry() -> void:
for entry_variant in _wmo_registry.values():
if not (entry_variant is Dictionary):
continue
var entry: Dictionary = entry_variant
var node: Node = entry.get("node", null)
if node and is_instance_valid(node):
node.queue_free()
_wmo_registry.clear()
if _world_wmo_root and is_instance_valid(_world_wmo_root):
_world_wmo_root.queue_free()
_world_wmo_root = null
func _build_tile_m2_assets(tile_root: Node3D, tile_origin: Vector3, m2_names: PackedStringArray, m2_placements: Array) -> bool:
if m2_names.is_empty() or m2_placements.is_empty():
return false
var existing_m2_root := tile_root.get_node_or_null("M2s")
if existing_m2_root != null:
return not existing_m2_root.is_queued_for_deletion()
var groups: Dictionary = {}
for placement_variant in m2_placements:
@@ -1169,14 +1395,15 @@ func _build_tile_m2_assets(tile_root: Node3D, tile_origin: Vector3, m2_names: Pa
var pos: Vector3 = placement.get("pos", Vector3.ZERO) - tile_origin
var rot: Vector3 = placement.get("rot", Vector3.ZERO)
var scale_value: float = float(placement.get("scale", 1.0))
var xform := Transform3D(Basis.from_euler(rot).scaled(Vector3.ONE * maxf(scale_value, 0.0001)), pos)
var basis := Basis.from_euler(rot) * Basis(Vector3.UP, M2_RIGHT_YAW_OFFSET)
var xform := Transform3D(basis.scaled(Vector3.ONE * maxf(scale_value, 0.0001)), pos)
if not groups.has(normalized):
groups[normalized] = []
(groups[normalized] as Array).append(xform)
if groups.is_empty():
return
return false
var m2_root := Node3D.new()
m2_root.name = "M2s"
@@ -1200,15 +1427,40 @@ func _build_tile_m2_assets(tile_root: Node3D, tile_origin: Vector3, m2_names: Pa
mmi.name = rel_path.get_file().get_basename()
mmi.multimesh = mm
mmi.cast_shadow = (
GeometryInstance3D.SHADOW_CASTING_SETTING_ON
if m2_cast_shadows
else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
GeometryInstance3D.SHADOW_CASTING_SETTING_ON
if m2_cast_shadows
else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
)
if m2_visibility_range > 0.0:
mmi.visibility_range_end = m2_visibility_range
mmi.visibility_range_end_margin = CHUNK_SIZE
m2_root.add_child(mmi)
_set_editor_owner_recursive(mmi)
if m2_root.get_child_count() == 0:
m2_root.queue_free()
return false
return true
func _remove_tile_m2_assets(state: Dictionary) -> void:
var tile_root: Node = state.get("root", null)
if tile_root == null or not is_instance_valid(tile_root):
return
var m2_root := tile_root.get_node_or_null("M2s")
if m2_root:
m2_root.queue_free()
func _apply_visibility_range_recursive(node: Node, range_end: float) -> void:
if range_end <= 0.0:
return
if node is GeometryInstance3D:
var geometry := node as GeometryInstance3D
geometry.visibility_range_end = range_end
geometry.visibility_range_end_margin = CHUNK_SIZE
for child in node.get_children():
_apply_visibility_range_recursive(child, range_end)
func _get_or_load_m2_mesh(rel_path: String) -> Mesh:
@@ -1317,23 +1569,23 @@ func _instantiate_m2(rel_path: String, placement: Dictionary, tile_origin: Vecto
node.name = rel_path.get_file().get_basename()
node.position = placement.get("pos", Vector3.ZERO) - tile_origin
node.rotation = placement.get("rot", Vector3.ZERO)
node.rotation = placement.get("rot", Vector3.ZERO) + Vector3(0.0, M2_RIGHT_YAW_OFFSET, 0.0)
var scale_value: float = float(placement.get("scale", 1.0))
node.scale = Vector3.ONE * scale_value
return node
func _instantiate_wmo(rel_path: String, placement: Dictionary, tile_origin: Vector3) -> Node3D:
func _instantiate_wmo_world(rel_path: String, placement: Dictionary) -> Node3D:
var prototype := _get_or_load_wmo_prototype(rel_path)
if prototype == null:
return null
var instance := prototype.duplicate()
instance.name = rel_path.get_file().get_basename()
instance.position = placement.get("pos", Vector3.ZERO) - tile_origin
var rot: Vector3 = placement.get("rot", Vector3.ZERO)
rot.y += PI
instance.rotation = rot
# WMOs are parented to _terrain_root, whose position absorbs the editor
# offset — use world-space placement.pos directly (no tile_origin subtraction).
instance.position = placement.get("pos", Vector3.ZERO)
instance.rotation = placement.get("rot", Vector3.ZERO)
var scale_value: float = float(placement.get("scale", 1.0))
instance.scale = Vector3.ONE * scale_value
return instance
+243 -2
View File
@@ -1,11 +1,29 @@
extends SceneTree
## Usage:
## godot --headless --path <project> --script res://src/tools/bake_adt_terrain_cache.gd -- \
## --map Azeroth --jobs 4 --force
##
## `--jobs N` starts N separate Godot worker processes. Process isolation keeps
## ResourceSaver/Image/loader state independent while using multiple CPU cores.
const ADT_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/adt_builder.gd")
const BAKED_TILE_SCRIPT := preload("res://src/resources/baked_adt_tile.gd")
const PROGRESS_DIR := "res://data/cache/bake_progress"
const PROGRESS_REPORT_INTERVAL_MS := 2000
var _builder
var _loader
var _image_cache: Dictionary = {}
var _progress_path := ""
var _progress_worker_index := -1
var _progress_worker_count := 1
var _progress_total := 0
var _progress_processed := 0
var _progress_baked := 0
var _progress_skipped := 0
var _progress_failed := 0
var _progress_last_tile := ""
func _initialize() -> void:
@@ -13,13 +31,13 @@ func _initialize() -> void:
var map_name := _get_arg_value(args, "--map", "Azeroth")
var extracted_dir := _normalize_res_path(_get_arg_value(args, "--extracted", "res://data/extracted"))
var output_dir := _normalize_res_path(_get_arg_value(args, "--output", "res://data/cache/baked_terrain_v2"))
var legacy_texture_size = _get_optional_int_arg(args, "--texture-size")
var legacy_texture_size: Variant = _get_optional_int_arg(args, "--texture-size")
var full_texture_size := maxi(
256,
_get_arg_value(
args,
"--full-texture-size",
str(legacy_texture_size if legacy_texture_size != null else 2048)
str(legacy_texture_size if legacy_texture_size != null else 4196)
).to_int()
)
var coarse_texture_size := maxi(
@@ -32,8 +50,15 @@ func _initialize() -> void:
)
var tile_x: Variant = _get_optional_int_arg(args, "--tile-x")
var tile_y: Variant = _get_optional_int_arg(args, "--tile-y")
var jobs := maxi(1, _get_arg_value(args, "--jobs", "1").to_int())
var worker_index: Variant = _get_optional_int_arg(args, "--worker-index")
var worker_count := maxi(1, _get_arg_value(args, "--worker-count", "1").to_int())
var force := args.has("--force")
if jobs > 1 and worker_index == null:
_run_parallel_controller(args, jobs)
return
_builder = ADT_BUILDER_SCRIPT.new()
if not ClassDB.class_exists("ADTLoader"):
push_error("ADTLoader not found. Rebuild GDExtension first.")
@@ -59,6 +84,9 @@ func _initialize() -> void:
var skipped := 0
var failed := 0
var started_ms := Time.get_ticks_msec()
var tile_index := 0
var worker_total := _count_worker_tiles(dir.get_files(), map_name, tile_x, tile_y, worker_index, worker_count)
_progress_setup(worker_index, worker_count, worker_total)
for file_name in dir.get_files():
if not file_name.ends_with(".adt"):
@@ -77,6 +105,11 @@ func _initialize() -> void:
continue
if tile_y != null and ty != tile_y:
continue
if worker_index != null:
if tile_index % worker_count != int(worker_index):
tile_index += 1
continue
tile_index += 1
var source_res_path := map_dir.path_join(file_name)
var source_abs_path := ProjectSettings.globalize_path(source_res_path)
var output_res_path := output_dir.path_join(map_name).path_join("%s_%d_%d.res" % [map_name, tx, ty])
@@ -84,12 +117,14 @@ func _initialize() -> void:
if not force and FileAccess.file_exists(output_abs_path):
skipped += 1
_progress_update(tx, ty, baked, skipped, failed)
continue
var data: Dictionary = _loader.call("load_adt", source_abs_path)
if data.is_empty() or not data.has("chunks"):
push_warning("Bake failed to parse ADT: %s" % source_res_path)
failed += 1
_progress_update(tx, ty, baked, skipped, failed)
continue
var full_payload: Dictionary = _builder.build_baked_tile_render_payload(
@@ -108,6 +143,7 @@ func _initialize() -> void:
if full_payload.is_empty():
push_warning("Bake produced empty full mesh: %s" % source_res_path)
failed += 1
_progress_update(tx, ty, baked, skipped, failed)
continue
var baked_tile: Resource = BAKED_TILE_SCRIPT.new()
@@ -129,18 +165,223 @@ func _initialize() -> void:
if save_err != OK:
push_warning("Failed to save baked tile: %s (err=%d)" % [output_res_path, save_err])
failed += 1
_progress_update(tx, ty, baked, skipped, failed)
continue
baked += 1
_progress_update(tx, ty, baked, skipped, failed)
if baked % 25 == 0:
print("Baked %d tiles..." % baked)
var elapsed_ms := Time.get_ticks_msec() - started_ms
print("Bake finished. baked=%d skipped=%d failed=%d elapsed=%.2fs" % [
baked, skipped, failed, float(elapsed_ms) / 1000.0])
_progress_finish(baked, skipped, failed)
quit(0 if failed == 0 else 2)
func _run_parallel_controller(args: PackedStringArray, jobs: int) -> void:
var started_ms := Time.get_ticks_msec()
var last_report_ms := 0
var threads: Array[Thread] = []
_prepare_progress_dir()
print("Starting parallel ADT bake: jobs=%d" % jobs)
for worker_index in jobs:
var thread := Thread.new()
var err := thread.start(_run_worker_process.bind(args, worker_index, jobs))
if err != OK:
push_error("Failed to start bake worker thread %d (err=%d)" % [worker_index, err])
quit(1)
return
threads.append(thread)
var finished := false
while not finished:
OS.delay_msec(250)
var now := Time.get_ticks_msec()
if now - last_report_ms >= PROGRESS_REPORT_INTERVAL_MS:
_print_parallel_progress(jobs, started_ms)
last_report_ms = now
finished = true
for thread in threads:
if thread.is_alive():
finished = false
break
var failed := 0
for thread in threads:
var exit_code: int = int(thread.wait_to_finish())
if exit_code != 0:
failed += 1
var elapsed_ms := Time.get_ticks_msec() - started_ms
_print_parallel_progress(jobs, started_ms)
print("Parallel bake finished. jobs=%d failed_workers=%d elapsed=%.2fs" % [
jobs, failed, float(elapsed_ms) / 1000.0])
quit(0 if failed == 0 else 2)
func _run_worker_process(args: PackedStringArray, worker_index: int, worker_count: int) -> int:
var worker_args := PackedStringArray([
"--headless",
"--path",
ProjectSettings.globalize_path("res://"),
"--script",
"res://src/tools/bake_adt_terrain_cache.gd",
"--",
])
var skip_next := false
for i in args.size():
if skip_next:
skip_next = false
continue
var arg := args[i]
if arg == "--jobs":
skip_next = i + 1 < args.size()
continue
if arg == "--worker-index" or arg == "--worker-count":
skip_next = i + 1 < args.size()
continue
worker_args.append(arg)
worker_args.append("--jobs")
worker_args.append("1")
worker_args.append("--worker-index")
worker_args.append(str(worker_index))
worker_args.append("--worker-count")
worker_args.append(str(worker_count))
print("Worker %d/%d started" % [worker_index + 1, worker_count])
var exit_code := OS.execute(OS.get_executable_path(), worker_args)
print("Worker %d/%d finished with exit=%d" % [worker_index + 1, worker_count, exit_code])
return exit_code
func _prepare_progress_dir() -> void:
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(PROGRESS_DIR))
var dir := DirAccess.open(PROGRESS_DIR)
if dir == null:
return
for file_name in dir.get_files():
if file_name.begins_with("adt_worker_") and file_name.ends_with(".json"):
dir.remove(file_name)
func _progress_setup(worker_index: Variant, worker_count: int, total: int) -> void:
_progress_worker_index = int(worker_index) if worker_index != null else 0
_progress_worker_count = worker_count
_progress_total = total
_progress_processed = 0
_progress_baked = 0
_progress_skipped = 0
_progress_failed = 0
_progress_last_tile = ""
_progress_path = PROGRESS_DIR.path_join("adt_worker_%d.json" % _progress_worker_index)
_progress_write(false)
func _progress_update(tx: int, ty: int, baked: int, skipped: int, failed: int) -> void:
_progress_processed += 1
_progress_baked = baked
_progress_skipped = skipped
_progress_failed = failed
_progress_last_tile = "%d_%d" % [tx, ty]
_progress_write(false)
func _progress_finish(baked: int, skipped: int, failed: int) -> void:
_progress_baked = baked
_progress_skipped = skipped
_progress_failed = failed
_progress_processed = _progress_total
_progress_write(true)
func _progress_write(done: bool) -> void:
if _progress_path.is_empty():
return
var file := FileAccess.open(_progress_path, FileAccess.WRITE)
if file == null:
return
file.store_string(JSON.stringify({
"worker": _progress_worker_index,
"workers": _progress_worker_count,
"total": _progress_total,
"processed": _progress_processed,
"baked": _progress_baked,
"skipped": _progress_skipped,
"failed": _progress_failed,
"last_tile": _progress_last_tile,
"done": done,
}))
func _print_parallel_progress(jobs: int, started_ms: int) -> void:
var now := Time.get_ticks_msec()
var total := 0
var processed := 0
var baked := 0
var skipped := 0
var failed := 0
var done := 0
for worker_index in jobs:
var path := PROGRESS_DIR.path_join("adt_worker_%d.json" % worker_index)
if not FileAccess.file_exists(path):
continue
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
continue
var parsed: Variant = JSON.parse_string(file.get_as_text())
if not (parsed is Dictionary):
continue
var row: Dictionary = parsed
total += int(row.get("total", 0))
processed += int(row.get("processed", 0))
baked += int(row.get("baked", 0))
skipped += int(row.get("skipped", 0))
failed += int(row.get("failed", 0))
if bool(row.get("done", false)):
done += 1
var elapsed := maxf(0.001, float(now - started_ms) / 1000.0)
var rate := float(processed) / elapsed
print("Progress: %d/%d tiles, baked=%d skipped=%d failed=%d, workers done=%d/%d, %.2f tiles/s" % [
processed, total, baked, skipped, failed, done, jobs, rate])
func _count_worker_tiles(
files: PackedStringArray,
map_name: String,
tile_x: Variant,
tile_y: Variant,
worker_index: Variant,
worker_count: int) -> int:
var count := 0
var tile_index := 0
for file_name in files:
if not file_name.ends_with(".adt"):
continue
var parts := file_name.trim_suffix(".adt").split("_")
if parts.size() != 3 or parts[0] != map_name:
continue
if not parts[1].is_valid_int() or not parts[2].is_valid_int():
continue
var tx := parts[1].to_int()
var ty := parts[2].to_int()
if tile_x != null and tx != tile_x:
continue
if tile_y != null and ty != tile_y:
continue
if worker_index != null and tile_index % worker_count != int(worker_index):
tile_index += 1
continue
tile_index += 1
count += 1
return count
func _get_arg_value(args: PackedStringArray, name: String, default_value: String) -> String:
var index := args.find(name)
if index >= 0 and index + 1 < args.size():
+1
View File
@@ -81,6 +81,7 @@ func _initialize() -> void:
var elapsed := float(Time.get_ticks_msec() - total_start) / 1000.0
print("\nAll done in %.1fs." % elapsed)
WMO_BUILDER_SCRIPT.clear_caches()
quit(0)
+1
View File
@@ -104,6 +104,7 @@ func _initialize() -> void:
print("[%d/%d] baked=%d skipped=%d failed=%d" % [i, total, baked, skipped, failed])
print("Done. baked=%d skipped=%d failed=%d" % [baked, skipped, failed])
WMO_BUILDER_SCRIPT.clear_caches()
quit(0)