Menu:

¯
RSS  RSS 2.0

Neptune

Go get flash player

Shortly after I made my Earth thing, I realized that it would simply be incomplete without also implementing it for arbitrary angles. So, I grabbed Neptune map by Björn Jónsson and started working on new SWF. Little I knew then how far will it take me in the end... Drag your mouse to change angle and release it to rebuild displacement map; meanwhile I'll tell you how it's done.

So, I was looking for Flash 3D engine in order to render planet «preview» you see there while dragging. Probably I live a bit behind times and I still use Flash 8, so I ended up messing with Sandy 1.1 and reading great tutorials by Petit.

To rotate some object with mouse in Sandy you use an instance of Transform3D class. You call its rot() method with px and pz being functions of mouse coordinates. You don't have to use 3rd angle, since you can scroll texture for that. You can then retrieve Euler rotation matrix with getMatrix() method at any time.

Blah blah blah, but what about computing a displacement map? Since I alredy had Sandy's math in my SWF, I made some use of it here as well. How? First, you need to make a vector for every sphere pixel. This is straightforward, once you understand Sandy coordinates:

R = 128;
for (var x:Number = 0; x < 2 * R; x++)
for (var y:Number = 0; y < 2 * R; y++) {
    // vector in Sandy coordinates
    var v:Vector = new Vector (x-R, y-R,
        -Math.sqrt( R*R - (x-R)*(x-R) - (y-R)*(y-R) ));

Ok, now you need to know what position was this vector rotated from:

    // apply reverse rotation if v falls inside the sphere
    if (v.x*v.x + v.y*v.y < R*R)
        Matrix4Math.vectorMult (
            Matrix4Math.getInverse (rotaion.getMatrix()), v);

To improve performance, rotaion.getMatrix() should be inverted just once outside the loop. From here you basically repeat all the steps you should already know:

    // horizontal radius
    var r:Number = Math.sqrt (R*R - v.y*v.y);
						
    // lattitude & longitude
    var phi:Number = Math.asin( v.y/R );
    var psi:Number = (Math.abs( v.x ) < r) ? Math.asin( v.x/r ) : Math.PI/2;

    // correct psi for invisible hemisphere
    if (v.z > 0) psi = ((psi > 0) ? Math.PI : -Math.PI) - psi;

    // source pixel coordinates for (x,y)
    var xS:Number = R + R * psi / (Math.PI/2) * 0.5;
    var yS:Number = R + R * phi / (Math.PI/2);

    // wrap coordinates to compensate for round-off errors
    xS = (xS < 0) ? (xS + 255) : ((xS > 255) ? (xS - 255) : xS);
    yS = (yS < 0) ? 0 : ((yS > 255) ? 255 : yS);

    // calculate & store displacement in B & G channels
    dmap.setPixel (x, y,
        bf * (128 + Math.round(0.5 * (xS - x)) ) +
        gf * (128 + Math.round(0.5 * (yS - y)) ) );
}

The 0.5 in expression for xS is there to use square texture: in my case, I have both map and texture 256x256. This is also the reason why there is 0.5 in setPixel() - we need to make sure there will be no out-of-range values even with the most extreme angles. To compensate for that, you need to double your filter scale:

// something like this:
new DisplacementMapFilter (dmap, new Point (), 4, 2, 2*256, 2*256)

That's it. Since you don't really need to code all of that yourself, I made the map visible in the SWF. So all you really have to do for this effect, is to make a screenshot, crop it and import to your FLA library. Enjoy!

Comments