Porsche Black Edition

Thanks to Porsche, good people of UDG – United Digital Group and Technical Director Frank Reitberger, I was selected as one of three digital artist to collaborate with them to create a WebGL art piece around the new Porsche design line; The Black Edition. Other two artists were very well known old fellows Mario Klingemann and Frederik Vanhoutte.

In this post I’ll explain some of the technical things needed to get my particles dance with the engine sound in maximum quality and performance. Technology used for this piece was Fuse and its awesome programming language Uno.

I started the project by producing three different concepts around the engine sound. Client liked the particle pressure wave idea most and that was selected. I don’t know if I have permission to tell what other two ideas were, so I’ll just keep my fingers shut up about those. Perhaps they’ll materialize in the future.

The idea was to have a lot of particles on the scene and they would react to Porsche engine sound, creating interesting pressure waves. My presumption was that the client would not allow the car model vertice count go too low. The car should always look good. But still, the thing with particles is that more is more and not enough is same as not cool at all. I also wanted to have particle shadows.

Alright Simo… Make 200 000 particles dance smoothly based on sound and add 613 581 vertice car model with 29 draw calls in middle of it… in WebGL :)

Porsche 1

So I made a design decision: Camera would be still when particles are on the screen. I actually wanted to do this anyway. I’ve seen too much of that freely flying camera. And I think still camera would give the particles more role in the piece. The technical reason behind this is that it allows me to “cache” a lot of heavy draw passes to framebuffers. (+some more :) )

So what happens is that everything related to car model is rendered only once when camera is moved to another position. This includes:

Depthmap
DepthMap, packed to two components of RGBA. Giving it 16bit precision. (a trick learned from Fuse Tools WowFactory-package, I believe it was written by Lorents Etternavn)

public static float2 PackFloat16(float depth)
{
        var enc = float2(1.0f, 255.0f) * depth;
        enc = float2(Frac(enc.X), Frac(enc.Y));
        enc -= float2(enc.Y * 0.00392156862f, 0);
        return enc;
}

public static float UnpackFloat16(float2 rg_depth)
{
        return rg_depth.X + rg_depth.Y * 0.00392156862f;
}

Also the shadowmap depth for the car can be cached.
ShadowMap
First I draw the shadowmap for floor, then draw the cached car shadowmap to it…

draw Fuse.Drawing.Primitives.Quad
{
        WriteDepth: false;
        DepthTestEnabled: false;
        BlendSrc: Uno.Graphics.BlendOperand.SrcAlpha;
        BlendDst: Uno.Graphics.BlendOperand.OneMinusSrcAlpha;
        BlendEnabled: true;
                                                               
        CullFace: Uno.Graphics.PolygonFace.None;
        float4 px : sample(frame_shadow_map.ColorBuffer, ClipPosition.XY * 0.5f + 0.5f, Uno.Graphics.SamplerState.LinearClamp);
        PixelColor : float4(px.XYZ, (px.X+px.Y+px.Z));
};

…and then added particle depths.

So the rendering order was always the same with particles drawn as last. This means I cannot use the hardware z-sorting with particles. But since I got the depth already I created this little block

block DepthCut
{
        apply simppafi.RenderLibrary.GlobalDepthLinear;
        float LinearDepth : prev;
        float cut : GlobalDepthLinear > LinearDepth ? 1f : 0f;
        PixelColor : prev * cut;
}

Here’s actually one quality issue I left because of performance. The z-cut is quite harsh and it could have been more smooth (but eat more performance) when doing it like this:

block DepthCut
{
        apply simppafi.RenderLibrary.GlobalDepthLinear;
        float LinearDepth : prev;
        apply simppafi.RenderLibrary.ValuePixelOnScreen;
        float GlobalDepthLinear : simppafi.Utils.BufferPacker.UnpackFloat16(sample(simppafi.FramebufferStorage.GlobalDepthBuffer.ColorBuffer, ValuePixelOnScreen, Uno.Graphics.SamplerState.LinearClamp).ZW);

        float2 BLUR_RES : float2((1.0f/simppafi.FramebufferStorage.GlobalDepthBuffer.Size.X), (1.0f/simppafi.FramebufferStorage.GlobalDepthBuffer.Size.Y));

        float checkA : simppafi.Utils.BufferPacker.UnpackFloat16(sample(simppafi.FramebufferStorage.GlobalDepthBuffer.ColorBuffer, ValuePixelOnScreen + BLUR_RES.X, Uno.Graphics.SamplerState.LinearClamp).ZW) > LinearDepth ? 1f : 0f;
        float checkB : simppafi.Utils.BufferPacker.UnpackFloat16(sample(simppafi.FramebufferStorage.GlobalDepthBuffer.ColorBuffer, ValuePixelOnScreen BLUR_RES.X, Uno.Graphics.SamplerState.LinearClamp).ZW) > LinearDepth ? 1f : 0f;
        float checkC : simppafi.Utils.BufferPacker.UnpackFloat16(sample(simppafi.FramebufferStorage.GlobalDepthBuffer.ColorBuffer, ValuePixelOnScreen + BLUR_RES.Y, Uno.Graphics.SamplerState.LinearClamp).ZW) > LinearDepth ? 1f : 0f;
        float checkD : simppafi.Utils.BufferPacker.UnpackFloat16(sample(simppafi.FramebufferStorage.GlobalDepthBuffer.ColorBuffer, ValuePixelOnScreen BLUR_RES.Y, Uno.Graphics.SamplerState.LinearClamp).ZW) > LinearDepth ? 1f : 0f;

        float amount : (checkA+checkB+checkC+checkD) / 4f;

        float cut : GlobalDepthLinear > LinearDepth ? 1f : amount;
        PixelColor : prev * cut;
}

But this little detail can only be seen when particle is cut at the edge of the car. So I decided to go with better performance.

This is nice and all, but it won’t give shadows of the particles on top of the car. Since it’s cached. To pull out that miracle I needed to render more framebuffers. In order to render shadows on top the car I would need either its pixel coordinates, or even better, the shadowmap coordinates. In shadow mapping you would get the shadowmap RGBA value with the following code:

float4 ShadowCoord:
        req(WorldPosition as float3)
                Vector.Transform(float4(WorldPosition,1), EnvShadow.lightMatrix);

float2 ShadowTC: (pixel ShadowCoord.XY / pixel ShadowCoord.W) * 0.5f + 0.5f;

float4 ShadowMap : sample(EnvShadow.shadowMap.ColorBuffer, ShadowTC, Uno.Graphics.SamplerState.LinearClamp);

But this can be stored to framebuffer with this little block (with 16bit precision)

public block ShadowCoords
{
        float4 ShadowCoord:
                req(WorldPosition as float3)
                        Vector.Transform(float4(WorldPosition,1), EnvShadow.lightMatrix);
        float2 ShadowTC: (pixel ShadowCoord.XY / pixel ShadowCoord.W) * 0.5f + 0.5f;

        PixelColor : float4(simppafi.Utils.BufferPacker.PackFloat16(ShadowTC.X), simppafi.Utils.BufferPacker.PackFloat16(ShadowTC.Y));                         
}

Still this isn’t enough! I also need the depth to light source for every pixel of the car.
ShadowDepthCar

public block ShadowDepth
{
        float Depth:
                req(WorldPosition as float3)
                        (Uno.Vector.Length(pixel WorldPosition EnvShadow.lightPosition) / EnvShadow.lightDepth);

        PixelColor : float4(simppafi.Utils.BufferPacker.PackFloat16(Depth), 0,0);
}

All this probably sounds like heavy process, but you have to remember it’s only rendered once when the camera move. To hide all possible glitches I also set the scene to dark and fade back to light every time user switch the camera position. Also not all of these framebuffers have to be drawn in full resolution. Smoke and mirrors :)

Finally, in everyframe, all I do is sampling.

apply simppafi.RenderLibrary.ValuePixelOnScreen;
                       
float4 coords : sample(simppafi.FramebufferStorage.GlobalShadowCoordsBuffer.ColorBuffer, ValuePixelOnScreen, Uno.Graphics.SamplerState.LinearClamp);
float2 ShadowTC: float2(simppafi.Utils.BufferPacker.UnpackFloat16(coords.XY), simppafi.Utils.BufferPacker.UnpackFloat16(coords.ZW));

float Depth : simppafi.Utils.BufferPacker.UnpackFloat16(sample(simppafi.FramebufferStorage.GlobalShadowDepthBuffer.ColorBuffer, ValuePixelOnScreen, Uno.Graphics.SamplerState.LinearClamp).XY);
                       
float4 ShadowMap : sample(EnvShadow.shadowMap.ColorBuffer, pixel ShadowTC, Uno.Graphics.SamplerState.LinearClamp);

TADAAA!!

We have a shadows casted to and from cached high poly car in WebGL :)

BUT, and this BUT is rather enormous, that’s not all. Since we are drawing the car only once we can do something I thought I would never ever do in WebGL or in any other platform. The most performance heavy anti-aliasing, but best looking method out there. The SSAA, Supersample antialiasing. This is very simple to do, but eats a lot of performance for obvious reasons. You simply render the scene in 2X size and then downsample it to full res.
SSAA
Obviously I only do this for the shaded car model.

The shadow softness is done with technique called Variance Shadow Mapping (more detail)

car3

car4

car2

I should probably also mention that there are some DOF on particles (particles are scaled based on distance to camera) and little lens halo as post process. Here’s a link to all art works and campaign site.

These days Fuse is marketing their technology as mobile dev tool for Android and iOS, but you can also do .net and webgl exports with it. I use it for all of my WebGL gigs. Simply because I love its programming language, Uno, and how I can create very complex rendering pipelines with it. Fuse UI framework is very solid these days, but it’s also very 3D compatible :)

It feels pretty good to have a cool sports car brand in my portfolio :) Probably should create a new website in order to get more freelance gigs in the future. However, all I can care about now is to spent next two months for combined father leave / summer vacation. I still should write that technical blog post about the Android demo d159, since it contains even more tricks I learned this winter. Must say that I probably have never learned so much in so sort time then this winter. It feels good to know I can still evolve. All these projects will look like crap in few years, but what remains are skills I managed to gain during this time.

More skills equals more joy in programming. (this applies to everything)