littlejsengine 1.20.0 → 1.21.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 +408 -26
- package/dist/littlejs.esm.js +3360 -161
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +3344 -161
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +1448 -141
- package/package.json +1 -1
- package/plugins/gltf.js +54 -8
- package/plugins/math3d.js +90 -10
- package/plugins/pluginExport.js +10 -0
- package/plugins/postProcess.js +41 -12
- package/plugins/render3d.js +424 -55
- package/plugins/render3dDebug.js +2 -1
- package/plugins/render3dEditor.js +1812 -0
- package/plugins/render3dExtras.js +199 -5
- package/plugins/render3dLevel.js +189 -0
- package/plugins/render3dVoxels.js +12 -0
- package/plugins/sceneSystem.js +58 -0
- package/plugins/tweenSystem.js +4 -2
- package/src/engine.js +199 -34
- package/src/engineAudio.js +2 -2
- package/src/engineBuild.mjs +3 -0
- package/src/engineDebug.js +55 -1
- package/src/engineDraw.js +97 -6
- package/src/engineEditor.js +41 -20
- package/src/engineExport.js +6 -0
- package/src/engineInput.js +8 -4
- package/src/engineLogo.js +19 -0
- package/src/engineObject.js +3 -2
- package/src/engineRelease.js +13 -1
- package/src/engineSettings.js +38 -0
package/plugins/render3d.js
CHANGED
|
@@ -72,9 +72,11 @@ function render3DCanDraw()
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
// the draw state fields a batch is drawn under; lights and fog are not captured, they are read live at flush
|
|
75
|
-
// the three functions below write them out by hand for speed, so a new field goes in all four
|
|
75
|
+
// the three functions below write them out by hand for speed, so a new field goes in all four; emissiveMapColor is
|
|
76
|
+
// compared by its values, so two objects with equal colors batch
|
|
76
77
|
const RENDER3D_STATE_FIELDS = ['blend', 'additive', 'depthTest', 'depthWrite', 'cullBackFaces', 'mirrored', 'lighting',
|
|
77
|
-
'emissive', 'receiveShadow', 'specular', 'pixelated', 'shader'
|
|
78
|
+
'emissive', 'receiveShadow', 'specular', 'pixelated', 'shader', 'normalMap', 'normalScale', 'shininess',
|
|
79
|
+
'reflectivity', 'emissiveMap', 'emissiveMapColor'];
|
|
78
80
|
|
|
79
81
|
// a copy of the draw state in one fixed shape, the fields of RENDER3D_STATE_FIELDS written out so the
|
|
80
82
|
// compare below stays a handful of direct reads, it runs for every instance drawn
|
|
@@ -83,7 +85,9 @@ function render3DCaptureBatchState()
|
|
|
83
85
|
const r = render3D;
|
|
84
86
|
return {blend: r.blend, additive: r.additive, depthTest: r.depthTest, depthWrite: r.depthWrite,
|
|
85
87
|
cullBackFaces: r.cullBackFaces, mirrored: r.mirrored, lighting: r.lighting, emissive: r.emissive,
|
|
86
|
-
receiveShadow: r.receiveShadow, specular: r.specular, pixelated: r.pixelated, shader: r.shader
|
|
88
|
+
receiveShadow: r.receiveShadow, specular: r.specular, pixelated: r.pixelated, shader: r.shader,
|
|
89
|
+
normalMap: r.normalMap, normalScale: r.normalScale, shininess: r.shininess, reflectivity: r.reflectivity,
|
|
90
|
+
emissiveMap: r.emissiveMap, emissiveMapColor: (r.emissiveMapColor || WHITE).copy()}; // the caller may change it
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
// put a captured draw state back, written out the same way; the transparent stage does this for every queued draw
|
|
@@ -92,7 +96,9 @@ function render3DApplyBatchState(s)
|
|
|
92
96
|
const r = render3D;
|
|
93
97
|
r.blend = s.blend, r.additive = s.additive, r.depthTest = s.depthTest, r.depthWrite = s.depthWrite,
|
|
94
98
|
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
|
|
99
|
+
r.receiveShadow = s.receiveShadow, r.specular = s.specular, r.pixelated = s.pixelated, r.shader = s.shader,
|
|
100
|
+
r.normalMap = s.normalMap, r.normalScale = s.normalScale, r.shininess = s.shininess,
|
|
101
|
+
r.reflectivity = s.reflectivity, r.emissiveMap = s.emissiveMap, r.emissiveMapColor = s.emissiveMapColor;
|
|
96
102
|
}
|
|
97
103
|
|
|
98
104
|
// true when the current draw state differs from a captured one, so a pending batch must flush first
|
|
@@ -102,13 +108,20 @@ function render3DStateChanged(s)
|
|
|
102
108
|
return r.blend !== s.blend || r.additive !== s.additive || r.depthTest !== s.depthTest
|
|
103
109
|
|| r.depthWrite !== s.depthWrite || r.cullBackFaces !== s.cullBackFaces || r.mirrored !== s.mirrored
|
|
104
110
|
|| r.lighting !== s.lighting || r.emissive !== s.emissive || r.receiveShadow !== s.receiveShadow
|
|
105
|
-
|| r.specular !== s.specular || r.pixelated !== s.pixelated || r.shader !== s.shader
|
|
111
|
+
|| r.specular !== s.specular || r.pixelated !== s.pixelated || r.shader !== s.shader
|
|
112
|
+
|| r.normalMap !== s.normalMap || r.normalScale !== s.normalScale || r.shininess !== s.shininess
|
|
113
|
+
|| r.reflectivity !== s.reflectivity || r.emissiveMap !== s.emissiveMap
|
|
114
|
+
|| render3DColorChanged(r.emissiveMapColor || WHITE, s.emissiveMapColor);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// whether two colors differ in their rgb, for the batch key's emissiveMapColor
|
|
118
|
+
function render3DColorChanged(a, b) { return a.r !== b.r || a.g !== b.g || a.b !== b.b;
|
|
106
119
|
}
|
|
107
120
|
|
|
108
121
|
// whether a sphere is inside the view, or the shadow map's box during the shadow pass, without a vector
|
|
109
122
|
function render3DSphereVisible(x, y, z, radius)
|
|
110
123
|
{
|
|
111
|
-
const planes = render3D.shadowPass ? render3D.shadowPlanes : render3D.frustumPlanes;
|
|
124
|
+
const planes = render3D.shadowPass && !render3D.depthPass ? render3D.shadowPlanes : render3D.frustumPlanes;
|
|
112
125
|
for (let i = 0; i < planes.length; ++i)
|
|
113
126
|
{
|
|
114
127
|
const p = planes[i];
|
|
@@ -208,6 +221,15 @@ function render3DSetObjectState(o)
|
|
|
208
221
|
r.emissive = emissive;
|
|
209
222
|
r.additive = !!o?.additive;
|
|
210
223
|
r.specular = o?.specular || 0;
|
|
224
|
+
const shininess = o?.shininess ?? 16, reflectivity = o?.reflectivity || 0;
|
|
225
|
+
ASSERT(isNumber(shininess) && shininess > 0, 'shininess must be a number above 0', shininess);
|
|
226
|
+
ASSERT(isNumber(reflectivity) && reflectivity >= 0 && reflectivity <= 1, 'reflectivity must be 0 to 1', reflectivity);
|
|
227
|
+
r.shininess = shininess;
|
|
228
|
+
r.reflectivity = reflectivity;
|
|
229
|
+
r.normalMap = o?.normalMap || undefined;
|
|
230
|
+
r.normalScale = o?.normalScale ?? 1;
|
|
231
|
+
r.emissiveMap = o?.emissiveMap || undefined;
|
|
232
|
+
r.emissiveMapColor = o?.emissiveMapColor || WHITE;
|
|
211
233
|
r.receiveShadow = !o || o.receiveShadow;
|
|
212
234
|
r.cullBackFaces = r.mirrored = false; // each mesh sets these as it draws
|
|
213
235
|
r.pixelated = !!o?.pixelated;
|
|
@@ -441,6 +463,11 @@ class Render3DPlugin
|
|
|
441
463
|
* flat faces */
|
|
442
464
|
this.smoothShading = false;
|
|
443
465
|
|
|
466
|
+
/** @property {boolean} - Draw the camera's depth into a texture each frame for post processing:
|
|
467
|
+
* PostProcessPlugin hands it to its shader as iChannel2, read with sceneDepth(uv); off by default and free
|
|
468
|
+
* when off, on it draws the solid objects of the default layer once more, depth only */
|
|
469
|
+
this.depthTexture = false;
|
|
470
|
+
|
|
444
471
|
// shadows
|
|
445
472
|
/** @property {boolean} - Cast real shadows from the sun, off by default and free when off */
|
|
446
473
|
this.shadows = false;
|
|
@@ -477,8 +504,23 @@ class Render3DPlugin
|
|
|
477
504
|
this.cullBackFaces = false;
|
|
478
505
|
this.mirrored = false;
|
|
479
506
|
/** @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
|
|
507
|
+
* 1 adds a light's full color at its brightest; shininess sets its size */
|
|
481
508
|
this.specular = 0;
|
|
509
|
+
/** @property {number} - The highlight's exponent, how small and sharp it is: 4 is broad like rubber, 100 sharp
|
|
510
|
+
* like polished metal; set from each object's shininess */
|
|
511
|
+
this.shininess = 16;
|
|
512
|
+
/** @property {TextureInfo|undefined} - Normal map for the next draws, set from each object's normalMap
|
|
513
|
+
* @type {TextureInfo|undefined} */
|
|
514
|
+
this.normalMap = undefined;
|
|
515
|
+
/** @property {number} - How strongly the normal map bends the surface, set from each object's normalScale */
|
|
516
|
+
this.normalScale = 1;
|
|
517
|
+
/** @property {number} - How much the surface reflects the sky, 0 to 1, set from each object's reflectivity */
|
|
518
|
+
this.reflectivity = 0;
|
|
519
|
+
/** @property {TextureInfo|undefined} - Emissive map for the next draws, set from each object's emissiveMap
|
|
520
|
+
* @type {TextureInfo|undefined} */
|
|
521
|
+
this.emissiveMap = undefined;
|
|
522
|
+
/** @property {Color} - Multiplies the emissive map, set from each object's emissiveMapColor */
|
|
523
|
+
this.emissiveMapColor = WHITE;
|
|
482
524
|
/** @property {Shader|undefined} - Custom Shader for the next draws, set from each object's shader; undefined
|
|
483
525
|
* draws with the plugin's own
|
|
484
526
|
* @type {Shader|undefined} */
|
|
@@ -580,6 +622,13 @@ class Render3DPlugin
|
|
|
580
622
|
/** @type {WebGLFramebuffer|undefined} */
|
|
581
623
|
this.shadowFramebuffer = undefined;
|
|
582
624
|
this.shadowTextureSize = 0;
|
|
625
|
+
/** @type {WebGLTexture|undefined} */
|
|
626
|
+
this.cameraDepthTexture = undefined; // the camera's depth, drawn when depthTexture is on
|
|
627
|
+
/** @type {WebGLFramebuffer|undefined} */
|
|
628
|
+
this.cameraDepthFramebuffer = undefined;
|
|
629
|
+
this.cameraDepthWidth = 0;
|
|
630
|
+
this.cameraDepthHeight = 0;
|
|
631
|
+
this.depthPass = false; // drawing the camera's depth, a shadow pass seen from the camera
|
|
583
632
|
this.contextGeneration = 0; // counts context losses, a mesh uploaded under an older one uploads again
|
|
584
633
|
this.uniforms = new Map; // uniform locations by program
|
|
585
634
|
/** @type {Object<string, Array<number>>} */
|
|
@@ -1007,7 +1056,8 @@ class Render3DPlugin
|
|
|
1007
1056
|
// a far plane at Infinity has no midpoint, so put it a long way out instead
|
|
1008
1057
|
const {near, far} = this.camera;
|
|
1009
1058
|
const radius = far == Infinity ? near * 1e4 : (near + far) / 2;
|
|
1010
|
-
render3DWithState({lighting: false, blend: false, depthTest: false, depthWrite: false, fogEnd: 0, shader: undefined
|
|
1059
|
+
render3DWithState({lighting: false, blend: false, depthTest: false, depthWrite: false, fogEnd: 0, shader: undefined,
|
|
1060
|
+
emissiveMap: undefined}, ()=>
|
|
1011
1061
|
this.drawMesh(this.sky, buildMatrix(this.camera.pos, undefined, vec3(radius))));
|
|
1012
1062
|
}
|
|
1013
1063
|
|
|
@@ -1132,8 +1182,10 @@ class Render3DPlugin
|
|
|
1132
1182
|
drawQuad(a, b, c, d, tileInfo, color=WHITE)
|
|
1133
1183
|
{
|
|
1134
1184
|
if (this.transparentQueue) // the queue replays later, so it keeps copies of what a caller may reuse
|
|
1185
|
+
{
|
|
1135
1186
|
a = a.copy(), b = b.copy(), c = c.copy(), d = d.copy();
|
|
1136
1187
|
color = isArray(color) ? color.map(k=> k.copy()) : color.copy();
|
|
1188
|
+
}
|
|
1137
1189
|
this.drawStrip(render3DQuadStrip(a, b, c, d), render3DFaceNormal(a, b, c, d), RENDER3D_QUAD_UVS,
|
|
1138
1190
|
render3DQuadValues(color), tileInfo);
|
|
1139
1191
|
}
|
|
@@ -1432,7 +1484,9 @@ const RENDER3D_SNIPPET_NAMES =
|
|
|
1432
1484
|
// uniforms: lightDir (xyz the way the sunlight travels, w = emissive, 1 or more skips the lighting),
|
|
1433
1485
|
// lightColor (the sun's rgb, a = specular), ambientFog (rgb, a = fogEnd), fogColor (rgb, a = fogStart),
|
|
1434
1486
|
// 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)
|
|
1487
|
+
// w = how the draw finishes: 1 opaque and alpha tested, 0 blended, -1 additive),
|
|
1488
|
+
// materialParams (x = normal map scale, 0 skips it, y = shininess, z = reflectivity), emissiveTint (rgb, a = the
|
|
1489
|
+
// emissive map is on), skyTop, skyHorizon, skyBottom (what a reflection shows), normalTex, emissiveTex
|
|
1436
1490
|
function render3DFragmentSource(fragmentCode)
|
|
1437
1491
|
{
|
|
1438
1492
|
return '#version 300 es\n' +
|
|
@@ -1441,7 +1495,8 @@ function render3DFragmentSource(fragmentCode)
|
|
|
1441
1495
|
'uniform vec4 extraLights[' + RENDER3D_MAX_LIGHTS + '],extraLightColors[' + RENDER3D_MAX_LIGHTS + '];' +
|
|
1442
1496
|
'uniform int extraLightCount;' +
|
|
1443
1497
|
'uniform vec3 cameraPos;' +
|
|
1444
|
-
'uniform
|
|
1498
|
+
'uniform vec4 materialParams,emissiveTint,skyTop,skyHorizon,skyBottom;' +
|
|
1499
|
+
'uniform sampler2D tex,normalTex,emissiveTex;' +
|
|
1445
1500
|
'uniform bool premultipliedTexture;' + // is the texture a render target, which holds premultiplied color
|
|
1446
1501
|
'uniform highp sampler2DShadow shadowMap;' +
|
|
1447
1502
|
'in vec3 P,N;in vec2 T,L;in vec4 C,S;' +
|
|
@@ -1456,6 +1511,17 @@ function render3DFragmentSource(fragmentCode)
|
|
|
1456
1511
|
'for(int x=-1;x<=1;++x)for(int y=-1;y<=1;++y)' +
|
|
1457
1512
|
's+=texture(shadowMap,vec3(q.xy+vec2(x,y)*shadowParams.z,q.z));' +
|
|
1458
1513
|
'return s/9.;}' +
|
|
1514
|
+
// the normal map's normal here, in the frame that the position and texture coordinate change along across the
|
|
1515
|
+
// screen, so a mesh needs no tangents; green points up the image and v runs down it, so up is -v;
|
|
1516
|
+
// a mesh with no texture coordinates has no frame and keeps its own normal
|
|
1517
|
+
'vec3 normalMapNormal(vec3 n){' +
|
|
1518
|
+
'vec3 p1=dFdx(P),p2=dFdy(P);vec2 t1=dFdx(T),t2=dFdy(T);' +
|
|
1519
|
+
'vec3 a=cross(p2,n),b=cross(n,p1),u=a*t1.x+b*t2.x,v=a*t1.y+b*t2.y;' +
|
|
1520
|
+
'float k=max(dot(u,u),dot(v,v));' +
|
|
1521
|
+
'if(k<=0.)return n;' +
|
|
1522
|
+
'vec3 m=texture(normalTex,T).xyz*2.-1.;' +
|
|
1523
|
+
'm.xy*=materialParams.x;' +
|
|
1524
|
+
'return normalize((u*m.x-v*m.y)*inversesqrt(k)+n*m.z);}' +
|
|
1459
1525
|
(fragmentCode ? RENDER3D_SNIPPET_NAMES + fragmentCode + '\n' : '') +
|
|
1460
1526
|
'void main(){' +
|
|
1461
1527
|
(fragmentCode ? 'vec4 t;mainImage(t,T);' : 'vec4 t=texture(tex,T);') +
|
|
@@ -1466,6 +1532,7 @@ function render3DFragmentSource(fragmentCode)
|
|
|
1466
1532
|
'if(e<1.){' +
|
|
1467
1533
|
'vec3 n=dot(N,N)>0.?normalize(N):vec3(0,1,0);' +
|
|
1468
1534
|
'if(!gl_FrontFacing)n=-n;' + // only a double sided mesh shows a back face, light it on the side that is seen
|
|
1535
|
+
'if(materialParams.x!=0.)n=normalMapNormal(n);' +
|
|
1469
1536
|
'float nl=dot(n,-lightDir.xyz);' +
|
|
1470
1537
|
'float s=shadow();' +
|
|
1471
1538
|
// the ambient: one color, or blended from the ground color below to the sky color above by the way the face points
|
|
@@ -1484,15 +1551,24 @@ function render3DFragmentSource(fragmentCode)
|
|
|
1484
1551
|
'float ln=dot(n,v);' +
|
|
1485
1552
|
'vec3 lc=extraLightColors[i].rgb*extraLightColors[i].a*a*a;' +
|
|
1486
1553
|
'l+=lc*max(0.,ln);' +
|
|
1487
|
-
'if(lightColor.a>0.)sp+=lc*pow(max(dot(reflect(-v,n),eye),0.),
|
|
1554
|
+
'if(lightColor.a>0.)sp+=lc*pow(max(dot(reflect(-v,n),eye),0.),materialParams.y)*step(0.,ln);' +
|
|
1488
1555
|
'}' +
|
|
1489
1556
|
'c.rgb*=l*(1.-e)+e;' + // lit, blended toward its own color by how emissive it is
|
|
1490
1557
|
// specular: the sun's only where its light hits and out of shadow, then the Light3D highlights,
|
|
1491
1558
|
// skipped entirely when the strength is zero
|
|
1492
1559
|
'if(lightColor.a>0.){' +
|
|
1493
1560
|
'vec3 r=reflect(lightDir.xyz,n);' +
|
|
1494
|
-
'c.rgb+=lightColor.rgb*pow(max(dot(r,eye),0.),
|
|
1561
|
+
'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);' +
|
|
1562
|
+
'}' +
|
|
1563
|
+
// the sky along the reflected view, more at a glancing angle (Schlick's Fresnel); sky light, so the sun's
|
|
1564
|
+
// shadow does not dim it
|
|
1565
|
+
'if(materialParams.z>0.){' +
|
|
1566
|
+
'vec3 w=normalize(P-cameraPos),q=reflect(w,n);' +
|
|
1567
|
+
'float f=materialParams.z+(1.-materialParams.z)*pow(1.-max(dot(n,-w),0.),5.);' +
|
|
1568
|
+
'c.rgb=mix(c.rgb,q.y>0.?mix(skyHorizon.rgb,skyTop.rgb,q.y):mix(skyHorizon.rgb,skyBottom.rgb,-q.y),f);' +
|
|
1495
1569
|
'}}else c.rgb*=e;' + // fully emissive: its own color, or brighter, with no lighting to work out
|
|
1570
|
+
// the emissive map adds its light on top, lit or not
|
|
1571
|
+
'if(emissiveTint.a>0.)c.rgb+=texture(emissiveTex,T).rgb*emissiveTint.rgb;' +
|
|
1496
1572
|
'if(ambientFog.a>0.){' +
|
|
1497
1573
|
'float z=distance(cameraPos,P);' +
|
|
1498
1574
|
'c.rgb=mix(c.rgb,shadowParams.w<0.?vec3(0):fogColor.rgb,smoothstep(fogColor.a,ambientFog.a,z));' +
|
|
@@ -1519,6 +1595,8 @@ function render3DUseProgram(program)
|
|
|
1519
1595
|
gl.uniformMatrix4fv(render3DUniform('lightViewProj'), false, r.shadowMatrix.m);
|
|
1520
1596
|
gl.uniform1i(render3DUniform('tex'), 0);
|
|
1521
1597
|
gl.uniform1i(render3DUniform('shadowMap'), 1);
|
|
1598
|
+
gl.uniform1i(render3DUniform('normalTex'), 2);
|
|
1599
|
+
gl.uniform1i(render3DUniform('emissiveTex'), 3);
|
|
1522
1600
|
const c = r.camera.pos;
|
|
1523
1601
|
gl.uniform3f(render3DUniform('cameraPos'), c.x, c.y, c.z);
|
|
1524
1602
|
gl.uniform1i(render3DUniform('extraLightCount'), r.lightCount);
|
|
@@ -1611,6 +1689,8 @@ function render3DContextLost()
|
|
|
1611
1689
|
render3DClearInstances();
|
|
1612
1690
|
r.shadowFramebuffer = r.shadowTexture = undefined;
|
|
1613
1691
|
r.shadowTextureSize = 0;
|
|
1692
|
+
r.cameraDepthFramebuffer = r.cameraDepthTexture = undefined;
|
|
1693
|
+
r.cameraDepthWidth = r.cameraDepthHeight = 0;
|
|
1614
1694
|
r.streamCount = 0;
|
|
1615
1695
|
++r.contextGeneration; // every uploaded mesh is stale now, the soft dot is a TextureInfo the engine restores
|
|
1616
1696
|
}
|
|
@@ -1689,25 +1769,68 @@ function render3DSampler(wrap, pixelated)
|
|
|
1689
1769
|
}
|
|
1690
1770
|
|
|
1691
1771
|
// 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
|
-
|
|
1772
|
+
// matches its wrap mode; the first time a texture is used in 3D it gets its mipmaps; on unit 0, the color texture,
|
|
1773
|
+
// or another unit, the material maps, after which unit 0 is active again
|
|
1774
|
+
function render3DBindTexture(tileInfo, state=render3D, unit=0)
|
|
1694
1775
|
{
|
|
1695
1776
|
const gl = glContext, r = render3D;
|
|
1696
1777
|
const textureInfo = render3DTextureOf(tileInfo);
|
|
1697
1778
|
const texture = textureInfo?.glTexture || r.whiteTexture;
|
|
1779
|
+
unit && gl.activeTexture(gl.TEXTURE0 + unit);
|
|
1698
1780
|
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
1699
1781
|
if (texture === r.whiteTexture || !r.mipmaps && !state.pixelated)
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1782
|
+
gl.bindSampler(unit, null); // the texture's own filtering, as in 2D; the white texel needs no mipmaps
|
|
1783
|
+
// or anisotropy, and filtering it that way costs every untextured fragment
|
|
1784
|
+
else
|
|
1785
|
+
{
|
|
1786
|
+
gl.bindSampler(unit, render3DSampler(textureInfo?.wrap, state.pixelated));
|
|
1787
|
+
glUpdateMipmaps(texture); // drawn into since its mipmaps were made
|
|
1788
|
+
if (!state.pixelated && !glMipmappedTextures.has(texture)) // a hard edged draw never reads them
|
|
1789
|
+
{
|
|
1790
|
+
glMipmappedTextures.add(texture); // the core makes them again when the texture changes
|
|
1791
|
+
gl.generateMipmap(gl.TEXTURE_2D);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
unit && gl.activeTexture(gl.TEXTURE0);
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
// the material's uniforms and maps for a draw: the normal map on unit 2 and the emissive map on unit 3, white when
|
|
1798
|
+
// unused and bound only when they change, and the sky a reflection shows, the colors setSky was given or the
|
|
1799
|
+
// ambient ones; with nothing set every part of it is skipped in the shader, and a map with no GL texture yet, or
|
|
1800
|
+
// one freed, counts as none, since the white texture in its place would bend every normal or light the surface
|
|
1801
|
+
function render3DSetMaterialUniforms(state)
|
|
1802
|
+
{
|
|
1803
|
+
const r = render3D, loaded = (map)=> render3DTextureOf(map)?.glTexture ? map : undefined;
|
|
1804
|
+
const normalMap = state.normalScale ? loaded(state.normalMap) : undefined, emissiveMap = loaded(state.emissiveMap);
|
|
1805
|
+
render3DUniform4f('materialParams', normalMap ? state.normalScale : 0, state.shininess, state.reflectivity, 0);
|
|
1806
|
+
const ec = state.emissiveMapColor || WHITE;
|
|
1807
|
+
emissiveMap ? render3DUniform4f('emissiveTint', ec.r, ec.g, ec.b, 1) : render3DUniform4f('emissiveTint', 0, 0, 0, 0);
|
|
1808
|
+
render3DBindMap(2, normalMap, state);
|
|
1809
|
+
render3DBindMap(3, emissiveMap, state);
|
|
1810
|
+
if (state.reflectivity > 0)
|
|
1705
1811
|
{
|
|
1706
|
-
|
|
1707
|
-
|
|
1812
|
+
const sky = r.sky && render3DSkyColors.get(r.sky), a = r.ambientColor, g = r.ambientGroundColor || a;
|
|
1813
|
+
const top = sky ? sky[0] : a, bottom = sky ? sky[2] : g;
|
|
1814
|
+
render3DUniform4f('skyTop', top.r, top.g, top.b, 1);
|
|
1815
|
+
render3DUniform4f('skyBottom', bottom.r, bottom.g, bottom.b, 1);
|
|
1816
|
+
sky ? render3DUniform4f('skyHorizon', sky[1].r, sky[1].g, sky[1].b, 1) :
|
|
1817
|
+
render3DUniform4f('skyHorizon', (a.r + g.r) / 2, (a.g + g.g) / 2, (a.b + g.b) / 2, 1);
|
|
1708
1818
|
}
|
|
1709
1819
|
}
|
|
1710
1820
|
|
|
1821
|
+
// what each material map unit has bound, index 2 and 3 the maps and 4 and 5 whether each is pixelated, forgotten at
|
|
1822
|
+
// the start and end of each pass
|
|
1823
|
+
let render3DBoundMaps = [];
|
|
1824
|
+
|
|
1825
|
+
// bind a material map to its unit, or white for none, only when it or its filtering changed since the last draw
|
|
1826
|
+
function render3DBindMap(unit, map, state)
|
|
1827
|
+
{
|
|
1828
|
+
const bound = render3DBoundMaps, pixelated = !!map && state.pixelated;
|
|
1829
|
+
if (bound[unit] === map && bound[unit + 2] === pixelated) return;
|
|
1830
|
+
bound[unit] = map, bound[unit + 2] = pixelated;
|
|
1831
|
+
render3DBindTexture(map, state, unit);
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1711
1834
|
// send a vec4 uniform of the main shader only when its value changed since the last send
|
|
1712
1835
|
function render3DUniform4f(name, x, y, z, w)
|
|
1713
1836
|
{
|
|
@@ -1845,6 +1968,7 @@ function render3DSetDrawUniforms(matrix, tileInfo, tint, uvRect, state=render3D)
|
|
|
1845
1968
|
// a render target's texture holds premultiplied color, the blend writes it that way, so the shader undoes it
|
|
1846
1969
|
const textureInfo = render3DTextureOf(tileInfo);
|
|
1847
1970
|
render3DUniform1i('premultipliedTexture', +!!(textureInfo?.glTexture && glPremultipliedTextures.has(textureInfo.glTexture)));
|
|
1971
|
+
render3DSetMaterialUniforms(state);
|
|
1848
1972
|
}
|
|
1849
1973
|
|
|
1850
1974
|
// the six flat sides of the camera's visible box, each as [x, y, z, w] facing inward
|
|
@@ -1866,6 +1990,7 @@ function render3DFrustumPlanes(matrix)
|
|
|
1866
1990
|
function render3DPreRender()
|
|
1867
1991
|
{
|
|
1868
1992
|
const r = render3D;
|
|
1993
|
+
editor3DCameraBegin(); // debug builds draw with the free camera or the 3D editor's, the game's is back after
|
|
1869
1994
|
r.updateMatrices();
|
|
1870
1995
|
r.shadowMapDrawn = false;
|
|
1871
1996
|
render3DRenderPass(false);
|
|
@@ -1875,6 +2000,7 @@ function render3DPreRender()
|
|
|
1875
2000
|
function render3DRender()
|
|
1876
2001
|
{
|
|
1877
2002
|
render3DRenderPass(true);
|
|
2003
|
+
editor3DCameraEnd();
|
|
1878
2004
|
}
|
|
1879
2005
|
|
|
1880
2006
|
// 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
|
|
@@ -1924,7 +2050,8 @@ function render3DRenderPass(after2D)
|
|
|
1924
2050
|
// may have left its own texture, which fails every draw; the shadow map goes back there even with shadows off
|
|
1925
2051
|
gl.activeTexture(gl.TEXTURE1);
|
|
1926
2052
|
gl.bindTexture(gl.TEXTURE_2D, r.shadowTexture || null);
|
|
1927
|
-
|
|
2053
|
+
// the material maps' units start white, a 2D plugin may have left its own textures there
|
|
2054
|
+
render3DSetMapUnits(r.whiteTexture);
|
|
1928
2055
|
gl.depthMask(true);
|
|
1929
2056
|
gl.clear(gl.DEPTH_BUFFER_BIT);
|
|
1930
2057
|
|
|
@@ -1937,6 +2064,8 @@ function render3DRenderPass(after2D)
|
|
|
1937
2064
|
render3DRenderShadowMap();
|
|
1938
2065
|
r.shadowMapDrawn = true;
|
|
1939
2066
|
}
|
|
2067
|
+
// the camera's depth for post processing, from the default layer
|
|
2068
|
+
r.depthTexture && isDefault && render3DRenderDepth();
|
|
1940
2069
|
render3DUseProgram(r.program); // after the shadow map, so the light matrix it sends is this frame's
|
|
1941
2070
|
r.renderStages(objects, isDefault);
|
|
1942
2071
|
}
|
|
@@ -1952,6 +2081,7 @@ function render3DRenderPass(after2D)
|
|
|
1952
2081
|
gl.depthMask(true);
|
|
1953
2082
|
gl.frontFace(gl.CCW);
|
|
1954
2083
|
gl.bindSampler(0, null); // back to the textures' own filtering for 2D
|
|
2084
|
+
render3DSetMapUnits(null); // the material maps leave no texture or sampler behind for 2D
|
|
1955
2085
|
if (glActiveTexture)
|
|
1956
2086
|
gl.bindTexture(gl.TEXTURE_2D, glActiveTexture);
|
|
1957
2087
|
// ARRAY_BUFFER is not part of VAO state in WebGL2, so bindVertexArray alone would not restore it
|
|
@@ -1960,6 +2090,21 @@ function render3DRenderPass(after2D)
|
|
|
1960
2090
|
}
|
|
1961
2091
|
}
|
|
1962
2092
|
|
|
2093
|
+
// put a texture on both material map units with no sampler, white at the start of the pass and none at its end, and
|
|
2094
|
+
// forget what the map cache thought was bound; unit 0 is active after
|
|
2095
|
+
function render3DSetMapUnits(texture)
|
|
2096
|
+
{
|
|
2097
|
+
const gl = glContext;
|
|
2098
|
+
for (const unit of [2, 3])
|
|
2099
|
+
{
|
|
2100
|
+
gl.activeTexture(gl.TEXTURE0 + unit);
|
|
2101
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
2102
|
+
gl.bindSampler(unit, null);
|
|
2103
|
+
}
|
|
2104
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
2105
|
+
render3DBoundMaps = [];
|
|
2106
|
+
}
|
|
2107
|
+
|
|
1963
2108
|
// create the shadow map depth texture and framebuffer at a size, or keep them when the size matches
|
|
1964
2109
|
function render3DUpdateShadowMap(size)
|
|
1965
2110
|
{
|
|
@@ -2036,6 +2181,65 @@ function render3DRenderShadowMap()
|
|
|
2036
2181
|
}
|
|
2037
2182
|
}
|
|
2038
2183
|
|
|
2184
|
+
// the camera's depth texture and its framebuffer at a size, made again when the canvas changes size; depth values
|
|
2185
|
+
// are not filtered, so it is read texel by texel
|
|
2186
|
+
function render3DUpdateCameraDepth(width, height)
|
|
2187
|
+
{
|
|
2188
|
+
const gl = glContext, r = render3D;
|
|
2189
|
+
if (r.cameraDepthTexture && r.cameraDepthWidth === width && r.cameraDepthHeight === height) return;
|
|
2190
|
+
r.cameraDepthTexture && gl.deleteTexture(r.cameraDepthTexture);
|
|
2191
|
+
r.cameraDepthFramebuffer && gl.deleteFramebuffer(r.cameraDepthFramebuffer);
|
|
2192
|
+
const texture = r.cameraDepthTexture = gl.createTexture();
|
|
2193
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
2194
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT24, width, height, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_INT, null);
|
|
2195
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
2196
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
2197
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
2198
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
2199
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
2200
|
+
const framebuffer = r.cameraDepthFramebuffer = gl.createFramebuffer();
|
|
2201
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
|
2202
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, texture, 0);
|
|
2203
|
+
gl.drawBuffers([gl.NONE]); // depth only
|
|
2204
|
+
gl.readBuffer(gl.NONE);
|
|
2205
|
+
ASSERT(gl.checkFramebufferStatus(gl.FRAMEBUFFER) == gl.FRAMEBUFFER_COMPLETE, 'camera depth framebuffer is incomplete');
|
|
2206
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
2207
|
+
r.cameraDepthWidth = width, r.cameraDepthHeight = height;
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
// draw the camera's depth for post processing: the solid objects from the camera with the shadow map's depth only
|
|
2211
|
+
// shader, into a texture the size of the canvas; what is see through is cut by its alpha as it is for shadows
|
|
2212
|
+
function render3DRenderDepth()
|
|
2213
|
+
{
|
|
2214
|
+
const gl = glContext, r = render3D, width = glCanvas.width, height = glCanvas.height;
|
|
2215
|
+
render3DUpdateCameraDepth(width, height);
|
|
2216
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, r.cameraDepthFramebuffer);
|
|
2217
|
+
gl.viewport(0, 0, width, height);
|
|
2218
|
+
gl.clear(gl.DEPTH_BUFFER_BIT);
|
|
2219
|
+
gl.useProgram(r.shadowShader);
|
|
2220
|
+
gl.uniformMatrix4fv(render3DUniform('viewProj', r.shadowShader), false, r.viewProjection.m);
|
|
2221
|
+
gl.enable(gl.DEPTH_TEST);
|
|
2222
|
+
gl.depthMask(true);
|
|
2223
|
+
gl.disable(gl.BLEND);
|
|
2224
|
+
gl.disable(gl.CULL_FACE);
|
|
2225
|
+
|
|
2226
|
+
r.shadowPass = r.depthPass = true;
|
|
2227
|
+
try
|
|
2228
|
+
{
|
|
2229
|
+
const solids = render3DLayerObjects(!!r.renderAfter2D).filter(o=> !o.additive && (!o.transparent || o.tileInfo));
|
|
2230
|
+
render3DDrawObjects(solids);
|
|
2231
|
+
r.onRenderOpaque?.();
|
|
2232
|
+
r.flush();
|
|
2233
|
+
render3DFlushInstances();
|
|
2234
|
+
}
|
|
2235
|
+
finally
|
|
2236
|
+
{
|
|
2237
|
+
r.shadowPass = r.depthPass = false;
|
|
2238
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
2239
|
+
gl.viewport(0, 0, width, height);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2039
2243
|
///////////////////////////////////////////////////////////////////////////////
|
|
2040
2244
|
// Strips: every strip repeats its first point once at the start and its last
|
|
2041
2245
|
// point once at the end. Those repeats make flat triangles with no area, which
|
|
@@ -2189,18 +2393,26 @@ function render3DStripTriangles(count, remap, place)
|
|
|
2189
2393
|
}
|
|
2190
2394
|
|
|
2191
2395
|
// 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
|
|
2396
|
+
// written into data when given, the buffer an earlier call returned for the same layout; it measures the mesh's
|
|
2397
|
+
// radius and box as it goes, as computeRadius does, over the vertices once instead of every strip entry
|
|
2193
2398
|
function render3DMeshVertexData(mesh, vertices, data=new ArrayBuffer(vertices.length * RENDER3D_VERTEX_BYTES))
|
|
2194
2399
|
{
|
|
2195
2400
|
const count = vertices.length;
|
|
2196
2401
|
const floats = new Float32Array(data), ints = new Uint32Array(data);
|
|
2402
|
+
let r = 0, x0 = Infinity, y0 = Infinity, z0 = Infinity, x1 = -Infinity, y1 = -Infinity, z1 = -Infinity;
|
|
2197
2403
|
for (let j = 0; j < count; ++j)
|
|
2198
2404
|
{
|
|
2199
2405
|
// a hand built mesh may leave normals, uvs and colors empty
|
|
2200
2406
|
const i = vertices[j], p = mesh.points[i], uv = mesh.uvs[i] || RENDER3D_DEFAULT_UV;
|
|
2201
|
-
|
|
2407
|
+
const x = p.x, y = p.y, z = p.z;
|
|
2408
|
+
render3DWriteVertex(floats, ints, j * RENDER3D_VERTEX_FLOATS, x, y, z,
|
|
2202
2409
|
mesh.normals[i] || RENDER3D_DEFAULT_NORMAL, uv.x, uv.y, (mesh.colors[i] || WHITE).rgbaInt());
|
|
2410
|
+
r = max(r, x*x + y*y + z*z);
|
|
2411
|
+
x0 = min(x0, x), y0 = min(y0, y), z0 = min(z0, z);
|
|
2412
|
+
x1 = max(x1, x), y1 = max(y1, y), z1 = max(z1, z);
|
|
2203
2413
|
}
|
|
2414
|
+
mesh.radius = r ** .5;
|
|
2415
|
+
mesh.bounds = count ? {min: vec3(x0, y0, z0), max: vec3(x1, y1, z1)} : {min: vec3(), max: vec3()};
|
|
2204
2416
|
return data;
|
|
2205
2417
|
}
|
|
2206
2418
|
|
|
@@ -2712,8 +2924,8 @@ class Mesh
|
|
|
2712
2924
|
* @return {Mesh} */
|
|
2713
2925
|
upload()
|
|
2714
2926
|
{
|
|
2715
|
-
|
|
2716
|
-
if (!render3D?.program || !glContext) return this;
|
|
2927
|
+
// the packing below measures the radius and box, with nothing to pack to they are measured here
|
|
2928
|
+
if (!render3D?.program || !glContext) return this.computeRadius(), this;
|
|
2717
2929
|
const gl = glContext, layout = this.vertexLayout;
|
|
2718
2930
|
// no layout when dynamicDraw was turned on after an upload, the next upload makes one
|
|
2719
2931
|
if (this.dynamicDraw && layout && this.buffer && this.contextGeneration === render3D.contextGeneration)
|
|
@@ -3215,25 +3427,30 @@ function buildSky(topColor=hsl(.6, .8, .55), horizonColor=hsl(.6, 1, .9), bottom
|
|
|
3215
3427
|
// a sphere turned inside out so it is seen from within, each point colored by how high it is
|
|
3216
3428
|
const mesh = buildSphere(2, sides, rings, true).flipNormals();
|
|
3217
3429
|
mesh.colors = mesh.points.map(p=> p.y < 0 ? horizonColor.lerp(bottomColor, -p.y) : horizonColor.lerp(topColor, p.y));
|
|
3430
|
+
render3DSkyColors.set(mesh, [topColor.copy(), horizonColor.copy(), bottomColor.copy()]);
|
|
3218
3431
|
return mesh;
|
|
3219
3432
|
}
|
|
3220
3433
|
|
|
3434
|
+
// the top, horizon and bottom colors of each sky dome buildSky made, which a reflection shows while that dome is
|
|
3435
|
+
// render3D.sky, whether setSky set it or the game did
|
|
3436
|
+
const render3DSkyColors = new WeakMap;
|
|
3437
|
+
|
|
3221
3438
|
///////////////////////////////////////////////////////////////////////////////
|
|
3222
3439
|
/**
|
|
3223
3440
|
* EngineObject3D - An EngineObject with a 3D transform and a mesh
|
|
3224
3441
|
* - Set pos3D, rotation3D and scale3D instead of the 2D pos, size and angle
|
|
3225
3442
|
* - Gets update, children, timers, destroy and renderOrder from EngineObject
|
|
3226
|
-
* - velocity3D is added to pos3D each frame,
|
|
3443
|
+
* - velocity3D is added to pos3D each frame, slowed by damping, and render3D.gravity pulls it once it has a mass
|
|
3227
3444
|
* - Objects face -Z, the same way the camera does, so lookAt turns them to face a point
|
|
3228
3445
|
* - 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
|
-
*
|
|
3446
|
+
* - These inherited fields are 2D only and do nothing here: angle, angleVelocity, additiveColor, drawSize and mirror;
|
|
3447
|
+
* damping, angleDamping, clampSpeed, friction and groundObject work as in 2D, on velocity3D and angleVelocity3D
|
|
3231
3448
|
* - The inherited shader works here as in 2D, and with emissive at 1 its snippet does its own lighting
|
|
3232
3449
|
* - Set sync2D for a 2D game with 3D looks, pos and angle then drive pos3D and rotation3D,
|
|
3233
3450
|
* which is the one way those 2D fields reach a 3D object
|
|
3234
3451
|
* - setCollision takes the same flags as in 2D, but the solid collision happens in 3D against size3D
|
|
3235
|
-
* - The solid box
|
|
3236
|
-
*
|
|
3452
|
+
* - The solid box turns with rotation3D, so a turned wall or a ramp collides as it looks; resting on one no steeper
|
|
3453
|
+
* than groundAngle stands there
|
|
3237
3454
|
* - Its tile and raycast halves are 2D only so they default off here, and a child sits solid collision out
|
|
3238
3455
|
* - A sync2D object collides in 2D instead, which needs the 2D size set as well as size3D
|
|
3239
3456
|
* - setMesh swaps the mesh and frees the old one, for text and terrain that get built again
|
|
@@ -3281,7 +3498,7 @@ class EngineObject3D extends EngineObject
|
|
|
3281
3498
|
/** @property {Vector3} - Added to pos3D each frame by the engine before update, like the 2D velocity, no super
|
|
3282
3499
|
* call needed; damping and render3D.gravity act on it once the object has a mass */
|
|
3283
3500
|
this.velocity3D = vec3();
|
|
3284
|
-
/** @property {Vector3} - Added to rotation3D each frame by the engine before update,
|
|
3501
|
+
/** @property {Vector3} - Added to rotation3D each frame by the engine before update, slowed by angleDamping */
|
|
3285
3502
|
this.angleVelocity3D = vec3();
|
|
3286
3503
|
/** @property {Mesh|undefined} - Mesh to draw
|
|
3287
3504
|
* @type {Mesh|undefined} */
|
|
@@ -3312,8 +3529,27 @@ class EngineObject3D extends EngineObject
|
|
|
3312
3529
|
* lamps and glowing things, between is partly self lit, and above 1 is brighter than its color, for bloom */
|
|
3313
3530
|
this.emissive = 0;
|
|
3314
3531
|
/** @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
|
|
3532
|
+
* 1 adds a light's full color at its brightest; shininess sets its size */
|
|
3316
3533
|
this.specular = 0;
|
|
3534
|
+
/** @property {number} - The highlight's exponent, how small and sharp it is: 4 is broad like rubber, 16 the
|
|
3535
|
+
* default, 100 sharp like polished metal; shows only with specular above 0 */
|
|
3536
|
+
this.shininess = 16;
|
|
3537
|
+
/** @property {TextureInfo|undefined} - A normal map that bends the surface at each texel so it catches the
|
|
3538
|
+
* light like bumps and grooves, green pointing up the image as OpenGL and glTF have it; read at the color
|
|
3539
|
+
* texture's coordinates, see normalMapFromHeight to make one in code
|
|
3540
|
+
* @type {TextureInfo|undefined} */
|
|
3541
|
+
this.normalMap = undefined;
|
|
3542
|
+
/** @property {number} - How strongly the normal map bends the surface, 0 turns it off, as glTF's scale */
|
|
3543
|
+
this.normalScale = 1;
|
|
3544
|
+
/** @property {number} - How much it reflects the sky, 0 none and 1 a mirror of it; the edges seen at a
|
|
3545
|
+
* glancing angle reflect more either way, as water and glass do */
|
|
3546
|
+
this.reflectivity = 0;
|
|
3547
|
+
/** @property {TextureInfo|undefined} - A texture of where it glows, added on top of the lit surface so it
|
|
3548
|
+
* shows in the dark, like lit windows; read at the color texture's coordinates
|
|
3549
|
+
* @type {TextureInfo|undefined} */
|
|
3550
|
+
this.emissiveMap = undefined;
|
|
3551
|
+
/** @property {Color} - Multiplies the emissive map, as glTF's emissiveFactor */
|
|
3552
|
+
this.emissiveMapColor = WHITE;
|
|
3317
3553
|
/** @property {boolean} - Draw into the shadow map when render3D.shadows is on; sprites and cut out textures
|
|
3318
3554
|
* cast their outline, an object faded below half its alpha casts nothing, a see through one casts only when
|
|
3319
3555
|
* textured, and additive objects never cast */
|
|
@@ -3321,6 +3557,9 @@ class EngineObject3D extends EngineObject
|
|
|
3321
3557
|
/** @property {boolean} - Collide as the sphere that fits size3D instead of as the size3D box, so it rolls
|
|
3322
3558
|
* around corners */
|
|
3323
3559
|
this.collideAsSphere3D = false;
|
|
3560
|
+
/** @property {number} - The steepest slope it stands on, in radians from level, PI/4 by default: resting on
|
|
3561
|
+
* a solid within this of flat sets groundObject and holds it still, steeper it slides down */
|
|
3562
|
+
this.groundAngle = PI / 4;
|
|
3324
3563
|
/** @property {boolean} - Darkened by the shadow map when render3D.shadows is on */
|
|
3325
3564
|
this.receiveShadow = true;
|
|
3326
3565
|
/** @property {boolean|undefined} - Draw this object over the 2D scene, undefined uses render3D.renderAfter2D
|
|
@@ -3351,10 +3590,32 @@ class EngineObject3D extends EngineObject
|
|
|
3351
3590
|
'a sync2D object collides in 2D, so give it a 2D size as well as a size3D', this.size);
|
|
3352
3591
|
if (this.sync2D)
|
|
3353
3592
|
super.updatePhysics();
|
|
3593
|
+
ASSERT(isNumber(this.groundAngle) && this.groundAngle >= 0 && this.groundAngle < PI / 2,
|
|
3594
|
+
'groundAngle must be 0 to less than PI/2, a slope from level', this.groundAngle);
|
|
3595
|
+
// what it stands on is found again each frame, by the level and by the solids it rests on; a sync2D
|
|
3596
|
+
// object's is the 2D physics'
|
|
3597
|
+
const ground = this.groundObject;
|
|
3598
|
+
this.sync2D || (this.groundObject = undefined);
|
|
3599
|
+
if (this.clampSpeed && !this.sync2D && (this.collideSolidObjects || this.collideLevel && this.mass))
|
|
3600
|
+
{
|
|
3601
|
+
// each axis within objectMaxSpeed, as in 2D, so a fast object does not pass through a thin wall; only
|
|
3602
|
+
// for what collides, anything else moves as fast as it is told
|
|
3603
|
+
const v = this.velocity3D, s = objectMaxSpeed;
|
|
3604
|
+
v.x = clamp(v.x, -s, s), v.y = clamp(v.y, -s, s), v.z = clamp(v.z, -s, s);
|
|
3605
|
+
}
|
|
3354
3606
|
// a moving object keeps out of the level, the height maps and voxel maps, from where it was before it moved
|
|
3355
3607
|
const oldPos = this.collideLevel && this.mass && !this.sync2D ? this.pos3D.copy() : undefined;
|
|
3356
3608
|
render3DMove(this);
|
|
3357
|
-
|
|
3609
|
+
if (ground && this.mass && !this.sync2D)
|
|
3610
|
+
{
|
|
3611
|
+
// sliding on what it stood on slows by friction, the less grippy of the two, relative to that one's own
|
|
3612
|
+
// speed so a moving platform carries it, as in 2D
|
|
3613
|
+
const friction = max(this.friction, ground.friction), v = this.velocity3D;
|
|
3614
|
+
const moving = ground instanceof EngineObject3D ? ground.velocity3D : undefined;
|
|
3615
|
+
const gx = moving?.x ?? 0, gz = moving?.z ?? 0;
|
|
3616
|
+
v.x = gx + (v.x - gx) * friction, v.z = gz + (v.z - gz) * friction;
|
|
3617
|
+
}
|
|
3618
|
+
oldPos && render3DCollideLevel(this, oldPos, ground);
|
|
3358
3619
|
// the engine only runs this for objects that own where they are, a child rides along with its parent
|
|
3359
3620
|
if (this.collideSolidObjects && !this.sync2D)
|
|
3360
3621
|
render3DCollideSolid(this);
|
|
@@ -3387,7 +3648,7 @@ class EngineObject3D extends EngineObject
|
|
|
3387
3648
|
|
|
3388
3649
|
/** Set how this object collides, the same flags as in 2D
|
|
3389
3650
|
* - Solid collision happens in 3D here, against size3D boxes or spheres; a child sits it out
|
|
3390
|
-
* - The boxes
|
|
3651
|
+
* - The boxes turn with rotation3D, against height maps and voxel maps an object is still its upright box
|
|
3391
3652
|
* - A sync2D object collides in 2D instead, against the 2D size, so set that as well as size3D
|
|
3392
3653
|
* @param {boolean} [collideSolidObjects] - Take part in solid collision
|
|
3393
3654
|
* @param {boolean} [isSolid] - Block other objects, a pair where neither one blocks passes through;
|
|
@@ -3575,27 +3836,29 @@ function render3DObjectMatrix(o)
|
|
|
3575
3836
|
return o.worldMatrix;
|
|
3576
3837
|
}
|
|
3577
3838
|
|
|
3578
|
-
// move an object by its 3D velocities, an object with mass falling with render3D.gravity
|
|
3839
|
+
// move an object by its 3D velocities, each slowed by its damping, an object with mass falling with render3D.gravity
|
|
3579
3840
|
function render3DMove(o)
|
|
3580
3841
|
{
|
|
3581
3842
|
// the vectors change in place, as the 2D object's do: this runs for every object every frame
|
|
3843
|
+
// a sync2D object's damping is the 2D physics', its 3D velocities are its own to set
|
|
3582
3844
|
const p = o.pos3D, v = o.velocity3D, r = o.rotation3D, a = o.angleVelocity3D;
|
|
3845
|
+
const d = o.sync2D ? 1 : o.damping, e = o.sync2D ? 1 : o.angleDamping;
|
|
3846
|
+
// damped first and gravity added after, the order EngineObject.updatePhysics uses,
|
|
3847
|
+
// so the same mass, damping and gravity fall the same way in both
|
|
3848
|
+
v.x *= d, v.y *= d, v.z *= d;
|
|
3583
3849
|
if (o.mass && !o.sync2D) // a 2D driven object gets the 2D gravity instead
|
|
3584
3850
|
{
|
|
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;
|
|
3851
|
+
const g = render3D.gravity, s = o.gravityScale;
|
|
3852
|
+
v.x += g.x * s, v.y += g.y * s, v.z += g.z * s;
|
|
3589
3853
|
}
|
|
3590
3854
|
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;
|
|
3855
|
+
r.x += a.x *= e, r.y += a.y *= e, r.z += a.z *= e;
|
|
3592
3856
|
}
|
|
3593
3857
|
|
|
3594
3858
|
// keep an object that moved out of the level's solid geometry, clearing what it stood on for the level to set again
|
|
3595
|
-
|
|
3859
|
+
// ground is what it stood on last frame, which a height map keeps it on going down a slope
|
|
3860
|
+
function render3DCollideLevel(o, oldPos, ground)
|
|
3596
3861
|
{
|
|
3597
|
-
const ground = o.groundObject;
|
|
3598
|
-
o.groundObject = undefined;
|
|
3599
3862
|
for (const level of render3DLevel)
|
|
3600
3863
|
level.destroyed || level.levelCollide3D(o, oldPos, level === ground);
|
|
3601
3864
|
}
|
|
@@ -3612,7 +3875,23 @@ function render3DSolidShape(o)
|
|
|
3612
3875
|
const kx = abs(k.x), ky = abs(k.y), kz = abs(k.z);
|
|
3613
3876
|
if (o.collideAsSphere3D)
|
|
3614
3877
|
return {pos: o.pos3D.copy(), radius: max(s.x, s.y, s.z) / 2 * max(kx, ky, kz)};
|
|
3615
|
-
|
|
3878
|
+
// 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
|
|
3879
|
+
// neither does a sprite, whose rotation turns how it faces the camera, not its box
|
|
3880
|
+
const axes = !render3DIsSprite(o) && isTurned3D(o.rotation3D) ? render3DSolidAxes(o) : undefined;
|
|
3881
|
+
return {pos: o.pos3D.copy(), size: vec3(s.x * kx, s.y * ky, s.z * kz), axes};
|
|
3882
|
+
}
|
|
3883
|
+
|
|
3884
|
+
// a turned solid's three axes, worked out when its rotation changes and kept until then, so the pairs it is tested
|
|
3885
|
+
// against each frame do not each work them out again
|
|
3886
|
+
const render3DSolidAxesCache = new WeakMap;
|
|
3887
|
+
function render3DSolidAxes(o)
|
|
3888
|
+
{
|
|
3889
|
+
const r = o.rotation3D, kept = render3DSolidAxesCache.get(o);
|
|
3890
|
+
if (kept && kept.x === r.x && kept.y === r.y && kept.z === r.z)
|
|
3891
|
+
return kept.axes;
|
|
3892
|
+
const axes = boxAxes3D(r);
|
|
3893
|
+
render3DSolidAxesCache.set(o, {x: r.x, y: r.y, z: r.z, axes});
|
|
3894
|
+
return axes;
|
|
3616
3895
|
}
|
|
3617
3896
|
|
|
3618
3897
|
// how far a solid shape can reach from its own center, for a quick reject before the exact test
|
|
@@ -3630,15 +3909,30 @@ function render3DSolidReach(o)
|
|
|
3630
3909
|
// what it takes to move shape a clear of shape b, whichever pair of shapes they are, or undefined for no touch
|
|
3631
3910
|
function render3DSolidPush(a, b)
|
|
3632
3911
|
{
|
|
3912
|
+
// a turned box is tested by its kept axes, the upright pairs as they always were
|
|
3913
|
+
const sphereBox = (sphere, box)=> box.axes ? collideSphereOrientedBox3D(sphere.pos, sphere.radius, box.pos,
|
|
3914
|
+
box.size, box.axes) : collideSphereBox(sphere.pos, sphere.radius, box.pos, box.size);
|
|
3633
3915
|
if (!a.size) // a is a sphere
|
|
3634
|
-
return b.size ?
|
|
3635
|
-
: collideSphereSphere(a.pos, a.radius, b.pos, b.radius);
|
|
3916
|
+
return b.size ? sphereBox(a, b) : collideSphereSphere(a.pos, a.radius, b.pos, b.radius);
|
|
3636
3917
|
if (!b.size) // only b is, so push b out of a and turn it around
|
|
3637
3918
|
{
|
|
3638
|
-
const push =
|
|
3919
|
+
const push = sphereBox(b, a);
|
|
3639
3920
|
return push && push.scale(-1);
|
|
3640
3921
|
}
|
|
3641
|
-
return
|
|
3922
|
+
return a.axes || b.axes ? collideOrientedBoxes3D(a.pos, a.size, a.axes ?? BOX_WORLD_AXES, b.pos, b.size,
|
|
3923
|
+
b.axes ?? BOX_WORLD_AXES) : collideBoxBox3D(a.pos, a.size, b.pos, b.size);
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
// a sprite, a tile with no mesh, which faces the camera however it is turned
|
|
3927
|
+
const render3DIsSprite = (o)=> !o.mesh && !!o.tileInfo;
|
|
3928
|
+
|
|
3929
|
+
// 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
|
|
3930
|
+
// no face to stand on
|
|
3931
|
+
function render3DOnFace(push, shape)
|
|
3932
|
+
{
|
|
3933
|
+
if (!shape.size) return false;
|
|
3934
|
+
const n = push.normalize(), axes = shape.axes ?? BOX_WORLD_AXES;
|
|
3935
|
+
return max(abs(n.dot(axes[0])), abs(n.dot(axes[1])), abs(n.dot(axes[2]))) > 1 - 1e-6;
|
|
3642
3936
|
}
|
|
3643
3937
|
|
|
3644
3938
|
// 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 +3958,8 @@ function render3DCollideSolid(a)
|
|
|
3664
3958
|
if (dx*dx + dy*dy + dz*dz > reach*reach)
|
|
3665
3959
|
continue;
|
|
3666
3960
|
|
|
3667
|
-
const
|
|
3961
|
+
const shapeB = render3DSolidShape(b);
|
|
3962
|
+
let push = render3DSolidPush(shapeA, shapeB);
|
|
3668
3963
|
if (!push) continue;
|
|
3669
3964
|
|
|
3670
3965
|
// both objects hear about it, and either one can take the touch over
|
|
@@ -3672,6 +3967,19 @@ function render3DCollideSolid(a)
|
|
|
3672
3967
|
const resolveB = b.collideWithObject(a, push.scale(-1));
|
|
3673
3968
|
if (!resolveA || !resolveB) continue;
|
|
3674
3969
|
|
|
3970
|
+
// standing: resting on a box's face within the upper one's groundAngle of level holds it there, the push
|
|
3971
|
+
// turned straight up, as far as it takes to leave the surface, so it does not creep down a ramp; only what
|
|
3972
|
+
// moves stands, and a sphere, an edge or a corner is nothing to stand on, what rests there rolls off
|
|
3973
|
+
const lengthSquared = push.lengthSquared(), up = push.y / lengthSquared ** .5;
|
|
3974
|
+
const aStands = up > 0 && a.mass && up >= cos(a.groundAngle) && render3DOnFace(push, shapeB);
|
|
3975
|
+
const bStands = up < 0 && b.mass && -up >= cos(b.groundAngle) && render3DOnFace(push, shapeA);
|
|
3976
|
+
if (aStands || bStands)
|
|
3977
|
+
{
|
|
3978
|
+
aStands ? a.groundObject = b : b.groundObject = a;
|
|
3979
|
+
if (push.x || push.z) // one straight up already is used as it is
|
|
3980
|
+
push = vec3(0, lengthSquared / push.y, 0);
|
|
3981
|
+
}
|
|
3982
|
+
|
|
3675
3983
|
// heavier objects move less, mass 0 stays put; then bounce apart when moving toward each other
|
|
3676
3984
|
const total = a.mass + b.mass;
|
|
3677
3985
|
const weightA = !a.mass ? 0 : !b.mass ? 1 : b.mass / total;
|
|
@@ -3692,8 +4000,8 @@ function render3DCollideSolid(a)
|
|
|
3692
4000
|
|
|
3693
4001
|
/**
|
|
3694
4002
|
* Collect the EngineObject3D objects whose boxes overlap a sphere or a box, the 3D twin of engineObjectsCollect
|
|
3695
|
-
* -
|
|
3696
|
-
* and are never collected
|
|
4003
|
+
* - Each object is its size3D box at its world position, turned as it is turned; lights, emitters and trails have no
|
|
4004
|
+
* size and are never collected
|
|
3697
4005
|
* @param {Vector3} pos - Center of the area
|
|
3698
4006
|
* @param {Vector3|number} size - Diameter of a sphere if a number, 0 for a point, full size of a box if a Vector3
|
|
3699
4007
|
* @param {Array<EngineObject>} [objects] - Defaults to every object
|
|
@@ -3715,9 +4023,27 @@ function engineObjectsCollect3D(pos, size, objects=engineObjects, testCenters=fa
|
|
|
3715
4023
|
if (!(s.x || s.y || s.z)) continue;
|
|
3716
4024
|
const center = vec3(m[12], m[13], m[14]);
|
|
3717
4025
|
const worldSize = testCenters ? vec3() : render3DWorldSize(o, m);
|
|
4026
|
+
// a turned box is tested as turned, its axes read off the matrix, its columns with the scale taken out
|
|
4027
|
+
const turned = !testCenters && !render3DIsSprite(o) && (m[1] || m[2] || m[4] || m[6] || m[8] || m[9]);
|
|
4028
|
+
const axis = (i)=> vec3(m[i], m[i+1], m[i+2]).normalize();
|
|
4029
|
+
let axes = turned ? [axis(0), axis(4), axis(8)] : undefined;
|
|
4030
|
+
if (axes && (abs(axes[0].dot(axes[1])) > 1e-6 || abs(axes[0].dot(axes[2])) > 1e-6 ||
|
|
4031
|
+
abs(axes[1].dot(axes[2])) > 1e-6))
|
|
4032
|
+
{
|
|
4033
|
+
// a turned child of an unevenly scaled parent is sheared, its edges no longer square, which the box
|
|
4034
|
+
// tests do not take; the upright box around it stands in, which may take a little more but never misses
|
|
4035
|
+
const e = axes.map((a, i)=> a.scale(i ? i > 1 ? worldSize.z / 2 : worldSize.y / 2 : worldSize.x / 2));
|
|
4036
|
+
const reach = (c)=> 2 * (abs(e[0][c]) + abs(e[1][c]) + abs(e[2][c]));
|
|
4037
|
+
worldSize.set(reach('x'), reach('y'), reach('z'));
|
|
4038
|
+
axes = undefined;
|
|
4039
|
+
}
|
|
3718
4040
|
let hit;
|
|
3719
|
-
if (box)
|
|
4041
|
+
if (box && axes)
|
|
4042
|
+
hit = !!collideOrientedBoxes3D(pos, box, BOX_WORLD_AXES, center, worldSize, axes);
|
|
4043
|
+
else if (box)
|
|
3720
4044
|
hit = isOverlapping3D(pos, box, center, worldSize);
|
|
4045
|
+
else if (axes)
|
|
4046
|
+
hit = !!collideSphereOrientedBox3D(pos, radiusSquared ** .5, center, worldSize, axes);
|
|
3721
4047
|
else
|
|
3722
4048
|
{
|
|
3723
4049
|
// a sphere against the nearest point of the box
|
|
@@ -4040,10 +4366,53 @@ class Light3D extends EngineObject3D
|
|
|
4040
4366
|
/** @property {boolean} - Shine from far away, from its position toward the origin, instead of out from its
|
|
4041
4367
|
* position with a falloff; DirectionalLight3D sets it */
|
|
4042
4368
|
this.directional = false;
|
|
4369
|
+
/** @property {number} - Size of a soft hazy glow drawn over the light, like a lamp at night, 0 for none; it
|
|
4370
|
+
* is added onto what is behind it, and what is in front of the light hides it */
|
|
4371
|
+
this.glow = 0;
|
|
4372
|
+
/** @property {number} - How fast the glow fades from its middle: 1 by default, .5 a wide haze, 2 a tight
|
|
4373
|
+
* bright core */
|
|
4374
|
+
this.glowFalloff = 1;
|
|
4375
|
+
this.additive = true; // the glow is added on, in the transparent stage; a light with none draws nothing
|
|
4043
4376
|
}
|
|
4044
4377
|
|
|
4045
|
-
/**
|
|
4046
|
-
|
|
4378
|
+
/** Draw the glow, a quad facing the camera with a soft round glow on it, pulled toward the camera by half its
|
|
4379
|
+
* size so a lamp at the light does not cut into it, but never past the camera */
|
|
4380
|
+
render3D()
|
|
4381
|
+
{
|
|
4382
|
+
if (!(this.glow > 0) || this.directional) return;
|
|
4383
|
+
const r = render3D, c = this.color, pos = render3DObjectMatrix(this).getTranslation();
|
|
4384
|
+
const toCamera = r.camera.pos.subtract(pos), distance = toCamera.length();
|
|
4385
|
+
const at = distance ? pos.add(toCamera.scale(min(this.glow, distance) / 2 / distance)) : pos;
|
|
4386
|
+
const color = rgb(c.r, c.g, c.b, c.a * min(this.intensity, 1));
|
|
4387
|
+
r.drawBillboard(at, vec2(this.glow), render3DGlow(this.glowFalloff), color);
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
4390
|
+
|
|
4391
|
+
// how strong a light's glow is at a distance from its middle, 0 there to 1 at the edge: a bell, full in the middle
|
|
4392
|
+
// and nothing at the edge, fading faster the higher the falloff
|
|
4393
|
+
function render3DGlowAlpha(r, falloff)
|
|
4394
|
+
{
|
|
4395
|
+
const k = 3.5 * falloff, edge = Math.exp(-k);
|
|
4396
|
+
return (Math.exp(-k * r * r) - edge) / (1 - edge);
|
|
4397
|
+
}
|
|
4398
|
+
|
|
4399
|
+
// the soft round glow of the lights, one texture for each falloff, rounded to a tenth so a changing falloff makes
|
|
4400
|
+
// only a few, each made once from a canvas; undefined headless or without a canvas
|
|
4401
|
+
const render3DGlowTextures = new Map;
|
|
4402
|
+
function render3DGlow(falloff=1)
|
|
4403
|
+
{
|
|
4404
|
+
ASSERT(isNumber(falloff) && falloff > 0, 'glowFalloff must be a number above 0');
|
|
4405
|
+
const key = max(round(falloff * 10), 1) / 10;
|
|
4406
|
+
let texture = render3DGlowTextures.get(key);
|
|
4407
|
+
if (texture || !glContext || typeof OffscreenCanvas == 'undefined') return texture;
|
|
4408
|
+
const size = 64, context = createCanvasContext(size), steps = 16;
|
|
4409
|
+
const gradient = context.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
|
|
4410
|
+
for (let i = 0; i <= steps; ++i)
|
|
4411
|
+
gradient.addColorStop(i / steps, 'rgba(255,255,255,' + render3DGlowAlpha(i / steps, key).toFixed(4) + ')');
|
|
4412
|
+
context.fillStyle = gradient;
|
|
4413
|
+
context.fillRect(0, 0, size, size);
|
|
4414
|
+
render3DGlowTextures.set(key, texture = new TextureInfo(context.canvas));
|
|
4415
|
+
return texture;
|
|
4047
4416
|
}
|
|
4048
4417
|
|
|
4049
4418
|
///////////////////////////////////////////////////////////////////////////////
|