|
#%% <- this is vscode stuff, this will run on the command line but plots might come out weird |
|
import numpy as np |
|
u32 = np.uint32 |
|
i32 = np.int32 |
|
|
|
def golden_ratio_sequence(i: u32) -> u32: |
|
return u32(i) * u32(2654435769) # 0.614... in 0.32 fixed point |
|
|
|
def reverse_bits32(x: u32) -> u32: |
|
x = u32(x) |
|
x = ((x >> u32(1)) & u32(0x55555555)) | ((x & u32(0x55555555)) << u32(1)) |
|
x = ((x >> u32(2)) & u32(0x33333333)) | ((x & u32(0x33333333)) << u32(2)) |
|
x = ((x >> u32(4)) & u32(0x0F0F0F0F)) | ((x & u32(0x0F0F0F0F)) << u32(4)) |
|
x = ((x >> u32(8)) & u32(0x00FF00FF)) | ((x & u32(0x00FF00FF)) << u32(8)) |
|
x = ( x >> u32(16) ) | ( x << u32(16)) |
|
return x |
|
|
|
def nested_uniform_scramble(x: u32) -> u32: |
|
x = reverse_bits32(x) |
|
x ^= x * u32(0x6c50b47c) |
|
x ^= x * u32(0xb82f1e52) |
|
x ^= x * u32(0xc7afe638) |
|
x ^= x * u32(0x8d22f6e6) |
|
return reverse_bits32(x) |
|
|
|
def okay_blue_noise(i: u32) -> u32: |
|
return golden_ratio_sequence(nested_uniform_scramble(i)) |
|
|
|
#%% |
|
import matplotlib.pyplot as plt |
|
plt.rcParams['figure.figsize'] = (9,2) |
|
|
|
def spectrum(seq): |
|
S = np.fft.rfft(2 * seq - 1.0) |
|
return np.abs(S) / len(seq) |
|
|
|
def plots(name: str, seq_u32: u32): |
|
seq = seq_u32 * np.ldexp(1, -32) |
|
|
|
figure, (histogram_axis, spectrum_axis) = plt.subplots(1, 2) |
|
|
|
histogram_axis.hist(seq, 128) |
|
histogram_axis.set_xlabel(f"{name} histogram: {len(seq)} points") |
|
|
|
dft = spectrum(seq) |
|
spectrum_axis.plot(dft) |
|
spectrum_axis.set_xlabel(f"{name} spectrum: {len(seq)} points") |
|
|
|
n = 3333 |
|
|
|
#%% |
|
from numpy.random import default_rng |
|
rng = default_rng() |
|
plots("PRNG", rng.integers(0, 0x1_0000_0000, size=n)) |
|
|
|
#%% |
|
i = np.arange(n) |
|
plots("golden ratio sequence", golden_ratio_sequence(i)) |
|
|
|
#%% |
|
plots("nested uniform scramble-shuffled grs", okay_blue_noise(i)) |
|
|
|
#%% |
|
def xorshift(x): |
|
x = u32(x) |
|
x ^= x << u32(13) |
|
x ^= x >> u32(17) |
|
x ^= x << u32(5) |
|
return x |
|
|
|
i = np.arange(16) |
|
print(i) |
|
print(xorshift(i) & 15) |
|
|
|
#%% |
|
print(xorshift(i + 64 + 16) & 15) |
|
|
|
#%% |
|
def xorshift_star(x): |
|
x = u32(x) |
|
x ^= x << u32(13) |
|
x ^= x >> u32(17) |
|
x ^= x << u32(5) |
|
x *= u32(0x9e02ad0d) |
|
return x |
|
|
|
print(xorshift_star(i + 64 + 16) & 15) |
|
|
|
#%% |
|
print(len(np.unique(xorshift_star(np.arange(0xffff)) & 0xffff)), 0xffff) |
|
print(len(np.unique(xorshift_star(np.arange(0x1ffff)) & 0x1ffff)), 0x1ffff) |
|
|
|
#%% |
|
def all_ones_below_high_bit(x: u32) -> u32: |
|
x = u32(x) |
|
x |= (x >> u32(16)) |
|
x |= (x >> u32(8)) |
|
x |= (x >> u32(4)) |
|
x |= (x >> u32(2)) |
|
x |= (x >> u32(1)) |
|
# note this last shift: we don't include the high bit |
|
return x >> u32(1) |
|
|
|
def unfolded_masked_xorshift(x: u32, cap_mask: u32) -> u32: |
|
mask = all_ones_below_high_bit(x & cap_mask) |
|
upper = x & ~mask |
|
lower = xorshift_star(x) & mask |
|
result = upper + lower |
|
return result |
|
|
|
def masked_xorshift(x: u32, bits: u32 = 8) -> u32: |
|
# all ones if (x & 0x100) == 0x100, all zeros otherwise |
|
sign_mask = i32(x << u32(31 - bits)) >> i32(31) |
|
sign_mask = u32(sign_mask) |
|
cap_mask = u32((1 << bits) - 1) |
|
return unfolded_masked_xorshift(x ^ sign_mask, cap_mask) ^ sign_mask |
|
|
|
i = np.arange(1024) |
|
plt.plot(i - masked_xorshift(i)) |
|
|
|
#%% |
|
def white_shuffle(i: u32) -> u32: |
|
s = i |
|
s = masked_xorshift(s) |
|
s = nested_uniform_scramble(s) |
|
return s |
|
|
|
def white(i: u32) -> u32: |
|
return golden_ratio_sequence(white_shuffle(i)) |
|
|
|
i = np.arange(3333) |
|
plots("white", white(i)) |
|
|
|
#%% |
|
plt.rcParams['image.cmap'] = 'gray' |
|
plt.rcParams['image.interpolation'] = 'none' |
|
px = 1/plt.rcParams['figure.dpi'] |
|
n = 512; i = np.arange(n*n) |
|
f, ax = plt.subplots(figsize=(n*px, n*px)); ax.axis('off') |
|
ax.imshow(white(i).reshape((n,n)) * np.ldexp(1, -32)) |
|
|
|
#%% |
|
def white_shuffle(i: u32) -> u32: |
|
s = i |
|
s = nested_uniform_scramble(s) |
|
s = masked_xorshift(s) |
|
s = nested_uniform_scramble(s) |
|
return s |
|
|
|
def white(i: u32) -> u32: |
|
return golden_ratio_sequence(white_shuffle(i)) |
|
|
|
f, ax = plt.subplots(figsize=(n*px, n*px)); ax.axis('off') |
|
ax.imshow(white(i).reshape((n,n)) * np.ldexp(1, -32)) |
|
|
|
#%% |
|
def kronecker_sequence(i: u32, a: u32) -> u32: |
|
return u32(i) * u32(a) |
|
|
|
def blue(i: u32) -> u32: |
|
s = white_shuffle(i >> 1) |
|
b = kronecker_sequence(s, 2654435770) # 0.31 fixed point golden ratio |
|
odd = u32(i & 1) |
|
b ^= (odd ^ u32(1)) - u32(1) # negate on odd indices |
|
b += odd |
|
b ^= b >> u32(6) # gray code round |
|
return b |
|
|
|
i = np.arange(3333) |
|
plots("blue", blue(i)) |
|
|
|
#%% |
|
def spectrum_2d(img): |
|
dft = np.abs(np.fft.fftshift(np.fft.fft2(img))) |
|
dft /= dft.shape[0] |
|
return np.clip(dft, 0, 1) |
|
|
|
def lookit(image): |
|
f, ax = plt.subplots(figsize=(2*image.shape[0]*px, image.shape[1]*px)) |
|
ax.axis('off') |
|
ax.imshow(np.hstack((image, spectrum_2d(image)))) |
|
|
|
n = 384 |
|
i = np.arange(n*n).reshape((n,n)) |
|
noise = blue(i) * np.ldexp(1, -32) |
|
lookit(noise) |
|
|
|
#%% |
|
def spiral(n: u32, lo: float, hi: float): |
|
x, y = np.meshgrid(np.linspace(lo, hi, n), np.linspace(lo, hi, n)) |
|
# two sqrts: one for the distance, two to adjust for spirals closer to 0 being tighter (think random sampling in a circle) |
|
xy = np.round(np.sqrt(np.sqrt(x*x + y*y)) * np.sqrt(n*n + n*n)) |
|
# rescaled to a reasonable range that makes debugging possible without a third eye |
|
angles = (np.arctan2(y, x) + np.pi) / (2 * np.pi) |
|
# sort by magnitudes then angles (I don't know why lexsort is little endian), then invert the sort |
|
spiral = np.lexsort((angles.flatten(), xy.flatten())).argsort() |
|
return spiral.reshape((n,n)) |
|
|
|
s = spiral(n, -1.0, 1.0) |
|
noise = blue(s) * np.ldexp(1, -32) |
|
lookit(noise) |
|
|
|
#%% |
|
s = spiral(n, 2.0, 4.0) |
|
noise = blue(s) * np.ldexp(1, -32) |
|
lookit(noise) |
|
|
|
#%% |
|
spiral(8, 2.0, 4.0) |
|
|
|
#%% |
|
def left_shift_2(x: u32) -> u32: |
|
x = (x ^ (x << 16)) & 0x0000ffff |
|
x = (x ^ (x << 8)) & 0x00ff00ff |
|
x = (x ^ (x << 4)) & 0x0f0f0f0f |
|
x = (x ^ (x << 2)) & 0x33333333 |
|
x = (x ^ (x << 1)) & 0x55555555 |
|
return u32(x) |
|
|
|
def z_order(x: u32, y: u32) -> u32: |
|
return left_shift_2(x) + (left_shift_2(y) << u32(1)) |
|
|
|
tile_bits = 6 |
|
tile_n = 1 << tile_bits |
|
tile_mask = tile_n - 1 |
|
tile_path = spiral(tile_n, 2.0, 4.0) |
|
|
|
def blue_2d(x: u32, y: u32) -> u32: |
|
x_lo = x & tile_mask |
|
y_lo = y & tile_mask |
|
x_hi = x >> tile_bits |
|
y_hi = y >> tile_bits |
|
tile = z_order(x_hi, y_hi) |
|
i = (tile << u32(2*tile_bits)) + tile_path[y_lo, x_lo] |
|
return blue(i) |
|
|
|
x, y = np.meshgrid(np.arange(n), np.arange(n)) |
|
noise = blue_2d(x, y) * np.ldexp(1, -32) |
|
lookit(noise) |
|
|
|
#%% |
|
plt.hist(noise.flatten(), 384) |
|
pass |
|
|
|
#%% |
|
s = (spiral(384, -0.08, 0.08) & 7) / 8 |
|
lookit(s) |
|
|
|
#%% |
|
s = u32(spiral(384, -0.08, 0.08)) |
|
s = masked_xorshift(s, 2) |
|
s ^= s >> 1 |
|
s = reverse_bits32(s) |
|
s = nested_uniform_scramble(s) |
|
lookit(s / s.max()) |
|
|
|
#%% |
|
f32 = np.float32 |
|
|
|
def uniform_to_triangle_dist(x): |
|
# From demofox @ https://www.shadertoy.com/view/4t2SDh |
|
x = (x + 0.5) % 1 |
|
orig = x * 2.0 - 1.0 |
|
nz = orig != 0 |
|
x[~nz] = -1 |
|
x[nz] = orig[nz] / np.sqrt(np.abs(orig[nz])) |
|
x = x - np.sign(orig) + 0.5 |
|
x = (x - 0.5) * 0.5 + 0.5 |
|
return x |
|
|
|
def quantize_dithered(img: f32, noise: f32, dither_bits: int, output_bits: int) -> f32: |
|
output_scale = 2**output_bits |
|
dither_scale = 2**dither_bits / output_scale |
|
img_plus_noise = img + dither_scale * (noise - 0.5) |
|
quantized = np.round(img_plus_noise * (output_scale - 1)) / (output_scale - 1) |
|
return np.clip(quantized, 0, 1) |
|
|
|
def dither_gradient(dither_bits, output_bits): |
|
n = 512 |
|
m = 100 |
|
gradient = np.tile(np.linspace(0,1,n), (m*5, 2)) |
|
|
|
vac_texture = np.concatenate((plt.imread("HDR_L_0.png").T, plt.imread("HDR_L_0.png").T)).T |
|
|
|
x, y = np.meshgrid(np.arange(n), np.arange(m)) |
|
|
|
random = rng.random((m,n)) |
|
lds_white = white(y*m + x) * np.ldexp(1, -32) |
|
lds_blue = blue_2d(x, y) * np.ldexp(1, -32) |
|
vac_blue = vac_texture[:m,:n] |
|
|
|
random_tri = 0.5*(rng.random((m,n)) + rng.random((m,n))) |
|
lds_white_tri = uniform_to_triangle_dist(lds_white) |
|
lds_blue_tri = uniform_to_triangle_dist(lds_blue) |
|
vac_blue_tri = uniform_to_triangle_dist(vac_texture[:m,:n]) |
|
|
|
noise = np.zeros((m*5, n*2)) |
|
noise[0*m:1*m//2, 0:2*n] = 0.5 |
|
noise[1*m:2*m, 0:1*n] = random |
|
noise[1*m:2*m, n:2*n] = random_tri |
|
noise[2*m:3*m, 0:1*n] = lds_white |
|
noise[2*m:3*m, n:2*n] = lds_white_tri |
|
noise[3*m:4*m, 0:1*n] = lds_blue |
|
noise[3*m:4*m, n:2*n] = lds_blue_tri |
|
noise[4*m:5*m, 0:1*n] = vac_blue |
|
noise[4*m:5*m, n:2*n] = vac_blue_tri |
|
|
|
image = quantize_dithered(gradient, noise, dither_bits, output_bits) |
|
image[1*m//2:1*m, 0:2*n] = gradient[1*m//2:1*m, 0:2*n] |
|
|
|
f, ax = plt.subplots(figsize=(image.shape[1]*px, image.shape[0]*px)); ax.axis('off') |
|
ax.imshow(image) |
|
dither_gradient(2,4) |
|
|
|
# %% |
|
def red(i: u32) -> u32: |
|
s = white_shuffle(i >> 1) |
|
r = kronecker_sequence(s, 2654435770) # 0.31 fixed point golden ratio |
|
r[(i & 1) == 1] ^= r[(i & 1) == 1] >> u32(6) |
|
return r |
|
|
|
def red_2d(x: u32, y: u32) -> u32: |
|
i = left_shift_2(x) + (left_shift_2(y) >> u32(1)) |
|
return red(i) |
|
|
|
noise = red_2d(x, y) * np.ldexp(1, -32) |
|
lookit(noise) |
|
|
|
#%% |
|
plt.hist(noise.flatten(), 384) |
|
pass |
|
|
|
#%% |
|
n = 64 |
|
|
|
border = 2 |
|
gap = border + 8 |
|
rows = 4 |
|
cols = 3 |
|
|
|
img = np.zeros((rows*(border-1) + n*rows + (rows-1)*gap, cols*(border-1) + 1 + n*cols + (cols-1)*gap)) |
|
mask = np.zeros_like(img) |
|
x = border |
|
y = border |
|
for i in range(rows): |
|
offset = rng.integers(0x1000)*n*n |
|
b = border |
|
x = b |
|
img[y-b:y+n+b, x-b:x+n+b] = 0.5 |
|
img[y:y+n, x:x+n] = f32(rng.random(size=(n,n))) < 0.5 |
|
mask[y-b:y+n+b, x-b:x+n+b] = 1 |
|
x += n + gap |
|
img[y-b:y+n+b, x-b:x+n+b] = 0.5 |
|
img[y:y+n, x:x+n] = (white(offset +np.arange(n*n)).reshape((n,n)) * np.ldexp(1, -32)) < 0.5 |
|
mask[y-b:y+n+b, x-b:x+n+b] = 1 |
|
x += n + gap |
|
img[y-b:y+n+b, x-b:x+n+b] = 0.5 |
|
bx, by = np.meshgrid(offset + np.arange(n), offset + np.arange(n)) |
|
img[y:y+n, x:x+n] = (blue_2d(bx, by) * np.ldexp(1, -32)) < 0.5 |
|
mask[y-b:y+n+b, x-b:x+n+b] = 1 |
|
y += n + gap |
|
|
|
img_rgba = np.zeros(img.shape + (4,)) |
|
img_rgba[...,0] = img |
|
img_rgba[...,1] = img |
|
img_rgba[...,2] = img |
|
img_rgba[...,3] = mask |
|
|
|
def scale_image(img, scale): |
|
return np.repeat(np.repeat(img, scale, axis=0), scale, axis=1) |
|
|
|
img_rgba = scale_image(img_rgba, 3) |
|
|
|
f, ax = plt.subplots(figsize=(img_rgba.shape[0]*px, img_rgba.shape[1]*px)); ax.axis('off') |
|
ax.imshow(img_rgba) |