pybevy_pbr 0.2.1

PBR components for PyBevy
Documentation
pub mod atmosphere;
pub mod atmosphere_settings;
pub mod default_opaque_renderer_method;
pub mod distance_fog;
pub mod falloff;
pub mod fog_falloff;
pub mod forward_decal;
pub mod lightmap;
pub mod no_wireframe;
pub mod opaque_renderer_method;
pub mod parallax_mapping_method;
pub mod plugin;
pub mod scattering_medium;
pub mod screen_space_ambient_occlusion;
pub mod screen_space_reflections;
pub mod shader_material;
pub mod shader_material_py;
pub mod ssao_quality_level;
pub mod standard_material;
pub mod uv_channel;
pub mod wireframe;
pub mod wireframe_color;
pub mod wireframe_config;
pub mod wireframe_material;

pub use atmosphere::PyAtmosphere;
pub use atmosphere_settings::PyAtmosphereSettings;
use bevy::pbr::{
    Atmosphere, AtmosphereSettings, DefaultOpaqueRendererMethod, DistanceFog, Lightmap,
    MeshMaterial3d, ScatteringMedium, ScreenSpaceAmbientOcclusion, ScreenSpaceReflections,
    StandardMaterial,
    decal::ForwardDecal,
    wireframe::{NoWireframe, Wireframe, WireframeColor, WireframeConfig, WireframeMaterial},
};
pub use default_opaque_renderer_method::PyDefaultOpaqueRendererMethod;
pub use distance_fog::PyDistanceFog;
pub use falloff::PyFalloff;
pub use fog_falloff::PyFogFalloff;
pub use forward_decal::PyForwardDecal;
pub use lightmap::PyLightmap;
pub use no_wireframe::PyNoWireframe;
pub use opaque_renderer_method::PyOpaqueRendererMethod;
pub use parallax_mapping_method::PyParallaxMappingMethod;
pub use plugin::PyPbrPlugin;
use pybevy_core::{plugin::plugin_registry, registry::global_registry};
use pybevy_macros::{
    asset_bridge, component_bridge, handle_bridge, plugin_bridge, resource_bridge, unit_bridge,
};
use pyo3::prelude::*;
pub use scattering_medium::PyScatteringMedium;
pub use screen_space_ambient_occlusion::PyScreenSpaceAmbientOcclusion;
pub use screen_space_reflections::PyScreenSpaceReflections;
use shader_material::ShaderMaterial;
pub use shader_material_py::{PyMeshMaterial3dShader, PyShaderMaterial, PyShaderMaterialPlugin};
pub use ssao_quality_level::PyScreenSpaceAmbientOcclusionQualityLevel;
pub use standard_material::PyStandardMaterial;
pub use uv_channel::PyUvChannel;
pub use wireframe::PyWireframe;
pub use wireframe_color::PyWireframeColor;
pub use wireframe_config::PyWireframeConfig;
pub use wireframe_material::PyWireframeMaterial;

// Generate bridges for ComponentStorage-based components
component_bridge!(
    DistanceFog,
    PyDistanceFog,
    view_fields = [directional_light_exponent],
    batch_only_fields = [color, directional_light_color]
);
component_bridge!(
    ScreenSpaceAmbientOcclusion,
    PyScreenSpaceAmbientOcclusion,
    view_fields = [constant_object_thickness]
);
component_bridge!(
    ScreenSpaceReflections,
    PyScreenSpaceReflections,
    view_fields = [
        perceptual_roughness_threshold,
        thickness,
        linear_steps,
        linear_march_exponent,
        bisection_steps,
        use_secant
    ]
);
component_bridge!(Atmosphere, PyAtmosphere, view_only_fields = [bottom_radius: f32, top_radius: f32]);
component_bridge!(
    AtmosphereSettings,
    PyAtmosphereSettings,
    view_fields = [
        transmittance_lut_samples,
        multiscattering_lut_dirs,
        multiscattering_lut_samples,
        sky_view_lut_samples,
        aerial_view_lut_samples,
        aerial_view_lut_max_distance,
        scene_units_to_m,
        sky_max_samples
    ]
);
component_bridge!(Wireframe, PyWireframe);
component_bridge!(
    WireframeColor,
    PyWireframeColor,
    batch_only_fields = [color]
);
component_bridge!(NoWireframe, PyNoWireframe);
component_bridge!(Lightmap, PyLightmap);
unit_bridge!(ForwardDecal, PyForwardDecal);

// Generate plugin bridges via macro
plugin_bridge!(PyPbrPlugin, bevy::pbr::PbrPlugin);

// Generate resource bridges
resource_bridge!(WireframeConfig, PyWireframeConfig);
resource_bridge!(DefaultOpaqueRendererMethod, PyDefaultOpaqueRendererMethod);

// Generate asset bridges
asset_bridge!(StandardMaterial, PyStandardMaterial);
asset_bridge!(WireframeMaterial, PyWireframeMaterial, not_loadable);
asset_bridge!(ScatteringMedium, PyScatteringMedium, not_loadable);
asset_bridge!(ShaderMaterial, PyShaderMaterial, not_loadable);

// Generate handle bridge for ShaderMaterial's MeshMaterial3d
handle_bridge!(
    MeshMaterial3d::<ShaderMaterial>,
    PyMeshMaterial3dShader,
    "MeshMaterial3dShader"
);

// Generate plugin bridge with custom build logic
plugin_bridge!(
    PyShaderMaterialPlugin,
    bevy::pbr::MaterialPlugin::<ShaderMaterial>,
    |_py_plugin: &pyo3::Bound<'_, pyo3::PyAny>, app: &mut bevy::app::App| {
        // Clear stale handles from previous app runs in the same process
        shader_material::clear_shader_registries();
        app.add_plugins(bevy::pbr::MaterialPlugin::<ShaderMaterial>::default());
        app.add_systems(bevy::app::Last, shader_material::sync_shader_handles);
        Ok(())
    }
);
pub fn register_pbr_bridges() {
    global_registry::register_component_bridge(DistanceFogBridge);
    global_registry::register_component_bridge(ScreenSpaceAmbientOcclusionBridge);
    global_registry::register_component_bridge(ScreenSpaceReflectionsBridge);
    global_registry::register_component_bridge(AtmosphereBridge);
    global_registry::register_component_bridge(AtmosphereSettingsBridge);
    global_registry::register_component_bridge(WireframeBridge);
    global_registry::register_component_bridge(WireframeColorBridge);
    global_registry::register_component_bridge(NoWireframeBridge);
    global_registry::register_component_bridge(LightmapBridge);
    global_registry::register_component_bridge(ForwardDecalBridge);
    register_distance_fog_batch();
    register_screen_space_ambient_occlusion_batch();
    register_screen_space_reflections_batch();
    register_atmosphere_settings_batch();
    register_wireframe_color_batch();

    // Plugins
    plugin_registry::register_plugin_bridge(PbrPluginBridge);

    // Resources
    global_registry::register_resource_bridge(WireframeConfigBridge);
    global_registry::register_resource_bridge(DefaultOpaqueRendererMethodBridge);

    // Assets
    global_registry::register_asset_bridge(StandardMaterialBridge);
    global_registry::register_asset_bridge(WireframeMaterialBridge);
    global_registry::register_asset_bridge(ScatteringMediumBridge);
    global_registry::register_asset_bridge(ShaderMaterialBridge);

    // ShaderMaterial component + plugin
    global_registry::register_component_bridge(MeshMaterial3dShaderBridge);
    plugin_registry::register_plugin_bridge(MaterialPluginBridge);
}
pub fn add_pbr_classes(m: &Bound<'_, PyModule>) -> PyResult<()> {
    register_pbr_bridges();

    // Plugins
    m.add_class::<PyPbrPlugin>()?;

    m.add_class::<PyDistanceFog>()?;
    m.add_class::<PyFogFalloff>()?;
    m.add_class::<PyScreenSpaceAmbientOcclusion>()?;
    m.add_class::<PyScreenSpaceAmbientOcclusionQualityLevel>()?;
    m.add_class::<PyScreenSpaceReflections>()?;
    m.add_class::<PyAtmosphere>()?;
    m.add_class::<PyAtmosphereSettings>()?;
    m.add_class::<PyParallaxMappingMethod>()?;
    m.add_class::<PyStandardMaterial>()?;
    m.add_class::<PyUvChannel>()?;
    m.add_class::<PyWireframe>()?;
    m.add_class::<PyWireframeColor>()?;
    m.add_class::<PyNoWireframe>()?;
    m.add_class::<PyWireframeConfig>()?;
    m.add_class::<PyWireframeMaterial>()?;
    m.add_class::<PyScatteringMedium>()?;
    m.add_class::<PyDefaultOpaqueRendererMethod>()?;
    m.add_class::<PyLightmap>()?;
    m.add_class::<PyForwardDecal>()?;
    m.add_class::<PyFalloff>()?;
    m.add_class::<PyOpaqueRendererMethod>()?;

    // ShaderMaterial
    m.add_class::<PyShaderMaterial>()?;
    m.add_class::<PyShaderMaterialPlugin>()?;
    m.add_class::<PyMeshMaterial3dShader>()?;
    Ok(())
}

pub fn add_module(parent: &Bound<'_, PyModule>) -> PyResult<()> {
    let m = PyModule::new(parent.py(), "pbr")?;
    add_pbr_classes(&m)?;
    // Render types re-exported through the pbr Python module
    m.add_class::<pybevy_render::PyOpaqueRenderMethod>()?;
    m.add_class::<pybevy_render::PyAtmosphereMode>()?;
    parent.add_submodule(&m)
}

#[cfg(test)]
mod tests {
    use bevy::pbr::{OpaqueRendererMethod, ParallaxMappingMethod};

    use super::*;

    #[test]
    fn test_pyparallaxmappingmethod_roundtrip() {
        let default = PyParallaxMappingMethod::Occlusion();
        let rust: ParallaxMappingMethod = default.into();
        let py: PyParallaxMappingMethod = rust.into();
        assert_eq!(default, py);
    }

    #[test]
    fn test_pyparallaxmappingmethod_default_match() {
        let default = PyParallaxMappingMethod::new();
        assert_eq!(default, ParallaxMappingMethod::default().into());
    }

    #[test]
    fn test_pyopaquerendermethod_roundtrip() {
        let default = pybevy_render::PyOpaqueRenderMethod::Forward;
        let rust: OpaqueRendererMethod = default.into();
        let py: pybevy_render::PyOpaqueRenderMethod = rust.into();
        assert_eq!(default, py);
    }

    #[test]
    fn test_pyopaquerendermethod_default_match() {
        let default = pybevy_render::PyOpaqueRenderMethod::Forward;
        assert_eq!(default, OpaqueRendererMethod::default().into());
    }

    #[test]
    fn test_pyuvochannel_roundtrip() {
        let default = PyUvChannel::Uv0;
        let rust: bevy::pbr::UvChannel = default.into();
        let py: PyUvChannel = rust.into();
        assert_eq!(default, py);
    }

    #[test]
    fn test_pyuvochannel_default_match() {
        let default = PyUvChannel::new();
        assert_eq!(default, bevy::pbr::UvChannel::default().into());
    }

    #[test]
    fn test_pystandardmaterial_roundtrip() {
        pyo3::Python::attach(|py| {
            use pyo3::PyTypeInfo;
            let py_default = PyStandardMaterial::type_object(py)
                .call0()
                .unwrap()
                .extract::<PyStandardMaterial>()
                .unwrap();

            let rust: bevy::pbr::StandardMaterial = py_default.clone().try_into().unwrap();
            let pymat: PyStandardMaterial = rust.into();

            let py_mat = py_default.as_ref().unwrap();
            let roundtrip_mat = pymat.as_ref().unwrap();
            assert_eq!(py_mat.base_color, roundtrip_mat.base_color);
            assert_eq!(
                py_mat.perceptual_roughness,
                roundtrip_mat.perceptual_roughness
            );
            assert_eq!(py_mat.metallic, roundtrip_mat.metallic);
            assert_eq!(py_mat.reflectance, roundtrip_mat.reflectance);
        });
    }

    #[test]
    fn test_pystandardmaterial_default_match() {
        pyo3::Python::attach(|py| {
            use pyo3::PyTypeInfo;
            let py_default = PyStandardMaterial::type_object(py)
                .call0()
                .unwrap()
                .extract::<PyStandardMaterial>()
                .unwrap();

            let bevy_default = bevy::pbr::StandardMaterial::default();

            let py_mat = py_default.as_ref().unwrap();
            assert_eq!(py_mat.base_color, bevy_default.base_color);
            assert_eq!(
                py_mat.perceptual_roughness,
                bevy_default.perceptual_roughness
            );
            assert_eq!(py_mat.metallic, bevy_default.metallic);
            assert_eq!(py_mat.reflectance, bevy_default.reflectance);
            assert_eq!(py_mat.unlit, bevy_default.unlit);
            assert_eq!(py_mat.double_sided, bevy_default.double_sided);
        });
    }
}