|
"""Generates psychedelic color textures in the spirit of Blender's magic texture shader using Python/Numpy |
|
|
|
https://github.com/cheind/magic-texture |
|
""" |
|
from typing import Tuple, Optional |
|
import numpy as np |
|
|
|
|
|
def coordinate_grid(shape: Tuple[int, int], dtype=np.float32): |
|
"""Returns a three-dimensional coordinate grid of given shape for use in `magic`.""" |
|
x = np.linspace(-1, 1, shape[1], endpoint=True, dtype=dtype) |
|
y = np.linspace(-1, 1, shape[0], endpoint=True, dtype=dtype) |
|
X, Y = np.meshgrid(x, y) |
|
XYZ = np.stack((X, Y, np.ones_like(X)), -1) |
|
return XYZ |
|
|
|
|
|
def random_transform(coords: np.ndarray, rng: np.random.Generator = None): |
|
"""Returns randomly transformed coordinates""" |
|
H, W = coords.shape[:2] |
|
rng = rng or np.random.default_rng() |
|
m = rng.uniform(-1.0, 1.0, size=(3, 3)).astype(coords.dtype) |
|
return (coords.reshape(-1, 3) @ m.T).reshape(H, W, 3) |
|
|
|
|
|
def magic( |
|
coords: np.ndarray, |
|
depth: Optional[int] = None, |
|
distortion: Optional[int] = None, |
|
rng: np.random.Generator = None, |
|
): |
|
"""Returns color magic color texture. |
|
|
|
The implementation is based on Blender's (https://www.blender.org/) magic |
|
texture shader. The following adaptions have been made: |
|
- we exchange the nested if-cascade by a probabilistic iterative approach |
|
|
|
Kwargs |
|
------ |
|
coords: HxWx3 array |
|
Coordinates transformed into colors by this method. See |
|
`magictex.coordinate_grid` to generate the default. |
|
depth: int (optional) |
|
Number of transformations applied. Higher numbers lead to more |
|
nested patterns. If not specified, randomly sampled. |
|
distortion: float (optional) |
|
Distortion of patterns. Larger values indicate more distortion, |
|
lower values tend to generate smoother patterns. If not specified, |
|
randomly sampled. |
|
rng: np.random.Generator |
|
Optional random generator to draw samples from. |
|
|
|
Returns |
|
------- |
|
colors: HxWx3 array |
|
Three channel color image in range [0,1] |
|
""" |
|
rng = rng or np.random.default_rng() |
|
if distortion is None: |
|
distortion = rng.uniform(1, 4) |
|
if depth is None: |
|
depth = rng.integers(1, 5) |
|
|
|
H, W = coords.shape[:2] |
|
XYZ = coords |
|
x = np.sin((XYZ[..., 0] + XYZ[..., 1] + XYZ[..., 2]) * distortion) |
|
y = np.cos((-XYZ[..., 0] + XYZ[..., 1] - XYZ[..., 2]) * distortion) |
|
z = -np.cos((-XYZ[..., 0] - XYZ[..., 1] + XYZ[..., 2]) * distortion) |
|
|
|
if depth > 0: |
|
x *= distortion |
|
y *= distortion |
|
z *= distortion |
|
y = -np.cos(x - y + z) |
|
y *= distortion |
|
|
|
xyz = [x, y, z] |
|
fns = [np.cos, np.sin] |
|
for _ in range(1, depth): |
|
axis = rng.choice(3) |
|
fn = fns[rng.choice(2)] |
|
signs = rng.binomial(n=1, p=0.5, size=4) * 2 - 1 |
|
|
|
xyz[axis] = signs[-1] * fn( |
|
signs[0] * xyz[0] + signs[1] * xyz[1] + signs[2] * xyz[2] |
|
) |
|
xyz[axis] *= distortion |
|
|
|
x, y, z = xyz |
|
x /= 2 * distortion |
|
y /= 2 * distortion |
|
z /= 2 * distortion |
|
c = 0.5 - np.stack((x, y, z), -1) |
|
np.clip(c, 0, 1.0) |
|
return c |