This page describes the code used in the Papervision3D clouded planet Earth tutorial and source post.
Planets and Earth
Since I’m trying to learn Papervision3D to make a spaceshooter, I’ve decided to make a generic Planet class and a subclass, Earth, which provides specific planet details such as textures and clouds. This way I can easily create new kinds of planets, including ones with procedurally generated textures.
In pseudo code, here’s how the Planet and Earth classes work together:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class Planet extends DisplayObject3D { constructor { createAddAndConfigureSpheresAndPlanes(createMaterials()); configureLayeringAndMasking(); } abstract function createMaterials() { // provided by subclasses (Earth) } } class Earth extends Planet { override function createMaterials() { // create Earth materials } } |
Planet code
Let’s begin with the Planet baseclass.
1 2 | public function Planet(viewport:Viewport3D, diameter:Number, sunLight:PointLight3D = null, quality:Number = QUALITY_MEDIUM, planetRotationSpeed:Number = .3, cloudsRotationSpeed:Number = .15, updateFrequency:Number = 125, phasePerSecond:Number = 1) { |
As you can see the constructor is rather large. This is because there are many aspects of the planet’s quality you can control. In addition there are some parameters that go to Planet’s superclass RealtimeDisplayObject3D. I haven’t mentioned this class yet, because it is outside of the scope of this article and I’ve written a blogpost about it earlier. In short, it is a very simple class that performs updates based on time events rather than frame events, so that we can update a planet in realtime (such as the clouds) instead of how many fps we get.
The quality parameter only controls the quality of the main sphere with the land texture. In theory it could be passed down to the subclasses in the createMaterial() methods giving the subclasses like Earth a chance to change the quality of the materials. This way you could for example create three versions of the same planet for different distances from the camera, so that far away planets have very low detail quality.
1 2 3 4 5 6 7 8 9 10 | // for some reason I can't get the planes for circle masks and fresnell // glow to get the exact same diameter as the spheres const GLOWSIZE:Number = diameter * 1.06; const GRADIENT_QUALITY:Number = 500; addChild(_planet = new Sphere(createPlanetMaterial(), diameter / 2, quality, quality)); addChild(clouds = new Sphere(createCloudMaterial(), diameter / 2, QUALITY_LOW, QUALITY_LOW)); addChild(planetMask = new Plane(createPlanetMaskMaterial(GRADIENT_QUALITY), GLOWSIZE, GLOWSIZE)); addChild(cloudMask = new Plane(createPlanetMaskMaterial(GRADIENT_QUALITY), GLOWSIZE, GLOWSIZE)); addChild(glow = new Plane(createGlowMaterial(GRADIENT_QUALITY, [0, .1, .3, 0], [0xBB, 0xE9, 0xFA, 0xFF]), GLOWSIZE, GLOWSIZE)); |
As you can see the sphere’s are created with materials obtained from other methods like createPlanetMaterial(). These are provided by the Earth subclass.
1 2 3 4 | // apparently planes always need to be flipped over (or have doublesided materials) glow.geometry.flipFaces(); planetMask.geometry.flipFaces(); cloudMask.geometry.flipFaces(); |
To solve this you can make the material doublesided, or you can just flip the normals (faces) of the planes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // structure layers so that the glow is always on top of the planet var containerLayer:ViewportLayer = PV3DUtil.createLayer(viewport, null, viewport.containerSprite); var planetLayer:ViewportLayer = PV3DUtil.createLayer(viewport, planet, containerLayer, 0); var cloudLayer:ViewportLayer = PV3DUtil.createLayer(viewport, clouds, containerLayer, 1); var planetMaskLayer:ViewportLayer = PV3DUtil.createLayer(viewport, planetMask, containerLayer, 2); var cloudMaskLayer:ViewportLayer = PV3DUtil.createLayer(viewport, cloudMask, containerLayer, 3); var glowLayer:ViewportLayer = PV3DUtil.createLayer(viewport, glow, containerLayer, 4); containerLayer.sortMode = ViewportLayerSortMode.INDEX_SORT; /** * (Utility function in a separate utility class, PV3DUtil) * Creates a preconfigured ViewportLayer. Preconfigured are: layerIndex, parent layer, added DisplayObject3D. */ public static function PV3DUtil.createLayer(viewport:Viewport3D, do3d:DisplayObject3D = null, parent:ViewportLayer = null, layerIndex:Number = 0):ViewportLayer { var viewportLayer:ViewportLayer = new ViewportLayer(viewport, do3d); viewportLayer.layerIndex = layerIndex; if (parent != null) { parent.addLayer(viewportLayer); } return viewportLayer; } |
1 2 3 4 5 | // apply planet mask, so that the planet is guaranteed covered by the glow planetLayer.cacheAsBitmap = planetMaskLayer.cacheAsBitmap = true; cloudLayer.cacheAsBitmap = cloudMaskLayer.cacheAsBitmap = true; planetLayer.mask = planetMaskLayer; cloudLayer.mask = cloudMaskLayer; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | /** * Defines a masking disc the same size as the fresnell glow 'around' Earth. */ private function createPlanetMaskMaterial(size:Number):MaterialObject3D { var planetMaskTexture:Sprite = PV3DUtil.createGradientSprite(size, [0, 0], [1, 0], [0xFA, 0xFF]); return new MovieMaterial(planetMaskTexture, true); } /** * (Utility function in a separate utility class, PV3DUtil) * Performs a basic gradient fill and returns it on a new sprite of the specified size. Used to avoid boilerplate code. */ public static function PV3DUtil.createGradientSprite(size:Number, colors:Array, alphas:Array, ratios:Array):Sprite { var mat:Matrix = new Matrix(); mat.createGradientBox(size, size); var sprite:Sprite = new Sprite(); sprite.graphics.beginGradientFill(GradientType.RADIAL, colors, alphas, ratios, mat); sprite.graphics.drawRect(0, 0, size, size); sprite.graphics.endFill(); return sprite; } |
So this mask is applied to the entire planet, both land/cloud spheres. The planet’s glow goes on top of all this which has the exact same size as the mask so that it fits nicely.
1 2 3 4 5 6 7 8 9 10 11 | /** * Rotates Earth and clouds separately. Makes sure the glow- and mask planes are pointed towards the camera */ public override function update(camera:Camera3D):void { planet.yaw(planetRotationSpeed); clouds.yaw(cloudsRotationSpeed); glow.lookAt(camera); planetMask.copyTransform(glow); cloudMask.copyTransform(glow); super.update(camera); } |
One note is in order perhaps: I’ve mentioned Planet extends RealtimeDisplayObject3D. I haven’t used this to use realtime rotation for the planets, which I could’ve done as well. Instead, rotation is done based on frame events while Earth updates the clouds animation (not rotation) based on time events.
Earth code
1 2 3 4 5 6 | [Embed (source="../../../../assets/earth.jpg")] private var BitmapEarth:Class; [Embed (source="../../../../assets/clouds2.png")] private var BitmapClouds:Class; [Embed (source="../../../../assets/earth_heightmap.jpg")] private var BitmapHeightmap:Class; |
1 | private var realtimeCloudsTexture:RealtimeCloudsTexture; |
1 2 3 | public function Earth(viewport:Viewport3D, diameter:Number, sunLight:PointLight3D = null) { super(viewport, diameter, sunLight, Planet.QUALITY_MEDIUM, .3, .15, 100, 5); } |
1 2 3 4 5 6 7 8 9 | /** * Defines the geological texture as a phong shaded material. */ protected override function createPlanetMaterial():MaterialObject3D { var earthBitmap:BitmapData = new BitmapEarth().bitmapData; var heightmapBitmap:BitmapData = new BitmapHeightmap().bitmapData; var earthShader:PhongShader = new PhongShader(sunLight, 0xFFFFFF, 0x111111, 20, heightmapBitmap, null); return new ShadedMaterial(new BitmapMaterial(earthBitmap), earthShader); } |
1 2 3 4 5 6 7 8 9 10 | /** * Defines the clouds texture as a phong shaded material. The clouds texture * contains a runtime-maintained alpha channel so you can look through the animated clouds. */ protected override function createCloudMaterial():MaterialObject3D { realtimeCloudsTexture = new RealtimeCloudsTexture(new BitmapClouds().bitmapData); var realtimeCloudsMaterial:MovieMaterial = new MovieMaterial(realtimeCloudsTexture, true, true); var cloudShader:PhongShader = new PhongShader(sunLight, 0xFFFFFF, 0x000000, 20, null, null); return new ShadedMaterial(realtimeCloudsMaterial, cloudShader); } |
Cloud code
As stated in the blog post, the clouds have an alpha channel which is updated in runtime with a perlin noise map.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class RealtimeCloudsTexture extends Sprite { // original clouds texture to perform realtime visiblity mapping on private var cloudsBitmap:BitmapData; private var phase1:Number = 0; // used for octave 1 in perlin noise map private var phase2:Number = 0; // used for octave 2 in perlin noise map private var phase3:Number = 0; // used for octave 3 in perlin noise map public function RealtimeCloudsTexture(cloudsBitmap:BitmapData) { this.cloudsBitmap = cloudsBitmap; // for some reason we need to prepare sprite size in the constructor, so let's just do that PV3DUtil.drawBitmapToSprite(new BitmapData(cloudsBitmap.width, cloudsBitmap.height), this); } } |
So the phases represent the progression of the clouds in time and map directly to the octaves properties in the perlin noise map we’re using. The cloudsBitmap passed in is never modified. Instead we treat it like a blueprint, copying it each frame and applying the alpha channel modifications. This was necessary to avoid stacked alpha channel modifications.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | /** * Updates the clouds phase (visibility map) using perlin noise. * * 1). Perlin Noise is used for alpha channel assigned to the clouds texture * 2). Since we're drawing perlin noise in runtime, let's just keep the perlin size very small * and just resize the noise to the target bitmap. */ public function update(stepsize:Number):void { // produce phased perlin noise alpha map var point1:Point = new Point(phase1 -= stepsize * 2); var point2:Point = new Point(phase2 += stepsize); var point3:Point = new Point(phase3 -= stepsize); var cloudsPhase:BitmapData = new BitmapData(120, 120); // pick any channel as long the same channel is used to convert to alpha channel when combing it all cloudsPhase.perlinNoise(20, 20, 2, 754, true, true, BitmapDataChannel.RED, false, [point1, point2]); // resize alpha map to fit the cloudstexture var cloudsPhaseResized:BitmapData = new BitmapData(cloudsBitmap.width, cloudsBitmap.height); var m:Matrix = new Matrix(); m.scale(cloudsBitmap.width / cloudsPhase.width, cloudsBitmap.height / cloudsPhase.height); // draw resized visibility map and slightly increase contrast to completely hide/show clouds cloudsPhaseResized.draw(cloudsPhase, m, new ColorTransform(1, 1, 1, 1, -25, -25, -25)); // combine everything on a new bitmap var cloudsCombined:BitmapData = new BitmapData(cloudsBitmap.width, cloudsBitmap.height); cloudsCombined.copyPixels(cloudsBitmap, cloudsBitmap.rect, new Point(0, 0)); cloudsCombined.copyChannel(cloudsPhaseResized, cloudsBitmap.rect, new Point(0, 0), BitmapDataChannel.RED, BitmapDataChannel.ALPHA); graphics.clear(); PV3DUtil.drawBitmapToSprite(cloudsCombined, this); } |
- Create a low quality progressed perlin noise map
- Resize the perlin noise map to the size of the clouds texture
- Apply the resized noise map to a copy of the clouds texture blueprint
- Draw the entire combination on itself as the new clouds sprite used in the moviematerial