littlejsengine 1.21.0 → 1.23.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/README.md +1 -1
- package/dist/littlejs.d.ts +1258 -141
- package/dist/littlejs.esm.js +5023 -429
- package/dist/littlejs.esm.min.js +22 -9
- package/dist/littlejs.js +4977 -430
- package/dist/littlejs.min.js +21 -8
- package/dist/littlejs.release.js +2759 -313
- package/package.json +3 -2
- package/plugins/gltf.js +7 -1
- package/plugins/lightSystem.js +50 -5
- package/plugins/math3d.js +42 -20
- package/plugins/parallax.js +174 -0
- package/plugins/particleEffects.js +760 -0
- package/plugins/pluginExport.js +40 -0
- package/plugins/postProcess.js +206 -28
- package/plugins/render3d.js +495 -188
- package/plugins/render3dEditor.js +1754 -80
- package/plugins/render3dExtras.js +294 -21
- package/plugins/render3dLevel.js +463 -6
- package/plugins/sceneSystem.js +3 -2
- package/src/engine.js +1 -1
- package/src/engineAudio.js +25 -8
- package/src/engineBuild.mjs +2 -0
- package/src/engineDraw.js +59 -4
- package/src/engineEditor.js +475 -37
- package/src/engineExport.js +7 -0
- package/src/engineInput.js +48 -6
- package/src/engineParticles.js +80 -11
- package/src/engineRelease.js +13 -2
- package/src/engineSettings.js +14 -0
- 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,41 @@ 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
|
+
// the widest cone a shadow map can look down, to each side; the light that casts the shadows lights no wider, or
|
|
437
|
+
// what is past the map's edge would be lit with nothing to shade it
|
|
438
|
+
const RENDER3D_SHADOW_CONE_MAX = 1.35;
|
|
439
|
+
|
|
440
|
+
// a light's cone as the four numbers the shader gets: the way it shines, scaled so that its dot with the way to
|
|
441
|
+
// 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
|
|
442
|
+
// gets numbers that make it 1 every way
|
|
443
|
+
function render3DLightCone(light)
|
|
444
|
+
{
|
|
445
|
+
const widest = light === render3DShadowCaster() ? RENDER3D_SHADOW_CONE_MAX : PI;
|
|
446
|
+
const angle = light.directional ? 0 : min(light.coneAngle, widest);
|
|
447
|
+
if (!(angle > 0)) return [0, 0, 0, -1];
|
|
448
|
+
const outer = cos(angle), inner = cos(angle * (1 - clamp(light.coneSoftness)));
|
|
449
|
+
const k = 1 / max(inner - outer, 1e-4), forward = light.getForward3D();
|
|
450
|
+
return [forward.x * k, forward.y * k, forward.z * k, outer * k];
|
|
451
|
+
}
|
|
452
|
+
|
|
377
453
|
// unit circle directions for a number of sides, [cos, sin, cos, sin, ...] including the closing point, cached
|
|
378
454
|
const render3DCircleCache = new Map;
|
|
379
455
|
function render3DCircle(sides)
|
|
@@ -425,7 +501,15 @@ class Render3DPlugin
|
|
|
425
501
|
{
|
|
426
502
|
ASSERT(!render3D, 'Render3D plugin already initialized');
|
|
427
503
|
render3D = this;
|
|
428
|
-
|
|
504
|
+
// the draw state, read at each draw through the accessors below the constructor
|
|
505
|
+
/** @type {Render3DDrawState} */
|
|
506
|
+
this.drawState = {blend: false, additive: false, depthTest: true, depthWrite: true, cullBackFaces: false,
|
|
507
|
+
mirrored: false, lighting: true, emissive: 0, receiveShadow: true, specular: 0, pixelated: false,
|
|
508
|
+
shader: undefined, normalMap: undefined, normalScale: 1, shininess: 16, reflectivity: 0,
|
|
509
|
+
emissiveMap: undefined, emissiveMapColor: WHITE, emissiveR: 1, emissiveG: 1, emissiveB: 1};
|
|
510
|
+
/** @property {number} - Goes up when a draw state field changes, so a batch sees at a glance that none did */
|
|
511
|
+
this.stateVersion = 0;
|
|
512
|
+
ASSERT(Object.keys(render3DCaptureBatchState()).join() === [...RENDER3D_STATE_FIELDS, 'version'].join(),
|
|
429
513
|
'the batch state functions must list RENDER3D_STATE_FIELDS');
|
|
430
514
|
|
|
431
515
|
/** @property {Camera3D} - The camera */
|
|
@@ -471,8 +555,16 @@ class Render3DPlugin
|
|
|
471
555
|
// shadows
|
|
472
556
|
/** @property {boolean} - Cast real shadows from the sun, off by default and free when off */
|
|
473
557
|
this.shadows = false;
|
|
558
|
+
/** @property {number} - How far a sound played with playSound is heard when it was made with no range of
|
|
559
|
+
* its own, in world units; further than the 2D soundDefaultRange, a 3D world is bigger */
|
|
560
|
+
this.soundDefaultRange = 100;
|
|
474
561
|
/** @property {number} - Size of the shadow map in pixels, bigger is sharper and slower */
|
|
475
562
|
this.shadowMapSize = 1024;
|
|
563
|
+
/** @property {Light3D|undefined} - A spotlight, a Light3D with a coneAngle, to cast the shadows in place
|
|
564
|
+
* of the sun, a flashlight in the dark: the shadow map looks down its cone, as far as its radius, and the
|
|
565
|
+
* sun still lights the scene but casts none; undefined for the sun, as is a light that is off or has no cone
|
|
566
|
+
* @type {Light3D|undefined} */
|
|
567
|
+
this.shadowLight = undefined;
|
|
476
568
|
/** @property {number} - World size the shadow map covers around shadowCenter, smaller is sharper; it is a
|
|
477
569
|
* square facing the light, so it turns as the light does, and about 1.5 times an area's width covers it from
|
|
478
570
|
* any angle */
|
|
@@ -485,50 +577,6 @@ class Render3DPlugin
|
|
|
485
577
|
/** @property {number} - How much to blur the shadow edges */
|
|
486
578
|
this.shadowSoftness = 1;
|
|
487
579
|
|
|
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
580
|
// the pass
|
|
533
581
|
/** @property {Function|undefined} - Draw solid world here, it runs again for shadows so only draw in it
|
|
534
582
|
* @type {Function|undefined} */
|
|
@@ -551,9 +599,6 @@ class Render3DPlugin
|
|
|
551
599
|
/** @property {boolean} - Sample textures through mipmaps so they do not shimmer in the distance, false uses
|
|
552
600
|
* each texture's own filtering like 2D */
|
|
553
601
|
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
602
|
/** @property {number} - Anisotropic filtering for textures seen at an angle, 1 to 16, 1 is off; needs mipmaps */
|
|
558
603
|
this.anisotropy = 4;
|
|
559
604
|
|
|
@@ -597,7 +642,6 @@ class Render3DPlugin
|
|
|
597
642
|
this.cameraBack = vec3(0, 0, 1); // its opposite, the normal of camera facing draws
|
|
598
643
|
|
|
599
644
|
// internal state
|
|
600
|
-
this.blend = false; // blending on, set by the stages
|
|
601
645
|
/** @type {Array<Array<number>>} */
|
|
602
646
|
this.frustumPlanes = []; // the view as six inward planes [x, y, z, w]
|
|
603
647
|
/** @type {Array<Array<number>>} */
|
|
@@ -637,6 +681,9 @@ class Render3DPlugin
|
|
|
637
681
|
this.passIsDefault = true; // the running pass is the default layer, the only one shadowed
|
|
638
682
|
this.lightPositions = new Float32Array(RENDER3D_MAX_LIGHTS * 4); // Light3D uniforms, filled each pass
|
|
639
683
|
this.lightColors = new Float32Array(RENDER3D_MAX_LIGHTS * 4);
|
|
684
|
+
this.lightCones = new Float32Array(RENDER3D_MAX_LIGHTS * 4); // each light's cone, see render3DLightCone
|
|
685
|
+
this.shadowLightIndex = -1; // which of the lights sent casts the shadows, -1 for the sun
|
|
686
|
+
this.shadowDepthBias = 0; // the bias as the shadow lookup takes it, set with the shadow matrix
|
|
640
687
|
|
|
641
688
|
// the stream of immediate mode draws
|
|
642
689
|
/** @type {WebGLBuffer|undefined} */
|
|
@@ -664,6 +711,116 @@ class Render3DPlugin
|
|
|
664
711
|
engineAddPlugin(undefined, render3DRender, render3DContextLost, render3DContextRestored, render3DPreRender);
|
|
665
712
|
}
|
|
666
713
|
|
|
714
|
+
///////////////////////////////////////////////////////////////////////////
|
|
715
|
+
// Draw state: each field an accessor over drawState, whose setter moves stateVersion only on a real change
|
|
716
|
+
|
|
717
|
+
/** Apply lighting, when false draws plain vertex color times texture and casts no shadow;
|
|
718
|
+
* off for billboards, lines, ribbons and soft discs, an object sets emissive instead
|
|
719
|
+
* @return {boolean} */
|
|
720
|
+
get lighting() { return this.drawState.lighting; }
|
|
721
|
+
set lighting(v) { const d = this.drawState; d.lighting === v || (d.lighting = v, ++this.stateVersion); }
|
|
722
|
+
|
|
723
|
+
/** How much a surface lights itself, set per object by its emissive
|
|
724
|
+
* @return {number} */
|
|
725
|
+
get emissive() { return this.drawState.emissive; }
|
|
726
|
+
set emissive(v) { const d = this.drawState; d.emissive === v || (d.emissive = v, ++this.stateVersion); }
|
|
727
|
+
|
|
728
|
+
/** Additive blending instead of alpha, in the transparent stage
|
|
729
|
+
* @return {boolean} */
|
|
730
|
+
get additive() { return this.drawState.additive; }
|
|
731
|
+
set additive(v) { const d = this.drawState; d.additive === v || (d.additive = v, ++this.stateVersion); }
|
|
732
|
+
|
|
733
|
+
/** Test against the depth buffer, reset to true before each object and callback; a draw
|
|
734
|
+
* with it off goes over what was drawn before it and under what is drawn after, by render order, which
|
|
735
|
+
* ends the batch of meshes before it, so one per object costs a draw per object
|
|
736
|
+
* @return {boolean} */
|
|
737
|
+
get depthTest() { return this.drawState.depthTest; }
|
|
738
|
+
set depthTest(v) { const d = this.drawState; d.depthTest === v || (d.depthTest = v, ++this.stateVersion); }
|
|
739
|
+
|
|
740
|
+
/** Write to the depth buffer, owned by the stages: on for opaque, off for transparent
|
|
741
|
+
* @return {boolean} */
|
|
742
|
+
get depthWrite() { return this.drawState.depthWrite; }
|
|
743
|
+
set depthWrite(v) { const d = this.drawState; d.depthWrite === v || (d.depthWrite = v, ++this.stateVersion); }
|
|
744
|
+
|
|
745
|
+
/** Skip back faces, set by drawMesh from each mesh: off for strips so they show from
|
|
746
|
+
* both sides
|
|
747
|
+
* @return {boolean} */
|
|
748
|
+
get cullBackFaces() { return this.drawState.cullBackFaces; }
|
|
749
|
+
set cullBackFaces(v) { const d = this.drawState; d.cullBackFaces === v || (d.cullBackFaces = v, ++this.stateVersion); }
|
|
750
|
+
|
|
751
|
+
/** The transform mirrors the draw, so the other winding is the front, set by drawMesh
|
|
752
|
+
* from each transform
|
|
753
|
+
* @return {boolean} */
|
|
754
|
+
get mirrored() { return this.drawState.mirrored; }
|
|
755
|
+
set mirrored(v) { const d = this.drawState; d.mirrored === v || (d.mirrored = v, ++this.stateVersion); }
|
|
756
|
+
|
|
757
|
+
/** Strength of the highlight where the sun and the Light3D objects reflect, 0 is none and
|
|
758
|
+
* 1 adds a light's full color at its brightest; shininess sets its size
|
|
759
|
+
* @return {number} */
|
|
760
|
+
get specular() { return this.drawState.specular; }
|
|
761
|
+
set specular(v) { const d = this.drawState; d.specular === v || (d.specular = v, ++this.stateVersion); }
|
|
762
|
+
|
|
763
|
+
/** The highlight's exponent, how small and sharp it is: 4 is broad like rubber, 100 sharp
|
|
764
|
+
* like polished metal; set from each object's shininess
|
|
765
|
+
* @return {number} */
|
|
766
|
+
get shininess() { return this.drawState.shininess; }
|
|
767
|
+
set shininess(v) { const d = this.drawState; d.shininess === v || (d.shininess = v, ++this.stateVersion); }
|
|
768
|
+
|
|
769
|
+
/** Normal map for the next draws, set from each object's normalMap
|
|
770
|
+
* @return {TextureInfo|undefined} */
|
|
771
|
+
get normalMap() { return this.drawState.normalMap; }
|
|
772
|
+
set normalMap(v) { const d = this.drawState; d.normalMap === v || (d.normalMap = v, ++this.stateVersion); }
|
|
773
|
+
|
|
774
|
+
/** How strongly the normal map bends the surface, set from each object's normalScale
|
|
775
|
+
* @return {number} */
|
|
776
|
+
get normalScale() { return this.drawState.normalScale; }
|
|
777
|
+
set normalScale(v) { const d = this.drawState; d.normalScale === v || (d.normalScale = v, ++this.stateVersion); }
|
|
778
|
+
|
|
779
|
+
/** How much the surface reflects the sky, 0 to 1, set from each object's reflectivity
|
|
780
|
+
* @return {number} */
|
|
781
|
+
get reflectivity() { return this.drawState.reflectivity; }
|
|
782
|
+
set reflectivity(v) { const d = this.drawState; d.reflectivity === v || (d.reflectivity = v, ++this.stateVersion); }
|
|
783
|
+
|
|
784
|
+
/** Emissive map for the next draws, set from each object's emissiveMap
|
|
785
|
+
* @return {TextureInfo|undefined} */
|
|
786
|
+
get emissiveMap() { return this.drawState.emissiveMap; }
|
|
787
|
+
set emissiveMap(v) { const d = this.drawState; d.emissiveMap === v || (d.emissiveMap = v, ++this.stateVersion); }
|
|
788
|
+
|
|
789
|
+
/** Multiplies the emissive map, set from each object's emissiveMapColor; compared by its rgb, so a Color
|
|
790
|
+
* changed in place is seen when it is set again, and undefined is white
|
|
791
|
+
* @return {Color} */
|
|
792
|
+
get emissiveMapColor() { return this.drawState.emissiveMapColor; }
|
|
793
|
+
set emissiveMapColor(v)
|
|
794
|
+
{
|
|
795
|
+
const d = this.drawState, c = v || WHITE;
|
|
796
|
+
d.emissiveMapColor = v;
|
|
797
|
+
if (c.r !== d.emissiveR || c.g !== d.emissiveG || c.b !== d.emissiveB)
|
|
798
|
+
d.emissiveR = c.r, d.emissiveG = c.g, d.emissiveB = c.b, ++this.stateVersion;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/** Custom Shader for the next draws, set from each object's shader; undefined draws
|
|
802
|
+
* with the plugin's own
|
|
803
|
+
* @return {Shader|undefined} */
|
|
804
|
+
get shader() { return this.drawState.shader; }
|
|
805
|
+
set shader(v) { const d = this.drawState; d.shader === v || (d.shader = v, ++this.stateVersion); }
|
|
806
|
+
|
|
807
|
+
/** Darken by the shadow map when shadows are on, turn it off for things that should
|
|
808
|
+
* stay lit inside a shadow
|
|
809
|
+
* @return {boolean} */
|
|
810
|
+
get receiveShadow() { return this.drawState.receiveShadow; }
|
|
811
|
+
set receiveShadow(v) { const d = this.drawState; d.receiveShadow === v || (d.receiveShadow = v, ++this.stateVersion); }
|
|
812
|
+
|
|
813
|
+
/** Keep texture pixels hard edged, no mipmaps and no blending between them, set per
|
|
814
|
+
* object by pixelated
|
|
815
|
+
* @return {boolean} */
|
|
816
|
+
get pixelated() { return this.drawState.pixelated; }
|
|
817
|
+
set pixelated(v) { const d = this.drawState; d.pixelated === v || (d.pixelated = v, ++this.stateVersion); }
|
|
818
|
+
|
|
819
|
+
/** Blending on, set by the stages
|
|
820
|
+
* @return {boolean} */
|
|
821
|
+
get blend() { return this.drawState.blend; }
|
|
822
|
+
set blend(v) { const d = this.drawState; d.blend === v || (d.blend = v, ++this.stateVersion); }
|
|
823
|
+
|
|
667
824
|
///////////////////////////////////////////////////////////////////////////
|
|
668
825
|
// Matrices and picking
|
|
669
826
|
|
|
@@ -773,7 +930,8 @@ class Render3DPlugin
|
|
|
773
930
|
}
|
|
774
931
|
|
|
775
932
|
/** 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
|
|
933
|
+
* with a 2D position; a sound made with no range of its own is heard to soundDefaultRange, 100, and one given
|
|
934
|
+
* a range keeps it
|
|
777
935
|
* @param {Sound} sound
|
|
778
936
|
* @param {Vector3} pos3D
|
|
779
937
|
* @param {number} [volume]
|
|
@@ -787,7 +945,9 @@ class Render3DPlugin
|
|
|
787
945
|
// the 3D range fade and pan, Sound.play with no position does the rest
|
|
788
946
|
ASSERT(sound instanceof Sound, 'sound must be a Sound');
|
|
789
947
|
ASSERT(isVector3(pos3D), 'pos3D must be a vec3');
|
|
790
|
-
|
|
948
|
+
// a sound with no range of its own is heard further in 3D than in 2D
|
|
949
|
+
const offset = pos3D.subtract(this.camera.pos);
|
|
950
|
+
const range = sound.rangeIsDefault ? this.soundDefaultRange : sound.range;
|
|
791
951
|
if (range)
|
|
792
952
|
{
|
|
793
953
|
const distance = offset.length();
|
|
@@ -851,15 +1011,15 @@ class Render3DPlugin
|
|
|
851
1011
|
if (this.frustumCulling && !render3DSphereVisible(m[12], m[13], m[14], mesh.radius * render3DMaxStretch(m)))
|
|
852
1012
|
return;
|
|
853
1013
|
// 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
|
|
1014
|
+
// determinant, turns the winding around so the other one is its front; the stage draws a batch at its
|
|
1015
|
+
// end, which keys on those itself
|
|
859
1016
|
if (!this.blend && this.depthTest && (mesh.instanced ?? this.instancing))
|
|
860
1017
|
render3DInstance(mesh, matrix, tileInfo, color);
|
|
861
1018
|
else
|
|
862
1019
|
{
|
|
1020
|
+
const cullBackFaces = this.cullBackFaces, mirrored = this.mirrored;
|
|
1021
|
+
this.cullBackFaces = !mesh.doubleSided;
|
|
1022
|
+
this.mirrored = render3DMirrors(matrix);
|
|
863
1023
|
this.flush();
|
|
864
1024
|
render3DFlushBeforeOverlay();
|
|
865
1025
|
render3DSetDrawUniforms(matrix, tileInfo, color);
|
|
@@ -867,8 +1027,8 @@ class Render3DPlugin
|
|
|
867
1027
|
glContext.drawElements(glContext.TRIANGLES, mesh.bufferCount, mesh.indexType, 0);
|
|
868
1028
|
++drawCount;
|
|
869
1029
|
primitiveCount += mesh.bufferCount / 3;
|
|
1030
|
+
this.cullBackFaces = cullBackFaces, this.mirrored = mirrored;
|
|
870
1031
|
}
|
|
871
|
-
this.cullBackFaces = cullBackFaces, this.mirrored = mirrored;
|
|
872
1032
|
}
|
|
873
1033
|
|
|
874
1034
|
/** Draw a triangle strip, batched into the stream with the current draw state
|
|
@@ -1064,6 +1224,24 @@ class Render3DPlugin
|
|
|
1064
1224
|
/** Rebuild the light's view projection around the shadow center, called automatically each frame shadows are on */
|
|
1065
1225
|
updateShadowMatrix()
|
|
1066
1226
|
{
|
|
1227
|
+
const caster = render3DShadowCaster();
|
|
1228
|
+
if (caster)
|
|
1229
|
+
{
|
|
1230
|
+
// a spotlight's map looks down its cone with perspective, from just in front of it out to its radius
|
|
1231
|
+
const pos = caster.getWorldPos3D(), forward = caster.getForward3D();
|
|
1232
|
+
const up = abs(forward.y) > .99 ? vec3(0, 0, 1) : vec3(0, 1, 0);
|
|
1233
|
+
// the near plane stays in front of the far one however small the light
|
|
1234
|
+
const far = caster.radius, near = min(max(far / 500, .02), far / 2);
|
|
1235
|
+
const fov = min(caster.coneAngle, RENDER3D_SHADOW_CONE_MAX) * 2 + .1;
|
|
1236
|
+
this.shadowMatrix = Matrix4.perspective(fov, 1, near, far).multiply(
|
|
1237
|
+
Matrix4.lookAt(pos, pos.add(forward), up).invert());
|
|
1238
|
+
this.shadowPlanes = render3DFrustumPlanes(this.shadowMatrix);
|
|
1239
|
+
// depth is not even with perspective: the lookup divides this by the distance squared, which makes
|
|
1240
|
+
// the bias the same distance in the world near the light and far from it
|
|
1241
|
+
this.shadowDepthBias = this.shadowBias * far * far * near / (far - near);
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
this.shadowDepthBias = this.shadowBias; // the sun's map is flat, its depth even
|
|
1067
1245
|
ASSERT(this.shadowRange > 0, 'shadowRange must be positive');
|
|
1068
1246
|
const range = this.shadowRange > 0 ? this.shadowRange : 1, half = range / 2;
|
|
1069
1247
|
const toSun = this.sunDirection.normalize();
|
|
@@ -1493,6 +1671,7 @@ function render3DFragmentSource(fragmentCode)
|
|
|
1493
1671
|
'precision highp float;' +
|
|
1494
1672
|
'uniform vec4 lightDir,lightColor,ambientFog,ambientGround,fogColor,shadowParams;' +
|
|
1495
1673
|
'uniform vec4 extraLights[' + RENDER3D_MAX_LIGHTS + '],extraLightColors[' + RENDER3D_MAX_LIGHTS + '];' +
|
|
1674
|
+
'uniform vec4 extraLightCones[' + RENDER3D_MAX_LIGHTS + '];' +
|
|
1496
1675
|
'uniform int extraLightCount;' +
|
|
1497
1676
|
'uniform vec3 cameraPos;' +
|
|
1498
1677
|
'uniform vec4 materialParams,emissiveTint,skyTop,skyHorizon,skyBottom;' +
|
|
@@ -1501,12 +1680,14 @@ function render3DFragmentSource(fragmentCode)
|
|
|
1501
1680
|
'uniform highp sampler2DShadow shadowMap;' +
|
|
1502
1681
|
'in vec3 P,N;in vec2 T,L;in vec4 C,S;' +
|
|
1503
1682
|
'out vec4 o;' +
|
|
1504
|
-
// the
|
|
1683
|
+
// the shadow at this fragment, 0 to 1: the light's depth map with a 3x3 blur, outside the map is lit; the
|
|
1684
|
+
// 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
|
|
1685
|
+
// bias shrinks by its square to stay the same distance in the world
|
|
1505
1686
|
'float shadow(){' +
|
|
1506
|
-
'if(shadowParams.x<=0.)return 1.;' +
|
|
1687
|
+
'if(shadowParams.x<=0.||S.w<=0.)return 1.;' +
|
|
1507
1688
|
'vec3 q=S.xyz/S.w*.5+.5;' +
|
|
1508
1689
|
'if(any(greaterThanEqual(abs(q-.5),vec3(.5))))return 1.;' +
|
|
1509
|
-
'q.z-=shadowParams.y;' +
|
|
1690
|
+
'q.z-=shadowParams.y/(S.w*S.w);' +
|
|
1510
1691
|
'float s=0.;' +
|
|
1511
1692
|
'for(int x=-1;x<=1;++x)for(int y=-1;y<=1;++y)' +
|
|
1512
1693
|
's+=texture(shadowMap,vec3(q.xy+vec2(x,y)*shadowParams.z,q.z));' +
|
|
@@ -1534,7 +1715,9 @@ function render3DFragmentSource(fragmentCode)
|
|
|
1534
1715
|
'if(!gl_FrontFacing)n=-n;' + // only a double sided mesh shows a back face, light it on the side that is seen
|
|
1535
1716
|
'if(materialParams.x!=0.)n=normalMapNormal(n);' +
|
|
1536
1717
|
'float nl=dot(n,-lightDir.xyz);' +
|
|
1537
|
-
|
|
1718
|
+
// the shadow is the sun's, or one spotlight's: shadowParams.x is 1 for the sun, 2 and up for that Light3D
|
|
1719
|
+
'float sh=shadow(),s=shadowParams.x<1.5?sh:1.;' +
|
|
1720
|
+
'int si=int(shadowParams.x+.5)-2;' +
|
|
1538
1721
|
// the ambient: one color, or blended from the ground color below to the sky color above by the way the face points
|
|
1539
1722
|
'vec3 l=(ambientGround.a>0.?mix(ambientGround.rgb,ambientFog.rgb,n.y*.5+.5):ambientFog.rgb)+lightColor.rgb*max(nl,0.)*s;' +
|
|
1540
1723
|
// the Light3D objects: a point light falls off with distance, a directional one does not and carries the
|
|
@@ -1550,6 +1733,11 @@ function render3DFragmentSource(fragmentCode)
|
|
|
1550
1733
|
'v/=max(d,1e-6);' +
|
|
1551
1734
|
'float ln=dot(n,v);' +
|
|
1552
1735
|
'vec3 lc=extraLightColors[i].rgb*extraLightColors[i].a*a*a;' +
|
|
1736
|
+
// a spotlight's cone: full where its fade starts, nothing at its edge, smooth between; 1 with no cone
|
|
1737
|
+
'vec4 K=extraLightCones[i];' +
|
|
1738
|
+
'float k=clamp(dot(K.xyz,-v)-K.w,0.,1.);' +
|
|
1739
|
+
'lc*=k*k*(3.-2.*k);' +
|
|
1740
|
+
'if(i==si)lc*=sh;' +
|
|
1553
1741
|
'l+=lc*max(0.,ln);' +
|
|
1554
1742
|
'if(lightColor.a>0.)sp+=lc*pow(max(dot(reflect(-v,n),eye),0.),materialParams.y)*step(0.,ln);' +
|
|
1555
1743
|
'}' +
|
|
@@ -1604,6 +1792,7 @@ function render3DUseProgram(program)
|
|
|
1604
1792
|
{
|
|
1605
1793
|
gl.uniform4fv(render3DUniform('extraLights'), r.lightPositions, 0, r.lightCount * 4);
|
|
1606
1794
|
gl.uniform4fv(render3DUniform('extraLightColors'), r.lightColors, 0, r.lightCount * 4);
|
|
1795
|
+
gl.uniform4fv(render3DUniform('extraLightCones'), r.lightCones, 0, r.lightCount * 4);
|
|
1607
1796
|
}
|
|
1608
1797
|
if (program !== r.program)
|
|
1609
1798
|
{
|
|
@@ -1744,18 +1933,19 @@ function render3DUpdateSamplers()
|
|
|
1744
1933
|
r.samplers.clear();
|
|
1745
1934
|
}
|
|
1746
1935
|
|
|
1747
|
-
// the sampler for a texture's wrap modes, smooth or hard edged, made the first time it is needed
|
|
1748
|
-
|
|
1936
|
+
// the sampler for a texture's wrap modes, smooth or hard edged, made the first time it is needed; pixelated is the
|
|
1937
|
+
// draw's, texturePixelated the texture's own, which a smooth texture in a pixel art game sets false
|
|
1938
|
+
function render3DSampler(wrap, pixelated, texturePixelated=tilesPixelated)
|
|
1749
1939
|
{
|
|
1750
1940
|
const gl = glContext, r = render3D, [wrapS, wrapT] = glWrapModes(wrap);
|
|
1751
|
-
const key = wrapS * 1e5 + wrapT *
|
|
1941
|
+
const key = wrapS * 1e5 + wrapT * 4 + (pixelated ? 1 : 0) + (texturePixelated ? 2 : 0); // the modes are 5 digit numbers
|
|
1752
1942
|
let sampler = r.samplers.get(key);
|
|
1753
1943
|
if (sampler) return sampler;
|
|
1754
1944
|
sampler = gl.createSampler();
|
|
1755
|
-
const sharp = pixelated ||
|
|
1945
|
+
const sharp = pixelated || texturePixelated;
|
|
1756
1946
|
gl.samplerParameteri(sampler, gl.TEXTURE_MAG_FILTER, sharp ? gl.NEAREST : gl.LINEAR);
|
|
1757
1947
|
gl.samplerParameteri(sampler, gl.TEXTURE_MIN_FILTER, pixelated ? gl.NEAREST
|
|
1758
|
-
:
|
|
1948
|
+
: texturePixelated ? gl.NEAREST_MIPMAP_LINEAR : gl.LINEAR_MIPMAP_LINEAR);
|
|
1759
1949
|
gl.samplerParameteri(sampler, gl.TEXTURE_WRAP_S, wrapS);
|
|
1760
1950
|
gl.samplerParameteri(sampler, gl.TEXTURE_WRAP_T, wrapT);
|
|
1761
1951
|
const anisotropy = gl.getExtension('EXT_texture_filter_anisotropic');
|
|
@@ -1783,7 +1973,7 @@ function render3DBindTexture(tileInfo, state=render3D, unit=0)
|
|
|
1783
1973
|
// or anisotropy, and filtering it that way costs every untextured fragment
|
|
1784
1974
|
else
|
|
1785
1975
|
{
|
|
1786
|
-
gl.bindSampler(unit, render3DSampler(textureInfo?.wrap, state.pixelated));
|
|
1976
|
+
gl.bindSampler(unit, render3DSampler(textureInfo?.wrap, state.pixelated, textureInfo?.pixelated ?? tilesPixelated));
|
|
1787
1977
|
glUpdateMipmaps(texture); // drawn into since its mipmaps were made
|
|
1788
1978
|
if (!state.pixelated && !glMipmappedTextures.has(texture)) // a hard edged draw never reads them
|
|
1789
1979
|
{
|
|
@@ -1963,7 +2153,9 @@ function render3DSetDrawUniforms(matrix, tileInfo, tint, uvRect, state=render3D)
|
|
|
1963
2153
|
// 0 blends them away instead, and -1 is additive, which has to fade into fog differently
|
|
1964
2154
|
const blendMode = state.blend ? (state.additive ? -1 : 0) : 1;
|
|
1965
2155
|
const receives = r.shadows && r.passIsDefault && state.receiveShadow ? 1 : 0;
|
|
1966
|
-
|
|
2156
|
+
// 1 the sun's shadow, 2 and up the shadow of that Light3D, the spotlight that casts them
|
|
2157
|
+
const shadowOf = receives && r.shadowLightIndex >= 0 ? 2 + r.shadowLightIndex : receives;
|
|
2158
|
+
render3DUniform4f('shadowParams', shadowOf, r.shadowDepthBias, r.shadowSoftness / r.shadowTextureSize, blendMode);
|
|
1967
2159
|
|
|
1968
2160
|
// a render target's texture holds premultiplied color, the blend writes it that way, so the shader undoes it
|
|
1969
2161
|
const textureInfo = render3DTextureOf(tileInfo);
|
|
@@ -2029,6 +2221,8 @@ function render3DRenderPass(after2D)
|
|
|
2029
2221
|
// gathered before the gl state is taken over, so an assert here leaves nothing to hand back
|
|
2030
2222
|
const lights = render3DCollectLights();
|
|
2031
2223
|
r.lightCount = lights.length;
|
|
2224
|
+
const caster = render3DShadowCaster();
|
|
2225
|
+
r.shadowLightIndex = caster ? lights.indexOf(caster) : -1;
|
|
2032
2226
|
const positions = r.lightPositions, colors = r.lightColors;
|
|
2033
2227
|
lights.forEach((light, i)=>
|
|
2034
2228
|
{
|
|
@@ -2039,6 +2233,7 @@ function render3DRenderPass(after2D)
|
|
|
2039
2233
|
positions[k] = p.x, positions[k+1] = p.y, positions[k+2] = p.z;
|
|
2040
2234
|
positions[k+3] = light.directional ? -1 : max(0, light.radius); // a negative radius marks a direction
|
|
2041
2235
|
colors[k] = c.r, colors[k+1] = c.g, colors[k+2] = c.b, colors[k+3] = c.a * light.intensity;
|
|
2236
|
+
r.lightCones.set(render3DLightCone(light), k);
|
|
2042
2237
|
});
|
|
2043
2238
|
|
|
2044
2239
|
// take over the gl state
|
|
@@ -2110,6 +2305,7 @@ function render3DUpdateShadowMap(size)
|
|
|
2110
2305
|
{
|
|
2111
2306
|
const gl = glContext, r = render3D;
|
|
2112
2307
|
ASSERT(size > 0, 'shadowMapSize must be positive');
|
|
2308
|
+
size = glClampTextureSize(size); // a size the device does not have falls back to the largest it has
|
|
2113
2309
|
if (r.shadowTexture && r.shadowTextureSize === size) return;
|
|
2114
2310
|
r.shadowTexture && gl.deleteTexture(r.shadowTexture);
|
|
2115
2311
|
r.shadowFramebuffer && gl.deleteFramebuffer(r.shadowFramebuffer);
|
|
@@ -4162,20 +4358,24 @@ function engineObjectsCallback3D(pos, size, callback, objects=engineObjects, tes
|
|
|
4162
4358
|
* InstancedMesh3D - Many copies of one mesh drawn as one call, with their transforms kept on the GPU
|
|
4163
4359
|
* - For big sets that mostly stay put: an instance costs nothing per frame until it changes, so a hundred thousand
|
|
4164
4360
|
* 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
|
|
4361
|
+
* - setTransformAt, setMatrixAt and setColorAt change one instance, and only the changed range uploads before the
|
|
4362
|
+
* next draw; the matrices and the colors are kept apart, so moving instances uploads 64 bytes each
|
|
4363
|
+
* - For many instances moving every frame, setTransforms places a run of them from arrays of positions, rotations
|
|
4364
|
+
* and scales in one loop, the fastest way; or write matrixData directly and call markDirtyRange
|
|
4166
4365
|
* - 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,
|
|
4366
|
+
* - The whole set is culled by one bounding sphere around the origin, worked out when culling reads it, or set by hand
|
|
4367
|
+
* with radius; it casts and receives shadows like any object
|
|
4168
4368
|
* - The object's flags cover the whole set, one emissive, one tileInfo, one shader; only the colors are per instance
|
|
4169
4369
|
* - A mirrored instance, one with a negative scale, shows its inside unless the mesh is doubleSided
|
|
4170
4370
|
* - A transparent set draws in one go in the transparent stage, its instances are not sorted against each other;
|
|
4171
4371
|
* 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
|
|
4372
|
+
* - pick, the raycast and the collect helpers do not see the instances, test them yourself from matrixData
|
|
4173
4373
|
* @extends EngineObject3D
|
|
4174
4374
|
* @memberof Render3D
|
|
4175
4375
|
* @example
|
|
4176
4376
|
* const forest = new InstancedMesh3D(treeMesh, 1000);
|
|
4177
4377
|
* for (let i = 0; i < 1000; ++i)
|
|
4178
|
-
* forest.
|
|
4378
|
+
* forest.setTransformAt(i, randomGroundPos(), vec3(0, rand(2*PI), 0));
|
|
4179
4379
|
*/
|
|
4180
4380
|
class InstancedMesh3D extends EngineObject3D
|
|
4181
4381
|
{
|
|
@@ -4194,39 +4394,112 @@ class InstancedMesh3D extends EngineObject3D
|
|
|
4194
4394
|
this.count = count;
|
|
4195
4395
|
/** @property {number} - How many instances it was made with */
|
|
4196
4396
|
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 */
|
|
4397
|
+
/** @property {Float32Array} - Each instance's matrix, 16 floats, what moving them uploads; edit it directly and
|
|
4398
|
+
* call markDirty for the instances changed */
|
|
4399
|
+
this.matrixData = new Float32Array(count * RENDER3D_MATRIX_FLOATS);
|
|
4400
|
+
/** @property {Float32Array} - Each instance's color and uv rect, 8 floats, uploaded only when they change; edit
|
|
4401
|
+
* it directly and call markColorDirty for the instances changed */
|
|
4402
|
+
this.colorData = new Float32Array(count * RENDER3D_COLOR_FLOATS);
|
|
4403
|
+
/** @property {number} - First instance whose matrix uploads before the next draw */
|
|
4206
4404
|
this.dirtyStart = 0;
|
|
4207
|
-
/** @property {number} - One past the last instance
|
|
4405
|
+
/** @property {number} - One past the last instance whose matrix uploads, nothing when it is not past dirtyStart */
|
|
4208
4406
|
this.dirtyEnd = count;
|
|
4209
|
-
|
|
4210
|
-
this.
|
|
4211
|
-
|
|
4212
|
-
|
|
4407
|
+
/** @property {number} - First instance whose color uploads before the next draw */
|
|
4408
|
+
this.colorDirtyStart = 0;
|
|
4409
|
+
/** @property {number} - One past the last instance whose color uploads */
|
|
4410
|
+
this.colorDirtyEnd = count;
|
|
4411
|
+
// the farthest any instance's position has been from the origin, and the largest scale any has had, both
|
|
4412
|
+
// squared, and the instances set since radius last took them in: setting one does no bounds work at all
|
|
4413
|
+
this.reachSquared = 0;
|
|
4414
|
+
this.scaleSquared = 0;
|
|
4415
|
+
this.boundsStart = 0;
|
|
4416
|
+
this.boundsEnd = count;
|
|
4417
|
+
/** @type {number|undefined} */
|
|
4418
|
+
this.fixedRadius = undefined; // a radius set by hand, see radius
|
|
4419
|
+
this.buffer = undefined; // the GPU copy of matrixData
|
|
4420
|
+
/** @type {WebGLBuffer|undefined} */
|
|
4421
|
+
this.colorBuffer = undefined; // and of colorData
|
|
4422
|
+
this.bufferGeneration = -1; // the context they were made under
|
|
4423
|
+
this.uvTileInfo = tileInfo; // the tile the uv rects were written for
|
|
4424
|
+
const uv = render3DGetTileUVs(tileInfo), matrices = this.matrixData, colors = this.colorData;
|
|
4213
4425
|
for (let i = 0; i < count; ++i)
|
|
4214
4426
|
{
|
|
4215
|
-
|
|
4216
|
-
const k = i *
|
|
4217
|
-
|
|
4218
|
-
|
|
4427
|
+
matrices.set(RENDER3D_IDENTITY.m, i * RENDER3D_MATRIX_FLOATS);
|
|
4428
|
+
const k = i * RENDER3D_COLOR_FLOATS;
|
|
4429
|
+
colors[k] = color.r; colors[k+1] = color.g; colors[k+2] = color.b; colors[k+3] = color.a;
|
|
4430
|
+
colors[k+4] = uv.x; colors[k+5] = uv.y; colors[k+6] = uv.w; colors[k+7] = uv.h;
|
|
4219
4431
|
}
|
|
4220
4432
|
}
|
|
4221
4433
|
|
|
4434
|
+
/** Place an instance by its position, rotation and scale, what buildMatrix takes, written straight in with no
|
|
4435
|
+
* matrix made; in world space
|
|
4436
|
+
* @param {number} i
|
|
4437
|
+
* @param {Vector3} [pos]
|
|
4438
|
+
* @param {Vector3} [rotation] - Euler angles in radians
|
|
4439
|
+
* @param {Vector3} [scale] */
|
|
4440
|
+
setTransformAt(i, pos, rotation, scale)
|
|
4441
|
+
{
|
|
4442
|
+
ASSERT(i >= 0 && i < this.maxCount, 'instance index out of range');
|
|
4443
|
+
matrix4Compose(this.matrixData, i * RENDER3D_MATRIX_FLOATS, pos, rotation, scale);
|
|
4444
|
+
this.markDirty(i);
|
|
4445
|
+
}
|
|
4446
|
+
|
|
4447
|
+
/** Place a run of instances from arrays indexed by instance, plain or typed, as a game keeps them: each is placed
|
|
4448
|
+
* as setTransformAt places it, all in one loop with nothing made, the fastest way to move many every frame;
|
|
4449
|
+
* in world space
|
|
4450
|
+
* @param {number} start - First instance
|
|
4451
|
+
* @param {number} count - How many
|
|
4452
|
+
* @param {ArrayLike<number>} x - Positions
|
|
4453
|
+
* @param {ArrayLike<number>} y
|
|
4454
|
+
* @param {ArrayLike<number>} z
|
|
4455
|
+
* @param {ArrayLike<number>} [rx] - Euler angles in radians, all three or none for upright
|
|
4456
|
+
* @param {ArrayLike<number>} [ry]
|
|
4457
|
+
* @param {ArrayLike<number>} [rz]
|
|
4458
|
+
* @param {ArrayLike<number>} [sx] - Scales, all three or none for 1
|
|
4459
|
+
* @param {ArrayLike<number>} [sy]
|
|
4460
|
+
* @param {ArrayLike<number>} [sz] */
|
|
4461
|
+
setTransforms(start, count, x, y, z, rx, ry, rz, sx, sy, sz)
|
|
4462
|
+
{
|
|
4463
|
+
ASSERT(start >= 0 && count >= 0 && start + count <= this.maxCount, 'instance range out of range');
|
|
4464
|
+
ASSERT(!rx === !ry && !rx === !rz && !sx === !sy && !sx === !sz, 'rotations and scales come in threes');
|
|
4465
|
+
const m = this.matrixData, end = start + count;
|
|
4466
|
+
for (let i = start; i < end; ++i)
|
|
4467
|
+
{
|
|
4468
|
+
// matrix4Compose's expressions, so a run placed here is the same as each placed by setTransformAt
|
|
4469
|
+
const k = i * RENDER3D_MATRIX_FLOATS;
|
|
4470
|
+
const scaleX = sx ? sx[i] : 1, scaleY = sy ? sy[i] : 1, scaleZ = sz ? sz[i] : 1;
|
|
4471
|
+
if (rx)
|
|
4472
|
+
{
|
|
4473
|
+
const cx = cos(rx[i]), sinX = sin(rx[i]);
|
|
4474
|
+
const cy = cos(ry[i]), sinY = sin(ry[i]);
|
|
4475
|
+
const cz = cos(rz[i]), sinZ = sin(rz[i]);
|
|
4476
|
+
m[k] = (cy*cz + sinY*sinX*sinZ) * scaleX; m[k+1] = cx*sinZ * scaleX;
|
|
4477
|
+
m[k+2] = (-sinY*cz + cy*sinX*sinZ) * scaleX;
|
|
4478
|
+
m[k+4] = (-cy*sinZ + sinY*sinX*cz) * scaleY; m[k+5] = cx*cz * scaleY;
|
|
4479
|
+
m[k+6] = (sinY*sinZ + cy*sinX*cz) * scaleY;
|
|
4480
|
+
m[k+8] = sinY*cx * scaleZ; m[k+9] = -sinX * scaleZ; m[k+10] = cy*cx * scaleZ;
|
|
4481
|
+
}
|
|
4482
|
+
else
|
|
4483
|
+
{
|
|
4484
|
+
m[k] = scaleX; m[k+1] = 0; m[k+2] = 0;
|
|
4485
|
+
m[k+4] = 0; m[k+5] = scaleY; m[k+6] = 0;
|
|
4486
|
+
m[k+8] = 0; m[k+9] = 0; m[k+10] = scaleZ;
|
|
4487
|
+
}
|
|
4488
|
+
m[k+3] = m[k+7] = m[k+11] = 0; m[k+15] = 1;
|
|
4489
|
+
m[k+12] = x[i], m[k+13] = y[i], m[k+14] = z[i];
|
|
4490
|
+
}
|
|
4491
|
+
count && this.markDirtyRange(start, end);
|
|
4492
|
+
}
|
|
4493
|
+
|
|
4222
4494
|
/** Place an instance, in world space
|
|
4223
4495
|
* @param {number} i
|
|
4224
4496
|
* @param {Matrix4} matrix */
|
|
4225
4497
|
setMatrixAt(i, matrix)
|
|
4226
4498
|
{
|
|
4227
4499
|
ASSERT(i >= 0 && i < this.maxCount, 'instance index out of range');
|
|
4228
|
-
const data = this.
|
|
4229
|
-
|
|
4500
|
+
const data = this.matrixData, k = i * RENDER3D_MATRIX_FLOATS, m = matrix.m;
|
|
4501
|
+
for (let j = 0; j < 16; ++j)
|
|
4502
|
+
data[k+j] = m[j]; // for 16 floats a loop beats set's call
|
|
4230
4503
|
this.markDirty(i);
|
|
4231
4504
|
}
|
|
4232
4505
|
|
|
@@ -4236,8 +4509,8 @@ class InstancedMesh3D extends EngineObject3D
|
|
|
4236
4509
|
getMatrixAt(i)
|
|
4237
4510
|
{
|
|
4238
4511
|
ASSERT(i >= 0 && i < this.maxCount, 'instance index out of range');
|
|
4239
|
-
const k = i *
|
|
4240
|
-
matrix.m.set(this.
|
|
4512
|
+
const k = i * RENDER3D_MATRIX_FLOATS, matrix = new Matrix4;
|
|
4513
|
+
matrix.m.set(this.matrixData.subarray(k, k + 16));
|
|
4241
4514
|
return matrix;
|
|
4242
4515
|
}
|
|
4243
4516
|
|
|
@@ -4248,26 +4521,71 @@ class InstancedMesh3D extends EngineObject3D
|
|
|
4248
4521
|
{
|
|
4249
4522
|
ASSERT(i >= 0 && i < this.maxCount, 'instance index out of range');
|
|
4250
4523
|
ASSERT(isColor(color), 'color must be a Color');
|
|
4251
|
-
const data = this.
|
|
4252
|
-
data[k
|
|
4253
|
-
this.
|
|
4524
|
+
const data = this.colorData, k = i * RENDER3D_COLOR_FLOATS;
|
|
4525
|
+
data[k] = color.r; data[k+1] = color.g; data[k+2] = color.b; data[k+3] = color.a;
|
|
4526
|
+
this.markColorDirty(i);
|
|
4254
4527
|
}
|
|
4255
4528
|
|
|
4256
|
-
/** Note that an instance changed, so it uploads before the next draw and the bounds
|
|
4257
|
-
*
|
|
4529
|
+
/** Note that an instance's matrix changed, so it uploads before the next draw and the bounds take it in when
|
|
4530
|
+
* they are read; setTransformAt and setMatrixAt call this, and so must an edit made straight to matrixData
|
|
4258
4531
|
* @param {number} i */
|
|
4259
4532
|
markDirty(i)
|
|
4260
4533
|
{
|
|
4261
|
-
this.dirtyStart
|
|
4262
|
-
this.dirtyEnd
|
|
4534
|
+
if (i < this.dirtyStart) this.dirtyStart = i;
|
|
4535
|
+
if (i >= this.dirtyEnd) this.dirtyEnd = i + 1;
|
|
4536
|
+
if (i < this.boundsStart) this.boundsStart = i;
|
|
4537
|
+
if (i >= this.boundsEnd) this.boundsEnd = i + 1;
|
|
4538
|
+
}
|
|
4539
|
+
|
|
4540
|
+
/** Note that a run of instances' matrices changed, for code that writes matrixData directly, the lowest level
|
|
4541
|
+
* way to move many: they upload before the next draw and the bounds take them in when they are read
|
|
4542
|
+
* @param {number} start - First instance
|
|
4543
|
+
* @param {number} end - One past the last */
|
|
4544
|
+
markDirtyRange(start, end)
|
|
4545
|
+
{
|
|
4546
|
+
ASSERT(start >= 0 && end <= this.maxCount && start <= end, 'instance range out of range');
|
|
4547
|
+
if (start < this.dirtyStart) this.dirtyStart = start;
|
|
4548
|
+
if (end > this.dirtyEnd) this.dirtyEnd = end;
|
|
4549
|
+
if (start < this.boundsStart) this.boundsStart = start;
|
|
4550
|
+
if (end > this.boundsEnd) this.boundsEnd = end;
|
|
4551
|
+
}
|
|
4263
4552
|
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
this.
|
|
4553
|
+
/** Note that an instance's color changed, so it uploads before the next draw; setColorAt calls this, and so must
|
|
4554
|
+
* an edit made straight to colorData
|
|
4555
|
+
* @param {number} i */
|
|
4556
|
+
markColorDirty(i)
|
|
4557
|
+
{
|
|
4558
|
+
if (i < this.colorDirtyStart) this.colorDirtyStart = i;
|
|
4559
|
+
if (i >= this.colorDirtyEnd) this.colorDirtyEnd = i + 1;
|
|
4560
|
+
}
|
|
4561
|
+
|
|
4562
|
+
/** Radius of the sphere around the origin that holds every instance set so far, for culling: the farthest
|
|
4563
|
+
* instance, and the mesh's size at the largest scale; it only grows, and takes in the instances set since it
|
|
4564
|
+
* was last read, so with frustum culling off it is never worked out. Set it to fix the sphere, which a big set
|
|
4565
|
+
* that is culled can use to skip that work, and set it to undefined to go back to the bounds
|
|
4566
|
+
* @return {number} */
|
|
4567
|
+
get radius()
|
|
4568
|
+
{
|
|
4569
|
+
if (this.fixedRadius !== undefined) return this.fixedRadius;
|
|
4570
|
+
if (this.boundsEnd > this.boundsStart)
|
|
4571
|
+
{
|
|
4572
|
+
// the bounds grow to hold where each is now and how big, read back from its matrix, squared
|
|
4573
|
+
const d = this.matrixData;
|
|
4574
|
+
let reachSquared = this.reachSquared, scaleSquared = this.scaleSquared;
|
|
4575
|
+
for (let i = this.boundsStart; i < this.boundsEnd; ++i)
|
|
4576
|
+
{
|
|
4577
|
+
const k = i * RENDER3D_MATRIX_FLOATS, x = d[k+12], y = d[k+13], z = d[k+14];
|
|
4578
|
+
const reach = x*x + y*y + z*z, scale = render3DMaxStretchSquared(d, k);
|
|
4579
|
+
if (reach > reachSquared) reachSquared = reach;
|
|
4580
|
+
if (scale > scaleSquared) scaleSquared = scale;
|
|
4581
|
+
}
|
|
4582
|
+
this.reachSquared = reachSquared, this.scaleSquared = scaleSquared;
|
|
4583
|
+
this.boundsStart = Infinity, this.boundsEnd = 0;
|
|
4584
|
+
}
|
|
4585
|
+
const mesh = this.mesh, meshRadius = mesh.radius || mesh.computeRadius(); // the mesh measured once
|
|
4586
|
+
return this.reachSquared ** .5 + meshRadius * this.scaleSquared ** .5;
|
|
4270
4587
|
}
|
|
4588
|
+
set radius(radius) { this.fixedRadius = radius; }
|
|
4271
4589
|
|
|
4272
4590
|
/** Draws every instance as one call, uploading the ones that changed first
|
|
4273
4591
|
* @return {void} */
|
|
@@ -4281,41 +4599,46 @@ class InstancedMesh3D extends EngineObject3D
|
|
|
4281
4599
|
if (r.shadowPass && !r.lighting) return; // unlit things cast no shadow
|
|
4282
4600
|
render3DMeshUpload(mesh);
|
|
4283
4601
|
if (!mesh.bufferCount) return;
|
|
4284
|
-
this.radius = this.reach + mesh.radius * this.maxScale; // the mesh may have grown since
|
|
4285
4602
|
if (r.frustumCulling && !render3DSphereVisible(0, 0, 0, this.radius)) return;
|
|
4286
4603
|
|
|
4287
4604
|
// the uv rect is the object's tile for every instance, rewritten when the tile changes
|
|
4288
4605
|
if (this.uvTileInfo !== this.tileInfo)
|
|
4289
4606
|
{
|
|
4290
|
-
const uv = render3DGetTileUVs(this.tileInfo), data = this.
|
|
4291
|
-
for (let k =
|
|
4607
|
+
const uv = render3DGetTileUVs(this.tileInfo), data = this.colorData;
|
|
4608
|
+
for (let k = 4; k < data.length; k += RENDER3D_COLOR_FLOATS)
|
|
4292
4609
|
data[k] = uv.x, data[k+1] = uv.y, data[k+2] = uv.w, data[k+3] = uv.h;
|
|
4293
4610
|
this.uvTileInfo = this.tileInfo;
|
|
4294
|
-
this.
|
|
4611
|
+
this.colorDirtyStart = 0, this.colorDirtyEnd = this.maxCount;
|
|
4295
4612
|
}
|
|
4296
4613
|
|
|
4297
|
-
// the GPU
|
|
4614
|
+
// the GPU copies: all of each under a fresh context, otherwise just the changed ranges
|
|
4298
4615
|
if (!this.buffer || this.bufferGeneration !== r.contextGeneration)
|
|
4299
4616
|
{
|
|
4300
|
-
this.buffer = gl.createBuffer();
|
|
4301
4617
|
this.bufferGeneration = r.contextGeneration;
|
|
4302
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
|
4303
|
-
gl.bufferData(gl.ARRAY_BUFFER, this.
|
|
4618
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer = gl.createBuffer());
|
|
4619
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.matrixData, gl.DYNAMIC_DRAW);
|
|
4620
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer = gl.createBuffer());
|
|
4621
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.colorData, gl.DYNAMIC_DRAW);
|
|
4304
4622
|
}
|
|
4305
|
-
else
|
|
4623
|
+
else
|
|
4306
4624
|
{
|
|
4307
|
-
const
|
|
4308
|
-
|
|
4309
|
-
|
|
4625
|
+
const upload = (buffer, data, floats, start, end)=>
|
|
4626
|
+
{
|
|
4627
|
+
if (end <= start) return;
|
|
4628
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
4629
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, start * floats * 4, data, start * floats, (end - start) * floats);
|
|
4630
|
+
};
|
|
4631
|
+
upload(this.buffer, this.matrixData, RENDER3D_MATRIX_FLOATS, this.dirtyStart, this.dirtyEnd);
|
|
4632
|
+
upload(this.colorBuffer, this.colorData, RENDER3D_COLOR_FLOATS, this.colorDirtyStart, this.colorDirtyEnd);
|
|
4310
4633
|
}
|
|
4311
|
-
this.dirtyStart = Infinity, this.dirtyEnd = 0;
|
|
4634
|
+
this.dirtyStart = this.colorDirtyStart = Infinity, this.dirtyEnd = this.colorDirtyEnd = 0;
|
|
4312
4635
|
|
|
4313
4636
|
// one draw under the object's state, the mesh setting the culling as drawMesh does
|
|
4314
4637
|
r.flush();
|
|
4315
4638
|
render3DFlushBeforeOverlay(); // as in drawMesh, what was drawn before goes under it
|
|
4316
4639
|
const cullBackFaces = r.cullBackFaces, tileInfo = this.tileInfo;
|
|
4317
4640
|
r.cullBackFaces = !mesh.doubleSided;
|
|
4318
|
-
render3DDrawInstanced(mesh, this.buffer, this.count, render3DTextureOf(tileInfo), r);
|
|
4641
|
+
render3DDrawInstanced(mesh, this.buffer, this.count, render3DTextureOf(tileInfo), r, this.colorBuffer);
|
|
4319
4642
|
r.cullBackFaces = cullBackFaces;
|
|
4320
4643
|
}
|
|
4321
4644
|
|
|
@@ -4324,8 +4647,8 @@ class InstancedMesh3D extends EngineObject3D
|
|
|
4324
4647
|
destroy(immediate)
|
|
4325
4648
|
{
|
|
4326
4649
|
if (this.buffer && this.bufferGeneration === render3D?.contextGeneration)
|
|
4327
|
-
glContext?.deleteBuffer(this.buffer);
|
|
4328
|
-
this.buffer = undefined;
|
|
4650
|
+
glContext?.deleteBuffer(this.buffer), glContext?.deleteBuffer(this.colorBuffer);
|
|
4651
|
+
this.buffer = this.colorBuffer = undefined;
|
|
4329
4652
|
super.destroy(immediate);
|
|
4330
4653
|
}
|
|
4331
4654
|
}
|
|
@@ -4334,13 +4657,16 @@ class InstancedMesh3D extends EngineObject3D
|
|
|
4334
4657
|
/**
|
|
4335
4658
|
* Light3D - A light that is an EngineObject3D, so it can move, follow a parent or be destroyed like anything else
|
|
4336
4659
|
* - A point light: it lights what is near it and fades out by its radius, DirectionalLight3D shines from far away
|
|
4337
|
-
* -
|
|
4660
|
+
* - A coneAngle makes it a spotlight: it shines along its own forward, turned by rotation3D or by what it is
|
|
4661
|
+
* attached to, inside the cone, fading over the part of it coneSoftness says
|
|
4662
|
+
* - The sun, render3D.sunDirection, casts the shadows; a spotlight can cast them in its place, see
|
|
4663
|
+
* render3D.shadowLight; the other lights light and make highlights without one
|
|
4338
4664
|
* - Only the 8 lights nearest the camera are used each frame
|
|
4339
4665
|
* - radius is where the light fades out, and it fades fast, so a small radius wants a higher intensity
|
|
4340
4666
|
* - intensity multiplies the color, above 1 for a light brighter than white
|
|
4341
4667
|
* - radius is a world distance, so scale3D does not change it
|
|
4342
4668
|
* - An alpha, an intensity or a radius of 0 switches it off, and a light that is off takes none of those slots
|
|
4343
|
-
* - Draws nothing
|
|
4669
|
+
* - Draws nothing but its glow, when it has one; add a small emissive mesh if the lamp itself should be seen
|
|
4344
4670
|
* @extends EngineObject3D
|
|
4345
4671
|
* @memberof Render3D
|
|
4346
4672
|
* @example
|
|
@@ -4366,6 +4692,12 @@ class Light3D extends EngineObject3D
|
|
|
4366
4692
|
/** @property {boolean} - Shine from far away, from its position toward the origin, instead of out from its
|
|
4367
4693
|
* position with a falloff; DirectionalLight3D sets it */
|
|
4368
4694
|
this.directional = false;
|
|
4695
|
+
/** @property {number} - Makes it a spotlight: the angle in radians from its forward out to the edge of its
|
|
4696
|
+
* cone, so the beam is twice this across; 0 for a light that shines every way */
|
|
4697
|
+
this.coneAngle = 0;
|
|
4698
|
+
/** @property {number} - How much of the cone is its fading edge: 0 a hard edge, .2 by default, the outer
|
|
4699
|
+
* fifth, 1 fading all the way from the middle of the beam */
|
|
4700
|
+
this.coneSoftness = .2;
|
|
4369
4701
|
/** @property {number} - Size of a soft hazy glow drawn over the light, like a lamp at night, 0 for none; it
|
|
4370
4702
|
* is added onto what is behind it, and what is in front of the light hides it */
|
|
4371
4703
|
this.glow = 0;
|
|
@@ -4383,45 +4715,20 @@ class Light3D extends EngineObject3D
|
|
|
4383
4715
|
const r = render3D, c = this.color, pos = render3DObjectMatrix(this).getTranslation();
|
|
4384
4716
|
const toCamera = r.camera.pos.subtract(pos), distance = toCamera.length();
|
|
4385
4717
|
const at = distance ? pos.add(toCamera.scale(min(this.glow, distance) / 2 / distance)) : pos;
|
|
4386
|
-
const color = rgb(c.r, c.g, c.b, c.a * min(this.intensity, 1));
|
|
4387
|
-
|
|
4718
|
+
const color = rgb(c.r, c.g, c.b, c.a * min(this.intensity, 1)), texture = engineGlowTexture(this.glowFalloff);
|
|
4719
|
+
// a browser that can not make the glow's texture draws no glow, not a plain square
|
|
4720
|
+
if (texture || !glContext)
|
|
4721
|
+
r.drawBillboard(at, vec2(this.glow), texture, color);
|
|
4388
4722
|
}
|
|
4389
4723
|
}
|
|
4390
4724
|
|
|
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
4725
|
///////////////////////////////////////////////////////////////////////////////
|
|
4419
4726
|
/**
|
|
4420
4727
|
* DirectionalLight3D - A Light3D that shines from far away with no falloff, like sunlight
|
|
4421
4728
|
* - It shines from its position toward the origin, like a three.js DirectionalLight: only the direction to it
|
|
4422
4729
|
* counts, so moving it or its parent swings the light around; parent it to a sun in the sky and it follows
|
|
4423
4730
|
* - It cannot sit on the origin, since that leaves no direction
|
|
4424
|
-
* -
|
|
4731
|
+
* - It casts no shadow: the sun, render3D.sunDirection, does, or a spotlight set as render3D.shadowLight
|
|
4425
4732
|
* @extends Light3D
|
|
4426
4733
|
* @memberof Render3D
|
|
4427
4734
|
* @example
|