Very complete reverse-engineering of Georg Nees' SCHOTTER

Schotter (tr.: gravel) is an early piece of generative art, and recreating an approximation of it is almost the “Hello, World!” for new generative art people.

Zellyn Hunter goes way beyond with Schotter - Georg Nees - Part 1 by finding the original ALGOL source and reverse-engineering the pRNG used by Nees to end up with exactly the same plot

4 Likes

I don’t think that listed source file is the actual original source, though - it has errors. Probably re-keyed from a paper listing.

Posting runnable code, especially of the Algol kind, doesn’t seem to be the article’s goal. Schotter - Georg Nees - Part 2 has a frankly heroic tracking down the exact pRNG parameters that Nees used to produce the best-known version of the graphic

Note that the article doesn’t contain the complete code to generate the graphic in Python, but there’s enough there that one can piece together something that generates the figure exactly

2 Likes

I really enjoy mathematically generated graphics for old systems. I’ve been porting them to the Atari 8-bit in Turbo Basic.

I did something screwy with placing the coordinates on the screen, because I realized why the Python code reverses X and Y when drawing the “quads”. Otherwise, the graphics show up “sideways” on the screen, “tumbling” from left to right, rather than from the top down.

Here’s the listing:

``````10 R=5*1.4142:PIHALB=3.14159*0.5:PI4T=PIHALB*0.5:I=0
20 GRAPHICS 8+16:COLOR 1
30 QUER=10:HOCH=10:XMAL=22:YMAL=12:EXEC SERIE
40 GOTO 40
60   JE1=5*I/264:JA1=-JE1:JE2=PI4T*(1+I/264):JA2=PI4T*(1-I/264)
70   EXEC J1RND:EXEC J2RND
80   P1=P+5+J1:Q1=Q+5+J1:PSI=J2
90   X=P1+R*COS(PSI):Y=Q1+R*SIN(PSI):EXEC LEER
100   FOR S=1 TO 4
110     PSI=PSI+PIHALB
120     X=P1+R*COS(PSI):Y=Q1+R*SIN(PSI):EXEC LINE
130   NEXT S:I=I+1
140 ENDPROC
150 PROC LEER
180 ENDPROC
190 PROC SERIE
200   P=-QUER*XMAL*0.5:Q=-HOCH*YMAL*0.5:YANF=Q
210   FOR COUNTX=1 TO XMAL
220     Q=YANF
230     FOR COUNTY=1 TO YMAL
250     NEXT COUNTY:P=P+QUER
260   NEXT COUNTX
270 ENDPROC
280 PROC J1RND
290   RANGE=ABS(JA1-JE1):J1=RND(0)*RANGE+JE1
300 ENDPROC
310 PROC J2RND
320   RANGE=ABS(JA2-JE2):J2=RND(0)*RANGE+JE2
330 ENDPROC
340 PROC LINE
370 ENDPROC
``````

I got a lot of it from looking at the original code, and used the Python code as a sanity check on my interpretation.

2 Likes

I haven’t tried drawing it yet, not currently having a suitable library installed, but this might work… I fixed up the Algol from the article, then hand translated from Algol to Imp followed by a machine translation from Imp to C…

``````// cc -o schotter schotter.c -lm

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

int main(int argc, char **argv) {
double X, Y;

void Open(double X, double Y) {}
void Close(void) {}
void Leer(double X, double Y) { printf("\nMoveto(%d, %d)\n", (int)round(X), (int)round(Y)); }
void Line(double X, double Y) { printf("Lineto(%d, %d)\n", (int)round(X), (int)round(Y)); }
void Sonk(int X) {}

double J(double Ja, double Je) {  //  ZUFALLS-GENERATOR J
static double Ji = 1306859721;
Ji = 5 * Ji;
if (Ji >= 8589934592) Ji -= 8589934592;
if (Ji >= 4294967296) Ji -= 4294967296;
if (Ji >= 2147483648) Ji -= 2147483648;
return ((Ji / 2147483648) * (Je - Ja) + Ja);
}

//  SCHOTTER
double R, Pihalb, Pi4t, P, Q, P1, Q1, Xm, Ym, Hor, Ver, Jli, Jre, Jun, Job;
int I, M, T;

void Serie(double Quer, double Hoch, int Xmal, int Ymal, void Figur(void)) {
double Yanf;
int Countx, County;

P = -Quer * Xmal * .5;
Yanf = -Hoch * Ymal * .5;
Q = Yanf;
for (Countx = 1; Countx <= Xmal; Countx++) {
Q = Yanf;
for (County = 1; County <= Ymal; County++) {
Figur();
Q += Hoch;
}
P += Quer;
}
Leer(-148.0, -105.0);
Close();
Sonk(11);
Open(X, Y);
}

double P1, Q1, Psi, Ja1, Je1, Ja2, Je2;
int S;

Je1 = 5 * 1 / 264;
Ja1 = -Je1;
Je2 = Pi4t * (1 + (I / 264));
Ja2 = Pi4t * (1 - (I / 264));
P1 = P + 5 + J(Ja1, Je1);
Q1 = Q + 5 + J(Ja1, Je1);
Psi = J(Ja2, Je2);
Leer(P1 + R * cos(Psi), Q1 + R * sin(Psi));
for (S = 1; S <= 4; S++) {
Psi += Pihalb;
Line(P1 + R * cos(Psi), Q1 + R * sin(Psi));
}
I++;
}

X = Y = 0;
Open(X, Y);
R = 5 * 1.4142;
Pihalb = 3.14159 * .5;
Pi4t = Pihalb * .5;
I = 0;
Serie(10.0, 10.0, 22, 12, &Quad);
Close();

exit(0);
return (1);
}
``````

If you’re interested, here’s the Algol… in Unicode encoding, close to publication syntax.

``````b̲e̲g̲i̲n̲

r̲e̲a̲l̲ X, Y;
p̲r̲o̲c̲e̲d̲u̲r̲e̲ OPEN(X, Y);
v̲a̲l̲u̲e̲ X, Y;
r̲e̲a̲l̲ X, Y;
b̲e̲g̲i̲n̲
e̲n̲d̲ OPEN;

p̲r̲o̲c̲e̲d̲u̲r̲e̲ CLOSE;
b̲e̲g̲i̲n̲
e̲n̲d̲ CLOSE;

p̲r̲o̲c̲e̲d̲u̲r̲e̲ LEER(X, Y);
v̲a̲l̲u̲e̲ X, Y;
r̲e̲a̲l̲ X, Y;
b̲e̲g̲i̲n̲
c̲o̲m̲m̲e̲n̲t̲ MOVETO;
e̲n̲d̲ LEER;

p̲r̲o̲c̲e̲d̲u̲r̲e̲ LINE(X, Y);
v̲a̲l̲u̲e̲ X, Y;
r̲e̲a̲l̲ X, Y;
b̲e̲g̲i̲n̲
e̲n̲d̲ LINE;

p̲r̲o̲c̲e̲d̲u̲r̲e̲ SONK(X);
v̲a̲l̲u̲e̲ X;
i̲n̲t̲e̲g̲e̲r̲ X;
b̲e̲g̲i̲n̲
e̲n̲d̲ SONK;

r̲e̲a̲l̲ p̲r̲o̲c̲e̲d̲u̲r̲e̲ COS(A);
v̲a̲l̲u̲e̲ A;
r̲e̲a̲l̲ A;
b̲e̲g̲i̲n̲
COS := 0·0;
e̲n̲d̲ COS;

r̲e̲a̲l̲ p̲r̲o̲c̲e̲d̲u̲r̲e̲ SIN(A);
v̲a̲l̲u̲e̲ A;
r̲e̲a̲l̲ A;
b̲e̲g̲i̲n̲
SIN := 0·0;
e̲n̲d̲ SIN;

i̲n̲t̲e̲g̲e̲r̲ JI;
r̲e̲a̲l̲ p̲r̲o̲c̲e̲d̲u̲r̲e̲ J(JA, JE);
v̲a̲l̲u̲e̲ JA, JE;
r̲e̲a̲l̲ JA, JE;
b̲e̲g̲i̲n̲
c̲o̲m̲m̲e̲n̲t̲ ZUFALLS-GENERATOR J;
JI := 5×JI;
i̲f̲ JI ≥ 8589934592 t̲h̲e̲n̲ JI := JI - 8589934592;
i̲f̲ JI ≥ 4294967296 t̲h̲e̲n̲ JI := JI - 4294967296;
i̲f̲ JI ≥ 2147483648 t̲h̲e̲n̲ JI := JI - 2147483648;
J := JI/2147483648×(JE-JA) + JA
e̲n̲d̲ ZUFALLS GENERATOR J;

c̲o̲m̲m̲e̲n̲t̲ SCHOTTER;

r̲e̲a̲l̲    R, PIHALB, PI4T;
r̲e̲a̲l̲    P, Q, P1, Q1, XM, YM, HOR, VER, JLI, JRE, JUN, JOB;
i̲n̲t̲e̲g̲e̲r̲ I, M, T;

p̲r̲o̲c̲e̲d̲u̲r̲e̲ SERIE(QUER, HOCH, XMAL, YMAL, FIGUR);
v̲a̲l̲u̲e̲   QUER, HOCH, XMAL, YMAL;
r̲e̲a̲l̲    QUER, HOCH;
i̲n̲t̲e̲g̲e̲r̲ XMAL, YMAL;
p̲r̲o̲c̲e̲d̲u̲r̲e̲ FIGUR;
b̲e̲g̲i̲n̲
r̲e̲a̲l̲    YANF;
i̲n̲t̲e̲g̲e̲r̲ COUNTX, COUNTY;

P := -QUER×XMAL×·5;
Q := YANF := -HOCH×YMAL×·5;
f̲o̲r̲ COUNTX := 1 s̲t̲e̲p̲ 1 u̲n̲t̲i̲l̲ XMAL d̲o̲
b̲e̲g̲i̲n̲
Q := YANF;
f̲o̲r̲ COUNTY := 1 s̲t̲e̲p̲ 1 u̲n̲t̲i̲l̲ YMAL d̲o̲
b̲e̲g̲i̲n̲
FIGUR;
Q := Q+HOCH
e̲n̲d̲;
P := P+QUER
e̲n̲d̲;

LEER(-148·0, -105·0);
CLOSE;
SONK(11);
OPEN(X, Y);
e̲n̲d̲ SERIE;

b̲e̲g̲i̲n̲
r̲e̲a̲l̲ P1, Q1, PSI, JA1, JE1, JA2, JE2;
i̲n̲t̲e̲g̲e̲r̲ S;

JE1 := 5×1/264;
JA1 := -JE1;
JE2 := PI4T×(1+I/264);
JA2 := PI4T×(1-I/264);
P1  := P+5+J(JA1, JE1);
Q1  := Q+5+J(JA1, JE1);
PSI := J(JA2, JE2);
LEER(P1+R×COS(PSI), Q1+R×SIN(PSI));
f̲o̲r̲ S := 1 s̲t̲e̲p̲ 1 u̲n̲t̲i̲l̲ 4 d̲o̲
b̲e̲g̲i̲n̲
PSI := PSI+PIHALB;
LINE(P1+R×COS(PSI), Q1+R×SIN(PSI));
e̲n̲d̲;
I   := I+1

X := Y := 0;
OPEN(X, Y);

JI     := 1306859721;
R      := 5×1·4142;
PIHALB := 3·14159×·5;
PI4T   := PIHALB×·5;
I      := 0;
SERIE(10·0, 10·0, 22, 12, QUAD)

CLOSE;

e̲n̲d̲ SCHOTTER;
``````

And just for grins, here’s the Imp version…

``````%begin

%external %long %real %fn %spec cos %alias "cos" (%long %real angle) ;! C's "double cos(double x)"
%external %long %real %fn %spec sin %alias "sin" (%long %real angle)

%long %real X, Y
%routine OPEN(%long %real X, Y)
%end;  !  OPEN

%routine CLOSE
%end;  !  CLOSE

%routine LEER(%long %real X, Y);  ! MOVETO
newline; print string("Moveto("); print(X,6);  print string(", ");  print(Y,6); print string(")"); newline
%end;  !  LEER

%routine LINE(%long %real X, Y)
print string("Lineto("); print(X,6);  print string(", ");  print(Y,6); print string(")"); newline
%end;  !  LINE

%routine SONK(%integer X)
%end;  !  SONK

%long %real JI
%long %real %function J(%long %real JA, JE)
%comment ZUFALLS-GENERATOR J
JI = 5 * JI
%if JI >= 8589934592 %then JI = JI - 8589934592
%if JI >= 4294967296 %then JI = JI - 4294967296
%if JI >= 2147483648 %then JI = JI - 2147483648
%result = JI/2147483648 * (JE-JA) + JA
%end;  !  ZUFALLS GENERATOR J

%comment SCHOTTER

%long %real    R, PIHALB, PI4T
%long %real    P, Q, P1, Q1, XM, YM, HOR, VER, JLI, JRE, JUN, JOB
%integer I, M, T

%routine SERIE(%long %real QUER, HOCH, %integer XMAL, YMAL, %routine FIGUR)
%long %real    YANF
%integer COUNTX, COUNTY

P = -QUER * XMAL * .5
YANF = -HOCH * YMAL * .5; Q = YANF

%for COUNTX = 1, 1, XMAL %cycle
Q = YANF
%for COUNTY = 1, 1, YMAL %cycle
FIGUR
Q = Q+HOCH
%repeat
P = P+QUER
%repeat

LEER(-148.0, -105.0)
CLOSE
SONK(11)
OPEN(X, Y)
%end;  !  SERIE

%long %real P1, Q1, PSI, JA1, JE1, JA2, JE2
%integer S

JE1 = 5 * 1/264
JA1 = -JE1
JE2 = PI4T * (1+I/264)
JA2 = PI4T * (1-I/264)
P1  = P+5+J(JA1, JE1)
Q1  = Q+5+J(JA1, JE1)
PSI = J(JA2, JE2)
LEER(P1+R * cos(PSI), Q1+R * sin(PSI))
%for S = 1, 1, 4 %cycle
PSI = PSI+PIHALB
LINE(P1+R * cos(PSI), Q1+R * sin(PSI))
%repeat
I   = I+1

Y = 0; X = Y
OPEN(X, Y)

JI     = 1306859721
R      = 5 * 1.4142
PIHALB = 3.14159 * .5
PI4T   = PIHALB * .5
I      = 0
SERIE(10.0, 10.0, 22, 12, QUAD)

CLOSE

%end %of %program;  !  SCHOTTER
``````

Just to note, if you write the word

text

after the triple backticks (on the same line) you can turn off the highlighting.

Thanks, I’d forgotten about that tweak again. It did the trick.

1 Like

The article author still hasn’t posted working source code, so here’s what I patched together from the various fragments:

``````#!/usr/bin/env python3
# recreates Georg Nees' Schotter perfectly
# for details, see https://zellyn.com/2024/06/schotter-1/
# -*- coding: utf-8 -*-

import math
import drawsvg as draw  # https://pypi.org/project/drawsvg/

class Random:
def __init__(self, seed):
self.JI = seed

def next(self, JA, JE):
self.JI = (self.JI * 5) % 2147483648
return self.JI / 2147483648 * (JE - JA) + JA

def draw_square(g, x, y, i, r1, r2):
r = 5 * math.sqrt(2)
pi = math.pi
move_limit = 5 * i / 264
twist_limit = pi / 4 * i / 264

y_center = y + 5 + r1.next(-move_limit, move_limit)
x_center = x + 5 + r1.next(-move_limit, move_limit)
angle = r2.next(pi / 4 - twist_limit, pi / 4 + twist_limit)

p = draw.Path()
p.M(x_center + r * math.sin(angle), y_center + r * math.cos(angle))
for step in range(4):
angle += pi / 2
p.L(
x_center + r * math.sin(angle),
y_center + r * math.cos(angle),
)
g.append(p)

def draw_plot(x_size, y_size, x_count, y_count, s1, s2):
r1 = Random(s1)
r2 = Random(s2)
d = draw.Drawing(
180, 280, origin="center", style="background-color:#eae6e2"
)
g = draw.Group(
stroke="#41403a",
stroke_width="0.4",
fill="none",
stroke_linecap="round",
stroke_linejoin="round",
)

y = -y_size * y_count * 0.5
x0 = -x_size * x_count * 0.5
i = 0

for _ in range(y_count):
x = x0
for _ in range(x_count):
draw_square(g, x, y, i, r1, r2)
x += x_size
i += 1
y += y_size
d.append(g)
return d

d = draw_plot(10.0, 10.0, 12, 22, 1922110153, 1769133315)
d.set_render_size(w=1000)
d.save_svg("schotter.svg")
``````

btw Google Translate says Falter is a butterfly but a butterfly in German is a Schmetterling and I’m fairly sure that a Falter is actually a moth. Although I’d look for a second opinion from a native speaker to be 100% sure.

Not a native speaker, but Falter is definitely another word for butterfly or moth. Looking at the Collins German-English website, I recognize the lead examples for the word in a corpus I loaded onto the system when I worked there. Die Tageszeitung were very generous with their archives, and thus our corpus skewed fairly heavily their way.

I was slightly surprised that my code came out with no highlighting, despite my posting it with the right formatting code.

Possibly, but as both an amateur entomologist and an amateur etymologist one thing I’m very sure of is that butterflies are not moths! English has no word in common use that I know of which encompasses both (I’m excluding Lepidoptera as not being at the same level of informality as Falter) - maybe German has, but if they do, they shouldn’t! (And on a related note, it’s taken me 20 years of marriage to convince my Texan wife not to call caterpillars “worms” :-/ )
Which reminds me… (getting back to retro subjects) Edinburgh’s AI2LOGO interpreter saved its workspace in a memory-mapped file named “LOGOFILE”. I remember our lecturer saying “You’ll know why we called it that if you are one.”