From 64ad00fa1d5463b4ddccbee48ca43b5fc57db17b Mon Sep 17 00:00:00 2001 From: Toastconcern Date: Wed, 21 Oct 2026 23:22:34 -0500 Subject: [PATCH] Remove wall spots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wall spots were markers for where SteamVR panels/windows could dock on a home's walls (Meta's WallPlacementComponent). Removed the feature end to end: the WorldWallSpot type and its JSON/binary serialization, the loader that read WallPlacementComponent, auto-generation of spots and the hotspots placed in front of them, the Studio editor's Wall Spot kind (Add menu, properties, overlay draw, picking, selection, report), the debug overlay, the home report counts, and the walktest viz. The PanelDock subsystem (src/panel_dock.h) existed only to dock panels onto wall spots and is deleted with its use in Frame Home's frame loop. Baked scene format is bumped to v14: new bakes omit wall spots, and older baked scenes still load (the wall-spot block is read past). Collision walls (WorldCollider::Wall) are unaffected — only the panel-placement markers are gone. Docs updated. Note: the pre-existing FRAME_HOME_PC Windows build break in walkgrid.h (the `near` macro) is unrelated and left as-is. Co-Authored-By: Claude Opus 4.8 --- README.md | 2 +- docs/environments-and-fenv.md | 2 +- docs/studio-guide.md | 1 - docs/using-frame-home.md | 2 +- src/debug_geom.h | 8 +- src/home_report.h | 10 +- src/hsl_scene.cpp | 2 +- src/hsl_scene.h | 2 +- src/main.cpp | 6 - src/overlays.h | 2 +- src/panel_dock.h | 177 ------------------------- src/perf_settings.h | 2 +- src/scene_bake.cpp | 6 +- src/scene_bake.h | 2 +- src/switcher.cpp | 2 +- src/world_gen.h | 89 +------------ studio/studio.cpp | 108 ++++----------- third_party/qhe/core/world.h | 30 +---- third_party/qhe/loaders/scene_loader.h | 11 +- tools/walktest/walkviz.cpp | 10 +- tools/walktest/worldrender.cpp | 12 +- 21 files changed, 63 insertions(+), 423 deletions(-) delete mode 100644 src/panel_dock.h diff --git a/README.md b/README.md index e6f6fe7..b464711 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ very largest homes may not fit. - Opens Meta homes (HSL, Clay, glTF), `.fenv` environments and Blender or Unity models; pairs rooms with vistas. - Move, rotate, scale, hide and delete objects, with undo and auto-saved versions. -- Shape the walkable area: auto-generated surfaces, drawn floors, spawns, seats, hotspots, wall spots, gravity zones. +- Shape the walkable area: auto-generated surfaces, drawn floors, spawns, seats, hotspots, gravity zones. - Add a background sound and a picture; set and calculate per-world settings. - Saves baked `.fenv` files and leaves out what can't be seen; sends them to the headset in one click. - Packs several environments into one `.fbundle` to share or install together ([bundles](docs/bundles.md)). diff --git a/docs/environments-and-fenv.md b/docs/environments-and-fenv.md index d9404cd..908ec3d 100644 --- a/docs/environments-and-fenv.md +++ b/docs/environments-and-fenv.md @@ -24,7 +24,7 @@ There are two kinds: | `fenv.json` | every `.fenv` | The manifest: name, where the home came from (package and version of each `.apk`), and which other members are there. It also holds the environment's own settings. | | `scene.bin.lz` | baked | The baked scene, packed with LZMA (much smaller than ZIP's own packing for textures). It is unpacked once, into a cache, then loaded from there. | | `apks/...` | wrapped | The original Meta home files (a room, and maybe its vista) | -| `world.json` | wrapped | The world layer: spawn points, seats, teleport hotspots, walkable surfaces, wall spots, gravity zones. A baked scene carries this inside `scene.bin`. | +| `world.json` | wrapped | The world layer: spawn points, seats, teleport hotspots, walkable surfaces, gravity zones. A baked scene carries this inside `scene.bin`. | | `edits.json` | when you edited | What you changed in Studio: moved and deleted objects, the added sound and how loud the home's own sound plays | | a sound file | when you added one | The sound you added in Studio (`.ogg`, `.opus`, `.wav`, `.mp3`, `.flac`), named in `edits.json` | | `thumb.png` | usually | The picture the Environments panel shows | diff --git a/docs/studio-guide.md b/docs/studio-guide.md index 77b503e..d5e0e56 100644 --- a/docs/studio-guide.md +++ b/docs/studio-guide.md @@ -114,7 +114,6 @@ edges, grey are floors you can't reach. **W** shows or hides it. | **Seat** | a place to sit: point at it and pull the trigger; its "stand up here" ring is where you get up | | **Teleport Hotspot** | a spot the teleport arc snaps to | | **Surface** | a box you can scale into a floor, a step or a wall | -| **Wall Spot** | where SteamVR's panels dock | | **Invisible Wall** | stops you; never drawn on the headset. Scale and turn it to fit; where it stands is no longer walkable | | **Gravity Zone** | inside it, "down" is the zone's own: a direction, or any surface you stand on | diff --git a/docs/using-frame-home.md b/docs/using-frame-home.md index eb3aa5f..8ece2dd 100644 --- a/docs/using-frame-home.md +++ b/docs/using-frame-home.md @@ -157,7 +157,7 @@ Frame Home restarts. | Item | What it does | |---|---| | View | Normal, Wireframe (every triangle edge), Meshes (each in its own colour), GPU cost (green is cheap, red is costly), Overdraw (bright where surfaces pile up) | -| World layer | Shows where you can walk (green), surfaces and colliders, spawns, seats, wall spots and gravity zones | +| World layer | Shows where you can walk (green), surfaces and colliders, spawns, seats and gravity zones | | See-through surfaces | Leave out glass, water, glow and fog, to see whether they're what's slow | | Cut-out surfaces | Leave out leaves, grass and fences | | Sky | Leave out the sky | diff --git a/src/debug_geom.h b/src/debug_geom.h index 6f46768..b86ae81 100644 --- a/src/debug_geom.h +++ b/src/debug_geom.h @@ -1,6 +1,6 @@ #pragma once // The world layer drawn over the home in the headset (the Debug page's "World layer"): the walkable area as Frame -// Home walks it, the surfaces and colliders, spawns, seats, hotspots, wall spots and gravity zones, in the colours +// Home walks it, the surfaces and colliders, spawns, seats, hotspots and gravity zones, in the colours // Frame Studio uses for them. Built once per home as guide geometry (guide_geom.h) and appended to the guide each // frame. Lines are thin crossed ribbons, so they read from any side without being rebuilt as you move. #include "guide_geom.h" @@ -58,7 +58,7 @@ inline void marker(std::vector& o, const WorldPoint& p, float r, fl constexpr C kWalk{0.15f, 0.85f, 0.35f, 0.32f}, kWalkEdge{0.25f, 1.0f, 0.45f, 0.9f}, kBridged{0.95f, 0.75f, 0.2f, 0.25f}; constexpr C kSolid{0.95f, 0.95f, 0.95f, 0.55f}, kObject{1.0f, 0.55f, 0.25f, 0.7f}, kTrigger{0.6f, 0.6f, 1.0f, 0.45f}, kWalkable{0.3f, 1.0f, 0.5f, 0.7f}; constexpr C kSpawn{0.3f, 0.75f, 1.0f, 0.95f}, kSeat{1.0f, 0.8f, 0.25f, 0.95f}, kHotspot{0.85f, 0.45f, 1.0f, 0.95f}; -constexpr C kSpot{0.4f, 0.9f, 1.0f, 0.8f}, kGravity{1.0f, 0.35f, 0.6f, 0.6f}; +constexpr C kGravity{1.0f, 0.35f, 0.6f, 0.6f}; // Everything, in world space. Mesh surfaces are drawn as their boundary-free wire (every edge) only up to a budget: // a mesh collider can be a whole room's geometry. @@ -165,10 +165,6 @@ inline void build(const HomeWorld& w, const WalkGrid* g, std::vector= 0 ? "; walkable " + num(walkArea, 0) + " m²\n" : "\n"; if (!biggestMeshes.empty()) o += "most triangles:\n"; @@ -72,7 +72,7 @@ struct HomeReport { n("meshes", meshes); n("triangles", triangles); n("vertices", vertices); n("seeThrough", seeThrough); n("seeThroughTris", seeThroughTris); n("cutout", cutout); n("skinned", skinned); n("animated", animated); n("lightmapped", lightmapped); n("programs", programs); n("textures", textures); n("textureMB", std::round(textureMB)); n("decodedMB", std::round(decodedMB)); n("geometryMB", std::round(geometryMB)); - n("spawns", spawns); n("seats", seats); n("colliders", colliders); n("wallSpots", wallSpots); n("walkArea", std::round(walkArea)); + n("spawns", spawns); n("seats", seats); n("colliders", colliders); n("walkArea", std::round(walkArea)); n("loadSec", std::round(loadSec * 10) / 10); o += ",\"biggestMeshes\":["; for (size_t i = 0; i < biggestMeshes.size(); ++i) o += (i ? ",{\"name\":" : "{\"name\":") + q(meshName(biggestMeshes[i].mesh)) + ",\"tris\":" + num(biggestMeshes[i].tris, 0) + "}"; @@ -209,7 +209,7 @@ inline HomeReport makeHomeReport(const HslScene& s, const HomeWorld* walk = null // The world. const HomeWorld& w = walk ? *walk : s.world; r.spawns = w.spawns.size(); r.seats = w.seats.size(); r.hotspots = w.hotspots.size(); r.colliders = w.colliders.size(); - r.wallSpots = w.wallSpots.size(); r.gravity = w.gravity.size(); + r.gravity = w.gravity.size(); const WalkGrid* g = walk ? walk->walkGridCache.get() : nullptr; float floorY = 0; if (w.spawns.empty() && w.gravity.empty() && (!g || !g->floorAt(0, 0, 0, 0.5f, 1.0f, floorY))) diff --git a/src/hsl_scene.cpp b/src/hsl_scene.cpp index 3b5b77e..8dc324d 100644 --- a/src/hsl_scene.cpp +++ b/src/hsl_scene.cpp @@ -485,7 +485,7 @@ bool loadHslScene(const std::vector& apkPaths, HslScene& scene, std HomeWorld& w = loaders[i]->world; auto add = [](auto& to, auto& from) { to.insert(to.end(), std::make_move_iterator(from.begin()), std::make_move_iterator(from.end())); }; add(scene.world.spawns, w.spawns); add(scene.world.seats, w.seats); add(scene.world.hotspots, w.hotspots); - add(scene.world.colliders, w.colliders); add(scene.world.wallSpots, w.wallSpots); + add(scene.world.colliders, w.colliders); if (!scene.world.empty()) scene.world.origin = "meta"; } for (auto& md : loaders[i]->meshes) { diff --git a/src/hsl_scene.h b/src/hsl_scene.h index 275af52..4816de0 100644 --- a/src/hsl_scene.h +++ b/src/hsl_scene.h @@ -77,7 +77,7 @@ inline bool loadHslScene(const std::string& apkPath, HslScene& scene, std::strin // The world to walk in: the home's own (Meta's converted, or a .fenv's) when it has floors or walls; else floors and // walls taken from the drawn geometry (opaque, not the sky, not distant scenery) — Clay homes and homes like Theater -// ship no collision at all. Spawns, seats and wall spots are kept either way. +// ship no collision at all. Spawns and seats are kept either way. // The world carries its walkable area (walkGridCache): the one Frame Studio baked (world.json), else one baked here from // the drawn geometry plus the colliders (bakeWalkGrid). Gravity homes also get the geometry's floors and walls as // colliders while nothing blocks (the gravity walker reads colliders). diff --git a/src/main.cpp b/src/main.cpp index 27a445f..62ea99a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,7 +16,6 @@ #include "hsl_scene.h" #include "scene_bake.h" #include "locomotion.h" -#include "panel_dock.h" #include "panorama.h" #include "core/audio.h" #include "env_export.h" @@ -1087,8 +1086,6 @@ int runVr(const std::vector& apks, bool system = false) { loco.setOptions(perf.walk, perf.teleport, perf.turn, perf.snapAngle, perf.moveSpeed); loco.setRingFallback(!r.guideAvailable()); // Frame Home draws the arc itself; the overlay ring only if it can't std::vector guideVerts; - PanelDock dock; // SteamVR panels docking on the home's wall spots - // Homes are parsed (and their textures decoded) on a worker thread and uploaded between frames, // so the headset keeps getting frames while a new home loads. const bool follow = apks.empty(); // environment mode: follow ~/frame-home/current @@ -1415,7 +1412,6 @@ int runVr(const std::vector& apks, bool system = false) { LOG("report: %zu things to look at (logs/report.txt), first: %s", pendingReport.problems.size(), pendingReport.problems.front().what.c_str()); pendingReport = HomeReport(); } - dock.setWorld(scene->world); LOG("showing %s (%zu spawns, %zu seats, %zu colliders)", shown.front().c_str(), scene->world.spawns.size(), scene->world.seats.size(), scene->world.colliders.size()); } else { @@ -1691,7 +1687,6 @@ int runVr(const std::vector& apks, bool system = false) { lastFrame = frameNow; Mat4 rig; loco.rig(rig.m); - dock.update(rig.m); const Mat4 headWorld = mul(rig, head); if (system && widenFixed <= 0) { // head turn rate (rad/s) from the rotation since the last frame: angle of R_prev^T * R @@ -1890,7 +1885,6 @@ int runVr(const std::vector& apks, bool system = false) { if (worker.joinable()) worker.join(); if (soundJob.joinable()) soundJob.join(); publisher.finish(); - dock.stop(); // SteamVR first: its client holds our Vulkan device for the frames it composited, and cleaning up after a // device we'd already destroyed crashed every exit (game launches, restarts). vr::VR_Shutdown(); diff --git a/src/overlays.h b/src/overlays.h index 7694ff4..01b17d1 100644 --- a/src/overlays.h +++ b/src/overlays.h @@ -1,7 +1,7 @@ #pragma once // SteamVR's panels (overlays) of every app. OpenVR can't list them, but a handle is (slot << 32 | serial) with both // small, so probing that range finds them all; a whole sweep takes about 0.1 s. Used to tell whether a hand points -// at a panel (frame-switcher) and to dock free-floating panels on a home's wall spots (Frame Home). +// at a panel (frame-switcher). #include #include diff --git a/src/panel_dock.h b/src/panel_dock.h deleted file mode 100644 index 5eca702..0000000 --- a/src/panel_dock.h +++ /dev/null @@ -1,177 +0,0 @@ -#pragma once -// Wall spots at work: a SteamVR panel (a desktop or game window, any free-floating overlay) let go of near one of -// the home's wall spots docks onto it — flush with the wall, facing the room, no wider than the spot — and stays -// there as you walk and teleport (panels live in tracking space, so a docked one is re-placed from the home's frame -// whenever you move). Grab it away and it's free again, back at its own size. Docked panels are re-placed only when -// the rig moves: setting one every frame would overwrite a grab before it could be seen. -// Meta's homes mark these spots with WallPlacementComponent; Frame Studio edits them (World > Wall Spot). -#include "core/world.h" -#include "overlays.h" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class PanelDock { -public: - ~PanelDock() { stop(); } - // Before VR_Shutdown: the scanner calls into SteamVR's client library, which shutting down unloads. - void stop() { stop_ = true; if (scanner_.joinable()) scanner_.join(); } - - void setWorld(const HomeWorld& w) { - undockAll(); - spots_ = w.wallSpots; - if (!spots_.empty() && !scanner_.joinable()) scanner_ = std::thread([this] { scanLoop(); }); - } - - // Once per frame with worldFromTracking. - void update(const float rig[16]) { - if (spots_.empty()) return; - std::vector panels; - { std::lock_guard l(mutex_); panels = panels_; } - float inv[16]; - invertRigid(rig, inv); - const auto now = std::chrono::steady_clock::now(); - vr::IVROverlay* ov = vr::VROverlay(); - // Panels whose app went away: their state (and any spot they held) goes too. - for (auto it = state_.begin(); it != state_.end();) { - bool present = false; - for (const OverlayInfo& p : panels) present |= p.handle == it->first; - it = present ? std::next(it) : state_.erase(it); - } - for (const OverlayInfo& p : panels) { - if (!ov->IsOverlayVisible(p.handle)) continue; - vr::VROverlayTransformType tt; - if (ov->GetOverlayTransformType(p.handle, &tt) != vr::VROverlayError_None || tt != vr::VROverlayTransform_Absolute) continue; - vr::ETrackingUniverseOrigin origin; - vr::HmdMatrix34_t cur; - if (ov->GetOverlayTransformAbsolute(p.handle, &origin, &cur) != vr::VROverlayError_None) continue; - if (origin != vr::TrackingUniverseStanding) continue; // the spots are placed in standing space - State& s = state_[p.handle]; - if (s.spot >= 0) { - // Docked: still where we put it? Then keep it on the wall as the rig moves; else it was grabbed away. - if (differs(cur, s.placed, 0.03f)) { undock(p.handle, s, "moved away"); s.still = now; s.last = cur; continue; } - // Re-placed only once the rig has moved a few millimetres since it was placed (per panel, so one - // hidden while you teleported is put right when it shows again). Tiny continuous rig changes — - // seated eye-height easing, step easing — would otherwise overwrite a grab every frame. - float moved = 0; - for (int k = 0; k < 16; ++k) moved = std::max(moved, std::fabs(rig[k] - s.rig[k])); - if (moved < 0.005f) continue; - vr::HmdMatrix34_t m = spotInTracking(s.spot, inv, s.flip); - ov->SetOverlayTransformAbsolute(p.handle, origin, &m); - s.placed = m; - std::memcpy(s.rig, rig, sizeof s.rig); - continue; - } - if (differs(cur, s.last, 0.002f)) { s.last = cur; s.still = now; continue; } // still moving - if (now - s.still < std::chrono::milliseconds(500) || s.cooldown > now) continue; - // At rest: dock onto the nearest free spot within 0.6 m. - float pt[3]; - for (int k = 0; k < 3; ++k) pt[k] = rig[k] * cur.m[0][3] + rig[4 + k] * cur.m[1][3] + rig[8 + k] * cur.m[2][3] + rig[12 + k]; - int best = -1; - float bestD = 0.6f; - for (size_t i = 0; i < spots_.size(); ++i) { - if (taken(static_cast(i))) continue; - const float* c = &spots_[i].world[12]; - const float d = std::sqrt((pt[0] - c[0]) * (pt[0] - c[0]) + (pt[1] - c[1]) * (pt[1] - c[1]) + (pt[2] - c[2]) * (pt[2] - c[2])); - if (d < bestD) { bestD = d; best = static_cast(i); } - } - if (best < 0) { s.cooldown = now + std::chrono::milliseconds(500); continue; } - // Face the panel the way it was facing (towards the user), whichever way the spot's frame points. - const float* z = &spots_[best].world[8]; - float pz[3]; - for (int k = 0; k < 3; ++k) pz[k] = rig[k] * cur.m[0][2] + rig[4 + k] * cur.m[1][2] + rig[8 + k] * cur.m[2][2]; - s.flip = z[0] * pz[0] + z[1] * pz[1] + z[2] * pz[2] < 0; - ov->GetOverlayWidthInMeters(p.handle, &s.width); - const float fit = std::min(s.width, spots_[best].width); - ov->SetOverlayWidthInMeters(p.handle, fit); - vr::HmdMatrix34_t m = spotInTracking(best, inv, s.flip); - ov->SetOverlayTransformAbsolute(p.handle, origin, &m); - s.placed = m; - std::memcpy(s.rig, rig, sizeof s.rig); - s.spot = best; - std::fprintf(stderr, "frame-home: docked panel %s on wall spot %s\n", p.key.c_str(), spots_[best].name.c_str()); - } - } - -private: - struct State { - int spot = -1; - bool flip = false; - float width = 1; // its own width, given back when it leaves the wall - vr::HmdMatrix34_t placed = {}, last = {}; - float rig[16] = {}; // the rig it was last placed for - std::chrono::steady_clock::time_point still{}, cooldown{}; - }; - std::vector spots_; - std::map state_; - std::vector panels_; - std::mutex mutex_; - std::thread scanner_; - std::atomic stop_{false}; - - // The panel list, refreshed every 2 s off the frame loop (a sweep takes ~0.1 s). - void scanLoop() { - while (!stop_) { - std::vector found; - for (const OverlayInfo& o : listOverlays()) - if (isUserPanel(o) && o.key.rfind("system.", 0) != 0) found.push_back(o); - { std::lock_guard l(mutex_); panels_ = std::move(found); } - for (int i = 0; i < 20 && !stop_; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } - } - - bool taken(int spot) const { - for (const auto& kv : state_) if (kv.second.spot == spot) return true; - return false; - } - - void undock(vr::VROverlayHandle_t h, State& s, const char* why) { - vr::VROverlay()->SetOverlayWidthInMeters(h, s.width); - std::fprintf(stderr, "frame-home: panel left wall spot %s (%s)\n", spots_[s.spot].name.c_str(), why); - s.spot = -1; - s.cooldown = std::chrono::steady_clock::now() + std::chrono::seconds(2); - } - - void undockAll() { - for (auto& kv : state_) if (kv.second.spot >= 0) undock(kv.first, kv.second, "new home"); - state_.clear(); - } - - // The spot's frame in tracking space, 1 cm off the wall, optionally turned to face the other way. - vr::HmdMatrix34_t spotInTracking(int i, const float inv[16], bool flip) const { - float w[16]; - std::memcpy(w, spots_[i].world, 64); - for (int c = 0; c < 3; ++c) { // unit axes (a scaled spot frame would scale the panel) - const float L = std::sqrt(w[c * 4] * w[c * 4] + w[c * 4 + 1] * w[c * 4 + 1] + w[c * 4 + 2] * w[c * 4 + 2]); - if (L > 1e-6f) for (int r = 0; r < 3; ++r) w[c * 4 + r] /= L; - } - if (flip) for (int r = 0; r < 3; ++r) { w[r] = -w[r]; w[8 + r] = -w[8 + r]; } // 180 degrees about Y - for (int r = 0; r < 3; ++r) w[12 + r] += w[8 + r] * 0.01f; - float t[16]; - for (int c = 0; c < 4; ++c) - for (int r = 0; r < 4; ++r) t[c * 4 + r] = inv[r] * w[c * 4] + inv[4 + r] * w[c * 4 + 1] + inv[8 + r] * w[c * 4 + 2] + inv[12 + r] * w[c * 4 + 3]; - vr::HmdMatrix34_t m; - for (int r = 0; r < 3; ++r) for (int c = 0; c < 4; ++c) m.m[r][c] = t[c * 4 + r]; - return m; - } - - static void invertRigid(const float m[16], float o[16]) { - for (int r = 0; r < 3; ++r) for (int c = 0; c < 3; ++c) o[c * 4 + r] = m[r * 4 + c]; - for (int r = 0; r < 3; ++r) o[12 + r] = -(o[r] * m[12] + o[4 + r] * m[13] + o[8 + r] * m[14]); - o[3] = o[7] = o[11] = 0; o[15] = 1; - } - - static bool differs(const vr::HmdMatrix34_t& a, const vr::HmdMatrix34_t& b, float eps) { - for (int r = 0; r < 3; ++r) for (int c = 0; c < 4; ++c) if (std::fabs(a.m[r][c] - b.m[r][c]) > eps) return true; - return false; - } -}; diff --git a/src/perf_settings.h b/src/perf_settings.h index 562f691..b501390 100644 --- a/src/perf_settings.h +++ b/src/perf_settings.h @@ -123,7 +123,7 @@ struct PerfSettings { // why a home looks or runs wrong, not for keeping: Frame Home clears the file each time it starts. struct DebugSettings { int view = 0; // HslRenderer::DebugView: 0 normal, 1 wireframe, 2 meshes, 3 GPU cost, 4 overdraw - int world = 0; // the world layer drawn over the home: walkable area, surfaces, spawns, seats, wall spots + int world = 0; // the world layer drawn over the home: walkable area, surfaces, spawns, seats int hideSeeThrough = 0, hideCutout = 0, hideSky = 0; // leave those parts of the home out int shot = 0; // asks for a picture of what's shown (logs/frame-