gloss-renderer 0.9.0

Core renderer for gloss
extern crate nalgebra as na;

use crate::components::EnvironmentMapGpu;
use crate::components::ShadowCaster;
use crate::components::ShadowMap;
use crate::forward_renderer::render_passes::upload_pass::PerFrameUniforms;
use crate::forward_renderer::renderer::GTarget;
use crate::forward_renderer::renderer::RenderParams;
use crate::light::Light;
use crate::scene::Scene;
use easy_wgpu::bind_group::BindGroupBuilder;
use easy_wgpu::bind_group::BindGroupDesc;
use easy_wgpu::bind_group::BindGroupWrapper;
use easy_wgpu::bind_group_layout::BindGroupLayoutBuilder;
use easy_wgpu::bind_group_layout::BindGroupLayoutDesc;
use easy_wgpu::gbuffer::Gbuffer;
use easy_wgpu::gpu::Gpu;
use easy_wgpu::pipeline::RenderPipelineDescBuilder;
use easy_wgpu::texture::Texture;

// use super::upload_pass::MAX_NUM_LIGHTS;
use super::upload_pass::MAX_NUM_SHADOWS;

//shaders
#[allow(clippy::approx_constant)]
#[include_wgsl_oil::include_wgsl_oil("../../../shaders/compose.wgsl")]
mod shader_code {}

/// Pass that uses the gbuffer, lights and shadowmaps and composes everything into the final image.
pub struct ComposePass {
    render_pipeline: wgpu::RenderPipeline,
    /// Output texture of the compose pass
    pub out_texture: Texture,
    /// layout of the input to the compose pass. Usually contains gbuffer textures, shadow maps, etc.
    input_layout: wgpu::BindGroupLayout,
    input_bind_group: Option<BindGroupWrapper>,
    //misc
    /// stores some entities that should be local to this pass and don't need to be stored in the main scene
    local_scene: Scene,
    /// Light that is used as a dummy. Serves to bind to the [``input_layout``] and [``input_bind_group``] and satisfy the layout needs even if we don't actually use this light.
    passthrough_light: Light,
}

impl ComposePass {
    pub fn new(gpu: &Gpu) -> Self {
        let input_layout_desc = Self::input_layout_desc();
        let input_layout = input_layout_desc
            .clone()
            .into_bind_group_layout(gpu.device());

        //start with some random size of the output texture, it will get resized either way at the beggining of the frame
        let out_texture = Texture::new(
            gpu.device(),
            4,
            4,
            wgpu::TextureFormat::Rgba8Unorm,
            wgpu::TextureUsages::RENDER_ATTACHMENT
                | wgpu::TextureUsages::TEXTURE_BINDING
                | wgpu::TextureUsages::COPY_SRC,
        );

        //render pipeline
        let render_pipeline = RenderPipelineDescBuilder::new()
            .label("compose pipeline")
            .shader_code(shader_code::SOURCE)
            .shader_label("compose_shader")
            .add_bind_group_layout_desc(PerFrameUniforms::layout_desc())
            .add_bind_group_layout_desc(input_layout_desc) //input
            .add_render_target(wgpu::ColorTargetState {
                format: out_texture.texture.format(),
                blend: None,
                write_mask: wgpu::ColorWrites::ALL,
            })
            .multisample(wgpu::MultisampleState::default())
            .build_pipeline(gpu.device());

        //we want to add a dummy light and we do it in a local world so we don't have to depend on the main scene
        let mut local_scene = Scene::new();
        // let passthrough_tex = Texture::create_default_texture(gpu.device(), gpu.queue());
        let passthrough_tex = Texture::new(
            gpu.device(),
            4,
            4,
            wgpu::TextureFormat::Depth32Float,
            wgpu::TextureUsages::TEXTURE_BINDING,
        );
        // let passthrough_tex2 = Texture::create_default_texture(gpu.device(), gpu.queue());
        let passthrough_light = Light::new("compose_pass_passthrough_light", &mut local_scene);
        let _ = local_scene.world.insert_one(
            passthrough_light.entity,
            ShadowMap {
                tex_depth: passthrough_tex,
                // tex_depth_moments: passthrough_tex2,
            },
        );

        Self {
            render_pipeline,
            out_texture,
            input_layout,
            input_bind_group: None,
            //misc
            local_scene,
            passthrough_light,
        }
    }

    /// # Panics
    /// Will panic if the `input_bind_group` is not created. It should be created automatically when doing run() by the `update_bind_group()`
    pub fn run(
        &mut self,
        gpu: &Gpu,
        per_frame_uniforms: &PerFrameUniforms,
        gbuffer: &Gbuffer<GTarget>,
        scene: &mut Scene,
        render_params: &RenderParams,
    ) {
        self.begin_pass(gpu, gbuffer);

        //read from the a gbuffer texture into an output texture
        let mut encoder = gpu
            .device()
            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
                label: Some("Compose Encoder"),
            });

        {
            //update the bind group in case the input_texture or the shadowmaps changed
            self.update_bind_group(gpu, scene, gbuffer, per_frame_uniforms);

            {
                let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                    label: Some("Compose Pass"),
                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                        view: &self.out_texture.view,
                        resolve_target: None,
                        ops: wgpu::Operations {
                            load: wgpu::LoadOp::Clear(wgpu::Color {
                                r: f64::from(render_params.bg_color.x),
                                g: f64::from(render_params.bg_color.y),
                                b: f64::from(render_params.bg_color.z),
                                a: f64::from(render_params.bg_color.w),
                            }),
                            store: true,
                        },
                    })],
                    depth_stencil_attachment: None,
                });

                render_pass.set_pipeline(&self.render_pipeline);

                //global binding
                render_pass.set_bind_group(0, &per_frame_uniforms.bind_group, &[]);
                //input binding
                render_pass.set_bind_group(1, self.input_bind_group.as_ref().unwrap().bg(), &[]);

                //draw a quad
                render_pass.draw(0..4, 0..1);
            }
        }

        gpu.queue().submit(Some(encoder.finish()));

        self.end_pass();
    }

    fn begin_pass(&mut self, gpu: &Gpu, gbuffer: &Gbuffer<GTarget>) {
        if gbuffer.width != self.out_texture.width() || gbuffer.height != self.out_texture.height()
        {
            self.out_texture
                .resize(gpu.device(), gbuffer.width, gbuffer.height);
        }
    }

    fn end_pass(&mut self) {}

    fn input_layout_desc() -> BindGroupLayoutDesc {
        BindGroupLayoutBuilder::new()
            .label("compose_input_layout")
            //albedo
            .add_entry_tex(
                wgpu::ShaderStages::FRAGMENT,
                wgpu::TextureSampleType::Float { filterable: true },
            )
            //position
            .add_entry_tex(
                wgpu::ShaderStages::FRAGMENT,
                wgpu::TextureSampleType::Float { filterable: false },
            )
            //normal
            .add_entry_tex(
                wgpu::ShaderStages::FRAGMENT,
                wgpu::TextureSampleType::Float { filterable: false },
            )
            //metalness_roughness
            .add_entry_tex(
                wgpu::ShaderStages::FRAGMENT,
                wgpu::TextureSampleType::Float { filterable: false },
            )
            //depth
            .add_entry_tex(
                wgpu::ShaderStages::FRAGMENT,
                wgpu::TextureSampleType::Float { filterable: false },
            )
            //diffuse cubemap
            .add_entry_cubemap(
                wgpu::ShaderStages::FRAGMENT,
                wgpu::TextureSampleType::Float { filterable: true },
            )
            //specular cubemap
            .add_entry_cubemap(
                wgpu::ShaderStages::FRAGMENT,
                wgpu::TextureSampleType::Float { filterable: true },
            )
            //shadow_maps
            .add_entries_tex(
                wgpu::ShaderStages::FRAGMENT,
                wgpu::TextureSampleType::Depth, //float textures cannot be linearly filtered on webgpu :( But you can use a sampler with comparison and it does a hardware 2x2 pcf
                // wgpu::TextureSampleType::Float { filterable: false }, //float textures cannot be linearly filtered on webgpu :(
                MAX_NUM_SHADOWS,
            )
            .build()
    }

    fn update_bind_group(
        &mut self,
        gpu: &Gpu,
        scene: &Scene,
        gbuffer: &Gbuffer<GTarget>,
        // lights: &'b [Entity],
        per_frame_uniforms: &PerFrameUniforms,
    ) {
        //get envmap
        let env_map = scene.get_resource::<&EnvironmentMapGpu>().unwrap();

        let mut shadow_maps = Vec::new();

        //attempt adding shadow maps for all the lights, first the real lights that are actually in the scene and afterwards we will with dummy values
        // for i in 0..MAX_NUM_LIGHTS {
        // for i in 0..8 {
        for i in 0..MAX_NUM_SHADOWS {
            let is_within_valid_lights: bool = i < per_frame_uniforms.idx_ubo2light.len();
            if is_within_valid_lights
                && scene
                    .world
                    .has::<ShadowCaster>(per_frame_uniforms.idx_ubo2light[i])
                    .unwrap()
            {
                let entity = per_frame_uniforms.idx_ubo2light[i];
                let shadow: gloss_hecs::Ref<'_, ShadowMap> = scene.get_comp::<&ShadowMap>(&entity);
                shadow_maps.push(shadow);
            } else {
                //dummy passthough shadowmap
                let shadow: gloss_hecs::Ref<'_, ShadowMap> = self
                    .local_scene
                    .get_comp::<&ShadowMap>(&self.passthrough_light.entity);
                shadow_maps.push(shadow);
            }
        }

        let entries = BindGroupBuilder::new()
            .add_entry_tex(gbuffer.get(GTarget::Albedo).unwrap())
            .add_entry_tex(gbuffer.get(GTarget::Position).unwrap())
            .add_entry_tex(gbuffer.get(GTarget::Normal).unwrap())
            .add_entry_tex(gbuffer.get(GTarget::MetalnessRoughness).unwrap())
            .add_entry_tex(gbuffer.get(GTarget::Depth).unwrap())
            .add_entry_tex(&env_map.diffuse_tex)
            .add_entry_tex(&env_map.specular_tex)
            .add_entry_tex(&shadow_maps[0].tex_depth)
            .add_entry_tex(&shadow_maps[1].tex_depth)
            .add_entry_tex(&shadow_maps[2].tex_depth)
            .add_entry_tex(&shadow_maps[3].tex_depth)
            .add_entry_tex(&shadow_maps[4].tex_depth)
            .add_entry_tex(&shadow_maps[5].tex_depth)
            .add_entry_tex(&shadow_maps[6].tex_depth)
            .add_entry_tex(&shadow_maps[7].tex_depth)
            .build_entries();
        let stale = self
            .input_bind_group
            .as_ref()
            .map_or(true, |b| b.is_stale(&entries)); //returns true if the bg has not been created or if stale
        if stale {
            debug!("compose input bind group is stale, recreating");
            self.input_bind_group = Some(
                BindGroupDesc::new("compose_input_bg", entries)
                    .into_bind_group_wrapper(gpu.device(), &self.input_layout),
            );
        }
    }
}