littlejsengine 1.21.0 → 1.23.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/README.md +1 -1
- package/dist/littlejs.d.ts +1258 -141
- package/dist/littlejs.esm.js +5023 -429
- package/dist/littlejs.esm.min.js +22 -9
- package/dist/littlejs.js +4977 -430
- package/dist/littlejs.min.js +21 -8
- package/dist/littlejs.release.js +2759 -313
- package/package.json +3 -2
- package/plugins/gltf.js +7 -1
- package/plugins/lightSystem.js +50 -5
- package/plugins/math3d.js +42 -20
- package/plugins/parallax.js +174 -0
- package/plugins/particleEffects.js +760 -0
- package/plugins/pluginExport.js +40 -0
- package/plugins/postProcess.js +206 -28
- package/plugins/render3d.js +495 -188
- package/plugins/render3dEditor.js +1754 -80
- package/plugins/render3dExtras.js +294 -21
- package/plugins/render3dLevel.js +463 -6
- package/plugins/sceneSystem.js +3 -2
- package/src/engine.js +1 -1
- package/src/engineAudio.js +25 -8
- package/src/engineBuild.mjs +2 -0
- package/src/engineDraw.js +59 -4
- package/src/engineEditor.js +475 -37
- package/src/engineExport.js +7 -0
- package/src/engineInput.js +48 -6
- package/src/engineParticles.js +80 -11
- package/src/engineRelease.js +13 -2
- package/src/engineSettings.js +14 -0
- 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 */
|
|
@@ -1049,6 +1058,9 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1049
1058
|
/** @property {boolean} - Particles hit the level, the height maps and voxel maps, bouncing by restitution and
|
|
1050
1059
|
* sliding along by friction; off by default, it tests each particle's move against the level every frame */
|
|
1051
1060
|
this.collideLevel = false;
|
|
1061
|
+
/** @property {number} - How much a particle grips where it lands, 0 to 1: its speed along the surface is
|
|
1062
|
+
* cut by this much on each hit, on top of the friction, 1 stops it there */
|
|
1063
|
+
this.stick = 0;
|
|
1052
1064
|
/** @property {Particle3DCallback|undefined} - Called with each particle as it is made
|
|
1053
1065
|
* @type {Particle3DCallback|undefined} */
|
|
1054
1066
|
this.particleCreateCallback = undefined;
|
|
@@ -1068,7 +1080,7 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1068
1080
|
/** @property {Vector3|undefined} - Where the emitter was at its last update, for when its parent is destroyed
|
|
1069
1081
|
* @type {Vector3|undefined} */
|
|
1070
1082
|
this.worldPos3D = undefined;
|
|
1071
|
-
this.emitTimeBuffer =
|
|
1083
|
+
this.emitTimeBuffer = 1; // the first particle comes at once, as the 2D emitter's does
|
|
1072
1084
|
}
|
|
1073
1085
|
|
|
1074
1086
|
/** Spawn new particles, move the live ones, and go away when done */
|
|
@@ -1078,15 +1090,17 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1078
1090
|
const matrix = render3DObjectMatrix(this); // the object's own, read only
|
|
1079
1091
|
this.worldPos3D = matrix.getTranslation(); // remembered for when the parent is destroyed
|
|
1080
1092
|
const scale = render3DMaxScale(matrix.m);
|
|
1093
|
+
this.particleView.scale = scale; // the callbacks read it from the view instead of working it out each time
|
|
1081
1094
|
|
|
1082
1095
|
// emit until the emit time is up, then wait for the last particle and go away
|
|
1083
1096
|
if (!this.emitTime || this.getAliveTime() <= this.emitTime)
|
|
1084
1097
|
{
|
|
1085
1098
|
// a rate of zero is an emitter fed by hand, and the global scale only quiets it,
|
|
1086
1099
|
// neither is a reason to stop counting down the emit time
|
|
1087
|
-
|
|
1100
|
+
const rate = this.emitRate * particleEmitRateScale;
|
|
1101
|
+
if (rate > 0 && rate < Infinity)
|
|
1088
1102
|
{
|
|
1089
|
-
this.emitTimeBuffer +=
|
|
1103
|
+
this.emitTimeBuffer += rate * timeDelta;
|
|
1090
1104
|
for (; this.emitTimeBuffer >= 1; --this.emitTimeBuffer)
|
|
1091
1105
|
this.emitParticle();
|
|
1092
1106
|
}
|
|
@@ -1110,7 +1124,11 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1110
1124
|
const F = RENDER3D_PARTICLE_FLOATS;
|
|
1111
1125
|
let data = this.particleData, trail = this.trailData;
|
|
1112
1126
|
const reread = ()=> { data = this.particleData, trail = this.trailData; };
|
|
1113
|
-
|
|
1127
|
+
// its own fall and a share of the world's, grown by the scale: a bigger effect has to fall faster to keep the
|
|
1128
|
+
// same arc
|
|
1129
|
+
const damping = this.damping, angleDamping = this.angleDamping, g = render3D.gravity, share = this.gravityScale;
|
|
1130
|
+
const gravityX = g.x * share * scale, gravityY = (this.gravity + g.y * share) * scale;
|
|
1131
|
+
const gravityZ = g.z * share * scale;
|
|
1114
1132
|
const collideLevel = this.collideLevel && render3DLevel.length;
|
|
1115
1133
|
const updateCallback = this.particleUpdateCallback, destroyCallback = this.particleDestroyCallback;
|
|
1116
1134
|
for (let i = this.particleCount; i--;)
|
|
@@ -1118,10 +1136,13 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1118
1136
|
// damped first and gravity added after, the order the 2D particle uses, so the same
|
|
1119
1137
|
// damping and gravity give the same arc in both
|
|
1120
1138
|
const k = i * F;
|
|
1121
|
-
const vx = data[k+3]
|
|
1139
|
+
const vx = data[k+3] = data[k+3] * damping + gravityX, vy = data[k+4] = data[k+4] * damping + gravityY;
|
|
1140
|
+
const vz = data[k+5] = data[k+5] * damping + gravityZ;
|
|
1122
1141
|
data[k] += vx, data[k+1] += vy, data[k+2] += vz;
|
|
1123
1142
|
data[k+18] += data[k+19] *= angleDamping;
|
|
1124
|
-
|
|
1143
|
+
// true when its collide callback destroyed it, it has had its last callback but the destroy one
|
|
1144
|
+
const gone = collideLevel && this.particleCollide(k, data[k] - vx, data[k+1] - vy, data[k+2] - vz);
|
|
1145
|
+
collideLevel && reread();
|
|
1125
1146
|
const t = i * trailMax * 3;
|
|
1126
1147
|
if (trailMax)
|
|
1127
1148
|
{
|
|
@@ -1133,7 +1154,7 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1133
1154
|
trail[j] = data[k], trail[j+1] = data[k+1], trail[j+2] = data[k+2];
|
|
1134
1155
|
data[k+20] = n + 1;
|
|
1135
1156
|
}
|
|
1136
|
-
updateCallback && (this.particleCall(updateCallback, k), reread());
|
|
1157
|
+
updateCallback && !gone && (this.particleCall(updateCallback, k), reread());
|
|
1137
1158
|
if ((data[k+17] += timeDelta) >= data[k+16])
|
|
1138
1159
|
{
|
|
1139
1160
|
// dead: the last particle takes its slot, trail and all, order does not matter
|
|
@@ -1166,10 +1187,18 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1166
1187
|
// the whole effect grows with the emitter, not just the area the particles start in
|
|
1167
1188
|
const scale = render3DMaxScale(matrix.m);
|
|
1168
1189
|
|
|
1169
|
-
// spawn offset: inside a box or a sphere
|
|
1170
|
-
const size = this.emitSize, box = /** @type {Vector3} */ (size);
|
|
1171
|
-
|
|
1172
|
-
|
|
1190
|
+
// spawn offset: inside a box or a sphere, or flat across the emitter's up, a flat rectangle or a disc
|
|
1191
|
+
const size = this.emitSize, box = /** @type {Vector3} */ (size), flat = this.emitFlat;
|
|
1192
|
+
let offset;
|
|
1193
|
+
if (isVector3(size))
|
|
1194
|
+
offset = vec3(rand(-.5, .5) * box.x, flat ? 0 : rand(-.5, .5) * box.y, rand(-.5, .5) * box.z);
|
|
1195
|
+
else if (flat)
|
|
1196
|
+
{
|
|
1197
|
+
const disc = randInCircle(/** @type {number} */ (size) / 2);
|
|
1198
|
+
offset = vec3(disc.x, 0, disc.y);
|
|
1199
|
+
}
|
|
1200
|
+
else
|
|
1201
|
+
offset = randInSphere(/** @type {number} */ (size) / 2);
|
|
1173
1202
|
|
|
1174
1203
|
// direction inside the cone around local +Y
|
|
1175
1204
|
const direction = matrix.transformDirection(randVector3(1, this.emitConeAngle)).normalize();
|
|
@@ -1227,13 +1256,14 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1227
1256
|
out[k] = view.pos.x, out[k+1] = view.pos.y, out[k+2] = view.pos.z;
|
|
1228
1257
|
out[k+3] = view.velocity.x, out[k+4] = view.velocity.y, out[k+5] = view.velocity.z;
|
|
1229
1258
|
if (view.destroyed)
|
|
1230
|
-
out[k+17] = max(out[k+17], out[k+16]
|
|
1259
|
+
out[k+17] = max(out[k+17], out[k+16]); // its life lived: a step short of it can round to just under
|
|
1231
1260
|
return result;
|
|
1232
1261
|
}
|
|
1233
1262
|
|
|
1234
1263
|
// stop the particle at k where its last move went into the level, from where it was: back at the surface, a hair
|
|
1235
1264
|
// off it, its speed into it turned around by restitution and its speed along it kept by friction, unless the
|
|
1236
|
-
// collide callback lets it through; one that starts inside is let go, as a 2D one is
|
|
1265
|
+
// collide callback lets it through; one that starts inside is let go, as a 2D one is; true when the callback
|
|
1266
|
+
// destroyed it
|
|
1237
1267
|
particleCollide(k, x, y, z)
|
|
1238
1268
|
{
|
|
1239
1269
|
let data = this.particleData;
|
|
@@ -1246,18 +1276,21 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1246
1276
|
if (h && (!hit || h.distance < hit.distance))
|
|
1247
1277
|
hit = h, level = l;
|
|
1248
1278
|
}
|
|
1249
|
-
if (!hit) return;
|
|
1279
|
+
if (!hit) return false;
|
|
1250
1280
|
const point = from.lerp(to, hit.distance);
|
|
1251
1281
|
const callback = this.particleCollideCallback;
|
|
1252
1282
|
if (callback && (!this.particleCall(callback, k, level, point.copy()) || this.particleView.destroyed))
|
|
1253
|
-
return; // let through, or destroyed by the callback
|
|
1283
|
+
return this.particleView.destroyed; // let through, or destroyed by the callback
|
|
1254
1284
|
data = this.particleData; // as the callback left it, an emit may have grown it
|
|
1255
1285
|
|
|
1256
1286
|
const n = hit.normal, v = vec3(data[k+3], data[k+4], data[k+5]), into = n.scale(v.dot(n));
|
|
1257
|
-
|
|
1287
|
+
// the larger friction of the two, as objects take it, then the emitter's own grip, which a level can not undo
|
|
1288
|
+
const restitution = max(this.restitution, level.restitution);
|
|
1289
|
+
const friction = max(this.friction, level.friction) * (1 - clamp(this.stick));
|
|
1258
1290
|
const out = v.subtract(into).scale(friction).subtract(into.scale(restitution)), p = point.add(n.scale(1e-3));
|
|
1259
1291
|
data[k] = p.x, data[k+1] = p.y, data[k+2] = p.z;
|
|
1260
1292
|
data[k+3] = out.x, data[k+4] = out.y, data[k+5] = out.z;
|
|
1293
|
+
return false;
|
|
1261
1294
|
}
|
|
1262
1295
|
|
|
1263
1296
|
/** Draw the particles, as flat squares or as streaks when trailTime is set
|
|
@@ -1439,6 +1472,246 @@ class Trail3D extends EngineObject3D
|
|
|
1439
1472
|
}
|
|
1440
1473
|
}
|
|
1441
1474
|
|
|
1475
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
1476
|
+
// lens flare
|
|
1477
|
+
|
|
1478
|
+
// the shapes a flare is made of, white on clear on one smooth texture made the first time it is asked for: a soft
|
|
1479
|
+
// glow, a flat disc with a brighter rim, and a ring; undefined headless or without a canvas
|
|
1480
|
+
let render3DFlareTiles;
|
|
1481
|
+
function render3DFlareTile(shape)
|
|
1482
|
+
{
|
|
1483
|
+
if (!render3DFlareTiles)
|
|
1484
|
+
{
|
|
1485
|
+
if (headlessMode || !glContext || typeof OffscreenCanvas == 'undefined') return;
|
|
1486
|
+
const cell = 128, shapes = ['glow', 'disc', 'ring'], context = createCanvasContext(cell * shapes.length, cell);
|
|
1487
|
+
// each shape is how see-through it is from its middle out
|
|
1488
|
+
const alpha =
|
|
1489
|
+
{
|
|
1490
|
+
glow: (t)=> engineGlowAlpha(t, 1),
|
|
1491
|
+
disc: (t)=> t < .8 ? .4 + .1 * t : t < .92 ? .48 + (t - .8) * 3 : (1 - t) / .08 * .84,
|
|
1492
|
+
ring: (t)=> max(0, 1 - abs(t - .86) / .12),
|
|
1493
|
+
};
|
|
1494
|
+
shapes.forEach((shape, i)=>
|
|
1495
|
+
{
|
|
1496
|
+
const x = i * cell + cell / 2, y = cell / 2, r = cell / 2 - 2, steps = 32;
|
|
1497
|
+
const gradient = context.createRadialGradient(x, y, 0, x, y, r);
|
|
1498
|
+
for (let k = 0; k <= steps; ++k)
|
|
1499
|
+
gradient.addColorStop(k / steps, 'rgba(255,255,255,' + clamp(alpha[shape](k / steps)).toFixed(4) + ')');
|
|
1500
|
+
context.fillStyle = gradient;
|
|
1501
|
+
context.beginPath();
|
|
1502
|
+
context.arc(x, y, r, 0, 2 * PI);
|
|
1503
|
+
context.fill();
|
|
1504
|
+
});
|
|
1505
|
+
const texture = new TextureInfo(context.canvas, true, false, false); // smooth even in a pixel art game
|
|
1506
|
+
render3DFlareTiles = new Map(shapes.map((shape, i)=>
|
|
1507
|
+
[shape, new TileInfo(vec2(i * cell, 0), vec2(cell), texture)]));
|
|
1508
|
+
}
|
|
1509
|
+
return render3DFlareTiles.get(shape) || render3DFlareTiles.get('glow');
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
/**
|
|
1513
|
+
* LensFlare3D - The sun's lens flare, the old kind: a glow at the sun and a row of discs and rings of different
|
|
1514
|
+
* sizes along the line from the sun through the middle of the screen
|
|
1515
|
+
* - Make one and it shows, over the 3D scene and under what the game draws after, a HUD; destroy it to take it away
|
|
1516
|
+
* - It follows render3D.sunDirection, and fades out as the sun leaves the screen or goes behind something
|
|
1517
|
+
* - flareSize, count, intensity and saturation set its look, seed picks another arrangement, and its color tints it,
|
|
1518
|
+
* with the sun's own color; or give it elements of your own
|
|
1519
|
+
* - visible is how much of the sun shows, 0 to 1, eased over fadeTime, there for a game to read
|
|
1520
|
+
* - What hides the sun is found with a ray from the camera, against the level and every object that is not see
|
|
1521
|
+
* through, each as the box around its mesh, see render3D.pick; turn it off with occlusion
|
|
1522
|
+
* - It needs WebGL, and it draws nothing in the shadow of renderAfter2D
|
|
1523
|
+
* @extends EngineObject3D
|
|
1524
|
+
* @memberof Render3D
|
|
1525
|
+
* @example
|
|
1526
|
+
* new LensFlare3D; // the sun flares
|
|
1527
|
+
* new LensFlare3D(1.5, 10, .7, 0); // bigger, 10 ghosts, dimmer, all one color
|
|
1528
|
+
*/
|
|
1529
|
+
class LensFlare3D extends EngineObject3D
|
|
1530
|
+
{
|
|
1531
|
+
/** Create the sun's lens flare
|
|
1532
|
+
* @param {number} [size] - Scales every part of it, 1 by default
|
|
1533
|
+
* @param {number} [count] - How many ghosts there are along the line, besides the glow at the sun
|
|
1534
|
+
* @param {number} [intensity] - How bright it is
|
|
1535
|
+
* @param {number} [saturation] - How colorful the ghosts are, 0 for all the flare's own color, 1 a rainbow
|
|
1536
|
+
* @param {Color} [color] - Tints the whole flare, with the sun's color */
|
|
1537
|
+
constructor(size=1, count=7, intensity=1, saturation=1, color=WHITE)
|
|
1538
|
+
{
|
|
1539
|
+
super(vec3(), undefined, undefined, color);
|
|
1540
|
+
this.size3D = vec3(); // not a thing to pick or collect
|
|
1541
|
+
/** @property {number} - Scales every part of the flare */
|
|
1542
|
+
this.flareSize = size;
|
|
1543
|
+
/** @property {number} - How many ghosts there are along the line, besides the glow at the sun */
|
|
1544
|
+
this.count = count;
|
|
1545
|
+
/** @property {number} - How bright it is */
|
|
1546
|
+
this.intensity = intensity;
|
|
1547
|
+
/** @property {number} - How colorful the ghosts are, 0 for all the flare's own color, 1 a rainbow */
|
|
1548
|
+
this.saturation = saturation;
|
|
1549
|
+
/** @property {number} - Picks the arrangement of the ghosts, another seed is another flare */
|
|
1550
|
+
this.seed = 1;
|
|
1551
|
+
/** @property {Array<{at: number, size: number, color: Color, shape: string}>|undefined} - The parts of the
|
|
1552
|
+
* flare, to set your own in place of the ones made from count, seed and saturation: at is where along the
|
|
1553
|
+
* 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
|
|
1554
|
+
* height; shape is glow, disc or ring
|
|
1555
|
+
* @type {Array<{at: number, size: number, color: Color, shape: string}>|undefined} */
|
|
1556
|
+
this.elements = undefined;
|
|
1557
|
+
/** @property {Light3D|undefined} - A light the flare is of in place of the sun, a lamp or a spotlight: the
|
|
1558
|
+
* flare is at the light and in its color, smaller from farther than the light reaches, hidden by what is
|
|
1559
|
+
* in front of the light, and a spotlight's shows from inside its beam only
|
|
1560
|
+
* @type {Light3D|undefined} */
|
|
1561
|
+
this.light = undefined;
|
|
1562
|
+
/** @property {boolean} - Fade out when something is between the camera and the sun */
|
|
1563
|
+
this.occlusion = true;
|
|
1564
|
+
/** @property {number} - Seconds the flare takes to fade out or in when the sun is hidden or shows again */
|
|
1565
|
+
this.fadeTime = .15;
|
|
1566
|
+
/** @property {number} - How much of the sun shows, 0 hidden or behind the camera to 1 in plain view, eased */
|
|
1567
|
+
this.visible = 1;
|
|
1568
|
+
this.renderOrder = 1e9; // over the game's sprites, it is light in the lens
|
|
1569
|
+
this.madeKey = '';
|
|
1570
|
+
/** @type {Array<{at: number, size: number, color: Color, shape: string}>} */
|
|
1571
|
+
this.made = [];
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
/** The parts of the flare: the elements set by hand, or the ones made from count, seed and saturation, a glow
|
|
1575
|
+
* and a core at the sun and the ghosts, made again when one of those changes
|
|
1576
|
+
* @return {Array<{at: number, size: number, color: Color, shape: string}>} */
|
|
1577
|
+
getElements()
|
|
1578
|
+
{
|
|
1579
|
+
if (this.elements) return this.elements;
|
|
1580
|
+
const key = [this.count, this.seed, this.saturation].join();
|
|
1581
|
+
if (key !== this.madeKey)
|
|
1582
|
+
{
|
|
1583
|
+
const random = new RandomGenerator(this.seed * 7919 + 1), s = clamp(this.saturation);
|
|
1584
|
+
const made = this.made = [
|
|
1585
|
+
{at: 0, size: .7, color: hsl(0, 0, 1, .5), shape: 'glow'},
|
|
1586
|
+
{at: 0, size: .25, color: hsl(0, 0, 1, .9), shape: 'glow'}];
|
|
1587
|
+
for (let i = 0; i < this.count; ++i)
|
|
1588
|
+
{
|
|
1589
|
+
// spread along the line, each a place of its own, the far ones bigger
|
|
1590
|
+
const at = (i + random.float(.2, .8)) / max(this.count, 1) * 1.9 + .2;
|
|
1591
|
+
const pick = random.float(), shape = pick < .5 ? 'disc' : pick < .8 ? 'ring' : 'glow';
|
|
1592
|
+
made.push({at, size: random.float(.04, .1) * (1 + at), shape,
|
|
1593
|
+
color: hsl(random.float(), s * .9, .6, random.float(.15, .4))});
|
|
1594
|
+
}
|
|
1595
|
+
this.madeKey = key;
|
|
1596
|
+
}
|
|
1597
|
+
return this.made;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
// what the flare is of, seen from the camera: the way to it, how far it is, Infinity for the sun, and a point
|
|
1601
|
+
// to find it on the screen by; undefined with no sun direction, or a light that is gone or at the camera
|
|
1602
|
+
flareSource()
|
|
1603
|
+
{
|
|
1604
|
+
const camera = render3D.camera.pos, light = this.light;
|
|
1605
|
+
if (!light)
|
|
1606
|
+
{
|
|
1607
|
+
const sun = render3D.sunDirection;
|
|
1608
|
+
if (!sun.lengthSquared()) return;
|
|
1609
|
+
const direction = sun.normalize();
|
|
1610
|
+
return {direction, distance: Infinity, pos: camera.add(direction.scale(100))};
|
|
1611
|
+
}
|
|
1612
|
+
if (light.destroyed) return;
|
|
1613
|
+
const pos = light.getWorldPos3D(), offset = pos.subtract(camera), distance = offset.length();
|
|
1614
|
+
return distance ? {direction: offset.scale(1 / distance), distance, pos} : undefined;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
/** Where the sun, or the flare's light, is on the screen, in pixels like mousePosScreen, undefined when it is
|
|
1618
|
+
* behind the camera
|
|
1619
|
+
* @return {Vector2|undefined} */
|
|
1620
|
+
getSunScreenPos()
|
|
1621
|
+
{
|
|
1622
|
+
const source = this.flareSource();
|
|
1623
|
+
return source && render3D.worldToScreen(source.pos);
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
/** The parts of the flare as they are drawn now: each one's place on the screen, its size in pixels and its
|
|
1627
|
+
* color, dimmed by how much of the sun shows; empty when there is nothing to draw
|
|
1628
|
+
* @return {Array<{pos: Vector2, size: number, color: Color, shape: string}>} */
|
|
1629
|
+
getScreenElements()
|
|
1630
|
+
{
|
|
1631
|
+
const sun = this.getSunScreenPos(), center = mainCanvasSize.scale(.5);
|
|
1632
|
+
if (!sun || !center.x || !center.y) return [];
|
|
1633
|
+
// it fades as the sun leaves the screen, gone when it is a third of the screen past the edge
|
|
1634
|
+
const off = max(abs(sun.x - center.x) / center.x, abs(sun.y - center.y) / center.y);
|
|
1635
|
+
const strength = this.visible * clamp((1.3 - off) / .5) * this.intensity;
|
|
1636
|
+
if (!(strength > 0)) return [];
|
|
1637
|
+
let tint = this.color.multiply(render3D.sunColor), height = mainCanvasSize.y * this.flareSize;
|
|
1638
|
+
const light = this.light, source = light && this.flareSource();
|
|
1639
|
+
if (light)
|
|
1640
|
+
{
|
|
1641
|
+
if (!source) return [];
|
|
1642
|
+
// a light's flare is the light's color, as bright as the light up to 1, and only from inside its cone
|
|
1643
|
+
const cone = render3DLightCone(light), d = source.direction;
|
|
1644
|
+
const inCone = clamp(-(cone[0] * d.x + cone[1] * d.y + cone[2] * d.z) - cone[3]);
|
|
1645
|
+
const c = light.color, bright = c.a * clamp(light.intensity) * inCone;
|
|
1646
|
+
if (!(bright > 0)) return [];
|
|
1647
|
+
tint = this.color.multiply(rgb(c.r, c.g, c.b, bright));
|
|
1648
|
+
height *= min(1, light.radius / source.distance); // smaller from farther than it reaches
|
|
1649
|
+
}
|
|
1650
|
+
return this.getElements().map((e)=>
|
|
1651
|
+
{
|
|
1652
|
+
const c = e.color.multiply(tint);
|
|
1653
|
+
// along the line and past the middle, a lerp would stop there
|
|
1654
|
+
return {pos: sun.add(center.subtract(sun).scale(e.at)), size: e.size * height, shape: e.shape,
|
|
1655
|
+
color: rgb(c.r, c.g, c.b, c.a * strength)};
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
/** Is something between the camera and the sun, or the flare's light: the level, or an object that is not see
|
|
1660
|
+
* through
|
|
1661
|
+
* @return {boolean} */
|
|
1662
|
+
isSunHidden()
|
|
1663
|
+
{
|
|
1664
|
+
const source = this.flareSource();
|
|
1665
|
+
if (!source) return true;
|
|
1666
|
+
const ray = new Ray3D(render3D.camera.pos, source.direction), reach = source.distance;
|
|
1667
|
+
const blockers = engineObjects.filter((o)=> o !== this && o instanceof EngineObject3D && !o.destroyed &&
|
|
1668
|
+
!o.transparent && !o.additive);
|
|
1669
|
+
// a voxel map says block by block what is see through: glass, water and leaves let the sun by, and the
|
|
1670
|
+
// ray goes on to the blocks behind them
|
|
1671
|
+
const maps = /** @type {Array<VoxelMap>} */ (typeof VoxelMap == 'undefined' ? [] :
|
|
1672
|
+
blockers.filter((o)=> o instanceof VoxelMap));
|
|
1673
|
+
for (const map of maps)
|
|
1674
|
+
if (map.raycast(ray, reach, (type)=> !map.blockType(type).seeThrough))
|
|
1675
|
+
return true;
|
|
1676
|
+
// an object is hit by its box, and a box the camera is inside, a room, a wide floor or the player's own
|
|
1677
|
+
// body, says nothing of what is in the way: the ray the other way hits it too, and it is left out; so is
|
|
1678
|
+
// one the light is inside, its lamp
|
|
1679
|
+
const backward = ray.direction.scale(-1), back = new Ray3D(ray.origin, backward);
|
|
1680
|
+
const lamp = this.light && [new Ray3D(source.pos, ray.direction), new Ray3D(source.pos, backward)];
|
|
1681
|
+
const hits = (r, o)=> render3DRaycastObject(r, o) !== undefined;
|
|
1682
|
+
const around = (o)=> !(o instanceof HeightMap) && (hits(back, o) || lamp && hits(lamp[0], o) && hits(lamp[1], o));
|
|
1683
|
+
const hit = render3D.pick(ray, blockers.filter((o)=> !maps.some((map)=> map === o) && !around(o)));
|
|
1684
|
+
return !!hit && hit.distance < reach;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
/** Ease visible toward whether the sun shows, called automatically each frame */
|
|
1688
|
+
update()
|
|
1689
|
+
{
|
|
1690
|
+
super.update();
|
|
1691
|
+
const shows = !!this.getSunScreenPos() && !(this.occlusion && this.isSunHidden());
|
|
1692
|
+
const step = this.fadeTime > 0 ? timeDelta / this.fadeTime : 1;
|
|
1693
|
+
this.visible = clamp(this.visible + (shows ? step : -step));
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
/** Draw the flare over the 3D scene, added onto it, called automatically in the 2D pass */
|
|
1697
|
+
render()
|
|
1698
|
+
{
|
|
1699
|
+
if (headlessMode || !glEnable) return;
|
|
1700
|
+
const elements = this.getScreenElements();
|
|
1701
|
+
if (!elements.length) return;
|
|
1702
|
+
setAdditiveBlendMode(true);
|
|
1703
|
+
for (const e of elements)
|
|
1704
|
+
{
|
|
1705
|
+
const tileInfo = render3DFlareTile(e.shape);
|
|
1706
|
+
tileInfo && drawTile(e.pos, vec2(e.size), tileInfo, e.color, 0, false, undefined, true, true);
|
|
1707
|
+
}
|
|
1708
|
+
setAdditiveBlendMode(false);
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
/** A flare has nothing to draw in the 3D pass */
|
|
1712
|
+
render3D() {}
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1442
1715
|
///////////////////////////////////////////////////////////////////////////////
|
|
1443
1716
|
// OBJ meshes
|
|
1444
1717
|
|