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:
ScarKy0
2026-05-20 23:17:48 +02:00
committed by GitHub
parent 864b741942
commit 1819201cdb
5 changed files with 285 additions and 60 deletions
@@ -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 ]