Editables

Images, meshes and sounds in memory on both runtimes: an EditableImage drawn, saved as a PNG and loaded back, an EditableMesh built from vertices, faces and UVs, an AudioClip saved as a WAV and loaded back, and what still needs the native runtime.

An image

use ImageService reaches the image service. CreateEditableImage makes an image of that many pixels, every pixel transparent black. The draw members take positions and sizes in pixels, counted from 0 at the top left, a Color3, a transparency from 0 to 1 and how to mix: "Overwrite" replaces the pixels, "BlendSourceOver" lays the colour over them ("AlphaBlend", "Add" and "Multiply" are the others). SaveAsync writes the image as a PNG, or as a JPEG (quality 90), BMP or TGA file when the path ends in .jpg or .jpeg, .bmp or .tga; a relative path is in the project's folder, the one its .ssp is in. LoadAsync reads a PNG, JPEG, BMP or TGA file back as a new image (a WEBP file stops the run: WEBP isn't readable in this runtime (PNG, JPEG, BMP and TGA are)), and GetInfoAsync reads only the file's header, of any of the five:

use ImageService

local image = ImageService:CreateEditableImage({ Size = Vector2.new(8, 8) })
image:DrawRectangle(Vector2.new(0, 0), Vector2.new(8, 8), Color3.new(1, 1, 1), 0, "Overwrite")
image:DrawCircle(Vector2.new(4, 4), 2, Color3.fromRGB(255, 0, 0), 0, "Overwrite")
image:SaveAsync("Dot.png")

local loaded = ImageService:LoadAsync("Dot.png")
local color, alpha = loaded:GetPixel(Vector2.new(4, 4))
print(loaded.Size, color, alpha)
local info = ImageService:GetInfoAsync("Dot.png")
print(info.Format, info.Size, info.Bytes)

shot run Demo.ssp prints

<Vector2(8, 8)> <Color3(1, 0, 0)> 1
PNG <Vector2(8, 8)> 93

and leaves Dot.png beside Demo.ssp. GetPixel gives a pixel's colour and its alpha from 0 to 1, and SetPixel sets one. ReadPixelsBuffer and WritePixelsBuffer move a whole region as an RGBA8 buffer, four bytes a pixel, row by row from the top left. EncodePng gives the PNG file's bytes without writing a file, and ImageService:ToDataUri a data:image/png;base64 URI for a request body or a web page. ImageService:Pack(images, options) lays a list of images into one sprite sheet and gives {Image, Rects}: the sheet, and each image's Rect in the list's order for ImageRectOffset and ImageRectSize. options.Padding (0) is the pixels between images and from the edges, options.MaxSize (4096, at most 16384) the largest side, and PowerOfTwo = true makes the sides powers of two; images that don't fit stop the run with Pack: the images don't fit in 4096 x 4096.

A mesh

The library's AssetService (use AssetService) makes an empty mesh with CreateEditableMesh. AddVertex adds a vertex and returns its id, and AddTriangle a face through three vertex ids, returning the face's id. AddUV adds a UV and returns its id; SetFaceUVs gives a face's three corners their UVs, in the order of the face's vertices. Normals and colours work the same way: AddNormal with SetFaceNormals, AddColor with SetFaceColors.

use AssetService

local mesh = AssetService:CreateEditableMesh()
local a = mesh:AddVertex(Vector3.new(0, 0, 0))
local b = mesh:AddVertex(Vector3.new(2, 0, 0))
local c = mesh:AddVertex(Vector3.new(2, 2, 0))
local d = mesh:AddVertex(Vector3.new(0, 2, 0))
local first = mesh:AddTriangle(a, b, c)
local second = mesh:AddTriangle(a, c, d)

local uv0 = mesh:AddUV(Vector2.new(0, 0))
local uv1 = mesh:AddUV(Vector2.new(1, 0))
local uv2 = mesh:AddUV(Vector2.new(1, 1))
local uv3 = mesh:AddUV(Vector2.new(0, 1))
mesh:SetFaceUVs(first, { uv0, uv1, uv2 })
mesh:SetFaceUVs(second, { uv0, uv2, uv3 })

print(mesh.VertexCount, mesh.TriangleCount)
print(mesh:GetBounds())
local triangles = mesh:ReadTriangles()
print(buffer.len(triangles), buffer.readf32(triangles, 72 + 24), buffer.readf32(triangles, 72 + 28))

prints

4 2
<Vector3(0, 0, 0)> <Vector3(2, 2, 0)>
216 1 1

GetBounds gives the smallest and the largest corner of the box around the mesh. ReadTriangles gives every triangle in one buffer, 36 bytes a corner and three corners a triangle: the position and the normal as three f32 each, the UV as two f32 and the colour as four u8 (RGBA). So two triangles take 216 bytes, and the first triangle's third corner has its UV at byte 72 + 24. WriteTriangles replaces the whole mesh with a buffer laid out the same way. GetPosition, SetPosition, GetVertices, GetFaces, GetFaceVertices, RemoveFace and Clone work on both runtimes too, and so do Normalize, PlaceAs, Scale, SetColor, Append, GetFacesInBox, SplitConnected, SplitByUVIsland, RecalculateNormals, BakeColorsToAtlas and ToBlx (on Lune a mesh has no bones, so ToBlx writes none). MeshService:LoadAsync(source, format) reads a file (a path in the project's folder) or its bytes (a buffer) as a new mesh. The format is the one given ("RobloxMesh", "Obj", "Fbx", "Gltf" or "Glb"), else the file's ending (.mesh, .obj, .fbx, .gltf or .glb, in any case), else what the bytes start with; a Roblox .mesh may be a version 6 or 7 one whose core is Draco-compressed. A file of several objects loads as one mesh, and MeshService:LoadObjectsAsync(path) gives each as its own, {[name] = mesh}: an unnamed object is "Mesh", and a name that comes again becomes "Name (2)", "Name (3)" and so on. On Lune a .gltf given as a buffer finds its other files only at full paths, and an .fbx's n-gons are fanned into triangles. MeshService:SaveAsync writes a mesh, or named meshes ({[name] = mesh}, an object each in name order), to a .mesh (version 2.00), .obj, .glb, .gltf or .fbx path; options.Texture (an EditableImage) goes beside an .obj (with its .mtl) or an .fbx as a PNG, and inside a .glb or .gltf.

MeshService:GetTriangleColorsAsync(mesh, image) gives {Colors, Islands, Objects}, three buffers over the faces in order. Colors is three u8 a face: the mean RGB of the image's pixels whose centres are inside the face's UV triangle, leaving out transparent ones (the pixel under the face's centre when none is). Islands and Objects are a u16 a face, little-endian, with ids from 0: an object is the faces that share positions, an island the faces of one object that share UVs. SplitByColorAsync(mesh, image, countOrPalette) gives a new mesh a colour, empty ones too, each with a Color attribute: a palette is a list of Color3, and a count from 1 to 256 picks its colours the same way on every run. PackIslandsAsync(meshes, margin) gives packed copies and leaves the meshes as they were: each UV island is scaled to its area in 3D and turned to its smallest box, and they are laid into the unit square as large as they fit, margin (0.005, under 0.5) apart. A margin too wide for them stops the run with PackIslandsAsync: the islands don't fit with that margin.

A sound

use AudioService reaches the audio service. CreateClip makes a silent clip of that many samples a channel at that many samples a second. SetSample sets one sample, counted from 1, to a value from -1 to 1 (GetSample reads one back). SaveAsync writes the clip as a WAV file, and LoadAsync reads one back as a new clip; OGG and MP3 files go through ffmpeg, which has to be installed:

use AudioService

local clip = AudioService:CreateClip(8000, 1, 800)
for index = 1, clip.SampleCount do
    clip:SetSample(index, 0.5 * math.sin(2 * math.pi * 440 * index / clip.SampleRate))
end
clip:SaveAsync("Beep.wav")

local loaded = AudioService:LoadAsync("Beep.wav")
print(loaded.SampleRate, loaded.Channels, loaded.SampleCount, loaded.TimeLength)
print(string.format("%.3f", loaded:GetSample(3)))

prints

8000 1 800 0.1
0.430

and leaves Beep.wav beside Demo.ssp. ReadSamplesBuffer and WriteSamplesBuffer move a run of samples as 32-bit floats, Crop and CropTime cut a new clip by samples or by seconds, Resample gives the same sound at another sample rate, and GetPeaks gives each part's minimum and maximum for drawing a waveform. Play plays the clip on the default sound device and keeps the program running until it ends, and Stop stops it; on Lune Play goes through ffplay, which has to be on the PATH (without it the error names winget install Gyan.FFmpeg).

On both runtimes

Public installs run Lune, and everything above works there as it does on the native runtime; the few differences are named where they come up (a mesh's bones, Play through ffplay). The library's AssetService gives the same images, meshes and sounds on both runtimes too: CreateEditableImage, CreateEditableMesh, LoadImageAsync (a PNG or JPEG), LoadMeshAsync (a Roblox .mesh) and LoadAudioAsync (an OGG or MP3, through ffmpeg), and its SaveAsync writes a mesh to a .glb, .obj or .fbx path, the same file on each runtime.

See also

Servicesthe services, and what needs the native runtime
FileServicefiles and folders
Instancesinstances, Destroy