@netforge/engine 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GAME-DESIGN.md +195 -0
- package/LICENSE +21 -0
- package/NOTICE +30 -0
- package/README.md +147 -2
- package/dist/api/appearance.d.ts +37 -0
- package/dist/api/appearance.js +41 -0
- package/dist/api/cards.d.ts +105 -0
- package/dist/api/cards.js +133 -0
- package/dist/api/collector.d.ts +114 -0
- package/dist/api/collector.js +204 -0
- package/dist/api/dungeon.d.ts +58 -0
- package/dist/api/dungeon.js +216 -0
- package/dist/api/harness.d.ts +149 -0
- package/dist/api/harness.js +549 -0
- package/dist/api/idle.d.ts +71 -0
- package/dist/api/idle.js +104 -0
- package/dist/api/race-harness.d.ts +3 -0
- package/dist/api/race-harness.js +148 -0
- package/dist/api/registry.d.ts +241 -0
- package/dist/api/registry.js +394 -0
- package/dist/api/scheduler.d.ts +56 -0
- package/dist/api/scheduler.js +160 -0
- package/dist/api/schemas.d.ts +958 -0
- package/dist/api/schemas.js +859 -0
- package/dist/api/tower.d.ts +127 -0
- package/dist/api/tower.js +183 -0
- package/dist/game/adapter.d.ts +69 -0
- package/dist/game/adapter.js +341 -0
- package/dist/game/arena-bot.d.ts +66 -0
- package/dist/game/arena-bot.js +188 -0
- package/dist/game/bot.d.ts +95 -0
- package/dist/game/bot.js +285 -0
- package/dist/game/compile.d.ts +97 -0
- package/dist/game/compile.js +476 -0
- package/dist/game/director.d.ts +42 -0
- package/dist/game/director.js +179 -0
- package/dist/game/earn-source.d.ts +122 -0
- package/dist/game/earn-source.js +386 -0
- package/dist/game/earn.d.ts +49 -0
- package/dist/game/earn.js +55 -0
- package/dist/game/index.d.ts +37 -0
- package/dist/game/index.js +25 -0
- package/dist/game/platformer.d.ts +156 -0
- package/dist/game/platformer.js +619 -0
- package/dist/game/profile.d.ts +97 -0
- package/dist/game/profile.js +140 -0
- package/dist/game/race.d.ts +177 -0
- package/dist/game/race.js +386 -0
- package/dist/game/racer.d.ts +98 -0
- package/dist/game/racer.js +379 -0
- package/dist/game/runner.d.ts +286 -0
- package/dist/game/runner.js +824 -0
- package/dist/game/threats.d.ts +23 -0
- package/dist/game/threats.js +49 -0
- package/dist/game/view.d.ts +47 -0
- package/dist/game/view.js +123 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +24 -0
- package/dist/net/bits.d.ts +33 -0
- package/dist/net/bits.js +135 -0
- package/dist/net/client.d.ts +75 -0
- package/dist/net/client.js +151 -0
- package/dist/net/index.d.ts +23 -0
- package/dist/net/index.js +23 -0
- package/dist/net/input.d.ts +49 -0
- package/dist/net/input.js +112 -0
- package/dist/net/interpolation.d.ts +37 -0
- package/dist/net/interpolation.js +97 -0
- package/dist/net/netsim.d.ts +22 -0
- package/dist/net/netsim.js +52 -0
- package/dist/net/prediction.d.ts +31 -0
- package/dist/net/prediction.js +101 -0
- package/dist/net/profile-client.d.ts +75 -0
- package/dist/net/profile-client.js +173 -0
- package/dist/net/protocol.d.ts +208 -0
- package/dist/net/protocol.js +8 -0
- package/dist/net/room-client.d.ts +57 -0
- package/dist/net/room-client.js +144 -0
- package/dist/net/schema.d.ts +78 -0
- package/dist/net/schema.js +121 -0
- package/dist/net/sim-adapter.d.ts +134 -0
- package/dist/net/sim-adapter.js +22 -0
- package/dist/net/snapshot.d.ts +61 -0
- package/dist/net/snapshot.js +301 -0
- package/dist/rules/adapter.d.ts +53 -0
- package/dist/rules/adapter.js +261 -0
- package/dist/rules/cards.d.ts +115 -0
- package/dist/rules/cards.js +547 -0
- package/dist/rules/collector.d.ts +161 -0
- package/dist/rules/collector.js +732 -0
- package/dist/rules/genres.d.ts +13 -0
- package/dist/rules/genres.js +38 -0
- package/dist/rules/harness.d.ts +16 -0
- package/dist/rules/harness.js +127 -0
- package/dist/rules/idle.d.ts +115 -0
- package/dist/rules/idle.js +370 -0
- package/dist/rules/index.d.ts +16 -0
- package/dist/rules/index.js +14 -0
- package/dist/rules/kit.d.ts +171 -0
- package/dist/rules/kit.js +88 -0
- package/dist/rules/tower.d.ts +214 -0
- package/dist/rules/tower.js +655 -0
- package/dist/runtime/arena.d.ts +86 -0
- package/dist/runtime/arena.js +269 -0
- package/dist/runtime/assets.d.ts +26 -0
- package/dist/runtime/assets.js +35 -0
- package/dist/runtime/camera.d.ts +113 -0
- package/dist/runtime/camera.js +247 -0
- package/dist/runtime/crowd.d.ts +18 -0
- package/dist/runtime/crowd.js +62 -0
- package/dist/runtime/feel/audio.d.ts +150 -0
- package/dist/runtime/feel/audio.js +327 -0
- package/dist/runtime/feel/clock.d.ts +52 -0
- package/dist/runtime/feel/clock.js +80 -0
- package/dist/runtime/feel/flash.d.ts +28 -0
- package/dist/runtime/feel/flash.js +60 -0
- package/dist/runtime/feel/index.d.ts +27 -0
- package/dist/runtime/feel/index.js +16 -0
- package/dist/runtime/feel/kit.d.ts +159 -0
- package/dist/runtime/feel/kit.js +226 -0
- package/dist/runtime/feel/knockback.d.ts +41 -0
- package/dist/runtime/feel/knockback.js +70 -0
- package/dist/runtime/feel/noise.d.ts +6 -0
- package/dist/runtime/feel/noise.js +30 -0
- package/dist/runtime/feel/numbers.d.ts +91 -0
- package/dist/runtime/feel/numbers.js +172 -0
- package/dist/runtime/feel/rumble.d.ts +40 -0
- package/dist/runtime/feel/rumble.js +58 -0
- package/dist/runtime/feel/sound.d.ts +96 -0
- package/dist/runtime/feel/sound.js +176 -0
- package/dist/runtime/feel/spring.d.ts +42 -0
- package/dist/runtime/feel/spring.js +95 -0
- package/dist/runtime/feel/trauma.d.ts +48 -0
- package/dist/runtime/feel/trauma.js +76 -0
- package/dist/runtime/feel/tween.d.ts +66 -0
- package/dist/runtime/feel/tween.js +180 -0
- package/dist/runtime/feel/types.d.ts +30 -0
- package/dist/runtime/feel/types.js +5 -0
- package/dist/runtime/index.d.ts +28 -0
- package/dist/runtime/index.js +26 -0
- package/dist/runtime/input.d.ts +38 -0
- package/dist/runtime/input.js +159 -0
- package/dist/runtime/interpolate.d.ts +6 -0
- package/dist/runtime/interpolate.js +13 -0
- package/dist/runtime/render/animation.d.ts +63 -0
- package/dist/runtime/render/animation.js +128 -0
- package/dist/runtime/render/atmosphere.d.ts +32 -0
- package/dist/runtime/render/atmosphere.js +59 -0
- package/dist/runtime/render/crowd-motion.d.ts +68 -0
- package/dist/runtime/render/crowd-motion.js +163 -0
- package/dist/runtime/render/decals.d.ts +64 -0
- package/dist/runtime/render/decals.js +222 -0
- package/dist/runtime/render/highlight.d.ts +29 -0
- package/dist/runtime/render/highlight.js +110 -0
- package/dist/runtime/render/index.d.ts +35 -0
- package/dist/runtime/render/index.js +22 -0
- package/dist/runtime/render/lighting.d.ts +50 -0
- package/dist/runtime/render/lighting.js +142 -0
- package/dist/runtime/render/particles.d.ts +117 -0
- package/dist/runtime/render/particles.js +289 -0
- package/dist/runtime/render/post.d.ts +92 -0
- package/dist/runtime/render/post.js +191 -0
- package/dist/runtime/render/presets.d.ts +57 -0
- package/dist/runtime/render/presets.js +116 -0
- package/dist/runtime/render/quality.d.ts +113 -0
- package/dist/runtime/render/quality.js +180 -0
- package/dist/runtime/render/rig.d.ts +29 -0
- package/dist/runtime/render/rig.js +107 -0
- package/dist/runtime/render/sprite.d.ts +34 -0
- package/dist/runtime/render/sprite.js +58 -0
- package/dist/runtime/render/stack.d.ts +74 -0
- package/dist/runtime/render/stack.js +132 -0
- package/dist/runtime/render/vat.d.ts +78 -0
- package/dist/runtime/render/vat.js +219 -0
- package/dist/runtime/renderer.d.ts +19 -0
- package/dist/runtime/renderer.js +18 -0
- package/dist/runtime/stats.d.ts +11 -0
- package/dist/runtime/stats.js +29 -0
- package/dist/runtime/styles/handdrawn/ground.d.ts +11 -0
- package/dist/runtime/styles/handdrawn/ground.js +282 -0
- package/dist/runtime/styles/handdrawn/index.d.ts +4 -0
- package/dist/runtime/styles/handdrawn/index.js +94 -0
- package/dist/runtime/styles/handdrawn/ink.d.ts +31 -0
- package/dist/runtime/styles/handdrawn/ink.js +68 -0
- package/dist/runtime/styles/handdrawn/looks.d.ts +18 -0
- package/dist/runtime/styles/handdrawn/looks.js +42 -0
- package/dist/runtime/styles/handdrawn/paint.d.ts +59 -0
- package/dist/runtime/styles/handdrawn/paint.js +129 -0
- package/dist/runtime/styles/handdrawn/palette.d.ts +49 -0
- package/dist/runtime/styles/handdrawn/palette.js +70 -0
- package/dist/runtime/styles/handdrawn/textures.d.ts +27 -0
- package/dist/runtime/styles/handdrawn/textures.js +150 -0
- package/dist/runtime/styles/handdrawn/vfx.d.ts +14 -0
- package/dist/runtime/styles/handdrawn/vfx.js +73 -0
- package/dist/runtime/styles/index.d.ts +9 -0
- package/dist/runtime/styles/index.js +32 -0
- package/dist/runtime/styles/kit.d.ts +193 -0
- package/dist/runtime/styles/kit.js +1 -0
- package/dist/runtime/styles/pixel/index.d.ts +46 -0
- package/dist/runtime/styles/pixel/index.js +167 -0
- package/dist/runtime/styles/ps1/index.d.ts +29 -0
- package/dist/runtime/styles/ps1/index.js +41 -0
- package/dist/runtime/styles/ps1/lights.d.ts +47 -0
- package/dist/runtime/styles/ps1/lights.js +146 -0
- package/dist/runtime/styles/ps1/materials.d.ts +62 -0
- package/dist/runtime/styles/ps1/materials.js +285 -0
- package/dist/runtime/styles/ps1/pipeline.d.ts +52 -0
- package/dist/runtime/styles/ps1/pipeline.js +111 -0
- package/dist/runtime/styles/ps1/surfaces.d.ts +44 -0
- package/dist/runtime/styles/ps1/surfaces.js +228 -0
- package/dist/runtime/styles/toon/index.d.ts +5 -0
- package/dist/runtime/styles/toon/index.js +178 -0
- package/dist/runtime/styles/toon/ink.d.ts +39 -0
- package/dist/runtime/styles/toon/ink.js +73 -0
- package/dist/runtime/styles/toon/look.d.ts +10 -0
- package/dist/runtime/styles/toon/look.js +112 -0
- package/dist/runtime/styles/toon/material.d.ts +74 -0
- package/dist/runtime/styles/toon/material.js +208 -0
- package/dist/runtime/styles/toon/normals.d.ts +26 -0
- package/dist/runtime/styles/toon/normals.js +127 -0
- package/dist/runtime/styles/toon/shadows.d.ts +24 -0
- package/dist/runtime/styles/toon/shadows.js +42 -0
- package/dist/runtime/styles/toon/vfx.d.ts +15 -0
- package/dist/runtime/styles/toon/vfx.js +47 -0
- package/dist/runtime/styles/view.d.ts +35 -0
- package/dist/runtime/styles/view.js +34 -0
- package/dist/runtime/styles/voxel/core.d.ts +114 -0
- package/dist/runtime/styles/voxel/core.js +577 -0
- package/dist/runtime/styles/voxel/geometry.d.ts +27 -0
- package/dist/runtime/styles/voxel/geometry.js +40 -0
- package/dist/runtime/styles/voxel/index.d.ts +21 -0
- package/dist/runtime/styles/voxel/index.js +23 -0
- package/dist/runtime/styles/voxel/kit.d.ts +6 -0
- package/dist/runtime/styles/voxel/kit.js +11 -0
- package/dist/runtime/styles/voxel/look.d.ts +10 -0
- package/dist/runtime/styles/voxel/look.js +16 -0
- package/dist/runtime/styles/voxel/props.d.ts +9 -0
- package/dist/runtime/styles/voxel/props.js +52 -0
- package/dist/runtime/styles/voxel/shapes.d.ts +41 -0
- package/dist/runtime/styles/voxel/shapes.js +83 -0
- package/dist/runtime/styles/voxel/world.d.ts +49 -0
- package/dist/runtime/styles/voxel/world.js +422 -0
- package/llms.txt +183 -0
- package/package.json +82 -3
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The voxel style's core (voxel): a voxel grid, a
|
|
3
|
+
* triangle voxelizer, interior fill, and a greedy mesher that bakes ambient
|
|
4
|
+
* occlusion per voxel face. Pure TypeScript with no three.js and no DOM, so
|
|
5
|
+
* the same code runs in the asset library build (Node: library models are
|
|
6
|
+
* rebuilt as voxels there) and in the browser (voxel terrain, pillars,
|
|
7
|
+
* trees and pickups made at load).
|
|
8
|
+
*
|
|
9
|
+
* Colours are packed 0xRRGGBB in sRGB, 8 bits a channel, so near-identical
|
|
10
|
+
* colours merge into one face. The mesher writes linear colours with the
|
|
11
|
+
* occlusion multiplied in, which is what glTF COLOR_0 and three.js vertex
|
|
12
|
+
* colours expect.
|
|
13
|
+
*/
|
|
14
|
+
/** No voxel in a cell. */
|
|
15
|
+
export declare const EMPTY = -1;
|
|
16
|
+
/** A dense box of voxels: `nx * ny * nz` cells of `size` metres, the first cell's corner at `origin`. */
|
|
17
|
+
export declare class VoxelGrid {
|
|
18
|
+
readonly nx: number;
|
|
19
|
+
readonly ny: number;
|
|
20
|
+
readonly nz: number;
|
|
21
|
+
readonly size: number;
|
|
22
|
+
readonly origin: readonly [number, number, number];
|
|
23
|
+
/** 0xRRGGBB (sRGB) per cell, or EMPTY. */
|
|
24
|
+
readonly color: Int32Array;
|
|
25
|
+
/** Which rigid part (bone or node) a cell moves with. Faces between different parts are kept, so parts can move apart. */
|
|
26
|
+
readonly part: Uint16Array;
|
|
27
|
+
constructor(nx: number, ny: number, nz: number, size: number, origin?: readonly [number, number, number]);
|
|
28
|
+
index(x: number, y: number, z: number): number;
|
|
29
|
+
inside(x: number, y: number, z: number): boolean;
|
|
30
|
+
/** The cell's colour, or EMPTY (also outside the grid). */
|
|
31
|
+
get(x: number, y: number, z: number): number;
|
|
32
|
+
solid(x: number, y: number, z: number): boolean;
|
|
33
|
+
set(x: number, y: number, z: number, rgb: number, part?: number): void;
|
|
34
|
+
clear(x: number, y: number, z: number): void;
|
|
35
|
+
/** Solid cells. */
|
|
36
|
+
count(): number;
|
|
37
|
+
}
|
|
38
|
+
/** sRGB 0..255 channel to linear 0..1. */
|
|
39
|
+
export declare function srgbToLinear(c8: number): number;
|
|
40
|
+
/** Linear 0..1 to an sRGB 0..255 channel. */
|
|
41
|
+
export declare function linearToSrgb8(c: number): number;
|
|
42
|
+
/** Pack linear r, g, b (0..1) into 0xRRGGBB sRGB. */
|
|
43
|
+
export declare function packLinear(r: number, g: number, b: number): number;
|
|
44
|
+
/** A 0xRRGGBB colour scaled in brightness (sRGB space), clamped. */
|
|
45
|
+
export declare function shadeRgb(rgb: number, k: number): number;
|
|
46
|
+
/** Mix two 0xRRGGBB colours (sRGB space). */
|
|
47
|
+
export declare function mixRgb(a: number, b: number, t: number): number;
|
|
48
|
+
/** A deterministic 0..1 hash of integer coordinates (per-voxel colour variation that is the same on every client). */
|
|
49
|
+
export declare function hash3(x: number, y: number, z: number, seed?: number): number;
|
|
50
|
+
/** What a voxel looks like at a point on a triangle: its colour and the part it moves with. */
|
|
51
|
+
export interface VoxelSample {
|
|
52
|
+
readonly rgb: number;
|
|
53
|
+
readonly part: number;
|
|
54
|
+
}
|
|
55
|
+
/** Shades a triangle at barycentrics (u, v, w); null leaves the voxel to another triangle (a transparent texel). */
|
|
56
|
+
export type TriangleShader = (triangle: number, u: number, v: number, w: number) => VoxelSample | null;
|
|
57
|
+
/**
|
|
58
|
+
* Rasterize a triangle soup (9 floats a triangle, in the grid's metres)
|
|
59
|
+
* into the grid. Every voxel a triangle touches (separating-axis test, so
|
|
60
|
+
* surfaces are watertight) takes the colour of the triangle closest to its
|
|
61
|
+
* centre. Returns the number of solid voxels written.
|
|
62
|
+
*/
|
|
63
|
+
export declare function voxelizeTriangles(grid: VoxelGrid, positions: Float32Array | number[], shade: TriangleShader): number;
|
|
64
|
+
/**
|
|
65
|
+
* Make a voxelized shell solid: every empty cell the outside cannot reach
|
|
66
|
+
* (6-connected flood from the grid's border) becomes solid, with the colour
|
|
67
|
+
* and part of the nearest shell voxel. Returns the cells filled.
|
|
68
|
+
*/
|
|
69
|
+
export declare function fillInterior(grid: VoxelGrid): number;
|
|
70
|
+
/**
|
|
71
|
+
* Remove voxels that touch no empty cell (hidden inside), keeping a model's
|
|
72
|
+
* shell. Saves nothing in the mesh (hidden faces are never emitted) but
|
|
73
|
+
* shortens a list of voxels to throw when a model shatters.
|
|
74
|
+
*/
|
|
75
|
+
export declare function surfaceVoxels(grid: VoxelGrid): {
|
|
76
|
+
x: number;
|
|
77
|
+
y: number;
|
|
78
|
+
z: number;
|
|
79
|
+
rgb: number;
|
|
80
|
+
part: number;
|
|
81
|
+
}[];
|
|
82
|
+
export interface MeshOptions {
|
|
83
|
+
/** Bake ambient occlusion into vertex colours (default true). */
|
|
84
|
+
readonly ao?: boolean;
|
|
85
|
+
/** Brightness at each occlusion level, fully occluded (0) to open (3). Default [0.42, 0.62, 0.82, 1]. */
|
|
86
|
+
readonly aoLevels?: readonly [number, number, number, number];
|
|
87
|
+
/** Keep faces between voxels of different parts (rigid parts that move apart). Default true. */
|
|
88
|
+
readonly splitParts?: boolean;
|
|
89
|
+
/** Skip faces pointing this way (e.g. the bottoms of terrain): "-y" etc. */
|
|
90
|
+
readonly skip?: readonly ("+x" | "-x" | "+y" | "-y" | "+z" | "-z")[];
|
|
91
|
+
/** Treat cells outside the grid as solid on these faces (a chunk's neighbours): default none. */
|
|
92
|
+
readonly solidOutside?: (x: number, y: number, z: number) => boolean;
|
|
93
|
+
}
|
|
94
|
+
/** A mesh in the grid's metres: indexed quads with flat normals, linear colours (occlusion baked in) and a part per vertex. */
|
|
95
|
+
export interface VoxelMesh {
|
|
96
|
+
readonly positions: Float32Array;
|
|
97
|
+
readonly normals: Float32Array;
|
|
98
|
+
readonly colors: Float32Array;
|
|
99
|
+
readonly parts: Uint16Array;
|
|
100
|
+
readonly indices: Uint32Array;
|
|
101
|
+
readonly quads: number;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Greedy-mesh a voxel grid (0fps "Meshing in a Minecraft game", with the
|
|
105
|
+
* occlusion-aware merge from "Ambient occlusion for Minecraft-like
|
|
106
|
+
* worlds"): coplanar faces with the same colour, part and corner occlusion
|
|
107
|
+
* merge into one quad. Faces are emitted where a voxel meets an empty cell,
|
|
108
|
+
* or a voxel of another part when `splitParts` is on.
|
|
109
|
+
*/
|
|
110
|
+
export declare function greedyMesh(grid: VoxelGrid, opts?: MeshOptions): VoxelMesh;
|
|
111
|
+
/** Fill every cell whose centre (in grid cells, from the grid's corner) passes `inside`, coloured by `paint`. */
|
|
112
|
+
export declare function fillWhere(grid: VoxelGrid, inside: (x: number, y: number, z: number) => boolean, paint: (x: number, y: number, z: number) => number, part?: number): number;
|
|
113
|
+
/** A small palette shade jitter: the colour scaled by 1 +- `amount`, chosen by the voxel's hash. */
|
|
114
|
+
export declare function jitter(rgb: number, x: number, y: number, z: number, amount?: number, seed?: number): number;
|
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The voxel style's core (voxel): a voxel grid, a
|
|
3
|
+
* triangle voxelizer, interior fill, and a greedy mesher that bakes ambient
|
|
4
|
+
* occlusion per voxel face. Pure TypeScript with no three.js and no DOM, so
|
|
5
|
+
* the same code runs in the asset library build (Node: library models are
|
|
6
|
+
* rebuilt as voxels there) and in the browser (voxel terrain, pillars,
|
|
7
|
+
* trees and pickups made at load).
|
|
8
|
+
*
|
|
9
|
+
* Colours are packed 0xRRGGBB in sRGB, 8 bits a channel, so near-identical
|
|
10
|
+
* colours merge into one face. The mesher writes linear colours with the
|
|
11
|
+
* occlusion multiplied in, which is what glTF COLOR_0 and three.js vertex
|
|
12
|
+
* colours expect.
|
|
13
|
+
*/
|
|
14
|
+
/** No voxel in a cell. */
|
|
15
|
+
export const EMPTY = -1;
|
|
16
|
+
/** A dense box of voxels: `nx * ny * nz` cells of `size` metres, the first cell's corner at `origin`. */
|
|
17
|
+
export class VoxelGrid {
|
|
18
|
+
nx;
|
|
19
|
+
ny;
|
|
20
|
+
nz;
|
|
21
|
+
size;
|
|
22
|
+
origin;
|
|
23
|
+
/** 0xRRGGBB (sRGB) per cell, or EMPTY. */
|
|
24
|
+
color;
|
|
25
|
+
/** Which rigid part (bone or node) a cell moves with. Faces between different parts are kept, so parts can move apart. */
|
|
26
|
+
part;
|
|
27
|
+
constructor(nx, ny, nz, size, origin = [0, 0, 0]) {
|
|
28
|
+
this.nx = nx;
|
|
29
|
+
this.ny = ny;
|
|
30
|
+
this.nz = nz;
|
|
31
|
+
this.size = size;
|
|
32
|
+
this.origin = origin;
|
|
33
|
+
if (![nx, ny, nz].every((n) => Number.isInteger(n) && n > 0))
|
|
34
|
+
throw new RangeError("VoxelGrid: dimensions are positive integers");
|
|
35
|
+
if (!(size > 0))
|
|
36
|
+
throw new RangeError("VoxelGrid: size must be positive");
|
|
37
|
+
if (nx * ny * nz > 16_000_000)
|
|
38
|
+
throw new RangeError("VoxelGrid: more than 16M cells");
|
|
39
|
+
this.color = new Int32Array(nx * ny * nz).fill(EMPTY);
|
|
40
|
+
this.part = new Uint16Array(nx * ny * nz);
|
|
41
|
+
}
|
|
42
|
+
index(x, y, z) {
|
|
43
|
+
return x + this.nx * (y + this.ny * z);
|
|
44
|
+
}
|
|
45
|
+
inside(x, y, z) {
|
|
46
|
+
return x >= 0 && y >= 0 && z >= 0 && x < this.nx && y < this.ny && z < this.nz;
|
|
47
|
+
}
|
|
48
|
+
/** The cell's colour, or EMPTY (also outside the grid). */
|
|
49
|
+
get(x, y, z) {
|
|
50
|
+
return this.inside(x, y, z) ? (this.color[this.index(x, y, z)] ?? EMPTY) : EMPTY;
|
|
51
|
+
}
|
|
52
|
+
solid(x, y, z) {
|
|
53
|
+
return this.get(x, y, z) !== EMPTY;
|
|
54
|
+
}
|
|
55
|
+
set(x, y, z, rgb, part = 0) {
|
|
56
|
+
if (!this.inside(x, y, z))
|
|
57
|
+
return;
|
|
58
|
+
const i = this.index(x, y, z);
|
|
59
|
+
this.color[i] = rgb & 0xffffff;
|
|
60
|
+
this.part[i] = part;
|
|
61
|
+
}
|
|
62
|
+
clear(x, y, z) {
|
|
63
|
+
if (this.inside(x, y, z))
|
|
64
|
+
this.color[this.index(x, y, z)] = EMPTY;
|
|
65
|
+
}
|
|
66
|
+
/** Solid cells. */
|
|
67
|
+
count() {
|
|
68
|
+
let n = 0;
|
|
69
|
+
for (let i = 0; i < this.color.length; i++)
|
|
70
|
+
if ((this.color[i] ?? EMPTY) !== EMPTY)
|
|
71
|
+
n++;
|
|
72
|
+
return n;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Colour helpers
|
|
77
|
+
/** sRGB 0..255 channel to linear 0..1. */
|
|
78
|
+
export function srgbToLinear(c8) {
|
|
79
|
+
const c = c8 / 255;
|
|
80
|
+
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
81
|
+
}
|
|
82
|
+
/** Linear 0..1 to an sRGB 0..255 channel. */
|
|
83
|
+
export function linearToSrgb8(c) {
|
|
84
|
+
const v = c <= 0 ? 0 : c >= 1 ? 1 : c <= 0.0031308 ? c * 12.92 : 1.055 * c ** (1 / 2.4) - 0.055;
|
|
85
|
+
return Math.round(v * 255);
|
|
86
|
+
}
|
|
87
|
+
/** Pack linear r, g, b (0..1) into 0xRRGGBB sRGB. */
|
|
88
|
+
export function packLinear(r, g, b) {
|
|
89
|
+
return (linearToSrgb8(r) << 16) | (linearToSrgb8(g) << 8) | linearToSrgb8(b);
|
|
90
|
+
}
|
|
91
|
+
/** A 0xRRGGBB colour scaled in brightness (sRGB space), clamped. */
|
|
92
|
+
export function shadeRgb(rgb, k) {
|
|
93
|
+
const ch = (s) => Math.max(0, Math.min(255, Math.round(((rgb >> s) & 0xff) * k)));
|
|
94
|
+
return (ch(16) << 16) | (ch(8) << 8) | ch(0);
|
|
95
|
+
}
|
|
96
|
+
/** Mix two 0xRRGGBB colours (sRGB space). */
|
|
97
|
+
export function mixRgb(a, b, t) {
|
|
98
|
+
const ch = (s) => Math.round(((a >> s) & 0xff) * (1 - t) + ((b >> s) & 0xff) * t);
|
|
99
|
+
return (ch(16) << 16) | (ch(8) << 8) | ch(0);
|
|
100
|
+
}
|
|
101
|
+
/** A deterministic 0..1 hash of integer coordinates (per-voxel colour variation that is the same on every client). */
|
|
102
|
+
export function hash3(x, y, z, seed = 0) {
|
|
103
|
+
let h = Math.imul(x | 0, 0x8da6b343) ^ Math.imul(y | 0, 0xd8163841) ^ Math.imul(z | 0, 0xcb1ab31f) ^ Math.imul(seed | 0, 0x165667b1);
|
|
104
|
+
h = Math.imul(h ^ (h >>> 15), 0x2c1b3c6d);
|
|
105
|
+
h = Math.imul(h ^ (h >>> 12), 0x297a2d39);
|
|
106
|
+
h ^= h >>> 15;
|
|
107
|
+
return (h >>> 0) / 4294967296;
|
|
108
|
+
}
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Triangle voxelizer
|
|
111
|
+
/** Akenine-Möller triangle / box overlap (separating axis test). `c` box centre, `h` half size. */
|
|
112
|
+
function triBoxOverlap(cx, cy, cz, h, t, o) {
|
|
113
|
+
const v0x = (t[o] ?? 0) - cx, v0y = (t[o + 1] ?? 0) - cy, v0z = (t[o + 2] ?? 0) - cz;
|
|
114
|
+
const v1x = (t[o + 3] ?? 0) - cx, v1y = (t[o + 4] ?? 0) - cy, v1z = (t[o + 5] ?? 0) - cz;
|
|
115
|
+
const v2x = (t[o + 6] ?? 0) - cx, v2y = (t[o + 7] ?? 0) - cy, v2z = (t[o + 8] ?? 0) - cz;
|
|
116
|
+
const e0x = v1x - v0x, e0y = v1y - v0y, e0z = v1z - v0z;
|
|
117
|
+
const e1x = v2x - v1x, e1y = v2y - v1y, e1z = v2z - v1z;
|
|
118
|
+
const e2x = v0x - v2x, e2y = v0y - v2y, e2z = v0z - v2z;
|
|
119
|
+
// Nine edge cross axes.
|
|
120
|
+
const axis = (ax, ay, az) => {
|
|
121
|
+
const p0 = v0x * ax + v0y * ay + v0z * az;
|
|
122
|
+
const p1 = v1x * ax + v1y * ay + v1z * az;
|
|
123
|
+
const p2 = v2x * ax + v2y * ay + v2z * az;
|
|
124
|
+
const r = h * (Math.abs(ax) + Math.abs(ay) + Math.abs(az));
|
|
125
|
+
return !(Math.min(p0, p1, p2) > r || Math.max(p0, p1, p2) < -r);
|
|
126
|
+
};
|
|
127
|
+
if (!axis(0, -e0z, e0y) || !axis(0, -e1z, e1y) || !axis(0, -e2z, e2y))
|
|
128
|
+
return false;
|
|
129
|
+
if (!axis(e0z, 0, -e0x) || !axis(e1z, 0, -e1x) || !axis(e2z, 0, -e2x))
|
|
130
|
+
return false;
|
|
131
|
+
if (!axis(-e0y, e0x, 0) || !axis(-e1y, e1x, 0) || !axis(-e2y, e2x, 0))
|
|
132
|
+
return false;
|
|
133
|
+
// The box's own axes.
|
|
134
|
+
if (Math.min(v0x, v1x, v2x) > h || Math.max(v0x, v1x, v2x) < -h)
|
|
135
|
+
return false;
|
|
136
|
+
if (Math.min(v0y, v1y, v2y) > h || Math.max(v0y, v1y, v2y) < -h)
|
|
137
|
+
return false;
|
|
138
|
+
if (Math.min(v0z, v1z, v2z) > h || Math.max(v0z, v1z, v2z) < -h)
|
|
139
|
+
return false;
|
|
140
|
+
// The triangle's plane.
|
|
141
|
+
const nx = e0y * e1z - e0z * e1y;
|
|
142
|
+
const ny = e0z * e1x - e0x * e1z;
|
|
143
|
+
const nz = e0x * e1y - e0y * e1x;
|
|
144
|
+
const d = -(nx * v0x + ny * v0y + nz * v0z);
|
|
145
|
+
const r = h * (Math.abs(nx) + Math.abs(ny) + Math.abs(nz));
|
|
146
|
+
return Math.abs(d) <= r;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Closest point on a triangle to p (Ericson, Real-Time Collision
|
|
150
|
+
* Detection 5.1.5). Writes barycentrics (u, v, w for corners 0, 1, 2) into
|
|
151
|
+
* `out` and returns the squared distance.
|
|
152
|
+
*/
|
|
153
|
+
function closestOnTriangle(px, py, pz, t, o, out) {
|
|
154
|
+
const ax = t[o] ?? 0, ay = t[o + 1] ?? 0, az = t[o + 2] ?? 0;
|
|
155
|
+
const bx = t[o + 3] ?? 0, by = t[o + 4] ?? 0, bz = t[o + 5] ?? 0;
|
|
156
|
+
const cx = t[o + 6] ?? 0, cy = t[o + 7] ?? 0, cz = t[o + 8] ?? 0;
|
|
157
|
+
const abx = bx - ax, aby = by - ay, abz = bz - az;
|
|
158
|
+
const acx = cx - ax, acy = cy - ay, acz = cz - az;
|
|
159
|
+
const apx = px - ax, apy = py - ay, apz = pz - az;
|
|
160
|
+
const d1 = abx * apx + aby * apy + abz * apz;
|
|
161
|
+
const d2 = acx * apx + acy * apy + acz * apz;
|
|
162
|
+
let u = 1, v = 0, w = 0;
|
|
163
|
+
const finish = () => {
|
|
164
|
+
out[0] = u;
|
|
165
|
+
out[1] = v;
|
|
166
|
+
out[2] = w;
|
|
167
|
+
const qx = ax * u + bx * v + cx * w - px;
|
|
168
|
+
const qy = ay * u + by * v + cy * w - py;
|
|
169
|
+
const qz = az * u + bz * v + cz * w - pz;
|
|
170
|
+
return qx * qx + qy * qy + qz * qz;
|
|
171
|
+
};
|
|
172
|
+
if (d1 <= 0 && d2 <= 0)
|
|
173
|
+
return finish();
|
|
174
|
+
const bpx = px - bx, bpy = py - by, bpz = pz - bz;
|
|
175
|
+
const d3 = abx * bpx + aby * bpy + abz * bpz;
|
|
176
|
+
const d4 = acx * bpx + acy * bpy + acz * bpz;
|
|
177
|
+
if (d3 >= 0 && d4 <= d3) {
|
|
178
|
+
u = 0;
|
|
179
|
+
v = 1;
|
|
180
|
+
return finish();
|
|
181
|
+
}
|
|
182
|
+
const vc = d1 * d4 - d3 * d2;
|
|
183
|
+
if (vc <= 0 && d1 >= 0 && d3 <= 0) {
|
|
184
|
+
const t1 = d1 / (d1 - d3);
|
|
185
|
+
u = 1 - t1;
|
|
186
|
+
v = t1;
|
|
187
|
+
return finish();
|
|
188
|
+
}
|
|
189
|
+
const cpx = px - cx, cpy = py - cy, cpz = pz - cz;
|
|
190
|
+
const d5 = abx * cpx + aby * cpy + abz * cpz;
|
|
191
|
+
const d6 = acx * cpx + acy * cpy + acz * cpz;
|
|
192
|
+
if (d6 >= 0 && d5 <= d6) {
|
|
193
|
+
u = 0;
|
|
194
|
+
w = 1;
|
|
195
|
+
return finish();
|
|
196
|
+
}
|
|
197
|
+
const vb = d5 * d2 - d1 * d6;
|
|
198
|
+
if (vb <= 0 && d2 >= 0 && d6 <= 0) {
|
|
199
|
+
const t2 = d2 / (d2 - d6);
|
|
200
|
+
u = 1 - t2;
|
|
201
|
+
w = t2;
|
|
202
|
+
return finish();
|
|
203
|
+
}
|
|
204
|
+
const va = d3 * d6 - d5 * d4;
|
|
205
|
+
if (va <= 0 && d4 - d3 >= 0 && d5 - d6 >= 0) {
|
|
206
|
+
const t3 = (d4 - d3) / (d4 - d3 + (d5 - d6));
|
|
207
|
+
u = 0;
|
|
208
|
+
v = 1 - t3;
|
|
209
|
+
w = t3;
|
|
210
|
+
return finish();
|
|
211
|
+
}
|
|
212
|
+
const denom = 1 / (va + vb + vc);
|
|
213
|
+
v = vb * denom;
|
|
214
|
+
w = vc * denom;
|
|
215
|
+
u = 1 - v - w;
|
|
216
|
+
return finish();
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Rasterize a triangle soup (9 floats a triangle, in the grid's metres)
|
|
220
|
+
* into the grid. Every voxel a triangle touches (separating-axis test, so
|
|
221
|
+
* surfaces are watertight) takes the colour of the triangle closest to its
|
|
222
|
+
* centre. Returns the number of solid voxels written.
|
|
223
|
+
*/
|
|
224
|
+
export function voxelizeTriangles(grid, positions, shade) {
|
|
225
|
+
const n = Math.floor(positions.length / 9);
|
|
226
|
+
const s = grid.size;
|
|
227
|
+
const [ox, oy, oz] = grid.origin;
|
|
228
|
+
const h = s * 0.5 * (1 + 1e-5);
|
|
229
|
+
const cells = grid.nx * grid.ny * grid.nz;
|
|
230
|
+
const best = new Float32Array(cells).fill(Infinity);
|
|
231
|
+
const bestTri = new Int32Array(cells).fill(-1);
|
|
232
|
+
const bary = new Float32Array(cells * 3);
|
|
233
|
+
const tmp = new Float64Array(3);
|
|
234
|
+
for (let t = 0; t < n; t++) {
|
|
235
|
+
const o = t * 9;
|
|
236
|
+
let minx = Infinity, miny = Infinity, minz = Infinity, maxx = -Infinity, maxy = -Infinity, maxz = -Infinity;
|
|
237
|
+
for (let k = 0; k < 3; k++) {
|
|
238
|
+
const x = positions[o + k * 3] ?? 0, y = positions[o + k * 3 + 1] ?? 0, z = positions[o + k * 3 + 2] ?? 0;
|
|
239
|
+
if (x < minx)
|
|
240
|
+
minx = x;
|
|
241
|
+
if (y < miny)
|
|
242
|
+
miny = y;
|
|
243
|
+
if (z < minz)
|
|
244
|
+
minz = z;
|
|
245
|
+
if (x > maxx)
|
|
246
|
+
maxx = x;
|
|
247
|
+
if (y > maxy)
|
|
248
|
+
maxy = y;
|
|
249
|
+
if (z > maxz)
|
|
250
|
+
maxz = z;
|
|
251
|
+
}
|
|
252
|
+
if (!Number.isFinite(minx + miny + minz + maxx + maxy + maxz))
|
|
253
|
+
continue;
|
|
254
|
+
const x0 = Math.max(0, Math.floor((minx - ox) / s - 1e-4)), x1 = Math.min(grid.nx - 1, Math.floor((maxx - ox) / s + 1e-4));
|
|
255
|
+
const y0 = Math.max(0, Math.floor((miny - oy) / s - 1e-4)), y1 = Math.min(grid.ny - 1, Math.floor((maxy - oy) / s + 1e-4));
|
|
256
|
+
const z0 = Math.max(0, Math.floor((minz - oz) / s - 1e-4)), z1 = Math.min(grid.nz - 1, Math.floor((maxz - oz) / s + 1e-4));
|
|
257
|
+
for (let z = z0; z <= z1; z++) {
|
|
258
|
+
const cz = oz + (z + 0.5) * s;
|
|
259
|
+
for (let y = y0; y <= y1; y++) {
|
|
260
|
+
const cy = oy + (y + 0.5) * s;
|
|
261
|
+
for (let x = x0; x <= x1; x++) {
|
|
262
|
+
const cx = ox + (x + 0.5) * s;
|
|
263
|
+
if (!triBoxOverlap(cx, cy, cz, h, positions, o))
|
|
264
|
+
continue;
|
|
265
|
+
const d = closestOnTriangle(cx, cy, cz, positions, o, tmp);
|
|
266
|
+
const i = grid.index(x, y, z);
|
|
267
|
+
if (d < (best[i] ?? Infinity)) {
|
|
268
|
+
best[i] = d;
|
|
269
|
+
bestTri[i] = t;
|
|
270
|
+
bary[i * 3] = tmp[0] ?? 1;
|
|
271
|
+
bary[i * 3 + 1] = tmp[1] ?? 0;
|
|
272
|
+
bary[i * 3 + 2] = tmp[2] ?? 0;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
let written = 0;
|
|
279
|
+
for (let i = 0; i < cells; i++) {
|
|
280
|
+
const t = bestTri[i] ?? -1;
|
|
281
|
+
if (t < 0)
|
|
282
|
+
continue;
|
|
283
|
+
const sample = shade(t, bary[i * 3] ?? 1, bary[i * 3 + 1] ?? 0, bary[i * 3 + 2] ?? 0);
|
|
284
|
+
if (!sample)
|
|
285
|
+
continue;
|
|
286
|
+
grid.color[i] = sample.rgb & 0xffffff;
|
|
287
|
+
grid.part[i] = sample.part;
|
|
288
|
+
written++;
|
|
289
|
+
}
|
|
290
|
+
return written;
|
|
291
|
+
}
|
|
292
|
+
const N6 = [
|
|
293
|
+
[1, 0, 0],
|
|
294
|
+
[-1, 0, 0],
|
|
295
|
+
[0, 1, 0],
|
|
296
|
+
[0, -1, 0],
|
|
297
|
+
[0, 0, 1],
|
|
298
|
+
[0, 0, -1],
|
|
299
|
+
];
|
|
300
|
+
/**
|
|
301
|
+
* Make a voxelized shell solid: every empty cell the outside cannot reach
|
|
302
|
+
* (6-connected flood from the grid's border) becomes solid, with the colour
|
|
303
|
+
* and part of the nearest shell voxel. Returns the cells filled.
|
|
304
|
+
*/
|
|
305
|
+
export function fillInterior(grid) {
|
|
306
|
+
const { nx, ny, nz } = grid;
|
|
307
|
+
const cells = nx * ny * nz;
|
|
308
|
+
const outside = new Uint8Array(cells);
|
|
309
|
+
const queue = new Int32Array(cells);
|
|
310
|
+
let head = 0;
|
|
311
|
+
let tail = 0;
|
|
312
|
+
const push = (x, y, z) => {
|
|
313
|
+
const i = grid.index(x, y, z);
|
|
314
|
+
if (outside[i] || (grid.color[i] ?? EMPTY) !== EMPTY)
|
|
315
|
+
return;
|
|
316
|
+
outside[i] = 1;
|
|
317
|
+
queue[tail++] = i;
|
|
318
|
+
};
|
|
319
|
+
for (let z = 0; z < nz; z++)
|
|
320
|
+
for (let y = 0; y < ny; y++)
|
|
321
|
+
for (let x = 0; x < nx; x++)
|
|
322
|
+
if (x === 0 || y === 0 || z === 0 || x === nx - 1 || y === ny - 1 || z === nz - 1)
|
|
323
|
+
push(x, y, z);
|
|
324
|
+
while (head < tail) {
|
|
325
|
+
const i = queue[head++] ?? 0;
|
|
326
|
+
const x = i % nx;
|
|
327
|
+
const y = Math.floor(i / nx) % ny;
|
|
328
|
+
const z = Math.floor(i / (nx * ny));
|
|
329
|
+
for (const [dx, dy, dz] of N6) {
|
|
330
|
+
const X = x + dx, Y = y + dy, Z = z + dz;
|
|
331
|
+
if (X >= 0 && Y >= 0 && Z >= 0 && X < nx && Y < ny && Z < nz)
|
|
332
|
+
push(X, Y, Z);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// Multi-source flood from the shell into the enclosed cells.
|
|
336
|
+
head = 0;
|
|
337
|
+
tail = 0;
|
|
338
|
+
for (let i = 0; i < cells; i++)
|
|
339
|
+
if ((grid.color[i] ?? EMPTY) !== EMPTY)
|
|
340
|
+
queue[tail++] = i;
|
|
341
|
+
let filled = 0;
|
|
342
|
+
while (head < tail) {
|
|
343
|
+
const i = queue[head++] ?? 0;
|
|
344
|
+
const x = i % nx;
|
|
345
|
+
const y = Math.floor(i / nx) % ny;
|
|
346
|
+
const z = Math.floor(i / (nx * ny));
|
|
347
|
+
for (const [dx, dy, dz] of N6) {
|
|
348
|
+
const X = x + dx, Y = y + dy, Z = z + dz;
|
|
349
|
+
if (X < 0 || Y < 0 || Z < 0 || X >= nx || Y >= ny || Z >= nz)
|
|
350
|
+
continue;
|
|
351
|
+
const j = grid.index(X, Y, Z);
|
|
352
|
+
if (outside[j] || (grid.color[j] ?? EMPTY) !== EMPTY)
|
|
353
|
+
continue;
|
|
354
|
+
grid.color[j] = grid.color[i] ?? 0;
|
|
355
|
+
grid.part[j] = grid.part[i] ?? 0;
|
|
356
|
+
queue[tail++] = j;
|
|
357
|
+
filled++;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return filled;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Remove voxels that touch no empty cell (hidden inside), keeping a model's
|
|
364
|
+
* shell. Saves nothing in the mesh (hidden faces are never emitted) but
|
|
365
|
+
* shortens a list of voxels to throw when a model shatters.
|
|
366
|
+
*/
|
|
367
|
+
export function surfaceVoxels(grid) {
|
|
368
|
+
const out = [];
|
|
369
|
+
for (let z = 0; z < grid.nz; z++)
|
|
370
|
+
for (let y = 0; y < grid.ny; y++)
|
|
371
|
+
for (let x = 0; x < grid.nx; x++) {
|
|
372
|
+
const c = grid.get(x, y, z);
|
|
373
|
+
if (c === EMPTY)
|
|
374
|
+
continue;
|
|
375
|
+
if (N6.some(([dx, dy, dz]) => !grid.solid(x + dx, y + dy, z + dz)))
|
|
376
|
+
out.push({ x, y, z, rgb: c, part: grid.part[grid.index(x, y, z)] ?? 0 });
|
|
377
|
+
}
|
|
378
|
+
return out;
|
|
379
|
+
}
|
|
380
|
+
const AO_DEFAULT = [0.42, 0.62, 0.82, 1];
|
|
381
|
+
/**
|
|
382
|
+
* Greedy-mesh a voxel grid (0fps "Meshing in a Minecraft game", with the
|
|
383
|
+
* occlusion-aware merge from "Ambient occlusion for Minecraft-like
|
|
384
|
+
* worlds"): coplanar faces with the same colour, part and corner occlusion
|
|
385
|
+
* merge into one quad. Faces are emitted where a voxel meets an empty cell,
|
|
386
|
+
* or a voxel of another part when `splitParts` is on.
|
|
387
|
+
*/
|
|
388
|
+
export function greedyMesh(grid, opts = {}) {
|
|
389
|
+
const useAo = opts.ao ?? true;
|
|
390
|
+
const levels = opts.aoLevels ?? AO_DEFAULT;
|
|
391
|
+
const split = opts.splitParts ?? true;
|
|
392
|
+
const skip = new Set(opts.skip ?? []);
|
|
393
|
+
const outside = opts.solidOutside;
|
|
394
|
+
const dims = [grid.nx, grid.ny, grid.nz];
|
|
395
|
+
const s = grid.size;
|
|
396
|
+
const [ox, oy, oz] = grid.origin;
|
|
397
|
+
const pos = [];
|
|
398
|
+
const nor = [];
|
|
399
|
+
const col = [];
|
|
400
|
+
const prt = [];
|
|
401
|
+
const idx = [];
|
|
402
|
+
const solidAt = (x, y, z) => (grid.inside(x, y, z) ? grid.solid(x, y, z) : (outside?.(x, y, z) ?? false));
|
|
403
|
+
// Linear colour cache.
|
|
404
|
+
const lin = new Map();
|
|
405
|
+
const linear = (rgb) => {
|
|
406
|
+
let c = lin.get(rgb);
|
|
407
|
+
if (!c) {
|
|
408
|
+
c = [srgbToLinear((rgb >> 16) & 0xff), srgbToLinear((rgb >> 8) & 0xff), srgbToLinear(rgb & 0xff)];
|
|
409
|
+
lin.set(rgb, c);
|
|
410
|
+
}
|
|
411
|
+
return c;
|
|
412
|
+
};
|
|
413
|
+
const xyz = [0, 0, 0];
|
|
414
|
+
const q = [0, 0, 0];
|
|
415
|
+
for (let d = 0; d < 3; d++) {
|
|
416
|
+
const u = (d + 1) % 3;
|
|
417
|
+
const v = (d + 2) % 3;
|
|
418
|
+
const nu = dims[u];
|
|
419
|
+
const nv = dims[v];
|
|
420
|
+
const nd = dims[d];
|
|
421
|
+
const maskColor = new Int32Array(nu * nv);
|
|
422
|
+
const maskKey = new Int32Array(nu * nv);
|
|
423
|
+
for (const side of [1, -1]) {
|
|
424
|
+
const dirName = `${side > 0 ? "+" : "-"}${"xyz"[d]}`;
|
|
425
|
+
if (skip.has(dirName))
|
|
426
|
+
continue;
|
|
427
|
+
for (let slice = 0; slice < nd; slice++) {
|
|
428
|
+
// Build the face mask for this slice.
|
|
429
|
+
let any = false;
|
|
430
|
+
for (let j = 0; j < nv; j++) {
|
|
431
|
+
for (let i = 0; i < nu; i++) {
|
|
432
|
+
xyz[d] = slice;
|
|
433
|
+
xyz[u] = i;
|
|
434
|
+
xyz[v] = j;
|
|
435
|
+
const x = xyz[0] ?? 0, y = xyz[1] ?? 0, z = xyz[2] ?? 0;
|
|
436
|
+
const m = i + j * nu;
|
|
437
|
+
maskColor[m] = EMPTY;
|
|
438
|
+
const c = grid.get(x, y, z);
|
|
439
|
+
if (c === EMPTY)
|
|
440
|
+
continue;
|
|
441
|
+
q[0] = x;
|
|
442
|
+
q[1] = y;
|
|
443
|
+
q[2] = z;
|
|
444
|
+
q[d] = (q[d] ?? 0) + side;
|
|
445
|
+
const bx = q[0] ?? 0, by = q[1] ?? 0, bz = q[2] ?? 0;
|
|
446
|
+
const pa = grid.part[grid.index(x, y, z)] ?? 0;
|
|
447
|
+
let visible;
|
|
448
|
+
if (grid.inside(bx, by, bz)) {
|
|
449
|
+
const nb = grid.get(bx, by, bz);
|
|
450
|
+
visible = nb === EMPTY || (split && (grid.part[grid.index(bx, by, bz)] ?? 0) !== pa);
|
|
451
|
+
}
|
|
452
|
+
else
|
|
453
|
+
visible = !(outside?.(bx, by, bz) ?? false);
|
|
454
|
+
if (!visible)
|
|
455
|
+
continue;
|
|
456
|
+
let ao = 0xff;
|
|
457
|
+
if (useAo) {
|
|
458
|
+
// Corners (-u,-v), (+u,-v), (+u,+v), (-u,+v) on the layer in front of the face.
|
|
459
|
+
const occ = (du, dv) => {
|
|
460
|
+
q[0] = x;
|
|
461
|
+
q[1] = y;
|
|
462
|
+
q[2] = z;
|
|
463
|
+
q[d] = (q[d] ?? 0) + side;
|
|
464
|
+
q[u] = (q[u] ?? 0) + du;
|
|
465
|
+
q[v] = (q[v] ?? 0) + dv;
|
|
466
|
+
return solidAt(q[0] ?? 0, q[1] ?? 0, q[2] ?? 0);
|
|
467
|
+
};
|
|
468
|
+
const level = (du, dv) => {
|
|
469
|
+
const s1 = occ(du, 0);
|
|
470
|
+
const s2 = occ(0, dv);
|
|
471
|
+
if (s1 && s2)
|
|
472
|
+
return 0;
|
|
473
|
+
return 3 - ((s1 ? 1 : 0) + (s2 ? 1 : 0) + (occ(du, dv) ? 1 : 0));
|
|
474
|
+
};
|
|
475
|
+
ao = level(-1, -1) | (level(1, -1) << 2) | (level(1, 1) << 4) | (level(-1, 1) << 6);
|
|
476
|
+
}
|
|
477
|
+
maskColor[m] = c;
|
|
478
|
+
maskKey[m] = (pa << 8) | ao;
|
|
479
|
+
any = true;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (!any)
|
|
483
|
+
continue;
|
|
484
|
+
// Greedy merge.
|
|
485
|
+
for (let j = 0; j < nv; j++) {
|
|
486
|
+
for (let i = 0; i < nu;) {
|
|
487
|
+
const m = i + j * nu;
|
|
488
|
+
const c = maskColor[m] ?? EMPTY;
|
|
489
|
+
if (c === EMPTY) {
|
|
490
|
+
i++;
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
const key = maskKey[m] ?? 0;
|
|
494
|
+
let w = 1;
|
|
495
|
+
while (i + w < nu && maskColor[m + w] === c && maskKey[m + w] === key)
|
|
496
|
+
w++;
|
|
497
|
+
let h = 1;
|
|
498
|
+
outer: while (j + h < nv) {
|
|
499
|
+
for (let k = 0; k < w; k++) {
|
|
500
|
+
const mm = i + k + (j + h) * nu;
|
|
501
|
+
if (maskColor[mm] !== c || maskKey[mm] !== key)
|
|
502
|
+
break outer;
|
|
503
|
+
}
|
|
504
|
+
h++;
|
|
505
|
+
}
|
|
506
|
+
for (let jj = 0; jj < h; jj++)
|
|
507
|
+
for (let ii = 0; ii < w; ii++)
|
|
508
|
+
maskColor[i + ii + (j + jj) * nu] = EMPTY;
|
|
509
|
+
// Emit the quad.
|
|
510
|
+
const plane = slice + (side > 0 ? 1 : 0);
|
|
511
|
+
const corner = (cu, cv) => {
|
|
512
|
+
xyz[d] = plane;
|
|
513
|
+
xyz[u] = cu;
|
|
514
|
+
xyz[v] = cv;
|
|
515
|
+
pos.push(ox + (xyz[0] ?? 0) * s, oy + (xyz[1] ?? 0) * s, oz + (xyz[2] ?? 0) * s);
|
|
516
|
+
};
|
|
517
|
+
const base = pos.length / 3;
|
|
518
|
+
corner(i, j);
|
|
519
|
+
corner(i + w, j);
|
|
520
|
+
corner(i + w, j + h);
|
|
521
|
+
corner(i, j + h);
|
|
522
|
+
const ao = key & 0xff;
|
|
523
|
+
const a = [(ao & 3), (ao >> 2) & 3, (ao >> 4) & 3, (ao >> 6) & 3];
|
|
524
|
+
const [lr, lg, lb] = linear(c);
|
|
525
|
+
const part = key >>> 8;
|
|
526
|
+
for (let k = 0; k < 4; k++) {
|
|
527
|
+
const b = useAo ? (levels[a[k] ?? 3] ?? 1) : 1;
|
|
528
|
+
col.push(lr * b, lg * b, lb * b);
|
|
529
|
+
nor.push(d === 0 ? side : 0, d === 1 ? side : 0, d === 2 ? side : 0);
|
|
530
|
+
prt.push(part);
|
|
531
|
+
}
|
|
532
|
+
// Pick the diagonal the way 0fps does, so occlusion interpolates without a crease across the quad.
|
|
533
|
+
const flip = (a[0] ?? 0) + (a[2] ?? 0) > (a[1] ?? 0) + (a[3] ?? 0);
|
|
534
|
+
if (side > 0) {
|
|
535
|
+
if (flip)
|
|
536
|
+
idx.push(base + 1, base + 2, base + 3, base + 1, base + 3, base);
|
|
537
|
+
else
|
|
538
|
+
idx.push(base, base + 1, base + 2, base, base + 2, base + 3);
|
|
539
|
+
}
|
|
540
|
+
else if (flip)
|
|
541
|
+
idx.push(base + 1, base + 3, base + 2, base + 1, base, base + 3);
|
|
542
|
+
else
|
|
543
|
+
idx.push(base, base + 2, base + 1, base, base + 3, base + 2);
|
|
544
|
+
i += w;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return {
|
|
551
|
+
positions: new Float32Array(pos),
|
|
552
|
+
normals: new Float32Array(nor),
|
|
553
|
+
colors: new Float32Array(col),
|
|
554
|
+
parts: new Uint16Array(prt),
|
|
555
|
+
indices: new Uint32Array(idx),
|
|
556
|
+
quads: idx.length / 6,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
// ---------------------------------------------------------------------------
|
|
560
|
+
// Shapes for procedural voxel models (pickups, scenery)
|
|
561
|
+
/** Fill every cell whose centre (in grid cells, from the grid's corner) passes `inside`, coloured by `paint`. */
|
|
562
|
+
export function fillWhere(grid, inside, paint, part = 0) {
|
|
563
|
+
let n = 0;
|
|
564
|
+
for (let z = 0; z < grid.nz; z++)
|
|
565
|
+
for (let y = 0; y < grid.ny; y++)
|
|
566
|
+
for (let x = 0; x < grid.nx; x++) {
|
|
567
|
+
if (!inside(x + 0.5, y + 0.5, z + 0.5))
|
|
568
|
+
continue;
|
|
569
|
+
grid.set(x, y, z, paint(x, y, z), part);
|
|
570
|
+
n++;
|
|
571
|
+
}
|
|
572
|
+
return n;
|
|
573
|
+
}
|
|
574
|
+
/** A small palette shade jitter: the colour scaled by 1 +- `amount`, chosen by the voxel's hash. */
|
|
575
|
+
export function jitter(rgb, x, y, z, amount = 0.06, seed = 0) {
|
|
576
|
+
return shadeRgb(rgb, 1 + (hash3(x, y, z, seed) * 2 - 1) * amount);
|
|
577
|
+
}
|