@livekit/track-processors 0.5.4 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ import { createCanvas } from '../utils';
1
2
  import { setupWebGL } from '../webgl/index';
2
3
  import { VideoTrackTransformer, VideoTransformerInitOptions } from './types';
3
4
 
@@ -6,7 +7,7 @@ export default abstract class VideoTransformer<Options extends Record<string, un
6
7
  {
7
8
  transformer?: TransformStream;
8
9
 
9
- canvas?: OffscreenCanvas;
10
+ canvas?: OffscreenCanvas | HTMLCanvasElement;
10
11
 
11
12
  // ctx?: OffscreenCanvasRenderingContext2D;
12
13
 
@@ -31,7 +32,7 @@ export default abstract class VideoTransformer<Options extends Record<string, un
31
32
  if (outputCanvas) {
32
33
  // this.ctx = this.canvas?.getContext('2d') || undefined;
33
34
  this.gl = setupWebGL(
34
- this.canvas || new OffscreenCanvas(inputVideo.videoWidth, inputVideo.videoHeight),
35
+ this.canvas || createCanvas(inputVideo.videoWidth, inputVideo.videoHeight),
35
36
  );
36
37
  }
37
38
  this.inputVideo = inputVideo;
@@ -42,7 +43,7 @@ export default abstract class VideoTransformer<Options extends Record<string, un
42
43
  this.canvas = outputCanvas || null;
43
44
  this.gl?.cleanup();
44
45
  this.gl = setupWebGL(
45
- this.canvas || new OffscreenCanvas(inputVideo.videoWidth, inputVideo.videoHeight),
46
+ this.canvas || createCanvas(inputVideo.videoWidth, inputVideo.videoHeight),
46
47
  );
47
48
 
48
49
  this.inputVideo = inputVideo;
@@ -3,7 +3,7 @@ export type TrackTransformerInitOptions = {
3
3
  };
4
4
 
5
5
  export interface VideoTransformerInitOptions extends TrackTransformerInitOptions {
6
- outputCanvas: OffscreenCanvas;
6
+ outputCanvas: OffscreenCanvas | HTMLCanvasElement;
7
7
  inputElement: HTMLVideoElement;
8
8
  }
9
9
 
package/src/utils.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  /* eslint-disable @typescript-eslint/naming-convention */
2
- export const supportsProcessor = typeof MediaStreamTrackGenerator !== 'undefined';
3
- export const supportsOffscreenCanvas = typeof OffscreenCanvas !== 'undefined';
2
+ export const supportsOffscreenCanvas = () => typeof OffscreenCanvas !== 'undefined';
4
3
 
5
4
  async function sleep(time: number) {
6
5
  return new Promise((resolve) => setTimeout(resolve, time));
@@ -23,3 +22,13 @@ export async function waitForTrackResolution(track: MediaStreamTrack) {
23
22
  }
24
23
  return { width: undefined, height: undefined };
25
24
  }
25
+
26
+ export function createCanvas(width: number, height: number) {
27
+ if (supportsOffscreenCanvas()) {
28
+ return new OffscreenCanvas(width, height);
29
+ }
30
+ const canvas = document.createElement('canvas');
31
+ canvas.width = width;
32
+ canvas.height = height;
33
+ return canvas;
34
+ }
@@ -1,22 +1,30 @@
1
- import { MPMask } from '@mediapipe/tasks-vision';
1
+ /**
2
+ * WebGL setup for the mask processor
3
+ * potential improvements:
4
+ * - downsample the video texture in background blur scenario before applying the (gaussian) blur for better performance
5
+ *
6
+ */
2
7
  import { applyBlur, createBlurProgram } from './shader-programs/blurShader';
3
8
  import { createBoxBlurProgram } from './shader-programs/boxBlurShader';
4
9
  import { createCompositeProgram } from './shader-programs/compositeShader';
10
+ import { applyDownsampling, createDownSampler } from './shader-programs/downSampler';
5
11
  import {
6
12
  createFramebuffer,
7
13
  createVertexBuffer,
8
- emptyImageData,
14
+ getEmptyImageData,
9
15
  initTexture,
10
16
  resizeImageToCover,
11
17
  } from './utils';
12
18
 
13
- export const setupWebGL = (canvas: OffscreenCanvas) => {
19
+ export const setupWebGL = (canvas: OffscreenCanvas | HTMLCanvasElement) => {
14
20
  const gl = canvas.getContext('webgl2', {
15
21
  antialias: true,
16
22
  premultipliedAlpha: true,
17
23
  }) as WebGL2RenderingContext;
18
24
 
19
25
  let blurRadius: number | null = null;
26
+ let maskBlurRadius: number | null = 8;
27
+ const downsampleFactor = 4;
20
28
 
21
29
  if (!gl) {
22
30
  console.error('Failed to create WebGL context');
@@ -57,29 +65,44 @@ export const setupWebGL = (canvas: OffscreenCanvas) => {
57
65
  // Create additional textures and framebuffers for processing
58
66
  let bgBlurTextures: WebGLTexture[] = [];
59
67
  let bgBlurFrameBuffers: WebGLFramebuffer[] = [];
60
- let maskBlurTextures: WebGLTexture[] = [];
61
- let maskBlurFrameBuffers: WebGLFramebuffer[] = [];
68
+ let blurredMaskTexture: WebGLTexture | null = null;
69
+
70
+ // For double buffering the final mask
71
+ let finalMaskTextures: WebGLTexture[] = [];
72
+ let readMaskIndex = 0; // Index for renderFrame to read from
73
+ let writeMaskIndex = 1; // Index for updateMask to write to
62
74
 
63
75
  // Create textures for background processing (blur)
64
76
  bgBlurTextures.push(initTexture(gl, 3)); // For blur pass 1
65
77
  bgBlurTextures.push(initTexture(gl, 4)); // For blur pass 2
66
78
 
67
- // Create framebuffers for background processing
68
- bgBlurFrameBuffers.push(createFramebuffer(gl, bgBlurTextures[0], canvas.width, canvas.height));
69
- bgBlurFrameBuffers.push(createFramebuffer(gl, bgBlurTextures[1], canvas.width, canvas.height));
79
+ const bgBlurTextureWidth = Math.floor(canvas.width / downsampleFactor);
80
+ const bgBlurTextureHeight = Math.floor(canvas.height / downsampleFactor);
70
81
 
71
- // Create textures for mask processing (blur)
72
- maskBlurTextures.push(initTexture(gl, 5)); // For mask blur pass 1
73
- maskBlurTextures.push(initTexture(gl, 6)); // For mask blur pass 2
82
+ const downSampler = createDownSampler(gl, bgBlurTextureWidth, bgBlurTextureHeight);
74
83
 
75
- // Create framebuffers for mask processing
76
- maskBlurFrameBuffers.push(
77
- createFramebuffer(gl, maskBlurTextures[0], canvas.width, canvas.height),
84
+ // Create framebuffers for background processing
85
+ bgBlurFrameBuffers.push(
86
+ createFramebuffer(gl, bgBlurTextures[0], bgBlurTextureWidth, bgBlurTextureHeight),
78
87
  );
79
- maskBlurFrameBuffers.push(
80
- createFramebuffer(gl, maskBlurTextures[1], canvas.width, canvas.height),
88
+ bgBlurFrameBuffers.push(
89
+ createFramebuffer(gl, bgBlurTextures[1], bgBlurTextureWidth, bgBlurTextureHeight),
81
90
  );
82
91
 
92
+ // Initialize texture for the first mask blur pass
93
+ const tempMaskTexture = initTexture(gl, 5);
94
+ const tempMaskFrameBuffer = createFramebuffer(gl, tempMaskTexture, canvas.width, canvas.height);
95
+
96
+ // Initialize two textures for double-buffering the final mask
97
+ finalMaskTextures.push(initTexture(gl, 6)); // For reading in renderFrame
98
+ finalMaskTextures.push(initTexture(gl, 7)); // For writing in updateMask
99
+
100
+ // Create framebuffers for the final mask textures
101
+ const finalMaskFrameBuffers = [
102
+ createFramebuffer(gl, finalMaskTextures[0], canvas.width, canvas.height),
103
+ createFramebuffer(gl, finalMaskTextures[1], canvas.width, canvas.height),
104
+ ];
105
+
83
106
  // Set up uniforms for the composite shader
84
107
  gl.useProgram(compositeProgram);
85
108
  gl.uniform1i(bgTextureLocation, 0);
@@ -87,10 +110,10 @@ export const setupWebGL = (canvas: OffscreenCanvas) => {
87
110
  gl.uniform1i(maskTextureLocation, 2);
88
111
 
89
112
  // Store custom background image
90
- let customBackgroundImage: ImageBitmap | ImageData = emptyImageData;
113
+ let customBackgroundImage: ImageBitmap | ImageData = getEmptyImageData();
91
114
 
92
- function render(frame: VideoFrame, mask: MPMask) {
93
- if (frame.codedWidth === 0 || mask.width === 0) {
115
+ function renderFrame(frame: VideoFrame) {
116
+ if (frame.codedWidth === 0 || finalMaskTextures.length === 0) {
94
117
  return;
95
118
  }
96
119
 
@@ -106,11 +129,19 @@ export const setupWebGL = (canvas: OffscreenCanvas) => {
106
129
  let backgroundTexture = bgTexture;
107
130
 
108
131
  if (blurRadius) {
109
- backgroundTexture = applyBlur(
132
+ const downSampledFrameTexture = applyDownsampling(
110
133
  gl,
111
134
  frameTexture,
112
- width,
113
- height,
135
+ downSampler,
136
+ vertexBuffer!,
137
+ bgBlurTextureWidth,
138
+ bgBlurTextureHeight,
139
+ );
140
+ backgroundTexture = applyBlur(
141
+ gl,
142
+ downSampledFrameTexture,
143
+ bgBlurTextureWidth,
144
+ bgBlurTextureHeight,
114
145
  blurRadius,
115
146
  blurProgram,
116
147
  blurUniforms,
@@ -125,20 +156,6 @@ export const setupWebGL = (canvas: OffscreenCanvas) => {
125
156
  backgroundTexture = bgTexture;
126
157
  }
127
158
 
128
- // Apply box blur to mask texture
129
- const blurredMaskTexture = applyBlur(
130
- gl,
131
- mask.getAsWebGLTexture(),
132
- width,
133
- height,
134
- blurRadius || 1.0, // Use a default blur radius if not set
135
- boxBlurProgram,
136
- boxBlurUniforms,
137
- vertexBuffer!,
138
- maskBlurFrameBuffers,
139
- maskBlurTextures,
140
- );
141
-
142
159
  // Render the final composite
143
160
  gl.viewport(0, 0, width, height);
144
161
  gl.clearColor(1.0, 1.0, 1.0, 1.0);
@@ -159,13 +176,11 @@ export const setupWebGL = (canvas: OffscreenCanvas) => {
159
176
  gl.bindTexture(gl.TEXTURE_2D, frameTexture);
160
177
  gl.uniform1i(frameTextureLocation, 1);
161
178
 
162
- // Set blurred mask texture
179
+ // Set mask texture - always read from the current read index
163
180
  gl.activeTexture(gl.TEXTURE2);
164
- gl.bindTexture(gl.TEXTURE_2D, blurredMaskTexture);
181
+ gl.bindTexture(gl.TEXTURE_2D, finalMaskTextures[readMaskIndex]);
165
182
  gl.uniform1i(maskTextureLocation, 2);
166
183
  gl.drawArrays(gl.TRIANGLES, 0, 6);
167
-
168
- mask.close();
169
184
  }
170
185
 
171
186
  /**
@@ -174,7 +189,7 @@ export const setupWebGL = (canvas: OffscreenCanvas) => {
174
189
  */
175
190
  async function setBackgroundImage(image: ImageBitmap | null) {
176
191
  // Clear existing background
177
- customBackgroundImage = emptyImageData;
192
+ customBackgroundImage = getEmptyImageData();
178
193
 
179
194
  if (image) {
180
195
  try {
@@ -197,42 +212,82 @@ export const setupWebGL = (canvas: OffscreenCanvas) => {
197
212
  }
198
213
 
199
214
  function setBlurRadius(radius: number | null) {
200
- blurRadius = radius;
215
+ blurRadius = radius ? Math.max(1, Math.floor(radius / downsampleFactor)) : null; // we are downsampling the blur texture, so decrease the radius here for better performance with a similar visual result
201
216
  setBackgroundImage(null);
202
217
  }
203
218
 
219
+ function updateMask(mask: WebGLTexture) {
220
+ // Use the existing applyBlur function to apply the first blur pass
221
+ // The second blur pass will be written to finalMaskTextures[writeMaskIndex]
222
+
223
+ // Create temporary arrays for the single blur operation
224
+ const tempFramebuffers = [tempMaskFrameBuffer, finalMaskFrameBuffers[writeMaskIndex]];
225
+
226
+ const tempTextures = [tempMaskTexture, finalMaskTextures[writeMaskIndex]];
227
+
228
+ // Apply the blur using the existing function
229
+ applyBlur(
230
+ gl,
231
+ mask,
232
+ canvas.width,
233
+ canvas.height,
234
+ maskBlurRadius || 1.0,
235
+ boxBlurProgram,
236
+ boxBlurUniforms,
237
+ vertexBuffer!,
238
+ tempFramebuffers,
239
+ tempTextures,
240
+ );
241
+
242
+ // Swap indices for the next frame
243
+ readMaskIndex = writeMaskIndex;
244
+ writeMaskIndex = 1 - writeMaskIndex;
245
+ }
246
+
204
247
  function cleanup() {
205
248
  gl.deleteProgram(compositeProgram);
206
249
  gl.deleteProgram(blurProgram);
207
250
  gl.deleteProgram(boxBlurProgram);
208
251
  gl.deleteTexture(bgTexture);
209
252
  gl.deleteTexture(frameTexture);
253
+ gl.deleteTexture(tempMaskTexture);
254
+ gl.deleteFramebuffer(tempMaskFrameBuffer);
255
+
210
256
  for (const texture of bgBlurTextures) {
211
257
  gl.deleteTexture(texture);
212
258
  }
213
259
  for (const framebuffer of bgBlurFrameBuffers) {
214
260
  gl.deleteFramebuffer(framebuffer);
215
261
  }
216
- for (const texture of maskBlurTextures) {
262
+ for (const texture of finalMaskTextures) {
217
263
  gl.deleteTexture(texture);
218
264
  }
219
- for (const framebuffer of maskBlurFrameBuffers) {
265
+ for (const framebuffer of finalMaskFrameBuffers) {
220
266
  gl.deleteFramebuffer(framebuffer);
221
267
  }
222
268
  gl.deleteBuffer(vertexBuffer);
223
269
 
270
+ if (blurredMaskTexture) {
271
+ gl.deleteTexture(blurredMaskTexture);
272
+ }
273
+
274
+ if (downSampler) {
275
+ gl.deleteTexture(downSampler.texture);
276
+ gl.deleteFramebuffer(downSampler.framebuffer);
277
+ gl.deleteProgram(downSampler.program);
278
+ }
279
+
224
280
  // Release any ImageBitmap resources
225
281
  if (customBackgroundImage) {
226
282
  if (customBackgroundImage instanceof ImageBitmap) {
227
283
  customBackgroundImage.close();
228
284
  }
229
- customBackgroundImage = emptyImageData;
285
+ customBackgroundImage = getEmptyImageData();
230
286
  }
231
287
  bgBlurTextures = [];
232
288
  bgBlurFrameBuffers = [];
233
- maskBlurTextures = [];
234
- maskBlurFrameBuffers = [];
289
+ finalMaskTextures = [];
235
290
  }
236
291
 
237
- return { render, setBackgroundImage, setBlurRadius, cleanup };
292
+ return { renderFrame, updateMask, setBackgroundImage, setBlurRadius, cleanup };
238
293
  };
@@ -1,9 +1,9 @@
1
- import { glsl } from '../utils';
1
+ import { createProgram, createShader, glsl } from '../utils';
2
2
  import { vertexShaderSource } from './vertexShader';
3
3
 
4
4
  // Define the blur fragment shader
5
5
  export const blurFragmentShader = glsl`#version 300 es
6
- precision highp float;
6
+ precision mediump float;
7
7
  in vec2 texCoords;
8
8
  uniform sampler2D u_texture;
9
9
  uniform vec2 u_texelSize;
@@ -33,42 +33,10 @@ export const blurFragmentShader = glsl`#version 300 es
33
33
  `;
34
34
 
35
35
  export function createBlurProgram(gl: WebGL2RenderingContext) {
36
- // Create blur shader
37
- const blurFrag = gl.createShader(gl.FRAGMENT_SHADER);
38
- if (!blurFrag) {
39
- throw Error('cannot create blur shader');
40
- }
41
- gl.shaderSource(blurFrag, blurFragmentShader);
42
- gl.compileShader(blurFrag);
43
-
44
- // Get compile status and log errors if any
45
- if (!gl.getShaderParameter(blurFrag, gl.COMPILE_STATUS)) {
46
- const info = gl.getShaderInfoLog(blurFrag);
47
- throw Error(`Failed to compile blur shader: ${info}`);
48
- }
36
+ const blurVertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource());
37
+ const blurFrag = createShader(gl, gl.FRAGMENT_SHADER, blurFragmentShader);
49
38
 
50
- // Create blur vertex shader
51
- const blurVertexShader = gl.createShader(gl.VERTEX_SHADER);
52
- if (!blurVertexShader) {
53
- throw Error('cannot create blur vertex shader');
54
- }
55
- gl.shaderSource(blurVertexShader, vertexShaderSource());
56
- gl.compileShader(blurVertexShader);
57
-
58
- // Create blur program
59
- const blurProgram = gl.createProgram();
60
- if (!blurProgram) {
61
- throw Error('cannot create blur program');
62
- }
63
- gl.attachShader(blurProgram, blurVertexShader);
64
- gl.attachShader(blurProgram, blurFrag);
65
- gl.linkProgram(blurProgram);
66
-
67
- // Check blur program link status
68
- if (!gl.getProgramParameter(blurProgram, gl.LINK_STATUS)) {
69
- const info = gl.getProgramInfoLog(blurProgram);
70
- throw Error(`Failed to link blur program: ${info}`);
71
- }
39
+ const blurProgram = createProgram(gl, blurVertexShader, blurFrag);
72
40
 
73
41
  // Get uniform locations
74
42
  const blurUniforms = {
@@ -1,4 +1,4 @@
1
- import { glsl } from '../utils';
1
+ import { createProgram, createShader, glsl } from '../utils';
2
2
  import { vertexShaderSource } from './vertexShader';
3
3
 
4
4
  export const boxBlurFragmentShader = glsl`#version 300 es
@@ -37,48 +37,10 @@ void main() {
37
37
  * Create the box blur shader program
38
38
  */
39
39
  export function createBoxBlurProgram(gl: WebGL2RenderingContext) {
40
- // Create vertex shader
41
- const vertexShader = gl.createShader(gl.VERTEX_SHADER);
42
- if (!vertexShader) {
43
- throw Error('cannot create vertex shader');
44
- }
45
- gl.shaderSource(vertexShader, vertexShaderSource());
46
- gl.compileShader(vertexShader);
47
-
48
- // Check vertex shader compilation
49
- if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
50
- const info = gl.getShaderInfoLog(vertexShader);
51
- throw Error(`Failed to compile vertex shader: ${info}`);
52
- }
53
-
54
- // Create fragment shader
55
- const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
56
- if (!fragmentShader) {
57
- throw Error('cannot create fragment shader');
58
- }
59
- gl.shaderSource(fragmentShader, boxBlurFragmentShader);
60
- gl.compileShader(fragmentShader);
40
+ const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource());
41
+ const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, boxBlurFragmentShader);
61
42
 
62
- // Check fragment shader compilation
63
- if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
64
- const info = gl.getShaderInfoLog(fragmentShader);
65
- throw Error(`Failed to compile box blur shader: ${info}`);
66
- }
67
-
68
- // Create the program
69
- const program = gl.createProgram();
70
- if (!program) {
71
- throw Error('cannot create box blur program');
72
- }
73
- gl.attachShader(program, vertexShader);
74
- gl.attachShader(program, fragmentShader);
75
- gl.linkProgram(program);
76
-
77
- // Check program link status
78
- if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
79
- const info = gl.getProgramInfoLog(program);
80
- throw Error(`Failed to link box blur program: ${info}`);
81
- }
43
+ const program = createProgram(gl, vertexShader, fragmentShader);
82
44
 
83
45
  // Get attribute and uniform locations
84
46
  const uniforms = {
@@ -1,9 +1,9 @@
1
- import { glsl } from '../utils';
1
+ import { createProgram, createShader, glsl } from '../utils';
2
2
  import { vertexShaderSource } from './vertexShader';
3
3
 
4
4
  // Fragment shader source for compositing
5
5
  export const compositeFragmentShader = glsl`#version 300 es
6
- precision highp float;
6
+ precision mediump float;
7
7
  in vec2 texCoords;
8
8
  uniform sampler2D background;
9
9
  uniform sampler2D frame;
@@ -37,48 +37,10 @@ export const compositeFragmentShader = glsl`#version 300 es
37
37
  * Create the composite shader program
38
38
  */
39
39
  export function createCompositeProgram(gl: WebGL2RenderingContext) {
40
- // Create vertex shader
41
- const vertexShader = gl.createShader(gl.VERTEX_SHADER);
42
- if (!vertexShader) {
43
- throw Error('cannot create vertex shader');
44
- }
45
- gl.shaderSource(vertexShader, vertexShaderSource());
46
- gl.compileShader(vertexShader);
47
-
48
- // Check vertex shader compilation
49
- if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
50
- const info = gl.getShaderInfoLog(vertexShader);
51
- throw Error(`Failed to compile vertex shader: ${info}`);
52
- }
53
-
54
- // Create fragment shader
55
- const compositeShader = gl.createShader(gl.FRAGMENT_SHADER);
56
- if (!compositeShader) {
57
- throw Error('cannot create fragment shader');
58
- }
59
- gl.shaderSource(compositeShader, compositeFragmentShader);
60
- gl.compileShader(compositeShader);
40
+ const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource());
41
+ const compositeShader = createShader(gl, gl.FRAGMENT_SHADER, compositeFragmentShader);
61
42
 
62
- // Check fragment shader compilation
63
- if (!gl.getShaderParameter(compositeShader, gl.COMPILE_STATUS)) {
64
- const info = gl.getShaderInfoLog(compositeShader);
65
- throw Error(`Failed to compile composite shader: ${info}`);
66
- }
67
-
68
- // Create the program
69
- const compositeProgram = gl.createProgram();
70
- if (!compositeProgram) {
71
- throw Error('cannot create composite program');
72
- }
73
- gl.attachShader(compositeProgram, vertexShader);
74
- gl.attachShader(compositeProgram, compositeShader);
75
- gl.linkProgram(compositeProgram);
76
-
77
- // Check program link status
78
- if (!gl.getProgramParameter(compositeProgram, gl.LINK_STATUS)) {
79
- const info = gl.getProgramInfoLog(compositeProgram);
80
- throw Error(`Failed to link composite program: ${info}`);
81
- }
43
+ const compositeProgram = createProgram(gl, vertexShader, compositeShader);
82
44
 
83
45
  // Get attribute and uniform locations
84
46
  const attribLocations = {
@@ -0,0 +1,94 @@
1
+ import { createProgram, createShader } from '../utils';
2
+
3
+ export function createDownSampler(
4
+ gl: WebGL2RenderingContext,
5
+ width: number,
6
+ height: number,
7
+ ): {
8
+ framebuffer: WebGLFramebuffer;
9
+ texture: WebGLTexture;
10
+ program: WebGLProgram;
11
+ uniforms: any;
12
+ } {
13
+ // Create texture
14
+ const texture = gl.createTexture()!;
15
+ gl.bindTexture(gl.TEXTURE_2D, texture);
16
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
17
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
18
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
19
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
20
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
21
+
22
+ // Create framebuffer
23
+ const framebuffer = gl.createFramebuffer()!;
24
+ gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
25
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
26
+
27
+ // Create shader program for copying
28
+ const vertexSource = `
29
+ attribute vec2 position;
30
+ varying vec2 v_uv;
31
+ void main() {
32
+ v_uv = (position + 1.0) * 0.5;
33
+ gl_Position = vec4(position, 0.0, 1.0);
34
+ }
35
+ `;
36
+
37
+ const fragmentSource = `
38
+ precision mediump float;
39
+ varying vec2 v_uv;
40
+ uniform sampler2D u_texture;
41
+ void main() {
42
+ gl_FragColor = texture2D(u_texture, v_uv);
43
+ }
44
+ `;
45
+
46
+ const vertShader = createShader(gl, gl.VERTEX_SHADER, vertexSource);
47
+ const fragShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);
48
+ const program = createProgram(gl, vertShader, fragShader);
49
+
50
+ const uniforms = {
51
+ texture: gl.getUniformLocation(program, 'u_texture'),
52
+ position: gl.getAttribLocation(program, 'position'),
53
+ };
54
+
55
+ return {
56
+ framebuffer,
57
+ texture,
58
+ program,
59
+ uniforms,
60
+ };
61
+ }
62
+
63
+ export function applyDownsampling(
64
+ gl: WebGL2RenderingContext,
65
+ inputTexture: WebGLTexture,
66
+ downSampler: {
67
+ framebuffer: WebGLFramebuffer;
68
+ texture: WebGLTexture;
69
+ program: WebGLProgram;
70
+ uniforms: any;
71
+ },
72
+ vertexBuffer: WebGLBuffer,
73
+ width: number,
74
+ height: number,
75
+ ): WebGLTexture {
76
+ gl.useProgram(downSampler.program);
77
+
78
+ gl.bindFramebuffer(gl.FRAMEBUFFER, downSampler.framebuffer);
79
+ gl.viewport(0, 0, width, height);
80
+
81
+ gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
82
+ gl.enableVertexAttribArray(downSampler.uniforms.position);
83
+ gl.vertexAttribPointer(downSampler.uniforms.position, 2, gl.FLOAT, false, 0, 0);
84
+
85
+ gl.activeTexture(gl.TEXTURE0);
86
+ gl.bindTexture(gl.TEXTURE_2D, inputTexture);
87
+ gl.uniform1i(downSampler.uniforms.texture, 0);
88
+
89
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
90
+
91
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
92
+
93
+ return downSampler.texture;
94
+ }
@@ -15,6 +15,38 @@ export function initTexture(gl: WebGL2RenderingContext, texIndex: number) {
15
15
  return texture;
16
16
  }
17
17
 
18
+ export function createShader(
19
+ gl: WebGL2RenderingContext,
20
+ type: number,
21
+ source: string,
22
+ ): WebGLShader {
23
+ const shader = gl.createShader(type)!;
24
+ gl.shaderSource(shader, source);
25
+ gl.compileShader(shader);
26
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
27
+ console.error('Shader compile failed:', gl.getShaderInfoLog(shader));
28
+ gl.deleteShader(shader);
29
+ throw new Error('Shader compile failed');
30
+ }
31
+ return shader;
32
+ }
33
+
34
+ export function createProgram(
35
+ gl: WebGL2RenderingContext,
36
+ vs: WebGLShader,
37
+ fs: WebGLShader,
38
+ ): WebGLProgram {
39
+ const program = gl.createProgram()!;
40
+ gl.attachShader(program, vs);
41
+ gl.attachShader(program, fs);
42
+ gl.linkProgram(program);
43
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
44
+ console.error('Program link failed:', gl.getProgramInfoLog(program));
45
+ throw new Error('Program link failed');
46
+ }
47
+ return program;
48
+ }
49
+
18
50
  /**
19
51
  * Create a WebGL framebuffer with the given texture as color attachment
20
52
  */
@@ -99,12 +131,20 @@ export async function resizeImageToCover(
99
131
  });
100
132
  }
101
133
 
102
- const emptyImageData = new ImageData(2, 2);
103
- emptyImageData.data[0] = 0;
104
- emptyImageData.data[1] = 0;
105
- emptyImageData.data[2] = 0;
106
- emptyImageData.data[3] = 0;
134
+ let emptyImageData: ImageData | undefined;
135
+
136
+ function getEmptyImageData() {
137
+ if (!emptyImageData) {
138
+ emptyImageData = new ImageData(2, 2);
139
+ emptyImageData.data[0] = 0;
140
+ emptyImageData.data[1] = 0;
141
+ emptyImageData.data[2] = 0;
142
+ emptyImageData.data[3] = 0;
143
+ }
144
+
145
+ return emptyImageData;
146
+ }
107
147
 
108
148
  const glsl = (source: any) => source;
109
149
 
110
- export { emptyImageData, glsl };
150
+ export { getEmptyImageData, glsl };