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