原文:Jgit的使用筆記 - Stars-One的雜貨小窩 之前整的一個系統,涉及到git代碼的推送,是通過cmd命令去推送的,然後最近在產品驗收的時候,測試部門隨意填了個git倉庫,然後導致倉庫代碼被覆蓋了,還好本地留有備份,沒出現啥大問題 然後就計劃於是就改為使用Jgit庫來實現推送代碼的功能, ...
元宇宙(Metaverse),是人類運用數字技術構建的,由現實世界映射或超越現實世界,可與現實世界交互的虛擬世界,具備新型社會體系的數字生活空間。
可見元宇宙第一步是創建專屬虛擬形象,但創建3D虛擬形象需要3D基礎知識。對於大部分android
開發者(包括我本人)來說沒有這方面的積累。難道因此我們就難以進入元宇宙的世界嗎?不,今天我們藉助即構平臺提供的Avatar SDK,只要有Android
基礎即可進入最火的元宇宙世界!先看效果:
上面gif被壓縮的比較狠,這裡放一張截圖:
1 免費註冊即構開發者
前往即構控制台網站:https://console.zego.im/註冊開發者賬戶。註冊成功後,創建項目:
控制臺中可以得到AppID和AppSign兩個數據,這兩個數據是重要憑證,後面會用到。
由於我們用到了即構的Avatar
功能,但目前官方沒有提供線上自動開啟方式,需要主動找客服申請(當然,這是免費的),只需提供自己項目的包名,即可開通Avatar許可權。打開https://doc-zh.zego.im/article/15206右下角有“聯繫我們”
,點擊即可跟客服申請免費開通許可權。
註意,如果不向客服申請Avatar
許可權,調用AvatarSDK
會失敗!
2 準備開發環境
前往即構官方元宇宙開發SDK網站https://doc-zh.zego.im/article/15302下載SDK,得到如下文件列表:
接下來過程如下:
- 打開
SDK
目錄,將裡面的ZegoAvatar.aar
拷貝至app/libs
目錄下。 - 添加
SDK
引用。打開app/build.gradle
文件,在dependencies
節點引入libs
下所有的jar
和aar
:
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', "*.aar"]) //通配引入
//其他略
}
- 設置許可權。根據實際應用需要,設置應用所需許可權。進入
app/src/main/AndroidManifest.xml
文件,添加許可權。
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-feature
android:glEsVersion="0x00020000"
android:required="true" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
因為
Android 6.0
在一些比較重要的許可權上要求必須申請動態許可權,不能只通過AndroidMainfest.xml
文件申請靜態許可權。具體動態請求許可權代碼可看附件源碼。
3 導入資源
上一小節下載的zip
文件中,有個assets
目錄。裡面包含了Avatar
形象相關資源,如:衣服、眉毛、鞋子等。這是即構官方免費提供的資源,可以滿足一般性需求了。當然了,如果想要自己定製資源也是可以的。assets
文件內容如下:
資源名稱 | 說明 |
---|---|
AIModel.bundle | Avatar 的 AI 模型資源。當使用表情隨動、聲音隨動、AI 捏臉等能力時,必須先將該資源的絕對路徑設置給 Avatar SDK。 |
base.bundle | 美術資源,包含基礎 3D 人物模型資源、資源映射表、人物模型預設外形等。 |
Packages | 美妝、掛件、裝飾等資源。 每個資源 200 KB ~ 1 MB 不等,跟資源複雜度相關。 |
註意:由於資源文件很大,上面下載的美術資源只包含少量必須資源。如果需要全部資源,可以去官網找客服索要:https://doc-zh.zego.im/article/14882
上面的文件需要存放到Android
本地SDCard
上,這裡有2個方案可供參考:
- 方案一: 將資源先放入到
app/src/assets
目錄內,然後在app啟動時,自動將assets的內容拷貝到SDcard
中。 - 方案二: 將資源放入到伺服器端,運行時自動從伺服器端下載。
為了簡單起見,我們這裡採用方案一。參考代碼如下, 詳細代碼可以看附件:
AssetsFileTransfer.copyAssetsDir2Phone(this.getApplication(),
"AIModel.bundle", "assets");
AssetsFileTransfer.copyAssetsDir2Phone(this.getApplication(),
"base.bundle", "assets");
AssetsFileTransfer.copyAssetsDir2Phone(this.getApplication(),
"Packages", "assets");
4 創建虛擬形象
創建虛擬形象本質上來說就是調用即構的Avatar SDK
,其大致流程如下:
接下來我們逐步實現上面流程。
需要註意的是,上面示意圖中採用的是AvatarView,可以非常方便的直接展示Avatar形象,但是不方便後期將畫面通過RTC實時傳遞, 因此,我們後面的具體實現視通過TextureView替代AvatarView。
4.1 申請權鑒
這裡再次強調一下,一定要打開https://doc-zh.zego.im/article/15206點擊右下角有“聯繫我們”
,向客服申請免費開通Avatar許可權。否則無法使用Avatar SDK
。
申請權鑒代碼如下:
public class KeyCenter {
// 控制臺地址: https://console.zego.im/dashboard
public static long APP_ID = 這裡值可以在控制台查詢,參考第一節; //這裡填寫APPID
public static String APP_SIGN = 這裡值可以在控制台查詢,參考第一節;
// 鑒權伺服器的地址
public final static String BACKEND_API_URL = "https://aieffects-api.zego.im?Action=DescribeAvatarLicense";
public static String avatarLicense = null;
public static String getURL(String authInfo) {
Uri.Builder builder = Uri.parse(BACKEND_API_URL).buildUpon();
builder.appendQueryParameter("AppId", String.valueOf(APP_ID));
builder.appendQueryParameter("AuthInfo", authInfo);
return builder.build().toString();
}
public interface IGetLicenseCallback {
void onGetLicense(int code, String message, ZegoLicense license);
}
/**
* 線上拉取 license
* @param context
* @param callback
*/
public static void getLicense(Context context, final IGetLicenseCallback callback) {
requestLicense(ZegoAvatarService.getAuthInfo(APP_SIGN, context), callback);
}
/**
* 獲取license
* */
public static void requestLicense(String authInfo, final IGetLicenseCallback callback) {
String url = getURL(authInfo);
HttpRequest.asyncGet(url, ZegoLicense.class, (code, message, responseJsonBean) -> {
if (callback != null) {
callback.onGetLicense(code, message, responseJsonBean);
}
});
}
public class ZegoLicense {
@SerializedName("License")
private String license;
public String getLicense() {
return license;
}
public void setLicense(String license) {
this.license = license;
}
}
}
在獲取Lincense時,只需調用getLicense
函數,例如在Activity
類中只需如下調用:
KeyCenter.getLicense(this, (code, message, response) -> {
if (code == 0) {
KeyCenter.avatarLicense = response.getLicense();
showLoading("正在初始化...");
avatarMngr = AvatarMngr.getInstance(getApplication());
avatarMngr.setLicense(KeyCenter.avatarLicense, this);
} else {
toast("License 獲取失敗, code: " + code);
}
});
4.2 初始化AvatarService
初始化AvatarService過程比較漫長(可能要幾秒),通過開啟worker線程後臺載入以避免主線程阻塞。因此我們定義一個回調函數,待完成初始化後回調通知:
public interface OnAvatarServiceInitSucced {
void onInitSucced();
}
public void setLicense(String license, OnAvatarServiceInitSucced listener) {
this.listener = listener;
ZegoAvatarService.addServiceObserver(this);
String aiPath = FileUtils.getPhonePath(mApp, "AIModel.bundle", "assets"); // AI 模型的絕對路徑
ZegoServiceConfig config = new ZegoServiceConfig(license, aiPath);
ZegoAvatarService.init(mApp, config);
}
@Override
public void onStateChange(ZegoAvatarServiceState state) {
if (state == ZegoAvatarServiceState.InitSucceed) {
Log.i("ZegoAvatar", "Init success");
// 要記得及時移除通知
ZegoAvatarService.removeServiceObserver(this);
if (listener != null) listener.onInitSucced();
}
}
這裡setLicense
函數內完成初始化AvatarService
,初始化完成後會回調onStateChange函數。但是要註意,在初始化之前必須把資源文件拷貝到本地SDCard,即完成資源導入:
private void initRes(Application app) {
// 先把資源拷貝到SD卡,註意:線上使用時,需要做一下判斷,避免多次拷貝。資源也可以做成從網路下載。
if (!FileUtils.checkFile(app, "AIModel.bundle", "assets"))
FileUtils.copyAssetsDir2Phone(app, "AIModel.bundle", "assets");
if (!FileUtils.checkFile(app, "base.bundle", "assets"))
FileUtils.copyAssetsDir2Phone(app, "base.bundle", "assets");
if (!FileUtils.checkFile(app, "human.bundle", "assets"))
FileUtils.copyAssetsDir2Phone(app, "human.bundle", "assets");
if (!FileUtils.checkFile(app, "Packages", "assets"))
FileUtils.copyAssetsDir2Phone(app, "Packages", "assets");
}
4.3 創建虛擬形象
前面在https://doc-zh.zego.im/article/15302下載SDK
包含了helper
目錄,這個目錄裡面有非常重要的兩個文件:
其中ZegoCharacterHelper
文件是個介面定義類,即雖然是個類,但具體的實現全部在ZegoCharacterHelperImpl
中。我們先一睹為快,看看ZegoCharacterHelper
包含了哪些可處理的屬性:
public class ZegoCharacterHelper {
public static final String MODEL_ID_MALE = "male";
public static final String MODEL_ID_FEMALE = "female";
//****************************** 捏臉維度的 key 值 ******************************/
public static final String FACESHAPE_BROW_SIZE_Y = "faceshape_brow_size_y";// 眉毛厚度, 取值範圍0.0-1.0,預設值0.5
public static final String FACESHAPE_BROW_SIZE_X = "faceshape_brow_size_x";// 眉毛長度, 取值範圍0.0-1.0,預設值0.5
public static final String FACESHAPE_BROW_ALL_Y = "faceshape_brow_all_y";// 眉毛高度, 取值範圍0.0-1.0,預設值0.5
public static final String FACESHAPE_BROW_ALL_ROLL_Z = "faceshape_brow_all_roll_z";// 眉毛旋轉, 取值範圍0.0-1.0,預設值0.5
public static final String FACESHAPE_EYE_SIZE = "faceshape_eye_size"; // 眼睛大小, 取值範圍0.0-1.0,預設值0.5
public static final String FACESHAPE_EYE_SIZE_Y = "faceshape_eye_size_y";
public static final String FACESHAPE_EYE_ROLL_Y = "faceshape_eye_roll_y";// 眼睛高度, 取值範圍0.0-1.0,預設值0.5
public static final String FACESHAPE_EYE_ROLL_Z = "faceshape_eye_roll_z";// 眼睛旋轉, 取值範圍0.0-1.0,預設值0.5
public static final String FACESHAPE_EYE_X = "faceshape_eye_x";// 雙眼眼距, 取值範圍0.0-1.0,預設值0.5
public static final String FACESHAPE_NOSE_ALL_X = "faceshape_nose_all_x";// 鼻子寬度, 取值範圍0.0-1.0,預設值0.5
public static final String FACESHAPE_NOSE_ALL_Y = "faceshape_nose_all_y";// 鼻子高度, 取值範圍0.0-1.0,預設值0.5
public static final String FACESHAPE_NOSE_SIZE_Z = "faceshape_nose_size_z";
public static final String FACESHAPE_NOSE_ALL_ROLL_Y = "faceshape_nose_all_roll_y";// 鼻頭旋轉, 取值範圍0.0-1.0,預設值0.5
public static final String FACESHAPE_NOSTRIL_ROLL_Y = "faceshape_nostril_roll_y";// 鼻翼旋轉, 取值範圍0.0-1.0,預設值0.5
public static final String FACESHAPE_NOSTRIL_X = "faceshape_nostril_x";
public static final String FACESHAPE_MOUTH_ALL_Y = "faceshape_mouth_all_y";// 嘴巴上下, 取值範圍0.0-1.0,預設值0.5
public static final String FACESHAPE_LIP_ALL_SIZE_Y = "faceshape_lip_all_size_y";// 嘴唇厚度, 取值範圍0.0-1.0,預設值0.5
public static final String FACESHAPE_LIPCORNER_Y = "faceshape_lipcorner_y";// 嘴角旋轉, 取值範圍0.0-1.0,預設值0.5
public static final String FACESHAPE_LIP_UPPER_SIZE_X = "faceshape_lip_upper_size_x"; // 上唇寬度, 取值範圍0.0-1.0,預設值0.5
public static final String FACESHAPE_LIP_LOWER_SIZE_X = "faceshape_lip_lower_size_x"; // 下唇寬度, 取值範圍0.0-1.0,預設值0.5
public static final String FACESHAPE_JAW_ALL_SIZE_X = "faceshape_jaw_all_size_x";// 下巴寬度, 取值範圍0.0-1.0,預設值0.5
public static final String FACESHAPE_JAW_Y = "faceshape_jaw_y";// 下巴高度, 取值範圍0.0-1.0,預設值0.5
public static final String FACESHAPE_CHEEK_ALL_SIZE_X = "faceshape_cheek_all_size_x";// 臉頰寬度, 取值範圍0.0-1.0,預設值0.5
//其他函數略
}
可以看到,上面數據基本包含所有人臉屬性了,基本具備了捏臉能力,篇幅原因,我們這裡不具體去實現。有這方面需求的讀者,可以通過在界面上調整上面相關屬性來實現。
接下來我們開始創建虛擬形象,首先創建一個User實體類:
public class User {
public String userName; //用戶名
public String userId; //用戶ID
public boolean isMan; //性別
public int width; //預覽寬度
public int height; //預覽高度
public int bgColor; //背景顏色
public int shirtIdx = 0; // T-shirt資源id
public int browIdx = 0; //眉毛資源id
public User(String userName, String userId, int width, int height) {
this.userName = userName;
this.userId = userId;
this.width = width;
this.height = height;
this.isMan = true;
bgColor = Color.argb(255, 33, 66, 99);
}
}
示例作用,為了簡單起見,我們這裡只針對眉毛和衣服資源做選取。接下來創建一個Activity:
public class AvatarActivity extends BaseActivity {
private int vWidth = 720;
private int vHeight = 1080;
private User user = new User("C_0001", "C_0001", vWidth, vHeight);
private TextureView mTextureView; //用於顯示Avatar形象
private AvatarMngr mAvatarMngr; // 用於維護管理Avatar
private ColorPickerDialog colorPickerDialog; //用於背景色選取
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_avatar);
// ....
// 其他初始化界面相關代碼略...
// ....
user.isMan = true;
mTextureView = findViewById(R.id.avatar_view);
mZegoMngr = ZegoMngr.getInstance(getApplication());
// 開啟虛擬形象預覽
mAvatarMngr.start(mTextureView, user);
}
最後一行代碼中開啟了虛擬形象預覽,那麼這裡開啟虛擬形象預覽具體做了哪些工作呢?show me the code:
public class AvatarMngr implements ZegoAvatarServiceDelegate, RTCMngr.CaptureListener {
private static final String TAG = "AvatarMngr";
private static AvatarMngr mInstance;
private boolean mIsStop = false;
private User mUser = null;
private TextureBgRender mBgRender = null;
private OnAvatarServiceInitSucced listener;
private ZegoCharacterHelper mCharacterHelper;
private Application mApp;
public interface OnAvatarServiceInitSucced {
void onInitSucced();
}
public void setLicense(String license, OnAvatarServiceInitSucced listener) {
this.listener = listener;
ZegoAvatarService.addServiceObserver(this);
String aiPath = FileUtils.getPhonePath(mApp, "AIModel.bundle", "assets"); // AI 模型的絕對路徑
ZegoServiceConfig config = new ZegoServiceConfig(license, aiPath);
ZegoAvatarService.init(mApp, config);
}
public void stop() {
mIsStop = true;
mUser = null;
stopExpression();
}
public void updateUser(User user) {
mUser = user;
if (user.shirtIdx == 0) {
mCharacterHelper.setPackage("m-shirt01");
} else {
mCharacterHelper.setPackage("m-shirt02");
}
if (user.browIdx == 0) {
mCharacterHelper.setPackage("brows_1");
} else {
mCharacterHelper.setPackage("brows_2");
}
}
/**
* 啟動Avatar,調用此函數之前,請確保已經調用過setLicense
*
* @param avatarView
*/
public void start(TextureView avatarView, User user) {
mUser = user;
mIsStop = false;
initAvatar(avatarView, user);
startExpression();
}
private void initAvatar(TextureView avatarView, User user) {
String sex = ZegoCharacterHelper.MODEL_ID_MALE;
if (!user.isMan) sex = ZegoCharacterHelper.MODEL_ID_FEMALE;
// 創建 helper 簡化調用
// base.bundle 是頭模, human.bundle 是全身人模
mCharacterHelper = new ZegoCharacterHelper(FileUtils.getPhonePath(mApp, "human.bundle", "assets"));
mCharacterHelper.setExtendPackagePath(FileUtils.getPhonePath(mApp, "Packages", "assets"));
// 設置形象配置
mCharacterHelper.setDefaultAvatar(sex);
updateUser(user);
// 獲取當前妝容數據, 可以保存到用戶資料中
String json = mCharacterHelper.getAvatarJson();
}
// 啟動表情檢測
private void startExpression() {
// 啟動表情檢測前要申請攝像頭許可權, 這裡是在 MainActivity 已經申請過了
ZegoAvatarService.getInteractEngine().startDetectExpression(ZegoExpressionDetectMode.Camera, expression -> {
// 表情直接塞給 avatar 驅動
mCharacterHelper.setExpression(expression);
});
}
// 停止表情檢測
private void stopExpression() {
// 不用的時候記得停止
ZegoAvatarService.getInteractEngine().stopDetectExpression();
}
// 獲取到 avatar 紋理後的處理
public void onCaptureAvatar(int textureId, int width, int height) {
if (mIsStop || mUser == null) { // rtc 的 onStop 是非同步的, 可能activity已經運行到onStop了, rtc還沒
return;
}
boolean useFBO = true;
if (mBgRender == null) {
mBgRender = new TextureBgRender(textureId, useFBO, width, height, Texture2dProgram.ProgramType.TEXTURE_2D_BG);
}
mBgRender.setInputTexture(textureId);
float r = Color.red(mUser.bgColor) / 255f;
float g = Color.green(mUser.bgColor) / 255f;
float b = Color.blue(mUser.bgColor) / 255f;
float a = Color.alpha(mUser.bgColor) / 255f;
mBgRender.setBgColor(r, g, b, a);
mBgRender.draw(useFBO); // 畫到 fbo 上需要反向的
ZegoExpressEngine.getEngine().sendCustomVideoCaptureTextureData(mBgRender.getOutputTextureID(), width, height, System.currentTimeMillis());
}
@Override
public void onStartCapture() {
if (mUser == null) return;
// // 收到回調後,開發者需要執行啟動視頻採集相關的業務邏輯,例如開啟攝像頭等
AvatarCaptureConfig config = new AvatarCaptureConfig(mUser.width, mUser.height);
// // 開始捕獲紋理
mCharacterHelper.startCaptureAvatar(config, this::onCaptureAvatar);
}
@Override
public void onStopCapture() {
mCharacterHelper.stopCaptureAvatar();
stopExpression();
}
private void initRes(Application app) {
// 先把資源拷貝到SD卡,註意:線上使用時,需要做一下判斷,避免多次拷貝。資源也可以做成從網路下載。
if (!FileUtils.checkFile(app, "AIModel.bundle", "assets"))
FileUtils.copyAssetsDir2Phone(app, "AIModel.bundle", "assets");
if (!FileUtils.checkFile(app, "base.bundle", "assets"))
FileUtils.copyAssetsDir2Phone(app, "base.bundle", "assets");
if (!FileUtils.checkFile(app, "human.bundle", "assets"))
FileUtils.copyAssetsDir2Phone(app, "human.bundle", "assets");
if (!FileUtils.checkFile(app, "Packages", "assets"))
FileUtils.copyAssetsDir2Phone(app, "Packages", "assets");
}
@Override
public void onError(ZegoAvatarErrorCode code, String desc) {
Log.e(TAG, "errorcode : " + code.getErrorCode() + ",desc : " + desc);
}
@Override
public void onStateChange(ZegoAvatarServiceState state) {
if (state == ZegoAvatarServiceState.InitSucceed) {
Log.i("ZegoAvatar", "Init success");
// 要記得及時移除通知
ZegoAvatarService.removeServiceObserver(this);
if (listener != null) listener.onInitSucced();
}
}
private AvatarMngr(Application app) {
mApp = app;
initRes(app);
}
public static AvatarMngr getInstance(Application app) {
if (null == mInstance) {
synchronized (AvatarMngr.class) {
if (null == mInstance) {
mInstance = new AvatarMngr(app);
}
}
}
return mInstance;
}
}
以上代碼完成了整個虛擬形象的創建,關鍵代碼全部展示,如果還需要具體全部代碼,直接從附件中下載即可。