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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "littlejsengine",
3
- "version": "1.20.0",
3
+ "version": "1.21.0",
4
4
  "description": "LittleJS - Tiny and Fast HTML5 Game Engine",
5
5
  "main": "dist/littlejs.esm.js",
6
6
  "types": "dist/littlejs.d.ts",
package/plugins/gltf.js CHANGED
@@ -8,6 +8,8 @@
8
8
  * GLTFObject that createObject makes; a skinned character's walk is not read
9
9
  * - Materials give a base color and texture and whether they blend; glass made with KHR_materials_transmission blends too,
10
10
  * and a KHR_materials_unlit material comes in emissive, its own color with no shading
11
+ * - A material's normal map and emissive map load too, with its normal scale and emissive factor, read at the base
12
+ * color texture's uvs; roughness, metalness and occlusion maps are not loaded
11
13
  * - The base color texture reads the uv set its texCoord names, moved by KHR_texture_transform as gltfpack and
12
14
  * Blender write it
13
15
  * - An OPAQUE material, the default, ignores its texture's alpha as the format says: a texture only such materials
@@ -56,6 +58,17 @@ class GLTFPart
56
58
  /** @property {boolean} - The material is unlit (KHR_materials_unlit), its own color with no shading; the object
57
59
  * createObject makes draws it with emissive 1 */
58
60
  this.unlit = false;
61
+ /** @property {TextureInfo|undefined} - The material's normal map, drawn with the base color texture's uvs
62
+ * @type {TextureInfo|undefined} */
63
+ this.normalMap = undefined;
64
+ /** @property {number} - The normal map's strength, its scale in the file */
65
+ this.normalScale = 1;
66
+ /** @property {TextureInfo|undefined} - The material's emissive map, or a white texture when it has an
67
+ * emissiveFactor and no texture, so it glows all over
68
+ * @type {TextureInfo|undefined} */
69
+ this.emissiveMap = undefined;
70
+ /** @property {Color} - The emissiveFactor, which multiplies the emissive map */
71
+ this.emissiveMapColor = WHITE;
59
72
  }
60
73
  }
61
74
 
@@ -201,8 +214,8 @@ class GLTFModel
201
214
  for (const part of this.parts)
202
215
  part.mesh.dispose();
203
216
  this.mesh.dispose();
204
- for (const textureInfo of new Set(this.parts.map(p=> p.textureInfo)))
205
- textureInfo?.destroyWebGLTexture();
217
+ for (const textureInfo of new Set(this.parts.flatMap(p=> [p.textureInfo, p.normalMap, p.emissiveMap])))
218
+ textureInfo !== gltfWhiteTextureInfo && textureInfo?.destroyWebGLTexture(); // the white one is shared
206
219
  }
207
220
 
208
221
  /** Make an object at a position with a child per part, so each keeps its own texture, color and blending, and
@@ -260,6 +273,8 @@ class GLTFObject extends EngineObject3D
260
273
  o.transparent = part.transparent;
261
274
  o.pixelated = part.pixelated;
262
275
  o.emissive = part.unlit ? 1 : 0;
276
+ o.normalMap = part.normalMap, o.normalScale = part.normalScale;
277
+ o.emissiveMap = part.emissiveMap, o.emissiveMapColor = part.emissiveMapColor.copy(); // its own, as color is
263
278
  const rest = model.nodeTree?.restPose?.[part.node];
264
279
  if (rest)
265
280
  {
@@ -396,13 +411,15 @@ async function parseGLTF(data, baseUrl='')
396
411
  return gltfFetch(buffer.uri, baseUrl).then(r=> r.arrayBuffer());
397
412
  }));
398
413
 
399
- // the textures, decoded together first; none without WebGL, and a failed image only logs; only the base color
400
- // textures are drawn with, so the normal, roughness and other maps are not loaded at all
401
- const baseColorTextures = new Set((json.materials || []).map(m=> m.pbrMetallicRoughness?.baseColorTexture?.index));
414
+ // the textures, decoded together first; none without WebGL, and a failed image only logs; the base color, normal
415
+ // and emissive textures are drawn with, the roughness and occlusion maps are not loaded at all
416
+ const usedTextures = new Set((json.materials || []).flatMap(m=> [m.pbrMetallicRoughness?.baseColorTexture?.index,
417
+ m.normalTexture?.index, m.emissiveTexture?.index]));
418
+ const normalTextures = new Set((json.materials || []).map(m=> m.normalTexture?.index));
402
419
  const opaqueTextures = gltfOpaqueTextures(json);
403
420
  const textures = await Promise.all((json.textures || []).map(async (texture, index)=>
404
421
  {
405
- if (!glContext || typeof createImageBitmap == 'undefined' || !baseColorTextures.has(index)) return;
422
+ if (!glContext || typeof createImageBitmap == 'undefined' || !usedTextures.has(index)) return;
406
423
  try
407
424
  {
408
425
  // a WebP or AVIF image is named by its extension, the browser decodes those like any other
@@ -419,7 +436,11 @@ async function parseGLTF(data, baseUrl='')
419
436
  // an OPAQUE material ignores its texture's alpha, which the 3D pass would cut holes by, so a texture only
420
437
  // those use has it set to 1; decoded as stored for that, and a jpeg has no alpha to set
421
438
  const opaque = opaqueTextures.has(index) && blob.type !== 'image/jpeg';
422
- const bitmap = opaque ? await createImageBitmap(blob, {premultiplyAlpha: 'none'}).then(gltfOpaqueImage) : await createImageBitmap(blob);
439
+ // a normal map is directions, not colors, so it is decoded as stored; one a material also uses as a
440
+ // color, an odd file, is decoded that way too
441
+ const bitmap = normalTextures.has(index) ?
442
+ await createImageBitmap(blob, {colorSpaceConversion: 'none', premultiplyAlpha: 'none'}) :
443
+ opaque ? await createImageBitmap(blob, {premultiplyAlpha: 'none'}).then(gltfOpaqueImage) : await createImageBitmap(blob);
423
444
  return new TextureInfo(bitmap, true, [sampler.wrapS ?? 10497, sampler.wrapT ?? 10497]); // REPEAT by default
424
445
  }
425
446
  catch (e) { LOG('glTF image not loaded', e); }
@@ -495,7 +516,7 @@ async function parseGLTF(data, baseUrl='')
495
516
  }
496
517
 
497
518
  // a texture none of the parts draws with, another scene's, goes now too, since dispose finds them by the parts
498
- const used = new Set(parts.map(p=> p.textureInfo));
519
+ const used = new Set(parts.flatMap(p=> [p.textureInfo, p.normalMap, p.emissiveMap]));
499
520
  for (const texture of textures)
500
521
  texture && !used.has(texture) && texture.destroyWebGLTexture();
501
522
  return new GLTFModel(parts, animations, {nodes: json.nodes, parents, restInverse, restPose});
@@ -731,9 +752,34 @@ function gltfPart(json, buffers, textures, primitive, matrix, name)
731
752
  textureRef ? textures[textureRef.index] : undefined, blend || transmission > 0);
732
753
  part.pixelated = json.samplers?.[texture?.sampler]?.magFilter === 9728; // NEAREST
733
754
  part.unlit = !!material.extensions?.KHR_materials_unlit;
755
+
756
+ // the normal and emissive maps, read at the base color texture's uvs; the emissive texture is multiplied by the
757
+ // factor, which is black by default as the format says, so a texture alone does not glow; a factor with no
758
+ // texture glows all over
759
+ const normalRef = material.normalTexture, emissiveRef = material.emissiveTexture;
760
+ part.normalMap = normalRef && textures[normalRef.index];
761
+ part.normalScale = normalRef?.scale ?? 1;
762
+ const [er, eg, eb] = material.emissiveFactor || [0, 0, 0];
763
+ if (er || eg || eb)
764
+ {
765
+ part.emissiveMap = emissiveRef ? textures[emissiveRef.index] : gltfWhiteTexture();
766
+ part.emissiveMapColor = rgb(gltfSRGB(er), gltfSRGB(eg), gltfSRGB(eb));
767
+ }
734
768
  return part;
735
769
  }
736
770
 
771
+ // a 1 by 1 white texture, the emissive map of a material that glows all over with no texture; made once and never
772
+ // freed, like the engine's own white texture, undefined without WebGL
773
+ let gltfWhiteTextureInfo;
774
+ function gltfWhiteTexture()
775
+ {
776
+ if (gltfWhiteTextureInfo || !glContext || typeof OffscreenCanvas == 'undefined') return gltfWhiteTextureInfo;
777
+ const context = createCanvasContext(1);
778
+ context.fillStyle = '#fff';
779
+ context.fillRect(0, 0, 1, 1);
780
+ return gltfWhiteTextureInfo = new TextureInfo(context.canvas);
781
+ }
782
+
737
783
  // a glTF color factor or vertex color is linear, where the renderer works in sRGB like the textures, so it is
738
784
  // brought across at load or a mid gray material would come out nearly black; 0 and 1 stay exact
739
785
  function gltfSRGB(c)
package/plugins/math3d.js CHANGED
@@ -652,36 +652,62 @@ class Ray3D
652
652
 
653
653
  ///////////////////////////////////////////////////////////////////////////////
654
654
  // 3D collision helpers, none of them change anything that is passed in
655
- // Boxes sit centered on pos and take a full size, like drawRect
655
+ // Boxes sit centered on pos and take a full size, like drawRect, upright unless given a rotation, an Euler vec3 like
656
+ // rotation3D, which turns them about their center
656
657
  // Cylinders stand up the Y axis, centered on pos, with a full height
657
658
  // Names that could be mistaken for 2D functions get a 3D suffix
658
659
 
660
+ // whether a rotation turns anything, a missing one does not
661
+ const isTurned3D = (rotation)=> !!rotation && !!(rotation.x || rotation.y || rotation.z);
662
+
663
+ // a turned box's three unit axes, the columns of its rotation
664
+ function boxAxes3D(rotation)
665
+ {
666
+ const m = buildMatrix(undefined, rotation).m;
667
+ return [vec3(m[0], m[1], m[2]), vec3(m[4], m[5], m[6]), vec3(m[8], m[9], m[10])];
668
+ }
669
+
670
+ // a point in a turned box's own space, from its center, and a vector from that space back to the world's
671
+ function boxLocal3D(point, pos, axes)
672
+ {
673
+ const d = point.subtract(pos);
674
+ return vec3(d.dot(axes[0]), d.dot(axes[1]), d.dot(axes[2]));
675
+ }
676
+ const boxWorld3D = (v, axes)=> axes[0].scale(v.x).add(axes[1].scale(v.y)).add(axes[2].scale(v.z));
677
+
659
678
  /**
660
- * Check if a point is inside an axis aligned box, boundary is inclusive
679
+ * Check if a point is inside a box, boundary is inclusive
661
680
  * @param {Vector3} point
662
681
  * @param {Vector3} pos - Center of the box
663
682
  * @param {Vector3} size - Full size of the box
683
+ * @param {Vector3} [rotation] - How the box is turned, upright when left out
664
684
  * @return {boolean}
665
685
  * @memberof Math3D
666
686
  */
667
- function isPointInBox3D(point, pos, size)
687
+ function isPointInBox3D(point, pos, size, rotation)
668
688
  {
689
+ if (isTurned3D(rotation))
690
+ return isPointInBox3D(boxLocal3D(point, pos, boxAxes3D(rotation)), vec3(), size);
669
691
  return abs(point.x - pos.x) <= size.x/2 &&
670
692
  abs(point.y - pos.y) <= size.y/2 &&
671
693
  abs(point.z - pos.z) <= size.z/2;
672
694
  }
673
695
 
674
696
  /**
675
- * Check if two axis aligned boxes are overlapping, touching edges do not overlap
697
+ * Check if two boxes are overlapping, touching edges do not overlap
676
698
  * @param {Vector3} posA
677
699
  * @param {Vector3} sizeA - Full size of box A
678
700
  * @param {Vector3} posB
679
701
  * @param {Vector3} [sizeB] - Full size of box B, zero for a point
702
+ * @param {Vector3} [rotationA] - How box A is turned, upright when left out
703
+ * @param {Vector3} [rotationB] - How box B is turned
680
704
  * @return {boolean}
681
705
  * @memberof Math3D
682
706
  */
683
- function isOverlapping3D(posA, sizeA, posB, sizeB=vec3())
707
+ function isOverlapping3D(posA, sizeA, posB, sizeB=vec3(), rotationA, rotationB)
684
708
  {
709
+ if (isTurned3D(rotationA) || isTurned3D(rotationB))
710
+ return !!collideBoxBox3D(posA, sizeA, posB, sizeB, rotationA, rotationB);
685
711
  const d = posA.subtract(posB);
686
712
  return abs(d.x) < (sizeA.x + sizeB.x)/2 &&
687
713
  abs(d.y) < (sizeA.y + sizeB.y)/2 &&
@@ -710,16 +736,19 @@ function collideSphereSphere(posA, radiusA, posB, radiusB)
710
736
  }
711
737
 
712
738
  /**
713
- * Returns the vector to move a sphere out of an axis aligned box, or undefined
739
+ * Returns the vector to move a sphere out of a box, or undefined
714
740
  * @param {Vector3} pos - Sphere center
715
741
  * @param {number} radius
716
742
  * @param {Vector3} boxPos
717
743
  * @param {Vector3} boxSize - Full size of the box
744
+ * @param {Vector3} [boxRotation] - How the box is turned, upright when left out
718
745
  * @return {Vector3|undefined}
719
746
  * @memberof Math3D
720
747
  */
721
- function collideSphereBox(pos, radius, boxPos, boxSize)
748
+ function collideSphereBox(pos, radius, boxPos, boxSize, boxRotation)
722
749
  {
750
+ if (isTurned3D(boxRotation))
751
+ return collideSphereOrientedBox3D(pos, radius, boxPos, boxSize, boxAxes3D(boxRotation));
723
752
  const h = boxSize.scale(.5);
724
753
  const closest = vec3(
725
754
  clamp(pos.x, boxPos.x - h.x, boxPos.x + h.x),
@@ -807,15 +836,20 @@ function collideSphereCylinder(pos, radius, cylinderPos, cylinderRadius, cylinde
807
836
  /**
808
837
  * Returns the vector to move box A by so it no longer overlaps box B, the shortest way out, or undefined
809
838
  * - The 3D twin of collideBoxBox
839
+ * - Turned boxes are tested by the separating axis test, the push is along the direction they overlap least on
810
840
  * @param {Vector3} posA
811
841
  * @param {Vector3} sizeA - Full size of box A
812
842
  * @param {Vector3} posB
813
843
  * @param {Vector3} sizeB - Full size of box B
844
+ * @param {Vector3} [rotationA] - How box A is turned, upright when left out
845
+ * @param {Vector3} [rotationB] - How box B is turned
814
846
  * @return {Vector3|undefined}
815
847
  * @memberof Math3D
816
848
  */
817
- function collideBoxBox3D(posA, sizeA, posB, sizeB)
849
+ function collideBoxBox3D(posA, sizeA, posB, sizeB, rotationA, rotationB)
818
850
  {
851
+ if (isTurned3D(rotationA) || isTurned3D(rotationB))
852
+ return collideOrientedBoxes3D(posA, sizeA, boxAxes3D(rotationA), posB, sizeB, boxAxes3D(rotationB));
819
853
  const d = posA.subtract(posB);
820
854
  const overlapX = (sizeA.x + sizeB.x)/2 - abs(d.x);
821
855
  const overlapY = (sizeA.y + sizeB.y)/2 - abs(d.y);
@@ -825,6 +859,44 @@ function collideBoxBox3D(posA, sizeA, posB, sizeB)
825
859
  return pushOutAxis3D(d, overlapX, overlapY, overlapZ);
826
860
  }
827
861
 
862
+ // the push to move a sphere out of a box turned to these axes: in the box's own space it is upright, and the push
863
+ // goes back out turned
864
+ function collideSphereOrientedBox3D(pos, radius, boxPos, boxSize, axes)
865
+ {
866
+ const push = collideSphereBox(boxLocal3D(pos, boxPos, axes), radius, vec3(), boxSize);
867
+ return push && boxWorld3D(push, axes);
868
+ }
869
+
870
+ // the world's axes, for an upright box among turned ones
871
+ const BOX_WORLD_AXES = Object.freeze([vec3(1, 0, 0), vec3(0, 1, 0), vec3(0, 0, 1)]);
872
+
873
+ // the shortest push to move box A out of box B by the separating axis test: each box's three face directions and
874
+ // the nine across an edge of each; a gap on any of them is no touch, and the push is along the least overlap, from
875
+ // B to A; an edge direction has to overlap clearly less to win, so a box lying on another leaves by the face
876
+ function collideOrientedBoxes3D(posA, sizeA, axesA, posB, sizeB, axesB)
877
+ {
878
+ const d = posA.subtract(posB), ha = sizeA.scale(.5), hb = sizeB.scale(.5);
879
+ const reach = (h, axes, n)=> h.x * abs(axes[0].dot(n)) + h.y * abs(axes[1].dot(n)) + h.z * abs(axes[2].dot(n));
880
+ let push, least = Infinity;
881
+ const separated = (axis, weight)=>
882
+ {
883
+ const lengthSquared = axis.lengthSquared();
884
+ if (lengthSquared < 1e-12) return false; // parallel edges give no direction to test
885
+ const n = axis.scale(lengthSquared ** -.5), along = d.dot(n);
886
+ const overlap = reach(ha, axesA, n) + reach(hb, axesB, n) - abs(along);
887
+ if (overlap <= 0) return true;
888
+ if (overlap * weight < least)
889
+ least = overlap * weight, push = n.scale(along < 0 ? -overlap : overlap);
890
+ return false;
891
+ };
892
+ for (const axis of [...axesA, ...axesB])
893
+ if (separated(axis, 1)) return;
894
+ for (const a of axesA)
895
+ for (const b of axesB)
896
+ if (separated(a.cross(b), 1.05)) return;
897
+ return push;
898
+ }
899
+
828
900
  /**
829
901
  * Returns the distance along the ray to the first intersection with a sphere, or undefined
830
902
  * - The hit is ray.getPosition(distance), a direction that is not unit length scales the distance
@@ -873,17 +945,25 @@ function raycastPlane(ray, planePos, planeNormal)
873
945
  }
874
946
 
875
947
  /**
876
- * Returns the distance along the ray to the first intersection with an axis aligned box, or undefined
948
+ * Returns the distance along the ray to the first intersection with a box, or undefined
877
949
  * - The hit is ray.getPosition(distance), a direction that is not unit length scales the distance
878
950
  * - A ray starting inside the box is already there, so it gets back 0
879
951
  * @param {Ray3D} ray
880
952
  * @param {Vector3} pos - Center of the box
881
953
  * @param {Vector3} size - Full size of the box
954
+ * @param {Vector3} [rotation] - How the box is turned, upright when left out
882
955
  * @return {number|undefined}
883
956
  * @memberof Math3D
884
957
  */
885
- function raycastBox(ray, pos, size)
958
+ function raycastBox(ray, pos, size, rotation)
886
959
  {
960
+ if (isTurned3D(rotation))
961
+ {
962
+ // the ray in the box's own space, where the box is upright; turning keeps the distances
963
+ const axes = boxAxes3D(rotation), d = ray.direction;
964
+ const direction = vec3(d.dot(axes[0]), d.dot(axes[1]), d.dot(axes[2]));
965
+ return raycastBox(new Ray3D(boxLocal3D(ray.origin, pos, axes), direction), vec3(), size);
966
+ }
887
967
  const {origin, direction} = ray;
888
968
  const h = size.scale(.5);
889
969
  const boxMin = pos.subtract(h), boxMax = pos.add(h);
@@ -103,6 +103,10 @@ export
103
103
  tweenUpdate,
104
104
  Ease,
105
105
 
106
+ // Scene System
107
+ setScene,
108
+ getScene,
109
+
106
110
  // Path Finding
107
111
  PathFinder,
108
112
  PathFinderNode,
@@ -151,6 +155,7 @@ export
151
155
  buildRibbon,
152
156
  buildLoft,
153
157
  buildSky,
158
+ normalMapFromHeight,
154
159
  buildExtrude,
155
160
  buildText3D,
156
161
  HeightMap,
@@ -177,6 +182,11 @@ export
177
182
  debugLine3D,
178
183
  debugPoint3D,
179
184
 
185
+ // 3D Level
186
+ level3DAddType,
187
+ level3DAddMesh,
188
+ level3DLoad,
189
+
180
190
  // Three.js
181
191
  threeJS,
182
192
  ThreeJSPlugin,
@@ -93,18 +93,7 @@ class PostProcessPlugin
93
93
  'gl_Position=vec4(p+p-1.,1,1);'+ // set position
94
94
  '}' // end of shader
95
95
  ,
96
- '#version 300 es\n' + // specify GLSL ES version
97
- 'precision highp float;'+ // use highp for accuracy
98
- 'uniform sampler2D iChannel0;'+ // input texture
99
- 'uniform sampler2D iChannel1;'+ // the previous frame's output, when feedbackTexture is set
100
- 'uniform vec3 iResolution;'+ // size of output texture
101
- 'uniform float iTime;'+ // time
102
- 'out vec4 c;'+ // out color
103
- '\n' + shaderCode + '\n'+ // insert custom shader code
104
- 'void main(){'+ // shader entry point
105
- 'mainImage(c,gl_FragCoord.xy);'+ // call post process function
106
- 'c.a=1.;'+ // always use full alpha
107
- '}' // end of shader
96
+ postProcessFragmentSource(shaderCode)
108
97
  );
109
98
 
110
99
  // setup VAO for post processing
@@ -192,10 +181,20 @@ class PostProcessPlugin
192
181
  glContext.bindTexture(glContext.TEXTURE_2D, postProcess.feedbackTexture);
193
182
  }
194
183
 
184
+ // the 3D depth, when render3D draws it, on the third texture unit, with what sceneDepth needs to read it
185
+ const depth = typeof render3D != 'undefined' && render3D?.depthTexture ? render3D : undefined;
186
+ glContext.activeTexture(glContext.TEXTURE2);
187
+ glContext.bindTexture(glContext.TEXTURE_2D, depth?.cameraDepthTexture || null);
188
+ glContext.activeTexture(glContext.TEXTURE0);
189
+ const camera = depth?.camera;
190
+ camera && glContext.uniform3f(glUniformLocation(postProcess.shader, 'iDepthRange'), camera.near,
191
+ camera.far == Infinity ? 0 : camera.far, camera.orthographic ? 1 : 0);
192
+
195
193
  // set uniforms and draw
196
194
  const uniformLocation = (name)=>glUniformLocation(postProcess.shader, name);
197
195
  glContext.uniform1i(uniformLocation('iChannel0'), 0);
198
196
  glContext.uniform1i(uniformLocation('iChannel1'), 1);
197
+ glContext.uniform1i(uniformLocation('iChannel2'), 2);
199
198
  glContext.uniform1f(uniformLocation('iTime'), time);
200
199
  glContext.uniform3f(uniformLocation('iResolution'), mainCanvas.width, mainCanvas.height, 1);
201
200
  glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, 4);
@@ -207,6 +206,11 @@ class PostProcessPlugin
207
206
  glContext.activeTexture(glContext.TEXTURE0);
208
207
  }
209
208
 
209
+ // the depth leaves the third unit for whatever draws next
210
+ glContext.activeTexture(glContext.TEXTURE2);
211
+ glContext.bindTexture(glContext.TEXTURE_2D, null);
212
+ glContext.activeTexture(glContext.TEXTURE0);
213
+
210
214
  // restore defaults so subsequent dynamic texture uploads aren't flipped or premultiplied
211
215
  glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, false);
212
216
  glContext.pixelStorei(glContext.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
@@ -223,6 +227,31 @@ class PostProcessPlugin
223
227
 
224
228
  ///////////////////////////////////////////////////////////////////////////////
225
229
 
230
+ // the fragment shader of the post process pass around its mainImage snippet: the frame on iChannel0, the last output
231
+ // on iChannel1 with a feedback texture, and the 3D depth on iChannel2 when render3D.depthTexture is on, read with
232
+ // sceneDepth(uv), the distance from the camera along its view in world units, uv 0 to 1 across the screen
233
+ function postProcessFragmentSource(shaderCode)
234
+ {
235
+ return '#version 300 es\n' + // specify GLSL ES version
236
+ 'precision highp float;'+ // use highp for accuracy
237
+ 'uniform sampler2D iChannel0;'+ // input texture
238
+ 'uniform sampler2D iChannel1;'+ // the previous frame's output, when feedbackTexture is set
239
+ 'uniform sampler2D iChannel2;'+ // the 3D depth, when render3D.depthTexture is on
240
+ 'uniform vec3 iResolution;'+ // size of output texture
241
+ 'uniform float iTime;'+ // time
242
+ 'uniform vec3 iDepthRange;'+ // the camera's near, its far or 0 for none, and 1 when orthographic
243
+ 'out vec4 c;'+ // out color
244
+ // the depth texture's value back to a distance, as the camera's projection put it there
245
+ 'float sceneDepth(vec2 uv){'+
246
+ 'float d=texture(iChannel2,uv).r*2.-1.,n=iDepthRange.x,f=iDepthRange.y;'+
247
+ 'return iDepthRange.z>0.?(d*(f-n)+f+n)/2.:f>0.?2.*n*f/(f+n-d*(f-n)):2.*n/(1.-d);}'+
248
+ '\n' + shaderCode + '\n'+ // insert custom shader code
249
+ 'void main(){'+ // shader entry point
250
+ 'mainImage(c,gl_FragCoord.xy);'+ // call post process function
251
+ 'c.a=1.;'+ // always use full alpha
252
+ '}'; // end of shader
253
+ }
254
+
226
255
  /**
227
256
  * Shader code for a bloom effect, the bright parts of the image blurred back over it
228
257
  * - Pass it to PostProcessPlugin, or edit the string to build an effect on top of it