littlejsengine 1.24.3 → 1.26.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/EDITOR.md +2 -2
  3. package/FAQ.md +55 -11
  4. package/README.md +6 -2
  5. package/REFERENCE.md +203 -57
  6. package/dist/littlejs.d.ts +933 -313
  7. package/dist/littlejs.esm.js +5143 -2419
  8. package/dist/littlejs.esm.min.js +38 -24
  9. package/dist/littlejs.js +5132 -2416
  10. package/dist/littlejs.min.js +37 -23
  11. package/dist/littlejs.release.js +4905 -2232
  12. package/package.json +4 -4
  13. package/plugins/box2d.js +141 -56
  14. package/plugins/drawUtilities.js +374 -380
  15. package/plugins/gltf.js +383 -60
  16. package/plugins/lightSystem.js +412 -30
  17. package/plugins/math3d.js +1015 -1014
  18. package/plugins/medalSystem.js +19 -7
  19. package/plugins/newgrounds.js +5 -6
  20. package/plugins/parallax.js +1 -0
  21. package/plugins/particleEffects.js +47 -14
  22. package/plugins/pathFinder.js +60 -38
  23. package/plugins/pluginExport.js +11 -0
  24. package/plugins/postProcess.js +200 -67
  25. package/plugins/render3d.js +493 -2391
  26. package/plugins/render3dDebug.js +5 -8
  27. package/plugins/render3dEditor.js +84 -9
  28. package/plugins/render3dExtras.js +122 -33
  29. package/plugins/render3dLevel.js +67 -8
  30. package/plugins/render3dMesh.js +1158 -0
  31. package/plugins/render3dObject.js +1451 -0
  32. package/plugins/render3dVoxels.js +15 -3
  33. package/plugins/textureSheet.js +9 -8
  34. package/plugins/threejs.js +1 -1
  35. package/plugins/tweenSystem.js +117 -87
  36. package/plugins/uiSystem.js +62 -28
  37. package/plugins/wavedash.js +229 -0
  38. package/src/engine.js +192 -40
  39. package/src/engineAudio.js +47 -27
  40. package/src/engineBuild.mjs +6 -1
  41. package/src/engineDebug.js +23 -162
  42. package/src/engineDraw.js +148 -43
  43. package/src/engineEditor.js +153 -40
  44. package/src/engineExport.js +0 -3
  45. package/src/engineInput.js +31 -25
  46. package/src/engineMath.js +38 -20
  47. package/src/engineObject.js +68 -50
  48. package/src/engineParticles.js +66 -18
  49. package/src/engineRelease.js +18 -15
  50. package/src/engineSettings.js +48 -9
  51. package/src/engineTileLayer.js +44 -18
  52. package/src/engineUtilities.js +56 -11
  53. package/src/engineWebGL.js +68 -22
@@ -6,8 +6,9 @@
6
6
  * - The 3D scene draws under the 2D sprites, so HUD and text land on top
7
7
  * - Lighting is the sun plus ambient, with optional extra lights, fog and shadows
8
8
  * - Any object or draw can bring its own Shader, a mainImage snippet the lighting then applies to
9
- * - Build shapes with buildBox, buildSphere, buildGrid and buildLathe; the other builders, terrain, particles,
10
- * camera controls and the OBJ loader are in the Render3D Extras plugin, which goes after this one
9
+ * - Meshes and the basic builders are in render3dMesh.js, EngineObject3D, instancing and lights in
10
+ * render3dObject.js, both right after this one; the other builders, terrain, particles, camera controls and the
11
+ * OBJ loader are in the Render3D Extras plugin, which goes after those
11
12
  * - Requires the Math3D plugin
12
13
  * @namespace Render3D
13
14
  */
@@ -99,13 +100,14 @@ function render3DCanDraw()
99
100
  * @property {number} reflectivity
100
101
  * @property {TextureInfo|undefined} emissiveMap
101
102
  * @property {Color} emissiveMapColor
103
+ * @property {CubeMap|undefined} environmentMap
102
104
  * @property {number} emissiveR
103
105
  * @property {number} emissiveG
104
106
  * @property {number} emissiveB
105
107
  * @memberof Render3D */
106
108
  const RENDER3D_STATE_FIELDS = ['blend', 'additive', 'depthTest', 'depthWrite', 'cullBackFaces', 'mirrored', 'lighting',
107
109
  'emissive', 'receiveShadow', 'specular', 'pixelated', 'shader', 'normalMap', 'normalScale', 'shininess',
108
- 'reflectivity', 'emissiveMap', 'emissiveMapColor'];
110
+ 'reflectivity', 'emissiveMap', 'emissiveMapColor', 'environmentMap'];
109
111
 
110
112
  // a copy of the draw state in one fixed shape, the fields of RENDER3D_STATE_FIELDS written out, and the version
111
113
  // it was taken at
@@ -117,7 +119,7 @@ function render3DCaptureBatchState()
117
119
  receiveShadow: d.receiveShadow, specular: d.specular, pixelated: d.pixelated, shader: d.shader,
118
120
  normalMap: d.normalMap, normalScale: d.normalScale, shininess: d.shininess, reflectivity: d.reflectivity,
119
121
  emissiveMap: d.emissiveMap, emissiveMapColor: (d.emissiveMapColor || WHITE).copy(), // the caller may change it
120
- version: r.stateVersion};
122
+ environmentMap: d.environmentMap, version: r.stateVersion};
121
123
  }
122
124
 
123
125
  // put a captured draw state back, written out the same way; the transparent stage does this for every queued draw
@@ -128,7 +130,8 @@ function render3DApplyBatchState(s)
128
130
  r.cullBackFaces = s.cullBackFaces, r.mirrored = s.mirrored, r.lighting = s.lighting, r.emissive = s.emissive,
129
131
  r.receiveShadow = s.receiveShadow, r.specular = s.specular, r.pixelated = s.pixelated, r.shader = s.shader,
130
132
  r.normalMap = s.normalMap, r.normalScale = s.normalScale, r.shininess = s.shininess,
131
- r.reflectivity = s.reflectivity, r.emissiveMap = s.emissiveMap, r.emissiveMapColor = s.emissiveMapColor;
133
+ r.reflectivity = s.reflectivity, r.emissiveMap = s.emissiveMap, r.emissiveMapColor = s.emissiveMapColor,
134
+ r.environmentMap = s.environmentMap;
132
135
  }
133
136
 
134
137
  // true when the current draw state differs from a captured one, so a pending batch must flush first; it runs for
@@ -144,7 +147,7 @@ function render3DStateChanged(s)
144
147
  || d.specular !== s.specular || d.pixelated !== s.pixelated || d.shader !== s.shader
145
148
  || d.normalMap !== s.normalMap || d.normalScale !== s.normalScale || d.shininess !== s.shininess
146
149
  || d.reflectivity !== s.reflectivity || d.emissiveMap !== s.emissiveMap
147
- || d.emissiveR !== c.r || d.emissiveG !== c.g || d.emissiveB !== c.b)
150
+ || d.environmentMap !== s.environmentMap || d.emissiveR !== c.r || d.emissiveG !== c.g || d.emissiveB !== c.b)
148
151
  return true;
149
152
  s.version = r.stateVersion; // set and set back to what it was: the batch goes on, checking the version again
150
153
  return false;
@@ -174,7 +177,8 @@ function render3DWithState(fields, fn)
174
177
  }
175
178
 
176
179
  // which side of the 2D scene an object draws on, its own flag or the plugin default
177
- function render3DIsAfter2D(o) { return !!(o.renderAfter2D ?? render3D.renderAfter2D); }
180
+ function render3DIsAfter2D(o)
181
+ { return !!((o.drawOwner ? render3DSetting(o, 'renderAfter2D') : o.renderAfter2D) ?? render3D.renderAfter2D); }
178
182
 
179
183
  // a size given as a number or a vec3
180
184
  /** @param {Vector3|number} size
@@ -250,29 +254,33 @@ function render3DQuadAxes(center, right, up)
250
254
  function render3DSetObjectState(o)
251
255
  {
252
256
  const r = render3D;
253
- const emissive = o?.emissive || 0;
257
+ const transparent = o?.transparent;
258
+ const p = o?.drawOwner ? render3DPartSettings(o) : o; // a part draws with its owner's settings
259
+ const emissive = p?.emissive || 0;
254
260
  ASSERT(isNumber(emissive) && emissive >= 0, 'emissive must be a number, 0 or more', emissive);
255
261
  r.lighting = true;
256
262
  r.emissive = emissive;
257
- r.additive = !!o?.additive;
258
- r.specular = o?.specular || 0;
259
- const shininess = o?.shininess ?? 16, reflectivity = o?.reflectivity || 0;
263
+ r.additive = !!p?.additive;
264
+ r.specular = p?.specular || 0;
265
+ const shininess = p?.shininess ?? 16, reflectivity = p?.reflectivity || 0;
260
266
  ASSERT(isNumber(shininess) && shininess > 0, 'shininess must be a number above 0', shininess);
261
267
  ASSERT(isNumber(reflectivity) && reflectivity >= 0 && reflectivity <= 1, 'reflectivity must be 0 to 1', reflectivity);
262
268
  r.shininess = shininess;
263
269
  r.reflectivity = reflectivity;
264
- r.normalMap = o?.normalMap || undefined;
265
- r.normalScale = o?.normalScale ?? 1;
266
- r.emissiveMap = o?.emissiveMap || undefined;
267
- r.emissiveMapColor = o?.emissiveMapColor || WHITE;
268
- r.receiveShadow = !o || o.receiveShadow;
270
+ r.normalMap = p?.normalMap || undefined;
271
+ r.normalScale = p?.normalScale ?? 1;
272
+ r.emissiveMap = p?.emissiveMap || undefined;
273
+ r.emissiveMapColor = p?.emissiveMapColor || WHITE;
274
+ ASSERT(!p?.environment || p.environment instanceof CubeMap, 'environment must be a CubeMap');
275
+ r.environmentMap = p?.environment || undefined;
276
+ r.receiveShadow = !p || p.receiveShadow;
269
277
  r.cullBackFaces = r.mirrored = false; // each mesh sets these as it draws
270
- r.pixelated = !!o?.pixelated;
271
- ASSERT(!o?.shader || o.shader instanceof Shader, 'shader must be a Shader, not the snippet itself');
272
- r.shader = o?.shader || undefined; // null is no shader too, so it batches with none
278
+ r.pixelated = !!p?.pixelated;
279
+ ASSERT(!p?.shader || p.shader instanceof Shader, 'shader must be a Shader, not the snippet itself');
280
+ r.shader = p?.shader || undefined; // null is no shader too, so it batches with none
273
281
  r.depthTest = true;
274
282
  if (r.shadowPass)
275
- r.blend = !!o?.transparent; // the depth shader cuts a see through caster by the alpha it would blend with
283
+ r.blend = !!transparent; // the depth shader cuts a see through caster by the alpha it would blend with
276
284
  }
277
285
 
278
286
  // draw objects each with the draw state set from its own flags, then reset to the defaults
@@ -510,7 +518,8 @@ class Render3DPlugin
510
518
  this.drawState = {blend: false, additive: false, depthTest: true, depthWrite: true, cullBackFaces: false,
511
519
  mirrored: false, lighting: true, emissive: 0, receiveShadow: true, specular: 0, pixelated: false,
512
520
  shader: undefined, normalMap: undefined, normalScale: 1, shininess: 16, reflectivity: 0,
513
- emissiveMap: undefined, emissiveMapColor: WHITE, emissiveR: 1, emissiveG: 1, emissiveB: 1};
521
+ emissiveMap: undefined, emissiveMapColor: WHITE, environmentMap: undefined, emissiveR: 1, emissiveG: 1,
522
+ emissiveB: 1};
514
523
  /** @property {number} - Goes up when a draw state field changes, so a batch sees at a glance that none did */
515
524
  this.stateVersion = 0;
516
525
  ASSERT(Object.keys(render3DCaptureBatchState()).join() === [...RENDER3D_STATE_FIELDS, 'version'].join(),
@@ -591,6 +600,18 @@ class Render3DPlugin
591
600
  /** @property {Mesh|undefined} - Sky dome from buildSky or setSky, drawn around the camera behind everything
592
601
  * @type {Mesh|undefined} */
593
602
  this.sky = undefined;
603
+ /** @property {CubeMap|undefined} - A cube map drawn as the sky, behind everything, in place of the sky dome;
604
+ * an orthographic camera looks the same way through every pixel, so it sees one color of it; fog fades to
605
+ * fogColor, not to the sky box, so with fog set fogColor to its horizon's color
606
+ * @type {CubeMap|undefined} */
607
+ this.skyBox = undefined;
608
+ /** @property {CubeMap|undefined} - The world around, what reflective surfaces reflect: an object's
609
+ * reflectivity says how much and its shininess how sharp, the same shininess that tightens its highlight:
610
+ * 10000 a mirror, 1000 polished, 10 a wide blur; undefined reflects the sky's colors as setSky gave them; a cube map holds GPU
611
+ * memory until its dispose(), so one made again and again, as for a sky that changes, disposes the one it
612
+ * replaces, or a captured one is captured into again
613
+ * @type {CubeMap|undefined} */
614
+ this.environment = undefined;
594
615
  /** @property {boolean} - Draw the 3D scene on top of the 2D scene instead of under it */
595
616
  this.renderAfter2D = false;
596
617
  /** @property {boolean} - Draw see through things far to near so they blend correctly */
@@ -660,6 +681,19 @@ class Render3DPlugin
660
681
  this.shadowShader = undefined;
661
682
  /** @type {WebGLVertexArrayObject|undefined} */
662
683
  this.vao = undefined;
684
+ /** @type {Set<CubeMap>} */
685
+ this.cubeCaptures = new Set; // the cube maps capture asked to draw in the next pass
686
+ /** @type {CubeMap|undefined} */
687
+ this.capturingCube = undefined; // the cube map being drawn, which nothing may read meanwhile
688
+ /** @type {WebGLFramebuffer|undefined} */
689
+ this.captureFramebuffer = undefined;
690
+ /** @type {WebGLRenderbuffer|undefined} */
691
+ this.captureDepth = undefined;
692
+ this.captureDepthSize = 0;
693
+ /** @type {WebGLProgram|undefined} */
694
+ this.skyBoxProgram = undefined; // draws the sky box, made on its first draw
695
+ /** @type {WebGLVertexArrayObject|undefined} */
696
+ this.skyBoxVao = undefined; // an empty one, its triangle comes from gl_VertexID
663
697
  /** @type {WebGLTexture|undefined} */
664
698
  this.whiteTexture = undefined; // 1x1 white for untextured draws
665
699
  /** @type {Map<number, WebGLSampler>} */
@@ -679,7 +713,8 @@ class Render3DPlugin
679
713
  this.cameraDepthHeight = 0;
680
714
  this.depthPass = false; // drawing the camera's depth, a shadow pass seen from the camera
681
715
  this.contextGeneration = 0; // counts context losses, a mesh uploaded under an older one uploads again
682
- this.uniforms = new Map; // uniform locations by program
716
+ /** @type {WeakMap<WebGLProgram, Object<string, WebGLUniformLocation|null>>} */
717
+ this.uniforms = new WeakMap; // uniform locations by program, weak so a freed one goes with it
683
718
  /** @type {Object<string, Array<number>>} */
684
719
  this.uniformValues = {}; // last values sent for the cached vec4 uniforms
685
720
  this.shadowMapDrawn = false; // the shadow map is drawn by the first pass of the frame
@@ -706,7 +741,8 @@ class Render3DPlugin
706
741
  this.streamCount = 0;
707
742
  /** @type {TextureInfo|undefined} */
708
743
  this.streamTileInfo = undefined;
709
- this.streamState = undefined; // captured state the pending batch was drawn under
744
+ /** @type {Object|undefined} */
745
+ this.streamState = undefined; // captured state the pending batch was drawn under, render3DCaptureBatchState's
710
746
  this.streamUnlit = false; // the pending batch is drawn unlit whatever that state says, billboards are
711
747
  /** @type {Mesh|undefined} */
712
748
  this.capture = undefined; // the mesh a bake is filling
@@ -792,6 +828,12 @@ class Render3DPlugin
792
828
  get emissiveMap() { return this.drawState.emissiveMap; }
793
829
  set emissiveMap(v) { const d = this.drawState; d.emissiveMap === v || (d.emissiveMap = v, ++this.stateVersion); }
794
830
 
831
+ /** The cube map the next draws reflect in place of render3D.environment, set from each object's environment
832
+ * @return {CubeMap|undefined} */
833
+ get environmentMap() { return this.drawState.environmentMap; }
834
+ set environmentMap(v)
835
+ { const d = this.drawState; d.environmentMap === v || (d.environmentMap = v, ++this.stateVersion); }
836
+
795
837
  /** Multiplies the emissive map, set from each object's emissiveMapColor; compared by its rgb, so a Color
796
838
  * changed in place is seen when it is set again, and undefined is white
797
839
  * @return {Color} */
@@ -957,8 +999,8 @@ class Render3DPlugin
957
999
  if (range)
958
1000
  {
959
1001
  const distance = offset.length();
960
- if (distance > range)
961
- return; // out of range
1002
+ if (distance >= range)
1003
+ return; // out of range, at it too, where the fade is silent
962
1004
  const taperRange = range * sound.taper;
963
1005
  if (distance > taperRange)
964
1006
  volume *= percent(distance, range, taperRange);
@@ -1137,14 +1179,15 @@ class Render3DPlugin
1137
1179
  {
1138
1180
  const opaque = [], transparent = [];
1139
1181
  for (const o of objects)
1140
- (o.transparent || o.additive ? transparent : opaque).push(o);
1182
+ (o.transparent || render3DSetting(o, 'additive') ? transparent : opaque).push(o);
1141
1183
 
1142
- isDefault && this.sky && this.drawSky();
1184
+ isDefault && (this.skyBox && this.skyBox !== this.capturingCube || this.sky) && this.drawSky();
1143
1185
 
1144
1186
  // opaque: no blending, depth writes on, by render order
1145
1187
  this.blend = false;
1146
1188
  this.depthWrite = true;
1147
- const byOrder = (a, b)=> a.renderOrder - b.renderOrder;
1189
+ const byOrder = (a, b)=> (a.drawOwner ? render3DSetting(a, 'renderOrder') : a.renderOrder) -
1190
+ (b.drawOwner ? render3DSetting(b, 'renderOrder') : b.renderOrder);
1148
1191
  opaque.sort(byOrder);
1149
1192
  transparent.sort(byOrder);
1150
1193
  render3DDrawObjects(opaque);
@@ -1170,7 +1213,7 @@ class Render3DPlugin
1170
1213
  isDefault && this.onRenderTransparent?.();
1171
1214
  }
1172
1215
  finally { this.flushTransparentQueue(); }
1173
- isDefault && render3DRenderDebug();
1216
+ isDefault && !this.capturingCube && render3DRenderDebug();
1174
1217
  this.flush();
1175
1218
 
1176
1219
  // leave the fields at the opaque defaults for anything reading them outside the pass
@@ -1216,10 +1259,14 @@ class Render3DPlugin
1216
1259
  finally { render3DApplyBatchState(state); }
1217
1260
  }
1218
1261
 
1219
- /** Draw render3D.sky around the camera, unlit, unfogged and behind everything, called automatically by the pass */
1262
+ /** Draw render3D.skyBox, or render3D.sky around the camera, unlit, unfogged and behind everything, called
1263
+ * automatically by the pass */
1220
1264
  drawSky()
1221
1265
  {
1222
1266
  this.flush();
1267
+ if (this.skyBox && this.skyBox !== this.capturingCube && render3DDrawSkyBox(this.skyBox))
1268
+ return; // a plain return, so the method is typed void and a subclass may override it so
1269
+ if (!this.sky) return; // a sky box that could not draw, with no dome to draw instead
1223
1270
  // the dome only has to sit between the clip planes, the pass draws it first with no depth test;
1224
1271
  // a far plane at Infinity has no midpoint, so put it a long way out instead
1225
1272
  const {near, far} = this.camera;
@@ -1569,7 +1616,8 @@ class Camera3D
1569
1616
  this.far = 1e3;
1570
1617
  /** @property {number} - Visible height in world units for an orthographic view, 0 is perspective */
1571
1618
  this.orthographic = 0;
1572
- /** @property {boolean} - Line the 3D camera up with the 2D camera, so 3D things at z=0 sit on the 2D sprites */
1619
+ /** @property {boolean} - Line the 3D camera up with the 2D camera, so 3D things at z=0 sit on the 2D sprites;
1620
+ * it lines up with the 2D view of the main canvas, whatever canvas size a screenToRay is given */
1573
1621
  this.align2D = false;
1574
1622
  }
1575
1623
 
@@ -1671,7 +1719,7 @@ const RENDER3D_VERTEX_SOURCE =
1671
1719
  'gl_Position=viewProj*w;' +
1672
1720
  'P=w.xyz;' +
1673
1721
  'vec3 c0=m0.xyz,c1=m1.xyz,c2=m2.xyz;' +
1674
- 'N=mat3(c0/dot(c0,c0),c1/dot(c1,c1),c2/dot(c2,c2))*n;' +
1722
+ 'N=mat3(c0/max(dot(c0,c0),1e-20),c1/max(dot(c1,c1),1e-20),c2/max(dot(c2,c2),1e-20))*n;' +
1675
1723
  'T=uvRect.xy+t*uvRect.zw;' +
1676
1724
  'L=t;' +
1677
1725
  'C=c*tint;' +
@@ -1711,6 +1759,9 @@ function render3DFragmentSource(fragmentCode)
1711
1759
  'uniform vec3 cameraPos;' +
1712
1760
  'uniform vec4 materialParams,emissiveTint,skyTop,skyHorizon,skyBottom,gelAxes;' +
1713
1761
  'uniform sampler2D tex,normalTex,emissiveTex,gelTex;' +
1762
+ 'uniform samplerCube envMap;' + // the environment, when envParams.x is 1, envParams.y its last mipmap, z the
1763
+ // level a shininess of -1 would read, log2(size * .45), and w the level a turn of one radian a pixel reads
1764
+ 'uniform vec4 envParams;' +
1714
1765
  'uniform bool premultipliedTexture;' + // is the texture a render target, which holds premultiplied color
1715
1766
  'uniform highp sampler2DShadow shadowMap;' +
1716
1767
  'in vec3 P,N;in vec2 T,L;in vec4 C,S;' +
@@ -1744,11 +1795,11 @@ function render3DFragmentSource(fragmentCode)
1744
1795
  'vec3 m=texture(normalTex,T).xyz*2.-1.;' +
1745
1796
  'm.xy*=materialParams.x;' +
1746
1797
  'return normalize((u*m.x-v*m.y)*inversesqrt(k)+n*m.z);}' +
1747
- (fragmentCode ? RENDER3D_SNIPPET_NAMES + fragmentCode + '\n' : '') +
1798
+ // a snippet's mainImage is declared here and written after main, so a define in it can not reach main
1799
+ (fragmentCode ? 'void mainImage(out vec4,vec2);' : '') +
1748
1800
  'void main(){' +
1749
1801
  (fragmentCode ? 'vec4 t;mainImage(t,T);' : 'vec4 t=texture(tex,T);') +
1750
1802
  'if(premultipliedTexture&&t.a>0.)t.rgb/=t.a;' + // back to straight color, what the lighting and blend expect
1751
- 'if(shadowParams.w>0.&&t.a<.5)discard;' + // an opaque draw drops see through texels, as the shadow map does
1752
1803
  'vec4 c=C*t;' +
1753
1804
  'float e=lightDir.w;' +
1754
1805
  'if(e<1.){' +
@@ -1793,8 +1844,13 @@ function render3DFragmentSource(fragmentCode)
1793
1844
  // shadow does not dim it
1794
1845
  'if(materialParams.z>0.){' +
1795
1846
  'vec3 w=normalize(P-cameraPos),q=reflect(w,n);' +
1796
- 'float f=materialParams.z+(1.-materialParams.z)*pow(1.-max(dot(n,-w),0.),5.);' +
1797
- 'c.rgb=mix(c.rgb,q.y>0.?mix(skyHorizon.rgb,skyTop.rgb,q.y):mix(skyHorizon.rgb,skyBottom.rgb,-q.y),f);' +
1847
+ 'float f=materialParams.z+(1.-materialParams.z)*pow(clamp(1.-dot(n,-w),0.,1.),5.);' +
1848
+ // the environment when there is one, blurred more the lower the shininess, a mirror at a shininess of
1849
+ // 10000 and toward its last mipmap at 1, or by how far the reflection turns from one pixel to the next when
1850
+ // that is more, so a small mirror reads the blur of what its pixels cover instead of sparkling
1851
+ 'c.rgb=mix(c.rgb,envParams.x>0.?textureLod(envMap,q,clamp(max(envParams.z-.5*log2(materialParams.y+2.),' +
1852
+ 'envParams.w+log2(max(max(length(dFdx(q)),length(dFdy(q))),1e-9))),0.,envParams.y)).rgb:' +
1853
+ 'q.y>0.?mix(skyHorizon.rgb,skyTop.rgb,q.y):mix(skyHorizon.rgb,skyBottom.rgb,-q.y),f);' +
1798
1854
  '}}else c.rgb*=e;' + // fully emissive: its own color, or brighter, with no lighting to work out
1799
1855
  // the emissive map adds its light on top, lit or not
1800
1856
  'if(emissiveTint.a>0.)c.rgb+=texture(emissiveTex,T).rgb*emissiveTint.rgb;' +
@@ -1802,15 +1858,21 @@ function render3DFragmentSource(fragmentCode)
1802
1858
  'float z=distance(cameraPos,P);' +
1803
1859
  'c.rgb=mix(c.rgb,shadowParams.w<0.?vec3(0):fogColor.rgb,smoothstep(fogColor.a,ambientFog.a,z));' +
1804
1860
  '}' +
1805
- 'o=vec4(c.rgb,shadowParams.w>0.?1.:c.a);' + // an opaque draw stays opaque whatever the tint alpha says
1806
- '}';
1861
+ // an opaque draw drops see through texels, as the shadow map does, at the end, after the normal map's
1862
+ // derivatives and the texture samples, which need the pixels around them, and stays opaque otherwise
1863
+ 'if(shadowParams.w>0.&&t.a<.5)discard;' +
1864
+ 'o=vec4(c.rgb,shadowParams.w>0.?1.:c.a);' +
1865
+ '}' +
1866
+ (fragmentCode ? '\n' + RENDER3D_SNIPPET_NAMES + fragmentCode + '\n' : '');
1807
1867
  }
1808
1868
 
1809
1869
  // a Shader's 3D program, compiled the first time a draw needs it
1810
1870
  function render3DShaderProgram(shader)
1811
1871
  {
1812
1872
  ASSERT(shader instanceof Shader, 'render3D.shader must be a Shader, not the snippet itself');
1813
- return shader.program3D ||= glCreateProgram(RENDER3D_VERTEX_SOURCE, render3DFragmentSource(shader.fragmentCode));
1873
+ shader.program3D || glShaderTrack(shader); // one let go of and drawn again is kept for a lost context again
1874
+ const program = shader.program3D ||= glCreateProgram(RENDER3D_VERTEX_SOURCE, render3DFragmentSource(shader.fragmentCode));
1875
+ return glFailedPrograms.has(program) ? render3D.program : program; // one that did not build draws as with none
1814
1876
  }
1815
1877
 
1816
1878
  // make a program current for the pass and send it the pass uniforms: the matrices, the camera and the lights,
@@ -1827,6 +1889,7 @@ function render3DUseProgram(program)
1827
1889
  gl.uniform1i(render3DUniform('normalTex'), 2);
1828
1890
  gl.uniform1i(render3DUniform('emissiveTex'), 3);
1829
1891
  gl.uniform1i(render3DUniform('gelTex'), 4);
1892
+ gl.uniform1i(render3DUniform('envMap'), 5);
1830
1893
  gl.uniform4fv(render3DUniform('gelAxes'), r.gelAxes);
1831
1894
  const c = r.camera.pos;
1832
1895
  gl.uniform3f(render3DUniform('cameraPos'), c.x, c.y, c.z);
@@ -1840,7 +1903,8 @@ function render3DUseProgram(program)
1840
1903
  if (program !== r.program)
1841
1904
  {
1842
1905
  gl.uniform1f(render3DUniform('iTime'), time);
1843
- gl.uniform3f(render3DUniform('iResolution'), glCanvas.width, glCanvas.height, 1);
1906
+ const face = r.capturingCube?.size; // in a capture the face is the screen
1907
+ gl.uniform3f(render3DUniform('iResolution'), face || glCanvas.width, face || glCanvas.height, 1);
1844
1908
  }
1845
1909
  }
1846
1910
 
@@ -1849,12 +1913,14 @@ function render3DInitGL()
1849
1913
  if (headlessMode) return;
1850
1914
  if (!glEnable || !glContext)
1851
1915
  {
1852
- console.warn('Render3DPlugin: WebGL not enabled, construct the plugin in gameInit with glEnable set');
1916
+ // a device whose WebGL could not draw, which fell back to Canvas2D, is not helped by setting glEnable
1917
+ console.warn(!glDeviceFailed ? 'Render3DPlugin: WebGL not enabled, construct the plugin in gameInit with glEnable set'
1918
+ : 'Render3DPlugin: this device can not draw WebGL, so nothing 3D is drawn');
1853
1919
  return;
1854
1920
  }
1855
1921
  const gl = glContext, r = render3D;
1856
1922
  glFlush(); // a pending 2D batch draws now, while the engine's own buffer, vertex array and program are bound
1857
- r.uniforms = new Map;
1923
+ r.uniforms = new WeakMap;
1858
1924
  r.uniformValues = {};
1859
1925
  render3DShadowCut = undefined; // the shadow shader is new too
1860
1926
  r.attribValues = []; // a fresh context has its own attribute defaults, so nothing sent before it counts
@@ -1881,6 +1947,15 @@ function render3DInitGL()
1881
1947
  'void main(){if(texture(tex,T).a*mix(1.,A,cut)<.5)discard;}'
1882
1948
  );
1883
1949
 
1950
+ // a program that did not build in a release build would fail every draw: 3D stays off, said once, since the pass
1951
+ // returns when there is no program, and a lost context that comes back tries again
1952
+ if (glFailedPrograms.has(r.program) || glFailedPrograms.has(r.shadowShader))
1953
+ {
1954
+ console.error('Render3DPlugin: its shaders did not build on this device, so nothing 3D is drawn');
1955
+ r.program = r.shadowShader = undefined;
1956
+ return;
1957
+ }
1958
+
1884
1959
  // the vertex array object with the attributes enabled once, pointers are set per buffer by render3DBindVertexBuffer
1885
1960
  // the per instance attributes get their divisor only while a batch has them on, see render3DDrawInstanced
1886
1961
  r.vao = gl.createVertexArray();
@@ -1908,10 +1983,346 @@ function render3DInitGL()
1908
1983
  glSetInstancedMode(true);
1909
1984
  }
1910
1985
 
1986
+ ///////////////////////////////////////////////////////////////////////////////
1987
+ /**
1988
+ * CubeMap - Six square images all around a point, the world far away: what reflective surfaces reflect as
1989
+ * render3D.environment, and the sky as render3D.skyBox
1990
+ * - makeCubeMap paints one from a function of direction, loadCubeMap loads six images
1991
+ * - Made with no faces it is drawn from the scene: capture(pos3D) draws everything around that point into it in the
1992
+ * next frame's 3D pass, six views of the whole scene, so capture once for a still scene or now and then for one
1993
+ * that moves
1994
+ * - Its faces are in WebGL's order and lay out, +x, -x, +y, -y, +z, -z: WebGL's cube is left handed, so in this right
1995
+ * handed world a face seen from the middle is mirrored; loadCubeMap turns a sky box set's images to it
1996
+ * @memberof Render3D
1997
+ * @example
1998
+ * render3D.environment = render3D.skyBox = makeCubeMap(64, (d)=> hsl(.6, .8, .4 + d.y * .4));
1999
+ * @example
2000
+ * // a chrome ball that reflects the scene around it, captured once
2001
+ * const ball = new EngineObject3D(vec3(0, 1, 0), render3D.sphereMesh, undefined, BLACK);
2002
+ * ball.reflectivity = 1;
2003
+ * ball.shininess = 1e4;
2004
+ * render3D.environment = new CubeMap(128);
2005
+ * render3D.environment.capture(ball.pos3D);
2006
+ */
2007
+ class CubeMap
2008
+ {
2009
+ /** Make a cube map from its six faces, as makeCubeMap and loadCubeMap do, or with none to capture the scene into
2010
+ * @param {number} size - Pixels a side of each face
2011
+ * @param {Array<Uint8Array|HTMLImageElement|HTMLCanvasElement|ImageBitmap|OffscreenCanvas>} [faces] - +x, -x,
2012
+ * +y, -y, +z and -z, RGBA pixels row by row or images; none for a cube map drawn by capture */
2013
+ constructor(size, faces)
2014
+ {
2015
+ ASSERT(isNumber(size) && size >= 1 && size % 1 === 0, 'a cube map\'s size must be a whole positive number', size);
2016
+ ASSERT(faces === undefined || isArray(faces) && faces.length === 6, 'a cube map has six faces');
2017
+ ASSERT(!faces || faces.every((face)=> face instanceof Uint8Array ? face.length === size * size * 4 :
2018
+ face.width === size && face.height === size), 'each face of a cube map is size by size, RGBA or an image');
2019
+ /** @property {number} - Pixels a side of each face */
2020
+ this.size = size;
2021
+ /** @property {Array<Uint8Array|HTMLImageElement|HTMLCanvasElement|ImageBitmap|OffscreenCanvas>|undefined} -
2022
+ * Its faces, kept to upload again after a lost context, undefined for one drawn by capture
2023
+ * @type {Array<Uint8Array|HTMLImageElement|HTMLCanvasElement|ImageBitmap|OffscreenCanvas>|undefined} */
2024
+ this.faces = faces;
2025
+ /** @property {Vector3|undefined} - Where it was last captured from, and is captured from again after a lost
2026
+ * context
2027
+ * @type {Vector3|undefined} */
2028
+ this.capturePos = undefined;
2029
+ /** @type {WebGLTexture|undefined} */
2030
+ this.glTexture = undefined; // made by the first pass that uses it
2031
+ this.contextGeneration = -1;
2032
+ }
2033
+
2034
+ /** Draw the scene around a point into a cube map made with no faces, in the next frame's 3D pass
2035
+ * - Every object of the layer render3D.renderAfter2D picks is drawn, with the sky and the render callbacks; the
2036
+ * camera's near and far planes, the lights and the fog are the scene's
2037
+ * - It is six more draws of the scene, so capture once for a still scene, or every few frames for a moving one
2038
+ * - A closed mesh around the point, like the ball that reflects it, is not seen from inside, so it hides nothing
2039
+ * - While it is drawn, what would reflect it reflects the sky's colors in its place
2040
+ * - It is seen from one point, so it suits what is near that point best, give each mirror its own with
2041
+ * obj.environment
2042
+ * @param {Vector3} pos3D - The point it is seen from */
2043
+ capture(pos3D)
2044
+ {
2045
+ ASSERT(!this.faces, 'only a cube map made with no faces can capture the scene');
2046
+ ASSERT(isVector3(pos3D), 'capture needs a Vector3');
2047
+ this.capturePos = pos3D.copy();
2048
+ render3D.cubeCaptures.add(this);
2049
+ }
2050
+
2051
+ /** Free its texture, which is made again if it is used after */
2052
+ dispose()
2053
+ {
2054
+ this.glTexture && glContext?.deleteTexture(this.glTexture);
2055
+ this.glTexture = undefined;
2056
+ render3D?.cubeCaptures.delete(this);
2057
+ }
2058
+ }
2059
+
2060
+ /** Make a cube map by asking a function the color of each direction
2061
+ * - The function gets a unit Vector3 for each pixel of each face and gives a Color
2062
+ * - It runs size * size * 6 times, so a size of 64 or 128 makes one at once, larger takes a moment
2063
+ * @param {number} size - Pixels a side of each face
2064
+ * @param {function(Vector3): Color} colorOf - The color seen looking that way
2065
+ * @return {CubeMap}
2066
+ * @example
2067
+ * // a sky, blue above and pale at the horizon, with a sun
2068
+ * const sun = vec3(1, 1, -1).normalize();
2069
+ * render3D.environment = makeCubeMap(64, (d)=> hsl(.6, .7, .9 - max(d.y, 0) * .5).lerp(WHITE, max(0, d.dot(sun)) ** 64));
2070
+ * @memberof Render3D */
2071
+ function makeCubeMap(size, colorOf)
2072
+ {
2073
+ ASSERT(isNumber(size) && size >= 1 && size % 1 === 0, 'makeCubeMap needs a whole size', size);
2074
+ ASSERT(typeof colorOf === 'function', 'makeCubeMap needs a function of direction');
2075
+ const faces = [];
2076
+ for (let face = 0; face < 6; ++face)
2077
+ {
2078
+ const data = new Uint8Array(size * size * 4);
2079
+ for (let y = 0; y < size; ++y)
2080
+ for (let x = 0; x < size; ++x)
2081
+ {
2082
+ const color = colorOf(render3DCubeDirection(face, (x + .5) / size * 2 - 1, (y + .5) / size * 2 - 1));
2083
+ const i = (x + y * size) * 4;
2084
+ data[i] = clamp(color.r) * 255 + .5 | 0;
2085
+ data[i+1] = clamp(color.g) * 255 + .5 | 0;
2086
+ data[i+2] = clamp(color.b) * 255 + .5 | 0;
2087
+ data[i+3] = clamp(color.a) * 255 + .5 | 0;
2088
+ }
2089
+ faces.push(data);
2090
+ }
2091
+ return new CubeMap(size, faces);
2092
+ }
2093
+
2094
+ /** Load a cube map from six square images of one size
2095
+ * - The images are a sky box set as three.js's CubeTextureLoader takes them, and show the same: each face seen from
2096
+ * inside as its image is drawn
2097
+ * @param {Array<string>} sources - The images of +x, -x, +y, -y, +z and -z, as a sky box set names them right,
2098
+ * left, top, bottom, front and back
2099
+ * @return {Promise<CubeMap>}
2100
+ * @memberof Render3D */
2101
+ async function loadCubeMap(sources)
2102
+ {
2103
+ ASSERT(isArray(sources) && sources.length === 6, 'loadCubeMap takes six images, +x, -x, +y, -y, +z and -z');
2104
+ const images = await Promise.all(sources.map((src)=> new Promise((resolve, reject)=>
2105
+ {
2106
+ const image = new Image;
2107
+ image.crossOrigin = 'anonymous';
2108
+ image.onload = ()=> resolve(image);
2109
+ image.onerror = ()=> reject(new Error('loadCubeMap could not load ' + src));
2110
+ image.src = src;
2111
+ })));
2112
+ const size = images[0].width;
2113
+ if (!images.every((image)=> image.width === size && image.height === size))
2114
+ throw new Error('loadCubeMap needs six square images of one size');
2115
+ // a sky box set is drawn for WebGL's cube, which is left handed: in this right handed world each face would show
2116
+ // mirrored, so as three.js does each is drawn mirrored, and +x and -x trade places, and every face is then seen
2117
+ // from inside as its image shows
2118
+ const faces = [1, 0, 2, 3, 4, 5].map((i)=>
2119
+ {
2120
+ const context = createCanvasContext(size);
2121
+ context.scale(-1, 1);
2122
+ context.drawImage(images[i], -size, 0);
2123
+ return context.canvas;
2124
+ });
2125
+ return new CubeMap(size, faces);
2126
+ }
2127
+
2128
+ // the direction of a point on a cube map's face, s and t from -1 to 1 across and down the face as WebGL lays it out
2129
+ function render3DCubeDirection(face, s, t)
2130
+ {
2131
+ const d = face === 0 ? vec3(1, -t, -s) : face === 1 ? vec3(-1, -t, s) : face === 2 ? vec3(s, 1, t) :
2132
+ face === 3 ? vec3(s, -1, -t) : face === 4 ? vec3(s, -t, 1) : vec3(-s, -t, -1);
2133
+ return d.normalize();
2134
+ }
2135
+
2136
+ // a cube map's texture, uploaded with its mipmaps the first time and again after a lost context, bound to the
2137
+ // cube map target of the active unit; undefined with no cube map or no WebGL
2138
+ function render3DCubeTexture(cube)
2139
+ {
2140
+ const gl = glContext;
2141
+ if (!cube || !gl) return;
2142
+ if (cube.glTexture && cube.contextGeneration === render3D.contextGeneration)
2143
+ return cube.glTexture;
2144
+ const texture = cube.glTexture = gl.createTexture();
2145
+ cube.contextGeneration = render3D.contextGeneration;
2146
+ gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture);
2147
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); // a face's first row is its top, as the cube's faces are laid out
2148
+ if (!cube.faces)
2149
+ {
2150
+ // drawn by a capture: empty faces, black until the capture, which happens again when the context was lost;
2151
+ // a size the device can not make falls back to the largest it can, as the shadow map's does
2152
+ cube.size = min(cube.size, gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE),
2153
+ gl.getParameter(gl.MAX_RENDERBUFFER_SIZE));
2154
+ for (let i = 0; i < 6; ++i)
2155
+ gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, gl.RGBA, cube.size, cube.size, 0, gl.RGBA,
2156
+ gl.UNSIGNED_BYTE, null);
2157
+ cube.capturePos && render3D.cubeCaptures.add(cube);
2158
+ }
2159
+ else cube.faces.forEach((face, i)=> face instanceof Uint8Array ?
2160
+ gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, gl.RGBA, cube.size, cube.size, 0, gl.RGBA,
2161
+ gl.UNSIGNED_BYTE, face) :
2162
+ gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, face));
2163
+ gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
2164
+ gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
2165
+ gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
2166
+ gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
2167
+ gl.generateMipmap(gl.TEXTURE_CUBE_MAP); // the blur of a rough surface's reflection
2168
+ return texture;
2169
+ }
2170
+
2171
+ // the transform of a camera at pos looking out through a cube map face, 90 degrees across: right, up and forward are
2172
+ // the face's directions toward its right edge, toward its last row and through its middle, from the same table
2173
+ // render3DCubeDirection reads, so a captured face lays out as an uploaded one; up is down the face, as a
2174
+ // framebuffer's rows go up
2175
+ function render3DCubeFaceMatrix(face, pos)
2176
+ {
2177
+ const [right, up, forward] = [
2178
+ [vec3(0, 0, -1), vec3(0, -1, 0), vec3(1, 0, 0)],
2179
+ [vec3(0, 0, 1), vec3(0, -1, 0), vec3(-1, 0, 0)],
2180
+ [vec3(1, 0, 0), vec3(0, 0, 1), vec3(0, 1, 0)],
2181
+ [vec3(1, 0, 0), vec3(0, 0, -1), vec3(0, -1, 0)],
2182
+ [vec3(1, 0, 0), vec3(0, -1, 0), vec3(0, 0, 1)],
2183
+ [vec3(-1, 0, 0), vec3(0, -1, 0), vec3(0, 0, -1)]][face];
2184
+ return new Matrix4([right.x, right.y, right.z, 0, up.x, up.y, up.z, 0,
2185
+ -forward.x, -forward.y, -forward.z, 0, pos.x, pos.y, pos.z, 1]);
2186
+ }
2187
+
2188
+ // draw the cube maps asked for by capture, each face a view of the layer's objects from its point, with the sky and
2189
+ // the render callbacks, into a framebuffer of its own; the camera, the matrices and the frame's viewport are put
2190
+ // back after, and the environment unit is bound again, since a texture can not be read while it is drawn
2191
+ function render3DCaptureCubes(objects)
2192
+ {
2193
+ const gl = glContext, r = render3D, camera = r.camera;
2194
+ const captures = [...r.cubeCaptures];
2195
+ r.cubeCaptures.clear();
2196
+ if (!r.captureFramebuffer)
2197
+ {
2198
+ r.captureFramebuffer = gl.createFramebuffer();
2199
+ r.captureDepth = gl.createRenderbuffer();
2200
+ r.captureDepthSize = 0;
2201
+ }
2202
+ // a camera looking out through each face in turn, its matrix the face's
2203
+ const view = new Camera3D;
2204
+ view.fov = PI / 2;
2205
+ view.near = camera.orthographic || !(camera.near > 0) ? .1 : camera.near; // an orthographic camera's may be behind it
2206
+ view.far = camera.far;
2207
+ let faceMatrix = new Matrix4;
2208
+ view.getMatrix = ()=> faceMatrix.copy();
2209
+ gl.bindFramebuffer(gl.FRAMEBUFFER, r.captureFramebuffer);
2210
+ try
2211
+ {
2212
+ for (const cube of captures)
2213
+ {
2214
+ const texture = render3DCubeTexture(cube), size = cube.size;
2215
+ r.cubeCaptures.delete(cube); // a texture made just now asks again, it is drawn here
2216
+ if (r.captureDepthSize !== size)
2217
+ {
2218
+ gl.bindRenderbuffer(gl.RENDERBUFFER, r.captureDepth);
2219
+ gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT24, size, size);
2220
+ gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, r.captureDepth);
2221
+ r.captureDepthSize = size;
2222
+ }
2223
+ // nothing reads the texture while it is drawn: the environment unit lets it go and the shader reflects
2224
+ // the sky's colors in its place, and it is not drawn as the sky box
2225
+ r.capturingCube = cube;
2226
+ gl.activeTexture(gl.TEXTURE0 + 5);
2227
+ gl.bindTexture(gl.TEXTURE_CUBE_MAP, null);
2228
+ gl.activeTexture(gl.TEXTURE0);
2229
+ render3DBoundEnvironment = null;
2230
+ gl.viewport(0, 0, size, size);
2231
+ view.pos = cube.capturePos;
2232
+ r.camera = view;
2233
+ for (let face = 0; face < 6; ++face)
2234
+ {
2235
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_CUBE_MAP_POSITIVE_X + face,
2236
+ texture, 0);
2237
+ ASSERT(gl.checkFramebufferStatus(gl.FRAMEBUFFER) == gl.FRAMEBUFFER_COMPLETE,
2238
+ 'cube map capture framebuffer is incomplete');
2239
+ faceMatrix = render3DCubeFaceMatrix(face, cube.capturePos);
2240
+ r.updateMatrices(1);
2241
+ if (face < 2 || face > 3)
2242
+ {
2243
+ // a side face's up is world down, as the cube lays its rows out, so sprites and particles, built
2244
+ // on the camera's right and up, would stand on their heads: half a turn about forward puts them
2245
+ // on world up, and leaves forward and the sort the same
2246
+ r.cameraRight = r.cameraRight.scale(-1);
2247
+ r.cameraUp = r.cameraUp.scale(-1);
2248
+ }
2249
+ const c = canvasClearColor;
2250
+ gl.clearColor(c.r, c.g, c.b, 1);
2251
+ gl.depthMask(true);
2252
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
2253
+ render3DUseProgram(r.program);
2254
+ r.renderStages(objects, true);
2255
+ }
2256
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, null, 0);
2257
+ gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture);
2258
+ gl.generateMipmap(gl.TEXTURE_CUBE_MAP); // the blur of a rough surface's reflection
2259
+ gl.bindTexture(gl.TEXTURE_CUBE_MAP, null);
2260
+ }
2261
+ }
2262
+ finally
2263
+ {
2264
+ r.capturingCube = undefined;
2265
+ r.camera = camera;
2266
+ r.updateMatrices();
2267
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
2268
+ gl.viewport(0, 0, glCanvas.width, glCanvas.height);
2269
+ gl.activeTexture(gl.TEXTURE0 + 5);
2270
+ gl.bindTexture(gl.TEXTURE_CUBE_MAP, render3DCubeTexture(r.environment) || null);
2271
+ gl.activeTexture(gl.TEXTURE0);
2272
+ render3DBoundEnvironment = r.environment || null;
2273
+ }
2274
+ }
2275
+
2276
+ // draw a cube map as the sky: one triangle over the whole screen, each pixel the cube's color the way it looks,
2277
+ // with its own program and an empty vertex array, the pass's state put back after; false when the program did not
2278
+ // build, in a release build, so the dome draws in its place
2279
+ function render3DDrawSkyBox(cube)
2280
+ {
2281
+ const gl = glContext, r = render3D;
2282
+ r.skyBoxProgram ||= glCreateProgram(
2283
+ '#version 300 es\nprecision highp float;out vec2 v;' +
2284
+ 'void main(){v=vec2(gl_VertexID&1,gl_VertexID>>1)*4.-1.;gl_Position=vec4(v,0,1);}',
2285
+ '#version 300 es\nprecision highp float;uniform mat4 inverseViewProj;uniform samplerCube sky;in vec2 v;' +
2286
+ 'out vec4 o;void main(){vec4 a=inverseViewProj*vec4(v,-1,1),b=inverseViewProj*vec4(v,0,1);' +
2287
+ 'o=vec4(texture(sky,b.xyz/b.w-a.xyz/a.w).rgb,1);}'); // the way through this pixel, near plane to beyond
2288
+ if (glFailedPrograms.has(r.skyBoxProgram)) return false;
2289
+ r.skyBoxVao ||= gl.createVertexArray();
2290
+ const depthTest = gl.isEnabled(gl.DEPTH_TEST), blend = gl.isEnabled(gl.BLEND), cull = gl.isEnabled(gl.CULL_FACE);
2291
+ const depthWrite = gl.getParameter(gl.DEPTH_WRITEMASK);
2292
+ gl.useProgram(r.skyBoxProgram);
2293
+ r.currentProgram = undefined; // the next draw picks its program again
2294
+ gl.activeTexture(gl.TEXTURE6);
2295
+ gl.bindTexture(gl.TEXTURE_CUBE_MAP, render3DCubeTexture(cube));
2296
+ gl.bindSampler(6, null);
2297
+ gl.uniform1i(glUniformLocation(r.skyBoxProgram, 'sky'), 6);
2298
+ // the view turned but not moved: the way through a pixel does not depend on where the camera is, and two points
2299
+ // near the origin keep their difference, where points a long way out lose it to float precision
2300
+ const turn = r.viewMatrix.copy();
2301
+ turn.m[12] = turn.m[13] = turn.m[14] = 0;
2302
+ gl.uniformMatrix4fv(glUniformLocation(r.skyBoxProgram, 'inverseViewProj'), false,
2303
+ r.projectionMatrix.copy().multiply(turn).invert().m);
2304
+ gl.disable(gl.DEPTH_TEST);
2305
+ gl.disable(gl.BLEND);
2306
+ gl.disable(gl.CULL_FACE);
2307
+ gl.depthMask(false);
2308
+ gl.bindVertexArray(r.skyBoxVao);
2309
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
2310
+ gl.bindVertexArray(r.vao);
2311
+ gl.bindTexture(gl.TEXTURE_CUBE_MAP, null);
2312
+ gl.activeTexture(gl.TEXTURE0);
2313
+ depthTest && gl.enable(gl.DEPTH_TEST);
2314
+ blend && gl.enable(gl.BLEND);
2315
+ cull && gl.enable(gl.CULL_FACE);
2316
+ gl.depthMask(depthWrite);
2317
+ return true;
2318
+ }
2319
+
1911
2320
  function render3DContextLost()
1912
2321
  {
1913
2322
  const r = render3D;
1914
2323
  r.program = r.currentProgram = r.shadowShader = r.vao = r.streamBuffer = r.whiteTexture = undefined;
2324
+ r.skyBoxProgram = r.skyBoxVao = r.captureFramebuffer = r.captureDepth = undefined;
2325
+ r.captureDepthSize = 0;
1915
2326
  for (const shader of glShaderObjects)
1916
2327
  shader.program3D = undefined; // compiled again by the next draw
1917
2328
  r.lightCount = 0;
@@ -2035,13 +2446,25 @@ function render3DSetMaterialUniforms(state)
2035
2446
  {
2036
2447
  const r = render3D, loaded = (map)=> render3DTextureOf(map)?.glTexture ? map : undefined;
2037
2448
  const normalMap = state.normalScale ? loaded(state.normalMap) : undefined, emissiveMap = loaded(state.emissiveMap);
2038
- render3DUniform4f('materialParams', normalMap ? state.normalScale : 0, state.shininess, state.reflectivity, 0);
2449
+ render3DUniform4f('materialParams', normalMap ? state.normalScale : 0, max(state.shininess, 1e-3), state.reflectivity, 0);
2039
2450
  const ec = state.emissiveMapColor || WHITE;
2040
2451
  emissiveMap ? render3DUniform4f('emissiveTint', ec.r, ec.g, ec.b, 1) : render3DUniform4f('emissiveTint', 0, 0, 0, 0);
2041
2452
  render3DBindMap(2, normalMap, state);
2042
2453
  render3DBindMap(3, emissiveMap, state);
2043
2454
  if (state.reflectivity > 0)
2044
2455
  {
2456
+ // the draw's own environment or the scene's, never the cube map a capture is drawing
2457
+ let environment = state.environmentMap || r.environment;
2458
+ environment === r.capturingCube && (environment = undefined);
2459
+ render3DBindEnvironment(environment);
2460
+ // a rough reflection is blurred over half of sqrt(2 / (shininess + 2)), the spread Blinn-Phong and Beckmann
2461
+ // give a shininess, narrower than the Phong highlight the shader draws, and a texel of mipmap
2462
+ // L spans (PI/2) / size * 2^L, so the shader reads level log2(size * .45) - log2(shininess + 2)
2463
+ // / 2: the same blur for any size of map; it stops at 4 by 4 a face, where sampling across the edges still
2464
+ // blends neighboring faces
2465
+ const size = environment ? environment.size : 1;
2466
+ // a turn of one radian a pixel covers size / (PI/2) texels of a face, log2 of that the level that blurs it
2467
+ render3DUniform4f('envParams', environment ? 1 : 0, max(0, log2(size) - 2), log2(size * .45), log2(size * 2 / PI));
2045
2468
  const sky = r.sky && render3DSkyColors.get(r.sky), a = r.ambientColor, g = r.ambientGroundColor || a;
2046
2469
  const top = sky ? sky[0] : a, bottom = sky ? sky[2] : g;
2047
2470
  render3DUniform4f('skyTop', top.r, top.g, top.b, 1);
@@ -2051,6 +2474,22 @@ function render3DSetMaterialUniforms(state)
2051
2474
  }
2052
2475
  }
2053
2476
 
2477
+ // the cube map bound to the environment unit, 5, so a draw binds only when it changes; null for none, undefined when
2478
+ // not known, forgotten at the start and end of each pass and around a capture
2479
+ let render3DBoundEnvironment;
2480
+
2481
+ // bind a draw's environment to unit 5 when it is not bound already; none leaves the unit as it is, its reflection
2482
+ // reads the sky's colors
2483
+ function render3DBindEnvironment(cube)
2484
+ {
2485
+ const gl = glContext;
2486
+ if (!cube || cube === render3DBoundEnvironment || !gl) return;
2487
+ gl.activeTexture(gl.TEXTURE0 + 5);
2488
+ gl.bindTexture(gl.TEXTURE_CUBE_MAP, render3DCubeTexture(cube) || null);
2489
+ gl.activeTexture(gl.TEXTURE0);
2490
+ render3DBoundEnvironment = cube;
2491
+ }
2492
+
2054
2493
  // what each material map unit has bound, index 2 and 3 the maps and 4 and 5 whether each is pixelated, forgotten at
2055
2494
  // the start and end of each pass
2056
2495
  let render3DBoundMaps = [];
@@ -2191,7 +2630,7 @@ function render3DSetDrawUniforms(matrix, tileInfo, tint, uvRect, state=render3D)
2191
2630
  // its alpha says whether the ground color is on
2192
2631
  gc ? render3DUniform4f('ambientGround', gc.r, gc.g, gc.b, 1) : render3DUniform4f('ambientGround', 0, 0, 0, 0);
2193
2632
 
2194
- render3DUniform4f('fogColor', fc.r, fc.g, fc.b, r.fogStart);
2633
+ render3DUniform4f('fogColor', fc.r, fc.g, fc.b, r.fogEnd ? min(r.fogStart, r.fogEnd - 1e-3) : r.fogStart);
2195
2634
  // how the fragment shader finishes: 1 drops see through texels and keeps the draw opaque,
2196
2635
  // 0 blends them away instead, and -1 is additive, which has to fade into fog differently
2197
2636
  const blendMode = state.blend ? (state.additive ? -1 : 0) : 1;
@@ -2312,6 +2751,8 @@ function render3DRenderPass(after2D)
2312
2751
  }
2313
2752
  // the camera's depth for post processing, from the default layer
2314
2753
  r.depthTexture && isDefault && render3DRenderDepth();
2754
+ // the cube maps asked for by capture, drawn with this frame's shadow map
2755
+ isDefault && r.cubeCaptures.size && render3DCaptureCubes(objects);
2315
2756
  render3DUseProgram(r.program); // after the shadow map, so the light matrix it sends is this frame's
2316
2757
  r.renderStages(objects, isDefault);
2317
2758
  }
@@ -2347,6 +2788,11 @@ function render3DSetMapUnits(texture)
2347
2788
  gl.bindTexture(gl.TEXTURE_2D, texture);
2348
2789
  gl.bindSampler(unit, null);
2349
2790
  }
2791
+ // the environment cube on unit 5 for the pass, emptied at its end with the others
2792
+ gl.activeTexture(gl.TEXTURE0 + 5);
2793
+ gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture && render3DCubeTexture(render3D.environment) || null);
2794
+ gl.bindSampler(5, null);
2795
+ render3DBoundEnvironment = texture && render3D.environment || null;
2350
2796
  gl.activeTexture(gl.TEXTURE0);
2351
2797
  render3DBoundMaps = [];
2352
2798
  }
@@ -2408,7 +2854,7 @@ function render3DRenderShadowMap()
2408
2854
  {
2409
2855
  // see through objects cast only when textured, their alpha cuts the shadow out
2410
2856
  const casters = render3DLayerObjects(!!r.renderAfter2D).filter(o=>
2411
- o.castShadow && !o.additive && (!o.transparent || o.tileInfo));
2857
+ render3DSetting(o, 'castShadow') && !render3DSetting(o, 'additive') && (!o.transparent || o.tileInfo));
2412
2858
  render3DDrawObjects(casters);
2413
2859
  r.onRenderOpaque?.();
2414
2860
  r.flush();
@@ -2473,7 +2919,8 @@ function render3DRenderDepth()
2473
2919
  r.shadowPass = r.depthPass = true;
2474
2920
  try
2475
2921
  {
2476
- const solids = render3DLayerObjects(!!r.renderAfter2D).filter(o=> !o.additive && (!o.transparent || o.tileInfo));
2922
+ const solids = render3DLayerObjects(!!r.renderAfter2D).filter(o=>
2923
+ !render3DSetting(o, 'additive') && (!o.transparent || o.tileInfo));
2477
2924
  render3DDrawObjects(solids);
2478
2925
  r.onRenderOpaque?.();
2479
2926
  r.flush();
@@ -2583,2348 +3030,3 @@ function render3DBillboardCorners(pos, size, angle, upright)
2583
3030
  vec3(pos.x - rx + ux, pos.y - ry + uy, pos.z - rz + uz), vec3(pos.x - rx - ux, pos.y - ry - uy, pos.z - rz - uz),
2584
3031
  vec3(pos.x + rx + ux, pos.y + ry + uy, pos.z + rz + uz), vec3(pos.x + rx - ux, pos.y + ry - uy, pos.z + rz - uz)];
2585
3032
  }
2586
-
2587
- // how many vertices a strip of n points takes with its repeats, and which point vertex k of it is
2588
- function render3DStripCount(n) { return n + 2 + (n & 1); }
2589
- function render3DStripIndex(k, n) { return k < 1 ? 0 : k <= n ? k - 1 : n - 1; }
2590
-
2591
- // walk a strip's vertices with the repeats applied, calling back with (point, normal, uv, color)
2592
- // normals, uvs and colors may be one value for all points, an array per point, or undefined
2593
- function render3DForEachStripVertex(points, normals, uvs, colors, callback)
2594
- {
2595
- ASSERT(isArray(points) && points.length > 2, 'strip needs at least 3 points');
2596
- const n = points.length, count = render3DStripCount(n);
2597
- const normalArray = isArray(normals), uvArray = isArray(uvs), colorArray = isArray(colors);
2598
- for (let k = 0; k < count; ++k)
2599
- {
2600
- const i = render3DStripIndex(k, n);
2601
- callback(points[i],
2602
- normalArray ? normals[i] : normals || RENDER3D_DEFAULT_NORMAL,
2603
- uvArray ? uvs[i] : uvs || RENDER3D_DEFAULT_UV,
2604
- colorArray ? colors[i] : colors || WHITE);
2605
- }
2606
- }
2607
-
2608
- // write one vertex into a packed buffer at float index j
2609
- function render3DWriteVertex(floats, ints, j, x, y, z, n, u, v, rgba)
2610
- {
2611
- floats[j] = x; floats[j+1] = y; floats[j+2] = z;
2612
- floats[j+3] = n.x; floats[j+4] = n.y; floats[j+5] = n.z;
2613
- floats[j+6] = u; floats[j+7] = v;
2614
- ints[j+8] = rgba;
2615
- }
2616
-
2617
- // turn every triangle of an index list the other way round, in place: the authoring form reads counter clockwise
2618
- // from the front and the pass draws clockwise, as a strip's triangles come out after its leading repeat
2619
- function render3DFlipTriangles(indices)
2620
- {
2621
- for (let t = 0; t < indices.length; t += 3)
2622
- {
2623
- const b = indices[t+1];
2624
- indices[t+1] = indices[t+2], indices[t+2] = b;
2625
- }
2626
- return indices;
2627
- }
2628
-
2629
- // the triangles of a strip of count entries as an index list, given which vertex each entry maps to and which place
2630
- // it is at: the strip's triangle i is (i-2, i-1, i), its odd ones read the other way as the GPU reads a strip, and a
2631
- // triangle with two corners in one place has no area, it is a join between pieces or a sliver at a pole, and is left out
2632
- function render3DStripTriangles(count, remap, place)
2633
- {
2634
- const indices = [];
2635
- for (let i = 2; i < count; ++i)
2636
- {
2637
- const a = i & 1 ? i - 1 : i - 2, b = i & 1 ? i - 2 : i - 1;
2638
- if (place[a] != place[b] && place[b] != place[i] && place[a] != place[i])
2639
- indices.push(remap[a], remap[b], remap[i]);
2640
- }
2641
- return indices;
2642
- }
2643
-
2644
- // a mesh's packed vertex data for the GPU, one vertex per entry of a layout, which is the strip index of each,
2645
- // written into data when given, the buffer an earlier call returned for the same layout; it measures the mesh's
2646
- // radius and box as it goes, as computeRadius does, over the vertices once instead of every strip entry
2647
- function render3DMeshVertexData(mesh, vertices, data=new ArrayBuffer(vertices.length * RENDER3D_VERTEX_BYTES))
2648
- {
2649
- const count = vertices.length;
2650
- const floats = new Float32Array(data), ints = new Uint32Array(data);
2651
- let r = 0, x0 = Infinity, y0 = Infinity, z0 = Infinity, x1 = -Infinity, y1 = -Infinity, z1 = -Infinity;
2652
- for (let j = 0; j < count; ++j)
2653
- {
2654
- // a hand built mesh may leave normals, uvs and colors empty
2655
- const i = vertices[j], p = mesh.points[i], uv = mesh.uvs[i] || RENDER3D_DEFAULT_UV;
2656
- const x = p.x, y = p.y, z = p.z;
2657
- render3DWriteVertex(floats, ints, j * RENDER3D_VERTEX_FLOATS, x, y, z,
2658
- mesh.normals[i] || RENDER3D_DEFAULT_NORMAL, uv.x, uv.y, (mesh.colors[i] || WHITE).rgbaInt());
2659
- r = max(r, x*x + y*y + z*z);
2660
- x0 = min(x0, x), y0 = min(y0, y), z0 = min(z0, z);
2661
- x1 = max(x1, x), y1 = max(y1, y), z1 = max(z1, z);
2662
- }
2663
- mesh.radius = r ** .5;
2664
- mesh.bounds = count ? {min: vec3(x0, y0, z0), max: vec3(x1, y1, z1)} : {min: vec3(), max: vec3()};
2665
- return data;
2666
- }
2667
-
2668
- // reorder a convex polygon's points, counter clockwise from outside, into one triangle strip
2669
- function render3DPolygonStrip(points)
2670
- {
2671
- const strip = [points[0]];
2672
- for (let i = 1, j = points.length - 1; i <= j; ++i, --j)
2673
- {
2674
- strip.push(points[i]);
2675
- if (i !== j)
2676
- strip.push(points[j]);
2677
- }
2678
- return strip;
2679
- }
2680
-
2681
- // turn every triangle of a mesh the other way round, the normals left alone: an index list reads each one in the
2682
- // other order, and a strip gets one extra point at each end, which flips every triangle and keeps the count even
2683
- function render3DFlipWinding(mesh)
2684
- {
2685
- if (mesh.indices)
2686
- return void render3DFlipTriangles(mesh.indices);
2687
- for (const key of ['points', 'normals', 'uvs', 'colors'])
2688
- {
2689
- const a = mesh[key];
2690
- if (a.length)
2691
- a.unshift(a[0]), a.push(a[a.length - 1]);
2692
- }
2693
- mesh.vertexKeys = undefined;
2694
- }
2695
-
2696
- ///////////////////////////////////////////////////////////////////////////////
2697
-
2698
- // frees the GPU buffer of a mesh that is garbage collected without dispose, some time after it goes; it holds the
2699
- // buffer and its context, never the mesh, or the mesh could not be collected, and dispose unregisters the mesh
2700
- const render3DMeshBuffers = typeof FinalizationRegistry == 'undefined' ? undefined :
2701
- new FinalizationRegistry(({buffer, indexBuffer, generation})=>
2702
- {
2703
- if (generation !== render3D?.contextGeneration || !glContext) return;
2704
- glContext.deleteBuffer(buffer);
2705
- glContext.deleteBuffer(indexBuffer);
2706
- });
2707
-
2708
- // the points at one place, to a hundred thousandth, which smooth normals are summed by: each point's group, numbered
2709
- // from 0, found by a number made from its place and checked against the place, so no text is made for a point
2710
- function render3DPlaceGroups(points)
2711
- {
2712
- const groups = new Int32Array(points.length), first = new Map, next = [], places = [];
2713
- for (let i = 0; i < points.length; ++i)
2714
- {
2715
- const p = points[i], x = round(p.x * 1e5), y = round(p.y * 1e5), z = round(p.z * 1e5);
2716
- const hash = Math.imul(x, 73856093) ^ Math.imul(y, 19349663) ^ Math.imul(z, 83492791);
2717
- let group = first.get(hash);
2718
- while (group !== undefined && !(places[group*3] === x && places[group*3+1] === y && places[group*3+2] === z))
2719
- group = next[group];
2720
- if (group === undefined)
2721
- {
2722
- group = next.length;
2723
- next.push(first.get(hash));
2724
- first.set(hash, group);
2725
- places.push(x, y, z);
2726
- }
2727
- groups[i] = group;
2728
- }
2729
- return {groups, count: next.length};
2730
- }
2731
-
2732
- // the face normals meeting at each place added up, each weighted by its corner angle so a cube corner averages its
2733
- // three faces evenly however they are cut into triangles; of a strip, every other triangle turned back, with no
2734
- // indices, or of the index list; in numbers, with no vector made for a triangle or a corner
2735
- function render3DSmoothNormalSums(points, indices)
2736
- {
2737
- const strip = !indices, triangles = strip ? max(points.length - 2, 0) : indices.length / 3 | 0;
2738
- const {groups, count} = render3DPlaceGroups(points);
2739
- const sums = new Float64Array(count * 3), touched = new Uint8Array(count);
2740
- let nx = 0, ny = 0, nz = 0;
2741
- const addCorner = (a, b, c, group)=>
2742
- {
2743
- // the angle at corner a, then the face normal scaled by it added to the sum at a's place
2744
- const ux = b.x - a.x, uy = b.y - a.y, uz = b.z - a.z, vx = c.x - a.x, vy = c.y - a.y, vz = c.z - a.z;
2745
- const dot = ux*vx + uy*vy + uz*vz, lengths = (ux**2 + uy**2 + uz**2)**.5 * (vx**2 + vy**2 + vz**2)**.5;
2746
- const angle = Math.acos(clamp(dot / (lengths || 1), -1, 1)), s = group * 3;
2747
- sums[s] += nx * angle, sums[s+1] += ny * angle, sums[s+2] += nz * angle;
2748
- touched[group] = 1;
2749
- };
2750
- for (let t = 0; t < triangles; ++t)
2751
- {
2752
- const i0 = strip ? t : indices[t*3], i1 = strip ? t + 1 : indices[t*3+1], i2 = strip ? t + 2 : indices[t*3+2];
2753
- const a = points[i0], b = points[i1], c = points[i2];
2754
- const ux = b.x - a.x, uy = b.y - a.y, uz = b.z - a.z, vx = c.x - a.x, vy = c.y - a.y, vz = c.z - a.z;
2755
- const cx = uy*vz - uz*vy, cy = uz*vx - ux*vz, cz = ux*vy - uy*vx;
2756
- const lengthSquared = cx**2 + cy**2 + cz**2;
2757
- if (!lengthSquared) continue; // no area, like a triangle joining two strips
2758
- const k = (strip && !(t & 1) ? -1 : 1) / lengthSquared**.5;
2759
- nx = cx * k, ny = cy * k, nz = cz * k;
2760
- addCorner(a, b, c, groups[i0]);
2761
- addCorner(b, c, a, groups[i1]);
2762
- addCorner(c, a, b, groups[i2]);
2763
- }
2764
- return {groups, sums, touched};
2765
- }
2766
-
2767
- /**
2768
- * Mesh - Triangles with positions, normals, uvs and colors, uploaded once and drawn by matrix
2769
- * - Build with addStrip, addQuad, combine or the shape builders, then render each frame
2770
- * - Its back faces are skipped unless doubleSided is set, which the open builders like buildGrid do for you
2771
- * - Two forms: a triangle strip, what the builders make, or an indexed list of triangles over their own vertices,
2772
- * what addTriangles and the model loaders make; upload sends the GPU an indexed list either way, see getTriangles,
2773
- * so a strip's joins between its pieces cost nothing to draw, and toIndexed turns a strip mesh into the list form
2774
- * - The GPU buffer is created lazily on first render and dropped by dispose, or freed once the mesh is garbage
2775
- * collected, so dispose is only needed to free it right away, like for a mesh rebuilt often
2776
- * @memberof Render3D
2777
- * @example
2778
- * const mesh = buildLathe([[0, -1], [1, 0], [0, 1]], 4); // octahedron
2779
- * mesh.render(buildMatrix(vec3(0, 1, 0)), undefined, RED);
2780
- */
2781
- class Mesh
2782
- {
2783
- /** Create an empty mesh */
2784
- constructor()
2785
- {
2786
- /** @property {Array<Vector3>} - Vertex positions, in strip order or one per vertex of an indexed mesh
2787
- * @type {Array<Vector3>} */
2788
- this.points = [];
2789
- /** @property {Array<Vector3>} - Vertex normals
2790
- * @type {Array<Vector3>} */
2791
- this.normals = [];
2792
- /** @property {Array<Vector2>} - Vertex texture coords, 0-1 across the tile
2793
- * @type {Array<Vector2>} */
2794
- this.uvs = [];
2795
- /** @property {Array<Color>} - Vertex colors
2796
- * @type {Array<Color>} */
2797
- this.colors = [];
2798
- /** @property {WebGLBuffer|undefined} - GPU vertex buffer, created by upload
2799
- * @type {WebGLBuffer|undefined} */
2800
- this.buffer = undefined;
2801
- /** @property {WebGLBuffer|undefined} - GPU index buffer, the triangles, created by upload
2802
- * @type {WebGLBuffer|undefined} */
2803
- this.indexBuffer = undefined;
2804
- /** @property {number} - Indices in the GPU index buffer, three per triangle */
2805
- this.bufferCount = 0;
2806
- this.indexType = 0; // gl.UNSIGNED_SHORT, or UNSIGNED_INT past 65535 vertices
2807
- /** @property {boolean} - The mesh changed and needs uploading again, set it yourself if you edit the arrays */
2808
- this.dirty = false;
2809
- /** @property {boolean|undefined} - Draw every use of this mesh in the opaque stage as one instanced call,
2810
- * undefined follows render3D.instancing
2811
- * @type {boolean|undefined} */
2812
- this.instanced = undefined;
2813
- /** @property {boolean} - Draw both sides, each lit as the side that is seen; off skips the faces pointing away,
2814
- * which is faster and right for closed shapes, the open builders like buildGrid and buildRibbon turn it on */
2815
- this.doubleSided = false;
2816
- /** @property {boolean} - The values change often but the shape never does, for a water surface or a cloth: set
2817
- * once, the mesh keeps its GPU layout and a dirty upload only rewrites the vertices into the buffer it has;
2818
- * the strip must keep the same points in the same order, a new point count asserts; the layout is decided by
2819
- * the first upload, so strip entries equal then stay one vertex and triangles with no area then stay dropped,
2820
- * set vertexKeys or give it distinct values at the start, not a flat grid of one color or points all in one place */
2821
- this.dynamicDraw = false;
2822
- /** @property {Array<number>|undefined} - The mesh as an indexed triangle list instead of a strip: the arrays
2823
- * hold each vertex once and this says how they join, three vertex numbers per triangle, counter clockwise seen
2824
- * from the front like a strip's first triangle; addTriangles and the loaders fill it, toIndexed turns a strip
2825
- * mesh into this form
2826
- * @type {Array<number>|undefined} */
2827
- this.indices = undefined;
2828
- /** @property {Int32Array|undefined} - Which strip entries are one vertex, set by a builder that knows, one
2829
- * whole number per entry with equal numbers meaning the same vertex; upload skips its search for them, then
2830
- * drops the keys, since an edit after that may tell the entries apart; adding geometry or recomputing normals
2831
- * drops them too
2832
- * @type {Int32Array|undefined} */
2833
- this.vertexKeys = undefined;
2834
- /** @type {{vertices: Array<number>, pointCount: number, data: ArrayBuffer}|undefined} */
2835
- // the strip index of each GPU vertex, the point count and packed data of the last upload, for a dynamicDraw mesh
2836
- this.vertexLayout = undefined;
2837
- this.instanceCount = 0; // draws waiting in this mesh's batch, with their values, texture and draw state
2838
- /** @type {Float32Array|undefined} */
2839
- this.instanceData = undefined;
2840
- /** @property {number} - Bounding sphere radius around the origin, for culling and picking, computed by upload */
2841
- this.radius = 0;
2842
- /** @property {{min: Vector3, max: Vector3}|undefined} - Bounding box, for picking, measured with the radius
2843
- * @type {{min: Vector3, max: Vector3}|undefined} */
2844
- this.bounds = undefined;
2845
- this.contextGeneration = 0; // the context the buffer belongs to, see render3D.contextGeneration
2846
- }
2847
-
2848
- /** Number of vertices in the mesh
2849
- * @return {number} */
2850
- get vertexCount() { return this.points.length; }
2851
-
2852
- /** Add a triangle strip, joined to the previous one by invisible flat triangles so one mesh holds many strips
2853
- * - Strip order: the first three points make a triangle, then each point makes another with the two before it
2854
- * - List the first three points counter clockwise as seen from the front, or the face points away
2855
- * and may vanish when back faces are culled
2856
- * @param {Array<Vector3>} points - Strip order
2857
- * @param {Vector3|Array<Vector3>} [normals] - One for all or one per point, default up
2858
- * @param {Vector2|Array<Vector2>} [uvs] - One for all or one per point, default zero
2859
- * @param {Color|Array<Color>} [colors] - One for all or one per point, default white
2860
- * @return {Mesh} */
2861
- addStrip(points, normals, uvs, colors)
2862
- {
2863
- if (this.indices)
2864
- {
2865
- // an indexed mesh takes the strip as the triangles it makes
2866
- const part = new Mesh().addStrip(points, normals, uvs, colors).toIndexed();
2867
- return this.addTriangles(part.points, part.indices, part.normals, part.uvs, part.colors);
2868
- }
2869
- render3DForEachStripVertex(points, normals, uvs, colors, (p, n, uv, c)=>
2870
- {
2871
- this.points.push(p);
2872
- this.normals.push(n);
2873
- this.uvs.push(uv);
2874
- this.colors.push(c);
2875
- });
2876
- this.vertexKeys = undefined; // the new entries have no keys
2877
- this.dirty = true;
2878
- return this;
2879
- }
2880
-
2881
- /** Add triangles over their own vertices, the indexed form a model file comes in
2882
- * - The mesh becomes indexed: a strip mesh is turned into triangles first, and strips added later join as triangles
2883
- * - List each triangle counter clockwise as seen from the front, like a strip's first triangle
2884
- * @param {Array<Vector3>} points - Each vertex once
2885
- * @param {Array<number>} indices - Three vertex numbers per triangle, into points
2886
- * @param {Vector3|Array<Vector3>} [normals] - One for all or one per point, default up
2887
- * @param {Vector2|Array<Vector2>} [uvs] - One for all or one per point, default zero
2888
- * @param {Color|Array<Color>} [colors] - One for all or one per point, default white
2889
- * @return {Mesh} */
2890
- addTriangles(points, indices, normals, uvs, colors)
2891
- {
2892
- ASSERT(isArray(points) && isArray(indices) && indices.length % 3 === 0,
2893
- 'addTriangles takes points and three indices per triangle');
2894
- ASSERT(indices.every(i=> i >= 0 && i < points.length && i % 1 === 0), 'an index points past the vertices given');
2895
- this.toIndexed();
2896
- const offset = this.points.length, normalArray = isArray(normals), uvArray = isArray(uvs), colorArray = isArray(colors);
2897
- for (let i = 0; i < points.length; ++i)
2898
- {
2899
- this.points.push(points[i]);
2900
- this.normals.push(normalArray ? normals[i] : normals || RENDER3D_DEFAULT_NORMAL);
2901
- this.uvs.push(uvArray ? uvs[i] : uvs || RENDER3D_DEFAULT_UV);
2902
- this.colors.push(colorArray ? colors[i] : colors || WHITE);
2903
- }
2904
- for (const i of indices)
2905
- this.indices.push(i + offset);
2906
- this.dirty = true;
2907
- return this;
2908
- }
2909
-
2910
- /** Turn a strip mesh into the indexed form, each distinct vertex once and the real triangles over them, in place
2911
- * - An indexed mesh is left as it is; the builders make strips and a loader makes this, and either draws the same
2912
- * @return {Mesh} */
2913
- toIndexed()
2914
- {
2915
- if (this.indices) return this;
2916
- const {vertices, indices} = this.getTriangles();
2917
- this.points = vertices.map(i=> this.points[i]);
2918
- this.normals = vertices.map(i=> this.normals[i] || RENDER3D_DEFAULT_NORMAL);
2919
- this.uvs = vertices.map(i=> this.uvs[i] || RENDER3D_DEFAULT_UV);
2920
- this.colors = vertices.map(i=> this.colors[i] || WHITE);
2921
- // the list upload sends reads clockwise, the pass draws it that way; the authoring form is counter clockwise
2922
- this.indices = render3DFlipTriangles(indices);
2923
- this.vertexKeys = undefined;
2924
- this.dirty = true;
2925
- return this;
2926
- }
2927
-
2928
- /** Add a flat quad from four corners in loop order, counter clockwise seen from the front, a is the top left of the texture
2929
- * @param {Vector3} a
2930
- * @param {Vector3} b
2931
- * @param {Vector3} c
2932
- * @param {Vector3} d
2933
- * @param {Color|Array<Color>} [color] - One for all or one per corner
2934
- * @param {Array<Vector2>} [uvs] - One per corner, default across the tile
2935
- * @return {Mesh} */
2936
- addQuad(a, b, c, d, color, uvs)
2937
- {
2938
- return this.addStrip(render3DQuadStrip(a, b, c, d), render3DFaceNormal(a, b, c, d),
2939
- uvs ? render3DQuadValues(uvs) : RENDER3D_QUAD_UVS, render3DQuadValues(color));
2940
- }
2941
-
2942
- /** Append another mesh transformed by a matrix, for building one shape out of several
2943
- * @param {Mesh} mesh
2944
- * @param {Matrix4|Vector3} [matrix] - Transform, or just a position to move it to
2945
- * @param {Color} [color] - Multiplies the appended vertex colors
2946
- * @return {Mesh} */
2947
- combine(mesh, matrix=RENDER3D_IDENTITY, color=WHITE)
2948
- {
2949
- matrix = render3DMatrix(matrix); // most parts only need moving into place
2950
- const normalMatrix = render3DNormalMatrix(matrix), mirrors = render3DMirrors(matrix);
2951
- let part = mesh, order;
2952
- if (this.indices || mesh.indices)
2953
- {
2954
- // one of them is indexed, so both are: the part as a copy if it is a strip
2955
- this.toIndexed();
2956
- part = mesh.indices ? mesh : new Mesh().combine(mesh).toIndexed();
2957
- const offset = this.points.length, indices = part.indices;
2958
- // the count read once, a mesh combined with itself grows as it is read; a mirror turns every triangle the
2959
- // other way round
2960
- for (let t = 0, n = indices.length; t < n; t += 3)
2961
- this.indices.push(indices[t] + offset, indices[t + (mirrors ? 2 : 1)] + offset,
2962
- indices[t + (mirrors ? 1 : 2)] + offset);
2963
- }
2964
- else if (mirrors && part.points.length)
2965
- {
2966
- // a strip reads the other way round with one more point at each end, as render3DFlipWinding does
2967
- const last = part.points.length - 1;
2968
- order = [0, ...part.points.keys(), last];
2969
- }
2970
- for (let j = 0, count = order ? order.length : part.points.length; j < count; ++j)
2971
- {
2972
- const i = order ? order[j] : j;
2973
- this.points.push(matrix.transformPoint(part.points[i]));
2974
- this.normals.push(normalMatrix.transformDirection(part.normals[i] || RENDER3D_DEFAULT_NORMAL).normalize());
2975
- this.uvs.push((part.uvs[i] || RENDER3D_DEFAULT_UV).copy());
2976
- this.colors.push((part.colors[i] || WHITE).multiply(color));
2977
- }
2978
- this.doubleSided ||= mesh.doubleSided; // an open part leaves the whole mesh open
2979
- this.vertexKeys = undefined; // the new entries have no keys
2980
- this.dirty = true;
2981
- return this;
2982
- }
2983
-
2984
- /** Returns a new mesh: this one and its mirror image across the plane through the origin facing axis
2985
- * - For modeling half a shape against that plane, a part that crosses it overlaps its image
2986
- * @param {Vector3} [axis] - Faces the mirror plane, vec3(1,0,0) mirrors across x
2987
- * @return {Mesh} */
2988
- mirror(axis=vec3(1, 0, 0))
2989
- {
2990
- ASSERT(isVector3(axis) && axis.lengthSquared() > 0, 'mirror needs an axis');
2991
- const n = axis.normalize(), a = -2 * n.x, b = -2 * n.y, c = -2 * n.z;
2992
- const reflect = new Matrix4([1 + a * n.x, b * n.x, c * n.x, 0, a * n.y, 1 + b * n.y, c * n.y, 0,
2993
- a * n.z, b * n.z, 1 + c * n.z, 0, 0, 0, 0, 1]);
2994
- return new Mesh().combine(this).combine(this, reflect); // combine turns the image's faces the right way out
2995
- }
2996
-
2997
- /** Returns a new mesh of count copies of this one, each turned further around an axis through the origin
2998
- * @param {number} count - Copies, spaced evenly around the whole turn
2999
- * @param {Vector3} [axis] - Up by default
3000
- * @return {Mesh} */
3001
- spin(count, axis=vec3(0, 1, 0))
3002
- {
3003
- ASSERT(count >= 1 && count % 1 === 0, 'spin count must be a whole number, 1 or more');
3004
- ASSERT(isVector3(axis) && axis.lengthSquared() > 0, 'spin needs an axis');
3005
- const mesh = new Mesh, unit = axis.normalize();
3006
- for (let i = 0; i < count; ++i)
3007
- mesh.combine(this, render3DAxisRotation(unit, i / count * 2 * PI));
3008
- return mesh;
3009
- }
3010
-
3011
- /** Returns a new mesh of everything in this mesh or the other, see subtract
3012
- * @param {Mesh} mesh - Closed, as the builders make them apart from the open ones like buildGrid
3013
- * @param {Matrix4|Vector3} [matrix] - Places the other mesh, or just a position to move it to
3014
- * @return {Mesh} */
3015
- union(mesh, matrix) { return render3DMeshCSG(this, mesh, matrix, 0); }
3016
-
3017
- /** Returns a new mesh of this one with the other cut out of it, CSG with BSP trees
3018
- * - Both must be closed, every edge shared by two triangles, as the builders make them apart from the open
3019
- * ones like buildGrid and buildRibbon; the result is closed and indexed, and neither mesh changes
3020
- * - The faces a cut makes come from the other mesh's surface, turned to face out, with its normals, uvs and
3021
- * colors, so a smooth cylinder drills a round hole
3022
- * - Parts that overlap must be joined with union to be one solid, not with combine, mirror or spin, which
3023
- * leave them overlapping, and CSG then gives a wrong shape
3024
- * - Cuts split the triangles near them, so the result has more: a few thousand triangles take tens to a few
3025
- * hundred milliseconds, so build shapes this way at load time, not every frame; joining several cutters
3026
- * with union and cutting once is quicker than cutting with each in turn
3027
- * - Details closer than about 1e-4 are made one, so build a very small part larger and scale it after
3028
- * @param {Mesh} mesh - Closed, as the builders make them apart from the open ones like buildGrid
3029
- * @param {Matrix4|Vector3} [matrix] - Places the other mesh, or just a position to move it to
3030
- * @return {Mesh}
3031
- * @example
3032
- * const wall = buildBox(vec3(4, 3, .5)).subtract(buildBox(vec3(1, 2, 1)), vec3(0, -.5, 0)); // a doorway */
3033
- subtract(mesh, matrix) { return render3DMeshCSG(this, mesh, matrix, 1); }
3034
-
3035
- /** Returns a new mesh of only what is in both this mesh and the other, see subtract
3036
- * @param {Mesh} mesh - Closed, as the builders make them apart from the open ones like buildGrid
3037
- * @param {Matrix4|Vector3} [matrix] - Places the other mesh, or just a position to move it to
3038
- * @return {Mesh} */
3039
- intersect(mesh, matrix) { return render3DMeshCSG(this, mesh, matrix, 2); }
3040
-
3041
- /** Scale every uv, so a whole texture repeats across the mesh when its TextureInfo wraps
3042
- * @param {Vector2|number} scale - Repeats across and up, a number for both
3043
- * @return {Mesh} */
3044
- scaleUVs(scale)
3045
- {
3046
- const s = render3DSize2(scale);
3047
- // new vectors, builders share uv objects between faces
3048
- this.uvs = this.uvs.map(uv=> vec2(uv.x * s.x, uv.y * s.y));
3049
- this.dirty = true;
3050
- return this;
3051
- }
3052
-
3053
- /** Move, turn or scale every vertex in place, normals follow along
3054
- * @param {Matrix4|Vector3} matrix - Transform, or just an offset to move by
3055
- * @return {Mesh} */
3056
- transform(matrix)
3057
- {
3058
- matrix = render3DMatrix(matrix);
3059
- const normalMatrix = render3DNormalMatrix(matrix);
3060
- for (let i = 0; i < this.points.length; ++i)
3061
- {
3062
- this.points[i] = matrix.transformPoint(this.points[i]);
3063
- // a mesh built by hand may have no normals yet, and then there is nothing to turn
3064
- this.normals[i] &&= normalMatrix.transformDirection(this.normals[i]).normalize();
3065
- }
3066
- render3DMirrors(matrix) && render3DFlipWinding(this); // a mirror would leave the faces pointing in
3067
- this.dirty = true;
3068
- return this;
3069
- }
3070
-
3071
- /** Turn the mesh inside out so it is lit and drawn from within, for rooms and domes
3072
- * @return {Mesh} */
3073
- flipNormals()
3074
- {
3075
- render3DFlipWinding(this);
3076
- this.normals = this.normals.map(n=> n.scale(-1));
3077
- this.dirty = true;
3078
- return this;
3079
- }
3080
-
3081
- /** Set every vertex color
3082
- * @param {Color} color
3083
- * @return {Mesh} */
3084
- setColor(color)
3085
- {
3086
- // one per point, not one per color already there, so a mesh built by hand with no
3087
- // colors gets them instead of quietly staying white
3088
- this.colors = this.points.map(()=> color);
3089
- this.dirty = true;
3090
- return this;
3091
- }
3092
-
3093
- /** Measure the axis aligned box around the vertices
3094
- * @return {{min: Vector3, max: Vector3}} */
3095
- getBounds()
3096
- {
3097
- if (!this.points.length)
3098
- return {min: vec3(), max: vec3()};
3099
- const lo = vec3(Infinity), hi = vec3(-Infinity);
3100
- for (const p of this.points)
3101
- {
3102
- lo.x = min(lo.x, p.x); lo.y = min(lo.y, p.y); lo.z = min(lo.z, p.z);
3103
- hi.x = max(hi.x, p.x); hi.y = max(hi.y, p.y); hi.z = max(hi.z, p.z);
3104
- }
3105
- return {min: lo, max: hi};
3106
- }
3107
-
3108
- /** Move the mesh so the center of its bounds is on the origin
3109
- * @return {Mesh} */
3110
- center()
3111
- {
3112
- const bounds = this.getBounds();
3113
- return this.transform(bounds.min.add(bounds.max).scale(-.5));
3114
- }
3115
-
3116
- /** Scale the mesh evenly so its largest extent is a size, for loaded models of unknown units
3117
- * @param {number} [size]
3118
- * @return {Mesh} */
3119
- fit(size=1)
3120
- {
3121
- const bounds = this.getBounds();
3122
- const extent = bounds.max.subtract(bounds.min);
3123
- const scale = size / (max(extent.x, extent.y, extent.z) || 1);
3124
- return this.transform(Matrix4.scaling(vec3(scale)));
3125
- }
3126
-
3127
- /** Measure the bounding sphere around the origin into radius, called by upload
3128
- * @return {number} */
3129
- computeRadius()
3130
- {
3131
- let r = 0;
3132
- for (const p of this.points)
3133
- r = max(r, p.lengthSquared());
3134
- this.bounds = this.getBounds(); // measured with it, a pick tests a mesh's box after its sphere
3135
- return this.radius = r ** .5;
3136
- }
3137
-
3138
- /** Derive normals from the triangles, of the strip or of the index list
3139
- * @param {boolean} [smooth] - Round the lighting across faces instead of giving each face a hard edge; flat
3140
- * normals on an indexed mesh give every corner its own vertex
3141
- * @return {Mesh} */
3142
- computeNormals(smooth=false)
3143
- {
3144
- if (this.indices)
3145
- {
3146
- // the triangles are listed: smooth normals add up around each position, weighted by the corner angle
3147
- // like the strip's, so vertices split apart at one place smooth back together and a mesh can go flat and
3148
- // smooth again; flat ones need a vertex per corner, so the vertices are split up first
3149
- if (smooth)
3150
- {
3151
- const {groups, sums, touched} = render3DSmoothNormalSums(this.points, this.indices);
3152
- this.normals = this.points.map((p, i)=>
3153
- {
3154
- const s = groups[i] * 3, x = sums[s], y = sums[s+1], z = sums[s+2];
3155
- const lengthSquared = x**2 + y**2 + z**2, k = 1 / lengthSquared**.5;
3156
- return touched[groups[i]] && lengthSquared ? vec3(x * k, y * k, z * k) : RENDER3D_DEFAULT_NORMAL;
3157
- });
3158
- this.dirty = true;
3159
- return this;
3160
- }
3161
- const split = (a)=> this.indices.map(i=> a[i]);
3162
- this.points = split(this.points), this.normals = split(this.normals);
3163
- this.uvs = split(this.uvs), this.colors = split(this.colors);
3164
- this.indices = this.indices.map((_, i)=> i);
3165
- const points = this.points, indices = this.indices;
3166
- const normals = points.map(()=> RENDER3D_DEFAULT_NORMAL);
3167
- for (let t = 0; t < indices.length; t += 3)
3168
- {
3169
- const a = points[indices[t]], b = points[indices[t+1]], c = points[indices[t+2]];
3170
- const cross = b.subtract(a).cross(c.subtract(a));
3171
- if (cross.lengthSquared()) // its own three corners
3172
- normals[indices[t]] = normals[indices[t+1]] = normals[indices[t+2]] = cross.normalize();
3173
- }
3174
- this.normals = normals;
3175
- this.dirty = true;
3176
- return this;
3177
- }
3178
-
3179
- // smooth normals are the sums at each place made unit length, or straight up for a place with none
3180
- const points = this.points, n = points.length;
3181
- if (smooth)
3182
- {
3183
- const {groups, sums, touched} = render3DSmoothNormalSums(points);
3184
- this.normals = points.map((p, i)=>
3185
- {
3186
- const s = groups[i] * 3, x = sums[s], y = sums[s+1], z = sums[s+2], l = (x**2 + y**2 + z**2)**.5;
3187
- return !touched[groups[i]] ? vec3(0, 1, 0) : l ? vec3(x * (1/l), y * (1/l), z * (1/l)) : vec3();
3188
- });
3189
- this.vertexKeys = undefined;
3190
- this.dirty = true;
3191
- return this;
3192
- }
3193
-
3194
- // the outward normal of each triangle in the strip
3195
- const faceNormals = [];
3196
- for (let i = 0; i + 2 < n; ++i)
3197
- {
3198
- const a = points[i], b = points[i+1], c = points[i+2];
3199
- const normal = b.subtract(a).cross(c.subtract(a));
3200
- // triangles in a strip alternate which way they wind, so every other one is flipped back
3201
- // a zero normal means a flat triangle joining two strips, so skip it
3202
- faceNormals.push(normal.lengthSquared() ? normal.normalize(i & 1 ? 1 : -1) : undefined);
3203
- }
3204
-
3205
- // then hand those to the vertices: every triangle writes its own three corners, so the only vertices left
3206
- // with the default are the repeats at the ends of a strip, which no triangle with any area uses
3207
- const normals = points.map(()=> RENDER3D_DEFAULT_NORMAL);
3208
- faceNormals.forEach((f, i)=> f && (normals[i] = normals[i+1] = normals[i+2] = f));
3209
-
3210
- this.normals = normals;
3211
- this.vertexKeys = undefined; // flat normals tell entries at one place apart
3212
- this.dirty = true;
3213
- return this;
3214
- }
3215
-
3216
- /** Pack the vertices and create the GPU buffer, called automatically by render
3217
- * @return {Mesh} */
3218
- upload()
3219
- {
3220
- // the packing below measures the radius and box, with nothing to pack to they are measured here
3221
- if (!render3D?.program || !glContext) return this.computeRadius(), this;
3222
- const gl = glContext, layout = this.vertexLayout;
3223
- // no layout when dynamicDraw was turned on after an upload, the next upload makes one
3224
- if (this.dynamicDraw && layout && this.buffer && this.contextGeneration === render3D.contextGeneration)
3225
- {
3226
- // the layout of the last upload stands, only the values are written again into the buffer it has,
3227
- // packed into the same memory each time so a mesh uploaded every frame makes no garbage
3228
- ASSERT(layout.pointCount === this.points.length,
3229
- 'a dynamicDraw mesh keeps its shape, the same points in the same order; ' +
3230
- 'for a new shape make a new mesh or turn dynamicDraw off', this.points.length);
3231
- if (layout.pointCount === this.points.length)
3232
- {
3233
- gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
3234
- gl.bufferSubData(gl.ARRAY_BUFFER, 0, render3DMeshVertexData(this, layout.vertices, layout.data));
3235
- gl.bindBuffer(gl.ARRAY_BUFFER, glArrayBuffer);
3236
- this.dirty = false;
3237
- return this;
3238
- }
3239
- }
3240
- this.dispose();
3241
- const {vertices, indices} = this.getTriangles(), count = vertices.length, wide = count > 65535;
3242
- const data = render3DMeshVertexData(this, vertices);
3243
- this.vertexLayout = this.dynamicDraw ? {vertices, pointCount: this.points.length, data} : undefined;
3244
- this.vertexKeys = undefined; // used once: an edit after this may tell entries apart
3245
- this.buffer = gl.createBuffer();
3246
- this.indexBuffer = gl.createBuffer();
3247
- this.bufferCount = indices.length;
3248
- this.indexType = wide ? gl.UNSIGNED_INT : gl.UNSIGNED_SHORT;
3249
- this.dirty = false;
3250
- gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
3251
- gl.bufferData(gl.ARRAY_BUFFER, data, this.dynamicDraw ? gl.DYNAMIC_DRAW : gl.STATIC_DRAW);
3252
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
3253
- gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, wide ? new Uint32Array(indices) : new Uint16Array(indices), gl.STATIC_DRAW);
3254
- this.contextGeneration = render3D.contextGeneration;
3255
- const buffers = {buffer: this.buffer, indexBuffer: this.indexBuffer, generation: this.contextGeneration};
3256
- render3DMeshBuffers?.register(this, buffers, this);
3257
- gl.bindBuffer(gl.ARRAY_BUFFER, glArrayBuffer); // the engine's 2D batch writes through this binding
3258
- return this;
3259
- }
3260
-
3261
- /** The mesh as an indexed triangle list, what upload sends to the GPU: the strip's real triangles over its
3262
- * distinct vertices, the joins between its pieces dropped and every triangle facing the way it did in the strip
3263
- * - Vertices are compared to a millionth, so two at one place with the same normal, uv and color are one
3264
- * @return {{vertices: Array<number>, indices: Array<number>}} - vertices are strip indices, one per distinct
3265
- * vertex; indices are the triangles, three per triangle, into vertices */
3266
- getTriangles()
3267
- {
3268
- const count = this.points.length, vertices = [];
3269
- if (this.indices)
3270
- {
3271
- // already a list: every vertex as it is, and the triangles read the way the pass draws, clockwise
3272
- for (let i = 0; i < count; ++i)
3273
- vertices.push(i);
3274
- return {vertices, indices: render3DFlipTriangles(this.indices.slice())};
3275
- }
3276
- const remap = new Int32Array(count), place = new Int32Array(count), keys = this.vertexKeys;
3277
- if (keys && keys.length === count)
3278
- {
3279
- // a builder said which entries are one vertex: the first entry with a key stands for every entry with it,
3280
- // and is their place, so nothing is searched
3281
- const first = new Int32Array(count).fill(-1);
3282
- for (let i = 0; i < count; ++i)
3283
- {
3284
- const key = keys[i];
3285
- ASSERT(key >= 0 && key < count, 'vertexKeys must be whole numbers below the entry count', key);
3286
- const j = first[key];
3287
- if (j < 0)
3288
- first[key] = i, remap[i] = vertices.length, vertices.push(i);
3289
- else
3290
- remap[i] = remap[j];
3291
- place[i] = first[key];
3292
- }
3293
- return {vertices, indices: render3DStripTriangles(count, remap, place)};
3294
- }
3295
-
3296
- // each vertex as nine whole numbers, its values in millionths and its color, so vertices hash and compare as
3297
- // numbers; a hash table over all nine finds the distinct vertices and one over the first three the places,
3298
- // each slot holding a vertex index plus one and a taken slot moving on to the next
3299
- const values = new Float64Array(count * 9);
3300
- let size = 1;
3301
- while (size < count * 2) size *= 2;
3302
- const mask = size - 1, seen = new Int32Array(size), places = new Int32Array(size);
3303
- const mix = (h, v)=> Math.imul(h ^ v, 0x9e3779b1) >>> 0;
3304
- for (let i = 0; i < count; ++i)
3305
- {
3306
- const p = this.points[i], n = this.normals[i] || RENDER3D_DEFAULT_NORMAL;
3307
- const uv = this.uvs[i] || RENDER3D_DEFAULT_UV, k = i * 9;
3308
- const x = values[k] = round(p.x * 1e6), y = values[k+1] = round(p.y * 1e6), z = values[k+2] = round(p.z * 1e6);
3309
- values[k+3] = round(n.x * 1e6), values[k+4] = round(n.y * 1e6), values[k+5] = round(n.z * 1e6);
3310
- values[k+6] = round(uv.x * 1e6), values[k+7] = round(uv.y * 1e6);
3311
- values[k+8] = (this.colors[i] || WHITE).rgbaInt();
3312
- let h = mix(mix(mix(0x811c9dc5, x), y), z);
3313
-
3314
- // the place, shared with a vertex there that has another normal or uv, as at a lathe's poles
3315
- for (let slot = h & mask;; slot = (slot + 1) & mask)
3316
- {
3317
- const o = places[slot] - 1;
3318
- if (o < 0) { places[slot] = i + 1, place[i] = i; break; }
3319
- if (values[o*9] === x && values[o*9+1] === y && values[o*9+2] === z) { place[i] = o; break; }
3320
- }
3321
-
3322
- // the whole vertex
3323
- for (let m = 3; m < 9; ++m)
3324
- h = mix(h, values[k+m]);
3325
- for (let slot = h & mask;; slot = (slot + 1) & mask)
3326
- {
3327
- const o = seen[slot] - 1;
3328
- if (o < 0) { seen[slot] = i + 1, remap[i] = vertices.length, vertices.push(i); break; }
3329
- let same = true;
3330
- for (let m = 0; same && m < 9; ++m)
3331
- same = values[o*9+m] === values[k+m];
3332
- if (same) { remap[i] = remap[o]; break; }
3333
- }
3334
- }
3335
- return {vertices, indices: render3DStripTriangles(count, remap, place)};
3336
- }
3337
-
3338
- /** Draw the mesh with the current draw state, batched with its other uses in the opaque stage
3339
- * @param {Matrix4|Vector3} [matrix] - Object transform, or just a position to draw it at
3340
- * @param {TileInfo|TextureInfo} [tileInfo] - Texture, mesh uvs map across the tile or the whole texture
3341
- * @param {Color} [color] - Tint */
3342
- render(matrix, tileInfo, color) { render3D?.drawMesh(this, matrix, tileInfo, color); }
3343
-
3344
- /** Delete the GPU buffer now, the CPU arrays stay so the mesh can be rendered again
3345
- * - Optional, the buffer is freed anyway once the mesh is garbage collected, this frees it right away */
3346
- dispose()
3347
- {
3348
- if (!this.buffer) return;
3349
- render3DMeshBuffers?.unregister(this); // freed here, so not again when the mesh is collected
3350
- // a buffer from a context that was lost is gone with it, and the new context refuses to delete it
3351
- if (this.contextGeneration === render3D?.contextGeneration)
3352
- glContext?.deleteBuffer(this.buffer), glContext?.deleteBuffer(this.indexBuffer);
3353
- this.buffer = this.indexBuffer = undefined;
3354
- this.bufferCount = 0;
3355
- }
3356
- }
3357
-
3358
- ///////////////////////////////////////////////////////////////////////////////
3359
- // Shape builders, all centered on the origin so buildMatrix does placement
3360
- // Sizes are full sizes like buildBox and the 2D drawCircle, sides go around an axis and rings along it
3361
-
3362
- /**
3363
- * Spin a flat outline around the Y axis to make a round shape, like a vase or a wheel
3364
- * - profile is [[radius, y], ...] from bottom to top
3365
- * - A profile that ends where it starts makes a closed ring like a donut
3366
- * - An end left open, with a radius and no cap, makes the mesh doubleSided so its inside shows
3367
- * - An end on the axis smooth shades as a round pole like a sphere's when its segment is within 45 degrees of
3368
- * level, and as a point like a cone's tip when it is steeper
3369
- * @param {Array<Array<number>>} profile
3370
- * @param {number} [sides] - Around the axis
3371
- * @param {boolean} [smooth] - Defaults to render3D.smoothShading
3372
- * @param {boolean} [capped] - Close the ends that have a radius with flat discs
3373
- * @return {Mesh}
3374
- * @memberof Render3D
3375
- * @example
3376
- * const vase = buildLathe([[0, -1], [.8, -.3], [.9, .2], [.4, .6], [0, 1]], 12);
3377
- */
3378
- function buildLathe(profile, sides=16, smooth=render3D?.smoothShading, capped=true)
3379
- {
3380
- ASSERT(isArray(profile) && profile.length > 1, 'lathe profile needs at least 2 points');
3381
- sides |= 0;
3382
- ASSERT(sides > 2, 'lathe needs at least 3 sides');
3383
- const mesh = new Mesh;
3384
- const rings = profile.length;
3385
- const point = (i, a)=> vec3(sin(a) * profile[i][0], profile[i][1], cos(a) * profile[i][0]);
3386
-
3387
- // 2D outward normal of each profile segment, in (radius, y) space
3388
- const segmentNormal = (i)=>
3389
- {
3390
- const [r0, y0] = profile[i], [r1, y1] = profile[i+1];
3391
- const n = vec2(y1 - y0, r0 - r1);
3392
- return n.length() ? n.normalize() : vec2(1, 0);
3393
- };
3394
- // vertex normal: average of the adjacent segment normals, across the seam when the profile is closed,
3395
- // and a closed profile needs no caps
3396
- const closed = rings > 2 && abs(profile[0][0] - profile[rings-1][0]) < 1e-9 &&
3397
- abs(profile[0][1] - profile[rings-1][1]) < 1e-9;
3398
- const segmentLength = (i)=> hypot(profile[i+1][0] - profile[i][0], profile[i+1][1] - profile[i][1]);
3399
- const vertexNormal = (i)=>
3400
- {
3401
- // an open end on the axis is a pole and points along it, like a sphere's, when the surface there is
3402
- // within 45 degrees of level; a steeper one is a point like a cone's tip and takes its side's normal
3403
- if (!closed && (!i || i == rings - 1) && abs(profile[i][0]) < 1e-9)
3404
- {
3405
- const up = i ? 1 : -1;
3406
- if (segmentNormal(i ? i - 1 : 0).y * up > Math.SQRT1_2 - 1e-9)
3407
- return vec2(0, up);
3408
- }
3409
- // otherwise the neighbors weighted by their length, so a short band does not tilt a long wall
3410
- let n = vec2();
3411
- const add = (s)=> n = n.add(segmentNormal(s).scale(segmentLength(s)));
3412
- if (i > 0) add(i - 1);
3413
- else if (closed) add(rings - 2);
3414
- if (i < rings - 1) add(i);
3415
- else if (closed) add(0);
3416
- return n.length() ? n.normalize() : vec2(1, 0);
3417
- };
3418
- const normal3D = (n, a)=> vec3(sin(a) * n.x, n.y, cos(a) * n.x);
3419
-
3420
- // v runs along the profile by arc length
3421
- const lengths = [0];
3422
- for (let i = 1; i < rings; ++i)
3423
- lengths[i] = lengths[i-1] + hypot(profile[i][0] - profile[i-1][0], profile[i][1] - profile[i-1][1]);
3424
- const total = lengths[rings - 1] || 1;
3425
- const v = (i)=> 1 - lengths[i] / total;
3426
-
3427
- for (let i = 0; i + 1 < rings; ++i)
3428
- {
3429
- if (smooth)
3430
- {
3431
- // one ribbon around the ring pair, top point then bottom point per column
3432
- const points = [], normals = [], uvs = [];
3433
- const n0 = vertexNormal(i), n1 = vertexNormal(i + 1);
3434
- // a point on the axis is one per column, each drawn by the face beside it, so its normal turns half
3435
- // a side toward the middle of that face; a pole's points along the axis and does not turn
3436
- const half = PI / sides;
3437
- const turn1 = abs(profile[i + 1][0]) < 1e-9 ? -half : 0, turn0 = abs(profile[i][0]) < 1e-9 ? half : 0;
3438
- for (let j = 0; j <= sides; ++j)
3439
- {
3440
- const a = j / sides * 2 * PI, u = j / sides;
3441
- points.push(point(i + 1, a), point(i, a));
3442
- normals.push(normal3D(n1, a + turn1), normal3D(n0, a + turn0));
3443
- uvs.push(vec2(u, v(i + 1)), vec2(u, v(i)));
3444
- }
3445
- mesh.addStrip(points, normals, uvs);
3446
- }
3447
- else
3448
- {
3449
- // one quad per side with its face normal
3450
- const n = segmentNormal(i);
3451
- for (let j = 0; j < sides; ++j)
3452
- {
3453
- const a0 = j / sides * 2 * PI, a1 = (j + 1) / sides * 2 * PI;
3454
- const u0 = j / sides, u1 = (j + 1) / sides;
3455
- mesh.addStrip(
3456
- [point(i + 1, a0), point(i, a0), point(i + 1, a1), point(i, a1)],
3457
- normal3D(n, (a0 + a1) / 2),
3458
- [vec2(u0, v(i + 1)), vec2(u0, v(i)), vec2(u1, v(i + 1)), vec2(u1, v(i))]);
3459
- }
3460
- }
3461
- }
3462
-
3463
- // flat discs close the ends that have a radius, a hard edge even when the sides are smooth
3464
- if (capped && !closed)
3465
- for (const [i, up] of /** @type {Array<[number, boolean]>} */ ([[0, false], [rings - 1, true]]))
3466
- {
3467
- if (abs(profile[i][0]) < 1e-9) continue; // a pole has no cap
3468
- const points = [], uvs = [];
3469
- for (let j = 0; j < sides; ++j)
3470
- {
3471
- const a = (up ? j : -j) / sides * 2 * PI; // counter clockwise seen from outside
3472
- points.push(point(i, a));
3473
- uvs.push(vec2(sin(a) * .5 + .5, cos(a) * .5 + .5));
3474
- }
3475
- mesh.addStrip(render3DPolygonStrip(points), vec3(0, up ? 1 : -1, 0), render3DPolygonStrip(uvs));
3476
- }
3477
-
3478
- // an end left open shows the inside, so it is seen from both sides
3479
- mesh.doubleSided = !closed && !capped && (abs(profile[0][0]) > 1e-9 || abs(profile[rings-1][0]) > 1e-9);
3480
- return mesh;
3481
- }
3482
-
3483
- /**
3484
- * Build a sphere centered on the origin
3485
- * @param {number} [size] - Diameter
3486
- * @param {number} [sides] - Around
3487
- * @param {number} [rings] - Top to bottom
3488
- * @param {boolean} [smooth] - Defaults to render3D.smoothShading
3489
- * @return {Mesh}
3490
- * @memberof Render3D
3491
- */
3492
- function buildSphere(size=1, sides=16, rings=8, smooth=render3D?.smoothShading)
3493
- {
3494
- ASSERT(rings > 1, 'sphere needs at least 2 rings');
3495
- const profile = [];
3496
- for (let i = 0; i <= rings; ++i)
3497
- {
3498
- const a = i / rings * PI - PI/2;
3499
- profile.push([cos(a) * size / 2, sin(a) * size / 2]);
3500
- }
3501
- return buildLathe(profile, sides, smooth);
3502
- }
3503
-
3504
- // a box with its edges cut by a bevel of size t: each face shrunk by t, a strip along each edge around a quarter
3505
- // circle, and an eighth of a sphere at each corner; one segment makes them flat, a chamfer
3506
- function render3DBevelBox(half, t, segments, faces)
3507
- {
3508
- const mesh = new Mesh, inner = half.subtract(vec3(t)), n = max(1, segments | 0);
3509
-
3510
- // a triangle facing out, since the box is convex around the origin, flat shaded or with the normals of the
3511
- // round, and uvs from the face it faces most, as a plain box maps that face
3512
- const triangle = (a, b, c, na, nb, nc)=>
3513
- {
3514
- let normal = b.subtract(a).cross(c.subtract(a));
3515
- if (normal.lengthSquared() < 1e-20) return; // an edge or face with no size
3516
- if (normal.dot(a.add(b).add(c)) < 0)
3517
- [b, c, nb, nc, normal] = [c, b, nc, nb, normal.scale(-1)];
3518
- normal = normal.normalize();
3519
- let face = faces[0];
3520
- for (const f of faces)
3521
- if (f[0].dot(normal) > face[0].dot(normal))
3522
- face = f;
3523
- const [, r, u] = face, hr = abs(half.dot(r)), hu = abs(half.dot(u));
3524
- const uv = (p)=> vec2(.5 + p.dot(r) / (2 * hr), .5 - p.dot(u) / (2 * hu));
3525
- mesh.addStrip([a, b, c], n > 1 && na ? [na, nb, nc] : normal, [uv(a), uv(b), uv(c)]);
3526
- };
3527
-
3528
- // the faces, shrunk by the bevel
3529
- for (const [normal, r, u] of faces)
3530
- {
3531
- const [a, b, c, d] = render3DQuadAxes(normal.multiply(half), r.multiply(inner), u.multiply(inner));
3532
- triangle(a, b, c);
3533
- triangle(c, b, d);
3534
- }
3535
-
3536
- // the edges, each a strip around a quarter circle from one face to the next
3537
- const axes = [vec3(1, 0, 0), vec3(0, 1, 0), vec3(0, 0, 1)];
3538
- for (let k = 0; k < 3; ++k)
3539
- for (const si of [-1, 1])
3540
- for (const sj of [-1, 1])
3541
- {
3542
- const n1 = axes[(k + 1) % 3].scale(si), n2 = axes[(k + 2) % 3].scale(sj);
3543
- const middle = n1.multiply(inner).add(n2.multiply(inner)), along = axes[k].multiply(inner);
3544
- const start = middle.subtract(along), end = middle.add(along);
3545
- const dir = (m)=> n1.scale(cos(m / n * PI / 2)).add(n2.scale(sin(m / n * PI / 2)));
3546
- for (let m = 0; m < n; ++m)
3547
- {
3548
- const d0 = dir(m), d1 = dir(m + 1);
3549
- const a = start.add(d0.scale(t)), b = end.add(d0.scale(t));
3550
- const c = start.add(d1.scale(t)), d = end.add(d1.scale(t));
3551
- triangle(a, b, c, d0, d0, d1);
3552
- triangle(c, b, d, d1, d0, d1);
3553
- }
3554
- }
3555
-
3556
- // the corners, an eighth of a sphere in rows from one face's axis down to the edge between the other two
3557
- for (const sx of [-1, 1])
3558
- for (const sy of [-1, 1])
3559
- for (const sz of [-1, 1])
3560
- {
3561
- const nx = vec3(sx, 0, 0), ny = vec3(0, sy, 0), nz = vec3(0, 0, sz), corner = inner.multiply(vec3(sx, sy, sz));
3562
- const dir = (i, j)=>
3563
- {
3564
- const polar = i / n * PI / 2, around = i ? j / i * PI / 2 : 0;
3565
- return nz.scale(cos(polar)).add(nx.scale(cos(around) * sin(polar))).add(ny.scale(sin(around) * sin(polar)));
3566
- };
3567
- const point = (i, j)=> corner.add(dir(i, j).scale(t));
3568
- for (let i = 0; i < n; ++i)
3569
- {
3570
- for (let j = 0; j <= i; ++j)
3571
- triangle(point(i, j), point(i + 1, j), point(i + 1, j + 1), dir(i, j), dir(i + 1, j), dir(i + 1, j + 1));
3572
- for (let j = 0; j < i; ++j)
3573
- triangle(point(i, j), point(i + 1, j + 1), point(i, j + 1), dir(i, j), dir(i + 1, j + 1), dir(i, j + 1));
3574
- }
3575
- }
3576
- return mesh;
3577
- }
3578
-
3579
- /**
3580
- * Build a box centered on the origin, six flat faces with uvs covering each face
3581
- * - bevel cuts its edges and corners: 1 segment is a flat chamfer, more round them, and the biggest bevel, half
3582
- * the smallest side, rounds a cube into a ball
3583
- * @param {Vector3|number} [size] - Full size, a number for a cube
3584
- * @param {number} [bevel] - Size of the cut on each edge, clamped to half the smallest side
3585
- * @param {number} [bevelSegments] - Steps around each edge, 1 for a flat chamfer
3586
- * @return {Mesh}
3587
- * @memberof Render3D
3588
- */
3589
- function buildBox(size=1, bevel=0, bevelSegments=1)
3590
- {
3591
- ASSERT(isNumber(bevel) && bevel >= 0, 'bevel must be a number, 0 or more');
3592
- const half = render3DSize3(size).scale(.5);
3593
- // each face: normal, right axis, up axis (right cross up = normal)
3594
- const faces = [
3595
- [vec3(0, 0, 1), vec3(1, 0, 0), vec3(0, 1, 0)],
3596
- [vec3(0, 0, -1), vec3(-1, 0, 0), vec3(0, 1, 0)],
3597
- [vec3(1, 0, 0), vec3(0, 0, -1), vec3(0, 1, 0)],
3598
- [vec3(-1, 0, 0), vec3(0, 0, 1), vec3(0, 1, 0)],
3599
- [vec3(0, 1, 0), vec3(1, 0, 0), vec3(0, 0, -1)],
3600
- [vec3(0, -1, 0), vec3(1, 0, 0), vec3(0, 0, 1)],
3601
- ];
3602
- const t = min(bevel, half.x, half.y, half.z);
3603
- if (t > 0)
3604
- return render3DBevelBox(half, t, bevelSegments, faces);
3605
- const mesh = new Mesh;
3606
- for (const [n, r, u] of faces)
3607
- {
3608
- const center = n.multiply(half);
3609
- const right = r.multiply(half), up = u.multiply(half);
3610
- mesh.addStrip(render3DQuadAxes(center, right, up), n, RENDER3D_QUAD_UVS);
3611
- }
3612
- return mesh;
3613
- }
3614
-
3615
- /**
3616
- * Build a heightfield grid in the XZ plane centered on the origin
3617
- * - smooth rounds the lighting across cells and colors each corner
3618
- * - flat lights and colors each cell on its own, so a checkerboard stays crisp
3619
- * - doubleSided, a sheet seen from both sides; turn it off for ground only ever seen from above
3620
- * - One cell is a plain square, render3D.planeMesh and planeMeshDoubleSided are shared ones
3621
- * @param {Vector2|number} [size] - World size along X and Z, a number for a square
3622
- * @param {Vector2|number} [segments] - Cells along X and Z, a number for both
3623
- * @param {Color|function(number, number): Color} [color] - One Color for the whole grid, or (x, z) => Color
3624
- * @param {function(number, number): number} [heightFunction] - (x, z) => y, default flat
3625
- * @param {boolean} [smooth] - Defaults to render3D.smoothShading with a heightFunction; a flat grid is flat shaded,
3626
- * since its light is even anyway and smoothing would only blend its cell colors; pass smooth to blend a gradient
3627
- * @return {Mesh}
3628
- * @memberof Render3D
3629
- * @example
3630
- * const ground = buildGrid(vec2(20), 10, (x, z)=> (floor(x / 2) + floor(z / 2)) & 1 ? GRAY : WHITE); // 2 unit checks
3631
- */
3632
- function buildGrid(size=vec2(1), segments=1, color, heightFunction, smooth=heightFunction && render3D?.smoothShading)
3633
- {
3634
- heightFunction ||= ()=> 0;
3635
- size = render3DSize2(size);
3636
- segments = render3DSize2(segments);
3637
- ASSERT(segments.x > 0 && segments.y > 0 && segments.x % 1 === 0 && segments.y % 1 === 0,
3638
- 'grid segments must be whole numbers above zero');
3639
- const mesh = new Mesh;
3640
- const segmentsX = segments.x, segmentsZ = segments.y;
3641
- const cellX = size.x / segmentsX, cellZ = size.y / segmentsZ;
3642
- const halfX = size.x / 2, halfZ = size.y / 2, ex = cellX / 2, ez = cellZ / 2;
3643
- const px = (i)=> i * cellX - halfX, pz = (j)=> j * cellZ - halfZ;
3644
- const colorAt = /** @type {function(number, number): Color} */ (color);
3645
- const cellColor = (i, j)=> !color ? WHITE : isColor(color) ? /** @type {Color} */ (color) : colorAt(px(i), pz(j));
3646
- // a big terrain has millions of vertices, so each one is made once, shared by the rows above and below it
3647
- const row = (j)=>
3648
- {
3649
- const points = [], normals = [], uvs = [], colors = [], z = pz(j);
3650
- for (let i = 0; i <= segmentsX; ++i)
3651
- {
3652
- const x = px(i);
3653
- points.push(vec3(x, heightFunction(x, z), z));
3654
- uvs.push(vec2(i / segmentsX, j / segmentsZ));
3655
- if (!smooth) continue;
3656
- normals.push(render3DSlopeNormal(heightFunction, x, z, ex, ez, halfX, halfZ));
3657
- colors.push(cellColor(i, j));
3658
- }
3659
- return {points, normals, uvs, colors};
3660
- };
3661
- let above = row(0);
3662
- for (let j = 0; j < segmentsZ; ++j)
3663
- {
3664
- const below = row(j + 1);
3665
- if (smooth)
3666
- {
3667
- // one ribbon per row with vertex normals from the slope
3668
- const points = [], normals = [], uvs = [], colors = [];
3669
- for (let i = 0; i <= segmentsX; ++i)
3670
- {
3671
- points.push(above.points[i], below.points[i]);
3672
- normals.push(above.normals[i], below.normals[i]);
3673
- uvs.push(above.uvs[i], below.uvs[i]);
3674
- colors.push(above.colors[i], below.colors[i]);
3675
- }
3676
- mesh.addStrip(points, normals, uvs, colors);
3677
- }
3678
- else
3679
- {
3680
- // one quad per cell with its face normal and one color sampled at its center
3681
- for (let i = 0; i < segmentsX; ++i)
3682
- mesh.addQuad(above.points[i], below.points[i], below.points[i+1], above.points[i+1], cellColor(i + .5, j + .5),
3683
- [above.uvs[i], below.uvs[i], below.uvs[i+1], above.uvs[i+1]]);
3684
- }
3685
- above = below;
3686
- }
3687
- if (smooth)
3688
- {
3689
- // the grid knows which strip entries are one vertex, each shared by the rows above and below it and by the
3690
- // repeats at the ends of its ribbons, so the upload of a big terrain skips searching millions of entries
3691
- const n = 2 * (segmentsX + 1) + 2, keys = mesh.vertexKeys = new Int32Array(mesh.points.length);
3692
- const id = (i, j)=> j * (segmentsX + 1) + i;
3693
- for (let j = 0; j < segmentsZ; ++j)
3694
- {
3695
- const start = j * n;
3696
- keys[start] = id(0, j); // the leading repeat
3697
- for (let i = 0; i <= segmentsX; ++i)
3698
- keys[start + 1 + 2*i] = id(i, j), keys[start + 2 + 2*i] = id(i, j + 1);
3699
- keys[start + n - 1] = id(segmentsX, j + 1); // the trailing repeat
3700
- }
3701
- }
3702
- mesh.doubleSided = true; // a sheet, seen from both sides; terrain seen only from above can turn it off
3703
- return mesh;
3704
- }
3705
-
3706
- /**
3707
- * Build a sky dome: a sphere colored by direction, wound to be seen from inside
3708
- * - set it as render3D.sky and the pass draws it around the camera behind everything
3709
- * @param {Color} [topColor] - Straight up
3710
- * @param {Color} [horizonColor] - Level with the camera
3711
- * @param {Color} [bottomColor] - Straight down, what a camera looking at the ground sees past its edge; defaults to the
3712
- * horizon color
3713
- * @param {number} [sides] - Around
3714
- * @param {number} [rings] - Top to bottom
3715
- * @return {Mesh}
3716
- * @memberof Render3D
3717
- */
3718
- function buildSky(topColor=hsl(.6, .8, .55), horizonColor=hsl(.6, 1, .9), bottomColor=horizonColor, sides=16, rings=8)
3719
- {
3720
- // a sphere turned inside out so it is seen from within, each point colored by how high it is
3721
- const mesh = buildSphere(2, sides, rings, true).flipNormals();
3722
- mesh.colors = mesh.points.map(p=> p.y < 0 ? horizonColor.lerp(bottomColor, -p.y) : horizonColor.lerp(topColor, p.y));
3723
- render3DSkyColors.set(mesh, [topColor.copy(), horizonColor.copy(), bottomColor.copy()]);
3724
- return mesh;
3725
- }
3726
-
3727
- // the top, horizon and bottom colors of each sky dome buildSky made, which a reflection shows while that dome is
3728
- // render3D.sky, whether setSky set it or the game did
3729
- const render3DSkyColors = new WeakMap;
3730
-
3731
- ///////////////////////////////////////////////////////////////////////////////
3732
- /**
3733
- * EngineObject3D - An EngineObject with a 3D transform and a mesh
3734
- * - Set pos3D, rotation3D and scale3D instead of the 2D pos, size and angle
3735
- * - Gets update, children, timers, destroy and renderOrder from EngineObject
3736
- * - velocity3D is added to pos3D each frame, slowed by damping, and render3D.gravity pulls it once it has a mass
3737
- * - Objects face -Z, the same way the camera does, so lookAt turns them to face a point
3738
- * - The 2D pos and velocity are still there but nothing draws them
3739
- * - These inherited fields are 2D only and do nothing here: angle, angleVelocity, additiveColor, drawSize and mirror;
3740
- * damping, angleDamping, clampSpeed, friction and groundObject work as in 2D, on velocity3D and angleVelocity3D
3741
- * - The inherited shader works here as in 2D, and with emissive at 1 its snippet does its own lighting
3742
- * - Set sync2D for a 2D game with 3D looks, pos and angle then drive pos3D and rotation3D,
3743
- * which is the one way those 2D fields reach a 3D object
3744
- * - setCollision takes the same flags as in 2D, but the solid collision happens in 3D against size3D
3745
- * - The solid box turns with rotation3D, so a turned wall or a ramp collides as it looks; resting on one no steeper
3746
- * than groundAngle stands there
3747
- * - Its tile and raycast halves are 2D only so they default off here, and a child sits solid collision out
3748
- * - A sync2D object collides in 2D instead, which needs the 2D size set as well as size3D
3749
- * - setMesh swaps the mesh and frees the old one, for text and terrain that get built again
3750
- * - addChild attaches the 3D transform, and pos3D becomes an offset from the parent; attach keeps the child where
3751
- * it is and works the offset out, and removeChild leaves it where it was in the world
3752
- * - The 2D offset arguments of addChild do nothing here, set the child's pos3D
3753
- * @extends EngineObject
3754
- * @memberof Render3D
3755
- * @example
3756
- * class Spinner extends EngineObject3D
3757
- * {
3758
- * constructor(pos) { super(pos, buildBox(), undefined, RED); }
3759
- * update() { this.rotation3D.y += .02; }
3760
- * }
3761
- */
3762
- class EngineObject3D extends EngineObject
3763
- {
3764
- /** Create a 3D object and add it to the object list
3765
- * @param {Vector3} [pos3D] - World space position
3766
- * @param {Mesh} [mesh] - Mesh to draw, undefined draws nothing
3767
- * @param {TileInfo|TextureInfo} [tileInfo] - Texture, mesh uvs map across the tile; a whole TextureInfo becomes
3768
- * the tile that covers it
3769
- * @param {Color} [color] - Tint */
3770
- constructor(pos3D=vec3(), mesh, tileInfo, color=WHITE)
3771
- {
3772
- ASSERT(!tileInfo || tileInfo instanceof TileInfo || tileInfo instanceof TextureInfo,
3773
- 'tileInfo must be a TileInfo or TextureInfo, it comes before color');
3774
- // a whole texture is stored as the tile that covers it, with no padding or bleed to trim
3775
- // the edges, so this is always a TileInfo like the 2D one and the object stays an EngineObject
3776
- if (tileInfo instanceof TextureInfo)
3777
- render3DWholeTiles.add(tileInfo = new TileInfo(vec2(), tileInfo.size, tileInfo, 0, 0));
3778
- // the 2D pos starts where the object is, so turning on sync2D keeps it there
3779
- super(vec2(pos3D.x, pos3D.y), vec2(), tileInfo, 0, color);
3780
- ASSERT(isVector3(pos3D), 'pos3D must be a vec3');
3781
- ASSERT(!mesh || mesh instanceof Mesh, 'mesh must be a Mesh or undefined');
3782
- this.mass = 0; // static: no 2D physics, and no 3D gravity until a mass is set
3783
-
3784
- /** @property {Vector3} - World space position, local to the parent when attached to an EngineObject3D */
3785
- this.pos3D = pos3D.copy();
3786
- /** @property {Vector3} - Rotation vec3(pitch, yaw, roll) in radians, local to the parent when attached to an
3787
- * EngineObject3D */
3788
- this.rotation3D = vec3();
3789
- /** @property {Vector3} - Scale, local to the parent when attached to an EngineObject3D */
3790
- this.scale3D = vec3(1);
3791
- /** @property {Vector3} - Added to pos3D each frame by the engine before update, like the 2D velocity, no super
3792
- * call needed; damping and render3D.gravity act on it once the object has a mass */
3793
- this.velocity3D = vec3();
3794
- /** @property {Vector3} - Added to rotation3D each frame by the engine before update, slowed by angleDamping */
3795
- this.angleVelocity3D = vec3();
3796
- /** @property {Mesh|undefined} - Mesh to draw
3797
- * @type {Mesh|undefined} */
3798
- this.mesh = mesh;
3799
- /** @property {Vector3} - Size for solid collision and the collect and callback helpers, and of the sprite when
3800
- * there is a tileInfo and no mesh; starts at the size of the mesh's box, or 1 with no mesh, and setMesh leaves
3801
- * it as it is; the box is centered on pos3D, so center() a mesh whose origin is not its middle, like a model
3802
- * standing on its feet, or set size3D; scale3D and any parent's scale grow it, so drawing and picking agree */
3803
- // a shared mesh measured since it last changed is not walked again for each object made from it
3804
- const bounds = mesh && mesh.points.length ? !mesh.dirty && mesh.bounds || mesh.getBounds() : undefined;
3805
- this.size3D = bounds ? bounds.max.subtract(bounds.min) : vec3(1);
3806
- /** @property {number} - Diameter of a soft shadow drawn under the object on render3D.softShadowHeight, 0 for
3807
- * none; scale3D and a parent's scale grow it, so set it once for the unscaled object */
3808
- this.softShadow = 0;
3809
- /** @property {boolean} - A sprite stands on world up instead of tilting toward the camera */
3810
- this.upright = false;
3811
- /** @property {boolean} - Keep this object's texture pixels hard edged, for pixel art that should not blur or bleed */
3812
- this.pixelated = false;
3813
- /** @property {boolean} - Copy the 2D pos and angle into pos3D and rotation3D each frame, for 2D games with 3D
3814
- * looks; set mass to use 2D physics, and pos3D.z stays yours to set or move with velocity3D.z */
3815
- this.sync2D = false;
3816
- /** @property {boolean} - Draw in the transparent stage, blended and sorted far to near with depth writes off;
3817
- * on for a sprite */
3818
- this.transparent = !mesh && !!tileInfo;
3819
- /** @property {boolean} - Additive blending, in the transparent stage */
3820
- this.additive = false;
3821
- /** @property {number} - How much it lights itself: 0 is lit as normal, 1 is its own color with no shading, for
3822
- * lamps and glowing things, between is partly self lit, and above 1 is brighter than its color, for bloom */
3823
- this.emissive = 0;
3824
- /** @property {number} - Strength of the highlight where the sun and the Light3D objects reflect, 0 is none and
3825
- * 1 adds a light's full color at its brightest; shininess sets its size */
3826
- this.specular = 0;
3827
- /** @property {number} - The highlight's exponent, how small and sharp it is: 4 is broad like rubber, 16 the
3828
- * default, 100 sharp like polished metal; shows only with specular above 0 */
3829
- this.shininess = 16;
3830
- /** @property {TextureInfo|undefined} - A normal map that bends the surface at each texel so it catches the
3831
- * light like bumps and grooves, green pointing up the image as OpenGL and glTF have it; read at the color
3832
- * texture's coordinates, see normalMapFromHeight to make one in code
3833
- * @type {TextureInfo|undefined} */
3834
- this.normalMap = undefined;
3835
- /** @property {number} - How strongly the normal map bends the surface, 0 turns it off, as glTF's scale */
3836
- this.normalScale = 1;
3837
- /** @property {number} - How much it reflects the sky, 0 none and 1 a mirror of it; the edges seen at a
3838
- * glancing angle reflect more either way, as water and glass do */
3839
- this.reflectivity = 0;
3840
- /** @property {TextureInfo|undefined} - A texture of where it glows, added on top of the lit surface so it
3841
- * shows in the dark, like lit windows; read at the color texture's coordinates
3842
- * @type {TextureInfo|undefined} */
3843
- this.emissiveMap = undefined;
3844
- /** @property {Color} - Multiplies the emissive map, as glTF's emissiveFactor */
3845
- this.emissiveMapColor = WHITE;
3846
- /** @property {boolean} - Draw into the shadow map when render3D.shadows is on; sprites and cut out textures
3847
- * cast their outline, an object faded below half its alpha casts nothing, a see through one casts only when
3848
- * textured, and additive objects never cast */
3849
- this.castShadow = true;
3850
- /** @property {boolean} - Collide as the sphere that fits size3D instead of as the size3D box, so it rolls
3851
- * around corners */
3852
- this.collideAsSphere3D = false;
3853
- /** @property {number} - The steepest slope it stands on, in radians from level, PI/4 by default: resting on
3854
- * a solid within this of flat sets groundObject and holds it still, steeper it slides down */
3855
- this.groundAngle = PI / 4;
3856
- /** @property {boolean} - Darkened by the shadow map when render3D.shadows is on */
3857
- this.receiveShadow = true;
3858
- /** @property {boolean|undefined} - Draw this object over the 2D scene, undefined uses render3D.renderAfter2D
3859
- * @type {boolean|undefined} */
3860
- this.renderAfter2D = undefined;
3861
- /** @property {Matrix4|undefined} - The transform from its parent, used in place of pos3D, rotation3D and
3862
- * scale3D when set, for one they cannot hold like a glTF pose with shear; read every frame it is set
3863
- * @type {Matrix4|undefined} */
3864
- this.localMatrix = undefined;
3865
- // the world transform, kept up to date by render3DObjectMatrix; getMatrix returns a copy
3866
- this.worldMatrix = new Matrix4;
3867
- this.matrixBuilt = new Float64Array(9).fill(NaN); // the position, rotation and scale it was built from
3868
- this.matrixVersion = 0; // counts the rebuilds, so a child knows when its parent's changed
3869
- /** @type {EngineObject3D|undefined} */
3870
- this.matrixParent = undefined; // the parent it was built under, and that parent's version then
3871
- this.matrixParentVersion = 0;
3872
- this.movePass = engineObjectsUpdateCount; // the engine pass a child last moved in, so a refresh is not a step
3873
- }
3874
-
3875
- /** Move by the 3D velocities and push out of solids, called automatically each frame before update, like the 2D physics
3876
- * - update runs once every object has moved and collided, so bounce off anything else there, it lands before the draw
3877
- * - Override this and call super to change how the object moves itself
3878
- * - A sync2D object runs the 2D physics as well, and collides there instead */
3879
- updatePhysics()
3880
- {
3881
- // a sync2D object collides in 2D, which measures the 2D size, and that starts at zero on a 3D object
3882
- ASSERT(!this.sync2D || !this.collideSolidObjects || (this.size.x && this.size.y),
3883
- 'a sync2D object collides in 2D, so give it a 2D size as well as a size3D', this.size);
3884
- if (this.sync2D)
3885
- super.updatePhysics();
3886
- ASSERT(isNumber(this.groundAngle) && this.groundAngle >= 0 && this.groundAngle < PI / 2,
3887
- 'groundAngle must be 0 to less than PI/2, a slope from level', this.groundAngle);
3888
- // what it stands on is found again each frame, by the level and by the solids it rests on; a sync2D
3889
- // object's is the 2D physics'
3890
- const ground = this.groundObject;
3891
- this.sync2D || (this.groundObject = undefined);
3892
- if (this.clampSpeed && !this.sync2D && (this.collideSolidObjects || this.collideLevel && this.mass))
3893
- {
3894
- // each axis within objectMaxSpeed, as in 2D, so a fast object does not pass through a thin wall; only
3895
- // for what collides, anything else moves as fast as it is told
3896
- const v = this.velocity3D, s = objectMaxSpeed;
3897
- v.x = clamp(v.x, -s, s), v.y = clamp(v.y, -s, s), v.z = clamp(v.z, -s, s);
3898
- }
3899
- // a moving object keeps out of the level, the height maps and voxel maps, from where it was before it moved
3900
- const oldPos = this.collideLevel && this.mass && !this.sync2D ? this.pos3D.copy() : undefined;
3901
- render3DMove(this);
3902
- if (ground && this.mass && !this.sync2D)
3903
- {
3904
- // sliding on what it stood on slows by friction, the less grippy of the two, relative to that one's own
3905
- // speed so a moving platform carries it, as in 2D
3906
- const friction = max(this.friction, ground.friction), v = this.velocity3D;
3907
- const moving = ground instanceof EngineObject3D ? ground.velocity3D : undefined;
3908
- const gx = moving?.x ?? 0, gz = moving?.z ?? 0;
3909
- v.x = gx + (v.x - gx) * friction, v.z = gz + (v.z - gz) * friction;
3910
- }
3911
- oldPos && render3DCollideLevel(this, oldPos, ground);
3912
- // the engine only runs this for objects that own where they are, a child rides along with its parent
3913
- if (this.collideSolidObjects && !this.sync2D)
3914
- render3DCollideSolid(this);
3915
- }
3916
-
3917
- /** Move a child by its own velocities, bring a sync2D object's pos3D up to its 2D pos, then update the children,
3918
- * called automatically each frame
3919
- * @param {boolean} [updateChildren] - Also update the children's transforms */
3920
- updateTransforms(updateChildren=true)
3921
- {
3922
- if (!paused)
3923
- {
3924
- // a child is never given updatePhysics, so it moves here, as an offset from its parent: once per
3925
- // engine pass, since addChild, attach and a game bring the transforms up to date too
3926
- if (this.parent && this.movePass !== engineObjectsUpdateCount)
3927
- {
3928
- this.movePass = engineObjectsUpdateCount;
3929
- render3DMove(this);
3930
- }
3931
- }
3932
-
3933
- // placed from its parent first, so a sync2D object copies where it is now, then its children from it
3934
- super.updateTransforms(false);
3935
- if (!paused && this.sync2D)
3936
- this.pos3D.x = this.pos.x, this.pos3D.y = this.pos.y, this.rotation3D.z = -this.angle;
3937
- if (updateChildren)
3938
- for (const child of this.children)
3939
- child.updateTransforms();
3940
- }
3941
-
3942
- /** Set how this object collides, the same flags as in 2D
3943
- * - Solid collision happens in 3D here, against size3D boxes or spheres; a child sits it out
3944
- * - The boxes turn with rotation3D, against height maps and voxel maps an object is still its upright box
3945
- * - A sync2D object collides in 2D instead, against the 2D size, so set that as well as size3D
3946
- * @param {boolean} [collideSolidObjects] - Take part in solid collision
3947
- * @param {boolean} [isSolid] - Block other objects, a pair where neither one blocks passes through;
3948
- * blocking needs collideSolidObjects, so isSolid on its own is not allowed
3949
- * @param {boolean} [collideLevel] - Collide with the level, the height maps and voxel maps, or the 2D tile layers
3950
- * for a sync2D object
3951
- * @param {boolean} [collideRaycast] - Raycasts, 2D only; 3D has render3D.pick and engineObjectsRaycast3D */
3952
- setCollision(collideSolidObjects=true, isSolid=true, collideLevel=true, collideRaycast=false)
3953
- { super.setCollision(collideSolidObjects, isSolid, collideLevel, collideRaycast); }
3954
-
3955
- /** Called by a VoxelMap to ask whether a block stops this object, a hook to let one through or react to it
3956
- * @param {number} type - The block's type, 1 to 255
3957
- * @param {Vector3} cell - The block's cell in the map
3958
- * @return {boolean} - true to be stopped by it, every block stops it by default */
3959
- collideWithVoxel(type, cell) { return true; }
3960
-
3961
- /** Returns the world position
3962
- * @return {Vector3} */
3963
- getWorldPos3D() { return render3DObjectMatrix(this).getTranslation(); }
3964
-
3965
- /** Returns the direction the object faces, its -Z axis in the world
3966
- * @return {Vector3} */
3967
- getForward3D() { return render3DAxis(render3DObjectMatrix(this).m, 8).normalize(-1); }
3968
-
3969
- /** Returns the object's right axis in the world
3970
- * @return {Vector3} */
3971
- getRight3D() { return render3DAxis(render3DObjectMatrix(this).m, 0).normalize(); }
3972
-
3973
- /** Returns the object's up axis in the world
3974
- * @return {Vector3} */
3975
- getUp3D() { return render3DAxis(render3DObjectMatrix(this).m, 4).normalize(); }
3976
-
3977
- /** Returns a copy of the object's world transform, the parent's included when attached to an EngineObject3D
3978
- * - The object keeps its matrix and rebuilds it only when its position, rotation or scale changed, so this is
3979
- * cheap to call
3980
- * @return {Matrix4} */
3981
- getMatrix() { return render3DObjectMatrix(this).copy(); }
3982
-
3983
- /** Turn the object so its -Z axis points at a world space target, sets pitch and yaw and clears roll
3984
- * @param {Vector3} target */
3985
- lookAt(target)
3986
- {
3987
- // rotation3D is local to the parent, so a child has to aim at the target from the parent's point of view
3988
- const parent = this.parent instanceof EngineObject3D ? this.parent : undefined;
3989
- const local = parent ? parent.getMatrix().invert().transformPoint(target) : target;
3990
- this.rotation3D = render3DLookRotation(local.subtract(this.pos3D), this.rotation3D);
3991
- }
3992
-
3993
- /** Attaches a child, its pos3D, rotation3D and scale3D taken as the offset from this one; returns child for chaining
3994
- * - The 2D offset arguments do nothing for an EngineObject3D child, set its pos3D
3995
- * @param {EngineObject} child
3996
- * @param {Vector2} [localPos]
3997
- * @param {number} [localAngle]
3998
- * @return {EngineObject} The child object attached */
3999
- addChild(child, localPos, localAngle)
4000
- {
4001
- // addChild brings the child's transform up to date, which is not a step: it already moved this pass as
4002
- // a root, or moves in the next one, so it must not move by its velocity again here
4003
- if (child instanceof EngineObject3D)
4004
- child.movePass = engineObjectsUpdateCount;
4005
- return super.addChild(child, localPos, localAngle);
4006
- }
4007
-
4008
- /** Attaches a child without moving it: its pos3D, rotation3D and scale3D become what they have to be under this
4009
- * parent to keep its world transform, where addChild takes them as the offset; returns child for chaining
4010
- * - A parent scaled unevenly and a child turned under it make a shear, which those three values cannot hold,
4011
- * so the child comes out as close as they can get; a uniform scale is exact
4012
- * @param {EngineObject} child
4013
- * @return {EngineObject} The child object attached */
4014
- attach(child)
4015
- {
4016
- if (!(child instanceof EngineObject3D))
4017
- return super.attach(child); // a 2D child only has the 2D transform to keep
4018
- child.parent?.removeChild(child); // keeps its world values, so its own matrix is its world matrix
4019
- const local = render3DObjectMatrix(this).copy().invert().multiply(render3DObjectMatrix(child));
4020
- super.attach(child);
4021
- child.pos3D = local.getTranslation();
4022
- child.rotation3D = local.getRotation();
4023
- child.scale3D = local.getScale();
4024
- if (child.localMatrix)
4025
- child.localMatrix = local; // a matrix given whole stays whole, shear and all
4026
- return child;
4027
- }
4028
-
4029
- /** Removes a child from this one, it stays where it is in the world: its pos3D, rotation3D and scale3D become
4030
- * its world values, with the same shear caveat as attach; a child being destroyed is let go as it is
4031
- * @param {EngineObject} child */
4032
- removeChild(child)
4033
- {
4034
- if (child instanceof EngineObject3D && !child.destroyed)
4035
- {
4036
- const world = render3DObjectMatrix(child);
4037
- child.pos3D = world.getTranslation();
4038
- child.rotation3D = world.getRotation();
4039
- child.scale3D = world.getScale();
4040
- if (child.localMatrix)
4041
- child.localMatrix = world.copy(); // a matrix given whole stays whole, shear and all
4042
- }
4043
- super.removeChild(child);
4044
- }
4045
-
4046
- /** Draw a different mesh and free the GPU buffer of the one it replaces
4047
- * - For a mesh built again when something changes, like a score, a rebuilt terrain or a loaded model
4048
- * - A mesh another object is still drawing is left alone, since builders are often shared
4049
- * - Freeing one held somewhere else only costs it an upload, the points it was built from stay
4050
- * @param {Mesh} [mesh] - The mesh to draw from now on, undefined to draw nothing
4051
- * @return {Mesh|undefined} - The mesh passed in */
4052
- setMesh(mesh)
4053
- {
4054
- ASSERT(!mesh || mesh instanceof Mesh, 'mesh must be a Mesh or undefined');
4055
- const old = this.mesh;
4056
- this.mesh = mesh;
4057
- // nothing to free and nothing to look for when it was never uploaded
4058
- if (old && old !== mesh && old.buffer && !engineObjects.some(o=> /** @type {EngineObject3D} */ (o).mesh === old))
4059
- old.dispose();
4060
- return mesh;
4061
- }
4062
-
4063
- /** 2D rendering is skipped, the mesh is drawn by render3D during the 3D pass */
4064
- render() {}
4065
-
4066
- /** Draw the object in 3D, called by the 3D pass with the draw state set from this object's flags, draws the mesh by default
4067
- * @return {void} */
4068
- render3D()
4069
- {
4070
- // an opaque draw comes out solid however low its alpha is, so a fade with no flag looks like nothing happened
4071
- ASSERT(this.transparent || this.additive || this.color.a >= 1,
4072
- 'an object that fades needs its transparent flag, an opaque draw ignores the color alpha', this.color);
4073
- // the matrix the object keeps, rebuilt only when it moved, the same one for the shadow pass and the main pass
4074
- const matrix = render3DObjectMatrix(this);
4075
- if (this.mesh)
4076
- render3D.drawMesh(this.mesh, matrix, this.tileInfo, this.color);
4077
- else if (this.tileInfo)
4078
- {
4079
- // a sprite, at the world size the collect, pick and solid collision helpers measure it at
4080
- const m = matrix.m, size = render3DWorldSize(this, m);
4081
- render3D.drawBillboard(vec3(m[12], m[13], m[14]), vec2(size.x, size.y), this.tileInfo, this.color,
4082
- this.rotation3D.z, this.upright);
4083
- }
4084
- }
4085
- }
4086
-
4087
- // an object's world matrix, the one it keeps: rebuilt only when its position, rotation or scale changed since the
4088
- // last build, or its parent's matrix did, so an object that stands still costs nine compares a frame instead of
4089
- // the trig and a new matrix; the matrix returned is the object's own, read it and never change it
4090
- const render3DLocalMatrix = new Matrix4;
4091
- function render3DObjectMatrix(o)
4092
- {
4093
- const parent = o.parent instanceof EngineObject3D ? o.parent : undefined;
4094
- const parentMatrix = parent && render3DObjectMatrix(parent); // the parent first, so its version is current
4095
- const p = o.pos3D, r = o.rotation3D, s = o.scale3D, k = o.matrixBuilt, local = o.localMatrix;
4096
- if (local)
4097
- {
4098
- // a matrix given whole is taken as it is each time, it can change in place
4099
- if (parent)
4100
- {
4101
- o.worldMatrix.m.set(parentMatrix.m);
4102
- o.worldMatrix.multiply(local);
4103
- o.matrixParentVersion = parent.matrixVersion;
4104
- }
4105
- else
4106
- o.worldMatrix.m.set(local.m);
4107
- k[0] = NaN; // built from the matrix, so going back to pos3D builds again
4108
- o.matrixParent = parent;
4109
- ++o.matrixVersion;
4110
- }
4111
- else if (k[0] !== p.x || k[1] !== p.y || k[2] !== p.z || k[3] !== r.x || k[4] !== r.y || k[5] !== r.z
4112
- || k[6] !== s.x || k[7] !== s.y || k[8] !== s.z || o.matrixParent !== parent
4113
- || parent && o.matrixParentVersion !== parent.matrixVersion)
4114
- {
4115
- k[0] = p.x, k[1] = p.y, k[2] = p.z, k[3] = r.x, k[4] = r.y, k[5] = r.z, k[6] = s.x, k[7] = s.y, k[8] = s.z;
4116
- if (parent)
4117
- {
4118
- // the parent's world matrix times the local one
4119
- buildMatrix(p, r, s, render3DLocalMatrix);
4120
- o.worldMatrix.m.set(parentMatrix.m);
4121
- o.worldMatrix.multiply(render3DLocalMatrix);
4122
- o.matrixParentVersion = parent.matrixVersion;
4123
- }
4124
- else
4125
- buildMatrix(p, r, s, o.worldMatrix);
4126
- o.matrixParent = parent;
4127
- ++o.matrixVersion;
4128
- }
4129
- return o.worldMatrix;
4130
- }
4131
-
4132
- // move an object by its 3D velocities, each slowed by its damping, an object with mass falling with render3D.gravity
4133
- function render3DMove(o)
4134
- {
4135
- // the vectors change in place, as the 2D object's do: this runs for every object every frame
4136
- // a sync2D object's damping is the 2D physics', its 3D velocities are its own to set
4137
- const p = o.pos3D, v = o.velocity3D, r = o.rotation3D, a = o.angleVelocity3D;
4138
- const d = o.sync2D ? 1 : o.damping, e = o.sync2D ? 1 : o.angleDamping;
4139
- // damped first and gravity added after, the order EngineObject.updatePhysics uses,
4140
- // so the same mass, damping and gravity fall the same way in both
4141
- v.x *= d, v.y *= d, v.z *= d;
4142
- if (o.mass && !o.sync2D) // a 2D driven object gets the 2D gravity instead
4143
- {
4144
- const g = render3D.gravity, s = o.gravityScale;
4145
- v.x += g.x * s, v.y += g.y * s, v.z += g.z * s;
4146
- }
4147
- p.x += v.x, p.y += v.y, p.z += v.z;
4148
- r.x += a.x *= e, r.y += a.y *= e, r.z += a.z *= e;
4149
- }
4150
-
4151
- // keep an object that moved out of the level's solid geometry, clearing what it stood on for the level to set again
4152
- // ground is what it stood on last frame, which a height map keeps it on going down a slope
4153
- function render3DCollideLevel(o, oldPos, ground)
4154
- {
4155
- for (const level of render3DLevel)
4156
- level.destroyed || level.levelCollide3D(o, oldPos, level === ground);
4157
- }
4158
-
4159
- // where a solid object is in the world and what it collides as: the sphere that fits size3D, or the size3D box,
4160
- // each grown by the object's scale
4161
- // only objects that own where they are take part, so pos3D is already world space, and however the object is
4162
- // turned its axes come out as long as its scale makes them; building the transform to read that back off it
4163
- // costs six trig calls and a matrix for every pair tested, which is the whole cost of a crowded scene
4164
- function render3DSolidShape(o)
4165
- {
4166
- ASSERT(!o.parent, 'a child rides along with its parent, it has no world pos3D of its own to collide with');
4167
- const s = o.size3D, k = o.scale3D;
4168
- const kx = abs(k.x), ky = abs(k.y), kz = abs(k.z);
4169
- if (o.collideAsSphere3D)
4170
- return {pos: o.pos3D.copy(), radius: max(s.x, s.y, s.z) / 2 * max(kx, ky, kz)};
4171
- // a turned box has its axes, a solid is never a child so its rotation is the world's; an upright one has none, and
4172
- // neither does a sprite, whose rotation turns how it faces the camera, not its box
4173
- const axes = !render3DIsSprite(o) && isTurned3D(o.rotation3D) ? render3DSolidAxes(o) : undefined;
4174
- return {pos: o.pos3D.copy(), size: vec3(s.x * kx, s.y * ky, s.z * kz), axes};
4175
- }
4176
-
4177
- // a turned solid's three axes, worked out when its rotation changes and kept until then, so the pairs it is tested
4178
- // against each frame do not each work them out again
4179
- const render3DSolidAxesCache = new WeakMap;
4180
- function render3DSolidAxes(o)
4181
- {
4182
- const r = o.rotation3D, kept = render3DSolidAxesCache.get(o);
4183
- if (kept && kept.x === r.x && kept.y === r.y && kept.z === r.z)
4184
- return kept.axes;
4185
- const axes = boxAxes3D(r);
4186
- render3DSolidAxesCache.set(o, {x: r.x, y: r.y, z: r.z, axes});
4187
- return axes;
4188
- }
4189
-
4190
- // how far a solid shape can reach from its own center, for a quick reject before the exact test
4191
- // it has to be the shape's own radius, or a wider one: a box reaches to its corner, and a sphere
4192
- // takes the largest scale the same way render3DSolidShape does, or the reject would skip real touches
4193
- function render3DSolidReach(o)
4194
- {
4195
- const s = o.size3D, k = o.scale3D;
4196
- const kx = abs(k.x), ky = abs(k.y), kz = abs(k.z);
4197
- if (o.collideAsSphere3D)
4198
- return max(s.x, s.y, s.z) / 2 * max(kx, ky, kz);
4199
- return hypot(s.x * kx, s.y * ky, s.z * kz) / 2;
4200
- }
4201
-
4202
- // what it takes to move shape a clear of shape b, whichever pair of shapes they are, or undefined for no touch
4203
- function render3DSolidPush(a, b)
4204
- {
4205
- // a turned box is tested by its kept axes, the upright pairs as they always were
4206
- const sphereBox = (sphere, box)=> box.axes ? collideSphereOrientedBox3D(sphere.pos, sphere.radius, box.pos,
4207
- box.size, box.axes) : collideSphereBox(sphere.pos, sphere.radius, box.pos, box.size);
4208
- if (!a.size) // a is a sphere
4209
- return b.size ? sphereBox(a, b) : collideSphereSphere(a.pos, a.radius, b.pos, b.radius);
4210
- if (!b.size) // only b is, so push b out of a and turn it around
4211
- {
4212
- const push = sphereBox(b, a);
4213
- return push && push.scale(-1);
4214
- }
4215
- return a.axes || b.axes ? collideOrientedBoxes3D(a.pos, a.size, a.axes ?? BOX_WORLD_AXES, b.pos, b.size,
4216
- b.axes ?? BOX_WORLD_AXES) : collideBoxBox3D(a.pos, a.size, b.pos, b.size);
4217
- }
4218
-
4219
- // a sprite, a tile with no mesh, which faces the camera however it is turned
4220
- const render3DIsSprite = (o)=> !o.mesh && !!o.tileInfo;
4221
-
4222
- // whether a push leaves a box by one of its faces, along one of its axes, not by an edge or a corner; a sphere has
4223
- // no face to stand on
4224
- function render3DOnFace(push, shape)
4225
- {
4226
- if (!shape.size) return false;
4227
- const n = push.normalize(), axes = shape.axes ?? BOX_WORLD_AXES;
4228
- return max(abs(n.dot(axes[0])), abs(n.dot(axes[1])), abs(n.dot(axes[2]))) > 1 - 1e-6;
4229
- }
4230
-
4231
- // push a solid object out of the solids before it in the engine's list of them, so each pair is resolved once:
4232
- // the ones after it update later and test against it then, and an object that is not in the list yet, because it
4233
- // turned collision on this frame, tests them all itself and is not tested back
4234
- // one pair per test is half the work of the 2D solver, which tests both directions; the difference only shows
4235
- // when a collideWithObject destroys some third object, whose own turn then finds the pair already gone
4236
- function render3DCollideSolid(a)
4237
- {
4238
- let shapeA = render3DSolidShape(a);
4239
- const reachA = render3DSolidReach(a);
4240
- for (const b of engineObjectsCollide)
4241
- {
4242
- if (b === a) break;
4243
- // a child is part of its parent
4244
- if (b.destroyed || !(b instanceof EngineObject3D) || b.parent || b.sync2D) continue;
4245
- if (!a.isSolid && !b.isSolid) continue; // neither one blocks, so they pass through each other
4246
-
4247
- // the pairs nowhere near each other are almost all of them in a scene of any size, so
4248
- // settle those with one distance check instead of building a shape for each
4249
- const p = shapeA.pos, q = b.pos3D, reach = reachA + render3DSolidReach(b);
4250
- const dx = p.x - q.x, dy = p.y - q.y, dz = p.z - q.z;
4251
- if (dx*dx + dy*dy + dz*dz > reach*reach)
4252
- continue;
4253
-
4254
- const shapeB = render3DSolidShape(b);
4255
- let push = render3DSolidPush(shapeA, shapeB);
4256
- if (!push) continue;
4257
-
4258
- // both objects hear about it, and either one can take the touch over
4259
- const resolveA = a.collideWithObject(b, push);
4260
- const resolveB = b.collideWithObject(a, push.scale(-1));
4261
- if (!resolveA || !resolveB) continue;
4262
-
4263
- // standing: resting on a box's face within the upper one's groundAngle of level holds it there, the push
4264
- // turned straight up, as far as it takes to leave the surface, so it does not creep down a ramp; only what
4265
- // moves stands, and a sphere, an edge or a corner is nothing to stand on, what rests there rolls off
4266
- const lengthSquared = push.lengthSquared(), up = push.y / lengthSquared ** .5;
4267
- const aStands = up > 0 && a.mass && up >= cos(a.groundAngle) && render3DOnFace(push, shapeB);
4268
- const bStands = up < 0 && b.mass && -up >= cos(b.groundAngle) && render3DOnFace(push, shapeA);
4269
- if (aStands || bStands)
4270
- {
4271
- aStands ? a.groundObject = b : b.groundObject = a;
4272
- if (push.x || push.z) // one straight up already is used as it is
4273
- push = vec3(0, lengthSquared / push.y, 0);
4274
- }
4275
-
4276
- // heavier objects move less, mass 0 stays put; then bounce apart when moving toward each other
4277
- const total = a.mass + b.mass;
4278
- const weightA = !a.mass ? 0 : !b.mass ? 1 : b.mass / total;
4279
- const weightB = !b.mass ? 0 : !a.mass ? 1 : a.mass / total;
4280
- a.pos3D = a.pos3D.add(push.scale(weightA));
4281
- b.pos3D = b.pos3D.subtract(push.scale(weightB));
4282
- if (weightA)
4283
- shapeA = render3DSolidShape(a); // it moved, so the next solid must be tested against where it is now
4284
- // mass 0 keeps its velocity too, so a moving platform keeps moving, and what hits it bounces by its own
4285
- // restitution as it would off a static wall
4286
- const normal = push.normalize();
4287
- if (weightA && a.velocity3D.dot(normal) < 0)
4288
- a.velocity3D = a.velocity3D.reflect(normal, a.restitution);
4289
- if (weightB && b.velocity3D.dot(normal) > 0)
4290
- b.velocity3D = b.velocity3D.reflect(normal, b.restitution);
4291
- }
4292
- }
4293
-
4294
- /**
4295
- * Collect the EngineObject3D objects whose boxes overlap a sphere or a box, the 3D twin of engineObjectsCollect
4296
- * - Each object is its size3D box at its world position, turned as it is turned; lights, emitters and trails have no
4297
- * size and are never collected
4298
- * @param {Vector3} pos - Center of the area
4299
- * @param {Vector3|number} size - Diameter of a sphere if a number, 0 for a point, full size of a box if a Vector3
4300
- * @param {Array<EngineObject>} [objects] - Defaults to every object
4301
- * @param {boolean} [testCenters] - Test only each object's center, a little faster, and ignores object sizes
4302
- * @return {Array<EngineObject3D>}
4303
- * @memberof Render3D
4304
- */
4305
- function engineObjectsCollect3D(pos, size, objects=engineObjects, testCenters=false)
4306
- {
4307
- // a size of 0 is a point, tested against each box, since a sphere of no size could never hit
4308
- const radiusSquared = typeof size === 'number' && size > 0 ? (size/2)**2 : undefined;
4309
- const box = radiusSquared ? undefined : typeof size === 'number' ? vec3() : render3DSize3(size);
4310
- const collected = [];
4311
- for (const o of objects)
4312
- {
4313
- if (!(o instanceof EngineObject3D) || o.destroyed) continue;
4314
- // the box in world space, scaled by the object and its parents
4315
- const m = render3DObjectMatrix(o).m, s = o.size3D;
4316
- if (!(s.x || s.y || s.z)) continue;
4317
- const center = vec3(m[12], m[13], m[14]);
4318
- const worldSize = testCenters ? vec3() : render3DWorldSize(o, m);
4319
- // a turned box is tested as turned, its axes read off the matrix, its columns with the scale taken out
4320
- const turned = !testCenters && !render3DIsSprite(o) && (m[1] || m[2] || m[4] || m[6] || m[8] || m[9]);
4321
- const axis = (i)=> vec3(m[i], m[i+1], m[i+2]).normalize();
4322
- let axes = turned ? [axis(0), axis(4), axis(8)] : undefined;
4323
- if (axes && (abs(axes[0].dot(axes[1])) > 1e-6 || abs(axes[0].dot(axes[2])) > 1e-6 ||
4324
- abs(axes[1].dot(axes[2])) > 1e-6))
4325
- {
4326
- // a turned child of an unevenly scaled parent is sheared, its edges no longer square, which the box
4327
- // tests do not take; the upright box around it stands in, which may take a little more but never misses
4328
- const e = axes.map((a, i)=> a.scale(i ? i > 1 ? worldSize.z / 2 : worldSize.y / 2 : worldSize.x / 2));
4329
- const reach = (c)=> 2 * (abs(e[0][c]) + abs(e[1][c]) + abs(e[2][c]));
4330
- worldSize.set(reach('x'), reach('y'), reach('z'));
4331
- axes = undefined;
4332
- }
4333
- let hit;
4334
- if (box && axes)
4335
- hit = !!collideOrientedBoxes3D(pos, box, BOX_WORLD_AXES, center, worldSize, axes);
4336
- else if (box)
4337
- hit = isOverlapping3D(pos, box, center, worldSize);
4338
- else if (axes)
4339
- hit = !!collideSphereOrientedBox3D(pos, radiusSquared ** .5, center, worldSize, axes);
4340
- else
4341
- {
4342
- // a sphere against the nearest point of the box
4343
- const dx = max(abs(pos.x - center.x) - worldSize.x/2, 0);
4344
- const dy = max(abs(pos.y - center.y) - worldSize.y/2, 0);
4345
- const dz = max(abs(pos.z - center.z) - worldSize.z/2, 0);
4346
- hit = dx*dx + dy*dy + dz*dz < radiusSquared;
4347
- }
4348
- hit && collected.push(o);
4349
- }
4350
- return collected;
4351
- }
4352
-
4353
- // how far along a ray an object is hit, or undefined for a miss; a mesh is hit on its triangles, in its own space,
4354
- // and a sprite as the quad it draws
4355
- const render3DRaycastCenter = vec3();
4356
- function render3DRaycastObject(ray, o)
4357
- {
4358
- if (o.destroyed || !(o instanceof EngineObject3D)) return;
4359
- // a height map or voxel map is hit on its surface, not its box
4360
- if (o instanceof HeightMap || o instanceof VoxelMap) return o.levelRaycast3D(ray);
4361
- if (!(o.mesh || o.tileInfo)) return;
4362
- if (o instanceof InstancedMesh3D) return; // its instances are not objects, and its one sphere is not a thing to hit
4363
- const matrix = render3DObjectMatrix(o), mesh = o.mesh;
4364
- if (!mesh) return render3DRaycastSprite(ray, o, matrix);
4365
- // a mesh that changed since it was measured is measured again, an upload may not have come yet
4366
- const radius = (mesh.dirty || !mesh.radius ? mesh.computeRadius() : mesh.radius) * render3DMaxStretch(matrix.m);
4367
- if (!(radius > 0)) return; // nothing to hit
4368
- // its place read from the matrix into a vector kept for this, a pick tests every object and most miss
4369
- const m = matrix.m, center = render3DRaycastCenter.set(m[12], m[13], m[14]);
4370
- const distance = raycastSphere(ray, center, radius);
4371
- if (distance === undefined) return;
4372
-
4373
- // the sphere and then the box in the mesh's own space are quick rejects, and a ray that meets the box is tested
4374
- // against the triangles; the direction is not made unit length, so the distance holds in the world;
4375
- // a mesh flattened to nothing on an axis has no inverse, it is hit as a disc like a sprite
4376
- if (!matrix.determinant()) return render3DRaycastDisc(ray, center, radius);
4377
- const inverse = matrix.copy().invert(), bounds = mesh.bounds || mesh.getBounds();
4378
- const local = new Ray3D(inverse.transformPoint(ray.origin), inverse.transformDirection(ray.direction));
4379
- const hit = raycastBox(local, bounds.min.add(bounds.max).scale(.5), bounds.max.subtract(bounds.min));
4380
- if (hit === undefined || !ray.direction.lengthSquared())
4381
- return hit; // a ray of no length is where it starts, inside the box
4382
- return render3DRaycastMesh(local, mesh);
4383
- }
4384
-
4385
- // how far along a ray, in the mesh's own space, its nearest triangle is hit, or undefined: the triangles that face
4386
- // the ray as the pass draws them, clockwise from the front, and the back faces too of a doubleSided mesh; a strip's
4387
- // triangle i is (i-2, i-1, i), the odd ones read the other way, and a join between its pieces has no area and is
4388
- // never hit; nothing is kept between calls, so a mesh that changed is hit as it is now
4389
- function render3DRaycastMesh(ray, mesh)
4390
- {
4391
- const points = mesh.points, indices = mesh.indices, both = mesh.doubleSided;
4392
- const ox = ray.origin.x, oy = ray.origin.y, oz = ray.origin.z;
4393
- const dx = ray.direction.x, dy = ray.direction.y, dz = ray.direction.z;
4394
- const count = indices ? indices.length / 3 | 0 : points.length - 2;
4395
- let nearest;
4396
- for (let i = 0; i < count; ++i)
4397
- {
4398
- // the corners clockwise from the front: an indexed list is written the other way round
4399
- let a, b, c;
4400
- if (indices)
4401
- a = points[indices[i*3]], b = points[indices[i*3+2]], c = points[indices[i*3+1]];
4402
- else
4403
- a = points[i & 1 ? i + 1 : i], b = points[i & 1 ? i : i + 1], c = points[i + 2];
4404
-
4405
- // Moller and Trumbore: the determinant is the ray along the face's normal, below zero from the front
4406
- const e1x = b.x - a.x, e1y = b.y - a.y, e1z = b.z - a.z;
4407
- const e2x = c.x - a.x, e2y = c.y - a.y, e2z = c.z - a.z;
4408
- const hx = dy * e2z - dz * e2y, hy = dz * e2x - dx * e2z, hz = dx * e2y - dy * e2x;
4409
- const det = e1x * hx + e1y * hy + e1z * hz;
4410
- if (both ? !det : !(det < 0)) continue;
4411
- const sx = ox - a.x, sy = oy - a.y, sz = oz - a.z;
4412
- const u = (sx * hx + sy * hy + sz * hz) / det;
4413
- if (!(u >= 0 && u <= 1)) continue;
4414
- const qx = sy * e1z - sz * e1y, qy = sz * e1x - sx * e1z, qz = sx * e1y - sy * e1x;
4415
- const v = (dx * qx + dy * qy + dz * qz) / det;
4416
- if (!(v >= 0 && u + v <= 1)) continue;
4417
- const t = (e2x * qx + e2y * qy + e2z * qz) / det;
4418
- if (t >= 0 && !(t >= nearest))
4419
- nearest = t;
4420
- }
4421
- return nearest;
4422
- }
4423
-
4424
- // a sprite is the quad drawBillboard draws, its size3D grown by the x and y scale as the draw does, facing the camera
4425
- // as it was last drawn; where the ray meets its plane, inside its half axes
4426
- function render3DRaycastSprite(ray, o, matrix)
4427
- {
4428
- const center = matrix.getTranslation(), worldSize = render3DWorldSize(o, matrix.m), size = vec2(worldSize.x, worldSize.y);
4429
- if (!(size.x > 0 && size.y > 0)) return; // nothing to hit
4430
- if (!ray.direction.lengthSquared()) // a ray of no length is where it starts
4431
- return render3DRaycastDisc(ray, center, hypot(size.x, size.y) / 2);
4432
- const a = render3DBillboardAxes(size, o.rotation3D.z, o.upright);
4433
- const right = vec3(a[0], a[1], a[2]), up = vec3(a[3], a[4], a[5]);
4434
- const t = raycastPlane(ray, center, right.cross(up));
4435
- if (t === undefined) return; // behind, or seen edge on
4436
- const p = ray.getPosition(t).subtract(center);
4437
- return abs(p.dot(right)) <= right.lengthSquared() && abs(p.dot(up)) <= up.lengthSquared() ? t : undefined;
4438
- }
4439
-
4440
- // a mesh flattened to nothing, or a sprite for a ray of no length: a disc facing the ray at its center's depth
4441
- function render3DRaycastDisc(ray, center, radius)
4442
- {
4443
- const d = ray.direction, oc = center.subtract(ray.origin), dd = d.dot(d);
4444
- const t = dd ? oc.dot(d) / dd : 0; // its depth along the ray, in the ray's own units
4445
- return t >= 0 && oc.subtract(d.scale(t)).lengthSquared() <= radius*radius ? t : undefined;
4446
- }
4447
-
4448
- /**
4449
- * Collect every EngineObject3D a ray passes through, nearest first, the 3D twin of engineObjectsRaycast
4450
- * - The ray has no end, so everything along it counts however far away it is
4451
- * - Use render3D.pick for the nearest one on its own, with the distance to it
4452
- * @param {Ray3D} ray - From render3D.screenToRay, or any ray
4453
- * @param {Array<EngineObject>} [objects] - Defaults to every object; only those with a mesh or a sprite count
4454
- * @return {Array<EngineObject3D>}
4455
- * @memberof Render3D
4456
- */
4457
- function engineObjectsRaycast3D(ray, objects=engineObjects)
4458
- {
4459
- const hits = [];
4460
- for (const o of objects)
4461
- {
4462
- const distance = render3DRaycastObject(ray, o);
4463
- if (distance !== undefined) // only a 3D object has a distance
4464
- hits.push({o: /** @type {EngineObject3D} */ (o), distance});
4465
- }
4466
- return hits.sort((a, b)=> a.distance - b.distance).map(hit=> hit.o);
4467
- }
4468
-
4469
- /**
4470
- * Call a function for each EngineObject3D whose box overlaps a sphere or a box
4471
- * - An object destroyed by an earlier callback is skipped
4472
- * @param {Vector3} pos - Center of the area
4473
- * @param {Vector3|number} size - Diameter of a sphere if a number, 0 for a point, full size of a box if a Vector3
4474
- * @param {function(EngineObject3D): void} callback
4475
- * @param {Array<EngineObject>} [objects] - Defaults to every object
4476
- * @param {boolean} [testCenters] - Test only each object's center, see engineObjectsCollect3D
4477
- * @memberof Render3D
4478
- */
4479
- function engineObjectsCallback3D(pos, size, callback, objects=engineObjects, testCenters=false)
4480
- {
4481
- for (const o of engineObjectsCollect3D(pos, size, objects, testCenters))
4482
- o.destroyed || callback(o);
4483
- }
4484
-
4485
- ///////////////////////////////////////////////////////////////////////////////
4486
- /**
4487
- * InstancedMesh3D - Many copies of one mesh drawn as one call, with their transforms kept on the GPU
4488
- * - For big sets that mostly stay put: an instance costs nothing per frame until it changes, so a hundred thousand
4489
- * trees cost what one tree does; objects and drawMesh batch by themselves too, but rebuild their batch every frame
4490
- * - setTransformAt, setMatrixAt and setColorAt change one instance, and only the changed range uploads before the
4491
- * next draw; the matrices and the colors are kept apart, so moving instances uploads 64 bytes each
4492
- * - For many instances moving every frame, setTransforms places a run of them from arrays of positions, rotations
4493
- * and scales in one loop, the fastest way; or write matrixData directly and call markDirtyRange
4494
- * - The instances are in world space; the object's own pos3D, rotation3D and scale3D do not move them
4495
- * - The whole set is culled by one bounding sphere around the origin, worked out when culling reads it, or set by hand
4496
- * with radius; it casts and receives shadows like any object
4497
- * - The object's flags cover the whole set, one emissive, one tileInfo, one shader; only the colors are per instance
4498
- * - A mirrored instance, one with a negative scale, shows its inside unless the mesh is doubleSided
4499
- * - A transparent set draws in one go in the transparent stage, its instances are not sorted against each other;
4500
- * the set sorts against other transparent draws by the object's position, so put pos3D at its middle
4501
- * - pick, the raycast and the collect helpers do not see the instances, test them yourself from matrixData
4502
- * @extends EngineObject3D
4503
- * @memberof Render3D
4504
- * @example
4505
- * const forest = new InstancedMesh3D(treeMesh, 1000);
4506
- * for (let i = 0; i < 1000; ++i)
4507
- * forest.setTransformAt(i, randomGroundPos(), vec3(0, rand(2*PI), 0));
4508
- */
4509
- class InstancedMesh3D extends EngineObject3D
4510
- {
4511
- /** Create a set of instances of a mesh, each at the origin in the object's color until it is set
4512
- * @param {Mesh} mesh
4513
- * @param {number} count - How many instances there is room for, all of them draw until count is lowered
4514
- * @param {TileInfo|TextureInfo} [tileInfo] - Texture for all of them
4515
- * @param {Color} [color] - The color they start with */
4516
- constructor(mesh, count, tileInfo, color=WHITE)
4517
- {
4518
- super(vec3(), mesh, tileInfo, color);
4519
- ASSERT(mesh instanceof Mesh, 'an InstancedMesh3D needs a Mesh');
4520
- ASSERT(count >= 1, 'an InstancedMesh3D needs room for at least one instance');
4521
- this.size3D = vec3(); // not a solid thing to pick or collect
4522
- /** @property {number} - How many instances draw, the first ones, up to the count it was made with */
4523
- this.count = count;
4524
- /** @property {number} - How many instances it was made with */
4525
- this.maxCount = count;
4526
- /** @property {Float32Array} - Each instance's matrix, 16 floats, what moving them uploads; edit it directly and
4527
- * call markDirty for the instances changed */
4528
- this.matrixData = new Float32Array(count * RENDER3D_MATRIX_FLOATS);
4529
- /** @property {Float32Array} - Each instance's color and uv rect, 8 floats, uploaded only when they change; edit
4530
- * it directly and call markColorDirty for the instances changed */
4531
- this.colorData = new Float32Array(count * RENDER3D_COLOR_FLOATS);
4532
- /** @property {number} - First instance whose matrix uploads before the next draw */
4533
- this.dirtyStart = 0;
4534
- /** @property {number} - One past the last instance whose matrix uploads, nothing when it is not past dirtyStart */
4535
- this.dirtyEnd = count;
4536
- /** @property {number} - First instance whose color uploads before the next draw */
4537
- this.colorDirtyStart = 0;
4538
- /** @property {number} - One past the last instance whose color uploads */
4539
- this.colorDirtyEnd = count;
4540
- // the farthest any instance's position has been from the origin, and the largest scale any has had, both
4541
- // squared, and the instances set since radius last took them in: setting one does no bounds work at all
4542
- this.reachSquared = 0;
4543
- this.scaleSquared = 0;
4544
- this.boundsStart = 0;
4545
- this.boundsEnd = count;
4546
- /** @type {number|undefined} */
4547
- this.fixedRadius = undefined; // a radius set by hand, see radius
4548
- this.buffer = undefined; // the GPU copy of matrixData
4549
- /** @type {WebGLBuffer|undefined} */
4550
- this.colorBuffer = undefined; // and of colorData
4551
- this.bufferGeneration = -1; // the context they were made under
4552
- this.uvTileInfo = tileInfo; // the tile the uv rects were written for
4553
- const uv = render3DGetTileUVs(tileInfo), matrices = this.matrixData, colors = this.colorData;
4554
- for (let i = 0; i < count; ++i)
4555
- {
4556
- matrices.set(RENDER3D_IDENTITY.m, i * RENDER3D_MATRIX_FLOATS);
4557
- const k = i * RENDER3D_COLOR_FLOATS;
4558
- colors[k] = color.r; colors[k+1] = color.g; colors[k+2] = color.b; colors[k+3] = color.a;
4559
- colors[k+4] = uv.x; colors[k+5] = uv.y; colors[k+6] = uv.w; colors[k+7] = uv.h;
4560
- }
4561
- }
4562
-
4563
- /** Place an instance by its position, rotation and scale, what buildMatrix takes, written straight in with no
4564
- * matrix made; in world space
4565
- * @param {number} i
4566
- * @param {Vector3} [pos]
4567
- * @param {Vector3} [rotation] - Euler angles in radians
4568
- * @param {Vector3} [scale] */
4569
- setTransformAt(i, pos, rotation, scale)
4570
- {
4571
- ASSERT(i >= 0 && i < this.maxCount, 'instance index out of range');
4572
- matrix4Compose(this.matrixData, i * RENDER3D_MATRIX_FLOATS, pos, rotation, scale);
4573
- this.markDirty(i);
4574
- }
4575
-
4576
- /** Place a run of instances from arrays indexed by instance, plain or typed, as a game keeps them: each is placed
4577
- * as setTransformAt places it, all in one loop with nothing made, the fastest way to move many every frame;
4578
- * in world space
4579
- * @param {number} start - First instance
4580
- * @param {number} count - How many
4581
- * @param {ArrayLike<number>} x - Positions
4582
- * @param {ArrayLike<number>} y
4583
- * @param {ArrayLike<number>} z
4584
- * @param {ArrayLike<number>} [rx] - Euler angles in radians, all three or none for upright
4585
- * @param {ArrayLike<number>} [ry]
4586
- * @param {ArrayLike<number>} [rz]
4587
- * @param {ArrayLike<number>} [sx] - Scales, all three or none for 1
4588
- * @param {ArrayLike<number>} [sy]
4589
- * @param {ArrayLike<number>} [sz] */
4590
- setTransforms(start, count, x, y, z, rx, ry, rz, sx, sy, sz)
4591
- {
4592
- ASSERT(start >= 0 && count >= 0 && start + count <= this.maxCount, 'instance range out of range');
4593
- ASSERT(!rx === !ry && !rx === !rz && !sx === !sy && !sx === !sz, 'rotations and scales come in threes');
4594
- const m = this.matrixData, end = start + count;
4595
- for (let i = start; i < end; ++i)
4596
- {
4597
- // matrix4Compose's expressions, so a run placed here is the same as each placed by setTransformAt
4598
- const k = i * RENDER3D_MATRIX_FLOATS;
4599
- const scaleX = sx ? sx[i] : 1, scaleY = sy ? sy[i] : 1, scaleZ = sz ? sz[i] : 1;
4600
- if (rx)
4601
- {
4602
- const cx = cos(rx[i]), sinX = sin(rx[i]);
4603
- const cy = cos(ry[i]), sinY = sin(ry[i]);
4604
- const cz = cos(rz[i]), sinZ = sin(rz[i]);
4605
- m[k] = (cy*cz + sinY*sinX*sinZ) * scaleX; m[k+1] = cx*sinZ * scaleX;
4606
- m[k+2] = (-sinY*cz + cy*sinX*sinZ) * scaleX;
4607
- m[k+4] = (-cy*sinZ + sinY*sinX*cz) * scaleY; m[k+5] = cx*cz * scaleY;
4608
- m[k+6] = (sinY*sinZ + cy*sinX*cz) * scaleY;
4609
- m[k+8] = sinY*cx * scaleZ; m[k+9] = -sinX * scaleZ; m[k+10] = cy*cx * scaleZ;
4610
- }
4611
- else
4612
- {
4613
- m[k] = scaleX; m[k+1] = 0; m[k+2] = 0;
4614
- m[k+4] = 0; m[k+5] = scaleY; m[k+6] = 0;
4615
- m[k+8] = 0; m[k+9] = 0; m[k+10] = scaleZ;
4616
- }
4617
- m[k+3] = m[k+7] = m[k+11] = 0; m[k+15] = 1;
4618
- m[k+12] = x[i], m[k+13] = y[i], m[k+14] = z[i];
4619
- }
4620
- count && this.markDirtyRange(start, end);
4621
- }
4622
-
4623
- /** Place an instance, in world space
4624
- * @param {number} i
4625
- * @param {Matrix4} matrix */
4626
- setMatrixAt(i, matrix)
4627
- {
4628
- ASSERT(i >= 0 && i < this.maxCount, 'instance index out of range');
4629
- const data = this.matrixData, k = i * RENDER3D_MATRIX_FLOATS, m = matrix.m;
4630
- for (let j = 0; j < 16; ++j)
4631
- data[k+j] = m[j]; // for 16 floats a loop beats set's call
4632
- this.markDirty(i);
4633
- }
4634
-
4635
- /** The matrix of an instance
4636
- * @param {number} i
4637
- * @return {Matrix4} */
4638
- getMatrixAt(i)
4639
- {
4640
- ASSERT(i >= 0 && i < this.maxCount, 'instance index out of range');
4641
- const k = i * RENDER3D_MATRIX_FLOATS, matrix = new Matrix4;
4642
- matrix.m.set(this.matrixData.subarray(k, k + 16));
4643
- return matrix;
4644
- }
4645
-
4646
- /** Color an instance
4647
- * @param {number} i
4648
- * @param {Color} color */
4649
- setColorAt(i, color)
4650
- {
4651
- ASSERT(i >= 0 && i < this.maxCount, 'instance index out of range');
4652
- ASSERT(isColor(color), 'color must be a Color');
4653
- const data = this.colorData, k = i * RENDER3D_COLOR_FLOATS;
4654
- data[k] = color.r; data[k+1] = color.g; data[k+2] = color.b; data[k+3] = color.a;
4655
- this.markColorDirty(i);
4656
- }
4657
-
4658
- /** Note that an instance's matrix changed, so it uploads before the next draw and the bounds take it in when
4659
- * they are read; setTransformAt and setMatrixAt call this, and so must an edit made straight to matrixData
4660
- * @param {number} i */
4661
- markDirty(i)
4662
- {
4663
- if (i < this.dirtyStart) this.dirtyStart = i;
4664
- if (i >= this.dirtyEnd) this.dirtyEnd = i + 1;
4665
- if (i < this.boundsStart) this.boundsStart = i;
4666
- if (i >= this.boundsEnd) this.boundsEnd = i + 1;
4667
- }
4668
-
4669
- /** Note that a run of instances' matrices changed, for code that writes matrixData directly, the lowest level
4670
- * way to move many: they upload before the next draw and the bounds take them in when they are read
4671
- * @param {number} start - First instance
4672
- * @param {number} end - One past the last */
4673
- markDirtyRange(start, end)
4674
- {
4675
- ASSERT(start >= 0 && end <= this.maxCount && start <= end, 'instance range out of range');
4676
- if (start < this.dirtyStart) this.dirtyStart = start;
4677
- if (end > this.dirtyEnd) this.dirtyEnd = end;
4678
- if (start < this.boundsStart) this.boundsStart = start;
4679
- if (end > this.boundsEnd) this.boundsEnd = end;
4680
- }
4681
-
4682
- /** Note that an instance's color changed, so it uploads before the next draw; setColorAt calls this, and so must
4683
- * an edit made straight to colorData
4684
- * @param {number} i */
4685
- markColorDirty(i)
4686
- {
4687
- if (i < this.colorDirtyStart) this.colorDirtyStart = i;
4688
- if (i >= this.colorDirtyEnd) this.colorDirtyEnd = i + 1;
4689
- }
4690
-
4691
- /** Radius of the sphere around the origin that holds every instance set so far, for culling: the farthest
4692
- * instance, and the mesh's size at the largest scale; it only grows, and takes in the instances set since it
4693
- * was last read, so with frustum culling off it is never worked out. Set it to fix the sphere, which a big set
4694
- * that is culled can use to skip that work, and set it to undefined to go back to the bounds
4695
- * @return {number} */
4696
- get radius()
4697
- {
4698
- if (this.fixedRadius !== undefined) return this.fixedRadius;
4699
- if (this.boundsEnd > this.boundsStart)
4700
- {
4701
- // the bounds grow to hold where each is now and how big, read back from its matrix, squared
4702
- const d = this.matrixData;
4703
- let reachSquared = this.reachSquared, scaleSquared = this.scaleSquared;
4704
- for (let i = this.boundsStart; i < this.boundsEnd; ++i)
4705
- {
4706
- const k = i * RENDER3D_MATRIX_FLOATS, x = d[k+12], y = d[k+13], z = d[k+14];
4707
- const reach = x*x + y*y + z*z, scale = render3DMaxStretchSquared(d, k);
4708
- if (reach > reachSquared) reachSquared = reach;
4709
- if (scale > scaleSquared) scaleSquared = scale;
4710
- }
4711
- this.reachSquared = reachSquared, this.scaleSquared = scaleSquared;
4712
- this.boundsStart = Infinity, this.boundsEnd = 0;
4713
- }
4714
- const mesh = this.mesh, meshRadius = mesh.radius || mesh.computeRadius(); // the mesh measured once
4715
- return this.reachSquared ** .5 + meshRadius * this.scaleSquared ** .5;
4716
- }
4717
- set radius(radius) { this.fixedRadius = radius; }
4718
-
4719
- /** Draws every instance as one call, uploading the ones that changed first
4720
- * @return {void} */
4721
- render3D()
4722
- {
4723
- const r = render3D, gl = glContext, mesh = this.mesh;
4724
- ASSERT(this.count >= 0 && this.count <= this.maxCount, 'count must be within the count it was made with');
4725
- if (r.transparentQueue) // sorted as one thing with the other transparent draws
4726
- return r.queueTransparent(this.getWorldPos3D(), ()=> this.render3D());
4727
- if (!mesh || !this.count || !render3DCanDraw()) return;
4728
- if (r.shadowPass && !r.lighting) return; // unlit things cast no shadow
4729
- render3DMeshUpload(mesh);
4730
- if (!mesh.bufferCount) return;
4731
- if (r.frustumCulling && !render3DSphereVisible(0, 0, 0, this.radius)) return;
4732
-
4733
- // the uv rect is the object's tile for every instance, rewritten when the tile changes
4734
- if (this.uvTileInfo !== this.tileInfo)
4735
- {
4736
- const uv = render3DGetTileUVs(this.tileInfo), data = this.colorData;
4737
- for (let k = 4; k < data.length; k += RENDER3D_COLOR_FLOATS)
4738
- data[k] = uv.x, data[k+1] = uv.y, data[k+2] = uv.w, data[k+3] = uv.h;
4739
- this.uvTileInfo = this.tileInfo;
4740
- this.colorDirtyStart = 0, this.colorDirtyEnd = this.maxCount;
4741
- }
4742
-
4743
- // the GPU copies: all of each under a fresh context, otherwise just the changed ranges
4744
- if (!this.buffer || this.bufferGeneration !== r.contextGeneration)
4745
- {
4746
- this.bufferGeneration = r.contextGeneration;
4747
- gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer = gl.createBuffer());
4748
- gl.bufferData(gl.ARRAY_BUFFER, this.matrixData, gl.DYNAMIC_DRAW);
4749
- gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer = gl.createBuffer());
4750
- gl.bufferData(gl.ARRAY_BUFFER, this.colorData, gl.DYNAMIC_DRAW);
4751
- }
4752
- else
4753
- {
4754
- const upload = (buffer, data, floats, start, end)=>
4755
- {
4756
- if (end <= start) return;
4757
- gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
4758
- gl.bufferSubData(gl.ARRAY_BUFFER, start * floats * 4, data, start * floats, (end - start) * floats);
4759
- };
4760
- upload(this.buffer, this.matrixData, RENDER3D_MATRIX_FLOATS, this.dirtyStart, this.dirtyEnd);
4761
- upload(this.colorBuffer, this.colorData, RENDER3D_COLOR_FLOATS, this.colorDirtyStart, this.colorDirtyEnd);
4762
- }
4763
- this.dirtyStart = this.colorDirtyStart = Infinity, this.dirtyEnd = this.colorDirtyEnd = 0;
4764
-
4765
- // one draw under the object's state, the mesh setting the culling as drawMesh does
4766
- r.flush();
4767
- render3DFlushBeforeOverlay(); // as in drawMesh, what was drawn before goes under it
4768
- const cullBackFaces = r.cullBackFaces, tileInfo = this.tileInfo;
4769
- r.cullBackFaces = !mesh.doubleSided;
4770
- render3DDrawInstanced(mesh, this.buffer, this.count, render3DTextureOf(tileInfo), r, this.colorBuffer);
4771
- r.cullBackFaces = cullBackFaces;
4772
- }
4773
-
4774
- /** Destroy the set and free its GPU buffer
4775
- * @param {boolean} [immediate] */
4776
- destroy(immediate)
4777
- {
4778
- if (this.buffer && this.bufferGeneration === render3D?.contextGeneration)
4779
- glContext?.deleteBuffer(this.buffer), glContext?.deleteBuffer(this.colorBuffer);
4780
- this.buffer = this.colorBuffer = undefined;
4781
- super.destroy(immediate);
4782
- }
4783
- }
4784
-
4785
- ///////////////////////////////////////////////////////////////////////////////
4786
- /**
4787
- * Light3D - A light that is an EngineObject3D, so it can move, follow a parent or be destroyed like anything else
4788
- * - A point light: it lights what is near it and fades out by its radius, DirectionalLight3D shines from far away
4789
- * - A coneAngle makes it a spotlight: it shines along its own forward, turned by rotation3D or by what it is
4790
- * attached to, inside the cone, fading over the part of it coneSoftness says
4791
- * - The sun, render3D.sunDirection, casts the shadows; a spotlight can cast them in its place, see
4792
- * render3D.shadowLight; the other lights light and make highlights without one
4793
- * - Only the 8 lights nearest the camera are used each frame
4794
- * - radius is where the light fades out, and it fades fast, so a small radius wants a higher intensity
4795
- * - intensity multiplies the color, above 1 for a light brighter than white
4796
- * - radius is a world distance, so scale3D does not change it
4797
- * - An alpha, an intensity or a radius of 0 switches it off, and a light that is off takes none of those slots
4798
- * - Draws nothing but its glow, when it has one; add a small emissive mesh if the lamp itself should be seen
4799
- * - addFlare gives it a lens flare, see LensFlare3D
4800
- * @extends EngineObject3D
4801
- * @memberof Render3D
4802
- * @example
4803
- * const torch = new Light3D(vec3(0, 3, 0), 10, hsl(.1, 1, .65));
4804
- * torch.addFlare(); // light in the lens when the torch is in view
4805
- */
4806
- class Light3D extends EngineObject3D
4807
- {
4808
- /** Create a point light
4809
- * @param {Vector3} [pos3D] - Where it is
4810
- * @param {number} [radius] - Distance where the light fades to nothing
4811
- * @param {Color} [color] - Light color, its alpha fades it
4812
- * @param {number} [intensity] - Brightness, multiplies the color, above 1 is brighter than white */
4813
- constructor(pos3D=vec3(), radius=5, color=WHITE, intensity=1)
4814
- {
4815
- super(pos3D, undefined, undefined, color);
4816
- ASSERT(radius >= 0, 'light radius cannot be negative, 0 is an off switch like an alpha of 0');
4817
- ASSERT(intensity >= 0, 'light intensity cannot be negative, 0 is an off switch');
4818
- this.size3D = vec3(); // not a solid thing to pick or collect
4819
- /** @property {number} - Distance where the light fades to nothing */
4820
- this.radius = radius;
4821
- /** @property {number} - Brightness, multiplies the color, above 1 is brighter than white */
4822
- this.intensity = intensity;
4823
- /** @property {boolean} - Shine from far away, from its position toward the origin, instead of out from its
4824
- * position with a falloff; DirectionalLight3D sets it */
4825
- this.directional = false;
4826
- /** @property {number} - Makes it a spotlight: the angle in radians from its forward out to the edge of its
4827
- * cone, so the beam is twice this across; 0 for a light that shines every way */
4828
- this.coneAngle = 0;
4829
- /** @property {number} - How much of the cone is its fading edge: 0 a hard edge, .2 by default, the outer
4830
- * fifth, 1 fading all the way from the middle of the beam */
4831
- this.coneSoftness = .2;
4832
- /** @property {number} - Size of a soft hazy glow drawn over the light, like a lamp at night, 0 for none; it
4833
- * is added onto what is behind it, and what is in front of the light hides it */
4834
- this.glow = 0;
4835
- /** @property {number} - How fast the glow fades from its middle: 1 by default, .5 a wide haze, 2 a tight
4836
- * bright core */
4837
- this.glowFalloff = 1;
4838
- /** @property {TextureInfo|TileInfo|undefined} - A gel: a picture the light shines through, like a stained glass
4839
- * window or the leaves of a tree, cast along its cone in its colors, upright as the light looks out, the whole
4840
- * texture of a TileInfo; only the spotlight that casts the shadows has one, with render3D.shadows on and it
4841
- * as render3D.shadowLight, and only on what takes its shadows: an object with receiveShadow off is lit
4842
- * without the gel; its alpha is not read, see through panes are dark
4843
- * @type {TextureInfo|TileInfo|undefined} */
4844
- this.gel = undefined;
4845
- this.additive = true; // the glow is added on, in the transparent stage; a light with none draws nothing
4846
- /** @type {LensFlare3D|undefined} */
4847
- this.flareObject = undefined;
4848
- }
4849
-
4850
- /** Give the light a lens flare, made with the arguments of LensFlare3D, in place of the one it had
4851
- * @param {number} [size] - Scales every part of it
4852
- * @param {number} [count] - How many ghosts there are, besides the glow at the light
4853
- * @param {number} [intensity] - How bright it is
4854
- * @param {number} [saturation] - How colorful the ghosts are
4855
- * @param {Color} [color] - Tints the flare, with the light's own color
4856
- * @return {LensFlare3D} - The flare, to change: light.addFlare().shapes = ['hex'] */
4857
- addFlare(size, count, intensity, saturation, color)
4858
- {
4859
- const flare = new LensFlare3D(size, count, intensity, saturation, color);
4860
- this.flare = flare;
4861
- return flare;
4862
- }
4863
-
4864
- /** The light's lens flare, undefined for none: addFlare makes it, or set a LensFlare3D of your own, and
4865
- * destroying the flare takes it away
4866
- * - The flare is the light's: it is attached to the light as its child, so it stays through a scene change
4867
- * when the light does, and is destroyed with the light or when another takes its place
4868
- * @type {LensFlare3D|undefined} */
4869
- get flare() { return this.flareObject && !this.flareObject.destroyed ? this.flareObject : undefined; }
4870
-
4871
- set flare(flare)
4872
- {
4873
- const old = this.flare; // a flare that was destroyed on its own is no flare
4874
- if (flare === old) return;
4875
- old && old.destroy();
4876
- if (flare)
4877
- {
4878
- flare.light = this;
4879
- flare.parent || this.addChild(flare);
4880
- }
4881
- this.flareObject = flare || undefined;
4882
- }
4883
-
4884
- /** Destroy the light, and its flare with it
4885
- * @param {boolean} [immediate] */
4886
- destroy(immediate)
4887
- {
4888
- this.flare = undefined;
4889
- super.destroy(immediate);
4890
- }
4891
-
4892
- /** Draw the glow, a quad facing the camera with a soft round glow on it, pulled toward the camera by half its
4893
- * size so a lamp at the light does not cut into it, but never past the camera */
4894
- render3D()
4895
- {
4896
- if (!(this.glow > 0) || this.directional) return;
4897
- const r = render3D, c = this.color, pos = render3DObjectMatrix(this).getTranslation();
4898
- const toCamera = r.camera.pos.subtract(pos), distance = toCamera.length();
4899
- const at = distance ? pos.add(toCamera.scale(min(this.glow, distance) / 2 / distance)) : pos;
4900
- const color = rgb(c.r, c.g, c.b, c.a * min(this.intensity, 1)), texture = engineGlowTexture(this.glowFalloff);
4901
- // a browser that can not make the glow's texture draws no glow, not a plain square
4902
- if (texture || !glContext)
4903
- r.drawBillboard(at, vec2(this.glow), texture, color);
4904
- }
4905
- }
4906
-
4907
- ///////////////////////////////////////////////////////////////////////////////
4908
- /**
4909
- * DirectionalLight3D - A Light3D that shines from far away with no falloff, like sunlight
4910
- * - It shines from its position toward the origin, like a three.js DirectionalLight: only the direction to it
4911
- * counts, so moving it or its parent swings the light around; parent it to a sun in the sky and it follows
4912
- * - It cannot sit on the origin, since that leaves no direction
4913
- * - It casts no shadow: the sun, render3D.sunDirection, does, or a spotlight set as render3D.shadowLight
4914
- * @extends Light3D
4915
- * @memberof Render3D
4916
- * @example
4917
- * const fill = new DirectionalLight3D(vec3(-1, 1, 1), hsl(.6, .5, .3)); // from the back left and above
4918
- */
4919
- class DirectionalLight3D extends Light3D
4920
- {
4921
- /** Create a directional light
4922
- * @param {Vector3} [pos3D] - Where it shines from, toward the origin
4923
- * @param {Color} [color] - Light color, its alpha fades it
4924
- * @param {number} [intensity] - Brightness, multiplies the color, above 1 is brighter than white */
4925
- constructor(pos3D=vec3(0, 1, 0), color=WHITE, intensity=1)
4926
- {
4927
- super(pos3D, 0, color, intensity);
4928
- this.directional = true;
4929
- }
4930
- }