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