initial commit
This commit is contained in:
commit
c2bb3893a9
1038 changed files with 75846 additions and 0 deletions
293
textures/td_sliced/weather.py
Normal file
293
textures/td_sliced/weather.py
Normal 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue