@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.
- package/dist/index.js +230 -166
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +230 -166
- package/dist/index.mjs.map +1 -1
- package/dist/src/ProcessorWrapper.d.ts +1 -1
- package/dist/src/transformers/BackgroundTransformer.d.ts +3 -1
- package/dist/src/transformers/VideoTransformer.d.ts +1 -1
- package/dist/src/transformers/types.d.ts +1 -1
- package/dist/src/utils.d.ts +2 -2
- package/dist/src/webgl/index.d.ts +3 -3
- package/dist/src/webgl/shader-programs/downSampler.d.ts +12 -0
- package/dist/src/webgl/utils.d.ts +4 -2
- package/package.json +1 -1
- package/src/ProcessorWrapper.ts +4 -4
- package/src/transformers/BackgroundTransformer.ts +42 -28
- package/src/transformers/VideoTransformer.ts +4 -3
- package/src/transformers/types.ts +1 -1
- package/src/utils.ts +11 -2
- package/src/webgl/index.ts +103 -48
- package/src/webgl/shader-programs/blurShader.ts +5 -37
- package/src/webgl/shader-programs/boxBlurShader.ts +4 -42
- package/src/webgl/shader-programs/compositeShader.ts +5 -43
- package/src/webgl/shader-programs/downSampler.ts +94 -0
- package/src/webgl/utils.ts +46 -6
|
@@ -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 ||
|
|
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 ||
|
|
46
|
+
this.canvas || createCanvas(inputVideo.videoWidth, inputVideo.videoHeight),
|
|
46
47
|
);
|
|
47
48
|
|
|
48
49
|
this.inputVideo = inputVideo;
|
package/src/utils.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/naming-convention */
|
|
2
|
-
export const
|
|
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
|
+
}
|
package/src/webgl/index.ts
CHANGED
|
@@ -1,22 +1,30 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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
|
|
61
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
createFramebuffer(gl,
|
|
84
|
+
// Create framebuffers for background processing
|
|
85
|
+
bgBlurFrameBuffers.push(
|
|
86
|
+
createFramebuffer(gl, bgBlurTextures[0], bgBlurTextureWidth, bgBlurTextureHeight),
|
|
78
87
|
);
|
|
79
|
-
|
|
80
|
-
createFramebuffer(gl,
|
|
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 =
|
|
113
|
+
let customBackgroundImage: ImageBitmap | ImageData = getEmptyImageData();
|
|
91
114
|
|
|
92
|
-
function
|
|
93
|
-
if (frame.codedWidth === 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
|
-
|
|
132
|
+
const downSampledFrameTexture = applyDownsampling(
|
|
110
133
|
gl,
|
|
111
134
|
frameTexture,
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
179
|
+
// Set mask texture - always read from the current read index
|
|
163
180
|
gl.activeTexture(gl.TEXTURE2);
|
|
164
|
-
gl.bindTexture(gl.TEXTURE_2D,
|
|
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 =
|
|
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
|
|
262
|
+
for (const texture of finalMaskTextures) {
|
|
217
263
|
gl.deleteTexture(texture);
|
|
218
264
|
}
|
|
219
|
-
for (const framebuffer of
|
|
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 =
|
|
285
|
+
customBackgroundImage = getEmptyImageData();
|
|
230
286
|
}
|
|
231
287
|
bgBlurTextures = [];
|
|
232
288
|
bgBlurFrameBuffers = [];
|
|
233
|
-
|
|
234
|
-
maskBlurFrameBuffers = [];
|
|
289
|
+
finalMaskTextures = [];
|
|
235
290
|
}
|
|
236
291
|
|
|
237
|
-
return {
|
|
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
|
|
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
|
-
|
|
37
|
-
const blurFrag =
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
41
|
-
const
|
|
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
|
-
|
|
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
|
+
}
|
package/src/webgl/utils.ts
CHANGED
|
@@ -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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
emptyImageData
|
|
106
|
-
emptyImageData
|
|
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 {
|
|
150
|
+
export { getEmptyImageData, glsl };
|