diff --git a/OpenRA.Game/Graphics/HardwarePalette.cs b/OpenRA.Game/Graphics/HardwarePalette.cs index 024902f09c..ddab900b7b 100644 --- a/OpenRA.Game/Graphics/HardwarePalette.cs +++ b/OpenRA.Game/Graphics/HardwarePalette.cs @@ -18,15 +18,19 @@ namespace OpenRA.Graphics public sealed class HardwarePalette : IDisposable { public ITexture Texture { get; private set; } + public ITexture ColorShifts { get; private set; } + public int Height { get; private set; } readonly Dictionary palettes = new Dictionary(); readonly Dictionary mutablePalettes = new Dictionary(); readonly Dictionary indices = new Dictionary(); byte[] buffer = new byte[0]; + float[] colorShiftBuffer = new float[0]; public HardwarePalette() { Texture = Game.Renderer.Context.CreateTexture(); + ColorShifts = Game.Renderer.Context.CreateTexture(); } public bool Contains(string name) @@ -55,14 +59,18 @@ namespace OpenRA.Graphics if (palettes.ContainsKey(name)) throw new InvalidOperationException($"Palette {name} has already been defined"); - int index = palettes.Count; + // PERF: the first row in the palette textures is reserved as a placeholder for non-indexed sprites + // that do not have a color-shift applied. This provides a quick shortcut to avoid querying the + // color-shift texture for every pixel only to find that most are not shifted. + var index = palettes.Count + 1; indices.Add(name, index); palettes.Add(name, p); - if (palettes.Count > Height) + if (index >= Height) { - Height = Exts.NextPowerOf2(palettes.Count); + Height = Exts.NextPowerOf2(index + 1); Array.Resize(ref buffer, Height * Palette.Size * 4); + Array.Resize(ref colorShiftBuffer, Height * 4); } if (allowModifiers) @@ -82,6 +90,21 @@ namespace OpenRA.Graphics CopyBufferToTexture(); } + public void SetColorShift(string name, float hueOffset, float satOffset, float minHue, float maxHue) + { + var index = GetPaletteIndex(name); + colorShiftBuffer[4 * index + 0] = hueOffset; + colorShiftBuffer[4 * index + 1] = satOffset; + colorShiftBuffer[4 * index + 2] = minHue; + colorShiftBuffer[4 * index + 3] = maxHue; + } + + public bool HasColorShift(string name) + { + var index = GetPaletteIndex(name); + return colorShiftBuffer[4 * index + 2] != 0 || colorShiftBuffer[4 * index + 3] != 0; + } + public void Initialize() { CopyModifiablePalettesToBuffer(); @@ -102,6 +125,7 @@ namespace OpenRA.Graphics void CopyBufferToTexture() { Texture.SetData(buffer, Palette.Size, Height); + ColorShifts.SetFloatData(colorShiftBuffer, 1, Height); } public void ApplyModifiers(IEnumerable paletteMods) @@ -125,6 +149,7 @@ namespace OpenRA.Graphics public void Dispose() { Texture.Dispose(); + ColorShifts.Dispose(); } } } diff --git a/OpenRA.Game/Graphics/PaletteReference.cs b/OpenRA.Game/Graphics/PaletteReference.cs index 26b948ac69..de883efafe 100644 --- a/OpenRA.Game/Graphics/PaletteReference.cs +++ b/OpenRA.Game/Graphics/PaletteReference.cs @@ -28,5 +28,7 @@ namespace OpenRA.Graphics this.index = index; this.hardwarePalette = hardwarePalette; } + + public bool HasColorShift => hardwarePalette.HasColorShift(Name); } } diff --git a/OpenRA.Game/Graphics/SpriteRenderable.cs b/OpenRA.Game/Graphics/SpriteRenderable.cs index 2953a9518a..8574083216 100644 --- a/OpenRA.Game/Graphics/SpriteRenderable.cs +++ b/OpenRA.Game/Graphics/SpriteRenderable.cs @@ -41,6 +41,12 @@ namespace OpenRA.Graphics this.isDecoration = isDecoration; this.tintModifiers = tintModifiers; this.alpha = alpha; + + // PERF: Remove useless palette assignments for RGBA sprites + // HACK: This is working around the fact that palettes are defined on traits rather than sequences + // and can be removed once this has been fixed + if (sprite.Channel == TextureChannel.RGBA && !(palette?.HasColorShift ?? false)) + this.palette = null; } public WPos Pos => pos + offset; diff --git a/OpenRA.Game/Graphics/SpriteRenderer.cs b/OpenRA.Game/Graphics/SpriteRenderer.cs index 9707057b36..6f67bb2d5e 100644 --- a/OpenRA.Game/Graphics/SpriteRenderer.cs +++ b/OpenRA.Game/Graphics/SpriteRenderer.cs @@ -116,14 +116,28 @@ namespace OpenRA.Graphics nv += 6; } + float ResolveTextureIndex(Sprite s, PaletteReference pal) + { + if (pal == null) + return 0; + + // PERF: Remove useless palette assignments for RGBA sprites + // HACK: This is working around the limitation that palettes are defined on traits rather than on sequences, + // and can be removed once this has been fixed + if (s.Channel == TextureChannel.RGBA && !pal.HasColorShift) + return 0; + + return pal.TextureIndex; + } + public void DrawSprite(Sprite s, in float3 location, PaletteReference pal) { - DrawSprite(s, location, pal.TextureIndex, s.Size); + DrawSprite(s, location, ResolveTextureIndex(s, pal), s.Size); } public void DrawSprite(Sprite s, in float3 location, PaletteReference pal, float3 size) { - DrawSprite(s, location, pal.TextureIndex, size); + DrawSprite(s, location, ResolveTextureIndex(s, pal), size); } public void DrawSprite(Sprite s, in float3 a, in float3 b, in float3 c, in float3 d) @@ -142,7 +156,7 @@ namespace OpenRA.Graphics public void DrawSprite(Sprite s, in float3 location, PaletteReference pal, in float3 size, in float3 tint, float alpha) { - DrawSprite(s, location, pal.TextureIndex, size, tint, alpha); + DrawSprite(s, location, ResolveTextureIndex(s, pal), size, tint, alpha); } public void DrawSprite(Sprite s, in float3 a, in float3 b, in float3 c, in float3 d, in float3 tint, float alpha) @@ -189,9 +203,10 @@ namespace OpenRA.Graphics nv += v.Length; } - public void SetPalette(ITexture palette) + public void SetPalette(ITexture palette, ITexture colorShifts) { shader.SetTexture("Palette", palette); + shader.SetTexture("ColorShifts", colorShifts); } public void SetViewportParams(Size screen, float depthScale, float depthOffset, int2 scroll) diff --git a/OpenRA.Game/Graphics/TerrainSpriteLayer.cs b/OpenRA.Game/Graphics/TerrainSpriteLayer.cs index 69ec803577..5d006292b0 100644 --- a/OpenRA.Game/Graphics/TerrainSpriteLayer.cs +++ b/OpenRA.Game/Graphics/TerrainSpriteLayer.cs @@ -164,6 +164,12 @@ namespace OpenRA.Graphics throw new InvalidDataException("Attempted to add sprite with a different blend mode"); samplers = new int2(GetOrAddSheetIndex(sprite.Sheet), GetOrAddSheetIndex((sprite as SpriteWithSecondaryData)?.SecondarySheet)); + + // PERF: Remove useless palette assignments for RGBA sprites + // HACK: This is working around the limitation that palettes are defined on traits rather than on sequences, + // and can be removed once this has been fixed + if (sprite.Channel == TextureChannel.RGBA && !(palette?.HasColorShift ?? false)) + palette = null; } else { diff --git a/OpenRA.Game/Graphics/UISpriteRenderable.cs b/OpenRA.Game/Graphics/UISpriteRenderable.cs index 23e57cce7b..01c3495af2 100644 --- a/OpenRA.Game/Graphics/UISpriteRenderable.cs +++ b/OpenRA.Game/Graphics/UISpriteRenderable.cs @@ -32,6 +32,12 @@ namespace OpenRA.Graphics this.palette = palette; this.scale = scale; this.alpha = alpha; + + // PERF: Remove useless palette assignments for RGBA sprites + // HACK: This is working around the fact that palettes are defined on traits rather than sequences + // and can be removed once this has been fixed + if (sprite.Channel == TextureChannel.RGBA && !(palette?.HasColorShift ?? false)) + this.palette = null; } // Does not exist in the world, so a world positions don't make sense diff --git a/OpenRA.Game/Graphics/WorldRenderer.cs b/OpenRA.Game/Graphics/WorldRenderer.cs index b793241ded..7584bf8458 100644 --- a/OpenRA.Game/Graphics/WorldRenderer.cs +++ b/OpenRA.Game/Graphics/WorldRenderer.cs @@ -109,6 +109,11 @@ namespace OpenRA.Graphics palettes[name].Palette = pal; } + public void SetPaletteColorShift(string name, float hueOffset, float satOffset, float minHue, float maxHue) + { + palette.SetColorShift(name, hueOffset, satOffset, minHue, maxHue); + } + // PERF: Avoid LINQ. void GenerateRenderables() { diff --git a/OpenRA.Game/Renderer.cs b/OpenRA.Game/Renderer.cs index bed41b3ca2..a2bd3fd9c9 100644 --- a/OpenRA.Game/Renderer.cs +++ b/OpenRA.Game/Renderer.cs @@ -249,14 +249,16 @@ namespace OpenRA public void SetPalette(HardwarePalette palette) { + // Note: palette.Texture and palette.ColorShifts are updated at the same time + // so we only need to check one of the two to know whether we must update the textures if (palette.Texture == currentPaletteTexture) return; Flush(); currentPaletteTexture = palette.Texture; - SpriteRenderer.SetPalette(currentPaletteTexture); - WorldSpriteRenderer.SetPalette(currentPaletteTexture); + SpriteRenderer.SetPalette(currentPaletteTexture, palette.ColorShifts); + WorldSpriteRenderer.SetPalette(currentPaletteTexture, palette.ColorShifts); WorldModelRenderer.SetPalette(currentPaletteTexture); } diff --git a/glsl/combined.frag b/glsl/combined.frag index 4f6f314695..d8efc2bfdc 100644 --- a/glsl/combined.frag +++ b/glsl/combined.frag @@ -12,6 +12,7 @@ uniform sampler2D Texture5; uniform sampler2D Texture6; uniform sampler2D Texture7; uniform sampler2D Palette; +uniform sampler2D ColorShifts; uniform bool EnableDepthPreview; uniform float DepthTextureScale; @@ -69,6 +70,49 @@ float jet_b(float x) return x < 0.3 ? 4.0 * x + 0.5 : -4.0 * x + 2.5; } +vec3 rgb2hsv(vec3 c) +{ + // From http://lolengine.net/blog/2013/07/27/rgb-to-hsv-in-glsl + vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0); + vec4 p = c.g < c.b ? vec4(c.bg, K.wz) : vec4(c.gb, K.xy); + vec4 q = c.r < p.x ? vec4(p.xyw, c.r) : vec4(c.r, p.yzx); + float d = q.x - min(q.w, q.y); + float e = 1.0e-10; + return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x); +} + +vec3 hsv2rgb(vec3 c) +{ + // From http://lolengine.net/blog/2013/07/27/rgb-to-hsv-in-glsl + vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); + return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); +} + +float srgb2linear(float c) +{ + // Standard gamma conversion equation: see e.g. http://entropymine.com/imageworsener/srgbformula/ + return c <= 0.04045f ? c / 12.92f : pow((c + 0.055f) / 1.055f, 2.4f); +} + +vec4 srgb2linear(vec4 c) +{ + // The SRGB color has pre-multiplied alpha which we must undo before removing the the gamma correction + return c.a * vec4(srgb2linear(c.r / c.a), srgb2linear(c.g / c.a), srgb2linear(c.b / c.a), 1.0f); +} + +float linear2srgb(float c) +{ + // Standard gamma conversion equation: see e.g. http://entropymine.com/imageworsener/srgbformula/ + return c <= 0.0031308 ? c * 12.92f : 1.055f * pow(c, 1.0f / 2.4f) - 0.055f; +} + +vec4 linear2srgb(vec4 c) +{ + // The linear color has pre-multiplied alpha which we must undo before applying the the gamma correction + return c.a * vec4(linear2srgb(c.r / c.a), linear2srgb(c.g / c.a), linear2srgb(c.b / c.a), 1.0f); +} + #if __VERSION__ == 120 vec2 Size(float samplerIndex) { @@ -178,6 +222,21 @@ vec4 SamplePalettedBilinear(float samplerIndex, vec2 coords, vec2 textureSize) return mix(mix(c1, c2, interp.x), mix(c3, c4, interp.x), interp.y); } +vec4 ColorShift(vec4 c, float p) +{ + #if __VERSION__ == 120 + vec4 shift = texture2D(ColorShifts, vec2(0.5, p)); + #else + vec4 shift = texture(ColorShifts, vec2(0.5, p)); + #endif + + vec3 hsv = rgb2hsv(srgb2linear(c).rgb); + if (hsv.r >= shift.b && shift.a >= hsv.r) + c = linear2srgb(vec4(hsv2rgb(vec3(hsv.r + shift.r, clamp(hsv.g + shift.g, 0.0, 1.0), hsv.b)), c.a)); + + return c; +} + void main() { vec2 coords = vTexCoord.st; @@ -215,6 +274,9 @@ void main() if (c.a == 0.0) discard; + if (vRGBAFraction.r > 0.0 && vTexMetadata.s > 0.0) + c = ColorShift(c, vTexMetadata.s); + float depth = gl_FragCoord.z; if (length(vDepthMask) > 0.0) {