Skip to content

Commit e32642d

Browse files
committed
Add a Panorama to Video Example
1 parent b28ddf7 commit e32642d

9 files changed

+229
-24
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
By: [Taryn Elliott](https://www.pexels.com/@taryn-elliott/)
2+
From: https://www.pexels.com/video/giraffe-walking-in-the-forest-5214261/
3+
License: https://www.pexels.com/license/
2.92 MB
Binary file not shown.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
By [Fabio Casati](https://www.youtube.com/channel/UCTnaAJ2DlSM6jtdUFXtGu8Q)
2+
From: https://commons.wikimedia.org/wiki/File:Video_360%C2%B0._Timelapse._Bled_Lake_in_Slovenia..webm
3+
License: [CC BY 3.0](https://creativecommons.org/licenses/by/3.0)
Binary file not shown.

sample/videoUploading/index.html

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,10 @@
2121
html, body {
2222
margin: 0; /* remove default margin */
2323
height: 100%; /* make body fill the browser window */
24-
display: flex;
25-
place-content: center center;
2624
}
2725
canvas {
28-
width: 600px;
29-
height: 600px;
26+
width: 100%;
27+
height: 100%;
3028
max-width: 100%;
3129
display: block;
3230
}

sample/videoUploading/main.ts

Lines changed: 164 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,39 @@
11
import { GUI } from 'dat.gui';
2+
import { mat3, mat4 } from 'wgpu-matrix';
23
import fullscreenTexturedQuadWGSL from '../../shaders/fullscreenTexturedQuad.wgsl';
3-
import sampleExternalTextureWGSL from '../../shaders/sampleExternalTexture.frag.wgsl';
4+
import sampleExternalTextureWGSL from './sampleExternalTexture.frag.wgsl';
5+
import sampleExternalTextureAsPanoramaWGSL from './sampleExternalTextureAsPanorama.wgsl';
46
import { quitIfWebGPUNotAvailable } from '../util';
57

68
const adapter = await navigator.gpu?.requestAdapter();
79
const device = await adapter?.requestDevice();
810
quitIfWebGPUNotAvailable(adapter, device);
911

10-
// Set video element
11-
const video = document.createElement('video');
12-
video.loop = true;
13-
video.playsInline = true;
14-
video.autoplay = true;
15-
video.muted = true;
16-
video.src = '../../assets/video/pano.webm';
17-
await video.play();
12+
const videos = {
13+
'giraffe (2d)': {
14+
url: '../../assets/video/5214261-hd_1920_1080_25fps.mp4',
15+
mode: 'cover',
16+
},
17+
'lhc (360)': {
18+
url: '../../assets/video/pano.webm',
19+
mode: '360',
20+
},
21+
'lake (360)': {
22+
url: '../../assets/video/Video_360°._Timelapse._Bled_Lake_in_Slovenia..webm.720p.vp9.webm',
23+
mode: '360',
24+
},
25+
} as const;
1826

1927
const canvas = document.querySelector('canvas') as HTMLCanvasElement;
2028
const context = canvas.getContext('webgpu') as GPUCanvasContext;
21-
const devicePixelRatio = window.devicePixelRatio;
22-
canvas.width = canvas.clientWidth * devicePixelRatio;
23-
canvas.height = canvas.clientHeight * devicePixelRatio;
2429
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
2530

2631
context.configure({
2732
device,
2833
format: presentationFormat,
2934
});
3035

31-
const pipeline = device.createRenderPipeline({
36+
const videoCoverPipeline = device.createRenderPipeline({
3237
layout: 'auto',
3338
vertex: {
3439
module: device.createShaderModule({
@@ -50,29 +55,81 @@ const pipeline = device.createRenderPipeline({
5055
},
5156
});
5257

58+
const module = device.createShaderModule({
59+
code: sampleExternalTextureAsPanoramaWGSL,
60+
});
61+
const video360Pipeline = device.createRenderPipeline({
62+
layout: 'auto',
63+
vertex: { module },
64+
fragment: {
65+
module,
66+
targets: [{ format: presentationFormat }],
67+
},
68+
primitive: {
69+
topology: 'triangle-list',
70+
},
71+
});
72+
5373
const sampler = device.createSampler({
5474
magFilter: 'linear',
5575
minFilter: 'linear',
5676
});
5777

78+
// make buffer big enough for either pipeline
79+
const uniformBuffer = device.createBuffer({
80+
size: (16 + 2 + 2) * 4, // (mat4x4f + vec2f + padding) vs (mat3x3f + padding)
81+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
82+
});
83+
84+
// Set video element
85+
const video = document.createElement('video');
86+
video.loop = true;
87+
video.playsInline = true;
88+
video.autoplay = true;
89+
video.muted = true;
90+
91+
let canReadVideo = false;
92+
93+
async function playVideo(videoName: keyof typeof videos) {
94+
canReadVideo = false;
95+
video.src = videos[videoName].url;
96+
await video.play();
97+
canReadVideo = true;
98+
}
99+
58100
const params = new URLSearchParams(window.location.search);
59101
const settings = {
60102
requestFrame: 'requestAnimationFrame',
61103
videoSource: params.get('videoSource') || 'videoElement',
104+
video: 'giraffe (2d)' as keyof typeof videos,
62105
};
106+
playVideo(settings.video);
63107

64108
const gui = new GUI();
65109
gui.add(settings, 'videoSource', ['videoElement', 'videoFrame']);
66110
gui.add(settings, 'requestFrame', [
67111
'requestAnimationFrame',
68112
'requestVideoFrameCallback',
69113
]);
114+
gui.add(settings, 'video', Object.keys(videos)).onChange(() => {
115+
playVideo(settings.video);
116+
});
70117

71-
function frame() {
118+
let yRotation = 0;
119+
let xRotation = 0;
120+
121+
function drawVideo() {
122+
const maxSize = device.limits.maxTextureDimension2D;
123+
canvas.width = Math.min(Math.max(1, canvas.offsetWidth), maxSize);
124+
canvas.height = Math.min(Math.max(1, canvas.offsetHeight), maxSize);
72125
const externalTextureSource =
73126
settings.videoSource === 'videoFrame' ? new VideoFrame(video) : video;
74127

75-
const uniformBindGroup = device.createBindGroup({
128+
const mode = videos[settings.video].mode;
129+
const pipeline = mode === '360' ? video360Pipeline : videoCoverPipeline;
130+
const canvasTexture = context.getCurrentTexture();
131+
132+
const bindGroup = device.createBindGroup({
76133
layout: pipeline.getBindGroupLayout(0),
77134
entries: [
78135
{
@@ -85,11 +142,60 @@ function frame() {
85142
source: externalTextureSource,
86143
}),
87144
},
145+
{
146+
binding: 3,
147+
resource: { buffer: uniformBuffer },
148+
},
88149
],
89150
});
90151

152+
if (mode === '360') {
153+
// Spin the camera around the y axis and add in the user's x and y rotation;
154+
const time = performance.now() * 0.001;
155+
const rotation = time * 0.1 + yRotation;
156+
const projection = mat4.perspective(
157+
(75 * Math.PI) / 180,
158+
canvas.clientWidth / canvas.clientHeight,
159+
0.5,
160+
100
161+
);
162+
163+
// Note: You can use any method you want to compute a view matrix,
164+
// just be sure to zero out the translation.
165+
const camera = mat4.identity();
166+
mat4.rotateY(camera, rotation, camera);
167+
mat4.rotateX(camera, xRotation, camera);
168+
mat4.setTranslation(camera, [0, 0, 0], camera);
169+
const view = mat4.inverse(camera);
170+
const viewDirectionProjection = mat4.multiply(projection, view);
171+
const viewDirectionProjectionInverse = mat4.inverse(
172+
viewDirectionProjection
173+
);
174+
175+
const uniforms = new Float32Array([
176+
...viewDirectionProjectionInverse,
177+
canvasTexture.width,
178+
canvasTexture.height,
179+
]);
180+
device.queue.writeBuffer(uniformBuffer, 0, uniforms);
181+
} else {
182+
// compute a `cover` matrix for a unit UV quad.
183+
const mat = mat3.identity();
184+
const videoAspect = video.videoWidth / video.videoHeight;
185+
const canvasAspect = canvas.offsetWidth / canvas.offsetHeight;
186+
const combinedAspect = videoAspect / canvasAspect;
187+
mat3.translate(mat, [0.5, 0.5], mat);
188+
mat3.scale(
189+
mat,
190+
combinedAspect > 1 ? [1 / combinedAspect, 1] : [1, combinedAspect],
191+
mat
192+
);
193+
mat3.translate(mat, [-0.5, -0.5], mat);
194+
device.queue.writeBuffer(uniformBuffer, 0, mat);
195+
}
196+
91197
const commandEncoder = device.createCommandEncoder();
92-
const textureView = context.getCurrentTexture().createView();
198+
const textureView = canvasTexture.createView();
93199

94200
const renderPassDescriptor: GPURenderPassDescriptor = {
95201
colorAttachments: [
@@ -104,15 +210,20 @@ function frame() {
104210

105211
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
106212
passEncoder.setPipeline(pipeline);
107-
passEncoder.setBindGroup(0, uniformBindGroup);
213+
passEncoder.setBindGroup(0, bindGroup);
108214
passEncoder.draw(6);
109215
passEncoder.end();
110216
device.queue.submit([commandEncoder.finish()]);
111217

112218
if (externalTextureSource instanceof VideoFrame) {
113219
externalTextureSource.close();
114220
}
221+
}
115222

223+
function frame() {
224+
if (canReadVideo) {
225+
drawVideo();
226+
}
116227
if (settings.requestFrame == 'requestVideoFrameCallback') {
117228
video.requestVideoFrameCallback(frame);
118229
} else {
@@ -125,3 +236,39 @@ if (settings.requestFrame == 'requestVideoFrameCallback') {
125236
} else {
126237
requestAnimationFrame(frame);
127238
}
239+
240+
let startX = 0;
241+
let startY = 0;
242+
let startYRotation = 0;
243+
let startTarget = 0;
244+
245+
const clamp = (value: number, min: number, max: number) => {
246+
return Math.max(min, Math.min(max, value));
247+
};
248+
249+
const drag = (e: PointerEvent) => {
250+
const deltaX = e.clientX - startX;
251+
const deltaY = e.clientY - startY;
252+
yRotation = startYRotation + deltaX * 0.01;
253+
xRotation = clamp(
254+
startTarget + deltaY * -0.01,
255+
-Math.PI * 0.4,
256+
Math.PI * 0.4
257+
);
258+
};
259+
260+
const stopDrag = () => {
261+
window.removeEventListener('pointermove', drag);
262+
window.removeEventListener('pointerup', stopDrag);
263+
};
264+
265+
const startDrag = (e: PointerEvent) => {
266+
window.addEventListener('pointermove', drag);
267+
window.addEventListener('pointerup', stopDrag);
268+
e.preventDefault();
269+
startX = e.clientX;
270+
startY = e.clientY;
271+
startYRotation = yRotation;
272+
startTarget = xRotation;
273+
};
274+
canvas.addEventListener('pointerdown', startDrag);

sample/videoUploading/meta.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
export default {
22
name: 'Video Uploading',
3-
description: 'This example shows how to upload video frame to WebGPU.',
3+
description: `\
4+
This example shows how to upload video frame to WebGPU.
5+
giraffe by [Taryn Elliott](https://www.pexels.com/video/giraffe-walking-in-the-forest-5214261/).
6+
lhc by [unknown](https://foo.com).
7+
lake by [Fabio Casati](https://commons.wikimedia.org/wiki/File:Video_360%C2%B0._Timelapse._Bled_Lake_in_Slovenia..webm), [CC BY 3.0](https://creativecommons.org/licenses/by/3.0)
8+
`,
49
filename: __DIRNAME__,
510
sources: [
611
{ path: 'main.ts' },
712
{ path: '../../shaders/fullscreenTexturedQuad.wgsl' },
8-
{ path: '../../shaders/sampleExternalTexture.frag.wgsl' },
13+
{ path: './sampleExternalTexture.frag.wgsl' },
14+
{ path: './sampleExternalTextureAsPanorama.wgsl' },
915
],
1016
};
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
@group(0) @binding(1) var mySampler: sampler;
22
@group(0) @binding(2) var myTexture: texture_external;
3+
@group(0) @binding(3) var<uniform> myMatrix: mat3x3f;
34

45
@fragment
56
fn main(@location(0) fragUV : vec2f) -> @location(0) vec4f {
6-
return textureSampleBaseClampToEdge(myTexture, mySampler, fragUV);
7+
let uv = (myMatrix * vec3f(fragUV, 1.0)).xy;
8+
return textureSampleBaseClampToEdge(myTexture, mySampler, uv);
79
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
struct Uniforms {
2+
viewDirectionProjectionInverse: mat4x4f,
3+
targetSize: vec2f,
4+
};
5+
6+
struct VSOutput {
7+
@builtin(position) position: vec4f,
8+
@location(0) uv: vec2f,
9+
};
10+
11+
@vertex
12+
fn vs(@builtin(vertex_index) vertexIndex: u32) -> VSOutput {
13+
let pos = array(
14+
vec2f(-1, -1),
15+
vec2f(-1, 3),
16+
vec2f( 3, -1),
17+
);
18+
19+
let xy = pos[vertexIndex];
20+
return VSOutput(
21+
vec4f(xy, 0.0, 1.0),
22+
xy * vec2f(0.5, -0.5) + vec2f(0.5)
23+
);
24+
}
25+
26+
@group(0) @binding(1) var panoramaSampler: sampler;
27+
@group(0) @binding(2) var panoramaTexture: texture_external;
28+
@group(0) @binding(3) var<uniform> uniforms: Uniforms;
29+
30+
const PI = radians(180.0);
31+
@fragment
32+
fn main(@builtin(position) position: vec4f) -> @location(0) vec4f {
33+
let pos = position.xy / uniforms.targetSize * 2.0 - 1.0;
34+
let t = uniforms.viewDirectionProjectionInverse * vec4f(pos, 0, 1);
35+
let dir = normalize(t.xyz / t.w);
36+
37+
let longitude = atan2(dir.z, dir.x);
38+
let latitude = asin(dir.y / length(dir));
39+
40+
let uv = vec2f(
41+
longitude / (2.0 * PI) + 0.5,
42+
latitude / PI + 0.5,
43+
);
44+
45+
return textureSampleBaseClampToEdge(panoramaTexture, panoramaSampler, uv);
46+
}

0 commit comments

Comments
 (0)