Signatur
Beschreibung
draw() ist der grundlegende Zeichenbefehl in WebGPU. Der Aufruf zeichnet nicht sofort, sondern zeichnet einen Befehl im Render-Pass auf. Ausgeführt wird er erst, wenn der zugehörige GPUCommandBuffer über device.queue.submit() an die GPU übergeben wird. Welche Primitive dabei entstehen (Dreiecke, Linien, Punkte), legt primitive.topology der mit setPipeline() gesetzten GPURenderPipeline fest.
Vorher müssen im Pass eine Render-Pipeline, alle Vertex-Puffer aus dem vertex.buffers-Layout der Pipeline (setVertexBuffer()) und alle nötigen Bind-Groups (setBindGroup()) gesetzt sein. Die Vertices werden der Reihe nach gelesen, beginnend bei firstVertex. Im Vertex-Shader stehen die Werte als @builtin(vertex_index) und @builtin(instance_index) zur Verfügung.
Mit instanceCount gibt es Instancing: Dieselbe Geometrie wird mehrfach gezeichnet, etwa für viele Bäume, Partikel oder Kacheln. Dafür braucht es nur einen einzigen Zeichenaufruf. Daten pro Instanz kommen aus Vertex-Puffern mit stepMode: "instance" oder aus einem Storage-Puffer, auf den man über instance_index zugreift. Für Geometrie mit Indexpuffer nimmt man drawIndexed(). Sollen die Zeichenparameter aus einem GPU-Puffer kommen, nimmt man drawIndirect().
Parameter
| Name | Typ | Default | Beschreibung |
|---|---|---|---|
| $vertexCount Pflicht | number | Anzahl der zu zeichnenden Vertices (ganzzahlig, GPUSize32). | |
| $instanceCount | number | 1 | Anzahl der Instanzen. Bei 0 wird nichts gezeichnet. |
| $firstVertex | number | 0 | Versatz in Vertices vom Anfang der Vertex-Puffer. Ab hier wird gelesen. Das ist auch der Startwert von vertex_index. |
| $firstInstance | number | 0 | Erste zu zeichnende Instanz. Das ist auch der Startwert von instance_index. |
Rückgabewert
Beispiele
Einfaches Dreieck aus einem Vertex-Puffer
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const canvas = document.querySelector('canvas');
const context = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format });
const shader = device.createShaderModule({ code: `
@vertex fn vs(@location(0) pos: vec2f) -> @builtin(position) vec4f {
return vec4f(pos, 0.0, 1.0);
}
@fragment fn fs() -> @location(0) vec4f {
return vec4f(1.0, 0.4, 0.0, 1.0);
}` });
const vertices = new Float32Array([0, 0.5, -0.5, -0.5, 0.5, -0.5]);
const vertexBuffer = device.createBuffer({
size: vertices.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(vertexBuffer, 0, vertices);
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module: shader,
entryPoint: 'vs',
buffers: [{ arrayStride: 8, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x2' }] }],
},
fragment: { module: shader, entryPoint: 'fs', targets: [{ format }] },
primitive: { topology: 'triangle-list' },
});
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
clearValue: { r: 0, g: 0, b: 0, a: 1 },
loadOp: 'clear',
storeOp: 'store',
}],
});
pass.setPipeline(pipeline);
pass.setVertexBuffer(0, vertexBuffer);
pass.draw(3); // 3 Vertices = 1 Dreieck
pass.end();
device.queue.submit([encoder.finish()]);
Instancing: 100 Kopien in einem Aufruf
// WGSL-Ausschnitt: Position je Instanz aus instance_index berechnen
const code = `
@vertex fn vs(@location(0) pos: vec2f,
@builtin(instance_index) i: u32) -> @builtin(position) vec4f {
let col = f32(i % 10u);
let row = f32(i / 10u);
let offset = vec2f(col * 0.2 - 0.9, row * 0.2 - 0.9);
return vec4f(pos * 0.08 + offset, 0.0, 1.0);
}`;
// ... Pipeline und Vertex-Puffer wie im ersten Beispiel ...
pass.setPipeline(pipeline);
pass.setVertexBuffer(0, vertexBuffer);
pass.draw(3, 100); // 3 Vertices x 100 Instanzen
pass.end();
Vollbild-Dreieck ohne Vertex-Puffer
// Vertices werden im Shader aus vertex_index erzeugt – kein setVertexBuffer() nötig
const shader = device.createShaderModule({ code: `
@vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
return vec4f(p[i], 0.0, 1.0);
}
@fragment fn fs(@builtin(position) c: vec4f) -> @location(0) vec4f {
return vec4f(c.x / 800.0, c.y / 600.0, 0.5, 1.0);
}` });
// Pipeline ohne vertex.buffers anlegen, dann:
pass.setPipeline(fullscreenPipeline);
pass.draw(3); // deckt den gesamten Viewport ab (Post-Processing)
// Wichtig · Fallstricke
- Validierungsfehler statt Exceptions: Fehlt die Pipeline, ein Vertex-Puffer oder eine Bind-Group, oder reicht der Vertex-Puffer für
firstVertex + vertexCountnicht aus, dann wirftdraw()nicht. Stattdessen wird der Encoder ungültig, und der Fehler erscheint erst beifinish()bzw.submit(). Er landet in der Konsole oder überdevice.pushErrorScope()/ das Ereignisuncapturederror. - Topologie beachten: Bei
triangle-listsolltevertexCountein Vielfaches von 3 sein, beiline-listein Vielfaches von 2. Überzählige Vertices werden verworfen. vertexCountgibt die Anzahl der Vertices an, nicht die Anzahl der Primitive und auch keine Bytes.firstInstanceungleich 0 verlangt keine eigene Geräte-Funktion. BeidrawIndirect()dagegen braucht einfirstInstanceungleich 0 das Feature"indirect-first-instance".- Performance: Viele kleine
draw()-Aufrufe kosten CPU-Zeit. Wo möglich, fasst man sie per Instancing zusammen oder zeichnet wiederkehrende Befehle inGPURenderBundles auf. - Kompatibilität: WebGPU gibt es nur in sicheren Kontexten. Die Unterstützung ist noch nicht in allen Browsern bzw. auf allen Plattformen vollständig. Deshalb vorher mit
if (!navigator.gpu)prüfen und eine Alternative vorsehen, zum Beispiel WebGL.