@spatialwalk/avatarkit 1.0.0-beta.77 → 1.0.0-beta.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1400,6 +1400,32 @@ function base64FromBytes$1(arr) {
1400
1400
  function isSet$1(value) {
1401
1401
  return value !== null && value !== void 0;
1402
1402
  }
1403
+ function convertProtoFlameToWasmParams(protoFlame) {
1404
+ var _a;
1405
+ return {
1406
+ translation: protoFlame.translation || [0, 0, 0],
1407
+ rotation: protoFlame.rotation || [0, 0, 0],
1408
+ neck_pose: protoFlame.neckPose || [0, 0, 0],
1409
+ jaw_pose: protoFlame.jawPose || [0, 0, 0],
1410
+ eyes_pose: protoFlame.eyePose || [0, 0, 0, 0, 0, 0],
1411
+ eyelid: protoFlame.eyeLid || [0, 0],
1412
+ expr_params: protoFlame.expression || [],
1413
+ shape_params: [],
1414
+ // Realtime doesn't provide shape params, use default
1415
+ has_eyelid: (((_a = protoFlame.eyeLid) == null ? void 0 : _a.length) || 0) > 0
1416
+ };
1417
+ }
1418
+ function convertWasmParamsToProtoFlame(wasmParams) {
1419
+ return {
1420
+ translation: wasmParams.translation || [0, 0, 0],
1421
+ rotation: wasmParams.rotation || [0, 0, 0],
1422
+ neckPose: wasmParams.neck_pose || [0, 0, 0],
1423
+ jawPose: wasmParams.jaw_pose || [0, 0, 0],
1424
+ eyePose: wasmParams.eyes_pose || [0, 0, 0, 0, 0, 0],
1425
+ eyeLid: wasmParams.eyelid || [0, 0],
1426
+ expression: wasmParams.expr_params || []
1427
+ };
1428
+ }
1403
1429
  const POSTHOG_HOST_INTL = "https://i.spatialwalk.ai";
1404
1430
  const POSTHOG_API_KEY_INTL = "phc_IFTLa6Z6VhTaNvsxB7klvG2JeNwcSpnnwz8YvZRC96Q";
1405
1431
  function getPostHogConfig(_environment) {
@@ -1452,32 +1478,6 @@ const APP_CONFIG = {
1452
1478
  }
1453
1479
  }
1454
1480
  };
1455
- function convertProtoFlameToWasmParams(protoFlame) {
1456
- var _a;
1457
- return {
1458
- translation: protoFlame.translation || [0, 0, 0],
1459
- rotation: protoFlame.rotation || [0, 0, 0],
1460
- neck_pose: protoFlame.neckPose || [0, 0, 0],
1461
- jaw_pose: protoFlame.jawPose || [0, 0, 0],
1462
- eyes_pose: protoFlame.eyePose || [0, 0, 0, 0, 0, 0],
1463
- eyelid: protoFlame.eyeLid || [0, 0],
1464
- expr_params: protoFlame.expression || [],
1465
- shape_params: [],
1466
- // Realtime doesn't provide shape params, use default
1467
- has_eyelid: (((_a = protoFlame.eyeLid) == null ? void 0 : _a.length) || 0) > 0
1468
- };
1469
- }
1470
- function convertWasmParamsToProtoFlame(wasmParams) {
1471
- return {
1472
- translation: wasmParams.translation || [0, 0, 0],
1473
- rotation: wasmParams.rotation || [0, 0, 0],
1474
- neckPose: wasmParams.neck_pose || [0, 0, 0],
1475
- jawPose: wasmParams.jaw_pose || [0, 0, 0],
1476
- eyePose: wasmParams.eyes_pose || [0, 0, 0, 0, 0, 0],
1477
- eyeLid: wasmParams.eyelid || [0, 0],
1478
- expression: wasmParams.expr_params || []
1479
- };
1480
- }
1481
1481
  var t = "undefined" != typeof window ? window : void 0, i = "undefined" != typeof globalThis ? globalThis : t;
1482
1482
  "undefined" == typeof self && (i.self = i), "undefined" == typeof File && (i.File = function() {
1483
1483
  });
@@ -7943,7 +7943,8 @@ function getCommonFields() {
7943
7943
  return {
7944
7944
  platform: "Web",
7945
7945
  sdk_version: sdkVersion,
7946
- environment: currentEnvironment || "unknown",
7946
+ env: currentEnvironment || "unknown",
7947
+ // environment 缩写为 env
7947
7948
  app_id: logContext.app_id || "",
7948
7949
  user_id: logContext.user_id || "",
7949
7950
  // 没有就传空字符串
@@ -8138,7 +8139,6 @@ function cleanupPostHog() {
8138
8139
  }
8139
8140
  function logEvent(event, level = "info", contents = {}) {
8140
8141
  const context = {
8141
- environment: currentEnvironment || "unknown",
8142
8142
  sessionToken: idManager.getSessionToken() ?? "",
8143
8143
  userId: idManager.getUserId() ?? "",
8144
8144
  ...contents
@@ -8243,7 +8243,7 @@ const _AnimationPlayer = class _AnimationPlayer {
8243
8243
  if (this.streamingPlayer) {
8244
8244
  return;
8245
8245
  }
8246
- const { StreamingAudioPlayer } = await import("./StreamingAudioPlayer-BtOgYxcz.js");
8246
+ const { StreamingAudioPlayer } = await import("./StreamingAudioPlayer-BNj8LpDw.js");
8247
8247
  const { AvatarSDK: AvatarSDK2 } = await Promise.resolve().then(() => AvatarSDK$1);
8248
8248
  const audioFormat = AvatarSDK2.getAudioFormat();
8249
8249
  this.streamingPlayer = new StreamingAudioPlayer({
@@ -8442,6 +8442,39 @@ const _AnimationPlayer = class _AnimationPlayer {
8442
8442
  };
8443
8443
  __publicField(_AnimationPlayer, "audioUnlocked", false);
8444
8444
  let AnimationPlayer = _AnimationPlayer;
8445
+ const FLAME_FRAME_RATE = 25;
8446
+ const START_TRANSITION_DURATION_S = 0.2;
8447
+ const END_TRANSITION_DURATION_S = 1.6;
8448
+ const START_TRANSITION_DURATION_MS = START_TRANSITION_DURATION_S * 1e3;
8449
+ const END_TRANSITION_DURATION_MS = END_TRANSITION_DURATION_S * 1e3;
8450
+ const AUDIO_SAMPLE_RATE = 16e3;
8451
+ const AUDIO_CHANNELS = 1;
8452
+ const AUDIO_BYTES_PER_SAMPLE = 2;
8453
+ const AUDIO_BYTES_PER_SECOND = AUDIO_SAMPLE_RATE * AUDIO_CHANNELS * AUDIO_BYTES_PER_SAMPLE;
8454
+ const BEZIER_CURVES$1 = {
8455
+ /** 下颌: 快速启动,平滑停止 */
8456
+ jaw: [0.2, 0.8, 0.3, 1],
8457
+ /** 表情: 平滑 S 曲线 */
8458
+ expression: [0.4, 0, 0.2, 1],
8459
+ /** 眼部: 柔和 S 曲线 */
8460
+ eye: [0.3, 0, 0.1, 1],
8461
+ /** 颈部: 慢启动,惯性停止 */
8462
+ neck: [0.1, 0.2, 0.2, 1],
8463
+ /** 全局: 标准 ease-in-out */
8464
+ global: [0.42, 0, 0.58, 1]
8465
+ };
8466
+ const TIME_SCALE = {
8467
+ /** 下颌: 40% 时间完成 (1/2.5) */
8468
+ jaw: 2.5,
8469
+ /** 表情: 62.5% 时间完成 (1/1.6) */
8470
+ expression: 1.6,
8471
+ /** 眼部: 77% 时间完成 (1/1.3) */
8472
+ eye: 1.3,
8473
+ /** 颈部: 100% 时间完成 */
8474
+ neck: 1,
8475
+ /** 全局: 100% 时间完成 */
8476
+ global: 1
8477
+ };
8445
8478
  const DEFAULT_SDK_CONFIG = {
8446
8479
  [Environment.cn]: "https://api.open.spatialwalk.top",
8447
8480
  [Environment.intl]: "https://api.intl.spatialwalk.cloud"
@@ -8756,52 +8789,6 @@ class AvatarCoreMemoryManager {
8756
8789
  flatData.set(this.module.HEAPF32.subarray(floatOffset, floatOffset + totalFloats));
8757
8790
  return flatData;
8758
8791
  }
8759
- /**
8760
- * 🆕 读取 AvatarFaceGeometryArray 结构体数据 (WebGPU优化路径)
8761
- * 每个Face Geometry: center[3] + scale + quat[4] = 8 floats
8762
- */
8763
- readFaceGeometryArray(arrayPtr) {
8764
- if (!arrayPtr) {
8765
- throw new Error("Invalid face geometry array pointer");
8766
- }
8767
- const geometriesPtr = this.module.getValue(arrayPtr, "i32");
8768
- const geometryCount = this.module.getValue(arrayPtr + 4, "i32");
8769
- if (geometryCount === 0 || !geometriesPtr) {
8770
- return null;
8771
- }
8772
- const floatsPerGeometry = 8;
8773
- const totalFloats = geometryCount * floatsPerGeometry;
8774
- const floatOffset = geometriesPtr / 4;
8775
- return this.module.HEAPF32.subarray(floatOffset, floatOffset + totalFloats);
8776
- }
8777
- /**
8778
- * 🆕 读取 AvatarOriginalSplatArray 结构体数据 (WebGPU优化路径)
8779
- * 每个Original Splat: 15 floats + 1 int32 = 64 bytes
8780
- */
8781
- readOriginalSplatArray(arrayPtr) {
8782
- if (!arrayPtr) {
8783
- throw new Error("Invalid original splat array pointer");
8784
- }
8785
- const splatsPtr = this.module.getValue(arrayPtr, "i32");
8786
- const splatCount = this.module.getValue(arrayPtr + 4, "i32");
8787
- if (splatCount === 0 || !splatsPtr) {
8788
- return null;
8789
- }
8790
- const floatsPerSplat = 16;
8791
- const totalFloats = splatCount * floatsPerSplat;
8792
- const splatData = new Float32Array(totalFloats);
8793
- const startFloatOffset = splatsPtr / 4;
8794
- for (let i2 = 0; i2 < splatCount; i2++) {
8795
- const splatFloatOffset = startFloatOffset + i2 * 16;
8796
- for (let j2 = 0; j2 < 15; j2++) {
8797
- splatData[i2 * 16 + j2] = this.module.HEAPF32[splatFloatOffset + j2];
8798
- }
8799
- const bindingByteOffset = splatsPtr + i2 * 64 + 60;
8800
- const bindingInt = this.module.getValue(bindingByteOffset, "i32");
8801
- splatData[i2 * 16 + 15] = bindingInt;
8802
- }
8803
- return { data: splatData, count: splatCount };
8804
- }
8805
8792
  /**
8806
8793
  * 读取AvatarMeshData结构体数据
8807
8794
  */
@@ -9063,13 +9050,7 @@ class AvatarCoreAdapter {
9063
9050
  // core, character, x, y, z
9064
9051
  resetEyeTracking: this.wasmModule.cwrap("avatar_core_reset_eye_tracking", "number", ["number"]),
9065
9052
  // FLAME information query
9066
- getFlameInfo: this.wasmModule.cwrap("avatar_core_get_flame_info", "number", ["number", "number", "number", "number"]),
9067
- // 🆕 GPU 相关 API
9068
- computeFrameAsFaceGeometry: this.wasmModule.cwrap("avatar_core_compute_frame_as_face_geometry", "number", ["number", "number", "number", "number"]),
9069
- getOriginalSplats: this.wasmModule.cwrap("avatar_core_get_original_splats", "number", ["number", "number", "number"]),
9070
- getFLAMETemplateData: this.wasmModule.cwrap("avatar_core_get_flame_template_data", "number", ["number", "number", "number"]),
9071
- freeFaceGeometry: this.wasmModule.cwrap("avatar_core_free_face_geometry", null, ["number"]),
9072
- freeOriginalSplats: this.wasmModule.cwrap("avatar_core_free_original_splats", null, ["number"])
9053
+ getFlameInfo: this.wasmModule.cwrap("avatar_core_get_flame_info", "number", ["number", "number", "number", "number"])
9073
9054
  };
9074
9055
  }
9075
9056
  /**
@@ -9722,181 +9703,6 @@ class AvatarCoreAdapter {
9722
9703
  }
9723
9704
  return null;
9724
9705
  }
9725
- // ==================== 🆕 GPU 相关方法 ====================
9726
- /**
9727
- * 🆕 GPU 路径: 计算帧并返回 Face Geometry 数据
9728
- */
9729
- async computeFrameAsFaceGeometry(params) {
9730
- if (!this.isCharacterLoaded) {
9731
- throw new Error("Character not loaded");
9732
- }
9733
- let outputPtr = null;
9734
- let paramsPtr = null;
9735
- try {
9736
- const frameIndex = (params == null ? void 0 : params.frameIndex) ?? 0;
9737
- const characterId = params == null ? void 0 : params.characterId;
9738
- paramsPtr = await this.getAnimationFrameParams(frameIndex, characterId);
9739
- outputPtr = this.wasmModule._malloc(12);
9740
- const result2 = this.api.computeFrameAsFaceGeometry(
9741
- this.coreHandle,
9742
- this.characterHandle,
9743
- paramsPtr,
9744
- outputPtr
9745
- );
9746
- this.checkError(result2, "avatar_core_compute_frame_as_face_geometry");
9747
- return this.memoryManager.readFaceGeometryArray(outputPtr);
9748
- } catch (error) {
9749
- const errorMessage = error instanceof Error ? error.message : String(error);
9750
- logger.errorWithError("❌ computeFrameAsFaceGeometry failed:", errorMessage);
9751
- throw error;
9752
- } finally {
9753
- if (paramsPtr !== null) {
9754
- this.wasmModule._free(paramsPtr);
9755
- }
9756
- if (outputPtr !== null) {
9757
- this.api.freeFaceGeometry(outputPtr);
9758
- this.wasmModule._free(outputPtr);
9759
- }
9760
- }
9761
- }
9762
- /**
9763
- * 🆕 获取原始3DGS点数据 (一次性调用)
9764
- */
9765
- async getOriginalSplatsData() {
9766
- if (!this.isCharacterLoaded) {
9767
- throw new Error("Character not loaded");
9768
- }
9769
- let outputPtr = null;
9770
- try {
9771
- outputPtr = this.wasmModule._malloc(8);
9772
- const result2 = this.api.getOriginalSplats(
9773
- this.coreHandle,
9774
- this.characterHandle,
9775
- outputPtr
9776
- );
9777
- this.checkError(result2, "avatar_core_get_original_splats");
9778
- const splatData = this.memoryManager.readOriginalSplatArray(outputPtr);
9779
- if (splatData) {
9780
- logger.log(`✅ Loaded ${splatData.count} original splats for WebGPU (${(splatData.data.byteLength / 1024 / 1024).toFixed(2)} MB)`);
9781
- }
9782
- return splatData;
9783
- } catch (error) {
9784
- const errorMessage = error instanceof Error ? error.message : String(error);
9785
- logger.errorWithError("❌ getOriginalSplatsData failed:", errorMessage);
9786
- throw error;
9787
- } finally {
9788
- if (outputPtr !== null) {
9789
- this.api.freeOriginalSplats(outputPtr);
9790
- this.wasmModule._free(outputPtr);
9791
- }
9792
- }
9793
- }
9794
- /**
9795
- * 🆕 获取角色 Shape 参数
9796
- */
9797
- async getCharacterShapeParams(characterId) {
9798
- if (!this.isInitialized) {
9799
- throw new Error("Avatar Core not initialized");
9800
- }
9801
- const charHandle = characterId ? this.characterHandles.get(characterId) || null : this.characterHandle;
9802
- if (!charHandle) {
9803
- throw new Error("Character not loaded");
9804
- }
9805
- try {
9806
- const paramsPtr = this.wasmModule._malloc(300 * 4);
9807
- const result2 = this.api.getCharacterShapeParams(charHandle, paramsPtr);
9808
- this.checkError(result2, "avatar_core_get_character_shape_params");
9809
- const buffer = this.wasmModule.HEAPU8.buffer;
9810
- const params = Array.from(new Float32Array(buffer, paramsPtr, 300));
9811
- this.wasmModule._free(paramsPtr);
9812
- return { params };
9813
- } catch (error) {
9814
- logger.errorWithError("getCharacterShapeParams failed:", error);
9815
- throw error;
9816
- }
9817
- }
9818
- /**
9819
- * 🆕 获取 FLAME 模板数据(用于 GPU FLAME Pipeline)
9820
- */
9821
- async getFLAMETemplateData(characterId) {
9822
- if (!this.isInitialized) {
9823
- throw new Error("Avatar Core not initialized");
9824
- }
9825
- const characterHandle = characterId ? this.characterHandles.get(characterId) || null : this.characterHandle;
9826
- let structPtr = null;
9827
- try {
9828
- structPtr = this.wasmModule._malloc(64);
9829
- const result2 = this.api.getFLAMETemplateData(
9830
- this.coreHandle,
9831
- characterHandle || 0,
9832
- structPtr
9833
- );
9834
- this.checkError(result2, "avatar_core_get_flame_template_data");
9835
- const vTemplatePtr = this.wasmModule.getValue(structPtr, "i32");
9836
- const vertexCount = this.wasmModule.getValue(structPtr + 4, "i32");
9837
- const shapedirsPtr = this.wasmModule.getValue(structPtr + 8, "i32");
9838
- const shapeParamCount = this.wasmModule.getValue(structPtr + 12, "i32");
9839
- const posedirsPtr = this.wasmModule.getValue(structPtr + 16, "i32");
9840
- const poseParamCount = this.wasmModule.getValue(structPtr + 20, "i32");
9841
- const jRegressorPtr = this.wasmModule.getValue(structPtr + 24, "i32");
9842
- const jointCount = this.wasmModule.getValue(structPtr + 28, "i32");
9843
- const lbsWeightsPtr = this.wasmModule.getValue(structPtr + 32, "i32");
9844
- const parentsPtr = this.wasmModule.getValue(structPtr + 36, "i32");
9845
- const facesPtr = this.wasmModule.getValue(structPtr + 40, "i32");
9846
- const faceCount = this.wasmModule.getValue(structPtr + 44, "i32");
9847
- const staticOffsetPtr = this.wasmModule.getValue(structPtr + 48, "i32");
9848
- const staticOffsetCount = this.wasmModule.getValue(structPtr + 52, "i32");
9849
- const buffer = this.wasmModule.HEAPU8.buffer;
9850
- const vTemplate = new Float32Array(buffer, vTemplatePtr, vertexCount * 3).slice();
9851
- const shapedirs = new Float32Array(buffer, shapedirsPtr, vertexCount * 3 * shapeParamCount).slice();
9852
- const posedirs = new Float32Array(buffer, posedirsPtr, vertexCount * 3 * poseParamCount).slice();
9853
- const effectiveJointCount = jointCount > 0 ? jointCount : 5;
9854
- const jRegressor = new Float32Array(buffer, jRegressorPtr, effectiveJointCount * vertexCount).slice();
9855
- const lbsWeights = new Float32Array(buffer, lbsWeightsPtr, vertexCount * effectiveJointCount).slice();
9856
- const parents = new Int32Array(buffer, parentsPtr, effectiveJointCount).slice();
9857
- const faces = new Uint32Array(buffer, facesPtr, faceCount * 3).slice();
9858
- const staticOffset = staticOffsetPtr && staticOffsetCount > 0 ? new Float32Array(buffer, staticOffsetPtr, staticOffsetCount * 3).slice() : null;
9859
- const optimizedShapedirs = transposeBlendshapeData(shapedirs, vertexCount, shapeParamCount);
9860
- const optimizedPosedirs = transposeBlendshapeData(posedirs, vertexCount, poseParamCount);
9861
- logger.log(`FLAME template data retrieved (${((vTemplate.byteLength + optimizedShapedirs.byteLength + optimizedPosedirs.byteLength) / 1024 / 1024).toFixed(2)} MB)`);
9862
- return {
9863
- vTemplate,
9864
- vertexCount,
9865
- shapedirs: optimizedShapedirs,
9866
- shapeParamCount,
9867
- posedirs: optimizedPosedirs,
9868
- poseParamCount,
9869
- jRegressor,
9870
- jointCount: effectiveJointCount,
9871
- lbsWeights,
9872
- parents,
9873
- faces,
9874
- faceCount,
9875
- staticOffset,
9876
- staticOffsetCount
9877
- };
9878
- } catch (error) {
9879
- logger.errorWithError("getFLAMETemplateData failed:", error);
9880
- throw error;
9881
- } finally {
9882
- if (structPtr !== null) {
9883
- this.wasmModule._free(structPtr);
9884
- }
9885
- }
9886
- }
9887
- }
9888
- function transposeBlendshapeData(data, vertexCount, paramCount) {
9889
- const result2 = new Float32Array(data.length);
9890
- for (let p2 = 0; p2 < paramCount; p2++) {
9891
- for (let v2 = 0; v2 < vertexCount; v2++) {
9892
- for (let c2 = 0; c2 < 3; c2++) {
9893
- const srcIdx = v2 * 3 * paramCount + c2 * paramCount + p2;
9894
- const dstIdx = p2 * vertexCount * 3 + v2 * 3 + c2;
9895
- result2[dstIdx] = data[srcIdx];
9896
- }
9897
- }
9898
- }
9899
- return result2;
9900
9706
  }
9901
9707
  class AvatarSDK {
9902
9708
  /**
@@ -10163,7 +9969,7 @@ class AvatarSDK {
10163
9969
  }
10164
9970
  __publicField(AvatarSDK, "_isInitialized", false);
10165
9971
  __publicField(AvatarSDK, "_configuration", null);
10166
- __publicField(AvatarSDK, "_version", "1.0.0-beta.77");
9972
+ __publicField(AvatarSDK, "_version", "1.0.0-beta.78");
10167
9973
  __publicField(AvatarSDK, "_avatarCore", null);
10168
9974
  __publicField(AvatarSDK, "_dynamicSdkConfig", null);
10169
9975
  const AvatarSDK$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
@@ -12000,8 +11806,6 @@ class AvatarController {
12000
11806
  // Character handle for multi-character support
12001
11807
  __publicField(this, "characterId", null);
12002
11808
  // Character ID for multi-character support (used for eye tracking)
12003
- __publicField(this, "useGPUPath", false);
12004
- // 🆕 是否使用 GPU 路径(跳过 splatData 计算)
12005
11809
  // ========== Post-processing Configuration ==========
12006
11810
  __publicField(this, "postProcessingConfig", null);
12007
11811
  // ========== Playback Loop ==========
@@ -12043,7 +11847,7 @@ class AvatarController {
12043
11847
  recvFirstFlameTimestamp: 0,
12044
11848
  didRecvFirstFlame: false
12045
11849
  });
12046
- __publicField(this, "audioBytesPerSecond", 16e3 * 2);
11850
+ __publicField(this, "audioBytesPerSecond", AUDIO_BYTES_PER_SECOND);
12047
11851
  this.avatar = avatar;
12048
11852
  this.playbackMode = (options == null ? void 0 : options.playbackMode) ?? DrivingServiceMode.sdk;
12049
11853
  if (this.playbackMode === DrivingServiceMode.sdk) {
@@ -12630,19 +12434,13 @@ class AvatarController {
12630
12434
  }
12631
12435
  /**
12632
12436
  * Set render callback (called by AvatarView)
12633
- * @param callback 渲染回调函数
12634
- * @param characterHandle 角色句柄
12635
- * @param useGPUPath 是否使用 GPU 路径(跳过 splatData 计算)
12636
12437
  * @internal
12637
12438
  */
12638
- setRenderCallback(callback, characterHandle, useGPUPath) {
12439
+ setRenderCallback(callback, characterHandle) {
12639
12440
  this.renderCallback = callback;
12640
12441
  if (characterHandle !== void 0) {
12641
12442
  this.characterHandle = characterHandle;
12642
12443
  }
12643
- if (useGPUPath !== void 0) {
12644
- this.useGPUPath = useGPUPath;
12645
- }
12646
12444
  }
12647
12445
  /**
12648
12446
  * Set character ID (for multi-character support, used for eye tracking)
@@ -12956,7 +12754,7 @@ class AvatarController {
12956
12754
  if (this.playbackLoopId) {
12957
12755
  return;
12958
12756
  }
12959
- const fps = APP_CONFIG.animation.fps;
12757
+ const fps = FLAME_FRAME_RATE;
12960
12758
  const playLoop = async () => {
12961
12759
  if (!this.isPlaying || this.currentState === AvatarState.paused || !this.animationPlayer) {
12962
12760
  this.playbackLoopId = null;
@@ -13019,22 +12817,16 @@ class AvatarController {
13019
12817
  }
13020
12818
  }
13021
12819
  if (arrayIndex >= 0 && arrayIndex < this.currentKeyframes.length) {
13022
- if (this.useGPUPath) {
13023
- if (this.renderCallback) {
13024
- this.renderCallback(new Float32Array(0), frameIndex);
13025
- }
13026
- } else {
13027
- const currentFrame = this.currentKeyframes[arrayIndex];
13028
- let wasmParams = convertProtoFlameToWasmParams(currentFrame);
13029
- if (this.postProcessingConfig) {
13030
- wasmParams = this.applyPostProcessingToParams(wasmParams);
13031
- }
13032
- const avatarCore = AvatarSDK.getAvatarCore();
13033
- if (avatarCore) {
13034
- const splatData = await avatarCore.computeFrameFlatFromParams(wasmParams, this.characterHandle ?? void 0);
13035
- if (splatData && this.renderCallback) {
13036
- this.renderCallback(splatData, frameIndex);
13037
- }
12820
+ const currentFrame = this.currentKeyframes[arrayIndex];
12821
+ let wasmParams = convertProtoFlameToWasmParams(currentFrame);
12822
+ if (this.postProcessingConfig) {
12823
+ wasmParams = this.applyPostProcessingToParams(wasmParams);
12824
+ }
12825
+ const avatarCore = AvatarSDK.getAvatarCore();
12826
+ if (avatarCore) {
12827
+ const splatData = await avatarCore.computeFrameFlatFromParams(wasmParams, this.characterHandle ?? void 0);
12828
+ if (splatData && this.renderCallback) {
12829
+ this.renderCallback(splatData, frameIndex);
13038
12830
  }
13039
12831
  }
13040
12832
  }
@@ -13532,47 +13324,71 @@ function getCacheInfo(url, response) {
13532
13324
  };
13533
13325
  }
13534
13326
  async function downloadResource(url, options) {
13327
+ const { signal, characterId, resourceType, maxRetries = 3 } = options || {};
13328
+ if (signal == null ? void 0 : signal.aborted) {
13329
+ throw new Error("Download cancelled");
13330
+ }
13535
13331
  try {
13536
13332
  let cached = null;
13537
13333
  let pwaCacheSubtype = void 0;
13538
- if (options == null ? void 0 : options.characterId) {
13539
- cached = await PwaCacheManager.getCharacterResource(options.characterId, url);
13334
+ if (characterId) {
13335
+ cached = await PwaCacheManager.getCharacterResource(characterId, url);
13540
13336
  if (cached) {
13541
13337
  pwaCacheSubtype = "character";
13542
13338
  }
13543
- } else if ((options == null ? void 0 : options.resourceType) === "template") {
13339
+ } else if (resourceType === "template") {
13544
13340
  cached = await PwaCacheManager.getTemplateResource(url);
13545
13341
  if (cached) {
13546
13342
  pwaCacheSubtype = "template";
13547
13343
  }
13548
13344
  }
13549
13345
  if (cached) {
13550
- const response2 = new Response(cached);
13346
+ const response = new Response(cached);
13551
13347
  await new Promise((resolve2) => setTimeout(resolve2, 0));
13552
- const cacheInfo2 = getCacheInfo(url, response2);
13553
- cacheInfo2.cacheHit = true;
13554
- cacheInfo2.cacheType = "pwa";
13555
- cacheInfo2.pwaCacheSubtype = pwaCacheSubtype;
13556
- return { data: cached, cacheInfo: cacheInfo2 };
13557
- }
13558
- const response = await fetch(url);
13559
- if (!response.ok) {
13560
- throw new Error(`HTTP ${response.status} ${response.statusText}`);
13561
- }
13562
- const arrayBuffer = await response.arrayBuffer();
13563
- if (options == null ? void 0 : options.characterId) {
13564
- PwaCacheManager.putCharacterResource(options.characterId, url, arrayBuffer).catch((err) => {
13565
- logger.warn(`[downloadResource] Failed to cache character resource:`, err);
13566
- });
13567
- } else if ((options == null ? void 0 : options.resourceType) === "template") {
13568
- PwaCacheManager.putTemplateResource(url, arrayBuffer).catch((err) => {
13569
- logger.warn(`[downloadResource] Failed to cache template resource:`, err);
13570
- });
13348
+ const cacheInfo = getCacheInfo(url, response);
13349
+ cacheInfo.cacheHit = true;
13350
+ cacheInfo.cacheType = "pwa";
13351
+ cacheInfo.pwaCacheSubtype = pwaCacheSubtype;
13352
+ return { data: cached, cacheInfo };
13353
+ }
13354
+ let lastError = null;
13355
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
13356
+ if (signal == null ? void 0 : signal.aborted) {
13357
+ throw new Error("Download cancelled");
13358
+ }
13359
+ try {
13360
+ const response = await fetch(url, { signal });
13361
+ if (!response.ok) {
13362
+ throw new Error(`HTTP ${response.status} ${response.statusText}`);
13363
+ }
13364
+ const arrayBuffer = await response.arrayBuffer();
13365
+ if (characterId) {
13366
+ PwaCacheManager.putCharacterResource(characterId, url, arrayBuffer).catch((err) => {
13367
+ logger.warn(`[downloadResource] Failed to cache character resource:`, err);
13368
+ });
13369
+ } else if (resourceType === "template") {
13370
+ PwaCacheManager.putTemplateResource(url, arrayBuffer).catch((err) => {
13371
+ logger.warn(`[downloadResource] Failed to cache template resource:`, err);
13372
+ });
13373
+ }
13374
+ await new Promise((resolve2) => setTimeout(resolve2, 0));
13375
+ const cacheInfo = getCacheInfo(url, response);
13376
+ return { data: arrayBuffer, cacheInfo };
13377
+ } catch (err) {
13378
+ if (err instanceof Error && (err.name === "AbortError" || err.message === "Download cancelled")) {
13379
+ throw err;
13380
+ }
13381
+ lastError = err instanceof Error ? err : new Error(String(err));
13382
+ if (attempt < maxRetries) {
13383
+ logger.warn(`[downloadResource] Attempt ${attempt}/${maxRetries} failed for ${url}, retrying immediately...`);
13384
+ }
13385
+ }
13571
13386
  }
13572
- await new Promise((resolve2) => setTimeout(resolve2, 0));
13573
- const cacheInfo = getCacheInfo(url, response);
13574
- return { data: arrayBuffer, cacheInfo };
13387
+ throw lastError || new Error(`Failed to download ${url} after ${maxRetries} attempts`);
13575
13388
  } catch (err) {
13389
+ if (err instanceof Error && (err.name === "AbortError" || err.message === "Download cancelled")) {
13390
+ throw err;
13391
+ }
13576
13392
  const msg = errorToMessage(err);
13577
13393
  throw new Error(`[downloadResource] ${url} → ${msg}`);
13578
13394
  }
@@ -13718,16 +13534,21 @@ class AvatarDownloader {
13718
13534
  * Load camera settings from CharacterMeta (optional)
13719
13535
  * @internal
13720
13536
  */
13721
- async loadCameraSettings(characterMeta) {
13537
+ async loadCameraSettings(characterMeta, options) {
13722
13538
  var _a, _b;
13539
+ const { signal } = options || {};
13723
13540
  const cameraUrl = (_b = (_a = characterMeta.camera) == null ? void 0 : _a.resource) == null ? void 0 : _b.remote;
13724
13541
  if (!cameraUrl) {
13725
13542
  logger.log("ℹ️ No camera resource URL provided");
13726
13543
  return void 0;
13727
13544
  }
13545
+ if (signal == null ? void 0 : signal.aborted) {
13546
+ throw new Error("Load cancelled");
13547
+ }
13728
13548
  try {
13729
13549
  logger.log(`📥 Loading camera info from: ${cameraUrl}`);
13730
13550
  const { data: arrayBuffer } = await downloadResource(cameraUrl, {
13551
+ signal,
13731
13552
  characterId: characterMeta.characterId ?? void 0,
13732
13553
  resourceType: "character"
13733
13554
  });
@@ -13746,7 +13567,10 @@ class AvatarDownloader {
13746
13567
  */
13747
13568
  async loadCharacterData(characterMeta, options) {
13748
13569
  var _a, _b, _c, _d, _e2, _f, _g, _h, _i2, _j;
13749
- const { progressCallback = null } = options || {};
13570
+ const { progressCallback = null, signal } = options || {};
13571
+ if (signal == null ? void 0 : signal.aborted) {
13572
+ throw new Error("Download cancelled");
13573
+ }
13750
13574
  const totalStartTime = Date.now();
13751
13575
  const shapeUrl = (_c = (_b = (_a = characterMeta.models) == null ? void 0 : _a.shape) == null ? void 0 : _b.resource) == null ? void 0 : _c.remote;
13752
13576
  const pointCloudUrl = (_f = (_e2 = (_d = characterMeta.models) == null ? void 0 : _d.gsStandard) == null ? void 0 : _e2.resource) == null ? void 0 : _f.remote;
@@ -13789,6 +13613,7 @@ class AvatarDownloader {
13789
13613
  updateProgress(filename, false);
13790
13614
  try {
13791
13615
  const { data: arrayBuffer, cacheInfo } = await downloadResource(url, {
13616
+ signal,
13792
13617
  characterId: characterMeta.characterId ?? void 0,
13793
13618
  resourceType: "character"
13794
13619
  });
@@ -13803,6 +13628,9 @@ class AvatarDownloader {
13803
13628
  updateProgress(filename, true);
13804
13629
  return { key, success: true, size: arrayBuffer.byteLength };
13805
13630
  } catch (error) {
13631
+ if (error instanceof Error && (error.name === "AbortError" || error.message === "Download cancelled")) {
13632
+ throw error;
13633
+ }
13806
13634
  if (!optional) {
13807
13635
  const errorMessage = error instanceof Error ? error.message : String(error);
13808
13636
  logEvent("download_avatar_assets_failed", "error", {
@@ -13835,9 +13663,12 @@ class AvatarDownloader {
13835
13663
  const totalSize = Object.values(characterData).reduce((sum, buffer) => {
13836
13664
  return sum + (buffer ? buffer.byteLength : 0);
13837
13665
  }, 0);
13838
- logEvent("download_avatar_assets_total_measure", "info", {
13666
+ logEvent("download_avatar_assets_latency", "info", {
13667
+ resolution: "default",
13668
+ // 目前写死 default
13839
13669
  avatar_id: characterMeta.characterId ?? "unknown",
13840
- total_duration: totalDuration,
13670
+ duration: totalDuration,
13671
+ // 保留其他有用的字段
13841
13672
  parallel_duration: parallelDuration,
13842
13673
  total_size: totalSize,
13843
13674
  file_count: filesToLoad.length,
@@ -13851,9 +13682,13 @@ class AvatarDownloader {
13851
13682
  * @internal
13852
13683
  */
13853
13684
  async preloadResources(characterMeta, options) {
13854
- const { progressCallback = null } = options || {};
13685
+ const { progressCallback = null, signal } = options || {};
13686
+ if (signal == null ? void 0 : signal.aborted) {
13687
+ throw new Error("Preload cancelled");
13688
+ }
13855
13689
  const [characterData, preloadCameraSettings] = await Promise.all([
13856
13690
  this.loadCharacterData(characterMeta, {
13691
+ signal,
13857
13692
  progressCallback: (info) => {
13858
13693
  if (progressCallback) {
13859
13694
  progressCallback({
@@ -13863,7 +13698,7 @@ class AvatarDownloader {
13863
13698
  }
13864
13699
  }
13865
13700
  }),
13866
- this.loadCameraSettings(characterMeta)
13701
+ this.loadCameraSettings(characterMeta, { signal })
13867
13702
  ]);
13868
13703
  return {
13869
13704
  characterData,
@@ -13897,7 +13732,8 @@ class AvatarDownloader {
13897
13732
  ...headers,
13898
13733
  ...options.headers
13899
13734
  },
13900
- body: options.body ? JSON.stringify(options.body) : void 0
13735
+ body: options.body ? JSON.stringify(options.body) : void 0,
13736
+ signal: options.signal
13901
13737
  });
13902
13738
  if (!response.ok) {
13903
13739
  let error;
@@ -13930,16 +13766,21 @@ class AvatarDownloader {
13930
13766
  * Returns CharacterMeta with nested resource structure
13931
13767
  * @internal
13932
13768
  */
13933
- async getCharacterById(characterId) {
13769
+ async getCharacterById(characterId, options) {
13934
13770
  var _a;
13771
+ const { signal } = options || {};
13935
13772
  const startTime = Date.now();
13936
13773
  try {
13774
+ if (signal == null ? void 0 : signal.aborted) {
13775
+ throw new Error("Request cancelled");
13776
+ }
13937
13777
  const client = this.getSdkApiClient();
13938
13778
  const response = await client.request(`/v2/character/${characterId}`, {
13939
- method: "GET"
13779
+ method: "GET",
13780
+ signal
13940
13781
  });
13941
13782
  const duration = Date.now() - startTime;
13942
- logEvent("fetch_avatar_metadata_measure", "info", {
13783
+ logEvent("fetch_avatar_metadata_latency", "info", {
13943
13784
  avatar_id: characterId,
13944
13785
  duration
13945
13786
  });
@@ -13971,10 +13812,12 @@ const _AvatarManager = class _AvatarManager {
13971
13812
  constructor() {
13972
13813
  __publicField(this, "avatarDownloader", null);
13973
13814
  __publicField(this, "avatarCache", /* @__PURE__ */ new Map());
13974
- __publicField(this, "loadingPromises", /* @__PURE__ */ new Map());
13975
- // 下载队列:确保资源下载串行执行
13815
+ /** 下载队列:FIFO 顺序 */
13976
13816
  __publicField(this, "downloadQueue", []);
13977
- __publicField(this, "isDownloading", false);
13817
+ /** 当前正在执行的任务 */
13818
+ __publicField(this, "currentTask", null);
13819
+ /** 任务索引:快速查找 id 对应的任务 */
13820
+ __publicField(this, "taskIndex", /* @__PURE__ */ new Map());
13978
13821
  }
13979
13822
  /**
13980
13823
  * Access via global singleton
@@ -13992,28 +13835,139 @@ const _AvatarManager = class _AvatarManager {
13992
13835
  * @returns Promise<Avatar>
13993
13836
  */
13994
13837
  async load(id, onProgress) {
13995
- const loadingPromise = this.loadingPromises.get(id);
13996
- if (loadingPromise) {
13997
- logger.log(`[AvatarManager] Avatar ${id} is already loading, reusing promise`);
13998
- return loadingPromise;
13999
- }
14000
13838
  if (!AvatarSDK.isInitialized) {
14001
13839
  throw new Error("AvatarSDK not initialized. Please call AvatarSDK.initialize() first.");
14002
13840
  }
14003
13841
  if (!this.avatarDownloader) {
14004
13842
  this.avatarDownloader = new AvatarDownloader();
14005
13843
  }
14006
- logger.log(`[AvatarManager] Fetching latest metadata for avatar ${id}...`);
14007
- onProgress == null ? void 0 : onProgress({ type: LoadProgress.downloading, progress: 5 });
14008
- const newCharacterMeta = await this.avatarDownloader.getCharacterById(id);
13844
+ const existingTask = this.taskIndex.get(id);
13845
+ if (existingTask && existingTask.status !== "completed" && existingTask.status !== "failed" && existingTask.status !== "cancelled") {
13846
+ logger.log(`[AvatarManager] Avatar ${id} already in queue/loading, adding callback to existing task`);
13847
+ onProgress == null ? void 0 : onProgress({ type: LoadProgress.downloading, progress: 0 });
13848
+ return new Promise((resolve2, reject) => {
13849
+ existingTask.requests.push({ onProgress, resolve: resolve2, reject });
13850
+ });
13851
+ }
13852
+ logger.log(`[AvatarManager] Queuing new avatar download for id: ${id}`);
13853
+ let taskResolve;
13854
+ let taskReject;
13855
+ const taskPromise = new Promise((resolve2, reject) => {
13856
+ taskResolve = resolve2;
13857
+ taskReject = reject;
13858
+ });
13859
+ const task = {
13860
+ id,
13861
+ status: "queued",
13862
+ requests: [{ onProgress, resolve: taskResolve, reject: taskReject }],
13863
+ abortController: new AbortController(),
13864
+ promise: taskPromise,
13865
+ _resolve: taskResolve,
13866
+ _reject: taskReject
13867
+ };
13868
+ onProgress == null ? void 0 : onProgress({ type: LoadProgress.downloading, progress: 0 });
13869
+ this.downloadQueue.push(task);
13870
+ this.taskIndex.set(id, task);
13871
+ this.processQueue();
13872
+ return taskPromise;
13873
+ }
13874
+ /**
13875
+ * Cancel a pending or running download task
13876
+ * @param id Avatar ID to cancel
13877
+ * @returns true if task was found and cancelled
13878
+ */
13879
+ cancelLoad(id) {
13880
+ const task = this.taskIndex.get(id);
13881
+ if (!task) {
13882
+ logger.log(`[AvatarManager] No task found for id: ${id}`);
13883
+ return false;
13884
+ }
13885
+ if (task.status === "completed" || task.status === "failed" || task.status === "cancelled") {
13886
+ logger.log(`[AvatarManager] Task ${id} already finished with status: ${task.status}`);
13887
+ return false;
13888
+ }
13889
+ logger.log(`[AvatarManager] Cancelling task for id: ${id}`);
13890
+ task.abortController.abort();
13891
+ task.status = "cancelled";
13892
+ const cancelError = new Error(`Download cancelled for avatar: ${id}`);
13893
+ this.notifyAllRequests(task, { type: LoadProgress.failed, error: cancelError });
13894
+ task.requests.forEach((req) => req.reject(cancelError));
13895
+ this.taskIndex.delete(id);
13896
+ const queueIndex = this.downloadQueue.indexOf(task);
13897
+ if (queueIndex !== -1) {
13898
+ this.downloadQueue.splice(queueIndex, 1);
13899
+ }
13900
+ if (this.currentTask === task) {
13901
+ this.currentTask = null;
13902
+ this.processQueue();
13903
+ }
13904
+ return true;
13905
+ }
13906
+ /**
13907
+ * Process download queue (FIFO)
13908
+ * @internal
13909
+ */
13910
+ async processQueue() {
13911
+ var _a, _b;
13912
+ if (this.currentTask || this.downloadQueue.length === 0) {
13913
+ return;
13914
+ }
13915
+ const task = this.downloadQueue.shift();
13916
+ this.currentTask = task;
13917
+ logger.log(`[AvatarManager] Processing task: ${task.id} (${this.downloadQueue.length} items remaining)`);
13918
+ try {
13919
+ const avatar = await this.executeTask(task);
13920
+ if (task.status === "cancelled") {
13921
+ return;
13922
+ }
13923
+ task.status = "completed";
13924
+ this.notifyAllRequests(task, { type: LoadProgress.completed });
13925
+ task.requests.forEach((req) => req.resolve(avatar));
13926
+ (_a = task._resolve) == null ? void 0 : _a.call(task, avatar);
13927
+ } catch (error) {
13928
+ if (task.status === "cancelled") {
13929
+ return;
13930
+ }
13931
+ task.status = "failed";
13932
+ const err = error instanceof Error ? error : new Error(String(error));
13933
+ this.notifyAllRequests(task, { type: LoadProgress.failed, error: err });
13934
+ task.requests.forEach((req) => req.reject(err));
13935
+ (_b = task._reject) == null ? void 0 : _b.call(task, err);
13936
+ } finally {
13937
+ if (task.status !== "cancelled") {
13938
+ this.taskIndex.delete(task.id);
13939
+ }
13940
+ this.currentTask = null;
13941
+ this.processQueue();
13942
+ }
13943
+ }
13944
+ /**
13945
+ * Execute a single download task
13946
+ * @internal
13947
+ */
13948
+ async executeTask(task) {
13949
+ const { id, abortController } = task;
13950
+ const signal = abortController.signal;
13951
+ const startTime = Date.now();
13952
+ if (signal.aborted) {
13953
+ throw new Error("Task cancelled");
13954
+ }
13955
+ task.status = "fetching-meta";
13956
+ logger.log(`[AvatarManager] Fetching metadata for avatar ${id}...`);
13957
+ const characterMeta = await this.avatarDownloader.getCharacterById(id, { signal });
13958
+ task.characterMeta = characterMeta;
13959
+ if (signal.aborted) {
13960
+ throw new Error("Task cancelled");
13961
+ }
14009
13962
  const cached = this.avatarCache.get(id);
14010
13963
  if (cached) {
14011
13964
  const cachedMeta = cached.getCharacterMeta();
14012
13965
  const cachedVersion = cachedMeta.version;
14013
- const newVersion = newCharacterMeta.version;
13966
+ const newVersion = characterMeta.version;
14014
13967
  if (cachedVersion === newVersion) {
14015
- logger.log(`[AvatarManager] Avatar ${id} found in cache with same version (${cachedVersion}), updating meta and returning cached`);
14016
- cached.updateCharacterMeta(newCharacterMeta);
13968
+ logger.log(`[AvatarManager] Avatar ${id} found in cache with same version (${cachedVersion}), returning cached`);
13969
+ cached.updateCharacterMeta(characterMeta);
13970
+ this.notifyAllRequests(task, { type: LoadProgress.downloading, progress: 100 });
14017
13971
  return cached;
14018
13972
  } else {
14019
13973
  logger.log(`[AvatarManager] Avatar ${id} version mismatch: cached=${cachedVersion}, new=${newVersion}, reloading...`);
@@ -14022,77 +13976,71 @@ const _AvatarManager = class _AvatarManager {
14022
13976
  await PwaCacheManager2.clearCharacterCache(id);
14023
13977
  }
14024
13978
  }
14025
- logger.log(`[AvatarManager] Queuing avatar download for id: ${id}`);
14026
- const loadPromise = new Promise((resolve2, reject) => {
14027
- this.downloadQueue.push({
14028
- id,
14029
- characterMeta: newCharacterMeta,
14030
- onProgress,
14031
- resolve: resolve2,
14032
- reject
14033
- });
13979
+ const totalAssets = this.countTotalAssets(characterMeta);
13980
+ const metaProgress = Math.round(1 / (1 + totalAssets) * 100);
13981
+ this.notifyAllRequests(task, { type: LoadProgress.downloading, progress: metaProgress });
13982
+ if (signal.aborted) {
13983
+ throw new Error("Task cancelled");
13984
+ }
13985
+ task.status = "downloading";
13986
+ logger.log("[AvatarManager] Downloading resources...");
13987
+ let downloadedCount = 0;
13988
+ const resources = await this.avatarDownloader.preloadResources(characterMeta, {
13989
+ signal,
13990
+ progressCallback: (info) => {
13991
+ if (info.loaded > downloadedCount) {
13992
+ downloadedCount = info.loaded;
13993
+ const progress = Math.round((1 + downloadedCount) / (1 + totalAssets) * 100);
13994
+ this.notifyAllRequests(task, { type: LoadProgress.downloading, progress });
13995
+ }
13996
+ }
13997
+ });
13998
+ if (signal.aborted) {
13999
+ throw new Error("Task cancelled");
14000
+ }
14001
+ logger.log("[AvatarManager] Creating Avatar instance...");
14002
+ const avatar = new Avatar(id, characterMeta, resources);
14003
+ this.avatarCache.set(id, avatar);
14004
+ logger.log("[AvatarManager] Avatar loaded successfully");
14005
+ const totalDuration = Date.now() - startTime;
14006
+ logEvent("fetch_avatar_latency", "info", {
14007
+ resolution: "default",
14008
+ // 目前写死 default
14009
+ avatar_id: id,
14010
+ duration: totalDuration
14034
14011
  });
14035
- this.loadingPromises.set(id, loadPromise);
14036
- loadPromise.finally(() => {
14037
- this.loadingPromises.delete(id);
14012
+ logEvent("character_load", "info", {
14013
+ avatar_id: id,
14014
+ event: "load_success"
14038
14015
  });
14039
- this.processDownloadQueue();
14040
- return loadPromise;
14016
+ return avatar;
14041
14017
  }
14042
14018
  /**
14043
- * Process download queue (ensure serial execution)
14019
+ * 计算需要下载的资源总数
14044
14020
  * @internal
14045
14021
  */
14046
- async processDownloadQueue() {
14047
- if (this.isDownloading || this.downloadQueue.length === 0) {
14048
- return;
14049
- }
14050
- this.isDownloading = true;
14051
- const item = this.downloadQueue.shift();
14052
- logger.log(`[AvatarManager] Processing download queue item: ${item.id} (${this.downloadQueue.length} items remaining)`);
14053
- try {
14054
- const avatar = await this.doLoad(item.id, item.characterMeta, item.onProgress);
14055
- item.resolve(avatar);
14056
- } catch (error) {
14057
- item.reject(error instanceof Error ? error : new Error(String(error)));
14058
- } finally {
14059
- this.isDownloading = false;
14060
- this.processDownloadQueue();
14061
- }
14022
+ countTotalAssets(meta) {
14023
+ var _a, _b, _c, _d, _e2, _f, _g, _h, _i2, _j, _k;
14024
+ let count = 0;
14025
+ if ((_c = (_b = (_a = meta.models) == null ? void 0 : _a.shape) == null ? void 0 : _b.resource) == null ? void 0 : _c.remote) count++;
14026
+ if ((_f = (_e2 = (_d = meta.models) == null ? void 0 : _d.gsStandard) == null ? void 0 : _e2.resource) == null ? void 0 : _f.remote) count++;
14027
+ if ((_i2 = (_h = (_g = meta.animations) == null ? void 0 : _g.frameIdle) == null ? void 0 : _h.resource) == null ? void 0 : _i2.remote) count++;
14028
+ if ((_k = (_j = meta.camera) == null ? void 0 : _j.resource) == null ? void 0 : _k.remote) count++;
14029
+ return count;
14062
14030
  }
14063
14031
  /**
14064
- * Execute actual loading logic (private method)
14032
+ * 通知所有请求者进度更新
14065
14033
  * @internal
14066
14034
  */
14067
- async doLoad(id, characterMeta, onProgress) {
14068
- try {
14069
- logger.log("[AvatarManager] Step 1: Downloading resources...");
14070
- onProgress == null ? void 0 : onProgress({ type: LoadProgress.downloading, progress: 30 });
14071
- const resources = await this.avatarDownloader.preloadResources(characterMeta, {
14072
- progressCallback: (info) => {
14073
- const externalProgress = 30 + info.progress / 100 * 70;
14074
- onProgress == null ? void 0 : onProgress({
14075
- type: LoadProgress.downloading,
14076
- progress: Math.round(externalProgress)
14077
- });
14078
- }
14079
- });
14080
- logger.log("[AvatarManager] Step 2: Creating Avatar instance...");
14081
- const avatar = new Avatar(id, characterMeta, resources);
14082
- this.avatarCache.set(id, avatar);
14083
- logger.log("[AvatarManager] Avatar loaded successfully");
14084
- onProgress == null ? void 0 : onProgress({ type: LoadProgress.completed });
14085
- logEvent("character_load", "info", {
14086
- avatar_id: id,
14087
- event: "load_success"
14088
- });
14089
- return avatar;
14090
- } catch (error) {
14091
- const message = error instanceof Error ? error.message : String(error);
14092
- logger.error("Failed to load avatar:", message);
14093
- onProgress == null ? void 0 : onProgress({ type: LoadProgress.failed, error });
14094
- throw error;
14095
- }
14035
+ notifyAllRequests(task, progress) {
14036
+ task.requests.forEach((req) => {
14037
+ var _a;
14038
+ try {
14039
+ (_a = req.onProgress) == null ? void 0 : _a.call(req, progress);
14040
+ } catch (err) {
14041
+ logger.warn(`[AvatarManager] Error in progress callback:`, err);
14042
+ }
14043
+ });
14096
14044
  }
14097
14045
  /**
14098
14046
  * Get cached avatar
@@ -14107,22 +14055,58 @@ const _AvatarManager = class _AvatarManager {
14107
14055
  * @param id Avatar ID
14108
14056
  */
14109
14057
  clear(id) {
14058
+ this.cancelLoad(id);
14110
14059
  const removed = this.avatarCache.delete(id);
14111
14060
  if (removed) {
14112
14061
  logger.log(`[AvatarManager] Cleared avatar cache for id: ${id}`);
14113
14062
  }
14114
14063
  }
14115
14064
  /**
14116
- * Clear all avatar cache and resource loader cache
14065
+ * Clear all avatar cache and cancel all tasks
14117
14066
  */
14118
14067
  clearAll() {
14068
+ for (const task of this.taskIndex.values()) {
14069
+ if (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
14070
+ this.cancelLoad(task.id);
14071
+ }
14072
+ }
14119
14073
  this.avatarCache.clear();
14120
- logger.log("[AvatarManager] Cleared all avatar cache");
14074
+ this.downloadQueue = [];
14075
+ this.taskIndex.clear();
14076
+ this.currentTask = null;
14077
+ logger.log("[AvatarManager] Cleared all avatar cache and cancelled all tasks");
14121
14078
  }
14122
- };
14123
- __publicField(_AvatarManager, "_instance", null);
14124
- let AvatarManager = _AvatarManager;
14125
- let depthBuffer = null;
14079
+ /**
14080
+ * Get the number of pending tasks in the queue
14081
+ * @internal For testing purposes
14082
+ */
14083
+ get pendingTaskCount() {
14084
+ return this.downloadQueue.length + (this.currentTask ? 1 : 0);
14085
+ }
14086
+ /**
14087
+ * Check if a task is in progress for the given ID
14088
+ * @internal For testing purposes
14089
+ */
14090
+ isLoading(id) {
14091
+ const task = this.taskIndex.get(id);
14092
+ return task !== void 0 && task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled";
14093
+ }
14094
+ /**
14095
+ * @internal For testing - get loadingPromises map (compatibility)
14096
+ */
14097
+ get loadingPromises() {
14098
+ const map = /* @__PURE__ */ new Map();
14099
+ for (const [id, task] of this.taskIndex) {
14100
+ if (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
14101
+ map.set(id, task.promise);
14102
+ }
14103
+ }
14104
+ return map;
14105
+ }
14106
+ };
14107
+ __publicField(_AvatarManager, "_instance", null);
14108
+ let AvatarManager = _AvatarManager;
14109
+ let depthBuffer = null;
14126
14110
  let depthInt = null;
14127
14111
  let indices0 = null;
14128
14112
  let indices1 = null;
@@ -14771,1589 +14755,6 @@ class WebGLRenderer {
14771
14755
  }
14772
14756
  const renderShaderCode = "/**\n * WebGPU 3DGS 渲染着色器\n *\n * 实例化渲染:每个 splat 绘制一个四边形\n * 对应 WebGL 版本的 GLSL 着色器\n */\n\n// ============ Uniform Bindings ============\n\nstruct Uniforms {\n viewMatrix: mat4x4f,\n projectionMatrix: mat4x4f,\n screenSize: vec2f,\n enableFrustumCulling: u32,\n}\n\n@group(0) @binding(0) var<uniform> uniforms: Uniforms;\n\n// ============ Storage Buffer Bindings (间接索引渲染) ============\n\n@group(1) @binding(0) var<storage, read> sortIndices: array<u32>;\n@group(1) @binding(1) var<storage, read> splatData: array<f32>;\n\n// ============ Vertex Shader ============\n\nstruct VertexInput {\n // 共享四边形顶点 (per-vertex)\n @location(0) quadVertex: vec2f,\n}\n\nstruct VertexOutput {\n @builtin(position) position: vec4f,\n @location(0) relativePosition: vec2f,\n @location(1) color: vec4f,\n}\n\n// 常量定义\nconst BOUNDS_RADIUS: f32 = 3.0;\n\n/**\n * 计算2D协方差矩阵(复刻 WebGL 版本)\n */\nfn calcCovariance2D(\n viewPos: vec3f,\n cov3Da: vec3f,\n cov3Db: vec3f,\n viewMatrix: mat4x4f,\n projectionMatrix: mat4x4f,\n screenSize: vec2f\n) -> vec3f {\n let invViewPosZ = 1.0 / viewPos.z;\n let invViewPosZSquared = invViewPosZ * invViewPosZ;\n\n // FOV 限制\n let tanHalfFovX = 1.0 / projectionMatrix[0][0];\n let tanHalfFovY = 1.0 / projectionMatrix[1][1];\n let limX = 1.3 * tanHalfFovX;\n let limY = 1.3 * tanHalfFovY;\n\n var clampedViewPos = viewPos;\n clampedViewPos.x = clamp(viewPos.x * invViewPosZ, -limX, limX) * viewPos.z;\n clampedViewPos.y = clamp(viewPos.y * invViewPosZ, -limY, limY) * viewPos.z;\n\n // 焦距计算\n let focalX = screenSize.x * projectionMatrix[0][0] / 2.0;\n let focalY = screenSize.y * projectionMatrix[1][1] / 2.0;\n\n // 雅可比矩阵 J\n let J = mat3x3f(\n focalX * invViewPosZ, 0.0, -(focalX * clampedViewPos.x) * invViewPosZSquared,\n 0.0, focalY * invViewPosZ, -(focalY * clampedViewPos.y) * invViewPosZSquared,\n 0.0, 0.0, 0.0\n );\n\n // 视图变换矩阵 W (仅旋转部分) - 对齐 Android SDK,不使用转置\n let W = mat3x3f(\n viewMatrix[0].xyz,\n viewMatrix[1].xyz,\n viewMatrix[2].xyz\n );\n\n // 投影变换 T = J * W\n let T = J * W;\n\n // 3D 协方差矩阵 Vrk(对称矩阵)\n let Vrk = mat3x3f(\n cov3Da.x, cov3Da.y, cov3Da.z,\n cov3Da.y, cov3Db.x, cov3Db.y,\n cov3Da.z, cov3Db.y, cov3Db.z\n );\n\n // 2D 协方差矩阵: cov = T * Vrk * T^T\n let cov = T * Vrk * transpose(T);\n\n // 低通滤波器\n var result = vec3f(cov[0][0], cov[0][1], cov[1][1]);\n result.x += 0.3;\n result.z += 0.3;\n\n return result;\n}\n\n/**\n * 分解协方差矩阵\n */\nfn decomposeCovariance(cov2D: vec3f) -> array<vec2f, 2> {\n let a = cov2D.x;\n let b = cov2D.y;\n let d = cov2D.z;\n\n let det = a * d - b * b;\n let trace = a + d;\n\n let mean = 0.5 * trace;\n let dist = max(0.1, sqrt(mean * mean - det));\n\n // 特征值\n var lambda1 = mean + dist;\n var lambda2 = mean - dist;\n\n // 确保特征值为正\n lambda1 = max(lambda1, 0.01);\n lambda2 = max(lambda2, 0.01);\n\n // 特征向量(复刻 WebGL MetalSplatter 算法)\n var eigenvector1: vec2f;\n if (abs(b) < 1e-6) {\n eigenvector1 = select(vec2f(0.0, 1.0), vec2f(1.0, 0.0), a > d);\n } else {\n eigenvector1 = normalize(vec2f(b, d - lambda2));\n }\n\n // 正交特征向量\n let eigenvector2 = vec2f(eigenvector1.y, -eigenvector1.x);\n\n let v1 = eigenvector1 * sqrt(lambda1);\n let v2 = eigenvector2 * sqrt(lambda2);\n\n return array<vec2f, 2>(v1, v2);\n}\n\n@vertex\nfn vertexMain(\n input: VertexInput,\n @builtin(instance_index) instanceIndex: u32\n) -> VertexOutput {\n var output: VertexOutput;\n\n // 🚀 间接索引:通过排序索引读取实际数据\n let sortedIdx = sortIndices[instanceIndex];\n let dataOffset = sortedIdx * 13u;\n\n // 从 storage buffer 读取 splat 数据\n let position = vec3f(\n splatData[dataOffset + 0u],\n splatData[dataOffset + 1u],\n splatData[dataOffset + 2u]\n );\n let color = vec4f(\n splatData[dataOffset + 3u],\n splatData[dataOffset + 4u],\n splatData[dataOffset + 5u],\n splatData[dataOffset + 6u]\n );\n let covA = vec3f(\n splatData[dataOffset + 7u],\n splatData[dataOffset + 8u],\n splatData[dataOffset + 9u]\n );\n let covB = vec3f(\n splatData[dataOffset + 10u],\n splatData[dataOffset + 11u],\n splatData[dataOffset + 12u]\n );\n\n // 转换到视图空间\n let viewPosition4 = uniforms.viewMatrix * vec4f(position, 1.0);\n let viewPosition3 = viewPosition4.xyz;\n\n // 计算 2D 协方差矩阵\n let cov2D = calcCovariance2D(\n viewPosition3,\n covA,\n covB,\n uniforms.viewMatrix,\n uniforms.projectionMatrix,\n uniforms.screenSize\n );\n\n // 分解协方差矩阵\n let axes = decomposeCovariance(cov2D);\n let axis1 = axes[0];\n let axis2 = axes[1];\n\n // 投影到屏幕空间\n let projectedCenter = uniforms.projectionMatrix * viewPosition4;\n\n // 视锥体剔除\n if (uniforms.enableFrustumCulling == 1u) {\n let bounds = 1.2 * projectedCenter.w;\n if (projectedCenter.z < 0.0 ||\n projectedCenter.z > projectedCenter.w ||\n projectedCenter.x < -bounds ||\n projectedCenter.x > bounds ||\n projectedCenter.y < -bounds ||\n projectedCenter.y > bounds) {\n // 剔除到屏幕外\n output.position = vec4f(2.0, 2.0, 0.0, 1.0);\n output.relativePosition = vec2f(0.0);\n output.color = vec4f(0.0);\n return output;\n }\n }\n\n // 使用实例化的四边形顶点\n let relativeCoord = input.quadVertex;\n\n // 计算椭圆变换后的相对位置(像素单位)\n let ellipseRelativePos = relativeCoord.x * axis1 + relativeCoord.y * axis2;\n\n // 计算屏幕空间偏移\n let projectedScreenDelta = ellipseRelativePos * 2.0 * BOUNDS_RADIUS / uniforms.screenSize;\n\n // 最终顶点位置\n output.position = vec4f(\n projectedCenter.x + projectedScreenDelta.x * projectedCenter.w,\n projectedCenter.y + projectedScreenDelta.y * projectedCenter.w,\n projectedCenter.z,\n projectedCenter.w\n );\n\n // 传递给 fragment shader\n output.relativePosition = relativeCoord * BOUNDS_RADIUS;\n output.color = color;\n\n return output;\n}\n\n// ============ Fragment Shader ============\n\nconst BOUNDS_RADIUS_SQUARED: f32 = BOUNDS_RADIUS * BOUNDS_RADIUS;\n\nfn splatFragmentAlpha(relativePosition: vec2f, splatAlpha: f32) -> f32 {\n // 复刻 WebGL MetalSplatter 计算方式\n let negativeMagnitudeSquared = -dot(relativePosition, relativePosition);\n\n // 边界检查:超出椭圆边界的点被剔除\n if (negativeMagnitudeSquared < -BOUNDS_RADIUS_SQUARED) {\n return 0.0;\n }\n\n // 高斯衰减\n return exp(0.5 * negativeMagnitudeSquared) * splatAlpha;\n}\n\n@fragment\nfn fragmentMain(input: VertexOutput) -> @location(0) vec4f {\n let alpha = splatFragmentAlpha(input.relativePosition, input.color.a);\n\n // ✅ 优化:提前丢弃几乎透明的片段(提升性能和质量,对齐 Android SDK)\n if (alpha < 0.001) {\n discard;\n }\n\n // 预乘 alpha 输出(匹配 alphaMode: 'premultiplied')\n return vec4f(input.color.rgb * alpha, alpha);\n}\n";
14773
14757
  const blitShaderCode = "/**\n * WebGPU Blit Shader\n * 用于将 render texture 绘制到屏幕,应用 transform\n */\n\nstruct BlitUniforms {\n offset: vec2f, // 屏幕空间偏移(NDC坐标)\n scale: f32, // 缩放因子\n}\n\n@group(0) @binding(0) var<uniform> blitUniforms: BlitUniforms;\n@group(1) @binding(0) var texture: texture_2d<f32>;\n@group(1) @binding(1) var textureSampler: sampler;\n\nstruct VertexInput {\n @location(0) position: vec2f,\n @location(1) texCoord: vec2f,\n}\n\nstruct VertexOutput {\n @builtin(position) position: vec4f,\n @location(0) texCoord: vec2f,\n}\n\n@vertex\nfn vertexMain(input: VertexInput) -> VertexOutput {\n var output: VertexOutput;\n // 应用缩放和偏移\n let pos = input.position * blitUniforms.scale + blitUniforms.offset;\n output.position = vec4f(pos, 0.0, 1.0);\n // WebGPU framebuffer 纹理坐标需要翻转 Y 轴\n // framebuffer 的内容是从上到下存储的,但纹理坐标 (0,0) 在左上角,所以需要翻转\n output.texCoord = vec2f(input.texCoord.x, 1.0 - input.texCoord.y);\n return output;\n}\n\n@fragment\nfn fragmentMain(input: VertexOutput) -> @location(0) vec4f {\n return textureSample(texture, textureSampler, input.texCoord);\n}\n\n";
14774
- const transformShaderCode = "/**\n * WebGPU 3DGS Transform Compute Shader\n *\n * 功能: 在GPU上执行3DGS变换 + 协方差计算\n * 输入: Original Splats (110K点) + Face Geometry (15.4K面)\n * 输出: Transformed Splats with Covariance (GPU格式)\n */\n\n// ============================================================================\n// 数据结构定义\n// ============================================================================\n\n// ============================================================================\n// Bindings (使用flat array避免struct padding问题)\n// ============================================================================\n\n// Original Splats: 每个splat 16 floats (64 bytes紧密排列)\n// [position.xyz, scale.xyz, rotation.xyzw, color.rgba, opacity, binding(as float)]\n@group(0) @binding(0) var<storage, read> originalSplatsData: array<f32>;\n\n// Face Geometries: 每个face 8 floats (32 bytes紧密排列)\n// [center.xyz, scale, quat.xyzw]\n@group(0) @binding(1) var<storage, read> faceGeometriesData: array<f32>;\n\n// 输出为flat float array: [pos.xyz, color.rgba, cov[6]] = 13 floats per splat\n@group(0) @binding(2) var<storage, read_write> transformedData: array<f32>;\n\n// 🚀 性能优化: 单独输出紧凑的positions (用于排序)\n// [xyz] = 3 floats per splat\n@group(0) @binding(3) var<storage, read_write> positionsOutput: array<f32>;\n\n// 🆕 GPU排序优化: ViewMatrix uniform (用于计算view-space depth)\nstruct Uniforms {\n viewMatrix: mat4x4f,\n}\n@group(0) @binding(4) var<uniform> uniforms: Uniforms;\n\n// 🆕 GPU排序优化: 输出深度值 (Uint32格式, 已处理降序)\n@group(0) @binding(5) var<storage, read_write> depthsOutput: array<u32>;\n\n// ============================================================================\n// 辅助函数\n// ============================================================================\n\n/**\n * 四元数归一化\n */\nfn normalizeQuaternion(q: vec4f) -> vec4f {\n let norm = length(q);\n if (norm < 1e-8) {\n return vec4f(0.0, 0.0, 0.0, 1.0); // 单位四元数\n }\n return q / norm;\n}\n\n/**\n * 四元数乘法 (q1 * q2)\n * 注意: 四元数乘法不可交换\n */\nfn multiplyQuaternions(q1: vec4f, q2: vec4f) -> vec4f {\n return vec4f(\n q1.w * q2.x + q1.x * q2.w + q1.y * q2.z - q1.z * q2.y, // x\n q1.w * q2.y - q1.x * q2.z + q1.y * q2.w + q1.z * q2.x, // y\n q1.w * q2.z + q1.x * q2.y - q1.y * q2.x + q1.z * q2.w, // z\n q1.w * q2.w - q1.x * q2.x - q1.y * q2.y - q1.z * q2.z // w\n );\n}\n\n/**\n * 用四元数旋转向量\n * v_rotated = q * v * q_conjugate\n */\nfn rotateVectorByQuaternion(q: vec4f, v: vec3f) -> vec3f {\n // 优化版本: v' = v + 2.0 * cross(q.xyz, cross(q.xyz, v) + q.w * v)\n let qxyz = q.xyz;\n let qw = q.w;\n let t = 2.0 * cross(qxyz, v);\n return v + qw * t + cross(qxyz, t);\n}\n\n/**\n * 将四元数转换为3x3旋转矩阵\n * ⚠️ CRITICAL: C++存储的是TRANSPOSED matrix!\n * 所以quaternion → matrix转换后需要再次转置才能匹配C++的orientation_mat\n */\nfn quaternionToMatrix(q: vec4f) -> mat3x3f {\n let qx = q.x;\n let qy = q.y;\n let qz = q.z;\n let qw = q.w;\n\n // 标准quaternion到matrix转换\n let m00 = 1.0 - 2.0 * (qy*qy + qz*qz);\n let m01 = 2.0 * (qx*qy - qz*qw);\n let m02 = 2.0 * (qx*qz + qy*qw);\n\n let m10 = 2.0 * (qx*qy + qz*qw);\n let m11 = 1.0 - 2.0 * (qx*qx + qz*qz);\n let m12 = 2.0 * (qy*qz - qx*qw);\n\n let m20 = 2.0 * (qx*qz - qy*qw);\n let m21 = 2.0 * (qy*qz + qx*qw);\n let m22 = 1.0 - 2.0 * (qx*qx + qy*qy);\n\n // WGSL mat3x3f is column-major\n // Standard quaternion-to-matrix conversion (no transpose)\n return mat3x3f(\n vec3f(m00, m10, m20), // column 0\n vec3f(m01, m11, m21), // column 1\n vec3f(m02, m12, m22) // column 2\n );\n}\n\n/**\n * 从四元数构建旋转矩阵并计算协方差\n * Covariance = (R*S) * (R*S)^T\n */\nfn computeCovariance3D(scale: vec3f, rotation: vec4f) -> array<f32, 6> {\n // 1. 归一化四元数\n let q = normalizeQuaternion(rotation);\n let qx = q.x;\n let qy = q.y;\n let qz = q.z;\n let qw = q.w;\n\n // 2. 构建旋转矩阵 R (3x3)\n let r00 = 1.0 - 2.0 * (qy*qy + qz*qz);\n let r01 = 2.0 * (qx*qy - qz*qw);\n let r02 = 2.0 * (qx*qz + qy*qw);\n\n let r10 = 2.0 * (qx*qy + qz*qw);\n let r11 = 1.0 - 2.0 * (qx*qx + qz*qz);\n let r12 = 2.0 * (qy*qz - qx*qw);\n\n let r20 = 2.0 * (qx*qz - qy*qw);\n let r21 = 2.0 * (qy*qz + qx*qw);\n let r22 = 1.0 - 2.0 * (qx*qx + qy*qy);\n\n // 3. 计算 R * S\n let sx = scale.x;\n let sy = scale.y;\n let sz = scale.z;\n\n let rs00 = r00 * sx;\n let rs01 = r01 * sy;\n let rs02 = r02 * sz;\n\n let rs10 = r10 * sx;\n let rs11 = r11 * sy;\n let rs12 = r12 * sz;\n\n let rs20 = r20 * sx;\n let rs21 = r21 * sy;\n let rs22 = r22 * sz;\n\n // 4. 计算协方差矩阵上三角 = (R*S) * (R*S)^T\n var cov: array<f32, 6>;\n cov[0] = rs00*rs00 + rs01*rs01 + rs02*rs02; // cov[0][0]\n cov[1] = rs00*rs10 + rs01*rs11 + rs02*rs12; // cov[0][1]\n cov[2] = rs00*rs20 + rs01*rs21 + rs02*rs22; // cov[0][2]\n cov[3] = rs10*rs10 + rs11*rs11 + rs12*rs12; // cov[1][1]\n cov[4] = rs10*rs20 + rs11*rs21 + rs12*rs22; // cov[1][2]\n cov[5] = rs20*rs20 + rs21*rs21 + rs22*rs22; // cov[2][2]\n\n return cov;\n}\n\n/**\n * 计算可排序深度\n *\n * View space: Z轴负方向,物体Z < 0,越远越小\n * RadixSort: ascending (小到大)\n * 目标: far-to-near (远到近)\n *\n * depth = viewPos.z (负数,远点如-10,近点如-2)\n * 转sortable: 负数小 → sortable小\n * Ascending: 小在前 → 远在前 ✅\n *\n * 🚀 优化: 只需要 viewPos.z,因此只提取 viewMatrix 第3行的点积\n * viewPos.z = row3 · [worldPosition, 1]\n */\nfn computeSortableDepth(worldPosition: vec3f) -> u32 {\n // 🚀 优化: 直接计算 viewPos.z,无需完整矩阵乘法\n // WGSL 列主序: uniforms.viewMatrix[col][row]\n // 第3行 = [viewMatrix[0][2], viewMatrix[1][2], viewMatrix[2][2], viewMatrix[3][2]]\n let depth = uniforms.viewMatrix[0][2] * worldPosition.x +\n uniforms.viewMatrix[1][2] * worldPosition.y +\n uniforms.viewMatrix[2][2] * worldPosition.z +\n uniforms.viewMatrix[3][2];\n\n let depthBits = bitcast<u32>(depth);\n let depthSortable = depthBits ^ select(0x80000000u, 0xffffffffu, depth < 0.0);\n return depthSortable;\n}\n\n// ============================================================================\n// Compute Shader Main\n// ============================================================================\n\n@compute @workgroup_size(256)\nfn main(@builtin(global_invocation_id) global_id: vec3u) {\n let idx = global_id.x;\n\n // 边界检查 (originalSplatsData长度 / 16 = splat数量)\n let splatCount = arrayLength(&originalSplatsData) / 16u;\n if (idx >= splatCount) {\n return;\n }\n\n // ============================================================================\n // 读取Original Splat (16 floats per splat)\n // [position.xyz, scale.xyz, rotation.xyzw, color.rgba, opacity, binding]\n // ============================================================================\n let splatOffset = idx * 16u;\n\n let position = vec3f(\n originalSplatsData[splatOffset + 0u],\n originalSplatsData[splatOffset + 1u],\n originalSplatsData[splatOffset + 2u]\n );\n\n let scale = vec3f(\n originalSplatsData[splatOffset + 3u],\n originalSplatsData[splatOffset + 4u],\n originalSplatsData[splatOffset + 5u]\n );\n\n let rotation = vec4f(\n originalSplatsData[splatOffset + 6u],\n originalSplatsData[splatOffset + 7u],\n originalSplatsData[splatOffset + 8u],\n originalSplatsData[splatOffset + 9u]\n );\n\n let color = vec4f(\n originalSplatsData[splatOffset + 10u],\n originalSplatsData[splatOffset + 11u],\n originalSplatsData[splatOffset + 12u],\n originalSplatsData[splatOffset + 13u]\n );\n\n let opacity = originalSplatsData[splatOffset + 14u];\n let binding = i32(originalSplatsData[splatOffset + 15u]);\n\n // ============================================================================\n // 获取绑定的Face Geometry (带边界检查)\n // ============================================================================\n let faceCount = arrayLength(&faceGeometriesData) / 8u;\n if (binding < 0 || u32(binding) >= faceCount) {\n // 绑定无效,跳过 (设置为无效点)\n let baseOffset = idx * 13u;\n for (var i = 0u; i < 13u; i++) {\n transformedData[baseOffset + i] = 0.0;\n }\n return;\n }\n\n // ============================================================================\n // 读取Face Geometry (8 floats per face)\n // [center.xyz, scale, quat.xyzw]\n // ============================================================================\n let faceOffset = u32(binding) * 8u;\n\n let faceCenter = vec3f(\n faceGeometriesData[faceOffset + 0u],\n faceGeometriesData[faceOffset + 1u],\n faceGeometriesData[faceOffset + 2u]\n );\n\n let faceScale = faceGeometriesData[faceOffset + 3u];\n\n let faceQuat = vec4f(\n faceGeometriesData[faceOffset + 4u],\n faceGeometriesData[faceOffset + 5u],\n faceGeometriesData[faceOffset + 6u],\n faceGeometriesData[faceOffset + 7u]\n );\n\n // ============================================================================\n // 1. 位置变换: position = orientation_mat * (original_pos * face_scale) + face_center\n // 匹配C++: transformed_splat.position = face_geometry.orientation_mat * original_splat.position * face_geometry.scaling + face_geometry.center;\n // ============================================================================\n let orientationMat = quaternionToMatrix(faceQuat);\n let scaledPosition = position * faceScale;\n let rotated = orientationMat * scaledPosition;\n let transformedPosition = rotated + faceCenter;\n\n // ============================================================================\n // 2. 缩放变换: scale = original_scale * face_scale\n // ============================================================================\n let transformedScale = scale * faceScale;\n\n // ============================================================================\n // 3. 旋转变换: rotation = quat_multiply(normalize(original_rotation), face_quat)\n // ============================================================================\n let normalizedOriginalRotation = normalizeQuaternion(rotation);\n let transformedRotation = multiplyQuaternions(normalizedOriginalRotation, faceQuat);\n\n // ============================================================================\n // 4. 计算3D协方差矩阵\n // ============================================================================\n let covariance = computeCovariance3D(transformedScale, transformedRotation);\n\n // ============================================================================\n // 5. 输出结果到flat array (13 floats per splat)\n // ============================================================================\n let baseOffset = idx * 13u;\n\n // position[3]\n transformedData[baseOffset + 0u] = transformedPosition.x;\n transformedData[baseOffset + 1u] = transformedPosition.y;\n transformedData[baseOffset + 2u] = transformedPosition.z;\n\n // color[4]: 从SH 0阶系数转换为RGB\n // SH_C0 = 0.28209479177387814\n // RGB = SH_C0 * sh[0] + 0.5\n let SH_C0 = 0.28209479177387814;\n let r = clamp(SH_C0 * color.r + 0.5, 0.0, 1.0);\n let g = clamp(SH_C0 * color.g + 0.5, 0.0, 1.0);\n let b = clamp(SH_C0 * color.b + 0.5, 0.0, 1.0);\n\n transformedData[baseOffset + 3u] = r;\n transformedData[baseOffset + 4u] = g;\n transformedData[baseOffset + 5u] = b;\n transformedData[baseOffset + 6u] = opacity;\n\n // covariance[6]\n transformedData[baseOffset + 7u] = covariance[0];\n transformedData[baseOffset + 8u] = covariance[1];\n transformedData[baseOffset + 9u] = covariance[2];\n transformedData[baseOffset + 10u] = covariance[3];\n transformedData[baseOffset + 11u] = covariance[4];\n transformedData[baseOffset + 12u] = covariance[5];\n\n // ============================================================================\n // 6. 🚀 同时输出紧凑的positions (用于排序,零额外开销)\n // ============================================================================\n let posOffset = idx * 3u;\n positionsOutput[posOffset + 0u] = transformedPosition.x;\n positionsOutput[posOffset + 1u] = transformedPosition.y;\n positionsOutput[posOffset + 2u] = transformedPosition.z;\n\n // ============================================================================\n // 7. 🆕 GPU排序优化: 输出可排序深度值 (Uint32, 降序)\n // ============================================================================\n depthsOutput[idx] = computeSortableDepth(transformedPosition);\n}\n\n";
14775
- class TransformPipeline {
14776
- constructor(device) {
14777
- __publicField(this, "device");
14778
- __publicField(this, "computePipeline", null);
14779
- __publicField(this, "bindGroup", null);
14780
- // GPU Buffers
14781
- __publicField(this, "originalSplatsBuffer", null);
14782
- __publicField(this, "faceGeometryBuffer", null);
14783
- __publicField(this, "transformedOutputBuffer", null);
14784
- __publicField(this, "positionsOutputBuffer", null);
14785
- // 🚀 紧凑的positions输出
14786
- __publicField(this, "viewMatrixBuffer", null);
14787
- // 🆕 View matrix uniform
14788
- __publicField(this, "depthsOutputBuffer", null);
14789
- // 🆕 深度输出 (Uint32, GPU排序用)
14790
- // 数据规模
14791
- __publicField(this, "splatCount", 0);
14792
- __publicField(this, "faceCount", 0);
14793
- // 🆕 GPU FLAME 支持:标记是否使用外部 GPU buffer
14794
- __publicField(this, "usesExternalFaceGeometryBuffer", false);
14795
- this.device = device;
14796
- }
14797
- /**
14798
- * 初始化Pipeline
14799
- */
14800
- async initialize() {
14801
- const shaderModule = this.device.createShaderModule({
14802
- label: "Transform Compute Shader",
14803
- code: transformShaderCode
14804
- });
14805
- this.computePipeline = await this.device.createComputePipelineAsync({
14806
- label: "Transform Compute Pipeline",
14807
- layout: "auto",
14808
- compute: {
14809
- module: shaderModule,
14810
- entryPoint: "main"
14811
- }
14812
- });
14813
- logger.log("✅ Transform Pipeline initialized");
14814
- }
14815
- /**
14816
- * 上传Original Splats (一次性调用)
14817
- * @param originalSplatsData Float32Array, 每个splat 16 floats (64 bytes)
14818
- * @param splatCount splat数量
14819
- */
14820
- uploadOriginalSplats(originalSplatsData, splatCount) {
14821
- var _a, _b;
14822
- if (!this.device) {
14823
- throw new Error("Device not initialized");
14824
- }
14825
- this.splatCount = splatCount;
14826
- const bufferSize = originalSplatsData.byteLength;
14827
- if (this.originalSplatsBuffer) {
14828
- this.originalSplatsBuffer.destroy();
14829
- }
14830
- this.originalSplatsBuffer = this.device.createBuffer({
14831
- label: "Original Splats Buffer",
14832
- size: bufferSize,
14833
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
14834
- });
14835
- this.device.queue.writeBuffer(
14836
- this.originalSplatsBuffer,
14837
- 0,
14838
- originalSplatsData.buffer,
14839
- originalSplatsData.byteOffset,
14840
- originalSplatsData.byteLength
14841
- );
14842
- this.createTransformedOutputBuffer();
14843
- this.createViewMatrixBuffer();
14844
- logger.log(`✅ [TransformPipeline] Original Splats uploaded: ${splatCount} splats (${(bufferSize / 1024 / 1024).toFixed(2)} MB)`, {
14845
- originalSplatsBufferSize: this.originalSplatsBuffer.size,
14846
- transformedOutputBufferSize: ((_a = this.transformedOutputBuffer) == null ? void 0 : _a.size) || 0,
14847
- viewMatrixBufferSize: ((_b = this.viewMatrixBuffer) == null ? void 0 : _b.size) || 0,
14848
- bindGroupCreated: false
14849
- // bind group在首次updateFaceGeometry时创建
14850
- });
14851
- }
14852
- /**
14853
- * 🆕 设置外部 GPU FaceGeometry Buffer(GPU FLAME 路径)
14854
- * @param externalBuffer 外部 GPU buffer(来自 FLAME Pipeline 的 faceGeometriesBuffer)
14855
- * @param faceCount face 数量
14856
- */
14857
- setFaceGeometryBufferFromGPU(externalBuffer, faceCount) {
14858
- if (!this.device) {
14859
- throw new Error("Device not initialized");
14860
- }
14861
- if (this.faceGeometryBuffer && !this.usesExternalFaceGeometryBuffer) {
14862
- this.faceGeometryBuffer.destroy();
14863
- }
14864
- this.faceGeometryBuffer = externalBuffer;
14865
- this.faceCount = faceCount;
14866
- this.usesExternalFaceGeometryBuffer = true;
14867
- this.createBindGroup();
14868
- }
14869
- /**
14870
- * 更新Face Geometry Buffer (每帧调用) - CPU 路径
14871
- * @param faceGeometryData Float32Array, 每个face 8 floats (32 bytes)
14872
- */
14873
- updateFaceGeometry(faceGeometryData) {
14874
- if (!this.device) {
14875
- throw new Error("Device not initialized");
14876
- }
14877
- const faceCount = faceGeometryData.length / 8;
14878
- const bufferSize = faceGeometryData.byteLength;
14879
- const needsRebuild = !this.faceGeometryBuffer || this.faceCount !== faceCount || this.usesExternalFaceGeometryBuffer;
14880
- if (needsRebuild) {
14881
- this.faceCount = faceCount;
14882
- if (this.faceGeometryBuffer && !this.usesExternalFaceGeometryBuffer) {
14883
- this.faceGeometryBuffer.destroy();
14884
- }
14885
- this.faceGeometryBuffer = this.device.createBuffer({
14886
- label: "Face Geometry Buffer (CPU path)",
14887
- size: bufferSize,
14888
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
14889
- });
14890
- this.usesExternalFaceGeometryBuffer = false;
14891
- this.createBindGroup();
14892
- }
14893
- if (!this.faceGeometryBuffer) {
14894
- throw new Error("FaceGeometry buffer not created");
14895
- }
14896
- this.device.queue.writeBuffer(
14897
- this.faceGeometryBuffer,
14898
- 0,
14899
- faceGeometryData.buffer,
14900
- faceGeometryData.byteOffset,
14901
- faceGeometryData.byteLength
14902
- );
14903
- }
14904
- /**
14905
- * 执行Transform计算 (在给定的command encoder中)
14906
- * @param commandEncoder 外部command encoder (与render共享以保证顺序)
14907
- */
14908
- executeInEncoder(commandEncoder) {
14909
- if (!this.device || !this.computePipeline || !this.bindGroup) {
14910
- return;
14911
- }
14912
- if (this.splatCount === 0) {
14913
- return;
14914
- }
14915
- const passEncoder = commandEncoder.beginComputePass({
14916
- label: "Transform Compute Pass"
14917
- });
14918
- passEncoder.setPipeline(this.computePipeline);
14919
- passEncoder.setBindGroup(0, this.bindGroup);
14920
- const workgroupCount = Math.ceil(this.splatCount / 256);
14921
- passEncoder.dispatchWorkgroups(workgroupCount);
14922
- passEncoder.end();
14923
- }
14924
- /**
14925
- * 获取Transformed Output Buffer (供渲染器使用)
14926
- */
14927
- getTransformedOutputBuffer() {
14928
- return this.transformedOutputBuffer;
14929
- }
14930
- /**
14931
- * 🚀 获取Positions Output Buffer (供排序使用)
14932
- */
14933
- getPositionsOutputBuffer() {
14934
- return this.positionsOutputBuffer;
14935
- }
14936
- /**
14937
- * 🆕 获取Depths Output Buffer (供GPU排序使用)
14938
- */
14939
- getDepthsOutputBuffer() {
14940
- return this.depthsOutputBuffer;
14941
- }
14942
- /**
14943
- * 🆕 更新View Matrix (每帧调用)
14944
- * @param viewMatrix 4x4 view matrix
14945
- */
14946
- updateViewMatrix(viewMatrix) {
14947
- if (!this.device || !this.viewMatrixBuffer) {
14948
- return;
14949
- }
14950
- this.device.queue.writeBuffer(
14951
- this.viewMatrixBuffer,
14952
- 0,
14953
- viewMatrix.buffer,
14954
- viewMatrix.byteOffset,
14955
- viewMatrix.byteLength
14956
- );
14957
- }
14958
- /**
14959
- * 获取Splat数量
14960
- */
14961
- getSplatCount() {
14962
- return this.splatCount;
14963
- }
14964
- /**
14965
- * 创建Transformed Output Buffer
14966
- * 格式: position[3] + color[4] + covariance[6] = 13 floats = 52 bytes
14967
- */
14968
- createTransformedOutputBuffer() {
14969
- if (!this.device || this.splatCount === 0) return;
14970
- const floatsPerSplat = 13;
14971
- const bufferSize = this.splatCount * floatsPerSplat * 4;
14972
- if (this.transformedOutputBuffer) {
14973
- this.transformedOutputBuffer.destroy();
14974
- }
14975
- this.transformedOutputBuffer = this.device.createBuffer({
14976
- label: "Transformed Output Buffer",
14977
- size: bufferSize,
14978
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
14979
- });
14980
- this.createPositionsOutputBuffer();
14981
- this.createDepthsOutputBuffer();
14982
- }
14983
- /**
14984
- * 🚀 创建Positions Output Buffer (用于排序)
14985
- * 格式: position[3] = 3 floats = 12 bytes per splat
14986
- */
14987
- createPositionsOutputBuffer() {
14988
- if (!this.device || this.splatCount === 0) return;
14989
- const floatsPerPosition = 3;
14990
- const bufferSize = this.splatCount * floatsPerPosition * 4;
14991
- if (this.positionsOutputBuffer) {
14992
- this.positionsOutputBuffer.destroy();
14993
- }
14994
- this.positionsOutputBuffer = this.device.createBuffer({
14995
- label: "Positions Output Buffer (for sorting)",
14996
- size: bufferSize,
14997
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
14998
- });
14999
- }
15000
- /**
15001
- * 🆕 创建View Matrix Buffer
15002
- */
15003
- createViewMatrixBuffer() {
15004
- if (!this.device) return;
15005
- const bufferSize = 64;
15006
- this.viewMatrixBuffer = this.device.createBuffer({
15007
- label: "View Matrix Uniform",
15008
- size: bufferSize,
15009
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
15010
- });
15011
- }
15012
- /**
15013
- * 🆕 创建Depths Output Buffer (用于GPU排序)
15014
- * 格式: depth (Uint32) = 4 bytes per splat
15015
- */
15016
- createDepthsOutputBuffer() {
15017
- if (!this.device || this.splatCount === 0) return;
15018
- const bufferSize = this.splatCount * 4;
15019
- this.depthsOutputBuffer = this.device.createBuffer({
15020
- label: "Depths Output Buffer (for GPU sorting)",
15021
- size: bufferSize,
15022
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
15023
- });
15024
- }
15025
- /**
15026
- * 创建Bind Group
15027
- */
15028
- createBindGroup() {
15029
- if (!this.device || !this.computePipeline || !this.originalSplatsBuffer || !this.faceGeometryBuffer || !this.transformedOutputBuffer || !this.positionsOutputBuffer || !this.viewMatrixBuffer || // 🆕 新增检查
15030
- !this.depthsOutputBuffer) {
15031
- return;
15032
- }
15033
- const bindGroupLayout = this.computePipeline.getBindGroupLayout(0);
15034
- this.bindGroup = this.device.createBindGroup({
15035
- label: "Transform Bind Group",
15036
- layout: bindGroupLayout,
15037
- entries: [
15038
- {
15039
- binding: 0,
15040
- // originalSplats
15041
- resource: { buffer: this.originalSplatsBuffer }
15042
- },
15043
- {
15044
- binding: 1,
15045
- // faceGeometries
15046
- resource: { buffer: this.faceGeometryBuffer }
15047
- },
15048
- {
15049
- binding: 2,
15050
- // transformedSplats
15051
- resource: { buffer: this.transformedOutputBuffer }
15052
- },
15053
- {
15054
- binding: 3,
15055
- // positionsOutput
15056
- resource: { buffer: this.positionsOutputBuffer }
15057
- },
15058
- {
15059
- binding: 4,
15060
- // 🆕 viewMatrix (uniform)
15061
- resource: { buffer: this.viewMatrixBuffer }
15062
- },
15063
- {
15064
- binding: 5,
15065
- // 🆕 depthsOutput
15066
- resource: { buffer: this.depthsOutputBuffer }
15067
- }
15068
- ]
15069
- });
15070
- }
15071
- /**
15072
- * 清理资源
15073
- */
15074
- destroy() {
15075
- var _a, _b, _c, _d, _e2;
15076
- (_a = this.originalSplatsBuffer) == null ? void 0 : _a.destroy();
15077
- if (this.faceGeometryBuffer && !this.usesExternalFaceGeometryBuffer) {
15078
- this.faceGeometryBuffer.destroy();
15079
- }
15080
- (_b = this.transformedOutputBuffer) == null ? void 0 : _b.destroy();
15081
- (_c = this.positionsOutputBuffer) == null ? void 0 : _c.destroy();
15082
- (_d = this.viewMatrixBuffer) == null ? void 0 : _d.destroy();
15083
- (_e2 = this.depthsOutputBuffer) == null ? void 0 : _e2.destroy();
15084
- this.originalSplatsBuffer = null;
15085
- this.faceGeometryBuffer = null;
15086
- this.transformedOutputBuffer = null;
15087
- this.positionsOutputBuffer = null;
15088
- this.viewMatrixBuffer = null;
15089
- this.depthsOutputBuffer = null;
15090
- this.bindGroup = null;
15091
- }
15092
- }
15093
- const flameCommonWGSL = "/**\n * FLAME Common Definitions\n *\n * 共享的结构体、常量和工具函数\n * 🔧 Updated: Added staticOffsetCount to FLAMEMetadata\n */\n\n// ============================================================================\n// 常量定义\n// ============================================================================\n\nconst PI: f32 = 3.14159265359;\n\n// ============================================================================\n// 结构体定义 (与 FLAMEGPUBuffers 对齐)\n// ============================================================================\n\n/**\n * FLAME 帧参数 (Uniform Buffer)\n * 🚀 优化: 移除 shapeParams (已分离为独立 Storage Buffer)\n * Layout (std140, vec4 对齐):\n * - exprParams: 25 vec4 (100 floats)\n * - rotation: 1 vec4 (3 floats + padding)\n * - translation: 1 vec4 (3 floats + padding)\n * - neckPose: 1 vec4 (3 floats + padding)\n * - jawPose: 1 vec4 (3 floats + padding)\n * - eyesPose: 2 vec4 (6 floats)\n * - eyelid: 1 vec4 (2 floats + padding)\n * Total: 32 vec4 = 512 bytes (was 1744 bytes, 节省 71%)\n */\nstruct FLAMEParams {\n exprParams: array<vec4<f32>, 25>, // [100] expression parameters\n rotation: vec4<f32>, // [3] global rotation (axis-angle) + padding\n translation: vec4<f32>, // [3] global translation + padding\n neckPose: vec4<f32>, // [3] neck pose + padding\n jawPose: vec4<f32>, // [3] jaw pose + padding\n eyesPose: array<vec4<f32>, 2>, // [6] eyes pose (2 vec4)\n eyelid: vec4<f32>, // [2] eyelid + padding\n}\n\n/**\n * FLAME 元数据 (Uniform Buffer)\n */\nstruct FLAMEMetadata {\n vertexCount: u32,\n faceCount: u32,\n jointCount: u32,\n shapeParamCount: u32,\n poseParamCount: u32,\n staticOffsetCount: u32, // 🆕 静态偏移顶点数量\n activeShapeCount: u32, // 🚀 活跃shape参数数量\n _padding0: u32, // 对齐到 32 bytes\n}\n\n// ============================================================================\n// 工具函数\n// ============================================================================\n\n/**\n * Rodrigues 公式: 轴角表示转换为旋转矩阵\n * @param axisAngle 轴角表示 (vec3)\n * @return 3x3 旋转矩阵\n */\nfn rodrigues(axisAngle: vec3<f32>) -> mat3x3<f32> {\n // 🔧 匹配 CPU 实现:将 epsilon 加到 axis_angle 的每个分量上\n // CPU: Vector3f axis_angle_with_epsilon(axis_angle.x + epsilon, axis_angle.y + epsilon, axis_angle.z + epsilon);\n const EPSILON = 1e-8;\n let axisAngleWithEpsilon = axisAngle + vec3<f32>(EPSILON);\n\n // 计算旋转角度 θ = ||axis_angle + epsilon||\n let theta = length(axisAngleWithEpsilon);\n\n // 归一化得到旋转轴\n let axis = axisAngleWithEpsilon / theta;\n let c = cos(theta);\n let s = sin(theta);\n let t = 1.0 - c;\n\n let x = axis.x;\n let y = axis.y;\n let z = axis.z;\n\n // 旋转矩阵 (列主序) - 使用 Rodrigues 公式:R = I + sin(θ)*K + (1-cos(θ))*K²\n // 其中 K 是反对称矩阵,K² 是 K 的平方\n return mat3x3<f32>(\n vec3<f32>(t*x*x + c, t*x*y + s*z, t*x*z - s*y),\n vec3<f32>(t*x*y - s*z, t*y*y + c, t*y*z + s*x),\n vec3<f32>(t*x*z + s*y, t*y*z - s*x, t*z*z + c)\n );\n}\n\n/**\n * 构建 4x4 变换矩阵 (从旋转和平移)\n * @param rotation 3x3 旋转矩阵\n * @param translation 平移向量\n * @return 4x4 变换矩阵\n */\nfn makeTransform(rotation: mat3x3<f32>, translation: vec3<f32>) -> mat4x4<f32> {\n return mat4x4<f32>(\n vec4<f32>(rotation[0], 0.0),\n vec4<f32>(rotation[1], 0.0),\n vec4<f32>(rotation[2], 0.0),\n vec4<f32>(translation, 1.0)\n );\n}\n\n/**\n * 🚀 优化: 从 vec4 数组提取 float,使用数组索引消除分支\n */\nfn extractFloatExpr(arr: array<vec4<f32>, 25>, idx: u32) -> f32 {\n let vecIdx = idx / 4u;\n let offset = idx % 4u;\n let v = arr[vecIdx];\n // 使用数组字面量代替 if-else,GPU 可以优化为 swizzle\n return array<f32, 4>(v.x, v.y, v.z, v.w)[offset];\n}\n\n/**\n * 矩阵乘法 (mat3x3 * vec3)\n */\nfn matMulVec3(m: mat3x3<f32>, v: vec3<f32>) -> vec3<f32> {\n return vec3<f32>(\n dot(m[0], v),\n dot(m[1], v),\n dot(m[2], v)\n );\n}\n\n/**\n * 矩阵乘法 (mat3x3 * mat3x3)\n */\nfn matMul3x3(a: mat3x3<f32>, b: mat3x3<f32>) -> mat3x3<f32> {\n return mat3x3<f32>(\n matMulVec3(a, b[0]),\n matMulVec3(a, b[1]),\n matMulVec3(a, b[2])\n );\n}\n\n/**\n * 计算三角形法向量\n */\nfn computeTriangleNormal(v0: vec3<f32>, v1: vec3<f32>, v2: vec3<f32>) -> vec3<f32> {\n let edge1 = v1 - v0;\n let edge2 = v2 - v0;\n return normalize(cross(edge1, edge2));\n}\n\n/**\n * 计算三角形质心\n */\nfn computeTriangleCentroid(v0: vec3<f32>, v1: vec3<f32>, v2: vec3<f32>) -> vec3<f32> {\n return (v0 + v1 + v2) / 3.0;\n}\n\n/**\n * 四元数转旋转矩阵\n * @param q 四元数 (x, y, z, w)\n * @return 3x3 旋转矩阵\n */\nfn quaternionToMatrix(q: vec4<f32>) -> mat3x3<f32> {\n let x = q.x;\n let y = q.y;\n let z = q.z;\n let w = q.w;\n\n let x2 = x * x;\n let y2 = y * y;\n let z2 = z * z;\n let xy = x * y;\n let xz = x * z;\n let yz = y * z;\n let wx = w * x;\n let wy = w * y;\n let wz = w * z;\n\n return mat3x3<f32>(\n vec3<f32>(1.0 - 2.0*(y2 + z2), 2.0*(xy + wz), 2.0*(xz - wy)),\n vec3<f32>(2.0*(xy - wz), 1.0 - 2.0*(x2 + z2), 2.0*(yz + wx)),\n vec3<f32>(2.0*(xz + wy), 2.0*(yz - wx), 1.0 - 2.0*(x2 + y2))\n );\n}\n\n";
15094
- const flameShapeBlendWGSL = "@group(0) @binding(0) var<uniform> metadata: FLAMEMetadata;\n@group(0) @binding(1) var<storage, read> activeShapeIndices: array<u32>; // 🚀 活跃shape参数索引\n@group(0) @binding(2) var<storage, read> activeShapeValues: array<f32>; // 🚀 活跃shape参数值\n@group(0) @binding(3) var<uniform> params: FLAMEParams;\n\n@group(1) @binding(0) var<storage, read> vTemplate: array<f32>;\n@group(1) @binding(1) var<storage, read> shapedirs: array<f32>;\n@group(1) @binding(2) var<storage, read_write> vShaped: array<f32>;\n@group(1) @binding(3) var<storage, read> staticOffset: array<f32>;\n\n@compute @workgroup_size(256)\nfn main(@builtin(global_invocation_id) globalId: vec3<u32>) {\n let vertexIdx = globalId.x;\n let vertexCount = metadata.vertexCount;\n \n // 🔧 边界检查:确保不处理超出范围的顶点\n if (vertexIdx >= vertexCount) {\n return;\n }\n\n let baseIdx = vertexIdx * 3u;\n var vertex = vec3<f32>(\n vTemplate[baseIdx],\n vTemplate[baseIdx + 1u],\n vTemplate[baseIdx + 2u]\n );\n\n let numExprParams = 100u;\n let numActiveShapeParams = metadata.activeShapeCount; // 🚀 使用活跃参数数量\n\n // 🚀 优化: 只循环活跃的shape参数(零参数过滤)\n for (var i = 0u; i < numActiveShapeParams; i++) {\n let shapeParamIdx = activeShapeIndices[i]; // 原始参数索引 [0, 300)\n let shapeParam = activeShapeValues[i]; // 参数值\n\n let offset = shapeParamIdx * vertexCount * 3u + vertexIdx * 3u;\n let dx = shapedirs[offset];\n let dy = shapedirs[offset + 1u];\n let dz = shapedirs[offset + 2u];\n\n vertex += vec3<f32>(dx, dy, dz) * shapeParam;\n }\n\n for (var e = 0u; e < numExprParams; e++) {\n let exprParam = extractFloatExpr(params.exprParams, e);\n let paramIdx = 300u + e; // shape参数固定为300个\n\n let offset = paramIdx * vertexCount * 3u + vertexIdx * 3u;\n let dx = shapedirs[offset];\n let dy = shapedirs[offset + 1u];\n let dz = shapedirs[offset + 2u];\n\n vertex += vec3<f32>(dx, dy, dz) * exprParam;\n }\n\n if (vertexIdx < metadata.staticOffsetCount) {\n vertex.x += staticOffset[baseIdx];\n vertex.y += staticOffset[baseIdx + 1u];\n vertex.z += staticOffset[baseIdx + 2u];\n }\n\n vShaped[baseIdx] = vertex.x;\n vShaped[baseIdx + 1u] = vertex.y;\n vShaped[baseIdx + 2u] = vertex.z;\n}\n\n";
15095
- const flamePoseDeformWGSL = "@group(0) @binding(0) var<uniform> params: FLAMEParams;\n@group(0) @binding(1) var<uniform> metadata: FLAMEMetadata;\n\n@group(1) @binding(0) var<storage, read> vShaped: array<f32>;\n@group(1) @binding(1) var<storage, read> posedirs: array<f32>;\n@group(1) @binding(2) var<storage, read_write> vPosed: array<f32>;\n\n/**\n * 计算 pose_feature 向量 (36 维)\n *\n * 从 5 个关节的旋转参数计算:\n * - Joint 0 (global): 跳过\n * - Joints 1-4 (neck, jaw, left_eye, right_eye): 各贡献 9 个元素\n *\n * 🔧 关键修复: WGSL mat3x3 是列主序的!\n * - mat[col][row] = M[row][col]\n * - 要按 C++ 行主序展平 (M[0][0], M[0][1], M[0][2], M[1][0], ...)\n * - 必须用 mat[col][row] 访问 M[row][col]\n */\nfn computePoseFeature() -> array<f32, 36> {\n var pose_feature: array<f32, 36>;\n\n // Joint 1: neck (indices 0-8)\n let R_neck = rodrigues(vec3<f32>(params.neckPose.x, params.neckPose.y, params.neckPose.z));\n let I = mat3x3<f32>(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0);\n let rel_neck = R_neck - I;\n\n // 🔧 修复: 按行主序展平 - mat[col][row] 对应 M[row][col]\n // 第 0 行: M[0][0], M[0][1], M[0][2] = mat[0][0], mat[1][0], mat[2][0]\n pose_feature[0] = rel_neck[0][0]; pose_feature[1] = rel_neck[1][0]; pose_feature[2] = rel_neck[2][0];\n // 第 1 行: M[1][0], M[1][1], M[1][2] = mat[0][1], mat[1][1], mat[2][1]\n pose_feature[3] = rel_neck[0][1]; pose_feature[4] = rel_neck[1][1]; pose_feature[5] = rel_neck[2][1];\n // 第 2 行: M[2][0], M[2][1], M[2][2] = mat[0][2], mat[1][2], mat[2][2]\n pose_feature[6] = rel_neck[0][2]; pose_feature[7] = rel_neck[1][2]; pose_feature[8] = rel_neck[2][2];\n\n // Joint 2: jaw (indices 9-17)\n let R_jaw = rodrigues(vec3<f32>(params.jawPose.x, params.jawPose.y, params.jawPose.z));\n let rel_jaw = R_jaw - I;\n\n pose_feature[9] = rel_jaw[0][0]; pose_feature[10] = rel_jaw[1][0]; pose_feature[11] = rel_jaw[2][0];\n pose_feature[12] = rel_jaw[0][1]; pose_feature[13] = rel_jaw[1][1]; pose_feature[14] = rel_jaw[2][1];\n pose_feature[15] = rel_jaw[0][2]; pose_feature[16] = rel_jaw[1][2]; pose_feature[17] = rel_jaw[2][2];\n\n // Joint 3: left_eye (indices 18-26)\n let R_left_eye = rodrigues(vec3<f32>(params.eyesPose[0].x, params.eyesPose[0].y, params.eyesPose[0].z));\n let rel_left_eye = R_left_eye - I;\n\n pose_feature[18] = rel_left_eye[0][0]; pose_feature[19] = rel_left_eye[1][0]; pose_feature[20] = rel_left_eye[2][0];\n pose_feature[21] = rel_left_eye[0][1]; pose_feature[22] = rel_left_eye[1][1]; pose_feature[23] = rel_left_eye[2][1];\n pose_feature[24] = rel_left_eye[0][2]; pose_feature[25] = rel_left_eye[1][2]; pose_feature[26] = rel_left_eye[2][2];\n\n // Joint 4: right_eye (indices 27-35)\n let R_right_eye = rodrigues(vec3<f32>(params.eyesPose[1].x, params.eyesPose[1].y, params.eyesPose[1].z));\n let rel_right_eye = R_right_eye - I;\n\n pose_feature[27] = rel_right_eye[0][0]; pose_feature[28] = rel_right_eye[1][0]; pose_feature[29] = rel_right_eye[2][0];\n pose_feature[30] = rel_right_eye[0][1]; pose_feature[31] = rel_right_eye[1][1]; pose_feature[32] = rel_right_eye[2][1];\n pose_feature[33] = rel_right_eye[0][2]; pose_feature[34] = rel_right_eye[1][2]; pose_feature[35] = rel_right_eye[2][2];\n\n return pose_feature;\n}\n\n// ============================================================================\n// Compute Shader\n// ============================================================================\n\n@compute @workgroup_size(256)\nfn main(@builtin(global_invocation_id) globalId: vec3<u32>) {\n let vertexIdx = globalId.x;\n let vertexCount = metadata.vertexCount;\n \n // 🔧 边界检查:确保不处理超出范围的顶点\n if (vertexIdx >= vertexCount) {\n return;\n }\n\n let baseIdx = vertexIdx * 3u;\n var vertex = vec3<f32>(\n vShaped[baseIdx],\n vShaped[baseIdx + 1u],\n vShaped[baseIdx + 2u]\n );\n\n // 计算 pose_feature (36 维)\n let pose_feature = computePoseFeature();\n let poseParamCount = metadata.poseParamCount; // 36\n\n var pose_offset = vec3<f32>(0.0, 0.0, 0.0);\n\n // 🔧 使用转置布局 [param][vertex][xyz] (GPU优化后的布局)\n // GPU 数据已通过 transposeBlendshapeData 转置,布局为 [param][vertex][xyz]\n // 索引公式: p * vertexCount * 3 + v * 3 + xyz\n for (var p = 0u; p < poseParamCount; p++) {\n let feature = pose_feature[p];\n\n let offset = p * vertexCount * 3u + vertexIdx * 3u;\n let dx = posedirs[offset];\n let dy = posedirs[offset + 1u];\n let dz = posedirs[offset + 2u];\n\n pose_offset += vec3<f32>(dx, dy, dz) * feature;\n }\n\n // 应用姿态偏移\n vertex += pose_offset;\n\n // 写回结果\n vPosed[baseIdx] = vertex.x;\n vPosed[baseIdx + 1u] = vertex.y;\n vPosed[baseIdx + 2u] = vertex.z;\n}\n\n";
15096
- const flameJointRegressWGSL = "@group(0) @binding(1) var<uniform> metadata: FLAMEMetadata;\n\n@group(1) @binding(0) var<storage, read> vShaped: array<f32>; // 🔧 修复: 使用v_shaped而不是v_posed\n@group(1) @binding(1) var<storage, read> jRegressor: array<f32>;\n@group(1) @binding(2) var<storage, read_write> joints: array<f32>;\n\n\n// 每个 workgroup 的局部累加缓冲区 (256 threads × 3 coords)\nvar<workgroup> sharedSumX: array<f32, 256>;\nvar<workgroup> sharedSumY: array<f32, 256>;\nvar<workgroup> sharedSumZ: array<f32, 256>;\n\n// ============================================================================\n// Compute Shader\n// ============================================================================\n\n/**\n * 计算策略:\n * - 对于 5 个关节,使用 5 个 workgroups (每个 workgroup 256 threads)\n * - 每个 workgroup 处理一个关节的所有顶点\n * - 使用 shared memory 进行树状归约\n *\n * Dispatch: (5, 1, 1) workgroups × (256, 1, 1) threads\n */\n@compute @workgroup_size(256, 1, 1)\nfn main(\n @builtin(global_invocation_id) globalId: vec3<u32>,\n @builtin(local_invocation_id) localId: vec3<u32>,\n @builtin(workgroup_id) workgroupId: vec3<u32>\n) {\n let jointIdx = workgroupId.x;\n let threadIdx = localId.x;\n let vertexCount = metadata.vertexCount;\n let jointCount = metadata.jointCount;\n\n // 🔧 边界检查:确保不处理超出范围的关节\n if (jointIdx >= jointCount) {\n return;\n }\n\n // 初始化局部累加器\n var localSumX: f32 = 0.0;\n var localSumY: f32 = 0.0;\n var localSumZ: f32 = 0.0;\n\n // 每个线程处理多个顶点(循环展开)\n // 8031 vertices / 256 threads ≈ 32 iterations per thread\n let stride = 256u;\n var vertexIdx = threadIdx;\n\n // 🔧 修复: 使用v_shaped而不是v_posed(与CPU逻辑一致)\n while (vertexIdx < vertexCount) {\n let regressorIdx = jointIdx * vertexCount + vertexIdx;\n let weight = jRegressor[regressorIdx];\n let vIdx = vertexIdx * 3u;\n \n // 🔧 边界检查:确保不访问超出范围的顶点\n if (vIdx + 2u < arrayLength(&vShaped)) {\n let vx = vShaped[vIdx];\n let vy = vShaped[vIdx + 1u];\n let vz = vShaped[vIdx + 2u];\n\n localSumX += weight * vx;\n localSumY += weight * vy;\n localSumZ += weight * vz;\n }\n\n vertexIdx += stride;\n }\n\n // 写入 shared memory\n sharedSumX[threadIdx] = localSumX;\n sharedSumY[threadIdx] = localSumY;\n sharedSumZ[threadIdx] = localSumZ;\n\n // 同步所有线程\n workgroupBarrier();\n\n // 🚀 优化: 树状归约 (256 → 128 → 64 → 32,然后展开最后 5 次迭代)\n // 前 3 次迭代需要 barrier (128 → 64 → 32)\n var activeThreads = 128u;\n for (var i = 0u; i < 3u; i++) {\n if (threadIdx < activeThreads) {\n sharedSumX[threadIdx] += sharedSumX[threadIdx + activeThreads];\n sharedSumY[threadIdx] += sharedSumY[threadIdx + activeThreads];\n sharedSumZ[threadIdx] += sharedSumZ[threadIdx + activeThreads];\n }\n workgroupBarrier();\n activeThreads = activeThreads / 2u;\n }\n\n // 🔧 展开最后 5 次迭代 (32 → 16 → 8 → 4 → 2 → 1)\n // 注意:即使在同一 subgroup 内,也需要 barrier 以确保正确性\n if (threadIdx < 32u) {\n sharedSumX[threadIdx] += sharedSumX[threadIdx + 16u];\n sharedSumY[threadIdx] += sharedSumY[threadIdx + 16u];\n sharedSumZ[threadIdx] += sharedSumZ[threadIdx + 16u];\n }\n workgroupBarrier();\n \n if (threadIdx < 16u) {\n sharedSumX[threadIdx] += sharedSumX[threadIdx + 8u];\n sharedSumY[threadIdx] += sharedSumY[threadIdx + 8u];\n sharedSumZ[threadIdx] += sharedSumZ[threadIdx + 8u];\n }\n workgroupBarrier();\n \n if (threadIdx < 8u) {\n sharedSumX[threadIdx] += sharedSumX[threadIdx + 4u];\n sharedSumY[threadIdx] += sharedSumY[threadIdx + 4u];\n sharedSumZ[threadIdx] += sharedSumZ[threadIdx + 4u];\n }\n workgroupBarrier();\n \n if (threadIdx < 4u) {\n sharedSumX[threadIdx] += sharedSumX[threadIdx + 2u];\n sharedSumY[threadIdx] += sharedSumY[threadIdx + 2u];\n sharedSumZ[threadIdx] += sharedSumZ[threadIdx + 2u];\n }\n workgroupBarrier();\n \n if (threadIdx < 2u) {\n sharedSumX[threadIdx] += sharedSumX[threadIdx + 1u];\n sharedSumY[threadIdx] += sharedSumY[threadIdx + 1u];\n sharedSumZ[threadIdx] += sharedSumZ[threadIdx + 1u];\n }\n workgroupBarrier();\n\n // 线程 0 写入最终结果\n if (threadIdx == 0u) {\n let outputIdx = jointIdx * 3u;\n joints[outputIdx] = sharedSumX[0];\n joints[outputIdx + 1u] = sharedSumY[0];\n joints[outputIdx + 2u] = sharedSumZ[0];\n }\n}\n\n";
15097
- const flameFKinematicsWGSL = "@group(0) @binding(0) var<uniform> params: FLAMEParams;\n@group(0) @binding(1) var<uniform> metadata: FLAMEMetadata;\n\n@group(1) @binding(0) var<storage, read> joints: array<f32>;\n@group(1) @binding(1) var<storage, read_write> jointTransforms: array<f32>;\n\n/**\n * 写入 4x4 矩阵到扁平数组 (列主序)\n */\nfn writeMat4(buffer: ptr<storage, array<f32>, read_write>, offset: u32, m: mat4x4<f32>) {\n (*buffer)[offset + 0u] = m[0][0];\n (*buffer)[offset + 1u] = m[0][1];\n (*buffer)[offset + 2u] = m[0][2];\n (*buffer)[offset + 3u] = m[0][3];\n (*buffer)[offset + 4u] = m[1][0];\n (*buffer)[offset + 5u] = m[1][1];\n (*buffer)[offset + 6u] = m[1][2];\n (*buffer)[offset + 7u] = m[1][3];\n (*buffer)[offset + 8u] = m[2][0];\n (*buffer)[offset + 9u] = m[2][1];\n (*buffer)[offset + 10u] = m[2][2];\n (*buffer)[offset + 11u] = m[2][3];\n (*buffer)[offset + 12u] = m[3][0];\n (*buffer)[offset + 13u] = m[3][1];\n (*buffer)[offset + 14u] = m[3][2];\n (*buffer)[offset + 15u] = m[3][3];\n}\n\n/**\n * 4x4 矩阵求逆 (简化版,假设是变换矩阵)\n * 对于变换矩阵 [R | t; 0 | 1],逆矩阵为 [R^T | -R^T*t; 0 | 1]\n *\n * 🔧 关键:WGSL 使用列主序!\n * m[0] = 第1列, m[1] = 第2列, m[2] = 第3列, m[3] = 第4列\n */\nfn invertTransform(m: mat4x4<f32>) -> mat4x4<f32> {\n // 🔧 正确提取旋转部分 (列主序)\n // m[col][row] -> M[row][col]\n let r00 = m[0][0]; let r01 = m[1][0]; let r02 = m[2][0]; // 第1行\n let r10 = m[0][1]; let r11 = m[1][1]; let r12 = m[2][1]; // 第2行\n let r20 = m[0][2]; let r21 = m[1][2]; let r22 = m[2][2]; // 第3行\n\n // 🔧 正确提取平移部分 (第4列)\n let tx = m[3][0]; // M[0][3]\n let ty = m[3][1]; // M[1][3]\n let tz = m[3][2]; // M[2][3]\n\n // 计算 R^T (旋转矩阵的转置)\n let rt00 = r00; let rt01 = r10; let rt02 = r20;\n let rt10 = r01; let rt11 = r11; let rt12 = r21;\n let rt20 = r02; let rt21 = r12; let rt22 = r22;\n\n // 计算 -R^T * t\n let ntx = -(rt00 * tx + rt01 * ty + rt02 * tz);\n let nty = -(rt10 * tx + rt11 * ty + rt12 * tz);\n let ntz = -(rt20 * tx + rt21 * ty + rt22 * tz);\n\n // 🔧 构建逆矩阵 (列主序)\n return mat4x4<f32>(\n vec4<f32>(rt00, rt10, rt20, 0.0), // 第1列\n vec4<f32>(rt01, rt11, rt21, 0.0), // 第2列\n vec4<f32>(rt02, rt12, rt22, 0.0), // 第3列\n vec4<f32>(ntx, nty, ntz, 1.0) // 第4列\n );\n}\n\n// ============================================================================\n// Compute Shader\n// ============================================================================\n\n@group(1) @binding(2) var<storage, read> parents: array<i32>;\n\n@compute @workgroup_size(1)\nfn main() {\n // 🔧 严格按照 CPU 路径逻辑:FLAME 标准有 5 个关节\n // CPU: poseToRotationMatrices 硬编码 5 个旋转矩阵\n // CPU: skinVerticesFlat 硬编码 5 个关节\n const NUM_JOINTS = 5u;\n \n // 🔧 使用 metadata 以防止编译器优化掉 binding\n // 确保 metadata 被实际使用(即使我们硬编码了 5)\n let jointCountFromMetadata = metadata.jointCount;\n // 验证:如果 metadata 中的 jointCount 不是 5,可能会有问题,但我们仍然使用硬编码的 5\n // 这只是为了防止编译器优化掉 metadata binding\n if (jointCountFromMetadata < NUM_JOINTS) {\n // 这个分支永远不会执行,但确保 metadata 被读取\n }\n \n // ========== Step 1: 读取关节位置 (绝对位置) ==========\n // 🔧 只读取前 5 个关节(与 CPU 逻辑一致)\n var J: array<vec3<f32>, NUM_JOINTS>;\n for (var i = 0u; i < NUM_JOINTS; i++) {\n let idx = i * 3u;\n J[i] = vec3<f32>(joints[idx], joints[idx + 1u], joints[idx + 2u]);\n }\n\n // ========== Step 2: 计算相对关节位置 ==========\n // C++: rel_joints[i] = joints[i] - joints[parents[i]]\n // CPU: for (size_t i = 1; i < num_joints; i++) { rel_joints[i] = rel_joints[i] - joints[parents[i]]; }\n var rel_J: array<vec3<f32>, NUM_JOINTS>;\n rel_J[0] = J[0]; // root: 无父关节\n for (var i = 1u; i < NUM_JOINTS; i++) {\n let parentIdx = parents[i];\n if (parentIdx >= 0) {\n rel_J[i] = J[i] - J[u32(parentIdx)];\n } else {\n rel_J[i] = J[i]; // 如果 parent 无效,使用绝对位置\n }\n }\n\n // ========== Step 3: 准备 pose 旋转矩阵 ==========\n // 🔧 严格按照 CPU: poseToRotationMatrices 从 15 维 full_pose 转换为 5 个旋转矩阵\n // full_pose[0:3] = rotation, [3:6] = neck, [6:9] = jaw, [9:12] = left_eye, [12:15] = right_eye\n let globalRotation = params.rotation.xyz; // full_pose[0:3]\n let neckPose = params.neckPose.xyz; // full_pose[3:6]\n let jawPose = params.jawPose.xyz; // full_pose[6:9]\n let leftEyePose = params.eyesPose[0].xyz; // full_pose[9:12] = [left_eye_pitch, left_eye_yaw, left_eye_roll]\n let rightEyePose = params.eyesPose[1].xyz; // full_pose[12:15] = [right_eye_pitch, right_eye_yaw, right_eye_roll]\n\n var R: array<mat3x3<f32>, NUM_JOINTS>;\n R[0] = rodrigues(globalRotation);\n R[1] = rodrigues(neckPose);\n R[2] = rodrigues(jawPose);\n R[3] = rodrigues(leftEyePose);\n R[4] = rodrigues(rightEyePose);\n\n // ========== Step 4: 创建局部变换矩阵 ==========\n // C++: transforms_mat[i] = Matrix4f(pose_matrices[i], rel_joints[i])\n var transforms_mat: array<mat4x4<f32>, NUM_JOINTS>;\n for (var i = 0u; i < NUM_JOINTS; i++) {\n transforms_mat[i] = makeTransform(R[i], rel_J[i]);\n }\n\n // ========== Step 5: 构建变换链 ==========\n // C++: transform_chain[0] = transforms_mat[0]\n // for (size_t i = 1; i < num_joints; i++) {\n // transform_chain[i] = transform_chain[parent] * transforms_mat[i]\n // }\n var transform_chain: array<mat4x4<f32>, NUM_JOINTS>;\n transform_chain[0] = transforms_mat[0]; // root\n \n for (var i = 1u; i < NUM_JOINTS; i++) {\n let parentIdx = parents[i];\n if (parentIdx >= 0 && parentIdx < i32(NUM_JOINTS)) {\n transform_chain[i] = transform_chain[u32(parentIdx)] * transforms_mat[i];\n } else {\n // CPU: 如果 parent 无效,使用 Identity\n transform_chain[i] = transforms_mat[i];\n }\n }\n\n // ========== Step 6: 应用 bind pose inverse ==========\n // C++: bind_pose = Matrix4f(Identity, joints[i])\n // rel_transforms[i] = transform_chain[i] * bind_pose.inverse()\n for (var i = 0u; i < NUM_JOINTS; i++) {\n let bind_pose = mat4x4<f32>(\n vec4<f32>(1.0, 0.0, 0.0, 0.0),\n vec4<f32>(0.0, 1.0, 0.0, 0.0),\n vec4<f32>(0.0, 0.0, 1.0, 0.0),\n vec4<f32>(J[i].x, J[i].y, J[i].z, 1.0)\n );\n\n let bind_pose_inv = invertTransform(bind_pose);\n let rel_transform = transform_chain[i] * bind_pose_inv;\n\n writeMat4(&jointTransforms, i * 16u, rel_transform);\n }\n}\n\n";
15098
- const flameLBSWGSL = "@group(0) @binding(0) var<uniform> metadata: FLAMEMetadata;\n\n@group(1) @binding(0) var<storage, read> vPosed: array<f32>;\n@group(1) @binding(1) var<storage, read> jointTransforms: array<f32>;\n@group(1) @binding(2) var<storage, read> lbsWeights: array<f32>;\n@group(1) @binding(3) var<storage, read_write> vDeformed: array<f32>;\n\n// 🚀 优化: 使用 shared memory 缓存 joint transforms (5 joints × 16 floats = 80 floats)\n// 所有线程都需要读取相同的 5 个 joint transforms,缓存到 shared memory 可以显著提升性能\nvar<workgroup> sharedJointTransforms: array<f32, 80>; // 5 joints × 16 floats\n\nfn readMat4FromShared(jointIdx: u32) -> mat4x4<f32> {\n let offset = jointIdx * 16u;\n return mat4x4<f32>(\n vec4<f32>(sharedJointTransforms[offset + 0u], sharedJointTransforms[offset + 1u], sharedJointTransforms[offset + 2u], sharedJointTransforms[offset + 3u]),\n vec4<f32>(sharedJointTransforms[offset + 4u], sharedJointTransforms[offset + 5u], sharedJointTransforms[offset + 6u], sharedJointTransforms[offset + 7u]),\n vec4<f32>(sharedJointTransforms[offset + 8u], sharedJointTransforms[offset + 9u], sharedJointTransforms[offset + 10u], sharedJointTransforms[offset + 11u]),\n vec4<f32>(sharedJointTransforms[offset + 12u], sharedJointTransforms[offset + 13u], sharedJointTransforms[offset + 14u], sharedJointTransforms[offset + 15u])\n );\n}\n\nfn transformPoint(m: mat4x4<f32>, p: vec3<f32>) -> vec3<f32> {\n let p4 = vec4<f32>(p, 1.0);\n let transformed = m * p4;\n return transformed.xyz;\n}\n\n@compute @workgroup_size(256)\nfn main(\n @builtin(global_invocation_id) globalId: vec3<u32>,\n @builtin(local_invocation_id) localId: vec3<u32>\n) {\n let vertexIdx = globalId.x;\n let threadIdx = localId.x;\n\n // 🚀 优化: 使用 shared memory 加载 joint transforms\n // 每个 workgroup 只需要加载一次,所有线程共享\n // 使用前 5 个线程加载 5 个 joint transforms (每个 16 floats)\n const NUM_JOINTS = 5u;\n if (threadIdx < NUM_JOINTS) {\n let globalOffset = threadIdx * 16u;\n let sharedOffset = threadIdx * 16u;\n // 加载 16 floats (一个 mat4x4)\n for (var i = 0u; i < 16u; i++) {\n sharedJointTransforms[sharedOffset + i] = jointTransforms[globalOffset + i];\n }\n }\n\n // 同步所有线程,确保 shared memory 已加载完成\n // 🔧 必须在边界检查之前执行 barrier,以保证 uniform control flow\n workgroupBarrier();\n\n // 🔧 边界检查:确保不处理超出范围的顶点(在 barrier 之后)\n let vertexCount = metadata.vertexCount;\n if (vertexIdx >= vertexCount) {\n return;\n }\n\n let baseIdx = vertexIdx * 3u;\n let vertex = vec3<f32>(\n vPosed[baseIdx],\n vPosed[baseIdx + 1u],\n vPosed[baseIdx + 2u]\n );\n\n // 🔧 严格按照 CPU 路径逻辑:FLAME 标准有 5 个关节\n // 🔧 使用 metadata 以防止编译器优化掉 binding\n let _jointCount = metadata.jointCount; // 防止优化,但实际使用硬编码的 5\n let weightBase = vertexIdx * NUM_JOINTS;\n\n // 🔧 完全按照 CPU 逻辑:只使用前 5 个关节的权重和变换矩阵\n let w0 = lbsWeights[weightBase + 0u];\n let w1 = lbsWeights[weightBase + 1u];\n let w2 = lbsWeights[weightBase + 2u];\n let w3 = lbsWeights[weightBase + 3u];\n let w4 = lbsWeights[weightBase + 4u];\n\n // 🚀 优化: 从 shared memory 读取 joint transforms\n let T0 = readMat4FromShared(0u);\n let T1 = readMat4FromShared(1u);\n let T2 = readMat4FromShared(2u);\n let T3 = readMat4FromShared(3u);\n let T4 = readMat4FromShared(4u);\n\n // 🔧 完全按照 CPU 逻辑:内联 transformPoint 并加权\n // 🔧 CPU的skinVerticesFlat不包含全局平移,所以这里也不应用\n var result = transformPoint(T0, vertex) * w0 +\n transformPoint(T1, vertex) * w1 +\n transformPoint(T2, vertex) * w2 +\n transformPoint(T3, vertex) * w3 +\n transformPoint(T4, vertex) * w4;\n\n // ❌ 移除:CPU的skinVerticesFlat不包含全局平移\n // 全局平移应该在LBS之后、Face Geometry之前单独应用(如果需要)\n // result += params.translation.xyz;\n\n vDeformed[baseIdx] = result.x;\n vDeformed[baseIdx + 1u] = result.y;\n vDeformed[baseIdx + 2u] = result.z;\n}\n\n";
15099
- const flameFaceGeometryWGSL = "/**\n * FLAME Face Geometry Compute Shader\n *\n * 为每个三角形面片计算几何信息:\n * - center: 面片重心 (v0 + v1 + v2) / 3\n * - scale: 面片缩放(基于面积)\n * - quaternion: 面片方向(从局部坐标系转换)\n *\n * 这些几何信息用于后续的 3DGS splat 变换\n *\n * Input:\n * - v_deformed: [vertexCount × 3] 最终变形顶点\n * - faces: [faceCount × 3] 面片索引\n *\n * Output:\n * - faceGeometries: [faceCount × 8] (center xyz, scale, quat xyzw)\n */\n\n// 导入公共定义\n// (WGSL 不支持 #include,在 TypeScript 中手动拼接)\n\n@group(0) @binding(0) var<uniform> params: FLAMEParams;\n@group(0) @binding(1) var<uniform> metadata: FLAMEMetadata;\n\n@group(1) @binding(0) var<storage, read> vDeformed: array<f32>;\n@group(1) @binding(1) var<storage, read> faces: array<u32>;\n@group(1) @binding(2) var<storage, read_write> faceGeometries: array<f32>;\n\n/**\n * 安全归一化向量(处理零长度情况)\n * 🔧 与CPU实现一致:长度为0时返回零向量\n */\nfn safeNormalize(v: vec3<f32>) -> vec3<f32> {\n let len = length(v);\n if (len > 1e-8) {\n return v / len;\n } else {\n // 🔧 与CPU一致:返回零向量(而不是默认方向)\n return vec3<f32>(0.0, 0.0, 0.0);\n }\n}\n\n/**\n * 3x3 旋转矩阵转四元数 (xyzw 顺序)\n *\n * 使用 Shepperd's method 确保数值稳定性\n */\nfn matrixToQuaternion(m: mat3x3<f32>) -> vec4<f32> {\n let trace = m[0][0] + m[1][1] + m[2][2];\n\n var quat: vec4<f32>;\n\n if (trace > 0.0) {\n // w 是最大分量\n let s = sqrt(trace + 1.0) * 2.0; // s = 4 * w\n quat.w = 0.25 * s;\n quat.x = (m[2][1] - m[1][2]) / s;\n quat.y = (m[0][2] - m[2][0]) / s;\n quat.z = (m[1][0] - m[0][1]) / s;\n } else if (m[0][0] > m[1][1] && m[0][0] > m[2][2]) {\n // x 是最大分量\n let s = sqrt(1.0 + m[0][0] - m[1][1] - m[2][2]) * 2.0; // s = 4 * x\n quat.w = (m[2][1] - m[1][2]) / s;\n quat.x = 0.25 * s;\n quat.y = (m[0][1] + m[1][0]) / s;\n quat.z = (m[0][2] + m[2][0]) / s;\n } else if (m[1][1] > m[2][2]) {\n // y 是最大分量\n let s = sqrt(1.0 + m[1][1] - m[0][0] - m[2][2]) * 2.0; // s = 4 * y\n quat.w = (m[0][2] - m[2][0]) / s;\n quat.x = (m[0][1] + m[1][0]) / s;\n quat.y = 0.25 * s;\n quat.z = (m[1][2] + m[2][1]) / s;\n } else {\n // z 是最大分量\n let s = sqrt(1.0 + m[2][2] - m[0][0] - m[1][1]) * 2.0; // s = 4 * z\n quat.w = (m[1][0] - m[0][1]) / s;\n quat.x = (m[0][2] + m[2][0]) / s;\n quat.y = (m[1][2] + m[2][1]) / s;\n quat.z = 0.25 * s;\n }\n\n // 手动归一化(与CPU实现一致)\n let len = length(quat);\n if (len > 1e-8) {\n // 🔧 确保 w 分量为正,消除符号歧义(q 和 -q 表示同一个旋转)\n // 在归一化之前检查未归一化的w(与CPU实现完全一致)\n var normalized = quat;\n if (quat.w < 0.0) {\n normalized.x = -quat.x;\n normalized.y = -quat.y;\n normalized.z = -quat.z;\n normalized.w = -quat.w;\n }\n \n // 然后归一化\n return normalized / len;\n } else {\n // 默认单位四元数\n return vec4<f32>(0.0, 0.0, 0.0, 1.0);\n }\n}\n\nfn computeFaceOrientationAndScaling(\n v0: vec3<f32>,\n v1: vec3<f32>,\n v2: vec3<f32>\n) -> mat3x3<f32> {\n let edge1 = v1 - v0;\n let edge2 = v2 - v0;\n\n let axis0 = safeNormalize(edge1);\n let tempAxis1 = cross(axis0, edge2);\n let axis1 = safeNormalize(tempAxis1);\n let tempAxis2 = cross(axis1, axis0);\n let axis2 = safeNormalize(tempAxis2) * -1.0;\n\n return mat3x3<f32>(\n axis0.x, axis1.x, axis2.x,\n axis0.y, axis1.y, axis2.y,\n axis0.z, axis1.z, axis2.z\n );\n}\n\n@compute @workgroup_size(256)\nfn main(@builtin(global_invocation_id) globalId: vec3<u32>) {\n let faceIdx = globalId.x;\n \n // 🔧 读取metadata以防止编译器优化掉binding\n let _faceCount = metadata.faceCount;\n let _vertexCount = metadata.vertexCount;\n\n let faceBaseIdx = faceIdx * 3u;\n let idx0 = faces[faceBaseIdx];\n let idx1 = faces[faceBaseIdx + 1u];\n let idx2 = faces[faceBaseIdx + 2u];\n\n // 🔧 读取顶点并应用全局平移(与CPU逻辑一致:CPU的Face Geometry使用已应用平移的顶点)\n let v0 = vec3<f32>(\n vDeformed[idx0 * 3u],\n vDeformed[idx0 * 3u + 1u],\n vDeformed[idx0 * 3u + 2u]\n ) + params.translation.xyz;\n\n let v1 = vec3<f32>(\n vDeformed[idx1 * 3u],\n vDeformed[idx1 * 3u + 1u],\n vDeformed[idx1 * 3u + 2u]\n ) + params.translation.xyz;\n\n let v2 = vec3<f32>(\n vDeformed[idx2 * 3u],\n vDeformed[idx2 * 3u + 1u],\n vDeformed[idx2 * 3u + 2u]\n ) + params.translation.xyz;\n\n let center = (v0 + v1 + v2) / 3.0;\n\n let edge1 = v1 - v0;\n let edge2 = v2 - v0;\n let orientationMatrix = computeFaceOrientationAndScaling(v0, v1, v2);\n\n let axis0 = safeNormalize(edge1);\n let tempAxis1 = cross(axis0, edge2);\n let axis1 = safeNormalize(tempAxis1);\n let tempAxis2 = cross(axis1, axis0);\n let axis2 = safeNormalize(tempAxis2) * -1.0;\n\n let s0 = length(edge1);\n let s1 = abs(dot(axis2, edge2));\n let scale = (s0 + s1) / 2.0;\n\n let quat = matrixToQuaternion(orientationMatrix);\n\n let outputIdx = faceIdx * 8u;\n faceGeometries[outputIdx] = center.x;\n faceGeometries[outputIdx + 1u] = center.y;\n faceGeometries[outputIdx + 2u] = center.z;\n faceGeometries[outputIdx + 3u] = scale;\n faceGeometries[outputIdx + 4u] = quat.x;\n faceGeometries[outputIdx + 5u] = quat.y;\n faceGeometries[outputIdx + 6u] = quat.z;\n faceGeometries[outputIdx + 7u] = quat.w;\n}\n\n";
15100
- class FLAMEPipeline {
15101
- // Face geometry 输出 [faceCount × 8]
15102
- constructor(device, buffers, vertexCount, faceCount, jointCount) {
15103
- __publicField(this, "device");
15104
- __publicField(this, "buffers");
15105
- // 元数据
15106
- __publicField(this, "vertexCount");
15107
- __publicField(this, "faceCount");
15108
- __publicField(this, "jointCount");
15109
- // 计算管线
15110
- __publicField(this, "shapeBlendPipeline");
15111
- __publicField(this, "poseDeformPipeline");
15112
- __publicField(this, "jointRegressPipeline");
15113
- __publicField(this, "fkPipeline");
15114
- __publicField(this, "lbsPipeline");
15115
- __publicField(this, "faceGeometryPipeline");
15116
- __publicField(this, "shapeBlendParamsBindGroup");
15117
- __publicField(this, "poseDeformParamsBindGroup");
15118
- __publicField(this, "jointRegressMetadataBindGroup");
15119
- __publicField(this, "fkParamsBindGroup");
15120
- __publicField(this, "lbsMetadataBindGroup");
15121
- __publicField(this, "shapeBlendBindGroup");
15122
- __publicField(this, "poseDeformBindGroup");
15123
- __publicField(this, "jointRegressBindGroup");
15124
- __publicField(this, "fkBindGroup");
15125
- __publicField(this, "lbsBindGroup");
15126
- __publicField(this, "faceGeometryParamsBindGroup");
15127
- __publicField(this, "faceGeometryBindGroup");
15128
- // 中间缓冲区
15129
- __publicField(this, "vShapedBuffer");
15130
- // Shape blending 输出
15131
- __publicField(this, "vPosedBuffer");
15132
- // Pose deformation 输出
15133
- __publicField(this, "jointsBuffer");
15134
- // Joint regression 输出 [jointCount × 3]
15135
- __publicField(this, "jointTransformsBuffer");
15136
- // FK 输出 [jointCount × 16] (mat4)
15137
- __publicField(this, "vDeformedBuffer");
15138
- // LBS 输出
15139
- __publicField(this, "faceGeometriesBuffer");
15140
- this.device = device;
15141
- this.buffers = buffers;
15142
- this.vertexCount = vertexCount;
15143
- this.faceCount = faceCount;
15144
- this.jointCount = jointCount;
15145
- this.initialize();
15146
- }
15147
- initialize() {
15148
- logger.log("🔧 Initializing FLAME Pipeline...");
15149
- this.createIntermediateBuffers();
15150
- this.clearIntermediateBuffers();
15151
- this.createComputePipelines();
15152
- this.createBindGroups();
15153
- logger.log("✅ FLAME Pipeline initialized");
15154
- }
15155
- /**
15156
- * 创建中间缓冲区
15157
- */
15158
- createIntermediateBuffers() {
15159
- const vertexBufferSize = this.vertexCount * 3 * 4;
15160
- const safeJointCount = Math.max(1, this.jointCount);
15161
- const jointBufferSize = safeJointCount * 3 * 4;
15162
- const transformBufferSize = safeJointCount * 16 * 4;
15163
- const faceGeometryBufferSize = this.faceCount * 8 * 4;
15164
- const minBufferSize = 4;
15165
- this.vShapedBuffer = this.device.createBuffer({
15166
- label: "v_shaped",
15167
- size: Math.max(vertexBufferSize, minBufferSize),
15168
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
15169
- });
15170
- this.vPosedBuffer = this.device.createBuffer({
15171
- label: "v_posed",
15172
- size: Math.max(vertexBufferSize, minBufferSize),
15173
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
15174
- });
15175
- this.jointsBuffer = this.device.createBuffer({
15176
- label: "joints",
15177
- size: Math.max(jointBufferSize, minBufferSize),
15178
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
15179
- });
15180
- this.jointTransformsBuffer = this.device.createBuffer({
15181
- label: "joint_transforms",
15182
- size: Math.max(transformBufferSize, minBufferSize),
15183
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
15184
- });
15185
- this.vDeformedBuffer = this.device.createBuffer({
15186
- label: "v_deformed",
15187
- size: vertexBufferSize,
15188
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
15189
- });
15190
- this.faceGeometriesBuffer = this.device.createBuffer({
15191
- label: "face_geometries",
15192
- size: faceGeometryBufferSize,
15193
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
15194
- });
15195
- }
15196
- /**
15197
- * 清零所有中间缓冲区 (避免未初始化的垃圾数据)
15198
- * 🔧 关键修复: LBS shader 如果某些顶点权重全为0,会跳过不写入,导致保留垃圾数据
15199
- */
15200
- clearIntermediateBuffers() {
15201
- const encoder = this.device.createCommandEncoder({ label: "Clear FLAME Buffers" });
15202
- encoder.clearBuffer(this.vShapedBuffer);
15203
- encoder.clearBuffer(this.vPosedBuffer);
15204
- encoder.clearBuffer(this.jointsBuffer);
15205
- encoder.clearBuffer(this.jointTransformsBuffer);
15206
- encoder.clearBuffer(this.vDeformedBuffer);
15207
- encoder.clearBuffer(this.faceGeometriesBuffer);
15208
- this.device.queue.submit([encoder.finish()]);
15209
- logger.log("🧹 Cleared all intermediate FLAME buffers");
15210
- }
15211
- /**
15212
- * 创建计算管线
15213
- */
15214
- createComputePipelines() {
15215
- this.shapeBlendPipeline = this.createPipeline(
15216
- flameShapeBlendWGSL,
15217
- "Shape Blending Pipeline"
15218
- );
15219
- this.poseDeformPipeline = this.createPipeline(
15220
- flamePoseDeformWGSL,
15221
- "Pose Deformation Pipeline"
15222
- );
15223
- this.jointRegressPipeline = this.createPipeline(
15224
- flameJointRegressWGSL,
15225
- "Joint Regression Pipeline"
15226
- );
15227
- this.fkPipeline = this.createPipeline(
15228
- flameFKinematicsWGSL,
15229
- "Forward Kinematics Pipeline"
15230
- );
15231
- this.lbsPipeline = this.createPipeline(
15232
- flameLBSWGSL,
15233
- "LBS Skinning Pipeline"
15234
- );
15235
- this.faceGeometryPipeline = this.createPipeline(
15236
- flameFaceGeometryWGSL,
15237
- "Face Geometry Pipeline"
15238
- );
15239
- }
15240
- /**
15241
- * 创建单个计算管线
15242
- */
15243
- createPipeline(shaderCode, label) {
15244
- const fullShaderCode = flameCommonWGSL + "\n" + shaderCode;
15245
- const shaderModule = this.device.createShaderModule({
15246
- label: `${label} Shader`,
15247
- code: fullShaderCode
15248
- });
15249
- return this.device.createComputePipeline({
15250
- label,
15251
- layout: "auto",
15252
- compute: {
15253
- module: shaderModule,
15254
- entryPoint: "main"
15255
- }
15256
- });
15257
- }
15258
- /**
15259
- * 创建绑定组
15260
- */
15261
- createBindGroups() {
15262
- this.shapeBlendParamsBindGroup = this.device.createBindGroup({
15263
- label: "Shape Blend Params Bind Group",
15264
- layout: this.shapeBlendPipeline.getBindGroupLayout(0),
15265
- entries: [
15266
- { binding: 0, resource: { buffer: this.buffers.metadata } },
15267
- { binding: 1, resource: { buffer: this.buffers.activeShapeIndices } },
15268
- // 🚀 活跃shape参数索引
15269
- { binding: 2, resource: { buffer: this.buffers.activeShapeValues } },
15270
- // 🚀 活跃shape参数值
15271
- { binding: 3, resource: { buffer: this.buffers.frameParams } }
15272
- // 动态 Uniform Buffer
15273
- ]
15274
- });
15275
- this.poseDeformParamsBindGroup = this.device.createBindGroup({
15276
- label: "Pose Deform Params Bind Group",
15277
- layout: this.poseDeformPipeline.getBindGroupLayout(0),
15278
- entries: [
15279
- { binding: 0, resource: { buffer: this.buffers.frameParams } },
15280
- { binding: 1, resource: { buffer: this.buffers.metadata } }
15281
- ]
15282
- });
15283
- this.fkParamsBindGroup = this.device.createBindGroup({
15284
- label: "FK Params Bind Group",
15285
- layout: this.fkPipeline.getBindGroupLayout(0),
15286
- entries: [
15287
- { binding: 0, resource: { buffer: this.buffers.frameParams } },
15288
- { binding: 1, resource: { buffer: this.buffers.metadata } }
15289
- ]
15290
- });
15291
- this.jointRegressMetadataBindGroup = this.device.createBindGroup({
15292
- label: "Joint Regress Metadata Bind Group",
15293
- layout: this.jointRegressPipeline.getBindGroupLayout(0),
15294
- entries: [
15295
- { binding: 1, resource: { buffer: this.buffers.metadata } }
15296
- ]
15297
- });
15298
- this.lbsMetadataBindGroup = this.device.createBindGroup({
15299
- label: "LBS Metadata Bind Group",
15300
- layout: this.lbsPipeline.getBindGroupLayout(0),
15301
- entries: [
15302
- { binding: 0, resource: { buffer: this.buffers.metadata } }
15303
- ]
15304
- });
15305
- const staticOffsetBuffer = this.buffers.staticOffset || this.device.createBuffer({
15306
- label: "dummy_static_offset",
15307
- size: 4,
15308
- // 最小 buffer 大小
15309
- usage: GPUBufferUsage.STORAGE
15310
- });
15311
- this.shapeBlendBindGroup = this.device.createBindGroup({
15312
- label: "Shape Blend Bind Group",
15313
- layout: this.shapeBlendPipeline.getBindGroupLayout(1),
15314
- entries: [
15315
- { binding: 0, resource: { buffer: this.buffers.vTemplate } },
15316
- { binding: 1, resource: { buffer: this.buffers.shapedirs } },
15317
- { binding: 2, resource: { buffer: this.vShapedBuffer } },
15318
- { binding: 3, resource: { buffer: staticOffsetBuffer } }
15319
- ]
15320
- });
15321
- this.poseDeformBindGroup = this.device.createBindGroup({
15322
- label: "Pose Deform Bind Group",
15323
- layout: this.poseDeformPipeline.getBindGroupLayout(1),
15324
- entries: [
15325
- { binding: 0, resource: { buffer: this.vShapedBuffer } },
15326
- { binding: 1, resource: { buffer: this.buffers.posedirs } },
15327
- { binding: 2, resource: { buffer: this.vPosedBuffer } }
15328
- ]
15329
- });
15330
- this.jointRegressBindGroup = this.device.createBindGroup({
15331
- label: "Joint Regress Bind Group",
15332
- layout: this.jointRegressPipeline.getBindGroupLayout(1),
15333
- entries: [
15334
- { binding: 0, resource: { buffer: this.vShapedBuffer } },
15335
- // 🔧 修复: 使用v_shaped
15336
- { binding: 1, resource: { buffer: this.buffers.jRegressor } },
15337
- { binding: 2, resource: { buffer: this.jointsBuffer } }
15338
- ]
15339
- });
15340
- this.fkBindGroup = this.device.createBindGroup({
15341
- label: "FK Bind Group",
15342
- layout: this.fkPipeline.getBindGroupLayout(1),
15343
- entries: [
15344
- { binding: 0, resource: { buffer: this.jointsBuffer } },
15345
- { binding: 1, resource: { buffer: this.jointTransformsBuffer } },
15346
- { binding: 2, resource: { buffer: this.buffers.parents } }
15347
- // 🔧 添加 parents buffer
15348
- ]
15349
- });
15350
- this.lbsBindGroup = this.device.createBindGroup({
15351
- label: "LBS Bind Group",
15352
- layout: this.lbsPipeline.getBindGroupLayout(1),
15353
- entries: [
15354
- { binding: 0, resource: { buffer: this.vPosedBuffer } },
15355
- { binding: 1, resource: { buffer: this.jointTransformsBuffer } },
15356
- { binding: 2, resource: { buffer: this.buffers.lbsWeights } },
15357
- { binding: 3, resource: { buffer: this.vDeformedBuffer } }
15358
- ]
15359
- });
15360
- this.faceGeometryParamsBindGroup = this.device.createBindGroup({
15361
- label: "Face Geometry Params Bind Group",
15362
- layout: this.faceGeometryPipeline.getBindGroupLayout(0),
15363
- entries: [
15364
- { binding: 0, resource: { buffer: this.buffers.frameParams } },
15365
- { binding: 1, resource: { buffer: this.buffers.metadata } }
15366
- ]
15367
- });
15368
- this.faceGeometryBindGroup = this.device.createBindGroup({
15369
- label: "Face Geometry Bind Group",
15370
- layout: this.faceGeometryPipeline.getBindGroupLayout(1),
15371
- entries: [
15372
- { binding: 0, resource: { buffer: this.vDeformedBuffer } },
15373
- { binding: 1, resource: { buffer: this.buffers.faces } },
15374
- { binding: 2, resource: { buffer: this.faceGeometriesBuffer } }
15375
- ]
15376
- });
15377
- }
15378
- /**
15379
- * 计算一帧 FLAME (主入口)
15380
- * 🚀 优化: 拆分为6个独立pass,支持详细的GPU profiling
15381
- */
15382
- compute(commandEncoder) {
15383
- const vertexWorkgroups = Math.ceil(this.vertexCount / 256);
15384
- const faceWorkgroups = Math.ceil(this.faceCount / 256);
15385
- const shapePass = commandEncoder.beginComputePass({
15386
- label: "FLAME Shape Blending"
15387
- });
15388
- shapePass.setPipeline(this.shapeBlendPipeline);
15389
- shapePass.setBindGroup(0, this.shapeBlendParamsBindGroup);
15390
- shapePass.setBindGroup(1, this.shapeBlendBindGroup);
15391
- shapePass.dispatchWorkgroups(vertexWorkgroups);
15392
- shapePass.end();
15393
- const NUM_JOINTS = 5;
15394
- const actualJointCount = Math.min(this.jointCount, NUM_JOINTS);
15395
- const jointRegressPass = commandEncoder.beginComputePass({
15396
- label: "FLAME Joint Regression"
15397
- });
15398
- jointRegressPass.setPipeline(this.jointRegressPipeline);
15399
- jointRegressPass.setBindGroup(0, this.jointRegressMetadataBindGroup);
15400
- jointRegressPass.setBindGroup(1, this.jointRegressBindGroup);
15401
- jointRegressPass.dispatchWorkgroups(actualJointCount, 1, 1);
15402
- jointRegressPass.end();
15403
- const posePass = commandEncoder.beginComputePass({
15404
- label: "FLAME Pose Deformation"
15405
- });
15406
- posePass.setPipeline(this.poseDeformPipeline);
15407
- posePass.setBindGroup(0, this.poseDeformParamsBindGroup);
15408
- posePass.setBindGroup(1, this.poseDeformBindGroup);
15409
- posePass.dispatchWorkgroups(vertexWorkgroups);
15410
- posePass.end();
15411
- const fkPass = commandEncoder.beginComputePass({
15412
- label: "FLAME Forward Kinematics"
15413
- });
15414
- fkPass.setPipeline(this.fkPipeline);
15415
- fkPass.setBindGroup(0, this.fkParamsBindGroup);
15416
- fkPass.setBindGroup(1, this.fkBindGroup);
15417
- fkPass.dispatchWorkgroups(1, 1, 1);
15418
- fkPass.end();
15419
- const lbsPass = commandEncoder.beginComputePass({
15420
- label: "FLAME LBS"
15421
- });
15422
- lbsPass.setPipeline(this.lbsPipeline);
15423
- lbsPass.setBindGroup(0, this.lbsMetadataBindGroup);
15424
- lbsPass.setBindGroup(1, this.lbsBindGroup);
15425
- lbsPass.dispatchWorkgroups(vertexWorkgroups);
15426
- lbsPass.end();
15427
- const faceGeomPass = commandEncoder.beginComputePass({
15428
- label: "FLAME Face Geometry"
15429
- });
15430
- faceGeomPass.setPipeline(this.faceGeometryPipeline);
15431
- faceGeomPass.setBindGroup(0, this.faceGeometryParamsBindGroup);
15432
- faceGeomPass.setBindGroup(1, this.faceGeometryBindGroup);
15433
- faceGeomPass.dispatchWorkgroups(faceWorkgroups);
15434
- faceGeomPass.end();
15435
- return {
15436
- faceGeometries: this.faceGeometriesBuffer,
15437
- faceCount: this.faceCount
15438
- };
15439
- }
15440
- /**
15441
- * 清理资源
15442
- */
15443
- destroy() {
15444
- var _a, _b, _c, _d, _e2, _f;
15445
- (_a = this.vShapedBuffer) == null ? void 0 : _a.destroy();
15446
- (_b = this.vPosedBuffer) == null ? void 0 : _b.destroy();
15447
- (_c = this.jointsBuffer) == null ? void 0 : _c.destroy();
15448
- (_d = this.jointTransformsBuffer) == null ? void 0 : _d.destroy();
15449
- (_e2 = this.vDeformedBuffer) == null ? void 0 : _e2.destroy();
15450
- (_f = this.faceGeometriesBuffer) == null ? void 0 : _f.destroy();
15451
- logger.log("🧹 FLAME Pipeline destroyed");
15452
- }
15453
- }
15454
- class FLAMEGPUBuffers {
15455
- constructor() {
15456
- __publicField(this, "device", null);
15457
- __publicField(this, "buffers", null);
15458
- // 缓存元数据
15459
- __publicField(this, "vertexCount", 0);
15460
- __publicField(this, "faceCount", 0);
15461
- __publicField(this, "jointCount", 0);
15462
- __publicField(this, "shapeParamCount", 0);
15463
- __publicField(this, "poseParamCount", 0);
15464
- __publicField(this, "staticOffsetCount", 0);
15465
- __publicField(this, "activeShapeCount", 0);
15466
- // 🚀 活跃shape参数数量
15467
- // 🚀 优化: 缓存参数数据数组,避免每帧创建新数组 (减少 GC 压力)
15468
- // 减小size:移除 shapeParams[300],只保留动态参数
15469
- __publicField(this, "paramDataCache", new Float32Array(32 * 4));
15470
- }
15471
- // 32 vec4 = 128 floats (expr[100] + poses[28])
15472
- /**
15473
- * 初始化 GPU 缓冲区并上传模板数据
15474
- * 🚀 优化: 需要传入 characterHandle 以获取静态 shape parameters
15475
- * @param activeShapeParams 活跃shape参数(零参数过滤优化,可选)
15476
- */
15477
- initialize(device, templateData, _shapeParams, activeShapeParams) {
15478
- var _a;
15479
- this.device = device;
15480
- this.vertexCount = templateData.vertexCount;
15481
- this.faceCount = templateData.faceCount;
15482
- this.jointCount = templateData.jointCount;
15483
- this.shapeParamCount = templateData.shapeParamCount;
15484
- this.poseParamCount = templateData.poseParamCount;
15485
- const ORIGINAL_FLAME_VERTEX_COUNT = 5023;
15486
- this.staticOffsetCount = Math.min(templateData.staticOffsetCount, ORIGINAL_FLAME_VERTEX_COUNT);
15487
- if (this.vertexCount === 0) {
15488
- throw new Error(`Invalid vertexCount: ${this.vertexCount}`);
15489
- }
15490
- if (this.faceCount === 0) {
15491
- throw new Error(`Invalid faceCount: ${this.faceCount}`);
15492
- }
15493
- if (this.jointCount === 0) {
15494
- throw new Error(`Invalid jointCount: ${this.jointCount}`);
15495
- }
15496
- const expectedJRegressorSize = this.jointCount * this.vertexCount * 4;
15497
- if (templateData.jRegressor.byteLength !== expectedJRegressorSize) {
15498
- throw new Error(`Invalid jRegressor size: expected ${expectedJRegressorSize}, got ${templateData.jRegressor.byteLength}`);
15499
- }
15500
- logger.log("🔧 FLAME metadata validation:", {
15501
- vertexCount: this.vertexCount,
15502
- faceCount: this.faceCount,
15503
- jointCount: this.jointCount,
15504
- jRegressorSize: templateData.jRegressor.byteLength,
15505
- expectedJRegressorSize
15506
- });
15507
- this.activeShapeCount = (activeShapeParams == null ? void 0 : activeShapeParams.count) || 0;
15508
- this.buffers = {
15509
- vTemplate: this.createStorageBuffer("v_template", templateData.vTemplate),
15510
- shapedirs: this.createStorageBuffer("shapedirs", templateData.shapedirs),
15511
- posedirs: this.createStorageBuffer("posedirs", templateData.posedirs),
15512
- jRegressor: this.createStorageBuffer("J_regressor", templateData.jRegressor),
15513
- lbsWeights: this.createStorageBuffer("lbs_weights", templateData.lbsWeights),
15514
- parents: this.createStorageBuffer("parents", templateData.parents),
15515
- faces: this.createStorageBuffer("faces", templateData.faces),
15516
- staticOffset: templateData.staticOffset ? this.createStorageBuffer(
15517
- "static_offset",
15518
- templateData.staticOffset.slice(0, this.staticOffsetCount * 3)
15519
- // 只上传前 5023 个顶点
15520
- ) : null,
15521
- // 🚀 优化: 使用活跃shape参数(零参数过滤)
15522
- activeShapeIndices: activeShapeParams ? this.createStorageBuffer("active_shape_indices", activeShapeParams.activeIndices) : this.createStorageBuffer("active_shape_indices", new Uint32Array(0)),
15523
- // 回退:空数组
15524
- activeShapeValues: activeShapeParams ? this.createStorageBuffer("active_shape_values", activeShapeParams.activeValues) : this.createStorageBuffer("active_shape_values", new Float32Array(0)),
15525
- // 回退:空数组
15526
- frameParams: this.createFrameParamsBuffer(),
15527
- metadata: this.createMetadataBuffer()
15528
- };
15529
- const totalSize = (templateData.vTemplate.byteLength + templateData.shapedirs.byteLength + templateData.posedirs.byteLength + templateData.jRegressor.byteLength + templateData.lbsWeights.byteLength + templateData.parents.byteLength + templateData.faces.byteLength + (((_a = templateData.staticOffset) == null ? void 0 : _a.byteLength) || 0)) / 1024 / 1024;
15530
- logger.log(`✅ FLAME GPU buffers initialized (${totalSize.toFixed(2)} MB uploaded)`);
15531
- logger.log(` Vertices: ${this.vertexCount}, Faces: ${this.faceCount}, Joints: ${this.jointCount}`);
15532
- if (this.staticOffsetCount > 0) {
15533
- logger.log(` Static offset: ${this.staticOffsetCount} vertices`);
15534
- }
15535
- }
15536
- /**
15537
- * 创建 Storage Buffer 并上传数据
15538
- */
15539
- createStorageBuffer(label, data) {
15540
- const minBufferSize = 4;
15541
- const bufferSize = Math.max(data.byteLength, minBufferSize);
15542
- const buffer = this.device.createBuffer({
15543
- label: `FLAME ${label}`,
15544
- size: bufferSize,
15545
- // 🔧 添加 COPY_SRC 以支持 debug 读取
15546
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
15547
- mappedAtCreation: true
15548
- });
15549
- if (data instanceof Float32Array) {
15550
- new Float32Array(buffer.getMappedRange()).set(data);
15551
- } else if (data instanceof Int32Array) {
15552
- new Int32Array(buffer.getMappedRange()).set(data);
15553
- } else if (data instanceof Uint32Array) {
15554
- new Uint32Array(buffer.getMappedRange()).set(data);
15555
- }
15556
- buffer.unmap();
15557
- return buffer;
15558
- }
15559
- /**
15560
- * 创建帧参数 Uniform Buffer
15561
- * 🚀 优化: 移除 shapeParams,减小 70% 大小
15562
- *
15563
- * Layout (std140):
15564
- * - exprParams: vec4[25] (100 floats, padded)
15565
- * - rotation: vec4 (3 floats + padding)
15566
- * - translation: vec4 (3 floats + padding)
15567
- * - neckPose: vec4 (3 floats + padding)
15568
- * - jawPose: vec4 (3 floats + padding)
15569
- * - eyesPose: vec4[2] (6 floats, split into 2 vec4)
15570
- * - eyelid: vec4 (2 floats + padding)
15571
- */
15572
- createFrameParamsBuffer() {
15573
- const size = 25 * 16 + // exprParams (25 vec4)
15574
- 16 + // rotation (1 vec4)
15575
- 16 + // translation (1 vec4)
15576
- 16 + // neckPose (1 vec4)
15577
- 16 + // jawPose (1 vec4)
15578
- 2 * 16 + // eyesPose (2 vec4)
15579
- 16;
15580
- return this.device.createBuffer({
15581
- label: "FLAME frame params",
15582
- size,
15583
- // 🔧 添加 COPY_SRC 以支持 debug 读取
15584
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC
15585
- });
15586
- }
15587
- /**
15588
- * 创建元数据 Uniform Buffer
15589
- *
15590
- * Layout:
15591
- * - vertexCount: u32
15592
- * - faceCount: u32
15593
- * - jointCount: u32
15594
- * - shapeParamCount: u32
15595
- * - poseParamCount: u32
15596
- * - staticOffsetCount: u32
15597
- * (padding to 256 bytes for alignment)
15598
- */
15599
- createMetadataBuffer() {
15600
- const buffer = this.device.createBuffer({
15601
- label: "FLAME metadata",
15602
- size: 256,
15603
- // 足够大且对齐
15604
- // 🔧 添加 COPY_SRC 以支持 debug 读取
15605
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
15606
- mappedAtCreation: true
15607
- });
15608
- const view = new Uint32Array(buffer.getMappedRange());
15609
- view[0] = this.vertexCount;
15610
- view[1] = this.faceCount;
15611
- view[2] = this.jointCount;
15612
- view[3] = this.shapeParamCount;
15613
- view[4] = this.poseParamCount;
15614
- view[5] = this.staticOffsetCount;
15615
- view[6] = this.activeShapeCount;
15616
- buffer.unmap();
15617
- return buffer;
15618
- }
15619
- /**
15620
- * 更新每帧参数
15621
- * 🚀 优化: 移除 shapeParams 打包,减少 70% 上传量
15622
- */
15623
- updateFrameParams(params) {
15624
- if (!this.buffers || !this.device) {
15625
- throw new Error("FLAME GPU buffers not initialized");
15626
- }
15627
- const data = this.paramDataCache;
15628
- let offset = 0;
15629
- for (let i2 = 0; i2 < 100; i2 += 4) {
15630
- data[offset++] = params.exprParams[i2] || 0;
15631
- data[offset++] = params.exprParams[i2 + 1] || 0;
15632
- data[offset++] = params.exprParams[i2 + 2] || 0;
15633
- data[offset++] = params.exprParams[i2 + 3] || 0;
15634
- }
15635
- data[offset++] = params.rotation[0] || 0;
15636
- data[offset++] = params.rotation[1] || 0;
15637
- data[offset++] = params.rotation[2] || 0;
15638
- data[offset++] = 0;
15639
- data[offset++] = params.translation[0] || 0;
15640
- data[offset++] = params.translation[1] || 0;
15641
- data[offset++] = params.translation[2] || 0;
15642
- data[offset++] = 0;
15643
- data[offset++] = params.neckPose[0] || 0;
15644
- data[offset++] = params.neckPose[1] || 0;
15645
- data[offset++] = params.neckPose[2] || 0;
15646
- data[offset++] = 0;
15647
- data[offset++] = params.jawPose[0] || 0;
15648
- data[offset++] = params.jawPose[1] || 0;
15649
- data[offset++] = params.jawPose[2] || 0;
15650
- data[offset++] = 0;
15651
- data[offset++] = params.eyesPose[0] || 0;
15652
- data[offset++] = params.eyesPose[1] || 0;
15653
- data[offset++] = params.eyesPose[2] || 0;
15654
- data[offset++] = 0;
15655
- data[offset++] = params.eyesPose[3] || 0;
15656
- data[offset++] = params.eyesPose[4] || 0;
15657
- data[offset++] = params.eyesPose[5] || 0;
15658
- data[offset++] = 0;
15659
- data[offset++] = params.eyelid[0] || 0;
15660
- data[offset++] = params.eyelid[1] || 0;
15661
- data[offset++] = 0;
15662
- data[offset++] = 0;
15663
- this.device.queue.writeBuffer(this.buffers.frameParams, 0, data);
15664
- }
15665
- /**
15666
- * 获取所有缓冲区
15667
- */
15668
- getBuffers() {
15669
- if (!this.buffers) {
15670
- throw new Error("FLAME GPU buffers not initialized");
15671
- }
15672
- return this.buffers;
15673
- }
15674
- /**
15675
- * 获取元数据
15676
- */
15677
- getMetadata() {
15678
- return {
15679
- vertexCount: this.vertexCount,
15680
- faceCount: this.faceCount,
15681
- jointCount: this.jointCount,
15682
- shapeParamCount: this.shapeParamCount,
15683
- poseParamCount: this.poseParamCount,
15684
- staticOffsetCount: this.staticOffsetCount
15685
- };
15686
- }
15687
- /**
15688
- * 清理资源
15689
- */
15690
- destroy() {
15691
- if (this.buffers) {
15692
- Object.values(this.buffers).forEach((buffer) => {
15693
- if (buffer) {
15694
- buffer.destroy();
15695
- }
15696
- });
15697
- this.buffers = null;
15698
- }
15699
- this.device = null;
15700
- logger.log("🗑️ FLAME GPU buffers destroyed");
15701
- }
15702
- }
15703
- const RADIX_SIZE = 256;
15704
- const WORKGROUP_SIZE = 256;
15705
- const ELEMENTS_PER_THREAD = 4;
15706
- const ELEMENTS_PER_WG = WORKGROUP_SIZE * ELEMENTS_PER_THREAD;
15707
- const computeDepthShader = (
15708
- /* wgsl */
15709
- `
15710
- struct Uniforms {
15711
- cameraPosition: vec3<f32>,
15712
- _pad0: f32,
15713
- cameraForward: vec3<f32>,
15714
- _pad1: f32,
15715
- splatCount: u32,
15716
- paddedCount: u32,
15717
- _pad2: u32,
15718
- _pad3: u32,
15719
- }
15720
-
15721
- @group(0) @binding(0) var<uniform> uniforms: Uniforms;
15722
- @group(0) @binding(1) var<storage, read> positions: array<f32>;
15723
- @group(0) @binding(2) var<storage, read_write> keys: array<u32>;
15724
- @group(0) @binding(3) var<storage, read_write> values: array<u32>;
15725
-
15726
- @compute @workgroup_size(${WORKGROUP_SIZE})
15727
- fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
15728
- let idx = globalId.x;
15729
-
15730
- if (idx >= uniforms.paddedCount) {
15731
- return;
15732
- }
15733
-
15734
- if (idx >= uniforms.splatCount) {
15735
- // 填充区域,使用最大 key 值,排序后会在最后
15736
- keys[idx] = 0xFFFFFFFFu;
15737
- values[idx] = idx;
15738
- return;
15739
- }
15740
-
15741
- let posOffset = idx * 3u;
15742
- let pos = vec3<f32>(
15743
- positions[posOffset],
15744
- positions[posOffset + 1u],
15745
- positions[posOffset + 2u]
15746
- );
15747
-
15748
- // 计算深度
15749
- let diff = pos - uniforms.cameraPosition;
15750
- let depth = dot(diff, uniforms.cameraForward);
15751
-
15752
- // 转换为可排序的 key(与 CPU sortSplats.ts 完全相同)
15753
- let d = bitcast<i32>(depth);
15754
- let signMask = d >> 31;
15755
- let negSignMask = -signMask;
15756
- let mask = negSignMask | i32(0x80000000u);
15757
- let sortableKey = u32(d ^ mask);
15758
-
15759
- // 不取反!CPU 是升序排序后翻转结果
15760
- keys[idx] = sortableKey;
15761
- values[idx] = idx;
15762
- }
15763
- `
15764
- );
15765
- const histogramShader = (
15766
- /* wgsl */
15767
- `
15768
- struct Params {
15769
- count: u32,
15770
- shift: u32, // 当前 pass 的 bit shift (0, 8, 16, 24)
15771
- numWorkgroups: u32,
15772
- _pad: u32,
15773
- }
15774
-
15775
- @group(0) @binding(0) var<uniform> params: Params;
15776
- @group(0) @binding(1) var<storage, read> keys: array<u32>;
15777
- @group(0) @binding(2) var<storage, read_write> histograms: array<u32>; // numWorkgroups * 256
15778
-
15779
- var<workgroup> localHist: array<atomic<u32>, ${RADIX_SIZE}>;
15780
-
15781
- @compute @workgroup_size(${WORKGROUP_SIZE})
15782
- fn main(
15783
- @builtin(local_invocation_id) localId: vec3<u32>,
15784
- @builtin(workgroup_id) groupId: vec3<u32>
15785
- ) {
15786
- let tid = localId.x;
15787
- let gid = groupId.x;
15788
-
15789
- // 初始化 local histogram
15790
- atomicStore(&localHist[tid], 0u);
15791
- workgroupBarrier();
15792
-
15793
- // 每个线程处理多个元素
15794
- let startIdx = gid * ${ELEMENTS_PER_WG}u + tid;
15795
- for (var i = 0u; i < ${ELEMENTS_PER_THREAD}u; i++) {
15796
- let idx = startIdx + i * ${WORKGROUP_SIZE}u;
15797
- if (idx < params.count) {
15798
- let key = keys[idx];
15799
- let bucket = (key >> params.shift) & 0xFFu;
15800
- atomicAdd(&localHist[bucket], 1u);
15801
- }
15802
- }
15803
-
15804
- workgroupBarrier();
15805
-
15806
- // 写入 global histogram
15807
- // 布局: histograms[bucket * numWorkgroups + gid] = 该 workgroup 在该 bucket 的数量
15808
- let histOffset = tid * params.numWorkgroups + gid;
15809
- histograms[histOffset] = atomicLoad(&localHist[tid]);
15810
- }
15811
- `
15812
- );
15813
- const scanShader = (
15814
- /* wgsl */
15815
- `
15816
- struct Params {
15817
- count: u32, // 要 scan 的元素数量
15818
- _pad0: u32,
15819
- _pad1: u32,
15820
- _pad2: u32,
15821
- }
15822
-
15823
- @group(0) @binding(0) var<uniform> params: Params;
15824
- @group(0) @binding(1) var<storage, read_write> data: array<u32>;
15825
- @group(0) @binding(2) var<storage, read_write> blockSums: array<u32>;
15826
-
15827
- var<workgroup> temp: array<u32, ${WORKGROUP_SIZE * 2}>;
15828
-
15829
- @compute @workgroup_size(${WORKGROUP_SIZE})
15830
- fn main(
15831
- @builtin(local_invocation_id) localId: vec3<u32>,
15832
- @builtin(workgroup_id) groupId: vec3<u32>
15833
- ) {
15834
- let tid = localId.x;
15835
- let gid = groupId.x;
15836
- let blockSize = ${WORKGROUP_SIZE * 2}u;
15837
- let offset = gid * blockSize;
15838
-
15839
- // 加载数据到 shared memory
15840
- let idx0 = offset + tid;
15841
- let idx1 = offset + tid + ${WORKGROUP_SIZE}u;
15842
- temp[tid] = select(0u, data[idx0], idx0 < params.count);
15843
- temp[tid + ${WORKGROUP_SIZE}u] = select(0u, data[idx1], idx1 < params.count);
15844
-
15845
- // Up-sweep (reduce)
15846
- var stride = 1u;
15847
- for (var d = blockSize >> 1u; d > 0u; d >>= 1u) {
15848
- workgroupBarrier();
15849
- if (tid < d) {
15850
- let ai = stride * (2u * tid + 1u) - 1u;
15851
- let bi = stride * (2u * tid + 2u) - 1u;
15852
- temp[bi] += temp[ai];
15853
- }
15854
- stride <<= 1u;
15855
- }
15856
-
15857
- // 保存 block sum 并清零最后一个元素
15858
- if (tid == 0u) {
15859
- blockSums[gid] = temp[blockSize - 1u];
15860
- temp[blockSize - 1u] = 0u;
15861
- }
15862
-
15863
- // Down-sweep
15864
- for (var d = 1u; d < blockSize; d <<= 1u) {
15865
- stride >>= 1u;
15866
- workgroupBarrier();
15867
- if (tid < d) {
15868
- let ai = stride * (2u * tid + 1u) - 1u;
15869
- let bi = stride * (2u * tid + 2u) - 1u;
15870
- let t = temp[ai];
15871
- temp[ai] = temp[bi];
15872
- temp[bi] += t;
15873
- }
15874
- }
15875
-
15876
- workgroupBarrier();
15877
-
15878
- // 写回
15879
- if (idx0 < params.count) { data[idx0] = temp[tid]; }
15880
- if (idx1 < params.count) { data[idx1] = temp[tid + ${WORKGROUP_SIZE}u]; }
15881
- }
15882
- `
15883
- );
15884
- const addBlockSumsShader = (
15885
- /* wgsl */
15886
- `
15887
- struct Params {
15888
- count: u32,
15889
- _pad0: u32,
15890
- _pad1: u32,
15891
- _pad2: u32,
15892
- }
15893
-
15894
- @group(0) @binding(0) var<uniform> params: Params;
15895
- @group(0) @binding(1) var<storage, read_write> data: array<u32>;
15896
- @group(0) @binding(2) var<storage, read> blockSums: array<u32>;
15897
-
15898
- @compute @workgroup_size(${WORKGROUP_SIZE})
15899
- fn main(
15900
- @builtin(local_invocation_id) localId: vec3<u32>,
15901
- @builtin(workgroup_id) groupId: vec3<u32>
15902
- ) {
15903
- let tid = localId.x;
15904
- let gid = groupId.x;
15905
-
15906
- if (gid == 0u) { return; } // 第一个 block 不需要加
15907
-
15908
- let blockSize = ${WORKGROUP_SIZE * 2}u;
15909
- let offset = gid * blockSize;
15910
- let blockSum = blockSums[gid];
15911
-
15912
- let idx0 = offset + tid;
15913
- let idx1 = offset + tid + ${WORKGROUP_SIZE}u;
15914
-
15915
- if (idx0 < params.count) { data[idx0] += blockSum; }
15916
- if (idx1 < params.count) { data[idx1] += blockSum; }
15917
- }
15918
- `
15919
- );
15920
- const scatterShader = (
15921
- /* wgsl */
15922
- `
15923
- struct Params {
15924
- count: u32,
15925
- shift: u32,
15926
- numWorkgroups: u32,
15927
- _pad: u32,
15928
- }
15929
-
15930
- @group(0) @binding(0) var<uniform> params: Params;
15931
- @group(0) @binding(1) var<storage, read> keysIn: array<u32>;
15932
- @group(0) @binding(2) var<storage, read> valuesIn: array<u32>;
15933
- @group(0) @binding(3) var<storage, read> globalOffsets: array<u32>;
15934
- @group(0) @binding(4) var<storage, read_write> keysOut: array<u32>;
15935
- @group(0) @binding(5) var<storage, read_write> valuesOut: array<u32>;
15936
-
15937
- var<workgroup> localOffsets: array<u32, ${RADIX_SIZE}>;
15938
- var<workgroup> localCounts: array<u32, ${RADIX_SIZE}>;
15939
- var<workgroup> elementOffsets: array<u32, ${ELEMENTS_PER_WG}>;
15940
-
15941
- @compute @workgroup_size(${WORKGROUP_SIZE})
15942
- fn main(
15943
- @builtin(local_invocation_id) localId: vec3<u32>,
15944
- @builtin(workgroup_id) groupId: vec3<u32>
15945
- ) {
15946
- let tid = localId.x;
15947
- let gid = groupId.x;
15948
-
15949
- // 加载该 workgroup 的 global offset
15950
- let globalIdx = tid * params.numWorkgroups + gid;
15951
- localOffsets[tid] = globalOffsets[globalIdx];
15952
- localCounts[tid] = 0u;
15953
-
15954
- workgroupBarrier();
15955
-
15956
- let baseIdx = gid * ${ELEMENTS_PER_WG}u;
15957
- let endIdx = min(baseIdx + ${ELEMENTS_PER_WG}u, params.count);
15958
- let elemCount = endIdx - baseIdx;
15959
-
15960
- // 阶段 1:线程 0 计算所有元素的 local offset(保证稳定性)
15961
- if (tid == 0u) {
15962
- for (var i = 0u; i < elemCount; i++) {
15963
- let idx = baseIdx + i;
15964
- let key = keysIn[idx];
15965
- let bucket = (key >> params.shift) & 0xFFu;
15966
-
15967
- elementOffsets[i] = localCounts[bucket];
15968
- localCounts[bucket] = localCounts[bucket] + 1u;
15969
- }
15970
- }
15971
-
15972
- workgroupBarrier();
15973
-
15974
- // 阶段 2:所有线程并行写入
15975
- for (var i = tid; i < elemCount; i += ${WORKGROUP_SIZE}u) {
15976
- let idx = baseIdx + i;
15977
- let key = keysIn[idx];
15978
- let value = valuesIn[idx];
15979
- let bucket = (key >> params.shift) & 0xFFu;
15980
-
15981
- let dstIdx = localOffsets[bucket] + elementOffsets[i];
15982
- keysOut[dstIdx] = key;
15983
- valuesOut[dstIdx] = value;
15984
- }
15985
- }
15986
- `
15987
- );
15988
- const reverseShader = (
15989
- /* wgsl */
15990
- `
15991
- struct Params {
15992
- count: u32,
15993
- _pad0: u32,
15994
- _pad1: u32,
15995
- _pad2: u32,
15996
- }
15997
-
15998
- @group(0) @binding(0) var<uniform> params: Params;
15999
- @group(0) @binding(1) var<storage, read> valuesIn: array<u32>;
16000
- @group(0) @binding(2) var<storage, read_write> valuesOut: array<u32>;
16001
-
16002
- @compute @workgroup_size(${WORKGROUP_SIZE})
16003
- fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
16004
- let idx = globalId.x;
16005
- if (idx >= params.count) {
16006
- return;
16007
- }
16008
-
16009
- // 翻转:第 i 个元素放到第 (count - 1 - i) 个位置
16010
- valuesOut[idx] = valuesIn[params.count - 1u - idx];
16011
- }
16012
- `
16013
- );
16014
- class GPURadixSort {
16015
- constructor(options) {
16016
- __publicField(this, "device");
16017
- __publicField(this, "maxSplatCount");
16018
- __publicField(this, "paddedCount");
16019
- __publicField(this, "numWorkgroups");
16020
- // Pipelines
16021
- __publicField(this, "depthPipeline", null);
16022
- __publicField(this, "histogramPipeline", null);
16023
- __publicField(this, "scanPipeline", null);
16024
- __publicField(this, "addBlockSumsPipeline", null);
16025
- __publicField(this, "scatterPipeline", null);
16026
- __publicField(this, "reversePipeline", null);
16027
- // Buffers
16028
- __publicField(this, "uniformBuffer", null);
16029
- __publicField(this, "paramsBuffer", null);
16030
- __publicField(this, "scanParamsBuffer", null);
16031
- // Double buffering for keys/values
16032
- __publicField(this, "keysBuffer0", null);
16033
- __publicField(this, "keysBuffer1", null);
16034
- __publicField(this, "valuesBuffer0", null);
16035
- __publicField(this, "valuesBuffer1", null);
16036
- // Histogram and scan buffers
16037
- __publicField(this, "histogramBuffer", null);
16038
- __publicField(this, "blockSumsBuffer", null);
16039
- __publicField(this, "blockSumsBuffer2", null);
16040
- // 用于多级 scan
16041
- // External
16042
- __publicField(this, "positionsBuffer", null);
16043
- this.device = options.device;
16044
- this.maxSplatCount = options.maxSplatCount;
16045
- this.paddedCount = Math.ceil(options.maxSplatCount / ELEMENTS_PER_WG) * ELEMENTS_PER_WG;
16046
- this.numWorkgroups = this.paddedCount / ELEMENTS_PER_WG;
16047
- this.initialize();
16048
- }
16049
- initialize() {
16050
- this.depthPipeline = this.device.createComputePipeline({
16051
- label: "Radix Sort - Depth",
16052
- layout: "auto",
16053
- compute: {
16054
- module: this.device.createShaderModule({ code: computeDepthShader }),
16055
- entryPoint: "main"
16056
- }
16057
- });
16058
- this.histogramPipeline = this.device.createComputePipeline({
16059
- label: "Radix Sort - Histogram",
16060
- layout: "auto",
16061
- compute: {
16062
- module: this.device.createShaderModule({ code: histogramShader }),
16063
- entryPoint: "main"
16064
- }
16065
- });
16066
- this.scanPipeline = this.device.createComputePipeline({
16067
- label: "Radix Sort - Scan",
16068
- layout: "auto",
16069
- compute: {
16070
- module: this.device.createShaderModule({ code: scanShader }),
16071
- entryPoint: "main"
16072
- }
16073
- });
16074
- this.addBlockSumsPipeline = this.device.createComputePipeline({
16075
- label: "Radix Sort - Add Block Sums",
16076
- layout: "auto",
16077
- compute: {
16078
- module: this.device.createShaderModule({ code: addBlockSumsShader }),
16079
- entryPoint: "main"
16080
- }
16081
- });
16082
- this.scatterPipeline = this.device.createComputePipeline({
16083
- label: "Radix Sort - Scatter",
16084
- layout: "auto",
16085
- compute: {
16086
- module: this.device.createShaderModule({ code: scatterShader }),
16087
- entryPoint: "main"
16088
- }
16089
- });
16090
- this.reversePipeline = this.device.createComputePipeline({
16091
- label: "Radix Sort - Reverse",
16092
- layout: "auto",
16093
- compute: {
16094
- module: this.device.createShaderModule({ code: reverseShader }),
16095
- entryPoint: "main"
16096
- }
16097
- });
16098
- const n2 = this.paddedCount;
16099
- this.uniformBuffer = this.device.createBuffer({
16100
- label: "Radix Sort - Uniforms",
16101
- size: 48,
16102
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
16103
- });
16104
- this.paramsBuffer = this.device.createBuffer({
16105
- label: "Radix Sort - Params",
16106
- size: 16,
16107
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
16108
- });
16109
- this.scanParamsBuffer = this.device.createBuffer({
16110
- label: "Radix Sort - Scan Params",
16111
- size: 16,
16112
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
16113
- });
16114
- this.keysBuffer0 = this.device.createBuffer({
16115
- label: "Radix Sort - Keys 0",
16116
- size: n2 * 4,
16117
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
16118
- });
16119
- this.keysBuffer1 = this.device.createBuffer({
16120
- label: "Radix Sort - Keys 1",
16121
- size: n2 * 4,
16122
- usage: GPUBufferUsage.STORAGE
16123
- });
16124
- this.valuesBuffer0 = this.device.createBuffer({
16125
- label: "Radix Sort - Values 0",
16126
- size: n2 * 4,
16127
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
16128
- });
16129
- this.valuesBuffer1 = this.device.createBuffer({
16130
- label: "Radix Sort - Values 1",
16131
- size: n2 * 4,
16132
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
16133
- });
16134
- const histogramSize = RADIX_SIZE * this.numWorkgroups * 4;
16135
- this.histogramBuffer = this.device.createBuffer({
16136
- label: "Radix Sort - Histogram",
16137
- size: histogramSize,
16138
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
16139
- });
16140
- const scanBlockSize = WORKGROUP_SIZE * 2;
16141
- const numScanBlocks = Math.ceil(histogramSize / 4 / scanBlockSize);
16142
- this.blockSumsBuffer = this.device.createBuffer({
16143
- label: "Radix Sort - Block Sums",
16144
- size: Math.max(numScanBlocks * 4, 16),
16145
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
16146
- });
16147
- this.blockSumsBuffer2 = this.device.createBuffer({
16148
- label: "Radix Sort - Block Sums 2",
16149
- size: 16,
16150
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
16151
- });
16152
- logger.log(`✅ [GPURadixSort] Initialized: maxSplatCount=${this.maxSplatCount}, paddedCount=${this.paddedCount}, numWorkgroups=${this.numWorkgroups}`);
16153
- }
16154
- setPositionsBuffer(buffer) {
16155
- this.positionsBuffer = buffer;
16156
- }
16157
- async sortAsync(viewMatrix, splatCount) {
16158
- if (!this.positionsBuffer) {
16159
- throw new Error("Positions buffer not set");
16160
- }
16161
- const cameraPosition = [
16162
- -viewMatrix[12],
16163
- -viewMatrix[13],
16164
- -viewMatrix[14]
16165
- ];
16166
- const cameraForward = [
16167
- -viewMatrix[2],
16168
- -viewMatrix[6],
16169
- -viewMatrix[10]
16170
- ];
16171
- await this.runDepthPass(cameraPosition, cameraForward, splatCount);
16172
- let keysIn = this.keysBuffer0;
16173
- let keysOut = this.keysBuffer1;
16174
- let valuesIn = this.valuesBuffer0;
16175
- let valuesOut = this.valuesBuffer1;
16176
- for (let pass = 0; pass < 4; pass++) {
16177
- const shift = pass * 8;
16178
- await this.runHistogramPass(keysIn, this.paddedCount, shift);
16179
- await this.runPrefixSum();
16180
- await this.runScatterPass(keysIn, valuesIn, keysOut, valuesOut, this.paddedCount, shift);
16181
- const tempK = keysIn;
16182
- keysIn = keysOut;
16183
- keysOut = tempK;
16184
- const tempV = valuesIn;
16185
- valuesIn = valuesOut;
16186
- valuesOut = tempV;
16187
- }
16188
- await this.runReversePass(valuesIn, valuesOut, splatCount);
16189
- return valuesOut;
16190
- }
16191
- async runReversePass(valuesIn, valuesOut, splatCount) {
16192
- const paramsData = new Uint32Array([splatCount, 0, 0, 0]);
16193
- this.device.queue.writeBuffer(this.paramsBuffer, 0, paramsData);
16194
- const bindGroup = this.device.createBindGroup({
16195
- layout: this.reversePipeline.getBindGroupLayout(0),
16196
- entries: [
16197
- { binding: 0, resource: { buffer: this.paramsBuffer } },
16198
- { binding: 1, resource: { buffer: valuesIn } },
16199
- { binding: 2, resource: { buffer: valuesOut } }
16200
- ]
16201
- });
16202
- const encoder = this.device.createCommandEncoder();
16203
- const pass = encoder.beginComputePass();
16204
- pass.setPipeline(this.reversePipeline);
16205
- pass.setBindGroup(0, bindGroup);
16206
- pass.dispatchWorkgroups(Math.ceil(splatCount / WORKGROUP_SIZE));
16207
- pass.end();
16208
- this.device.queue.submit([encoder.finish()]);
16209
- }
16210
- async runDepthPass(cameraPosition, cameraForward, splatCount) {
16211
- const uniformData = new ArrayBuffer(48);
16212
- const floatView = new Float32Array(uniformData);
16213
- const uintView = new Uint32Array(uniformData);
16214
- floatView[0] = cameraPosition[0];
16215
- floatView[1] = cameraPosition[1];
16216
- floatView[2] = cameraPosition[2];
16217
- floatView[3] = 0;
16218
- floatView[4] = cameraForward[0];
16219
- floatView[5] = cameraForward[1];
16220
- floatView[6] = cameraForward[2];
16221
- floatView[7] = 0;
16222
- uintView[8] = splatCount;
16223
- uintView[9] = this.paddedCount;
16224
- this.device.queue.writeBuffer(this.uniformBuffer, 0, uniformData);
16225
- const bindGroup = this.device.createBindGroup({
16226
- layout: this.depthPipeline.getBindGroupLayout(0),
16227
- entries: [
16228
- { binding: 0, resource: { buffer: this.uniformBuffer } },
16229
- { binding: 1, resource: { buffer: this.positionsBuffer } },
16230
- { binding: 2, resource: { buffer: this.keysBuffer0 } },
16231
- { binding: 3, resource: { buffer: this.valuesBuffer0 } }
16232
- ]
16233
- });
16234
- const encoder = this.device.createCommandEncoder();
16235
- const pass = encoder.beginComputePass();
16236
- pass.setPipeline(this.depthPipeline);
16237
- pass.setBindGroup(0, bindGroup);
16238
- pass.dispatchWorkgroups(Math.ceil(this.paddedCount / WORKGROUP_SIZE));
16239
- pass.end();
16240
- this.device.queue.submit([encoder.finish()]);
16241
- }
16242
- async runHistogramPass(keysBuffer, count, shift) {
16243
- const paramsData = new Uint32Array([count, shift, this.numWorkgroups, 0]);
16244
- this.device.queue.writeBuffer(this.paramsBuffer, 0, paramsData);
16245
- const bindGroup = this.device.createBindGroup({
16246
- layout: this.histogramPipeline.getBindGroupLayout(0),
16247
- entries: [
16248
- { binding: 0, resource: { buffer: this.paramsBuffer } },
16249
- { binding: 1, resource: { buffer: keysBuffer } },
16250
- { binding: 2, resource: { buffer: this.histogramBuffer } }
16251
- ]
16252
- });
16253
- const encoder = this.device.createCommandEncoder();
16254
- encoder.clearBuffer(this.histogramBuffer);
16255
- const pass = encoder.beginComputePass();
16256
- pass.setPipeline(this.histogramPipeline);
16257
- pass.setBindGroup(0, bindGroup);
16258
- pass.dispatchWorkgroups(this.numWorkgroups);
16259
- pass.end();
16260
- this.device.queue.submit([encoder.finish()]);
16261
- }
16262
- async runPrefixSum() {
16263
- const histogramCount = RADIX_SIZE * this.numWorkgroups;
16264
- const scanBlockSize = WORKGROUP_SIZE * 2;
16265
- const numBlocks = Math.ceil(histogramCount / scanBlockSize);
16266
- const scanParams = new Uint32Array([histogramCount, 0, 0, 0]);
16267
- this.device.queue.writeBuffer(this.scanParamsBuffer, 0, scanParams);
16268
- const scanBindGroup = this.device.createBindGroup({
16269
- layout: this.scanPipeline.getBindGroupLayout(0),
16270
- entries: [
16271
- { binding: 0, resource: { buffer: this.scanParamsBuffer } },
16272
- { binding: 1, resource: { buffer: this.histogramBuffer } },
16273
- { binding: 2, resource: { buffer: this.blockSumsBuffer } }
16274
- ]
16275
- });
16276
- const encoder = this.device.createCommandEncoder();
16277
- encoder.clearBuffer(this.blockSumsBuffer);
16278
- encoder.clearBuffer(this.blockSumsBuffer2);
16279
- const pass1 = encoder.beginComputePass();
16280
- pass1.setPipeline(this.scanPipeline);
16281
- pass1.setBindGroup(0, scanBindGroup);
16282
- pass1.dispatchWorkgroups(numBlocks);
16283
- pass1.end();
16284
- this.device.queue.submit([encoder.finish()]);
16285
- if (numBlocks > 1) {
16286
- const blockSumsParams = new Uint32Array([numBlocks, 0, 0, 0]);
16287
- this.device.queue.writeBuffer(this.scanParamsBuffer, 0, blockSumsParams);
16288
- const blockSumsScanBindGroup = this.device.createBindGroup({
16289
- layout: this.scanPipeline.getBindGroupLayout(0),
16290
- entries: [
16291
- { binding: 0, resource: { buffer: this.scanParamsBuffer } },
16292
- { binding: 1, resource: { buffer: this.blockSumsBuffer } },
16293
- { binding: 2, resource: { buffer: this.blockSumsBuffer2 } }
16294
- ]
16295
- });
16296
- const encoder2 = this.device.createCommandEncoder();
16297
- const pass2 = encoder2.beginComputePass();
16298
- pass2.setPipeline(this.scanPipeline);
16299
- pass2.setBindGroup(0, blockSumsScanBindGroup);
16300
- pass2.dispatchWorkgroups(1);
16301
- pass2.end();
16302
- this.device.queue.submit([encoder2.finish()]);
16303
- this.device.queue.writeBuffer(this.scanParamsBuffer, 0, scanParams);
16304
- const addBindGroup = this.device.createBindGroup({
16305
- layout: this.addBlockSumsPipeline.getBindGroupLayout(0),
16306
- entries: [
16307
- { binding: 0, resource: { buffer: this.scanParamsBuffer } },
16308
- { binding: 1, resource: { buffer: this.histogramBuffer } },
16309
- { binding: 2, resource: { buffer: this.blockSumsBuffer } }
16310
- ]
16311
- });
16312
- const encoder3 = this.device.createCommandEncoder();
16313
- const pass3 = encoder3.beginComputePass();
16314
- pass3.setPipeline(this.addBlockSumsPipeline);
16315
- pass3.setBindGroup(0, addBindGroup);
16316
- pass3.dispatchWorkgroups(numBlocks);
16317
- pass3.end();
16318
- this.device.queue.submit([encoder3.finish()]);
16319
- }
16320
- }
16321
- async runScatterPass(keysIn, valuesIn, keysOut, valuesOut, count, shift) {
16322
- const paramsData = new Uint32Array([count, shift, this.numWorkgroups, 0]);
16323
- this.device.queue.writeBuffer(this.paramsBuffer, 0, paramsData);
16324
- const bindGroup = this.device.createBindGroup({
16325
- layout: this.scatterPipeline.getBindGroupLayout(0),
16326
- entries: [
16327
- { binding: 0, resource: { buffer: this.paramsBuffer } },
16328
- { binding: 1, resource: { buffer: keysIn } },
16329
- { binding: 2, resource: { buffer: valuesIn } },
16330
- { binding: 3, resource: { buffer: this.histogramBuffer } },
16331
- { binding: 4, resource: { buffer: keysOut } },
16332
- { binding: 5, resource: { buffer: valuesOut } }
16333
- ]
16334
- });
16335
- const encoder = this.device.createCommandEncoder();
16336
- const pass = encoder.beginComputePass();
16337
- pass.setPipeline(this.scatterPipeline);
16338
- pass.setBindGroup(0, bindGroup);
16339
- pass.dispatchWorkgroups(this.numWorkgroups);
16340
- pass.end();
16341
- this.device.queue.submit([encoder.finish()]);
16342
- }
16343
- destroy() {
16344
- var _a, _b, _c, _d, _e2, _f, _g, _h, _i2, _j;
16345
- (_a = this.uniformBuffer) == null ? void 0 : _a.destroy();
16346
- (_b = this.paramsBuffer) == null ? void 0 : _b.destroy();
16347
- (_c = this.scanParamsBuffer) == null ? void 0 : _c.destroy();
16348
- (_d = this.keysBuffer0) == null ? void 0 : _d.destroy();
16349
- (_e2 = this.keysBuffer1) == null ? void 0 : _e2.destroy();
16350
- (_f = this.valuesBuffer0) == null ? void 0 : _f.destroy();
16351
- (_g = this.valuesBuffer1) == null ? void 0 : _g.destroy();
16352
- (_h = this.histogramBuffer) == null ? void 0 : _h.destroy();
16353
- (_i2 = this.blockSumsBuffer) == null ? void 0 : _i2.destroy();
16354
- (_j = this.blockSumsBuffer2) == null ? void 0 : _j.destroy();
16355
- }
16356
- }
16357
14758
  class WebGPURenderer {
16358
14759
  constructor(canvas, backgroundColor, alpha = true) {
16359
14760
  __publicField(this, "canvas");
@@ -16372,19 +14773,6 @@ class WebGPURenderer {
16372
14773
  __publicField(this, "storageBindGroup", null);
16373
14774
  __publicField(this, "bindGroupNeedsUpdate", false);
16374
14775
  // 标记 bind group 是否需要更新
16375
- // 🆕 Transform Pipeline (GPU 3DGS Transform优化)
16376
- __publicField(this, "transformPipeline", null);
16377
- __publicField(this, "useGPUTransform", false);
16378
- // 是否使用GPU Transform路径
16379
- // 🆕 FLAME Pipeline (GPU FLAME Forward Pass优化)
16380
- __publicField(this, "flamePipeline", null);
16381
- __publicField(this, "flameGPUBuffers", null);
16382
- __publicField(this, "useGPUFLAME", false);
16383
- // 是否使用GPU FLAME路径
16384
- // 🆕 GPU Radix Sort (GPU 深度排序优化)
16385
- __publicField(this, "gpuRadixSort", null);
16386
- __publicField(this, "useGPURadixSort", true);
16387
- // 是否使用 GPU 排序
16388
14776
  __publicField(this, "splatCount", 0);
16389
14777
  __publicField(this, "presentationFormat", "bgra8unorm");
16390
14778
  __publicField(this, "alpha");
@@ -16428,8 +14816,6 @@ class WebGPURenderer {
16428
14816
  this.createQuadVertexBuffer();
16429
14817
  await this.createRenderPipeline();
16430
14818
  await this.createBlitPipeline();
16431
- this.transformPipeline = new TransformPipeline(this.device);
16432
- await this.transformPipeline.initialize();
16433
14819
  }
16434
14820
  /**
16435
14821
  * 创建 Uniform Buffer
@@ -16786,336 +15172,60 @@ class WebGPURenderer {
16786
15172
  if (!this.splatDataBuffer || this.splatDataBuffer.size !== packedData.byteLength) {
16787
15173
  if (this.splatDataBuffer) {
16788
15174
  this.splatDataBuffer.destroy();
16789
- }
16790
- this.splatDataBuffer = this.device.createBuffer({
16791
- label: "Splat Data Buffer",
16792
- size: packedData.byteLength,
16793
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
16794
- });
16795
- this.bindGroupNeedsUpdate = true;
16796
- }
16797
- this.device.queue.writeBuffer(
16798
- this.splatDataBuffer,
16799
- 0,
16800
- packedData.buffer,
16801
- packedData.byteOffset,
16802
- packedData.byteLength
16803
- );
16804
- if (sortOrder) {
16805
- const indexBufferSize = sortOrder.byteLength;
16806
- if (!this.sortIndexBuffer || this.sortIndexBuffer.size !== indexBufferSize) {
16807
- if (this.sortIndexBuffer) {
16808
- this.sortIndexBuffer.destroy();
16809
- }
16810
- this.sortIndexBuffer = this.device.createBuffer({
16811
- label: "Sort Index Buffer",
16812
- size: indexBufferSize,
16813
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC
16814
- });
16815
- this.bindGroupNeedsUpdate = true;
16816
- }
16817
- this.device.queue.writeBuffer(
16818
- this.sortIndexBuffer,
16819
- 0,
16820
- sortOrder.buffer,
16821
- sortOrder.byteOffset,
16822
- sortOrder.byteLength
16823
- );
16824
- if (this.bindGroupNeedsUpdate && this.renderPipeline && this.sortIndexBuffer && this.splatDataBuffer) {
16825
- const storageBindGroupLayout = this.renderPipeline.getBindGroupLayout(1);
16826
- this.storageBindGroup = this.device.createBindGroup({
16827
- label: "Storage Bind Group",
16828
- layout: storageBindGroupLayout,
16829
- entries: [
16830
- {
16831
- binding: 0,
16832
- resource: { buffer: this.sortIndexBuffer }
16833
- },
16834
- {
16835
- binding: 1,
16836
- resource: { buffer: this.splatDataBuffer }
16837
- }
16838
- ]
16839
- });
16840
- this.bindGroupNeedsUpdate = false;
16841
- }
16842
- }
16843
- }
16844
- /**
16845
- * 🆕 上传原始Splats数据到GPU (一次性调用,角色加载时)
16846
- * @param originalSplatsData Float32Array, 每个splat 16 floats (64 bytes)
16847
- * @param splatCount splat数量
16848
- */
16849
- loadOriginalSplats(originalSplatsData, splatCount) {
16850
- if (!this.transformPipeline) {
16851
- logger.warn("⚠️ Transform pipeline not initialized, skipping original splats upload");
16852
- return;
16853
- }
16854
- this.transformPipeline.uploadOriginalSplats(originalSplatsData, splatCount);
16855
- this.splatCount = splatCount;
16856
- this.useGPUTransform = true;
16857
- if (this.useGPURadixSort && this.device) {
16858
- try {
16859
- this.gpuRadixSort = new GPURadixSort({
16860
- device: this.device,
16861
- maxSplatCount: splatCount
16862
- });
16863
- logger.log(`✅ [WebGPURenderer] GPU Radix Sort initialized for ${splatCount} splats`);
16864
- } catch (e2) {
16865
- logger.warn("⚠️ [WebGPURenderer] Failed to initialize GPU Radix Sort, falling back to CPU", e2);
16866
- this.useGPURadixSort = false;
16867
- }
16868
- }
16869
- if (!this.sortIndexBuffer || this.sortIndexBuffer.size !== splatCount * 4) {
16870
- if (this.sortIndexBuffer) {
16871
- this.sortIndexBuffer.destroy();
16872
- }
16873
- this.sortIndexBuffer = this.device.createBuffer({
16874
- label: "Sort Index Buffer",
16875
- size: splatCount * 4,
16876
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC
16877
- });
16878
- const defaultSortOrder = new Uint32Array(splatCount);
16879
- for (let i2 = 0; i2 < splatCount; i2++) {
16880
- defaultSortOrder[i2] = i2;
16881
- }
16882
- this.device.queue.writeBuffer(this.sortIndexBuffer, 0, defaultSortOrder);
16883
- this.bindGroupNeedsUpdate = true;
16884
- }
16885
- this.splatDataBuffer = this.transformPipeline.getTransformedOutputBuffer();
16886
- if (this.renderPipeline && this.sortIndexBuffer && this.splatDataBuffer) {
16887
- const storageBindGroupLayout = this.renderPipeline.getBindGroupLayout(1);
16888
- this.storageBindGroup = this.device.createBindGroup({
16889
- label: "Initial Storage Bind Group",
16890
- layout: storageBindGroupLayout,
16891
- entries: [
16892
- {
16893
- binding: 0,
16894
- resource: { buffer: this.sortIndexBuffer }
16895
- },
16896
- {
16897
- binding: 1,
16898
- resource: { buffer: this.splatDataBuffer }
16899
- }
16900
- ]
16901
- });
16902
- this.bindGroupNeedsUpdate = false;
16903
- logger.log("✅ [WebGPURenderer] loadOriginalSplats: Initial storage bind group created", {
16904
- sortIndexBufferSize: this.sortIndexBuffer.size,
16905
- splatDataBufferSize: this.splatDataBuffer.size,
16906
- splatCount
16907
- });
16908
- } else {
16909
- logger.warn("⚠️ [WebGPURenderer] loadOriginalSplats: 无法创建Initial storage bind group", {
16910
- hasRenderPipeline: !!this.renderPipeline,
16911
- hasSortIndexBuffer: !!this.sortIndexBuffer,
16912
- hasSplatDataBuffer: !!this.splatDataBuffer
16913
- });
16914
- }
16915
- logger.log(`✅ [WebGPURenderer] loadOriginalSplats: Original splats uploaded to GPU: ${splatCount} splats`);
16916
- }
16917
- /**
16918
- * 🆕 更新Face Geometry (每帧调用,用于GPU Transform优化)
16919
- * @param faceGeometryData Float32Array, 每个face 8 floats (32 bytes)
16920
- */
16921
- updateFaceGeometry(faceGeometryData) {
16922
- if (!this.transformPipeline) {
16923
- logger.warn("⚠️ Transform pipeline not initialized, skipping face geometry update");
16924
- return;
16925
- }
16926
- this.transformPipeline.updateFaceGeometry(faceGeometryData);
16927
- }
16928
- /**
16929
- * 🆕 加载 FLAME 模板数据到 GPU (一次性调用,角色加载时)
16930
- * @param templateData FLAME 模板数据
16931
- * @param shapeParams Shape 参数 [300]
16932
- * @param activeShapeParams 活跃shape参数(零参数过滤优化,可选)
16933
- */
16934
- loadFLAMETemplateData(templateData, shapeParams, activeShapeParams) {
16935
- if (!this.device) {
16936
- throw new Error("Device not initialized");
16937
- }
16938
- this.flameGPUBuffers = new FLAMEGPUBuffers();
16939
- this.flameGPUBuffers.initialize(this.device, templateData, shapeParams, activeShapeParams);
16940
- const metadata = this.flameGPUBuffers.getMetadata();
16941
- if (metadata.vertexCount === 0 || metadata.faceCount === 0 || metadata.jointCount === 0) {
16942
- throw new Error(`Invalid FLAME metadata: vertexCount=${metadata.vertexCount}, faceCount=${metadata.faceCount}, jointCount=${metadata.jointCount}`);
16943
- }
16944
- logger.log("🔧 FLAME Pipeline metadata:", {
16945
- vertexCount: metadata.vertexCount,
16946
- faceCount: metadata.faceCount,
16947
- jointCount: metadata.jointCount,
16948
- shapeParamCount: metadata.shapeParamCount,
16949
- poseParamCount: metadata.poseParamCount,
16950
- staticOffsetCount: metadata.staticOffsetCount
16951
- });
16952
- this.flamePipeline = new FLAMEPipeline(
16953
- this.device,
16954
- this.flameGPUBuffers.getBuffers(),
16955
- metadata.vertexCount,
16956
- metadata.faceCount,
16957
- metadata.jointCount
16958
- );
16959
- this.useGPUFLAME = true;
16960
- logger.log("✅ FLAME Pipeline initialized and GPU FLAME path enabled");
16961
- }
16962
- /**
16963
- * 🆕 更新 FLAME 帧参数 (每帧调用)
16964
- * @param frameParams FLAME 帧参数
16965
- */
16966
- updateFLAMEFrameParams(frameParams) {
16967
- if (!this.flameGPUBuffers) {
16968
- return;
16969
- }
16970
- this.flameGPUBuffers.updateFrameParams(frameParams);
16971
- }
16972
- /**
16973
- * 🆕 获取是否使用 GPU Transform 路径
16974
- */
16975
- getUseGPUTransform() {
16976
- return this.useGPUTransform;
16977
- }
16978
- /**
16979
- * 🆕 获取是否使用 GPU FLAME 路径
16980
- */
16981
- getUseGPUFLAME() {
16982
- return this.useGPUFLAME;
16983
- }
16984
- /**
16985
- * 🆕 使用Face Geometry渲染 (GPU Transform优化路径)
16986
- * 数据流: Face Geometry → GPU Transform → Render
16987
- *
16988
- * 支持两种模式:
16989
- * 1. CPU FLAME 路径:传入 faceGeometryData(从 CPU 计算)
16990
- * 2. GPU FLAME 路径:传入 frameParams(在 GPU 上计算 FLAME)
16991
- */
16992
- async renderWithFaceGeometry(faceGeometryDataOrFrameParams, viewMatrix, projectionMatrix, screenSize, transform) {
16993
- if (!this.transformPipeline || !this.useGPUTransform) {
16994
- logger.error(`❌ Transform pipeline not ready or GPU Transform not enabled: hasTransformPipeline=${!!this.transformPipeline}, useGPUTransform=${this.useGPUTransform}`);
16995
- return;
16996
- }
16997
- if (!this.device || !this.context || !this.renderPipeline || !this.uniformBindGroup) {
16998
- logger.error(`❌ [WebGPURenderer] Render resources not ready: hasDevice=${!!this.device}, hasContext=${!!this.context}, hasRenderPipeline=${!!this.renderPipeline}, hasUniformBindGroup=${!!this.uniformBindGroup}`);
16999
- return;
17000
- }
17001
- const [width, height] = screenSize;
17002
- const needsTransform = transform && (transform.x !== 0 || transform.y !== 0 || transform.scale !== 1);
17003
- let faceGeometryBuffer = null;
17004
- const isFLAMEFrameParams = !(faceGeometryDataOrFrameParams instanceof Float32Array);
17005
- const computeEncoder = this.device.createCommandEncoder({
17006
- label: "FLAME + Transform Command Encoder"
17007
- });
17008
- if (this.useGPUFLAME && this.flamePipeline && isFLAMEFrameParams) {
17009
- const frameParams = faceGeometryDataOrFrameParams;
17010
- this.updateFLAMEFrameParams(frameParams);
17011
- const flameOutput = this.flamePipeline.compute(computeEncoder);
17012
- faceGeometryBuffer = flameOutput.faceGeometries;
17013
- if (faceGeometryBuffer) {
17014
- this.transformPipeline.setFaceGeometryBufferFromGPU(faceGeometryBuffer, flameOutput.faceCount);
17015
- }
17016
- } else {
17017
- const faceGeometryData = faceGeometryDataOrFrameParams;
17018
- this.transformPipeline.updateFaceGeometry(faceGeometryData);
17019
- }
17020
- this.transformPipeline.updateViewMatrix(viewMatrix);
17021
- this.updateUniforms(viewMatrix, projectionMatrix, screenSize);
17022
- this.transformPipeline.executeInEncoder(computeEncoder);
17023
- const transformedBuffer = this.transformPipeline.getTransformedOutputBuffer();
17024
- if (!transformedBuffer) {
17025
- logger.error("❌ Transformed buffer not available");
17026
- return;
17027
- }
17028
- const bufferChanged = !this.splatDataBuffer || this.splatDataBuffer !== transformedBuffer;
17029
- if (bufferChanged) {
17030
- this.splatDataBuffer = transformedBuffer;
17031
- this.bindGroupNeedsUpdate = true;
17032
- }
17033
- this.device.queue.submit([computeEncoder.finish()]);
17034
- await this.updateSortIndexFromGPU(viewMatrix);
17035
- if (this.bindGroupNeedsUpdate && this.renderPipeline && this.sortIndexBuffer && this.splatDataBuffer && this.device) {
17036
- const storageBindGroupLayout = this.renderPipeline.getBindGroupLayout(1);
17037
- this.storageBindGroup = this.device.createBindGroup({
17038
- label: "Storage Bind Group",
17039
- layout: storageBindGroupLayout,
17040
- entries: [
17041
- {
17042
- binding: 0,
17043
- resource: { buffer: this.sortIndexBuffer }
17044
- },
17045
- {
17046
- binding: 1,
17047
- resource: { buffer: this.splatDataBuffer }
17048
- }
17049
- ]
17050
- });
17051
- this.bindGroupNeedsUpdate = false;
17052
- }
17053
- if (!this.storageBindGroup) {
17054
- logger.error(`❌ Storage bind group not ready: bindGroupNeedsUpdate=${this.bindGroupNeedsUpdate}, hasRenderPipeline=${!!this.renderPipeline}, hasSortIndexBuffer=${!!this.sortIndexBuffer}, hasSplatDataBuffer=${!!this.splatDataBuffer}, hasDevice=${!!this.device}`);
17055
- return;
17056
- }
17057
- const renderEncoder = this.device.createCommandEncoder({
17058
- label: "Render Command Encoder"
17059
- });
17060
- if (needsTransform) {
17061
- if (!this.renderTexture || this.framebufferWidth !== width || this.framebufferHeight !== height) {
17062
- this.createRenderTexture(width, height);
17063
- }
17064
- const renderPass = renderEncoder.beginRenderPass({
17065
- label: "Render to Texture Pass",
17066
- colorAttachments: [
17067
- {
17068
- view: this.renderTextureView,
17069
- clearValue: {
17070
- r: this.backgroundColor[0],
17071
- g: this.backgroundColor[1],
17072
- b: this.backgroundColor[2],
17073
- a: this.backgroundColor[3]
17074
- },
17075
- loadOp: "clear",
17076
- storeOp: "store"
17077
- }
17078
- ],
17079
- depthStencilAttachment: {
17080
- view: this.depthTexture.createView(),
17081
- depthLoadOp: "clear",
17082
- depthStoreOp: "store",
17083
- depthClearValue: 1
17084
- }
15175
+ }
15176
+ this.splatDataBuffer = this.device.createBuffer({
15177
+ label: "Splat Data Buffer",
15178
+ size: packedData.byteLength,
15179
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
17085
15180
  });
17086
- renderPass.setPipeline(this.renderTexturePipeline);
17087
- renderPass.setBindGroup(0, this.uniformBindGroup);
17088
- renderPass.setBindGroup(1, this.storageBindGroup);
17089
- renderPass.setVertexBuffer(0, this.quadVertexBuffer);
17090
- renderPass.draw(4, this.splatCount);
17091
- renderPass.end();
17092
- this.blitToScreen(renderEncoder, transform);
17093
- } else {
17094
- const textureView = this.context.getCurrentTexture().createView();
17095
- const renderPass = renderEncoder.beginRenderPass({
17096
- label: "Render Pass",
17097
- colorAttachments: [
17098
- {
17099
- view: textureView,
17100
- clearValue: {
17101
- r: this.backgroundColor[0],
17102
- g: this.backgroundColor[1],
17103
- b: this.backgroundColor[2],
17104
- a: this.backgroundColor[3]
15181
+ this.bindGroupNeedsUpdate = true;
15182
+ }
15183
+ this.device.queue.writeBuffer(
15184
+ this.splatDataBuffer,
15185
+ 0,
15186
+ packedData.buffer,
15187
+ packedData.byteOffset,
15188
+ packedData.byteLength
15189
+ );
15190
+ if (sortOrder) {
15191
+ const indexBufferSize = sortOrder.byteLength;
15192
+ if (!this.sortIndexBuffer || this.sortIndexBuffer.size !== indexBufferSize) {
15193
+ if (this.sortIndexBuffer) {
15194
+ this.sortIndexBuffer.destroy();
15195
+ }
15196
+ this.sortIndexBuffer = this.device.createBuffer({
15197
+ label: "Sort Index Buffer",
15198
+ size: indexBufferSize,
15199
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
15200
+ });
15201
+ this.bindGroupNeedsUpdate = true;
15202
+ }
15203
+ this.device.queue.writeBuffer(
15204
+ this.sortIndexBuffer,
15205
+ 0,
15206
+ sortOrder.buffer,
15207
+ sortOrder.byteOffset,
15208
+ sortOrder.byteLength
15209
+ );
15210
+ if (this.bindGroupNeedsUpdate && this.renderPipeline && this.sortIndexBuffer && this.splatDataBuffer) {
15211
+ const storageBindGroupLayout = this.renderPipeline.getBindGroupLayout(1);
15212
+ this.storageBindGroup = this.device.createBindGroup({
15213
+ label: "Storage Bind Group",
15214
+ layout: storageBindGroupLayout,
15215
+ entries: [
15216
+ {
15217
+ binding: 0,
15218
+ resource: { buffer: this.sortIndexBuffer }
17105
15219
  },
17106
- loadOp: "clear",
17107
- storeOp: "store"
17108
- }
17109
- ]
17110
- });
17111
- renderPass.setPipeline(this.renderPipeline);
17112
- renderPass.setBindGroup(0, this.uniformBindGroup);
17113
- renderPass.setBindGroup(1, this.storageBindGroup);
17114
- renderPass.setVertexBuffer(0, this.quadVertexBuffer);
17115
- renderPass.draw(4, this.splatCount);
17116
- renderPass.end();
15220
+ {
15221
+ binding: 1,
15222
+ resource: { buffer: this.splatDataBuffer }
15223
+ }
15224
+ ]
15225
+ });
15226
+ this.bindGroupNeedsUpdate = false;
15227
+ }
17117
15228
  }
17118
- this.device.queue.submit([renderEncoder.finish()]);
17119
15229
  }
17120
15230
  /**
17121
15231
  * 渲染一帧
@@ -17128,54 +15238,9 @@ class WebGPURenderer {
17128
15238
  const [width, height] = screenSize;
17129
15239
  const needsTransform = transform && (transform.x !== 0 || transform.y !== 0 || transform.scale !== 1);
17130
15240
  this.updateUniforms(viewMatrix, projectionMatrix, screenSize);
17131
- if (this.useGPUTransform && this.transformPipeline) {
17132
- this.transformPipeline.updateViewMatrix(viewMatrix);
17133
- const commandEncoder2 = this.device.createCommandEncoder({
17134
- label: "Transform + Render Command Encoder"
17135
- });
17136
- this.transformPipeline.executeInEncoder(commandEncoder2);
17137
- const transformedBuffer = this.transformPipeline.getTransformedOutputBuffer();
17138
- if (transformedBuffer) {
17139
- if (this.splatDataBuffer !== transformedBuffer) {
17140
- this.splatDataBuffer = transformedBuffer;
17141
- this.bindGroupNeedsUpdate = true;
17142
- }
17143
- }
17144
- this.renderWithCommandEncoder(commandEncoder2, viewMatrix, projectionMatrix, screenSize, transform, needsTransform || false, width, height);
17145
- this.device.queue.submit([commandEncoder2.finish()]);
17146
- return;
17147
- }
17148
15241
  const commandEncoder = this.device.createCommandEncoder({
17149
15242
  label: "Render Command Encoder"
17150
15243
  });
17151
- this.renderWithCommandEncoder(commandEncoder, viewMatrix, projectionMatrix, screenSize, transform, needsTransform || false, width, height);
17152
- this.device.queue.submit([commandEncoder.finish()]);
17153
- }
17154
- /**
17155
- * 🆕 渲染逻辑(提取为独立方法,供Transform和传统路径共用)
17156
- */
17157
- renderWithCommandEncoder(commandEncoder, _viewMatrix, _projectionMatrix, _screenSize, transform, needsTransform, width, height) {
17158
- if (this.bindGroupNeedsUpdate && this.renderPipeline && this.sortIndexBuffer && this.splatDataBuffer && this.device) {
17159
- const storageBindGroupLayout = this.renderPipeline.getBindGroupLayout(1);
17160
- this.storageBindGroup = this.device.createBindGroup({
17161
- label: "Storage Bind Group",
17162
- layout: storageBindGroupLayout,
17163
- entries: [
17164
- {
17165
- binding: 0,
17166
- resource: { buffer: this.sortIndexBuffer }
17167
- },
17168
- {
17169
- binding: 1,
17170
- resource: { buffer: this.splatDataBuffer }
17171
- }
17172
- ]
17173
- });
17174
- this.bindGroupNeedsUpdate = false;
17175
- }
17176
- if (!this.device || !this.context || !this.renderPipeline || !this.storageBindGroup) {
17177
- return;
17178
- }
17179
15244
  if (needsTransform) {
17180
15245
  if (!this.renderTexture || this.framebufferWidth !== width || this.framebufferHeight !== height) {
17181
15246
  this.createRenderTexture(width, height);
@@ -17234,6 +15299,7 @@ class WebGPURenderer {
17234
15299
  renderPass.draw(4, this.splatCount);
17235
15300
  renderPass.end();
17236
15301
  }
15302
+ this.device.queue.submit([commandEncoder.finish()]);
17237
15303
  }
17238
15304
  /**
17239
15305
  * 将 render texture 绘制到屏幕(应用 transform)
@@ -17323,99 +15389,11 @@ class WebGPURenderer {
17323
15389
  updateBackgroundColor(backgroundColor) {
17324
15390
  this.backgroundColor = backgroundColor;
17325
15391
  }
17326
- /**
17327
- * 🔍 关键修复:从GPU读取transform后的positions,进行深度排序,更新sortIndexBuffer
17328
- * 这解决了第一帧GPU路径渲染异常的问题(未排序导致渲染顺序错误)
17329
- */
17330
- async updateSortIndexFromGPU(viewMatrix) {
17331
- if (!this.device || !this.transformPipeline || !this.sortIndexBuffer) {
17332
- return;
17333
- }
17334
- if (this.useGPURadixSort && this.gpuRadixSort) {
17335
- const positionsBuffer2 = this.transformPipeline.getPositionsOutputBuffer();
17336
- if (!positionsBuffer2) {
17337
- logger.warn("⚠️ [WebGPURenderer] updateSortIndexFromGPU: positionsBuffer not available");
17338
- return;
17339
- }
17340
- this.gpuRadixSort.setPositionsBuffer(positionsBuffer2);
17341
- const sortedIndicesBuffer = await this.gpuRadixSort.sortAsync(viewMatrix, this.splatCount);
17342
- const copyEncoder = this.device.createCommandEncoder({ label: "Copy Sort Result" });
17343
- copyEncoder.copyBufferToBuffer(
17344
- sortedIndicesBuffer,
17345
- 0,
17346
- this.sortIndexBuffer,
17347
- 0,
17348
- this.splatCount * 4
17349
- );
17350
- this.device.queue.submit([copyEncoder.finish()]);
17351
- return;
17352
- }
17353
- performance.now();
17354
- const cameraPosition = [
17355
- -viewMatrix[12],
17356
- -viewMatrix[13],
17357
- -viewMatrix[14]
17358
- ];
17359
- const cameraForward = [
17360
- -viewMatrix[2],
17361
- -viewMatrix[6],
17362
- -viewMatrix[10]
17363
- ];
17364
- const positionsBuffer = this.transformPipeline.getPositionsOutputBuffer();
17365
- if (!positionsBuffer) {
17366
- logger.warn("⚠️ [WebGPURenderer] updateSortIndexFromGPU: positionsBuffer not available");
17367
- return;
17368
- }
17369
- const positionsSize = this.splatCount * 3 * 4;
17370
- const stagingBuffer = this.device.createBuffer({
17371
- size: positionsSize,
17372
- usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
17373
- });
17374
- const readbackStart = performance.now();
17375
- const readbackEncoder = this.device.createCommandEncoder();
17376
- readbackEncoder.copyBufferToBuffer(
17377
- positionsBuffer,
17378
- 0,
17379
- stagingBuffer,
17380
- 0,
17381
- positionsSize
17382
- );
17383
- this.device.queue.submit([readbackEncoder.finish()]);
17384
- await stagingBuffer.mapAsync(GPUMapMode.READ);
17385
- const positionsMapped = stagingBuffer.getMappedRange();
17386
- const positions = new Float32Array(positionsMapped);
17387
- performance.now() - readbackStart;
17388
- const convertStart = performance.now();
17389
- const floatsPerPoint = 13;
17390
- const packedData = new Float32Array(this.splatCount * floatsPerPoint);
17391
- for (let i2 = 0; i2 < this.splatCount; i2++) {
17392
- const offset = i2 * floatsPerPoint;
17393
- const posOffset = i2 * 3;
17394
- packedData[offset] = positions[posOffset];
17395
- packedData[offset + 1] = positions[posOffset + 1];
17396
- packedData[offset + 2] = positions[posOffset + 2];
17397
- }
17398
- performance.now() - convertStart;
17399
- const sortStart = performance.now();
17400
- const sortOrder = sortSplats(packedData, cameraPosition, cameraForward);
17401
- performance.now() - sortStart;
17402
- const writeStart = performance.now();
17403
- this.device.queue.writeBuffer(
17404
- this.sortIndexBuffer,
17405
- 0,
17406
- sortOrder.buffer,
17407
- sortOrder.byteOffset,
17408
- sortOrder.byteLength
17409
- );
17410
- performance.now() - writeStart;
17411
- stagingBuffer.unmap();
17412
- stagingBuffer.destroy();
17413
- }
17414
15392
  /**
17415
15393
  * 清理资源
17416
15394
  */
17417
15395
  dispose() {
17418
- var _a, _b, _c, _d, _e2, _f, _g, _h, _i2, _j, _k, _l, _m;
15396
+ var _a, _b, _c, _d, _e2, _f, _g, _h, _i2;
17419
15397
  (_a = this.sortIndexBuffer) == null ? void 0 : _a.destroy();
17420
15398
  (_b = this.splatDataBuffer) == null ? void 0 : _b.destroy();
17421
15399
  (_c = this.quadVertexBuffer) == null ? void 0 : _c.destroy();
@@ -17424,11 +15402,7 @@ class WebGPURenderer {
17424
15402
  (_f = this.depthTexture) == null ? void 0 : _f.destroy();
17425
15403
  (_g = this.blitUniformBuffer) == null ? void 0 : _g.destroy();
17426
15404
  (_h = this.blitQuadBuffer) == null ? void 0 : _h.destroy();
17427
- (_i2 = this.transformPipeline) == null ? void 0 : _i2.destroy();
17428
- (_j = this.flamePipeline) == null ? void 0 : _j.destroy();
17429
- (_k = this.flameGPUBuffers) == null ? void 0 : _k.destroy();
17430
- (_l = this.gpuRadixSort) == null ? void 0 : _l.destroy();
17431
- (_m = this.device) == null ? void 0 : _m.destroy();
15405
+ (_i2 = this.device) == null ? void 0 : _i2.destroy();
17432
15406
  this.sortIndexBuffer = null;
17433
15407
  this.splatDataBuffer = null;
17434
15408
  this.quadVertexBuffer = null;
@@ -17442,10 +15416,6 @@ class WebGPURenderer {
17442
15416
  this.blitQuadBuffer = null;
17443
15417
  this.blitPipeline = null;
17444
15418
  this.blitSampler = null;
17445
- this.transformPipeline = null;
17446
- this.flamePipeline = null;
17447
- this.flameGPUBuffers = null;
17448
- this.gpuRadixSort = null;
17449
15419
  this.device = null;
17450
15420
  this.context = null;
17451
15421
  this.renderPipeline = null;
@@ -17572,33 +15542,6 @@ class RenderSystem {
17572
15542
  const renderTime = performance.now() - startRender;
17573
15543
  this.renderTime = renderTime;
17574
15544
  }
17575
- /**
17576
- * 🆕 使用Face Geometry渲染 (GPU Transform优化路径)
17577
- * 数据流: Face Geometry → GPU Transform → Render
17578
- */
17579
- async renderFrameWithFaceGeometry(faceGeometryData, viewMatrix, projectionMatrix, screenSize, transform, cpuFaceGeometriesForComparison, avatarCore) {
17580
- if (!this.renderer || this.backend !== "webgpu") {
17581
- logger.warn("⚠️ renderFrameWithFaceGeometry only works with WebGPU, falling back to standard render");
17582
- return;
17583
- }
17584
- this.updateCameraMatrices();
17585
- const webgpuRenderer = this.renderer;
17586
- if (typeof webgpuRenderer.renderWithFaceGeometry === "function") {
17587
- await webgpuRenderer.renderWithFaceGeometry(
17588
- faceGeometryData,
17589
- viewMatrix ?? this.viewMatrix,
17590
- projectionMatrix ?? this.projectionMatrix,
17591
- screenSize ?? [this.canvas.width, this.canvas.height],
17592
- transform ?? (this.offsetX !== 0 || this.offsetY !== 0 || this.scale !== 1 ? { x: this.offsetX, y: this.offsetY, scale: this.scale } : void 0),
17593
- cpuFaceGeometriesForComparison,
17594
- avatarCore
17595
- );
17596
- } else {
17597
- logger.error("❌ WebGPU renderer does not support renderWithFaceGeometry");
17598
- return;
17599
- }
17600
- this.renderTime = performance.now();
17601
- }
17602
15545
  /**
17603
15546
  * Set transform for render texture blit
17604
15547
  * @param x - Horizontal offset in normalized coordinates (-1 to 1, where -1 = left edge, 0 = center, 1 = right edge)
@@ -17833,7 +15776,7 @@ function linearLerp(from, to2, progress) {
17833
15776
  expression: lerpArrays(from.expression || [], to2.expression || [], progress)
17834
15777
  };
17835
15778
  }
17836
- function generateTransitionFramesLinear(from, to2, durationMs, fps = 25) {
15779
+ function generateTransitionFramesLinear(from, to2, durationMs, fps = FLAME_FRAME_RATE) {
17837
15780
  const steps = Math.max(1, Math.floor(durationMs / 1e3 * fps));
17838
15781
  const frames = Array.from({ length: steps });
17839
15782
  if (steps === 1) {
@@ -17876,27 +15819,11 @@ function createBezierEasing(x1, y1, x2, y2) {
17876
15819
  };
17877
15820
  }
17878
15821
  const BEZIER_CURVES = {
17879
- // jaw: fast start, smooth stop
17880
- jaw: createBezierEasing(0.2, 0.8, 0.3, 1),
17881
- // expression: smooth S curve
17882
- expression: createBezierEasing(0.4, 0, 0.2, 1),
17883
- // eye: softer S curve
17884
- eye: createBezierEasing(0.3, 0, 0.1, 1),
17885
- // neck: slow start, inertial stop
17886
- neck: createBezierEasing(0.1, 0.2, 0.2, 1),
17887
- // global: standard ease-in-out
17888
- global: createBezierEasing(0.42, 0, 0.58, 1)
17889
- };
17890
- const TIME_SCALE = {
17891
- jaw: 2.5,
17892
- // 40% time to complete
17893
- expression: 1.6,
17894
- // 62.5% time to complete
17895
- eye: 1.3,
17896
- // 77% time to complete
17897
- neck: 1,
17898
- // 100% time to complete
17899
- global: 1
15822
+ jaw: createBezierEasing(...BEZIER_CURVES$1.jaw),
15823
+ expression: createBezierEasing(...BEZIER_CURVES$1.expression),
15824
+ eye: createBezierEasing(...BEZIER_CURVES$1.eye),
15825
+ neck: createBezierEasing(...BEZIER_CURVES$1.neck),
15826
+ global: createBezierEasing(...BEZIER_CURVES$1.global)
17900
15827
  };
17901
15828
  function bezierLerp(from, to2, progress) {
17902
15829
  const getT = (key) => {
@@ -17919,7 +15846,7 @@ function bezierLerp(from, to2, progress) {
17919
15846
  expression: lerpArrays(from.expression || [], to2.expression || [], getT("expression"))
17920
15847
  };
17921
15848
  }
17922
- function generateTransitionFrames(from, to2, durationMs, fps = 25) {
15849
+ function generateTransitionFrames(from, to2, durationMs, fps = FLAME_FRAME_RATE) {
17923
15850
  const steps = Math.max(1, Math.floor(durationMs / 1e3 * fps));
17924
15851
  const frames = Array.from({ length: steps });
17925
15852
  if (steps === 1) {
@@ -17969,9 +15896,9 @@ class AvatarView {
17969
15896
  // Transition animation data
17970
15897
  __publicField(this, "transitionKeyframes", []);
17971
15898
  __publicField(this, "transitionStartTime", 0);
17972
- __publicField(this, "startTransitionDurationMs", 200);
15899
+ __publicField(this, "startTransitionDurationMs", START_TRANSITION_DURATION_MS);
17973
15900
  // Idle -> Speaking 过渡时长
17974
- __publicField(this, "endTransitionDurationMs", 1600);
15901
+ __publicField(this, "endTransitionDurationMs", END_TRANSITION_DURATION_MS);
17975
15902
  // Speaking -> Idle 过渡时长
17976
15903
  __publicField(this, "cachedIdleFirstFrame", null);
17977
15904
  __publicField(this, "idleCurrentFrameIndex", 0);
@@ -17983,6 +15910,8 @@ class AvatarView {
17983
15910
  // Unique ID for this character instance
17984
15911
  // 纯渲染模式标志(阻止 idle 循环渲染)
17985
15912
  __publicField(this, "isPureRenderingMode", false);
15913
+ // 渲染开关标志(控制是否进行渲染循环)
15914
+ __publicField(this, "_renderingEnabled", true);
17986
15915
  // avatar_active 埋点相关
17987
15916
  __publicField(this, "avatarActiveTimer", null);
17988
15917
  __publicField(this, "AVATAR_ACTIVE_INTERVAL", 6e5);
@@ -18054,12 +15983,12 @@ class AvatarView {
18054
15983
  aligned.from,
18055
15984
  aligned.to,
18056
15985
  durationMs,
18057
- APP_CONFIG.animation.fps
15986
+ FLAME_FRAME_RATE
18058
15987
  ) : generateTransitionFrames(
18059
15988
  aligned.from,
18060
15989
  aligned.to,
18061
15990
  durationMs,
18062
- APP_CONFIG.animation.fps
15991
+ FLAME_FRAME_RATE
18063
15992
  );
18064
15993
  if (keyframes.length < 2) {
18065
15994
  keyframes = [aligned.from, aligned.to];
@@ -18198,7 +16127,6 @@ class AvatarView {
18198
16127
  logger.log("[AvatarView] Initializing render system...");
18199
16128
  const cameraConfig = this.resolveCameraConfig(resources);
18200
16129
  await this.initializeRenderSystem(cameraConfig);
18201
- await this.initializeGPUPath(avatarCore);
18202
16130
  if (APP_CONFIG.debug)
18203
16131
  logger.log("[AvatarView] Starting rendering...");
18204
16132
  await this.renderFirstFrame();
@@ -18234,78 +16162,6 @@ class AvatarView {
18234
16162
  if (APP_CONFIG.debug)
18235
16163
  logger.log("[AvatarView] Render system initialized successfully");
18236
16164
  }
18237
- /**
18238
- * 🆕 初始化 GPU 路径 (Transform + FLAME Pipeline)
18239
- * @internal
18240
- */
18241
- async initializeGPUPath(avatarCore) {
18242
- try {
18243
- logger.log("[AvatarView] 🚀 Initializing GPU Transform optimization...");
18244
- const renderer = this.renderSystem.renderer;
18245
- logger.log("[AvatarView] GPU Init - hasRenderer:", !!renderer, "hasLoadOriginalSplats:", typeof (renderer == null ? void 0 : renderer.loadOriginalSplats) === "function");
18246
- const originalSplatsResult = await avatarCore.getOriginalSplatsData();
18247
- logger.log("[AvatarView] GPU Init - originalSplatsResult:", !!originalSplatsResult, "hasData:", !!(originalSplatsResult == null ? void 0 : originalSplatsResult.data), "count:", originalSplatsResult == null ? void 0 : originalSplatsResult.count);
18248
- if (originalSplatsResult && originalSplatsResult.data) {
18249
- if (renderer && typeof renderer.loadOriginalSplats === "function") {
18250
- renderer.loadOriginalSplats(
18251
- originalSplatsResult.data,
18252
- originalSplatsResult.count
18253
- );
18254
- logger.log(`[AvatarView] ✅ Original splats uploaded to GPU: ${originalSplatsResult.count} splats`);
18255
- if (typeof renderer.getUseGPUTransform === "function") {
18256
- logger.log("[AvatarView] GPU Init - useGPUTransform after upload:", renderer.getUseGPUTransform());
18257
- }
18258
- } else {
18259
- logger.warn("[AvatarView] ⚠️ GPU Init - renderer.loadOriginalSplats not available!");
18260
- }
18261
- } else {
18262
- logger.warn("[AvatarView] ⚠️ GPU Init - No original splats data from WASM!");
18263
- }
18264
- try {
18265
- const templateData = await avatarCore.getFLAMETemplateData(this.characterId);
18266
- if (templateData && renderer && typeof renderer.loadFLAMETemplateData === "function") {
18267
- const shapeParamsResult = await avatarCore.getCharacterShapeParams(this.characterId);
18268
- if (shapeParamsResult && shapeParamsResult.params) {
18269
- const shapeParams = new Float32Array(shapeParamsResult.params);
18270
- const activeShapeIndices = [];
18271
- const activeShapeValues = [];
18272
- const EPSILON = 1e-6;
18273
- for (let i2 = 0; i2 < shapeParams.length && i2 < 300; i2++) {
18274
- if (Math.abs(shapeParams[i2]) > EPSILON) {
18275
- activeShapeIndices.push(i2);
18276
- activeShapeValues.push(shapeParams[i2]);
18277
- }
18278
- }
18279
- renderer.loadFLAMETemplateData(templateData, shapeParams, {
18280
- activeIndices: new Uint32Array(activeShapeIndices),
18281
- activeValues: new Float32Array(activeShapeValues),
18282
- count: activeShapeIndices.length
18283
- });
18284
- if (APP_CONFIG.debug)
18285
- logger.log(`[AvatarView] ✅ FLAME template data uploaded to GPU: ${templateData.vertexCount} vertices`);
18286
- }
18287
- }
18288
- } catch (flameError) {
18289
- logger.warn("[AvatarView] Failed to load FLAME template data:", flameError instanceof Error ? flameError.message : String(flameError));
18290
- }
18291
- const useGPUPath = typeof (renderer == null ? void 0 : renderer.getUseGPUTransform) === "function" && renderer.getUseGPUTransform() && typeof (renderer == null ? void 0 : renderer.getUseGPUFLAME) === "function" && renderer.getUseGPUFLAME();
18292
- if (useGPUPath) {
18293
- this.avatarController.setRenderCallback(
18294
- (splatData, frameIndex) => {
18295
- this.renderRealtimeFrame(splatData, frameIndex);
18296
- },
18297
- this.characterHandle,
18298
- true
18299
- // useGPUPath = true
18300
- );
18301
- logger.log("[AvatarView] ✅ GPU path enabled for AvatarController (skipping splatData computation)");
18302
- }
18303
- if (APP_CONFIG.debug)
18304
- logger.log("[AvatarView] ✅ GPU Transform optimization initialized");
18305
- } catch (error) {
18306
- logger.warn("[AvatarView] Failed to initialize GPU path, falling back to CPU:", error instanceof Error ? error.message : String(error));
18307
- }
18308
- }
18309
16165
  /**
18310
16166
  * Get default camera configuration
18311
16167
  * @internal
@@ -18403,76 +16259,27 @@ class AvatarView {
18403
16259
  if (!avatarCore) {
18404
16260
  throw new Error("AvatarCore not available");
18405
16261
  }
18406
- const backend = this.renderSystem.getBackend();
18407
- const renderer = this.renderSystem.renderer;
18408
- const hasGetUseGPUTransform = typeof (renderer == null ? void 0 : renderer.getUseGPUTransform) === "function";
18409
- const gpuTransformFlag = hasGetUseGPUTransform ? renderer.getUseGPUTransform() : false;
18410
- const useGPUTransform = backend === "webgpu" && renderer && hasGetUseGPUTransform && gpuTransformFlag;
18411
- logger.log("[AvatarView] renderFirstFrame - GPU path check:", {
18412
- backend,
18413
- hasRenderer: !!renderer,
18414
- hasGetUseGPUTransform,
18415
- gpuTransformFlag,
18416
- useGPUTransform
18417
- });
18418
- const useGPUFLAME = typeof (renderer == null ? void 0 : renderer.getUseGPUFLAME) === "function" && renderer.getUseGPUFLAME();
18419
- if (useGPUTransform && useGPUFLAME) {
18420
- logger.log("[AvatarView] 🚀🔥 Using FULL GPU path for first frame (GPU FLAME + GPU Transform)!");
18421
- try {
18422
- const neutralFrameParams = {
18423
- exprParams: new Float32Array(100),
18424
- rotation: new Float32Array([0, 0, 0]),
18425
- translation: new Float32Array([0, 0, 0]),
18426
- neckPose: new Float32Array([0, 0, 0]),
18427
- jawPose: new Float32Array([0, 0, 0]),
18428
- eyesPose: new Float32Array([0, 0, 0, 0, 0, 0]),
18429
- eyelid: new Float32Array([0, 0])
18430
- };
18431
- await this.renderSystem.renderFrameWithFaceGeometry(neutralFrameParams);
18432
- logger.log("[AvatarView] ✅ First frame rendered successfully (FULL GPU path)");
18433
- } catch (gpuFlameError) {
18434
- logger.error("[AvatarView] ❌ GPU FLAME path failed, falling back to CPU FLAME");
18435
- const faceGeometryData = await avatarCore.computeFrameAsFaceGeometry({ frameIndex: 0, characterId: this.characterId });
18436
- if (faceGeometryData) {
18437
- await this.renderSystem.renderFrameWithFaceGeometry(faceGeometryData);
18438
- logger.log("[AvatarView] ✅ First frame rendered successfully (fallback to CPU FLAME)");
18439
- } else {
18440
- throw new Error("Failed to compute first frame face geometry data");
18441
- }
18442
- }
18443
- } else if (useGPUTransform) {
18444
- logger.log("[AvatarView] 🚀 Using GPU Transform path for first frame (CPU FLAME)!");
18445
- const faceGeometryData = await avatarCore.computeFrameAsFaceGeometry({ frameIndex: 0, characterId: this.characterId });
18446
- if (faceGeometryData) {
18447
- await this.renderSystem.renderFrameWithFaceGeometry(faceGeometryData);
18448
- logger.log("[AvatarView] ✅ First frame rendered successfully (GPU Transform path)");
18449
- } else {
18450
- throw new Error("Failed to compute first frame face geometry data");
18451
- }
16262
+ const neutralParams = {
16263
+ shape_params: Array.from({ length: 100 }, () => 0),
16264
+ expr_params: Array.from({ length: 50 }, () => 0),
16265
+ rotation: [0, 0, 0],
16266
+ translation: [0, 0, 0],
16267
+ neck_pose: [0, 0, 0],
16268
+ jaw_pose: [0, 0, 0],
16269
+ eyes_pose: [0, 0, 0, 0, 0, 0]
16270
+ };
16271
+ const splatData = await avatarCore.computeFrameFlatFromParams(neutralParams, this.characterHandle ?? void 0);
16272
+ if (splatData) {
16273
+ this.renderSystem.loadSplatsFromPackedData(splatData);
16274
+ this.renderSystem.renderFrame();
16275
+ if (APP_CONFIG.debug)
16276
+ logger.log("[AvatarView] First frame rendered successfully");
16277
+ (_a = this.onFirstRendering) == null ? void 0 : _a.call(this);
16278
+ this.reportAvatarActive();
16279
+ this.startAvatarActiveHeartbeat();
18452
16280
  } else {
18453
- logger.log("[AvatarView] 📊 Using CPU path for first frame (GPU not ready)");
18454
- const neutralParams = {
18455
- shape_params: Array.from({ length: 100 }, () => 0),
18456
- expr_params: Array.from({ length: 50 }, () => 0),
18457
- rotation: [0, 0, 0],
18458
- translation: [0, 0, 0],
18459
- neck_pose: [0, 0, 0],
18460
- jaw_pose: [0, 0, 0],
18461
- eyes_pose: [0, 0, 0, 0, 0, 0]
18462
- };
18463
- const splatData = await avatarCore.computeFrameFlatFromParams(neutralParams, this.characterHandle ?? void 0);
18464
- if (splatData) {
18465
- this.renderSystem.loadSplatsFromPackedData(splatData);
18466
- this.renderSystem.renderFrame();
18467
- if (APP_CONFIG.debug)
18468
- logger.log("[AvatarView] First frame rendered successfully (CPU path)");
18469
- } else {
18470
- throw new Error("Failed to compute first frame splat data");
18471
- }
16281
+ throw new Error("Failed to compute first frame splat data");
18472
16282
  }
18473
- (_a = this.onFirstRendering) == null ? void 0 : _a.call(this);
18474
- this.reportAvatarActive();
18475
- this.startAvatarActiveHeartbeat();
18476
16283
  }
18477
16284
  /**
18478
16285
  * Update FPS statistics (called in requestAnimationFrame callback)
@@ -18510,7 +16317,7 @@ class AvatarView {
18510
16317
  }
18511
16318
  this.idleCurrentFrameIndex = 0;
18512
16319
  let lastTime = 0;
18513
- const targetFPS = APP_CONFIG.animation.fps;
16320
+ const targetFPS = FLAME_FRAME_RATE;
18514
16321
  const frameInterval = 1e3 / targetFPS;
18515
16322
  this.initFPS();
18516
16323
  const renderFrame = async (currentTime) => {
@@ -18532,50 +16339,24 @@ class AvatarView {
18532
16339
  this.idleAnimationLoopId = requestAnimationFrame(renderFrame);
18533
16340
  return;
18534
16341
  }
16342
+ if (!this._renderingEnabled) {
16343
+ this.idleAnimationLoopId = requestAnimationFrame(renderFrame);
16344
+ return;
16345
+ }
18535
16346
  const avatarCore = AvatarSDK.getAvatarCore();
18536
16347
  if (!avatarCore) {
18537
16348
  return;
18538
16349
  }
18539
- const backend = this.renderSystem.getBackend();
18540
- const renderer = this.renderSystem.renderer;
18541
- const useGPUTransform = backend === "webgpu" && renderer && typeof renderer.getUseGPUTransform === "function" && renderer.getUseGPUTransform();
18542
- const useGPUFLAME = typeof (renderer == null ? void 0 : renderer.getUseGPUFLAME) === "function" && renderer.getUseGPUFLAME();
18543
- if (useGPUTransform && useGPUFLAME) {
18544
- const flameParams = await avatarCore.getCurrentFrameParams(this.idleCurrentFrameIndex, this.characterId);
18545
- this.idleCurrentFrameIndex++;
16350
+ const splatData = await avatarCore.computeCompleteFrameFlat({ frameIndex: this.idleCurrentFrameIndex }, this.characterHandle ?? void 0);
16351
+ this.idleCurrentFrameIndex++;
16352
+ if (splatData) {
18546
16353
  if (this.renderingState !== "idle") {
18547
16354
  return;
18548
16355
  }
18549
16356
  if (this.isPureRenderingMode) {
18550
16357
  return;
18551
16358
  }
18552
- const frameParams = this.convertFlameParamsToGPUFormat(flameParams);
18553
- await this.renderSystem.renderFrameWithFaceGeometry(frameParams);
18554
- } else if (useGPUTransform) {
18555
- const faceGeometryData = await avatarCore.computeFrameAsFaceGeometry({ frameIndex: this.idleCurrentFrameIndex, characterId: this.characterId });
18556
- this.idleCurrentFrameIndex++;
18557
- if (faceGeometryData) {
18558
- if (this.renderingState !== "idle") {
18559
- return;
18560
- }
18561
- if (this.isPureRenderingMode) {
18562
- return;
18563
- }
18564
- await this.renderSystem.renderFrameWithFaceGeometry(faceGeometryData);
18565
- }
18566
- } else {
18567
- const splatData = await avatarCore.computeCompleteFrameFlat({ frameIndex: this.idleCurrentFrameIndex }, this.characterHandle ?? void 0);
18568
- this.idleCurrentFrameIndex++;
18569
- if (splatData) {
18570
- if (this.renderingState !== "idle") {
18571
- return;
18572
- }
18573
- if (this.isPureRenderingMode) {
18574
- return;
18575
- }
18576
- this.renderSystem.loadSplatsFromPackedData(splatData);
18577
- this.renderSystem.renderFrame();
18578
- }
16359
+ this.doRender(splatData);
18579
16360
  }
18580
16361
  this.idleAnimationLoopId = requestAnimationFrame(renderFrame);
18581
16362
  } catch (error) {
@@ -18596,7 +16377,7 @@ class AvatarView {
18596
16377
  this.stopRealtimeAnimationLoop();
18597
16378
  }
18598
16379
  let lastTime = 0;
18599
- const targetFPS = APP_CONFIG.animation.fps;
16380
+ const targetFPS = FLAME_FRAME_RATE;
18600
16381
  const frameInterval = 1e3 / targetFPS;
18601
16382
  this.initFPS();
18602
16383
  const renderFrame = async (currentTime) => {
@@ -18642,18 +16423,9 @@ class AvatarView {
18642
16423
  const wasmParams = convertProtoFlameToWasmParams(currentFrame);
18643
16424
  const avatarCore = AvatarSDK.getAvatarCore();
18644
16425
  if (avatarCore) {
18645
- const backend = this.renderSystem.getBackend();
18646
- const renderer = this.renderSystem.renderer;
18647
- const useGPUTransform = backend === "webgpu" && renderer && typeof renderer.getUseGPUTransform === "function" && renderer.getUseGPUTransform();
18648
- if (useGPUTransform) {
18649
- const frameParams = this.convertFlameParamsToGPUFormat(wasmParams);
18650
- await this.renderSystem.renderFrameWithFaceGeometry(frameParams);
18651
- } else {
18652
- const sd = await avatarCore.computeFrameFlatFromParams(wasmParams, this.characterHandle ?? void 0);
18653
- if (sd) {
18654
- this.renderSystem.loadSplatsFromPackedData(sd);
18655
- this.renderSystem.renderFrame();
18656
- }
16426
+ const sd = await avatarCore.computeFrameFlatFromParams(wasmParams, this.characterHandle ?? void 0);
16427
+ if (sd) {
16428
+ this.doRender(sd);
18657
16429
  }
18658
16430
  }
18659
16431
  if (progress >= 1) {
@@ -18731,51 +16503,32 @@ class AvatarView {
18731
16503
  this.stopIdleAnimationLoop();
18732
16504
  this.stopRealtimeAnimationLoop();
18733
16505
  }
16506
+ /**
16507
+ * Unified render method - all rendering goes through here
16508
+ * This is the single point of control for renderingEnabled flag
16509
+ * @internal
16510
+ */
16511
+ doRender(splatData) {
16512
+ if (!this.renderSystem || !this._renderingEnabled) {
16513
+ return;
16514
+ }
16515
+ this.renderSystem.loadSplatsFromPackedData(splatData);
16516
+ this.renderSystem.renderFrame();
16517
+ }
18734
16518
  /**
18735
16519
  * Render realtime frame (called by playback layer callback)
18736
16520
  * @internal
18737
16521
  */
18738
- async renderRealtimeFrame(splatData, frameIndex) {
16522
+ renderRealtimeFrame(splatData, frameIndex) {
18739
16523
  if (!this.renderSystem || this.renderingState !== "speaking") {
18740
16524
  return;
18741
16525
  }
18742
- const backend = this.renderSystem.getBackend();
18743
- const renderer = this.renderSystem.renderer;
18744
- const useGPUTransform = backend === "webgpu" && renderer && typeof renderer.getUseGPUTransform === "function" && renderer.getUseGPUTransform();
18745
- if (useGPUTransform) {
18746
- if (frameIndex >= 0 && frameIndex < this.currentKeyframes.length) {
18747
- const flame = this.currentKeyframes[frameIndex];
18748
- const wasmParams = convertProtoFlameToWasmParams(flame);
18749
- const frameParams = this.convertFlameParamsToGPUFormat(wasmParams);
18750
- await this.renderSystem.renderFrameWithFaceGeometry(frameParams);
18751
- this.lastRealtimeProtoFrame = flame;
18752
- }
18753
- } else {
18754
- this.renderSystem.loadSplatsFromPackedData(splatData);
18755
- this.renderSystem.renderFrame();
18756
- if (frameIndex >= 0 && frameIndex < this.currentKeyframes.length) {
18757
- this.lastRealtimeProtoFrame = this.currentKeyframes[frameIndex];
18758
- }
18759
- }
18760
16526
  this.lastRenderedFrameIndex = frameIndex;
18761
16527
  if (frameIndex >= 0 && frameIndex < this.currentKeyframes.length) {
18762
- this.currentPlayingFrame = this.currentKeyframes[frameIndex];
16528
+ this.lastRealtimeProtoFrame = this.currentKeyframes[frameIndex];
16529
+ this.currentPlayingFrame = this.lastRealtimeProtoFrame;
18763
16530
  }
18764
- }
18765
- /**
18766
- * 🆕 将 FlameParams 转换为 FLAMEFrameParams (GPU 格式)
18767
- * @internal
18768
- */
18769
- convertFlameParamsToGPUFormat(params) {
18770
- return {
18771
- exprParams: new Float32Array(params.expr_params || Array(100).fill(0)),
18772
- rotation: new Float32Array(params.rotation || [0, 0, 0]),
18773
- translation: new Float32Array(params.translation || [0, 0, 0]),
18774
- neckPose: new Float32Array(params.neck_pose || [0, 0, 0]),
18775
- jawPose: new Float32Array(params.jaw_pose || [0, 0, 0]),
18776
- eyesPose: new Float32Array(params.eyes_pose || [0, 0, 0, 0, 0, 0]),
18777
- eyelid: new Float32Array(params.eyelid || [0, 0])
18778
- };
16531
+ this.doRender(splatData);
18779
16532
  }
18780
16533
  /**
18781
16534
  * State transition method
@@ -19137,21 +16890,12 @@ class AvatarView {
19137
16890
  if (!avatarCore) {
19138
16891
  throw new Error("AvatarCore not available");
19139
16892
  }
19140
- const backend = this.renderSystem.getBackend();
19141
- const renderer = this.renderSystem.renderer;
19142
- const useGPUTransform = backend === "webgpu" && renderer && typeof renderer.getUseGPUTransform === "function" && renderer.getUseGPUTransform();
19143
- if (useGPUTransform) {
19144
- const frameParams = this.convertFlameParamsToGPUFormat(wasmParams);
19145
- await this.renderSystem.renderFrameWithFaceGeometry(frameParams);
19146
- } else {
19147
- const splatData = await avatarCore.computeFrameFlatFromParams(
19148
- wasmParams,
19149
- this.characterHandle ?? void 0
19150
- );
19151
- if (splatData) {
19152
- this.renderSystem.loadSplatsFromPackedData(splatData);
19153
- this.renderSystem.renderFrame();
19154
- }
16893
+ const splatData = await avatarCore.computeFrameFlatFromParams(
16894
+ wasmParams,
16895
+ this.characterHandle ?? void 0
16896
+ );
16897
+ if (splatData) {
16898
+ this.doRender(splatData);
19155
16899
  }
19156
16900
  } catch (error) {
19157
16901
  logger.error("[AvatarView] Failed to render flame:", error instanceof Error ? error.message : String(error));
@@ -19188,7 +16932,7 @@ class AvatarView {
19188
16932
  const aligned = this.alignFlamePair(fromFrame, toFrame);
19189
16933
  const alignedFrom = aligned.from;
19190
16934
  const alignedTo = aligned.to;
19191
- const fps = APP_CONFIG.animation.fps;
16935
+ const fps = FLAME_FRAME_RATE;
19192
16936
  const durationMs = frameCount / fps * 1e3;
19193
16937
  const transitionFrames = useLinear ? generateTransitionFramesLinear(alignedFrom, alignedTo, durationMs, fps) : generateTransitionFrames(alignedFrom, alignedTo, durationMs, fps);
19194
16938
  transitionFrames[0] = alignedFrom;
@@ -19220,6 +16964,76 @@ class AvatarView {
19220
16964
  this.renderSystem.handleResize();
19221
16965
  }
19222
16966
  }
16967
+ /**
16968
+ * Pause rendering loop
16969
+ *
16970
+ * When called:
16971
+ * - Rendering loop stops (no GPU/canvas updates)
16972
+ * - Audio playback continues normally
16973
+ * - Animation state machine continues running
16974
+ *
16975
+ * Use `resumeRendering()` to resume rendering.
16976
+ *
16977
+ * @example
16978
+ * // Stop rendering to save GPU resources (audio continues)
16979
+ * avatarView.pauseRendering()
16980
+ */
16981
+ pauseRendering() {
16982
+ if (!this._renderingEnabled) {
16983
+ return;
16984
+ }
16985
+ this._renderingEnabled = false;
16986
+ logger.log("[AvatarView] Rendering paused");
16987
+ }
16988
+ /**
16989
+ * Resume rendering loop
16990
+ *
16991
+ * When called:
16992
+ * - Rendering loop resumes from current state
16993
+ * - If in Idle state, immediately renders current frame to restore display
16994
+ *
16995
+ * @example
16996
+ * // Resume rendering
16997
+ * avatarView.resumeRendering()
16998
+ */
16999
+ resumeRendering() {
17000
+ if (this._renderingEnabled) {
17001
+ return;
17002
+ }
17003
+ this._renderingEnabled = true;
17004
+ logger.log("[AvatarView] Rendering resumed");
17005
+ if (this.isInitialized && this.renderSystem && this.renderingState === "idle") {
17006
+ this.renderCurrentIdleFrame();
17007
+ }
17008
+ }
17009
+ /**
17010
+ * Check if rendering is currently enabled
17011
+ * @returns true if rendering is enabled, false if paused
17012
+ */
17013
+ isRenderingEnabled() {
17014
+ return this._renderingEnabled;
17015
+ }
17016
+ /**
17017
+ * Render current idle frame immediately
17018
+ * @internal
17019
+ */
17020
+ async renderCurrentIdleFrame() {
17021
+ const avatarCore = AvatarSDK.getAvatarCore();
17022
+ if (!avatarCore || !this.renderSystem) {
17023
+ return;
17024
+ }
17025
+ try {
17026
+ const splatData = await avatarCore.computeCompleteFrameFlat(
17027
+ { frameIndex: this.idleCurrentFrameIndex },
17028
+ this.characterHandle ?? void 0
17029
+ );
17030
+ if (splatData) {
17031
+ this.doRender(splatData);
17032
+ }
17033
+ } catch (error) {
17034
+ logger.warn("[AvatarView] Failed to render current idle frame:", error);
17035
+ }
17036
+ }
19223
17037
  /**
19224
17038
  * 获取渲染性能统计
19225
17039
  * @returns 渲染性能统计数据,如果渲染系统未初始化则返回 null
@@ -19260,6 +17074,9 @@ class AvatarView {
19260
17074
  const { x: x2, y: y2, scale } = value;
19261
17075
  logger.log(`[AvatarView] Setting transform: x=${x2}, y=${y2}, scale=${scale}`);
19262
17076
  this.renderSystem.setTransform(x2, y2, scale);
17077
+ if (this.isInitialized && this.renderSystem && this._renderingEnabled) {
17078
+ this.renderSystem.renderFrame();
17079
+ }
19263
17080
  }
19264
17081
  /**
19265
17082
  * Report avatar_active event