littlejsengine 1.19.3 → 1.20.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/FAQ.md +841 -820
- package/README.md +29 -25
- package/dist/littlejs.d.ts +2455 -935
- package/dist/littlejs.esm.js +30344 -21110
- package/dist/littlejs.esm.min.js +4 -3
- package/dist/littlejs.js +30259 -21052
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +26567 -20455
- package/package.json +10 -5
- package/plugins/audioEffects.js +28 -24
- package/plugins/box2d.js +2516 -2073
- package/plugins/drawUtilities.js +206 -31
- package/plugins/gltf.js +740 -0
- package/plugins/jsconfig.json +19 -0
- package/plugins/lightSystem.js +719 -332
- package/plugins/math3d.js +58 -17
- package/plugins/medalSystem.js +326 -280
- package/plugins/newgrounds.js +400 -199
- package/plugins/pathFinder.js +208 -175
- package/plugins/pluginExport.js +15 -0
- package/plugins/postProcess.js +56 -13
- package/plugins/render3d.js +1643 -1577
- package/plugins/render3dDebug.js +109 -0
- package/plugins/render3dExtras.js +1754 -0
- package/plugins/render3dVoxels.js +502 -0
- package/plugins/textureSheet.js +496 -458
- package/plugins/threejs.js +183 -163
- package/plugins/tweakables.js +411 -0
- package/plugins/tweenSystem.js +263 -145
- package/plugins/uiSystem.js +2024 -1687
- package/plugins/zzfxm.js +159 -159
- package/src/engine.js +191 -75
- package/src/engineAudio.js +229 -125
- package/src/engineBuild.mjs +289 -256
- package/src/engineDebug.js +912 -742
- package/src/engineDraw.js +295 -193
- package/src/engineEditor.js +2431 -0
- package/src/engineExport.js +12 -0
- package/src/engineInput.js +1462 -1293
- package/src/engineMath.js +1274 -1252
- package/src/engineObject.js +194 -80
- package/src/engineParticles.js +583 -532
- package/src/engineRelease.js +67 -45
- package/src/engineSettings.js +77 -29
- package/src/engineTileLayer.js +995 -741
- package/src/engineUtilities.js +19 -13
- package/src/engineWebGL.js +239 -82
- package/src/jsconfig.json +17 -13
- package/plugins/box2d.wasm.js +0 -630
- package/plugins/box2d.wasm.wasm +0 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* LittleJS 3D Voxels Plugin
|
|
3
|
+
* - VoxelMap: a grid of blocks drawn with the faces between them left out, that objects with collideLevel collide
|
|
4
|
+
* with, the 3D twin of the 2D tile layers
|
|
5
|
+
* - Requires the Render3D plugin and goes after it, everything here is part of its Render3D namespace
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
11
|
+
|
|
12
|
+
// cells along each side of a chunk, each chunk its own mesh, built again only when something in or beside it changes
|
|
13
|
+
const RENDER3D_VOXEL_CHUNK = 16;
|
|
14
|
+
|
|
15
|
+
// each face of a block: its normal, then the right and up axes across it, right cross up is the normal, as buildBox
|
|
16
|
+
// has them, in the order setBlockType takes six faces: +x, -x, +y, -y, +z, -z
|
|
17
|
+
const RENDER3D_VOXEL_FACES = [
|
|
18
|
+
[vec3( 1, 0, 0), vec3(0, 0, -1), vec3(0, 1, 0)],
|
|
19
|
+
[vec3(-1, 0, 0), vec3(0, 0, 1), vec3(0, 1, 0)],
|
|
20
|
+
[vec3(0, 1, 0), vec3(1, 0, 0), vec3(0, 0, -1)],
|
|
21
|
+
[vec3(0, -1, 0), vec3(1, 0, 0), vec3(0, 0, 1)],
|
|
22
|
+
[vec3(0, 0, 1), vec3(1, 0, 0), vec3(0, 1, 0)],
|
|
23
|
+
[vec3(0, 0, -1), vec3(-1, 0, 0), vec3(0, 1, 0)],
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
// the shade of a corner by how many solid blocks crowd it, from 0, closed in, to 3, open
|
|
27
|
+
const RENDER3D_VOXEL_SHADES = [.55, .7, .85, 1].map(v=> Object.freeze(rgb(v, v, v)));
|
|
28
|
+
|
|
29
|
+
// each face's corners about a cell's center, as render3DQuadAxes gives them: top left, bottom left, top right, bottom
|
|
30
|
+
// right; then the sides of the face each corner is toward, for its shading, and the corners in strip order, or split
|
|
31
|
+
// along the other diagonal
|
|
32
|
+
const RENDER3D_VOXEL_CORNERS = RENDER3D_VOXEL_FACES.map(([normal, right, up])=>
|
|
33
|
+
render3DQuadAxes(normal.scale(.5), right.scale(.5), up.scale(.5)));
|
|
34
|
+
const RENDER3D_VOXEL_CORNER_SIDES = [[-1, 1], [-1, -1], [1, 1], [1, -1]];
|
|
35
|
+
const RENDER3D_VOXEL_ORDER = [0, 1, 2, 3], RENDER3D_VOXEL_FLIPPED = [1, 3, 0, 2];
|
|
36
|
+
|
|
37
|
+
/** What VoxelMap.raycast finds: how far along the ray, the block's cell and type, and the normal of the face it comes in
|
|
38
|
+
* through
|
|
39
|
+
* @typedef {{distance: number, cell: Vector3, normal: Vector3, type: number}} VoxelHit
|
|
40
|
+
* @memberof Render3D */
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* VoxelMap - A grid of blocks, a 3D tile map: it draws itself, and objects with collideLevel collide with it
|
|
44
|
+
* - pos3D is its corner, as a 2D tile layer's is, and each cell is one world unit, so cell (x, y, z) fills
|
|
45
|
+
* pos3D + (x..x+1, y..y+1, z..z+1); it stays upright and unscaled at the root
|
|
46
|
+
* - A block's type is a number from 1 to 255, 0 is empty; a type shows that tile of the sheet on every face unless
|
|
47
|
+
* setBlockType gives it its own faces, or makes it see-through or transparent
|
|
48
|
+
* - Faces between blocks are left out, and the map is drawn in chunks of 16 cells a side, a chunk built again only
|
|
49
|
+
* when a block in or beside it changes
|
|
50
|
+
* - Objects with collideLevel collide with it, see EngineObject3D.collideWithVoxel, and raycast finds the block a ray
|
|
51
|
+
* hits and the face it comes in through
|
|
52
|
+
* @extends EngineObject3D
|
|
53
|
+
* @memberof Render3D
|
|
54
|
+
* @example
|
|
55
|
+
* const map = new VoxelMap(vec3(), vec3(32, 16, 32), tile(0, 16));
|
|
56
|
+
* map.setBlockType(1, {top: 0, side: 1, bottom: 2}); // grass
|
|
57
|
+
* map.setVoxel(vec3(3, 0, 5), 1);
|
|
58
|
+
*/
|
|
59
|
+
class VoxelMap extends EngineObject3D
|
|
60
|
+
{
|
|
61
|
+
/** Create a voxel map, it draws itself and joins the level's collision
|
|
62
|
+
* @param {Vector3} [pos3D] - Its corner
|
|
63
|
+
* @param {Vector3} [mapSize] - Cells along X, Y and Z
|
|
64
|
+
* @param {TileInfo} [tileInfo] - The sheet's first tile, as for a TileLayer, a block's type counts tiles from it;
|
|
65
|
+
* give the tiles a border of their own edge pixels, 2 or more, and that padding, or the blurred mipmaps of the
|
|
66
|
+
* distance blend each tile with the ones beside it and show seams along the block edges */
|
|
67
|
+
constructor(pos3D=vec3(), mapSize=vec3(16), tileInfo=tile())
|
|
68
|
+
{
|
|
69
|
+
super(pos3D);
|
|
70
|
+
ASSERT(isVector3(mapSize) && mapSize.x >= 1 && mapSize.y >= 1 && mapSize.z >= 1, 'mapSize must be at least 1 cell each way');
|
|
71
|
+
ASSERT(tileInfo instanceof TileInfo, 'tileInfo must be a TileInfo, the first tile of the sheet');
|
|
72
|
+
/** @property {Vector3} - Cells along X, Y and Z */
|
|
73
|
+
this.mapSize = mapSize.floor();
|
|
74
|
+
/** @property {TileInfo} - The sheet's first tile, rebuild() after changing it or resizing its texture */
|
|
75
|
+
this.tileInfo = tileInfo;
|
|
76
|
+
/** @property {Uint8Array} - The block type of each cell, x + mapSize.x * (y + mapSize.y * z), 0 empty; call
|
|
77
|
+
* rebuild() after changing it directly */
|
|
78
|
+
this.data = new Uint8Array(this.mapSize.x * this.mapSize.y * this.mapSize.z);
|
|
79
|
+
/** @property {boolean} - Darken the corners where blocks meet, rebuild() after changing it */
|
|
80
|
+
this.ambientOcclusion = true;
|
|
81
|
+
this.size3D = vec3(); // not a thing to collect or push, its cells are the solid part
|
|
82
|
+
|
|
83
|
+
const n = RENDER3D_VOXEL_CHUNK, s = this.mapSize;
|
|
84
|
+
this.chunkCount = vec3(ceil(s.x / n), ceil(s.y / n), ceil(s.z / n));
|
|
85
|
+
/** @type {Array<Mesh|undefined>} */
|
|
86
|
+
this.chunkMeshes = []; // each chunk's solid and see-through blocks, drawn opaque
|
|
87
|
+
/** @type {Array<Mesh|undefined>} */
|
|
88
|
+
this.chunkTransparentMeshes = []; // each chunk's transparent blocks, drawn blended
|
|
89
|
+
/** @type {Array<Vector3>} */
|
|
90
|
+
this.chunkCenters = []; // a chunk mesh's points are around its center, which it is drawn at
|
|
91
|
+
this.chunksChanged = new Set;
|
|
92
|
+
/** @type {Array<{faces: Array<number>, seeThrough: boolean, transparent: boolean}|undefined>} */
|
|
93
|
+
this.blockTypes = [];
|
|
94
|
+
this.tiles = new Map; // tile index to the uvs of its corners
|
|
95
|
+
|
|
96
|
+
// the transparent blocks draw in the transparent stage, blended and sorted, through a child that draws them
|
|
97
|
+
const glass = new EngineObject3D;
|
|
98
|
+
glass.size3D = vec3(); // not a thing to collect either
|
|
99
|
+
glass.transparent = true;
|
|
100
|
+
glass.render3D = ()=> this.renderChunks(true);
|
|
101
|
+
this.addChild(glass);
|
|
102
|
+
render3DLevel.push(this);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The block type at a cell, 0 for empty or outside the map
|
|
106
|
+
* @param {Vector3} cell
|
|
107
|
+
* @return {number} */
|
|
108
|
+
getVoxel(cell)
|
|
109
|
+
{
|
|
110
|
+
ASSERT(isVector3(cell), 'cell must be a vec3');
|
|
111
|
+
return this.voxelAt(floor(cell.x), floor(cell.y), floor(cell.z));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The block type at whole number cell coordinates, 0 outside
|
|
115
|
+
* @param {number} x
|
|
116
|
+
* @param {number} y
|
|
117
|
+
* @param {number} z
|
|
118
|
+
* @return {number}
|
|
119
|
+
* @ignore */
|
|
120
|
+
voxelAt(x, y, z)
|
|
121
|
+
{
|
|
122
|
+
const s = this.mapSize;
|
|
123
|
+
return x < 0 || y < 0 || z < 0 || x >= s.x || y >= s.y || z >= s.z ? 0 : this.data[x + s.x * (y + s.y * z)];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Set the block at a cell, 0 clears it; a cell outside the map is ignored
|
|
127
|
+
* @param {Vector3} cell
|
|
128
|
+
* @param {number} type - 0 to 255 */
|
|
129
|
+
setVoxel(cell, type)
|
|
130
|
+
{
|
|
131
|
+
ASSERT(isVector3(cell), 'cell must be a vec3');
|
|
132
|
+
ASSERT(type >= 0 && type <= 255 && type % 1 === 0, 'a block type is a whole number from 0 to 255', type);
|
|
133
|
+
const x = floor(cell.x), y = floor(cell.y), z = floor(cell.z), s = this.mapSize;
|
|
134
|
+
if (x < 0 || y < 0 || z < 0 || x >= s.x || y >= s.y || z >= s.z) return;
|
|
135
|
+
const i = x + s.x * (y + s.y * z);
|
|
136
|
+
if (this.data[i] === type) return;
|
|
137
|
+
this.data[i] = type;
|
|
138
|
+
// its chunk, and the chunks of the blocks around it: face to face it can show or hide their faces, and with
|
|
139
|
+
// corner shading on, the blocks all around it take shade from it
|
|
140
|
+
this.markChunk(x, y, z);
|
|
141
|
+
for (let dz = -1; dz <= 1; ++dz)
|
|
142
|
+
for (let dy = -1; dy <= 1; ++dy)
|
|
143
|
+
for (let dx = -1; dx <= 1; ++dx)
|
|
144
|
+
if ((this.ambientOcclusion || abs(dx) + abs(dy) + abs(dz) === 1) && this.voxelAt(x + dx, y + dy, z + dz))
|
|
145
|
+
this.markChunk(x + dx, y + dy, z + dz);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Mark the chunk holding a cell as changed, a cell outside the map has none
|
|
149
|
+
* @param {number} x
|
|
150
|
+
* @param {number} y
|
|
151
|
+
* @param {number} z
|
|
152
|
+
* @ignore */
|
|
153
|
+
markChunk(x, y, z)
|
|
154
|
+
{
|
|
155
|
+
const s = this.mapSize, c = this.chunkCount, n = RENDER3D_VOXEL_CHUNK;
|
|
156
|
+
if (x < 0 || y < 0 || z < 0 || x >= s.x || y >= s.y || z >= s.z) return;
|
|
157
|
+
this.chunksChanged.add(floor(x / n) + c.x * (floor(y / n) + c.y * floor(z / n)));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Give a block type its own faces, or make it see-through or transparent
|
|
161
|
+
* @param {number} type - 1 to 255
|
|
162
|
+
* @param {number|Array<number>|{top?: number, side: number, bottom?: number}} faces - A tile index for every face,
|
|
163
|
+
* six in the order +x, -x, +y, -y, +z, -z, or the side's with the top and bottom's, which default to the side's
|
|
164
|
+
* @param {{seeThrough?: boolean, transparent?: boolean}} [options] - seeThrough for holes in its texture, like
|
|
165
|
+
* leaves, so the blocks beside it keep their faces; transparent to blend, like glass or water, drawn in the
|
|
166
|
+
* transparent stage, and see-through too */
|
|
167
|
+
setBlockType(type, faces, {seeThrough=false, transparent=false}={})
|
|
168
|
+
{
|
|
169
|
+
ASSERT(type >= 1 && type <= 255 && type % 1 === 0, 'a block type is a whole number from 1 to 255', type);
|
|
170
|
+
const f = /** @type {any} */ (faces);
|
|
171
|
+
const list = isNumber(f) ? [f, f, f, f, f, f] : isArray(f) ? f :
|
|
172
|
+
[f.side, f.side, f.top ?? f.side, f.bottom ?? f.side, f.side, f.side];
|
|
173
|
+
ASSERT(list.length === 6 && list.every(isNumber), 'faces is a tile index, six of them, or {top, side, bottom}');
|
|
174
|
+
this.blockTypes[type] = {faces: list, seeThrough: seeThrough || transparent, transparent};
|
|
175
|
+
this.rebuild();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** A block type's faces and how it is seen through
|
|
179
|
+
* @param {number} type
|
|
180
|
+
* @return {{faces: Array<number>, seeThrough: boolean, transparent: boolean}}
|
|
181
|
+
* @ignore */
|
|
182
|
+
blockType(type)
|
|
183
|
+
{
|
|
184
|
+
return this.blockTypes[type] ||= {faces: [type, type, type, type, type, type], seeThrough: false, transparent: false};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Build every chunk again, after changing data directly or ambientOcclusion */
|
|
188
|
+
rebuild()
|
|
189
|
+
{
|
|
190
|
+
this.tiles.clear(); // the tile uvs too, the texture may have changed
|
|
191
|
+
const c = this.chunkCount;
|
|
192
|
+
for (let i = c.x * c.y * c.z; i--;)
|
|
193
|
+
this.chunksChanged.add(i);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Build the chunks that changed, the map calls it before it draws */
|
|
197
|
+
buildChunks()
|
|
198
|
+
{
|
|
199
|
+
for (const index of this.chunksChanged)
|
|
200
|
+
this.buildChunk(index);
|
|
201
|
+
this.chunksChanged.clear();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Build one chunk's meshes, a face for each block side that shows
|
|
205
|
+
* @param {number} index
|
|
206
|
+
* @ignore */
|
|
207
|
+
buildChunk(index)
|
|
208
|
+
{
|
|
209
|
+
const c = this.chunkCount, n = RENDER3D_VOXEL_CHUNK, s = this.mapSize, data = this.data;
|
|
210
|
+
const x0 = index % c.x * n, y0 = floor(index / c.x) % c.y * n, z0 = floor(index / (c.x * c.y)) * n;
|
|
211
|
+
const x1 = min(x0 + n, s.x), y1 = min(y0 + n, s.y), z1 = min(z0 + n, s.z);
|
|
212
|
+
const center = vec3((x0 + x1) / 2, (y0 + y1) / 2, (z0 + z1) / 2);
|
|
213
|
+
const opaque = new Mesh, transparent = new Mesh, ao = this.ambientOcclusion;
|
|
214
|
+
// a block that shades a corner, the see-through ones let the light by
|
|
215
|
+
const solid = (x, y, z)=> { const t = this.voxelAt(x, y, z); return t && !this.blockType(t).seeThrough ? 1 : 0; };
|
|
216
|
+
const points = [], uvs = [], colors = [], shade = []; // one face at a time, addStrip copies them out
|
|
217
|
+
for (let z = z0; z < z1; ++z)
|
|
218
|
+
for (let y = y0; y < y1; ++y)
|
|
219
|
+
for (let x = x0; x < x1; ++x)
|
|
220
|
+
{
|
|
221
|
+
const type = data[x + s.x * (y + s.y * z)];
|
|
222
|
+
if (!type) continue;
|
|
223
|
+
const block = this.blockType(type), mesh = block.transparent ? transparent : opaque;
|
|
224
|
+
const cx = x + .5 - center.x, cy = y + .5 - center.y, cz = z + .5 - center.z;
|
|
225
|
+
for (let f = 0; f < 6; ++f)
|
|
226
|
+
{
|
|
227
|
+
// a face shows toward an empty cell, or a see-through block of another type
|
|
228
|
+
const [normal, right, up] = RENDER3D_VOXEL_FACES[f];
|
|
229
|
+
const bx = x + normal.x, by = y + normal.y, bz = z + normal.z, next = this.voxelAt(bx, by, bz);
|
|
230
|
+
if (next && (next === type || !this.blockType(next).seeThrough)) continue;
|
|
231
|
+
|
|
232
|
+
// each corner darkened by the solid blocks beside it on the face's side, the usual voxel shading
|
|
233
|
+
for (let k = 0; k < 4; ++k)
|
|
234
|
+
{
|
|
235
|
+
const [r, u] = RENDER3D_VOXEL_CORNER_SIDES[k];
|
|
236
|
+
const side1 = ao && solid(bx + right.x * r, by + right.y * r, bz + right.z * r);
|
|
237
|
+
const side2 = ao && solid(bx + up.x * u, by + up.y * u, bz + up.z * u);
|
|
238
|
+
const corner = ao && solid(bx + right.x * r + up.x * u, by + right.y * r + up.y * u, bz + right.z * r + up.z * u);
|
|
239
|
+
shade[k] = side1 && side2 ? 0 : 3 - +side1 - +side2 - +corner;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// its corners as a strip around the chunk's center, split along the diagonal that keeps the shading
|
|
243
|
+
// even, the other one shows a seam
|
|
244
|
+
const corners = RENDER3D_VOXEL_CORNERS[f], quad = this.tileUVs(block.faces[f]);
|
|
245
|
+
const order = shade[0] + shade[3] > shade[1] + shade[2] ? RENDER3D_VOXEL_FLIPPED : RENDER3D_VOXEL_ORDER;
|
|
246
|
+
for (let k = 0; k < 4; ++k)
|
|
247
|
+
{
|
|
248
|
+
const i = order[k], p = corners[i];
|
|
249
|
+
points[k] = vec3(cx + p.x, cy + p.y, cz + p.z);
|
|
250
|
+
uvs[k] = quad[i];
|
|
251
|
+
colors[k] = ao ? RENDER3D_VOXEL_SHADES[shade[i]] : WHITE;
|
|
252
|
+
}
|
|
253
|
+
mesh.addStrip(points, normal, uvs, colors);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
this.chunkMeshes[index]?.dispose();
|
|
257
|
+
this.chunkTransparentMeshes[index]?.dispose();
|
|
258
|
+
this.chunkMeshes[index] = opaque.points.length ? opaque : undefined;
|
|
259
|
+
this.chunkTransparentMeshes[index] = transparent.points.length ? transparent : undefined;
|
|
260
|
+
this.chunkCenters[index] = center;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** The uvs of a tile's corners, top left, bottom left, top right, bottom right, the tile counted from the map's
|
|
264
|
+
* first tile as a TileLayer counts them; kept, and shared by every face that shows it
|
|
265
|
+
* @param {number} index
|
|
266
|
+
* @return {Array<Vector2>}
|
|
267
|
+
* @ignore */
|
|
268
|
+
tileUVs(index)
|
|
269
|
+
{
|
|
270
|
+
let uvs = this.tiles.get(index);
|
|
271
|
+
if (!uvs)
|
|
272
|
+
{
|
|
273
|
+
const first = this.tileInfo;
|
|
274
|
+
const rect = render3DGetTileUVs(first.columns ? first.frame(index) : first.index(index)); // a shared rect
|
|
275
|
+
uvs = RENDER3D_QUAD_UVS.map(q=> Object.freeze(vec2(rect.x + q.x * rect.w, rect.y + q.y * rect.h)));
|
|
276
|
+
this.tiles.set(index, uvs);
|
|
277
|
+
}
|
|
278
|
+
return uvs;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Whether a box hits a block that stops the object, see EngineObject3D.collideWithVoxel
|
|
282
|
+
* @param {Vector3} pos - Center of the box in the world
|
|
283
|
+
* @param {Vector3} size
|
|
284
|
+
* @param {EngineObject3D} o
|
|
285
|
+
* @return {boolean}
|
|
286
|
+
* @ignore */
|
|
287
|
+
boxBlocked(pos, size, o)
|
|
288
|
+
{
|
|
289
|
+
// the cells the box is in along an axis, one only touching a cell's face is not in it, but a point, a box with
|
|
290
|
+
// no size, is in the cell it is at, even on a whole number where the two ends pass each other
|
|
291
|
+
const m = this.pos3D, s = this.mapSize, tiny = 1e-9;
|
|
292
|
+
const cells = (center, half, corner, count)=>
|
|
293
|
+
{
|
|
294
|
+
const first = floor(center - half - corner + tiny), last = max(ceil(center + half - corner - tiny) - 1, first);
|
|
295
|
+
return [max(first, 0), min(last, count - 1)];
|
|
296
|
+
};
|
|
297
|
+
const [x0, x1] = cells(pos.x, size.x / 2, m.x, s.x);
|
|
298
|
+
const [y0, y1] = cells(pos.y, size.y / 2, m.y, s.y);
|
|
299
|
+
const [z0, z1] = cells(pos.z, size.z / 2, m.z, s.z);
|
|
300
|
+
for (let z = z0; z <= z1; ++z)
|
|
301
|
+
for (let y = y0; y <= y1; ++y)
|
|
302
|
+
for (let x = x0; x <= x1; ++x)
|
|
303
|
+
{
|
|
304
|
+
const type = this.data[x + s.x * (y + s.y * z)];
|
|
305
|
+
if (type && o.collideWithVoxel(type, vec3(x, y, z)))
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Keep an object out of the blocks, one axis at a time as 2D tiles do, called by the engine for each object with
|
|
312
|
+
* collideLevel; a sphere collides as its box, and one moving more than about a cell a frame can pass through
|
|
313
|
+
* - One already in blocks, as when a block is set on it, is pushed up to stand on those in its lower half when
|
|
314
|
+
* there is room, or else left free to move out of them, but not into any it is not in already
|
|
315
|
+
* @param {EngineObject3D} o
|
|
316
|
+
* @param {Vector3} oldPos - Where it was before it moved
|
|
317
|
+
* @ignore */
|
|
318
|
+
levelCollide3D(o, oldPos)
|
|
319
|
+
{
|
|
320
|
+
const k = o.scale3D, size = vec3(o.size3D.x * abs(k.x), o.size3D.y * abs(k.y), o.size3D.z * abs(k.z));
|
|
321
|
+
const p = o.pos3D, v = o.velocity3D, m = this.pos3D, epsilon = 1e-4;
|
|
322
|
+
if (!this.boxBlocked(p, size, o)) return;
|
|
323
|
+
|
|
324
|
+
if (this.boxBlocked(oldPos, size, o))
|
|
325
|
+
{
|
|
326
|
+
// the top of the highest blocked layer from its bottom up to its middle, each layer a flat box
|
|
327
|
+
const half = size.y / 2, flat = vec3(size.x, 0, size.z);
|
|
328
|
+
let top;
|
|
329
|
+
for (let y = floor(oldPos.y - half - m.y); y <= floor(oldPos.y - m.y); ++y)
|
|
330
|
+
if (this.boxBlocked(vec3(oldPos.x, m.y + y + .5, oldPos.z), flat, o))
|
|
331
|
+
top = y + 1;
|
|
332
|
+
const up = top === undefined ? undefined : vec3(oldPos.x, m.y + top + half + epsilon, oldPos.z);
|
|
333
|
+
if (up && !this.boxBlocked(up, size, o))
|
|
334
|
+
{
|
|
335
|
+
p.set(up.x, up.y, up.z);
|
|
336
|
+
v.y = max(v.y, 0);
|
|
337
|
+
o.groundObject = this;
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// or else free to move out, each axis alone, stopped only by a block it would enter that it is not in
|
|
342
|
+
// already: the cells its leading face passes into beyond those the box reached
|
|
343
|
+
const tiny = 1e-9, place = oldPos.copy();
|
|
344
|
+
for (const axis of ['y', 'x', 'z'])
|
|
345
|
+
{
|
|
346
|
+
const move = p[axis] - place[axis], halfSize = size[axis] / 2, corner = m[axis];
|
|
347
|
+
if (!move) continue;
|
|
348
|
+
const lead = (at)=> move > 0 ? ceil(at + halfSize - corner - tiny) - 1 : floor(at - halfSize - corner + tiny);
|
|
349
|
+
const from = lead(place[axis]), to = lead(p[axis]);
|
|
350
|
+
const first = min(from, to) + (move > 0 ? 1 : 0), last = max(from, to) - (move > 0 ? 0 : 1);
|
|
351
|
+
const slab = place.copy(), slabSize = size.copy();
|
|
352
|
+
slab[axis] = corner + (first + last + 1) / 2, slabSize[axis] = last - first + 1;
|
|
353
|
+
if (from === to || !this.boxBlocked(slab, slabSize, o))
|
|
354
|
+
place[axis] = p[axis];
|
|
355
|
+
else
|
|
356
|
+
{
|
|
357
|
+
// stopped; a fall stopped by a block under it stands on it
|
|
358
|
+
if (axis === 'y' && move < 0)
|
|
359
|
+
o.groundObject = this;
|
|
360
|
+
v[axis] = 0;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
p.set(place.x, place.y, place.z);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// from where it was, each axis alone, y first so a landing wins; a blocked axis goes flush against the block
|
|
368
|
+
// it ran into, or stays where it was, and its speed bounces by the restitution
|
|
369
|
+
const restitution = max(o.restitution, this.restitution);
|
|
370
|
+
const place = oldPos.copy();
|
|
371
|
+
for (const axis of ['y', 'x', 'z'])
|
|
372
|
+
{
|
|
373
|
+
const moved = place.copy();
|
|
374
|
+
moved[axis] = p[axis];
|
|
375
|
+
if (!this.boxBlocked(moved, size, o))
|
|
376
|
+
{
|
|
377
|
+
place[axis] = p[axis];
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
const half = size[axis] / 2, move = p[axis] - oldPos[axis];
|
|
381
|
+
if (move)
|
|
382
|
+
{
|
|
383
|
+
moved[axis] = move < 0 ? m[axis] + floor(p[axis] - half - m[axis]) + 1 + half + epsilon :
|
|
384
|
+
m[axis] + ceil(p[axis] + half - m[axis]) - 1 - half - epsilon;
|
|
385
|
+
if (!this.boxBlocked(moved, size, o))
|
|
386
|
+
place[axis] = moved[axis];
|
|
387
|
+
}
|
|
388
|
+
if (axis === 'y' && move < 0)
|
|
389
|
+
o.groundObject = this;
|
|
390
|
+
v[axis] *= -restitution;
|
|
391
|
+
}
|
|
392
|
+
p.set(place.x, place.y, place.z);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** The first block a ray hits, walking the grid cell by cell: its distance along the ray, its cell, the normal of
|
|
396
|
+
* the face it comes in through, which a new block goes against, and its type
|
|
397
|
+
* - A ray that starts inside a block hits it at 0, its normal back along the ray
|
|
398
|
+
* @param {Ray3D} ray - Its distance is in the ray's own units, as the other raycasts
|
|
399
|
+
* @param {number} [maxDistance]
|
|
400
|
+
* @param {function(number, Vector3): boolean} [test] - (type, cell) says which blocks count, every block by default
|
|
401
|
+
* @return {VoxelHit|undefined} */
|
|
402
|
+
raycast(ray, maxDistance=Infinity, test=()=> true)
|
|
403
|
+
{
|
|
404
|
+
const s = this.mapSize, o = ray.origin.subtract(this.pos3D), d = ray.direction, axes = ['x', 'y', 'z'];
|
|
405
|
+
if (!d.lengthSquared()) return;
|
|
406
|
+
|
|
407
|
+
// where it enters the map's box, the axis of the face it comes in through, or where it starts inside it
|
|
408
|
+
let enter = 0, exit = maxDistance, entryAxis = -1;
|
|
409
|
+
for (let k = 0; k < 3; ++k)
|
|
410
|
+
{
|
|
411
|
+
const a = axes[k], dk = d[a], ok = o[a];
|
|
412
|
+
if (!dk)
|
|
413
|
+
{
|
|
414
|
+
if (ok < 0 || ok > s[a]) return; // alongside the map, never in it
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
let t0 = -ok / dk, t1 = (s[a] - ok) / dk;
|
|
418
|
+
if (t0 > t1) [t0, t1] = [t1, t0];
|
|
419
|
+
if (t0 > enter) enter = t0, entryAxis = k;
|
|
420
|
+
exit = min(exit, t1);
|
|
421
|
+
}
|
|
422
|
+
if (enter > exit) return;
|
|
423
|
+
|
|
424
|
+
// walk the cells, stepping across whichever cell edge comes next
|
|
425
|
+
const start = o.add(d.scale(enter)), cell = [], step = [], next = [], delta = [];
|
|
426
|
+
for (let k = 0; k < 3; ++k)
|
|
427
|
+
{
|
|
428
|
+
const a = axes[k], dk = d[a];
|
|
429
|
+
cell[k] = clamp(floor(start[a]), 0, s[a] - 1);
|
|
430
|
+
step[k] = sign(dk);
|
|
431
|
+
next[k] = dk ? enter + (cell[k] + (dk > 0 ? 1 : 0) - start[a]) / dk : Infinity;
|
|
432
|
+
delta[k] = dk ? abs(1 / dk) : Infinity;
|
|
433
|
+
}
|
|
434
|
+
const normalOn = (k)=> vec3(k === 0 ? -step[0] : 0, k === 1 ? -step[1] : 0, k === 2 ? -step[2] : 0);
|
|
435
|
+
let normal, t = enter;
|
|
436
|
+
if (entryAxis >= 0)
|
|
437
|
+
normal = normalOn(entryAxis);
|
|
438
|
+
else
|
|
439
|
+
{
|
|
440
|
+
// starting inside, the normal faces back along the ray's strongest axis
|
|
441
|
+
const k = abs(d.x) >= abs(d.y) && abs(d.x) >= abs(d.z) ? 0 : abs(d.y) >= abs(d.z) ? 1 : 2;
|
|
442
|
+
normal = normalOn(k);
|
|
443
|
+
}
|
|
444
|
+
while (t <= maxDistance)
|
|
445
|
+
{
|
|
446
|
+
const type = this.data[cell[0] + s.x * (cell[1] + s.y * cell[2])];
|
|
447
|
+
if (type)
|
|
448
|
+
{
|
|
449
|
+
const at = vec3(cell[0], cell[1], cell[2]);
|
|
450
|
+
if (test(type, at))
|
|
451
|
+
return {distance: t, cell: at, normal, type};
|
|
452
|
+
}
|
|
453
|
+
const k = next[0] < next[1] ? (next[0] < next[2] ? 0 : 2) : (next[1] < next[2] ? 1 : 2);
|
|
454
|
+
t = next[k];
|
|
455
|
+
cell[k] += step[k];
|
|
456
|
+
if (cell[k] < 0 || cell[k] >= s[axes[k]]) return;
|
|
457
|
+
next[k] += delta[k];
|
|
458
|
+
normal = normalOn(k);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** How far along a ray the first block is, for picking, see raycast
|
|
463
|
+
* @param {Ray3D} ray
|
|
464
|
+
* @return {number|undefined}
|
|
465
|
+
* @ignore */
|
|
466
|
+
levelRaycast3D(ray) { return this.raycast(ray)?.distance; }
|
|
467
|
+
|
|
468
|
+
/** Keeps an eye on its placement, called automatically each frame */
|
|
469
|
+
update()
|
|
470
|
+
{
|
|
471
|
+
super.update();
|
|
472
|
+
render3DLevelAssertPlaced(this);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Draw the solid and see-through blocks, the transparent ones draw through its child */
|
|
476
|
+
render3D() { this.renderChunks(false); }
|
|
477
|
+
|
|
478
|
+
/** Draw the chunks, each at its center, with the whole texture so each face shows its own tile
|
|
479
|
+
* @param {boolean} transparent
|
|
480
|
+
* @ignore */
|
|
481
|
+
renderChunks(transparent)
|
|
482
|
+
{
|
|
483
|
+
this.buildChunks();
|
|
484
|
+
const meshes = transparent ? this.chunkTransparentMeshes : this.chunkMeshes, p = this.pos3D;
|
|
485
|
+
const texture = this.tileInfo.textureInfo;
|
|
486
|
+
for (let i = 0; i < meshes.length; ++i)
|
|
487
|
+
{
|
|
488
|
+
const mesh = meshes[i], c = this.chunkCenters[i];
|
|
489
|
+
mesh && render3D.drawMesh(mesh, vec3(p.x + c.x, p.y + c.y, p.z + c.z), texture, this.color);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** Destroy the map, it leaves the level's collision and lets go of its meshes
|
|
494
|
+
* @param {boolean} [immediate] */
|
|
495
|
+
destroy(immediate)
|
|
496
|
+
{
|
|
497
|
+
render3DLevelLeave(this);
|
|
498
|
+
for (const mesh of [...this.chunkMeshes, ...this.chunkTransparentMeshes])
|
|
499
|
+
mesh?.dispose();
|
|
500
|
+
super.destroy(immediate);
|
|
501
|
+
}
|
|
502
|
+
}
|