Home Contact

[January 16, 2006]

Psyark’s DisplacementMapFilter Tutorial

Filed under: ActionScript — @ 3:11 pm

A couple of people requested the translation of the Japanese DMF Tutorial I referred to in my post Prospects for Immersive Panoramas in Flash. Here it is.

author: psyark
translation: Alan Shaw [all errors are mine]

Part 1
(Original text)

The DisplacementMapFilter is a filter that uses one bitmap to operate on another. The former bitmap is commonly called the mapBitmap; we’ll call it the map image.

Each pixel (x,y) of the output bitmap takes its value from an input image pixel specified by the color information of the pixel at (x,y) in the map image.

An output pixel with coordinates (x, y) is displayed according to the following formula. m[y][x], d[y][x], s[y][x] are the pixels at position (x, y) in the map image, destination image, and source image respectively.

var dx = (getComponent(m[y][x], componentX) - 0×80) * scaleX / 0×100;
var dy = (getComponent(m[y][x], componentY) - 0×80) * scaleY / 0×100;
d[y][x] = s[y + dy][x + dx];
function getComponent(color32:Number, component:Number):Number
{
	switch (component)
	{
		case 1 : return 0xFF & color32 >> 16;
		case 2 : return 0xFF & color32 >> 8;
		case 4 : return 0xFF & color32;
		case 8 : return 0xFF & color32 >> 24;
	}
}

We show several special cases. We assume that neither componentX nor componentY is 8 (the alpha channel).

· Where the map image is 50% gray (0x808080), the output image is the same as the input image.

· Where the map image is black (0x000000), the output image becomes the input image shifted by (scaleX/2, scaleY/2).

· Where the map image is white (0xFFFFFF), the output image becomes the input image shifted by (-scaleX/2, -scaleY/2). (Actually a little less; the factor is really 0x7F/0x100.)

Place the cursor over any pixel in the output image [on the left] to see its source pixel in the input image [on the right].

[Menu items: Parallel lines; Perspective; Earth globe]


Next time we’ll show how to make the map image that does the displacement.


Part 2
(Original text)

Using the properties of the DisplacementMapFilter described previously, we’ll show how to make the map image.

earth160.jpg

We choose for our example the displacement needed to map an equidistant cylindrical [i.e. equirectangular] projection of half of the world map onto the globe.

Each pixel of the globe output image corresponds to a certain pixel in the input image. Let’s try to show that relationship by a formula.

earth_out.gif

This image shows the displacements of the lines of latitude and longitude. You can see that although the meridians become curved, the parallels remain straight. In other words, points with equal Y coordinates in the output image will correspond to points with equal Y coordinates in the input image. Therefore we can show the correspondence of Y coordinates without considering the X coordinates.

So first we consider the correspondence in the Y direction (latitude).

trans_y.gif

This figure represents the view of the output image from the left. The input image is mapped onto the curved line shown in blue.

Let’s try to show the formula that relates the input image Y coordinate (Y) and the output image Y coordinate (Y’).

In the figure, the Y arrow comes 1/3 of the way down from the top of the input image. This can be expressed as π/6 north latitude.

In this case the distance from the equator is sin(π/6), so the output Y coordinate (Y’) becomes (1 - sin(π/6)) / 2. Generalizing, we get

Y' = (1 - sin(π * (0.5 - Y))) / 2.

Now for constructing the map image the question is “for a certain output coordinate, what input coordinate does it come from?” So we solve the equation for Y:

Y = 0.5 - asin(1 - Y' * 2) / π

And from this we can derive what we really need for the map image, the displacement:

yd = 0.5 - asin(1 - Y' * 2) / π - Y'

We’ve derived the equation for the Y-axis displacement. Let’s make the filter:

import flash.display.BitmapData;
import flash.filters.DisplacementMapFilter;
import flash.geom.Rectangle;
Stage.scaleMode = "noScale";
var W:Number = 160;
var H:Number = 160;
var earth:BitmapData = BitmapData.loadBitmap("earth");
var map:BitmapData = new BitmapData(W, H, false);
var out:BitmapData = map.clone();
for (var y = 0; y < H; ++y)
{
	var yr = y / H; // ratio of Y coordinate to image height
	var lat = Math.asin(1 - yr * 2); // latitude
	var yd = 0.5 - lat / Math.PI - yr; // difference between Y-coordinate ratios before and after transformation
	var yc = Math.round(yd * 0x100) + 0x80;
	map.fillRect(new Rectangle(0, y, W, 1), yc);
}
var disp:DisplacementMapFilter = new DisplacementMapFilter(map, null, 1, 4, 0, H);
out.applyFilter(earth, map.rectangle, null, disp);
attachBitmap(out, 0);

Execution result

Download fla

We’ve converted the Y coordinates. This has gotten long, so we’ll take care of the X coordinate next time.

※ I used NASA’s Blue Marble image of the earth to make the picture.

You can refer to NASA’s guidelines for use of the pictures at the NASA site.


Part 3
(Original text)

We continue to explain how to make the map image for the DisplacementMapFilter.

Last time we derived the equations for the Y coordinate displacement.

Y = 0.5 - asin(1 - Y' * 2) / π
yd = 0.5 - asin(1 - Y' * 2) / π - Y'

We can do the X coordinate transformation the same way.

trans_x.gif

This figure shows the output image viewed from above. The blue curve is the equator.

As before, we derive the equations relating the X coordinate (X) of a point on the equator of the input image to the X coordinate (X’) of the corresponding point on the equator of the output image, and their distance xd:

X' = (1 - cos(π * X)) / 2
X = acos((0.5 - X') * 2) / π
xd = acos((0.5 - X') * 2) / π - X'

The length of a latitude line becomes shorter as latitude rises; we denote the ratio of its length to the equator’s length by ew.

X' = (1 - cos(π * X) * ew) / 2
X = acos((0.5 - X') / ew * 2) / π
xd = acos((0.5 - X') / ew * 2) / π - X'

And since ew = cos(latitude), we refer to the way we calculated latitude last time and get

ew = cos(asin(1 - Y' * 2))

Thus we are able to express the (X, Y) coordinates of the corresponding point in the input image in terms of the (X’, Y’) coordinates in the output image.

Applying this to our previous ActionScript code:

for (var y = 0; y < H; ++y)
{
	var yr = y / H; // ratio of Y coordinate to image height
	var lat = Math.asin(1 - yr * 2); // latitude
	var yd = 0.5 - lat / Math.PI - yr; // difference between Y-coordinate ratios before and after transformation
	var yc = Math.round(yd * 0x100) + 0x80 << 8; // map's Y component
	var ew = Math.cos(lat); // length of parallel
	for (var x = 0; x < W; ++x)
	{
		var xr = x / W; // ratio of X coordinate to image width
		var xd = Math.acos((0.5 - xr) / ew * 2) / Math.PI - xr; // difference between X-coordinate ratios before and after transformation
		var xc = Math.round(xd * 0x100) + 0x80; // map's X component
		map.setPixel(x, y, yc | xc);
	}
}
var disp:DisplacementMapFilter = new DisplacementMapFilter(map, null, 4, 2, W, H);

However, since this code ignores the area outside the globe, that part of the input image remains unchanged.

Now outside the circle of the globe, (0.5 - xr) / ew * 2 is outside the range -1~1, and thus its arccosine is undefined. Let’s use this fact:

for (var x = 0; x < W; ++x)
{
	var xr = x / W; // ratio of X coordinate to image width
	var lap = Math.acos((0.5 - xr) / ew * 2); // longitude
	if (isNaN(lap))
	{ // undefined longitude means outside the circle
		var xc = xr > 0.5 ? 0xFF : 0;
			// set the map’s X component to a large number
	}
	else
	{
		var xd = lap / Math.PI - xr; // difference between X-coordinate ratios before and after transformation
		var xc = Math.round(xd * 0×100) + 0×80; // map’s X component
	}
	map.setPixel(x, y, yc | xc);
}

And, in order to paint the pixel black when its source coordinates cannot be obtained, we modify the filter parameters:

var disp:DisplacementMapFilter = new DisplacementMapFilter(map, null, 4, 2, W, H, "color", 0x000000);

With this the filter is complete.

Polite expression involving fatigue.

Execution result

Download fla

Addendum

Image quality can be improved by using as much as possible of the 0x00~0xFF range of each of the map image components. I won’t go into detail on the procedure, but these are the main points:

1. Multiply the map’s X component by N with 0x80 as a center
2. Divide the map’s scaleX by N

If N is so large that the range 0x00~0xFF is exceeded, adjust it appropriately.


Contents copyright © Alan Shaw 2005-2008

25 queries. 0.281 seconds. Powered by WordPress version 2.5.1