5.纹理采样:图片和视频处理

前言

本章内容简单分享一下 纹理采样 ,结合之前的例子,完成一个贴图效果,让例子更接近我们预期的效果。本章代码比较简单,较重理论,哪怕不管理论,使用默认值,最终的效果也并不会差。

集成效果

gif,略卡。

纹理采样

渲染原理

根据顶点、片元文章,我们聊过了WebGPU最底层的实现:由n个 顶点 组成n个 三角形的片元 ,由n个 片元 组成我们需要的,特定形状的面。

片元

细心的同学应该可以观察到,我们上一篇文章中的顶点数据,每一个面都是由6个顶点、两个三角形组成的,如下节选:

js 复制代码
    ...
    // 顶点组成的规则
    primitive: {
      topology: 'triangle-list',
      // cullMode: 'back'
    }
    ...

    ...
    // face1
    +1, -1, +1,
    -1, -1, +1,
    -1, -1, -1,
    +1, -1, -1,
    +1, -1, +1,
    -1, -1, -1,
    // face2
    ...

片元填充

根据我们之前分享的内容,我们知道在shaderfragment填充指定的内容,之前,我们填充的是颜色,红色三角形 节选如下:

js 复制代码
@fragment
fn main() -> @location(0) vec4<f32> {
  return vec4(1.0, 0.0, 0.0, 1.0);
}

这里代码的意思是:所有片元都填充红色

而,图片是由不同的像素点组成的,因此,我们理论上是可以计算不同片元的位置,结合图片,将图片按照像素点,像贴图一样贴上去,完成图像的渲染。

uv坐标

然而,计算片元、图片的工作量和算法都是很大的,应该由WebGPU完成;实际上,也是WebGPU完成的。就算如此,我们需要设置图片的相关 大小和规则 等,片元还是不符合使用预期的。

符合使用直觉的,应当是设置图片,片元组成的面 的位置,其他的交由WebGPU处理。

因此,引入了 uv坐标

  1. uv可以理解为xy轴,空间坐标使用了xy,所以,使用uv作区别
  2. uv是指 片元组成的面,相对图片的位置
  3. 图片的大小相对于uv,是固定的:左上角0,0 - 右下角1,1

图解uv

因为例图是四边形,由两个三角形组成,因此,四边形的组成是6个点:

js 复制代码
  var pos = array<vec2<f32>, 6>( // 建立1个四边形, 由2个三角形组成
    vec2(-0.5, -0.5),
    vec2(0.5, -0.5),
    vec2(-0.5, 0.5),
    vec2(0.5, -0.5),
    vec2(-0.5, 0.5),
    vec2(0.5, 0.5)
  );

为什么用6个点的:topology: 'triangle-list',而不是用4个点的:topology: 'triangle-strip'呢?

在简单的模型下,使用triangle-strip是没问题的,但是,在复杂图形就会带来很多的心智负担,因为后一个点始终与前两个点关联组成新的片元,如果我们构建的事物需要闭合,那么,我们是需要时刻关注前后关系。感兴趣的同学,可以尝试改造一下立方体的数据。

上面我们说了:图片的大小相对于uv,是固定的:左上角0,0 - 右下角1,1,那么,我们如何调整图片的显示内容(缩放等)?

有固定的,就有不固定的,不固定的是片元的每个点,设置 片元的每个点的uv 相对于 图片的uv,而WebGPU内部实现采样,实现具体的效果。

对比上面的顶点坐标和下面的uv进行理解,如下:

注意:顶点的坐标和uv的顺序要一致

  1. 面和图片一致
js 复制代码
    /** 上面四边形四个顶点对应uv值,uv表示2d图像的xy,u=1,v=1表示图像的右下角 */
    vec2(0, 1),
    vec2(1, 1),
    vec2(0, 0),
    vec2(1, 1),
    vec2(0, 0),
    vec2(1, 0)
  1. 放大、缩小
js 复制代码
    /** 
    * uv大于四边形面积,则表示缩小图像,空白地方的处理,
    * 根据device.createSampler的配置处理,
    * uv小于四边形面积,则表示放大图像
    * */
    
    // 放大
    vec2(0, 0.5),
    vec2(0.5, 0.5),
    vec2(0, 0),
    vec2(0.5, 0.5),
    vec2(0, 0),
    vec2(0.5, 0)
    // 缩小
    vec2(0, 2),
    vec2(2, 2),
    vec2(0, 0),
    vec2(2, 2),
    vec2(0, 0),
    vec2(2, 0)

3. 反转

js 复制代码
    /** 反转 */
    vec2(1, 1),
    vec2(0, 1),
    vec2(1, 0),
    vec2(0, 1),
    vec2(1, 0),
    vec2(0, 0)

至此,我们应该了解了 顶点的uv图像uv 的关系了,还不清晰的同学,可以再对比一下。

填充和采样

有基础好一些的同学,可能已经发现问题了,例如:

  1. 缩小的图形,空白地方应该如何处理?
  2. 如果图片较小,但是,顶点组成的面远大于图片,经过拉伸后,是否会有空白像素点?
  3. 在3D场景下存在大量缩放、形变情况,是如何处理的?
  4. ...

填充

顾名思义,就是对于缩小场景下空白区域的处理,WebGPU提供了水平和垂直方向上的处理,即,可以 单独配置uv方向 的填充方式,具体设置如下:

  1. clamp-to-edge:复用边缘的颜色
  2. repeat:重复
  3. mirror-repeat:反转重复

简单图解(from orillusion):

采样

在图形学中,只要音视频的质量(图片像素)与输入的格式不匹配都需要 重采样 。例如:一张 100*100像素点 的图片,要在 200*200像素点 的区域铺满,按照直观理解,应该有部分像素点是空缺的,这会导致图像显示异常,因此,需要经过算法,重新计算 200*200像素点 区域每个点应该放什么颜色。

WebGPU提供了两种默认的采样方式:

  1. nearest:临近采样,结果:图像放到最大时出现锯齿
  2. linear:线性采样,结果:图像放到最大时,图像边缘模糊过渡
  3. 其他非内置算法...

简单图解(from orillusion):

各种算法有各自的优劣势,WebGPU内置的两种算法已经够用了

示例代码

素材资源

因为WebGPU是在新版本浏览器引入的,所以,对应的浏览器支持更高压缩的素材,我们也应该注意素材资源的利用,尽量使用WEBP、AVIF等 更高压缩率的资源

js中的代码

  1. 加载图片素材,通过 createImageBitmap 方法将图片转化为 bitmap 格式,因为,js要传输到gpu中,需要基础类型,而返回的 blob类型 并不合适。
js 复制代码
  /** 通过fetch加载图片资源 */
  const res = await fetch(imgUrl)
  const bitmap = await createImageBitmap(await res.blob())
  1. 创建纹理,并将图片bitmap传入纹理
js 复制代码
  const textureSize = [bitmap.width, bitmap.height]
  /** 创建纹理 */
  const texture = device.createTexture({
    size: textureSize, /** 根据bitmap设置大小 */
    format,
    /** 设置usage, TEXTURE_BINDING: 设置该权限,shader中才能获取;COPY_DST: 允许拷贝,创建在内存,需要拷贝到gpu;RENDER_ATTACHMENT: 纹理在gpu中是以attachment形式表现的 */
    usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
  })
  /** 对于图像使用:copyExternalImageToTexture API, source不仅仅支持图像,也支持canvas */
  device.queue.copyExternalImageToTexture({ source: bitmap }, { texture }, textureSize)
  1. 创建采样器,设置采样规则
js 复制代码
/** 创建采样规则 */
  const sampler = device.createSampler({
    /**
     * 自带采样算法:
     * nearest:临近采样,结果:图像放到最大时出现锯齿;
     * linear:线性采样,结果:图像放到最大时,图像边缘模糊过渡
     *
     * 通常处理方式:可以选择更高质量素材(图片)
     * */
    magFilter: 'linear',
    minFilter: 'linear',
    /** 图像uv小于显示面积,空白部分应该如何处理:
     * clamp-to-edge:临近采样,取空白部分最接近图像的片元颜色;
     * repeat:图像重复;
     * mirror-repeat:图像反转重复
     *  */
    addressModeU: 'repeat',
    addressModeV: 'mirror-repeat'
  })
  1. 将内存纹理、采样器绑定到 group 中,便于 shader 中引用
js 复制代码
  const textureGroup = device.createBindGroup({
    layout: pipeline.getBindGroupLayout(0),
    entries: [
      { binding: 0, resource: sampler },
      { binding: 1, resource: texture.createView() }
    ]
  })
  1. 设置 group
js 复制代码
  ...
  passEncoder.setPipeline(pipeline)
  passEncoder.setBindGroup(0, textureGroup)
  passEncoder.draw(6)
  ...

shader中的代码

shader的代码改动并不多,并且 shader 提供对应的函数textureSample给我们使用。

js 复制代码
/** sampler类型 */
@group(0) @binding(0) var Sampler: sampler;
/** texture_2d针对图像的类型 */
@group(0) @binding(1) var Texture: texture_2d<f32>;

struct VertexObj {
  @builtin(position) position: vec4<f32>,
  @location(0) uv: vec2<f32>
}

@vertex
fn vertex_main(@builtin(vertex_index) VertexIndex: u32) -> VertexObj {
  var pos = array<vec2<f32>, 6>( // 建立1个四边形, 由2个三角形组成
    vec2(-0.5, -0.5),
    vec2(0.5, -0.5),
    vec2(-0.5, 0.5),
    vec2(0.5, -0.5),
    vec2(-0.5, 0.5),
    vec2(0.5, 0.5)
  );

  var uvList = array<vec2<f32>, 6>(
    /** 上面四边形四个顶点对应uv值,uv表示2d图像的xy,u=1,v=1表示图像的右下角 */
    // vec2(0, 1),
    // vec2(1, 1),
    // vec2(0, 0),
    // vec2(1, 1),
    // vec2(0, 0),
    // vec2(1, 0)

    /** 反转 */
    // vec2(1, 1),
    // vec2(0, 1),
    // vec2(1, 0),
    // vec2(0, 1),
    // vec2(1, 0),
    // vec2(0, 0)

    /** 
    * uv大于四边形面积,则表示缩小图像,空白地方的处理,
    * 根据device.createSampler的配置处理,
    * uv小于四边形面积,则表示放大图像
    * */
    vec2(0, 2),
    vec2(2, 2),
    vec2(0, 0),
    vec2(2, 2),
    vec2(0, 0),
    vec2(2, 0)

    // vec2(0, 0.5),
    // vec2(0.5, 0.5),
    // vec2(0, 0),
    // vec2(0.5, 0.5),
    // vec2(0, 0),
    // vec2(0.5, 0)
  );

  var vertexOutput: VertexObj;

  vertexOutput.position = vec4<f32>(pos[VertexIndex], 0.0, 1.0);
  vertexOutput.uv = uvList[VertexIndex];

  return vertexOutput;
}

@fragment
fn fragment_main(fragData: VertexObj) -> @location(0) vec4<f32> {
  /** textureSample提供给图片的内置函数 */
  return textureSample(Texture, Sampler, fragData.uv);
}

视频处理

视频处理改动点也不多,视频从某种角度来说是一系列图片的组合,感兴趣的同学可以直接 拉取代码,对比参考。

js中

视频是一个持续过程,所以,需要进行持续拷贝,并释放上一次拷贝的资源;需要针对视频的API importExternalTexture 进行处理,

js 复制代码
...
  function iframe() {
    /** importExternalTexture 是对于视频的api */
    const texture = device.importExternalTexture({ source: videoDom })
    const textureGroup = device.createBindGroup({
      layout: pipeline.getBindGroupLayout(0),
      entries: [
        {
          binding: 0,
          resource: sampler
        },
        {
          binding: 1,
          resource: texture
        }
      ]
    })

    draw(device, context, pipeline, textureGroup)
    requestAnimationFrame(iframe)
  }
  iframe()
...

shader中

js 复制代码
@fragment
fn fragment_main(fragData: VertexObj) -> @location(0) vec4<f32> {
  /** textureSampleBaseClampToEdge支持图片和视频 */
  return textureSampleBaseClampToEdge(Texture, Sampler, fragData.uv);
}

结语

初学WebGPU,图形学如浩瀚大海,不愧为 程序员三大浪漫,感动得眼泪直掉;随着慢慢地学习,会发现之前看到的浩瀚大海,只是小水坑。。。

各位,共勉。

相关推荐
undefined&&懒洋洋4 分钟前
Web和UE5像素流送、通信教程
前端·ue5
大前端爱好者2 小时前
React 19 新特性详解
前端
随云6322 小时前
WebGL编程指南之着色器语言GLSL ES(入门GLSL ES这篇就够了)
前端·webgl
随云6322 小时前
WebGL编程指南之进入三维世界
前端·webgl
寻找09之夏3 小时前
【Vue3实战】:用导航守卫拦截未保存的编辑,提升用户体验
前端·vue.js
非著名架构师3 小时前
js混淆的方式方法
开发语言·javascript·ecmascript
多多米10054 小时前
初学Vue(2)
前端·javascript·vue.js
敏编程4 小时前
网页前端开发之Javascript入门篇(5/9):函数
开发语言·javascript
柏箱4 小时前
PHP基本语法总结
开发语言·前端·html·php
新缸中之脑4 小时前
Llama 3.2 安卓手机安装教程
前端·人工智能·算法