/*
* 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
}
}