712 lines
29 KiB
C#
712 lines
29 KiB
C#
/*
|
|
* Copyright (c) 2007-2008, openmetaverse.co
|
|
* Copyright (c) 2024-2025, Sjofn LLC.
|
|
* 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.co 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 SkiaSharp;
|
|
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
|
|
{
|
|
#region Properties
|
|
/// <summary>Final baked texture</summary>
|
|
public AssetTexture BakedTexture => bakedTexture;
|
|
|
|
/// <summary>Component layers</summary>
|
|
public List<AppearanceManager.TextureData> Textures => textures;
|
|
|
|
/// <summary>Width of the final baked image and scratchpad</summary>
|
|
public int BakeWidth => bakeWidth;
|
|
|
|
/// <summary>Height of the final baked image and scratchpad</summary>
|
|
public int BakeHeight => bakeHeight;
|
|
|
|
/// <summary>Bake type</summary>
|
|
public BakeType BakeType => bakeType;
|
|
|
|
/// <summary>Is this one of the 3 skin bakes</summary>
|
|
private bool IsSkin => bakeType == BakeType.Head || bakeType == BakeType.LowerBody || bakeType == BakeType.UpperBody;
|
|
|
|
#endregion
|
|
|
|
#region Private fields
|
|
/// <summary>Final baked texture</summary>
|
|
private AssetTexture bakedTexture;
|
|
/// <summary>Component layers</summary>
|
|
private List<AppearanceManager.TextureData> textures = new List<AppearanceManager.TextureData>();
|
|
/// <summary>Width of the final baked image and scratchpad</summary>
|
|
private int bakeWidth;
|
|
/// <summary>Height of the final baked image and scratchpad</summary>
|
|
private int bakeHeight;
|
|
/// <summary>Bake type</summary>
|
|
private BakeType bakeType;
|
|
#endregion
|
|
|
|
#region Constructor
|
|
/// <summary>
|
|
/// Default constructor
|
|
/// </summary>
|
|
/// <param name="bakeType">Bake type</param>
|
|
public Baker(BakeType bakeType)
|
|
{
|
|
this.bakeType = bakeType;
|
|
|
|
if (bakeType == BakeType.Eyes)
|
|
{
|
|
bakeWidth = 128;
|
|
bakeHeight = 128;
|
|
}
|
|
else
|
|
{
|
|
bakeWidth = 1024;
|
|
bakeHeight = 1024;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Public methods
|
|
/// <summary>
|
|
/// Adds layer for baking
|
|
/// </summary>
|
|
/// <param name="tdata">TexturaData struct that contains texture and its params</param>
|
|
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));
|
|
|
|
// These are for head baking, they get special treatment
|
|
AppearanceManager.TextureData skinTexture = new AppearanceManager.TextureData();
|
|
List<AppearanceManager.TextureData> tattooTextures = new List<AppearanceManager.TextureData>();
|
|
List<ManagedImage> alphaWearableTextures = new List<ManagedImage>();
|
|
|
|
// 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);
|
|
}
|
|
|
|
// Sort out the special layers we need for head baking and alpha
|
|
foreach (AppearanceManager.TextureData tex in textures)
|
|
{
|
|
if (tex.Texture == null)
|
|
continue;
|
|
|
|
switch (tex.TextureIndex)
|
|
{
|
|
case AvatarTextureIndex.HeadBodypaint:
|
|
case AvatarTextureIndex.UpperBodypaint:
|
|
case AvatarTextureIndex.LowerBodypaint:
|
|
skinTexture = tex;
|
|
break;
|
|
case AvatarTextureIndex.HeadTattoo:
|
|
case AvatarTextureIndex.UpperTattoo:
|
|
case AvatarTextureIndex.LowerTattoo:
|
|
tattooTextures.Add(tex);
|
|
break;
|
|
}
|
|
|
|
if (tex.TextureIndex >= AvatarTextureIndex.LowerAlpha &&
|
|
tex.TextureIndex <= AvatarTextureIndex.HairAlpha)
|
|
{
|
|
if (tex.Texture.Image.Alpha != null)
|
|
{
|
|
alphaWearableTextures.Add(tex.Texture.Image.Clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bakeType == BakeType.Head)
|
|
{
|
|
if (DrawLayer(LoadResourceLayer("head_color.tga"), false) == true)
|
|
{
|
|
AddAlpha(bakedTexture.Image, LoadResourceLayer("head_alpha.tga"));
|
|
MultiplyLayerFromAlpha(bakedTexture.Image, LoadResourceLayer("head_skingrain.tga"));
|
|
Logger.Log("[Bake]: created head master bake", Helpers.LogLevel.Debug);
|
|
}
|
|
else
|
|
{
|
|
Logger.Log("[Bake]: Unable to draw layer from texture file", Helpers.LogLevel.Debug);
|
|
}
|
|
}
|
|
|
|
if (skinTexture.Texture == null)
|
|
{
|
|
if (bakeType == BakeType.UpperBody)
|
|
{
|
|
DrawLayer(LoadResourceLayer("upperbody_color.tga"), false);
|
|
}
|
|
|
|
if (bakeType == BakeType.LowerBody)
|
|
{
|
|
DrawLayer(LoadResourceLayer("lowerbody_color.tga"), false);
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
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 &&
|
|
(textures[i].TextureIndex == AvatarTextureIndex.HeadBodypaint ||
|
|
textures[i].TextureIndex == AvatarTextureIndex.HeadTattoo))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
ManagedImage texture = textures[i].Texture.Image.Clone();
|
|
//File.WriteAllBytes(bakeType + "-texture-layer-" + textures[i].TextureIndex + "-" + 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.Texture == null && bakeType == BakeType.Head && textures[i].TextureIndex == AvatarTextureIndex.Hair)
|
|
{
|
|
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 (!(textures[i].TextureIndex == AvatarTextureIndex.HeadBodypaint ||
|
|
textures[i].TextureIndex == AvatarTextureIndex.UpperBodypaint ||
|
|
textures[i].TextureIndex == AvatarTextureIndex.LowerBodypaint))
|
|
{
|
|
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<VisualAlphaParam, float> 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<VisualAlphaParam, float> kvp in textures[i].AlphaMasks)
|
|
{
|
|
if (!MaskBelongsToBake(kvp.Key.TGAFile)) continue;
|
|
|
|
if (kvp.Key.MultiplyBlend && (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 (bakeType == BakeType.Head)
|
|
{
|
|
if (skinTexture.Texture != null)
|
|
{
|
|
ManagedImage texture = skinTexture.Texture.Image.Clone();
|
|
if (texture.Width != bakeWidth || texture.Height != bakeHeight)
|
|
{
|
|
try { texture.ResizeNearestNeighbor(bakeWidth, bakeHeight); }
|
|
catch (Exception) { }
|
|
}
|
|
DrawLayer(texture, false);
|
|
}
|
|
|
|
foreach (AppearanceManager.TextureData tex in tattooTextures)
|
|
{
|
|
// Add head tattoo here (if available, order-dependant)
|
|
if (tex.Texture != null)
|
|
{
|
|
ManagedImage texture = tex.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
|
|
Logger.Log("[XBakes]: Number of alpha wearable textures: " + alphaWearableTextures.Count, Helpers.LogLevel.Debug);
|
|
foreach (ManagedImage img in alphaWearableTextures)
|
|
AddAlpha(bakedTexture.Image, img);
|
|
|
|
// 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
|
|
{
|
|
SKBitmap bitmap = null;
|
|
lock (ResourceSync)
|
|
{
|
|
using (Stream stream = Helpers.GetResourceStream(fileName, Path.Combine(Settings.RESOURCE_DIR, "static_assets")))
|
|
{
|
|
if (stream != null)
|
|
{
|
|
bitmap = Targa.Decode(stream);
|
|
}
|
|
}
|
|
}
|
|
if (bitmap == null)
|
|
{
|
|
Logger.Log($"Failed loading resource file: {fileName}", Helpers.LogLevel.Error);
|
|
return null;
|
|
}
|
|
else
|
|
{
|
|
return new ManagedImage(bitmap);
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Logger.Log($"Failed loading resource file: {fileName} ({e.Message})",
|
|
Helpers.LogLevel.Error, e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts avatar texture index (face) to Bake type
|
|
/// </summary>
|
|
/// <param name="index">Face number (AvatarTextureIndex)</param>
|
|
/// <returns>BakeType, layer to which this texture belongs to</returns>
|
|
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)
|
|
{
|
|
return (bakeType != BakeType.LowerBody || !mask.Contains("upper"))
|
|
&& (bakeType != BakeType.LowerBody || !mask.Contains("shirt"))
|
|
&& (bakeType != BakeType.UpperBody || !mask.Contains("lower"));
|
|
}
|
|
|
|
private bool DrawLayer(ManagedImage source, bool addSourceAlpha)
|
|
{
|
|
if (source == null) return false;
|
|
|
|
int i = 0;
|
|
|
|
var sourceHasColor = ((source.Channels & ManagedImage.ImageChannels.Color) != 0 &&
|
|
source.Red != null && source.Green != null && source.Blue != null);
|
|
var sourceHasAlpha = ((source.Channels & ManagedImage.ImageChannels.Alpha) != 0 && source.Alpha != null);
|
|
var 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;
|
|
|
|
bool loadedAlpha = false;
|
|
for (int y = 0; y < bakeHeight; y++)
|
|
{
|
|
for (int x = 0; x < bakeWidth; x++)
|
|
{
|
|
loadedAlpha = false;
|
|
alpha = 0;
|
|
alphaInv = 0;
|
|
|
|
if (sourceHasAlpha)
|
|
{
|
|
if (sourceAlpha.Length > i)
|
|
{
|
|
loadedAlpha = true;
|
|
alpha = sourceAlpha[i];
|
|
alphaInv = (byte)(byte.MaxValue - alpha);
|
|
}
|
|
}
|
|
|
|
if (sourceHasColor)
|
|
{
|
|
if ((bakedRed.Length > i) && (bakedGreen.Length > i) && (bakedBlue.Length > i))
|
|
{
|
|
if ((sourceRed.Length > i) && (sourceGreen.Length > i) && (sourceBlue.Length > i))
|
|
{
|
|
if (loadedAlpha == true)
|
|
{
|
|
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);
|
|
}
|
|
else
|
|
{
|
|
bakedRed[i] = sourceRed[i];
|
|
bakedGreen[i] = sourceGreen[i];
|
|
bakedBlue[i] = sourceBlue[i];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (addSourceAlpha)
|
|
{
|
|
if ((sourceAlpha.Length > i) && (bakedAlpha.Length > i))
|
|
{
|
|
if (sourceAlpha[i] < bakedAlpha[i])
|
|
{
|
|
bakedAlpha[i] = sourceAlpha[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (sourceHasBump)
|
|
{
|
|
if (sourceBump.Length > i)
|
|
{
|
|
bakedBump[i] = sourceBump[i];
|
|
}
|
|
}
|
|
|
|
++i;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Make sure images exist, resize source if needed to match the destination
|
|
/// </summary>
|
|
/// <param name="dest">Destination image</param>
|
|
/// <param name="src">Source image</param>
|
|
/// <returns>Sanitization was successful</returns>
|
|
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?.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 (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] * Utils.FloatToByte(src.R, 0f, 1f)) >> 8);
|
|
dest.Green[i] = (byte)((dest.Green[i] * Utils.FloatToByte(src.G, 0f, 1f)) >> 8);
|
|
dest.Blue[i] = (byte)((dest.Blue[i] * Utils.FloatToByte(src.B, 0f, 1f)) >> 8);
|
|
}
|
|
}
|
|
|
|
/// <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="color">Color of the base of this layer</param>
|
|
private void InitBakedLayerColor(Color4 color)
|
|
{
|
|
InitBakedLayerColor(color.R, color.G, color.B);
|
|
}
|
|
|
|
/// <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(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);
|
|
|
|
var rAlt = rByte;
|
|
var gAlt = gByte;
|
|
var 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
|
|
}
|
|
}
|