littlejsengine 1.25.0 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +87 -0
- package/FAQ.md +49 -11
- package/README.md +6 -2
- package/REFERENCE.md +120 -18
- package/dist/littlejs.d.ts +464 -99
- package/dist/littlejs.esm.js +2853 -704
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +2844 -701
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +2658 -512
- package/package.json +4 -4
- package/plugins/box2d.js +110 -34
- package/plugins/gltf.js +74 -22
- package/plugins/lightSystem.js +410 -28
- package/plugins/math3d.js +4 -2
- package/plugins/medalSystem.js +19 -7
- package/plugins/newgrounds.js +4 -2
- package/plugins/parallax.js +1 -0
- package/plugins/particleEffects.js +44 -10
- package/plugins/pathFinder.js +9 -11
- package/plugins/pluginExport.js +9 -0
- package/plugins/postProcess.js +4 -2
- package/plugins/render3d.js +485 -2385
- package/plugins/render3dDebug.js +5 -8
- package/plugins/render3dEditor.js +81 -6
- package/plugins/render3dExtras.js +103 -31
- package/plugins/render3dLevel.js +53 -5
- package/plugins/render3dMesh.js +1158 -0
- package/plugins/render3dObject.js +1451 -0
- package/plugins/render3dVoxels.js +15 -3
- package/plugins/textureSheet.js +9 -8
- package/plugins/threejs.js +1 -1
- package/plugins/tweenSystem.js +76 -51
- package/plugins/uiSystem.js +37 -8
- package/plugins/wavedash.js +229 -0
- package/src/engine.js +184 -36
- package/src/engineAudio.js +37 -21
- package/src/engineBuild.mjs +6 -1
- package/src/engineDebug.js +22 -162
- package/src/engineDraw.js +139 -34
- package/src/engineEditor.js +79 -18
- package/src/engineExport.js +0 -3
- package/src/engineInput.js +27 -18
- package/src/engineMath.js +35 -14
- package/src/engineObject.js +60 -41
- package/src/engineParticles.js +58 -11
- package/src/engineRelease.js +1 -5
- package/src/engineSettings.js +48 -9
- package/src/engineTileLayer.js +11 -4
- package/src/engineUtilities.js +41 -9
- package/src/engineWebGL.js +37 -13
|
@@ -0,0 +1,1451 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* LittleJS 3D Object Plugin
|
|
3
|
+
* - EngineObject3D, an EngineObject with a 3D transform and a mesh, its collision and its level collision
|
|
4
|
+
* - InstancedMesh3D, one mesh drawn many times in one call, and Light3D and DirectionalLight3D
|
|
5
|
+
* - Goes after the Render3D Mesh plugin, everything here is part of the Render3D namespace
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* EngineObject3D - An EngineObject with a 3D transform and a mesh
|
|
14
|
+
* - Set pos3D, rotation3D and scale3D instead of the 2D pos, size and angle
|
|
15
|
+
* - Gets update, children, timers, destroy and renderOrder from EngineObject
|
|
16
|
+
* - velocity3D is added to pos3D each frame, slowed by damping, and render3D.gravity pulls it once it has a mass
|
|
17
|
+
* - Objects face -Z, the same way the camera does, so lookAt turns them to face a point
|
|
18
|
+
* - The 2D pos and velocity are still there but nothing draws them
|
|
19
|
+
* - These inherited fields are 2D only and do nothing here: angle, angleVelocity, additiveColor, drawSize and mirror;
|
|
20
|
+
* damping, angleDamping, clampSpeed, friction and groundObject work as in 2D, on velocity3D and angleVelocity3D
|
|
21
|
+
* - The inherited shader works here as in 2D, and with emissive at 1 its snippet does its own lighting
|
|
22
|
+
* - Set sync2D for a 2D game with 3D looks, pos and angle then drive pos3D and rotation3D,
|
|
23
|
+
* which is the one way those 2D fields reach a 3D object
|
|
24
|
+
* - setCollision takes the same flags as in 2D, but the solid collision happens in 3D against size3D
|
|
25
|
+
* - The solid box turns with rotation3D, so a turned wall or a ramp collides as it looks; resting on one no steeper
|
|
26
|
+
* than groundAngle stands there
|
|
27
|
+
* - Its tile and raycast halves are 2D only so they default off here, and a child sits solid collision out
|
|
28
|
+
* - A sync2D object collides in 2D instead, which needs the 2D size set as well as size3D
|
|
29
|
+
* - setMesh swaps the mesh and frees the old one, for text and terrain that get built again
|
|
30
|
+
* - addChild attaches the 3D transform, and pos3D becomes an offset from the parent; attach keeps the child where
|
|
31
|
+
* it is and works the offset out, and removeChild leaves it where it was in the world
|
|
32
|
+
* - The 2D offset arguments of addChild do nothing here, set the child's pos3D
|
|
33
|
+
* @extends EngineObject
|
|
34
|
+
* @memberof Render3D
|
|
35
|
+
* @example
|
|
36
|
+
* class Spinner extends EngineObject3D
|
|
37
|
+
* {
|
|
38
|
+
* constructor(pos) { super(pos, buildBox(), undefined, RED); }
|
|
39
|
+
* update() { this.rotation3D.y += .02; }
|
|
40
|
+
* }
|
|
41
|
+
*/
|
|
42
|
+
class EngineObject3D extends EngineObject
|
|
43
|
+
{
|
|
44
|
+
/** Create a 3D object and add it to the object list
|
|
45
|
+
* @param {Vector3} [pos3D] - World space position
|
|
46
|
+
* @param {Mesh} [mesh] - Mesh to draw, undefined draws nothing
|
|
47
|
+
* @param {TileInfo|TextureInfo} [tileInfo] - Texture, mesh uvs map across the tile; a whole TextureInfo becomes
|
|
48
|
+
* the tile that covers it
|
|
49
|
+
* @param {Color} [color] - Tint */
|
|
50
|
+
constructor(pos3D=vec3(), mesh, tileInfo, color=WHITE)
|
|
51
|
+
{
|
|
52
|
+
ASSERT(!tileInfo || tileInfo instanceof TileInfo || tileInfo instanceof TextureInfo,
|
|
53
|
+
'tileInfo must be a TileInfo or TextureInfo, it comes before color');
|
|
54
|
+
// a whole texture is stored as the tile that covers it, with no padding or bleed to trim
|
|
55
|
+
// the edges, so this is always a TileInfo like the 2D one and the object stays an EngineObject
|
|
56
|
+
if (tileInfo instanceof TextureInfo)
|
|
57
|
+
render3DWholeTiles.add(tileInfo = new TileInfo(vec2(), tileInfo.size, tileInfo, 0, 0));
|
|
58
|
+
// the 2D pos starts where the object is, so turning on sync2D keeps it there
|
|
59
|
+
super(vec2(pos3D.x, pos3D.y), vec2(), tileInfo, 0, color);
|
|
60
|
+
ASSERT(isVector3(pos3D), 'pos3D must be a vec3');
|
|
61
|
+
ASSERT(!mesh || mesh instanceof Mesh, 'mesh must be a Mesh or undefined');
|
|
62
|
+
this.mass = 0; // static: no 2D physics, and no 3D gravity until a mass is set
|
|
63
|
+
|
|
64
|
+
/** @property {Vector3} - World space position, local to the parent when attached to an EngineObject3D */
|
|
65
|
+
this.pos3D = pos3D.copy();
|
|
66
|
+
/** @property {Vector3} - Rotation vec3(pitch, yaw, roll) in radians, local to the parent when attached to an
|
|
67
|
+
* EngineObject3D */
|
|
68
|
+
this.rotation3D = vec3();
|
|
69
|
+
/** @property {Vector3} - Scale, local to the parent when attached to an EngineObject3D */
|
|
70
|
+
this.scale3D = vec3(1);
|
|
71
|
+
/** @property {Vector3} - Added to pos3D each frame by the engine before update, like the 2D velocity, no super
|
|
72
|
+
* call needed; damping and render3D.gravity act on it once the object has a mass */
|
|
73
|
+
this.velocity3D = vec3();
|
|
74
|
+
/** @property {Vector3} - Added to rotation3D each frame by the engine before update, slowed by angleDamping */
|
|
75
|
+
this.angleVelocity3D = vec3();
|
|
76
|
+
/** @property {Mesh|undefined} - Mesh to draw
|
|
77
|
+
* @type {Mesh|undefined} */
|
|
78
|
+
this.mesh = mesh;
|
|
79
|
+
/** @property {Vector3} - Size for solid collision and the collect and callback helpers, and of the sprite when
|
|
80
|
+
* there is a tileInfo and no mesh; starts at the size of the mesh's box, or 1 with no mesh, and setMesh leaves
|
|
81
|
+
* it as it is; the box is centered on pos3D, so center() a mesh whose origin is not its middle, like a model
|
|
82
|
+
* standing on its feet, or set size3D; scale3D and any parent's scale grow it, so drawing and picking agree */
|
|
83
|
+
// a shared mesh measured since it last changed is not walked again for each object made from it
|
|
84
|
+
const bounds = mesh && mesh.points.length ? !mesh.dirty && mesh.bounds || mesh.getBounds() : undefined;
|
|
85
|
+
this.size3D = bounds ? bounds.max.subtract(bounds.min) : vec3(1);
|
|
86
|
+
/** @property {number} - Diameter of a soft shadow drawn under the object on render3D.softShadowHeight, 0 for
|
|
87
|
+
* none; scale3D and a parent's scale grow it, so set it once for the unscaled object */
|
|
88
|
+
this.softShadow = 0;
|
|
89
|
+
/** @property {boolean} - A sprite stands on world up instead of tilting toward the camera */
|
|
90
|
+
this.upright = false;
|
|
91
|
+
/** @property {boolean} - Keep this object's texture pixels hard edged, for pixel art that should not blur or bleed */
|
|
92
|
+
this.pixelated = false;
|
|
93
|
+
/** @property {boolean} - Copy the 2D pos and angle into pos3D and rotation3D each frame, for 2D games with 3D
|
|
94
|
+
* looks; set mass to use 2D physics, and pos3D.z stays yours to set or move with velocity3D.z */
|
|
95
|
+
this.sync2D = false;
|
|
96
|
+
/** @property {boolean} - Draw in the transparent stage, blended and sorted far to near with depth writes off;
|
|
97
|
+
* on for a sprite */
|
|
98
|
+
this.transparent = !mesh && !!tileInfo;
|
|
99
|
+
/** @property {boolean} - Additive blending, in the transparent stage */
|
|
100
|
+
this.additive = false;
|
|
101
|
+
/** @property {number} - How much it lights itself: 0 is lit as normal, 1 is its own color with no shading, for
|
|
102
|
+
* lamps and glowing things, between is partly self lit, and above 1 is brighter than its color, for bloom */
|
|
103
|
+
this.emissive = 0;
|
|
104
|
+
/** @property {number} - Strength of the highlight where the sun and the Light3D objects reflect, 0 is none and
|
|
105
|
+
* 1 adds a light's full color at its brightest; shininess sets its size */
|
|
106
|
+
this.specular = 0;
|
|
107
|
+
/** @property {number} - The highlight's exponent, how small and sharp it is: 4 is broad like rubber, 16 the
|
|
108
|
+
* default, 100 sharp like polished metal; shows only with specular above 0 */
|
|
109
|
+
this.shininess = 16;
|
|
110
|
+
/** @property {TextureInfo|undefined} - A normal map that bends the surface at each texel so it catches the
|
|
111
|
+
* light like bumps and grooves, green pointing up the image as OpenGL and glTF have it; read at the color
|
|
112
|
+
* texture's coordinates, see normalMapFromHeight to make one in code
|
|
113
|
+
* @type {TextureInfo|undefined} */
|
|
114
|
+
this.normalMap = undefined;
|
|
115
|
+
/** @property {number} - How strongly the normal map bends the surface, 0 turns it off, as glTF's scale */
|
|
116
|
+
this.normalScale = 1;
|
|
117
|
+
/** @property {number} - How much it reflects the sky, 0 none and 1 a mirror of it; the edges seen at a
|
|
118
|
+
* glancing angle reflect more either way, as water and glass do */
|
|
119
|
+
this.reflectivity = 0;
|
|
120
|
+
/** @property {TextureInfo|undefined} - A texture of where it glows, added on top of the lit surface so it
|
|
121
|
+
* shows in the dark, like lit windows; read at the color texture's coordinates
|
|
122
|
+
* @type {TextureInfo|undefined} */
|
|
123
|
+
this.emissiveMap = undefined;
|
|
124
|
+
/** @property {Color} - Multiplies the emissive map, as glTF's emissiveFactor */
|
|
125
|
+
this.emissiveMapColor = WHITE;
|
|
126
|
+
/** @property {CubeMap|undefined} - What it reflects in place of render3D.environment, as a mirror captures
|
|
127
|
+
* the scene from its own middle
|
|
128
|
+
* @type {CubeMap|undefined} */
|
|
129
|
+
this.environment = undefined;
|
|
130
|
+
/** @property {boolean} - Draw into the shadow map when render3D.shadows is on; sprites and cut out textures
|
|
131
|
+
* cast their outline, an object faded below half its alpha casts nothing, a see through one casts only when
|
|
132
|
+
* textured, and additive objects never cast */
|
|
133
|
+
this.castShadow = true;
|
|
134
|
+
/** @property {boolean} - Collide as the sphere that fits size3D instead of as the size3D box, so it rolls
|
|
135
|
+
* around corners */
|
|
136
|
+
this.collideAsSphere3D = false;
|
|
137
|
+
/** @property {number} - The steepest slope it stands on, in radians from level, PI/4 by default: resting on
|
|
138
|
+
* a solid within this of flat sets groundObject and holds it still, steeper it slides down */
|
|
139
|
+
this.groundAngle = PI / 4;
|
|
140
|
+
/** @property {boolean} - Darkened by the shadow map when render3D.shadows is on */
|
|
141
|
+
this.receiveShadow = true;
|
|
142
|
+
/** @property {boolean|undefined} - Draw this object over the 2D scene, undefined uses render3D.renderAfter2D
|
|
143
|
+
* @type {boolean|undefined} */
|
|
144
|
+
this.renderAfter2D = undefined;
|
|
145
|
+
/** @property {Matrix4|undefined} - The transform from its parent, used in place of pos3D, rotation3D and
|
|
146
|
+
* scale3D when set, for one they cannot hold like a glTF pose with shear; read every frame it is set
|
|
147
|
+
* @type {Matrix4|undefined} */
|
|
148
|
+
this.localMatrix = undefined;
|
|
149
|
+
// the world transform, kept up to date by render3DObjectMatrix; getMatrix returns a copy
|
|
150
|
+
this.worldMatrix = new Matrix4;
|
|
151
|
+
this.matrixBuilt = new Float64Array(9).fill(NaN); // the position, rotation and scale it was built from
|
|
152
|
+
this.matrixVersion = 0; // counts the rebuilds, so a child knows when its parent's changed
|
|
153
|
+
/** @type {EngineObject3D|undefined} */
|
|
154
|
+
this.matrixParent = undefined; // the parent it was built under, and that parent's version then
|
|
155
|
+
this.matrixParentVersion = 0;
|
|
156
|
+
this.movePass = engineObjectsUpdateCount; // the engine pass a child last moved in, so a refresh is not a step
|
|
157
|
+
this.collideFrom = vec3(); // where it was when the pass it collided in began, see render3DCollideFrom
|
|
158
|
+
this.collideFromPass = -1;
|
|
159
|
+
/** @type {EngineObject3D|undefined} */
|
|
160
|
+
this.drawOwner = undefined; // the object this one is drawn as a part of, see render3DShareSettings
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Move by the 3D velocities and push out of solids, called automatically each frame before update, like the 2D physics
|
|
164
|
+
* - update runs once every object has moved and collided, so bounce off anything else there, it lands before the draw
|
|
165
|
+
* - Override this and call super to change how the object moves itself
|
|
166
|
+
* - A sync2D object runs the 2D physics as well, and collides there instead */
|
|
167
|
+
updatePhysics()
|
|
168
|
+
{
|
|
169
|
+
// a sync2D object collides in 2D, which measures the 2D size, and that starts at zero on a 3D object
|
|
170
|
+
ASSERT(!this.sync2D || !this.collideSolidObjects || (this.size.x && this.size.y),
|
|
171
|
+
'a sync2D object collides in 2D, so give it a 2D size as well as a size3D', this.size);
|
|
172
|
+
if (this.sync2D)
|
|
173
|
+
super.updatePhysics();
|
|
174
|
+
ASSERT(this.damping >= 0 && this.damping <= 1, 'damping must be 0 to 1, the fraction of velocity kept each frame',
|
|
175
|
+
this.damping);
|
|
176
|
+
ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1,
|
|
177
|
+
'angleDamping must be 0 to 1, the fraction of angular velocity kept each frame', this.angleDamping);
|
|
178
|
+
ASSERT(isNumber(this.groundAngle) && this.groundAngle >= 0 && this.groundAngle < PI / 2,
|
|
179
|
+
'groundAngle must be 0 to less than PI/2, a slope from level', this.groundAngle);
|
|
180
|
+
// what it stands on is found again each frame, by the level and by the solids it rests on; a sync2D
|
|
181
|
+
// object's is the 2D physics'
|
|
182
|
+
const ground = this.groundObject;
|
|
183
|
+
this.sync2D || (this.groundObject = undefined);
|
|
184
|
+
// each axis within objectMaxSpeed, as in 2D, after gravity so no move is past it; with the push back to the
|
|
185
|
+
// side it came from, a move that overlaps a solid at all is stopped on its side, so a fast object goes through
|
|
186
|
+
// only one thinner than its move less its own size, as in 2D, while neither is turned; only for what collides,
|
|
187
|
+
// anything else moves as fast as it is told
|
|
188
|
+
// nothing of collision while setEnablePhysicsSolver is off, as in 2D, the game moves them its own way
|
|
189
|
+
const solve = enablePhysicsSolver && !this.sync2D;
|
|
190
|
+
const cap = this.clampSpeed && solve && (this.collideSolidObjects || this.collideLevel && this.mass) ?
|
|
191
|
+
objectMaxSpeed : undefined; // 0 holds it still, as in 2D
|
|
192
|
+
// a moving object keeps out of the level, the height maps and voxel maps, from where it was before it moved,
|
|
193
|
+
// and a solid it hits sends it back to the side it came from
|
|
194
|
+
const oldPos = (this.collideLevel || this.collideSolidObjects) && this.mass && !this.sync2D ?
|
|
195
|
+
this.pos3D.copy() : undefined;
|
|
196
|
+
this.collideSolidObjects && !this.sync2D && render3DCollideFrom(this);
|
|
197
|
+
render3DMove(this, cap);
|
|
198
|
+
if (ground && this.mass && !this.sync2D)
|
|
199
|
+
{
|
|
200
|
+
// sliding on what it stood on slows by friction, the less grippy of the two, relative to that one's own
|
|
201
|
+
// speed so a moving platform carries it, as in 2D
|
|
202
|
+
const friction = max(this.friction, ground.friction), v = this.velocity3D;
|
|
203
|
+
const moving = ground instanceof EngineObject3D ? ground.velocity3D : undefined;
|
|
204
|
+
const gx = moving?.x ?? 0, gz = moving?.z ?? 0;
|
|
205
|
+
v.x = gx + (v.x - gx) * friction, v.z = gz + (v.z - gz) * friction;
|
|
206
|
+
}
|
|
207
|
+
oldPos && this.collideLevel && solve && render3DCollideLevel(this, oldPos, ground);
|
|
208
|
+
// the engine only runs this for objects that own where they are, a child rides along with its parent
|
|
209
|
+
if (this.collideSolidObjects && solve)
|
|
210
|
+
render3DCollideSolid(this);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Move a child by its own velocities, bring a sync2D object's pos3D up to its 2D pos, then update the children,
|
|
214
|
+
* called automatically each frame
|
|
215
|
+
* @param {boolean} [updateChildren] - Also update the children's transforms */
|
|
216
|
+
updateTransforms(updateChildren=true)
|
|
217
|
+
{
|
|
218
|
+
if (!paused)
|
|
219
|
+
{
|
|
220
|
+
// a child is never given updatePhysics, so it moves here, as an offset from its parent: once per
|
|
221
|
+
// engine pass, since addChild, attach and a game bring the transforms up to date too
|
|
222
|
+
if (this.parent && this.movePass !== engineObjectsUpdateCount)
|
|
223
|
+
{
|
|
224
|
+
this.movePass = engineObjectsUpdateCount;
|
|
225
|
+
render3DMove(this);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// placed from its parent first, so a sync2D object copies where it is now, then its children from it
|
|
230
|
+
super.updateTransforms(false);
|
|
231
|
+
if (!paused && this.sync2D)
|
|
232
|
+
this.pos3D.x = this.pos.x, this.pos3D.y = this.pos.y, this.rotation3D.z = -this.angle;
|
|
233
|
+
if (updateChildren)
|
|
234
|
+
for (const child of this.children)
|
|
235
|
+
child.updateTransforms();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Set how this object collides, the same flags as in 2D
|
|
239
|
+
* - Solid collision happens in 3D here, against size3D boxes or spheres; a child sits it out
|
|
240
|
+
* - The boxes turn with rotation3D, against height maps and voxel maps an object is still its upright box
|
|
241
|
+
* - A sync2D object collides in 2D instead, against the 2D size, so set that as well as size3D
|
|
242
|
+
* @param {boolean} [collideSolidObjects] - Take part in solid collision
|
|
243
|
+
* @param {boolean} [isSolid] - Block other objects, a pair where neither one blocks passes through;
|
|
244
|
+
* blocking needs collideSolidObjects, so isSolid on its own is not allowed
|
|
245
|
+
* @param {boolean} [collideLevel] - Collide with the level, the height maps and voxel maps, or the 2D tile layers
|
|
246
|
+
* for a sync2D object
|
|
247
|
+
* @param {boolean} [collideRaycast] - Raycasts, 2D only; 3D has render3D.pick and engineObjectsRaycast3D */
|
|
248
|
+
setCollision(collideSolidObjects=true, isSolid=true, collideLevel=true, collideRaycast=false)
|
|
249
|
+
{ super.setCollision(collideSolidObjects, isSolid, collideLevel, collideRaycast); }
|
|
250
|
+
|
|
251
|
+
/** Called by a VoxelMap to ask whether a block stops this object, a hook to let one through or react to it
|
|
252
|
+
* @param {number} type - The block's type, 1 to 255
|
|
253
|
+
* @param {Vector3} cell - The block's cell in the map
|
|
254
|
+
* @return {boolean} - true to be stopped by it, every block stops it by default */
|
|
255
|
+
collideWithVoxel(type, cell) { return true; }
|
|
256
|
+
|
|
257
|
+
/** Returns the world position
|
|
258
|
+
* @return {Vector3} */
|
|
259
|
+
getWorldPos3D() { return render3DObjectMatrix(this).getTranslation(); }
|
|
260
|
+
|
|
261
|
+
/** Returns the direction the object faces, its -Z axis in the world
|
|
262
|
+
* @return {Vector3} */
|
|
263
|
+
getForward3D() { return render3DAxis(render3DObjectMatrix(this).m, 8).normalize(-1); }
|
|
264
|
+
|
|
265
|
+
/** Returns the object's right axis in the world
|
|
266
|
+
* @return {Vector3} */
|
|
267
|
+
getRight3D() { return render3DAxis(render3DObjectMatrix(this).m, 0).normalize(); }
|
|
268
|
+
|
|
269
|
+
/** Returns the object's up axis in the world
|
|
270
|
+
* @return {Vector3} */
|
|
271
|
+
getUp3D() { return render3DAxis(render3DObjectMatrix(this).m, 4).normalize(); }
|
|
272
|
+
|
|
273
|
+
/** Returns a copy of the object's world transform, the parent's included when attached to an EngineObject3D
|
|
274
|
+
* - The object keeps its matrix and rebuilds it only when its position, rotation or scale changed, so this is
|
|
275
|
+
* cheap to call
|
|
276
|
+
* @return {Matrix4} */
|
|
277
|
+
getMatrix() { return render3DObjectMatrix(this).copy(); }
|
|
278
|
+
|
|
279
|
+
/** How rough the surface is, 0 a mirror to 1 matte, as glTF and three.js's MeshStandardMaterial have it: another
|
|
280
|
+
* way to set shininess, which is what is kept, shininess = 2 / roughness^4 - 2 and at least 1; it sets both the
|
|
281
|
+
* highlight and how blurred a reflection is; read back it is the same up to about .9, above which shininess is 1
|
|
282
|
+
* and it reads .9
|
|
283
|
+
* @return {number} */
|
|
284
|
+
get roughness() { return (2 / (this.shininess + 2)) ** .25; }
|
|
285
|
+
set roughness(roughness)
|
|
286
|
+
{
|
|
287
|
+
ASSERT(isNumber(roughness) && roughness >= 0, 'roughness must be a number, 0 or more', roughness);
|
|
288
|
+
this.shininess = max(1, 2 / max(roughness, 1e-3) ** 4 - 2); // none is a mirror, still a finite number
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Turn the object so its -Z axis points at a world space target, sets pitch and yaw and clears roll
|
|
292
|
+
* @param {Vector3} target */
|
|
293
|
+
lookAt(target)
|
|
294
|
+
{
|
|
295
|
+
// rotation3D is local to the parent, so a child has to aim at the target from the parent's point of view
|
|
296
|
+
const parent = this.parent instanceof EngineObject3D ? this.parent : undefined;
|
|
297
|
+
const local = parent ? parent.getMatrix().invert().transformPoint(target) : target;
|
|
298
|
+
this.rotation3D = render3DLookRotation(local.subtract(this.pos3D), this.rotation3D);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Attaches a child, its pos3D, rotation3D and scale3D taken as the offset from this one; returns child for chaining
|
|
302
|
+
* - The 2D offset arguments do nothing for an EngineObject3D child, set its pos3D
|
|
303
|
+
* @param {EngineObject} child
|
|
304
|
+
* @param {Vector2} [localPos]
|
|
305
|
+
* @param {number} [localAngle]
|
|
306
|
+
* @return {EngineObject} The child object attached */
|
|
307
|
+
addChild(child, localPos, localAngle)
|
|
308
|
+
{
|
|
309
|
+
// addChild brings the child's transform up to date, which is not a step: it already moved this pass as
|
|
310
|
+
// a root, or moves in the next one, so it must not move by its velocity again here
|
|
311
|
+
if (child instanceof EngineObject3D)
|
|
312
|
+
child.movePass = engineObjectsUpdateCount;
|
|
313
|
+
return super.addChild(child, localPos, localAngle);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Attaches a child without moving it: its pos3D, rotation3D and scale3D become what they have to be under this
|
|
317
|
+
* parent to keep its world transform, where addChild takes them as the offset; returns child for chaining
|
|
318
|
+
* - A parent scaled unevenly and a child turned under it make a shear, which those three values cannot hold,
|
|
319
|
+
* so the child comes out as close as they can get; a uniform scale is exact
|
|
320
|
+
* @param {EngineObject} child
|
|
321
|
+
* @return {EngineObject} The child object attached */
|
|
322
|
+
attach(child)
|
|
323
|
+
{
|
|
324
|
+
if (!(child instanceof EngineObject3D))
|
|
325
|
+
return super.attach(child); // a 2D child only has the 2D transform to keep
|
|
326
|
+
child.parent?.removeChild(child); // keeps its world values, so its own matrix is its world matrix
|
|
327
|
+
const local = render3DObjectMatrix(this).copy().invert().multiply(render3DObjectMatrix(child));
|
|
328
|
+
super.attach(child);
|
|
329
|
+
child.pos3D = local.getTranslation();
|
|
330
|
+
child.rotation3D = local.getRotation();
|
|
331
|
+
child.scale3D = local.getScale();
|
|
332
|
+
if (child.localMatrix)
|
|
333
|
+
child.localMatrix = local; // a matrix given whole stays whole, shear and all
|
|
334
|
+
return child;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Removes a child from this one, it stays where it is in the world: its pos3D, rotation3D and scale3D become
|
|
338
|
+
* its world values, with the same shear caveat as attach; a child being destroyed is let go as it is
|
|
339
|
+
* @param {EngineObject} child */
|
|
340
|
+
removeChild(child)
|
|
341
|
+
{
|
|
342
|
+
if (child instanceof EngineObject3D && !child.destroyed)
|
|
343
|
+
render3DTakeWorld(child, render3DObjectMatrix(child));
|
|
344
|
+
if (child instanceof EngineObject3D && child.drawOwner === this)
|
|
345
|
+
child.drawOwner = undefined; // a part taken off draws with its own settings
|
|
346
|
+
super.removeChild(child);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Draw a different mesh and free the GPU buffer of the one it replaces
|
|
350
|
+
* - For a mesh built again when something changes, like a score, a rebuilt terrain or a loaded model
|
|
351
|
+
* - A mesh another object is still drawing is left alone, since builders are often shared
|
|
352
|
+
* - Freeing one held somewhere else only costs it an upload, the points it was built from stay
|
|
353
|
+
* @param {Mesh} [mesh] - The mesh to draw from now on, undefined to draw nothing
|
|
354
|
+
* @return {Mesh|undefined} - The mesh passed in */
|
|
355
|
+
setMesh(mesh)
|
|
356
|
+
{
|
|
357
|
+
ASSERT(!mesh || mesh instanceof Mesh, 'mesh must be a Mesh or undefined');
|
|
358
|
+
const old = this.mesh;
|
|
359
|
+
this.mesh = mesh;
|
|
360
|
+
// nothing to free and nothing to look for when it was never uploaded
|
|
361
|
+
if (old && old !== mesh && old.buffer && !engineObjects.some(o=> /** @type {EngineObject3D} */ (o).mesh === old))
|
|
362
|
+
old.dispose();
|
|
363
|
+
return mesh;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** 2D rendering is skipped, the mesh is drawn by render3D during the 3D pass */
|
|
367
|
+
render() {}
|
|
368
|
+
|
|
369
|
+
/** Draw the object in 3D, called by the 3D pass with the draw state set from this object's flags, draws the mesh by default
|
|
370
|
+
* @return {void} */
|
|
371
|
+
render3D()
|
|
372
|
+
{
|
|
373
|
+
// an opaque draw comes out solid however low its alpha is, so a fade with no flag looks like nothing happened
|
|
374
|
+
ASSERT(this.transparent || render3DSetting(this, 'additive') || this.color.a >= 1, // a part, its owner's
|
|
375
|
+
'an object that fades needs its transparent flag, an opaque draw ignores the color alpha', this.color);
|
|
376
|
+
// the matrix the object keeps, rebuilt only when it moved, the same one for the shadow pass and the main pass
|
|
377
|
+
const matrix = render3DObjectMatrix(this);
|
|
378
|
+
if (this.mesh)
|
|
379
|
+
render3D.drawMesh(this.mesh, matrix, this.tileInfo, this.color);
|
|
380
|
+
else if (this.tileInfo)
|
|
381
|
+
{
|
|
382
|
+
// a sprite, at the world size the collect, pick and solid collision helpers measure it at
|
|
383
|
+
const m = matrix.m, size = render3DWorldSize(this, m);
|
|
384
|
+
render3D.drawBillboard(vec3(m[12], m[13], m[14]), vec2(size.x, size.y), this.tileInfo, this.color,
|
|
385
|
+
this.rotation3D.z, this.upright);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// the draw settings an object hands to the parts it draws through, each with its default: a glTF model's parts, an
|
|
391
|
+
// attached prefab's, a voxel map's see through blocks
|
|
392
|
+
const RENDER3D_PART_SETTINGS = {emissive: 0, additive: false, specular: 0, shininess: 16, reflectivity: 0,
|
|
393
|
+
normalMap: undefined, normalScale: 1, emissiveMap: undefined, emissiveMapColor: WHITE, receiveShadow: true,
|
|
394
|
+
castShadow: true, pixelated: false, shader: undefined, environment: undefined, renderAfter2D: undefined,
|
|
395
|
+
renderOrder: 0};
|
|
396
|
+
|
|
397
|
+
// make a part draw with its owner's settings: it draws with the owner's where the owner set it away from the default,
|
|
398
|
+
// and its own otherwise, so a model's unlit or rough part keeps what its file gave it; a link the renderer follows
|
|
399
|
+
// where it reads them (render3DSetting), so a setting changed later is seen, before the stage is chosen too, and the
|
|
400
|
+
// part keeps its own values as plain properties, which keeps its shape as fast as any object's
|
|
401
|
+
function render3DShareSettings(part, owner) { part.drawOwner = owner; }
|
|
402
|
+
|
|
403
|
+
// the value of a draw setting an object draws with: for a part, its owner's where that is set, through every owner
|
|
404
|
+
// up, and its own otherwise
|
|
405
|
+
function render3DSetting(o, name)
|
|
406
|
+
{
|
|
407
|
+
const owner = o.drawOwner;
|
|
408
|
+
if (owner)
|
|
409
|
+
{
|
|
410
|
+
const value = render3DSetting(owner, name);
|
|
411
|
+
if (value !== RENDER3D_PART_SETTINGS[name])
|
|
412
|
+
return value;
|
|
413
|
+
}
|
|
414
|
+
return o[name];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// a part's draw settings gathered for the draw state, in one object kept for it, so nothing is made per draw
|
|
418
|
+
const render3DPartScratch = {...RENDER3D_PART_SETTINGS};
|
|
419
|
+
function render3DPartSettings(o)
|
|
420
|
+
{
|
|
421
|
+
for (const name in RENDER3D_PART_SETTINGS)
|
|
422
|
+
render3DPartScratch[name] = render3DSetting(o, name);
|
|
423
|
+
return render3DPartScratch;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// make an object's own transform a world one, for an object leaving its parent: its position, rotation and scale
|
|
427
|
+
// from the world matrix, and a matrix given whole, shear and all
|
|
428
|
+
function render3DTakeWorld(o, world)
|
|
429
|
+
{
|
|
430
|
+
o.pos3D = world.getTranslation();
|
|
431
|
+
o.rotation3D = world.getRotation();
|
|
432
|
+
o.scale3D = world.getScale();
|
|
433
|
+
if (o.localMatrix)
|
|
434
|
+
o.localMatrix = world.copy();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// an object's world matrix, the one it keeps: rebuilt only when its position, rotation or scale changed since the
|
|
438
|
+
// last build, or its parent's matrix did, so an object that stands still costs nine compares a frame instead of
|
|
439
|
+
// the trig and a new matrix; the matrix returned is the object's own, read it and never change it
|
|
440
|
+
const render3DLocalMatrix = new Matrix4;
|
|
441
|
+
function render3DObjectMatrix(o)
|
|
442
|
+
{
|
|
443
|
+
const parent = o.parent instanceof EngineObject3D ? o.parent : undefined;
|
|
444
|
+
const parentMatrix = parent && render3DObjectMatrix(parent); // the parent first, so its version is current
|
|
445
|
+
const p = o.pos3D, r = o.rotation3D, s = o.scale3D, k = o.matrixBuilt, local = o.localMatrix;
|
|
446
|
+
if (local)
|
|
447
|
+
{
|
|
448
|
+
// a matrix given whole is taken as it is each time, it can change in place
|
|
449
|
+
if (parent)
|
|
450
|
+
{
|
|
451
|
+
o.worldMatrix.m.set(parentMatrix.m);
|
|
452
|
+
o.worldMatrix.multiply(local);
|
|
453
|
+
o.matrixParentVersion = parent.matrixVersion;
|
|
454
|
+
}
|
|
455
|
+
else
|
|
456
|
+
o.worldMatrix.m.set(local.m);
|
|
457
|
+
k[0] = NaN; // built from the matrix, so going back to pos3D builds again
|
|
458
|
+
o.matrixParent = parent;
|
|
459
|
+
++o.matrixVersion;
|
|
460
|
+
}
|
|
461
|
+
else if (k[0] !== p.x || k[1] !== p.y || k[2] !== p.z || k[3] !== r.x || k[4] !== r.y || k[5] !== r.z
|
|
462
|
+
|| k[6] !== s.x || k[7] !== s.y || k[8] !== s.z || o.matrixParent !== parent
|
|
463
|
+
|| parent && o.matrixParentVersion !== parent.matrixVersion)
|
|
464
|
+
{
|
|
465
|
+
k[0] = p.x, k[1] = p.y, k[2] = p.z, k[3] = r.x, k[4] = r.y, k[5] = r.z, k[6] = s.x, k[7] = s.y, k[8] = s.z;
|
|
466
|
+
if (parent)
|
|
467
|
+
{
|
|
468
|
+
// the parent's world matrix times the local one
|
|
469
|
+
buildMatrix(p, r, s, render3DLocalMatrix);
|
|
470
|
+
o.worldMatrix.m.set(parentMatrix.m);
|
|
471
|
+
o.worldMatrix.multiply(render3DLocalMatrix);
|
|
472
|
+
o.matrixParentVersion = parent.matrixVersion;
|
|
473
|
+
}
|
|
474
|
+
else
|
|
475
|
+
buildMatrix(p, r, s, o.worldMatrix);
|
|
476
|
+
o.matrixParent = parent;
|
|
477
|
+
++o.matrixVersion;
|
|
478
|
+
}
|
|
479
|
+
return o.worldMatrix;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// move an object by its 3D velocities, each slowed by its damping, an object with mass falling with render3D.gravity
|
|
483
|
+
function render3DMove(o, cap)
|
|
484
|
+
{
|
|
485
|
+
// the vectors change in place, as the 2D object's do: this runs for every object every frame
|
|
486
|
+
// a sync2D object's damping is the 2D physics', its 3D velocities are its own to set
|
|
487
|
+
const p = o.pos3D, v = o.velocity3D, r = o.rotation3D, a = o.angleVelocity3D;
|
|
488
|
+
const d = o.sync2D ? 1 : o.damping, e = o.sync2D ? 1 : o.angleDamping;
|
|
489
|
+
// damped first and gravity added after, the order EngineObject.updatePhysics uses,
|
|
490
|
+
// so the same mass, damping and gravity fall the same way in both
|
|
491
|
+
v.x *= d, v.y *= d, v.z *= d;
|
|
492
|
+
if (o.mass && !o.sync2D) // a 2D driven object gets the 2D gravity instead
|
|
493
|
+
{
|
|
494
|
+
const g = render3D.gravity, s = o.gravityScale;
|
|
495
|
+
v.x += g.x * s, v.y += g.y * s, v.z += g.z * s;
|
|
496
|
+
}
|
|
497
|
+
if (cap !== undefined) // the speed cap of what collides, after gravity, as in 2D
|
|
498
|
+
v.x = clamp(v.x, -cap, cap), v.y = clamp(v.y, -cap, cap), v.z = clamp(v.z, -cap, cap);
|
|
499
|
+
p.x += v.x, p.y += v.y, p.z += v.z;
|
|
500
|
+
r.x += a.x *= e, r.y += a.y *= e, r.z += a.z *= e;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// keep an object that moved out of the level's solid geometry, clearing what it stood on for the level to set again
|
|
504
|
+
// ground is what it stood on last frame, which a height map keeps it on going down a slope
|
|
505
|
+
function render3DCollideLevel(o, oldPos, ground)
|
|
506
|
+
{
|
|
507
|
+
for (const level of render3DLevel)
|
|
508
|
+
level.destroyed || level.levelCollide3D(o, oldPos, level === ground);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// where a solid object is in the world and what it collides as: the sphere that fits size3D, or the size3D box,
|
|
512
|
+
// each grown by the object's scale
|
|
513
|
+
// only objects that own where they are take part, so pos3D is already world space, and however the object is
|
|
514
|
+
// turned its axes come out as long as its scale makes them; building the transform to read that back off it
|
|
515
|
+
// costs six trig calls and a matrix for every pair tested, which is the whole cost of a crowded scene
|
|
516
|
+
function render3DSolidShape(o)
|
|
517
|
+
{
|
|
518
|
+
ASSERT(!o.parent, 'a child rides along with its parent, it has no world pos3D of its own to collide with');
|
|
519
|
+
const s = o.size3D, k = o.scale3D;
|
|
520
|
+
const kx = abs(k.x), ky = abs(k.y), kz = abs(k.z);
|
|
521
|
+
if (o.collideAsSphere3D)
|
|
522
|
+
return {pos: o.pos3D.copy(), radius: max(s.x, s.y, s.z) / 2 * max(kx, ky, kz)};
|
|
523
|
+
// a turned box has its axes, a solid is never a child so its rotation is the world's; an upright one has none, and
|
|
524
|
+
// neither does a sprite, whose rotation turns how it faces the camera, not its box
|
|
525
|
+
const axes = !render3DIsSprite(o) && isTurned3D(o.rotation3D) ? render3DSolidAxes(o) : undefined;
|
|
526
|
+
return {pos: o.pos3D.copy(), size: vec3(s.x * kx, s.y * ky, s.z * kz), axes};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// a turned solid's three axes, worked out when its rotation changes and kept until then, so the pairs it is tested
|
|
530
|
+
// against each frame do not each work them out again
|
|
531
|
+
const render3DSolidAxesCache = new WeakMap;
|
|
532
|
+
function render3DSolidAxes(o)
|
|
533
|
+
{
|
|
534
|
+
const r = o.rotation3D, kept = render3DSolidAxesCache.get(o);
|
|
535
|
+
if (kept && kept.x === r.x && kept.y === r.y && kept.z === r.z)
|
|
536
|
+
return kept.axes;
|
|
537
|
+
const axes = boxAxes3D(r);
|
|
538
|
+
render3DSolidAxesCache.set(o, {x: r.x, y: r.y, z: r.z, axes});
|
|
539
|
+
return axes;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// how far a solid shape can reach from its own center, for a quick reject before the exact test
|
|
543
|
+
// it has to be the shape's own radius, or a wider one: a box reaches to its corner, and a sphere
|
|
544
|
+
// takes the largest scale the same way render3DSolidShape does, or the reject would skip real touches
|
|
545
|
+
function render3DSolidReach(o)
|
|
546
|
+
{
|
|
547
|
+
const s = o.size3D, k = o.scale3D;
|
|
548
|
+
const kx = abs(k.x), ky = abs(k.y), kz = abs(k.z);
|
|
549
|
+
if (o.collideAsSphere3D)
|
|
550
|
+
return max(s.x, s.y, s.z) / 2 * max(kx, ky, kz);
|
|
551
|
+
const x = s.x * kx, y = s.y * ky, z = s.z * kz;
|
|
552
|
+
return (x*x + y*y + z*z) ** .5 / 2; // written out, it is asked for every pair each frame
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// what it takes to move shape a clear of shape b, whichever pair of shapes they are, or undefined for no touch
|
|
556
|
+
function render3DSolidPush(a, b)
|
|
557
|
+
{
|
|
558
|
+
// a turned box is tested by its kept axes, the upright pairs as they always were
|
|
559
|
+
const sphereBox = (sphere, box)=> box.axes ? collideSphereOrientedBox3D(sphere.pos, sphere.radius, box.pos,
|
|
560
|
+
box.size, box.axes) : collideSphereBox(sphere.pos, sphere.radius, box.pos, box.size);
|
|
561
|
+
if (!a.size) // a is a sphere
|
|
562
|
+
return b.size ? sphereBox(a, b) : collideSphereSphere(a.pos, a.radius, b.pos, b.radius);
|
|
563
|
+
if (!b.size) // only b is, so push b out of a and turn it around
|
|
564
|
+
{
|
|
565
|
+
const push = sphereBox(b, a);
|
|
566
|
+
return push && push.scale(-1);
|
|
567
|
+
}
|
|
568
|
+
return a.axes || b.axes ? collideOrientedBoxes3D(a.pos, a.size, a.axes ?? BOX_WORLD_AXES, b.pos, b.size,
|
|
569
|
+
b.axes ?? BOX_WORLD_AXES) : collideBoxBox3D(a.pos, a.size, b.pos, b.size);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// whether a touching pair resolves: both hear about it, and the answer is kept for the frame, so a pair met again as
|
|
573
|
+
// one object is settled after being pushed is not asked twice; an object's own turn asks again, as it always has,
|
|
574
|
+
// unless a settle asked that pair already this pass (render3DSettleAsked), so a wall hears about it once a frame
|
|
575
|
+
function render3DCollideAsk(a, b, push, useKept=true)
|
|
576
|
+
{
|
|
577
|
+
const kept = useKept || render3DSettleAsked(a, b) ? engineObjectsCollidePairAnswer(a, b) : undefined;
|
|
578
|
+
if (kept !== undefined) return kept;
|
|
579
|
+
const resolveA = a.collideWithObject(b, push), resolveB = b.collideWithObject(a, push.scale(-1));
|
|
580
|
+
const resolve = !!(resolveA && resolveB);
|
|
581
|
+
engineObjectsCollidePairAdd(a, b, resolve);
|
|
582
|
+
engineObjectsCollidePairAdd(b, a, resolve);
|
|
583
|
+
useKept && render3DSettleAsked(a, b, true);
|
|
584
|
+
return resolve;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// the pairs a settle asked this pass, each object to the others; asked with mark set, it marks the pair
|
|
588
|
+
const render3DSettleAskedPairs = {pass: -1, pairs: new Map};
|
|
589
|
+
function render3DSettleAsked(a, b, mark=false)
|
|
590
|
+
{
|
|
591
|
+
const kept = render3DSettleAskedPairs;
|
|
592
|
+
if (kept.pass !== engineObjectsUpdateCount)
|
|
593
|
+
{
|
|
594
|
+
if (!mark) return false;
|
|
595
|
+
kept.pass = engineObjectsUpdateCount;
|
|
596
|
+
kept.pairs.clear();
|
|
597
|
+
}
|
|
598
|
+
if (mark)
|
|
599
|
+
{
|
|
600
|
+
for (const [one, other] of [[a, b], [b, a]])
|
|
601
|
+
{
|
|
602
|
+
const set = kept.pairs.get(one);
|
|
603
|
+
set ? set.add(other) : kept.pairs.set(one, new Set([other]));
|
|
604
|
+
}
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
return !!kept.pairs.get(a)?.has(b);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// where a solid was when this pass of the engine began, taken at the start of its own updatePhysics, or the first time
|
|
611
|
+
// a pair needs it if that comes first, as for one pushed or a mass 0 one not updated yet, so a
|
|
612
|
+
// pair resolved in the turn of either one knows where both came from
|
|
613
|
+
function render3DCollideFrom(o)
|
|
614
|
+
{
|
|
615
|
+
if (o.collideFromPass !== engineObjectsUpdateCount)
|
|
616
|
+
{
|
|
617
|
+
o.collideFromPass = engineObjectsUpdateCount;
|
|
618
|
+
o.collideFrom.set(o.pos3D.x, o.pos3D.y, o.pos3D.z);
|
|
619
|
+
}
|
|
620
|
+
return o.collideFrom;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// a push that would carry a on the way it is going relative to b, out the far side past b's middle, as a fast object
|
|
624
|
+
// goes through a thin wall or two movers through each other, is turned back to the side a came from, as 2D resolves
|
|
625
|
+
// from where an object was; both go by where they were when the pass began, so it is the same in either one's turn
|
|
626
|
+
function render3DPushFrom(shapeA, shapeB, a, b, push)
|
|
627
|
+
{
|
|
628
|
+
const aFrom = render3DCollideFrom(a), bFrom = render3DCollideFrom(b), pa = a.pos3D, pb = b.pos3D;
|
|
629
|
+
const mx = pa.x - aFrom.x - pb.x + bFrom.x, my = pa.y - aFrom.y - pb.y + bFrom.y, mz = pa.z - aFrom.z - pb.z + bFrom.z;
|
|
630
|
+
if (push.x * mx + push.y * my + push.z * mz <= 0)
|
|
631
|
+
return push;
|
|
632
|
+
return render3DSolidPushBack(shapeA, shapeB, aFrom, bFrom) ?? push;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// the solids that do not move, mass 0, in the order of the engine's list, gathered once a pass for the settles, which
|
|
636
|
+
// would otherwise walk every collider for each push between two movers
|
|
637
|
+
const render3DFixed = {pass: -1, list: []};
|
|
638
|
+
function render3DFixedSolids()
|
|
639
|
+
{
|
|
640
|
+
if (render3DFixed.pass !== engineObjectsUpdateCount)
|
|
641
|
+
{
|
|
642
|
+
render3DFixed.pass = engineObjectsUpdateCount;
|
|
643
|
+
render3DFixed.list = engineObjectsCollide.filter((b)=> !b.mass && b instanceof EngineObject3D &&
|
|
644
|
+
!b.parent && !b.sync2D);
|
|
645
|
+
}
|
|
646
|
+
return render3DFixed.list;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// push an object pushed by another out of the solids that do not move, mass 0, back to the side it came from;
|
|
650
|
+
// they take the touch as any pair does, so a one way platform still lets it through; not the one that pushed it, which
|
|
651
|
+
// already did, and a push of a rounding error is none, or a box on a turned ramp would be pushed along its slant;
|
|
652
|
+
// from is where it was before the push
|
|
653
|
+
function render3DSettleFixed(o, pusher, from)
|
|
654
|
+
{
|
|
655
|
+
if (o.destroyed || !o.collideSolidObjects) return;
|
|
656
|
+
let shape = render3DSolidShape(o);
|
|
657
|
+
const reachO = render3DSolidReach(o);
|
|
658
|
+
for (const b of render3DFixedSolids())
|
|
659
|
+
{
|
|
660
|
+
// a pair where neither is solid passes through, as in the pair loop; a solid against one that is not still meets
|
|
661
|
+
if (b === o || b === pusher || b.mass || b.destroyed || !o.isSolid && !b.isSolid)
|
|
662
|
+
continue;
|
|
663
|
+
const p = shape.pos, q = b.pos3D, reach = reachO + render3DSolidReach(b);
|
|
664
|
+
const dx = p.x - q.x, dy = p.y - q.y, dz = p.z - q.z;
|
|
665
|
+
if (dx*dx + dy*dy + dz*dz > reach*reach) continue;
|
|
666
|
+
const shapeB = render3DSolidShape(b);
|
|
667
|
+
let push = render3DSolidPush(shape, shapeB);
|
|
668
|
+
if (!push || push.lengthSquared() < 1e-12) continue;
|
|
669
|
+
push = render3DPushFrom(shape, shapeB, o, b, push);
|
|
670
|
+
if (!render3DCollideAsk(o, b, push)) continue;
|
|
671
|
+
// standing as in a pair: a push off a face within its groundAngle of up holds it there, turned straight up
|
|
672
|
+
const lengthSquared = push.lengthSquared(), up = push.y / lengthSquared ** .5;
|
|
673
|
+
if (up > 0 && o.mass && up >= cos(o.groundAngle) && render3DOnFace(push, shapeB))
|
|
674
|
+
{
|
|
675
|
+
o.groundObject = b;
|
|
676
|
+
if (push.x || push.z)
|
|
677
|
+
push = vec3(0, lengthSquared / push.y, 0);
|
|
678
|
+
}
|
|
679
|
+
o.pos3D = o.pos3D.add(push);
|
|
680
|
+
shape = render3DSolidShape(o);
|
|
681
|
+
const normal = push.normalize();
|
|
682
|
+
if (o.velocity3D.dot(normal) < 0)
|
|
683
|
+
o.velocity3D = o.velocity3D.reflect(normal, o.restitution);
|
|
684
|
+
}
|
|
685
|
+
// and out of the level, the voxel and height maps, from where it was before the push, so a move of the game's own,
|
|
686
|
+
// as a collision callback that sends it away, is not swept back across the level
|
|
687
|
+
if (o.collideLevel && o.mass)
|
|
688
|
+
render3DCollideLevel(o, from, undefined);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// what moves shape a back clear of shape b to the side it came from: along an axis the two were clear on where they
|
|
692
|
+
// came from, the least of those, as 2D resolves it from where the object was; undefined when they overlapped on every
|
|
693
|
+
// axis there too, or a box is turned, which keeps the least push
|
|
694
|
+
function render3DSolidPushBack(a, b, aFrom, bFrom)
|
|
695
|
+
{
|
|
696
|
+
if (a.axes || b.axes) return;
|
|
697
|
+
const half = (shape)=> shape.size ? shape.size.scale(.5) : vec3(shape.radius);
|
|
698
|
+
const halfA = half(a), halfB = half(b);
|
|
699
|
+
let axis, amount = Infinity, side = 0;
|
|
700
|
+
for (const k of ['x', 'y', 'z'])
|
|
701
|
+
{
|
|
702
|
+
const reach = halfA[k] + halfB[k], was = aFrom[k] - bFrom[k];
|
|
703
|
+
// it overlapped on this axis before it moved as well; one resting against it touches, which is clear, give or
|
|
704
|
+
// take the rounding of the push that put it there
|
|
705
|
+
if (abs(was) < reach - 1e-6) continue;
|
|
706
|
+
const s = sign(was), need = reach - s * (a.pos[k] - b.pos[k]);
|
|
707
|
+
if (need > 0 && need < amount)
|
|
708
|
+
axis = k, amount = need, side = s;
|
|
709
|
+
}
|
|
710
|
+
if (!axis) return;
|
|
711
|
+
const push = vec3();
|
|
712
|
+
push[axis] = side * amount;
|
|
713
|
+
return push;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// a sprite, a tile with no mesh, which faces the camera however it is turned
|
|
717
|
+
const render3DIsSprite = (o)=> !o.mesh && !!o.tileInfo;
|
|
718
|
+
|
|
719
|
+
// whether a push leaves a box by one of its faces, along one of its axes, not by an edge or a corner; a sphere has
|
|
720
|
+
// no face to stand on
|
|
721
|
+
function render3DOnFace(push, shape)
|
|
722
|
+
{
|
|
723
|
+
if (!shape.size) return false;
|
|
724
|
+
const n = push.normalize(), axes = shape.axes ?? BOX_WORLD_AXES;
|
|
725
|
+
return max(abs(n.dot(axes[0])), abs(n.dot(axes[1])), abs(n.dot(axes[2]))) > 1 - 1e-6;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// push a solid object out of the solids before it in the engine's list of them, so each pair is resolved once:
|
|
729
|
+
// the ones after it update later and test against it then, and an object that is not in the list yet, because it
|
|
730
|
+
// turned collision on this frame, tests them all itself and is not tested back
|
|
731
|
+
// one pair per test is half the work of the 2D solver, which tests both directions; the difference only shows
|
|
732
|
+
// when a collideWithObject destroys some third object, whose own turn then finds the pair already gone
|
|
733
|
+
function render3DCollideSolid(a)
|
|
734
|
+
{
|
|
735
|
+
let shapeA = render3DSolidShape(a);
|
|
736
|
+
const reachA = render3DSolidReach(a);
|
|
737
|
+
for (const b of engineObjectsCollide)
|
|
738
|
+
{
|
|
739
|
+
if (b === a) break;
|
|
740
|
+
// a child is part of its parent
|
|
741
|
+
if (b.destroyed || !(b instanceof EngineObject3D) || b.parent || b.sync2D) continue;
|
|
742
|
+
if (!a.isSolid && !b.isSolid) continue; // neither one blocks, so they pass through each other
|
|
743
|
+
|
|
744
|
+
// the pairs nowhere near each other are almost all of them in a scene of any size, so
|
|
745
|
+
// settle those with one distance check instead of building a shape for each
|
|
746
|
+
const p = shapeA.pos, q = b.pos3D, reach = reachA + render3DSolidReach(b);
|
|
747
|
+
const dx = p.x - q.x, dy = p.y - q.y, dz = p.z - q.z;
|
|
748
|
+
if (dx*dx + dy*dy + dz*dz > reach*reach)
|
|
749
|
+
continue;
|
|
750
|
+
|
|
751
|
+
const shapeB = render3DSolidShape(b);
|
|
752
|
+
let push = render3DSolidPush(shapeA, shapeB);
|
|
753
|
+
if (!push) continue;
|
|
754
|
+
push = render3DPushFrom(shapeA, shapeB, a, b, push);
|
|
755
|
+
|
|
756
|
+
// both objects hear about it, and either one can take the touch over
|
|
757
|
+
if (!render3DCollideAsk(a, b, push, false)) continue;
|
|
758
|
+
|
|
759
|
+
// standing: resting on a box's face within the upper one's groundAngle of level holds it there, the push
|
|
760
|
+
// turned straight up, as far as it takes to leave the surface, so it does not creep down a ramp; only what
|
|
761
|
+
// moves stands, and a sphere, an edge or a corner is nothing to stand on, what rests there rolls off
|
|
762
|
+
const lengthSquared = push.lengthSquared(), up = push.y / lengthSquared ** .5;
|
|
763
|
+
const aStands = up > 0 && a.mass && up >= cos(a.groundAngle) && render3DOnFace(push, shapeB);
|
|
764
|
+
const bStands = up < 0 && b.mass && -up >= cos(b.groundAngle) && render3DOnFace(push, shapeA);
|
|
765
|
+
if (aStands || bStands)
|
|
766
|
+
{
|
|
767
|
+
aStands ? a.groundObject = b : b.groundObject = a;
|
|
768
|
+
if (push.x || push.z) // one straight up already is used as it is
|
|
769
|
+
push = vec3(0, lengthSquared / push.y, 0);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// heavier objects move less, mass 0 stays put; then bounce apart when moving toward each other
|
|
773
|
+
const total = a.mass + b.mass;
|
|
774
|
+
const weightA = !a.mass ? 0 : !b.mass ? 1 : b.mass / total;
|
|
775
|
+
const weightB = !b.mass ? 0 : !a.mass ? 1 : a.mass / total;
|
|
776
|
+
const fromA = a.pos3D.copy(), fromB = b.pos3D.copy(); // where the push moves them from, for the settles
|
|
777
|
+
a.pos3D = a.pos3D.add(push.scale(weightA));
|
|
778
|
+
b.pos3D = b.pos3D.subtract(push.scale(weightB));
|
|
779
|
+
// mass 0 keeps its velocity too, so a moving platform keeps moving, and what hits it bounces by its own
|
|
780
|
+
// restitution as it would off a static wall
|
|
781
|
+
const normal = push.normalize();
|
|
782
|
+
if (weightA && a.velocity3D.dot(normal) < 0)
|
|
783
|
+
a.velocity3D = a.velocity3D.reflect(normal, a.restitution);
|
|
784
|
+
if (weightB && b.velocity3D.dot(normal) > 0)
|
|
785
|
+
b.velocity3D = b.velocity3D.reflect(normal, b.restitution);
|
|
786
|
+
|
|
787
|
+
// the settles come after the bounce, so a wall's push is the last word on the velocity
|
|
788
|
+
if (weightA && weightB)
|
|
789
|
+
{
|
|
790
|
+
// a met the solids before it in the list already, the fixed ones among them, so a push from something
|
|
791
|
+
// that moves is settled against those now, and b takes what a could not move, as if a were fixed
|
|
792
|
+
const pushed = a.pos3D.copy();
|
|
793
|
+
render3DSettleFixed(a, b, fromA);
|
|
794
|
+
b.pos3D = b.pos3D.add(a.pos3D.subtract(pushed));
|
|
795
|
+
}
|
|
796
|
+
if (weightB)
|
|
797
|
+
{
|
|
798
|
+
// b had its turn already, so a push into a wall is settled against the fixed solids now, or it would end
|
|
799
|
+
// the frame in the wall, and a hard enough shove would carry it through; then a takes what b could not
|
|
800
|
+
// move, as a pusher stops against a crate held by a wall, and is settled itself, squeezed at worst
|
|
801
|
+
const pushed = b.pos3D.copy();
|
|
802
|
+
render3DSettleFixed(b, a, fromB);
|
|
803
|
+
const backX = b.pos3D.x - pushed.x, backY = b.pos3D.y - pushed.y, backZ = b.pos3D.z - pushed.z;
|
|
804
|
+
if (weightA && (backX || backY || backZ))
|
|
805
|
+
{
|
|
806
|
+
a.pos3D = a.pos3D.add(vec3(backX, backY, backZ));
|
|
807
|
+
render3DSettleFixed(a, b, fromA);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
if (weightA)
|
|
811
|
+
shapeA = render3DSolidShape(a); // it moved, so the next solid must be tested against where it is now
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Collect the EngineObject3D objects whose boxes overlap a sphere or a box, the 3D twin of engineObjectsCollect
|
|
817
|
+
* - Each object is its size3D box at its world position, turned as it is turned; lights, emitters and trails have no
|
|
818
|
+
* size and are never collected
|
|
819
|
+
* @param {Vector3} pos - Center of the area
|
|
820
|
+
* @param {Vector3|number} size - Diameter of a sphere if a number, 0 for a point, full size of a box if a Vector3
|
|
821
|
+
* @param {Array<EngineObject>} [objects] - Defaults to every object
|
|
822
|
+
* @param {boolean} [testCenters] - Test only each object's center, a little faster, and ignores object sizes
|
|
823
|
+
* @return {Array<EngineObject3D>}
|
|
824
|
+
* @memberof Render3D
|
|
825
|
+
*/
|
|
826
|
+
function engineObjectsCollect3D(pos, size, objects=engineObjects, testCenters=false)
|
|
827
|
+
{
|
|
828
|
+
// a size of 0 is a point, tested against each box, since a sphere of no size could never hit
|
|
829
|
+
const radiusSquared = typeof size === 'number' && size > 0 ? (size/2)**2 : undefined;
|
|
830
|
+
const box = radiusSquared ? undefined : typeof size === 'number' ? vec3() : render3DSize3(size);
|
|
831
|
+
const collected = [];
|
|
832
|
+
for (const o of objects)
|
|
833
|
+
{
|
|
834
|
+
if (!(o instanceof EngineObject3D) || o.destroyed) continue;
|
|
835
|
+
// the box in world space, scaled by the object and its parents
|
|
836
|
+
const m = render3DObjectMatrix(o).m, s = o.size3D;
|
|
837
|
+
if (!(s.x || s.y || s.z)) continue;
|
|
838
|
+
const center = vec3(m[12], m[13], m[14]);
|
|
839
|
+
const worldSize = testCenters ? vec3() : render3DWorldSize(o, m);
|
|
840
|
+
// a turned box is tested as turned, its axes read off the matrix, its columns with the scale taken out
|
|
841
|
+
const turned = !testCenters && !render3DIsSprite(o) && (m[1] || m[2] || m[4] || m[6] || m[8] || m[9]);
|
|
842
|
+
const axis = (i)=> vec3(m[i], m[i+1], m[i+2]).normalize();
|
|
843
|
+
let axes = turned ? [axis(0), axis(4), axis(8)] : undefined;
|
|
844
|
+
if (axes && (abs(axes[0].dot(axes[1])) > 1e-6 || abs(axes[0].dot(axes[2])) > 1e-6 ||
|
|
845
|
+
abs(axes[1].dot(axes[2])) > 1e-6))
|
|
846
|
+
{
|
|
847
|
+
// a turned child of an unevenly scaled parent is sheared, its edges no longer square, which the box
|
|
848
|
+
// tests do not take; the upright box around it stands in, which may take a little more but never misses
|
|
849
|
+
const e = axes.map((a, i)=> a.scale(i ? i > 1 ? worldSize.z / 2 : worldSize.y / 2 : worldSize.x / 2));
|
|
850
|
+
const reach = (c)=> 2 * (abs(e[0][c]) + abs(e[1][c]) + abs(e[2][c]));
|
|
851
|
+
worldSize.set(reach('x'), reach('y'), reach('z'));
|
|
852
|
+
axes = undefined;
|
|
853
|
+
}
|
|
854
|
+
let hit;
|
|
855
|
+
if (box && axes)
|
|
856
|
+
hit = !!collideOrientedBoxes3D(pos, box, BOX_WORLD_AXES, center, worldSize, axes);
|
|
857
|
+
else if (box)
|
|
858
|
+
hit = isOverlapping3D(pos, box, center, worldSize);
|
|
859
|
+
else if (axes)
|
|
860
|
+
hit = !!collideSphereOrientedBox3D(pos, radiusSquared ** .5, center, worldSize, axes);
|
|
861
|
+
else
|
|
862
|
+
{
|
|
863
|
+
// a sphere against the nearest point of the box
|
|
864
|
+
const dx = max(abs(pos.x - center.x) - worldSize.x/2, 0);
|
|
865
|
+
const dy = max(abs(pos.y - center.y) - worldSize.y/2, 0);
|
|
866
|
+
const dz = max(abs(pos.z - center.z) - worldSize.z/2, 0);
|
|
867
|
+
hit = dx*dx + dy*dy + dz*dz < radiusSquared;
|
|
868
|
+
}
|
|
869
|
+
hit && collected.push(o);
|
|
870
|
+
}
|
|
871
|
+
return collected;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// 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,
|
|
875
|
+
// and a sprite as the quad it draws
|
|
876
|
+
const render3DRaycastCenter = vec3();
|
|
877
|
+
function render3DRaycastObject(ray, o)
|
|
878
|
+
{
|
|
879
|
+
if (o.destroyed || !(o instanceof EngineObject3D)) return;
|
|
880
|
+
// a height map or voxel map is hit on its surface, not its box
|
|
881
|
+
if (o instanceof HeightMap || o instanceof VoxelMap) return o.levelRaycast3D(ray);
|
|
882
|
+
if (!(o.mesh || o.tileInfo)) return;
|
|
883
|
+
if (o instanceof InstancedMesh3D) return; // its instances are not objects, and its one sphere is not a thing to hit
|
|
884
|
+
const matrix = render3DObjectMatrix(o), mesh = o.mesh;
|
|
885
|
+
if (!mesh) return render3DRaycastSprite(ray, o, matrix);
|
|
886
|
+
// a mesh that changed since it was measured is measured again, an upload may not have come yet
|
|
887
|
+
const radius = (mesh.dirty || !mesh.radius ? mesh.computeRadius() : mesh.radius) * render3DMaxStretch(matrix.m);
|
|
888
|
+
if (!(radius > 0)) return; // nothing to hit
|
|
889
|
+
// its place read from the matrix into a vector kept for this, a pick tests every object and most miss
|
|
890
|
+
const m = matrix.m, center = render3DRaycastCenter.set(m[12], m[13], m[14]);
|
|
891
|
+
const distance = raycastSphere(ray, center, radius);
|
|
892
|
+
if (distance === undefined) return;
|
|
893
|
+
|
|
894
|
+
// the sphere and then the box in the mesh's own space are quick rejects, and a ray that meets the box is tested
|
|
895
|
+
// against the triangles; the direction is not made unit length, so the distance holds in the world;
|
|
896
|
+
// a mesh flattened to nothing on an axis has no inverse, it is hit as a disc like a sprite
|
|
897
|
+
if (!matrix.determinant()) return render3DRaycastDisc(ray, center, radius);
|
|
898
|
+
const inverse = matrix.copy().invert(), bounds = mesh.bounds || mesh.getBounds();
|
|
899
|
+
const local = new Ray3D(inverse.transformPoint(ray.origin), inverse.transformDirection(ray.direction));
|
|
900
|
+
const hit = raycastBox(local, bounds.min.add(bounds.max).scale(.5), bounds.max.subtract(bounds.min));
|
|
901
|
+
if (hit === undefined || !ray.direction.lengthSquared())
|
|
902
|
+
return hit; // a ray of no length is where it starts, inside the box
|
|
903
|
+
return render3DRaycastMesh(local, mesh);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// how far along a ray, in the mesh's own space, its nearest triangle is hit, or undefined: the triangles that face
|
|
907
|
+
// the ray as the pass draws them, clockwise from the front, and the back faces too of a doubleSided mesh; a strip's
|
|
908
|
+
// 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
|
|
909
|
+
// never hit; nothing is kept between calls, so a mesh that changed is hit as it is now
|
|
910
|
+
function render3DRaycastMesh(ray, mesh)
|
|
911
|
+
{
|
|
912
|
+
const points = mesh.points, indices = mesh.indices, both = mesh.doubleSided;
|
|
913
|
+
const ox = ray.origin.x, oy = ray.origin.y, oz = ray.origin.z;
|
|
914
|
+
const dx = ray.direction.x, dy = ray.direction.y, dz = ray.direction.z;
|
|
915
|
+
const count = indices ? indices.length / 3 | 0 : points.length - 2;
|
|
916
|
+
let nearest;
|
|
917
|
+
for (let i = 0; i < count; ++i)
|
|
918
|
+
{
|
|
919
|
+
// the corners clockwise from the front: an indexed list is written the other way round
|
|
920
|
+
let a, b, c;
|
|
921
|
+
if (indices)
|
|
922
|
+
a = points[indices[i*3]], b = points[indices[i*3+2]], c = points[indices[i*3+1]];
|
|
923
|
+
else
|
|
924
|
+
a = points[i & 1 ? i + 1 : i], b = points[i & 1 ? i : i + 1], c = points[i + 2];
|
|
925
|
+
|
|
926
|
+
// Moller and Trumbore: the determinant is the ray along the face's normal, below zero from the front
|
|
927
|
+
const e1x = b.x - a.x, e1y = b.y - a.y, e1z = b.z - a.z;
|
|
928
|
+
const e2x = c.x - a.x, e2y = c.y - a.y, e2z = c.z - a.z;
|
|
929
|
+
const hx = dy * e2z - dz * e2y, hy = dz * e2x - dx * e2z, hz = dx * e2y - dy * e2x;
|
|
930
|
+
const det = e1x * hx + e1y * hy + e1z * hz;
|
|
931
|
+
if (both ? !det : !(det < 0)) continue;
|
|
932
|
+
const sx = ox - a.x, sy = oy - a.y, sz = oz - a.z;
|
|
933
|
+
const u = (sx * hx + sy * hy + sz * hz) / det;
|
|
934
|
+
if (!(u >= 0 && u <= 1)) continue;
|
|
935
|
+
const qx = sy * e1z - sz * e1y, qy = sz * e1x - sx * e1z, qz = sx * e1y - sy * e1x;
|
|
936
|
+
const v = (dx * qx + dy * qy + dz * qz) / det;
|
|
937
|
+
if (!(v >= 0 && u + v <= 1)) continue;
|
|
938
|
+
const t = (e2x * qx + e2y * qy + e2z * qz) / det;
|
|
939
|
+
if (t >= 0 && !(t >= nearest))
|
|
940
|
+
nearest = t;
|
|
941
|
+
}
|
|
942
|
+
return nearest;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// a sprite is the quad drawBillboard draws, its size3D grown by the x and y scale as the draw does, facing the camera
|
|
946
|
+
// as it was last drawn; where the ray meets its plane, inside its half axes
|
|
947
|
+
function render3DRaycastSprite(ray, o, matrix)
|
|
948
|
+
{
|
|
949
|
+
const center = matrix.getTranslation(), worldSize = render3DWorldSize(o, matrix.m), size = vec2(worldSize.x, worldSize.y);
|
|
950
|
+
if (!(size.x > 0 && size.y > 0)) return; // nothing to hit
|
|
951
|
+
if (!ray.direction.lengthSquared()) // a ray of no length is where it starts
|
|
952
|
+
return render3DRaycastDisc(ray, center, hypot(size.x, size.y) / 2);
|
|
953
|
+
const a = render3DBillboardAxes(size, o.rotation3D.z, o.upright);
|
|
954
|
+
const right = vec3(a[0], a[1], a[2]), up = vec3(a[3], a[4], a[5]);
|
|
955
|
+
const t = raycastPlane(ray, center, right.cross(up));
|
|
956
|
+
if (t === undefined) return; // behind, or seen edge on
|
|
957
|
+
const p = ray.getPosition(t).subtract(center);
|
|
958
|
+
return abs(p.dot(right)) <= right.lengthSquared() && abs(p.dot(up)) <= up.lengthSquared() ? t : undefined;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// a mesh flattened to nothing, or a sprite for a ray of no length: a disc facing the ray at its center's depth
|
|
962
|
+
function render3DRaycastDisc(ray, center, radius)
|
|
963
|
+
{
|
|
964
|
+
const d = ray.direction, oc = center.subtract(ray.origin), dd = d.dot(d);
|
|
965
|
+
const t = dd ? oc.dot(d) / dd : 0; // its depth along the ray, in the ray's own units
|
|
966
|
+
return t >= 0 && oc.subtract(d.scale(t)).lengthSquared() <= radius*radius ? t : undefined;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Collect every EngineObject3D a ray passes through, nearest first, the 3D twin of engineObjectsRaycast
|
|
971
|
+
* - The ray has no end, so everything along it counts however far away it is
|
|
972
|
+
* - Use render3D.pick for the nearest one on its own, with the distance to it
|
|
973
|
+
* @param {Ray3D} ray - From render3D.screenToRay, or any ray
|
|
974
|
+
* @param {Array<EngineObject>} [objects] - Defaults to every object; only those with a mesh or a sprite count
|
|
975
|
+
* @return {Array<EngineObject3D>}
|
|
976
|
+
* @memberof Render3D
|
|
977
|
+
*/
|
|
978
|
+
function engineObjectsRaycast3D(ray, objects=engineObjects)
|
|
979
|
+
{
|
|
980
|
+
const hits = [];
|
|
981
|
+
for (const o of objects)
|
|
982
|
+
{
|
|
983
|
+
const distance = render3DRaycastObject(ray, o);
|
|
984
|
+
if (distance !== undefined) // only a 3D object has a distance
|
|
985
|
+
hits.push({o: /** @type {EngineObject3D} */ (o), distance});
|
|
986
|
+
}
|
|
987
|
+
return hits.sort((a, b)=> a.distance - b.distance).map(hit=> hit.o);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Call a function for each EngineObject3D whose box overlaps a sphere or a box
|
|
992
|
+
* - An object destroyed by an earlier callback is skipped
|
|
993
|
+
* @param {Vector3} pos - Center of the area
|
|
994
|
+
* @param {Vector3|number} size - Diameter of a sphere if a number, 0 for a point, full size of a box if a Vector3
|
|
995
|
+
* @param {function(EngineObject3D): void} callback
|
|
996
|
+
* @param {Array<EngineObject>} [objects] - Defaults to every object
|
|
997
|
+
* @param {boolean} [testCenters] - Test only each object's center, see engineObjectsCollect3D
|
|
998
|
+
* @memberof Render3D
|
|
999
|
+
*/
|
|
1000
|
+
function engineObjectsCallback3D(pos, size, callback, objects=engineObjects, testCenters=false)
|
|
1001
|
+
{
|
|
1002
|
+
for (const o of engineObjectsCollect3D(pos, size, objects, testCenters))
|
|
1003
|
+
o.destroyed || callback(o);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
1007
|
+
/**
|
|
1008
|
+
* InstancedMesh3D - Many copies of one mesh drawn as one call, with their transforms kept on the GPU
|
|
1009
|
+
* - For big sets that mostly stay put: an instance costs nothing per frame until it changes, so a hundred thousand
|
|
1010
|
+
* trees cost what one tree does; objects and drawMesh batch by themselves too, but rebuild their batch every frame
|
|
1011
|
+
* - setTransformAt, setMatrixAt and setColorAt change one instance, and only the changed range uploads before the
|
|
1012
|
+
* next draw; the matrices and the colors are kept apart, so moving instances uploads 64 bytes each
|
|
1013
|
+
* - For many instances moving every frame, setTransforms places a run of them from arrays of positions, rotations
|
|
1014
|
+
* and scales in one loop, the fastest way; or write matrixData directly and call markDirtyRange
|
|
1015
|
+
* - The instances are in world space; the object's own pos3D, rotation3D and scale3D do not move them
|
|
1016
|
+
* - The whole set is culled by one bounding sphere around the origin, worked out when culling reads it, or set by hand
|
|
1017
|
+
* with radius; it casts and receives shadows like any object
|
|
1018
|
+
* - The object's flags cover the whole set, one emissive, one tileInfo, one shader; only the colors are per instance
|
|
1019
|
+
* - A mirrored instance, one with a negative scale, shows its inside unless the mesh is doubleSided
|
|
1020
|
+
* - A transparent set draws in one go in the transparent stage, its instances are not sorted against each other;
|
|
1021
|
+
* the set sorts against other transparent draws by the object's position, so put pos3D at its middle
|
|
1022
|
+
* - pick, the raycast and the collect helpers do not see the instances, test them yourself from matrixData
|
|
1023
|
+
* @extends EngineObject3D
|
|
1024
|
+
* @memberof Render3D
|
|
1025
|
+
* @example
|
|
1026
|
+
* const forest = new InstancedMesh3D(treeMesh, 1000);
|
|
1027
|
+
* for (let i = 0; i < 1000; ++i)
|
|
1028
|
+
* forest.setTransformAt(i, randomGroundPos(), vec3(0, rand(2*PI), 0));
|
|
1029
|
+
*/
|
|
1030
|
+
class InstancedMesh3D extends EngineObject3D
|
|
1031
|
+
{
|
|
1032
|
+
/** Create a set of instances of a mesh, each at the origin in the object's color until it is set
|
|
1033
|
+
* @param {Mesh} mesh
|
|
1034
|
+
* @param {number} count - How many instances there is room for, all of them draw until count is lowered
|
|
1035
|
+
* @param {TileInfo|TextureInfo} [tileInfo] - Texture for all of them
|
|
1036
|
+
* @param {Color} [color] - The color they start with */
|
|
1037
|
+
constructor(mesh, count, tileInfo, color=WHITE)
|
|
1038
|
+
{
|
|
1039
|
+
super(vec3(), mesh, tileInfo, color);
|
|
1040
|
+
ASSERT(mesh instanceof Mesh, 'an InstancedMesh3D needs a Mesh');
|
|
1041
|
+
ASSERT(count >= 1, 'an InstancedMesh3D needs room for at least one instance');
|
|
1042
|
+
this.size3D = vec3(); // not a solid thing to pick or collect
|
|
1043
|
+
/** @property {number} - How many instances draw, the first ones, up to the count it was made with */
|
|
1044
|
+
this.count = count;
|
|
1045
|
+
/** @property {number} - How many instances it was made with */
|
|
1046
|
+
this.maxCount = count;
|
|
1047
|
+
/** @property {Float32Array} - Each instance's matrix, 16 floats, what moving them uploads; edit it directly and
|
|
1048
|
+
* call markDirty for the instances changed */
|
|
1049
|
+
this.matrixData = new Float32Array(count * RENDER3D_MATRIX_FLOATS);
|
|
1050
|
+
/** @property {Float32Array} - Each instance's color and uv rect, 8 floats, uploaded only when they change; edit
|
|
1051
|
+
* it directly and call markColorDirty for the instances changed */
|
|
1052
|
+
this.colorData = new Float32Array(count * RENDER3D_COLOR_FLOATS);
|
|
1053
|
+
/** @property {number} - First instance whose matrix uploads before the next draw */
|
|
1054
|
+
this.dirtyStart = 0;
|
|
1055
|
+
/** @property {number} - One past the last instance whose matrix uploads, nothing when it is not past dirtyStart */
|
|
1056
|
+
this.dirtyEnd = count;
|
|
1057
|
+
/** @property {number} - First instance whose color uploads before the next draw */
|
|
1058
|
+
this.colorDirtyStart = 0;
|
|
1059
|
+
/** @property {number} - One past the last instance whose color uploads */
|
|
1060
|
+
this.colorDirtyEnd = count;
|
|
1061
|
+
// the farthest any instance's position has been from the origin, and the largest scale any has had, both
|
|
1062
|
+
// squared, and the instances set since radius last took them in: setting one does no bounds work at all
|
|
1063
|
+
this.reachSquared = 0;
|
|
1064
|
+
this.scaleSquared = 0;
|
|
1065
|
+
this.boundsStart = 0;
|
|
1066
|
+
this.boundsEnd = count;
|
|
1067
|
+
/** @type {number|undefined} */
|
|
1068
|
+
this.fixedRadius = undefined; // a radius set by hand, see radius
|
|
1069
|
+
this.buffer = undefined; // the GPU copy of matrixData
|
|
1070
|
+
/** @type {WebGLBuffer|undefined} */
|
|
1071
|
+
this.colorBuffer = undefined; // and of colorData
|
|
1072
|
+
this.bufferGeneration = -1; // the context they were made under
|
|
1073
|
+
this.uvTileInfo = tileInfo; // the tile the uv rects were written for
|
|
1074
|
+
const uv = render3DGetTileUVs(tileInfo), matrices = this.matrixData, colors = this.colorData;
|
|
1075
|
+
for (let i = 0; i < count; ++i)
|
|
1076
|
+
{
|
|
1077
|
+
matrices.set(RENDER3D_IDENTITY.m, i * RENDER3D_MATRIX_FLOATS);
|
|
1078
|
+
const k = i * RENDER3D_COLOR_FLOATS;
|
|
1079
|
+
colors[k] = color.r; colors[k+1] = color.g; colors[k+2] = color.b; colors[k+3] = color.a;
|
|
1080
|
+
colors[k+4] = uv.x; colors[k+5] = uv.y; colors[k+6] = uv.w; colors[k+7] = uv.h;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/** Place an instance by its position, rotation and scale, what buildMatrix takes, written straight in with no
|
|
1085
|
+
* matrix made; in world space
|
|
1086
|
+
* @param {number} i
|
|
1087
|
+
* @param {Vector3} [pos]
|
|
1088
|
+
* @param {Vector3} [rotation] - Euler angles in radians
|
|
1089
|
+
* @param {Vector3} [scale] */
|
|
1090
|
+
setTransformAt(i, pos, rotation, scale)
|
|
1091
|
+
{
|
|
1092
|
+
ASSERT(i >= 0 && i < this.maxCount, 'instance index out of range');
|
|
1093
|
+
matrix4Compose(this.matrixData, i * RENDER3D_MATRIX_FLOATS, pos, rotation, scale);
|
|
1094
|
+
this.markDirty(i);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/** Place a run of instances from arrays indexed by instance, plain or typed, as a game keeps them: each is placed
|
|
1098
|
+
* as setTransformAt places it, all in one loop with nothing made, the fastest way to move many every frame;
|
|
1099
|
+
* in world space
|
|
1100
|
+
* @param {number} start - First instance
|
|
1101
|
+
* @param {number} count - How many
|
|
1102
|
+
* @param {ArrayLike<number>} x - Positions
|
|
1103
|
+
* @param {ArrayLike<number>} y
|
|
1104
|
+
* @param {ArrayLike<number>} z
|
|
1105
|
+
* @param {ArrayLike<number>} [rx] - Euler angles in radians, all three or none for upright
|
|
1106
|
+
* @param {ArrayLike<number>} [ry]
|
|
1107
|
+
* @param {ArrayLike<number>} [rz]
|
|
1108
|
+
* @param {ArrayLike<number>} [sx] - Scales, all three or none for 1
|
|
1109
|
+
* @param {ArrayLike<number>} [sy]
|
|
1110
|
+
* @param {ArrayLike<number>} [sz] */
|
|
1111
|
+
setTransforms(start, count, x, y, z, rx, ry, rz, sx, sy, sz)
|
|
1112
|
+
{
|
|
1113
|
+
ASSERT(start >= 0 && count >= 0 && start + count <= this.maxCount, 'instance range out of range');
|
|
1114
|
+
ASSERT(!rx === !ry && !rx === !rz && !sx === !sy && !sx === !sz, 'rotations and scales come in threes');
|
|
1115
|
+
const m = this.matrixData, end = start + count;
|
|
1116
|
+
for (let i = start; i < end; ++i)
|
|
1117
|
+
{
|
|
1118
|
+
// matrix4Compose's expressions, so a run placed here is the same as each placed by setTransformAt
|
|
1119
|
+
const k = i * RENDER3D_MATRIX_FLOATS;
|
|
1120
|
+
const scaleX = sx ? sx[i] : 1, scaleY = sy ? sy[i] : 1, scaleZ = sz ? sz[i] : 1;
|
|
1121
|
+
if (rx)
|
|
1122
|
+
{
|
|
1123
|
+
const cx = cos(rx[i]), sinX = sin(rx[i]);
|
|
1124
|
+
const cy = cos(ry[i]), sinY = sin(ry[i]);
|
|
1125
|
+
const cz = cos(rz[i]), sinZ = sin(rz[i]);
|
|
1126
|
+
m[k] = (cy*cz + sinY*sinX*sinZ) * scaleX; m[k+1] = cx*sinZ * scaleX;
|
|
1127
|
+
m[k+2] = (-sinY*cz + cy*sinX*sinZ) * scaleX;
|
|
1128
|
+
m[k+4] = (-cy*sinZ + sinY*sinX*cz) * scaleY; m[k+5] = cx*cz * scaleY;
|
|
1129
|
+
m[k+6] = (sinY*sinZ + cy*sinX*cz) * scaleY;
|
|
1130
|
+
m[k+8] = sinY*cx * scaleZ; m[k+9] = -sinX * scaleZ; m[k+10] = cy*cx * scaleZ;
|
|
1131
|
+
}
|
|
1132
|
+
else
|
|
1133
|
+
{
|
|
1134
|
+
m[k] = scaleX; m[k+1] = 0; m[k+2] = 0;
|
|
1135
|
+
m[k+4] = 0; m[k+5] = scaleY; m[k+6] = 0;
|
|
1136
|
+
m[k+8] = 0; m[k+9] = 0; m[k+10] = scaleZ;
|
|
1137
|
+
}
|
|
1138
|
+
m[k+3] = m[k+7] = m[k+11] = 0; m[k+15] = 1;
|
|
1139
|
+
m[k+12] = x[i], m[k+13] = y[i], m[k+14] = z[i];
|
|
1140
|
+
}
|
|
1141
|
+
count && this.markDirtyRange(start, end);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/** Place an instance, in world space
|
|
1145
|
+
* @param {number} i
|
|
1146
|
+
* @param {Matrix4} matrix */
|
|
1147
|
+
setMatrixAt(i, matrix)
|
|
1148
|
+
{
|
|
1149
|
+
ASSERT(i >= 0 && i < this.maxCount, 'instance index out of range');
|
|
1150
|
+
const data = this.matrixData, k = i * RENDER3D_MATRIX_FLOATS, m = matrix.m;
|
|
1151
|
+
for (let j = 0; j < 16; ++j)
|
|
1152
|
+
data[k+j] = m[j]; // for 16 floats a loop beats set's call
|
|
1153
|
+
this.markDirty(i);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
/** The matrix of an instance
|
|
1157
|
+
* @param {number} i
|
|
1158
|
+
* @return {Matrix4} */
|
|
1159
|
+
getMatrixAt(i)
|
|
1160
|
+
{
|
|
1161
|
+
ASSERT(i >= 0 && i < this.maxCount, 'instance index out of range');
|
|
1162
|
+
const k = i * RENDER3D_MATRIX_FLOATS, matrix = new Matrix4;
|
|
1163
|
+
matrix.m.set(this.matrixData.subarray(k, k + 16));
|
|
1164
|
+
return matrix;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
/** Color an instance
|
|
1168
|
+
* @param {number} i
|
|
1169
|
+
* @param {Color} color */
|
|
1170
|
+
setColorAt(i, color)
|
|
1171
|
+
{
|
|
1172
|
+
ASSERT(i >= 0 && i < this.maxCount, 'instance index out of range');
|
|
1173
|
+
ASSERT(isColor(color), 'color must be a Color');
|
|
1174
|
+
const data = this.colorData, k = i * RENDER3D_COLOR_FLOATS;
|
|
1175
|
+
data[k] = color.r; data[k+1] = color.g; data[k+2] = color.b; data[k+3] = color.a;
|
|
1176
|
+
this.markColorDirty(i);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/** Note that an instance's matrix changed, so it uploads before the next draw and the bounds take it in when
|
|
1180
|
+
* they are read; setTransformAt and setMatrixAt call this, and so must an edit made straight to matrixData
|
|
1181
|
+
* @param {number} i */
|
|
1182
|
+
markDirty(i)
|
|
1183
|
+
{
|
|
1184
|
+
if (i < this.dirtyStart) this.dirtyStart = i;
|
|
1185
|
+
if (i >= this.dirtyEnd) this.dirtyEnd = i + 1;
|
|
1186
|
+
if (i < this.boundsStart) this.boundsStart = i;
|
|
1187
|
+
if (i >= this.boundsEnd) this.boundsEnd = i + 1;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
/** Note that a run of instances' matrices changed, for code that writes matrixData directly, the lowest level
|
|
1191
|
+
* way to move many: they upload before the next draw and the bounds take them in when they are read
|
|
1192
|
+
* @param {number} start - First instance
|
|
1193
|
+
* @param {number} end - One past the last */
|
|
1194
|
+
markDirtyRange(start, end)
|
|
1195
|
+
{
|
|
1196
|
+
ASSERT(start >= 0 && end <= this.maxCount && start <= end, 'instance range out of range');
|
|
1197
|
+
if (start < this.dirtyStart) this.dirtyStart = start;
|
|
1198
|
+
if (end > this.dirtyEnd) this.dirtyEnd = end;
|
|
1199
|
+
if (start < this.boundsStart) this.boundsStart = start;
|
|
1200
|
+
if (end > this.boundsEnd) this.boundsEnd = end;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/** Note that an instance's color changed, so it uploads before the next draw; setColorAt calls this, and so must
|
|
1204
|
+
* an edit made straight to colorData
|
|
1205
|
+
* @param {number} i */
|
|
1206
|
+
markColorDirty(i)
|
|
1207
|
+
{
|
|
1208
|
+
if (i < this.colorDirtyStart) this.colorDirtyStart = i;
|
|
1209
|
+
if (i >= this.colorDirtyEnd) this.colorDirtyEnd = i + 1;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/** Radius of the sphere around the origin that holds every instance set so far, for culling: the farthest
|
|
1213
|
+
* instance, and the mesh's size at the largest scale; it only grows, and takes in the instances set since it
|
|
1214
|
+
* was last read, so with frustum culling off it is never worked out. Set it to fix the sphere, which a big set
|
|
1215
|
+
* that is culled can use to skip that work, and set it to undefined to go back to the bounds
|
|
1216
|
+
* @return {number} */
|
|
1217
|
+
get radius()
|
|
1218
|
+
{
|
|
1219
|
+
if (this.fixedRadius !== undefined) return this.fixedRadius;
|
|
1220
|
+
if (this.boundsEnd > this.boundsStart)
|
|
1221
|
+
{
|
|
1222
|
+
// the bounds grow to hold where each is now and how big, read back from its matrix, squared
|
|
1223
|
+
const d = this.matrixData;
|
|
1224
|
+
let reachSquared = this.reachSquared, scaleSquared = this.scaleSquared;
|
|
1225
|
+
for (let i = this.boundsStart; i < this.boundsEnd; ++i)
|
|
1226
|
+
{
|
|
1227
|
+
const k = i * RENDER3D_MATRIX_FLOATS, x = d[k+12], y = d[k+13], z = d[k+14];
|
|
1228
|
+
const reach = x*x + y*y + z*z, scale = render3DMaxStretchSquared(d, k);
|
|
1229
|
+
if (reach > reachSquared) reachSquared = reach;
|
|
1230
|
+
if (scale > scaleSquared) scaleSquared = scale;
|
|
1231
|
+
}
|
|
1232
|
+
this.reachSquared = reachSquared, this.scaleSquared = scaleSquared;
|
|
1233
|
+
this.boundsStart = Infinity, this.boundsEnd = 0;
|
|
1234
|
+
}
|
|
1235
|
+
const mesh = this.mesh, meshRadius = mesh.radius || mesh.computeRadius(); // the mesh measured once
|
|
1236
|
+
return this.reachSquared ** .5 + meshRadius * this.scaleSquared ** .5;
|
|
1237
|
+
}
|
|
1238
|
+
set radius(radius) { this.fixedRadius = radius; }
|
|
1239
|
+
|
|
1240
|
+
/** Draws every instance as one call, uploading the ones that changed first
|
|
1241
|
+
* @return {void} */
|
|
1242
|
+
render3D()
|
|
1243
|
+
{
|
|
1244
|
+
const r = render3D, gl = glContext, mesh = this.mesh;
|
|
1245
|
+
ASSERT(this.count >= 0 && this.count <= this.maxCount, 'count must be within the count it was made with');
|
|
1246
|
+
if (r.transparentQueue) // sorted as one thing with the other transparent draws
|
|
1247
|
+
return r.queueTransparent(this.getWorldPos3D(), ()=> this.render3D());
|
|
1248
|
+
if (!mesh || !this.count || !render3DCanDraw()) return;
|
|
1249
|
+
if (r.shadowPass && !r.lighting) return; // unlit things cast no shadow
|
|
1250
|
+
render3DMeshUpload(mesh);
|
|
1251
|
+
if (!mesh.bufferCount) return;
|
|
1252
|
+
if (r.frustumCulling && !render3DSphereVisible(0, 0, 0, this.radius)) return;
|
|
1253
|
+
|
|
1254
|
+
// the uv rect is the object's tile for every instance, rewritten when the tile changes
|
|
1255
|
+
if (this.uvTileInfo !== this.tileInfo)
|
|
1256
|
+
{
|
|
1257
|
+
const uv = render3DGetTileUVs(this.tileInfo), data = this.colorData;
|
|
1258
|
+
for (let k = 4; k < data.length; k += RENDER3D_COLOR_FLOATS)
|
|
1259
|
+
data[k] = uv.x, data[k+1] = uv.y, data[k+2] = uv.w, data[k+3] = uv.h;
|
|
1260
|
+
this.uvTileInfo = this.tileInfo;
|
|
1261
|
+
this.colorDirtyStart = 0, this.colorDirtyEnd = this.maxCount;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// the GPU copies: all of each under a fresh context, otherwise just the changed ranges
|
|
1265
|
+
if (!this.buffer || this.bufferGeneration !== r.contextGeneration)
|
|
1266
|
+
{
|
|
1267
|
+
this.bufferGeneration = r.contextGeneration;
|
|
1268
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer = gl.createBuffer());
|
|
1269
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.matrixData, gl.DYNAMIC_DRAW);
|
|
1270
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer = gl.createBuffer());
|
|
1271
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.colorData, gl.DYNAMIC_DRAW);
|
|
1272
|
+
}
|
|
1273
|
+
else
|
|
1274
|
+
{
|
|
1275
|
+
const upload = (buffer, data, floats, start, end)=>
|
|
1276
|
+
{
|
|
1277
|
+
if (end <= start) return;
|
|
1278
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
1279
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, start * floats * 4, data, start * floats, (end - start) * floats);
|
|
1280
|
+
};
|
|
1281
|
+
upload(this.buffer, this.matrixData, RENDER3D_MATRIX_FLOATS, this.dirtyStart, this.dirtyEnd);
|
|
1282
|
+
upload(this.colorBuffer, this.colorData, RENDER3D_COLOR_FLOATS, this.colorDirtyStart, this.colorDirtyEnd);
|
|
1283
|
+
}
|
|
1284
|
+
this.dirtyStart = this.colorDirtyStart = Infinity, this.dirtyEnd = this.colorDirtyEnd = 0;
|
|
1285
|
+
|
|
1286
|
+
// one draw under the object's state, the mesh setting the culling as drawMesh does
|
|
1287
|
+
r.flush();
|
|
1288
|
+
render3DFlushBeforeOverlay(); // as in drawMesh, what was drawn before goes under it
|
|
1289
|
+
const cullBackFaces = r.cullBackFaces, tileInfo = this.tileInfo;
|
|
1290
|
+
r.cullBackFaces = !mesh.doubleSided;
|
|
1291
|
+
render3DDrawInstanced(mesh, this.buffer, this.count, render3DTextureOf(tileInfo), r, this.colorBuffer);
|
|
1292
|
+
r.cullBackFaces = cullBackFaces;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/** Destroy the set and free its GPU buffer
|
|
1296
|
+
* @param {boolean} [immediate] */
|
|
1297
|
+
destroy(immediate)
|
|
1298
|
+
{
|
|
1299
|
+
if (this.buffer && this.bufferGeneration === render3D?.contextGeneration)
|
|
1300
|
+
glContext?.deleteBuffer(this.buffer), glContext?.deleteBuffer(this.colorBuffer);
|
|
1301
|
+
this.buffer = this.colorBuffer = undefined;
|
|
1302
|
+
super.destroy(immediate);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
1307
|
+
/**
|
|
1308
|
+
* Light3D - A light that is an EngineObject3D, so it can move, follow a parent or be destroyed like anything else
|
|
1309
|
+
* - A point light: it lights what is near it and fades out by its radius, DirectionalLight3D shines from far away
|
|
1310
|
+
* - A coneAngle makes it a spotlight: it shines along its own forward, turned by rotation3D or by what it is
|
|
1311
|
+
* attached to, inside the cone, fading over the part of it coneSoftness says
|
|
1312
|
+
* - The sun, render3D.sunDirection, casts the shadows; a spotlight can cast them in its place, see
|
|
1313
|
+
* render3D.shadowLight; the other lights light and make highlights without one
|
|
1314
|
+
* - Only the 8 lights nearest the camera are used each frame
|
|
1315
|
+
* - radius is where the light fades out, and it fades fast, so a small radius wants a higher intensity
|
|
1316
|
+
* - intensity multiplies the color, above 1 for a light brighter than white
|
|
1317
|
+
* - radius is a world distance, so scale3D does not change it
|
|
1318
|
+
* - An alpha, an intensity or a radius of 0 switches it off, and a light that is off takes none of those slots
|
|
1319
|
+
* - Draws nothing but its glow, when it has one; add a small emissive mesh if the lamp itself should be seen
|
|
1320
|
+
* - addFlare gives it a lens flare, see LensFlare3D
|
|
1321
|
+
* @extends EngineObject3D
|
|
1322
|
+
* @memberof Render3D
|
|
1323
|
+
* @example
|
|
1324
|
+
* const torch = new Light3D(vec3(0, 3, 0), 10, hsl(.1, 1, .65));
|
|
1325
|
+
* torch.addFlare(); // light in the lens when the torch is in view
|
|
1326
|
+
*/
|
|
1327
|
+
class Light3D extends EngineObject3D
|
|
1328
|
+
{
|
|
1329
|
+
/** Create a point light
|
|
1330
|
+
* @param {Vector3} [pos3D] - Where it is
|
|
1331
|
+
* @param {number} [radius] - Distance where the light fades to nothing
|
|
1332
|
+
* @param {Color} [color] - Light color, its alpha fades it
|
|
1333
|
+
* @param {number} [intensity] - Brightness, multiplies the color, above 1 is brighter than white */
|
|
1334
|
+
constructor(pos3D=vec3(), radius=5, color=WHITE, intensity=1)
|
|
1335
|
+
{
|
|
1336
|
+
super(pos3D, undefined, undefined, color);
|
|
1337
|
+
ASSERT(radius >= 0, 'light radius cannot be negative, 0 is an off switch like an alpha of 0');
|
|
1338
|
+
ASSERT(intensity >= 0, 'light intensity cannot be negative, 0 is an off switch');
|
|
1339
|
+
this.size3D = vec3(); // not a solid thing to pick or collect
|
|
1340
|
+
/** @property {number} - Distance where the light fades to nothing */
|
|
1341
|
+
this.radius = radius;
|
|
1342
|
+
/** @property {number} - Brightness, multiplies the color, above 1 is brighter than white */
|
|
1343
|
+
this.intensity = intensity;
|
|
1344
|
+
/** @property {boolean} - Shine from far away, from its position toward the origin, instead of out from its
|
|
1345
|
+
* position with a falloff; DirectionalLight3D sets it */
|
|
1346
|
+
this.directional = false;
|
|
1347
|
+
/** @property {number} - Makes it a spotlight: the angle in radians from its forward out to the edge of its
|
|
1348
|
+
* cone, so the beam is twice this across; 0 for a light that shines every way */
|
|
1349
|
+
this.coneAngle = 0;
|
|
1350
|
+
/** @property {number} - How much of the cone is its fading edge: 0 a hard edge, .2 by default, the outer
|
|
1351
|
+
* fifth, 1 fading all the way from the middle of the beam */
|
|
1352
|
+
this.coneSoftness = .2;
|
|
1353
|
+
/** @property {number} - Size of a soft hazy glow drawn over the light, like a lamp at night, 0 for none; it
|
|
1354
|
+
* is added onto what is behind it, and what is in front of the light hides it */
|
|
1355
|
+
this.glow = 0;
|
|
1356
|
+
/** @property {number} - How fast the glow fades from its middle: 1 by default, .5 a wide haze, 2 a tight
|
|
1357
|
+
* bright core */
|
|
1358
|
+
this.glowFalloff = 1;
|
|
1359
|
+
/** @property {TextureInfo|TileInfo|undefined} - A gel: a picture the light shines through, like a stained glass
|
|
1360
|
+
* window or the leaves of a tree, cast along its cone in its colors, upright as the light looks out, the whole
|
|
1361
|
+
* texture of a TileInfo; only the spotlight that casts the shadows has one, with render3D.shadows on and it
|
|
1362
|
+
* as render3D.shadowLight, and only on what takes its shadows: an object with receiveShadow off is lit
|
|
1363
|
+
* without the gel; its alpha is not read, see through panes are dark
|
|
1364
|
+
* @type {TextureInfo|TileInfo|undefined} */
|
|
1365
|
+
this.gel = undefined;
|
|
1366
|
+
this.additive = true; // the glow is added on, in the transparent stage; a light with none draws nothing
|
|
1367
|
+
/** @type {LensFlare3D|undefined} */
|
|
1368
|
+
this.flareObject = undefined;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
/** Give the light a lens flare, made with the arguments of LensFlare3D, in place of the one it had
|
|
1372
|
+
* @param {number} [size] - Scales every part of it
|
|
1373
|
+
* @param {number} [count] - How many ghosts there are, besides the glow at the light
|
|
1374
|
+
* @param {number} [intensity] - How bright it is
|
|
1375
|
+
* @param {number} [saturation] - How colorful the ghosts are
|
|
1376
|
+
* @param {Color} [color] - Tints the flare, with the light's own color
|
|
1377
|
+
* @return {LensFlare3D} - The flare, to change: light.addFlare().shapes = ['hex'] */
|
|
1378
|
+
addFlare(size, count, intensity, saturation, color)
|
|
1379
|
+
{
|
|
1380
|
+
const flare = new LensFlare3D(size, count, intensity, saturation, color);
|
|
1381
|
+
this.flare = flare;
|
|
1382
|
+
return flare;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
/** The light's lens flare, undefined for none: addFlare makes it, or set a LensFlare3D of your own, and
|
|
1386
|
+
* destroying the flare takes it away
|
|
1387
|
+
* - The flare is the light's: it is attached to the light as its child, so it stays through a scene change
|
|
1388
|
+
* when the light does, and is destroyed with the light or when another takes its place
|
|
1389
|
+
* @type {LensFlare3D|undefined} */
|
|
1390
|
+
get flare() { return this.flareObject && !this.flareObject.destroyed ? this.flareObject : undefined; }
|
|
1391
|
+
|
|
1392
|
+
set flare(flare)
|
|
1393
|
+
{
|
|
1394
|
+
const old = this.flare; // a flare that was destroyed on its own is no flare
|
|
1395
|
+
if (flare === old) return;
|
|
1396
|
+
old && old.destroy();
|
|
1397
|
+
if (flare)
|
|
1398
|
+
{
|
|
1399
|
+
flare.light = this;
|
|
1400
|
+
flare.parent || this.addChild(flare);
|
|
1401
|
+
}
|
|
1402
|
+
this.flareObject = flare || undefined;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
/** Destroy the light, and its flare with it
|
|
1406
|
+
* @param {boolean} [immediate] */
|
|
1407
|
+
destroy(immediate)
|
|
1408
|
+
{
|
|
1409
|
+
this.flare = undefined;
|
|
1410
|
+
super.destroy(immediate);
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
/** Draw the glow, a quad facing the camera with a soft round glow on it, pulled toward the camera by half its
|
|
1414
|
+
* size so a lamp at the light does not cut into it, but never past the camera */
|
|
1415
|
+
render3D()
|
|
1416
|
+
{
|
|
1417
|
+
if (!(this.glow > 0) || this.directional) return;
|
|
1418
|
+
const r = render3D, c = this.color, pos = render3DObjectMatrix(this).getTranslation();
|
|
1419
|
+
const toCamera = r.camera.pos.subtract(pos), distance = toCamera.length();
|
|
1420
|
+
const at = distance ? pos.add(toCamera.scale(min(this.glow, distance) / 2 / distance)) : pos;
|
|
1421
|
+
const color = rgb(c.r, c.g, c.b, c.a * min(this.intensity, 1)), texture = engineGlowTexture(this.glowFalloff);
|
|
1422
|
+
// a browser that can not make the glow's texture draws no glow, not a plain square
|
|
1423
|
+
if (texture || !glContext)
|
|
1424
|
+
r.drawBillboard(at, vec2(this.glow), texture, color);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
1429
|
+
/**
|
|
1430
|
+
* DirectionalLight3D - A Light3D that shines from far away with no falloff, like sunlight
|
|
1431
|
+
* - It shines from its position toward the origin, like a three.js DirectionalLight: only the direction to it
|
|
1432
|
+
* counts, so moving it or its parent swings the light around; parent it to a sun in the sky and it follows
|
|
1433
|
+
* - It cannot sit on the origin, since that leaves no direction
|
|
1434
|
+
* - It casts no shadow: the sun, render3D.sunDirection, does, or a spotlight set as render3D.shadowLight
|
|
1435
|
+
* @extends Light3D
|
|
1436
|
+
* @memberof Render3D
|
|
1437
|
+
* @example
|
|
1438
|
+
* const fill = new DirectionalLight3D(vec3(-1, 1, 1), hsl(.6, .5, .3)); // from the back left and above
|
|
1439
|
+
*/
|
|
1440
|
+
class DirectionalLight3D extends Light3D
|
|
1441
|
+
{
|
|
1442
|
+
/** Create a directional light
|
|
1443
|
+
* @param {Vector3} [pos3D] - Where it shines from, toward the origin
|
|
1444
|
+
* @param {Color} [color] - Light color, its alpha fades it
|
|
1445
|
+
* @param {number} [intensity] - Brightness, multiplies the color, above 1 is brighter than white */
|
|
1446
|
+
constructor(pos3D=vec3(0, 1, 0), color=WHITE, intensity=1)
|
|
1447
|
+
{
|
|
1448
|
+
super(pos3D, 0, color, intensity);
|
|
1449
|
+
this.directional = true;
|
|
1450
|
+
}
|
|
1451
|
+
}
|