littlejsengine 1.21.0 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/littlejs.d.ts +649 -71
- package/dist/littlejs.esm.js +2919 -306
- package/dist/littlejs.esm.min.js +22 -9
- package/dist/littlejs.js +2885 -306
- package/dist/littlejs.min.js +21 -8
- package/dist/littlejs.release.js +2087 -281
- package/package.json +3 -2
- package/plugins/gltf.js +7 -1
- package/plugins/lightSystem.js +44 -1
- package/plugins/math3d.js +42 -20
- package/plugins/particleEffects.js +745 -0
- package/plugins/pluginExport.js +32 -0
- package/plugins/postProcess.js +206 -28
- package/plugins/render3d.js +484 -185
- package/plugins/render3dEditor.js +787 -23
- package/plugins/render3dExtras.js +223 -11
- package/plugins/render3dLevel.js +166 -3
- package/plugins/sceneSystem.js +3 -2
- package/src/engine.js +1 -1
- package/src/engineAudio.js +16 -7
- package/src/engineBuild.mjs +1 -0
- package/src/engineDraw.js +44 -1
- package/src/engineEditor.js +11 -2
- package/src/engineExport.js +2 -0
- package/src/engineParticles.js +80 -11
- package/src/engineWebGL.js +27 -10
package/plugins/render3d.js
CHANGED
|
@@ -29,6 +29,9 @@ const RENDER3D_VERTEX_BYTES = RENDER3D_VERTEX_FLOATS * 4;
|
|
|
29
29
|
// uv rect (12); constants for one draw, one per instance for a batch; the shader derives the normal matrix
|
|
30
30
|
const RENDER3D_INSTANCE_FLOATS = 24;
|
|
31
31
|
const RENDER3D_INSTANCE_BYTES = RENDER3D_INSTANCE_FLOATS * 4;
|
|
32
|
+
// an InstancedMesh3D keeps them apart: 16 floats of matrix, and 8 of tint and uv rect uploaded only when they change
|
|
33
|
+
const RENDER3D_MATRIX_FLOATS = 16;
|
|
34
|
+
const RENDER3D_COLOR_FLOATS = 8;
|
|
32
35
|
const RENDER3D_INSTANCE_ATTRIBS = [[4, 4, 0], [5, 4, 16], [6, 4, 32], [7, 4, 48], [11, 4, 64], [12, 4, 80]];
|
|
33
36
|
const RENDER3D_VERTEX_INPUTS =
|
|
34
37
|
'layout(location=0) in vec3 p;layout(location=1) in vec3 n;layout(location=2) in vec2 t;layout(location=3) in vec4 c;' +
|
|
@@ -72,22 +75,49 @@ function render3DCanDraw()
|
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
// the draw state fields a batch is drawn under; lights and fog are not captured, they are read live at flush
|
|
75
|
-
//
|
|
76
|
-
//
|
|
78
|
+
// each is an accessor on the plugin over render3D.drawState, and a real change moves render3D.stateVersion, so a
|
|
79
|
+
// batch checks that one number at each draw while the state holds; the three functions below write the fields out
|
|
80
|
+
// by hand for speed, so a new field goes in all four, in drawState and gets an accessor; emissiveMapColor is
|
|
81
|
+
// compared by its rgb, so two objects with equal colors batch
|
|
82
|
+
/** The draw state's values, behind render3D's accessors, and the rgb emissiveMapColor had when it was set
|
|
83
|
+
* @typedef {Object} Render3DDrawState
|
|
84
|
+
* @property {boolean} blend
|
|
85
|
+
* @property {boolean} additive
|
|
86
|
+
* @property {boolean} depthTest
|
|
87
|
+
* @property {boolean} depthWrite
|
|
88
|
+
* @property {boolean} cullBackFaces
|
|
89
|
+
* @property {boolean} mirrored
|
|
90
|
+
* @property {boolean} lighting
|
|
91
|
+
* @property {number} emissive
|
|
92
|
+
* @property {boolean} receiveShadow
|
|
93
|
+
* @property {number} specular
|
|
94
|
+
* @property {boolean} pixelated
|
|
95
|
+
* @property {Shader|undefined} shader
|
|
96
|
+
* @property {TextureInfo|undefined} normalMap
|
|
97
|
+
* @property {number} normalScale
|
|
98
|
+
* @property {number} shininess
|
|
99
|
+
* @property {number} reflectivity
|
|
100
|
+
* @property {TextureInfo|undefined} emissiveMap
|
|
101
|
+
* @property {Color} emissiveMapColor
|
|
102
|
+
* @property {number} emissiveR
|
|
103
|
+
* @property {number} emissiveG
|
|
104
|
+
* @property {number} emissiveB
|
|
105
|
+
* @memberof Render3D */
|
|
77
106
|
const RENDER3D_STATE_FIELDS = ['blend', 'additive', 'depthTest', 'depthWrite', 'cullBackFaces', 'mirrored', 'lighting',
|
|
78
107
|
'emissive', 'receiveShadow', 'specular', 'pixelated', 'shader', 'normalMap', 'normalScale', 'shininess',
|
|
79
108
|
'reflectivity', 'emissiveMap', 'emissiveMapColor'];
|
|
80
109
|
|
|
81
|
-
// a copy of the draw state in one fixed shape, the fields of RENDER3D_STATE_FIELDS written out
|
|
82
|
-
//
|
|
110
|
+
// a copy of the draw state in one fixed shape, the fields of RENDER3D_STATE_FIELDS written out, and the version
|
|
111
|
+
// it was taken at
|
|
83
112
|
function render3DCaptureBatchState()
|
|
84
113
|
{
|
|
85
|
-
const r = render3D;
|
|
86
|
-
return {blend:
|
|
87
|
-
cullBackFaces:
|
|
88
|
-
receiveShadow:
|
|
89
|
-
normalMap:
|
|
90
|
-
emissiveMap:
|
|
114
|
+
const r = render3D, d = r.drawState;
|
|
115
|
+
return {blend: d.blend, additive: d.additive, depthTest: d.depthTest, depthWrite: d.depthWrite,
|
|
116
|
+
cullBackFaces: d.cullBackFaces, mirrored: d.mirrored, lighting: d.lighting, emissive: d.emissive,
|
|
117
|
+
receiveShadow: d.receiveShadow, specular: d.specular, pixelated: d.pixelated, shader: d.shader,
|
|
118
|
+
normalMap: d.normalMap, normalScale: d.normalScale, shininess: d.shininess, reflectivity: d.reflectivity,
|
|
119
|
+
emissiveMap: d.emissiveMap, emissiveMapColor: (d.emissiveMapColor || WHITE).copy(), // the caller may change it
|
|
120
|
+
version: r.stateVersion};
|
|
91
121
|
}
|
|
92
122
|
|
|
93
123
|
// put a captured draw state back, written out the same way; the transparent stage does this for every queued draw
|
|
@@ -101,21 +131,23 @@ function render3DApplyBatchState(s)
|
|
|
101
131
|
r.reflectivity = s.reflectivity, r.emissiveMap = s.emissiveMap, r.emissiveMapColor = s.emissiveMapColor;
|
|
102
132
|
}
|
|
103
133
|
|
|
104
|
-
// true when the current draw state differs from a captured one, so a pending batch must flush first
|
|
134
|
+
// true when the current draw state differs from a captured one, so a pending batch must flush first; it runs for
|
|
135
|
+
// every instance drawn, so while nothing was set since the capture it checks the version alone
|
|
105
136
|
function render3DStateChanged(s)
|
|
106
137
|
{
|
|
107
138
|
const r = render3D;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
||
|
|
112
|
-
||
|
|
113
|
-
||
|
|
114
|
-
||
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
139
|
+
if (s.version === r.stateVersion) return false;
|
|
140
|
+
const d = r.drawState, c = s.emissiveMapColor;
|
|
141
|
+
if (d.blend !== s.blend || d.additive !== s.additive || d.depthTest !== s.depthTest
|
|
142
|
+
|| d.depthWrite !== s.depthWrite || d.cullBackFaces !== s.cullBackFaces || d.mirrored !== s.mirrored
|
|
143
|
+
|| d.lighting !== s.lighting || d.emissive !== s.emissive || d.receiveShadow !== s.receiveShadow
|
|
144
|
+
|| d.specular !== s.specular || d.pixelated !== s.pixelated || d.shader !== s.shader
|
|
145
|
+
|| d.normalMap !== s.normalMap || d.normalScale !== s.normalScale || d.shininess !== s.shininess
|
|
146
|
+
|| d.reflectivity !== s.reflectivity || d.emissiveMap !== s.emissiveMap
|
|
147
|
+
|| d.emissiveR !== c.r || d.emissiveG !== c.g || d.emissiveB !== c.b)
|
|
148
|
+
return true;
|
|
149
|
+
s.version = r.stateVersion; // set and set back to what it was: the batch goes on, checking the version again
|
|
150
|
+
return false;
|
|
119
151
|
}
|
|
120
152
|
|
|
121
153
|
// whether a sphere is inside the view, or the shadow map's box during the shadow pass, without a vector
|
|
@@ -193,7 +225,10 @@ function render3DMaxScale(m)
|
|
|
193
225
|
// how far a matrix at offset k can move a point that is one unit from its origin, for a bounding sphere: the
|
|
194
226
|
// longest axis when the axes are square to each other, more when they are not, as a turned child under a parent
|
|
195
227
|
// scaled on one axis leaves them; the axes' dot products bound the largest stretch by their largest row sum
|
|
196
|
-
function render3DMaxStretch(m, k=0)
|
|
228
|
+
function render3DMaxStretch(m, k=0) { return render3DMaxStretchSquared(m, k) ** .5; }
|
|
229
|
+
|
|
230
|
+
// the same, squared, for bounds that grow many times before they are read
|
|
231
|
+
function render3DMaxStretchSquared(m, k=0)
|
|
197
232
|
{
|
|
198
233
|
const xx = m[k]*m[k] + m[k+1]*m[k+1] + m[k+2]*m[k+2];
|
|
199
234
|
const yy = m[k+4]*m[k+4] + m[k+5]*m[k+5] + m[k+6]*m[k+6];
|
|
@@ -201,7 +236,7 @@ function render3DMaxStretch(m, k=0)
|
|
|
201
236
|
const xy = abs(m[k]*m[k+4] + m[k+1]*m[k+5] + m[k+2]*m[k+6]);
|
|
202
237
|
const xz = abs(m[k]*m[k+8] + m[k+1]*m[k+9] + m[k+2]*m[k+10]);
|
|
203
238
|
const yz = abs(m[k+4]*m[k+8] + m[k+5]*m[k+9] + m[k+6]*m[k+10]);
|
|
204
|
-
return max(xx + xy + xz, yy + xy + yz, zz + xz + yz)
|
|
239
|
+
return max(xx + xy + xz, yy + xy + yz, zz + xz + yz);
|
|
205
240
|
}
|
|
206
241
|
|
|
207
242
|
// a quad as a strip from its center and half axes, the same corner order as render3DQuadStrip
|
|
@@ -251,10 +286,11 @@ function render3DDrawObjects(objects)
|
|
|
251
286
|
render3DSetObjectState();
|
|
252
287
|
}
|
|
253
288
|
|
|
254
|
-
// add a draw of a mesh to its batch; a batch is one mesh under one texture and draw state, so a change flushes it
|
|
289
|
+
// add a draw of a mesh to its batch; a batch is one mesh under one texture and draw state, so a change flushes it,
|
|
290
|
+
// and under one winding: a mirroring transform, one with a negative determinant, turns it around
|
|
255
291
|
function render3DInstance(mesh, matrix, tileInfo, color)
|
|
256
292
|
{
|
|
257
|
-
const k = render3DInstanceSlot(mesh, render3DTextureOf(tileInfo)), data = mesh.instanceData;
|
|
293
|
+
const k = render3DInstanceSlot(mesh, render3DTextureOf(tileInfo), render3DMirrors(matrix)), data = mesh.instanceData;
|
|
258
294
|
data.set(matrix.m, k);
|
|
259
295
|
// the tint; this batch is never blended, so in the shadow pass the depth shader cuts it by its texture alpha
|
|
260
296
|
// alone, a see through caster draws blended instead and is cut by its tint alpha too
|
|
@@ -265,14 +301,18 @@ function render3DInstance(mesh, matrix, tileInfo, color)
|
|
|
265
301
|
|
|
266
302
|
// make room for one more instance of a mesh under a texture and the current draw state, flushing a batch that
|
|
267
303
|
// differs first, and return where its 24 floats go in mesh.instanceData: the matrix, the tint and the uv rect
|
|
268
|
-
|
|
304
|
+
// drawMesh passes whether its matrix mirrors, and its batch then culls by the mesh and winds by that at the flush,
|
|
305
|
+
// so it never sets the draw state for them, which would move the state's version at every draw
|
|
306
|
+
function render3DInstanceSlot(mesh, textureInfo, mirrored)
|
|
269
307
|
{
|
|
270
308
|
const r = render3D;
|
|
271
|
-
if (mesh.instanceCount && (mesh.instanceTextureInfo !== textureInfo ||
|
|
309
|
+
if (mesh.instanceCount && (mesh.instanceTextureInfo !== textureInfo || mesh.instanceMirrored !== mirrored
|
|
310
|
+
|| render3DStateChanged(mesh.instanceState)))
|
|
272
311
|
render3DFlushInstances(mesh);
|
|
273
312
|
if (!mesh.instanceCount)
|
|
274
313
|
{
|
|
275
314
|
mesh.instanceTextureInfo = textureInfo;
|
|
315
|
+
mesh.instanceMirrored = mirrored;
|
|
276
316
|
mesh.instanceState = render3DCaptureBatchState();
|
|
277
317
|
r.instanceMeshes.push(mesh);
|
|
278
318
|
}
|
|
@@ -303,7 +343,10 @@ function render3DFlushInstances(only)
|
|
|
303
343
|
const buffers = r.instanceBuffers, buffer = buffers[r.instanceBufferIndex = (r.instanceBufferIndex + 1) % buffers.length];
|
|
304
344
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
305
345
|
gl.bufferData(gl.ARRAY_BUFFER, mesh.instanceData, gl.DYNAMIC_DRAW, 0, count * RENDER3D_INSTANCE_FLOATS);
|
|
306
|
-
|
|
346
|
+
const state = mesh.instanceState;
|
|
347
|
+
if (mesh.instanceMirrored !== undefined) // a drawMesh batch: culled by its mesh, wound by its matrices
|
|
348
|
+
state.cullBackFaces = !mesh.doubleSided, state.mirrored = mesh.instanceMirrored;
|
|
349
|
+
render3DDrawInstanced(mesh, buffer, count, mesh.instanceTextureInfo, state);
|
|
307
350
|
}
|
|
308
351
|
if (!only)
|
|
309
352
|
r.instanceMeshes.length = 0;
|
|
@@ -318,13 +361,16 @@ function render3DFlushInstances(only)
|
|
|
318
361
|
// the arrays are turned on with their instance divisor for this one call and both are turned off after: a single
|
|
319
362
|
// draw reads these slots as constant attributes, and in Firefox a draw that reads a constant through a slot whose
|
|
320
363
|
// divisor is set leaves the next batch on that slot reading the wrong values
|
|
321
|
-
|
|
364
|
+
// colorBuffer holds the tint and uv rect apart from the matrices, as an InstancedMesh3D keeps them
|
|
365
|
+
function render3DDrawInstanced(mesh, buffer, count, textureInfo, state, colorBuffer)
|
|
322
366
|
{
|
|
323
|
-
const gl = glContext;
|
|
324
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
367
|
+
const gl = glContext, matrixBytes = RENDER3D_MATRIX_FLOATS * 4;
|
|
325
368
|
for (const [location, size, offset] of RENDER3D_INSTANCE_ATTRIBS)
|
|
326
369
|
{
|
|
327
|
-
|
|
370
|
+
const inColors = colorBuffer && offset >= matrixBytes;
|
|
371
|
+
const stride = !colorBuffer ? RENDER3D_INSTANCE_BYTES : inColors ? RENDER3D_COLOR_FLOATS * 4 : matrixBytes;
|
|
372
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, inColors ? colorBuffer : buffer);
|
|
373
|
+
gl.vertexAttribPointer(location, size, gl.FLOAT, false, stride, inColors ? offset - matrixBytes : offset);
|
|
328
374
|
gl.enableVertexAttribArray(location);
|
|
329
375
|
gl.vertexAttribDivisor(location, 1);
|
|
330
376
|
}
|
|
@@ -369,11 +415,36 @@ function render3DCollectLights()
|
|
|
369
415
|
for (const light of lights)
|
|
370
416
|
distances.set(light, light.directional ? -1 : light.getWorldPos3D().distanceSquared(cameraPos));
|
|
371
417
|
lights.sort((a, b)=> distances.get(a) - distances.get(b));
|
|
418
|
+
// the light that casts the shadows keeps a slot however far it is, in place of the farthest
|
|
419
|
+
const caster = render3DShadowCaster();
|
|
420
|
+
if (caster && lights.indexOf(caster) >= RENDER3D_MAX_LIGHTS)
|
|
421
|
+
lights[RENDER3D_MAX_LIGHTS - 1] = caster;
|
|
372
422
|
lights.length = RENDER3D_MAX_LIGHTS;
|
|
373
423
|
}
|
|
374
424
|
return lights;
|
|
375
425
|
}
|
|
376
426
|
|
|
427
|
+
// the spotlight that casts the shadows in place of the sun: render3D.shadowLight while it is a light that is on
|
|
428
|
+
// and has a cone, undefined for the sun
|
|
429
|
+
function render3DShadowCaster()
|
|
430
|
+
{
|
|
431
|
+
const light = render3D.shadowLight;
|
|
432
|
+
return light && !light.destroyed && !light.directional && light.coneAngle > 0 && light.radius > 0 &&
|
|
433
|
+
light.intensity > 0 && light.color.a > 0 ? light : undefined;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// a light's cone as the four numbers the shader gets: the way it shines, scaled so that its dot with the way to
|
|
437
|
+
// a point, less the fourth number, is 0 at the edge of the cone and 1 where its fade starts; a light with no cone
|
|
438
|
+
// gets numbers that make it 1 every way
|
|
439
|
+
function render3DLightCone(light)
|
|
440
|
+
{
|
|
441
|
+
const angle = light.directional ? 0 : min(light.coneAngle, PI);
|
|
442
|
+
if (!(angle > 0)) return [0, 0, 0, -1];
|
|
443
|
+
const outer = cos(angle), inner = cos(angle * (1 - clamp(light.coneSoftness)));
|
|
444
|
+
const k = 1 / max(inner - outer, 1e-4), forward = light.getForward3D();
|
|
445
|
+
return [forward.x * k, forward.y * k, forward.z * k, outer * k];
|
|
446
|
+
}
|
|
447
|
+
|
|
377
448
|
// unit circle directions for a number of sides, [cos, sin, cos, sin, ...] including the closing point, cached
|
|
378
449
|
const render3DCircleCache = new Map;
|
|
379
450
|
function render3DCircle(sides)
|
|
@@ -425,7 +496,15 @@ class Render3DPlugin
|
|
|
425
496
|
{
|
|
426
497
|
ASSERT(!render3D, 'Render3D plugin already initialized');
|
|
427
498
|
render3D = this;
|
|
428
|
-
|
|
499
|
+
// the draw state, read at each draw through the accessors below the constructor
|
|
500
|
+
/** @type {Render3DDrawState} */
|
|
501
|
+
this.drawState = {blend: false, additive: false, depthTest: true, depthWrite: true, cullBackFaces: false,
|
|
502
|
+
mirrored: false, lighting: true, emissive: 0, receiveShadow: true, specular: 0, pixelated: false,
|
|
503
|
+
shader: undefined, normalMap: undefined, normalScale: 1, shininess: 16, reflectivity: 0,
|
|
504
|
+
emissiveMap: undefined, emissiveMapColor: WHITE, emissiveR: 1, emissiveG: 1, emissiveB: 1};
|
|
505
|
+
/** @property {number} - Goes up when a draw state field changes, so a batch sees at a glance that none did */
|
|
506
|
+
this.stateVersion = 0;
|
|
507
|
+
ASSERT(Object.keys(render3DCaptureBatchState()).join() === [...RENDER3D_STATE_FIELDS, 'version'].join(),
|
|
429
508
|
'the batch state functions must list RENDER3D_STATE_FIELDS');
|
|
430
509
|
|
|
431
510
|
/** @property {Camera3D} - The camera */
|
|
@@ -471,8 +550,16 @@ class Render3DPlugin
|
|
|
471
550
|
// shadows
|
|
472
551
|
/** @property {boolean} - Cast real shadows from the sun, off by default and free when off */
|
|
473
552
|
this.shadows = false;
|
|
553
|
+
/** @property {number} - How far a sound played with playSound is heard when it was made with no range of
|
|
554
|
+
* its own, in world units; further than the 2D soundDefaultRange, a 3D world is bigger */
|
|
555
|
+
this.soundDefaultRange = 100;
|
|
474
556
|
/** @property {number} - Size of the shadow map in pixels, bigger is sharper and slower */
|
|
475
557
|
this.shadowMapSize = 1024;
|
|
558
|
+
/** @property {Light3D|undefined} - A spotlight, a Light3D with a coneAngle, to cast the shadows in place
|
|
559
|
+
* of the sun, a flashlight in the dark: the shadow map looks down its cone, as far as its radius, and the
|
|
560
|
+
* sun still lights the scene but casts none; undefined for the sun, as is a light that is off or has no cone
|
|
561
|
+
* @type {Light3D|undefined} */
|
|
562
|
+
this.shadowLight = undefined;
|
|
476
563
|
/** @property {number} - World size the shadow map covers around shadowCenter, smaller is sharper; it is a
|
|
477
564
|
* square facing the light, so it turns as the light does, and about 1.5 times an area's width covers it from
|
|
478
565
|
* any angle */
|
|
@@ -485,50 +572,6 @@ class Render3DPlugin
|
|
|
485
572
|
/** @property {number} - How much to blur the shadow edges */
|
|
486
573
|
this.shadowSoftness = 1;
|
|
487
574
|
|
|
488
|
-
// draw state, read at each draw
|
|
489
|
-
/** @property {boolean} - Apply lighting, when false draws plain vertex color times texture and casts no shadow;
|
|
490
|
-
* off for billboards, lines, ribbons and soft discs, an object sets emissive instead */
|
|
491
|
-
this.lighting = true;
|
|
492
|
-
/** @property {number} - How much a surface lights itself, set per object by its emissive */
|
|
493
|
-
this.emissive = 0;
|
|
494
|
-
/** @property {boolean} - Additive blending instead of alpha, in the transparent stage */
|
|
495
|
-
this.additive = false;
|
|
496
|
-
/** @property {boolean} - Test against the depth buffer, reset to true before each object and callback; a draw
|
|
497
|
-
* with it off goes over what was drawn before it and under what is drawn after, by render order, which
|
|
498
|
-
* ends the batch of meshes before it, so one per object costs a draw per object */
|
|
499
|
-
this.depthTest = true;
|
|
500
|
-
/** @property {boolean} - Write to the depth buffer, owned by the stages: on for opaque, off for transparent */
|
|
501
|
-
this.depthWrite = true;
|
|
502
|
-
// batch state set by drawMesh from each mesh: whether its back faces are skipped, off for strips so they
|
|
503
|
-
// show from both sides, and whether its transform mirrors it so the other winding is the front
|
|
504
|
-
this.cullBackFaces = false;
|
|
505
|
-
this.mirrored = false;
|
|
506
|
-
/** @property {number} - Strength of the highlight where the sun and the Light3D objects reflect, 0 is none and
|
|
507
|
-
* 1 adds a light's full color at its brightest; shininess sets its size */
|
|
508
|
-
this.specular = 0;
|
|
509
|
-
/** @property {number} - The highlight's exponent, how small and sharp it is: 4 is broad like rubber, 100 sharp
|
|
510
|
-
* like polished metal; set from each object's shininess */
|
|
511
|
-
this.shininess = 16;
|
|
512
|
-
/** @property {TextureInfo|undefined} - Normal map for the next draws, set from each object's normalMap
|
|
513
|
-
* @type {TextureInfo|undefined} */
|
|
514
|
-
this.normalMap = undefined;
|
|
515
|
-
/** @property {number} - How strongly the normal map bends the surface, set from each object's normalScale */
|
|
516
|
-
this.normalScale = 1;
|
|
517
|
-
/** @property {number} - How much the surface reflects the sky, 0 to 1, set from each object's reflectivity */
|
|
518
|
-
this.reflectivity = 0;
|
|
519
|
-
/** @property {TextureInfo|undefined} - Emissive map for the next draws, set from each object's emissiveMap
|
|
520
|
-
* @type {TextureInfo|undefined} */
|
|
521
|
-
this.emissiveMap = undefined;
|
|
522
|
-
/** @property {Color} - Multiplies the emissive map, set from each object's emissiveMapColor */
|
|
523
|
-
this.emissiveMapColor = WHITE;
|
|
524
|
-
/** @property {Shader|undefined} - Custom Shader for the next draws, set from each object's shader; undefined
|
|
525
|
-
* draws with the plugin's own
|
|
526
|
-
* @type {Shader|undefined} */
|
|
527
|
-
this.shader = undefined;
|
|
528
|
-
/** @property {boolean} - Darken by the shadow map when shadows are on, turn it off for things that should stay
|
|
529
|
-
* lit inside a shadow */
|
|
530
|
-
this.receiveShadow = true;
|
|
531
|
-
|
|
532
575
|
// the pass
|
|
533
576
|
/** @property {Function|undefined} - Draw solid world here, it runs again for shadows so only draw in it
|
|
534
577
|
* @type {Function|undefined} */
|
|
@@ -551,9 +594,6 @@ class Render3DPlugin
|
|
|
551
594
|
/** @property {boolean} - Sample textures through mipmaps so they do not shimmer in the distance, false uses
|
|
552
595
|
* each texture's own filtering like 2D */
|
|
553
596
|
this.mipmaps = true;
|
|
554
|
-
/** @property {boolean} - Draw state: keep texture pixels hard edged, no mipmaps and no blending between them,
|
|
555
|
-
* set per object by pixelated */
|
|
556
|
-
this.pixelated = false;
|
|
557
597
|
/** @property {number} - Anisotropic filtering for textures seen at an angle, 1 to 16, 1 is off; needs mipmaps */
|
|
558
598
|
this.anisotropy = 4;
|
|
559
599
|
|
|
@@ -597,7 +637,6 @@ class Render3DPlugin
|
|
|
597
637
|
this.cameraBack = vec3(0, 0, 1); // its opposite, the normal of camera facing draws
|
|
598
638
|
|
|
599
639
|
// internal state
|
|
600
|
-
this.blend = false; // blending on, set by the stages
|
|
601
640
|
/** @type {Array<Array<number>>} */
|
|
602
641
|
this.frustumPlanes = []; // the view as six inward planes [x, y, z, w]
|
|
603
642
|
/** @type {Array<Array<number>>} */
|
|
@@ -637,6 +676,9 @@ class Render3DPlugin
|
|
|
637
676
|
this.passIsDefault = true; // the running pass is the default layer, the only one shadowed
|
|
638
677
|
this.lightPositions = new Float32Array(RENDER3D_MAX_LIGHTS * 4); // Light3D uniforms, filled each pass
|
|
639
678
|
this.lightColors = new Float32Array(RENDER3D_MAX_LIGHTS * 4);
|
|
679
|
+
this.lightCones = new Float32Array(RENDER3D_MAX_LIGHTS * 4); // each light's cone, see render3DLightCone
|
|
680
|
+
this.shadowLightIndex = -1; // which of the lights sent casts the shadows, -1 for the sun
|
|
681
|
+
this.shadowDepthBias = 0; // the bias as the shadow lookup takes it, set with the shadow matrix
|
|
640
682
|
|
|
641
683
|
// the stream of immediate mode draws
|
|
642
684
|
/** @type {WebGLBuffer|undefined} */
|
|
@@ -664,6 +706,116 @@ class Render3DPlugin
|
|
|
664
706
|
engineAddPlugin(undefined, render3DRender, render3DContextLost, render3DContextRestored, render3DPreRender);
|
|
665
707
|
}
|
|
666
708
|
|
|
709
|
+
///////////////////////////////////////////////////////////////////////////
|
|
710
|
+
// Draw state: each field an accessor over drawState, whose setter moves stateVersion only on a real change
|
|
711
|
+
|
|
712
|
+
/** Apply lighting, when false draws plain vertex color times texture and casts no shadow;
|
|
713
|
+
* off for billboards, lines, ribbons and soft discs, an object sets emissive instead
|
|
714
|
+
* @return {boolean} */
|
|
715
|
+
get lighting() { return this.drawState.lighting; }
|
|
716
|
+
set lighting(v) { const d = this.drawState; d.lighting === v || (d.lighting = v, ++this.stateVersion); }
|
|
717
|
+
|
|
718
|
+
/** How much a surface lights itself, set per object by its emissive
|
|
719
|
+
* @return {number} */
|
|
720
|
+
get emissive() { return this.drawState.emissive; }
|
|
721
|
+
set emissive(v) { const d = this.drawState; d.emissive === v || (d.emissive = v, ++this.stateVersion); }
|
|
722
|
+
|
|
723
|
+
/** Additive blending instead of alpha, in the transparent stage
|
|
724
|
+
* @return {boolean} */
|
|
725
|
+
get additive() { return this.drawState.additive; }
|
|
726
|
+
set additive(v) { const d = this.drawState; d.additive === v || (d.additive = v, ++this.stateVersion); }
|
|
727
|
+
|
|
728
|
+
/** Test against the depth buffer, reset to true before each object and callback; a draw
|
|
729
|
+
* with it off goes over what was drawn before it and under what is drawn after, by render order, which
|
|
730
|
+
* ends the batch of meshes before it, so one per object costs a draw per object
|
|
731
|
+
* @return {boolean} */
|
|
732
|
+
get depthTest() { return this.drawState.depthTest; }
|
|
733
|
+
set depthTest(v) { const d = this.drawState; d.depthTest === v || (d.depthTest = v, ++this.stateVersion); }
|
|
734
|
+
|
|
735
|
+
/** Write to the depth buffer, owned by the stages: on for opaque, off for transparent
|
|
736
|
+
* @return {boolean} */
|
|
737
|
+
get depthWrite() { return this.drawState.depthWrite; }
|
|
738
|
+
set depthWrite(v) { const d = this.drawState; d.depthWrite === v || (d.depthWrite = v, ++this.stateVersion); }
|
|
739
|
+
|
|
740
|
+
/** Skip back faces, set by drawMesh from each mesh: off for strips so they show from
|
|
741
|
+
* both sides
|
|
742
|
+
* @return {boolean} */
|
|
743
|
+
get cullBackFaces() { return this.drawState.cullBackFaces; }
|
|
744
|
+
set cullBackFaces(v) { const d = this.drawState; d.cullBackFaces === v || (d.cullBackFaces = v, ++this.stateVersion); }
|
|
745
|
+
|
|
746
|
+
/** The transform mirrors the draw, so the other winding is the front, set by drawMesh
|
|
747
|
+
* from each transform
|
|
748
|
+
* @return {boolean} */
|
|
749
|
+
get mirrored() { return this.drawState.mirrored; }
|
|
750
|
+
set mirrored(v) { const d = this.drawState; d.mirrored === v || (d.mirrored = v, ++this.stateVersion); }
|
|
751
|
+
|
|
752
|
+
/** Strength of the highlight where the sun and the Light3D objects reflect, 0 is none and
|
|
753
|
+
* 1 adds a light's full color at its brightest; shininess sets its size
|
|
754
|
+
* @return {number} */
|
|
755
|
+
get specular() { return this.drawState.specular; }
|
|
756
|
+
set specular(v) { const d = this.drawState; d.specular === v || (d.specular = v, ++this.stateVersion); }
|
|
757
|
+
|
|
758
|
+
/** The highlight's exponent, how small and sharp it is: 4 is broad like rubber, 100 sharp
|
|
759
|
+
* like polished metal; set from each object's shininess
|
|
760
|
+
* @return {number} */
|
|
761
|
+
get shininess() { return this.drawState.shininess; }
|
|
762
|
+
set shininess(v) { const d = this.drawState; d.shininess === v || (d.shininess = v, ++this.stateVersion); }
|
|
763
|
+
|
|
764
|
+
/** Normal map for the next draws, set from each object's normalMap
|
|
765
|
+
* @return {TextureInfo|undefined} */
|
|
766
|
+
get normalMap() { return this.drawState.normalMap; }
|
|
767
|
+
set normalMap(v) { const d = this.drawState; d.normalMap === v || (d.normalMap = v, ++this.stateVersion); }
|
|
768
|
+
|
|
769
|
+
/** How strongly the normal map bends the surface, set from each object's normalScale
|
|
770
|
+
* @return {number} */
|
|
771
|
+
get normalScale() { return this.drawState.normalScale; }
|
|
772
|
+
set normalScale(v) { const d = this.drawState; d.normalScale === v || (d.normalScale = v, ++this.stateVersion); }
|
|
773
|
+
|
|
774
|
+
/** How much the surface reflects the sky, 0 to 1, set from each object's reflectivity
|
|
775
|
+
* @return {number} */
|
|
776
|
+
get reflectivity() { return this.drawState.reflectivity; }
|
|
777
|
+
set reflectivity(v) { const d = this.drawState; d.reflectivity === v || (d.reflectivity = v, ++this.stateVersion); }
|
|
778
|
+
|
|
779
|
+
/** Emissive map for the next draws, set from each object's emissiveMap
|
|
780
|
+
* @return {TextureInfo|undefined} */
|
|
781
|
+
get emissiveMap() { return this.drawState.emissiveMap; }
|
|
782
|
+
set emissiveMap(v) { const d = this.drawState; d.emissiveMap === v || (d.emissiveMap = v, ++this.stateVersion); }
|
|
783
|
+
|
|
784
|
+
/** Multiplies the emissive map, set from each object's emissiveMapColor; compared by its rgb, so a Color
|
|
785
|
+
* changed in place is seen when it is set again, and undefined is white
|
|
786
|
+
* @return {Color} */
|
|
787
|
+
get emissiveMapColor() { return this.drawState.emissiveMapColor; }
|
|
788
|
+
set emissiveMapColor(v)
|
|
789
|
+
{
|
|
790
|
+
const d = this.drawState, c = v || WHITE;
|
|
791
|
+
d.emissiveMapColor = v;
|
|
792
|
+
if (c.r !== d.emissiveR || c.g !== d.emissiveG || c.b !== d.emissiveB)
|
|
793
|
+
d.emissiveR = c.r, d.emissiveG = c.g, d.emissiveB = c.b, ++this.stateVersion;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/** Custom Shader for the next draws, set from each object's shader; undefined draws
|
|
797
|
+
* with the plugin's own
|
|
798
|
+
* @return {Shader|undefined} */
|
|
799
|
+
get shader() { return this.drawState.shader; }
|
|
800
|
+
set shader(v) { const d = this.drawState; d.shader === v || (d.shader = v, ++this.stateVersion); }
|
|
801
|
+
|
|
802
|
+
/** Darken by the shadow map when shadows are on, turn it off for things that should
|
|
803
|
+
* stay lit inside a shadow
|
|
804
|
+
* @return {boolean} */
|
|
805
|
+
get receiveShadow() { return this.drawState.receiveShadow; }
|
|
806
|
+
set receiveShadow(v) { const d = this.drawState; d.receiveShadow === v || (d.receiveShadow = v, ++this.stateVersion); }
|
|
807
|
+
|
|
808
|
+
/** Keep texture pixels hard edged, no mipmaps and no blending between them, set per
|
|
809
|
+
* object by pixelated
|
|
810
|
+
* @return {boolean} */
|
|
811
|
+
get pixelated() { return this.drawState.pixelated; }
|
|
812
|
+
set pixelated(v) { const d = this.drawState; d.pixelated === v || (d.pixelated = v, ++this.stateVersion); }
|
|
813
|
+
|
|
814
|
+
/** Blending on, set by the stages
|
|
815
|
+
* @return {boolean} */
|
|
816
|
+
get blend() { return this.drawState.blend; }
|
|
817
|
+
set blend(v) { const d = this.drawState; d.blend === v || (d.blend = v, ++this.stateVersion); }
|
|
818
|
+
|
|
667
819
|
///////////////////////////////////////////////////////////////////////////
|
|
668
820
|
// Matrices and picking
|
|
669
821
|
|
|
@@ -773,7 +925,8 @@ class Render3DPlugin
|
|
|
773
925
|
}
|
|
774
926
|
|
|
775
927
|
/** Play a sound at a 3D position, quieter with distance from the camera and panned by its side, like Sound.play
|
|
776
|
-
* with a 2D position
|
|
928
|
+
* with a 2D position; a sound made with no range of its own is heard to soundDefaultRange, 100, and one given
|
|
929
|
+
* a range keeps it
|
|
777
930
|
* @param {Sound} sound
|
|
778
931
|
* @param {Vector3} pos3D
|
|
779
932
|
* @param {number} [volume]
|
|
@@ -787,7 +940,9 @@ class Render3DPlugin
|
|
|
787
940
|
// the 3D range fade and pan, Sound.play with no position does the rest
|
|
788
941
|
ASSERT(sound instanceof Sound, 'sound must be a Sound');
|
|
789
942
|
ASSERT(isVector3(pos3D), 'pos3D must be a vec3');
|
|
790
|
-
|
|
943
|
+
// a sound with no range of its own is heard further in 3D than in 2D
|
|
944
|
+
const offset = pos3D.subtract(this.camera.pos);
|
|
945
|
+
const range = sound.rangeIsDefault ? this.soundDefaultRange : sound.range;
|
|
791
946
|
if (range)
|
|
792
947
|
{
|
|
793
948
|
const distance = offset.length();
|
|
@@ -851,15 +1006,15 @@ class Render3DPlugin
|
|
|
851
1006
|
if (this.frustumCulling && !render3DSphereVisible(m[12], m[13], m[14], mesh.radius * render3DMaxStretch(m)))
|
|
852
1007
|
return;
|
|
853
1008
|
// the mesh says whether its back faces can be skipped, and a mirroring transform, one with a negative
|
|
854
|
-
// determinant, turns the winding around so the other one is its front
|
|
855
|
-
|
|
856
|
-
this.cullBackFaces = !mesh.doubleSided;
|
|
857
|
-
this.mirrored = render3DMirrors(matrix);
|
|
858
|
-
// the stage draws the batch at its end
|
|
1009
|
+
// determinant, turns the winding around so the other one is its front; the stage draws a batch at its
|
|
1010
|
+
// end, which keys on those itself
|
|
859
1011
|
if (!this.blend && this.depthTest && (mesh.instanced ?? this.instancing))
|
|
860
1012
|
render3DInstance(mesh, matrix, tileInfo, color);
|
|
861
1013
|
else
|
|
862
1014
|
{
|
|
1015
|
+
const cullBackFaces = this.cullBackFaces, mirrored = this.mirrored;
|
|
1016
|
+
this.cullBackFaces = !mesh.doubleSided;
|
|
1017
|
+
this.mirrored = render3DMirrors(matrix);
|
|
863
1018
|
this.flush();
|
|
864
1019
|
render3DFlushBeforeOverlay();
|
|
865
1020
|
render3DSetDrawUniforms(matrix, tileInfo, color);
|
|
@@ -867,8 +1022,8 @@ class Render3DPlugin
|
|
|
867
1022
|
glContext.drawElements(glContext.TRIANGLES, mesh.bufferCount, mesh.indexType, 0);
|
|
868
1023
|
++drawCount;
|
|
869
1024
|
primitiveCount += mesh.bufferCount / 3;
|
|
1025
|
+
this.cullBackFaces = cullBackFaces, this.mirrored = mirrored;
|
|
870
1026
|
}
|
|
871
|
-
this.cullBackFaces = cullBackFaces, this.mirrored = mirrored;
|
|
872
1027
|
}
|
|
873
1028
|
|
|
874
1029
|
/** Draw a triangle strip, batched into the stream with the current draw state
|
|
@@ -1064,6 +1219,23 @@ class Render3DPlugin
|
|
|
1064
1219
|
/** Rebuild the light's view projection around the shadow center, called automatically each frame shadows are on */
|
|
1065
1220
|
updateShadowMatrix()
|
|
1066
1221
|
{
|
|
1222
|
+
const caster = render3DShadowCaster();
|
|
1223
|
+
if (caster)
|
|
1224
|
+
{
|
|
1225
|
+
// a spotlight's map looks down its cone with perspective, from just in front of it out to its radius
|
|
1226
|
+
const pos = caster.getWorldPos3D(), forward = caster.getForward3D();
|
|
1227
|
+
const up = abs(forward.y) > .99 ? vec3(0, 0, 1) : vec3(0, 1, 0);
|
|
1228
|
+
const far = caster.radius, near = max(far / 500, .02);
|
|
1229
|
+
const fov = min(min(caster.coneAngle, PI) * 2 + .1, 2.8);
|
|
1230
|
+
this.shadowMatrix = Matrix4.perspective(fov, 1, near, far).multiply(
|
|
1231
|
+
Matrix4.lookAt(pos, pos.add(forward), up).invert());
|
|
1232
|
+
this.shadowPlanes = render3DFrustumPlanes(this.shadowMatrix);
|
|
1233
|
+
// depth is not even with perspective: the lookup divides this by the distance squared, which makes
|
|
1234
|
+
// the bias the same distance in the world near the light and far from it
|
|
1235
|
+
this.shadowDepthBias = this.shadowBias * far * far * near / (far - near);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
this.shadowDepthBias = this.shadowBias; // the sun's map is flat, its depth even
|
|
1067
1239
|
ASSERT(this.shadowRange > 0, 'shadowRange must be positive');
|
|
1068
1240
|
const range = this.shadowRange > 0 ? this.shadowRange : 1, half = range / 2;
|
|
1069
1241
|
const toSun = this.sunDirection.normalize();
|
|
@@ -1493,6 +1665,7 @@ function render3DFragmentSource(fragmentCode)
|
|
|
1493
1665
|
'precision highp float;' +
|
|
1494
1666
|
'uniform vec4 lightDir,lightColor,ambientFog,ambientGround,fogColor,shadowParams;' +
|
|
1495
1667
|
'uniform vec4 extraLights[' + RENDER3D_MAX_LIGHTS + '],extraLightColors[' + RENDER3D_MAX_LIGHTS + '];' +
|
|
1668
|
+
'uniform vec4 extraLightCones[' + RENDER3D_MAX_LIGHTS + '];' +
|
|
1496
1669
|
'uniform int extraLightCount;' +
|
|
1497
1670
|
'uniform vec3 cameraPos;' +
|
|
1498
1671
|
'uniform vec4 materialParams,emissiveTint,skyTop,skyHorizon,skyBottom;' +
|
|
@@ -1501,12 +1674,14 @@ function render3DFragmentSource(fragmentCode)
|
|
|
1501
1674
|
'uniform highp sampler2DShadow shadowMap;' +
|
|
1502
1675
|
'in vec3 P,N;in vec2 T,L;in vec4 C,S;' +
|
|
1503
1676
|
'out vec4 o;' +
|
|
1504
|
-
// the
|
|
1677
|
+
// the shadow at this fragment, 0 to 1: the light's depth map with a 3x3 blur, outside the map is lit; the
|
|
1678
|
+
// sun's map is flat and S.w is 1, a spotlight's has perspective and S.w is the distance from it, where the
|
|
1679
|
+
// bias shrinks by its square to stay the same distance in the world
|
|
1505
1680
|
'float shadow(){' +
|
|
1506
|
-
'if(shadowParams.x<=0.)return 1.;' +
|
|
1681
|
+
'if(shadowParams.x<=0.||S.w<=0.)return 1.;' +
|
|
1507
1682
|
'vec3 q=S.xyz/S.w*.5+.5;' +
|
|
1508
1683
|
'if(any(greaterThanEqual(abs(q-.5),vec3(.5))))return 1.;' +
|
|
1509
|
-
'q.z-=shadowParams.y;' +
|
|
1684
|
+
'q.z-=shadowParams.y/(S.w*S.w);' +
|
|
1510
1685
|
'float s=0.;' +
|
|
1511
1686
|
'for(int x=-1;x<=1;++x)for(int y=-1;y<=1;++y)' +
|
|
1512
1687
|
's+=texture(shadowMap,vec3(q.xy+vec2(x,y)*shadowParams.z,q.z));' +
|
|
@@ -1534,7 +1709,9 @@ function render3DFragmentSource(fragmentCode)
|
|
|
1534
1709
|
'if(!gl_FrontFacing)n=-n;' + // only a double sided mesh shows a back face, light it on the side that is seen
|
|
1535
1710
|
'if(materialParams.x!=0.)n=normalMapNormal(n);' +
|
|
1536
1711
|
'float nl=dot(n,-lightDir.xyz);' +
|
|
1537
|
-
|
|
1712
|
+
// the shadow is the sun's, or one spotlight's: shadowParams.x is 1 for the sun, 2 and up for that Light3D
|
|
1713
|
+
'float sh=shadow(),s=shadowParams.x<1.5?sh:1.;' +
|
|
1714
|
+
'int si=int(shadowParams.x+.5)-2;' +
|
|
1538
1715
|
// the ambient: one color, or blended from the ground color below to the sky color above by the way the face points
|
|
1539
1716
|
'vec3 l=(ambientGround.a>0.?mix(ambientGround.rgb,ambientFog.rgb,n.y*.5+.5):ambientFog.rgb)+lightColor.rgb*max(nl,0.)*s;' +
|
|
1540
1717
|
// the Light3D objects: a point light falls off with distance, a directional one does not and carries the
|
|
@@ -1550,6 +1727,11 @@ function render3DFragmentSource(fragmentCode)
|
|
|
1550
1727
|
'v/=max(d,1e-6);' +
|
|
1551
1728
|
'float ln=dot(n,v);' +
|
|
1552
1729
|
'vec3 lc=extraLightColors[i].rgb*extraLightColors[i].a*a*a;' +
|
|
1730
|
+
// a spotlight's cone: full where its fade starts, nothing at its edge, smooth between; 1 with no cone
|
|
1731
|
+
'vec4 K=extraLightCones[i];' +
|
|
1732
|
+
'float k=clamp(dot(K.xyz,-v)-K.w,0.,1.);' +
|
|
1733
|
+
'lc*=k*k*(3.-2.*k);' +
|
|
1734
|
+
'if(i==si)lc*=sh;' +
|
|
1553
1735
|
'l+=lc*max(0.,ln);' +
|
|
1554
1736
|
'if(lightColor.a>0.)sp+=lc*pow(max(dot(reflect(-v,n),eye),0.),materialParams.y)*step(0.,ln);' +
|
|
1555
1737
|
'}' +
|
|
@@ -1604,6 +1786,7 @@ function render3DUseProgram(program)
|
|
|
1604
1786
|
{
|
|
1605
1787
|
gl.uniform4fv(render3DUniform('extraLights'), r.lightPositions, 0, r.lightCount * 4);
|
|
1606
1788
|
gl.uniform4fv(render3DUniform('extraLightColors'), r.lightColors, 0, r.lightCount * 4);
|
|
1789
|
+
gl.uniform4fv(render3DUniform('extraLightCones'), r.lightCones, 0, r.lightCount * 4);
|
|
1607
1790
|
}
|
|
1608
1791
|
if (program !== r.program)
|
|
1609
1792
|
{
|
|
@@ -1744,18 +1927,19 @@ function render3DUpdateSamplers()
|
|
|
1744
1927
|
r.samplers.clear();
|
|
1745
1928
|
}
|
|
1746
1929
|
|
|
1747
|
-
// the sampler for a texture's wrap modes, smooth or hard edged, made the first time it is needed
|
|
1748
|
-
|
|
1930
|
+
// the sampler for a texture's wrap modes, smooth or hard edged, made the first time it is needed; pixelated is the
|
|
1931
|
+
// draw's, texturePixelated the texture's own, which a smooth texture in a pixel art game sets false
|
|
1932
|
+
function render3DSampler(wrap, pixelated, texturePixelated=tilesPixelated)
|
|
1749
1933
|
{
|
|
1750
1934
|
const gl = glContext, r = render3D, [wrapS, wrapT] = glWrapModes(wrap);
|
|
1751
|
-
const key = wrapS * 1e5 + wrapT *
|
|
1935
|
+
const key = wrapS * 1e5 + wrapT * 4 + (pixelated ? 1 : 0) + (texturePixelated ? 2 : 0); // the modes are 5 digit numbers
|
|
1752
1936
|
let sampler = r.samplers.get(key);
|
|
1753
1937
|
if (sampler) return sampler;
|
|
1754
1938
|
sampler = gl.createSampler();
|
|
1755
|
-
const sharp = pixelated ||
|
|
1939
|
+
const sharp = pixelated || texturePixelated;
|
|
1756
1940
|
gl.samplerParameteri(sampler, gl.TEXTURE_MAG_FILTER, sharp ? gl.NEAREST : gl.LINEAR);
|
|
1757
1941
|
gl.samplerParameteri(sampler, gl.TEXTURE_MIN_FILTER, pixelated ? gl.NEAREST
|
|
1758
|
-
:
|
|
1942
|
+
: texturePixelated ? gl.NEAREST_MIPMAP_LINEAR : gl.LINEAR_MIPMAP_LINEAR);
|
|
1759
1943
|
gl.samplerParameteri(sampler, gl.TEXTURE_WRAP_S, wrapS);
|
|
1760
1944
|
gl.samplerParameteri(sampler, gl.TEXTURE_WRAP_T, wrapT);
|
|
1761
1945
|
const anisotropy = gl.getExtension('EXT_texture_filter_anisotropic');
|
|
@@ -1783,7 +1967,7 @@ function render3DBindTexture(tileInfo, state=render3D, unit=0)
|
|
|
1783
1967
|
// or anisotropy, and filtering it that way costs every untextured fragment
|
|
1784
1968
|
else
|
|
1785
1969
|
{
|
|
1786
|
-
gl.bindSampler(unit, render3DSampler(textureInfo?.wrap, state.pixelated));
|
|
1970
|
+
gl.bindSampler(unit, render3DSampler(textureInfo?.wrap, state.pixelated, textureInfo?.pixelated ?? tilesPixelated));
|
|
1787
1971
|
glUpdateMipmaps(texture); // drawn into since its mipmaps were made
|
|
1788
1972
|
if (!state.pixelated && !glMipmappedTextures.has(texture)) // a hard edged draw never reads them
|
|
1789
1973
|
{
|
|
@@ -1963,7 +2147,9 @@ function render3DSetDrawUniforms(matrix, tileInfo, tint, uvRect, state=render3D)
|
|
|
1963
2147
|
// 0 blends them away instead, and -1 is additive, which has to fade into fog differently
|
|
1964
2148
|
const blendMode = state.blend ? (state.additive ? -1 : 0) : 1;
|
|
1965
2149
|
const receives = r.shadows && r.passIsDefault && state.receiveShadow ? 1 : 0;
|
|
1966
|
-
|
|
2150
|
+
// 1 the sun's shadow, 2 and up the shadow of that Light3D, the spotlight that casts them
|
|
2151
|
+
const shadowOf = receives && r.shadowLightIndex >= 0 ? 2 + r.shadowLightIndex : receives;
|
|
2152
|
+
render3DUniform4f('shadowParams', shadowOf, r.shadowDepthBias, r.shadowSoftness / r.shadowTextureSize, blendMode);
|
|
1967
2153
|
|
|
1968
2154
|
// a render target's texture holds premultiplied color, the blend writes it that way, so the shader undoes it
|
|
1969
2155
|
const textureInfo = render3DTextureOf(tileInfo);
|
|
@@ -2029,6 +2215,8 @@ function render3DRenderPass(after2D)
|
|
|
2029
2215
|
// gathered before the gl state is taken over, so an assert here leaves nothing to hand back
|
|
2030
2216
|
const lights = render3DCollectLights();
|
|
2031
2217
|
r.lightCount = lights.length;
|
|
2218
|
+
const caster = render3DShadowCaster();
|
|
2219
|
+
r.shadowLightIndex = caster ? lights.indexOf(caster) : -1;
|
|
2032
2220
|
const positions = r.lightPositions, colors = r.lightColors;
|
|
2033
2221
|
lights.forEach((light, i)=>
|
|
2034
2222
|
{
|
|
@@ -2039,6 +2227,7 @@ function render3DRenderPass(after2D)
|
|
|
2039
2227
|
positions[k] = p.x, positions[k+1] = p.y, positions[k+2] = p.z;
|
|
2040
2228
|
positions[k+3] = light.directional ? -1 : max(0, light.radius); // a negative radius marks a direction
|
|
2041
2229
|
colors[k] = c.r, colors[k+1] = c.g, colors[k+2] = c.b, colors[k+3] = c.a * light.intensity;
|
|
2230
|
+
r.lightCones.set(render3DLightCone(light), k);
|
|
2042
2231
|
});
|
|
2043
2232
|
|
|
2044
2233
|
// take over the gl state
|
|
@@ -2110,6 +2299,7 @@ function render3DUpdateShadowMap(size)
|
|
|
2110
2299
|
{
|
|
2111
2300
|
const gl = glContext, r = render3D;
|
|
2112
2301
|
ASSERT(size > 0, 'shadowMapSize must be positive');
|
|
2302
|
+
size = glClampTextureSize(size); // a size the device does not have falls back to the largest it has
|
|
2113
2303
|
if (r.shadowTexture && r.shadowTextureSize === size) return;
|
|
2114
2304
|
r.shadowTexture && gl.deleteTexture(r.shadowTexture);
|
|
2115
2305
|
r.shadowFramebuffer && gl.deleteFramebuffer(r.shadowFramebuffer);
|
|
@@ -4162,20 +4352,24 @@ function engineObjectsCallback3D(pos, size, callback, objects=engineObjects, tes
|
|
|
4162
4352
|
* InstancedMesh3D - Many copies of one mesh drawn as one call, with their transforms kept on the GPU
|
|
4163
4353
|
* - For big sets that mostly stay put: an instance costs nothing per frame until it changes, so a hundred thousand
|
|
4164
4354
|
* trees cost what one tree does; objects and drawMesh batch by themselves too, but rebuild their batch every frame
|
|
4165
|
-
* - setMatrixAt and setColorAt change one instance, and only the changed range uploads before the
|
|
4355
|
+
* - setTransformAt, setMatrixAt and setColorAt change one instance, and only the changed range uploads before the
|
|
4356
|
+
* next draw; the matrices and the colors are kept apart, so moving instances uploads 64 bytes each
|
|
4357
|
+
* - For many instances moving every frame, setTransforms places a run of them from arrays of positions, rotations
|
|
4358
|
+
* and scales in one loop, the fastest way; or write matrixData directly and call markDirtyRange
|
|
4166
4359
|
* - The instances are in world space; the object's own pos3D, rotation3D and scale3D do not move them
|
|
4167
|
-
* - The whole set is culled by one bounding sphere around the origin,
|
|
4360
|
+
* - The whole set is culled by one bounding sphere around the origin, worked out when culling reads it, or set by hand
|
|
4361
|
+
* with radius; it casts and receives shadows like any object
|
|
4168
4362
|
* - The object's flags cover the whole set, one emissive, one tileInfo, one shader; only the colors are per instance
|
|
4169
4363
|
* - A mirrored instance, one with a negative scale, shows its inside unless the mesh is doubleSided
|
|
4170
4364
|
* - A transparent set draws in one go in the transparent stage, its instances are not sorted against each other;
|
|
4171
4365
|
* the set sorts against other transparent draws by the object's position, so put pos3D at its middle
|
|
4172
|
-
* - pick, the raycast and the collect helpers do not see the instances, test them yourself from
|
|
4366
|
+
* - pick, the raycast and the collect helpers do not see the instances, test them yourself from matrixData
|
|
4173
4367
|
* @extends EngineObject3D
|
|
4174
4368
|
* @memberof Render3D
|
|
4175
4369
|
* @example
|
|
4176
4370
|
* const forest = new InstancedMesh3D(treeMesh, 1000);
|
|
4177
4371
|
* for (let i = 0; i < 1000; ++i)
|
|
4178
|
-
* forest.
|
|
4372
|
+
* forest.setTransformAt(i, randomGroundPos(), vec3(0, rand(2*PI), 0));
|
|
4179
4373
|
*/
|
|
4180
4374
|
class InstancedMesh3D extends EngineObject3D
|
|
4181
4375
|
{
|
|
@@ -4194,29 +4388,101 @@ class InstancedMesh3D extends EngineObject3D
|
|
|
4194
4388
|
this.count = count;
|
|
4195
4389
|
/** @property {number} - How many instances it was made with */
|
|
4196
4390
|
this.maxCount = count;
|
|
4197
|
-
/** @property {Float32Array} -
|
|
4198
|
-
*
|
|
4199
|
-
this.
|
|
4200
|
-
/** @property {
|
|
4201
|
-
*
|
|
4202
|
-
this.
|
|
4203
|
-
|
|
4204
|
-
this.maxScale = 0; // and the largest scale any instance has had
|
|
4205
|
-
/** @property {number} - First instance to upload before the next draw */
|
|
4391
|
+
/** @property {Float32Array} - Each instance's matrix, 16 floats, what moving them uploads; edit it directly and
|
|
4392
|
+
* call markDirty for the instances changed */
|
|
4393
|
+
this.matrixData = new Float32Array(count * RENDER3D_MATRIX_FLOATS);
|
|
4394
|
+
/** @property {Float32Array} - Each instance's color and uv rect, 8 floats, uploaded only when they change; edit
|
|
4395
|
+
* it directly and call markColorDirty for the instances changed */
|
|
4396
|
+
this.colorData = new Float32Array(count * RENDER3D_COLOR_FLOATS);
|
|
4397
|
+
/** @property {number} - First instance whose matrix uploads before the next draw */
|
|
4206
4398
|
this.dirtyStart = 0;
|
|
4207
|
-
/** @property {number} - One past the last instance
|
|
4399
|
+
/** @property {number} - One past the last instance whose matrix uploads, nothing when it is not past dirtyStart */
|
|
4208
4400
|
this.dirtyEnd = count;
|
|
4209
|
-
|
|
4210
|
-
this.
|
|
4211
|
-
|
|
4212
|
-
|
|
4401
|
+
/** @property {number} - First instance whose color uploads before the next draw */
|
|
4402
|
+
this.colorDirtyStart = 0;
|
|
4403
|
+
/** @property {number} - One past the last instance whose color uploads */
|
|
4404
|
+
this.colorDirtyEnd = count;
|
|
4405
|
+
// the farthest any instance's position has been from the origin, and the largest scale any has had, both
|
|
4406
|
+
// squared, and the instances set since radius last took them in: setting one does no bounds work at all
|
|
4407
|
+
this.reachSquared = 0;
|
|
4408
|
+
this.scaleSquared = 0;
|
|
4409
|
+
this.boundsStart = 0;
|
|
4410
|
+
this.boundsEnd = count;
|
|
4411
|
+
/** @type {number|undefined} */
|
|
4412
|
+
this.fixedRadius = undefined; // a radius set by hand, see radius
|
|
4413
|
+
this.buffer = undefined; // the GPU copy of matrixData
|
|
4414
|
+
/** @type {WebGLBuffer|undefined} */
|
|
4415
|
+
this.colorBuffer = undefined; // and of colorData
|
|
4416
|
+
this.bufferGeneration = -1; // the context they were made under
|
|
4417
|
+
this.uvTileInfo = tileInfo; // the tile the uv rects were written for
|
|
4418
|
+
const uv = render3DGetTileUVs(tileInfo), matrices = this.matrixData, colors = this.colorData;
|
|
4213
4419
|
for (let i = 0; i < count; ++i)
|
|
4214
4420
|
{
|
|
4215
|
-
|
|
4216
|
-
const k = i *
|
|
4217
|
-
|
|
4218
|
-
|
|
4421
|
+
matrices.set(RENDER3D_IDENTITY.m, i * RENDER3D_MATRIX_FLOATS);
|
|
4422
|
+
const k = i * RENDER3D_COLOR_FLOATS;
|
|
4423
|
+
colors[k] = color.r; colors[k+1] = color.g; colors[k+2] = color.b; colors[k+3] = color.a;
|
|
4424
|
+
colors[k+4] = uv.x; colors[k+5] = uv.y; colors[k+6] = uv.w; colors[k+7] = uv.h;
|
|
4425
|
+
}
|
|
4426
|
+
}
|
|
4427
|
+
|
|
4428
|
+
/** Place an instance by its position, rotation and scale, what buildMatrix takes, written straight in with no
|
|
4429
|
+
* matrix made; in world space
|
|
4430
|
+
* @param {number} i
|
|
4431
|
+
* @param {Vector3} [pos]
|
|
4432
|
+
* @param {Vector3} [rotation] - Euler angles in radians
|
|
4433
|
+
* @param {Vector3} [scale] */
|
|
4434
|
+
setTransformAt(i, pos, rotation, scale)
|
|
4435
|
+
{
|
|
4436
|
+
ASSERT(i >= 0 && i < this.maxCount, 'instance index out of range');
|
|
4437
|
+
matrix4Compose(this.matrixData, i * RENDER3D_MATRIX_FLOATS, pos, rotation, scale);
|
|
4438
|
+
this.markDirty(i);
|
|
4439
|
+
}
|
|
4440
|
+
|
|
4441
|
+
/** Place a run of instances from arrays indexed by instance, plain or typed, as a game keeps them: each is placed
|
|
4442
|
+
* as setTransformAt places it, all in one loop with nothing made, the fastest way to move many every frame;
|
|
4443
|
+
* in world space
|
|
4444
|
+
* @param {number} start - First instance
|
|
4445
|
+
* @param {number} count - How many
|
|
4446
|
+
* @param {ArrayLike<number>} x - Positions
|
|
4447
|
+
* @param {ArrayLike<number>} y
|
|
4448
|
+
* @param {ArrayLike<number>} z
|
|
4449
|
+
* @param {ArrayLike<number>} [rx] - Euler angles in radians, all three or none for upright
|
|
4450
|
+
* @param {ArrayLike<number>} [ry]
|
|
4451
|
+
* @param {ArrayLike<number>} [rz]
|
|
4452
|
+
* @param {ArrayLike<number>} [sx] - Scales, all three or none for 1
|
|
4453
|
+
* @param {ArrayLike<number>} [sy]
|
|
4454
|
+
* @param {ArrayLike<number>} [sz] */
|
|
4455
|
+
setTransforms(start, count, x, y, z, rx, ry, rz, sx, sy, sz)
|
|
4456
|
+
{
|
|
4457
|
+
ASSERT(start >= 0 && count >= 0 && start + count <= this.maxCount, 'instance range out of range');
|
|
4458
|
+
ASSERT(!rx === !ry && !rx === !rz && !sx === !sy && !sx === !sz, 'rotations and scales come in threes');
|
|
4459
|
+
const m = this.matrixData, end = start + count;
|
|
4460
|
+
for (let i = start; i < end; ++i)
|
|
4461
|
+
{
|
|
4462
|
+
// matrix4Compose's expressions, so a run placed here is the same as each placed by setTransformAt
|
|
4463
|
+
const k = i * RENDER3D_MATRIX_FLOATS;
|
|
4464
|
+
const scaleX = sx ? sx[i] : 1, scaleY = sy ? sy[i] : 1, scaleZ = sz ? sz[i] : 1;
|
|
4465
|
+
if (rx)
|
|
4466
|
+
{
|
|
4467
|
+
const cx = cos(rx[i]), sinX = sin(rx[i]);
|
|
4468
|
+
const cy = cos(ry[i]), sinY = sin(ry[i]);
|
|
4469
|
+
const cz = cos(rz[i]), sinZ = sin(rz[i]);
|
|
4470
|
+
m[k] = (cy*cz + sinY*sinX*sinZ) * scaleX; m[k+1] = cx*sinZ * scaleX;
|
|
4471
|
+
m[k+2] = (-sinY*cz + cy*sinX*sinZ) * scaleX;
|
|
4472
|
+
m[k+4] = (-cy*sinZ + sinY*sinX*cz) * scaleY; m[k+5] = cx*cz * scaleY;
|
|
4473
|
+
m[k+6] = (sinY*sinZ + cy*sinX*cz) * scaleY;
|
|
4474
|
+
m[k+8] = sinY*cx * scaleZ; m[k+9] = -sinX * scaleZ; m[k+10] = cy*cx * scaleZ;
|
|
4475
|
+
}
|
|
4476
|
+
else
|
|
4477
|
+
{
|
|
4478
|
+
m[k] = scaleX; m[k+1] = 0; m[k+2] = 0;
|
|
4479
|
+
m[k+4] = 0; m[k+5] = scaleY; m[k+6] = 0;
|
|
4480
|
+
m[k+8] = 0; m[k+9] = 0; m[k+10] = scaleZ;
|
|
4481
|
+
}
|
|
4482
|
+
m[k+3] = m[k+7] = m[k+11] = 0; m[k+15] = 1;
|
|
4483
|
+
m[k+12] = x[i], m[k+13] = y[i], m[k+14] = z[i];
|
|
4219
4484
|
}
|
|
4485
|
+
count && this.markDirtyRange(start, end);
|
|
4220
4486
|
}
|
|
4221
4487
|
|
|
4222
4488
|
/** Place an instance, in world space
|
|
@@ -4225,8 +4491,9 @@ class InstancedMesh3D extends EngineObject3D
|
|
|
4225
4491
|
setMatrixAt(i, matrix)
|
|
4226
4492
|
{
|
|
4227
4493
|
ASSERT(i >= 0 && i < this.maxCount, 'instance index out of range');
|
|
4228
|
-
const data = this.
|
|
4229
|
-
|
|
4494
|
+
const data = this.matrixData, k = i * RENDER3D_MATRIX_FLOATS, m = matrix.m;
|
|
4495
|
+
for (let j = 0; j < 16; ++j)
|
|
4496
|
+
data[k+j] = m[j]; // for 16 floats a loop beats set's call
|
|
4230
4497
|
this.markDirty(i);
|
|
4231
4498
|
}
|
|
4232
4499
|
|
|
@@ -4236,8 +4503,8 @@ class InstancedMesh3D extends EngineObject3D
|
|
|
4236
4503
|
getMatrixAt(i)
|
|
4237
4504
|
{
|
|
4238
4505
|
ASSERT(i >= 0 && i < this.maxCount, 'instance index out of range');
|
|
4239
|
-
const k = i *
|
|
4240
|
-
matrix.m.set(this.
|
|
4506
|
+
const k = i * RENDER3D_MATRIX_FLOATS, matrix = new Matrix4;
|
|
4507
|
+
matrix.m.set(this.matrixData.subarray(k, k + 16));
|
|
4241
4508
|
return matrix;
|
|
4242
4509
|
}
|
|
4243
4510
|
|
|
@@ -4248,26 +4515,71 @@ class InstancedMesh3D extends EngineObject3D
|
|
|
4248
4515
|
{
|
|
4249
4516
|
ASSERT(i >= 0 && i < this.maxCount, 'instance index out of range');
|
|
4250
4517
|
ASSERT(isColor(color), 'color must be a Color');
|
|
4251
|
-
const data = this.
|
|
4252
|
-
data[k
|
|
4253
|
-
this.
|
|
4518
|
+
const data = this.colorData, k = i * RENDER3D_COLOR_FLOATS;
|
|
4519
|
+
data[k] = color.r; data[k+1] = color.g; data[k+2] = color.b; data[k+3] = color.a;
|
|
4520
|
+
this.markColorDirty(i);
|
|
4254
4521
|
}
|
|
4255
4522
|
|
|
4256
|
-
/** Note that an instance changed, so it uploads before the next draw and the bounds
|
|
4257
|
-
*
|
|
4523
|
+
/** Note that an instance's matrix changed, so it uploads before the next draw and the bounds take it in when
|
|
4524
|
+
* they are read; setTransformAt and setMatrixAt call this, and so must an edit made straight to matrixData
|
|
4258
4525
|
* @param {number} i */
|
|
4259
4526
|
markDirty(i)
|
|
4260
4527
|
{
|
|
4261
|
-
this.dirtyStart
|
|
4262
|
-
this.dirtyEnd
|
|
4528
|
+
if (i < this.dirtyStart) this.dirtyStart = i;
|
|
4529
|
+
if (i >= this.dirtyEnd) this.dirtyEnd = i + 1;
|
|
4530
|
+
if (i < this.boundsStart) this.boundsStart = i;
|
|
4531
|
+
if (i >= this.boundsEnd) this.boundsEnd = i + 1;
|
|
4532
|
+
}
|
|
4533
|
+
|
|
4534
|
+
/** Note that a run of instances' matrices changed, for code that writes matrixData directly, the lowest level
|
|
4535
|
+
* way to move many: they upload before the next draw and the bounds take them in when they are read
|
|
4536
|
+
* @param {number} start - First instance
|
|
4537
|
+
* @param {number} end - One past the last */
|
|
4538
|
+
markDirtyRange(start, end)
|
|
4539
|
+
{
|
|
4540
|
+
ASSERT(start >= 0 && end <= this.maxCount && start <= end, 'instance range out of range');
|
|
4541
|
+
if (start < this.dirtyStart) this.dirtyStart = start;
|
|
4542
|
+
if (end > this.dirtyEnd) this.dirtyEnd = end;
|
|
4543
|
+
if (start < this.boundsStart) this.boundsStart = start;
|
|
4544
|
+
if (end > this.boundsEnd) this.boundsEnd = end;
|
|
4545
|
+
}
|
|
4263
4546
|
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
this.
|
|
4547
|
+
/** Note that an instance's color changed, so it uploads before the next draw; setColorAt calls this, and so must
|
|
4548
|
+
* an edit made straight to colorData
|
|
4549
|
+
* @param {number} i */
|
|
4550
|
+
markColorDirty(i)
|
|
4551
|
+
{
|
|
4552
|
+
if (i < this.colorDirtyStart) this.colorDirtyStart = i;
|
|
4553
|
+
if (i >= this.colorDirtyEnd) this.colorDirtyEnd = i + 1;
|
|
4554
|
+
}
|
|
4555
|
+
|
|
4556
|
+
/** Radius of the sphere around the origin that holds every instance set so far, for culling: the farthest
|
|
4557
|
+
* instance, and the mesh's size at the largest scale; it only grows, and takes in the instances set since it
|
|
4558
|
+
* was last read, so with frustum culling off it is never worked out. Set it to fix the sphere, which a big set
|
|
4559
|
+
* that is culled can use to skip that work, and set it to undefined to go back to the bounds
|
|
4560
|
+
* @return {number} */
|
|
4561
|
+
get radius()
|
|
4562
|
+
{
|
|
4563
|
+
if (this.fixedRadius !== undefined) return this.fixedRadius;
|
|
4564
|
+
if (this.boundsEnd > this.boundsStart)
|
|
4565
|
+
{
|
|
4566
|
+
// the bounds grow to hold where each is now and how big, read back from its matrix, squared
|
|
4567
|
+
const d = this.matrixData;
|
|
4568
|
+
let reachSquared = this.reachSquared, scaleSquared = this.scaleSquared;
|
|
4569
|
+
for (let i = this.boundsStart; i < this.boundsEnd; ++i)
|
|
4570
|
+
{
|
|
4571
|
+
const k = i * RENDER3D_MATRIX_FLOATS, x = d[k+12], y = d[k+13], z = d[k+14];
|
|
4572
|
+
const reach = x*x + y*y + z*z, scale = render3DMaxStretchSquared(d, k);
|
|
4573
|
+
if (reach > reachSquared) reachSquared = reach;
|
|
4574
|
+
if (scale > scaleSquared) scaleSquared = scale;
|
|
4575
|
+
}
|
|
4576
|
+
this.reachSquared = reachSquared, this.scaleSquared = scaleSquared;
|
|
4577
|
+
this.boundsStart = Infinity, this.boundsEnd = 0;
|
|
4578
|
+
}
|
|
4579
|
+
const mesh = this.mesh, meshRadius = mesh.radius || mesh.computeRadius(); // the mesh measured once
|
|
4580
|
+
return this.reachSquared ** .5 + meshRadius * this.scaleSquared ** .5;
|
|
4270
4581
|
}
|
|
4582
|
+
set radius(radius) { this.fixedRadius = radius; }
|
|
4271
4583
|
|
|
4272
4584
|
/** Draws every instance as one call, uploading the ones that changed first
|
|
4273
4585
|
* @return {void} */
|
|
@@ -4281,41 +4593,46 @@ class InstancedMesh3D extends EngineObject3D
|
|
|
4281
4593
|
if (r.shadowPass && !r.lighting) return; // unlit things cast no shadow
|
|
4282
4594
|
render3DMeshUpload(mesh);
|
|
4283
4595
|
if (!mesh.bufferCount) return;
|
|
4284
|
-
this.radius = this.reach + mesh.radius * this.maxScale; // the mesh may have grown since
|
|
4285
4596
|
if (r.frustumCulling && !render3DSphereVisible(0, 0, 0, this.radius)) return;
|
|
4286
4597
|
|
|
4287
4598
|
// the uv rect is the object's tile for every instance, rewritten when the tile changes
|
|
4288
4599
|
if (this.uvTileInfo !== this.tileInfo)
|
|
4289
4600
|
{
|
|
4290
|
-
const uv = render3DGetTileUVs(this.tileInfo), data = this.
|
|
4291
|
-
for (let k =
|
|
4601
|
+
const uv = render3DGetTileUVs(this.tileInfo), data = this.colorData;
|
|
4602
|
+
for (let k = 4; k < data.length; k += RENDER3D_COLOR_FLOATS)
|
|
4292
4603
|
data[k] = uv.x, data[k+1] = uv.y, data[k+2] = uv.w, data[k+3] = uv.h;
|
|
4293
4604
|
this.uvTileInfo = this.tileInfo;
|
|
4294
|
-
this.
|
|
4605
|
+
this.colorDirtyStart = 0, this.colorDirtyEnd = this.maxCount;
|
|
4295
4606
|
}
|
|
4296
4607
|
|
|
4297
|
-
// the GPU
|
|
4608
|
+
// the GPU copies: all of each under a fresh context, otherwise just the changed ranges
|
|
4298
4609
|
if (!this.buffer || this.bufferGeneration !== r.contextGeneration)
|
|
4299
4610
|
{
|
|
4300
|
-
this.buffer = gl.createBuffer();
|
|
4301
4611
|
this.bufferGeneration = r.contextGeneration;
|
|
4302
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
|
4303
|
-
gl.bufferData(gl.ARRAY_BUFFER, this.
|
|
4612
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer = gl.createBuffer());
|
|
4613
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.matrixData, gl.DYNAMIC_DRAW);
|
|
4614
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer = gl.createBuffer());
|
|
4615
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.colorData, gl.DYNAMIC_DRAW);
|
|
4304
4616
|
}
|
|
4305
|
-
else
|
|
4617
|
+
else
|
|
4306
4618
|
{
|
|
4307
|
-
const
|
|
4308
|
-
|
|
4309
|
-
|
|
4619
|
+
const upload = (buffer, data, floats, start, end)=>
|
|
4620
|
+
{
|
|
4621
|
+
if (end <= start) return;
|
|
4622
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
4623
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, start * floats * 4, data, start * floats, (end - start) * floats);
|
|
4624
|
+
};
|
|
4625
|
+
upload(this.buffer, this.matrixData, RENDER3D_MATRIX_FLOATS, this.dirtyStart, this.dirtyEnd);
|
|
4626
|
+
upload(this.colorBuffer, this.colorData, RENDER3D_COLOR_FLOATS, this.colorDirtyStart, this.colorDirtyEnd);
|
|
4310
4627
|
}
|
|
4311
|
-
this.dirtyStart = Infinity, this.dirtyEnd = 0;
|
|
4628
|
+
this.dirtyStart = this.colorDirtyStart = Infinity, this.dirtyEnd = this.colorDirtyEnd = 0;
|
|
4312
4629
|
|
|
4313
4630
|
// one draw under the object's state, the mesh setting the culling as drawMesh does
|
|
4314
4631
|
r.flush();
|
|
4315
4632
|
render3DFlushBeforeOverlay(); // as in drawMesh, what was drawn before goes under it
|
|
4316
4633
|
const cullBackFaces = r.cullBackFaces, tileInfo = this.tileInfo;
|
|
4317
4634
|
r.cullBackFaces = !mesh.doubleSided;
|
|
4318
|
-
render3DDrawInstanced(mesh, this.buffer, this.count, render3DTextureOf(tileInfo), r);
|
|
4635
|
+
render3DDrawInstanced(mesh, this.buffer, this.count, render3DTextureOf(tileInfo), r, this.colorBuffer);
|
|
4319
4636
|
r.cullBackFaces = cullBackFaces;
|
|
4320
4637
|
}
|
|
4321
4638
|
|
|
@@ -4324,8 +4641,8 @@ class InstancedMesh3D extends EngineObject3D
|
|
|
4324
4641
|
destroy(immediate)
|
|
4325
4642
|
{
|
|
4326
4643
|
if (this.buffer && this.bufferGeneration === render3D?.contextGeneration)
|
|
4327
|
-
glContext?.deleteBuffer(this.buffer);
|
|
4328
|
-
this.buffer = undefined;
|
|
4644
|
+
glContext?.deleteBuffer(this.buffer), glContext?.deleteBuffer(this.colorBuffer);
|
|
4645
|
+
this.buffer = this.colorBuffer = undefined;
|
|
4329
4646
|
super.destroy(immediate);
|
|
4330
4647
|
}
|
|
4331
4648
|
}
|
|
@@ -4334,7 +4651,10 @@ class InstancedMesh3D extends EngineObject3D
|
|
|
4334
4651
|
/**
|
|
4335
4652
|
* Light3D - A light that is an EngineObject3D, so it can move, follow a parent or be destroyed like anything else
|
|
4336
4653
|
* - A point light: it lights what is near it and fades out by its radius, DirectionalLight3D shines from far away
|
|
4337
|
-
* -
|
|
4654
|
+
* - A coneAngle makes it a spotlight: it shines along its own forward, turned by rotation3D or by what it is
|
|
4655
|
+
* attached to, inside the cone, fading over the part of it coneSoftness says
|
|
4656
|
+
* - The sun, render3D.sunDirection, casts the shadows; a spotlight can cast them in its place, see
|
|
4657
|
+
* render3D.shadowLight; the other lights light and make highlights without one
|
|
4338
4658
|
* - Only the 8 lights nearest the camera are used each frame
|
|
4339
4659
|
* - radius is where the light fades out, and it fades fast, so a small radius wants a higher intensity
|
|
4340
4660
|
* - intensity multiplies the color, above 1 for a light brighter than white
|
|
@@ -4366,6 +4686,12 @@ class Light3D extends EngineObject3D
|
|
|
4366
4686
|
/** @property {boolean} - Shine from far away, from its position toward the origin, instead of out from its
|
|
4367
4687
|
* position with a falloff; DirectionalLight3D sets it */
|
|
4368
4688
|
this.directional = false;
|
|
4689
|
+
/** @property {number} - Makes it a spotlight: the angle in radians from its forward out to the edge of its
|
|
4690
|
+
* cone, so the beam is twice this across; 0 for a light that shines every way */
|
|
4691
|
+
this.coneAngle = 0;
|
|
4692
|
+
/** @property {number} - How much of the cone is its fading edge: 0 a hard edge, .2 by default, the outer
|
|
4693
|
+
* fifth, 1 fading all the way from the middle of the beam */
|
|
4694
|
+
this.coneSoftness = .2;
|
|
4369
4695
|
/** @property {number} - Size of a soft hazy glow drawn over the light, like a lamp at night, 0 for none; it
|
|
4370
4696
|
* is added onto what is behind it, and what is in front of the light hides it */
|
|
4371
4697
|
this.glow = 0;
|
|
@@ -4384,37 +4710,10 @@ class Light3D extends EngineObject3D
|
|
|
4384
4710
|
const toCamera = r.camera.pos.subtract(pos), distance = toCamera.length();
|
|
4385
4711
|
const at = distance ? pos.add(toCamera.scale(min(this.glow, distance) / 2 / distance)) : pos;
|
|
4386
4712
|
const color = rgb(c.r, c.g, c.b, c.a * min(this.intensity, 1));
|
|
4387
|
-
r.drawBillboard(at, vec2(this.glow),
|
|
4713
|
+
r.drawBillboard(at, vec2(this.glow), engineGlowTexture(this.glowFalloff), color);
|
|
4388
4714
|
}
|
|
4389
4715
|
}
|
|
4390
4716
|
|
|
4391
|
-
// how strong a light's glow is at a distance from its middle, 0 there to 1 at the edge: a bell, full in the middle
|
|
4392
|
-
// and nothing at the edge, fading faster the higher the falloff
|
|
4393
|
-
function render3DGlowAlpha(r, falloff)
|
|
4394
|
-
{
|
|
4395
|
-
const k = 3.5 * falloff, edge = Math.exp(-k);
|
|
4396
|
-
return (Math.exp(-k * r * r) - edge) / (1 - edge);
|
|
4397
|
-
}
|
|
4398
|
-
|
|
4399
|
-
// the soft round glow of the lights, one texture for each falloff, rounded to a tenth so a changing falloff makes
|
|
4400
|
-
// only a few, each made once from a canvas; undefined headless or without a canvas
|
|
4401
|
-
const render3DGlowTextures = new Map;
|
|
4402
|
-
function render3DGlow(falloff=1)
|
|
4403
|
-
{
|
|
4404
|
-
ASSERT(isNumber(falloff) && falloff > 0, 'glowFalloff must be a number above 0');
|
|
4405
|
-
const key = max(round(falloff * 10), 1) / 10;
|
|
4406
|
-
let texture = render3DGlowTextures.get(key);
|
|
4407
|
-
if (texture || !glContext || typeof OffscreenCanvas == 'undefined') return texture;
|
|
4408
|
-
const size = 64, context = createCanvasContext(size), steps = 16;
|
|
4409
|
-
const gradient = context.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
|
|
4410
|
-
for (let i = 0; i <= steps; ++i)
|
|
4411
|
-
gradient.addColorStop(i / steps, 'rgba(255,255,255,' + render3DGlowAlpha(i / steps, key).toFixed(4) + ')');
|
|
4412
|
-
context.fillStyle = gradient;
|
|
4413
|
-
context.fillRect(0, 0, size, size);
|
|
4414
|
-
render3DGlowTextures.set(key, texture = new TextureInfo(context.canvas));
|
|
4415
|
-
return texture;
|
|
4416
|
-
}
|
|
4417
|
-
|
|
4418
4717
|
///////////////////////////////////////////////////////////////////////////////
|
|
4419
4718
|
/**
|
|
4420
4719
|
* DirectionalLight3D - A Light3D that shines from far away with no falloff, like sunlight
|