Objects

A misc library of useful non-specific composition objects. ClonableContainer deep-clones its children; Movable and Sizable are transform groupers that move or scale every child relative to that child's individual baseline.

local Objects = import("Objects")

local rack = Objects.Sizable()
rack:AddChild(crateA)
rack:AddChild(crateB)
rack.Size = Vector.new(0.5, 0.5, 0.5) -- crates shrink to half size

local copy = Objects.ClonableContainer()
copy:AddChild(playerModel)
local npc = copy:Clone()                -- new player with the same shaders / deformations

Shared behavior

All three container types share:

Movable / Sizable: inherit-on-empty

When a Movable or Sizable is empty and you add a leaf (BasePart / GUI primitive) or a nested container that already has the right mode, the container adopts that child's state as its own:

Inheritance only happens when the container is empty — later adds use the current container state as the baseline (so the "shrink, add, shrink again" semantics still hold).

ClonableContainer

Objects.ClonableContainer() -> ClonableContainer
container:AddChild(child, removeFn?)

Attach a BasePart or GUI primitive. Optional removeFn is invoked on :Destroy().

container:Clone() -> ClonableContainer

Deep-clone the container. Every child is re-spawned and populated with the same:

  • CFrame, Size, Color, Render, Lit, CastShadow, ReceiveShadow, IgnoreInRaycast (for BaseParts)
  • Position, Size, Color, Transparency, ZIndex, Visible (for GUI primitives)
  • Model + texture references (shared Arcs, so the clone still shares mesh data with the original)
  • Deformed mesh state (shared Arc; further deformations on one don't bleed into the other since deformation rebuilds the Arc)
  • Attached shaders, each with an independent copy of its 16-float param block so updating the clone's params doesn't touch the original's
  • Text content / font reference (for Text primitives)
  • Nested containers (Movable / Sizable) are deep-cloned too — the clones contain fresh inner containers with fresh leaves, structurally matching the original.
  • Clippable parent links are remapped: if a GUI child was clipped by another GUI primitive that's also in the same clone tree, the clone's clip_parent is rewired to the clone of that parent. Clippable parents that live OUTSIDE the clone tree are dropped on the clone (the new primitives don't inherit the original's external clipping).
container:RemoveChild(child) -> boolean

Detach child from the list. Returns true on success. Fires the stored removeFn if present, leaves the child userdata alive.

container:GetChildren() -> { any }
container:Destroy()

Movable

Carries either a CFrame (3D mode) or a Dim Position (2D mode). Writing the property applies the delta to every child, where the delta is measured from that specific child's baseline of the container, recorded when it was added.

Objects.Movable() -> Movable
m.CFrame CFrame

3D-mode only. Writes through as a per-child positional and rotational delta. Errors when the container is locked to 2D.

m.Position Dim

2D-mode only. Writes through as a per-child Dim delta. Errors when the container is locked to 3D.

m.Mode "2D" | "3D" | "Unset" read-only
m:AddChild(child, removeFn?)

When the Movable is empty and the child carries a usable transform (BasePart CFrame, GUI Position, or a nested container of the same mode), the Movable inherits that value as its own current state. The first add is always a no-op move; later adds use the current container state as the baseline.

m:RemoveChild(child)-> boolean

Detach a child. Returns true on success. Fires the stored removeFn if registered, leaves the child alive.

m:GetChildren()-> { any }
m:Destroy()

Sizable

Carries a Size property of either type Vector (3D mode) or Dim (2D mode). Writing it scales every child by the per-axis ratio of Size to the child's baseline container size. Heterogeneous: a tiny child added to a small container scales the same proportionally as a big child added later.

Objects.Sizable() -> Sizable
s.Size Vector | Dim

3D-mode containers accept either a userdata Vector or a Luau native vec3 (the engine's value_to_vector_opt handles the unification). 2D-mode containers accept a Dim. Passing the wrong type for the locked mode errors.

s.Mode "2D" | "3D" | "Unset" read-only
s:AddChild(child, removeFn?)

When the Sizable is empty, inherits the new child's Size (BasePart Vector, GUI Dim, or a nested Sizable's Size) so the next resize applies as a clean ratio from there.

s:RemoveChild(child)-> boolean

Detach a child. Returns true on success. Fires the stored removeFn if registered, leaves the child alive.

s:GetChildren()-> { any }
s:Destroy()

Billboard

World-anchored UI surface. Link GUI primitives to a Billboard with :AddChild(primitive) and they render in the 3D scene at the Billboard's Position instead of on screen. Each linked primitive's Position is its offset (in pixels) inside the Billboard's local canvas; the canvas itself is centered on the world point. Behind-camera Billboards are culled.

A primitive can be linked to at most one Billboard at a time. Re-linking it to a different Billboard errors. Destroying a Billboard unlinks every child — those primitives fall back to normal screen-space rendering at whatever Position they currently hold.

local Objects = import("Objects")
local GUI     = import("GUI")
local Asset   = import("Asset")
local Vector  = import("Primitives").Vector
local Dim     = import("Primitives").Dim

local bb = Objects.Billboard()
bb.Position = Vector.new(5, 3, 0)
bb.Size     = Dim.new(300, 120)

local bg = GUI.Basic.Square()
bg.Size     = Dim.new(300, 120)
bg.Position = Dim.new(0, 0)        -- top-left of canvas
bb:AddChild(bg)

local name = GUI.Basic.Font(Asset.GetAsset("Font", "ui.body"))
name.Position = Dim.new(12, 12)
bb:AddChild(name)

Properties

PropertyTypeDefaultNotes
PositionVector(0, 0, 0)World-space anchor. Canvas is centered on this point's screen projection.
SizeDim(300, 300)Pixel canvas size. Child positions are offsets inside it (0,0 = top-left).
ScaleWithCamerabooleanfalseSee below.
AlivebooleanRead-only.
ChildCountnumberRead-only. Drops dead refs lazily.

ScaleWithCamera

False (default): the canvas is a fixed pixel size on screen. A 300×300 Billboard always covers 300×300 pixels regardless of camera distance — the classic Roblox-style nameplate billboard.

True: the canvas scales with distance like a real 3D object. The pixel size becomes a world-space size at the Billboard's depth, so walking closer makes it bigger and walking away makes it smaller, same falloff as a sprite in 3D.

Methods

bb:AddChild(primitive)method

Link a GUI primitive. Its Position becomes its in-canvas offset. Errors if the primitive is already linked to a different Billboard.

bb:RemoveChild(primitive)method

Unlink. The primitive returns to screen-space rendering.

bb:GetChildren()-> { GuiPrimitive }method
bb:Destroy()method

Mark dead and unlink every child.

Tweening

A Billboard is a valid target for TweenService.new. Tweenable properties:

Movable and Sizable are also valid tween targets:

UIEnvironment

Container for 3D parts that should render on top of the 2D GUI instead of in the world. Drop a BasePart in with :AddChild(part) and the renderer pulls it out of the normal world pass, then redraws it in a follow-up pass that clears the depth buffer first. The effect: the part always wins against world geometry (it appears in front of everything), but still depth-tests against other parts in the same UI overlay (so a closer cube can still occlude a farther one inside the same UIEnvironment).

Use it for 3D-in-a-window UI: inventory previews, character portraits, model turntables in menus, anything where the part is "decoration on the HUD" rather than part of the world.

local Objects    = import("Objects")
local Renderable = import("Renderable")
local Primitives = import("Primitives")
local Heart      = import("RunService").Heartbeat

local ui3d = Objects.UIEnvironment()
ui3d.ZIndex = 10

local trophy = Renderable.BasePart("Cube")
trophy.Size = Primitives.Vector.new(1, 1, 1)
trophy.CFrame = Primitives.CFrame.new(Primitives.Vector.new(0, 0, -3))
ui3d:AddChild(trophy)

Heart:Connect(function(dt)
    trophy.CFrame = trophy.CFrame * Primitives.CFrame.Angles(0, dt, 0)
end)
Position is still world-space. The part's CFrame is interpreted using the same camera the rest of the 3D scene uses. The depth-buffer clear means it'll always win against world geometry, but it still has to be in front of the camera (positive look-direction) or it's culled. For a "3D in a UI window" effect, position the part close to the camera (e.g. Z = -3) so it fills a sensible chunk of the screen.

Properties

PropertyTypeDefaultNotes
ZIndexnumber0Layer order. Higher = renders later (on top of lower ZIndex environments). Affects every child immediately.
AlivebooleanRead-only.
ChildCountnumberRead-only.

Methods

env:AddChild(part)method

Adopt a BasePart into the overlay. The part stops rendering in the world pass on the next frame and starts rendering in the UI overlay pass with the environment's current ZIndex. Idempotent — adding the same part twice has no effect.

env:RemoveChild(part)-> booleanmethod

Unlink the part. It returns to rendering in the world pass at its current CFrame. Returns true if the part was actually in this environment.

env:GetChildren()-> { BasePart }method

Snapshot array of live children, in insertion order.

env:Destroy()method

Mark dead, unlink every child. Children survive at their current world positions and resume rendering in the world pass.

Render order

Pipeline ordering each frame:

  1. Skybox
  2. World 3D parts (depth-tested, deepest first)
  3. 3D particles
  4. 2D GUI items (z-index sorted)
  5. UI overlay 3D parts — in a fresh render pass that clears the depth buffer, sorted by each environment's ZIndex ascending (higher ZIndex draws later, so it appears on top of lower ones).
  6. Optional post-effect pass
One environment per part. A part can be a child of at most one UIEnvironment at a time — :AddChild on a second environment just moves it (no error, but the first environment forgets about it next frame). If you need true multi-targeting, clone the part with ClonableContainer first.