Я исследовал способ webgpu для создания обтравочной маски.
Вот что я пробовал:
const pipeline1 = device.createRenderPipeline({
vertex: {
module: basicShaderModule,
entryPoint: 'vertex_main',
buffers: [{
attributes: [{
shaderLocation: 0,
offset: 0,
format: 'float32x2'
}],
arrayStride: 8,
stepMode: 'vertex'
}],
},
fragment: {
module: basicShaderModule,
entryPoint: 'fragment_main',
targets: [{ format }]
},
primitive: {
topology: 'triangle-strip',
},
layout: 'auto',
})
passEncoder.setPipeline(pipeline1);
const uniformValues1 = new Float32Array(4)
uniformValues1.set([1, 0, 0, 1], 0)
const uniformBuffer1 = device.createBuffer({
size: uniformValues1.byteLength,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(uniformBuffer1, 0, uniformValues1)
passEncoder.setBindGroup(0, device.createBindGroup({
layout: pipeline1.getBindGroupLayout(0),
entries: [
{
binding: 0, resource: {
buffer: uniformBuffer1
}
},
],
}));
const vertices1 = new Float32Array([-1, -1, 1, -1, 1, 1])
const verticesBuffer1 = device.createBuffer({
size: vertices1.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
})
device.queue.writeBuffer(verticesBuffer1, 0, vertices1, 0, vertices1.length)
passEncoder.setVertexBuffer(0, verticesBuffer1);
passEncoder.draw(3);
const pipeline2 = device.createRenderPipeline({
vertex: {
module: basicShaderModule,
entryPoint: 'vertex_main',
buffers: [{
attributes: [{
shaderLocation: 0,
offset: 0,
format: 'float32x2'
}],
arrayStride: 8,
stepMode: 'vertex'
}],
},
fragment: {
module: basicShaderModule,
entryPoint: 'fragment_main',
targets: [{ format }]
},
primitive: {
topology: 'line-strip',
},
layout: 'auto',
})
passEncoder.setPipeline(pipeline2);
const uniformValues2 = new Float32Array(4)
uniformValues2.set([0, 1, 0, 1], 0)
const uniformBuffer2 = device.createBuffer({
size: uniformValues2.byteLength,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(uniformBuffer2, 0, uniformValues2)
passEncoder.setBindGroup(0, device.createBindGroup({
layout: pipeline2.getBindGroupLayout(0),
entries: [
{
binding: 0, resource: {
buffer: uniformBuffer2
}
},
],
}));
const vertices2 = new Float32Array([0, -1, 1, -1, -1, 1, 0, -1])
const verticesBuffer2 = device.createBuffer({
size: vertices2.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
})
device.queue.writeBuffer(verticesBuffer2, 0, vertices2, 0, vertices2.length)
passEncoder.setVertexBuffer(0, verticesBuffer2);
passEncoder.draw(4);
passEncoder.end();
device.queue.submit([commandEncoder.finish()]);
Приведенный выше код рисует путь и содержимое, я ожидаю, что содержимое будет обрезано путем.
Вот текущий результат:
Вот ожидаемый результат:
Я проигнорировал некоторый общий код, потому что stackoverflow жалуется: «Похоже, что ваш пост в основном код; пожалуйста, добавьте некоторые подробности».
Есть бесконечные способы клипа. Несколько с головы
Альфа-маска имеет то преимущество, что ваша маска может смешиваться.
В любом случае, отсечение через трафаретную текстуру означает создание трафаретной текстуры, рендеринг на нее маски, а затем рендеринг других вещей, настроенных на отрисовку только там, где находится маска.
В частности, конвейер, устанавливающий маску, должен быть настроен на что-то вроде
const maskMakingPipeline = device.createRenderPipeline({
...
fragment: {
module,
entryPoint: 'fs',
targets: [],
},
// replace the stencil value when we draw
depthStencil: {
format: 'stencil8',
depthCompare: 'always',
depthWriteEnabled: false,
stencilFront: {
passOp:'replace',
},
},
});
Во фрагменте нет целей, потому что мы рисуем только текстуру трафарета. Мы установили так, что когда передние треугольники отрисовываются к этой текстуре и проходят тест глубины (который установлен на «всегда»), затем «заменяют» трафарет эталонным значением трафарета (мы установим это позже).
Конвейер для рисования второго треугольника (тот, который маскируется) выглядит следующим образом.
const maskedPipeline = device.createRenderPipeline({
...
fragment: {
module,
entryPoint: 'fs',
targets: [{format: presentationFormat}],
},
// draw only where stencil value matches
depthStencil: {
depthCompare: 'always',
depthWriteEnabled: false,
format: 'stencil8',
stencilFront: {
compare: 'equal',
},
},
});
fragment.targets
теперь установлены, потому что мы хотим отобразить цвет. depthStencil
установлен таким образом, что пиксели в треугольниках, обращенных вперед, будут отображаться только в том случае, если трафарет «равен» эталонному значению трафарета.
Во время отрисовки сначала мы визуализируем маску в текстуру трафарета.
{
const pass = encoder.beginRenderPass({
colorAttachments: [],
depthStencilAttachment: {
view: stencilTexture.createView(),
stencilClearValue: 0,
stencilLoadOp: 'clear',
stencilStoreOp: 'store',
}
});
// draw the mask
pass.setPipeline(maskMakingPipeline);
pass.setVertexBuffer(0, maskVertexBuffer);
pass.setStencilReference(1);
pass.draw(3);
pass.end();
}
Для трафарета было установлено значение 0, а для ссылки на трафарет установлено значение 1, поэтому, когда этот проход будет выполнен, будут 1 с, где мы хотим разрешить рендеринг.
Затем мы визуализируем 2-й треугольник в маске.
{
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
clearValue: [0, 0, 0, 1],
loadOp: 'clear',
storeOp: 'store',
}],
depthStencilAttachment: {
view: stencilTexture.createView(),
stencilLoadOp: 'load',
stencilStoreOp: 'store',
}
});
// draw only the mask is
pass.setPipeline(maskedPipeline);
pass.setStencilReference(1);
pass.setVertexBuffer(0, toBeMaskedVertexBuffer);
pass.draw(3);
pass.end();
}
Здесь мы «загружаем» текстуру трафарета обратно перед рендерингом и устанавливаем ссылку на трафарет в 1, поэтому мы будем рисовать только там, где в текстуре трафарета есть 1.
const code = `
struct VSIn {
@location(0) pos: vec4f,
};
struct VSOut {
@builtin(position) pos: vec4f,
};
@vertex fn vs(vsIn: VSIn) -> VSOut {
var vsOut: VSOut;
vsOut.pos = vsIn.pos;
return vsOut;
}
@fragment fn fs(vin: VSOut) -> @location(0) vec4f {
return vec4f(1, 0, 0, 1);
}
`;
(async() => {
const adapter = await navigator.gpu?.requestAdapter();
const device = await adapter?.requestDevice();
if (!device) {
alert('need webgpu');
return;
}
const canvas = document.querySelector("canvas")
const context = canvas.getContext('webgpu');
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device,
format: presentationFormat,
alphaMode: 'opaque',
});
const module = device.createShaderModule({code});
const maskMakingPipeline = device.createRenderPipeline({
label: 'pipeline for rendering the mask',
layout: 'auto',
vertex: {
module,
entryPoint: 'vs',
buffers: [
// position
{
arrayStride: 2 * 4, // 2 floats, 4 bytes each
attributes: [
{shaderLocation: 0, offset: 0, format: 'float32x2'},
],
},
],
},
fragment: {
module,
entryPoint: 'fs',
targets: [],
},
// replace the stencil value when we draw
depthStencil: {
format: 'stencil8',
depthCompare: 'always',
depthWriteEnabled: false,
stencilFront: {
passOp:'replace',
},
},
});
const maskedPipeline = device.createRenderPipeline({
label: 'pipeline for rendering only where the mask is',
layout: 'auto',
vertex: {
module,
entryPoint: 'vs',
buffers: [
// position
{
arrayStride: 2 * 4, // 2 floats, 4 bytes each
attributes: [
{shaderLocation: 0, offset: 0, format: 'float32x2'},
],
},
],
},
fragment: {
module,
entryPoint: 'fs',
targets: [{format: presentationFormat}],
},
// draw only where stencil value matches
depthStencil: {
depthCompare: 'always',
depthWriteEnabled: false,
format: 'stencil8',
stencilFront: {
compare: 'equal',
},
},
});
const maskVerts = new Float32Array([-1, -1, 1, -1, 1, 1]);
const toBeMaskedVerts = new Float32Array([0, -1, 1, -1, -1, 1]);
const maskVertexBuffer = device.createBuffer({
size: maskVerts.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(maskVertexBuffer, 0, maskVerts);
const toBeMaskedVertexBuffer = device.createBuffer({
size: toBeMaskedVerts.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(toBeMaskedVertexBuffer, 0, toBeMaskedVerts);
const stencilTexture = device.createTexture({
format: 'stencil8',
size: [canvas.width, canvas.height],
usage: GPUTextureUsage.RENDER_ATTACHMENT,
});
const encoder = device.createCommandEncoder();
{
const pass = encoder.beginRenderPass({
colorAttachments: [],
depthStencilAttachment: {
view: stencilTexture.createView(),
stencilClearValue: 0,
stencilLoadOp: 'clear',
stencilStoreOp: 'store',
}
});
// draw the mask
pass.setPipeline(maskMakingPipeline);
pass.setVertexBuffer(0, maskVertexBuffer);
pass.setStencilReference(1);
pass.draw(3);
pass.end();
}
{
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
clearValue: [0, 0, 0, 1],
loadOp: 'clear',
storeOp: 'store',
}],
depthStencilAttachment: {
view: stencilTexture.createView(),
stencilLoadOp: 'load',
stencilStoreOp: 'store',
}
});
// draw only the mask is
pass.setPipeline(maskedPipeline);
pass.setStencilReference(1);
pass.setVertexBuffer(0, toBeMaskedVertexBuffer);
pass.draw(3);
pass.end();
}
device.queue.submit([encoder.finish()]);
})();
<canvas></canvas>
Так же, как мы установили трафарет для сравнения с 'equal'
. Мы также можем маскировать, используя сравнение глубины и текстуру глубины.
Шаги:
Очистите текстуру глубины до 1.0.
Нарисуйте маску в текстуре глубины со значением Z, установленным на что-нибудь, например 0.0 (это то, что мы уже делали).
Это приведет к 0 в текстуре глубины, где первое, что мы нарисовали, и 1 во всех остальных местах.
Нарисуйте то, что мы хотим замаскировать, установив для сравнения глубины значение «равно» и значение Z также равное 0.0 (опять же, то, что мы уже делали).
В итоге мы будем рисовать только там, где 0.0 находится в текстуре глубины.
const code = `
struct VSIn {
@location(0) pos: vec4f,
};
struct VSOut {
@builtin(position) pos: vec4f,
};
@vertex fn vs(vsIn: VSIn) -> VSOut {
var vsOut: VSOut;
vsOut.pos = vsIn.pos;
return vsOut;
}
@fragment fn fs(vin: VSOut) -> @location(0) vec4f {
return vec4f(1, 0, 0, 1);
}
`;
(async() => {
const adapter = await navigator.gpu?.requestAdapter();
const device = await adapter?.requestDevice();
if (!device) {
alert('need webgpu');
return;
}
const canvas = document.querySelector("canvas")
const context = canvas.getContext('webgpu');
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device,
format: presentationFormat,
alphaMode: 'opaque',
});
const module = device.createShaderModule({code});
const maskMakingPipeline = device.createRenderPipeline({
label: 'pipeline for rendering the mask',
layout: 'auto',
vertex: {
module,
entryPoint: 'vs',
buffers: [
// position
{
arrayStride: 2 * 4, // 2 floats, 4 bytes each
attributes: [
{shaderLocation: 0, offset: 0, format: 'float32x2'},
],
},
],
},
fragment: {
module,
entryPoint: 'fs',
targets: [],
},
// replace the depth value when we draw
depthStencil: {
format: 'depth24plus',
depthCompare: 'always',
depthWriteEnabled: true,
},
});
const maskedPipeline = device.createRenderPipeline({
label: 'pipeline for rendering only where the mask is',
layout: 'auto',
vertex: {
module,
entryPoint: 'vs',
buffers: [
// position
{
arrayStride: 2 * 4, // 2 floats, 4 bytes each
attributes: [
{shaderLocation: 0, offset: 0, format: 'float32x2'},
],
},
],
},
fragment: {
module,
entryPoint: 'fs',
targets: [{format: presentationFormat}],
},
// draw only where stencil value matches
depthStencil: {
format: 'depth24plus',
depthCompare: 'equal',
depthWriteEnabled: false,
},
});
const maskVerts = new Float32Array([-1, -1, 1, -1, 1, 1]);
const toBeMaskedVerts = new Float32Array([0, -1, 1, -1, -1, 1]);
const maskVertexBuffer = device.createBuffer({
size: maskVerts.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(maskVertexBuffer, 0, maskVerts);
const toBeMaskedVertexBuffer = device.createBuffer({
size: toBeMaskedVerts.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(toBeMaskedVertexBuffer, 0, toBeMaskedVerts);
const depthTexture = device.createTexture({
format: 'depth24plus',
size: [canvas.width, canvas.height],
usage: GPUTextureUsage.RENDER_ATTACHMENT,
});
const encoder = device.createCommandEncoder();
{
const pass = encoder.beginRenderPass({
colorAttachments: [],
depthStencilAttachment: {
view: depthTexture.createView(),
depthClearValue: 1,
depthLoadOp: 'clear',
depthStoreOp: 'store',
}
});
// draw the mask
pass.setPipeline(maskMakingPipeline);
pass.setVertexBuffer(0, maskVertexBuffer);
pass.draw(3);
pass.end();
}
{
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
clearValue: [0, 0, 0, 1],
loadOp: 'clear',
storeOp: 'store',
}],
depthStencilAttachment: {
view: depthTexture.createView(),
depthLoadOp: 'load',
depthStoreOp: 'store',
}
});
// draw only the mask is
pass.setPipeline(maskedPipeline);
pass.setVertexBuffer(0, toBeMaskedVertexBuffer);
pass.draw(3);
pass.end();
}
device.queue.submit([encoder.finish()]);
})();
<canvas></canvas>