---
name: roblox-editable-mesh-image
description: Use when creating or changing meshes or images at runtime in Roblox - procedural geometry, deforming meshes, painting, drawing on textures, generating icons or textures - with AssetService:CreateEditableMesh/CreateEditableImage (and the Async versions), Content.fromObject, CreateMeshPartAsync, WritePixelsBuffer or CreateDataModelContentAsync. Covers enabling, permissions, memory budgets, limits and the current (2024-2026) API names.
---

# EditableMesh and EditableImage

`EditableMesh` and `EditableImage` let scripts build and modify geometry and pixels at runtime. They are
**Objects, not Instances**: you don't parent them, they don't replicate, and they show up only when a `Content`
property points at them. Create them with `AssetService`.

The API changed a lot in 2024-2025. Current names: `AssetService:CreateEditableMesh`, `CreateEditableMeshAsync`,
`CreateEditableImage`, `CreateEditableImageAsync`, `Content.fromObject`, `MeshPart.MeshContent`,
`TextureContent`, `ImageLabel.ImageContent`. Beta-era code that treated editables as Instances parented under
parts is outdated: rewrite it with the Content workflow below.

## Enabling it

- Studio and plugins: always works.
- Published games: Creator Dashboard > your game > Configure > Settings > **Enable Mesh / Image APIs**, or in Studio
  Experience Settings > Security > **Allow Mesh / Image APIs**. When Roblox opened these APIs to published games it
  required the owner (the group owner for group games) to be 13+ and ID-verified to turn this on; Roblox has said it
  is working on that requirement, so check the toggle in the dashboard. Without it, creation fails in live servers.

## Loading existing assets (permissions)

`CreateEditableMeshAsync` and `CreateEditableImageAsync` only load assets owned by, or explicitly shared with, the
game owner, the logged-in Studio user, the player (for client-side editables), or a group where one of them can
edit assets. Anything else throws. As of April 2026, sharing an asset with an experience ID did not yet work for
these APIs; share with the owner (user or group) instead. Non-asset images
such as `rbxthumb://` URIs are supported by `CreateEditableImageAsync`.

```luau
local AssetService = game:GetService("AssetService")

local ok, em = pcall(function()
	-- Assets load fixed-size by default (positions and attributes editable, no adding/removing)
	return AssetService:CreateEditableMeshAsync(Content.fromAssetId(1234567890), { FixedSize = false })
end)
```

## Memory budget and limits

- Clients have a strict, device-specific memory budget for editables. When it is exhausted, the create methods
  **return nil**. Always check. The server, Studio and plugins have no budget.
- Call `:Destroy()` on editables you no longer need; it frees memory immediately.
- One editable can feed several `Content` properties (several parts or labels) for the cost of one.
- `EditableMesh`: at most 60,000 vertices and 20,000 triangles (a quad counts as 2). Exceeding it errors.
- `EditableImage`: at most 1024 x 1024, default 512 x 512, can't be resized.
- Only one displayed `EditableImage` updates per frame. Updating three visible images takes three frames.

## Displaying them

```luau
local AssetService = game:GetService("AssetService")

-- Mesh: MeshContent is read-only to scripts, so make a MeshPart from the editable
local em = makeGrid(40, 2) -- see "Building a mesh" below; returns nil if over budget
if not em then
	return
end
local part = AssetService:CreateMeshPartAsync(Content.fromObject(em), {
	CollisionFidelity = Enum.CollisionFidelity.Box,
})
part.Anchored = true
part.Parent = workspace

-- Or copy mesh, texture and collision onto a MeshPart you already have
local existing = workspace:FindFirstChild("Rock") :: MeshPart
existing:ApplyMesh(part)

-- Image: Content properties accept it directly
local img = AssetService:CreateEditableImage({ Size = Vector2.new(256, 256) })
if img then
	local label = script.Parent :: ImageLabel
	label.ImageContent = Content.fromObject(img)
	part.TextureContent = Content.fromObject(img) -- the same image, no extra memory
end

-- PBR: build a SurfaceAppearance from editable maps (maps can't be swapped afterwards).
-- Keys: ColorMap, NormalMap, MetalnessMap, RoughnessMap, EmissiveMask.
if img then
	local appearance = AssetService:CreateSurfaceAppearanceAsync({ ColorMap = Content.fromObject(img) })
	appearance.Parent = part
end
```

Visual edits to an `EditableMesh` show up immediately. Collision does not: call `CreateMeshPartAsync` again and
`ApplyMesh` at the end of an edit (not after every vertex change).

## Building a mesh

IDs are stable integers that can have gaps: iterate `GetVertices()` and `GetFaces()` instead of counting. A face is
3 or 4 vertex IDs; the front side is the one where the vertices go counterclockwise (or set `MeshPart.DoubleSided`).
Use the batch methods for anything bigger than a few faces.

```luau
local function makeGrid(n: number, cell: number): EditableMesh?
	local em = AssetService:CreateEditableMesh()
	if not em then
		return nil
	end
	local positions = table.create((n + 1) * (n + 1))
	for z = 0, n do
		for x = 0, n do
			table.insert(positions, Vector3.new(x * cell, 0, z * cell))
		end
	end
	local v = em:BatchAdd(Enum.MeshAttribute.Vertex, positions)
	local faces = table.create(n * n * 2)
	for z = 0, n - 1 do
		for x = 0, n - 1 do
			local a = v[z * (n + 1) + x + 1]
			local b = v[z * (n + 1) + x + 2]
			local c = v[(z + 1) * (n + 1) + x + 1]
			local d = v[(z + 1) * (n + 1) + x + 2]
			table.insert(faces, { a, c, b }) -- counterclockwise seen from above (+Y)
			table.insert(faces, { b, c, d })
		end
	end
	em:BatchAdd(Enum.MeshAttribute.Face, faces)
	return em
end
```

Animating vertices (client side, batched):

```luau
local RunService = game:GetService("RunService")

local ids = em:GetVertices()
local rest = em:BatchGetValues(ids) -- { Vector3 } in the same order
local moved = table.create(#ids)

RunService.RenderStepped:Connect(function()
	local t = os.clock()
	for i, p in rest do
		moved[i] = Vector3.new(p.X, math.sin(p.X * 0.2 + t) * 0.5, p.Z)
	end
	em:BatchSetValues(ids, moved)
end)
```

Per-frame vertex writes are expensive on big meshes: keep animated meshes small, or animate with skinning
(`AddBone`, `SetVertexBones`, `SetVertexBoneWeights`) and move the bones instead.

Other useful methods: `AddVertex`, `AddTriangle`, `AddFace`, `AddNormal`, `AddUV`, `AddColor(color, alpha)`,
`SetFaceUVs`, `SetFaceNormals`, `SetFaceColors`, `GetPosition`, `SetPosition`, `RemoveFace`, `RemoveUnused`,
`MergeVertices`, `FindClosestVertex`, `FindVerticesWithinSphere`, `RaycastLocal`, `GetSize`. Query methods are safe
to call from parallel Luau; methods that change the mesh must run serially.

## Drawing on an image

Pixels are RGBA, 4 bytes each, row by row. `WritePixelsBuffer` uses **alpha** (255 = opaque); the `Draw*` methods
use **transparency** (0 = opaque).

```luau
local size = Vector2.new(128, 128)
local img = AssetService:CreateEditableImage({ Size = size })
if img then
	local buf = buffer.create(size.X * size.Y * 4)
	for y = 0, size.Y - 1 do
		for x = 0, size.X - 1 do
			local i = (y * size.X + x) * 4
			buffer.writeu8(buf, i, x * 2)     -- R
			buffer.writeu8(buf, i + 1, y * 2) -- G
			buffer.writeu8(buf, i + 2, 200)   -- B
			buffer.writeu8(buf, i + 3, 255)   -- A
		end
	end
	img:WritePixelsBuffer(Vector2.zero, size, buf)
	img:DrawCircle(Vector2.new(64, 64), 20, Color3.new(1, 1, 1), 0, Enum.ImageCombineType.BlendSourceOver)
	img:DrawRectangle(Vector2.new(8, 8), Vector2.new(30, 10), Color3.new(0, 0, 0), 0.5, Enum.ImageCombineType.BlendSourceOver)
	local pixels = img:ReadPixelsBuffer(Vector2.zero, size)
end
```

Also: `DrawLine`, `DrawImage(position, otherImage, combineType)`, `DrawImageTransformed` (scale, rotate, crop or
resize into a new image), `DrawImageProjected` (project decals onto meshes).

## Sharing generated content (CreateDataModelContentAsync)

Editables never replicate. To make server-generated content visible to every client, bake it into static
`Content` that lives for the server session:

```luau
-- Server
local result, meshContent = AssetService:CreateDataModelContentAsync(Content.fromObject(em))
if result == Enum.CreateContentResult.Success then
	local part = AssetService:CreateMeshPartAsync(meshContent)
	part.Anchored = true
	part.Parent = workspace -- replicates with its mesh data
	em:Destroy()            -- baked content doesn't use the editable budget
end
```

Baked content gets LODs/mips, replicates server to client, and can be loaded back with `CreateEditableMeshAsync` /
`CreateEditableImageAsync`. It is not saved after the session; to keep a creation, save your own data and rebuild.

## Mistakes to avoid

- Not checking for `nil` from the create methods on clients.
- Leaking editables: every one you stop using needs `:Destroy()`.
- Creating editables on the server and expecting clients to see them. Bake with `CreateDataModelContentAsync`, or
  build them on each client.
- Setting `MeshPart.MeshContent` or `MeshId` from a script. Use `CreateMeshPartAsync` and `ApplyMesh`.
- Mixing up alpha (buffer) and transparency (draw methods).
- Per-vertex `SetPosition` loops on big meshes every frame. Batch, shrink the mesh, or use bones.
- Loading assets the game owner doesn't own or wasn't shared, then shipping without the published-game toggle.
