Last active
April 2, 2025 14:38
-
-
Save ingoogni/8e1f74f8ea7250da778a202648e96e9b to your computer and use it in GitHub Desktop.
scripting in Nim with nimscripter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
compile | |
> nim c -d:release -d:danger renderer.nim | |
to render image from prompt: | |
> renderer sincos.nims | |
Render a function to a grey scale image, using nimscripter https://github.com/beef331/nimscripter for realtime scriptable applications. | |
Tonemapping can be done with GreyMap in the Pattern object. It uses linear interpolation to map input values to output values per section. | |
`GreyMap* = seq[tuple[greyVal: float, greyTarget: float]]` | |
Turbulence controls the distorion of the pattern. | |
`Turbulence* = tuple[strength: Vec2, lambda: float, omega: float, octaves: int]` | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import math, random | |
type | |
Vec2* = tuple[x, y: float] | |
# Permutation table for Perlin noise | |
var perm: array[512, int] | |
# Initialize the permutation table | |
proc initPermutation*(seed: int) = | |
var p: array[256, int] | |
for i in 0..<256: | |
p[i] = i | |
randomize(seed) | |
for i in countdown(255, 1): | |
let j = rand(i) | |
swap(p[i], p[j]) | |
for i in 0..<256: | |
perm[i] = p[i] | |
perm[i+256] = p[i] | |
# Gradient vectors for 2D Perlin noise | |
let grad2 = [ | |
(x: 1.0, y: 1.0), (x: -1.0, y: 1.0), | |
(x: 1.0, y: -1.0), (x: -1.0, y: -1.0), | |
(x: 1.0, y: 0.0), (x: -1.0, y: 0.0), | |
(x: 0.0, y: 1.0), (x: 0.0, y: -1.0) | |
] | |
proc fade(t: float): float = | |
# Perlin's improved smooth step function: 6t^5 - 15t^4 + 10t^3 | |
return t * t * t * (t * (t * 6.0 - 15.0) + 10.0) | |
proc fadeDeriv(t: float): float = | |
# Derivative of Perlin's fade function: 30t^4 - 60t^3 + 30t^2 | |
return 30.0 * t * t * (t * (t - 2.0) + 1.0) | |
proc dot(g: Vec2, x, y: float): float = | |
return g.x * x + g.y * y | |
proc gradIndex(hash: int): int = | |
return hash and 7 # Use lower 3 bits, modulo 8 | |
proc lerp(a, b, t: float): float = | |
return a + t * (b - a) | |
# 2D Perlin Noise function | |
proc noise*(x, y: float): float = | |
let | |
# Find unit square that contains point | |
X = int(floor(x)) and 255 | |
Y = int(floor(y)) and 255 | |
# Find relative x, y of point in square | |
xf = x - floor(x) | |
yf = y - floor(y) | |
# Fade curves | |
u = fade(xf) | |
v = fade(yf) | |
# Hash coords of the square's corners | |
A = perm[X] + Y | |
B = perm[X + 1] + Y | |
# Get gradients | |
g00 = grad2[gradIndex(perm[A])] | |
g01 = grad2[gradIndex(perm[A + 1])] | |
g10 = grad2[gradIndex(perm[B])] | |
g11 = grad2[gradIndex(perm[B + 1])] | |
# Dot products | |
n00 = dot(g00, xf, yf) | |
n01 = dot(g01, xf, yf - 1.0) | |
n10 = dot(g10, xf - 1.0, yf) | |
n11 = dot(g11, xf - 1.0, yf - 1.0) | |
# Bilinear interpolation | |
nx0 = lerp(n00, n10, u) | |
nx1 = lerp(n01, n11, u) | |
nxy = lerp(nx0, nx1, v) | |
return nxy | |
# dNoise2D - returns the gradient of the noise function in 2D | |
proc dNoise2D*(x, y: float): Vec2 = | |
# Find unit square that contains point | |
let | |
X = int(floor(x)) and 255 | |
Y = int(floor(y)) and 255 | |
# Find relative x, y of point in square | |
xf = x - floor(x) | |
yf = y - floor(y) | |
# Fade curves and their derivatives | |
u = fade(xf) | |
v = fade(yf) | |
du = fadeDeriv(xf) | |
dv = fadeDeriv(yf) | |
# Hash coordinates of the square's corners | |
A = perm[X] + Y | |
B = perm[X + 1] + Y | |
# Gradients for the corners | |
g00 = grad2[gradIndex(perm[A])] | |
g01 = grad2[gradIndex(perm[A+1])] | |
g10 = grad2[gradIndex(perm[B])] | |
g11 = grad2[gradIndex(perm[B+1])] | |
# Calculate the noise contribution from each corner | |
n00 = dot(g00, xf, yf) | |
n01 = dot(g01, xf, yf - 1.0) | |
n10 = dot(g10, xf - 1.0, yf) | |
n11 = dot(g11, xf - 1.0, yf - 1.0) | |
# For the partial derivative with respect to x: | |
# - Direct contribution from gradient at each corner | |
# - Contribution due to change in interpolation weight | |
gx00 = g00.x | |
gx01 = g01.x | |
gx10 = g10.x | |
gx11 = g11.x | |
nx0dx = lerp(gx00, gx10, u) + (n10 - n00) * du | |
nx1dx = lerp(gx01, gx11, u) + (n11 - n01) * du | |
nxydx = lerp(nx0dx, nx1dx, v) | |
gy00 = g00.y | |
gy01 = g01.y | |
gy10 = g10.y | |
gy11 = g11.y | |
ny0dy = lerp(gy00, gy10, u) | |
ny1dy = lerp(gy01, gy11, u) | |
nx0 = lerp(n00, n10, u) | |
nx1 = lerp(n01, n11, u) | |
nxydy = lerp(ny0dy, ny1dy, v) + (nx1 - nx0) * dv | |
return (x: nxydx, y: nxydy) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import math | |
type | |
Gamma* = enum | |
linear, srgb, bt709 | |
GammaFunction = proc(val: float): float | |
PGM* = object | |
path*: string | |
normalize*: bool = true | |
bit16*: bool = false | |
brightness*: float = 0.0 | |
contrast*: float = 0.0 | |
midpoint*: float = 0.5 | |
gamma*: Gamma = linear | |
proc initPGM*(path: string): PGM = | |
result = PGM( | |
path: path, | |
normalize: true, | |
bit16: false, | |
brightness: 0.0, | |
contrast: 0.0, | |
midpoint: 0.5, | |
gamma: linear | |
) | |
proc brightnessContrast*( | |
value: float, | |
brightness: float, | |
contrast: float, | |
midpoint: float = 0.5 | |
): float = | |
## Brightness and contrast with configurable midpoint | |
## Parameters: | |
## value: Input value in range [0.0, 1.0] | |
## brightness: Brightness adjustment [-1.0, 1.0] | |
## contrast: Contrast adjustment [-1.0, 1.0] | |
## midpoint: The pivot point for contrast adjustment [0.0, 1.0] | |
## Returns: Adjusted value clamped to [0.0, 1.0] | |
var pixelValue = value + brightness | |
let contrastFactor = tan((contrast + 1) * PI/4) | |
pixelValue = midpoint + contrastFactor * (pixelValue - midpoint) | |
return max(0.0, min(1.0, pixelValue)) | |
proc linearGamma(val: float): float = #silly! Compiler fodder? | |
return val | |
proc linearToBT709*(val: float): float = | |
## Converts a linear value to BT.709 gamma space | |
## Input and output are in range [0.0, 1.0] | |
if val >= 0.018: | |
return 1.099 * pow(val, 0.45) - 0.099 | |
else: | |
return 4.5 * val | |
proc bt709ToLinear*(val: float): float = | |
## Converts a BT.709 gamma value back to linear space | |
## Input and output are in range [0.0, 1.0] | |
if val >= 0.081: | |
return pow((val + 0.099) / 1.099, 1.0/0.45) | |
else: | |
return val / 4.5 | |
proc linearToSRGB*(val: float): float = | |
## Converts a linear grayscale value to sRGB space | |
if val > 0.0031308: | |
return 1.055 * pow(val, 1.0/2.4) - 0.055 | |
else: | |
return 12.92 * val | |
proc sRGBToLinear*(val: float): float = | |
## Converts a grayscale SRGB gamma value back to linear space | |
if val > 0.04045: | |
return pow((val + 0.055) / 1.055, 2.4) | |
else: | |
return val / 12.92 | |
# PGM output function | |
proc writePGM*( | |
pgm: PGM, | |
data: seq[seq[float]] | |
) = | |
## Writes a 2D array of float values to a binary PGM file (P5 format) | |
## Parameters: | |
## filename: Output file path | |
## data: 2D sequence of float values | |
## normalize: If true, values will be scaled to 0-1 range | |
## bit16: If true, outputs 16-bit PGM, otherwise 8-bit | |
## gamma: linear, srgb, bt709 | |
let height = data.len | |
if height == 0: | |
echo "Error: Empty data" | |
return | |
let width = data[0].len | |
# Find min/max for normalization if needed | |
var minVal = float.high | |
var maxVal = float.low | |
if pgm.normalize: | |
for y in 0..<height: | |
for x in 0..<width: | |
minVal = min(minVal, data[y][x]) | |
maxVal = max(maxVal, data[y][x]) | |
else: | |
minVal = 0.0 | |
maxVal = 1.0 | |
let range = maxVal - minVal | |
let maxPixelValue = if pgm.bit16: 65535 else: 255 | |
var gammaTxt: string = "" | |
var applyGamma: GammaFunction | |
if pgm.gamma == srgb: | |
applyGamma = linearToSRGB | |
gammaTxT = "srgb" | |
elif pgm.gamma == bt709: | |
applyGamma = linearToBT709 | |
gammaTxt = "BT.709" | |
else: | |
applyGamma = linearGamma | |
gammaTxt = "linear" | |
var f = open(pgm.path, fmWrite) | |
defer: f.close() | |
f.write("P5\n") | |
f.write("#Gamma = " & gammaTxt & "\n") | |
f.write($width & " " & $height & "\n") | |
f.write($maxPixelValue & "\n") | |
if pgm.bit16: | |
var pixelData = newSeq[uint16](width * height) | |
var i = 0 | |
for y in 0..<height: | |
for x in 0..<width: | |
var normalizedVal = | |
if range > 0.0: (data[y][x] - minVal) / range | |
else: 0.0 | |
normalizedVal = normalizedVal.brightnessContrast(pgm.brightness, | |
pgm.contrast, pgm.midpoint) | |
normalizedVal = normalizedVal.applyGamma | |
let pixelVal = uint16(normalizedVal * float(maxPixelValue)) | |
pixelData[i] = pixelVal | |
i += 1 | |
# Write binary data as big-endian bytes | |
var byteData = newSeq[uint8](pixelData.len * 2) | |
for i in 0..<pixelData.len: | |
byteData[i * 2] = uint8(pixelData[i] shr 8) # High byte | |
byteData[i * 2 + 1] = uint8(pixelData[i] and 0xFF) # Low byte | |
discard f.writeBytes(byteData, 0, byteData.len) | |
else: | |
# 8-bit version | |
var pixelData = newSeq[uint8](width * height) | |
var i = 0 | |
for y in 0..<height: | |
for x in 0..<width: | |
var normalizedVal = | |
if range > 0.0: (data[y][x] - minVal) / range | |
else: 0.0 | |
normalizedVal = normalizedVal.brightnessContrast(pgm.brightness, | |
pgm.contrast, pgm.midpoint) | |
normalizedVal = normalizedVal.applyGamma() | |
pixelData[i] = uint8(normalizedVal * float(maxPixelValue)) | |
i += 1 | |
discard f.writeBytes(pixelData, 0, pixelData.len) | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import std/[os, parseopt] | |
import pgm, dnoise2d | |
import nimscripter | |
type | |
Vec2* = tuple[x, y: float] #should use vmath! | |
GreyMap* = seq[tuple[greyVal: float, greyTarget: float]] | |
Turbulence* = tuple[strength: Vec2, lambda: float, omega: float, octaves: int] | |
Pattern* = object | |
funk*: proc(x, y: float): float | |
greymap*: GreyMap | |
turbulence*: Turbulence | |
initPermutation(7) #for DNoise | |
proc lmap*(val, minin, maxin, minout, maxout: float): float {.inline.} = | |
((val - minin) / (maxin - minin)) * (maxout - minout) + minout | |
proc greyMap*(gmap: GreyMap, value: float): float = | |
result = 0.0 | |
if value < gmap[0].greyVal: | |
result = gmap[0].greyTarget | |
elif value > gmap[gmap.len - 1].greyVal: | |
result = gmap[gmap.len - 1].greyTarget | |
else: | |
for i in 0..<gmap.len - 1: | |
if value >= gmap[i].greyVal and value <= gmap[i+1].greyVal: | |
result = lmap( | |
value, | |
gmap[i].greyVal, | |
gmap[i+1].greyVal, | |
gmap[i].greyTarget, | |
gmap[i+1].greyTarget | |
) | |
break | |
proc turbulence*(p: Vec2, turb: Turbulence): Vec2 = | |
result = p | |
var | |
freq = 0.5 | |
amp = 0.5 | |
for i in 0..<turb.octaves: | |
# Point-dependent noise vector | |
let | |
p2 = (x: result.x * freq, y: result.y * freq) | |
noise = dNoise2D(p2.x, p2.y) | |
result.x += noise.x * amp * turb.strength.x | |
result.y += noise.y * amp * turb.strength.y | |
# Scale for next octave | |
freq *= turb.lambda | |
amp *= turb.omega / 2 | |
template render*( | |
patt: Pattern, | |
width, height: int, | |
viewportWidth, viewportHeight: float, | |
viewportX: float = 0.0, | |
viewportY: float = 0.0 | |
): seq[seq[float]] = | |
var minVal = float.high | |
var maxVal = float.low | |
var pixel = newSeq[seq[float]](height) | |
let | |
aspectRatio = width.float / height.float | |
actualViewportHeight = if viewportHeight <= 0.0: | |
viewportWidth / aspectRatio | |
else: | |
viewportHeight | |
for y in 0..<height: | |
pixel[y] = newSeq[float](width) | |
let yf = viewportY + (y.float / height.float) * actualViewportHeight | |
for x in 0..<width: | |
let | |
xf = viewportX + (x.float / width.float) * viewportWidth | |
point: Vec2 = (x: xf, y: yf) | |
warped = if patt.turbulence.strength.x * | |
patt.turbulence.strength.y != 0.0: | |
turbulence(point, patt.turbulence) | |
else: | |
point | |
let val = patt.funk(warped.y, warped.x) | |
pixel[y][x] = val | |
minVal = min(minVal, val) | |
maxVal = max(maxVal, val) | |
echo "minVal: ", minVal,"\n","maxVal: ", maxVal | |
let valRange = maxVal - minVal | |
var normalizedVal: float | |
if patt.greymap.len > 0: | |
for y in 0..<height: | |
for x in 0..<width: | |
normalizedVal = | |
if valRange > 0.0: (pixel[y][x] - minVal) / valRange | |
else: 0.0 | |
pixel[y][x] = greymap(patt.greymap, normalizedVal) | |
pixel | |
exportTo(renderImpl, | |
Vec2, GreyMap, Turbulence, Pattern, Gamma, PGM, | |
initPGM, writePGM, turbulence, greyMap, lmap, render | |
) | |
proc main() = | |
let cp = commandLineParams() | |
if cp == @[]: | |
echo "no arguments given" | |
quit(3) | |
let co = quoteShellCommand(cp) | |
var positionalArgs = newSeq[string]() | |
var optparser = initOptParser(co) | |
for kind, key, val in optparser.getopt(): | |
case kind | |
of cmdArgument: | |
positionalArgs.add(key) | |
else: | |
continue | |
if positionalArgs[0] == "": | |
echo "no file given" | |
quit(3) | |
elif not fileExists(positionalArgs[0]): | |
echo "file does not exist" | |
quit(3) | |
let intr = loadScript( | |
NimScriptPath(positionalArgs[0]), | |
implNimScriptModule(renderImpl) | |
) | |
main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import math | |
proc shuheiKawachi(a: float, b: float): proc(x: float, y: float): float = | |
proc shuheiKawachi(x: float, y: float): float = | |
(cos(x) * cos(y) + cos((sqrt(a) * x - y) / b) * | |
cos((x + sqrt(a) * y) / b) + cos((sqrt(a) * x + y) / b) * | |
cos((x - sqrt(a) * y) / b)) / 6 + 0.5 | |
return shuheiKawachi | |
proc absShuheiKawachi(a: float, b: float): proc(x: float, y: float): float = | |
proc absShuheiKawachi(x: float, y: float): float = | |
abs(cos(x) * cos(y) + cos((sqrt(a) * x - y) / b)* | |
cos((x + sqrt(a) * y) / b) + cos((sqrt(a) * x + y) / b) * | |
cos((x - sqrt(a) * y) / b)) / 3 | |
return absShuheiKawachi | |
proc main() = | |
var img = initPGM("shuheiKawachi.pgm") | |
img.gamma = srgb | |
let | |
#sK = shuheiKawachi(TAU, PI) | |
sK = absShuheiKawachi(TAU, PI) | |
gm = Pattern( | |
funk: sK, | |
greymap: @[(0.0,0.0),(1.0,1.0)], | |
turbulence: ( | |
strength: (x: 4.6, y: 0.2), | |
lambda: 0.5, | |
omega: 0.3, | |
octaves: 8 | |
) | |
) | |
width = 640 | |
height = 480 | |
viewportWidth = 40.0 | |
viewportHeight = 0.0 #0.0 means auto calculate based on aspect ratio, width, height | |
#viewportHeight = viewportWidth * (height / width) # Maintain aspect ratio | |
grid = gm.render(width, height, viewportWidth, viewportHeight) | |
img.writePGM(grid) | |
when isMainModule: | |
main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import math | |
proc sincos(x, y: float): float = | |
return (1 + (sin(x) * cos(y))) * 0.5 | |
proc main() = | |
var img = initPGM("sincos.pgm") | |
img.gamma = srgb | |
let | |
gm = Pattern( | |
funk: sincos, | |
greymap: @[(0.0,0.0),(0.5,0.2),(0.8,0.7),(1.0,0.0)], | |
turbulence: ( | |
strength: (x: 1.6, y: 2.2), | |
lambda: 2.5, | |
omega: 0.7, | |
octaves: 6 | |
) | |
) | |
width = 640 | |
height = 480 | |
viewportWidth = 30.0 | |
viewportHeight = 0.0 #0.0 means auto calculate based on aspect ratio, width, height | |
#viewportHeight = viewportWidth * (height / width) # Maintain aspect ratio | |
grid = gm.render(width, height, viewportWidth, viewportHeight) | |
img.writePGM(grid) | |
when isMainModule: | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment