Importing
Importing brings outside content into a Space: Roblox places and models, Gaussian splat captures, glTF models, images, video, tables of data and heightmaps. Each importer turns a file into ordinary instances that appear in the Explorer and save with the Space.
01Overview
Ways In
An import copies a file into your Universe or Space and writes the instances that use it. Most files come in through one button: Import in the Home tab's File group. It opens a file picker listing every type it accepts and sends each file to the right importer by its extension.
| Way in | Takes |
|---|---|
| Home tab, File group: Import | Roblox places and models, Gaussian splats, images, videos |
| File menu: Import Roblox Place... | Roblox places and models only |
| Data tab: Import | CSV, JSON Lines and Parquet tables |
| Terrain menu, Assets: Import Heightmap... | Heightmap images and elevation grids |
| Assets panel: Import | Copies files into the Universe's .eustress/assets/meshes/ folder, without adding anything to the Space |
Copying a file into the Space's Workspace folder | glTF models (.glb, .gltf) |
Studio has no handler yet for files dropped onto its window from a file manager, so use the Import buttons or copy the file into the Space folder.
Roblox Places
Whole places into a new Space, with meshes, unions and terrain.
Gaussian Splats
Photoreal captures that render beside parts and collide.
Images and Video
A radial chooser picks the class a picture or clip becomes.
Models and Data
glTF files, datasets and heightmaps.
Supported Formats
| Files | Becomes in the Explorer | Stored at |
|---|---|---|
.rbxl, .rbxlx, .rbxm, .rbxmx | A new Space holding the place's instances | <Universe>/Spaces/<file name>/ |
.ply (3D Gaussian splats) | A GaussianSplats instance in Workspace | <Universe>/assets/splats/ |
.png, .jpg, .jpeg, .webp, .bmp, .gif, .tga | A Decal, Texture, ImageLabel or ImageButton, your choice | <Universe>/assets/images/ |
.mp4, .webm, .mov, .mkv | A VideoFrame or a 3D Video quad, your choice | <Universe>/assets/videos/ |
.glb, .gltf | A Part that renders the file's scene | Where you copy it in the Space |
.csv, .json, .jsonl, .parquet | A Dataset | A folder beside the Dataset's instance file |
.png, .r16, .raw, .hgt, .asc, .tif, .tiff (heightmaps) | Terrain | Workspace/Terrain/ |
.stl, .step, .obj, mesh .ply, .fbx, USD | Nothing yet (see Other Formats) | Not imported |
Copied files keep their names, with unsafe characters replaced and a suffix added when the name is already taken, so importing the same file twice never overwrites the first copy.
02Roblox Places
Running an Import
The Roblox importer reads all four Roblox file formats: binary places (.rbxl), XML places (.rbxlx) and the binary and XML
model files (.rbxm, .rbxmx). It checks the file's
own header rather than trusting the extension.
- Open any Space in the Universe that should receive the import.
- Press Import on the Home tab, or choose Import Roblox Place... from the File menu, and pick the file.
- Studio creates a new Space named after the file under
<Universe>/Spaces/, adding 2, 3 and so on when the name is taken, and removes the starter parts a new Space normally gets. - Every Roblox instance is written as a folder with its own
_instance.toml, downloading meshes, images and sounds as it goes. - A notification reports how many entities were imported and how many warnings were raised, then Studio opens the new Space.
The import runs as one step before Studio responds again, so a large place with many
assets to download takes a while. The full report (class counts, unmapped classes and
properties, asset warnings, approximations, skipped services, unresolved references
and renamed items) goes to the engine log in .eustress_engine/logs
under your home folder. A model file lands in a Space of its own in the same way.
The engine also builds a convert-to-eustress utility. It
copies the TOML folders of existing Spaces into their databases, the same step
Studio runs when it opens a Space. Roblox files always come in through Import;
the Universes page covers the database.
Classes and Services
Each Roblox service lands in the Space folder of the same name: Workspace, Lighting,
Players, StarterGui, StarterPack, ReplicatedStorage, ServerScriptService,
ServerStorage, SoundService, Chat, Teams and MaterialService. StarterPlayer's two
script folders become StarterPlayerScripts and StarterCharacterScripts, and
ReplicatedFirst goes to ReplicatedStorage/_replicated_first.
Runtime-only services such as RunService and TweenService have nothing saved in a
place file and are skipped. Services with no Eustress counterpart, such as
MarketplaceService, are kept under _imported/<Service>/ for you to
sort out.
| Roblox | Eustress |
|---|---|
| Part, MeshPart, WedgePart, CornerWedgePart, TrussPart | Part |
| UnionOperation | UnionOperation, drawn from the union's stored mesh |
| NegateOperation, IntersectOperation | Part, drawn the same way |
| Model, Folder, SpawnLocation, Seat, VehicleSeat, Camera | The same classes |
| Script, LocalScript, ModuleScript | LuauScript, LuauLocalScript, LuauModuleScript |
| Lights, Sky, Atmosphere, Clouds, Terrain | The same classes |
| Constraints and movers (WeldConstraint, HingeConstraint, Motor6D, AlignPosition, LinearVelocity and more) | The same classes |
| GUI (ScreenGui, BillboardGui, SurfaceGui, Frame, TextLabel, ImageLabel and more) | The same classes |
| Effects (ParticleEmitter, Beam, Sound, Fire, Smoke, Trail, post-processing effects) | The same classes |
| RemoteEvent, RemoteFunction, BindableEvent, BindableFunction | The same classes |
| SpecialMesh, BlockMesh, CylinderMesh | Folded into the parent part's mesh |
| The ten Value classes | Attributes on the parent |
| Classes with no counterpart | Skipped with their children, listed in the report |
Instance names are kept for display. When a Roblox name cannot be a folder name
(it contains :, ? or another reserved character,
ends in a dot, is longer than 96 characters, or is a Windows device name such as CON), only the folder name changes. References between instances,
such as a weld's two parts or an ObjectValue's target, are resolved to the target's
UUID; the ones that cannot be resolved are listed in the report.
Parts, Shapes and Units
- CFrame becomes position and rotation.
Orientationis used only when a part has no CFrame. - Shape picks the built-in mesh: Ball, Cylinder, Wedge and CornerWedge get their own; Block keeps the default. Cylinders are turned 90 degrees in their own frame, because Roblox runs a cylinder along X and Eustress's cylinder mesh runs along Y.
- Color and BrickColor become the part color. A BrickColor's palette number and the original color are also kept in the part's metadata.
- Transparency becomes the color's alpha; Material maps to the preset of the same name; Anchored, CanCollide, Reflectance, CastShadow and Locked carry over.
- Other properties are kept in
[properties.extras]so nothing is thrown away, even when Eustress does not use them yet.
Roblox lengths are written unchanged, and each instance's [metadata] sets unit to ft. Eustress reads one Roblox unit as
one foot and converts to meters when the Space loads, so a part 4 units long is 1.22 m.
Only a Part whose Shape is Wedge or CornerWedge gets a wedge mesh. The older WedgePart and CornerWedgePart classes, and TrussPart, have no Shape property, so they arrive as block-shaped Parts.
Scripts, Values and Joints
A script's source is written to script.luau beside its _instance.toml. The ten Value classes (NumberValue, IntValue,
BoolValue, StringValue, ObjectValue, Color3Value, Vector3Value, CFrameValue,
BrickColorValue, BinaryStringValue) become typed attributes on their parent, with a _2 suffix when two share a name. RayValue, IntConstrainedValue and
DoubleConstrainedValue are dropped and recorded in the report. Scripts are rewritten
to match:
-- Before import -- After import
local speed = car.Speed.Value local speed = car:GetAttribute("Speed")
car.Speed.Value = 40 car:SetAttribute("Speed", 40)
car.Speed.Changed:Connect(onChange) car:GetAttributeChangedSignal("Speed"):Connect(onChange)A pattern the rewrite cannot handle safely, such as a Value object stored in a local variable or created at run time, is left as it was and listed as a script warning. The Scripting page covers the Luau runtime.
Legacy surface joints hold assemblies together, so they are mapped rather than dropped: ManualWeld, Snap and Glue become Weld; Rotate becomes HingeConstraint; RotateP becomes Motor; RotateV becomes VelocityMotor.
03Meshes, Unions, Terrain
Asset Downloads
Roblox parts refer to meshes, images and sounds by asset id. During a Studio import
each id is fetched from assetdelivery.roblox.com and saved in the new
Space's assets folder:
- Meshes: Roblox
.meshfiles, versions 1.00 to 7.00, are decoded (the full-detail level only) toassets/meshes/rbx-<id>.glb, and the part's[asset] meshpoints at it. A version 6 or 7 mesh whose geometry is Draco-compressed keeps a placeholder, and the report names it. - Images: PNG, JPEG, WebP, GIF, BMP and DDS go to
assets/textures/rbx-<id>.<ext>. - Sounds: OGG, WAV and MP3 go to
assets/sounds/. - Anything else, such as a packaged model, keeps a placeholder path under
assets/_unresolved/and a warning in the report.
Downloads are cached in <Universe>/assets/.rbx_cache/, so a second
import of the same place fetches nothing it already has. An id that failed is marked
there too and skipped next time. Five environment variables, read when the import
starts, change where assets come from:
| Variable | Effect |
|---|---|
EUSTRESS_ROBLOX_ASSET_DIR | A local folder of assets named by id, tried before the network |
EUSTRESS_ROBLOX_NO_NETWORK=1 | No downloads; assets come only from the local folder, or keep placeholders |
EUSTRESS_ROBLOX_API_KEY | An Open Cloud API key with the legacy-asset:manage scope, for assets that need a signed-in account. Used instead of the cookie when both are set |
EUSTRESS_ROBLOSECURITY | A Roblox session cookie sent with each request, for assets that need a signed-in account |
EUSTRESS_ROBLOX_RETRY_ERRORS=1 | Retry ids the cache has marked as failed |
Without a credential, many assets are refused with HTTP 401. After 32 refusals in a row the importer stops asking for the rest of that import and keeps placeholders. With a credential set it allows 256, because one refused asset usually means that asset is private to another creator rather than that the credential is wrong. Refusals are never cached as failures, so the same place imports its assets once a credential is set. Rate limits (HTTP 429) and server errors are tried up to five times, waiting as long as Roblox asks, up to 30 seconds.
Whoever holds a session cookie can act as that account. Prefer an API key: it carries only the scope you give it and can be revoked on its own. Set either variable only in the terminal that launches Studio, and remove it when the import is done.
A part's [asset] section has a mesh slot but no texture slot.
A SpecialMesh texture is dropped and recorded in the report, and a MeshPart's
texture is downloaded but not used, so textured meshes arrive in their part
color.
Folded SpecialMesh, BlockMesh and CylinderMesh children set the parent's mesh: Brick is a block, Cylinder a cylinder, Sphere a ball, Wedge a wedge, FileMesh the downloaded mesh. Head becomes a ball and the remaining types a block, each recorded as an approximation. Their Scale and Offset change only how the part is drawn, never its collider; a negative Scale (a mirrored mesh) draws mirrored but gets no collider.
Unions
A Roblox union (UnionOperation, NegateOperation or IntersectOperation) stores the
finished result of its boolean operation as a triangle mesh inside the place file.
The importer reads that stored mesh, preferring the modern MeshData2 field over the legacy MeshData, decodes it (format versions 2, 4
and 5) and writes it as csg.glb in the union's folder. The union's [properties.extras] records csg_op (union, negate or
intersect), plus csg_triangles when the mesh was found.
When a union's stored mesh is missing, empty, or only an unbaked marker, Eustress does not rebuild the union from its parts. The union imports as a box the size of its bounds, and the report lists a CSG box fallback. Because the box fills the union's whole bounds, it can cover parts that sat inside them.
Terrain
Roblox terrain is a voxel grid. The importer decodes it into 32 x 32 x 32 chunks and
writes each one, compressed, to Workspace/Terrain/voxel_chunks/chunk_<cx>_<cy>_<cz>.bin, with the
terrain's material colors in Workspace/Terrain/_instance.toml.
Solid material and fill are kept for every voxel; water is read but not stored yet.
When the Space opens, the chunks are copied into its database.
The voxel terrain loader runs only for Spaces whose database is marked as fully converted, a mark the current engine does not set, and it draws only the top surface of each column. Imported voxel terrain is saved with the Space but does not appear in the viewport yet. See Building for the terrain you can sculpt today.
043D Models
glTF and GLB
glTF is the format Eustress renders natively. Copy a .glb or .gltf file into the Space's Workspace (or Lighting) folder while the Space is open in Studio. The file watcher
picks it up and adds a Part named after the file that renders the file's first scene,
materials included. Saving over the file reloads it in place. Put the file directly in
Workspace or in a plain folder: a file inside a part's folder is treated as that part's
own asset and is not added.
Eustress reads glTF through Bevy's loader. It cannot decode Draco-compressed meshes, so Draco files do not load and the log names them; export without Draco compression. Its image decoder in this build reads PNG, HDR and KTX2, so embed textures as PNG.
Custom Part Meshes
Any part can wear a GLB mesh instead of a primitive. Point its [asset] section at the file, relative to the part's own folder. The mesh must live inside the
Space folder, because meshes load through the Space's asset source. This is how the
Roblox importer attaches downloaded meshes.
[asset]
mesh = "../../assets/meshes/chair.glb" # relative to Workspace/Chair/
scene = "Scene0"- What renders: the first primitive of the file's first mesh, with the file's first material.
- Size: computed from the mesh bounds once it loads, times the part's scale.
- Collider: a box of the part's size when CanCollide is on.
- Draco: a Draco-compressed mesh falls back to the block primitive.
Eustress recognizes its built-in primitives by file name. A mesh whose file name
contains block, ball, cylinder, wedge or cone is treated as that primitive and
your file is not loaded: cone_tower.glb renders as the built-in
cone. Rename the file to avoid those words.
Other Formats
Studio watches the open Space for .stl, .step, .stp, .obj, .ply and .fbx files, meant to be converted to GLB beside the source. The
converters are not written yet, so each file found logs a failed conversion and
nothing is added. Convert these formats to GLB in another tool first.
- USD: the USD modules are empty placeholders, and no USD file is read.
- The Model tab's Import and Export buttons are not wired yet; pressing them prints a planned-feature notice in the Output.
- The Assets panel's Import copies the chosen files into
<Universe>/.eustress/assets/meshes/and refreshes the panel. It creates no instances.
Parametric parts made in Eustress export to GLB from the CAD tools.
05Gaussian Splats
Importing a Splat
A Gaussian splat is a photographic 3D capture stored as millions of small colored
blobs. Eustress renders standard 3D Gaussian Splatting .ply files
beside ordinary parts. Press Import on the Home tab and pick the .ply:
- The file is copied to
<Universe>/assets/splats/. - A
GaussianSplatsinstance named after the file is created in Workspace, 2 m above the origin. - Its
[gaussian_splats]section records the file's path, relative to the Universe folder.
[transform]
position = [0.0, 2.0, 0.0]
rotation = [1.0, 0.0, 0.0, 0.0] # 180 degrees about X
[gaussian_splats]
path = "assets/splats/bicycle.ply"
cull_floaters = true # both default to true when absent
ppisp = trueThe rotation stands a typical capture upright: captures trained from photographs usually store Y pointing down, and a half turn about X makes them Y-up like the rest of Eustress. A capture made another way may need a turn with the Rotate tool; the cloud file itself is never modified.
Imported splats and images live in the Universe's assets
folder, above the Space folder, so the engine loads them by absolute path. Bevy
refuses absolute asset paths by default, which made imports silently show
nothing. Eustress allows them, because it only loads local files you picked.
What Happens on Load
- Floater removal: before the cloud reaches the GPU, blobs that float alone in empty space are removed, judged by how much solid material surrounds them rather than by their opacity, so faint but dense detail such as foliage stays. Near-invisible blobs below 0.005 opacity go too. The environment variables
EUSTRESS_SPLAT_CULL_GRID,EUSTRESS_SPLAT_CULL_MIN_MASS,EUSTRESS_SPLAT_CULL_DUST,EUSTRESS_SPLAT_CULL_MAX_REMOVEandEUSTRESS_SPLAT_CULL_VOXELtune it. - Collider: the solid part of the cloud is fitted with boxes about 1/48 of its largest extent, between 5 cm and 2 m each, and becomes a static collider, so the capture is solid to physics.
- Properties: Appearance shows Path (read-only), CullFloaters and PPISP. The toggles are saved to the
[gaussian_splats]section. - Reopening: when a Space opens, Eustress scans Workspace for splat instances and loads their clouds again.
PPISP is a photometric correction for multi-camera captures. Its crate implements only
the exposure step so far, as a CPU reference, and the ppisp flag is
stored but does not change the cloud yet.
Over MCP
Agents insert a working splat with the insert_gaussian_splats tool of
the MCP server. It takes a .ply path
(absolute, or relative to the Universe), copies it into assets/splats/ unless it is already there, and writes the same instance the Import button writes.
Creating a GaussianSplats with create_entity gives an
empty instance, because that tool has no cloud path.
{ "tool": "insert_gaussian_splats", "arguments": {
"path": "C:/Captures/bicycle.ply",
"name": "Bicycle",
"position": [0, 2, 0],
"cull_floaters": true } }06Images and Video
The Radial Chooser
A picture or clip can become several different classes, so Eustress asks. Import an
image or video with the Home tab's Import button, or double-click
one that is already in the Assets panel. The file is copied to <Universe>/assets/images/ or assets/videos/ and a
radial menu opens in the middle of the window:
| Menu | Choice | Creates |
|---|---|---|
| Apply Image As | Decal | A Decal on the face of a part you click next |
| Apply Image As | Texture | A Texture tiled across the face of a part you click next |
| Apply Image As | Image Label, Image Button | A GUI element in StarterGui, with the image in [image] image |
| Apply Video As | Video Frame | A GUI element in StarterGui, with the clip in [video] video |
| Apply Video As | 3D Video | A video quad in Workspace, 6 x 3.375 m by default |
Click outside the menu or on its center to cancel. The copied file stays in the assets folder either way, so you can apply it later from the Assets panel. The UI Systems page covers the GUI classes.
Decals and Textures
Choosing Decal or Texture starts surface placement. A preview follows the surface under the cursor: green over a part you can use, red over a locked part or anything that is not a part.
- Move over the face that should carry the image.
- Click. The face is worked out from the surface you hit, and a Decal or Texture is added as a child of the part, in the part's folder, with
textureandfaceset. - Press
Escto cancel instead.
The part must be saved to disk first; a part that exists only in memory is refused with a message. A Texture repeats its image across the face and follows the part when it is resized.
What Displays Today
The chooser and placement always create and save the instance. Whether the picture or clip then shows depends on the class:
| Class | Shows today |
|---|---|
| 3D Video | Yes, for MP4 files with H.264 video, decoded in-process and looped. Other codecs play a moving test pattern. |
| Decal, Texture | Not yet. They load the stored path from the engine's own asset folder instead of the Universe folder, so the image is not found. |
| Image Label, Image Button | Not yet. The GUI loader resolves the path against the element's own folder, so the image is not found. |
| Video Frame | Not yet. Video in screen-space GUI needs a compositor that is not written. |
The 3D renderer in this build decodes PNG (plus HDR and KTX2). JPEG, WebP, BMP, GIF and TGA files import and save, but cannot be drawn on 3D surfaces. Convert pictures to PNG before importing them.
07Data and Heightmaps
Datasets
The Data tab's Import reads a table and creates a Dataset
instance. Pick a .csv, .json, .jsonl or .parquet file:
- A folder named after the file is created in the selected folder, or in Workspace when nothing file-backed is selected.
- The file is copied into that folder, beside a Dataset
_instance.toml. - Its attributes record the source file name, the row count and each column's name and type.
- The Output confirms the import with the row and column counts.
Both .json and .jsonl files are read as JSON
Lines: one JSON object per line. Write a JSON array out as one object per line
before importing it. When a file cannot be read, the Output says so and nothing
is created.
Heightmaps
The Terrain menu's Import Heightmap... turns an elevation file
into terrain for the open Space: PNG heightmaps, raw 16-bit .r16 and .raw grids, SRTM .hgt tiles, ASCII .asc grids and GeoTIFF .tif files. The heights are written as terrain
chunks under Workspace/Terrain, the same files Save writes, and the
terrain is rebuilt from them. Building covers terrain
editing.
08What's Next
Roblox Import
The import specification adds an options dialog before the import (choose the target Space, and switch scripts, terrain, union meshes and asset downloads on or off) and a report dialog after it, with one-click follow-ups to download missing assets and to save the report as JSON. Roblox files will also be accepted when dropped on the viewport. Unions without a stored mesh will be rebuilt from their parts with the CAD kernel's booleans instead of arriving as boxes.
More Formats
The mesh watcher's converters are designed and will land one format at a time: STL and PLY meshes written straight to GLB, OBJ through a parser, STEP through the truck STEP reader and the CAD tessellator, and FBX through an external FBX2glTF converter. For splats, the plan adds compact SOG and SPZ formats so large captures load as a fraction of their PLY size, and uses PPISP to separate a capture's lighting from its surfaces, so engine lights can relight it.
Bring in what you already have, then keep building.