littlejsengine 1.23.0 → 1.24.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 +234 -0
- package/COPYRIGHT.txt +38 -38
- package/EDITOR.md +359 -0
- package/FAQ.md +4 -9
- package/README.md +14 -8
- package/REFERENCE.md +2327 -0
- package/dist/littlejs.d.ts +149 -104
- package/dist/littlejs.esm.js +1652 -1332
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +1651 -1328
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +1553 -1294
- package/package.json +5 -3
- package/plugins/gltf.js +29 -13
- package/plugins/pluginExport.js +0 -4
- package/plugins/postProcess.js +5 -2
- package/plugins/render3d.js +136 -31
- package/plugins/render3dEditor.js +51 -17
- package/plugins/render3dExtras.js +166 -70
- package/plugins/render3dLevel.js +52 -6
- package/plugins/textureSheet.js +1 -0
- package/plugins/tweenSystem.js +39 -34
- package/plugins/uiSystem.js +7 -3
- package/src/engine.js +1 -1
- package/src/engineAudio.js +9 -3
- package/src/engineBuild.mjs +17 -3
- package/src/engineDraw.js +11 -7
- package/src/engineEditor.js +52 -18
- package/src/engineExport.js +1 -0
- package/src/engineObject.js +675 -675
- package/src/engineRelease.js +5 -1
- package/src/engineTileLayer.js +119 -1
- package/src/engineUtilities.js +291 -291
- package/src/engineWebGL.js +12 -3
- package/plugins/zzfxm.js +0 -160
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "littlejsengine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.0",
|
|
4
4
|
"description": "LittleJS - Tiny and Fast HTML5 Game Engine",
|
|
5
5
|
"main": "dist/littlejs.esm.js",
|
|
6
6
|
"types": "dist/littlejs.d.ts",
|
|
@@ -39,7 +39,10 @@
|
|
|
39
39
|
"plugins/",
|
|
40
40
|
"!plugins/box2d.wasm.*",
|
|
41
41
|
"COPYRIGHT.txt",
|
|
42
|
-
"FAQ.md"
|
|
42
|
+
"FAQ.md",
|
|
43
|
+
"CHANGELOG.md",
|
|
44
|
+
"REFERENCE.md",
|
|
45
|
+
"EDITOR.md"
|
|
43
46
|
],
|
|
44
47
|
"scripts": {
|
|
45
48
|
"build": "node src/engineBuild.mjs",
|
|
@@ -53,7 +56,6 @@
|
|
|
53
56
|
},
|
|
54
57
|
"devDependencies": {
|
|
55
58
|
"bestzip": "^2.2.1",
|
|
56
|
-
"clean-jsdoc-theme": "~4.2.9",
|
|
57
59
|
"electron": "^38.2.0",
|
|
58
60
|
"electron-packager": "^17.1.2",
|
|
59
61
|
"google-closure-compiler": "~20230502.0.0",
|
package/plugins/gltf.js
CHANGED
|
@@ -405,6 +405,11 @@ async function parseGLTF(data, baseUrl='')
|
|
|
405
405
|
for (const name of json.extensionsRequired || [])
|
|
406
406
|
if (name === 'KHR_draco_mesh_compression' || name === 'EXT_meshopt_compression')
|
|
407
407
|
throw new Error(`glTF with ${name} is not read, export it uncompressed`);
|
|
408
|
+
else if (name === 'KHR_texture_basisu')
|
|
409
|
+
throw new Error('glTF with KHR_texture_basisu textures is not read, export them as png or jpeg');
|
|
410
|
+
// one that only may use them has other images to fall back on, or none: said, since it loads with them left out
|
|
411
|
+
if (json.extensionsUsed?.includes('KHR_texture_basisu') && !json.extensionsRequired?.includes('KHR_texture_basisu'))
|
|
412
|
+
console.warn('glTF: KHR_texture_basisu textures are not read and are left out, export them as png or jpeg');
|
|
408
413
|
|
|
409
414
|
// the buffers: the GLB's own, a data uri, or a file beside the model
|
|
410
415
|
const buffers = await Promise.all((json.buffers || []).map((buffer, i)=>
|
|
@@ -447,7 +452,8 @@ async function parseGLTF(data, baseUrl='')
|
|
|
447
452
|
const bitmap = normalTextures.has(index) ?
|
|
448
453
|
await createImageBitmap(blob, {colorSpaceConversion: 'none', premultiplyAlpha: 'none'}) :
|
|
449
454
|
opaque ? await createImageBitmap(blob, {premultiplyAlpha: 'none'}).then(gltfOpaqueImage) : await createImageBitmap(blob);
|
|
450
|
-
|
|
455
|
+
// REPEAT by default, and hard edged only when its sampler says NEAREST, not as the game's tiles are
|
|
456
|
+
return new TextureInfo(bitmap, true, [sampler.wrapS ?? 10497, sampler.wrapT ?? 10497], sampler.magFilter === 9728);
|
|
451
457
|
}
|
|
452
458
|
catch (e) { LOG('glTF image not loaded', e); }
|
|
453
459
|
}));
|
|
@@ -557,17 +563,25 @@ function gltfOpaqueImage(image)
|
|
|
557
563
|
{
|
|
558
564
|
const gl = glContext, {width, height} = image;
|
|
559
565
|
if (gl.isContextLost()) return image; // nothing to read back through, it keeps its alpha
|
|
560
|
-
|
|
561
|
-
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
562
|
-
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
|
|
563
|
-
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
|
564
|
-
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
|
566
|
+
// the room to read into first: for a huge image it can fail, and must not with the framebuffer bound
|
|
565
567
|
const data = new Uint8ClampedArray(width * height * 4);
|
|
566
|
-
gl.
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
568
|
+
const texture = gl.createTexture(), framebuffer = gl.createFramebuffer(), bound = gl.getParameter(gl.FRAMEBUFFER_BINDING);
|
|
569
|
+
try
|
|
570
|
+
{
|
|
571
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
572
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
|
|
573
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
|
574
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
|
575
|
+
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, data); // row 0 is the image's first row, as uploaded
|
|
576
|
+
}
|
|
577
|
+
finally
|
|
578
|
+
{
|
|
579
|
+
// the 2D renderer's own bindings back, whatever happened
|
|
580
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, bound);
|
|
581
|
+
gl.bindTexture(gl.TEXTURE_2D, glActiveTexture);
|
|
582
|
+
gl.deleteFramebuffer(framebuffer);
|
|
583
|
+
gl.deleteTexture(texture);
|
|
584
|
+
}
|
|
571
585
|
for (let i = 3; i < data.length; i += 4)
|
|
572
586
|
data[i] = 255;
|
|
573
587
|
image.close();
|
|
@@ -704,7 +718,7 @@ function gltfAccessor(json, buffers, index)
|
|
|
704
718
|
}
|
|
705
719
|
if (scale !== 1)
|
|
706
720
|
for (let i = 0; i < out.length; ++i)
|
|
707
|
-
out[i]
|
|
721
|
+
out[i] = max(out[i] / scale, -1); // a signed type's lowest value is -1 too, as the format says
|
|
708
722
|
return {data: out, components, count: a.count};
|
|
709
723
|
}
|
|
710
724
|
|
|
@@ -768,8 +782,10 @@ function gltfPart(json, buffers, textures, primitive, matrix, name)
|
|
|
768
782
|
const [er, eg, eb] = material.emissiveFactor || [0, 0, 0];
|
|
769
783
|
if (er || eg || eb)
|
|
770
784
|
{
|
|
785
|
+
// KHR_materials_emissive_strength makes it that many times brighter, emission above 1 in Blender
|
|
786
|
+
const strength = material.extensions?.KHR_materials_emissive_strength?.emissiveStrength ?? 1;
|
|
771
787
|
part.emissiveMap = emissiveRef ? textures[emissiveRef.index] : gltfWhiteTexture();
|
|
772
|
-
part.emissiveMapColor = rgb(gltfSRGB(er), gltfSRGB(eg), gltfSRGB(eb));
|
|
788
|
+
part.emissiveMapColor = rgb(gltfSRGB(er) * strength, gltfSRGB(eg) * strength, gltfSRGB(eb) * strength);
|
|
773
789
|
}
|
|
774
790
|
return part;
|
|
775
791
|
}
|
package/plugins/pluginExport.js
CHANGED
package/plugins/postProcess.js
CHANGED
|
@@ -158,8 +158,11 @@ class PostProcessPlugin
|
|
|
158
158
|
{
|
|
159
159
|
// copy main canvas to work canvas at the backing store size,
|
|
160
160
|
// mainCanvasSize is css pixels so it would lose resolution
|
|
161
|
-
|
|
162
|
-
workCanvas.height
|
|
161
|
+
// sized again only when the canvas changed, setting a size remakes the canvas even to the same one
|
|
162
|
+
if (workCanvas.width !== mainCanvas.width || workCanvas.height !== mainCanvas.height)
|
|
163
|
+
workCanvas.width = mainCanvas.width, workCanvas.height = mainCanvas.height;
|
|
164
|
+
else
|
|
165
|
+
workContext.clearRect(0, 0, workCanvas.width, workCanvas.height);
|
|
163
166
|
glCopyToContext(workContext);
|
|
164
167
|
workContext.drawImage(mainCanvas, 0, 0);
|
|
165
168
|
// clear the main canvas with clearRect, resizing it would also
|
package/plugins/render3d.js
CHANGED
|
@@ -302,17 +302,19 @@ function render3DInstance(mesh, matrix, tileInfo, color)
|
|
|
302
302
|
// make room for one more instance of a mesh under a texture and the current draw state, flushing a batch that
|
|
303
303
|
// differs first, and return where its 24 floats go in mesh.instanceData: the matrix, the tint and the uv rect
|
|
304
304
|
// drawMesh passes whether its matrix mirrors, and its batch then culls by the mesh and winds by that at the flush,
|
|
305
|
-
// so it never sets the draw state for them, which would move the state's version at every draw
|
|
306
|
-
|
|
305
|
+
// so it never sets the draw state for them, which would move the state's version at every draw; drawBillboard
|
|
306
|
+
// passes that its batch is unlit, for the same reason
|
|
307
|
+
function render3DInstanceSlot(mesh, textureInfo, mirrored, unlit=false)
|
|
307
308
|
{
|
|
308
309
|
const r = render3D;
|
|
309
310
|
if (mesh.instanceCount && (mesh.instanceTextureInfo !== textureInfo || mesh.instanceMirrored !== mirrored
|
|
310
|
-
|| render3DStateChanged(mesh.instanceState)))
|
|
311
|
+
|| mesh.instanceUnlit !== unlit || render3DStateChanged(mesh.instanceState)))
|
|
311
312
|
render3DFlushInstances(mesh);
|
|
312
313
|
if (!mesh.instanceCount)
|
|
313
314
|
{
|
|
314
315
|
mesh.instanceTextureInfo = textureInfo;
|
|
315
316
|
mesh.instanceMirrored = mirrored;
|
|
317
|
+
mesh.instanceUnlit = unlit;
|
|
316
318
|
mesh.instanceState = render3DCaptureBatchState();
|
|
317
319
|
r.instanceMeshes.push(mesh);
|
|
318
320
|
}
|
|
@@ -346,6 +348,8 @@ function render3DFlushInstances(only)
|
|
|
346
348
|
const state = mesh.instanceState;
|
|
347
349
|
if (mesh.instanceMirrored !== undefined) // a drawMesh batch: culled by its mesh, wound by its matrices
|
|
348
350
|
state.cullBackFaces = !mesh.doubleSided, state.mirrored = mesh.instanceMirrored;
|
|
351
|
+
if (mesh.instanceUnlit) // a batch of sprites
|
|
352
|
+
state.lighting = false;
|
|
349
353
|
render3DDrawInstanced(mesh, buffer, count, mesh.instanceTextureInfo, state);
|
|
350
354
|
}
|
|
351
355
|
if (!only)
|
|
@@ -702,6 +706,7 @@ class Render3DPlugin
|
|
|
702
706
|
/** @type {TextureInfo|undefined} */
|
|
703
707
|
this.streamTileInfo = undefined;
|
|
704
708
|
this.streamState = undefined; // captured state the pending batch was drawn under
|
|
709
|
+
this.streamUnlit = false; // the pending batch is drawn unlit whatever that state says, billboards are
|
|
705
710
|
/** @type {Mesh|undefined} */
|
|
706
711
|
this.capture = undefined; // the mesh a bake is filling
|
|
707
712
|
/** @type {Array<{distance: number, state: Object, draw: function(): void}>|undefined} */
|
|
@@ -910,8 +915,8 @@ class Render3DPlugin
|
|
|
910
915
|
}
|
|
911
916
|
|
|
912
917
|
/** Find the nearest object under a screen position or along a ray, for clicking on things
|
|
913
|
-
* -
|
|
914
|
-
*
|
|
918
|
+
* - A mesh is hit on its triangles, the ones that face the ray as they are drawn, both sides of a doubleSided
|
|
919
|
+
* mesh; a sprite is hit as the quad it draws, and a height map or a voxel map on its surface
|
|
915
920
|
* - engineObjectsRaycast3D is the other half of this, every object along a ray instead of the nearest
|
|
916
921
|
* @param {Vector2|Ray3D} from - A screen position like mousePosScreen, or a ray to look along
|
|
917
922
|
* @param {Array<EngineObject>} [objects] - Defaults to every object; only those with a mesh or a sprite count
|
|
@@ -1092,6 +1097,8 @@ class Render3DPlugin
|
|
|
1092
1097
|
{
|
|
1093
1098
|
if (!this.streamCount || !render3DCanDraw()) return;
|
|
1094
1099
|
const gl = glContext;
|
|
1100
|
+
if (this.streamUnlit)
|
|
1101
|
+
this.streamState.lighting = false; // its own copy of the state
|
|
1095
1102
|
render3DSetDrawUniforms(RENDER3D_IDENTITY, this.streamTileInfo, WHITE, RENDER3D_FULL_UV_RECT, this.streamState);
|
|
1096
1103
|
render3DBindVertexBuffer(this.streamBuffer);
|
|
1097
1104
|
gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.streamFloats, 0, this.streamCount * RENDER3D_VERTEX_FLOATS);
|
|
@@ -1326,13 +1333,34 @@ class Render3DPlugin
|
|
|
1326
1333
|
return this.queueTransparent(p, ()=> this.drawBillboard(p, s, tileInfo, c, angle, upright));
|
|
1327
1334
|
}
|
|
1328
1335
|
|
|
1329
|
-
//
|
|
1330
|
-
//
|
|
1331
|
-
const
|
|
1332
|
-
this.
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
+
// unlit on screen, in the shadow map the object's flag decides; the batch is told, the draw state is not
|
|
1337
|
+
// set, which would move its version at every sprite
|
|
1338
|
+
const unlit = !this.shadowPass;
|
|
1339
|
+
if (!this.blend && this.depthTest && this.instancing)
|
|
1340
|
+
{
|
|
1341
|
+
// an opaque sprite is one more instance of the shared quad, as a mesh's uses are: every sprite of a
|
|
1342
|
+
// sheet is one instanced draw at the end of the stage, with nothing written per corner; the shader
|
|
1343
|
+
// drops its see through texels, so it needs no sorting
|
|
1344
|
+
if (!render3DCanDraw() || this.shadowPass && !this.lighting) return; // unlit things cast no shadow
|
|
1345
|
+
if (this.frustumCulling && !render3DSphereVisible(pos.x, pos.y, pos.z, hypot(size.x, size.y) / 2))
|
|
1346
|
+
return;
|
|
1347
|
+
const quad = this.billboardMesh, cb = this.cameraBack, uv = render3DGetTileUVs(tileInfo);
|
|
1348
|
+
render3DMeshUpload(quad);
|
|
1349
|
+
// the quad's half axes, doubled for its unit square
|
|
1350
|
+
const a = render3DBillboardAxes(size, angle, upright);
|
|
1351
|
+
const k = render3DInstanceSlot(quad, render3DTextureOf(tileInfo), false, unlit), data = quad.instanceData;
|
|
1352
|
+
data[k] = a[0] * 2; data[k+1] = a[1] * 2; data[k+2] = a[2] * 2; data[k+3] = 0;
|
|
1353
|
+
data[k+4] = a[3] * 2; data[k+5] = a[4] * 2; data[k+6] = a[5] * 2; data[k+7] = 0;
|
|
1354
|
+
data[k+8] = cb.x; data[k+9] = cb.y; data[k+10] = cb.z; data[k+11] = 0;
|
|
1355
|
+
data[k+12] = pos.x; data[k+13] = pos.y; data[k+14] = pos.z; data[k+15] = 1;
|
|
1356
|
+
data[k+16] = color.r; data[k+17] = color.g; data[k+18] = color.b; data[k+19] = color.a;
|
|
1357
|
+
data[k+20] = uv.x; data[k+21] = uv.y; data[k+22] = uv.w; data[k+23] = uv.h;
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// one that blends is drawn in its sorted place: the quad's six stream vertices written straight in with no
|
|
1362
|
+
// vectors made, for sprites that blend, and for particles when instancing is off
|
|
1363
|
+
const uvRect = render3DBeginStrip(6, tileInfo, unlit);
|
|
1336
1364
|
if (!uvRect) return;
|
|
1337
1365
|
const a = render3DBillboardAxes(size, angle, upright);
|
|
1338
1366
|
const rx = a[0], ry = a[1], rz = a[2], ux = a[3], uy = a[4], uz = a[5];
|
|
@@ -2444,8 +2472,9 @@ function render3DRenderDepth()
|
|
|
2444
2472
|
// which way they face, so keeping the count even keeps every strip facing out.
|
|
2445
2473
|
|
|
2446
2474
|
// make room in the stream for a strip of count vertices under the current state and texture, flushing a batch that
|
|
2447
|
-
// differs first; returns the uv rect to map the vertices with, or undefined when nothing can be drawn
|
|
2448
|
-
|
|
2475
|
+
// differs first; returns the uv rect to map the vertices with, or undefined when nothing can be drawn; unlit draws
|
|
2476
|
+
// the batch unlit whatever the state says, so a billboard does not set the state and set it back at every draw
|
|
2477
|
+
function render3DBeginStrip(count, tileInfo, unlit=false)
|
|
2449
2478
|
{
|
|
2450
2479
|
const r = render3D;
|
|
2451
2480
|
if (!render3DCanDraw()) return;
|
|
@@ -2453,12 +2482,13 @@ function render3DBeginStrip(count, tileInfo)
|
|
|
2453
2482
|
ASSERT(count <= RENDER3D_MAX_STREAM_VERTS, 'strip is too large for the stream, bake it into a mesh');
|
|
2454
2483
|
if (count > RENDER3D_MAX_STREAM_VERTS) return;
|
|
2455
2484
|
const textureInfo = render3DTextureOf(tileInfo);
|
|
2456
|
-
if (r.streamCount && (textureInfo !== r.streamTileInfo ||
|
|
2457
|
-
|| r.streamCount + count > RENDER3D_MAX_STREAM_VERTS))
|
|
2485
|
+
if (r.streamCount && (textureInfo !== r.streamTileInfo || r.streamUnlit !== unlit
|
|
2486
|
+
|| render3DStateChanged(r.streamState) || r.streamCount + count > RENDER3D_MAX_STREAM_VERTS))
|
|
2458
2487
|
r.flush();
|
|
2459
2488
|
render3DFlushBeforeOverlay();
|
|
2460
2489
|
if (!r.streamCount)
|
|
2461
2490
|
r.streamState = render3DCaptureBatchState();
|
|
2491
|
+
r.streamUnlit = unlit;
|
|
2462
2492
|
r.streamTileInfo = textureInfo;
|
|
2463
2493
|
return render3DGetTileUVs(tileInfo);
|
|
2464
2494
|
}
|
|
@@ -4253,8 +4283,8 @@ function engineObjectsCollect3D(pos, size, objects=engineObjects, testCenters=fa
|
|
|
4253
4283
|
return collected;
|
|
4254
4284
|
}
|
|
4255
4285
|
|
|
4256
|
-
// how far along a ray an object is hit, or undefined for a miss;
|
|
4257
|
-
//
|
|
4286
|
+
// 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,
|
|
4287
|
+
// and a sprite as the quad it draws
|
|
4258
4288
|
function render3DRaycastObject(ray, o)
|
|
4259
4289
|
{
|
|
4260
4290
|
if (o.destroyed || !(o instanceof EngineObject3D)) return;
|
|
@@ -4271,25 +4301,55 @@ function render3DRaycastObject(ray, o)
|
|
|
4271
4301
|
const distance = raycastSphere(ray, center, radius);
|
|
4272
4302
|
if (distance === undefined) return;
|
|
4273
4303
|
|
|
4274
|
-
// the sphere
|
|
4275
|
-
//
|
|
4304
|
+
// the sphere and then the box in the mesh's own space are quick rejects, and a ray that meets the box is tested
|
|
4305
|
+
// against the triangles; the direction is not made unit length, so the distance holds in the world;
|
|
4276
4306
|
// a mesh flattened to nothing on an axis has no inverse, it is hit as a disc like a sprite
|
|
4277
4307
|
if (!matrix.determinant()) return render3DRaycastDisc(ray, center, radius);
|
|
4278
4308
|
const inverse = matrix.copy().invert(), bounds = mesh.bounds || mesh.getBounds();
|
|
4279
4309
|
const local = new Ray3D(inverse.transformPoint(ray.origin), inverse.transformDirection(ray.direction));
|
|
4280
4310
|
const hit = raycastBox(local, bounds.min.add(bounds.max).scale(.5), bounds.max.subtract(bounds.min));
|
|
4281
|
-
if (hit
|
|
4311
|
+
if (hit === undefined || !ray.direction.lengthSquared())
|
|
4312
|
+
return hit; // a ray of no length is where it starts, inside the box
|
|
4313
|
+
return render3DRaycastMesh(local, mesh);
|
|
4314
|
+
}
|
|
4282
4315
|
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4316
|
+
// how far along a ray, in the mesh's own space, its nearest triangle is hit, or undefined: the triangles that face
|
|
4317
|
+
// the ray as the pass draws them, clockwise from the front, and the back faces too of a doubleSided mesh; a strip's
|
|
4318
|
+
// 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
|
|
4319
|
+
// never hit; nothing is kept between calls, so a mesh that changed is hit as it is now
|
|
4320
|
+
function render3DRaycastMesh(ray, mesh)
|
|
4321
|
+
{
|
|
4322
|
+
const points = mesh.points, indices = mesh.indices, both = mesh.doubleSided;
|
|
4323
|
+
const ox = ray.origin.x, oy = ray.origin.y, oz = ray.origin.z;
|
|
4324
|
+
const dx = ray.direction.x, dy = ray.direction.y, dz = ray.direction.z;
|
|
4325
|
+
const count = indices ? indices.length / 3 | 0 : points.length - 2;
|
|
4326
|
+
let nearest;
|
|
4327
|
+
for (let i = 0; i < count; ++i)
|
|
4328
|
+
{
|
|
4329
|
+
// the corners clockwise from the front: an indexed list is written the other way round
|
|
4330
|
+
let a, b, c;
|
|
4331
|
+
if (indices)
|
|
4332
|
+
a = points[indices[i*3]], b = points[indices[i*3+2]], c = points[indices[i*3+1]];
|
|
4333
|
+
else
|
|
4334
|
+
a = points[i & 1 ? i + 1 : i], b = points[i & 1 ? i : i + 1], c = points[i + 2];
|
|
4335
|
+
|
|
4336
|
+
// Moller and Trumbore: the determinant is the ray along the face's normal, below zero from the front
|
|
4337
|
+
const e1x = b.x - a.x, e1y = b.y - a.y, e1z = b.z - a.z;
|
|
4338
|
+
const e2x = c.x - a.x, e2y = c.y - a.y, e2z = c.z - a.z;
|
|
4339
|
+
const hx = dy * e2z - dz * e2y, hy = dz * e2x - dx * e2z, hz = dx * e2y - dy * e2x;
|
|
4340
|
+
const det = e1x * hx + e1y * hy + e1z * hz;
|
|
4341
|
+
if (both ? !det : !(det < 0)) continue;
|
|
4342
|
+
const sx = ox - a.x, sy = oy - a.y, sz = oz - a.z;
|
|
4343
|
+
const u = (sx * hx + sy * hy + sz * hz) / det;
|
|
4344
|
+
if (!(u >= 0 && u <= 1)) continue;
|
|
4345
|
+
const qx = sy * e1z - sz * e1y, qy = sz * e1x - sx * e1z, qz = sx * e1y - sy * e1x;
|
|
4346
|
+
const v = (dx * qx + dy * qy + dz * qz) / det;
|
|
4347
|
+
if (!(v >= 0 && u + v <= 1)) continue;
|
|
4348
|
+
const t = (e2x * qx + e2y * qy + e2z * qz) / det;
|
|
4349
|
+
if (t >= 0 && !(t >= nearest))
|
|
4350
|
+
nearest = t;
|
|
4351
|
+
}
|
|
4352
|
+
return nearest;
|
|
4293
4353
|
}
|
|
4294
4354
|
|
|
4295
4355
|
// a sprite is the quad drawBillboard draws, its size3D grown by the x and y scale as the draw does, facing the camera
|
|
@@ -4667,10 +4727,12 @@ class InstancedMesh3D extends EngineObject3D
|
|
|
4667
4727
|
* - radius is a world distance, so scale3D does not change it
|
|
4668
4728
|
* - An alpha, an intensity or a radius of 0 switches it off, and a light that is off takes none of those slots
|
|
4669
4729
|
* - Draws nothing but its glow, when it has one; add a small emissive mesh if the lamp itself should be seen
|
|
4730
|
+
* - addFlare gives it a lens flare, see LensFlare3D
|
|
4670
4731
|
* @extends EngineObject3D
|
|
4671
4732
|
* @memberof Render3D
|
|
4672
4733
|
* @example
|
|
4673
4734
|
* const torch = new Light3D(vec3(0, 3, 0), 10, hsl(.1, 1, .65));
|
|
4735
|
+
* torch.addFlare(); // light in the lens when the torch is in view
|
|
4674
4736
|
*/
|
|
4675
4737
|
class Light3D extends EngineObject3D
|
|
4676
4738
|
{
|
|
@@ -4705,6 +4767,49 @@ class Light3D extends EngineObject3D
|
|
|
4705
4767
|
* bright core */
|
|
4706
4768
|
this.glowFalloff = 1;
|
|
4707
4769
|
this.additive = true; // the glow is added on, in the transparent stage; a light with none draws nothing
|
|
4770
|
+
/** @type {LensFlare3D|undefined} */
|
|
4771
|
+
this.flareObject = undefined;
|
|
4772
|
+
}
|
|
4773
|
+
|
|
4774
|
+
/** Give the light a lens flare, made with the arguments of LensFlare3D, in place of the one it had
|
|
4775
|
+
* @param {number} [size] - Scales every part of it
|
|
4776
|
+
* @param {number} [count] - How many ghosts there are, besides the glow at the light
|
|
4777
|
+
* @param {number} [intensity] - How bright it is
|
|
4778
|
+
* @param {number} [saturation] - How colorful the ghosts are
|
|
4779
|
+
* @return {LensFlare3D} - The flare, to change: light.addFlare().shapes = ['hex'] */
|
|
4780
|
+
addFlare(size, count, intensity, saturation)
|
|
4781
|
+
{
|
|
4782
|
+
const flare = new LensFlare3D(size, count, intensity, saturation);
|
|
4783
|
+
this.flare = flare;
|
|
4784
|
+
return flare;
|
|
4785
|
+
}
|
|
4786
|
+
|
|
4787
|
+
/** The light's lens flare, undefined for none: addFlare makes it, or set a LensFlare3D of your own, and
|
|
4788
|
+
* destroying the flare takes it away
|
|
4789
|
+
* - The flare is the light's: it is attached to the light as its child, so it stays through a scene change
|
|
4790
|
+
* when the light does, and is destroyed with the light or when another takes its place
|
|
4791
|
+
* @type {LensFlare3D|undefined} */
|
|
4792
|
+
get flare() { return this.flareObject && !this.flareObject.destroyed ? this.flareObject : undefined; }
|
|
4793
|
+
|
|
4794
|
+
set flare(flare)
|
|
4795
|
+
{
|
|
4796
|
+
const old = this.flare; // a flare that was destroyed on its own is no flare
|
|
4797
|
+
if (flare === old) return;
|
|
4798
|
+
old && old.destroy();
|
|
4799
|
+
if (flare)
|
|
4800
|
+
{
|
|
4801
|
+
flare.light = this;
|
|
4802
|
+
flare.parent || this.addChild(flare);
|
|
4803
|
+
}
|
|
4804
|
+
this.flareObject = flare || undefined;
|
|
4805
|
+
}
|
|
4806
|
+
|
|
4807
|
+
/** Destroy the light, and its flare with it
|
|
4808
|
+
* @param {boolean} [immediate] */
|
|
4809
|
+
destroy(immediate)
|
|
4810
|
+
{
|
|
4811
|
+
this.flare = undefined;
|
|
4812
|
+
super.destroy(immediate);
|
|
4708
4813
|
}
|
|
4709
4814
|
|
|
4710
4815
|
/** Draw the glow, a quad facing the camera with a soft round glow on it, pulled toward the camera by half its
|
|
@@ -354,7 +354,7 @@ function editor3DSceneState()
|
|
|
354
354
|
const r = render3D;
|
|
355
355
|
return {sky: r.sky, ambientColor: r.ambientColor.copy(), ambientGroundColor: r.ambientGroundColor?.copy(),
|
|
356
356
|
fogStart: r.fogStart, fogEnd: r.fogEnd, fogColor: r.fogColor?.copy(), sunDirection: r.sunDirection.copy(),
|
|
357
|
-
sunColor: r.sunColor.copy(), shadows: r.shadows};
|
|
357
|
+
sunColor: r.sunColor.copy(), shadows: r.shadows, sunFlare: !!level3DSunFlare && !level3DSunFlare.destroyed};
|
|
358
358
|
}
|
|
359
359
|
function editor3DSceneRestore(state)
|
|
360
360
|
{
|
|
@@ -369,6 +369,7 @@ function editor3DSceneRestore(state)
|
|
|
369
369
|
r.sunDirection = state.sunDirection.copy();
|
|
370
370
|
r.sunColor = state.sunColor.copy();
|
|
371
371
|
r.shadows = state.shadows;
|
|
372
|
+
level3DSceneFlare(state.sunFlare); // a flare an earlier level's scene gave the sun, never the game's own
|
|
372
373
|
}
|
|
373
374
|
|
|
374
375
|
// the scene on screen as a block, to start a level's scene from: a sky only when the dome's colors are known
|
|
@@ -390,6 +391,7 @@ function editor3DSceneFromView()
|
|
|
390
391
|
scene.fog = [editor3DRound(r.fogStart || 0), editor3DRound(r.fogEnd || 0)];
|
|
391
392
|
scene.fogColor = editor3DHex(r.fogColor || canvasClearColor);
|
|
392
393
|
scene.shadows = !!r.shadows;
|
|
394
|
+
scene.lensFlare = level3DSunHasFlare();
|
|
393
395
|
return scene;
|
|
394
396
|
}
|
|
395
397
|
|
|
@@ -528,7 +530,7 @@ function editor3DLevelLoaded(level)
|
|
|
528
530
|
const original = editor3DCopy(editor3DObjects()), originalParts = editor3DLevelParts();
|
|
529
531
|
const hash = editor3DContentHash(original, originalParts);
|
|
530
532
|
// sceneBase is the scene the game set, taken before the level's own block is applied
|
|
531
|
-
const record = {fileName: url
|
|
533
|
+
const record = {fileName: editorFileName(url, 'level3D.json'), key: url ?? 'level #' + hash, original,
|
|
532
534
|
originalParts, sceneBase: render3D ? editor3DSceneState() : undefined, hash, pending: undefined,
|
|
533
535
|
fileHandle: undefined, undo: [], redo: []};
|
|
534
536
|
editor3DRecords.set(level, record);
|
|
@@ -629,6 +631,7 @@ function editor3DChange(change)
|
|
|
629
631
|
const before = editor3DCopy(editor3DObjects()), after = editor3DCopy(before);
|
|
630
632
|
change(after);
|
|
631
633
|
if (editor3DSame(before, after)) return false;
|
|
634
|
+
editor3DFixIds(after); // an object added with no id, or with one in use, gets one of its own
|
|
632
635
|
editor3DSetObjects(after);
|
|
633
636
|
editor3DStroke ||= {before, parts: editor3DLevelParts()};
|
|
634
637
|
return true;
|
|
@@ -1402,7 +1405,7 @@ function editor3DPrefabJSON(name)
|
|
|
1402
1405
|
|
|
1403
1406
|
// the prefabs opened one inside the other, each with the level it was opened from, its name, the selection there
|
|
1404
1407
|
// and the ids of that level's objects that had something made for them
|
|
1405
|
-
/** @type {Array<{level: Object, name: string, selection: Array<number>, made: Set<number>, entered?: string
|
|
1408
|
+
/** @type {Array<{level: Object, name: string, selection: Array<number>, made: Set<number>, entered?: string}>} */
|
|
1406
1409
|
const editor3DPrefabStack = [];
|
|
1407
1410
|
// the levels that are a prefab being edited, they have no autosave of their own
|
|
1408
1411
|
const editor3DPrefabLevels = new WeakSet;
|
|
@@ -1411,6 +1414,28 @@ const editor3DPrefabLevels = new WeakSet;
|
|
|
1411
1414
|
let editor3DPrefabEdits = {};
|
|
1412
1415
|
// the prefabs of the game's own that were edited and are not in a file yet
|
|
1413
1416
|
const editor3DPrefabDirty = new Set;
|
|
1417
|
+
// what is known of each prefab's file, by its name, so it outlasts the prefab being closed and opened again: the
|
|
1418
|
+
// save being written, for the next to wait on, what the file has since a Save, and what the prefab holds now
|
|
1419
|
+
/** @type {Map<string, {saving?: Promise<any>, saved?: string, content?: string}>} */
|
|
1420
|
+
const editor3DPrefabFiles = new Map;
|
|
1421
|
+
function editor3DPrefabFile(name)
|
|
1422
|
+
{
|
|
1423
|
+
editor3DPrefabFiles.has(name) || editor3DPrefabFiles.set(name, {});
|
|
1424
|
+
return editor3DPrefabFiles.get(name);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// say if a prefab of the game's own is in its file, now that it holds this: once a Save has written it, it is
|
|
1428
|
+
// when what it holds is what was written; before any Save it is not once it was edited
|
|
1429
|
+
function editor3DPrefabFileCheck(name, content, edited=false)
|
|
1430
|
+
{
|
|
1431
|
+
const file = editor3DPrefabFile(name);
|
|
1432
|
+
file.content = content;
|
|
1433
|
+
if (level3DPrefabs.get(name)?.fromLevel) return; // one of the level's own is saved with the level
|
|
1434
|
+
if (file.saved !== undefined)
|
|
1435
|
+
content === file.saved ? editor3DPrefabDirty.delete(name) : editor3DPrefabDirty.add(name);
|
|
1436
|
+
else if (edited)
|
|
1437
|
+
editor3DPrefabDirty.add(name);
|
|
1438
|
+
}
|
|
1414
1439
|
|
|
1415
1440
|
// destroy what the level being edited has in the world, its objects' and its maps'
|
|
1416
1441
|
function editor3DPrefabClear()
|
|
@@ -1464,9 +1489,10 @@ function editor3DPrefabBack()
|
|
|
1464
1489
|
level3DPrefabSet(frame.name, prefab, fromLevel);
|
|
1465
1490
|
if (fromLevel)
|
|
1466
1491
|
editor3DPrefabEdits[frame.name] = prefab;
|
|
1467
|
-
else if (JSON.stringify(objects) !== frame.saved)
|
|
1468
|
-
editor3DPrefabDirty.add(frame.name); // not what Save wrote while it was open
|
|
1469
1492
|
}
|
|
1493
|
+
// one of the game's own is in its file or not by what it holds now, edited here or not: undone back to how it
|
|
1494
|
+
// was opened after a Save, it is not what the file has
|
|
1495
|
+
editor3DPrefabFileCheck(frame.name, JSON.stringify(objects), changed);
|
|
1470
1496
|
|
|
1471
1497
|
// the level it was opened from is the editor's again, with its own undo
|
|
1472
1498
|
const level = editor3DLevel = frame.level, record = editor3DRecords.get(level);
|
|
@@ -1794,8 +1820,10 @@ function editor3DAutosave(level=editor3DLevel, known)
|
|
|
1794
1820
|
if (known?.fromLevel && JSON.stringify(objects) !== frame.entered)
|
|
1795
1821
|
edits[frame.name] = known.attached ? {attached: true, objects} : {objects};
|
|
1796
1822
|
});
|
|
1797
|
-
|
|
1798
|
-
|
|
1823
|
+
// with nothing edited any more, an undo back to how the prefab was, the level's autosave is the level's own
|
|
1824
|
+
// again, which takes away one of an edit that was undone and keeps the level's other edits
|
|
1825
|
+
const prefabs = {...editor3DLevelPart('prefabs', root), ...edits};
|
|
1826
|
+
root && editor3DAutosave(Object.keys(prefabs).length ? {...root, prefabs} : root, editor3DRecords.get(root));
|
|
1799
1827
|
return;
|
|
1800
1828
|
}
|
|
1801
1829
|
const record = known ?? editor3DRecords.get(level);
|
|
@@ -1809,12 +1837,9 @@ function editor3DAutosave(level=editor3DLevel, known)
|
|
|
1809
1837
|
else
|
|
1810
1838
|
saves[record.key] = {hash: record.hash, savedHash: record.savedHash, objects: editor3DCopy(objects),
|
|
1811
1839
|
...parts};
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
editor3DSaveFailed = false;
|
|
1816
|
-
}
|
|
1817
|
-
catch { editor3DSaveFailed = true; }
|
|
1840
|
+
const failed = editorSaveFailed; // the 2D editor's own flag is its own
|
|
1841
|
+
editor3DSaveFailed = editorWriteSaves(saves, record.key, editor3DSaveName());
|
|
1842
|
+
editorSaveFailed = failed;
|
|
1818
1843
|
}
|
|
1819
1844
|
|
|
1820
1845
|
// put the autosaved edits of a file that changed into the level, as one undo
|
|
@@ -1871,16 +1896,23 @@ async function editor3DSave(pickAgain=false)
|
|
|
1871
1896
|
// inside a prefab, Save writes the prefab as a file of its own, or the game keeps it itself
|
|
1872
1897
|
const text = editor3DLevelJSON(level), name = open.name + '.json';
|
|
1873
1898
|
const written = JSON.stringify(editor3DObjects()); // as the file has it
|
|
1899
|
+
// its place in line is the prefab's, by its name: closed and opened again it is a new level here, and its
|
|
1900
|
+
// saves still go in the order they were asked for
|
|
1901
|
+
const file = editor3DPrefabFile(open.name), before = file.saving;
|
|
1874
1902
|
const saved = (async ()=>
|
|
1875
1903
|
{
|
|
1876
|
-
await
|
|
1904
|
+
await before;
|
|
1877
1905
|
const kept = await editorCall('onSave', text, name) === true;
|
|
1878
1906
|
kept || saveText(text, name, 'application/json');
|
|
1879
|
-
|
|
1880
|
-
|
|
1907
|
+
file.saved = written;
|
|
1908
|
+
// it is in its file when what it holds now is what was written: edited again while the save was
|
|
1909
|
+
// kept, or gone back from with more edits, it still waits
|
|
1910
|
+
const at = editor3DPrefabStack.indexOf(open);
|
|
1911
|
+
const now = at < 0 ? file.content : JSON.stringify((editor3DPrefabStack[at + 1]?.level ?? editor3DLevel).objects);
|
|
1912
|
+
editor3DPrefabFileCheck(open.name, now ?? written);
|
|
1881
1913
|
return kept ? 'kept' : 'downloaded';
|
|
1882
1914
|
})();
|
|
1883
|
-
|
|
1915
|
+
file.saving = saved.catch(()=> {});
|
|
1884
1916
|
return saved;
|
|
1885
1917
|
}
|
|
1886
1918
|
// what is written, kept as it is now: the level can change while the file is picked and written, and those
|
|
@@ -3348,6 +3380,8 @@ function editor3DSceneUpdate(box)
|
|
|
3348
3380
|
color('Fog color', 'The sky\'s horizon color when the level has a sky and no fog color',
|
|
3349
3381
|
scene.fogColor ?? (sky ? sky[1] : view.fogColor), (hex)=> change('fogColor', hex));
|
|
3350
3382
|
toggle('Shadows', 'The sun casts shadows', !!value('shadows'), (on)=> change('shadows', on));
|
|
3383
|
+
toggle('Lens flare', 'The sun flares in the lens when it is in view', !!value('lensFlare'), (on)=>
|
|
3384
|
+
change('lensFlare', on));
|
|
3351
3385
|
}
|
|
3352
3386
|
|
|
3353
3387
|
// the properties box: the position, rotation and scale of the one selected object and an input for each default
|