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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user