Skip to content

Output screen texture to png #1207

@ctangell

Description

@ctangell

What problem does this solve or what need does it fill?

Would enable using Bevy as a general purpose simulation tool for robotics and machine learning research by allowing the screen texture to piped over to a machine learning engine, or for incorporating direct inference in Bevy.

What solution would you like?

The actual solution that would be nice is to have an additional node that can be attached to the render pipeline that will return a png of what is shown on the screen. Being able to scale that also so it's not produced every frame but rather every 10 frames or 100 frames would be helpful for performance issues.

I have tried on my own the following so far:

created in WgpuRenderResourceContext the following code to copy a texture to a buffer:

#[allow(clippy::too_many_arguments)]
    pub fn copy_texture_to_buffer(
        &self, 
        command_encoder: &mut wgpu::CommandEncoder,
        source_texture: TextureId, 
        source_origin: [u32; 3], // TODO: replace with math type
        source_mip_level: u32,
        destination_buffer: BufferId,
        destination_offset: u64,
        destination_bytes_per_row: u32,
        size: Extent3d,
    ) {
        let buffers = self.resources.buffers.read();
        let textures = self.resources.textures.read();

        let source = textures.get(&source_texture).unwrap();
        let destination = buffers.get(&destination_buffer).unwrap();

        command_encoder.copy_texture_to_buffer(
            wgpu::TextureCopyView {
                texture: source,
                mip_level: source_mip_level,
                origin: wgpu::Origin3d {
                    x: source_origin[0],
                    y: source_origin[1],
                    z: source_origin[2],
                },
            },
            wgpu::BufferCopyView {
                buffer: destination,
                layout: wgpu::TextureDataLayout {
                    offset: destination_offset,
                    bytes_per_row: destination_bytes_per_row,
                    rows_per_image: size.height,
                },
            },
            size.wgpu_into(),
        );
    }

and then created in impl RenderResourceContext for WgpuRenderResourceContext the following function to get the buffer out of the gpu:

fn copy_buffer_to_png(&self, buffer_id: BufferId, descriptor: TextureDescriptor) -> () {
        let buffers = self.resources.buffers.read();
        let buffer = buffers.get(&buffer_id).unwrap();

        // reading out from a buffer: https://github.com/gfx-rs/wgpu/issues/239
        // now read the buffer
        let buffer_slice = buffer.slice(..);
        let buffer_future = buffer_slice.map_async(wgpu::MapMode::Read);
        self.device.poll(wgpu::Maintain::Wait);
        if future::block_on(buffer_future).is_ok() {
            let data = buffer_slice.get_mapped_range();

            match descriptor.format {
                TextureFormat::Bgra8UnormSrgb => {
                    // saving for testing only, should output to struct and pass that back
                    image::save_buffer("test.png", 
                                        data.as_ref(),
                                        descriptor.size.width,
                                        descriptor.size.height,
                                        image::ColorType::Bgra8);
                },
                    
                TextureFormat::Depth32Float => {
                    // todo: convert data to f32 then scale then convert to u16 then convert to &[u8]
                    /*image::save_buffer("test_depth.png", 
                                        data.as_ref(),
                                        descriptor.size.width,
                                        descriptor.size.height,
                                        image::ColorType::???);*/
                },

                _ => (),
            };

            drop(data);
            buffer.unmap();
        }
    }

and then tried to implement in impl Node for WindowTextureNode fn update:

if let Some(RenderResourceId::Texture(texture)) = output.get(WINDOW_TEXTURE) {
            let render_resource_context = render_context.resources_mut();
            let descriptor = self.descriptor;
            let width = descriptor.size.width as usize;
            let aligned_width =
                render_resource_context.get_aligned_texture_size(width);
            let format_size = descriptor.format.pixel_size();
            println!("{} {:?}", descriptor.format.pixel_info().type_size, descriptor.size);
            println!("{:?}", descriptor.format);
    
            let texture_buffer = render_resource_context.create_buffer(BufferInfo {
                size: descriptor.size.volume() * format_size,
                buffer_usage: BufferUsage::MAP_READ | BufferUsage::COPY_DST,
                mapped_at_creation: false,
            });
    
            render_context.copy_texture_to_buffer(
                texture,
                [0, 0, 0],
                0,
                texture_buffer,
                0,
                (format_size * aligned_width) as u32,
                descriptor.size,
            );

            let render_resource_context = render_context.resources_mut();
            render_resource_context.copy_buffer_to_png(texture_buffer, descriptor);
    
            // remove the created buffer... for now
            render_resource_context.remove_buffer(texture_buffer);
        }

Unfortunately that is as far as I got as the resulting png image is empty (what comes out is an array of zeros). Ideally this should be it's own node that's attached to the final end of the render pipeline after the screen texture is written.

What alternative(s) have you considered?

Not understanding how the render pipeline works, chose to try window_texture_node.rs as that is the only place with the necessary bevy_render::texture::TextureDescriptor for the screen buffer. From a comment on discord it seems that really the texture to extract is more likely in window_swapchain_node.rs. The problem is, there isn't the necessary information in a TextureDescriptor in that node in order to do the necessary texture -> buffer -> png copying. So some type of information passing from the WindowTextureNode (where the relevant TextureDescriptor is stored) to finally which ever node actually has the access to the final screen texture is needed.

I tried to understand the default render pipeline in base.rs but couldn't make much sense of what was being passed around.

Additional context

The above code causes the game to crash when the window is re-sized.

Additionally, a compute shader that converts the depth buffer to a scaled u16 buffer scaled to match the output from a physical depth camera (like an intel realsense) would be super handy.

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-RenderingDrawing game state to the screenC-FeatureA new feature, making something new possible

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions