use super::super::transpiler_common::ShaderLang;
use crate::types::SdfNode;
use std::fmt::Write;
pub struct WgslLang;
impl crate::compiled::transpiler_common::private::Sealed for WgslLang {}
impl ShaderLang for WgslLang {
const NAME: &'static str = "wgsl";
fn vec2_ctor(x: &str, y: &str) -> String {
format!("vec2<f32>({}, {})", x, y)
}
fn vec3_ctor(x: &str, y: &str, z: &str) -> String {
format!("vec3<f32>({}, {}, {})", x, y, z)
}
fn vec4_ctor(x: &str, y: &str, z: &str, w: &str) -> String {
format!("vec4<f32>({}, {}, {}, {})", x, y, z, w)
}
fn vec2_zero() -> &'static str {
"vec2<f32>(0.0)"
}
fn vec3_zero() -> &'static str {
"vec3<f32>(0.0)"
}
fn vec2_splat(v: &str) -> String {
format!("vec2<f32>({})", v)
}
fn vec3_splat(v: &str) -> String {
format!("vec3<f32>({})", v)
}
fn decl_float(name: &str, expr: &str) -> String {
format!(" let {} = {};\n", name, expr)
}
fn decl_vec2(name: &str, expr: &str) -> String {
format!(" let {} = {};\n", name, expr)
}
fn decl_vec3(name: &str, expr: &str) -> String {
format!(" let {} = {};\n", name, expr)
}
fn decl_mut_float(name: &str, expr: &str) -> String {
format!(" var {} = {};\n", name, expr)
}
fn decl_mut_vec2(name: &str, expr: &str) -> String {
format!(" var {} = {};\n", name, expr)
}
fn decl_mut_vec3(name: &str, expr: &str) -> String {
format!(" var {} = {};\n", name, expr)
}
fn decl_mut_float_typed(name: &str) -> String {
format!(" var {}: f32;\n", name)
}
fn decl_mut_vec3_typed(name: &str) -> String {
format!(" var {}: vec3<f32>;\n", name)
}
fn select_expr(cond: &str, true_val: &str, false_val: &str) -> String {
format!("select({}, {}, {})", false_val, true_val, cond)
}
fn modulo_expr(a: &str, b: &str) -> String {
format!("(({a}) - ({b}) * floor(({a}) / ({b})))")
}
fn cast_float(expr: &str) -> String {
format!("f32({})", expr)
}
fn cast_int(expr: &str) -> String {
format!("i32({})", expr)
}
fn decl_int(name: &str, expr: &str) -> String {
format!(" let {name}: i32 = {expr};\n")
}
fn global_float_array(name: &str, values: &[f32]) -> String {
let n = values.len();
let items: Vec<String> = values
.iter()
.map(|v| super::super::transpiler_common::lit(*v))
.collect();
format!(
"var<private> {name}: array<f32, {n}> = array<f32, {n}>({});\n",
items.join(", ")
)
}
fn global_vec3_fn(name: &str, body: &str) -> String {
format!("fn {name}(p: vec3<f32>) -> vec3<f32> {{\n{body}}}\n")
}
fn select_float(cond: &str, a: &str, b: &str) -> String {
format!("select({b}, {a}, {cond})")
}
fn for_loop_int(name: &str, init: i32, cond: &str, _incr: &str) -> String {
format!(
" var {} = {}i;\n loop {{\n if !({}) {{ break; }}\n",
name, init, cond
)
}
fn param_dynamic(vec_idx: usize, comp: &str) -> String {
format!("sdf_params.data[{}].{}", vec_idx, comp)
}
fn func_signature() -> &'static str {
"fn sdf_eval(p: vec3<f32>) -> f32 {"
}
const CAPSULE_DEGENERATE_GUARD: bool = true;
const CAPSULE_RE_EMIT_PARAMS: bool = false;
fn helper_source(name: &str) -> Option<&'static str> {
match name {
"smooth_min" => Some(HELPER_SMOOTH_MIN),
"smooth_max" => Some(HELPER_SMOOTH_MAX),
"quat_rotate" => Some(HELPER_QUAT_ROTATE),
"alice_atan2" => Some(HELPER_ALICE_ATAN2),
"hash_noise" => Some(HELPER_HASH_NOISE),
"taper_bound" => Some(HELPER_TAPER_BOUND),
"perlin_noise" => Some(HELPER_PERLIN_NOISE),
"sdf_rounded_cone" => Some(HELPER_SDF_ROUNDED_CONE),
"sdf_pyramid" => Some(HELPER_SDF_PYRAMID),
"sdf_octahedron" => Some(HELPER_SDF_OCTAHEDRON),
"sdf_hex_prism" => Some(HELPER_SDF_HEX_PRISM),
"sdf_link" => Some(HELPER_SDF_LINK),
"sdf_triangle" => Some(HELPER_SDF_TRIANGLE),
"sdf_bezier" => Some(HELPER_SDF_BEZIER),
"sdf_capped_cone" => Some(HELPER_SDF_CAPPED_CONE),
"sdf_capped_torus" => Some(HELPER_SDF_CAPPED_TORUS),
"sdf_rounded_cylinder" => Some(HELPER_SDF_ROUNDED_CYLINDER),
"sdf_triangular_prism" => Some(HELPER_SDF_TRIANGULAR_PRISM),
"sdf_cut_sphere" => Some(HELPER_SDF_CUT_SPHERE),
"sdf_cut_hollow_sphere" => Some(HELPER_SDF_CUT_HOLLOW_SPHERE),
"sdf_death_star" => Some(HELPER_SDF_DEATH_STAR),
"sdf_solid_angle" => Some(HELPER_SDF_SOLID_ANGLE),
"sdf_rhombus" => Some(HELPER_SDF_RHOMBUS),
"sdf_horseshoe" => Some(HELPER_SDF_HORSESHOE),
"sdf_vesica" => Some(HELPER_SDF_VESICA),
"sdf_infinite_cone" => Some(HELPER_SDF_INFINITE_CONE),
"sdf_heart" => Some(HELPER_SDF_HEART),
"sdf_tube" => Some(HELPER_SDF_TUBE),
"sdf_barrel" => Some(HELPER_SDF_BARREL),
"sdf_diamond" => Some(HELPER_SDF_DIAMOND),
"sdf_chamfered_cube" => Some(HELPER_SDF_CHAMFERED_CUBE),
"sdf_superellipsoid" => Some(HELPER_SDF_SUPERELLIPSOID),
"sdf_rounded_x" => Some(HELPER_SDF_ROUNDED_X),
"sdf_pie" => Some(HELPER_SDF_PIE),
"columns" => Some(HELPER_COLUMNS),
"icosahedral_fold" => Some(HELPER_ICOSAHEDRAL_FOLD),
"octant_mirror" => Some(HELPER_OCTANT_MIRROR),
"sdf_trapezoid" => Some(HELPER_SDF_TRAPEZOID),
"sdf_parallelogram" => Some(HELPER_SDF_PARALLELOGRAM),
"sdf_tunnel" => Some(HELPER_SDF_TUNNEL),
"sdf_uneven_capsule" => Some(HELPER_SDF_UNEVEN_CAPSULE),
"sdf_egg" => Some(HELPER_SDF_EGG),
"sdf_arc_shape" => Some(HELPER_SDF_ARC_SHAPE),
"sdf_moon" => Some(HELPER_SDF_MOON),
"sdf_cross_shape" => Some(HELPER_SDF_CROSS_SHAPE),
"sdf_blobby_cross" => Some(HELPER_SDF_BLOBBY_CROSS),
"sdf_parabola_segment" => Some(HELPER_SDF_PARABOLA_SEGMENT),
"sdf_regular_polygon" => Some(HELPER_SDF_REGULAR_POLYGON),
"sdf_star_polygon" => Some(HELPER_SDF_STAR_POLYGON),
"sdf_stairs" => Some(HELPER_SDF_STAIRS),
"sdf_helix" => Some(HELPER_SDF_HELIX),
"sdf_ellipsoid" => Some(HELPER_SDF_ELLIPSOID),
"bezier_distance_2d" => Some(HELPER_BEZIER_DISTANCE_2D),
"sdf_tetrahedron" => Some(HELPER_SDF_TETRAHEDRON),
"sdf_dodecahedron" => Some(HELPER_SDF_DODECAHEDRON),
"sdf_icosahedron" => Some(HELPER_SDF_ICOSAHEDRON),
"sdf_truncated_octahedron" => Some(HELPER_SDF_TRUNCATED_OCTAHEDRON),
"sdf_truncated_icosahedron" => Some(HELPER_SDF_TRUNCATED_ICOSAHEDRON),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TranspileMode {
Hardcoded,
Dynamic,
}
#[derive(Debug, Clone)]
pub struct WgslShader {
pub source: String,
pub helper_count: usize,
pub param_layout: Vec<f32>,
pub mode: TranspileMode,
pub workgroup_size: u32,
}
impl WgslShader {
pub fn transpile(node: &SdfNode, mode: TranspileMode) -> Self {
let mut transpiler = WgslTranspiler::new(mode);
let body = transpiler.transpile_node(node, "p");
let source = transpiler.generate_shader(&body);
Self {
source,
helper_count: transpiler.helper_functions.len(),
param_layout: transpiler.params,
mode,
workgroup_size: 256, }
}
pub fn extract_params(node: &SdfNode) -> Vec<f32> {
let mut transpiler = WgslTranspiler::new(TranspileMode::Dynamic);
let _ = transpiler.transpile_node(node, "p");
transpiler.params
}
pub fn transpile_material(node: &SdfNode, mode: TranspileMode) -> String {
let mut materials: Vec<(u32, &SdfNode)> = Vec::new();
Self::collect_materials(node, &mut materials);
if materials.is_empty() {
return String::new();
}
let main_shader = Self::transpile(node, mode);
let main_source = &main_shader.source;
let mut helpers_all = String::new();
let mut eval_fns = String::new();
let mut body = String::from("fn sdf_eval_material(p: vec3<f32>) -> f32 {\n");
body.push_str(" var closest_dist: f32 = 1e10;\n");
body.push_str(" var mat_id: f32 = 0.0;\n");
for (i, (mid, child)) in materials.iter().enumerate() {
let mut transpiler = WgslTranspiler::new(mode);
let fn_name = format!("sdf_mat_sub_{}", i);
let inner_body = transpiler.transpile_node(child, "p");
for helper in &transpiler.helper_functions {
if let Some(src) = WgslLang::helper_source(helper) {
let fn_sig = format!("fn {}(", helper);
if !helpers_all.contains(&fn_sig) && !main_source.contains(&fn_sig) {
helpers_all.push_str(src);
helpers_all.push('\n');
}
}
}
write!(
eval_fns,
"fn {}(p: vec3<f32>) -> f32 {{\n{}}}\n\n",
fn_name, inner_body
)
.unwrap();
write!(
body,
" let d_m{i} = {fn_name}(p);\n if (d_m{i} < closest_dist) {{ closest_dist = d_m{i}; mat_id = {mid}.0; }}\n",
i = i,
fn_name = fn_name,
mid = mid,
)
.unwrap();
}
body.push_str(" return mat_id;\n}\n");
format!("{}{}{}", helpers_all, eval_fns, body)
}
fn collect_materials<'a>(node: &'a SdfNode, out: &mut Vec<(u32, &'a SdfNode)>) {
match node {
SdfNode::WithMaterial { child, material_id } => {
out.push((*material_id, child));
}
SdfNode::Union { a, b }
| SdfNode::Intersection { a, b }
| SdfNode::Subtraction { a, b }
| SdfNode::SmoothUnion { a, b, .. }
| SdfNode::SmoothIntersection { a, b, .. }
| SdfNode::SmoothSubtraction { a, b, .. }
| SdfNode::ChamferUnion { a, b, .. }
| SdfNode::ChamferIntersection { a, b, .. }
| SdfNode::ChamferSubtraction { a, b, .. }
| SdfNode::StairsUnion { a, b, .. }
| SdfNode::StairsIntersection { a, b, .. }
| SdfNode::StairsSubtraction { a, b, .. }
| SdfNode::XOR { a, b }
| SdfNode::Morph { a, b, .. }
| SdfNode::ColumnsUnion { a, b, .. }
| SdfNode::ColumnsIntersection { a, b, .. }
| SdfNode::ColumnsSubtraction { a, b, .. }
| SdfNode::Pipe { a, b, .. }
| SdfNode::Engrave { a, b, .. }
| SdfNode::Groove { a, b, .. }
| SdfNode::Tongue { a, b, .. }
| SdfNode::ExpSmoothUnion { a, b, .. }
| SdfNode::ExpSmoothIntersection { a, b, .. }
| SdfNode::ExpSmoothSubtraction { a, b, .. } => {
Self::collect_materials(a, out);
Self::collect_materials(b, out);
}
SdfNode::Translate { child, .. }
| SdfNode::Rotate { child, .. }
| SdfNode::Scale { child, .. }
| SdfNode::ScaleNonUniform { child, .. }
| SdfNode::Twist { child, .. }
| SdfNode::Bend { child, .. }
| SdfNode::RepeatInfinite { child, .. }
| SdfNode::RepeatFinite { child, .. }
| SdfNode::Noise { child, .. }
| SdfNode::Round { child, .. }
| SdfNode::Onion { child, .. }
| SdfNode::Elongate { child, .. }
| SdfNode::Mirror { child, .. }
| SdfNode::Revolution { child, .. }
| SdfNode::Extrude { child, .. }
| SdfNode::SweepBezier { child, .. }
| SdfNode::Taper { child, .. }
| SdfNode::Displacement { child, .. }
| SdfNode::SineDisplacement { child, .. }
| SdfNode::PolarRepeat { child, .. }
| SdfNode::OctantMirror { child, .. }
| SdfNode::Shear { child, .. }
| SdfNode::Animated { child, .. }
| SdfNode::IcosahedralSymmetry { child, .. }
| SdfNode::IFS { child, .. }
| SdfNode::HeightmapDisplacement { child, .. }
| SdfNode::SurfaceRoughness { child, .. }
| SdfNode::ProjectiveTransform { child, .. }
| SdfNode::LatticeDeform { child, .. }
| SdfNode::SdfSkinning { child, .. } => {
Self::collect_materials(child, out);
}
_ => {}
}
}
pub fn with_workgroup_size(mut self, max_workgroup_x: u32) -> Self {
let clamped = max_workgroup_x.clamp(64, 1024);
self.workgroup_size = 1 << clamped.ilog2();
self
}
pub fn to_compute_shader(&self) -> String {
let params_decl = if self.mode == TranspileMode::Dynamic {
"\nstruct Params {\n data: array<vec4<f32>, 1024>,\n}\n\n@group(0) @binding(3) var<uniform> sdf_params: Params;\n"
} else {
""
};
format!(
r"// ALICE-SDF Generated Compute Shader ({mode:?} Mode)
struct InputPoint {{
x: f32,
y: f32,
z: f32,
_pad: f32,
}}
struct OutputDistance {{
distance: f32,
_pad1: f32,
_pad2: f32,
_pad3: f32,
}}
@group(0) @binding(0) var<storage, read> input_points: array<InputPoint>;
@group(0) @binding(1) var<storage, read_write> output_distances: array<OutputDistance>;
@group(0) @binding(2) var<uniform> point_count: u32;
{params_decl}
{sdf_func}
@compute @workgroup_size({wg_size})
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {{
let idx = global_id.x;
if (idx >= point_count) {{
return;
}}
let point = input_points[idx];
let p = vec3<f32>(point.x, point.y, point.z);
let distance = sdf_eval(p);
output_distances[idx].distance = distance;
}}
",
mode = self.mode,
params_decl = params_decl,
sdf_func = self.source,
wg_size = self.workgroup_size,
)
}
pub fn to_compute_shader_with_normals(&self) -> String {
let params_decl = if self.mode == TranspileMode::Dynamic {
"\nstruct Params {\n data: array<vec4<f32>, 1024>,\n}\n\n@group(0) @binding(3) var<uniform> sdf_params: Params;\n"
} else {
""
};
format!(
r"// ALICE-SDF Generated Compute Shader - Distance + Normals ({mode:?} Mode)
struct InputPoint {{
x: f32,
y: f32,
z: f32,
_pad: f32,
}}
struct Output {{
dist: f32,
nx: f32,
ny: f32,
nz: f32,
}}
@group(0) @binding(0) var<storage, read> input_points: array<InputPoint>;
@group(0) @binding(1) var<storage, read_write> output: array<Output>;
@group(0) @binding(2) var<uniform> point_count: u32;
{params_decl}
{sdf_func}
fn estimate_normal(p: vec3<f32>) -> vec3<f32> {{
let e = 0.001;
let k0 = vec3<f32>(1.0, -1.0, -1.0);
let k1 = vec3<f32>(-1.0, -1.0, 1.0);
let k2 = vec3<f32>(-1.0, 1.0, -1.0);
let k3 = vec3<f32>(1.0, 1.0, 1.0);
let d0 = sdf_eval(p + k0 * e);
let d1 = sdf_eval(p + k1 * e);
let d2 = sdf_eval(p + k2 * e);
let d3 = sdf_eval(p + k3 * e);
return normalize(k0 * d0 + k1 * d1 + k2 * d2 + k3 * d3);
}}
@compute @workgroup_size({wg_size})
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {{
let idx = gid.x;
if (idx >= point_count) {{ return; }}
let pt = input_points[idx];
let p = vec3<f32>(pt.x, pt.y, pt.z);
let d = sdf_eval(p);
let n = estimate_normal(p);
output[idx].dist = d;
output[idx].nx = n.x;
output[idx].ny = n.y;
output[idx].nz = n.z;
}}
",
mode = self.mode,
params_decl = params_decl,
sdf_func = self.source,
wg_size = self.workgroup_size,
)
}
pub fn get_eval_function(&self) -> &str {
&self.source
}
#[cfg(feature = "volume")]
pub fn to_volume_shader(&self) -> String {
format!(
r"// ALICE-SDF Volume Bake Shader (3D Dispatch)
struct VolumeUniforms {{
resolution: vec4<u32>,
bounds_min: vec4<f32>,
bounds_max: vec4<f32>,
}}
@group(0) @binding(0) var<storage, read_write> output_volume: array<f32>;
@group(0) @binding(1) var<uniform> uniforms: VolumeUniforms;
{sdf_func}
@compute @workgroup_size(4, 4, 4)
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {{
let res = uniforms.resolution.xyz;
if (gid.x >= res.x || gid.y >= res.y || gid.z >= res.z) {{
return;
}}
let fres = vec3<f32>(f32(res.x) - 1.0, f32(res.y) - 1.0, f32(res.z) - 1.0);
let t = vec3<f32>(f32(gid.x), f32(gid.y), f32(gid.z)) / max(fres, vec3<f32>(1.0));
let p = mix(uniforms.bounds_min.xyz, uniforms.bounds_max.xyz, t);
let distance = sdf_eval(p);
let idx = gid.x + gid.y * res.x + gid.z * res.x * res.y;
output_volume[idx] = distance;
}}
",
sdf_func = self.source,
)
}
}
struct WgslTranspiler {
var_counter: usize,
helper_functions: Vec<&'static str>,
mode: TranspileMode,
params: Vec<f32>,
globals: String,
}
impl WgslTranspiler {
const fn new(mode: TranspileMode) -> Self {
Self {
var_counter: 0,
helper_functions: Vec::new(),
mode,
params: Vec::new(),
globals: String::new(),
}
}
fn transpile_node(&mut self, node: &SdfNode, point_var: &str) -> String {
use super::super::transpiler_common::{GenericTranspiler, TranspileModeLang};
let mode = match self.mode {
TranspileMode::Hardcoded => TranspileModeLang::Hardcoded,
TranspileMode::Dynamic => TranspileModeLang::Dynamic,
};
let mut generic: GenericTranspiler<WgslLang> = GenericTranspiler::new(mode);
generic.var_counter = self.var_counter;
generic.params = std::mem::take(&mut self.params);
generic.helper_functions = std::mem::take(&mut self.helper_functions);
generic.globals = std::mem::take(&mut self.globals);
let body = generic.transpile_node(node, point_var);
self.globals = generic.globals;
self.var_counter = generic.var_counter;
self.params = generic.params;
self.helper_functions = generic.helper_functions;
body
}
fn generate_shader(&self, body: &str) -> String {
use super::super::transpiler_common::{GenericTranspiler, TranspileModeLang};
let mut generic: GenericTranspiler<WgslLang> =
GenericTranspiler::new(TranspileModeLang::Hardcoded);
generic.helper_functions.clone_from(&self.helper_functions);
generic.globals.clone_from(&self.globals);
generic.generate_shader(body)
}
}
const HELPER_SMOOTH_MIN: &str = r"// Deep Fried Edition: Division-free smooth_min
// inv_k should be pre-computed as 1.0/k
fn smooth_min_fast(a: f32, b: f32, k: f32, inv_k: f32) -> f32 {
let h = max(1.0 - abs(a - b) * inv_k, 0.0); // same rounding as the CPU law
return min(a, b) - h * h * (k * 0.25);
}
// Legacy smooth_min (slower, uses division)
fn smooth_min(a: f32, b: f32, k: f32) -> f32 {
return smooth_min_fast(a, b, k, 1.0 / k);
}";
const HELPER_SMOOTH_MAX: &str = r"// Deep Fried Edition: Division-free smooth_max
fn smooth_max_fast(a: f32, b: f32, k: f32, inv_k: f32) -> f32 {
let h = max(1.0 - abs(a - b) * inv_k, 0.0);
return max(a, b) + h * h * (k * 0.25);
}
fn smooth_max(a: f32, b: f32, k: f32) -> f32 {
return smooth_max_fast(a, b, k, 1.0 / k);
}";
const HELPER_TAPER_BOUND: &str = r"// Same law as `alice_sdf::compiled::real::taper_bound` (Jacobian ball + cone ∩ slab).
fn alice_taper_bound(d: f32, p: vec3<f32>, factor: f32, rx: f32, ry: f32) -> f32 {
let fa = abs(factor);
if (fa == 0.0) { return d; }
let den_abs = max(abs(1.0 - p.y * factor), 1e-6);
let s = 1.0 / den_abs;
let rho = sqrt(p.x * p.x + p.z * p.z);
let d_abs = abs(d);
let j0 = max(s, 1.0) + fa * s * s * rho;
let r = min(d_abs / j0, den_abs / (fa + fa));
let den1 = max(den_abs - fa * r, 1e-6);
let s1 = 1.0 / den1;
let j1 = max(s1, 1.0) + fa * s1 * s1 * (rho + r);
let mag = min(d_abs / j1, r);
let d_j = select(mag, -mag, d < 0.0);
if (rx >= 1e30 || ry >= 1e30) { return d_j; }
let k = rx * fa;
let inv_n = 1.0 / sqrt(1.0 + k * k);
let big_y = p.y - 1.0 / factor;
let d_cone = (rho - k * abs(big_y)) * inv_n;
let d_slab = abs(p.y) - ry;
let d_region = max(d_cone, d_slab);
return select(max(d_j, d_region), d_j, d < 0.0);
}";
const HELPER_HASH_NOISE: &str = crate::modifiers::HASH_NOISE_WGSL;
const HELPER_PERLIN_NOISE: &str = r"fn alice_perlin_hash(x: i32, y: i32, z: i32, seed: u32) -> u32 {
var h = seed;
h ^= bitcast<u32>(x);
h *= 0x85EBCA6Bu;
h ^= bitcast<u32>(y);
h *= 0xC2B2AE35u;
h ^= bitcast<u32>(z);
h *= 0x27D4EB2Du;
h ^= h >> 16u;
return h;
}
fn alice_perlin_grad(h: u32, c0: vec3<f32>) -> f32 {
var lut = array<u32, 16>(4u, 5u, 6u, 7u, 8u, 9u, 10u, 11u, 24u, 25u, 26u, 27u, 16u, 10u, 18u, 9u);
var c = c0;
let e = lut[h & 15u];
let u_src = (e >> 4u) & 3u;
let v_src = (e >> 2u) & 3u;
let u_sign = 1.0 - 2.0 * f32((e >> 1u) & 1u);
let v_sign = 1.0 - 2.0 * f32(e & 1u);
return c[u_src] * u_sign + c[v_src] * v_sign;
}
fn alice_perlin_fade(t: f32) -> f32 {
return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
}
// Same law as `alice_sdf::modifiers::perlin_noise_3d` (integer lattice hash + 16-gradient LUT + quintic fade).
fn perlin_noise_3d(p: vec3<f32>, seed: u32) -> f32 {
let fl = floor(p);
let xi = i32(fl.x);
let yi = i32(fl.y);
let zi = i32(fl.z);
let f = p - fl;
let u = alice_perlin_fade(f.x);
let v = alice_perlin_fade(f.y);
let w = alice_perlin_fade(f.z);
let aaa = alice_perlin_grad(alice_perlin_hash(xi, yi, zi, seed), f);
let aba = alice_perlin_grad(alice_perlin_hash(xi, yi + 1, zi, seed), vec3<f32>(f.x, f.y - 1.0, f.z));
let aab = alice_perlin_grad(alice_perlin_hash(xi, yi, zi + 1, seed), vec3<f32>(f.x, f.y, f.z - 1.0));
let abb = alice_perlin_grad(alice_perlin_hash(xi, yi + 1, zi + 1, seed), vec3<f32>(f.x, f.y - 1.0, f.z - 1.0));
let baa = alice_perlin_grad(alice_perlin_hash(xi + 1, yi, zi, seed), vec3<f32>(f.x - 1.0, f.y, f.z));
let bba = alice_perlin_grad(alice_perlin_hash(xi + 1, yi + 1, zi, seed), vec3<f32>(f.x - 1.0, f.y - 1.0, f.z));
let bab = alice_perlin_grad(alice_perlin_hash(xi + 1, yi, zi + 1, seed), vec3<f32>(f.x - 1.0, f.y, f.z - 1.0));
let bbb = alice_perlin_grad(alice_perlin_hash(xi + 1, yi + 1, zi + 1, seed), vec3<f32>(f.x - 1.0, f.y - 1.0, f.z - 1.0));
let x0 = aaa + u * (baa - aaa);
let x1 = aba + u * (bba - aba);
let x2 = aab + u * (bab - aab);
let x3 = abb + u * (bbb - abb);
let y0 = x0 + v * (x1 - x0);
let y1 = x2 + v * (x3 - x2);
return y0 + w * (y1 - y0);
}";
const HELPER_ALICE_ATAN2: &str = r"fn alice_atan2(y: f32, x: f32) -> f32 {
let sy = bitcast<i32>(y) < 0;
let sx = bitcast<i32>(x) < 0;
var r = atan2(y, x);
r = select(r, select(1.5707964, -1.5707964, sy), x == 0.0);
r = select(r, select(y, select(3.1415927, -3.1415927, sy), sx), y == 0.0);
return r;
}
";
const HELPER_QUAT_ROTATE: &str = r"fn quat_rotate(v: vec3<f32>, q: vec4<f32>) -> vec3<f32> {
let t = 2.0 * cross(q.xyz, v);
return v + q.w * t + cross(q.xyz, t);
}";
const HELPER_SDF_ROUNDED_CONE: &str = r"fn sdf_rounded_cone(p: vec3<f32>, r1: f32, r2: f32, h: f32) -> f32 {
let qx = length(p.xz);
let qy = p.y + h;
let ht = h * 2.0;
let b = (r1 - r2) / ht;
let a = sqrt(1.0 - b * b);
let k = qx * (-b) + qy * a;
if (k < 0.0) { return length(vec2<f32>(qx, qy)) - r1; }
if (k > a * ht) { return length(vec2<f32>(qx, qy - ht)) - r2; }
return qx * a + qy * b - r1;
}
";
const HELPER_SDF_PYRAMID: &str = r"fn sdf_pyramid(p: vec3<f32>, h: f32) -> f32 {
let ht = h * 2.0;
let m2 = ht * ht + 0.25;
let py = p.y + h;
var px = abs(p.x);
var pz = abs(p.z);
if (pz > px) { let tmp = px; px = pz; pz = tmp; }
px -= 0.5;
pz -= 0.5;
let qx = pz;
let qy = ht * py - 0.5 * px;
let qz = ht * px + 0.5 * py;
let s = max(-qx, 0.0);
let t = clamp((qy - 0.5 * pz) / (m2 + 0.25), 0.0, 1.0);
let a = m2 * (qx + s) * (qx + s) + qy * qy;
let b = m2 * (qx + 0.5 * t) * (qx + 0.5 * t) + (qy - m2 * t) * (qy - m2 * t);
var d2: f32;
if (min(-qx * m2 - qy * 0.5, qy) > 0.0) { d2 = 0.0; } else { d2 = min(a, b); }
return sqrt((d2 + qz * qz) / m2) * select(1.0, -1.0, max(qz, -py) < 0.0);
}
";
const HELPER_SDF_OCTAHEDRON: &str = r"fn sdf_octahedron(p: vec3<f32>, s: f32) -> f32 {
let ap = abs(p);
let m = ap.x + ap.y + ap.z - s;
var q: vec3<f32>;
if (3.0 * ap.x < m) { q = ap; }
else if (3.0 * ap.y < m) { q = vec3<f32>(ap.y, ap.z, ap.x); }
else if (3.0 * ap.z < m) { q = vec3<f32>(ap.z, ap.x, ap.y); }
else { return m * 0.57735027; }
let k = clamp(0.5 * (q.z - q.y + s), 0.0, s);
return length(vec3<f32>(q.x, q.y - s + k, q.z - k));
}
";
const HELPER_SDF_HEX_PRISM: &str = r"fn sdf_hex_prism(p: vec3<f32>, hex_r: f32, h: f32) -> f32 {
let kx: f32 = -0.8660254;
let ky: f32 = 0.5;
let kz: f32 = 0.57735027;
var px = abs(p.x);
var py = abs(p.y);
let pz = abs(p.z);
let dot_kxy = kx * px + ky * py;
let refl = 2.0 * min(dot_kxy, 0.0);
px -= refl * kx;
py -= refl * ky;
let clamped_x = clamp(px, -kz * hex_r, kz * hex_r);
let dx = px - clamped_x;
let dy = py - hex_r;
let d_xy = sqrt(dx * dx + dy * dy) * select(1.0, -1.0, dy < 0.0);
let d_z = pz - h;
return min(max(d_xy, d_z), 0.0) + length(max(vec2<f32>(d_xy, d_z), vec2<f32>(0.0)));
}
";
const HELPER_SDF_TRIANGLE: &str = r"fn sdf_triangle(p: vec3<f32>, a: vec3<f32>, b: vec3<f32>, c: vec3<f32>) -> f32 {
let ba = b - a; let pa = p - a;
let cb = c - b; let pb = p - b;
let ac = a - c; let pc = p - c;
let nor = cross(ba, ac);
let sign_check = sign(dot(cross(ba, nor), pa)) +
sign(dot(cross(cb, nor), pb)) +
sign(dot(cross(ac, nor), pc));
if (sign_check < 2.0) {
let d1 = dot(ba * clamp(dot(ba, pa) / dot(ba, ba), 0.0, 1.0) - pa,
ba * clamp(dot(ba, pa) / dot(ba, ba), 0.0, 1.0) - pa);
let d2 = dot(cb * clamp(dot(cb, pb) / dot(cb, cb), 0.0, 1.0) - pb,
cb * clamp(dot(cb, pb) / dot(cb, cb), 0.0, 1.0) - pb);
let d3 = dot(ac * clamp(dot(ac, pc) / dot(ac, ac), 0.0, 1.0) - pc,
ac * clamp(dot(ac, pc) / dot(ac, ac), 0.0, 1.0) - pc);
return sqrt(min(min(d1, d2), d3));
} else {
return sqrt(dot(nor, pa) * dot(nor, pa) / dot(nor, nor));
}
}
";
const HELPER_SDF_BEZIER: &str = r"fn sdf_bezier(pos: vec3<f32>, A: vec3<f32>, B: vec3<f32>, C: vec3<f32>, rad: f32) -> f32 {
let a = B - A;
let b = A - 2.0 * B + C;
let c = a * 2.0;
let d = A - pos;
let kk = 1.0 / max(dot(b, b), 1e-10);
let kx = kk * dot(a, b);
let ky = kk * (2.0 * dot(a, a) + dot(d, b)) / 3.0;
let kz = kk * dot(d, a);
let p = ky - kx * kx;
let q = kx * (2.0 * kx * kx - 3.0 * ky) + kz;
let p3 = p * p * p;
let q2 = q * q;
let h = q2 + 4.0 * p3;
var res: f32;
if (h >= 0.0) {
let sh = sqrt(h);
let x = (vec2<f32>(sh, -sh) - q) * 0.5;
let uv = sign(x) * pow(abs(x), vec2<f32>(1.0 / 3.0));
let t = clamp(uv.x + uv.y - kx, 0.0, 1.0);
let qp = d + (c + b * t) * t;
res = dot(qp, qp);
} else {
let z = sqrt(-p);
let v = acos(q / (p * z * 2.0)) / 3.0;
let m = cos(v);
let n = sin(v) * 1.732050808;
let t0 = clamp(( m + m) * z - kx, 0.0, 1.0);
let t1 = clamp((-n - m) * z - kx, 0.0, 1.0);
let qp0 = d + (c + b * t0) * t0;
let qp1 = d + (c + b * t1) * t1;
res = min(dot(qp0, qp0), dot(qp1, qp1));
}
return sqrt(res) - rad;
}
";
const HELPER_SDF_LINK: &str = r"fn sdf_link(p: vec3<f32>, le: f32, r1: f32, r2: f32) -> f32 {
let qx = p.x;
let qy = max(abs(p.y) - le, 0.0);
let qz = p.z;
let xy_len = sqrt(qx * qx + qy * qy) - r1;
return sqrt(xy_len * xy_len + qz * qz) - r2;
}
";
const HELPER_SDF_CAPPED_CONE: &str = r"fn sdf_capped_cone(p: vec3<f32>, h: f32, r1: f32, r2: f32) -> f32 {
let qx = length(p.xz);
let qy = p.y;
let k1 = vec2<f32>(r2, h);
let k2 = vec2<f32>(r2 - r1, 2.0 * h);
var min_r: f32;
if (qy < 0.0) { min_r = r1; } else { min_r = r2; }
let ca = vec2<f32>(qx - min(qx, min_r), abs(qy) - h);
let q = vec2<f32>(qx, qy);
let cb = q - k1 + k2 * clamp(dot(k1 - q, k2) / dot(k2, k2), 0.0, 1.0);
var s: f32;
if (cb.x < 0.0 && ca.y < 0.0) { s = -1.0; } else { s = 1.0; }
return s * sqrt(min(dot(ca, ca), dot(cb, cb)));
}
";
const HELPER_SDF_CAPPED_TORUS: &str = r"fn sdf_capped_torus(p: vec3<f32>, ra: f32, rb: f32, an: f32) -> f32 {
let sc = vec2<f32>(sin(an), cos(an));
let px = abs(p.x);
var k: f32;
if (sc.y * px > sc.x * p.y) {
k = dot(vec2<f32>(px, p.y), sc);
} else {
k = length(vec2<f32>(px, p.y));
}
return sqrt(px * px + p.y * p.y + p.z * p.z + ra * ra - 2.0 * ra * k) - rb;
}
";
const HELPER_SDF_ROUNDED_CYLINDER: &str = r"fn sdf_rounded_cylinder(p: vec3<f32>, r: f32, rr: f32, h: f32) -> f32 {
let d = vec2<f32>(length(p.xz) - r + rr, abs(p.y) - h);
return min(max(d.x, d.y), 0.0) + length(max(d, vec2<f32>(0.0))) - rr;
}
";
const HELPER_SDF_TRIANGULAR_PRISM: &str = r"fn sdf_triangular_prism(p: vec3<f32>, w: f32, h: f32) -> f32 {
let q = abs(p);
return max(q.z - h, max(q.x * 0.866025 + p.y * 0.5, -p.y) - w * 0.5);
}
";
const HELPER_SDF_CUT_SPHERE: &str = r"fn sdf_cut_sphere(p: vec3<f32>, r: f32, h: f32) -> f32 {
let w = sqrt(max(r * r - h * h, 0.0));
let q = vec2<f32>(length(p.xz), p.y);
let s = max((h - r) * q.x * q.x + w * w * (h + r - 2.0 * q.y), h * q.x - w * q.y);
if (s < 0.0) { return length(q) - r; }
if (q.x < w) { return h - q.y; }
return length(q - vec2<f32>(w, h));
}
";
const HELPER_SDF_CUT_HOLLOW_SPHERE: &str = r"fn sdf_cut_hollow_sphere(p: vec3<f32>, r: f32, h: f32, t: f32) -> f32 {
let w = sqrt(max(r * r - h * h, 0.0));
let q = vec2<f32>(length(p.xz), p.y);
if (h * q.x < w * q.y) {
return length(q - vec2<f32>(w, h)) - t;
}
return abs(length(q) - r) - t;
}
";
const HELPER_SDF_DEATH_STAR: &str = r"fn sdf_death_star(p: vec3<f32>, ra: f32, rb: f32, d: f32) -> f32 {
let a = (ra * ra - rb * rb + d * d) / (2.0 * d);
let b = sqrt(max(ra * ra - a * a, 0.0));
let q = vec2<f32>(p.x, length(p.yz));
if (q.x * b - q.y * a > d * max(b - q.y, 0.0)) {
return length(q - vec2<f32>(a, b));
}
return max(length(q) - ra, -(length(q - vec2<f32>(d, 0.0)) - rb));
}
";
const HELPER_SDF_SOLID_ANGLE: &str = r"fn sdf_solid_angle(p: vec3<f32>, an: f32, ra: f32) -> f32 {
let c = vec2<f32>(sin(an), cos(an));
let q = vec2<f32>(length(p.xz), p.y);
let l = length(q) - ra;
let m = length(q - c * clamp(dot(q, c), 0.0, ra));
return max(l, m * sign(c.y * q.x - c.x * q.y));
}
";
const HELPER_SDF_RHOMBUS: &str = r"fn ndot_rh(a: vec2<f32>, b: vec2<f32>) -> f32 {
return a.x * b.x - a.y * b.y;
}
fn sdf_rhombus(p: vec3<f32>, la: f32, lb: f32, h: f32, ra: f32) -> f32 {
let ap = abs(p);
let b = vec2<f32>(la, lb);
let f = clamp(ndot_rh(b, b - 2.0 * ap.xz) / dot(b, b), -1.0, 1.0);
let dxz = length(ap.xz - 0.5 * b * vec2<f32>(1.0 - f, 1.0 + f)) * sign(ap.x * b.y + ap.z * b.x - b.x * b.y) - ra;
let q = vec2<f32>(dxz, ap.y - h);
return min(max(q.x, q.y), 0.0) + length(max(q, vec2<f32>(0.0)));
}
";
const HELPER_SDF_HORSESHOE: &str = r"fn sdf_horseshoe(pos: vec3<f32>, an: f32, r: f32, le: f32, w: f32, t: f32) -> f32 {
let c = vec2<f32>(cos(an), sin(an));
let px = abs(pos.x);
let l = length(vec2<f32>(px, pos.y));
var qx = -c.x * px + c.y * pos.y;
var qy = c.y * px + c.x * pos.y;
if (!(qy > 0.0 || qx > 0.0)) { qx = l * sign(-c.x); }
if (!(qx > 0.0)) { qy = l; }
let bx = qx - le;
let by = abs(qy - r) - w;
let d2 = length(max(vec2<f32>(bx, by), vec2<f32>(0.0))) + min(max(bx, by), 0.0);
let dz = abs(pos.z) - t;
return length(max(vec2<f32>(d2, dz), vec2<f32>(0.0))) + min(max(d2, dz), 0.0);
}
";
const HELPER_SDF_VESICA: &str = r"fn sdf_vesica(p: vec3<f32>, r: f32, d: f32) -> f32 {
// Lens of revolution about Y: 2-D vesica in (length(p.xz), |y|) — same law as `sdf_vesica` on the CPU.
let q = vec2<f32>(length(p.xz), abs(p.y));
let b = sqrt(max(r * r - d * d, 0.0));
if ((q.y - b) * d > q.x * b) {
return length(vec2<f32>(q.x, q.y - b));
}
return length(vec2<f32>(q.x + d, q.y)) - r;
}
";
const HELPER_SDF_INFINITE_CONE: &str = r"fn sdf_infinite_cone(p: vec3<f32>, an: f32) -> f32 {
let c = vec2<f32>(sin(an), cos(an));
let q = vec2<f32>(length(p.xz), -p.y);
let d = length(q - c * max(dot(q, c), 0.0));
if (q.x * c.y - q.y * c.x < 0.0) { return -d; }
return d;
}
";
const HELPER_SDF_HEART: &str = r"fn sdf_heart(p: vec3<f32>, s: f32) -> f32 {
// IQ sdHeart (2-D) revolved about Y, shifted so the centre is at the origin — same law as `sdf_heart` on the CPU.
let sp = p / s;
let qx = length(sp.xz);
let qy = sp.y - 0.5;
if (qx + qy > 1.0) {
let e = vec2<f32>(qx - 0.25, qy - 0.75);
return (sqrt(dot(e, e)) - 0.35355339) * s;
}
let e1 = vec2<f32>(qx, qy - 1.0);
let t = max(qx + qy, 0.0) * 0.5;
let e2 = vec2<f32>(qx - t, qy - t);
let d = sqrt(min(dot(e1, e1), dot(e2, e2)));
return select(-d, d, qx > qy) * s;
}
";
const HELPER_SDF_TUBE: &str = r"fn sdf_tube(p: vec3<f32>, outer_r: f32, thick: f32, h: f32) -> f32 {
let r = length(p.xz);
let dr = abs(r - outer_r) - thick;
let dy = abs(p.y) - h;
let w = max(vec2<f32>(dr, dy), vec2<f32>(0.0));
return min(max(dr, dy), 0.0) + length(w);
}
";
const HELPER_SDF_BARREL: &str = r"fn sdf_barrel(p: vec3<f32>, radius: f32, h: f32, bulge: f32) -> f32 {
let r = length(p.xz);
let yn = clamp(p.y / h, -1.0, 1.0);
let er = radius + bulge * (1.0 - yn * yn);
let dr = r - er;
let dy = abs(p.y) - h;
let w = max(vec2<f32>(dr, dy), vec2<f32>(0.0));
return min(max(dr, dy), 0.0) + length(w);
}
";
const HELPER_SDF_DIAMOND: &str = r"fn sdf_diamond(p: vec3<f32>, r: f32, h: f32) -> f32 {
let q = vec2<f32>(length(p.xz), abs(p.y));
let ba = vec2<f32>(-r, h);
let qa = q - vec2<f32>(r, 0.0);
let t = clamp(dot(qa, ba) / dot(ba, ba), 0.0, 1.0);
let closest = vec2<f32>(r, 0.0) + ba * t;
let dist = length(q - closest);
if (q.x * h + q.y * r < r * h) { return -dist; }
return dist;
}
";
const HELPER_SDF_CHAMFERED_CUBE: &str = r"fn sdf_chamfered_cube(p: vec3<f32>, hx: f32, hy: f32, hz: f32, ch: f32) -> f32 {
let ap = abs(p);
let q = ap - vec3<f32>(hx, hy, hz);
let d_box = min(max(q.x, max(q.y, q.z)), 0.0) + length(max(q, vec3<f32>(0.0)));
let s = hx + hy + hz;
let d_ch = (ap.x + ap.y + ap.z - s + ch) * 0.57735;
return max(d_box, d_ch);
}
";
const HELPER_SDF_SUPERELLIPSOID: &str = r"fn sdf_superellipsoid(p: vec3<f32>, hx: f32, hy: f32, hz: f32, e1: f32, e2: f32) -> f32 {
let qx = max(abs(p.x / hx), 0.00001);
let qy = max(abs(p.y / hy), 0.00001);
let qz = max(abs(p.z / hz), 0.00001);
let ee1 = max(e1, 0.02);
let ee2 = max(e2, 0.02);
let m1 = 2.0 / ee2;
let m2 = 2.0 / ee1;
let w = pow(qx, m1) + pow(qz, m1);
let v = pow(w, ee2 / ee1) + pow(qy, m2);
let f = pow(v, ee1 * 0.5);
return (f - 1.0) * min(hx, min(hy, hz)) * 0.5;
}
";
const HELPER_SDF_ROUNDED_X: &str = r"fn sdf_rounded_x(p: vec3<f32>, w: f32, r: f32, h: f32) -> f32 {
let q = abs(p.xz);
let s = min(q.x + q.y, w) * 0.5;
let d2d = length(q - vec2<f32>(s)) - r;
let dy = abs(p.y) - h;
let ww = max(vec2<f32>(d2d, dy), vec2<f32>(0.0));
return min(max(d2d, dy), 0.0) + length(ww);
}
";
const HELPER_SDF_PIE: &str = r"fn sdf_pie(p: vec3<f32>, angle: f32, radius: f32, h: f32) -> f32 {
// IQ sdPie in the XZ plane (|x|, z), extruded along Y — same law as `sdf_pie` on the CPU.
let q = vec2<f32>(abs(p.x), p.z);
let sc = vec2<f32>(sin(angle), cos(angle));
let l = length(q) - radius;
let m = length(q - sc * clamp(dot(q, sc), 0.0, radius));
let cr = sc.y * q.x - sc.x * q.y;
let sgn = select(select(0.0, -1.0, cr < 0.0), 1.0, cr > 0.0);
let d2d = max(l, m * sgn);
let dy = abs(p.y) - h;
let w = max(vec2<f32>(d2d, dy), vec2<f32>(0.0));
return min(max(d2d, dy), 0.0) + length(w);
}
";
const HELPER_COLUMNS: &str = r"// Columns (hg_sdf fOpUnionColumns family) — same law as `sdf_columns_*_r` on the CPU: 45° rotate, shift by r/√2, wrap into r·2/n cells, rotate back.
fn alice_columns_union(a: f32, b: f32, r: f32, n: f32) -> f32 {
let m = min(a, b);
let a2 = min(a, b);
let b2 = max(a, b);
let cs = r * (2.0 / n);
let s = 0.70710678;
var ra = s * (a2 + b2) - r * 0.70710678;
let rb = s * (b2 - a2);
let hc = cs * 0.5;
let y = ra + hc;
ra = y - cs * floor(y / cs) - hc;
let a3 = s * (ra + rb);
let b3 = s * (rb - ra);
let inner = min(min(a3, b3), m);
return select(inner, m, m > r);
}
fn alice_columns_subtraction(a0: f32, b: f32, r: f32, n: f32) -> f32 {
let a = -a0;
let m = min(a, b);
let a2 = min(a, b);
let b2 = max(a, b);
let cs = r * (2.0 / n);
let s = 0.70710678;
var ra = s * (a2 + b2) - r * 0.70710678;
let rb = s * (b2 - a2);
let hc = cs * 0.5;
let y = ra + hc;
ra = y - cs * floor(y / cs) - hc;
let a3 = s * (ra + rb);
let b3 = s * (rb - ra);
let inner = -min(min(a3, b3), m);
return select(inner, -m, m > r);
}
fn alice_columns_intersection(a: f32, b: f32, r: f32, n: f32) -> f32 {
return alice_columns_subtraction(a, -b, r, n);
}
";
const HELPER_ICOSAHEDRAL_FOLD: &str = r"// Icosahedral fold — same law as `icosahedral_fold` on the CPU (octant fold, 8 × sort + 3 plane reflections).
fn alice_icosahedral_fold(p0: vec3<f32>) -> vec3<f32> {
var p = abs(p0);
let n0 = normalize(vec3<f32>(0.0, 1.0, 1.618034));
let n1 = normalize(vec3<f32>(1.0, 1.618034, 0.0));
let n2 = normalize(vec3<f32>(1.618034, 0.0, 1.0));
for (var i = 0; i < 8; i = i + 1) {
if (p.y > p.x) { p = vec3<f32>(p.y, p.x, p.z); }
if (p.z > p.y) { p = vec3<f32>(p.x, p.z, p.y); }
if (p.y > p.x) { p = vec3<f32>(p.y, p.x, p.z); }
let d0 = dot(p, n0);
if (d0 < 0.0) { p = p - n0 * (2.0 * d0); }
let d1 = dot(p, n1);
if (d1 < 0.0) { p = p - n1 * (2.0 * d1); }
let d2 = dot(p, n2);
if (d2 < 0.0) { p = p - n2 * (2.0 * d2); }
}
return p;
}
";
const HELPER_OCTANT_MIRROR: &str = r"// Octant mirror — same law as `octant_mirror` on the CPU: abs then sort descending (x ≥ y ≥ z).
fn alice_octant_mirror(p0: vec3<f32>) -> vec3<f32> {
let a = abs(p0);
var x = max(a.x, a.y);
var y = min(a.x, a.y);
let z0 = a.z;
let y1 = max(y, z0);
let z = min(y, z0);
let x2 = max(x, y1);
let y2 = min(x, y1);
return vec3<f32>(x2, y2, z);
}
";
const HELPER_SDF_TRAPEZOID: &str = r"fn _trap_ds(px: f32, py: f32, ax: f32, ay: f32, bx: f32, by: f32) -> f32 {
let dx = bx - ax;
let dy = by - ay;
let len_sq = dx * dx + dy * dy;
var t = 0.0;
if (len_sq > 0.0) { t = ((px - ax) * dx + (py - ay) * dy) / len_sq; }
let tc = clamp(t, 0.0, 1.0);
let cx = ax + dx * tc;
let cy = ay + dy * tc;
return sqrt((px - cx) * (px - cx) + (py - cy) * (py - cy));
}
fn sdf_trapezoid(p: vec3<f32>, r1: f32, r2: f32, th: f32, hd: f32) -> f32 {
let px = abs(p.x);
let py = p.y;
let he = th;
let d_bot = _trap_ds(px, py, 0.0, -he, r1, -he);
let d_slant = _trap_ds(px, py, r1, -he, r2, he);
let d_top = _trap_ds(px, py, r2, he, 0.0, he);
let d_unsigned = min(d_bot, min(d_slant, d_top));
let nx = 2.0 * he;
let ny = r1 - r2;
let d_slant_plane = (px - r1) * nx + (py + he) * ny;
var d2d = d_unsigned;
if (py >= -he && py <= he && d_slant_plane <= 0.0) { d2d = -d_unsigned; }
let dz = abs(p.z) - hd;
let ww = max(vec2<f32>(d2d, dz), vec2<f32>(0.0));
return min(max(d2d, dz), 0.0) + length(ww);
}
";
const HELPER_SDF_PARALLELOGRAM: &str = r"fn _para_ds(px: f32, py: f32, ax: f32, ay: f32, bx: f32, by: f32) -> f32 {
let dx = bx - ax;
let dy = by - ay;
let len_sq = dx * dx + dy * dy;
var t = 0.0;
if (len_sq > 0.0) { t = ((px - ax) * dx + (py - ay) * dy) / len_sq; }
let tc = clamp(t, 0.0, 1.0);
let cx = ax + dx * tc;
let cy = ay + dy * tc;
return sqrt((px - cx) * (px - cx) + (py - cy) * (py - cy));
}
fn _para_c2d(px: f32, py: f32, ax: f32, ay: f32, bx: f32, by: f32) -> f32 {
return (px - ax) * (by - ay) - (py - ay) * (bx - ax);
}
fn sdf_parallelogram(p: vec3<f32>, w: f32, ph: f32, sk: f32, hd: f32) -> f32 {
let px = p.x;
let py = p.y;
let vdx = w - sk; let vdy = -ph;
let vax = w + sk; let vay = ph;
let vbx = -w + sk; let vby = ph;
let vcx = -w - sk; let vcy = -ph;
let d1 = _para_ds(px, py, vdx, vdy, vax, vay);
let d2 = _para_ds(px, py, vax, vay, vbx, vby);
let d3 = _para_ds(px, py, vbx, vby, vcx, vcy);
let d4 = _para_ds(px, py, vcx, vcy, vdx, vdy);
let d_unsigned = min(d1, min(d2, min(d3, d4)));
let c1 = _para_c2d(px, py, vdx, vdy, vax, vay);
let c2 = _para_c2d(px, py, vax, vay, vbx, vby);
let c3 = _para_c2d(px, py, vbx, vby, vcx, vcy);
let c4 = _para_c2d(px, py, vcx, vcy, vdx, vdy);
var d2d = d_unsigned;
if (c1 <= 0.0 && c2 <= 0.0 && c3 <= 0.0 && c4 <= 0.0) { d2d = -d_unsigned; }
let dz = abs(p.z) - hd;
let ww = max(vec2<f32>(d2d, dz), vec2<f32>(0.0));
return min(max(d2d, dz), 0.0) + length(ww);
}
";
const HELPER_SDF_TUNNEL: &str = r"fn sdf_tunnel(p: vec3<f32>, w: f32, h2d: f32, hd: f32) -> f32 {
let px = abs(p.x);
let py = p.y;
let dx = px - w;
let dy_rect = abs(py) - h2d;
let d_rect = length(max(vec2<f32>(dx, dy_rect), vec2<f32>(0.0))) + min(max(dx, dy_rect), 0.0);
let d_circle = length(vec2<f32>(px, py - h2d)) - w;
var d2d = d_rect;
if (py > h2d) { d2d = min(d_rect, d_circle); }
let dz = abs(p.z) - hd;
let ww = max(vec2<f32>(d2d, dz), vec2<f32>(0.0));
return min(max(d2d, dz), 0.0) + length(ww);
}
";
const HELPER_SDF_UNEVEN_CAPSULE: &str = r"fn sdf_uneven_capsule(p: vec3<f32>, r1: f32, r2: f32, ch: f32, hd: f32) -> f32 {
let px = abs(p.x);
let hh = ch * 2.0;
let b = (r1 - r2) / hh;
let a = sqrt(max(1.0 - b * b, 0.0));
let k = dot(vec2<f32>(-b, a), vec2<f32>(px, p.y));
var d2d: f32;
if (k < 0.0) {
d2d = length(vec2<f32>(px, p.y)) - r1;
} else if (k > a * hh) {
d2d = length(vec2<f32>(px, p.y - hh)) - r2;
} else {
d2d = dot(vec2<f32>(px, p.y), vec2<f32>(a, b)) - r1;
}
let dz = abs(p.z) - hd;
let ww = max(vec2<f32>(d2d, dz), vec2<f32>(0.0));
return min(max(d2d, dz), 0.0) + length(ww);
}
";
const HELPER_SDF_EGG: &str = r"fn sdf_egg(p: vec3<f32>, ra: f32, rb: f32) -> f32 {
let k = 1.7320508;
let px = length(p.xz);
let py = p.y;
let r = ra - rb;
var d: f32;
if (py < 0.0) {
d = length(vec2<f32>(px, py)) - r;
} else if (k * (px + r) < py) {
d = length(vec2<f32>(px, py - k * r)) - r;
} else {
d = length(vec2<f32>(px + r, py)) - 2.0 * r;
}
return d - rb;
}
";
const HELPER_SDF_ARC_SHAPE: &str = r"fn sdf_arc_shape(p: vec3<f32>, aperture: f32, radius: f32, thickness: f32, h: f32) -> f32 {
let qx = abs(p.x);
let qz = p.z;
let sc = vec2<f32>(sin(aperture), cos(aperture));
var d2d: f32;
if (sc.y * qx > sc.x * qz) {
d2d = length(vec2<f32>(qx, qz) - sc * radius) - thickness;
} else {
d2d = abs(length(vec2<f32>(qx, qz)) - radius) - thickness;
}
let dy = abs(p.y) - h;
let ww = max(vec2<f32>(d2d, dy), vec2<f32>(0.0));
return min(max(d2d, dy), 0.0) + length(ww);
}
";
const HELPER_SDF_MOON: &str = r"fn sdf_moon(p: vec3<f32>, d: f32, ra: f32, rb: f32, h: f32) -> f32 {
let qx = abs(p.x);
let qz = p.z;
let d_outer = length(vec2<f32>(qx, qz)) - ra;
let d_inner = length(vec2<f32>(qx - d, qz)) - rb;
let d2d = max(d_outer, -d_inner);
let dy = abs(p.y) - h;
let ww = max(vec2<f32>(d2d, dy), vec2<f32>(0.0));
return min(max(d2d, dy), 0.0) + length(ww);
}
";
const HELPER_SDF_CROSS_SHAPE: &str = r"fn sdf_cross_shape(p: vec3<f32>, len: f32, th: f32, rr: f32, h: f32) -> f32 {
let qx = abs(p.x);
let qz = abs(p.z);
let dh = vec2<f32>(qx - len, qz - th);
let dv = vec2<f32>(qx - th, qz - len);
let dh_sdf = length(max(dh, vec2<f32>(0.0))) + min(max(dh.x, dh.y), 0.0);
let dv_sdf = length(max(dv, vec2<f32>(0.0))) + min(max(dv.x, dv.y), 0.0);
let d2d = min(dh_sdf, dv_sdf) - rr;
let dy = abs(p.y) - h;
let ww = max(vec2<f32>(d2d, dy), vec2<f32>(0.0));
return min(max(d2d, dy), 0.0) + length(ww);
}
";
const HELPER_SDF_BLOBBY_CROSS: &str = r"fn sdf_blobby_cross(p: vec3<f32>, size: f32, hh: f32) -> f32 {
let he = 0.5;
var pos = abs(vec2<f32>(p.x, p.z) / size);
pos = vec2<f32>(abs(pos.x - pos.y), 1.0 - pos.x - pos.y) * 0.70710678;
let pp = (he - pos.y - 0.25 / he) / (6.0 * he);
let q = pos.x / (he * he * 16.0);
let h = q * q - pp * pp * pp;
var x: f32;
if (h >= 0.0) {
let r = sqrt(h);
x = pow(q + r, 1.0 / 3.0) - pow(abs(q - r), 1.0 / 3.0) * sign(r - q);
} else {
let r = sqrt(pp);
x = 2.0 * r * cos(acos(q / (pp * r)) / 3.0);
}
x = min(x, 0.70710678);
let z = vec2<f32>(x, he * (1.0 - 2.0 * x * x)) - pos;
let d_2d = length(z) * sign(z.y) * size;
let d_y = abs(p.y) - hh;
let w = vec2<f32>(max(d_2d, 0.0), max(d_y, 0.0));
return min(max(d_2d, d_y), 0.0) + length(w);
}
";
const HELPER_SDF_PARABOLA_SEGMENT: &str = r"fn sdf_parabola_segment(p: vec3<f32>, w: f32, ph: f32, hd: f32) -> f32 {
let px = abs(p.x);
let py = p.y;
let ww_sq = w * w;
let y_arch = ph * (1.0 - px * px / ww_sq);
var is_in = false;
if (px <= w && py >= 0.0 && py <= y_arch) { is_in = true; }
var t = clamp(px, 0.0, w);
for (var i = 0; i < 8; i = i + 1) {
let ft = ph * (1.0 - t * t / ww_sq);
let dft = -2.0 * ph * t / ww_sq;
let ex = px - t;
let ey = py - ft;
let f = -ex + ey * dft;
let df = 1.0 + dft * dft + ey * (-2.0 * ph / ww_sq);
if (abs(df) > 1e-10) { t = clamp(t - f / df, 0.0, w); }
}
let cy = ph * (1.0 - t * t / ww_sq);
let d_para = length(vec2<f32>(px - t, py - cy));
var d_base: f32;
if (px <= w) { d_base = abs(py); } else { d_base = length(vec2<f32>(px - w, py)); }
let d_unsigned = min(d_para, d_base);
var d2d = d_unsigned;
if (is_in) { d2d = -d_unsigned; }
let dz = abs(p.z) - hd;
let ext = max(vec2<f32>(d2d, dz), vec2<f32>(0.0));
return min(max(d2d, dz), 0.0) + length(ext);
}
";
const HELPER_SDF_REGULAR_POLYGON: &str = r"fn sdf_regular_polygon(p: vec3<f32>, radius: f32, n: f32, hh: f32) -> f32 {
// IQ regular polygon (exact), XZ plane, radius = circumradius (1.9.2)
let nn = trunc(max(n, 3.0));
let an = 3.14159265358979 / nn;
let acs = vec2<f32>(cos(an), sin(an));
let a0 = alice_atan2(p.x, p.z);
let bn = a0 - 2.0 * an * floor(a0 / (2.0 * an)) - an;
let r = length(p.xz);
var q = vec2<f32>(r * cos(bn), abs(r * sin(bn)));
q = q - radius * acs;
q.y = q.y + clamp(-q.y, 0.0, radius * acs.y);
let d2d = length(q) * sign(q.x);
let dy = abs(p.y) - hh;
let w = max(vec2<f32>(d2d, dy), vec2<f32>(0.0));
return min(max(d2d, dy), 0.0) + length(w);
}
";
const HELPER_SDF_STAR_POLYGON: &str = r"fn sdf_star_polygon(p: vec3<f32>, radius: f32, np: f32, m: f32, hh: f32) -> f32 {
let qx = abs(p.x);
let qz = p.z;
let n = max(np, 3.0);
let an = 3.14159265358979 / n;
let r = length(vec2<f32>(qx, qz));
var angle = alice_atan2(qx, qz);
angle = ((angle % (2.0 * an)) + 2.0 * an) % (2.0 * an);
if (angle > an) { angle = 2.0 * an - angle; }
let pt = vec2<f32>(r * cos(angle), r * sin(angle));
let a = vec2<f32>(radius, 0.0);
let b = vec2<f32>(m * cos(an), m * sin(an));
let ab = b - a;
let ap = pt - a;
let t = clamp(dot(ap, ab) / dot(ab, ab), 0.0, 1.0);
let closest = a + ab * t;
let dist = length(pt - closest);
let cross_val = ab.x * ap.y - ab.y * ap.x;
var d2d = dist;
if (cross_val > 0.0) { d2d = -dist; }
let dy = abs(p.y) - hh;
let w = max(vec2<f32>(d2d, dy), vec2<f32>(0.0));
return min(max(d2d, dy), 0.0) + length(w);
}
";
const HELPER_SDF_STAIRS: &str = r"fn _stair_box(lx: f32, ly: f32, s: f32, sw: f32, sh: f32) -> f32 {
let cx = s * sw + sw * 0.5;
let hy = (s + 1.0) * sh * 0.5;
let dx = abs(lx - cx) - sw * 0.5;
let dy = abs(ly - hy) - hy;
return length(max(vec2<f32>(dx, dy), vec2<f32>(0.0))) + min(max(dx, dy), 0.0);
}
fn sdf_stairs(p: vec3<f32>, sw: f32, sh: f32, ns: f32, hd: f32) -> f32 {
let n = max(ns, 1.0);
let tw = n * sw;
let th = n * sh;
let lx = p.x + tw * 0.5;
let ly = p.y + th * 0.5;
var d2d = 1e30;
for (var si = 0.0; si < n; si = si + 1.0) {
d2d = min(d2d, _stair_box(lx, ly, si, sw, sh));
}
let dz = abs(p.z) - hd;
let w = max(vec2<f32>(d2d, dz), vec2<f32>(0.0));
return min(max(d2d, dz), 0.0) + length(w);
}
";
const HELPER_SDF_TETRAHEDRON: &str = r"fn sdf_tetrahedron(p: vec3<f32>, s: f32) -> f32 {
var d = dot(p, vec3<f32>(0.57735026, 0.57735026, 0.57735026));
d = max(d, dot(p, vec3<f32>(-0.57735026, -0.57735026, 0.57735026)));
d = max(d, dot(p, vec3<f32>(-0.57735026, 0.57735026, -0.57735026)));
d = max(d, dot(p, vec3<f32>(0.57735026, -0.57735026, -0.57735026)));
return d - s;
}
";
const HELPER_SDF_DODECAHEDRON: &str = r"fn sdf_dodecahedron(p: vec3<f32>, r: f32) -> f32 {
var d = abs(dot(p, vec3<f32>(0.0, 0.8506508, 0.5257311)));
d = max(d, abs(dot(p, vec3<f32>(0.0, 0.8506508, -0.5257311))));
d = max(d, abs(dot(p, vec3<f32>(0.8506508, 0.5257311, 0.0))));
d = max(d, abs(dot(p, vec3<f32>(-0.8506508, 0.5257311, 0.0))));
d = max(d, abs(dot(p, vec3<f32>(0.5257311, 0.0, 0.8506508))));
d = max(d, abs(dot(p, vec3<f32>(0.5257311, 0.0, -0.8506508))));
return d - r;
}
";
const HELPER_SDF_ICOSAHEDRON: &str = r"fn sdf_icosahedron(p: vec3<f32>, r: f32) -> f32 {
var d = abs(dot(p, vec3<f32>(0.57735026, 0.57735026, 0.57735026)));
d = max(d, abs(dot(p, vec3<f32>(-0.57735026, 0.57735026, 0.57735026))));
d = max(d, abs(dot(p, vec3<f32>(0.57735026, -0.57735026, 0.57735026))));
d = max(d, abs(dot(p, vec3<f32>(0.57735026, 0.57735026, -0.57735026))));
d = max(d, abs(dot(p, vec3<f32>(0.0, 0.5257311, 0.8506508))));
d = max(d, abs(dot(p, vec3<f32>(0.0, 0.5257311, -0.8506508))));
d = max(d, abs(dot(p, vec3<f32>(0.5257311, 0.8506508, 0.0))));
d = max(d, abs(dot(p, vec3<f32>(0.5257311, -0.8506508, 0.0))));
d = max(d, abs(dot(p, vec3<f32>(0.8506508, 0.0, 0.5257311))));
d = max(d, abs(dot(p, vec3<f32>(-0.8506508, 0.0, 0.5257311))));
return d - r;
}
";
const HELPER_SDF_TRUNCATED_OCTAHEDRON: &str = r"fn sdf_truncated_octahedron(p: vec3<f32>, r: f32) -> f32 {
var d = abs(dot(p, vec3<f32>(1.0, 0.0, 0.0)));
d = max(d, abs(dot(p, vec3<f32>(0.0, 1.0, 0.0))));
d = max(d, abs(dot(p, vec3<f32>(0.0, 0.0, 1.0))));
d = max(d, abs(dot(p, vec3<f32>(0.57735026, 0.57735026, 0.57735026))));
d = max(d, abs(dot(p, vec3<f32>(-0.57735026, 0.57735026, 0.57735026))));
d = max(d, abs(dot(p, vec3<f32>(0.57735026, -0.57735026, 0.57735026))));
d = max(d, abs(dot(p, vec3<f32>(0.57735026, 0.57735026, -0.57735026))));
return d - r;
}
";
const HELPER_SDF_TRUNCATED_ICOSAHEDRON: &str = r"fn sdf_truncated_icosahedron(p: vec3<f32>, r: f32) -> f32 {
var d = abs(dot(p, vec3<f32>(0.57735026, 0.57735026, 0.57735026)));
d = max(d, abs(dot(p, vec3<f32>(-0.57735026, 0.57735026, 0.57735026))));
d = max(d, abs(dot(p, vec3<f32>(0.57735026, -0.57735026, 0.57735026))));
d = max(d, abs(dot(p, vec3<f32>(0.57735026, 0.57735026, -0.57735026))));
d = max(d, abs(dot(p, vec3<f32>(0.0, 0.5257311, 0.8506508))));
d = max(d, abs(dot(p, vec3<f32>(0.0, 0.5257311, -0.8506508))));
d = max(d, abs(dot(p, vec3<f32>(0.5257311, 0.8506508, 0.0))));
d = max(d, abs(dot(p, vec3<f32>(0.5257311, -0.8506508, 0.0))));
d = max(d, abs(dot(p, vec3<f32>(0.8506508, 0.0, 0.5257311))));
d = max(d, abs(dot(p, vec3<f32>(-0.8506508, 0.0, 0.5257311))));
d = max(d, abs(dot(p, vec3<f32>(0.0, 0.8506508, 0.5257311))));
d = max(d, abs(dot(p, vec3<f32>(0.0, 0.8506508, -0.5257311))));
d = max(d, abs(dot(p, vec3<f32>(0.8506508, 0.5257311, 0.0))));
d = max(d, abs(dot(p, vec3<f32>(-0.8506508, 0.5257311, 0.0))));
d = max(d, abs(dot(p, vec3<f32>(0.5257311, 0.0, 0.8506508))));
d = max(d, abs(dot(p, vec3<f32>(0.5257311, 0.0, -0.8506508))));
return d - r;
}
";
const HELPER_BEZIER_DISTANCE_2D: &str = r"fn bezier_distance_2d(q: vec2<f32>, p0: vec2<f32>, p1: vec2<f32>, p2: vec2<f32>) -> f32 {
let a = p1 - p0;
let b = p0 - p1 * 2.0 + p2;
let c = a * 2.0;
let d = p0 - q;
let bb = dot(b, b);
if (bb < 1e-12) {
let ac = p2 - p0;
let t = clamp(-dot(d, ac) / max(dot(ac, ac), 1e-12), 0.0, 1.0);
return length(d + ac * t);
}
let kk = 1.0 / bb;
let kx = kk * dot(a, b);
let ky = kk * (2.0 * dot(a, a) + dot(d, b)) / 3.0;
let kz = kk * dot(d, a);
let p = ky - kx * kx;
let p3 = p * p * p;
let qq = kx * (2.0 * kx * kx - 3.0 * ky) + kz;
let h = qq * qq + 4.0 * p3;
var res: f32;
if (h >= 0.0) {
let hs = sqrt(h);
let x0 = (hs - qq) * 0.5;
let x1 = (-hs - qq) * 0.5;
let u = sign(x0) * pow(abs(x0), 1.0 / 3.0);
let v = sign(x1) * pow(abs(x1), 1.0 / 3.0);
let t = clamp(u + v - kx, 0.0, 1.0);
let e = d + (c + b * t) * t;
res = dot(e, e);
} else {
let z = sqrt(-p);
let v = acos(clamp(qq / (p * z * 2.0), -1.0, 1.0)) / 3.0;
let m = cos(v);
let n = sin(v) * 1.7320508;
let t0 = clamp((m + m) * z - kx, 0.0, 1.0);
let t1 = clamp((-n - m) * z - kx, 0.0, 1.0);
let e0 = d + (c + b * t0) * t0;
let e1 = d + (c + b * t1) * t1;
res = min(dot(e0, e0), dot(e1, e1));
}
return sqrt(res);
}
";
const HELPER_SDF_ELLIPSOID: &str = r"fn _ell_bisect(n: i32, r0: f32, r1: f32, r2: f32, z0: f32, z1: f32, z2: f32, g: f32) -> f32 {
let n0 = r0 * z0;
let n1 = select(0.0, r1 * z1, n > 1);
let n2 = select(0.0, r2 * z2, n > 2);
let zl = select(select(z0, z1, n == 2), z2, n == 3);
var s0 = zl - 1.0;
var s1 = select(sqrt(n0 * n0 + n1 * n1 + n2 * n2) - 1.0, 0.0, g < 0.0);
var s = 0.0;
for (var it = 0; it < 64; it = it + 1) {
s = 0.5 * (s0 + s1);
if (s == s0 || s == s1) { break; }
var gs = -1.0;
let q0 = n0 / (s + r0);
gs = gs + q0 * q0;
if (n > 1) { let q1 = n1 / (s + r1); gs = gs + q1 * q1; }
if (n > 2) { let q2 = n2 / (s + r2); gs = gs + q2 * q2; }
if (gs > 0.0) { s0 = s; } else if (gs < 0.0) { s1 = s; } else { break; }
}
return s;
}
fn _ell_sqr_dist_n(n: i32, e0: f32, e1: f32, e2: f32, y0: f32, y1: f32, y2: f32) -> f32 {
// Eberly SqrDistanceSpecial for the first n sorted axes, assuming y[n-1] > 0
let el = select(select(e0, e1, n == 2), e2, n == 3);
let z0 = y0 / e0;
let z1 = select(0.0, y1 / e1, n > 1);
let z2 = select(0.0, y2 / e2, n > 2);
let g = z0 * z0 + z1 * z1 + z2 * z2 - 1.0;
if (g == 0.0) { return 0.0; }
let r0 = (e0 / el) * (e0 / el);
let r1 = (e1 / el) * (e1 / el);
let r2 = (e2 / el) * (e2 / el);
let sb = _ell_bisect(n, r0, r1, r2, z0, z1, z2, g);
let x0 = r0 * y0 / (sb + r0) - y0;
let x1 = select(0.0, r1 * y1 / (sb + r1) - y1, n > 1);
let x2 = select(0.0, r2 * y2 / (sb + r2) - y2, n > 2);
return x0 * x0 + x1 * x1 + x2 * x2;
}
fn sdf_ellipsoid(p: vec3<f32>, radii: vec3<f32>) -> f32 {
var e = max(radii, vec3<f32>(1e-10));
var y = abs(p);
if (e.x < e.y) { let t = e.x; e.x = e.y; e.y = t; let u = y.x; y.x = y.y; y.y = u; }
if (e.y < e.z) { let t = e.y; e.y = e.z; e.z = t; let u = y.y; y.y = y.z; y.z = u; }
if (e.x < e.y) { let t = e.x; e.x = e.y; e.y = t; let u = y.x; y.x = y.y; y.y = u; }
var d2: f32;
if (y.z > 0.0) {
d2 = _ell_sqr_dist_n(3, e.x, e.y, e.z, y.x, y.y, y.z);
} else {
// y.z == 0: nearest point may leave the plane (Eberly reduction)
let n0 = e.x * y.x; let n1 = e.y * y.y;
let d0 = e.x * e.x - e.z * e.z; let d1 = e.y * e.y - e.z * e.z;
var done = false;
d2 = 0.0;
if (n0 < d0 && n1 < d1) {
let x0 = n0 / d0; let x1 = n1 / d1;
let discr = 1.0 - x0 * x0 - x1 * x1;
if (discr > 0.0) {
let a = e.x * x0 - y.x; let b = e.y * x1 - y.y; let c = e.z * sqrt(discr);
d2 = a * a + b * b + c * c;
done = true;
}
}
if (!done) {
if (y.y > 0.0) {
d2 = _ell_sqr_dist_n(2, e.x, e.y, e.z, y.x, y.y, 0.0);
} else {
// y.y == 0 too: 2-D reduction with y[1] = 0
let m0 = e.x * y.x;
let q0 = e.x * e.x - e.y * e.y;
var done2 = false;
if (m0 < q0) {
let x0 = m0 / q0;
let discr = 1.0 - x0 * x0;
if (discr > 0.0) {
let a = e.x * x0 - y.x; let b = e.y * sqrt(discr);
d2 = a * a + b * b;
done2 = true;
}
}
if (!done2) { let a = y.x - e.x; d2 = a * a; }
}
}
}
let inside = (y.x / e.x) * (y.x / e.x) + (y.y / e.y) * (y.y / e.y) + (y.z / e.z) * (y.z / e.z) < 1.0;
let d = sqrt(max(d2, 0.0));
return select(d, -d, inside);
}
";
const HELPER_SDF_HELIX: &str = r"fn sdf_helix(p: vec3<f32>, major_r: f32, minor_r: f32, pitch: f32, hh: f32) -> f32 {
let tau = 6.28318530717959;
let r = length(vec2<f32>(p.x, p.z));
let theta = select(0.0, alice_atan2(p.z, p.x), r > 0.0);
let py = p.y;
let c = pitch / tau;
let two_rr = 2.0 * r * major_r;
let k = floor((py - theta * c) / pitch + 0.5);
var best = 1e30;
for (var dk = -1.0; dk <= 1.0; dk = dk + 1.0) {
var phi = theta + (k + dk) * tau;
for (var it = 0; it < 6; it = it + 1) {
let s = sin(phi - theta);
let co = cos(phi - theta);
let dy = py - c * phi;
let f1 = two_rr * s - 2.0 * c * dy;
let f2 = max(two_rr * co + 2.0 * c * c, 1e-6);
phi = phi - clamp(f1 / f2, -1.5707963, 1.5707963);
}
let co = cos(phi - theta);
let dy = py - c * phi;
let d2 = dy * dy + max(r * r + major_r * major_r - two_rr * co, 0.0);
best = min(best, d2);
}
let d_tube = sqrt(best) - minor_r;
let d_cap = abs(py) - hh;
return max(d_tube, d_cap);
}
";
#[cfg(test)]
mod tests {
use super::*;
use glam::Vec3;
#[test]
fn test_transpile_sphere() {
let sphere = SdfNode::Sphere { radius: 1.0 };
let shader = WgslShader::transpile(&sphere, TranspileMode::Hardcoded);
assert!(shader.source.contains("fn sdf_eval"));
assert!(shader.source.contains("length(p)"));
assert!(shader.source.contains("1.0"));
assert!(shader.param_layout.is_empty());
}
#[test]
fn test_transpile_sphere_dynamic() {
let sphere = SdfNode::Sphere { radius: 1.5 };
let shader = WgslShader::transpile(&sphere, TranspileMode::Dynamic);
assert!(shader.source.contains("fn sdf_eval"));
assert!(shader.source.contains("sdf_params.data[0].x"));
assert_eq!(shader.param_layout, vec![1.5]);
assert_eq!(shader.mode, TranspileMode::Dynamic);
}
#[test]
fn test_transpile_box() {
let box3d = SdfNode::Box3d {
half_extents: Vec3::new(1.0, 0.5, 0.5),
};
let shader = WgslShader::transpile(&box3d, TranspileMode::Hardcoded);
assert!(shader.source.contains("fn sdf_eval"));
assert!(shader.source.contains("abs(p)"));
}
#[test]
fn test_transpile_box_dynamic() {
let box3d = SdfNode::Box3d {
half_extents: Vec3::new(1.0, 2.0, 3.0),
};
let shader = WgslShader::transpile(&box3d, TranspileMode::Dynamic);
assert!(shader.source.contains("sdf_params.data[0].x")); assert!(shader.source.contains("sdf_params.data[0].y")); assert!(shader.source.contains("sdf_params.data[0].z")); assert_eq!(shader.param_layout, vec![1.0, 2.0, 3.0]);
}
#[test]
fn test_transpile_union() {
let shape = SdfNode::Sphere { radius: 1.0 }.union(
SdfNode::Box3d {
half_extents: Vec3::splat(0.5),
}
.translate(2.0, 0.0, 0.0),
);
let shader = WgslShader::transpile(&shape, TranspileMode::Hardcoded);
assert!(shader.source.contains("fn sdf_eval"));
assert!(shader.source.contains("min("));
}
#[test]
fn test_transpile_smooth_union() {
let shape = SdfNode::Sphere { radius: 1.0 }.smooth_union(
SdfNode::Cylinder {
radius: 0.5,
half_height: 1.0,
},
0.2,
);
let shader = WgslShader::transpile(&shape, TranspileMode::Hardcoded);
assert!(shader.source.contains("fn sdf_eval"));
assert!(shader.source.contains("* 5.0")); assert!(shader.source.contains("* 0.25"));
assert_eq!(shader.helper_count, 0);
}
#[test]
fn test_transpile_smooth_union_dynamic() {
let shape = SdfNode::Sphere { radius: 1.0 }.smooth_union(
SdfNode::Cylinder {
radius: 0.5,
half_height: 1.0,
},
0.2,
);
let shader = WgslShader::transpile(&shape, TranspileMode::Dynamic);
assert!(shader.source.contains("sdf_params.data["));
assert!(shader.param_layout.len() >= 5);
}
#[test]
fn test_transpile_rotate() {
let shape =
SdfNode::Sphere { radius: 1.0 }.rotate_euler(std::f32::consts::FRAC_PI_2, 0.0, 0.0);
let shader = WgslShader::transpile(&shape, TranspileMode::Hardcoded);
assert!(shader.source.contains("fn quat_rotate"));
assert!(shader.source.contains("quat_rotate("));
}
#[test]
fn test_compute_shader_generation() {
let sphere = SdfNode::Sphere { radius: 1.0 };
let shader = WgslShader::transpile(&sphere, TranspileMode::Hardcoded);
let compute = shader.to_compute_shader();
assert!(compute.contains("@compute"));
assert!(compute.contains("@workgroup_size("));
assert!(compute.contains("input_points"));
assert!(compute.contains("output_distances"));
assert!(!compute.contains("sdf_params"));
}
#[test]
fn test_compute_shader_dynamic() {
let sphere = SdfNode::Sphere { radius: 1.0 };
let shader = WgslShader::transpile(&sphere, TranspileMode::Dynamic);
let compute = shader.to_compute_shader();
assert!(compute.contains("@compute"));
assert!(compute.contains("sdf_params"));
assert!(compute.contains("@group(0) @binding(3)"));
assert!(compute.contains("array<vec4<f32>, 1024>"));
}
#[test]
fn test_compute_shader_with_normals() {
let sphere = SdfNode::Sphere { radius: 1.0 };
let shader = WgslShader::transpile(&sphere, TranspileMode::Dynamic);
let compute = shader.to_compute_shader_with_normals();
assert!(compute.contains("estimate_normal"));
assert!(compute.contains("output[idx].dist"));
assert!(compute.contains("output[idx].nx"));
assert!(compute.contains("sdf_params"));
}
#[test]
fn test_extract_params() {
let shape = SdfNode::Sphere { radius: 1.5 }.translate(1.0, 2.0, 3.0);
let params = WgslShader::extract_params(&shape);
assert_eq!(params.len(), 4);
assert_eq!(params[0], 1.0); assert_eq!(params[1], 2.0); assert_eq!(params[2], 3.0); assert_eq!(params[3], 1.5); }
#[test]
fn test_extract_params_matches_transpile() {
let shape = SdfNode::Sphere { radius: 1.0 }.smooth_union(
SdfNode::Box3d {
half_extents: Vec3::new(0.5, 0.5, 0.5),
},
0.3,
);
let shader = WgslShader::transpile(&shape, TranspileMode::Dynamic);
let extracted = WgslShader::extract_params(&shape);
assert_eq!(shader.param_layout, extracted);
}
#[test]
fn test_transpile_complex() {
let shape = SdfNode::Sphere { radius: 1.0 }
.smooth_union(
SdfNode::Cylinder {
radius: 0.3,
half_height: 0.75,
}
.rotate_euler(std::f32::consts::FRAC_PI_2, 0.0, 0.0),
0.2,
)
.subtract(SdfNode::Box3d {
half_extents: Vec3::splat(0.4),
})
.translate(0.5, 0.0, 0.0);
let shader = WgslShader::transpile(&shape, TranspileMode::Hardcoded);
assert!(shader.source.contains("fn quat_rotate"));
assert!(shader.source.contains("quat_rotate("));
assert!(shader.source.contains("* 5.0")); assert_eq!(shader.helper_count, 1);
assert!(shader.source.contains("fn sdf_eval"));
assert!(shader.source.contains("return"));
}
#[test]
fn test_dynamic_no_constant_fold() {
let shape = SdfNode::Sphere { radius: 1.0 }.smooth_union(
SdfNode::Sphere { radius: 0.5 },
0.0, );
let hardcoded = WgslShader::transpile(&shape, TranspileMode::Hardcoded);
assert!(hardcoded.source.contains("min("));
assert!(!hardcoded.source.contains("0.25"));
let dynamic = WgslShader::transpile(&shape, TranspileMode::Dynamic);
assert!(dynamic.source.contains("0.25")); assert!(dynamic.source.contains("sdf_params.data["));
}
#[test]
fn test_transpile_all_variants_exhaustive() {
use glam::Vec2;
let primitives: Vec<(&str, SdfNode)> = vec![
("sphere", SdfNode::sphere(1.0)),
("box3d", SdfNode::box3d(1.0, 0.5, 0.5)),
("cylinder", SdfNode::cylinder(0.5, 1.0)),
("torus", SdfNode::torus(1.0, 0.3)),
(
"plane",
SdfNode::Plane {
normal: Vec3::Y,
distance: 0.0,
},
),
("capsule", SdfNode::capsule(Vec3::ZERO, Vec3::Y, 0.3)),
("cone", SdfNode::cone(0.5, 1.0)),
("ellipsoid", SdfNode::ellipsoid(1.0, 0.5, 0.3)),
("rounded_cone", SdfNode::rounded_cone(0.5, 0.2, 1.0)),
("pyramid", SdfNode::pyramid(1.0)),
("octahedron", SdfNode::octahedron(1.0)),
("hex_prism", SdfNode::hex_prism(0.5, 1.0)),
("link", SdfNode::link(0.5, 0.3, 0.1)),
("rounded_box", SdfNode::rounded_box(1.0, 0.5, 0.5, 0.1)),
("capped_cone", SdfNode::capped_cone(1.0, 0.5, 0.2)),
("capped_torus", SdfNode::capped_torus(1.0, 0.3, 1.0)),
("rounded_cylinder", SdfNode::rounded_cylinder(0.5, 0.1, 1.0)),
("triangular_prism", SdfNode::triangular_prism(0.5, 1.0)),
("cut_sphere", SdfNode::cut_sphere(1.0, 0.3)),
(
"cut_hollow_sphere",
SdfNode::cut_hollow_sphere(1.0, 0.3, 0.1),
),
("death_star", SdfNode::death_star(1.0, 0.8, 0.5)),
("solid_angle", SdfNode::solid_angle(0.5, 1.0)),
("rhombus", SdfNode::rhombus(0.5, 0.3, 1.0, 0.05)),
("horseshoe", SdfNode::horseshoe(1.0, 0.5, 0.3, 0.1, 0.05)),
("vesica", SdfNode::vesica(1.0, 0.5)),
("infinite_cylinder", SdfNode::infinite_cylinder(0.5)),
("infinite_cone", SdfNode::infinite_cone(0.5)),
("gyroid", SdfNode::gyroid(1.0, 0.1)),
("schwarz_p", SdfNode::schwarz_p(1.0, 0.1)),
("heart", SdfNode::heart(1.0)),
("tube", SdfNode::tube(0.5, 0.1, 1.0)),
("barrel", SdfNode::barrel(0.5, 1.0, 0.2)),
("diamond", SdfNode::diamond(0.5, 1.0)),
("egg", SdfNode::egg(1.0, 0.5)),
(
"superellipsoid",
SdfNode::superellipsoid(1.0, 0.5, 0.5, 0.5, 0.5),
),
("rounded_x", SdfNode::rounded_x(0.5, 0.1, 1.0)),
("pie", SdfNode::pie(1.0, 0.5, 1.0)),
("trapezoid", SdfNode::trapezoid(0.5, 0.3, 1.0, 0.5)),
("parallelogram", SdfNode::parallelogram(0.5, 1.0, 0.2, 0.5)),
("tunnel", SdfNode::tunnel(0.5, 1.0, 0.5)),
(
"uneven_capsule",
SdfNode::uneven_capsule(0.3, 0.5, 1.0, 0.5),
),
("arc_shape", SdfNode::arc_shape(1.0, 0.5, 0.1, 1.0)),
("moon", SdfNode::moon(0.5, 1.0, 0.8, 1.0)),
("cross_shape", SdfNode::cross_shape(0.5, 0.1, 0.05, 1.0)),
("blobby_cross", SdfNode::blobby_cross(0.5, 1.0)),
("parabola_segment", SdfNode::parabola_segment(0.5, 1.0, 0.5)),
("regular_polygon", SdfNode::regular_polygon(0.5, 6, 1.0)),
("star_polygon", SdfNode::star_polygon(0.5, 5, 2.0, 1.0)),
(
"chamfered_cube",
SdfNode::chamfered_cube(0.5, 0.5, 0.5, 0.1),
),
("stairs", SdfNode::stairs(0.3, 0.2, 5, 0.5)),
("helix", SdfNode::helix(1.0, 0.1, 0.5, 2.0)),
("tetrahedron", SdfNode::tetrahedron(1.0)),
("dodecahedron", SdfNode::dodecahedron(1.0)),
("icosahedron", SdfNode::icosahedron(1.0)),
("truncated_octahedron", SdfNode::truncated_octahedron(1.0)),
("truncated_icosahedron", SdfNode::truncated_icosahedron(1.0)),
("box_frame", SdfNode::box_frame(Vec3::splat(1.0), 0.1)),
("diamond_surface", SdfNode::diamond_surface(1.0, 0.1)),
("neovius", SdfNode::neovius(1.0, 0.1)),
("lidinoid", SdfNode::lidinoid(1.0, 0.1)),
("iwp", SdfNode::iwp(1.0, 0.1)),
("frd", SdfNode::frd(1.0, 0.1)),
("fischer_koch_s", SdfNode::fischer_koch_s(1.0, 0.1)),
("pmy", SdfNode::pmy(1.0, 0.1)),
("triangle", SdfNode::triangle(Vec3::X, Vec3::Y, Vec3::Z)),
(
"bezier",
SdfNode::bezier(Vec3::ZERO, Vec3::new(0.5, 1.0, 0.0), Vec3::X, 0.1),
),
("circle_2d", SdfNode::circle_2d(0.5, 1.0)),
("rect_2d", SdfNode::rect_2d(0.5, 0.3, 1.0)),
(
"segment_2d",
SdfNode::segment_2d(0.0, 0.0, 1.0, 1.0, 0.1, 1.0),
),
(
"polygon_2d",
SdfNode::polygon_2d(
vec![
Vec2::new(0.0, 0.0),
Vec2::new(1.0, 0.0),
Vec2::new(0.5, 1.0),
],
1.0,
),
),
(
"rounded_rect_2d",
SdfNode::rounded_rect_2d(0.5, 0.3, 0.1, 1.0),
),
("annular_2d", SdfNode::annular_2d(0.5, 0.1, 1.0)),
];
for (name, prim) in &primitives {
let shader = WgslShader::transpile(prim, TranspileMode::Hardcoded);
assert!(
shader.source.contains("fn sdf_eval"),
"WGSL transpile failed for primitive '{}': missing sdf_eval",
name,
);
}
let a = SdfNode::sphere(1.0);
let b = SdfNode::box3d(0.5, 0.5, 0.5);
let operations: Vec<(&str, SdfNode)> = vec![
("union", a.clone().union(b.clone())),
("intersection", a.clone().intersection(b.clone())),
("subtract", a.clone().subtract(b.clone())),
("smooth_union", a.clone().smooth_union(b.clone(), 0.2)),
(
"smooth_intersection",
a.clone().smooth_intersection(b.clone(), 0.2),
),
("smooth_subtract", a.clone().smooth_subtract(b.clone(), 0.2)),
("chamfer_union", a.clone().chamfer_union(b.clone(), 0.2)),
(
"chamfer_intersection",
a.clone().chamfer_intersection(b.clone(), 0.2),
),
(
"chamfer_subtract",
a.clone().chamfer_subtract(b.clone(), 0.2),
),
("stairs_union", a.clone().stairs_union(b.clone(), 0.2, 4.0)),
(
"stairs_intersection",
a.clone().stairs_intersection(b.clone(), 0.2, 4.0),
),
(
"stairs_subtract",
a.clone().stairs_subtract(b.clone(), 0.2, 4.0),
),
(
"columns_union",
a.clone().columns_union(b.clone(), 0.2, 4.0),
),
(
"columns_intersection",
a.clone().columns_intersection(b.clone(), 0.2, 4.0),
),
(
"columns_subtract",
a.clone().columns_subtract(b.clone(), 0.2, 4.0),
),
("xor", a.clone().xor(b.clone())),
("morph", a.clone().morph(b.clone(), 0.5)),
("pipe", a.clone().pipe(b.clone(), 0.2)),
("engrave", a.clone().engrave(b.clone(), 0.1)),
("groove", a.clone().groove(b.clone(), 0.2, 0.1)),
("tongue", a.clone().tongue(b.clone(), 0.2, 0.1)),
(
"exp_smooth_union",
a.clone().exp_smooth_union(b.clone(), 0.2),
),
(
"exp_smooth_intersection",
a.clone().exp_smooth_intersection(b.clone(), 0.2),
),
("exp_smooth_subtract", a.exp_smooth_subtract(b, 0.2)),
];
for (name, op) in &operations {
let shader = WgslShader::transpile(op, TranspileMode::Hardcoded);
assert!(
shader.source.contains("fn sdf_eval"),
"WGSL transpile failed for operation '{}': missing sdf_eval",
name,
);
}
let s = SdfNode::sphere(1.0);
let modifiers: Vec<(&str, SdfNode)> = vec![
("translate", s.clone().translate(1.0, 0.0, 0.0)),
("rotate", s.clone().rotate_euler(0.5, 0.0, 0.0)),
("scale", s.clone().scale(2.0)),
("scale_xyz", s.clone().scale_xyz(1.0, 2.0, 0.5)),
("twist", s.clone().twist(1.0)),
("bend", s.clone().bend(0.5)),
("round", s.clone().round(0.1)),
("onion", s.clone().onion(0.1)),
("elongate", s.clone().elongate(0.5, 0.0, 0.0)),
("repeat_infinite", s.clone().repeat_infinite(3.0, 3.0, 3.0)),
("mirror", s.clone().mirror(true, false, false)),
("octant_mirror", s.clone().octant_mirror()),
("revolution", s.clone().revolution(0.5)),
("extrude", s.clone().extrude(0.5)),
("noise", s.clone().noise(0.1, 2.0, 42)),
("taper", s.clone().taper(0.5)),
("displacement", s.clone().displacement(0.1)),
("polar_repeat", s.clone().polar_repeat(6)),
("shear", s.clone().shear(0.1, 0.0, 0.0)),
("animated", s.clone().animated(1.0, 0.5)),
("with_material", s.with_material(1)),
];
for (name, m) in &modifiers {
let shader = WgslShader::transpile(m, TranspileMode::Hardcoded);
assert!(
shader.source.contains("fn sdf_eval"),
"WGSL transpile failed for modifier '{}': missing sdf_eval",
name,
);
}
}
}