proof-engine 0.2.3

Real-time graphics from math: glyphs and particles moved by ODEs, strange attractors and force fields, drawn with HDR bloom on OpenGL.
Documentation

// ── Material System ───────────────────────────────────────────────────────────

#[derive(Clone, Debug)]
pub struct Material {
    pub id: u32,
    pub name: String,
    pub shader_program_id: u32,
    pub textures: Vec<MaterialTexture>,
    pub properties: HashMap<String, MaterialProperty>,
    pub pipeline: PipelineState,
    pub render_queue: u32,
    pub cast_shadows: bool,
    pub receive_shadows: bool,
    pub instanced: bool,
    pub double_sided: bool,
}

#[derive(Clone, Debug)]
pub struct MaterialTexture {
    pub name: String,
    pub texture_id: u32,
    pub slot: u32,
    pub sampler_type: SamplerType,
    pub uv_channel: u32,
}

#[derive(Clone, Debug)]
pub enum MaterialProperty {
    Float(f32),
    Vec2([f32; 2]),
    Vec3([f32; 3]),
    Vec4([f32; 4]),
    Int(i32),
    Bool(bool),
    Color([f32; 4]),
}

#[derive(Clone, Debug, PartialEq)]
pub enum SamplerType { Linear, Nearest, LinearMipmap, Trilinear, Anisotropic }

impl Material {
    pub fn new(id: u32, name: impl Into<String>, shader_id: u32) -> Self {
        Self { id, name: name.into(), shader_program_id: shader_id, textures: Vec::new(), properties: HashMap::new(), pipeline: PipelineState::default(), render_queue: 2000, cast_shadows: true, receive_shadows: true, instanced: false, double_sided: false }
    }
    pub fn add_texture(&mut self, name: impl Into<String>, tex_id: u32, slot: u32) {
        self.textures.push(MaterialTexture { name: name.into(), texture_id: tex_id, slot, sampler_type: SamplerType::Trilinear, uv_channel: 0 });
    }
    pub fn set_float(&mut self, name: impl Into<String>, value: f32) { self.properties.insert(name.into(), MaterialProperty::Float(value)); }
    pub fn set_color(&mut self, name: impl Into<String>, r: f32, g: f32, b: f32, a: f32) { self.properties.insert(name.into(), MaterialProperty::Color([r, g, b, a])); }
    pub fn set_vec4(&mut self, name: impl Into<String>, v: [f32; 4]) { self.properties.insert(name.into(), MaterialProperty::Vec4(v)); }
    pub fn is_transparent(&self) -> bool { self.pipeline.is_transparent() }
    pub fn is_opaque(&self) -> bool { !self.is_transparent() }
    pub fn texture_count(&self) -> usize { self.textures.len() }
    pub fn property_count(&self) -> usize { self.properties.len() }
    pub fn make_transparent(&mut self) { self.pipeline = PipelineState::transparent(); self.render_queue = 3000; }
    pub fn make_additive(&mut self) { self.pipeline = PipelineState::additive(); self.render_queue = 3500; }
}

#[derive(Clone, Debug)]
pub struct MaterialLibrary {
    pub materials: HashMap<u32, Material>,
    pub next_id: u32,
    pub default_material_id: u32,
}

impl MaterialLibrary {
    pub fn new() -> Self { Self { materials: HashMap::new(), next_id: 1, default_material_id: 0 } }
    pub fn add(&mut self, mat: Material) -> u32 {
        let id = self.next_id; self.next_id += 1;
        self.materials.insert(id, mat);
        id
    }
    pub fn get(&self, id: u32) -> Option<&Material> { self.materials.get(&id) }
    pub fn get_mut(&mut self, id: u32) -> Option<&mut Material> { self.materials.get_mut(&id) }
    pub fn find_by_name(&self, name: &str) -> Option<&Material> { self.materials.values().find(|m| m.name == name) }
    pub fn opaque_materials(&self) -> Vec<&Material> { self.materials.values().filter(|m| m.is_opaque()).collect() }
    pub fn transparent_materials(&self) -> Vec<&Material> { self.materials.values().filter(|m| m.is_transparent()).collect() }
    pub fn count(&self) -> usize { self.materials.len() }
    pub fn remove(&mut self, id: u32) -> bool { self.materials.remove(&id).is_some() }
    pub fn set_default(&mut self, id: u32) { self.default_material_id = id; }
}

impl Default for MaterialLibrary {
    fn default() -> Self { Self::new() }
}

// ── Render Pass System ────────────────────────────────────────────────────────

#[derive(Clone, Debug)]
pub struct RenderPass {
    pub id: u32,
    pub name: String,
    pub pass_type: RenderPassType,
    pub color_attachments: Vec<ColorAttachment>,
    pub depth_attachment: Option<DepthAttachment>,
    pub clear_color: [f32; 4],
    pub clear_depth: f32,
    pub clear_stencil: u8,
    pub viewport: (u32, u32, u32, u32),
    pub enabled: bool,
    pub order: u32,
}

#[derive(Clone, Debug, PartialEq)]
pub enum RenderPassType { Opaque, Transparent, Shadow, PostProcess, Ui, Compute, Custom }

#[derive(Clone, Debug)]
pub struct ColorAttachment {
    pub texture_id: u32,
    pub format: AttachmentFormat,
    pub load_op: LoadOp,
    pub store_op: StoreOp,
    pub mip_level: u32,
    pub layer: u32,
}

#[derive(Clone, Debug)]
pub struct DepthAttachment {
    pub texture_id: u32,
    pub format: AttachmentFormat,
    pub load_op: LoadOp,
    pub store_op: StoreOp,
    pub read_only: bool,
}

#[derive(Clone, Debug, PartialEq)]
pub enum AttachmentFormat { Rgba8, Rgba16F, Rgba32F, R8, R16F, R32F, Rg8, Rg16F, Depth16, Depth24, Depth32F, Depth24Stencil8 }

#[derive(Clone, Debug, PartialEq)]
pub enum LoadOp { Load, Clear, DontCare }
#[derive(Clone, Debug, PartialEq)]
pub enum StoreOp { Store, DontCare }

impl RenderPass {
    pub fn new(id: u32, name: impl Into<String>, pass_type: RenderPassType) -> Self {
        Self { id, name: name.into(), pass_type, color_attachments: Vec::new(), depth_attachment: None, clear_color: [0.0, 0.0, 0.0, 1.0], clear_depth: 1.0, clear_stencil: 0, viewport: (0, 0, 1920, 1080), enabled: true, order: 0 }
    }
    pub fn add_color_attachment(&mut self, tex_id: u32, format: AttachmentFormat) {
        self.color_attachments.push(ColorAttachment { texture_id: tex_id, format, load_op: LoadOp::Clear, store_op: StoreOp::Store, mip_level: 0, layer: 0 });
    }
    pub fn set_depth(&mut self, tex_id: u32) {
        self.depth_attachment = Some(DepthAttachment { texture_id: tex_id, format: AttachmentFormat::Depth32F, load_op: LoadOp::Clear, store_op: StoreOp::Store, read_only: false });
    }
    pub fn is_shadow_pass(&self) -> bool { self.pass_type == RenderPassType::Shadow }
    pub fn is_postprocess(&self) -> bool { self.pass_type == RenderPassType::PostProcess }
    pub fn attachment_count(&self) -> usize { self.color_attachments.len() + if self.depth_attachment.is_some() { 1 } else { 0 } }
    pub fn set_viewport(&mut self, x: u32, y: u32, w: u32, h: u32) { self.viewport = (x, y, w, h); }
}

#[derive(Clone, Debug)]
pub struct RenderPipeline {
    pub passes: Vec<RenderPass>,
    pub next_id: u32,
}

impl RenderPipeline {
    pub fn new() -> Self { Self { passes: Vec::new(), next_id: 1 } }
    pub fn add_pass(&mut self, pass: RenderPass) { self.passes.push(pass); }
    pub fn sorted_passes(&self) -> Vec<&RenderPass> {
        let mut sorted: Vec<_> = self.passes.iter().filter(|p| p.enabled).collect();
        sorted.sort_by_key(|p| p.order);
        sorted
    }
    pub fn pass_count(&self) -> usize { self.passes.len() }
    pub fn find_by_type(&self, pass_type: &RenderPassType) -> Vec<&RenderPass> {
        self.passes.iter().filter(|p| &p.pass_type == pass_type).collect()
    }
    pub fn enable_pass(&mut self, id: u32) { if let Some(p) = self.passes.iter_mut().find(|p| p.id == id) { p.enabled = true; } }
    pub fn disable_pass(&mut self, id: u32) { if let Some(p) = self.passes.iter_mut().find(|p| p.id == id) { p.enabled = false; } }
}

impl Default for RenderPipeline {
    fn default() -> Self { Self::new() }
}

// ── Post-Process Effect ───────────────────────────────────────────────────────

#[derive(Clone, Debug)]
pub struct PostProcessEffect {
    pub id: u32,
    pub name: String,
    pub effect_type: PostEffectType,
    pub shader_program_id: u32,
    pub properties: HashMap<String, f32>,
    pub enabled: bool,
    pub order: u32,
    pub input_texture: u32,
    pub output_texture: u32,
}

#[derive(Clone, Debug, PartialEq)]
pub enum PostEffectType { Bloom, Tonemap, Vignette, ChromaticAberration, DepthOfField, MotionBlur, Ssao, Fxaa, Custom }

impl PostProcessEffect {
    pub fn new(id: u32, name: impl Into<String>, effect_type: PostEffectType, shader_id: u32) -> Self {
        Self { id, name: name.into(), effect_type, shader_program_id: shader_id, properties: HashMap::new(), enabled: true, order: 0, input_texture: 0, output_texture: 0 }
    }
    pub fn set_property(&mut self, key: impl Into<String>, val: f32) { self.properties.insert(key.into(), val); }
    pub fn get_property(&self, key: &str) -> f32 { self.properties.get(key).copied().unwrap_or(0.0) }
    pub fn bloom_intensity(&self) -> f32 { self.get_property("intensity") }
    pub fn vignette_strength(&self) -> f32 { self.get_property("strength") }
    pub fn is_bloom(&self) -> bool { self.effect_type == PostEffectType::Bloom }
    pub fn is_tonemap(&self) -> bool { self.effect_type == PostEffectType::Tonemap }
}

#[derive(Clone, Debug)]
pub struct PostProcessStack {
    pub effects: Vec<PostProcessEffect>,
    pub enabled: bool,
}

impl PostProcessStack {
    pub fn new() -> Self { Self { effects: Vec::new(), enabled: true } }
    pub fn add(&mut self, effect: PostProcessEffect) { self.effects.push(effect); }
    pub fn enabled_effects(&self) -> Vec<&PostProcessEffect> {
        let mut v: Vec<_> = self.effects.iter().filter(|e| e.enabled).collect();
        v.sort_by_key(|e| e.order);
        v
    }
    pub fn enable_effect(&mut self, id: u32) { if let Some(e) = self.effects.iter_mut().find(|e| e.id == id) { e.enabled = true; } }
    pub fn disable_effect(&mut self, id: u32) { if let Some(e) = self.effects.iter_mut().find(|e| e.id == id) { e.enabled = false; } }
    pub fn effect_count(&self) -> usize { self.effects.len() }
    pub fn active_count(&self) -> usize { self.effects.iter().filter(|e| e.enabled).count() }
}

impl Default for PostProcessStack {
    fn default() -> Self { Self::new() }
}

// ── Shader Node Graph ─────────────────────────────────────────────────────────

#[derive(Clone, Debug)]
pub struct ShaderNode {
    pub id: u32,
    pub node_type: ShaderNodeType,
    pub label: String,
    pub position: (f32, f32),
    pub inputs: Vec<ShaderNodePort>,
    pub outputs: Vec<ShaderNodePort>,
    pub properties: HashMap<String, String>,
}

#[derive(Clone, Debug, PartialEq)]
pub enum ShaderNodeType {
    Input, Output, Math, Texture, Color, Constant, Mix, Clamp, Normalize, Length, Dot, Cross, Reflect, Refract, Fresnel, NormalMap, VertexShader, FragmentShader, Custom(String),
}

#[derive(Clone, Debug)]
pub struct ShaderNodePort {
    pub id: u32,
    pub name: String,
    pub port_type: ShaderDataType,
    pub connected_to: Option<(u32, u32)>,
    pub default_value: String,
}

impl ShaderNode {
    pub fn new(id: u32, node_type: ShaderNodeType, label: impl Into<String>) -> Self {
        Self { id, node_type, label: label.into(), position: (0.0, 0.0), inputs: Vec::new(), outputs: Vec::new(), properties: HashMap::new() }
    }
    pub fn add_input(&mut self, name: impl Into<String>, port_type: ShaderDataType) -> u32 {
        let pid = self.inputs.len() as u32;
        self.inputs.push(ShaderNodePort { id: pid, name: name.into(), port_type, connected_to: None, default_value: "0.0".into() });
        pid
    }
    pub fn add_output(&mut self, name: impl Into<String>, port_type: ShaderDataType) -> u32 {
        let pid = self.outputs.len() as u32;
        self.outputs.push(ShaderNodePort { id: pid, name: name.into(), port_type, connected_to: None, default_value: "0.0".into() });
        pid
    }
    pub fn set_property(&mut self, key: impl Into<String>, val: impl Into<String>) { self.properties.insert(key.into(), val.into()); }
    pub fn is_output_node(&self) -> bool { self.node_type == ShaderNodeType::Output }
    pub fn is_input_node(&self) -> bool { self.node_type == ShaderNodeType::Input }
    pub fn input_count(&self) -> usize { self.inputs.len() }
    pub fn output_count(&self) -> usize { self.outputs.len() }
}

#[derive(Clone, Debug)]
pub struct ShaderNodeGraph {
    pub nodes: HashMap<u32, ShaderNode>,
    pub connections: Vec<(u32, u32, u32, u32)>,
    pub next_id: u32,
    pub name: String,
}

impl ShaderNodeGraph {
    pub fn new(name: impl Into<String>) -> Self { Self { nodes: HashMap::new(), connections: Vec::new(), next_id: 1, name: name.into() } }
    pub fn add_node(&mut self, node_type: ShaderNodeType, label: impl Into<String>) -> u32 {
        let id = self.next_id; self.next_id += 1;
        self.nodes.insert(id, ShaderNode::new(id, node_type, label));
        id
    }
    pub fn connect(&mut self, from_node: u32, from_port: u32, to_node: u32, to_port: u32) {
        self.connections.push((from_node, from_port, to_node, to_port));
        if let Some(node) = self.nodes.get_mut(&to_node) {
            if let Some(port) = node.inputs.get_mut(to_port as usize) { port.connected_to = Some((from_node, from_port)); }
        }
    }
    pub fn disconnect(&mut self, to_node: u32, to_port: u32) {
        self.connections.retain(|(_, _, tn, tp)| !(*tn == to_node && *tp == to_port));
        if let Some(node) = self.nodes.get_mut(&to_node) {
            if let Some(port) = node.inputs.get_mut(to_port as usize) { port.connected_to = None; }
        }
    }
    pub fn remove_node(&mut self, id: u32) {
        self.nodes.remove(&id);
        self.connections.retain(|(a, _, b, _)| *a != id && *b != id);
    }
    pub fn node_count(&self) -> usize { self.nodes.len() }
    pub fn connection_count(&self) -> usize { self.connections.len() }
    pub fn output_node(&self) -> Option<&ShaderNode> { self.nodes.values().find(|n| n.is_output_node()) }
    pub fn move_node(&mut self, id: u32, x: f32, y: f32) { if let Some(n) = self.nodes.get_mut(&id) { n.position = (x, y); } }
}

impl Default for ShaderNodeGraph {
    fn default() -> Self { Self::new("new_graph") }
}

// ── More constants ────────────────────────────────────────────────────────────

pub const MATERIAL_MAX_TEXTURES: usize = 16;
pub const MATERIAL_MAX_PROPERTIES: usize = 64;
pub const RENDER_PASS_MAX: usize = 32;
pub const POST_PROCESS_EFFECT_MAX: usize = 16;
pub const SHADER_NODE_GRAPH_MAX_NODES: usize = 512;
pub const SHADER_NODE_GRAPH_MAX_CONNECTIONS: usize = 1024;
pub const MATERIAL_OPAQUE_QUEUE: u32 = 2000;
pub const MATERIAL_TRANSPARENT_QUEUE: u32 = 3000;
pub const MATERIAL_OVERLAY_QUEUE: u32 = 4000;

pub fn attachment_format_name(fmt: &AttachmentFormat) -> &'static str {
    match fmt {
        AttachmentFormat::Rgba8 => "RGBA8", AttachmentFormat::Rgba16F => "RGBA16F",
        AttachmentFormat::Rgba32F => "RGBA32F", AttachmentFormat::Depth32F => "Depth32F",
        AttachmentFormat::Depth24Stencil8 => "Depth24Stencil8", _ => "Unknown",
    }
}
pub fn blend_factor_glsl(f: &BlendFactor) -> &'static str {
    match f {
        BlendFactor::Zero => "GL_ZERO", BlendFactor::One => "GL_ONE",
        BlendFactor::SrcAlpha => "GL_SRC_ALPHA", BlendFactor::OneMinusSrcAlpha => "GL_ONE_MINUS_SRC_ALPHA",
        _ => "GL_ONE",
    }
}