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
+78
View File
@@ -222,6 +222,84 @@ auto main() -> int {
check(names.lookup("map_mp_2_feasel4") == "Battlebase Beta", "CSF map names decode (byte-XOR 0xFF)");
check(names.lookup("unknown_map") == "unknown_map", "an unknown id falls back to itself");
// Map objects (`ObjectsList`): decode a synthetic one-object chunk.
const auto put16 = [](std::vector<core::uint8> &out, std::uint16_t v) {
out.push_back(static_cast<core::uint8>(v));
out.push_back(static_cast<core::uint8>(v >> 8U));
};
const auto put32 = [](std::vector<core::uint8> &out, std::uint32_t v) {
for (int i = 0; i < 4; ++i) out.push_back(static_cast<core::uint8>(v >> (8 * i)));
};
const auto putf = [&put32](std::vector<core::uint8> &out, float f) {
std::uint32_t bits = 0;
std::memcpy(&bits, &f, sizeof(bits));
put32(out, bits);
};
std::vector<core::uint8> object_data;
putf(object_data, 100.0F);
putf(object_data, 200.0F);
putf(object_data, 0.0F);
putf(object_data, 0.5F);
put32(object_data, 0U); // road type
const std::string object_type = "BB_TEST";
put16(object_data, static_cast<std::uint16_t>(object_type.size()));
object_data.insert(object_data.end(), object_type.begin(), object_type.end());
put16(object_data, 0U); // no properties
std::vector<core::uint8> object_asset;
put32(object_asset, 1U); // asset index
put16(object_asset, 1U); // version
put32(object_asset, static_cast<std::uint32_t>(object_data.size()));
object_asset.insert(object_asset.end(), object_data.begin(), object_data.end());
std::vector<core::uint8> ckmp;
ckmp.insert(ckmp.end(), {'C', 'k', 'M', 'p'});
put32(ckmp, 1U); // one asset name
const std::string chunk_name = "ObjectsList";
ckmp.push_back(static_cast<core::uint8>(chunk_name.size()));
ckmp.insert(ckmp.end(), chunk_name.begin(), chunk_name.end());
put32(ckmp, 1U); // name index
put32(ckmp, 1U); // chunk index
put16(ckmp, 3U); // chunk version
put32(ckmp, static_cast<std::uint32_t>(object_asset.size()));
ckmp.insert(ckmp.end(), object_asset.begin(), object_asset.end());
const auto objects = map::parse_objects(ckmp);
check(objects.size() == 1U, "ObjectsList parses one object");
check(!objects.empty() && objects[0].type == "BB_TEST", "object type-name decodes");
check(!objects.empty() && objects[0].x == 100.0F && objects[0].y == 200.0F && objects[0].angle == 0.5F, "object position/angle decode");
// DDS: a 1x1 uncompressed RGB32 image decodes with correct channels.
std::vector<core::uint8> dds(128U, 0U);
dds[0] = 'D';
dds[1] = 'D';
dds[2] = 'S';
dds[3] = ' ';
const auto put_dds = [&dds](std::size_t off, std::uint32_t v) {
for (int i = 0; i < 4; ++i) dds[off + static_cast<std::size_t>(i)] = static_cast<core::uint8>(v >> (8 * i));
};
put_dds(4U, 124U);
put_dds(8U, 0x1U); // flags: DDSD_CAPS|... (unused)
put_dds(12U, 1U); // height
put_dds(16U, 1U); // width
put_dds(76U, 32U); // pixel format size
put_dds(80U, 0x40U); // DDPF_RGB
put_dds(84U, 0U); // no fourcc
put_dds(88U, 32U); // bits per pixel
put_dds(92U, 0x00FF0000U);
put_dds(96U, 0x0000FF00U);
put_dds(100U, 0x000000FFU);
put_dds(104U, 0xFF000000U);
dds.push_back(0x00U); // B
dds.push_back(0x00U); // G
dds.push_back(0xFFU); // R
dds.push_back(0xFFU); // A
const auto decoded_dds = models::decode_dds(dds);
check(decoded_dds.width() == 1U && decoded_dds.height() == 1U, "DDS dimensions decode");
check(!decoded_dds.empty() && decoded_dds.data()[0] == render::argb(255, 0, 0), "DDS RGB32 channels map to ARGB");
check(models::decode_dds(std::vector<core::uint8>{1U, 2U, 3U}).empty(), "a non-DDS payload yields no image");
if (failures == 0) {
std::puts("ra3_tests: OK");
}