* Complete rewrite of AppearanceManager. Appearance editing has not been (re)implemented yet, but the normal appearance setting is much more reliable * Added a setting (defaulted to true) for automatically setting appearance * Various baking hacks to get slightly less ugly avatars * Added baked texture uploading through CAPS in AssetManager.RequestUploadBakedTexture(). UDP fallback is not implemented yet * Added Parallel.Invoke() and overloads for all three methods that take a threadCount git-svn-id: http://libopenmetaverse.googlecode.com/svn/libopenmetaverse/trunk@3038 52acb1d6-8a22-11de-b505-999d5b087335
459 lines
17 KiB
C#
459 lines
17 KiB
C#
/*
|
|
* Copyright (c) 2007-2008, 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.Reflection;
|
|
using System.Drawing;
|
|
using System.Drawing.Imaging;
|
|
using OpenMetaverse.Assets;
|
|
|
|
namespace OpenMetaverse.Imaging
|
|
{
|
|
/// <summary>
|
|
/// A set of textures that are layered on texture of each other and "baked"
|
|
/// in to a single texture, for avatar appearances
|
|
/// </summary>
|
|
public class Baker
|
|
{
|
|
public AssetTexture BakedTexture { get { return _bakedTexture; } }
|
|
public Dictionary<int, float> ParamValues { get { return _paramValues; } }
|
|
public Dictionary<AvatarTextureIndex, AssetTexture> Textures { get { return _textures; } }
|
|
public int TextureCount { get { return _textureCount; } }
|
|
public int BakeWidth { get { return _bakeWidth; } }
|
|
public int BakeHeight { get { return _bakeHeight; } }
|
|
public BakeType BakeType { get { return _bakeType; } }
|
|
|
|
/// <summary>Reference to the GridClient object</summary>
|
|
protected GridClient _client;
|
|
/// <summary>Finald baked texture</summary>
|
|
protected AssetTexture _bakedTexture;
|
|
/// <summary>Appearance parameters the drive the baking process</summary>
|
|
protected Dictionary<int, float> _paramValues;
|
|
/// <summary>Wearable textures</summary>
|
|
protected Dictionary<AvatarTextureIndex, AssetTexture> _textures = new Dictionary<AvatarTextureIndex, AssetTexture>();
|
|
/// <summary>Total number of textures in the bake</summary>
|
|
protected int _textureCount;
|
|
/// <summary>Width of the final baked image and scratchpad</summary>
|
|
protected int _bakeWidth;
|
|
/// <summary>Height of the final baked image and scratchpad</summary>
|
|
protected int _bakeHeight;
|
|
/// <summary>Bake type</summary>
|
|
protected BakeType _bakeType;
|
|
|
|
/// <summary>
|
|
/// Default constructor
|
|
/// </summary>
|
|
/// <param name="client">Reference to the GridClient object</param>
|
|
/// <param name="bakeType"></param>
|
|
/// <param name="textureCount">Total number of layers this layer set is
|
|
/// composed of</param>
|
|
/// <param name="paramValues">Appearance parameters the drive the
|
|
/// baking process</param>
|
|
public Baker(GridClient client, BakeType bakeType, int textureCount, Dictionary<int, float> paramValues)
|
|
{
|
|
_client = client;
|
|
_bakeType = bakeType;
|
|
_textureCount = textureCount;
|
|
|
|
if (bakeType == BakeType.Eyes)
|
|
{
|
|
_bakeWidth = 128;
|
|
_bakeHeight = 128;
|
|
}
|
|
else
|
|
{
|
|
_bakeWidth = 512;
|
|
_bakeHeight = 512;
|
|
}
|
|
|
|
_paramValues = paramValues;
|
|
|
|
if (textureCount == 0)
|
|
Bake();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds an image to this baking texture and potentially processes it, or
|
|
/// stores it for processing later
|
|
/// </summary>
|
|
/// <param name="index">The baking texture index of the image to be added</param>
|
|
/// <param name="texture">JPEG2000 compressed image to be
|
|
/// added to the baking texture</param>
|
|
/// <param name="needsDecode">True if <code>Decode()</code> needs to be
|
|
/// called for the texture, otherwise false</param>
|
|
/// <returns>True if this texture is completely baked and JPEG2000 data
|
|
/// is available, otherwise false</returns>
|
|
public bool AddTexture(AvatarTextureIndex index, AssetTexture texture, bool needsDecode)
|
|
{
|
|
lock (_textures)
|
|
{
|
|
if (needsDecode)
|
|
{
|
|
try
|
|
{
|
|
texture.Decode();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Logger.Log(String.Format("AddTexture({0}, {1})", index, texture.AssetID), Helpers.LogLevel.Error, e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
_textures.Add(index, texture);
|
|
Logger.DebugLog(String.Format("Added texture {0} (ID: {1}) to bake {2}", index, texture.AssetID, _bakeType), _client);
|
|
}
|
|
|
|
if (_textures.Count >= _textureCount)
|
|
{
|
|
Bake();
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public bool MissingTexture(AvatarTextureIndex index)
|
|
{
|
|
Logger.DebugLog(String.Format("Missing texture {0} in bake {1}", index, _bakeType), _client);
|
|
_textureCount--;
|
|
|
|
if (_textures.Count >= _textureCount)
|
|
{
|
|
Bake();
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
protected void Bake()
|
|
{
|
|
_bakedTexture = new AssetTexture(new ManagedImage(_bakeWidth, _bakeHeight,
|
|
ManagedImage.ImageChannels.Color | ManagedImage.ImageChannels.Alpha | ManagedImage.ImageChannels.Bump));
|
|
|
|
if (_bakeType == BakeType.Eyes)
|
|
{
|
|
InitBakedLayerColor(255, 255, 255);
|
|
DrawLayer(AvatarTextureIndex.EyesIris);
|
|
}
|
|
else if (_bakeType == BakeType.Head)
|
|
{
|
|
// FIXME: Need to use the visual parameters to determine the base skin color in RGB but
|
|
// it's not apparent how to define RGB levels from the skin color parameters, so
|
|
// for now use default color for the skin
|
|
InitBakedLayerColor(226, 160, 125);
|
|
DrawLayer(AvatarTextureIndex.HeadBodypaint);
|
|
|
|
// HACK: Bake the eyelashes in if we have them
|
|
ManagedImage eyelashes = LoadAlphaLayer("head_alpha.tga");
|
|
|
|
if (eyelashes != null)
|
|
{
|
|
Logger.DebugLog("Loaded head_alpha.tga, baking in eyelashes");
|
|
DrawLayer(eyelashes, true);
|
|
}
|
|
else
|
|
{
|
|
Logger.Log("head_alpha.tga resource not found, skipping eyelashes", Helpers.LogLevel.Info);
|
|
}
|
|
}
|
|
else if (_bakeType == BakeType.Skirt)
|
|
{
|
|
float skirtRed = 1.0f, skirtGreen = 1.0f, skirtBlue = 1.0f;
|
|
|
|
try
|
|
{
|
|
_paramValues.TryGetValue(VisualParams.Find("skirt_red", "skirt").ParamID, out skirtRed);
|
|
_paramValues.TryGetValue(VisualParams.Find("skirt_green", "skirt").ParamID, out skirtGreen);
|
|
_paramValues.TryGetValue(VisualParams.Find("skirt_blue", "skirt").ParamID, out skirtBlue);
|
|
}
|
|
catch
|
|
{
|
|
Logger.Log("Unable to determine skirt color from visual params", Helpers.LogLevel.Warning, _client);
|
|
}
|
|
|
|
// FIXME: We should be drawing the skirt first, then tinting with the skirt color
|
|
//InitBakedLayerColor((byte)(skirtRed * 255.0f), (byte)(skirtGreen * 255.0f), (byte)(skirtBlue * 255.0f));
|
|
DrawLayer(AvatarTextureIndex.Skirt);
|
|
}
|
|
else if (_bakeType == BakeType.UpperBody)
|
|
{
|
|
InitBakedLayerColor(226, 160, 125);
|
|
DrawLayer(AvatarTextureIndex.UpperBodypaint);
|
|
DrawLayer(AvatarTextureIndex.UpperUndershirt);
|
|
DrawLayer(AvatarTextureIndex.UpperGloves);
|
|
DrawLayer(AvatarTextureIndex.UpperShirt);
|
|
DrawLayer(AvatarTextureIndex.UpperJacket);
|
|
}
|
|
else if (_bakeType == BakeType.LowerBody)
|
|
{
|
|
InitBakedLayerColor(226, 160, 125);
|
|
DrawLayer(AvatarTextureIndex.LowerBodypaint);
|
|
DrawLayer(AvatarTextureIndex.LowerUnderpants);
|
|
DrawLayer(AvatarTextureIndex.LowerSocks);
|
|
DrawLayer(AvatarTextureIndex.LowerShoes);
|
|
DrawLayer(AvatarTextureIndex.LowerPants);
|
|
DrawLayer(AvatarTextureIndex.LowerJacket);
|
|
}
|
|
else if (_bakeType == BakeType.Hair)
|
|
{
|
|
InitBakedLayerColor(255, 255, 255);
|
|
DrawLayer(AvatarTextureIndex.Hair);
|
|
}
|
|
|
|
_bakedTexture.Encode();
|
|
}
|
|
|
|
private bool DrawLayer(AvatarTextureIndex textureIndex)
|
|
{
|
|
AssetTexture texture;
|
|
bool useSourceAlpha =
|
|
(textureIndex == AvatarTextureIndex.HeadBodypaint ||
|
|
textureIndex == AvatarTextureIndex.Skirt);
|
|
|
|
if (_textures.TryGetValue(textureIndex, out texture))
|
|
return DrawLayer(texture.Image, useSourceAlpha);
|
|
else
|
|
return false;
|
|
}
|
|
|
|
private bool DrawLayer(ManagedImage source, bool useSourceAlpha)
|
|
{
|
|
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);
|
|
|
|
useSourceAlpha = (useSourceAlpha && sourceHasAlpha);
|
|
|
|
if (source.Width != _bakeWidth || source.Height != _bakeHeight)
|
|
{
|
|
try { source.ResizeNearestNeighbor(_bakeWidth, _bakeHeight); }
|
|
catch { return false; }
|
|
}
|
|
|
|
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 (useSourceAlpha)
|
|
bakedAlpha[i] = sourceAlpha[i];
|
|
|
|
if (sourceHasBump)
|
|
bakedBump[i] = sourceBump[i];
|
|
|
|
++i;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
/// <param name="r">Red value</param>
|
|
/// <param name="g">Green value</param>
|
|
/// <param name="b">Blue value</param>
|
|
private void InitBakedLayerColor(byte r, byte g, byte b)
|
|
{
|
|
byte rByte = r;
|
|
byte gByte = g;
|
|
byte bByte = b;
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
public static ManagedImage LoadAlphaLayer(string fileName)
|
|
{
|
|
Stream stream = Helpers.GetResourceStream(fileName, Settings.RESOURCE_DIR);
|
|
|
|
if (stream != null)
|
|
{
|
|
try
|
|
{
|
|
// FIXME: It would save cycles and memory if we wrote a direct
|
|
// loader to ManagedImage for these files instead of using the
|
|
// TGA loader
|
|
Bitmap bitmap = LoadTGAClass.LoadTGA(stream);
|
|
stream.Close();
|
|
|
|
ManagedImage alphaLayer = new ManagedImage(bitmap);
|
|
|
|
// Disable all layers except the alpha layer
|
|
alphaLayer.Red = null;
|
|
alphaLayer.Green = null;
|
|
alphaLayer.Blue = null;
|
|
alphaLayer.Channels = ManagedImage.ImageChannels.Alpha;
|
|
|
|
return alphaLayer;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Logger.Log(String.Format("LoadAlphaLayer() failed on file: {0} ({1}", fileName, e.Message),
|
|
Helpers.LogLevel.Error, e);
|
|
return null;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return null;
|
|
}
|
|
|
|
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|