八、WebGPU 基础入门——加载图像纹理

八、WebGPU 基础入门------加载图像纹理

本节将介绍如何加载图像纹理到 WebGPU 中,并在屏幕上绘制出来。我们将使用 image crate 来加载图像文件,并将其转换为 WebGPU 可用的纹理格式。本节会使用egui 来实现一个简单的 UI 界面。如何集成egui,请参考kaphula/winit-egui-wgpu-template

1.安装依赖

bash 复制代码
cargo add image reqwest

2.编写shader

source/texture.wgsl中添加以下代码:

wgsl 复制代码
// 定义顶点输出结构体,包含位置和纹理坐标
struct VertexOutput {
    @builtin(position) position: vec4f,
    @location(0) texcoord: vec2f,
}

// 定义绑定组和绑定点的变量,用于采样器和纹理
@group(0) @binding(0) var ourSampler: sampler;
@group(0) @binding(1) var ourTexture: texture_2d<f32>;

// 定义uniform变量,用于传递缩放参数
// 这里的缩放参数是一个二维向量,表示在x和y方向上的缩放比例
@group(1) @binding(0) var<uniform> scale: vec2f;

// 顶点着色器函数,计算顶点位置和纹理坐标
@vertex
fn vs(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
    // 定义顶点位置数组
    let pos = array(vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0), vec2(1.0, -1.0), vec2(1.0, 1.0), vec2(-1.0, 1.0));

    // 返回顶点输出,位置经过缩放,纹理坐标归一化到[0, 1]
    return VertexOutput(vec4f(pos[vertex_index] * scale, 0.0, 1.0),(pos[vertex_index] + vec2(1.0, 1.0)) * 0.5);
}

// 片段着色器函数,采样纹理并返回颜色
@fragment
fn fs(in: VertexOutput) -> @location(0) vec4f {
    return textureSample(ourTexture, ourSampler, in.texcoord);
}

3.添加UI

src/controls.rs中添加以下代码:

rust 复制代码
// 定义一个结构体 Controls,用于存储控件的状态
#[derive(Debug, Clone)]
pub struct Controls {
    pub mag_filter: wgpu::FilterMode,      // 纹理放大过滤模式
    pub address_mode_u: wgpu::AddressMode, // 纹理 U 轴寻址模式
    pub address_mode_v: wgpu::AddressMode, // 纹理 V 轴寻址模式
    pub image_url: String,                 // 图像 URL
}

impl Controls {
    // 创建一个新的 Controls 实例,初始化默认值
    pub fn new() -> Self {
        Self {
            mag_filter: wgpu::FilterMode::Nearest, // 默认使用最近点采样
            address_mode_u: wgpu::AddressMode::ClampToEdge, // 默认 U 轴边缘拉伸
            address_mode_v: wgpu::AddressMode::ClampToEdge, // 默认 V 轴边缘拉伸
            image_url: String::new(),              // 默认空字符串
        }
    }

    // 渲染控件的 UI
    pub fn render(
        &mut self,
        ctx: &egui::Context,             // egui 上下文
        mut on_change: impl FnMut(Self), // 当控件值改变时的回调函数
    ) {
        egui::Window::new("Controls").show(ctx, |ui| {
            // 渲染 Mag Filter 下拉框
            ui.horizontal(|ui| {
                ui.label("Mag Filter");
                egui::ComboBox::from_id_salt("mag_filter")
                    .selected_text(format!("{:?}", self.mag_filter))
                    .show_ui(ui, |ui| {
                        let a = ui
                            .selectable_value(
                                &mut self.mag_filter,
                                wgpu::FilterMode::Nearest,
                                "Nearest",
                            )
                            .changed();
                        let b = ui
                            .selectable_value(
                                &mut self.mag_filter,
                                wgpu::FilterMode::Linear,
                                "Linear",
                            )
                            .changed();
                        a || b
                    })
                    .inner
                    .map(|changed| {
                        if changed {
                            on_change(self.clone()); // 如果值改变,调用回调函数
                        }
                    })
            });
            ui.add_space(16.0); // 添加间距

            // 渲染 Address Mode U 下拉框
            ui.horizontal(|ui| {
                ui.label("Address Mode U");
                egui::ComboBox::from_id_salt("address_mode_u")
                    .selected_text(format!("{:?}", self.address_mode_u))
                    .show_ui(ui, |ui| {
                        let a = ui
                            .selectable_value(
                                &mut self.address_mode_u,
                                wgpu::AddressMode::ClampToEdge,
                                "ClampToEdge",
                            )
                            .changed();
                        let b = ui
                            .selectable_value(
                                &mut self.address_mode_u,
                                wgpu::AddressMode::Repeat,
                                "Repeat",
                            )
                            .changed();
                        a || b
                    })
                    .inner
                    .map(|changed| {
                        if changed {
                            on_change(self.clone()); // 如果值改变,调用回调函数
                        }
                    })
            });
            ui.add_space(16.0); // 添加间距

            // 渲染 Address Mode V 下拉框
            ui.horizontal(|ui| {
                ui.label("Address Mode V");

                egui::ComboBox::from_id_salt("address_mode_v")
                    .selected_text(format!("{:?}", self.address_mode_v))
                    .show_ui(ui, |ui| {
                        let a = ui
                            .selectable_value(
                                &mut self.address_mode_v,
                                wgpu::AddressMode::ClampToEdge,
                                "ClampToEdge",
                            )
                            .changed();
                        let b = ui
                            .selectable_value(
                                &mut self.address_mode_v,
                                wgpu::AddressMode::Repeat,
                                "Repeat",
                            )
                            .changed();
                        a || b
                    })
                    .inner
                    .map(|changed| {
                        if changed {
                            on_change(self.clone()); // 如果值改变,调用回调函数
                        }
                    })
            });
            ui.add_space(16.0); // 添加间距

            // 渲染 Image URL 文本框和加载按钮
            ui.vertical(|ui| {
                ui.label("Image URL");
                ui.add_space(8.0); // 添加间距
                ui.horizontal(|ui| {
                    ui.text_edit_singleline(&mut self.image_url); // 文本框输入 URL
                    if ui.button("Load").clicked() {
                        on_change(self.clone()); // 点击加载按钮时调用回调函数
                    }
                });
            })
        });
    }
}

4.添加utils模块

src/utils.rs中添加以下代码:

rust 复制代码
use image::DynamicImage;
// 使用 `reqwest` 和 `image` 库从给定的 URL 加载图像数据,并返回 `DynamicImage` 类型的结果
pub fn load_image_data(url: &str) -> anyhow::Result<DynamicImage> {
    // 通过 `reqwest::blocking::get` 获取 URL 的数据并转换为字节
    let data = reqwest::blocking::get(url)?.bytes()?;
    // 使用 `image::load_from_memory` 从字节数据加载图像
    Ok(image::load_from_memory(&data)?)
}

// 计算图像在屏幕上的缩放比例,返回一个包含宽度和高度缩放比例的数组
pub fn calc_scale(image: [f32; 2], screen: [f32; 2]) -> [f32; 2] {
    // 解构图像和屏幕的宽度和高度
    let [width, height] = image;
    let [screen_width, screen_height] = screen;

    // 计算图像和屏幕的宽高比
    let image_ratio = width / height;
    let screen_ratio = screen_width / screen_height;

    // 根据宽高比调整缩放比例
    if image_ratio > screen_ratio {
        [1.0, screen_ratio / image_ratio] // 图像宽度占满屏幕,高度按比例缩放
    } else {
        [image_ratio / screen_ratio, 1.0] // 图像高度占满屏幕,宽度按比例缩放
    }
}

5.修改WgpuApp模块

rust 复制代码
use anyhow::Result;
use egui_wgpu::ScreenDescriptor;
use image::GenericImageView;
use std::sync::Arc;
use utils::calc_scale;
use wgpu::{Color, include_wgsl, util::DeviceExt};
use winit::window::Window;
pub mod controls;
pub mod egui_render;
pub mod utils;


// Wgpu应用核心结构体
pub struct WgpuApp {
    // ...
    pub egui_renderer: egui_render::EguiRenderer, // Egui渲染器
    pub controls: controls::Controls,
    pub scale_bind_group: wgpu::BindGroup,
    pub scale_buffer: wgpu::Buffer,
    pub image_dimensions: [f32; 2],
}

impl WgpuApp {
    /// 异步构造函数:初始化WebGPU环境
    pub async fn new(window: Arc<Window>) -> Result<Self> {
        // ...
        
        // 5. 配置表面(设置像素格式、尺寸等)
        let config = surface
            .get_default_config(
                &adapter,
                window.inner_size().width.max(1),  // 确保最小宽度为1
                window.inner_size().height.max(1), // 确保最小高度为1
            )
            .unwrap();
        surface.configure(&device, &config);
    
        let egui_render = egui_render::EguiRenderer::new(&device, config.format, None, 1, &window);

        // 6. 创建着色器模块(加载WGSL着色器)
        let shader = device.create_shader_module(include_wgsl!("../../source/texture.wgsl"));

        // 7. 创建渲染管线

        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some("Render Pipeline"),
            layout: None, // 使用默认管线布局
            vertex: wgpu::VertexState {
                module: &shader,         // 顶点着色器模块
                entry_point: Some("vs"), // 入口函数
                buffers: &[],            // 顶点缓冲区布局(本示例为空)
                compilation_options: Default::default(),
            },
            fragment: Some(wgpu::FragmentState {
                module: &shader,         // 片元着色器模块
                entry_point: Some("fs"), // 入口函数
                targets: &[Some(wgpu::ColorTargetState {
                    format: config.format,                  // 使用表面配置的格式
                    blend: Some(wgpu::BlendState::REPLACE), // 混合模式:直接替换
                    write_mask: wgpu::ColorWrites::ALL,     // 允许写入所有颜色通道
                })],
                compilation_options: Default::default(),
            }),
            primitive: Default::default(), // 使用默认图元配置(三角形列表)
            depth_stencil: None,           // 禁用深度/模板测试
            multisample: Default::default(), // 多重采样配置
            multiview: None,
            cache: None,
        });

        let texture_data = gen_texture_data();
        let texture_size = wgpu::Extent3d {
            width: 5,
            height: 7,
            ..Default::default()
        };
        let texture = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("texture"),
            size: texture_size,
            format: wgpu::TextureFormat::Rgba8Unorm,
            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            view_formats: &[],
        });

        queue.write_texture(
            wgpu::TexelCopyTextureInfoBase {
                texture: &texture,
                mip_level: 0,
                origin: wgpu::Origin3d::ZERO,
                aspect: wgpu::TextureAspect::All,
            },
            &texture_data,
            wgpu::TexelCopyBufferLayout {
                offset: 0,
                bytes_per_row: Some(texture_size.width * 4),
                rows_per_image: None,
            },
            texture_size,
        );
        // 创建控件
        let controls = controls::Controls::new();

        // 创建采样器
        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
            address_mode_u: controls.address_mode_u,
            address_mode_v: controls.address_mode_v,
            mag_filter: controls.mag_filter,
            ..Default::default()
        });

        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: None,
            layout: &pipeline.get_bind_group_layout(0),
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: wgpu::BindingResource::Sampler(&sampler),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: wgpu::BindingResource::TextureView(
                        &texture.create_view(&Default::default()),
                    ),
                },
            ],
        });

        // 创建缩放缓冲区
        let scale = calc_scale([5.0, 7.0], [config.width as f32, config.height as f32]);
        let scale_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some("Scale Buffer"),
            contents: bytemuck::cast_slice(&scale),
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
        });
        let scale_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: None,
            layout: &pipeline.get_bind_group_layout(1),
            entries: &[wgpu::BindGroupEntry {
                binding: 0,
                resource: scale_buffer.as_entire_binding(),
            }],
        });

        Ok(Self {
            window,
            surface,
            device,
            queue,
            config,
            pipeline,
            bind_group,
            egui_renderer: egui_render,
            texture,
            controls,
            scale_bind_group,
            scale_buffer,
            image_dimensions: [5.0, 7.0],
        })
    }

    /// 执行渲染操作
    pub fn render(&mut self) -> Result<()> {
        // 1. 获取当前帧缓冲区
        let output = self.surface.get_current_texture()?;

        // 2. 创建纹理视图
        let view = output
            .texture
            .create_view(&wgpu::TextureViewDescriptor::default());

        // 3. 创建命令编码器
        let mut encoder = self
            .device
            .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());

        let screen_descriptor = ScreenDescriptor {
            size_in_pixels: [self.config.width, self.config.height],
            pixels_per_point: self.window.as_ref().scale_factor() as f32,
        };

        // 4. 开始渲染通道
        {
            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("Render Pass"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: &view,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Clear(Color::BLACK), // 用黑色清除背景
                        store: wgpu::StoreOp::Store,             // 存储渲染结果
                    },
                    resolve_target: None,
                })],
                depth_stencil_attachment: None,
                timestamp_writes: None,
                occlusion_query_set: None,
            });

            // 5. 设置渲染管线
            pass.set_pipeline(&self.pipeline);

            // 6. 设置绑定组
            pass.set_bind_group(0, &self.bind_group, &[]);
            pass.set_bind_group(1, &self.scale_bind_group, &[]);
            // 7. 使用实例化绘制
            pass.draw(0..6, 0..1);
        }

        {
            // 开始Egui帧渲染
            self.egui_renderer.begin_frame(&self.window);

            // 渲染控件并处理用户交互
            self.controls
                .render(self.egui_renderer.context(), |controls| {
                    // 检查控件中是否有新的图像URL
                    let url = controls.image_url;
                    if !url.is_empty() {
                        // 如果URL不为空,尝试加载图像数据
                        if let Ok(image) = utils::load_image_data(&url) {
                            log::info!("Loaded image from URL: {}", url);
                            let (width, height) = image.dimensions();
                            self.image_dimensions = [width as f32, height as f32];

                            // 计算缩放比例并更新缓冲区
                            let scale = calc_scale(
                                self.image_dimensions,
                                [self.config.width as f32, self.config.height as f32],
                            );
                            self.queue.write_buffer(
                                &self.scale_buffer,
                                0,
                                bytemuck::cast_slice(&scale),
                            );

                            // 创建新的纹理并加载图像数据
                            let texture_size = wgpu::Extent3d {
                                width,
                                height,
                                ..Default::default()
                            };

                            // 将图像数据转换为RGBA8格式
                            // flipv()用于翻转图像数据
                            let texture_data = image.flipv().to_rgba8().to_vec();

                            self.texture = self.device.create_texture_with_data(
                                &self.queue,
                                &wgpu::TextureDescriptor {
                                    label: Some("net_texture"),
                                    size: texture_size,
                                    format: wgpu::TextureFormat::Rgba8UnormSrgb,
                                    usage: wgpu::TextureUsages::TEXTURE_BINDING
                                        | wgpu::TextureUsages::COPY_DST
                                        | wgpu::TextureUsages::RENDER_ATTACHMENT,
                                    mip_level_count: 1,
                                    sample_count: 1,
                                    dimension: wgpu::TextureDimension::D2,
                                    view_formats: &[],
                                },
                                wgpu::util::TextureDataOrder::MipMajor,
                                &texture_data,
                            );
                        }
                    }

                    // 根据控件设置创建采样器
                    let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
                        address_mode_u: controls.address_mode_u,
                        address_mode_v: controls.address_mode_v,
                        mag_filter: controls.mag_filter,
                        ..Default::default()
                    });

                    // 创建绑定组并更新
                    let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
                        label: None,
                        layout: &self.pipeline.get_bind_group_layout(0),
                        entries: &[
                            wgpu::BindGroupEntry {
                                binding: 0,
                                resource: wgpu::BindingResource::Sampler(&sampler),
                            },
                            wgpu::BindGroupEntry {
                                binding: 1,
                                resource: wgpu::BindingResource::TextureView(
                                    &self.texture.create_view(&Default::default()),
                                ),
                            },
                        ],
                    });
                    self.bind_group = bind_group;
                });

            // 结束Egui帧并绘制
            self.egui_renderer.end_frame_and_draw(
                &self.device,
                &self.queue,
                &mut encoder,
                &self.window,
                &view,
                screen_descriptor,
            );
        }

        // 7. 提交命令到队列
        let command_buffer = encoder.finish();
        self.queue.submit(std::iter::once(command_buffer));

        // 8. 呈现渲染结果
        output.present();

        Ok(())
    }

    /// 处理窗口大小变化
    pub fn resize(&mut self, size: winit::dpi::PhysicalSize<u32>) {
        self.config.width = size.width.max(1);
        self.config.height = size.height.max(1);
        // 重新配置表面(更新尺寸)
        self.surface.configure(&self.device, &self.config);

        // 更新缩放比例
        let scale = calc_scale(
            self.image_dimensions,
            [self.config.width as f32, self.config.height as f32],
        );
        self.queue
            .write_buffer(&self.scale_buffer, 0, bytemuck::cast_slice(&scale));
    }
}

然后运行

bash 复制代码
cargo run

在网上随便找一张图片,输入URL,点击Load按钮,就可以看到效果了。

通过上面的代码,你可以在WGPU中创建一个简单的图像渲染程序,并使用Egui来处理用户界面。可以发现,加载图片纹理的过程非常简单,只需要使用image库来加载图片数据,然后将其转换为WGPU可以使用的纹理格式即可。

最后

本节源码位于Github

如果本文对你有启发,欢迎点赞⭐收藏📚关注👀,你的支持是我持续创作深度技术内容的最大动力。

相关推荐
小墨宝23 分钟前
js 生成pdf 并上传文件
前端·javascript·pdf
HED38 分钟前
用扣子快速手撸人生中第一个AI智能应用!
前端·人工智能
DN金猿42 分钟前
使用npm install或cnpm install报错解决
前端·npm·node.js
丘山子43 分钟前
一些鲜为人知的 IP 地址怪异写法
前端·后端·tcp/ip
志存高远661 小时前
Kotlin 的 suspend 关键字
前端
www_pp_1 小时前
# 构建词汇表:自然语言处理中的关键步骤
前端·javascript·自然语言处理·easyui
天天扭码2 小时前
总所周知,JavaScript中有很多函数定义方式,如何“因地制宜”?(ˉ﹃ˉ)
前端·javascript·面试
一个专注写代码的程序媛2 小时前
为什么vue的key值,不用index?
前端·javascript·vue.js
장숙혜2 小时前
ElementUi的Dropdown下拉菜单的详细介绍及使用
前端·javascript·vue.js
火柴盒zhang2 小时前
websheet之 编辑器
开发语言·前端·javascript·编辑器·spreadsheet·websheet