littlejsengine 1.22.0 → 1.23.1

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.
@@ -302,17 +302,19 @@ function render3DInstance(mesh, matrix, tileInfo, color)
302
302
  // make room for one more instance of a mesh under a texture and the current draw state, flushing a batch that
303
303
  // differs first, and return where its 24 floats go in mesh.instanceData: the matrix, the tint and the uv rect
304
304
  // drawMesh passes whether its matrix mirrors, and its batch then culls by the mesh and winds by that at the flush,
305
- // so it never sets the draw state for them, which would move the state's version at every draw
306
- function render3DInstanceSlot(mesh, textureInfo, mirrored)
305
+ // so it never sets the draw state for them, which would move the state's version at every draw; drawBillboard
306
+ // passes that its batch is unlit, for the same reason
307
+ function render3DInstanceSlot(mesh, textureInfo, mirrored, unlit=false)
307
308
  {
308
309
  const r = render3D;
309
310
  if (mesh.instanceCount && (mesh.instanceTextureInfo !== textureInfo || mesh.instanceMirrored !== mirrored
310
- || render3DStateChanged(mesh.instanceState)))
311
+ || mesh.instanceUnlit !== unlit || render3DStateChanged(mesh.instanceState)))
311
312
  render3DFlushInstances(mesh);
312
313
  if (!mesh.instanceCount)
313
314
  {
314
315
  mesh.instanceTextureInfo = textureInfo;
315
316
  mesh.instanceMirrored = mirrored;
317
+ mesh.instanceUnlit = unlit;
316
318
  mesh.instanceState = render3DCaptureBatchState();
317
319
  r.instanceMeshes.push(mesh);
318
320
  }
@@ -346,6 +348,8 @@ function render3DFlushInstances(only)
346
348
  const state = mesh.instanceState;
347
349
  if (mesh.instanceMirrored !== undefined) // a drawMesh batch: culled by its mesh, wound by its matrices
348
350
  state.cullBackFaces = !mesh.doubleSided, state.mirrored = mesh.instanceMirrored;
351
+ if (mesh.instanceUnlit) // a batch of sprites
352
+ state.lighting = false;
349
353
  render3DDrawInstanced(mesh, buffer, count, mesh.instanceTextureInfo, state);
350
354
  }
351
355
  if (!only)
@@ -433,12 +437,17 @@ function render3DShadowCaster()
433
437
  light.intensity > 0 && light.color.a > 0 ? light : undefined;
434
438
  }
435
439
 
440
+ // the widest cone a shadow map can look down, to each side; the light that casts the shadows lights no wider, or
441
+ // what is past the map's edge would be lit with nothing to shade it
442
+ const RENDER3D_SHADOW_CONE_MAX = 1.35;
443
+
436
444
  // a light's cone as the four numbers the shader gets: the way it shines, scaled so that its dot with the way to
437
445
  // a point, less the fourth number, is 0 at the edge of the cone and 1 where its fade starts; a light with no cone
438
446
  // gets numbers that make it 1 every way
439
447
  function render3DLightCone(light)
440
448
  {
441
- const angle = light.directional ? 0 : min(light.coneAngle, PI);
449
+ const widest = light === render3DShadowCaster() ? RENDER3D_SHADOW_CONE_MAX : PI;
450
+ const angle = light.directional ? 0 : min(light.coneAngle, widest);
442
451
  if (!(angle > 0)) return [0, 0, 0, -1];
443
452
  const outer = cos(angle), inner = cos(angle * (1 - clamp(light.coneSoftness)));
444
453
  const k = 1 / max(inner - outer, 1e-4), forward = light.getForward3D();
@@ -697,6 +706,7 @@ class Render3DPlugin
697
706
  /** @type {TextureInfo|undefined} */
698
707
  this.streamTileInfo = undefined;
699
708
  this.streamState = undefined; // captured state the pending batch was drawn under
709
+ this.streamUnlit = false; // the pending batch is drawn unlit whatever that state says, billboards are
700
710
  /** @type {Mesh|undefined} */
701
711
  this.capture = undefined; // the mesh a bake is filling
702
712
  /** @type {Array<{distance: number, state: Object, draw: function(): void}>|undefined} */
@@ -1087,6 +1097,8 @@ class Render3DPlugin
1087
1097
  {
1088
1098
  if (!this.streamCount || !render3DCanDraw()) return;
1089
1099
  const gl = glContext;
1100
+ if (this.streamUnlit)
1101
+ this.streamState.lighting = false; // its own copy of the state
1090
1102
  render3DSetDrawUniforms(RENDER3D_IDENTITY, this.streamTileInfo, WHITE, RENDER3D_FULL_UV_RECT, this.streamState);
1091
1103
  render3DBindVertexBuffer(this.streamBuffer);
1092
1104
  gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.streamFloats, 0, this.streamCount * RENDER3D_VERTEX_FLOATS);
@@ -1225,8 +1237,9 @@ class Render3DPlugin
1225
1237
  // a spotlight's map looks down its cone with perspective, from just in front of it out to its radius
1226
1238
  const pos = caster.getWorldPos3D(), forward = caster.getForward3D();
1227
1239
  const up = abs(forward.y) > .99 ? vec3(0, 0, 1) : vec3(0, 1, 0);
1228
- const far = caster.radius, near = max(far / 500, .02);
1229
- const fov = min(min(caster.coneAngle, PI) * 2 + .1, 2.8);
1240
+ // the near plane stays in front of the far one however small the light
1241
+ const far = caster.radius, near = min(max(far / 500, .02), far / 2);
1242
+ const fov = min(caster.coneAngle, RENDER3D_SHADOW_CONE_MAX) * 2 + .1;
1230
1243
  this.shadowMatrix = Matrix4.perspective(fov, 1, near, far).multiply(
1231
1244
  Matrix4.lookAt(pos, pos.add(forward), up).invert());
1232
1245
  this.shadowPlanes = render3DFrustumPlanes(this.shadowMatrix);
@@ -1320,13 +1333,34 @@ class Render3DPlugin
1320
1333
  return this.queueTransparent(p, ()=> this.drawBillboard(p, s, tileInfo, c, angle, upright));
1321
1334
  }
1322
1335
 
1323
- // the quad's six stream vertices written straight in with no vectors made, unlit, for sprites, and for
1324
- // particles when instancing is off
1325
- const lit = this.lighting;
1326
- this.lighting = this.shadowPass && lit; // unlit on screen, in the shadow map the object's flag decides
1327
- let uvRect;
1328
- try { uvRect = render3DBeginStrip(6, tileInfo); }
1329
- finally { this.lighting = lit; }
1336
+ // unlit on screen, in the shadow map the object's flag decides; the batch is told, the draw state is not
1337
+ // set, which would move its version at every sprite
1338
+ const unlit = !this.shadowPass;
1339
+ if (!this.blend && this.depthTest && this.instancing)
1340
+ {
1341
+ // an opaque sprite is one more instance of the shared quad, as a mesh's uses are: every sprite of a
1342
+ // sheet is one instanced draw at the end of the stage, with nothing written per corner; the shader
1343
+ // drops its see through texels, so it needs no sorting
1344
+ if (!render3DCanDraw() || this.shadowPass && !this.lighting) return; // unlit things cast no shadow
1345
+ if (this.frustumCulling && !render3DSphereVisible(pos.x, pos.y, pos.z, hypot(size.x, size.y) / 2))
1346
+ return;
1347
+ const quad = this.billboardMesh, cb = this.cameraBack, uv = render3DGetTileUVs(tileInfo);
1348
+ render3DMeshUpload(quad);
1349
+ // the quad's half axes, doubled for its unit square
1350
+ const a = render3DBillboardAxes(size, angle, upright);
1351
+ const k = render3DInstanceSlot(quad, render3DTextureOf(tileInfo), false, unlit), data = quad.instanceData;
1352
+ data[k] = a[0] * 2; data[k+1] = a[1] * 2; data[k+2] = a[2] * 2; data[k+3] = 0;
1353
+ data[k+4] = a[3] * 2; data[k+5] = a[4] * 2; data[k+6] = a[5] * 2; data[k+7] = 0;
1354
+ data[k+8] = cb.x; data[k+9] = cb.y; data[k+10] = cb.z; data[k+11] = 0;
1355
+ data[k+12] = pos.x; data[k+13] = pos.y; data[k+14] = pos.z; data[k+15] = 1;
1356
+ data[k+16] = color.r; data[k+17] = color.g; data[k+18] = color.b; data[k+19] = color.a;
1357
+ data[k+20] = uv.x; data[k+21] = uv.y; data[k+22] = uv.w; data[k+23] = uv.h;
1358
+ return;
1359
+ }
1360
+
1361
+ // one that blends is drawn in its sorted place: the quad's six stream vertices written straight in with no
1362
+ // vectors made, for sprites that blend, and for particles when instancing is off
1363
+ const uvRect = render3DBeginStrip(6, tileInfo, unlit);
1330
1364
  if (!uvRect) return;
1331
1365
  const a = render3DBillboardAxes(size, angle, upright);
1332
1366
  const rx = a[0], ry = a[1], rz = a[2], ux = a[3], uy = a[4], uz = a[5];
@@ -2438,8 +2472,9 @@ function render3DRenderDepth()
2438
2472
  // which way they face, so keeping the count even keeps every strip facing out.
2439
2473
 
2440
2474
  // make room in the stream for a strip of count vertices under the current state and texture, flushing a batch that
2441
- // differs first; returns the uv rect to map the vertices with, or undefined when nothing can be drawn
2442
- function render3DBeginStrip(count, tileInfo)
2475
+ // differs first; returns the uv rect to map the vertices with, or undefined when nothing can be drawn; unlit draws
2476
+ // the batch unlit whatever the state says, so a billboard does not set the state and set it back at every draw
2477
+ function render3DBeginStrip(count, tileInfo, unlit=false)
2443
2478
  {
2444
2479
  const r = render3D;
2445
2480
  if (!render3DCanDraw()) return;
@@ -2447,12 +2482,13 @@ function render3DBeginStrip(count, tileInfo)
2447
2482
  ASSERT(count <= RENDER3D_MAX_STREAM_VERTS, 'strip is too large for the stream, bake it into a mesh');
2448
2483
  if (count > RENDER3D_MAX_STREAM_VERTS) return;
2449
2484
  const textureInfo = render3DTextureOf(tileInfo);
2450
- if (r.streamCount && (textureInfo !== r.streamTileInfo || render3DStateChanged(r.streamState)
2451
- || r.streamCount + count > RENDER3D_MAX_STREAM_VERTS))
2485
+ if (r.streamCount && (textureInfo !== r.streamTileInfo || r.streamUnlit !== unlit
2486
+ || render3DStateChanged(r.streamState) || r.streamCount + count > RENDER3D_MAX_STREAM_VERTS))
2452
2487
  r.flush();
2453
2488
  render3DFlushBeforeOverlay();
2454
2489
  if (!r.streamCount)
2455
2490
  r.streamState = render3DCaptureBatchState();
2491
+ r.streamUnlit = unlit;
2456
2492
  r.streamTileInfo = textureInfo;
2457
2493
  return render3DGetTileUVs(tileInfo);
2458
2494
  }
@@ -4660,7 +4696,7 @@ class InstancedMesh3D extends EngineObject3D
4660
4696
  * - intensity multiplies the color, above 1 for a light brighter than white
4661
4697
  * - radius is a world distance, so scale3D does not change it
4662
4698
  * - An alpha, an intensity or a radius of 0 switches it off, and a light that is off takes none of those slots
4663
- * - Draws nothing itself, add a glow with drawSoftDisc or a small emissive mesh if it should be seen
4699
+ * - Draws nothing but its glow, when it has one; add a small emissive mesh if the lamp itself should be seen
4664
4700
  * @extends EngineObject3D
4665
4701
  * @memberof Render3D
4666
4702
  * @example
@@ -4709,8 +4745,10 @@ class Light3D extends EngineObject3D
4709
4745
  const r = render3D, c = this.color, pos = render3DObjectMatrix(this).getTranslation();
4710
4746
  const toCamera = r.camera.pos.subtract(pos), distance = toCamera.length();
4711
4747
  const at = distance ? pos.add(toCamera.scale(min(this.glow, distance) / 2 / distance)) : pos;
4712
- const color = rgb(c.r, c.g, c.b, c.a * min(this.intensity, 1));
4713
- r.drawBillboard(at, vec2(this.glow), engineGlowTexture(this.glowFalloff), color);
4748
+ const color = rgb(c.r, c.g, c.b, c.a * min(this.intensity, 1)), texture = engineGlowTexture(this.glowFalloff);
4749
+ // a browser that can not make the glow's texture draws no glow, not a plain square
4750
+ if (texture || !glContext)
4751
+ r.drawBillboard(at, vec2(this.glow), texture, color);
4714
4752
  }
4715
4753
  }
4716
4754
 
@@ -4720,7 +4758,7 @@ class Light3D extends EngineObject3D
4720
4758
  * - It shines from its position toward the origin, like a three.js DirectionalLight: only the direction to it
4721
4759
  * counts, so moving it or its parent swings the light around; parent it to a sun in the sky and it follows
4722
4760
  * - It cannot sit on the origin, since that leaves no direction
4723
- * - Like every Light3D it casts no shadow, only the sun, render3D.sunDirection, does
4761
+ * - It casts no shadow: the sun, render3D.sunDirection, does, or a spotlight set as render3D.shadowLight
4724
4762
  * @extends Light3D
4725
4763
  * @memberof Render3D
4726
4764
  * @example