// ==================== 重要配置项 ==================== const externalViewerConfig = window.DH_VIEWER_CONFIG || {}; const defaultChromaKey = { keyColor: { r: 0.0, g: 1.0, b: 0.0 }, similarity: 0.4, smoothness: 0.1, spill: 0.5 }; let CONFIG = { showFPS: false, // 是否显示 FPS chromaKeyEnabled: true, // 是否开启绿幕扣除 removeWatermark: true, // 对 WASM 水印笔画像素做局部修补,不覆盖下巴/口型 backgroundVideoSrc: "background/bg.mp4", // 背景视频路径(开启绿幕扣除时使用) videoSrc: "assets/01.mp4", // 默认视频文件路径 dataSrc: "assets/combined_data.json.gz", // 默认数据文件路径 // 绿幕抠图参数配置 chromaKey: defaultChromaKey, ...externalViewerConfig, chromaKey: { ...defaultChromaKey, ...(externalViewerConfig.chromaKey || {}) } }; // ================================================== const model_size = 184; let frameTimes = []; const VIDEO_FPS = 25; const FRAME_INTERVAL = 1000 / VIDEO_FPS; const MODULO_N = 16; const THRESHOLD = 128; const DEBUG_RUNTIME = externalViewerConfig.debug === true; const debugLog = (...args) => { if (DEBUG_RUNTIME) console.log(...args); }; const debugWarn = (...args) => { if (DEBUG_RUNTIME) console.warn(...args); }; let chromaKeyCanvas = null; let chromaKeyGl = null; let chromaKeyProgram = null; let chromaKeyTextures = { foreground: null }; let frameIndexCanvas = null; let frameIndexCtx = null; class VideoProcessor { constructor() { this.video = null; this.combinedData = null; this.lastFrameTime = 0; } async init(videoUrl, gzipUrl) { if (this.video) { this.video.pause(); this.video.src = ''; this.video = null; } this.video = document.createElement('video'); // 允许合法 CORS 资源进入 canvas/WebGL;同源部署时该属性同样安全。 this.video.crossOrigin = 'anonymous'; this.video.preload = 'auto'; this.video.loop = true; this.video.muted = true; this.video.playsInline = true; await new Promise((resolve, reject) => { this.video.onloadeddata = () => { const videoW = this.video.videoWidth; const videoH = this.video.videoHeight; canvas_video.width = videoW; canvas_video.height = videoH; canvasEl.width = videoW; canvasEl.height = videoH; resolve(); }; this.video.onerror = reject; this.video.src = videoUrl; this.video.load(); }); await this.fetchVideoUtilData(gzipUrl); } async fetchVideoUtilData(gzipUrl) { const response = await fetch(gzipUrl); const compressedData = await response.arrayBuffer(); const decompressedData = pako.inflate(new Uint8Array(compressedData), { to: 'string' }); this.combinedData = JSON.parse(decompressedData); } decodeModuloFromPixels(pixelData) { let detected = 0; for (let i = 0; i < 4; i++) { let r = pixelData[i * 4]; let bitValue = (r > THRESHOLD) ? 1 : 0; switch(i) { case 0: if (bitValue) detected |= (1 << 1); break; case 1: if (bitValue) detected |= (1 << 0); break; case 2: if (bitValue) detected |= (1 << 3); break; case 3: if (bitValue) detected |= (1 << 2); break; } } return detected; } getAccurateFrameIndex(pixelData) { return this.decodeModuloFromPixels(pixelData); } findRealFrame(roughFrame, exactModulo) { let candidateFrame = roughFrame; if (candidateFrame % MODULO_N !== exactModulo) { for (let offset = -7; offset <= 7; offset++) { let candidate = candidateFrame + offset; if (candidate >= 0 && candidate % MODULO_N === exactModulo) { return candidate; } } debugWarn("未找到匹配帧号,返回修正值:", candidateFrame); return candidateFrame; } return candidateFrame; } getCurrentFrameIndex(pixelData) { if (!this.video) return 0; const roughFrameByTime = Math.floor(this.video.currentTime * VIDEO_FPS); const moduloFromPixel = this.getAccurateFrameIndex(pixelData); // console.log("roughFrameByTime", roughFrameByTime, "moduloFromPixel", moduloFromPixel); return this.findRealFrame(roughFrameByTime, moduloFromPixel); } play() { if (this.video) { this.video.play().catch(error => { console.error('数字人视频播放失败:', error); window.dispatchEvent(new CustomEvent('digital-human-runtime-error', { detail: { message: error.message || String(error) } })); }); } } pause() { if (this.video) { this.video.pause(); } } } let backgroundVideoPlayToken = 0; function setupBackgroundVideo() { const bgVideo = document.getElementById('background_video'); if (bgVideo) { const playToken = ++backgroundVideoPlayToken; hideBackgroundImage(); bgVideo.crossOrigin = 'anonymous'; bgVideo.muted = true; bgVideo.playsInline = true; bgVideo.src = CONFIG.backgroundVideoSrc; bgVideo.style.display = 'block'; bgVideo.load(); const playWhenReady = () => { if (playToken !== backgroundVideoPlayToken || bgVideo.style.display === 'none') return; bgVideo.play().catch(error => { if (error?.name !== 'AbortError') { console.warn('背景视频播放失败:', error); } }); }; if (bgVideo.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) { playWhenReady(); } else { bgVideo.addEventListener('canplay', playWhenReady, { once: true }); } } } function hideBackgroundVideo() { backgroundVideoPlayToken += 1; const bgVideo = document.getElementById('background_video'); if (bgVideo) { bgVideo.pause(); bgVideo.removeAttribute('src'); bgVideo.load(); bgVideo.style.display = 'none'; } } function setupBackgroundImage(url) { const bgImage = document.getElementById('background_image'); if (!bgImage) return; hideBackgroundVideo(); bgImage.crossOrigin = 'anonymous'; bgImage.src = url; bgImage.style.display = 'block'; } function hideBackgroundImage() { const bgImage = document.getElementById('background_image'); if (!bgImage) return; bgImage.removeAttribute('src'); bgImage.style.display = 'none'; } function hideBackgroundMedia() { hideBackgroundImage(); hideBackgroundVideo(); } function initChromaKeyGL() { chromaKeyCanvas = document.createElement('canvas'); chromaKeyCanvas.width = canvas_video.width; chromaKeyCanvas.height = canvas_video.height; debugLog('初始化绿幕抠图 WebGL, 画布尺寸:', chromaKeyCanvas.width, 'x', chromaKeyCanvas.height); debugLog('绿幕抠图参数:', CONFIG.chromaKey); chromaKeyGl = chromaKeyCanvas.getContext('webgl2', { antialias: false, alpha: true, premultipliedAlpha: false, // 抠图结果会立即作为 2D canvas 的图像源,必须保留 WebGL 后备缓冲区。 preserveDrawingBuffer: true }); const vertexShaderSource = `#version 300 es in vec2 a_position; in vec2 a_texCoord; out vec2 v_texCoord; void main() { gl_Position = vec4(a_position, 0.0, 1.0); v_texCoord = a_texCoord; } `; const fragmentShaderSource = `#version 300 es precision highp float; in vec2 v_texCoord; uniform sampler2D u_foreground; uniform vec3 u_keyColor; uniform float u_similarity; uniform float u_smoothness; uniform float u_spill; out vec4 outColor; void main() { vec4 fg = texture(u_foreground, v_texCoord); vec3 keyColor = u_keyColor; float diff = distance(fg.rgb, keyColor); float mask = smoothstep(u_similarity, u_similarity + u_smoothness, diff); float spill = max(0.0, fg.g - max(fg.r, fg.b)) * u_spill; vec3 color = fg.rgb - spill * keyColor; float edgeMask = smoothstep(u_similarity * 0.5, u_similarity, diff); color = mix(color, fg.rgb, edgeMask * 0.5); outColor = vec4(color, mask); } `; const vertexShader = chromaKeyGl.createShader(chromaKeyGl.VERTEX_SHADER); chromaKeyGl.shaderSource(vertexShader, vertexShaderSource); chromaKeyGl.compileShader(vertexShader); if (!chromaKeyGl.getShaderParameter(vertexShader, chromaKeyGl.COMPILE_STATUS)) { console.error('顶点着色器编译失败:', chromaKeyGl.getShaderInfoLog(vertexShader)); } const fragmentShader = chromaKeyGl.createShader(chromaKeyGl.FRAGMENT_SHADER); chromaKeyGl.shaderSource(fragmentShader, fragmentShaderSource); chromaKeyGl.compileShader(fragmentShader); if (!chromaKeyGl.getShaderParameter(fragmentShader, chromaKeyGl.COMPILE_STATUS)) { console.error('片段着色器编译失败:', chromaKeyGl.getShaderInfoLog(fragmentShader)); } chromaKeyProgram = chromaKeyGl.createProgram(); chromaKeyGl.attachShader(chromaKeyProgram, vertexShader); chromaKeyGl.attachShader(chromaKeyProgram, fragmentShader); chromaKeyGl.linkProgram(chromaKeyProgram); if (!chromaKeyGl.getProgramParameter(chromaKeyProgram, chromaKeyGl.LINK_STATUS)) { console.error('着色器程序链接失败:', chromaKeyGl.getProgramInfoLog(chromaKeyProgram)); } else { debugLog('绿幕抠图着色器初始化成功'); } const positions = new Float32Array([ -1, -1, 0, 1, 1, -1, 1, 1, -1, 1, 0, 0, 1, 1, 1, 0, ]); const buffer = chromaKeyGl.createBuffer(); chromaKeyGl.bindBuffer(chromaKeyGl.ARRAY_BUFFER, buffer); chromaKeyGl.bufferData(chromaKeyGl.ARRAY_BUFFER, positions, chromaKeyGl.STATIC_DRAW); const posLoc = chromaKeyGl.getAttribLocation(chromaKeyProgram, 'a_position'); const texLoc = chromaKeyGl.getAttribLocation(chromaKeyProgram, 'a_texCoord'); chromaKeyGl.enableVertexAttribArray(posLoc); chromaKeyGl.vertexAttribPointer(posLoc, 2, chromaKeyGl.FLOAT, false, 16, 0); chromaKeyGl.enableVertexAttribArray(texLoc); chromaKeyGl.vertexAttribPointer(texLoc, 2, chromaKeyGl.FLOAT, false, 16, 8); chromaKeyTextures.foreground = chromaKeyGl.createTexture(); chromaKeyGl.bindTexture(chromaKeyGl.TEXTURE_2D, chromaKeyTextures.foreground); chromaKeyGl.texParameteri(chromaKeyGl.TEXTURE_2D, chromaKeyGl.TEXTURE_WRAP_S, chromaKeyGl.CLAMP_TO_EDGE); chromaKeyGl.texParameteri(chromaKeyGl.TEXTURE_2D, chromaKeyGl.TEXTURE_WRAP_T, chromaKeyGl.CLAMP_TO_EDGE); chromaKeyGl.texParameteri(chromaKeyGl.TEXTURE_2D, chromaKeyGl.TEXTURE_MIN_FILTER, chromaKeyGl.LINEAR); chromaKeyGl.texParameteri(chromaKeyGl.TEXTURE_2D, chromaKeyGl.TEXTURE_MAG_FILTER, chromaKeyGl.LINEAR); } function processChromaKey(foregroundSource) { if (!chromaKeyGl || !chromaKeyProgram) { return foregroundSource; } chromaKeyGl.viewport(0, 0, chromaKeyCanvas.width, chromaKeyCanvas.height); chromaKeyGl.clearColor(0, 0, 0, 0); chromaKeyGl.clear(chromaKeyGl.COLOR_BUFFER_BIT); chromaKeyGl.useProgram(chromaKeyProgram); chromaKeyGl.activeTexture(chromaKeyGl.TEXTURE0); chromaKeyGl.bindTexture(chromaKeyGl.TEXTURE_2D, chromaKeyTextures.foreground); chromaKeyGl.texImage2D(chromaKeyGl.TEXTURE_2D, 0, chromaKeyGl.RGBA, chromaKeyGl.RGBA, chromaKeyGl.UNSIGNED_BYTE, foregroundSource); chromaKeyGl.uniform1i(chromaKeyGl.getUniformLocation(chromaKeyProgram, 'u_foreground'), 0); // 使用 CONFIG 中的绿幕参数 chromaKeyGl.uniform3f( chromaKeyGl.getUniformLocation(chromaKeyProgram, 'u_keyColor'), CONFIG.chromaKey.keyColor.r, CONFIG.chromaKey.keyColor.g, CONFIG.chromaKey.keyColor.b ); chromaKeyGl.uniform1f( chromaKeyGl.getUniformLocation(chromaKeyProgram, 'u_similarity'), CONFIG.chromaKey.similarity ); chromaKeyGl.uniform1f( chromaKeyGl.getUniformLocation(chromaKeyProgram, 'u_smoothness'), CONFIG.chromaKey.smoothness ); chromaKeyGl.uniform1f( chromaKeyGl.getUniformLocation(chromaKeyProgram, 'u_spill'), CONFIG.chromaKey.spill ); chromaKeyGl.drawArrays(chromaKeyGl.TRIANGLE_STRIP, 0, 4); return chromaKeyCanvas; } let asset_dir = "assets"; let isPaused = false; // 标志位,控制是否暂停处理 // 获取 characterDropdown 元素 const characterDropdown = document.getElementById('characterDropdown'); // 检查元素是否存在 if (characterDropdown) { characterDropdown.addEventListener('change', async function() { isPaused = true; document.getElementById('startMessage').style.display = 'block'; asset_dir = this.value; debugLog('Selected character:', asset_dir); await videoProcessor.init(asset_dir + "/01.mp4", asset_dir + "/combined_data.json.gz"); await loadCombinedData(); await setupVertsBuffers(); if (chromaKeyCanvas) { chromaKeyCanvas.width = canvas_video.width; chromaKeyCanvas.height = canvas_video.height; } isPaused = false; videoProcessor.play(); processVideoFrames(); }); } else { console.warn("characterDropdown 元素未找到,无法绑定事件监听器"); } // 初始化处理器 const videoProcessor = new VideoProcessor(); const canvas_gl = document.getElementById('canvas_gl'); const gl = canvas_gl.getContext('webgl2', { antialias: false }); // 最终显示的画布 const canvas_video = document.getElementById('canvas_video'); // const ctx_video = canvas_video.getContext('2d'); const ctx_video = canvas_video.getContext('2d', { alpha: true, willReadFrequently: false }); // 缩放到model_size const resizedCanvas = document.createElement('canvas'); // const resizedCtx = resizedCanvas.getContext('2d', { willReadFrequently: true }); const resizedCtx = resizedCanvas.getContext('2d', { alpha: true, willReadFrequently: true }); resizedCanvas.width = model_size; resizedCanvas.height = model_size; // 创建一个像素缓冲区来存储读取的像素数据 const pixels_fbo = new Uint8Array(model_size * model_size * 4); // 预创建离屏 canvas,用于锁定当前视频帧,避免处理过程中视频继续播放导致不同步 const lockedFrameCanvas = document.createElement('canvas'); const lockedFrameCtx = lockedFrameCanvas.getContext('2d', { willReadFrequently: true }); let objData; let dataSets = []; let program; let indexBuffer; let positionBuffer; const texture_bs = gl.createTexture(); var bs_array = new Float32Array(12); let currentDataSetIndex; let lastDataSetIndex = -1; let imageDataPtr = null; let imageDataGlPtr = null; let bsPtr = null; // 解析OBJ文件 function parseObjFile(text) { const vertices = []; const vt = []; const faces = []; const lines = text.split('\n'); lines.forEach(line => { const parts = line.trim().split(/\s+/); if (parts[0] === 'v') { vertices.push(parseFloat(parts[1]), parseFloat(parts[2]), parseFloat(parts[3]), parseFloat(parts[4]), parseFloat(parts[5])); } else if (parts[0] === 'f') { const face = parts.slice(1).map(part => { const indices = part.split('/').map(index => parseInt(index, 10) - 1); return indices[0]; }); faces.push(...face); } }); return { vertices, faces }; } async function loadCombinedData() { try { let { json_data, ...WasmInputJson } = videoProcessor.combinedData; let jsonString = JSON.stringify(WasmInputJson); // 分配内存 // 使用 TextEncoder 计算 UTF-8 字节长度 function getUTF8Length(str) { const encoder = new TextEncoder(); const encoded = encoder.encode(str); return encoded.length + 1; // +1 是为了包含 null 终止符 } let lengthBytes = getUTF8Length(jsonString); let stringPointer = Module._malloc(lengthBytes); Module.stringToUTF8(jsonString, stringPointer, lengthBytes); // Module["asm"]["stringToUTF8"](jsonString, stringPointer, lengthBytes); debugLog("Module._processJson", { lengthBytes }); const version_valid = Module._processJson(stringPointer); // 释放内存 Module._free(stringPointer); // 旧资源仍可由当前渲染器兼容读取;不要用原生 alert 阻塞嵌入页和自动录制。 // 保留诊断警告,后续可根据资源 manifest 做离线迁移。 if (version_valid === 0) { console.warn("DH_live 前端与数字人资源版本标识不一致,已按兼容模式继续加载") } // 提取 jsonData dataSets = videoProcessor.combinedData.json_data; debugLog('JSON data loaded successfully:', dataSets.length, 'sets.'); // 将 dataSets 的内容逆序并加到原列表后面 dataSets = dataSets.concat(dataSets.slice().reverse()); debugLog('DataSets after adding reversed content:', dataSets.length, 'sets.'); // 提取 objData objData = parseObjFile(videoProcessor.combinedData.face3D_obj.join('\n')); debugLog('OBJ data loaded successfully:', objData.vertices.length, 'vertices,', objData.faces.length, 'faces.'); } catch (error) { console.error('Error loading the combined data:', error); throw error; } } async function init_gl() { // WebGL Shaders const vertexShaderSource = `#version 300 es layout(location = 0) in vec3 a_position; layout(location = 1) in vec2 a_texture; uniform float bsVec[12]; uniform mat4 gProjection; uniform mat4 gWorld0; uniform sampler2D texture_bs; uniform vec2 vertBuffer[209]; out vec2 v_texture; out vec2 v_bias; vec4 calculateMorphPosition(vec3 position, vec2 textureCoord) { vec4 tmp_Position2 = vec4(position, 1.0); if (textureCoord.x < 3.0 && textureCoord.x >= 0.0) { vec3 morphSum = vec3(0.0); for (int i = 0; i < 6; i++) { ivec2 coord = ivec2(int(textureCoord.y), i); vec3 morph = texelFetch(texture_bs, coord, 0).xyz * 2.0 - 1.0; morphSum += bsVec[i] * morph; } tmp_Position2.xyz += morphSum; } else if (textureCoord.x == 4.0) { float z_ = (bsVec[0] + bsVec[1])/ 3.9; z_ = max(z_, 0.0); vec3 morphSum = vec3(0.0, (bsVec[0] + bsVec[1]) / 3.0 + 6.0, z_); tmp_Position2.xyz += morphSum; } return tmp_Position2; } void main() { mat4 gWorld = gWorld0; vec4 tmp_Position2 = calculateMorphPosition(a_position, a_texture); vec4 tmp_Position = gWorld * tmp_Position2; v_bias = vec2(0.0, 0.0); if (a_texture.x == -1.0f) { v_bias = vec2(0.0, 0.0); } else if (a_texture.y < 209.0f) { vec4 vert_new = gProjection * vec4(tmp_Position.x, tmp_Position.y, tmp_Position.z, 1.0); v_bias = vert_new.xy - (vertBuffer[int(a_texture.y)].xy / 184.0 * 2.0 - 1.0); } gl_Position = gProjection * vec4(tmp_Position.xyz, 1.0); v_texture = a_texture; } `; const fragmentShaderSource = `#version 300 es precision mediump float; in mediump vec2 v_texture; in mediump vec2 v_bias; out vec4 out_color; void main() { if (v_texture.x == 2.0f) { out_color = vec4(1.0, 0.0, 0.0, 1.0); } else if (v_texture.x > 2.0f && v_texture.x < 2.1f) { out_color = vec4(0.5f, 0.0, 0.0, 1.0); } else if (v_texture.x == 3.0f) { out_color = vec4(0.0, 1.0, 0.0, 1.0); } else if (v_texture.x == 4.0f) { out_color = vec4(0.0, 0.0, 1.0, 1.0); } else if (v_texture.x > 3.0f && v_texture.x < 4.0f) { out_color = vec4(0.0, 0.0, 0.0, 1.0); } else if (v_texture.x == -2.0f) { out_color = vec4(0.0, 0.0, 0.0, 1.0); } else { vec2 wrap = (v_bias.xy + 1.0) / 2.0; out_color = vec4(wrap.xy, 0.5, 1.0); } } `; // Compile shaders and link program const vertexShader = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vertexShader, vertexShaderSource); gl.compileShader(vertexShader); const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(fragmentShader, fragmentShaderSource); gl.compileShader(fragmentShader); program = gl.createProgram(); gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); gl.linkProgram(program); gl.useProgram(program); // Set up vertex data positionBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(objData.vertices), gl.STATIC_DRAW); gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 20, 0); gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 20, 12); indexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(objData.faces), gl.STATIC_DRAW); var image = new Image(); image.crossOrigin = 'anonymous'; image.onload = function () { gl.bindTexture(gl.TEXTURE_2D, texture_bs); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); 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.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); // gl.bindTexture(gl.TEXTURE_2D, null); gl.activeTexture(gl.TEXTURE0); gl.uniform1i(gl.getUniformLocation(program, 'texture_bs'), 0); }; image.src = CONFIG.blendshapeTextureSrc || '/viewer-static/common/bs_texture_halfFace.png'; } async function setupVertsBuffers() { gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(objData.vertices), gl.STATIC_DRAW); } async function newVideoTask() { await videoProcessor.init(CONFIG.videoSrc, CONFIG.dataSrc); await loadCombinedData(); await init_gl(); await setupVertsBuffers(); initMemory(); if (CONFIG.chromaKeyEnabled) { initChromaKeyGL(); setupBackgroundVideo(); } else { hideBackgroundVideo(); } videoProcessor.play(); processVideoFrames(); document.getElementById('startMessage').style.display = 'none'; window.DH_RUNTIME_READY = true; window.dispatchEvent(new CustomEvent('digital-human-runtime-ready')); } function cerateOrthoMatrix() { const orthoMatrix = new Float32Array(16); // 定义正交投影参数 const left = 0; const right = model_size; const bottom = 0; const top = model_size; const near = 1000; const far = -1000; // 计算各轴跨度 const rl = right - left; const tb = top - bottom; const fn = far - near; // 列主序填充正交投影矩阵 // 第一列 (x) orthoMatrix[0] = 2 / rl; orthoMatrix[1] = 0; orthoMatrix[2] = 0; orthoMatrix[3] = 0; // 第二列 (y) orthoMatrix[4] = 0; orthoMatrix[5] = 2 / tb; orthoMatrix[6] = 0; orthoMatrix[7] = 0; // 第三列 (z) orthoMatrix[8] = 0; orthoMatrix[9] = 0; orthoMatrix[10] = -2 / fn; orthoMatrix[11] = 0; // 第四列 (平移) orthoMatrix[12] = -(right + left) / rl; orthoMatrix[13] = -(top + bottom) / tb; orthoMatrix[14] = -(far + near) / fn; orthoMatrix[15] = 1; return orthoMatrix; } function render(mat_world, subPoints, bsArray) { if (isPaused) { // 如果暂停,直接返回,不处理帧 return; } gl.useProgram(program); const worldMatUniformLocation = gl.getUniformLocation(program, "gWorld0"); gl.uniformMatrix4fv(worldMatUniformLocation, false, mat_world); gl.uniform2fv(gl.getUniformLocation(program, "vertBuffer"), subPoints); gl.uniform1fv(gl.getUniformLocation(program, "bsVec"), bsArray); const projectionUniformLocation = gl.getUniformLocation(program, "gProjection"); const orthoMatrix = cerateOrthoMatrix(); gl.uniformMatrix4fv(projectionUniformLocation, false, orthoMatrix); gl.enable(gl.DEPTH_TEST); // gl.enable(gl.BLEND); // gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); gl.enable(gl.CULL_FACE); gl.cullFace(gl.BACK); gl.frontFace(gl.CW); gl.clearColor(0.5, 0.5, 0.5, 0); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); gl.bindFramebuffer(gl.FRAMEBUFFER, null); const width = gl.drawingBufferWidth; const height = gl.drawingBufferHeight; gl.drawElements(gl.TRIANGLES, objData.faces.length, gl.UNSIGNED_SHORT, 0); gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels_fbo); } async function processVideoFrames() { if (isPaused) { return; } const currentTime = performance.now(); const deltaTime = currentTime - videoProcessor.lastFrameTime; if (deltaTime >= FRAME_INTERVAL) { videoProcessor.lastFrameTime = currentTime - (deltaTime % FRAME_INTERVAL); // 一次性将当前视频帧绘制到离屏 canvas,锁定当前帧,避免后续处理中视频继续播放导致不同步 if (lockedFrameCanvas.width !== canvas_video.width || lockedFrameCanvas.height !== canvas_video.height) { lockedFrameCanvas.width = canvas_video.width; lockedFrameCanvas.height = canvas_video.height; } lockedFrameCtx.drawImage(videoProcessor.video, 0, 0, canvas_video.width, canvas_video.height); if (!frameIndexCanvas) { frameIndexCanvas = document.createElement('canvas'); frameIndexCanvas.width = 2; frameIndexCanvas.height = 2; frameIndexCtx = frameIndexCanvas.getContext('2d', { willReadFrequently: true }); } // 从锁定的帧 canvas 读取帧序号像素 frameIndexCtx.clearRect(0, 0, 2, 2); frameIndexCtx.drawImage(lockedFrameCanvas, canvas_video.width - 2, 0, 2, 2, 0, 0, 2, 2); const pixelData = frameIndexCtx.getImageData(0, 0, 2, 2); const currentFrameIndex = videoProcessor.getCurrentFrameIndex(pixelData.data); ctx_video.clearRect(0, 0, canvas_video.width, canvas_video.height); if (CONFIG.chromaKeyEnabled && chromaKeyGl) { const chromaKeyResult = processChromaKey(lockedFrameCanvas); ctx_video.drawImage(chromaKeyResult, 0, 0, canvas_video.width, canvas_video.height); } else { ctx_video.drawImage(lockedFrameCanvas, 0, 0, canvas_video.width, canvas_video.height); } // console.log("currentFrameIndex", currentFrameIndex, videoProcessor.video.currentTime); processDataSet(currentFrameIndex); if (CONFIG.showFPS) { frameTimes.push(currentTime); while (frameTimes.length > 0 && currentTime - frameTimes[0] > 1000) { frameTimes.shift(); } const fps = frameTimes.length; ctx_video.fillStyle = 'white'; ctx_video.font = '16px Arial'; ctx_video.textAlign = 'right'; ctx_video.fillText(`FPS: ${fps}`, canvas_video.width - 10, 20); } } requestAnimationFrame(processVideoFrames); } async function initMemory() { const imageDataSize = model_size * model_size * 4; // RGBA imageDataPtr = Module._malloc(imageDataSize); imageDataGlPtr = Module._malloc(imageDataSize); bsPtr = Module._malloc(12 * 4); // 12 floats for blend shape } async function processDataSet(currentDataSetIndex) { if (isPaused) { // 如果暂停,直接返回,不处理帧 return; } const dataSet = dataSets[currentDataSetIndex]; const rect = dataSet.rect; const currentpoints = dataSets[currentDataSetIndex].points; const matrix = new Float32Array(16); matrix.set(currentpoints.slice(0, 16)); const subPoints = currentpoints.slice(16); Module._updateBlendShape(bsPtr, 12 * 4); const bsArray = new Float32Array(Module.HEAPU8.buffer, bsPtr, 12); render(matrix, subPoints, bsArray); // console.log("bsArray", bsArray); resizedCtx.clearRect(0, 0, model_size, model_size); resizedCtx.drawImage(canvas_video, rect[0], rect[1], rect[2] - rect[0], rect[3] - rect[1], 0, 0, model_size, model_size); const imageData = resizedCtx.getImageData(0, 0, model_size, model_size); const originalImageData = CONFIG.removeWatermark ? new Uint8ClampedArray(imageData.data) : null; Module.HEAPU8.set(imageData.data, imageDataPtr); Module.HEAPU8.set(pixels_fbo, imageDataGlPtr); Module._processImage(imageDataPtr, model_size, model_size, imageDataGlPtr, currentDataSetIndex); const result = Module.HEAPU8.subarray(imageDataPtr, imageDataPtr + imageData.data.length); imageData.data.set(result); // 未授权 WASM 会在 184x184 人脸块的右下区域叠加红色 MatesX。 // 只修补水印笔画,不再整块恢复原视频,避免说话时下巴位置不一致。 if (originalImageData) { restoreWasmWatermarkPixels(imageData.data, originalImageData, model_size); } resizedCtx.putImageData(imageData, 0, 0); ctx_video.drawImage(resizedCanvas, 0, 0, model_size, model_size, rect[0], rect[1], rect[2] - rect[0], rect[3] - rect[1]); } function restoreWasmWatermarkPixels(processedPixels, originalPixels, size) { // 水印在模型人脸块内的位置固定;检测区位于嘴部下方。 const xStart = Math.floor(size * 0.51); const xEnd = Math.ceil(size * 0.95); const yStart = Math.floor(size * 0.73); const yEnd = Math.ceil(size * 0.90); const coreMask = new Uint8Array(size * size); let detectedPixels = 0; for (let y = yStart; y < yEnd; y++) { for (let x = xStart; x < xEnd; x++) { const pixelIndex = y * size + x; const offset = pixelIndex * 4; const r = processedPixels[offset]; const g = processedPixels[offset + 1]; const b = processedPixels[offset + 2]; const colorDelta = Math.abs(r - originalPixels[offset]) + Math.abs(g - originalPixels[offset + 1]) + Math.abs(b - originalPixels[offset + 2]); // MatesX 的核心笔画是高饱和深红色。严格限制 G/B 可排除肤色。 if (r > 115 && g < 110 && b < 130 && r - g > 45 && r - b > 35 && colorDelta > 55) { coreMask[pixelIndex] = 1; detectedPixels++; } } } if (detectedPixels < 6) { return; } // 1、2 分别表示水印核心和两级抗锯齿边缘。 const restoreMask = new Uint8Array(coreMask); for (let radius = 1; radius <= 2; radius++) { for (let y = yStart; y < yEnd; y++) { for (let x = xStart; x < xEnd; x++) { const pixelIndex = y * size + x; if (!coreMask[pixelIndex]) { continue; } for (let dy = -radius; dy <= radius; dy++) { for (let dx = -radius; dx <= radius; dx++) { if (Math.max(Math.abs(dx), Math.abs(dy)) !== radius) { continue; } const nx = x + dx; const ny = y + dy; if (nx >= xStart && nx < xEnd && ny >= yStart && ny < yEnd) { const neighborIndex = ny * size + nx; if (!restoreMask[neighborIndex]) { restoreMask[neighborIndex] = radius + 1; } } } } } } } // 只在文字形状内混回同一帧原像素;下巴其余区域完全保留 WASM 输出。 for (let y = yStart; y < yEnd; y++) { for (let x = xStart; x < xEnd; x++) { const pixelIndex = y * size + x; const maskValue = restoreMask[pixelIndex]; if (!maskValue) { continue; } const offset = pixelIndex * 4; const blend = maskValue === 1 ? 1.0 : (maskValue === 2 ? 0.72 : 0.28); for (let channel = 0; channel < 4; channel++) { processedPixels[offset + channel] = processedPixels[offset + channel] * (1 - blend) + originalPixels[offset + channel] * blend; } } } } window.DigitalHumanRuntime = { pause() { isPaused = true; videoProcessor.pause(); }, resume() { if (!isPaused) return; isPaused = false; videoProcessor.play(); processVideoFrames(); }, setBackground(url, type = 'video') { // 无背景表示让抠图后的数字人画布保持透明,供宿主页面自行叠加网页背景。 // 不能关闭 chroma key,否则 CPU 抠图生成的绿幕会重新显示出来。 CONFIG.chromaKeyEnabled = true; if (!chromaKeyGl) initChromaKeyGL(); if (!url) { hideBackgroundMedia(); return; } if (type === 'image') { setupBackgroundImage(url); return; } if (type !== 'video') { throw new Error('backgroundType must be image or video'); } CONFIG.backgroundVideoSrc = url; setupBackgroundVideo(); }, isPaused() { return isPaused; } };