Envision, Create, Share

Welcome to HBGames, a leading amateur game development forum and Discord server. All are welcome, and amongst our ranks you will find experts in their field from all aspects of video game design and development.

Recoloring

To cut down on filesize (so I don't have 7+ slightly recolored versions of each animation) I'd like to include recoloring somehow. But as we all know that's very laggy. However, what I'm recoloring is simple enough that it should be plausible, by one of two ways I can think of.



One, http://www.rmxp.org/forums/index.php?topic=39324.0
This seems to give access to creating methods and such that work directly with bitmaps in C, so it should be nice and fast, right? If so, this would be best. If anyone knows how to edit bitmaps in C and could explain the basics, or link to something explaining it, I could probably work it out from there.

Of course if I go this method, I'd also like to create a dll or whatever out of the end result C file so the player doesn't mess with it. ._.



Two, since all of my animations are 16 colors and only 5-8 should need changed, I think I could have a secondary project process all of the images and create arrays of all their pixels that are the colors I want to change.

ie an outer hash contains one array per image file, and then each of those arrays has one for each color. Colors are indexed, and the array for each color contains the x,y locations of every pixel of that color.

This gets rid of the whole get_pixel part in game, so I'm just setting pixels. Still slow, but it should be much faster, at least enough so that I could change the colors over a few frames and make it not noticeable (the recoloring would take place on about 2 bitmaps at the start of the battle scene)

However, I'd need to save the information I get about each image so that it could be recalled during gameplay. So I'd need to know how to create a file (encrypted rxdata or something, right?) and then open it again, process it and get arrays/hashes out of it later.



Any help would be greatly appreciated. :D

(RMXP btw)
 
Well I don't know anything about C, but I managed to do this in Ruby. But ruby being ruby, this is very slow. Even finding a hue between 2 hues, adding a value and reseting the value takes time (although this could be reduced through caching) for an icon.

Code:
class Scene_Title
  alias main_sprite2 main_sprite
  def main_sprite
    main_sprite2
    @sprite2 = Sprite.new
    @sprite2.bitmap = RPG::Cache.icon('001-Weapon01')
    @sprite2.bitmap.recolor(210, 230, -90)
    @sprite2.zoom_x = 4
    @sprite2.zoom_y = 4
  end
end

class Bitmap
  Clear_Pixel = Color.new(255, 255, 255)
  def recolor(start_hue, end_hue, hue_change)
    for x in 0...width
      for y in 0...height
        c = get_pixel(x, y)
        next if c == Clear_Pixel
        h, s, b = c.to_hsb
        if h > start_hue && h < end_hue
          c2 = Color.hsb_new(h + hue_change, s, b)
          set_pixel(x, y, c2)
        end
        Graphics.update
      end
    end
  end
end

class Color
  def self.to_hsb(r = rand(255), g = rand(255), b = rand(255))
    r = r / 255.0
    g = g / 255.0
    b = b / 255.0
    min = [r, g, b].min
    max = [r, g, b].max
    v = max
    delta = max - min
    if max != 0
      s = delta / max
    else
      s = 0
      h = -1
      return h, s, v
    end
    if r == max
      h = (g - b) / delta
    elsif g == max
      h = 2 + (b - r) / delta
    else
      h = 4 + (r - g) / delta
    end
    h *= 60
    h %= 360
    return h, s, v
  end
  def self.hsb_new(hue, sat, bri, alpha = 255)
    red, green, blue = Color.hsb_to_rgb(hue, sat, bri)
    return Color.new(red, green, blue, alpha)
  end 
  def self.hsb_to_rgb(hue, sat, bri)
    # Convert All To Floats
    hue, sat, bri = hue.to_f, sat.to_f , bri.to_f
    # Ensure Hue is [0, 360)
    hue %= 360
    # Reduce to [0, 1]
    sat = sat > 100 ? 1.0 : sat / 100
    bri = bri > 100 ? 1.0 : bri / 100
    # Get Sector
    sector = (hue / 60).to_i
    f = hue / 60 - sector
    p = bri * (1 - sat)
    q = bri * (1 - f * sat)
    t = bri * (1 - (1 - f) * sat)
    # Branch By Sector and get r,g,b values
    case sector
    when 0...1
      r,g,b = bri,t,p
    when 1...2
      r,g,b = q,bri,p
    when 2...3
      r,g,b = p,bri,t
    when 3...4
      r,g,b = p,q,bri
    when 4...5
      r,g,b = t,p,bri
    when 5..6
      r,g,b = bri,p,q
    end
    # Set Color
    color = [r,g,b]
    # Convert to [0, 255] Range
    color.collect! {|value| value * 255}
    # Return Color
    return color
  end
end

I think something is wrong there, as something just doesn't seem to be working right. Basically what I tried there was passing through each pixel, getting the pixel, converting the rgb to hsb, if the hue is between to values, adding a value to the hue, convert the new hsb to rgb and setting a new pixel. Again, this is far too slow and could probably only be used to generate bitmaps to get before the game is actually run.
 
I don't know what you mean by not working right, but for me it crashed on to_hsb for a while (it was calling from a Color object but the method is self.to_hsb, I dunno if that should work but it did after I removed the 'self.' and rewrote it a bit to use the rgb of the object calling it) then it just randomly blitted black pixels, but I think that's cause the get_hsb returns value and saturation from 0..1 while the hsb_to_rgb expects them from 0..100. Also alpha wasn't taken into account. It seems to work now though:

Code:
class Scene_Title
  alias main_sprite2 main_sprite
  def main_sprite
    main_sprite2
    @sprite2 = Sprite.new
    @sprite2.bitmap = RPG::Cache.icon('001-Weapon01')
    @sprite2.bitmap.recolor(210, 230, -90)
    @sprite2.zoom_x = 4
    @sprite2.zoom_y = 4
  end
end

class Bitmap
  Clear_Pixel = Color.new(255, 255, 255)
  def recolor(start_hue, end_hue, hue_change)
    for x in 0...width
      for y in 0...height
        c = get_pixel(x, y)
        next if c == Clear_Pixel
        h, s, b = c.to_hsb
        if h > start_hue && h < end_hue
          c2 = Color.hsb_new(h + hue_change, s, b, c.alpha)
          set_pixel(x, y, c2)
        end
        #Graphics.update
      end
    end
  end
end

class Color
  def to_hsb(color_fix = false, r = rand(255), g = rand(255), b = rand(255))
    if color_fix
      r = r / 255.0
      g = g / 255.0
      b = b / 255.0
    else
      r = self.red / 255.0
      g = self.green / 255.0
      b = self.blue / 255.0
    end
    min = [r, g, b].min
    max = [r, g, b].max
    v = max * 100
    delta = max - min
    if max != 0
      s = delta / max * 100
    else
      s = 0
      h = -1
      return h, s, v
    end
    if r == max
      h = (g - b) / delta
    elsif g == max
      h = 2 + (b - r) / delta
    else
      h = 4 + (r - g) / delta
    end
    h *= 60
    h %= 360
    return h, s, v
  end
  def self.hsb_new(hue, sat, bri, alpha = 255)
    red, green, blue = Color.hsb_to_rgb(hue, sat, bri)
    return Color.new(red, green, blue, alpha)
  end 
  def self.hsb_to_rgb(hue, sat, bri)
    # Convert All To Floats
    hue, sat, bri = hue.to_f, sat.to_f , bri.to_f
    # Ensure Hue is [0, 360)
    hue %= 360
    # Reduce to [0, 1]
    sat = sat > 100 ? 1.0 : sat / 100
    bri = bri > 100 ? 1.0 : bri / 100
    # Get Sector
    sector = (hue / 60).to_i
    f = hue / 60 - sector
    p = bri * (1 - sat)
    q = bri * (1 - f * sat)
    t = bri * (1 - (1 - f) * sat)
    # Branch By Sector and get r,g,b values
    case sector
    when 0...1
      r,g,b = bri,t,p
    when 1...2
      r,g,b = q,bri,p
    when 2...3
      r,g,b = p,bri,t
    when 3...4
      r,g,b = p,q,bri
    when 4...5
      r,g,b = t,p,bri
    when 5..6
      r,g,b = bri,p,q
    end
    # Set Color
    color = [r,g,b]
    # Convert to [0, 255] Range
    color.collect! {|value| value * 255}
    # Return Color
    return color
  end
end

It actually ran pretty fast when I commented out the Graphics.update, but is that necessary to keep from getting a script is hanging error? Maybe run it once every 50 or 100 searches.
But that's still not what I'm looking for, since I want to change a small set of colors into exact other colors.


My second idea, for example

I create a seconary project, import all the animations I plan to recolor, and process them:

I set a list of an image's colors that it can expect to recolor
For the example, the image 'Fighter-Axe' that has 5 colors I'll need to change.
Color 0: 160, 128, 0
Color 1: 192, 176, 32
Color 2: 232, 215, 48
Color 3: 128, 0, 8
Color 4: 216, 0, 0

Then search the image for all the x,y locations of each color.
I'll use [0,5], [6,7], and [9,5] for color 0 as an example.
Then I put the x,y's for each color in an array (ie color_0 = [[0,5], [6,7], [9,5]]) and put those in an array with the colors' numbers as the indexes (ie img_colors[0] = [[0,5], [6,7], [9,5]])
Then a number of those, one for each image, goes in a hash (recolor_list['Fighter-Axe'][0] = [[0,5], [6,7], [9,5]])

I'd need to save this hash somehow so I could reopen during gameplay, so I can end up with an array of locations that need their color changed.


The point of this is to do some prior prep work so that the get_pixel part isn't performed during gameplay, so it's not going through every pixel of an 800 * y image, but instead singling out the few hundred or so of each color and setting them. Since I have it arranged by color I could space those operations over a second, during the battle loading screen even, to make it near unnoticeable.
 
At that point you are still increasing the scripts.rxdata file probably more than having separate files for each recolor. You also have to remember, Color.new(255, 200, 0) != Color.new(254, 200, 0) so you have to use a small color palette.

But anyways, doing it your way, you could just store that into a constant.

Code:
module Recoloring
  Animations = {}
  # Add filename
  Animations['filename'] = {}
  # For filename, apply y changes to x pixel color
  Animations['filename'][[r, g, b]] = [[x, y], ...]
end

class Game_System
  attr_accessor :recolors
  alias_method :seph_recolors_gmsys_init, :initialize
  def initialize
    seph_recolors_gmsys_init
    @recolors = {}
    @recolors['animations'] = {}
  end
  def recolor_animation(filename, start_color, target_color)
    if @recolors['animations'].has_key?(filename) == false
      @recolors['animations'][filename] = {}
    end
    key = [start_color.red, start_color.green, start_color.blue]
    if Recoloring::Animations[filename].include?(key)
      @recolors['animations'][filename][key] = target_color
    end
  end
end

class RPG::Cache
  def self.animation(filename, hue)
    bitmap = self.load_bitmap('Graphics/Animations/', filename, hue)
    if $game_system != nil && $game_system.recolors['animations'].include?(filename)
      new_bitmap = Bitmap.new(bitmap.width, bitmap.height)
      new_bitmap.blt(0, 0, bitmap, bitmap.rect)
      $game_system.recolors['animations'][filename].each do |rgb, trgb|
        Animations['filename'][rgb].each do |xy|
          x, y = *xy
          new_bitmap.set_pixel(x, y, Color.new(*trgb))
        end
      end
      return new_bitmap
    end
    return bitmap
  end
end

I think that should work but I haven't tested it.
 
That's funny, I actually tried to make a palette script yesterday.
Basically, in Cache#load_bitmap, I gather all the different colors from that bitmap and store them in a custom Palette class and then store the palette in an hash. Then, when I want to change the palette of a bitmap, I scan each pixels and replace them, just like you did Seph.

But, as I expected, its way to painful to do that in RGSS. It's time consumming and heavy for the system. I think I'll make a C++ script, compile it and then I'll pass my Bitmap object to the dll.
 
My only suggestion for this is not looking for a rgb value. Two colors that look a lot alike can have very different rgb values. However, hue works pretty well because it works on a color scale, that gets all these values. It is better for converting colors, just is slower because of the rbg -> hsv -> new_hsv -> new_rgb

I am going to look it over and see if I could optimize it (getting color hue only, comparing adding, then getting sv values then creating a new rgb value. The idea however is to limit caching values or up end up with one big cache and more memory use. Clearing cached values would be best after performing hue changes. Secondly, you could cache the final bitmap, after hue change(s) are completed in the @cache so the same does not need to be preformed multiple times.
 
Whatever I use will go by exact values because I'm a spriter first, and there's something wrong if I can't get my palette exact. Besides, when I do it right, smaller filesize. Also it means less mistaken recolors that hue shifting might accidentally cause.
 
Turns out saving and loading is easier than I thought, though I don't know if the saving can be encrypted or something, cause it looks almost like plaintext it in Notepad. Anyway.

-EDIT-
Just realized encryption for the rxdata isn't important since I'm the only one who has the rxdata and the rgssad the end user gets will have it inside and encrypted anyway.
---

This is what I meant by the second option.

To setup the colors available to be recolored up to the far left 16 pixels in the top row of each recolorable animation are set as colors in the image that might need changed.
This is in the secondary project for setup:
Code:
module RPG
  module Cache
    # Ignores the first 16 colors in the top row of an animation
    def self.animation(filename, hue = 0, remove_corner = true)
      bitmap = self.load_bitmap("Graphics/Animations/", filename, hue)
      bitmap.blt(0,0,Bitmap.new(16,1),Rect.new(0,0,16,1)) if remove_corner
      return bitmap
    end
  end
end


class Recolor
	attr_reader :recolor_locations
	# A list of animation files that need recoloring allowed
	Recolor_Animations = ['Fighter-Axe', 'Thief-Sword']
	def initialize
		@recolor_locations = {}
	end
	
	def setup_recolors
		for anim in Recolor_Animations
			color_array = []
			bmp = RPG::Cache.animation(anim, 0, false)
			# Checks the top row far left 16 pixels for what colors might need recolored
			for i in 0..15
				color = bmp.get_pixel(i,0)
				if color.alpha != 0
					color_array.push(color)
				end
			end
			# Searches the whole image for colors found and store their locations
			if color_array.length > 0
				@recolor_locations[anim] = []
				for i in 0...color_array.length
          @recolor_locations[anim].push([])
        end
				for y in 0...bmp.height
          Graphics.update if y%500 == 0
					for x in 0...bmp.width
						if x == 0 and y == 0; x = 15; next; end;
						color = bmp.get_pixel(x,y)
						if color_array.include?(color)
							@recolor_locations[anim][color_array.index(color)].push([x,y])
						end
					end
				end
			end
		end
	end
end

Run this from somewhere
Code:
    $game_recolor = Recolor.new
    $game_recolor.setup_recolors
    save_data($game_recolor.recolor_locations, "Data/Recolors.rxdata")
    print hash = $game_recolor.recolor_locations.keys.length.to_s +
        ' animations processed'
    print 'Recolor setup complete'

Then copy the Recolors.rxdata, and in the actual game have
Code:
module RPG
  module Cache
    # Ignores the first 16 colors in the top row of an animation
    def self.animation(filename, hue = 0, remove_corner = true)
      bitmap = self.load_bitmap("Graphics/Animations/", filename, hue)
      bitmap.blt(0,0,Bitmap.new(16,1),Rect.new(0,0,16,1)) if remove_corner
      return bitmap
    end
  end
end



# A hash of names/countries by class hash
RECOLOR_LIST =
{
  17 => # Thief
  {
      'Leonard' => 
          [[104, 96, 0], # Cloak
          [168, 160, 0],
          [224, 224, 32],
          [88, 24, 192], # Pants
          [144, 88, 216],
          [40, 96, 96], #Hair
          [72, 176, 160],
          [120, 200, 192]],
      'Bandit' => 
          [[80, 56, 48],
          [120, 88, 72],
          [144, 112, 104],
          [120, 128, 128],
          [176, 184, 184],
          [88, 64, 48],
          [128, 104, 64],
          [152, 136, 96]]
  },
  24 => # Fighter
  {
      'Bandit' => 
          [[80, 56, 48], # Clothes
          [120, 88, 72],
          [144, 112, 104],
          [88, 64, 48], # Hair
          [128, 104, 64]]
  }
}



class Recolor
	attr_reader :recolor_locations
	# A list of animation files that need recoloring allowed
	Recolor_Animations = ['Fighter-Axe', 'Thief-Sword']
	def initialize
		@recolor_locations = {}
	end
	
	def load_recolors
    @recolor_locations = load_data("Data/Recolors.rxdata")
	end
end

class Bitmap
	def recolor(image_name, index, color)
    locations = $game_recolor.recolor_locations[image_name][index]
    for j in locations
      self.set_pixel(j[0], j[1], color)
    end
  end
end

And to use it on a bitmap
Code:
if $game_recolor.recolor_locations.keys.include?(filename) and
        RECOLOR_LIST.keys.include?(class_id)
    if RECOLOR_LIST[class_id].include?(char_name)
      for i in 0...$game_recolor.recolor_locations[filename].length
        ary = RECOLOR_LIST[class_id][char_name][i]
        color = Color.new(ary[0], ary[1], ary[2])
        @anim_bitmap.recolor(filename, i, color)
      end
    end
  end

It runs surprisingly fast for... well I was getting about 5000 pixels total set for two bitmaps and I don't know if it took more than a 10th of a second, and nothing elsewas happening anyway. And the Recolors.rxdata is 146 KB for two 4MB images, so it definitely saves on space.
 

Thank you for viewing

HBGames is a leading amateur video game development forum and Discord server open to all ability levels. Feel free to have a nosey around!

Discord

Join our growing and active Discord server to discuss all aspects of game making in a relaxed environment. Join Us

Content

  • Our Games
  • Games in Development
  • Emoji by Twemoji.
    Top