use serde::{Deserialize, Serialize};
fn default_puddle_bevel() -> f32 {
0.08
}
fn default_puddle_meniscus() -> f32 {
0.01
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "primitive", rename_all = "camelCase")]
pub enum SdfPrimitive {
Sphere {
radius: f32,
},
Box {
size: [f32; 3],
},
RoundBox {
size: [f32; 3],
radius: f32,
},
Ellipsoid {
radii: [f32; 3],
},
RoundBoxShell {
size: [f32; 3],
radius: f32,
thickness: f32,
},
Cylinder {
radius: f32,
height: f32,
},
Capsule {
radius: f32,
height: f32,
},
Torus {
major_radius: f32,
minor_radius: f32,
},
Cone {
angle: f32,
height: f32,
},
Plane {
normal: [f32; 3],
offset: f32,
},
TaperedCapsule {
a: [f32; 3],
b: [f32; 3],
radius_a: f32,
radius_b: f32,
},
TubePath {
points: Vec<[f32; 3]>,
radii: Vec<f32>,
k: f32,
},
InfRepeat {
spacing: [f32; 3],
},
Puddle {
radius: f32,
height: f32,
noise_freq: f32,
noise_amp: f32,
#[serde(default = "default_puddle_bevel")]
bevel: f32,
#[serde(default = "default_puddle_meniscus")]
meniscus: f32,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "op", rename_all = "camelCase")]
pub enum SdfOp {
Union,
Intersection,
Difference,
SmoothUnion { k: f32 },
SmoothIntersection { k: f32 },
SmoothDifference { k: f32 },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "transform", rename_all = "camelCase")]
pub enum SdfTransform {
Translate { offset: [f32; 3] },
Rotate { angles: [f32; 3] },
Scale { factor: [f32; 3] },
Twist { strength: f32 },
Bend { strength: f32 },
Elongate { amount: [f32; 3] },
Round { radius: f32 },
Shell { thickness: f32 },
Repeat { spacing: [f32; 3], count: [u32; 3] },
Mirror { axis: [f32; 3] },
Displace {
frequency: f32,
amplitude: f32,
octaves: u32,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SdfMaterial {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub slot: Option<String>,
#[serde(default = "default_color")]
pub color: [f32; 3],
#[serde(default = "default_roughness")]
pub roughness: f32,
#[serde(default)]
pub metallic: f32,
#[serde(default)]
pub emission: [f32; 3],
#[serde(default = "default_ior")]
pub ior: f32,
}
fn default_color() -> [f32; 3] {
[0.8, 0.8, 0.8]
}
fn default_roughness() -> f32 {
0.5
}
fn default_ior() -> f32 {
1.5
}
impl Default for SdfMaterial {
fn default() -> Self {
Self {
slot: None,
color: default_color(),
roughness: default_roughness(),
metallic: 0.0,
emission: [0.0; 3],
ior: default_ior(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SceneSettings {
#[serde(default = "default_camera_pos")]
pub camera_pos: [f32; 3],
#[serde(default)]
pub camera_target: [f32; 3],
#[serde(default = "default_fov")]
pub fov: f32,
#[serde(default = "default_max_steps")]
pub max_steps: u32,
#[serde(default = "default_max_dist")]
pub max_dist: f32,
#[serde(default = "default_epsilon")]
pub epsilon: f32,
#[serde(default = "default_bg")]
pub background: [f32; 3],
#[serde(default = "default_ambient")]
pub ambient: f32,
#[serde(default = "default_light_dir")]
pub light_dir: [f32; 3],
#[serde(default = "default_light_color")]
pub light_color: [f32; 3],
#[serde(default)]
pub soft_shadows: bool,
#[serde(default = "default_shadow_k")]
pub shadow_k: f32,
#[serde(default = "default_true")]
pub ao: bool,
#[serde(default = "default_width")]
pub width: u32,
#[serde(default = "default_height")]
pub height: u32,
#[serde(default)]
pub custom_shade_wgsl: String,
#[serde(default)]
pub time: f32,
}
fn default_camera_pos() -> [f32; 3] {
[3.0, 2.0, 4.0]
}
fn default_fov() -> f32 {
45.0
}
fn default_max_steps() -> u32 {
128
}
fn default_max_dist() -> f32 {
100.0
}
fn default_epsilon() -> f32 {
0.001
}
fn default_bg() -> [f32; 3] {
[0.05, 0.05, 0.08]
}
fn default_ambient() -> f32 {
0.15
}
fn default_light_dir() -> [f32; 3] {
[0.577, 0.577, -0.577]
}
fn default_light_color() -> [f32; 3] {
[1.0, 0.95, 0.9]
}
fn default_shadow_k() -> f32 {
16.0
}
fn default_true() -> bool {
true
}
fn default_width() -> u32 {
512
}
fn default_height() -> u32 {
512
}
impl Default for SceneSettings {
fn default() -> Self {
Self {
camera_pos: default_camera_pos(),
camera_target: [0.0; 3],
fov: default_fov(),
max_steps: default_max_steps(),
max_dist: default_max_dist(),
epsilon: default_epsilon(),
background: default_bg(),
ambient: default_ambient(),
light_dir: default_light_dir(),
light_color: default_light_color(),
soft_shadows: false,
shadow_k: default_shadow_k(),
ao: true,
width: default_width(),
height: default_height(),
custom_shade_wgsl: String::new(),
time: 0.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum SdfNode {
Primitive {
#[serde(flatten)]
shape: SdfPrimitive,
#[serde(default)]
material: Option<SdfMaterial>,
},
Operation {
#[serde(flatten)]
op: SdfOp,
left: Box<SdfNode>,
right: Box<SdfNode>,
},
Transform {
#[serde(flatten)]
transform: SdfTransform,
child: Box<SdfNode>,
},
Material {
material: SdfMaterial,
child: Box<SdfNode>,
},
Ref { name: String },
Scene {
root: Box<SdfNode>,
settings: SceneSettings,
},
}
impl SdfNode {
pub fn sphere(radius: f32) -> Self {
SdfNode::Primitive {
shape: SdfPrimitive::Sphere { radius },
material: None,
}
}
pub fn cube(size: f32) -> Self {
SdfNode::Primitive {
shape: SdfPrimitive::Box { size: [size; 3] },
material: None,
}
}
pub fn box3(size: [f32; 3]) -> Self {
SdfNode::Primitive {
shape: SdfPrimitive::Box { size },
material: None,
}
}
pub fn round_box(size: [f32; 3], radius: f32) -> Self {
SdfNode::Primitive {
shape: SdfPrimitive::RoundBox { size, radius },
material: None,
}
}
pub fn ellipsoid(radii: [f32; 3]) -> Self {
SdfNode::Primitive {
shape: SdfPrimitive::Ellipsoid { radii },
material: None,
}
}
pub fn round_box_shell(size: [f32; 3], radius: f32, thickness: f32) -> Self {
SdfNode::Primitive {
shape: SdfPrimitive::RoundBoxShell {
size,
radius,
thickness,
},
material: None,
}
}
pub fn cylinder(radius: f32, height: f32) -> Self {
SdfNode::Primitive {
shape: SdfPrimitive::Cylinder { radius, height },
material: None,
}
}
pub fn torus(major: f32, minor: f32) -> Self {
SdfNode::Primitive {
shape: SdfPrimitive::Torus {
major_radius: major,
minor_radius: minor,
},
material: None,
}
}
pub fn capsule(radius: f32, height: f32) -> Self {
SdfNode::Primitive {
shape: SdfPrimitive::Capsule { radius, height },
material: None,
}
}
pub fn plane(normal: [f32; 3], offset: f32) -> Self {
SdfNode::Primitive {
shape: SdfPrimitive::Plane { normal, offset },
material: None,
}
}
pub fn inf_repeat(spacing: [f32; 3]) -> Self {
SdfNode::Primitive {
shape: SdfPrimitive::InfRepeat { spacing },
material: None,
}
}
pub fn puddle(radius: f32, height: f32, noise_freq: f32, noise_amp: f32) -> Self {
Self::puddle_profiled(
radius,
height,
noise_freq,
noise_amp,
(radius * 0.12).max(height * 6.0),
height * 0.35,
)
}
pub fn puddle_profiled(
radius: f32,
height: f32,
noise_freq: f32,
noise_amp: f32,
bevel: f32,
meniscus: f32,
) -> Self {
SdfNode::Primitive {
shape: SdfPrimitive::Puddle {
radius,
height,
noise_freq,
noise_amp,
bevel,
meniscus,
},
material: None,
}
}
pub fn cone(angle: f32, height: f32) -> Self {
SdfNode::Primitive {
shape: SdfPrimitive::Cone { angle, height },
material: None,
}
}
pub fn tube_path(points: Vec<[f32; 3]>, radii: Vec<f32>, k: f32) -> Self {
SdfNode::Primitive {
shape: SdfPrimitive::TubePath { points, radii, k },
material: None,
}
}
pub fn tapered_capsule(a: [f32; 3], b: [f32; 3], radius_a: f32, radius_b: f32) -> Self {
SdfNode::Primitive {
shape: SdfPrimitive::TaperedCapsule {
a,
b,
radius_a,
radius_b,
},
material: None,
}
}
pub fn union(a: SdfNode, b: SdfNode) -> Self {
SdfNode::Operation {
op: SdfOp::Union,
left: Box::new(a),
right: Box::new(b),
}
}
pub fn intersection(a: SdfNode, b: SdfNode) -> Self {
SdfNode::Operation {
op: SdfOp::Intersection,
left: Box::new(a),
right: Box::new(b),
}
}
pub fn difference(a: SdfNode, b: SdfNode) -> Self {
SdfNode::Operation {
op: SdfOp::Difference,
left: Box::new(a),
right: Box::new(b),
}
}
pub fn smooth_union(a: SdfNode, b: SdfNode, k: f32) -> Self {
SdfNode::Operation {
op: SdfOp::SmoothUnion { k },
left: Box::new(a),
right: Box::new(b),
}
}
pub fn smooth_intersection(a: SdfNode, b: SdfNode, k: f32) -> Self {
SdfNode::Operation {
op: SdfOp::SmoothIntersection { k },
left: Box::new(a),
right: Box::new(b),
}
}
pub fn smooth_difference(a: SdfNode, b: SdfNode, k: f32) -> Self {
SdfNode::Operation {
op: SdfOp::SmoothDifference { k },
left: Box::new(a),
right: Box::new(b),
}
}
pub fn translate(self, offset: [f32; 3]) -> Self {
SdfNode::Transform {
transform: SdfTransform::Translate { offset },
child: Box::new(self),
}
}
pub fn rotate(self, angles: [f32; 3]) -> Self {
SdfNode::Transform {
transform: SdfTransform::Rotate { angles },
child: Box::new(self),
}
}
pub fn scale(self, factor: [f32; 3]) -> Self {
SdfNode::Transform {
transform: SdfTransform::Scale { factor },
child: Box::new(self),
}
}
pub fn scale_uniform(self, s: f32) -> Self {
self.scale([s, s, s])
}
pub fn twist(self, strength: f32) -> Self {
SdfNode::Transform {
transform: SdfTransform::Twist { strength },
child: Box::new(self),
}
}
pub fn bend(self, strength: f32) -> Self {
SdfNode::Transform {
transform: SdfTransform::Bend { strength },
child: Box::new(self),
}
}
pub fn round(self, radius: f32) -> Self {
SdfNode::Transform {
transform: SdfTransform::Round { radius },
child: Box::new(self),
}
}
pub fn shell(self, thickness: f32) -> Self {
SdfNode::Transform {
transform: SdfTransform::Shell { thickness },
child: Box::new(self),
}
}
pub fn repeat(self, spacing: [f32; 3], count: [u32; 3]) -> Self {
SdfNode::Transform {
transform: SdfTransform::Repeat { spacing, count },
child: Box::new(self),
}
}
pub fn mirror(self, axis: [f32; 3]) -> Self {
SdfNode::Transform {
transform: SdfTransform::Mirror { axis },
child: Box::new(self),
}
}
pub fn displace(self, frequency: f32, amplitude: f32, octaves: u32) -> Self {
SdfNode::Transform {
transform: SdfTransform::Displace {
frequency,
amplitude,
octaves,
},
child: Box::new(self),
}
}
pub fn with_material(self, material: SdfMaterial) -> Self {
SdfNode::Material {
material,
child: Box::new(self),
}
}
pub fn with_color(self, r: f32, g: f32, b: f32) -> Self {
self.with_material(SdfMaterial {
color: [r, g, b],
..Default::default()
})
}
pub fn into_scene(self) -> Self {
SdfNode::Scene {
root: Box::new(self),
settings: SceneSettings::default(),
}
}
pub fn into_scene_with(self, settings: SceneSettings) -> Self {
SdfNode::Scene {
root: Box::new(self),
settings,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builder_api() {
let scene = SdfNode::smooth_union(
SdfNode::sphere(1.0)
.translate([0.0, 0.5, 0.0])
.with_color(1.0, 0.2, 0.1),
SdfNode::cube(1.0).twist(0.5),
0.3,
)
.into_scene();
let json = serde_json::to_string_pretty(&scene).unwrap();
assert!(json.contains("smoothUnion"));
assert!(json.contains("sphere"));
assert!(json.contains("\"twist\""));
}
#[test]
fn test_roundtrip() {
let node = SdfNode::smooth_union(
SdfNode::sphere(1.0).translate([1.0, 0.0, 0.0]),
SdfNode::torus(1.5, 0.3),
0.4,
);
let json = serde_json::to_string(&node).unwrap();
let _parsed: SdfNode = serde_json::from_str(&json).unwrap();
}
#[test]
fn test_default_scene_settings() {
let s = SceneSettings::default();
assert_eq!(s.max_steps, 128);
assert_eq!(s.width, 512);
assert!(s.ao);
}
}