Signatur
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
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);
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();