
元宇宙(Metaverse),是人類運用數(shù)字技術構建的,由現(xiàn)實世界映射或超越現(xiàn)實世界,可與現(xiàn)實世界交互的虛擬世界,具備新型社會體系的數(shù)字生活空間。
可見元宇宙第一步是創(chuàng)建專屬虛擬形象,但創(chuàng)建3D虛擬形象需要3D基礎知識。對于大部分??android?
??開發(fā)者(包括我本人)來說沒有這方面的積累。難道因此我們就難以進入元宇宙的世界嗎?不,今天我們借助即構平臺提供的Avatar SDK?,只要有??Android?
?基礎即可進入最火的元宇宙世界!先看效果:
上面gif被壓縮的比較狠,這里放一張截圖:
(資料圖片)
前往即構控制臺網站:注冊開發(fā)者賬戶。注冊成功后,創(chuàng)建項目:
控制臺中可以得到AppID和AppSign兩個數(shù)據,這兩個數(shù)據是重要憑證,后面會用到。
由于我們用到了即構的??Avatar?
?功能,但目前官方沒有提供線上自動開啟方式,需要主動找客服申請(當然,這是免費的),只需提供自己項目的包名,即可開通Avatar權限。
注意,如果不向客服申請??Avatar?
?權限,調用??AvatarSDK?
?會失?。?/strong>
前往即構官方元宇宙開發(fā)SDK網站??https://doc-zh.zego.im/article/15302??下載SDK,得到如下文件列表:
接下來過程如下:
打開??SDK?
??目錄,將里面的??ZegoAvatar.aar?
??拷貝至??app/libs?
?目錄下。添加??SDK?
??引用。打開??app/build.gradle?
??文件,在??dependencies?
??節(jié)點引入 ??libs?
??下所有的??jar?
??和??aar?
?:dependencies { implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"]) //通配引入 //其他略}設置權限。根據實際應用需要,設置應用所需權限。進入?
?app/src/main/AndroidManifest.xml?
? 文件,添加權限。因為?
?Android 6.0?
??在一些比較重要的權限上要求必須申請動態(tài)權限,不能只通過 ??AndroidMainfest.xml?
?文件申請靜態(tài)權限。具體動態(tài)請求權限代碼可看附件源碼。
上一小節(jié)下載的??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");
創(chuàng)建虛擬形象本質上來說就是調用即構的??Avatar SDK?
?,其大致流程如下:
接下來我們逐步實現(xiàn)上面流程。
需要注意的是,上面示意圖中采用的是AvatarView,可以非常方便的直接展示Avatar形象,但是不方便后期將畫面通過RTC實時傳遞, 因此,我們后面的具體實現(xiàn)視通過TextureView替代AvatarView。
這里再次強調一下,一定要打開??https://doc-zh.zego.im/article/15206???點擊右下角有??“聯(lián)系我們”?
??,向客服申請免費開通Avatar權限。否則無法使用??Avatar SDK?
?。
申請權鑒代碼如下:
public class KeyCenter { // 控制臺地址: https://console.zego.im/dashboard public static long APP_ID = 這里值可以在控制臺查詢,參考第一節(jié); //這里填寫APPID public static String APP_SIGN = 這里值可以在控制臺查詢,參考第一節(jié); // 鑒權服務器的地址 public final static String BACKEND_API_URL = "https://aieffects-api.zego.im?Actinotallow=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?
??函數(shù),例如在??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); } });
初始化AvatarService過程比較漫長(可能要幾秒),通過開啟worker線程后臺加載以避免主線程阻塞。因此我們定義一個回調函數(shù),待完成初始化后回調通知:
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);}@Overridepublic void onStateChange(ZegoAvatarServiceState state) { if (state == ZegoAvatarServiceState.InitSucceed) { Log.i("ZegoAvatar", "Init success"); // 要記得及時移除通知 ZegoAvatarService.removeServiceObserver(this); if (listener != null) listener.onInitSucced(); }}
這里??setLicense?
??函數(shù)內完成初始化??AvatarService?
?,初始化完成后會回調onStateChange函數(shù)。但是要注意,在初始化之前必須把資源文件拷貝到本地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");}
前面在??https://doc-zh.zego.im/article/15302???下載??SDK?
??包含了??helper?
?目錄,這個目錄里面有非常重要的兩個文件:
其中??ZegoCharacterHelper?
??文件是個接口定義類,即雖然是個類,但具體的實現(xiàn)全部在??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 //其他函數(shù)略}
可以看到,上面數(shù)據基本包含所有人臉屬性了,基本具備了捏臉能力,篇幅原因,我們這里不具體去實現(xiàn)。有這方面需求的讀者,可以通過在界面上調整上面相關屬性來實現(xiàn)。
接下來我們開始創(chuàng)建虛擬形象,首先創(chuàng)建一個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); } }
示例作用,為了簡單起見,我們這里只針對眉毛和衣服資源做選取。接下來創(chuàng)建一個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,調用此函數(shù)之前,請確保已經調用過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; // 創(chuàng)建 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); // 獲取當前妝容數(shù)據, 可以保存到用戶資料中 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;// // 收到回調后,開發(fā)者需要執(zhí)行啟動視頻采集相關的業(yè)務邏輯,例如開啟攝像頭等 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; }}
以上代碼完成了整個虛擬形象的創(chuàng)建,關鍵代碼全部展示。