littlejsengine 1.23.1 → 1.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +261 -0
- package/COPYRIGHT.txt +0 -27
- package/FAQ.md +4 -9
- package/README.md +4 -3
- package/REFERENCE.md +34 -18
- package/dist/littlejs.d.ts +160 -105
- package/dist/littlejs.esm.js +490 -277
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +489 -273
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +463 -264
- package/package.json +2 -1
- package/plugins/box2d.js +1 -1
- package/plugins/pluginExport.js +0 -4
- package/plugins/render3d.js +93 -17
- package/plugins/render3dEditor.js +9 -3
- package/plugins/render3dExtras.js +169 -70
- package/plugins/render3dLevel.js +24 -4
- package/plugins/render3dVoxels.js +15 -8
- package/src/engine.js +1 -1
- package/src/engineAudio.js +2 -2
- package/src/engineBuild.mjs +0 -1
- package/src/engineEditor.js +17 -6
- package/src/engineExport.js +1 -0
- package/src/engineSettings.js +5 -1
- package/src/engineTileLayer.js +154 -1
- package/plugins/zzfxm.js +0 -162
package/dist/littlejs.release.js
CHANGED
|
@@ -35,7 +35,7 @@ const engineName = 'LittleJS';
|
|
|
35
35
|
* @type {string}
|
|
36
36
|
* @default
|
|
37
37
|
* @memberof Engine */
|
|
38
|
-
const engineVersion = '1.
|
|
38
|
+
const engineVersion = '1.24.1';
|
|
39
39
|
|
|
40
40
|
/** Frames per second to update
|
|
41
41
|
* @type {number}
|
|
@@ -3211,7 +3211,11 @@ function setGLEnable(enable)
|
|
|
3211
3211
|
/** Set how many sided polygons to use when drawing circles and ellipses with WebGL
|
|
3212
3212
|
* @param {number} sides
|
|
3213
3213
|
* @memberof Settings */
|
|
3214
|
-
function setGLCircleSides(sides)
|
|
3214
|
+
function setGLCircleSides(sides)
|
|
3215
|
+
{
|
|
3216
|
+
false&&ASSERT(isNumber(sides) && sides >= 3, 'circles need at least 3 sides');
|
|
3217
|
+
glCircleSides = sides;
|
|
3218
|
+
}
|
|
3215
3219
|
|
|
3216
3220
|
/** Set default size of tiles in pixels
|
|
3217
3221
|
* @param {Vector2} size
|
|
@@ -7557,7 +7561,7 @@ function touchGamepadPointerUp(e)
|
|
|
7557
7561
|
* - 2D spatial audio based on camera position with distance-based falloff
|
|
7558
7562
|
* - Sound instance management (pause, resume, stop)
|
|
7559
7563
|
* - Speech synthesis for text-to-speech
|
|
7560
|
-
* - Music playback
|
|
7564
|
+
* - Music playback from audio files, looping by default
|
|
7561
7565
|
* - Web Audio API integration with master gain control
|
|
7562
7566
|
* - Sounds and the master bus can route through effects, see the audio effects plugin
|
|
7563
7567
|
* @namespace Audio
|
|
@@ -8435,7 +8439,7 @@ function playAudioBuffer(buffer, volume=1, rate=1, pan=0, loop=false, gainNode,
|
|
|
8435
8439
|
}
|
|
8436
8440
|
|
|
8437
8441
|
///////////////////////////////////////////////////////////////////////////////
|
|
8438
|
-
//
|
|
8442
|
+
// ZzFX - Zuper Zmall Zound Zynth - v1.4.0 by Frank Force
|
|
8439
8443
|
|
|
8440
8444
|
/** Generate and play a ZzFX sound
|
|
8441
8445
|
*
|
|
@@ -8720,6 +8724,10 @@ const tileLayersTiledFlips = [[0,0], [3,1], [2,1], [3,0], [0,1], [1,0], [2,0], [
|
|
|
8720
8724
|
* Load tile layers from exported data
|
|
8721
8725
|
* - Tiled maps come in as they are, flipped and turned tiles included, from one tileset image (a second tileset's
|
|
8722
8726
|
* tiles continue its numbering), finite maps in the CSV or array layer format; layer offsets and parallax are not read
|
|
8727
|
+
* - A tileset kept in the map with a margin or a spacing, a sheet with gaps between its tiles, is read where its
|
|
8728
|
+
* tiles are, whatever padding the tile info has; one in a file of its own (a tsx) is not read, pass a tile info
|
|
8729
|
+
* with its padding
|
|
8730
|
+
* - An LDtk level loads through tileLayersFromLDtk, which makes it a map like these
|
|
8723
8731
|
* - Group layers are flattened in order, each replaced by the layers inside it, so the layer indices
|
|
8724
8732
|
* (collisionLayer and the returned array) count that flattened list; a group's tint, opacity and
|
|
8725
8733
|
* visibility carry to the layers inside it
|
|
@@ -8729,7 +8737,7 @@ const tileLayersTiledFlips = [[0,0], [3,1], [2,1], [3,0], [0,1], [1,0], [2,0], [
|
|
|
8729
8737
|
* @param {Object} tileMapData - Level data from exported data
|
|
8730
8738
|
* @param {TileInfo} [tileInfo] - Default tile info (used for size and texture), tile() by default, none when no image is loaded
|
|
8731
8739
|
* @param {number} [renderOrder] - Render order of the top layer
|
|
8732
|
-
* @param {number}
|
|
8740
|
+
* @param {number|string} [collisionLayer] - Layer to use for collision if any, by its index or its name
|
|
8733
8741
|
* @param {boolean} [draw] - Should the layer be drawn automatically
|
|
8734
8742
|
* @return {Array<TileCollisionLayer>}
|
|
8735
8743
|
* @memberof TileLayers */
|
|
@@ -8752,6 +8760,19 @@ function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrde
|
|
|
8752
8760
|
false&&ASSERT(tileMapData.width && tileMapData.height);
|
|
8753
8761
|
false&&ASSERT(tileMapData.layers && tileMapData.layers.length);
|
|
8754
8762
|
|
|
8763
|
+
// a sheet with a margin around its tiles or a spacing between them: the first tile is at the margin and each
|
|
8764
|
+
// cell is a tile and a spacing, which is a padding of half the spacing counted from there, by the tileset's
|
|
8765
|
+
// own count of columns, since the image's width does not say with a spacing that is not all around
|
|
8766
|
+
const tileset = tileMapData.tilesets?.[0];
|
|
8767
|
+
if (tileInfo && tileset && (tileset.margin > 0 || tileset.spacing > 0))
|
|
8768
|
+
{
|
|
8769
|
+
const margin = tileset.margin || 0, spacing = tileset.spacing || 0, size = tileInfo.size;
|
|
8770
|
+
const width = tileInfo.textureInfo?.size.x || 0;
|
|
8771
|
+
const columns = tileset.columns || max(1, floor((width - margin*2 + spacing) / (size.x + spacing)));
|
|
8772
|
+
const origin = tileInfo.pos.subtract(vec2(tileInfo.padding)); // where the sheet starts, in an atlas too
|
|
8773
|
+
tileInfo = new TileInfo(origin.add(vec2(margin)), size, tileInfo.textureInfo, spacing / 2, tileInfo.bleed, columns);
|
|
8774
|
+
}
|
|
8775
|
+
|
|
8755
8776
|
// flatten group layers in order, a group's color and visibility carry to the layers inside it
|
|
8756
8777
|
/** @type {Array<{dataLayer: Object, color?: Color, visible?: boolean}>} */
|
|
8757
8778
|
const layers = [];
|
|
@@ -8780,6 +8801,14 @@ function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrde
|
|
|
8780
8801
|
};
|
|
8781
8802
|
addLayers(tileMapData.layers, WHITE, true);
|
|
8782
8803
|
|
|
8804
|
+
// a layer named for collision is the first tile layer of that name, which stays the same when layers are added
|
|
8805
|
+
if (typeof collisionLayer === 'string')
|
|
8806
|
+
{
|
|
8807
|
+
const name = collisionLayer;
|
|
8808
|
+
collisionLayer = layers.findIndex((l)=> l.color && l.dataLayer.name === name);
|
|
8809
|
+
false&&ASSERT(collisionLayer >= 0, 'no tile layer named ' + name);
|
|
8810
|
+
}
|
|
8811
|
+
|
|
8783
8812
|
// create tile layers and fill with data
|
|
8784
8813
|
const tileLayers = [];
|
|
8785
8814
|
const levelSize = vec2(tileMapData.width, tileMapData.height);
|
|
@@ -8829,6 +8858,134 @@ function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrde
|
|
|
8829
8858
|
function tileLayersColor(hex)
|
|
8830
8859
|
{ return new Color().setHex(hex.length === 9 ? '#' + hex.slice(3) + hex.slice(1, 3) : hex); }
|
|
8831
8860
|
|
|
8861
|
+
// the Tiled property type of an LDtk field type, for the ones Tiled has a property for
|
|
8862
|
+
const tileLayersLDtkTypes = {Int: 'int', Float: 'float', Bool: 'bool', String: 'string', Multilines: 'string',
|
|
8863
|
+
Color: 'color', FilePath: 'file'};
|
|
8864
|
+
|
|
8865
|
+
/**
|
|
8866
|
+
* Make a Tiled map of a level of an LDtk project, to load with tileLayersLoad and objectLayersLoad
|
|
8867
|
+
* - Each Tiles, AutoLayer and IntGrid layer is a tile layer, the bottom one first as in Tiled; where LDtk stacks
|
|
8868
|
+
* tiles in a cell, an edge over a fill, each tile over another goes in a layer of its own just above, named with
|
|
8869
|
+
* (2), (3) and so on, and a tile LDtk draws see-through goes in one of its own with that opacity, times the
|
|
8870
|
+
* layer's, named with it, like Ground .25
|
|
8871
|
+
* - An IntGrid layer is a hidden layer of its values under its own name, for collision, whatever tiles its rules
|
|
8872
|
+
* make, which are layers over it named with tiles, like Collisions tiles; pass the name as collisionLayer, since
|
|
8873
|
+
* the stacked and see-through layers change the indices from level to level:
|
|
8874
|
+
* tileLayersLoad(map, tile(0, 16), 0, 'Collisions')
|
|
8875
|
+
* - An Entities layer is an object layer: an entity's name is its type for objectLayersAddType, it is placed at
|
|
8876
|
+
* its middle, and its Int, Float, Bool, String, Color and FilePath fields are its properties (an enum is a string)
|
|
8877
|
+
* - The tileset is the first tile layer's, with its padding and spacing; give tileLayersLoad a tile info of its image
|
|
8878
|
+
* - The level is in the project file (not saved as separate level files), its layers of one grid size; a layer of
|
|
8879
|
+
* another grid size or another tileset is left out, with a warning in debug builds, and a project with no tileset
|
|
8880
|
+
* keeps its tiles for the tile info the game gives; a project of several worlds has its levels counted in order
|
|
8881
|
+
* - Layer offsets are not read
|
|
8882
|
+
* - The level editor edits the map this returns, and saves it as a Tiled map
|
|
8883
|
+
* @param {Object} ldtk - The LDtk project, its JSON
|
|
8884
|
+
* @param {number|string} [level] - Which level, by its index or its identifier
|
|
8885
|
+
* @return {Object} - A Tiled map: width, height, tilewidth, tileheight, tilesets and layers
|
|
8886
|
+
* @example
|
|
8887
|
+
* const map = tileLayersFromLDtk(await fetchJSON('world.ldtk'), 'Level_0');
|
|
8888
|
+
* const layers = tileLayersLoad(map, tile(0, 16), 0, 'Collisions'); // its IntGrid layer is solid
|
|
8889
|
+
* objectLayersLoad(map);
|
|
8890
|
+
* @memberof TileLayers */
|
|
8891
|
+
function tileLayersFromLDtk(ldtk, level=0)
|
|
8892
|
+
{
|
|
8893
|
+
const levels = ldtk?.levels?.length ? ldtk.levels : (ldtk?.worlds || []).flatMap((w)=> w.levels || []);
|
|
8894
|
+
const data = typeof level === 'string' ? levels.find((l)=> l.identifier === level) : levels[level];
|
|
8895
|
+
if (!data)
|
|
8896
|
+
throw new Error('tileLayersFromLDtk: no level ' + level);
|
|
8897
|
+
if (!isArray(data.layerInstances))
|
|
8898
|
+
throw new Error('tileLayersFromLDtk: level ' + data.identifier + ' has no layers, levels saved as separate files are not read');
|
|
8899
|
+
const instances = data.layerInstances;
|
|
8900
|
+
const tiled = (l)=> l.__type !== 'Entities';
|
|
8901
|
+
const first = instances.find((l)=> tiled(l) && l.__tilesetDefUid != undefined) || instances.find(tiled) || instances[0];
|
|
8902
|
+
const grid = first?.__gridSize || 16;
|
|
8903
|
+
const width = first?.__cWid || ceil(data.pxWid / grid), height = first?.__cHei || ceil(data.pxHei / grid);
|
|
8904
|
+
const map = {width, height, tilewidth: grid, tileheight: grid, orientation: 'orthogonal', renderorder: 'right-down',
|
|
8905
|
+
infinite: false, layers: [], nextlayerid: 1, nextobjectid: 1};
|
|
8906
|
+
|
|
8907
|
+
// the tileset of the first layer that has one, where its tiles are in its image
|
|
8908
|
+
const tileset = (ldtk?.defs?.tilesets || []).find((t)=> t.uid === first?.__tilesetDefUid);
|
|
8909
|
+
if (tileset)
|
|
8910
|
+
map.tilesets = [{firstgid: 1, name: String(tileset.relPath || '').replace(/^.*[\\/]/, '').replace(/\.\w+$/, ''),
|
|
8911
|
+
image: tileset.relPath, imagewidth: tileset.pxWid, imageheight: tileset.pxHei,
|
|
8912
|
+
tilewidth: tileset.tileGridSize, tileheight: tileset.tileGridSize, margin: tileset.padding || 0,
|
|
8913
|
+
spacing: tileset.spacing || 0, columns: tileset.__cWid, tilecount: tileset.__cWid * tileset.__cHei}];
|
|
8914
|
+
|
|
8915
|
+
// LDtk lists the top layer first, Tiled the bottom one
|
|
8916
|
+
for (let i = instances.length; i--;)
|
|
8917
|
+
{
|
|
8918
|
+
const instance = instances[i], name = instance.__identifier, id = map.nextlayerid++;
|
|
8919
|
+
if (!tiled(instance))
|
|
8920
|
+
{
|
|
8921
|
+
const objects = (instance.entityInstances || []).map((entity)=>
|
|
8922
|
+
{
|
|
8923
|
+
// an entity is placed by its pivot, an object here by its middle
|
|
8924
|
+
const [pivotX=0, pivotY=0] = entity.__pivot || [], w = entity.width || 0, h = entity.height || 0;
|
|
8925
|
+
const properties = [];
|
|
8926
|
+
for (const field of entity.fieldInstances || [])
|
|
8927
|
+
{
|
|
8928
|
+
const type = tileLayersLDtkTypes[field.__type] || (/^(Local|Extern)Enum\./.test(field.__type) && 'string');
|
|
8929
|
+
type && field.__value != undefined && properties.push({name: field.__identifier, type, value: field.__value});
|
|
8930
|
+
}
|
|
8931
|
+
return {id: map.nextobjectid++, name: '', type: entity.__identifier, x: entity.px[0] + (.5 - pivotX) * w,
|
|
8932
|
+
y: entity.px[1] + (.5 - pivotY) * h, width: w, height: h, rotation: 0, visible: true, properties};
|
|
8933
|
+
});
|
|
8934
|
+
map.layers.push({id, name, type: 'objectgroup', objects, opacity: 1, visible: true, x: 0, y: 0});
|
|
8935
|
+
continue;
|
|
8936
|
+
}
|
|
8937
|
+
const tiles = [...(instance.autoLayerTiles || []), ...(instance.gridTiles || [])];
|
|
8938
|
+
const ownTiles = !tiles.length || !tileset ||
|
|
8939
|
+
instance.__tilesetDefUid === tileset.uid && tileset.tileGridSize === grid;
|
|
8940
|
+
if (instance.__gridSize !== grid || instance.__cWid !== width || instance.__cHei !== height || !ownTiles)
|
|
8941
|
+
{
|
|
8942
|
+
debug && console.warn(`tileLayersFromLDtk: layer ${name} has another grid or tileset, left out`);
|
|
8943
|
+
continue;
|
|
8944
|
+
}
|
|
8945
|
+
// an IntGrid layer's values, hidden, under its own name, for collision whatever its tiles do
|
|
8946
|
+
const empty = ()=> new Array(width * height).fill(0), opacity = instance.__opacity ?? 1;
|
|
8947
|
+
const values = instance.__type === 'IntGrid';
|
|
8948
|
+
if (values)
|
|
8949
|
+
{
|
|
8950
|
+
const data = empty();
|
|
8951
|
+
(instance.intGridCsv || []).forEach((value, k)=> k < data.length && (data[k] = value));
|
|
8952
|
+
map.layers.push({id, name, type: 'tilelayer', width, height, data, opacity, visible: false, x: 0, y: 0});
|
|
8953
|
+
if (!tiles.length) continue;
|
|
8954
|
+
}
|
|
8955
|
+
|
|
8956
|
+
// a layer for each depth in a cell and each opacity, the first tiles drawn solid first
|
|
8957
|
+
const tilesName = values ? name + ' tiles' : name;
|
|
8958
|
+
const stacks = [{depth: 0, alpha: 1, data: empty()}], stack = (depth, alpha)=>
|
|
8959
|
+
{
|
|
8960
|
+
let s = stacks.find((s)=> s.depth === depth && s.alpha === alpha);
|
|
8961
|
+
s || stacks.push(s = {depth, alpha, data: empty()});
|
|
8962
|
+
return s.data;
|
|
8963
|
+
};
|
|
8964
|
+
{
|
|
8965
|
+
// a tile's place in pixels, its tile in the sheet, its flips, bit 0 across and bit 1 down, which are
|
|
8966
|
+
// Tiled's two top bits, and its opacity; LDtk lists them in the order it draws them, so the first in a
|
|
8967
|
+
// cell is the bottom one and each after it goes a layer higher
|
|
8968
|
+
const depth = new Uint16Array(width * height);
|
|
8969
|
+
for (const t of tiles)
|
|
8970
|
+
{
|
|
8971
|
+
const x = floor(t.px[0] / grid), y = floor(t.px[1] / grid), cell = x + y * width;
|
|
8972
|
+
if (x >= 0 && x < width && y >= 0 && y < height)
|
|
8973
|
+
stack(depth[cell]++, round(clamp(t.a ?? 1) * 1e3) / 1e3)[cell] =
|
|
8974
|
+
(t.t + 1 | (t.f & 1 ? 0x80000000 : 0) | (t.f & 2 ? 0x40000000 : 0)) >>> 0;
|
|
8975
|
+
}
|
|
8976
|
+
}
|
|
8977
|
+
const visible = instance.visible !== false;
|
|
8978
|
+
stacks.sort((a, b)=> a.depth - b.depth || b.alpha - a.alpha);
|
|
8979
|
+
for (const {depth, alpha, data} of stacks)
|
|
8980
|
+
{
|
|
8981
|
+
const layerName = tilesName + (depth ? ` (${depth + 1})` : '') + (alpha < 1 ? ' ' + String(alpha).replace(/^0/, '') : '');
|
|
8982
|
+
map.layers.push({id: values || depth || alpha < 1 ? map.nextlayerid++ : id, name: layerName, type: 'tilelayer',
|
|
8983
|
+
width, height, data, opacity: opacity * alpha, visible, x: 0, y: 0});
|
|
8984
|
+
}
|
|
8985
|
+
}
|
|
8986
|
+
return map;
|
|
8987
|
+
}
|
|
8988
|
+
|
|
8832
8989
|
///////////////////////////////////////////////////////////////////////////////
|
|
8833
8990
|
// Object layers
|
|
8834
8991
|
|
|
@@ -13570,166 +13727,6 @@ class Light extends EngineObject
|
|
|
13570
13727
|
}
|
|
13571
13728
|
}
|
|
13572
13729
|
|
|
13573
|
-
/**
|
|
13574
|
-
* LittleJS ZzFXM Plugin
|
|
13575
|
-
* - A port of ZzFXM, the Zuper Zmall Zound Zynth music player, by Keith Clark and Frank Force, MIT licensed,
|
|
13576
|
-
* https://github.com/keithclark/ZzFXM; its notice is in COPYRIGHT.txt
|
|
13577
|
-
* @namespace ZzFXM
|
|
13578
|
-
*/
|
|
13579
|
-
|
|
13580
|
-
/**
|
|
13581
|
-
* Music Object - Stores a zzfx music track for later use
|
|
13582
|
-
*
|
|
13583
|
-
* <a href=https://keithclark.github.io/ZzFXM/>Create music with the ZzFXM tracker.</a>
|
|
13584
|
-
* @extends Sound
|
|
13585
|
-
* @memberof ZzFXM
|
|
13586
|
-
* @example
|
|
13587
|
-
* // create some music
|
|
13588
|
-
* const music_example = new ZzFXMusic(
|
|
13589
|
-
* [
|
|
13590
|
-
* [ // instruments
|
|
13591
|
-
* [,0,400] // simple note
|
|
13592
|
-
* ],
|
|
13593
|
-
* [ // patterns
|
|
13594
|
-
* [ // pattern 1
|
|
13595
|
-
* [ // channel 0
|
|
13596
|
-
* 0, -1, // instrument 0, left speaker
|
|
13597
|
-
* 1, 0, 9, 1 // channel notes
|
|
13598
|
-
* ],
|
|
13599
|
-
* [ // channel 1
|
|
13600
|
-
* 0, 1, // instrument 0, right speaker
|
|
13601
|
-
* 0, 12, 17, -1 // channel notes
|
|
13602
|
-
* ]
|
|
13603
|
-
* ],
|
|
13604
|
-
* ],
|
|
13605
|
-
* [0, 0, 0, 0], // sequence, play pattern 0 four times
|
|
13606
|
-
* 90 // BPM
|
|
13607
|
-
* ]);
|
|
13608
|
-
*
|
|
13609
|
-
* // play the music on a loop
|
|
13610
|
-
* music_example.playMusic();
|
|
13611
|
-
*/
|
|
13612
|
-
class ZzFXMusic extends Sound
|
|
13613
|
-
{
|
|
13614
|
-
/** Create a music object and cache the zzfx music samples for later use
|
|
13615
|
-
* @param {[Array, Array, Array, number?, ...any[]]} zzfxMusic - Array of zzfx music parameters: instruments, patterns, sequence, and an optional BPM; anything after it, like the tracker's metadata, is ignored
|
|
13616
|
-
*/
|
|
13617
|
-
constructor(zzfxMusic)
|
|
13618
|
-
{
|
|
13619
|
-
super(undefined);
|
|
13620
|
-
|
|
13621
|
-
if (!soundEnable || headlessMode) return;
|
|
13622
|
-
const [instruments, patterns, sequence, BPM] = zzfxMusic;
|
|
13623
|
-
super.sampleChannels = zzfxM(instruments, patterns, sequence, BPM); // the setter, without declaring a field that hides it in the typings
|
|
13624
|
-
this.buildSampleBuffer(); // hand the samples to an audio buffer now, like a zzfx sound, so the arrays are released
|
|
13625
|
-
this.loadedPercent = 1; // generated in place, so it is loaded like a zzfx sound
|
|
13626
|
-
}
|
|
13627
|
-
}
|
|
13628
|
-
|
|
13629
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
13630
|
-
// ZzFX Music Renderer v2.0.3 by Keith Clark and Frank Force
|
|
13631
|
-
|
|
13632
|
-
/** Generate samples for a ZzFM song with given parameters
|
|
13633
|
-
* @param {Array} instruments - Array of ZzFX sound parameters
|
|
13634
|
-
* @param {Array} patterns - Array of pattern data
|
|
13635
|
-
* @param {Array} sequence - Array of pattern indexes
|
|
13636
|
-
* @param {number} [BPM] - Playback speed of the song in BPM
|
|
13637
|
-
* @return {Array} - Left and right channel sample data
|
|
13638
|
-
* @memberof ZzFXM */
|
|
13639
|
-
function zzfxM(instruments, patterns, sequence, BPM = 125)
|
|
13640
|
-
{
|
|
13641
|
-
let i, j, k;
|
|
13642
|
-
let instrumentParameters;
|
|
13643
|
-
let note;
|
|
13644
|
-
let sample;
|
|
13645
|
-
let patternChannel;
|
|
13646
|
-
let notFirstBeat;
|
|
13647
|
-
let stop;
|
|
13648
|
-
let instrument;
|
|
13649
|
-
let attenuation;
|
|
13650
|
-
let outSampleOffset;
|
|
13651
|
-
let isSequenceEnd;
|
|
13652
|
-
let sampleOffset = 0;
|
|
13653
|
-
let nextSampleOffset;
|
|
13654
|
-
let sampleBuffer = [];
|
|
13655
|
-
let leftChannelBuffer = [];
|
|
13656
|
-
let rightChannelBuffer = [];
|
|
13657
|
-
let channelIndex = 0;
|
|
13658
|
-
let panning = 0;
|
|
13659
|
-
let hasMore = 1;
|
|
13660
|
-
let sampleCache = {};
|
|
13661
|
-
let beatLength = audioDefaultSampleRate / BPM * 60 >> 2;
|
|
13662
|
-
|
|
13663
|
-
// for each channel in order until there are no more
|
|
13664
|
-
for (; hasMore; channelIndex++) {
|
|
13665
|
-
|
|
13666
|
-
// reset current values
|
|
13667
|
-
sampleBuffer = [hasMore = notFirstBeat = outSampleOffset = 0];
|
|
13668
|
-
|
|
13669
|
-
// for each pattern in sequence
|
|
13670
|
-
sequence.forEach((patternIndex, sequenceIndex)=> {
|
|
13671
|
-
// get pattern for current channel, use empty 1 note pattern if none found
|
|
13672
|
-
patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
|
|
13673
|
-
|
|
13674
|
-
// check if there are more channels
|
|
13675
|
-
hasMore |= patterns[patternIndex][channelIndex]&&1;
|
|
13676
|
-
|
|
13677
|
-
// get next offset, use the length of first channel
|
|
13678
|
-
nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength;
|
|
13679
|
-
// for each beat in pattern, plus one extra if end of sequence
|
|
13680
|
-
isSequenceEnd = sequenceIndex === sequence.length - 1;
|
|
13681
|
-
for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
|
|
13682
|
-
|
|
13683
|
-
// <channel-note>
|
|
13684
|
-
note = patternChannel[i];
|
|
13685
|
-
|
|
13686
|
-
// stop if end, different instrument or new note
|
|
13687
|
-
stop = i === patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
|
|
13688
|
-
instrument !== (patternChannel[0] || 0) || note | 0;
|
|
13689
|
-
|
|
13690
|
-
// fill buffer with samples for previous beat, most cpu intensive part
|
|
13691
|
-
for (j = 0; j < beatLength && notFirstBeat;
|
|
13692
|
-
|
|
13693
|
-
// fade off attenuation at end of beat if stopping note, prevents clicking
|
|
13694
|
-
j++ > beatLength - 99 && stop && attenuation < 1? attenuation += 1 / 99 : 0
|
|
13695
|
-
) {
|
|
13696
|
-
// copy sample to stereo buffers with panning
|
|
13697
|
-
sample = (1 - attenuation) * sampleBuffer[sampleOffset++] / 2 || 0;
|
|
13698
|
-
leftChannelBuffer[k] = (leftChannelBuffer[k] || 0) - sample * panning + sample;
|
|
13699
|
-
rightChannelBuffer[k] = (rightChannelBuffer[k++] || 0) + sample * panning + sample;
|
|
13700
|
-
}
|
|
13701
|
-
|
|
13702
|
-
// set up for next note
|
|
13703
|
-
if (note) {
|
|
13704
|
-
// set attenuation
|
|
13705
|
-
attenuation = note % 1;
|
|
13706
|
-
panning = patternChannel[1] || 0;
|
|
13707
|
-
if (note |= 0) {
|
|
13708
|
-
// get cached sample
|
|
13709
|
-
sampleBuffer = sampleCache[
|
|
13710
|
-
[
|
|
13711
|
-
instrument = patternChannel[sampleOffset = 0] || 0,
|
|
13712
|
-
note
|
|
13713
|
-
]
|
|
13714
|
-
] = sampleCache[[instrument, note]] || (
|
|
13715
|
-
// add sample to cache
|
|
13716
|
-
instrumentParameters = [...instruments[instrument]],
|
|
13717
|
-
instrumentParameters[2] = (instrumentParameters[2] || 220) * 2**(note / 12 - 1),
|
|
13718
|
-
|
|
13719
|
-
// allow negative values to stop notes
|
|
13720
|
-
note > 0 ? zzfxG(...instrumentParameters) : []
|
|
13721
|
-
);
|
|
13722
|
-
}
|
|
13723
|
-
}
|
|
13724
|
-
}
|
|
13725
|
-
|
|
13726
|
-
// update the sample offset
|
|
13727
|
-
outSampleOffset = nextSampleOffset;
|
|
13728
|
-
});
|
|
13729
|
-
}
|
|
13730
|
-
|
|
13731
|
-
return [leftChannelBuffer, rightChannelBuffer];
|
|
13732
|
-
}
|
|
13733
13730
|
/**
|
|
13734
13731
|
* LittleJS Audio Effects Plugin
|
|
13735
13732
|
* - Web Audio effects with a wet/dry mix: filter, reverb, delay, distortion, compressor
|
|
@@ -16545,7 +16542,7 @@ class Box2dObject extends EngineObject
|
|
|
16545
16542
|
|
|
16546
16543
|
const points = [];
|
|
16547
16544
|
const radius = diameter/2;
|
|
16548
|
-
for (let i=sides; i
|
|
16545
|
+
for (let i=sides; i-- > 0;) // a count that is not a whole number still ends
|
|
16549
16546
|
points.push(vec2(radius,0).rotate((i+.5)/sides*PI*2));
|
|
16550
16547
|
return this.addPoly(points, density, friction, restitution, isSensor);
|
|
16551
16548
|
}
|
|
@@ -23152,8 +23149,8 @@ class Render3DPlugin
|
|
|
23152
23149
|
}
|
|
23153
23150
|
|
|
23154
23151
|
/** Find the nearest object under a screen position or along a ray, for clicking on things
|
|
23155
|
-
* -
|
|
23156
|
-
*
|
|
23152
|
+
* - A mesh is hit on its triangles, the ones that face the ray as they are drawn, both sides of a doubleSided
|
|
23153
|
+
* mesh; a sprite is hit as the quad it draws, and a height map or a voxel map on its surface
|
|
23157
23154
|
* - engineObjectsRaycast3D is the other half of this, every object along a ray instead of the nearest
|
|
23158
23155
|
* @param {Vector2|Ray3D} from - A screen position like mousePosScreen, or a ray to look along
|
|
23159
23156
|
* @param {Array<EngineObject>} [objects] - Defaults to every object; only those with a mesh or a sprite count
|
|
@@ -26520,8 +26517,8 @@ function engineObjectsCollect3D(pos, size, objects=engineObjects, testCenters=fa
|
|
|
26520
26517
|
return collected;
|
|
26521
26518
|
}
|
|
26522
26519
|
|
|
26523
|
-
// how far along a ray an object is hit, or undefined for a miss;
|
|
26524
|
-
//
|
|
26520
|
+
// 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,
|
|
26521
|
+
// and a sprite as the quad it draws
|
|
26525
26522
|
function render3DRaycastObject(ray, o)
|
|
26526
26523
|
{
|
|
26527
26524
|
if (o.destroyed || !(o instanceof EngineObject3D)) return;
|
|
@@ -26538,25 +26535,55 @@ function render3DRaycastObject(ray, o)
|
|
|
26538
26535
|
const distance = raycastSphere(ray, center, radius);
|
|
26539
26536
|
if (distance === undefined) return;
|
|
26540
26537
|
|
|
26541
|
-
// the sphere
|
|
26542
|
-
//
|
|
26538
|
+
// the sphere and then the box in the mesh's own space are quick rejects, and a ray that meets the box is tested
|
|
26539
|
+
// against the triangles; the direction is not made unit length, so the distance holds in the world;
|
|
26543
26540
|
// a mesh flattened to nothing on an axis has no inverse, it is hit as a disc like a sprite
|
|
26544
26541
|
if (!matrix.determinant()) return render3DRaycastDisc(ray, center, radius);
|
|
26545
26542
|
const inverse = matrix.copy().invert(), bounds = mesh.bounds || mesh.getBounds();
|
|
26546
26543
|
const local = new Ray3D(inverse.transformPoint(ray.origin), inverse.transformDirection(ray.direction));
|
|
26547
26544
|
const hit = raycastBox(local, bounds.min.add(bounds.max).scale(.5), bounds.max.subtract(bounds.min));
|
|
26548
|
-
if (hit
|
|
26549
|
-
|
|
26550
|
-
|
|
26551
|
-
|
|
26552
|
-
|
|
26553
|
-
|
|
26545
|
+
if (hit === undefined || !ray.direction.lengthSquared())
|
|
26546
|
+
return hit; // a ray of no length is where it starts, inside the box
|
|
26547
|
+
return render3DRaycastMesh(local, mesh);
|
|
26548
|
+
}
|
|
26549
|
+
|
|
26550
|
+
// how far along a ray, in the mesh's own space, its nearest triangle is hit, or undefined: the triangles that face
|
|
26551
|
+
// the ray as the pass draws them, clockwise from the front, and the back faces too of a doubleSided mesh; a strip's
|
|
26552
|
+
// 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
|
|
26553
|
+
// never hit; nothing is kept between calls, so a mesh that changed is hit as it is now
|
|
26554
|
+
function render3DRaycastMesh(ray, mesh)
|
|
26555
|
+
{
|
|
26556
|
+
const points = mesh.points, indices = mesh.indices, both = mesh.doubleSided;
|
|
26557
|
+
const ox = ray.origin.x, oy = ray.origin.y, oz = ray.origin.z;
|
|
26558
|
+
const dx = ray.direction.x, dy = ray.direction.y, dz = ray.direction.z;
|
|
26559
|
+
const count = indices ? indices.length / 3 | 0 : points.length - 2;
|
|
26560
|
+
let nearest;
|
|
26561
|
+
for (let i = 0; i < count; ++i)
|
|
26554
26562
|
{
|
|
26555
|
-
|
|
26556
|
-
|
|
26557
|
-
|
|
26563
|
+
// the corners clockwise from the front: an indexed list is written the other way round
|
|
26564
|
+
let a, b, c;
|
|
26565
|
+
if (indices)
|
|
26566
|
+
a = points[indices[i*3]], b = points[indices[i*3+2]], c = points[indices[i*3+1]];
|
|
26567
|
+
else
|
|
26568
|
+
a = points[i & 1 ? i + 1 : i], b = points[i & 1 ? i : i + 1], c = points[i + 2];
|
|
26569
|
+
|
|
26570
|
+
// Moller and Trumbore: the determinant is the ray along the face's normal, below zero from the front
|
|
26571
|
+
const e1x = b.x - a.x, e1y = b.y - a.y, e1z = b.z - a.z;
|
|
26572
|
+
const e2x = c.x - a.x, e2y = c.y - a.y, e2z = c.z - a.z;
|
|
26573
|
+
const hx = dy * e2z - dz * e2y, hy = dz * e2x - dx * e2z, hz = dx * e2y - dy * e2x;
|
|
26574
|
+
const det = e1x * hx + e1y * hy + e1z * hz;
|
|
26575
|
+
if (both ? !det : !(det < 0)) continue;
|
|
26576
|
+
const sx = ox - a.x, sy = oy - a.y, sz = oz - a.z;
|
|
26577
|
+
const u = (sx * hx + sy * hy + sz * hz) / det;
|
|
26578
|
+
if (!(u >= 0 && u <= 1)) continue;
|
|
26579
|
+
const qx = sy * e1z - sz * e1y, qy = sz * e1x - sx * e1z, qz = sx * e1y - sy * e1x;
|
|
26580
|
+
const v = (dx * qx + dy * qy + dz * qz) / det;
|
|
26581
|
+
if (!(v >= 0 && u + v <= 1)) continue;
|
|
26582
|
+
const t = (e2x * qx + e2y * qy + e2z * qz) / det;
|
|
26583
|
+
if (t >= 0 && !(t >= nearest))
|
|
26584
|
+
nearest = t;
|
|
26558
26585
|
}
|
|
26559
|
-
return
|
|
26586
|
+
return nearest;
|
|
26560
26587
|
}
|
|
26561
26588
|
|
|
26562
26589
|
// a sprite is the quad drawBillboard draws, its size3D grown by the x and y scale as the draw does, facing the camera
|
|
@@ -26934,10 +26961,12 @@ class InstancedMesh3D extends EngineObject3D
|
|
|
26934
26961
|
* - radius is a world distance, so scale3D does not change it
|
|
26935
26962
|
* - An alpha, an intensity or a radius of 0 switches it off, and a light that is off takes none of those slots
|
|
26936
26963
|
* - Draws nothing but its glow, when it has one; add a small emissive mesh if the lamp itself should be seen
|
|
26964
|
+
* - addFlare gives it a lens flare, see LensFlare3D
|
|
26937
26965
|
* @extends EngineObject3D
|
|
26938
26966
|
* @memberof Render3D
|
|
26939
26967
|
* @example
|
|
26940
26968
|
* const torch = new Light3D(vec3(0, 3, 0), 10, hsl(.1, 1, .65));
|
|
26969
|
+
* torch.addFlare(); // light in the lens when the torch is in view
|
|
26941
26970
|
*/
|
|
26942
26971
|
class Light3D extends EngineObject3D
|
|
26943
26972
|
{
|
|
@@ -26972,6 +27001,50 @@ class Light3D extends EngineObject3D
|
|
|
26972
27001
|
* bright core */
|
|
26973
27002
|
this.glowFalloff = 1;
|
|
26974
27003
|
this.additive = true; // the glow is added on, in the transparent stage; a light with none draws nothing
|
|
27004
|
+
/** @type {LensFlare3D|undefined} */
|
|
27005
|
+
this.flareObject = undefined;
|
|
27006
|
+
}
|
|
27007
|
+
|
|
27008
|
+
/** Give the light a lens flare, made with the arguments of LensFlare3D, in place of the one it had
|
|
27009
|
+
* @param {number} [size] - Scales every part of it
|
|
27010
|
+
* @param {number} [count] - How many ghosts there are, besides the glow at the light
|
|
27011
|
+
* @param {number} [intensity] - How bright it is
|
|
27012
|
+
* @param {number} [saturation] - How colorful the ghosts are
|
|
27013
|
+
* @param {Color} [color] - Tints the flare, with the light's own color
|
|
27014
|
+
* @return {LensFlare3D} - The flare, to change: light.addFlare().shapes = ['hex'] */
|
|
27015
|
+
addFlare(size, count, intensity, saturation, color)
|
|
27016
|
+
{
|
|
27017
|
+
const flare = new LensFlare3D(size, count, intensity, saturation, color);
|
|
27018
|
+
this.flare = flare;
|
|
27019
|
+
return flare;
|
|
27020
|
+
}
|
|
27021
|
+
|
|
27022
|
+
/** The light's lens flare, undefined for none: addFlare makes it, or set a LensFlare3D of your own, and
|
|
27023
|
+
* destroying the flare takes it away
|
|
27024
|
+
* - The flare is the light's: it is attached to the light as its child, so it stays through a scene change
|
|
27025
|
+
* when the light does, and is destroyed with the light or when another takes its place
|
|
27026
|
+
* @type {LensFlare3D|undefined} */
|
|
27027
|
+
get flare() { return this.flareObject && !this.flareObject.destroyed ? this.flareObject : undefined; }
|
|
27028
|
+
|
|
27029
|
+
set flare(flare)
|
|
27030
|
+
{
|
|
27031
|
+
const old = this.flare; // a flare that was destroyed on its own is no flare
|
|
27032
|
+
if (flare === old) return;
|
|
27033
|
+
old && old.destroy();
|
|
27034
|
+
if (flare)
|
|
27035
|
+
{
|
|
27036
|
+
flare.light = this;
|
|
27037
|
+
flare.parent || this.addChild(flare);
|
|
27038
|
+
}
|
|
27039
|
+
this.flareObject = flare || undefined;
|
|
27040
|
+
}
|
|
27041
|
+
|
|
27042
|
+
/** Destroy the light, and its flare with it
|
|
27043
|
+
* @param {boolean} [immediate] */
|
|
27044
|
+
destroy(immediate)
|
|
27045
|
+
{
|
|
27046
|
+
this.flare = undefined;
|
|
27047
|
+
super.destroy(immediate);
|
|
26975
27048
|
}
|
|
26976
27049
|
|
|
26977
27050
|
/** Draw the glow, a quad facing the camera with a soft round glow on it, pulled toward the camera by half its
|
|
@@ -28490,31 +28563,74 @@ class Trail3D extends EngineObject3D
|
|
|
28490
28563
|
// lens flare
|
|
28491
28564
|
|
|
28492
28565
|
// the shapes a flare is made of, white on clear on one smooth texture made the first time it is asked for: a soft
|
|
28493
|
-
// glow, a flat disc with a brighter rim,
|
|
28566
|
+
// glow, a flat disc with a brighter rim, a ring, a hexagon like the blades of a lens, a streak across, and a star
|
|
28567
|
+
// of six rays; undefined headless or without a canvas
|
|
28568
|
+
const render3DFlareShapes = ['glow', 'disc', 'ring', 'hex', 'streak', 'star'];
|
|
28494
28569
|
let render3DFlareTiles;
|
|
28495
28570
|
function render3DFlareTile(shape)
|
|
28496
28571
|
{
|
|
28497
28572
|
if (!render3DFlareTiles)
|
|
28498
28573
|
{
|
|
28499
28574
|
if (headlessMode || !glContext || typeof OffscreenCanvas == 'undefined') return;
|
|
28500
|
-
const cell = 128, shapes =
|
|
28575
|
+
const cell = 128, shapes = render3DFlareShapes, context = createCanvasContext(cell * shapes.length, cell);
|
|
28501
28576
|
// each shape is how see-through it is from its middle out
|
|
28502
28577
|
const alpha =
|
|
28503
28578
|
{
|
|
28504
28579
|
glow: (t)=> engineGlowAlpha(t, 1),
|
|
28505
28580
|
disc: (t)=> t < .8 ? .4 + .1 * t : t < .92 ? .48 + (t - .8) * 3 : (1 - t) / .08 * .84,
|
|
28506
28581
|
ring: (t)=> max(0, 1 - abs(t - .86) / .12),
|
|
28582
|
+
hex: (t)=> t < .8 ? .4 + .1 * t : t < .92 ? .48 + (t - .8) * 3 : (1 - t) / .08 * .84,
|
|
28583
|
+
streak: (t)=> (1 - t) ** 1.5,
|
|
28507
28584
|
};
|
|
28508
|
-
|
|
28585
|
+
// a round gradient from the middle of the cell out, squashed for a ray and cut to a path for the hexagon
|
|
28586
|
+
const fill = (shape, r, path)=>
|
|
28509
28587
|
{
|
|
28510
|
-
const
|
|
28511
|
-
const gradient = context.createRadialGradient(x, y, 0, x, y, r);
|
|
28588
|
+
const steps = 32, gradient = context.createRadialGradient(0, 0, 0, 0, 0, r);
|
|
28512
28589
|
for (let k = 0; k <= steps; ++k)
|
|
28513
28590
|
gradient.addColorStop(k / steps, 'rgba(255,255,255,' + clamp(alpha[shape](k / steps)).toFixed(4) + ')');
|
|
28514
28591
|
context.fillStyle = gradient;
|
|
28515
28592
|
context.beginPath();
|
|
28516
|
-
context.arc(
|
|
28593
|
+
path ? path() : context.arc(0, 0, r, 0, 2 * PI);
|
|
28517
28594
|
context.fill();
|
|
28595
|
+
};
|
|
28596
|
+
const ray = (r, angle)=>
|
|
28597
|
+
{
|
|
28598
|
+
context.save();
|
|
28599
|
+
context.rotate(angle);
|
|
28600
|
+
context.scale(1, .07);
|
|
28601
|
+
fill('streak', r);
|
|
28602
|
+
context.restore();
|
|
28603
|
+
};
|
|
28604
|
+
shapes.forEach((shape, i)=>
|
|
28605
|
+
{
|
|
28606
|
+
const r = cell / 2 - 2;
|
|
28607
|
+
context.save();
|
|
28608
|
+
context.translate(i * cell + cell / 2, cell / 2);
|
|
28609
|
+
if (shape == 'hex')
|
|
28610
|
+
{
|
|
28611
|
+
// a gradient is round, so the hexagon is set pixel by pixel, by how far each is toward a side
|
|
28612
|
+
const image = context.createImageData(cell, cell), data = image.data, apothem = r * cos(PI / 6);
|
|
28613
|
+
for (let k = 0; k < cell * cell; ++k)
|
|
28614
|
+
{
|
|
28615
|
+
const x = abs(k % cell + .5 - cell / 2), y = abs((k / cell | 0) + .5 - cell / 2);
|
|
28616
|
+
const t = max(y, x * cos(PI / 6) + y / 2) / apothem;
|
|
28617
|
+
data[k * 4] = data[k * 4 + 1] = data[k * 4 + 2] = 255;
|
|
28618
|
+
data[k * 4 + 3] = t < 1 ? clamp(alpha.hex(t)) * 255 : 0;
|
|
28619
|
+
}
|
|
28620
|
+
context.putImageData(image, i * cell, 0);
|
|
28621
|
+
}
|
|
28622
|
+
else if (shape == 'streak')
|
|
28623
|
+
ray(r, 0);
|
|
28624
|
+
else if (shape == 'star')
|
|
28625
|
+
{
|
|
28626
|
+
for (let k = 3; k--;)
|
|
28627
|
+
ray(r, k * PI / 3 + PI / 2);
|
|
28628
|
+
context.scale(.3, .3);
|
|
28629
|
+
fill('glow', r);
|
|
28630
|
+
}
|
|
28631
|
+
else
|
|
28632
|
+
fill(shape, r);
|
|
28633
|
+
context.restore();
|
|
28518
28634
|
});
|
|
28519
28635
|
const texture = new TextureInfo(context.canvas, true, false, false); // smooth even in a pixel art game
|
|
28520
28636
|
render3DFlareTiles = new Map(shapes.map((shape, i)=>
|
|
@@ -28523,16 +28639,32 @@ function render3DFlareTile(shape)
|
|
|
28523
28639
|
return render3DFlareTiles.get(shape) || render3DFlareTiles.get('glow');
|
|
28524
28640
|
}
|
|
28525
28641
|
|
|
28642
|
+
/**
|
|
28643
|
+
* A part of a lens flare
|
|
28644
|
+
* @typedef {Object} LensFlareElement
|
|
28645
|
+
* @property {number} at - Where along the line: 0 the sun, 1 the middle of the screen, 2 as far past it
|
|
28646
|
+
* @property {number|Vector2} size - How big across, as a part of the screen's height, a vector for a part wider
|
|
28647
|
+
* than it is tall
|
|
28648
|
+
* @property {Color} color - Its color, the alpha how bright
|
|
28649
|
+
* @property {string} [shape] - glow, disc, ring, hex, streak or star, a glow when left out
|
|
28650
|
+
* @property {TileInfo} [tileInfo] - A tile of the game's own to draw in place of a shape, best white on clear
|
|
28651
|
+
* @property {number} [angle] - How far it is turned, in radians
|
|
28652
|
+
* @memberof Render3D
|
|
28653
|
+
*/
|
|
28654
|
+
|
|
28526
28655
|
/**
|
|
28527
28656
|
* LensFlare3D - The sun's lens flare, the old kind: a glow at the sun and a row of discs and rings of different
|
|
28528
28657
|
* sizes along the line from the sun through the middle of the screen
|
|
28529
28658
|
* - Make one and it shows, over the 3D scene and under what the game draws after, a HUD; destroy it to take it away
|
|
28530
28659
|
* - It follows render3D.sunDirection, and fades out as the sun leaves the screen or goes behind something
|
|
28660
|
+
* - A Light3D gets one of its own with light.addFlare()
|
|
28531
28661
|
* - flareSize, count, intensity and saturation set its look, seed picks another arrangement, and its color tints it,
|
|
28532
|
-
* with the sun's own color;
|
|
28533
|
-
*
|
|
28662
|
+
* with the sun's own color; shapes says what its ghosts are, glowSize and ghostSize how big its parts are; or
|
|
28663
|
+
* give it elements of your own, which may be tiles of the game's
|
|
28664
|
+
* - visible is how much of the sun shows, 0 to 1, eased over fadeTime, there for a game to read; it is 0 while the
|
|
28665
|
+
* sun is too far off the screen for the flare to show, a third of the screen past its edge, where nothing is tested
|
|
28534
28666
|
* - What hides the sun is found with a ray from the camera, against the level and every object that is not see
|
|
28535
|
-
* through, each
|
|
28667
|
+
* through, each on the triangles of its mesh, see render3D.pick; turn it off with occlusion
|
|
28536
28668
|
* - It needs WebGL, and it draws nothing in the shadow of renderAfter2D
|
|
28537
28669
|
* @extends EngineObject3D
|
|
28538
28670
|
* @memberof Render3D
|
|
@@ -28562,48 +28694,62 @@ class LensFlare3D extends EngineObject3D
|
|
|
28562
28694
|
this.saturation = saturation;
|
|
28563
28695
|
/** @property {number} - Picks the arrangement of the ghosts, another seed is another flare */
|
|
28564
28696
|
this.seed = 1;
|
|
28565
|
-
/** @property {Array<
|
|
28566
|
-
*
|
|
28567
|
-
*
|
|
28568
|
-
*
|
|
28569
|
-
|
|
28697
|
+
/** @property {Array<string>|undefined} - The shapes the ghosts are picked from, glow, disc, ring, hex,
|
|
28698
|
+
* streak or star: ['hex'] makes every ghost a hexagon, and a shape listed twice is picked twice as often;
|
|
28699
|
+
* discs, rings and glows when not set
|
|
28700
|
+
* @type {Array<string>|undefined} */
|
|
28701
|
+
this.shapes = undefined;
|
|
28702
|
+
/** @property {number} - Scales the glow at the sun, 0 for none */
|
|
28703
|
+
this.glowSize = 1;
|
|
28704
|
+
/** @property {number} - Scales the ghosts */
|
|
28705
|
+
this.ghostSize = 1;
|
|
28706
|
+
/** @property {Array<LensFlareElement>|undefined} - The parts of the flare, to set your own in place of the
|
|
28707
|
+
* ones made from count, seed, saturation, shapes, glowSize and ghostSize
|
|
28708
|
+
* @type {Array<LensFlareElement>|undefined} */
|
|
28570
28709
|
this.elements = undefined;
|
|
28571
28710
|
/** @property {Light3D|undefined} - A light the flare is of in place of the sun, a lamp or a spotlight: the
|
|
28572
28711
|
* flare is at the light and in its color, smaller from farther than the light reaches, hidden by what is
|
|
28573
|
-
* in front of the light,
|
|
28712
|
+
* in front of the light but its lamp, a mesh around it no wider than half its radius, and a spotlight's
|
|
28713
|
+
* shows from inside its beam only; a DirectionalLight3D's is far
|
|
28714
|
+
* away where it shines from, like the sun's; the flare is destroyed when its light is; light.addFlare
|
|
28715
|
+
* sets this
|
|
28574
28716
|
* @type {Light3D|undefined} */
|
|
28575
28717
|
this.light = undefined;
|
|
28576
28718
|
/** @property {boolean} - Fade out when something is between the camera and the sun */
|
|
28577
28719
|
this.occlusion = true;
|
|
28578
28720
|
/** @property {number} - Seconds the flare takes to fade out or in when the sun is hidden or shows again */
|
|
28579
28721
|
this.fadeTime = .15;
|
|
28580
|
-
/** @property {number} - How much of the sun shows, 0 hidden or behind the camera to 1 in
|
|
28722
|
+
/** @property {number} - How much of the sun shows, 0 hidden, off the screen or behind the camera to 1 in
|
|
28723
|
+
* plain view, eased */
|
|
28581
28724
|
this.visible = 1;
|
|
28582
28725
|
this.renderOrder = 1e9; // over the game's sprites, it is light in the lens
|
|
28583
28726
|
this.madeKey = '';
|
|
28584
|
-
/** @type {Array<
|
|
28727
|
+
/** @type {Array<LensFlareElement>} */
|
|
28585
28728
|
this.made = [];
|
|
28586
28729
|
}
|
|
28587
28730
|
|
|
28588
|
-
/** The parts of the flare: the elements set by hand, or the ones made from count, seed
|
|
28589
|
-
* and a core at the sun and the ghosts, made again when one of those changes
|
|
28590
|
-
* @return {Array<
|
|
28731
|
+
/** The parts of the flare: the elements set by hand, or the ones made from count, seed, saturation, shapes,
|
|
28732
|
+
* glowSize and ghostSize, a glow and a core at the sun and the ghosts, made again when one of those changes
|
|
28733
|
+
* @return {Array<LensFlareElement>} */
|
|
28591
28734
|
getElements()
|
|
28592
28735
|
{
|
|
28593
28736
|
if (this.elements) return this.elements;
|
|
28594
|
-
const
|
|
28737
|
+
const shapes = this.shapes?.length ? this.shapes : undefined, glow = this.glowSize, ghost = this.ghostSize;
|
|
28738
|
+
const key = [this.count, this.seed, this.saturation, glow, ghost, shapes?.length, shapes].join();
|
|
28595
28739
|
if (key !== this.madeKey)
|
|
28596
28740
|
{
|
|
28597
28741
|
const random = new RandomGenerator(this.seed * 7919 + 1), s = clamp(this.saturation);
|
|
28598
|
-
|
|
28599
|
-
|
|
28600
|
-
{at: 0, size: .
|
|
28742
|
+
/** @type {Array<LensFlareElement>} */
|
|
28743
|
+
const made = this.made = glow > 0 ? [
|
|
28744
|
+
{at: 0, size: .7 * glow, color: hsl(0, 0, 1, .5), shape: 'glow'},
|
|
28745
|
+
{at: 0, size: .25 * glow, color: hsl(0, 0, 1, .9), shape: 'glow'}] : [];
|
|
28601
28746
|
for (let i = 0; i < this.count; ++i)
|
|
28602
28747
|
{
|
|
28603
28748
|
// spread along the line, each a place of its own, the far ones bigger
|
|
28604
28749
|
const at = (i + random.float(.2, .8)) / max(this.count, 1) * 1.9 + .2;
|
|
28605
|
-
const pick = random.float(), shape = pick
|
|
28606
|
-
|
|
28750
|
+
const pick = random.float(), shape = shapes ? shapes[min(pick * shapes.length | 0, shapes.length - 1)] :
|
|
28751
|
+
pick < .5 ? 'disc' : pick < .8 ? 'ring' : 'glow';
|
|
28752
|
+
made.push({at, size: random.float(.04, .1) * (1 + at) * ghost, shape,
|
|
28607
28753
|
color: hsl(random.float(), s * .9, .6, random.float(.15, .4))});
|
|
28608
28754
|
}
|
|
28609
28755
|
this.madeKey = key;
|
|
@@ -28611,69 +28757,85 @@ class LensFlare3D extends EngineObject3D
|
|
|
28611
28757
|
return this.made;
|
|
28612
28758
|
}
|
|
28613
28759
|
|
|
28614
|
-
// what the flare is of, seen from the camera: the way to it, how far it is, Infinity for the sun
|
|
28615
|
-
//
|
|
28760
|
+
// what the flare is of, seen from the camera: the way to it, how far it is, Infinity for the sun and for a
|
|
28761
|
+
// directional light, which shines from its place toward the origin, and a point to find it on the screen by;
|
|
28762
|
+
// undefined with no direction, or a light that is gone or at the camera
|
|
28616
28763
|
flareSource()
|
|
28617
28764
|
{
|
|
28618
28765
|
const camera = render3D.camera.pos, light = this.light;
|
|
28619
|
-
if (
|
|
28766
|
+
if (light?.destroyed) return;
|
|
28767
|
+
if (!light || light.directional)
|
|
28620
28768
|
{
|
|
28621
|
-
const
|
|
28622
|
-
if (!
|
|
28623
|
-
const direction =
|
|
28769
|
+
const from = light ? light.getWorldPos3D() : render3D.sunDirection;
|
|
28770
|
+
if (!from.lengthSquared()) return;
|
|
28771
|
+
const direction = from.normalize();
|
|
28624
28772
|
return {direction, distance: Infinity, pos: camera.add(direction.scale(100))};
|
|
28625
28773
|
}
|
|
28626
|
-
if (light.destroyed) return;
|
|
28627
28774
|
const pos = light.getWorldPos3D(), offset = pos.subtract(camera), distance = offset.length();
|
|
28628
28775
|
return distance ? {direction: offset.scale(1 / distance), distance, pos} : undefined;
|
|
28629
28776
|
}
|
|
28630
28777
|
|
|
28631
|
-
|
|
28632
|
-
|
|
28633
|
-
|
|
28634
|
-
|
|
28635
|
-
{
|
|
28636
|
-
const source = this.flareSource();
|
|
28637
|
-
return source && render3D.worldToScreen(source.pos);
|
|
28638
|
-
}
|
|
28639
|
-
|
|
28640
|
-
/** The parts of the flare as they are drawn now: each one's place on the screen, its size in pixels and its
|
|
28641
|
-
* color, dimmed by how much of the sun shows; empty when there is nothing to draw
|
|
28642
|
-
* @return {Array<{pos: Vector2, size: number, color: Color, shape: string}>} */
|
|
28643
|
-
getScreenElements()
|
|
28778
|
+
// how the flare would show with nothing in the way: where its source is on the screen, how strong it is there,
|
|
28779
|
+
// fading as it leaves the screen, its tint and the height its sizes are parts of; undefined when it would not
|
|
28780
|
+
// show at all, behind the camera, off the screen, or of a light that is off or seen from outside its cone
|
|
28781
|
+
flareLook()
|
|
28644
28782
|
{
|
|
28645
|
-
const
|
|
28646
|
-
|
|
28783
|
+
const source = this.flareSource(), center = mainCanvasSize.scale(.5);
|
|
28784
|
+
const sun = source && render3D.worldToScreen(source.pos);
|
|
28785
|
+
if (!sun || !center.x || !center.y) return;
|
|
28647
28786
|
// it fades as the sun leaves the screen, gone when it is a third of the screen past the edge
|
|
28648
28787
|
const off = max(abs(sun.x - center.x) / center.x, abs(sun.y - center.y) / center.y);
|
|
28649
|
-
const strength =
|
|
28650
|
-
if (!(strength > 0)) return
|
|
28788
|
+
const strength = clamp((1.3 - off) / .5) * this.intensity;
|
|
28789
|
+
if (!(strength > 0)) return;
|
|
28651
28790
|
let tint = this.color.multiply(render3D.sunColor), height = mainCanvasSize.y * this.flareSize;
|
|
28652
|
-
const light = this.light
|
|
28791
|
+
const light = this.light;
|
|
28653
28792
|
if (light)
|
|
28654
28793
|
{
|
|
28655
|
-
if (!source) return [];
|
|
28656
28794
|
// a light's flare is the light's color, as bright as the light up to 1, and only from inside its cone
|
|
28657
28795
|
const cone = render3DLightCone(light), d = source.direction;
|
|
28658
28796
|
const inCone = clamp(-(cone[0] * d.x + cone[1] * d.y + cone[2] * d.z) - cone[3]);
|
|
28659
28797
|
const c = light.color, bright = c.a * clamp(light.intensity) * inCone;
|
|
28660
|
-
if (!(bright > 0)) return
|
|
28798
|
+
if (!(bright > 0)) return;
|
|
28661
28799
|
tint = this.color.multiply(rgb(c.r, c.g, c.b, bright));
|
|
28662
|
-
|
|
28800
|
+
if (!light.directional)
|
|
28801
|
+
height *= min(1, light.radius / source.distance); // smaller from farther than it reaches
|
|
28663
28802
|
}
|
|
28803
|
+
return height > 0 ? {sun, center, strength, tint, height} : undefined;
|
|
28804
|
+
}
|
|
28805
|
+
|
|
28806
|
+
/** Where the sun, or the flare's light, is on the screen, in pixels like mousePosScreen, undefined when it is
|
|
28807
|
+
* behind the camera
|
|
28808
|
+
* @return {Vector2|undefined} */
|
|
28809
|
+
getScreenPos()
|
|
28810
|
+
{
|
|
28811
|
+
const source = this.flareSource();
|
|
28812
|
+
return source && render3D.worldToScreen(source.pos);
|
|
28813
|
+
}
|
|
28814
|
+
|
|
28815
|
+
/** The parts of the flare as they are drawn now: each one's place on the screen, its size in pixels, a vector
|
|
28816
|
+
* when the element's is, and its color, dimmed by how much of the sun shows; empty when there is nothing to draw
|
|
28817
|
+
* @return {Array<{pos: Vector2, size: number|Vector2, color: Color, shape: string|undefined,
|
|
28818
|
+
* tileInfo: TileInfo|undefined, angle: number}>} */
|
|
28819
|
+
getScreenElements()
|
|
28820
|
+
{
|
|
28821
|
+
const look = this.visible > 0 && this.flareLook();
|
|
28822
|
+
if (!look) return [];
|
|
28823
|
+
const {sun, center, tint, height} = look, strength = look.strength * this.visible;
|
|
28664
28824
|
return this.getElements().map((e)=>
|
|
28665
28825
|
{
|
|
28666
28826
|
const c = e.color.multiply(tint);
|
|
28667
28827
|
// along the line and past the middle, a lerp would stop there
|
|
28668
|
-
|
|
28669
|
-
|
|
28828
|
+
const size = typeof e.size == 'number' ? e.size * height : e.size.scale(height);
|
|
28829
|
+
return {pos: sun.add(center.subtract(sun).scale(e.at)), size, shape: e.shape, tileInfo: e.tileInfo,
|
|
28830
|
+
angle: e.angle || 0, color: rgb(c.r, c.g, c.b, c.a * strength)};
|
|
28670
28831
|
});
|
|
28671
28832
|
}
|
|
28672
28833
|
|
|
28673
28834
|
/** Is something between the camera and the sun, or the flare's light: the level, or an object that is not see
|
|
28674
|
-
* through
|
|
28835
|
+
* through, hit on its triangles as render3D.pick hits it, so a mesh the camera is inside hides nothing unless
|
|
28836
|
+
* it is doubleSided
|
|
28675
28837
|
* @return {boolean} */
|
|
28676
|
-
|
|
28838
|
+
isHidden()
|
|
28677
28839
|
{
|
|
28678
28840
|
const source = this.flareSource();
|
|
28679
28841
|
if (!source) return true;
|
|
@@ -28687,22 +28849,31 @@ class LensFlare3D extends EngineObject3D
|
|
|
28687
28849
|
for (const map of maps)
|
|
28688
28850
|
if (map.raycast(ray, reach, (type)=> !map.blockType(type).seeThrough))
|
|
28689
28851
|
return true;
|
|
28690
|
-
//
|
|
28691
|
-
//
|
|
28692
|
-
//
|
|
28693
|
-
const
|
|
28694
|
-
|
|
28695
|
-
|
|
28696
|
-
|
|
28697
|
-
|
|
28852
|
+
// a light's lamp, the mesh the light is inside, does not hide it: one whose box, in its own space, the light
|
|
28853
|
+
// is in, and no wider than half the light's reach, since a room or a whole level has a box around its lights
|
|
28854
|
+
// too, and its walls do hide them from outside
|
|
28855
|
+
const lamp = (o)=>
|
|
28856
|
+
{
|
|
28857
|
+
if (!this.light || reach === Infinity || !o.mesh || o instanceof HeightMap) return false;
|
|
28858
|
+
const matrix = render3DObjectMatrix(o), b = o.mesh.bounds || o.mesh.getBounds();
|
|
28859
|
+
const side = max(b.max.x - b.min.x, b.max.y - b.min.y, b.max.z - b.min.z) * render3DMaxStretch(matrix.m);
|
|
28860
|
+
if (!matrix.determinant() || side > this.light.radius / 2) return false;
|
|
28861
|
+
const p = matrix.copy().invert().transformPoint(source.pos);
|
|
28862
|
+
return p.x >= b.min.x && p.x <= b.max.x && p.y >= b.min.y && p.y <= b.max.y &&
|
|
28863
|
+
p.z >= b.min.z && p.z <= b.max.z;
|
|
28864
|
+
};
|
|
28865
|
+
const hit = render3D.pick(ray, blockers.filter((o)=> !maps.some((map)=> map === o) && !lamp(o)));
|
|
28698
28866
|
return !!hit && hit.distance < reach;
|
|
28699
28867
|
}
|
|
28700
28868
|
|
|
28701
|
-
/** Ease visible toward whether the sun shows, called automatically each frame
|
|
28869
|
+
/** Ease visible toward whether the sun shows, called automatically each frame; a flare that would not show
|
|
28870
|
+
* wherever the sun is, off the screen or its light off, looks for nothing in the way */
|
|
28702
28871
|
update()
|
|
28703
28872
|
{
|
|
28704
28873
|
super.update();
|
|
28705
|
-
|
|
28874
|
+
if (this.light?.destroyed)
|
|
28875
|
+
return this.destroy(); // the flare of a light that is gone
|
|
28876
|
+
const shows = !!this.flareLook() && !(this.occlusion && this.isHidden());
|
|
28706
28877
|
const step = this.fadeTime > 0 ? timeDelta / this.fadeTime : 1;
|
|
28707
28878
|
this.visible = clamp(this.visible + (shows ? step : -step));
|
|
28708
28879
|
}
|
|
@@ -28716,8 +28887,9 @@ class LensFlare3D extends EngineObject3D
|
|
|
28716
28887
|
setAdditiveBlendMode(true);
|
|
28717
28888
|
for (const e of elements)
|
|
28718
28889
|
{
|
|
28719
|
-
const tileInfo = render3DFlareTile(e.shape);
|
|
28720
|
-
|
|
28890
|
+
const tileInfo = e.tileInfo || render3DFlareTile(e.shape);
|
|
28891
|
+
const size = typeof e.size == 'number' ? vec2(e.size) : e.size;
|
|
28892
|
+
tileInfo && drawTile(e.pos, size, tileInfo, e.color, e.angle, false, undefined, true, true);
|
|
28721
28893
|
}
|
|
28722
28894
|
setAdditiveBlendMode(false);
|
|
28723
28895
|
}
|
|
@@ -29323,7 +29495,7 @@ class VoxelMap extends EngineObject3D
|
|
|
29323
29495
|
/** @type {Array<Vector3>} */
|
|
29324
29496
|
this.chunkCenters = []; // a chunk mesh's points are around its center, which it is drawn at
|
|
29325
29497
|
this.chunksChanged = new Set;
|
|
29326
|
-
/** @type {Array<{faces: Array<number>, seeThrough: boolean, transparent: boolean}|undefined>} */
|
|
29498
|
+
/** @type {Array<{faces: Array<number>, seeThrough: boolean, transparent: boolean, doubleSided: boolean}|undefined>} */
|
|
29327
29499
|
this.blockTypes = [];
|
|
29328
29500
|
this.tiles = new Map; // tile index to the uvs of its corners
|
|
29329
29501
|
|
|
@@ -29395,27 +29567,29 @@ class VoxelMap extends EngineObject3D
|
|
|
29395
29567
|
* @param {number} type - 1 to 255
|
|
29396
29568
|
* @param {number|Array<number>|{top?: number, side: number, bottom?: number}} faces - A tile index for every face,
|
|
29397
29569
|
* six in the order +x, -x, +y, -y, +z, -z, or the side's with the top and bottom's, which default to the side's
|
|
29398
|
-
* @param {{seeThrough?: boolean, transparent?: boolean}} [options] - seeThrough for holes
|
|
29399
|
-
* leaves, so the blocks beside it keep their faces; transparent to blend, like glass or
|
|
29400
|
-
* transparent stage, and see-through too
|
|
29401
|
-
|
|
29570
|
+
* @param {{seeThrough?: boolean, transparent?: boolean, doubleSided?: boolean}} [options] - seeThrough for holes
|
|
29571
|
+
* in its texture, like leaves, so the blocks beside it keep their faces; transparent to blend, like glass or
|
|
29572
|
+
* water, drawn in the transparent stage, and see-through too; doubleSided for faces seen from inside the
|
|
29573
|
+
* block as well, like water, whose surface then shows from under it */
|
|
29574
|
+
setBlockType(type, faces, {seeThrough=false, transparent=false, doubleSided=false}={})
|
|
29402
29575
|
{
|
|
29403
29576
|
false&&ASSERT(type >= 1 && type <= 255 && type % 1 === 0, 'a block type is a whole number from 1 to 255', type);
|
|
29404
29577
|
const f = /** @type {any} */ (faces);
|
|
29405
29578
|
const list = isNumber(f) ? [f, f, f, f, f, f] : isArray(f) ? f :
|
|
29406
29579
|
[f.side, f.side, f.top ?? f.side, f.bottom ?? f.side, f.side, f.side];
|
|
29407
29580
|
false&&ASSERT(list.length === 6 && list.every(isNumber), 'faces is a tile index, six of them, or {top, side, bottom}');
|
|
29408
|
-
this.blockTypes[type] = {faces: list, seeThrough: seeThrough || transparent, transparent};
|
|
29581
|
+
this.blockTypes[type] = {faces: list, seeThrough: seeThrough || transparent, transparent, doubleSided};
|
|
29409
29582
|
this.rebuild();
|
|
29410
29583
|
}
|
|
29411
29584
|
|
|
29412
29585
|
/** A block type's faces and how it is seen through
|
|
29413
29586
|
* @param {number} type
|
|
29414
|
-
* @return {{faces: Array<number>, seeThrough: boolean, transparent: boolean}}
|
|
29587
|
+
* @return {{faces: Array<number>, seeThrough: boolean, transparent: boolean, doubleSided: boolean}}
|
|
29415
29588
|
* @ignore */
|
|
29416
29589
|
blockType(type)
|
|
29417
29590
|
{
|
|
29418
|
-
return this.blockTypes[type] ||= {faces: [type, type, type, type, type, type], seeThrough: false, transparent: false
|
|
29591
|
+
return this.blockTypes[type] ||= {faces: [type, type, type, type, type, type], seeThrough: false, transparent: false,
|
|
29592
|
+
doubleSided: false};
|
|
29419
29593
|
}
|
|
29420
29594
|
|
|
29421
29595
|
/** Build every chunk again, after changing data directly or ambientOcclusion */
|
|
@@ -29485,6 +29659,11 @@ class VoxelMap extends EngineObject3D
|
|
|
29485
29659
|
colors[k] = ao ? RENDER3D_VOXEL_SHADES[shade[i]] : WHITE;
|
|
29486
29660
|
}
|
|
29487
29661
|
mesh.addStrip(points, normal, uvs, colors);
|
|
29662
|
+
|
|
29663
|
+
// a double sided block shows the face from inside too, its corners the other way round
|
|
29664
|
+
if (block.doubleSided)
|
|
29665
|
+
mesh.addStrip([points[1], points[0], points[3], points[2]], normal.scale(-1),
|
|
29666
|
+
[uvs[1], uvs[0], uvs[3], uvs[2]], [colors[1], colors[0], colors[3], colors[2]]);
|
|
29488
29667
|
}
|
|
29489
29668
|
}
|
|
29490
29669
|
this.chunkMeshes[index]?.dispose();
|
|
@@ -29831,8 +30010,9 @@ function level3DAddMesh(name, mesh, tileInfo, color=WHITE)
|
|
|
29831
30010
|
* are added before its objects are made, one the game added itself keeps its place
|
|
29832
30011
|
* - A level may set the scene too, in a scene block beside its objects: sky, three colors for straight up, the
|
|
29833
30012
|
* horizon and straight down, ambient, how much of them lights the scene, .5 when not given, sunDirection and
|
|
29834
|
-
* sunColor, fog, its start and end, fogColor, the horizon color when not given, and
|
|
29835
|
-
* leaves out stays as the game set it, and a level with no block changes
|
|
30013
|
+
* sunColor, fog, its start and end, fogColor, the horizon color when not given, shadows, and lensFlare, the
|
|
30014
|
+
* sun's lens flare; what the block leaves out stays as the game set it, and a level with no block changes
|
|
30015
|
+
* nothing
|
|
29836
30016
|
* @param {Object} level - The level, the level editor edits this same object
|
|
29837
30017
|
* @return {Array<any>} - What each object's type made, a function that made nothing is left out
|
|
29838
30018
|
* @memberof Level3D */
|
|
@@ -30057,7 +30237,24 @@ function level3DSceneApply(scene)
|
|
|
30057
30237
|
r.fogColor = color(scene.fogColor) || r.fogColor;
|
|
30058
30238
|
if (typeof scene.shadows === 'boolean')
|
|
30059
30239
|
r.shadows = scene.shadows;
|
|
30240
|
+
if (typeof scene.lensFlare === 'boolean')
|
|
30241
|
+
level3DSceneFlare(scene.lensFlare);
|
|
30242
|
+
}
|
|
30243
|
+
|
|
30244
|
+
// the sun's lens flare as a scene sets it: on makes one unless the sun has a flare already, the game's own or an
|
|
30245
|
+
// earlier level's, and off takes away the one a scene made, never the game's
|
|
30246
|
+
let level3DSunFlare;
|
|
30247
|
+
function level3DSceneFlare(on)
|
|
30248
|
+
{
|
|
30249
|
+
if (!on)
|
|
30250
|
+
{
|
|
30251
|
+
level3DSunFlare && !level3DSunFlare.destroyed && level3DSunFlare.destroy();
|
|
30252
|
+
level3DSunFlare = undefined;
|
|
30253
|
+
}
|
|
30254
|
+
else if (!level3DSunHasFlare())
|
|
30255
|
+
level3DSunFlare = new LensFlare3D;
|
|
30060
30256
|
}
|
|
30257
|
+
const level3DSunHasFlare = ()=> engineObjects.some((o)=> o instanceof LensFlare3D && !o.destroyed && !o.light);
|
|
30061
30258
|
|
|
30062
30259
|
// a vec3 of an array of three numbers, as the file has them, or the fallback
|
|
30063
30260
|
function level3DVector(value, fallback)
|
|
@@ -30407,7 +30604,8 @@ level3DAddType('Cylinder', function(pos, properties)
|
|
|
30407
30604
|
{ return level3DMakeShape(pos, properties, level3DCylinderMesh ||= buildCylinder()); },
|
|
30408
30605
|
{color: WHITE, tile: -1, solid: true});
|
|
30409
30606
|
// a light, a spotlight with a cone: cone is the angle in degrees from its forward out to the edge of its beam, the
|
|
30410
|
-
// object's rotation aims it, softness is how much of the cone fades,
|
|
30607
|
+
// object's rotation aims it, softness is how much of the cone fades, shadows makes it the one that casts them, and
|
|
30608
|
+
// lensFlare gives it a lens flare; not flare, since a property is also set on the object, where flare is the flare
|
|
30411
30609
|
level3DAddType('Light', function(pos, properties)
|
|
30412
30610
|
{
|
|
30413
30611
|
const light = new Light3D(pos, properties.radius, properties.color, properties.intensity);
|
|
@@ -30415,8 +30613,9 @@ level3DAddType('Light', function(pos, properties)
|
|
|
30415
30613
|
light.coneSoftness = properties.softness;
|
|
30416
30614
|
if (properties.shadows && render3D)
|
|
30417
30615
|
render3D.shadowLight = light;
|
|
30616
|
+
properties.lensFlare && light.addFlare();
|
|
30418
30617
|
return light;
|
|
30419
|
-
}, {color: WHITE, radius: 5, intensity: 1, cone: 0, softness: .2, shadows: false});
|
|
30618
|
+
}, {color: WHITE, radius: 5, intensity: 1, cone: 0, softness: .2, shadows: false, lensFlare: false});
|
|
30420
30619
|
|
|
30421
30620
|
/**
|
|
30422
30621
|
* LittleJS Particle Effects Plugin
|