/* * Copyright (c) 2006-2014, openmetaverse.org * All rights reserved. * * - Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * - Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * - Neither the name of the openmetaverse.org nor the names * of its contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ using System; using System.Collections.Generic; using System.IO; using System.Drawing; using OpenMetaverse.Assets; namespace OpenMetaverse.Imaging { /// /// A set of textures that are layered on texture of each other and "baked" /// in to a single texture, for avatar appearances /// public class Baker { public static readonly UUID IMG_INVISIBLE = new UUID("3a367d1c-bef1-6d43-7595-e88c1e3aadb3"); #region Properties /// Final baked texture public AssetTexture BakedTexture { get { return bakedTexture; } } /// Component layers public List Textures { get { return textures; } } /// Width of the final baked image and scratchpad public int BakeWidth { get { return bakeWidth; } } /// Height of the final baked image and scratchpad public int BakeHeight { get { return bakeHeight; } } /// Bake type public BakeType BakeType { get { return bakeType; } } /// Is this one of the 3 skin bakes private bool IsSkin { get { return bakeType == BakeType.Head || bakeType == BakeType.LowerBody || bakeType == BakeType.UpperBody; } } #endregion #region Private fields /// Final baked texture private AssetTexture bakedTexture; /// Component layers private List textures = new List(); /// Width of the final baked image and scratchpad private int bakeWidth; /// Height of the final baked image and scratchpad private int bakeHeight; /// Bake type private BakeType bakeType; #endregion #region Constructor /// /// Default constructor /// /// Bake type public Baker(BakeType bakeType) { this.bakeType = bakeType; if (bakeType == BakeType.Eyes) { bakeWidth = 128; bakeHeight = 128; } else { bakeWidth = 512; bakeHeight = 512; } } #endregion #region Public methods /// /// Adds layer for baking /// /// TexturaData struct that contains texture and its params public void AddTexture(AppearanceManager.TextureData tdata) { lock (textures) { textures.Add(tdata); } } public void Bake() { bakedTexture = new AssetTexture(new ManagedImage(bakeWidth, bakeHeight, ManagedImage.ImageChannels.Color | ManagedImage.ImageChannels.Alpha | ManagedImage.ImageChannels.Bump)); // Base color for eye bake is white, color of layer0 for others if (bakeType == BakeType.Eyes) { InitBakedLayerColor(Color4.White); } else if (textures.Count > 0) { InitBakedLayerColor(textures[0].Color); } // Do we have skin texture? bool SkinTexture = textures.Count > 0 && textures[0].Texture != null; if (bakeType == BakeType.Head) { DrawLayer(LoadResourceLayer("head_color.tga"), false); AddAlpha(bakedTexture.Image, LoadResourceLayer("head_alpha.tga")); MultiplyLayerFromAlpha(bakedTexture.Image, LoadResourceLayer("head_skingrain.tga")); } if (!SkinTexture && bakeType == BakeType.UpperBody) { DrawLayer(LoadResourceLayer("upperbody_color.tga"), false); } if (!SkinTexture && bakeType == BakeType.LowerBody) { DrawLayer(LoadResourceLayer("lowerbody_color.tga"), false); } ManagedImage alphaWearableTexture = null; // Layer each texture on top of one other, applying alpha masks as we go for (int i = 0; i < textures.Count; i++) { // Skip if we have no texture on this layer if (textures[i].Texture == null) continue; // Is this Alpha wearable and does it have an alpha channel? if (textures[i].TextureIndex >= AvatarTextureIndex.LowerAlpha && textures[i].TextureIndex <= AvatarTextureIndex.HairAlpha) { if (textures[i].Texture.Image.Alpha != null) { alphaWearableTexture = textures[i].Texture.Image.Clone(); } else if (textures[i].TextureID == IMG_INVISIBLE) { alphaWearableTexture = new ManagedImage(bakeWidth, bakeHeight, ManagedImage.ImageChannels.Alpha); } continue; } // Don't draw skin and tattoo on head bake first // For head bake the skin and texture are drawn last, go figure if (bakeType == BakeType.Head && (i == 0 || i == 1)) continue; ManagedImage texture = textures[i].Texture.Image.Clone(); //File.WriteAllBytes(bakeType + "-texture-layer-" + i + ".tga", texture.ExportTGA()); // Resize texture to the size of baked layer // FIXME: if texture is smaller than the layer, don't stretch it, tile it if (texture.Width != bakeWidth || texture.Height != bakeHeight) { try { texture.ResizeNearestNeighbor(bakeWidth, bakeHeight); } catch (Exception) { continue; } } // Special case for hair layer for the head bake // If we don't have skin texture, we discard hair alpha // and apply hair(i==2) pattern over the texture if (!SkinTexture && bakeType == BakeType.Head && i == 2) { if (texture.Alpha != null) { for (int j = 0; j < texture.Alpha.Length; j++) texture.Alpha[j] = (byte)255; } MultiplyLayerFromAlpha(texture, LoadResourceLayer("head_hair.tga")); } // Aply tint and alpha masks except for skin that has a texture // on layer 0 which always overrides other skin settings if (!(IsSkin && i == 0)) { ApplyTint(texture, textures[i].Color); // For hair bake, we skip all alpha masks // and use one from the texture, for both // alpha and morph layers if (bakeType == BakeType.Hair) { if (texture.Alpha != null) { bakedTexture.Image.Bump = texture.Alpha; } else { for (int j = 0; j < bakedTexture.Image.Bump.Length; j++) bakedTexture.Image.Bump[j] = byte.MaxValue; } } // Apply parametrized alpha masks else if (textures[i].AlphaMasks != null && textures[i].AlphaMasks.Count > 0) { // Combined mask for the layer, fully transparent to begin with ManagedImage combinedMask = new ManagedImage(bakeWidth, bakeHeight, ManagedImage.ImageChannels.Alpha); int addedMasks = 0; // First add mask in normal blend mode foreach (KeyValuePair kvp in textures[i].AlphaMasks) { if (!MaskBelongsToBake(kvp.Key.TGAFile)) continue; if (kvp.Key.MultiplyBlend == false && (kvp.Value > 0f || !kvp.Key.SkipIfZero)) { ApplyAlpha(combinedMask, kvp.Key, kvp.Value); //File.WriteAllBytes(bakeType + "-layer-" + i + "-mask-" + addedMasks + ".tga", combinedMask.ExportTGA()); addedMasks++; } } // If there were no mask in normal blend mode make aplha fully opaque if (addedMasks == 0) for (int l = 0; l < combinedMask.Alpha.Length; l++) combinedMask.Alpha[l] = 255; // Add masks in multiply blend mode foreach (KeyValuePair kvp in textures[i].AlphaMasks) { if (!MaskBelongsToBake(kvp.Key.TGAFile)) continue; if (kvp.Key.MultiplyBlend == true && (kvp.Value > 0f || !kvp.Key.SkipIfZero)) { ApplyAlpha(combinedMask, kvp.Key, kvp.Value); //File.WriteAllBytes(bakeType + "-layer-" + i + "-mask-" + addedMasks + ".tga", combinedMask.ExportTGA()); addedMasks++; } } if (addedMasks > 0) { // Apply combined alpha mask to the cloned texture AddAlpha(texture, combinedMask); } // Is this layer used for morph mask? If it is, use its // alpha as the morth for the whole bake if (Textures[i].TextureIndex == AppearanceManager.MorphLayerForBakeType(bakeType)) { bakedTexture.Image.Bump = texture.Alpha; } //File.WriteAllBytes(bakeType + "-masked-texture-" + i + ".tga", texture.ExportTGA()); } } bool useAlpha = i == 0 && (BakeType == BakeType.Skirt || BakeType == BakeType.Hair); DrawLayer(texture, useAlpha); //File.WriteAllBytes(bakeType + "-layer-" + i + ".tga", texture.ExportTGA()); } // For head and tattoo, we add skin last if (IsSkin && bakeType == BakeType.Head) { ManagedImage texture; if (textures[0].Texture != null) { texture = textures[0].Texture.Image.Clone(); if (texture.Width != bakeWidth || texture.Height != bakeHeight) { try { texture.ResizeNearestNeighbor(bakeWidth, bakeHeight); } catch (Exception) { } } DrawLayer(texture, false); } // Add head tattoo here (if available, order-dependant) if (textures.Count > 1 && textures[1].Texture != null) { texture = textures[1].Texture.Image.Clone(); if (texture.Width != bakeWidth || texture.Height != bakeHeight) { try { texture.ResizeNearestNeighbor(bakeWidth, bakeHeight); } catch (Exception) { } } DrawLayer(texture, false); } } // Apply any alpha wearable textures to make parts of the avatar disappear if (alphaWearableTexture != null) { AddAlpha(bakedTexture.Image, alphaWearableTexture); } // We are done, encode asset for finalized bake bakedTexture.Encode(); //File.WriteAllBytes(bakeType + ".tga", bakedTexture.Image.ExportTGA()); } private static object ResourceSync = new object(); public static ManagedImage LoadResourceLayer(string fileName) { try { Bitmap bitmap = null; lock (ResourceSync) { using (Stream stream = Helpers.GetResourceStream(fileName, Settings.RESOURCE_DIR)) { bitmap = LoadTGAClass.LoadTGA(stream); } } if (bitmap == null) { Logger.Log(String.Format("Failed loading resource file: {0}", fileName), Helpers.LogLevel.Error); return null; } else { ManagedImage image = new ManagedImage(bitmap); bitmap.Dispose(); return image; } } catch (Exception e) { Logger.Log(String.Format("Failed loading resource file: {0} ({1})", fileName, e.Message), Helpers.LogLevel.Error, e); return null; } } /// /// Converts avatar texture index (face) to Bake type /// /// Face number (AvatarTextureIndex) /// BakeType, layer to which this texture belongs to public static BakeType BakeTypeFor(AvatarTextureIndex index) { switch (index) { case AvatarTextureIndex.HeadBodypaint: return BakeType.Head; case AvatarTextureIndex.UpperBodypaint: case AvatarTextureIndex.UpperGloves: case AvatarTextureIndex.UpperUndershirt: case AvatarTextureIndex.UpperShirt: case AvatarTextureIndex.UpperJacket: return BakeType.UpperBody; case AvatarTextureIndex.LowerBodypaint: case AvatarTextureIndex.LowerUnderpants: case AvatarTextureIndex.LowerSocks: case AvatarTextureIndex.LowerShoes: case AvatarTextureIndex.LowerPants: case AvatarTextureIndex.LowerJacket: return BakeType.LowerBody; case AvatarTextureIndex.EyesIris: return BakeType.Eyes; case AvatarTextureIndex.Skirt: return BakeType.Skirt; case AvatarTextureIndex.Hair: return BakeType.Hair; default: return BakeType.Unknown; } } #endregion #region Private layer compositing methods private bool MaskBelongsToBake(string mask) { if ((bakeType == BakeType.LowerBody && mask.Contains("upper")) || (bakeType == BakeType.LowerBody && mask.Contains("shirt")) || (bakeType == BakeType.UpperBody && mask.Contains("lower"))) { return false; } else { return true; } } private bool DrawLayer(ManagedImage source, bool addSourceAlpha) { if (source == null) return false; bool sourceHasColor; bool sourceHasAlpha; bool sourceHasBump; int i = 0; sourceHasColor = ((source.Channels & ManagedImage.ImageChannels.Color) != 0 && source.Red != null && source.Green != null && source.Blue != null); sourceHasAlpha = ((source.Channels & ManagedImage.ImageChannels.Alpha) != 0 && source.Alpha != null); sourceHasBump = ((source.Channels & ManagedImage.ImageChannels.Bump) != 0 && source.Bump != null); addSourceAlpha = (addSourceAlpha && sourceHasAlpha); byte alpha = Byte.MaxValue; byte alphaInv = (byte)(Byte.MaxValue - alpha); byte[] bakedRed = bakedTexture.Image.Red; byte[] bakedGreen = bakedTexture.Image.Green; byte[] bakedBlue = bakedTexture.Image.Blue; byte[] bakedAlpha = bakedTexture.Image.Alpha; byte[] bakedBump = bakedTexture.Image.Bump; byte[] sourceRed = source.Red; byte[] sourceGreen = source.Green; byte[] sourceBlue = source.Blue; byte[] sourceAlpha = sourceHasAlpha ? source.Alpha : null; byte[] sourceBump = sourceHasBump ? source.Bump : null; for (int y = 0; y < bakeHeight; y++) { for (int x = 0; x < bakeWidth; x++) { if (sourceHasAlpha) { alpha = sourceAlpha[i]; alphaInv = (byte)(Byte.MaxValue - alpha); } if (sourceHasColor) { bakedRed[i] = (byte)((bakedRed[i] * alphaInv + sourceRed[i] * alpha) >> 8); bakedGreen[i] = (byte)((bakedGreen[i] * alphaInv + sourceGreen[i] * alpha) >> 8); bakedBlue[i] = (byte)((bakedBlue[i] * alphaInv + sourceBlue[i] * alpha) >> 8); } if (addSourceAlpha) { if (sourceAlpha[i] < bakedAlpha[i]) { bakedAlpha[i] = sourceAlpha[i]; } } if (sourceHasBump) bakedBump[i] = sourceBump[i]; ++i; } } return true; } /// /// Make sure images exist, resize source if needed to match the destination /// /// Destination image /// Source image /// Sanitization was succefull private bool SanitizeLayers(ManagedImage dest, ManagedImage src) { if (dest == null || src == null) return false; if ((dest.Channels & ManagedImage.ImageChannels.Alpha) == 0) { dest.ConvertChannels(dest.Channels | ManagedImage.ImageChannels.Alpha); } if (dest.Width != src.Width || dest.Height != src.Height) { try { src.ResizeNearestNeighbor(dest.Width, dest.Height); } catch (Exception) { return false; } } return true; } private void ApplyAlpha(ManagedImage dest, VisualAlphaParam param, float val) { ManagedImage src = LoadResourceLayer(param.TGAFile); if (dest == null || src == null || src.Alpha == null) return; if ((dest.Channels & ManagedImage.ImageChannels.Alpha) == 0) { dest.ConvertChannels(ManagedImage.ImageChannels.Alpha | dest.Channels); } if (dest.Width != src.Width || dest.Height != src.Height) { try { src.ResizeNearestNeighbor(dest.Width, dest.Height); } catch (Exception) { return; } } for (int i = 0; i < dest.Alpha.Length; i++) { byte alpha = src.Alpha[i] <= ((1 - val) * 255) ? (byte)0 : (byte)255; if (alpha != 255) { } if (param.MultiplyBlend) { dest.Alpha[i] = (byte)((dest.Alpha[i] * alpha) >> 8); } else { if (alpha > dest.Alpha[i]) { dest.Alpha[i] = alpha; } } } } private void AddAlpha(ManagedImage dest, ManagedImage src) { if (!SanitizeLayers(dest, src)) return; for (int i = 0; i < dest.Alpha.Length; i++) { if (src.Alpha[i] < dest.Alpha[i]) { dest.Alpha[i] = src.Alpha[i]; } } } private void MultiplyLayerFromAlpha(ManagedImage dest, ManagedImage src) { if (!SanitizeLayers(dest, src)) return; for (int i = 0; i < dest.Red.Length; i++) { dest.Red[i] = (byte)((dest.Red[i] * src.Alpha[i]) >> 8); dest.Green[i] = (byte)((dest.Green[i] * src.Alpha[i]) >> 8); dest.Blue[i] = (byte)((dest.Blue[i] * src.Alpha[i]) >> 8); } } private void ApplyTint(ManagedImage dest, Color4 src) { if (dest == null) return; for (int i = 0; i < dest.Red.Length; i++) { dest.Red[i] = (byte)((dest.Red[i] * ((byte)(src.R * byte.MaxValue))) >> 8); dest.Green[i] = (byte)((dest.Green[i] * ((byte)(src.G * byte.MaxValue))) >> 8); dest.Blue[i] = (byte)((dest.Blue[i] * ((byte)(src.B * byte.MaxValue))) >> 8); } } /// /// Fills a baked layer as a solid *appearing* color. The colors are /// subtly dithered on a 16x16 grid to prevent the JPEG2000 stage from /// compressing it too far since it seems to cause upload failures if /// the image is a pure solid color /// /// Color of the base of this layer private void InitBakedLayerColor(Color4 color) { InitBakedLayerColor(color.R, color.G, color.B); } /// /// Fills a baked layer as a solid *appearing* color. The colors are /// subtly dithered on a 16x16 grid to prevent the JPEG2000 stage from /// compressing it too far since it seems to cause upload failures if /// the image is a pure solid color /// /// Red value /// Green value /// Blue value private void InitBakedLayerColor(float r, float g, float b) { byte rByte = Utils.FloatToByte(r, 0f, 1f); byte gByte = Utils.FloatToByte(g, 0f, 1f); byte bByte = Utils.FloatToByte(b, 0f, 1f); byte rAlt, gAlt, bAlt; rAlt = rByte; gAlt = gByte; bAlt = bByte; if (rByte < Byte.MaxValue) rAlt++; else rAlt--; if (gByte < Byte.MaxValue) gAlt++; else gAlt--; if (bByte < Byte.MaxValue) bAlt++; else bAlt--; int i = 0; byte[] red = bakedTexture.Image.Red; byte[] green = bakedTexture.Image.Green; byte[] blue = bakedTexture.Image.Blue; byte[] alpha = bakedTexture.Image.Alpha; byte[] bump = bakedTexture.Image.Bump; for (int y = 0; y < bakeHeight; y++) { for (int x = 0; x < bakeWidth; x++) { if (((x ^ y) & 0x10) == 0) { red[i] = rAlt; green[i] = gByte; blue[i] = bByte; alpha[i] = Byte.MaxValue; bump[i] = 0; } else { red[i] = rByte; green[i] = gAlt; blue[i] = bAlt; alpha[i] = Byte.MaxValue; bump[i] = 0; } ++i; } } } #endregion } }