-
Notifications
You must be signed in to change notification settings - Fork 538
/
Copy pathmain.js
462 lines (384 loc) · 15.5 KB
/
main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
/*
The code of this demonstration may seems complicated but it is not
If you want to play only with the 2D canvas displayed above the head,
Its context is CTX and it matches the canvas element CANVAS2D
If you draw something on the 2D canvas using CTX.drawStuff(...)
You should call update_canvasTexture(); after in order to update the 3D canvas
at the next rendering.
*/
// SETTINGS of this demo:
const SETTINGS = {
strokeStyle: 'red',
rotationOffsetX: 0, // negative -> look upper. in radians
cameraFOV: 40, // in degrees, 3D camera FOV
pivotOffsetYZ: [0.2,0.2], // XYZ of the distance between the center of the cube and the pivot
detectionThreshold: 0.75, // sensibility, between 0 and 1. Less -> more sensitive
detectionHysteresis: 0.05,
scale: [1,1.24], // scale of the 2D canvas along horizontal and vertical 2D axis
offsetYZ: [-0.1,-0.2], // offset of the 2D canvas along vertical and depth 3D axis
canvasSizePx: 512 // resolution of the 2D canvas in pixels
};
// some globalz:
let CV = null, CANVAS2D = null, CTX = null, GL = null, CANVASTEXTURE = null, CANVASTEXTURENEEDSUPDATE = false;
let PROJMATRIX = null, PROJMATRIXNEEDSUPDATE = true;
let VBO_VERTEX = null, VBO_FACES = null, SHADERCANVAS = null;
let SHADERVIDEO = null, VIDEOTEXTURE = null, VIDEOTRANSFORMMAT2 = null;
let MOVMATRIX = create_mat4Identity(), MOVMATRIXINV = create_mat4Identity();
let ZPLANE = 0, YPLANE = 0;
let ISDETECTED = false;
// callback: launched if a face is detected or lost.
function detect_callback(isDetected){
if (isDetected){
console.log('INFO in detect_callback(): DETECTED');
} else {
console.log('INFO in detect_callback(): LOST');
}
}
//BEGIN MATRIX ALGEBRA FUNCTIONS
function create_mat4Identity(){
return [
1,0,0,0,
0,1,0,0,
0,0,1,0,
0,0,0,1
];
}
// set the position part of a flattened transposed mat4:
function set_mat4Position(m, x,y,z){
m[12] = x;
m[13] = y;
m[14] = z;
}
// set the rotation part of a flattened transposed mat4 - see https://en.wikipedia.org/wiki/Euler_angles
function set_mat4RotationXYZ(m, rx,ry,rz){
const c1=Math.cos(rx), s1=Math.sin(rx),
c2=Math.cos(ry), s2=Math.sin(ry),
c3=Math.cos(rz), s3=Math.sin(rz);
// first line (not transposed)
m[0] = c2*c3;
m[4] = -c2*s3;
m[8] = s2;
// second line (not transposed)
m[1] = c1*s3+c3*s1*s2;
m[5] = c1*c3-s1*s2*s3;
m[9] = -c2*s1;
// third line (not transposed)
m[2] = s1*s3-c1*c3*s2;
m[6] = c3*s1+c1*s2*s3;
m[10] = c1*c2;
}
// inverse a mat4 move matrix m and put result to mat4 matrix r
function inverse_mat4MoveMatrix(m, r){
// rotation part: the inverse = the transpose
r[0] = m[0];
r[1] = m[4];
r[2] = m[8];
r[4] = m[1];
r[5] = m[5];
r[6] = m[9];
r[8] = m[2];
r[9] = m[6];
r[10] = m[10];
// translation part: = -tR.T where T=[m[12], m[13], m[14]]
r[12] = -(m[0]*m[12]+m[1]*m[13]+m[2]*m[14]);
r[13] = -(m[4]*m[12]+m[5]*m[13]+m[6]*m[14]);
r[14] = -(m[8]*m[12]+m[9]*m[13]+m[10]*m[14]);
}
function multiply_matVec4(m,v){
return [
m[0]*v[0]+m[4]*v[1]+ m[8]*v[2]+m[12]*v[3],
m[1]*v[0]+m[5]*v[1]+ m[9]*v[2]+m[13]*v[3],
m[2]*v[0]+m[6]*v[1]+m[10]*v[2]+m[14]*v[3],
m[3]*v[0]+m[7]*v[1]+m[11]*v[2]+m[15]*v[3]
];
}
function get_mat4Pos(m){
return [m[12], m[13], m[14]]
}
//END MATRIX ALGEBRA FUNCTIONS
//BEGIN WEBGL HELPERS
// compile a shader:
function compile_shader(source, glType, typeString) {
const glShader = GL.createShader(glType);
GL.shaderSource(glShader, source);
GL.compileShader(glShader);
if (!GL.getShaderParameter(glShader, GL.COMPILE_STATUS)) {
alert("ERROR IN " + typeString + " SHADER: " + GL.getShaderInfoLog(glShader));
console.log('Buggy shader source: \n', source);
return null;
}
return glShader;
};
// helper function to build the shader program:
function build_shaderProgram(shaderVertexSource, shaderFragmentSource, id) {
// compile both shader separately:
const glShaderVertex = compile_shader(shaderVertexSource, GL.VERTEX_SHADER, "VERTEX "+id);
const glShaderFragment = compile_shader(shaderFragmentSource, GL.FRAGMENT_SHADER, "FRAGMENT "+id);
const glShaderProgram = GL.createProgram();
GL.attachShader(glShaderProgram, glShaderVertex);
GL.attachShader(glShaderProgram, glShaderFragment);
// start the linking stage:
GL.linkProgram(glShaderProgram);
return glShaderProgram;
}
// helper function to create the projection matrix:
function update_projMatrix() {
const tan = Math.tan(0.5*SETTINGS.cameraFOV*Math.PI/180),
zMax=100, zMin=0.1, a=CV.width/CV.height;
const A = -(zMax+zMin) / (zMax-zMin),
B = (-2*zMax*zMin) / (zMax-zMin);
PROJMATRIX = [
1.0/tan, 0 , 0, 0,
0, a/tan, 0, 0,
0, 0, A, -1,
0, 0, B, 0
];
GL.uniformMatrix4fv(SHADERCANVAS.projMatrix, false, PROJMATRIX);
PROJMATRIXNEEDSUPDATE = false;
}
//END WEBGL HELPERS
//build the 3D. called once when Jeeliz Face Filter is OK
function init_scene(spec){
// affect some globalz:
GL = spec.GL;
CV = spec.canvasElement;
VIDEOTEXTURE = spec.videoTexture;
VIDEOTRANSFORMMAT2 = spec.videoTransformMat2;
// create and size the 2D canvas and its drawing context:
CANVAS2D = document.createElement('canvas');
CANVAS2D.width = SETTINGS.canvasSizePx;
CANVAS2D.height = Math.round(SETTINGS.canvasSizePx*SETTINGS.scale[1]/SETTINGS.scale[0]);
CTX = CANVAS2D.getContext('2d');
CTX.strokeStyle = SETTINGS.strokeStyle;
CTX.lineWidth = 4;
const frameImage = new Image()
frameImage.src = 'frame.png';
frameImage.onload = function(){
CTX.drawImage(frameImage, 0, 0, frameImage.width, frameImage.height, 0, 0, CANVAS2D.width, CANVAS2D.height);
update_canvasTexture();
}
// create the WebGL texture with the canvas:
CANVASTEXTURE = GL.createTexture();
GL.bindTexture(GL.TEXTURE_2D, CANVASTEXTURE);
GL.texImage2D(GL.TEXTURE_2D, 0, GL.RGBA, GL.RGBA, GL.UNSIGNED_BYTE, CANVAS2D);
GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_MAG_FILTER, GL.LINEAR);
GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_MIN_FILTER, GL.LINEAR);
GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_WRAP_S, GL.CLAMP_TO_EDGE);
GL.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_WRAP_T, GL.CLAMP_TO_EDGE);
// create the face plane:
const sx = SETTINGS.scale[0], sy = SETTINGS.scale[1]; // scale
YPLANE = SETTINGS.offsetYZ[0] + SETTINGS.pivotOffsetYZ[0]; // offset
ZPLANE = SETTINGS.offsetYZ[1] + SETTINGS.pivotOffsetYZ[1];
VBO_VERTEX = GL.createBuffer ();
GL.bindBuffer(GL.ARRAY_BUFFER, VBO_VERTEX);
GL.bufferData(GL.ARRAY_BUFFER,
new Float32Array([ //format of each vertex: x,y,z, u,v
-sx, -sy+YPLANE, ZPLANE, 1,1,
sx, -sy+YPLANE, ZPLANE, 0,1,
sx, sy+YPLANE, ZPLANE, 0,0,
-sx, sy+YPLANE, ZPLANE, 1,0
]), GL.STATIC_DRAW);
// FACES:
VBO_FACES = GL.createBuffer ();
GL.bindBuffer(GL.ELEMENT_ARRAY_BUFFER, VBO_FACES);
GL.bufferData(GL.ELEMENT_ARRAY_BUFFER,
new Uint16Array([
0,1,2, 0,2,3
]), GL.STATIC_DRAW);
// create the shaders:
const copyCropVertexShaderSource = "attribute vec2 position;\n\
uniform mat2 videoTransformMat2;\n\
varying vec2 vUV;\n\
void main(void){\n\
gl_Position = vec4(position, 0., 1.);\n\
vUV = vec2(0.5,0.5) + videoTransformMat2 * position;\n\
}";
const copyFragmentShaderSource = "precision lowp float;\n\
uniform sampler2D samplerImage;\n\
varying vec2 vUV;\n\
\n\
void main(void){\n\
gl_FragColor = texture2D(samplerImage, vUV);\n\
}";
const shpVideo = build_shaderProgram(copyCropVertexShaderSource, copyFragmentShaderSource, 'VIDEO');
SHADERVIDEO = {
program: shpVideo,
videoTransformMat2: GL.getUniformLocation(shpVideo, 'videoTransformMat2')
};
let uSampler = GL.getUniformLocation(shpVideo, 'samplerImage');
GL.useProgram(shpVideo);
GL.uniform1i(uSampler, 0);
const shpCanvas = build_shaderProgram( //basic 3D projection shader
"attribute vec3 position;\n\
attribute vec2 uv;\n\
uniform mat4 projMatrix, movMatrix;\n\
varying vec2 vUV;\n\
void main(void){\n\
gl_Position = projMatrix*movMatrix*vec4(position, 1.);\n\
vUV = uv;\n\
}",
copyFragmentShaderSource, 'CANVAS');
SHADERCANVAS = {
program: shpCanvas,
projMatrix: GL.getUniformLocation(shpCanvas, 'projMatrix'),
movMatrix: GL.getUniformLocation(shpCanvas, 'movMatrix'),
position: GL.getAttribLocation(shpCanvas, 'position'),
uv: GL.getAttribLocation(shpCanvas, 'uv')
};
uSampler = GL.getUniformLocation(shpCanvas, 'samplerImage');
GL.useProgram(shpCanvas);
GL.uniform1i(uSampler, 0);
GL.disableVertexAttribArray(shpCanvas, SHADERCANVAS.position);
GL.disableVertexAttribArray(shpCanvas, SHADERCANVAS.uv);
} //end init_scene()
//BEGIN MOUSE/TOUCH EVENTS FUNCTIONS
const MOUSESTATES = {
idle: 0,
drag: 1
};
let MOUSESTATE = MOUSESTATES.idle, OLDXY;
function init_eventListeners(){
// add touch and mouse event listeners:
CV.addEventListener('mousedown', onMouseDown, false);
CV.addEventListener('touchdown', onMouseDown, false);
CV.addEventListener('touchstart', onMouseDown, false); // for IOS
CV.addEventListener('mouseup', onMouseUp, false);
CV.addEventListener('touchup', onMouseUp, false);
CV.addEventListener('mousemove', onMouseMove, false);
CV.addEventListener('touchmove', onMouseMove, false);
}
function get_eventLoc(event){ // return the position of the picked point in pixels in the canvas2D
// get cursor position in pixel in the HTML page ref:
const isTouch = (event.touches && event.touches.length) ? true : false;
const xPx = (isTouch) ? event.touches[0].clientX : event.clientX;
const yPx = (isTouch) ? event.touches[0].clientY : event.clientY;
// convert it to viewport coordinates (between -1 and 1, Y up):
const canvasRect = CV.getBoundingClientRect();
let xvp = (xPx-canvasRect.left) / canvasRect.width; // between 0 and 1
let yvp = (yPx-canvasRect.top) / canvasRect.height; // between 0 and 1
xvp = -2*xvp+1; // inverse X too because it is inverted with CSS
yvp = -2*yvp+1; // between 0 and 1, inverted
// multiply if by the inverse of the projection matrix to get the vector in 3D in the camera ref:
const xc = (1/PROJMATRIX[0])*xvp;
const yc = (1/PROJMATRIX[5])*yvp;
const vc = [xc,yc,-1,0];
// compute the inverse of the move matrix to pass this vector to the plane ref:
inverse_mat4MoveMatrix(MOVMATRIX, MOVMATRIXINV);
const vec = multiply_matVec4(MOVMATRIXINV, vc); //vector
const origin = get_mat4Pos(MOVMATRIXINV);
// compute the intersection between the ray (origin, vec) and the plane defined by Z=ZPLANE:
const k = (ZPLANE-origin[2]) / vec[2];// k so that origin+k*vec belongs to the plane
const xi=origin[0]+k*vec[0], // x of the intersection point
yi=origin[1]+k*vec[1] // y of the intersection point
// normalize xi and yi between [-1 and 1] according to the size of the canvas in the space:
const xin = xi/SETTINGS.scale[0];
const yin = (yi-YPLANE)/SETTINGS.scale[1];
// convert xin and yin to canvas pixel coordinates and re-invert Y axis:
const xCv = CANVAS2D.width*(-xin+1)/2;
const yCv = CANVAS2D.height*(-yin+1)/2;
return [xCv, yCv];
} //end get_eventLoc()
function onMouseDown(event){
if (MOUSESTATE!==MOUSESTATES.idle || !ISDETECTED) return;
MOUSESTATE = MOUSESTATES.drag;
OLDXY = get_eventLoc(event);
CTX.beginPath();
CTX.moveTo(OLDXY[0], OLDXY[1]);
}
function onMouseMove(event){
if (MOUSESTATE!==MOUSESTATES.drag) return;
if (!ISDETECTED){
onMouseUp(event);
return;
}
const xy = get_eventLoc(event);
CTX.lineTo(xy[0], xy[1]);
CTX.stroke();
update_canvasTexture();
OLDXY = xy;
event.preventDefault(); // disable scroll or fancy stuffs
}
function onMouseUp(event){
if (MOUSESTATE!==MOUSESTATES.drag) return;
MOUSESTATE = MOUSESTATES.idle;
}
//END MOUSE/TOUCH EVENTS FUNCTIONS
function update_canvasTexture(){
CANVASTEXTURENEEDSUPDATE = true;
}
// entry point - launched by body.onload():
function main(){
JEELIZFACEFILTER.init({
canvasId: 'jeeFaceFilterCanvas',
NNCPath: '../../../neuralNets/', // root of NN_DEFAULT.json file
callbackReady: function(errCode, spec){
if (errCode){
console.log('AN ERROR HAPPENS. SORRY BRO :( . ERR =', errCode);
return;
}
console.log('INFO: JEELIZFACEFILTER IS READY');
init_scene(spec);
init_eventListeners();
}, //end callbackReady()
// called at each render iteration (drawing loop):
callbackTrack: function(detectState){
if (ISDETECTED && detectState.detected<SETTINGS.detectionThreshold-SETTINGS.detectionHysteresis){
// DETECTION LOST
detect_callback(false);
ISDETECTED = false;
} else if (!ISDETECTED && detectState.detected>SETTINGS.detectionThreshold+SETTINGS.detectionHysteresis){
// FACE DETECTED
detect_callback(true);
ISDETECTED = true;
}
// render the video screen:
GL.viewport(0,0,CV.width, CV.height);
GL.useProgram(SHADERVIDEO.program);
GL.uniformMatrix2fv(SHADERVIDEO.videoTransformMat2, false, VIDEOTRANSFORMMAT2);
GL.bindTexture(GL.TEXTURE_2D, VIDEOTEXTURE);
GL.drawElements(GL.TRIANGLES, 3, GL.UNSIGNED_SHORT, 0);
if (ISDETECTED){
const aspect = CV.width / CV.height;
// move the cube in order to fit the head:
const tanFOV = Math.tan(aspect*SETTINGS.cameraFOV*Math.PI/360); // tan(FOV/2), in radians
const W = detectState.s; // relative width of the detection window (1-> whole width of the detection window)
const D = 1 / (2*W*tanFOV); // distance between the front face of the cube and the camera
// coords in 2D of the center of the detection window in the viewport:
const xv = detectState.x;
const yv = detectState.y;
// coords in 3D of the center of the cube (in the view coordinates system):
const z = -D-0.5; // minus because view coordinate system Z goes backward. -0.5 because z is the coord of the center of the cube (not the front face)
const x = xv * D * tanFOV;
const y = yv * D * tanFOV / aspect;
// move and rotate the cube:
set_mat4Position(MOVMATRIX, x,y+SETTINGS.pivotOffsetYZ[0],z+SETTINGS.pivotOffsetYZ[1]);
set_mat4RotationXYZ(MOVMATRIX, detectState.rx+SETTINGS.rotationOffsetX, detectState.ry, detectState.rz);
// render the canvas above:
GL.clear(GL.DEPTH_BUFFER_BIT);
GL.enable(GL.BLEND);
GL.blendFunc(GL.SRC_ALPHA, GL.ONE_MINUS_SRC_ALPHA);
GL.useProgram(SHADERCANVAS.program);
GL.enableVertexAttribArray(SHADERCANVAS.position);
GL.enableVertexAttribArray(SHADERCANVAS.uv);
GL.uniformMatrix4fv(SHADERCANVAS.movMatrix, false, MOVMATRIX);
if (PROJMATRIXNEEDSUPDATE){
update_projMatrix();
}
GL.bindTexture(GL.TEXTURE_2D, CANVASTEXTURE);
if (CANVASTEXTURENEEDSUPDATE){
GL.texImage2D(GL.TEXTURE_2D, 0, GL.RGBA, GL.RGBA, GL.UNSIGNED_BYTE, CANVAS2D);
CANVASTEXTURENEEDSUPDATE = false;
}
GL.bindBuffer(GL.ARRAY_BUFFER, VBO_VERTEX);
GL.vertexAttribPointer(SHADERCANVAS.position, 3, GL.FLOAT, false,20,0);
GL.vertexAttribPointer(SHADERCANVAS.uv, 2, GL.FLOAT, false,20,12);
GL.bindBuffer(GL.ELEMENT_ARRAY_BUFFER, VBO_FACES);
GL.drawElements(GL.TRIANGLES, 6, GL.UNSIGNED_SHORT, 0);
GL.disableVertexAttribArray(SHADERCANVAS.uv);
GL.disableVertexAttribArray(SHADERCANVAS.position);
GL.disable(GL.BLEND);
} //end if face detected
} //end callbackTrack()
}); //end JEELIZFACEFILTER.init call
}
window.addEventListener('load', main);