Indexed 16 colour palette simulation?

Questions about the LÖVE API, installing LÖVE and other support related questions go here.
Forum rules
Before you make a thread asking for help, read this.
Domarius
Prole
Posts: 11
Joined: Tue Jun 26, 2018 1:07 am

Re: Indexed 16 colour palette simulation?

Post by Domarius »

Stifu wrote: Thu Jul 05, 2018 9:52 am Why not? If it's fast enough...
True, I shouldn't say "couldn't", I would have to test on the target hardware :)

Though of course it's not nearly as efficient as true palette swapping :D
Domarius
Prole
Posts: 11
Joined: Tue Jun 26, 2018 1:07 am

Re: Indexed 16 colour palette simulation?

Post by Domarius »

Stifu wrote: Sun Jul 01, 2018 11:41 am We have color swaps in our game, and I think that's pretty much the same idea. We use shaders for that.
Our color swap definitions are here: https://github.com/thomasgoldstein/zabu ... haders.lua
Dude, I have to thank you. I got what I wanted in the end, with this video tutorial to get my head around what's going on, your example for some guidance, and my own programming experience.

I'm using the latest Love 11, so my colour rgb components are from 0 to 1, instead of 0 to 255, and I just use a source image to load the colour palette from (by getting the colour vaules from a row of pixels) rather than work out how to type them directly.

Here's the result - something that behaves pretty much like Pico 8, with it's fixed palette (I love that palette) and just simple commands to replace any of those colours with one of the others - using a 'base palette' which is never modified (to copy colours from) and a 'live palette' which is the one you modify.

The way you use it is to only create sprites that use the colours from Pico 8's palette (easy with Aseprite, you just load the built in Pico 8 palette), and the palette.png is a row of pixel colours, I've attached the sample project for anyone to unpack. (It includes a little chiptune song I made :) )

Basically you are a cowboy, and one of the enemies is identical to you but his hat is yellow, and his friend is drawn after and still identical to you, to demonstrate modifying the palette mid-draw to colourise individual sprites. Simply with pal(10,11), which swaps orange for yellow.

Below is the key parts - the shader that refers to the palettes, creating the palette, and using the pal functions to adjust the colours of the sprites (this code won't run on it's own, it's just for an overview).

Code: Select all

local shader_code = [[
extern vec4[16] base_palette;
extern vec4[16] live_palette;

vec4 effect(vec4 colour, Image image, vec2 uvs, vec2 screen_coords) {
	vec4 pixel = Texel(image, uvs);
	for(int i = 0; i < 16; i++){
		vec4 colour = base_palette[i];
		if(pixel == colour)
			return live_palette[i];
	}
	return pixel;
}
]]

function love.load()

	--SHADER STUFF
	--load Pico8 style palette for shader
	local palette_img = love.image.newImageData('assets/palette.png')
	for x = 1, palette_img:getWidth() do
        local r,g,b,a = palette_img:getPixel(x - 1, 0)
        base_palette[#base_palette + 1] = {r,g,b,a}
	end
	live_palette = {unpack(base_palette)}--quick way to copy table
	shader = love.graphics.newShader(shader_code)
	shader:send("base_palette", unpack(base_palette))
	shader:send("live_palette", unpack(live_palette))
end

function love.draw(dt)
	--START LOW RES
	push:start()
	--set the 16 colour palette shader
	love.graphics.setShader(shader)
	--Draw some text in the low res window
	love.graphics.print("Hello World", 0, 0)
	love.graphics.print("Again-------", 0, 48)
	--Draw a sprite in the low res window
	love.graphics.draw(player.img, player.x, player.y)
	--affect the palette of one enemy
	pal(10,11)
	love.graphics.draw(enemy.img, enemy.x, enemy.y)
	pal_reset()
	love.graphics.draw(enemy2.img, enemy2.x, enemy2.y)
	--Clear the shader
	love.graphics.setShader()
	--end the low res stuff, reset to desktop res
	push:finish()
	--END LOW RES
end

--source: index to change
--destination: index of colour to change it to
--[[ Uses the base pallete to get colours, so you don't
have to worry about the destination colour being 
different than you expect]]
function pal(s,d)
	live_palette[s] = base_palette[d]
	shader:send("live_palette", unpack(live_palette))
end
--quick version of above, doesn't send the whole array every time. 
--Use when making a number of changes, then finish with pal_update()
function pal_q(s,d)
	live_palette[s] = base_palette[d]
end
function pal_update()
	shader:send("live_palette", unpack(live_palette))
end
--resets pallete to default
function pal_reset()
	for i=1,#base_palette do
		live_palette[i]=base_palette[i]
	end
	shader:send("live_palette", unpack(live_palette))
end
--sets entire palette to the one colour, good for flashing a sprite when hit
function pal_flood(d)
	for i=1,#base_palette do
		live_palette[i]=base_palette[d]
	end
	shader:send("live_palette", unpack(live_palette))
end
Attachments
shader_palette_test.love
(64.64 KiB) Downloaded 359 times
User avatar
Stifu
Party member
Posts: 106
Joined: Mon Mar 14, 2016 9:53 am
Contact:

Re: Indexed 16 colour palette simulation?

Post by Stifu »

Domarius wrote: Sun Jul 08, 2018 10:42 am Dude, I have to thank you. I got what I wanted in the end, with this video tutorial to get my head around what's going on, your example for some guidance, and my own programming experience.
Nice. :) You're welcome.
Zabuyaki, our upcoming beat 'em up: https://www.zabuyaki.com
DarkShroom
Citizen
Posts: 86
Joined: Mon Jul 17, 2017 2:07 pm

Re: Indexed 16 colour palette simulation?

Post by DarkShroom »

i could do palette animations fast, i could just save the frames in advance... however it depends what you need of course

anyway i only really need this to take say my grey template images and add colour, myself i can only really be bothered for like say player colour, blue and red enemies on teams etc... way easier to just do some pixel editing for me (and i won't go near my shaders for that where it becomes harder and harder to string these shaders together, i already have gaussian there)... yeah though, no right or wrong just my solution, depends what you want
User avatar
pgimeno
Party member
Posts: 3672
Joined: Sun Oct 18, 2015 2:58 pm

Re: Indexed 16 colour palette simulation?

Post by pgimeno »

Domarius wrote: Sun Jul 08, 2018 10:42 am Below is the key parts - the shader that refers to the palettes, creating the palette, and using the pal functions to adjust the colours of the sprites (this code won't run on it's own, it's just for an overview).

Code: Select all

local shader_code = [[
...
	for(int i = 0; i < 16; i++){
		vec4 colour = base_palette[i];
		if(pixel == colour)
			return live_palette[i];
	}
I recommend against this approach. Long ago, I was told that shaders don't like conditional jumps (which if's and for's compile to).

I've made a benchmark. This code uses a slightly modified version of your shader, just a one-line change in order to take into account the current colour so I can use it to draw rectangles, to verify it's working as expected:

Code: Select all

local paletteShader = love.graphics.newShader[[
extern vec4[16] base_palette;
extern vec4[16] live_palette;

vec4 effect(vec4 colour, Image image, vec2 uvs, vec2 screen_coords) {
	vec4 pixel = Texel(image, uvs) * colour;
	for(int i = 0; i < 16; i++){
		vec4 colour = base_palette[i];
		if(pixel == colour)
			return live_palette[i];
	}
	return pixel;
}

]]

do
  local base_palette = {}
  for i = 0, 15 do
    base_palette[i + 1] = {i % 4 / 3, math.floor(i / 4) / 3, 0, 1}
  end
  paletteShader:send('base_palette', unpack(base_palette))
end

local function changePalette(t)
  paletteShader:send('live_palette', unpack(t))
end

local palette =
  {{0,  0, 0, 1}, {0,  0, .7, 1}, {.7,  0, 0, 1}, {.7,  0, .7, 1},
   {0, .7, 0, 1}, {0, .7, .7, 1}, {.7, .7, 0, 1}, {.7, .7, .7, 1},
   {.3,.3,.3, 1}, {0,  0,  1, 1}, { 1,  0, 0, 1}, { 1,  0,  1, 1},
   {0,  1, 0, 1}, {0,  1,  1, 1}, { 1,  1, 0, 1}, { 1,  1,  1, 1}}

function love.load(args)
  changePalette(palette)
end

local function setColourIndex(index)
  love.graphics.setColor(index%4 / 3, math.floor(index/4) / 3, 0, 1)
end

function love.draw()
  love.graphics.setShader(paletteShader)
  local w, h = love.graphics.getWidth()/4, love.graphics.getHeight()/4
  for index = 0, 15 do
    setColourIndex(index)
    local x, y = index % 4, math.floor(index / 4)
    love.graphics.rectangle("fill", x*w, y*h, w, h)
  end
  love.graphics.setShader()
  love.graphics.print(love.timer.getFPS())
end

function love.keypressed(k)
  if k == "escape" then return love.event.quit() end
end
I got ~400 FPS.

Then I tried this approach which uses the red and green components as the coordinates of the texture:

Code: Select all

local paletteShader = love.graphics.newShader[[

extern Image palette;

vec4 effect(vec4 colour, Image tex, vec2 pos, vec2 scr)
{
  vec4 pixel = Texel(tex, pos) * colour;
  return Texel(palette, pixel.xy);
}

]]

local paletteData = love.image.newImageData(4, 4)
local paletteImg-- = love.graphics.newImage(paletteData)
--paletteImg:setFilter("nearest", "nearest")

--love.graphics.setShader(paletteShader) -- this causes replacePixels() to cease working

local function changePalette(t)
  for y = 0, 3 do
    for x = 0, 3 do
      local pix = t[y * 4 + x + 1]
      paletteData:setPixel(x, y, pix[1], pix[2], pix[3], pix[4])
    end
  end
--  paletteImg:replacePixels(paletteData)
paletteImg = love.graphics.newImage(paletteData)
paletteImg:setFilter("nearest", "nearest")
  paletteShader:send('palette', paletteImg)
end

local palette =
  {{0,  0, 0, 1}, {0,  0, .7, 1}, {.7,  0, 0, 1}, {.7,  0, .7, 1},
   {0, .7, 0, 1}, {0, .7, .7, 1}, {.7, .7, 0, 1}, {.7, .7, .7, 1},
   {.3,.3,.3, 1}, {0,  0,  1, 1}, { 1,  0, 0, 1}, { 1,  0,  1, 1},
   {0,  1, 0, 1}, {0,  1,  1, 1}, { 1,  1, 0, 1}, { 1,  1,  1, 1}}

function love.load(args)
  changePalette(palette)
end

local function setColourIndex(index)
  love.graphics.setColor((index%4 + 0.5) / 4, (math.floor(index/4) + 0.5) / 4, 0, 1)
end

function love.draw()
  love.graphics.setShader(paletteShader)
  local w, h = love.graphics.getWidth()/4, love.graphics.getHeight()/4
  for index = 0, 15 do
    setColourIndex(index)
    local x, y = index % 4, math.floor(index / 4)
    love.graphics.rectangle("fill", x*w, y*h, w, h)
  end
  love.graphics.setShader()
  love.graphics.print(love.timer.getFPS())
end

function love.keypressed(k)
  if k == "escape" then return love.event.quit() end
end
I got ~870 FPS.

My conclusion is that the person who told me that shaders don't like conditional jumps was quite right, and that it's preferable to avoid them where possible.

I couldn't make it work with (Image):replacePixels for some reason. I asked in IRC but got no response.

Edit: Edited the second program to make it target pixel centres, for enhanced precision. The replacePixels problem is definitely a bug, most likely in my graphics driver but I'm still hoping for someone to confirm it. It's triggered by activating a shader that uses a sampler2D uniform (aka extern Image), regardless of whether that shader is used or not.
Domarius
Prole
Posts: 11
Joined: Tue Jun 26, 2018 1:07 am

Re: Indexed 16 colour palette simulation?

Post by Domarius »

DarkShroom wrote: Tue Jul 10, 2018 2:42 pm i could do palette animations fast, i could just save the frames in advance... however it depends what you need of course
Yes absolutely! I totally understand.
pgimeno wrote: Tue Jul 10, 2018 7:09 pm I recommend against this approach. Long ago, I was told that shaders don't like conditional jumps (which if's and for's compile to).
Heheh, well it's exactly what @Stifu is doing in his zabayuki game too :)

Thank you, I love that someone is taking the time to optimse my code.

Actually I originally wanted to use the RGB values as some sort of array lookup, but loop and if, while clearly not what you want in rendering code, was quick and got the job done. I figured if there were performance problems on the lowest spec hardware I might revisit it.

But I'm new to this and hadn't thought of using the RGB as some sort of lookup into a texture.

I have a couple questions;

1. In your example, you use only the R and G components to look up a colour reference. But that wouldn't perfectly identify the colour being used of course, because it's missing B. What's a reliable way I can use R G and B as some sort of direct look up, into a texture?

2. If we're using a texture to look up the final colour value, can we easily edit this texture in real time every frame so that each time the same colour is looked up, it changes, to create palette animations?
User avatar
pgimeno
Party member
Posts: 3672
Joined: Sun Oct 18, 2015 2:58 pm

Re: Indexed 16 colour palette simulation?

Post by pgimeno »

Domarius wrote: Wed Jul 11, 2018 2:26 am Heheh, well it's exactly what @Stifu is doing in his zabayuki game too :)
Maybe Stifu can take advantage of that technique too :) But his purpose is a bit different, so maybe not.

Domarius wrote: Wed Jul 11, 2018 2:26 am 1. In your example, you use only the R and G components to look up a colour reference. But that wouldn't perfectly identify the colour being used of course, because it's missing B. What's a reliable way I can use R G and B as some sort of direct look up, into a texture?
You need to encode the palette index somehow as a colour, in a way that you can easily decode it in the shader. I chose an encoding that uses the R and G components, but any encoding works. For example, using 16 different intensities of a single component would work as well. Using R, G, B and A as if they were binary digits would work too (but it would be harder to work with in a drawing program). Or any other encoding.

By using 16 intensities, you could just make the images in greyscale, which would probably be the simplest, and the translation to colour index is more intuitive and easier to work with in a drawing program (assuming there's no gamma or colour management interfering).

Domarius wrote: Wed Jul 11, 2018 2:26 am 2. If we're using a texture to look up the final colour value, can we easily edit this texture in real time every frame so that each time the same colour is looked up, it changes, to create palette animations?
That's the idea. The replacePixels bug hinders it somewhat, though. You can still use an array instead of an image for palette lookup. I probably was overcomplicating things by using an image.

Code: Select all

local paletteShader = love.graphics.newShader[[

extern vec4 palette[16];

vec4 effect(vec4 colour, Image tex, vec2 pos, vec2 scr)
{
  vec4 pixel = Texel(tex, pos) * colour;
  return palette[int(pixel.x * 16.0 + 0.5)];
}

]]

love.graphics.setShader(paletteShader)

local function changePalette(t)
  paletteShader:send('palette', unpack(t))
end

local palette =
  {{0,  0, 0, 1}, {0,  0, .7, 1}, {.7,  0, 0, 1}, {.7,  0, .7, 1},
   {0, .7, 0, 1}, {0, .7, .7, 1}, {.7, .7, 0, 1}, {.7, .7, .7, 1},
   {.3,.3,.3, 1}, {0,  0,  1, 1}, { 1,  0, 0, 1}, { 1,  0,  1, 1},
   {0,  1, 0, 1}, {0,  1,  1, 1}, { 1,  1, 0, 1}, { 1,  1,  1, 1}}

function love.load(args)
  changePalette(palette)
end

local function setColourIndex(index)
  love.graphics.setColor((index + 0.5) / 16, 0, 0, 1)
end

function love.draw()
  love.graphics.setShader(paletteShader)
  local w, h = love.graphics.getWidth()/4, love.graphics.getHeight()/4

  -- Change red component of index 3 every frame
  palette[4][1] = love.timer.getTime() % 1
  changePalette(palette)

  for index = 0, 15 do
    setColourIndex(index)
    local x, y = index % 4, math.floor(index / 4)
    love.graphics.rectangle("fill", x*w, y*h, w, h)
  end
  love.graphics.setShader()
  love.graphics.setColor(1,1,1,1)
  love.graphics.print(love.timer.getFPS())
end

function love.keypressed(k)
  if k == "escape" then return love.event.quit() end
end
Also ~870 FPS.
Domarius
Prole
Posts: 11
Joined: Tue Jun 26, 2018 1:07 am

Re: Indexed 16 colour palette simulation?

Post by Domarius »

pgimeno wrote: Wed Jul 11, 2018 7:52 am By using 16 intensities, you could just make the images in greyscale, which would probably be the simplest, and the translation to colour index is more intuitive and easier to work with in a drawing program (assuming there's no gamma or colour management interfering).
Ahh, you have opened my eyes! I'm going to have to try this out!

Maybe I can come up with something that converts all the assets to some special 16 intensity format automatically.

In fact it probably wouldn't even be that slow to do during the game when the sprite is first loaded, with the tiny sprites I'll be using.

And thank you very much for the demonstration code, that's proof it indeed works as designed.

I always wondered if "send" was expensive, sending the entire array every frame. Seems like the biggest hog was the loop and the if...
Remains to be seen once we have several sprites changing their colours during one frame...
User avatar
Stifu
Party member
Posts: 106
Joined: Mon Mar 14, 2016 9:53 am
Contact:

Re: Indexed 16 colour palette simulation?

Post by Stifu »

pgimeno wrote: Wed Jul 11, 2018 7:52 am
Domarius wrote: Wed Jul 11, 2018 2:26 am Heheh, well it's exactly what @Stifu is doing in his zabayuki game too :)
Maybe Stifu can take advantage of that technique too :) But his purpose is a bit different, so maybe not.
I'll see about that with the game programmer, because although I can code, I'm actually the graphic artist for that game. Thanks for the heads up. :)
Zabuyaki, our upcoming beat 'em up: https://www.zabuyaki.com
User avatar
Ref
Party member
Posts: 702
Joined: Wed May 02, 2012 11:05 pm

Re: Indexed 16 colour palette simulation?

Post by Ref »

Hey pgimeno!
Late to the party but probably missing the point.
Can do the same thing as your example without a shader.

Code: Select all

local palette =
  {{0,  0, 0, 1}, {0,  0, .7, 1}, {.7,  0, 0, 1}, {.7,  0, .7, 1},
   {0, .7, 0, 1}, {0, .7, .7, 1}, {.7, .7, 0, 1}, {.7, .7, .7, 1},
   {.3,.3,.3, 1}, {0,  0,  1, 1}, { 1,  0, 0, 1}, { 1,  0,  1, 1},
   {0,  1, 0, 1}, {0,  1,  1, 1}, { 1,  1, 0, 1}, { 1,  1,  1, 1}}

function love.load(  )
	end

function love.draw()
	local w, h = love.graphics.getWidth()/4, love.graphics.getHeight()/4
	-- Change red component of index 3 every frame
	palette[4][1] = love.timer.getTime() % 1
	for index = 0, 15 do
		love.graphics.setColor(unpack(palette[index+1]))
		local x, y = index % 4, math.floor(index / 4)
		love.graphics.rectangle( 'fill', x*w, y*h, w, h )
		end
	love.graphics.setColor( 1,1,1,1 )
	love.graphics.print(love.timer.getFPS())
	end

function love.keypressed( k )
	if k == "escape" then return love.event.quit() end
	end
Probably missing the boat but I prefer flying.
Best
Post Reply

Who is online

Users browsing this forum: No registered users and 2 guests