在 WebGPU 的片元着色器中访问帧缓冲坐标
1. 技术说明
- 使用最新 Edge/Chrome Canary 浏览器
- 使用 VSCode 插件
LiveServer
的 HTTP 服务器对本机提供 5500 端口的页面服务,即http://localhost:5500/index.html
- 使用 es-module 风格的 JavaScript 实现
2. 三角形例子
先上效果,后面再解析片元着色器:
HTML
html 部分就简单一些
<canvas id="c" width="600" height="600" style="border: 1px solid darkseagreen;"></canvas>
<script type="module" src="./main.js"></script>
不出意外的话,你可以看到一个带暗绿色边框的 canvas,长宽均为 600 像素。
JavaScript
JavaScript 代码也比较简单,省略大部分动态代码和有无判断代码:
const canvas = document.getElementById('c')
const shaderText = `/* 着色器代码,后面会给 */`
const init = async () => {
const adapter = await navigation.gpu.requestAdapter()
const device = await adapter.requestDevice()
const context = canvas.getContext('webgpu')
const presentationFormat = context.getPreferredFormat(adapter)
context.configure({
device,
format: presentationFormat,
size: [ 600, 600 ], // canvas 的画图尺寸
})
const pipeline = device.createRenderPipeline({
vertex: {
module: device.createShaderModule({
code: shaderText
}),
entryPoint: 'vertexMain'
},
fragment: {
module: device.createShaderModule({
code: shaderText
}),
entryPoint: 'fragmentMain',
targets: [{ format: presentationFormat }],
},
primitive: { topology: 'triangle-list' },
})
const render = () => {
/*
每帧创建编码器并“录制”编码过程,最终提交给设备
*/
const commandEncoder = device.createCommandEncoder()
const textureView = context.getCurrentTexture().createView()
const renderPassDescriptor = {
colorAttachments: [
{
view: textureView,
clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
loadOp: 'clear',
storeOp: 'store',
},
],
}
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor)
passEncoder.setPipeline(pipeline)
passEncoder.draw(3, 1, 0, 0)
passEncoder.end()
device.queue.submit([commandEncoder.finish()])
requestAnimationFrame(render)
}
requestAnimationFrame(render)
} // async function init
init()
我保留了完整的 rAF 帧动画结构。
为了方便说明内置在片元着色器中的帧缓冲坐标变量,我将三角形顶点值写死在顶点着色器中,见下文。
3. 着色器解析
着色器代码:
const shaderText = /* wgsl */`
@stage(vertex)
fn vertexMain(
@builtin(vertex_index) VertexIndex: u32
) -> @builtin(position) vec4<f32> {
var pos = array<vec2<f32>, 3>(
vec2<f32>(0.0, 0.5),
vec2<f32>(-0.5, -0.5),
vec2<f32>(0.5, -0.5)
);
return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
}
@stage(fragment)
fn fragmentMain(
@builtin(position) FrameBufferCoord: vec4<f32>
) -> @location(0) vec4<f32> {
var color = vec4<f32>(1.0, 0.5, 0.0, .5);
let x: f32 = (FrameBufferCoord.x - 300.0) / 300.0;
let y: f32 = (-FrameBufferCoord.y + 300.0) / 300.0;
let r: f32 = sqrt(x * x + y * y);
if (x > -0.1 && x < 0.1) {
return vec4<f32>(1.0, 0.0, 0.5, 1.0);
} else if (y > -0.1 && y < 0.1) {
return vec4<f32>(0.0, 0.5, 1.0, 1.0);
} else if (r < 0.4) {
return vec4<f32>(FrameBufferCoord.rgb / 600.0, 0.5);
} else {
discard;
}
}
`
WGSL 褒贬不一,就不说它的语法如何了。
主要是看片元着色器的输入,@builtin(position) FrameBufferCoord: vec4<f32>
,它向每一个片元着色器传入了当前片元的帧缓冲坐标,类型是 vec4<f32>
帧缓冲坐标图示
帧缓冲在 WebGPU 规范中有说明,它的坐标轴、原点和坐标值域是这样的:
我对帧缓冲坐标进行了缩放、平移,也就是计算了 x
、y
,将原点移动到 canvas 中央,然后把坐标区间从 [0, 600]
映射到 [-1, 1]
。
然后就是最后的那个多步逻辑分支了,也很简单:
- 第一个 if 对应图中的 粉色
- 第二个 if 对应图中的 蓝色
- 第三个 if 对应图中三角形区域内、蓝色、粉色像素外的 圆区域,半径是 0.4(映射后的半径),它使用帧缓冲坐标作为颜色值(除以帧缓冲的长宽 600 映射到了
[0, 1]
),加了 0.5 的透明度 - 最后其它的片元使用语句
discard
丢弃,即不渲染
OK,今天就学到这里。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
2020-04-27 git 版本回退 速查
2019-04-27 SpaceSyntax【空间句法】之DepthMapX学习:唠叨(目录)