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

各位,共勉。

相关推荐
码事漫谈13 分钟前
解决 Anki 启动器下载错误的完整指南
前端
im_AMBER33 分钟前
Web 开发 27
前端·javascript·笔记·后端·学习·web
蓝胖子的多啦A梦1 小时前
低版本Chrome导致弹框无法滚动的解决方案
前端·css·html·chrome浏览器·版本不同造成问题·弹框页面无法滚动
玩代码1 小时前
vue项目安装chromedriver超时解决办法
前端·javascript·vue.js
訾博ZiBo1 小时前
React 状态管理中的循环更新陷阱与解决方案
前端
StarPrayers.2 小时前
旅行商问题(TSP)(2)(heuristics.py)(TSP 的两种贪心启发式算法实现)
前端·人工智能·python·算法·pycharm·启发式算法
一壶浊酒..2 小时前
ajax局部更新
前端·ajax·okhttp
苏打水com2 小时前
JavaScript 面试题标准答案模板(对应前文核心考点)
javascript·面试
Wx-bishekaifayuan2 小时前
基于微信小程序的社区图书共享平台设计与实现 计算机毕业设计源码44991
javascript·vue.js·windows·mysql·pycharm·tomcat·php
DoraBigHead3 小时前
React 架构重生记:从递归地狱到时间切片
前端·javascript·react.js