前言
本章内容简单分享一下 纹理采样 ,结合之前的例子,完成一个贴图效果,让例子更接近我们预期的效果。本章代码比较简单,较重理论,哪怕不管理论,使用默认值,最终的效果也并不会差。
集成效果
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
...
片元填充
根据我们之前分享的内容,我们知道在shader
的fragment
填充指定的内容,之前,我们填充的是颜色,红色三角形 节选如下:
js
@fragment
fn main() -> @location(0) vec4<f32> {
return vec4(1.0, 0.0, 0.0, 1.0);
}
这里代码的意思是:所有片元都填充红色。
而,图片是由不同的像素点组成的,因此,我们理论上是可以计算不同片元的位置,结合图片,将图片按照像素点,像贴图一样贴上去,完成图像的渲染。
uv坐标
然而,计算片元、图片的工作量和算法都是很大的,应该由WebGPU完成;实际上,也是WebGPU完成的。就算如此,我们需要设置图片的相关 大小和规则 等,片元还是不符合使用预期的。
符合使用直觉的,应当是设置图片,片元组成的面 的位置,其他的交由WebGPU处理。
因此,引入了 uv坐标:
- uv可以理解为xy轴,空间坐标使用了xy,所以,使用uv作区别
- uv是指 片元组成的面,相对图片的位置
- 图片的大小相对于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的顺序要一致
- 面和图片一致
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)
- 放大、缩小
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 的关系了,还不清晰的同学,可以再对比一下。
填充和采样
有基础好一些的同学,可能已经发现问题了,例如:
- 缩小的图形,空白地方应该如何处理?
- 如果图片较小,但是,顶点组成的面远大于图片,经过拉伸后,是否会有空白像素点?
- 在3D场景下存在大量缩放、形变情况,是如何处理的?
- ...
填充
顾名思义,就是对于缩小场景下空白区域的处理,WebGPU提供了水平和垂直方向上的处理,即,可以 单独配置uv方向 的填充方式,具体设置如下:
- clamp-to-edge:复用边缘的颜色
- repeat:重复
- mirror-repeat:反转重复
简单图解(from orillusion):
采样
在图形学中,只要音视频的质量(图片像素)与输入的格式不匹配都需要 重采样 。例如:一张 100*100像素点 的图片,要在 200*200像素点 的区域铺满,按照直观理解,应该有部分像素点是空缺的,这会导致图像显示异常,因此,需要经过算法,重新计算 200*200像素点 区域每个点应该放什么颜色。
WebGPU提供了两种默认的采样方式:
- nearest:临近采样,结果:图像放到最大时出现锯齿
- linear:线性采样,结果:图像放到最大时,图像边缘模糊过渡
- 其他非内置算法...
简单图解(from orillusion):
各种算法有各自的优劣势,WebGPU内置的两种算法已经够用了
示例代码
素材资源
因为WebGPU是在新版本浏览器引入的,所以,对应的浏览器支持更高压缩的素材,我们也应该注意素材资源的利用,尽量使用WEBP、AVIF等 更高压缩率的资源
js中的代码
- 加载图片素材,通过
createImageBitmap
方法将图片转化为bitmap
格式,因为,js要传输到gpu中,需要基础类型,而返回的blob类型
并不合适。
js
/** 通过fetch加载图片资源 */
const res = await fetch(imgUrl)
const bitmap = await createImageBitmap(await res.blob())
- 创建纹理,并将图片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)
- 创建采样器,设置采样规则
js
/** 创建采样规则 */
const sampler = device.createSampler({
/**
* 自带采样算法:
* nearest:临近采样,结果:图像放到最大时出现锯齿;
* linear:线性采样,结果:图像放到最大时,图像边缘模糊过渡
*
* 通常处理方式:可以选择更高质量素材(图片)
* */
magFilter: 'linear',
minFilter: 'linear',
/** 图像uv小于显示面积,空白部分应该如何处理:
* clamp-to-edge:临近采样,取空白部分最接近图像的片元颜色;
* repeat:图像重复;
* mirror-repeat:图像反转重复
* */
addressModeU: 'repeat',
addressModeV: 'mirror-repeat'
})
- 将内存纹理、采样器绑定到
group
中,便于shader
中引用
js
const textureGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: sampler },
{ binding: 1, resource: texture.createView() }
]
})
- 设置
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,图形学如浩瀚大海,不愧为 程序员三大浪漫,感动得眼泪直掉;随着慢慢地学习,会发现之前看到的浩瀚大海,只是小水坑。。。
各位,共勉。