mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-21 16:02:37 +00:00
- Bake: each layer is sampled only on its own painted area; analytic projections used to stack every layer over every painted region, so the top layer's texture showed on all of them (colour sampler too) - Auto resolution follows the texture's texel size and sharpness again - Unwrap: charts cut by each face's own normal (a cube gives 6 islands, not 12 triangles); non-disk charts (tubes, closed shells) are split until they flatten; connected nets test real triangle overlap, grow from the largest chart and are packed side by side - UV edits are stored per unwrapped copy, so dragging a seam vertex no longer moves its copies in neighbouring islands - UV pane: tool strip with unwrap settings moved in from the panel, sharp HiDPI icons, clearer island/edge/selection drawing with hover, texture picker from the thumbnail, texture no longer lost on reopen (GL state from the 3D view, background upload retries) - Panel: whole-model select/erase as icons in the tools row; inactive layers' paint shown muted; colour textures shown in colour in the picker - Built-in displacement texture library - Tests for unwrap segmentation, connected nets, UV edits and per-layer sampling
478 lines
21 KiB
Python
478 lines
21 KiB
Python
#!/usr/bin/env python3
|
|
"""Generates the displacement textures shipped in resources/textures/displacement/.
|
|
|
|
Every texture is procedural - periodic functions and wrap-around noise on a unit tile - so the images
|
|
tile seamlessly in both directions and carry no third-party material. Grey PNGs are height maps
|
|
(white = raised). The colour PNGs are RGB where the luminance is the height, which is what
|
|
decode_height_texture() reads, so they displace and colour at the same time.
|
|
|
|
python3 scripts/generate_displacement_textures.py [--size 1024] [--out resources/textures/displacement]
|
|
|
|
Needs numpy and Pillow. Regenerating overwrites the files this script owns and nothing else.
|
|
"""
|
|
import argparse, os
|
|
import numpy as np
|
|
from PIL import Image
|
|
|
|
SS = 2 # supersampling factor; the final image is the box-filtered average
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# helpers: everything works on u, v in [0, 1) with period 1 on the tile
|
|
# ---------------------------------------------------------------------------------------------
|
|
def grid(n):
|
|
s = n * SS
|
|
v, u = np.meshgrid((np.arange(s) + 0.5) / s, (np.arange(s) + 0.5) / s, indexing='ij')
|
|
return u, v
|
|
|
|
def down(img):
|
|
s = img.shape[0] // SS
|
|
return img.reshape(s, SS, s, SS).mean(axis=(1, 3))
|
|
|
|
def norm(h):
|
|
h = h - h.min(); m = h.max()
|
|
return h / m if m > 0 else h
|
|
|
|
def smoothstep(e0, e1, x):
|
|
t = np.clip((x - e0) / (e1 - e0), 0.0, 1.0)
|
|
return t * t * (3 - 2 * t)
|
|
|
|
def value_noise(u, v, freq, seed):
|
|
"""Tileable value noise: a random lattice of `freq` cells with wrap-around, smooth interpolation."""
|
|
rng = np.random.default_rng(seed)
|
|
lat = rng.random((freq, freq))
|
|
x = u * freq; y = v * freq
|
|
x0 = np.floor(x).astype(int); y0 = np.floor(y).astype(int)
|
|
fx = x - x0; fy = y - y0
|
|
fx = fx * fx * (3 - 2 * fx); fy = fy * fy * (3 - 2 * fy)
|
|
x1 = (x0 + 1) % freq; y1 = (y0 + 1) % freq; x0 %= freq; y0 %= freq
|
|
a = lat[y0, x0]; b = lat[y0, x1]; c = lat[y1, x0]; d = lat[y1, x1]
|
|
return (a * (1 - fx) + b * fx) * (1 - fy) + (c * (1 - fx) + d * fx) * fy
|
|
|
|
def fbm(u, v, freq, seed, octaves=4, gain=0.5):
|
|
out = np.zeros_like(u); amp = 1.0; total = 0.0
|
|
for o in range(octaves):
|
|
out += amp * value_noise(u, v, freq * 2 ** o, seed + o); total += amp; amp *= gain
|
|
return out / total
|
|
|
|
def wrapped_points(n, seed, jitter=0.35):
|
|
"""n x n jittered lattice points on the unit tile (periodic)."""
|
|
rng = np.random.default_rng(seed)
|
|
gx, gy = np.meshgrid((np.arange(n) + 0.5) / n, (np.arange(n) + 0.5) / n)
|
|
pts = np.stack([gx.ravel(), gy.ravel()], 1) + (rng.random((n * n, 2)) - 0.5) * (jitter / n)
|
|
return pts % 1.0
|
|
|
|
def voronoi(u, v, pts):
|
|
"""Distance to the nearest and second-nearest point on the torus, and the nearest point's index."""
|
|
d1 = np.full(u.shape, 9.0); d2 = np.full(u.shape, 9.0); idx = np.zeros(u.shape, int)
|
|
for i, (px, py) in enumerate(pts):
|
|
dx = u - px; dx -= np.round(dx); dy = v - py; dy -= np.round(dy)
|
|
d = np.sqrt(dx * dx + dy * dy)
|
|
closer = d < d1
|
|
d2 = np.where(closer, d1, np.minimum(d2, d)); d1 = np.where(closer, d, d1); idx = np.where(closer, i, idx)
|
|
return d1, d2, idx
|
|
|
|
def tri_wave(x):
|
|
return np.abs(2 * (x - np.floor(x + 0.5))) # 0..1 triangle wave, period 1
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# the textures
|
|
# ---------------------------------------------------------------------------------------------
|
|
def diamond_plate(u, v):
|
|
# Raised diamonds in two offset rows, bevelled; the classic tread plate.
|
|
n = 4
|
|
h = np.zeros_like(u)
|
|
for ox, oy in ((0.0, 0.0), (0.5, 0.5)):
|
|
x = (u * n + ox) % 1 - 0.5; y = (v * n + oy) % 1 - 0.5
|
|
d = np.abs(x) / 0.36 + np.abs(y) / 0.18 # a long diamond, 2:1
|
|
h = np.maximum(h, smoothstep(1.0, 0.72, d))
|
|
h += 0.06 * fbm(u, v, 32, 11)
|
|
return norm(h)
|
|
|
|
def hex_cells(u, v, nx):
|
|
"""Distance to the nearest hexagon centre, normalised so the hexagon's edge is at 1, plus a cell id.
|
|
nx hexagons across; the rows are stretched by at most ~2 % so a whole number fit the tile."""
|
|
W = 1.0 / nx # hexagon width (flat-to-flat, pointy-top layout)
|
|
H = 2 * W / np.sqrt(3) # ideal corner-to-corner height
|
|
ny = 2 * max(1, int(round(1.0 / (1.5 * H)))) # rows per tile, even: the two staggered lattices
|
|
H = 1.0 / (0.75 * ny) # stretched so the rows tile exactly
|
|
best = np.full(u.shape, 9.0); cid = np.zeros(u.shape, int)
|
|
for k, (ox, oy) in enumerate(((0.0, 0.0), (0.5, 0.5))):
|
|
cx = (np.round(u / W - ox) + ox) * W; cy = (np.round(v / (1.5 * H) - oy) + oy) * 1.5 * H
|
|
dx = u - cx
|
|
dy = (v - cy) * (2 * W / np.sqrt(3)) / H # undo the row stretch: regular-hex units
|
|
ax = np.abs(dx) / (W / 2); ay = np.abs(dy) / (W / 2)
|
|
d = np.maximum(ax, (ax + ay * np.sqrt(3)) / 2) # hexagon SDF, edge at 1
|
|
ix = np.round(u / W - ox).astype(int) % nx; iy = np.round(v / (1.5 * H) - oy).astype(int) % (ny // 2)
|
|
this = (ix * 7 + iy * 13 + k * 3) % 1009 # periodic, so a cell's colour matches across the seam
|
|
cid = np.where(d < best, this, cid); best = np.minimum(best, d)
|
|
return best, cid
|
|
|
|
def honeycomb(u, v):
|
|
d, _ = hex_cells(u, v, 7)
|
|
return norm(smoothstep(1.0, 0.86, d))
|
|
|
|
def knurl(u, v):
|
|
n = 24
|
|
a = tri_wave((u + v) * n); b = tri_wave((u - v) * n)
|
|
return norm(1 - np.maximum(a, b)) # pyramids
|
|
|
|
def scales(u, v):
|
|
n = 8
|
|
h = np.zeros_like(u)
|
|
# rows of circles, each row offset by half a scale and drawn over the row below
|
|
for row in range(-1, 2 * n + 1):
|
|
cy = row / (2 * n); ox = 0.5 if row % 2 else 0.0
|
|
x = (u * n + ox) % 1 - 0.5; y = v - cy
|
|
y -= np.round(y)
|
|
r = np.sqrt((x / 1.0) ** 2 + (y * n) ** 2)
|
|
inside = r < 0.5
|
|
dome = np.sqrt(np.clip(0.25 - r * r, 0, None)) * 2 # spherical cap
|
|
ramp = 0.4 + 0.6 * np.clip((0.5 * n * -y) / 0.5 + 0.5, 0, 1) # thicker at the exposed edge
|
|
cand = np.where(inside, 0.35 + 0.65 * dome * ramp, 0)
|
|
h = np.where(inside & (y * n <= 0.02), cand, h)
|
|
return norm(h)
|
|
|
|
def herringbone(u, v):
|
|
# 2:1 bricks in the domino herringbone, turned 45 degrees. In cell coordinates (x, y) the brick a
|
|
# cell belongs to follows from (i - j) mod 4: 0/1 pair horizontally, 2/3 pair vertically. An even
|
|
# cell count per tile edge keeps that rule periodic.
|
|
n = 4
|
|
x = (u + v) * n; y = (u - v) * n
|
|
i = np.floor(x); j = np.floor(y); fx = x - i; fy = y - j
|
|
k = ((i - j) % 4 + 4) % 4
|
|
bx = np.where(k == 0, fx / 2, np.where(k == 1, 0.5 + fx / 2, fx))
|
|
by = np.where(k == 2, 0.5 + fy / 2, np.where(k == 3, fy / 2, fy))
|
|
# local coordinates on a 2x1 brick: the long axis is x for k in {0,1}, y for k in {2,3}
|
|
long_ = np.where(k < 2, bx, by); short = np.where(k < 2, by, bx)
|
|
gap_l = 0.03; gap_s = 0.06
|
|
m = smoothstep(0, gap_l, long_) * smoothstep(0, gap_l, 1 - long_) * smoothstep(0, gap_s, short) * smoothstep(0, gap_s, 1 - short)
|
|
return norm(0.85 * m + 0.15 * m * fbm(u, v, 16, 5))
|
|
|
|
def cobblestone(u, v):
|
|
pts = wrapped_points(6, 21, 0.55)
|
|
d1, d2, idx = voronoi(u, v, pts)
|
|
edge = d2 - d1
|
|
rng = np.random.default_rng(22); tops = rng.random(len(pts)) * 0.25 + 0.75
|
|
h = smoothstep(0.0, 0.05, edge) * (0.55 + 0.45 * np.sqrt(np.clip(1 - (d1 / 0.11) ** 2, 0, None)))
|
|
h *= tops[idx]
|
|
h += 0.08 * fbm(u, v, 48, 23)
|
|
return norm(h)
|
|
|
|
def leather(u, v):
|
|
pts = wrapped_points(22, 31, 0.9)
|
|
d1, d2, idx = voronoi(u, v, pts)
|
|
grooves = 1 - smoothstep(0.0, 0.012, d2 - d1)
|
|
h = 1 - 0.55 * grooves - 0.25 * fbm(u, v, 12, 33) - 0.1 * fbm(u, v, 96, 34)
|
|
return norm(h)
|
|
|
|
def carbon_fibre(u, v):
|
|
n = 8
|
|
x = u * n; y = v * n
|
|
cx = np.floor(x); cy = np.floor(y)
|
|
over = ((cx + cy) % 4) < 2 # 2x2 twill
|
|
fx = x % 1; fy = y % 1
|
|
warp = 1 - 0.5 * (2 * np.abs(fx - 0.5)) ** 2 + 0.06 * np.sin(fy * 2 * np.pi * 14)
|
|
weft = 1 - 0.5 * (2 * np.abs(fy - 0.5)) ** 2 + 0.06 * np.sin(fx * 2 * np.pi * 14)
|
|
h = np.where(over, warp, weft * 0.92)
|
|
return norm(h)
|
|
|
|
def chevron(u, v):
|
|
n = 6
|
|
h = tri_wave(v * n * 2 + tri_wave(u * n) * 1.0)
|
|
return norm(1 - smoothstep(0.35, 0.65, h))
|
|
|
|
def ripples(u, v):
|
|
# Rings spreading from a few points on the torus, fading with distance - rain on water.
|
|
pts = wrapped_points(3, 81, 1.0)
|
|
h = np.zeros_like(u)
|
|
rng = np.random.default_rng(82)
|
|
for (px, py), f, ph in zip(pts, rng.random(len(pts)) * 6 + 10, rng.random(len(pts)) * 6.28):
|
|
dx = u - px; dx -= np.round(dx); dy = v - py; dy -= np.round(dy)
|
|
r = np.sqrt(dx * dx + dy * dy)
|
|
h += np.exp(-r / 0.22) * np.cos(2 * np.pi * r * f + ph)
|
|
return norm(h)
|
|
|
|
def hammered(u, v):
|
|
pts = wrapped_points(9, 41, 0.8)
|
|
d1, d2, idx = voronoi(u, v, pts)
|
|
rng = np.random.default_rng(42); rad = rng.random(len(pts)) * 0.04 + 0.07
|
|
dent = np.clip(1 - (d1 / rad[idx]) ** 2, 0, None)
|
|
return norm(1 - 0.8 * dent + 0.05 * fbm(u, v, 64, 43))
|
|
|
|
def perforated(u, v):
|
|
n = 8
|
|
x = (u * n) % 1 - 0.5; y = (v * n) % 1 - 0.5
|
|
r = np.sqrt(x * x + y * y)
|
|
return norm(smoothstep(0.30, 0.34, r))
|
|
|
|
def rope(u, v):
|
|
n = 6 # ropes per tile, running along v
|
|
x = (u * n) % 1 - 0.5
|
|
body = np.sqrt(np.clip(0.25 - x * x, 0, None)) * 2
|
|
twist = 0.5 + 0.5 * np.sin(2 * np.pi * (v * 12 + x * 1.6))
|
|
return norm(body * (0.65 + 0.35 * twist))
|
|
|
|
def stone_wall(u, v):
|
|
rows = 5
|
|
y = v * rows; row = np.floor(y); fy = y % 1
|
|
rng = np.random.default_rng(51)
|
|
h = np.zeros_like(u)
|
|
for r in range(rows):
|
|
# stones of varying width along the row, periodic in u
|
|
widths = rng.random(6) * 0.6 + 0.7; widths *= 1.0 / widths.sum()
|
|
edges = np.concatenate([[0], np.cumsum(widths)]) + rng.random() * 0.3
|
|
xu = (u + 0.0) % 1
|
|
in_row = row == r
|
|
for i in range(len(widths)):
|
|
a = edges[i] % 1; w = widths[i]
|
|
dx = (xu - a) % 1
|
|
inside = dx < w
|
|
gap = 0.035
|
|
m = smoothstep(0, gap, dx) * smoothstep(0, gap, w - dx) * smoothstep(0, 0.12, fy) * smoothstep(0, 0.12, 1 - fy)
|
|
top = 0.7 + 0.3 * rng.random()
|
|
h = np.where(in_row & inside, m * top, h)
|
|
h = h * (0.85 + 0.15 * fbm(u, v, 24, 52)) + 0.04 * fbm(u, v, 96, 53)
|
|
return norm(h)
|
|
|
|
# ---- colour textures: (rgb in 0..1, height = luminance by construction)
|
|
def lum(rgb):
|
|
return 0.299 * rgb[..., 0] + 0.587 * rgb[..., 1] + 0.114 * rgb[..., 2]
|
|
|
|
def colour_bricks(u, v):
|
|
n = 6
|
|
y = v * n; row = np.floor(y); x = u * n * 2 + 0.5 * (row % 2)
|
|
bx = x % 1; by = y % 1
|
|
gap = 0.07
|
|
m = smoothstep(0, gap, bx) * smoothstep(0, gap, 1 - bx) * smoothstep(0, gap * 2, by) * smoothstep(0, gap * 2, 1 - by)
|
|
rng = np.random.default_rng(61)
|
|
cell = (np.floor(x).astype(int) * 7 + row.astype(int) * 13) % 97
|
|
tone = rng.random(97)[cell]
|
|
# brick reds of varying warmth over a dark grey mortar
|
|
brick = np.stack([0.70 + 0.2 * tone, 0.30 + 0.12 * tone, 0.22 + 0.06 * tone], -1)
|
|
brick *= (0.85 + 0.15 * fbm(u, v, 48, 62))[..., None]
|
|
mortar = np.array([0.30, 0.29, 0.27])
|
|
rgb = brick * m[..., None] + mortar * (1 - m[..., None])
|
|
return rgb
|
|
|
|
def mosaic(u, v):
|
|
n = 8
|
|
x = (u * n) % 1; y = (v * n) % 1
|
|
gap = 0.08
|
|
m = smoothstep(0, gap, x) * smoothstep(0, gap, 1 - x) * smoothstep(0, gap, y) * smoothstep(0, gap, 1 - y)
|
|
rng = np.random.default_rng(71)
|
|
cell = (np.floor(u * n).astype(int) * 31 + np.floor(v * n).astype(int) * 17) % 64
|
|
pal = np.array([[0.90, 0.85, 0.70], [0.20, 0.45, 0.75], [0.85, 0.35, 0.25], [0.35, 0.65, 0.40]])
|
|
tile = pal[rng.integers(0, 4, 64)[cell]]
|
|
grout = np.array([0.18, 0.18, 0.18])
|
|
return tile * m[..., None] + grout * (1 - m[..., None])
|
|
|
|
def hex_tiles(u, v):
|
|
d, cid = hex_cells(u, v, 7)
|
|
m = smoothstep(1.0, 0.9, d)
|
|
pal = np.array([[0.95, 0.93, 0.88], [0.25, 0.55, 0.60], [0.80, 0.55, 0.20]])
|
|
rng = np.random.default_rng(91)
|
|
tile = pal[rng.integers(0, 3, 1009)[cid]]
|
|
grout = np.array([0.15, 0.15, 0.16])
|
|
return tile * m[..., None] + grout * (1 - m[..., None])
|
|
|
|
|
|
def wood_planks(u, v):
|
|
n = 4 # planks across, running along v; staggered ends
|
|
x = u * n; col = np.floor(x); fx = x - col
|
|
rng = np.random.default_rng(101)
|
|
y = v * 2 + rng.random(n)[col.astype(int) % n] # each plank column has its own end offset
|
|
fy = y % 1
|
|
gap = 0.04
|
|
m = smoothstep(0, gap, fx) * smoothstep(0, gap, 1 - fx) * smoothstep(0, gap * 1.5, fy) * smoothstep(0, gap * 1.5, 1 - fy)
|
|
# grain: stretched noise along the plank, per plank phase
|
|
grain = fbm((u * 1.0 + rng.random(n)[col.astype(int) % n]) % 1, v, 6, 102, octaves=5)
|
|
rings = 0.5 + 0.5 * np.sin(2 * np.pi * (fx * 3 + grain * 2.5))
|
|
return norm(m * (0.75 + 0.25 * rings))
|
|
|
|
def basket_weave(u, v):
|
|
n = 4
|
|
x = u * n; y = v * n
|
|
cx = np.floor(x); cy = np.floor(y); fx = x % 1; fy = y % 1
|
|
horiz = (cx + cy) % 2 == 0
|
|
strips = 3 # strips per cell
|
|
along = np.where(horiz, fx, fy); across = np.where(horiz, fy, fx)
|
|
strip = (across * strips) % 1
|
|
body = np.sqrt(np.clip(1 - (2 * strip - 1) ** 2, 0, None)) # rounded strip
|
|
ends = smoothstep(0, 0.06, along) * smoothstep(0, 0.06, 1 - along)
|
|
return norm(0.35 + 0.65 * body * ends)
|
|
|
|
def chainmail(u, v):
|
|
n = 6
|
|
h = np.zeros_like(u)
|
|
for ox, oy in ((0.0, 0.0), (0.5, 0.5)):
|
|
x = (u * n + ox) % 1 - 0.5; y = (v * n + oy) % 1 - 0.5
|
|
r = np.sqrt(x * x + y * y)
|
|
ring = np.exp(-((r - 0.36) / 0.09) ** 2)
|
|
h = np.maximum(h, ring)
|
|
return norm(h)
|
|
|
|
def pyramids(u, v):
|
|
n = 8
|
|
x = np.abs((u * n) % 1 - 0.5); y = np.abs((v * n) % 1 - 0.5)
|
|
return norm(0.5 - np.maximum(x, y))
|
|
|
|
def waffle(u, v):
|
|
n = 6
|
|
x = (u * n) % 1; y = (v * n) % 1
|
|
gap = 0.12
|
|
m = smoothstep(0, gap, x) * smoothstep(0, gap, 1 - x) * smoothstep(0, gap, y) * smoothstep(0, gap, 1 - y)
|
|
return norm(m)
|
|
|
|
def bubbles(u, v):
|
|
rng = np.random.default_rng(111)
|
|
h = np.zeros_like(u)
|
|
for (px, py), r in zip(wrapped_points(7, 112, 0.9), rng.random(49) * 0.05 + 0.03):
|
|
dx = u - px; dx -= np.round(dx); dy = v - py; dy -= np.round(dy)
|
|
d2 = dx * dx + dy * dy
|
|
h = np.maximum(h, np.sqrt(np.clip(r * r - d2, 0, None)) / 0.08)
|
|
return norm(h)
|
|
|
|
def cracked_earth(u, v):
|
|
pts = wrapped_points(7, 121, 0.7)
|
|
d1, d2, idx = voronoi(u, v, pts)
|
|
crack = 1 - smoothstep(0.0, 0.03, d2 - d1)
|
|
plates = 0.8 + 0.2 * fbm(u, v, 24, 122)
|
|
curl = 1 - 0.35 * np.clip(1 - d1 / 0.12, 0, 1) # plates curl up at the edges
|
|
return norm(plates * (2 - curl) * (1 - 0.9 * crack))
|
|
|
|
def sand_ripples(u, v):
|
|
n = 8
|
|
wob = 0.06 * np.sin(2 * np.pi * u * 2) + 0.03 * fbm(u, v, 4, 131)
|
|
h = 0.5 + 0.5 * np.sin(2 * np.pi * (v * n + wob))
|
|
h = h ** 1.6 # sharp crests, soft troughs
|
|
return norm(h + 0.05 * fbm(u, v, 64, 132))
|
|
|
|
def bark(u, v):
|
|
grooves = fbm((u * 3) % 1, v, 4, 141, octaves=4) # the ridges below stretch it along v
|
|
ridges = np.abs(np.sin(2 * np.pi * (u * 9 + grooves * 1.5)))
|
|
return norm(ridges ** 0.7 * (0.7 + 0.3 * fbm(u, v, 12, 142)) + 0.1 * fbm(u, v, 48, 143))
|
|
|
|
def slate(u, v):
|
|
h = fbm(u, v, 3, 151, octaves=6, gain=0.55)
|
|
steps = np.floor(h * 6) / 6 + 0.4 * (h * 6 - np.floor(h * 6)) / 6 # cleaved layers
|
|
return norm(steps + 0.05 * fbm(u, v, 48, 152))
|
|
|
|
def triangles(u, v):
|
|
n = 6
|
|
x = u * n; y = v * n * np.sqrt(3) / 1.5 # rows of equilateral triangles
|
|
row = np.floor(y); fy = y - row
|
|
xs = x + 0.5 * (row % 2)
|
|
fx = xs % 1
|
|
up = fx < 1 - fy # which triangle of the rhombus
|
|
# distance to the nearest edge of the triangle, in either orientation
|
|
d_up = np.minimum(np.minimum(fy, fx - 0 * fy), (1 - fy - fx))
|
|
d_dn = np.minimum(np.minimum(1 - fy, 1 - fx), (fx + fy - 1))
|
|
d = np.where(up, d_up, d_dn)
|
|
return norm(smoothstep(0.0, 0.08, d))
|
|
|
|
def roof_tiles(u, v):
|
|
n = 6
|
|
h = np.zeros_like(u)
|
|
for row in range(-1, 2 * n + 1):
|
|
cy = row / (2 * n); ox = 0.5 if row % 2 else 0.0
|
|
x = (u * n + ox) % 1 - 0.5; y = v - cy; y -= np.round(y)
|
|
yy = y * n * 2 # 0 at the row's exposed edge, rising toward the covered end
|
|
inside = (yy >= -0.05) & (yy < 1.0)
|
|
arch = np.cos(x * np.pi) * 0.6 + 0.4
|
|
cand = np.where(inside, 0.3 + 0.7 * arch * (1 - 0.35 * yy), 0)
|
|
h = np.where(inside & (cand > 0), cand, h)
|
|
return norm(h)
|
|
|
|
def star_tiles(u, v):
|
|
# 8-point stars and crosses (the classic Islamic star-and-cross tiling)
|
|
n = 4
|
|
x = (u * n) % 1 - 0.5; y = (v * n) % 1 - 0.5
|
|
a = np.abs(x); b = np.abs(y)
|
|
star = np.maximum(np.maximum(a, b), (a + b) / np.sqrt(2) * 1.15)
|
|
m = smoothstep(0.42, 0.36, star)
|
|
# the crosses between the stars sit on the half-offset lattice
|
|
x2 = (u * n + 0.5) % 1 - 0.5; y2 = (v * n + 0.5) % 1 - 0.5
|
|
a2 = np.abs(x2); b2 = np.abs(y2)
|
|
cross = np.minimum(np.maximum(a2 / 0.12, b2 / 0.30), np.maximum(a2 / 0.30, b2 / 0.12))
|
|
m2 = smoothstep(1.0, 0.85, cross) * 0.8
|
|
return norm(np.maximum(m, m2))
|
|
|
|
def terrazzo(u, v):
|
|
base = np.array([0.82, 0.80, 0.76])
|
|
rgb = np.broadcast_to(base, u.shape + (3,)).copy() * (0.95 + 0.05 * fbm(u, v, 32, 161))[..., None]
|
|
rng = np.random.default_rng(162)
|
|
pal = np.array([[0.85, 0.30, 0.25], [0.20, 0.35, 0.55], [0.25, 0.25, 0.25], [0.95, 0.90, 0.80], [0.80, 0.60, 0.20]])
|
|
for (px, py), r, c, ang in zip(wrapped_points(12, 163, 1.0), rng.random(144) * 0.02 + 0.012, rng.integers(0, 5, 144), rng.random(144) * 3.14):
|
|
dx = u - px; dx -= np.round(dx); dy = v - py; dy -= np.round(dy)
|
|
ca, sa = np.cos(ang), np.sin(ang)
|
|
ex = (dx * ca - dy * sa) / (r * 1.4); ey = (dx * sa + dy * ca) / r
|
|
inside = (np.abs(ex) + np.abs(ey) * 0.7 + np.maximum(np.abs(ex), np.abs(ey)) * 0.5) < 1.0
|
|
rgb[inside] = pal[c] * 0.92 # chips sit a touch below the matrix: darker = lower
|
|
return rgb
|
|
|
|
def camouflage(u, v):
|
|
pal = np.array([[0.36, 0.42, 0.24], [0.55, 0.50, 0.32], [0.22, 0.26, 0.17], [0.60, 0.58, 0.45]])
|
|
a = fbm(u, v, 3, 171, octaves=4); b = fbm(u, v, 3, 172, octaves=4); c = fbm(u, v, 5, 173, octaves=3)
|
|
idx = (a > 0.55).astype(int) + 2 * (b > 0.5).astype(int)
|
|
idx = np.where(c > 0.72, 3, idx)
|
|
rgb = pal[idx].astype(float)
|
|
return rgb * (0.94 + 0.06 * fbm(u, v, 48, 174))[..., None]
|
|
|
|
def tartan(u, v):
|
|
n = 2
|
|
def stripes(t):
|
|
t = (t * n) % 1
|
|
band = np.zeros_like(t)
|
|
for a, w, val in ((0.0, 0.32, 1), (0.32, 0.06, 2), (0.38, 0.24, 0), (0.62, 0.06, 2), (0.68, 0.32, 1)):
|
|
band = np.where((t >= a) & (t < a + w), val, band)
|
|
return band
|
|
pal = np.array([[0.12, 0.25, 0.20], [0.55, 0.12, 0.14], [0.90, 0.80, 0.30]])
|
|
su = stripes(u).astype(int); sv = stripes(v).astype(int)
|
|
weave = ((np.floor(u * 400) + np.floor(v * 400)) % 2) == 0 # the two thread directions alternate
|
|
rgb = np.where(weave[..., None], pal[su], pal[sv]).astype(float)
|
|
return rgb * (0.9 + 0.1 * (0.5 + 0.5 * np.sin(2 * np.pi * (u + v) * 200)))[..., None]
|
|
|
|
GREY = {
|
|
'Diamond Plate': diamond_plate, 'Honeycomb': honeycomb, 'Fine Knurl': knurl, 'Scales': scales,
|
|
'Herringbone': herringbone, 'Cobblestone': cobblestone, 'Leather': leather, 'Carbon Fibre': carbon_fibre,
|
|
'Chevron': chevron, 'Ripples': ripples, 'Hammered': hammered, 'Perforated': perforated, 'Rope': rope,
|
|
'Stone Wall': stone_wall, 'Wood Planks': wood_planks, 'Basket Weave': basket_weave, 'Chainmail': chainmail,
|
|
'Pyramids': pyramids, 'Waffle': waffle, 'Bubbles': bubbles, 'Cracked Earth': cracked_earth,
|
|
'Sand Ripples': sand_ripples, 'Bark': bark, 'Slate': slate, 'Triangles': triangles, 'Roof Tiles': roof_tiles,
|
|
'Star Tiles': star_tiles,
|
|
}
|
|
COLOUR = {'Colour Bricks': colour_bricks, 'Mosaic Tiles': mosaic, 'Hex Tiles': hex_tiles, 'Terrazzo': terrazzo,
|
|
'Camouflage': camouflage, 'Tartan': tartan}
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument('--size', type=int, default=1024)
|
|
ap.add_argument('--out', default='resources/textures/displacement')
|
|
ap.add_argument('--sheet', default='')
|
|
args = ap.parse_args()
|
|
os.makedirs(args.out, exist_ok=True)
|
|
u, v = grid(args.size)
|
|
thumbs = []
|
|
for name, fn in GREY.items():
|
|
img = (np.clip(down(fn(u, v)), 0, 1) * 255 + 0.5).astype(np.uint8)
|
|
Image.fromarray(img, 'L').save(os.path.join(args.out, name + '.png'), optimize=True)
|
|
thumbs.append((name, np.stack([img] * 3, -1)))
|
|
print('wrote', name)
|
|
for name, fn in COLOUR.items():
|
|
rgb = fn(u, v)
|
|
rgb = np.stack([down(rgb[..., c]) for c in range(3)], -1)
|
|
img = (np.clip(rgb, 0, 1) * 255 + 0.5).astype(np.uint8)
|
|
Image.fromarray(img, 'RGB').save(os.path.join(args.out, name + '.png'), optimize=True)
|
|
thumbs.append((name, img))
|
|
print('wrote', name, '(colour)')
|
|
if args.sheet:
|
|
t = 256; cols = 7; rows = (len(thumbs) + cols - 1) // cols
|
|
sheet = Image.new('RGB', (cols * t, rows * t), (40, 40, 40))
|
|
for i, (name, img) in enumerate(thumbs):
|
|
im = Image.fromarray(img).resize((t, t), Image.LANCZOS)
|
|
sheet.paste(im, ((i % cols) * t, (i // cols) * t))
|
|
sheet.save(args.sheet); print('sheet', args.sheet)
|
|
|
|
if __name__ == '__main__':
|
|
main()
|