phoria2 1.3.0 → 1.4.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.
Files changed (56) hide show
  1. package/README.md +9 -6
  2. package/dist/bloom.js +4 -4
  3. package/dist/bloom.js.map +1 -1
  4. package/dist/charts.js +436 -436
  5. package/dist/charts.js.map +1 -1
  6. package/dist/core.js +1223 -862
  7. package/dist/core.js.map +1 -1
  8. package/dist/debug.js +297 -0
  9. package/dist/debug.js.map +1 -0
  10. package/dist/edit.js +40 -40
  11. package/dist/edit.js.map +1 -1
  12. package/dist/explain.js +32 -13
  13. package/dist/explain.js.map +1 -1
  14. package/dist/gltf.js +128 -128
  15. package/dist/gltf.js.map +1 -1
  16. package/dist/keyboard.js +19 -19
  17. package/dist/keyboard.js.map +1 -1
  18. package/dist/motion.js +35 -35
  19. package/dist/motion.js.map +1 -1
  20. package/dist/particles.js +36 -36
  21. package/dist/particles.js.map +1 -1
  22. package/dist/passes.js +116 -104
  23. package/dist/passes.js.map +1 -1
  24. package/dist/phoria2.js +1 -1
  25. package/dist/picking.js +88 -71
  26. package/dist/picking.js.map +1 -1
  27. package/dist/snapshot.js +1 -1
  28. package/dist/tooltip.js +1 -1
  29. package/dist/types/View.d.ts +16 -0
  30. package/dist/types/debug.d.ts +74 -0
  31. package/dist/types/explain.d.ts +4 -0
  32. package/dist/types/frame.d.ts +5 -0
  33. package/dist/types/material/Material.d.ts +52 -0
  34. package/dist/types/watch.d.ts +8 -1
  35. package/dist/types/webgl/hooks.d.ts +48 -0
  36. package/dist/types/webgl/passes.d.ts +4 -0
  37. package/dist/types/webgl/programs.d.ts +15 -5
  38. package/dist/types/webgl/shaders.d.ts +36 -5
  39. package/llms-full.txt +227 -0
  40. package/llms.txt +1 -1
  41. package/package.json +1 -1
  42. package/schema/scene.schema.json +27 -0
  43. package/src/View.js +105 -7
  44. package/src/debug.js +283 -0
  45. package/src/explain.js +19 -0
  46. package/src/frame.js +7 -2
  47. package/src/material/Material.js +121 -2
  48. package/src/scene/drawables.js +21 -4
  49. package/src/watch.js +18 -2
  50. package/src/webgl/WebGLRenderer.js +19 -2
  51. package/src/webgl/hooks.js +121 -0
  52. package/src/webgl/passes.js +17 -2
  53. package/src/webgl/picking.js +23 -2
  54. package/src/webgl/programs.js +20 -9
  55. package/src/webgl/resources.js +1 -1
  56. package/src/webgl/shaders.js +443 -35
@@ -116,9 +116,17 @@ export const CLIP_ONLY_FRAGMENT = /* glsl */ `
116
116
  precision highp float;
117
117
  in vec3 vWorldPosition;
118
118
  out vec4 fragColor;
119
+ #if defined(ALPHA_TEST) || defined(HOOK_FRAGMENT)
120
+ in vec2 vUv;
121
+ #endif
122
+ #ifdef HOOK_FRAGMENT
123
+ // a fragment hook's discards cut the shadow too - a dissolving thing casts what is left of it
124
+ in vec3 vNormal;
125
+ in vec3 vColor;
126
+ //@hook uniforms
127
+ #endif
119
128
  #ifdef ALPHA_TEST
120
129
  // a cut-out casts the shadow of what is left of it - leaves, fences, grilles - not of its rectangle
121
- in vec2 vUv;
122
130
  uniform float uOpacity;
123
131
  uniform float uAlphaTest;
124
132
  #ifdef MAP
@@ -129,6 +137,15 @@ ${SAMPLING}
129
137
  ${CLIP}
130
138
  void main() {
131
139
  clipAway(vWorldPosition);
140
+ #ifdef HOOK_FRAGMENT
141
+ {
142
+ vec4 base = vec4(vColor, 1.0);
143
+ vec3 emissive = vec3(0.0);
144
+ vec3 normal = normalize(vNormal);
145
+ //@hook fragment
146
+ if (base.a <= 0.0) discard;
147
+ }
148
+ #endif
132
149
  #ifdef ALPHA_TEST
133
150
  float alpha = uOpacity;
134
151
  #ifdef MAP
@@ -225,6 +242,7 @@ uniform int uHighlightIndex; // -2 none, -1 the whole node, otherwise the instan
225
242
  uniform mat4 uView;
226
243
  out float vFogDepth;
227
244
  #endif
245
+ //@hook uniforms
228
246
 
229
247
  out vec3 vWorldPosition;
230
248
  out vec3 vNormal;
@@ -248,25 +266,34 @@ ${COMMON}
248
266
  void main() {
249
267
  vColor = vec3(1.0);
250
268
  vHighlight = uHighlightIndex == -1 ? 1.0 : 0.0;
269
+ // the vertex in the geometry's own space - where a shader hook moves it
270
+ vec3 local = position;
271
+ vec3 n = normal;
272
+ #ifdef HOOK_VERTEX
273
+ {
274
+ int index = gl_InstanceID;
275
+ //@hook vertex
276
+ }
277
+ #endif
251
278
  #ifdef INSTANCED
252
- vec4 local = instanceMatrix * vec4(position, 1.0);
279
+ vec4 placed = instanceMatrix * vec4(local, 1.0);
253
280
  // the instance's normal matrix as its cofactor: the inverse transpose up to scale, from three
254
281
  // cross products, flipped back for mirroring transforms
255
282
  mat3 m = mat3(instanceMatrix);
256
283
  mat3 instanceNormal = mat3(cross(m[1], m[2]), cross(m[2], m[0]), cross(m[0], m[1])) * sign(dot(m[0], cross(m[1], m[2])));
257
- vec3 n = instanceNormal * normal;
284
+ vec3 placedNormal = instanceNormal * n;
258
285
  vColor *= instanceColor;
259
286
  if (gl_InstanceID == uHighlightIndex) vHighlight = 1.0;
260
287
  #else
261
- vec4 local = vec4(position, 1.0);
262
- vec3 n = normal;
288
+ vec4 placed = vec4(local, 1.0);
289
+ vec3 placedNormal = n;
263
290
  #endif
264
291
  #ifdef VERTEX_COLORS
265
292
  vColor *= srgbToLinear(color);
266
293
  #endif
267
- vec4 world = uModel * local;
294
+ vec4 world = uModel * placed;
268
295
  vWorldPosition = world.xyz;
269
- vNormal = uNormalMatrix * n;
296
+ vNormal = uNormalMatrix * placedNormal;
270
297
  vUv = uv;
271
298
  #ifdef UV2
272
299
  vUv2 = uv2;
@@ -280,8 +307,8 @@ void main() {
280
307
  #endif
281
308
  #endif
282
309
  #ifdef TRIPLANAR
283
- vObjectPosition = position;
284
- vObjectNormal = normal;
310
+ vObjectPosition = local;
311
+ vObjectNormal = n;
285
312
  #ifdef INSTANCED
286
313
  vObjectToWorld = uNormalMatrix * instanceNormal;
287
314
  #else
@@ -322,6 +349,7 @@ uniform float uAlphaTest;
322
349
  #ifdef EMISSIVE_MAP
323
350
  uniform sampler2D uEmissiveMap;
324
351
  #endif
352
+ //@hook uniforms
325
353
 
326
354
  ${SAMPLING}
327
355
  #ifdef LIT
@@ -738,6 +766,25 @@ void main() {
738
766
  vec4 base = vec4(uColor * vColor, uOpacity);
739
767
  #ifdef MAP
740
768
  base *= uMapTexel; // sRGB textures are decoded to linear by the hardware
769
+ #endif
770
+ vec3 emissive = uEmissive;
771
+ #ifdef EMISSIVE_MAP
772
+ emissive *= uEmissiveMapTexel.rgb;
773
+ #endif
774
+ #ifdef HOOK_FRAGMENT
775
+ // the shading normal in world space, facing the viewer - a fragment hook can bend it, for relief
776
+ // lit per pixel (bumps from noise) whatever the mesh's resolution
777
+ #ifdef FLAT_SHADING
778
+ vec3 normal = normalize(cross(dFdx(vWorldPosition), dFdy(vWorldPosition)));
779
+ #else
780
+ vec3 normal = normalize(vNormal);
781
+ #ifdef DOUBLE_SIDED
782
+ if (!gl_FrontFacing) normal = -normal;
783
+ #endif
784
+ #endif
785
+ {
786
+ //@hook fragment
787
+ }
741
788
  #endif
742
789
  #ifdef ALPHA_TEST
743
790
  if (base.a < uAlphaTest) discard;
@@ -745,19 +792,18 @@ void main() {
745
792
  #endif
746
793
  // how much of what is behind it this hides: its opacity - less for glass, which lets light through
747
794
  float coverage = base.a;
748
- vec3 emissive = uEmissive;
749
- #ifdef EMISSIVE_MAP
750
- emissive *= uEmissiveMapTexel.rgb;
751
- #endif
752
795
 
753
796
  #ifdef LIT
754
- #ifdef FLAT_SHADING
797
+ #ifdef HOOK_FRAGMENT
798
+ // the fragment hook's normal, facing the viewer already
799
+ vec3 n = normalize(normal);
800
+ #elif defined(FLAT_SHADING)
755
801
  // from the screen-space slope: it faces the camera whichever side of the face is seen
756
802
  vec3 n = normalize(cross(dFdx(vWorldPosition), dFdy(vWorldPosition)));
757
803
  #else
758
804
  vec3 n = normalize(vNormal);
759
805
  #endif
760
- #if defined(DOUBLE_SIDED) && !defined(FLAT_SHADING)
806
+ #if defined(DOUBLE_SIDED) && !defined(FLAT_SHADING) && !defined(HOOK_FRAGMENT)
761
807
  if (!gl_FrontFacing) n = -n;
762
808
  #endif
763
809
  vec3 surfaceNormal = n;
@@ -948,6 +994,14 @@ void main() {
948
994
  #endif
949
995
  #else
950
996
  vec3 color = base.rgb + emissive;
997
+ #endif
998
+ #ifdef HOOK_LIT
999
+ {
1000
+ // a fade here fades what it hides too
1001
+ float before = max(base.a, 1e-5);
1002
+ //@hook lit
1003
+ coverage *= clamp(base.a / before, 0.0, 1.0);
1004
+ }
951
1005
  #endif
952
1006
  color = mix(color, uHighlight.rgb, vHighlight * uHighlight.a);
953
1007
  // a highlight shows on glass as on anything else
@@ -981,9 +1035,13 @@ uniform mat4 uProjection;
981
1035
  uniform float uSize; // pixels (with the pixel ratio applied), or world units when attenuated
982
1036
  uniform float uScale; // projection focal term x half the buffer height: world size -> pixels at depth 1
983
1037
  uniform int uHighlightIndex; // -2 none, -1 every point, otherwise the point
1038
+ //@hook uniforms
984
1039
 
985
1040
  out vec3 vColor;
986
1041
  flat out float vHighlight;
1042
+ #ifdef HOOK_FRAGMENT
1043
+ flat out float vPointIndex;
1044
+ #endif
987
1045
  #ifdef PICK
988
1046
  flat out uint vPickElement; // which instance, point or segment, for GPU picking
989
1047
  #endif
@@ -994,29 +1052,46 @@ out float vFogDepth;
994
1052
  ${COMMON}
995
1053
 
996
1054
  void main() {
997
- vec4 viewPosition = uView * (uModel * vec4(position, 1.0));
1055
+ // the point in the geometry's own space, and its size as a multiple of the material's - where a
1056
+ // shader hook changes them
1057
+ vec3 local = position;
1058
+ #ifdef SPRITES
1059
+ int self = int(pointIndex + 0.5);
1060
+ float size = pointSize;
1061
+ #else
1062
+ int self = gl_VertexID;
1063
+ float size = 1.0;
1064
+ #endif
1065
+ #ifdef HOOK_VERTEX
1066
+ {
1067
+ int index = self;
1068
+ //@hook vertex
1069
+ }
1070
+ #endif
1071
+ #ifdef HOOK_FRAGMENT
1072
+ vPointIndex = float(self);
1073
+ #endif
1074
+ vec4 viewPosition = uView * (uModel * vec4(local, 1.0));
998
1075
  #ifdef SPRITES
999
1076
  // the square's corner, turned by the point's angle: the texture turns with it
1000
1077
  vec2 c = vec2(corner.x * 2.0 - 1.0, corner.y);
1001
1078
  float s = sin(pointAngle), k = cos(pointAngle);
1002
1079
  vec2 turned = vec2(c.x * k - c.y * s, c.x * s + c.y * k);
1003
- float size = uSize * pointSize;
1080
+ float across = uSize * size;
1004
1081
  #ifdef SIZE_ATTENUATION
1005
1082
  // world units: the square sits in the view, facing the camera
1006
- viewPosition.xy += turned * size * 0.5;
1083
+ viewPosition.xy += turned * across * 0.5;
1007
1084
  gl_Position = uProjection * viewPosition;
1008
1085
  #else
1009
1086
  // pixels: the square is laid out on the screen
1010
1087
  gl_Position = uProjection * viewPosition;
1011
- gl_Position.xy += turned * size / uResolution * gl_Position.w;
1088
+ gl_Position.xy += turned * across / uResolution * gl_Position.w;
1012
1089
  #endif
1013
1090
  // the image upright on the square, its top at the top, as a point's is
1014
1091
  vUv = vec2(c.x * 0.5 + 0.5, 0.5 - c.y * 0.5);
1015
1092
  vOpacity = pointOpacity;
1016
- int self = int(pointIndex + 0.5);
1017
1093
  #else
1018
1094
  gl_Position = uProjection * viewPosition;
1019
- int self = gl_VertexID;
1020
1095
  #endif
1021
1096
  #ifdef FOG
1022
1097
  vFogDepth = -viewPosition.z;
@@ -1025,9 +1100,9 @@ void main() {
1025
1100
  #elif defined(SIZE_ATTENUATION)
1026
1101
  // perspective shrinks with distance; an orthographic projection (w row 0, 0, 0, 1) doesn't
1027
1102
  float depth = uProjection[3][3] == 1.0 ? 1.0 : max(-viewPosition.z, 1e-4);
1028
- gl_PointSize = uSize * uScale / depth;
1103
+ gl_PointSize = uSize * size * uScale / depth;
1029
1104
  #else
1030
- gl_PointSize = uSize;
1105
+ gl_PointSize = uSize * size;
1031
1106
  #endif
1032
1107
  vColor = vec3(1.0);
1033
1108
  #ifdef VERTEX_COLORS
@@ -1054,6 +1129,10 @@ uniform float uOpacity;
1054
1129
  #ifdef MAP
1055
1130
  uniform sampler2D uMap;
1056
1131
  #endif
1132
+ #ifdef HOOK_FRAGMENT
1133
+ flat in float vPointIndex;
1134
+ #endif
1135
+ //@hook uniforms
1057
1136
  out vec4 fragColor;
1058
1137
 
1059
1138
  ${COMMON}
@@ -1075,10 +1154,22 @@ void main() {
1075
1154
  float r = dot(p, p);
1076
1155
  float edge = fwidth(r);
1077
1156
  base.a *= 1.0 - smoothstep(1.0 - edge, 1.0, r);
1157
+ #endif
1158
+ #ifdef HOOK_FRAGMENT
1159
+ {
1160
+ vec2 pointUv = at;
1161
+ int index = int(vPointIndex + 0.5);
1162
+ //@hook fragment
1163
+ }
1078
1164
  #endif
1079
1165
  if (base.a <= 0.0) discard;
1080
- base.rgb = fogged(mix(base.rgb, uHighlight.rgb, vHighlight * uHighlight.a));
1081
- fragColor = vec4(linearToSrgb(base.rgb) * base.a, base.a);
1166
+ vec3 color = fogged(mix(base.rgb, uHighlight.rgb, vHighlight * uHighlight.a));
1167
+ #ifdef HOOK_LIT
1168
+ {
1169
+ //@hook lit
1170
+ }
1171
+ #endif
1172
+ fragColor = vec4(linearToSrgb(color) * base.a, base.a);
1082
1173
  }
1083
1174
  `
1084
1175
 
@@ -1099,14 +1190,20 @@ uniform float uWidth; // pixels, already scaled by the pixel ratio
1099
1190
  uniform float uNear;
1100
1191
  uniform int uArrows; // arrowheads: 1 at the start of each chain, 2 at its end, 3 at both
1101
1192
  uniform float uArrowSize; // their length in pixels, scaled by the pixel ratio
1193
+ //@hook uniforms
1102
1194
 
1103
1195
  // six vertices an instance make the segment's quad, as two triangles - x: 0 at its start, 1 at its
1104
1196
  // end; y: -1 or 1 across - and six more its two arrowheads, drawn only when asked for
1105
1197
  const vec2 QUAD[6] = vec2[6](vec2(0.0, -1.0), vec2(0.0, 1.0), vec2(1.0, -1.0), vec2(1.0, -1.0), vec2(0.0, 1.0), vec2(1.0, 1.0));
1106
1198
 
1107
1199
  out vec3 vColor;
1108
- #ifdef DASHED
1200
+ #if defined(DASHED) || defined(HOOK_FRAGMENT)
1109
1201
  out float vDistance; // along the chain, in the model's units
1202
+ #endif
1203
+ #ifdef HOOK_FRAGMENT
1204
+ flat out float vSegment; // which segment, for a fragment hook's index
1205
+ #endif
1206
+ #ifdef DASHED
1110
1207
  flat out float vArrow; // 1 on an arrowhead, which is never dashed
1111
1208
  #endif
1112
1209
  #ifdef PICK
@@ -1118,9 +1215,22 @@ out float vFogDepth;
1118
1215
 
1119
1216
  ${COMMON}
1120
1217
 
1218
+ #ifdef HOOK_VERTEX
1219
+ // a shader hook moves each end of each segment: local is the end, along 0 at the start and 1 at the end
1220
+ vec3 hookVertex(vec3 local, float along, int index) {
1221
+ //@hook vertex
1222
+ return local;
1223
+ }
1224
+ #endif
1225
+
1121
1226
  void main() {
1122
- vec4 a = uView * (uModel * vec4(segmentStart, 1.0));
1123
- vec4 b = uView * (uModel * vec4(segmentEnd, 1.0));
1227
+ vec3 start = segmentStart, end = segmentEnd;
1228
+ #ifdef HOOK_VERTEX
1229
+ start = hookVertex(segmentStart, 0.0, gl_InstanceID);
1230
+ end = hookVertex(segmentEnd, 1.0, gl_InstanceID);
1231
+ #endif
1232
+ vec4 a = uView * (uModel * vec4(start, 1.0));
1233
+ vec4 b = uView * (uModel * vec4(end, 1.0));
1124
1234
  // clip the segment to the near plane so lines passing behind the camera stay correct
1125
1235
  float clipZ = -uNear;
1126
1236
  if (a.z > clipZ && b.z > clipZ) {
@@ -1170,8 +1280,13 @@ void main() {
1170
1280
  #endif
1171
1281
  clip.xy += offset / (uResolution * 0.5) * clip.w;
1172
1282
  gl_Position = clip;
1283
+ #if defined(DASHED) || defined(HOOK_FRAGMENT)
1284
+ vDistance = segmentInfo.x + length(end - start) * along;
1285
+ #endif
1286
+ #ifdef HOOK_FRAGMENT
1287
+ vSegment = float(gl_InstanceID);
1288
+ #endif
1173
1289
  #ifdef DASHED
1174
- vDistance = segmentInfo.x + length(segmentEnd - segmentStart) * along;
1175
1290
  vArrow = arrow;
1176
1291
  #endif
1177
1292
  #ifdef PICK
@@ -1190,8 +1305,14 @@ precision highp float;
1190
1305
  in vec3 vColor;
1191
1306
  uniform vec3 uColor;
1192
1307
  uniform float uOpacity;
1193
- #ifdef DASHED
1308
+ #if defined(DASHED) || defined(HOOK_FRAGMENT)
1194
1309
  in float vDistance;
1310
+ #endif
1311
+ #ifdef HOOK_FRAGMENT
1312
+ flat in float vSegment;
1313
+ #endif
1314
+ //@hook uniforms
1315
+ #ifdef DASHED
1195
1316
  flat in float vArrow;
1196
1317
  uniform float uDash; // dash and gap lengths, in the model's units
1197
1318
  uniform float uGap;
@@ -1206,8 +1327,22 @@ void main() {
1206
1327
  #ifdef DASHED
1207
1328
  if (vArrow < 0.5 && mod(vDistance - uDashOffset, uDash + uGap) >= uDash) discard;
1208
1329
  #endif
1209
- vec3 color = fogged(uColor * vColor);
1210
- fragColor = vec4(linearToSrgb(color) * uOpacity, uOpacity);
1330
+ vec4 base = vec4(uColor * vColor, uOpacity);
1331
+ #ifdef HOOK_FRAGMENT
1332
+ {
1333
+ float distance = vDistance;
1334
+ int index = int(vSegment + 0.5);
1335
+ //@hook fragment
1336
+ }
1337
+ if (base.a <= 0.0) discard;
1338
+ #endif
1339
+ vec3 color = fogged(base.rgb);
1340
+ #ifdef HOOK_LIT
1341
+ {
1342
+ //@hook lit
1343
+ }
1344
+ #endif
1345
+ fragColor = vec4(linearToSrgb(color) * base.a, base.a);
1211
1346
  }
1212
1347
  `
1213
1348
 
@@ -1220,13 +1355,286 @@ const SOURCES = {
1220
1355
  }
1221
1356
 
1222
1357
  /**
1223
- * Vertex and fragment source for a kind of drawable, or a pass's own pair, with a set of feature defines.
1358
+ * GLSL of a material's own, spliced in at the shaders' `//@hook` lines: uniform declarations, and
1359
+ * code run on each vertex, before lighting and after it.
1360
+ * @typedef {object} HookSource
1361
+ * @property {string} uniforms declarations
1362
+ * @property {string} [helpers] functions every hook can call - hash, noise, fbm
1363
+ * @property {string} vertex
1364
+ * @property {string} fragment
1365
+ * @property {string} lit
1366
+ */
1367
+
1368
+ // Functions every shader hook can call - hooks are statements, so they can't define their own. All of
1369
+ // it is built on an integer bit-mix (lowbias32), which GLSL ES 3.00 defines exactly, so lattice
1370
+ // values and gradients are the same on every GPU - fract(sin(x) * 43758.5453) is not. Everything 0-1:
1371
+ // - hash(x): a float or vector's exact bits, mixed
1372
+ // - vnoise(p): value noise - random values at whole-number points, eased between (quintic); cheap,
1373
+ // blocky-looking at low frequencies
1374
+ // - snoise(p): simplex noise - gradients on a simplex grid; smoother, less grid-bound, the better
1375
+ // default; snoised(p) also gives its gradient, for normals
1376
+ // - vfbm / sfbm (p[, octaves]): octaves of either added up, each turned and scaled by 2.03 so their
1377
+ // grids never line up; sfbmd adds the gradient. Five octaves unless told; 1-12
1378
+ // - voronoi(p): cellular (Worley) noise - a random point in every unit cell; returns the distance to
1379
+ // the nearest (F1) and the second nearest (F2), in cells, and a random 0-1 for the nearest's cell
1380
+ // And the everyday ones people (and agents) reach for: srgb() to turn a colour as picked into the
1381
+ // linear one hooks work in, hsv2rgb() and palette() (Quilez's cosine palette) - both giving linear
1382
+ // colour - fresnel() for rim light, rotate2() / rotate3() matrices, remap() and saturate().
1383
+ // Every int highp: a fragment shader's ints are mediump unless they say so.
1384
+ export const HOOK_HELPERS = /* glsl */ `
1385
+ highp uint phoriaMix(highp uint h) {
1386
+ h ^= h >> 16u; h *= 0x7feb352du; h ^= h >> 15u; h *= 0x846ca68bu; h ^= h >> 16u;
1387
+ return h;
1388
+ }
1389
+ float phoriaUnit(highp uint h) { return float(h >> 8u) * (1.0 / 16777216.0); }
1390
+ float hash(float x) { return phoriaUnit(phoriaMix(floatBitsToUint(x))); }
1391
+ float hash(vec2 p) { highp uvec2 u = floatBitsToUint(p); return phoriaUnit(phoriaMix(u.x ^ phoriaMix(u.y + 0x9e3779b9u))); }
1392
+ float hash(vec3 p) { highp uvec3 u = floatBitsToUint(p); return phoriaUnit(phoriaMix(u.x ^ phoriaMix(u.y ^ phoriaMix(u.z + 0x9e3779b9u)))); }
1393
+ // a lattice point's bits: each coordinate times its own large odd number, mixed once
1394
+ highp uint phoriaCell(highp ivec3 c) {
1395
+ highp uvec3 u = uvec3(c) * uvec3(0x8da6b343u, 0xd8163841u, 0xcb1ab31fu);
1396
+ return phoriaMix(u.x ^ u.y ^ u.z);
1397
+ }
1398
+
1399
+ float vnoise(vec3 p) {
1400
+ vec3 i = floor(p), f = p - i;
1401
+ vec3 s = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
1402
+ highp ivec3 c = ivec3(i);
1403
+ float a = mix(phoriaUnit(phoriaCell(c)), phoriaUnit(phoriaCell(c + ivec3(1, 0, 0))), s.x);
1404
+ float b = mix(phoriaUnit(phoriaCell(c + ivec3(0, 1, 0))), phoriaUnit(phoriaCell(c + ivec3(1, 1, 0))), s.x);
1405
+ float d = mix(phoriaUnit(phoriaCell(c + ivec3(0, 0, 1))), phoriaUnit(phoriaCell(c + ivec3(1, 0, 1))), s.x);
1406
+ float e = mix(phoriaUnit(phoriaCell(c + ivec3(0, 1, 1))), phoriaUnit(phoriaCell(c + ivec3(1, 1, 1))), s.x);
1407
+ return mix(mix(a, b, s.y), mix(d, e, s.y), s.z);
1408
+ }
1409
+ float vnoise(vec2 p) {
1410
+ vec2 i = floor(p), f = p - i;
1411
+ vec2 s = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
1412
+ highp ivec3 c = ivec3(ivec2(i), 0);
1413
+ return mix(mix(phoriaUnit(phoriaCell(c)), phoriaUnit(phoriaCell(c + ivec3(1, 0, 0))), s.x),
1414
+ mix(phoriaUnit(phoriaCell(c + ivec3(0, 1, 0))), phoriaUnit(phoriaCell(c + ivec3(1, 1, 0))), s.x), s.y);
1415
+ }
1416
+
1417
+ // simplex: a corner's share of the value and the gradient - t^4 times the gradient's ramp, falling
1418
+ // to nothing at the corner's reach
1419
+ vec2 phoriaGrad2(highp ivec2 c) {
1420
+ float a = phoriaUnit(phoriaCell(ivec3(c, 7))) * 6.28318530718;
1421
+ return vec2(cos(a), sin(a));
1422
+ }
1423
+ void phoriaCorner2(vec2 x, highp ivec2 c, inout float n, inout vec2 d) {
1424
+ float t = 0.5 - dot(x, x);
1425
+ if (t <= 0.0) return;
1426
+ vec2 g = phoriaGrad2(c);
1427
+ float t2 = t * t, gx = dot(g, x);
1428
+ n += t2 * t2 * gx;
1429
+ d += t2 * t2 * g - 8.0 * t2 * t * gx * x;
1430
+ }
1431
+ vec3 snoised(vec2 p) {
1432
+ const float F = 0.366025403784, G = 0.211324865405;
1433
+ vec2 i = floor(p + (p.x + p.y) * F);
1434
+ vec2 x0 = p - i + (i.x + i.y) * G;
1435
+ vec2 o = x0.x > x0.y ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
1436
+ highp ivec2 c = ivec2(i);
1437
+ float n = 0.0;
1438
+ vec2 d = vec2(0.0);
1439
+ phoriaCorner2(x0, c, n, d);
1440
+ phoriaCorner2(x0 - o + G, c + ivec2(o), n, d);
1441
+ phoriaCorner2(x0 - 1.0 + 2.0 * G, c + 1, n, d);
1442
+ // unit gradients peak near 1/70: scaled to -1-1, then to 0-1
1443
+ return vec3(0.5 + 35.0 * n, 35.0 * d);
1444
+ }
1445
+ float snoise(vec2 p) { return snoised(p).x; }
1446
+
1447
+ const vec3 PHORIA_EDGES[12] = vec3[12](vec3(1, 1, 0), vec3(-1, 1, 0), vec3(1, -1, 0), vec3(-1, -1, 0),
1448
+ vec3(1, 0, 1), vec3(-1, 0, 1), vec3(1, 0, -1), vec3(-1, 0, -1), vec3(0, 1, 1), vec3(0, -1, 1), vec3(0, 1, -1), vec3(0, -1, -1));
1449
+ void phoriaCorner3(vec3 x, highp ivec3 c, inout float n, inout vec3 d) {
1450
+ float t = 0.6 - dot(x, x);
1451
+ if (t <= 0.0) return;
1452
+ vec3 g = PHORIA_EDGES[phoriaCell(c) % 12u];
1453
+ float t2 = t * t, gx = dot(g, x);
1454
+ n += t2 * t2 * gx;
1455
+ d += t2 * t2 * g - 8.0 * t2 * t * gx * x;
1456
+ }
1457
+ vec4 snoised(vec3 p) {
1458
+ const float F = 1.0 / 3.0, G = 1.0 / 6.0;
1459
+ vec3 i = floor(p + dot(p, vec3(F)));
1460
+ vec3 x0 = p - i + dot(i, vec3(G));
1461
+ vec3 g = step(x0.yzx, x0.xyz), l = 1.0 - g;
1462
+ vec3 i1 = min(g, l.zxy), i2 = max(g, l.zxy);
1463
+ highp ivec3 c = ivec3(i);
1464
+ float n = 0.0;
1465
+ vec3 d = vec3(0.0);
1466
+ phoriaCorner3(x0, c, n, d);
1467
+ phoriaCorner3(x0 - i1 + G, c + ivec3(i1), n, d);
1468
+ phoriaCorner3(x0 - i2 + 2.0 * G, c + ivec3(i2), n, d);
1469
+ phoriaCorner3(x0 - 1.0 + 3.0 * G, c + 1, n, d);
1470
+ // edge gradients peak near 1/32: scaled to -1-1, then to 0-1
1471
+ return vec4(0.5 + 16.0 * n, 16.0 * d);
1472
+ }
1473
+ float snoise(vec3 p) { return snoised(p).x; }
1474
+
1475
+ // fbm: each octave turned (so grids don't line up into a visible lattice), scaled by 2.03 and moved
1476
+ // on, its amplitude halved; the sum divided by the amplitudes, so 0-1 like the noise
1477
+ const mat2 PHORIA_TURN2 = mat2(0.8, 0.6, -0.6, 0.8);
1478
+ const mat3 PHORIA_TURN3 = mat3(0.00, 0.80, 0.60, -0.80, 0.36, -0.48, -0.60, -0.48, 0.64);
1479
+ float vfbm(vec2 p, int octaves) {
1480
+ float v = 0.0, a = 0.5, sum = 0.0;
1481
+ for (int i = 0; i < clamp(octaves, 1, 12); i++) { v += a * vnoise(p); sum += a; p = PHORIA_TURN2 * p * 2.03 + 17.0; a *= 0.5; }
1482
+ return v / sum;
1483
+ }
1484
+ float vfbm(vec3 p, int octaves) {
1485
+ float v = 0.0, a = 0.5, sum = 0.0;
1486
+ for (int i = 0; i < clamp(octaves, 1, 12); i++) { v += a * vnoise(p); sum += a; p = PHORIA_TURN3 * p * 2.03 + 17.0; a *= 0.5; }
1487
+ return v / sum;
1488
+ }
1489
+ float sfbm(vec2 p, int octaves) {
1490
+ float v = 0.0, a = 0.5, sum = 0.0;
1491
+ for (int i = 0; i < clamp(octaves, 1, 12); i++) { v += a * snoise(p); sum += a; p = PHORIA_TURN2 * p * 2.03 + 17.0; a *= 0.5; }
1492
+ return v / sum;
1493
+ }
1494
+ float sfbm(vec3 p, int octaves) {
1495
+ float v = 0.0, a = 0.5, sum = 0.0;
1496
+ for (int i = 0; i < clamp(octaves, 1, 12); i++) { v += a * snoise(p); sum += a; p = PHORIA_TURN3 * p * 2.03 + 17.0; a *= 0.5; }
1497
+ return v / sum;
1498
+ }
1499
+ // with the gradient: each octave's, carried back through the turns and scalings so far
1500
+ vec3 sfbmd(vec2 p, int octaves) {
1501
+ float v = 0.0, a = 0.5, sum = 0.0;
1502
+ vec2 d = vec2(0.0);
1503
+ mat2 m = mat2(1.0);
1504
+ for (int i = 0; i < clamp(octaves, 1, 12); i++) {
1505
+ vec3 n = snoised(p);
1506
+ v += a * n.x;
1507
+ d += a * (transpose(m) * n.yz);
1508
+ sum += a;
1509
+ p = PHORIA_TURN2 * p * 2.03 + 17.0;
1510
+ m = PHORIA_TURN2 * m * 2.03;
1511
+ a *= 0.5;
1512
+ }
1513
+ return vec3(v, d) / sum;
1514
+ }
1515
+ vec4 sfbmd(vec3 p, int octaves) {
1516
+ float v = 0.0, a = 0.5, sum = 0.0;
1517
+ vec3 d = vec3(0.0);
1518
+ mat3 m = mat3(1.0);
1519
+ for (int i = 0; i < clamp(octaves, 1, 12); i++) {
1520
+ vec4 n = snoised(p);
1521
+ v += a * n.x;
1522
+ d += a * (transpose(m) * n.yzw);
1523
+ sum += a;
1524
+ p = PHORIA_TURN3 * p * 2.03 + 17.0;
1525
+ m = PHORIA_TURN3 * m * 2.03;
1526
+ a *= 0.5;
1527
+ }
1528
+ return vec4(v, d) / sum;
1529
+ }
1530
+ float vfbm(vec2 p) { return vfbm(p, 5); }
1531
+ float vfbm(vec3 p) { return vfbm(p, 5); }
1532
+ float sfbm(vec2 p) { return sfbm(p, 5); }
1533
+ float sfbm(vec3 p) { return sfbm(p, 5); }
1534
+ vec3 sfbmd(vec2 p) { return sfbmd(p, 5); }
1535
+ vec4 sfbmd(vec3 p) { return sfbmd(p, 5); }
1536
+
1537
+ // cellular noise: each unit cell's own point, from its hash; the nearest and second nearest of the
1538
+ // 3 x 3 (or 3 x 3 x 3) cells round p - F1, F2 - and the nearest cell's own random number
1539
+ vec3 voronoi(vec2 p) {
1540
+ vec2 i = floor(p), f = p - i;
1541
+ highp ivec2 c = ivec2(i);
1542
+ float f1 = 8.0, f2 = 8.0, id = 0.0;
1543
+ for (int y = -1; y <= 1; y++) {
1544
+ for (int x = -1; x <= 1; x++) {
1545
+ highp uint h = phoriaCell(ivec3(c + ivec2(x, y), 11));
1546
+ vec2 o = vec2(float(x), float(y)) + vec2(phoriaUnit(h), phoriaUnit(phoriaMix(h ^ 0x68e31da4u))) - f;
1547
+ float d = dot(o, o);
1548
+ if (d < f1) { f2 = f1; f1 = d; id = phoriaUnit(phoriaMix(h + 0x1b56c4e9u)); }
1549
+ else if (d < f2) f2 = d;
1550
+ }
1551
+ }
1552
+ return vec3(sqrt(f1), sqrt(f2), id);
1553
+ }
1554
+ vec3 voronoi(vec3 p) {
1555
+ vec3 i = floor(p), f = p - i;
1556
+ highp ivec3 c = ivec3(i);
1557
+ float f1 = 8.0, f2 = 8.0, id = 0.0;
1558
+ for (int z = -1; z <= 1; z++) {
1559
+ for (int y = -1; y <= 1; y++) {
1560
+ for (int x = -1; x <= 1; x++) {
1561
+ highp uint h = phoriaCell(c + ivec3(x, y, z)) ^ 0x2545f491u;
1562
+ h = phoriaMix(h);
1563
+ vec3 o = vec3(float(x), float(y), float(z)) - f
1564
+ + vec3(phoriaUnit(h), phoriaUnit(phoriaMix(h ^ 0x68e31da4u)), phoriaUnit(phoriaMix(h ^ 0xb5297a4du)));
1565
+ float d = dot(o, o);
1566
+ if (d < f1) { f2 = f1; f1 = d; id = phoriaUnit(phoriaMix(h + 0x1b56c4e9u)); }
1567
+ else if (d < f2) f2 = d;
1568
+ }
1569
+ }
1570
+ }
1571
+ return vec3(sqrt(f1), sqrt(f2), id);
1572
+ }
1573
+
1574
+ // a colour as picked (sRGB) to the linear colour hooks work in
1575
+ vec3 srgb(vec3 c) { return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(vec3(0.04045), c)); }
1576
+ // hue, saturation and value 0-1 to a linear colour - the colour a colour picker shows
1577
+ vec3 hsv2rgb(vec3 hsv) {
1578
+ vec3 k = clamp(abs(mod(hsv.x * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);
1579
+ return srgb(hsv.z * mix(vec3(1.0), k, hsv.y));
1580
+ }
1581
+ vec3 hsv2rgb(float h, float s, float v) { return hsv2rgb(vec3(h, s, v)); }
1582
+ // a cosine palette (Quilez): a + b cos(2 pi (c t + d)), its four vectors as picked; a linear colour
1583
+ vec3 palette(float t, vec3 a, vec3 b, vec3 c, vec3 d) { return srgb(clamp(a + b * cos(6.28318530718 * (c * t + d)), 0.0, 1.0)); }
1584
+ // how edge-on a surface is seen: 0 facing the camera, 1 side on, sharpened by the power - rim light
1585
+ float fresnel(vec3 normal, vec3 position, float power) {
1586
+ vec3 v = normalize(cameraPosition - position);
1587
+ return pow(1.0 - clamp(abs(dot(normalize(normal), v)), 0.0, 1.0), power);
1588
+ }
1589
+ // turning: rotate2(a) * v anticlockwise by a radians; rotate3(axis, a) * v about an axis
1590
+ mat2 rotate2(float a) { float s = sin(a), c = cos(a); return mat2(c, s, -s, c); }
1591
+ mat3 rotate3(vec3 axis, float a) {
1592
+ vec3 u = normalize(axis);
1593
+ float s = sin(a), c = cos(a), t = 1.0 - c;
1594
+ return mat3(t * u.x * u.x + c, t * u.x * u.y + s * u.z, t * u.x * u.z - s * u.y,
1595
+ t * u.x * u.y - s * u.z, t * u.y * u.y + c, t * u.y * u.z + s * u.x,
1596
+ t * u.x * u.z + s * u.y, t * u.y * u.z - s * u.x, t * u.z * u.z + c);
1597
+ }
1598
+ // x from one range to another, not clamped
1599
+ float remap(float x, float a, float b, float c, float d) { return c + (x - a) * (d - c) / (b - a); }
1600
+ vec2 remap(vec2 x, float a, float b, float c, float d) { return c + (x - a) * (d - c) / (b - a); }
1601
+ vec3 remap(vec3 x, float a, float b, float c, float d) { return c + (x - a) * (d - c) / (b - a); }
1602
+ // clamped to 0-1, as HLSL has it
1603
+ float saturate(float x) { return clamp(x, 0.0, 1.0); }
1604
+ vec2 saturate(vec2 x) { return clamp(x, 0.0, 1.0); }
1605
+ vec3 saturate(vec3 x) { return clamp(x, 0.0, 1.0); }
1606
+ vec4 saturate(vec4 x) { return clamp(x, 0.0, 1.0); }
1607
+ `
1608
+
1609
+ // a hook's own lines are numbered from 1 in a source string of their own, so compile errors point
1610
+ // at the line of the hook, not of the generated shader
1611
+ export const HOOK_SOURCE_NUMBERS = { vertex: 101, fragment: 102, lit: 103 }
1612
+
1613
+ /**
1614
+ * Vertex and fragment source for a kind of drawable, or a pass's own pair, with a set of feature
1615
+ * defines - and a material's shader hooks, when it has them.
1224
1616
  * @param {ShaderKind | [string, string]} kind
1225
1617
  * @param {string[]} defines e.g. ['LIT', 'MAP']
1618
+ * @param {HookSource | null} [hooks]
1226
1619
  * @returns {[string, string]}
1227
1620
  */
1228
- export function shaderSource(kind, defines) {
1621
+ export function shaderSource(kind, defines, hooks = null) {
1229
1622
  const header = `#version 300 es\n${defines.map((d) => `#define ${d}\n`).join('')}`
1230
1623
  const [vs, fs] = typeof kind === 'string' ? SOURCES[kind] : kind
1231
- return [header + vs, header + fs]
1624
+ if (!hooks) return [header + vs, header + fs]
1625
+ const first = 2 + defines.length
1626
+ return [header + splice(vs, hooks, first), header + splice(fs, hooks, first)]
1627
+ }
1628
+
1629
+ /** @param {string} source @param {HookSource} hooks @param {number} first the line number of the source's first line */
1630
+ function splice(source, hooks, first) {
1631
+ return source.split('\n').map((line, i) => {
1632
+ const at = /^\s*\/\/@hook (\w+)$/.exec(line)
1633
+ if (!at) return line
1634
+ const part = /** @type {keyof HookSource} */ (at[1])
1635
+ // the shader's own numbering carries on after the hook as if it weren't there
1636
+ const resume = `#line ${first + i + 1} 0`
1637
+ if (part === 'uniforms') return `${hooks.uniforms}\n${hooks.helpers ?? ''}\n${resume}`
1638
+ return `#line 1 ${HOOK_SOURCE_NUMBERS[part]}\n${hooks[part]}\n${resume}`
1639
+ }).join('\n')
1232
1640
  }