Releases / v0.72.1

v0.72.1 Wear It Well

2026-08-20 Engine

Materials on models, sorted out. A material you put on a model now actually replaces what the model came with, one part of a model can be given its own look from a script, and a model's own textures can be taken out of it as files. Plus a tint that leaves everything else alone, and a sprite you can flip in one line.

A material you apply is the material you get

Putting a Material on an imported model and pointing it at a texture left the model wearing its old picture. Its colour tinted, its emissive glowed, its roughness took — everything except the thing you could see.

A Material on a model now supersedes the materials the model was imported with. Every part draws with it, textures included, so a material naming no texture draws untextured. That is what the component is for: this whole thing is made of THIS. A model that already looks right needs no Material at all.

To change one part instead, use the model's own material list — or, now, a script.

On upgrade: a model wearing a node Material will look different, because it is finally wearing it. If you had one on there to nudge a colour or add a normal map, either move it to the parts you meant (◑ Model materials in the Inspector) or replace it with a tint, below.

Dressing one part of a model, from a script

lua
for _, slot in ipairs(node:materials()) do
  print(slot.material, slot.object, slot.textured, slot.overridden)
end
-- Clothing  Torso#2    true  false
-- Head      Head#2     true  false
-- Pants     RightLeg#2 true  false

node:material("Clothing").texture = "art/shirt.png"
node:material("Pants").texture    = "art/jeans.png"

node:materials() says what a model's parts are called; node:material(name) is one of them, read and assigned. Every field of a material is there — the texture and the surface maps by path, color / emissive / specular / rim as colours, and alpha, roughness, metallic, emissiveStrength, unlit, fog, cell as values.

Ask before you address: a model that repeats an object name has it renamed on import (Torso becomes Torso#2), so a guessed name reaches nothing. Every part answers to two — its object, which is exactly one part, and its material, which is every part wearing it. A character's Clothing is usually its torso and both arms, which is the group a clothing system wants.

A material's fields read back now, too. mat.texture used to answer nil however many times it had been set, so a script could tell a material what to wear and never ask.

Taking a model's own textures out of it

A .glb keeps its images inside itself, where nothing else can point at one. Select a model — or its node — and press ⬇ Extract textures: each material's image is written beside the model as <model>_textures/<material>.png, and from then on it is an ordinary project texture. Paint over it in the 🖼 Image tab, assign it to anything, use it as the base layer a costume is drawn on.

Overriding one part of a textured model extracts that part's texture on the spot and starts the override wearing it, so "override" never means "go blank".

The same model, but red

lua
node:setTint(color(1, 0.3, 0.3))     -- hit flash
node:setTint(teamColor)               -- one prefab, four teams
node:setTint(color(1, 1, 1), 0.4)     -- ghosted while being placed
node:setTint()                        -- back to normal

A tint is multiplied over whatever a node already draws, so the model keeps its own textures and each part keeps its own colour. It needs no Material and does not create one. In the Inspector it is the ◐ tint swatch beside a model's material list; white is no tint.

It is a component, so an animation clip can key it — a hit flash is a tint faded back to white over a fifth of a second — and a script can read it back with node:getcomponent("Tint").

Flipping a sprite is one line

lua
local sp = node:sprite()
sp.flipX = mx > 0

node:sprite() is a Sprite's own drawing numbers as something you hold: flipX, flipY, cell, ppu, size, pivotX, pivotY, each readable and assignable. Reads answer with what you last wrote, so asking which way a character is facing is a question you can now ask.

vec3 and vec2 also answer to :magnitude(), the name most engines use for :length().

A table of options written as a list is now refused. node:setSprite{ 8, 1, true } set nothing at all and said nothing about it — every option is read by name. It now tells you so, and names the keys it reads. This applies to every call in the API that takes an options table.

Shots and headless runs tell the truth

floptle shot drew a scene in the textures its models were imported with, whatever their materials said, and photographed planets before their ground had loaded. It waits for the world now, and says on the error stream when it ran out of patience rather than handing you a finished-looking picture of an unfinished world.

floptle run reported the full span of simulated time even when it had simulated none of it — a paused session steps happily at a dt of zero. It reports the time it actually advanced, and says when the two differ. Runs of a project with generated terrain now advance at all.

Games that build their world as the player travels can ask terrain.busy() whether the background worker is occupied — true from the moment a fill is queued, so building one system and waiting for quiet before the next is a loop you can write.

Smaller things

  • A library script has its settings before anything calls it. A script with no start and no update — one that exists so others can call into it — held params = nil until something happened to tick it, so the first call into it raised. Whether it raised depended on the order nodes sat in the scene file.
  • ⊕ Transform ▸ … ▸ Reset transform puts a node back to the origin, no rotation, scale 1. Copy and paste values were already there.
  • The right edge of the panels is no longer under the scrollbar. A slider's label could be cut to its first letter, and the menus sat half over the edge.
  • node:getcomponent("Sprite").flipX reads back as a boolean. It was a number, and 0 is true in Lua — so "is it flipped" was always yes.
  • Headless runs no longer warn that perfectly good models have nothing to draw.

Upgrading

Open your project once and the editor updates its editor-support files.

The one thing to check is any model with a Material on the node: it now wears that material instead of blending with what it came with. The Inspector's ◑ Model materials list is where to put it back per part, and ◐ tint is what to reach for if what you wanted all along was "the same model, but coloured".

Floptle Hub

Linux x86_64 7.5 MB Download → sha256 0d412bb8ed80e10ed0e85fb53f09f6b1b8ed0c55ba0e61f886c8041e77311241
macOS Apple Silicon 5.7 MB Download → sha256 3dfb7fdc37fb8bd882ef665f7269d6f906e9ae7bf76b890cb5e768c730bd8aa3
macOS Intel 6.2 MB Download → sha256 cbcf4b6632b84efe8dd9a773dffac32ef03815c8c7795decdc16747ed94af67f
Windows x86_64 7.1 MB Download → sha256 3e082f327a119174d7b127f5a644a9e774f8601d92b394e3941ee4defa66b186

Engine

Linux x86_64 20.7 MB Download → sha256 54b0f0dede6213f41faa104cc8f68a72b5604728f0545cc3e7dfe51c11f78c2c
macOS Apple Silicon 16.7 MB Download → sha256 0107cefa72356290cde3f77acbe931ae27e503d4bad9e179dbcdb59ab29e12b9
macOS Intel 18.0 MB Download → sha256 0e232a7ddd1495265bd02e95d69a3721640ce41b0f711c956814560388e39045
Windows x86_64 20.4 MB Download → sha256 137b23b05c22bdc345d360412674047a5619b5f72900e6ab6e71c00714edadd4

The binaries are not signed yet. On macOS, right-click the app and pick Open the first time. On Windows, if SmartScreen appears, pick More info then Run anyway.