| import { clamp01 } from './utils'; |
| import { clipBounds, clipBoundsToPixelRect, type ClipBounds } from './geometry'; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
|
|
| export const BlendMode = { |
| HSLColor: 0, |
| Multiply: 1, |
| SoftLight: 2, |
| Screen: 3, |
| } as const; |
| export type BlendModeId = (typeof BlendMode)[keyof typeof BlendMode]; |
|
|
| export type MakeupRegionOpts = { |
| |
| add: Array<Float32Array | undefined>; |
| |
| subtract?: Array<Float32Array | undefined>; |
| |
| blurStrength: number; |
| |
| topRGB: [number, number, number]; |
| |
| bottomRGB: [number, number, number]; |
| |
| intensity: number; |
| blendMode: BlendModeId; |
| |
| multiplyMix?: number; |
| |
| |
| |
| |
| |
| lumaLift?: number; |
| |
| specGuard?: readonly [number, number]; |
| }; |
|
|
| export interface MakeupRenderer { |
| |
| readonly canvas: HTMLCanvasElement; |
| |
| syncMaskFbosToCanvas(): void; |
| |
| uploadVideoFrame(video: HTMLVideoElement): void; |
| |
| drawVideoQuad(): void; |
| |
| drawMakeupRegion(opts: MakeupRegionOpts): void; |
| } |
|
|
| |
| |
| |
|
|
| const quadVS = `#version 300 es |
| in vec2 a_pos; |
| out vec2 v_uv; |
| void main() { |
| v_uv = (a_pos + 1.0) * 0.5; |
| gl_Position = vec4(a_pos, 0.0, 1.0); |
| }`; |
|
|
| const quadFS = `#version 300 es |
| precision mediump float; |
| uniform sampler2D u_tex; |
| in vec2 v_uv; |
| out vec4 outColor; |
| void main() { |
| // Mirror horizontally (more natural for users + matches typical filters). |
| vec2 uv = vec2(1.0 - v_uv.x, v_uv.y); |
| outColor = texture(u_tex, uv); |
| }`; |
|
|
| const maskVS = `#version 300 es |
| in vec2 a_pos; |
| void main() { |
| gl_Position = vec4(a_pos, 0.0, 1.0); |
| }`; |
|
|
| const maskFS = `#version 300 es |
| precision mediump float; |
| uniform float u_value; |
| out vec4 outColor; |
| void main() { |
| outColor = vec4(u_value); |
| }`; |
|
|
| const blurVS = `#version 300 es |
| in vec2 a_pos; |
| out vec2 v_uv; |
| void main() { |
| v_uv = (a_pos + 1.0) * 0.5; |
| gl_Position = vec4(a_pos, 0.0, 1.0); |
| }`; |
|
|
| |
| |
| const blurFS = `#version 300 es |
| precision mediump float; |
| uniform sampler2D u_tex; |
| uniform vec2 u_offset; |
| in vec2 v_uv; |
| out vec4 outColor; |
| const float w0 = 0.2270270270; |
| const float w1 = 0.1945945946; |
| const float w2 = 0.1216216216; |
| const float w3 = 0.0540540541; |
| const float w4 = 0.0162162162; |
| void main() { |
| vec4 c = texture(u_tex, v_uv) * w0; |
| c += texture(u_tex, v_uv + u_offset * 1.0) * w1; |
| c += texture(u_tex, v_uv - u_offset * 1.0) * w1; |
| c += texture(u_tex, v_uv + u_offset * 2.0) * w2; |
| c += texture(u_tex, v_uv - u_offset * 2.0) * w2; |
| c += texture(u_tex, v_uv + u_offset * 3.0) * w3; |
| c += texture(u_tex, v_uv - u_offset * 3.0) * w3; |
| c += texture(u_tex, v_uv + u_offset * 4.0) * w4; |
| c += texture(u_tex, v_uv - u_offset * 4.0) * w4; |
| outColor = c; |
| }`; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const makeupCompositeVS = `#version 300 es |
| in vec2 a_pos; |
| out vec2 v_uv; |
| void main() { |
| v_uv = (a_pos + 1.0) * 0.5; |
| gl_Position = vec4(a_pos, 0.0, 1.0); |
| }`; |
|
|
| const makeupCompositeFS = `#version 300 es |
| precision mediump float; |
| uniform sampler2D u_video; |
| uniform sampler2D u_mask; |
| uniform vec3 u_topRGB; |
| uniform vec3 u_bottomRGB; |
| uniform vec2 u_yRange; // (yMin, yMax) in v_uv space; top of region = yMax |
| uniform float u_intensity; // 0..1 (slider * region cap) |
| uniform float u_multiplyMix; // mode 0 only: 0 = pure HSL color, 1 = pure multiply |
| uniform float u_lumaLift; // mode 0 only: 0 = keep skin luminance, 1 = pull L to 0.55 midpoint |
| uniform vec2 u_specGuard; // smoothstep(start, end, luma) -> 0 = preserve highlight |
| uniform int u_blendMode; |
| in vec2 v_uv; |
| out vec4 outColor; |
| |
| vec3 rgb2hsl(vec3 c) { |
| float maxc = max(max(c.r, c.g), c.b); |
| float minc = min(min(c.r, c.g), c.b); |
| float l = (maxc + minc) * 0.5; |
| float h = 0.0; |
| float s = 0.0; |
| float d = maxc - minc; |
| if (d > 1e-6) { |
| s = (l > 0.5) ? d / (2.0 - maxc - minc) : d / (maxc + minc); |
| if (maxc == c.r) h = (c.g - c.b) / d + (c.g < c.b ? 6.0 : 0.0); |
| else if (maxc == c.g) h = (c.b - c.r) / d + 2.0; |
| else h = (c.r - c.g) / d + 4.0; |
| h /= 6.0; |
| } |
| return vec3(h, s, l); |
| } |
| |
| float hue2rgb(float p, float q, float t) { |
| if (t < 0.0) t += 1.0; |
| if (t > 1.0) t -= 1.0; |
| if (t < 1.0/6.0) return p + (q - p) * 6.0 * t; |
| if (t < 0.5) return q; |
| if (t < 2.0/3.0) return p + (q - p) * (2.0/3.0 - t) * 6.0; |
| return p; |
| } |
| |
| vec3 hsl2rgb(vec3 hsl) { |
| float h = hsl.x, s = hsl.y, l = hsl.z; |
| if (s < 1e-6) return vec3(l); |
| float q = (l < 0.5) ? l * (1.0 + s) : l + s - l * s; |
| float p = 2.0 * l - q; |
| return vec3(hue2rgb(p, q, h + 1.0/3.0), hue2rgb(p, q, h), hue2rgb(p, q, h - 1.0/3.0)); |
| } |
| |
| vec3 blendSoftLight(vec3 b, vec3 s) { |
| // Photoshop soft-light: branch per-component on whether the overlay is darker or lighter than 50%. |
| // sqrt branch lifts highlights, b*b branch deepens shadows — preserves underlying texture either way. |
| return mix( |
| 2.0 * b * s + b * b * (1.0 - 2.0 * s), |
| sqrt(b) * (2.0 * s - 1.0) + 2.0 * b * (1.0 - s), |
| step(0.5, s) |
| ); |
| } |
| |
| void main() { |
| float m = texture(u_mask, v_uv).r; |
| if (m < 0.001) { outColor = vec4(0.0); return; } |
| |
| // Match the horizontal mirror that quadFS does so 'skin' is the same pixel the user sees. |
| vec2 videoUV = vec2(1.0 - v_uv.x, v_uv.y); |
| vec3 skin = texture(u_video, videoUV).rgb; |
| |
| // Top -> bottom color gradient. yMax = top of screen. |
| float t = clamp((u_yRange.y - v_uv.y) / max(u_yRange.y - u_yRange.x, 1e-5), 0.0, 1.0); |
| vec3 color = mix(u_topRGB, u_bottomRGB, t); |
| |
| vec3 makeup; |
| if (u_blendMode == 0) { |
| // HSL color blend with optional luma lift. Pulling the transferred L toward a bright midpoint |
| // (0.55) stops dark lips/eyelids from dragging vivid pinks/reds into a grey-brown muddle. |
| float skinL = rgb2hsl(skin).z; |
| float liftedL = mix(skinL, 0.55, u_lumaLift); |
| vec3 hslColor = hsl2rgb(vec3(rgb2hsl(color).xy, liftedL)); |
| vec3 mult = skin * color; |
| makeup = mix(hslColor, mult, u_multiplyMix); |
| } else if (u_blendMode == 1) { |
| makeup = skin * color; |
| } else if (u_blendMode == 2) { |
| makeup = blendSoftLight(skin, color); |
| } else if (u_blendMode == 3) { |
| makeup = vec3(1.0) - (vec3(1.0) - skin) * (vec3(1.0) - color); |
| } else { |
| makeup = color; |
| } |
| |
| // Specular guard: dim makeup where the underlying skin is glossy/bright so highlights survive. |
| float luma = dot(skin, vec3(0.299, 0.587, 0.114)); |
| float specGuard = 1.0 - smoothstep(u_specGuard.x, u_specGuard.y, luma); |
| |
| float a = clamp(m * u_intensity * specGuard, 0.0, 1.0); |
| outColor = vec4(makeup, a); |
| }`; |
|
|
| |
| |
| |
|
|
| type Fbo = { fbo: WebGLFramebuffer; tex: WebGLTexture; w: number; h: number }; |
|
|
| export function createMakeupRenderer(canvas: HTMLCanvasElement): MakeupRenderer { |
| |
| const gl = canvas.getContext('webgl2', { alpha: false, antialias: true, stencil: true }); |
| if (!gl) throw new Error('WebGL2 not available'); |
|
|
| |
| gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); |
|
|
| |
| |
| |
|
|
| function compileShader(type: number, src: string): WebGLShader { |
| const s = gl!.createShader(type); |
| if (!s) throw new Error('createShader failed'); |
| gl!.shaderSource(s, src); |
| gl!.compileShader(s); |
| if (!gl!.getShaderParameter(s, gl!.COMPILE_STATUS)) { |
| throw new Error(gl!.getShaderInfoLog(s) || 'shader compile failed'); |
| } |
| return s; |
| } |
|
|
| function createProgram(vsSrc: string, fsSrc: string): WebGLProgram { |
| const vs = compileShader(gl!.VERTEX_SHADER, vsSrc); |
| const fs = compileShader(gl!.FRAGMENT_SHADER, fsSrc); |
| const p = gl!.createProgram(); |
| if (!p) throw new Error('createProgram failed'); |
| gl!.attachShader(p, vs); |
| gl!.attachShader(p, fs); |
| gl!.linkProgram(p); |
| if (!gl!.getProgramParameter(p, gl!.LINK_STATUS)) { |
| throw new Error(gl!.getProgramInfoLog(p) || 'program link failed'); |
| } |
| gl!.deleteShader(vs); |
| gl!.deleteShader(fs); |
| return p; |
| } |
|
|
| |
| |
| |
|
|
| const quadProgram = createProgram(quadVS, quadFS); |
| const quadVAO = gl.createVertexArray()!; |
| gl.bindVertexArray(quadVAO); |
| const quadBuf = gl.createBuffer()!; |
| gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf); |
| gl.bufferData( |
| gl.ARRAY_BUFFER, |
| new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), |
| gl.STATIC_DRAW, |
| ); |
| const quadPosLoc = gl.getAttribLocation(quadProgram, 'a_pos'); |
| gl.enableVertexAttribArray(quadPosLoc); |
| gl.vertexAttribPointer(quadPosLoc, 2, gl.FLOAT, false, 0, 0); |
| gl.bindVertexArray(null); |
|
|
| const videoTex = gl.createTexture()!; |
| gl.bindTexture(gl.TEXTURE_2D, videoTex); |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_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); |
| gl.bindTexture(gl.TEXTURE_2D, null); |
|
|
| |
| |
| |
| const regionVAO = gl.createVertexArray()!; |
| const regionBuf = gl.createBuffer()!; |
|
|
| |
| |
| |
|
|
| function createFBO(width: number, height: number): Fbo { |
| const tex = gl!.createTexture()!; |
| gl!.bindTexture(gl!.TEXTURE_2D, tex); |
| gl!.texImage2D(gl!.TEXTURE_2D, 0, gl!.RGBA8, width, height, 0, gl!.RGBA, gl!.UNSIGNED_BYTE, null); |
| gl!.texParameteri(gl!.TEXTURE_2D, gl!.TEXTURE_MIN_FILTER, gl!.LINEAR); |
| gl!.texParameteri(gl!.TEXTURE_2D, gl!.TEXTURE_MAG_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); |
| const fbo = gl!.createFramebuffer()!; |
| gl!.bindFramebuffer(gl!.FRAMEBUFFER, fbo); |
| gl!.framebufferTexture2D(gl!.FRAMEBUFFER, gl!.COLOR_ATTACHMENT0, gl!.TEXTURE_2D, tex, 0); |
| if (gl!.checkFramebufferStatus(gl!.FRAMEBUFFER) !== gl!.FRAMEBUFFER_COMPLETE) { |
| throw new Error('FBO incomplete'); |
| } |
| gl!.bindTexture(gl!.TEXTURE_2D, null); |
| gl!.bindFramebuffer(gl!.FRAMEBUFFER, null); |
| return { fbo, tex, w: width, h: height }; |
| } |
|
|
| function resizeFBO(f: Fbo, w: number, h: number) { |
| if (f.w === w && f.h === h) return; |
| gl!.bindTexture(gl!.TEXTURE_2D, f.tex); |
| gl!.texImage2D(gl!.TEXTURE_2D, 0, gl!.RGBA8, w, h, 0, gl!.RGBA, gl!.UNSIGNED_BYTE, null); |
| gl!.bindTexture(gl!.TEXTURE_2D, null); |
| f.w = w; |
| f.h = h; |
| } |
|
|
| |
| function maskTargetSize() { |
| return { |
| w: Math.max(2, Math.floor(canvas.width / 2)), |
| h: Math.max(2, Math.floor(canvas.height / 2)), |
| }; |
| } |
|
|
| const initSize = maskTargetSize(); |
| const maskFbo = createFBO(initSize.w, initSize.h); |
| const blurPingFbo = createFBO(initSize.w, initSize.h); |
| const blurPongFbo = createFBO(initSize.w, initSize.h); |
|
|
| |
| |
| |
|
|
| const maskProgram = createProgram(maskVS, maskFS); |
| const maskPosLoc = gl.getAttribLocation(maskProgram, 'a_pos'); |
| const maskValueLoc = gl.getUniformLocation(maskProgram, 'u_value'); |
|
|
| const blurProgram = createProgram(blurVS, blurFS); |
| const blurPosLoc = gl.getAttribLocation(blurProgram, 'a_pos'); |
| const blurTexLoc = gl.getUniformLocation(blurProgram, 'u_tex'); |
| const blurOffsetLoc = gl.getUniformLocation(blurProgram, 'u_offset'); |
|
|
| const makeupProgram = createProgram(makeupCompositeVS, makeupCompositeFS); |
| const makeupPosLoc = gl.getAttribLocation(makeupProgram, 'a_pos'); |
| const makeupVideoLoc = gl.getUniformLocation(makeupProgram, 'u_video'); |
| const makeupMaskLoc = gl.getUniformLocation(makeupProgram, 'u_mask'); |
| const makeupTopLoc = gl.getUniformLocation(makeupProgram, 'u_topRGB'); |
| const makeupBottomLoc = gl.getUniformLocation(makeupProgram, 'u_bottomRGB'); |
| const makeupYRangeLoc = gl.getUniformLocation(makeupProgram, 'u_yRange'); |
| const makeupIntensityLoc = gl.getUniformLocation(makeupProgram, 'u_intensity'); |
| const makeupMultiplyMixLoc = gl.getUniformLocation(makeupProgram, 'u_multiplyMix'); |
| const makeupLumaLiftLoc = gl.getUniformLocation(makeupProgram, 'u_lumaLift'); |
| const makeupSpecGuardLoc = gl.getUniformLocation(makeupProgram, 'u_specGuard'); |
| const makeupBlendModeLoc = gl.getUniformLocation(makeupProgram, 'u_blendMode'); |
|
|
| |
| |
| |
|
|
| function syncMaskFbosToCanvas() { |
| const { w, h } = maskTargetSize(); |
| resizeFBO(maskFbo, w, h); |
| resizeFBO(blurPingFbo, w, h); |
| resizeFBO(blurPongFbo, w, h); |
| } |
|
|
| function uploadVideoFrame(video: HTMLVideoElement) { |
| gl!.bindTexture(gl!.TEXTURE_2D, videoTex); |
| gl!.texImage2D(gl!.TEXTURE_2D, 0, gl!.RGB, gl!.RGB, gl!.UNSIGNED_BYTE, video); |
| } |
|
|
| function drawVideoQuad() { |
| gl!.viewport(0, 0, canvas.width, canvas.height); |
| gl!.disable(gl!.BLEND); |
| gl!.useProgram(quadProgram); |
| gl!.bindVertexArray(quadVAO); |
| gl!.activeTexture(gl!.TEXTURE0); |
| gl!.bindTexture(gl!.TEXTURE_2D, videoTex); |
| gl!.drawArrays(gl!.TRIANGLE_STRIP, 0, 4); |
| gl!.bindVertexArray(null); |
| } |
|
|
| function drawMakeupRegion(opts: MakeupRegionOpts) { |
| if (opts.intensity <= 0) return; |
| const adds = opts.add.filter((p): p is Float32Array => !!p && p.length > 0); |
| if (adds.length === 0) return; |
| const subs = (opts.subtract ?? []).filter((p): p is Float32Array => !!p && p.length > 0); |
|
|
| |
| let xMin = Infinity; |
| let xMax = -Infinity; |
| let yMin = Infinity; |
| let yMax = -Infinity; |
| for (const p of adds) { |
| const b = clipBounds(p); |
| if (b.xMin < xMin) xMin = b.xMin; |
| if (b.xMax > xMax) xMax = b.xMax; |
| if (b.yMin < yMin) yMin = b.yMin; |
| if (b.yMax > yMax) yMax = b.yMax; |
| } |
| if (!Number.isFinite(xMin) || !Number.isFinite(yMin)) return; |
| const bbox: ClipBounds = { xMin, xMax, yMin, yMax }; |
| const yMinUV = (yMin + 1) * 0.5; |
| const yMaxUV = (yMax + 1) * 0.5; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| const reachPx = Math.ceil(4 * opts.blurStrength) + 1; |
| const innerMaskPad = reachPx + 1; |
| const outerMaskPad = innerMaskPad + reachPx; |
|
|
| const innerMask = clipBoundsToPixelRect(maskFbo.w, maskFbo.h, bbox, innerMaskPad); |
| const outerMask = clipBoundsToPixelRect(maskFbo.w, maskFbo.h, bbox, outerMaskPad); |
| if (innerMask.w === 0 || innerMask.h === 0) return; |
|
|
| gl!.enable(gl!.SCISSOR_TEST); |
|
|
| |
| |
| gl!.disable(gl!.BLEND); |
| gl!.disable(gl!.STENCIL_TEST); |
| gl!.clearColor(0, 0, 0, 0); |
| gl!.scissor(outerMask.x, outerMask.y, outerMask.w, outerMask.h); |
| for (const f of [maskFbo, blurPingFbo, blurPongFbo]) { |
| gl!.bindFramebuffer(gl!.FRAMEBUFFER, f.fbo); |
| gl!.viewport(0, 0, f.w, f.h); |
| gl!.clear(gl!.COLOR_BUFFER_BIT); |
| } |
|
|
| |
| gl!.scissor(innerMask.x, innerMask.y, innerMask.w, innerMask.h); |
|
|
| |
| gl!.bindFramebuffer(gl!.FRAMEBUFFER, maskFbo.fbo); |
| gl!.viewport(0, 0, maskFbo.w, maskFbo.h); |
|
|
| gl!.useProgram(maskProgram); |
| gl!.bindVertexArray(regionVAO); |
| gl!.bindBuffer(gl!.ARRAY_BUFFER, regionBuf); |
| gl!.enableVertexAttribArray(maskPosLoc); |
| gl!.uniform1f(maskValueLoc, 1.0); |
| for (const p of adds) { |
| gl!.bufferData(gl!.ARRAY_BUFFER, p, gl!.DYNAMIC_DRAW); |
| gl!.vertexAttribPointer(maskPosLoc, 2, gl!.FLOAT, false, 0, 0); |
| gl!.drawArrays(gl!.TRIANGLES, 0, p.length / 2); |
| } |
| gl!.uniform1f(maskValueLoc, 0.0); |
| for (const p of subs) { |
| gl!.bufferData(gl!.ARRAY_BUFFER, p, gl!.DYNAMIC_DRAW); |
| gl!.vertexAttribPointer(maskPosLoc, 2, gl!.FLOAT, false, 0, 0); |
| gl!.drawArrays(gl!.TRIANGLES, 0, p.length / 2); |
| } |
| gl!.bindVertexArray(null); |
|
|
| |
| gl!.useProgram(blurProgram); |
| gl!.bindVertexArray(quadVAO); |
| gl!.bindBuffer(gl!.ARRAY_BUFFER, quadBuf); |
| gl!.enableVertexAttribArray(blurPosLoc); |
| gl!.vertexAttribPointer(blurPosLoc, 2, gl!.FLOAT, false, 0, 0); |
| gl!.uniform1i(blurTexLoc, 0); |
| gl!.activeTexture(gl!.TEXTURE0); |
|
|
| gl!.bindFramebuffer(gl!.FRAMEBUFFER, blurPingFbo.fbo); |
| gl!.viewport(0, 0, blurPingFbo.w, blurPingFbo.h); |
| gl!.bindTexture(gl!.TEXTURE_2D, maskFbo.tex); |
| gl!.uniform2f(blurOffsetLoc, opts.blurStrength / maskFbo.w, 0); |
| gl!.drawArrays(gl!.TRIANGLE_STRIP, 0, 4); |
|
|
| gl!.bindFramebuffer(gl!.FRAMEBUFFER, blurPongFbo.fbo); |
| gl!.viewport(0, 0, blurPongFbo.w, blurPongFbo.h); |
| gl!.bindTexture(gl!.TEXTURE_2D, blurPingFbo.tex); |
| gl!.uniform2f(blurOffsetLoc, 0, opts.blurStrength / blurPingFbo.h); |
| gl!.drawArrays(gl!.TRIANGLE_STRIP, 0, 4); |
| gl!.bindVertexArray(null); |
|
|
| |
| const innerFull = clipBoundsToPixelRect(canvas.width, canvas.height, bbox, innerMaskPad * 2); |
| gl!.bindFramebuffer(gl!.FRAMEBUFFER, null); |
| gl!.viewport(0, 0, canvas.width, canvas.height); |
| gl!.scissor(innerFull.x, innerFull.y, innerFull.w, innerFull.h); |
|
|
| gl!.useProgram(makeupProgram); |
| gl!.bindVertexArray(quadVAO); |
| gl!.bindBuffer(gl!.ARRAY_BUFFER, quadBuf); |
| gl!.enableVertexAttribArray(makeupPosLoc); |
| gl!.vertexAttribPointer(makeupPosLoc, 2, gl!.FLOAT, false, 0, 0); |
|
|
| gl!.activeTexture(gl!.TEXTURE0); |
| gl!.bindTexture(gl!.TEXTURE_2D, videoTex); |
| gl!.uniform1i(makeupVideoLoc, 0); |
| gl!.activeTexture(gl!.TEXTURE1); |
| gl!.bindTexture(gl!.TEXTURE_2D, blurPongFbo.tex); |
| gl!.uniform1i(makeupMaskLoc, 1); |
|
|
| gl!.uniform3f(makeupTopLoc, opts.topRGB[0], opts.topRGB[1], opts.topRGB[2]); |
| gl!.uniform3f(makeupBottomLoc, opts.bottomRGB[0], opts.bottomRGB[1], opts.bottomRGB[2]); |
| gl!.uniform2f(makeupYRangeLoc, yMinUV, yMaxUV); |
| gl!.uniform1f(makeupIntensityLoc, clamp01(opts.intensity)); |
| gl!.uniform1f(makeupMultiplyMixLoc, opts.multiplyMix ?? 0); |
| gl!.uniform1f(makeupLumaLiftLoc, opts.lumaLift ?? 0); |
| const sg = opts.specGuard ?? [0.85, 0.99]; |
| gl!.uniform2f(makeupSpecGuardLoc, sg[0], sg[1]); |
| gl!.uniform1i(makeupBlendModeLoc, opts.blendMode); |
|
|
| gl!.enable(gl!.BLEND); |
| gl!.blendFunc(gl!.SRC_ALPHA, gl!.ONE_MINUS_SRC_ALPHA); |
| gl!.drawArrays(gl!.TRIANGLE_STRIP, 0, 4); |
| gl!.disable(gl!.BLEND); |
| gl!.bindVertexArray(null); |
| gl!.activeTexture(gl!.TEXTURE0); |
|
|
| gl!.disable(gl!.SCISSOR_TEST); |
| } |
|
|
| return { |
| canvas, |
| syncMaskFbosToCanvas, |
| uploadVideoFrame, |
| drawVideoQuad, |
| drawMakeupRegion, |
| }; |
| } |
|
|