/// <summary> /// Gets Texture2D atlas from Daggerfall texture archive. /// Every record and frame in the archive will be added to atlas. /// An array of rects will be returned with sub-texture rect for each record index and frame. /// Currently supports one archive per atlas. Super-atlas packing (multiple archives) is in the works. /// </summary> /// <param name="settings">Get texture settings.</param> /// <param name="alphaTextureFormat">Alpha TextureFormat.</param> /// <param name="nonAlphaFormat">Non-alpha TextureFormat.</param> /// <returns>GetTextureResults.</returns> public GetTextureResults GetTexture2DAtlas( GetTextureSettings settings, SupportedAlphaTextureFormats alphaTextureFormat = SupportedAlphaTextureFormats.RGBA32, SupportedNonAlphaTextureFormats nonAlphaFormat = SupportedNonAlphaTextureFormats.RGB24) { GetTextureResults results = new GetTextureResults(); // Individual textures must remain readable to pack into atlas bool stayReadable = settings.stayReadable; settings.stayReadable = true; // Assign texture file TextureFile textureFile; if (settings.textureFile == null) { textureFile = new TextureFile(Path.Combine(Arena2Path, TextureFile.IndexToFileName(settings.archive)), FileUsage.UseMemory, true); settings.textureFile = textureFile; } else textureFile = settings.textureFile; // Create lists results.atlasSizes = new List<Vector2>(textureFile.RecordCount); results.atlasScales = new List<Vector2>(textureFile.RecordCount); results.atlasOffsets = new List<Vector2>(textureFile.RecordCount); results.atlasFrameCounts = new List<int>(textureFile.RecordCount); // Read every texture in archive bool hasNormalMaps = false; bool hasEmissionMaps = false; bool hasAnimation = false; List<Texture2D> albedoTextures = new List<Texture2D>(); List<Texture2D> normalTextures = new List<Texture2D>(); List<Texture2D> emissionTextures = new List<Texture2D>(); List<RecordIndex> indices = new List<RecordIndex>(); for (int record = 0; record < textureFile.RecordCount; record++) { // Get record index and frame count settings.record = record; int frames = textureFile.GetFrameCount(record); if (frames > 1) hasAnimation = true; // Get record information DFSize size = textureFile.GetSize(record); DFSize scale = textureFile.GetScale(record); DFPosition offset = textureFile.GetOffset(record); RecordIndex ri = new RecordIndex() { startIndex = albedoTextures.Count, frameCount = frames, width = size.Width, height = size.Height, }; indices.Add(ri); for (int frame = 0; frame < frames; frame++) { settings.frame = frame; GetTextureResults nextTextureResults = GetTexture2D(settings, alphaTextureFormat, nonAlphaFormat); albedoTextures.Add(nextTextureResults.albedoMap); if (nextTextureResults.normalMap != null) { normalTextures.Add(nextTextureResults.normalMap); hasNormalMaps = true; } if (nextTextureResults.emissionMap != null) { emissionTextures.Add(nextTextureResults.emissionMap); hasEmissionMaps = true; } } results.atlasSizes.Add(new Vector2(size.Width, size.Height)); results.atlasScales.Add(new Vector2(scale.Width, scale.Height)); results.atlasOffsets.Add(new Vector2(offset.X, offset.Y)); results.atlasFrameCounts.Add(frames); results.textureFile = textureFile; } // Pack albedo textures into atlas and get our rects Texture2D atlasAlbedoMap = new Texture2D(settings.atlasMaxSize, settings.atlasMaxSize, ParseTextureFormat(alphaTextureFormat), MipMaps); Rect[] rects = atlasAlbedoMap.PackTextures(albedoTextures.ToArray(), settings.atlasPadding, settings.atlasMaxSize, !stayReadable); // Pack normal textures into atlas Texture2D atlasNormalMap = null; if (hasNormalMaps) { // Normals must be ARGB32 atlasNormalMap = new Texture2D(settings.atlasMaxSize, settings.atlasMaxSize, TextureFormat.ARGB32, MipMaps); atlasNormalMap.PackTextures(normalTextures.ToArray(), settings.atlasPadding, settings.atlasMaxSize, !stayReadable); } // Pack emission textures into atlas // TODO: Change this as packing not consistent Texture2D atlasEmissionMap = null; if (hasEmissionMaps) { // Repacking to ensure correct mix of lit and unlit atlasEmissionMap = new Texture2D(settings.atlasMaxSize, settings.atlasMaxSize, ParseTextureFormat(alphaTextureFormat), MipMaps); atlasEmissionMap.PackTextures(emissionTextures.ToArray(), settings.atlasPadding, settings.atlasMaxSize, !stayReadable); } // Add to results if (results.atlasRects == null) results.atlasRects = new List<Rect>(rects.Length); if (results.atlasIndices == null) results.atlasIndices = new List<RecordIndex>(indices.Count); results.atlasRects.AddRange(rects); results.atlasIndices.AddRange(indices); // Shrink UV rect to compensate for internal border float ru = 1f / atlasAlbedoMap.width; float rv = 1f / atlasAlbedoMap.height; int finalBorder = settings.borderSize + settings.atlasShrinkUVs; for (int i = 0; i < results.atlasRects.Count; i++) { Rect rct = results.atlasRects[i]; rct.xMin += finalBorder * ru; rct.xMax -= finalBorder * ru; rct.yMin += finalBorder * rv; rct.yMax -= finalBorder * rv; results.atlasRects[i] = rct; } // Store results results.albedoMap = atlasAlbedoMap; results.normalMap = atlasNormalMap; results.emissionMap = atlasEmissionMap; results.isAtlasAnimated = hasAnimation; results.isEmissive = hasEmissionMaps; return results; }
/// <summary> /// Reads any Daggerfall image file to ImageData package. /// </summary> /// <param name="filename">Name of standalone file as it appears in arena2 folder.</param> /// <param name="record">Which image record to read for multi-image files.</param> /// <param name="frame">Which frame to read for multi-frame images.</param> /// <param name="hasAlpha">Enable this for image cutouts.</param> /// <param name="createTexture">Create a Texture2D.</param> /// <returns>ImageData. If result.type == ImageTypes.None then read failed.</returns> public static ImageData GetImageData(string filename, int record = 0, int frame = 0, bool hasAlpha = false, bool createTexture = true) { // Check API ready DaggerfallUnity dfUnity = DaggerfallUnity.Instance; if (!dfUnity.IsReady) return new ImageData(); // Parse image file type ImageTypes fileType; try { fileType = ParseFileType(filename); } catch { return new ImageData(); } // Create base image data ImageData imageData = new ImageData(); imageData.type = fileType; imageData.filename = filename; imageData.record = record; imageData.frame = frame; imageData.hasAlpha = hasAlpha; // Read supported image files DFBitmap dfBitmap = null; switch (fileType) { case ImageTypes.TEXTURE: TextureFile textureFile = new TextureFile(Path.Combine(dfUnity.Arena2Path, filename), FileUsage.UseMemory, true); textureFile.LoadPalette(Path.Combine(dfUnity.Arena2Path, textureFile.PaletteName)); dfBitmap = textureFile.GetDFBitmap(record, frame); imageData.offset = textureFile.GetOffset(record); imageData.scale = textureFile.GetScale(record); imageData.size = textureFile.GetSize(record); break; case ImageTypes.IMG: ImgFile imgFile = new ImgFile(Path.Combine(dfUnity.Arena2Path, filename), FileUsage.UseMemory, true); imgFile.LoadPalette(Path.Combine(dfUnity.Arena2Path, imgFile.PaletteName)); dfBitmap = imgFile.GetDFBitmap(); imageData.offset = imgFile.ImageOffset; imageData.scale = new DFSize(); imageData.size = imgFile.GetSize(0); break; case ImageTypes.CIF: case ImageTypes.RCI: CifRciFile cifFile = new CifRciFile(Path.Combine(dfUnity.Arena2Path, filename), FileUsage.UseMemory, true); cifFile.LoadPalette(Path.Combine(dfUnity.Arena2Path, cifFile.PaletteName)); dfBitmap = cifFile.GetDFBitmap(record, frame); imageData.offset = cifFile.GetOffset(record); imageData.scale = new DFSize(); imageData.size = cifFile.GetSize(record); break; default: return new ImageData(); } // Store bitmap imageData.dfBitmap = dfBitmap; imageData.width = dfBitmap.Width; imageData.height = dfBitmap.Height; // Create Texture2D if (createTexture) { // Get colors array Color32[] colors = GetColors(imageData); if (colors == null) return new ImageData(); // Create new Texture2D imageData.texture = GetTexture(colors, imageData.width, imageData.height); } return imageData; }
/// <summary> /// Gets Unity textures from Daggerfall texture with all options. /// Returns all supported texture maps for Standard shader in one call. /// </summary> /// <param name="settings">Get texture settings.</param> /// <param name="alphaTextureFormat">Alpha TextureFormat.</param> /// <param name="nonAlphaFormat">Non-alpha TextureFormat.</param> /// <returns>GetTextureResults.</returns> public GetTextureResults GetTexture2D( GetTextureSettings settings, SupportedAlphaTextureFormats alphaTextureFormat = SupportedAlphaTextureFormats.RGBA32, SupportedNonAlphaTextureFormats nonAlphaFormat = SupportedNonAlphaTextureFormats.RGB24) { GetTextureResults results = new GetTextureResults(); // Check if window or auto-emissive bool isWindow = ClimateSwaps.IsExteriorWindow(settings.archive, settings.record); bool isEmissive = (settings.autoEmission) ? IsEmissive(settings.archive, settings.record) : false; // Assign texture file TextureFile textureFile; if (settings.textureFile == null) textureFile = new TextureFile(Path.Combine(Arena2Path, TextureFile.IndexToFileName(settings.archive)), FileUsage.UseMemory, true); else textureFile = settings.textureFile; // Get starting DFBitmap and albedo Color32 array DFSize sz; DFBitmap srcBitmap = textureFile.GetDFBitmap(settings.record, settings.frame); Color32[] albedoColors = textureFile.GetColor32(srcBitmap, settings.alphaIndex, settings.borderSize, out sz); // Sharpen source image if (settings.sharpen) albedoColors = ImageProcessing.Sharpen(ref albedoColors, sz.Width, sz.Height); // Dilate edges if (settings.borderSize > 0 && settings.dilate && !settings.copyToOppositeBorder) ImageProcessing.DilateColors(ref albedoColors, sz); // Copy to opposite border if (settings.borderSize > 0 && settings.copyToOppositeBorder) ImageProcessing.WrapBorder(ref albedoColors, sz, settings.borderSize); // Create albedo texture Texture2D albedoMap = null; if (settings.alphaIndex < 0) albedoMap = new Texture2D(sz.Width, sz.Height, ParseTextureFormat(nonAlphaFormat), MipMaps); else albedoMap = new Texture2D(sz.Width, sz.Height, ParseTextureFormat(alphaTextureFormat), MipMaps); albedoMap.SetPixels32(albedoColors); albedoMap.Apply(true, !settings.stayReadable); // Create normal texture - must be ARGB32 // Normal maps are bypassed for solid-colour textures Texture2D normalMap = null; if (settings.createNormalMap && textureFile.SolidType == TextureFile.SolidTypes.None) { Color32[] normalColors; normalColors = ImageProcessing.GetBumpMap(ref albedoColors, sz.Width, sz.Height); normalColors = ImageProcessing.ConvertBumpToNormals(ref normalColors, sz.Width, sz.Height, settings.normalStrength); normalMap = new Texture2D(sz.Width, sz.Height, TextureFormat.ARGB32, MipMaps); normalMap.SetPixels32(normalColors); normalMap.Apply(true, !settings.stayReadable); } // Create basic emissive texture Texture2D emissionMap = null; bool resultEmissive = false; if (settings.createEmissionMap || (settings.autoEmission && isEmissive) && !isWindow) { // Just reuse albedo map for basic colour emission emissionMap = albedoMap; resultEmissive = true; } // Windows need special handling as only glass parts are emissive if ((settings.createEmissionMap || settings.autoEmissionForWindows) && isWindow) { // Create custom emission texture for glass area of windows Color32[] emissionColors = textureFile.GetWindowColors32(srcBitmap); emissionMap = new Texture2D(sz.Width, sz.Height, ParseTextureFormat(alphaTextureFormat), MipMaps); emissionMap.SetPixels32(emissionColors); emissionMap.Apply(true, !settings.stayReadable); resultEmissive = true; } // Lights need special handling as this archive contains a mix of emissive and non-emissive flats // This can cause problems with atlas packing due to mismatch between albedo and emissive texture counts if ((settings.createEmissionMap || settings.autoEmission) && settings.archive == LightsTextureArchive) { // For the unlit flats we create a null-emissive black texture if (!isEmissive) { Color32[] emissionColors = new Color32[sz.Width * sz.Height]; emissionMap = new Texture2D(sz.Width, sz.Height, ParseTextureFormat(alphaTextureFormat), MipMaps); emissionMap.SetPixels32(emissionColors); emissionMap.Apply(true, !settings.stayReadable); resultEmissive = true; } } // Shrink UV rect to compensate for internal border float ru = 1f / sz.Width; float rv = 1f / sz.Height; results.singleRect = new Rect( settings.borderSize * ru, settings.borderSize * rv, (sz.Width - settings.borderSize * 2) * ru, (sz.Height - settings.borderSize * 2) * rv); // Store results results.albedoMap = albedoMap; results.normalMap = normalMap; results.emissionMap = emissionMap; results.isWindow = isWindow; results.isEmissive = resultEmissive; results.textureFile = textureFile; return results; }
/// <summary> /// Gets specially packed tileset atlas for terrains. /// This needs to be improved to create each mip level manually to further reduce artifacts. /// </summary> /// <param name="archive">Archive index.</param> /// <param name="stayReadable">Texture should stay readable.</param> /// <param name="nonAlphaFormat">Non-alpha TextureFormat.</param> /// <returns></returns> public GetTextureResults GetTerrainTilesetTexture( int archive, bool stayReadable = false, SupportedNonAlphaTextureFormats nonAlphaFormat = SupportedNonAlphaTextureFormats.RGB24) { const int atlasDim = 2048; const int gutterSize = 32; GetTextureResults results = new GetTextureResults(); // Load texture file and check count matches terrain tiles TextureFile textureFile = new TextureFile(Path.Combine(Arena2Path, TextureFile.IndexToFileName(archive)), FileUsage.UseMemory, true); if (textureFile.RecordCount != 56) return results; // Rollout tiles into atlas. // This is somewhat inefficient, but fortunately atlases only // need to be generated once and can be prepared offline where // startup time is critical. int x = 0, y = 0; Color32[] atlasColors = new Color32[atlasDim * atlasDim]; for (int record = 0; record < textureFile.RecordCount; record++) { // Create base image with gutter DFSize sz; Color32[] albedo = textureFile.GetColor32(record, 0, -1, gutterSize, out sz); // Wrap and clamp textures based on tile switch (record) { // Textures that do not tile in any direction case 5: case 7: case 10: case 12: case 15: case 17: case 20: case 22: case 25: case 27: case 30: case 32: case 34: case 35: case 36: case 37: case 38: case 39: case 40: case 41: case 42: case 43: case 44: case 45: case 47: case 48: case 49: case 50: case 51: case 52: case 53: case 55: ImageProcessing.ClampBorder(ref albedo, sz, gutterSize); break; // Textures that clamp horizontally and tile vertically case 6: case 11: case 16: case 21: case 26: case 31: ImageProcessing.WrapBorder(ref albedo, sz, gutterSize, false); ImageProcessing.ClampBorder(ref albedo, sz, gutterSize, true, false); break; // Textures that tile in all directions default: ImageProcessing.WrapBorder(ref albedo, sz, gutterSize); break; } // Create variants Color32[] rotate = ImageProcessing.RotateColors(ref albedo, sz.Width, sz.Height); Color32[] flip = ImageProcessing.FlipColors(ref albedo, sz.Width, sz.Height); Color32[] rotateFlip = ImageProcessing.RotateColors(ref flip, sz.Width, sz.Height); // Insert into atlas ImageProcessing.InsertColors(ref albedo, ref atlasColors, x, y, sz.Width, sz.Height, atlasDim, atlasDim); ImageProcessing.InsertColors(ref rotate, ref atlasColors, x + sz.Width, y, sz.Width, sz.Height, atlasDim, atlasDim); ImageProcessing.InsertColors(ref flip, ref atlasColors, x + sz.Width * 2, y, sz.Width, sz.Height, atlasDim, atlasDim); ImageProcessing.InsertColors(ref rotateFlip, ref atlasColors, x + sz.Width * 3, y, sz.Width, sz.Height, atlasDim, atlasDim); // Increment position x += sz.Width * 4; if (x >= atlasDim) { y += sz.Height; x = 0; } } // Create Texture2D Texture2D albedoAtlas = new Texture2D(atlasDim, atlasDim, ParseTextureFormat(nonAlphaFormat), MipMaps); albedoAtlas.SetPixels32(atlasColors); albedoAtlas.Apply(true, !stayReadable); // Change settings for these textures albedoAtlas.wrapMode = TextureWrapMode.Clamp; albedoAtlas.anisoLevel = 8; // Store results results.albedoMap = albedoAtlas; return results; }
/// <summary> /// TEMP: Creates super-atlas populated with all archives in array. /// Currently does not support animated textures, normal map, or emission map. /// TODO: Integrate this feature fully with material system. /// </summary> /// <param name="archives">Archive array.</param> /// <param name="borderSize">Number of pixels border to add around image.</param> /// <param name="dilate">Blend texture into surrounding empty pixels. Requires border.</param> /// <param name="maxAtlasSize">Maximum atlas size.</param> /// <param name="alphaTextureFormat">Alpha TextureFormat.</param> /// <param name="nonAlphaFormat">Non-alpha TextureFormat.</param> /// <returns>TextureAtlasBuilder.</returns> public TextureAtlasBuilder CreateTextureAtlasBuilder( int[] archives, int borderSize = 0, bool dilate = false, int maxAtlasSize = 2048, SupportedAlphaTextureFormats alphaTextureFormat = SupportedAlphaTextureFormats.RGBA32, SupportedNonAlphaTextureFormats nonAlphaFormat = SupportedNonAlphaTextureFormats.RGB24) { // Iterate archives TextureFile textureFile = new TextureFile(); TextureAtlasBuilder builder = new TextureAtlasBuilder(); GetTextureSettings settings = TextureReader.CreateTextureSettings(0, 0, 0, 0, borderSize, dilate, maxAtlasSize); settings.stayReadable = true; for (int i = 0; i < archives.Length; i++) { // Load texture file settings.archive = archives[i]; textureFile.Load(Path.Combine(Arena2Path, TextureFile.IndexToFileName(settings.archive)), FileUsage.UseMemory, true); // Add all records for this archive - single frame only for (int record = 0; record < textureFile.RecordCount; record++) { settings.record = record; GetTextureResults results = GetTexture2D(settings, alphaTextureFormat, nonAlphaFormat); DFSize size = textureFile.GetSize(record); DFSize scale = textureFile.GetScale(record); builder.AddTextureItem( results.albedoMap, settings.archive, settings.record, 0, 1, new Vector2(size.Width, size.Height), new Vector2(scale.Width, scale.Height)); } } // Apply the builder builder.Rebuild(borderSize); return builder; }
private bool ReadyCheck() { // Ensure texture reader is ready if (textureFile == null) { textureFile = new TextureFile(); if (!textureFile.Palette.Load(Path.Combine(Arena2Path, textureFile.PaletteName))) { DaggerfallUnity.LogMessage("TextureReader: Failed to load palette file, is Arena2Path correct?", true); textureFile = null; return false; } } return true; }
/// <summary> /// Precalculate and cache billboard scale for every record. /// This will change based on animation state and orientation. /// Cache this to array so it only needs to be calculated once. /// Also store number of frames for state animations. /// </summary> /// <param name="dfUnity">DaggerfallUnity singleton. Required for content readers and settings.</param> /// <param name="archive">Texture archive index derived from type and gender.</param> private void CacheRecordSizesAndFrames(DaggerfallUnity dfUnity, int archive) { // Open texture file string path = Path.Combine(dfUnity.Arena2Path, TextureFile.IndexToFileName(archive)); TextureFile textureFile = new TextureFile(path, FileUsage.UseMemory, true); // Cache size and scale for each record summary.RecordSizes = new Vector2[textureFile.RecordCount]; summary.RecordFrames = new int[textureFile.RecordCount]; for (int i = 0; i < textureFile.RecordCount; i++) { // Get size and scale of this texture DFSize size = textureFile.GetSize(i); DFSize scale = textureFile.GetScale(i); // Set start size Vector2 startSize; startSize.x = size.Width; startSize.y = size.Height; // Apply scale Vector2 finalSize; int xChange = (int)(size.Width * (scale.Width / BlocksFile.ScaleDivisor)); int yChange = (int)(size.Height * (scale.Height / BlocksFile.ScaleDivisor)); finalSize.x = (size.Width + xChange); finalSize.y = (size.Height + yChange); // Store final size and frame count summary.RecordSizes[i] = finalSize * MeshReader.GlobalScale; summary.RecordFrames[i] = textureFile.GetFrameCount(i); } }