Design Tips and Demoscene effects with pseudo code
Contents
[hide]- 1 Introduction
- 2 General Tips
- 3 Effects and Techniques
- 4 Common Demoscene Effects
- 4.1 Size / CPU-Load / Effect table
- 4.2 Oscillators
- 4.3 2D Starfields
- 4.4 Pixel-summing / Automatons
- 4.5 3D Starfield
- 4.6 Dot Tunnels
- 4.7 Fractals
- 4.8 Attractors
- 4.9 Mathematical Equations
- 4.10 Plasma
- 4.11 Sine Plotters
- 4.12 Twisterbar
- 4.13 Image/UV Distorters
- 4.14 Raycasting
- 4.15 Raymarching
- 4.16 Raytracing
- 5 Design Tips
Introduction
On this page we will provide a bunch of general design tips as well as examples for commonly used effects. Its always good to know in a more broader sense how certain effects are approached, before thinking about how to do them in a small size or how to best implement them for a specific platform.
General Tips
Before we start diving into the effects themselves, here are some general tips if you are new to demoscene coding:
- Make prototypes in another language you're familiar with
- Make sure to write comments, your future self will thank you.
- Learn from the source code of others (a lot of productions include the sourcecode)
- Collect your own bag of tricks and document them for easy access
- If your idea is complex: approach your problems one at a time in seperate files and merge them together at a later stage
- Write down a simplified list how much bytes each of the commonly used instruction costs
- Don't be afraid to itterate on existing ideas and code
- Get comfortable making mistakes.
- Cherish the happy accidents
Effects and Techniques
There are basically two strains of effects being used.
- The first would be Hardware Effects (rasterbars, sprites,etc.) that are often used on 8/16bit machines to compensate for the lack of CPU power.
- The second group is pixel-based effects where the pixel color or index is calculated for each pixel (or chunky pixel on older platforms) on the screen.
Common Demoscene Effects
Size / CPU-Load / Effect table
Size | Effects |
---|---|
Small | Oscillators, Fractals, 2D Starfields, Hardware hacks |
Small | Pixel-Summing, Mathematical Functions |
Medium | Sine-plotters, Twisterbars, 3D Starfield, Dot Tunnels |
Medium | Plasma, Attractors |
Medium | UV/Image Distorters, Rotozoomers |
Medium | Raycasting, Voxels |
Large | Raymarching, Raytracing |
Oscillators
Oscillators can be use to draw circles and other interesting fractal-type shapes in only a few lines of code. Sometimes these oscillators are refered to as Minsky circles or Minsky machines. Note that oscillators can also be used in different settings as an alternative for sine/cosine in case of 2D or 3D Rotations.
In its most basic form it looks something like this:
s=128
t=0
for i=0, numdots do
x = x + y / 2
y = y - x / 2
plotPixel(x,y,color)
end
2D Starfields
2D Starfield can be generated cheaply using a pseudorandom for horizontal position and locking the speed to the vertical position/loopcounter. These can ofcourse be done in any 2D direction.
r=0
for y=0,height do
tt=time<<(y&3)
x=(y+tt+r)%256
plotPixel(x,y,color)
r=r+11
end
Pixel-summing / Automatons
Here you add a number of different pixels and shift back the result to create interesting effects, e.g. Fire effect, Random Walk, Color fight, GameOfLife, etc.
color = (img[x,y] + img[x,y+1] + img[x-1,y+1] + img[x+1,y+1]) / 4;
3D Starfield
Similar to a 2D starfield, but with 3D to 2D projection, here we are projecting a set of pseudo-random dots in 3D space down to 2D pixel coordinates
y=74
for i=0,numdots do
x=y
y=((y*5)&127)-64
z=(i*2-time)&127
plotPixel(x * 127 /z + centerx,y * 127 / z + centery,color)
end
Dot Tunnels
Dot tunnels have been classic demoscene effects since the early 90s. These are very similar to the 3D starfield, but instead of projecting random dots, you are now projecting various moving layers (zz) of circles (sin(a)/cos(a)) and moving the center position about.
for a=0,32 do
for z=0,32 do
// project tunnel dot
zz = (z-time) % 32
x = SIN(6.28/32*a) * 127 / zz
y = COS(6.28/32*a) * 127 / zz
// move tunnel center
centerx = SIN(t/32+zz/16)*64 + 120
centery = SIN(t/9+zz/14)*64 + 68
plotPixel(x + centerx, y + centery, color)
end end
Fractals
Rendering and/or animating a fractal like the IFS, Mandelbrot of Julia Set or something more simple like a sirpinski triangle. Here is an example of a julia fractal:
iterations=16;
while (--iterations>0) {
Y_new = (X+X)*Y-p
X_new = X*X-Y*Y+q;
X = X_new; Y = Y_new;
if (X*Y<3.5) goto storepixel;
}
color=iterations;
Attractors
An attractor is a pattern or set of behaviors that a system naturally settles into over time, even if it starts from different points. In systems like the Lorenz attractor, you repeatedly feed the output of a function back into itself, creating a feedback loop. Although the results may look chaotic, they tend to form a consistent shape or behavior, showing how complex systems can have underlying order. As a shorter hack, a form of attractor can be achieved by feeding the previous output of a sine/cosine calculation back into the phase of another function, like for example the 'universe' example.
n=40
m=200
r=2*PI/235
for i=0,n do
for j=0,m do
a=i+v
b=r*i+x
u=SIN(a)+SIN(b)
v=COS(a)+COS(b)
drawPixel(u * SCALE + centerx, v * SCALE + centery , color)
x=u+t
end
end
t=t+0.02
Mathematical Equations
Rendering a (2D) mathematical equation for each x,y value in relation to a algebraic formula. For example the heart-shaped equation looks something like this:
a=heartsize;
if (pow(y*y + x*x - a*a, 3)<y*y*y * x*x * a) then we are inside the heartshaped curve;
Plasma
Calculate the color for each pixel by adding a number of sine values, e.g.
color = sin(x*val+t) + sin(y*val2+t) + sin((x+y)*val3 + t)
Sine Plotters
Drawing pixels or shapes onto (a combination of) sine curves. These can be done on any number of axes. The most common ones use 2 axes, but for example you can imagine just locking the X or Y axis to the dot counter and only updating the other axis (i.e. starballs, starcoils, etc.).
for i=0, numdots do
x = sin(t/17 + i/spacing) * xrange + centerx;
y = sin(t/19 + i/spacing) * yrange + centery;
plotpixel_or_shape(x,y,color)
end
Twisterbar
Colored or textured twister bars connecting and clipping the edges of 3 or 4 sinevalues that differ across the Y axis. Here is an example of per-pixel twisterbar, but on oldschool platforms it is usually done with drawings simple (vertical) slices of a precalculated image.
w=(x-centerx)/32
v=(y-centery)/29 - 4
r = v * sin(t/19)%1.57 - 183
c = sin(r+11)
barshade = ((w-c)//2+c)*4
color = (w>S(r+22) AND w<S(r)) ? barshade : bgcolor
plotpixel(x,y,color)
Image/UV Distorters
Distort each pixel by essentially transforming your X,Y screenspace to something different (for example a polar space by taking the atan to center for x-axis and distance to center for y-axis). Other know distortion spaces for example are the Rotozoomer and Floorcast, which transforms your X,Y space into a perspective floor.
Perspective Floor
dx=x-centerx;dy=y-centerx
z = abs(dy);
U = x * scalevalue / z
V = perspectivevalue / z
Polar Coordinates/Tunnel
dx=x-centerx;dy=y-centerx
U = atan2(dx,dy) * 128/MATH_PI;
V = sqrt(dx*dx + dy*dy);
V=somevalue/sqrt(dx*dx+dy*dy)
Rotozoomer
dx = x-centerx
dy = y-centery
U = cos(angle)*dx - sin(angle)*dy
V = sin(angle)*dx + cos(angle)*dy
These U,V coordinates can then be used for either a bitwise operation (like a XOR or AND pattern for example) or lookup coordinates for your texture.
Raycasting
Walking through a scene using a fixed step in the Z direction (usually using fixed point math) and using these values inside a (logic) formula. e.g. 3D Sirpinski (Deus faber) or cubic grids (into a new era). See here a small example of pseudo code as used in into a new era:
dx = x-centerx
dy = y-centery
for d = -10,-99,-1 do
h = ((d*d + (d*dx) xor (dy*d)) / 256 + 4) and (d-t)
if (h&8>0) then plotpixel(x,y,(h-7 + ((x xor y)&1) )) break end
end
Raymarching
Step through 3D Space in Z direction using a variable step size, doing depth testing with all kinds of geometric equations (SDF's or Signed Distance Fields) to see if you've hit something, and if not step forward with that distance (usually multiplied with a factor less than 1 to avoid overstepping), getting you closer and closer to the actual distance each time. More often used in 4k and 1k intros than the really smaller ones because it takes a bit more setup, but definitely also possible for simple SDF's in smaller sizes.
vec3 ro = vec3(0, 0, -1); // ray origin
vec3 rd = normalize(vec3(uv.x, uv.y, 1)); // ray dir
vec3 r=ro;
float z=0;
for (i = 0; i < 64; i++) {
// repetition in all 3 axis
p = mod(r,1.0f) - 0.5f;
float d = sqrt(p.x*p.x + p.y*p.y + p.z*p.z) - 0.3; // 0.3 = sphere size
if (d < 0.01) break; // break out when hit
// step through scene
r += rd * 0.5f;
z += rd * 0.5f;
}
color=1.0/z;
The benefit of this technique vs raytracing is that you can find distances to a lot more shapes than analytical intersections, which gives you a lot more flexibility. Also you can distort the raydirection in any way you like during the stepping process, which can lead to interesting visuals. The downsides are ofcourse the code/cycles involved in the stepping process as well as the trickery required to get fake normals and somewhat decent UV coordinates.
Raytracing
CPU/FPU heavy calculations allow for calculating a intersection point and/or light model between your ray and 3D Solid equation (often spheres or planes) (see: stainless steel, the grid 512b ). Slower than raymarching in most cases, but often slightly less setup. Here is an example of a ray/sphere intersection, based on the ABC-formula we've been taught in highschool and you thought you would never need in your life probably 😉
//quadratic formula (zray is assumed 1, r = spheresize)
a = (xray * xray) + (yray * yray);
b = 2*(originx * xray) + (originy*yray);
c = (originx*originx) + (originy*originy) - (r*r);
// find intersection distance
d = (b*b)-(4*a*c);
if (d<0) t=0;
if (d==0) t=-b/(2*a);
if (d>0) {
t1=(-b-sqrt(d))/(2*a);
t2=(-b+sqrt(d))/(2*a);
t=min(t1,t2);
}
}
// Find intersection point
intersectionX=originx + t*xray;
intersectionY=originy + t*yray;
intersectionZ=originz + t*zray;
// Optional: calculate UV
u=fabs(intersectionZ*0.4);
v=fabs(atan2(intersectionY,intersectionX)*128/pi);
Design Tips
Design tip #1
Try to look around to see what is out there and where there is room for new stuff: Whenever i start experimenting with a new platform (like spectrum, atari, fantasyconsole, etc.) I literally watch EVERYTHING that is ever released in 256, 128 and below for that platform to get a feel for what is out there and where i can maybe add something.
Design tip #2
The actual execution is in (re-)combining tools from your bag of tricks... The more tricks in your bag, the amount of combining possible increases tenfold... So imho its always worth to try experimenting with (new) things/techniques without ever having a point of releasing it, or doing something concrete with it...
Design tip #3
Don't try to optimise early, but don't do things 'the expensive way' deliberately either. Try to sketch out the idea as complete as possible to what we had in mind. (trying to make zero to little compromises, regardless of size). So your intro will probably always go overboard by quite a bit in the beginning (300-350 bytes for 256, ~160-170 for 128 byte stuff) and once you're happy with the end result tone back from there and THEN start to make compromises (or not at all, so a 128 byte intro ends up as a 256b down the road , or a 256b intro is on hold , changes platform, or dismantled for parts) .
Design tip #4
If you can afford the luxury: Ideally you'd like to have at least 1 intro ready for release at any time , without a party in sight... This allows you to have a choice, maybe tweak on the end-result a bit more or to have something to submit last minute.