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
5 changes: 5 additions & 0 deletions .changeset/wacky-squids-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@jolly-pixel/voxel.renderer": major
---

Introduce a new class to preload Tileset textures and remove async APIs from VoxelRenderer class
38 changes: 17 additions & 21 deletions packages/editors/voxel-map/src/scene/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from "@jolly-pixel/engine";
import {
VoxelRenderer,
TilesetLoader,
type TilesetDefinition,
type VoxelWorldJSON
} from "@jolly-pixel/voxel.renderer";
Expand Down Expand Up @@ -32,7 +33,7 @@ export interface EditorSceneOptions {
}

export class EditorScene extends Systems.Scene {
#texture: THREE.Texture<HTMLImageElement>;
#tilesetLoader!: TilesetLoader;
#defaultLayerName: string;
#defaultTileset: TilesetDefinition;
#pendingLoad: VoxelWorldJSON | null = null;
Expand All @@ -47,15 +48,14 @@ export class EditorScene extends Systems.Scene {
): Promise<void> {
const { assetManager } = context;

const textureLoader = new THREE.TextureLoader(
assetManager.context.manager
);
const texture = await textureLoader.loadAsync(
this.#defaultTileset.src
);

this.#texture = texture;
this.#tilesetLoader = new TilesetLoader({ manager: assetManager.context.manager });
this.#pendingLoad = LocalStoragePersistence.load();

// Pre-load world tilesets first (if restoring), then default (idempotent if already loaded).
if (this.#pendingLoad !== null) {
await this.#tilesetLoader.fromWorld(this.#pendingLoad);
}
await this.#tilesetLoader.fromTileDefinition(this.#defaultTileset);
}

constructor(
Expand Down Expand Up @@ -104,13 +104,12 @@ export class EditorScene extends Systems.Scene {
material.transparent = true;
},
alphaTest: 0,
onLayerUpdated: (evt) => this.editorState.dispatchLayerUpdated(evt)
onLayerUpdated: (evt) => this.editorState.dispatchLayerUpdated(evt),
tilesetLoader: this.#tilesetLoader
});
this.vr = vr;
this.editorState.setSelectedLayer(this.#defaultLayerName);

vr.loadTilesetSync(this.#defaultTileset, this.#texture);

// Skip default blocks when restoring a saved world — vr.load() will
// register the persisted definitions (which carry user edits).
// Registering defaults first would cause load() to skip saved blocks
Expand All @@ -126,15 +125,12 @@ export class EditorScene extends Systems.Scene {
persistence.start();

if (this.#pendingLoad !== null) {
vr.load(this.#pendingLoad)
.then(() => {
this.editorState.dispatchBlockRegistryChanged();
const layers = vr.world.getLayers();
if (layers.length > 0) {
this.editorState.setSelectedLayer(layers[0].name);
}
})
.catch(console.error);
vr.load(this.#pendingLoad);
this.editorState.dispatchBlockRegistryChanged();
const layers = vr.world.getLayers();
if (layers.length > 0) {
this.editorState.setSelectedLayer(layers[0].name);
}
this.#pendingLoad = null;
}

Expand Down
75 changes: 72 additions & 3 deletions packages/voxel-renderer/docs/Tileset.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ Tileset loading, UV computation, and pixel-art texture management.
`NearestFilter` and `SRGBColorSpace` are applied automatically to preserve pixel-art crispness.

```ts
const vr = new VoxelRenderer({});

await vr.loadTileset({
// Pre-load tilesets using TilesetLoader, then pass the loader to VoxelRenderer.
const loader = new TilesetLoader();
await loader.fromTileDefinition({
id: "default",
src: "assets/tileset.png",
tileSize: 16
// cols and rows are optional — derived from the image at load time
});

const vr = actor.addComponentAndGet(VoxelRenderer, { tilesetLoader: loader });

// Tile at column 2, row 0 — uses the default tileset
const tileRef: TileRef = {
col: 2,
Expand Down Expand Up @@ -135,3 +137,70 @@ interface TilesetDefaultBlockOptions {
#### `dispose(): void`

Disposes all textures and materials and clears the registry.

## TilesetLoader

Pre-loading utility that fetches tileset textures asynchronously before a `VoxelRenderer`
is constructed. Pass the populated loader via `VoxelRendererOptions.tilesetLoader` so all
textures register synchronously during construction — no async code is needed inside
lifecycle methods (`awake`, `start`, `update`).

### TilesetLoaderOptions

```ts
interface TilesetLoaderOptions {
/**
* Optional THREE.LoadingManager to track load progress.
*/
manager?: THREE.LoadingManager;
/**
* Custom loader implementation. For testing only.
*/
loader?: { loadAsync(url: string): Promise<THREE.Texture<HTMLImageElement>> };
}
```

### Properties

```ts
readonly tilesets: Map<string, TilesetEntry>;
```

Map from tileset ID to `{ def: TilesetDefinition, texture: THREE.Texture<HTMLImageElement> }`.
Populated by `fromTileDefinition` and `fromWorld`.

### Methods

#### `fromTileDefinition(def: TilesetDefinition): Promise<void>`

Loads the atlas image at `def.src` and stores the result in `tilesets`. Idempotent —
calling with the same `def.id` a second time is a no-op (the loader is not invoked again).

#### `fromWorld(data: VoxelWorldJSON): Promise<void>`

Iterates `data.tilesets` and calls `fromTileDefinition` for each. Useful when restoring a
saved world before constructing `VoxelRenderer`.

### Usage examples

**Single tileset:**

```ts
const loader = new TilesetLoader();
await loader.fromTileDefinition({ id: "default", src: "tileset.png", tileSize: 16 });

const vr = actor.addComponentAndGet(VoxelRenderer, { tilesetLoader: loader });
```

**Restoring a saved world (multi-tileset):**

```ts
const snapshot = JSON.parse(localStorage.getItem("world")!);

const loader = new TilesetLoader({ manager: assetManager.context.manager });
await loader.fromWorld(snapshot); // pre-load every tileset
await loader.fromTileDefinition(defaultTilesetDef); // idempotent if already loaded

const vr = actor.addComponentAndGet(VoxelRenderer, { tilesetLoader: loader });
vr.load(snapshot); // fully synchronous
```
41 changes: 25 additions & 16 deletions packages/voxel-renderer/docs/VoxelRenderer.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,16 @@
Each chunk is rebuilt only when its content changes, keeping GPU work proportional to edits rather than world size.

```ts
// Pre-load tilesets before constructing VoxelRenderer (no async in lifecycle).
const loader = new TilesetLoader();
await loader.fromTileDefinition({
id: "default",
src: "tileset.png",
tileSize: 16
});

const vr = actor.addComponentAndGet(VoxelRenderer, {
tilesetLoader: loader,
layers: ["Ground"],
blocks: [
{
Expand All @@ -21,12 +30,6 @@ const vr = actor.addComponentAndGet(VoxelRenderer, {
]
});

await vr.loadTileset({
id: "default",
src: "tileset.png",
tileSize: 16
});

vr.setVoxel("Ground", {
position: { x: 0, y: 0, z: 0 },
blockId: 1
Expand Down Expand Up @@ -136,6 +139,13 @@ interface VoxelRendererOptions {
* Useful for synchronizing external systems with changes to the voxel world.
*/
onLayerUpdated?: VoxelLayerHookListener;

/**
* Optional pre-loaded tileset collection. All tilesets in the loader are
* registered synchronously during construction. Use `TilesetLoader.fromTileDefinition()`
* or `TilesetLoader.fromWorld()` to populate it before constructing `VoxelRenderer`.
*/
tilesetLoader?: TilesetLoader;
}
```

Expand Down Expand Up @@ -285,23 +295,22 @@ getVoxelNeighbour(layerName: string, position: VoxelCoord, face: Face): VoxelEnt
Returns the voxel immediately adjacent to `position` in the given face direction.
Composited (first overload) or restricted to a specific layer (second overload).

#### `loadTileset(def: TilesetDefinition): Promise<void>`

Loads a tileset image via the actor's loading manager. The first loaded tileset becomes
the default for `TileRef` values with no explicit `tilesetId`.

#### `loadTilesetSync(def: TilesetDefinition, texture: THREE.Texture< HTMLImageElement >): void`
#### `loadTileset(def: TilesetDefinition, texture: THREE.Texture<HTMLImageElement>): void`

Same as `loadTileset` but synchronous and you have to provide the texture.
Registers an already-loaded texture for a tileset definition. The first registered tileset
becomes the default for `TileRef` values with no explicit `tilesetId`.
Prefer passing a `TilesetLoader` via `VoxelRendererOptions.tilesetLoader` for pre-loading;
use this method only when adding a tileset after construction.

#### `save(): VoxelWorldJSON`

Serialises the full world state (layers, voxels, tileset metadata) to a plain JSON object.

#### `load(data: VoxelWorldJSON): Promise<void>`
#### `load(data: VoxelWorldJSON): void`

Clears the current world, restores state from a JSON snapshot, and reloads any
referenced tilesets that are not already loaded.
Clears the current world and restores state from a JSON snapshot. All tilesets referenced
by the snapshot must have been pre-loaded via `TilesetLoader` before this call — if a
tileset is missing, an error is thrown. Already-registered tilesets are skipped.

#### `markAllChunksDirty(source?: string): void`

Expand Down
Loading
Loading