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:
+1
-1
@@ -26,7 +26,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|||||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
set(CMAKE_CXX_SCAN_FOR_MODULES ON)
|
set(CMAKE_CXX_SCAN_FOR_MODULES ON)
|
||||||
|
|
||||||
project(OpenRA3 VERSION 0.6.0 LANGUAGES C CXX)
|
project(OpenRA3 VERSION 0.8.0 LANGUAGES C CXX)
|
||||||
|
|
||||||
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||||
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
|
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ Ghidra.
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
OpenRA3 is at **v0.7.0**. The engine compiles and runs headless, and a **minimal
|
OpenRA3 is at **v0.8.0**. The engine compiles and runs headless, and a **minimal
|
||||||
skirmish** is playable: it reads a real multiplayer map out of your install,
|
skirmish** is playable: it reads a real multiplayer map out of your install,
|
||||||
recovers the player start waypoints, and simulates two sides building a base,
|
recovers the player start waypoints, and simulates two sides building a base,
|
||||||
extracting ore and fighting until one side is wiped out. The balance is the
|
extracting ore and fighting until one side is wiped out. The balance is the
|
||||||
@@ -307,9 +307,10 @@ path rasterises it with a z-buffer. Match state is
|
|||||||
overlaid (start markers in yellow, player 0 in blue, player 1 in red).
|
overlaid (start markers in yellow, player 0 in blue, player 1 in red).
|
||||||
`--thumbnail` uses the old `<map>_art.tga` overview instead.
|
`--thumbnail` uses the old `<map>_art.tga` overview instead.
|
||||||
|
|
||||||
> **Not yet drawn:** the map's `Road` objects (the sidewalk/road segments that
|
> **Roads / sidewalks** are drawn too: the map stores each as a
|
||||||
> reference a `Road` template rather than a mesh) need the retail road-network
|
> `RoadType::Start`/`End` pair of objects, and `render` emits a flat textured
|
||||||
> mesher and are skipped; they are reported in the `objects: … missing` count.
|
> ribbon along each segment, lifted onto the terrain with a small depth bias so
|
||||||
|
> it does not clip into the ground.
|
||||||
|
|
||||||
**Offscreen image** (works anywhere, no display needed):
|
**Offscreen image** (works anywhere, no display needed):
|
||||||
|
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ namespace {
|
|||||||
const auto stream = ra3::models::asset_stream::load_files(paths->manifest, paths->bin);
|
const auto stream = ra3::models::asset_stream::load_files(paths->manifest, paths->bin);
|
||||||
std::vector<ra3::models::placement> placements;
|
std::vector<ra3::models::placement> placements;
|
||||||
for (const auto &object: ra3::map::parse_objects(ckmp)) {
|
for (const auto &object: ra3::map::parse_objects(ckmp)) {
|
||||||
placements.push_back({object.type, object.x, object.y, object.z, object.angle, object.scale});
|
placements.push_back({object.type, object.x, object.y, object.z, object.angle, object.scale, object.road_type});
|
||||||
}
|
}
|
||||||
const auto world_w = terrain.world_width();
|
const auto world_w = terrain.world_width();
|
||||||
const auto world_h = terrain.world_height();
|
const auto world_h = terrain.world_height();
|
||||||
@@ -265,8 +265,8 @@ namespace {
|
|||||||
const auto cy = std::min(terrain.height - 1U, static_cast<ra3::core::uint32>((world_h - y) / ra3::terrain::cell_size));
|
const auto cy = std::min(terrain.height - 1U, static_cast<ra3::core::uint32>((world_h - y) / ra3::terrain::cell_size));
|
||||||
return static_cast<float>(terrain.elevation(cx, cy)) * options.z_scale;
|
return static_cast<float>(terrain.elevation(cx, cy)) * options.z_scale;
|
||||||
});
|
});
|
||||||
std::printf("objects: %zu placed, %zu missing, %zu triangles, %zu textures (%s)\n", scene.placed, scene.missing, scene.triangle_count(),
|
std::printf("objects: %zu placed, %zu missing, %zu road segments, %zu triangles, %zu textures (%s)\n", scene.placed, scene.missing,
|
||||||
scene.textures.size(), paths->bin.filename().string().c_str());
|
scene.roads, scene.triangle_count(), scene.textures.size(), paths->bin.filename().string().c_str());
|
||||||
} catch (const std::exception &error) {
|
} catch (const std::exception &error) {
|
||||||
std::printf("objects: failed to build scene (%s)\n", error.what());
|
std::printf("objects: failed to build scene (%s)\n", error.what());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -328,8 +328,29 @@ The map's `ObjectsList` chunk is a list of nested `Object` assets —
|
|||||||
`Coord3D`, Z `angle`, `RoadType` u32, a `u16`-prefixed type-name and a property
|
`Coord3D`, Z `angle`, `RoadType` u32, a `u16`-prefixed type-name and a property
|
||||||
list whose keys index the shared name table (`ra3.map::parse_objects`). Each
|
list whose keys index the shared name table (`ra3.map::parse_objects`). Each
|
||||||
type resolves to the `W3DMesh` parts whose instance name equals it or starts
|
type resolves to the `W3DMesh` parts whose instance name equals it or starts
|
||||||
with `<type>.`. Objects whose type is a `Road` template (the sidewalk/road
|
with `<type>.`.
|
||||||
segments) are not meshes and are not drawn yet.
|
|
||||||
|
### Roads / sidewalks (`Road` assets)
|
||||||
|
|
||||||
|
The flat sidewalk and road decals are not meshes: the map places them as
|
||||||
|
**consecutive pairs** of objects — a `RoadType::Start` (`2`) and a
|
||||||
|
`RoadType::End` (`4`) at the segment's two ends (`Angled` `8`, `TightCurve`
|
||||||
|
`64` and `EndCap` `128` are curve/cap flags; `BridgeStart/End` `16/32` are
|
||||||
|
bridges). The road's type-name (`IslandFortressSidewalk01`, ...) resolves to a
|
||||||
|
compiled `Road` asset in the same art stream:
|
||||||
|
|
||||||
|
```
|
||||||
|
Road: u32 version, u32 pad, u32 textureCount, f32 roadWidth, f32 pad,
|
||||||
|
f32 ???, then textureCount texture references (diffuse, normal)
|
||||||
|
```
|
||||||
|
|
||||||
|
`ra3.models` pairs the objects in file order, emits a flat quad ribbon of
|
||||||
|
`roadWidth` along each segment (overlapping the joins by half a width), sampled
|
||||||
|
with the diffuse texture, and lifts it onto the terrain. Retail gives roads a
|
||||||
|
small depth offset toward the screen so they do not clip into the ground; the
|
||||||
|
object pass reproduces that with the same negative depth bias used for the other
|
||||||
|
ground decals (`depthBiasConstant/SlopeFactor` on Vulkan, a half-unit bias in the
|
||||||
|
software rasteriser).
|
||||||
|
|
||||||
### Map display names
|
### Map display names
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
+2
-1
@@ -13,6 +13,7 @@ layout(location = 0) in vec3 in_pos;
|
|||||||
layout(location = 1) in vec3 in_normal;
|
layout(location = 1) in vec3 in_normal;
|
||||||
layout(location = 2) in vec2 in_uv;
|
layout(location = 2) in vec2 in_uv;
|
||||||
layout(location = 3) in float in_layer;
|
layout(location = 3) in float in_layer;
|
||||||
|
layout(location = 4) in float in_bias;
|
||||||
|
|
||||||
layout(push_constant) uniform Push {
|
layout(push_constant) uniform Push {
|
||||||
vec4 cam_pos; // xyz = eye position
|
vec4 cam_pos; // xyz = eye position
|
||||||
@@ -36,7 +37,7 @@ void main() {
|
|||||||
float c = dot(r, pc.up.xyz);
|
float c = dot(r, pc.up.xyz);
|
||||||
float th = pc.right.w;
|
float th = pc.right.w;
|
||||||
float th_aspect = pc.up.w;
|
float th_aspect = pc.up.w;
|
||||||
float depth = clamp((a - NEAR) / (FAR - NEAR), 0.0, 1.0);
|
float depth = clamp((a - in_bias - NEAR) / (FAR - NEAR), 0.0, 1.0);
|
||||||
gl_Position = vec4(b / th_aspect, -c / th, depth * a, a);
|
gl_Position = vec4(b / th_aspect, -c / th, depth * a, a);
|
||||||
out_uv = in_uv;
|
out_uv = in_uv;
|
||||||
out_normal = in_normal;
|
out_normal = in_normal;
|
||||||
|
|||||||
@@ -370,6 +370,8 @@ export namespace ra3::client {
|
|||||||
|
|
||||||
bool running = true;
|
bool running = true;
|
||||||
bool presented = false;
|
bool presented = false;
|
||||||
|
const auto default_camera = camera;
|
||||||
|
bool middle_dragged = false;
|
||||||
while (running) {
|
while (running) {
|
||||||
ui_event event;
|
ui_event event;
|
||||||
float drag_x = 0.0F;
|
float drag_x = 0.0F;
|
||||||
@@ -388,13 +390,23 @@ export namespace ra3::client {
|
|||||||
} else if (event.type == ui_event_type::mouse_move) {
|
} else if (event.type == ui_event_type::mouse_move) {
|
||||||
mouse_x = event.x;
|
mouse_x = event.x;
|
||||||
mouse_y = event.y;
|
mouse_y = event.y;
|
||||||
if (event.left) {
|
// Retail RA3: the middle button orbits the camera; the
|
||||||
|
// left button is selection only, not camera rotation.
|
||||||
|
if (event.middle) {
|
||||||
drag_x += event.dx;
|
drag_x += event.dx;
|
||||||
drag_y += event.dy;
|
drag_y += event.dy;
|
||||||
|
if (event.dx != 0.0F || event.dy != 0.0F) middle_dragged = true;
|
||||||
}
|
}
|
||||||
} else if (event.type == ui_event_type::wheel) {
|
} else if (event.type == ui_event_type::wheel) {
|
||||||
camera.height = std::clamp(camera.height * (event.wheel > 0.0F ? (1.0F / 1.15F) : 1.15F), camera.min_height, camera.max_height);
|
camera.height = std::clamp(camera.height * (event.wheel > 0.0F ? (1.0F / 1.15F) : 1.15F), camera.min_height, camera.max_height);
|
||||||
camera_moved = true;
|
camera_moved = true;
|
||||||
|
} else if (event.type == ui_event_type::mouse_button && event.middle && event.released) {
|
||||||
|
if (!middle_dragged) {
|
||||||
|
camera.yaw = default_camera.yaw;
|
||||||
|
camera.pitch = default_camera.pitch;
|
||||||
|
camera.height = default_camera.height;
|
||||||
|
camera_moved = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!running) break;
|
if (!running) break;
|
||||||
@@ -404,8 +416,10 @@ export namespace ra3::client {
|
|||||||
last = now;
|
last = now;
|
||||||
|
|
||||||
if (drag_x != 0.0F || drag_y != 0.0F) {
|
if (drag_x != 0.0F || drag_y != 0.0F) {
|
||||||
camera.yaw -= drag_x * 0.005F;
|
// SAGE LookAtTranslator: middle-drag rotates yaw (X) and
|
||||||
camera.pitch = std::clamp(camera.pitch + drag_y * 0.004F, 0.15F, 1.45F);
|
// pitch (Y) at 0.01 rad per pixel; pitch is clamped to +-36 deg.
|
||||||
|
camera.yaw -= drag_x * 0.01F;
|
||||||
|
camera.pitch = std::clamp(camera.pitch + drag_y * 0.01F, 0.15F, 1.45F);
|
||||||
camera_moved = true;
|
camera_moved = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -218,6 +218,7 @@ export namespace ra3::map {
|
|||||||
real z = 0.0F;
|
real z = 0.0F;
|
||||||
real angle = 0.0F;
|
real angle = 0.0F;
|
||||||
real scale = 1.0F;
|
real scale = 1.0F;
|
||||||
|
uint32 road_type = 0U; ///< SAGE `RoadType` flags; 0 for a non-road object.
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace detail {
|
namespace detail {
|
||||||
@@ -303,6 +304,7 @@ export namespace ra3::map {
|
|||||||
object.y = read_f32(p + 4U);
|
object.y = read_f32(p + 4U);
|
||||||
object.z = read_f32(p + 8U);
|
object.z = read_f32(p + 8U);
|
||||||
object.angle = read_f32(p + 12U);
|
object.angle = read_f32(p + 12U);
|
||||||
|
object.road_type = read_u32(p + 16U);
|
||||||
p += 20U; // Coord3D + angle + road type
|
p += 20U; // Coord3D + angle + road type
|
||||||
const auto name_len = read_u16(p);
|
const auto name_len = read_u16(p);
|
||||||
p += 2U;
|
p += 2U;
|
||||||
|
|||||||
@@ -753,6 +753,7 @@ export namespace ra3::models {
|
|||||||
float u = 0.0F;
|
float u = 0.0F;
|
||||||
float v = 0.0F;
|
float v = 0.0F;
|
||||||
float layer = 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. */
|
/** 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;
|
uint32 texture_size = 0;
|
||||||
usize placed = 0;
|
usize placed = 0;
|
||||||
usize missing = 0;
|
usize missing = 0;
|
||||||
|
usize roads = 0;
|
||||||
|
|
||||||
[[nodiscard]] auto empty() const -> bool { return indices.empty(); }
|
[[nodiscard]] auto empty() const -> bool { return indices.empty(); }
|
||||||
[[nodiscard]] auto triangle_count() const -> usize { return indices.size() / 3U; }
|
[[nodiscard]] auto triangle_count() const -> usize { return indices.size() / 3U; }
|
||||||
@@ -776,6 +778,7 @@ export namespace ra3::models {
|
|||||||
float z = 0.0F;
|
float z = 0.0F;
|
||||||
float angle = 0.0F;
|
float angle = 0.0F;
|
||||||
float scale = 1.0F;
|
float scale = 1.0F;
|
||||||
|
uint32 road_type = 0U; ///< SAGE `RoadType` flags (0 = a normal object).
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace detail {
|
namespace detail {
|
||||||
@@ -856,7 +859,125 @@ export namespace ra3::models {
|
|||||||
return it->second.empty() ? nullptr : &it->second;
|
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) {
|
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);
|
const auto meshes = stream.meshes_for(item.type);
|
||||||
if (meshes.empty()) {
|
if (meshes.empty()) {
|
||||||
++out.missing;
|
++out.missing;
|
||||||
|
|||||||
@@ -277,6 +277,9 @@ export namespace ra3::render {
|
|||||||
float dy = 0.0F;
|
float dy = 0.0F;
|
||||||
float wheel = 0.0F;
|
float wheel = 0.0F;
|
||||||
bool left = false;
|
bool left = false;
|
||||||
|
bool middle = false;
|
||||||
|
bool right = false;
|
||||||
|
bool released = false; ///< mouse_button: true for a button-up event.
|
||||||
};
|
};
|
||||||
|
|
||||||
/** A warm red/gold loading screen with a progress bar (0..1). */
|
/** A warm red/gold loading screen with a progress bar (0..1). */
|
||||||
@@ -675,7 +678,9 @@ export namespace ra3::render {
|
|||||||
float pitch = 1.02F; ///< Radians above the horizon (~58 degrees down).
|
float pitch = 1.02F; ///< Radians above the horizon (~58 degrees down).
|
||||||
float height = 420.0F; ///< Camera height above the target's ground.
|
float height = 420.0F; ///< Camera height above the target's ground.
|
||||||
float fov = 0.85F; ///< Vertical field of view, radians.
|
float fov = 0.85F; ///< Vertical field of view, radians.
|
||||||
float min_height = 120.0F;
|
// Zoom range mirrors the retail TacticalView (zoom 0.2..1.3 around the
|
||||||
float max_height = 1600.0F;
|
// default height): closer/farther than that is clamped.
|
||||||
|
float min_height = 320.0F;
|
||||||
|
float max_height = 2100.0F;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -841,9 +841,11 @@ export namespace ra3::terrain {
|
|||||||
const auto inv_a = w0 * p0.inv_a + w1 * p1.inv_a + w2 * p2.inv_a;
|
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 depth = 1.0F / inv_a;
|
||||||
const auto pixel = static_cast<usize>(y) * static_cast<usize>(rw) + static_cast<usize>(x);
|
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
|
// Ground decals carry a per-vertex bias toward the camera
|
||||||
// (mirrors the GPU depth bias) keeps them from z-fighting.
|
// (the retail shader's screen-space depth offset); it
|
||||||
if (depth - 0.5F >= zbuf[pixel]) continue;
|
// keeps roads/sidewalks from z-fighting the terrain.
|
||||||
|
const auto bias = w0 * v0.bias + w1 * v1.bias + w2 * v2.bias;
|
||||||
|
if (depth - bias - 1.0F >= 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 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;
|
const auto tv = (w0 * v0.v * p0.inv_a + w1 * v1.v * p1.inv_a + w2 * v2.v * p2.inv_a) * depth;
|
||||||
|
|||||||
+18
-4
@@ -101,6 +101,8 @@ export namespace ra3::ui {
|
|||||||
out.dx = event.motion.xrel;
|
out.dx = event.motion.xrel;
|
||||||
out.dy = event.motion.yrel;
|
out.dy = event.motion.yrel;
|
||||||
out.left = (event.motion.state & SDL_BUTTON_LMASK) != 0U;
|
out.left = (event.motion.state & SDL_BUTTON_LMASK) != 0U;
|
||||||
|
out.middle = (event.motion.state & SDL_BUTTON_MMASK) != 0U;
|
||||||
|
out.right = (event.motion.state & SDL_BUTTON_RMASK) != 0U;
|
||||||
return true;
|
return true;
|
||||||
case SDL_EVENT_MOUSE_BUTTON_DOWN:
|
case SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||||
out = {};
|
out = {};
|
||||||
@@ -108,6 +110,18 @@ export namespace ra3::ui {
|
|||||||
out.x = event.button.x;
|
out.x = event.button.x;
|
||||||
out.y = event.button.y;
|
out.y = event.button.y;
|
||||||
out.left = event.button.button == SDL_BUTTON_LEFT;
|
out.left = event.button.button == SDL_BUTTON_LEFT;
|
||||||
|
out.middle = event.button.button == SDL_BUTTON_MIDDLE;
|
||||||
|
out.right = event.button.button == SDL_BUTTON_RIGHT;
|
||||||
|
return true;
|
||||||
|
case SDL_EVENT_MOUSE_BUTTON_UP:
|
||||||
|
out = {};
|
||||||
|
out.type = ra3::render::ui_event_type::mouse_button;
|
||||||
|
out.x = event.button.x;
|
||||||
|
out.y = event.button.y;
|
||||||
|
out.released = true;
|
||||||
|
out.left = event.button.button == SDL_BUTTON_LEFT;
|
||||||
|
out.middle = event.button.button == SDL_BUTTON_MIDDLE;
|
||||||
|
out.right = event.button.button == SDL_BUTTON_RIGHT;
|
||||||
return true;
|
return true;
|
||||||
case SDL_EVENT_MOUSE_WHEEL:
|
case SDL_EVENT_MOUSE_WHEEL:
|
||||||
out = {};
|
out = {};
|
||||||
@@ -132,10 +146,10 @@ export namespace ra3::ui {
|
|||||||
const bool *keys = SDL_GetKeyboardState(nullptr);
|
const bool *keys = SDL_GetKeyboardState(nullptr);
|
||||||
if (keys == nullptr) return false;
|
if (keys == nullptr) return false;
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case ra3::render::ui_key::up: return keys[SDL_SCANCODE_W] || keys[SDL_SCANCODE_UP];
|
case ra3::render::ui_key::up: return keys[SDL_SCANCODE_UP];
|
||||||
case ra3::render::ui_key::down: return keys[SDL_SCANCODE_S] || keys[SDL_SCANCODE_DOWN];
|
case ra3::render::ui_key::down: return keys[SDL_SCANCODE_DOWN];
|
||||||
case ra3::render::ui_key::left: return keys[SDL_SCANCODE_A] || keys[SDL_SCANCODE_LEFT];
|
case ra3::render::ui_key::left: return keys[SDL_SCANCODE_LEFT];
|
||||||
case ra3::render::ui_key::right: return keys[SDL_SCANCODE_D] || keys[SDL_SCANCODE_RIGHT];
|
case ra3::render::ui_key::right: return keys[SDL_SCANCODE_RIGHT];
|
||||||
default: return false;
|
default: return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -295,6 +295,8 @@ export namespace ra3::vulkan {
|
|||||||
out.dx = event.motion.xrel;
|
out.dx = event.motion.xrel;
|
||||||
out.dy = event.motion.yrel;
|
out.dy = event.motion.yrel;
|
||||||
out.left = (event.motion.state & SDL_BUTTON_LMASK) != 0U;
|
out.left = (event.motion.state & SDL_BUTTON_LMASK) != 0U;
|
||||||
|
out.middle = (event.motion.state & SDL_BUTTON_MMASK) != 0U;
|
||||||
|
out.right = (event.motion.state & SDL_BUTTON_RMASK) != 0U;
|
||||||
return true;
|
return true;
|
||||||
case SDL_EVENT_MOUSE_BUTTON_DOWN:
|
case SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||||
out = {};
|
out = {};
|
||||||
@@ -302,6 +304,18 @@ export namespace ra3::vulkan {
|
|||||||
out.x = event.button.x;
|
out.x = event.button.x;
|
||||||
out.y = event.button.y;
|
out.y = event.button.y;
|
||||||
out.left = event.button.button == SDL_BUTTON_LEFT;
|
out.left = event.button.button == SDL_BUTTON_LEFT;
|
||||||
|
out.middle = event.button.button == SDL_BUTTON_MIDDLE;
|
||||||
|
out.right = event.button.button == SDL_BUTTON_RIGHT;
|
||||||
|
return true;
|
||||||
|
case SDL_EVENT_MOUSE_BUTTON_UP:
|
||||||
|
out = {};
|
||||||
|
out.type = ra3::render::ui_event_type::mouse_button;
|
||||||
|
out.x = event.button.x;
|
||||||
|
out.y = event.button.y;
|
||||||
|
out.released = true;
|
||||||
|
out.left = event.button.button == SDL_BUTTON_LEFT;
|
||||||
|
out.middle = event.button.button == SDL_BUTTON_MIDDLE;
|
||||||
|
out.right = event.button.button == SDL_BUTTON_RIGHT;
|
||||||
return true;
|
return true;
|
||||||
case SDL_EVENT_MOUSE_WHEEL:
|
case SDL_EVENT_MOUSE_WHEEL:
|
||||||
out = {};
|
out = {};
|
||||||
@@ -326,10 +340,10 @@ export namespace ra3::vulkan {
|
|||||||
const bool *keys = SDL_GetKeyboardState(nullptr);
|
const bool *keys = SDL_GetKeyboardState(nullptr);
|
||||||
if (keys == nullptr) return false;
|
if (keys == nullptr) return false;
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case ra3::render::ui_key::up: return keys[SDL_SCANCODE_W] || keys[SDL_SCANCODE_UP];
|
case ra3::render::ui_key::up: return keys[SDL_SCANCODE_UP];
|
||||||
case ra3::render::ui_key::down: return keys[SDL_SCANCODE_S] || keys[SDL_SCANCODE_DOWN];
|
case ra3::render::ui_key::down: return keys[SDL_SCANCODE_DOWN];
|
||||||
case ra3::render::ui_key::left: return keys[SDL_SCANCODE_A] || keys[SDL_SCANCODE_LEFT];
|
case ra3::render::ui_key::left: return keys[SDL_SCANCODE_LEFT];
|
||||||
case ra3::render::ui_key::right: return keys[SDL_SCANCODE_D] || keys[SDL_SCANCODE_RIGHT];
|
case ra3::render::ui_key::right: return keys[SDL_SCANCODE_RIGHT];
|
||||||
default: return false;
|
default: return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -844,17 +858,18 @@ export namespace ra3::vulkan {
|
|||||||
binding.binding = 0U;
|
binding.binding = 0U;
|
||||||
binding.stride = sizeof(ra3::models::vertex);
|
binding.stride = sizeof(ra3::models::vertex);
|
||||||
binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
|
binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
|
||||||
VkVertexInputAttributeDescription attributes[4]{};
|
VkVertexInputAttributeDescription attributes[5]{};
|
||||||
attributes[0] = {0U, 0U, VK_FORMAT_R32G32B32_SFLOAT, 0U};
|
attributes[0] = {0U, 0U, VK_FORMAT_R32G32B32_SFLOAT, 0U};
|
||||||
attributes[1] = {1U, 0U, VK_FORMAT_R32G32B32_SFLOAT, 12U};
|
attributes[1] = {1U, 0U, VK_FORMAT_R32G32B32_SFLOAT, 12U};
|
||||||
attributes[2] = {2U, 0U, VK_FORMAT_R32G32_SFLOAT, 24U};
|
attributes[2] = {2U, 0U, VK_FORMAT_R32G32_SFLOAT, 24U};
|
||||||
attributes[3] = {3U, 0U, VK_FORMAT_R32_SFLOAT, 32U};
|
attributes[3] = {3U, 0U, VK_FORMAT_R32_SFLOAT, 32U};
|
||||||
static_assert(sizeof(ra3::models::vertex) == 36U, "object vertex layout changed");
|
attributes[4] = {4U, 0U, VK_FORMAT_R32_SFLOAT, 36U};
|
||||||
|
static_assert(sizeof(ra3::models::vertex) == 40U, "object vertex layout changed");
|
||||||
VkPipelineVertexInputStateCreateInfo vertex_input{};
|
VkPipelineVertexInputStateCreateInfo vertex_input{};
|
||||||
vertex_input.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
|
vertex_input.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
|
||||||
vertex_input.vertexBindingDescriptionCount = 1U;
|
vertex_input.vertexBindingDescriptionCount = 1U;
|
||||||
vertex_input.pVertexBindingDescriptions = &binding;
|
vertex_input.pVertexBindingDescriptions = &binding;
|
||||||
vertex_input.vertexAttributeDescriptionCount = 4U;
|
vertex_input.vertexAttributeDescriptionCount = 5U;
|
||||||
vertex_input.pVertexAttributeDescriptions = attributes;
|
vertex_input.pVertexAttributeDescriptions = attributes;
|
||||||
|
|
||||||
VkPipelineInputAssemblyStateCreateInfo assembly{};
|
VkPipelineInputAssemblyStateCreateInfo assembly{};
|
||||||
@@ -873,8 +888,8 @@ export namespace ra3::vulkan {
|
|||||||
// Ground decals (sidewalks/roads/deck pieces) sit exactly on the
|
// Ground decals (sidewalks/roads/deck pieces) sit exactly on the
|
||||||
// terrain; a small negative depth bias keeps them from z-fighting.
|
// terrain; a small negative depth bias keeps them from z-fighting.
|
||||||
raster.depthBiasEnable = VK_TRUE;
|
raster.depthBiasEnable = VK_TRUE;
|
||||||
raster.depthBiasConstantFactor = -1.0F;
|
raster.depthBiasConstantFactor = -4.0F;
|
||||||
raster.depthBiasSlopeFactor = -1.0F;
|
raster.depthBiasSlopeFactor = -4.0F;
|
||||||
VkPipelineMultisampleStateCreateInfo multisample{};
|
VkPipelineMultisampleStateCreateInfo multisample{};
|
||||||
multisample.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
|
multisample.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
|
||||||
multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
|
multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
|
||||||
|
|||||||
Reference in New Issue
Block a user