Как создать 2D обтравочную маску в webgpu?

Я исследовал способ 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 жалуется: «Похоже, что ваш пост в основном код; пожалуйста, добавьте некоторые подробности».

Как использовать d3.js для рисования 2D SVG-элементов в приложении Angular?
Как использовать d3.js для рисования 2D SVG-элементов в приложении Angular?
D3.js - это обширная библиотека, используемая для привязки произвольных данных к объектной модели документа (DOM). Мы разберем основные варианты...
2
0
80
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Есть бесконечные способы клипа. Несколько с головы

  • Вырезать через вершинное математическое пересечение
  • Клип с текстурой глубины
  • Клип с трафаретной текстурой
  • Клип с альфа-маской
  • Клип с пересечением областей (например, SDF или CSG)

Альфа-маска имеет то преимущество, что ваша маска может смешиваться.

В любом случае, отсечение через трафаретную текстуру означает создание трафаретной текстуры, рендеринг на нее маски, а затем рендеринг других вещей, настроенных на отрисовку только там, где находится маска.

В частности, конвейер, устанавливающий маску, должен быть настроен на что-то вроде

  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. Очистите текстуру глубины до 1.0.

  2. Нарисуйте маску в текстуре глубины со значением Z, установленным на что-нибудь, например 0.0 (это то, что мы уже делали).

    Это приведет к 0 в текстуре глубины, где первое, что мы нарисовали, и 1 во всех остальных местах.

  3. Нарисуйте то, что мы хотим замаскировать, установив для сравнения глубины значение «равно» и значение 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>

Другие вопросы по теме