initial commit

This commit is contained in:
saarsena@gmail.com 2026-04-22 10:19:57 -04:00
commit c2bb3893a9
1038 changed files with 75846 additions and 0 deletions

View file

@ -0,0 +1,293 @@
#!/usr/bin/env python3
"""
Aggressive pixel art wall weathering with zoomed comparison output.
"""
import colorsys
import random
from noise import pnoise2
from PIL import Image, ImageDraw, ImageFont
def make_noise_map(w, h, seed=0, scale=0.08, octaves=3):
"""Generate a 2D Perlin noise map normalized to 0.0-1.0"""
nmap = []
for y in range(h):
row = []
for x in range(w):
# pnoise2 returns roughly -1 to 1, normalize to 0-1
val = pnoise2(
x * scale + seed * 100,
y * scale + seed * 100,
octaves=octaves,
persistence=0.5,
lacunarity=2.0,
)
row.append((val + 1.0) / 2.0) # normalize to 0-1
nmap.append(row)
return nmap
def rgb_to_hsv(r, g, b):
h, s, v = colorsys.rgb_to_hsv(r / 255, g / 255, b / 255)
return h * 360, s * 100, v * 100
def hsv_to_rgb(h, s, v):
r, g, b = colorsys.hsv_to_rgb(h / 360, s / 100, v / 100)
return (int(r * 255), int(g * 255), int(b * 255))
def shift_color(rgb, hue_shift=0, sat_shift=0, val_shift=0):
h, s, v = rgb_to_hsv(*rgb)
h = (h + hue_shift) % 360
s = max(0, min(100, s + sat_shift))
v = max(0, min(100, v + val_shift))
return hsv_to_rgb(h, s, v)
def is_mortar(pixel, threshold=120):
return pixel[1] < threshold
def is_bright_brick(pixel, threshold=180):
return pixel[1] > threshold
def apply_weathering(img, noise_seed=0):
result = img.copy()
w, h = img.size
random.seed(42 + noise_seed)
# Generate noise maps at different scales for different effects
# Coarse noise: large blotchy weathering patterns
noise_coarse = make_noise_map(w, h, seed=noise_seed, scale=0.06, octaves=2)
# Fine noise: small pixel-level variation
noise_fine = make_noise_map(w, h, seed=noise_seed + 50, scale=0.15, octaves=3)
# PASS 1: Deepen mortar intersections - MORE AGGRESSIVE
for y in range(h):
for x in range(w):
px = img.getpixel((x, y))
if not is_mortar(px):
continue
mortar_neighbors = 0
for dx, dy in [
(-1, 0),
(1, 0),
(0, -1),
(0, 1),
(-1, -1),
(1, -1),
(-1, 1),
(1, 1),
]:
nx, ny = (x + dx) % w, (y + dy) % h
if is_mortar(img.getpixel((nx, ny))):
mortar_neighbors += 1
if mortar_neighbors >= 6:
# Deep blue-shifted shadow
new_px = shift_color(px, hue_shift=25, sat_shift=15, val_shift=-20)
result.putpixel((x, y), new_px)
elif mortar_neighbors >= 4:
new_px = shift_color(px, hue_shift=12, sat_shift=8, val_shift=-10)
result.putpixel((x, y), new_px)
# PASS 2: Warm highlights on brick edges
for y in range(h):
for x in range(w):
px = img.getpixel((x, y))
if is_mortar(px):
continue
above = img.getpixel((x, (y - 1) % h))
left = img.getpixel(((x - 1) % w, y))
is_top_edge = is_mortar(above)
is_left_edge = is_mortar(left)
if is_top_edge or is_left_edge:
random.seed(hash((x // 6, y // 4)) + 99)
if random.random() < 0.4:
# Warm yellow highlight
new_px = shift_color(px, hue_shift=-15, sat_shift=5, val_shift=10)
result.putpixel((x, y), new_px)
random.seed(42)
# PASS 3: Vary whole brick colors - MORE VISIBLE
for y in range(h):
for x in range(w):
px = img.getpixel((x, y))
if is_mortar(px):
continue
brick_id = (x // 12, y // 8)
random.seed(hash(brick_id) + 42)
roll = random.random()
if roll < 0.2:
# Darker, warmer brick
current = result.getpixel((x, y))
new_px = shift_color(current, hue_shift=8, sat_shift=6, val_shift=-12)
result.putpixel((x, y), new_px)
elif roll < 0.35:
# Slightly cooler brick
current = result.getpixel((x, y))
new_px = shift_color(current, hue_shift=-8, sat_shift=4, val_shift=-5)
result.putpixel((x, y), new_px)
random.seed(42 + noise_seed)
# PASS 4: Full-height darkening gradient (rising damp / ground grime)
# Modulated by Perlin noise so the damp line isn't perfectly horizontal
GRADIENT_EXPONENT = 3.5
GRADIENT_MAX_DARKEN = -30
GRADIENT_MAX_HUE_COOL = 25
GRADIENT_MAX_SAT_BUMP = 12
for y in range(h):
progress = y / (h - 1)
curve = progress**GRADIENT_EXPONENT
for x in range(w):
# Noise pushes the gradient up or down per-pixel
# coarse noise warps the damp line by +/- 30% of its intensity
noise_mod = 0.7 + noise_coarse[y][x] * 0.6 # range: 0.7 to 1.3
modulated = min(1.0, curve * noise_mod)
px = result.getpixel((x, y))
val_drop = GRADIENT_MAX_DARKEN * modulated
hue_cool = GRADIENT_MAX_HUE_COOL * modulated
sat_bump = GRADIENT_MAX_SAT_BUMP * modulated
new_px = shift_color(
px, hue_shift=hue_cool, sat_shift=sat_bump, val_shift=val_drop
)
result.putpixel((x, y), new_px)
# PASS 5: Stain patches - BIGGER AND MORE VISIBLE
num_stains = 6
for _ in range(num_stains):
cx = random.randint(0, w - 1)
cy = random.randint(0, h - 1)
radius = random.randint(3, 7)
for dy in range(-radius, radius + 1):
for dx in range(-radius, radius + 1):
dist = (dx * dx + dy * dy) ** 0.5
if dist > radius:
continue
nx, ny = (cx + dx) % w, (cy + dy) % h
if is_mortar(img.getpixel((nx, ny))):
continue
intensity = 1.0 - (dist / radius)
current = result.getpixel((nx, ny))
new_px = shift_color(
current, hue_shift=8, sat_shift=6, val_shift=-10 * intensity
)
result.putpixel((nx, ny), new_px)
# PASS 6: Crack line (one diagonal crack)
crack_x = random.randint(w // 4, 3 * w // 4)
crack_y = random.randint(0, h // 3)
for i in range(12):
cx = crack_x + random.choice([-1, 0, 0, 1])
cy = crack_y + i
crack_x = cx
if 0 <= cx < w and 0 <= cy < h:
px = result.getpixel((cx, cy))
if not is_mortar(img.getpixel((cx, cy))):
# Dark cool crack pixel
crack_color = shift_color(px, hue_shift=20, sat_shift=10, val_shift=-25)
result.putpixel((cx, cy), crack_color)
# PASS 7: Moss specks in mortar - MORE OF THEM
random.seed(77 + noise_seed)
for y in range(h // 3, h):
for x in range(w):
px = img.getpixel((x, y))
if not is_mortar(px):
continue
depth = (y - h // 3) / (2 * h // 3)
# Noise modulates where moss appears
moss_chance = 0.06 * depth * (0.5 + noise_fine[y][x])
if random.random() < moss_chance:
current = result.getpixel((x, y))
moss = shift_color(current, hue_shift=-80, sat_shift=20, val_shift=-10)
result.putpixel((x, y), moss)
# PASS 8: Perlin noise surface grime overlay
# Fine noise adds subtle per-pixel value/hue variation across brick faces
# This is the pass that makes each seed look distinctly different
for y in range(h):
for x in range(w):
px = img.getpixel((x, y))
if is_mortar(px):
continue # Only affect brick faces
current = result.getpixel((x, y))
n = noise_fine[y][x] # 0.0 to 1.0
# Map noise to subtle shifts: center around 0
# n=0.5 means no change, n=0 means darken, n=1 means lighten slightly
centered = n - 0.5 # -0.5 to 0.5
val_shift = centered * 10 # -5 to +5 value
hue_shift = centered * 8 # -4 to +4 hue
sat_shift = abs(centered) * 4 # darker areas get slightly more saturated
new_px = shift_color(
current, hue_shift=hue_shift, sat_shift=sat_shift, val_shift=val_shift
)
result.putpixel((x, y), new_px)
return result
def main():
img = Image.open(
"/home/saarsena/godot_projects/trench/textures/td_sliced/BRICK_1A.png"
).convert("RGB")
w, h = img.size
# Generate 4 variants with different noise seeds
variants = []
for seed in range(4):
variant = apply_weathering(img, noise_seed=seed)
variant.save(f"./weathered_wall_v{seed}.png")
variants.append(variant)
print(f"Generated variant {seed}")
# Create a 2x2 grid comparison of all 4 variants, zoomed 6x
zoom = 6
zw, zh = w * zoom, h * zoom
grid = Image.new("RGB", (zw * 2 + 4, zh * 2 + 4), (30, 30, 30))
for i, v in enumerate(variants):
zoomed = v.resize((zw, zh), Image.NEAREST)
col = i % 2
row = i // 2
grid.paste(zoomed, (col * (zw + 4), row * (zh + 4)))
grid.save("./wall_variants_grid.png")
# Also make a side-by-side original vs variant 0
orig_zoomed = img.resize((zw, zh), Image.NEAREST)
weath_zoomed = variants[0].resize((zw, zh), Image.NEAREST)
gap = 20
comparison = Image.new("RGB", (zw * 2 + gap, zh + 40), (30, 30, 30))
comparison.paste(orig_zoomed, (0, 40))
comparison.paste(weath_zoomed, (zw + gap, 40))
draw = ImageDraw.Draw(comparison)
draw.text((zw // 2 - 30, 10), "ORIGINAL", fill=(200, 200, 200))
draw.text((zw + gap + zw // 2 - 35, 10), "WEATHERED", fill=(200, 200, 200))
comparison.save("./wall_comparison.png")
print("Saved: 4 variants, grid comparison, and side-by-side")
if __name__ == "__main__":
main()