Skip to content

Instantly share code, notes, and snippets.

@graemephi
Created August 10, 2022 10:16
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save graemephi/3a90bc543aa974f7de04fa100c66bdc2 to your computer and use it in GitHub Desktop.
Save graemephi/3a90bc543aa974f7de04fa100c66bdc2 to your computer and use it in GitHub Desktop.
#%% <- 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment