在 WebGPU 的片元着色器中訪問幀緩衝座標


1. 技術說明

  • 使用最新 Edge/Chrome Canary 瀏覽器
  • 使用 VSCode 插件 LiveServer 的 HTTP 服務器對本機提供 5500 端口的頁面服務,即 http://localhost:5500/index.html
  • 使用 es-module 風格的 JavaScript 實現

2. 三角形例子

先上效果,後面再解析片元着色器:

image

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 規範中有說明,它的座標軸、原點和座標值域是這樣的:

image


我對幀緩衝座標進行了縮放、平移,也就是計算了 xy,將原點移動到 canvas 中央,然後把座標區間從 [0, 600] 映射到 [-1, 1]

然後就是最後的那個多步邏輯分支了,也很簡單:

  • 第一個 if 對應圖中的 粉色
  • 第二個 if 對應圖中的 藍色
  • 第三個 if 對應圖中三角形區域內、藍色、粉色像素外的 圓區域,半徑是 0.4(映射後的半徑),它使用幀緩衝座標作爲顏色值(除以幀緩衝的長寬 600 映射到了 [0, 1]),加了 0.5 的透明度
  • 最後其它的片元使用語句 discard 丟棄,即不渲染

OK,今天就學到這裏。

源碼

微雲

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章