mirror of
https://git.minetest.land/VoxeLibre/VoxeLibre.git
synced 2024-12-23 00:19:32 +01:00
Better map colors (+ tools) (#4697)
Reviewed-on: https://git.minetest.land/VoxeLibre/VoxeLibre/pulls/4697 Reviewed-by: the-real-herowl <the-real-herowl@noreply.git.minetest.land> Co-authored-by: kno10 <erich.schubert@gmail.com> Co-committed-by: kno10 <erich.schubert@gmail.com>
This commit is contained in:
parent
dec332c822
commit
972185907f
12 changed files with 5306 additions and 12126 deletions
File diff suppressed because it is too large
Load diff
|
@ -30,11 +30,6 @@ local function load_json_file(name)
|
||||||
end
|
end
|
||||||
|
|
||||||
local texture_colors = load_json_file("colors")
|
local texture_colors = load_json_file("colors")
|
||||||
local palettes_grass = load_json_file("palettes_grass")
|
|
||||||
local palettes_foliage = load_json_file("palettes_foliage")
|
|
||||||
local palettes_water = load_json_file("palettes_water")
|
|
||||||
|
|
||||||
local color_cache = {}
|
|
||||||
|
|
||||||
local creating_maps = {}
|
local creating_maps = {}
|
||||||
local loaded_maps = {}
|
local loaded_maps = {}
|
||||||
|
@ -66,80 +61,48 @@ function mcl_maps.create_map(pos)
|
||||||
local param2data = vm:get_param2_data()
|
local param2data = vm:get_param2_data()
|
||||||
local area = VoxelArea:new({ MinEdge = emin, MaxEdge = emax })
|
local area = VoxelArea:new({ MinEdge = emin, MaxEdge = emax })
|
||||||
local pixels = {}
|
local pixels = {}
|
||||||
local last_heightmap
|
for z = 1, 128 do
|
||||||
for x = 1, 128 do
|
local map_z = minp.z - 1 + z
|
||||||
local map_x = minp.x - 1 + x
|
local last_height
|
||||||
local heightmap = {}
|
for x = 1, 128 do
|
||||||
for z = 1, 128 do
|
local map_x = minp.x - 1 + x
|
||||||
local map_z = minp.z - 1 + z
|
local cagg, alpha, height = { 0, 0, 0 }, 0
|
||||||
local color, height
|
|
||||||
for map_y = maxp.y, minp.y, -1 do
|
for map_y = maxp.y, minp.y, -1 do
|
||||||
local index = area:index(map_x, map_y, map_z)
|
local index = area:index(map_x, map_y, map_z)
|
||||||
local c_id = data[index]
|
local c_id = data[index]
|
||||||
if c_id ~= c_air then
|
if c_id ~= c_air then
|
||||||
color = color_cache[c_id]
|
local color = texture_colors[minetest.get_name_from_content_id(c_id)]
|
||||||
if color == nil then
|
-- use param2 if available:
|
||||||
local nodename = minetest.get_name_from_content_id(c_id)
|
if color and type(color[1]) == "table" then
|
||||||
local def = minetest.registered_nodes[nodename]
|
color = color[param2data[index] + 1] or color[1]
|
||||||
if def then
|
|
||||||
local texture
|
|
||||||
if def.palette then
|
|
||||||
texture = def.palette
|
|
||||||
elseif def.tiles then
|
|
||||||
texture = def.tiles[1]
|
|
||||||
if type(texture) == "table" then
|
|
||||||
texture = texture.name
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if texture then
|
|
||||||
texture = texture:match("([^=^%^]-([^.]+))$"):split("^")[1]
|
|
||||||
end
|
|
||||||
if def.palette == "mcl_core_palette_grass.png" then
|
|
||||||
local palette = palettes_grass[texture]
|
|
||||||
color = palette and { palette = palette }
|
|
||||||
elseif def.palette == "mcl_core_palette_foliage.png" then
|
|
||||||
local palette = palettes_foliage[texture]
|
|
||||||
color = palette and { palette = palette }
|
|
||||||
elseif def.palette == "mcl_core_palette_water.png" then
|
|
||||||
local palette = palettes_water[texture]
|
|
||||||
color = palette and { palette = palette }
|
|
||||||
else
|
|
||||||
color = texture_colors[texture]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
if color then
|
||||||
|
local a = (color[4] or 255) / 255
|
||||||
|
local f = a * (1 - alpha)
|
||||||
|
cagg[1] = cagg[1] + f * color[1]
|
||||||
|
cagg[2] = cagg[2] + f * color[2]
|
||||||
|
cagg[3] = cagg[3] + f * color[3]
|
||||||
|
alpha = alpha + f
|
||||||
|
|
||||||
if color and color.palette then
|
-- ground estimate with transparent blocks
|
||||||
color = color.palette[param2data[index] + 1]
|
if alpha > 0.70 and not height then height = map_y end
|
||||||
else
|
-- adjust color to give a 3d effect
|
||||||
color_cache[c_id] = color or false
|
if alpha >= 0.99 and last_height and height then
|
||||||
end
|
local dheight = math.min(math.max((height - last_height) * 8, -32), 32)
|
||||||
|
cagg = {
|
||||||
if color and last_heightmap then
|
math.max(0, math.min(255, cagg[1] + dheight)),
|
||||||
local last_height = last_heightmap[z]
|
math.max(0, math.min(255, cagg[2] + dheight)),
|
||||||
if last_height < map_y then
|
math.max(0, math.min(255, cagg[3] + dheight)),
|
||||||
color = {
|
|
||||||
math.min(255, color[1] + 16),
|
|
||||||
math.min(255, color[2] + 16),
|
|
||||||
math.min(255, color[3] + 16),
|
|
||||||
}
|
|
||||||
elseif last_height > map_y then
|
|
||||||
color = {
|
|
||||||
math.max(0, color[1] - 16),
|
|
||||||
math.max(0, color[2] - 16),
|
|
||||||
math.max(0, color[3] - 16),
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
if alpha >= 0.99 then break end
|
||||||
end
|
end
|
||||||
height = map_y
|
|
||||||
break
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
heightmap[z] = height or minp.y
|
last_height = height
|
||||||
pixels[z] = pixels[z] or {}
|
pixels[z] = pixels[z] or {}
|
||||||
pixels[z][x] = color or { 0, 0, 0 }
|
pixels[z][x] = cagg or { 0, 0, 0 }
|
||||||
end
|
end
|
||||||
last_heightmap = heightmap
|
|
||||||
end
|
end
|
||||||
tga_encoder.image(pixels):save(map_textures_path .. "mcl_maps_map_texture_" .. id .. ".tga")
|
tga_encoder.image(pixels):save(map_textures_path .. "mcl_maps_map_texture_" .. id .. ".tga")
|
||||||
creating_maps[id] = nil
|
creating_maps[id] = nil
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
{"mcl_core_palette_foliage.png": [[86, 164, 117], [109, 196, 117], [118, 177, 120], [159, 193, 114], [159, 193, 114], [74, 107, 58], [94, 190, 107], [94, 190, 107], [222, 188, 101], [90, 197, 87], [35, 175, 105], [92, 182, 119], [93, 181, 76], [93, 181, 76], [82, 153, 81], [91, 177, 85], [86, 164, 117], [94, 190, 107]]}
|
|
|
@ -1 +0,0 @@
|
||||||
{"mcl_core_palette_grass.png": [[109, 196, 117], [159, 193, 114], [118, 177, 120], [118, 177, 120], [107, 186, 107], [118, 177, 120], [92, 182, 119], [92, 182, 119], [92, 182, 119], [92, 182, 119], [118, 177, 120], [109, 196, 117], [35, 175, 105], [94, 190, 107], [94, 190, 107], [94, 190, 107], [94, 190, 107], [159, 193, 114], [76, 176, 84], [164, 150, 110], [164, 150, 110], [164, 150, 110], [164, 150, 110], [159, 193, 114], [93, 181, 76], [93, 181, 76], [93, 181, 76], [93, 181, 76], [76, 118, 60], [94, 190, 107]]}
|
|
|
@ -1 +0,0 @@
|
||||||
{"mcl_core_palette_water.png": [[63, 118, 228], [82, 121, 179], [66, 149, 235], [65, 174, 233], [62, 104, 221], [60, 93, 215], [46, 100, 218], [61, 120, 181]]}
|
|
4167
tools/colors.txt
4167
tools/colors.txt
File diff suppressed because it is too large
Load diff
4
tools/minetestmapper/AUTHORS
Normal file
4
tools/minetestmapper/AUTHORS
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
Miroslav Bendík <miroslav.bendik@gmail.com>
|
||||||
|
ShadowNinja <shadowninja@minetest.net>
|
||||||
|
sfan5 <sfan5@live.de>
|
||||||
|
kno10 <erich.schubert@gmail.com>
|
22
tools/minetestmapper/COPYING
Normal file
22
tools/minetestmapper/COPYING
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
Copyright (c) 2013-2014, Miroslav Bendík and various contributors (see AUTHORS)
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
13
tools/minetestmapper/README.md
Normal file
13
tools/minetestmapper/README.md
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
Customized version of the minetestmapper utilities for voxelibre.
|
||||||
|
|
||||||
|
This is *not* the full minetestmapper, just the mod + script to generate colors.txt
|
||||||
|
|
||||||
|
Original version:
|
||||||
|
https://github.com/minetest/minetestmapper/tree/master/util/
|
||||||
|
|
||||||
|
To use minetestmapper, first install minetestmapper (e.g., "apt install minetestmapper").
|
||||||
|
|
||||||
|
Example call:
|
||||||
|
```
|
||||||
|
minetestmapper -i worlds/myworld --colors games/voxelibre/tools/colors.txt --drawalpha -o /tmp/map.png
|
||||||
|
```
|
28
tools/minetestmapper/dumpnodes/init.lua
Normal file
28
tools/minetestmapper/dumpnodes/init.lua
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
minetest.register_chatcommand("dumpnodes", {
|
||||||
|
description = "Dump node and texture list for use with minetestmapper",
|
||||||
|
func = function()
|
||||||
|
local out, err = io.open(minetest.get_worldpath() .. "/nodes.txt", 'wb')
|
||||||
|
if not out then return true, err end
|
||||||
|
for name, def in pairs(minetest.registered_nodes) do
|
||||||
|
local tiles = def.tiles or def.tile_images
|
||||||
|
if tiles and def.drawtype ~= 'airlike' then
|
||||||
|
local tex = nil
|
||||||
|
for _, tile in pairs(tiles) do
|
||||||
|
tex = type(tile) == 'table' and (tile.name or tile.image) or tile
|
||||||
|
if tex ~= "blank.png" then break end
|
||||||
|
end
|
||||||
|
if tex then
|
||||||
|
out:write(name .. " " .. tex)
|
||||||
|
if def.paramtype2 and def.paramtype2:sub(1,5) == "color" and def.palette ~= "" then
|
||||||
|
out:write(" " .. def.paramtype2 .. " " .. def.palette)
|
||||||
|
elseif def.color and def.color ~= "" then
|
||||||
|
out:write(" " .. def.color)
|
||||||
|
end
|
||||||
|
out:write('\n')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
out:close()
|
||||||
|
return true, "Finished node dump for minetestmapper."
|
||||||
|
end,
|
||||||
|
})
|
2
tools/minetestmapper/dumpnodes/mod.conf
Normal file
2
tools/minetestmapper/dumpnodes/mod.conf
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
name = dumpnodes
|
||||||
|
description = minetestmapper development mod (node dumper)
|
407
tools/minetestmapper/generate_colorstxt.py
Executable file
407
tools/minetestmapper/generate_colorstxt.py
Executable file
|
@ -0,0 +1,407 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import sys, os.path, getopt, re, json
|
||||||
|
from pyparsing import CharsNotIn, Suppress, infix_notation, opAssoc, ZeroOrMore
|
||||||
|
from PIL import Image, ImageColor, ImageChops, ImageEnhance, ImageMath # aka "pillow"
|
||||||
|
|
||||||
|
############
|
||||||
|
############
|
||||||
|
# Instructions for generating a colors.txt file for custom games and/or mods:
|
||||||
|
# 1) Add the dumpnodes mod to a Minetest world with the chosen game and mods enabled.
|
||||||
|
# 2) Join ingame and run the /dumpnodes chat command.
|
||||||
|
# 3) Run this script and poin it to the installation path of the game using -g,
|
||||||
|
# the path(s) where mods are stored using -m and the nodes.txt in your world folder.
|
||||||
|
# Example command line:
|
||||||
|
# python3 generate_colorstxt.py \
|
||||||
|
# --game /usr/share/minetest/games/minetest_game \
|
||||||
|
# --mods ~/.minetest/mods \
|
||||||
|
# --mods /usr/share/minetest/textures \
|
||||||
|
# ~/.minetest/worlds/my_world/nodes.txt
|
||||||
|
# 4) Copy the resulting colors.txt file to your world folder or to any other places
|
||||||
|
# and use it with minetestmapper's --colors option.
|
||||||
|
# 5) Copy the resulting colors.json file to the mcl_maps mod
|
||||||
|
###########
|
||||||
|
###########
|
||||||
|
|
||||||
|
# adjust water transparency, primarily for minetestmapper
|
||||||
|
def adjust_water_transparency(cs):
|
||||||
|
if isinstance(cs[0], int):
|
||||||
|
return (cs[0],cs[1],cs[2], 224, 128)
|
||||||
|
return [(x[0],x[1],x[2], 224, 128) for x in cs]
|
||||||
|
def strip_alpha(cs):
|
||||||
|
if isinstance(cs[0], int):
|
||||||
|
return (cs[0],cs[1],cs[2])
|
||||||
|
return [(x[0],x[1],x[2]) for x in cs]
|
||||||
|
# called with nodename, colors
|
||||||
|
REPLACEMENTS = [(re.compile(pat),rule) for pat,rule in [
|
||||||
|
(r'^fireflies:firefly$',None),
|
||||||
|
(r'^butterflies:butterfly_',None),
|
||||||
|
# Nicer colors for water and lava
|
||||||
|
(r'^(default|mclx?_core):river_water_(flowing|source)$', (36, 67, 130, 224, 128)),
|
||||||
|
(r'^(default|mclx?_core):water_(flowing|source)$', adjust_water_transparency), # was (36, 67, 130, 224, 128)),
|
||||||
|
(r'^(default|mcl_core):lava_(flowing|source)$', (230, 90, 0)),
|
||||||
|
# Transparency for glass nodes and panes
|
||||||
|
(r'^default:.*glass$', lambda c: (c[0], c[1], c[2], 64, 16)),
|
||||||
|
(r'^doors:.*glass[^ ]*$', lambda c: (c[0], c[1], c[2], 64, 16)),
|
||||||
|
(r'^mcl_core:.*glass[^ ]*$', lambda c: (c[0], c[1], c[2], 64, 16)),
|
||||||
|
(r'^xpanes:.*(pane|bar)', lambda c: (c[0], c[1], c[2], 64, 16)),
|
||||||
|
(r'^mcl_core:.*leaves(_orphan)?$', strip_alpha), # no alpha
|
||||||
|
(r'^mcl_core:.*ice(?:_\d+)?$', lambda c: (c[0], c[1], c[2])), # no alpha
|
||||||
|
(r'^mcl_core:snow$', lambda c: (c[0], c[1], c[2], 223, 31)), # almost no alpha
|
||||||
|
(r'^mcl_bamboo:bamboo(_endcap)?$', lambda c: (c[0], c[1], c[2], 32)), # much more alpha
|
||||||
|
]]
|
||||||
|
|
||||||
|
def usage():
|
||||||
|
print("Usage: generate_colorstxt.py [options] [input file]")
|
||||||
|
print("If not specified the input file defaults to ./nodes.txt")
|
||||||
|
print("The output will be written as ./colors.txt for minetestmapper")
|
||||||
|
print("and as ./colors.json for the mcl_maps module")
|
||||||
|
print(" -g / --game <folder>\t\tSet path to the game (for textures), required")
|
||||||
|
print(" -m / --mods <folder>\t\tAdd search path for mod textures")
|
||||||
|
|
||||||
|
############ Reduce an input texture to an average color.
|
||||||
|
def average_color(name, inp, color2):
|
||||||
|
data = inp.load()
|
||||||
|
|
||||||
|
c, w = [0, 0, 0], 0
|
||||||
|
for y in range(inp.size[0]):
|
||||||
|
for x in range(inp.size[1]):
|
||||||
|
px = data[y, x]
|
||||||
|
a = px[3] / 255
|
||||||
|
if a == 0: continue
|
||||||
|
c[0] = c[0] + px[0] * a
|
||||||
|
c[1] = c[1] + px[1] * a
|
||||||
|
c[2] = c[2] + px[2] * a
|
||||||
|
w = w + a
|
||||||
|
|
||||||
|
if w == 0:
|
||||||
|
print(f"Texture all transparent: {name}", file=sys.stderr)
|
||||||
|
return None
|
||||||
|
c0, c1, c2 = c[0] / w, c[1] / w, c[2] / w
|
||||||
|
if color2: # param2 blending
|
||||||
|
c0, c1, c2 = c0 * color2[0] / 255., c1 * color2[1] / 255., c2 * color2[2] / 255.
|
||||||
|
# for alpha, find maximum alpha in chunks to account for complex textures
|
||||||
|
a = 0
|
||||||
|
for y2 in range(0,inp.size[0]-15,8):
|
||||||
|
for x2 in range(0,inp.size[1]-15,8):
|
||||||
|
a2 = 0
|
||||||
|
for y in range(y2, min(y2+16,inp.size[0])):
|
||||||
|
for x in range(x2, min(x2+16,inp.size[1])):
|
||||||
|
a2 = a2 + data[y,x][3]
|
||||||
|
a2 = a2 / 256
|
||||||
|
a = max(a, a2)
|
||||||
|
|
||||||
|
if a > 0 and a < 190:
|
||||||
|
return tuple(int(round(x)) for x in (c0, c1, c2, a))
|
||||||
|
return tuple(int(round(x)) for x in (c0, c1, c2))
|
||||||
|
|
||||||
|
_param2_cache = dict()
|
||||||
|
def get_param2_colors(filename):
|
||||||
|
if not filename: return None
|
||||||
|
cols = _param2_cache.get(filename)
|
||||||
|
if not cols and filename in textures:
|
||||||
|
inp = Image.open(textures[filename]).convert('RGBA')
|
||||||
|
data = inp.load()
|
||||||
|
cols = []
|
||||||
|
for y in range(inp.size[1]):
|
||||||
|
for x in range(inp.size[0]):
|
||||||
|
col = data[x, y]
|
||||||
|
if col[3] == 0: break
|
||||||
|
assert len(cols) == x + y * inp.size[0]
|
||||||
|
cols.append((col[0], col[1], col[2])) # copy
|
||||||
|
_param2_cache[filename] = cols
|
||||||
|
return cols
|
||||||
|
|
||||||
|
def get_param2_color(filename, param2):
|
||||||
|
if not filename: return None
|
||||||
|
cols = get_param2_colors(filename)
|
||||||
|
return cols[param2] if cols else None
|
||||||
|
|
||||||
|
def apply_sed(line, exprs):
|
||||||
|
for expr in exprs:
|
||||||
|
if expr[0] == '/':
|
||||||
|
if not expr.endswith("/d"): raise ValueError()
|
||||||
|
if re.search(expr[1:-2], line):
|
||||||
|
return ''
|
||||||
|
elif expr[0] == 's':
|
||||||
|
expr = expr.split(expr[1])
|
||||||
|
if len(expr) != 4 or expr[3] != '': raise ValueError()
|
||||||
|
line = re.sub(expr[1], expr[2], line)
|
||||||
|
else:
|
||||||
|
raise ValueError()
|
||||||
|
return line
|
||||||
|
|
||||||
|
# global texture cache
|
||||||
|
textures = {}
|
||||||
|
|
||||||
|
########################### Texture parser
|
||||||
|
# Pure image load
|
||||||
|
class Filename:
|
||||||
|
def __init__(self, tokens):
|
||||||
|
self.fn = tokens[0]
|
||||||
|
|
||||||
|
def gen(self, prev=None):
|
||||||
|
if not self.fn in textures:
|
||||||
|
raise FileNotFoundError(self.fn)
|
||||||
|
im = Image.open(textures[self.fn]).convert('RGBA')
|
||||||
|
if not prev: return im
|
||||||
|
prev.alpha_composite(im)
|
||||||
|
return prev
|
||||||
|
|
||||||
|
def pprint(self):
|
||||||
|
print("Load " + self.fn)
|
||||||
|
|
||||||
|
# Filter operations - todo: split them in the parser already?
|
||||||
|
class Filter:
|
||||||
|
_combinere = re.compile(r"(-?\d+),(-?\d+)=(.*)")
|
||||||
|
|
||||||
|
def __init__(self, tokens):
|
||||||
|
self.fname = tokens[0]
|
||||||
|
self.opts = tokens[1:]
|
||||||
|
|
||||||
|
def gen(self, prev=None):
|
||||||
|
# complex image loading filter, the most important one
|
||||||
|
if self.fname in ["combine"]:
|
||||||
|
assert prev is None
|
||||||
|
#print(self.fname, self.opts)
|
||||||
|
w, h = map(int, self.opts[0].split("x"))
|
||||||
|
im = Image.new("RGBA", (w,h), (255,255,0,0))
|
||||||
|
for blit in self.opts[1:]:
|
||||||
|
blit = Filter._combinere.match(blit).groups()
|
||||||
|
x, y, bfn = int(blit[0]), int(blit[1]), blit[2]
|
||||||
|
if not bfn in textures:
|
||||||
|
print("Skipping missing texture:", bfn, file=sys.stderr)
|
||||||
|
return im
|
||||||
|
t = Image.open(textures[bfn]).convert('RGBA')
|
||||||
|
im.alpha_composite(t, dest=(x,y))
|
||||||
|
return im
|
||||||
|
elif self.fname == "transformFX":
|
||||||
|
return prev.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
|
||||||
|
elif self.fname == "transformFY":
|
||||||
|
return prev.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
|
||||||
|
elif self.fname == "transformR90":
|
||||||
|
return prev.transpose(Image.Transpose.ROTATE_90)
|
||||||
|
elif self.fname == "transformR180":
|
||||||
|
return prev.transpose(Image.Transpose.ROTATE_180)
|
||||||
|
elif self.fname == "transformR270":
|
||||||
|
return prev.transpose(Image.Transpose.ROTATE_270)
|
||||||
|
elif self.fname == "opacity":
|
||||||
|
#print(self.fname, self.opts)
|
||||||
|
f = int(self.opts[0]) / 255.
|
||||||
|
bands = prev.split()
|
||||||
|
bands[3].point(lambda x: x * f)
|
||||||
|
return Image.merge('RGBA', bands)
|
||||||
|
elif self.fname == "noalpha":
|
||||||
|
prev.putalpha(255)
|
||||||
|
return prev
|
||||||
|
elif self.fname == "multiply":
|
||||||
|
#print(self.fname, self.opts)
|
||||||
|
col = ImageColor.getrgb(self.opts[0])
|
||||||
|
im = Image.new("RGB", prev.size, col)
|
||||||
|
bands = prev.split()
|
||||||
|
im = ImageChops.multiply(im, Image.merge('RGB', bands[:3]))
|
||||||
|
im.putalpha(bands[3])
|
||||||
|
return im
|
||||||
|
elif self.fname == "brighten":
|
||||||
|
im = Image.new("RGB", prev.size, (255,255,255))
|
||||||
|
bands = prev.split()
|
||||||
|
im = Image.blend(im, Image.merge('RGB', bands[:3]), 0.5)
|
||||||
|
im.putalpha(bands[3])
|
||||||
|
return im
|
||||||
|
elif self.fname == "hsl":
|
||||||
|
#print(self.fname, self.opts)
|
||||||
|
assert self.opts[0] == "0", "Color shifts are currently not implemented." ## TODO
|
||||||
|
assert len(self.opts) == 2, "Only saturation is currently supported." ## TODO
|
||||||
|
f = int(self.opts[1])
|
||||||
|
return ImageEnhance.Color(prev).enhance(f/100. + 1)
|
||||||
|
elif self.fname == "colorize":
|
||||||
|
# Needs testing.
|
||||||
|
# print(self.fname, self.opts, prev.size)
|
||||||
|
col = ImageColor.getrgb(self.opts[0])
|
||||||
|
if len(self.opts) == 1:
|
||||||
|
im = Image.new("RGB", prev.size, col)
|
||||||
|
mask = prev.getchannel("A")
|
||||||
|
mask.point(lambda x: 255 if x > 0 else 0)
|
||||||
|
im.putalpha(mask)
|
||||||
|
return im
|
||||||
|
elif self.opts[1] == "alpha":
|
||||||
|
im = Image.new("RGB", prev.size, col)
|
||||||
|
im.putalpha(prev.getchannel("A"))
|
||||||
|
return im
|
||||||
|
else:
|
||||||
|
f = int(self.opts[1]) / 255.
|
||||||
|
im = Image.new("RGBA", prev.size, col)
|
||||||
|
mask = prev.getchannel("A")
|
||||||
|
mask.point(lambda x: 255 if x > 0 else 0)
|
||||||
|
im = Image.blend(prev, im, f)
|
||||||
|
im.putalpha(mask)
|
||||||
|
assert im.has_transparency_data
|
||||||
|
return im
|
||||||
|
elif self.fname in ["resize"]:
|
||||||
|
#print(self.fname, self.opts)
|
||||||
|
w, h = map(int, self.opts[0].split("x"))
|
||||||
|
return prev.resize((w,h))
|
||||||
|
elif self.fname in ["mask"]:
|
||||||
|
#print(self.fname, self.opts)
|
||||||
|
# bitwise AND, very odd operation
|
||||||
|
mfn = self.opts[0]
|
||||||
|
if not mfn in textures:
|
||||||
|
print("Skipping missing texture:", mfn, file=sys.stderr)
|
||||||
|
return prev
|
||||||
|
m = Image.open(textures[mfn]).convert('RGBA')
|
||||||
|
return Image.merge('RGBA', [ImageMath.unsafe_eval("a&b", a=a, b=b).convert("L") for a,b in zip(prev.split(), m.split())])
|
||||||
|
elif self.fname in ["lowpart"]:
|
||||||
|
#print(self.fname, self.opts)
|
||||||
|
f = int(self.opts[0]) / 100
|
||||||
|
t = int(prev.size[1] * f)
|
||||||
|
return prev.crop((0, t, prev.size[0], prev.size[1]))
|
||||||
|
elif self.fname in ["verticalframe"]:
|
||||||
|
#print(self.fname, self.opts)
|
||||||
|
vdiv, idx = int(self.opts[0]), int(self.opts[1])
|
||||||
|
h = prev.size[1] // vdiv
|
||||||
|
return prev.crop((0, h * idx, prev.size[0], h * (idx + 1)))
|
||||||
|
print("Texture filter", self.fname, *self.opts, "not implemented yet.", file=sys.stderr)
|
||||||
|
|
||||||
|
def pprint(self):
|
||||||
|
print(self.fname, *self.opts)
|
||||||
|
|
||||||
|
class Overlay:
|
||||||
|
def __init__(self, tokens):
|
||||||
|
self.overlays = tokens[0]
|
||||||
|
|
||||||
|
def gen(self, prev=None):
|
||||||
|
cur = prev
|
||||||
|
for o in self.overlays:
|
||||||
|
cur = o.gen(cur)
|
||||||
|
return cur
|
||||||
|
|
||||||
|
def pprint(self):
|
||||||
|
for o in self.overlays:
|
||||||
|
o.pprint()
|
||||||
|
|
||||||
|
|
||||||
|
# not sure how we would define escapes for filenames with ^ : or backslash
|
||||||
|
filt = (Suppress("[") + CharsNotIn("^[():")("name") + ZeroOrMore(Suppress(":") + CharsNotIn("^[():")("opt*")))("filter*")
|
||||||
|
filt.set_parse_action(Filter)
|
||||||
|
fname = CharsNotIn("^():\\")("filename*")
|
||||||
|
fname.set_parse_action(Filename)
|
||||||
|
parser = infix_notation(filt ^ fname, lpar=Suppress('('), rpar=Suppress(')'),
|
||||||
|
op_list=[(Suppress("^"), 2, opAssoc.LEFT, Overlay)])
|
||||||
|
|
||||||
|
try:
|
||||||
|
opts, args = getopt.getopt(sys.argv[1:], "hg:m:", ["help", "game=", "mods="])
|
||||||
|
except getopt.GetoptError as e:
|
||||||
|
print(str(e))
|
||||||
|
exit(1)
|
||||||
|
if ('-h', '') in opts or ('--help', '') in opts:
|
||||||
|
usage()
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
input_file = "./nodes.txt"
|
||||||
|
output_file = "./colors.txt"
|
||||||
|
json_file = "./colors.json"
|
||||||
|
texturepaths = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
gamepath = next(o[1] for o in opts if o[0] in ('-g', '--game'))
|
||||||
|
if not os.path.isdir(os.path.join(gamepath, "mods")):
|
||||||
|
print(f"'{gamepath}' doesn't exist or does not contain a game.", file=sys.stderr)
|
||||||
|
exit(1)
|
||||||
|
texturepaths.append(gamepath)
|
||||||
|
except StopIteration:
|
||||||
|
print("No game path set but one is required. (see --help)", file=sys.stderr)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
for o in opts:
|
||||||
|
if o[0] not in ('-m', '--mods'): continue
|
||||||
|
if not os.path.isdir(o[1]):
|
||||||
|
print(f"Given path '{o[1]}' does not exist.'", file=sys.stderr)
|
||||||
|
exit(1)
|
||||||
|
texturepaths.append(o[1])
|
||||||
|
|
||||||
|
if len(args) > 2:
|
||||||
|
print("Too many arguments.", file=sys.stderr)
|
||||||
|
exit(1)
|
||||||
|
if len(args) > 0:
|
||||||
|
input_file = args[0]
|
||||||
|
|
||||||
|
if not os.path.exists(input_file) or os.path.isdir(input_file):
|
||||||
|
print(f"Input file '{input_file}' does not exist.", file=sys.stderr)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
# Build a cache to locate textures
|
||||||
|
print(f"Collecting textures from {len(texturepaths)} path(s)... ", end="", flush=True)
|
||||||
|
for path in texturepaths:
|
||||||
|
for dirpath, dirnames, filenames in os.walk(path):
|
||||||
|
for f in filenames:
|
||||||
|
if not f in textures:
|
||||||
|
textures[f] = os.path.join(dirpath, f)
|
||||||
|
|
||||||
|
print("done", len(textures), "files")
|
||||||
|
|
||||||
|
print("Processing nodes...")
|
||||||
|
cmap = dict()
|
||||||
|
fin = open(input_file, 'r')
|
||||||
|
for line in fin:
|
||||||
|
line = line.rstrip('\r\n')
|
||||||
|
if not line or line[0] == '#':
|
||||||
|
#fout.write(line + '\n')
|
||||||
|
continue
|
||||||
|
line = line.split(" ")
|
||||||
|
node, tex = line[0], line[1]
|
||||||
|
if not tex or tex == "blank.png":
|
||||||
|
continue
|
||||||
|
im = None
|
||||||
|
if "^" in tex or "[" in tex:
|
||||||
|
#print(node, tex)
|
||||||
|
im = parser.parse_string(tex)[0].gen()
|
||||||
|
#assert not "/" in node
|
||||||
|
#im.save(os.path.join("/tmp/test",node+".png"))
|
||||||
|
elif tex not in textures:
|
||||||
|
print(f"skip {node} texture {tex} not found")
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
im = Image.open(textures[tex]).convert("RGBA")
|
||||||
|
# TODO: full param2 support
|
||||||
|
color2 = None
|
||||||
|
if len(line) == 3 and line[2].startswith("#"):
|
||||||
|
color2 = ImageColor.getrgb(line[2])
|
||||||
|
elif len(line) > 3 and line[2].startswith("color"):
|
||||||
|
if line[3].startswith("[combine:16x2:0,0="): line[3] = line[3][len("[combine:16x2:0,0="):] # simple resize for colorwallmounted
|
||||||
|
tints = get_param2_colors(line[3])
|
||||||
|
if tints:
|
||||||
|
cmap[node] = [average_color(node+" "+tex, im, v) for v in tints]
|
||||||
|
continue
|
||||||
|
print("Unsupported:", *line)
|
||||||
|
elif len(line) > 2:
|
||||||
|
print("Unsupported:", *line[2:])
|
||||||
|
color = average_color(node+" "+tex, im, color2)
|
||||||
|
cmap[node] = color
|
||||||
|
fin.close()
|
||||||
|
|
||||||
|
# fix some missing values, perform some substitutions
|
||||||
|
for node, color in sorted(cmap.items()):
|
||||||
|
# Try stripping off last _postfix
|
||||||
|
if not color: color = cmap.get(node.rsplit("_", 1)[0])
|
||||||
|
for pat, rule in REPLACEMENTS:
|
||||||
|
if pat.search(node):
|
||||||
|
color = rule(color) if callable(rule) else rule
|
||||||
|
cmap[node] = color
|
||||||
|
|
||||||
|
cmap = dict((x,y) for x,y in sorted(cmap.items()) if y) # remove remaining null entries
|
||||||
|
|
||||||
|
n = 0
|
||||||
|
fout = open(output_file, 'w')
|
||||||
|
prefix = ""
|
||||||
|
for node, color in cmap.items():
|
||||||
|
if not prefix or not node.startswith(prefix):
|
||||||
|
prefix = node.split(":")[0] + ":"
|
||||||
|
fout.write("\n# " + prefix[:-1] + "\n")
|
||||||
|
if not isinstance(color[0], int): color = color[0] # param2 needs minetestmapper support first
|
||||||
|
color = " ".join(str(x) for x in color)
|
||||||
|
fout.write(f"{node} {color}\n")
|
||||||
|
n += 1
|
||||||
|
fout.close()
|
||||||
|
js = json.dumps(cmap, indent="\t", separators=(",",":\t"))
|
||||||
|
js = re.sub(r'\n\t*(?!["\t}])', " ", js) # partially undo indenting to make more compact
|
||||||
|
open(json_file, "w").write(js)
|
||||||
|
print(f"Done, {n} entries written.")
|
Loading…
Reference in a new issue