WebGPU-js向着色器传参

656 阅读7分钟

课件:github.com/buglas/webg…

知识点:

  • js向顶点着色器传递数据
  • js向片元着色器传递数据

前言

在上节课我们画三角形的时候,用到了一对着色器:

  • 顶点着色器,负责塑形
@vertex
fn main(@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);
}
  • 片元着色器,负责着色
@fragment
fn main() -> @location(0) vec4<f32> {
    return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}

当前的这两个着色器里的顶点点位和片元颜色都是写死的,接下来咱们说一下如何用js向着两个这色器传递参数。

1-js向顶点着色器传递数据

接着咱们上节课画的三角形做修改。

1.准备一份顶点数据。

const vertex = new Float32Array([
    // 0
    0, 0.5, 0,
    // 1
    -0.5, -0.5, 0,
    // 2
    0.5, -0.5, 0.0,
])

这是一个三角形。

2.为顶点数据创建缓冲区对象,设置其尺寸和用途,使其作用于顶点着色器,并可写。

// 创建渲染管线
async function initPipeline(
    device: GPUDevice,
    format: GPUTextureFormat
): Promise<GPURenderPipeline> {
    // 建立顶点缓冲区
    const vertexBuffer = device.createBuffer({
        // 顶点长度,以字节为单位
        size: vertex.byteLength,
        // 用途,用于顶点着色,可写
        usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
    })
  ……
}

这里的顶点缓冲区的概念和webgl 里的顶点缓冲区是一个原理的。它就是一块存储空间,先让js 把顶点数据放进去,然后再让着色器从里面读取。

至于为什么js不通过一个简单的方法直接把顶点数据传递给着色器,这是因为js和着色器用的是两种不一样的语言,它们无法直接对话,因此需要一个缓冲地带,也就是缓冲区对象。

GPUBufferUsage.VERTEX 里VERTEX 的概念和WebGL 里attribute 变量的概念类似,都表示一种与顶点相关的变量。

3.把顶点数据写入到上面建立的缓冲区对象。

device.queue.writeBuffer(vertexBuffer, 0, vertex)

writeBuffer(buffer,bufferOffset,data)

  • buffer 缓冲区对象
  • bufferOffset 从数据源的什么位置写入数据,以字节为单位
  • data 数据源

4.建立顶点着色文件,在其中获取js传递的顶点数据。

  • src/shaders/position.frag.wgsl
@vertex
fn main(@location(0) position : vec3<f32>) -> @builtin(position) vec4<f32> {
    return vec4<f32>(position, 1.0);
}

上面的@location(0) 对应的便是js传递进来的顶点数据。

返回的vec4(position, 1.0) 是32位浮点类型的齐次坐标。

wgsl的语法规则和webgl 的着色语言有很多共通之处,所以有WebGL 基础的同学学习WebGPU 是很简单的。

5.把顶点着色代码传递给顶点着色器。

import positionVert from "./shaders/position.vert.wgsl?raw"
……

// 创建渲染管线
async function initPipeline(
    device: GPUDevice,
    format: GPUTextureFormat
){
    // 建立顶点缓冲区
    const vertexBuffer = device.createBuffer({
        // 顶点长度
        size: vertex.byteLength,
        // 用途,用于顶点着色,可写
        usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
    })
    // 写入数据
    device.queue.writeBuffer(vertexBuffer, 0, vertex)

    const descriptor: GPURenderPipelineDescriptor = {
        // 顶点着色器
        vertex: {
            // 着色程序
            module: device.createShaderModule({
                code: positionVert,
            }),
            // 主函数
            entryPoint: "main",
            //缓冲数据,1个渲染管道可最多传入8个缓冲数据
            buffers: [
                {
                    // 顶点长度,以字节为单位
                    arrayStride: 3 * 4,
                    attributes: [
                        {
                            // 遍历索引
                            shaderLocation: 0,
                            // 偏移
                            offset: 0,
                            // 参数格式
                            format: "float32x3",
                        },
                    ],
                },
            ],
        },
        // 片元着色器
        fragment: {
            // 着色程序
            module: device.createShaderModule({
                code: redFrag,
            }),
            // 主函数
            entryPoint: "main",
            // 渲染目标
            targets: [
                {
                    // 颜色格式
                    format: format,
                },
            ],
        },
        // 初始配置
        primitive: {
            //拓扑结构,triangle-list为绘制独立三角形
            topology: "triangle-list",
        },
        // 渲染管线的布局
        layout: "auto",
    }
    // 创建异步管线
    const pipeline = await device.createRenderPipelineAsync
  //返回异步管线、顶点缓冲区
    return { pipeline, vertexBuffer}
}

顶点着色器中的buffers属性就是缓冲区集合,其中一个元素对应一个缓冲对象。

缓冲对象在buffers 里索引值对应的就是着色器语言中@location(0)里的数字。

6.创建绘图指令的时候,把顶点缓冲区写入渲染通道中。

function draw(
    device: GPUDevice,
    context: GPUCanvasContext,
    pipelineObj: {
        pipeline: GPURenderPipeline
        vertexBuffer: GPUBuffer
    }
) {
    ……
    // 建立渲染通道,类似图层
    const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor)
    // 传入渲染管线
    passEncoder.setPipeline(pipelineObj.pipeline)
    // 写入顶点缓冲区
    passEncoder.setVertexBuffer(0, pipelineObj.vertexBuffer)
    // 绘图,3 个顶点
    passEncoder.draw(3)
    // 结束编码
    passEncoder.end()
    // 结束指令编写,并返回GPU指令缓冲区
    const gpuCommandBuffer = commandEncoder.finish()
    // 向GPU提交绘图指令,所有指令将在提交后执行
    device.queue.submit([gpuCommandBuffer])
}

7.绘图

async function run() {
    const canvas = document.querySelector("canvas")
    if (!canvas) throw new Error("No Canvas")
    // 初始化WebGPU
    const { device, context, format } = await initWebGPU(canvas)
    // 初始化渲染管道
      const pipelineObj = await initPipeline(device, format)
    //绘图
    draw(device, context, pipelineObj)

    // re-configure context on resize
    window.addEventListener("resize", () => {
    canvas.width=canvas.clientWidth * devicePixelRatio
    canvas.height=canvas.clientHeight * devicePixelRatio
        context.configure({
            device,
            format,
            alphaMode: "opaque",
        })
        draw(device, context, pipelineObj)
    })
}
run()

效果如下:

image-20220519192421905

关于js向顶点着色器传递顶点数据的原理咱们就说到这,接下来咱们说一下js 如何向片元着色传递数据,比如顶点颜色。

2-js向片元着色器传递数据

1.准备一个颜色。

const color = new Float32Array([1, 0, 0, 1])

上面类型数组里的4个数字分别对应着颜色的rgba,其定义域的[0,1]。

2.为上面的颜色创建缓冲区对象,设置其尺寸和用途,使其作用于片元着色器,并可写。

const colorBuffer = device.createBuffer({
  size: color.byteLength, //4 * 4,
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
})

上面GPUBufferUsage.UNIFORM 里UNIFORM 的概念与WebGL 里的uniform 变量类似,都表示一种变量类型。

3.把颜色数据写入到上面建立的缓冲区对象。

device.queue.writeBuffer(colorBuffer, 0, color)

4.建立片元着色文件,在其中获取js传递的颜色数据。

  • src/shaders/color.frag.wgsl
@group(0) @binding(0) var<uniform> color : vec4<f32>;
@fragment
fn main() -> @location(0) vec4<f32> {
    return color;
}

上面的@location(0) 对应的便是js传递进来的颜色数据。

5.把片元着色代码传递给片元着色器。

import colorFrag from "./shaders/color.frag.wgsl?raw"
……

// 创建渲染管线
async function initPipeline(device: GPUDevice, format: GPUTextureFormat) {
    // 建立顶点缓冲区
    const vertexBuffer = device.createBuffer({
        // 顶点长度
        size: vertex.byteLength,
        // 用途,用于顶点着色,可写
        usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
    })
    // 写入数据
    device.queue.writeBuffer(vertexBuffer, 0, vertex)

    // 缓冲区对象-存储颜色
    const colorBuffer = device.createBuffer({
        size: color.byteLength, //4 * 4,
        usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
    })
    // 写入数据
    device.queue.writeBuffer(colorBuffer, 0, color)

    const descriptor: GPURenderPipelineDescriptor = {
        // 顶点着色器
        vertex: {
            // 着色程序
            module: device.createShaderModule({
                code: positionVert,
            }),
            // 主函数
            entryPoint: "main",
            //缓冲数据,1个渲染管道可最多传入8个缓冲数据
            buffers: [
                {
                    // 顶点长度,以字节为单位
                    arrayStride: 3 * 4,
                    attributes: [
                        {
                            // 遍历索引
                            shaderLocation: 0,
                            // 偏移
                            offset: 0,
                            // 参数格式
                            format: "float32x3",
                        },
                    ],
                },
            ],
        },
        // 片元着色器
        fragment: {
            // 着色程序
            module: device.createShaderModule({
                code: colorFrag,
            }),
            // 主函数
            entryPoint: "main",
            // 渲染目标
            targets: [
                {
                    // 颜色格式
                    format: format,
                },
            ],
        },
        // 初始配置
        primitive: {
            //绘制独立三角形
            topology: "triangle-list",
        },
        // 渲染管线的布局
        layout: "auto",
    }
    // 创建异步管线
    const pipeline = await device.createRenderPipelineAsync(descriptor)
    // 对buffer进行组合
    const uniformGroup = device.createBindGroup({
        // 布局
        layout: pipeline.getBindGroupLayout(0),
        // 添加buffer
        entries: [
            {
                // 位置
                binding: 0,
                // 资源
                resource: {
                    buffer: colorBuffer,
                },
            },
        ],
    })
    //返回异步管线、顶点缓冲区、BindGroup
    return { pipeline, vertexBuffer, uniformGroup }
}

颜色缓冲区对象在建立完成后,是需要将其装进BindGroup中的,也就是上面声明的uniformGroup,之后我们会将这个BindGroup 传递非渲染通道。

6.创建绘图指令的时候,把含有颜色缓冲区的BindGroup写入渲染通道。

// 编写绘图指令,并传递给本地的GPU设备
function draw(
    device: GPUDevice,
    context: GPUCanvasContext,
    pipelineObj: {
        pipeline: GPURenderPipeline
        vertexBuffer: GPUBuffer
        uniformGroup: GPUBindGroup
    }
) {
    ……
    // 建立渲染通道,类似图层
    const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor)
    // 传入渲染管线
    passEncoder.setPipeline(pipelineObj.pipeline)
    // 把顶点缓冲区写入渲染通道
    passEncoder.setVertexBuffer(0, pipelineObj.vertexBuffer)
    // 把含有颜色缓冲区的BindGroup写入渲染通道
    passEncoder.setBindGroup(0, pipelineObj.uniformGroup)
    // 绘图,3 个顶点
    passEncoder.draw(3)
    // 结束编码
    passEncoder.end()
    // 结束指令编写,并返回GPU指令缓冲区
    const gpuCommandBuffer = commandEncoder.finish()
    // 向GPU提交绘图指令,所有指令将在提交后执行
    device.queue.submit([gpuCommandBuffer])
}

7.绘图

async function run() {
    const canvas = document.querySelector("canvas")
    if (!canvas) throw new Error("No Canvas")
    // 初始化WebGPU
    const { device, context, format } = await initWebGPU(canvas)
    // 初始化渲染管道
  const pipelineObj = await initPipeline(device, format)
    // 绘图
    draw(device, context, pipelineObj)

    // 自适应窗口
    window.addEventListener("resize", () => {
        context.configure({
            device,
            format,
            size: {
                width: canvas.clientWidth * devicePixelRatio,
                height: canvas.clientHeight * devicePixelRatio,
            },
            compositingAlphaMode: "opaque",
        })
        draw(device, context, pipelineObj)
    })
}
run()

效果如下:

image-20220519192421905

总结

通过上面的案例,我们会发现js向顶点着色器和片元着色器传递数据的共同点:都要把数据放缓冲对象里,然后再把缓冲对象传递给渲染通道。

只不过,js向片元着色器传递数据的时候,会多一步操作,它会把缓冲对象打个包,整合到BindGroup中,然后再把BindGroup传递给渲染通道。

关于js向着色器传递顶点数据的原理咱们就说到这,下节课我们会说一下矩阵变换。

参考链接:www.bilibili.com/video/BV13u…