309 lines
22 KiB
Markdown
309 lines
22 KiB
Markdown
# Level editor – render package (`editor.render`)
|
||
|
||
Phase 1 of the level editor (notes/editor-design.md): OpenGL viewport, level rendering, overlays,
|
||
camera, picking, transform gizmo. Package `com.eliasmueller.llamamod.editor.render`; it has no
|
||
dependency on `editor.model`/`editor.io` – the UI adapts the scene model to `RenderScene`.
|
||
|
||

|
||
|
||
## Build / platform
|
||
|
||
* Gradle: LWJGL **3.4.2** (`lwjgl`, `lwjgl-opengl`, `lwjgl-jawt`, `lwjgl-egl`) + **lwjgl3-awt 0.2.5**
|
||
(`AWTGLCanvas`, a heavyweight AWT canvas inside Swing; `transitive = false`, its POM pulls
|
||
Vulkan/macOS natives) + **JOML 1.10.8** (math). Natives `natives-linux` and `natives-windows`
|
||
of `lwjgl` and `lwjgl-opengl` are runtime dependencies, so the fat jar (and with it both packages)
|
||
carries `linux/x64/…/*.so` and `windows/x64/…/*.dll`; LWJGL extracts them to a temp dir at runtime.
|
||
Fat jar 9.7 MB.
|
||
* Fat jar manifest gets `Multi-Release: true` (LWJGL is a multi-release jar), `module-info.class`
|
||
files are excluded. jlink modules + `jdk.unsupported` (LWJGL/JOML use `sun.misc.Unsafe`).
|
||
`packageLinux`/`packageWindows` otherwise unchanged.
|
||
* **OpenGL 3.3 core profile, no 2.1 fallback** (decision): shaders are GLSL 330, texture pages are
|
||
one `GL_TEXTURE_2D_ARRAY`, VAOs/VBOs. 3.3 is supported by every GPU/driver since ~2010 (Intel HD
|
||
2000+, Mesa llvmpipe gives 4.5); a 2.1 path would double the renderer for hardware that cannot run
|
||
the rest of the toolchain comfortably anyway. The canvas first asks for 4× MSAA and retries
|
||
without multisampling; if no 3.3 context can be created the viewport shows a message instead
|
||
of crashing (`Viewport.glError()`). No swap interval is requested (Xvfb/GLX has no swap control,
|
||
lwjgl3-awt then fails context creation).
|
||
* Tested:
|
||
* Linux native (Xvfb + llvmpipe, `DISPLAY=:8`): fat jar and the jlinked `LlamaMod-linux-x64`
|
||
runtime – rendering, picking, gizmo drags, fly mode via xdotool.
|
||
* **Windows package under Wine 6.0.3** (own win64 prefix, `JDK_JAVA_OPTIONS=-XX:ActiveProcessorCount=4`,
|
||
`runtime\bin\java.exe -cp LlamaMod.exe …ViewerDemo`): Wine's WGL gives a 3.3 core context on the host
|
||
llvmpipe; the screenshot is **pixel-identical** to the Linux one. `LlamaMod.exe --mods` still works.
|
||
Real Windows hardware not tested.
|
||
* `-Dllamamod.editor.msaa=0` disables multisampling (2–3× faster on software GL).
|
||
|
||
## Demo
|
||
|
||
```
|
||
java -cp build/libs/llamamod-*-all.jar com.eliasmueller.llamamod.editor.render.ViewerDemo \
|
||
<level.wad | testworld> [--cd DIR] [--size 1280x720] [--collision] [--no-markers]
|
||
[--hide waypoints,zones] [--view top|front|side] [--cam x,y,z,yawDeg,pitchDeg]
|
||
[--select o12,t5,w3] [--frame] [--gizmo translate|rotate|scale] [--shot out.png] [--bench] [--list]
|
||
```
|
||
`testworld` builds the Testwelt (TestWorldMod) from the CD's village WAD. The window has layer
|
||
check boxes, view and gizmo mode selectors and a status bar (hover/pick info). `--list` prints
|
||
all strats with ids and positions. `--bench` orbits 120 frames and prints timings.
|
||
|
||
## Controls (Viewport)
|
||
|
||
Navigation presets (`Navigation.Preset`, View › Navigation, Preferences › Viewport & navigation,
|
||
Help › Viewport navigation; setting `viewnav.preset`, default **Fusion 360**):
|
||
|
||
| Input | Fusion 360 (default) | Blender | Unity / Unreal |
|
||
|---|---|---|---|
|
||
| middle drag | pan | orbit | pan |
|
||
| Shift+middle drag | orbit | pan | pan |
|
||
| Ctrl+middle drag | zoom | zoom | zoom |
|
||
| double-click middle | fit all | – | – |
|
||
| Alt+left drag | orbit | orbit | orbit |
|
||
| right drag | look around + **W/A/S/D** move, **Q/E** down/up, **Shift** ×4, wheel = fly speed (all presets) |||
|
||
| Alt+right drag | zoom (all presets) |||
|
||
| wheel | smooth exponential zoom towards the cursor (all presets) |||
|
||
|
||
| Input | Action |
|
||
|---|---|
|
||
| view cube (top right) | click a face / edge / corner = look from there (animated, pivot and projection kept), drag = orbit, house = home (iso view from front-right-top + frame all) |
|
||
| F / Home | frame selection / frame all tiles (smooth 220 ms transition) |
|
||
| numpad 7 / 1 / 3 / 5 | top / front / side (orthographic) / toggle perspective |
|
||
| W / E / R, Q (when not flying) | gizmo translate / rotate / scale, toggle world/local |
|
||
| left click | pick (listener); left drag on a gizmo handle = transform; Ctrl = invert snapping; Esc = cancel drag |
|
||
|
||
* **Zoom** (2026-11-07): constant percentage per wheel notch (`zoomFactor`, default 12 %, 2–40 %,
|
||
"invert zoom"), fractional touchpad deltas scale smoothly, animated 120 ms (ease-out; further
|
||
notches accumulate on the target). Anchor (`anchorDistance`): the depth-buffer hit under the
|
||
cursor if it lies in front of the near plane and closer than 3× the pivot depth (and the far
|
||
plane), else the current pivot depth along the cursor ray – pointing at the sky or a far cliff
|
||
no longer makes a huge step, a near wall no longer stalls. The anchor becomes the new pivot depth.
|
||
Old behaviour: a fixed `0.15 × distance` dolly step along the ray, independent of what is under
|
||
the cursor (sky/near ground felt extremely fast/slow).
|
||
* **Orbit**: pivot = surface point under the cursor at drag start (fallback: selection centre,
|
||
then view centre), shown as a small RGB cross; the camera keeps its camera-space offset to the
|
||
pivot, so the pivot stays under the cursor; constant angular speed (default 0.35 °/px), pitch
|
||
clamped to ±89°. `Camera.viewMatrix` now uses the mirrored yaw direction as up vector when looking
|
||
straight **up**, so pitching across the vertical threshold no longer flips the picture.
|
||
* **Pan** moves the grabbed point (its view depth) exactly with the cursor (sky: pivot depth).
|
||
* Viewport API: `setNavigation/zoomAt/setZoomSpeed/setInvertZoom/setOrbitSpeed/setViewCubeVisible/
|
||
home/snapView`; math in `Navigation` and `ViewCube` (pure, `NavigationTest`, `ViewCubeTest`).
|
||
Plugin `tools.viewnav.ViewNavEditorPlugin` (menus, preferences dialog, cheat sheet; the status
|
||
bar shows a one-line summary on open and when the preset changes).
|
||
|
||
Orbiting a standard ortho view switches back to perspective. Hover picking runs at most every
|
||
40 ms while the mouse moves (`setHoverPicking(false)` turns it off).
|
||
|
||

|
||
|
||
## API
|
||
|
||
```java
|
||
RenderScene scene = LevelSceneAdapter.fromWad(wadBytes).scene(); // or built by the editor UI
|
||
Viewport vp = new Viewport(); // JPanel; add to any Swing layout
|
||
vp.setScene(scene);
|
||
vp.setGrid(0, new float[] {0, -H, W, 0}); // grid plane y and bounds (x0, z0, x1, z1)
|
||
vp.setLayerVisible(LevelSceneAdapter.LAYER_COLLISION, true);
|
||
vp.setSelection(List.of(PickId.of(PickId.OBJECT, 12))); // highlight (+ unbatched tiles)
|
||
vp.gizmo().setVisible(true); vp.gizmo().setTarget(matrix); vp.gizmo().setMode(Gizmo.Mode.ROTATE);
|
||
vp.gizmo().setSnap(new Gizmo.Snap(true, 0.25f, 15f, 0.1f)); // grid snapping hook
|
||
vp.addViewportListener(new ViewportListener() {
|
||
public void picked(PickEvent e) { … e.id(), e.world(), e.modifiers(), e.clickCount() … }
|
||
public void hovered(int id) { … }
|
||
public void gizmo(GizmoEvent e) { … } // START / DRAG / END / CANCEL
|
||
public void cameraChanged(Camera c) { … }
|
||
});
|
||
vp.frameAll(); vp.frameSelection(); vp.setView(Camera.View.TOP);
|
||
vp.pick(x, y); vp.surfacePoint(x, y); // synchronous id buffer / depth reads
|
||
vp.screenshot(); vp.stats(); vp.glInfo(); vp.requestRender(); vp.dispose();
|
||
```
|
||
|
||
### Scene data (render-facing interface)
|
||
|
||
* `RenderScene`: `pages` (`TexturePage(w, h, int[] 0xRRGGBBAA)`), `instances`, `markers`,
|
||
`overlays` (world-space `Overlay` per layer name), `background` (0xRRGGBB, also fog colour).
|
||
After edits: `changed(instance)` (rebuilds only that instance's chunk), `changed()` (everything,
|
||
markers/overlays), `texturesChanged()`. Modify on the Swing thread.
|
||
* `RenderMesh`: triangle mesh in local space – per vertex positions/normals, per triangle
|
||
corner UVs (normalised to the page), per triangle page (-1 = untextured), colour (multiplied,
|
||
flat colours / translucency), flags (`DOUBLE_SIDED`, `NO_VERTEX_COLOR`, `LIT` = shade with
|
||
normals) and blend class (`BLEND_OPAQUE` = alpha-tested cut-out, `BLEND_ALPHA`, `BLEND_ADD`).
|
||
Optional `overlay` = local-space helper geometry that follows every instance (collision).
|
||
* `RenderInstance(id, kind, mesh)`: `transform` (JOML `Matrix4f`), `vertexColors` (game
|
||
convention 0x80 = 1.0, doubled with clamp), `tint`, `visible`, `layer`. Kind `TILE` instances are
|
||
baked into static batches; `OBJECT`/`PREVIEW` are drawn individually with their matrix.
|
||
* `Marker`: pickable icon (cube/diamond/flat arrow, pole, direction tick, colour, layer, label).
|
||
* `Overlay`: coloured lines + translucent triangles (`box`, `line`, `triangle`).
|
||
* Pick ids are plain ints, 0 = nothing; `PickId` is the convention used by the adapter
|
||
(kind << 24 | index: TILE, OBJECT, WAYPOINT, ZONE, LIGHT, USER…).
|
||
* `Legend`: collision colours by material/slope and marker category colours, same as
|
||
notes/debugviz.md (`Legend.category(stratName)`).
|
||
|
||
### Gizmo
|
||
|
||
`Gizmo` is pure math (unit-tested in `GizmoTest`): modes TRANSLATE (3 axes, 3 planes, centre =
|
||
screen plane), ROTATE (3 rings, back halves faded, edge-on rings fall back to screen angle), SCALE
|
||
(3 axes in local space, centre = uniform). World or local space (scale always local). Handles
|
||
keep a constant screen size (`sizePx`, default 95). Hit testing in screen space (9 px).
|
||
`GizmoEvent` carries the **total** delta since drag start (translation, axis+angle about the start
|
||
pivot, scale factors) plus the start matrix: `e.result()` = new target transform, `e.applyTo(m)` for
|
||
other selected objects. The UI shows a live preview on DRAG, emits one undoable command on END
|
||
and restores `e.start()` on CANCEL; it calls `gizmo().setTarget()` so the handles follow
|
||
(axes stay as at drag start). Snapping: `Snap(enabled, translate step, rotate degrees, scale step)`,
|
||
Ctrl inverts `enabled` while dragging.
|
||
|
||
## Coordinate conventions (handedness fix, 2026-11-06)
|
||
|
||
* **The view is left-handed like the game's Direct3D renderer.** The engine draws export-space data
|
||
(tile floats, TRAK/STPC vertices, strat (x, y, −z)) with a D3D camera, so a plain OpenGL view of
|
||
the same coordinates was the game's picture mirrored left-right (owner report: "levels mirrored").
|
||
`Camera.viewMatrix()` = `scale(−1, 1, 1) · lookAt(…)`; the renderer culls with `glFrontFace(GL_CW)`;
|
||
data, picking (`ray`/`project` use the matrices) and the scene model are unchanged.
|
||
* `Camera.yaw/pitch` equal the game camera angles (0x5790a4/0x5790a0, 4096 = 2π): yaw 0 looks along
|
||
game +z (export −z), yaw 90° along +x, **positive yaw turns left**. `forward()` = (sin y·cos p,
|
||
sin p, −cos y·cos p) (export), `right()` = screen right = (−cos y, 0, −sin y) (game: (−cos y, sin y),
|
||
as found by the freecam mod), `up()` = forward × right. Fly look/orbit: dragging right decreases yaw.
|
||
`ViewerDemo --cam x,y,−gz,yawDeg,pitchDeg [--fov vDeg]` reproduces a game camera.
|
||
* Rotate gizmo: the edge-on fallback uses the screen angle with the mirrored sign.
|
||
* Standard views: **top** = x to the right, game z (grid rows) downwards (cell (0,0) top left, as the
|
||
MAP/region grid is laid out; yaw π); **front** looks along game −z (x right); **side** looks along −x.
|
||
* Spaces: render/export space = (x, y, −z_game); model and inspector values (strats, waypoints,
|
||
zones, lights, collision) are game space, tiles store export floats. LGHT point lights/directions
|
||
are stored in game space (engine negates z on load). Strat yaw: `rotateY(−ry·2π/4096)` in export
|
||
space with model front = local −z → yaw 0 faces game +z, 1024 faces +x (verified: llama ry 3061 and
|
||
Pacha at his runtime yaw 830 face the same way as in game). Tiles: `rotateY(−rot)`.
|
||
* Evidence: in-game screenshots (apptest + freecam, camera read with winedbg from 0x5790b0 and the
|
||
look-at target 0x6d9d80) vs. ViewerDemo from the same camera: `img/editor/fix_compare_start.jpg`
|
||
(game / before / after), `fix_compare_back.jpg`, `fix_compare_yaw.jpg`, `fix_compare_top.jpg`.
|
||
The game's horizontal FOV matches 60° vertical at 16:9; its vertical FOV is ~15 % smaller
|
||
(non-uniform projection, not reproduced). Regression test: `CameraHandednessTest` (projected
|
||
pixel x of the llama and Pacha against the game screenshots, axis conventions).
|
||
|
||
## Rendering details
|
||
|
||
* **Batching**: tile instances are pre-transformed into 8×8-cell chunks; each chunk is one VBO
|
||
(36-byte vertices: position, normal, UV, colour, pick id, page layer + flags) sorted into 6
|
||
ranges (blend class × double-sided) plus a line VBO (collision outlines/fills). Frustum culling
|
||
per chunk AABB / per object (`Culling`, below). All pages live in one texture array (+1 white
|
||
layer for flat colours), so a chunk needs at most 6 draw calls. Objects use one VBO per mesh.
|
||
Selected tiles are taken out of their chunk and drawn alone, so dragging them never rebuilds a
|
||
chunk; `changed(instance)` on a tile rebuilds its chunk (~20k vertices, a few ms).
|
||
* Passes: opaque (alpha test, back-face culling except double-sided faces), **back sides** (below),
|
||
overlay lines,
|
||
translucent (alpha, then additive; depth write off, not sorted), overlay fills, selection
|
||
(orange tint + hidden-line ghost + wireframe; hover = pale wireframe), gizmo (depth cleared).
|
||
Optional distance fog (40–160 cells) in the level's backdrop colour.
|
||
* **Picking**: id-buffer pass into an offscreen RGBA8/depth FBO (scissored to a 9×9 window, the
|
||
nearest non-zero pixel wins so small markers are easy to hit), depth → world hit point.
|
||
Cut-out texels are not pickable (click through leaves).
|
||
* Textures: TEXT pages ABGR1555 → RGBA, 0x0000 transparent, STP=0 = half alpha on blend pages
|
||
(page class 4/5/7); transparent texels are dilated (no dark fringes), 3 mip levels.
|
||
Blend class per face: texture entry bits 12–14 (1/3 → alpha, 2/4 → additive) or, for mode 0,
|
||
page class (4/7 alpha, 5 additive) – an approximation of the engine's table at 0x573eb8; flat
|
||
colour entries (bit 0) use u0/u1/v0 as RGB, alpha in v1 when a blend mode is set.
|
||
|
||
## Culling and "disappearing objects" (2026-11-07)
|
||
|
||
Owner report: objects, especially large ones, disappear at the screen edge.
|
||
|
||
* **Root cause: not the frustum culling** but the Environment plugin's *fog preview* (on by
|
||
default): it sets a `FogOverride` with `clip` + `aroundPivot`, and the mesh shader **discarded
|
||
every fragment farther than the fog end from the orbit pivot** (radial). The pivot is the view
|
||
centre, so geometry towards the screen edges was cut away, and it popped in/out while panning.
|
||
Evidence: `img/editor/cull_root_fogclip.jpg` (old build, same camera: fog preview on / off – the
|
||
left islands are missing with it on). Fix (render package only): with `aroundPivot` the shader
|
||
draws a desaturated **ghost** beyond the radius instead of discarding (`uClipGhost`); the eye-based
|
||
far clip is unchanged.
|
||
* Frustum culling itself was correct (JOML plane extraction is orientation-independent; the mirrored
|
||
view only swaps the left/right planes). Hardened anyway: `Culling` boxes include every vertex
|
||
**and** the instance's overlay (collision lines/fills – a collision-only chunk had an empty box and
|
||
was always culled), objects use their local box transformed with the full instance matrix
|
||
(rotation/scale, 8 corners), skinned characters are culled with their posed vertices, + a small
|
||
absolute/relative margin. `-Dllamamod.editor.noCull=true` disables culling (diagnostics).
|
||
* Tests: `CullingTest` – random perspective/ortho cameras (any yaw/pitch, fov 30–100°, aspect),
|
||
random rotated/scaled meshes + every village instance: whenever any vertex projects inside the
|
||
clip volume the box must be visible (no false culls). Pixel check: ViewerDemo village, city
|
||
(t1l6m003) and Testwelt from inside the level at 8 yaws each, culling on vs `noCull`: **0
|
||
differing pixels** in all 24 pairs (`img/editor/cull_overview.jpg`).
|
||
|
||
### Back sides of one-sided faces
|
||
|
||
The game culls faces without flag 0x400 from behind; the editor now shows those back sides
|
||
(View › Show back faces, default on; opacity in Viewport preferences, default 30 %): pass 3 after
|
||
the opaque pass draws the single-sided opaque/cut-out ranges again with `glCullFace(GL_FRONT)`,
|
||
depth-tested without depth writes, blended, darkened + desaturated with a fine screen-space
|
||
diagonal hatch. Front faces look exactly as before (the pass only fills pixels whose depth is
|
||
not covered). Translucent one-sided faces (island skirts) get no back side (it showed through them
|
||
everywhere). Picking hits both sides; the shader sets bit 31 of the pick id for a back side
|
||
(`PickHit.backFace`, `Viewport.lastPickBackFace()/hoverBackFace()`), the status bar appends
|
||
"(back side)" to the hovered item. `img/editor/cull_backfaces.jpg` (one-sided cliff walls seen from
|
||
behind: game look / editor).
|
||
|
||
## Level adapter (`LevelSceneAdapter.fromWad`)
|
||
|
||
* Tiles: TRAK model per MAP tile, transform `translation(x, y, z).rotateY(-rot)` (same as
|
||
`Geometry.TileXform`), vertex colours = first colour frame (alt models of WFPC 0x10000 tiles are
|
||
not shown). UVs via `Geometry.faceUvs`, face flags 0x400 / 0x4000 honoured.
|
||
* Collision: every TRAK model's polygons (`Collision.decode/polygon`) as outlines 1 cm above the
|
||
face, coloured as in debugviz (floor green, slope yellow-green, wall blue, ceiling orange,
|
||
materials 16/17/18/30/… fixed colours); fills for material 16 (exit floor) and faintly 17/18.
|
||
Invisible-collision detection (red fill of the in-game debug mod) is not done.
|
||
* Strats: game space → export space (z negated), yaw = ry. **Object models**: the strat code sets
|
||
its model with `0xb2 <abs STPC offset of the model header>; 0x54` – `StratModels` scans the strat
|
||
from its entry to the next strat's name site and takes the first such pair; spawn stubs
|
||
(`b2 <code> e0`) are followed. **Characters** (skinned models, header +0x84 = 0, nBones × u16
|
||
vertex counts) are posed with frame 0 of an animation set with the same bone count (first one
|
||
referenced by `0xb2 <set>` in the strat code, else the first in STPC): bone matrices 4×4 f32,
|
||
row vectors. Result: villagers, kids, Pacha, Tipo, bunnies look right, the llama is shown in a
|
||
crouching pose (frame 0 of its set). Village: 20 of 32 placed types get a model (8 of them
|
||
skinned); the rest are invisible logic objects, except RisingColumns, SwitchPlatformPadVillage
|
||
and AppearingPlatformsVillage, which set their models some other way (shown as markers).
|
||
* Invisible strats (categories of debugviz: start, exit, checkpoint, camera, trigger, force,
|
||
secret, misc) become markers on poles (exit: purple cell box); start/checkpoint additionally
|
||
show their model. Objects without a resolvable model: small grey cube; unposable character:
|
||
grey diamond.
|
||
* Waypoints: every path node a pickable diamond (layer `waypoints`), chains (`next ≠ 0` → i+1)
|
||
and strat → first node as lines. Some chains are point lists across the whole level (e.g. the
|
||
llama's), so the layer gets busy – the UI should show it for the selection only.
|
||
* Environment zones: pink cell boxes + pickable marker (layer `zones`); the default zone's
|
||
backdrop colour (file offset 0x4e, 0x00BBGGRR) becomes `scene.background` (cyan village sky,
|
||
purple river fog, dark blue city).
|
||
|
||
## Performance (1280×720, llvmpipe software GL on this machine, 4× MSAA / no MSAA)
|
||
|
||
| Level | tiles/objects/markers | triangles drawn | draw calls | orbit avg MSAA 4 | no MSAA |
|
||
|---|---|---|---|---|---|
|
||
| village t1l1m001 (+collision) | 1458 inst., 218 markers | 27k | 125–145 | 17–27 ms | 9 ms (109 fps) |
|
||
| river t1l3m002 (+collision) | 1494 / 322 | 22k | 145–170 | 17–40 ms | 10 ms (103 fps) |
|
||
| city t1l6m003 (largest WAD) | 2177 / 439 | 41k | 120–140 | 26–50 ms | 18 ms (55 fps) |
|
||
|
||
(Ranges: several agents shared the CPU.) Scene conversion from WAD 160–290 ms. On any real GPU
|
||
this is far below 1 ms of GPU time; the CPU side is ~150 draw calls per frame.
|
||
|
||
## GPU resources (2026-11-07)
|
||
|
||
* `GlRenderer.dispose()` frees everything it created: batches, texture array, pick FBO, programs and
|
||
the dynamic `dynVao`/`dynVbo` (gizmo, overlays).
|
||
* Chunks that lose their last member are deleted with their batch (before: an empty chunk kept a
|
||
`Chunk` entry until the next full rebuild). Shared per-mesh batches (`meshBatches`, objects and
|
||
tiles without vertex colours) whose mesh no instance uses any more are freed once there are more
|
||
than 256 (limit doubles with the live count) – replaced meshes used to stay until a level switch.
|
||
* `Viewport.liveGlBuffers()` counts live VAO/VBO pairs of all batches (diagnostics).
|
||
`ui.view.GlLeakCheck` (test sources, X display): 3 rounds over Village 1 / world 4 / Village 2, each
|
||
load followed by 60 rounds of tile moves, object moves, delete + undo while rendering → the count
|
||
is identical per level in every round (84 / 87 / 76), 0 with no level loaded.
|
||
* `SceneRenderer` drops the pick ids of nodes that are no longer in the scene every 256 removals
|
||
(nodes that come back by undo get a new id; selected nodes keep theirs) – `SceneRendererIdsTest`.
|
||
Tiles marked "no collision" use a copy of their model mesh without the collision overlay.
|
||
|
||
## Known limits / next steps
|
||
|
||
* Only the first vertex colour frame and the primary tile model (no alt/swap models).
|
||
* Translucent faces are not depth-sorted (as the game does per batch; artefacts are rare).
|
||
* Engine blend table only approximated; no texture animations (TEXT nAnim block), no sky/backdrop
|
||
geometry, no lights (LGHT) yet.
|
||
* Characters: frame 0 of a guessed animation set; models whose pose fails show a diamond.
|
||
* Collision of object models (moving platforms, doors) is not drawn (same as the debug mod).
|
||
* `Viewport.dispose()` plus disposing the frame can hang on Xvfb without a window manager (AWT
|
||
focus round trip); the demo exits with `System.exit` after a shot.
|
||
|
||
Screenshots: `img/editor/01_village.jpg` … `12_viewer_ui.jpg` (village/river/city/Testwelt with
|
||
and without collision, ortho top views, translate/rotate gizmo, the demo window with a selection).
|