Accretion Disk - OldMoses - 03-12-2026
My goal with this one was to simulate a black hole's accretion disk using my standard vector library tools. It will eventually settle down to the entropic disk that I was after, but I wasn't expecting the initial splash patterns. I like how the particles will arrange themselves into discrete orbit shells. That said, this is NOT a gravity simulation, as attraction does not vary with distance.
Use the space bar to display a green "event horizon", which shows how close the orbits skim it.
It's an amusement to change some of the parameters and see how it affects the pattern and/or number of survivors.
The number in the upper right corner is the number of surviving particles, the rest are lost, forever lost! 
If you stare at it long enough the world will start spinning too. Maybe you might also begin to feel sleepy and have an irresistible urge to send me a sizeable check.
Code: (Select All) OPTION _EXPLICIT
TYPE V2 ' R2 vector format: used with R2_ subs & functions
x AS SINGLE
y AS SINGLE
END TYPE
TYPE particle
ex AS INTEGER ' particle exists -1=yes 0=no
p AS V2 ' position
d AS V2 ' displacement aka movement
END TYPE
DIM c&, bs&, rs&
DIM vl%, vecs%, attrad%, wd%, ht%, eh%
DIM AS V2 tmp, op, cn
DIM speed!, attstr!
wd% = _DESKTOPWIDTH
ht% = _DESKTOPHEIGHT
SCREEN _NEWIMAGE(wd%, ht%, 32)
DO UNTIL _SCREENEXISTS: LOOP
_SCREENMOVE 0, 0
_TITLE "Accretion disk {esc to quit}"
vecs% = 10000 ' total starting particles
attrad% = 50 ' radius of the attractor
attstr! = .1 ' pull of the attractor
speed! = 1 ' particle orbit velocity
bs& = &HFFAFAFFF ' particle color blue shift
rs& = &HFFFFAFAF ' particle color red shift
cn.x = _SHR(wd%, 1): cn.y = _SHR(ht%, 1) ' screen center
'set initial particle data
RANDOMIZE TIMER
DIM SHARED V(vecs%) AS particle
FOR vl% = 0 TO UBOUND(V)
V(vl%).ex = -1 ' particle exists at start
DO
V(vl%).p.x = INT(RND * wd%) ' random position start
V(vl%).p.y = INT(RND * wd%) - _SHR(wd% - ht%, 1)
R2_P2P tmp, V(vl%).p, cn ' get particle position vector <tmp> relative to screen center
LOOP UNTIL R2_Mag(tmp) < _SHR(wd%, 1) ' redo until particle within a circular area
R2_Orth tmp, tmp, -1 ' convert position vector to an orthogonal rotation vector - CCW + CW
R2_Norm V(vl%).d, tmp, speed! ' set magnitude of rotation vector to speed! and assign it to particle displacement
NEXT vl%
'Main program loop
DO
IF _KEYDOWN(32) THEN eh% = NOT eh%
CLS
_PRINTSTRING (0, 0), STR$(vecs%) ' number of surviving particles
IF eh% THEN CIRCLE (cn.x, cn.y), attrad%, &HFF00FF00 ' space bar to toggle the boundary of the attractor in green if desired
FOR vl% = 0 TO UBOUND(V)
IF NOT V(vl%).ex THEN _CONTINUE ' skip particles that have been swallowed by attractor
op = V(vl%).p
R2_P2P tmp, op, cn ' obtain the inward pointing vector between particle and attractor
c& = _IIF(R2_Dot(tmp, V(vl%).d) > 0, rs&, bs&) ' blue shift = approaching apogee, red shift = approaching perigee
IF R2_Mag(tmp) < attrad% THEN ' if particle is within "event horizon"
V(vl%).ex = 0: vecs% = vecs% - 1 ' particle has been absorbed by attractor
ELSE
R2_Norm tmp, tmp, attstr! ' attractor force vector
R2_Add V(vl%).d, tmp, 1 ' add that to V(vl%).d
R2_Add V(vl%).p, V(vl%).d, 1 ' add displacement to position aka move it
LINE (op.x, op.y)-(V(vl%).p.x, V(vl%).p.y), c& ' show the vector
END IF
NEXT vl%
_LIMIT 30
_DISPLAY
LOOP UNTIL _KEYDOWN(27)
END
'------------------------------------------------------------------------------
'SUBROUTINES
'------------------------------------------------------------------------------
SUB R2_P2P (re AS V2, st AS V2, nd AS V2)
'ÞßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßÝ
'Þ Return a 2D position vector <re> from point (st) to point (nd) Ý
'ÞÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÝ
re.x = nd.x - st.x
re.y = nd.y - st.y
END SUB 'R2_P2P
'------------------------------------------------------------------------------
SUB R2_Add (re AS V2, se AS V2, m AS SINGLE)
'ÞßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßÝ
'Þ Adds (or subtracts) a scalar multiple (m) of vector <se> to vector <re> Ý
'Þ if <re> is to be preserved in its prior state then a copy should be Ý
'Þ made for the subroutine call. Send a negative scalar (m) to subtract Ý
'Þ Returns: vector <re> Ý
'ÞÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÝ
re.x = re.x + se.x * m
re.y = re.y + se.y * m
END SUB 'R2_Add
'------------------------------------------------------------------------------
FUNCTION R2_Dot (v AS V2, v2 AS V2)
'ÞßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßÝ
'Þ Return scalar dot product between two V2 vectors <v> & <v2> Ý
'ÞÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÝ
R2_Dot = v.x * v2.x + v.y * v2.y
END FUNCTION 'R3_Dot
'------------------------------------------------------------------------------
FUNCTION R2_Mag! (v AS V2)
'ÞßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßÝ
'Þ Return the scalar magnitude of 2D vector (v) Ý
'ÞÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÝ
R2_Mag! = _HYPOT(v.x, v.y)
END FUNCTION 'R2_Mag!
'------------------------------------------------------------------------------
SUB R2_Orth (re AS V2, v AS V2, d AS INTEGER)
'ÞßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßÝ
'Þ Returns vector <re> as a 90ø orthogonal of vector <v>. Sending a Ý
'Þ positive value for d rotates clockwise, while a negative value for Ý
'Þ d rotates counterclockwise. <re> & <v> can be the same. Ý
'ÞÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÝ
DIM x!, y!
x! = v.x: y! = v.y
re.x = y! * SGN(d)
re.y = -x! * SGN(d)
END SUB 'R2_Orth
'------------------------------------------------------------------------------
SUB R2_Norm (re AS V2, v AS V2, scalar AS SINGLE)
'ÞßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßÝ
'Þ Grow/Shrink V2 vector <v> to (scalar) length. <re> & <v> can be the Ý
'Þ same vectors, overwriting the original. Vector direction is preserved. Ý
'Þ Returns: vector <re> as new length of <v> Ý
'ÞÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÝ
DIM m!
DIM t AS V2 ' temporary vector
t = v ' V vector to temporary container
m! = R2_Mag!(t) ' magnitude of temporary
IF m! = 0 THEN
re.x = 0: re.y = 0 ' zero vector- no direction information available
ELSE
re.x = (t.x / m!) * scalar ' reduce to unit and expand to scalar
re.y = (t.y / m!) * scalar ' and assign to return vector
END IF
END SUB 'R2_Norm
RE: Accretion Disk - Dimster - 03-12-2026
If its not gravity then the attraction would be positive/negative charge? Or perhaps that force carrier stuff of exchanging particles neutrons/protons which I'm still working on understanding.
Reminds me of the Big Bang but they say that is gravity at work.
After watching this for a few hours I did have my wallet out but the moth that few out broke the trance.
RE: Accretion Disk - bplus - 03-12-2026
+1 very cool!
_ScreenMove 0, 0 ' <<< oddly the screen corner is not going to 0 for x and code does delay for the screen ??
RE: Accretion Disk - OldMoses - 03-12-2026
(Yesterday, 12:28 PM)Dimster Wrote: If its not gravity then the attraction would be positive/negative charge? Or perhaps that force carrier stuff of exchanging particles neutrons/protons which I'm still working on understanding.
Reminds me of the Big Bang but they say that is gravity at work.
After watching this for a few hours I did have my wallet out but the moth that few out broke the trance. The attstr! variable just adds the same amount (.1 as posted) to the particle's displacement vector, in the direction of the attractor, with each iteration of the DO LOOP. I considered doing a gravity calculation, but was unsure how to set the mass part of the calculation.
BTW, it's not the first time moths have cost me money.
RE: Accretion Disk - NakedApe - 03-12-2026
+1 beautiful and mesmerizing, @OldMoses. And very nice code - though over my head.
RE: Accretion Disk - OldMoses - 03-12-2026
(Yesterday, 05:12 PM)NakedApe Wrote: +1 beautiful and mesmerizing, @OldMoses. And very nice code - though over my head.
Thanks, and based on the things I've seen you post, I doubt that any of it is truly "over" you. It's just my shorthand way of dealing with X/Y pairs that might appear a bit cryptic. I like having a library of SUBs and FUNCTIONs that I can send those pairs to as a TYPE without constantly typing .x and .y at the end of everything. They're the same core routines I used on my billiards program. I keep the math as dead simple as possible so that the processor can concentrate on iterating ten thousand things quickly.
RE: Accretion Disk - Dav - 03-12-2026
Super cool effect, @OldMoses!
+1 from me too.
- Dav
RE: Accretion Disk - a740g - 03-12-2026
Beautiful. +1
RE: Accretion Disk - Unseen Machine - 03-13-2026
Nice, just NICE!!!! This is my cuppa tea!
Here's my (AI assisted version) I went through the Kerr, Schwarzschild and Einstein variations but as Pset is slow have moved on to Titan and using shaders...though its a headache and will be a while if it ever comes to fruition!
Code: (Select All)
' QB64 - RELATIVISTIC SINGULARITY (FINAL CINEMATIC VERSION)
' ---------------------------------------------------------------------------
wd = 800: ht = 600
SCREEN _NEWIMAGE(wd, ht, 32)
vecs = 18000 ' Max density for beauty
DIM Px(vecs), Py(vecs), Vx(vecs), Vy(vecs)
G = 2.5: M = 950.0: RS = 24.0
FOR i = 0 TO vecs
a = RND * 6.283: d = RS + 40 + RND * 380
Px(i) = COS(a) * d: Py(i) = SIN(a) * d
v_orb = SQR((G * M) / d)
Vx(i) = -SIN(a) * v_orb * 0.99: Vy(i) = COS(a) * v_orb * 0.99
NEXT i
DO
CLS
' --- THE EVENT HORIZON ---
FOR r_loop = RS TO 0 STEP -1: CIRCLE (wd / 2, ht / 2), r_loop, _RGB32(0, 0, 0): NEXT r_loop
FOR i = 0 TO vecs
r = _HYPOT(Px(i), Py(i))
' --- CONSUMPTION & RESET ---
resetted = 0
IF r < RS OR r > 580 THEN
d = 400 + RND * 50: a = RND * 6.283
Px(i) = COS(a) * d: Py(i) = SIN(a) * d
v = SQR((G * M) / d): Vx(i) = -SIN(a) * v: Vy(i) = COS(a) * v
resetted = 1
END IF
' --- PHYSICS ---
mag = (G * M) / (r * r)
Vx(i) = Vx(i) - (Px(i) / r) * mag: Vy(i) = Vy(i) - (Py(i) / r) * mag
Px(i) = Px(i) + Vx(i): Py(i) = Py(i) + Vy(i)
IF resetted = 0 THEN
' --- DOPPLER / RELATIVISTIC BEAMING ---
' Particles moving LEFT (Vx < 0) are moving TOWARD the observer.
' We boost their brightness and add a "Blue" tint.
doppler = 1.0 - (Vx(i) / 15.0) ' Boost if Vx is negative (moving left)
' --- HEAT GRADIENT ---
heat_val = (RS * 18) / (r - RS + 5) * 15 * doppler
IF heat_val > 255 THEN heat_val = 255
r_c = heat_val * 1.6: IF r_c > 255 THEN r_c = 255
g_c = heat_val * 0.9: IF g_c > 255 THEN g_c = 255
b_c = heat_val * (0.3 + (doppler * 0.2)): IF b_c > 255 THEN b_c = 255
clr = _RGB32(r_c, g_c, b_c)
' --- PROJECTIONS ---
screenX = (wd / 2) + Px(i)
screenY = (ht / 2) + (Py(i) * 0.22)
' Main Disk Render
IF screenX > 0 AND screenX < wd AND screenY > 0 AND screenY < ht THEN
PSET (screenX, screenY), clr
END IF
' --- THE EINSTEIN RING (Lensed Light) ---
' Masked to prevent the "horizontal lines" glitch
IF Py(i) < -2 AND ABS(Px(i)) < RS * 3.8 THEN
distort = (RS * 30) / (ABS(Px(i)) + 20)
warpY1 = (ht / 2) - (RS + distort)
warpY2 = (ht / 2) + (RS + distort)
alpha = 1.0 - (ABS(Px(i)) / (RS * 3.8))
IF alpha > 0 THEN
l_clr = _RGB32(r_c * alpha, g_c * alpha, b_c * alpha)
IF screenX > 0 AND screenX < wd THEN
IF warpY1 > 0 AND warpY1 < ht THEN PSET (screenX, warpY1), l_clr
IF warpY2 > 0 AND warpY2 < ht THEN PSET (screenX, warpY2), l_clr
END IF
END IF
END IF
END IF
NEXT i
CIRCLE (wd / 2, ht / 2), RS, _RGB32(255, 140, 50)
_DISPLAY: _LIMIT 60
LOOP UNTIL _KEYDOWN(27)
I love physics nearly as much as i love coding so +1 from me too!
John
RE: Accretion Disk - hsiangch_ong - 03-13-2026
(Yesterday, 12:08 PM)OldMoses Wrote: If you stare at it long enough the world will start spinning too. Maybe you might also begin to feel sleepy ...
squeeze it, explode it! very nice!
pset might be slow. but this program is quite good enough! i have a 15-year-old laptop which originally came with windows8. running this under linux/debian.
you might want to stay away. from the "cp437" ascii lines in basic code. for posting on this forum. because they are coming out as unicode slop. which confuses some people. who like to look at neat code.
|