Skip to content

Commit

Permalink
Implement color blending in /icon.Blend(), ICON_ADD, and `ICON_SU…
Browse files Browse the repository at this point in the history
…BTRACT`
  • Loading branch information
wixoaGit committed Dec 14, 2022
1 parent e902aae commit 7640e98
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 40 deletions.
2 changes: 1 addition & 1 deletion OpenDreamRuntime/DreamManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public void StartWorld() {

// Call global <init> with waitfor=FALSE
if (_compiledJson.GlobalInitProc is ProcDefinitionJson initProcDef) {
var globalInitProc = new DMProc(DreamPath.Root, "(global init)", null, null, null, initProcDef.Bytecode, initProcDef.MaxStackSize, initProcDef.Attributes, initProcDef.VerbName, initProcDef.VerbCategory, initProcDef.VerbDesc, initProcDef.Invisibility, _objectTree);
var globalInitProc = new DMProc(DreamPath.Root, "(global init)", null, null, null, initProcDef.Bytecode, initProcDef.MaxStackSize, initProcDef.Attributes, initProcDef.VerbName, initProcDef.VerbCategory, initProcDef.VerbDesc, initProcDef.Invisibility, _objectTree, _dreamResourceManager);
globalInitProc.Spawn(WorldInstance, new DreamProcArguments());
}

Expand Down
106 changes: 81 additions & 25 deletions OpenDreamRuntime/Objects/DreamIcon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using Color = Robust.Shared.Maths.Color;
using ParsedDMIDescription = OpenDreamShared.Resources.DMIParser.ParsedDMIDescription;
using ParsedDMIState = OpenDreamShared.Resources.DMIParser.ParsedDMIState;
using ParsedDMIFrame = OpenDreamShared.Resources.DMIParser.ParsedDMIFrame;
Expand Down Expand Up @@ -280,7 +281,8 @@ public interface IDreamIconOperation {
public void ApplyToFrame(Rgba32[] pixels, int imageSpan, int frame, UIBox2i bounds);
}

public sealed class DreamIconOperationBlend : IDreamIconOperation {
[Virtual]
public class DreamIconOperationBlend : IDreamIconOperation {
// With the same values as the ICON_* defines in DMStandard
public enum BlendType {
Add = 0,
Expand All @@ -294,24 +296,74 @@ public enum BlendType {

private readonly BlendType _type;
private readonly int _xOffset, _yOffset;
private readonly Image<Rgba32> _blending;
private readonly ParsedDMIDescription _blendingDescription;

public DreamIconOperationBlend(BlendType type, DreamValue blending, int xOffset, int yOffset) {
protected DreamIconOperationBlend(BlendType type, int xOffset, int yOffset) {
_type = type;
_xOffset = xOffset;
_yOffset = yOffset;

if (_type is not BlendType.Overlay and not BlendType.Underlay and not BlendType.Add and not BlendType.Subtract)
throw new NotImplementedException($"\"{_type}\" blending is not implemented");
}

public virtual void ApplyToFrame(Rgba32[] pixels, int imageSpan, int frame, UIBox2i bounds) {
throw new NotImplementedException();
}

protected void BlendPixel(Rgba32[] pixels, int dstPixelPosition, Rgba32 src) {
Rgba32 dst = pixels[dstPixelPosition];

switch (_type) {
case BlendType.Add: {
pixels[dstPixelPosition].R = (byte)Math.Min(dst.R + src.R, byte.MaxValue);
pixels[dstPixelPosition].G = (byte)Math.Min(dst.G + src.G, byte.MaxValue);
pixels[dstPixelPosition].B = (byte)Math.Min(dst.B + src.B, byte.MaxValue);

// BYOND uses the smaller of the two alphas
pixels[dstPixelPosition].A = Math.Min(dst.A, src.A);
break;
}
case BlendType.Subtract: {
pixels[dstPixelPosition].R = (byte)Math.Max(dst.R - src.R, byte.MinValue);
pixels[dstPixelPosition].G = (byte)Math.Max(dst.G - src.G, byte.MinValue);
pixels[dstPixelPosition].B = (byte)Math.Max(dst.B - src.B, byte.MinValue);

// BYOND uses the smaller of the two alphas
pixels[dstPixelPosition].A = Math.Min(dst.A, src.A);
break;
}

case BlendType.Overlay: {
pixels[dstPixelPosition].R = (byte) (dst.R + (src.R - dst.R) * src.A / 255);
pixels[dstPixelPosition].G = (byte) (dst.G + (src.G - dst.G) * src.A / 255);
pixels[dstPixelPosition].B = (byte) (dst.B + (src.B - dst.B) * src.A / 255);

byte highAlpha = Math.Max(dst.A, src.A);
byte lowAlpha = Math.Min(dst.A, src.A);
pixels[dstPixelPosition].A = (byte) (highAlpha + (highAlpha * lowAlpha / 255));
break;
}
case BlendType.Underlay: {
// Opposite of overlay
(dst, src) = (src, dst);
goto case BlendType.Overlay;
}
}
}
}

public sealed class DreamIconOperationBlendImage : DreamIconOperationBlend {
private readonly Image<Rgba32> _blending;
private readonly ParsedDMIDescription _blendingDescription;

public DreamIconOperationBlendImage(BlendType type, int xOffset, int yOffset, DreamValue blending) : base(type, xOffset, yOffset) {
var objectTree = IoCManager.Resolve<IDreamObjectTree>();
var resourceManager = IoCManager.Resolve<DreamResourceManager>();
(var blendingResource, _blendingDescription) = DreamMetaObjectIcon.GetIconResourceAndDescription(objectTree, resourceManager, blending);
_blending = resourceManager.LoadImage(blendingResource);

if (_type is not BlendType.Overlay and not BlendType.Underlay)
throw new NotImplementedException($"\"{_type}\" blending is not implemented");
}

public void ApplyToFrame(Rgba32[] pixels, int imageSpan, int frame, UIBox2i bounds) {
public override void ApplyToFrame(Rgba32[] pixels, int imageSpan, int frame, UIBox2i bounds) {
_blending.ProcessPixelRows(accessor => {
// The first frame of the source image blends with the first frame of the destination image
// The second frame blends with the second, and so on
Expand All @@ -330,23 +382,7 @@ public void ApplyToFrame(Rgba32[] pixels, int imageSpan, int frame, UIBox2i boun
Rgba32 dst = pixels[dstPixelPosition];
Rgba32 src = row[srcFramePos.Value.X + x - bounds.Left];

switch (_type) {
case BlendType.Overlay: {
pixels[dstPixelPosition].R = (byte) (dst.R + (src.R - dst.R) * src.A / 255);
pixels[dstPixelPosition].G = (byte) (dst.G + (src.G - dst.G) * src.A / 255);
pixels[dstPixelPosition].B = (byte) (dst.B + (src.B - dst.B) * src.A / 255);

byte highAlpha = Math.Max(dst.A, src.A);
byte lowAlpha = Math.Min(dst.A, src.A);
pixels[dstPixelPosition].A = (byte) (highAlpha + (highAlpha * lowAlpha / 255));
break;
}
case BlendType.Underlay: {
// Opposite of overlay
(dst, src) = (src, dst);
goto case BlendType.Overlay;
}
}
BlendPixel(pixels, dstPixelPosition, src);
}
}
});
Expand All @@ -369,3 +405,23 @@ public void ApplyToFrame(Rgba32[] pixels, int imageSpan, int frame, UIBox2i boun
return (column * _blendingDescription.Width, row * _blendingDescription.Height);
}
}

public sealed class DreamIconOperationBlendColor : DreamIconOperationBlend {
private readonly Rgba32 _color;

public DreamIconOperationBlendColor(BlendType type, int xOffset, int yOffset, Color color) : base(type, xOffset, yOffset) {
_color = new Rgba32(color.RByte, color.GByte, color.BByte, color.AByte);
}

public override void ApplyToFrame(Rgba32[] pixels, int imageSpan, int frame, UIBox2i bounds) {
// TODO: x & y offsets

for (int y = bounds.Top; y < bounds.Bottom; y++) {
for (int x = bounds.Left; x < bounds.Right; x++) {
int dstPixelPosition = (y * imageSpan) + x;

BlendPixel(pixels, dstPixelPosition, _color);
}
}
}
}
4 changes: 3 additions & 1 deletion OpenDreamRuntime/Objects/DreamObjectTree.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public sealed class DreamObjectTree : IDreamObjectTree {
private Dictionary<DreamPath, TreeEntry> _pathToType = new();
private Dictionary<string, int> _globalProcIds;

[Dependency] private readonly DreamResourceManager _resourceManager = default!;

public void LoadJson(DreamCompiledJson json) {
Strings = json.Strings;

Expand Down Expand Up @@ -296,7 +298,7 @@ public DreamProc LoadProcJson(DreamTypeJson[] types, ProcDefinitionJson procDefi
}

DreamPath owningType = new DreamPath(types[procDefinition.OwningTypeId].Path);
var proc = new DMProc(owningType, procDefinition.Name, null, argumentNames, argumentTypes, bytecode, procDefinition.MaxStackSize, procDefinition.Attributes, procDefinition.VerbName, procDefinition.VerbCategory, procDefinition.VerbDesc, procDefinition.Invisibility, this);
var proc = new DMProc(owningType, procDefinition.Name, null, argumentNames, argumentTypes, bytecode, procDefinition.MaxStackSize, procDefinition.Attributes, procDefinition.VerbName, procDefinition.VerbCategory, procDefinition.VerbDesc, procDefinition.Invisibility, this, _resourceManager);
proc.Source = procDefinition.Source;
proc.Line = procDefinition.Line;
return proc;
Expand Down
15 changes: 13 additions & 2 deletions OpenDreamRuntime/Objects/MetaObjects/DreamMetaObjectIcon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ public void OnObjectCreated(DreamObject dreamObject, DreamProcArguments creation
DreamValue frame = creationArguments.GetArgument(3, "frame");
DreamValue moving = creationArguments.GetArgument(4, "moving");

DreamIcon dreamIcon = new(_rscMan);
ObjectToDreamIcon.Add(dreamObject, dreamIcon);
var dreamIcon = InitializeIcon(_rscMan, dreamObject);

if (icon != DreamValue.Null) {
// TODO: Could maybe have an alternative path for /icon values so the DMI doesn't have to be generated
Expand Down Expand Up @@ -61,6 +60,18 @@ public void OnVariableSet(DreamObject dreamObject, string varName, DreamValue va
}
}

/// <summary>
/// A fast path for initializing an /icon object
/// </summary>
/// <remarks>Doesn't call any DM code</remarks>
/// <returns>The /icon's DreamIcon</returns>
public static DreamIcon InitializeIcon(DreamResourceManager rscMan, DreamObject icon) {
DreamIcon dreamIcon = new(rscMan);

ObjectToDreamIcon.Add(icon, dreamIcon);
return dreamIcon;
}

public static (DreamResource Resource, ParsedDMIDescription Description) GetIconResourceAndDescription(
IDreamObjectTree objectTree, DreamResourceManager resourceManager, DreamValue value) {
if (value.TryGetValueAsDreamObjectOfType(objectTree.Icon, out var iconObj)) {
Expand Down
18 changes: 12 additions & 6 deletions OpenDreamRuntime/Procs/DMOpcodeHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Threading.Tasks;
using OpenDreamRuntime.Objects;
using OpenDreamRuntime.Objects.MetaObjects;
using OpenDreamRuntime.Procs.Native;
using OpenDreamRuntime.Resources;
using OpenDreamShared.Dream;
using OpenDreamShared.Dream.Procs;
Expand Down Expand Up @@ -616,11 +617,21 @@ private static void HandleSuffixPronoun(ref StringBuilder formattedString, ReadO

return null;
} else {
throw new Exception("Invalid append operation on " + first + " and " + second);
throw new Exception($"Invalid append operation on {first} and {second}");
}
} else {
result = second;
}
} else if (first.TryGetValueAsDreamResource(out _) || first.TryGetValueAsDreamObjectOfType(state.Proc.ObjectTree.Icon, out _)) {
// Implicitly create a new /icon and ICON_ADD blend it
// Note that BYOND creates something other than an /icon, but it behaves the same as one in most reasonable interactions
DreamObject iconObj = state.Proc.ObjectTree.CreateObject(DreamPath.Icon);
var icon = DreamMetaObjectIcon.InitializeIcon(state.Proc.ResourceManager, iconObj);
var from = DreamMetaObjectIcon.GetIconResourceAndDescription(state.Proc.ObjectTree, state.Proc.ResourceManager, first);

icon.InsertStates(from.Resource, from.Description, DreamValue.Null, DreamValue.Null, DreamValue.Null);
DreamProcNativeIcon.Blend(icon, second, DreamIconOperationBlend.BlendType.Add, 0, 0);
result = new DreamValue(iconObj);
} else if (second.Value != null) {
switch (first.Type) {
case DreamValue.DreamValueType.Float when second.Type == DreamValue.DreamValueType.Float:
Expand All @@ -629,11 +640,6 @@ private static void HandleSuffixPronoun(ref StringBuilder formattedString, ReadO
case DreamValue.DreamValueType.String when second.Type == DreamValue.DreamValueType.String:
result = new DreamValue(first.GetValueAsString() + second.GetValueAsString());
break;
case DreamValue.DreamValueType.DreamResource when (second.Type == DreamValue.DreamValueType.String && first.TryGetValueAsDreamResource(out var rsc) && rsc.ResourcePath.EndsWith("dmi")):
// TODO icon += hexcolor is the same as Blend()
state.DreamManager.WriteWorldLog("Appending colors to DMIs is not implemented", LogLevel.Warning, "opendream.unimplemented");
result = first;
break;
default:
throw new Exception("Invalid append operation on " + first + " and " + second);
}
Expand Down
5 changes: 4 additions & 1 deletion OpenDreamRuntime/Procs/DMProc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,25 @@
using OpenDreamRuntime.Objects;
using OpenDreamRuntime.Objects.MetaObjects;
using OpenDreamRuntime.Procs.DebugAdapter;
using OpenDreamRuntime.Resources;
using OpenDreamShared.Dream;
using OpenDreamShared.Dream.Procs;

namespace OpenDreamRuntime.Procs {
sealed class DMProc : DreamProc {
public readonly IDreamObjectTree ObjectTree;
public readonly DreamResourceManager ResourceManager;
public byte[] Bytecode { get; }

private readonly int _maxStackSize;

public string? Source { get; set; }
public int Line { get; set; }

public DMProc(DreamPath owningType, string name, DreamProc superProc, List<String> argumentNames, List<DMValueType> argumentTypes, byte[] bytecode, int maxStackSize, ProcAttributes attributes, string? verbName, string? verbCategory, string? verbDesc, sbyte? invisibility, IDreamObjectTree objectTree)
public DMProc(DreamPath owningType, string name, DreamProc superProc, List<String> argumentNames, List<DMValueType> argumentTypes, byte[] bytecode, int maxStackSize, ProcAttributes attributes, string? verbName, string? verbCategory, string? verbDesc, sbyte? invisibility, IDreamObjectTree objectTree, DreamResourceManager resourceManager)
: base(owningType, name, superProc, attributes, argumentNames, argumentTypes, verbName, verbCategory, verbDesc, invisibility) {
ObjectTree = objectTree;
ResourceManager = resourceManager;
Bytecode = bytecode;
_maxStackSize = maxStackSize;
}
Expand Down
18 changes: 14 additions & 4 deletions OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using OpenDreamRuntime.Objects;
using OpenDreamRuntime.Objects.MetaObjects;
using OpenDreamRuntime.Resources;
using OpenDreamShared.Dream;
using BlendType = OpenDreamRuntime.Objects.DreamIconOperationBlend.BlendType;

namespace OpenDreamRuntime.Procs.Native {
static class DreamProcNativeIcon {
Expand Down Expand Up @@ -46,6 +48,17 @@ public static DreamValue NativeProc_Insert(DreamObject instance, DreamObject usr
return DreamValue.Null;
}

public static void Blend(DreamIcon icon, DreamValue blend, BlendType function, int x, int y) {
if (blend.TryGetValueAsString(out var colorStr)) {
if (!ColorHelpers.TryParseColor(colorStr, out var color))
throw new Exception($"Invalid color {colorStr}");

icon.ApplyOperation(new DreamIconOperationBlendColor(function, x, y, color));
} else {
icon.ApplyOperation(new DreamIconOperationBlendImage(function, x, y, blend));
}
}

[DreamProc("Blend")]
[DreamProcParameter("icon", Type = DreamValue.DreamValueType.DreamObject)]
[DreamProcParameter("function", Type = DreamValue.DreamValueType.Float)]
Expand All @@ -63,10 +76,7 @@ public static DreamValue NativeProc_Blend(DreamObject instance, DreamObject usr,
if (!function.TryGetValueAsInteger(out var functionValue))
throw new Exception($"Invalid 'function' argument {function}");

var blendType = (DreamIconOperationBlend.BlendType) functionValue;

DreamIcon iconObj = DreamMetaObjectIcon.ObjectToDreamIcon[instance];
iconObj.ApplyOperation(new DreamIconOperationBlend(blendType, icon, x, y));
Blend(DreamMetaObjectIcon.ObjectToDreamIcon[instance], icon, (BlendType)functionValue, x, y);
return DreamValue.Null;
}

Expand Down

0 comments on commit 7640e98

Please sign in to comment.