mirror of
https://github.com/Toastconcern/FrameStudio.git
synced 2026-10-28 17:26:06 +01:00
Remove wall spots
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
a0804df866
commit
64ad00fa1d
21 files changed
+63
-423
No files matched your search
@@ -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)).
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
+2
-6
@@ -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<GuideVertex>& 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<GuideVertex
|
||||
const P ex{p.pos[0] + p.exit[0], p.pos[1] + p.exit[1] + 0.02f, p.pos[2] + p.exit[2]};
|
||||
if (p.exit[0] != 0 || p.exit[2] != 0) { line(o, at, ex, guide::alpha(kSeat, 0.5f)); circle(o, ex, 0.12f, guide::alpha(kSeat, 0.7f)); }
|
||||
}
|
||||
for (const WorldWallSpot& s : w.wallSpots) {
|
||||
const float h[3] = {s.width * 0.5f, s.height * 0.5f, 0.01f};
|
||||
box(o, s.world, h, kSpot);
|
||||
}
|
||||
for (const WorldGravity& z : w.gravity) {
|
||||
if (z.kind == WorldGravity::Planet) circle(o, xf(z.world, {0, 0, 0}), z.radius, kGravity, 64);
|
||||
else box(o, z.world, z.half, kGravity);
|
||||
|
||||
+5
-5
@@ -24,7 +24,7 @@ struct HomeReport {
|
||||
size_t seeThrough = 0, seeThroughTris = 0, cutout = 0, skinned = 0, animated = 0, lightmapped = 0, sky = 0, programs = 0;
|
||||
size_t textures = 0; // distinct
|
||||
double textureMB = 0, decodedMB = 0, geometryMB = 0; // compressed (as the GPU gets it when it can), decoded RGBA, vertices + indices
|
||||
size_t spawns = 0, seats = 0, hotspots = 0, colliders = 0, wallSpots = 0, gravity = 0;
|
||||
size_t spawns = 0, seats = 0, hotspots = 0, colliders = 0, gravity = 0;
|
||||
double walkArea = -1; // m², < 0: no walkable area known
|
||||
double loadSec = 0; // set by the caller
|
||||
struct Mesh { size_t tris; int mesh; };
|
||||
@@ -50,8 +50,8 @@ struct HomeReport {
|
||||
o += b;
|
||||
std::snprintf(b, sizeof b, "%zu textures: %.0f MB compressed, %.0f MB decoded; geometry %.0f MB\n", textures, textureMB, decodedMB, geometryMB);
|
||||
o += b;
|
||||
std::snprintf(b, sizeof b, "world: %zu spawns, %zu seats, %zu hotspots, %zu surfaces, %zu wall spots, %zu gravity zones", spawns, seats, hotspots,
|
||||
colliders, wallSpots, gravity);
|
||||
std::snprintf(b, sizeof b, "world: %zu spawns, %zu seats, %zu hotspots, %zu surfaces, %zu gravity zones", spawns, seats, hotspots,
|
||||
colliders, gravity);
|
||||
o += b;
|
||||
o += walkArea >= 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)))
|
||||
|
||||
+1
-1
@@ -485,7 +485,7 @@ bool loadHslScene(const std::vector<std::string>& 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) {
|
||||
|
||||
+1
-1
@@ -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).
|
||||
|
||||
@@ -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<std::string>& 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<GuideVertex> 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<std::string>& 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<std::string>& 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<std::string>& 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();
|
||||
|
||||
+1
-1
@@ -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 <openvr.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
@@ -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 <openvr.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
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<OverlayInfo> panels;
|
||||
{ std::lock_guard<std::mutex> 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<int>(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<int>(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<WorldWallSpot> spots_;
|
||||
std::map<vr::VROverlayHandle_t, State> state_;
|
||||
std::vector<OverlayInfo> panels_;
|
||||
std::mutex mutex_;
|
||||
std::thread scanner_;
|
||||
std::atomic<bool> 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<OverlayInfo> found;
|
||||
for (const OverlayInfo& o : listOverlays())
|
||||
if (isUserPanel(o) && o.key.rfind("system.", 0) != 0) found.push_back(o);
|
||||
{ std::lock_guard<std::mutex> 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;
|
||||
}
|
||||
};
|
||||
+1
-1
@@ -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-<time>.png): the time it was asked
|
||||
// (Unix seconds); one from before Frame Home started is old
|
||||
|
||||
+2
-4
@@ -177,8 +177,6 @@ void writeWorld(Writer& w, const HomeWorld& wd, const std::vector<MeshData>& mes
|
||||
w.str(c.mesh);
|
||||
}
|
||||
w.vec(wd.walkPositions); w.vec(wd.walkIndices);
|
||||
w.u(static_cast<u32>(wd.wallSpots.size()));
|
||||
for (const WorldWallSpot& ws : wd.wallSpots) { w.str(ws.name); w.fa(ws.world, 16); w.f(ws.width); w.f(ws.height); }
|
||||
w.u(static_cast<u32>(wd.gravity.size()));
|
||||
for (const WorldGravity& g : wd.gravity) { w.str(g.name); w.pod(g.kind); w.fa(g.world, 16); w.fa(g.half, 3); w.f(g.radius); w.pod(g.priority); }
|
||||
w.f(wd.fallLimit);
|
||||
@@ -202,8 +200,8 @@ void readWorld(Reader& r, HomeWorld& wd, u32 version, std::vector<std::pair<size
|
||||
if (version >= 4) c.mesh = r.str();
|
||||
}
|
||||
wd.walkPositions = r.vec<float>(); wd.walkIndices = r.vec<u32>();
|
||||
wd.wallSpots.resize(r.count(76));
|
||||
for (WorldWallSpot& ws : wd.wallSpots) { if (r.bad) break; ws.name = r.str(); r.fa(ws.world, 16); ws.width = r.f(); ws.height = r.f(); }
|
||||
if (version < 14) // wall spots, now removed: read past them
|
||||
for (u32 n = r.count(76); n-- && !r.bad;) { float m[16]; r.str(); r.fa(m, 16); r.f(); r.f(); }
|
||||
if (version >= 6) {
|
||||
wd.gravity.resize(r.count(92));
|
||||
for (WorldGravity& g : wd.gravity) { if (r.bad) break; g.name = r.str(); g.kind = r.pod<int>(); r.fa(g.world, 16); r.fa(g.half, 3); g.radius = r.f(); g.priority = r.pod<int>(); }
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
constexpr u32 kSceneBakeVersion = 13; // 2: world; 3: HDR maps as ASTC, binary world; 4: roles; 5: mesh refs; 6: gravity; 7: mesh paths; 8: ambience name; 9: compressed 8-bit lightmaps; 10: reflection cube as ASTC; 11: walkable area (and simplified colliders); 12: prepared and specialized shaders; 13: Clay / glTF CPU animation
|
||||
constexpr u32 kSceneBakeVersion = 14; // 2: world; 3: HDR maps as ASTC, binary world; 4: roles; 5: mesh refs; 6: gravity; 7: mesh paths; 8: ambience name; 9: compressed 8-bit lightmaps; 10: reflection cube as ASTC; 11: walkable area (and simplified colliders); 12: prepared and specialized shaders; 13: Clay / glTF CPU animation; 14: wall spots removed
|
||||
|
||||
struct VisibilityResult;
|
||||
struct ObjectEdit;
|
||||
|
||||
+1
-1
@@ -902,7 +902,7 @@ struct Panel {
|
||||
"Hold still stops the environment's moving parts; animated environments then cost less.",
|
||||
"With the menu open the environment is only its backdrop. Half rate leaves the GPU to the menu and its apps.",
|
||||
"Wireframe: every triangle edge. Meshes: each in its own colour. GPU cost: green is cheap, red is costly. Overdraw: bright where surfaces pile up.",
|
||||
"Where you can walk (green), surfaces and colliders, spawns, seats, wall spots and gravity zones.",
|
||||
"Where you can walk (green), surfaces and colliders, spawns, seats and gravity zones.",
|
||||
"Glass, water, glow and fog. Leave them out to see whether they're what's slow.",
|
||||
"Leaves, grass and fences. Leave them out to see whether they're what's slow.",
|
||||
"The sky around the environment.",
|
||||
|
||||
+7
-82
@@ -7,9 +7,8 @@
|
||||
// Off unless GenParams asks (guesses, and a wrong one is worse than none):
|
||||
// walls — Wall boxes on flat upright faces that are architecture (named a wall, or 2.4 m+ / up to the ceiling),
|
||||
// and along drops inside a tall structure
|
||||
// wall spots — the middle of a tall, solid stretch of wall with room to stand back from it (SteamVR panels)
|
||||
// seats — a top 35-62 cm over the floor with a backrest behind and floor in front, or a stool
|
||||
// hotspots — in front of wall spots, lookouts, the middles of open spaces
|
||||
// hotspots — lookouts, the middles of open spaces
|
||||
// Everything made is named "Auto …" (Frame Studio replaces them on a re-run).
|
||||
#include "walkgrid.h"
|
||||
|
||||
@@ -25,19 +24,18 @@ struct GenParams {
|
||||
float wallMinHeight = 0.9f; // a wall is at least this tall over the floor in front of it (a railing)
|
||||
float wallMinRun = 0.6f; // shortest wall (m)
|
||||
float seatLo = 0.35f, seatHi = 0.62f; // a seat's top over the floor in front of it (m): low lounge sofas to thick cushions
|
||||
int maxSpots = 5, maxHotspots = 12;
|
||||
int maxHotspots = 12;
|
||||
// Seats and hotspots are guesses about how people use a place, and a wrong one (a seat on a planter's rim) is worse
|
||||
// than none: off unless asked for. Auto-generate makes the floor, the walls, the objects' collision and wall spots.
|
||||
// than none: off unless asked for. Auto-generate makes the floor, the walls and the objects' collision.
|
||||
bool seats = false, hotspots = false;
|
||||
// Walls (boxes on faces judged to be walls) and wall spots are guesses too: off. Every mesh gets its own collision
|
||||
// Walls (boxes on faces judged to be walls) are guesses too: off. Every mesh gets its own collision
|
||||
// instead (GenResult::objects), whatever it is.
|
||||
bool walls = false, spots = false;
|
||||
bool walls = false;
|
||||
};
|
||||
|
||||
struct GenResult {
|
||||
std::vector<WorldCollider> walls, objects;
|
||||
std::vector<WorldPoint> seats, hotspots;
|
||||
std::vector<WorldWallSpot> spots;
|
||||
};
|
||||
|
||||
namespace world_gen_detail {
|
||||
@@ -618,63 +616,6 @@ inline GenResult generateWorldItems(const WalkGrid& g, const HomeWorld& have, co
|
||||
for (WorldCollider& c : encl) out.walls.push_back(std::move(c));
|
||||
}
|
||||
|
||||
// ── Wall spots: the solid middle of a tall wall, with room to stand back from it ──
|
||||
if (P.spots) {
|
||||
const float sx = have.spawns.empty() ? 0.0f : have.spawns[0].pos[0], sz = have.spawns.empty() ? 0.0f : have.spawns[0].pos[2];
|
||||
struct Cand { float score, x, y, z, ux, uz, fx, fz, width, height; };
|
||||
std::vector<Cand> cands;
|
||||
for (const Run& rn : runs) {
|
||||
const Wall& w = *rn.w;
|
||||
const float L = (rn.i1 - rn.i0 + 1) * kCell;
|
||||
if (w.glass || w.hint == kHintBlocker || L < 1.6f || rn.top - rn.floor < 2.2f) continue; // (a blocker wall: Meta's, not one to hang on)
|
||||
const float width = std::min(L - 0.4f, 2.4f), height = std::clamp(rn.top - rn.floor - 1.0f, 0.9f, 1.4f);
|
||||
const float bottom = rn.floor + 0.85f;
|
||||
const int wc = static_cast<int>(std::ceil(width / kCell));
|
||||
const float fx = w.nx * rn.side, fz = w.nz * rn.side, ux = -w.nz, uz = w.nx;
|
||||
// slide along the wall from its middle until the panel sits on solid wall (no window, door or gap)
|
||||
const int mid = (rn.i0 + rn.i1) / 2;
|
||||
bool placed = false;
|
||||
for (int off = 0; off <= (rn.i1 - rn.i0 - wc) / 2 + 1 && !placed; off += 2)
|
||||
for (int sgn : {1, -1}) {
|
||||
const int c0 = mid + sgn * off - wc / 2, c1 = c0 + wc - 1;
|
||||
if (c0 < rn.i0 || c1 > rn.i1 || w.fill(c0, c1, bottom - 0.1f, bottom + height + 0.1f) < 0.97f) continue;
|
||||
const float s = w.s0 + (c0 + c1 + 1) * 0.5f * kCell, px = w.nx * w.d + ux * s, pz = w.nz * w.d + uz * s;
|
||||
// clear air in front at panel height (2.5 m straight out, 1.5 m across all of it), floor 2 m back
|
||||
bool clear = true;
|
||||
for (float t = 0.3f; t <= 2.5f && clear; t += 0.2f)
|
||||
for (float lat : {-width * 0.5f, -width * 0.25f, 0.0f, width * 0.25f, width * 0.5f})
|
||||
if ((lat == 0 || t <= 1.5f) && solidBetween(g, px + fx * t + ux * lat, pz + fz * t + uz * lat, rn.floor + 0.7f, rn.floor + 2.2f)) { clear = false; break; }
|
||||
if (!clear || !areaFloor(g, px + fx * 2.0f, pz + fz * 2.0f, rn.floor, 0.3f)) continue;
|
||||
// a wall round a space, not a slab standing between two paths (floor behind it too)
|
||||
bool divider = false;
|
||||
for (float t : {0.3f, 0.5f, 0.7f}) divider |= areaFloor(g, px - fx * t, pz - fz * t, rn.floor, 0.5f) != nullptr;
|
||||
if (divider) continue;
|
||||
const float toSpawn = std::hypot(sx - px, sz - pz), facing = ((sx - px) * fx + (sz - pz) * fz) / std::max(0.1f, toSpawn);
|
||||
cands.push_back({width + (facing > 0.3f && toSpawn < 9 ? 2.0f : 0.0f), px + fx * 0.02f, bottom + height * 0.5f, pz + fz * 0.02f, ux, uz, fx, fz, width, height});
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::sort(cands.begin(), cands.end(), [](const Cand& a, const Cand& b) { return a.score > b.score; });
|
||||
for (const Cand& c : cands) {
|
||||
if (static_cast<int>(out.spots.size()) >= P.maxSpots) break;
|
||||
bool close = false;
|
||||
for (const WorldWallSpot& w : have.wallSpots) close |= std::hypot(w.world[12] - c.x, w.world[14] - c.z) < 1.5f && std::fabs(w.world[13] - c.y) < 1.5f;
|
||||
for (const WorldWallSpot& w : out.spots) close |= std::hypot(w.world[12] - c.x, w.world[14] - c.z) < 3.0f;
|
||||
if (close) continue;
|
||||
// the frame: X along the wall, Y up, Z (= X x Y) out into the room
|
||||
float ux = c.ux, uz = c.uz;
|
||||
if (-uz * c.fx + ux * c.fz < 0) { ux = -ux; uz = -uz; }
|
||||
WorldWallSpot w;
|
||||
w.name = "Auto wall spot";
|
||||
const float m[16] = {ux, 0, uz, 0, 0, 1, 0, 0, -uz, 0, ux, 0, c.x, c.y, c.z, 1};
|
||||
std::copy_n(m, 16, w.world);
|
||||
w.width = c.width;
|
||||
w.height = c.height;
|
||||
out.spots.push_back(w);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Seats: where on a seat-high top you'd sit — a backrest behind, floor in front — or a free-standing stool ──
|
||||
if (P.seats) {
|
||||
const float cs = g.p.cell;
|
||||
@@ -994,23 +935,7 @@ inline GenResult generateWorldItems(const WalkGrid& g, const HomeWorld& have, co
|
||||
out.hotspots.push_back(p);
|
||||
taken.push_back({x, y, z});
|
||||
};
|
||||
// 1. in front of each wall spot, looking at it
|
||||
std::vector<const WorldWallSpot*> spots;
|
||||
for (const WorldWallSpot& w : have.wallSpots) spots.push_back(&w);
|
||||
for (const WorldWallSpot& w : out.spots) spots.push_back(&w);
|
||||
for (const WorldWallSpot* w : spots)
|
||||
for (float sgn : {1.0f, -1.0f}) {
|
||||
const float fx = w->world[8] * sgn, fz = w->world[10] * sgn, fl = std::hypot(fx, fz);
|
||||
if (fl < 0.5f) continue;
|
||||
float x = w->world[12] + fx / fl * 2.2f, z = w->world[14] + fz / fl * 2.2f;
|
||||
const WalkSpan* f = areaFloor(g, x, z, w->world[13] - 1.5f, 1.2f);
|
||||
if (!f) continue;
|
||||
const float y = f->hi;
|
||||
if (!g.roomAt(x, y, z, 0.5f) || !free(x, y, z, 2.0f)) continue;
|
||||
add(x, y, z, -fx / fl, -fz / fl);
|
||||
break;
|
||||
}
|
||||
// 2. lookouts: where the area's edge is a drop with open air beyond it (a terrace's rail, a balcony)
|
||||
// 1. lookouts: where the area's edge is a drop with open air beyond it (a terrace's rail, a balcony)
|
||||
{
|
||||
struct Edge { float x, y, z, ox, oz; };
|
||||
std::vector<Edge> view;
|
||||
@@ -1041,7 +966,7 @@ inline GenResult generateWorldItems(const WalkGrid& g, const HomeWorld& have, co
|
||||
++lookouts;
|
||||
}
|
||||
}
|
||||
// 3. the middles of open spaces (1.5 m or more from any edge, 6 m apart, five at most), facing the longest view
|
||||
// 2. the middles of open spaces (1.5 m or more from any edge, 6 m apart, five at most), facing the longest view
|
||||
{
|
||||
std::vector<std::array<float, 4>> cand;
|
||||
for (int z = 0; z < g.nz; z += 2)
|
||||
|
||||
+27
-81
@@ -393,13 +393,13 @@ struct UndoStep {
|
||||
};
|
||||
|
||||
// The world layer's kinds of things (Studio's world editor).
|
||||
enum WorldKind { kSpawn, kSeat, kHotspot, kCollider, kWallSpot, kGravity, kWorldKinds };
|
||||
const char* kWorldNames[kWorldKinds] = {"Spawns", "Seats", "Teleport Hotspots", "Walls & Surfaces", "Wall Spots", "Gravity Zones"};
|
||||
enum WorldKind { kSpawn, kSeat, kHotspot, kCollider, kGravity, kWorldKinds };
|
||||
const char* kWorldNames[kWorldKinds] = {"Spawns", "Seats", "Teleport Hotspots", "Walls & Surfaces", "Gravity Zones"};
|
||||
|
||||
// What the viewport can show, each with its own switch (View > Show, the explorer's World headers).
|
||||
enum ShowCat { kShowSpawns, kShowSeats, kShowHotspots, kShowFloors, kShowWalls, kShowSolidMeshes, kShowTriggers, kShowWallSpots, kShowGravity, kShowWalkArea, kShowUnreached, kShowCats };
|
||||
const char* kShowNames[kShowCats] = {"Spawn points", "Seats", "Teleport hotspots", "Drawn floors", "Surface shapes", "Surface & collider meshes", "Triggers", "Wall spots", "Gravity zones", "Walkable area", "Floors out of reach"};
|
||||
const uint32_t kShowColors[kShowCats] = {0x3fd2ff, 0x5fe07a, 0xffd84a, 0x4cd964, 0xff7a2e, 0xffa033, 0x9a9a9a, 0x5b8def, 0xc07aff, 0x2ec756, 0x808080};
|
||||
enum ShowCat { kShowSpawns, kShowSeats, kShowHotspots, kShowFloors, kShowWalls, kShowSolidMeshes, kShowTriggers, kShowGravity, kShowWalkArea, kShowUnreached, kShowCats };
|
||||
const char* kShowNames[kShowCats] = {"Spawn points", "Seats", "Teleport hotspots", "Drawn floors", "Surface shapes", "Surface & collider meshes", "Triggers", "Gravity zones", "Walkable area", "Floors out of reach"};
|
||||
const uint32_t kShowColors[kShowCats] = {0x3fd2ff, 0x5fe07a, 0xffd84a, 0x4cd964, 0xff7a2e, 0xffa033, 0x9a9a9a, 0xc07aff, 0x2ec756, 0x808080};
|
||||
|
||||
// A gravity zone's kind (WorldGravity::Kind), as Studio names and explains it.
|
||||
const char* kGravityNames[3] = {"Direction", "Planet", "Walk on any surface"};
|
||||
@@ -673,7 +673,7 @@ struct Studio {
|
||||
int wKind = -1, wIndex = -1;
|
||||
bool showWorld = true;
|
||||
// What the viewport shows. View only: the per-item eyes (hidden) are kept with the world layer.
|
||||
bool showCat[kShowCats] = {true, true, true, true, true, false, false, true, true, true, true};
|
||||
bool showCat[kShowCats] = {true, true, true, true, true, false, false, true, true, true};
|
||||
bool showMeshes = true; // the home's own objects
|
||||
bool isolating = false; // View > Show Only Selected (I)
|
||||
int isoObject = -1, isoKind = -1, isoIndex = -1;
|
||||
@@ -991,8 +991,8 @@ struct Studio {
|
||||
ImGui::TextWrapped("%zu meshes, %.2f M triangles (%zu see-through, %zu cut-out, %zu animated), %zu shaders", r.meshes, r.triangles / 1e6, r.seeThrough,
|
||||
r.cutout, r.animated, r.programs);
|
||||
ImGui::TextWrapped("%zu textures: %.0f MB compressed, %.0f MB decoded; geometry %.0f MB", r.textures, r.textureMB, r.decodedMB, r.geometryMB);
|
||||
if (r.walkArea >= 0) ImGui::TextWrapped("%.0f m² to walk on; %zu spawns, %zu seats, %zu surfaces, %zu wall spots", r.walkArea, r.spawns, r.seats, r.colliders, r.wallSpots);
|
||||
else ImGui::TextWrapped("%zu spawns, %zu seats, %zu surfaces, %zu wall spots", r.spawns, r.seats, r.colliders, r.wallSpots);
|
||||
if (r.walkArea >= 0) ImGui::TextWrapped("%.0f m² to walk on; %zu spawns, %zu seats, %zu surfaces", r.walkArea, r.spawns, r.seats, r.colliders);
|
||||
else ImGui::TextWrapped("%zu spawns, %zu seats, %zu surfaces", r.spawns, r.seats, r.colliders);
|
||||
if (ImGui::Button("Check Again")) report.reset();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Copy as Text")) ImGui::SetClipboardText(r.text().c_str());
|
||||
@@ -1131,7 +1131,7 @@ struct Studio {
|
||||
static const char* views[] = {"Normal", "Wireframe", "Meshes", "GPU cost", "Overdraw"};
|
||||
if (int p = choice("View##hd", views, 5, d.view, "What Frame Home draws: the environment, or how it's made and what it costs."); p >= 0) { d.view = p; changed = true; }
|
||||
static const char* wl[] = {"Hidden", "Shown"};
|
||||
if (int p = choice("World layer##hd", wl, 2, d.world, "Where you can walk (green), surfaces and colliders, spawns, seats, wall spots and gravity zones."); p >= 0) {
|
||||
if (int p = choice("World layer##hd", wl, 2, d.world, "Where you can walk (green), surfaces and colliders, spawns, seats and gravity zones."); p >= 0) {
|
||||
d.world = p; changed = true; }
|
||||
static const char* lo[] = {"Shown", "Left out"};
|
||||
if (int p = choice("See-through##hd", lo, 2, d.hideSeeThrough, "Glass, water, glow and fog."); p >= 0) { d.hideSeeThrough = p; changed = true; }
|
||||
@@ -2961,7 +2961,7 @@ struct Studio {
|
||||
bool ok = false;
|
||||
int replaced = 0, kept = 0;
|
||||
bool spawnAdded = false;
|
||||
int walls = 0, objects = 0, colliders = 0, seats = 0, spots = 0, hotspots = 0; // world items made
|
||||
int walls = 0, objects = 0, colliders = 0, seats = 0, hotspots = 0; // world items made
|
||||
WalkReport walk;
|
||||
float ms = 0;
|
||||
} autoReport;
|
||||
@@ -3006,11 +3006,10 @@ struct Studio {
|
||||
w.colliders.erase(std::remove_if(w.colliders.begin(), w.colliders.end(), [](const WorldCollider& c) { return isAuto(c.name); }), w.colliders.end());
|
||||
r.replaced = static_cast<int>(n0 - w.colliders.size());
|
||||
w.spawns.erase(std::remove_if(w.spawns.begin(), w.spawns.end(), [](const WorldPoint& p) { return p.name == "Auto spawn"; }), w.spawns.end());
|
||||
// the walls, seats, wall spots and hotspots it made last time go too (yours stay)
|
||||
// the walls, seats and hotspots it made last time go too (yours stay)
|
||||
auto autoPoint = [](const WorldPoint& p) { return isAuto(p.name); };
|
||||
w.seats.erase(std::remove_if(w.seats.begin(), w.seats.end(), autoPoint), w.seats.end());
|
||||
w.hotspots.erase(std::remove_if(w.hotspots.begin(), w.hotspots.end(), autoPoint), w.hotspots.end());
|
||||
w.wallSpots.erase(std::remove_if(w.wallSpots.begin(), w.wallSpots.end(), [](const WorldWallSpot& s) { return isAuto(s.name); }), w.wallSpots.end());
|
||||
for (const Object& o : objects) if (!o.deleted && surfaceIndex(o) >= 0) ++r.kept;
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
{ // Everything that can be a floor is one: a mesh you stand on somewhere (the floor, stairs, a rug, a deck,
|
||||
@@ -3166,9 +3165,8 @@ struct Studio {
|
||||
if (r.objects) { swatch(kShowColors[kShowSolidMeshes]); ImGui::Text("%d floor mesh%s made surfaces: walkable where flat, blocking where too steep", r.objects, r.objects == 1 ? "" : "es"); }
|
||||
if (r.colliders) { swatch(kRoleColors[WorldCollider::Object]); ImGui::Text("%d object%s given a collider: you don't walk through %s", r.colliders, r.colliders == 1 ? "" : "s", r.colliders == 1 ? "it" : "them"); }
|
||||
if (r.seats) { swatch(kShowColors[kShowSeats]); ImGui::Text("%d seat%s on the chairs, sofas and stools it found", r.seats, r.seats == 1 ? "" : "s"); }
|
||||
if (r.spots) { swatch(kShowColors[kShowWallSpots]); ImGui::Text("%d wall spot%s for panels, on large flat walls", r.spots, r.spots == 1 ? "" : "s"); }
|
||||
if (r.hotspots) { swatch(kShowColors[kShowHotspots]); ImGui::Text("%d teleport hotspot%s spread over the floor", r.hotspots, r.hotspots == 1 ? "" : "s"); }
|
||||
if (r.walls + r.objects + r.colliders + r.seats + r.spots + r.hotspots) ImGui::TextDisabled("They're all named \"Auto …\" in the World list: edit or delete any; running it again replaces them.");
|
||||
if (r.walls + r.objects + r.colliders + r.seats + r.hotspots) ImGui::TextDisabled("They're all named \"Auto …\" in the World list: edit or delete any; running it again replaces them.");
|
||||
ImGui::Spacing();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, hex(kDim));
|
||||
if (r.kept) ImGui::Text(r.kept == 1 ? "1 object keeps the surface you gave it (Surface or Walk through)" : "%d objects keep the surfaces you gave them (Surface or Walk through)", r.kept);
|
||||
@@ -3243,7 +3241,7 @@ struct Studio {
|
||||
if (ImGui::Button(walkSaved() ? "Auto-generate Again" : "Auto-generate Walkable Surfaces", ImVec2(-1, 0))) autoGenerate();
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Bakes where you can walk from everything solid in the home and saves it with it (one undo step),\n"
|
||||
"and adds walls along its edges, seats on chairs and sofas, wall spots for panels and teleport hotspots.\n"
|
||||
"and adds walls along its edges, seats on chairs and sofas and teleport hotspots.\n"
|
||||
"Floors need headroom; furniture, plants, railings and walls block; steps up to the step height connect;\n"
|
||||
"seams are bridged; floors you can't reach from the start are left out. Object Surfaces override it.");
|
||||
}
|
||||
@@ -3285,7 +3283,7 @@ struct Studio {
|
||||
updateTitle();
|
||||
}
|
||||
|
||||
// ── World layer (spawns, seats, hotspots, walls, wall spots, walkable areas) ──
|
||||
// ── World layer (spawns, seats, hotspots, walls, walkable areas) ──
|
||||
|
||||
int worldCount(int kind) const {
|
||||
if (!scene) return 0;
|
||||
@@ -3295,7 +3293,6 @@ struct Studio {
|
||||
case kSeat: return static_cast<int>(w.seats.size());
|
||||
case kHotspot: return static_cast<int>(w.hotspots.size());
|
||||
case kCollider: return static_cast<int>(w.colliders.size());
|
||||
case kWallSpot: return static_cast<int>(w.wallSpots.size());
|
||||
case kGravity: return static_cast<int>(w.gravity.size());
|
||||
default: return 0;
|
||||
}
|
||||
@@ -3306,7 +3303,6 @@ struct Studio {
|
||||
case kSpawn: return kShowSpawns;
|
||||
case kSeat: return kShowSeats;
|
||||
case kHotspot: return kShowHotspots;
|
||||
case kWallSpot: return kShowWallSpots;
|
||||
case kGravity: return kShowGravity;
|
||||
default: {
|
||||
const WorldCollider& c = scene->world.colliders[i];
|
||||
@@ -3321,7 +3317,6 @@ struct Studio {
|
||||
bool* worldHidden(int kind, int i) {
|
||||
if (WorldPoint* p = worldPoint(kind, i)) return &p->hidden;
|
||||
if (kind == kCollider) return &scene->world.colliders[i].hidden;
|
||||
if (kind == kWallSpot) return &scene->world.wallSpots[i].hidden;
|
||||
if (kind == kGravity) return &scene->world.gravity[i].hidden;
|
||||
return nullptr;
|
||||
}
|
||||
@@ -3347,7 +3342,6 @@ struct Studio {
|
||||
static const char* shape[] = {"box", "sphere", "capsule", "mesh"};
|
||||
return (c.name.empty() ? std::string("surface ") + std::to_string(i + 1) : c.name) + " (" + shape[std::clamp(c.kind, 0, 3)] + ")";
|
||||
}
|
||||
if (kind == kWallSpot) { const WorldWallSpot& ws = scene->world.wallSpots[i]; return ws.name.empty() ? "wall spot " + std::to_string(i + 1) : ws.name; }
|
||||
if (kind == kGravity) {
|
||||
const WorldGravity& g = scene->world.gravity[i];
|
||||
return (g.name.empty() ? "gravity " + std::to_string(i + 1) : g.name) + " (" + kGravityNames[std::clamp(g.kind, 0, 2)] + ")";
|
||||
@@ -3370,7 +3364,6 @@ struct Studio {
|
||||
std::memcpy(m, c.world, 64);
|
||||
return true;
|
||||
}
|
||||
if (kind == kWallSpot) { std::memcpy(m, scene->world.wallSpots[i].world, 64); return true; }
|
||||
if (kind == kGravity) { std::memcpy(m, scene->world.gravity[i].world, 64); return true; }
|
||||
return false;
|
||||
}
|
||||
@@ -3399,13 +3392,6 @@ struct Studio {
|
||||
if (c.kind == WorldCollider::Box) for (int k = 0; k < 3; ++k) c.half[k] = std::max(0.01f, b.half[k] * sc[k]);
|
||||
else if (c.kind == WorldCollider::Sphere) c.radius = std::max(0.01f, b.radius * (sc[0] + sc[1] + sc[2]) / 3);
|
||||
else { c.radius = std::max(0.01f, b.radius * (sc[0] + sc[2]) * 0.5f); c.length = std::max(0.0f, b.length * sc[1]); }
|
||||
} else if (kind == kWallSpot) {
|
||||
WorldWallSpot& ws = scene->world.wallSpots[i];
|
||||
const WorldWallSpot& b = i < static_cast<int>(dragBase.wallSpots.size()) ? dragBase.wallSpots[i] : ws;
|
||||
float sc[3];
|
||||
unscale(m, ws.world, sc);
|
||||
ws.width = std::max(0.05f, b.width * sc[0]);
|
||||
ws.height = std::max(0.05f, b.height * sc[1]);
|
||||
} else if (kind == kGravity) { // scaling resizes the region (box) or the pull (planet)
|
||||
WorldGravity& g = scene->world.gravity[i];
|
||||
const WorldGravity& b = i < static_cast<int>(dragBase.gravity.size()) ? dragBase.gravity[i] : g;
|
||||
@@ -3500,11 +3486,6 @@ struct Studio {
|
||||
wIndex = combineTouching(static_cast<int>(w.colliders.size()) - 1);
|
||||
selected = -1;
|
||||
return;
|
||||
} else if (kind == kWallSpot) {
|
||||
WorldWallSpot ws;
|
||||
ws.name = "Wall spot " + std::to_string(w.wallSpots.size() + 1);
|
||||
translation(ws.world, x, y + 1.4f, z);
|
||||
w.wallSpots.push_back(ws);
|
||||
}
|
||||
wKind = kind;
|
||||
wIndex = worldCount(kind) - 1;
|
||||
@@ -3513,7 +3494,7 @@ struct Studio {
|
||||
|
||||
// Points the camera at a world item (double-click in the explorer, F).
|
||||
void frameWorld(int kind, int i) {
|
||||
float m[16], mn[3], mx[3];
|
||||
float mn[3], mx[3];
|
||||
if (WorldPoint* p = worldPoint(kind, i)) {
|
||||
for (int k = 0; k < 3; ++k) { mn[k] = p->pos[k] - 1; mx[k] = p->pos[k] + 1; }
|
||||
} else if (kind == kCollider) {
|
||||
@@ -3532,10 +3513,6 @@ struct Studio {
|
||||
const WorldGravity& g = scene->world.gravity[i];
|
||||
const float r = g.kind == WorldGravity::Planet ? g.radius : std::max({g.half[0], g.half[1], g.half[2]});
|
||||
for (int k = 0; k < 3; ++k) { mn[k] = g.world[12 + k] - r; mx[k] = g.world[12 + k] + r; }
|
||||
} else if (kind == kWallSpot && worldMatrix(kind, i, m)) {
|
||||
const WorldWallSpot& ws = scene->world.wallSpots[i];
|
||||
const float r = std::max(ws.width, ws.height) * 0.5f;
|
||||
for (int k = 0; k < 3; ++k) { mn[k] = m[12 + k] - r; mx[k] = m[12 + k] + r; }
|
||||
} else return;
|
||||
frameBounds(mn, mx);
|
||||
}
|
||||
@@ -3556,7 +3533,6 @@ struct Studio {
|
||||
case kSeat: w.seats.erase(w.seats.begin() + wIndex); break;
|
||||
case kHotspot: w.hotspots.erase(w.hotspots.begin() + wIndex); break;
|
||||
case kCollider: w.colliders.erase(w.colliders.begin() + wIndex); break;
|
||||
case kWallSpot: w.wallSpots.erase(w.wallSpots.begin() + wIndex); break;
|
||||
case kGravity: w.gravity.erase(w.gravity.begin() + wIndex); break;
|
||||
}
|
||||
if (isolating && isoKind == wKind) { // keep Show Only Selected on the same item (or end it with it)
|
||||
@@ -4715,7 +4691,7 @@ struct Studio {
|
||||
}
|
||||
}
|
||||
}
|
||||
// World: the home's spawns, seats, hotspots, walls and wall spots (Studio's own format, from Meta's when converted).
|
||||
// World: the home's spawns, seats, hotspots and walls (Studio's own format, from Meta's when converted).
|
||||
if (scene && !objects.empty()) {
|
||||
int total = 0;
|
||||
for (int k = 0; k < kWorldKinds; ++k) total += worldCount(k);
|
||||
@@ -5014,16 +4990,6 @@ struct Studio {
|
||||
if (c.kind == WorldCollider::Sphere || c.kind == WorldCollider::Capsule) { fieldLabel("Radius"); ImGui::DragFloat("##wrad", &c.radius, 0.01f, 0.01f, 50.0f, "%.2f m"); track(); }
|
||||
if (c.kind == WorldCollider::Capsule) { fieldLabel("Length"); ImGui::DragFloat("##wlen", &c.length, 0.01f, 0.0f, 50.0f, "%.2f m"); track(); }
|
||||
}
|
||||
} else if (wKind == kWallSpot) {
|
||||
WorldWallSpot& ws = scene->world.wallSpots[wIndex];
|
||||
float t[3], r[3], sc[3];
|
||||
ImGuizmo::DecomposeMatrixToComponents(ws.world, t, r, sc);
|
||||
bool moved = false;
|
||||
fieldLabel("Location"); moved |= ImGui::DragFloat3("##wsloc", t, 0.01f, 0, 0, "%.2f m"); track();
|
||||
fieldLabel("Rotation"); tidyZero(r, 0.001f); moved |= ImGui::DragFloat3("##wsrot", r, 0.5f, 0, 0, "%.0f\u00b0"); track();
|
||||
if (moved) { sc[0] = sc[1] = sc[2] = 1; ImGuizmo::RecomposeMatrixFromComponents(t, r, sc, ws.world); }
|
||||
fieldLabel("Width"); ImGui::DragFloat("##wwid", &ws.width, 0.01f, 0.1f, 20.0f, "%.2f m"); track();
|
||||
fieldLabel("Height"); ImGui::DragFloat("##whei", &ws.height, 0.01f, 0.1f, 20.0f, "%.2f m"); track();
|
||||
}
|
||||
if (wKind == kGravity) {
|
||||
WorldGravity& g = scene->world.gravity[wIndex];
|
||||
@@ -5569,7 +5535,7 @@ struct Studio {
|
||||
}
|
||||
|
||||
// The world layer drawn over the 3D view (x-ray: always visible): spawns cyan, seats green, hotspots yellow,
|
||||
// walls red (triggers dim), wall spots blue, walkable areas green. The selected item is drawn brighter.
|
||||
// walls red (triggers dim), walkable areas green. The selected item is drawn brighter.
|
||||
void drawWorld(ImDrawList* dl, const float* viewProj) {
|
||||
if (!scene || !showWorld) return;
|
||||
const HomeWorld& w = scene->world;
|
||||
@@ -5778,15 +5744,6 @@ struct Studio {
|
||||
x += (k ? ww : wf) + gap * 1.4f;
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < w.wallSpots.size(); ++i) {
|
||||
if (!worldShown(kWallSpot, static_cast<int>(i))) continue;
|
||||
const WorldWallSpot& ws = w.wallSpots[i];
|
||||
const bool sel = wKind == kWallSpot && wIndex == static_cast<int>(i), hov = hoverKind == kWallSpot && hoverIndex == static_cast<int>(i);
|
||||
float v[4][3];
|
||||
const float hw = ws.width * 0.5f, hh = ws.height * 0.5f;
|
||||
xf(ws.world, -hw, -hh, 0, v[0]); xf(ws.world, hw, -hh, 0, v[1]); xf(ws.world, hw, hh, 0, v[2]); xf(ws.world, -hw, hh, 0, v[3]);
|
||||
for (int k = 0; k < 4; ++k) line(v[k], v[(k + 1) % 4], hov && !sel ? col(0xfff2b0) : col(0x5b8def, sel ? 1.0f : 0.8f), sel || hov ? th * 2 : th);
|
||||
}
|
||||
// Gravity zones: the region, and arrows showing which way is down.
|
||||
for (size_t i = 0; i < w.gravity.size(); ++i) {
|
||||
if (!worldShown(kGravity, static_cast<int>(i))) continue;
|
||||
@@ -5848,8 +5805,8 @@ struct Studio {
|
||||
}
|
||||
|
||||
// The world item under a click: points and shape centres within a few pixels.
|
||||
// The world item under a viewport pixel: a wall or surface box anywhere on it (a placed mesh on its triangles, a
|
||||
// wall spot on its panel), a spawn, seat or hotspot within a finger's width of its marker (seats: the chair drawn),
|
||||
// The world item under a viewport pixel: a wall or surface box anywhere on it (a placed mesh on its triangles),
|
||||
// a spawn, seat or hotspot within a finger's width of its marker (seats: the chair drawn),
|
||||
// a gravity zone at its centre. Markers win over a box they're in front of. hit: the point grabbed (dragging keeps
|
||||
// it under the mouse).
|
||||
bool worldUnder(float mx, float my, const float* viewProj, int& kind, int& index, float hit[3]) {
|
||||
@@ -5910,13 +5867,6 @@ struct Studio {
|
||||
}
|
||||
if (ok && t < bestT) { bestT = t; vk = kCollider; vi = static_cast<int>(i); }
|
||||
}
|
||||
for (size_t i = 0; i < w.wallSpots.size(); ++i) { // the panel's rectangle
|
||||
const WorldWallSpot& ws = w.wallSpots[i];
|
||||
if (!worldShown(kWallSpot, static_cast<int>(i))) continue;
|
||||
const float h[3] = {ws.width * 0.5f, ws.height * 0.5f, 0.05f};
|
||||
float t;
|
||||
if (boxHit(ws.world, h, t) && t < bestT) { bestT = t; vk = kWallSpot; vi = static_cast<int>(i); }
|
||||
}
|
||||
// markers: the nearest on screen within reach, unless a shape is well in front of it
|
||||
float bestPx = 20 * scale, depth = 0;
|
||||
int pk = -1, pi = -1;
|
||||
@@ -6679,8 +6629,6 @@ struct Studio {
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("A 2 x 2 m box: scale it into a floor, a step or a wall");
|
||||
if (ImGui::MenuItem("Invisible Wall", nullptr, false, on)) addInvisibleWall();
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Stops the player, never drawn on the headset. Scale and turn it to fit;\nwhere it stands is no longer walkable.");
|
||||
if (ImGui::MenuItem("Wall Spot", nullptr, false, on)) addWorld(kWallSpot);
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Where SteamVR's panels dock");
|
||||
ImGui::Separator();
|
||||
if (ImGui::BeginMenu("Gravity Zone", on)) {
|
||||
if (ImGui::MenuItem("Direction")) addGravity(WorldGravity::Direction);
|
||||
@@ -7222,7 +7170,6 @@ struct Studio {
|
||||
{"Add Teleport Hotspot", "", on, [this] { addWorld(kHotspot); }},
|
||||
{"Add Surface", "", on, [this] { addWorld(kCollider); }},
|
||||
{"Add Invisible Wall", "", on, [this] { addInvisibleWall(); }},
|
||||
{"Add Wall Spot", "", on, [this] { addWorld(kWallSpot); }},
|
||||
{"Add Gravity Zone", "", on, [this] { addGravity(WorldGravity::Direction); }},
|
||||
{"Import Models", "", on && !uploading, [this] { importModelDialog(); }},
|
||||
{"Add Vista", "", on && homePaths.size() == 1, [this] { addVistaDialog(); }},
|
||||
@@ -8733,20 +8680,19 @@ struct SelfTest {
|
||||
r.spawnAdded ? "added" : "kept");
|
||||
check(r.ok && r.walk.walkArea > 1 && a.walkSaved() && autos == r.objects + r.colliders && r.objects > 0 && r.colliders > 0, "Auto-generate bakes the walkable area and saves it with the home", d);
|
||||
check(a.surfaceMode(a.objects[obj]) == kSurfSolid && !a.autoMade(a.objects[obj]), "...and keeps the surface set by hand");
|
||||
{ // the world items: walls (Wall-role boxes), objects' collision (meshes) and wall spots; no seats or hotspots
|
||||
int walls = 0, objs = 0, seats = 0, spots = 0, hots = 0;
|
||||
{ // the world items: walls (Wall-role boxes) and objects' collision (meshes); no seats or hotspots
|
||||
int walls = 0, objs = 0, seats = 0, hots = 0;
|
||||
for (const WorldCollider& c : a.scene->world.colliders) {
|
||||
walls += c.name == "Auto wall" && c.role == WorldCollider::Wall && c.kind == WorldCollider::Box;
|
||||
objs += (c.name.rfind("Auto surface", 0) == 0 && c.role == WorldCollider::Solid) || (c.name.rfind("Auto collider", 0) == 0 && c.role == WorldCollider::Object);
|
||||
}
|
||||
for (const WorldPoint& p : a.scene->world.seats) seats += p.name == "Auto seat";
|
||||
for (const WorldWallSpot& p : a.scene->world.wallSpots) spots += p.name == "Auto wall spot";
|
||||
for (const WorldPoint& p : a.scene->world.hotspots) hots += p.name == "Auto hotspot";
|
||||
check(walls == 0 && r.walls == 0 && objs == r.objects + r.colliders && r.objects > 0 && spots == 0 && seats == 0 && hots == 0,
|
||||
"...and makes floor meshes surfaces and the other objects colliders (no walls, wall spots, seats or hotspots)",
|
||||
std::to_string(r.objects) + " surfaces, " + std::to_string(r.colliders) + " colliders, " + std::to_string(spots) + " wall spots, " + std::to_string(seats) +
|
||||
check(walls == 0 && r.walls == 0 && objs == r.objects + r.colliders && r.objects > 0 && seats == 0 && hots == 0,
|
||||
"...and makes floor meshes surfaces and the other objects colliders (no walls, seats or hotspots)",
|
||||
std::to_string(r.objects) + " surfaces, " + std::to_string(r.colliders) + " colliders, " + std::to_string(seats) +
|
||||
" seats, " + std::to_string(hots) + " hotspots");
|
||||
genCounts = {walls, objs, spots, seats + hots}; // (objs: surfaces + colliders)
|
||||
genCounts = {walls, objs, seats + hots}; // (objs: surfaces + colliders)
|
||||
collidersAfter = a.scene->world.colliders.size();
|
||||
}
|
||||
firstGrid = a.scene->world.walkGrid;
|
||||
@@ -8798,7 +8744,7 @@ struct SelfTest {
|
||||
for (const WorldPoint& p : a.scene->world.spawns) autoSpawns += p.name == "Auto spawn";
|
||||
const auto& r = a.autoReport;
|
||||
check(r.ok && a.scene->world.walkGrid == firstGrid && autoSpawns <= 1 && a.scene->world.colliders.size() == collidersAfter &&
|
||||
std::array<int, 4>{r.walls, r.objects + r.colliders, r.spots, r.seats + r.hotspots} == genCounts,
|
||||
std::array<int, 3>{r.walls, r.objects + r.colliders, r.seats + r.hotspots} == genCounts,
|
||||
"re-running gives the same area and items (no duplicate spawns, surfaces, walls or objects)", std::to_string(a.scene->world.spawns.size()) + " spawns");
|
||||
if (a.autoUndoAt.x >= 0) click(a.autoUndoAt); // the summary's Undo: just this run
|
||||
return 4;
|
||||
@@ -9229,7 +9175,7 @@ struct SelfTest {
|
||||
float invisibleWallAt[3] = {}; // the invisible-wall test's spot (on the spawn's floor)
|
||||
size_t libraryBefore = 0; // My Environments' count before the bundle is unpacked into it
|
||||
std::string titleWas, imageWas;
|
||||
std::array<int, 4> genCounts{}; // Auto-generate's walls, objects, wall spots, seats + hotspots (first run)
|
||||
std::array<int, 3> genCounts{}; // Auto-generate's walls, objects, seats + hotspots (first run)
|
||||
size_t collidersAfter = 0;
|
||||
size_t linkedBefore = 0; // mesh-linked surfaces before Make Walkable Floors (the home links its own) // the background-sound test's files (removed when it ends)
|
||||
|
||||
|
||||
Vendored
+3
-27
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
// A home's world layer: where the player stands and can go — the parts of Meta's environment system that are not
|
||||
// drawn. Converted from Meta's components (SpawnPoint, AvatarSitting, LocomotionHotspot, Collider Box / Sphere /
|
||||
// Capsule / Mesh / ConvexMesh, WallPlacement) into one plain format that a .fenv stores as world.json, Frame Studio
|
||||
// Capsule / Mesh / ConvexMesh) into one plain format that a .fenv stores as world.json, Frame Studio
|
||||
// edits and Frame Home uses for the floor, the start position, walking, teleporting and sitting.
|
||||
// Everything is in world space, metres, +Y up.
|
||||
#include "core/tinyjson.h"
|
||||
@@ -52,13 +52,6 @@ struct WorldCollider {
|
||||
bool trigger() const { return role == Trigger; }
|
||||
};
|
||||
|
||||
struct WorldWallSpot { // where a panel or window can hang (WallPlacementComponent)
|
||||
std::string name;
|
||||
float world[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};
|
||||
float width = 1, height = 1;
|
||||
bool hidden = false; // Frame Studio's view only
|
||||
};
|
||||
|
||||
// Which way is down. Without zones it's -Y everywhere (Meta's homes); a zone changes it inside its region:
|
||||
// Direction: down is the zone frame's -Y — a room where a wall is the floor, a ceiling walk
|
||||
// Planet: down points at the zone's centre, within `radius` of it — walk all the way round a sphere
|
||||
@@ -84,13 +77,12 @@ struct HomeWorld {
|
||||
std::vector<WorldCollider> colliders;
|
||||
std::vector<float> walkPositions; // legacy walkable triangles (v1 files): read as one walkable-floor area
|
||||
std::vector<u32> walkIndices;
|
||||
std::vector<WorldWallSpot> wallSpots;
|
||||
std::vector<WorldGravity> gravity; // empty: down is -Y everywhere
|
||||
float fallLimit = 30; // falling further than this (metres) puts you back at the spawn
|
||||
std::string origin; // "meta" (converted), "studio" (edited) or "" (none)
|
||||
std::string walkGrid; // the walkable area Frame Studio baked (walkgrid.h encoding); "": Frame Home bakes one at load
|
||||
std::shared_ptr<const WalkGrid> walkGridCache; // walkGrid decoded, or one just built (never saved)
|
||||
bool empty() const { return spawns.empty() && seats.empty() && hotspots.empty() && colliders.empty() && walkIndices.empty() && wallSpots.empty() && gravity.empty() && walkGrid.empty(); }
|
||||
bool empty() const { return spawns.empty() && seats.empty() && hotspots.empty() && colliders.empty() && walkIndices.empty() && gravity.empty() && walkGrid.empty(); }
|
||||
};
|
||||
|
||||
namespace world_detail {
|
||||
@@ -167,15 +159,7 @@ inline std::string worldToJson(const HomeWorld& w) {
|
||||
o += "],\n\"walkable\":{\"positions\":"; arr(o, w.walkPositions.data(), w.walkPositions.size());
|
||||
o += ",\"indices\":[";
|
||||
for (size_t k = 0; k < w.walkIndices.size(); ++k) { if (k) o += ','; o += std::to_string(w.walkIndices[k]); }
|
||||
o += "]},\n\"wallSpots\":[";
|
||||
for (size_t i = 0; i < w.wallSpots.size(); ++i) {
|
||||
const WorldWallSpot& s = w.wallSpots[i];
|
||||
if (i) o += ',';
|
||||
o += "{\"name\":" + q(s.name) + ",\"world\":"; arr(o, s.world, 16); o += ",\"width\":"; num(o, s.width); o += ",\"height\":"; num(o, s.height);
|
||||
if (s.hidden) o += ",\"hidden\":true";
|
||||
o += '}';
|
||||
}
|
||||
o += "],\n\"gravity\":[";
|
||||
o += "]},\n\"gravity\":[";
|
||||
for (size_t i = 0; i < w.gravity.size(); ++i) {
|
||||
const WorldGravity& g = w.gravity[i];
|
||||
if (i) o += ',';
|
||||
@@ -219,14 +203,6 @@ inline bool worldFromJson(const std::string& json, HomeWorld& w) {
|
||||
w.walkPositions.resize(wk["positions"].size());
|
||||
readF(wk["positions"], w.walkPositions.data(), w.walkPositions.size());
|
||||
for (size_t k = 0; k < wk["indices"].size(); ++k) w.walkIndices.push_back(static_cast<u32>(wk["indices"][k].asFloat()));
|
||||
for (size_t i = 0; i < d["wallSpots"].size(); ++i) {
|
||||
const auto& v = d["wallSpots"][i];
|
||||
WorldWallSpot s;
|
||||
s.name = v["name"].asString(); readF(v["world"], s.world, 16);
|
||||
s.width = static_cast<float>(v["width"].asFloat()); s.height = static_cast<float>(v["height"].asFloat());
|
||||
s.hidden = v["hidden"].boolVal;
|
||||
w.wallSpots.push_back(s);
|
||||
}
|
||||
for (size_t i = 0; i < d["gravity"].size(); ++i) {
|
||||
const auto& v = d["gravity"][i];
|
||||
WorldGravity g;
|
||||
|
||||
+3
-8
@@ -224,7 +224,7 @@ public:
|
||||
// tag "local" = the player start (cyan), "remote" = other spawns (yellow).
|
||||
struct SpawnPt { std::string name, tag; float pos[3]={0,0,0}, euler[3]={0,0,0}; int meshIdx=-1; };
|
||||
std::vector<SpawnPt> spawnPts;
|
||||
HomeWorld world; // spawns, seats, hotspots, colliders, wall spots (world space), from every HSTF
|
||||
HomeWorld world; // spawns, seats, hotspots, colliders (world space), from every HSTF
|
||||
|
||||
// Whether an HSTF entity draws anything (a MeshPlatformComponent): its colliders are then that thing's.
|
||||
static bool entityDrawn(const tinyjson::Value& ent) {
|
||||
@@ -614,11 +614,6 @@ public:
|
||||
world.seats.push_back(p);
|
||||
} else if (cls.find("LocomotionHotspotComponent") != std::string::npos) {
|
||||
WorldPoint p; point(p); world.hotspots.push_back(p);
|
||||
} else if (cls.find("WallPlacementComponent") != std::string::npos) {
|
||||
WorldWallSpot ws; ws.name = name; std::memcpy(ws.world, wm, 64);
|
||||
ws.width = d.has("propMaxWidth") ? static_cast<float>(d["propMaxWidth"].asFloat()) : 1.0f;
|
||||
ws.height = d.has("propMaxHeight") ? static_cast<float>(d["propMaxHeight"].asFloat()) : 1.0f;
|
||||
world.wallSpots.push_back(ws);
|
||||
} else if (cls.find("Collider") != std::string::npos && cls.find("PlatformComponent") != std::string::npos) {
|
||||
WorldCollider col; col.name = name; std::memcpy(col.world, wm, 64); col.role = trigger || hotspot || seat ? WorldCollider::Trigger : WorldCollider::Solid;
|
||||
if (cls.find("ColliderBox") != std::string::npos) {
|
||||
@@ -813,8 +808,8 @@ public:
|
||||
float identMat[16] = {1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1};
|
||||
loadHstfRec(*spacePath, 6, Transform{}, identMat, HstfDeltas{}, std::string());
|
||||
world.origin = world.empty() ? "" : "meta";
|
||||
log(" World: %zu spawns, %zu seats, %zu hotspots, %zu colliders, %zu wall spots", world.spawns.size(), world.seats.size(),
|
||||
world.hotspots.size(), world.colliders.size(), world.wallSpots.size());
|
||||
log(" World: %zu spawns, %zu seats, %zu hotspots, %zu colliders", world.spawns.size(), world.seats.size(),
|
||||
world.hotspots.size(), world.colliders.size());
|
||||
}
|
||||
// DEDUP empty-transform template duplicates. A mesh defined in a level .usda with NO transform lands
|
||||
// at the ORIGIN (it's the PROTOTYPE); a sibling entity of the SAME NAME placed by an .hstf carries the
|
||||
|
||||
@@ -164,8 +164,8 @@ int main(int argc, char** argv) {
|
||||
std::printf(" lost cells: %d\n", lost);
|
||||
std::printf(" re-baked with the walls and objects: %.1f m2 (was %.1f)\n", r2.walkArea, r.walkArea);
|
||||
}
|
||||
std::printf(" generated in %.1f ms: %zu walls, %zu objects, %zu seats, %zu wall spots, %zu hotspots\n", std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - t0).count(),
|
||||
gr.walls.size(), gr.objects.size(), gr.seats.size(), gr.spots.size(), gr.hotspots.size());
|
||||
std::printf(" generated in %.1f ms: %zu walls, %zu objects, %zu seats, %zu hotspots\n", std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - t0).count(),
|
||||
gr.walls.size(), gr.objects.size(), gr.seats.size(), gr.hotspots.size());
|
||||
auto line = [&](float x0, float z0, float x1, float z1, int R, int G, int B) {
|
||||
const int n = static_cast<int>(std::hypot(x1 - x0, z1 - z0) / g->p.cell * px) + 1;
|
||||
for (int i = 0; i <= n; ++i) {
|
||||
@@ -192,12 +192,6 @@ int main(int argc, char** argv) {
|
||||
const float xa = v[0], xb = v[3], za = v[2], zb = v[14];
|
||||
line(xa, za, xb, za, 40, 160, 255); line(xb, za, xb, zb, 40, 160, 255); line(xb, zb, xa, zb, 40, 160, 255); line(xa, zb, xa, za, 40, 160, 255);
|
||||
}
|
||||
for (const WorldWallSpot& w : gr.spots) {
|
||||
const float hx = w.width * 0.5f, ux = w.world[0], uz = w.world[2];
|
||||
line(w.world[12] - ux * hx, w.world[14] - uz * hx, w.world[12] + ux * hx, w.world[14] + uz * hx, 255, 230, 0);
|
||||
line(w.world[12], w.world[14], w.world[12] + w.world[8] * 0.6f, w.world[14] + w.world[10] * 0.6f, 255, 230, 0);
|
||||
std::printf(" wall spot %.1f x %.1f m at (%.2f %.2f %.2f)\n", w.width, w.height, w.world[12], w.world[13], w.world[14]);
|
||||
}
|
||||
for (const WorldPoint& p : gr.seats) {
|
||||
const float fx = -std::sin(p.yaw), fz = -std::cos(p.yaw);
|
||||
line(p.pos[0] - 0.15f, p.pos[2], p.pos[0] + 0.15f, p.pos[2], 255, 0, 255);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Renders a home headlessly (lavapipe: VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.json) with its walkable area
|
||||
// and World > Auto-generate's items drawn over it as Frame Studio's guide pass does:
|
||||
// walkable area green, walls red boxes, seats magenta posts + facing bar, wall spots yellow panels, hotspots cyan posts
|
||||
// walkable area green, walls red boxes, seats magenta posts + facing bar, hotspots cyan posts
|
||||
// worldrender <apk[+vista]> <out-prefix> [size] [x,y,z,yaw,pitch ...] (no views: four from above the area's corners)
|
||||
#include "hsl_renderer.h"
|
||||
#include "hsl_scene.h"
|
||||
@@ -49,7 +49,7 @@ int main(int argc, char** argv) {
|
||||
w.walkGridCache = bakeWalkGrid(scene, w, WalkParams{});
|
||||
const WalkGrid& g = *w.walkGridCache;
|
||||
const GenResult gen = generateWorldItems(g, w, drawnWalkSources(scene, w, nullptr, nullptr, false));
|
||||
std::printf("%zu walls, %zu seats, %zu wall spots, %zu hotspots\n", gen.walls.size(), gen.seats.size(), gen.spots.size(), gen.hotspots.size());
|
||||
std::printf("%zu walls, %zu seats, %zu hotspots\n", gen.walls.size(), gen.seats.size(), gen.hotspots.size());
|
||||
|
||||
std::vector<GuideVertex> v;
|
||||
auto vert = [&](const float* p, const float* c) { GuideVertex q; std::memcpy(q.pos, p, 12); std::memcpy(q.rgba, c, 16); v.push_back(q); };
|
||||
@@ -63,7 +63,7 @@ int main(int argc, char** argv) {
|
||||
static const int f[12][3] = {{0,1,3},{0,3,2},{4,6,7},{4,7,5},{0,4,5},{0,5,1},{2,3,7},{2,7,6},{0,2,6},{0,6,4},{1,5,7},{1,7,3}};
|
||||
for (auto& t : f) tri(p[t[0]], p[t[1]], p[t[2]], col);
|
||||
};
|
||||
const float green[4] = {0.1f, 0.85f, 0.3f, 0.35f}, red[4] = {1, 0.15f, 0.1f, 0.55f}, mag[4] = {1, 0, 1, 0.9f}, yel[4] = {1, 0.9f, 0, 0.6f}, cyan[4] = {0, 0.9f, 1, 0.9f};
|
||||
const float green[4] = {0.1f, 0.85f, 0.3f, 0.35f}, red[4] = {1, 0.15f, 0.1f, 0.55f}, mag[4] = {1, 0, 1, 0.9f}, cyan[4] = {0, 0.9f, 1, 0.9f};
|
||||
g.outline([&](const float* a, const float* b, const float* c, bool) {
|
||||
const float A[3] = {a[0], a[1] + 0.03f, a[2]}, B[3] = {b[0], b[1] + 0.03f, b[2]}, C[3] = {c[0], c[1] + 0.03f, c[2]};
|
||||
tri(A, B, C, green);
|
||||
@@ -76,7 +76,6 @@ int main(int argc, char** argv) {
|
||||
const float m2[16] = {fz,0,-fx,0, 0,1,0,0, fx,0,fz,0, p.pos[0] + fx * 0.2f, p.pos[1] + 0.02f, p.pos[2] + fz * 0.2f, 1}, h2[3] = {0.03f, 0.02f, 0.2f};
|
||||
box(m2, h2, mag);
|
||||
}
|
||||
for (const WorldWallSpot& s : gen.spots) { const float h[3] = {s.width * 0.5f, s.height * 0.5f, 0.02f}; box(s.world, h, yel); }
|
||||
for (const WorldPoint& p : gen.hotspots) {
|
||||
const float m[16] = {1,0,0,0, 0,1,0,0, 0,0,1,0, p.pos[0], p.pos[1] + 0.75f, p.pos[2], 1}, h[3] = {0.05f, 0.75f, 0.05f};
|
||||
box(m, h, cyan);
|
||||
@@ -123,11 +122,6 @@ int main(int argc, char** argv) {
|
||||
const float fx = -std::sin(p.yaw), fz = -std::cos(p.yaw);
|
||||
cams.push_back({p.pos[0] + fx * 1.6f, p.pos[1] + 1.1f, p.pos[2] + fz * 1.6f, p.yaw * 57.2958f + 180, -28});
|
||||
}
|
||||
if (mode == "spots") // each wall spot from 2.5 m back
|
||||
for (const WorldWallSpot& w : gen.spots) {
|
||||
const float fx = w.world[8], fz = w.world[10];
|
||||
cams.push_back({w.world[12] + fx * 2.5f, w.world[13], w.world[14] + fz * 2.5f, std::atan2(fx, fz) * 57.2958f, 0});
|
||||
}
|
||||
if (cams.empty() && mode.empty()) { // an overview from high over the area, then eye-level views from spots across it, both ways
|
||||
float lo[2] = {1e9f, 1e9f}, hi[2] = {-1e9f, -1e9f}, y = 0; int n = 0;
|
||||
for (int z = 0; z < g.nz; ++z) for (int x = 0; x < g.nx; ++x) for (const WalkSpan* s = g.begin(x, z); s != g.end(x, z); ++s)
|
||||
|
||||
Reference in new issue
Block a user