The site is under maintenance, so logging in is temporarily unavailable. All resources and the MCP still work.
EditableMesh and EditableImage
By Roblox Vibecoders · roblox-editable-mesh-image
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.
Install it
mkdir -p ~/.claude/skills/roblox-editable-mesh-image && curl -fsSL https://robloxvibecoders.com/skills/roblox-editable-mesh-image/SKILL.md -o ~/.claude/skills/roblox-editable-mesh-image/SKILL.md
New-Item -ItemType Directory -Force "$HOME\.claude\skills\roblox-editable-mesh-image" | Out-Null; Invoke-WebRequest https://robloxvibecoders.com/skills/roblox-editable-mesh-image/SKILL.md -OutFile "$HOME\.claude\skills\roblox-editable-mesh-image\SKILL.md"
mkdir -p .cursor/rulescurl -fsSL https://robloxvibecoders.com/skills/roblox-editable-mesh-image/SKILL.md -o .cursor/rules/roblox-editable-mesh-image.mdc
Use the robloxvibecoders MCP: call get_resource with id "skill-roblox-editable-mesh-image" and install it as a Claude Code skill in this project (.claude/skills/roblox-editable-mesh-image/SKILL.md).
Needs the Roblox Vibecoders MCP connected.
Skills in ~/.claude/skills work in every project. Put them in your project's .claude/skills folder to share them with your team.
What your AI reads
---
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.