2020-06-17 20:22:00 +00:00
#region = = = = = = = = = = = = = = = = = = Copyright ( c ) 2020 Boris Iwanski
/ *
* This program is free software : you can redistribute it and / or modify
*
* it under the terms of the GNU General Public License as published by
*
* the Free Software Foundation , either version 3 of the License , or
* ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
*
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
*
* GNU General Public License for more details .
*
* You should have received a copy of the GNU General Public License
* along with this program . If not , see < http : //www.gnu.org/licenses/>.
* /
#endregion
using System ;
using System.Collections.Generic ;
using System.Drawing ;
using System.Drawing.Imaging ;
using System.Drawing.Drawing2D ;
using System.IO ;
using System.Linq ;
using System.Text ;
using System.Threading.Tasks ;
2020-08-01 10:28:23 +00:00
using CodeImp.DoomBuilder.Data ;
2020-06-17 20:22:00 +00:00
using CodeImp.DoomBuilder.Geometry ;
using CodeImp.DoomBuilder.Map ;
using System.Diagnostics ;
namespace CodeImp.DoomBuilder.BuilderModes.IO
{
2020-06-24 20:10:17 +00:00
#region = = = = = = = = = = = = = = = = = = Structs
2020-06-17 20:22:00 +00:00
internal struct ImageExportSettings
{
public string Path ;
2020-06-24 20:10:17 +00:00
public string Name ;
public string Extension ;
2020-06-17 20:22:00 +00:00
public bool Floor ;
2020-06-24 20:10:17 +00:00
public bool Fullbright ;
public bool Brightmap ;
public bool Tiles ;
2020-06-17 20:22:00 +00:00
public PixelFormat PixelFormat ;
public ImageFormat ImageFormat ;
2020-06-24 20:10:17 +00:00
public ImageExportSettings ( string path , string name , string extension , bool floor , bool fullbright , bool brightmap , bool tiles , PixelFormat pformat , ImageFormat iformat )
2020-06-17 20:22:00 +00:00
{
Path = path ;
2020-06-24 20:10:17 +00:00
Name = name ;
Extension = extension ;
2020-06-17 20:22:00 +00:00
Floor = floor ;
2020-06-24 20:10:17 +00:00
Brightmap = brightmap ;
Tiles = tiles ;
Fullbright = fullbright ;
2020-06-17 20:22:00 +00:00
PixelFormat = pformat ;
ImageFormat = iformat ;
}
}
2020-06-24 20:10:17 +00:00
#endregion
2020-06-17 20:22:00 +00:00
internal class ImageExporter
{
2020-06-24 20:10:17 +00:00
#region = = = = = = = = = = = = = = = = = = Variables
2020-06-17 20:22:00 +00:00
2020-06-24 20:10:17 +00:00
private ICollection < Sector > sectors ;
private ImageExportSettings settings ;
2020-06-17 20:22:00 +00:00
2020-06-24 20:10:17 +00:00
#endregion
2020-06-17 20:22:00 +00:00
2020-06-24 20:10:17 +00:00
#region = = = = = = = = = = = = = = = = = = Constants
2020-06-17 20:22:00 +00:00
2020-06-24 20:10:17 +00:00
private const int TILE_SIZE = 64 ;
2020-06-17 20:22:00 +00:00
2020-06-24 20:10:17 +00:00
#endregion
2020-06-17 20:22:00 +00:00
2020-06-24 20:10:17 +00:00
#region = = = = = = = = = = = = = = = = = = Constructors
public ImageExporter ( ICollection < Sector > sectors , ImageExportSettings settings )
{
this . sectors = sectors ;
this . settings = settings ;
}
#endregion
#region = = = = = = = = = = = = = = = = = = Methods
2020-06-17 20:22:00 +00:00
2020-06-24 20:10:17 +00:00
/// <summary>
/// Exports the sectors to images
/// </summary>
public void Export ( )
{
Bitmap texturebitmap = null ;
Bitmap brightmapbitmap = null ;
Graphics gbrightmap = null ;
Graphics gtexture = null ;
Vector2D offset ;
Vector2D size ;
GetSizeAndOffset ( out size , out offset ) ;
2020-06-17 20:22:00 +00:00
2020-06-24 20:10:17 +00:00
// 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
}
2020-06-17 20:22:00 +00:00
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.0 ;
Vector2D v2 = s . Triangles . Vertices [ i * 3 + 1 ] - offset ; v2 . y * = - 1.0 ;
Vector2D v3 = s . Triangles . Vertices [ i * 3 + 2 ] - offset ; v3 . y * = - 1.0 ;
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 ( ) ;
}
2020-06-24 20:10:17 +00:00
Bitmap brushtexture ;
2020-07-12 09:43:25 +00:00
Vector2D textureoffset = new Vector2D ( ) ;
Vector2D texturescale = new Vector2D ( ) ;
2020-06-24 20:10:17 +00:00
if ( settings . Floor )
2020-07-12 09:43:25 +00:00
{
2020-08-01 10:28:23 +00:00
// The image might have a color correction applied, but we need it without. So we use LocalGetBitmap, because it reloads the image,
// but doesn't applie the color correction if we set UseColorCorrection to false first
ImageData imagedata = General . Map . Data . GetFlatImage ( s . FloorTexture ) ;
imagedata . UseColorCorrection = false ;
brushtexture = imagedata . LocalGetBitmap ( ) ;
imagedata . UseColorCorrection = true ;
2020-07-12 09:43:25 +00:00
textureoffset . x = s . Fields . GetValue ( "xpanningfloor" , 0.0 ) ;
textureoffset . y = s . Fields . GetValue ( "ypanningfloor" , 0.0 ) ;
// GZDoom uses bigger numbers for smaller scales (i.e. a scale of 2 will halve the size), so we need to change the scale
texturescale . x = 1.0 / s . Fields . GetValue ( "xscalefloor" , 1.0 ) ;
texturescale . y = 1.0 / s . Fields . GetValue ( "yscalefloor" , 1.0 ) ;
}
2020-06-17 20:22:00 +00:00
else
2020-07-12 09:43:25 +00:00
{
2020-08-01 10:28:23 +00:00
// The image might have a color correction applied, but we need it without. So we use LocalGetBitmap, because it reloads the image,
// but doesn't applie the color correction if we set UseColorCorrection to false first
ImageData imagedata = General . Map . Data . GetFlatImage ( s . CeilTexture ) ;
imagedata . UseColorCorrection = false ;
brushtexture = imagedata . LocalGetBitmap ( ) ;
imagedata . UseColorCorrection = true ;
2020-06-24 20:10:17 +00:00
2020-07-12 09:43:25 +00:00
textureoffset . x = s . Fields . GetValue ( "xpanningceiling" , 0.0 ) ;
textureoffset . y = s . Fields . GetValue ( "ypanningceiling" , 0.0 ) ;
// GZDoom uses bigger numbers for smaller scales (i.e. a scale of 2 will halve the size), so we need to change the scale
texturescale . x = 1.0 / s . Fields . GetValue ( "xscaleceiling" , 1.0 ) ;
texturescale . y = 1.0 / s . Fields . GetValue ( "yscaleceiling" , 1.0 ) ;
}
2020-06-24 20:10:17 +00:00
if ( ! settings . Fullbright )
brushtexture = AdjustBrightness ( brushtexture , s . Brightness > 0 ? s . Brightness / 255.0f : 0.0f ) ;
2020-06-17 20:22:00 +00:00
// 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
2020-07-12 09:43:25 +00:00
matrix . Scale ( ( float ) texturescale . x , ( float ) texturescale . y ) ;
2020-06-17 20:22:00 +00:00
// Create the texture brush and apply the matrix
2020-06-24 20:10:17 +00:00
TextureBrush tbrush = new TextureBrush ( brushtexture ) ;
tbrush . Transform = matrix ;
2020-06-17 20:22:00 +00:00
// Draw the islands of the sector
2020-06-24 20:10:17 +00:00
gtexture . FillPath ( tbrush , p ) ;
2020-06-17 20:22:00 +00:00
2020-06-24 20:10:17 +00:00
// 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 ) ;
}
2020-06-17 20:22:00 +00:00
}
2020-06-24 20:10:17 +00:00
// 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 ) ;
}
2020-06-17 20:22:00 +00:00
}
2020-06-24 20:10:17 +00:00
/// <summary>
/// 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
/// </summary>
/// <param name="image">the image to split into tiles</param>
/// <param name="suffix">additional suffix for filenames</param>
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 + + ;
}
}
}
/// <summary>
/// Generates a list of images file names that will be creates
/// </summary>
/// <returns>List of image file names</returns>
public List < string > GetImageNames ( )
{
List < string > imagenames = new List < string > ( ) ;
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 ;
}
/// <summary>
/// Gets the size of the image, based on the sectors, and the offset from the map origin
/// </summary>
/// <param name="size">stores the size of the size of the image</param>
/// <param name="offset">stores the offset from the map origin</param>
private void GetSizeAndOffset ( out Vector2D size , out Vector2D offset )
{
offset = new Vector2D ( double . MaxValue , double . MinValue ) ;
size = new Vector2D ( double . MinValue , double . MaxValue ) ;
// Find the top left and bottom right corners of the selection
foreach ( Sector s in sectors )
{
foreach ( Sidedef sd in s . Sidedefs )
{
foreach ( Vertex v in new Vertex [ ] { sd . Line . Start , sd . Line . End } )
{
if ( v . Position . x < offset . x )
offset . x = v . Position . x ;
if ( v . Position . x > size . x )
size . x = v . Position . x ;
if ( v . Position . y > offset . y )
offset . y = v . Position . y ;
if ( v . Position . y < size . y )
size . y = v . Position . y ;
}
}
}
// Right now "size" is the bottom right corener of the selection, so subtract the offset
// (top left corner of the selection). y will always be negative, so make it positive
size - = offset ;
size . y * = - 1.0 ;
}
/// <summary>
/// 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/
/// </summary>
/// <param name="image">Base image</param>
/// <param name="brightness">Brightness between 0.0f and 1.0f</param>
/// <returns>The new image with changed brightness</returns>
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 ) ;
// 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 ) ;
// Make the result bitmap.
Bitmap bm = new Bitmap ( image . Width , image . Height ) ;
using ( Graphics gr = Graphics . FromImage ( bm ) )
{
gr . DrawImage ( image , points , rect , GraphicsUnit . Pixel , attributes ) ;
}
// Return the result.
return bm ;
}
#endregion
2020-06-17 20:22:00 +00:00
}
}