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:
+15
-2
@@ -255,6 +255,15 @@ target_sources(ra3_terrain PUBLIC FILE_SET CXX_MODULES FILES src/terrain/ra3.ter
|
|||||||
target_link_libraries(ra3_terrain PUBLIC ra3_core ra3_fs ra3_render)
|
target_link_libraries(ra3_terrain PUBLIC ra3_core ra3_fs ra3_render)
|
||||||
openra3_target_defaults(ra3_terrain)
|
openra3_target_defaults(ra3_terrain)
|
||||||
|
|
||||||
|
# --- map static art (compiled W3D meshes) ------------------------------------
|
||||||
|
add_library(ra3_models STATIC)
|
||||||
|
target_sources(ra3_models PUBLIC FILE_SET CXX_MODULES FILES src/models/ra3.models.cppm)
|
||||||
|
target_link_libraries(ra3_models PUBLIC ra3_core ra3_fs ra3_render)
|
||||||
|
openra3_target_defaults(ra3_models)
|
||||||
|
|
||||||
|
# A map's terrain carries the objects placed on it (props/buildings).
|
||||||
|
target_link_libraries(ra3_terrain PUBLIC ra3_models)
|
||||||
|
|
||||||
# The presentation facade imports render + terrain (for the terrain capability).
|
# The presentation facade imports render + terrain (for the terrain capability).
|
||||||
target_link_libraries(ra3_client PUBLIC ra3_render ra3_terrain)
|
target_link_libraries(ra3_client PUBLIC ra3_render ra3_terrain)
|
||||||
|
|
||||||
@@ -278,13 +287,17 @@ if(OPENRA3_HAS_VULKAN AND OPENRA3_HAS_SDL3)
|
|||||||
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/generated/scene.frag.spv"
|
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/generated/scene.frag.spv"
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/generated/terrain.vert.spv"
|
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/generated/terrain.vert.spv"
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/generated/terrain.frag.spv"
|
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/generated/terrain.frag.spv"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/generated/object.vert.spv"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/generated/object.frag.spv"
|
||||||
)
|
)
|
||||||
# The blobs are read at configure time, so re-run CMake when they change.
|
# The blobs are read at configure time, so re-run CMake when they change.
|
||||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
|
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/generated/scene.vert.spv"
|
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/generated/scene.vert.spv"
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/generated/scene.frag.spv"
|
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/generated/scene.frag.spv"
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/generated/terrain.vert.spv"
|
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/generated/terrain.vert.spv"
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/generated/terrain.frag.spv")
|
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/generated/terrain.frag.spv"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/generated/object.vert.spv"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/generated/object.frag.spv")
|
||||||
target_sources(ra3_vulkan PUBLIC FILE_SET CXX_MODULES FILES src/vulkan/ra3.vulkan.sdl.cppm)
|
target_sources(ra3_vulkan PUBLIC FILE_SET CXX_MODULES FILES src/vulkan/ra3.vulkan.sdl.cppm)
|
||||||
target_include_directories(ra3_vulkan PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/generated")
|
target_include_directories(ra3_vulkan PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/generated")
|
||||||
target_link_libraries(ra3_vulkan PUBLIC ra3_core ra3_render ra3_terrain ra3_client openra3_sdl3 ra3_volk)
|
target_link_libraries(ra3_vulkan PUBLIC ra3_core ra3_render ra3_terrain ra3_client openra3_sdl3 ra3_volk)
|
||||||
@@ -410,7 +423,7 @@ openra3_target_defaults(ra3_display)
|
|||||||
# --- umbrella -----------------------------------------------------------------
|
# --- umbrella -----------------------------------------------------------------
|
||||||
add_library(ra3 STATIC)
|
add_library(ra3 STATIC)
|
||||||
target_sources(ra3 PUBLIC FILE_SET CXX_MODULES FILES src/ra3.cppm)
|
target_sources(ra3 PUBLIC FILE_SET CXX_MODULES FILES src/ra3.cppm)
|
||||||
target_link_libraries(ra3 PUBLIC ra3_core ra3_logic ra3_client ra3_data ra3_game ra3_fs ra3_map ra3_skirmish ra3_render ra3_terrain ra3_display
|
target_link_libraries(ra3 PUBLIC ra3_core ra3_logic ra3_client ra3_data ra3_game ra3_fs ra3_map ra3_skirmish ra3_render ra3_terrain ra3_models ra3_display
|
||||||
ra3_ui ra3_vulkan ra3_dx ra3_wasmgl ra3_webgpu ra3_enderlog)
|
ra3_ui ra3_vulkan ra3_dx ra3_wasmgl ra3_webgpu ra3_enderlog)
|
||||||
openra3_target_defaults(ra3)
|
openra3_target_defaults(ra3)
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ Ghidra.
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
OpenRA3 is at **v0.6.0**. The engine compiles and runs headless, and a **minimal
|
OpenRA3 is at **v0.7.0**. The engine compiles and runs headless, and a **minimal
|
||||||
skirmish** is playable: it reads a real multiplayer map out of your install,
|
skirmish** is playable: it reads a real multiplayer map out of your install,
|
||||||
recovers the player start waypoints, and simulates two sides building a base,
|
recovers the player start waypoints, and simulates two sides building a base,
|
||||||
extracting ore and fighting until one side is wiped out. The balance is the
|
extracting ore and fighting until one side is wiped out. The balance is the
|
||||||
@@ -31,7 +31,10 @@ software blit and null fallbacks; the backend is selectable from the in-game men
|
|||||||
and the software renderer still produces headless images. Terrain tiles
|
and the software renderer still produces headless images. Terrain tiles
|
||||||
cross-fade the way the retail `Terrain.fx` does (a per-cell blend ramp plus a
|
cross-fade the way the retail `Terrain.fx` does (a per-cell blend ramp plus a
|
||||||
gutter-padded atlas), so material boundaries are smooth instead of a grid of
|
gutter-padded atlas), so material boundaries are smooth instead of a grid of
|
||||||
hard lines. An in-window **menu** lists the maps by their localized name and
|
hard lines; the **buildings and props a map places** are decoded from the retail
|
||||||
|
compiled `W3DMesh` art (the uncompressed `worldbuilder.bin` stream and its
|
||||||
|
embedded DDS textures) and drawn with a depth test over the terrain. An
|
||||||
|
in-window **menu** lists the maps by their localized name and
|
||||||
exposes every render/skirmish option for tweaking before launch. The whole tree
|
exposes every render/skirmish option for tweaking before launch. The whole tree
|
||||||
builds for **Linux** (clang + libc++) and cross-compiles to **Windows**
|
builds for **Linux** (clang + libc++) and cross-compiles to **Windows**
|
||||||
(`openra3.exe` + `SDL3.dll`) with llvm-mingw — both using C++26 modules and
|
(`openra3.exe` + `SDL3.dll`) with llvm-mingw — both using C++26 modules and
|
||||||
@@ -93,6 +96,7 @@ built-in test map so the project still builds and runs in CI.
|
|||||||
| `src/map/ra3.map.cppm` | map catalog, `EAR`/RefPack unwrap, start waypoints, `gamestrings.csf` display names |
|
| `src/map/ra3.map.cppm` | map catalog, `EAR`/RefPack unwrap, start waypoints, `gamestrings.csf` display names |
|
||||||
| `src/skirmish/ra3.skirmish.cppm` | base building, economy, AI, combat, win condition |
|
| `src/skirmish/ra3.skirmish.cppm` | base building, economy, AI, combat, win condition |
|
||||||
| `src/terrain/ra3.terrain.cppm` | `CkMp` terrain: `HeightMapData`, `BlendTileData` (tiles + blends), `Terrain.big` tiles |
|
| `src/terrain/ra3.terrain.cppm` | `CkMp` terrain: `HeightMapData`, `BlendTileData` (tiles + blends), `Terrain.big` tiles |
|
||||||
|
| `src/models/ra3.models.cppm` | compiled `W3DMesh` art (BAB stream + DDS textures) for the objects a map places |
|
||||||
| `src/render/ra3.render.cppm` | ARGB framebuffer, TGA decode, BMP encode, map compositing, bitmap-font text |
|
| `src/render/ra3.render.cppm` | ARGB framebuffer, TGA decode, BMP encode, map compositing, bitmap-font text |
|
||||||
| `src/ui/ra3.ui.*.cppm` | SDL3 window viewer and menu (null backend when SDL3 is absent) |
|
| `src/ui/ra3.ui.*.cppm` | SDL3 window viewer and menu (null backend when SDL3 is absent) |
|
||||||
| `src/vulkan/ra3.vulkan.*.cppm` | Vulkan presentation backend and menu (null fallback without a loader) |
|
| `src/vulkan/ra3.vulkan.*.cppm` | Vulkan presentation backend and menu (null fallback without a loader) |
|
||||||
@@ -290,9 +294,22 @@ textures from `Data\Terrain.big` (RefPack + TGA), and rasterises the map with an
|
|||||||
elevation shade. Each cell cross-fades into its blend neighbour with the same
|
elevation shade. Each cell cross-fades into its blend neighbour with the same
|
||||||
linear ramp the retail `Terrain.fx` uses, so material transitions are smooth; on
|
linear ramp the retail `Terrain.fx` uses, so material transitions are smooth; on
|
||||||
the GPU path the tile atlas is padded with a replicated gutter so filtering
|
the GPU path the tile atlas is padded with a replicated gutter so filtering
|
||||||
never bleeds between tiles. Match state is overlaid (start markers in yellow,
|
never bleeds between tiles. It also draws the **buildings and props the map
|
||||||
player 0 in blue, player 1 in red). `--thumbnail` uses the old `<map>_art.tga`
|
places**: the `ObjectsList` chunk is decoded into `(type, position, angle)`
|
||||||
overview instead.
|
instances, the type resolves to its compiled `W3DMesh` parts in the map's art
|
||||||
|
stream (`Data\WBData.big`'s uncompressed `worldbuilder.bin`), the embedded DDS
|
||||||
|
textures are decoded, bind-pose skinning places each mesh's bone-space vertices
|
||||||
|
(`W3DHierarchy`), and the scene is flattened into one world-space triangle soup.
|
||||||
|
The terrain raymarcher writes a depth value so the model pass depth-tests
|
||||||
|
against the relief; the GPU path draws it in a second pipeline (shared camera
|
||||||
|
basis, with a small depth bias so ground decals do not z-fight) and the software
|
||||||
|
path rasterises it with a z-buffer. Match state is
|
||||||
|
overlaid (start markers in yellow, player 0 in blue, player 1 in red).
|
||||||
|
`--thumbnail` uses the old `<map>_art.tga` overview instead.
|
||||||
|
|
||||||
|
> **Not yet drawn:** the map's `Road` objects (the sidewalk/road segments that
|
||||||
|
> reference a `Road` template rather than a mesh) need the retail road-network
|
||||||
|
> mesher and are skipped; they are reported in the `objects: … missing` count.
|
||||||
|
|
||||||
**Offscreen image** (works anywhere, no display needed):
|
**Offscreen image** (works anywhere, no display needed):
|
||||||
|
|
||||||
|
|||||||
+76
-2
@@ -200,6 +200,79 @@ namespace {
|
|||||||
return std::nullopt;
|
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. */
|
/** 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 {
|
auto write_map_names(const std::filesystem::path &data_dir, const std::filesystem::path &assets) -> bool {
|
||||||
try {
|
try {
|
||||||
@@ -465,8 +538,9 @@ namespace {
|
|||||||
|
|
||||||
const bool offscreen = option_value(args, "--out").has_value() || has_flag(args, "--no-window");
|
const bool offscreen = option_value(args, "--out").has_value() || has_flag(args, "--no-window");
|
||||||
bool gpu_shown = false;
|
bool gpu_shown = false;
|
||||||
|
const auto objects = build_object_scene(assets, picked->id, terrain, terrain_options, bytes);
|
||||||
if (!offscreen) {
|
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;
|
ra3::client::display_options options;
|
||||||
options.title = scene.title;
|
options.title = scene.title;
|
||||||
options.width = static_cast<int>(width);
|
options.width = static_cast<int>(width);
|
||||||
@@ -486,7 +560,7 @@ namespace {
|
|||||||
}
|
}
|
||||||
if (offscreen) {
|
if (offscreen) {
|
||||||
// Offscreen: the software raymarcher produces the 3D image.
|
// 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;
|
perspective = true;
|
||||||
} else {
|
} else {
|
||||||
// Windowed without a GPU: the cheap top-down raster.
|
// Windowed without a GPU: the cheap top-down raster.
|
||||||
|
|||||||
+13
-9
@@ -79,7 +79,7 @@ umbrella module re-exports the SDK; applications import `ra3` only.
|
|||||||
```
|
```
|
||||||
|
|
||||||
The current concrete modules (`ra3.core`, `ra3.logic`, `ra3.data`,
|
The current concrete modules (`ra3.core`, `ra3.logic`, `ra3.data`,
|
||||||
`ra3.skirmish`, `ra3.fs`, `ra3.map`, `ra3.terrain`, `ra3.render`, `ra3.ui.*`,
|
`ra3.skirmish`, `ra3.fs`, `ra3.map`, `ra3.terrain`, `ra3.models`, `ra3.render`, `ra3.ui.*`,
|
||||||
`ra3.vulkan.*`, `ra3.dx.*`, `ra3.webgpu.*`, `ra3.wasmgl.*`, `ra3.display`, `ra3.game`, `ra3.client`, and the
|
`ra3.vulkan.*`, `ra3.dx.*`, `ra3.webgpu.*`, `ra3.wasmgl.*`, `ra3.display`, `ra3.game`, `ra3.client`, and the
|
||||||
vendored `ender.log`) are the **seeds** of the
|
vendored `ender.log`) are the **seeds** of the
|
||||||
target modules below. `ra3.skirmish` and `ra3.game` will be absorbed into
|
target modules below. `ra3.skirmish` and `ra3.game` will be absorbed into
|
||||||
@@ -206,10 +206,11 @@ Mirrors SAGE `GameEngine/Common`. Everything else speaks this.
|
|||||||
- `[ ]` DDS / DXT compressed textures `(v0.5)`
|
- `[ ]` DDS / DXT compressed textures `(v0.5)`
|
||||||
- `[ ]` atlas + mip generation, gutter padding (GPU) `[~]`
|
- `[ ]` atlas + mip generation, gutter padding (GPU) `[~]`
|
||||||
- `[ ]` async upload / streaming `(v0.6)`
|
- `[ ]` async upload / streaming `(v0.6)`
|
||||||
- **F2 Models** `[ ]` `!!`
|
- **F2 Models** `[~]`
|
||||||
- `[ ]` W3D container parse (chunks, hierarchy, meshes) `(v0.6)`
|
- `[x]` compiled `W3DMesh` decode (vertex buffer + D3D9 declaration + triangle list) from the BAB static/worldbuilder stream
|
||||||
- `[ ]` materials, shaders, texture references
|
- `[x]` embedded DDS textures (DXT1/3/5 + uncompressed RGB) → ARGB
|
||||||
- `[ ]` LOD sets, collision meshes
|
- `[x]` `W3DHierarchy` decode + static bind-pose skinning (bone-space vertices)
|
||||||
|
- `[ ]` W3D container/hierarchy animation, LOD sets, collision meshes `!!` `(v0.6)`
|
||||||
- **F3 Animation** `[ ]` `!!`
|
- **F3 Animation** `[ ]` `!!`
|
||||||
- `[ ]` W3D animation chunks, bone poses `(v0.6)`
|
- `[ ]` W3D animation chunks, bone poses `(v0.6)`
|
||||||
- `[ ]` blend trees / transition animations
|
- `[ ]` blend trees / transition animations
|
||||||
@@ -533,8 +534,10 @@ Mirrors SAGE `GameEngine/Common`. Everything else speaks this.
|
|||||||
- **F3 Terrain render** `[~]`
|
- **F3 Terrain render** `[~]`
|
||||||
- `[x]` top-down software + GPU heightfield with blend ramp + gutter atlas
|
- `[x]` top-down software + GPU heightfield with blend ramp + gutter atlas
|
||||||
- `[ ]` perspective terrain mesh, LOD, cliff, water `(v0.6)`
|
- `[ ]` perspective terrain mesh, LOD, cliff, water `(v0.6)`
|
||||||
- **F4 Model render** `[ ]` `!!`
|
- **F4 Model render** `[~]`
|
||||||
- `[ ]` W3D draw, skinning, materials, team colors `(v0.6)`
|
- `[x]` static map-object triangle soup, world-space, depth-tested over the terrain (Vulkan + software)
|
||||||
|
- `[x]` bind-pose skinning (bone-space vertices) + ground-decal depth bias
|
||||||
|
- `[ ]` animated W3D draw, materials, team colors `!!` `(v0.6)`
|
||||||
- `[ ]` shadows, decals, ground marks `(v0.6)`
|
- `[ ]` shadows, decals, ground marks `(v0.6)`
|
||||||
- **F5 VFX** `[ ]`
|
- **F5 VFX** `[ ]`
|
||||||
- `[ ]` particle systems, beams, muzzle flashes, explosions `(v0.6)`
|
- `[ ]` particle systems, beams, muzzle flashes, explosions `(v0.6)`
|
||||||
@@ -575,8 +578,9 @@ Mirrors SAGE `GameEngine/Common`. Everything else speaks this.
|
|||||||
- `[ ]` formal RHI integration (see M16 F1) `(v0.6)`
|
- `[ ]` formal RHI integration (see M16 F1) `(v0.6)`
|
||||||
- **F2 Terrain presentation** `[D]`
|
- **F2 Terrain presentation** `[D]`
|
||||||
- `[x]` GPU heightfield raymarch, mipmapped atlas, gutter, retail blend ramp
|
- `[x]` GPU heightfield raymarch, mipmapped atlas, gutter, retail blend ramp
|
||||||
- **F3 Materials & pipelines** `[ ]`
|
- **F3 Materials & pipelines** `[~]`
|
||||||
- `[ ]` model/particle/HUD pipelines `(v0.6)`
|
- `[x]` static-model pipeline (vertex/index buffers, texture array, depth test)
|
||||||
|
- `[ ]` particle/HUD pipelines `(v0.6)`
|
||||||
- **F4 Null fallback** `[D]`
|
- **F4 Null fallback** `[D]`
|
||||||
- `[x]` report failure when no Vulkan loader is present
|
- `[x]` report failure when no Vulkan loader is present
|
||||||
|
|
||||||
|
|||||||
@@ -212,6 +212,125 @@ per-cell restart is the remaining source of grid lines. Switching the atlas
|
|||||||
from 64 px blocks to continuous 32 px regions (or the Morton 8x8 layout) is the
|
from 64 px blocks to continuous 32 px regions (or the Morton 8x8 layout) is the
|
||||||
next step, pending an art correlation check.
|
next step, pending an art correlation check.
|
||||||
|
|
||||||
|
### Compiled art (BinaryAssetBuilder) and map objects
|
||||||
|
|
||||||
|
Retail RA3 ships no `.w3x`/`.w3d` files: BinaryAssetBuilder bakes every model,
|
||||||
|
texture and script into a *binary asset stream* — a `.manifest` index plus a
|
||||||
|
`.bin` of relocatable instance data (and optional `.relo`/`.imp` fixups). The
|
||||||
|
layout (little-endian) is:
|
||||||
|
|
||||||
|
```
|
||||||
|
ManifestHeader (48 B) isBigEndian u8, isLinked u8, version u16,
|
||||||
|
streamChecksum, allTypesHash, assetCount u32,
|
||||||
|
totalInstanceDataSize, maxInstance/maxRelocation/
|
||||||
|
maxImportsChunkSize, assetReferenceBufferSize,
|
||||||
|
referenceManifestNameBufferSize, assetNameBufferSize,
|
||||||
|
sourceFileNameBufferSize
|
||||||
|
AssetEntry (48 B) * count
|
||||||
|
typeId, instanceId, typeHash, instanceHash,
|
||||||
|
assetReferenceOffset i32, assetReferenceCount i32,
|
||||||
|
nameOffset i32, sourceFileNameOffset i32,
|
||||||
|
instanceDataSize i32, relocationDataSize i32,
|
||||||
|
importsDataSize i32, tokenized u32
|
||||||
|
then the reference / referenced-name / asset-name / source-name buffers
|
||||||
|
```
|
||||||
|
|
||||||
|
Asset names are `Type:Instance` (e.g. `W3DMesh:BB_GRASS02`). Instance pointers
|
||||||
|
are stored as offsets from the start of the instance data (which begins at byte
|
||||||
|
4 of `.bin`, after the stream checksum), so a slice is readable without the
|
||||||
|
relocation stream.
|
||||||
|
|
||||||
|
Each multiplayer map carries its own stream (`data\maps\official\<id>\map.bin`)
|
||||||
|
but it is **linked**: only map-specific assets (the terrain texture atlas,
|
||||||
|
scripts, `GameMap`) have data; the rendered props are imported and therefore
|
||||||
|
have `instanceDataSize == 0`. The complete prop art (meshes + textures) lives in
|
||||||
|
`Data\WBData.big`'s `data\worldbuilder.bin`, which is **uncompressed** (first
|
||||||
|
four bytes are the stream checksum, not `10 FB`), so `ra3.models` reads the
|
||||||
|
1.1 GB stream lazily — the manifest is parsed and only the needed instance
|
||||||
|
slices are read.
|
||||||
|
|
||||||
|
`W3DMesh` compiled layout (offsets from the instance start):
|
||||||
|
|
||||||
|
```
|
||||||
|
+4 vertexBufferPtr +52 triangleCount +56 triangleItemPtr
|
||||||
|
+60 shaderNameLength +64 shaderNamePtr
|
||||||
|
vertexBuffer: +0 numVertices, +4 stride, +8 elementDataPtr,
|
||||||
|
+12 declarationBytes, +16 declarationPtr
|
||||||
|
declaration: text "p0:00:3f32 n0:0C:3f32 t0:1C:2f32" (D3D9 usage:index:offset:type)
|
||||||
|
triangles: triangleCount * { u32 indexCount, u32 indexPtr } (24 B each), u32 indices
|
||||||
|
```
|
||||||
|
|
||||||
|
The diffuse texture is found through the mesh's `FXShaderConstant`s
|
||||||
|
(`+76` count, `+80` items): a texture-valued constant (TypeId `0xA59096A6`)
|
||||||
|
names its role (`DiffuseTexture`, `NormalMap`, `SpecMap`) and points at a
|
||||||
|
1-based index into the mesh's cross-asset references, which resolve by
|
||||||
|
`(typeId, instanceId)`. The `Texture` instance embeds a standard DDS file (at
|
||||||
|
`u32@+4`, or scan for `"DDS "`); `ra3.models::decode_dds` decodes DXT1/3/5 and
|
||||||
|
uncompressed 16/24/32-bit.
|
||||||
|
|
||||||
|
**Vertices are stored in bone space, not object space.** A mesh whose vertex
|
||||||
|
declaration carries blend data (`i0:..:4u8 w0:..:4u8n`, e.g. buildings and
|
||||||
|
vehicles) must be skinned; props without it (`BB_GRASS02`, `IF_STREETSEGMENT01`)
|
||||||
|
are already in object space. The skeleton is a `W3DHierarchy` asset named after
|
||||||
|
the mesh's instance prefix (`W3DMesh:FI_STRUCTURE_02.NEWSKIN_CIV01` →
|
||||||
|
`W3DHierarchy:FI_STRUCTURE_02`). Compiled layout:
|
||||||
|
|
||||||
|
```
|
||||||
|
W3DHierarchy: u32 pad, u32 boneCount, u32 headerBytes, then boneCount records
|
||||||
|
100 B each:
|
||||||
|
u32 nameHash, i32 parent (-1 = root), f32 translation[3],
|
||||||
|
f32 quaternion[4] (x, y, z, w), f32 matrix[12]
|
||||||
|
```
|
||||||
|
|
||||||
|
The default (bind) pose is rebuilt by composing each bone's local
|
||||||
|
translation/quaternion down the parent chain, then `skinnedPos = Σ wᵢ ·
|
||||||
|
(Rᵢ·p + Tᵢ)` (and the normal by the rotation only). `ra3.models` does this before
|
||||||
|
placing the mesh at the map object's `(x, y, angle)`.
|
||||||
|
|
||||||
|
The compiled shader (below) binds **one joint per vertex** — `WorldBones` holds
|
||||||
|
64 bones as 2 `float4` each (quaternion `c[128+2j]`, translation `c[129+2j]`) —
|
||||||
|
so the skin is rigid: `blendindices.x` selects the joint, remapped through the
|
||||||
|
mesh's per-model **bone table** (vertex-descriptor `+0x14` = bone count,
|
||||||
|
`+0x18` = `u16` bone indices into the `W3DHierarchy`). Applying the raw blend
|
||||||
|
index without that remap tears models apart (`FI_BUILDING01`'s table is
|
||||||
|
`[0,14,15,16,17,18]`, not `[0..5]`).
|
||||||
|
|
||||||
|
The map objects that lie flat on the ground (sidewalks, deck pieces) are
|
||||||
|
coplanar with the terrain; retail biases their depth in the shader so they do
|
||||||
|
not z-fight. The object pass reproduces that with a small negative depth bias
|
||||||
|
(Vulkan `depthBiasConstant/SlopeFactor`, and a half-unit bias in the software
|
||||||
|
rasteriser).
|
||||||
|
|
||||||
|
Meshes whose material has no diffuse texture (`DefaultW3D.fx`, `BasicW3D.fx` —
|
||||||
|
`FXLIGHTS`/ambient helper billboards) are not opaque geometry and are skipped;
|
||||||
|
drawing them fills the frame with garbage triangles. Likewise the
|
||||||
|
`BuildingsGenericDamageFill.fx` **damage-fill** sub-meshes are skipped: they are
|
||||||
|
the wrecked-interior shell (e.g. `CBBuilding_Wood`, an orange plank texture) that
|
||||||
|
retail only reveals through damage holes, but our opaque pass would paint it over
|
||||||
|
the main shell and tint whole buildings warm.
|
||||||
|
|
||||||
|
### Official shader behaviour (`Shaders.big` → `*.fxo`)
|
||||||
|
|
||||||
|
The compiled D3D9 effects in `Data\Shaders.big` name their parameters, so the
|
||||||
|
model pipeline is recoverable. `buildingsgeneric.fxo` (`BuildingsGeneric.fx`)
|
||||||
|
vertex stage: skinning (above), `World`/`ViewProjection`, and vertex color
|
||||||
|
`c0` multiplied into the lit color
|
||||||
|
(`(Ambient·AmbientColor + Σ DLᵢ.Color·max(N·DLᵢ,0)) · DiffuseColor · vertexColor`).
|
||||||
|
Pixel stage samples `DiffuseTexture`/`NormalMap`/`SpecMap`/`DamagedTexture`/
|
||||||
|
`CloudTexture` (all at **UV0**, except `DamagedTexture` at `v0.wz` = transposed
|
||||||
|
UV1), then `final.rgb *= TintColor` and `*= ShroudTexture.rgb`. `DiffuseVelocity`
|
||||||
|
is not used for static structures. So the diffuse texture is UV0 and is tinted by
|
||||||
|
vertex color and `TintColor`; `basicw3d.fxo` instead modulates a single
|
||||||
|
macro/lightmap with `(vertexColor + additive) * diffuse * 2` and has no normal
|
||||||
|
map. (Recovered by disassembling the embedded `vs_3_0`/`ps_3_0` bytecode.)
|
||||||
|
|
||||||
|
The map's `ObjectsList` chunk is a list of nested `Object` assets —
|
||||||
|
`Coord3D`, Z `angle`, `RoadType` u32, a `u16`-prefixed type-name and a property
|
||||||
|
list whose keys index the shared name table (`ra3.map::parse_objects`). Each
|
||||||
|
type resolves to the `W3DMesh` parts whose instance name equals it or starts
|
||||||
|
with `<type>.`. Objects whose type is a `Road` template (the sidewalk/road
|
||||||
|
segments) are not meshes and are not drawn yet.
|
||||||
|
|
||||||
### Map display names
|
### Map display names
|
||||||
|
|
||||||
The skirmish map list labels live in `Data\English.big`'s
|
The skirmish map list labels live in `Data\English.big`'s
|
||||||
@@ -227,5 +346,9 @@ values are UTF-16 code units whose low byte is XORed with `0xFF`
|
|||||||
- `MPPositionList` layout (per-player starts for maps without waypoints).
|
- `MPPositionList` layout (per-player starts for maps without waypoints).
|
||||||
- Cliff textures and the `CliffTextureMapping` UV remap (`CliffTextures` is
|
- Cliff textures and the `CliffTextureMapping` UV remap (`CliffTextures` is
|
||||||
parsed but not yet drawn).
|
parsed but not yet drawn).
|
||||||
|
- The `Road` network mesher (the map's sidewalk/road objects reference `Road`
|
||||||
|
templates, not `W3DMesh` assets).
|
||||||
|
- W3D container/hierarchy assembly and animation (props are drawn as their
|
||||||
|
static mesh parts; skinned/animated in-game models are not).
|
||||||
- Compiled asset blobs (`global.bin`, `static.*.bin`) and the `.manifest`
|
- Compiled asset blobs (`global.bin`, `static.*.bin`) and the `.manifest`
|
||||||
schema used to deserialise them.
|
schema used to deserialise them.
|
||||||
|
|||||||
+2
-2
@@ -25,9 +25,9 @@ compile() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
for name in scene terrain; do
|
for name in scene terrain object; do
|
||||||
compile "$DIR/$name.vert" "$OUT/$name.vert.spv"
|
compile "$DIR/$name.vert" "$OUT/$name.vert.spv"
|
||||||
compile "$DIR/$name.frag" "$OUT/$name.frag.spv"
|
compile "$DIR/$name.frag" "$OUT/$name.frag.spv"
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "wrote $OUT/{scene,terrain}.{vert,frag}.spv"
|
echo "wrote $OUT/{scene,terrain,object}.{vert,frag}.spv"
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,36 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
// Static-map model fragment stage: sample the shared texture array and apply
|
||||||
|
// the same directional sun the terrain uses. Cut-out props (trees, fences)
|
||||||
|
// carry an alpha mask in their diffuse texture; discard the transparent texels
|
||||||
|
// so the ground shows through.
|
||||||
|
|
||||||
|
layout(binding = 0) uniform sampler2DArray atlas;
|
||||||
|
|
||||||
|
layout(push_constant) uniform Push {
|
||||||
|
vec4 cam_pos;
|
||||||
|
vec4 fwd;
|
||||||
|
vec4 right;
|
||||||
|
vec4 up;
|
||||||
|
vec4 sun;
|
||||||
|
} pc;
|
||||||
|
|
||||||
|
layout(location = 0) in vec2 in_uv;
|
||||||
|
layout(location = 1) in vec3 in_normal;
|
||||||
|
layout(location = 2) flat in float in_layer;
|
||||||
|
|
||||||
|
layout(location = 0) out vec4 out_color;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
int layers = textureSize(atlas, 0).z;
|
||||||
|
int layer = clamp(int(in_layer + 0.5), 0, layers - 1);
|
||||||
|
vec4 tex = texture(atlas, vec3(in_uv, float(layer)));
|
||||||
|
if (tex.a < 0.5) discard;
|
||||||
|
|
||||||
|
vec3 n = normalize(in_normal);
|
||||||
|
if (!gl_FrontFacing) n = -n;
|
||||||
|
float lambert = max(0.0, dot(n, normalize(pc.sun.xyz)));
|
||||||
|
float ambient = pc.sun.w;
|
||||||
|
vec3 lit = tex.rgb * (ambient + (1.0 - ambient) * lambert);
|
||||||
|
out_color = vec4(lit, 1.0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
// Static-map model vertex stage. Draws the world-space triangle soup built by
|
||||||
|
// ra3::models (buildings and props the map places) against the exact camera the
|
||||||
|
// terrain raymarcher uses, so both passes share one projection and depth test.
|
||||||
|
//
|
||||||
|
// The camera is passed as its orthonormal basis so the same math as
|
||||||
|
// terrain.frag applies: ndc.x = dot(r, right) / (a * tan(fov/2) * aspect),
|
||||||
|
// ndc.y = -dot(r, up) / (a * tan(fov/2)), depth = (a - NEAR) / (FAR - NEAR)
|
||||||
|
// with `a = dot(r, fwd)` the view-space depth.
|
||||||
|
|
||||||
|
layout(location = 0) in vec3 in_pos;
|
||||||
|
layout(location = 1) in vec3 in_normal;
|
||||||
|
layout(location = 2) in vec2 in_uv;
|
||||||
|
layout(location = 3) in float in_layer;
|
||||||
|
|
||||||
|
layout(push_constant) uniform Push {
|
||||||
|
vec4 cam_pos; // xyz = eye position
|
||||||
|
vec4 fwd; // xyz = forward
|
||||||
|
vec4 right; // xyz = right, w = tan(fov / 2)
|
||||||
|
vec4 up; // xyz = up, w = tan(fov / 2) * aspect
|
||||||
|
vec4 sun; // xyz = sun direction, w = ambient
|
||||||
|
} pc;
|
||||||
|
|
||||||
|
layout(location = 0) out vec2 out_uv;
|
||||||
|
layout(location = 1) out vec3 out_normal;
|
||||||
|
layout(location = 2) flat out float out_layer;
|
||||||
|
|
||||||
|
const float NEAR = 10.0;
|
||||||
|
const float FAR = 60000.0;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec3 r = in_pos - pc.cam_pos.xyz;
|
||||||
|
float a = dot(r, pc.fwd.xyz);
|
||||||
|
float b = dot(r, pc.right.xyz);
|
||||||
|
float c = dot(r, pc.up.xyz);
|
||||||
|
float th = pc.right.w;
|
||||||
|
float th_aspect = pc.up.w;
|
||||||
|
float depth = clamp((a - NEAR) / (FAR - NEAR), 0.0, 1.0);
|
||||||
|
gl_Position = vec4(b / th_aspect, -c / th, depth * a, a);
|
||||||
|
out_uv = in_uv;
|
||||||
|
out_normal = in_normal;
|
||||||
|
out_layer = in_layer;
|
||||||
|
}
|
||||||
+11
-1
@@ -25,6 +25,11 @@ layout(location = 0) out vec4 out_color;
|
|||||||
|
|
||||||
const float CELL = 10.0; // must match ra3::terrain::cell_size
|
const float CELL = 10.0; // must match ra3::terrain::cell_size
|
||||||
|
|
||||||
|
// Must match object.vert: the shared projection writes the view-space depth
|
||||||
|
// into gl_FragDepth so the static-map models depth-test against the terrain.
|
||||||
|
const float NEAR = 10.0;
|
||||||
|
const float FAR = 60000.0;
|
||||||
|
|
||||||
float height_at(ivec2 c) {
|
float height_at(ivec2 c) {
|
||||||
c = clamp(c, ivec2(0), ivec2(pc.mapinfo.xy) - 1);
|
c = clamp(c, ivec2(0), ivec2(pc.mapinfo.xy) - 1);
|
||||||
return texelFetch(heightmap, c, 0).r * 65535.0 * pc.mapinfo.w;
|
return texelFetch(heightmap, c, 0).r * 65535.0 * pc.mapinfo.w;
|
||||||
@@ -112,6 +117,7 @@ void main() {
|
|||||||
vec3 dir = normalize(fwd + right * ndc.x * th * aspect + up * ndc.y * th);
|
vec3 dir = normalize(fwd + right * ndc.x * th * aspect + up * ndc.y * th);
|
||||||
|
|
||||||
if (dir.z >= -1e-4) {
|
if (dir.z >= -1e-4) {
|
||||||
|
gl_FragDepth = 1.0;
|
||||||
out_color = vec4(sky_color(dir), 1.0);
|
out_color = vec4(sky_color(dir), 1.0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -133,7 +139,7 @@ void main() {
|
|||||||
if (w.z <= world_height(w.x, w.y)) { hit = true; hit_t = t; break; }
|
if (w.z <= world_height(w.x, w.y)) { hit = true; hit_t = t; break; }
|
||||||
prev = t; dt *= 1.10; t += dt;
|
prev = t; dt *= 1.10; t += dt;
|
||||||
}
|
}
|
||||||
if (!hit) { out_color = vec4(sky_color(dir), 1.0); return; }
|
if (!hit) { gl_FragDepth = 1.0; out_color = vec4(sky_color(dir), 1.0); return; }
|
||||||
|
|
||||||
float lo = prev, hi = hit_t;
|
float lo = prev, hi = hit_t;
|
||||||
for (int i = 0; i < 6; ++i) {
|
for (int i = 0; i < 6; ++i) {
|
||||||
@@ -144,6 +150,10 @@ void main() {
|
|||||||
}
|
}
|
||||||
vec3 hitpos = cam + dir * hi;
|
vec3 hitpos = cam + dir * hi;
|
||||||
|
|
||||||
|
// View-space depth of the hit (project onto the forward axis), matching the
|
||||||
|
// projection object.vert applies to the model vertices.
|
||||||
|
gl_FragDepth = clamp((dot(hitpos - cam, fwd) - NEAR) / (FAR - NEAR), 0.0, 1.0);
|
||||||
|
|
||||||
vec3 sun = normalize(pc.sun.xyz);
|
vec3 sun = normalize(pc.sun.xyz);
|
||||||
float ambient = pc.sun.w;
|
float ambient = pc.sun.w;
|
||||||
|
|
||||||
|
|||||||
+133
-1
@@ -19,8 +19,9 @@ export import ra3.fs;
|
|||||||
export namespace ra3::map {
|
export namespace ra3::map {
|
||||||
using ra3::core::coord3d;
|
using ra3::core::coord3d;
|
||||||
using ra3::core::real;
|
using ra3::core::real;
|
||||||
using ra3::core::uint32;
|
|
||||||
using ra3::core::uint8;
|
using ra3::core::uint8;
|
||||||
|
using ra3::core::uint16;
|
||||||
|
using ra3::core::uint32;
|
||||||
using ra3::core::usize;
|
using ra3::core::usize;
|
||||||
|
|
||||||
/** A player start location extracted from a map's waypoints. */
|
/** A player start location extracted from a map's waypoints. */
|
||||||
@@ -202,6 +203,137 @@ export namespace ra3::map {
|
|||||||
return starts;
|
return starts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One object the map places on the ground: a building, a prop, a lamp, a
|
||||||
|
* tree — anything in the `ObjectsList` chunk.
|
||||||
|
*
|
||||||
|
* `type` is the SAGE `ThingTemplate` name (e.g. `BB_GRASS02`,
|
||||||
|
* `AlliedBarracks`); the per-map compiled art stream resolves it to the
|
||||||
|
* `W3DMesh` assets that draw it. `angle` is the Z rotation in radians.
|
||||||
|
*/
|
||||||
|
struct map_object {
|
||||||
|
std::string type;
|
||||||
|
real x = 0.0F;
|
||||||
|
real y = 0.0F;
|
||||||
|
real z = 0.0F;
|
||||||
|
real angle = 0.0F;
|
||||||
|
real scale = 1.0F;
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace detail {
|
||||||
|
struct ckmp_chunk {
|
||||||
|
std::string name;
|
||||||
|
uint16 version = 0;
|
||||||
|
usize offset = 0;
|
||||||
|
usize size = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Parse the `CkMp` chunk tree and its `index -> name` table. */
|
||||||
|
[[nodiscard]] inline auto parse_ckmp_chunks(std::span<const uint8> data) -> std::pair<std::vector<std::string>, std::vector<ckmp_chunk>> {
|
||||||
|
if (data.size() < 8U || std::memcmp(data.data(), "CkMp", 4) != 0) return {};
|
||||||
|
usize pos = 4;
|
||||||
|
const auto read_u32 = [&](usize at) {
|
||||||
|
return static_cast<uint32>(data[at]) | (static_cast<uint32>(data[at + 1U]) << 8U) | (static_cast<uint32>(data[at + 2U]) << 16U) |
|
||||||
|
(static_cast<uint32>(data[at + 3U]) << 24U);
|
||||||
|
};
|
||||||
|
const auto read_u16 = [&](usize at) { return static_cast<uint16>(static_cast<uint32>(data[at]) | (static_cast<uint32>(data[at + 1U]) << 8U)); };
|
||||||
|
const auto count = read_u32(pos);
|
||||||
|
pos += 4;
|
||||||
|
std::vector<std::string> names(count + 1U);
|
||||||
|
for (uint32 i = count; i >= 1U && pos < data.size(); --i) {
|
||||||
|
const auto len = data[pos++];
|
||||||
|
if (pos + len + 4U > data.size()) break;
|
||||||
|
names[i] = std::string{reinterpret_cast<const char *>(data.data() + pos), len};
|
||||||
|
pos += len + 4U;
|
||||||
|
}
|
||||||
|
std::vector<ckmp_chunk> chunks;
|
||||||
|
while (pos + 10U <= data.size()) {
|
||||||
|
const auto index = read_u32(pos);
|
||||||
|
const auto version = read_u16(pos + 4U);
|
||||||
|
const auto size = read_u32(pos + 6U);
|
||||||
|
pos += 10U;
|
||||||
|
if (index >= names.size() || pos + size > data.size()) break;
|
||||||
|
chunks.push_back({names[index], version, pos, size});
|
||||||
|
pos += size;
|
||||||
|
}
|
||||||
|
return {std::move(names), std::move(chunks)};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every object the map places (the `ObjectsList` chunk).
|
||||||
|
*
|
||||||
|
* Layout (OpenSAGE `Data/Map/{ObjectsList,MapObject,AssetProperty}.cs`):
|
||||||
|
* the chunk is a list of nested `Object` assets, each a `Coord3D`, a Z
|
||||||
|
* `angle`, a `RoadType`, a `u16`-prefixed type-name and a property list
|
||||||
|
* whose keys index the shared name table.
|
||||||
|
*
|
||||||
|
* @return The objects in chunk order; empty when the map has no object list.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] inline auto parse_objects(std::span<const uint8> ckmp) -> std::vector<map_object> {
|
||||||
|
const auto [names, chunks] = detail::parse_ckmp_chunks(ckmp);
|
||||||
|
(void) names;
|
||||||
|
std::vector<map_object> objects;
|
||||||
|
const auto read_u16 = [&](usize at) { return static_cast<uint16>(static_cast<uint32>(ckmp[at]) | (static_cast<uint32>(ckmp[at + 1U]) << 8U)); };
|
||||||
|
const auto read_u32 = [&](usize at) {
|
||||||
|
return static_cast<uint32>(ckmp[at]) | (static_cast<uint32>(ckmp[at + 1U]) << 8U) | (static_cast<uint32>(ckmp[at + 2U]) << 16U) |
|
||||||
|
(static_cast<uint32>(ckmp[at + 3U]) << 24U);
|
||||||
|
};
|
||||||
|
const auto read_f32 = [&](usize at) {
|
||||||
|
const auto bits = read_u32(at);
|
||||||
|
real value = 0.0F;
|
||||||
|
std::memcpy(&value, &bits, sizeof(value));
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto &chunk: chunks) {
|
||||||
|
if (chunk.name != "ObjectsList") continue;
|
||||||
|
usize p = chunk.offset;
|
||||||
|
const auto end = chunk.offset + chunk.size;
|
||||||
|
while (p + 6U <= end) {
|
||||||
|
p += 4U; // asset index (always `Object`)
|
||||||
|
p += 2U; // asset version
|
||||||
|
const auto asset_size = read_u32(p);
|
||||||
|
p += 4U;
|
||||||
|
if (asset_size < 24U || p + asset_size > end) break;
|
||||||
|
const auto asset_end = p + asset_size;
|
||||||
|
|
||||||
|
map_object object;
|
||||||
|
object.x = read_f32(p);
|
||||||
|
object.y = read_f32(p + 4U);
|
||||||
|
object.z = read_f32(p + 8U);
|
||||||
|
object.angle = read_f32(p + 12U);
|
||||||
|
p += 20U; // Coord3D + angle + road type
|
||||||
|
const auto name_len = read_u16(p);
|
||||||
|
p += 2U;
|
||||||
|
if (p + name_len > asset_end) break;
|
||||||
|
object.type = std::string{reinterpret_cast<const char *>(ckmp.data() + p), name_len};
|
||||||
|
p += name_len;
|
||||||
|
|
||||||
|
const auto property_count = static_cast<uint32>(read_u16(p));
|
||||||
|
p += 2U;
|
||||||
|
for (uint32 i = 0; i < property_count && p + 4U <= asset_end; ++i) {
|
||||||
|
const auto type = ckmp[p++];
|
||||||
|
p += 3U; // property name index (u24) into the shared name table
|
||||||
|
usize value_size = 0;
|
||||||
|
if (type == 0U) {
|
||||||
|
value_size = 1U; // boolean
|
||||||
|
} else if (type == 1U || type == 2U) {
|
||||||
|
value_size = 4U; // integer / real
|
||||||
|
} else if (p + 2U <= asset_end) {
|
||||||
|
const auto len = read_u16(p);
|
||||||
|
value_size = 2U + static_cast<usize>(len) * ((type == 4U) ? 2U : 1U); // ascii/unicode/unknown
|
||||||
|
}
|
||||||
|
p += value_size;
|
||||||
|
if (p > asset_end) break;
|
||||||
|
}
|
||||||
|
objects.push_back(std::move(object));
|
||||||
|
p = asset_end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return objects;
|
||||||
|
}
|
||||||
|
|
||||||
/** Map id -> localized display name, keyed by lowercased id. */
|
/** Map id -> localized display name, keyed by lowercased id. */
|
||||||
struct map_name_table {
|
struct map_name_table {
|
||||||
std::unordered_map<std::string, std::string> names;
|
std::unordered_map<std::string, std::string> names;
|
||||||
|
|||||||
@@ -0,0 +1,939 @@
|
|||||||
|
export module ra3.models;
|
||||||
|
|
||||||
|
import std;
|
||||||
|
|
||||||
|
export import ra3.core;
|
||||||
|
export import ra3.render;
|
||||||
|
export import ra3.fs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The map's static art: the compiled W3D models that draw the buildings and
|
||||||
|
* props a map places on the ground.
|
||||||
|
*
|
||||||
|
* Retail RA3 does not ship `.w3x`/`.w3d` files. BinaryAssetBuilder bakes every
|
||||||
|
* model into a *binary asset stream* — a `.manifest` index plus a `.bin` of
|
||||||
|
* relocatable instance data — and each map carries its own stream in
|
||||||
|
* `Maps*.big` under `data\maps\official\<id>\map.{manifest,bin}`. Those streams
|
||||||
|
* hold the `W3DMesh` assets for the map's props (sidewalks, deck props, lights,
|
||||||
|
* walls, civilian buildings, ...) and the `Texture` assets they sample.
|
||||||
|
*
|
||||||
|
* This module parses that stream, decodes the compiled `W3DMesh` vertex/index
|
||||||
|
* buffers and the embedded DDS textures, and flattens the map's objects into a
|
||||||
|
* single world-space triangle soup the renderers upload directly.
|
||||||
|
*
|
||||||
|
* Format references: ra3-headless `ra3tools/ra3_binary.py` (BAB
|
||||||
|
* `ManifestHeader`/`AssetEntry`, `SageBinaryData/W3D.cs`) and OpenSAGE
|
||||||
|
* `Data/Map/MapObject.cs`. Instance pointers are stored as offsets from the
|
||||||
|
* start of the instance data, so no relocation pass is needed.
|
||||||
|
*/
|
||||||
|
export namespace ra3::models {
|
||||||
|
using ra3::core::uint8;
|
||||||
|
using ra3::core::uint16;
|
||||||
|
using ra3::core::uint32;
|
||||||
|
using ra3::core::int32;
|
||||||
|
using ra3::core::usize;
|
||||||
|
using ra3::render::argb;
|
||||||
|
using ra3::render::image;
|
||||||
|
|
||||||
|
/** Thrown when a compiled asset stream or a model payload is malformed. */
|
||||||
|
class model_error : public std::runtime_error {
|
||||||
|
public:
|
||||||
|
using std::runtime_error::runtime_error;
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace detail {
|
||||||
|
[[nodiscard]] inline auto u16(std::span<const uint8> b, usize off) -> uint16 {
|
||||||
|
return off + 2U <= b.size() ? static_cast<uint16>(b[off]) | (static_cast<uint16>(b[off + 1U]) << 8U) : 0U;
|
||||||
|
}
|
||||||
|
[[nodiscard]] inline auto u32(std::span<const uint8> b, usize off) -> uint32 {
|
||||||
|
return off + 4U <= b.size() ? static_cast<uint32>(b[off]) | (static_cast<uint32>(b[off + 1U]) << 8U) | (static_cast<uint32>(b[off + 2U]) << 16U) |
|
||||||
|
(static_cast<uint32>(b[off + 3U]) << 24U)
|
||||||
|
: 0U;
|
||||||
|
}
|
||||||
|
[[nodiscard]] inline auto i32(std::span<const uint8> b, usize off) -> ra3::core::int32 { return static_cast<ra3::core::int32>(u32(b, off)); }
|
||||||
|
[[nodiscard]] inline auto f32(std::span<const uint8> b, usize off) -> float {
|
||||||
|
const auto bits = u32(b, off);
|
||||||
|
float value = 0.0F;
|
||||||
|
std::memcpy(&value, &bits, sizeof(value));
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
[[nodiscard]] inline auto cstr(std::span<const uint8> b, usize off, ra3::core::int32 length) -> std::string {
|
||||||
|
if (off == 0U || length <= 0 || off + static_cast<usize>(length) > b.size()) return {};
|
||||||
|
return std::string{reinterpret_cast<const char *>(b.data() + off), static_cast<usize>(length)};
|
||||||
|
}
|
||||||
|
/** NUL-terminated string in the manifest name/source buffers. */
|
||||||
|
[[nodiscard]] inline auto nul_string(std::span<const uint8> b, usize off) -> std::string {
|
||||||
|
if (off >= b.size()) return {};
|
||||||
|
usize end = off;
|
||||||
|
while (end < b.size() && b[end] != 0U) ++end;
|
||||||
|
return std::string{reinterpret_cast<const char *>(b.data() + off), end - off};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A parsed `BinaryAsset` stream: the manifest index plus the concatenated
|
||||||
|
* instance data. Assets are addressed by `Type:Instance` or by their
|
||||||
|
* `(typeId, instanceId)` pair (used by cross-asset references).
|
||||||
|
*/
|
||||||
|
class asset_stream {
|
||||||
|
public:
|
||||||
|
struct asset {
|
||||||
|
std::string name; ///< `Type:Instance`
|
||||||
|
std::string source;
|
||||||
|
uint32 type_id = 0;
|
||||||
|
uint32 instance_id = 0;
|
||||||
|
usize instance_offset = 0;
|
||||||
|
usize instance_size = 0;
|
||||||
|
std::vector<std::pair<uint32, uint32>> references; ///< `(typeId, instanceId)` targets.
|
||||||
|
|
||||||
|
[[nodiscard]] auto type_name() const -> std::string_view { return std::string_view{name}.substr(0, name.find(':')); }
|
||||||
|
[[nodiscard]] auto instance_name() const -> std::string_view {
|
||||||
|
const auto at = name.find(':');
|
||||||
|
return at == std::string::npos ? std::string_view{name} : std::string_view{name}.substr(at + 1U);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr uint32 header_size = 48U;
|
||||||
|
static constexpr uint32 entry_size = 48U;
|
||||||
|
|
||||||
|
/** Parse an in-memory `.manifest` + `.bin` pair. */
|
||||||
|
[[nodiscard]] static auto load(std::span<const uint8> manifest_raw, std::span<const uint8> data_raw) -> asset_stream {
|
||||||
|
asset_stream stream;
|
||||||
|
stream.data_ = fs::maybe_decompress(data_raw);
|
||||||
|
stream.parse(fs::maybe_decompress(manifest_raw));
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a `.manifest` + `.bin` pair from disk.
|
||||||
|
*
|
||||||
|
* Retail's worldbuilder stream is ~1.1 GB and uncompressed, so a small
|
||||||
|
* `.bin` is read into memory but a large one is read lazily per asset
|
||||||
|
* (its pointers are offsets, so a slice needs no random-access decode).
|
||||||
|
*/
|
||||||
|
[[nodiscard]] static auto load_files(const std::filesystem::path &manifest_path, const std::filesystem::path &bin_path,
|
||||||
|
usize inline_limit = 128U * 1024U * 1024U) -> asset_stream {
|
||||||
|
const auto read_all = [](const std::filesystem::path &path) {
|
||||||
|
std::ifstream in(path, std::ios::binary);
|
||||||
|
if (!in) throw model_error("cannot open " + path.string());
|
||||||
|
return std::vector<uint8>{std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>()};
|
||||||
|
};
|
||||||
|
std::error_code ec;
|
||||||
|
const auto bin_size = std::filesystem::file_size(bin_path, ec);
|
||||||
|
asset_stream stream;
|
||||||
|
if (ec || bin_size <= inline_limit) {
|
||||||
|
stream.data_ = fs::maybe_decompress(read_all(bin_path));
|
||||||
|
} else {
|
||||||
|
stream.bin_path_ = bin_path;
|
||||||
|
// The large retail streams (worldbuilder.bin) are stored raw; a
|
||||||
|
// RefPack stream would not be randomly addressable anyway.
|
||||||
|
const auto probe = [&] {
|
||||||
|
std::ifstream in(bin_path, std::ios::binary);
|
||||||
|
std::array<uint8, 16> head{};
|
||||||
|
in.read(reinterpret_cast<char *>(head.data()), static_cast<std::streamsize>(head.size()));
|
||||||
|
return fs::is_refpack(head);
|
||||||
|
}();
|
||||||
|
if (probe) throw model_error("compressed asset stream is too large to map");
|
||||||
|
}
|
||||||
|
stream.parse(read_all(manifest_path));
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] auto assets() const -> const std::vector<asset> & { return assets_; }
|
||||||
|
|
||||||
|
[[nodiscard]] auto find(uint32 type_id, uint32 instance_id) const -> const asset * {
|
||||||
|
const auto it = index_.find((static_cast<unsigned long long>(type_id) << 32U) | instance_id);
|
||||||
|
return it == index_.end() ? nullptr : &assets_[it->second];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** First asset whose `Type:Instance` name equals `name` (case-insensitive). */
|
||||||
|
[[nodiscard]] auto find_name(std::string_view name) const -> const asset * {
|
||||||
|
const auto want = lower(name);
|
||||||
|
for (const auto &a: assets_) {
|
||||||
|
if (lower(a.name) == want) return &a;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Every `W3DMesh` whose instance name equals `base` or starts with `base.`. */
|
||||||
|
[[nodiscard]] auto meshes_for(std::string_view base) const -> const std::vector<const asset *> & {
|
||||||
|
static const std::vector<const asset *> none;
|
||||||
|
const auto it = mesh_index_.find(lower(base));
|
||||||
|
return it == mesh_index_.end() ? none : it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The asset's instance bytes (from memory, or lazily from disk). */
|
||||||
|
[[nodiscard]] auto read_instance(const asset &a) const -> std::vector<uint8> {
|
||||||
|
if (!data_.empty()) {
|
||||||
|
if (a.instance_offset + a.instance_size > data_.size()) return {};
|
||||||
|
return {data_.begin() + static_cast<std::ptrdiff_t>(a.instance_offset),
|
||||||
|
data_.begin() + static_cast<std::ptrdiff_t>(a.instance_offset + a.instance_size)};
|
||||||
|
}
|
||||||
|
if (bin_path_.empty() || a.instance_size == 0U) return {};
|
||||||
|
std::ifstream in(bin_path_, std::ios::binary);
|
||||||
|
if (!in) return {};
|
||||||
|
in.seekg(static_cast<std::streamoff>(a.instance_offset));
|
||||||
|
std::vector<uint8> bytes(a.instance_size);
|
||||||
|
in.read(reinterpret_cast<char *>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] static auto lower(std::string_view text) -> std::string {
|
||||||
|
std::string out{text};
|
||||||
|
std::transform(out.begin(), out.end(), out.begin(), [](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
/** Parse the manifest into `assets_` + the lookup indexes. */
|
||||||
|
auto parse(std::span<const uint8> manifest) -> void {
|
||||||
|
if (manifest.size() < header_size) throw model_error("asset manifest is too short");
|
||||||
|
if (manifest[0] != 0U) throw model_error("big-endian asset manifest is unsupported");
|
||||||
|
|
||||||
|
const auto count = detail::u32(manifest, 12U);
|
||||||
|
const auto ref_buf_size = detail::u32(manifest, 32U);
|
||||||
|
const auto ref_name_buf_size = detail::u32(manifest, 36U);
|
||||||
|
const auto name_buf_size = detail::u32(manifest, 40U);
|
||||||
|
const auto src_buf_size = detail::u32(manifest, 44U);
|
||||||
|
(void) src_buf_size;
|
||||||
|
if (header_size + static_cast<usize>(count) * entry_size > manifest.size()) throw model_error("asset manifest entry table is truncated");
|
||||||
|
|
||||||
|
const usize entries_off = header_size;
|
||||||
|
const usize ref_off = entries_off + static_cast<usize>(count) * entry_size;
|
||||||
|
const usize refname_off = ref_off + ref_buf_size;
|
||||||
|
const usize name_off = refname_off + ref_name_buf_size;
|
||||||
|
const usize src_off = name_off + name_buf_size;
|
||||||
|
|
||||||
|
assets_.reserve(count);
|
||||||
|
usize instance_offset = 4U; // the first four bytes of `.bin` are the stream checksum
|
||||||
|
for (uint32 i = 0; i < count; ++i) {
|
||||||
|
const usize e = entries_off + static_cast<usize>(i) * entry_size;
|
||||||
|
asset a;
|
||||||
|
a.type_id = detail::u32(manifest, e);
|
||||||
|
a.instance_id = detail::u32(manifest, e + 4U);
|
||||||
|
const auto aref_off = detail::i32(manifest, e + 16U);
|
||||||
|
const auto aref_count = detail::i32(manifest, e + 20U);
|
||||||
|
a.instance_size = detail::u32(manifest, e + 32U);
|
||||||
|
a.instance_offset = instance_offset;
|
||||||
|
a.name = detail::nul_string(manifest, name_off + static_cast<usize>(std::max(0, detail::i32(manifest, e + 24U))));
|
||||||
|
a.source = detail::nul_string(manifest, src_off + static_cast<usize>(std::max(0, detail::i32(manifest, e + 28U))));
|
||||||
|
if (aref_off >= 0 && aref_count > 0 && ref_off + static_cast<usize>(aref_off) + static_cast<usize>(aref_count) * 8U <= manifest.size()) {
|
||||||
|
a.references.reserve(static_cast<usize>(aref_count));
|
||||||
|
for (int r = 0; r < aref_count; ++r) {
|
||||||
|
const usize ro = ref_off + static_cast<usize>(aref_off) + static_cast<usize>(r) * 8U;
|
||||||
|
a.references.emplace_back(detail::u32(manifest, ro), detail::u32(manifest, ro + 4U));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
index_.try_emplace((static_cast<unsigned long long>(a.type_id) << 32U) | a.instance_id, assets_.size());
|
||||||
|
assets_.push_back(std::move(a));
|
||||||
|
instance_offset += assets_.back().instance_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index meshes by the base of their instance name (`A.B` -> `a` and
|
||||||
|
// `a.b`) so a map object type resolves to its mesh parts in one look.
|
||||||
|
for (const auto &a: assets_) {
|
||||||
|
if (a.type_name() != "W3DMesh") continue;
|
||||||
|
const auto instance = lower(a.instance_name());
|
||||||
|
mesh_index_[instance].push_back(&a);
|
||||||
|
const auto dot = instance.find('.');
|
||||||
|
if (dot != std::string::npos) mesh_index_[instance.substr(0, dot)].push_back(&a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<asset> assets_;
|
||||||
|
std::unordered_map<unsigned long long, usize> index_;
|
||||||
|
std::unordered_map<std::string, std::vector<const asset *>> mesh_index_;
|
||||||
|
std::vector<uint8> data_;
|
||||||
|
std::filesystem::path bin_path_;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** A decoded compiled `W3DMesh`: a vertex buffer plus a triangle list, in model space. */
|
||||||
|
struct mesh {
|
||||||
|
std::vector<float> positions; ///< 3 floats per vertex.
|
||||||
|
std::vector<float> normals; ///< 3 floats per vertex, or empty.
|
||||||
|
std::vector<float> uvs; ///< 2 floats per vertex, or empty.
|
||||||
|
std::vector<uint32> indices;
|
||||||
|
std::string shader; ///< FX shader name (`BuildingsGeneric.fx`, ...).
|
||||||
|
std::vector<uint8> blend_indices; ///< 4 bone indices per vertex, or empty (static mesh).
|
||||||
|
std::vector<uint16> bone_remap; ///< Per-mesh blend index -> hierarchy bone map, or empty.
|
||||||
|
std::vector<float> blend_weights; ///< 4 weights per vertex, or empty.
|
||||||
|
|
||||||
|
[[nodiscard]] auto skinned() const -> bool { return !blend_indices.empty(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
/** One bone of a `W3DHierarchy`: name hash, parent and the local (bind) transform. */
|
||||||
|
struct bone {
|
||||||
|
uint32 name_hash = 0;
|
||||||
|
int32 parent = -1; ///< -1 for the root.
|
||||||
|
float tx = 0.0F;
|
||||||
|
float ty = 0.0F;
|
||||||
|
float tz = 0.0F;
|
||||||
|
float qx = 0.0F;
|
||||||
|
float qy = 0.0F;
|
||||||
|
float qz = 0.0F;
|
||||||
|
float qw = 1.0F;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** A decoded `W3DHierarchy` (the skeleton the meshes are bound to). */
|
||||||
|
struct hierarchy {
|
||||||
|
std::vector<bone> bones;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a compiled `W3DHierarchy`.
|
||||||
|
*
|
||||||
|
* Layout (from the retail data): `u32 pad, u32 boneCount, u32 headerBytes`,
|
||||||
|
* then `boneCount` 100-byte records: `u32 nameHash, i32 parent, f32 t[3],
|
||||||
|
* f32 quaternion[4] (x,y,z,w), f32 matrix[12]`. Meshes are bound to bones in
|
||||||
|
* *bone space*, so a static render must rebuild each bone's world transform
|
||||||
|
* from this default pose (see `bone_world_matrices`).
|
||||||
|
*/
|
||||||
|
[[nodiscard]] inline auto decode_hierarchy(std::span<const uint8> data) -> hierarchy {
|
||||||
|
hierarchy out;
|
||||||
|
const auto bone_count = detail::u32(data, 4U);
|
||||||
|
const auto header = detail::u32(data, 8U);
|
||||||
|
if (bone_count == 0U || bone_count > 4096U || header + static_cast<usize>(bone_count) * 100U > data.size()) return out;
|
||||||
|
out.bones.reserve(bone_count);
|
||||||
|
for (uint32 i = 0; i < bone_count; ++i) {
|
||||||
|
const auto o = header + static_cast<usize>(i) * 100U;
|
||||||
|
bone b;
|
||||||
|
b.name_hash = detail::u32(data, o);
|
||||||
|
b.parent = detail::i32(data, o + 4U);
|
||||||
|
b.tx = detail::f32(data, o + 8U);
|
||||||
|
b.ty = detail::f32(data, o + 12U);
|
||||||
|
b.tz = detail::f32(data, o + 16U);
|
||||||
|
b.qx = detail::f32(data, o + 20U);
|
||||||
|
b.qy = detail::f32(data, o + 24U);
|
||||||
|
b.qz = detail::f32(data, o + 28U);
|
||||||
|
b.qw = detail::f32(data, o + 32U);
|
||||||
|
out.bones.push_back(b);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A bone's world transform: 3x3 rotation (row-major) followed by a translation. */
|
||||||
|
struct bone_matrix {
|
||||||
|
std::array<float, 9> rotation{1.0F, 0.0F, 0.0F, 0.0F, 1.0F, 0.0F, 0.0F, 0.0F, 1.0F};
|
||||||
|
std::array<float, 3> translation{0.0F, 0.0F, 0.0F};
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace detail {
|
||||||
|
/** Quaternion `(x, y, z, w)` to a row-major 3x3 rotation matrix. */
|
||||||
|
[[nodiscard]] inline auto quaternion_matrix(float x, float y, float z, float w) -> std::array<float, 9> {
|
||||||
|
const auto n = std::sqrt(x * x + y * y + z * z + w * w);
|
||||||
|
if (n <= 1.0e-8F) return {1.0F, 0.0F, 0.0F, 0.0F, 1.0F, 0.0F, 0.0F, 0.0F, 1.0F};
|
||||||
|
x /= n;
|
||||||
|
y /= n;
|
||||||
|
z /= n;
|
||||||
|
w /= n;
|
||||||
|
return {1.0F - 2.0F * (y * y + z * z), 2.0F * (x * y - z * w), 2.0F * (x * z + y * w),
|
||||||
|
2.0F * (x * y + z * w), 1.0F - 2.0F * (x * x + z * z), 2.0F * (y * z - x * w),
|
||||||
|
2.0F * (x * z - y * w), 2.0F * (y * z + x * w), 1.0F - 2.0F * (x * x + y * y)};
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline auto multiply(const std::array<float, 9> &a, const std::array<float, 9> &b) -> std::array<float, 9> {
|
||||||
|
std::array<float, 9> out{};
|
||||||
|
for (int i = 0; i < 3; ++i) {
|
||||||
|
for (int j = 0; j < 3; ++j) {
|
||||||
|
out[static_cast<usize>(i) * 3U + static_cast<usize>(j)] = a[static_cast<usize>(i) * 3U] * b[static_cast<usize>(j)] +
|
||||||
|
a[static_cast<usize>(i) * 3U + 1U] * b[3U + static_cast<usize>(j)] +
|
||||||
|
a[static_cast<usize>(i) * 3U + 2U] * b[6U + static_cast<usize>(j)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline auto rotate(const std::array<float, 9> &m, const std::array<float, 3> &v) -> std::array<float, 3> {
|
||||||
|
return {m[0] * v[0] + m[1] * v[1] + m[2] * v[2], m[3] * v[0] + m[4] * v[1] + m[5] * v[2], m[6] * v[0] + m[7] * v[1] + m[8] * v[2]};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Each bone's world transform in the hierarchy's default (bind) pose. */
|
||||||
|
[[nodiscard]] inline auto bone_world_matrices(const hierarchy &h) -> std::vector<bone_matrix> {
|
||||||
|
std::vector<bone_matrix> world(h.bones.size());
|
||||||
|
for (usize i = 0; i < h.bones.size(); ++i) {
|
||||||
|
const auto &b = h.bones[i];
|
||||||
|
const auto local_rotation = detail::quaternion_matrix(b.qx, b.qy, b.qz, b.qw);
|
||||||
|
const std::array<float, 3> local_translation{b.tx, b.ty, b.tz};
|
||||||
|
if (b.parent >= 0 && static_cast<usize>(b.parent) < i) {
|
||||||
|
const auto &parent = world[static_cast<usize>(b.parent)];
|
||||||
|
world[i].rotation = detail::multiply(parent.rotation, local_rotation);
|
||||||
|
const auto rotated = detail::rotate(parent.rotation, local_translation);
|
||||||
|
world[i].translation = {parent.translation[0] + rotated[0], parent.translation[1] + rotated[1], parent.translation[2] + rotated[2]};
|
||||||
|
} else {
|
||||||
|
world[i].rotation = local_rotation;
|
||||||
|
world[i].translation = local_translation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return world;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace detail {
|
||||||
|
/** One vertex element: which attribute, its byte offset and its type code. */
|
||||||
|
struct vertex_element {
|
||||||
|
char usage = 0;
|
||||||
|
uint32 index = 0;
|
||||||
|
uint32 offset = 0;
|
||||||
|
std::string type;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] inline auto usage_of(char letter) -> bool {
|
||||||
|
return letter == 'p' || letter == 'n' || letter == 't' || letter == 'c' || letter == 'g' || letter == 'b' || letter == 'i' || letter == 'w';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse the D3D9 text vertex declaration `"p0:00:3f32 n0:0C:3f32 t0:1C:2f32"`. */
|
||||||
|
[[nodiscard]] inline auto parse_vertex_format(std::string_view decl) -> std::vector<vertex_element> {
|
||||||
|
std::vector<vertex_element> elements;
|
||||||
|
for (usize start = 0; start < decl.size();) {
|
||||||
|
const auto end = decl.find(' ', start);
|
||||||
|
const auto token = decl.substr(start, end == std::string_view::npos ? std::string_view::npos : end - start);
|
||||||
|
start = end == std::string_view::npos ? decl.size() : end + 1U;
|
||||||
|
const auto c1 = token.find(':');
|
||||||
|
if (c1 == std::string_view::npos) continue;
|
||||||
|
const auto c2 = token.find(':', c1 + 1U);
|
||||||
|
if (c2 == std::string_view::npos) continue;
|
||||||
|
const auto name = token.substr(0, c1);
|
||||||
|
if (name.empty() || !usage_of(name[0])) continue;
|
||||||
|
vertex_element element;
|
||||||
|
element.usage = name[0];
|
||||||
|
element.index = name.size() > 1U ? static_cast<uint32>(std::strtoul(std::string{name.substr(1)}.c_str(), nullptr, 10)) : 0U;
|
||||||
|
element.offset = static_cast<uint32>(std::strtoul(std::string{token.substr(c1 + 1U, c2 - c1 - 1U)}.c_str(), nullptr, 16));
|
||||||
|
element.type = std::string{token.substr(c2 + 1U)};
|
||||||
|
elements.push_back(std::move(element));
|
||||||
|
}
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Decode one vertex element (a small type grammar: `<count><kind>`). */
|
||||||
|
[[nodiscard]] inline auto decode_vertex_element(std::span<const uint8> buf, usize off, std::string_view type, float *out, uint32 wanted) -> uint32 {
|
||||||
|
usize digits = 0;
|
||||||
|
while (digits < type.size() && std::isdigit(static_cast<unsigned char>(type[digits]))) ++digits;
|
||||||
|
if (digits == 0U) return 0U;
|
||||||
|
const auto count = static_cast<uint32>(std::strtoul(std::string{type.substr(0, digits)}.c_str(), nullptr, 10));
|
||||||
|
const auto kind = type.substr(digits);
|
||||||
|
const auto take = std::min(count, wanted);
|
||||||
|
const auto bytes_per_element = (kind == "f32") ? 4U : ((kind == "u8n" || kind == "u8") ? 1U : 2U);
|
||||||
|
const auto lerp = [&](uint32 i, float value) { out[i] = value; };
|
||||||
|
for (uint32 i = 0; i < take; ++i) {
|
||||||
|
const auto at = off + static_cast<usize>(i) * bytes_per_element;
|
||||||
|
if (kind == "f32") {
|
||||||
|
lerp(i, f32(buf, off + static_cast<usize>(i) * 4U));
|
||||||
|
} else if (kind == "u8n") {
|
||||||
|
lerp(i, at < buf.size() ? static_cast<float>(buf[at]) / 255.0F : 0.0F);
|
||||||
|
} else if (kind == "u8") {
|
||||||
|
lerp(i, at < buf.size() ? static_cast<float>(buf[at]) : 0.0F);
|
||||||
|
} else if (kind == "s16n") {
|
||||||
|
lerp(i, at + 2U <= buf.size() ? static_cast<float>(static_cast<std::int16_t>(u16(buf, at))) / 32767.0F : 0.0F);
|
||||||
|
} else if (kind == "u16n") {
|
||||||
|
lerp(i, at + 2U <= buf.size() ? static_cast<float>(u16(buf, at)) / 65535.0F : 0.0F);
|
||||||
|
} else if (kind == "s16") {
|
||||||
|
lerp(i, at + 2U <= buf.size() ? static_cast<float>(static_cast<std::int16_t>(u16(buf, at))) : 0.0F);
|
||||||
|
} else if (kind == "u16") {
|
||||||
|
lerp(i, at + 2U <= buf.size() ? static_cast<float>(u16(buf, at)) : 0.0F);
|
||||||
|
} else if (kind == "f16") {
|
||||||
|
const auto raw = at + 2U <= buf.size() ? u16(buf, at) : 0U;
|
||||||
|
const auto sign = (raw & 0x8000U) != 0U ? -1.0F : 1.0F;
|
||||||
|
const auto exp = (raw >> 10U) & 0x1FU;
|
||||||
|
const auto mant = raw & 0x3FFU;
|
||||||
|
float value = exp == 0U ? static_cast<float>(mant) / 1024.0F * std::pow(2.0F, -14.0F)
|
||||||
|
: (1.0F + static_cast<float>(mant) / 1024.0F) * std::pow(2.0F, static_cast<float>(exp) - 15.0F);
|
||||||
|
lerp(i, sign * value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return take;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a compiled `W3DMesh` instance.
|
||||||
|
*
|
||||||
|
* The compiled struct stores a GPU vertex buffer (declaration string +
|
||||||
|
* packed vertices) and a triangle list; every pointer is an offset from the
|
||||||
|
* instance start, so the payload is read in place.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] inline auto decode_mesh(std::span<const uint8> data) -> mesh {
|
||||||
|
mesh out;
|
||||||
|
const auto vertex_off = detail::u32(data, 4U);
|
||||||
|
const auto triangle_count = detail::u32(data, 52U);
|
||||||
|
const auto triangle_items = detail::u32(data, 56U);
|
||||||
|
if (vertex_off == 0U || vertex_off + 20U > data.size()) return out;
|
||||||
|
|
||||||
|
const auto vertex_count = detail::u32(data, vertex_off);
|
||||||
|
const auto stride = detail::u32(data, vertex_off + 4U);
|
||||||
|
const auto element_items = detail::u32(data, vertex_off + 8U);
|
||||||
|
const auto decl_bytes = detail::u32(data, vertex_off + 12U);
|
||||||
|
const auto decl_items = detail::u32(data, vertex_off + 16U);
|
||||||
|
if (vertex_count == 0U || stride == 0U || decl_items + decl_bytes > data.size()) return out;
|
||||||
|
const std::string_view decl{reinterpret_cast<const char *>(data.data() + decl_items), decl_bytes};
|
||||||
|
const auto elements = detail::parse_vertex_format(decl);
|
||||||
|
|
||||||
|
const auto shader_len = detail::i32(data, 60U);
|
||||||
|
const auto shader_ptr = detail::u32(data, 64U);
|
||||||
|
if (shader_len > 0 && shader_ptr != 0U && shader_ptr + static_cast<usize>(shader_len) <= data.size()) {
|
||||||
|
out.shader.assign(reinterpret_cast<const char *>(data.data() + shader_ptr), static_cast<usize>(shader_len));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-mesh bone remap: a vertex's blend index selects an entry here, and
|
||||||
|
// that entry is the hierarchy bone. Without it the wrong bones are used
|
||||||
|
// and skinned models tear apart.
|
||||||
|
const auto bone_count = detail::u32(data, vertex_off + 0x14U);
|
||||||
|
const auto bone_ptr = detail::u32(data, vertex_off + 0x18U);
|
||||||
|
if (bone_count > 0U && bone_count < 4096U && bone_ptr != 0U && bone_ptr + static_cast<usize>(bone_count) * 2U <= data.size()) {
|
||||||
|
out.bone_remap.resize(bone_count);
|
||||||
|
for (uint32 i = 0; i < bone_count; ++i) out.bone_remap[i] = detail::u16(data, bone_ptr + static_cast<usize>(i) * 2U);
|
||||||
|
}
|
||||||
|
|
||||||
|
out.positions.assign(static_cast<usize>(vertex_count) * 3U, 0.0F);
|
||||||
|
std::vector<float> normals(static_cast<usize>(vertex_count) * 3U, 0.0F);
|
||||||
|
std::vector<float> uvs(static_cast<usize>(vertex_count) * 2U, 0.0F);
|
||||||
|
std::vector<uint8> blend_indices(static_cast<usize>(vertex_count) * 4U, 0U);
|
||||||
|
std::vector<float> blend_weights(static_cast<usize>(vertex_count) * 4U, 0.0F);
|
||||||
|
bool have_normals = false;
|
||||||
|
bool have_uvs = false;
|
||||||
|
bool have_blend = false;
|
||||||
|
for (uint32 i = 0; i < vertex_count; ++i) {
|
||||||
|
const auto base = static_cast<usize>(element_items) + static_cast<usize>(i) * stride;
|
||||||
|
float normal[3] = {0.0F, 0.0F, 1.0F};
|
||||||
|
float uv[2] = {0.0F, 0.0F};
|
||||||
|
for (const auto &element: elements) {
|
||||||
|
float value[4] = {0.0F, 0.0F, 0.0F, 0.0F};
|
||||||
|
if (element.usage == 'p' && element.index == 0U) {
|
||||||
|
if (detail::decode_vertex_element(data, base + element.offset, element.type, value, 3U) >= 3U) {
|
||||||
|
out.positions[static_cast<usize>(i) * 3U + 0U] = value[0];
|
||||||
|
out.positions[static_cast<usize>(i) * 3U + 1U] = value[1];
|
||||||
|
out.positions[static_cast<usize>(i) * 3U + 2U] = value[2];
|
||||||
|
}
|
||||||
|
} else if (element.usage == 'n' && element.index == 0U) {
|
||||||
|
if (detail::decode_vertex_element(data, base + element.offset, element.type, value, 3U) >= 3U) {
|
||||||
|
normal[0] = value[0];
|
||||||
|
normal[1] = value[1];
|
||||||
|
normal[2] = value[2];
|
||||||
|
have_normals = true;
|
||||||
|
}
|
||||||
|
} else if (element.usage == 't' && element.index == 0U) {
|
||||||
|
if (detail::decode_vertex_element(data, base + element.offset, element.type, value, 2U) >= 2U) {
|
||||||
|
uv[0] = value[0];
|
||||||
|
uv[1] = value[1];
|
||||||
|
have_uvs = true;
|
||||||
|
}
|
||||||
|
} else if (element.usage == 'i' && element.index == 0U) {
|
||||||
|
if (detail::decode_vertex_element(data, base + element.offset, element.type, value, 4U) >= 4U) {
|
||||||
|
for (usize k = 0; k < 4U; ++k) blend_indices[static_cast<usize>(i) * 4U + k] = static_cast<uint8>(std::clamp(value[k], 0.0F, 255.0F));
|
||||||
|
have_blend = true;
|
||||||
|
}
|
||||||
|
} else if (element.usage == 'w' && element.index == 0U) {
|
||||||
|
if (detail::decode_vertex_element(data, base + element.offset, element.type, value, 4U) >= 4U) {
|
||||||
|
for (usize k = 0; k < 4U; ++k) blend_weights[static_cast<usize>(i) * 4U + k] = value[k];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
normals[static_cast<usize>(i) * 3U + 0U] = normal[0];
|
||||||
|
normals[static_cast<usize>(i) * 3U + 1U] = normal[1];
|
||||||
|
normals[static_cast<usize>(i) * 3U + 2U] = normal[2];
|
||||||
|
uvs[static_cast<usize>(i) * 2U + 0U] = uv[0];
|
||||||
|
uvs[static_cast<usize>(i) * 2U + 1U] = uv[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint32 i = 0; i < triangle_count; ++i) {
|
||||||
|
const auto base = static_cast<usize>(triangle_items) + static_cast<usize>(i) * 24U;
|
||||||
|
const auto count = detail::u32(data, base);
|
||||||
|
const auto ptr = detail::u32(data, base + 4U);
|
||||||
|
if (count == 0U || ptr == 0U || ptr + static_cast<usize>(count) * 4U > data.size()) continue;
|
||||||
|
std::vector<uint32> ring(count);
|
||||||
|
for (uint32 k = 0; k < count; ++k) ring[k] = detail::u32(data, ptr + static_cast<usize>(k) * 4U);
|
||||||
|
for (uint32 k = 1; k + 1U < count; ++k) { // fan any n-gon
|
||||||
|
out.indices.push_back(ring[0]);
|
||||||
|
out.indices.push_back(ring[k]);
|
||||||
|
out.indices.push_back(ring[k + 1U]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (have_normals) out.normals = std::move(normals);
|
||||||
|
if (have_uvs) out.uvs = std::move(uvs);
|
||||||
|
if (have_blend) {
|
||||||
|
out.blend_indices = std::move(blend_indices);
|
||||||
|
out.blend_weights = std::move(blend_weights);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- DDS --------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace detail {
|
||||||
|
inline auto decode_dxt_color(std::span<const uint8> block, usize offset, std::vector<uint32> &out, uint32 w, uint32 h, uint32 x, uint32 y, bool force_four) -> void {
|
||||||
|
const auto c0 = static_cast<uint32>(block[offset]) | (static_cast<uint32>(block[offset + 1U]) << 8U);
|
||||||
|
const auto c1 = static_cast<uint32>(block[offset + 2U]) | (static_cast<uint32>(block[offset + 3U]) << 8U);
|
||||||
|
const auto hilo = [](uint32 c) -> std::array<uint8, 3> {
|
||||||
|
return {static_cast<uint8>(((c >> 11U) & 0x1FU) * 255U / 31U), static_cast<uint8>(((c >> 5U) & 0x3FU) * 255U / 63U),
|
||||||
|
static_cast<uint8>((c & 0x1FU) * 255U / 31U)};
|
||||||
|
};
|
||||||
|
const auto a = hilo(c0);
|
||||||
|
const auto b = hilo(c1);
|
||||||
|
std::array<uint32, 4> colors{};
|
||||||
|
colors[0] = argb(a[0], a[1], a[2]);
|
||||||
|
colors[1] = argb(b[0], b[1], b[2]);
|
||||||
|
if (c0 > c1 || force_four) {
|
||||||
|
colors[2] = argb(static_cast<uint8>((2U * a[0] + b[0]) / 3U), static_cast<uint8>((2U * a[1] + b[1]) / 3U), static_cast<uint8>((2U * a[2] + b[2]) / 3U));
|
||||||
|
colors[3] = argb(static_cast<uint8>((a[0] + 2U * b[0]) / 3U), static_cast<uint8>((a[1] + 2U * b[1]) / 3U), static_cast<uint8>((a[2] + 2U * b[2]) / 3U));
|
||||||
|
} else {
|
||||||
|
colors[2] = argb(static_cast<uint8>((a[0] + b[0]) / 2U), static_cast<uint8>((a[1] + b[1]) / 2U), static_cast<uint8>((a[2] + b[2]) / 2U));
|
||||||
|
colors[3] = 0x00000000U;
|
||||||
|
}
|
||||||
|
uint32 indices = 0;
|
||||||
|
for (uint32 k = 0; k < 4U; ++k) indices |= static_cast<uint32>(block[offset + 4U + k]) << (8U * k);
|
||||||
|
for (uint32 py = 0; py < 4U; ++py) {
|
||||||
|
for (uint32 px = 0; px < 4U; ++px) {
|
||||||
|
if (x + px >= w || y + py >= h) continue;
|
||||||
|
out[static_cast<usize>(y + py) * w + (x + px)] = colors[(indices >> (2U * (py * 4U + px))) & 3U];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Decode a DDS (DXT1/3/5 or uncompressed RGB) to an ARGB image; empty when unsupported. */
|
||||||
|
[[nodiscard]] inline auto decode_dds(std::span<const uint8> data) -> image {
|
||||||
|
if (data.size() < 128U || std::memcmp(data.data(), "DDS ", 4) != 0) return {};
|
||||||
|
const auto height = detail::u32(data, 12U);
|
||||||
|
const auto width = detail::u32(data, 16U);
|
||||||
|
const auto pfflags = detail::u32(data, 80U);
|
||||||
|
const auto bits = detail::u32(data, 88U);
|
||||||
|
const auto rmask = detail::u32(data, 92U);
|
||||||
|
const auto gmask = detail::u32(data, 96U);
|
||||||
|
const auto bmask = detail::u32(data, 100U);
|
||||||
|
const auto amask = detail::u32(data, 104U);
|
||||||
|
if (width == 0U || height == 0U || width > 8192U || height > 8192U) return {};
|
||||||
|
const auto fourcc = std::string_view{reinterpret_cast<const char *>(data.data() + 84U), 4U};
|
||||||
|
std::vector<uint32> pixels(static_cast<usize>(width) * height, 0xFF000000U);
|
||||||
|
|
||||||
|
if (fourcc == "DXT1" || fourcc == "DXT3" || fourcc == "DXT5") {
|
||||||
|
const auto stride = fourcc == "DXT1" ? 8U : 16U;
|
||||||
|
usize pos = 128U;
|
||||||
|
const auto blocks_x = (width + 3U) / 4U;
|
||||||
|
const auto blocks_y = (height + 3U) / 4U;
|
||||||
|
for (uint32 by = 0; by < blocks_y; ++by) {
|
||||||
|
for (uint32 bx = 0; bx < blocks_x; ++bx) {
|
||||||
|
if (pos + stride > data.size()) goto dds_done; // truncated: keep what we decoded
|
||||||
|
const auto block = data.subspan(pos, stride);
|
||||||
|
pos += stride;
|
||||||
|
const uint32 x = bx * 4U;
|
||||||
|
const uint32 y = by * 4U;
|
||||||
|
if (fourcc == "DXT1") {
|
||||||
|
detail::decode_dxt_color(block, 0U, pixels, width, height, x, y, false);
|
||||||
|
} else {
|
||||||
|
if (fourcc == "DXT3") {
|
||||||
|
for (uint32 py = 0; py < 4U; ++py) {
|
||||||
|
for (uint32 px = 0; px < 4U; ++px) {
|
||||||
|
const auto i = py * 4U + px;
|
||||||
|
const auto byte = block[i / 2U];
|
||||||
|
const auto alpha = static_cast<uint8>((i % 2U == 0U) ? (byte & 0x0FU) : (byte >> 4U));
|
||||||
|
const auto at = static_cast<usize>(y + py) * width + (x + px);
|
||||||
|
if (y + py < height && x + px < width) pixels[at] = (pixels[at] & 0x00FFFFFFU) | (static_cast<uint32>(alpha) * 17U << 24U);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { // DXT5
|
||||||
|
const auto a0 = block[0];
|
||||||
|
const auto a1 = block[1];
|
||||||
|
std::array<uint8, 8> palette{};
|
||||||
|
palette[0] = a0;
|
||||||
|
palette[1] = a1;
|
||||||
|
if (a0 > a1) {
|
||||||
|
for (uint32 i = 1U; i < 7U; ++i) palette[i + 1U] = static_cast<uint8>(((7U - i) * a0 + i * a1) / 7U);
|
||||||
|
} else {
|
||||||
|
for (uint32 i = 1U; i < 5U; ++i) palette[i + 1U] = static_cast<uint8>(((5U - i) * a0 + i * a1) / 5U);
|
||||||
|
palette[6] = 0U;
|
||||||
|
palette[7] = 255U;
|
||||||
|
}
|
||||||
|
std::uint64_t abits = 0;
|
||||||
|
for (uint32 k = 0; k < 6U; ++k) abits |= static_cast<std::uint64_t>(block[2U + k]) << (8U * k);
|
||||||
|
for (uint32 py = 0; py < 4U; ++py) {
|
||||||
|
for (uint32 px = 0; px < 4U; ++px) {
|
||||||
|
const auto i = py * 4U + px;
|
||||||
|
const auto alpha = palette[(abits >> (3U * i)) & 7U];
|
||||||
|
const auto at = static_cast<usize>(y + py) * width + (x + px);
|
||||||
|
if (y + py < height && x + px < width) pixels[at] = (pixels[at] & 0x00FFFFFFU) | (static_cast<uint32>(alpha) << 24U);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
detail::decode_dxt_color(block, 8U, pixels, width, height, x, y, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ((pfflags & 0x40U) != 0U && (bits == 16U || bits == 24U || bits == 32U)) {
|
||||||
|
const auto shift_of = [](uint32 mask) -> std::pair<uint32, uint32> {
|
||||||
|
if (mask == 0U) return {0U, 0U};
|
||||||
|
uint32 shift = 0;
|
||||||
|
while (((mask >> shift) & 1U) == 0U) ++shift;
|
||||||
|
uint32 size = 0;
|
||||||
|
while (((mask >> (shift + size)) & 1U) != 0U) ++size;
|
||||||
|
return {shift, size};
|
||||||
|
};
|
||||||
|
const auto channels = {shift_of(rmask), shift_of(gmask), shift_of(bmask), shift_of(amask)};
|
||||||
|
const auto bpp = bits / 8U;
|
||||||
|
const auto *raw = data.data() + 128U;
|
||||||
|
const auto available = data.size() > 128U ? (data.size() - 128U) / bpp : 0U;
|
||||||
|
for (usize i = 0; i < static_cast<usize>(width) * height && i < available; ++i) {
|
||||||
|
uint32 pixel = 0;
|
||||||
|
for (uint32 b = 0; b < bpp; ++b) pixel |= static_cast<uint32>(raw[i * bpp + b]) << (8U * b);
|
||||||
|
uint32 out = 0xFF000000U;
|
||||||
|
uint32 channel = 0;
|
||||||
|
for (const auto [shift, size]: channels) {
|
||||||
|
const auto maxv = size != 0U ? ((1U << size) - 1U) : 0U;
|
||||||
|
const auto value = maxv != 0U ? (((pixel >> shift) & maxv) * 255U / maxv) : 255U;
|
||||||
|
if (channel < 3U) {
|
||||||
|
out |= value << (16U - channel * 8U);
|
||||||
|
} else if (size != 0U) {
|
||||||
|
out = (out & 0x00FFFFFFU) | (value << 24U);
|
||||||
|
}
|
||||||
|
++channel;
|
||||||
|
}
|
||||||
|
pixels[i] = out;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
dds_done:;
|
||||||
|
image result(width, height);
|
||||||
|
std::copy(pixels.begin(), pixels.end(), result.data());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Decode the DDS embedded in a compiled `Texture` instance; empty when none. */
|
||||||
|
[[nodiscard]] inline auto decode_texture(std::span<const uint8> instance) -> image {
|
||||||
|
const auto off = detail::u32(instance, 4U);
|
||||||
|
std::span<const uint8> dds{};
|
||||||
|
if (off > 0U && off < instance.size() && off + 4U <= instance.size() && std::memcmp(instance.data() + off, "DDS ", 4) == 0) {
|
||||||
|
dds = instance.subspan(off);
|
||||||
|
} else {
|
||||||
|
for (usize i = 0; i + 4U <= instance.size(); ++i) {
|
||||||
|
if (std::memcmp(instance.data() + i, "DDS ", 4) == 0) {
|
||||||
|
dds = instance.subspan(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dds.empty() ? image{} : decode_dds(dds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The diffuse texture a mesh samples, or `nullptr`.
|
||||||
|
*
|
||||||
|
* A `W3DMesh` stores `FXShaderConstant`s at `+76` (count) / `+80` (items);
|
||||||
|
* a texture-valued constant (`0xA59096A6`) names its role and points at a
|
||||||
|
* 1-based index into the mesh's cross-asset references.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] inline auto mesh_diffuse_texture(const asset_stream &stream, const asset_stream::asset &mesh_asset) -> const asset_stream::asset * {
|
||||||
|
const auto instance = stream.read_instance(mesh_asset);
|
||||||
|
if (instance.empty()) return nullptr;
|
||||||
|
const auto count = detail::u32(instance, 76U);
|
||||||
|
const auto items = detail::u32(instance, 80U);
|
||||||
|
for (uint32 i = 0; i < count; ++i) {
|
||||||
|
const auto constant = detail::u32(instance, items + static_cast<usize>(i) * 4U);
|
||||||
|
if (constant == 0U || detail::u32(instance, constant) != 0xA59096A6U) continue;
|
||||||
|
const auto name = asset_stream::lower(detail::cstr(instance, detail::u32(instance, constant + 8U), detail::i32(instance, constant + 4U)));
|
||||||
|
const bool is_diffuse = name.contains("diffuse") || name.contains("albedo") || name.contains("base") || name == "texture_0" || name == "texture0";
|
||||||
|
if (!is_diffuse) continue;
|
||||||
|
const auto reference = detail::u32(instance, constant + 12U);
|
||||||
|
if (reference >= 1U && reference <= mesh_asset.references.size()) {
|
||||||
|
const auto [type_id, instance_id] = mesh_asset.references[reference - 1U];
|
||||||
|
if (const auto *texture = stream.find(type_id, instance_id); texture != nullptr) return texture;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- scene ------------------------------------------------------------
|
||||||
|
|
||||||
|
/** A world-space triangle-soup vertex (position, normal, uv, texture layer). */
|
||||||
|
struct vertex {
|
||||||
|
float x = 0.0F;
|
||||||
|
float y = 0.0F;
|
||||||
|
float z = 0.0F;
|
||||||
|
float nx = 0.0F;
|
||||||
|
float ny = 0.0F;
|
||||||
|
float nz = 1.0F;
|
||||||
|
float u = 0.0F;
|
||||||
|
float v = 0.0F;
|
||||||
|
float layer = 0.0F;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Every model the map draws, flattened to world space and ready to upload. */
|
||||||
|
struct scene {
|
||||||
|
std::vector<vertex> vertices;
|
||||||
|
std::vector<uint32> indices;
|
||||||
|
std::vector<image> textures; ///< All `texture_size` square, ARGB.
|
||||||
|
uint32 texture_size = 0;
|
||||||
|
usize placed = 0;
|
||||||
|
usize missing = 0;
|
||||||
|
|
||||||
|
[[nodiscard]] auto empty() const -> bool { return indices.empty(); }
|
||||||
|
[[nodiscard]] auto triangle_count() const -> usize { return indices.size() / 3U; }
|
||||||
|
};
|
||||||
|
|
||||||
|
/** One placement request: an object type at a world position with a Z rotation. */
|
||||||
|
struct placement {
|
||||||
|
std::string type;
|
||||||
|
float x = 0.0F;
|
||||||
|
float y = 0.0F;
|
||||||
|
float z = 0.0F;
|
||||||
|
float angle = 0.0F;
|
||||||
|
float scale = 1.0F;
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace detail {
|
||||||
|
/** Box-downscale `src` to a `size` square (averages, so atlas detail survives). */
|
||||||
|
[[nodiscard]] inline auto to_square(const image &src, uint32 size) -> image {
|
||||||
|
image out(size, size);
|
||||||
|
if (src.empty() || size == 0U) return out;
|
||||||
|
for (uint32 y = 0; y < size; ++y) {
|
||||||
|
const auto y0 = y * src.height() / size;
|
||||||
|
const auto y1 = std::max(y0 + 1U, (y + 1U) * src.height() / size);
|
||||||
|
for (uint32 x = 0; x < size; ++x) {
|
||||||
|
const auto x0 = x * src.width() / size;
|
||||||
|
const auto x1 = std::max(x0 + 1U, (x + 1U) * src.width() / size);
|
||||||
|
uint32 r = 0;
|
||||||
|
uint32 g = 0;
|
||||||
|
uint32 b = 0;
|
||||||
|
uint32 a = 0;
|
||||||
|
uint32 count = 0;
|
||||||
|
for (uint32 sy = y0; sy < y1; ++sy) {
|
||||||
|
for (uint32 sx = x0; sx < x1; ++sx) {
|
||||||
|
const auto texel = src.data()[static_cast<usize>(sy) * src.width() + sx];
|
||||||
|
r += (texel >> 16U) & 0xFFU;
|
||||||
|
g += (texel >> 8U) & 0xFFU;
|
||||||
|
b += texel & 0xFFU;
|
||||||
|
a += (texel >> 24U) & 0xFFU;
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.data()[static_cast<usize>(y) * size + x] =
|
||||||
|
argb(static_cast<uint8>(r / count), static_cast<uint8>(g / count), static_cast<uint8>(b / count), static_cast<uint8>(a / count));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flatten every placement into one world-space scene.
|
||||||
|
*
|
||||||
|
* @param stream The map's compiled art stream.
|
||||||
|
* @param placements The objects to draw.
|
||||||
|
* @param ground_height Terrain height (world Z) at a world `(x, y)`, so
|
||||||
|
* objects sit on the relief instead of a flat plane.
|
||||||
|
* @param texture_size Edge length of the shared texture array (0 = auto).
|
||||||
|
*/
|
||||||
|
[[nodiscard]] inline auto build_scene(const asset_stream &stream, std::span<const placement> placements,
|
||||||
|
const std::function<float(float, float)> &ground_height = {}, uint32 texture_size = 128U) -> scene {
|
||||||
|
scene out;
|
||||||
|
out.texture_size = texture_size == 0U ? 128U : texture_size;
|
||||||
|
std::unordered_map<std::string, uint32> texture_layers;
|
||||||
|
|
||||||
|
const auto layer_for = [&](const asset_stream::asset *texture) -> uint32 {
|
||||||
|
if (texture == nullptr) return 0xFFFFFFFFU;
|
||||||
|
const auto key = asset_stream::lower(texture->name);
|
||||||
|
if (const auto it = texture_layers.find(key); it != texture_layers.end()) return it->second;
|
||||||
|
auto decoded = decode_texture(stream.read_instance(*texture));
|
||||||
|
if (decoded.empty()) return 0xFFFFFFFFU;
|
||||||
|
const auto layer = static_cast<uint32>(out.textures.size());
|
||||||
|
out.textures.push_back(detail::to_square(decoded, out.texture_size));
|
||||||
|
texture_layers.emplace(key, layer);
|
||||||
|
return layer;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bone world transforms per skeleton, cached by hierarchy name. Meshes are
|
||||||
|
// skinned; their vertices are stored in *bone space*, so a static render
|
||||||
|
// must rebuild the default pose and blend.
|
||||||
|
std::unordered_map<std::string, std::vector<bone_matrix>> skeletons;
|
||||||
|
const auto skeleton_for = [&](std::string_view mesh_instance) -> const std::vector<bone_matrix> * {
|
||||||
|
const auto dot = mesh_instance.find('.');
|
||||||
|
const auto base = std::string{mesh_instance.substr(0, dot)};
|
||||||
|
const auto key = asset_stream::lower(base);
|
||||||
|
if (const auto it = skeletons.find(key); it != skeletons.end()) return it->second.empty() ? nullptr : &it->second;
|
||||||
|
std::vector<bone_matrix> matrices;
|
||||||
|
if (const auto *asset = stream.find_name("W3DHierarchy:" + base); asset != nullptr) {
|
||||||
|
matrices = bone_world_matrices(decode_hierarchy(stream.read_instance(*asset)));
|
||||||
|
}
|
||||||
|
const auto it = skeletons.emplace(key, std::move(matrices)).first;
|
||||||
|
return it->second.empty() ? nullptr : &it->second;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto &item: placements) {
|
||||||
|
const auto meshes = stream.meshes_for(item.type);
|
||||||
|
if (meshes.empty()) {
|
||||||
|
++out.missing;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto cos_a = std::cos(item.angle);
|
||||||
|
const auto sin_a = std::sin(item.angle);
|
||||||
|
const auto base_z = (ground_height ? ground_height(item.x, item.y) : 0.0F) + item.z;
|
||||||
|
bool drawn = false;
|
||||||
|
for (const auto *mesh_asset: meshes) {
|
||||||
|
// Only opaque material parts are drawn; meshes with no diffuse
|
||||||
|
// role are effects (FX-light billboards, `DefaultW3D.fx`) or
|
||||||
|
// ambient helpers and would otherwise render as garbage.
|
||||||
|
const auto *diffuse = mesh_diffuse_texture(stream, *mesh_asset);
|
||||||
|
if (diffuse == nullptr) continue;
|
||||||
|
const auto geometry = decode_mesh(stream.read_instance(*mesh_asset));
|
||||||
|
if (geometry.indices.empty()) continue;
|
||||||
|
// Damage-fill shells (damaged interior/wreckage) are drawn on top
|
||||||
|
// of the main shell in our opaque pass and would show its
|
||||||
|
// interior texture (e.g. orange `CBBuilding_Wood`); skip them.
|
||||||
|
if (asset_stream::lower(geometry.shader).contains("damagefill")) continue;
|
||||||
|
drawn = true;
|
||||||
|
const auto layer = layer_for(diffuse);
|
||||||
|
const auto *skeleton = geometry.skinned() ? skeleton_for(mesh_asset->instance_name()) : nullptr;
|
||||||
|
const auto base_vertex = static_cast<uint32>(out.vertices.size());
|
||||||
|
const auto vertex_count = geometry.positions.size() / 3U;
|
||||||
|
for (usize i = 0; i < vertex_count; ++i) {
|
||||||
|
auto px = geometry.positions[i * 3U + 0U];
|
||||||
|
auto py = geometry.positions[i * 3U + 1U];
|
||||||
|
auto pz = geometry.positions[i * 3U + 2U];
|
||||||
|
auto nx = geometry.normals.empty() ? 0.0F : geometry.normals[i * 3U + 0U];
|
||||||
|
auto ny = geometry.normals.empty() ? 0.0F : geometry.normals[i * 3U + 1U];
|
||||||
|
auto nz = geometry.normals.empty() ? 1.0F : geometry.normals[i * 3U + 2U];
|
||||||
|
|
||||||
|
if (skeleton != nullptr) {
|
||||||
|
// The compiled shader binds one joint per vertex
|
||||||
|
// (`blendindices.x`), remapped through the mesh's bone
|
||||||
|
// table, then transforms by the joint's world matrix.
|
||||||
|
auto joint = static_cast<uint32>(geometry.blend_indices[i * 4U + 0U]);
|
||||||
|
if (joint < geometry.bone_remap.size()) joint = geometry.bone_remap[joint];
|
||||||
|
if (joint < skeleton->size()) {
|
||||||
|
const auto &m = (*skeleton)[joint];
|
||||||
|
const std::array<float, 3> p{px, py, pz};
|
||||||
|
const auto rp = detail::rotate(m.rotation, p);
|
||||||
|
const std::array<float, 3> n{nx, ny, nz};
|
||||||
|
const auto rn = detail::rotate(m.rotation, n);
|
||||||
|
px = rp[0] + m.translation[0];
|
||||||
|
py = rp[1] + m.translation[1];
|
||||||
|
pz = rp[2] + m.translation[2];
|
||||||
|
nx = rn[0];
|
||||||
|
ny = rn[1];
|
||||||
|
nz = rn[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vertex v;
|
||||||
|
v.x = item.x + (px * item.scale) * cos_a - (py * item.scale) * sin_a;
|
||||||
|
v.y = item.y + (px * item.scale) * sin_a + (py * item.scale) * cos_a;
|
||||||
|
v.z = base_z + pz * item.scale;
|
||||||
|
v.nx = nx * cos_a - ny * sin_a;
|
||||||
|
v.ny = nx * sin_a + ny * cos_a;
|
||||||
|
v.nz = nz;
|
||||||
|
if (!geometry.uvs.empty()) {
|
||||||
|
v.u = geometry.uvs[i * 2U + 0U];
|
||||||
|
v.v = geometry.uvs[i * 2U + 1U];
|
||||||
|
}
|
||||||
|
v.layer = layer == 0xFFFFFFFFU ? 0.0F : static_cast<float>(layer);
|
||||||
|
out.vertices.push_back(v);
|
||||||
|
}
|
||||||
|
for (const auto index: geometry.indices) out.indices.push_back(base_vertex + index);
|
||||||
|
}
|
||||||
|
if (drawn) {
|
||||||
|
++out.placed;
|
||||||
|
} else {
|
||||||
|
++out.missing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ export import ra3.map;
|
|||||||
export import ra3.skirmish;
|
export import ra3.skirmish;
|
||||||
export import ra3.render;
|
export import ra3.render;
|
||||||
export import ra3.terrain;
|
export import ra3.terrain;
|
||||||
|
export import ra3.models;
|
||||||
export import ra3.ui;
|
export import ra3.ui;
|
||||||
export import ra3.vulkan;
|
export import ra3.vulkan;
|
||||||
export import ra3.dx;
|
export import ra3.dx;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import std;
|
|||||||
export import ra3.core;
|
export import ra3.core;
|
||||||
export import ra3.fs;
|
export import ra3.fs;
|
||||||
export import ra3.render;
|
export import ra3.render;
|
||||||
|
export import ra3.models;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The map's real terrain, read from the compiled `CkMp` chunk tree.
|
* The map's real terrain, read from the compiled `CkMp` chunk tree.
|
||||||
@@ -481,6 +482,7 @@ export namespace ra3::terrain {
|
|||||||
float z_scale = 0.0390625F;
|
float z_scale = 0.0390625F;
|
||||||
bool has_water = false;
|
bool has_water = false;
|
||||||
float water_z = 0.0F;
|
float water_z = 0.0F;
|
||||||
|
ra3::models::scene objects; ///< Buildings and props placed on the map (world-space triangle soup).
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -492,14 +494,15 @@ export namespace ra3::terrain {
|
|||||||
* base texture layer, the blend (and three-way) secondary layer and the
|
* base texture layer, the blend (and three-way) secondary layer and the
|
||||||
* packed blend direction/flags, which the shader ramps across the cell.
|
* packed blend direction/flags, which the shader ramps across the cell.
|
||||||
*/
|
*/
|
||||||
[[nodiscard]] inline auto build_gpu_terrain(const map_data &map, const texture_set &set, const render_options &options = {},
|
[[nodiscard]] inline auto build_gpu_terrain(const map_data &map, const texture_set &set, const render_options &options,
|
||||||
const std::function<void(float)> &progress = {}) -> gpu_terrain {
|
const ra3::models::scene &objects, const std::function<void(float)> &progress = {}) -> gpu_terrain {
|
||||||
gpu_terrain out;
|
gpu_terrain out;
|
||||||
out.width = map.width;
|
out.width = map.width;
|
||||||
out.height = map.height;
|
out.height = map.height;
|
||||||
out.z_scale = options.z_scale;
|
out.z_scale = options.z_scale;
|
||||||
out.has_water = map.has_water;
|
out.has_water = map.has_water;
|
||||||
out.water_z = map.water_plane_z;
|
out.water_z = map.water_plane_z;
|
||||||
|
out.objects = objects;
|
||||||
|
|
||||||
// Cell index -> texture layer.
|
// Cell index -> texture layer.
|
||||||
const auto layer_of = [&](uint32 cell_index) -> uint16 {
|
const auto layer_of = [&](uint32 cell_index) -> uint16 {
|
||||||
@@ -566,6 +569,12 @@ export namespace ra3::terrain {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Terrain without any placed objects. */
|
||||||
|
[[nodiscard]] inline auto build_gpu_terrain(const map_data &map, const texture_set &set, const render_options &options = {},
|
||||||
|
const std::function<void(float)> &progress = {}) -> gpu_terrain {
|
||||||
|
return build_gpu_terrain(map, set, options, ra3::models::scene{}, progress);
|
||||||
|
}
|
||||||
|
|
||||||
namespace detail {
|
namespace detail {
|
||||||
/**
|
/**
|
||||||
* The source texture a tile cell maps to. The texture is sampled
|
* The source texture a tile cell maps to. The texture is sampled
|
||||||
@@ -721,6 +730,144 @@ export namespace ra3::terrain {
|
|||||||
const auto c2 = f2 > 0.0F ? sample_layer(layers.three, wx, wy) : c0;
|
const auto c2 = f2 > 0.0F ? sample_layer(layers.three, wx, wy) : c0;
|
||||||
return mix_color(mix_color(c0, c1, f1), c2, f2);
|
return mix_color(mix_color(c0, c1, f1), c2, f2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rasterise the map's building/prop scene over an already ray-marched
|
||||||
|
* terrain image, depth-testing against it.
|
||||||
|
*
|
||||||
|
* The camera basis is the one `render3d` used, so the two passes agree;
|
||||||
|
* `zbuf` holds the terrain's view-space depth per pixel (large where the
|
||||||
|
* ray hit nothing). Triangles are z-tested and perspective-correct.
|
||||||
|
*/
|
||||||
|
inline auto rasterize_objects(image &hi, std::vector<float> &zbuf, const ra3::models::scene &scene, const std::array<float, 3> &cam,
|
||||||
|
const std::array<float, 3> &f, const std::array<float, 3> &r, const std::array<float, 3> &u, float tan_half,
|
||||||
|
float aspect) -> void {
|
||||||
|
const auto rw = static_cast<int>(hi.width());
|
||||||
|
const auto rh = static_cast<int>(hi.height());
|
||||||
|
if (rw <= 0 || rh <= 0) return;
|
||||||
|
const auto dot3 = [](const std::array<float, 3> &a, const std::array<float, 3> &b) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; };
|
||||||
|
float sun[3] = {0.45F, 0.35F, 0.82F};
|
||||||
|
const auto sl = std::sqrt(sun[0] * sun[0] + sun[1] * sun[1] + sun[2] * sun[2]);
|
||||||
|
sun[0] /= sl;
|
||||||
|
sun[1] /= sl;
|
||||||
|
sun[2] /= sl;
|
||||||
|
constexpr float ambient = 0.38F;
|
||||||
|
constexpr float near_plane = 10.0F;
|
||||||
|
|
||||||
|
const auto sample = [&](uint32 layer, float tu, float tv) -> uint32 {
|
||||||
|
if (layer >= scene.textures.size() || scene.textures[layer].empty()) return argb(140, 140, 140);
|
||||||
|
const auto &img = scene.textures[layer];
|
||||||
|
const auto wrap = [](float x) { return x - std::floor(x); };
|
||||||
|
const auto sx = std::min(img.width() - 1U, static_cast<uint32>(wrap(tu) * static_cast<float>(img.width())));
|
||||||
|
const auto sy = std::min(img.height() - 1U, static_cast<uint32>(wrap(tv) * static_cast<float>(img.height())));
|
||||||
|
return img.data()[static_cast<usize>(sy) * img.width() + sx];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Average colour per texture, used when a triangle covers fewer pixels
|
||||||
|
// than texels (minification) — a cheap mip-0-away fallback that keeps
|
||||||
|
// distant props from shimmering.
|
||||||
|
std::vector<uint32> average(scene.textures.size(), argb(140, 140, 140));
|
||||||
|
for (usize i = 0; i < scene.textures.size(); ++i) {
|
||||||
|
const auto &img = scene.textures[i];
|
||||||
|
if (img.empty()) continue;
|
||||||
|
usize r = 0;
|
||||||
|
usize g = 0;
|
||||||
|
usize b = 0;
|
||||||
|
for (usize p = 0; p < static_cast<usize>(img.width()) * img.height(); ++p) {
|
||||||
|
r += (img.data()[p] >> 16U) & 0xFFU;
|
||||||
|
g += (img.data()[p] >> 8U) & 0xFFU;
|
||||||
|
b += img.data()[p] & 0xFFU;
|
||||||
|
}
|
||||||
|
const auto n = static_cast<usize>(img.width()) * img.height();
|
||||||
|
average[i] = argb(static_cast<uint8>(r / n), static_cast<uint8>(g / n), static_cast<uint8>(b / n));
|
||||||
|
}
|
||||||
|
|
||||||
|
struct projected {
|
||||||
|
float sx = 0.0F;
|
||||||
|
float sy = 0.0F;
|
||||||
|
float inv_a = 0.0F; ///< 1 / view-space depth
|
||||||
|
};
|
||||||
|
const auto project = [&](const ra3::models::vertex &v, projected &out) -> bool {
|
||||||
|
const std::array<float, 3> rel{v.x - cam[0], v.y - cam[1], v.z - cam[2]};
|
||||||
|
const auto a = dot3(rel, f);
|
||||||
|
if (a <= near_plane) return false;
|
||||||
|
const auto ndc_x = (dot3(rel, r) / a) / (tan_half * aspect);
|
||||||
|
const auto ndc_y = (dot3(rel, u) / a) / tan_half;
|
||||||
|
out.sx = (ndc_x * 0.5F + 0.5F) * static_cast<float>(rw);
|
||||||
|
out.sy = (0.5F - ndc_y * 0.5F) * static_cast<float>(rh);
|
||||||
|
out.inv_a = 1.0F / a;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (usize t = 0; t + 2U < scene.indices.size(); t += 3U) {
|
||||||
|
const auto &v0 = scene.vertices[scene.indices[t]];
|
||||||
|
const auto &v1 = scene.vertices[scene.indices[t + 1U]];
|
||||||
|
const auto &v2 = scene.vertices[scene.indices[t + 2U]];
|
||||||
|
projected p0, p1, p2;
|
||||||
|
if (!project(v0, p0) || !project(v1, p1) || !project(v2, p2)) continue;
|
||||||
|
|
||||||
|
const auto area = (p1.sx - p0.sx) * (p2.sy - p0.sy) - (p1.sy - p0.sy) * (p2.sx - p0.sx);
|
||||||
|
if (std::abs(area) < 1.0e-6F) continue;
|
||||||
|
const auto sign = area < 0.0F ? -1.0F : 1.0F;
|
||||||
|
|
||||||
|
// Texture footprint: if the triangle covers more texels than
|
||||||
|
// pixels it is minified, so fall back to the texture average.
|
||||||
|
const auto uv_area = std::abs((v1.u - v0.u) * (v2.v - v0.v) - (v2.u - v0.u) * (v1.v - v0.v));
|
||||||
|
const auto texel_footprint = uv_area * static_cast<float>(scene.texture_size) * static_cast<float>(scene.texture_size);
|
||||||
|
const auto minified = texel_footprint > 2.0F * std::abs(area);
|
||||||
|
const auto flat_layer = static_cast<uint32>(v0.layer + 0.5F);
|
||||||
|
const auto flat_color = flat_layer < average.size() ? average[flat_layer] : argb(140, 140, 140);
|
||||||
|
|
||||||
|
const auto min_x = std::max(0, static_cast<int>(std::floor(std::min({p0.sx, p1.sx, p2.sx}))));
|
||||||
|
const auto max_x = std::min(rw - 1, static_cast<int>(std::ceil(std::max({p0.sx, p1.sx, p2.sx}))));
|
||||||
|
const auto min_y = std::max(0, static_cast<int>(std::floor(std::min({p0.sy, p1.sy, p2.sy}))));
|
||||||
|
const auto max_y = std::min(rh - 1, static_cast<int>(std::ceil(std::max({p0.sy, p1.sy, p2.sy}))));
|
||||||
|
|
||||||
|
for (int y = min_y; y <= max_y; ++y) {
|
||||||
|
for (int x = min_x; x <= max_x; ++x) {
|
||||||
|
const auto px = static_cast<float>(x) + 0.5F;
|
||||||
|
const auto py = static_cast<float>(y) + 0.5F;
|
||||||
|
auto w0 = ((p1.sx - p0.sx) * (py - p0.sy) - (p1.sy - p0.sy) * (px - p0.sx)) * sign;
|
||||||
|
auto w1 = ((p2.sx - p1.sx) * (py - p1.sy) - (p2.sy - p1.sy) * (px - p1.sx)) * sign;
|
||||||
|
auto w2 = ((p0.sx - p2.sx) * (py - p2.sy) - (p0.sy - p2.sy) * (px - p2.sx)) * sign;
|
||||||
|
if (w0 < 0.0F || w1 < 0.0F || w2 < 0.0F) continue;
|
||||||
|
const auto sum = w0 + w1 + w2;
|
||||||
|
if (sum <= 0.0F) continue;
|
||||||
|
w0 /= sum;
|
||||||
|
w1 /= sum;
|
||||||
|
w2 /= sum;
|
||||||
|
|
||||||
|
// Perspective-correct depth and attributes.
|
||||||
|
const auto inv_a = w0 * p0.inv_a + w1 * p1.inv_a + w2 * p2.inv_a;
|
||||||
|
const auto depth = 1.0F / inv_a;
|
||||||
|
const auto pixel = static_cast<usize>(y) * static_cast<usize>(rw) + static_cast<usize>(x);
|
||||||
|
// Ground decals sit exactly on the terrain; a small bias
|
||||||
|
// (mirrors the GPU depth bias) keeps them from z-fighting.
|
||||||
|
if (depth - 0.5F >= zbuf[pixel]) continue;
|
||||||
|
|
||||||
|
const auto tu = (w0 * v0.u * p0.inv_a + w1 * v1.u * p1.inv_a + w2 * v2.u * p2.inv_a) * depth;
|
||||||
|
const auto tv = (w0 * v0.v * p0.inv_a + w1 * v1.v * p1.inv_a + w2 * v2.v * p2.inv_a) * depth;
|
||||||
|
float nx = w0 * v0.nx + w1 * v1.nx + w2 * v2.nx;
|
||||||
|
float ny = w0 * v0.ny + w1 * v1.ny + w2 * v2.ny;
|
||||||
|
float nz = w0 * v0.nz + w1 * v1.nz + w2 * v2.nz;
|
||||||
|
const auto nl = std::sqrt(nx * nx + ny * ny + nz * nz);
|
||||||
|
if (nl > 1.0e-6F) {
|
||||||
|
nx /= nl;
|
||||||
|
ny /= nl;
|
||||||
|
nz /= nl;
|
||||||
|
}
|
||||||
|
const auto lambert = std::max(0.0F, std::abs(nx * sun[0] + ny * sun[1] + nz * sun[2]));
|
||||||
|
const auto shade = ambient + (1.0F - ambient) * lambert;
|
||||||
|
const auto texel = minified ? flat_color : sample(flat_layer, tu, tv);
|
||||||
|
const auto mod = [&](uint32 shift) {
|
||||||
|
return static_cast<uint8>(std::clamp(static_cast<float>((texel >> shift) & 0xFFU) * shade, 0.0F, 255.0F));
|
||||||
|
};
|
||||||
|
hi.data()[pixel] = argb(mod(16U), mod(8U), mod(0U));
|
||||||
|
zbuf[pixel] = depth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -851,7 +998,7 @@ export namespace ra3::terrain {
|
|||||||
* a transformed 2D image.
|
* a transformed 2D image.
|
||||||
*/
|
*/
|
||||||
[[nodiscard]] inline auto render3d(const map_data &map, const texture_set &set, const camera3d &camera, uint32 out_w, uint32 out_h,
|
[[nodiscard]] inline auto render3d(const map_data &map, const texture_set &set, const camera3d &camera, uint32 out_w, uint32 out_h,
|
||||||
const render_options &options = {}) -> image {
|
const render_options &options = {}, const ra3::models::scene *objects = nullptr) -> image {
|
||||||
if (!map.valid) throw terrain_error("terrain not parsed");
|
if (!map.valid) throw terrain_error("terrain not parsed");
|
||||||
out_w = std::max(1U, out_w);
|
out_w = std::max(1U, out_w);
|
||||||
out_h = std::max(1U, out_h);
|
out_h = std::max(1U, out_h);
|
||||||
@@ -968,6 +1115,7 @@ export namespace ra3::terrain {
|
|||||||
};
|
};
|
||||||
|
|
||||||
image hi(rw, rh, argb(0, 0, 0));
|
image hi(rw, rh, argb(0, 0, 0));
|
||||||
|
std::vector<float> zbuf(static_cast<usize>(rw) * rh, 1.0e30F);
|
||||||
for (uint32 py = 0; py < rh; ++py) {
|
for (uint32 py = 0; py < rh; ++py) {
|
||||||
const auto ndc_y = 1.0F - 2.0F * (static_cast<float>(py) + 0.5F) / static_cast<float>(rh);
|
const auto ndc_y = 1.0F - 2.0F * (static_cast<float>(py) + 0.5F) / static_cast<float>(rh);
|
||||||
for (uint32 px = 0; px < rw; ++px) {
|
for (uint32 px = 0; px < rw; ++px) {
|
||||||
@@ -979,10 +1127,11 @@ export namespace ra3::terrain {
|
|||||||
dx /= dlen;
|
dx /= dlen;
|
||||||
dy /= dlen;
|
dy /= dlen;
|
||||||
dz /= dlen;
|
dz /= dlen;
|
||||||
|
const auto pixel = static_cast<usize>(py) * rw + px;
|
||||||
|
|
||||||
if (dz >= -1.0e-4F) {
|
if (dz >= -1.0e-4F) {
|
||||||
const auto t = std::clamp(std::abs(dz) * 2.0F, 0.0F, 1.0F);
|
const auto t = std::clamp(std::abs(dz) * 2.0F, 0.0F, 1.0F);
|
||||||
hi.data()[static_cast<usize>(py) * rw + px] =
|
hi.data()[pixel] =
|
||||||
argb(static_cast<uint8>(120.0F + 60.0F * t), static_cast<uint8>(150.0F + 45.0F * t), static_cast<uint8>(190.0F + 40.0F * t));
|
argb(static_cast<uint8>(120.0F + 60.0F * t), static_cast<uint8>(150.0F + 45.0F * t), static_cast<uint8>(190.0F + 40.0F * t));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1017,7 +1166,7 @@ export namespace ra3::terrain {
|
|||||||
t += dt;
|
t += dt;
|
||||||
}
|
}
|
||||||
if (!hit) {
|
if (!hit) {
|
||||||
hi.data()[static_cast<usize>(py) * rw + px] = argb(150, 170, 200);
|
hi.data()[pixel] = argb(150, 170, 200);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1035,10 +1184,16 @@ export namespace ra3::terrain {
|
|||||||
lo = mid;
|
lo = mid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
hi.data()[static_cast<usize>(py) * rw + px] = hit_color(cam_x + dx * up, cam_y + dy * up, cam_z + dz * up);
|
hi.data()[pixel] = hit_color(cam_x + dx * up, cam_y + dy * up, cam_z + dz * up);
|
||||||
|
// View-space depth of the hit, for the object pass below.
|
||||||
|
zbuf[pixel] = up * (dx * fx + dy * fy + dz * fz);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (objects != nullptr && !objects->empty()) {
|
||||||
|
detail::rasterize_objects(hi, zbuf, *objects, {cam_x, cam_y, cam_z}, {fx, fy, fz}, {rx, ry, rz}, {ux, uy, uz}, tan_half, aspect);
|
||||||
|
}
|
||||||
|
|
||||||
if (ss == 1U) return hi;
|
if (ss == 1U) return hi;
|
||||||
image out(out_w, out_h, argb(0, 0, 0));
|
image out(out_w, out_h, argb(0, 0, 0));
|
||||||
for (uint32 y = 0; y < out_h; ++y) {
|
for (uint32 y = 0; y < out_h; ++y) {
|
||||||
|
|||||||
+373
-17
@@ -92,6 +92,7 @@ export namespace ra3::vulkan {
|
|||||||
if (!this->create_device()) return false;
|
if (!this->create_device()) return false;
|
||||||
if (!this->create_swapchain()) return false;
|
if (!this->create_swapchain()) return false;
|
||||||
if (!this->create_render_pass()) return false;
|
if (!this->create_render_pass()) return false;
|
||||||
|
if (!this->create_framebuffers()) return false;
|
||||||
if (!this->create_pipeline()) return false;
|
if (!this->create_pipeline()) return false;
|
||||||
if (!this->create_commands()) return false;
|
if (!this->create_commands()) return false;
|
||||||
if (!this->create_sync()) return false;
|
if (!this->create_sync()) return false;
|
||||||
@@ -341,6 +342,12 @@ export namespace ra3::vulkan {
|
|||||||
if (!this->create_terrain_pipeline()) return false;
|
if (!this->create_terrain_pipeline()) return false;
|
||||||
if (!this->create_terrain_images(terrain)) return false;
|
if (!this->create_terrain_images(terrain)) return false;
|
||||||
if (!this->create_terrain_descriptors()) return false;
|
if (!this->create_terrain_descriptors()) return false;
|
||||||
|
if (!terrain.objects.empty()) {
|
||||||
|
if (!this->create_object_pipeline()) return false;
|
||||||
|
if (!this->create_object_buffers(terrain)) return false;
|
||||||
|
if (!this->create_object_descriptors()) return false;
|
||||||
|
objects_ready_ = true;
|
||||||
|
}
|
||||||
terrain_ready_ = true;
|
terrain_ready_ = true;
|
||||||
}
|
}
|
||||||
if (overlay.label_changed) this->update_overlay(overlay_label_, overlay_label_set_, overlay_label_w_, overlay_label_h_, overlay.label);
|
if (overlay.label_changed) this->update_overlay(overlay_label_, overlay_label_set_, overlay_label_w_, overlay_label_h_, overlay.label);
|
||||||
@@ -669,6 +676,11 @@ export namespace ra3::vulkan {
|
|||||||
VkPipelineMultisampleStateCreateInfo multisample{};
|
VkPipelineMultisampleStateCreateInfo multisample{};
|
||||||
multisample.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
|
multisample.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
|
||||||
multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
|
multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
|
||||||
|
VkPipelineDepthStencilStateCreateInfo depth_stencil{};
|
||||||
|
depth_stencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
|
||||||
|
depth_stencil.depthTestEnable = VK_TRUE;
|
||||||
|
depth_stencil.depthWriteEnable = VK_TRUE;
|
||||||
|
depth_stencil.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL;
|
||||||
VkPipelineColorBlendAttachmentState blend_attachment{};
|
VkPipelineColorBlendAttachmentState blend_attachment{};
|
||||||
blend_attachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
|
blend_attachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
|
||||||
VkPipelineColorBlendStateCreateInfo blend{};
|
VkPipelineColorBlendStateCreateInfo blend{};
|
||||||
@@ -690,6 +702,7 @@ export namespace ra3::vulkan {
|
|||||||
pipeline_info.pViewportState = &viewport;
|
pipeline_info.pViewportState = &viewport;
|
||||||
pipeline_info.pRasterizationState = &raster;
|
pipeline_info.pRasterizationState = &raster;
|
||||||
pipeline_info.pMultisampleState = &multisample;
|
pipeline_info.pMultisampleState = &multisample;
|
||||||
|
pipeline_info.pDepthStencilState = &depth_stencil;
|
||||||
pipeline_info.pColorBlendState = &blend;
|
pipeline_info.pColorBlendState = &blend;
|
||||||
pipeline_info.pDynamicState = &dynamic;
|
pipeline_info.pDynamicState = &dynamic;
|
||||||
pipeline_info.layout = terrain_pipeline_layout_;
|
pipeline_info.layout = terrain_pipeline_layout_;
|
||||||
@@ -737,6 +750,197 @@ export namespace ra3::vulkan {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Host-visible vertex/index buffer, filled once (the map's geometry is static). */
|
||||||
|
[[nodiscard]] auto make_host_buffer(const void *data, VkDeviceSize bytes, VkBufferUsageFlags usage, VkBuffer &buffer,
|
||||||
|
VkDeviceMemory &memory) -> bool {
|
||||||
|
VkBufferCreateInfo info{};
|
||||||
|
info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
|
||||||
|
info.size = bytes;
|
||||||
|
info.usage = usage;
|
||||||
|
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||||
|
if (!detail::check(vkCreateBuffer(device_, &info, nullptr, &buffer), "vkCreateBuffer(object)")) return false;
|
||||||
|
VkMemoryRequirements requirements{};
|
||||||
|
vkGetBufferMemoryRequirements(device_, buffer, &requirements);
|
||||||
|
VkMemoryAllocateInfo allocate{};
|
||||||
|
allocate.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
|
||||||
|
allocate.allocationSize = requirements.size;
|
||||||
|
allocate.memoryTypeIndex = this->memory_type(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
|
||||||
|
if (!detail::check(vkAllocateMemory(device_, &allocate, nullptr, &memory), "vkAllocateMemory(object)")) return false;
|
||||||
|
if (!detail::check(vkBindBufferMemory(device_, buffer, memory, 0), "vkBindBufferMemory(object)")) return false;
|
||||||
|
void *mapped = nullptr;
|
||||||
|
if (!detail::check(vkMapMemory(device_, memory, 0, bytes, 0, &mapped), "vkMapMemory(object)")) return false;
|
||||||
|
std::memcpy(mapped, data, static_cast<std::size_t>(bytes));
|
||||||
|
vkUnmapMemory(device_, memory);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Upload the static-map geometry and its texture array. */
|
||||||
|
[[nodiscard]] auto create_object_buffers(const ra3::terrain::gpu_terrain &terrain) -> bool {
|
||||||
|
const auto &scene = terrain.objects;
|
||||||
|
if (scene.empty()) return true;
|
||||||
|
const auto vertex_bytes = static_cast<VkDeviceSize>(scene.vertices.size()) * sizeof(ra3::models::vertex);
|
||||||
|
const auto index_bytes = static_cast<VkDeviceSize>(scene.indices.size()) * sizeof(uint32_t);
|
||||||
|
if (!this->make_host_buffer(scene.vertices.data(), vertex_bytes, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, object_vertex_buffer_, object_vertex_memory_)) return false;
|
||||||
|
if (!this->make_host_buffer(scene.indices.data(), index_bytes, VK_BUFFER_USAGE_INDEX_BUFFER_BIT, object_index_buffer_, object_index_memory_)) return false;
|
||||||
|
object_index_count_ = static_cast<uint32_t>(scene.indices.size());
|
||||||
|
|
||||||
|
const auto size = std::max(1U, scene.texture_size);
|
||||||
|
const auto layers = std::max<size_t>(1U, scene.textures.size());
|
||||||
|
std::vector<uint32_t> pixels(static_cast<size_t>(size) * size * layers, 0xFFFFFFFFU);
|
||||||
|
for (size_t layer = 0; layer < scene.textures.size(); ++layer) {
|
||||||
|
const auto &texture = scene.textures[layer];
|
||||||
|
if (texture.empty()) continue;
|
||||||
|
// The scene already resized every texture to `texture_size`.
|
||||||
|
for (uint32_t y = 0; y < std::min(size, texture.height()); ++y) {
|
||||||
|
for (uint32_t x = 0; x < std::min(size, texture.width()); ++x) {
|
||||||
|
pixels[(static_cast<size_t>(layer) * size + y) * size + x] = texture.data()[static_cast<size_t>(y) * texture.width() + x];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this->make_gpu_image(object_texture_, size, size, VK_FORMAT_B8G8R8A8_UNORM, pixels.data(),
|
||||||
|
static_cast<VkDeviceSize>(pixels.size()) * 4U, this->mip_count(size), VK_FILTER_LINEAR, VK_SAMPLER_MIPMAP_MODE_LINEAR,
|
||||||
|
static_cast<uint32_t>(layers), VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Full mip chain length for a square texture. */
|
||||||
|
[[nodiscard]] static auto mip_count(uint32_t size) -> uint32_t {
|
||||||
|
uint32_t mips = 1U;
|
||||||
|
while (size > 1U) {
|
||||||
|
size >>= 1U;
|
||||||
|
++mips;
|
||||||
|
}
|
||||||
|
return mips;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] auto create_object_pipeline() -> bool {
|
||||||
|
VkPushConstantRange push_range{};
|
||||||
|
push_range.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
|
||||||
|
push_range.size = sizeof(float) * 20U;
|
||||||
|
|
||||||
|
VkPipelineLayoutCreateInfo layout_info{};
|
||||||
|
layout_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
|
||||||
|
layout_info.setLayoutCount = 1U;
|
||||||
|
layout_info.pSetLayouts = &descriptor_layout_; // reused: one combined image sampler
|
||||||
|
layout_info.pushConstantRangeCount = 1U;
|
||||||
|
layout_info.pPushConstantRanges = &push_range;
|
||||||
|
if (!detail::check(vkCreatePipelineLayout(device_, &layout_info, nullptr, &object_pipeline_layout_), "vkCreatePipelineLayout(object)")) return false;
|
||||||
|
|
||||||
|
VkShaderModule vertex = VK_NULL_HANDLE;
|
||||||
|
VkShaderModule fragment = VK_NULL_HANDLE;
|
||||||
|
if (!this->make_shader_module(ra3_shaders::object_vert_spv, ra3_shaders::object_vert_spv_words, vertex)) return false;
|
||||||
|
if (!this->make_shader_module(ra3_shaders::object_frag_spv, ra3_shaders::object_frag_spv_words, fragment)) return false;
|
||||||
|
|
||||||
|
VkPipelineShaderStageCreateInfo stages[2]{};
|
||||||
|
stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
||||||
|
stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
|
||||||
|
stages[0].module = vertex;
|
||||||
|
stages[0].pName = "main";
|
||||||
|
stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
||||||
|
stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
|
||||||
|
stages[1].module = fragment;
|
||||||
|
stages[1].pName = "main";
|
||||||
|
|
||||||
|
VkVertexInputBindingDescription binding{};
|
||||||
|
binding.binding = 0U;
|
||||||
|
binding.stride = sizeof(ra3::models::vertex);
|
||||||
|
binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
|
||||||
|
VkVertexInputAttributeDescription attributes[4]{};
|
||||||
|
attributes[0] = {0U, 0U, VK_FORMAT_R32G32B32_SFLOAT, 0U};
|
||||||
|
attributes[1] = {1U, 0U, VK_FORMAT_R32G32B32_SFLOAT, 12U};
|
||||||
|
attributes[2] = {2U, 0U, VK_FORMAT_R32G32_SFLOAT, 24U};
|
||||||
|
attributes[3] = {3U, 0U, VK_FORMAT_R32_SFLOAT, 32U};
|
||||||
|
static_assert(sizeof(ra3::models::vertex) == 36U, "object vertex layout changed");
|
||||||
|
VkPipelineVertexInputStateCreateInfo vertex_input{};
|
||||||
|
vertex_input.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
|
||||||
|
vertex_input.vertexBindingDescriptionCount = 1U;
|
||||||
|
vertex_input.pVertexBindingDescriptions = &binding;
|
||||||
|
vertex_input.vertexAttributeDescriptionCount = 4U;
|
||||||
|
vertex_input.pVertexAttributeDescriptions = attributes;
|
||||||
|
|
||||||
|
VkPipelineInputAssemblyStateCreateInfo assembly{};
|
||||||
|
assembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
|
||||||
|
assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||||
|
VkPipelineViewportStateCreateInfo viewport{};
|
||||||
|
viewport.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
|
||||||
|
viewport.viewportCount = 1U;
|
||||||
|
viewport.scissorCount = 1U;
|
||||||
|
VkPipelineRasterizationStateCreateInfo raster{};
|
||||||
|
raster.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
|
||||||
|
raster.polygonMode = VK_POLYGON_MODE_FILL;
|
||||||
|
raster.cullMode = VK_CULL_MODE_NONE;
|
||||||
|
raster.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
|
||||||
|
raster.lineWidth = 1.0F;
|
||||||
|
// Ground decals (sidewalks/roads/deck pieces) sit exactly on the
|
||||||
|
// terrain; a small negative depth bias keeps them from z-fighting.
|
||||||
|
raster.depthBiasEnable = VK_TRUE;
|
||||||
|
raster.depthBiasConstantFactor = -1.0F;
|
||||||
|
raster.depthBiasSlopeFactor = -1.0F;
|
||||||
|
VkPipelineMultisampleStateCreateInfo multisample{};
|
||||||
|
multisample.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
|
||||||
|
multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
|
||||||
|
VkPipelineDepthStencilStateCreateInfo depth_stencil{};
|
||||||
|
depth_stencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
|
||||||
|
depth_stencil.depthTestEnable = VK_TRUE;
|
||||||
|
depth_stencil.depthWriteEnable = VK_TRUE;
|
||||||
|
depth_stencil.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL;
|
||||||
|
VkPipelineColorBlendAttachmentState blend_attachment{};
|
||||||
|
blend_attachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
|
||||||
|
blend_attachment.blendEnable = VK_FALSE;
|
||||||
|
VkPipelineColorBlendStateCreateInfo blend{};
|
||||||
|
blend.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
|
||||||
|
blend.attachmentCount = 1U;
|
||||||
|
blend.pAttachments = &blend_attachment;
|
||||||
|
const VkDynamicState dynamic_states[] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
|
||||||
|
VkPipelineDynamicStateCreateInfo dynamic{};
|
||||||
|
dynamic.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
|
||||||
|
dynamic.dynamicStateCount = 2U;
|
||||||
|
dynamic.pDynamicStates = dynamic_states;
|
||||||
|
|
||||||
|
VkGraphicsPipelineCreateInfo pipeline_info{};
|
||||||
|
pipeline_info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
|
||||||
|
pipeline_info.stageCount = 2U;
|
||||||
|
pipeline_info.pStages = stages;
|
||||||
|
pipeline_info.pVertexInputState = &vertex_input;
|
||||||
|
pipeline_info.pInputAssemblyState = &assembly;
|
||||||
|
pipeline_info.pViewportState = &viewport;
|
||||||
|
pipeline_info.pRasterizationState = &raster;
|
||||||
|
pipeline_info.pMultisampleState = &multisample;
|
||||||
|
pipeline_info.pDepthStencilState = &depth_stencil;
|
||||||
|
pipeline_info.pColorBlendState = &blend;
|
||||||
|
pipeline_info.pDynamicState = &dynamic;
|
||||||
|
pipeline_info.layout = object_pipeline_layout_;
|
||||||
|
pipeline_info.renderPass = render_pass_;
|
||||||
|
pipeline_info.subpass = 0U;
|
||||||
|
const auto created = detail::check(vkCreateGraphicsPipelines(device_, VK_NULL_HANDLE, 1U, &pipeline_info, nullptr, &object_pipeline_),
|
||||||
|
"vkCreateGraphicsPipelines(object)");
|
||||||
|
vkDestroyShaderModule(device_, vertex, nullptr);
|
||||||
|
vkDestroyShaderModule(device_, fragment, nullptr);
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] auto create_object_descriptors() -> bool {
|
||||||
|
if (object_texture_.view == VK_NULL_HANDLE) return false;
|
||||||
|
VkDescriptorSetAllocateInfo set_info{};
|
||||||
|
set_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
|
||||||
|
set_info.descriptorPool = descriptor_pool_;
|
||||||
|
set_info.descriptorSetCount = 1U;
|
||||||
|
set_info.pSetLayouts = &descriptor_layout_;
|
||||||
|
if (!detail::check(vkAllocateDescriptorSets(device_, &set_info, &object_set_), "vkAllocateDescriptorSets(object)")) return false;
|
||||||
|
VkDescriptorImageInfo info{};
|
||||||
|
info.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||||
|
info.imageView = object_texture_.view;
|
||||||
|
info.sampler = object_texture_.sampler;
|
||||||
|
VkWriteDescriptorSet write{};
|
||||||
|
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||||
|
write.dstSet = object_set_;
|
||||||
|
write.dstBinding = 0U;
|
||||||
|
write.descriptorCount = 1U;
|
||||||
|
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
||||||
|
write.pImageInfo = &info;
|
||||||
|
vkUpdateDescriptorSets(device_, 1U, &write, 0U, nullptr);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
[[nodiscard]] auto draw_terrain(const ra3::terrain::gpu_terrain &terrain, const ra3::render::camera3d &camera, float time_s, float aspect) -> bool {
|
[[nodiscard]] auto draw_terrain(const ra3::terrain::gpu_terrain &terrain, const ra3::render::camera3d &camera, float time_s, float aspect) -> bool {
|
||||||
vkWaitForFences(device_, 1U, &in_flight_[current_frame_], VK_TRUE, UINT64_MAX);
|
vkWaitForFences(device_, 1U, &in_flight_[current_frame_], VK_TRUE, UINT64_MAX);
|
||||||
|
|
||||||
@@ -754,13 +958,16 @@ export namespace ra3::vulkan {
|
|||||||
|
|
||||||
VkClearValue clear{};
|
VkClearValue clear{};
|
||||||
clear.color = {{0.45F, 0.55F, 0.70F, 1.0F}};
|
clear.color = {{0.45F, 0.55F, 0.70F, 1.0F}};
|
||||||
|
VkClearValue depth_clear{};
|
||||||
|
depth_clear.depthStencil = {1.0F, 0U};
|
||||||
|
const VkClearValue clears[2] = {clear, depth_clear};
|
||||||
VkRenderPassBeginInfo render_pass_begin{};
|
VkRenderPassBeginInfo render_pass_begin{};
|
||||||
render_pass_begin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
|
render_pass_begin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
|
||||||
render_pass_begin.renderPass = render_pass_;
|
render_pass_begin.renderPass = render_pass_;
|
||||||
render_pass_begin.framebuffer = framebuffers_[image_index];
|
render_pass_begin.framebuffer = framebuffers_[image_index];
|
||||||
render_pass_begin.renderArea = {{0U, 0U}, swapchain_extent_};
|
render_pass_begin.renderArea = {{0U, 0U}, swapchain_extent_};
|
||||||
render_pass_begin.clearValueCount = 1U;
|
render_pass_begin.clearValueCount = 2U;
|
||||||
render_pass_begin.pClearValues = &clear;
|
render_pass_begin.pClearValues = clears;
|
||||||
vkCmdBeginRenderPass(cmd, &render_pass_begin, VK_SUBPASS_CONTENTS_INLINE);
|
vkCmdBeginRenderPass(cmd, &render_pass_begin, VK_SUBPASS_CONTENTS_INLINE);
|
||||||
|
|
||||||
VkViewport viewport{0.0F, 0.0F, static_cast<float>(swapchain_extent_.width), static_cast<float>(swapchain_extent_.height), 0.0F, 1.0F};
|
VkViewport viewport{0.0F, 0.0F, static_cast<float>(swapchain_extent_.width), static_cast<float>(swapchain_extent_.height), 0.0F, 1.0F};
|
||||||
@@ -793,6 +1000,49 @@ export namespace ra3::vulkan {
|
|||||||
vkCmdPushConstants(cmd, terrain_pipeline_layout_, VK_SHADER_STAGE_FRAGMENT_BIT, 0U, sizeof(push), push);
|
vkCmdPushConstants(cmd, terrain_pipeline_layout_, VK_SHADER_STAGE_FRAGMENT_BIT, 0U, sizeof(push), push);
|
||||||
vkCmdDraw(cmd, 3U, 1U, 0U, 0U);
|
vkCmdDraw(cmd, 3U, 1U, 0U, 0U);
|
||||||
|
|
||||||
|
// Static-map models (buildings/props) depth-test against the terrain.
|
||||||
|
if (objects_ready_ && object_index_count_ > 0U) {
|
||||||
|
const auto z_scale = terrain.z_scale;
|
||||||
|
const auto world_w = static_cast<float>(terrain.width) * 10.0F;
|
||||||
|
const auto world_h = static_cast<float>(terrain.height) * 10.0F;
|
||||||
|
const auto sample = [&](float wx, float wy) -> float {
|
||||||
|
if (wx < 0.0F || wy < 0.0F || wx >= world_w || wy >= world_h) return -1.0e9F;
|
||||||
|
const auto cx = std::min(terrain.width - 1U, static_cast<uint32_t>(wx / 10.0F));
|
||||||
|
const auto cy = std::min(terrain.height - 1U, static_cast<uint32_t>((world_h - wy) / 10.0F));
|
||||||
|
return static_cast<float>(terrain.heights[static_cast<size_t>(cy) * terrain.width + cx]) * z_scale;
|
||||||
|
};
|
||||||
|
float target_z = sample(camera.target_x, camera.target_y);
|
||||||
|
if (target_z < -1.0e8F) target_z = 0.0F;
|
||||||
|
const auto pitch = std::clamp(camera.pitch, 0.15F, 1.45F);
|
||||||
|
const auto fov = std::clamp(camera.fov, 0.3F, 1.4F);
|
||||||
|
const auto cp = std::cos(pitch);
|
||||||
|
const float fwd[3] = {cp * std::sin(camera.yaw), cp * std::cos(camera.yaw), -std::sin(pitch)};
|
||||||
|
float right[3] = {fwd[1], -fwd[0], 0.0F};
|
||||||
|
const auto rl = std::sqrt(right[0] * right[0] + right[1] * right[1]);
|
||||||
|
right[0] /= rl;
|
||||||
|
right[1] /= rl;
|
||||||
|
float up[3] = {right[1] * fwd[2], -right[0] * fwd[2], right[0] * fwd[1] - right[1] * fwd[0]};
|
||||||
|
const auto ul = std::sqrt(up[0] * up[0] + up[1] * up[1] + up[2] * up[2]);
|
||||||
|
up[0] /= ul;
|
||||||
|
up[1] /= ul;
|
||||||
|
up[2] /= ul;
|
||||||
|
const auto dist = camera.height / std::sin(pitch);
|
||||||
|
const float cam[3] = {camera.target_x - fwd[0] * dist, camera.target_y - fwd[1] * dist, target_z + camera.height - fwd[2] * dist};
|
||||||
|
const auto th = std::tan(fov * 0.5F);
|
||||||
|
const float obj_push[20] = {cam[0], cam[1], cam[2], 0.0F,
|
||||||
|
fwd[0], fwd[1], fwd[2], 0.0F,
|
||||||
|
right[0], right[1], right[2], th,
|
||||||
|
up[0], up[1], up[2], th * aspect,
|
||||||
|
0.45F, 0.35F, 0.82F, 0.38F};
|
||||||
|
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, object_pipeline_);
|
||||||
|
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, object_pipeline_layout_, 0U, 1U, &object_set_, 0U, nullptr);
|
||||||
|
vkCmdPushConstants(cmd, object_pipeline_layout_, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0U, sizeof(obj_push), obj_push);
|
||||||
|
const VkDeviceSize offset = 0U;
|
||||||
|
vkCmdBindVertexBuffers(cmd, 0U, 1U, &object_vertex_buffer_, &offset);
|
||||||
|
vkCmdBindIndexBuffer(cmd, object_index_buffer_, 0U, VK_INDEX_TYPE_UINT32);
|
||||||
|
vkCmdDrawIndexed(cmd, object_index_count_, 1U, 0U, 0, 0U);
|
||||||
|
}
|
||||||
|
|
||||||
// Overlays (top-left FPS label, bottom-right minimap) use the image
|
// Overlays (top-left FPS label, bottom-right minimap) use the image
|
||||||
// pipeline with alpha blending, drawn into the same render pass.
|
// pipeline with alpha blending, drawn into the same render pass.
|
||||||
const auto draw_overlay = [&](const gpu_image &overlay, VkDescriptorSet set, uint32_t w, uint32_t h, float x, float y) {
|
const auto draw_overlay = [&](const gpu_image &overlay, VkDescriptorSet set, uint32_t w, uint32_t h, float x, float y) {
|
||||||
@@ -861,6 +1111,27 @@ export namespace ra3::vulkan {
|
|||||||
terrain_set_ = VK_NULL_HANDLE;
|
terrain_set_ = VK_NULL_HANDLE;
|
||||||
overlay_label_set_ = VK_NULL_HANDLE;
|
overlay_label_set_ = VK_NULL_HANDLE;
|
||||||
overlay_minimap_set_ = VK_NULL_HANDLE;
|
overlay_minimap_set_ = VK_NULL_HANDLE;
|
||||||
|
|
||||||
|
if (object_vertex_buffer_ != VK_NULL_HANDLE) vkDestroyBuffer(device_, object_vertex_buffer_, nullptr);
|
||||||
|
if (object_index_buffer_ != VK_NULL_HANDLE) vkDestroyBuffer(device_, object_index_buffer_, nullptr);
|
||||||
|
if (object_vertex_memory_ != VK_NULL_HANDLE) vkFreeMemory(device_, object_vertex_memory_, nullptr);
|
||||||
|
if (object_index_memory_ != VK_NULL_HANDLE) vkFreeMemory(device_, object_index_memory_, nullptr);
|
||||||
|
if (object_texture_.sampler != VK_NULL_HANDLE) vkDestroySampler(device_, object_texture_.sampler, nullptr);
|
||||||
|
if (object_texture_.view != VK_NULL_HANDLE) vkDestroyImageView(device_, object_texture_.view, nullptr);
|
||||||
|
if (object_texture_.image != VK_NULL_HANDLE) vkDestroyImage(device_, object_texture_.image, nullptr);
|
||||||
|
if (object_texture_.memory != VK_NULL_HANDLE) vkFreeMemory(device_, object_texture_.memory, nullptr);
|
||||||
|
if (object_pipeline_ != VK_NULL_HANDLE) vkDestroyPipeline(device_, object_pipeline_, nullptr);
|
||||||
|
if (object_pipeline_layout_ != VK_NULL_HANDLE) vkDestroyPipelineLayout(device_, object_pipeline_layout_, nullptr);
|
||||||
|
object_vertex_buffer_ = VK_NULL_HANDLE;
|
||||||
|
object_index_buffer_ = VK_NULL_HANDLE;
|
||||||
|
object_vertex_memory_ = VK_NULL_HANDLE;
|
||||||
|
object_index_memory_ = VK_NULL_HANDLE;
|
||||||
|
object_texture_ = {};
|
||||||
|
object_pipeline_ = VK_NULL_HANDLE;
|
||||||
|
object_pipeline_layout_ = VK_NULL_HANDLE;
|
||||||
|
object_set_ = VK_NULL_HANDLE;
|
||||||
|
object_index_count_ = 0U;
|
||||||
|
objects_ready_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1009,23 +1280,78 @@ export namespace ra3::vulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
framebuffers_.resize(actual);
|
framebuffers_.resize(actual);
|
||||||
for (uint32_t i = 0; i < actual; ++i) {
|
return true;
|
||||||
VkImageView attachments[] = {swapchain_views_[i]};
|
}
|
||||||
|
|
||||||
|
/** Create one color+depth framebuffer per swapchain image (needs `render_pass_`). */
|
||||||
|
[[nodiscard]] auto create_framebuffers() -> bool {
|
||||||
|
this->create_depth_resources();
|
||||||
|
for (size_t i = 0; i < framebuffers_.size(); ++i) {
|
||||||
|
VkImageView attachments[] = {swapchain_views_[i], depth_views_[i]};
|
||||||
VkFramebufferCreateInfo framebuffer_info{};
|
VkFramebufferCreateInfo framebuffer_info{};
|
||||||
framebuffer_info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
|
framebuffer_info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
|
||||||
framebuffer_info.renderPass = render_pass_;
|
framebuffer_info.renderPass = render_pass_;
|
||||||
framebuffer_info.attachmentCount = 1U;
|
framebuffer_info.attachmentCount = 2U;
|
||||||
framebuffer_info.pAttachments = attachments;
|
framebuffer_info.pAttachments = attachments;
|
||||||
framebuffer_info.width = extent.width;
|
framebuffer_info.width = swapchain_extent_.width;
|
||||||
framebuffer_info.height = extent.height;
|
framebuffer_info.height = swapchain_extent_.height;
|
||||||
framebuffer_info.layers = 1U;
|
framebuffer_info.layers = 1U;
|
||||||
if (!detail::check(vkCreateFramebuffer(device_, &framebuffer_info, nullptr, &framebuffers_[i]), "vkCreateFramebuffer")) return false;
|
if (!detail::check(vkCreateFramebuffer(device_, &framebuffer_info, nullptr, &framebuffers_[i]), "vkCreateFramebuffer")) return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Allocate a depth image + view per swapchain image (static-map models depth-test against the terrain). */
|
||||||
|
auto create_depth_resources() -> void {
|
||||||
|
this->destroy_depth_resources();
|
||||||
|
depth_images_.resize(swapchain_images_.size());
|
||||||
|
depth_memories_.resize(swapchain_images_.size());
|
||||||
|
depth_views_.resize(swapchain_images_.size());
|
||||||
|
for (size_t i = 0; i < swapchain_images_.size(); ++i) {
|
||||||
|
VkImageCreateInfo image_info{};
|
||||||
|
image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
||||||
|
image_info.imageType = VK_IMAGE_TYPE_2D;
|
||||||
|
image_info.extent = {swapchain_extent_.width, swapchain_extent_.height, 1U};
|
||||||
|
image_info.mipLevels = 1U;
|
||||||
|
image_info.arrayLayers = 1U;
|
||||||
|
image_info.format = VK_FORMAT_D32_SFLOAT;
|
||||||
|
image_info.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||||
|
image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
|
image_info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
|
||||||
|
image_info.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||||
|
image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||||
|
if (vkCreateImage(device_, &image_info, nullptr, &depth_images_[i]) != VK_SUCCESS) return;
|
||||||
|
VkMemoryRequirements requirements{};
|
||||||
|
vkGetImageMemoryRequirements(device_, depth_images_[i], &requirements);
|
||||||
|
VkMemoryAllocateInfo allocate{};
|
||||||
|
allocate.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
|
||||||
|
allocate.allocationSize = requirements.size;
|
||||||
|
allocate.memoryTypeIndex = this->memory_type(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
||||||
|
if (vkAllocateMemory(device_, &allocate, nullptr, &depth_memories_[i]) != VK_SUCCESS) return;
|
||||||
|
vkBindImageMemory(device_, depth_images_[i], depth_memories_[i], 0);
|
||||||
|
VkImageViewCreateInfo view_info{};
|
||||||
|
view_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
|
||||||
|
view_info.image = depth_images_[i];
|
||||||
|
view_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||||
|
view_info.format = VK_FORMAT_D32_SFLOAT;
|
||||||
|
view_info.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0U, 1U, 0U, 1U};
|
||||||
|
if (vkCreateImageView(device_, &view_info, nullptr, &depth_views_[i]) != VK_SUCCESS) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto destroy_depth_resources() -> void {
|
||||||
|
if (device_ == VK_NULL_HANDLE) return;
|
||||||
|
for (const auto view: depth_views_) vkDestroyImageView(device_, view, nullptr);
|
||||||
|
for (const auto image: depth_images_) vkDestroyImage(device_, image, nullptr);
|
||||||
|
for (const auto memory: depth_memories_) vkFreeMemory(device_, memory, nullptr);
|
||||||
|
depth_views_.clear();
|
||||||
|
depth_images_.clear();
|
||||||
|
depth_memories_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
auto cleanup_swapchain() -> void {
|
auto cleanup_swapchain() -> void {
|
||||||
for (const auto framebuffer: framebuffers_) vkDestroyFramebuffer(device_, framebuffer, nullptr);
|
for (const auto framebuffer: framebuffers_) vkDestroyFramebuffer(device_, framebuffer, nullptr);
|
||||||
|
this->destroy_depth_resources();
|
||||||
for (const auto view: swapchain_views_) vkDestroyImageView(device_, view, nullptr);
|
for (const auto view: swapchain_views_) vkDestroyImageView(device_, view, nullptr);
|
||||||
framebuffers_.clear();
|
framebuffers_.clear();
|
||||||
swapchain_views_.clear();
|
swapchain_views_.clear();
|
||||||
@@ -1041,7 +1367,7 @@ export namespace ra3::vulkan {
|
|||||||
if (width == 0 || height == 0) return false;
|
if (width == 0 || height == 0) return false;
|
||||||
vkDeviceWaitIdle(device_);
|
vkDeviceWaitIdle(device_);
|
||||||
this->cleanup_swapchain();
|
this->cleanup_swapchain();
|
||||||
return this->create_swapchain();
|
return this->create_swapchain() && this->create_framebuffers();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- pipeline --------------------------------------------------------
|
// ---- pipeline --------------------------------------------------------
|
||||||
@@ -1058,22 +1384,36 @@ export namespace ra3::vulkan {
|
|||||||
color.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
|
color.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
|
||||||
|
|
||||||
VkAttachmentReference color_ref{0U, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
|
VkAttachmentReference color_ref{0U, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
|
||||||
|
|
||||||
|
VkAttachmentDescription depth{};
|
||||||
|
depth.format = VK_FORMAT_D32_SFLOAT;
|
||||||
|
depth.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||||
|
depth.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
|
||||||
|
depth.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
||||||
|
depth.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||||
|
depth.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
||||||
|
depth.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
|
depth.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||||||
|
VkAttachmentReference depth_ref{1U, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL};
|
||||||
|
|
||||||
VkSubpassDescription subpass{};
|
VkSubpassDescription subpass{};
|
||||||
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||||||
subpass.colorAttachmentCount = 1U;
|
subpass.colorAttachmentCount = 1U;
|
||||||
subpass.pColorAttachments = &color_ref;
|
subpass.pColorAttachments = &color_ref;
|
||||||
|
subpass.pDepthStencilAttachment = &depth_ref;
|
||||||
|
|
||||||
VkSubpassDependency dependency{};
|
VkSubpassDependency dependency{};
|
||||||
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
|
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
|
||||||
dependency.dstSubpass = 0U;
|
dependency.dstSubpass = 0U;
|
||||||
dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
|
||||||
dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
|
||||||
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
|
||||||
|
|
||||||
|
const VkAttachmentDescription attachments[] = {color, depth};
|
||||||
VkRenderPassCreateInfo render_pass_info{};
|
VkRenderPassCreateInfo render_pass_info{};
|
||||||
render_pass_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
|
render_pass_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
|
||||||
render_pass_info.attachmentCount = 1U;
|
render_pass_info.attachmentCount = 2U;
|
||||||
render_pass_info.pAttachments = &color;
|
render_pass_info.pAttachments = attachments;
|
||||||
render_pass_info.subpassCount = 1U;
|
render_pass_info.subpassCount = 1U;
|
||||||
render_pass_info.pSubpasses = &subpass;
|
render_pass_info.pSubpasses = &subpass;
|
||||||
render_pass_info.dependencyCount = 1U;
|
render_pass_info.dependencyCount = 1U;
|
||||||
@@ -1211,10 +1551,10 @@ export namespace ra3::vulkan {
|
|||||||
command_buffers_.resize(frames_in_flight);
|
command_buffers_.resize(frames_in_flight);
|
||||||
if (!detail::check(vkAllocateCommandBuffers(device_, &allocate, command_buffers_.data()), "vkAllocateCommandBuffers")) return false;
|
if (!detail::check(vkAllocateCommandBuffers(device_, &allocate, command_buffers_.data()), "vkAllocateCommandBuffers")) return false;
|
||||||
|
|
||||||
VkDescriptorPoolSize pool_size{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 4U};
|
VkDescriptorPoolSize pool_size{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 8U};
|
||||||
VkDescriptorPoolCreateInfo descriptor_pool_info{};
|
VkDescriptorPoolCreateInfo descriptor_pool_info{};
|
||||||
descriptor_pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
descriptor_pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
||||||
descriptor_pool_info.maxSets = 4U;
|
descriptor_pool_info.maxSets = 8U;
|
||||||
descriptor_pool_info.poolSizeCount = 1U;
|
descriptor_pool_info.poolSizeCount = 1U;
|
||||||
descriptor_pool_info.pPoolSizes = &pool_size;
|
descriptor_pool_info.pPoolSizes = &pool_size;
|
||||||
if (!detail::check(vkCreateDescriptorPool(device_, &descriptor_pool_info, nullptr, &descriptor_pool_), "vkCreateDescriptorPool")) return false;
|
if (!detail::check(vkCreateDescriptorPool(device_, &descriptor_pool_info, nullptr, &descriptor_pool_), "vkCreateDescriptorPool")) return false;
|
||||||
@@ -1307,13 +1647,16 @@ export namespace ra3::vulkan {
|
|||||||
|
|
||||||
VkClearValue clear{};
|
VkClearValue clear{};
|
||||||
clear.color = {{0.05F, 0.06F, 0.08F, 1.0F}};
|
clear.color = {{0.05F, 0.06F, 0.08F, 1.0F}};
|
||||||
|
VkClearValue depth_clear{};
|
||||||
|
depth_clear.depthStencil = {1.0F, 0U};
|
||||||
|
const VkClearValue clears[2] = {clear, depth_clear};
|
||||||
VkRenderPassBeginInfo render_pass_begin{};
|
VkRenderPassBeginInfo render_pass_begin{};
|
||||||
render_pass_begin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
|
render_pass_begin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
|
||||||
render_pass_begin.renderPass = render_pass_;
|
render_pass_begin.renderPass = render_pass_;
|
||||||
render_pass_begin.framebuffer = framebuffers_[image_index];
|
render_pass_begin.framebuffer = framebuffers_[image_index];
|
||||||
render_pass_begin.renderArea = {{0U, 0U}, swapchain_extent_};
|
render_pass_begin.renderArea = {{0U, 0U}, swapchain_extent_};
|
||||||
render_pass_begin.clearValueCount = 1U;
|
render_pass_begin.clearValueCount = 2U;
|
||||||
render_pass_begin.pClearValues = &clear;
|
render_pass_begin.pClearValues = clears;
|
||||||
vkCmdBeginRenderPass(cmd, &render_pass_begin, VK_SUBPASS_CONTENTS_INLINE);
|
vkCmdBeginRenderPass(cmd, &render_pass_begin, VK_SUBPASS_CONTENTS_INLINE);
|
||||||
|
|
||||||
VkViewport viewport{0.0F, 0.0F, static_cast<float>(swapchain_extent_.width), static_cast<float>(swapchain_extent_.height), 0.0F, 1.0F};
|
VkViewport viewport{0.0F, 0.0F, static_cast<float>(swapchain_extent_.width), static_cast<float>(swapchain_extent_.height), 0.0F, 1.0F};
|
||||||
@@ -1433,6 +1776,9 @@ export namespace ra3::vulkan {
|
|||||||
std::vector<VkImage> swapchain_images_;
|
std::vector<VkImage> swapchain_images_;
|
||||||
std::vector<VkImageView> swapchain_views_;
|
std::vector<VkImageView> swapchain_views_;
|
||||||
std::vector<VkFramebuffer> framebuffers_;
|
std::vector<VkFramebuffer> framebuffers_;
|
||||||
|
std::vector<VkImage> depth_images_;
|
||||||
|
std::vector<VkDeviceMemory> depth_memories_;
|
||||||
|
std::vector<VkImageView> depth_views_;
|
||||||
VkRenderPass render_pass_ = VK_NULL_HANDLE;
|
VkRenderPass render_pass_ = VK_NULL_HANDLE;
|
||||||
VkDescriptorSetLayout descriptor_layout_ = VK_NULL_HANDLE;
|
VkDescriptorSetLayout descriptor_layout_ = VK_NULL_HANDLE;
|
||||||
VkPipelineLayout pipeline_layout_ = VK_NULL_HANDLE;
|
VkPipelineLayout pipeline_layout_ = VK_NULL_HANDLE;
|
||||||
@@ -1464,6 +1810,16 @@ export namespace ra3::vulkan {
|
|||||||
VkPipeline terrain_pipeline_ = VK_NULL_HANDLE;
|
VkPipeline terrain_pipeline_ = VK_NULL_HANDLE;
|
||||||
VkDescriptorPool terrain_pool_ = VK_NULL_HANDLE;
|
VkDescriptorPool terrain_pool_ = VK_NULL_HANDLE;
|
||||||
VkDescriptorSet terrain_set_ = VK_NULL_HANDLE;
|
VkDescriptorSet terrain_set_ = VK_NULL_HANDLE;
|
||||||
|
VkBuffer object_vertex_buffer_ = VK_NULL_HANDLE;
|
||||||
|
VkDeviceMemory object_vertex_memory_ = VK_NULL_HANDLE;
|
||||||
|
VkBuffer object_index_buffer_ = VK_NULL_HANDLE;
|
||||||
|
VkDeviceMemory object_index_memory_ = VK_NULL_HANDLE;
|
||||||
|
uint32_t object_index_count_ = 0U;
|
||||||
|
gpu_image object_texture_;
|
||||||
|
VkPipelineLayout object_pipeline_layout_ = VK_NULL_HANDLE;
|
||||||
|
VkPipeline object_pipeline_ = VK_NULL_HANDLE;
|
||||||
|
VkDescriptorSet object_set_ = VK_NULL_HANDLE;
|
||||||
|
bool objects_ready_ = false;
|
||||||
std::vector<VkSemaphore> image_available_;
|
std::vector<VkSemaphore> image_available_;
|
||||||
std::vector<VkSemaphore> render_finished_;
|
std::vector<VkSemaphore> render_finished_;
|
||||||
std::vector<VkFence> in_flight_;
|
std::vector<VkFence> in_flight_;
|
||||||
|
|||||||
@@ -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("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");
|
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) {
|
if (failures == 0) {
|
||||||
std::puts("ra3_tests: OK");
|
std::puts("ra3_tests: OK");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user