My First Christmas Experiment
0

I am glad that I could participate the Christmas Experiments last year in 2014. My experiment was Christmas Card to Everyone. It allows you to customize your own Christmas cards and share it to your friends! It was also the first experiment that contains sexual content and strong language as far as I know. Christmas Card to Everyone shows you how unprofessional I can be and how to make a silly joke in a serious way. Well, of course it also allows you to spread your Christmas spirit to your friends.

Due to the tight timeline of this project, I couldn't make it work on mobile. So, if you are using mobile or your browser doesn't support WebGL, you can watch this video for now:

But all those crazy weird content aside, there are some visual techniques I used in the experiment that I think they are worth sharing. However, I don't want to make this post become another TLDR; article. I will mainly talk about these 3 effects:

  1. Spinning Background
  2. Text particles
  3. Motion blurred snow

If you are interested in other effects in the experiment or want to have a look at my code styles, you can grab a copy on Github

Before talking about the codes, I would like to talk about the concept and the design for a little bit so that you can get the idea of how/why the website ended up like that.

Concept

The idea of this experiment was to create a Christmas card sharing system so that people can create their own Christmas card and share it to their friends. In this experiment, I would like my audience to be involved with the experiment more rather than just see some cool visual effects. For that matter, the user will not only be able to write their greeting messages, they can also drag and drop the 3D items to customize their Christmas cards.

At first, I only planned to build this experiment with 2, 3 weekends time, but then there were some schedule postponing of my freelance work, I got extra free time to add more stuff in it. At some point, when I was working on the transition between the preloader and the main scene, I just unprofessionally added some filthy elements into the scene and then I added more and more. At the end I had to create warning screen for the perventMode as it was getting outrageous. I guessed my creativity is just sometimes used for the wrong purpose.

Design

As I wanted to practice my design skills and I have no designer friend because I am a dick, I had to handle the UI design and the 3D modeling on my own. Even though I am bad at design, luckily Low Poly design was still a hot trend in 2014 and it is pretty easy for new newbie like me to create something cool with Blender.

Before this experiment, my experience with Blender was nothing more than doing model format conversion tasks. So, it was a good timing for me to try learn some 3D modeling with it. So I spent some time to watch all of these tutorials from CG Cookie. After that I was looking for the tutorials for creating low poly terrain with Blender, and this particular one is perfect for my desires:

[image from tutsplus.com]

Based on the tutorials above, I managed to create a simple terrain and different 3D components with Blender:

Like what I mentioned before, my design skills are pretty bad. Sometimes when I couldn't create something how I wanted to properly, I would find an alternative(Same as coding). For example, for the low poly background of the Christmas card, I initially tried to work it out using Illustrator but I just couldn't do it. So, instead of spending more time to get it working, I simply opened the Blender and print-screened it(not even using a proper rendering). After that I just used that printed screenshot and worked it out in the Photoshop. Shameless.

Development

Before I talk about how I made those visual effects in the experiment, I really want to thank for the authors and contributors of THREE.JS and PIXI.JS. I can't really imagine what it would be like when I do my daily projects without these 2 awesome libraries. They made 3D and 2D front-end development 100 times easier.

So... Let's begin!

Spinning Background

At first, I just wanted to create a screen to show a simple prompt for the sexual warning. As I wanted to put it before the preloader so that the website can preload what it needs depending on the user's decision, I had to keep the effect as lightweight as possible. But after I almost finished the whole experiment, I decided to add a funky background to emphasize the idea of the warning like this:

For this effect, I used PIXI.JS because it is very easy to create an effect like this with it. I simply created a PIXI.Graphics, drew a radiation and then applied the twist shader afterwards. That's it. The details are as following:

Step1

First of all, I needed to determinate the length of the radial shape. To make sure the radial shape have perfectly covered the viewport, I needed to find the ideal radius of it like this:

And here is a snippet of the codes I used to create the radial shape using PIXI.JS:

// CONFIGS
var SEGMENTS = 24;
var WIDTH = 1000;
var HEIGHT = 400;
var RADIUS = Math.sqrt(WIDTH * WIDTH + HEIGHT * HEIGHT) / 2;

// PIXI initialization
var renderer = new PIXI.WebGLRenderer(WIDTH, HEIGHT, {
    antialiasing: false,
    transparent: false
});
var stage = new PIXI.Stage(0x000000);
document.body.appendChild(renderer.view);

// create the plain color background
var graphics = new PIXI.Graphics();
graphics.clear();
graphics.beginFill(0xffb0fc);
graphics.drawRect(-RADIUS, -RADIUS, RADIUS * 2, RADIUS * 2);
graphics.endFill();

// create the pinky radial shape background
graphics.beginFill(0xff15af);
graphics.moveTo(0, 0);
var angle = 0;
var deltaAngle = Math.PI / SEGMENTS;
for(var i = 0; i < SEGMENTS; i++) {
    graphics.lineTo(Math.sin(angle) * RADIUS, Math.cos(angle) * RADIUS);
    angle += deltaAngle;
    graphics.lineTo(Math.sin(angle) * RADIUS, Math.cos(angle) * RADIUS);
    graphics.lineTo(0, 0);
    angle += deltaAngle;
}
graphics.endFill();

// move it to the center of the stage
graphics.position.x = WIDTH / 2;
graphics.position.y = HEIGHT / 2;

// add it to the stage
stage.addChild(graphics);

// render call, should trigger it with requestAnimationFrame()
function render() {
    graphics.rotation += 0.008;
}

Using the script above, you can create a rotating background like this:

Sorry, this snippet requires pixi feature.
Please update your browser!
Loading...

Step2

After that, I needed to add the real juice - fragment shader. Creating your custom shader in PIXI.JS is as simple as this:

// define the uniforms
var uniforms = {
    u_shading: { type: '1f', value: 2.6},
    u_twist: { type: '1f', value: 4.3}
}

// fs is a string contains the raw GLSL of the fragment shader
stage.filters = [new PIXI.AbstractFilter(fs.split('\n'), uniforms)];

The steps of doing the twist effect in GLSL is something like:

  1. Convert the Cartesian form current coordinate into Polar form to get the distance and the angle from the current coordinate to the center point.
  2. Add the angle offset based on the distance to the center point. The lower distance it is, the higher angle offset it gets.
  3. Convert the Polar form coordinate back into the Caresian form.
  4. Get the color from the texture with the updated coordinate.
  5. Add some fake shading based on the difference of the updated coordinate and the original coordinate.

There is the GLSL codes:

precision mediump float;
varying vec2 vTextureCoord;
varying vec4 vColor;
uniform sampler2D uSampler;
uniform float u_shading;
uniform float u_twist;

void main(void) {
    vec2 coord = vTextureCoord;
    float level = pow(coord.x * coord.y, 0.3);
    coord = coord - vec2(0.5, 0.5);
    float d = length(coord);
    float a = atan(coord.y, coord.x) + u_twist * pow(1.0 - d, u_twist) * (level);
    coord = vec2(cos(a) * d, sin(a) * d) + vec2(0.5, 0.5);
    float delta = length(coord - vTextureCoord);
    vec4 color = texture2D(uSampler, coord);
    color.rgb += delta * sin(a) * u_shading * color.a; // fake shading
    gl_FragColor = color;
}

Here is the live demo:

Sorry, this snippet requires pixi feature.
Please update your browser!
Loading...

Text particles

To take advantages of the performance of using WebGL, all text particle movements were rendered in GLSL. In the Christmas Experiment, I used 3 vec3 attributes: a_offset_x, a_offset_y and the THREE.JS internal position to store the offset x, offset y and the alpha channel of the text particles in all 3 steps. By doing the interpolation on the uniform u_step which represent the current step index in the website, I could get the position and alpha channel information from any certain point from step 1 to step 3. I also created the virtual steps 0 and 4 so that the particles can seamlessly go from step 3 back to the step 1:

The whole development journal is as following:

  1. Draw the text into a canvas
  2. Get the non-completely transparent X, Y points and their alpha value from the canvas through context2d.getImageData()
  3. Pass them to the GLSL as attributes
  4. Animate the X, Y points in GLSL

To draw the text into a canvas was actually not as simple as I thought because I needed to overcome the word wrap and multiline issues. Luckily I discovered context2d.measureText() from this post - HTML5 Canvas Text Wrap Tutorial. I won't say that the codes in that tutorial are perfect, but it gave me a good place to start.

Just a reminder, context2d.measureText() will return a float instead of integer. You should always to apply a Math.ceil() after you obtain the value to prevent some side effects.(I wasted an hour on it)

After drawing the text on the canvas, I needed to get the X, Y and the alpha values of each non-transparent pixel. I simply did something like this:

function getPointsData(canvas);
    var x, y;
    var width = canvas.width;
    var height = canvas.height;
    var data = canvas.getContext('2d').getImageData(0, 0, width, height).data;
    var pointsData = [];
    for(var i = 0, len = data.length / 4; i < len; i++) {
        // only take the non-completely transparent particles to help the performance
        if(data[i * 4 + 3] > 0) {
            pointsData.push(
                // If you used context2d.measureText() without Math.ceil() and stored it as width,
                // you will get an italic style text particles :S
                i % width, // x position
                i / width | 0, // y position
                data[i * 4 + 3] / 255 // alpha channel in GLSL is 0-1 but not 0-255
            );
        }
    }
    return pointsData;
}

After getting the pointsData from the text canvases, I needed to ensure the particles amount are the same in each steps. For demo purpose, I will only use 2 steps so that you can have a better idea of what it is like. So, in order to make sure 2 steps use the same amount of particles, I simply filled in some fake point information into the pointsData which has less particle information:

var lessPointsData = getPointsData(textCanvas1);
var morePointsData = getPointsData(textCanvas2);
var maxPointsAmount = Math.max(lessPointsData.length, morePointsData.length) / 3;
var from = lessPointsData.length / 3;
var referenceIndex;
for(var i = from; i < maxPointsAmount; i++) {
    // pick a reference point from the existed point set so that the extra particles from other steps
    // will fade into some random existed position instead fading to one static point.
    referenceIndex = ~~(Math.random() * from);
    lessPointsData[i * 3] = lessPointsData[referenceIndex * 3];
    lessPointsData[i * 3 + 1] = lessPointsData[referenceIndex * 3 + 1];
    lessPointsData[i * 3 + 2] = 0;
}

// if you are using THREE.JS, you can add the attributes like this:
var pointGeometry = new THREE.BufferGeometry();
pointGeometry.addAttribute( 'a_text_info_1', new THREE.BufferAttribute( lessPointsData, 3 ) );
pointGeometry.addAttribute( 'a_text_info_2', new THREE.BufferAttribute( morePointsData, 3 ) );

In this case, each a_text_info_* attribute will contain all x, y and alpha information of the text in a certain step.

Whenever I play with particles, I always pass the index of the particles or a precalcuated Math.random() to into an attribute so that I can use different easing/noise for each particle. For example, in order to add the delay movement on the particles, there is a very simple trick you can do in GLSL:

attribute float a_random; // 0 to 1
uniform float u_animation; // 0 to 1
...
float animation = smoothstep(a_random * 0.3, 0.7 + a_random * 0.3, u_animation);

Using this smoothstep call, you can get an animation value in the 0 to 1 domain with up to 30% delay of the whole animation. Very handy isn't it?

From step 1 to step 2, I also wanted to add some noises to make it more dynamic. So, I used the simplex noise and here is the snippet of the codes:

vec3 pos = vec3(
    mix(a_text_info_1.x, a_text_info_2.x, step1To2Ratio),
    mix(a_text_info_1.y, a_text_info_2.y, step1To2Ratio),
    0.0);

// get a ratio from 0 to 1 and back to 0 for step1To2Ratio in [0..1] 
float midPointRatio = 1.0 - abs(step1To2Ratio - 0.5) * 2.0;

// add some noises to the x y offsets
pos.xy += vec2(
        snoise(pos.xy * 20.0 + u_time) * 30.0,
        snoise(pos.xy * 20.0 + 20.0 + u_time) * 30.0
    ) * midPointRatio;

After playing with the numbers and some easing functions, I created something like this:

Sorry, this snippet requires webgl feature.
Please update your browser!
Loading...

Motion blurred snows

In the experiment, if you took a deep look at the snow, you will notice that the snow balls are stretched when they are moving quickly and it looks like there is some motion blur effect on top of it.

For that, I used an inexpensive trick. Basically, what I did was I used a sprite sheet texture with the stretched snow animation in 16 frames like this:

It looks kind of lame when I lay it out like this but I found it very flexible to use a texture because I can fine tune it in Photoshop afterward to add some realistic motion blur effect if I want to. In my experiment, I kind of prefer it solid so I kept it that way.

With this texture, I could use the delta by comparing the current position offset with the one in the previous frame to determine the ideal index of the sprite sheet and then rotate the particle sprite in the fragment shader afterward.

To rotate the texture in fragment shader, I found this snippet

varying float v_index;
varying float v_rotation;

vec3 getPosOffset(float ratio) {...}

vec3 offsetPos = getPosOffset(ratio);
vec3 prevOffsetPos = getPosOffset(prevFrameRatio);
float delta = length(offsetPos.xy - prevOffsetPos.xy);

float max_delta = 3.0;
float esp = 0.0001;
v_index = floor(pow(clamp(delta, 0.0, max_delta) / max_delta, 3.0) * 16.00 - esp);
v_rotation = atan((offsetPos.x - prevOffsetPos.x) / (offsetPos.y - prevOffsetPos.y));

Another tip for animating particles: The snow dropping animation was controlled by one single uniform as well. Say you passed a precalcuated Math.random() as an attribute to the vertex shader and also created a u_time uniform which will increase a bit on every single frame, then you can get an individual animation ratio for each particle by doing something like this:

attribute float a_random; // a precalculated random number in [0..1] 

float thershold = 0.7 + a_random * 0.3;
float animation = mod(u_time - a_random * 2.0, thershold);

After putting everything together, it will be something like this:

Sorry, this snippet requires webgl feature.
Please update your browser!
Loading...

Conclusion

In this experiment, I have learned a lot. Especially trying to do as much as I can within the limited timeline. Sometimes when I couldn't mathematically resolve a problem, I would try to visually resolve it because at the end of the day, it is all about the visuals.

Also, here are some points I would love to improve if I have more time to work on:

  1. I would like to add some dynamically light effects which are synchronous to the background music because I have never played with Audio API.
  2. For the pubic/firework after the preloader, I should update the vertices directly instead of relaying on the PIXI.Graphics redrawing. The performance is very bad for doing heavy redrawing in PIXI.JS.
  3. I couldn't figure it out the way to load models into THREE.JS using the exporter, so I used .OBJ and .MTL which caused so many issues and in result I had to hardcode most of the texture color parameters.
  4. For the spinning effect, the ideal radius should be a bit larger as the twist effect can push the texture coordinate out of the 0..1 domain.
  5. Same for the spinning effect, drawing with Graphics is actually drawing with triangles in WebGL behind the scene. Since I applied some custom shaders, the WebGL default anti-alasing won't work anyway and in result I will get the radial shape with some ugly edges. Drawing the radial shape in Canvas and pass it to PIXI.JS as a texture of a Sprite will have a better result and keep the edges nice and smooth.

Well, lesson Learned.

I hope you have learned something from my experience. Cheers!

Share:
Comments
No comment yet
Cancel
What do you think?




All comments require my approval in order to be public. So... Spammers, please get the fuck out of my blog. Cunt!