mirror of
https://github.com/wega-team/ss14-wega.git
synced 2026-06-09 10:06:49 +02:00
Hue based skin color clamping (#43889)
* works!!!!!!!!!! * rewrite it AGAIN * wtf * vox coloring * fuck this comment in particular * Apply suggestions from code review Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * docs * review p2 * review? * review from stinker * stuff * i SWEAR these are supposed to be reversed right??? * more clear test errors please * ig this was correct???? * reason * unused * review * cleanup * i was a silly boy * grah * fix gametest locally --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
This commit is contained in:
@@ -250,6 +250,7 @@ public abstract partial class GameTest
|
||||
catch (Exception)
|
||||
{
|
||||
_pairDestroyed = true;
|
||||
Assert.Fail();
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Content.IntegrationTests.Fixtures;
|
||||
using Content.IntegrationTests.Fixtures.Attributes;
|
||||
using Content.IntegrationTests.Utility;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.Humanoid;
|
||||
@@ -21,10 +23,10 @@ public sealed class HumanoidProfileTests : GameTest
|
||||
private static readonly ProtoId<SpeciesPrototype> Vox = "Vox";
|
||||
private static string[] _species = GameDataScrounger.PrototypesOfKind<SpeciesPrototype>();
|
||||
|
||||
private BodySystem _bodySystem;
|
||||
private HumanoidProfileSystem _humanoidProfile;
|
||||
private MarkingManager _markingManager;
|
||||
private SharedVisualBodySystem _visualBody;
|
||||
[SidedDependency(Side.Server)] private BodySystem _bodySystem = default!;
|
||||
[SidedDependency(Side.Server)] private HumanoidProfileSystem _humanoidProfile = default!;
|
||||
[SidedDependency(Side.Server)] private MarkingManager _markingManager = default!;
|
||||
[SidedDependency(Side.Server)] private SharedVisualBodySystem _visualBody = default!;
|
||||
|
||||
[Test]
|
||||
public async Task EnsureValidLoading()
|
||||
@@ -36,17 +38,15 @@ public sealed class HumanoidProfileTests : GameTest
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var entityManager = server.ResolveDependency<IEntityManager>();
|
||||
var humanoidProfile = entityManager.System<HumanoidProfileSystem>();
|
||||
var human = entityManager.Spawn(BaseSpecies);
|
||||
humanoidProfile.ApplyProfileTo(human,
|
||||
LoadDependencies(out var body, out var humanoidComponent);
|
||||
|
||||
_humanoidProfile.ApplyProfileTo(body,
|
||||
new HumanoidCharacterProfile()
|
||||
.WithSex(Sex.Female)
|
||||
.WithAge(67)
|
||||
.WithGender(Gender.Neuter)
|
||||
.WithSpecies(Vox));
|
||||
var humanoidComponent = entityManager.GetComponent<HumanoidProfileComponent>(human);
|
||||
var voiceComponent = entityManager.GetComponent<VocalComponent>(human);
|
||||
var voiceComponent = SEntMan.GetComponent<VocalComponent>(body);
|
||||
|
||||
Assert.That(humanoidComponent.Age, Is.EqualTo(67));
|
||||
Assert.That(humanoidComponent.Sex, Is.EqualTo(Sex.Female));
|
||||
@@ -71,6 +71,7 @@ public sealed class HumanoidProfileTests : GameTest
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
LoadDependencies(out var body, out var humanoidComponent);
|
||||
|
||||
var profile = HumanoidCharacterProfile.Random();
|
||||
_humanoidProfile.ApplyProfileTo(body, profile);
|
||||
_visualBody.ApplyProfileTo(body, profile);
|
||||
@@ -91,17 +92,17 @@ public sealed class HumanoidProfileTests : GameTest
|
||||
{
|
||||
LoadDependencies(out var body, out var humanoidComponent);
|
||||
|
||||
var proto = Server.ProtoMan.Index<SpeciesPrototype>(species);
|
||||
var proto = SProtoMan.Index<SpeciesPrototype>(species);
|
||||
var profile = HumanoidCharacterProfile.RandomWithSpecies(species);
|
||||
_humanoidProfile.ApplyProfileTo(body, profile);
|
||||
_visualBody.ApplyProfileTo(body, profile);
|
||||
|
||||
Assert.That(humanoidComponent.Age, Is.LessThanOrEqualTo(proto.MaxAge));
|
||||
Assert.That(humanoidComponent.Age, Is.GreaterThanOrEqualTo(proto.MinAge));
|
||||
Assert.That(proto.Sexes.Contains(humanoidComponent.Sex), Is.True);
|
||||
Assert.That(humanoidComponent.Species, Is.EqualTo(species));
|
||||
Assert.That(humanoidComponent.Age, Is.LessThanOrEqualTo(proto.MaxAge), $"Expected age is above the maximum age limit! Current: {humanoidComponent.Age} Max: {proto.MaxAge}");
|
||||
Assert.That(humanoidComponent.Age, Is.GreaterThanOrEqualTo(proto.MinAge), $"Expected age is below the minimum age limit! Current: {humanoidComponent.Age} Min: {proto.MinAge}");
|
||||
Assert.That(proto.Sexes.Contains(humanoidComponent.Sex), Is.True, $"Character has sex not found in the species prototype! Current: {humanoidComponent.Sex}");
|
||||
Assert.That(humanoidComponent.Species, Is.EqualTo(species), $"Species does not match! Expected: {species} Current: {humanoidComponent.Species}");
|
||||
var strategy = Server.ProtoMan.Index(proto.SkinColoration).Strategy;
|
||||
Assert.That(strategy.VerifySkinColor(profile.Appearance.SkinColor), Is.True);
|
||||
Assert.That(strategy.VerifySkinColor(profile.Appearance.SkinColor, out var reason), Is.True, $"Failed to verify the skin color from strategy {strategy}. Reason: {reason}");
|
||||
|
||||
AssertValidProfile((body, humanoidComponent), profile);
|
||||
});
|
||||
@@ -109,29 +110,24 @@ public sealed class HumanoidProfileTests : GameTest
|
||||
|
||||
private void LoadDependencies(out EntityUid body, out HumanoidProfileComponent humanoidComponent)
|
||||
{
|
||||
var entityManager = Server.ResolveDependency<IEntityManager>();
|
||||
_humanoidProfile = entityManager.System<HumanoidProfileSystem>();
|
||||
_markingManager = Server.ResolveDependency<MarkingManager>();
|
||||
_visualBody = entityManager.System<SharedVisualBodySystem>();
|
||||
_bodySystem = entityManager.System<BodySystem>();
|
||||
body = entityManager.Spawn(BaseSpecies);
|
||||
humanoidComponent = entityManager.GetComponent<HumanoidProfileComponent>(body);
|
||||
body = SEntMan.Spawn(BaseSpecies);
|
||||
humanoidComponent = SEntMan.GetComponent<HumanoidProfileComponent>(body);
|
||||
}
|
||||
|
||||
private void AssertValidProfile(Entity<HumanoidProfileComponent> body, HumanoidCharacterProfile profile)
|
||||
{
|
||||
_bodySystem.TryGetOrgansWithComponent<VisualOrganComponent>(body.Owner, out var organs);
|
||||
|
||||
foreach (var (_, visualOrgan) in organs)
|
||||
foreach (var (uid, visualOrgan) in organs)
|
||||
{
|
||||
Assert.That(visualOrgan.Profile.Sex, Is.EqualTo(profile.Sex));
|
||||
Assert.That(visualOrgan.Profile.EyeColor, Is.EqualTo(profile.Appearance.EyeColor));
|
||||
Assert.That(visualOrgan.Profile.SkinColor, Is.EqualTo(profile.Appearance.SkinColor));
|
||||
Assert.That(visualOrgan.Profile.Sex, Is.EqualTo(profile.Sex), $"Organ {uid} has invalid sex appearance! Expected: {profile.Sex} Current: {visualOrgan.Profile.Sex}");
|
||||
Assert.That(visualOrgan.Profile.EyeColor, Is.EqualTo(profile.Appearance.EyeColor), $"Organ {uid} has invalid eye color! Expected: {profile.Appearance.EyeColor} Current: {visualOrgan.Profile.EyeColor}");
|
||||
Assert.That(visualOrgan.Profile.SkinColor, Is.EqualTo(profile.Appearance.SkinColor), $"Organ {uid} has invalid skin color! Expected: {profile.Appearance.SkinColor} Current: {visualOrgan.Profile.SkinColor}");
|
||||
}
|
||||
|
||||
_bodySystem.TryGetOrgansWithComponent<VisualOrganMarkingsComponent>(body.Owner, out var markings);
|
||||
|
||||
foreach (var (_, markingOrgan) in markings)
|
||||
foreach (var (uid, markingOrgan) in markings)
|
||||
{
|
||||
// Needed to avoid access restrictions
|
||||
var data = markingOrgan.MarkingData;
|
||||
@@ -143,9 +139,9 @@ public sealed class HumanoidProfileTests : GameTest
|
||||
{
|
||||
var markingProto = Server.ProtoMan.Index(marking.MarkingId);
|
||||
|
||||
Assert.That(markingProto.Sprites.Count, Is.EqualTo(marking.MarkingColors.Count));
|
||||
Assert.That(_markingManager.CanBeApplied(data.Group, profile.Sex, markingProto), Is.True);
|
||||
Assert.That(data.Layers.Contains(markingProto.BodyPart), Is.True);
|
||||
Assert.That(markingProto.Sprites.Count, Is.EqualTo(marking.MarkingColors.Count), $"Organ {uid} has invald amount of marking sprites! Expected: {marking.MarkingColors.Count} Current: {markingProto.Sprites.Count}");
|
||||
Assert.That(_markingManager.CanBeApplied(data.Group, profile.Sex, markingProto), Is.True, $"Marking {markingProto.ID} cannot be applied to group {data.Group.Id} with sex {profile.Sex}");
|
||||
Assert.That(data.Layers.Contains(markingProto.BodyPart), Is.True, $"Organ {uid} marking visual layers do not contain an entry for {markingProto.BodyPart}");
|
||||
if (!markingProto.ForcedColoring && groupProto.Appearances.GetValueOrDefault(markingProto.BodyPart)?.MatchSkin != true)
|
||||
freeMarkings.Add(marking);
|
||||
|
||||
@@ -172,7 +168,7 @@ public sealed class HumanoidProfileTests : GameTest
|
||||
Is.EqualTo(MarkingColoring.GetMarkingLayerColors(markingProto, profile.Appearance.SkinColor, profile.Appearance.EyeColor, markingOrgan.AppliedMarkings)));
|
||||
|
||||
if (markingProto.SexRestriction != null)
|
||||
Assert.That(markingProto.SexRestriction, Is.EqualTo(profile.Sex));
|
||||
Assert.That(markingProto.SexRestriction, Is.EqualTo(profile.Sex), $"Marking {markingProto.ID} has invalid sex restriction! Expected: {profile.Sex} Current: {markingProto.SexRestriction}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
@@ -51,8 +51,9 @@ public interface ISkinColorationStrategy
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether or not the provided <see cref="Color" /> is within bounds of this strategy
|
||||
/// Outs a reason if the verification fails.
|
||||
/// </summary>
|
||||
bool VerifySkinColor(Color color);
|
||||
bool VerifySkinColor(Color color, [NotNullWhen(false)] out string? reason);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the closest skin color that this strategy would provide to the given <see cref="Color" />
|
||||
@@ -64,7 +65,7 @@ public interface ISkinColorationStrategy
|
||||
/// </summary>
|
||||
Color EnsureVerified(Color color)
|
||||
{
|
||||
if (VerifySkinColor(color))
|
||||
if (VerifySkinColor(color, out _))
|
||||
{
|
||||
return color;
|
||||
}
|
||||
@@ -101,8 +102,10 @@ public sealed partial class HumanTonedSkinColoration : ISkinColorationStrategy
|
||||
|
||||
public SkinColorationStrategyInput InputType => SkinColorationStrategyInput.Unary;
|
||||
|
||||
public bool VerifySkinColor(Color color)
|
||||
public bool VerifySkinColor(Color color, [NotNullWhen(false)] out string? reason)
|
||||
{
|
||||
reason = null;
|
||||
|
||||
var colorValues = Color.ToHsv(color);
|
||||
|
||||
var hue = Math.Round(colorValues.X * 360f);
|
||||
@@ -112,6 +115,7 @@ public sealed partial class HumanTonedSkinColoration : ISkinColorationStrategy
|
||||
// is 25 <= hue <= 45
|
||||
if (hue < 25f || hue > 45f)
|
||||
{
|
||||
reason = $"Hue {hue} is outside of expected ranges 25 and 45.";
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -120,6 +124,7 @@ public sealed partial class HumanTonedSkinColoration : ISkinColorationStrategy
|
||||
// where saturation increases to 100 and value decreases to 20
|
||||
if (sat < 20f || val < 20f)
|
||||
{
|
||||
reason = "Saturation or value are below expected number of 20.";
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -212,18 +217,29 @@ public sealed partial class ClampedHsvColoration : ISkinColorationStrategy
|
||||
|
||||
public SkinColorationStrategyInput InputType => SkinColorationStrategyInput.Color;
|
||||
|
||||
public bool VerifySkinColor(Color color)
|
||||
public bool VerifySkinColor(Color color, [NotNullWhen(false)] out string? reason)
|
||||
{
|
||||
reason = null;
|
||||
|
||||
var hsv = Color.ToHsv(color);
|
||||
|
||||
if (Hue is (var minHue, var maxHue) && !SkinColorationUtils.IsHueInRange(hsv.X, minHue, maxHue))
|
||||
{
|
||||
reason = $"Hue {Hue} is outside of range of min {minHue} max {maxHue}";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Saturation is (var minSat, var maxSat) && (hsv.Y < minSat - SkinColorationUtils.Epsilon || hsv.Y > maxSat + SkinColorationUtils.Epsilon))
|
||||
{
|
||||
reason = $"Saturation {Saturation} is outside of range of min {minSat} max {maxSat}";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Value is (var minVal, var maxVal) && (hsv.Z < minVal - SkinColorationUtils.Epsilon || hsv.Z > maxVal + SkinColorationUtils.Epsilon))
|
||||
{
|
||||
reason = $"Value {Value} is outside of range of min {minVal} max {maxVal}";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -271,18 +287,29 @@ public sealed partial class ClampedHslColoration : ISkinColorationStrategy
|
||||
|
||||
public SkinColorationStrategyInput InputType => SkinColorationStrategyInput.Color;
|
||||
|
||||
public bool VerifySkinColor(Color color)
|
||||
public bool VerifySkinColor(Color color, [NotNullWhen(false)] out string? reason)
|
||||
{
|
||||
reason = null;
|
||||
|
||||
var hsl = Color.ToHsl(color);
|
||||
|
||||
if (Hue is (var minHue, var maxHue) && !SkinColorationUtils.IsHueInRange(hsl.X, minHue, maxHue))
|
||||
{
|
||||
reason = $"Hue {Hue} is outside of range of min {minHue} max {maxHue}";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Saturation is (var minSat, var maxSat) && (hsl.Y < minSat - SkinColorationUtils.Epsilon || hsl.Y > maxSat + SkinColorationUtils.Epsilon))
|
||||
{
|
||||
reason = $"Saturation {Saturation} is outside of range of min {minSat} max {maxSat}";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Lightness is (var minLight, var maxLight) && (hsl.Z < minLight - SkinColorationUtils.Epsilon || hsl.Z > maxLight + SkinColorationUtils.Epsilon))
|
||||
{
|
||||
reason = $"Lightness {Lightness} is outside of range of min {minLight} max {maxLight}";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -302,6 +329,177 @@ public sealed partial class ClampedHslColoration : ISkinColorationStrategy
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Coloration strategy that clamps the color between nodes within the HSV colorspace.
|
||||
/// Clamped values depend on the nodes and are linearly interpolated between them.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For example:
|
||||
/// A node at hue 0 with a saturation of [0,1] and a node at hue 1 with a staturation of [0.1,0.8]
|
||||
/// At hue 0.5, the saturation would be clamped within [0.05,0.9]
|
||||
/// </remarks>
|
||||
[DataDefinition]
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class HueNodeClampedHsvColoration : ISkinColorationStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// List of valid nodes in this coloration.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public List<HueNodeClampedHsvColorationNode> Nodes = default!;
|
||||
|
||||
public SkinColorationStrategyInput InputType => SkinColorationStrategyInput.Color;
|
||||
|
||||
public bool VerifySkinColor(Color color, [NotNullWhen(false)] out string? reason)
|
||||
{
|
||||
reason = null;
|
||||
var hsv = Color.ToHsv(color);
|
||||
|
||||
// Clamp the hue between the first and last node.
|
||||
// We don't want anything going outside of these values.
|
||||
var hue = SkinColorationUtils.ClampHue(hsv.X, Nodes.First().Hue, Nodes.Last().Hue);
|
||||
|
||||
var range = GetNodeValuesForHue(hue);
|
||||
|
||||
// If no range was found, this color is invalid.
|
||||
if (range is null)
|
||||
{
|
||||
reason = "No valid range was found.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// If a range is found, check if the saturation is within the provided ranges.
|
||||
if (hsv.Y < range.Saturation.Min - SkinColorationUtils.Epsilon || hsv.Y > range.Saturation.Max + SkinColorationUtils.Epsilon)
|
||||
{
|
||||
reason = $"Saturation {hsv.Y} is outside of range of min {range.Saturation.Item1} max {range.Saturation.Item2}";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the value is within provided ranges.
|
||||
if (hsv.Z < range.Value.Min - SkinColorationUtils.Epsilon || hsv.Z > range.Value.Max + SkinColorationUtils.Epsilon)
|
||||
{
|
||||
reason = $"Value {hsv.Z} is outside of range of min {range.Value.Min} max {range.Value.Max}";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Color ClosestSkinColor(Color color)
|
||||
{
|
||||
var hsv = Color.ToHsv(color);
|
||||
|
||||
// Clamp within specified nodes.
|
||||
hsv.X = SkinColorationUtils.ClampHue(hsv.X, Nodes.First().Hue, Nodes.Last().Hue);
|
||||
|
||||
var range = GetNodeValuesForHue(hsv.X);
|
||||
if (range == null)
|
||||
return color;
|
||||
|
||||
hsv.Y = Math.Clamp(hsv.Y, range.Saturation.Min, range.Saturation.Max);
|
||||
hsv.Z = Math.Clamp(hsv.Z, range.Value.Min, range.Value.Max);
|
||||
|
||||
return Color.FromHsv(hsv);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the nodes affecting value and saturation at a specified hue value.
|
||||
/// </summary>
|
||||
/// <param name="hue">The hue value at which to find the nodes. Between 0 and 1.</param>
|
||||
/// <returns>The nodes taking effect at the specified hue.</returns>
|
||||
private (HueNodeClampedHsvColorationNode Prev, HueNodeClampedHsvColorationNode Next)? GetAffectingNodes(float hue)
|
||||
{
|
||||
if (Nodes.Count == 0)
|
||||
return null;
|
||||
|
||||
// If only one node is provided we just consider it to control all values.
|
||||
if (Nodes.Count == 1)
|
||||
return (Nodes.First(), Nodes.Last());
|
||||
|
||||
for (int i = 0; i < Nodes.Count; i++)
|
||||
{
|
||||
// We get the currently iterated element.
|
||||
var current = Nodes[i];
|
||||
|
||||
// If there is no element after this one, we just loop back to the first element.
|
||||
// Basically a node list of [0, 0.5] will fall back to node at 0 if the hue is ever higher than 0.5
|
||||
var next = Nodes.ElementAtOrDefault(i + 1) ?? Nodes.First();
|
||||
|
||||
// Is the hue within the range of the nodes we're considering?
|
||||
if (!SkinColorationUtils.IsHueInRange(hue, current.Hue, next.Hue))
|
||||
continue;
|
||||
|
||||
return (current, next);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the values at which to clamp the value and saturation based on the given hue.
|
||||
/// </summary>
|
||||
/// <param name="hue">The hue for which to get the clamping values. Between 0 and 1.</param>
|
||||
/// <returns>Node containing the value and saturation clamping.</returns>
|
||||
private HueNodeClampedHsvColorationNode? GetNodeValuesForHue(float hue)
|
||||
{
|
||||
if (Nodes.Count == 0)
|
||||
return null;
|
||||
|
||||
// No node is actually affecting this coloring, so it's invalid.
|
||||
if (GetAffectingNodes(hue) is not { } affectingNodes)
|
||||
return null;
|
||||
|
||||
var firstNode = affectingNodes.Prev;
|
||||
var secondNode = affectingNodes.Next;
|
||||
|
||||
// If both values are equal we just return 0f.
|
||||
// This is to prevent dividing by 0.
|
||||
var weight = MathHelper.CloseTo(firstNode.Hue, secondNode.Hue) ? 0f : (hue - firstNode.Hue) / (secondNode.Hue - firstNode.Hue);
|
||||
|
||||
// I know this is also used to define the nodes, however it contains all the data necessary
|
||||
// And I don't think creating a new DataDefinition is worth it just to get rid of the hue from this one.
|
||||
var finalNode = new HueNodeClampedHsvColorationNode();
|
||||
finalNode.Hue = hue;
|
||||
|
||||
finalNode.Saturation.Min = MathHelper.Lerp(firstNode.Saturation.Min, secondNode.Saturation.Min, weight);
|
||||
finalNode.Saturation.Max = MathHelper.Lerp(firstNode.Saturation.Max, secondNode.Saturation.Max, weight);
|
||||
|
||||
finalNode.Value.Min = MathHelper.Lerp(firstNode.Value.Min, secondNode.Value.Min, weight);
|
||||
finalNode.Value.Max = MathHelper.Lerp(firstNode.Value.Max, secondNode.Value.Max, weight);
|
||||
|
||||
return finalNode;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A node to be used with <see cref="HueNodeClampedHsvColoration"/>.
|
||||
/// Represents a single point on the hue spectrum with corresponding clamping limits for saturation and value.
|
||||
/// </summary>
|
||||
[DataDefinition]
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class HueNodeClampedHsvColorationNode
|
||||
{
|
||||
/// <summary>
|
||||
/// The point on the hue spectrum where this node is placed.
|
||||
/// Between 0 and 1.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float Hue;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the (min, max) saturation on the provided node.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public (float Min, float Max) Saturation;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the (min, max) value on the provided node.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public (float Min, float Max) Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains shared utility methods for handling color manipulations in skin coloration strategies.
|
||||
/// </summary>
|
||||
|
||||
@@ -43,7 +43,7 @@ public sealed class SkinTonesTest
|
||||
{
|
||||
var unaryInput = i / 100f; // Test values like 0.0, 0.01, ..., 100.0
|
||||
var color = strategy.FromUnary(unaryInput);
|
||||
Assert.That(strategy.VerifySkinColor(color), $"Color {color} from unary value {unaryInput} failed verification.");
|
||||
Assert.That(strategy.VerifySkinColor(color, out var reason), $"Color {color} from unary value {unaryInput} failed verification. Reason: {reason}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ public sealed class SkinTonesTest
|
||||
public void TestDefaultHumanSkinToneValid()
|
||||
{
|
||||
var strategy = new HumanTonedSkinColoration();
|
||||
Assert.That(strategy.VerifySkinColor(strategy.ValidHumanSkinTone));
|
||||
Assert.That(strategy.VerifySkinColor(strategy.ValidHumanSkinTone, out _));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -98,8 +98,8 @@ public sealed class SkinTonesTest
|
||||
var skinColor = strategy.ClosestSkinColor(color);
|
||||
LogDriftIfGreater(strategy, color, skinColor, TestContext.CurrentContext.Test.Name); // Monitor drift
|
||||
|
||||
Assert.That(strategy.VerifySkinColor(skinColor),
|
||||
$"Color {skinColor} (from input {color}) failed verification in {TestContext.CurrentContext.Test.Name} on iteration {i}");
|
||||
Assert.That(strategy.VerifySkinColor(skinColor, out var reason),
|
||||
$"Color {skinColor} (from input {color}) failed verification in {TestContext.CurrentContext.Test.Name} on iteration {i}. Reason: {reason}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,8 +122,8 @@ public sealed class SkinTonesTest
|
||||
var skinColor = strategy.ClosestSkinColor(color);
|
||||
LogDriftIfGreater(strategy, color, skinColor, TestContext.CurrentContext.Test.Name); // Monitor drift
|
||||
|
||||
Assert.That(strategy.VerifySkinColor(skinColor),
|
||||
$"Color {skinColor} (from input {color}) failed verification in {TestContext.CurrentContext.Test.Name} on iteration {i}");
|
||||
Assert.That(strategy.VerifySkinColor(skinColor, out var reason),
|
||||
$"Color {skinColor} (from input {color}) failed verification in {TestContext.CurrentContext.Test.Name} on iteration {i}. Reason: {reason}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,8 +147,8 @@ public sealed class SkinTonesTest
|
||||
var skinColor = strategy.ClosestSkinColor(color);
|
||||
LogDriftIfGreater(strategy, color, skinColor, TestContext.CurrentContext.Test.Name); // Monitor drift
|
||||
|
||||
Assert.That(strategy.VerifySkinColor(skinColor),
|
||||
$"Color {skinColor} (from input {color}) failed verification in {TestContext.CurrentContext.Test.Name} on iteration {i}");
|
||||
Assert.That(strategy.VerifySkinColor(skinColor, out var reason),
|
||||
$"Color {skinColor} (from input {color}) failed verification in {TestContext.CurrentContext.Test.Name} on iteration {i}. Reason: {reason}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,8 +172,8 @@ public sealed class SkinTonesTest
|
||||
var skinColor = strategy.ClosestSkinColor(color);
|
||||
LogDriftIfGreater(strategy, color, skinColor, TestContext.CurrentContext.Test.Name); // Monitor drift
|
||||
|
||||
Assert.That(strategy.VerifySkinColor(skinColor),
|
||||
$"Color {skinColor} (from input {color}) failed verification in {TestContext.CurrentContext.Test.Name} on iteration {i}");
|
||||
Assert.That(strategy.VerifySkinColor(skinColor, out var reason),
|
||||
$"Color {skinColor} (from input {color}) failed verification in {TestContext.CurrentContext.Test.Name} on iteration {i}. Reason: {reason}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,8 +197,8 @@ public sealed class SkinTonesTest
|
||||
var skinColor = strategy.ClosestSkinColor(color);
|
||||
LogDriftIfGreater(strategy, color, skinColor, TestContext.CurrentContext.Test.Name); // Monitor drift
|
||||
|
||||
Assert.That(strategy.VerifySkinColor(skinColor),
|
||||
$"Color {skinColor} (from input {color}) with circular hue failed verification in {TestContext.CurrentContext.Test.Name} on iteration {i}");
|
||||
Assert.That(strategy.VerifySkinColor(skinColor, out var reason),
|
||||
$"Color {skinColor} (from input {color}) with circular hue failed verification in {TestContext.CurrentContext.Test.Name} on iteration {i}. Reason: {reason}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ public sealed class SkinTonesTest
|
||||
var validColor = Color.FromHsl(new Vector4(0.5f, 0.5f, 0.5f, 1.0f));
|
||||
var result = strategy.ClosestSkinColor(validColor);
|
||||
|
||||
Assert.That(strategy.VerifySkinColor(result), Is.True);
|
||||
Assert.That(strategy.VerifySkinColor(result, out _), Is.True);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -236,7 +236,7 @@ public sealed class SkinTonesTest
|
||||
var invalidColor = Color.FromHsl(new Vector4(0.5f, 0.9f, 0.2f, 1.0f));
|
||||
var result = strategy.ClosestSkinColor(invalidColor);
|
||||
|
||||
Assert.That(strategy.VerifySkinColor(result), Is.True);
|
||||
Assert.That(strategy.VerifySkinColor(result, out _), Is.True);
|
||||
Assert.That(result, Is.Not.EqualTo(invalidColor));
|
||||
}
|
||||
|
||||
|
||||
@@ -11,10 +11,23 @@
|
||||
|
||||
- type: skinColoration
|
||||
id: VoxFeathers
|
||||
strategy: !type:ClampedHsvColoration
|
||||
hue: [0.081, 0.48]
|
||||
strategy: !type:HueNodeClampedHsvColoration
|
||||
nodes:
|
||||
- hue: 0
|
||||
saturation: [ 0.2, 0.5 ]
|
||||
value: [ 0.36, 0.55 ]
|
||||
- hue: 0.045
|
||||
saturation: [ 0.2, 0.8 ]
|
||||
value: [ 0.36, 0.55 ]
|
||||
- hue: 0.44
|
||||
saturation: [ 0.2, 0.8 ]
|
||||
value: [ 0.36, 0.55 ]
|
||||
- hue: 0.55
|
||||
saturation: [ 0.2, 0.5 ]
|
||||
value: [ 0.36, 0.55 ]
|
||||
- hue: 1
|
||||
saturation: [ 0.2, 0.5 ]
|
||||
value: [ 0.36, 0.5 ]
|
||||
|
||||
- type: skinColoration
|
||||
id: HumanToned
|
||||
@@ -22,6 +35,23 @@
|
||||
|
||||
- type: skinColoration
|
||||
id: VulpkaninColors
|
||||
strategy: !type:ClampedHslColoration
|
||||
saturation: [0.0, 0.60]
|
||||
lightness: [0.2, 0.9]
|
||||
strategy: !type:HueNodeClampedHsvColoration
|
||||
nodes:
|
||||
- hue: 0 # From red to orange slowly increase saturation. (0.75 -> 0.9)
|
||||
saturation: [ 0, 0.75 ]
|
||||
value: [ 0.2, 0.9 ]
|
||||
- hue: 0.05 # From orange to yellow, keep it the same. (0.9 -> 0.8)
|
||||
saturation: [ 0, 0.9 ]
|
||||
value: [ 0.2, 0.9 ]
|
||||
- hue: 0.1666 # From yellow to green, slowly decrease it. (0.8 -> 0.65)
|
||||
saturation: [ 0, 0.8 ]
|
||||
value: [ 0.2, 0.9 ]
|
||||
- hue: 0.33 # From green to pink keep it the same. (0.65 -> 0.65)
|
||||
saturation: [ 0, 0.65 ]
|
||||
value: [ 0.2, 0.9 ]
|
||||
- hue: 0.94 # From pink, increase saturation for red. (0.65 -> 0.75)
|
||||
saturation: [ 0, 0.65 ]
|
||||
value: [ 0.2, 0.9 ]
|
||||
- hue: 1 # Red
|
||||
saturation: [ 0, 0.75 ]
|
||||
value: [ 0.2, 0.9 ]
|
||||
|
||||
Reference in New Issue
Block a user