littlejsengine 1.20.0 → 1.22.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/dist/littlejs.d.ts +1041 -81
- package/dist/littlejs.esm.js +6191 -379
- package/dist/littlejs.esm.min.js +22 -9
- package/dist/littlejs.js +6238 -476
- package/dist/littlejs.min.js +21 -8
- package/dist/littlejs.release.js +3500 -387
- package/package.json +3 -2
- package/plugins/gltf.js +61 -9
- package/plugins/lightSystem.js +44 -1
- package/plugins/math3d.js +132 -30
- package/plugins/particleEffects.js +745 -0
- package/plugins/pluginExport.js +42 -0
- package/plugins/postProcess.js +246 -39
- package/plugins/render3d.js +850 -182
- package/plugins/render3dDebug.js +2 -1
- package/plugins/render3dEditor.js +2576 -0
- package/plugins/render3dExtras.js +421 -15
- package/plugins/render3dLevel.js +352 -0
- package/plugins/render3dVoxels.js +12 -0
- package/plugins/sceneSystem.js +59 -0
- package/plugins/tweenSystem.js +4 -2
- package/src/engine.js +199 -34
- package/src/engineAudio.js +18 -9
- package/src/engineBuild.mjs +4 -0
- package/src/engineDebug.js +55 -1
- package/src/engineDraw.js +141 -7
- package/src/engineEditor.js +52 -22
- package/src/engineExport.js +8 -0
- package/src/engineInput.js +8 -4
- package/src/engineLogo.js +19 -0
- package/src/engineObject.js +3 -2
- package/src/engineParticles.js +80 -11
- package/src/engineRelease.js +13 -1
- package/src/engineSettings.js +38 -0
- package/src/engineWebGL.js +27 -10
|
@@ -38,6 +38,54 @@ function render3DSoftDot()
|
|
|
38
38
|
return render3DSoftDotTexture = new TextureInfo(context.canvas);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/** Make a normal map from a height at each pixel, for bumps and grooves with no image file: the slope at each pixel
|
|
42
|
+
* from its neighbors' heights, taken around the edges so the map tiles; set it as an object's normalMap
|
|
43
|
+
* @param {Vector2} size - In pixels
|
|
44
|
+
* @param {function(number, number): number} heightFunction - The height 0 to 1 at a pixel, x across and y down
|
|
45
|
+
* @param {number} [strength] - How steep the slopes are: a height change of 1 over one pixel leans the normal
|
|
46
|
+
* by strength
|
|
47
|
+
* @return {TextureInfo} - Wraps; headless it has no image
|
|
48
|
+
* @memberof Render3D */
|
|
49
|
+
function normalMapFromHeight(size, heightFunction, strength=1)
|
|
50
|
+
{
|
|
51
|
+
ASSERT(isVector2(size) && size.x >= 1 && size.y >= 1, 'normalMapFromHeight size must be a Vector2 of pixels');
|
|
52
|
+
const width = size.x | 0, height = size.y | 0;
|
|
53
|
+
if (typeof OffscreenCanvas == 'undefined')
|
|
54
|
+
{
|
|
55
|
+
// headless, nothing to draw into; the right size so what reads it still works
|
|
56
|
+
const textureInfo = new TextureInfo(undefined, false, true);
|
|
57
|
+
textureInfo.size = vec2(width, height), textureInfo.sizeInverse = vec2(1 / width, 1 / height);
|
|
58
|
+
return textureInfo;
|
|
59
|
+
}
|
|
60
|
+
const context = createCanvasContext(width, height);
|
|
61
|
+
const pixels = render3DNormalMapPixels(width, height, heightFunction, strength);
|
|
62
|
+
context.putImageData(new ImageData(pixels, width, height), 0, 0);
|
|
63
|
+
return new TextureInfo(context.canvas, true, true);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// the pixels of a normal map made from heights, rgba with the top row first: each normal leans away from the uphill
|
|
67
|
+
// side, green up the image as OpenGL has it, and a flat height is (128, 128, 255); rounded before they are stored,
|
|
68
|
+
// since the clamped array would round a flat 127.5 to even
|
|
69
|
+
function render3DNormalMapPixels(width, height, heightFunction, strength)
|
|
70
|
+
{
|
|
71
|
+
const heights = new Float32Array(width * height), pixels = new Uint8ClampedArray(width * height * 4);
|
|
72
|
+
for (let y = 0; y < height; ++y)
|
|
73
|
+
for (let x = 0; x < width; ++x)
|
|
74
|
+
heights[y * width + x] = heightFunction(x, y);
|
|
75
|
+
const h = (x, y)=> heights[mod(y, height) * width + mod(x, width)];
|
|
76
|
+
for (let y = 0; y < height; ++y)
|
|
77
|
+
for (let x = 0; x < width; ++x)
|
|
78
|
+
{
|
|
79
|
+
const dx = (h(x+1, y) - h(x-1, y)) / 2 * strength, dy = (h(x, y+1) - h(x, y-1)) / 2 * strength;
|
|
80
|
+
const k = 1 / hypot(dx, dy, 1), i = (y * width + x) * 4;
|
|
81
|
+
pixels[i] = round((-dx * k * .5 + .5) * 255);
|
|
82
|
+
pixels[i+1] = round((dy * k * .5 + .5) * 255);
|
|
83
|
+
pixels[i+2] = round((k * .5 + .5) * 255);
|
|
84
|
+
pixels[i+3] = 255;
|
|
85
|
+
}
|
|
86
|
+
return pixels;
|
|
87
|
+
}
|
|
88
|
+
|
|
41
89
|
///////////////////////////////////////////////////////////////////////////////
|
|
42
90
|
// The rest of the shape builders: buildLathe, buildSphere, buildBox, buildGrid and buildSky live with the
|
|
43
91
|
// renderer, since it hands those out itself
|
|
@@ -574,6 +622,22 @@ class HeightMap extends EngineObject3D
|
|
|
574
622
|
* @ignore */
|
|
575
623
|
levelRaycast3D(ray) { return this.raycast(ray); }
|
|
576
624
|
|
|
625
|
+
/** Where a short move goes under the surface, for a particle's move in one frame: the part of the move made
|
|
626
|
+
* before it, 0 to 1, and the surface normal there; undefined when it stays above, starts under, or is off the map
|
|
627
|
+
* @param {Vector3} from
|
|
628
|
+
* @param {Vector3} to
|
|
629
|
+
* @return {{distance: number, normal: Vector3}|undefined}
|
|
630
|
+
* @ignore */
|
|
631
|
+
levelSegment3D(from, to)
|
|
632
|
+
{
|
|
633
|
+
const m = this.pos3D, size = this.mapSize;
|
|
634
|
+
if (abs(to.x - m.x) > size.x / 2 || abs(to.z - m.z) > size.y / 2) return; // off the map
|
|
635
|
+
const a = from.y - this.getHeight(from.x, from.z), b = to.y - this.getHeight(to.x, to.z);
|
|
636
|
+
if (a < 0 || b >= 0) return; // above all the way, or under from the start
|
|
637
|
+
const distance = a / (a - b);
|
|
638
|
+
return {distance, normal: this.getNormal(from.x + (to.x - from.x) * distance, from.z + (to.z - from.z) * distance)};
|
|
639
|
+
}
|
|
640
|
+
|
|
577
641
|
/** Keep an object above the ground, called by the engine for each object with collideLevel
|
|
578
642
|
* @param {EngineObject3D} o
|
|
579
643
|
* @param {Vector3} oldPos - Where it was before it moved
|
|
@@ -762,7 +826,7 @@ class FirstPersonCamera3D extends EngineObject3D
|
|
|
762
826
|
/** @property {boolean} - Capture the mouse on a click, so looking needs no button held */
|
|
763
827
|
this.lockPointer = true;
|
|
764
828
|
/** @property {number} - Speed of a jump in world units a frame, 0 for none; Space or gamepad button 0 jumps
|
|
765
|
-
* while it stands on something, a height map
|
|
829
|
+
* while it stands on something, a height map, a voxel map or a solid, see groundObject */
|
|
766
830
|
this.jumpSpeed = 0;
|
|
767
831
|
/** @property {number} - How far above its position the eye is, in its own space, so with a size3D the eye can
|
|
768
832
|
* sit toward the top of the body instead of its middle; keep it under half the body's height, or the eye is
|
|
@@ -833,6 +897,51 @@ class FirstPersonCamera3D extends EngineObject3D
|
|
|
833
897
|
const RENDER3D_PARTICLE_FLOATS = 21;
|
|
834
898
|
// the position, color and size of the particle being drawn, shared by every emitter so the draw loop makes no objects
|
|
835
899
|
const render3DParticlePos = vec3(), render3DParticleColor = new Color, render3DParticleSize = vec2();
|
|
900
|
+
// where a particle was and is, for the level collision
|
|
901
|
+
const render3DParticleFrom = vec3(), render3DParticleTo = vec3();
|
|
902
|
+
|
|
903
|
+
// the emitters inside a particle callback now, whose nested calls each get a view of their own
|
|
904
|
+
const render3DParticlesCalling = new Set;
|
|
905
|
+
|
|
906
|
+
// a particle as a ParticleEmitter3D's callbacks see it, set from the particle for each call
|
|
907
|
+
/** @param {ParticleEmitter3D} emitter
|
|
908
|
+
* @return {Particle3D} */
|
|
909
|
+
function render3DParticleView(emitter)
|
|
910
|
+
{
|
|
911
|
+
return {emitter, pos: vec3(), velocity: vec3(), age: 0, lifeTime: 0, scale: 1, destroyed: false,
|
|
912
|
+
destroy() { this.destroyed = true; }};
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* A particle as a ParticleEmitter3D's callbacks see it: one object the emitter reuses, set from the particle for each
|
|
917
|
+
* call and written back after it, so copy what you keep
|
|
918
|
+
* @typedef {Object} Particle3D
|
|
919
|
+
* @property {ParticleEmitter3D} emitter - The emitter it is in
|
|
920
|
+
* @property {Vector3} pos - Where it is, change it to move it
|
|
921
|
+
* @property {Vector3} velocity - How far it moves each frame
|
|
922
|
+
* @property {number} age - Seconds it has lived
|
|
923
|
+
* @property {number} lifeTime - Seconds it lives
|
|
924
|
+
* @property {number} scale - How much the emitter grows its effect, from its scale3D and its parents', as the 2D
|
|
925
|
+
* particle's scale
|
|
926
|
+
* @property {boolean} destroyed - Set by destroy
|
|
927
|
+
* @property {function(): void} destroy - End it this update, the destroy callback gets it
|
|
928
|
+
* @memberof Render3D
|
|
929
|
+
*/
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* @callback Particle3DCallback - A function a ParticleEmitter3D calls with one of its particles
|
|
933
|
+
* @param {Particle3D} particle
|
|
934
|
+
* @memberof Render3D
|
|
935
|
+
*/
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* @callback Particle3DCollideCallback - Decides whether a particle stops where it hits the level, a filter as in 2D
|
|
939
|
+
* @param {Particle3D} particle
|
|
940
|
+
* @param {EngineObject3D} level - The HeightMap or VoxelMap it hit
|
|
941
|
+
* @param {Vector3} pos - Where it hit
|
|
942
|
+
* @return {boolean|void} - true to stop it there; a callback that returns nothing lets it pass through
|
|
943
|
+
* @memberof Render3D
|
|
944
|
+
*/
|
|
836
945
|
|
|
837
946
|
/**
|
|
838
947
|
* ParticleEmitter3D - Spawns camera facing particles, the 3D twin of ParticleEmitter
|
|
@@ -843,9 +952,14 @@ const render3DParticlePos = vec3(), render3DParticleColor = new Color, render3DP
|
|
|
843
952
|
* - emitConeAngle spreads them, PI sprays in every direction
|
|
844
953
|
* - Speeds are per frame and sizes are world units, the same as the 2D emitter
|
|
845
954
|
* - scale3D, its own or a parent's, grows the whole effect: the spawn area, the sizes, the speed and the fall
|
|
846
|
-
* - gravity here is its own number added to velocity y each frame
|
|
847
|
-
*
|
|
955
|
+
* - gravity here is its own number added to velocity y each frame, so an effect keeps its own fall wherever it is
|
|
956
|
+
* used, the same as the 2D emitter's gravity; gravityScale adds a share of render3D.gravity on top, as the 2D
|
|
957
|
+
* emitter's gravityScale adds the engine's gravity
|
|
848
958
|
* - An emitter with an emitTime destroys itself once its last particle is gone, like the 2D emitter
|
|
959
|
+
* - Callbacks as the 2D emitter's: particleCreateCallback, particleUpdateCallback, particleCollideCallback and
|
|
960
|
+
* particleDestroyCallback, each given a Particle3D, one object the emitter reuses for every particle and call
|
|
961
|
+
* - collideLevel, off by default, has particles hit the height maps and voxel maps, bouncing by restitution and
|
|
962
|
+
* sliding by friction
|
|
849
963
|
* @extends EngineObject3D
|
|
850
964
|
* @memberof Render3D
|
|
851
965
|
* @example
|
|
@@ -870,8 +984,8 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
870
984
|
* @param {number} [sizeEnd] - Particle size at end of life
|
|
871
985
|
* @param {number} [speed] - Spawn speed in world units per frame
|
|
872
986
|
* @param {number} [damping] - Per frame velocity multiplier, 1 is none
|
|
873
|
-
* @param {number} [gravity] - Per frame change to velocity y, negative pulls down; its own number,
|
|
874
|
-
*
|
|
987
|
+
* @param {number} [gravity] - Per frame change to velocity y, negative pulls down; its own number, and
|
|
988
|
+
* gravityScale adds a share of render3D.gravity on top
|
|
875
989
|
* @param {number} [fadeRate] - Fraction of life spent fading, half in and half out
|
|
876
990
|
* @param {number} [randomness] - Extra randomness applied to speed, size and life
|
|
877
991
|
* @param {boolean} [additive] - Additive blending */
|
|
@@ -886,6 +1000,9 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
886
1000
|
|
|
887
1001
|
/** @property {number|Vector3} - Spawn area, a number for a sphere diameter or a vec3 for a box */
|
|
888
1002
|
this.emitSize = emitSize;
|
|
1003
|
+
/** @property {boolean} - Flatten the spawn area across the way it emits, its own up: a sphere becomes a disc
|
|
1004
|
+
* and a box a flat rectangle, for rain from a sheet of sky or flames from a patch of ground */
|
|
1005
|
+
this.emitFlat = false;
|
|
889
1006
|
/** @property {number} - How long to keep emitting, 0 is forever */
|
|
890
1007
|
this.emitTime = emitTime;
|
|
891
1008
|
/** @property {number} - Particles per second, 0 does not emit */
|
|
@@ -912,13 +1029,17 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
912
1029
|
this.damping = damping;
|
|
913
1030
|
/** @property {number} - Per frame change to velocity y, its own number and not render3D.gravity */
|
|
914
1031
|
this.gravity = gravity;
|
|
1032
|
+
/** @property {number} - Share of render3D.gravity added each frame on top of its own gravity, 0 by default so
|
|
1033
|
+
* an effect keeps its own fall wherever it is used; 1 falls with the world, like debris */
|
|
1034
|
+
this.gravityScale = 0;
|
|
915
1035
|
/** @property {number} - Fraction of life spent fading, half in and half out */
|
|
916
1036
|
this.fadeRate = fadeRate;
|
|
917
1037
|
/** @property {number} - Extra randomness applied to speed, size and life */
|
|
918
1038
|
this.randomness = randomness;
|
|
919
1039
|
/** @property {boolean} - Additive blending */
|
|
920
1040
|
this.additive = additive;
|
|
921
|
-
/** @property {number} - Seconds of each particle's path to draw as a ribbon behind it, 0 draws billboards
|
|
1041
|
+
/** @property {number} - Seconds of each particle's path to draw as a ribbon behind it, 0 draws billboards;
|
|
1042
|
+
* with engineVariableStep it keeps trailTime * frameRate updates of path */
|
|
922
1043
|
this.trailTime = 0;
|
|
923
1044
|
/** @property {number} - Radians per frame each particle turns in the camera plane, either way; 0 is no spin */
|
|
924
1045
|
this.angleSpeed = 0;
|
|
@@ -934,6 +1055,25 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
934
1055
|
this.trailData = undefined;
|
|
935
1056
|
/** @property {number} - Trail points kept per particle, from trailTime */
|
|
936
1057
|
this.trailMax = 0;
|
|
1058
|
+
/** @property {boolean} - Particles hit the level, the height maps and voxel maps, bouncing by restitution and
|
|
1059
|
+
* sliding along by friction; off by default, it tests each particle's move against the level every frame */
|
|
1060
|
+
this.collideLevel = false;
|
|
1061
|
+
/** @property {Particle3DCallback|undefined} - Called with each particle as it is made
|
|
1062
|
+
* @type {Particle3DCallback|undefined} */
|
|
1063
|
+
this.particleCreateCallback = undefined;
|
|
1064
|
+
/** @property {Particle3DCallback|undefined} - Called with each particle each update, after it moves
|
|
1065
|
+
* @type {Particle3DCallback|undefined} */
|
|
1066
|
+
this.particleUpdateCallback = undefined;
|
|
1067
|
+
/** @property {Particle3DCollideCallback|undefined} - Decides if a particle stops where it hits the level,
|
|
1068
|
+
* with collideLevel on; a callback that returns nothing lets it through
|
|
1069
|
+
* @type {Particle3DCollideCallback|undefined} */
|
|
1070
|
+
this.particleCollideCallback = undefined;
|
|
1071
|
+
/** @property {Particle3DCallback|undefined} - Called with each particle as it goes, its life over or destroyed
|
|
1072
|
+
* @type {Particle3DCallback|undefined} */
|
|
1073
|
+
this.particleDestroyCallback = undefined;
|
|
1074
|
+
/** @property {Particle3D} - The particle the callbacks get, one object for every particle and call
|
|
1075
|
+
* @type {Particle3D} */
|
|
1076
|
+
this.particleView = render3DParticleView(this);
|
|
937
1077
|
/** @property {Vector3|undefined} - Where the emitter was at its last update, for when its parent is destroyed
|
|
938
1078
|
* @type {Vector3|undefined} */
|
|
939
1079
|
this.worldPos3D = undefined;
|
|
@@ -947,6 +1087,7 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
947
1087
|
const matrix = render3DObjectMatrix(this); // the object's own, read only
|
|
948
1088
|
this.worldPos3D = matrix.getTranslation(); // remembered for when the parent is destroyed
|
|
949
1089
|
const scale = render3DMaxScale(matrix.m);
|
|
1090
|
+
this.particleView.scale = scale; // the callbacks read it from the view instead of working it out each time
|
|
950
1091
|
|
|
951
1092
|
// emit until the emit time is up, then wait for the last particle and go away
|
|
952
1093
|
if (!this.emitTime || this.getAliveTime() <= this.emitTime)
|
|
@@ -963,8 +1104,9 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
963
1104
|
else if (!this.particleCount)
|
|
964
1105
|
this.destroy();
|
|
965
1106
|
|
|
966
|
-
// the trail storage follows trailTime, a change starts every trail over
|
|
967
|
-
|
|
1107
|
+
// the trail storage follows trailTime at the fixed rate, a change starts every trail over, so a varying
|
|
1108
|
+
// timeDelta can not; with engineVariableStep a trail keeps that many updates
|
|
1109
|
+
const trailMax = this.trailTime ? max(1, round(this.trailTime * frameRate)) : 0;
|
|
968
1110
|
if (trailMax !== this.trailMax)
|
|
969
1111
|
{
|
|
970
1112
|
this.trailMax = trailMax;
|
|
@@ -974,16 +1116,27 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
974
1116
|
}
|
|
975
1117
|
|
|
976
1118
|
// move the particles and drop the dead ones, all in the typed array: this runs for every particle every frame
|
|
977
|
-
|
|
978
|
-
const
|
|
1119
|
+
// a callback can emit, which grows the arrays when they are full, so they are read again after each one
|
|
1120
|
+
const F = RENDER3D_PARTICLE_FLOATS;
|
|
1121
|
+
let data = this.particleData, trail = this.trailData;
|
|
1122
|
+
const reread = ()=> { data = this.particleData, trail = this.trailData; };
|
|
1123
|
+
// its own fall and a share of the world's, grown by the scale: a bigger effect has to fall faster to keep the
|
|
1124
|
+
// same arc
|
|
1125
|
+
const damping = this.damping, angleDamping = this.angleDamping, g = render3D.gravity, share = this.gravityScale;
|
|
1126
|
+
const gravityX = g.x * share * scale, gravityY = (this.gravity + g.y * share) * scale;
|
|
1127
|
+
const gravityZ = g.z * share * scale;
|
|
1128
|
+
const collideLevel = this.collideLevel && render3DLevel.length;
|
|
1129
|
+
const updateCallback = this.particleUpdateCallback, destroyCallback = this.particleDestroyCallback;
|
|
979
1130
|
for (let i = this.particleCount; i--;)
|
|
980
1131
|
{
|
|
981
1132
|
// damped first and gravity added after, the order the 2D particle uses, so the same
|
|
982
1133
|
// damping and gravity give the same arc in both
|
|
983
1134
|
const k = i * F;
|
|
984
|
-
const vx = data[k+3]
|
|
1135
|
+
const vx = data[k+3] = data[k+3] * damping + gravityX, vy = data[k+4] = data[k+4] * damping + gravityY;
|
|
1136
|
+
const vz = data[k+5] = data[k+5] * damping + gravityZ;
|
|
985
1137
|
data[k] += vx, data[k+1] += vy, data[k+2] += vz;
|
|
986
1138
|
data[k+18] += data[k+19] *= angleDamping;
|
|
1139
|
+
collideLevel && (this.particleCollide(k, data[k] - vx, data[k+1] - vy, data[k+2] - vz), reread());
|
|
987
1140
|
const t = i * trailMax * 3;
|
|
988
1141
|
if (trailMax)
|
|
989
1142
|
{
|
|
@@ -995,9 +1148,11 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
995
1148
|
trail[j] = data[k], trail[j+1] = data[k+1], trail[j+2] = data[k+2];
|
|
996
1149
|
data[k+20] = n + 1;
|
|
997
1150
|
}
|
|
1151
|
+
updateCallback && (this.particleCall(updateCallback, k), reread());
|
|
998
1152
|
if ((data[k+17] += timeDelta) >= data[k+16])
|
|
999
1153
|
{
|
|
1000
1154
|
// dead: the last particle takes its slot, trail and all, order does not matter
|
|
1155
|
+
destroyCallback && (this.particleCall(destroyCallback, k), reread());
|
|
1001
1156
|
const last = --this.particleCount, kl = last * F;
|
|
1002
1157
|
if (i !== last)
|
|
1003
1158
|
{
|
|
@@ -1026,10 +1181,18 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1026
1181
|
// the whole effect grows with the emitter, not just the area the particles start in
|
|
1027
1182
|
const scale = render3DMaxScale(matrix.m);
|
|
1028
1183
|
|
|
1029
|
-
// spawn offset: inside a box or a sphere
|
|
1030
|
-
const size = this.emitSize, box = /** @type {Vector3} */ (size);
|
|
1031
|
-
|
|
1032
|
-
|
|
1184
|
+
// spawn offset: inside a box or a sphere, or flat across the emitter's up, a flat rectangle or a disc
|
|
1185
|
+
const size = this.emitSize, box = /** @type {Vector3} */ (size), flat = this.emitFlat;
|
|
1186
|
+
let offset;
|
|
1187
|
+
if (isVector3(size))
|
|
1188
|
+
offset = vec3(rand(-.5, .5) * box.x, flat ? 0 : rand(-.5, .5) * box.y, rand(-.5, .5) * box.z);
|
|
1189
|
+
else if (flat)
|
|
1190
|
+
{
|
|
1191
|
+
const disc = randInCircle(/** @type {number} */ (size) / 2);
|
|
1192
|
+
offset = vec3(disc.x, 0, disc.y);
|
|
1193
|
+
}
|
|
1194
|
+
else
|
|
1195
|
+
offset = randInSphere(/** @type {number} */ (size) / 2);
|
|
1033
1196
|
|
|
1034
1197
|
// direction inside the cone around local +Y
|
|
1035
1198
|
const direction = matrix.transformDirection(randVector3(1, this.emitConeAngle)).normalize();
|
|
@@ -1064,6 +1227,60 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1064
1227
|
data[k+18] = this.angleSpeed ? rand(2*PI) : 0;
|
|
1065
1228
|
data[k+19] = this.angleSpeed ? this.angleSpeed * random() * randSign() : 0;
|
|
1066
1229
|
data[k+20] = 0; // trail points
|
|
1230
|
+
this.particleCreateCallback && this.particleCall(this.particleCreateCallback, k);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// call a particle callback with the particle at k: the emitter's one Particle3D is set from its numbers, and
|
|
1234
|
+
// what the callback changes is written back; a particle it destroys has lived its life, the update removes it
|
|
1235
|
+
particleCall(callback, k, level, pos)
|
|
1236
|
+
{
|
|
1237
|
+
// a callback that emits runs the create callback inside itself, which gets a view of its own so this one
|
|
1238
|
+
// keeps its particle; what the callback changes is written to the arrays as they are after it, since an
|
|
1239
|
+
// emit into a full emitter grows them
|
|
1240
|
+
const nested = render3DParticlesCalling.has(this);
|
|
1241
|
+
const view = nested ? render3DParticleView(this) : this.particleView, data = this.particleData;
|
|
1242
|
+
view.pos.set(data[k], data[k+1], data[k+2]);
|
|
1243
|
+
view.velocity.set(data[k+3], data[k+4], data[k+5]);
|
|
1244
|
+
view.age = data[k+17], view.lifeTime = data[k+16], view.destroyed = false;
|
|
1245
|
+
render3DParticlesCalling.add(this);
|
|
1246
|
+
let result;
|
|
1247
|
+
try { result = callback(view, level, pos); }
|
|
1248
|
+
finally { nested || render3DParticlesCalling.delete(this); }
|
|
1249
|
+
const out = this.particleData;
|
|
1250
|
+
out[k] = view.pos.x, out[k+1] = view.pos.y, out[k+2] = view.pos.z;
|
|
1251
|
+
out[k+3] = view.velocity.x, out[k+4] = view.velocity.y, out[k+5] = view.velocity.z;
|
|
1252
|
+
if (view.destroyed)
|
|
1253
|
+
out[k+17] = max(out[k+17], out[k+16] - timeDelta); // aged past its life this update
|
|
1254
|
+
return result;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// stop the particle at k where its last move went into the level, from where it was: back at the surface, a hair
|
|
1258
|
+
// off it, its speed into it turned around by restitution and its speed along it kept by friction, unless the
|
|
1259
|
+
// collide callback lets it through; one that starts inside is let go, as a 2D one is
|
|
1260
|
+
particleCollide(k, x, y, z)
|
|
1261
|
+
{
|
|
1262
|
+
let data = this.particleData;
|
|
1263
|
+
const from = render3DParticleFrom.set(x, y, z);
|
|
1264
|
+
const to = render3DParticleTo.set(data[k], data[k+1], data[k+2]);
|
|
1265
|
+
let hit, level;
|
|
1266
|
+
for (const l of render3DLevel)
|
|
1267
|
+
{
|
|
1268
|
+
const h = l.destroyed ? undefined : l.levelSegment3D(from, to);
|
|
1269
|
+
if (h && (!hit || h.distance < hit.distance))
|
|
1270
|
+
hit = h, level = l;
|
|
1271
|
+
}
|
|
1272
|
+
if (!hit) return;
|
|
1273
|
+
const point = from.lerp(to, hit.distance);
|
|
1274
|
+
const callback = this.particleCollideCallback;
|
|
1275
|
+
if (callback && (!this.particleCall(callback, k, level, point.copy()) || this.particleView.destroyed))
|
|
1276
|
+
return; // let through, or destroyed by the callback
|
|
1277
|
+
data = this.particleData; // as the callback left it, an emit may have grown it
|
|
1278
|
+
|
|
1279
|
+
const n = hit.normal, v = vec3(data[k+3], data[k+4], data[k+5]), into = n.scale(v.dot(n));
|
|
1280
|
+
const restitution = max(this.restitution, level.restitution), friction = max(this.friction, level.friction);
|
|
1281
|
+
const out = v.subtract(into).scale(friction).subtract(into.scale(restitution)), p = point.add(n.scale(1e-3));
|
|
1282
|
+
data[k] = p.x, data[k+1] = p.y, data[k+2] = p.z;
|
|
1283
|
+
data[k+3] = out.x, data[k+4] = out.y, data[k+5] = out.z;
|
|
1067
1284
|
}
|
|
1068
1285
|
|
|
1069
1286
|
/** Draw the particles, as flat squares or as streaks when trailTime is set
|
|
@@ -1245,6 +1462,195 @@ class Trail3D extends EngineObject3D
|
|
|
1245
1462
|
}
|
|
1246
1463
|
}
|
|
1247
1464
|
|
|
1465
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
1466
|
+
// lens flare
|
|
1467
|
+
|
|
1468
|
+
// the shapes a flare is made of, white on clear on one smooth texture made the first time it is asked for: a soft
|
|
1469
|
+
// glow, a flat disc with a brighter rim, and a ring; undefined headless or without a canvas
|
|
1470
|
+
let render3DFlareTiles;
|
|
1471
|
+
function render3DFlareTile(shape)
|
|
1472
|
+
{
|
|
1473
|
+
if (!render3DFlareTiles)
|
|
1474
|
+
{
|
|
1475
|
+
if (headlessMode || !glContext || typeof OffscreenCanvas == 'undefined') return;
|
|
1476
|
+
const cell = 128, shapes = ['glow', 'disc', 'ring'], context = createCanvasContext(cell * shapes.length, cell);
|
|
1477
|
+
// each shape is how see-through it is from its middle out
|
|
1478
|
+
const alpha =
|
|
1479
|
+
{
|
|
1480
|
+
glow: (t)=> engineGlowAlpha(t, 1),
|
|
1481
|
+
disc: (t)=> t < .8 ? .4 + .1 * t : t < .92 ? .48 + (t - .8) * 3 : (1 - t) / .08 * .84,
|
|
1482
|
+
ring: (t)=> max(0, 1 - abs(t - .86) / .12),
|
|
1483
|
+
};
|
|
1484
|
+
shapes.forEach((shape, i)=>
|
|
1485
|
+
{
|
|
1486
|
+
const x = i * cell + cell / 2, y = cell / 2, r = cell / 2 - 2, steps = 32;
|
|
1487
|
+
const gradient = context.createRadialGradient(x, y, 0, x, y, r);
|
|
1488
|
+
for (let k = 0; k <= steps; ++k)
|
|
1489
|
+
gradient.addColorStop(k / steps, 'rgba(255,255,255,' + clamp(alpha[shape](k / steps)).toFixed(4) + ')');
|
|
1490
|
+
context.fillStyle = gradient;
|
|
1491
|
+
context.beginPath();
|
|
1492
|
+
context.arc(x, y, r, 0, 2 * PI);
|
|
1493
|
+
context.fill();
|
|
1494
|
+
});
|
|
1495
|
+
const texture = new TextureInfo(context.canvas, true, false, false); // smooth even in a pixel art game
|
|
1496
|
+
render3DFlareTiles = new Map(shapes.map((shape, i)=>
|
|
1497
|
+
[shape, new TileInfo(vec2(i * cell, 0), vec2(cell), texture)]));
|
|
1498
|
+
}
|
|
1499
|
+
return render3DFlareTiles.get(shape) || render3DFlareTiles.get('glow');
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
/**
|
|
1503
|
+
* LensFlare3D - The sun's lens flare, the old kind: a glow at the sun and a row of discs and rings of different
|
|
1504
|
+
* sizes along the line from the sun through the middle of the screen
|
|
1505
|
+
* - Make one and it shows, over the 3D scene and under what the game draws after, a HUD; destroy it to take it away
|
|
1506
|
+
* - It follows render3D.sunDirection, and fades out as the sun leaves the screen or goes behind something
|
|
1507
|
+
* - flareSize, count, intensity and saturation set its look, seed picks another arrangement, and its color tints it,
|
|
1508
|
+
* with the sun's own color; or give it elements of your own
|
|
1509
|
+
* - visible is how much of the sun shows, 0 to 1, eased over fadeTime, there for a game to read
|
|
1510
|
+
* - What hides the sun is found with a ray from the camera, against the level and every object that is not see
|
|
1511
|
+
* through, each as the box around its mesh, see render3D.pick; turn it off with occlusion
|
|
1512
|
+
* - It needs WebGL, and it draws nothing in the shadow of renderAfter2D
|
|
1513
|
+
* @extends EngineObject3D
|
|
1514
|
+
* @memberof Render3D
|
|
1515
|
+
* @example
|
|
1516
|
+
* new LensFlare3D; // the sun flares
|
|
1517
|
+
* new LensFlare3D(1.5, 10, .7, 0); // bigger, 10 ghosts, dimmer, all one color
|
|
1518
|
+
*/
|
|
1519
|
+
class LensFlare3D extends EngineObject3D
|
|
1520
|
+
{
|
|
1521
|
+
/** Create the sun's lens flare
|
|
1522
|
+
* @param {number} [size] - Scales every part of it, 1 by default
|
|
1523
|
+
* @param {number} [count] - How many ghosts there are along the line, besides the glow at the sun
|
|
1524
|
+
* @param {number} [intensity] - How bright it is
|
|
1525
|
+
* @param {number} [saturation] - How colorful the ghosts are, 0 for all the flare's own color, 1 a rainbow
|
|
1526
|
+
* @param {Color} [color] - Tints the whole flare, with the sun's color */
|
|
1527
|
+
constructor(size=1, count=7, intensity=1, saturation=1, color=WHITE)
|
|
1528
|
+
{
|
|
1529
|
+
super(vec3(), undefined, undefined, color);
|
|
1530
|
+
this.size3D = vec3(); // not a thing to pick or collect
|
|
1531
|
+
/** @property {number} - Scales every part of the flare */
|
|
1532
|
+
this.flareSize = size;
|
|
1533
|
+
/** @property {number} - How many ghosts there are along the line, besides the glow at the sun */
|
|
1534
|
+
this.count = count;
|
|
1535
|
+
/** @property {number} - How bright it is */
|
|
1536
|
+
this.intensity = intensity;
|
|
1537
|
+
/** @property {number} - How colorful the ghosts are, 0 for all the flare's own color, 1 a rainbow */
|
|
1538
|
+
this.saturation = saturation;
|
|
1539
|
+
/** @property {number} - Picks the arrangement of the ghosts, another seed is another flare */
|
|
1540
|
+
this.seed = 1;
|
|
1541
|
+
/** @property {Array<{at: number, size: number, color: Color, shape: string}>|undefined} - The parts of the
|
|
1542
|
+
* flare, to set your own in place of the ones made from count, seed and saturation: at is where along the
|
|
1543
|
+
* line, 0 the sun, 1 the middle of the screen, 2 as far past it; size is across, as a part of the screen's
|
|
1544
|
+
* height; shape is glow, disc or ring
|
|
1545
|
+
* @type {Array<{at: number, size: number, color: Color, shape: string}>|undefined} */
|
|
1546
|
+
this.elements = undefined;
|
|
1547
|
+
/** @property {boolean} - Fade out when something is between the camera and the sun */
|
|
1548
|
+
this.occlusion = true;
|
|
1549
|
+
/** @property {number} - Seconds the flare takes to fade out or in when the sun is hidden or shows again */
|
|
1550
|
+
this.fadeTime = .15;
|
|
1551
|
+
/** @property {number} - How much of the sun shows, 0 hidden or behind the camera to 1 in plain view, eased */
|
|
1552
|
+
this.visible = 1;
|
|
1553
|
+
this.renderOrder = 1e9; // over the game's sprites, it is light in the lens
|
|
1554
|
+
this.madeKey = '';
|
|
1555
|
+
/** @type {Array<{at: number, size: number, color: Color, shape: string}>} */
|
|
1556
|
+
this.made = [];
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
/** The parts of the flare: the elements set by hand, or the ones made from count, seed and saturation, a glow
|
|
1560
|
+
* and a core at the sun and the ghosts, made again when one of those changes
|
|
1561
|
+
* @return {Array<{at: number, size: number, color: Color, shape: string}>} */
|
|
1562
|
+
getElements()
|
|
1563
|
+
{
|
|
1564
|
+
if (this.elements) return this.elements;
|
|
1565
|
+
const key = [this.count, this.seed, this.saturation].join();
|
|
1566
|
+
if (key !== this.madeKey)
|
|
1567
|
+
{
|
|
1568
|
+
const random = new RandomGenerator(this.seed * 7919 + 1), s = clamp(this.saturation);
|
|
1569
|
+
const made = this.made = [
|
|
1570
|
+
{at: 0, size: .7, color: hsl(0, 0, 1, .5), shape: 'glow'},
|
|
1571
|
+
{at: 0, size: .25, color: hsl(0, 0, 1, .9), shape: 'glow'}];
|
|
1572
|
+
for (let i = 0; i < this.count; ++i)
|
|
1573
|
+
{
|
|
1574
|
+
// spread along the line, each a place of its own, the far ones bigger
|
|
1575
|
+
const at = (i + random.float(.2, .8)) / max(this.count, 1) * 1.9 + .2;
|
|
1576
|
+
const pick = random.float(), shape = pick < .5 ? 'disc' : pick < .8 ? 'ring' : 'glow';
|
|
1577
|
+
made.push({at, size: random.float(.04, .1) * (1 + at), shape,
|
|
1578
|
+
color: hsl(random.float(), s * .9, .6, random.float(.15, .4))});
|
|
1579
|
+
}
|
|
1580
|
+
this.madeKey = key;
|
|
1581
|
+
}
|
|
1582
|
+
return this.made;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
/** Where the sun is on the screen, in pixels like mousePosScreen, undefined when it is behind the camera
|
|
1586
|
+
* @return {Vector2|undefined} */
|
|
1587
|
+
getSunScreenPos()
|
|
1588
|
+
{
|
|
1589
|
+
const direction = render3D.sunDirection;
|
|
1590
|
+
if (!direction.lengthSquared()) return;
|
|
1591
|
+
return render3D.worldToScreen(render3D.camera.pos.add(direction.normalize(100)));
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
/** The parts of the flare as they are drawn now: each one's place on the screen, its size in pixels and its
|
|
1595
|
+
* color, dimmed by how much of the sun shows; empty when there is nothing to draw
|
|
1596
|
+
* @return {Array<{pos: Vector2, size: number, color: Color, shape: string}>} */
|
|
1597
|
+
getScreenElements()
|
|
1598
|
+
{
|
|
1599
|
+
const sun = this.getSunScreenPos(), center = mainCanvasSize.scale(.5);
|
|
1600
|
+
if (!sun || !center.x || !center.y) return [];
|
|
1601
|
+
// it fades as the sun leaves the screen, gone when it is a third of the screen past the edge
|
|
1602
|
+
const off = max(abs(sun.x - center.x) / center.x, abs(sun.y - center.y) / center.y);
|
|
1603
|
+
const strength = this.visible * clamp((1.3 - off) / .5) * this.intensity;
|
|
1604
|
+
if (!(strength > 0)) return [];
|
|
1605
|
+
const tint = this.color.multiply(render3D.sunColor), height = mainCanvasSize.y * this.flareSize;
|
|
1606
|
+
return this.getElements().map((e)=>
|
|
1607
|
+
{
|
|
1608
|
+
const c = e.color.multiply(tint);
|
|
1609
|
+
// along the line and past the middle, a lerp would stop there
|
|
1610
|
+
return {pos: sun.add(center.subtract(sun).scale(e.at)), size: e.size * height, shape: e.shape,
|
|
1611
|
+
color: rgb(c.r, c.g, c.b, c.a * strength)};
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
/** Is something between the camera and the sun: the level, or an object that is not see through
|
|
1616
|
+
* @return {boolean} */
|
|
1617
|
+
isSunHidden()
|
|
1618
|
+
{
|
|
1619
|
+
const direction = render3D.sunDirection;
|
|
1620
|
+
if (!direction.lengthSquared()) return true;
|
|
1621
|
+
const blockers = engineObjects.filter((o)=> o !== this && o instanceof EngineObject3D && !o.transparent &&
|
|
1622
|
+
!o.additive);
|
|
1623
|
+
return !!render3D.pick(new Ray3D(render3D.camera.pos, direction.normalize()), blockers);
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
/** Ease visible toward whether the sun shows, called automatically each frame */
|
|
1627
|
+
update()
|
|
1628
|
+
{
|
|
1629
|
+
super.update();
|
|
1630
|
+
const shows = !!this.getSunScreenPos() && !(this.occlusion && this.isSunHidden());
|
|
1631
|
+
const step = this.fadeTime > 0 ? timeDelta / this.fadeTime : 1;
|
|
1632
|
+
this.visible = clamp(this.visible + (shows ? step : -step));
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
/** Draw the flare over the 3D scene, added onto it, called automatically in the 2D pass */
|
|
1636
|
+
render()
|
|
1637
|
+
{
|
|
1638
|
+
if (headlessMode || !glEnable) return;
|
|
1639
|
+
const elements = this.getScreenElements();
|
|
1640
|
+
if (!elements.length) return;
|
|
1641
|
+
setAdditiveBlendMode(true);
|
|
1642
|
+
for (const e of elements)
|
|
1643
|
+
{
|
|
1644
|
+
const tileInfo = render3DFlareTile(e.shape);
|
|
1645
|
+
tileInfo && drawTile(e.pos, vec2(e.size), tileInfo, e.color, 0, false, undefined, true, true);
|
|
1646
|
+
}
|
|
1647
|
+
setAdditiveBlendMode(false);
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
/** A flare has nothing to draw in the 3D pass */
|
|
1651
|
+
render3D() {}
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1248
1654
|
///////////////////////////////////////////////////////////////////////////////
|
|
1249
1655
|
// OBJ meshes
|
|
1250
1656
|
|