phoria2 1.2.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 (69) hide show
  1. package/README.md +11 -9
  2. package/dist/bloom.js +4 -4
  3. package/dist/bloom.js.map +1 -1
  4. package/dist/charts.js +527 -510
  5. package/dist/charts.js.map +1 -1
  6. package/dist/core.js +1760 -1170
  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 +60 -60
  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 +155 -155
  15. package/dist/gltf.js.map +1 -1
  16. package/dist/keyboard.js +188 -0
  17. package/dist/keyboard.js.map +1 -0
  18. package/dist/motion.js +35 -35
  19. package/dist/motion.js.map +1 -1
  20. package/dist/particles.js +187 -114
  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 +95 -62
  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 +27 -0
  30. package/dist/types/area.d.ts +18 -0
  31. package/dist/types/debug.d.ts +74 -0
  32. package/dist/types/explain.d.ts +4 -0
  33. package/dist/types/frame.d.ts +5 -0
  34. package/dist/types/geometry/Geometry.d.ts +16 -0
  35. package/dist/types/index.d.ts +2 -0
  36. package/dist/types/material/Material.d.ts +67 -0
  37. package/dist/types/material/Texture.d.ts +18 -2
  38. package/dist/types/particles/Emitter.d.ts +39 -2
  39. package/dist/types/scene/drawables.d.ts +23 -2
  40. package/dist/types/watch.d.ts +8 -1
  41. package/dist/types/webgl/hooks.d.ts +48 -0
  42. package/dist/types/webgl/passes.d.ts +4 -0
  43. package/dist/types/webgl/programs.d.ts +15 -5
  44. package/dist/types/webgl/resources.d.ts +12 -2
  45. package/dist/types/webgl/shaders.d.ts +41 -5
  46. package/llms-full.txt +275 -1
  47. package/llms.txt +1 -1
  48. package/package.json +1 -1
  49. package/schema/scene.schema.json +75 -0
  50. package/src/View.js +121 -7
  51. package/src/area.js +131 -0
  52. package/src/charts/marks.js +29 -2
  53. package/src/debug.js +283 -0
  54. package/src/explain.js +19 -0
  55. package/src/frame.js +7 -2
  56. package/src/geometry/Geometry.js +16 -1
  57. package/src/index.js +1 -0
  58. package/src/material/Material.js +130 -4
  59. package/src/material/Texture.js +55 -2
  60. package/src/particles/Emitter.js +121 -10
  61. package/src/scene/drawables.js +89 -8
  62. package/src/watch.js +26 -3
  63. package/src/webgl/WebGLRenderer.js +55 -7
  64. package/src/webgl/hooks.js +121 -0
  65. package/src/webgl/passes.js +17 -2
  66. package/src/webgl/picking.js +45 -3
  67. package/src/webgl/programs.js +20 -9
  68. package/src/webgl/resources.js +169 -15
  69. package/src/webgl/shaders.js +545 -38
@@ -12,12 +12,18 @@ export const ATTRIBUTES = {
12
12
  instanceColor: 8,
13
13
  tangent: 9, // brushed metal's direction: along the UVs' u, and which way their v runs
14
14
  uv2: 10, // a second UV set, for baked occlusion and light maps
15
+ // points drawn as squares (sprites): each one's size, opacity and angle, and which point it is
16
+ pointSize: 11,
17
+ pointOpacity: 12,
18
+ pointAngle: 13,
19
+ pointIndex: 14,
15
20
  // lines: per-segment endpoints and colours, and the corner of the quad being drawn
16
21
  segmentStart: 0,
17
22
  segmentEnd: 1,
18
23
  segmentColorStart: 2,
19
24
  segmentColorEnd: 3,
20
25
  corner: 4,
26
+ segmentInfo: 5, // how far along its chain the segment starts, and whether it starts or ends one
21
27
  }
22
28
 
23
29
  // a section plane: fragments on its far side are cut away
@@ -110,9 +116,17 @@ export const CLIP_ONLY_FRAGMENT = /* glsl */ `
110
116
  precision highp float;
111
117
  in vec3 vWorldPosition;
112
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
113
128
  #ifdef ALPHA_TEST
114
129
  // a cut-out casts the shadow of what is left of it - leaves, fences, grilles - not of its rectangle
115
- in vec2 vUv;
116
130
  uniform float uOpacity;
117
131
  uniform float uAlphaTest;
118
132
  #ifdef MAP
@@ -123,6 +137,15 @@ ${SAMPLING}
123
137
  ${CLIP}
124
138
  void main() {
125
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
126
149
  #ifdef ALPHA_TEST
127
150
  float alpha = uOpacity;
128
151
  #ifdef MAP
@@ -219,6 +242,7 @@ uniform int uHighlightIndex; // -2 none, -1 the whole node, otherwise the instan
219
242
  uniform mat4 uView;
220
243
  out float vFogDepth;
221
244
  #endif
245
+ //@hook uniforms
222
246
 
223
247
  out vec3 vWorldPosition;
224
248
  out vec3 vNormal;
@@ -242,25 +266,34 @@ ${COMMON}
242
266
  void main() {
243
267
  vColor = vec3(1.0);
244
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
245
278
  #ifdef INSTANCED
246
- vec4 local = instanceMatrix * vec4(position, 1.0);
279
+ vec4 placed = instanceMatrix * vec4(local, 1.0);
247
280
  // the instance's normal matrix as its cofactor: the inverse transpose up to scale, from three
248
281
  // cross products, flipped back for mirroring transforms
249
282
  mat3 m = mat3(instanceMatrix);
250
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])));
251
- vec3 n = instanceNormal * normal;
284
+ vec3 placedNormal = instanceNormal * n;
252
285
  vColor *= instanceColor;
253
286
  if (gl_InstanceID == uHighlightIndex) vHighlight = 1.0;
254
287
  #else
255
- vec4 local = vec4(position, 1.0);
256
- vec3 n = normal;
288
+ vec4 placed = vec4(local, 1.0);
289
+ vec3 placedNormal = n;
257
290
  #endif
258
291
  #ifdef VERTEX_COLORS
259
292
  vColor *= srgbToLinear(color);
260
293
  #endif
261
- vec4 world = uModel * local;
294
+ vec4 world = uModel * placed;
262
295
  vWorldPosition = world.xyz;
263
- vNormal = uNormalMatrix * n;
296
+ vNormal = uNormalMatrix * placedNormal;
264
297
  vUv = uv;
265
298
  #ifdef UV2
266
299
  vUv2 = uv2;
@@ -274,8 +307,8 @@ void main() {
274
307
  #endif
275
308
  #endif
276
309
  #ifdef TRIPLANAR
277
- vObjectPosition = position;
278
- vObjectNormal = normal;
310
+ vObjectPosition = local;
311
+ vObjectNormal = n;
279
312
  #ifdef INSTANCED
280
313
  vObjectToWorld = uNormalMatrix * instanceNormal;
281
314
  #else
@@ -316,6 +349,7 @@ uniform float uAlphaTest;
316
349
  #ifdef EMISSIVE_MAP
317
350
  uniform sampler2D uEmissiveMap;
318
351
  #endif
352
+ //@hook uniforms
319
353
 
320
354
  ${SAMPLING}
321
355
  #ifdef LIT
@@ -732,6 +766,25 @@ void main() {
732
766
  vec4 base = vec4(uColor * vColor, uOpacity);
733
767
  #ifdef MAP
734
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
+ }
735
788
  #endif
736
789
  #ifdef ALPHA_TEST
737
790
  if (base.a < uAlphaTest) discard;
@@ -739,19 +792,18 @@ void main() {
739
792
  #endif
740
793
  // how much of what is behind it this hides: its opacity - less for glass, which lets light through
741
794
  float coverage = base.a;
742
- vec3 emissive = uEmissive;
743
- #ifdef EMISSIVE_MAP
744
- emissive *= uEmissiveMapTexel.rgb;
745
- #endif
746
795
 
747
796
  #ifdef LIT
748
- #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)
749
801
  // from the screen-space slope: it faces the camera whichever side of the face is seen
750
802
  vec3 n = normalize(cross(dFdx(vWorldPosition), dFdy(vWorldPosition)));
751
803
  #else
752
804
  vec3 n = normalize(vNormal);
753
805
  #endif
754
- #if defined(DOUBLE_SIDED) && !defined(FLAT_SHADING)
806
+ #if defined(DOUBLE_SIDED) && !defined(FLAT_SHADING) && !defined(HOOK_FRAGMENT)
755
807
  if (!gl_FrontFacing) n = -n;
756
808
  #endif
757
809
  vec3 surfaceNormal = n;
@@ -942,6 +994,14 @@ void main() {
942
994
  #endif
943
995
  #else
944
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
+ }
945
1005
  #endif
946
1006
  color = mix(color, uHighlight.rgb, vHighlight * uHighlight.a);
947
1007
  // a highlight shows on glass as on anything else
@@ -957,6 +1017,17 @@ void main() {
957
1017
  export const POINTS_VERTEX = /* glsl */ `
958
1018
  layout(location = 0) in vec3 position;
959
1019
  layout(location = 3) in vec3 color;
1020
+ #ifdef SPRITES
1021
+ // each point a square: one instance a point, drawn as a strip over the corners (along 0-1, across -1-1)
1022
+ layout(location = 4) in vec2 corner;
1023
+ layout(location = 11) in float pointSize;
1024
+ layout(location = 12) in float pointOpacity;
1025
+ layout(location = 13) in float pointAngle;
1026
+ layout(location = 14) in float pointIndex;
1027
+ uniform vec2 uResolution; // drawing buffer size in pixels
1028
+ out vec2 vUv;
1029
+ out float vOpacity;
1030
+ #endif
960
1031
 
961
1032
  uniform mat4 uModel;
962
1033
  uniform mat4 uView;
@@ -964,9 +1035,13 @@ uniform mat4 uProjection;
964
1035
  uniform float uSize; // pixels (with the pixel ratio applied), or world units when attenuated
965
1036
  uniform float uScale; // projection focal term x half the buffer height: world size -> pixels at depth 1
966
1037
  uniform int uHighlightIndex; // -2 none, -1 every point, otherwise the point
1038
+ //@hook uniforms
967
1039
 
968
1040
  out vec3 vColor;
969
1041
  flat out float vHighlight;
1042
+ #ifdef HOOK_FRAGMENT
1043
+ flat out float vPointIndex;
1044
+ #endif
970
1045
  #ifdef PICK
971
1046
  flat out uint vPickElement; // which instance, point or segment, for GPU picking
972
1047
  #endif
@@ -977,25 +1052,65 @@ out float vFogDepth;
977
1052
  ${COMMON}
978
1053
 
979
1054
  void main() {
980
- 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));
1075
+ #ifdef SPRITES
1076
+ // the square's corner, turned by the point's angle: the texture turns with it
1077
+ vec2 c = vec2(corner.x * 2.0 - 1.0, corner.y);
1078
+ float s = sin(pointAngle), k = cos(pointAngle);
1079
+ vec2 turned = vec2(c.x * k - c.y * s, c.x * s + c.y * k);
1080
+ float across = uSize * size;
1081
+ #ifdef SIZE_ATTENUATION
1082
+ // world units: the square sits in the view, facing the camera
1083
+ viewPosition.xy += turned * across * 0.5;
1084
+ gl_Position = uProjection * viewPosition;
1085
+ #else
1086
+ // pixels: the square is laid out on the screen
1087
+ gl_Position = uProjection * viewPosition;
1088
+ gl_Position.xy += turned * across / uResolution * gl_Position.w;
1089
+ #endif
1090
+ // the image upright on the square, its top at the top, as a point's is
1091
+ vUv = vec2(c.x * 0.5 + 0.5, 0.5 - c.y * 0.5);
1092
+ vOpacity = pointOpacity;
1093
+ #else
981
1094
  gl_Position = uProjection * viewPosition;
1095
+ #endif
982
1096
  #ifdef FOG
983
1097
  vFogDepth = -viewPosition.z;
984
1098
  #endif
985
- #ifdef SIZE_ATTENUATION
1099
+ #ifdef SPRITES
1100
+ #elif defined(SIZE_ATTENUATION)
986
1101
  // perspective shrinks with distance; an orthographic projection (w row 0, 0, 0, 1) doesn't
987
1102
  float depth = uProjection[3][3] == 1.0 ? 1.0 : max(-viewPosition.z, 1e-4);
988
- gl_PointSize = uSize * uScale / depth;
1103
+ gl_PointSize = uSize * size * uScale / depth;
989
1104
  #else
990
- gl_PointSize = uSize;
1105
+ gl_PointSize = uSize * size;
991
1106
  #endif
992
1107
  vColor = vec3(1.0);
993
1108
  #ifdef VERTEX_COLORS
994
1109
  vColor = srgbToLinear(color);
995
1110
  #endif
996
- vHighlight = uHighlightIndex == -1 || gl_VertexID == uHighlightIndex ? 1.0 : 0.0;
1111
+ vHighlight = uHighlightIndex == -1 || self == uHighlightIndex ? 1.0 : 0.0;
997
1112
  #ifdef PICK
998
- vPickElement = uint(gl_VertexID);
1113
+ vPickElement = uint(self);
999
1114
  #endif
1000
1115
  }
1001
1116
  `
@@ -1004,31 +1119,57 @@ const POINTS_FRAGMENT = /* glsl */ `
1004
1119
  precision highp float;
1005
1120
  in vec3 vColor;
1006
1121
  flat in float vHighlight;
1122
+ #ifdef SPRITES
1123
+ in vec2 vUv;
1124
+ in float vOpacity;
1125
+ #endif
1007
1126
  uniform vec4 uHighlight;
1008
1127
  uniform vec3 uColor;
1009
1128
  uniform float uOpacity;
1010
1129
  #ifdef MAP
1011
1130
  uniform sampler2D uMap;
1012
1131
  #endif
1132
+ #ifdef HOOK_FRAGMENT
1133
+ flat in float vPointIndex;
1134
+ #endif
1135
+ //@hook uniforms
1013
1136
  out vec4 fragColor;
1014
1137
 
1015
1138
  ${COMMON}
1016
1139
  ${FOG}
1017
1140
 
1018
1141
  void main() {
1142
+ #ifdef SPRITES
1143
+ vec2 at = vUv;
1144
+ vec4 base = vec4(uColor * vColor, uOpacity * vOpacity);
1145
+ #else
1146
+ vec2 at = gl_PointCoord;
1019
1147
  vec4 base = vec4(uColor * vColor, uOpacity);
1148
+ #endif
1020
1149
  #ifdef MAP
1021
- base *= texture(uMap, vec2(gl_PointCoord.x, gl_PointCoord.y));
1150
+ base *= texture(uMap, at);
1022
1151
  #else
1023
1152
  // round points with a one pixel soft edge
1024
- vec2 p = gl_PointCoord * 2.0 - 1.0;
1153
+ vec2 p = at * 2.0 - 1.0;
1025
1154
  float r = dot(p, p);
1026
1155
  float edge = fwidth(r);
1027
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
+ }
1028
1164
  #endif
1029
1165
  if (base.a <= 0.0) discard;
1030
- base.rgb = fogged(mix(base.rgb, uHighlight.rgb, vHighlight * uHighlight.a));
1031
- 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);
1032
1173
  }
1033
1174
  `
1034
1175
 
@@ -1037,7 +1178,9 @@ layout(location = 0) in vec3 segmentStart;
1037
1178
  layout(location = 1) in vec3 segmentEnd;
1038
1179
  layout(location = 2) in vec3 segmentColorStart;
1039
1180
  layout(location = 3) in vec3 segmentColorEnd;
1040
- layout(location = 4) in vec2 corner; // x: 0 at start, 1 at end; y: -1 or 1 across
1181
+ // the distance along its chain (segments joined end to start) where it starts, in the model's
1182
+ // units; 1 if it starts a chain; 1 if it ends one
1183
+ layout(location = 5) in vec3 segmentInfo;
1041
1184
 
1042
1185
  uniform mat4 uModel;
1043
1186
  uniform mat4 uView;
@@ -1045,8 +1188,24 @@ uniform mat4 uProjection;
1045
1188
  uniform vec2 uResolution; // drawing buffer size in pixels
1046
1189
  uniform float uWidth; // pixels, already scaled by the pixel ratio
1047
1190
  uniform float uNear;
1191
+ uniform int uArrows; // arrowheads: 1 at the start of each chain, 2 at its end, 3 at both
1192
+ uniform float uArrowSize; // their length in pixels, scaled by the pixel ratio
1193
+ //@hook uniforms
1194
+
1195
+ // six vertices an instance make the segment's quad, as two triangles - x: 0 at its start, 1 at its
1196
+ // end; y: -1 or 1 across - and six more its two arrowheads, drawn only when asked for
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));
1048
1198
 
1049
1199
  out vec3 vColor;
1200
+ #if defined(DASHED) || defined(HOOK_FRAGMENT)
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
1207
+ flat out float vArrow; // 1 on an arrowhead, which is never dashed
1208
+ #endif
1050
1209
  #ifdef PICK
1051
1210
  flat out uint vPickElement; // which instance, point or segment, for GPU picking
1052
1211
  #endif
@@ -1056,9 +1215,22 @@ out float vFogDepth;
1056
1215
 
1057
1216
  ${COMMON}
1058
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
+
1059
1226
  void main() {
1060
- vec4 a = uView * (uModel * vec4(segmentStart, 1.0));
1061
- 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));
1062
1234
  // clip the segment to the near plane so lines passing behind the camera stay correct
1063
1235
  float clipZ = -uNear;
1064
1236
  if (a.z > clipZ && b.z > clipZ) {
@@ -1077,21 +1249,53 @@ void main() {
1077
1249
  dir = len > 0.0 ? dir / len : vec2(1.0, 0.0);
1078
1250
  vec2 normal = vec2(-dir.y, dir.x);
1079
1251
 
1080
- vec4 clip = mix(ca, cb, corner.x);
1252
+ bool arrowStart = (uArrows & 1) != 0 && segmentInfo.y > 0.5;
1253
+ bool arrowEnd = (uArrows & 2) != 0 && segmentInfo.z > 0.5;
1254
+ float along, arrow = 0.0;
1255
+ vec2 offset;
1256
+ if (gl_VertexID < 6) {
1257
+ vec2 c = QUAD[gl_VertexID];
1258
+ along = c.x;
1259
+ // past each end by half the width so joins look continuous - or short of it, under an arrowhead
1260
+ float startOut = arrowStart ? -uArrowSize * 0.8 : uWidth * 0.5;
1261
+ float endOut = arrowEnd ? -uArrowSize * 0.8 : uWidth * 0.5;
1262
+ offset = normal * c.y * uWidth * 0.5 + dir * (along > 0.5 ? endOut : -startOut);
1263
+ } else {
1264
+ // an arrowhead: its tip on the chain's end, pointing out along the segment
1265
+ int which = (gl_VertexID - 6) / 3, k = gl_VertexID - 6 - which * 3;
1266
+ if (which == 0 ? !arrowStart : !arrowEnd) {
1267
+ gl_Position = vec4(2.0, 2.0, 2.0, 1.0);
1268
+ return;
1269
+ }
1270
+ along = float(which);
1271
+ vec2 d = which == 1 ? dir : -dir;
1272
+ float wing = uArrowSize * 0.45;
1273
+ offset = k == 0 ? vec2(0.0) : -d * uArrowSize + normal * (k == 1 ? wing : -wing);
1274
+ arrow = 1.0;
1275
+ }
1276
+
1277
+ vec4 clip = mix(ca, cb, along);
1081
1278
  #ifdef FOG
1082
- vFogDepth = -mix(a.z, b.z, corner.x);
1279
+ vFogDepth = -mix(a.z, b.z, along);
1083
1280
  #endif
1084
- // extend past each end by half the width so joins look continuous
1085
- vec2 offset = normal * corner.y * uWidth * 0.5 + dir * (corner.x * 2.0 - 1.0) * uWidth * 0.5;
1086
1281
  clip.xy += offset / (uResolution * 0.5) * clip.w;
1087
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
1289
+ #ifdef DASHED
1290
+ vArrow = arrow;
1291
+ #endif
1088
1292
  #ifdef PICK
1089
1293
  vPickElement = uint(gl_InstanceID);
1090
1294
  #endif
1091
1295
 
1092
1296
  vColor = vec3(1.0);
1093
1297
  #ifdef VERTEX_COLORS
1094
- vColor = srgbToLinear(mix(segmentColorStart, segmentColorEnd, corner.x));
1298
+ vColor = srgbToLinear(mix(segmentColorStart, segmentColorEnd, along));
1095
1299
  #endif
1096
1300
  }
1097
1301
  `
@@ -1101,14 +1305,44 @@ precision highp float;
1101
1305
  in vec3 vColor;
1102
1306
  uniform vec3 uColor;
1103
1307
  uniform float uOpacity;
1308
+ #if defined(DASHED) || defined(HOOK_FRAGMENT)
1309
+ in float vDistance;
1310
+ #endif
1311
+ #ifdef HOOK_FRAGMENT
1312
+ flat in float vSegment;
1313
+ #endif
1314
+ //@hook uniforms
1315
+ #ifdef DASHED
1316
+ flat in float vArrow;
1317
+ uniform float uDash; // dash and gap lengths, in the model's units
1318
+ uniform float uGap;
1319
+ uniform float uDashOffset; // moves the dashes along: animate it for flow
1320
+ #endif
1104
1321
  out vec4 fragColor;
1105
1322
 
1106
1323
  ${COMMON}
1107
1324
  ${FOG}
1108
1325
 
1109
1326
  void main() {
1110
- vec3 color = fogged(uColor * vColor);
1111
- fragColor = vec4(linearToSrgb(color) * uOpacity, uOpacity);
1327
+ #ifdef DASHED
1328
+ if (vArrow < 0.5 && mod(vDistance - uDashOffset, uDash + uGap) >= uDash) discard;
1329
+ #endif
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);
1112
1346
  }
1113
1347
  `
1114
1348
 
@@ -1121,13 +1355,286 @@ const SOURCES = {
1121
1355
  }
1122
1356
 
1123
1357
  /**
1124
- * 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.
1125
1616
  * @param {ShaderKind | [string, string]} kind
1126
1617
  * @param {string[]} defines e.g. ['LIT', 'MAP']
1618
+ * @param {HookSource | null} [hooks]
1127
1619
  * @returns {[string, string]}
1128
1620
  */
1129
- export function shaderSource(kind, defines) {
1621
+ export function shaderSource(kind, defines, hooks = null) {
1130
1622
  const header = `#version 300 es\n${defines.map((d) => `#define ${d}\n`).join('')}`
1131
1623
  const [vs, fs] = typeof kind === 'string' ? SOURCES[kind] : kind
1132
- 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')
1133
1640
  }