@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.
- package/dist/index.js +380 -221
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +380 -221
- package/dist/index.mjs.map +1 -1
- package/dist/src/webgl/index.d.ts +0 -2
- package/dist/src/webgl/shader-programs/blurShader.d.ts +14 -0
- package/dist/src/webgl/shader-programs/boxBlurShader.d.ts +16 -0
- package/dist/src/webgl/shader-programs/compositeShader.d.ts +18 -0
- package/dist/src/webgl/shader-programs/vertexShader.d.ts +1 -0
- package/dist/src/webgl/utils.d.ts +23 -0
- package/package.json +2 -2
- package/src/index.ts +9 -11
- package/src/webgl/index.ts +119 -339
- package/src/webgl/shader-programs/blurShader.ts +140 -0
- package/src/webgl/shader-programs/boxBlurShader.ts +98 -0
- package/src/webgl/shader-programs/compositeShader.ts +102 -0
- package/src/webgl/shader-programs/vertexShader.ts +11 -0
- package/src/webgl/utils.ts +110 -0
package/src/webgl/index.ts
CHANGED
|
@@ -1,250 +1,84 @@
|
|
|
1
1
|
import { MPMask } from '@mediapipe/tasks-vision';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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', {
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
247
|
-
|
|
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 |
|
|
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
|
-
|
|
318
|
-
|
|
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
|
-
//
|
|
329
|
-
const
|
|
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,
|
|
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 =
|
|
177
|
+
customBackgroundImage = emptyImageData;
|
|
367
178
|
|
|
368
179
|
if (image) {
|
|
369
180
|
try {
|
|
370
|
-
//
|
|
371
|
-
const
|
|
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(
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
|
210
|
+
for (const texture of bgBlurTextures) {
|
|
441
211
|
gl.deleteTexture(texture);
|
|
442
212
|
}
|
|
443
|
-
for (const framebuffer of
|
|
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
|
|
451
|
-
|
|
226
|
+
if (customBackgroundImage instanceof ImageBitmap) {
|
|
227
|
+
customBackgroundImage.close();
|
|
228
|
+
}
|
|
229
|
+
customBackgroundImage = emptyImageData;
|
|
452
230
|
}
|
|
453
|
-
|
|
454
|
-
|
|
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
|
+
}
|