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:
EnderTheCoder
2026-09-29 20:37:24 +08:00
parent 70f35d8382
commit 8047e80ed6
14 changed files with 229 additions and 33 deletions
+1 -1
View File
@@ -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)
+5 -4
View File
@@ -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):
+1 -1
View File
@@ -1 +1 @@
0.7.0 0.8.0
+3 -3
View File
@@ -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());
} }
+23 -2
View File
@@ -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
View File
@@ -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;
+17 -3
View File
@@ -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;
} }
+2
View File
@@ -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;
+121
View File
@@ -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;
+7 -2
View File
@@ -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;
}; };
} }
+5 -3
View File
@@ -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
View File
@@ -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;
} }
} }
+24 -9
View File
@@ -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;