save code

This commit is contained in:
xsl
2026-04-26 00:12:56 +08:00
parent 50680ee854
commit c5ec7c7dc5
22 changed files with 1170 additions and 216 deletions
+289
View File
@@ -0,0 +1,289 @@
# Face SDK 使用说明
基于 Vulkan + MediaPipe FaceLandmarker 的实时人脸贴图渲染 SDK,主要面向**美妆 / AR 试妆**场景:摄像头预览 → 人脸 landmark 检测 → 多帧 PNG 贴图按 motion 序列贴在人脸上。
支持任意分辨率手机(采用"短边正方形画布 + 长边 letterbox"布局)、前置 / 后置摄像头自动切换、镜子镜像效果。
---
## 目录结构
```
face_sdk/
├── app/ ← SDK 主体(可作为 AAR 给业务方接入)
│ ├── src/main/java/com/hmwl/face_sdk/
│ │ ├── FaceActivity.java ← 业务方继承的核心 Activity
│ │ ├── FaceLandmarkerHelper.kt ← MediaPipe 封装
│ │ ├── DebugLog.java ← Java/C++ 统一日志(写文件)
│ │ ├── InitArg.java / Motion.java / MotionList.java / Frame.java ← JSON 配置 POJO
│ │ └── ...
│ ├── src/main/cpp/main.cpp ← JNI + native 主循环
│ └── src/main/assets/shaders/ ← Vulkan GLSLbg/texture/...
├── vulkan/ ← 跨平台 Vulkan 渲染核心
│ ├── Application.{h,cpp} ← Vulkan 初始化 / 窗口生命周期
│ └── FaceApp.{h,cpp} ← 业务渲染(face mesh + bg quad + motion 调度)
├── example/ ← 示例 app,演示完整接入
│ ├── src/main/java/com/inewme/uvmirror/
│ │ ├── MainActivity.kt ← 入口(Compose UI,前/后置选择)
│ │ ├── MakeupActivity.kt ← 业务 Activity(继承 FaceActivity
│ │ └── MotionManager.kt ← motion 资源加载封装
│ ├── src/main/AndroidManifest.xml
│ └── src/main/assets/pic/
│ ├── motion_data_ex.json ← motion 元数据(帧文件名 + 锚点 x/y)
│ └── <motion_id>/000xxx.png ← motion 各帧贴图
├── compile_shader.bat ← 编译所有 .vert/.frag 为 .spv(必须在改 shader 后执行)
└── third_party/ ← VMA / glm / glfw / mediapipe …
```
---
## 快速上手
### 1. 编译 + 运行 example
```powershell
# Windows 上,确保 SDK 路径在 local.properties 里,以及 NDK 已安装
.\gradlew.bat :example:installDebug
```
启动后:
- 主界面有"前置摄像头 / 后置摄像头"两个 RadioButton(默认前置)
- 点击 "Go to Makeup" 进入 `MakeupActivity`
- `MakeupActivity` 自动加载 `assets/pic/motion_data_ex.json` 配置的 motion,按 `update()` 里的脚本播放
### 2. 改 shader 后必须重编
```powershell
.\compile_shader.bat
.\gradlew.bat :example:installDebug
```
---
## 在自己的 App 里集成
### Step 1:依赖 SDK 模块
在你的 `settings.gradle` / `build.gradle` 引入 `:app` 模块(或打包成 AAR)。
### Step 2:在 `AndroidManifest.xml` 声明业务 Activity
业务 Activity 必须**竖屏**,且**屏蔽常见 configChanges 不要重建**
```xml
<activity
android:name=".MakeupActivity"
android:exported="false"
android:screenOrientation="portrait"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|keyboard|navigation|smallestScreenSize" />
```
### Step 3:继承 `FaceActivity` 写业务 Activity
```kotlin
class MakeupActivity : FaceActivity() {
lateinit var motionMgr: MotionManager
private var isMotionLoadFinished = false
override fun onCreate(savedInstanceState: Bundle?) {
// 在 super.onCreate 之前构造 MotionManager(它会调 SetInitArg
motionMgr = MotionManager(
this,
/* fps */ 10,
/* zoom */ 1.5f,
/* r,g,b */ 0f, 0f, 1f, // letterbox / 圆外纯色(蓝)
/* radius */ 240f, // 480 设计基线下的圆半径,SDK 自动按 canvas_size/480 缩放
/* offset */ 200f, -200f // 480 设计基线下的中心偏移
)
super.onCreate(savedInstanceState)
// 一次性加载所有要用到的 motion,加载完才能 PlayMotionList
motionMgr.LoadMotionList(listOf("4", "56")) {
isMotionLoadFinished = true
}
// 自己的 update 循环(生命周期感知,避免泄漏)
lifecycleScope.launch {
while (true) {
update()
delay(10)
}
}
}
private fun update() {
if (!isMotionLoadFinished) return
// 想播什么就调,调 SDK API(详见下面"API 参考"
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (keyCode == KeyEvent.KEYCODE_BACK) {
Stop() // ← 退出 Activity 前必须先 Stop(),否则 native 线程仍在跑
finish()
return true
}
return super.onKeyDown(keyCode, event)
}
}
```
### Step 4:从入口 Activity 启动业务 Activity,传入摄像头朝向
```kotlin
import androidx.camera.core.CameraSelector
import com.hmwl.face_sdk.FaceActivity
val intent = Intent(context, MakeupActivity::class.java).apply {
// 前置:CameraSelector.LENS_FACING_FRONT;后置:LENS_FACING_BACK
// 不传时 SDK 默认走后置(向后兼容)
putExtra(FaceActivity.EXTRA_LENS_FACING, CameraSelector.LENS_FACING_FRONT)
}
context.startActivity(intent)
```
前置摄像头 SDK 会自动:
- 在 GPU 端把 bg + face mesh 整体水平翻转 → 镜子效果
- mediapipe 输入仍是 raw(未镜像)帧,所以 landmark 与 raw 帧 UV 严格对齐
---
## API 参考(`FaceActivity` 公开方法)
| 方法 | 说明 |
| --- | --- |
| `static final String EXTRA_LENS_FACING` | Intent extra key,值为 `CameraSelector.LENS_FACING_FRONT``LENS_FACING_BACK` |
| `void SetInitArg(String json)` | 设置全局参数(zoom / r,g,b / radius / offset_x,offset_y / action_fps),JSON 格式见 `InitArg` |
| `String PreLoadAction(String json, LoadMotionListFinishedCallback cb)` | 异步预加载一组 motion 资源;加载完毕回调 cb |
| `void PlayMotionList(List<String> motionList, boolean loop, PlayMotionListFinishedCallback cb)` | 播放 motion 序列,`loop=true` 时无限循环,`cb` 为播完一轮的回调(loop 模式下不会触发,传 `null` 即可) |
| `void StopMotion()` | 暂停当前 motion 播放(停在当前帧) |
| `void ResumeMotion()` | 恢复 `StopMotion()` 之前的播放进度 |
| `void Stop()` | **完全停止 native 渲染**,退出 Activity 前必须调 |
**注意:** `LoadMotionList` / `PreLoadAction` 是异步的,回调里设置 `isMotionLoadFinished=true` 之后才能 `PlayMotionList`,否则播放命令会被静默丢弃。
---
## 资源准备
### Motion 帧图
每个 motion 用一个独立目录,PNG 帧按文件名顺序播放:
```
assets/pic/4/000000.png
assets/pic/4/000001.png
...
assets/pic/4/000031.png
```
PNG 必须是 **RGBA**(透明通道),SDK 直接采样后 alpha-blend 到 bg 上方。
### `motion_data_ex.json`
motion 元数据,描述每帧贴图的**锚点偏移**(贴在脸上的相对位置,画布坐标系,原点在屏幕中心):
```json
{
"4": {
"000000.png": { "x": 0.0, "y": 0.0 },
"000001.png": { "x": 0.0, "y": 0.0 },
...
},
"56": {
"000000.png": { "x": 0.0, "y": 0.0 },
...
}
}
```
---
## 设计要点(接入方可不读,但要改 shader / 渲染时必读)
### 1. 画布与 letterbox
- **画布**:屏幕短边的正方形,居中。屏幕长边方向自然 letterbox(上下或左右黑边)。
- 画布坐标系("画布 NDC")∈ [-1, +1]²,原点在画布中心。
- mediapipe FaceLandmarker 输出的归一化 landmark ∈ [0, 1]² 直接对应画布 NDC(经过 `*2-1`)。
- 业务参数 `zoom / offset_x / offset_y / radius` 都是**屏幕物理像素**(SDK 端按 `canvas_size / 480` 自动缩放,业务方用 480 设计基线即可)。
### 2. 摄像头帧 → 画布的几何流水线
```
raw 摄像头帧 (W×H, 4:3 典型)
└─① 中央裁切 min(W,H)×min(W,H) 正方形
└─② Matrix.postRotate(rotationDegrees) 顺时针旋转 → 正立 face crop
└─③ MediaPipe 输出 landmark ∈ [0,1]² ─────▶ 画布 NDC
└─ shader 做反向变换贴回 raw 帧 UV (bg)
└─ shader 直接当 face mesh 顶点 (face)
```
### 3. 镜子效果(前置摄像头)
`PushConstants.mirror_x ∈ {0.0, 1.0}`
- 前置时 = 1.0shader 里把最终 NDC.x 翻转 (`pos.x *= 1.0 - 2.0*mirror_x`)
- bg + face mesh 都做同样翻转 → 整体水平镜像,对齐保持
- mediapipe 仍接收**未镜像**的 raw 帧,避免双重镜像把 landmark 弄反
### 4. bg 与 face mesh 对齐不变量
bg 圆内显示摄像头视频,face mesh 把 motion png 贴到 landmark 位置——两者对齐依赖:
> **屏幕同一像素位置,bg 显示的 raw UV = 把这个屏幕位置反算回 mediapipe landmark 坐标**
实现方式:bg quad 与 face mesh **完全相同**地经过 `zoom × offset × letterbox × mirror` 几何链路;同时 bg 的 `outTexCoord` 基于"未变换的 quad 顶点 pos"算 raw UV——这样屏幕 fragment 反算的 pos 就等价于"对应 landmark 的画布 NDC"UV 完美贴合。
bg quad 顶点取 `±10`(而非 `±1`),保证任何 zoom/offset 下 quad 在屏幕上 GPU clip 后整屏覆盖,letterbox 也被 bg.frag 的 r/g/b 蓝色填充。
### 5. 摄像头分辨率适配
- CameraX 用 `setTargetResolution(640, 480)` 强制拿低分辨率帧(SDK 不需要高分辨率)
- shader 用 `camera_aspect = W/H``camera_rotation`CameraX 给的 rotationDegrees)做 UV 反变换
- 摄像头帧分辨率/朝向变化时,SDK 会在 `processCameraFrame` 里自动重建 bg 纹理
### 6. 窗口生命周期
- `APP_CMD_TERM_WINDOW``Application::cleanupForWindowLost()`:只销毁**窗口资源**surface / swapchain / framebuffers / imageViews / commandBuffers / sync),保留 renderPass / pipelines / device / VMA / textures
- `APP_CMD_INIT_WINDOW``Application::reinitForNewWindow()`:复用全局资源,只重建窗口资源
- `android_main` 退出时**不再额外销毁** Vulkan 资源(保留给下次 NativeActivity 复用),避免 use-after-free 崩溃
---
## 调试
### 日志文件
SDK 把 Java/Kotlin 日志(通过 `com.hmwl.face_sdk.DebugLog.i / e`)和 C++ 日志(`DebugLog::log`**统一写入**
```
/data/user/0/<your.package>/files/face_sdk_debug.log
```
```powershell
# 拉到本地分析
adb pull /data/user/0/com.inewme.uvmirror.f20260113/files/face_sdk_debug.log .
```
崩溃时还会自动追加一段含信号、寄存器、backtrace 的 dump`========== FACE_SDK CRASH ==========`)。
### 常见问题
| 现象 | 排查方向 |
| --- | --- |
| 黑屏 / 没有摄像头预览 | 检查相机权限是否授予;查看日志是否有 `processImageNative` 调用 |
| 人脸贴图不出现 | 检查 `motion_data_ex.json` 里的 motion id 是否在 `LoadMotionList` 里加载过;检查 `isMotionLoadFinished` 是否被回调置 true |
| 贴图不贴合人脸 | bg 与 face mesh 的 zoom/offset/mirror 必须严格一致——见"设计要点 #4 对齐不变量" |
| 切换摄像头后崩溃 | 见 `cleanupSecondInit` 注释;不要在 Android 路径调用它,仅 desktop 入口可用 |
| 改了 shader 不生效 | 必须先跑 `compile_shader.bat` 重新生成 `.spv` |
---
## 兼容性
- minSdk 29targetSdk 看 `app/build.gradle`
- 设备需支持 Vulkan 1.0+ 和 MediaPipe TFLite delegate
- 已在 MI 9Android 10、Adreno 640、Vulkan vendor `qglinternal`)上验证通过