예제 #1
0
        static Texture CreateTextureFromImage(string path, string textureName, TextureSettings textureSettings, bool isDecalsWad)
        {
            // Load the main texture image, and any available mipmap images:
            using (var images = new DisposableList <Image <Rgba32> >(GetMipmapFilePaths(path).Prepend(path)
                                                                     .Select(imagePath => File.Exists(imagePath) ? ImageReading.ReadImage(imagePath) : null)))
            {
                // Verify image sizes:
                if (images[0].Width % 16 != 0 || images[0].Height % 16 != 0)
                {
                    throw new InvalidDataException($"Texture '{path}' width or height is not a multiple of 16.");
                }

                for (int i = 1; i < images.Count; i++)
                {
                    if (images[i] != null && (images[i].Width != images[0].Width >> i || images[i].Height != images[0].Height >> i))
                    {
                        throw new InvalidDataException($"Mipmap {i} for texture '{path}' width or height does not match texture size.");
                    }
                }

                if (isDecalsWad)
                {
                    return(CreateDecalTexture(textureName, images.ToArray(), textureSettings));
                }


                var filename             = Path.GetFileName(path);
                var isTransparentTexture = filename.StartsWith("{");
                var isAnimatedTexture    = AnimatedTextureNameRegex.IsMatch(filename);
                var isWaterTexture       = filename.StartsWith("!");

                // Create a suitable palette, taking special texture types into account:
                var transparencyThreshold = isTransparentTexture ? Clamp(textureSettings.TransparencyThreshold ?? 128, 0, 255) : 0;
                Func <Rgba32, bool> isTransparentPredicate = null;
                if (textureSettings.TransparencyColor != null)
                {
                    var transparencyColor = textureSettings.TransparencyColor.Value;
                    isTransparentPredicate = color => color.A < transparencyThreshold || (color.R == transparencyColor.R && color.G == transparencyColor.G && color.B == transparencyColor.B);
                }
                else
                {
                    isTransparentPredicate = color => color.A < transparencyThreshold;
                }

                var colorHistogram = ColorQuantization.GetColorHistogram(images.Where(image => image != null), isTransparentPredicate);
                var maxColors      = 256 - (isTransparentTexture ? 1 : 0) - (isWaterTexture ? 2 : 0);
                var colorClusters  = ColorQuantization.GetColorClusters(colorHistogram, maxColors);

                // Always make sure we've got a 256-color palette (some tools can't handle smaller palettes):
                if (colorClusters.Length < maxColors)
                {
                    colorClusters = colorClusters
                                    .Concat(Enumerable
                                            .Range(0, maxColors - colorClusters.Length)
                                            .Select(i => (new Rgba32(), new[] { new Rgba32() })))
                                    .ToArray();
                }

                // Make palette adjustments for special textures:
                if (isWaterTexture)
                {
                    var fogColor     = textureSettings.WaterFogColor ?? ColorQuantization.GetAverageColor(colorHistogram);
                    var fogIntensity = new Rgba32((byte)Clamp(textureSettings.WaterFogColor?.A ?? (int)((1f - GetBrightness(fogColor)) * 255), 0, 255), 0, 0);

                    colorClusters = colorClusters.Take(3)
                                    .Append((fogColor, new[] { fogColor }))         // Slot 3: water fog color
                                    .Append((fogIntensity, new[] { fogIntensity })) // Slot 4: fog intensity (stored in red channel)
                                    .Concat(colorClusters.Skip(3))
                                    .ToArray();
                }

                if (isTransparentTexture)
                {
                    var colorKey = new Rgba32(0, 0, 255);
                    colorClusters = colorClusters
                                    .Append((colorKey, new[] { colorKey })) // Slot 255: used for transparent pixels
                                    .ToArray();
                }

                // Create the actual palette, and a color index lookup cache:
                var palette = colorClusters
                              .Select(cluster => cluster.Item1)
                              .ToArray();
                var colorIndexMappingCache = new Dictionary <Rgba32, int>();
                for (int i = 0; i < colorClusters.Length; i++)
                {
                    (_, var colors) = colorClusters[i];
                    foreach (var color in colors)
                    {
                        colorIndexMappingCache[color] = i;
                    }
                }

                // Create any missing mipmaps:
                for (int i = 1; i < images.Count; i++)
                {
                    if (images[i] == null)
                    {
                        images[i] = images[0].Clone(context => context.Resize(images[0].Width >> i, images[0].Height >> i));
                    }
                }

                // Create texture data:
                var textureData = images
                                  .Select(image => CreateTextureData(image, palette, colorIndexMappingCache, textureSettings, isTransparentPredicate, disableDithering: isAnimatedTexture))
                                  .ToArray();

                return(Texture.CreateMipmapTexture(
                           name: textureName,
                           width: images[0].Width,
                           height: images[0].Height,
                           imageData: textureData[0],
                           palette: palette,
                           mipmap1Data: textureData[1],
                           mipmap2Data: textureData[2],
                           mipmap3Data: textureData[3]));
            }
        }
예제 #2
0
        static void MakeWad(string inputDirectory, string outputWadFilePath, bool fullRebuild, bool includeSubDirectories)
        {
            var stopwatch = Stopwatch.StartNew();

            var texturesAdded   = 0;
            var texturesUpdated = 0;
            var texturesRemoved = 0;

            var wadMakingSettings = WadMakingSettings.Load(inputDirectory);
            var updateExistingWad = !fullRebuild && File.Exists(outputWadFilePath);
            var wad = updateExistingWad ? Wad.Load(outputWadFilePath) : new Wad();
            var lastWadUpdateTime         = updateExistingWad ? new FileInfo(outputWadFilePath).LastWriteTimeUtc : (DateTime?)null;
            var wadTextureNames           = wad.Textures.Select(texture => texture.Name.ToLowerInvariant()).ToHashSet();
            var conversionOutputDirectory = ExternalConversion.GetConversionOutputDirectory(inputDirectory);
            var isDecalsWad = Path.GetFileNameWithoutExtension(outputWadFilePath).ToLowerInvariant() == "decals";

            // Multiple files can map to the same texture, due to different extensions and upper/lower-case differences.
            // We'll group files by texture name, to make these collisions easy to detect:
            var allInputDirectoryFiles = Directory.EnumerateFiles(inputDirectory, "*", includeSubDirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly)
                                         .Where(path => !ExternalConversion.IsConversionOutputDirectory(path))
                                         .ToHashSet();
            var textureImagePaths = allInputDirectoryFiles
                                    .Where(path => ImageReading.IsSupported(path) || wadMakingSettings.GetTextureSettings(Path.GetFileName(path)).settings.Converter != null)
                                    .Where(path => !path.Contains(".mipmap"))
                                    .Where(path => !WadMakingSettings.IsConfigurationFile(path))
                                    .GroupBy(path => GetTextureName(path));

            // Check for new and updated images:
            try
            {
                foreach (var imagePathsGroup in textureImagePaths)
                {
                    var textureName = imagePathsGroup.Key;
                    if (!IsValidTextureName(textureName))
                    {
                        Log($"WARNING: '{textureName}' is not a valid texture name ({string.Join(", ", imagePathsGroup)}). Skipping file(s).");
                        continue;
                    }
                    else if (textureName.Length > 15)
                    {
                        Log($"WARNING: The name '{textureName}' is too long ({string.Join(", ", imagePathsGroup)}). Skipping file(s).");
                        continue;
                    }
                    else if (imagePathsGroup.Count() > 1)
                    {
                        Log($"WARNING: multiple input files detected for '{textureName}' ({string.Join(", ", imagePathsGroup)}). Skipping files.");
                        continue;
                    }
                    // NOTE: Texture dimensions (which must be multiples of 16) are checked later, in CreateTextureFromImage.


                    var filePath            = imagePathsGroup.Single();
                    var isExistingImage     = wadTextureNames.Contains(textureName.ToLowerInvariant());
                    var isSupportedFileType = ImageReading.IsSupported(filePath);

                    // For files that are not directly supported, we'll include their extension when looking up conversion settings:
                    (var textureSettings, var lastSettingsChangeTime) = wadMakingSettings.GetTextureSettings(isSupportedFileType ? textureName : Path.GetFileName(filePath));
                    if (isExistingImage && updateExistingWad)
                    {
                        // NOTE: A texture will not be rebuilt if one of its mipmap files has been removed. In order to detect such cases,
                        //       WadMaker would need to store additional bookkeeping data, but right now that doesn't seem worth the trouble.
                        // NOTE: Mipmaps must have the same extension as the main image file.
                        var isImageUpdated = GetMipmapFilePaths(filePath)
                                             .Prepend(filePath)
                                             .Where(allInputDirectoryFiles.Contains)
                                             .Select(path => new FileInfo(path).LastWriteTimeUtc)
                                             .Any(dateTime => dateTime > lastWadUpdateTime);
                        if (!isImageUpdated && lastSettingsChangeTime < lastWadUpdateTime)
                        {
                            //Log($"No modifications detected for '{textureName}' ({filePath}). Skipping file.");
                            continue;
                        }
                    }

                    try
                    {
                        var imageFilePath = filePath;
                        if (textureSettings.Converter != null)
                        {
                            if (textureSettings.ConverterArguments == null)
                            {
                                throw new InvalidDataException($"Unable to convert '{filePath}': missing converter arguments.");
                            }

                            imageFilePath = Path.Combine(conversionOutputDirectory, textureName);
                            CreateDirectory(conversionOutputDirectory);

                            var outputFilePaths = ExternalConversion.ExecuteConversionCommand(textureSettings.Converter, textureSettings.ConverterArguments, filePath, imageFilePath, Log);
                            if (imageFilePath.Length < 1)
                            {
                                throw new IOException("Unable to find converter output file. An output file must have the same name as the input file (different extensions are ok).");
                            }

                            var supportedOutputFilePaths = outputFilePaths.Where(ImageReading.IsSupported).ToArray();
                            if (supportedOutputFilePaths.Length < 1)
                            {
                                throw new IOException("The converter did not produce a supported file type.");
                            }
                            else if (supportedOutputFilePaths.Length > 1)
                            {
                                throw new IOException("The converted produced multiple supported file types. Only one output file should be created.");
                            }

                            imageFilePath = supportedOutputFilePaths[0];
                        }

                        // Create texture from image:
                        var texture = CreateTextureFromImage(imageFilePath, textureName, textureSettings, isDecalsWad);

                        if (isExistingImage)
                        {
                            // Update (replace) existing texture:
                            for (int i = 0; i < wad.Textures.Count; i++)
                            {
                                if (wad.Textures[i].Name == texture.Name)
                                {
                                    wad.Textures[i] = texture;
                                    break;
                                }
                            }
                            texturesUpdated += 1;
                            Log($"Updated texture '{textureName}' (from '{filePath}').");
                        }
                        else
                        {
                            // Add new texture:
                            wad.Textures.Add(texture);
                            wadTextureNames.Add(textureName);
                            texturesAdded += 1;
                            Log($"Added texture '{textureName}' (from '{filePath}').");
                        }
                    }
                    catch (Exception ex)
                    {
                        Log($"ERROR: failed to build '{filePath}': {ex.GetType().Name}: '{ex.Message}'.");
                    }
                }

                if (updateExistingWad)
                {
                    // Check for removed images:
                    var directoryTextureNames = textureImagePaths
                                                .Select(group => group.Key)
                                                .ToHashSet();
                    foreach (var textureName in wadTextureNames)
                    {
                        if (!directoryTextureNames.Contains(textureName))
                        {
                            // Delete texture:
                            wad.Textures.Remove(wad.Textures.First(texture => texture.Name.ToLowerInvariant() == textureName));
                            texturesRemoved += 1;
                            Log($"Removed texture '{textureName}'.");
                        }
                    }
                }

                // Finally, save the wad file:
                CreateDirectory(Path.GetDirectoryName(outputWadFilePath));
                wad.Save(outputWadFilePath);
            }
            finally
            {
                try
                {
                    if (Directory.Exists(conversionOutputDirectory))
                    {
                        Directory.Delete(conversionOutputDirectory, true);
                    }
                }
                catch (Exception ex)
                {
                    Log($"WARNING: Failed to delete temporary conversion output directory: {ex.GetType().Name}: '{ex.Message}'.");
                }
            }

            if (updateExistingWad)
            {
                Log($"Updated {outputWadFilePath} from {inputDirectory}: added {texturesAdded}, updated {texturesUpdated} and removed {texturesRemoved} textures, in {stopwatch.Elapsed.TotalSeconds:0.000} seconds.");
            }
            else
            {
                Log($"Created {outputWadFilePath}, with {texturesAdded} textures from {inputDirectory}, in {stopwatch.Elapsed.TotalSeconds:0.000} seconds.");
            }
        }