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/strict-otters-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@jolly-pixel/voxel.renderer": minor
---

Implement hooks callback for layer event in VoxelRenderer class
88 changes: 73 additions & 15 deletions packages/voxel-renderer/docs/VoxelRenderer.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,67 @@ type MaterialCustomizerFn = (
) => void;

interface VoxelRendererOptions {
/** Side length of each chunk in voxels. Default: `16`. */
/**
* @default 16
*/
chunkSize?: number;

/** Chunk material preset. `"standard"` enables PBR at higher GPU cost. Default: `"lambert"`. */
/**
* Enables collision shapes when provided.
* disabled by default to avoid forcing Rapier as a dependency for users who don't need physics.
*/
rapier?: {
/** Rapier3D module (static API) */
api: RapierAPI;
/** Rapier3D world instance */
world: RapierWorld;
};
/**
* @default "lambert"
* The type of material to use for rendering chunks. "standard" supports
* roughness and metalness maps but is more expensive to render; "lambert"
* is faster but only supports a simple diffuse map.
*/
material?: "lambert" | "standard";

/**
* Optional callback to customize each material after it is created.
* Called with the material instance and the tileset ID it corresponds to
*/
materialCustomizer?: MaterialCustomizerFn;

/**
* Fragments with alpha below this value are discarded.
* Set `0` to disable cutout transparency. Default: `0.1`.
* Optional list of layer names to create on initialization.
*/
alphaTest?: number;
/** Layer names to create at initialisation. */
layers?: string[];
/** Block definitions to pre-register. */
/**
* Optional initial block definitions to register.
* Block ID 0 is reserved for air
*/
blocks?: BlockDefinition[];
/** Custom shapes added on top of the built-in registry. */
/**
* Optional block shapes to register in addition to the default
* shapes provided by BlockShapeRegistry.createDefault().
*/
shapes?: BlockShape[];
/** Enables Rapier3D collision. Omit to disable physics entirely. */
rapier?: {
api: RapierAPI;
world: RapierWorld;
};
/**
* Alpha value below which fragments are discarded (cutout transparency).
* Set to 0 to disable alpha testing entirely (useful when your tileset tiles
* have no transparency, or during debugging to confirm geometry is present).
* @default 0.1
*/
alphaTest?: number;

/**
* Optional logger instance for debug output.
* Uses the engine's default logger if not provided.
*/
logger?: Systems.Logger;

/**
* Optional callback that is called whenever a layer is added, removed, or updated.
* Useful for synchronizing external systems with changes to the voxel world.
*/
onLayerUpdated?: VoxelLayerHookListener;
}
```

Expand Down Expand Up @@ -123,6 +156,10 @@ interface VoxelLayerConfigurableOptions {
}
```

#### `updateLayer(name: string, options?: Partial< VoxelLayerConfigurableOptions >): boolean`

Update a layer that already exists. Return `false` if no layer is found with the given name and `true` when updated.

#### `removeLayer(name: string): VoxelLayer`

Remove and returns a boolean confirming layer deletion.
Expand Down Expand Up @@ -179,6 +216,27 @@ Serialises the full world state (layers, voxels, tileset metadata) to a plain JS
Clears the current world, restores state from a JSON snapshot, and reloads any
referenced tilesets that are not already loaded.

### Hooks

```ts
export type VoxelLayerHookAction =
| "added"
| "removed"
| "updated"
| "offset-updated"
| "voxel-set"
| "voxel-removed";

export interface VoxelLayerHookEvent {
layerName: string;
action: VoxelLayerHookAction;
metadata: Record<string, any>;
}
export type VoxelLayerHookListener = (
event: VoxelLayerHookEvent
) => void;
```

---

## Example
Expand Down
11 changes: 11 additions & 0 deletions packages/voxel-renderer/examples/scripts/components/VoxelMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export class VoxelBehavior extends ActorComponent {
world = loadVoxelTiledMap("tilemap/brackeys-level.tmj", {
layerMode: "stacked"
});
// @ts-ignore
voxelRenderer: VoxelRenderer;

constructor(
actor: Actor
Expand All @@ -31,8 +33,17 @@ export class VoxelBehavior extends ActorComponent {
if (!voxelRenderer) {
throw new Error("VoxelRenderer component not found on actor");
}
this.voxelRenderer = voxelRenderer;
voxelRenderer
.load(world)
.catch(console.error);
}

start() {
setTimeout(() => {
console.log("Toggling visibility of 'Ground' layer...");
const success = this.voxelRenderer.removeLayer("Ground");
console.log(`Layer visibility update ${success ? "succeeded" : "failed"}`);
}, 5000);
}
}
2 changes: 2 additions & 0 deletions packages/voxel-renderer/examples/scripts/demo-tiled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ const runtime = new Runtime(canvas, {
});

const { world } = runtime;
world.logger.setLevel("debug");
world.logger.enableNamespace("*");

// ── Scene ─────────────────────────────────────────────────────────────────────
const scene = world.sceneManager.getSource();
Expand Down
70 changes: 67 additions & 3 deletions packages/voxel-renderer/src/VoxelRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ import { VoxelChunk } from "./world/VoxelChunk.ts";
import type { VoxelEntry, VoxelCoord } from "./world/types.ts";
import { packTransform, type FACE } from "./utils/math.ts";
import { FACE_OFFSETS } from "./mesh/math.ts";
import type {
VoxelLayerHookListener
} from "./hooks.ts";

type MaterialCustomizerFn = (
material: THREE.MeshLambertMaterial | THREE.MeshStandardMaterial,
Expand Down Expand Up @@ -131,6 +134,12 @@ export interface VoxelRendererOptions {
* Uses the engine's default logger if not provided.
*/
logger?: Systems.Logger;

/**
* Optional callback that is called whenever a layer is added, removed, or updated.
* Useful for synchronizing external systems with changes to the voxel world.
*/
onLayerUpdated?: VoxelLayerHookListener;
}

/**
Expand Down Expand Up @@ -172,6 +181,7 @@ export class VoxelRenderer extends ActorComponent {
#alphaTest: number;

#logger: Systems.Logger;
#onLayerUpdated?: VoxelLayerHookListener;

constructor(
actor: Actor<any>,
Expand All @@ -191,12 +201,14 @@ export class VoxelRenderer extends ActorComponent {
blocks = [],
shapes = [],
alphaTest = 0.1,
logger = actor.world.logger
logger = actor.world.logger,
onLayerUpdated
} = options;

this.#materialType = material;
this.#materialCustomizer = materialCustomizer;
this.#alphaTest = alphaTest;
this.#onLayerUpdated = onLayerUpdated;
this.#logger = logger.child({
namespace: "VoxelRenderer"
});
Expand Down Expand Up @@ -304,13 +316,23 @@ export class VoxelRenderer extends ActorComponent {
position,
{ blockId, transform }
);
this.#onLayerUpdated?.({
action: "voxel-set",
layerName,
metadata: { position, blockId, rotation, flipX, flipZ }
});
}

removeVoxel(
layerName: string,
options: VoxelRemoveOptions
): void {
this.world.removeVoxelAt(layerName, options.position);
this.#onLayerUpdated?.({
action: "voxel-removed",
layerName,
metadata: { position: options.position }
});
}

getVoxel(position: THREE.Vector3Like): VoxelEntry | undefined;
Expand Down Expand Up @@ -359,27 +381,69 @@ export class VoxelRenderer extends ActorComponent {
name: string,
options: VoxelLayerConfigurableOptions = {}
): VoxelLayer {
return this.world.addLayer(name, options);
const layer = this.world.addLayer(name, options);
this.#onLayerUpdated?.({
action: "added",
layerName: name,
metadata: { options }
});

return layer;
}

updateLayer(
name: string,
options: Partial<VoxelLayerConfigurableOptions>
): boolean {
const result = this.world.updateLayer(name, options);
if (result) {
this.#onLayerUpdated?.({
action: "updated",
layerName: name,
metadata: { options }
});
}

return result;
}

removeLayer(
name: string
): boolean {
return this.world.removeLayer(name);
const result = this.world.removeLayer(name);
if (result) {
this.#onLayerUpdated?.({
action: "removed",
layerName: name,
metadata: {}
});
}

return result;
}

setLayerOffset(
name: string,
offset: VoxelCoord
): void {
this.world.setLayerOffset(name, offset);
this.#onLayerUpdated?.({
action: "offset-updated",
layerName: name,
metadata: { offset }
});
}

translateLayer(
name: string,
delta: VoxelCoord
): void {
this.world.translateLayer(name, delta);
this.#onLayerUpdated?.({
action: "offset-updated",
layerName: name,
metadata: { delta }
});
}

async loadTileset(
Expand Down
16 changes: 16 additions & 0 deletions packages/voxel-renderer/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export type VoxelLayerHookAction =
| "added"
| "removed"
| "updated"
| "offset-updated"
| "voxel-set"
| "voxel-removed";

export interface VoxelLayerHookEvent {
layerName: string;
action: VoxelLayerHookAction;
metadata: Record<string, any>;
}
export type VoxelLayerHookListener = (
event: VoxelLayerHookEvent
) => void;
1 change: 1 addition & 0 deletions packages/voxel-renderer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export {
type VoxelSetOptions,
type VoxelRemoveOptions
} from "./VoxelRenderer.ts";
export * from "./hooks.ts";

// Built-in shapes
export { Cube } from "./blocks/shapes/Cube.ts";
Expand Down
33 changes: 28 additions & 5 deletions packages/voxel-renderer/src/world/VoxelWorld.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,25 @@ export class VoxelWorld {
return layer;
}

updateLayer(
name: string,
options: Partial<VoxelLayerConfigurableOptions>
): boolean {
const layer = this.getLayer(name);
if (!layer) {
return false;
}

if (options.properties) {
layer.properties = structuredClone(options.properties);
}
if (options.visible !== undefined) {
layer.visible = options.visible;
}

return true;
}

removeLayer(
name: string
): boolean {
Expand Down Expand Up @@ -242,14 +261,18 @@ export class VoxelWorld {
this.#layers.sort((a, b) => b.order - a.order);
}

#markAllLayersDirty(): void {
for (const layer of this.#layers) {
for (const chunk of layer.getChunks()) {
chunk.dirty = true;
}
#markLayerDirty(
layer: VoxelLayer
): void {
for (const chunk of layer.getChunks()) {
chunk.dirty = true;
}
}

#markAllLayersDirty(): void {
this.#layers.forEach((layer) => this.#markLayerDirty(layer));
}

/**
* When a voxel on a chunk boundary changes, the adjacent chunk also needs its
* mesh rebuilt so boundary faces are culled correctly.
Expand Down
Loading