Cubist artwork with the help of a GPU

Tags

, , , , ,

I wanted to come up with a shattered glass effect for my game. That didn’t quite work as intended, but I was nonetheless happy with the result. In this article I’ll describe the shader code used to create impressionist artwork.

cubist_stairs

Structure

The effect is basically a large number of convex quadrilaterals. They are tiled with a relatively simple loop.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
for( int y = 0; y < steps.Y; ++y ) {
    for( int x = 0; x < steps.X; ++x ) {
        if( C.rand.NextFloat() > config.segDensity ) {
            continue;
        }

        var xAt = (x + 0.5f)/steps.X;
        var yAt = (y + 0.5f)/steps.Y;
        var at = float2(xAt,  yAt) + (C.rand.NextFloat()*2-1) * gSize * config.segOffset;
        AddBlock( at, config.segScale * gSize );
    }
}

Where config is my parameters object controlling the effect. segDensity allows some tiles to be omitted. segOffset causes tiles to be shifted from their natural position, giving it a more chaotic look.

1
void AddBlock( float2 center, float2 size, float vary, bool edge )

Note that I also place one block of the full image behind these blocks. This ensures the entire view surface is drawn.

Vertex Attributes

As with anything OpenGL my effect is broken down into triangles and vertex attributes. For each block I created two triangles and assigned these properties to the vertices:

  • VertexCenter, float3( x, y, z )
  • PosLenAngle, float2( length, angle )
  • UVLenAngle, float2( length, angle )
  • UVRelative, float2( offsetX, offsetY )
  • IntensityHue, float2( intensity, hue )
  • Speed, float

All the vertices are stored in a large vertex buffer with an accompanying index buffer.

cubist_rabbits

VertexCenter and PosLenAngle

I wanted my effect to be animated, something simple like rotating the blocks. I didn’t want to waste CPU time on this, so I needed a way to do it on the GPU. The key to this is how I expressed the vertex positions.

Instead of storing the location of the vertices directly I store the center of the block and the offset to the corner. To make rotation easy the offset is expressed in polar coordinates. Given the center and size in AddBlock I calculate these values as follows:

1
2
3
4
var angleBase = Math.Atan2( size.X, size.Y );
var diag = Vector.Length( size ) / 2;

var angles = new []{ Math.PIf + angleBase, -angleBase, Math.PIf - angleBase, angleBase };

There are four angles, one for each corner. angleBase is calculated using some basic trigonometry to get the angle to the top-right corner. Since this is a rectangle we simply mirror that a few times to get the other corners.

cubism

Each vertex attribute stores a copy of center even though it is the same for all vertices in the block. This allows the GPU to calculate each vertex independently of the others.

Reconstruction of the corner is done in the shader as follows:

1
2
3
float2 PosOffset: float2( Math.Cos( PosLenAngle.Y ) * PosLenAngle.X,
    Math.Sin( PosLenAngle.Y ) * PosLenAngle.X );
float2 VertexPosition: VertexCenter.XY + PosOffset;

In this notation it is also very easy to deform the rectangle when producing the block. Simply add a bit of variation to the angle and the length.

UVLenAngle

The cubism effect is produced by drawing a slightly different image in each of the blocks. As a base we set the texture coordinates to the area covered by the block. If drawn this way there would be no effect, we’d just recreate the image. So instead each texture corner is rotated somewhat and scaled slightly.

1
2
var uvLen = posLen * uvScale;
var uvAngle = posAngle + uvRotate;

The first attribute UVLenAngle has the same structure as PosLenAngle. It indicates the position of the corner relative to the center of the block. These are actually just the values of the PosLenAngle with a small additional rotation and scaling applied.

At first I tried randomizing each corner on its own, but that produces strange visual distortion. So instead each block has a single scaling and rotation adjustment applied to all the corners.

cubist_girl

UVRelative

UVRelative stores the normalized coordinates for the block corners. Each corner gets one of these values:

1
var uvRels = new []{ float2(-1,-1), float2(1,-1), float2(-1,1), float2(1,1) };

This information can be used in the shader to determine how close the pixel is to the edge of the rectangle. In my effect I use this to darken, or lighten the edges.

1
2
3
float RectOff: Math.Max( Math.Abs( pixel UVRelative.X ), Math.Abs( pixel UVRelative.Y ) );
float Shade: Math.Pow( RectOff, config.shadeEdge ) * config.shadeLevel;
float3 ShadeColor: config.shadeColor;

Where the config values specify the width, intensity and color of the edging.

IntensityHue

This attribute is really two attributes packed into one: the intensity and hue adjustment.

The intensity is a simple multiplier applied to the color of the the pixels in the block. It makes each block slightly darker or lighter. The variation here is primarily to get some distinction between the blocks. I didn’t find the difference to be great enough, though putting the variation too high wasn’t pleasing.

I added a hue adjustment. Each block alters its hue slightly, and this produces a nice visual difference. It’s also capable of making the pretty mosaic-like pattern shown earlier.

Adjusting the actual hue of a color would require converting to HSL encoding, doing the adjustment, then converting back to RGB. It’s not a linear conversion either, which puts a bunch of conditionals into the shader. Another option would be YIQ where the conversion is linear.

Instead I took the cheap approach. The hue adjustment is a rotation of the color around the 1,1,1 vector:

1
2
float4 HueColor: float4( Vector.TransformAffine( TexMapColor.XYZ,
    Matrix.RotationAxis( float3(1), IntensityHue.Y ) ), TexMapColor.W );

It may not be a proper hue adjustment, and it incorrectly modifies the intensity, but for the effect that was okay.

Speed

The final attribute, Speed specifies how fast the block is rotating, and in which direction (positive or negative). The code I gave previously for the vertex position was without speed, with speed it looks like below:

1
2
3
4
5
float Rotation: Time * Speed;
float PosRotation: PosLenAngle.Y + Rotation * config.posRotSpeed;
float2 PosOffset: float2( Math.Cos( PosRotation ) * PosLenAngle.X,
    Math.Sin( PosRotation ) * PosLenAngle.X );
float2 VertexPosition: VertexCenter.XY + PosOffset;

Where Time is a time expressed in seconds, either the wall time or an elapsed time. Though I suppose an elapsed time would be better, since at high values the precision may be lost — though I didn’t notice this in my tests.

Particles

The initial generation of the blocks takes a long time, especially in the JavaScript version. Once generated however all the work is on the GPU. This allowed me to push the number of blocks up to very high amounts. It was a good test for building a particle effect system.

cubist_stairs_particles

A problem encountered is that not all targets support 32-bit index buffers, some are limited to 16-bits. I had to split up the blocks into several groups.

Running at full-screen on my system the above image consists of about a half million blocks. The rotation of the blocks can’t be seen very well, but a continuous shifting in the pattern is detectable.

Tunnel

At first I was looking for an effect I could use in my upcoming game Radial Blitz. I thought it might work as a shattered glass effect, but the result was simply too distorted to be playable. I could just turn down the variation, but it just doesn’t look interesting then. Here’s an image of what it looks like:

cubist_tunnel

I did a short video showing the effect applied to the tunnel.

About these ads
Follow

Get every new post delivered to your Inbox.

Join 303 other followers