From 8047e80ed62b4404616582dc0f0caebc049ed3f6 Mon Sep 17 00:00:00 2001 From: EnderTheCoder Date: Tue, 29 Sep 2026 20:37:24 +0800 Subject: [PATCH] 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. --- CMakeLists.txt | 2 +- README.md | 9 ++- VERSION | 2 +- apps/openra3/main.cpp | 6 +- docs/REVERSE_ENGINEERING.md | 25 +++++- shaders/generated/object.vert.spv | Bin 2812 -> 2900 bytes shaders/object.vert | 3 +- src/client/ra3.client.cppm | 20 ++++- src/map/ra3.map.cppm | 2 + src/models/ra3.models.cppm | 121 ++++++++++++++++++++++++++++++ src/render/ra3.render.cppm | 9 ++- src/terrain/ra3.terrain.cppm | 8 +- src/ui/ra3.ui.sdl.cppm | 22 +++++- src/vulkan/ra3.vulkan.sdl.cppm | 33 +++++--- 14 files changed, 229 insertions(+), 33 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2a501d7..a8b4dbf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,7 +26,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) 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) set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) diff --git a/README.md b/README.md index 4b20249..2eed0dc 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Ghidra. ## 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, 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 @@ -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). `--thumbnail` uses the old `_art.tga` overview instead. -> **Not yet drawn:** the map's `Road` objects (the sidewalk/road segments that -> reference a `Road` template rather than a mesh) need the retail road-network -> mesher and are skipped; they are reported in the `objects: … missing` count. +> **Roads / sidewalks** are drawn too: the map stores each as a +> `RoadType::Start`/`End` pair of objects, and `render` emits a flat textured +> 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): diff --git a/VERSION b/VERSION index faef31a..8adc70f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.7.0 +0.8.0 \ No newline at end of file diff --git a/apps/openra3/main.cpp b/apps/openra3/main.cpp index d4598d3..2d23935 100644 --- a/apps/openra3/main.cpp +++ b/apps/openra3/main.cpp @@ -255,7 +255,7 @@ namespace { const auto stream = ra3::models::asset_stream::load_files(paths->manifest, paths->bin); std::vector placements; 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_h = terrain.world_height(); @@ -265,8 +265,8 @@ namespace { const auto cy = std::min(terrain.height - 1U, static_cast((world_h - y) / ra3::terrain::cell_size)); return static_cast(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(), - scene.textures.size(), paths->bin.filename().string().c_str()); + std::printf("objects: %zu placed, %zu missing, %zu road segments, %zu triangles, %zu textures (%s)\n", scene.placed, scene.missing, + scene.roads, scene.triangle_count(), scene.textures.size(), paths->bin.filename().string().c_str()); } catch (const std::exception &error) { std::printf("objects: failed to build scene (%s)\n", error.what()); } diff --git a/docs/REVERSE_ENGINEERING.md b/docs/REVERSE_ENGINEERING.md index 07dbbae..be24d96 100644 --- a/docs/REVERSE_ENGINEERING.md +++ b/docs/REVERSE_ENGINEERING.md @@ -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 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 -with `.`. Objects whose type is a `Road` template (the sidewalk/road -segments) are not meshes and are not drawn yet. +with `.`. + +### 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 diff --git a/shaders/generated/object.vert.spv b/shaders/generated/object.vert.spv index ff09037d419eb0497fb5de7aa6c8a04a555a7db4..4c7f326ce10d8975a0872c4095b5a36f92ea23c0 100644 GIT binary patch literal 2900 zcmZ9MYjaao6o!v&Q&2!e1Vph0idIA`Djh)%cs`rX1C7}aa?K#ywN^)a4{{N?iQGfJM>dfvQb!udG`}hIza%NKdwhQh z-?RAoh(7bRI@MlxP+%*rjRucUbXZCqkhiv-2hFm_82fwh^VUv`_4=)ckB0faBF0HZ zJ@XXUJ_e%>S*NoXuzNJCeH6QfXqVB>p}l~19_>Z+kgiJn&gOQtHt01Q)^m=J&|A%( zAvyMpn5*8Z4ZyS64e-Nub*vFG{}Pv`H?GtpO1zS!GH*qR5)D1V%8#M+0kX3=Hj2>ba1$g%!1csE*3 z5kH5v5AmghV`lg7gEybE%@Ji^a+6}N{qTO<#+Xa&-N_vSA3&E8>zz*cMQoS*52B4V zxA%E6ABQE`7-$@YiLVGZ9#;adh*^QJ=IDwmpSk?EitB`+r0m8*fDH84b712I0%DgSf3|G09VZ_mDcZ!T**fhnQg>-+W2q8H!D(Ft6hzzY+2aRU2) zKAzA2a`6)N)O!)Xi50r=jlP8Vu6?`4cf-M&{k6~NW_a8N1!xO<)Y?W*yA^X-WeU0xq53kQXz9VtuyMXN(2fl=T5%CRJ z?-Rrr`QR=mehK||id)8(_rGQ@@5{Z{5a;#`mXQ_YDquQb=!*v805%sRF)YkVd9 z(BB~TXMW@L`%TDx>=de{OtlCO>y K|Ho-xK>i0FJ+_>_P1aKO6n2*l(S8oQfoNCI_mjPVCZymO(F0ah=69j6YmI)lxn({3cn`hZ z>>2`|$yU)1TDA3NZ?oAOG=BlYj(BZ-oqj&ZJ8jt6BHo?~K5q{;^52@qh0p%*-Du_A zn|XiGXm2%lefFtu*lO+i?8P&3xIWLSGaS^0KaH^G#=Z;jmy0vh-|qBw8m$rMnGzFJ z5$72j*J}LQ9Gzh~tB>M>%BtyH=inVx*5aAj!!5GJna=BE1ZV$po=tGhk*kbxrNntz zBhIrmZW*!ODmvzM0^7)tin6_4B|cZ zt{USx97p7DvqI;bNSwUG{_X2ea_B#anAf;#bZJj=QS*6hxu|&x+nV0@sQDtceAM)t zA*VQ^=0DVQ?_Wa9Yn(ORH@T>J8hgCvJlL9NQjC4RjL1jLR}v@hn5_9KoOzA2W(6%5 zHDALXulYLInrBmtHP0dPQS*(&$vY-%z6obu0h`u`s f>zl>?9`XC6&+nEv*3iJNBl`SCiT`7_FC+g06Roay diff --git a/shaders/object.vert b/shaders/object.vert index f42941a..d75b67c 100644 --- a/shaders/object.vert +++ b/shaders/object.vert @@ -13,6 +13,7 @@ layout(location = 0) in vec3 in_pos; layout(location = 1) in vec3 in_normal; layout(location = 2) in vec2 in_uv; layout(location = 3) in float in_layer; +layout(location = 4) in float in_bias; layout(push_constant) uniform Push { vec4 cam_pos; // xyz = eye position @@ -36,7 +37,7 @@ void main() { float c = dot(r, pc.up.xyz); float th = pc.right.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); out_uv = in_uv; out_normal = in_normal; diff --git a/src/client/ra3.client.cppm b/src/client/ra3.client.cppm index 1570b64..6bd2349 100644 --- a/src/client/ra3.client.cppm +++ b/src/client/ra3.client.cppm @@ -370,6 +370,8 @@ export namespace ra3::client { bool running = true; bool presented = false; + const auto default_camera = camera; + bool middle_dragged = false; while (running) { ui_event event; float drag_x = 0.0F; @@ -388,13 +390,23 @@ export namespace ra3::client { } else if (event.type == ui_event_type::mouse_move) { mouse_x = event.x; 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_y += event.dy; + if (event.dx != 0.0F || event.dy != 0.0F) middle_dragged = true; } } 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_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; @@ -404,8 +416,10 @@ export namespace ra3::client { last = now; if (drag_x != 0.0F || drag_y != 0.0F) { - camera.yaw -= drag_x * 0.005F; - camera.pitch = std::clamp(camera.pitch + drag_y * 0.004F, 0.15F, 1.45F); + // SAGE LookAtTranslator: middle-drag rotates yaw (X) and + // 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; } diff --git a/src/map/ra3.map.cppm b/src/map/ra3.map.cppm index f59781c..5924cbb 100644 --- a/src/map/ra3.map.cppm +++ b/src/map/ra3.map.cppm @@ -218,6 +218,7 @@ export namespace ra3::map { real z = 0.0F; real angle = 0.0F; real scale = 1.0F; + uint32 road_type = 0U; ///< SAGE `RoadType` flags; 0 for a non-road object. }; namespace detail { @@ -303,6 +304,7 @@ export namespace ra3::map { object.y = read_f32(p + 4U); object.z = read_f32(p + 8U); object.angle = read_f32(p + 12U); + object.road_type = read_u32(p + 16U); p += 20U; // Coord3D + angle + road type const auto name_len = read_u16(p); p += 2U; diff --git a/src/models/ra3.models.cppm b/src/models/ra3.models.cppm index 55aba22..4b59a49 100644 --- a/src/models/ra3.models.cppm +++ b/src/models/ra3.models.cppm @@ -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 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(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(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(k) / static_cast(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(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; diff --git a/src/render/ra3.render.cppm b/src/render/ra3.render.cppm index 454a41a..dbdb507 100644 --- a/src/render/ra3.render.cppm +++ b/src/render/ra3.render.cppm @@ -277,6 +277,9 @@ export namespace ra3::render { float dy = 0.0F; float wheel = 0.0F; 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). */ @@ -675,7 +678,9 @@ export namespace ra3::render { float pitch = 1.02F; ///< Radians above the horizon (~58 degrees down). float height = 420.0F; ///< Camera height above the target's ground. float fov = 0.85F; ///< Vertical field of view, radians. - float min_height = 120.0F; - float max_height = 1600.0F; + // Zoom range mirrors the retail TacticalView (zoom 0.2..1.3 around the + // default height): closer/farther than that is clamped. + float min_height = 320.0F; + float max_height = 2100.0F; }; } diff --git a/src/terrain/ra3.terrain.cppm b/src/terrain/ra3.terrain.cppm index 7804f87..525d040 100644 --- a/src/terrain/ra3.terrain.cppm +++ b/src/terrain/ra3.terrain.cppm @@ -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 depth = 1.0F / inv_a; const auto pixel = static_cast(y) * static_cast(rw) + static_cast(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; + // Ground decals carry a per-vertex bias toward the camera + // (the retail shader's screen-space depth offset); it + // 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 tv = (w0 * v0.v * p0.inv_a + w1 * v1.v * p1.inv_a + w2 * v2.v * p2.inv_a) * depth; diff --git a/src/ui/ra3.ui.sdl.cppm b/src/ui/ra3.ui.sdl.cppm index 96b919b..7285582 100644 --- a/src/ui/ra3.ui.sdl.cppm +++ b/src/ui/ra3.ui.sdl.cppm @@ -101,6 +101,8 @@ export namespace ra3::ui { out.dx = event.motion.xrel; out.dy = event.motion.yrel; 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; case SDL_EVENT_MOUSE_BUTTON_DOWN: out = {}; @@ -108,6 +110,18 @@ export namespace ra3::ui { out.x = event.button.x; out.y = event.button.y; 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; case SDL_EVENT_MOUSE_WHEEL: out = {}; @@ -132,10 +146,10 @@ export namespace ra3::ui { const bool *keys = SDL_GetKeyboardState(nullptr); if (keys == nullptr) return false; switch (key) { - case ra3::render::ui_key::up: return keys[SDL_SCANCODE_W] || keys[SDL_SCANCODE_UP]; - case ra3::render::ui_key::down: return keys[SDL_SCANCODE_S] || keys[SDL_SCANCODE_DOWN]; - case ra3::render::ui_key::left: return keys[SDL_SCANCODE_A] || keys[SDL_SCANCODE_LEFT]; - case ra3::render::ui_key::right: return keys[SDL_SCANCODE_D] || keys[SDL_SCANCODE_RIGHT]; + case ra3::render::ui_key::up: return keys[SDL_SCANCODE_UP]; + case ra3::render::ui_key::down: return keys[SDL_SCANCODE_DOWN]; + case ra3::render::ui_key::left: return keys[SDL_SCANCODE_LEFT]; + case ra3::render::ui_key::right: return keys[SDL_SCANCODE_RIGHT]; default: return false; } } diff --git a/src/vulkan/ra3.vulkan.sdl.cppm b/src/vulkan/ra3.vulkan.sdl.cppm index b0c62af..dfa7e17 100644 --- a/src/vulkan/ra3.vulkan.sdl.cppm +++ b/src/vulkan/ra3.vulkan.sdl.cppm @@ -295,6 +295,8 @@ export namespace ra3::vulkan { out.dx = event.motion.xrel; out.dy = event.motion.yrel; 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; case SDL_EVENT_MOUSE_BUTTON_DOWN: out = {}; @@ -302,6 +304,18 @@ export namespace ra3::vulkan { out.x = event.button.x; out.y = event.button.y; 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; case SDL_EVENT_MOUSE_WHEEL: out = {}; @@ -326,10 +340,10 @@ export namespace ra3::vulkan { const bool *keys = SDL_GetKeyboardState(nullptr); if (keys == nullptr) return false; switch (key) { - case ra3::render::ui_key::up: return keys[SDL_SCANCODE_W] || keys[SDL_SCANCODE_UP]; - case ra3::render::ui_key::down: return keys[SDL_SCANCODE_S] || keys[SDL_SCANCODE_DOWN]; - case ra3::render::ui_key::left: return keys[SDL_SCANCODE_A] || keys[SDL_SCANCODE_LEFT]; - case ra3::render::ui_key::right: return keys[SDL_SCANCODE_D] || keys[SDL_SCANCODE_RIGHT]; + case ra3::render::ui_key::up: return keys[SDL_SCANCODE_UP]; + case ra3::render::ui_key::down: return keys[SDL_SCANCODE_DOWN]; + case ra3::render::ui_key::left: return keys[SDL_SCANCODE_LEFT]; + case ra3::render::ui_key::right: return keys[SDL_SCANCODE_RIGHT]; default: return false; } } @@ -844,17 +858,18 @@ export namespace ra3::vulkan { binding.binding = 0U; binding.stride = sizeof(ra3::models::vertex); binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; - VkVertexInputAttributeDescription attributes[4]{}; + VkVertexInputAttributeDescription attributes[5]{}; 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"); + attributes[4] = {4U, 0U, VK_FORMAT_R32_SFLOAT, 36U}; + static_assert(sizeof(ra3::models::vertex) == 40U, "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.vertexAttributeDescriptionCount = 5U; vertex_input.pVertexAttributeDescriptions = attributes; VkPipelineInputAssemblyStateCreateInfo assembly{}; @@ -873,8 +888,8 @@ export namespace ra3::vulkan { // 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; + raster.depthBiasConstantFactor = -4.0F; + raster.depthBiasSlopeFactor = -4.0F; VkPipelineMultisampleStateCreateInfo multisample{}; multisample.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;