122 lines
7.5 KiB
Markdown
122 lines
7.5 KiB
Markdown
# Level editor – texture tools (`editor.tools.textures`)
|
||
|
||
Plugin `TexturesEditorPlugin` (phase 3). User guide: `editor-guide.md#textures`.
|
||
|
||

|
||
|
||
## What it adds
|
||
|
||
| Item | Where | |
|
||
|---|---|---|
|
||
| Texture manager | panel `textures` (bottom dock, Ctrl+Shift+T; **Window** button = own 1300×760 frame) | pages, atlas, table, details, animation editor |
|
||
| Import texture… | File menu (replaces the built-in `file.importTexture`), panel toolbar | `ImportDialog` |
|
||
| Replace texture pixels… | Edit menu, panel toolbar | image → the selected texture's rectangle |
|
||
| Paint texture | tool `texpaint`, key **M** (own toolbar group) | `PaintTool` |
|
||
| Texture eyedropper | Edit menu, key **I**; Shift+click in the paint tool | |
|
||
| Checks | Build/Check | > 127 pages = error, entry on a missing page = error |
|
||
|
||
`TexState` holds the window's *current texture* (shared by panel, tool, eyedropper; selecting a
|
||
`TextureEntry` anywhere, e.g. in the outliner, makes it current).
|
||
|
||
### Texture manager (`TextureManagerPanel`)
|
||
|
||
* **Pages**: thumbnail, index, blend class of the page format (`Blend.ofPage`). Context menu:
|
||
export page PNG, replace page from PNG (scaled to 256×256, reduced with the page's class).
|
||
* **Atlas**: the page (nearest-neighbour, fit to the view until the wheel zooms) with all
|
||
rectangles: white = used, red = unused, cyan = animated, orange = current with 8 handles.
|
||
Drag inside the current rectangle = move, handles = resize (one undo step per drag:
|
||
`TextureLibrary.setRect` with a merge key). Click selects the smallest rectangle under the
|
||
cursor; double-click activates the paint tool; tooltips show size/usage.
|
||
* **Table**: #, preview, name (editable), page, rectangle, blend, faces (tile-model faces +
|
||
brushes), models, animation ("frame" = used as a frame). Filters: all / on this page / used /
|
||
unused / animated / imported; search by name or number.
|
||
* **Texture tab**: name, X/Y/W/H spinners, blend class (sets the entry flags), raw flags,
|
||
usage line, *Duplicate* (new entry with the same rectangle, e.g. for frames), *Select users*
|
||
(tiles whose models use it).
|
||
* **Animation tab** (`AnimEditor`): create/remove, type flip-book / scroll up / scroll down,
|
||
frame delay (≈ fps at 30 game frames/s), back and forth, frame list (+ − ◀ ▶), scroll band /
|
||
speed / top row, live preview (`AnimSpec.frameAt/offsetAt`).
|
||
|
||
Usage counts (`TextureLibrary.usage`): faces of TRAK tile models (`faces[i+4]`), brush string
|
||
parameters `"#n"`, animation targets and frames. Object (STPC) models are not counted – their
|
||
textures are in the same table, so "unused" means *not used by tiles/brushes/animations*.
|
||
|
||
### Import (`ImportDialog`, `ColorReduction`, `TextureLibrary.importItems`)
|
||
|
||

|
||
|
||
* Size = longest side (16…256 or original), aspect kept; rectangles need not be powers of two.
|
||
* **Blend classes** (`Blend`), derived from the flag pairs of all original level WADs
|
||
(entry flags ↔ page flags & 0x7f, counts over 33 WADs):
|
||
|
||
| Class | entry flags | page flags | originals |
|
||
|---|---|---|---|
|
||
| Opaque | 0x0104 | 0x81 | 16 551 entries |
|
||
| Cut-out | 0x0406 | 0x84 | 1 774 |
|
||
| Semi-transparent (mode 1) | 0x1406 | 0x87 | 528 |
|
||
| Additive (mode 2) | 0x2406 | 0x85 | 649 |
|
||
| Darken (mode 3) | 0x3406 | 0x82 | 574 |
|
||
|
||
Others seen: 0x1104/class 6 (186), 0x2104/class 3 (38), 0x4406/class 5 (469), object-model
|
||
entries 0x8cc6/0xbbc6/0x9cc6/0xb8c6 (bit 15). Entry bits 12–14 = blend mode, 0x400 = has
|
||
transparent pixels.
|
||
* **Colour reduction** to ABGR1555: alpha < cut → 0x0000; with "keep half-transparent" alpha
|
||
< 192 → STP 0 (black becomes 0x0421, never 0); optional colour key (magenta / top-left pixel)
|
||
and 4×4 ordered dithering; opaque class ignores alpha. Preview = the game's 15-bit result.
|
||
* **Packing**: with "pack into free space" the importer searches pages of the same format
|
||
class for a free block (`FreeMap`, summed-area table over a 256×256 occupancy mask built from
|
||
all rectangles plus scroll bands), 8-pixel grid, largest images first; otherwise / if full it
|
||
appends pages (≤ 127). Optional 1-pixel border (edge pixels repeated) against filtering seams.
|
||
One undoable `Batch`: `SetProp pixels` of changed existing pages (the project stores only the
|
||
changed pixel runs), `AddNode` pages, `AddNode` entries (always appended – faces reference the
|
||
index).
|
||
* Village test: two 32×32 imports land in free space of existing pages, a 200×100 semi-transparent
|
||
one gets a new 0x87 page; the WAD builds and re-imports (`TextureLibraryTest`).
|
||
|
||
### Paint tool (`PaintTool`)
|
||
|
||

|
||
|
||
* Hover shows the face under the cursor (white outline) and model/face/texture/flags in the
|
||
status bar (plus "model shared by n tiles"). Face picking: `ctx.pickNode` → tile, then a
|
||
Möller–Trumbore ray test against that tile model's triangles (tile transform, export space).
|
||
* Click = assign the current texture; scope **Face / Same texture in model / Whole model**.
|
||
Shift+click or the eyedropper button / **I** = pick. The clicked face stays selected (orange).
|
||
* **Rotate** cycles the face's corners (v0,v1,v2 → v1,v2,v0; winding and plane unchanged),
|
||
**Mirror** toggles 0x20 (mirror U), **Other half** cycles 0x800/0x10 (`Geometry.faceUvs`: no
|
||
0x800 = corners (a,v1)(b,v1)(a,v0); 0x800 = (a,v1)(b,v1)(b,v0); 0x810 = (b,v0)(a,v0)(a,v1)).
|
||
* **Unique**: if the tile's model is shared, the edit first adds a copy of the model and points
|
||
the tile at it (one undo step).
|
||
* Brushes: the clicked side's normal (from three depth samples) picks the parameter `top`
|
||
(n.y > 0.7) / `bottom` / `side`, else the first texture parameter (`tex`, `post`, `trunk` …);
|
||
the value becomes `"#n"`. Rotate increments `texRot` (0–3) of the last painted brush.
|
||
|
||
## Texture animations (decoded, `AnimSpec`)
|
||
|
||
Loader FUN_00406e40 (16-byte runtime records at 0x581138, count 0x581134), update FUN_0040c6e0,
|
||
called once per rendered frame from FUN_0054b590 while the game is not paused. The runtime
|
||
texture record (20 bytes at 0x581154 + tex·0x14) holds the page and the UVs as floats
|
||
(+0xc / +0x10 = v0 / v1, divided by the page height at 0x58114c + page·0x20 + 8).
|
||
|
||
| Type | File payload | Behaviour |
|
||
|---|---|---|
|
||
| 8 flip-book | delay, n, pingPong, n × u16 texture | every delay+1 frames the texture's runtime record is replaced by the next frame's (loop or back and forth). 214 in the levels (torches, water, flags), e.g. `03 08 00 d0 03 …` |
|
||
| 1 / 2 scroll | band−1, speed, v0 | V range = v0+off … v0+off+band, off −= / += speed per frame, wraps in the band. 7× type 1, 2× type 2 (rivers, lava: `1f 02 01`) |
|
||
| 10 / 11 | band−1, speed, v0, n, n × u16 | scroll as 2 / 1, each wrap switches to the next / previous frame. Unused |
|
||
| 7 / 9 | u16 palette, u16 delay, u8 first, u8 last | palette cycling of 8-bit pages. Unused (no palettes) |
|
||
|
||
The editor viewport does not animate textures; the Animation tab previews them.
|
||
|
||
## Known limits
|
||
|
||
* Texture entries are never deleted (indices are referenced by faces and object models);
|
||
unused ones can be reused by moving their rectangle.
|
||
* Object (STPC) model faces cannot be painted.
|
||
* Painting a shared original tile model changes every tile using it unless **Unique** is on.
|
||
* The page format of an existing page is not changed by the importer; importing into a page of
|
||
another class is avoided.
|
||
|
||
Files: `src/main/java/com/eliasmueller/llamamod/editor/tools/textures/*`, tests
|
||
`TextureLibraryTest` (colour reduction, anim round trip of all village animations, packing,
|
||
build + undo), tour steps `TexTourSteps` (with `playtest.EdTour`).
|