@guinetik/gcanvas 1.0.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 (349) hide show
  1. package/.github/workflows/release.yaml +70 -0
  2. package/.jshintrc +4 -0
  3. package/.vscode/settings.json +22 -0
  4. package/CLAUDE.md +310 -0
  5. package/blackhole.jpg +0 -0
  6. package/demo.png +0 -0
  7. package/demos/CNAME +1 -0
  8. package/demos/animations.html +31 -0
  9. package/demos/basic.html +38 -0
  10. package/demos/baskara.html +31 -0
  11. package/demos/bezier.html +35 -0
  12. package/demos/beziersignature.html +29 -0
  13. package/demos/blackhole.html +28 -0
  14. package/demos/blob.html +35 -0
  15. package/demos/demos.css +289 -0
  16. package/demos/easing.html +28 -0
  17. package/demos/events.html +195 -0
  18. package/demos/fluent.html +647 -0
  19. package/demos/fractals.html +36 -0
  20. package/demos/genart.html +26 -0
  21. package/demos/gendream.html +26 -0
  22. package/demos/group.html +36 -0
  23. package/demos/home.html +587 -0
  24. package/demos/index.html +364 -0
  25. package/demos/isometric.html +34 -0
  26. package/demos/js/animations.js +452 -0
  27. package/demos/js/basic.js +204 -0
  28. package/demos/js/baskara.js +751 -0
  29. package/demos/js/bezier.js +692 -0
  30. package/demos/js/beziersignature.js +241 -0
  31. package/demos/js/blackhole/accretiondisk.obj.js +379 -0
  32. package/demos/js/blackhole/blackhole.obj.js +318 -0
  33. package/demos/js/blackhole/index.js +409 -0
  34. package/demos/js/blackhole/particle.js +56 -0
  35. package/demos/js/blackhole/starfield.obj.js +218 -0
  36. package/demos/js/blob.js +2263 -0
  37. package/demos/js/easing.js +477 -0
  38. package/demos/js/fluent.js +183 -0
  39. package/demos/js/fractals.js +931 -0
  40. package/demos/js/fractalworker.js +93 -0
  41. package/demos/js/genart.js +268 -0
  42. package/demos/js/gendream.js +209 -0
  43. package/demos/js/group.js +140 -0
  44. package/demos/js/info-toggle.js +25 -0
  45. package/demos/js/isometric.js +863 -0
  46. package/demos/js/kerr.js +1556 -0
  47. package/demos/js/lavalamp.js +590 -0
  48. package/demos/js/layout.js +354 -0
  49. package/demos/js/mondrian.js +285 -0
  50. package/demos/js/opacity.js +275 -0
  51. package/demos/js/painter.js +484 -0
  52. package/demos/js/particles-showcase.js +514 -0
  53. package/demos/js/particles.js +299 -0
  54. package/demos/js/patterns.js +397 -0
  55. package/demos/js/penrose/artifact.js +69 -0
  56. package/demos/js/penrose/blackhole.js +121 -0
  57. package/demos/js/penrose/constants.js +73 -0
  58. package/demos/js/penrose/game.js +943 -0
  59. package/demos/js/penrose/lore.js +278 -0
  60. package/demos/js/penrose/penrosescene.js +892 -0
  61. package/demos/js/penrose/ship.js +216 -0
  62. package/demos/js/penrose/sounds.js +211 -0
  63. package/demos/js/penrose/voidparticle.js +55 -0
  64. package/demos/js/penrose/voidscene.js +258 -0
  65. package/demos/js/penrose/voidship.js +144 -0
  66. package/demos/js/penrose/wormhole.js +46 -0
  67. package/demos/js/pipeline.js +555 -0
  68. package/demos/js/scene.js +304 -0
  69. package/demos/js/scenes.js +320 -0
  70. package/demos/js/schrodinger.js +410 -0
  71. package/demos/js/schwarzschild.js +1023 -0
  72. package/demos/js/shapes.js +628 -0
  73. package/demos/js/space/alien.js +171 -0
  74. package/demos/js/space/boom.js +98 -0
  75. package/demos/js/space/boss.js +353 -0
  76. package/demos/js/space/buff.js +73 -0
  77. package/demos/js/space/bullet.js +102 -0
  78. package/demos/js/space/constants.js +85 -0
  79. package/demos/js/space/game.js +1884 -0
  80. package/demos/js/space/hud.js +112 -0
  81. package/demos/js/space/laserbeam.js +179 -0
  82. package/demos/js/space/lightning.js +277 -0
  83. package/demos/js/space/minion.js +192 -0
  84. package/demos/js/space/missile.js +212 -0
  85. package/demos/js/space/player.js +430 -0
  86. package/demos/js/space/powerup.js +90 -0
  87. package/demos/js/space/starfield.js +58 -0
  88. package/demos/js/space/starpower.js +90 -0
  89. package/demos/js/spacetime.js +559 -0
  90. package/demos/js/svgtween.js +204 -0
  91. package/demos/js/tde/accretiondisk.js +418 -0
  92. package/demos/js/tde/blackhole.js +219 -0
  93. package/demos/js/tde/blackholescene.js +209 -0
  94. package/demos/js/tde/config.js +59 -0
  95. package/demos/js/tde/index.js +695 -0
  96. package/demos/js/tde/jets.js +290 -0
  97. package/demos/js/tde/lensedstarfield.js +147 -0
  98. package/demos/js/tde/tdestar.js +317 -0
  99. package/demos/js/tde/tidalstream.js +356 -0
  100. package/demos/js/tde_old/blackhole.obj.js +354 -0
  101. package/demos/js/tde_old/debris.obj.js +791 -0
  102. package/demos/js/tde_old/flare.obj.js +239 -0
  103. package/demos/js/tde_old/index.js +448 -0
  104. package/demos/js/tde_old/star.obj.js +812 -0
  105. package/demos/js/tiles.js +312 -0
  106. package/demos/js/tweendemo.js +79 -0
  107. package/demos/js/visibility.js +102 -0
  108. package/demos/kerr.html +28 -0
  109. package/demos/lavalamp.html +27 -0
  110. package/demos/layouts.html +37 -0
  111. package/demos/logo.svg +4 -0
  112. package/demos/loop.html +84 -0
  113. package/demos/mondrian.html +32 -0
  114. package/demos/og_image.png +0 -0
  115. package/demos/opacity.html +36 -0
  116. package/demos/painter.html +39 -0
  117. package/demos/particles-showcase.html +28 -0
  118. package/demos/particles.html +24 -0
  119. package/demos/patterns.html +33 -0
  120. package/demos/penrose-game.html +31 -0
  121. package/demos/pipeline.html +737 -0
  122. package/demos/scene.html +33 -0
  123. package/demos/scenes.html +96 -0
  124. package/demos/schrodinger.html +27 -0
  125. package/demos/schwarzschild.html +27 -0
  126. package/demos/shapes.html +16 -0
  127. package/demos/space.html +85 -0
  128. package/demos/spacetime.html +27 -0
  129. package/demos/svgtween.html +29 -0
  130. package/demos/tde.html +28 -0
  131. package/demos/tiles.html +28 -0
  132. package/demos/transforms.html +400 -0
  133. package/demos/tween.html +45 -0
  134. package/demos/visibility.html +33 -0
  135. package/disk_example.png +0 -0
  136. package/docs/README.md +222 -0
  137. package/docs/concepts/architecture-overview.md +204 -0
  138. package/docs/concepts/lifecycle.md +255 -0
  139. package/docs/concepts/rendering-pipeline.md +279 -0
  140. package/docs/concepts/tde-zorder.md +106 -0
  141. package/docs/concepts/two-layer-architecture.md +229 -0
  142. package/docs/getting-started/first-game.md +354 -0
  143. package/docs/getting-started/hello-world.md +269 -0
  144. package/docs/getting-started/installation.md +157 -0
  145. package/docs/modules/collision/README.md +453 -0
  146. package/docs/modules/fluent/README.md +1075 -0
  147. package/docs/modules/game/README.md +303 -0
  148. package/docs/modules/isometric-camera.md +210 -0
  149. package/docs/modules/isometric.md +275 -0
  150. package/docs/modules/painter/README.md +328 -0
  151. package/docs/modules/particle/README.md +559 -0
  152. package/docs/modules/shapes/README.md +221 -0
  153. package/docs/modules/shapes/base/euclidian.md +123 -0
  154. package/docs/modules/shapes/base/geometry2d.md +204 -0
  155. package/docs/modules/shapes/base/renderable.md +215 -0
  156. package/docs/modules/shapes/base/shape.md +262 -0
  157. package/docs/modules/shapes/base/transformable.md +243 -0
  158. package/docs/modules/shapes/hierarchy.md +218 -0
  159. package/docs/modules/state/README.md +577 -0
  160. package/docs/modules/util/README.md +99 -0
  161. package/docs/modules/util/camera3d.md +412 -0
  162. package/docs/modules/util/scene3d.md +395 -0
  163. package/index.html +17 -0
  164. package/jsdoc.json +50 -0
  165. package/package.json +55 -0
  166. package/readme.md +599 -0
  167. package/scripts/build-demo.js +69 -0
  168. package/scripts/bundle4llm.js +276 -0
  169. package/scripts/clearconsole.js +48 -0
  170. package/src/collision/collision-system.js +332 -0
  171. package/src/collision/collision.js +303 -0
  172. package/src/collision/index.js +10 -0
  173. package/src/fluent/fluent-game.js +430 -0
  174. package/src/fluent/fluent-go.js +1060 -0
  175. package/src/fluent/fluent-layer.js +152 -0
  176. package/src/fluent/fluent-scene.js +291 -0
  177. package/src/fluent/index.js +98 -0
  178. package/src/fluent/sketch.js +380 -0
  179. package/src/game/game.js +467 -0
  180. package/src/game/index.js +49 -0
  181. package/src/game/objects/go.js +220 -0
  182. package/src/game/objects/imagego.js +30 -0
  183. package/src/game/objects/index.js +54 -0
  184. package/src/game/objects/isometric-scene.js +260 -0
  185. package/src/game/objects/layoutscene.js +549 -0
  186. package/src/game/objects/scene.js +175 -0
  187. package/src/game/objects/scene3d.js +118 -0
  188. package/src/game/objects/text.js +221 -0
  189. package/src/game/objects/wrapper.js +232 -0
  190. package/src/game/pipeline.js +243 -0
  191. package/src/game/ui/button.js +396 -0
  192. package/src/game/ui/cursor.js +93 -0
  193. package/src/game/ui/fps.js +91 -0
  194. package/src/game/ui/index.js +5 -0
  195. package/src/game/ui/togglebutton.js +93 -0
  196. package/src/game/ui/tooltip.js +249 -0
  197. package/src/index.js +25 -0
  198. package/src/io/events.js +20 -0
  199. package/src/io/index.js +86 -0
  200. package/src/io/input.js +70 -0
  201. package/src/io/keys.js +152 -0
  202. package/src/io/mouse.js +61 -0
  203. package/src/io/touch.js +39 -0
  204. package/src/logger/debugtab.js +138 -0
  205. package/src/logger/index.js +3 -0
  206. package/src/logger/loggable.js +47 -0
  207. package/src/logger/logger.js +113 -0
  208. package/src/math/complex.js +37 -0
  209. package/src/math/constants.js +1 -0
  210. package/src/math/fractal.js +1271 -0
  211. package/src/math/gr.js +201 -0
  212. package/src/math/heat.js +202 -0
  213. package/src/math/index.js +12 -0
  214. package/src/math/noise.js +433 -0
  215. package/src/math/orbital.js +191 -0
  216. package/src/math/patterns.js +1339 -0
  217. package/src/math/penrose.js +259 -0
  218. package/src/math/quantum.js +115 -0
  219. package/src/math/random.js +195 -0
  220. package/src/math/tensor.js +1009 -0
  221. package/src/mixins/anchor.js +131 -0
  222. package/src/mixins/draggable.js +72 -0
  223. package/src/mixins/index.js +2 -0
  224. package/src/motion/bezier.js +132 -0
  225. package/src/motion/bounce.js +58 -0
  226. package/src/motion/easing.js +349 -0
  227. package/src/motion/float.js +130 -0
  228. package/src/motion/follow.js +125 -0
  229. package/src/motion/hop.js +52 -0
  230. package/src/motion/index.js +82 -0
  231. package/src/motion/motion.js +1124 -0
  232. package/src/motion/orbit.js +49 -0
  233. package/src/motion/oscillate.js +39 -0
  234. package/src/motion/parabolic.js +141 -0
  235. package/src/motion/patrol.js +147 -0
  236. package/src/motion/pendulum.js +48 -0
  237. package/src/motion/pulse.js +88 -0
  238. package/src/motion/shake.js +83 -0
  239. package/src/motion/spiral.js +144 -0
  240. package/src/motion/spring.js +150 -0
  241. package/src/motion/swing.js +47 -0
  242. package/src/motion/tween.js +92 -0
  243. package/src/motion/tweenetik.js +139 -0
  244. package/src/motion/waypoint.js +210 -0
  245. package/src/painter/index.js +8 -0
  246. package/src/painter/painter.colors.js +331 -0
  247. package/src/painter/painter.effects.js +230 -0
  248. package/src/painter/painter.img.js +229 -0
  249. package/src/painter/painter.js +295 -0
  250. package/src/painter/painter.lines.js +189 -0
  251. package/src/painter/painter.opacity.js +41 -0
  252. package/src/painter/painter.shapes.js +277 -0
  253. package/src/painter/painter.text.js +273 -0
  254. package/src/particle/emitter.js +124 -0
  255. package/src/particle/index.js +11 -0
  256. package/src/particle/particle-system.js +322 -0
  257. package/src/particle/particle.js +71 -0
  258. package/src/particle/updaters.js +170 -0
  259. package/src/shapes/arc.js +43 -0
  260. package/src/shapes/arrow.js +33 -0
  261. package/src/shapes/bezier.js +42 -0
  262. package/src/shapes/circle.js +62 -0
  263. package/src/shapes/clouds.js +56 -0
  264. package/src/shapes/cone.js +219 -0
  265. package/src/shapes/cross.js +70 -0
  266. package/src/shapes/cube.js +244 -0
  267. package/src/shapes/cylinder.js +254 -0
  268. package/src/shapes/diamond.js +48 -0
  269. package/src/shapes/euclidian.js +111 -0
  270. package/src/shapes/figure.js +115 -0
  271. package/src/shapes/geometry.js +220 -0
  272. package/src/shapes/group.js +375 -0
  273. package/src/shapes/heart.js +42 -0
  274. package/src/shapes/hexagon.js +26 -0
  275. package/src/shapes/image.js +192 -0
  276. package/src/shapes/index.js +111 -0
  277. package/src/shapes/line.js +29 -0
  278. package/src/shapes/pattern.js +90 -0
  279. package/src/shapes/pin.js +44 -0
  280. package/src/shapes/poly.js +31 -0
  281. package/src/shapes/prism.js +226 -0
  282. package/src/shapes/rect.js +35 -0
  283. package/src/shapes/renderable.js +333 -0
  284. package/src/shapes/ring.js +26 -0
  285. package/src/shapes/roundrect.js +95 -0
  286. package/src/shapes/shape.js +117 -0
  287. package/src/shapes/slice.js +26 -0
  288. package/src/shapes/sphere.js +314 -0
  289. package/src/shapes/sphere3d.js +537 -0
  290. package/src/shapes/square.js +15 -0
  291. package/src/shapes/star.js +99 -0
  292. package/src/shapes/svg.js +408 -0
  293. package/src/shapes/text.js +553 -0
  294. package/src/shapes/traceable.js +83 -0
  295. package/src/shapes/transform.js +357 -0
  296. package/src/shapes/transformable.js +172 -0
  297. package/src/shapes/triangle.js +26 -0
  298. package/src/sound/index.js +17 -0
  299. package/src/sound/sound.js +473 -0
  300. package/src/sound/synth.analyzer.js +149 -0
  301. package/src/sound/synth.effects.js +207 -0
  302. package/src/sound/synth.envelope.js +59 -0
  303. package/src/sound/synth.js +229 -0
  304. package/src/sound/synth.musical.js +160 -0
  305. package/src/sound/synth.noise.js +85 -0
  306. package/src/sound/synth.oscillators.js +293 -0
  307. package/src/state/index.js +10 -0
  308. package/src/state/state-machine.js +371 -0
  309. package/src/util/camera3d.js +438 -0
  310. package/src/util/index.js +6 -0
  311. package/src/util/isometric-camera.js +235 -0
  312. package/src/util/layout.js +317 -0
  313. package/src/util/position.js +147 -0
  314. package/src/util/tasks.js +47 -0
  315. package/src/util/zindex.js +287 -0
  316. package/src/webgl/index.js +9 -0
  317. package/src/webgl/shaders/sphere-shaders.js +994 -0
  318. package/src/webgl/webgl-renderer.js +388 -0
  319. package/tde.png +0 -0
  320. package/test/math/orbital.test.js +61 -0
  321. package/test/math/tensor.test.js +114 -0
  322. package/test/particle/emitter.test.js +204 -0
  323. package/test/particle/particle-system.test.js +310 -0
  324. package/test/particle/particle.test.js +116 -0
  325. package/test/particle/updaters.test.js +386 -0
  326. package/test/setup.js +120 -0
  327. package/test/shapes/euclidian.test.js +44 -0
  328. package/test/shapes/geometry.test.js +86 -0
  329. package/test/shapes/group.test.js +86 -0
  330. package/test/shapes/rectangle.test.js +64 -0
  331. package/test/shapes/transform.test.js +379 -0
  332. package/test/util/camera3d.test.js +428 -0
  333. package/test/util/scene3d.test.js +352 -0
  334. package/types/collision.d.ts +249 -0
  335. package/types/common.d.ts +155 -0
  336. package/types/game.d.ts +497 -0
  337. package/types/index.d.ts +309 -0
  338. package/types/io.d.ts +188 -0
  339. package/types/logger.d.ts +127 -0
  340. package/types/math.d.ts +268 -0
  341. package/types/mixins.d.ts +92 -0
  342. package/types/motion.d.ts +678 -0
  343. package/types/painter.d.ts +378 -0
  344. package/types/shapes.d.ts +864 -0
  345. package/types/sound.d.ts +672 -0
  346. package/types/state.d.ts +251 -0
  347. package/types/util.d.ts +253 -0
  348. package/vite.config.js +50 -0
  349. package/vitest.config.js +13 -0
@@ -0,0 +1,994 @@
1
+ /**
2
+ * Sphere Shaders for WebGL Rendering
3
+ *
4
+ * These shaders render a sphere using ray-sphere intersection on a 2D quad.
5
+ * This approach (sometimes called "impostor" or "billboard" spheres) is very
6
+ * efficient as it only requires 2 triangles regardless of sphere detail.
7
+ *
8
+ * The fragment shader does the heavy lifting:
9
+ * 1. Ray-sphere intersection to determine if we hit the sphere
10
+ * 2. Normal calculation for lighting
11
+ * 3. UV calculation for texture/procedural patterns
12
+ * 4. Various procedural effects (noise, gradients, etc.)
13
+ */
14
+
15
+ // =============================================================================
16
+ // VERTEX SHADER
17
+ // =============================================================================
18
+
19
+ /**
20
+ * Standard vertex shader for sphere impostor rendering
21
+ * Simply passes through position and UV coordinates
22
+ */
23
+ export const SPHERE_VERTEX = `
24
+ precision highp float;
25
+
26
+ attribute vec2 aPosition;
27
+ attribute vec2 aUv;
28
+
29
+ varying vec2 vUv;
30
+
31
+ void main() {
32
+ vUv = aUv;
33
+ gl_Position = vec4(aPosition, 0.0, 1.0);
34
+ }
35
+ `;
36
+
37
+ // =============================================================================
38
+ // COMMON SHADER FUNCTIONS
39
+ // =============================================================================
40
+
41
+ /**
42
+ * Common functions included in all fragment shaders
43
+ * - Noise functions
44
+ * - Ray-sphere intersection
45
+ * - Lighting calculations
46
+ */
47
+ export const SPHERE_COMMON = `
48
+ precision highp float;
49
+
50
+ varying vec2 vUv;
51
+
52
+ // Uniforms common to all sphere shaders
53
+ uniform float uTime;
54
+ uniform vec2 uResolution;
55
+ uniform vec3 uCameraRotation; // rotationX, rotationY, rotationZ
56
+
57
+ // =============================================================================
58
+ // NOISE FUNCTIONS
59
+ // =============================================================================
60
+
61
+ float hash(float n) {
62
+ return fract(sin(n) * 43758.5453123);
63
+ }
64
+
65
+ float hash2(vec2 p) {
66
+ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
67
+ }
68
+
69
+ float hash3(vec3 p) {
70
+ return fract(sin(dot(p, vec3(127.1, 311.7, 74.7))) * 43758.5453123);
71
+ }
72
+
73
+ // 3D Value noise
74
+ float noise3D(vec3 x) {
75
+ vec3 i = floor(x);
76
+ vec3 f = fract(x);
77
+ f = f * f * (3.0 - 2.0 * f);
78
+
79
+ float n = dot(i, vec3(1.0, 57.0, 113.0));
80
+
81
+ return mix(
82
+ mix(mix(hash(n + 0.0), hash(n + 1.0), f.x),
83
+ mix(hash(n + 57.0), hash(n + 58.0), f.x), f.y),
84
+ mix(mix(hash(n + 113.0), hash(n + 114.0), f.x),
85
+ mix(hash(n + 170.0), hash(n + 171.0), f.x), f.y), f.z
86
+ );
87
+ }
88
+
89
+ // FBM (Fractional Brownian Motion)
90
+ float fbm(vec3 p, int octaves) {
91
+ float value = 0.0;
92
+ float amplitude = 0.5;
93
+ float frequency = 1.0;
94
+
95
+ for (int i = 0; i < 8; i++) {
96
+ if (i >= octaves) break;
97
+ value += amplitude * noise3D(p * frequency);
98
+ frequency *= 2.0;
99
+ amplitude *= 0.5;
100
+ }
101
+
102
+ return value;
103
+ }
104
+
105
+ // =============================================================================
106
+ // RAY-SPHERE INTERSECTION
107
+ // =============================================================================
108
+
109
+ /**
110
+ * Ray-sphere intersection
111
+ * @param rayOrigin - Ray origin (camera position)
112
+ * @param rayDir - Normalized ray direction
113
+ * @param sphereCenter - Sphere center
114
+ * @param sphereRadius - Sphere radius
115
+ * @return t value for intersection, -1.0 if no hit
116
+ */
117
+ float raySphereIntersect(vec3 rayOrigin, vec3 rayDir, vec3 sphereCenter, float sphereRadius) {
118
+ vec3 oc = rayOrigin - sphereCenter;
119
+ float a = dot(rayDir, rayDir);
120
+ float b = 2.0 * dot(oc, rayDir);
121
+ float c = dot(oc, oc) - sphereRadius * sphereRadius;
122
+ float discriminant = b * b - 4.0 * a * c;
123
+
124
+ if (discriminant < 0.0) {
125
+ return -1.0;
126
+ }
127
+
128
+ return (-b - sqrt(discriminant)) / (2.0 * a);
129
+ }
130
+
131
+ // =============================================================================
132
+ // CAMERA AND ROTATION
133
+ // =============================================================================
134
+
135
+ /**
136
+ * Create rotation matrix from Euler angles
137
+ */
138
+ mat3 rotationMatrix(vec3 rotation) {
139
+ float cx = cos(rotation.x);
140
+ float sx = sin(rotation.x);
141
+ float cy = cos(rotation.y);
142
+ float sy = sin(rotation.y);
143
+ float cz = cos(rotation.z);
144
+ float sz = sin(rotation.z);
145
+
146
+ mat3 rx = mat3(
147
+ 1.0, 0.0, 0.0,
148
+ 0.0, cx, -sx,
149
+ 0.0, sx, cx
150
+ );
151
+
152
+ mat3 ry = mat3(
153
+ cy, 0.0, sy,
154
+ 0.0, 1.0, 0.0,
155
+ -sy, 0.0, cy
156
+ );
157
+
158
+ mat3 rz = mat3(
159
+ cz, -sz, 0.0,
160
+ sz, cz, 0.0,
161
+ 0.0, 0.0, 1.0
162
+ );
163
+
164
+ return rz * ry * rx;
165
+ }
166
+
167
+ /**
168
+ * Calculate ray direction from UV coordinates
169
+ * Uses a simple pinhole camera model
170
+ */
171
+ vec3 getRayDirection(vec2 uv) {
172
+ // Convert UV to normalized device coordinates (-1 to 1)
173
+ vec2 ndc = uv * 2.0 - 1.0;
174
+ // Field of view ~53 degrees (atan(0.5) * 2)
175
+ return normalize(vec3(ndc * 0.5, 1.0));
176
+ }
177
+
178
+ // =============================================================================
179
+ // LIGHTING
180
+ // =============================================================================
181
+
182
+ /**
183
+ * Simple diffuse + ambient lighting
184
+ */
185
+ float lighting(vec3 normal, vec3 lightDir, float ambient) {
186
+ float diffuse = max(0.0, dot(normal, lightDir));
187
+ return ambient + (1.0 - ambient) * diffuse;
188
+ }
189
+
190
+ /**
191
+ * Fresnel effect for rim lighting
192
+ */
193
+ float fresnel(vec3 normal, vec3 viewDir, float power) {
194
+ return pow(1.0 - abs(dot(normal, viewDir)), power);
195
+ }
196
+ `;
197
+
198
+ // =============================================================================
199
+ // STAR SHADER
200
+ // =============================================================================
201
+
202
+ /**
203
+ * Star surface shader with plasma/fire effects, corona, and self-rotation
204
+ * Features:
205
+ * - Boiling plasma surface with spherical UV distortion
206
+ * - Multi-layer corona with flame structures
207
+ * - Hot bubbles that appear and pop
208
+ * - Self-rotation around axis
209
+ * - Temperature-based 4-tier color system
210
+ * - Limb darkening and organic rim glow
211
+ */
212
+ export const STAR_FRAGMENT = `
213
+ ${SPHERE_COMMON}
214
+
215
+ uniform vec3 uStarColor;
216
+ uniform float uTemperature; // Kelvin, affects color
217
+ uniform float uActivityLevel; // 0-1, affects turbulence
218
+ uniform float uRotationSpeed; // Self-rotation speed (radians/second)
219
+
220
+ // Tidal disruption uniforms
221
+ uniform float uTidalStretch; // 0 = sphere, 1+ = elongated toward BH
222
+ uniform float uStretchDirX; // Direction to black hole (X component)
223
+ uniform float uStretchDirZ; // Direction to black hole (Z component)
224
+ uniform float uStressLevel; // 0-1, surface chaos from tidal forces
225
+ uniform float uBaseRadius; // Dynamic base radius for proper sizing
226
+ uniform float uTidalFlare; // 0-1, sudden brightness burst at disruption start
227
+ uniform float uTidalWobble; // 0-1, violent geometry wobble during trauma
228
+
229
+ // =============================================================================
230
+ // TIDAL DISTORTION - True Spaghettification via Ellipsoid Deformation
231
+ // Uses ray-ellipsoid intersection for physically correct stretching
232
+ // =============================================================================
233
+
234
+ /**
235
+ * Ray-Ellipsoid intersection
236
+ * Ellipsoid defined by semi-axes (a, b, c) where:
237
+ * - a = stretch along BH direction (in XZ plane)
238
+ * - b = Y axis (slight compression)
239
+ * - c = perpendicular to BH direction in XZ plane (compression)
240
+ *
241
+ * Technique: Transform ray into "unit sphere space" via inverse scaling
242
+ */
243
+ float rayEllipsoidIntersect(vec3 rayOrigin, vec3 rayDir, vec3 center, vec3 semiAxes) {
244
+ // Scale ray into unit sphere space
245
+ vec3 scaledOrigin = (rayOrigin - center) / semiAxes;
246
+ vec3 scaledDir = rayDir / semiAxes;
247
+
248
+ // Standard ray-sphere intersection in scaled space
249
+ float a = dot(scaledDir, scaledDir);
250
+ float b = 2.0 * dot(scaledOrigin, scaledDir);
251
+ float c = dot(scaledOrigin, scaledOrigin) - 1.0;
252
+ float discriminant = b * b - 4.0 * a * c;
253
+
254
+ if (discriminant < 0.0) {
255
+ return -1.0;
256
+ }
257
+
258
+ return (-b - sqrt(discriminant)) / (2.0 * a);
259
+ }
260
+
261
+ /**
262
+ * Calculate ellipsoid normal at hit point
263
+ * Normal = gradient of ellipsoid equation = 2*(x/a², y/b², z/c²)
264
+ */
265
+ vec3 ellipsoidNormal(vec3 hitPoint, vec3 center, vec3 semiAxes) {
266
+ vec3 localPos = hitPoint - center;
267
+ // Gradient of (x/a)² + (y/b)² + (z/c)² = 1
268
+ vec3 grad = localPos / (semiAxes * semiAxes);
269
+ return normalize(grad);
270
+ }
271
+
272
+ /**
273
+ * Build tidal stretch axes from BH direction
274
+ * Returns semi-axes (stretchAxis, Y, perpAxis) for ellipsoid
275
+ * Includes violent wobble effect during trauma
276
+ */
277
+ vec3 tidalSemiAxes(float stretch, vec2 stretchDir, float baseRadius, float wobble, float time) {
278
+ // Stretch factor along BH direction (elongation toward/away from BH)
279
+ float stretchFactor = 1.0 + stretch * 0.8; // Up to 1.8x longer
280
+
281
+ // Compression factor perpendicular (volume roughly conserved)
282
+ float compressFactor = 1.0 / sqrt(stretchFactor); // Compress to conserve volume
283
+
284
+ // Y axis gets slight compression too
285
+ float yFactor = 1.0 - stretch * 0.15;
286
+
287
+ // === TRAUMA WOBBLE ===
288
+ // Violent, chaotic geometry distortion during tidal shock
289
+ if (wobble > 0.01) {
290
+ // Multiple frequency wobbles for organic chaos
291
+ float wobble1 = sin(time * 12.0) * cos(time * 7.3);
292
+ float wobble2 = sin(time * 19.0 + 1.5) * cos(time * 11.0);
293
+ float wobble3 = sin(time * 8.0 + 3.0);
294
+
295
+ // Asymmetric wobble - more violent on stretch axis
296
+ float stretchWobble = wobble * (0.3 + wobble1 * 0.2 + wobble2 * 0.15);
297
+ float yWobble = wobble * (wobble2 * 0.25 + wobble3 * 0.15);
298
+ float perpWobble = wobble * (wobble3 * 0.2 + wobble1 * 0.1);
299
+
300
+ stretchFactor *= (1.0 + stretchWobble);
301
+ yFactor *= (1.0 + yWobble);
302
+ compressFactor *= (1.0 + perpWobble);
303
+ }
304
+
305
+ return vec3(
306
+ baseRadius * stretchFactor, // Stretch along BH radial
307
+ baseRadius * yFactor, // Slight Y compression
308
+ baseRadius * compressFactor // Compress perpendicular
309
+ );
310
+ }
311
+
312
+ // =============================================================================
313
+ // PLASMA NOISE with flowing distortion
314
+ // =============================================================================
315
+
316
+ float plasmaNoise(vec3 p, float time) {
317
+ float value = 0.0;
318
+ float amplitude = 1.0;
319
+ float frequency = 1.0;
320
+ float totalAmp = 0.0;
321
+
322
+ for (int i = 0; i < 5; i++) {
323
+ vec3 offset = vec3(
324
+ sin(time * 0.1 + float(i)) * 0.5,
325
+ cos(time * 0.15 + float(i) * 0.7) * 0.5,
326
+ time * 0.05
327
+ );
328
+ value += amplitude * noise3D((p + offset) * frequency);
329
+ totalAmp += amplitude;
330
+ amplitude *= 0.5;
331
+ frequency *= 2.0;
332
+ }
333
+
334
+ return value / totalAmp;
335
+ }
336
+
337
+ // =============================================================================
338
+ // HOT BUBBLES - bright spots that appear and pop
339
+ // =============================================================================
340
+
341
+ float hotBubbles(vec3 p, float time) {
342
+ // Large slow bubbles
343
+ vec3 p1 = p * 5.0 + vec3(0.0, time * 0.06, 0.0);
344
+ float b1 = noise3D(p1);
345
+ b1 = smoothstep(0.3, 0.6, b1);
346
+
347
+ // Medium bubbles, faster
348
+ vec3 p2 = p * 9.0 + vec3(time * 0.04, time * 0.08, 0.0);
349
+ float b2 = noise3D(p2);
350
+ b2 = smoothstep(0.35, 0.65, b2);
351
+
352
+ // Small rapid bubbles
353
+ vec3 p3 = p * 16.0 + vec3(time * 0.1, 0.0, time * 0.12);
354
+ float b3 = noise3D(p3);
355
+ b3 = smoothstep(0.4, 0.7, b3);
356
+
357
+ float bubbles = b1 * 0.5 + b2 * 0.35 + b3 * 0.15;
358
+ float pulse = sin(time * 2.0 + p.x * 10.0) * 0.3 + 0.7;
359
+
360
+ return bubbles * pulse;
361
+ }
362
+
363
+ // =============================================================================
364
+ // BOILING TURBULENCE - fast chaotic movement
365
+ // =============================================================================
366
+
367
+ float boilingTurbulence(vec3 p, float time) {
368
+ float turb = 0.0;
369
+ float amp = 1.0;
370
+ float freq = 4.0;
371
+
372
+ for (int i = 0; i < 4; i++) {
373
+ vec3 offset = vec3(
374
+ sin(time * 0.3 + float(i) * 1.7) * 0.5,
375
+ cos(time * 0.25 + float(i) * 2.3) * 0.5,
376
+ time * 0.15 * (1.0 + float(i) * 0.3)
377
+ );
378
+ turb += amp * abs(noise3D(p * freq + offset));
379
+ amp *= 0.5;
380
+ freq *= 2.1;
381
+ }
382
+ return turb;
383
+ }
384
+
385
+ // =============================================================================
386
+ // CORONA FLAMES - structures around the edge
387
+ // =============================================================================
388
+
389
+ float coronaFlames(float angle, float rimFactor, float time, float activity) {
390
+ // Multiple flame frequencies
391
+ float flames = 0.0;
392
+
393
+ // Large slow flames
394
+ float f1 = sin(angle * 5.0 + time * 0.5) * 0.5 + 0.5;
395
+ f1 *= noise3D(vec3(angle * 2.0, time * 0.3, 0.0));
396
+
397
+ // Medium flames
398
+ float f2 = sin(angle * 12.0 + time * 0.8) * 0.5 + 0.5;
399
+ f2 *= noise3D(vec3(angle * 4.0, time * 0.5, 5.0));
400
+
401
+ // Small rapid flames
402
+ float f3 = sin(angle * 25.0 + time * 1.5) * 0.5 + 0.5;
403
+ f3 *= noise3D(vec3(angle * 8.0, time * 0.8, 10.0));
404
+
405
+ flames = f1 * 0.5 + f2 * 0.3 + f3 * 0.2;
406
+
407
+ // Flames only visible at rim
408
+ flames *= pow(rimFactor, 1.5);
409
+ flames *= 0.5 + activity * 0.5;
410
+
411
+ return flames;
412
+ }
413
+
414
+ // =============================================================================
415
+ // SELF ROTATION - rotate normal around Y axis
416
+ // =============================================================================
417
+
418
+ vec3 rotateY(vec3 v, float angle) {
419
+ float c = cos(angle);
420
+ float s = sin(angle);
421
+ return vec3(v.x * c + v.z * s, v.y, -v.x * s + v.z * c);
422
+ }
423
+
424
+ void main() {
425
+ // === CIRCULAR MASK - prevents square canvas artifacts ===
426
+ vec2 center = vUv - 0.5;
427
+ float distFromCenter = length(center) * 2.0;
428
+
429
+ // Wider cutoff for stretched ellipsoid
430
+ if (distFromCenter > 1.6) {
431
+ gl_FragColor = vec4(0.0);
432
+ return;
433
+ }
434
+
435
+ float circularMask = 1.0 - smoothstep(1.3, 1.6, distFromCenter);
436
+
437
+ // Setup ray - camera looking at sphere from fixed position
438
+ vec3 rayOrigin = vec3(0.0, 0.0, -2.5);
439
+ vec3 rayDir = getRayDirection(vUv);
440
+
441
+ float time = uTime;
442
+ float selfRotation = time * uRotationSpeed;
443
+
444
+ // === TIDAL ELLIPSOID SETUP ===
445
+ // Direction toward black hole in XZ plane
446
+ vec2 stretchDir2D = normalize(vec2(uStretchDirX, uStretchDirZ) + 0.0001);
447
+ float stretch = uTidalStretch;
448
+
449
+ // Build rotation matrix to align ellipsoid X-axis with stretch direction
450
+ // This rotates the ellipsoid so its long axis points toward the BH
451
+ float stretchAngle = atan(stretchDir2D.y, stretchDir2D.x);
452
+ float cs = cos(stretchAngle);
453
+ float sn = sin(stretchAngle);
454
+
455
+ // Rotation matrix around Y axis (to align stretch in XZ plane)
456
+ mat3 stretchRot = mat3(
457
+ cs, 0.0, -sn,
458
+ 0.0, 1.0, 0.0,
459
+ sn, 0.0, cs
460
+ );
461
+ mat3 stretchRotInv = mat3(
462
+ cs, 0.0, sn,
463
+ 0.0, 1.0, 0.0,
464
+ -sn, 0.0, cs
465
+ );
466
+
467
+ // Transform ray into ellipsoid-aligned space
468
+ vec3 rotatedRayDir = stretchRotInv * rayDir;
469
+ vec3 rotatedRayOrigin = stretchRotInv * rayOrigin;
470
+
471
+ // Calculate ellipsoid semi-axes based on stretch
472
+ // Use dynamic base radius passed from JS (scales with render texture size)
473
+ // Falls back to 0.4 if uniform not set
474
+ float baseRadius = uBaseRadius > 0.0 ? uBaseRadius : 0.4;
475
+ vec3 semiAxes = tidalSemiAxes(stretch, stretchDir2D, baseRadius, uTidalWobble, time);
476
+
477
+ // Ray-ellipsoid intersection for SURFACE
478
+ float t = rayEllipsoidIntersect(rotatedRayOrigin, rotatedRayDir, vec3(0.0), semiAxes);
479
+
480
+ // === NO CORONA - just render solid ellipsoid ===
481
+ // If ray doesn't hit the surface, render transparent
482
+ if (t < 0.0) {
483
+ gl_FragColor = vec4(0.0);
484
+ return;
485
+ }
486
+
487
+ // === SURFACE RENDERING ===
488
+ // Calculate hit point in rotated (ellipsoid-aligned) space
489
+ vec3 rotatedHitPoint = rotatedRayOrigin + rotatedRayDir * t;
490
+
491
+ // Calculate ellipsoid normal (gradient of implicit surface)
492
+ vec3 rotatedNormalRaw = ellipsoidNormal(rotatedHitPoint, vec3(0.0), semiAxes);
493
+
494
+ // Transform hit point and normal back to world space
495
+ vec3 hitPoint = stretchRot * rotatedHitPoint;
496
+ vec3 normal = normalize(stretchRot * rotatedNormalRaw);
497
+
498
+ // Apply inverse camera rotation to the normal (camera orbit)
499
+ mat3 camRotMat = rotationMatrix(-uCameraRotation);
500
+ vec3 rotatedNormal = camRotMat * normal;
501
+
502
+ // Apply self-rotation to surface features
503
+ rotatedNormal = rotateY(rotatedNormal, selfRotation);
504
+
505
+ // === SPHERICAL DISTORTION for boiling effect ===
506
+ vec2 sp = normal.xy;
507
+ float r = dot(sp, sp);
508
+
509
+ float brightness = 0.15 + (uTemperature / 10000.0) * 0.1;
510
+ float distortStrength = 2.0 - brightness;
511
+
512
+ vec2 warpedUV;
513
+ if (r < 0.0001) {
514
+ // At pole - use alternative coords
515
+ float poleAngle = atan(rotatedNormal.y, rotatedNormal.x) + time * 0.15;
516
+ float poleElev = acos(clamp(rotatedNormal.z, -1.0, 1.0));
517
+ warpedUV = vec2(cos(poleAngle), sin(poleAngle)) * (poleElev / 3.14159) * distortStrength;
518
+ } else {
519
+ sp *= distortStrength;
520
+ r = dot(sp, sp);
521
+ float f = (1.0 - sqrt(abs(1.0 - r))) / (r + 0.001) + brightness * 0.5;
522
+ warpedUV = sp * f + vec2(time * 0.05, 0.0);
523
+ }
524
+
525
+ // === PLASMA TEXTURE ===
526
+ vec3 plasmaCoord = vec3(warpedUV * 3.0, time * 0.12);
527
+ float plasma1 = plasmaNoise(plasmaCoord, time);
528
+ float plasma2 = plasmaNoise(plasmaCoord * 1.3 + vec3(50.0), time * 1.2);
529
+ float plasma = plasma1 * 0.6 + plasma2 * 0.4;
530
+ plasma = plasma * 0.5 + 0.5;
531
+
532
+ // === VIEW GEOMETRY ===
533
+ float viewAngle = dot(normal, -rayDir);
534
+ float edgeDist = 1.0 - viewAngle;
535
+ float limbDarkening = pow(max(0.0, viewAngle), 0.4);
536
+
537
+ // === TIDAL FACE INTENSITY ===
538
+ // The side facing the black hole experiences more violent tidal forces
539
+ // Calculate how much this surface point faces the BH direction
540
+ vec3 bhDir3D = normalize(vec3(uStretchDirX, 0.0, uStretchDirZ));
541
+ float facingBH = dot(normal, bhDir3D); // -1 to 1, positive = facing BH
542
+ float tidalFace = smoothstep(-0.2, 0.8, facingBH); // Gradual transition
543
+ tidalFace = tidalFace * tidalFace; // More concentrated on BH side
544
+
545
+ // Tidal face boost - up to 3x more violent on the BH-facing side
546
+ float tidalFaceBoost = 1.0 + tidalFace * uStressLevel * 2.0;
547
+
548
+ // === MULTI-LAYER EFFECTS (stress-enhanced) ===
549
+ // Stress amplifies all turbulent effects - star is being torn apart!
550
+ // Much more violent - up to 5x chaos at max stress
551
+ float stressBoost = 1.0 + uStressLevel * 4.0;
552
+
553
+ // Combined boost: general stress + extra violence on BH-facing side
554
+ float combinedBoost = stressBoost * tidalFaceBoost;
555
+
556
+ float turbIntensity = boilingTurbulence(rotatedNormal, time * combinedBoost) * 0.6;
557
+ turbIntensity *= combinedBoost;
558
+
559
+ float bubbles = hotBubbles(rotatedNormal, time * combinedBoost);
560
+ bubbles *= combinedBoost * 1.5; // More dramatic bubbles on tidal face
561
+
562
+ // Granulation becomes violent under stress - larger and faster
563
+ float gran = noise3D(rotatedNormal * 15.0 + time * 0.5 * combinedBoost);
564
+ gran *= combinedBoost * 1.2;
565
+
566
+ // === TIDAL FRACTURING ===
567
+ // Stress causes visible cracks/tears - concentrated on BH-facing side
568
+ float fractures = 0.0;
569
+ if (uStressLevel > 0.15) { // Start fractures earlier (was 0.3)
570
+ // Fractures are more intense on the tidal face
571
+ float fractureBoost = 1.0 + tidalFace * 2.0; // Up to 3x on BH side
572
+ float fractureNoise = noise3D(rotatedNormal * 6.0 + time * 0.8 * fractureBoost);
573
+ float fractureThreshold = 1.0 - (uStressLevel - 0.15) * 1.2 * fractureBoost;
574
+ fractures = smoothstep(fractureThreshold, fractureThreshold + 0.08, fractureNoise);
575
+ fractures *= uStressLevel * 1.2 * fractureBoost; // More intense on BH side
576
+ }
577
+
578
+ // === PULSATION (amplified by stress) ===
579
+ float pulse1 = cos(time * 0.5) * 0.5;
580
+ float pulse2 = sin(time * 0.25) * 0.5;
581
+ float pulseAmp = uActivityLevel * (1.0 + uStressLevel);
582
+ float pulse = (pulse1 + pulse2) * 0.3 * pulseAmp;
583
+
584
+ // === TIDAL HOTSPOT ===
585
+ // Bright glowing region on the BH-facing side - like matter being pulled off
586
+ float tidalHotspot = pow(tidalFace, 3.0) * uStressLevel;
587
+ // Add some flickering/chaos to the hotspot
588
+ tidalHotspot *= 0.7 + 0.3 * noise3D(rotatedNormal * 8.0 + time * 2.0);
589
+
590
+ // === COMBINED INTENSITY ===
591
+ float totalIntensity = plasma * 0.35 + turbIntensity * 0.25 + gran * 0.2;
592
+ totalIntensity += bubbles * 0.4;
593
+ totalIntensity += fractures * 0.5; // Fractures glow hot
594
+ totalIntensity += tidalHotspot * 0.8; // Bright tidal hotspot
595
+ totalIntensity *= 1.0 + pulse;
596
+
597
+ // === 4-TIER COLOR SYSTEM ===
598
+ vec3 baseColor = uStarColor;
599
+ float maxComp = max(baseColor.r, max(baseColor.g, baseColor.b));
600
+ if (maxComp > 0.01) baseColor = baseColor / maxComp * 0.85;
601
+
602
+ // Temperature-based color blending
603
+ float tempBlend = smoothstep(5000.0, 7500.0, uTemperature);
604
+
605
+ vec3 hotColor = baseColor * vec3(1.6, 1.35, 1.2);
606
+ vec3 coolColor = mix(baseColor * vec3(0.5, 0.3, 0.2), baseColor * vec3(0.7, 0.8, 0.95), tempBlend);
607
+ vec3 warmColor = mix(baseColor * vec3(1.2, 1.0, 0.85), baseColor * vec3(1.0, 1.05, 1.2), tempBlend);
608
+ vec3 blazingColor = mix(baseColor * vec3(2.0, 1.6, 1.3), baseColor * vec3(1.4, 1.5, 1.8), tempBlend);
609
+
610
+ // Map intensity to color
611
+ vec3 surfaceColor;
612
+ if (totalIntensity < 0.35) {
613
+ surfaceColor = mix(coolColor, warmColor, totalIntensity / 0.35);
614
+ } else if (totalIntensity < 0.65) {
615
+ surfaceColor = mix(warmColor, hotColor, (totalIntensity - 0.35) / 0.3);
616
+ } else if (totalIntensity < 1.0) {
617
+ surfaceColor = mix(hotColor, blazingColor, (totalIntensity - 0.65) / 0.35);
618
+ } else {
619
+ surfaceColor = blazingColor * (1.0 + (totalIntensity - 1.0) * 0.8);
620
+ }
621
+
622
+ // Bubble highlights
623
+ float bubbleHighlight = pow(bubbles, 1.5) * turbIntensity;
624
+ surfaceColor += blazingColor * bubbleHighlight * 0.6;
625
+
626
+ // === LIMB DARKENING ===
627
+ surfaceColor *= 0.75 + limbDarkening * 0.25;
628
+
629
+ // === ORGANIC RIM GLOW ===
630
+ float rimAngle = atan(normal.y, normal.x) + selfRotation;
631
+ float rimNoise = noise3D(vec3(rimAngle * 3.0, edgeDist * 2.0, time * 0.2));
632
+ rimNoise = rimNoise * 0.5 + 0.5;
633
+
634
+ float rimIntensity = pow(edgeDist, 2.0) * (0.4 + rimNoise * 0.6);
635
+ vec3 rimColor = baseColor * vec3(1.3, 0.95, 0.6);
636
+ surfaceColor += rimColor * rimIntensity * 0.6 * uActivityLevel;
637
+
638
+ // === EDGE GLOW (corona bleeding into surface) ===
639
+ float edgeGlow = pow(edgeDist, 0.5) * 0.3 * uActivityLevel;
640
+ surfaceColor += warmColor * edgeGlow;
641
+
642
+ // === CENTER BOOST ===
643
+ float centerBoost = pow(viewAngle, 1.5) * 0.2;
644
+ surfaceColor += baseColor * centerBoost;
645
+
646
+ // === SHIMMER ===
647
+ float shimmer = sin(turbIntensity * 10.0 + time * 3.0) * 0.05 + 1.0;
648
+ surfaceColor *= shimmer;
649
+
650
+ // === TIDAL FLARE ===
651
+ // Sudden brightness burst when disruption begins
652
+ // Concentrated on the BH-facing side with violent flickering
653
+ if (uTidalFlare > 0.01) {
654
+ // Flare is brightest on the BH-facing side
655
+ float flareFace = 0.3 + tidalFace * 0.7;
656
+
657
+ // Violent flickering during the flare
658
+ float flareFlicker = 0.7 + 0.3 * noise3D(rotatedNormal * 10.0 + time * 8.0);
659
+
660
+ // White-hot flare color
661
+ vec3 flareColor = vec3(1.0, 0.95, 0.8);
662
+
663
+ // Additive flare - makes entire star brighter
664
+ float flareIntensity = uTidalFlare * flareFace * flareFlicker * 2.0;
665
+ surfaceColor += flareColor * flareIntensity;
666
+
667
+ // Extra bloom at the BH-facing tip
668
+ float tipFlare = pow(tidalFace, 4.0) * uTidalFlare * 1.5;
669
+ surfaceColor += vec3(1.0, 0.9, 0.7) * tipFlare;
670
+ }
671
+
672
+ surfaceColor = clamp(surfaceColor, 0.0, 3.5); // Allow brighter for flare
673
+
674
+ gl_FragColor = vec4(surfaceColor, 1.0);
675
+ }
676
+ `;
677
+
678
+ // =============================================================================
679
+ // BLACK HOLE SHADER
680
+ // =============================================================================
681
+
682
+ /**
683
+ * Black hole shader - subtle, physics-based with gravitational lensing
684
+ *
685
+ * Design philosophy:
686
+ * - Dormant: Nearly invisible, just a dark void with subtle edge
687
+ * - Main effect is gravitational lensing of background starfield
688
+ * - Only shows glow effects when awakened/feeding
689
+ * - The accretion disk handles most visual drama (separate component)
690
+ */
691
+ export const BLACK_HOLE_FRAGMENT = `
692
+ ${SPHERE_COMMON}
693
+
694
+ uniform float uAwakeningLevel; // 0 = dormant, 1 = fully active
695
+ uniform float uFeedingPulse; // Temporary glow from feeding
696
+ uniform float uRotation; // Black hole spin angle (Kerr rotation)
697
+
698
+ void main() {
699
+ vec2 uv = vUv;
700
+ vec2 center = uv - 0.5;
701
+ float dist = length(center) * 2.0; // 0 at center, 1 at edge
702
+ float angle = atan(center.y, center.x);
703
+
704
+ float time = uTime;
705
+ float awakeFactor = uAwakeningLevel;
706
+ float pulseFactor = uFeedingPulse;
707
+
708
+ // Spin angle for rotating effects (frame dragging)
709
+ // Using uTime since custom uniforms don't update properly
710
+ float spinAngle = angle + uTime * 4.0; // Faster spin
711
+
712
+ // === RADII (normalized to quad size) ===
713
+ float eventHorizon = 0.42; // Slightly larger core
714
+ float photonSphere = 0.54; // Tighten ring closer to horizon
715
+ float shadowEdge = 0.5; // Shadow boundary
716
+
717
+ // === CIRCULAR MASK ===
718
+ if (dist > 1.5) {
719
+ gl_FragColor = vec4(0.0);
720
+ return;
721
+ }
722
+
723
+ // === NO INTERNAL STARFIELD ===
724
+ // The real starfield is rendered separately in the scene
725
+ // This shader just renders the dark void + subtle edge effects
726
+ // True gravitational lensing would require render-to-texture of background
727
+
728
+ // === EVENT HORIZON - Gradient from pure black to very dark edge ===
729
+ // Edge color ~#110b06 = RGB(17,11,6) = vec3(0.067, 0.043, 0.024)
730
+ if (dist < shadowEdge) {
731
+ // Use shadowEdge (0.52) as outer boundary for smooth transition to ring
732
+ float edgeT = dist / shadowEdge; // 0 at center, 1 at shadow edge
733
+
734
+ // Very steep curve - stays pure black until very close to edge
735
+ float glowFactor = pow(edgeT, 8.0);
736
+
737
+ // Very dark brownish-black - NO yellow, matches #110b06
738
+ vec3 edgeColor = vec3(0.067, 0.043, 0.024) * glowFactor;
739
+
740
+ gl_FragColor = vec4(edgeColor, 1.0);
741
+ return;
742
+ }
743
+
744
+ // === PHOTON SPHERE - Subtle ring with gentler spin asymmetry ===
745
+ float photonRingWidth = 0.035;
746
+ float photonDist = abs(dist - photonSphere);
747
+ float photonRing = exp(-photonDist * photonDist / (photonRingWidth * photonRingWidth));
748
+
749
+ // Softer Doppler asymmetry to avoid pointy highlights
750
+ float doppler = 0.78 + 0.22 * cos(spinAngle); // narrower asymmetry
751
+ photonRing *= 0.18 + doppler * 0.38; // 18%..56% brightness
752
+
753
+ // Soft tip highlight to indicate spin without a spike
754
+ float tipAlign = max(0.0, cos(spinAngle));
755
+ float tipRadial = smoothstep(photonRingWidth * 1.2, 0.0, photonDist);
756
+ float hotSpotGlow = tipAlign * tipAlign * tipRadial * 0.25;
757
+
758
+ // Scale with awakening - more visible when feeding
759
+ photonRing *= 0.15 + awakeFactor * 0.35;
760
+
761
+ // === FEEDING PULSE - Subtle ripple when consuming ===
762
+ float pulseRipple = 0.0;
763
+ if (pulseFactor > 0.01) {
764
+ float ripplePhase = fract(time * 1.5) * 0.3;
765
+ float rippleRadius = shadowEdge + ripplePhase;
766
+ float ripple = exp(-pow(dist - rippleRadius, 2.0) * 80.0);
767
+ pulseRipple = ripple * pulseFactor * 0.15; // Subtle
768
+ }
769
+
770
+ // === EDGE GLOW - keep subtle; avoid wide smear
771
+ float edgeGlow = 0.0;
772
+ if (dist > shadowEdge && dist < photonSphere + 0.08) {
773
+ float edgeFactor = smoothstep(shadowEdge, photonSphere, dist);
774
+ edgeFactor *= smoothstep(photonSphere + 0.08, photonSphere, dist);
775
+ edgeGlow = edgeFactor * pulseFactor * 0.06;
776
+ }
777
+
778
+ // === COMBINE EFFECTS ===
779
+ vec3 color = vec3(0.0);
780
+
781
+ // Photon sphere ring (warm orange-yellow)
782
+ vec3 photonColor = vec3(1.0, 0.8, 0.45);
783
+ color += photonColor * photonRing;
784
+
785
+ // Soft tip highlight (spin indicator)
786
+ vec3 hotSpotColor = vec3(1.0, 0.9, 0.65);
787
+ color += hotSpotColor * hotSpotGlow;
788
+
789
+ // Edge glow when feeding
790
+ vec3 glowColor = vec3(1.0, 0.5, 0.2);
791
+ color += glowColor * edgeGlow;
792
+
793
+ // Feeding pulse ripple
794
+ vec3 pulseColor = vec3(1.0, 0.6, 0.3);
795
+ color += pulseColor * pulseRipple;
796
+
797
+ // === OUTER FADE ===
798
+ float outerFade = 1.0 - smoothstep(0.9, 1.25, dist);
799
+ color *= outerFade;
800
+
801
+ // === ALPHA ===
802
+ // Event horizon and photon sphere are fully opaque to occlude background
803
+ float alpha;
804
+ if (dist < photonSphere) {
805
+ alpha = 1.0;
806
+ color = (dist < shadowEdge) ? vec3(0.0) : color; // keep core solid black
807
+ } else {
808
+ // Alpha based on visible content
809
+ float contentBrightness = max(max(color.r, color.g), color.b);
810
+ alpha = smoothstep(0.01, 0.06, contentBrightness);
811
+ alpha = max(alpha, smoothstep(photonSphere + 0.12, shadowEdge, dist) * 0.25);
812
+ alpha *= outerFade;
813
+ }
814
+
815
+ gl_FragColor = vec4(color, alpha);
816
+ }
817
+ `;
818
+
819
+ // =============================================================================
820
+ // ROCKY PLANET SHADER
821
+ // =============================================================================
822
+
823
+ /**
824
+ * Rocky planet shader with terrain and atmosphere
825
+ */
826
+ export const ROCKY_PLANET_FRAGMENT = `
827
+ ${SPHERE_COMMON}
828
+
829
+ uniform vec3 uBaseColor;
830
+ uniform float uHasAtmosphere; // 0-1
831
+ uniform float uSeed;
832
+
833
+ void main() {
834
+ // Setup ray - camera looking at sphere from fixed position
835
+ vec3 rayOrigin = vec3(0.0, 0.0, -2.5);
836
+ vec3 rayDir = getRayDirection(vUv);
837
+
838
+ // Ray-sphere intersection (sphere at origin with radius 0.5)
839
+ float t = raySphereIntersect(rayOrigin, rayDir, vec3(0.0), 0.5);
840
+
841
+ if (t < 0.0) {
842
+ // Atmosphere halo
843
+ if (uHasAtmosphere > 0.0) {
844
+ vec2 center = vUv - 0.5;
845
+ float dist = length(center) * 2.0;
846
+ float atmo = smoothstep(0.6, 0.5, dist) * smoothstep(0.45, 0.52, dist);
847
+ atmo *= uHasAtmosphere * 0.4;
848
+ vec3 atmoColor = vec3(0.5, 0.7, 1.0) * atmo;
849
+ // Premultiplied alpha
850
+ gl_FragColor = vec4(atmoColor * atmo, atmo);
851
+ } else {
852
+ gl_FragColor = vec4(0.0);
853
+ }
854
+ return;
855
+ }
856
+
857
+ // Calculate hit point and normal
858
+ vec3 hitPoint = rayOrigin + rayDir * t;
859
+ vec3 normal = normalize(hitPoint);
860
+
861
+ // Apply inverse camera rotation for surface features
862
+ mat3 rotMat = rotationMatrix(-uCameraRotation);
863
+ vec3 rotatedNormal = rotMat * normal;
864
+
865
+ // Seeded noise for consistent terrain
866
+ vec3 noiseCoord = rotatedNormal * 4.0 + uSeed * 100.0;
867
+ float terrain = fbm(noiseCoord, 5);
868
+
869
+ // Height-based coloring
870
+ vec3 lowColor = uBaseColor * 0.6; // Valleys/lowlands
871
+ vec3 highColor = uBaseColor * 1.2; // Mountains/highlands
872
+ vec3 surfaceColor = mix(lowColor, highColor, terrain);
873
+
874
+ // Add some variation
875
+ float variation = noise3D(rotatedNormal * 10.0 + uSeed * 50.0);
876
+ surfaceColor *= 0.9 + variation * 0.2;
877
+
878
+ // Lighting
879
+ vec3 lightDir = normalize(vec3(1.0, 1.0, 0.5));
880
+ float light = lighting(normal, lightDir, 0.3);
881
+ surfaceColor *= light;
882
+
883
+ // Atmosphere scattering at edges
884
+ if (uHasAtmosphere > 0.0) {
885
+ float rim = fresnel(normal, -rayDir, 3.0);
886
+ vec3 atmoColor = vec3(0.5, 0.7, 1.0);
887
+ surfaceColor = mix(surfaceColor, atmoColor, rim * uHasAtmosphere * 0.4);
888
+ }
889
+
890
+ gl_FragColor = vec4(surfaceColor, 1.0);
891
+ }
892
+ `;
893
+
894
+ // =============================================================================
895
+ // GAS GIANT SHADER
896
+ // =============================================================================
897
+
898
+ /**
899
+ * Gas giant shader with banded atmosphere and storms
900
+ */
901
+ export const GAS_GIANT_FRAGMENT = `
902
+ ${SPHERE_COMMON}
903
+
904
+ uniform vec3 uBaseColor;
905
+ uniform float uSeed;
906
+ uniform float uStormIntensity; // 0-1
907
+
908
+ void main() {
909
+ // Setup ray - camera looking at sphere from fixed position
910
+ vec3 rayOrigin = vec3(0.0, 0.0, -2.5);
911
+ vec3 rayDir = getRayDirection(vUv);
912
+
913
+ // Ray-sphere intersection (sphere at origin with radius 0.5)
914
+ float t = raySphereIntersect(rayOrigin, rayDir, vec3(0.0), 0.5);
915
+
916
+ if (t < 0.0) {
917
+ gl_FragColor = vec4(0.0);
918
+ return;
919
+ }
920
+
921
+ // Calculate hit point and normal
922
+ vec3 hitPoint = rayOrigin + rayDir * t;
923
+ vec3 normal = normalize(hitPoint);
924
+
925
+ // Apply inverse camera rotation for surface features
926
+ mat3 rotMat = rotationMatrix(-uCameraRotation);
927
+ vec3 rotatedNormal = rotMat * normal;
928
+
929
+ // Convert to spherical coordinates for banding (use rotated normal)
930
+ float latitude = asin(rotatedNormal.y); // -PI/2 to PI/2
931
+ float longitude = atan(rotatedNormal.z, rotatedNormal.x); // -PI to PI
932
+
933
+ // Animated rotation
934
+ float time = uTime * 0.1;
935
+
936
+ // Create bands based on latitude
937
+ float bands = sin(latitude * 15.0 + time) * 0.5 + 0.5;
938
+ bands += sin(latitude * 25.0 - time * 0.5) * 0.25;
939
+ bands += sin(latitude * 40.0 + time * 0.3) * 0.125;
940
+
941
+ // Turbulent distortion of bands
942
+ vec3 noiseCoord = vec3(longitude + time * 0.2, latitude * 3.0, uSeed);
943
+ float turb = fbm(noiseCoord * 5.0, 4) * 0.3;
944
+ bands += turb;
945
+
946
+ // Color variation based on bands
947
+ vec3 lightBand = uBaseColor * 1.3;
948
+ vec3 darkBand = uBaseColor * 0.7;
949
+ vec3 surfaceColor = mix(darkBand, lightBand, bands);
950
+
951
+ // Add storm features
952
+ if (uStormIntensity > 0.0) {
953
+ // Great red spot style storm
954
+ float stormLat = 0.3; // Storm latitude
955
+ float stormLon = time * 0.5; // Storm drifts
956
+ vec2 stormCenter = vec2(stormLon, stormLat);
957
+ vec2 pos = vec2(longitude, latitude);
958
+ float stormDist = length(pos - stormCenter);
959
+ float storm = smoothstep(0.5, 0.2, stormDist);
960
+ storm *= uStormIntensity;
961
+
962
+ // Storm color and swirl
963
+ vec3 stormColor = vec3(0.8, 0.3, 0.2);
964
+ float swirl = sin(stormDist * 20.0 - time * 3.0) * 0.5 + 0.5;
965
+ surfaceColor = mix(surfaceColor, stormColor * swirl, storm);
966
+ }
967
+
968
+ // Lighting with some subsurface scattering effect
969
+ vec3 lightDir = normalize(vec3(1.0, 0.5, 0.3));
970
+ float light = lighting(normal, lightDir, 0.4);
971
+ surfaceColor *= light;
972
+
973
+ // Limb darkening
974
+ float viewAngle = dot(normal, -rayDir);
975
+ surfaceColor *= 0.7 + max(0.0, viewAngle) * 0.3;
976
+
977
+ gl_FragColor = vec4(surfaceColor, 1.0);
978
+ }
979
+ `;
980
+
981
+ // =============================================================================
982
+ // SHADER LIBRARY EXPORT
983
+ // =============================================================================
984
+
985
+ export const SPHERE_SHADERS = {
986
+ vertex: SPHERE_VERTEX,
987
+ common: SPHERE_COMMON,
988
+ star: STAR_FRAGMENT,
989
+ blackHole: BLACK_HOLE_FRAGMENT,
990
+ rockyPlanet: ROCKY_PLANET_FRAGMENT,
991
+ gasGiant: GAS_GIANT_FRAGMENT,
992
+ };
993
+
994
+ export default SPHERE_SHADERS;