Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions MODULES.md
Original file line number Diff line number Diff line change
Expand Up @@ -502,10 +502,21 @@ if (api.quality) {
if (api.lod) {
const lod = api.lod(enemyFigure, { ratios: [0.5, 0.2], distances: [6, 18] });
lod.ready.then((n) => console.log(n, "meshes have levels")); // lod.remove() undoes it
lod.force?.(2); // 1.19: draw level 2 on THIS screen whatever the distance (null = auto)
lod.levels?.(); // 1.19: the level each mesh drew last (0 = full)
}
mesh.userData.lod = false; // keep one mesh out of auto LOD
```

**1.19 — LOD groups on scene objects.** A replicated object may carry a LOD GROUP
(`userData.lod`, edited in its Properties ▸ LOD section, or written by a pack item's `lods`):
`{mode: 'auto'|'forced', forced?, bias?, cull?, levels: [{source: 'self'|'pack'|'generated'|
'explorer'|'object', ref?, ratio?, screenSize, offset?, material?}]}` with thresholds by SCREEN
SIZE (the share of the viewport height the object covers). It is scene data (saved, replicated,
undone) and drawn at render time like `api.lod` — the tree never holds a level. A module that
builds scene objects can write one through the same path the panel uses
(`objectParameters {parameter: 'lod'}` is the wire shape); module-only geometry keeps `api.lod`.

Both are LOCAL (a fact about this machine) — never let them change replicated state, or two
peers on different hardware disagree about the game. Skinned meshes and morph targets get no
levels (the simplifier cannot carry weights): pre-decimate those offline.
Expand Down
29 changes: 29 additions & 0 deletions PACKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,35 @@ CC0, OpenGameArt CC0). Keep each file under the 5 MB share cap so it round-trips
peers. Installed audio items appear in the Explorer library and can be assigned to
a **Sound** node (spatial) or the **Scene music** channel (global).

### Levels of detail: the `lods` field (contract P1, 1.19)

A model item in an item list (the `default.json` row beside `variants`) MAY list offline
LOD files:

```jsonc
{ "name": "WallStone", "label": "Wall — sandstone (2 × 3 m)",
"variants": { "glTF-Binary": "wall-stone.glb" },
"lods": [ { "file": "wall-stone.lod1.glb", "ratio": 0.5 },
{ "file": "wall-stone.lod2.glb", "ratio": 0.2 } ] }
```

- `file` is relative to the item's `glTF-Binary/` folder (no `..`, no absolute URLs);
`ratio` is the level's triangle count as a share of LOD0 (informational — the app sorts
levels finest first by it and shows it in the LOD panel).
- **Keep the node names and hierarchy of LOD0.** The app matches each level mesh to the
placed object's mesh BY NODE NAME (falling back to traverse order when both have the same
mesh count) and draws the level's GEOMETRY with the object's OWN material — so a level
file's materials and textures are not used at all (ship them shared or tiny), and an
animated item keeps animating, because the mixer still drives the same nodes.
- Bake the same node transforms as LOD0 where you can; a level mesh placed differently is
re-expressed in LOD0's frame on load, which costs a geometry copy per placement.
- Placing an item writes its group into the object (`userData.lod`, refs as
PACKS_BASE-relative paths), so it saves, replicates and undoes with the object. A piece
placed BEFORE its pack gained `lods` picks them up from the pack row at runtime.
- Levels are fetched LAZILY — only when an object is small enough on screen to need one —
and parsed once per file for every placement. Items without `lods` keep the automatic
runtime LOD (meshoptimizer, for meshes of 3000+ triangles).

## Repo / .zip structure

```
Expand Down
12 changes: 9 additions & 3 deletions src/App.svelte

Large diffs are not rendered by default.

14 changes: 13 additions & 1 deletion src/components/Scene.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import { drawMode, drawTool, strokePointFromRay, endStroke, setDrawScene } from '$lib/drawMode';
import { splinePlaceFromRay, splineToolActive, finishSpline } from '$lib/splineTool';
import { flattenPicking, flattenPickClick } from '$lib/flattenActions';
import { onLodProxyMoved, onLodProxyDragChanged } from '$lib/lodLevelEdit';
import { splineEditObject, splineEditClick, splineEditRightClick, exitSplineEdit, beginRadiusDrag, radiusDragMove, endRadiusDrag, radiusDragActive, onSplineProxyMoved, onSplineProxyDragChanged, tickSplineEdit } from '$lib/splineEdit';
import { capturePathClick } from '$lib/pathCapture';
import { surfaceSnap, dropToSurface } from '$lib/snapping';
Expand Down Expand Up @@ -424,7 +425,7 @@
// multi-select pivot excludes EVERY member (P3: its drags snap too).
if (!event.value) endSnapDrag();
else if (object.userData?.isMultiPivot) beginSnapDrag([...$selectedObjects]);
else if (!object.userData?.isVertexProxy && !object.userData?.isFaceProxy)
else if (!object.userData?.isVertexProxy && !object.userData?.isFaceProxy && !object.userData?.isLodLevelProxy)
beginSnapDrag([object.uuid]);
// vertex handles record their own history entries
if (object.userData?.isVertexProxy) {
Expand All @@ -441,6 +442,11 @@
onSplineProxyDragChanged(event.value);
return;
}
// 33: one LOD level's offset — ONE lod write per drag, never a move
if (object.userData?.isLodLevelProxy) {
onLodProxyDragChanged(event.value);
return;
}
// the multi-select pivot records per-member entries (multiTransform)
if (object.userData?.isMultiPivot) return;
if (event.value) {
Expand Down Expand Up @@ -1492,6 +1498,12 @@
onSplineProxyMoved();
return;
}
// 33: a LOD level's proxy — the level follows locally; the release commits
if ($TControls.object?.userData?.isLodLevelProxy) {
$TControls.visible = true;
onLodProxyMoved();
return;
}
// multi-select pivot: multiTransform drives + broadcasts the members,
// the pivot itself is local-only (its uuid means nothing to peers)
if ($TControls.object?.userData?.isMultiPivot) {
Expand Down
8 changes: 6 additions & 2 deletions src/components/editors/Explorer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5292,7 +5292,9 @@
kind: item.kind,
name: item.name,
prefabId: item.prefabId ?? null,
url: item.glbUrl ?? null
url: item.glbUrl ?? null,
// 33: a pack item's LOD files travel with the drag so the drop places the group
...(item.lods ? { lods: item.lods } : {})
};
}
/**
Expand Down Expand Up @@ -5702,8 +5704,10 @@
return showToast('Could not fetch the pack item');
}
// 30c: the placed piece carries its pack reference (packRefs.js)
const { placementGroupFor } = await import('$lib/lodGroup');
await importFile(new File([await res.blob()], item.name + '.glb'), item.name, 'glb', undefined, undefined, {
packRef: packRefFromUrl(item.glbUrl, { pack: item.packName, item: item.name })
packRef: packRefFromUrl(item.glbUrl, { pack: item.packName, item: item.name }),
lod: placementGroupFor(item.glbUrl, item.lods)
});
dismiss();
} catch {
Expand Down
8 changes: 8 additions & 0 deletions src/components/menu/Inspector.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import ThemedSelect from '../ui/ThemedSelect.svelte';
import PanelHeader from '../ui/PanelHeader.svelte';
import Section from '../ui/Section.svelte';
import LodGroupPanel from './LodGroupPanel.svelte';
import SliderRow from '../ui/SliderRow.svelte';
import DragRow from '../ui/DragRow.svelte';
import ColorPicker, { ChromeVariant } from 'svelte-awesome-color-picker';
Expand Down Expand Up @@ -3202,6 +3203,13 @@
</Section>
{/if}

{#if !isLight && !multiCount && $selectedObject?.uuid}
<!-- 33 (K6): the object's LOD GROUP — levels, transitions, Force LOD, edit a level -->
<Section label="LOD">
<LodGroupPanel uuid={$selectedObject.uuid} />
</Section>
{/if}

{#if devPrimary}
<Section label="Device">
<!-- 23-B4: the device's declared params, fanned over the selection; presets and
Expand Down
Loading
Loading