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

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

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

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

Ground-decal meshes with no diffuse role and the Road templates themselves are
still not drawn.
This commit is contained in:
EnderTheCoder
2026-09-29 18:08:11 +08:00
parent 6e5df350c7
commit 70f35d8382
18 changed files with 2027 additions and 45 deletions
+133 -1
View File
@@ -19,8 +19,9 @@ export import ra3.fs;
export namespace ra3::map {
using ra3::core::coord3d;
using ra3::core::real;
using ra3::core::uint32;
using ra3::core::uint8;
using ra3::core::uint16;
using ra3::core::uint32;
using ra3::core::usize;
/** A player start location extracted from a map's waypoints. */
@@ -202,6 +203,137 @@ export namespace ra3::map {
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. */
struct map_name_table {
std::unordered_map<std::string, std::string> names;
+939
View File
@@ -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;
}
}
+1
View File
@@ -16,6 +16,7 @@ export import ra3.map;
export import ra3.skirmish;
export import ra3.render;
export import ra3.terrain;
export import ra3.models;
export import ra3.ui;
export import ra3.vulkan;
export import ra3.dx;
+161 -6
View File
@@ -5,6 +5,7 @@ import std;
export import ra3.core;
export import ra3.fs;
export import ra3.render;
export import ra3.models;
/**
* 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;
bool has_water = false;
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
* 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 = {},
const std::function<void(float)> &progress = {}) -> gpu_terrain {
[[nodiscard]] inline auto build_gpu_terrain(const map_data &map, const texture_set &set, const render_options &options,
const ra3::models::scene &objects, const std::function<void(float)> &progress = {}) -> gpu_terrain {
gpu_terrain out;
out.width = map.width;
out.height = map.height;
out.z_scale = options.z_scale;
out.has_water = map.has_water;
out.water_z = map.water_plane_z;
out.objects = objects;
// Cell index -> texture layer.
const auto layer_of = [&](uint32 cell_index) -> uint16 {
@@ -566,6 +569,12 @@ export namespace ra3::terrain {
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 {
/**
* 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;
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.
*/
[[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");
out_w = std::max(1U, out_w);
out_h = std::max(1U, out_h);
@@ -968,6 +1115,7 @@ export namespace ra3::terrain {
};
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) {
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) {
@@ -979,10 +1127,11 @@ export namespace ra3::terrain {
dx /= dlen;
dy /= dlen;
dz /= dlen;
const auto pixel = static_cast<usize>(py) * rw + px;
if (dz >= -1.0e-4F) {
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));
continue;
}
@@ -1017,7 +1166,7 @@ export namespace ra3::terrain {
t += dt;
}
if (!hit) {
hi.data()[static_cast<usize>(py) * rw + px] = argb(150, 170, 200);
hi.data()[pixel] = argb(150, 170, 200);
continue;
}
@@ -1035,10 +1184,16 @@ export namespace ra3::terrain {
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;
image out(out_w, out_h, argb(0, 0, 0));
for (uint32 y = 0; y < out_h; ++y) {
+373 -17
View File
@@ -92,6 +92,7 @@ export namespace ra3::vulkan {
if (!this->create_device()) return false;
if (!this->create_swapchain()) return false;
if (!this->create_render_pass()) return false;
if (!this->create_framebuffers()) return false;
if (!this->create_pipeline()) return false;
if (!this->create_commands()) 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_images(terrain)) 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;
}
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{};
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;
VkPipelineColorBlendStateCreateInfo blend{};
@@ -690,6 +702,7 @@ export namespace ra3::vulkan {
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 = terrain_pipeline_layout_;
@@ -737,6 +750,197 @@ export namespace ra3::vulkan {
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 {
vkWaitForFences(device_, 1U, &in_flight_[current_frame_], VK_TRUE, UINT64_MAX);
@@ -754,13 +958,16 @@ export namespace ra3::vulkan {
VkClearValue clear{};
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{};
render_pass_begin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
render_pass_begin.renderPass = render_pass_;
render_pass_begin.framebuffer = framebuffers_[image_index];
render_pass_begin.renderArea = {{0U, 0U}, swapchain_extent_};
render_pass_begin.clearValueCount = 1U;
render_pass_begin.pClearValues = &clear;
render_pass_begin.clearValueCount = 2U;
render_pass_begin.pClearValues = clears;
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};
@@ -793,6 +1000,49 @@ export namespace ra3::vulkan {
vkCmdPushConstants(cmd, terrain_pipeline_layout_, VK_SHADER_STAGE_FRAGMENT_BIT, 0U, sizeof(push), push);
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
// 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) {
@@ -861,6 +1111,27 @@ export namespace ra3::vulkan {
terrain_set_ = VK_NULL_HANDLE;
overlay_label_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);
for (uint32_t i = 0; i < actual; ++i) {
VkImageView attachments[] = {swapchain_views_[i]};
return true;
}
/** 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{};
framebuffer_info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
framebuffer_info.renderPass = render_pass_;
framebuffer_info.attachmentCount = 1U;
framebuffer_info.attachmentCount = 2U;
framebuffer_info.pAttachments = attachments;
framebuffer_info.width = extent.width;
framebuffer_info.height = extent.height;
framebuffer_info.width = swapchain_extent_.width;
framebuffer_info.height = swapchain_extent_.height;
framebuffer_info.layers = 1U;
if (!detail::check(vkCreateFramebuffer(device_, &framebuffer_info, nullptr, &framebuffers_[i]), "vkCreateFramebuffer")) return false;
}
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 {
for (const auto framebuffer: framebuffers_) vkDestroyFramebuffer(device_, framebuffer, nullptr);
this->destroy_depth_resources();
for (const auto view: swapchain_views_) vkDestroyImageView(device_, view, nullptr);
framebuffers_.clear();
swapchain_views_.clear();
@@ -1041,7 +1367,7 @@ export namespace ra3::vulkan {
if (width == 0 || height == 0) return false;
vkDeviceWaitIdle(device_);
this->cleanup_swapchain();
return this->create_swapchain();
return this->create_swapchain() && this->create_framebuffers();
}
// ---- pipeline --------------------------------------------------------
@@ -1058,22 +1384,36 @@ export namespace ra3::vulkan {
color.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
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{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1U;
subpass.pColorAttachments = &color_ref;
subpass.pDepthStencilAttachment = &depth_ref;
VkSubpassDependency dependency{};
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
dependency.dstSubpass = 0U;
dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_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 | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_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{};
render_pass_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
render_pass_info.attachmentCount = 1U;
render_pass_info.pAttachments = &color;
render_pass_info.attachmentCount = 2U;
render_pass_info.pAttachments = attachments;
render_pass_info.subpassCount = 1U;
render_pass_info.pSubpasses = &subpass;
render_pass_info.dependencyCount = 1U;
@@ -1211,10 +1551,10 @@ export namespace ra3::vulkan {
command_buffers_.resize(frames_in_flight);
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{};
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.pPoolSizes = &pool_size;
if (!detail::check(vkCreateDescriptorPool(device_, &descriptor_pool_info, nullptr, &descriptor_pool_), "vkCreateDescriptorPool")) return false;
@@ -1307,13 +1647,16 @@ export namespace ra3::vulkan {
VkClearValue clear{};
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{};
render_pass_begin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
render_pass_begin.renderPass = render_pass_;
render_pass_begin.framebuffer = framebuffers_[image_index];
render_pass_begin.renderArea = {{0U, 0U}, swapchain_extent_};
render_pass_begin.clearValueCount = 1U;
render_pass_begin.pClearValues = &clear;
render_pass_begin.clearValueCount = 2U;
render_pass_begin.pClearValues = clears;
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};
@@ -1433,6 +1776,9 @@ export namespace ra3::vulkan {
std::vector<VkImage> swapchain_images_;
std::vector<VkImageView> swapchain_views_;
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;
VkDescriptorSetLayout descriptor_layout_ = VK_NULL_HANDLE;
VkPipelineLayout pipeline_layout_ = VK_NULL_HANDLE;
@@ -1464,6 +1810,16 @@ export namespace ra3::vulkan {
VkPipeline terrain_pipeline_ = VK_NULL_HANDLE;
VkDescriptorPool terrain_pool_ = 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> render_finished_;
std::vector<VkFence> in_flight_;