Preserve your Horizon Worlds creations before VR access ends June 15, 2026.
A community-built set of tools to extract and preserve Meta Horizon Worlds projects as standard 3D formats (glTF, FBX, JSON) that can be opened in Blender, Unity, Godot, or any other 3D tool.
VR access ends June 15, 2026. After that date the Horizon Worlds app is removed from Quest and you can no longer visit worlds, run scripts, or recover any data from the platform. Do not wait.
| File | Purpose |
|---|---|
hw_extractor.py |
Python CLI — packages Desktop Editor project assets and generates a glTF scene file |
hw_scene_exporter.ts |
TypeScript script — paste into a live Horizon World to capture the scene graph |
hw_reconstruct.py |
Blender Python script — merges scene.gltf layout with your original FBX files |
requirements.txt |
Python dependencies (all optional, graceful fallback) |
This is the workflow that has been tested and confirmed working as of March 2026.
- Meta Horizon Worlds Desktop Editor installed on Windows
- Python 3.10+ installed
- The world you want to preserve opened and loaded in the Desktop Editor
pip install pygltflib Pillow rich
These are all optional. The tool works without them but rich gives nicer output
and pygltflib enables proper glTF validation.
The Desktop Editor does not store scene layout data locally — it lives on Meta's servers. You need to run a TypeScript script inside your world to capture it while the platform is still live.
- Open your world in the Desktop Editor
- In the Assets panel, right-click → New Script
- Name it
SceneExporter - Open it in VS Code (it should open automatically)
- Delete everything in the file and paste in the entire contents of
hw_scene_exporter.ts - Save the file
This is the critical step for flat worlds (no nested hierarchy).
The scene exporter captures objects by walking the parent/child hierarchy. If your objects are all sitting flat at the same level with no nesting, the script can only see the one object it is attached to. The fix:
- In the Desktop Editor's scene hierarchy panel, press Ctrl+A to select all objects
- Create a new Empty object (Add → Empty or equivalent in the editor)
- With all your world objects selected, parent them to the empty (drag them onto the empty in the hierarchy panel)
- All your objects are now children of that one empty
If your world already has objects nested inside parents, skip this step — the exporter will find them automatically by walking up to the root.
- Drag the
SceneExporterscript onto the empty you created in Step 3 (attach it to just the one empty, not to every individual object) - Hit the Play button in the Desktop Editor
- Open the console panel (tab at the bottom of the editor)
- Wait a few seconds — you will see log messages starting with
[HWExporter] - Look for:
[HWExporter] === SCENE DUMP START (X chunks) === - Copy everything from the opening
{to the closing}of the JSON output (the stack trace lines after each log message are normal, ignore them) - Paste into a text editor and save the file as
scene_dump.json
A successful export looks like this:
[HWExporter] Collected 10 entities
[HWExporter] Export complete: 10 entities captured
[HWExporter] Copy JSON above, save as scene_dump.json
python hw_extractor.py --scene-json scene_dump.json
This produces a timestamped folder like ./hw_exports/World_20260318_013000/
containing:
| File/Folder | Contents |
|---|---|
scene.gltf |
Full scene with all object positions, rotations, and scales |
scene_manifest.json |
Complete inventory of everything captured |
README.md |
Per-world import instructions |
mesh/ |
Any FBX/glTF assets found in the local project folder |
texture/ |
Any texture files found in the local project folder |
script/ |
Any TypeScript scripts found in the local project folder |
primitive_meshes/ |
OBJ stand-ins for any detected Meta primitive shapes |
python hw_extractor.py --validate ./hw_exports/World_20260318_013000
Checks the export folder and reports what is present and what is missing before you delete anything from your machine.
- Open Blender
File → Import → glTF 2.0 (.glb/.gltf)- Select your
scene.gltf - All captured objects appear as empties at their correct world positions, rotations, and scales
Your scene layout is now preserved in Blender. Object names, positions, rotations, and scales are all correct.
Asset re-integration (linking your original FBX files back to the correct positions) is handled by
hw_reconstruct.py— see the section below once you have confirmed your scene.gltf opens correctly.
Run Steps 3–6 for each world you want to preserve. Open the next world in the Desktop Editor and repeat from Step 3. You do not need to reinstall or reconfigure anything between worlds.
Add --zip to create a single archive per world:
python hw_extractor.py --scene-json scene_dump.json --zip
| Item | Status |
|---|---|
| Scene layout (positions, rotations, scales) | ✅ Fully working |
| Object names | ✅ Captured when names are set in the editor |
| TypeScript world scripts | ✅ Preserved if local project files exist |
| Custom imported FBX files | ✅ Preserved if local project files exist |
| Textures | ✅ Preserved if local project files exist |
| Meta built-in asset library meshes | ❌ Not exportable — server-side, proprietary |
| Meta AI-generated meshes | ❌ Not exportable — server-side |
| Material and shader parameters | ❌ Must recreate manually |
| Baked lighting | ❌ Not exportable |
| Physics settings | ⚠ Check TypeScript scripts for values |
The following was discovered by reverse-engineering the horizon/core TypeScript
API during development. None of this appears to be officially documented anywhere.
Entity handles are lazy wrappers. The Entity type is { getter, _cache }.
Call .get() on it to resolve the actual underlying data object. Every property
on an entity — name, children, parent, transform — is also a lazy handle
that requires .get() to unwrap.
World is a function constructor, not an instance. It exposes only onUpdate
and onPrePhysicsUpdate as own keys. There is no entity registry accessible
through the World object.
children only works with hierarchy. Calling .get() on the children handle
returns an array, but for flat worlds (no nesting) that array is always empty.
The solution is to parent all objects to a single empty before running the exporter.
Confirmed working imports from horizon/core:
Component, Entity, Player, PropTypes, World, Vec3, CodeBlockEvents
Quat is not exported by horizon/core. Use plain objects for quaternion data.
PropTypes syntax: use { type: PropTypes.Boolean, default: true },
not PropTypes.Boolean(true).
This is a community preservation project. Pull requests and issue reports welcome, especially:
- Testing on different world types and editor versions
- Worlds with complex nested hierarchies
- Asset re-integration results with
hw_reconstruct.py - Godot import instructions
- Better name resolution for worlds where names are not capturing
If something does not work, open an issue with your full console output.
MIT — do whatever you want with it. Make it better and preserve those worlds!
Not affiliated with Meta Platforms Inc. Built out of spite, because creators' work deserves to survive corporate pivots.