diff --git a/Source/Plugins/BuilderModes/General/BuilderPlug.cs b/Source/Plugins/BuilderModes/General/BuilderPlug.cs index c67237e..f8e81a7 100644 --- a/Source/Plugins/BuilderModes/General/BuilderPlug.cs +++ b/Source/Plugins/BuilderModes/General/BuilderPlug.cs @@ -889,20 +889,28 @@ namespace CodeImp.DoomBuilder.BuilderModes return; } - ImageExporter exporter = new ImageExporter(); - ImageExportSettingsForm form = new ImageExportSettingsForm(); if (form.ShowDialog() == DialogResult.OK) { - ImageExportSettings settings = new ImageExportSettings(Path.GetFileName(form.FilePath), Path.GetDirectoryName(form.FilePath), form.Floor, form.GetPixelFormat(), form.GetImageFormat()); + ImageExportSettings settings = new ImageExportSettings(Path.GetDirectoryName(form.FilePath), Path.GetFileNameWithoutExtension(form.FilePath), Path.GetExtension(form.FilePath), form.Floor, form.Fullbright, form.Brightmap, form.Tiles, form.GetPixelFormat(), form.GetImageFormat()); + ImageExporter exporter = new ImageExporter(sectors, settings); - try + string text = "The following images will be created:\n\n" + string.Join("\n", exporter.GetImageNames().ToArray()); + + DialogResult result = MessageBox.Show(text, "Export to image", MessageBoxButtons.OKCancel); + + if (result == DialogResult.OK) { - exporter.Export(sectors, settings); - } - catch(ArgumentException e) // Happens if there's not enough consecutive memory so create the file - { - MessageBox.Show("Exporting failed. There's likely not enough consecutive free memory to create the image. Try a lower color depth or file format", "Export failed", MessageBoxButtons.OK, MessageBoxIcon.Error); + try + { + exporter.Export(); + + MessageBox.Show("Export successful.", "Export to image", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + catch (ArgumentException e) // Happens if there's not enough consecutive memory to create the file + { + MessageBox.Show("Exporting failed. There's likely not enough consecutive free memory to create the image. Try a lower color depth or file format", "Export failed", MessageBoxButtons.OK, MessageBoxIcon.Error); + } } } } diff --git a/Source/Plugins/BuilderModes/IO/ImageExporter.cs b/Source/Plugins/BuilderModes/IO/ImageExporter.cs index ebdfe5f..a2c1ecc 100644 --- a/Source/Plugins/BuilderModes/IO/ImageExporter.cs +++ b/Source/Plugins/BuilderModes/IO/ImageExporter.cs @@ -35,36 +35,259 @@ using System.Diagnostics; namespace CodeImp.DoomBuilder.BuilderModes.IO { + #region ================== Structs + internal struct ImageExportSettings { - public string Name; public string Path; + public string Name; + public string Extension; public bool Floor; + public bool Fullbright; + public bool Brightmap; + public bool Tiles; public PixelFormat PixelFormat; public ImageFormat ImageFormat; - public ImageExportSettings(string name, string path, bool floor, PixelFormat pformat, ImageFormat iformat) + public ImageExportSettings(string path, string name, string extension, bool floor, bool fullbright, bool brightmap, bool tiles, PixelFormat pformat, ImageFormat iformat) { - Name = name; Path = path; + Name = name; + Extension = extension; Floor = floor; + Brightmap = brightmap; + Tiles = tiles; + Fullbright = fullbright; PixelFormat = pformat; ImageFormat = iformat; } } + #endregion + internal class ImageExporter { - public void Export(ICollection sectors, ImageExportSettings settings) - { - Bitmap bitmap; - Vector2D offset = new Vector2D(float.MaxValue, float.MinValue); - Vector2D size = new Vector2D(float.MinValue, float.MaxValue); + #region ================== Variables - HashSet vertices = new HashSet(); + private ICollection sectors; + private ImageExportSettings settings; + + #endregion + + #region ================== Constants + + private const int TILE_SIZE = 64; + + #endregion + + #region ================== Constructors + + public ImageExporter(ICollection sectors, ImageExportSettings settings) + { + this.sectors = sectors; + this.settings = settings; + } + + #endregion + + #region ================== Methods + + /// + /// Exports the sectors to images + /// + public void Export() + { + Bitmap texturebitmap = null; + Bitmap brightmapbitmap = null; + Graphics gbrightmap = null; + Graphics gtexture = null; + Vector2D offset; + Vector2D size; + + GetSizeAndOffset(out size, out offset); + + // Normal texture + texturebitmap = new Bitmap((int)size.x, (int)size.y, settings.PixelFormat); + gtexture = Graphics.FromImage(texturebitmap); + gtexture.Clear(Color.Black); // If we don't clear to black we'll see seams where the sectors touch, due to the AA + gtexture.InterpolationMode = InterpolationMode.HighQualityBilinear; + gtexture.CompositingQuality = CompositingQuality.HighQuality; + gtexture.PixelOffsetMode = PixelOffsetMode.HighQuality; + gtexture.SmoothingMode = SmoothingMode.AntiAlias; // Without AA the sector edges will be quite rough + + // Brightmap + if (settings.Brightmap) + { + brightmapbitmap = new Bitmap((int)size.x, (int)size.y, settings.PixelFormat); + gbrightmap = Graphics.FromImage(brightmapbitmap); + gbrightmap.Clear(Color.Black); // If we don't clear to black we'll see seams where the sectors touch, due to the AA + gbrightmap.InterpolationMode = InterpolationMode.HighQualityBilinear; + gbrightmap.CompositingQuality = CompositingQuality.HighQuality; + gbrightmap.PixelOffsetMode = PixelOffsetMode.HighQuality; + gbrightmap.SmoothingMode = SmoothingMode.AntiAlias; // Without AA the sector edges will be quite rough + } + + foreach (Sector s in sectors) + { + GraphicsPath p = new GraphicsPath(); + float rotation = (float)s.Fields.GetValue("rotationfloor", 0.0); + + // If a sector is rotated any offset is on the rotated axes. But we need to offset by + // map coordinates. We'll use this vector to compute that offset + Vector2D rotationvector = Vector2D.FromAngle(Angle2D.DegToRad(rotation) + Angle2D.PIHALF); + + // Sectors are triangulated, so draw every triangle + for (int i = 0; i < s.Triangles.Vertices.Count / 3; i++) + { + // The GDI image has the 0/0 coordinate in the top left, so invert the y component + Vector2D v1 = s.Triangles.Vertices[i * 3] - offset; v1.y *= -1.0f; + Vector2D v2 = s.Triangles.Vertices[i * 3 + 1] - offset; v2.y *= -1.0f; + Vector2D v3 = s.Triangles.Vertices[i * 3 + 2] - offset; v3.y *= -1.0f; + + p.AddLine((float)v1.x, (float)v1.y, (float)v2.x, (float)v2.y); + p.AddLine((float)v2.x, (float)v2.y, (float)v3.x, (float)v3.y); + p.CloseFigure(); + } + + Bitmap brushtexture; + + if (settings.Floor) + brushtexture = General.Map.Data.GetFlatImage(s.FloorTexture).GetBitmap(); + else + brushtexture = General.Map.Data.GetFlatImage(s.CeilTexture).GetBitmap(); + + if (!settings.Fullbright) + brushtexture = AdjustBrightness(brushtexture, s.Brightness > 0 ? s.Brightness / 255.0f : 0.0f); + + Vector2D textureoffset = new Vector2D(); + textureoffset.x = s.Fields.GetValue("xpanningfloor", 0.0f); + textureoffset.y = s.Fields.GetValue("ypanningfloor", 0.0f); + + // Create the transformation matrix + Matrix matrix = new Matrix(); + matrix.Rotate(rotation); + matrix.Translate((float)(-offset.x * rotationvector.x), (float)(offset.x * rotationvector.y)); // Left/right offset from the map origin + matrix.Translate((float)(offset.y * rotationvector.y), (float)(offset.y * rotationvector.x)); // Up/down offset from the map origin + matrix.Translate(-(float)textureoffset.x, -(float)textureoffset.y); // Texture offset + + // Create the texture brush and apply the matrix + TextureBrush tbrush = new TextureBrush(brushtexture); + tbrush.Transform = matrix; + + // Draw the islands of the sector + gtexture.FillPath(tbrush, p); + + // Create the brightmap based on the sector brightness + if (settings.Brightmap) + { + SolidBrush sbrush = new SolidBrush(Color.FromArgb(255, s.Brightness, s.Brightness, s.Brightness)); + gbrightmap.FillPath(sbrush, p); + } + } + + // Finally save the image(s) + if (settings.Tiles) + { + SaveImageAsTiles(texturebitmap); + + if (settings.Brightmap) + SaveImageAsTiles(brightmapbitmap, "_brightmap"); + } + else + { + texturebitmap.Save(Path.Combine(settings.Path, settings.Name) + settings.Extension, settings.ImageFormat); + + if (settings.Brightmap) + brightmapbitmap.Save(Path.Combine(settings.Path, settings.Name) + "_brightmap" + settings.Extension, settings.ImageFormat); + } + } + + /// + /// Saves the image in several uniform sized tiles. It will add numbers starting from 1, going from top left to bottom right, to the filename + /// + /// the image to split into tiles + /// additional suffix for filenames + private void SaveImageAsTiles(Bitmap image, string suffix="") + { + int xnum = (int)Math.Ceiling(image.Size.Width / (double)TILE_SIZE); + int ynum = (int)Math.Ceiling(image.Size.Height / (double)TILE_SIZE); + int imagenum = 1; + + for (int y = 0; y < ynum; y++) + { + for (int x = 0; x < xnum; x++) + { + int width = TILE_SIZE; + int height = TILE_SIZE; + + // If the width and height are not divisible without remainder make sure only part of the source image is copied + if (x * TILE_SIZE + TILE_SIZE > image.Size.Width) + width = image.Size.Width - (x * TILE_SIZE); + + if(y * TILE_SIZE + TILE_SIZE > image.Size.Height) + height = image.Size.Height - (y * TILE_SIZE); + + + Bitmap bitmap = new Bitmap(TILE_SIZE, TILE_SIZE); + Graphics g = Graphics.FromImage(bitmap); + g.Clear(Color.Black); + g.DrawImage(image, new Rectangle(0, 0, width, height), new Rectangle(x*TILE_SIZE, y*TILE_SIZE, width, height), GraphicsUnit.Pixel); + + bitmap.Save(string.Format("{0}{1}{2}{3}", Path.Combine(settings.Path, settings.Name), suffix, imagenum, settings.Extension)); + + imagenum++; + } + } + } + + /// + /// Generates a list of images file names that will be creates + /// + /// List of image file names + public List GetImageNames() + { + List imagenames = new List(); + Vector2D offset; + Vector2D size; + + GetSizeAndOffset(out size, out offset); + + if(settings.Tiles) + { + int x = (int)Math.Ceiling(size.x / TILE_SIZE); + int y = (int)Math.Ceiling(size.y / TILE_SIZE); + + for (int i = 1; i <= x * y; i++) + imagenames.Add(string.Format("{0}{1}{2}", Path.Combine(settings.Path, settings.Name), i, settings.Extension)); + + if(settings.Brightmap) + for (int i = 1; i <= x * y; i++) + imagenames.Add(string.Format("{0}{1}_brightmap{2}", Path.Combine(settings.Path, settings.Name), i, settings.Extension)); + } + else + { + imagenames.Add(string.Format("{0}{1}", Path.Combine(settings.Path, settings.Name), settings.Extension)); + + if(settings.Brightmap) + imagenames.Add(string.Format("{0}_brightmap{1}", Path.Combine(settings.Path, settings.Name), settings.Extension)); + } + + return imagenames; + + } + + /// + /// Gets the size of the image, based on the sectors, and the offset from the map origin + /// + /// stores the size of the size of the image + /// stores the offset from the map origin + private void GetSizeAndOffset(out Vector2D size, out Vector2D offset) + { + offset = new Vector2D(float.MaxValue, float.MinValue); + size = new Vector2D(float.MinValue, float.MaxValue); // Find the top left and bottom right corners of the selection - foreach(Sector s in sectors) + foreach (Sector s in sectors) { foreach (Sidedef sd in s.Sidedefs) { @@ -89,67 +312,47 @@ namespace CodeImp.DoomBuilder.BuilderModes.IO // (top left corner of the selection). y will always be negative, so make it positive size -= offset; size.y *= -1.0f; + } - bitmap = new Bitmap((int)size.x, (int)size.y, settings.PixelFormat); + /// + /// Adjusts the brightness of an image. Code by Rod Stephens http://csharphelper.com/blog/2014/10/use-an-imageattributes-object-to-adjust-an-images-brightness-in-c/ + /// + /// Base image + /// Brightness between 0.0f and 1.0f + /// The new image with changed brightness + private Bitmap AdjustBrightness(Image image, float brightness) + { + // Make the ColorMatrix. + float b = brightness; + ColorMatrix cm = new ColorMatrix(new float[][] { + new float[] {b, 0, 0, 0, 0}, + new float[] {0, b, 0, 0, 0}, + new float[] {0, 0, b, 0, 0}, + new float[] {0, 0, 0, 1, 0}, + new float[] {0, 0, 0, 0, 1}, + }); + ImageAttributes attributes = new ImageAttributes(); + attributes.SetColorMatrix(cm); - Graphics g = Graphics.FromImage(bitmap); - g.Clear(Color.Black); // If we don't clear to black we'll see seams where the sectors touch, due to the AA - g.InterpolationMode = InterpolationMode.HighQualityBilinear; - g.CompositingQuality = CompositingQuality.HighQuality; - g.PixelOffsetMode = PixelOffsetMode.HighQuality; - g.SmoothingMode = SmoothingMode.AntiAlias; // Without AA the sector edges will be quite rough + // Draw the image onto the new bitmap while applying the new ColorMatrix. + Point[] points = { + new Point(0, 0), + new Point(image.Width, 0), + new Point(0, image.Height), + }; + Rectangle rect = new Rectangle(0, 0, image.Width, image.Height); - foreach (Sector s in sectors) + // Make the result bitmap. + Bitmap bm = new Bitmap(image.Width, image.Height); + using (Graphics gr = Graphics.FromImage(bm)) { - GraphicsPath p = new GraphicsPath(); - float rotation = (float)s.Fields.GetValue("rotationfloor", 0.0); - - // If a sector is rotated any offset is on the rotated axes. But we need to offset by - // map coordinates. We'll use this vector to compute that offset - Vector2D rotationvector = Vector2D.FromAngle(Angle2D.DegToRad(rotation) + Angle2D.PIHALF); - - // Sectors are triangulated, so draw every triangle - for (int i = 0; i < s.Triangles.Vertices.Count / 3; i++) - { - // The GDI image has the 0/0 coordinate in the top left, so invert the y component - Vector2D v1 = s.Triangles.Vertices[i * 3] - offset; v1.y *= -1.0f; - Vector2D v2 = s.Triangles.Vertices[i * 3 + 1] - offset; v2.y *= -1.0f; - Vector2D v3 = s.Triangles.Vertices[i * 3 + 2] - offset; v3.y *= -1.0f; - - p.AddLine((float)v1.x, (float)v1.y, (float)v2.x, (float)v2.y); - p.AddLine((float)v2.x, (float)v2.y, (float)v3.x, (float)v3.y); - p.CloseFigure(); - } - - Bitmap texture; - - if(settings.Floor) - texture = General.Map.Data.GetFlatImage(s.FloorTexture).GetBitmap(); - else - texture = General.Map.Data.GetFlatImage(s.CeilTexture).GetBitmap(); - - Vector2D textureoffset = new Vector2D(); - textureoffset.x = s.Fields.GetValue("xpanningfloor", 0.0f); - textureoffset.y = s.Fields.GetValue("ypanningfloor", 0.0f); - - // Create the transformation matrix - Matrix matrix = new Matrix(); - matrix.Rotate(rotation); - matrix.Translate((float)(-offset.x * rotationvector.x), (float)(offset.x * rotationvector.y)); // Left/right offset from the map origin - matrix.Translate((float)(offset.y * rotationvector.y), (float)(offset.y * rotationvector.x)); // Up/down offset from the map origin - matrix.Translate(-(float)textureoffset.x, -(float)textureoffset.y); // Texture offset - - // Create the texture brush and apply the matrix - TextureBrush t = new TextureBrush(texture); - t.Transform = matrix; - - // Draw the islands of the sector - g.FillPath(t, p); - + gr.DrawImage(image, points, rect, GraphicsUnit.Pixel, attributes); } - // Finally save the image - bitmap.Save(Path.Combine(settings.Path, settings.Name), settings.ImageFormat); + // Return the result. + return bm; } + + #endregion } } diff --git a/Source/Plugins/BuilderModes/Interface/ImageExportSettingsForm.Designer.cs b/Source/Plugins/BuilderModes/Interface/ImageExportSettingsForm.Designer.cs index fb9165d..88093ca 100644 --- a/Source/Plugins/BuilderModes/Interface/ImageExportSettingsForm.Designer.cs +++ b/Source/Plugins/BuilderModes/Interface/ImageExportSettingsForm.Designer.cs @@ -40,6 +40,9 @@ this.label3 = new System.Windows.Forms.Label(); this.rbFloor = new System.Windows.Forms.RadioButton(); this.rbCeiling = new System.Windows.Forms.RadioButton(); + this.cbFullbright = new System.Windows.Forms.CheckBox(); + this.cbBrightmap = new System.Windows.Forms.CheckBox(); + this.cbTiles = new System.Windows.Forms.CheckBox(); this.SuspendLayout(); // // tbExportPath @@ -71,7 +74,7 @@ // cancel // this.cancel.DialogResult = System.Windows.Forms.DialogResult.Cancel; - this.cancel.Location = new System.Drawing.Point(360, 110); + this.cancel.Location = new System.Drawing.Point(360, 153); this.cancel.Name = "cancel"; this.cancel.Size = new System.Drawing.Size(75, 23); this.cancel.TabIndex = 7; @@ -81,7 +84,7 @@ // // export // - this.export.Location = new System.Drawing.Point(279, 110); + this.export.Location = new System.Drawing.Point(279, 153); this.export.Name = "export"; this.export.Size = new System.Drawing.Size(75, 23); this.export.TabIndex = 6; @@ -141,7 +144,7 @@ // this.rbFloor.AutoSize = true; this.rbFloor.Checked = true; - this.rbFloor.Location = new System.Drawing.Point(227, 38); + this.rbFloor.Location = new System.Drawing.Point(197, 39); this.rbFloor.Name = "rbFloor"; this.rbFloor.Size = new System.Drawing.Size(48, 17); this.rbFloor.TabIndex = 12; @@ -152,20 +155,55 @@ // rbCeiling // this.rbCeiling.AutoSize = true; - this.rbCeiling.Location = new System.Drawing.Point(227, 60); + this.rbCeiling.Location = new System.Drawing.Point(197, 61); this.rbCeiling.Name = "rbCeiling"; this.rbCeiling.Size = new System.Drawing.Size(56, 17); this.rbCeiling.TabIndex = 13; this.rbCeiling.Text = "Ceiling"; this.rbCeiling.UseVisualStyleBackColor = true; // + // cbFullbright + // + this.cbFullbright.AutoSize = true; + this.cbFullbright.Checked = true; + this.cbFullbright.CheckState = System.Windows.Forms.CheckState.Checked; + this.cbFullbright.Location = new System.Drawing.Point(279, 40); + this.cbFullbright.Name = "cbFullbright"; + this.cbFullbright.Size = new System.Drawing.Size(87, 17); + this.cbFullbright.TabIndex = 14; + this.cbFullbright.Text = "Use fullbright"; + this.cbFullbright.UseVisualStyleBackColor = true; + // + // cbBrightmap + // + this.cbBrightmap.AutoSize = true; + this.cbBrightmap.Location = new System.Drawing.Point(279, 64); + this.cbBrightmap.Name = "cbBrightmap"; + this.cbBrightmap.Size = new System.Drawing.Size(106, 17); + this.cbBrightmap.TabIndex = 15; + this.cbBrightmap.Text = "Create brightmap"; + this.cbBrightmap.UseVisualStyleBackColor = true; + // + // cbTiles + // + this.cbTiles.AutoSize = true; + this.cbTiles.Location = new System.Drawing.Point(279, 88); + this.cbTiles.Name = "cbTiles"; + this.cbTiles.Size = new System.Drawing.Size(110, 17); + this.cbTiles.TabIndex = 16; + this.cbTiles.Text = "Create 64x64 tiles"; + this.cbTiles.UseVisualStyleBackColor = true; + // // ImageExportSettingsForm // this.AcceptButton = this.export; this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.CancelButton = this.cancel; - this.ClientSize = new System.Drawing.Size(447, 145); + this.ClientSize = new System.Drawing.Size(447, 188); + this.Controls.Add(this.cbTiles); + this.Controls.Add(this.cbBrightmap); + this.Controls.Add(this.cbFullbright); this.Controls.Add(this.rbCeiling); this.Controls.Add(this.rbFloor); this.Controls.Add(this.label3); @@ -201,5 +239,8 @@ private System.Windows.Forms.Label label3; private System.Windows.Forms.RadioButton rbFloor; private System.Windows.Forms.RadioButton rbCeiling; + private System.Windows.Forms.CheckBox cbFullbright; + private System.Windows.Forms.CheckBox cbBrightmap; + private System.Windows.Forms.CheckBox cbTiles; } } \ No newline at end of file diff --git a/Source/Plugins/BuilderModes/Interface/ImageExportSettingsForm.cs b/Source/Plugins/BuilderModes/Interface/ImageExportSettingsForm.cs index 705ad2d..eee7653 100644 --- a/Source/Plugins/BuilderModes/Interface/ImageExportSettingsForm.cs +++ b/Source/Plugins/BuilderModes/Interface/ImageExportSettingsForm.cs @@ -40,6 +40,9 @@ namespace CodeImp.DoomBuilder.BuilderModes.Interface public string FilePath { get { return tbExportPath.Text.Trim(); } } public bool Floor { get { return rbFloor.Checked; } } + public bool Fullbright { get { return cbFullbright.Checked; } } + public bool Brightmap { get { return cbBrightmap.Checked; } } + public bool Tiles { get { return cbTiles.Checked; } } #endregion @@ -65,6 +68,10 @@ namespace CodeImp.DoomBuilder.BuilderModes.Interface saveFileDialog.FileName = Path.GetDirectoryName(General.Map.FilePathName) + Path.DirectorySeparatorChar + name + ".png"; tbExportPath.Text = saveFileDialog.FileName; } + + cbFullbright.Checked = General.Settings.ReadPluginSetting("imageexportfullbright", true); + cbBrightmap.Checked = General.Settings.ReadPluginSetting("imageexportbrightmap", false); + cbTiles.Checked = General.Settings.ReadPluginSetting("imageexporttiles", false); } #endregion @@ -125,6 +132,10 @@ namespace CodeImp.DoomBuilder.BuilderModes.Interface private void export_Click(object sender, EventArgs e) { + General.Settings.WritePluginSetting("imageexportfullbright", cbFullbright.Checked); + General.Settings.WritePluginSetting("imageexportbrightmap", cbBrightmap.Checked); + General.Settings.WritePluginSetting("imageexporttiles", cbTiles.Checked); + this.DialogResult = DialogResult.OK; this.Close(); }