Add highly experimental texture generation

This commit is contained in:
Lars Mueller 2023-06-10 14:56:07 +02:00
parent 91a4ee521f
commit 15ad69b0fe
5 changed files with 581 additions and 4 deletions

@ -26,7 +26,8 @@ for _, file in pairs{
"base64", "base64",
"persistence", "persistence",
"debug", "debug",
"web" "web",
"tex"
} do } do
modules[file] = file modules[file] = file
end end

@ -9,7 +9,8 @@ local texmod, metatable = component"dsl"
local methods = metatable.__index local methods = metatable.__index
methods.write = component"write" methods.write = component"write"
texmod.read = component("read", texmod) texmod.read = component("read", texmod)
methods.calc_dims = component("calc_dims", texmod) methods.calc_dims = component"calc_dims"
methods.gen_tex = component"gen_tex"
function metatable:__tostring() function metatable:__tostring()
local rope = {} local rope = {}

@ -46,7 +46,6 @@ do
return over_w, over_h return over_w, over_h
end end
cd.blit = upscale_to_higher_res cd.blit = upscale_to_higher_res
cd.overlay = upscale_to_higher_res
cd.hardlight = upscale_to_higher_res cd.hardlight = upscale_to_higher_res
end end

190
minetest/texmod/gen_tex.lua Normal file

@ -0,0 +1,190 @@
local tex = modlib.tex
local paths = modlib.minetest.media.paths
local function read_png(fname)
if fname == "blank.png" then return tex.new{w=1,h=1,0} end
return tex.read_png(assert(paths[fname]))
end
local gt = {}
-- TODO colorizehsl, hsl, contrast
-- TODO (...) inventorycube; this is nontrivial.
function gt:file()
return read_png(self.filename)
end
function gt:opacity()
local t = self.base:gen_tex()
t:opacity(self.ratio / 255)
return t
end
function gt:invert()
local t = self.base:gen_tex()
t:invert(self.r, self.g, self.b, self.a)
return t
end
function gt:brighten()
local t = self.base:gen_tex()
t:brighten()
return t
end
function gt:noalpha()
local t = self.base:gen_tex()
t:noalpha()
return t
end
function gt:makealpha()
local t = self.base:gen_tex()
t:makealpha(self.r, self.g, self.b)
return t
end
function gt:multiply()
local c = self.color
local t = self.base:gen_tex()
t:multiply_rgb(c.r, c.g, c.b)
return t
end
function gt:screen()
local c = self.color
local t = self.base:gen_tex()
t:screen_blend_rgb(c.r, c.g, c.b)
return t
end
function gt:colorize()
local c = self.color
local t = self.base:gen_tex()
t:colorize(c.r, c.g, c.b, self.ratio)
return t
end
local function resized_to_larger(a, b)
if a.w * a.h > b.w * b.h then
b = b:resized(a.w, a.h)
else
a = a:resized(b.w, b.h)
end
return a, b
end
function gt:mask()
local a, b = resized_to_larger(self.base:gen_tex(), self._mask:gen_tex())
a:band(b)
return a
end
function gt:lowpart()
local t = self.base:gen_tex()
local over = self.over:gen_tex()
local lowpart_h = math.ceil(self.percent/100 * over.h) -- TODO (?) ceil or floor
if lowpart_h > 0 then
t, over = resized_to_larger(t, over)
local y = over.h - lowpart_h + 1
over:crop(1, y, over.w, over.h)
t:blit(1, y, over)
end
return t
end
function gt:resize()
return self.base:gen_tex():resized(self.w, self.h)
end
function gt:combine()
local t = tex.filled(self.w, self.h, 0)
for _, blt in ipairs(self.blits) do
t:blit(blt.x + 1, blt.y + 1, blt.texture:gen_tex())
end
return t
end
function gt:fill()
if self.base then
return self.base:gen_tex():fill(self.w, self.h, self.x, self.y, self.color:to_number())
end
return tex.filled(self.w, self.h, self.color:to_number())
end
function gt:blit()
local t, o = resized_to_larger(self.base:gen_tex(), self.over:gen_tex())
t:blit(1, 1, o)
return t
end
function gt:hardlight()
local t, o = resized_to_larger(self.base:gen_tex(), self.over:gen_tex())
t:hardlight_blend(o)
return t
end
-- TODO (...?) optimize this
function gt:transform()
local t = self.base:gen_tex()
if self.flip_axis == "x" then
t:flip_x()
elseif self.flip_axis == "y" then
t:flip_y()
end
-- TODO implement counterclockwise rotations to get rid of this hack
for _ = 1, 360 - self.rotation_deg / 90 do
t = t:rotated_90()
end
return t
end
local frame = function(t, frame, framecount)
local fh = math.floor(t.h / framecount)
t:crop(1, frame * fh + 1, t.w, (frame + 1) * fh)
end
local crack = function(self, o)
local crack = read_png"crack_anylength.png"
frame(crack, self.frame, math.floor(crack.h / crack.w))
local t = self.base:gen_tex()
local tile_w, tile_h = math.floor(t.w / self.tilecount), math.floor(t.h / self.framecount)
crack = crack:resized(tile_w, tile_h)
for ty = 1, t.h, tile_h do
for tx = 1, t.w, tile_w do
t[o and "blito" or "blit"](t, tx, ty, crack)
end
end
return t
end
function gt:crack()
return crack(self, false)
end
function gt:cracko()
return crack(self, true)
end
function gt:verticalframe()
local t = self.base:gen_tex()
frame(t, self.frame, self.framecount)
return t
end
function gt:sheet()
local t = self.base:gen_tex()
local tw, th = math.floor(t.w / self.w), math.floor(t.h / self.h)
local x, y = self.x, self.y
t:crop(x * tw + 1, y * th + 1, (x + 1) * tw, (y + 1) * th)
return t
end
function gt:png()
return tex.read_png_string(self.data)
end
return function(self)
return assert(gt[self.type])(self)
end

386
tex.lua Normal file

@ -0,0 +1,386 @@
--[[
This file does not follow the usual conventions;
it duplicates some code for performance reasons.
In particular, use of `modlib.minetest.colorspec` is avoided.
Most methods operate *in-place* (imperative method names)
rather than returning a modified copy (past participle method names).
Outside-facing methods consistently use 1-based indexing; indices are inclusive.
]]
local min, max, floor, ceil = math.min, math.max, math.floor, math.ceil
local function round(x) return floor(x + 0.5) end
local function clamp(x, mn, mx) return max(min(x, mx), mn) end
-- ARGB handling utilities
local function unpack_argb(argb)
return floor(argb / 0x1000000),
floor(argb / 0x10000) % 0x100,
floor(argb / 0x100) % 0x100,
argb % 0x100
end
local function pack_argb(a, r, g, b)
local argb = (((a * 0x100 + r) * 0x100) + g) * 0x100 + b
return argb
end
local function round_argb(a, r, g, b)
return round(a), round(r), round(g), round(b)
end
local function scale_0_1_argb(a, r, g, b)
return a / 255, r / 255, g / 255, b / 255
end
local function scale_0_255_argb(a, r, g, b)
return a * 255, r * 255, g * 255, b * 255
end
local tex = {}
local metatable = {__index = tex}
function metatable:__eq(other)
if self.w ~= other.w or self.h ~= other.h then return false end
for i = 1, #self do if self[i] ~= other[i] then return false end end
return true
end
function tex:new()
return setmetatable(self, metatable)
end
function tex.filled(w, h, argb)
local self = {w = w, h = h}
for i = 1, w*h do
self[i] = argb
end
return tex.new(self)
end
function tex:copy()
local copy = {w = self.w, h = self.h}
for i = 1, #self do
copy[i] = self[i]
end
return tex.new(copy)
end
-- Reading & writing
function tex.read_png_string(str)
local stream = modlib.text.inputstream(str)
local png = modlib.minetest.decode_png(stream)
assert(stream:read(1) == nil, "eof expected")
modlib.minetest.convert_png_to_argb8(png)
png.data.w, png.data.h = png.width, png.height
return tex.new(png.data)
end
function tex.read_png(path)
local png
modlib.file.with_open(path, "rb", function(f)
png = modlib.minetest.decode_png(f)
assert(f:read(1) == nil, "eof expected")
end)
modlib.minetest.convert_png_to_argb8(png)
png.data.w, png.data.h = png.width, png.height
return tex.new(png.data)
end
function tex:write_png_string()
return modlib.minetest.encode_png(self.w, self.h, self)
end
function tex:write_png(path)
modlib.file.write_binary(path, self:write_png_string())
end
function tex:fill(sx, sy, argb)
local w, h = self.w, self.h
for y = sy, h do
local i = (y - 1) * w + sx
for _ = sx, w do
self[i] = argb
i = i + 1
end
end
end
function tex:in_bounds(x, y)
return x >= 1 and y >= 1 and x <= self.w and y <= self.h
end
function tex:get_argb_packed(x, y)
return self[(y - 1) * self.w + x]
end
function tex:get_argb(x, y)
return unpack_argb(self[(y - 1) * self.w + x])
end
function tex:set_argb_packed(x, y, argb)
self[(y - 1) * self.w + x] = argb
end
function tex:set_argb(x, y, a, r, g, b)
self[(y - 1) * self.w + x] = pack_argb(a, r, g, b)
end
function tex:map_argb(func)
for i = 1, #self do
self[i] = pack_argb(func(unpack_argb(self[i])))
end
end
local function blit(s, x, y, t, o)
local sw, sh = s.w, s.h
local tw, th = t.w, t.h
-- Restrict to overlapping region
x, y = clamp(x, 1, sw), clamp(y, 1, sh)
local min_tx, min_ty = max(1, 2 - x), max(1, 2 - y)
local max_tx, max_ty = min(tw, sw - x + 1), min(th, sh - y + 1)
for ty = min_ty, max_ty do
local ti, si = (ty - 1) * tw, (y + ty - 2) * sw + x - 1
for _ = min_tx, max_tx do
ti, si = ti + 1, si + 1
local sa, sr, sg, sb = scale_0_1_argb(unpack_argb(s[si]))
if sa == 1 or not o then -- HACK because of dirty `[cracko`
local ta, tr, tg, tb = scale_0_1_argb(unpack_argb(t[ti]))
-- "`t` over `s`" (Porter-Duff-Algorithm)
local sata = sa * (1 - ta)
local ra = ta + sata
assert(ra > 0 or (sa == 0 and ta == 0))
if ra > 0 then
s[si] = pack_argb(round_argb(scale_0_255_argb(
ra,
(ta * tr + sata * sr) / ra,
(ta * tg + sata * sg) / ra,
(ta * tb + sata * sb) / ra
)))
end
end
end
end
end
-- Blitting with proper alpha blending.
function tex.blit(s, x, y, t)
return blit(s, x, y, t, false)
end
-- Blit, but only on fully opaque base pixels. Only `[cracko` uses this.
function tex.blito(s, x, y, t)
return blit(s, x, y, t, true)
end
function tex.combine_argb(s, t, cf)
assert(#s == #t)
for i = 1, #s do
s[i] = cf(s[i], t[i])
end
end
-- See https://github.com/TheAlgorithms/Lua/blob/162c4c59f5514c6115e0add8a2b4d56afd6d3204/src/bit/uint53/and.lua
-- TODO (?) optimize fallback band using caching, move somewhere else
local band = bit and bit.band or function(n, m)
local res = 0
local bit = 1
while n * m ~= 0 do -- while both are nonzero
local n_bit, m_bit = n % 2, m % 2 -- extract LSB
res = res + (n_bit * m_bit) * bit -- add AND of LSBs
n, m = (n - n_bit) / 2, (m - m_bit) / 2 -- remove LSB from n & m
bit = bit * 2 -- next bit
end
return res
end
function tex.band(s, t)
return s:combine_argb(t, band)
end
function tex.hardlight_blend(s, t)
return s:combine_argb(t, function(sargb, targb)
local sa, sr, sg, sb = scale_0_1_argb(unpack_argb(sargb))
local _, tr, tg, tb = scale_0_1_argb(unpack_argb(targb))
return pack_argb(round_argb(scale_0_255_argb(
sa,
sr < 0.5 and 2*sr*tr or 1 - 2*(1-sr)*(1-tr),
sr < 0.5 and 2*sg*tg or 1 - 2*(1-sg)*(1-tg),
sr < 0.5 and 2*sb*tb or 1 - 2*(1-sb)*(1-tb)
)))
end)
end
function tex:brighten()
return self:map_argb(function(a, r, g, b)
return round_argb((255 + a) / 2, (255 + r) / 2, (255 + g) / 2, (255 + b) / 2)
end)
end
function tex:noalpha()
for i = 1, #self do
self[i] = 0xFF000000 + self[i] % 0x1000000
end
end
function tex:makealpha(r, g, b)
local mrgb = r * 0x10000 + g * 0x100 + b
for i = 1, #self do
local rgb = self[i] % 0x1000000
if rgb == mrgb then
self[i] = rgb
end
end
end
function tex:opacity(factor)
for i = 1, #self do
self[i] = round(floor(self[i] / 0x1000000) * factor) * 0x1000000 + self[i] % 0x1000000
end
end
function tex:invert(ir, ig, ib, ia)
return self:map_argb(function(a, r, g, b)
if ia then a = 255 - a end
if ir then r = 255 - r end
if ig then g = 255 - g end
if ib then b = 255 - b end
return a, r, g, b
end)
end
function tex:multiply_rgb(r, g, b)
return self:map_argb(function(sa, sr, sg, sb)
return round_argb(sa, r * sr, g * sg, b * sb)
end)
end
function tex:screen_blend_rgb(r, g, b)
return self:map_argb(function(sa, sr, sg, sb)
return round_argb(sa,
255 - ((255 - sr) * (255 - r)) / 255,
255 - ((255 - sg) * (255 - g)) / 255,
255 - ((255 - sb) * (255 - b)) / 255)
end)
end
function tex:colorize(cr, cg, cb, ratio)
return self:map_argb(function(a, r, g, b)
local rat = ratio == "alpha" and a or ratio
return round_argb(
a,
rat * r + (1 - rat) * cr,
rat * g + (1 - rat) * cg,
rat * b + (1 - rat) * cb
)
end)
end
function tex:crop(from_x, from_y, to_x, to_y)
local w = self.w
local i = 1
for y = from_y, to_y do
local j = (y - 1) * w + from_x
for _ = from_x, to_x do
self[i] = self[j]
i, j = i + 1, j + 1
end
end
-- Remove remaining pixels
for j = i, #self do self[j] = nil end
self.w, self.h = to_x - from_x + 1, to_y - from_y + 1
end
function tex:flip_x()
for y = 1, self.h do
local i = (y - 1) * self.w
local j = i + self.w + 1
while i < j do
i, j = i + 1, j - 1
self[i], self[j] = self[j], self[i]
end
end
end
function tex:flip_y()
for x = 1, self.w do
local i, j = x, (self.h - 1) * self.w + x
while i < j do
i, j = i + self.w, j - self.w
self[i], self[j] = self[j], self[i]
end
end
end
--> copy of the texture, rotated 90 degrees clockwise
function tex:rotated_90()
local w, h = self.w, self.h
local t = {w = h, h = w}
local i = 0
for y = 1, w do
for x = 1, h do
i = i + 1
t[i] = self[(h-x)*w + y]
end
end
t = tex.new(t)
return t
end
-- Uses box sampling. Hard to optimize.
-- TODO (...) interpolate box samples; match what Minetest does
--> copy of `self` resized to `w` x `h`
function tex:resized(w, h)
--! This function works with 0-based indices.
local sw, sh = self.w, self.h
local fx, fy = sw / w, sh / h
local t = {w = w, h = h}
local i = 0
for y = 0, h - 1 do
for x = 0, w - 1 do
-- Sample the area
local vy_from = y * fy
local vy_to = vy_from + fy
local vx_from = x * fx
local vx_to = vx_from + fx
local a, r, g, b = 0, 0, 0, 0
local pf_sum = 0
local function blend(sx, sy, pf)
if pf <= 0 then return end
local sa, sr, sg, sb = unpack_argb(self[sy * sw + sx + 1])
pf_sum = pf_sum + pf -- TODO (?) eliminate `pf_sum`
sa = sa * pf
a = a + sa
r, g, b = r + sa * sr, g + sa * sg, b + sa * sb
end
local function srow(sy, pf)
if pf <= 0 then return end
local sx_from, sx_to = ceil(vx_from), floor(vx_to)
for sx = sx_from, sx_to - 1 do blend(sx, sy, pf) end -- whole pixels
-- Pixels at edges
blend(floor(vx_from), sy, pf * (sx_from - vx_from))
blend(floor(vx_to), sy, pf * (vx_to - sx_to))
end
local sy_from, sy_to = ceil(vy_from), floor(vy_to)
for sy = sy_from, sy_to - 1 do srow(sy, 1) end -- whole pixels
-- Pixels at edges
srow(floor(vy_from), sy_from - vy_from)
srow(floor(vy_to), vy_to - sy_to)
if a > 0 then r, g, b = r / a, g / a, b / a end
assert(pf_sum > 0)
i = i + 1
t[i] = pack_argb(round_argb(a / pf_sum, r, g, b))
end
end
return tex.new(t)
end
return tex