@volter/threejs-runtime 0.5.65
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/LICENSE +202 -0
- package/NOTICE +12 -0
- package/package.json +62 -0
- package/src/adapter/body-marks.ts +69 -0
- package/src/adapter/constraint.ts +78 -0
- package/src/adapter/first-party-navigation-system.ts +48 -0
- package/src/adapter/hierarchy-marks.ts +156 -0
- package/src/adapter/ingest/scene-capture.ts +865 -0
- package/src/adapter/ingest/structural-ids.ts +139 -0
- package/src/adapter/ingest/visible-capture-window.ts +302 -0
- package/src/adapter/object3d-authoring-subject.ts +50 -0
- package/src/adapter/rapier-physics-adapter.ts +226 -0
- package/src/adapter/reflection-probe.ts +75 -0
- package/src/adapter/renderer-config.ts +116 -0
- package/src/adapter/trigger-volume.ts +29 -0
- package/src/ai/navigation.ts +627 -0
- package/src/animation/animation-clock.ts +479 -0
- package/src/animation/runtime-inspection.ts +45 -0
- package/src/animation/xstate-animation-binding.ts +655 -0
- package/src/animation/xstate-animation-meta.ts +360 -0
- package/src/asset-formats/camera.ts +18 -0
- package/src/asset-formats/collider.ts +66 -0
- package/src/asset-formats/index.ts +45 -0
- package/src/asset-formats/light.ts +50 -0
- package/src/asset-formats/material.ts +93 -0
- package/src/asset-formats/mesh.ts +78 -0
- package/src/asset-formats/particles.ts +426 -0
- package/src/asset-formats/render-env.ts +318 -0
- package/src/asset-formats/tuples.ts +21 -0
- package/src/asset-loaders.ts +242 -0
- package/src/asset-parse-error.ts +29 -0
- package/src/assets.ts +68 -0
- package/src/behavior/xstate-inspection.ts +51 -0
- package/src/defaults.ts +135 -0
- package/src/ecs/object-marks.ts +75 -0
- package/src/ecs/scene-index.ts +439 -0
- package/src/ecs/scene-query.ts +43 -0
- package/src/ecs/user-data.ts +272 -0
- package/src/loader.ts +134 -0
- package/src/physics/collider-dimensions.ts +121 -0
- package/src/physics/collision-system.ts +76 -0
- package/src/physics/physics-registry.ts +83 -0
- package/src/physics/transform-writer.ts +41 -0
- package/src/render/directional-shadow-fit.ts +158 -0
- package/src/render/environment-capture.ts +133 -0
- package/src/render/ibl-override-material.ts +202 -0
- package/src/render/light-camera-factory.ts +103 -0
- package/src/render/matcap-texture.ts +92 -0
- package/src/render/material-factory.ts +201 -0
- package/src/render/mesh-shadow.ts +18 -0
- package/src/render/particles-factory.ts +589 -0
- package/src/render/render-batch-system.ts +260 -0
- package/src/render/render-features.ts +128 -0
- package/src/render/render-settings.ts +71 -0
- package/src/render/soft-particle-depth.ts +360 -0
- package/src/render/spark-renderer-lifecycle.ts +64 -0
- package/src/render/structural-signature.ts +231 -0
- package/src/render/viewport-shading.ts +163 -0
- package/src/render/voxel-surface.ts +358 -0
- package/src/setup/setup-particles.ts +32 -0
- package/src/setup/setup-physics.ts +67 -0
- package/src/setup/setup-renderer.ts +524 -0
- package/src/types-n8ao.d.ts +37 -0
- package/src/types-realism-effects.d.ts +61 -0
- package/vendor/realism-effects/LICENSE.md +21 -0
- package/vendor/realism-effects/UPSTREAM.md +19 -0
- package/vendor/realism-effects/dist/index.cjs +3447 -0
- package/vendor/realism-effects/dist/index.d.ts +59 -0
- package/vendor/realism-effects/dist/index.js +3434 -0
- package/vendor/realism-effects/package.json +23 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { createNeutralMatcapTexture } from './matcap-texture';
|
|
3
|
+
|
|
4
|
+
/** Temporary developer-facing shading modes. These never become scene data. */
|
|
5
|
+
export type ViewportShadingMode =
|
|
6
|
+
| 'solid'
|
|
7
|
+
| 'clay'
|
|
8
|
+
| 'unlit'
|
|
9
|
+
| 'wireframe'
|
|
10
|
+
| 'matcap'
|
|
11
|
+
| 'normals'
|
|
12
|
+
| 'overdraw';
|
|
13
|
+
|
|
14
|
+
type MaterialPair = {
|
|
15
|
+
clay: THREE.MeshStandardMaterial;
|
|
16
|
+
unlit: THREE.MeshBasicMaterial;
|
|
17
|
+
wireframe: THREE.MeshBasicMaterial;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function materialColor(material: THREE.Material): THREE.Color {
|
|
21
|
+
const color = (material as THREE.Material & { color?: THREE.Color }).color;
|
|
22
|
+
return color?.clone() ?? new THREE.Color(0xbec7d1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function textureOf(material: THREE.Material, key: 'map' | 'alphaMap'): THREE.Texture | null {
|
|
26
|
+
return (
|
|
27
|
+
(material as THREE.Material & { map?: THREE.Texture | null; alphaMap?: THREE.Texture | null })[
|
|
28
|
+
key
|
|
29
|
+
] ?? null
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Applies a render-only material view and restores every native material in a
|
|
35
|
+
* `finally` block. Gameplay and authoring therefore always observe the real
|
|
36
|
+
* materials; only renderer work inside {@link render} sees the diagnostic view.
|
|
37
|
+
*/
|
|
38
|
+
export class ViewportShadingRenderer {
|
|
39
|
+
private readonly _derived = new Map<THREE.Material, MaterialPair>();
|
|
40
|
+
private readonly _normals = new THREE.MeshNormalMaterial();
|
|
41
|
+
/**
|
|
42
|
+
* The clay/sculpt look. Built on FIRST USE, not at construction: the matcap
|
|
43
|
+
* sphere is drawn into a 2D canvas, and every ViewportShadingRenderer that
|
|
44
|
+
* only ever draws `solid` would otherwise pay for a texture nobody samples.
|
|
45
|
+
* `toneMapped: false` for the same reason the overdraw material sets it —
|
|
46
|
+
* a diagnostic look must read exactly as authored, not as the document's
|
|
47
|
+
* exposure and tone curve happen to grade it.
|
|
48
|
+
*/
|
|
49
|
+
private _matcap: THREE.MeshMatcapMaterial | null = null;
|
|
50
|
+
private _matcapTexture: THREE.Texture | null = null;
|
|
51
|
+
private readonly _overdraw = new THREE.MeshBasicMaterial({
|
|
52
|
+
color: 0xffffff,
|
|
53
|
+
transparent: true,
|
|
54
|
+
opacity: 0.08,
|
|
55
|
+
depthTest: false,
|
|
56
|
+
depthWrite: false,
|
|
57
|
+
blending: THREE.AdditiveBlending,
|
|
58
|
+
side: THREE.DoubleSide,
|
|
59
|
+
toneMapped: false,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
render(
|
|
63
|
+
scene: THREE.Scene,
|
|
64
|
+
mode: ViewportShadingMode,
|
|
65
|
+
draw: () => void,
|
|
66
|
+
include: (mesh: THREE.Mesh) => boolean = () => true,
|
|
67
|
+
): void {
|
|
68
|
+
if (mode === 'solid') {
|
|
69
|
+
draw();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const originals: Array<{
|
|
74
|
+
mesh: THREE.Mesh;
|
|
75
|
+
material: THREE.Material | THREE.Material[];
|
|
76
|
+
}> = [];
|
|
77
|
+
scene.traverse((object) => {
|
|
78
|
+
const mesh = object as THREE.Mesh;
|
|
79
|
+
if (!mesh.isMesh || !mesh.material || !include(mesh)) return;
|
|
80
|
+
originals.push({ mesh, material: mesh.material });
|
|
81
|
+
mesh.material = Array.isArray(mesh.material)
|
|
82
|
+
? mesh.material.map((material) => this._materialFor(material, mode))
|
|
83
|
+
: this._materialFor(mesh.material, mode);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
draw();
|
|
88
|
+
} finally {
|
|
89
|
+
for (const { mesh, material } of originals) mesh.material = material;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
dispose(): void {
|
|
94
|
+
for (const pair of this._derived.values()) {
|
|
95
|
+
pair.clay.dispose();
|
|
96
|
+
pair.unlit.dispose();
|
|
97
|
+
pair.wireframe.dispose();
|
|
98
|
+
}
|
|
99
|
+
this._derived.clear();
|
|
100
|
+
this._normals.dispose();
|
|
101
|
+
this._overdraw.dispose();
|
|
102
|
+
this._matcap?.dispose();
|
|
103
|
+
this._matcapTexture?.dispose();
|
|
104
|
+
this._matcap = null;
|
|
105
|
+
this._matcapTexture = null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private _materialFor(
|
|
109
|
+
material: THREE.Material,
|
|
110
|
+
mode: Exclude<ViewportShadingMode, 'solid'>,
|
|
111
|
+
): THREE.Material {
|
|
112
|
+
if (mode === 'normals') return this._normals;
|
|
113
|
+
if (mode === 'overdraw') return this._overdraw;
|
|
114
|
+
if (mode === 'matcap') {
|
|
115
|
+
if (!this._matcap) {
|
|
116
|
+
this._matcapTexture = createNeutralMatcapTexture();
|
|
117
|
+
this._matcap = new THREE.MeshMatcapMaterial({
|
|
118
|
+
matcap: this._matcapTexture,
|
|
119
|
+
toneMapped: false,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return this._matcap;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let pair = this._derived.get(material);
|
|
126
|
+
if (!pair) {
|
|
127
|
+
const common: THREE.MeshBasicMaterialParameters = {
|
|
128
|
+
color: materialColor(material),
|
|
129
|
+
map: textureOf(material, 'map'),
|
|
130
|
+
alphaMap: textureOf(material, 'alphaMap'),
|
|
131
|
+
alphaTest: material.alphaTest,
|
|
132
|
+
opacity: material.opacity,
|
|
133
|
+
transparent: material.transparent,
|
|
134
|
+
side: material.side,
|
|
135
|
+
depthTest: material.depthTest,
|
|
136
|
+
depthWrite: material.depthWrite,
|
|
137
|
+
vertexColors: Boolean(
|
|
138
|
+
(material as THREE.Material & { vertexColors?: boolean }).vertexColors,
|
|
139
|
+
),
|
|
140
|
+
fog: (material as THREE.Material & { fog?: boolean }).fog ?? true,
|
|
141
|
+
};
|
|
142
|
+
pair = {
|
|
143
|
+
clay: new THREE.MeshStandardMaterial({
|
|
144
|
+
color: 0xaeb6c0,
|
|
145
|
+
roughness: 0.82,
|
|
146
|
+
metalness: 0,
|
|
147
|
+
flatShading: true,
|
|
148
|
+
alphaMap: textureOf(material, 'alphaMap'),
|
|
149
|
+
alphaTest: material.alphaTest,
|
|
150
|
+
opacity: material.opacity,
|
|
151
|
+
transparent: material.transparent,
|
|
152
|
+
side: material.side,
|
|
153
|
+
depthTest: material.depthTest,
|
|
154
|
+
depthWrite: material.depthWrite,
|
|
155
|
+
}),
|
|
156
|
+
unlit: new THREE.MeshBasicMaterial(common),
|
|
157
|
+
wireframe: new THREE.MeshBasicMaterial({ ...common, wireframe: true }),
|
|
158
|
+
};
|
|
159
|
+
this._derived.set(material, pair);
|
|
160
|
+
}
|
|
161
|
+
return pair[mode];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A face-culled surface mesh for a dense grid of cubic cells, as one real `THREE.BufferGeometry`.
|
|
3
|
+
*
|
|
4
|
+
* A game that stores its world as a grid of cubes — terrain a player digs, a builder's block
|
|
5
|
+
* palette, a level authored as a 3D tilemap — cannot draw a `BoxGeometry` per cell: a modest
|
|
6
|
+
* 80×7×80 volume is 44 800 boxes, and the ones buried inside the solid are invisible forever. The
|
|
7
|
+
* fix is a single surface built from only the faces that touch an empty neighbour. That is what
|
|
8
|
+
* this file is, and it is the whole of it.
|
|
9
|
+
*
|
|
10
|
+
* ## It is a HELPER, not a wrapper
|
|
11
|
+
*
|
|
12
|
+
* `buildVoxelSurface` takes plain arrays and hands back three's own `BufferGeometry`. Every call
|
|
13
|
+
* after it is three's API — pick your own material, wrap it in a `Mesh` or an `InstancedMesh`,
|
|
14
|
+
* `.translate()` it, hand it to Rapier's trimesh collider. Nothing here holds a scene, a camera, a
|
|
15
|
+
* frame loop or a cache, and nothing here is a class you inherit from.
|
|
16
|
+
*
|
|
17
|
+
* ## The rung audit that put ~200 lines here instead of a dependency (2026-08-14)
|
|
18
|
+
*
|
|
19
|
+
* Checked against the npm registry on the day this landed, not from memory:
|
|
20
|
+
*
|
|
21
|
+
* - **`voxel-mesh` 0.3.0** (voxel.js) — last publish **2013-06-13**. Its `index.js` builds a
|
|
22
|
+
* `THREE.Geometry` out of `THREE.Face3`, both REMOVED from three in r125. It cannot run against
|
|
23
|
+
* this repo's three at all.
|
|
24
|
+
* - **`voxel-mesher` 0.14.4** (2016-03-26) and **`greedy-mesher` 1.0.3** (2016-02-12) — the
|
|
25
|
+
* voxel.js meshing pair. Both are `ndarray`-shaped, untyped, and stop at a list of quads: the
|
|
26
|
+
* `BufferGeometry` is still yours to write. `voxel-mesher` additionally pulls a WebGL1 buffer
|
|
27
|
+
* stack (`gl-buffer`, `gl-vao`) that has nothing to do with three.
|
|
28
|
+
* - **`voxel-engine` 0.20.2** (2015-04-11) — a whole game framework (20 dependencies, its own
|
|
29
|
+
* camera, controls and physics). The thing this rule bans.
|
|
30
|
+
* - **`@voxelize/core` 3.0.0** (2026-08-03) — the one MAINTAINED option, and the one to say no to
|
|
31
|
+
* deliberately. It is a client for a multiplayer voxel platform: `socket.io-client`, its own
|
|
32
|
+
* wire protocol, its own physics engine and a WASM mesher fed by its own chunk/world objects.
|
|
33
|
+
* You do not call its mesher; you build your game inside it. Adopting it to draw a static grid
|
|
34
|
+
* would put a platform between the game and three.
|
|
35
|
+
*
|
|
36
|
+
* So: every maintained option is an ENGINE you build inside, and every option that is a helper died
|
|
37
|
+
* with voxel.js a decade ago against a three that no longer exists. The general shape *is* ready —
|
|
38
|
+
* "cull the hidden faces of a dense grid" has been settled since 2012 — it is the packaging that is
|
|
39
|
+
* not. Hence a helper here, in host vocabulary, that hands back three's own object.
|
|
40
|
+
*
|
|
41
|
+
* ## What is deliberately NOT here
|
|
42
|
+
*
|
|
43
|
+
* **Greedy meshing.** Merging coplanar same-type faces into larger quads is the standard next step
|
|
44
|
+
* and it does not fall out of this one: it needs a per-slice sweep with its own visited mask and
|
|
45
|
+
* its own bugs, and it changes the vertex↔cell relationship that per-face culling keeps exact.
|
|
46
|
+
* Per-face culling already removes the interior, which is the order-of-magnitude win. Add greedy
|
|
47
|
+
* meshing the day a measured frame budget asks for it, as its own function beside this one.
|
|
48
|
+
*
|
|
49
|
+
* **Lighting, ambient occlusion, textures, atlases, transparency sorting and chunking.** Each is a
|
|
50
|
+
* real decision a game makes for itself; this returns geometry and gets out of the way.
|
|
51
|
+
*/
|
|
52
|
+
import { BufferAttribute, BufferGeometry, Color } from 'three';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A dense grid of cubic cells.
|
|
56
|
+
*
|
|
57
|
+
* `cells` is indexed **Y-major, then Z, then X** — `((y * sizeZ) + z) * sizeX + x`. Y outermost is
|
|
58
|
+
* the ordering that makes each horizontal LAYER contiguous, which is what lets a mostly-flat world
|
|
59
|
+
* ship as a handful of runs (see {@link expandVoxelRuns}) instead of a megabyte of literals.
|
|
60
|
+
*/
|
|
61
|
+
export interface VoxelVolume {
|
|
62
|
+
/** Cell counts along X, Y and Z. */
|
|
63
|
+
readonly size: readonly [number, number, number];
|
|
64
|
+
/** `sizeX * sizeY * sizeZ` type ids. `0` is empty unless {@link VoxelSurfaceOptions.emptyType}. */
|
|
65
|
+
readonly cells: ArrayLike<number>;
|
|
66
|
+
/** World position of the volume's minimum corner. Defaults to the origin. */
|
|
67
|
+
readonly origin?: readonly [number, number, number];
|
|
68
|
+
/** World edge length of one cell. Defaults to 1. */
|
|
69
|
+
readonly cellSize?: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface VoxelSurfaceOptions {
|
|
73
|
+
/**
|
|
74
|
+
* Type id → `0xRRGGBB`, written into the geometry's `color` attribute. Supply it and the mesh
|
|
75
|
+
* draws with `vertexColors: true` in one draw call; omit it and the geometry carries no colour
|
|
76
|
+
* attribute at all. A solid type absent from the map is an ERROR, not a silent default: a
|
|
77
|
+
* missing colour is the shape of a forgotten row, and a mesh that quietly paints it white looks
|
|
78
|
+
* like a lighting bug three files away.
|
|
79
|
+
*/
|
|
80
|
+
readonly colors?: ReadonlyMap<number, number>;
|
|
81
|
+
/** The type id that means "no cell here". Defaults to `0`. */
|
|
82
|
+
readonly emptyType?: number;
|
|
83
|
+
/**
|
|
84
|
+
* Whether cells just outside the volume count as solid. `false` (the default) walls the volume
|
|
85
|
+
* in — the right answer for a standalone model. `true` leaves the boundary open, which is what a
|
|
86
|
+
* chunk meshed beside its neighbours wants.
|
|
87
|
+
*/
|
|
88
|
+
readonly closedBoundary?: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface VoxelSurface {
|
|
92
|
+
/** Indexed, with `position`, `normal` and (when colours were supplied) `color`. */
|
|
93
|
+
readonly geometry: BufferGeometry;
|
|
94
|
+
/** Faces that survived culling. `vertices` is `4 ×` this, `triangles` `2 ×`. */
|
|
95
|
+
readonly quads: number;
|
|
96
|
+
readonly vertices: number;
|
|
97
|
+
readonly triangles: number;
|
|
98
|
+
/** Cells whose type is not the empty type — the input's own size, for a sanity check. */
|
|
99
|
+
readonly solidCells: number;
|
|
100
|
+
/** Faces contributed per type id, in ascending id order. */
|
|
101
|
+
readonly quadsByType: ReadonlyMap<number, number>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export class VoxelSurfaceError extends Error {
|
|
105
|
+
constructor(message: string) {
|
|
106
|
+
super(message);
|
|
107
|
+
this.name = 'VoxelSurfaceError';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The six faces of the unit cell, each as four corners in counter-clockwise order seen from
|
|
113
|
+
* OUTSIDE — which is the winding three's default `FrontSide` draws.
|
|
114
|
+
*/
|
|
115
|
+
const FACES: readonly {
|
|
116
|
+
readonly normal: readonly [number, number, number];
|
|
117
|
+
readonly corners: readonly (readonly [number, number, number])[];
|
|
118
|
+
}[] = [
|
|
119
|
+
{
|
|
120
|
+
normal: [1, 0, 0],
|
|
121
|
+
corners: [
|
|
122
|
+
[1, 0, 1],
|
|
123
|
+
[1, 0, 0],
|
|
124
|
+
[1, 1, 0],
|
|
125
|
+
[1, 1, 1],
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
normal: [-1, 0, 0],
|
|
130
|
+
corners: [
|
|
131
|
+
[0, 0, 0],
|
|
132
|
+
[0, 0, 1],
|
|
133
|
+
[0, 1, 1],
|
|
134
|
+
[0, 1, 0],
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
normal: [0, 1, 0],
|
|
139
|
+
corners: [
|
|
140
|
+
[0, 1, 1],
|
|
141
|
+
[1, 1, 1],
|
|
142
|
+
[1, 1, 0],
|
|
143
|
+
[0, 1, 0],
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
normal: [0, -1, 0],
|
|
148
|
+
corners: [
|
|
149
|
+
[0, 0, 0],
|
|
150
|
+
[1, 0, 0],
|
|
151
|
+
[1, 0, 1],
|
|
152
|
+
[0, 0, 1],
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
normal: [0, 0, 1],
|
|
157
|
+
corners: [
|
|
158
|
+
[0, 0, 1],
|
|
159
|
+
[1, 0, 1],
|
|
160
|
+
[1, 1, 1],
|
|
161
|
+
[0, 1, 1],
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
normal: [0, 0, -1],
|
|
166
|
+
corners: [
|
|
167
|
+
[1, 0, 0],
|
|
168
|
+
[0, 0, 0],
|
|
169
|
+
[0, 1, 0],
|
|
170
|
+
[1, 1, 0],
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Expand `[count, type, count, type, …]` run pairs into a dense cell array.
|
|
177
|
+
*
|
|
178
|
+
* A voxel volume is enormous and almost entirely repetitive, so the compact form is what a game
|
|
179
|
+
* SHIPS — in a source file, in a save, over the wire — and the dense form is what it meshes. The
|
|
180
|
+
* total of the counts must be exactly `cellCount`: a short or long run list is a corrupt volume,
|
|
181
|
+
* and silently padding it with empties would mesh a plausible wrong world.
|
|
182
|
+
*/
|
|
183
|
+
export function expandVoxelRuns(
|
|
184
|
+
runs: ArrayLike<number>,
|
|
185
|
+
cellCount: number,
|
|
186
|
+
): Uint16Array | Uint32Array {
|
|
187
|
+
if (runs.length % 2 !== 0) {
|
|
188
|
+
throw new VoxelSurfaceError(
|
|
189
|
+
`a run list is [count, type] pairs, so its length must be even; got ${runs.length}`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
let widest = 0;
|
|
193
|
+
let total = 0;
|
|
194
|
+
for (let pair = 0; pair < runs.length; pair += 2) {
|
|
195
|
+
const count = runs[pair] as number;
|
|
196
|
+
if (!Number.isInteger(count) || count <= 0) {
|
|
197
|
+
throw new VoxelSurfaceError(`run ${pair / 2} has a count of ${count}; counts are positive`);
|
|
198
|
+
}
|
|
199
|
+
total += count;
|
|
200
|
+
widest = Math.max(widest, runs[pair + 1] as number);
|
|
201
|
+
}
|
|
202
|
+
if (total !== cellCount) {
|
|
203
|
+
throw new VoxelSurfaceError(`the runs cover ${total} cells; the volume holds ${cellCount}`);
|
|
204
|
+
}
|
|
205
|
+
const cells = widest > 0xffff ? new Uint32Array(cellCount) : new Uint16Array(cellCount);
|
|
206
|
+
let at = 0;
|
|
207
|
+
for (let pair = 0; pair < runs.length; pair += 2) {
|
|
208
|
+
const count = runs[pair] as number;
|
|
209
|
+
cells.fill(runs[pair + 1] as number, at, at + count);
|
|
210
|
+
at += count;
|
|
211
|
+
}
|
|
212
|
+
return cells;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** The volume's size, after checking that the cell array is exactly the size it claims. */
|
|
216
|
+
function assertVolumeShape(volume: VoxelVolume): readonly [number, number, number] {
|
|
217
|
+
const [sizeX, sizeY, sizeZ] = volume.size;
|
|
218
|
+
if (sizeX <= 0 || sizeY <= 0 || sizeZ <= 0) {
|
|
219
|
+
throw new VoxelSurfaceError(`a volume needs a positive size on every axis; got ${volume.size}`);
|
|
220
|
+
}
|
|
221
|
+
const cellCount = sizeX * sizeY * sizeZ;
|
|
222
|
+
if (volume.cells.length !== cellCount) {
|
|
223
|
+
throw new VoxelSurfaceError(
|
|
224
|
+
`a ${sizeX}×${sizeY}×${sizeZ} volume holds ${cellCount} cells; got ${volume.cells.length}`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
return [sizeX, sizeY, sizeZ];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Every array the walk appends to, so the per-cell step is a plain function. */
|
|
231
|
+
interface SurfaceBuffers {
|
|
232
|
+
readonly positions: number[];
|
|
233
|
+
readonly normals: number[];
|
|
234
|
+
readonly colors: number[];
|
|
235
|
+
readonly indices: number[];
|
|
236
|
+
readonly quadsByType: Map<number, number>;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** The linear-space vertex colour for one type, or `undefined` when colours were not asked for. */
|
|
240
|
+
function vertexColor(
|
|
241
|
+
colors: ReadonlyMap<number, number> | undefined,
|
|
242
|
+
type: number,
|
|
243
|
+
where: string,
|
|
244
|
+
into: Color,
|
|
245
|
+
): Color | undefined {
|
|
246
|
+
if (colors === undefined) return undefined;
|
|
247
|
+
const hex = colors.get(type);
|
|
248
|
+
if (hex === undefined) {
|
|
249
|
+
throw new VoxelSurfaceError(
|
|
250
|
+
`no colour for type ${type}, which cell ${where} holds — every solid type in the volume ` +
|
|
251
|
+
'needs a row, so a forgotten one fails here rather than drawing white',
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
// `setHex` defaults to `SRGBColorSpace` and converts INTO the working space itself, which is
|
|
255
|
+
// exactly what a material does with a colour uniform. Do not convert again on top of it: an
|
|
256
|
+
// extra `convertSRGBToLinear()` here squared the conversion and drew a mid-green world in near
|
|
257
|
+
// black, with every channel still plausible enough to read as a lighting problem.
|
|
258
|
+
return into.setHex(hex);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Append the faces of one solid cell that face an empty neighbour. Returns how many. */
|
|
262
|
+
function emitCellFaces(
|
|
263
|
+
buffers: SurfaceBuffers,
|
|
264
|
+
cell: readonly [number, number, number],
|
|
265
|
+
type: number,
|
|
266
|
+
color: Color | undefined,
|
|
267
|
+
solidAt: (x: number, y: number, z: number) => boolean,
|
|
268
|
+
place: (x: number, y: number, z: number) => [number, number, number],
|
|
269
|
+
): number {
|
|
270
|
+
const [x, y, z] = cell;
|
|
271
|
+
let emitted = 0;
|
|
272
|
+
for (const face of FACES) {
|
|
273
|
+
const [nx, ny, nz] = face.normal;
|
|
274
|
+
if (solidAt(x + nx, y + ny, z + nz)) continue;
|
|
275
|
+
const base = buffers.positions.length / 3;
|
|
276
|
+
for (const [cx, cy, cz] of face.corners) {
|
|
277
|
+
buffers.positions.push(...place(x + cx, y + cy, z + cz));
|
|
278
|
+
buffers.normals.push(nx, ny, nz);
|
|
279
|
+
if (color !== undefined) buffers.colors.push(color.r, color.g, color.b);
|
|
280
|
+
}
|
|
281
|
+
buffers.indices.push(base, base + 1, base + 2, base, base + 2, base + 3);
|
|
282
|
+
buffers.quadsByType.set(type, (buffers.quadsByType.get(type) ?? 0) + 1);
|
|
283
|
+
emitted += 1;
|
|
284
|
+
}
|
|
285
|
+
return emitted;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Build the visible surface of a voxel volume: one indexed `BufferGeometry` carrying every face
|
|
290
|
+
* that touches an empty neighbour, and nothing else.
|
|
291
|
+
*/
|
|
292
|
+
export function buildVoxelSurface(
|
|
293
|
+
volume: VoxelVolume,
|
|
294
|
+
options: VoxelSurfaceOptions = {},
|
|
295
|
+
): VoxelSurface {
|
|
296
|
+
const [sizeX, sizeY, sizeZ] = assertVolumeShape(volume);
|
|
297
|
+
const empty = options.emptyType ?? 0;
|
|
298
|
+
const openBoundary = options.closedBoundary !== true;
|
|
299
|
+
const scale = volume.cellSize ?? 1;
|
|
300
|
+
const [originX, originY, originZ] = volume.origin ?? [0, 0, 0];
|
|
301
|
+
const cells = volume.cells;
|
|
302
|
+
|
|
303
|
+
const at = (x: number, y: number, z: number): number =>
|
|
304
|
+
cells[(y * sizeZ + z) * sizeX + x] as number;
|
|
305
|
+
const solidAt = (x: number, y: number, z: number): boolean => {
|
|
306
|
+
const outsideVolume = x < 0 || y < 0 || z < 0 || x >= sizeX || y >= sizeY || z >= sizeZ;
|
|
307
|
+
return outsideVolume ? !openBoundary : at(x, y, z) !== empty;
|
|
308
|
+
};
|
|
309
|
+
const place = (x: number, y: number, z: number): [number, number, number] => [
|
|
310
|
+
originX + x * scale,
|
|
311
|
+
originY + y * scale,
|
|
312
|
+
originZ + z * scale,
|
|
313
|
+
];
|
|
314
|
+
|
|
315
|
+
const buffers: SurfaceBuffers = {
|
|
316
|
+
positions: [],
|
|
317
|
+
normals: [],
|
|
318
|
+
colors: [],
|
|
319
|
+
indices: [],
|
|
320
|
+
quadsByType: new Map(),
|
|
321
|
+
};
|
|
322
|
+
const scratch = new Color();
|
|
323
|
+
let solidCells = 0;
|
|
324
|
+
let quads = 0;
|
|
325
|
+
for (let y = 0; y < sizeY; y += 1) {
|
|
326
|
+
for (let z = 0; z < sizeZ; z += 1) {
|
|
327
|
+
for (let x = 0; x < sizeX; x += 1) {
|
|
328
|
+
const type = at(x, y, z);
|
|
329
|
+
if (type === empty) continue;
|
|
330
|
+
solidCells += 1;
|
|
331
|
+
const color = vertexColor(options.colors, type, `(${x}, ${y}, ${z})`, scratch);
|
|
332
|
+
quads += emitCellFaces(buffers, [x, y, z], type, color, solidAt, place);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const geometry = new BufferGeometry();
|
|
338
|
+
geometry.setAttribute('position', new BufferAttribute(new Float32Array(buffers.positions), 3));
|
|
339
|
+
geometry.setAttribute('normal', new BufferAttribute(new Float32Array(buffers.normals), 3));
|
|
340
|
+
if (options.colors !== undefined) {
|
|
341
|
+
geometry.setAttribute('color', new BufferAttribute(new Float32Array(buffers.colors), 3));
|
|
342
|
+
}
|
|
343
|
+
const vertices = buffers.positions.length / 3;
|
|
344
|
+
const indices =
|
|
345
|
+
vertices > 0xffff ? new Uint32Array(buffers.indices) : new Uint16Array(buffers.indices);
|
|
346
|
+
geometry.setIndex(new BufferAttribute(indices, 1));
|
|
347
|
+
geometry.computeBoundingSphere();
|
|
348
|
+
geometry.computeBoundingBox();
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
geometry,
|
|
352
|
+
quads,
|
|
353
|
+
vertices,
|
|
354
|
+
triangles: quads * 2,
|
|
355
|
+
solidCells,
|
|
356
|
+
quadsByType: new Map([...buffers.quadsByType].sort(([a], [b]) => a - b)),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type * as THREE from 'three';
|
|
2
|
+
import { BatchedRenderer } from 'three.quarks';
|
|
3
|
+
import { farDepthTexture } from '../render/soft-particle-depth';
|
|
4
|
+
|
|
5
|
+
export interface ParticlesContext {
|
|
6
|
+
batchedRenderer: BatchedRenderer;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Creates the three.quarks BatchedRenderer for GPU particle systems.
|
|
11
|
+
*
|
|
12
|
+
* three.quarks renders all particle systems in a single draw call via instancing.
|
|
13
|
+
* Add particle systems to the batchedRenderer, not directly to the scene.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* const particles = setupParticles(scene);
|
|
17
|
+
* // In render loop: particles.batchedRenderer.update(dt);
|
|
18
|
+
*/
|
|
19
|
+
export function setupParticles(scene: THREE.Scene): ParticlesContext {
|
|
20
|
+
const batchedRenderer = new BatchedRenderer();
|
|
21
|
+
// Seed the soft-particle depth sampler BEFORE any batch exists, so every
|
|
22
|
+
// batch this renderer later builds inherits it (`BatchedRenderer.addSystem`
|
|
23
|
+
// applies `this.depthTexture` to each new batch). A `softParticles` batch
|
|
24
|
+
// whose `depthTexture` uniform is still `null` samples black and multiplies
|
|
25
|
+
// every particle away; the far-depth default makes that same batch render
|
|
26
|
+
// exactly as an unfaded one until a real depth pass supplies the scene's own.
|
|
27
|
+
// See `render/soft-particle-depth.ts`.
|
|
28
|
+
batchedRenderer.setDepthTexture(farDepthTexture());
|
|
29
|
+
scene.add(batchedRenderer);
|
|
30
|
+
|
|
31
|
+
return { batchedRenderer };
|
|
32
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type RAPIER from '@dimforge/rapier3d-compat';
|
|
2
|
+
import * as THREE from 'three';
|
|
3
|
+
|
|
4
|
+
export interface PhysicsContext {
|
|
5
|
+
rapierWorld: RAPIER.World;
|
|
6
|
+
eventQueue: RAPIER.EventQueue;
|
|
7
|
+
debugMesh: THREE.LineSegments;
|
|
8
|
+
debugEnabled: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates the Rapier physics world and debug visualization.
|
|
13
|
+
*
|
|
14
|
+
* Call RAPIER.init() before this function.
|
|
15
|
+
* This is direct Rapier setup — no abstraction. Edit freely.
|
|
16
|
+
*/
|
|
17
|
+
export function setupPhysics(
|
|
18
|
+
rapier: typeof RAPIER,
|
|
19
|
+
scene: THREE.Scene,
|
|
20
|
+
gravity: { x: number; y: number; z: number } = { x: 0, y: -9.81, z: 0 },
|
|
21
|
+
): PhysicsContext {
|
|
22
|
+
const rapierWorld = new rapier.World(gravity);
|
|
23
|
+
const eventQueue = new rapier.EventQueue(true);
|
|
24
|
+
|
|
25
|
+
// Debug visualization (toggled at runtime)
|
|
26
|
+
const debugGeometry = new THREE.BufferGeometry();
|
|
27
|
+
const debugMaterial = new THREE.LineBasicMaterial({ color: 0x00ff00, vertexColors: true });
|
|
28
|
+
const debugMesh = new THREE.LineSegments(debugGeometry, debugMaterial);
|
|
29
|
+
debugMesh.visible = false;
|
|
30
|
+
scene.add(debugMesh);
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
rapierWorld,
|
|
34
|
+
eventQueue,
|
|
35
|
+
debugMesh,
|
|
36
|
+
debugEnabled: false,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Update the debug line visualization from Rapier's debug render data.
|
|
42
|
+
* Call this each frame if debug is enabled.
|
|
43
|
+
*/
|
|
44
|
+
export function updatePhysicsDebug(rapierWorld: RAPIER.World, debugMesh: THREE.LineSegments) {
|
|
45
|
+
const buffers = rapierWorld.debugRender();
|
|
46
|
+
const geom = debugMesh.geometry;
|
|
47
|
+
|
|
48
|
+
// Reuse the existing BufferAttributes whenever the vertex count is stable
|
|
49
|
+
// (the common case). Allocating a new Float32Array + BufferAttribute every
|
|
50
|
+
// frame orphaned a fresh GPU buffer each time. We only allocate when the
|
|
51
|
+
// size actually changes.
|
|
52
|
+
const posAttr = geom.getAttribute('position') as THREE.BufferAttribute | undefined;
|
|
53
|
+
if (posAttr && posAttr.array.length === buffers.vertices.length) {
|
|
54
|
+
(posAttr.array as Float32Array).set(buffers.vertices);
|
|
55
|
+
posAttr.needsUpdate = true;
|
|
56
|
+
} else {
|
|
57
|
+
geom.setAttribute('position', new THREE.BufferAttribute(new Float32Array(buffers.vertices), 3));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const colAttr = geom.getAttribute('color') as THREE.BufferAttribute | undefined;
|
|
61
|
+
if (colAttr && colAttr.array.length === buffers.colors.length) {
|
|
62
|
+
(colAttr.array as Float32Array).set(buffers.colors);
|
|
63
|
+
colAttr.needsUpdate = true;
|
|
64
|
+
} else {
|
|
65
|
+
geom.setAttribute('color', new THREE.BufferAttribute(new Float32Array(buffers.colors), 4));
|
|
66
|
+
}
|
|
67
|
+
}
|