Dithering with matrix

Showcase your libraries, tools and other projects that help your fellow love users.
User avatar
darkfrei
Party member
Posts: 1204
Joined: Sat Feb 08, 2020 11:09 pm

Dithering with matrix

Post by darkfrei »

Hi all!

Here is a small tool to make dithering with matrix 3x3:

Code: Select all

-- dithering with matrix 3x3 pixels
local function dithering(data)
	local width, height = data:getDimensions()
	local canvasImageData = love.image.newImageData(width, height)
	local matrix = {
		{ 230, 51, 128 },
		{ 25, 102, 179 },
		{ 154,205, 77  },
	}	

	local sum = 0
	for i = 1, 10 do
		local v = 256/10 + (i-1)*256/10
		print (v)
		sum = sum + v
	end
	
	for y = 0, height - 1 do
		for x = 0, width - 1 do
			local r, g, b, a = love.math.colorToBytes(data:getPixel(x, y))
			local i, j = x%3+1, y%3+1
			local matrixValue = matrix[j][i]
			r = r >= matrixValue and 1 or 0
			g = g >= matrixValue and 1 or 0
			b = b >= matrixValue and 1 or 0
			canvasImageData:setPixel(x, y, r, g, b, a)
		end
	end
	local canvas = love.graphics.newImage(canvasImageData)
	return canvas
end

return dithering
2023-03-08-dithering.png
2023-03-08-dithering.png (61.02 KiB) Viewed 24113 times
Attachments
dithering-01.love
License CC0
(143.89 KiB) Downloaded 605 times
:awesome: in Lua we Löve
:awesome: Platformer Guide
:awesome: freebies
User avatar
dusoft
Party member
Posts: 655
Joined: Fri Nov 08, 2013 12:07 am
Location: Europe usually
Contact:

Re: Dithering with matrix

Post by dusoft »

Nice!
User avatar
Bigfoot71
Party member
Posts: 287
Joined: Fri Mar 11, 2022 11:07 am

Re: Dithering with matrix

Post by Bigfoot71 »

Super cool, I love it!

Small suggestion, there is the mapPixel function to do this more efficiently, I slightly rewrote the function for this:

Code: Select all

-- dithering matrix 3x3

local matrix = {
	{ 230, 51, 128 },
	{ 25, 102,  179 },
	{ 154, 205, 77 },
}

local function dithering(data)

	local clone = data:clone()

	clone:mapPixel(function(x, y, r, g, b, a)
		local matrixValue = matrix[(y%3)+1][(x%3)+1]
		r = r*255 >= matrixValue and 1 or 0
		g = g*255 >= matrixValue and 1 or 0
		b = b*255 >= matrixValue and 1 or 0
		return r, g, b, a
	end)

	local image = love.graphics.newImage(clone)
	clone:release()

	return image

end

return dithering

Edit: I just made a small shader quickly, the effect is different but I love the rendering!

Code: Select all

extern float matrix[9];

vec4 effect(vec4 color, Image tex, vec2 texCoords, vec2 screenCoords) {

    vec4 pixel = Texel(tex, texCoords);

    int x = int(mod(screenCoords.x, 3.0));
    int y = int(mod(screenCoords.y, 3.0));
    float matrixValue = matrix[x+y*3]/255.0;

    float r = step(matrixValue, pixel.r);
    float g = step(matrixValue, pixel.g);
    float b = step(matrixValue, pixel.b);

    return vec4(r, g, b, pixel.a);

}
Image

Here's how to have the same effect as the origin, you have to work on the coordinates of the texture (uv*img_dimensions) and not of the screen:

Code: Select all

extern float matrix[9];
extern vec2 img_dim;

vec4 effect(vec4 color, Image tex, vec2 texCoords, vec2 screenCoords) {

    vec4 pixel = Texel(tex, texCoords);

    int x = int(mod(texCoords.x*img_dim.x, 3.0));
    int y = int(mod(texCoords.y*img_dim.y, 3.0));
    float matrixValue = matrix[x+y*3]/255.0;

    float r = step(matrixValue, pixel.r);
    float g = step(matrixValue, pixel.g);
    float b = step(matrixValue, pixel.b);

    return vec4(r, g, b, pixel.a);

}
Image
My avatar code for the curious :D V1, V2, V3.
User avatar
darkfrei
Party member
Posts: 1204
Joined: Sat Feb 08, 2020 11:09 pm

Re: Dithering with matrix

Post by darkfrei »

The color dithering.

1. Find 2 nearest palette colors (point 1 and point 2) in 3D space to the current pixel color:

Code: Select all

function nearestColorIndex(r, g, b, palette)
	local firstIndex = 1
	local secondIndex = 1
	local min1 = math.huge
	local min2 = math.huge
	for index, color in ipairs(palette) do
		local dr = r - color[1]
		local dg = g - color[2]
		local db = b - color[3]
		local value = dr*dr + dg*dg + db*db
		if value < min1 then
			min2 = min1
			min1 = value
			secondIndex = firstIndex
			firstIndex = index
		elseif value < min2 then
			secondIndex = index
			min2 = value
		end
	end
	return firstIndex, secondIndex
end
2. Make the line between this two palette colors.
3. Project the current color point to this line and get the value t, that changes from 0 to 1 and represents x=x1+t*(x2-x1); y=y1+t*(y2-y1); z=z1+t*(z2-z1):

Code: Select all

function projectPointOnLineSegment(aX, aY, aZ, bX, bY, bZ, cX, cY, cZ)
    local dx, dy, dz = bX - aX, bY - aY, bZ - aZ
    local len2 = (dx*dx + dy*dy + dz*dz)
		if len2 == 0 then
			return 0
		end
    local dot = (cX - aX)*dx + (cY - aY)*dy + (cZ - aZ)*dz
    local t = dot / len2^0.5
    return t
end
Use this t as comparing value to the matrix:

Code: Select all

local ditherMatrix = {
	{0, 7, 3},
	{6, 5, 2},
	{4, 1, 8}
}
4. Draw color 1 if the value is lower or color 2 if not.

Code: Select all

-- License CC0 (Creative Commons license) (c) darkfrei, 2023

local imageName = 'lenna-512.png'
--local imageName = 'ball.png'

local palettes = {}
local i = 0
for line in love.filesystem.lines("palettes.txt") do
	i = i + 1
	local palette = loadstring ("return " .. line)()
	table.insert (palettes, palette)
end
paletteID = 129

palette = palettes[paletteID]

local lennaPalette = {
		{206/255,95/255,93/255},
		{230/255,133/255,128/255},
		{222/255,106/255,99/255},
		{195/255,127/255,120/255},
		{231/255,198/255,199/255},
		{96/255,23/255,62/255},
		{138/255,96/255,150/255},
		{227/255,93/255,105/255},
		{241/255,204/255,190/255},
		{237/255,182/255,167/255},
	}
table.insert (palettes, lennaPalette)

local nPal = {}
for r = 0, 3 do
	for g = 0, 3 do
		for b = 0, 3 do
			table.insert (nPal, {r/3, g/3, b/3})
		end
	end
end
palettes[128] = nPal

nPal = {}
for r = 0, 2 do
	for g = 0, 2 do
		for b = 0, 2 do
			table.insert (nPal, {r/2, g/2, b/2})
		end
	end
end
palettes[130] = nPal
palettes[131] = {{0,0,0}, {1,1,1}}
palettes[132] = {{0,0,0}, {0.5,0.5,0.5}, {1,1,1}}
palettes[133] = {{211/255, 219/255, 233/255}, {129/255, 154/255, 193/255}, {18/255,64/255, 138/255}, {153/255,123/255, 75/255}}

paletteID = 124
palette = palettes[paletteID]
love.window.setTitle ('palette ' .. paletteID .. ' ('..#palette..' colors)')

function nearestColorIndex(r, g, b, palette)
	local firstIndex = 1
	local secondIndex = 1
	local min1 = math.huge
	local min2 = math.huge
	for index, color in ipairs(palette) do
		local dr = r - color[1]
		local dg = g - color[2]
		local db = b - color[3]
		local value = dr*dr + dg*dg + db*db
		if value < min1 then
			min2 = min1
			min1 = value
			secondIndex = firstIndex
			firstIndex = index
		elseif value < min2 then
			secondIndex = index
			min2 = value
		end
	end
	return firstIndex, secondIndex
end

local ditherMatrix = {
	{0, 7, 3},
	{6, 5, 2},
	{4, 1, 8}
}

function projectPointOnLineSegment(aX, aY, aZ, bX, bY, bZ, cX, cY, cZ)
    local dx, dy, dz = bX - aX, bY - aY, bZ - aZ
    local len2 = (dx*dx + dy*dy + dz*dz)
		if len2 == 0 then
			return 0
		end
    local dot = (cX - aX)*dx + (cY - aY)*dy + (cZ - aZ)*dz
    local t = dot / len2^0.5
    return t
end

function getValue (x, y)
	local r, g, b = 0, 0, 0
	if x < imageData:getWidth() and y < imageData:getHeight()
	and x > 0 and y > 0 then
		r, g, b = imageData:getPixel(x-1, y-1)
	end
	local firstIndex, secondIndex = nearestColorIndex(r, g, b, palette)
	local first, second	= palette[firstIndex], palette[secondIndex]
	local ditherValue = ditherMatrix[(y-1)%3+1][(x-1)%3+1]

	local compareValue = -0.25+4.5*projectPointOnLineSegment(
		first[1], first[2], first[3], 
		second[1], second[2], second[3], 
		r,g,b)

	if firstIndex < secondIndex  then
		-- magic!
		ditherValue = 4-(ditherValue -4)
	end
	
	if (ditherValue > compareValue) then
		r, g, b = first[1], first[2], first[3]
	else
		r, g, b = second[1], second[2], second[3]
	end

	return r, g, b, ditherValue, compareValue
end

function dither3x3(path, palette)
	imageData = love.image.newImageData(path)
	local width, height = imageData:getDimensions()
	for y = 1, height do
		for x = 1, width do
			local r, g, b = getValue (x, y)
			imageData:setPixel(x-1, y-1, r,g,b)
		end
	end
	return love.graphics.newImage(imageData)
end

function love.load()
	Image = dither3x3(imageName, palette)
	Image2 = love.graphics.newImage(imageName)
	Width = Image2:getWidth()
	love.window.setMode(Width*2, Image2:getHeight())
end

function love.draw()
	love.graphics.setColor (1,1,1)
	love.graphics.draw (Image2)
	love.graphics.draw (Image, Width, 0)
	love.graphics.print ('press SPACE to change palette')
end

function love.keypressed(key, scancode, isrepeat)
	if key == "space" then
		paletteID = paletteID + 1
		if paletteID > #palettes then paletteID = 1 end
		
		palette = palettes[paletteID]
		love.window.setTitle ('palette ' .. paletteID .. ' ('..#palette..' colors)')
		Image = dither3x3(imageName, palette)
	elseif key == "escape" then
		love.event.quit()
	end
end
Attachments
2023-04-27T15_22_42-palette 133 (4 colors).png
2023-04-27T15_22_42-palette 133 (4 colors).png (307.68 KiB) Viewed 23133 times
2023-04-28T08_38_55-palette 131 (2 colors).png
2023-04-28T08_38_55-palette 131 (2 colors).png (755.13 KiB) Viewed 23134 times
palette-dithering-03.png
palette-dithering-03.png (781.82 KiB) Viewed 23135 times
palette-dithering-03.love
(737.23 KiB) Downloaded 566 times
:awesome: in Lua we Löve
:awesome: Platformer Guide
:awesome: freebies
User avatar
darkfrei
Party member
Posts: 1204
Joined: Sat Feb 08, 2020 11:09 pm

Re: Dithering with matrix

Post by darkfrei »

The gradient can be much smooth:

Code: Select all

function projectPointOnLineSegment(aX, aY, aZ, bX, bY, bZ, cX, cY, cZ)
    local dx, dy, dz = bX - aX, bY - aY, bZ - aZ
    local len2 = (dx*dx + dy*dy + dz*dz)
		if len2 == 0 then
			return 0
		end
    local dot = (cX - aX)*dx + (cY - aY)*dy + (cZ - aZ)*dz
    local t = dot / len2^0.5
		-- https://www.desmos.com/calculator/gnq3hm26t0
--		t = 4*(t-0.5)^3 + 0.5
		t = -8*(t-0.5)^4+0.5
		if t > 0.5 then
			print (t)
		end
    return t
end
Attachments
2023-04-28-palette 125 (4 colors).png
2023-04-28-palette 125 (4 colors).png (790.89 KiB) Viewed 23131 times
2023-04-28-palette 152 (10 colors).png
2023-04-28-palette 152 (10 colors).png (819.68 KiB) Viewed 23131 times
:awesome: in Lua we Löve
:awesome: Platformer Guide
:awesome: freebies
tourgen
Citizen
Posts: 53
Joined: Sat Mar 18, 2023 12:45 am

Re: Dithering with matrix

Post by tourgen »

great work! I'm probably going to use this.. or something similar. I want an CGA and EGA mode for my little text adventure. now it has some still gfx... AI generated
nSun
Prole
Posts: 1
Joined: Tue Nov 21, 2023 1:22 pm

Re: Dithering with matrix

Post by nSun »

I'm a beginner with Löve, and working on my first project (to test) based on this topic.
Particularly on Error-diffusion dithering algorithms:
Floyd–Steinberg, Jarvis, Judice & Ninke, Stucki, Burkes, Sierra, Two-row Sierra, Sierra Lite, Atkinson...

https://en.wikipedia.org/wiki/Dither#Algorithms

love-dithering-tests.png
love-dithering-tests.png (37.29 KiB) Viewed 20857 times

Just drop your file/picture in the window then keypress numpad :
0: Gray scale
1: Atkinson
2: Floyd-Steinberg
3: Jarvis, Judice, and Ninke Dithering
4: Stucki
5: Burkes
6: Sierra
7: Sierra 2 rows
8: Sierra Lite
9: none (original)

I use a "sensitive" grayscale :

Code: Select all

local red, green, blue, alpha = self.data:getPixel( x, y )
-- logical grayscale 
-- color = (red + green + blue) /3
-- sensitive grayscale 
color = 0.3 * red + 0.59 * green + 0.11 * blue
Because a "logical" gray scale produces a darker image. But to the eye, a 100% red appears darker than a 100% green.


I used a class that I had coded in PHP/CLI tool, excuse me if it's not clean...
Attachments
main.lua
(2.6 KiB) Downloaded 442 times
dithering.lua
(16.75 KiB) Downloaded 450 times
classic.lua
(1.05 KiB) Downloaded 445 times
Trystan
Prole
Posts: 15
Joined: Fri Nov 24, 2023 9:30 am

Re: Dithering with matrix

Post by Trystan »

I was playing around with Dithering the other day and thought I'd add my script here. It's nothing that hasn't been done (better) above but it was fun to create.

There are a few different dithers and colour reduction methods you can do by changing the arguments of the dither function. My dither matrices are stored in a kind of unusual way, instead of being a proper matrix they're a table of tables with an xoffset, yoffset and then the proportion of error to be diffused to that position.

I had used this originally to dither images for pico-8 that could then be loaded directly to vram from a string so there are a few lines commented out that create that string and print it to the console, I don't reccomend turning them on for a large image, they're very slow.

Finally, depending on the size of the picture you're doing you will need to change the "scale" variable. I could have made it autoadjust but didn't really feel that was necessary for what I wanted it to do.

Dithering to the pico-8 palette (Floyd-Steinberg)
Dither Pico8.png
Dither Pico8.png (102.93 KiB) Viewed 18860 times
Dithering to 5 bit colour (4 values of red, 4 of green, 2 of blue) (Floyd-Steinberg)
Dither 442.png
Dither 442.png (88.02 KiB) Viewed 18860 times
The pico-8 code I was using to load an image was simply the below that replaces everything in vram with the contents of a string. Note, this string could be a lot shorter with some simple RLE but I was just wanting to see something on screen.

Code: Select all

function _init()
 imgstr="[paste string here]"
 loadpic(imgstr)
 palt(0,false)
end

function loadpic(imgstr)
 for i=1,#imgstr,2 do
  local n1=tonum(imgstr[i],0x1)
  local n2=tonum(imgstr[i+1],0x1)
  poke(0x6000+(i-1)/2,n2*16+n1)
 end
end

function _update()

end

function _draw()

end
And this is the love2d file for doing the dithering:

Code: Select all

-- Disable output buffer so debug messages print in real time
io.stdout:setvbuf("no")

function love.load()
    love.graphics.setDefaultFilter("nearest", "nearest", 1)
    love.window.setMode(1536, 768)
    love.window.setTitle("Error Diffusion Dithering")
    scale = 6 -- scale to draw the result at
    imgPath = "Images/Rainbow128.jpg" -- image to use

    -- pico-8 palette for paletted quantizing
    pal = {}
    pal.Pico8 = {
        {29/255, 43/255, 83/255},
        {126/255, 37/255, 83/255},
        {0/255, 135/255, 81/255},
        {171/255, 82/255, 54/255},
        {95/255, 87/255, 79/255},
        {194/255, 195/255, 199/255},
        {255/255, 241/255, 232/255},
        {255/255, 0/255, 77/255},
        {255/255, 163/255, 0/255},
        {255/255, 236/255, 39/255},
        {0/255, 228/255, 54/255},
        {41/255, 173/255, 255/255},
        {131/255, 118/255, 156/255},
        {255/255, 119/255, 168/255},
        {255/255, 204/255, 170/255},
        {0/255, 0/255, 0/255},
    }
    palStr = ""

    -- Setup dither matrices matrix, each is a table of {xoff, yoff, proportion of error to give}
    dMat = {}
    dMat.FloydSteinberg = {
        { 1, 0, 7/16},
        {-1, 1, 3/16},
        { 0, 1, 5/16},
        { 1, 1, 1/16},
    }
    dMat.Atkinson = {
        { 1, 0, 1/8},
        { 2, 0, 1/8},
        {-1, 1, 1/8},
        { 0, 1, 1/8},
        { 1, 1, 1/8},
        { 0, 2, 1/8},
    }
    dMat.JarvisJudiceNinke = {
        { 1, 0, 7/48},
        { 2, 0, 5/48},
        {-2, 1, 3/48},
        {-1, 1, 5/48},
        { 0, 1, 7/48},
        { 1, 1, 5/48},
        { 2, 1, 3/48},
        {-2, 2, 1/48},
        {-1, 2, 3/48},
        { 0, 2, 5/48},
        { 1, 2, 3/48},
        { 2, 2, 1/48},
    }
    dMat.None = {}

    local originalData = love.image.newImageData(imgPath)
    originalImg = love.graphics.newImage(originalData)
    --transformedImg = dither(originalData, quantPalette, pal.Pico8, false, dMat.FloydSteinberg)
    transformedImg = dither(originalData, quantReduce, {4, 4, 2}, false, dMat.FloydSteinberg)
end

function quantReduce(r, g, b, valuesPerColour)
    local lr, lg, lb
    if type(valuesPerColour) == "table" then
        -- we have a table, set levels seperately
        lr, lg, lb = valuesPerColour[1], valuesPerColour[2], valuesPerColour[3]
    else
        -- we have a number, set all the same
        lr, lg, lb = valuesPerColour, valuesPerColour, valuesPerColour
    end
    r = math.floor(r * (lr - 0.5)) / (lr - 1)
    g = math.floor(g * (lg - 0.5)) / (lg - 1)
    b = math.floor(b * (lb - 0.5)) / (lb - 1)
    return r, g, b
end

function quantPalette(r, g, b, pal)
    local dist, bestInd, bestDist
    bestDist = 1000
    for i = 1, #pal do
        dist = math.abs(r - pal[i][1]) + math.abs(g - pal[i][2]) + math.abs(b - pal[i][3])
        if dist < bestDist then
            bestDist = dist
            bestInd = i
        end
    end
    -- uncomment to add the char to the palette string
    -- palStr = palStr .. string.format("%x", bestInd % 16)
    return pal[bestInd][1], pal[bestInd][2], pal[bestInd][3]
end

-- Dithers an imageData. 2nd argument is the quantization function (quantReduce to reduce colour bit depth and quantPalette to match to a palette). 
-- quantArg is a number of values per colour for reduce (or a table of 3 like {8, 8, 4} for classic 8-bit reduction). 
-- quantArg is a palette table of colours for the palette. Greyscale sets if the image should be turned greyscale first. ditherMatric sets the matric to use.
function dither(data, quantFunc, quantArg, greyscale, ditherMatrix)
    local r, g, b, er, eg, eb, val, tx, ty
    local height = data:getHeight() - 1
    local width = data:getWidth() - 1
    -- loop through all pixels, with x as inner loop
    for y = 0, height do
        for x = 0, width do
            -- get pixel
            r, g, b = data:getPixel(x, y)
            if greyscale then
                -- make greyscale
                val = (0.299 * r) + (0.587 * g) + (0.114 * b)
                r = val; g = val; b = val
            end
            -- backup r, g, b for working out the error later
            er, eg, eb = r, g, b
            -- quantize
            r, g, b = quantFunc(r, g, b, quantArg)
            data:setPixel(x, y, r, g, b, 1)
            -- get difference from original pixel (error)
            er = er - r
            eg = eg - g
            eb = eb - b
            -- pass error on to neighbours
            for i = 1, #ditherMatrix do
                tx = x + ditherMatrix[i][1]
                ty = y + ditherMatrix[i][2]
                -- check it's in bounds, we don't need to check ty > 0 though, we never pass error up
                if tx > 0 and tx < width and ty < height then
                    r, g, b = data:getPixel(tx, ty)
                    r = r + (er * ditherMatrix[i][3])
                    g = g + (eg * ditherMatrix[i][3])
                    b = b + (eb * ditherMatrix[i][3])
                    data:setPixel(tx, ty, r, g, b, 1)
                end
            end
        end
    end
    -- uncomment to print the palette string to the console
    -- print(palStr)
    return love.graphics.newImage(data)
end

function love.draw()
    -- draw original image
    love.graphics.draw(originalImg, 0, 0, 0, scale, scale)
    -- draw dithered image
    love.graphics.draw(transformedImg, 768, 0, 0, scale, scale)
end
Trystan
Prole
Posts: 15
Joined: Fri Nov 24, 2023 9:30 am

Re: Dithering with matrix

Post by Trystan »

I was thinking about dithing the other day and played around with the idea of error being pushed to the right and downwards through a maze (where the mazes passability allows it).
CurveDither.png
CurveDither.png (325 KiB) Viewed 17688 times
The attached uses this for a few different wall layouts, "t" toggles showing the layout on the left picture:
F1 - All walls on (no error is passed so this is just quantizing each pixel to black and white based on the threshold)
F2 - All walls cleared, error is free to go down and right forever
F3 - Random walls
F4 - Walls generated by a maze algorithm (for both this and random "a" toggles animation)
F5 - Error is pushed along the path of a Hilbert Curve (starting in the bottom left)
F6 - Generates the Hilbert Curve but error is pushed down and right from the top left as with the other layouts.

It generates some fun results but nothing that to my eye looks any better than the faster methods above. There is room though for trying different curves/layouts, changing how you loop through the grid and where you push error to and maybe pushing the error further to spread it over more cells

The main code is here:

Code: Select all

-- https://en.wikipedia.org/wiki/Space-filling_curve
-- https://en.wikipedia.org/wiki/Maze_generation_algorithm

-- Disable output buffer so debug messages print in real time
io.stdout:setvbuf("no")

function love.load()
    bit = require "bit"
    math.randomseed(os.time())
    math.random(); math.random(); math.random()
    love.window.setTitle("Space-filling curve Dithering")
    love.graphics.setDefaultFilter("nearest", "nearest", 1)
    imgData = love.image.newImageData("David.png")
    origImg = love.graphics.newImage(imgData)
    scale = 6
    imgSize = imgData:getWidth()
    love.window.setMode(imgSize * scale * 2, imgSize * scale)

    threshold = 0.5 -- The threshold for seperating white and black pixels
    drawGridToggle = false
    animateDither = true
    animationDelay = 20
    timer = 0

    currentCurve = setWallsZero
    currentCurve()
end

function love.keypressed(key)
    if key == "f1" then
        currentCurve = setWallsZero
        currentCurve()
    end
    if key == "f2" then
        currentCurve = setWallsMax
        currentCurve()
    end
    if key == "f3" then
        currentCurve = setWallsRandom
        currentCurve()
    end
    if key == "f4" then
        currentCurve = setWallsBacktracker
        currentCurve()
    end
    if key == "f5" then
        currentCurve = setWallsHilbertFollow
        currentCurve()
    end
    if key == "f6" then
        currentCurve = setWallsHilbert
        currentCurve()
    end
    if key == "t" then
        drawGridToggle = not drawGridToggle
    end
    if key == "a" then
        animateDither = not animateDither
    end
    if key == "escape" then
        love.event.quit()
    end
end

function resetCellGrid()
    -- cellGrid will hold a number of allowed directions, 1 = up, 2 = right, 4 = down, 8 = left
    -- 0 is unvisited
    -- the second entry is the red from our picture, reset this each run as we mess with cellGrid when dithering
    cellGrid = {}
    for x = 0, imgSize - 1 do
        cellGrid[x] = {}
        for y = 0, imgSize - 1 do
            cellGrid[x][y] = {0, imgData:getPixel(x, y)}
        end
    end
end

function setWallsZero()
    resetCellGrid()
    -- set no passability, essentialy threshold shading
    for x = 0, imgSize - 1 do
        for y = 0, imgSize - 1 do
            cellGrid[x][y][1] = 0
        end
    end
    if ditheredImg then ditheredImg:release() end
    ditheredImg = applyDither()
end

function setWallsMax()
    resetCellGrid()
    -- set all passability, allowing error to travel forever
    for x = 0, imgSize - 1 do
        for y = 0, imgSize - 1 do
            cellGrid[x][y][1] = 15
            if x == imgSize - 1 then
                cellGrid[x][y][1] = bit.band(cellGrid[x][y][1], 13)
            end
            if y == imgSize - 1 then
                cellGrid[x][y][1] = bit.band(cellGrid[x][y][1], 11)
            end
        end
    end
    if ditheredImg then ditheredImg:release() end
    ditheredImg = applyDither()
end

function setWallsRandom()
    resetCellGrid()
    -- set passability randomly, block off last row and column
    for x = 0, imgSize - 1 do
        for y = 0, imgSize - 1 do
            cellGrid[x][y][1] = math.random(16)
            if x == imgSize - 1 then
                cellGrid[x][y][1] = bit.band(cellGrid[x][y][1], 13)
            end
            if y == imgSize - 1 then
                cellGrid[x][y][1] = bit.band(cellGrid[x][y][1], 11)
            end
        end
    end
    if ditheredImg then ditheredImg:release() end
    ditheredImg = applyDither()
end

function setWallsBacktracker()
    resetCellGrid()
    -- set passability with a recursive backtracker
    local currX, currY = 0, 0
    local neighbours, r, dir
    local stack = {{currX, currY}}
    while #stack > 0 do
        -- get neightbours
        neighbours = getNeighbours(currX, currY)
        if #neighbours == 0 then
            -- we have no eligible neighbours, step back
            table.remove(stack)
            if #stack > 0 then
                currX, currY = stack[#stack][1], stack[#stack][2]
            end
        else
            -- we have neighbours, get a random one
            r = math.random(#neighbours)
            dir = neighbours[r][3]
            -- set our exit
            cellGrid[currX][currY][1] = bit.bor(cellGrid[currX][currY][1], dir)
            -- set new current
            currX = neighbours[r][1]
            currY = neighbours[r][2]
            table.insert(stack, {currX, currY})
            -- set our entry
            if dir == 1 then
                dir = 4
            elseif dir == 2 then
                dir = 8
            elseif dir == 4 then
                dir = 1
            else
                dir = 2
            end
            cellGrid[currX][currY][1] = bit.bor(cellGrid[currX][currY][1], dir)
        end
    end
    if ditheredImg then ditheredImg:release() end
    ditheredImg = applyDither()
end

function setWallsHilbert()
    resetCellGrid()
    if ditheredImg then ditheredImg:release() end
    local hText = require "hilbert"
    local dir = 2
    local currX, currY = 0, 127
    local char
    for i = 1, #hText do
        char = hText:sub(i, i)
        if char == "-" then
            -- turn left
            dir = dir / 2
            if dir < 1 then dir = 8 end
        elseif char == "+" then
            -- turn right
            dir = dir * 2
            if dir > 8 then dir = 1 end
        end
        --move forwards
        cellGrid[currX][currY][1] = bit.bor(cellGrid[currX][currY][1], dir)
        col = cellGrid[currX][currY][2]
        if dir == 1 then
            currY = currY - 1
            cellGrid[currX][currY][1] = bit.bor(cellGrid[currX][currY][1], 4)
        elseif dir == 2 then
            currX = currX + 1
            cellGrid[currX][currY][1] = bit.bor(cellGrid[currX][currY][1], 8)
        elseif dir == 4 then
            currY = currY + 1
            cellGrid[currX][currY][1] = bit.bor(cellGrid[currX][currY][1], 1)
        else
            currX = currX - 1
            cellGrid[currX][currY][1] = bit.bor(cellGrid[currX][currY][1], 2)
        end
    end
    if ditheredImg then ditheredImg:release() end
    ditheredImg = applyDither()
end

function setWallsHilbertFollow()
    resetCellGrid()
    if ditheredImg then ditheredImg:release() end
    local col, err
    local newImgData = love.image.newImageData(imgSize, imgSize)
    local hText = require "hilbert"
    local dir = 2
    local currX, currY = 0, 127
    local char
    for i = 1, #hText do
        char = hText:sub(i, i)
        if char == "-" then
            -- turn left
            dir = dir / 2
            if dir < 1 then dir = 8 end
        elseif char == "+" then
            -- turn right
            dir = dir * 2
            if dir > 8 then dir = 1 end
        end
        --move forwards
        cellGrid[currX][currY][1] = bit.bor(cellGrid[currX][currY][1], dir)
        col = cellGrid[currX][currY][2]
        -- set cell and get error
        if col < threshold then
            err = col
            col = 0
        else
            err = col - 1
            col = 1
        end
        -- draw pixel
        newImgData:setPixel(currX, currY, col, col, col, 1)
        if dir == 1 then
            currY = currY - 1
            cellGrid[currX][currY][1] = bit.bor(cellGrid[currX][currY][1], 4)
        elseif dir == 2 then
            currX = currX + 1
            cellGrid[currX][currY][1] = bit.bor(cellGrid[currX][currY][1], 8)
        elseif dir == 4 then
            currY = currY + 1
            cellGrid[currX][currY][1] = bit.bor(cellGrid[currX][currY][1], 1)
        else
            currX = currX - 1
            cellGrid[currX][currY][1] = bit.bor(cellGrid[currX][currY][1], 2)
        end
        -- push error
        cellGrid[currX][currY][2] = cellGrid[currX][currY][2] + err
    end
    ditheredImg = love.graphics.newImage(newImgData)
end

function getNeighbours(x, y)
    local neighbours = {}
    -- up
    if y > 0 then
        if cellGrid[x][y - 1][1] == 0 then
            table.insert(neighbours, {x, y - 1, 1})
        end
    end
    -- right
    if x < imgSize - 1 then
        if cellGrid[x + 1][y][1] == 0 then
            table.insert(neighbours, {x + 1, y, 2})
        end
    end
    -- down
    if y < imgSize - 1 then
        if cellGrid[x][y + 1][1] == 0 then
            table.insert(neighbours, {x, y + 1, 4})
        end
    end
    -- left
    if x > 0 then
        if cellGrid[x - 1][y][1] == 0 then
            table.insert(neighbours, {x - 1, y, 8})
        end
    end
    return neighbours
end

function applyDither()
    local col, err, val
    local newImgData = love.image.newImageData(imgSize, imgSize)
    for x = 0, imgSize - 1 do
        for y = 0, imgSize - 1 do
            val = cellGrid[x][y][1]
            col = cellGrid[x][y][2]
            -- set cell and get error
            if col < threshold then
                err = col
                col = 0
            else
                err = col - 1
                col = 1
            end
            -- get neighbours to pass error
            -- both
            if bit.band(val, 6) == 6 then
                err = err / 2
                cellGrid[x + 1][y][2] = cellGrid[x + 1][y][2] + err
                cellGrid[x][y + 1][2] = cellGrid[x][y + 1][2] + err
            -- right
            elseif bit.band(val, 2) == 2 then
                cellGrid[x + 1][y][2] = cellGrid[x + 1][y][2] + err
            -- down
            elseif bit.band(val, 4) == 4 then
                cellGrid[x][y + 1][2] = cellGrid[x][y + 1][2] + err
            end
            -- set the image data
            newImgData:setPixel(x, y, col, col, col, 1)
        end
    end
    return love.graphics.newImage(newImgData)
end

function drawGrid()
    -- draw grid, only need to draw right and down
    local val
    love.graphics.setLineWidth(1 / scale)
    for x = 0, imgSize - 1 do
        for y = 0, imgSize - 1 do
            val = cellGrid[x][y][1]
            -- check if we are blocked right
            if bit.band(val, 2) == 0 then
                love.graphics.line(x + 1, y, x + 1, y + 1)
            end
            -- check if we are blocked down
            if bit.band(val, 4) == 0 then
                love.graphics.line(x, y + 1, x + 1, y + 1)
            end
        end
    end
end

function love.update(dt)
    timer = timer + 1
    if timer % animationDelay == 0 and animateDither then
        currentCurve()
    end
end

function love.draw()
    local start = love.timer.getTime()

    love.graphics.push()
    love.graphics.scale(scale)
    love.graphics.draw(origImg)
    if drawGridToggle == true then
        love.graphics.setColor(0.2, 0.4, 0.8)
        drawGrid()
        love.graphics.setColor(1, 1, 1)
    end
    love.graphics.draw(ditheredImg, imgSize, 0)

    love.graphics.pop()
    -- ui
    love.graphics.print([[F1 - All walls blocked
F2 - All walls open
F3 - Random walls
F4 - Recursive backtracker maze
F5 - Hilbert Curve (following curve)
F6 - Hilbert Curve (from top left)

T - Toggle grid
A - Toggle animation]], 5, 5)
end
The love file is attached which includes my test image and the export from a L-System program I was using to quickly generate the Hilbert curve (probably not the best way to do it but it was fast to implement with what I already had)
Attachments
CurveDither.love
(29.28 KiB) Downloaded 230 times
User avatar
darkfrei
Party member
Posts: 1204
Joined: Sat Feb 08, 2020 11:09 pm

Re: Dithering with matrix

Post by darkfrei »

Dithering with pseudo-hexagonal matrix pattern:

Code: Select all

function dithering(data)
	local width, height = data:getDimensions()
	local canvasImageData = love.image.newImageData(width, height)

	local matrix = {
-- 9 colors
		{ 28,   85,  28,  85 },
		{ 142, 198, 142, 198 },
		{ 113,  57, 113,  57 },
		{ 227, 170, 227, 170 },
	}

	local mh = #matrix
	local mw = #matrix[1]

	for y = 0, height - 1 do
		for x = 0, width - 1 do
			local r, g, b, a = love.math.colorToBytes(data:getPixel(x, y))
			local i, j = x%mw+1, y%mh+1
			local matrixValue = matrix[j][i]
			r = r >= matrixValue and 1 or 0
			g = g >= matrixValue and 1 or 0
			b = b >= matrixValue and 1 or 0
			canvasImageData:setPixel(x, y, r, g, b, a)
		end
	end
	local canvas = love.graphics.newImage(canvasImageData)
	return canvas
end
2024-04-26T17_49_37-Untitled.png
2024-04-26T17_49_37-Untitled.png (113.05 KiB) Viewed 17321 times

Update: it must be like:
Travis Strikes Again: No More Heroes
Image
Attachments
dithering-matrix-02.love
(143.77 KiB) Downloaded 235 times
:awesome: in Lua we Löve
:awesome: Platformer Guide
:awesome: freebies
Post Reply

Who is online

Users browsing this forum: Ahrefs [Bot] and 6 guests