PAGES

23

Oct 12

The Tech Behind a Contre Jour Snot



While the award winning game Contre Jour is amazing on many fronts, its stunning visuals are probably its most unique asset. As detailed in a colleague’s blog post the game creates its graphics through the OpenGL drawing framework and several elements are dynamically rendered in a more complex manner than by simply drawing images. One of these is the Snot (also known as the Tentacle). They are the most common tools used to help guide Petit through the perils of his epic journey. So sit back and relax as we take a deep dive into how they are constructed as well as how we overcame the pitfalls of translating one into HTML5

The Physics Magic

In order to understand how drawing snots works, it is important to understand the basic structure of one. Please note that Contre Jour uses the 2d physics engine Box2d for its physics. We aren’t going to go over much of the physics aspect of the snot in this blog, but if you’ve never dealt with a physics engine before a quick overview of the documentation may help you understand the next sentence. The talented programmers at Mokus Games designed the snot out of multiple Box2d bodies which are sequentially connected to each other with joints (with the type of joint depending on the type of snot). You can think of each body as being a hinge that flexes when forces are applied to it. Since each body is connected to one another, forces applied to one body will cause the others to react as well.  What is most important for us, however, is that the current location of each body is accessible. These control points will serve as the basis of all the upcoming drawing techniques.

Drawing a Snot

Now that we have a very basic understanding of the inner workings of a snot, we can take off our Box2d hats and think about how we are going to draw one. As shown in the picture below, there are two distinct sections of the snot that will require us to draw them using a different technique.

snot sections

The yellow sections contain the top and bottom portions of the snot and are comprised completely of images, so all we have to do is get the respective positions of the top and bottom Box2d bodies and draw the images at the appropriate location. This is easy to do in the HTML5 canvas and only requires the use of the drawImage method. The blue section that denotes the middle of the snot, however, is a bit more complex to draw. In order to precisely mimic the look and feel of the movement of the underlying bodies, a static image just won’t cut it. Instead, the game’s creators developed a formula that calculated the control points for a bezier curve from the snot’s bodies, calculated an array of intermediate points from the control points, and then gave the array to OpenGL to draw the smooth curve out of many triangles. At first glance, this may seem an impossible technique to translate to javascript. After all, we have no access to OpenGL. However, using the power of the HTML5, it is actually easier to implement the drawing of the body in the canvas than it is through native OpenGL! We simply took the control point calculation from the original code and drew quadratic curves through the canvas’s quadraticCurveTo function and filled the resulting path. The code and results are shown below:

// variables: ctx = HTML5 context, color = rgba color string,
// pairs: array of control point pairs, with each element containing a 
// first and second point that mirror each other along the body.
ctx.fillStyle = color;
ctx.beginPath();

start = pairs[0].first, control, anchor;
ctx.moveTo(start.x, start.y);

for (var i = 1; i < pairs.length; i += 2) {
   control = pairs[i].first;
   anchor = pairs[i + 1].first;
   ctx.quadraticCurveTo(control.x, control.y, anchor.x, anchor.y);
}

ctx.lineTo(pairs[pairs.length - 1].second.x, points[pairs.length -1].second.y);

for (var i = pairs.length - 2; i > 0; i -= 2) {
   control = pairs[i].second;
   anchor = pairs[i - 1].second;
   ctx.quadraticCurveTo(control.x, control.y, anchor.x, anchor.y);
}

ctx.lineTo(start.x, start.y);
ctx.closePath();
ctx.fill();

iOS snot on left, HTML5 snot on right

Drawing a Gradient Snot

While drawing a solid colored snot in HTML5 isn’t a big challenge compared to the native application, drawing the snots for the “Night” world (chapter 2) is a somewhat thornier issue. To modify the code to shade the snot with a gradient, the original version simply gives OpenGL a color array that it calculates based on the number of calculated curve points. When OpenGL draws the snot vertices, it uses the colors stored in the array to color the resulting shapes appropriately. Unfortunately, this is an area where canvas does not have the same flexibility. What canvas does have, however, is an API for creating linear gradients, which is what the snot body looks like. It isn’t as simple as creating a gradient and filling the snot body in one shot as we did with the solid black snot though. As the name implies, a linear gradient is defined as being, well, linear. The API expects a start point and an end point, which it takes and creates a gradient that goes straight between the two points. So if we simply create a gradient that goes from the snot head to its tail, the gradient will not fit correctly on the snot body unless it is in a straight line between the two points. Since a snot is very elastic, this will not always be the case. Luckily for us, simply segmenting how the snot is drawn solves this problem for us. Instead of drawing a path for the whole snot body at once, we break the control points up into groups that roughly correspond to the segments between the Box2d bodies and draw each segment individually and with a gradient that maps to the top and bottom of said segment.

visual representation of the gradient segments

While not 100% accurate, the behavior of the bodies connected by joints guarantees us that each segment will be fairly linear most of the time (except when a snot is bent at a hard angle). Though using phrases such as “not 100% accurate” and “fairly linear” are hardly confidence inspiring, the results of this technique are very close to the original.

// The colors parameter is an array of colors that represent
// the color the snot should be at each control point.
var point0, point1, point2, grd, color0, color1;
context.save();
var colorIndex = 0;

// Iterate through all the segments and draw each one individually.
for (var i = 1; i < points.length; i += 2) {
   point0 = points[i - 1];
   point1 = points[i];
   point2 = points[i + 1];
   color0 = colors[colorIndex];
   color1 = colors[colorIndex + 1];

   // In order to hide breaks it helps to draw the segments
   // with a slight overlap.
   if (i > 1) {
      Box2dUtil.b2Vec2Lerp(point0.first, points[i - 2].first, 0.05, point0.first);
      Box2dUtil.b2Vec2Lerp(point0.second, points[i - 2].second, 0.05, point0.second);
   }

   Box2dUtil.getCenter(point0.first, point0.second, DrawUtil.center);
   Box2dUtil.getCenter(point2.first, point2.second, DrawUtil.center2);

   // Create a gradient that runs from the top to the bottom of the segment
   // using the color value array and set the gradient as the fillStyle.
   grd = context.createLinearGradient(DrawUtil.center.x, DrawUtil.center.y, DrawUtil.center2.x, DrawUtil.center2.y);
   grd.addColorStop(0, color0);
   grd.addColorStop(1, color1);
   context.fillStyle = grd;

   // Fill in the segment.
   context.beginPath();
   context.moveTo(point0.first.x, point0.first.y);
   context.quadraticCurveTo(point1.first.x, point1.first.y, point2.first.x, point2.first.y);
   context.lineTo(point2.second.x, point2.second.y);
   context.quadraticCurveTo(point1.second.x, point1.second.y,point0.second.x, point0.second.y);
   context.lineTo(point0.first.x, point0.first.y);
   context.closePath();
   context.fill();
   colorIndex++;
}

context.restore();

iOS snot on left, HTML5 snot on right

Texturing the Rope

Despite the daunting task of porting all the OpenGL code to HTML5, so far we’ve done all right. The solutions we’ve come up with are fairly easy to understand and implement and stay pretty true to the original game. Sadly, there is always that one unruly individual who likes to rain on everyone’s parade and steal all the cookies. In our case this came to us in the form of the rope snot. In the iOS game, the striped pattern on the rope body is created by using a single texture:

rope texture

The texture is then mapped to the snot body shape by OpenGL using texture mapping in a repeat pattern along the length of the body. This technique ensures that the texture fits perfectly with no overlap or breaks between sections.

Now the million dollar question is how to emulate the results of texture mapping in HTML5 (which has no concept of true texture coordinates) without slowing the browser to a crawl in the process. We tried several techniques to get this to work, including linear gradients, canvas pattern fills and even experimented with trying to create true texture mapping in the canvas. None of these created an effect that looked at all like the original. Luckily, after all these options were exhausted there was one method left to be tried, a rough approximation of texture mapping. It’s really an extension of the gradient technique from the last section. First, the snot body was segmented in the same fashion as the night snot. These sections were then cut into two halves. For each half, we calculated a transform to apply to the texture that mapped as close as possible to the half size. The center of the half was used as the texture’s position, while the rotation was found through calculating the angle of the vector from the top of the segment to the bottom. The x and y scale values were both individually computed by calculating a proportion of the average length and width of the segment with the length and width of the texture. Then, the canvas clip method was used to ensure that any excess texture outside of the segment wouldn’t be drawn.

The big flaw with this method is that without anything else it produces a noticeable disjoint between the individual segments of the rope and the outer curve loses some of its smoothness.

disjointed texture sections

However, this can be minimized by drawing the entire body in with a solid color (like the normal snots) that matches the start and end of the texture pattern before drawing the texture segments. While it effectively hides the seams, it does create a small border on the outside of the snot that becomes more noticeable when the snot does a lot of twisting and turning. It actually creates a sort of 3D effect because of the color of the texture border, so we can chalk this one up as a “feature” and call it a day.

// backColor is a passed in rgba string that we can use to set the
// context color.
var point0, point1, point2;

// Fill in back with solid color to hide breaks using the method
// described in the solid snot section.
DrawUtil.drawBezier(context, points, backColor); 

// Fill the rest of the straight areas with texture.
// This method makes use of a custom Bitmap class to handle drawing
// the texture.
context.save();
for (var i = 1; i < points.length-1; i += 2) {
   point0 = points[i - 1];
   point1 = points[i];
   point2 = points[i + 1];

   if (i > 1){
      Box2dUtil.b2Vec2Lerp(point0.first, points[i - 2].first, 0.05, point0.first);
      Box2dUtil.b2Vec2Lerp(point0.second, points[i - 2].second, 0.05, point0.second);
   }

   // Draw segment path.
   context.beginPath();
   context.moveTo(point0.first.x, point0.first.y);
   context.quadraticCurveTo(point1.first.x, point1.first.y,  point2.first.x, point2.first.y);
   context.lineTo(point2.second.x, point2.second.y);
   context.quadraticCurveTo(point1.second.x, point1.second.y, point0.second.x, point0.second.y);
   context.lineTo(point0.first.x, point0.first.y);
   context.closePath();

   // Calculate a position, rotation, and scale to map half of the
   // texture with the top half of the segment. 
   context.save();

   // Set the texture to draw only the front half.
   texture.frame = 0;
 
   Box2dUtil.getCenter(point0.first, point0.second, DrawUtil.center);
   Box2dUtil.getCenter(point1.first, point1.second, DrawUtil.center2);
   // Calculate the rotation of the texture from the vector between the top and bottom centers of the top half segment.
   var direction = Box2dUtil.b2Vec2Subtract(DrawUtil.center2, DrawUtil.center);
   var rotation = Box2dUtil.atan2Vec(direction);

   // Calculate the y scale by dividing the segment's length by the
   // texture's length.
   var scaleY = direction.Length() / 16;

   // Calculate the x scale by averaging the widths of the top and
   // bottom of the segment and then divide that by
   // twice the width of the texture (because we are averaging two together).
   var point1Width = Box2dUtil.b2Vec2Distance(point1.first, point1.second);
   var scaleX =(Box2dUtil.b2Vec2Distance(point0.first, point0.second) + point1Width) / (32 * 2);
   // Calculate the center of this half of the segment.
    Box2dUtil.getCenter(DrawUtil.center, DrawUtil.center2, DrawUtil.center);  
   // Clip the context and use a draw method of the Bitmap to draw
   // the texture with the calculated values.     
   context.clip();
   texture.drawNoTransform(context, DrawUtil.center.x, DrawUtil.center.y, rotation, scaleX, scaleY); 
   context.restore();

   // Repeat for the bottom part of the segment.
   context.save();
   texture.frame = 1;
   Box2dUtil.getCenter(point2.first, point2.second, DrawUtil.center);
   var direction = Box2dUtil.b2Vec2Subtract(DrawUtil.center, DrawUtil.center2);
   var rotation = Box2dUtil.atan2Vec(direction);
   scaleY = direction.Length() / 16;
   scaleX = (point1Width + Box2dUtil.b2Vec2Distance(point2.first, point2.second)) / (32 * 2);
   Box2dUtil.getCenter(DrawUtil.center, DrawUtil.center2,DrawUtil.center);            
   context.clip();
   texture.drawNoTransform(context, DrawUtil.center.x, DrawUtil.center.y, rotation, scaleX, scaleY);
   context.restore();
}
context.restore();

iOS rope on left, HTML5 rope on right

As you can see, it took some creativity to replace OpenGL drawing with the canvas, but with a little elbow grease and the processing power provided by hardware-accelerated browsers such as IE10 we managed to create a pretty consistent look with the native application that managed not to slow it down to a crawl. So I encourage you to go out and experiment with HTML5 yourself. Maybe you’ll accidentally make the next big browser game.

0 comments , permalink


Tagged , , , ,