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:
+76
-2
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user