Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 
 
 

987 rader
35 KiB

  1. // ==================== 重要配置项 ====================
  2. const externalViewerConfig = window.DH_VIEWER_CONFIG || {};
  3. const defaultChromaKey = {
  4. keyColor: { r: 0.0, g: 1.0, b: 0.0 },
  5. similarity: 0.4,
  6. smoothness: 0.1,
  7. spill: 0.5
  8. };
  9. let CONFIG = {
  10. showFPS: false, // 是否显示 FPS
  11. chromaKeyEnabled: true, // 是否开启绿幕扣除
  12. removeWatermark: true, // 对 WASM 水印笔画像素做局部修补,不覆盖下巴/口型
  13. backgroundVideoSrc: "background/bg.mp4", // 背景视频路径(开启绿幕扣除时使用)
  14. videoSrc: "assets/01.mp4", // 默认视频文件路径
  15. dataSrc: "assets/combined_data.json.gz", // 默认数据文件路径
  16. // 绿幕抠图参数配置
  17. chromaKey: defaultChromaKey,
  18. ...externalViewerConfig,
  19. chromaKey: { ...defaultChromaKey, ...(externalViewerConfig.chromaKey || {}) }
  20. };
  21. // ==================================================
  22. const model_size = 184;
  23. let frameTimes = [];
  24. const VIDEO_FPS = 25;
  25. const FRAME_INTERVAL = 1000 / VIDEO_FPS;
  26. const MODULO_N = 16;
  27. const THRESHOLD = 128;
  28. const DEBUG_RUNTIME = externalViewerConfig.debug === true;
  29. const debugLog = (...args) => {
  30. if (DEBUG_RUNTIME) console.log(...args);
  31. };
  32. const debugWarn = (...args) => {
  33. if (DEBUG_RUNTIME) console.warn(...args);
  34. };
  35. let chromaKeyCanvas = null;
  36. let chromaKeyGl = null;
  37. let chromaKeyProgram = null;
  38. let chromaKeyTextures = { foreground: null };
  39. let frameIndexCanvas = null;
  40. let frameIndexCtx = null;
  41. class VideoProcessor {
  42. constructor() {
  43. this.video = null;
  44. this.combinedData = null;
  45. this.lastFrameTime = 0;
  46. }
  47. async init(videoUrl, gzipUrl) {
  48. if (this.video) {
  49. this.video.pause();
  50. this.video.src = '';
  51. this.video = null;
  52. }
  53. this.video = document.createElement('video');
  54. // 允许合法 CORS 资源进入 canvas/WebGL;同源部署时该属性同样安全。
  55. this.video.crossOrigin = 'anonymous';
  56. this.video.preload = 'auto';
  57. this.video.loop = true;
  58. this.video.muted = true;
  59. this.video.playsInline = true;
  60. await new Promise((resolve, reject) => {
  61. this.video.onloadeddata = () => {
  62. const videoW = this.video.videoWidth;
  63. const videoH = this.video.videoHeight;
  64. canvas_video.width = videoW;
  65. canvas_video.height = videoH;
  66. canvasEl.width = videoW;
  67. canvasEl.height = videoH;
  68. resolve();
  69. };
  70. this.video.onerror = reject;
  71. this.video.src = videoUrl;
  72. this.video.load();
  73. });
  74. await this.fetchVideoUtilData(gzipUrl);
  75. }
  76. async fetchVideoUtilData(gzipUrl) {
  77. const response = await fetch(gzipUrl);
  78. const compressedData = await response.arrayBuffer();
  79. const decompressedData = pako.inflate(new Uint8Array(compressedData), { to: 'string' });
  80. this.combinedData = JSON.parse(decompressedData);
  81. }
  82. decodeModuloFromPixels(pixelData) {
  83. let detected = 0;
  84. for (let i = 0; i < 4; i++) {
  85. let r = pixelData[i * 4];
  86. let bitValue = (r > THRESHOLD) ? 1 : 0;
  87. switch(i) {
  88. case 0:
  89. if (bitValue) detected |= (1 << 1);
  90. break;
  91. case 1:
  92. if (bitValue) detected |= (1 << 0);
  93. break;
  94. case 2:
  95. if (bitValue) detected |= (1 << 3);
  96. break;
  97. case 3:
  98. if (bitValue) detected |= (1 << 2);
  99. break;
  100. }
  101. }
  102. return detected;
  103. }
  104. getAccurateFrameIndex(pixelData) {
  105. return this.decodeModuloFromPixels(pixelData);
  106. }
  107. findRealFrame(roughFrame, exactModulo) {
  108. let candidateFrame = roughFrame;
  109. if (candidateFrame % MODULO_N !== exactModulo) {
  110. for (let offset = -7; offset <= 7; offset++) {
  111. let candidate = candidateFrame + offset;
  112. if (candidate >= 0 && candidate % MODULO_N === exactModulo) {
  113. return candidate;
  114. }
  115. }
  116. debugWarn("未找到匹配帧号,返回修正值:", candidateFrame);
  117. return candidateFrame;
  118. }
  119. return candidateFrame;
  120. }
  121. getCurrentFrameIndex(pixelData) {
  122. if (!this.video) return 0;
  123. const roughFrameByTime = Math.floor(this.video.currentTime * VIDEO_FPS);
  124. const moduloFromPixel = this.getAccurateFrameIndex(pixelData);
  125. // console.log("roughFrameByTime", roughFrameByTime, "moduloFromPixel", moduloFromPixel);
  126. return this.findRealFrame(roughFrameByTime, moduloFromPixel);
  127. }
  128. play() {
  129. if (this.video) {
  130. this.video.play().catch(error => {
  131. console.error('数字人视频播放失败:', error);
  132. window.dispatchEvent(new CustomEvent('digital-human-runtime-error', {
  133. detail: { message: error.message || String(error) }
  134. }));
  135. });
  136. }
  137. }
  138. pause() {
  139. if (this.video) {
  140. this.video.pause();
  141. }
  142. }
  143. }
  144. let backgroundVideoPlayToken = 0;
  145. function setupBackgroundVideo() {
  146. const bgVideo = document.getElementById('background_video');
  147. if (bgVideo) {
  148. const playToken = ++backgroundVideoPlayToken;
  149. hideBackgroundImage();
  150. bgVideo.crossOrigin = 'anonymous';
  151. bgVideo.muted = true;
  152. bgVideo.playsInline = true;
  153. bgVideo.src = CONFIG.backgroundVideoSrc;
  154. bgVideo.style.display = 'block';
  155. bgVideo.load();
  156. const playWhenReady = () => {
  157. if (playToken !== backgroundVideoPlayToken || bgVideo.style.display === 'none') return;
  158. bgVideo.play().catch(error => {
  159. if (error?.name !== 'AbortError') {
  160. console.warn('背景视频播放失败:', error);
  161. }
  162. });
  163. };
  164. if (bgVideo.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
  165. playWhenReady();
  166. } else {
  167. bgVideo.addEventListener('canplay', playWhenReady, { once: true });
  168. }
  169. }
  170. }
  171. function hideBackgroundVideo() {
  172. backgroundVideoPlayToken += 1;
  173. const bgVideo = document.getElementById('background_video');
  174. if (bgVideo) {
  175. bgVideo.pause();
  176. bgVideo.removeAttribute('src');
  177. bgVideo.load();
  178. bgVideo.style.display = 'none';
  179. }
  180. }
  181. function setupBackgroundImage(url) {
  182. const bgImage = document.getElementById('background_image');
  183. if (!bgImage) return;
  184. hideBackgroundVideo();
  185. bgImage.crossOrigin = 'anonymous';
  186. bgImage.src = url;
  187. bgImage.style.display = 'block';
  188. }
  189. function hideBackgroundImage() {
  190. const bgImage = document.getElementById('background_image');
  191. if (!bgImage) return;
  192. bgImage.removeAttribute('src');
  193. bgImage.style.display = 'none';
  194. }
  195. function hideBackgroundMedia() {
  196. hideBackgroundImage();
  197. hideBackgroundVideo();
  198. }
  199. function initChromaKeyGL() {
  200. chromaKeyCanvas = document.createElement('canvas');
  201. chromaKeyCanvas.width = canvas_video.width;
  202. chromaKeyCanvas.height = canvas_video.height;
  203. debugLog('初始化绿幕抠图 WebGL, 画布尺寸:', chromaKeyCanvas.width, 'x', chromaKeyCanvas.height);
  204. debugLog('绿幕抠图参数:', CONFIG.chromaKey);
  205. chromaKeyGl = chromaKeyCanvas.getContext('webgl2', {
  206. antialias: false,
  207. alpha: true,
  208. premultipliedAlpha: false,
  209. // 抠图结果会立即作为 2D canvas 的图像源,必须保留 WebGL 后备缓冲区。
  210. preserveDrawingBuffer: true
  211. });
  212. const vertexShaderSource = `#version 300 es
  213. in vec2 a_position;
  214. in vec2 a_texCoord;
  215. out vec2 v_texCoord;
  216. void main() {
  217. gl_Position = vec4(a_position, 0.0, 1.0);
  218. v_texCoord = a_texCoord;
  219. }
  220. `;
  221. const fragmentShaderSource = `#version 300 es
  222. precision highp float;
  223. in vec2 v_texCoord;
  224. uniform sampler2D u_foreground;
  225. uniform vec3 u_keyColor;
  226. uniform float u_similarity;
  227. uniform float u_smoothness;
  228. uniform float u_spill;
  229. out vec4 outColor;
  230. void main() {
  231. vec4 fg = texture(u_foreground, v_texCoord);
  232. vec3 keyColor = u_keyColor;
  233. float diff = distance(fg.rgb, keyColor);
  234. float mask = smoothstep(u_similarity, u_similarity + u_smoothness, diff);
  235. float spill = max(0.0, fg.g - max(fg.r, fg.b)) * u_spill;
  236. vec3 color = fg.rgb - spill * keyColor;
  237. float edgeMask = smoothstep(u_similarity * 0.5, u_similarity, diff);
  238. color = mix(color, fg.rgb, edgeMask * 0.5);
  239. outColor = vec4(color, mask);
  240. }
  241. `;
  242. const vertexShader = chromaKeyGl.createShader(chromaKeyGl.VERTEX_SHADER);
  243. chromaKeyGl.shaderSource(vertexShader, vertexShaderSource);
  244. chromaKeyGl.compileShader(vertexShader);
  245. if (!chromaKeyGl.getShaderParameter(vertexShader, chromaKeyGl.COMPILE_STATUS)) {
  246. console.error('顶点着色器编译失败:', chromaKeyGl.getShaderInfoLog(vertexShader));
  247. }
  248. const fragmentShader = chromaKeyGl.createShader(chromaKeyGl.FRAGMENT_SHADER);
  249. chromaKeyGl.shaderSource(fragmentShader, fragmentShaderSource);
  250. chromaKeyGl.compileShader(fragmentShader);
  251. if (!chromaKeyGl.getShaderParameter(fragmentShader, chromaKeyGl.COMPILE_STATUS)) {
  252. console.error('片段着色器编译失败:', chromaKeyGl.getShaderInfoLog(fragmentShader));
  253. }
  254. chromaKeyProgram = chromaKeyGl.createProgram();
  255. chromaKeyGl.attachShader(chromaKeyProgram, vertexShader);
  256. chromaKeyGl.attachShader(chromaKeyProgram, fragmentShader);
  257. chromaKeyGl.linkProgram(chromaKeyProgram);
  258. if (!chromaKeyGl.getProgramParameter(chromaKeyProgram, chromaKeyGl.LINK_STATUS)) {
  259. console.error('着色器程序链接失败:', chromaKeyGl.getProgramInfoLog(chromaKeyProgram));
  260. } else {
  261. debugLog('绿幕抠图着色器初始化成功');
  262. }
  263. const positions = new Float32Array([
  264. -1, -1, 0, 1,
  265. 1, -1, 1, 1,
  266. -1, 1, 0, 0,
  267. 1, 1, 1, 0,
  268. ]);
  269. const buffer = chromaKeyGl.createBuffer();
  270. chromaKeyGl.bindBuffer(chromaKeyGl.ARRAY_BUFFER, buffer);
  271. chromaKeyGl.bufferData(chromaKeyGl.ARRAY_BUFFER, positions, chromaKeyGl.STATIC_DRAW);
  272. const posLoc = chromaKeyGl.getAttribLocation(chromaKeyProgram, 'a_position');
  273. const texLoc = chromaKeyGl.getAttribLocation(chromaKeyProgram, 'a_texCoord');
  274. chromaKeyGl.enableVertexAttribArray(posLoc);
  275. chromaKeyGl.vertexAttribPointer(posLoc, 2, chromaKeyGl.FLOAT, false, 16, 0);
  276. chromaKeyGl.enableVertexAttribArray(texLoc);
  277. chromaKeyGl.vertexAttribPointer(texLoc, 2, chromaKeyGl.FLOAT, false, 16, 8);
  278. chromaKeyTextures.foreground = chromaKeyGl.createTexture();
  279. chromaKeyGl.bindTexture(chromaKeyGl.TEXTURE_2D, chromaKeyTextures.foreground);
  280. chromaKeyGl.texParameteri(chromaKeyGl.TEXTURE_2D, chromaKeyGl.TEXTURE_WRAP_S, chromaKeyGl.CLAMP_TO_EDGE);
  281. chromaKeyGl.texParameteri(chromaKeyGl.TEXTURE_2D, chromaKeyGl.TEXTURE_WRAP_T, chromaKeyGl.CLAMP_TO_EDGE);
  282. chromaKeyGl.texParameteri(chromaKeyGl.TEXTURE_2D, chromaKeyGl.TEXTURE_MIN_FILTER, chromaKeyGl.LINEAR);
  283. chromaKeyGl.texParameteri(chromaKeyGl.TEXTURE_2D, chromaKeyGl.TEXTURE_MAG_FILTER, chromaKeyGl.LINEAR);
  284. }
  285. function processChromaKey(foregroundSource) {
  286. if (!chromaKeyGl || !chromaKeyProgram) {
  287. return foregroundSource;
  288. }
  289. chromaKeyGl.viewport(0, 0, chromaKeyCanvas.width, chromaKeyCanvas.height);
  290. chromaKeyGl.clearColor(0, 0, 0, 0);
  291. chromaKeyGl.clear(chromaKeyGl.COLOR_BUFFER_BIT);
  292. chromaKeyGl.useProgram(chromaKeyProgram);
  293. chromaKeyGl.activeTexture(chromaKeyGl.TEXTURE0);
  294. chromaKeyGl.bindTexture(chromaKeyGl.TEXTURE_2D, chromaKeyTextures.foreground);
  295. chromaKeyGl.texImage2D(chromaKeyGl.TEXTURE_2D, 0, chromaKeyGl.RGBA, chromaKeyGl.RGBA, chromaKeyGl.UNSIGNED_BYTE, foregroundSource);
  296. chromaKeyGl.uniform1i(chromaKeyGl.getUniformLocation(chromaKeyProgram, 'u_foreground'), 0);
  297. // 使用 CONFIG 中的绿幕参数
  298. chromaKeyGl.uniform3f(
  299. chromaKeyGl.getUniformLocation(chromaKeyProgram, 'u_keyColor'),
  300. CONFIG.chromaKey.keyColor.r,
  301. CONFIG.chromaKey.keyColor.g,
  302. CONFIG.chromaKey.keyColor.b
  303. );
  304. chromaKeyGl.uniform1f(
  305. chromaKeyGl.getUniformLocation(chromaKeyProgram, 'u_similarity'),
  306. CONFIG.chromaKey.similarity
  307. );
  308. chromaKeyGl.uniform1f(
  309. chromaKeyGl.getUniformLocation(chromaKeyProgram, 'u_smoothness'),
  310. CONFIG.chromaKey.smoothness
  311. );
  312. chromaKeyGl.uniform1f(
  313. chromaKeyGl.getUniformLocation(chromaKeyProgram, 'u_spill'),
  314. CONFIG.chromaKey.spill
  315. );
  316. chromaKeyGl.drawArrays(chromaKeyGl.TRIANGLE_STRIP, 0, 4);
  317. return chromaKeyCanvas;
  318. }
  319. let asset_dir = "assets";
  320. let isPaused = false; // 标志位,控制是否暂停处理
  321. // 获取 characterDropdown 元素
  322. const characterDropdown = document.getElementById('characterDropdown');
  323. // 检查元素是否存在
  324. if (characterDropdown) {
  325. characterDropdown.addEventListener('change', async function() {
  326. isPaused = true;
  327. document.getElementById('startMessage').style.display = 'block';
  328. asset_dir = this.value;
  329. debugLog('Selected character:', asset_dir);
  330. await videoProcessor.init(asset_dir + "/01.mp4", asset_dir + "/combined_data.json.gz");
  331. await loadCombinedData();
  332. await setupVertsBuffers();
  333. if (chromaKeyCanvas) {
  334. chromaKeyCanvas.width = canvas_video.width;
  335. chromaKeyCanvas.height = canvas_video.height;
  336. }
  337. isPaused = false;
  338. videoProcessor.play();
  339. processVideoFrames();
  340. });
  341. } else {
  342. console.warn("characterDropdown 元素未找到,无法绑定事件监听器");
  343. }
  344. // 初始化处理器
  345. const videoProcessor = new VideoProcessor();
  346. const canvas_gl = document.getElementById('canvas_gl');
  347. const gl = canvas_gl.getContext('webgl2', { antialias: false });
  348. // 最终显示的画布
  349. const canvas_video = document.getElementById('canvas_video');
  350. // const ctx_video = canvas_video.getContext('2d');
  351. const ctx_video = canvas_video.getContext('2d', {
  352. alpha: true,
  353. willReadFrequently: false
  354. });
  355. // 缩放到model_size
  356. const resizedCanvas = document.createElement('canvas');
  357. // const resizedCtx = resizedCanvas.getContext('2d', { willReadFrequently: true });
  358. const resizedCtx = resizedCanvas.getContext('2d', {
  359. alpha: true,
  360. willReadFrequently: true
  361. });
  362. resizedCanvas.width = model_size;
  363. resizedCanvas.height = model_size;
  364. // 创建一个像素缓冲区来存储读取的像素数据
  365. const pixels_fbo = new Uint8Array(model_size * model_size * 4);
  366. // 预创建离屏 canvas,用于锁定当前视频帧,避免处理过程中视频继续播放导致不同步
  367. const lockedFrameCanvas = document.createElement('canvas');
  368. const lockedFrameCtx = lockedFrameCanvas.getContext('2d', { willReadFrequently: true });
  369. let objData;
  370. let dataSets = [];
  371. let program;
  372. let indexBuffer;
  373. let positionBuffer;
  374. const texture_bs = gl.createTexture();
  375. var bs_array = new Float32Array(12);
  376. let currentDataSetIndex;
  377. let lastDataSetIndex = -1;
  378. let imageDataPtr = null;
  379. let imageDataGlPtr = null;
  380. let bsPtr = null;
  381. // 解析OBJ文件
  382. function parseObjFile(text) {
  383. const vertices = [];
  384. const vt = [];
  385. const faces = [];
  386. const lines = text.split('\n');
  387. lines.forEach(line => {
  388. const parts = line.trim().split(/\s+/);
  389. if (parts[0] === 'v') {
  390. vertices.push(parseFloat(parts[1]), parseFloat(parts[2]), parseFloat(parts[3]),
  391. parseFloat(parts[4]), parseFloat(parts[5]));
  392. } else if (parts[0] === 'f') {
  393. const face = parts.slice(1).map(part => {
  394. const indices = part.split('/').map(index => parseInt(index, 10) - 1);
  395. return indices[0];
  396. });
  397. faces.push(...face);
  398. }
  399. });
  400. return { vertices, faces };
  401. }
  402. async function loadCombinedData() {
  403. try {
  404. let { json_data, ...WasmInputJson } = videoProcessor.combinedData;
  405. let jsonString = JSON.stringify(WasmInputJson);
  406. // 分配内存
  407. // 使用 TextEncoder 计算 UTF-8 字节长度
  408. function getUTF8Length(str) {
  409. const encoder = new TextEncoder();
  410. const encoded = encoder.encode(str);
  411. return encoded.length + 1; // +1 是为了包含 null 终止符
  412. }
  413. let lengthBytes = getUTF8Length(jsonString);
  414. let stringPointer = Module._malloc(lengthBytes);
  415. Module.stringToUTF8(jsonString, stringPointer, lengthBytes);
  416. // Module["asm"]["stringToUTF8"](jsonString, stringPointer, lengthBytes);
  417. debugLog("Module._processJson", { lengthBytes });
  418. const version_valid = Module._processJson(stringPointer);
  419. // 释放内存
  420. Module._free(stringPointer);
  421. // 旧资源仍可由当前渲染器兼容读取;不要用原生 alert 阻塞嵌入页和自动录制。
  422. // 保留诊断警告,后续可根据资源 manifest 做离线迁移。
  423. if (version_valid === 0) {
  424. console.warn("DH_live 前端与数字人资源版本标识不一致,已按兼容模式继续加载")
  425. }
  426. // 提取 jsonData
  427. dataSets = videoProcessor.combinedData.json_data;
  428. debugLog('JSON data loaded successfully:', dataSets.length, 'sets.');
  429. // 将 dataSets 的内容逆序并加到原列表后面
  430. dataSets = dataSets.concat(dataSets.slice().reverse());
  431. debugLog('DataSets after adding reversed content:', dataSets.length, 'sets.');
  432. // 提取 objData
  433. objData = parseObjFile(videoProcessor.combinedData.face3D_obj.join('\n'));
  434. debugLog('OBJ data loaded successfully:', objData.vertices.length, 'vertices,', objData.faces.length, 'faces.');
  435. } catch (error) {
  436. console.error('Error loading the combined data:', error);
  437. throw error;
  438. }
  439. }
  440. async function init_gl() {
  441. // WebGL Shaders
  442. const vertexShaderSource = `#version 300 es
  443. layout(location = 0) in vec3 a_position;
  444. layout(location = 1) in vec2 a_texture;
  445. uniform float bsVec[12];
  446. uniform mat4 gProjection;
  447. uniform mat4 gWorld0;
  448. uniform sampler2D texture_bs;
  449. uniform vec2 vertBuffer[209];
  450. out vec2 v_texture;
  451. out vec2 v_bias;
  452. vec4 calculateMorphPosition(vec3 position, vec2 textureCoord) {
  453. vec4 tmp_Position2 = vec4(position, 1.0);
  454. if (textureCoord.x < 3.0 && textureCoord.x >= 0.0) {
  455. vec3 morphSum = vec3(0.0);
  456. for (int i = 0; i < 6; i++) {
  457. ivec2 coord = ivec2(int(textureCoord.y), i);
  458. vec3 morph = texelFetch(texture_bs, coord, 0).xyz * 2.0 - 1.0;
  459. morphSum += bsVec[i] * morph;
  460. }
  461. tmp_Position2.xyz += morphSum;
  462. }
  463. else if (textureCoord.x == 4.0) {
  464. float z_ = (bsVec[0] + bsVec[1])/ 3.9;
  465. z_ = max(z_, 0.0);
  466. vec3 morphSum = vec3(0.0, (bsVec[0] + bsVec[1]) / 3.0 + 6.0, z_);
  467. tmp_Position2.xyz += morphSum;
  468. }
  469. return tmp_Position2;
  470. }
  471. void main() {
  472. mat4 gWorld = gWorld0;
  473. vec4 tmp_Position2 = calculateMorphPosition(a_position, a_texture);
  474. vec4 tmp_Position = gWorld * tmp_Position2;
  475. v_bias = vec2(0.0, 0.0);
  476. if (a_texture.x == -1.0f) {
  477. v_bias = vec2(0.0, 0.0);
  478. }
  479. else if (a_texture.y < 209.0f) {
  480. vec4 vert_new = gProjection * vec4(tmp_Position.x, tmp_Position.y, tmp_Position.z, 1.0);
  481. v_bias = vert_new.xy - (vertBuffer[int(a_texture.y)].xy / 184.0 * 2.0 - 1.0);
  482. }
  483. gl_Position = gProjection * vec4(tmp_Position.xyz, 1.0);
  484. v_texture = a_texture;
  485. }
  486. `;
  487. const fragmentShaderSource = `#version 300 es
  488. precision mediump float;
  489. in mediump vec2 v_texture;
  490. in mediump vec2 v_bias;
  491. out vec4 out_color;
  492. void main() {
  493. if (v_texture.x == 2.0f) {
  494. out_color = vec4(1.0, 0.0, 0.0, 1.0);
  495. }
  496. else if (v_texture.x > 2.0f && v_texture.x < 2.1f) {
  497. out_color = vec4(0.5f, 0.0, 0.0, 1.0);
  498. }
  499. else if (v_texture.x == 3.0f) {
  500. out_color = vec4(0.0, 1.0, 0.0, 1.0);
  501. }
  502. else if (v_texture.x == 4.0f) {
  503. out_color = vec4(0.0, 0.0, 1.0, 1.0);
  504. }
  505. else if (v_texture.x > 3.0f && v_texture.x < 4.0f) {
  506. out_color = vec4(0.0, 0.0, 0.0, 1.0);
  507. }
  508. else if (v_texture.x == -2.0f) {
  509. out_color = vec4(0.0, 0.0, 0.0, 1.0);
  510. }
  511. else {
  512. vec2 wrap = (v_bias.xy + 1.0) / 2.0;
  513. out_color = vec4(wrap.xy, 0.5, 1.0);
  514. }
  515. }
  516. `;
  517. // Compile shaders and link program
  518. const vertexShader = gl.createShader(gl.VERTEX_SHADER);
  519. gl.shaderSource(vertexShader, vertexShaderSource);
  520. gl.compileShader(vertexShader);
  521. const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
  522. gl.shaderSource(fragmentShader, fragmentShaderSource);
  523. gl.compileShader(fragmentShader);
  524. program = gl.createProgram();
  525. gl.attachShader(program, vertexShader);
  526. gl.attachShader(program, fragmentShader);
  527. gl.linkProgram(program);
  528. gl.useProgram(program);
  529. // Set up vertex data
  530. positionBuffer = gl.createBuffer();
  531. gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  532. gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(objData.vertices), gl.STATIC_DRAW);
  533. gl.enableVertexAttribArray(0);
  534. gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 20, 0);
  535. gl.enableVertexAttribArray(1);
  536. gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 20, 12);
  537. indexBuffer = gl.createBuffer();
  538. gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
  539. gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(objData.faces), gl.STATIC_DRAW);
  540. var image = new Image();
  541. image.crossOrigin = 'anonymous';
  542. image.onload = function () {
  543. gl.bindTexture(gl.TEXTURE_2D, texture_bs);
  544. gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
  545. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  546. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  547. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  548. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  549. // gl.bindTexture(gl.TEXTURE_2D, null);
  550. gl.activeTexture(gl.TEXTURE0);
  551. gl.uniform1i(gl.getUniformLocation(program, 'texture_bs'), 0);
  552. };
  553. image.src = CONFIG.blendshapeTextureSrc || '/viewer-static/common/bs_texture_halfFace.png';
  554. }
  555. async function setupVertsBuffers() {
  556. gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  557. gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(objData.vertices), gl.STATIC_DRAW);
  558. }
  559. async function newVideoTask() {
  560. await videoProcessor.init(CONFIG.videoSrc, CONFIG.dataSrc);
  561. await loadCombinedData();
  562. await init_gl();
  563. await setupVertsBuffers();
  564. initMemory();
  565. if (CONFIG.chromaKeyEnabled) {
  566. initChromaKeyGL();
  567. setupBackgroundVideo();
  568. } else {
  569. hideBackgroundVideo();
  570. }
  571. videoProcessor.play();
  572. processVideoFrames();
  573. document.getElementById('startMessage').style.display = 'none';
  574. window.DH_RUNTIME_READY = true;
  575. window.dispatchEvent(new CustomEvent('digital-human-runtime-ready'));
  576. }
  577. function cerateOrthoMatrix()
  578. {
  579. const orthoMatrix = new Float32Array(16);
  580. // 定义正交投影参数
  581. const left = 0;
  582. const right = model_size;
  583. const bottom = 0;
  584. const top = model_size;
  585. const near = 1000;
  586. const far = -1000;
  587. // 计算各轴跨度
  588. const rl = right - left;
  589. const tb = top - bottom;
  590. const fn = far - near;
  591. // 列主序填充正交投影矩阵
  592. // 第一列 (x)
  593. orthoMatrix[0] = 2 / rl;
  594. orthoMatrix[1] = 0;
  595. orthoMatrix[2] = 0;
  596. orthoMatrix[3] = 0;
  597. // 第二列 (y)
  598. orthoMatrix[4] = 0;
  599. orthoMatrix[5] = 2 / tb;
  600. orthoMatrix[6] = 0;
  601. orthoMatrix[7] = 0;
  602. // 第三列 (z)
  603. orthoMatrix[8] = 0;
  604. orthoMatrix[9] = 0;
  605. orthoMatrix[10] = -2 / fn;
  606. orthoMatrix[11] = 0;
  607. // 第四列 (平移)
  608. orthoMatrix[12] = -(right + left) / rl;
  609. orthoMatrix[13] = -(top + bottom) / tb;
  610. orthoMatrix[14] = -(far + near) / fn;
  611. orthoMatrix[15] = 1;
  612. return orthoMatrix;
  613. }
  614. function render(mat_world, subPoints, bsArray) {
  615. if (isPaused) {
  616. // 如果暂停,直接返回,不处理帧
  617. return;
  618. }
  619. gl.useProgram(program);
  620. const worldMatUniformLocation = gl.getUniformLocation(program, "gWorld0");
  621. gl.uniformMatrix4fv(worldMatUniformLocation, false, mat_world);
  622. gl.uniform2fv(gl.getUniformLocation(program, "vertBuffer"), subPoints);
  623. gl.uniform1fv(gl.getUniformLocation(program, "bsVec"), bsArray);
  624. const projectionUniformLocation = gl.getUniformLocation(program, "gProjection");
  625. const orthoMatrix = cerateOrthoMatrix();
  626. gl.uniformMatrix4fv(projectionUniformLocation, false, orthoMatrix);
  627. gl.enable(gl.DEPTH_TEST);
  628. // gl.enable(gl.BLEND);
  629. // gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
  630. gl.enable(gl.CULL_FACE);
  631. gl.cullFace(gl.BACK);
  632. gl.frontFace(gl.CW);
  633. gl.clearColor(0.5, 0.5, 0.5, 0);
  634. gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  635. gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
  636. gl.bindFramebuffer(gl.FRAMEBUFFER, null);
  637. const width = gl.drawingBufferWidth;
  638. const height = gl.drawingBufferHeight;
  639. gl.drawElements(gl.TRIANGLES, objData.faces.length, gl.UNSIGNED_SHORT, 0);
  640. gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels_fbo);
  641. }
  642. async function processVideoFrames() {
  643. if (isPaused) {
  644. return;
  645. }
  646. const currentTime = performance.now();
  647. const deltaTime = currentTime - videoProcessor.lastFrameTime;
  648. if (deltaTime >= FRAME_INTERVAL) {
  649. videoProcessor.lastFrameTime = currentTime - (deltaTime % FRAME_INTERVAL);
  650. // 一次性将当前视频帧绘制到离屏 canvas,锁定当前帧,避免后续处理中视频继续播放导致不同步
  651. if (lockedFrameCanvas.width !== canvas_video.width ||
  652. lockedFrameCanvas.height !== canvas_video.height) {
  653. lockedFrameCanvas.width = canvas_video.width;
  654. lockedFrameCanvas.height = canvas_video.height;
  655. }
  656. lockedFrameCtx.drawImage(videoProcessor.video, 0, 0, canvas_video.width, canvas_video.height);
  657. if (!frameIndexCanvas) {
  658. frameIndexCanvas = document.createElement('canvas');
  659. frameIndexCanvas.width = 2;
  660. frameIndexCanvas.height = 2;
  661. frameIndexCtx = frameIndexCanvas.getContext('2d', { willReadFrequently: true });
  662. }
  663. // 从锁定的帧 canvas 读取帧序号像素
  664. frameIndexCtx.clearRect(0, 0, 2, 2);
  665. frameIndexCtx.drawImage(lockedFrameCanvas,
  666. canvas_video.width - 2, 0, 2, 2,
  667. 0, 0, 2, 2);
  668. const pixelData = frameIndexCtx.getImageData(0, 0, 2, 2);
  669. const currentFrameIndex = videoProcessor.getCurrentFrameIndex(pixelData.data);
  670. ctx_video.clearRect(0, 0, canvas_video.width, canvas_video.height);
  671. if (CONFIG.chromaKeyEnabled && chromaKeyGl) {
  672. const chromaKeyResult = processChromaKey(lockedFrameCanvas);
  673. ctx_video.drawImage(chromaKeyResult, 0, 0, canvas_video.width, canvas_video.height);
  674. } else {
  675. ctx_video.drawImage(lockedFrameCanvas, 0, 0, canvas_video.width, canvas_video.height);
  676. }
  677. // console.log("currentFrameIndex", currentFrameIndex, videoProcessor.video.currentTime);
  678. processDataSet(currentFrameIndex);
  679. if (CONFIG.showFPS) {
  680. frameTimes.push(currentTime);
  681. while (frameTimes.length > 0 && currentTime - frameTimes[0] > 1000) {
  682. frameTimes.shift();
  683. }
  684. const fps = frameTimes.length;
  685. ctx_video.fillStyle = 'white';
  686. ctx_video.font = '16px Arial';
  687. ctx_video.textAlign = 'right';
  688. ctx_video.fillText(`FPS: ${fps}`, canvas_video.width - 10, 20);
  689. }
  690. }
  691. requestAnimationFrame(processVideoFrames);
  692. }
  693. async function initMemory() {
  694. const imageDataSize = model_size * model_size * 4; // RGBA
  695. imageDataPtr = Module._malloc(imageDataSize);
  696. imageDataGlPtr = Module._malloc(imageDataSize);
  697. bsPtr = Module._malloc(12 * 4); // 12 floats for blend shape
  698. }
  699. async function processDataSet(currentDataSetIndex) {
  700. if (isPaused) {
  701. // 如果暂停,直接返回,不处理帧
  702. return;
  703. }
  704. const dataSet = dataSets[currentDataSetIndex];
  705. const rect = dataSet.rect;
  706. const currentpoints = dataSets[currentDataSetIndex].points;
  707. const matrix = new Float32Array(16);
  708. matrix.set(currentpoints.slice(0, 16));
  709. const subPoints = currentpoints.slice(16);
  710. Module._updateBlendShape(bsPtr, 12 * 4);
  711. const bsArray = new Float32Array(Module.HEAPU8.buffer, bsPtr, 12);
  712. render(matrix, subPoints, bsArray);
  713. // console.log("bsArray", bsArray);
  714. resizedCtx.clearRect(0, 0, model_size, model_size);
  715. resizedCtx.drawImage(canvas_video, rect[0], rect[1], rect[2] - rect[0], rect[3] - rect[1], 0, 0, model_size, model_size);
  716. const imageData = resizedCtx.getImageData(0, 0, model_size, model_size);
  717. const originalImageData = CONFIG.removeWatermark
  718. ? new Uint8ClampedArray(imageData.data)
  719. : null;
  720. Module.HEAPU8.set(imageData.data, imageDataPtr);
  721. Module.HEAPU8.set(pixels_fbo, imageDataGlPtr);
  722. Module._processImage(imageDataPtr, model_size, model_size, imageDataGlPtr, currentDataSetIndex);
  723. const result = Module.HEAPU8.subarray(imageDataPtr, imageDataPtr + imageData.data.length);
  724. imageData.data.set(result);
  725. // 未授权 WASM 会在 184x184 人脸块的右下区域叠加红色 MatesX。
  726. // 只修补水印笔画,不再整块恢复原视频,避免说话时下巴位置不一致。
  727. if (originalImageData) {
  728. restoreWasmWatermarkPixels(imageData.data, originalImageData, model_size);
  729. }
  730. resizedCtx.putImageData(imageData, 0, 0);
  731. ctx_video.drawImage(resizedCanvas, 0, 0, model_size, model_size, rect[0], rect[1], rect[2] - rect[0], rect[3] - rect[1]);
  732. }
  733. function restoreWasmWatermarkPixels(processedPixels, originalPixels, size) {
  734. // 水印在模型人脸块内的位置固定;检测区位于嘴部下方。
  735. const xStart = Math.floor(size * 0.51);
  736. const xEnd = Math.ceil(size * 0.95);
  737. const yStart = Math.floor(size * 0.73);
  738. const yEnd = Math.ceil(size * 0.90);
  739. const coreMask = new Uint8Array(size * size);
  740. let detectedPixels = 0;
  741. for (let y = yStart; y < yEnd; y++) {
  742. for (let x = xStart; x < xEnd; x++) {
  743. const pixelIndex = y * size + x;
  744. const offset = pixelIndex * 4;
  745. const r = processedPixels[offset];
  746. const g = processedPixels[offset + 1];
  747. const b = processedPixels[offset + 2];
  748. const colorDelta = Math.abs(r - originalPixels[offset])
  749. + Math.abs(g - originalPixels[offset + 1])
  750. + Math.abs(b - originalPixels[offset + 2]);
  751. // MatesX 的核心笔画是高饱和深红色。严格限制 G/B 可排除肤色。
  752. if (r > 115 && g < 110 && b < 130
  753. && r - g > 45 && r - b > 35 && colorDelta > 55) {
  754. coreMask[pixelIndex] = 1;
  755. detectedPixels++;
  756. }
  757. }
  758. }
  759. if (detectedPixels < 6) {
  760. return;
  761. }
  762. // 1、2 分别表示水印核心和两级抗锯齿边缘。
  763. const restoreMask = new Uint8Array(coreMask);
  764. for (let radius = 1; radius <= 2; radius++) {
  765. for (let y = yStart; y < yEnd; y++) {
  766. for (let x = xStart; x < xEnd; x++) {
  767. const pixelIndex = y * size + x;
  768. if (!coreMask[pixelIndex]) {
  769. continue;
  770. }
  771. for (let dy = -radius; dy <= radius; dy++) {
  772. for (let dx = -radius; dx <= radius; dx++) {
  773. if (Math.max(Math.abs(dx), Math.abs(dy)) !== radius) {
  774. continue;
  775. }
  776. const nx = x + dx;
  777. const ny = y + dy;
  778. if (nx >= xStart && nx < xEnd && ny >= yStart && ny < yEnd) {
  779. const neighborIndex = ny * size + nx;
  780. if (!restoreMask[neighborIndex]) {
  781. restoreMask[neighborIndex] = radius + 1;
  782. }
  783. }
  784. }
  785. }
  786. }
  787. }
  788. }
  789. // 只在文字形状内混回同一帧原像素;下巴其余区域完全保留 WASM 输出。
  790. for (let y = yStart; y < yEnd; y++) {
  791. for (let x = xStart; x < xEnd; x++) {
  792. const pixelIndex = y * size + x;
  793. const maskValue = restoreMask[pixelIndex];
  794. if (!maskValue) {
  795. continue;
  796. }
  797. const offset = pixelIndex * 4;
  798. const blend = maskValue === 1 ? 1.0 : (maskValue === 2 ? 0.72 : 0.28);
  799. for (let channel = 0; channel < 4; channel++) {
  800. processedPixels[offset + channel] = processedPixels[offset + channel] * (1 - blend)
  801. + originalPixels[offset + channel] * blend;
  802. }
  803. }
  804. }
  805. }
  806. window.DigitalHumanRuntime = {
  807. pause() {
  808. isPaused = true;
  809. videoProcessor.pause();
  810. },
  811. resume() {
  812. if (!isPaused) return;
  813. isPaused = false;
  814. videoProcessor.play();
  815. processVideoFrames();
  816. },
  817. setBackground(url, type = 'video') {
  818. // 无背景表示让抠图后的数字人画布保持透明,供宿主页面自行叠加网页背景。
  819. // 不能关闭 chroma key,否则 CPU 抠图生成的绿幕会重新显示出来。
  820. CONFIG.chromaKeyEnabled = true;
  821. if (!chromaKeyGl) initChromaKeyGL();
  822. if (!url) {
  823. hideBackgroundMedia();
  824. return;
  825. }
  826. if (type === 'image') {
  827. setupBackgroundImage(url);
  828. return;
  829. }
  830. if (type !== 'video') {
  831. throw new Error('backgroundType must be image or video');
  832. }
  833. CONFIG.backgroundVideoSrc = url;
  834. setupBackgroundVideo();
  835. },
  836. isPaused() {
  837. return isPaused;
  838. }
  839. };