知识点:
- 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()
效果如下:
关于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()
效果如下:
总结
通过上面的案例,我们会发现js向顶点着色器和片元着色器传递数据的共同点:都要把数据放缓冲对象里,然后再把缓冲对象传递给渲染通道。
只不过,js向片元着色器传递数据的时候,会多一步操作,它会把缓冲对象打个包,整合到BindGroup中,然后再把BindGroup传递给渲染通道。
关于js向着色器传递顶点数据的原理咱们就说到这,下节课我们会说一下矩阵变换。