EditableMesh

Show Deprecated
Not Creatable

EditableMesh changes the applied visual mesh when linked to a MeshPart, allowing for querying and modification of the mesh both in Studio and in experience.

Enabling for Published Experiences

For security purposes, using EditableMesh fails by default for published experiences. To enable usage of EditableMesh, you must be 13+ age verified and ID verified. After you are verified, open Studio's Game Settings, select Security, and enable the Allow Mesh & Image APIs toggle. Remember to review the Terms of Use before enabling the toggle.

Permissions

To prevent misuse, AssetService:CreateEditableMeshAsync() will only allow you to load and edit mesh assets:

  • That are owned by the creator of the experience (if the experience is owned by an individual).
  • That are owned by a group (if the experience is owned by the group).
  • That are owned by the logged in Studio user (if the place file has not yet been saved or published to Roblox).

Memory Limits

Editable assets are currently expensive for memory usage. To minimize its impact on client performance, EditableMesh has strict client-side memory budgets, although the server, Studio, and plugins operate with unlimited memory. Using FixedSize may help you stay within the memory budget and, in some scenarios, linking one EditableMesh to multiple MeshParts (multi-referencing) can help with memory optimization.

Creation and Display

An EditableMesh can be created from an existing Content of a MeshPart or a mesh ID using AssetService:CreateEditableMeshAsync(), or a blank EditableMesh can be created with AssetService:CreateEditableMesh(). It can then be displayed, modified, and its collision model updated. Not all of the steps are necessary; for example, you might want to create an EditableMesh just to raycast without ever displaying it.


local AssetService = game:GetService("AssetService")
-- Create empty EditableMesh
local editableMesh = AssetService:CreateEditableMesh()
-- Create EditableMesh from asset ID
local editableMeshFromAsset = nil
local success, errorMessage = pcall(function()
editableMeshFromAsset = AssetService:CreateEditableMeshAsync(Content.fromAssetId(ASSET_ID))
end)
-- Create EditableMesh from another EditableMesh
local editableMeshFromAnother = nil
local success, errorMessage = pcall(function()
editableMeshFromAnother = AssetService:CreateEditableMeshAsync(Content.fromObject(OTHER_EDITABLE_MESH))
end)
-- Create EditableMesh from MeshPart
local editableMeshFromMeshPart = nil
local success, errorMessage = pcall(function()
editableMeshFromMeshPart = AssetService:CreateEditableMeshAsync(MESH_PART.MeshContent)
end)

An EditableMesh is displayed when it's linked to a new MeshPart, through AssetService:CreateMeshPartAsync(). You can create more MeshPart instances that reference the same EditableMesh Content, or link to an existing MeshPart through MeshPart:ApplyMesh().


local AssetService = game:GetService("AssetService")
local Workspace = game:GetService("Workspace")
-- Create EditableMesh from asset ID
local editableMeshFromAsset = nil
local success, errorMessage = pcall(function()
editableMeshFromAsset = AssetService:CreateEditableMeshAsync(Content.fromAssetId(ASSET_ID))
end)
-- Create new MeshPart linked to the EditableMesh
local newMeshPart = nil
local success, errorMessage = pcall(function()
newMeshPart = AssetService:CreateMeshPartAsync(Content.fromObject(editableMeshFromAsset))
end)
-- Alternatively, link the new MeshPart created above to an existing MeshPart
local existingMeshPart = Workspace:FindFirstChild("EXISTING_MESH_PART")
existingMeshPart:ApplyMesh(newMeshPart)

To recalculate collision and fluid geometry after editing, you can again call AssetService:CreateMeshPartAsync() and MeshPart:ApplyMesh() to update an existing MeshPart. It's generally recommended to do this at the end of a conceptual edit, not after individual calls to methods that manipulate geometry. Visual changes to the mesh will always be immediately reflected by the engine, without the need to call AssetService:CreateMeshPartAsync().

Fixed-Size Meshes

When creating an EditableMesh from an existing mesh asset (via AssetService:CreateEditableMeshAsync()), the resulting editable mesh is fixed-size by default. Fixed-size meshes are more efficient in terms of memory but you cannot change the number of vertices, faces, or attributes. Only the values of vertex attributes and positions can be edited.


local AssetService = game:GetService("AssetService")
-- Create EditableMesh without fixed-size default
local editableMeshFromAsset = nil
local success, errorMessage = pcall(function()
editableMeshFromAsset = AssetService:CreateEditableMeshAsync(Content.fromAssetId(ASSET_ID), {FixedSize = false})
end)

Stable Vertex/Face IDs

Many EditableMesh methods take vertex, normal, UV, color and face IDs. These are represented as integers in Luau but they require some special handling. The main difference is that IDs are stable and they remain the same even if other parts of the mesh change. For example, if an EditableMesh has five vertices {1, 2, 3, 4, 5} and you remove vertex 4, the new vertices will be {1, 2, 3, 5}.

Note that the IDs are not guaranteed to be in order and there may be holes in the numbering, so when iterating through vertices or faces, you should iterate through the table returned by GetVertices() or GetFaces().

Split Vertex Attributes

A vertex is a corner of a face, and topologically connects faces together. Vertices can have several attributes: position, normal, UV coordinate, color, and transparency.

Sometimes it's useful for all faces that touch a vertex to use the same attribute values, but sometimes you'll want different faces to use different attribute values on the same vertex. For example, on a smooth sphere, each vertex will only have a single normal. In contrast, at the corner of a cube, the vertex will have 3 different normals (one for each adjacent face). You can also have seams in the UV coordinates or sharp changes in the vertex colors.

When creating faces, every vertex will by default have one of each attribute: one normal, one UV coordinate, and one color/transparency. If you want to create a seam, you should create new attributes and set them on the face. For example, this code will create a sharp cube:


local AssetService = game:GetService("AssetService")
-- Given 4 vertex IDs, adds a new normal and 2 triangles, making a sharp quad
local function addSharpQuad(editableMesh, vid0, vid1, vid2, vid3)
local nid = editableMesh:AddNormal() -- This creates a normal ID which is automatically computed
local fid1 = editableMesh:AddTriangle(vid0, vid1, vid2)
editableMesh:SetFaceNormals(fid1, {nid, nid, nid})
local fid2 = editableMesh:AddTriangle(vid0, vid2, vid3)
editableMesh:SetFaceNormals(fid2, {nid, nid, nid})
end
-- Makes a cube with creased edges between the 6 sides
local function makeSharpCube()
local editableMesh = AssetService:CreateEditableMesh()
local v1 = editableMesh:AddVertex(Vector3.new(0, 0, 0))
local v2 = editableMesh:AddVertex(Vector3.new(1, 0, 0))
local v3 = editableMesh:AddVertex(Vector3.new(0, 1, 0))
local v4 = editableMesh:AddVertex(Vector3.new(1, 1, 0))
local v5 = editableMesh:AddVertex(Vector3.new(0, 0, 1))
local v6 = editableMesh:AddVertex(Vector3.new(1, 0, 1))
local v7 = editableMesh:AddVertex(Vector3.new(0, 1, 1))
local v8 = editableMesh:AddVertex(Vector3.new(1, 1, 1))
addSharpQuad(editableMesh, v5, v6, v8, v7) -- Front
addSharpQuad(editableMesh, v1, v3, v4, v2) -- Back
addSharpQuad(editableMesh, v1, v5, v7, v3) -- Left
addSharpQuad(editableMesh, v2, v4, v8, v6) -- Right
addSharpQuad(editableMesh, v1, v2, v6, v5) -- Bottom
addSharpQuad(editableMesh, v3, v7, v8, v4) -- Top
editableMesh:RemoveUnused()
return editableMesh
end

Winding

Mesh faces have a front side and a back side. When drawing meshes, only the front of the faces are drawn by default, although you can change this by setting the mesh' DoubleSided property to true.

The order of the vertices around the face determines whether you are looking at the front or the back. The front of the face is visible when the vertices go counterclockwise around it.

Order of the vertices around the face

FACS Poses

Animatable heads use the Facial Action Coding System (FACS). See the FACS poses reference for helpful information when using GetFacsPoses() and similar methods.

Each FACS pose is specified by an Enum.FacsActionUnit value. For the FACS pose, virtual bones can each have a CFrame that transforms the bones' initial CFrame in the bind pose of the mesh into the CFrame for that FACS action unit's pose. All bone CFrames are in the mesh's local space.

These FACS poses are blended together during animation. Sometimes, the blending of the base poses produces poor results. In those cases, you can override the blending of specific combinations of base poses with a corrective pose that is more pleasing. A corrective pose is specified by 2 or 3 Enum.FacsActionUnit values. Like a base FACS pose, for a corrective pose, virtual bones can each have a CFrame that transforms the bones' initial CFrame in the bind pose of the mesh into the CFrame for that FACS corrective.

Limitations

EditableMesh currently has a limit of 60,000 vertices and 20,000 triangles. Attempting to add too many vertices or triangles will cause an error.

Summary

Properties

  • Read Only
    Not Replicated
    Roblox Security
    Read Parallel

    Returns true if a mesh is fixed-size.

Methods

Properties

FixedSize

Read Only
Not Replicated
Roblox Security
Read Parallel

Methods

AddBone

Parameters

boneProperties: Dictionary

Returns

AddColor

Parameters

color: Color3
alpha: number
Default Value: ""

Returns

AddNormal

Parameters

normal: Vector3

Returns

AddTriangle

Parameters

vertexId0: number
Default Value: ""
vertexId1: number
Default Value: ""
vertexId2: number
Default Value: ""

Returns

AddUV

Parameters


Returns

AddVertex

Parameters


Returns

Destroy

()

Returns

()

FindClosestPointOnSurface

Parameters

point: Vector3

Returns

FindClosestVertex

Parameters

toThisPoint: Vector3

Returns

FindVerticesWithinSphere

Parameters

center: Vector3
radius: number
Default Value: ""

Returns

GetAdjacentFaces

Parameters

faceId: number
Default Value: ""

Returns

GetAdjacentVertices

Parameters

vertexId: number
Default Value: ""

Returns

GetBoneByName

Parameters

boneName: string

Returns

GetBoneCFrame

Parameters

boneId: number
Default Value: ""

Returns

GetBoneIsVirtual

Parameters

boneId: number
Default Value: ""

Returns

GetBoneName

Parameters

boneId: number
Default Value: ""

Returns

GetBoneParent

Parameters

boneId: number
Default Value: ""

Returns

GetBones


Returns

GetCenter


Returns

GetColor

Parameters

colorId: number
Default Value: ""

Returns

GetColorAlpha

Parameters

colorId: number
Default Value: ""

Returns

GetColors


Returns

GetFaceColors

Parameters

faceId: number
Default Value: ""

Returns

GetFaceNormals

Parameters

faceId: number
Default Value: ""

Returns

GetFaceUVs

Parameters

faceId: number
Default Value: ""

Returns

GetFaceVertices

Parameters

faceId: number
Default Value: ""

Returns

GetFaces


Returns

GetFacsCorrectivePose

Parameters

actions: Array

Returns

GetFacsCorrectivePoses


Returns

GetFacsPose

Parameters


Returns

GetFacsPoses


Returns

GetNormal

Parameters

normalId: number
Default Value: ""

Returns

GetNormals


Returns

GetPosition

Parameters

vertexId: number
Default Value: ""

Returns

GetSize


Returns

GetUV

Parameters

uvId: number
Default Value: ""

Returns

GetUVs


Returns

GetVertexBoneWeights

Parameters

vertexId: number
Default Value: ""

Returns

GetVertexBones

Parameters

vertexId: number
Default Value: ""

Returns

GetVertices


Returns

IdDebugString

Parameters

id: number
Default Value: ""

Returns

MergeVertices

Map

Parameters

mergeTolerance: number
Default Value: ""

Returns

Map

RaycastLocal

Parameters

origin: Vector3
direction: Vector3

Returns

Code Samples

EditableMesh:RaycastLocal()

local AssetService = game:GetService("AssetService")
local Workspace = game:GetService("Workspace")
-- Initialize EditableMesh in space
local editableMesh = nil
local success, errorMsg = pcall(function()
editableMesh = AssetService:CreateEditableMeshAsync(Content.fromUri("rbxassetid://ASSET_ID"))
end)
local meshPart = nil
if success and editableMesh then
meshPart = AssetService:CreateMeshPartAsync(
Content.fromObject(editableMesh),
{ CollisionFidelity = Enum.CollisionFidelity.Hull }
)
meshPart.Parent = Workspace
else
warn(errorMsg)
end
-- Function that will cast a ray from the given point, returning the world point of the hit and the UV coordinate
local function castRayFromCamera(meshPart : MeshPart, editableMesh : EditableMesh, viewportPoint : Vector3)
if not meshPart then
return
end
-- Calculate how much the object is being scaled in each dimension
local renderScale = meshPart.Size / meshPart.MeshSize
-- Create ray from camera along the direction of a clicked point
local ray = Workspace.CurrentCamera:ViewportPointToRay(viewportPoint.X, viewportPoint.Y)
-- Convert to object space to use with RaycastLocal()
local relativeOrigin = meshPart.CFrame:PointToObjectSpace(ray.Origin) / renderScale
local relativeTarget = meshPart.CFrame:PointToObjectSpace(ray.Origin + ray.Direction * 100) / renderScale
local relativeDirection = relativeTarget - relativeOrigin
local faceId, point, barycentricCoordinate, vertId1, vertId2, vertId3 = editableMesh:RaycastLocal(relativeOrigin, relativeDirection)
if not faceId then
-- Didn't hit any faces
return
end
-- Compute the hit point in world space
local worldHitPoint = meshPart.CFrame:PointToWorldSpace(point * renderScale)
-- Get the UVs on the face
local uvId1 = editableMesh:GetVertexFaceUV(vertId1, faceId)
local uvId2 = editableMesh:GetVertexFaceUV(vertId2, faceId)
local uvId3 = editableMesh:GetVertexFaceUV(vertId3, faceId)
local uv1 = editableMesh:GetUV(uvId1)
local uv2 = editableMesh:GetUV(uvId2)
local uv3 = editableMesh:GetUV(uvId3)
-- Interpolate UVs within the face based on the barycentric coordinate
local u = (barycentricCoordinate.x * uv1.x) + (barycentricCoordinate.y * uv2.x) + (barycentricCoordinate.z * uv3.x)
local v = (barycentricCoordinate.x * uv1.y) + (barycentricCoordinate.y * uv2.y) + (barycentricCoordinate.z * uv3.y)
return worldHitPoint, Vector2.new(u, v)
end

RemoveBone

()

Parameters

boneId: number
Default Value: ""

Returns

()

RemoveFace

()

Parameters

faceId: number
Default Value: ""

Returns

()

RemoveUnused


Returns

ResetNormal

()

Parameters

normalId: number
Default Value: ""

Returns

()

SetBoneCFrame

()

Parameters

boneId: number
Default Value: ""
cframe: CFrame

Returns

()

SetBoneIsVirtual

()

Parameters

boneId: number
Default Value: ""
virtual: boolean

Returns

()

SetBoneName

()

Parameters

boneId: number
Default Value: ""
name: string

Returns

()

SetBoneParent

()

Parameters

boneId: number
Default Value: ""
parentBoneId: number
Default Value: ""

Returns

()

SetColor

()

Parameters

colorId: number
Default Value: ""
color: Color3

Returns

()

SetColorAlpha

()

Parameters

colorId: number
Default Value: ""
alpha: number
Default Value: ""

Returns

()

SetFaceColors

()

Parameters

faceId: number
Default Value: ""
ids: Array

Returns

()

SetFaceNormals

()

Parameters

faceId: number
Default Value: ""
ids: Array

Returns

()

SetFaceUVs

()

Parameters

faceId: number
Default Value: ""
ids: Array

Returns

()

SetFaceVertices

()

Parameters

faceId: number
Default Value: ""
ids: Array

Returns

()

SetFacsBonePose

()

Parameters

boneId: number
Default Value: ""
cframe: CFrame

Returns

()

SetFacsCorrectivePose

()

Parameters

actions: Array
boneIds: Array
cframes: Array

Returns

()

SetFacsPose

()

Parameters

boneIds: Array
cframes: Array

Returns

()

SetNormal

()

Parameters

normalId: number
Default Value: ""
normal: Vector3

Returns

()

SetPosition

()

Parameters

vertexId: number
Default Value: ""

Returns

()

SetUV

()

Parameters

uvId: number
Default Value: ""

Returns

()

SetVertexBoneWeights

()

Parameters

vertexId: number
Default Value: ""
boneWeights: Array

Returns

()

SetVertexBones

()

Parameters

vertexId: number
Default Value: ""
boneIDs: Array

Returns

()

Triangulate

()

Returns

()

Events