public override void WriteColorMap(int height, string filePath)
        {
            var width = 2 * height;

            if (ColorNoiseMap == null)
            {
                GenerateColorMap(height);
            }
            var bitmap       = new DirectBitmap(width, height);
            var tempMean     = Random.Uniform("life temperature mean", FreezingPoint, BoilingPoint);
            var tempVariance = Random.Uniform("life temperature variance", 20, 70);
            var sinPower     = Random.Uniform("sin power", 0, 2);

            for (var x = 0; x < width; x++)
            {
                for (var y = 0; y < height; y++)
                {
                    var value = ColorNoiseMap.GetValue(x, y);
                    var col   = OceanColor;
                    if (!HasOcean || HeightNoiseMap.GetValue(x, y) > OceanLevel)
                    {
                        col = ColorScale.Color(value);
                    }
                    if (HasLife && HeightNoiseMap.GetValue(x, y) > OceanLevel)
                    {
                        var delta = Abs(TemperatureNoiseMap.GetValue(x, y) - tempMean) / tempVariance;
                        if (delta < 0)
                        {
                            delta = 0;
                        }
                        if (delta > 1)
                        {
                            delta = 1;
                        }
                        delta *= (float)Pow(Sin(PI * y / height), sinPower);
                        col    = Interpolate(col, LifeColorScale.Color(value), delta);
                    }

                    if (HasAtmosphere && TemperatureNoiseMap.GetValue(x, y) < FreezingPoint * IceFactor)
                    {
                        col = IceColor;
                    }
                    bitmap.SetPixel(x, y, col);
                }
            }
            bitmap.Save(filePath);
        }
        private void GenerateTemperatureMap(int height)
        {
            var width = 2 * height;

            if (HeightNoiseMap == null)
            {
                GenerateHeightMap(height);
            }
            var builder = new SphereNoiseMapBuilder();

            builder.SetDestSize(width, height);
            builder.SetBounds(-90, 90, -180, 180);
            TemperatureNoiseMap  = new NoiseMap();
            builder.SourceModule = TemperatureNoiseModule;
            builder.DestNoiseMap = TemperatureNoiseMap;
            builder.Build();

            MinTemperature = 1000000.0f;
            MaxTemperature = 0.0f;

            var solarHeat = Pow(SurfaceTemperature, 4);
            var extraHeat = Pow(GeothermalTemperature, 4) + Pow(GreenhouseTemp, 4);
            var coldness  = 1 - (float)Random.Uniform("coldness", Pow(extraHeat / (solarHeat + extraHeat), 0.25), 1);

            if (HasAtmosphere)
            {
                for (var x = 0; x < width; x++)
                {
                    for (var y = 0; y < height; y++)
                    {
                        var heat        = extraHeat + solarHeat * 2 * Sin(PI * y / height);
                        var temperature = (float)Pow(heat, 0.25);
                        temperature *= 1 - coldness * HeightNoiseMap.GetValue(x, y);
                        if (temperature < MinTemperature)
                        {
                            MinTemperature = temperature;
                        }
                        else if (temperature > MaxTemperature)
                        {
                            MaxTemperature = temperature;
                        }
                        temperature *= 1 + 0.05f * (TemperatureNoiseMap.GetValue(x, y) - 1);
                        TemperatureNoiseMap.SetValue(x, y, temperature);
                    }
                }
            }
        }
        /// <summary>Writes an equirectangular height map of the planet at the specified path.</summary>
        /// <param name="height">The height of the output image, half the width.</param>
        /// <param name="filePath">The path of the output file including file extension.</param>
        public void WriteHeightMap(int height, string filePath)
        {
            if (HeightNoiseMap == null)
            {
                GenerateHeightMap(height);
            }
            var bitmap = new DirectBitmap(HeightNoiseMap.Width, HeightNoiseMap.Height);

            for (var x = 0; x < HeightNoiseMap.Width; x++)
            {
                for (var y = 0; y < HeightNoiseMap.Height; y++)
                {
                    var col = Interpolate(Color.Black, Color.White, HeightNoiseMap.GetValue(x, y));
                    bitmap.SetPixel(x, y, col);
                }
            }

            bitmap.Save(filePath);
        }
        /// <summary>Writes an equirectangular specular map of the planet at the specified path.</summary>
        /// <param name="height">The height of the output image, half the width.</param>
        /// <param name="filePath">The path of the output file including file extension.</param>
        public void WriteSpecMap(int height, string filePath)
        {
            if (HasOcean)
            {
                var bitmap = new DirectBitmap(HeightNoiseMap.Width, HeightNoiseMap.Height);
                if (HasOcean)
                {
                    for (var x = 0; x < HeightNoiseMap.Width; x++)
                    {
                        for (var y = 0; y < HeightNoiseMap.Height; y++)
                        {
                            var col = Color.Black;
                            if (HeightNoiseMap.GetValue(x, y) <= OceanLevel && TemperatureNoiseMap.GetValue(x, y) > FreezingPoint)
                            {
                                col = Color.White;
                            }
                            bitmap.SetPixel(x, y, col);
                        }
                    }
                }

                bitmap.Save(filePath);
            }
        }
        /// <summary>Writes an equirectangular normal map of the planet at the specified path.</summary>
        /// <param name="height">The height of the output image, half the width.</param>
        /// <param name="filePath">The path of the output file including file extension.</param>
        /// <param name="altMode">Whether to use an alternate normal map format.</param>
        public void WriteNormalMap(int height, string filePath, bool altMode = false)
        {
            var width = 2 * height;

            if (HeightNoiseMap == null)
            {
                GenerateHeightMap(height);
            }
            var array  = new float[width, height];
            var bitmap = new DirectBitmap(width, height);

            for (var x = 0; x < width; x++)
            {
                for (var y = 0; y < height; y++)
                {
                    var value = HeightNoiseMap.GetValue(x, y);
                    if (value < OceanLevel)
                    {
                        value = 0;
                    }
                    else
                    {
                        value = (value - OceanLevel) / (1 - OceanLevel);
                    }
                    value      *= (float)Sin(PI * y / height);
                    array[x, y] = value;
                }
            }

            for (var x = 0; x < width; x++)
            {
                for (var y = 0; y < height; y++)
                {
                    var r = 127;
                    var g = 127;
                    var b = 255;
                    if (x > 0 && x < width - 1 && y > 0 && y < height - 1)
                    {
                        var vx = new Vector3(100f / height, 0, array[x + 1, y] - array[x - 1, y]);
                        var vy = new Vector3(0, 100f / height, array[x, y + 1] - array[x, y - 1]);
                        var c  = Vector3.Cross(vx, vy);
                        c /= c.Length();
                        r  = (int)(255 * (1 + c.X) / 2);
                        g  = (int)(255 * (1 - c.Y) / 2);
                        b  = (int)(255 * (1 + c.Z) / 2);
                    }

                    if (altMode)
                    {
                        var A = r;
                        var G = (int)(g * A / 255.0);
                        if (A < 0 || A > 255 || G < 0 || G > 255)
                        {
                            bitmap.SetPixel(x, y, Color.FromArgb(127, 255, 127, 255));
                        }
                        else
                        {
                            bitmap.SetPixel(x, y, Color.FromArgb(A, G, G, G));
                        }
                    }
                    else
                    {
                        if (r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255)
                        {
                            bitmap.SetPixel(x, y, Color.FromArgb(127, 127, 255));
                        }
                        else
                        {
                            bitmap.SetPixel(x, y, Color.FromArgb(r, g, b));
                        }
                    }
                }
            }

            bitmap.Save(filePath);
        }