v0.8.0: map roads/sidewalks, per-vertex depth bias, retail camera controls
- ra3.map: capture the object RoadType; ra3.models pairs the map's Start/End road objects and drapes a flat textured ribbon along each segment (tessellated to the relief). - Ground decals (roads/sidewalks) get a per-vertex screen-space depth offset (scene::vertex.bias) applied in the object shader and the software rasteriser, so they no longer z-fight or clip into the terrain at top-down angles. - Tactical view aligned with retail (SAGE LookAtTranslator): middle-drag orbits yaw/pitch, a middle click resets angle/pitch/zoom, arrow keys pan, wheel zooms within the retail zoom range; WASD pan removed and left-drag no longer rotates. - ui_event gains middle/right/released buttons; Vulkan + SDL map them. - docs: roads, depth bias and camera controls.
This commit is contained in:
@@ -753,6 +753,7 @@ export namespace ra3::models {
|
||||
float u = 0.0F;
|
||||
float v = 0.0F;
|
||||
float layer = 0.0F;
|
||||
float bias = 0.0F; ///< Depth offset toward the camera (world units) for ground decals.
|
||||
};
|
||||
|
||||
/** Every model the map draws, flattened to world space and ready to upload. */
|
||||
@@ -763,6 +764,7 @@ export namespace ra3::models {
|
||||
uint32 texture_size = 0;
|
||||
usize placed = 0;
|
||||
usize missing = 0;
|
||||
usize roads = 0;
|
||||
|
||||
[[nodiscard]] auto empty() const -> bool { return indices.empty(); }
|
||||
[[nodiscard]] auto triangle_count() const -> usize { return indices.size() / 3U; }
|
||||
@@ -776,6 +778,7 @@ export namespace ra3::models {
|
||||
float z = 0.0F;
|
||||
float angle = 0.0F;
|
||||
float scale = 1.0F;
|
||||
uint32 road_type = 0U; ///< SAGE `RoadType` flags (0 = a normal object).
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
@@ -856,7 +859,125 @@ export namespace ra3::models {
|
||||
return it->second.empty() ? nullptr : &it->second;
|
||||
};
|
||||
|
||||
// Road/sidewalk decals: the map stores each segment as a consecutive
|
||||
// start/end pair of objects. They are flat ribbons laid on the terrain
|
||||
// (the GPU/software pass biases their depth so they do not clip).
|
||||
std::unordered_map<std::string, uint32> road_layers;
|
||||
const auto road_layer_for = [&](const std::string &type) -> uint32 {
|
||||
if (const auto it = road_layers.find(type); it != road_layers.end()) return it->second;
|
||||
uint32 layer = 0xFFFFFFFFU;
|
||||
if (const auto *road = stream.find_name("Road:" + type); road != nullptr && !road->references.empty()) {
|
||||
const auto [type_id, instance_id] = road->references.front();
|
||||
if (const auto *texture = stream.find(type_id, instance_id); texture != nullptr) layer = layer_for(texture);
|
||||
}
|
||||
road_layers.emplace(type, layer);
|
||||
return layer;
|
||||
};
|
||||
const auto road_width = [&](const std::string &type) -> float {
|
||||
if (const auto *road = stream.find_name("Road:" + type); road != nullptr) {
|
||||
const auto instance = stream.read_instance(*road);
|
||||
if (instance.size() >= 16U) return detail::f32(instance, 12U);
|
||||
}
|
||||
return 5.0F;
|
||||
};
|
||||
const auto emit_road = [&](const placement &a, const placement &b) {
|
||||
const auto dx = b.x - a.x;
|
||||
const auto dy = b.y - a.y;
|
||||
const auto length = std::sqrt(dx * dx + dy * dy);
|
||||
if (length < 1.0e-3F) return;
|
||||
const auto dir_x = dx / length;
|
||||
const auto dir_y = dy / length;
|
||||
const auto perp_x = -dir_y;
|
||||
const auto perp_y = dir_x;
|
||||
const auto half = 0.5F * road_width(a.type);
|
||||
const auto tile = std::max(1.0F, 2.0F * half);
|
||||
const auto extent = 0.0F; // no overlap: overlapping ribbons z-fight
|
||||
const auto total = length + 2.0F * extent;
|
||||
const auto start_x = a.x - dir_x * extent;
|
||||
const auto start_y = a.y - dir_y * extent;
|
||||
const auto height = [&](float x, float y) {
|
||||
// Take the highest terrain sample in the ribbon's footprint so it
|
||||
// never sinks into a cell step; the object pass adds the small
|
||||
// depth bias that retail uses for coplanar ground decals.
|
||||
const auto radius = std::max(2.0F, half);
|
||||
const auto sample = [&](float px, float py) { return ground_height ? ground_height(px, py) : 0.0F; };
|
||||
auto best = sample(x, y);
|
||||
best = std::max(best, sample(x + radius, y));
|
||||
best = std::max(best, sample(x - radius, y));
|
||||
best = std::max(best, sample(x, y + radius));
|
||||
best = std::max(best, sample(x, y - radius));
|
||||
return best + 1.0F;
|
||||
};
|
||||
const auto layer = road_layer_for(a.type);
|
||||
const auto layer_value = layer == 0xFFFFFFFFU ? 0.0F : static_cast<float>(layer);
|
||||
|
||||
// Drape the ribbon on the terrain: the segment is tessellated along
|
||||
// its length (one sample per terrain cell) so it follows the relief
|
||||
// instead of cutting through hills. The remaining coplanar z-fight is
|
||||
// handled by the object pass's depth bias.
|
||||
constexpr float sample_step = 10.0F;
|
||||
const auto steps = std::clamp(static_cast<int>(std::ceil(total / sample_step)), 1, 512);
|
||||
const auto vspan = total / tile;
|
||||
uint32 previous_left = 0U;
|
||||
uint32 previous_right = 0U;
|
||||
for (int k = 0; k <= steps; ++k) {
|
||||
const auto t = static_cast<float>(k) / static_cast<float>(steps);
|
||||
const auto cx = start_x + dir_x * total * t;
|
||||
const auto cy = start_y + dir_y * total * t;
|
||||
const auto lx = cx + perp_x * half;
|
||||
const auto ly = cy + perp_y * half;
|
||||
const auto rx = cx - perp_x * half;
|
||||
const auto ry = cy - perp_y * half;
|
||||
const auto left_index = static_cast<uint32>(out.vertices.size());
|
||||
vertex left;
|
||||
left.x = lx;
|
||||
left.y = ly;
|
||||
left.z = height(lx, ly);
|
||||
left.nx = 0.0F;
|
||||
left.ny = 0.0F;
|
||||
left.nz = 1.0F;
|
||||
left.u = 0.0F;
|
||||
left.v = vspan * t;
|
||||
left.layer = layer_value;
|
||||
left.bias = 2.0F;
|
||||
out.vertices.push_back(left);
|
||||
vertex right;
|
||||
right.x = rx;
|
||||
right.y = ry;
|
||||
right.z = height(rx, ry);
|
||||
right.nx = 0.0F;
|
||||
right.ny = 0.0F;
|
||||
right.nz = 1.0F;
|
||||
right.u = 1.0F;
|
||||
right.v = vspan * t;
|
||||
right.layer = layer_value;
|
||||
right.bias = 2.0F;
|
||||
out.vertices.push_back(right);
|
||||
const auto right_index = left_index + 1U;
|
||||
if (k > 0) {
|
||||
out.indices.push_back(previous_left);
|
||||
out.indices.push_back(previous_right);
|
||||
out.indices.push_back(right_index);
|
||||
out.indices.push_back(previous_left);
|
||||
out.indices.push_back(right_index);
|
||||
out.indices.push_back(left_index);
|
||||
}
|
||||
previous_left = left_index;
|
||||
previous_right = right_index;
|
||||
}
|
||||
++out.roads;
|
||||
};
|
||||
for (usize i = 0; i + 1U < placements.size(); ++i) {
|
||||
const auto &a = placements[i];
|
||||
if ((a.road_type & 2U) == 0U) continue; // RoadType::Start
|
||||
const auto &b = placements[i + 1U];
|
||||
if ((b.road_type & 4U) == 0U || b.type != a.type) continue; // RoadType::End
|
||||
emit_road(a, b);
|
||||
++i;
|
||||
}
|
||||
|
||||
for (const auto &item: placements) {
|
||||
if (item.road_type != 0U) continue; // handled as a road decal above
|
||||
const auto meshes = stream.meshes_for(item.type);
|
||||
if (meshes.empty()) {
|
||||
++out.missing;
|
||||
|
||||
Reference in New Issue
Block a user