Design Tips and Demoscene effects with pseudo code

From SizeCoding
Jump to: navigation, search

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);
For a perspective tunnel:
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.