Hi Creators,
We’re announcing two major Studio Beta upgrades to EditableMesh that make mesh editing significantly faster:
- Parallel queries: Every EditableMesh query method can now be safely called from parallel Luau, so you can spread expensive read work (raycasts, closest-point lookups, spatial queries) across multiple threads.
- Batching APIs: Nine new methods that let you create, read, update, and remove mesh elements in bulk instead of one call per element. Available now as a Studio Beta.

FPS comparison of using batch API vs. existing API
Enabling the Studio Beta
In Studio, go to File > Beta Features and check the box for EditableMesh Batching. Restart Studio for the new methods to become available.
Parallel Queries
All of EditableMesh’s query methods — such as RaycastLocal(), FindClosestPointOnSurface(), FindClosestVertex(), and the various Get* accessors — are now safe to call from parallel Luau.
This means you can distribute heavy read workloads across Actor instances and run them concurrently after calling task.desynchronize(). A common use case is casting thousands of rays against a mesh for procedural placement, sampling, or custom collision: split the work across Actors and each one queries the same mesh in parallel.
Click here to view a code sample
-- Script parented under an Actor. Each Actor runs this in parallel.
local actor = script:GetActor()
local direction = Vector3.new(0, -1, 0)
actor:BindToMessage("StartWork", function(origins, samplePoints)
-- origins is a {Vector3} slice of work for this Actor
-- samplePoints is a {Part} slice of sampling points
task.desynchronize() -- enter parallel execution
local hits = {}
for i, origin in origins do
-- RaycastLocal is a query, so it's safe to call in parallel
local faceId, point = editableMesh:RaycastLocal(origin, direction)
if faceId then hits[i] = point
else hits[i] = nil
end
task.synchronize() -- return to serial before touching shared/mutating state
for i = 1, #samplePoints do
samplePoints[i].Position = hits[i]
end
end)
Batching
Previously, building or updating an EditableMesh meant calling a singular method (AddVertex, SetPosition, GetColor, and so on) once per element. For large meshes, the per-call overhead adds up fast. The new batch methods let you do the same work across many elements in a single call, which is typically much more performant than handling them one by one.
Rather than one method per attribute, each batch method shares a small set of general entry points and uses an Enum.MeshAttribute value — or the type already encoded in each mesh ID — to determine which attribute you mean. For example, BatchSetValues() sets positions when called with vertex IDs, but sets normals when called with normal IDs. You cannot mix-and-match ID types within a single call.
New APIs
Creation & Removal:
BatchAdd(Enum.MeshAttribute, data...) → {id}— Creates new mesh elements of the given attribute type and returns their IDs. When given Enum.MeshAttribute.Color, this expects both {Color3} and {number} for color and alpha.BatchRemove(faceIds) → ()— Removes a batch of faces.Clear() -> ()- Removes all mesh data, including vertices, faces, bones, FACS, etc from the mesh.
Modification:
BatchSetValues(ids, values) → ()— Writes attribute values to a batch of mesh element IDs.BatchSetFaceAttributes(faceIds, attrIdArrays) → ()— Sets the per-corner attribute IDs for each face in a batch.BatchSetVertexFaceAttributes(vertexIds, faceIds, attrIds) → ()— Sets the attribute at a specific corner for each vertex–face pair in a batch.
Querying:
BatchGetValues(ids) → values, values?— Given ColorIDs, this method has 2 returns:{Color3}and{Number}(representing the Color and alpha values). For every other attribute, it just returns a single table with the value{value}BatchGetFaceAttributes(Enum.MeshAttribute, faceIds) → {{id}}— Returns the per-corner attribute IDs for each face in a batch.BatchGetVertexAttributes(Enum.MeshAttribute, vertexIds) → {{id}}— Returns the attribute IDs associated with each vertex in a batch.BatchGetVertexFaceAttributes(Enum.MeshAttribute, vertexIds, faceIds) → {id}— Returns the attribute ID at each specified corner for a batch of vertex–face pairs.
The new Enum.MeshAttribute enum identifies which attribute a batch method operates on. Its values are Vertex, Normal, UV, Color, and Face
Example
Here’s a quad built entirely with batch calls, then animated by reading and writing every vertex position in one shot per frame:
Click here to view
local function createAndAnimate(editableMesh)
-- Build a quad from four corner positions in one call
local positions = {
Vector3.new(0, 0, 0),
Vector3.new(10, 0, 0),
Vector3.new(10, 10, 0),
Vector3.new(0, 10, 0),
}
local vIds = editableMesh:BatchAdd(Enum.MeshAttribute.Vertex, positions)
-- Two triangles. Faces take a 2D array of vertex IDs, not a flat list.
local fIds = editableMesh:BatchAdd(Enum.MeshAttribute.Face, {
{vIds[1], vIds[2], vIds[3]},
{vIds[1], vIds[3], vIds[4]},
})
-- Displace every vertex with noise each frame, in one read and one write
game:GetService("RunService").Heartbeat:Connect(function()
local verts = editableMesh:GetVertices()
local pts = editableMesh:BatchGetValues(verts)
for i, p in pts do
pts[i] = p + Vector3.new(0, math.noise(p.X, p.Z, os.clock()), 0)
end
editableMesh:BatchSetValues(verts, pts)
end)
end

This is what the above code produces.
Example Place File
To help you get started, we’ve prepared an example place where you can observe the performance differences firsthand:

EditableMeshBatchingDemoPlaceV4.rbxl (108.6 KB)
This example demonstrates how batching and parallelism can significantly improve performance.
To test it, open the attached RBXL file in Roblox Studio and press Play to spawn meshes in the designated spawn areas. Use the provided tools to interact with the meshes and monitor the impact on your FPS.
Tip: For the most accurate performance comparison, test one mesh type at a time rather than spawning both simultaneously.
Performance
Depending on the API used, batching can be up to ~8x faster than calling the singular methods in a loop, and parallel queries deliver up to ~2x throughput on read-heavy workloads. Actual gains depend on your mesh size and access pattern. The below graphs were tested on a 2019 Huawei Phone 2GB RAM.
Graph comparing render time for BatchSetValues(Enum.MeshAttribute.Vertex, …) and SetPosition(…) under different batch sizes
Graph comparing render time for SetFaceColors(…) in a loop and SetFaceAttributes(Enum.MeshAttribute.Face, …)
Graph comparing render time for parallel and serial version of scanner tool in demo place
Known Issues & Limitations
- Skinning is not supported for batching or parallelism, since skinning data is typically not part of the per-frame hot path.
- Parallel writes are not supported. Only query methods are parallel-safe; any method that mutates the mesh must run in serial.
What’s Next?
- Once we have addressed feedback on this new API, we will make these APIs available in published experiences.
- We are continuing to work on mitigating the following Editable* API roadbumps. Please stay tuned:
- Enabling replication
- Reconciling the extra ID Verification requirement to use Editable* APIs
- Permissions restrictions
- In additional to that, we are considering supporting quads on the EditableMesh API as well as editing the results of Solid Modeling operations like Union, Subtract, Intersect, Fragment and Sweep. If you have use cases that would benefit from quads, please share below!
We can’t wait to see what you build with these — drop your feedback, questions, and creations in this thread.
Happy (batch) meshing!
@TheMeshBoy and @monsterjunjun on behalf of the Geometry team





