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.
@@ -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: it is neither the engine's 2D
954
- * gravity nor render3D.gravity, so an effect keeps its own fall wherever it is used
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
- * not render3D.gravity, so the 2D emitter's gravityScale has no equivalent here
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 = 0;
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
- if (this.emitRate && particleEmitRateScale)
1100
+ const rate = this.emitRate * particleEmitRateScale;
1101
+ if (rate > 0 && rate < Infinity)
1088
1102
  {
1089
- this.emitTimeBuffer += this.emitRate * particleEmitRateScale * timeDelta;
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
- const damping = this.damping, gravity = this.gravity * scale, angleDamping = this.angleDamping; // a bigger effect has to fall faster to keep the same arc
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] *= damping, vy = data[k+4] = data[k+4] * damping + gravity, vz = data[k+5] *= damping;
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
- collideLevel && (this.particleCollide(k, data[k] - vx, data[k+1] - vy, data[k+2] - vz), reread());
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
- const offset = isVector3(size) ? vec3(rand(-.5, .5) * box.x, rand(-.5, .5) * box.y, rand(-.5, .5) * box.z)
1172
- : randInSphere(/** @type {number} */ (size) / 2);
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] - timeDelta); // aged past its life this update
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
- const restitution = max(this.restitution, level.restitution), friction = max(this.friction, level.friction);
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