render map objects: decode compiled W3DMesh art, skin and draw over terrain

Read the buildings and props a map places from its compiled art (the
uncompressed worldbuilder stream + embedded DDS textures):

- ra3.models: BAB asset-stream parser (lazy slices), W3DMesh + D3DHierarchy
  decode, DDS (DXT1/3/5 + uncompressed) decode, per-mesh bone remap and
  bind-pose single-joint skinning, flattened to one world-space triangle soup.
- ra3.map: parse the ObjectsList chunk into (type, x, y, z, angle).
- ra3.terrain: render3d rasterises the scene over the raymarched terrain with
  a z-buffer; gpu_terrain carries the scene for the GPU backends.
- ra3.vulkan: second pipeline + depth attachment, terrain.frag writes
  gl_FragDepth, and a small depth bias keeps ground decals from z-fighting.
- objects.fx shaders (compiled to SPIR-V), embedded like scene/terrain.

Only opaque parts are drawn: FX-light billboards (DefaultW3D.fx / BasicW3D.fx)
and damage-fill shells (BuildingsGenericDamageFill.fx) are skipped, since the
latter paint the wrecked interior (e.g. orange CBBuilding_Wood) over the shell.

Ground-decal meshes with no diffuse role and the Road templates themselves are
still not drawn.
This commit is contained in:
EnderTheCoder
2026-09-29 18:08:11 +08:00
parent 6e5df350c7
commit 70f35d8382
18 changed files with 2027 additions and 45 deletions
+76 -2
View File
@@ -200,6 +200,79 @@ namespace {
return std::nullopt;
}
/** A map's compiled art stream (`map.manifest` + `map.bin`). */
struct model_stream_paths {
std::filesystem::path manifest;
std::filesystem::path bin;
};
/**
* Locate the compiled art stream that holds a map's buildings/props.
*
* Retail bakes the shared static art (the props a map places) into
* `Data\WBData.big`'s uncompressed `worldbuilder.bin`, so that stream is
* preferred; the per-map `map.bin` (which links to it) is the fallback.
*/
auto find_map_stream(const std::filesystem::path &root, std::string_view id) -> std::optional<model_stream_paths> {
std::error_code ec;
const auto consider = [&](const std::filesystem::path &dir) -> std::optional<model_stream_paths> {
const auto manifest = dir / "map.manifest";
const auto bin = dir / "map.bin";
if (std::filesystem::exists(manifest, ec) && std::filesystem::exists(bin, ec)) return model_stream_paths{manifest, bin};
return std::nullopt;
};
// A `worldbuilder` stream (the full prop art) takes precedence.
for (const auto &dir: {root / "models", root / "raw" / "WBData" / "data", root}) {
const auto manifest = dir / "worldbuilder.manifest";
const auto bin = dir / "worldbuilder.bin";
if (std::filesystem::exists(manifest, ec) && std::filesystem::exists(bin, ec)) return model_stream_paths{manifest, bin};
}
for (const auto &entry: std::filesystem::recursive_directory_iterator(root, ec)) {
if (entry.is_regular_file() && entry.path().filename() == "worldbuilder.bin") {
const auto manifest = entry.path().parent_path() / "worldbuilder.manifest";
if (std::filesystem::exists(manifest, ec)) return model_stream_paths{manifest, entry.path()};
}
}
if (auto found = consider(root / "models" / id); found) return found;
if (auto found = consider(root / "maps" / id); found) return found;
for (const auto &entry: std::filesystem::recursive_directory_iterator(root, ec)) {
if (!entry.is_directory() || entry.path().filename().string() != id) continue;
if (auto found = consider(entry.path()); found) return found;
}
return std::nullopt;
}
/** Build the map's building/prop scene from its compiled art stream. */
auto build_object_scene(const std::filesystem::path &assets, std::string_view id, const ra3::terrain::map_data &terrain,
const ra3::terrain::render_options &options, std::span<const ra3::core::uint8> ckmp) -> ra3::models::scene {
ra3::models::scene scene;
try {
const auto paths = find_map_stream(assets, id);
if (!paths) {
std::puts("objects: no compiled art stream found");
return scene;
}
const auto stream = ra3::models::asset_stream::load_files(paths->manifest, paths->bin);
std::vector<ra3::models::placement> placements;
for (const auto &object: ra3::map::parse_objects(ckmp)) {
placements.push_back({object.type, object.x, object.y, object.z, object.angle, object.scale});
}
const auto world_w = terrain.world_width();
const auto world_h = terrain.world_height();
scene = ra3::models::build_scene(stream, placements, [&](float x, float y) -> float {
if (x < 0.0F || y < 0.0F || x >= world_w || y >= world_h) return 0.0F;
const auto cx = std::min(terrain.width - 1U, static_cast<ra3::core::uint32>(x / ra3::terrain::cell_size));
const auto cy = std::min(terrain.height - 1U, static_cast<ra3::core::uint32>((world_h - y) / ra3::terrain::cell_size));
return static_cast<float>(terrain.elevation(cx, cy)) * options.z_scale;
});
std::printf("objects: %zu placed, %zu missing, %zu triangles, %zu textures (%s)\n", scene.placed, scene.missing, scene.triangle_count(),
scene.textures.size(), paths->bin.filename().string().c_str());
} catch (const std::exception &error) {
std::printf("objects: failed to build scene (%s)\n", error.what());
}
return scene;
}
/** Write `<assets>/maps/map_names.tsv` from the install's localized string table. */
auto write_map_names(const std::filesystem::path &data_dir, const std::filesystem::path &assets) -> bool {
try {
@@ -465,8 +538,9 @@ namespace {
const bool offscreen = option_value(args, "--out").has_value() || has_flag(args, "--no-window");
bool gpu_shown = false;
const auto objects = build_object_scene(assets, picked->id, terrain, terrain_options, bytes);
if (!offscreen) {
const auto gpu = terrain::build_gpu_terrain(terrain, textures, terrain_options);
const auto gpu = terrain::build_gpu_terrain(terrain, textures, terrain_options, objects);
ra3::client::display_options options;
options.title = scene.title;
options.width = static_cast<int>(width);
@@ -486,7 +560,7 @@ namespace {
}
if (offscreen) {
// Offscreen: the software raymarcher produces the 3D image.
composed = terrain::render3d(terrain, textures, camera, width, height, terrain_options);
composed = terrain::render3d(terrain, textures, camera, width, height, terrain_options, &objects);
perspective = true;
} else {
// Windowed without a GPU: the cheap top-down raster.