Skip to content
Engine

World

GeometryService

Generate MeshParts at runtime from code, heightmaps, density fields, extrusions and lathes, and slice, deform or simplify existing meshes.

ServiceShared

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:

  1. Build it yourself with a MeshBuilder: add vertices and triangles, then build.
  2. Generate it from a heightmap, a density function (marching cubes), a 2D shape swept along a Spline, or a profile spun around an axis.
  3. 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).

Terrain from noise
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.

FieldDescription
Name, ParentName, and where to put the part. Without Parent the part is returned unparented.
Anchored, CanCollide, CanTouch, CanQueryPhysics flags.
Material, Color, Transparency, DoubleSidedAppearance.
CollisionFidelity, RenderFidelityMesh fidelity enums.

Building by hand

CreateBuilder

GeometryService:CreateBuilder(): MeshBuilder
A simple ramp
local 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 })
MemberDescription
:AddVertex(position, normal?, uv?, color?, alpha?): numberAdds a vertex and returns its index.
:AddTriangle(a, b, c): numberAdds 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): numberAdds another builder's mesh; returns the index offset of its vertices.
:Weld(tolerance): numberMerges vertices closer than tolerance; returns how many were removed.
:GetBounds(): (min, max)
:Clear(), :Destroy()
:BuildAsync(config?): Promise<MeshPart>Creates the part.
VertexCount, TriangleCount, DestroyedRead-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.

MethodDescription
: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.MeshDataHelpers: 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

Defaults from DefaultConfiguration/Services/GeometryService16 settings
SettingDefaultWhat it does
DefaultHeightScale1Vertical scale :FromHeightmapAsync() applies when config.heightScale is omitted.
DefaultUvScale1UV tiling :FromHeightmapAsync() applies when config.uvScale is omitted.
DefaultIso0Density threshold :MarchingCubesAsync() treats as "inside" when config.iso is omitted.
DefaultExtrudeSegments16Segments :ExtrudeAsync() sweeps along the path when config.segments is omitted.
DefaultCloseEndstrueWhether :ExtrudeAsync() caps both ends when config.closeEnds is omitted.
DefaultName"GeneratedMesh"Name of every MeshPart this service creates when BuildConfig.Name is omitted.
DefaultAnchoredfalseAnchored of a created MeshPart when BuildConfig.Anchored is omitted.
DefaultCanCollidetrueCanCollide of a created MeshPart when BuildConfig.CanCollide is omitted.
DefaultCanTouchtrueCanTouch of a created MeshPart when BuildConfig.CanTouch is omitted.
DefaultCanQuerytrueCanQuery of a created MeshPart when BuildConfig.CanQuery is omitted.
DefaultDoubleSidedfalseDoubleSided of a created MeshPart when BuildConfig.DoubleSided is omitted.
DefaultMaterial"Plastic"Enum.Material name of a created MeshPart when BuildConfig.Material is omitted.
DefaultColorColor3.new(1, 1, 1)Color of a created MeshPart when BuildConfig.Color is omitted.
DefaultTransparency0Transparency 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? }