七、WebGPU 基础入门——Texture 纹理

七、WebGPU 基础入门------Texture 纹理

之前,我们探讨了用于向着色器传递数据的关键数据类型GPUBuffer,这其中包括了UniformBufferStorageBuffer以及VertexBuffer等。今天我们将介绍向着色器传递数据的另一种主要方式------纹理(Texture)。由于作者精力有限,且网络上关于typescript 的代码案例比较多,所以后面的章节代码实践部分只用rust 编写。

什么是纹理?

纹理本质上是存储颜色值的多维数据结构(最常见为2D图像),但其核心价值并非仅作为二维数组存在。与直接使用存储缓冲区不同,纹理通过**采样器(Sampler)**这一专用硬件单元实现高效访问:采样器能够自动从纹理中读取相邻的多个像素值(最多可达16个),并根据采样位置进行插值混合,这在处理纹理过滤(如抗锯齿、缩放)、 mipmaps 或复杂采样模式时至关重要。

创建纹理

首先先创建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>;

@vertex
fn vs(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
    let pos = array(
        // 1st triangle
        vec2f(0.0, 0.0), // center
        vec2f(1.0, 0.0), // right, center
        vec2f(0.0, 1.0), // center, top
        // 2st triangle
        vec2f(0.0, 1.0), // center, top
        vec2f(1.0, 0.0), // right, center
        vec2f(1.0, 1.0), // right, top
    );
    return VertexOutput(vec4f(pos[vertex_index], 0.0, 1.0), pos[vertex_index]);
}

@fragment
fn fs(in: VertexOutput) -> @location(0) vec4f {
    // 纹理采样
    // 第一个参数是纹理对象,第二个参数是采样器对象,第三个参数是纹理坐标
    return textureSample(ourTexture, ourSampler, in.texcoord);
}

然后在lib.rs中: 编写工具函数gen_texture_data,用于创建一个 5x7 的像素化 F。

rust 复制代码
fn gen_texture_data() -> Vec<u8> {
    let red = [255u8, 0, 0, 255]; // 红色
    let yellow = [255, 255, 0, 255]; // 黄色
    let blue = [0, 0, 255, 255]; // 蓝色

    // 定义二维纹理数据结构
    let rows = [
        [blue, red, red, red, red],         // 第一行
        [red, yellow, yellow, yellow, red], // 第二行
        [red, yellow, red, red, red],       // 第三行
        [red, yellow, yellow, red, red],    // 第四行
        [red, yellow, red, red, red],       // 第五行
        [red, yellow, red, red, red],       // 第六行
        [red, red, red, red, red],          // 第七行
    ];

    // 将二维数组展平为一维字节数组
    rows.iter().flatten().flatten().copied().collect()
}

接下来我们在WgpuApp::new中创建纹理:

rust 复制代码
// ...
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 sampler = device.create_sampler(&wgpu::SamplerDescriptor::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()),
            ),
        },
    ],
});

// ...

我们创建了一个 Rgba8Unorm 纹理。rgba8unorm 表示纹理将有有红、绿、蓝和 alpha 值。每个值都是 8 位无符号值,并且在纹理中使用时将进行归一化处理。unorm 表示 unsigned normalized。意思是 "无符号归一化",它将 0255 的值转换为 0.01.0 之间的浮点数值。

换句话说,如果我们在纹理中输入的值是[64, 128, 192, 255],那么着色器中的值最终将是[64 / 255, 128 / 255, 192 / 255, 255 / 255]。或者换一种说法,在 shader 中最终的值是[0.25, 0.50, 0.75, 1.00]。

然后在render函数中绑定纹理:

rust 复制代码
// 设置绑定组
pass.set_bind_group(0, &self.bind_group, &[]);
// 绘制矩形
pass.draw(0..6, 0..1);

然后运行可以看到一个倒着的F出现在画面中

为什么是倒的? 因为画布的坐标系与纹理的坐标系不同。画布的原点是画布的中心点,而纹理的原点是左上角。常见的解决方法是翻转纹理数据。

rust 复制代码
fn gen_texture_data() -> Vec<u8> {
    let red = [255u8, 0, 0, 255]; // 红色
    let yellow = [255, 255, 0, 255]; // 黄色
    let blue = [0, 0, 255, 255]; // 蓝色

    // // 定义二维纹理数据结构
    // let rows = [
    //     [blue, red, red, red, red],         // 第一行
    //     [red, yellow, yellow, yellow, red], // 第二行
    //     [red, yellow, red, red, red],       // 第三行
    //     [red, yellow, yellow, red, red],    // 第四行
    //     [red, yellow, red, red, red],       // 第五行
    //     [red, yellow, red, red, red],       // 第六行
    //     [red, red, red, red, red],          // 第七行
    // ];

    // 定义二维纹理数据结构并翻转
    let rows = [
        [red, red, red, red, red],          // 第七行
        [red, yellow, red, red, red],       // 第六行
        [red, yellow, red, red, red],       // 第五行
        [red, yellow, yellow, red, red],    // 第四行
        [red, yellow, red, red, red],       // 第三行
        [red, yellow, yellow, yellow, red], // 第二行
        [blue, red, red, red, red],         // 第一行
    ];

    // 将二维数组展平为一维字节数组
    rows.iter().flatten().flatten().copied().collect()
}

运行后可以看到 F 变成了正常的方向。

你可能已经注意到,我的图片左上角有一个悬浮窗,这是 egui 提供的一个小部件。egui 是一个用 Rust 编写的即时模式(Immediate Mode)图形用户界面库,我在这里用它实现了一些小控件,方便我们进行学习和演示。

在后面的章节中,我们将使用 egui 来实现一些简单的 UI 界面,帮助我们更好地理解 WebGPU 的使用。

最后

本节源码位于Github

如何集成egui,请参考kaphula/winit-egui-wgpu-template

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

相关推荐
小墨宝17 分钟前
js 生成pdf 并上传文件
前端·javascript·pdf
HED32 分钟前
用扣子快速手撸人生中第一个AI智能应用!
前端·人工智能
DN金猿37 分钟前
使用npm install或cnpm install报错解决
前端·npm·node.js
丘山子37 分钟前
一些鲜为人知的 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