ghost multiplayer: cloth on ghosts + player nameplates
Phase 4:
- Cloth: remote ghosts now wear this player's cloth garments (tee/pants).
The local player's rigid character.cloth_ropa items (CPU-sim deformed
root-local VB) are cloned per ghost and drawn with the REMOTE entity world
from the pose, so each ghost is draped in its own root frame. No mesh or
cloth data is transmitted; the shader already applies item.world to the
rigid cloth path (zero blend weights). Sim-inactive (skinned) garments and
garments not near the local body are skipped.
- Nameplates: display names ride the wire. JOIN carries a display name
(skate3_ghost_name cvar, default the OS user name); the relay stores it
per client and echoes {session_id, name} pairs in the HELLO roster
(RosterEntry). Clients re-JOIN every 5 s so the roster/names stay current
as members join and leave. A GhostNameplateOverlay (always registered,
draws only while enabled) projects each ghost's entity origin +1.75 m
through the published camera and draws a colored name label above it.
Uses skate3::native_scene::GetPublishedCamera (new thread-safe snapshot of
the published view-proj/cam) so the UI thread can project world points.
- GhostDialog gains a display-name field. Protocol version bumped to 3.
This commit is contained in:
parent
cceeac7c4f
commit
36b5169e27
12 changed files with 278 additions and 15 deletions
|
|
@ -1,10 +1,12 @@
|
|||
#include "skate3_native_ghost.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <bit>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
|
@ -25,6 +27,10 @@
|
|||
#include <rex/cvar.h>
|
||||
#include <rex/logging.h>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
// ---- cvars -----------------------------------------------------------------
|
||||
// The default public relay lives in ghost_proto.h (kGhostDefaultServer);
|
||||
// leave skate3_ghost_server empty to use it. Set it to reach a custom relay
|
||||
|
|
@ -50,6 +56,10 @@ REXCVAR_DEFINE_STRING(skate3_ghost_room, "", "Skate 3",
|
|||
"assigns one and it is shown in the multiplayer menu. "
|
||||
"Players sharing a code see each other's ghosts.")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
REXCVAR_DEFINE_STRING(skate3_ghost_name, "", "Skate 3",
|
||||
"Display name shown on this player's nameplate above "
|
||||
"the ghost. Empty = derived from the OS user name.")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
REXCVAR_DEFINE_DOUBLE(skate3_ghost_rate, 15.0, "Skate 3",
|
||||
"Ghost pose broadcast rate in Hz (a full skeleton pose "
|
||||
"is a few KB; 15 Hz is the LAN/internet sweet spot).")
|
||||
|
|
@ -207,6 +217,10 @@ uint8_t g_room_len = 0;
|
|||
size_t g_roster = 0;
|
||||
char g_peer[96] = {};
|
||||
|
||||
// Display names of the room members, keyed by their session id (from the
|
||||
// latest HELLO roster; refreshed by the periodic re-JOIN). g_mu-guarded.
|
||||
std::unordered_map<uint32_t, std::array<char, kGhostMaxNameLen>> g_roster_names;
|
||||
|
||||
// Network thread.
|
||||
std::thread g_thread;
|
||||
std::atomic<bool> g_stop{false};
|
||||
|
|
@ -250,6 +264,42 @@ bool Enabled() { return REXCVAR_GET(skate3_ghost_enable); }
|
|||
|
||||
bool Debug() { return REXCVAR_GET(skate3_ghost_debug); }
|
||||
|
||||
// This player's display name (cvar override or the OS user name).
|
||||
void CurrentDisplayName(char out[kGhostMaxNameLen], uint8_t* out_len) {
|
||||
std::string name = REXCVAR_GET(skate3_ghost_name);
|
||||
if (name.empty()) {
|
||||
const char* user = nullptr;
|
||||
#if defined(_WIN32)
|
||||
static char s_win_user[64] = {};
|
||||
if (s_win_user[0] == '\0') {
|
||||
DWORD n = sizeof(s_win_user);
|
||||
if (GetUserNameA(s_win_user, &n) == 0) {
|
||||
std::snprintf(s_win_user, sizeof(s_win_user), "Player");
|
||||
}
|
||||
}
|
||||
user = s_win_user;
|
||||
#else
|
||||
user = std::getenv("USER");
|
||||
#endif
|
||||
if (user != nullptr && user[0] != '\0') {
|
||||
name = user;
|
||||
} else {
|
||||
name = "Player";
|
||||
}
|
||||
}
|
||||
uint8_t len = 0;
|
||||
for (char c : name) {
|
||||
if (len >= kGhostMaxNameLen) {
|
||||
break;
|
||||
}
|
||||
if (c < 0x20 || c == 0x7F) {
|
||||
continue; // printable ASCII only (payload is a raw byte array)
|
||||
}
|
||||
out[len++] = c;
|
||||
}
|
||||
*out_len = len;
|
||||
}
|
||||
|
||||
void CopyRoomFromPayload(const char* room, uint8_t len) {
|
||||
std::lock_guard<std::mutex> lock(g_mu);
|
||||
g_room_len = std::min<uint8_t>(len, kGhostMaxRoomLen);
|
||||
|
|
@ -271,6 +321,9 @@ void SendJoin(GhostSock sock, const GhostAddr& to, bool ping) {
|
|||
std::memcpy(jp.room, g_room, g_room_len);
|
||||
jp.room_len = g_room_len;
|
||||
}
|
||||
// Display name: JOIN carries it so the relay can echo it in the roster;
|
||||
// the LAN discovery PING carries it too (a host can tag probes).
|
||||
CurrentDisplayName(jp.name, &jp.name_len);
|
||||
PacketBuf p;
|
||||
BuildPacket(p, ping ? kTypePing : kTypeJoin, &jp, sizeof(jp));
|
||||
GhostSockSendTo(sock, to, &p, PacketSize(p));
|
||||
|
|
@ -422,6 +475,17 @@ void HandlePacket(const GhostAddr& from, const PacketBuf& p, double now) {
|
|||
std::lock_guard<std::mutex> lock(g_mu);
|
||||
g_roster = hp.roster_count;
|
||||
g_connected = true;
|
||||
// Refresh the room members' display names (nameplate labels).
|
||||
g_roster_names.clear();
|
||||
for (uint8_t i = 0; i < hp.roster_count; ++i) {
|
||||
const RosterEntry& e = hp.roster[i];
|
||||
std::array<char, kGhostMaxNameLen> name = {};
|
||||
// Reserve one byte for the NUL so the label can always snprintf.
|
||||
const uint8_t n =
|
||||
std::min<uint8_t>(e.name_len, kGhostMaxNameLen - 1);
|
||||
std::memcpy(name.data(), e.name, n);
|
||||
g_roster_names[e.session_id] = name;
|
||||
}
|
||||
}
|
||||
SetStatusPeer(from);
|
||||
if (Debug()) {
|
||||
|
|
@ -494,6 +558,7 @@ void ResetSession() {
|
|||
g_ghosts.clear();
|
||||
g_ghost_index.clear();
|
||||
g_pending_poses.clear();
|
||||
g_roster_names.clear();
|
||||
g_session_public = 0;
|
||||
g_session_lan = 0;
|
||||
g_peer_public = Peer{};
|
||||
|
|
@ -598,9 +663,11 @@ void GhostNetworkLoop() {
|
|||
last_port = port;
|
||||
}
|
||||
|
||||
// Public relay: JOIN every kJoinIntervalSec until HELLO, then send POSE.
|
||||
// Public relay: JOIN every kJoinIntervalSec (a periodic refresh that also
|
||||
// keeps the roster/name labels current as members join and leave), and
|
||||
// reconnect when the HELLO stream goes quiet.
|
||||
if (g_peer_public.configured) {
|
||||
if (!g_peer_public.joined && now - g_peer_public.last_join >= kJoinIntervalSec) {
|
||||
if (now - g_peer_public.last_join >= kJoinIntervalSec) {
|
||||
SendJoin(sock, g_peer_public.addr, /*ping=*/false);
|
||||
g_peer_public.last_join = now;
|
||||
}
|
||||
|
|
@ -647,7 +714,7 @@ void GhostNetworkLoop() {
|
|||
|
||||
// The LAN peer (a discovered host, or our own loopback host): JOIN + POSE.
|
||||
if (g_peer_lan.configured) {
|
||||
if (!g_peer_lan.joined && now - g_peer_lan.last_join >= kJoinIntervalSec) {
|
||||
if (now - g_peer_lan.last_join >= kJoinIntervalSec) {
|
||||
SendJoin(sock, g_peer_lan.addr, /*ping=*/false);
|
||||
g_peer_lan.last_join = now;
|
||||
}
|
||||
|
|
@ -869,6 +936,28 @@ bool GetLocalWorld(float world[16]) {
|
|||
return true;
|
||||
}
|
||||
|
||||
bool GetGhostLabel(uint32_t index, float pos[3], float color[3], char* name,
|
||||
size_t name_max) {
|
||||
std::lock_guard<std::mutex> lock(g_mu);
|
||||
if (index >= g_ghosts.size()) {
|
||||
return false;
|
||||
}
|
||||
const Ghost& gh = g_ghosts[index];
|
||||
pos[0] = gh.world[12];
|
||||
pos[1] = gh.world[13];
|
||||
pos[2] = gh.world[14];
|
||||
GhostColor(gh.session_id, color);
|
||||
if (name != nullptr && name_max > 0) {
|
||||
const auto it = g_roster_names.find(gh.session_id);
|
||||
if (it != g_roster_names.end() && it->second[0] != '\0') {
|
||||
std::snprintf(name, name_max, "%s", it->second.data());
|
||||
} else {
|
||||
std::snprintf(name, name_max, "P%u", gh.session_id);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- cube geometry -----------------------------------------------------------
|
||||
// Unit cube centered at the origin, half-extent 0.5, 12 triangles as a
|
||||
// non-indexed triangle list (36 vertices). All attribute lanes except the
|
||||
|
|
|
|||
|
|
@ -70,6 +70,12 @@ bool GetGhostPose(uint32_t index, float world[16], float color[3],
|
|||
// Self-check cube at the local captured pose (skate3_ghost_draw_local).
|
||||
bool GetLocalWorld(float world[16]);
|
||||
|
||||
// Lightweight per-ghost query for the nameplate overlay: the ghost's world
|
||||
// position (entity world), marker color, and display name (from the room
|
||||
// roster, or a session fallback). Returns false if the ghost is gone.
|
||||
bool GetGhostLabel(uint32_t index, float pos[3], float color[3], char* name,
|
||||
size_t name_max);
|
||||
|
||||
// ---- cube geometry (56-byte records matching kSceneInputLayout) -----------------
|
||||
struct GhostCubeVertex {
|
||||
float pos[3];
|
||||
|
|
|
|||
|
|
@ -23,10 +23,11 @@
|
|||
namespace skate3::native_ghost {
|
||||
|
||||
constexpr uint32_t kGhostMagic = 0x54484753u; // "GHST" (LE)
|
||||
constexpr uint8_t kGhostVersion = 2;
|
||||
constexpr uint8_t kGhostVersion = 3;
|
||||
constexpr uint16_t kGhostDefaultPort = 40001;
|
||||
constexpr uint32_t kGhostMaxRoomLen = 8;
|
||||
constexpr uint32_t kGhostMaxRoster = 32;
|
||||
constexpr uint32_t kGhostMaxNameLen = 16; // display name, UTF-8-safe ASCII
|
||||
// Max skeleton rows carried in a POSE (3 float4s per bone). The Skate 3
|
||||
// skater skeleton fits comfortably under this; rows beyond the sender's
|
||||
// captured count are identity-padded so every mesh's referenced bones stay
|
||||
|
|
@ -55,18 +56,29 @@ struct alignas(1) PacketHeader {
|
|||
};
|
||||
|
||||
// JOIN and PING share the room payload (PING may carry the probing client's
|
||||
// room so LAN hosts can answer only matching rooms).
|
||||
// room so LAN hosts can answer only matching rooms); JOIN additionally
|
||||
// carries the client's display name, which the relay echoes to room members
|
||||
// in the HELLO roster so nameplates can label every ghost.
|
||||
struct alignas(1) JoinPayload {
|
||||
uint8_t room_len;
|
||||
char room[kGhostMaxRoomLen];
|
||||
uint8_t name_len;
|
||||
char name[kGhostMaxNameLen];
|
||||
};
|
||||
|
||||
// One room member as reported in a HELLO roster: session id + display name.
|
||||
struct alignas(1) RosterEntry {
|
||||
uint32_t session_id;
|
||||
uint8_t name_len;
|
||||
char name[kGhostMaxNameLen];
|
||||
};
|
||||
|
||||
struct alignas(1) HelloPayload {
|
||||
uint32_t session_id; // our assigned session id (0 = refused)
|
||||
uint8_t room_len;
|
||||
char room[kGhostMaxRoomLen];
|
||||
uint8_t roster_count; // other members' session ids in this room
|
||||
uint32_t roster[kGhostMaxRoster];
|
||||
uint8_t roster_count; // other members in this room
|
||||
RosterEntry roster[kGhostMaxRoster];
|
||||
};
|
||||
|
||||
// Full-pose blob (the character-model path): the sender's skater skeleton,
|
||||
|
|
|
|||
|
|
@ -126,6 +126,12 @@ void GhostRelay::HandlePacket(const GhostAddr& from, const PacketBuf& p,
|
|||
std::memcpy(me->room, jp.room, jp.room_len);
|
||||
me->room_len = jp.room_len;
|
||||
}
|
||||
// Keep the client's display name (JOIN carries it; PINGs from probes
|
||||
// may omit it, so only overwrite with a non-empty name).
|
||||
if (jp.name_len != 0 && jp.name_len <= kGhostMaxNameLen) {
|
||||
std::memcpy(me->name, jp.name, jp.name_len);
|
||||
me->name_len = jp.name_len;
|
||||
}
|
||||
// Empty room: assign one (uppercase alnum, 4 chars, seeded by the
|
||||
// session id and the wall of the receiving thread).
|
||||
if (me->room_len == 0) {
|
||||
|
|
@ -152,7 +158,10 @@ void GhostRelay::HandlePacket(const GhostAddr& from, const PacketBuf& p,
|
|||
if (&c != me && c.room_len == me->room_len &&
|
||||
RoomEquals(c.room, c.room_len, me->room, me->room_len) &&
|
||||
hp.roster_count < kGhostMaxRoster) {
|
||||
hp.roster[hp.roster_count++] = c.session_id;
|
||||
RosterEntry& entry = hp.roster[hp.roster_count++];
|
||||
entry.session_id = c.session_id;
|
||||
entry.name_len = c.name_len;
|
||||
std::memcpy(entry.name, c.name, c.name_len);
|
||||
}
|
||||
}
|
||||
BuildPacket(out, kTypeHello, &hp, static_cast<uint16_t>(sizeof(hp)));
|
||||
|
|
@ -168,7 +177,10 @@ void GhostRelay::HandlePacket(const GhostAddr& from, const PacketBuf& p,
|
|||
if (&c != me && c.room_len == me->room_len &&
|
||||
RoomEquals(c.room, c.room_len, me->room, me->room_len) &&
|
||||
hp.roster_count < kGhostMaxRoster) {
|
||||
hp.roster[hp.roster_count++] = c.session_id;
|
||||
RosterEntry& entry = hp.roster[hp.roster_count++];
|
||||
entry.session_id = c.session_id;
|
||||
entry.name_len = c.name_len;
|
||||
std::memcpy(entry.name, c.name, c.name_len);
|
||||
}
|
||||
}
|
||||
BuildPacket(out, kTypeHello, &hp, static_cast<uint16_t>(sizeof(hp)));
|
||||
|
|
|
|||
|
|
@ -59,6 +59,8 @@ class GhostRelay {
|
|||
uint32_t session_id = 0;
|
||||
char room[kGhostMaxRoomLen] = {};
|
||||
uint8_t room_len = 0;
|
||||
char name[kGhostMaxNameLen] = {};
|
||||
uint8_t name_len = 0;
|
||||
double last_seen = 0.0;
|
||||
};
|
||||
mutable std::mutex mu_;
|
||||
|
|
|
|||
|
|
@ -683,6 +683,8 @@ void Skate3BaseApp::OnCreateDialogs(rex::ui::ImGuiDrawer* drawer) {
|
|||
// skate3_native_render_mode_indicator shows it live). Input-transparent,
|
||||
// so it never affects cursor or focus handling.
|
||||
render_mode_indicator_ = std::make_unique<skate3::RenderModeIndicator>(drawer);
|
||||
ghost_nameplate_overlay_ =
|
||||
std::make_unique<skate3::GhostNameplateOverlay>(drawer);
|
||||
rex::ui::RegisterBind("bind_skate3_menu", "Escape", "Skate 3 settings", [this] {
|
||||
// Escape backs out of the settings screen level by level (rows ->
|
||||
// category rail -> closed); when the screen is closed it opens it.
|
||||
|
|
@ -835,6 +837,7 @@ void Skate3BaseApp::OnShutdown() {
|
|||
simple_settings_dialog_.reset();
|
||||
native_debug_dialog_.reset();
|
||||
ghost_dialog_.reset();
|
||||
ghost_nameplate_overlay_.reset();
|
||||
render_mode_indicator_.reset();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ class Skate3BaseApp : public rex::ReXApp {
|
|||
std::unique_ptr<rex::ui::SimpleSettingsDialog> simple_settings_dialog_;
|
||||
std::unique_ptr<skate3::NativeDebugDialog> native_debug_dialog_;
|
||||
std::unique_ptr<skate3::GhostDialog> ghost_dialog_;
|
||||
std::unique_ptr<skate3::GhostNameplateOverlay> ghost_nameplate_overlay_;
|
||||
std::unique_ptr<skate3::RenderModeIndicator> render_mode_indicator_;
|
||||
bool recipe_overlay_installed_ = false;
|
||||
bool big_device_aliases_installed_ = false;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "skate3_native_ghost_dialog.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
|
|
@ -9,11 +10,13 @@
|
|||
#include <rex/logging.h>
|
||||
|
||||
#include "native/skate3_native_ghost.h"
|
||||
#include "skate3_native_scene.h"
|
||||
|
||||
REXCVAR_DECLARE(bool, skate3_ghost_enable);
|
||||
REXCVAR_DECLARE(std::string, skate3_ghost_server);
|
||||
REXCVAR_DECLARE(int32_t, skate3_ghost_port);
|
||||
REXCVAR_DECLARE(std::string, skate3_ghost_room);
|
||||
REXCVAR_DECLARE(std::string, skate3_ghost_name);
|
||||
REXCVAR_DECLARE(double, skate3_ghost_rate);
|
||||
REXCVAR_DECLARE(bool, skate3_ghost_draw_local);
|
||||
|
||||
|
|
@ -45,6 +48,8 @@ void GhostDialog::SyncBuffersFromCvars() {
|
|||
std::snprintf(room_input_, sizeof(room_input_), "%s", room.c_str());
|
||||
const std::string server = REXCVAR_GET(skate3_ghost_server);
|
||||
std::snprintf(server_input_, sizeof(server_input_), "%s", server.c_str());
|
||||
const std::string name = REXCVAR_GET(skate3_ghost_name);
|
||||
std::snprintf(name_input_, sizeof(name_input_), "%s", name.c_str());
|
||||
synced_ = true;
|
||||
}
|
||||
|
||||
|
|
@ -94,6 +99,17 @@ void GhostDialog::OnDraw(ImGuiIO& io) {
|
|||
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::Text("Display name");
|
||||
if (ImGui::InputText("##ghost_name", name_input_, sizeof(name_input_),
|
||||
ImGuiInputTextFlags_EnterReturnsTrue)) {
|
||||
REXCVAR_SET(skate3_ghost_name, std::string(name_input_));
|
||||
ImGui::SetKeyboardFocusHere(-1);
|
||||
}
|
||||
ImGui::TextDisabled("Shown on your nameplate above the ghost. Empty = "
|
||||
"your OS user name.");
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::Text("Relay server (optional)");
|
||||
ImGui::PushItemWidth(-1.0f);
|
||||
if (ImGui::InputText("##ghost_server", server_input_, sizeof(server_input_),
|
||||
|
|
@ -144,4 +160,59 @@ void GhostDialog::OnDraw(ImGuiIO& io) {
|
|||
}
|
||||
}
|
||||
|
||||
void GhostNameplateOverlay::OnDraw(ImGuiIO& io) {
|
||||
if (!REXCVAR_GET(skate3_ghost_enable)) {
|
||||
return;
|
||||
}
|
||||
if (io.DisplaySize.x < 16.0f || io.DisplaySize.y < 16.0f) {
|
||||
return;
|
||||
}
|
||||
float vp[16];
|
||||
float cam[3];
|
||||
if (!skate3::native_scene::GetPublishedCamera(vp, cam)) {
|
||||
return;
|
||||
}
|
||||
const uint32_t count = skate3::native_ghost::GhostCount();
|
||||
if (count == 0) {
|
||||
return;
|
||||
}
|
||||
ImDrawList* dl = ImGui::GetBackgroundDrawList();
|
||||
const float w = io.DisplaySize.x;
|
||||
const float h = io.DisplaySize.y;
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
float pos[3];
|
||||
float color[3];
|
||||
char name[32];
|
||||
if (!skate3::native_ghost::GetGhostLabel(i, pos, color, name,
|
||||
sizeof(name))) {
|
||||
continue;
|
||||
}
|
||||
// Project the point ~1.75 m above the entity origin (roughly the head).
|
||||
const float p[3] = {pos[0], pos[1] + 1.75f, pos[2]};
|
||||
float c[4];
|
||||
for (int r = 0; r < 4; ++r) {
|
||||
c[r] = p[0] * vp[r] + p[1] * vp[4 + r] + p[2] * vp[8 + r] + vp[12 + r];
|
||||
}
|
||||
if (c[3] < 0.01f) {
|
||||
continue; // behind the camera
|
||||
}
|
||||
const float ndc_x = c[0] / c[3];
|
||||
const float ndc_y = c[1] / c[3];
|
||||
if (ndc_x < -1.6f || ndc_x > 1.6f || ndc_y < -1.6f || ndc_y > 1.6f) {
|
||||
continue;
|
||||
}
|
||||
const float sx = (ndc_x * 0.5f + 0.5f) * w;
|
||||
const float sy = (1.0f - (ndc_y * 0.5f + 0.5f)) * h;
|
||||
const ImU32 text_col =
|
||||
IM_COL32(int(color[0] * 255.0f), int(color[1] * 255.0f),
|
||||
int(color[2] * 255.0f), 235);
|
||||
const ImVec2 text_size = ImGui::CalcTextSize(name);
|
||||
// Small dark backdrop so the label stays readable over the world.
|
||||
dl->AddRectFilled(ImVec2(sx - 4.0f, sy - 14.0f),
|
||||
ImVec2(sx + text_size.x + 4.0f, sy + 2.0f),
|
||||
IM_COL32(0, 0, 0, 140));
|
||||
dl->AddText(ImVec2(sx, sy - 14.0f), text_col, name);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace skate3
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
#pragma once
|
||||
|
||||
// Ghost multiplayer menu: enable toggle, room-code entry, relay override and
|
||||
// a live connection status readout. A player-facing dialog (bound to M by
|
||||
// default in skate3_app_common.cpp); everything it edits is a hot-reload
|
||||
// cvar in skate3_native_ghost.cpp.
|
||||
// Ghost multiplayer UI: the multiplayer menu (GhostDialog, bound to M) and
|
||||
// the always-present nameplate overlay (GhostNameplateOverlay) that labels
|
||||
// each remote ghost with its owner's display name.
|
||||
|
||||
#include <rex/ui/imgui_dialog.h>
|
||||
|
||||
#include "native/skate3_native_ghost_proto.h"
|
||||
|
||||
namespace skate3 {
|
||||
|
||||
class GhostDialog final : public rex::ui::ImGuiDialog {
|
||||
|
|
@ -28,7 +29,20 @@ class GhostDialog final : public rex::ui::ImGuiDialog {
|
|||
bool synced_ = false;
|
||||
char room_input_[16] = {};
|
||||
char server_input_[128] = {};
|
||||
char name_input_[skate3::native_ghost::kGhostMaxNameLen + 1] = {};
|
||||
int rate_int_ = 20;
|
||||
};
|
||||
|
||||
// Draws a small colored nameplate above every remote ghost whenever ghost
|
||||
// multiplayer is enabled. Registers like the render-mode indicator (always
|
||||
// present, draws only while active).
|
||||
class GhostNameplateOverlay final : public rex::ui::ImGuiDialog {
|
||||
public:
|
||||
explicit GhostNameplateOverlay(rex::ui::ImGuiDrawer* drawer)
|
||||
: ImGuiDialog(drawer) {}
|
||||
|
||||
protected:
|
||||
void OnDraw(ImGuiIO& io) override;
|
||||
};
|
||||
|
||||
} // namespace skate3
|
||||
|
|
@ -2756,6 +2756,16 @@ bool ToggleSceneEnabled() {
|
|||
return enabled;
|
||||
}
|
||||
|
||||
bool GetPublishedCamera(float view_proj[16], float cam_pos[3]) {
|
||||
std::lock_guard<std::mutex> lock(g_scene_mutex);
|
||||
if (!g_scene) {
|
||||
return false;
|
||||
}
|
||||
std::memcpy(view_proj, g_scene->view_proj, sizeof(float) * 16);
|
||||
std::memcpy(cam_pos, g_scene->cam_pos, sizeof(float) * 3);
|
||||
return true;
|
||||
}
|
||||
|
||||
void OnRegisterTexture(uint64_t guid, uint32_t texture) {
|
||||
if (texture == 0) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -689,6 +689,12 @@ void Install();
|
|||
// layer was not enabled at boot.
|
||||
bool ToggleSceneEnabled();
|
||||
|
||||
// Snapshot of the latest published frame's camera (smoothed view-proj +
|
||||
// world position). Thread-safe (any thread); returns false before the first
|
||||
// publish. Used by the ghost nameplate overlay to project world positions
|
||||
// into screen space on the UI thread.
|
||||
bool GetPublishedCamera(float view_proj[16], float cam_pos[3]);
|
||||
|
||||
// True once the scene renderer has hit an unrecoverable pipeline/resource
|
||||
// failure and yields to the emulated output until re-enabled. Surfaced so
|
||||
// the render-mode indicator can show the degraded state even though the
|
||||
|
|
|
|||
|
|
@ -10360,6 +10360,11 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
|||
// skinned non-ropa, non-LivingWorld items), found the same way the
|
||||
// ghost capture on the game thread finds the body.
|
||||
std::vector<const DrawItem*> player_pieces;
|
||||
// The local player's cloth garments (rigid character.cloth_ropa items:
|
||||
// CPU-sim deformed ROOT-LOCAL VB, drawn with the entity world affine).
|
||||
// Cloned for ghosts and rendered with the REMOTE entity world so the
|
||||
// ghost keeps its tee/pants, draped to its root frame.
|
||||
std::vector<const DrawItem*> ghost_cloth;
|
||||
{
|
||||
const DrawItem* body = nullptr;
|
||||
uint32_t best_bones = 0;
|
||||
|
|
@ -10404,6 +10409,19 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
|||
player_pieces.push_back(&it);
|
||||
}
|
||||
}
|
||||
// Cloth garments near the body (world translation = the entity
|
||||
// position; only the rigid state has a root-local deformed VB).
|
||||
for (const DrawItem& it : scene.items) {
|
||||
if (!it.ropa || !it.bones.empty()) {
|
||||
continue;
|
||||
}
|
||||
const float px = it.world[12], py = it.world[13], pz = it.world[14];
|
||||
const float dx = px - body_p[0], dy = py - body_p[1],
|
||||
dz = pz - body_p[2];
|
||||
if (dx * dx + dy * dy + dz * dz <= 6.25f) {
|
||||
ghost_cloth.push_back(&it);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -10455,7 +10473,8 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
|||
cmd->Draw(kCubeVerts, 0);
|
||||
};
|
||||
|
||||
const auto draw_model = [&](const float* pose_rows, uint16_t pose_count) {
|
||||
const auto draw_model = [&](const float* pose_rows, uint16_t pose_count,
|
||||
const float* ghost_world) {
|
||||
if (player_pieces.empty()) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -10489,6 +10508,24 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
|||
g.lw_alpha = -1.0f;
|
||||
timed_draw(g);
|
||||
}
|
||||
// Cloth: clone the local player's garments (root-local deformed VB)
|
||||
// and draw them with the REMOTE entity world, so each ghost wears
|
||||
// this player's tee/pants draped in its own root frame.
|
||||
for (const DrawItem* cloth : ghost_cloth) {
|
||||
DrawItem g = *cloth;
|
||||
std::memcpy(g.world, ghost_world, sizeof(g.world));
|
||||
g.bones.clear();
|
||||
g.skinned = false;
|
||||
g.pending = false;
|
||||
g.selected = false;
|
||||
g.retained = false;
|
||||
g.dyn_entity = false;
|
||||
g.unlit = false;
|
||||
g.transparent = false;
|
||||
g.water = false;
|
||||
g.lw_alpha = -1.0f;
|
||||
timed_draw(g);
|
||||
}
|
||||
};
|
||||
|
||||
float ghost_world[16];
|
||||
|
|
@ -10501,7 +10538,7 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
|||
uint16_t(skate3::native_ghost::kGhostMaxBones), pose_scratch,
|
||||
&ghost_rows)) {
|
||||
if (ghost_rows != 0) {
|
||||
draw_model(pose_scratch, ghost_rows);
|
||||
draw_model(pose_scratch, ghost_rows, ghost_world);
|
||||
continue;
|
||||
}
|
||||
draw_cube(ghost_world, ghost_color);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue