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-