public DirectBitmap RenderTileFrom(DataReader.Region region, DirectBitmap bitmap)
        {
            var watch = System.Diagnostics.Stopwatch.StartNew();

            // Draw the terrain.
            LOG.Debug($"Rendering Maptile Terrain (Textured) for region {region.Id} named '{region.Name}'");
            watch.Restart();
            var terrain = TerrainToBitmap(region, bitmap);

            watch.Stop();
            LOG.Info($"Completed terrain for region {region.Id} named '{region.Name}' in {watch.ElapsedMilliseconds}ms");

            // Get the prims
            LOG.Debug($"Getting prims for region {region.Id} named '{region.Name}'");
            watch.Restart();
            var prims = region.GetPrims();

            watch.Stop();
            LOG.Debug($"Completed getting {prims?.Count()} prims for region {region.Id} named '{region.Name}' in {watch.ElapsedMilliseconds}ms");

            if (prims != null)
            {
                // Draw the prims.
                LOG.Debug($"Rendering OBB prims for region {region.Id} named '{region.Name}'");
                watch.Restart();
                DrawObjects(prims, terrain, bitmap);
                watch.Stop();
                LOG.Debug($"Completed OBB prims for region {region.Id} named '{region.Name}' in {watch.ElapsedMilliseconds}ms");
            }
            else
            {
                LOG.Debug($"Unable to render OBB prims for region {region.Id} named '{region.Name}': there was a problem getting the prims from the DB.");
            }

            return(bitmap);
        }
        private Terrain TerrainToBitmap(DataReader.Region region, DirectBitmap mapbmp)
        {
            var terrain = region.GetTerrain();

            var textures = new Texture[4];

            try {
                textures[0] = Texture.GetByUUID(terrain.TerrainTexture1, Texture.TERRAIN_TEXTURE_1_COLOR);
            }
            catch (InvalidOperationException e) {
                LOG.Warn($"Error decoding image asset {terrain.TerrainTexture1} for terrain texture 1 in region {region.Id}, continuing using default texture color.", e);

                textures[0] = new Texture(color: Texture.TERRAIN_TEXTURE_1_COLOR);
            }

            try {
                textures[1] = Texture.GetByUUID(terrain.TerrainTexture2, Texture.TERRAIN_TEXTURE_2_COLOR);
            }
            catch (InvalidOperationException e) {
                LOG.Warn($"Error decoding image asset {terrain.TerrainTexture2} for terrain texture 2 in region {region.Id}, continuing using default texture color.", e);

                textures[1] = new Texture(color: Texture.TERRAIN_TEXTURE_2_COLOR);
            }

            try {
                textures[2] = Texture.GetByUUID(terrain.TerrainTexture3, Texture.TERRAIN_TEXTURE_3_COLOR);
            }
            catch (InvalidOperationException e) {
                LOG.Warn($"Error decoding image asset {terrain.TerrainTexture3} for terrain texture 3 in region {region.Id}, continuing using default texture color.", e);

                textures[2] = new Texture(color: Texture.TERRAIN_TEXTURE_3_COLOR);
            }

            try {
                textures[3] = Texture.GetByUUID(terrain.TerrainTexture4, Texture.TERRAIN_TEXTURE_4_COLOR);
            }
            catch (InvalidOperationException e) {
                LOG.Warn($"Error decoding image asset {terrain.TerrainTexture4} for terrain texture 4 in region {region.Id}, continuing using default texture color.", e);

                textures[3] = new Texture(color: Texture.TERRAIN_TEXTURE_4_COLOR);
            }

            // the four terrain colors as HSVs for interpolation
            var hsv1 = new HSV(textures[0].AverageColor);
            var hsv2 = new HSV(textures[1].AverageColor);
            var hsv3 = new HSV(textures[2].AverageColor);
            var hsv4 = new HSV(textures[3].AverageColor);

            for (int x = 0; x < mapbmp.Width; x++)
            {
                var columnRatio = (double)x / (mapbmp.Width - 1);                 // 0 - 1, for interpolation

                for (int y = 0; y < mapbmp.Height; y++)
                {
                    var rowRatio = (double)y / (mapbmp.Height - 1);                     // 0 - 1, for interpolation

                    // Y flip the cordinates for the bitmap: hf origin is lower left, bm origin is upper left
                    var yr = (mapbmp.Height - 1) - y;

                    var heightvalue = mapbmp.Width == 256 ?
                                      terrain.GetBlendedHeight(x, y) :
                                      terrain.GetBlendedHeight(255 * columnRatio, 255 * rowRatio)
                    ;

                    if (double.IsInfinity(heightvalue) || double.IsNaN(heightvalue))
                    {
                        heightvalue = 0d;
                    }

                    var tileScalarX = 256f / mapbmp.Width;                     // Used to hack back in those constants that were hand-tuned to 256px tiles.
                    var tileScalarY = 256f / mapbmp.Height;                    // Used to hack back in those constants that were hand-tuned to 256px tiles.

                    // add a bit noise for breaking up those flat colors:
                    // - a large-scale noise, for the "patches" (using an doubled s-curve for sharper contrast)
                    // - a small-scale noise, for bringing in some small scale variation
                    //float bigNoise = (float)TerrainUtil.InterpolatedNoise(x / 8.0, y / 8.0) * .5f + .5f; // map to 0.0 - 1.0
                    //float smallNoise = (float)TerrainUtil.InterpolatedNoise(x + 33, y + 43) * .5f + .5f;
                    //float hmod = heightvalue + smallNoise * 3f + S(S(bigNoise)) * 10f;
                    var hmod =
                        heightvalue +
                        TerrainUtil.InterpolatedNoise(tileScalarX * (x + 33 + (int)region.Location?.X * mapbmp.Width), tileScalarY * (y + 43 + (int)region.Location?.Y * mapbmp.Height)) * 1.5f + 1.5f +                                                  // 0 - 3
                        MathUtilities.SCurve(MathUtilities.SCurve(TerrainUtil.InterpolatedNoise(tileScalarX * (x + (int)region.Location?.X * mapbmp.Width) / 8.0, tileScalarY * (y + (int)region.Location?.Y * mapbmp.Height) / 8.0) * .5f + .5f)) * 10f; // 0 - 10

                    // find the low/high values for this point (interpolated bilinearily)
                    // (and remember, x=0,y=0 is SW)
                    var low = terrain.ElevationSWLow * (1f - rowRatio) * (1f - columnRatio) +
                              terrain.ElevationSELow * (1f - rowRatio) * columnRatio +
                              terrain.ElevationNWLow * rowRatio * (1f - columnRatio) +
                              terrain.ElevationNELow * rowRatio * columnRatio
                    ;
                    var high = terrain.ElevationSWHigh * (1f - rowRatio) * (1f - columnRatio) +
                               terrain.ElevationSEHigh * (1f - rowRatio) * columnRatio +
                               terrain.ElevationNWHigh * rowRatio * (1f - columnRatio) +
                               terrain.ElevationNEHigh * rowRatio * columnRatio
                    ;
                    if (high < low)
                    {
                        // someone tried to fool us. High value should be higher than low every time
                        var tmp = high;
                        high = low;
                        low  = tmp;
                    }

                    Color result;
                    if (heightvalue > terrain.WaterHeight)
                    {
                        HSV hsv;
                        // Above water
                        if (hmod <= low)
                        {
                            hsv = hsv1;                             // too low
                        }
                        else if (hmod >= high)
                        {
                            hsv = hsv4;                             // too high
                        }
                        else
                        {
                            // HSV-interpolate along the colors
                            // first, rescale h to 0.0 - 1.0
                            hmod = (hmod - low) / (high - low);
                            // now we have to split: 0.00 => color1, 0.33 => color2, 0.67 => color3, 1.00 => color4
                            if (hmod < 1d / 3d)
                            {
                                hsv = hsv1.InterpolateHSV(ref hsv2, (float)(hmod * 3d));
                            }
                            else if (hmod < 2d / 3d)
                            {
                                hsv = hsv2.InterpolateHSV(ref hsv3, (float)((hmod * 3d) - 1d));
                            }
                            else
                            {
                                hsv = hsv3.InterpolateHSV(ref hsv4, (float)((hmod * 3d) - 2d));
                            }
                        }

                        result = hsv.ToColor();
                    }
                    else
                    {
                        // Under water.
                        var deepwater  = new HSV(_waterColor);
                        var beachwater = new HSV(_beachColor);

                        var water = deepwater.InterpolateHSV(ref beachwater, (float)MathUtilities.SCurve(heightvalue / terrain.WaterHeight));

                        if (_waterOverlay == null)
                        {
                            result = water.ToColor();
                        }
                        else
                        {
                            // Overlay the water image
                            var baseColor = water.ToColor();

                            var overlayColor = _waterOverlay[x, y];

                            var resultR = overlayColor.R + (baseColor.R * (255 - overlayColor.A) / 255);
                            var resultG = overlayColor.G + (baseColor.G * (255 - overlayColor.A) / 255);
                            var resultB = overlayColor.B + (baseColor.B * (255 - overlayColor.A) / 255);

                            result = Color.FromArgb(resultR, resultG, resultB);
                        }
                    }

                    // Shade the terrain for shadows
                    //if (x < (mapbmp.Width - 1) && y < (mapbmp.Height - 1)) {
                    //	var hfvaluecompare = getHeight(region.heightmapData, (x + 1) / (mapbmp.Width - 1), (y + 1) / (mapbmp.Height - 1)); // light from north-east => look at land height there
                    //	if (Double.IsInfinity(hfvaluecompare) || Double.IsNaN(hfvaluecompare))
                    //		hfvaluecompare = 0d;
                    //
                    //	var hfdiff = heightvalue - hfvaluecompare;  // => positive if NE is lower, negative if here is lower
                    //	hfdiff *= 0.06d; // some random factor so "it looks good"
                    //	if (hfdiff > 0.02d) {
                    //		var highlightfactor = 0.18d;
                    //		// NE is lower than here
                    //		// We have to desaturate and lighten the land at the same time
                    //		hsv.s = (hsv.s - (hfdiff * highlightfactor) > 0d) ? (float)(hsv.s - (hfdiff * highlightfactor)) : 0f;
                    //		hsv.v = (hsv.v + (hfdiff * highlightfactor) < 1d) ? (float)(hsv.v + (hfdiff * highlightfactor)) : 1f;
                    //	}
                    //	else if (hfdiff < -0.02f) {
                    //		var highlightfactor = 1.0d;
                    //		// here is lower than NE:
                    //		// We have to desaturate and blacken the land at the same time
                    //		hsv.s = (hsv.s + (hfdiff * highlightfactor) > 0f) ? (float)(hsv.s + (hfdiff * highlightfactor)) : 0f;
                    //		hsv.v = (hsv.v + (hfdiff * highlightfactor) > 0f) ? (float)(hsv.v + (hfdiff * highlightfactor)) : 0f;
                    //	}
                    //}

                    mapbmp.Bitmap.SetPixel(x, yr, result);
                }
            }

            return(terrain);
        }