#==========================================
#水彩画フィルター
#
# 参考文献: Montesdeoca et al. (2017) "Art-directed Watercolor Render"
#
#[アルゴリズム構成]
# 1. Kuwahara Filter ... 画像のノイズを抑え、絵画的な平滑化を行う
# 2. Domain Warping .... 紙の繊維(ノイズ)に沿って座標を歪ませ、滲みを表現
# 3. Beer-Lambert ...... 顔料の厚みに基づく光の吸収・透過(透明感)を計算
# 4. Paper Lighting .... 紙の凹凸(法線マップ)と表面光沢を合成
# ==========================================
#--- 1. 絵具と筆の設定 (Paint Parameters) ---
#筆の大きさ (値を上げると細かいディテールが消え、油絵風になる)
@param_i32 radius(SLIDER, label="[筆] ブラシサイズ", min=2, max=8, init=5)
#滲みの強さ (紙の繊維に沿ったインクの拡散具合)
@param_f32 bleed_amt(SLIDER, label="[筆] 滲み強度", min=0.0, max=5.0, init=1.15)
# 顔料の濃度 (低い=透明水彩、高い=ポスターカラー)
@param_f32 pigment_density(SLIDER, label="[絵具] 顔料濃度", min=0.5, max=5.0, init=1.0)
# 粒状化 (紙の凹みに顔料が溜まるザラザラ感)
@param_f32 granulation(SLIDER, label="[絵具] 粒状感 (Granulation)", min=0.0, max=1.0, init=0.2)
# 水の量 (値を上げると色が薄まり、紙の白地が透ける)
@param_f32 water_amount(SLIDER, label="[絵具] 水の希釈", min=0.0, max=1.0, init=0.25)
#--- 2. 紙と光の設定 (Paper Parameters) ---
# 紙の目の細かさ (高いほど高級な細目画用紙になる)
@param_f32 paper_scale(SLIDER, label="[紙] 繊維スケール", min=1.0, max=10.0, init=8.0)
# 紙の凹凸の深さ (陰影の強さ)
@param_f32 relief_strength(SLIDER, label="[紙] 凹凸の深さ", min=0.0, max=5.0, init=1.0)
#表面の光沢 (斜めから見た時の反射光)
@param_f32 paper_sheen(SLIDER, label="[紙] 表面光沢 (Sheen)", min=0.0, max=1.0, init=0.6)
# 光源の方向 (左右)
@param_f32 light_dir_x(SLIDER, label="[環境] 光源方向 X", min=-1.0, max=1.0, init=-0.14)
#--- 3. ノイズ生成関数---
fn hash12 |p:f32v2| {
let d = p.x * 12.9898 + p.y * 78.233
fract(sin(d) * 43758.5453)
}
fn value_noise |uv:f32v2| {
let ix = floor(uv.x)
let iy = floor(uv.y)
let fx = fract(uv.x)
let fy = fract(uv.y)
let i = [ix, iy]
let a = hash12(i)
let b = hash12(i + [1.0, 0.0])
let c = hash12(i + [0.0, 1.0])
let d = hash12(i + [1.0, 1.0])
let ux = fx*fx*(3.0-2.0*fx)
let uy = fy*fy*(3.0-2.0*fy)
mix(mix(a, b, ux), mix(c, d, ux), uy)
}
fn rotate |p:f32v2, angle:f32| {
let c = cos(angle)
let s = sin(angle)
[p.x*c - p.y*s, p.x*s + p.y*c]
}
# 紙の繊維のための FBM (Fractal Brownian Motion)
fn fbm_paper |uv:f32v2| {
let v0 = 0.0
let amp0 = 0.5
let p0 = uv
let v1 = v0 + value_noise(p0) * amp0
let p1 = rotate(p0, 1.0) * 2.0
let amp1 = amp0 * 0.5
let v2 = v1 + value_noise(p1) * amp1
let p2 = rotate(p1, 1.0) * 2.0
let amp2 = amp1 * 0.5
let v3 = v2 + value_noise(p2) * amp2
v3 / 0.875
}
# --- 4. 色空間の変換 (Color Space: Oklab) ---
fn rgb_to_oklab |c:f32v3| {
let r = c.x
let g = c.y
let b = c.z
let l_t = 0.4122*r + 0.5363*g + 0.0514*b
let m_t = 0.2119*r + 0.6807*g + 0.1074*b
let s_t = 0.0883*r + 0.2817*g + 0.6300*b
let l_c = max(0.0, l_t)^(1.0/3.0)
let m_c = max(0.0, m_t)^(1.0/3.0)
let s_c = max(0.0, s_t)^(1.0/3.0)
[0.2105*l_c + 0.7936*m_c - 0.0041*s_c, 1.9780*l_c - 2.4286*m_c + 0.4506*s_c, 0.0259*l_c + 0.7828*m_c - 0.8087*s_c]
}
fn oklab_to_rgb |c:f32v3| {
let L = c.x
let a = c.y
let b = c.z
let l_ = L + 0.3963*a + 0.2158*b
let m_ = L - 0.1056*a - 0.0639*b
let s_ = L - 0.0895*a - 1.2915*b
let l_c = l_^3.0
let m_c = m_^3.0
let s_c = s_^3.0
[4.0767*l_c - 3.3077*m_c + 0.2310*s_c, -1.2684*l_c + 2.6098*m_c - 0.3413*s_c, -0.0042*l_c - 0.7034*m_c + 1.7076*s_c]
}
#--- 5. 物理演算ヘルパー ---
#紙の高さを取得 (滲みと照明で共通使用)
fn get_paper_height |x:i32, y:i32| {
let uv = [f32(x), f32(y)] * 0.05 * paper_scale
fbm_paper(uv)
}
# Domain Warping: 繊維に沿って座標をずらす
fn get_warped_oklab |x:i32, y:i32| {
let h_c = get_paper_height(x, y)
let h_r = get_paper_height(x+2, y)
let h_d = get_paper_height(x, y+2)
let flow = [h_r - h_c, h_d - h_c]
let offset = flow * bleed_amt * 10.0
let sx = x + i32(offset.x)
let sy = y + i32(offset.y)
let src_bgra = input_u8(sx, sy)
let lin_bgra = to_lbgra(src_bgra)
let rgb = [lin_bgra.z, lin_bgra.y, lin_bgra.x]
rgb_to_oklab(rgb)
}
# 法線マップ (Normal Map) の計算
fn get_normal |x:i32, y:i32, depth:f32| {
let h_c = get_paper_height(x, y)
let h_r = get_paper_height(x+1, y)
let h_d = get_paper_height(x, y+1)
let dx = (h_c - h_r) * depth
let dy = (h_c - h_d) * depth
let z = 1.0
let len_sq = dx*dx + dy*dy + z*z
let len = len_sq ^ 0.5
[dx / len, dy / len, z / len]
}
# --- 6. メインシェーダーロジック ---
def result_u8 |x, y| {
let r = radius
let n_pixels = f32((r + 1) * (r + 1))
# ----------------------------------------
#Step 1: 塗りのシミュレーション (桑原フィルター)
#----------------------------------------
# 第1象限 (左上)
let s1 = rsum(-r ..< 1, -r ..< 1) |dx, dy| {
let c = get_warped_oklab(x+dx, y+dy)
[c.x, c.y, c.z, dot(c, c)]
}
let mean1 = [s1.x, s1.y, s1.z] / n_pixels
let var1 = max(0.0, (s1.w / n_pixels) - dot(mean1, mean1))
# 第2象限 (右上)
let s2 = rsum(0 ..< r+1, -r ..< 1) |dx, dy| {
let c = get_warped_oklab(x+dx, y+dy)
[c.x, c.y, c.z, dot(c, c)]
}
let mean2 = [s2.x, s2.y, s2.z] / n_pixels
let var2 = max(0.0, (s2.w / n_pixels) - dot(mean2, mean2))
# 第3象限 (左下)
let s3 = rsum(-r ..< 1, 0 ..< r+1) |dx, dy| {
let c = get_warped_oklab(x+dx, y+dy)
[c.x, c.y, c.z, dot(c, c)]
}
let mean3 = [s3.x, s3.y, s3.z] / n_pixels
let var3 = max(0.0, (s3.w / n_pixels) - dot(mean3, mean3))
# 第4象限 (右下)
let s4 = rsum(0 ..< r+1, 0 ..< r+1) |dx, dy| {
let c = get_warped_oklab(x+dx, y+dy)
[c.x, c.y, c.z, dot(c, c)]
}
let mean4 = [s4.x, s4.y, s4.z] / n_pixels
let var4 = max(0.0, (s4.w / n_pixels) - dot(mean4, mean4))
#最小分散の選択
let min12 = min(var1, var2)
let c12 = ifel(var1 < var2, mean1, mean2)
let min34 = min(var3, var4)
let c34 = ifel(var3 < var4, mean3, mean4)
let paint_oklab = ifel(min12 < min34, c12, c34)
#----------------------------------------
# Step 2: 顔料の物理挙動 (ベール・ランベルトの法則)
# ----------------------------------------
let h = get_paper_height(x, y)
let valley = 1.0 - h
# 顔料が溜まる「谷」のマスク
let grain_mask = valley ^ 2.0
#顔料の厚み (Granulation)
let thickness = pigment_density * (1.0 + grain_mask * granulation * 5.0)
#吸収係数 (Oklab明度の反転で近似)
let absorption = 1.0 - paint_oklab.x
# 透過率 (Transmittance) = exp(-absorption * thickness)
let transmittance = 2.7182818 ^ (-absorption * thickness)
# 水による希釈
let diluted_transmittance = mix(transmittance, 1.0, water_amount)
# 減法混色 (紙の白地への合成)
let paint_L = 1.0 * diluted_transmittance
let paint_a = paint_oklab.y
let paint_b = paint_oklab.z
let paint_rgb = oklab_to_rgb([paint_L, paint_a, paint_b])
# ----------------------------------------
# Step 3: 紙の照明と合成
#----------------------------------------
let normal = get_normal(x, y, relief_strength)
# 光源計算
let lx = light_dir_x
let ly = -0.5
let lz = 1.0
let l_len = (lx*lx + ly*ly + lz*lz) ^ 0.5
let light_vec = [lx/l_len, ly/l_len, lz/l_len]
# Diffuse (拡散反射)
let diffuse = max(0.0, dot(normal, light_vec))
# Sheen (微細光沢)
let view_dot = max(0.0, normal.z)
let sheen = ((1.0 - view_dot) ^ 3.0) * paper_sheen
#テクスチャの注入 (紙の陰影を合成)
let lit_rgb = paint_rgb * mix(0.7, 1.0, diffuse)
# 光沢を加算
let final_rgb = lit_rgb + [sheen, sheen, sheen]
# 出力 (0.0-1.0の範囲に制限)
let r_safe = min(max(final_rgb.x, 0.0), 1.0)
let g_safe = min(max(final_rgb.y, 0.0), 1.0)
let b_safe = min(max(final_rgb.z, 0.0), 1.0)
u8[b_safe * 255.0, g_safe * 255.0, r_safe * 255.0, 255.0]
}