littlejsengine 1.21.0 → 1.22.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/dist/littlejs.d.ts +649 -71
- package/dist/littlejs.esm.js +2919 -306
- package/dist/littlejs.esm.min.js +22 -9
- package/dist/littlejs.js +2885 -306
- package/dist/littlejs.min.js +21 -8
- package/dist/littlejs.release.js +2087 -281
- package/package.json +3 -2
- package/plugins/gltf.js +7 -1
- package/plugins/lightSystem.js +44 -1
- package/plugins/math3d.js +42 -20
- package/plugins/particleEffects.js +745 -0
- package/plugins/pluginExport.js +32 -0
- package/plugins/postProcess.js +206 -28
- package/plugins/render3d.js +484 -185
- package/plugins/render3dEditor.js +787 -23
- package/plugins/render3dExtras.js +223 -11
- package/plugins/render3dLevel.js +166 -3
- package/plugins/sceneSystem.js +3 -2
- package/src/engine.js +1 -1
- package/src/engineAudio.js +16 -7
- package/src/engineBuild.mjs +1 -0
- package/src/engineDraw.js +44 -1
- package/src/engineEditor.js +11 -2
- package/src/engineExport.js +2 -0
- package/src/engineParticles.js +80 -11
- package/src/engineWebGL.js +27 -10
|
@@ -908,7 +908,7 @@ const render3DParticlesCalling = new Set;
|
|
|
908
908
|
* @return {Particle3D} */
|
|
909
909
|
function render3DParticleView(emitter)
|
|
910
910
|
{
|
|
911
|
-
return {emitter, pos: vec3(), velocity: vec3(), age: 0, lifeTime: 0, destroyed: false,
|
|
911
|
+
return {emitter, pos: vec3(), velocity: vec3(), age: 0, lifeTime: 0, scale: 1, destroyed: false,
|
|
912
912
|
destroy() { this.destroyed = true; }};
|
|
913
913
|
}
|
|
914
914
|
|
|
@@ -921,6 +921,8 @@ function render3DParticleView(emitter)
|
|
|
921
921
|
* @property {Vector3} velocity - How far it moves each frame
|
|
922
922
|
* @property {number} age - Seconds it has lived
|
|
923
923
|
* @property {number} lifeTime - Seconds it lives
|
|
924
|
+
* @property {number} scale - How much the emitter grows its effect, from its scale3D and its parents', as the 2D
|
|
925
|
+
* particle's scale
|
|
924
926
|
* @property {boolean} destroyed - Set by destroy
|
|
925
927
|
* @property {function(): void} destroy - End it this update, the destroy callback gets it
|
|
926
928
|
* @memberof Render3D
|
|
@@ -950,8 +952,9 @@ function render3DParticleView(emitter)
|
|
|
950
952
|
* - emitConeAngle spreads them, PI sprays in every direction
|
|
951
953
|
* - Speeds are per frame and sizes are world units, the same as the 2D emitter
|
|
952
954
|
* - scale3D, its own or a parent's, grows the whole effect: the spawn area, the sizes, the speed and the fall
|
|
953
|
-
* - gravity here is its own number added to velocity y each frame
|
|
954
|
-
*
|
|
955
|
+
* - gravity here is its own number added to velocity y each frame, so an effect keeps its own fall wherever it is
|
|
956
|
+
* used, the same as the 2D emitter's gravity; gravityScale adds a share of render3D.gravity on top, as the 2D
|
|
957
|
+
* emitter's gravityScale adds the engine's gravity
|
|
955
958
|
* - An emitter with an emitTime destroys itself once its last particle is gone, like the 2D emitter
|
|
956
959
|
* - Callbacks as the 2D emitter's: particleCreateCallback, particleUpdateCallback, particleCollideCallback and
|
|
957
960
|
* particleDestroyCallback, each given a Particle3D, one object the emitter reuses for every particle and call
|
|
@@ -981,8 +984,8 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
981
984
|
* @param {number} [sizeEnd] - Particle size at end of life
|
|
982
985
|
* @param {number} [speed] - Spawn speed in world units per frame
|
|
983
986
|
* @param {number} [damping] - Per frame velocity multiplier, 1 is none
|
|
984
|
-
* @param {number} [gravity] - Per frame change to velocity y, negative pulls down; its own number,
|
|
985
|
-
*
|
|
987
|
+
* @param {number} [gravity] - Per frame change to velocity y, negative pulls down; its own number, and
|
|
988
|
+
* gravityScale adds a share of render3D.gravity on top
|
|
986
989
|
* @param {number} [fadeRate] - Fraction of life spent fading, half in and half out
|
|
987
990
|
* @param {number} [randomness] - Extra randomness applied to speed, size and life
|
|
988
991
|
* @param {boolean} [additive] - Additive blending */
|
|
@@ -997,6 +1000,9 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
997
1000
|
|
|
998
1001
|
/** @property {number|Vector3} - Spawn area, a number for a sphere diameter or a vec3 for a box */
|
|
999
1002
|
this.emitSize = emitSize;
|
|
1003
|
+
/** @property {boolean} - Flatten the spawn area across the way it emits, its own up: a sphere becomes a disc
|
|
1004
|
+
* and a box a flat rectangle, for rain from a sheet of sky or flames from a patch of ground */
|
|
1005
|
+
this.emitFlat = false;
|
|
1000
1006
|
/** @property {number} - How long to keep emitting, 0 is forever */
|
|
1001
1007
|
this.emitTime = emitTime;
|
|
1002
1008
|
/** @property {number} - Particles per second, 0 does not emit */
|
|
@@ -1023,6 +1029,9 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1023
1029
|
this.damping = damping;
|
|
1024
1030
|
/** @property {number} - Per frame change to velocity y, its own number and not render3D.gravity */
|
|
1025
1031
|
this.gravity = gravity;
|
|
1032
|
+
/** @property {number} - Share of render3D.gravity added each frame on top of its own gravity, 0 by default so
|
|
1033
|
+
* an effect keeps its own fall wherever it is used; 1 falls with the world, like debris */
|
|
1034
|
+
this.gravityScale = 0;
|
|
1026
1035
|
/** @property {number} - Fraction of life spent fading, half in and half out */
|
|
1027
1036
|
this.fadeRate = fadeRate;
|
|
1028
1037
|
/** @property {number} - Extra randomness applied to speed, size and life */
|
|
@@ -1078,6 +1087,7 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1078
1087
|
const matrix = render3DObjectMatrix(this); // the object's own, read only
|
|
1079
1088
|
this.worldPos3D = matrix.getTranslation(); // remembered for when the parent is destroyed
|
|
1080
1089
|
const scale = render3DMaxScale(matrix.m);
|
|
1090
|
+
this.particleView.scale = scale; // the callbacks read it from the view instead of working it out each time
|
|
1081
1091
|
|
|
1082
1092
|
// emit until the emit time is up, then wait for the last particle and go away
|
|
1083
1093
|
if (!this.emitTime || this.getAliveTime() <= this.emitTime)
|
|
@@ -1110,7 +1120,11 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1110
1120
|
const F = RENDER3D_PARTICLE_FLOATS;
|
|
1111
1121
|
let data = this.particleData, trail = this.trailData;
|
|
1112
1122
|
const reread = ()=> { data = this.particleData, trail = this.trailData; };
|
|
1113
|
-
|
|
1123
|
+
// its own fall and a share of the world's, grown by the scale: a bigger effect has to fall faster to keep the
|
|
1124
|
+
// same arc
|
|
1125
|
+
const damping = this.damping, angleDamping = this.angleDamping, g = render3D.gravity, share = this.gravityScale;
|
|
1126
|
+
const gravityX = g.x * share * scale, gravityY = (this.gravity + g.y * share) * scale;
|
|
1127
|
+
const gravityZ = g.z * share * scale;
|
|
1114
1128
|
const collideLevel = this.collideLevel && render3DLevel.length;
|
|
1115
1129
|
const updateCallback = this.particleUpdateCallback, destroyCallback = this.particleDestroyCallback;
|
|
1116
1130
|
for (let i = this.particleCount; i--;)
|
|
@@ -1118,7 +1132,8 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1118
1132
|
// damped first and gravity added after, the order the 2D particle uses, so the same
|
|
1119
1133
|
// damping and gravity give the same arc in both
|
|
1120
1134
|
const k = i * F;
|
|
1121
|
-
const vx = data[k+3]
|
|
1135
|
+
const vx = data[k+3] = data[k+3] * damping + gravityX, vy = data[k+4] = data[k+4] * damping + gravityY;
|
|
1136
|
+
const vz = data[k+5] = data[k+5] * damping + gravityZ;
|
|
1122
1137
|
data[k] += vx, data[k+1] += vy, data[k+2] += vz;
|
|
1123
1138
|
data[k+18] += data[k+19] *= angleDamping;
|
|
1124
1139
|
collideLevel && (this.particleCollide(k, data[k] - vx, data[k+1] - vy, data[k+2] - vz), reread());
|
|
@@ -1166,10 +1181,18 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1166
1181
|
// the whole effect grows with the emitter, not just the area the particles start in
|
|
1167
1182
|
const scale = render3DMaxScale(matrix.m);
|
|
1168
1183
|
|
|
1169
|
-
// spawn offset: inside a box or a sphere
|
|
1170
|
-
const size = this.emitSize, box = /** @type {Vector3} */ (size);
|
|
1171
|
-
|
|
1172
|
-
|
|
1184
|
+
// spawn offset: inside a box or a sphere, or flat across the emitter's up, a flat rectangle or a disc
|
|
1185
|
+
const size = this.emitSize, box = /** @type {Vector3} */ (size), flat = this.emitFlat;
|
|
1186
|
+
let offset;
|
|
1187
|
+
if (isVector3(size))
|
|
1188
|
+
offset = vec3(rand(-.5, .5) * box.x, flat ? 0 : rand(-.5, .5) * box.y, rand(-.5, .5) * box.z);
|
|
1189
|
+
else if (flat)
|
|
1190
|
+
{
|
|
1191
|
+
const disc = randInCircle(/** @type {number} */ (size) / 2);
|
|
1192
|
+
offset = vec3(disc.x, 0, disc.y);
|
|
1193
|
+
}
|
|
1194
|
+
else
|
|
1195
|
+
offset = randInSphere(/** @type {number} */ (size) / 2);
|
|
1173
1196
|
|
|
1174
1197
|
// direction inside the cone around local +Y
|
|
1175
1198
|
const direction = matrix.transformDirection(randVector3(1, this.emitConeAngle)).normalize();
|
|
@@ -1439,6 +1462,195 @@ class Trail3D extends EngineObject3D
|
|
|
1439
1462
|
}
|
|
1440
1463
|
}
|
|
1441
1464
|
|
|
1465
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
1466
|
+
// lens flare
|
|
1467
|
+
|
|
1468
|
+
// the shapes a flare is made of, white on clear on one smooth texture made the first time it is asked for: a soft
|
|
1469
|
+
// glow, a flat disc with a brighter rim, and a ring; undefined headless or without a canvas
|
|
1470
|
+
let render3DFlareTiles;
|
|
1471
|
+
function render3DFlareTile(shape)
|
|
1472
|
+
{
|
|
1473
|
+
if (!render3DFlareTiles)
|
|
1474
|
+
{
|
|
1475
|
+
if (headlessMode || !glContext || typeof OffscreenCanvas == 'undefined') return;
|
|
1476
|
+
const cell = 128, shapes = ['glow', 'disc', 'ring'], context = createCanvasContext(cell * shapes.length, cell);
|
|
1477
|
+
// each shape is how see-through it is from its middle out
|
|
1478
|
+
const alpha =
|
|
1479
|
+
{
|
|
1480
|
+
glow: (t)=> engineGlowAlpha(t, 1),
|
|
1481
|
+
disc: (t)=> t < .8 ? .4 + .1 * t : t < .92 ? .48 + (t - .8) * 3 : (1 - t) / .08 * .84,
|
|
1482
|
+
ring: (t)=> max(0, 1 - abs(t - .86) / .12),
|
|
1483
|
+
};
|
|
1484
|
+
shapes.forEach((shape, i)=>
|
|
1485
|
+
{
|
|
1486
|
+
const x = i * cell + cell / 2, y = cell / 2, r = cell / 2 - 2, steps = 32;
|
|
1487
|
+
const gradient = context.createRadialGradient(x, y, 0, x, y, r);
|
|
1488
|
+
for (let k = 0; k <= steps; ++k)
|
|
1489
|
+
gradient.addColorStop(k / steps, 'rgba(255,255,255,' + clamp(alpha[shape](k / steps)).toFixed(4) + ')');
|
|
1490
|
+
context.fillStyle = gradient;
|
|
1491
|
+
context.beginPath();
|
|
1492
|
+
context.arc(x, y, r, 0, 2 * PI);
|
|
1493
|
+
context.fill();
|
|
1494
|
+
});
|
|
1495
|
+
const texture = new TextureInfo(context.canvas, true, false, false); // smooth even in a pixel art game
|
|
1496
|
+
render3DFlareTiles = new Map(shapes.map((shape, i)=>
|
|
1497
|
+
[shape, new TileInfo(vec2(i * cell, 0), vec2(cell), texture)]));
|
|
1498
|
+
}
|
|
1499
|
+
return render3DFlareTiles.get(shape) || render3DFlareTiles.get('glow');
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
/**
|
|
1503
|
+
* LensFlare3D - The sun's lens flare, the old kind: a glow at the sun and a row of discs and rings of different
|
|
1504
|
+
* sizes along the line from the sun through the middle of the screen
|
|
1505
|
+
* - Make one and it shows, over the 3D scene and under what the game draws after, a HUD; destroy it to take it away
|
|
1506
|
+
* - It follows render3D.sunDirection, and fades out as the sun leaves the screen or goes behind something
|
|
1507
|
+
* - flareSize, count, intensity and saturation set its look, seed picks another arrangement, and its color tints it,
|
|
1508
|
+
* with the sun's own color; or give it elements of your own
|
|
1509
|
+
* - visible is how much of the sun shows, 0 to 1, eased over fadeTime, there for a game to read
|
|
1510
|
+
* - What hides the sun is found with a ray from the camera, against the level and every object that is not see
|
|
1511
|
+
* through, each as the box around its mesh, see render3D.pick; turn it off with occlusion
|
|
1512
|
+
* - It needs WebGL, and it draws nothing in the shadow of renderAfter2D
|
|
1513
|
+
* @extends EngineObject3D
|
|
1514
|
+
* @memberof Render3D
|
|
1515
|
+
* @example
|
|
1516
|
+
* new LensFlare3D; // the sun flares
|
|
1517
|
+
* new LensFlare3D(1.5, 10, .7, 0); // bigger, 10 ghosts, dimmer, all one color
|
|
1518
|
+
*/
|
|
1519
|
+
class LensFlare3D extends EngineObject3D
|
|
1520
|
+
{
|
|
1521
|
+
/** Create the sun's lens flare
|
|
1522
|
+
* @param {number} [size] - Scales every part of it, 1 by default
|
|
1523
|
+
* @param {number} [count] - How many ghosts there are along the line, besides the glow at the sun
|
|
1524
|
+
* @param {number} [intensity] - How bright it is
|
|
1525
|
+
* @param {number} [saturation] - How colorful the ghosts are, 0 for all the flare's own color, 1 a rainbow
|
|
1526
|
+
* @param {Color} [color] - Tints the whole flare, with the sun's color */
|
|
1527
|
+
constructor(size=1, count=7, intensity=1, saturation=1, color=WHITE)
|
|
1528
|
+
{
|
|
1529
|
+
super(vec3(), undefined, undefined, color);
|
|
1530
|
+
this.size3D = vec3(); // not a thing to pick or collect
|
|
1531
|
+
/** @property {number} - Scales every part of the flare */
|
|
1532
|
+
this.flareSize = size;
|
|
1533
|
+
/** @property {number} - How many ghosts there are along the line, besides the glow at the sun */
|
|
1534
|
+
this.count = count;
|
|
1535
|
+
/** @property {number} - How bright it is */
|
|
1536
|
+
this.intensity = intensity;
|
|
1537
|
+
/** @property {number} - How colorful the ghosts are, 0 for all the flare's own color, 1 a rainbow */
|
|
1538
|
+
this.saturation = saturation;
|
|
1539
|
+
/** @property {number} - Picks the arrangement of the ghosts, another seed is another flare */
|
|
1540
|
+
this.seed = 1;
|
|
1541
|
+
/** @property {Array<{at: number, size: number, color: Color, shape: string}>|undefined} - The parts of the
|
|
1542
|
+
* flare, to set your own in place of the ones made from count, seed and saturation: at is where along the
|
|
1543
|
+
* line, 0 the sun, 1 the middle of the screen, 2 as far past it; size is across, as a part of the screen's
|
|
1544
|
+
* height; shape is glow, disc or ring
|
|
1545
|
+
* @type {Array<{at: number, size: number, color: Color, shape: string}>|undefined} */
|
|
1546
|
+
this.elements = undefined;
|
|
1547
|
+
/** @property {boolean} - Fade out when something is between the camera and the sun */
|
|
1548
|
+
this.occlusion = true;
|
|
1549
|
+
/** @property {number} - Seconds the flare takes to fade out or in when the sun is hidden or shows again */
|
|
1550
|
+
this.fadeTime = .15;
|
|
1551
|
+
/** @property {number} - How much of the sun shows, 0 hidden or behind the camera to 1 in plain view, eased */
|
|
1552
|
+
this.visible = 1;
|
|
1553
|
+
this.renderOrder = 1e9; // over the game's sprites, it is light in the lens
|
|
1554
|
+
this.madeKey = '';
|
|
1555
|
+
/** @type {Array<{at: number, size: number, color: Color, shape: string}>} */
|
|
1556
|
+
this.made = [];
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
/** The parts of the flare: the elements set by hand, or the ones made from count, seed and saturation, a glow
|
|
1560
|
+
* and a core at the sun and the ghosts, made again when one of those changes
|
|
1561
|
+
* @return {Array<{at: number, size: number, color: Color, shape: string}>} */
|
|
1562
|
+
getElements()
|
|
1563
|
+
{
|
|
1564
|
+
if (this.elements) return this.elements;
|
|
1565
|
+
const key = [this.count, this.seed, this.saturation].join();
|
|
1566
|
+
if (key !== this.madeKey)
|
|
1567
|
+
{
|
|
1568
|
+
const random = new RandomGenerator(this.seed * 7919 + 1), s = clamp(this.saturation);
|
|
1569
|
+
const made = this.made = [
|
|
1570
|
+
{at: 0, size: .7, color: hsl(0, 0, 1, .5), shape: 'glow'},
|
|
1571
|
+
{at: 0, size: .25, color: hsl(0, 0, 1, .9), shape: 'glow'}];
|
|
1572
|
+
for (let i = 0; i < this.count; ++i)
|
|
1573
|
+
{
|
|
1574
|
+
// spread along the line, each a place of its own, the far ones bigger
|
|
1575
|
+
const at = (i + random.float(.2, .8)) / max(this.count, 1) * 1.9 + .2;
|
|
1576
|
+
const pick = random.float(), shape = pick < .5 ? 'disc' : pick < .8 ? 'ring' : 'glow';
|
|
1577
|
+
made.push({at, size: random.float(.04, .1) * (1 + at), shape,
|
|
1578
|
+
color: hsl(random.float(), s * .9, .6, random.float(.15, .4))});
|
|
1579
|
+
}
|
|
1580
|
+
this.madeKey = key;
|
|
1581
|
+
}
|
|
1582
|
+
return this.made;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
/** Where the sun is on the screen, in pixels like mousePosScreen, undefined when it is behind the camera
|
|
1586
|
+
* @return {Vector2|undefined} */
|
|
1587
|
+
getSunScreenPos()
|
|
1588
|
+
{
|
|
1589
|
+
const direction = render3D.sunDirection;
|
|
1590
|
+
if (!direction.lengthSquared()) return;
|
|
1591
|
+
return render3D.worldToScreen(render3D.camera.pos.add(direction.normalize(100)));
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
/** The parts of the flare as they are drawn now: each one's place on the screen, its size in pixels and its
|
|
1595
|
+
* color, dimmed by how much of the sun shows; empty when there is nothing to draw
|
|
1596
|
+
* @return {Array<{pos: Vector2, size: number, color: Color, shape: string}>} */
|
|
1597
|
+
getScreenElements()
|
|
1598
|
+
{
|
|
1599
|
+
const sun = this.getSunScreenPos(), center = mainCanvasSize.scale(.5);
|
|
1600
|
+
if (!sun || !center.x || !center.y) return [];
|
|
1601
|
+
// it fades as the sun leaves the screen, gone when it is a third of the screen past the edge
|
|
1602
|
+
const off = max(abs(sun.x - center.x) / center.x, abs(sun.y - center.y) / center.y);
|
|
1603
|
+
const strength = this.visible * clamp((1.3 - off) / .5) * this.intensity;
|
|
1604
|
+
if (!(strength > 0)) return [];
|
|
1605
|
+
const tint = this.color.multiply(render3D.sunColor), height = mainCanvasSize.y * this.flareSize;
|
|
1606
|
+
return this.getElements().map((e)=>
|
|
1607
|
+
{
|
|
1608
|
+
const c = e.color.multiply(tint);
|
|
1609
|
+
// along the line and past the middle, a lerp would stop there
|
|
1610
|
+
return {pos: sun.add(center.subtract(sun).scale(e.at)), size: e.size * height, shape: e.shape,
|
|
1611
|
+
color: rgb(c.r, c.g, c.b, c.a * strength)};
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
/** Is something between the camera and the sun: the level, or an object that is not see through
|
|
1616
|
+
* @return {boolean} */
|
|
1617
|
+
isSunHidden()
|
|
1618
|
+
{
|
|
1619
|
+
const direction = render3D.sunDirection;
|
|
1620
|
+
if (!direction.lengthSquared()) return true;
|
|
1621
|
+
const blockers = engineObjects.filter((o)=> o !== this && o instanceof EngineObject3D && !o.transparent &&
|
|
1622
|
+
!o.additive);
|
|
1623
|
+
return !!render3D.pick(new Ray3D(render3D.camera.pos, direction.normalize()), blockers);
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
/** Ease visible toward whether the sun shows, called automatically each frame */
|
|
1627
|
+
update()
|
|
1628
|
+
{
|
|
1629
|
+
super.update();
|
|
1630
|
+
const shows = !!this.getSunScreenPos() && !(this.occlusion && this.isSunHidden());
|
|
1631
|
+
const step = this.fadeTime > 0 ? timeDelta / this.fadeTime : 1;
|
|
1632
|
+
this.visible = clamp(this.visible + (shows ? step : -step));
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
/** Draw the flare over the 3D scene, added onto it, called automatically in the 2D pass */
|
|
1636
|
+
render()
|
|
1637
|
+
{
|
|
1638
|
+
if (headlessMode || !glEnable) return;
|
|
1639
|
+
const elements = this.getScreenElements();
|
|
1640
|
+
if (!elements.length) return;
|
|
1641
|
+
setAdditiveBlendMode(true);
|
|
1642
|
+
for (const e of elements)
|
|
1643
|
+
{
|
|
1644
|
+
const tileInfo = render3DFlareTile(e.shape);
|
|
1645
|
+
tileInfo && drawTile(e.pos, vec2(e.size), tileInfo, e.color, 0, false, undefined, true, true);
|
|
1646
|
+
}
|
|
1647
|
+
setAdditiveBlendMode(false);
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
/** A flare has nothing to draw in the 3D pass */
|
|
1651
|
+
render3D() {}
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1442
1654
|
///////////////////////////////////////////////////////////////////////////////
|
|
1443
1655
|
// OBJ meshes
|
|
1444
1656
|
|
package/plugins/render3dLevel.js
CHANGED
|
@@ -73,6 +73,16 @@ function level3DAddMesh(name, mesh, tileInfo, color=WHITE)
|
|
|
73
73
|
* - What a file written by hand gets wrong uses the default: a value that is not of its default's type
|
|
74
74
|
* - An object its type can not make is skipped with an error in debug builds, where asserts throw, and the rest
|
|
75
75
|
* of the level is made
|
|
76
|
+
* - A level may hold a map of blocks, in a voxels block: pos, its corner, size, its cells along x, y and z, and
|
|
77
|
+
* blocks, runs of a count and a type along x, then y, then z; it is made a VoxelMap, the first of what is
|
|
78
|
+
* returned, see level3DVoxelSetup for its sheet
|
|
79
|
+
* - A level may hold a terrain, in a terrain block: pos, its center, size, its size in the world along x and z,
|
|
80
|
+
* height, how tall a full height is, color, and heights, rows of 0 to 1 from -z to +z, each from -x to +x; it
|
|
81
|
+
* is made a HeightMap, returned with what else was made
|
|
82
|
+
* - A level may set the scene too, in a scene block beside its objects: sky, three colors for straight up, the
|
|
83
|
+
* horizon and straight down, ambient, how much of them lights the scene, .5 when not given, sunDirection and
|
|
84
|
+
* sunColor, fog, its start and end, fogColor, the horizon color when not given, and shadows; what the block
|
|
85
|
+
* leaves out stays as the game set it, and a level with no block changes nothing
|
|
76
86
|
* @param {Object} level - The level, the level editor edits this same object
|
|
77
87
|
* @return {Array<any>} - What each object's type made, a function that made nothing is left out
|
|
78
88
|
* @memberof Level3D */
|
|
@@ -81,7 +91,11 @@ function level3DLoad(level)
|
|
|
81
91
|
ASSERT(!!level && typeof level === 'object', 'a level is an object, {} for a new one');
|
|
82
92
|
ASSERT(!(level.littlejs3D > LEVEL3D_VERSION), 'the level was made by a newer LittleJS');
|
|
83
93
|
editor3DLevelLoaded(level); // debug builds: the 3D editor takes the level, its autosaved edits go in first
|
|
84
|
-
|
|
94
|
+
level3DSceneApply(level.scene);
|
|
95
|
+
const made = [], map = level3DVoxelMap = level3DVoxelsMake(level.voxels);
|
|
96
|
+
map && made.push(map);
|
|
97
|
+
const terrain = level3DTerrainMap = level3DTerrainMake(level.terrain);
|
|
98
|
+
terrain && made.push(terrain);
|
|
85
99
|
for (const object of isArray(level.objects) ? level.objects : [])
|
|
86
100
|
{
|
|
87
101
|
if (!object || typeof object !== 'object') continue;
|
|
@@ -92,6 +106,147 @@ function level3DLoad(level)
|
|
|
92
106
|
return made;
|
|
93
107
|
}
|
|
94
108
|
|
|
109
|
+
// the sheet a level's block map is made with, and what sets each one up, see level3DVoxelSetup; and the map the
|
|
110
|
+
// level loaded last made, the one the 3D editor paints
|
|
111
|
+
let level3DVoxelTiles, level3DVoxelSetupMap, level3DVoxelMap;
|
|
112
|
+
|
|
113
|
+
/** How a level's block map is made: the sheet its blocks show tiles of, and a function to set it up
|
|
114
|
+
* - A level's voxels block makes a VoxelMap when the level loads, with texture 0 and the default tile size unless
|
|
115
|
+
* a sheet is given here; a block's type shows that tile of the sheet on every face
|
|
116
|
+
* - setup is called with each map a level makes, to give block types their own faces or make them see-through
|
|
117
|
+
* - Call it before level3DLoad; with no arguments the defaults are back
|
|
118
|
+
* @param {TileInfo} [tileInfo] - The sheet's first tile, as for a VoxelMap
|
|
119
|
+
* @param {function(VoxelMap): void} [setup]
|
|
120
|
+
* @example
|
|
121
|
+
* level3DVoxelSetup(tile(0, 16, 1), (map)=> map.setBlockType(1, {top: 0, side: 1, bottom: 2})); // grass
|
|
122
|
+
* @memberof Level3D */
|
|
123
|
+
function level3DVoxelSetup(tileInfo, setup)
|
|
124
|
+
{
|
|
125
|
+
level3DVoxelTiles = tileInfo;
|
|
126
|
+
level3DVoxelSetupMap = setup;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// the most cells a level's map may have, 256 each way
|
|
130
|
+
const LEVEL3D_VOXEL_CELLS = 2 ** 24;
|
|
131
|
+
|
|
132
|
+
// a map's blocks as the file keeps them, runs of a count and a type, cell by cell in the map's own order
|
|
133
|
+
function level3DVoxelsEncode(data)
|
|
134
|
+
{
|
|
135
|
+
const blocks = [];
|
|
136
|
+
for (let i = 0; i < data.length;)
|
|
137
|
+
{
|
|
138
|
+
let end = i + 1;
|
|
139
|
+
while (end < data.length && data[end] === data[i]) ++end;
|
|
140
|
+
blocks.push(end - i, data[i]);
|
|
141
|
+
i = end;
|
|
142
|
+
}
|
|
143
|
+
return blocks;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// runs into a map's cells: what they do not reach is empty, a type that is not 0 to 255 is empty, and a run that
|
|
147
|
+
// is not two numbers ends them
|
|
148
|
+
function level3DVoxelsDecode(blocks, data)
|
|
149
|
+
{
|
|
150
|
+
data.fill(0);
|
|
151
|
+
if (!isArray(blocks)) return;
|
|
152
|
+
for (let i = 0, cell = 0; i + 1 < blocks.length && cell < data.length; i += 2)
|
|
153
|
+
{
|
|
154
|
+
const count = blocks[i], type = blocks[i + 1];
|
|
155
|
+
if (!isNumber(count) || !isNumber(type) || count < 0) return;
|
|
156
|
+
const end = min(cell + floor(count), data.length);
|
|
157
|
+
data.fill(type >= 0 && type <= 255 && type % 1 === 0 ? type : 0, cell, end);
|
|
158
|
+
cell = end;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// the corner and the size in cells of a level's voxels block, undefined when it is not one a map can be made of
|
|
163
|
+
function level3DVoxelsShape(voxels)
|
|
164
|
+
{
|
|
165
|
+
if (!voxels || typeof voxels !== 'object') return;
|
|
166
|
+
const size = level3DVector(voxels.size);
|
|
167
|
+
if (!size || [size.x, size.y, size.z].some((n)=> n < 1 || n % 1) ||
|
|
168
|
+
size.x * size.y * size.z > LEVEL3D_VOXEL_CELLS) return;
|
|
169
|
+
return {pos: level3DVector(voxels.pos, vec3()), size};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// the VoxelMap of a level's voxels block, undefined when it has none or it is wrong
|
|
173
|
+
function level3DVoxelsMake(voxels)
|
|
174
|
+
{
|
|
175
|
+
const shape = level3DVoxelsShape(voxels);
|
|
176
|
+
if (!shape || typeof VoxelMap == 'undefined') return;
|
|
177
|
+
const map = new VoxelMap(shape.pos, shape.size, level3DVoxelTiles || tile());
|
|
178
|
+
level3DVoxelsDecode(voxels.blocks, map.data);
|
|
179
|
+
level3DVoxelSetupMap?.(map);
|
|
180
|
+
map.rebuild();
|
|
181
|
+
return map;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// the terrain the level loaded last made, the one the 3D editor sculpts, and the most samples it may have a side
|
|
185
|
+
let level3DTerrainMap;
|
|
186
|
+
const LEVEL3D_TERRAIN_SAMPLES = 129;
|
|
187
|
+
|
|
188
|
+
// what a level's terrain block is made with: its center, its size in the world along x and z, how tall a full
|
|
189
|
+
// height is, and its heights as rows of 0 to 1, a copy, each row as long as the first; undefined when it is not
|
|
190
|
+
// one a HeightMap can be made of
|
|
191
|
+
function level3DTerrainShape(terrain)
|
|
192
|
+
{
|
|
193
|
+
if (!terrain || typeof terrain !== 'object') return;
|
|
194
|
+
const size = terrain.size, rows = terrain.heights, n = LEVEL3D_TERRAIN_SAMPLES;
|
|
195
|
+
if (!isArray(size) || size.length !== 2 || !size.every((v)=> isNumber(v) && v > 0)) return;
|
|
196
|
+
if (!isArray(rows) || rows.length < 2 || rows.length > n || !isArray(rows[0])) return;
|
|
197
|
+
const columns = rows[0].length;
|
|
198
|
+
if (columns < 2 || columns > n || !rows.every((row)=> isArray(row) && row.length === columns)) return;
|
|
199
|
+
const heights = rows.map((row)=> row.map((v)=> isNumber(v) ? clamp(v) : 0));
|
|
200
|
+
return {pos: level3DVector(terrain.pos, vec3()), size: vec2(size[0], size[1]),
|
|
201
|
+
height: isNumber(terrain.height) && terrain.height > 0 ? terrain.height : 1, heights};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// the HeightMap of a level's terrain block, undefined when it has none or it is wrong
|
|
205
|
+
function level3DTerrainMake(terrain)
|
|
206
|
+
{
|
|
207
|
+
const shape = level3DTerrainShape(terrain);
|
|
208
|
+
if (!shape || typeof HeightMap == 'undefined') return;
|
|
209
|
+
const map = new HeightMap(shape.heights, shape.size, shape.height, undefined, shape.pos);
|
|
210
|
+
if (/^#([0-9a-f]{6}|[0-9a-f]{8})$/i.test(terrain.color))
|
|
211
|
+
map.color = new Color().setHex(terrain.color);
|
|
212
|
+
return map;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// the sky domes levels made, a level's sky takes the place of the one before and disposes it; a dome the game made
|
|
216
|
+
// itself is left whole, for the game to put back
|
|
217
|
+
const level3DSkies = new WeakSet;
|
|
218
|
+
|
|
219
|
+
// set the scene a level's scene block gives: sky as its top, horizon and bottom colors, with ambient how much of
|
|
220
|
+
// them lights the scene, sunDirection and sunColor, fog as its start and end with fogColor, the horizon without one,
|
|
221
|
+
// and shadows; a setting the block does not have, or has wrong, stays as it is
|
|
222
|
+
function level3DSceneApply(scene)
|
|
223
|
+
{
|
|
224
|
+
const r = render3D;
|
|
225
|
+
if (!r || !scene || typeof scene !== 'object') return;
|
|
226
|
+
const color = (value)=> /^#([0-9a-f]{6}|[0-9a-f]{8})$/i.test(value) ? new Color().setHex(value) : undefined;
|
|
227
|
+
const sky = isArray(scene.sky) && scene.sky.length === 3 ? scene.sky.map(color) : [];
|
|
228
|
+
if (sky.length && sky.every((c)=> c))
|
|
229
|
+
{
|
|
230
|
+
// as setSky does, without disposing a dome that is the game's own
|
|
231
|
+
const [top, horizon, bottom] = sky, ambient = isNumber(scene.ambient) ? scene.ambient : .5;
|
|
232
|
+
level3DSkies.has(r.sky) && r.sky.dispose();
|
|
233
|
+
level3DSkies.add(r.sky = buildSky(top, horizon, bottom));
|
|
234
|
+
r.fogColor = horizon.copy();
|
|
235
|
+
r.ambientColor = top.scale(ambient, 1);
|
|
236
|
+
r.ambientGroundColor = bottom.scale(ambient, 1);
|
|
237
|
+
}
|
|
238
|
+
const sun = level3DVector(scene.sunDirection);
|
|
239
|
+
if (sun && sun.lengthSquared())
|
|
240
|
+
r.sunDirection = sun;
|
|
241
|
+
r.sunColor = color(scene.sunColor) || r.sunColor;
|
|
242
|
+
const fog = scene.fog;
|
|
243
|
+
if (isArray(fog) && fog.length === 2 && fog.every((v)=> isNumber(v)))
|
|
244
|
+
r.fogStart = fog[0], r.fogEnd = fog[1];
|
|
245
|
+
r.fogColor = color(scene.fogColor) || r.fogColor;
|
|
246
|
+
if (typeof scene.shadows === 'boolean')
|
|
247
|
+
r.shadows = scene.shadows;
|
|
248
|
+
}
|
|
249
|
+
|
|
95
250
|
// a vec3 of an array of three numbers, as the file has them, or the fallback
|
|
96
251
|
function level3DVector(value, fallback)
|
|
97
252
|
{
|
|
@@ -184,6 +339,14 @@ level3DAddType('Sphere', function(pos, properties)
|
|
|
184
339
|
level3DAddType('Cylinder', function(pos, properties)
|
|
185
340
|
{ return level3DMakeShape(pos, properties, level3DCylinderMesh ||= buildCylinder()); },
|
|
186
341
|
{color: WHITE, tile: -1, solid: true});
|
|
342
|
+
// a light, a spotlight with a cone: cone is the angle in degrees from its forward out to the edge of its beam, the
|
|
343
|
+
// object's rotation aims it, softness is how much of the cone fades, and shadows makes it the one that casts them
|
|
187
344
|
level3DAddType('Light', function(pos, properties)
|
|
188
|
-
|
|
189
|
-
|
|
345
|
+
{
|
|
346
|
+
const light = new Light3D(pos, properties.radius, properties.color, properties.intensity);
|
|
347
|
+
light.coneAngle = max(properties.cone, 0) * PI / 180;
|
|
348
|
+
light.coneSoftness = properties.softness;
|
|
349
|
+
if (properties.shadows && render3D)
|
|
350
|
+
render3D.shadowLight = light;
|
|
351
|
+
return light;
|
|
352
|
+
}, {color: WHITE, radius: 5, intensity: 1, cone: 0, softness: .2, shadows: false});
|
package/plugins/sceneSystem.js
CHANGED
|
@@ -38,9 +38,10 @@ function setScene(scene)
|
|
|
38
38
|
|
|
39
39
|
if (sceneCurrent)
|
|
40
40
|
{
|
|
41
|
+
// a leave that throws still lets the next switch through
|
|
41
42
|
sceneLeaving = true;
|
|
42
|
-
sceneCurrent.leave?.();
|
|
43
|
-
sceneLeaving = false;
|
|
43
|
+
try { sceneCurrent.leave?.(); }
|
|
44
|
+
finally { sceneLeaving = false; }
|
|
44
45
|
}
|
|
45
46
|
engineObjectsDestroy();
|
|
46
47
|
sceneCurrent = scene;
|
package/src/engine.js
CHANGED
package/src/engineAudio.js
CHANGED
|
@@ -220,9 +220,12 @@ class Sound
|
|
|
220
220
|
* @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
|
|
221
221
|
* @param {SoundLoadCallback} [onloadCallback] - callback function to call when sound is loaded
|
|
222
222
|
*/
|
|
223
|
-
constructor(asset, randomness, range
|
|
223
|
+
constructor(asset, randomness, range, taper=soundDefaultTaper, onloadCallback)
|
|
224
224
|
{
|
|
225
225
|
if (!soundEnable || headlessMode) return;
|
|
226
|
+
const rangeIsDefault = range === undefined;
|
|
227
|
+
if (rangeIsDefault)
|
|
228
|
+
range = soundDefaultRange;
|
|
226
229
|
|
|
227
230
|
ASSERT(!asset || isArray(asset) || isStringLike(asset), 'asset must be a file name or zzfx array');
|
|
228
231
|
ASSERT(randomness === undefined || isNumber(randomness), 'randomness must be a number');
|
|
@@ -232,6 +235,9 @@ class Sound
|
|
|
232
235
|
|
|
233
236
|
/** @property {number} - World space max range of sound */
|
|
234
237
|
this.range = range;
|
|
238
|
+
/** @property {boolean} - No range was given, so it got soundDefaultRange; played in 3D such a sound is heard
|
|
239
|
+
* to render3D.soundDefaultRange instead, which is further */
|
|
240
|
+
this.rangeIsDefault = rangeIsDefault;
|
|
235
241
|
/** @property {number} - At what percentage of range should it start tapering */
|
|
236
242
|
this.taper = taper;
|
|
237
243
|
/** @property {number} - How much to randomize frequency each time sound plays
|
|
@@ -867,7 +873,7 @@ function playAudioBuffer(buffer, volume=1, rate=1, pan=0, loop=false, gainNode,
|
|
|
867
873
|
}
|
|
868
874
|
|
|
869
875
|
///////////////////////////////////////////////////////////////////////////////
|
|
870
|
-
// ZzFXMicro - Zuper Zmall Zound Zynth - v1.
|
|
876
|
+
// ZzFXMicro - Zuper Zmall Zound Zynth - v1.4.0 by Frank Force
|
|
871
877
|
|
|
872
878
|
/** Generate and play a ZzFX sound
|
|
873
879
|
*
|
|
@@ -899,7 +905,7 @@ function zzfx(...zzfxSound) { return playSamples([zzfxG(...zzfxSound)]); }
|
|
|
899
905
|
* @param {number} [decay] - Decay time, how long to reach sustain after attack (seconds)
|
|
900
906
|
* @param {number} [tremolo] - Trembling effect, rate controlled by repeat time (percent)
|
|
901
907
|
* @param {number} [filter] - Filter cutoff frequency, positive for HPF, negative for LPF (Hz)
|
|
902
|
-
* @return {
|
|
908
|
+
* @return {Float32Array} - The audio samples
|
|
903
909
|
* @memberof Audio */
|
|
904
910
|
function zzfxG
|
|
905
911
|
(
|
|
@@ -937,7 +943,7 @@ function zzfxG
|
|
|
937
943
|
crush = 0, // bit crush offset
|
|
938
944
|
jump = 1, // pitch jump timer
|
|
939
945
|
length, // sample length
|
|
940
|
-
b
|
|
946
|
+
b, // sample buffer
|
|
941
947
|
t = 0, // sample time
|
|
942
948
|
i = 0, // sample index
|
|
943
949
|
s = 0, // sample value
|
|
@@ -964,9 +970,12 @@ function zzfxG
|
|
|
964
970
|
pitchJumpTime *= sampleRate;
|
|
965
971
|
repeatTime = repeatTime * sampleRate | 0;
|
|
966
972
|
|
|
973
|
+
// allocate the full sample buffer up front, much faster than growing an array
|
|
974
|
+
length = attack + decay + sustain + release + delay | 0;
|
|
975
|
+
b = new Float32Array(length > 0 ? length : 0);
|
|
976
|
+
|
|
967
977
|
// generate waveform
|
|
968
|
-
for (
|
|
969
|
-
i < length; b[i++] = s * volume) // sample
|
|
978
|
+
for (; i < length; b[i++] = s * volume) // sample
|
|
970
979
|
{
|
|
971
980
|
if (!(++crush%(bitCrush*100|0))) // bit crush
|
|
972
981
|
{
|
|
@@ -1002,7 +1011,7 @@ function zzfxG
|
|
|
1002
1011
|
|
|
1003
1012
|
f = (frequency += slide += deltaSlide) *// frequency
|
|
1004
1013
|
cos(modulation*modOffset++); // modulation
|
|
1005
|
-
t += f + f*noise*
|
|
1014
|
+
t += f + f*noise*(i*i*PI2%2-1); // noise
|
|
1006
1015
|
|
|
1007
1016
|
if (jump && ++jump > pitchJumpTime) // pitch jump
|
|
1008
1017
|
{
|
package/src/engineBuild.mjs
CHANGED