World
GeometryService
Generate MeshParts at runtime from code, heightmaps, density fields, extrusions and lathes, and slice, deform or simplify existing meshes.
Getting it
local GeometryService = Engine:GetService("GeometryService")Overview
GeometryService turns triangles into real MeshParts while the game runs. It uses Roblox's EditableMesh and AssetService:CreateMeshPartAsync under the hood, so the result is a normal MeshPart with collisions that you can parent, colour and move like any other part.
There are three ways to get a mesh:
- Build it yourself with a
MeshBuilder: add vertices and triangles, then build. - Generate it from a heightmap, a density function (marching cubes), a 2D shape swept along a Spline, or a profile spun around an axis.
- Change an existing MeshPart: slice it in two, move its vertices, or reduce its triangle count.
Every method that creates a part is asynchronous and returns a Promise that resolves with the MeshPart (or two parts for a slice).
local GeometryService = Engine:GetService("GeometryService")
local Noise = Engine:GetPackage("Noise")
local field = Noise.Scale(Noise.FBM(Noise.Simplex(42)), 1 / 80)
GeometryService:FromHeightmapAsync(function(x, z)
return field:Sample2D(x, z) * 30
end, {
size = Vector2.new(256, 256),
resolution = Vector2.new(128, 128),
}, {
Anchored = true,
Material = Enum.Material.Grass,
Parent = workspace,
}):andThen(function(part)
print("terrain ready", part)
end)Note
Editable meshes count against Roblox's memory budget for runtime meshes. GetMemoryUsage() shows how much this service has created.
Build settings
Every method that creates a part takes an optional BuildConfig. Leave a field out and the configured default is used.
| Field | Description |
|---|---|
Name, Parent | Name, and where to put the part. Without Parent the part is returned unparented. |
Anchored, CanCollide, CanTouch, CanQuery | Physics flags. |
Material, Color, Transparency, DoubleSided | Appearance. |
CollisionFidelity, RenderFidelity | Mesh fidelity enums. |
Building by hand
CreateBuilder
GeometryService:CreateBuilder(): MeshBuilderlocal builder = GeometryService:CreateBuilder()
local a = builder:AddVertex(Vector3.new(0, 0, 0))
local b = builder:AddVertex(Vector3.new(10, 0, 0))
local c = builder:AddVertex(Vector3.new(10, 5, 10))
local d = builder:AddVertex(Vector3.new(0, 5, 10))
builder:AddQuad(a, b, c, d)
builder:ComputeNormals()
builder:BuildAsync({ Anchored = true, Parent = workspace })| Member | Description |
|---|---|
:AddVertex(position, normal?, uv?, color?, alpha?): number | Adds a vertex and returns its index. |
:AddTriangle(a, b, c): number | Adds a triangle from three vertex indices. |
:AddQuad(a, b, c, d): (number, number) | Adds two triangles. |
:SetVertexPosition/Normal/UV/Color(index, ...) | Edit a vertex. |
:ComputeNormals(overwrite?) | Calculates smooth normals for vertices that have none (or all, with overwrite). |
:Transform(cframe) | Moves every vertex. |
:Append(other): number | Adds another builder's mesh; returns the index offset of its vertices. |
:Weld(tolerance): number | Merges vertices closer than tolerance; returns how many were removed. |
:GetBounds(): (min, max) | |
:Clear(), :Destroy() | |
:BuildAsync(config?): Promise<MeshPart> | Creates the part. |
VertexCount, TriangleCount, Destroyed | Read-only. |
Generators
FromHeightmapAsync
GeometryService:FromHeightmapAsync(source: buffer | (x: number, z: number) -> number, config: HeightmapConfig, build: BuildConfig?): Promise<MeshPart>A grid of resolution.X by resolution.Y vertices covering size studs. source is a function called with each vertex's local X and Z in studs, or a buffer of 32-bit floats in row order. Heights are multiplied by heightScale; uvScale tiles the texture coordinates.
MarchingCubesAsync
GeometryService:MarchingCubesAsync(density: (pos: Vector3) -> number, region: Region3, cellSize: number, iso: number?, build: BuildConfig?): Promise<MeshPart>Builds the surface where density(pos) crosses iso (default 0) inside region, sampled every cellSize studs. Use it for caves, blobs and overhangs that a heightmap cannot express.
-- A sphere of radius 20
GeometryService:MarchingCubesAsync(function(pos)
return 20 - pos.Magnitude
end, Region3.new(Vector3.one * -24, Vector3.one * 24), 2)ExtrudeAsync
GeometryService:ExtrudeAsync(profile: { Vector2 }, path: Spline, config: { segments: number?, closeEnds: boolean? }?, build: BuildConfig?): Promise<MeshPart>Sweeps a 2D shape (at least 3 points) along a spline: rails, pipes, roads, tubes. segments is how many steps along the path; closeEnds caps both ends.
LatheAsync
GeometryService:LatheAsync(profile: { Vector2 }, segments: number, config: { startAngle: number?, endAngle: number?, capEnds: boolean? }?, build: BuildConfig?): Promise<MeshPart>Spins a 2D profile (at least 2 points; X is the distance from the axis, Y the height) around the vertical axis in segments steps: vases, columns, bottles, wheels. A partial angle range makes a slice; partial shapes cap their ends by default.
Changing existing meshes
SliceAsync
GeometryService:SliceAsync(part: MeshPart, plane: CFrame, build: BuildConfig?): Promise<(MeshPart, MeshPart)>Cuts a MeshPart in two along a plane that passes through plane.Position with normal plane.UpVector. The plane is in the mesh's own object space (the same space as DecomposeAsync data), so CFrame.new() cuts through the mesh's centre horizontally. The original part is left as is and the new parts are not positioned for you: place them (for example at the original part's CFrame) and destroy the original if the halves replace it.
DeformAsync
GeometryService:DeformAsync(meshPart: MeshPart, fn: (vertexPos: Vector3) -> Vector3): Promise<MeshPart>Moves every vertex through fn and updates the part's geometry in place, recalculating normals.
-- Make a wobbly version of a mesh
GeometryService:DeformAsync(blob, function(p)
return p + p.Unit * math.sin(p.Y * 2) * 0.3
end)SimplifyAsync
GeometryService:SimplifyAsync(meshPart: MeshPart, targetTriangles: number, build: BuildConfig?): Promise<MeshPart>Creates a lower-detail copy with roughly targetTriangles triangles, for example for distant objects.
Working with mesh data
For advanced use you can work with raw mesh data between steps.
| Method | Description |
|---|---|
:DecomposeAsync(part): Promise<MeshData> | Reads a MeshPart's triangles into a MeshData table. |
:SliceData(data, plane, capRegions?): (MeshData, MeshData) | Slices data without creating parts. capRegions is an optional list of UV rectangles { Min: Vector2, Max: Vector2 }: a cut face whose edge lies in one of them gets its UVs inside that rectangle, so it shows the texture of the part it was cut from in a texture atlas. |
:CommitAsync(data, build?): Promise<MeshPart> | Turns data into a part. Rejects for data with no triangles. |
GeometryService.MeshData | Helpers: new, Clone, AddVertex, AddTriangle, AddQuad, SetVertex..., GetVertexCount, GetTriangleCount, ComputeNormals, Transform, Merge, GetBounds, Weld. |
A MeshData is a plain table: { Positions, Normals, UVs, Colors, Alphas, Triangles }, where each triangle is { a, b, c }.
GetMemoryUsage
GeometryService:GetMemoryUsage(): { Meshes: number, Mb: number }Configuration
DefaultConfiguration/Services/GeometryService16 settings| Setting | Default | What it does |
|---|---|---|
| DefaultHeightScale | 1 | Vertical scale :FromHeightmapAsync() applies when config.heightScale is omitted. |
| DefaultUvScale | 1 | UV tiling :FromHeightmapAsync() applies when config.uvScale is omitted. |
| DefaultIso | 0 | Density threshold :MarchingCubesAsync() treats as "inside" when config.iso is omitted. |
| DefaultExtrudeSegments | 16 | Segments :ExtrudeAsync() sweeps along the path when config.segments is omitted. |
| DefaultCloseEnds | true | Whether :ExtrudeAsync() caps both ends when config.closeEnds is omitted. |
| DefaultName | "GeneratedMesh" | Name of every MeshPart this service creates when BuildConfig.Name is omitted. |
| DefaultAnchored | false | Anchored of a created MeshPart when BuildConfig.Anchored is omitted. |
| DefaultCanCollide | true | CanCollide of a created MeshPart when BuildConfig.CanCollide is omitted. |
| DefaultCanTouch | true | CanTouch of a created MeshPart when BuildConfig.CanTouch is omitted. |
| DefaultCanQuery | true | CanQuery of a created MeshPart when BuildConfig.CanQuery is omitted. |
| DefaultDoubleSided | false | DoubleSided of a created MeshPart when BuildConfig.DoubleSided is omitted. |
| DefaultMaterial | "Plastic" | Enum.Material name of a created MeshPart when BuildConfig.Material is omitted. |
| DefaultColor | Color3.new(1, 1, 1) | Color of a created MeshPart when BuildConfig.Color is omitted. |
| DefaultTransparency | 0 | Transparency of a created MeshPart when BuildConfig.Transparency is omitted. |
| DefaultCollisionFidelity | "Default" | Enum.CollisionFidelity name of a created MeshPart when BuildConfig.CollisionFidelity is omitted. |
| DefaultRenderFidelity | "Precise" | Enum.RenderFidelity name of a created MeshPart. |
Types
export type MeshBuilder
export type MeshData
export type BuildConfig
export type HeightmapConfig = { size: Vector2, resolution: Vector2, heightScale: number?, uvScale: number? }
export type LatheConfig = { startAngle: number?, endAngle: number?, capEnds: boolean? }
export type ExtrudeConfig = { segments: number?, closeEnds: boolean? }