mirror of
https://github.com/freedoom/freedoom.git
synced 2025-09-03 19:25:45 -04:00
The tags are shorthand for the license of each file and avoid copying the full license text into each one (and avoids having to manually update the dates in each one...).
229 lines
6.4 KiB
Python
Executable file
229 lines
6.4 KiB
Python
Executable file
#!/usr/bin/env python
|
|
# Copyright (C) 2001 Colin Phipps <cphipps@doomworld.com>
|
|
# Copyright (C) 2008, 2013 Simon Howard
|
|
# Parts copyright (C) 1999 by id Software (http://www.idsoftware.com/)
|
|
#
|
|
# SPDX-License-Identifier: GPL-2.0+
|
|
#
|
|
# Takes PLAYPAL as input (filename is the only parameter)
|
|
# Produces a light graduated COLORMAP on stdout
|
|
# O(n^2)
|
|
#
|
|
# This was originally a Perl script by Colin Phipps; it was converted
|
|
# to Python and now is a more generic tool for generating all kinds of
|
|
# COLORMAP effects.
|
|
#
|
|
|
|
import os
|
|
import sys
|
|
import struct
|
|
|
|
# Parameters affecting colormap generation:
|
|
|
|
# "Darkness" is this color, which is usually black, but can be
|
|
# overridden (RGB 0-255):
|
|
dark_color = (0, 0, 0)
|
|
|
|
# Color to tint the colormap (RGB 0.0-1.0):
|
|
tint_color = (255, 255, 255)
|
|
|
|
# Fractional balance between tint and normal color. 0 is no tint applied,
|
|
# 1.0 is full tint.
|
|
tint_frac = 0
|
|
|
|
# Fudge factor to adjust brightness when calculating 'tinted' version
|
|
# of colors. Larger values are brighter but may cause color clipping.
|
|
# A value of 0.33 is a straight-average of the RGB channels. Maximum
|
|
# sensible value is 1.0, though it can be overdriven for fancy
|
|
# brightness effects.
|
|
tint_bright = 0.5
|
|
|
|
def read_palette(filename):
|
|
"""Read palette from file and return a list of tuples containing
|
|
RGB values."""
|
|
f = open(filename, "rb")
|
|
|
|
colors = []
|
|
|
|
for i in range(256):
|
|
data = f.read(3)
|
|
|
|
color = struct.unpack("BBB", data)
|
|
colors.append(color)
|
|
|
|
return colors
|
|
|
|
# Return closest palette entry to the given RGB triple
|
|
|
|
def search_palette(palette, target):
|
|
"""Search the given palette and find the nearest matching
|
|
color to the given color, returning an index into the
|
|
palette of the color that best matches."""
|
|
best_diff = None
|
|
best_index = None
|
|
|
|
def square(x):
|
|
return x * x
|
|
|
|
for i in range(len(palette)):
|
|
color = palette[i]
|
|
|
|
diff = square(target[0] - color[0]) \
|
|
+ square(target[1] - color[1]) \
|
|
+ square(target[2] - color[2])
|
|
|
|
if best_index is None or diff < best_diff:
|
|
best_diff = diff
|
|
best_index = i
|
|
|
|
return best_index
|
|
|
|
def generate_colormap(colors, palette):
|
|
"""Given a list of colors, translate these into indexes into
|
|
the given palette, finding the nearest color where an exact
|
|
match cannot be found."""
|
|
result = []
|
|
|
|
for color in colors:
|
|
index = search_palette(palette, color)
|
|
result.append(index)
|
|
|
|
return result
|
|
|
|
def tint_colors(colors, tint, bright=0.5):
|
|
"""Given a list of colors, tint them a particular color."""
|
|
|
|
result = []
|
|
for c in colors:
|
|
# I've experimented with different methods of calculating
|
|
# intensity, but this seems to work the best. This is basically
|
|
# doing an average of the full channels, but a straight
|
|
# average causes the picture to get darker - eg. (0,0,255)
|
|
# maps to (87,87,87). So we have a controllable brightness
|
|
# factor that allows the brightness to be adjusted.
|
|
intensity = min((c[0] + c[1] + c[2]) * bright, 255) / 255.0
|
|
result.append((
|
|
tint[0] * intensity,
|
|
tint[1] * intensity,
|
|
tint[2] * intensity,
|
|
))
|
|
|
|
return result
|
|
|
|
def blend_colors(colors1, colors2, factor=0.5):
|
|
"""Blend the two given lists of colors, with 'factor' controlling
|
|
the mix between the two. factor=0 is exactly colors1, while
|
|
factor=1 is exactly colors2. Returns a list of blended colors."""
|
|
result = []
|
|
|
|
for index, c1 in enumerate(colors1):
|
|
c2 = colors2[index]
|
|
|
|
result.append((
|
|
c2[0] * factor + c1[0] * (1 - factor),
|
|
c2[1] * factor + c1[1] * (1 - factor),
|
|
c2[2] * factor + c1[2] * (1 - factor),
|
|
))
|
|
|
|
return result
|
|
|
|
def invert_colors(colors):
|
|
"""Given a list of colors, translate them to inverted monochrome."""
|
|
result = []
|
|
|
|
for color in colors:
|
|
average = (color[0] + color[1] + color[2]) // 3
|
|
inverse = 255 - average
|
|
|
|
result.append((inverse, inverse, inverse))
|
|
|
|
return result
|
|
|
|
def solid_color_list(color):
|
|
"""Generate a 256-entry palette where all entries are the
|
|
same color."""
|
|
return [color] * 256
|
|
|
|
def output_colormap(colormap):
|
|
"""Output the given palette to stdout."""
|
|
for c in colormap:
|
|
x = struct.pack("B", c)
|
|
os.write(sys.stdout.fileno(), x)
|
|
|
|
def print_palette(colors):
|
|
for y in range(16):
|
|
for x in range(16):
|
|
color = colors[y * 16 + x]
|
|
|
|
print("#%02x%02x%02x" % color)
|
|
|
|
print()
|
|
|
|
def parse_color_code(s):
|
|
"""Parse a color code in HTML color code format, into an RGB
|
|
3-tuple value."""
|
|
if not s.startswith('#') or len(s) != 7:
|
|
raise Exception('Not in HTML color code form: %s' % s)
|
|
return (int(s[1:3], 16), int(s[3:5], 16), int(s[5:7], 16))
|
|
|
|
def set_parameter(name, value):
|
|
"""Set configuration value, from command line parameters."""
|
|
global dark_color, tint_color, tint_frac, tint_bright
|
|
|
|
if name == 'dark_color':
|
|
dark_color = parse_color_code(value)
|
|
elif name == 'tint_color':
|
|
tint_color = parse_color_code(value)
|
|
elif name == 'tint_pct':
|
|
tint_frac = int(value) / 100.0
|
|
elif name == 'tint_bright':
|
|
tint_bright = float(value)
|
|
else:
|
|
raise Exception("Unknown parameter: '%s'" % name)
|
|
|
|
# Parse command line.
|
|
|
|
playpal_filename = None
|
|
|
|
for arg in sys.argv[1:]:
|
|
if arg.startswith('--') and '=' in arg:
|
|
key, val = arg[2:].split('=', 2)
|
|
set_parameter(key, val)
|
|
else:
|
|
playpal_filename = arg
|
|
|
|
if playpal_filename is None:
|
|
print("Usage: %s playpal.lmp > output-file.lmp" % sys.argv[0])
|
|
sys.exit(1)
|
|
|
|
palette = read_palette(playpal_filename)
|
|
colors = palette
|
|
|
|
# Apply tint, if enabled.
|
|
# The tint is intentionally applied *before* the darkening effect is
|
|
# applied. This allows us to darken to a different color than the tint
|
|
# color, if so desired.
|
|
if tint_frac > 0:
|
|
colors = blend_colors(palette,
|
|
tint_colors(colors, tint_color, tint_bright),
|
|
tint_frac)
|
|
|
|
# Generate colormaps for different darkness levels, by blending between
|
|
# the default colors and a palette where every entry is the "dark" color.
|
|
dark = solid_color_list(dark_color)
|
|
|
|
for i in range(32):
|
|
darken_factor = (32 - i) / 32.0
|
|
darkened_colors = blend_colors(dark, colors, darken_factor)
|
|
output_colormap(generate_colormap(darkened_colors, palette))
|
|
|
|
# Inverse color map for invulnerability effect.
|
|
inverse_colors = invert_colors(palette)
|
|
output_colormap(generate_colormap(inverse_colors, palette))
|
|
|
|
# Last colormap is all black, and is actually unused in Vanilla Doom
|
|
# (it was mistakenly included by the dcolors.c utility). It's
|
|
# strictly unneeded, though some utilities (SLADE) do not detect a
|
|
# lump as a COLORMAP unless it is the right length.
|
|
output_colormap(generate_colormap(dark, palette))
|
|
|