Learn/Importing

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.

Time19 min readLevelIntermediateUpdatedUpdated Sep 2026

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 inTakes
Home tab, File group: ImportRoblox places and models, Gaussian splats, images, videos
File menu: Import Roblox Place...Roblox places and models only
Data tab: ImportCSV, JSON Lines and Parquet tables
Terrain menu, Assets: Import Heightmap...Heightmap images and elevation grids
Assets panel: ImportCopies files into the Universe's .eustress/assets/meshes/ folder, without adding anything to the Space
Copying a file into the Space's Workspace folderglTF 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

Roblox Places

Whole places into a new Space, with meshes, unions and terrain.

Splats

Gaussian Splats

Photoreal captures that render beside parts and collide.

Media

Images and Video

A radial chooser picks the class a picture or clip becomes.

Models

Models and Data

glTF files, datasets and heightmaps.

Supported Formats

FilesBecomes in the ExplorerStored at
.rbxl, .rbxlx, .rbxm, .rbxmxA 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, .tgaA Decal, Texture, ImageLabel or ImageButton, your choice<Universe>/assets/images/
.mp4, .webm, .mov, .mkvA VideoFrame or a 3D Video quad, your choice<Universe>/assets/videos/
.glb, .gltfA Part that renders the file's sceneWhere you copy it in the Space
.csv, .json, .jsonl, .parquetA DatasetA folder beside the Dataset's instance file
.png, .r16, .raw, .hgt, .asc, .tif, .tiff (heightmaps)TerrainWorkspace/Terrain/
.stl, .step, .obj, mesh .ply, .fbx, USDNothing 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.

  1. Open any Space in the Universe that should receive the import.
  2. Press Import on the Home tab, or choose Import Roblox Place... from the File menu, and pick the file.
  3. 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.
  4. Every Roblox instance is written as a folder with its own _instance.toml, downloading meshes, images and sounds as it goes.
  5. A notification reports how many entities were imported and how many warnings were raised, then Studio opens the new Space.
.rbxl fileParseNew SpaceWriteone folder eachOpenAsset downloadscached per Universea bad file stops herenamed after the file
Every Roblox import builds a new Space. Parsing happens first, so a file that cannot be read leaves nothing behind, and the Space you had open is never written to.

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.

Info
convert-to-eustress is a storage tool

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.

RobloxEustress
Part, MeshPart, WedgePart, CornerWedgePart, TrussPartPart
UnionOperationUnionOperation, drawn from the union's stored mesh
NegateOperation, IntersectOperationPart, drawn the same way
Model, Folder, SpawnLocation, Seat, VehicleSeat, CameraThe same classes
Script, LocalScript, ModuleScriptLuauScript, LuauLocalScript, LuauModuleScript
Lights, Sky, Atmosphere, Clouds, TerrainThe 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, BindableFunctionThe same classes
SpecialMesh, BlockMesh, CylinderMeshFolded into the parent part's mesh
The ten Value classesAttributes on the parent
Classes with no counterpartSkipped 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. Orientation is 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.

Pitfall
Wedge classes import as blocks

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:

Luau
-- 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 .mesh files, versions 1.00 to 7.00, are decoded (the full-detail level only) to assets/meshes/rbx-<id>.glb, and the part's [asset] mesh points 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:

VariableEffect
EUSTRESS_ROBLOX_ASSET_DIRA local folder of assets named by id, tried before the network
EUSTRESS_ROBLOX_NO_NETWORK=1No downloads; assets come only from the local folder, or keep placeholders
EUSTRESS_ROBLOX_API_KEYAn 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_ROBLOSECURITYA Roblox session cookie sent with each request, for assets that need a signed-in account
EUSTRESS_ROBLOX_RETRY_ERRORS=1Retry 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.

Pitfall
The session cookie is a password

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.

Pitfall
Mesh textures are not applied

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.

Pitfall
Unions without a stored mesh become boxes

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.

Pitfall
Imported terrain is stored, not yet drawn

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.

Workspace/Chair/_instance.toml
[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.
Pitfall
Name custom meshes carefully

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:

  1. The file is copied to <Universe>/assets/splats/.
  2. A GaussianSplats instance named after the file is created in Workspace, 2 m above the origin.
  3. Its [gaussian_splats] section records the file's path, relative to the Universe folder.
Workspace/bicycle/_instance.toml (excerpt)
[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 = true

The 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.

Info
Why absolute paths are allowed

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_REMOVE and EUSTRESS_SPLAT_CULL_VOXEL tune 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.

MCP
{ "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:

MenuChoiceCreates
Apply Image AsDecalA Decal on the face of a part you click next
Apply Image AsTextureA Texture tiled across the face of a part you click next
Apply Image AsImage Label, Image ButtonA GUI element in StarterGui, with the image in [image] image
Apply Video AsVideo FrameA GUI element in StarterGui, with the clip in [video] video
Apply Video As3D VideoA 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.

  1. Move over the face that should carry the image.
  2. 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 texture and face set.
  3. Press Esc to 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:

ClassShows today
3D VideoYes, for MP4 files with H.264 video, decoded in-process and looped. Other codecs play a moving test pattern.
Decal, TextureNot 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 ButtonNot yet. The GUI loader resolves the path against the element's own folder, so the image is not found.
Video FrameNot yet. Video in screen-space GUI needs a compositor that is not written.
Pitfall
Only PNG decodes on 3D surfaces

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:

  1. A folder named after the file is created in the selected folder, or in Workspace when nothing file-backed is selected.
  2. The file is copied into that folder, beside a Dataset _instance.toml.
  3. Its attributes record the source file name, the row count and each column's name and type.
  4. The Output confirms the import with the row and column counts.
Pitfall
JSON means JSON Lines

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.

Loading Eustress Engine...