WebGPU用矩阵变换图形

552 阅读5分钟

课件:github.com/buglas/webg…

知识点:向顶点着色器传递矩阵数据

1-基本思路

对于矩阵概念,我们这里就不做详解了。

矩阵是图形学的基础,如果有同学没有学过矩阵,可以看一下闫令琪老师的课

我们接下要说的是attribute数据、uniform数据和顶点着色器、片元着色器的对应关系。

其实,这个对应关系和WebGL 是一样的。

  • attribute 变量

    • 对应顶点相关的数据。这么说可能有点笼统,我们可以再详细点,attribute 对应的是与顶点存在一对一关系的数据。
    • 只能出现在顶点着色器里
  • uniform 变量:

    • 对应的是与点前渲染管线pipeline 里的所有顶点都存在关系的数据,它跟顶点的关系式一对全部的。
    • 可以出现在顶点着色器里,也可以出现在片元着色器里。

我们可以把attribute 理解为基于某一个顶点的局部变量,而uniform 则是基于所有顶点的全局变量。

接下来,我们所要说的矩阵,就属于uniform 遍历,因为它是要对pipeline 里的所有顶点进行变换的。

2-用矩阵变换图形

接下我们会以模型矩阵为例,说一下如何缩放一个三角形。

我接着之前js向片元着色器传递参数的代码改了。

1.声明模型矩阵modelMatrix。

const modelMatrix=new Float32Array([
    0.5,0,0,0,
    0,0.5,0,0,
    0,0,0.5,0,
    0,0,0,1,
])

这个矩阵会把模型缩小一半。

2.在建立渲染管线的时候,建立用于存储模型矩阵的缓冲对象。

//模型矩阵的缓冲区
const modelBuffer = device.createBuffer({
    size: 4 * 4 * 4, //行数*列数*BYTES_PER_ELEMENT
    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
})
device.queue.writeBuffer(modelBuffer, 0, modelMatrix)

3.将模型矩阵的缓冲对象添加到BindGroup里,这样我们才能在着色器里通过BindGroup索引获取相应数据。

// 对buffer进行组合
const uniformGroup = device.createBindGroup({
    // 布局
    layout: pipeline.getBindGroupLayout(0),
    // 添加buffer
    entries: [
        // 图形颜色
        {
            // 位置
            binding: 0,
            // 资源
            resource: {
                buffer: colorBuffer,
            },
        },
        //  模型矩阵
        {
            // 位置
            binding: 1,
            // 资源
            resource: {
                buffer: modelBuffer,
            },
        },
    ],
})

在当前的BindGroup已经有两个uniform 数据了:

  • 图形颜色,会在片元着色器里整体设置所有顶点所定义的片元区域的颜色。
  • 模型矩阵,会在顶点着色器里变换所有顶点。

4.在顶点着色器里获取并应用模型矩阵。

@group(0) @binding(1) var<uniform> modelMatrix:mat4x4<f32>;

@vertex
fn main(@location(0) position : vec3<f32>) -> @builtin(position) vec4<f32> {
  return modelMatrix*vec4<f32>(position, 1.0);
}
  • @group(0) 对应BindGroup里的layout:pipeline.getBindGroupLayout(0)
  • @binding(1) 对应BindGroup里的binding: 1
  • @location(0) 对应Pipeline 里的shaderLocation: 0

现在我们便实现了模型矩阵对图形的缩放,效果如下:

image-20220701101207753

整体代码如下:

  • /src/matrix.ts
import positionVert from "./shaders/matrix.vert.wgsl?raw"
import colorFrag from "./shaders/color.frag.wgsl?raw"

// 初始化WebGPU
async function initWebGPU(canvas: HTMLCanvasElement) {
    // 判断当前设备是否支持WebGPU
    if (!navigator.gpu) throw new Error("Not Support WebGPU")
    // 请求Adapter对象,GPU在浏览器中的抽象代理
    const adapter = await navigator.gpu.requestAdapter({
        /* 电源偏好
            high-performance 高性能电源管理
            low-power 节能电源管理模式 
        */
        powerPreference: "high-performance",
    })
    if (!adapter) throw new Error("No Adapter Found")
    //请求GPU设备
    const device = await adapter.requestDevice()
    //获取WebGPU上下文对象
    const context = canvas.getContext("webgpu") as GPUCanvasContext
    //获取浏览器默认的颜色格式
    const format = navigator.gpu.getPreferredCanvasFormat()
    //设备分辨率
    const devicePixelRatio = window.devicePixelRatio || 1
    //canvas尺寸
    const size = {
        width: canvas.clientWidth * devicePixelRatio,
        height: canvas.clientHeight * devicePixelRatio,
    }
  canvas.width = size.width
    canvas.height =size.height
    //配置WebGPU
    context.configure({
        device,
        format,
        // Alpha合成模式,opaque为不透明
        alphaMode: "opaque",
    })

    return { device, context, format, size }
}

// 顶点点位
const vertex = new Float32Array([
    // 0
    0, 0.5, 0,
    // 1
    -0.5, -0.5, 0,
    // 2
    0.5, -0.5, 0.0,
])
// 顶点颜色
const color = new Float32Array([1, 1, 0, 1])

// 模型矩阵
const modelMatrix=new Float32Array([
    0.5,0,0,0,
    0,0.5,0,0,
    0,0,0.5,0,
    0,0,0,1,
])

// 创建渲染管线
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 modelBuffer = device.createBuffer({
        size: 4 * 4 * 4, //行数*列数*BYTES_PER_ELEMENT
        usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
    })
  device.queue.writeBuffer(modelBuffer, 0, modelMatrix)

    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,
                },
            ],
        },
        // 初始配置
        primitive: {
            //拓扑结构,triangle-list为绘制独立三角形
            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,
                },
            },
      //  模型矩阵
            {
                // 位置
                binding: 1,
                // 资源
                resource: {
                    buffer: modelBuffer,
                },
            },
        ],
    })
    //返回异步管线、顶点缓冲区、BindGroup
    return { pipeline, vertexBuffer, uniformGroup }
}

// 编写绘图指令,并传递给本地的GPU设备
function draw(
    device: GPUDevice,
    context: GPUCanvasContext,
    pipelineObj: {
        pipeline: GPURenderPipeline
        vertexBuffer: GPUBuffer
        uniformGroup: GPUBindGroup
    }
) {
    // 创建指令编码器
    const commandEncoder = device.createCommandEncoder()
    // GPU纹理视图
    const view = context.getCurrentTexture().createView()
    // 渲染通道配置数据
    const renderPassDescriptor: GPURenderPassDescriptor = {
        // 颜色附件
        colorAttachments: [
            {
                view: view,
                // 绘图前是否清空view,建议清空clear
                loadOp: "clear", // clear/load
                // 清理画布的颜色
                clearValue: { r: 0, g: 0, b: 0, a: 1.0 },
                //绘制完成后,是否保留颜色信息
                storeOp: "store", // store/discard
            },
        ],
    }
    // 建立渲染通道,类似图层
    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])
}

async function run() {
    const canvas = document.querySelector("canvas")
    if (!canvas) throw new Error("No Canvas")
    // 初始化WebGPU
    const { device, context, format } = await initWebGPU(canvas)
  // 初始化Pipeline
    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()

现在我们已经掌握向顶点着色器传递矩阵数据的方法,视图矩阵和投影矩阵也是按照同样原理来传递的,这个我会在后面的课程里说。

下节课,我们会先说一下多attribute变量,即在一个顶点着色器里写入多个与顶点相关的attribute变量,如顶点点位和颜色。