@livekit/track-processors 0.5.2 → 0.5.4

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,250 +1,84 @@
1
1
  import { MPMask } from '@mediapipe/tasks-vision';
2
-
3
- // Define the blur fragment shader
4
- const blurFragmentShader = `
5
- precision highp float;
6
- varying vec2 texCoords;
7
- uniform sampler2D u_texture;
8
- uniform vec2 u_texelSize;
9
- uniform vec2 u_direction;
10
- uniform float u_radius;
11
-
12
- void main() {
13
- float sigma = u_radius;
14
- float twoSigmaSq = 2.0 * sigma * sigma;
15
- float totalWeight = 0.0;
16
- vec3 result = vec3(0.0);
17
- const int MAX_SAMPLES = 16;
18
- int radius = int(min(float(MAX_SAMPLES), ceil(u_radius)));
19
-
20
- for (int i = -MAX_SAMPLES; i <= MAX_SAMPLES; ++i) {
21
- float offset = float(i);
22
- if (abs(offset) > float(radius)) continue;
23
- float weight = exp(-(offset * offset) / twoSigmaSq);
24
- vec2 sampleCoord = texCoords + u_direction * u_texelSize * offset;
25
- result += texture2D(u_texture, sampleCoord).rgb * weight;
26
- totalWeight += weight;
27
- }
28
-
29
- gl_FragColor = vec4(result / totalWeight, 1.0);
30
- }
31
- `;
32
-
33
- const createShaderProgram = (gl: WebGL2RenderingContext) => {
34
- const vs = `
35
- attribute vec2 position;
36
- varying vec2 texCoords;
37
-
38
- void main() {
39
- texCoords = (position + 1.0) / 2.0;
40
- texCoords.y = 1.0 - texCoords.y;
41
- gl_Position = vec4(position, 0, 1.0);
42
- }
43
- `;
44
-
45
- const cS = `
46
- precision highp float;
47
- varying vec2 texCoords;
48
- uniform sampler2D background;
49
- uniform sampler2D frame;
50
- uniform sampler2D mask;
51
- void main() {
52
- vec4 maskTex = texture2D(mask, texCoords);
53
- vec4 frameTex = texture2D(frame, texCoords);
54
- vec4 bgTex = texture2D(background, texCoords);
55
-
56
-
57
- float a = maskTex.r;
58
-
59
- gl_FragColor = mix(bgTex, vec4(frameTex.rgb, 1.0), 1.0 - a);
60
-
61
- }
62
- `;
63
-
64
- const vertexShader = gl.createShader(gl.VERTEX_SHADER);
65
- if (!vertexShader) {
66
- throw Error('can not create vertex shader');
67
- }
68
- gl.shaderSource(vertexShader, vs);
69
- gl.compileShader(vertexShader);
70
-
71
- // Create our fragment shader
72
- const compositeShader = gl.createShader(gl.FRAGMENT_SHADER);
73
- if (!compositeShader) {
74
- throw Error('can not create fragment shader');
75
- }
76
- gl.shaderSource(compositeShader, cS);
77
- gl.compileShader(compositeShader);
78
-
79
- // Create the composite program
80
- const compositeProgram = gl.createProgram();
81
- if (!compositeProgram) {
82
- throw Error('can not create composite program');
83
- }
84
- gl.attachShader(compositeProgram, vertexShader);
85
- gl.attachShader(compositeProgram, compositeShader);
86
- gl.linkProgram(compositeProgram);
87
-
88
- let blurProgram = null;
89
- let blurVertexShader = null;
90
- let blurFrag = null;
91
- let blurUniforms = null;
92
-
93
- // Create blur shader if enabled
94
- blurFrag = gl.createShader(gl.FRAGMENT_SHADER);
95
- if (!blurFrag) {
96
- throw Error('can not create blur shader');
97
- }
98
- gl.shaderSource(blurFrag, blurFragmentShader);
99
- gl.compileShader(blurFrag);
100
-
101
- // Get compile status and log errors if any
102
- if (!gl.getShaderParameter(blurFrag, gl.COMPILE_STATUS)) {
103
- const info = gl.getShaderInfoLog(blurFrag);
104
- throw Error(`Failed to compile blur shader: ${info}`);
105
- }
106
-
107
- // Create blur program
108
- blurVertexShader = gl.createShader(gl.VERTEX_SHADER);
109
- if (!blurVertexShader) {
110
- throw Error('can not create blur vertex shader');
111
- }
112
- gl.shaderSource(blurVertexShader, vs);
113
- gl.compileShader(blurVertexShader);
114
-
115
- blurProgram = gl.createProgram();
116
- if (!blurProgram) {
117
- throw Error('can not create blur program');
118
- }
119
- gl.attachShader(blurProgram, blurVertexShader);
120
- gl.attachShader(blurProgram, blurFrag);
121
- gl.linkProgram(blurProgram);
122
-
123
- // Check blur program link status
124
- if (!gl.getProgramParameter(blurProgram, gl.LINK_STATUS)) {
125
- const info = gl.getProgramInfoLog(blurProgram);
126
- throw Error(`Failed to link blur program: ${info}`);
127
- }
128
-
129
- blurUniforms = {
130
- position: gl.getAttribLocation(blurProgram, 'position'),
131
- texture: gl.getUniformLocation(blurProgram, 'u_texture'),
132
- texelSize: gl.getUniformLocation(blurProgram, 'u_texelSize'),
133
- direction: gl.getUniformLocation(blurProgram, 'u_direction'),
134
- radius: gl.getUniformLocation(blurProgram, 'u_radius'),
135
- };
136
-
137
- return {
138
- vertexShader,
139
- compositeShader,
140
- blurShader: blurFrag,
141
- compositeProgram,
142
- blurProgram,
143
- attribLocations: {
144
- position: gl.getAttribLocation(compositeProgram, 'position'),
145
- },
146
- uniformLocations: {
147
- mask: gl.getUniformLocation(compositeProgram, 'mask')!,
148
- frame: gl.getUniformLocation(compositeProgram, 'frame')!,
149
- background: gl.getUniformLocation(compositeProgram, 'background')!,
150
- },
151
- blurUniforms,
152
- };
153
- };
154
-
155
- export function initTexture(gl: WebGL2RenderingContext, texIndex: number) {
156
- const texRef = gl.TEXTURE0 + texIndex;
157
- gl.activeTexture(texRef);
158
- const texture = gl.createTexture();
159
- gl.bindTexture(gl.TEXTURE_2D, texture);
160
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
161
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
162
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
163
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
164
- gl.bindTexture(gl.TEXTURE_2D, texture);
165
-
166
- return texture;
167
- }
168
-
169
- export function createFramebuffer(
170
- gl: WebGL2RenderingContext,
171
- texture: WebGLTexture,
172
- width: number,
173
- height: number,
174
- ) {
175
- const framebuffer = gl.createFramebuffer();
176
- gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
177
-
178
- // Set the texture as the color attachment
179
- gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
180
-
181
- // Ensure texture dimensions match the provided width and height
182
- gl.bindTexture(gl.TEXTURE_2D, texture);
183
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
184
-
185
- // Check if framebuffer is complete
186
- const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
187
- if (status !== gl.FRAMEBUFFER_COMPLETE) {
188
- throw new Error('Framebuffer not complete');
189
- }
190
-
191
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
192
- return framebuffer;
193
- }
194
-
195
- const createVertexBuffer = (gl: WebGL2RenderingContext) => {
196
- if (!gl) {
197
- return null;
198
- }
199
- const vertexBuffer = gl.createBuffer();
200
- gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
201
- gl.bufferData(
202
- gl.ARRAY_BUFFER,
203
- new Float32Array([-1, -1, -1, 1, 1, 1, -1, -1, 1, 1, 1, -1]),
204
- gl.STATIC_DRAW,
205
- );
206
- return vertexBuffer;
207
- };
2
+ import { applyBlur, createBlurProgram } from './shader-programs/blurShader';
3
+ import { createBoxBlurProgram } from './shader-programs/boxBlurShader';
4
+ import { createCompositeProgram } from './shader-programs/compositeShader';
5
+ import {
6
+ createFramebuffer,
7
+ createVertexBuffer,
8
+ emptyImageData,
9
+ initTexture,
10
+ resizeImageToCover,
11
+ } from './utils';
208
12
 
209
13
  export const setupWebGL = (canvas: OffscreenCanvas) => {
210
- const gl = canvas.getContext('webgl2', { premultipliedAlpha: false }) as WebGL2RenderingContext;
14
+ const gl = canvas.getContext('webgl2', {
15
+ antialias: true,
16
+ premultipliedAlpha: true,
17
+ }) as WebGL2RenderingContext;
211
18
 
212
19
  let blurRadius: number | null = null;
213
20
 
214
21
  if (!gl) {
22
+ console.error('Failed to create WebGL context');
215
23
  return undefined;
216
24
  }
217
25
 
218
26
  gl.enable(gl.BLEND);
219
27
  gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
220
28
 
29
+ // Create the composite program
30
+ const composite = createCompositeProgram(gl);
31
+ const compositeProgram = composite.program;
32
+ const positionLocation = composite.attribLocations.position;
221
33
  const {
222
- compositeProgram,
223
- blurProgram,
224
- attribLocations: { position: positionLocation },
225
- uniformLocations: {
226
- mask: maskTextureLocation,
227
- frame: frameTextureLocation,
228
- background: bgTextureLocation,
229
- },
230
- blurUniforms,
231
- } = createShaderProgram(gl);
34
+ mask: maskTextureLocation,
35
+ frame: frameTextureLocation,
36
+ background: bgTextureLocation,
37
+ } = composite.uniformLocations;
38
+
39
+ // Create the blur program using the same vertex shader source
40
+ const blur = createBlurProgram(gl);
41
+ const blurProgram = blur.program;
42
+ const blurUniforms = blur.uniforms;
43
+
44
+ // Create the box blur program
45
+ const boxBlur = createBoxBlurProgram(gl);
46
+ const boxBlurProgram = boxBlur.program;
47
+ const boxBlurUniforms = boxBlur.uniforms;
232
48
 
233
49
  const bgTexture = initTexture(gl, 0);
234
50
  const frameTexture = initTexture(gl, 1);
235
51
  const vertexBuffer = createVertexBuffer(gl);
236
52
 
237
- // Create additional textures and framebuffers for processing
238
- let processTextures: WebGLTexture[] = [];
239
- let processFramebuffers: WebGLFramebuffer[] = [];
240
-
241
- // Create textures for processing (blur)
242
- processTextures.push(initTexture(gl, 3));
243
- processTextures.push(initTexture(gl, 4));
53
+ if (!vertexBuffer) {
54
+ throw new Error('Failed to create vertex buffer');
55
+ }
244
56
 
245
- // Create framebuffers for processing
246
- processFramebuffers.push(createFramebuffer(gl, processTextures[0], canvas.width, canvas.height));
247
- processFramebuffers.push(createFramebuffer(gl, processTextures[1], canvas.width, canvas.height));
57
+ // Create additional textures and framebuffers for processing
58
+ let bgBlurTextures: WebGLTexture[] = [];
59
+ let bgBlurFrameBuffers: WebGLFramebuffer[] = [];
60
+ let maskBlurTextures: WebGLTexture[] = [];
61
+ let maskBlurFrameBuffers: WebGLFramebuffer[] = [];
62
+
63
+ // Create textures for background processing (blur)
64
+ bgBlurTextures.push(initTexture(gl, 3)); // For blur pass 1
65
+ bgBlurTextures.push(initTexture(gl, 4)); // For blur pass 2
66
+
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));
70
+
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
74
+
75
+ // Create framebuffers for mask processing
76
+ maskBlurFrameBuffers.push(
77
+ createFramebuffer(gl, maskBlurTextures[0], canvas.width, canvas.height),
78
+ );
79
+ maskBlurFrameBuffers.push(
80
+ createFramebuffer(gl, maskBlurTextures[1], canvas.width, canvas.height),
81
+ );
248
82
 
249
83
  // Set up uniforms for the composite shader
250
84
  gl.useProgram(compositeProgram);
@@ -253,50 +87,7 @@ export const setupWebGL = (canvas: OffscreenCanvas) => {
253
87
  gl.uniform1i(maskTextureLocation, 2);
254
88
 
255
89
  // Store custom background image
256
- let customBackgroundImage: ImageBitmap | null = null;
257
-
258
- function applyBlur(sourceTexture: WebGLTexture, width: number, height: number) {
259
- if (!blurRadius || !blurProgram || !blurUniforms) return bgTexture;
260
-
261
- gl.useProgram(blurProgram);
262
-
263
- // Set common attributes
264
- gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
265
- gl.vertexAttribPointer(blurUniforms.position, 2, gl.FLOAT, false, 0, 0);
266
- gl.enableVertexAttribArray(blurUniforms.position);
267
-
268
- const texelWidth = 1.0 / width;
269
- const texelHeight = 1.0 / height;
270
-
271
- // First pass - horizontal blur
272
- gl.bindFramebuffer(gl.FRAMEBUFFER, processFramebuffers[0]);
273
- gl.viewport(0, 0, width, height);
274
-
275
- gl.activeTexture(gl.TEXTURE0);
276
- gl.bindTexture(gl.TEXTURE_2D, sourceTexture);
277
- gl.uniform1i(blurUniforms.texture, 0);
278
- gl.uniform2f(blurUniforms.texelSize, texelWidth, texelHeight);
279
- gl.uniform2f(blurUniforms.direction, 1.0, 0.0); // Horizontal
280
- gl.uniform1f(blurUniforms.radius, blurRadius);
281
-
282
- gl.drawArrays(gl.TRIANGLES, 0, 6);
283
-
284
- // Second pass - vertical blur
285
- gl.bindFramebuffer(gl.FRAMEBUFFER, processFramebuffers[1]);
286
- gl.viewport(0, 0, width, height);
287
-
288
- gl.activeTexture(gl.TEXTURE0);
289
- gl.bindTexture(gl.TEXTURE_2D, processTextures[0]);
290
- gl.uniform1i(blurUniforms.texture, 0);
291
- gl.uniform2f(blurUniforms.direction, 0.0, 1.0); // Vertical
292
-
293
- gl.drawArrays(gl.TRIANGLES, 0, 6);
294
-
295
- // Reset framebuffer
296
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
297
-
298
- return processTextures[1];
299
- }
90
+ let customBackgroundImage: ImageBitmap | ImageData = emptyImageData;
300
91
 
301
92
  function render(frame: VideoFrame, mask: MPMask) {
302
93
  if (frame.codedWidth === 0 || mask.width === 0) {
@@ -314,19 +105,39 @@ export const setupWebGL = (canvas: OffscreenCanvas) => {
314
105
  // Apply blur if enabled (and no custom background is set)
315
106
  let backgroundTexture = bgTexture;
316
107
 
317
- // If we have a custom background image, use that
318
- if (customBackgroundImage) {
108
+ if (blurRadius) {
109
+ backgroundTexture = applyBlur(
110
+ gl,
111
+ frameTexture,
112
+ width,
113
+ height,
114
+ blurRadius,
115
+ blurProgram,
116
+ blurUniforms,
117
+ vertexBuffer!,
118
+ bgBlurFrameBuffers,
119
+ bgBlurTextures,
120
+ );
121
+ } else {
319
122
  gl.activeTexture(gl.TEXTURE0);
320
123
  gl.bindTexture(gl.TEXTURE_2D, bgTexture);
321
124
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, customBackgroundImage);
322
125
  backgroundTexture = bgTexture;
323
- } else if (blurRadius) {
324
- // Otherwise, if blur is enabled, apply blur effect to the frame
325
- backgroundTexture = applyBlur(frameTexture, width, height);
326
126
  }
327
127
 
328
- // Get the mask texture
329
- const maskTexture = mask.getAsWebGLTexture();
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
+ );
330
141
 
331
142
  // Render the final composite
332
143
  gl.viewport(0, 0, width, height);
@@ -348,9 +159,9 @@ export const setupWebGL = (canvas: OffscreenCanvas) => {
348
159
  gl.bindTexture(gl.TEXTURE_2D, frameTexture);
349
160
  gl.uniform1i(frameTextureLocation, 1);
350
161
 
351
- // Set mask texture
162
+ // Set blurred mask texture
352
163
  gl.activeTexture(gl.TEXTURE2);
353
- gl.bindTexture(gl.TEXTURE_2D, maskTexture);
164
+ gl.bindTexture(gl.TEXTURE_2D, blurredMaskTexture);
354
165
  gl.uniform1i(maskTextureLocation, 2);
355
166
  gl.drawArrays(gl.TRIANGLES, 0, 6);
356
167
 
@@ -363,68 +174,26 @@ export const setupWebGL = (canvas: OffscreenCanvas) => {
363
174
  */
364
175
  async function setBackgroundImage(image: ImageBitmap | null) {
365
176
  // Clear existing background
366
- customBackgroundImage = null;
177
+ customBackgroundImage = emptyImageData;
367
178
 
368
179
  if (image) {
369
180
  try {
370
- // Get current canvas dimensions
371
- const canvasWidth = canvas.width;
372
- const canvasHeight = canvas.height;
373
-
374
- // Calculate dimensions and crop for "cover" mode
375
- const imgAspect = image.width / image.height;
376
- const canvasAspect = canvasWidth / canvasHeight;
377
-
378
- let sx = 0;
379
- let sy = 0;
380
- let sWidth = image.width;
381
- let sHeight = image.height;
382
-
383
- // For cover mode, we need to crop some parts of the image
384
- // to ensure it covers the canvas while maintaining aspect ratio
385
- if (imgAspect > canvasAspect) {
386
- // Image is wider than canvas - crop the sides
387
- sWidth = Math.round(image.height * canvasAspect);
388
- sx = Math.round((image.width - sWidth) / 2); // Center the crop horizontally
389
- } else if (imgAspect < canvasAspect) {
390
- // Image is taller than canvas - crop the top/bottom
391
- sHeight = Math.round(image.width / canvasAspect);
392
- sy = Math.round((image.height - sHeight) / 2); // Center the crop vertically
393
- }
394
-
395
- // Create a new ImageBitmap with the cropped portion
396
- const croppedImage = await createImageBitmap(image, sx, sy, sWidth, sHeight, {
397
- resizeWidth: canvasWidth,
398
- resizeHeight: canvasHeight,
399
- resizeQuality: 'medium',
400
- });
181
+ // Resize and crop the image to cover the canvas
182
+ const croppedImage = await resizeImageToCover(image, canvas.width, canvas.height);
401
183
 
402
184
  // Store the cropped and resized image
403
185
  customBackgroundImage = croppedImage;
404
-
405
- // Load the image into the texture
406
- gl.activeTexture(gl.TEXTURE0);
407
- gl.bindTexture(gl.TEXTURE_2D, bgTexture);
408
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, croppedImage);
409
186
  } catch (error) {
410
- console.error('Error processing background image:', error);
411
- // Fallback to original image on error
412
- customBackgroundImage = image;
413
- gl.activeTexture(gl.TEXTURE0);
414
- gl.bindTexture(gl.TEXTURE_2D, bgTexture);
415
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
187
+ console.error(
188
+ 'Error processing background image, falling back to black background:',
189
+ error,
190
+ );
416
191
  }
417
- } else {
418
- // set the background texture to an empty 2x2 image
419
- const emptyImage = new ImageData(2, 2);
420
- emptyImage.data[0] = 0;
421
- emptyImage.data[1] = 0;
422
- emptyImage.data[2] = 0;
423
- emptyImage.data[3] = 0;
424
- gl.activeTexture(gl.TEXTURE0);
425
- gl.bindTexture(gl.TEXTURE_2D, bgTexture);
426
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, emptyImage);
427
192
  }
193
+
194
+ gl.activeTexture(gl.TEXTURE0);
195
+ gl.bindTexture(gl.TEXTURE_2D, bgTexture);
196
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, customBackgroundImage);
428
197
  }
429
198
 
430
199
  function setBlurRadius(radius: number | null) {
@@ -435,23 +204,34 @@ export const setupWebGL = (canvas: OffscreenCanvas) => {
435
204
  function cleanup() {
436
205
  gl.deleteProgram(compositeProgram);
437
206
  gl.deleteProgram(blurProgram);
207
+ gl.deleteProgram(boxBlurProgram);
438
208
  gl.deleteTexture(bgTexture);
439
209
  gl.deleteTexture(frameTexture);
440
- for (const texture of processTextures) {
210
+ for (const texture of bgBlurTextures) {
441
211
  gl.deleteTexture(texture);
442
212
  }
443
- for (const framebuffer of processFramebuffers) {
213
+ for (const framebuffer of bgBlurFrameBuffers) {
214
+ gl.deleteFramebuffer(framebuffer);
215
+ }
216
+ for (const texture of maskBlurTextures) {
217
+ gl.deleteTexture(texture);
218
+ }
219
+ for (const framebuffer of maskBlurFrameBuffers) {
444
220
  gl.deleteFramebuffer(framebuffer);
445
221
  }
446
222
  gl.deleteBuffer(vertexBuffer);
447
223
 
448
224
  // Release any ImageBitmap resources
449
225
  if (customBackgroundImage) {
450
- customBackgroundImage.close();
451
- customBackgroundImage = null;
226
+ if (customBackgroundImage instanceof ImageBitmap) {
227
+ customBackgroundImage.close();
228
+ }
229
+ customBackgroundImage = emptyImageData;
452
230
  }
453
- processTextures = [];
454
- processFramebuffers = [];
231
+ bgBlurTextures = [];
232
+ bgBlurFrameBuffers = [];
233
+ maskBlurTextures = [];
234
+ maskBlurFrameBuffers = [];
455
235
  }
456
236
 
457
237
  return { render, setBackgroundImage, setBlurRadius, cleanup };
@@ -0,0 +1,140 @@
1
+ import { glsl } from '../utils';
2
+ import { vertexShaderSource } from './vertexShader';
3
+
4
+ // Define the blur fragment shader
5
+ export const blurFragmentShader = glsl`#version 300 es
6
+ precision highp float;
7
+ in vec2 texCoords;
8
+ uniform sampler2D u_texture;
9
+ uniform vec2 u_texelSize;
10
+ uniform vec2 u_direction;
11
+ uniform float u_radius;
12
+ out vec4 fragColor;
13
+
14
+ void main() {
15
+ float sigma = u_radius;
16
+ float twoSigmaSq = 2.0 * sigma * sigma;
17
+ float totalWeight = 0.0;
18
+ vec3 result = vec3(0.0);
19
+ const int MAX_SAMPLES = 16;
20
+ int radius = int(min(float(MAX_SAMPLES), ceil(u_radius)));
21
+
22
+ for (int i = -MAX_SAMPLES; i <= MAX_SAMPLES; ++i) {
23
+ float offset = float(i);
24
+ if (abs(offset) > float(radius)) continue;
25
+ float weight = exp(-(offset * offset) / twoSigmaSq);
26
+ vec2 sampleCoord = texCoords + u_direction * u_texelSize * offset;
27
+ result += texture(u_texture, sampleCoord).rgb * weight;
28
+ totalWeight += weight;
29
+ }
30
+
31
+ fragColor = vec4(result / totalWeight, 1.0);
32
+ }
33
+ `;
34
+
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
+ }
49
+
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
+ }
72
+
73
+ // Get uniform locations
74
+ const blurUniforms = {
75
+ position: gl.getAttribLocation(blurProgram, 'position'),
76
+ texture: gl.getUniformLocation(blurProgram, 'u_texture'),
77
+ texelSize: gl.getUniformLocation(blurProgram, 'u_texelSize'),
78
+ direction: gl.getUniformLocation(blurProgram, 'u_direction'),
79
+ radius: gl.getUniformLocation(blurProgram, 'u_radius'),
80
+ };
81
+
82
+ return {
83
+ program: blurProgram,
84
+ shader: blurFrag,
85
+ vertexShader: blurVertexShader,
86
+ uniforms: blurUniforms,
87
+ };
88
+ }
89
+
90
+ export function applyBlur(
91
+ gl: WebGL2RenderingContext,
92
+ sourceTexture: WebGLTexture,
93
+ width: number,
94
+ height: number,
95
+ blurRadius: number,
96
+ blurProgram: WebGLProgram,
97
+ blurUniforms: any,
98
+ vertexBuffer: WebGLBuffer,
99
+ processFramebuffers: WebGLFramebuffer[],
100
+ processTextures: WebGLTexture[],
101
+ ) {
102
+ gl.useProgram(blurProgram);
103
+
104
+ // Set common attributes
105
+ gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
106
+ gl.vertexAttribPointer(blurUniforms.position, 2, gl.FLOAT, false, 0, 0);
107
+ gl.enableVertexAttribArray(blurUniforms.position);
108
+
109
+ const texelWidth = 1.0 / width;
110
+ const texelHeight = 1.0 / height;
111
+
112
+ // First pass - horizontal blur
113
+ gl.bindFramebuffer(gl.FRAMEBUFFER, processFramebuffers[0]);
114
+ gl.viewport(0, 0, width, height);
115
+
116
+ gl.activeTexture(gl.TEXTURE0);
117
+ gl.bindTexture(gl.TEXTURE_2D, sourceTexture);
118
+ gl.uniform1i(blurUniforms.texture, 0);
119
+ gl.uniform2f(blurUniforms.texelSize, texelWidth, texelHeight);
120
+ gl.uniform2f(blurUniforms.direction, 1.0, 0.0); // Horizontal
121
+ gl.uniform1f(blurUniforms.radius, blurRadius);
122
+
123
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
124
+
125
+ // Second pass - vertical blur
126
+ gl.bindFramebuffer(gl.FRAMEBUFFER, processFramebuffers[1]);
127
+ gl.viewport(0, 0, width, height);
128
+
129
+ gl.activeTexture(gl.TEXTURE0);
130
+ gl.bindTexture(gl.TEXTURE_2D, processTextures[0]);
131
+ gl.uniform1i(blurUniforms.texture, 0);
132
+ gl.uniform2f(blurUniforms.direction, 0.0, 1.0); // Vertical
133
+
134
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
135
+
136
+ // Reset framebuffer
137
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
138
+
139
+ return processTextures[1];
140
+ }