Start · Sprachen · JavaScript · Referenz · GPUExternalTexture

GPUExternalTexture

Schnittstelle

Stellt einen schreibgeschützten Snapshot eines Videobilds (<code>HTMLVideoElement</code> oder <code>VideoFrame</code>) als Textur für WebGPU-Shader bereit, ohne das Bild vorher zu kopieren.

seit JavaScript WebGPU: Chrome/Edge 113+, Safari 26+, Firefox 141+ Kategorie: webapi

Signatur

interface GPUExternalTexture

Beschreibung

GPUExternalTexture ist ein Wrapper-Objekt der WebGPU-API. Es enthält einen Snapshot des aktuellen Bilds einer Videoquelle, also eines HTMLVideoElement oder eines WebCodecs-VideoFrame. Man erstellt es nicht mit einem Konstruktor, sondern mit GPUDevice.importExternalTexture(). Der Browser kann dabei direkt auf die dekodierten Videodaten zugreifen, oft ohne Kopie und inklusive Farbraumumrechnung (z. B. YUV → RGB). Das ist deutlich schneller als copyExternalImageToTexture() in eine normale GPUTexture.

Typische Einsatzgebiete sind Videoeffekte und -filter in Echtzeit, Kamera-Vorschau mit Shadern, Videos als Textur in 3D-Szenen und Bildanalyse im Compute-Shader. Im Shader-Code (WGSL) hat eine externe Textur den eigenen Typ texture_external. Gelesen wird sie mit textureSampleBaseClampToEdge() oder textureLoad(), die Größe liefert textureDimensions(). Im GPUBindGroupLayout gibt man für den Eintrag externalTexture: {} an.

Lebensdauer: Eine externe Textur gilt nur kurz. Stammt sie aus einem HTMLVideoElement, läuft sie automatisch ab, sobald die aktuelle Aufgabe (Task) endet, meist also nach dem aktuellen Frame-Callback. Stammt sie aus einem VideoFrame, bleibt sie gültig, bis der Frame mit close() geschlossen wird. Deshalb ruft man importExternalTexture() in jedem Frame neu auf und baut die zugehörige Bind Group jedes Mal neu.

Das Interface hat nur die Eigenschaft label, einen frei wählbaren Namen für Fehlermeldungen und Debugging-Werkzeuge. Methoden gibt es keine.

Parameter

Name Typ Default Beschreibung
$descriptor.source Pflicht HTMLVideoElement | VideoFrame Parameter von GPUDevice.importExternalTexture(descriptor): die Videoquelle, deren aktuelles Bild übernommen wird.
$descriptor.label string "" Name für Debugging und Fehlermeldungen. Er steht danach in GPUExternalTexture.label.
$descriptor.colorSpace "srgb" | "display-p3" "srgb" Zielfarbraum, in den die Videodaten beim Lesen im Shader umgerechnet werden.

Rückgabewert

Typ
GPUExternalTexture
Beschreibung
importExternalTexture() gibt ein GPUExternalTexture-Objekt zurück. Es wird als Ressource in einer GPUBindGroup (resource: externalTexture) genutzt. Die einzige Eigenschaft ist label (string).

Beispiele

WGSL-Shader für eine externe Textur

// Fullscreen-Dreieck, das das Videobild zeigt
const shaderCode = /* wgsl */ `
  @group(0) @binding(0) var videoSampler: sampler;
  @group(0) @binding(1) var videoTex: texture_external;

  struct VSOut {
    @builtin(position) pos: vec4f,
    @location(0) uv: vec2f,
  };

  @vertex
  fn vs(@builtin(vertex_index) i: u32) -> VSOut {
    let p = array(vec2f(-1.0, -3.0), vec2f(3.0, 1.0), vec2f(-1.0, 1.0));
    var out: VSOut;
    out.pos = vec4f(p[i], 0.0, 1.0);
    out.uv = p[i] * vec2f(0.5, -0.5) + 0.5;
    return out;
  }

  @fragment
  fn fs(in: VSOut) -> @location(0) vec4f {
    let c = textureSampleBaseClampToEdge(videoTex, videoSampler, in.uv);
    // Einfacher Graustufen-Filter
    let g = dot(c.rgb, vec3f(0.299, 0.587, 0.114));
    return vec4f(vec3f(g), 1.0);
  }
`;
console.log('Shader-Länge:', shaderCode.length > 0);
Shader-Länge: true

Video in jedem Frame importieren und rendern

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 video = document.querySelector('video');
video.muted = true;
await video.play();

const module = device.createShaderModule({ code: shaderCode }); // aus Beispiel 1
const pipeline = device.createRenderPipeline({
  layout: 'auto',
  vertex: { module, entryPoint: 'vs' },
  fragment: { module, entryPoint: 'fs', targets: [{ format }] },
});
const sampler = device.createSampler({ magFilter: 'linear', minFilter: 'linear' });

const renderFrame = () => {
  // Muss in JEDEM Frame neu importiert werden – läuft am Task-Ende ab
  const externalTexture = device.importExternalTexture({
    source: video,
    label: 'video-frame',
  });

  const bindGroup = device.createBindGroup({
    layout: pipeline.getBindGroupLayout(0),
    entries: [
      { binding: 0, resource: sampler },
      { binding: 1, resource: externalTexture },
    ],
  });

  const encoder = device.createCommandEncoder();
  const pass = encoder.beginRenderPass({
    colorAttachments: [{
      view: context.getCurrentTexture().createView(),
      loadOp: 'clear',
      storeOp: 'store',
      clearValue: { r: 0, g: 0, b: 0, a: 1 },
    }],
  });
  pass.setPipeline(pipeline);
  pass.setBindGroup(0, bindGroup);
  pass.draw(3);
  pass.end();
  device.queue.submit([encoder.finish()]);

  video.requestVideoFrameCallback(renderFrame);
};

video.requestVideoFrameCallback(renderFrame);

Import aus einem WebCodecs-VideoFrame mit explizitem Layout

const bindGroupLayout = device.createBindGroupLayout({
  entries: [
    { binding: 0, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
    { binding: 1, visibility: GPUShaderStage.FRAGMENT, externalTexture: {} },
  ],
});

const frame = new VideoFrame(video, { timestamp: 0 });
const extTex = device.importExternalTexture({
  source: frame,
  label: 'kamera-snapshot',
  colorSpace: 'srgb',
});
console.log(extTex.label, frame.displayWidth, frame.displayHeight);

const bindGroup = device.createBindGroup({
  layout: bindGroupLayout,
  entries: [
    { binding: 0, resource: device.createSampler() },
    { binding: 1, resource: extTex },
  ],
});

// ... Render-/Compute-Pass ausführen und abschicken ...

// Erst nach dem Submit schließen – danach ist extTex abgelaufen
frame.close();
kamera-snapshot 1280 720