littlejsengine 1.20.0 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/littlejs.d.ts +1041 -81
- package/dist/littlejs.esm.js +6191 -379
- package/dist/littlejs.esm.min.js +22 -9
- package/dist/littlejs.js +6238 -476
- package/dist/littlejs.min.js +21 -8
- package/dist/littlejs.release.js +3500 -387
- package/package.json +3 -2
- package/plugins/gltf.js +61 -9
- package/plugins/lightSystem.js +44 -1
- package/plugins/math3d.js +132 -30
- package/plugins/particleEffects.js +745 -0
- package/plugins/pluginExport.js +42 -0
- package/plugins/postProcess.js +246 -39
- package/plugins/render3d.js +850 -182
- package/plugins/render3dDebug.js +2 -1
- package/plugins/render3dEditor.js +2576 -0
- package/plugins/render3dExtras.js +421 -15
- package/plugins/render3dLevel.js +352 -0
- package/plugins/render3dVoxels.js +12 -0
- package/plugins/sceneSystem.js +59 -0
- package/plugins/tweenSystem.js +4 -2
- package/src/engine.js +199 -34
- package/src/engineAudio.js +18 -9
- package/src/engineBuild.mjs +4 -0
- package/src/engineDebug.js +55 -1
- package/src/engineDraw.js +141 -7
- package/src/engineEditor.js +52 -22
- package/src/engineExport.js +8 -0
- package/src/engineInput.js +8 -4
- package/src/engineLogo.js +19 -0
- package/src/engineObject.js +3 -2
- package/src/engineParticles.js +80 -11
- package/src/engineRelease.js +13 -1
- package/src/engineSettings.js +38 -0
- package/src/engineWebGL.js +27 -10
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,18 +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
|
-
//
|
|
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 */
|
|
76
106
|
const RENDER3D_STATE_FIELDS = ['blend', 'additive', 'depthTest', 'depthWrite', 'cullBackFaces', 'mirrored', 'lighting',
|
|
77
|
-
'emissive', 'receiveShadow', 'specular', 'pixelated', 'shader'
|
|
107
|
+
'emissive', 'receiveShadow', 'specular', 'pixelated', 'shader', 'normalMap', 'normalScale', 'shininess',
|
|
108
|
+
'reflectivity', 'emissiveMap', 'emissiveMapColor'];
|
|
78
109
|
|
|
79
|
-
// a copy of the draw state in one fixed shape, the fields of RENDER3D_STATE_FIELDS written out
|
|
80
|
-
//
|
|
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
|
|
81
112
|
function render3DCaptureBatchState()
|
|
82
113
|
{
|
|
83
|
-
const r = render3D;
|
|
84
|
-
return {blend:
|
|
85
|
-
cullBackFaces:
|
|
86
|
-
receiveShadow:
|
|
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};
|
|
87
121
|
}
|
|
88
122
|
|
|
89
123
|
// put a captured draw state back, written out the same way; the transparent stage does this for every queued draw
|
|
@@ -92,23 +126,34 @@ function render3DApplyBatchState(s)
|
|
|
92
126
|
const r = render3D;
|
|
93
127
|
r.blend = s.blend, r.additive = s.additive, r.depthTest = s.depthTest, r.depthWrite = s.depthWrite,
|
|
94
128
|
r.cullBackFaces = s.cullBackFaces, r.mirrored = s.mirrored, r.lighting = s.lighting, r.emissive = s.emissive,
|
|
95
|
-
r.receiveShadow = s.receiveShadow, r.specular = s.specular, r.pixelated = s.pixelated, r.shader = s.shader
|
|
129
|
+
r.receiveShadow = s.receiveShadow, r.specular = s.specular, r.pixelated = s.pixelated, r.shader = s.shader,
|
|
130
|
+
r.normalMap = s.normalMap, r.normalScale = s.normalScale, r.shininess = s.shininess,
|
|
131
|
+
r.reflectivity = s.reflectivity, r.emissiveMap = s.emissiveMap, r.emissiveMapColor = s.emissiveMapColor;
|
|
96
132
|
}
|
|
97
133
|
|
|
98
|
-
// 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
|
|
99
136
|
function render3DStateChanged(s)
|
|
100
137
|
{
|
|
101
138
|
const r = render3D;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
||
|
|
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;
|
|
106
151
|
}
|
|
107
152
|
|
|
108
153
|
// whether a sphere is inside the view, or the shadow map's box during the shadow pass, without a vector
|
|
109
154
|
function render3DSphereVisible(x, y, z, radius)
|
|
110
155
|
{
|
|
111
|
-
const planes = render3D.shadowPass ? render3D.shadowPlanes : render3D.frustumPlanes;
|
|
156
|
+
const planes = render3D.shadowPass && !render3D.depthPass ? render3D.shadowPlanes : render3D.frustumPlanes;
|
|
112
157
|
for (let i = 0; i < planes.length; ++i)
|
|
113
158
|
{
|
|
114
159
|
const p = planes[i];
|
|
@@ -180,7 +225,10 @@ function render3DMaxScale(m)
|
|
|
180
225
|
// how far a matrix at offset k can move a point that is one unit from its origin, for a bounding sphere: the
|
|
181
226
|
// longest axis when the axes are square to each other, more when they are not, as a turned child under a parent
|
|
182
227
|
// scaled on one axis leaves them; the axes' dot products bound the largest stretch by their largest row sum
|
|
183
|
-
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)
|
|
184
232
|
{
|
|
185
233
|
const xx = m[k]*m[k] + m[k+1]*m[k+1] + m[k+2]*m[k+2];
|
|
186
234
|
const yy = m[k+4]*m[k+4] + m[k+5]*m[k+5] + m[k+6]*m[k+6];
|
|
@@ -188,7 +236,7 @@ function render3DMaxStretch(m, k=0)
|
|
|
188
236
|
const xy = abs(m[k]*m[k+4] + m[k+1]*m[k+5] + m[k+2]*m[k+6]);
|
|
189
237
|
const xz = abs(m[k]*m[k+8] + m[k+1]*m[k+9] + m[k+2]*m[k+10]);
|
|
190
238
|
const yz = abs(m[k+4]*m[k+8] + m[k+5]*m[k+9] + m[k+6]*m[k+10]);
|
|
191
|
-
return max(xx + xy + xz, yy + xy + yz, zz + xz + yz)
|
|
239
|
+
return max(xx + xy + xz, yy + xy + yz, zz + xz + yz);
|
|
192
240
|
}
|
|
193
241
|
|
|
194
242
|
// a quad as a strip from its center and half axes, the same corner order as render3DQuadStrip
|
|
@@ -208,6 +256,15 @@ function render3DSetObjectState(o)
|
|
|
208
256
|
r.emissive = emissive;
|
|
209
257
|
r.additive = !!o?.additive;
|
|
210
258
|
r.specular = o?.specular || 0;
|
|
259
|
+
const shininess = o?.shininess ?? 16, reflectivity = o?.reflectivity || 0;
|
|
260
|
+
ASSERT(isNumber(shininess) && shininess > 0, 'shininess must be a number above 0', shininess);
|
|
261
|
+
ASSERT(isNumber(reflectivity) && reflectivity >= 0 && reflectivity <= 1, 'reflectivity must be 0 to 1', reflectivity);
|
|
262
|
+
r.shininess = shininess;
|
|
263
|
+
r.reflectivity = reflectivity;
|
|
264
|
+
r.normalMap = o?.normalMap || undefined;
|
|
265
|
+
r.normalScale = o?.normalScale ?? 1;
|
|
266
|
+
r.emissiveMap = o?.emissiveMap || undefined;
|
|
267
|
+
r.emissiveMapColor = o?.emissiveMapColor || WHITE;
|
|
211
268
|
r.receiveShadow = !o || o.receiveShadow;
|
|
212
269
|
r.cullBackFaces = r.mirrored = false; // each mesh sets these as it draws
|
|
213
270
|
r.pixelated = !!o?.pixelated;
|
|
@@ -229,10 +286,11 @@ function render3DDrawObjects(objects)
|
|
|
229
286
|
render3DSetObjectState();
|
|
230
287
|
}
|
|
231
288
|
|
|
232
|
-
// 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
|
|
233
291
|
function render3DInstance(mesh, matrix, tileInfo, color)
|
|
234
292
|
{
|
|
235
|
-
const k = render3DInstanceSlot(mesh, render3DTextureOf(tileInfo)), data = mesh.instanceData;
|
|
293
|
+
const k = render3DInstanceSlot(mesh, render3DTextureOf(tileInfo), render3DMirrors(matrix)), data = mesh.instanceData;
|
|
236
294
|
data.set(matrix.m, k);
|
|
237
295
|
// the tint; this batch is never blended, so in the shadow pass the depth shader cuts it by its texture alpha
|
|
238
296
|
// alone, a see through caster draws blended instead and is cut by its tint alpha too
|
|
@@ -243,14 +301,18 @@ function render3DInstance(mesh, matrix, tileInfo, color)
|
|
|
243
301
|
|
|
244
302
|
// make room for one more instance of a mesh under a texture and the current draw state, flushing a batch that
|
|
245
303
|
// differs first, and return where its 24 floats go in mesh.instanceData: the matrix, the tint and the uv rect
|
|
246
|
-
|
|
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)
|
|
247
307
|
{
|
|
248
308
|
const r = render3D;
|
|
249
|
-
if (mesh.instanceCount && (mesh.instanceTextureInfo !== textureInfo ||
|
|
309
|
+
if (mesh.instanceCount && (mesh.instanceTextureInfo !== textureInfo || mesh.instanceMirrored !== mirrored
|
|
310
|
+
|| render3DStateChanged(mesh.instanceState)))
|
|
250
311
|
render3DFlushInstances(mesh);
|
|
251
312
|
if (!mesh.instanceCount)
|
|
252
313
|
{
|
|
253
314
|
mesh.instanceTextureInfo = textureInfo;
|
|
315
|
+
mesh.instanceMirrored = mirrored;
|
|
254
316
|
mesh.instanceState = render3DCaptureBatchState();
|
|
255
317
|
r.instanceMeshes.push(mesh);
|
|
256
318
|
}
|
|
@@ -281,7 +343,10 @@ function render3DFlushInstances(only)
|
|
|
281
343
|
const buffers = r.instanceBuffers, buffer = buffers[r.instanceBufferIndex = (r.instanceBufferIndex + 1) % buffers.length];
|
|
282
344
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
283
345
|
gl.bufferData(gl.ARRAY_BUFFER, mesh.instanceData, gl.DYNAMIC_DRAW, 0, count * RENDER3D_INSTANCE_FLOATS);
|
|
284
|
-
|
|
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);
|
|
285
350
|
}
|
|
286
351
|
if (!only)
|
|
287
352
|
r.instanceMeshes.length = 0;
|
|
@@ -296,13 +361,16 @@ function render3DFlushInstances(only)
|
|
|
296
361
|
// the arrays are turned on with their instance divisor for this one call and both are turned off after: a single
|
|
297
362
|
// draw reads these slots as constant attributes, and in Firefox a draw that reads a constant through a slot whose
|
|
298
363
|
// divisor is set leaves the next batch on that slot reading the wrong values
|
|
299
|
-
|
|
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)
|
|
300
366
|
{
|
|
301
|
-
const gl = glContext;
|
|
302
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
367
|
+
const gl = glContext, matrixBytes = RENDER3D_MATRIX_FLOATS * 4;
|
|
303
368
|
for (const [location, size, offset] of RENDER3D_INSTANCE_ATTRIBS)
|
|
304
369
|
{
|
|
305
|
-
|
|
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);
|
|
306
374
|
gl.enableVertexAttribArray(location);
|
|
307
375
|
gl.vertexAttribDivisor(location, 1);
|
|
308
376
|
}
|
|
@@ -347,11 +415,36 @@ function render3DCollectLights()
|
|
|
347
415
|
for (const light of lights)
|
|
348
416
|
distances.set(light, light.directional ? -1 : light.getWorldPos3D().distanceSquared(cameraPos));
|
|
349
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;
|
|
350
422
|
lights.length = RENDER3D_MAX_LIGHTS;
|
|
351
423
|
}
|
|
352
424
|
return lights;
|
|
353
425
|
}
|
|
354
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
|
+
|
|
355
448
|
// unit circle directions for a number of sides, [cos, sin, cos, sin, ...] including the closing point, cached
|
|
356
449
|
const render3DCircleCache = new Map;
|
|
357
450
|
function render3DCircle(sides)
|
|
@@ -403,7 +496,15 @@ class Render3DPlugin
|
|
|
403
496
|
{
|
|
404
497
|
ASSERT(!render3D, 'Render3D plugin already initialized');
|
|
405
498
|
render3D = this;
|
|
406
|
-
|
|
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(),
|
|
407
508
|
'the batch state functions must list RENDER3D_STATE_FIELDS');
|
|
408
509
|
|
|
409
510
|
/** @property {Camera3D} - The camera */
|
|
@@ -441,11 +542,24 @@ class Render3DPlugin
|
|
|
441
542
|
* flat faces */
|
|
442
543
|
this.smoothShading = false;
|
|
443
544
|
|
|
545
|
+
/** @property {boolean} - Draw the camera's depth into a texture each frame for post processing:
|
|
546
|
+
* PostProcessPlugin hands it to its shader as iChannel2, read with sceneDepth(uv); off by default and free
|
|
547
|
+
* when off, on it draws the solid objects of the default layer once more, depth only */
|
|
548
|
+
this.depthTexture = false;
|
|
549
|
+
|
|
444
550
|
// shadows
|
|
445
551
|
/** @property {boolean} - Cast real shadows from the sun, off by default and free when off */
|
|
446
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;
|
|
447
556
|
/** @property {number} - Size of the shadow map in pixels, bigger is sharper and slower */
|
|
448
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;
|
|
449
563
|
/** @property {number} - World size the shadow map covers around shadowCenter, smaller is sharper; it is a
|
|
450
564
|
* square facing the light, so it turns as the light does, and about 1.5 times an area's width covers it from
|
|
451
565
|
* any angle */
|
|
@@ -458,35 +572,6 @@ class Render3DPlugin
|
|
|
458
572
|
/** @property {number} - How much to blur the shadow edges */
|
|
459
573
|
this.shadowSoftness = 1;
|
|
460
574
|
|
|
461
|
-
// draw state, read at each draw
|
|
462
|
-
/** @property {boolean} - Apply lighting, when false draws plain vertex color times texture and casts no shadow;
|
|
463
|
-
* off for billboards, lines, ribbons and soft discs, an object sets emissive instead */
|
|
464
|
-
this.lighting = true;
|
|
465
|
-
/** @property {number} - How much a surface lights itself, set per object by its emissive */
|
|
466
|
-
this.emissive = 0;
|
|
467
|
-
/** @property {boolean} - Additive blending instead of alpha, in the transparent stage */
|
|
468
|
-
this.additive = false;
|
|
469
|
-
/** @property {boolean} - Test against the depth buffer, reset to true before each object and callback; a draw
|
|
470
|
-
* with it off goes over what was drawn before it and under what is drawn after, by render order, which
|
|
471
|
-
* ends the batch of meshes before it, so one per object costs a draw per object */
|
|
472
|
-
this.depthTest = true;
|
|
473
|
-
/** @property {boolean} - Write to the depth buffer, owned by the stages: on for opaque, off for transparent */
|
|
474
|
-
this.depthWrite = true;
|
|
475
|
-
// batch state set by drawMesh from each mesh: whether its back faces are skipped, off for strips so they
|
|
476
|
-
// show from both sides, and whether its transform mirrors it so the other winding is the front
|
|
477
|
-
this.cullBackFaces = false;
|
|
478
|
-
this.mirrored = false;
|
|
479
|
-
/** @property {number} - Strength of the highlight where the sun and the Light3D objects reflect, 0 is none and
|
|
480
|
-
* 1 adds a light's full color at its brightest; its size is fixed */
|
|
481
|
-
this.specular = 0;
|
|
482
|
-
/** @property {Shader|undefined} - Custom Shader for the next draws, set from each object's shader; undefined
|
|
483
|
-
* draws with the plugin's own
|
|
484
|
-
* @type {Shader|undefined} */
|
|
485
|
-
this.shader = undefined;
|
|
486
|
-
/** @property {boolean} - Darken by the shadow map when shadows are on, turn it off for things that should stay
|
|
487
|
-
* lit inside a shadow */
|
|
488
|
-
this.receiveShadow = true;
|
|
489
|
-
|
|
490
575
|
// the pass
|
|
491
576
|
/** @property {Function|undefined} - Draw solid world here, it runs again for shadows so only draw in it
|
|
492
577
|
* @type {Function|undefined} */
|
|
@@ -509,9 +594,6 @@ class Render3DPlugin
|
|
|
509
594
|
/** @property {boolean} - Sample textures through mipmaps so they do not shimmer in the distance, false uses
|
|
510
595
|
* each texture's own filtering like 2D */
|
|
511
596
|
this.mipmaps = true;
|
|
512
|
-
/** @property {boolean} - Draw state: keep texture pixels hard edged, no mipmaps and no blending between them,
|
|
513
|
-
* set per object by pixelated */
|
|
514
|
-
this.pixelated = false;
|
|
515
597
|
/** @property {number} - Anisotropic filtering for textures seen at an angle, 1 to 16, 1 is off; needs mipmaps */
|
|
516
598
|
this.anisotropy = 4;
|
|
517
599
|
|
|
@@ -555,7 +637,6 @@ class Render3DPlugin
|
|
|
555
637
|
this.cameraBack = vec3(0, 0, 1); // its opposite, the normal of camera facing draws
|
|
556
638
|
|
|
557
639
|
// internal state
|
|
558
|
-
this.blend = false; // blending on, set by the stages
|
|
559
640
|
/** @type {Array<Array<number>>} */
|
|
560
641
|
this.frustumPlanes = []; // the view as six inward planes [x, y, z, w]
|
|
561
642
|
/** @type {Array<Array<number>>} */
|
|
@@ -580,6 +661,13 @@ class Render3DPlugin
|
|
|
580
661
|
/** @type {WebGLFramebuffer|undefined} */
|
|
581
662
|
this.shadowFramebuffer = undefined;
|
|
582
663
|
this.shadowTextureSize = 0;
|
|
664
|
+
/** @type {WebGLTexture|undefined} */
|
|
665
|
+
this.cameraDepthTexture = undefined; // the camera's depth, drawn when depthTexture is on
|
|
666
|
+
/** @type {WebGLFramebuffer|undefined} */
|
|
667
|
+
this.cameraDepthFramebuffer = undefined;
|
|
668
|
+
this.cameraDepthWidth = 0;
|
|
669
|
+
this.cameraDepthHeight = 0;
|
|
670
|
+
this.depthPass = false; // drawing the camera's depth, a shadow pass seen from the camera
|
|
583
671
|
this.contextGeneration = 0; // counts context losses, a mesh uploaded under an older one uploads again
|
|
584
672
|
this.uniforms = new Map; // uniform locations by program
|
|
585
673
|
/** @type {Object<string, Array<number>>} */
|
|
@@ -588,6 +676,9 @@ class Render3DPlugin
|
|
|
588
676
|
this.passIsDefault = true; // the running pass is the default layer, the only one shadowed
|
|
589
677
|
this.lightPositions = new Float32Array(RENDER3D_MAX_LIGHTS * 4); // Light3D uniforms, filled each pass
|
|
590
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
|
|
591
682
|
|
|
592
683
|
// the stream of immediate mode draws
|
|
593
684
|
/** @type {WebGLBuffer|undefined} */
|
|
@@ -615,6 +706,116 @@ class Render3DPlugin
|
|
|
615
706
|
engineAddPlugin(undefined, render3DRender, render3DContextLost, render3DContextRestored, render3DPreRender);
|
|
616
707
|
}
|
|
617
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
|
+
|
|
618
819
|
///////////////////////////////////////////////////////////////////////////
|
|
619
820
|
// Matrices and picking
|
|
620
821
|
|
|
@@ -724,7 +925,8 @@ class Render3DPlugin
|
|
|
724
925
|
}
|
|
725
926
|
|
|
726
927
|
/** Play a sound at a 3D position, quieter with distance from the camera and panned by its side, like Sound.play
|
|
727
|
-
* 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
|
|
728
930
|
* @param {Sound} sound
|
|
729
931
|
* @param {Vector3} pos3D
|
|
730
932
|
* @param {number} [volume]
|
|
@@ -738,7 +940,9 @@ class Render3DPlugin
|
|
|
738
940
|
// the 3D range fade and pan, Sound.play with no position does the rest
|
|
739
941
|
ASSERT(sound instanceof Sound, 'sound must be a Sound');
|
|
740
942
|
ASSERT(isVector3(pos3D), 'pos3D must be a vec3');
|
|
741
|
-
|
|
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;
|
|
742
946
|
if (range)
|
|
743
947
|
{
|
|
744
948
|
const distance = offset.length();
|
|
@@ -802,15 +1006,15 @@ class Render3DPlugin
|
|
|
802
1006
|
if (this.frustumCulling && !render3DSphereVisible(m[12], m[13], m[14], mesh.radius * render3DMaxStretch(m)))
|
|
803
1007
|
return;
|
|
804
1008
|
// the mesh says whether its back faces can be skipped, and a mirroring transform, one with a negative
|
|
805
|
-
// determinant, turns the winding around so the other one is its front
|
|
806
|
-
|
|
807
|
-
this.cullBackFaces = !mesh.doubleSided;
|
|
808
|
-
this.mirrored = render3DMirrors(matrix);
|
|
809
|
-
// 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
|
|
810
1011
|
if (!this.blend && this.depthTest && (mesh.instanced ?? this.instancing))
|
|
811
1012
|
render3DInstance(mesh, matrix, tileInfo, color);
|
|
812
1013
|
else
|
|
813
1014
|
{
|
|
1015
|
+
const cullBackFaces = this.cullBackFaces, mirrored = this.mirrored;
|
|
1016
|
+
this.cullBackFaces = !mesh.doubleSided;
|
|
1017
|
+
this.mirrored = render3DMirrors(matrix);
|
|
814
1018
|
this.flush();
|
|
815
1019
|
render3DFlushBeforeOverlay();
|
|
816
1020
|
render3DSetDrawUniforms(matrix, tileInfo, color);
|
|
@@ -818,8 +1022,8 @@ class Render3DPlugin
|
|
|
818
1022
|
glContext.drawElements(glContext.TRIANGLES, mesh.bufferCount, mesh.indexType, 0);
|
|
819
1023
|
++drawCount;
|
|
820
1024
|
primitiveCount += mesh.bufferCount / 3;
|
|
1025
|
+
this.cullBackFaces = cullBackFaces, this.mirrored = mirrored;
|
|
821
1026
|
}
|
|
822
|
-
this.cullBackFaces = cullBackFaces, this.mirrored = mirrored;
|
|
823
1027
|
}
|
|
824
1028
|
|
|
825
1029
|
/** Draw a triangle strip, batched into the stream with the current draw state
|
|
@@ -1007,13 +1211,31 @@ class Render3DPlugin
|
|
|
1007
1211
|
// a far plane at Infinity has no midpoint, so put it a long way out instead
|
|
1008
1212
|
const {near, far} = this.camera;
|
|
1009
1213
|
const radius = far == Infinity ? near * 1e4 : (near + far) / 2;
|
|
1010
|
-
render3DWithState({lighting: false, blend: false, depthTest: false, depthWrite: false, fogEnd: 0, shader: undefined
|
|
1214
|
+
render3DWithState({lighting: false, blend: false, depthTest: false, depthWrite: false, fogEnd: 0, shader: undefined,
|
|
1215
|
+
emissiveMap: undefined}, ()=>
|
|
1011
1216
|
this.drawMesh(this.sky, buildMatrix(this.camera.pos, undefined, vec3(radius))));
|
|
1012
1217
|
}
|
|
1013
1218
|
|
|
1014
1219
|
/** Rebuild the light's view projection around the shadow center, called automatically each frame shadows are on */
|
|
1015
1220
|
updateShadowMatrix()
|
|
1016
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
|
|
1017
1239
|
ASSERT(this.shadowRange > 0, 'shadowRange must be positive');
|
|
1018
1240
|
const range = this.shadowRange > 0 ? this.shadowRange : 1, half = range / 2;
|
|
1019
1241
|
const toSun = this.sunDirection.normalize();
|
|
@@ -1132,8 +1354,10 @@ class Render3DPlugin
|
|
|
1132
1354
|
drawQuad(a, b, c, d, tileInfo, color=WHITE)
|
|
1133
1355
|
{
|
|
1134
1356
|
if (this.transparentQueue) // the queue replays later, so it keeps copies of what a caller may reuse
|
|
1357
|
+
{
|
|
1135
1358
|
a = a.copy(), b = b.copy(), c = c.copy(), d = d.copy();
|
|
1136
1359
|
color = isArray(color) ? color.map(k=> k.copy()) : color.copy();
|
|
1360
|
+
}
|
|
1137
1361
|
this.drawStrip(render3DQuadStrip(a, b, c, d), render3DFaceNormal(a, b, c, d), RENDER3D_QUAD_UVS,
|
|
1138
1362
|
render3DQuadValues(color), tileInfo);
|
|
1139
1363
|
}
|
|
@@ -1432,30 +1656,47 @@ const RENDER3D_SNIPPET_NAMES =
|
|
|
1432
1656
|
// uniforms: lightDir (xyz the way the sunlight travels, w = emissive, 1 or more skips the lighting),
|
|
1433
1657
|
// lightColor (the sun's rgb, a = specular), ambientFog (rgb, a = fogEnd), fogColor (rgb, a = fogStart),
|
|
1434
1658
|
// cameraPos, tex, shadowMap, shadowParams (x = shadows on, y = bias, z = blur step in texture space,
|
|
1435
|
-
// w = how the draw finishes: 1 opaque and alpha tested, 0 blended, -1 additive)
|
|
1659
|
+
// w = how the draw finishes: 1 opaque and alpha tested, 0 blended, -1 additive),
|
|
1660
|
+
// materialParams (x = normal map scale, 0 skips it, y = shininess, z = reflectivity), emissiveTint (rgb, a = the
|
|
1661
|
+
// emissive map is on), skyTop, skyHorizon, skyBottom (what a reflection shows), normalTex, emissiveTex
|
|
1436
1662
|
function render3DFragmentSource(fragmentCode)
|
|
1437
1663
|
{
|
|
1438
1664
|
return '#version 300 es\n' +
|
|
1439
1665
|
'precision highp float;' +
|
|
1440
1666
|
'uniform vec4 lightDir,lightColor,ambientFog,ambientGround,fogColor,shadowParams;' +
|
|
1441
1667
|
'uniform vec4 extraLights[' + RENDER3D_MAX_LIGHTS + '],extraLightColors[' + RENDER3D_MAX_LIGHTS + '];' +
|
|
1668
|
+
'uniform vec4 extraLightCones[' + RENDER3D_MAX_LIGHTS + '];' +
|
|
1442
1669
|
'uniform int extraLightCount;' +
|
|
1443
1670
|
'uniform vec3 cameraPos;' +
|
|
1444
|
-
'uniform
|
|
1671
|
+
'uniform vec4 materialParams,emissiveTint,skyTop,skyHorizon,skyBottom;' +
|
|
1672
|
+
'uniform sampler2D tex,normalTex,emissiveTex;' +
|
|
1445
1673
|
'uniform bool premultipliedTexture;' + // is the texture a render target, which holds premultiplied color
|
|
1446
1674
|
'uniform highp sampler2DShadow shadowMap;' +
|
|
1447
1675
|
'in vec3 P,N;in vec2 T,L;in vec4 C,S;' +
|
|
1448
1676
|
'out vec4 o;' +
|
|
1449
|
-
// 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
|
|
1450
1680
|
'float shadow(){' +
|
|
1451
|
-
'if(shadowParams.x<=0.)return 1.;' +
|
|
1681
|
+
'if(shadowParams.x<=0.||S.w<=0.)return 1.;' +
|
|
1452
1682
|
'vec3 q=S.xyz/S.w*.5+.5;' +
|
|
1453
1683
|
'if(any(greaterThanEqual(abs(q-.5),vec3(.5))))return 1.;' +
|
|
1454
|
-
'q.z-=shadowParams.y;' +
|
|
1684
|
+
'q.z-=shadowParams.y/(S.w*S.w);' +
|
|
1455
1685
|
'float s=0.;' +
|
|
1456
1686
|
'for(int x=-1;x<=1;++x)for(int y=-1;y<=1;++y)' +
|
|
1457
1687
|
's+=texture(shadowMap,vec3(q.xy+vec2(x,y)*shadowParams.z,q.z));' +
|
|
1458
1688
|
'return s/9.;}' +
|
|
1689
|
+
// the normal map's normal here, in the frame that the position and texture coordinate change along across the
|
|
1690
|
+
// screen, so a mesh needs no tangents; green points up the image and v runs down it, so up is -v;
|
|
1691
|
+
// a mesh with no texture coordinates has no frame and keeps its own normal
|
|
1692
|
+
'vec3 normalMapNormal(vec3 n){' +
|
|
1693
|
+
'vec3 p1=dFdx(P),p2=dFdy(P);vec2 t1=dFdx(T),t2=dFdy(T);' +
|
|
1694
|
+
'vec3 a=cross(p2,n),b=cross(n,p1),u=a*t1.x+b*t2.x,v=a*t1.y+b*t2.y;' +
|
|
1695
|
+
'float k=max(dot(u,u),dot(v,v));' +
|
|
1696
|
+
'if(k<=0.)return n;' +
|
|
1697
|
+
'vec3 m=texture(normalTex,T).xyz*2.-1.;' +
|
|
1698
|
+
'm.xy*=materialParams.x;' +
|
|
1699
|
+
'return normalize((u*m.x-v*m.y)*inversesqrt(k)+n*m.z);}' +
|
|
1459
1700
|
(fragmentCode ? RENDER3D_SNIPPET_NAMES + fragmentCode + '\n' : '') +
|
|
1460
1701
|
'void main(){' +
|
|
1461
1702
|
(fragmentCode ? 'vec4 t;mainImage(t,T);' : 'vec4 t=texture(tex,T);') +
|
|
@@ -1466,8 +1707,11 @@ function render3DFragmentSource(fragmentCode)
|
|
|
1466
1707
|
'if(e<1.){' +
|
|
1467
1708
|
'vec3 n=dot(N,N)>0.?normalize(N):vec3(0,1,0);' +
|
|
1468
1709
|
'if(!gl_FrontFacing)n=-n;' + // only a double sided mesh shows a back face, light it on the side that is seen
|
|
1710
|
+
'if(materialParams.x!=0.)n=normalMapNormal(n);' +
|
|
1469
1711
|
'float nl=dot(n,-lightDir.xyz);' +
|
|
1470
|
-
|
|
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;' +
|
|
1471
1715
|
// the ambient: one color, or blended from the ground color below to the sky color above by the way the face points
|
|
1472
1716
|
'vec3 l=(ambientGround.a>0.?mix(ambientGround.rgb,ambientFog.rgb,n.y*.5+.5):ambientFog.rgb)+lightColor.rgb*max(nl,0.)*s;' +
|
|
1473
1717
|
// the Light3D objects: a point light falls off with distance, a directional one does not and carries the
|
|
@@ -1483,16 +1727,30 @@ function render3DFragmentSource(fragmentCode)
|
|
|
1483
1727
|
'v/=max(d,1e-6);' +
|
|
1484
1728
|
'float ln=dot(n,v);' +
|
|
1485
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;' +
|
|
1486
1735
|
'l+=lc*max(0.,ln);' +
|
|
1487
|
-
'if(lightColor.a>0.)sp+=lc*pow(max(dot(reflect(-v,n),eye),0.),
|
|
1736
|
+
'if(lightColor.a>0.)sp+=lc*pow(max(dot(reflect(-v,n),eye),0.),materialParams.y)*step(0.,ln);' +
|
|
1488
1737
|
'}' +
|
|
1489
1738
|
'c.rgb*=l*(1.-e)+e;' + // lit, blended toward its own color by how emissive it is
|
|
1490
1739
|
// specular: the sun's only where its light hits and out of shadow, then the Light3D highlights,
|
|
1491
1740
|
// skipped entirely when the strength is zero
|
|
1492
1741
|
'if(lightColor.a>0.){' +
|
|
1493
1742
|
'vec3 r=reflect(lightDir.xyz,n);' +
|
|
1494
|
-
'c.rgb+=lightColor.rgb*pow(max(dot(r,eye),0.),
|
|
1743
|
+
'c.rgb+=lightColor.rgb*pow(max(dot(r,eye),0.),materialParams.y)*lightColor.a*step(0.,nl)*s*(1.-e)+sp*lightColor.a*(1.-e);' +
|
|
1744
|
+
'}' +
|
|
1745
|
+
// the sky along the reflected view, more at a glancing angle (Schlick's Fresnel); sky light, so the sun's
|
|
1746
|
+
// shadow does not dim it
|
|
1747
|
+
'if(materialParams.z>0.){' +
|
|
1748
|
+
'vec3 w=normalize(P-cameraPos),q=reflect(w,n);' +
|
|
1749
|
+
'float f=materialParams.z+(1.-materialParams.z)*pow(1.-max(dot(n,-w),0.),5.);' +
|
|
1750
|
+
'c.rgb=mix(c.rgb,q.y>0.?mix(skyHorizon.rgb,skyTop.rgb,q.y):mix(skyHorizon.rgb,skyBottom.rgb,-q.y),f);' +
|
|
1495
1751
|
'}}else c.rgb*=e;' + // fully emissive: its own color, or brighter, with no lighting to work out
|
|
1752
|
+
// the emissive map adds its light on top, lit or not
|
|
1753
|
+
'if(emissiveTint.a>0.)c.rgb+=texture(emissiveTex,T).rgb*emissiveTint.rgb;' +
|
|
1496
1754
|
'if(ambientFog.a>0.){' +
|
|
1497
1755
|
'float z=distance(cameraPos,P);' +
|
|
1498
1756
|
'c.rgb=mix(c.rgb,shadowParams.w<0.?vec3(0):fogColor.rgb,smoothstep(fogColor.a,ambientFog.a,z));' +
|
|
@@ -1519,6 +1777,8 @@ function render3DUseProgram(program)
|
|
|
1519
1777
|
gl.uniformMatrix4fv(render3DUniform('lightViewProj'), false, r.shadowMatrix.m);
|
|
1520
1778
|
gl.uniform1i(render3DUniform('tex'), 0);
|
|
1521
1779
|
gl.uniform1i(render3DUniform('shadowMap'), 1);
|
|
1780
|
+
gl.uniform1i(render3DUniform('normalTex'), 2);
|
|
1781
|
+
gl.uniform1i(render3DUniform('emissiveTex'), 3);
|
|
1522
1782
|
const c = r.camera.pos;
|
|
1523
1783
|
gl.uniform3f(render3DUniform('cameraPos'), c.x, c.y, c.z);
|
|
1524
1784
|
gl.uniform1i(render3DUniform('extraLightCount'), r.lightCount);
|
|
@@ -1526,6 +1786,7 @@ function render3DUseProgram(program)
|
|
|
1526
1786
|
{
|
|
1527
1787
|
gl.uniform4fv(render3DUniform('extraLights'), r.lightPositions, 0, r.lightCount * 4);
|
|
1528
1788
|
gl.uniform4fv(render3DUniform('extraLightColors'), r.lightColors, 0, r.lightCount * 4);
|
|
1789
|
+
gl.uniform4fv(render3DUniform('extraLightCones'), r.lightCones, 0, r.lightCount * 4);
|
|
1529
1790
|
}
|
|
1530
1791
|
if (program !== r.program)
|
|
1531
1792
|
{
|
|
@@ -1611,6 +1872,8 @@ function render3DContextLost()
|
|
|
1611
1872
|
render3DClearInstances();
|
|
1612
1873
|
r.shadowFramebuffer = r.shadowTexture = undefined;
|
|
1613
1874
|
r.shadowTextureSize = 0;
|
|
1875
|
+
r.cameraDepthFramebuffer = r.cameraDepthTexture = undefined;
|
|
1876
|
+
r.cameraDepthWidth = r.cameraDepthHeight = 0;
|
|
1614
1877
|
r.streamCount = 0;
|
|
1615
1878
|
++r.contextGeneration; // every uploaded mesh is stale now, the soft dot is a TextureInfo the engine restores
|
|
1616
1879
|
}
|
|
@@ -1664,18 +1927,19 @@ function render3DUpdateSamplers()
|
|
|
1664
1927
|
r.samplers.clear();
|
|
1665
1928
|
}
|
|
1666
1929
|
|
|
1667
|
-
// the sampler for a texture's wrap modes, smooth or hard edged, made the first time it is needed
|
|
1668
|
-
|
|
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)
|
|
1669
1933
|
{
|
|
1670
1934
|
const gl = glContext, r = render3D, [wrapS, wrapT] = glWrapModes(wrap);
|
|
1671
|
-
const key = wrapS * 1e5 + wrapT *
|
|
1935
|
+
const key = wrapS * 1e5 + wrapT * 4 + (pixelated ? 1 : 0) + (texturePixelated ? 2 : 0); // the modes are 5 digit numbers
|
|
1672
1936
|
let sampler = r.samplers.get(key);
|
|
1673
1937
|
if (sampler) return sampler;
|
|
1674
1938
|
sampler = gl.createSampler();
|
|
1675
|
-
const sharp = pixelated ||
|
|
1939
|
+
const sharp = pixelated || texturePixelated;
|
|
1676
1940
|
gl.samplerParameteri(sampler, gl.TEXTURE_MAG_FILTER, sharp ? gl.NEAREST : gl.LINEAR);
|
|
1677
1941
|
gl.samplerParameteri(sampler, gl.TEXTURE_MIN_FILTER, pixelated ? gl.NEAREST
|
|
1678
|
-
:
|
|
1942
|
+
: texturePixelated ? gl.NEAREST_MIPMAP_LINEAR : gl.LINEAR_MIPMAP_LINEAR);
|
|
1679
1943
|
gl.samplerParameteri(sampler, gl.TEXTURE_WRAP_S, wrapS);
|
|
1680
1944
|
gl.samplerParameteri(sampler, gl.TEXTURE_WRAP_T, wrapT);
|
|
1681
1945
|
const anisotropy = gl.getExtension('EXT_texture_filter_anisotropic');
|
|
@@ -1689,25 +1953,68 @@ function render3DSampler(wrap, pixelated)
|
|
|
1689
1953
|
}
|
|
1690
1954
|
|
|
1691
1955
|
// bind the texture of a tile or texture, white when there is none or it is not loaded, with the 3D sampler that
|
|
1692
|
-
// matches its wrap mode; the first time a texture is used in 3D it gets its mipmaps
|
|
1693
|
-
|
|
1956
|
+
// matches its wrap mode; the first time a texture is used in 3D it gets its mipmaps; on unit 0, the color texture,
|
|
1957
|
+
// or another unit, the material maps, after which unit 0 is active again
|
|
1958
|
+
function render3DBindTexture(tileInfo, state=render3D, unit=0)
|
|
1694
1959
|
{
|
|
1695
1960
|
const gl = glContext, r = render3D;
|
|
1696
1961
|
const textureInfo = render3DTextureOf(tileInfo);
|
|
1697
1962
|
const texture = textureInfo?.glTexture || r.whiteTexture;
|
|
1963
|
+
unit && gl.activeTexture(gl.TEXTURE0 + unit);
|
|
1698
1964
|
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
1699
1965
|
if (texture === r.whiteTexture || !r.mipmaps && !state.pixelated)
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
glUpdateMipmaps(texture); // drawn into since its mipmaps were made
|
|
1704
|
-
if (!state.pixelated && !glMipmappedTextures.has(texture)) // a hard edged draw never reads them
|
|
1966
|
+
gl.bindSampler(unit, null); // the texture's own filtering, as in 2D; the white texel needs no mipmaps
|
|
1967
|
+
// or anisotropy, and filtering it that way costs every untextured fragment
|
|
1968
|
+
else
|
|
1705
1969
|
{
|
|
1706
|
-
|
|
1707
|
-
|
|
1970
|
+
gl.bindSampler(unit, render3DSampler(textureInfo?.wrap, state.pixelated, textureInfo?.pixelated ?? tilesPixelated));
|
|
1971
|
+
glUpdateMipmaps(texture); // drawn into since its mipmaps were made
|
|
1972
|
+
if (!state.pixelated && !glMipmappedTextures.has(texture)) // a hard edged draw never reads them
|
|
1973
|
+
{
|
|
1974
|
+
glMipmappedTextures.add(texture); // the core makes them again when the texture changes
|
|
1975
|
+
gl.generateMipmap(gl.TEXTURE_2D);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
unit && gl.activeTexture(gl.TEXTURE0);
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
// the material's uniforms and maps for a draw: the normal map on unit 2 and the emissive map on unit 3, white when
|
|
1982
|
+
// unused and bound only when they change, and the sky a reflection shows, the colors setSky was given or the
|
|
1983
|
+
// ambient ones; with nothing set every part of it is skipped in the shader, and a map with no GL texture yet, or
|
|
1984
|
+
// one freed, counts as none, since the white texture in its place would bend every normal or light the surface
|
|
1985
|
+
function render3DSetMaterialUniforms(state)
|
|
1986
|
+
{
|
|
1987
|
+
const r = render3D, loaded = (map)=> render3DTextureOf(map)?.glTexture ? map : undefined;
|
|
1988
|
+
const normalMap = state.normalScale ? loaded(state.normalMap) : undefined, emissiveMap = loaded(state.emissiveMap);
|
|
1989
|
+
render3DUniform4f('materialParams', normalMap ? state.normalScale : 0, state.shininess, state.reflectivity, 0);
|
|
1990
|
+
const ec = state.emissiveMapColor || WHITE;
|
|
1991
|
+
emissiveMap ? render3DUniform4f('emissiveTint', ec.r, ec.g, ec.b, 1) : render3DUniform4f('emissiveTint', 0, 0, 0, 0);
|
|
1992
|
+
render3DBindMap(2, normalMap, state);
|
|
1993
|
+
render3DBindMap(3, emissiveMap, state);
|
|
1994
|
+
if (state.reflectivity > 0)
|
|
1995
|
+
{
|
|
1996
|
+
const sky = r.sky && render3DSkyColors.get(r.sky), a = r.ambientColor, g = r.ambientGroundColor || a;
|
|
1997
|
+
const top = sky ? sky[0] : a, bottom = sky ? sky[2] : g;
|
|
1998
|
+
render3DUniform4f('skyTop', top.r, top.g, top.b, 1);
|
|
1999
|
+
render3DUniform4f('skyBottom', bottom.r, bottom.g, bottom.b, 1);
|
|
2000
|
+
sky ? render3DUniform4f('skyHorizon', sky[1].r, sky[1].g, sky[1].b, 1) :
|
|
2001
|
+
render3DUniform4f('skyHorizon', (a.r + g.r) / 2, (a.g + g.g) / 2, (a.b + g.b) / 2, 1);
|
|
1708
2002
|
}
|
|
1709
2003
|
}
|
|
1710
2004
|
|
|
2005
|
+
// what each material map unit has bound, index 2 and 3 the maps and 4 and 5 whether each is pixelated, forgotten at
|
|
2006
|
+
// the start and end of each pass
|
|
2007
|
+
let render3DBoundMaps = [];
|
|
2008
|
+
|
|
2009
|
+
// bind a material map to its unit, or white for none, only when it or its filtering changed since the last draw
|
|
2010
|
+
function render3DBindMap(unit, map, state)
|
|
2011
|
+
{
|
|
2012
|
+
const bound = render3DBoundMaps, pixelated = !!map && state.pixelated;
|
|
2013
|
+
if (bound[unit] === map && bound[unit + 2] === pixelated) return;
|
|
2014
|
+
bound[unit] = map, bound[unit + 2] = pixelated;
|
|
2015
|
+
render3DBindTexture(map, state, unit);
|
|
2016
|
+
}
|
|
2017
|
+
|
|
1711
2018
|
// send a vec4 uniform of the main shader only when its value changed since the last send
|
|
1712
2019
|
function render3DUniform4f(name, x, y, z, w)
|
|
1713
2020
|
{
|
|
@@ -1840,11 +2147,14 @@ function render3DSetDrawUniforms(matrix, tileInfo, tint, uvRect, state=render3D)
|
|
|
1840
2147
|
// 0 blends them away instead, and -1 is additive, which has to fade into fog differently
|
|
1841
2148
|
const blendMode = state.blend ? (state.additive ? -1 : 0) : 1;
|
|
1842
2149
|
const receives = r.shadows && r.passIsDefault && state.receiveShadow ? 1 : 0;
|
|
1843
|
-
|
|
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);
|
|
1844
2153
|
|
|
1845
2154
|
// a render target's texture holds premultiplied color, the blend writes it that way, so the shader undoes it
|
|
1846
2155
|
const textureInfo = render3DTextureOf(tileInfo);
|
|
1847
2156
|
render3DUniform1i('premultipliedTexture', +!!(textureInfo?.glTexture && glPremultipliedTextures.has(textureInfo.glTexture)));
|
|
2157
|
+
render3DSetMaterialUniforms(state);
|
|
1848
2158
|
}
|
|
1849
2159
|
|
|
1850
2160
|
// the six flat sides of the camera's visible box, each as [x, y, z, w] facing inward
|
|
@@ -1866,6 +2176,7 @@ function render3DFrustumPlanes(matrix)
|
|
|
1866
2176
|
function render3DPreRender()
|
|
1867
2177
|
{
|
|
1868
2178
|
const r = render3D;
|
|
2179
|
+
editor3DCameraBegin(); // debug builds draw with the free camera or the 3D editor's, the game's is back after
|
|
1869
2180
|
r.updateMatrices();
|
|
1870
2181
|
r.shadowMapDrawn = false;
|
|
1871
2182
|
render3DRenderPass(false);
|
|
@@ -1875,6 +2186,7 @@ function render3DPreRender()
|
|
|
1875
2186
|
function render3DRender()
|
|
1876
2187
|
{
|
|
1877
2188
|
render3DRenderPass(true);
|
|
2189
|
+
editor3DCameraEnd();
|
|
1878
2190
|
}
|
|
1879
2191
|
|
|
1880
2192
|
// one 3D pass for the objects of a layer: take over the gl state, draw the shadow map once a frame and the stages, hand
|
|
@@ -1903,6 +2215,8 @@ function render3DRenderPass(after2D)
|
|
|
1903
2215
|
// gathered before the gl state is taken over, so an assert here leaves nothing to hand back
|
|
1904
2216
|
const lights = render3DCollectLights();
|
|
1905
2217
|
r.lightCount = lights.length;
|
|
2218
|
+
const caster = render3DShadowCaster();
|
|
2219
|
+
r.shadowLightIndex = caster ? lights.indexOf(caster) : -1;
|
|
1906
2220
|
const positions = r.lightPositions, colors = r.lightColors;
|
|
1907
2221
|
lights.forEach((light, i)=>
|
|
1908
2222
|
{
|
|
@@ -1913,6 +2227,7 @@ function render3DRenderPass(after2D)
|
|
|
1913
2227
|
positions[k] = p.x, positions[k+1] = p.y, positions[k+2] = p.z;
|
|
1914
2228
|
positions[k+3] = light.directional ? -1 : max(0, light.radius); // a negative radius marks a direction
|
|
1915
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);
|
|
1916
2231
|
});
|
|
1917
2232
|
|
|
1918
2233
|
// take over the gl state
|
|
@@ -1924,7 +2239,8 @@ function render3DRenderPass(after2D)
|
|
|
1924
2239
|
// may have left its own texture, which fails every draw; the shadow map goes back there even with shadows off
|
|
1925
2240
|
gl.activeTexture(gl.TEXTURE1);
|
|
1926
2241
|
gl.bindTexture(gl.TEXTURE_2D, r.shadowTexture || null);
|
|
1927
|
-
|
|
2242
|
+
// the material maps' units start white, a 2D plugin may have left its own textures there
|
|
2243
|
+
render3DSetMapUnits(r.whiteTexture);
|
|
1928
2244
|
gl.depthMask(true);
|
|
1929
2245
|
gl.clear(gl.DEPTH_BUFFER_BIT);
|
|
1930
2246
|
|
|
@@ -1937,6 +2253,8 @@ function render3DRenderPass(after2D)
|
|
|
1937
2253
|
render3DRenderShadowMap();
|
|
1938
2254
|
r.shadowMapDrawn = true;
|
|
1939
2255
|
}
|
|
2256
|
+
// the camera's depth for post processing, from the default layer
|
|
2257
|
+
r.depthTexture && isDefault && render3DRenderDepth();
|
|
1940
2258
|
render3DUseProgram(r.program); // after the shadow map, so the light matrix it sends is this frame's
|
|
1941
2259
|
r.renderStages(objects, isDefault);
|
|
1942
2260
|
}
|
|
@@ -1952,6 +2270,7 @@ function render3DRenderPass(after2D)
|
|
|
1952
2270
|
gl.depthMask(true);
|
|
1953
2271
|
gl.frontFace(gl.CCW);
|
|
1954
2272
|
gl.bindSampler(0, null); // back to the textures' own filtering for 2D
|
|
2273
|
+
render3DSetMapUnits(null); // the material maps leave no texture or sampler behind for 2D
|
|
1955
2274
|
if (glActiveTexture)
|
|
1956
2275
|
gl.bindTexture(gl.TEXTURE_2D, glActiveTexture);
|
|
1957
2276
|
// ARRAY_BUFFER is not part of VAO state in WebGL2, so bindVertexArray alone would not restore it
|
|
@@ -1960,11 +2279,27 @@ function render3DRenderPass(after2D)
|
|
|
1960
2279
|
}
|
|
1961
2280
|
}
|
|
1962
2281
|
|
|
2282
|
+
// put a texture on both material map units with no sampler, white at the start of the pass and none at its end, and
|
|
2283
|
+
// forget what the map cache thought was bound; unit 0 is active after
|
|
2284
|
+
function render3DSetMapUnits(texture)
|
|
2285
|
+
{
|
|
2286
|
+
const gl = glContext;
|
|
2287
|
+
for (const unit of [2, 3])
|
|
2288
|
+
{
|
|
2289
|
+
gl.activeTexture(gl.TEXTURE0 + unit);
|
|
2290
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
2291
|
+
gl.bindSampler(unit, null);
|
|
2292
|
+
}
|
|
2293
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
2294
|
+
render3DBoundMaps = [];
|
|
2295
|
+
}
|
|
2296
|
+
|
|
1963
2297
|
// create the shadow map depth texture and framebuffer at a size, or keep them when the size matches
|
|
1964
2298
|
function render3DUpdateShadowMap(size)
|
|
1965
2299
|
{
|
|
1966
2300
|
const gl = glContext, r = render3D;
|
|
1967
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
|
|
1968
2303
|
if (r.shadowTexture && r.shadowTextureSize === size) return;
|
|
1969
2304
|
r.shadowTexture && gl.deleteTexture(r.shadowTexture);
|
|
1970
2305
|
r.shadowFramebuffer && gl.deleteFramebuffer(r.shadowFramebuffer);
|
|
@@ -2036,6 +2371,65 @@ function render3DRenderShadowMap()
|
|
|
2036
2371
|
}
|
|
2037
2372
|
}
|
|
2038
2373
|
|
|
2374
|
+
// the camera's depth texture and its framebuffer at a size, made again when the canvas changes size; depth values
|
|
2375
|
+
// are not filtered, so it is read texel by texel
|
|
2376
|
+
function render3DUpdateCameraDepth(width, height)
|
|
2377
|
+
{
|
|
2378
|
+
const gl = glContext, r = render3D;
|
|
2379
|
+
if (r.cameraDepthTexture && r.cameraDepthWidth === width && r.cameraDepthHeight === height) return;
|
|
2380
|
+
r.cameraDepthTexture && gl.deleteTexture(r.cameraDepthTexture);
|
|
2381
|
+
r.cameraDepthFramebuffer && gl.deleteFramebuffer(r.cameraDepthFramebuffer);
|
|
2382
|
+
const texture = r.cameraDepthTexture = gl.createTexture();
|
|
2383
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
2384
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT24, width, height, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_INT, null);
|
|
2385
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
2386
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
2387
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
2388
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
2389
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
2390
|
+
const framebuffer = r.cameraDepthFramebuffer = gl.createFramebuffer();
|
|
2391
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
|
2392
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, texture, 0);
|
|
2393
|
+
gl.drawBuffers([gl.NONE]); // depth only
|
|
2394
|
+
gl.readBuffer(gl.NONE);
|
|
2395
|
+
ASSERT(gl.checkFramebufferStatus(gl.FRAMEBUFFER) == gl.FRAMEBUFFER_COMPLETE, 'camera depth framebuffer is incomplete');
|
|
2396
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
2397
|
+
r.cameraDepthWidth = width, r.cameraDepthHeight = height;
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
// draw the camera's depth for post processing: the solid objects from the camera with the shadow map's depth only
|
|
2401
|
+
// shader, into a texture the size of the canvas; what is see through is cut by its alpha as it is for shadows
|
|
2402
|
+
function render3DRenderDepth()
|
|
2403
|
+
{
|
|
2404
|
+
const gl = glContext, r = render3D, width = glCanvas.width, height = glCanvas.height;
|
|
2405
|
+
render3DUpdateCameraDepth(width, height);
|
|
2406
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, r.cameraDepthFramebuffer);
|
|
2407
|
+
gl.viewport(0, 0, width, height);
|
|
2408
|
+
gl.clear(gl.DEPTH_BUFFER_BIT);
|
|
2409
|
+
gl.useProgram(r.shadowShader);
|
|
2410
|
+
gl.uniformMatrix4fv(render3DUniform('viewProj', r.shadowShader), false, r.viewProjection.m);
|
|
2411
|
+
gl.enable(gl.DEPTH_TEST);
|
|
2412
|
+
gl.depthMask(true);
|
|
2413
|
+
gl.disable(gl.BLEND);
|
|
2414
|
+
gl.disable(gl.CULL_FACE);
|
|
2415
|
+
|
|
2416
|
+
r.shadowPass = r.depthPass = true;
|
|
2417
|
+
try
|
|
2418
|
+
{
|
|
2419
|
+
const solids = render3DLayerObjects(!!r.renderAfter2D).filter(o=> !o.additive && (!o.transparent || o.tileInfo));
|
|
2420
|
+
render3DDrawObjects(solids);
|
|
2421
|
+
r.onRenderOpaque?.();
|
|
2422
|
+
r.flush();
|
|
2423
|
+
render3DFlushInstances();
|
|
2424
|
+
}
|
|
2425
|
+
finally
|
|
2426
|
+
{
|
|
2427
|
+
r.shadowPass = r.depthPass = false;
|
|
2428
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
2429
|
+
gl.viewport(0, 0, width, height);
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2039
2433
|
///////////////////////////////////////////////////////////////////////////////
|
|
2040
2434
|
// Strips: every strip repeats its first point once at the start and its last
|
|
2041
2435
|
// point once at the end. Those repeats make flat triangles with no area, which
|
|
@@ -2189,18 +2583,26 @@ function render3DStripTriangles(count, remap, place)
|
|
|
2189
2583
|
}
|
|
2190
2584
|
|
|
2191
2585
|
// a mesh's packed vertex data for the GPU, one vertex per entry of a layout, which is the strip index of each,
|
|
2192
|
-
// written into data when given, the buffer an earlier call returned for the same layout
|
|
2586
|
+
// written into data when given, the buffer an earlier call returned for the same layout; it measures the mesh's
|
|
2587
|
+
// radius and box as it goes, as computeRadius does, over the vertices once instead of every strip entry
|
|
2193
2588
|
function render3DMeshVertexData(mesh, vertices, data=new ArrayBuffer(vertices.length * RENDER3D_VERTEX_BYTES))
|
|
2194
2589
|
{
|
|
2195
2590
|
const count = vertices.length;
|
|
2196
2591
|
const floats = new Float32Array(data), ints = new Uint32Array(data);
|
|
2592
|
+
let r = 0, x0 = Infinity, y0 = Infinity, z0 = Infinity, x1 = -Infinity, y1 = -Infinity, z1 = -Infinity;
|
|
2197
2593
|
for (let j = 0; j < count; ++j)
|
|
2198
2594
|
{
|
|
2199
2595
|
// a hand built mesh may leave normals, uvs and colors empty
|
|
2200
2596
|
const i = vertices[j], p = mesh.points[i], uv = mesh.uvs[i] || RENDER3D_DEFAULT_UV;
|
|
2201
|
-
|
|
2597
|
+
const x = p.x, y = p.y, z = p.z;
|
|
2598
|
+
render3DWriteVertex(floats, ints, j * RENDER3D_VERTEX_FLOATS, x, y, z,
|
|
2202
2599
|
mesh.normals[i] || RENDER3D_DEFAULT_NORMAL, uv.x, uv.y, (mesh.colors[i] || WHITE).rgbaInt());
|
|
2600
|
+
r = max(r, x*x + y*y + z*z);
|
|
2601
|
+
x0 = min(x0, x), y0 = min(y0, y), z0 = min(z0, z);
|
|
2602
|
+
x1 = max(x1, x), y1 = max(y1, y), z1 = max(z1, z);
|
|
2203
2603
|
}
|
|
2604
|
+
mesh.radius = r ** .5;
|
|
2605
|
+
mesh.bounds = count ? {min: vec3(x0, y0, z0), max: vec3(x1, y1, z1)} : {min: vec3(), max: vec3()};
|
|
2204
2606
|
return data;
|
|
2205
2607
|
}
|
|
2206
2608
|
|
|
@@ -2712,8 +3114,8 @@ class Mesh
|
|
|
2712
3114
|
* @return {Mesh} */
|
|
2713
3115
|
upload()
|
|
2714
3116
|
{
|
|
2715
|
-
|
|
2716
|
-
if (!render3D?.program || !glContext) return this;
|
|
3117
|
+
// the packing below measures the radius and box, with nothing to pack to they are measured here
|
|
3118
|
+
if (!render3D?.program || !glContext) return this.computeRadius(), this;
|
|
2717
3119
|
const gl = glContext, layout = this.vertexLayout;
|
|
2718
3120
|
// no layout when dynamicDraw was turned on after an upload, the next upload makes one
|
|
2719
3121
|
if (this.dynamicDraw && layout && this.buffer && this.contextGeneration === render3D.contextGeneration)
|
|
@@ -3215,25 +3617,30 @@ function buildSky(topColor=hsl(.6, .8, .55), horizonColor=hsl(.6, 1, .9), bottom
|
|
|
3215
3617
|
// a sphere turned inside out so it is seen from within, each point colored by how high it is
|
|
3216
3618
|
const mesh = buildSphere(2, sides, rings, true).flipNormals();
|
|
3217
3619
|
mesh.colors = mesh.points.map(p=> p.y < 0 ? horizonColor.lerp(bottomColor, -p.y) : horizonColor.lerp(topColor, p.y));
|
|
3620
|
+
render3DSkyColors.set(mesh, [topColor.copy(), horizonColor.copy(), bottomColor.copy()]);
|
|
3218
3621
|
return mesh;
|
|
3219
3622
|
}
|
|
3220
3623
|
|
|
3624
|
+
// the top, horizon and bottom colors of each sky dome buildSky made, which a reflection shows while that dome is
|
|
3625
|
+
// render3D.sky, whether setSky set it or the game did
|
|
3626
|
+
const render3DSkyColors = new WeakMap;
|
|
3627
|
+
|
|
3221
3628
|
///////////////////////////////////////////////////////////////////////////////
|
|
3222
3629
|
/**
|
|
3223
3630
|
* EngineObject3D - An EngineObject with a 3D transform and a mesh
|
|
3224
3631
|
* - Set pos3D, rotation3D and scale3D instead of the 2D pos, size and angle
|
|
3225
3632
|
* - Gets update, children, timers, destroy and renderOrder from EngineObject
|
|
3226
|
-
* - velocity3D is added to pos3D each frame,
|
|
3633
|
+
* - velocity3D is added to pos3D each frame, slowed by damping, and render3D.gravity pulls it once it has a mass
|
|
3227
3634
|
* - Objects face -Z, the same way the camera does, so lookAt turns them to face a point
|
|
3228
3635
|
* - The 2D pos and velocity are still there but nothing draws them
|
|
3229
|
-
* - These inherited fields are 2D only and do nothing here: angle, angleVelocity,
|
|
3230
|
-
*
|
|
3636
|
+
* - These inherited fields are 2D only and do nothing here: angle, angleVelocity, additiveColor, drawSize and mirror;
|
|
3637
|
+
* damping, angleDamping, clampSpeed, friction and groundObject work as in 2D, on velocity3D and angleVelocity3D
|
|
3231
3638
|
* - The inherited shader works here as in 2D, and with emissive at 1 its snippet does its own lighting
|
|
3232
3639
|
* - Set sync2D for a 2D game with 3D looks, pos and angle then drive pos3D and rotation3D,
|
|
3233
3640
|
* which is the one way those 2D fields reach a 3D object
|
|
3234
3641
|
* - setCollision takes the same flags as in 2D, but the solid collision happens in 3D against size3D
|
|
3235
|
-
* - The solid box
|
|
3236
|
-
*
|
|
3642
|
+
* - The solid box turns with rotation3D, so a turned wall or a ramp collides as it looks; resting on one no steeper
|
|
3643
|
+
* than groundAngle stands there
|
|
3237
3644
|
* - Its tile and raycast halves are 2D only so they default off here, and a child sits solid collision out
|
|
3238
3645
|
* - A sync2D object collides in 2D instead, which needs the 2D size set as well as size3D
|
|
3239
3646
|
* - setMesh swaps the mesh and frees the old one, for text and terrain that get built again
|
|
@@ -3281,7 +3688,7 @@ class EngineObject3D extends EngineObject
|
|
|
3281
3688
|
/** @property {Vector3} - Added to pos3D each frame by the engine before update, like the 2D velocity, no super
|
|
3282
3689
|
* call needed; damping and render3D.gravity act on it once the object has a mass */
|
|
3283
3690
|
this.velocity3D = vec3();
|
|
3284
|
-
/** @property {Vector3} - Added to rotation3D each frame by the engine before update,
|
|
3691
|
+
/** @property {Vector3} - Added to rotation3D each frame by the engine before update, slowed by angleDamping */
|
|
3285
3692
|
this.angleVelocity3D = vec3();
|
|
3286
3693
|
/** @property {Mesh|undefined} - Mesh to draw
|
|
3287
3694
|
* @type {Mesh|undefined} */
|
|
@@ -3312,8 +3719,27 @@ class EngineObject3D extends EngineObject
|
|
|
3312
3719
|
* lamps and glowing things, between is partly self lit, and above 1 is brighter than its color, for bloom */
|
|
3313
3720
|
this.emissive = 0;
|
|
3314
3721
|
/** @property {number} - Strength of the highlight where the sun and the Light3D objects reflect, 0 is none and
|
|
3315
|
-
* 1 adds a light's full color at its brightest; its size
|
|
3722
|
+
* 1 adds a light's full color at its brightest; shininess sets its size */
|
|
3316
3723
|
this.specular = 0;
|
|
3724
|
+
/** @property {number} - The highlight's exponent, how small and sharp it is: 4 is broad like rubber, 16 the
|
|
3725
|
+
* default, 100 sharp like polished metal; shows only with specular above 0 */
|
|
3726
|
+
this.shininess = 16;
|
|
3727
|
+
/** @property {TextureInfo|undefined} - A normal map that bends the surface at each texel so it catches the
|
|
3728
|
+
* light like bumps and grooves, green pointing up the image as OpenGL and glTF have it; read at the color
|
|
3729
|
+
* texture's coordinates, see normalMapFromHeight to make one in code
|
|
3730
|
+
* @type {TextureInfo|undefined} */
|
|
3731
|
+
this.normalMap = undefined;
|
|
3732
|
+
/** @property {number} - How strongly the normal map bends the surface, 0 turns it off, as glTF's scale */
|
|
3733
|
+
this.normalScale = 1;
|
|
3734
|
+
/** @property {number} - How much it reflects the sky, 0 none and 1 a mirror of it; the edges seen at a
|
|
3735
|
+
* glancing angle reflect more either way, as water and glass do */
|
|
3736
|
+
this.reflectivity = 0;
|
|
3737
|
+
/** @property {TextureInfo|undefined} - A texture of where it glows, added on top of the lit surface so it
|
|
3738
|
+
* shows in the dark, like lit windows; read at the color texture's coordinates
|
|
3739
|
+
* @type {TextureInfo|undefined} */
|
|
3740
|
+
this.emissiveMap = undefined;
|
|
3741
|
+
/** @property {Color} - Multiplies the emissive map, as glTF's emissiveFactor */
|
|
3742
|
+
this.emissiveMapColor = WHITE;
|
|
3317
3743
|
/** @property {boolean} - Draw into the shadow map when render3D.shadows is on; sprites and cut out textures
|
|
3318
3744
|
* cast their outline, an object faded below half its alpha casts nothing, a see through one casts only when
|
|
3319
3745
|
* textured, and additive objects never cast */
|
|
@@ -3321,6 +3747,9 @@ class EngineObject3D extends EngineObject
|
|
|
3321
3747
|
/** @property {boolean} - Collide as the sphere that fits size3D instead of as the size3D box, so it rolls
|
|
3322
3748
|
* around corners */
|
|
3323
3749
|
this.collideAsSphere3D = false;
|
|
3750
|
+
/** @property {number} - The steepest slope it stands on, in radians from level, PI/4 by default: resting on
|
|
3751
|
+
* a solid within this of flat sets groundObject and holds it still, steeper it slides down */
|
|
3752
|
+
this.groundAngle = PI / 4;
|
|
3324
3753
|
/** @property {boolean} - Darkened by the shadow map when render3D.shadows is on */
|
|
3325
3754
|
this.receiveShadow = true;
|
|
3326
3755
|
/** @property {boolean|undefined} - Draw this object over the 2D scene, undefined uses render3D.renderAfter2D
|
|
@@ -3351,10 +3780,32 @@ class EngineObject3D extends EngineObject
|
|
|
3351
3780
|
'a sync2D object collides in 2D, so give it a 2D size as well as a size3D', this.size);
|
|
3352
3781
|
if (this.sync2D)
|
|
3353
3782
|
super.updatePhysics();
|
|
3783
|
+
ASSERT(isNumber(this.groundAngle) && this.groundAngle >= 0 && this.groundAngle < PI / 2,
|
|
3784
|
+
'groundAngle must be 0 to less than PI/2, a slope from level', this.groundAngle);
|
|
3785
|
+
// what it stands on is found again each frame, by the level and by the solids it rests on; a sync2D
|
|
3786
|
+
// object's is the 2D physics'
|
|
3787
|
+
const ground = this.groundObject;
|
|
3788
|
+
this.sync2D || (this.groundObject = undefined);
|
|
3789
|
+
if (this.clampSpeed && !this.sync2D && (this.collideSolidObjects || this.collideLevel && this.mass))
|
|
3790
|
+
{
|
|
3791
|
+
// each axis within objectMaxSpeed, as in 2D, so a fast object does not pass through a thin wall; only
|
|
3792
|
+
// for what collides, anything else moves as fast as it is told
|
|
3793
|
+
const v = this.velocity3D, s = objectMaxSpeed;
|
|
3794
|
+
v.x = clamp(v.x, -s, s), v.y = clamp(v.y, -s, s), v.z = clamp(v.z, -s, s);
|
|
3795
|
+
}
|
|
3354
3796
|
// a moving object keeps out of the level, the height maps and voxel maps, from where it was before it moved
|
|
3355
3797
|
const oldPos = this.collideLevel && this.mass && !this.sync2D ? this.pos3D.copy() : undefined;
|
|
3356
3798
|
render3DMove(this);
|
|
3357
|
-
|
|
3799
|
+
if (ground && this.mass && !this.sync2D)
|
|
3800
|
+
{
|
|
3801
|
+
// sliding on what it stood on slows by friction, the less grippy of the two, relative to that one's own
|
|
3802
|
+
// speed so a moving platform carries it, as in 2D
|
|
3803
|
+
const friction = max(this.friction, ground.friction), v = this.velocity3D;
|
|
3804
|
+
const moving = ground instanceof EngineObject3D ? ground.velocity3D : undefined;
|
|
3805
|
+
const gx = moving?.x ?? 0, gz = moving?.z ?? 0;
|
|
3806
|
+
v.x = gx + (v.x - gx) * friction, v.z = gz + (v.z - gz) * friction;
|
|
3807
|
+
}
|
|
3808
|
+
oldPos && render3DCollideLevel(this, oldPos, ground);
|
|
3358
3809
|
// the engine only runs this for objects that own where they are, a child rides along with its parent
|
|
3359
3810
|
if (this.collideSolidObjects && !this.sync2D)
|
|
3360
3811
|
render3DCollideSolid(this);
|
|
@@ -3387,7 +3838,7 @@ class EngineObject3D extends EngineObject
|
|
|
3387
3838
|
|
|
3388
3839
|
/** Set how this object collides, the same flags as in 2D
|
|
3389
3840
|
* - Solid collision happens in 3D here, against size3D boxes or spheres; a child sits it out
|
|
3390
|
-
* - The boxes
|
|
3841
|
+
* - The boxes turn with rotation3D, against height maps and voxel maps an object is still its upright box
|
|
3391
3842
|
* - A sync2D object collides in 2D instead, against the 2D size, so set that as well as size3D
|
|
3392
3843
|
* @param {boolean} [collideSolidObjects] - Take part in solid collision
|
|
3393
3844
|
* @param {boolean} [isSolid] - Block other objects, a pair where neither one blocks passes through;
|
|
@@ -3575,27 +4026,29 @@ function render3DObjectMatrix(o)
|
|
|
3575
4026
|
return o.worldMatrix;
|
|
3576
4027
|
}
|
|
3577
4028
|
|
|
3578
|
-
// move an object by its 3D velocities, an object with mass falling with render3D.gravity
|
|
4029
|
+
// move an object by its 3D velocities, each slowed by its damping, an object with mass falling with render3D.gravity
|
|
3579
4030
|
function render3DMove(o)
|
|
3580
4031
|
{
|
|
3581
4032
|
// the vectors change in place, as the 2D object's do: this runs for every object every frame
|
|
4033
|
+
// a sync2D object's damping is the 2D physics', its 3D velocities are its own to set
|
|
3582
4034
|
const p = o.pos3D, v = o.velocity3D, r = o.rotation3D, a = o.angleVelocity3D;
|
|
4035
|
+
const d = o.sync2D ? 1 : o.damping, e = o.sync2D ? 1 : o.angleDamping;
|
|
4036
|
+
// damped first and gravity added after, the order EngineObject.updatePhysics uses,
|
|
4037
|
+
// so the same mass, damping and gravity fall the same way in both
|
|
4038
|
+
v.x *= d, v.y *= d, v.z *= d;
|
|
3583
4039
|
if (o.mass && !o.sync2D) // a 2D driven object gets the 2D gravity instead
|
|
3584
4040
|
{
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
const g = render3D.gravity, s = o.gravityScale, d = o.damping;
|
|
3588
|
-
v.x = v.x * d + g.x * s, v.y = v.y * d + g.y * s, v.z = v.z * d + g.z * s;
|
|
4041
|
+
const g = render3D.gravity, s = o.gravityScale;
|
|
4042
|
+
v.x += g.x * s, v.y += g.y * s, v.z += g.z * s;
|
|
3589
4043
|
}
|
|
3590
4044
|
p.x += v.x, p.y += v.y, p.z += v.z;
|
|
3591
|
-
r.x += a.x, r.y += a.y, r.z += a.z;
|
|
4045
|
+
r.x += a.x *= e, r.y += a.y *= e, r.z += a.z *= e;
|
|
3592
4046
|
}
|
|
3593
4047
|
|
|
3594
4048
|
// keep an object that moved out of the level's solid geometry, clearing what it stood on for the level to set again
|
|
3595
|
-
|
|
4049
|
+
// ground is what it stood on last frame, which a height map keeps it on going down a slope
|
|
4050
|
+
function render3DCollideLevel(o, oldPos, ground)
|
|
3596
4051
|
{
|
|
3597
|
-
const ground = o.groundObject;
|
|
3598
|
-
o.groundObject = undefined;
|
|
3599
4052
|
for (const level of render3DLevel)
|
|
3600
4053
|
level.destroyed || level.levelCollide3D(o, oldPos, level === ground);
|
|
3601
4054
|
}
|
|
@@ -3612,7 +4065,23 @@ function render3DSolidShape(o)
|
|
|
3612
4065
|
const kx = abs(k.x), ky = abs(k.y), kz = abs(k.z);
|
|
3613
4066
|
if (o.collideAsSphere3D)
|
|
3614
4067
|
return {pos: o.pos3D.copy(), radius: max(s.x, s.y, s.z) / 2 * max(kx, ky, kz)};
|
|
3615
|
-
|
|
4068
|
+
// a turned box has its axes, a solid is never a child so its rotation is the world's; an upright one has none, and
|
|
4069
|
+
// neither does a sprite, whose rotation turns how it faces the camera, not its box
|
|
4070
|
+
const axes = !render3DIsSprite(o) && isTurned3D(o.rotation3D) ? render3DSolidAxes(o) : undefined;
|
|
4071
|
+
return {pos: o.pos3D.copy(), size: vec3(s.x * kx, s.y * ky, s.z * kz), axes};
|
|
4072
|
+
}
|
|
4073
|
+
|
|
4074
|
+
// a turned solid's three axes, worked out when its rotation changes and kept until then, so the pairs it is tested
|
|
4075
|
+
// against each frame do not each work them out again
|
|
4076
|
+
const render3DSolidAxesCache = new WeakMap;
|
|
4077
|
+
function render3DSolidAxes(o)
|
|
4078
|
+
{
|
|
4079
|
+
const r = o.rotation3D, kept = render3DSolidAxesCache.get(o);
|
|
4080
|
+
if (kept && kept.x === r.x && kept.y === r.y && kept.z === r.z)
|
|
4081
|
+
return kept.axes;
|
|
4082
|
+
const axes = boxAxes3D(r);
|
|
4083
|
+
render3DSolidAxesCache.set(o, {x: r.x, y: r.y, z: r.z, axes});
|
|
4084
|
+
return axes;
|
|
3616
4085
|
}
|
|
3617
4086
|
|
|
3618
4087
|
// how far a solid shape can reach from its own center, for a quick reject before the exact test
|
|
@@ -3630,15 +4099,30 @@ function render3DSolidReach(o)
|
|
|
3630
4099
|
// what it takes to move shape a clear of shape b, whichever pair of shapes they are, or undefined for no touch
|
|
3631
4100
|
function render3DSolidPush(a, b)
|
|
3632
4101
|
{
|
|
4102
|
+
// a turned box is tested by its kept axes, the upright pairs as they always were
|
|
4103
|
+
const sphereBox = (sphere, box)=> box.axes ? collideSphereOrientedBox3D(sphere.pos, sphere.radius, box.pos,
|
|
4104
|
+
box.size, box.axes) : collideSphereBox(sphere.pos, sphere.radius, box.pos, box.size);
|
|
3633
4105
|
if (!a.size) // a is a sphere
|
|
3634
|
-
return b.size ?
|
|
3635
|
-
: collideSphereSphere(a.pos, a.radius, b.pos, b.radius);
|
|
4106
|
+
return b.size ? sphereBox(a, b) : collideSphereSphere(a.pos, a.radius, b.pos, b.radius);
|
|
3636
4107
|
if (!b.size) // only b is, so push b out of a and turn it around
|
|
3637
4108
|
{
|
|
3638
|
-
const push =
|
|
4109
|
+
const push = sphereBox(b, a);
|
|
3639
4110
|
return push && push.scale(-1);
|
|
3640
4111
|
}
|
|
3641
|
-
return
|
|
4112
|
+
return a.axes || b.axes ? collideOrientedBoxes3D(a.pos, a.size, a.axes ?? BOX_WORLD_AXES, b.pos, b.size,
|
|
4113
|
+
b.axes ?? BOX_WORLD_AXES) : collideBoxBox3D(a.pos, a.size, b.pos, b.size);
|
|
4114
|
+
}
|
|
4115
|
+
|
|
4116
|
+
// a sprite, a tile with no mesh, which faces the camera however it is turned
|
|
4117
|
+
const render3DIsSprite = (o)=> !o.mesh && !!o.tileInfo;
|
|
4118
|
+
|
|
4119
|
+
// whether a push leaves a box by one of its faces, along one of its axes, not by an edge or a corner; a sphere has
|
|
4120
|
+
// no face to stand on
|
|
4121
|
+
function render3DOnFace(push, shape)
|
|
4122
|
+
{
|
|
4123
|
+
if (!shape.size) return false;
|
|
4124
|
+
const n = push.normalize(), axes = shape.axes ?? BOX_WORLD_AXES;
|
|
4125
|
+
return max(abs(n.dot(axes[0])), abs(n.dot(axes[1])), abs(n.dot(axes[2]))) > 1 - 1e-6;
|
|
3642
4126
|
}
|
|
3643
4127
|
|
|
3644
4128
|
// push a solid object out of the solids before it in the engine's list of them, so each pair is resolved once:
|
|
@@ -3664,7 +4148,8 @@ function render3DCollideSolid(a)
|
|
|
3664
4148
|
if (dx*dx + dy*dy + dz*dz > reach*reach)
|
|
3665
4149
|
continue;
|
|
3666
4150
|
|
|
3667
|
-
const
|
|
4151
|
+
const shapeB = render3DSolidShape(b);
|
|
4152
|
+
let push = render3DSolidPush(shapeA, shapeB);
|
|
3668
4153
|
if (!push) continue;
|
|
3669
4154
|
|
|
3670
4155
|
// both objects hear about it, and either one can take the touch over
|
|
@@ -3672,6 +4157,19 @@ function render3DCollideSolid(a)
|
|
|
3672
4157
|
const resolveB = b.collideWithObject(a, push.scale(-1));
|
|
3673
4158
|
if (!resolveA || !resolveB) continue;
|
|
3674
4159
|
|
|
4160
|
+
// standing: resting on a box's face within the upper one's groundAngle of level holds it there, the push
|
|
4161
|
+
// turned straight up, as far as it takes to leave the surface, so it does not creep down a ramp; only what
|
|
4162
|
+
// moves stands, and a sphere, an edge or a corner is nothing to stand on, what rests there rolls off
|
|
4163
|
+
const lengthSquared = push.lengthSquared(), up = push.y / lengthSquared ** .5;
|
|
4164
|
+
const aStands = up > 0 && a.mass && up >= cos(a.groundAngle) && render3DOnFace(push, shapeB);
|
|
4165
|
+
const bStands = up < 0 && b.mass && -up >= cos(b.groundAngle) && render3DOnFace(push, shapeA);
|
|
4166
|
+
if (aStands || bStands)
|
|
4167
|
+
{
|
|
4168
|
+
aStands ? a.groundObject = b : b.groundObject = a;
|
|
4169
|
+
if (push.x || push.z) // one straight up already is used as it is
|
|
4170
|
+
push = vec3(0, lengthSquared / push.y, 0);
|
|
4171
|
+
}
|
|
4172
|
+
|
|
3675
4173
|
// heavier objects move less, mass 0 stays put; then bounce apart when moving toward each other
|
|
3676
4174
|
const total = a.mass + b.mass;
|
|
3677
4175
|
const weightA = !a.mass ? 0 : !b.mass ? 1 : b.mass / total;
|
|
@@ -3692,8 +4190,8 @@ function render3DCollideSolid(a)
|
|
|
3692
4190
|
|
|
3693
4191
|
/**
|
|
3694
4192
|
* Collect the EngineObject3D objects whose boxes overlap a sphere or a box, the 3D twin of engineObjectsCollect
|
|
3695
|
-
* -
|
|
3696
|
-
* and are never collected
|
|
4193
|
+
* - Each object is its size3D box at its world position, turned as it is turned; lights, emitters and trails have no
|
|
4194
|
+
* size and are never collected
|
|
3697
4195
|
* @param {Vector3} pos - Center of the area
|
|
3698
4196
|
* @param {Vector3|number} size - Diameter of a sphere if a number, 0 for a point, full size of a box if a Vector3
|
|
3699
4197
|
* @param {Array<EngineObject>} [objects] - Defaults to every object
|
|
@@ -3715,9 +4213,27 @@ function engineObjectsCollect3D(pos, size, objects=engineObjects, testCenters=fa
|
|
|
3715
4213
|
if (!(s.x || s.y || s.z)) continue;
|
|
3716
4214
|
const center = vec3(m[12], m[13], m[14]);
|
|
3717
4215
|
const worldSize = testCenters ? vec3() : render3DWorldSize(o, m);
|
|
4216
|
+
// a turned box is tested as turned, its axes read off the matrix, its columns with the scale taken out
|
|
4217
|
+
const turned = !testCenters && !render3DIsSprite(o) && (m[1] || m[2] || m[4] || m[6] || m[8] || m[9]);
|
|
4218
|
+
const axis = (i)=> vec3(m[i], m[i+1], m[i+2]).normalize();
|
|
4219
|
+
let axes = turned ? [axis(0), axis(4), axis(8)] : undefined;
|
|
4220
|
+
if (axes && (abs(axes[0].dot(axes[1])) > 1e-6 || abs(axes[0].dot(axes[2])) > 1e-6 ||
|
|
4221
|
+
abs(axes[1].dot(axes[2])) > 1e-6))
|
|
4222
|
+
{
|
|
4223
|
+
// a turned child of an unevenly scaled parent is sheared, its edges no longer square, which the box
|
|
4224
|
+
// tests do not take; the upright box around it stands in, which may take a little more but never misses
|
|
4225
|
+
const e = axes.map((a, i)=> a.scale(i ? i > 1 ? worldSize.z / 2 : worldSize.y / 2 : worldSize.x / 2));
|
|
4226
|
+
const reach = (c)=> 2 * (abs(e[0][c]) + abs(e[1][c]) + abs(e[2][c]));
|
|
4227
|
+
worldSize.set(reach('x'), reach('y'), reach('z'));
|
|
4228
|
+
axes = undefined;
|
|
4229
|
+
}
|
|
3718
4230
|
let hit;
|
|
3719
|
-
if (box)
|
|
4231
|
+
if (box && axes)
|
|
4232
|
+
hit = !!collideOrientedBoxes3D(pos, box, BOX_WORLD_AXES, center, worldSize, axes);
|
|
4233
|
+
else if (box)
|
|
3720
4234
|
hit = isOverlapping3D(pos, box, center, worldSize);
|
|
4235
|
+
else if (axes)
|
|
4236
|
+
hit = !!collideSphereOrientedBox3D(pos, radiusSquared ** .5, center, worldSize, axes);
|
|
3721
4237
|
else
|
|
3722
4238
|
{
|
|
3723
4239
|
// a sphere against the nearest point of the box
|
|
@@ -3836,20 +4352,24 @@ function engineObjectsCallback3D(pos, size, callback, objects=engineObjects, tes
|
|
|
3836
4352
|
* InstancedMesh3D - Many copies of one mesh drawn as one call, with their transforms kept on the GPU
|
|
3837
4353
|
* - For big sets that mostly stay put: an instance costs nothing per frame until it changes, so a hundred thousand
|
|
3838
4354
|
* trees cost what one tree does; objects and drawMesh batch by themselves too, but rebuild their batch every frame
|
|
3839
|
-
* - 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
|
|
3840
4359
|
* - The instances are in world space; the object's own pos3D, rotation3D and scale3D do not move them
|
|
3841
|
-
* - 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
|
|
3842
4362
|
* - The object's flags cover the whole set, one emissive, one tileInfo, one shader; only the colors are per instance
|
|
3843
4363
|
* - A mirrored instance, one with a negative scale, shows its inside unless the mesh is doubleSided
|
|
3844
4364
|
* - A transparent set draws in one go in the transparent stage, its instances are not sorted against each other;
|
|
3845
4365
|
* the set sorts against other transparent draws by the object's position, so put pos3D at its middle
|
|
3846
|
-
* - 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
|
|
3847
4367
|
* @extends EngineObject3D
|
|
3848
4368
|
* @memberof Render3D
|
|
3849
4369
|
* @example
|
|
3850
4370
|
* const forest = new InstancedMesh3D(treeMesh, 1000);
|
|
3851
4371
|
* for (let i = 0; i < 1000; ++i)
|
|
3852
|
-
* forest.
|
|
4372
|
+
* forest.setTransformAt(i, randomGroundPos(), vec3(0, rand(2*PI), 0));
|
|
3853
4373
|
*/
|
|
3854
4374
|
class InstancedMesh3D extends EngineObject3D
|
|
3855
4375
|
{
|
|
@@ -3868,29 +4388,101 @@ class InstancedMesh3D extends EngineObject3D
|
|
|
3868
4388
|
this.count = count;
|
|
3869
4389
|
/** @property {number} - How many instances it was made with */
|
|
3870
4390
|
this.maxCount = count;
|
|
3871
|
-
/** @property {Float32Array} -
|
|
3872
|
-
*
|
|
3873
|
-
this.
|
|
3874
|
-
/** @property {
|
|
3875
|
-
*
|
|
3876
|
-
this.
|
|
3877
|
-
|
|
3878
|
-
this.maxScale = 0; // and the largest scale any instance has had
|
|
3879
|
-
/** @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 */
|
|
3880
4398
|
this.dirtyStart = 0;
|
|
3881
|
-
/** @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 */
|
|
3882
4400
|
this.dirtyEnd = count;
|
|
3883
|
-
|
|
3884
|
-
this.
|
|
3885
|
-
|
|
3886
|
-
|
|
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;
|
|
3887
4419
|
for (let i = 0; i < count; ++i)
|
|
3888
4420
|
{
|
|
3889
|
-
|
|
3890
|
-
const k = i *
|
|
3891
|
-
|
|
3892
|
-
|
|
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];
|
|
3893
4484
|
}
|
|
4485
|
+
count && this.markDirtyRange(start, end);
|
|
3894
4486
|
}
|
|
3895
4487
|
|
|
3896
4488
|
/** Place an instance, in world space
|
|
@@ -3899,8 +4491,9 @@ class InstancedMesh3D extends EngineObject3D
|
|
|
3899
4491
|
setMatrixAt(i, matrix)
|
|
3900
4492
|
{
|
|
3901
4493
|
ASSERT(i >= 0 && i < this.maxCount, 'instance index out of range');
|
|
3902
|
-
const data = this.
|
|
3903
|
-
|
|
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
|
|
3904
4497
|
this.markDirty(i);
|
|
3905
4498
|
}
|
|
3906
4499
|
|
|
@@ -3910,8 +4503,8 @@ class InstancedMesh3D extends EngineObject3D
|
|
|
3910
4503
|
getMatrixAt(i)
|
|
3911
4504
|
{
|
|
3912
4505
|
ASSERT(i >= 0 && i < this.maxCount, 'instance index out of range');
|
|
3913
|
-
const k = i *
|
|
3914
|
-
matrix.m.set(this.
|
|
4506
|
+
const k = i * RENDER3D_MATRIX_FLOATS, matrix = new Matrix4;
|
|
4507
|
+
matrix.m.set(this.matrixData.subarray(k, k + 16));
|
|
3915
4508
|
return matrix;
|
|
3916
4509
|
}
|
|
3917
4510
|
|
|
@@ -3922,27 +4515,72 @@ class InstancedMesh3D extends EngineObject3D
|
|
|
3922
4515
|
{
|
|
3923
4516
|
ASSERT(i >= 0 && i < this.maxCount, 'instance index out of range');
|
|
3924
4517
|
ASSERT(isColor(color), 'color must be a Color');
|
|
3925
|
-
const data = this.
|
|
3926
|
-
data[k
|
|
3927
|
-
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);
|
|
3928
4521
|
}
|
|
3929
4522
|
|
|
3930
|
-
/** Note that an instance changed, so it uploads before the next draw and the bounds
|
|
3931
|
-
*
|
|
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
|
|
3932
4525
|
* @param {number} i */
|
|
3933
4526
|
markDirty(i)
|
|
3934
4527
|
{
|
|
3935
|
-
this.dirtyStart
|
|
3936
|
-
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
|
+
}
|
|
3937
4533
|
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
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;
|
|
3944
4545
|
}
|
|
3945
4546
|
|
|
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;
|
|
4581
|
+
}
|
|
4582
|
+
set radius(radius) { this.fixedRadius = radius; }
|
|
4583
|
+
|
|
3946
4584
|
/** Draws every instance as one call, uploading the ones that changed first
|
|
3947
4585
|
* @return {void} */
|
|
3948
4586
|
render3D()
|
|
@@ -3955,41 +4593,46 @@ class InstancedMesh3D extends EngineObject3D
|
|
|
3955
4593
|
if (r.shadowPass && !r.lighting) return; // unlit things cast no shadow
|
|
3956
4594
|
render3DMeshUpload(mesh);
|
|
3957
4595
|
if (!mesh.bufferCount) return;
|
|
3958
|
-
this.radius = this.reach + mesh.radius * this.maxScale; // the mesh may have grown since
|
|
3959
4596
|
if (r.frustumCulling && !render3DSphereVisible(0, 0, 0, this.radius)) return;
|
|
3960
4597
|
|
|
3961
4598
|
// the uv rect is the object's tile for every instance, rewritten when the tile changes
|
|
3962
4599
|
if (this.uvTileInfo !== this.tileInfo)
|
|
3963
4600
|
{
|
|
3964
|
-
const uv = render3DGetTileUVs(this.tileInfo), data = this.
|
|
3965
|
-
for (let k =
|
|
4601
|
+
const uv = render3DGetTileUVs(this.tileInfo), data = this.colorData;
|
|
4602
|
+
for (let k = 4; k < data.length; k += RENDER3D_COLOR_FLOATS)
|
|
3966
4603
|
data[k] = uv.x, data[k+1] = uv.y, data[k+2] = uv.w, data[k+3] = uv.h;
|
|
3967
4604
|
this.uvTileInfo = this.tileInfo;
|
|
3968
|
-
this.
|
|
4605
|
+
this.colorDirtyStart = 0, this.colorDirtyEnd = this.maxCount;
|
|
3969
4606
|
}
|
|
3970
4607
|
|
|
3971
|
-
// the GPU
|
|
4608
|
+
// the GPU copies: all of each under a fresh context, otherwise just the changed ranges
|
|
3972
4609
|
if (!this.buffer || this.bufferGeneration !== r.contextGeneration)
|
|
3973
4610
|
{
|
|
3974
|
-
this.buffer = gl.createBuffer();
|
|
3975
4611
|
this.bufferGeneration = r.contextGeneration;
|
|
3976
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
|
3977
|
-
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);
|
|
3978
4616
|
}
|
|
3979
|
-
else
|
|
4617
|
+
else
|
|
3980
4618
|
{
|
|
3981
|
-
const
|
|
3982
|
-
|
|
3983
|
-
|
|
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);
|
|
3984
4627
|
}
|
|
3985
|
-
this.dirtyStart = Infinity, this.dirtyEnd = 0;
|
|
4628
|
+
this.dirtyStart = this.colorDirtyStart = Infinity, this.dirtyEnd = this.colorDirtyEnd = 0;
|
|
3986
4629
|
|
|
3987
4630
|
// one draw under the object's state, the mesh setting the culling as drawMesh does
|
|
3988
4631
|
r.flush();
|
|
3989
4632
|
render3DFlushBeforeOverlay(); // as in drawMesh, what was drawn before goes under it
|
|
3990
4633
|
const cullBackFaces = r.cullBackFaces, tileInfo = this.tileInfo;
|
|
3991
4634
|
r.cullBackFaces = !mesh.doubleSided;
|
|
3992
|
-
render3DDrawInstanced(mesh, this.buffer, this.count, render3DTextureOf(tileInfo), r);
|
|
4635
|
+
render3DDrawInstanced(mesh, this.buffer, this.count, render3DTextureOf(tileInfo), r, this.colorBuffer);
|
|
3993
4636
|
r.cullBackFaces = cullBackFaces;
|
|
3994
4637
|
}
|
|
3995
4638
|
|
|
@@ -3998,8 +4641,8 @@ class InstancedMesh3D extends EngineObject3D
|
|
|
3998
4641
|
destroy(immediate)
|
|
3999
4642
|
{
|
|
4000
4643
|
if (this.buffer && this.bufferGeneration === render3D?.contextGeneration)
|
|
4001
|
-
glContext?.deleteBuffer(this.buffer);
|
|
4002
|
-
this.buffer = undefined;
|
|
4644
|
+
glContext?.deleteBuffer(this.buffer), glContext?.deleteBuffer(this.colorBuffer);
|
|
4645
|
+
this.buffer = this.colorBuffer = undefined;
|
|
4003
4646
|
super.destroy(immediate);
|
|
4004
4647
|
}
|
|
4005
4648
|
}
|
|
@@ -4008,7 +4651,10 @@ class InstancedMesh3D extends EngineObject3D
|
|
|
4008
4651
|
/**
|
|
4009
4652
|
* Light3D - A light that is an EngineObject3D, so it can move, follow a parent or be destroyed like anything else
|
|
4010
4653
|
* - A point light: it lights what is near it and fades out by its radius, DirectionalLight3D shines from far away
|
|
4011
|
-
* -
|
|
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
|
|
4012
4658
|
* - Only the 8 lights nearest the camera are used each frame
|
|
4013
4659
|
* - radius is where the light fades out, and it fades fast, so a small radius wants a higher intensity
|
|
4014
4660
|
* - intensity multiplies the color, above 1 for a light brighter than white
|
|
@@ -4040,10 +4686,32 @@ class Light3D extends EngineObject3D
|
|
|
4040
4686
|
/** @property {boolean} - Shine from far away, from its position toward the origin, instead of out from its
|
|
4041
4687
|
* position with a falloff; DirectionalLight3D sets it */
|
|
4042
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;
|
|
4695
|
+
/** @property {number} - Size of a soft hazy glow drawn over the light, like a lamp at night, 0 for none; it
|
|
4696
|
+
* is added onto what is behind it, and what is in front of the light hides it */
|
|
4697
|
+
this.glow = 0;
|
|
4698
|
+
/** @property {number} - How fast the glow fades from its middle: 1 by default, .5 a wide haze, 2 a tight
|
|
4699
|
+
* bright core */
|
|
4700
|
+
this.glowFalloff = 1;
|
|
4701
|
+
this.additive = true; // the glow is added on, in the transparent stage; a light with none draws nothing
|
|
4043
4702
|
}
|
|
4044
4703
|
|
|
4045
|
-
/**
|
|
4046
|
-
|
|
4704
|
+
/** Draw the glow, a quad facing the camera with a soft round glow on it, pulled toward the camera by half its
|
|
4705
|
+
* size so a lamp at the light does not cut into it, but never past the camera */
|
|
4706
|
+
render3D()
|
|
4707
|
+
{
|
|
4708
|
+
if (!(this.glow > 0) || this.directional) return;
|
|
4709
|
+
const r = render3D, c = this.color, pos = render3DObjectMatrix(this).getTranslation();
|
|
4710
|
+
const toCamera = r.camera.pos.subtract(pos), distance = toCamera.length();
|
|
4711
|
+
const at = distance ? pos.add(toCamera.scale(min(this.glow, distance) / 2 / distance)) : pos;
|
|
4712
|
+
const color = rgb(c.r, c.g, c.b, c.a * min(this.intensity, 1));
|
|
4713
|
+
r.drawBillboard(at, vec2(this.glow), engineGlowTexture(this.glowFalloff), color);
|
|
4714
|
+
}
|
|
4047
4715
|
}
|
|
4048
4716
|
|
|
4049
4717
|
///////////////////////////////////////////////////////////////////////////////
|