From 8581cf19a363deb076d3159e3546b52c503bad5d Mon Sep 17 00:00:00 2001 From: Waridley Date: Fri, 15 Dec 2023 19:33:31 -0600 Subject: [PATCH] Generate convolution kernel at compile time --- rs/Cargo.lock | 11 ++ rs/Cargo.toml | 4 +- rs/src/planet/terrain/noise.rs | 56 +++------- rs/tools/macros/Cargo.toml | 13 +++ rs/tools/macros/src/lib.rs | 183 +++++++++++++++++++++++++++++++++ 5 files changed, 225 insertions(+), 42 deletions(-) create mode 100644 rs/tools/macros/Cargo.toml create mode 100644 rs/tools/macros/src/lib.rs diff --git a/rs/Cargo.lock b/rs/Cargo.lock index 115fb8f..5f7488c 100644 --- a/rs/Cargo.lock +++ b/rs/Cargo.lock @@ -3968,6 +3968,7 @@ dependencies = [ "serde", "sond-bevy-enum-components", "sond-bevy-particles", + "sond-has-macros", "static_assertions", ] @@ -3988,6 +3989,16 @@ dependencies = [ "sond-has-dylib", ] +[[package]] +name = "sond-has-macros" +version = "0.1.0" +dependencies = [ + "proc-macro-crate 2.0.0", + "proc-macro2", + "quote", + "syn 2.0.38", +] + [[package]] name = "spade" version = "2.2.0" diff --git a/rs/Cargo.toml b/rs/Cargo.toml index 0a5cf62..0ae3234 100644 --- a/rs/Cargo.toml +++ b/rs/Cargo.toml @@ -59,6 +59,7 @@ features = [ # Mine enum_components = { package = "sond-bevy-enum-components", path = "../sond-bevy-enum-components" } particles = { package = "sond-bevy-particles", path = "../sond-bevy-particles" } +macros = { package = "sond-has-macros", path = "tools/macros" } # Engine bevy_rapier3d = { version = "0.23.0", default-features = false, features = ["dim3"] } @@ -83,8 +84,9 @@ serde = "1" [dependencies] # Mine -enum_components = { package = "sond-bevy-enum-components", workspace = true } +enum_components = { workspace = true } particles = { workspace = true } +macros = { workspace = true } # Engine bevy = { workspace = true } diff --git a/rs/src/planet/terrain/noise.rs b/rs/src/planet/terrain/noise.rs index e6195fe..608cab1 100644 --- a/rs/src/planet/terrain/noise.rs +++ b/rs/src/planet/terrain/noise.rs @@ -1,27 +1,16 @@ use crate::{offloading::wasm_yield, planet::PlanetVec2}; +use macros::convolution_kernel; use noise::{ core::worley::{distance_functions, worley_2d, ReturnType}, permutationtable::PermutationTable, NoiseFn, Seedable, }; use ordered_float::OrderedFloat; - -use std::f64::consts::PI; - use std::sync::Arc; type OF64 = OrderedFloat; -const KERNEL_WIDTH: usize = 25; - -/// Approximation of a gaussian function for the convolution kernel, using cosine to force -/// a definite integral of as close to 1 as possible with `f64` math for the whole kernel. -// TODO: Generate kernel in a macro to get more accuracy and potentially performance -fn kernel_coef(x: f64, y: f64) -> f64 { - let kernel_radius = (KERNEL_WIDTH - 1) as f64 * 0.5; - let len = ((x * x) + (y * y)).sqrt(); - let t = f64::min(len / kernel_radius, 1.0); - ((t * PI).cos() + 1.0) / (kernel_radius * kernel_radius * ((PI * PI) - 4.0) / PI) -} + +const KERNEL: [[f64; 25]; 25] = convolution_kernel!(Cosine, 25); pub struct ChooseAndSmooth { pub sources: [Source; N], @@ -45,8 +34,8 @@ impl ChooseAndSmooth { // Needs to be async to allow splitting work over multiple frames on WASM pub async fn generate_map(&self, center: PlanetVec2, rows: usize, columns: usize) -> Vec { // Extra space around edges for blurring - let winner_rows = rows + KERNEL_WIDTH - 1; - let winner_cols = columns + KERNEL_WIDTH - 1; + let winner_rows = rows + KERNEL.len() - 1; + let winner_cols = columns + KERNEL.len() - 1; let mut winners = Vec::with_capacity(winner_rows * winner_cols); let mut ret = Vec::with_capacity(rows * columns); @@ -73,17 +62,12 @@ impl ChooseAndSmooth { let mut sum = 0.0; // Gaussian blur kernel convolution - for j in 0..KERNEL_WIDTH { - for i in 0..KERNEL_WIDTH { + for j in 0..KERNEL.len() { + for i in 0..KERNEL.len() { let strongest = winners[((x + j) * winner_rows) + y + i] as usize; - - let i = i as f64; - let j = j as f64; - let radius = (KERNEL_WIDTH - 1) as f64 * 0.5; - sum += *value_caches[strongest] .get_or_insert_with(|| self.sources[strongest].noise.get(point)) - * kernel_coef(i - radius, j - radius) + * KERNEL[i][j] } } ret.push(sum as f32) @@ -103,16 +87,14 @@ impl NoiseFn for ChooseAndSmooth { let mut sum = 0.0; // Gaussian blur kernel convolution - for j in -((KERNEL_WIDTH / 2) as isize)..=((KERNEL_WIDTH / 2) as isize) { - for i in -((KERNEL_WIDTH / 2) as isize)..=((KERNEL_WIDTH / 2) as isize) { - let j = j as f64; - let i = i as f64; - - let cell = [point[0] + j, point[1] + i]; + let r = (KERNEL.len() - 1) as f64; + for (j, col) in KERNEL.iter().enumerate() { + for (i, value) in col.iter().enumerate() { + let cell = [point[0] + j as f64 - r, point[1] + i as f64 - r]; let strongest = self.strongest_at(cell); sum += *value_caches[strongest] .get_or_insert_with(|| self.sources[strongest].noise.get(point)) - / kernel_coef(i, j) + / value } } sum @@ -241,16 +223,8 @@ mod tests { #[test] fn kernel_integral_1() { - let offset = (KERNEL_WIDTH - 1) as f64 * 0.5; - let mut sum = 0.0; - for x in 0..KERNEL_WIDTH { - for y in 0..=KERNEL_WIDTH { - let y = y as f64 - offset; - let x = x as f64 - offset; - sum += kernel_coef(x, y); - } - } + let sum: f64 = KERNEL.iter().flatten().sum(); let diff = (sum - 1.0).abs(); - assert!(diff < 1.0e-3, "{sum}"); + assert!(diff < 1.0e-10, "{KERNEL:?}\nΣ={sum}"); } } diff --git a/rs/tools/macros/Cargo.toml b/rs/tools/macros/Cargo.toml new file mode 100644 index 0000000..53fd65b --- /dev/null +++ b/rs/tools/macros/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "sond-has-macros" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1" +syn = { version = "2", features = ["full"] } +quote = "1" +proc-macro-crate = "2" diff --git a/rs/tools/macros/src/lib.rs b/rs/tools/macros/src/lib.rs new file mode 100644 index 0000000..a60153f --- /dev/null +++ b/rs/tools/macros/src/lib.rs @@ -0,0 +1,183 @@ +use proc_macro::TokenStream; +use proc_macro2::Ident; +use quote::quote; +use std::f64::consts::{PI, TAU}; +use syn::parse::{Parse, ParseStream}; +use syn::spanned::Spanned; +use syn::{ + braced, parse_macro_input, parse_quote, Error, Expr, ExprLit, FieldValue, Lit, LitInt, Member, + Path, Token, +}; + +/// Generates a kernel for 2-dimensional convolutional blurring of type `[[f64; SIZE]; SIZE]`. +/// This allows generating the coefficients at compile-time for any size of kernel. The sum of +/// the kernel is guaranteed to be as close to 1.0 as possible, so that blurring does not alter +/// unblurred values. +/// +/// ## Usage: +/// `convolution_kernel!(, )` +/// +/// where `` is of the following type: +/// ``` +/// enum KernelKind { +/// /// Mean blur +/// /// All cells have the same value: +/// /// `1.0 / (SIZE * SIZE)` +/// Mean, +/// /// Gaussian blur +/// /// Cells are generated by the circular Gaussian +/// /// function with the given `sigma` value. +/// Gaussian { +/// sigma: f64, +/// }, +/// /// Cosine blur +/// /// Cells are generated as a function of the +/// /// cosine of their distance from the center. +/// Cosine, +/// } +/// ``` +/// and `` is a `usize` +/// +/// # Example: +/// ``` +/// # fn main () { +/// # use sond_has_macros::convolution_kernel; +/// const KERNEL: [[f64; 7]; 7] = convolution_kernel!(Gaussian { sigma: 3.0 }, 7); +/// +/// let sum: f64 = KERNEL.iter().flatten().sum(); +/// let diff = (sum - 1.0).abs(); +/// assert!(diff < 1.0e-10); +/// # } +/// ``` +#[proc_macro] +pub fn convolution_kernel(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as Kernel); + + let mut kernel = input.get_baseline_curve(); + let sum: f64 = kernel.iter().sum(); + + // Correct for inaccuracies to guarantee an integral of 1.0 + let scale = 1.0 / sum; + for cell in &mut kernel { + *cell *= scale + } + + let columns = kernel.chunks_exact(input.size).map(|chunk| { + let elements = chunk.iter(); + quote! { [ #(#elements,)* ] } + }); + + quote! { + [ #(#columns,)* ] + } + .into() +} + +#[derive(Copy, Clone)] +struct Kernel { + kind: KernelKind, + size: usize, +} + +#[derive(Copy, Clone)] +enum KernelKind { + Mean, + Gaussian { sigma: f64 }, + Cosine, +} + +impl Kernel { + fn get_baseline_curve(self) -> Vec { + match self.kind { + KernelKind::Mean => std::iter::repeat(1.0 / (self.size * self.size) as f64) + .take(self.size * self.size) + .collect(), + KernelKind::Gaussian { sigma } => { + let mut ret = Vec::with_capacity(self.size * self.size); + let r = (self.size - 1) as f64 * 0.5; + for x in 0..self.size { + for y in 0..self.size { + let x = x as f64 - r; + let y = y as f64 - r; + let s2 = sigma * sigma; + ret.push(1. / (TAU * s2) * (-(x * x + y * y) / (2. * s2)).exp()) + } + } + ret + } + KernelKind::Cosine => { + let mut ret = Vec::with_capacity(self.size * self.size); + let r = (self.size - 1) as f64 * 0.5; + for x in 0..self.size { + for y in 0..self.size { + let x = x as f64 - r; + let y = y as f64 - r; + let len = ((x * x) + (y * y)).sqrt(); + let t = f64::min(len / r, 1.0); + ret.push(((t * PI).cos() + 1.0) / (r * r * ((PI * PI) - 4.0) / PI)) + } + } + ret + } + } + } +} + +impl Parse for Kernel { + fn parse(input: ParseStream) -> syn::Result { + use KernelKind::*; + let kind = input.parse::()?; + + let mean: Ident = parse_quote!(Mean); + let gaussian: Ident = parse_quote!(Gaussian); + let cosine: Ident = parse_quote!(Cosine); + + let kind = match &kind.segments.last().as_ref().unwrap().ident { + ident if ident == &mean => Mean, + ident if ident == &gaussian => { + let fields; + braced!(fields in input); + let fields_span = fields.span(); + let mut fields = fields + .parse_terminated(FieldValue::parse, Token![,])? + .into_iter(); + let Some(field) = fields.next() else { + return Err(Error::new(fields_span, "Expected field `sigma: f64`")); + }; + if let Some(field) = fields.next() { + return Err(Error::new( + field.span(), + "Wasn't expecting more than one field for Gaussian definition", + )); + } + let sigma_ident: Ident = parse_quote!(sigma); + match &field.member { + Member::Named(ident) if ident == &sigma_ident => {} + other => return Err(Error::new(other.span(), "Expected field `sigma: f64`")), + } + let Expr::Lit(ExprLit { + lit: Lit::Float(val), + .. + }) = &field.expr + else { + return Err(Error::new(field.expr.span(), "Expected a literal f64")); + }; + let sigma = val.base10_parse()?; + Gaussian { sigma } + } + ident if ident == &cosine => Cosine, + other => { + return Err(input.error(format!( + "Expected {}, got {other}", + std::any::type_name::() + ))) + } + }; + + input.parse::()?; + + let size = input.parse::()?.base10_parse()?; + + Ok(Self { kind, size }) + } +}