11import { GUI } from 'dat.gui' ;
2+ import { mat3 , mat4 } from 'wgpu-matrix' ;
23import 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' ;
46import { quitIfWebGPUNotAvailable } from '../util' ;
57
68const adapter = await navigator . gpu ?. requestAdapter ( ) ;
79const device = await adapter ?. requestDevice ( ) ;
810quitIfWebGPUNotAvailable ( 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
1927const canvas = document . querySelector ( 'canvas' ) as HTMLCanvasElement ;
2028const context = canvas . getContext ( 'webgpu' ) as GPUCanvasContext ;
21- const devicePixelRatio = window . devicePixelRatio ;
22- canvas . width = canvas . clientWidth * devicePixelRatio ;
23- canvas . height = canvas . clientHeight * devicePixelRatio ;
2429const presentationFormat = navigator . gpu . getPreferredCanvasFormat ( ) ;
2530
2631context . 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+
5373const 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+
58100const params = new URLSearchParams ( window . location . search ) ;
59101const settings = {
60102 requestFrame : 'requestAnimationFrame' ,
61103 videoSource : params . get ( 'videoSource' ) || 'videoElement' ,
104+ video : 'giraffe (2d)' as keyof typeof videos ,
62105} ;
106+ playVideo ( settings . video ) ;
63107
64108const gui = new GUI ( ) ;
65109gui . add ( settings , 'videoSource' , [ 'videoElement' , 'videoFrame' ] ) ;
66110gui . 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 ) ;
0 commit comments