在 WebGPU 中,纹理是一种存储图像数据的数据结构。视频纹理则是将视频帧数据视为纹理数据,通过不断更新纹理内容来实现视频的播放效果。
WebGPU 渲染视频纹理的基本过程是先获取视频数据,将其转换为适合 WebGPU 处理的纹理格式,然后在着色器中对纹理进行采样,最后将采样结果绘制到画布(Canvas)上。
代码如下: 创建index.html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
@import url(https://webgpufundamentals.org/webgpu/resources/webgpu-lesson.css);
html, body {
margin: 0; /* remove the default margin */
height: 100%; /* make the html,body fill the page */
}
canvas {
display: block; /* make the canvas act like a block */
width: 100%; /* make the canvas fill its container */
height: 100%;
}
#start {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
#start>div {
font-size: 200px;
cursor: pointer;
}
</style>
</head>
<body>
<canvas></canvas>
<div id="start">
<div>▶️</div>
</div>
<script type="module" src="./script1.js"></script>
<!-- <video controls="" autoplay="" name="media">
<source src="http://localhost:8083/1080ph265.mp4" type="video/mp4">
</video> -->
</body>
</html>
创建script1.js文件
// see https://webgpufundamentals.org/webgpu/lessons/webgpu-utils.html#wgpu-matrix
import {
mat4
} from 'https://webgpufundamentals.org/3rdparty/wgpu-matrix.module.js';
async function main() {
const adapter = await navigator.gpu?.requestAdapter();
const device = await adapter?.requestDevice();
if (!device) {
fail('need a browser that supports WebGPU');
return;
}
// Get a WebGPU context from the canvas and configure it
const canvas = document.querySelector('canvas');
const context = canvas.getContext('webgpu');
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device,
format: presentationFormat,
});
const module = device.createShaderModule({
label: 'our hardcoded textured quad shaders',
code: `
struct OurVertexShaderOutput {
@builtin(position) position: vec4f,
@location(0) texcoord: vec2f,
};
struct Uniforms {
matrix: mat4x4f,
};
@group(0) @binding(2) var<uniform> uni: Uniforms;
@vertex fn vs(
@builtin(vertex_index) vertexIndex : u32
) -> OurVertexShaderOutput {
let pos = array(
vec2f( 0.0, 0.0), // center
vec2f( 1.0, 0.0), // right, center
vec2f( 0.0, 1.0), // center, top
// 2st triangle
vec2f( 0.0, 1.0), // center, top
vec2f( 1.0, 0.0), // right, center
vec2f( 1.0, 1.0), // right, top
);
var vsOutput: OurVertexShaderOutput;
let xy = pos[vertexIndex];
vsOutput.position = uni.matrix * vec4f(xy, 0.0, 1.0);
// vsOutput.position = vec4f(xy, 0.0, 1.0);
vsOutput.texcoord = xy;
return vsOutput;
}
@group(0) @binding(0) var ourSampler: sampler;
@group(0) @binding(1) var ourTexture: texture_external;
@fragment fn fs(fsInput: OurVertexShaderOutput) -> @location(0) vec4f {
return textureSampleBaseClampToEdge(
ourTexture,
ourSampler,
fsInput.texcoord,
);
}
`,
});
const pipeline = device.createRenderPipeline({
label: 'hardcoded textured quad pipeline',
layout: 'auto',
vertex: {
module,
},
fragment: {
module,
targets: [{
format: presentationFormat
}],
},
});
function startPlayingAndWaitForVideo(video) {
return new Promise((resolve, reject) => {
video.addEventListener('error', reject);
if ('requestVideoFrameCallback' in video) {
video.requestVideoFrameCallback(resolve);
} else {
const timeWatcher = () => {
if (video.currentTime > 0) {
resolve();
} else {
requestAnimationFrame(timeWatcher);
}
};
timeWatcher();
}
video.play().catch(reject);
});
}
function waitForClick() {
return new Promise(resolve => {
window.addEventListener(
'click',
() => {
document.querySelector('#start').style.display = 'none';
resolve();
}, {
once: true
});
});
}
const video = document.createElement('video');
video.muted = true;
video.loop = true;
video.preload = 'auto';
video.src = './video/frag_bunny.mp4';
await waitForClick();
await startPlayingAndWaitForVideo(video);
canvas.addEventListener('click', () => {
if (video.paused) {
video.play();
} else {
video.pause();
}
});
// offsets to the various uniform values in float32 indices
const kMatrixOffset = 0;
// for (let i = 0; i < 4; ++i) {
const sampler = device.createSampler({
addressModeU: 'repeat',
addressModeV: 'repeat',
magFilter: 'linear',
minFilter: 'linear',
});
// create a buffer for the uniform values
const uniformBufferSize =
16 * 4; // matrix is 16 32bit floats (4bytes each)
const uniformBuffer = device.createBuffer({
label: 'uniforms for quad',
size: uniformBufferSize,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
// create a typedarray to hold the values for the uniforms in JavaScript
const uniformValues = new Float32Array(uniformBufferSize / 4);
const matrix = uniformValues.subarray(kMatrixOffset, 16);
// Save the data we need to render this object.
const objectInfos = {
sampler: sampler,
matrix: matrix,
uniformValues: uniformValues,
uniformBuffer: uniformBuffer,
}
// }
const renderPassDescriptor = {
label: 'our basic canvas renderPass',
colorAttachments: [{
// view: <- to be filled out when we render
clearValue: [0.3, 0.3, 0.3, 1],
loadOp: 'clear',
storeOp: 'store',
}, ],
};
function render() {
const fov = 60 * Math.PI / 180; // 60 degrees in radians
const aspect = canvas.clientWidth / canvas.clientHeight;
const zNear = 1;
const zFar = 2000;
const projectionMatrix = mat4.perspective(fov, aspect, zNear, zFar);
const cameraPosition = [0, 0, 2];
const up = [0, 1, 0];
const target = [0, 0, 0];
const viewMatrix = mat4.lookAt(cameraPosition, target, up);
const viewProjectionMatrix = mat4.multiply(projectionMatrix, viewMatrix);
// Get the current texture from the canvas context and
// set it as the texture to render to.
renderPassDescriptor.colorAttachments[0].view =
context.getCurrentTexture().createView();
const encoder = device.createCommandEncoder({
label: 'render quad encoder',
});
const pass = encoder.beginRenderPass(renderPassDescriptor);
pass.setPipeline(pipeline);
const texture = device.importExternalTexture({
source: video
});
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [{
binding: 0,
resource: objectInfos.sampler
},
{
binding: 1,
resource: texture
},
{
binding: 2,
resource: {
buffer: uniformBuffer
}
},
],
});
const i = 0
const xSpacing = 4;
const ySpacing = 1;
const zDepth = 0;
const x = i % 2 - .5;
const y = 1;
mat4.translate(viewProjectionMatrix, [x * xSpacing, y * ySpacing, -zDepth * 0.5], matrix);
mat4.scale(matrix, [4, -2, 1], matrix);
// mat4.translate(matrix, [-0.5, -0.5, 0], matrix);
// copy the values from JavaScript to the GPU
device.queue.writeBuffer(objectInfos.uniformBuffer, 0, objectInfos.uniformValues);
pass.setBindGroup(0, bindGroup);
pass.draw(6); // call our vertex shader 6 times
pass.end();
const commandBuffer = encoder.finish();
device.queue.submit([commandBuffer]);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
const canvas = entry.target;
const width = entry.contentBoxSize[0].inlineSize;
const height = entry.contentBoxSize[0].blockSize;
canvas.width = Math.max(1, Math.min(width, device.limits.maxTextureDimension2D));
canvas.height = Math.max(1, Math.min(height, device.limits.maxTextureDimension2D));
}
});
observer.observe(canvas);
}
function fail(msg) {
// eslint-disable-next-line no-alert
alert(msg);
}
main();
实现效果:
点击按钮,开始播放