零基礎開啟元宇宙|如何快速創(chuàng)建虛擬形象

2022-12-12 17:11:33 來源:51CTO博客

元宇宙(Metaverse),是人類運用數(shù)字技術構建的,由現(xiàn)實世界映射或超越現(xiàn)實世界,可與現(xiàn)實世界交互的虛擬世界,具備新型社會體系的數(shù)字生活空間。

可見元宇宙第一步是創(chuàng)建專屬虛擬形象,但創(chuàng)建3D虛擬形象需要3D基礎知識。對于大部分??android???開發(fā)者(包括我本人)來說沒有這方面的積累。難道因此我們就難以進入元宇宙的世界嗎?不,今天我們借助即構平臺提供的Avatar SDK?,只要有??Android??基礎即可進入最火的元宇宙世界!先看效果:

上面gif被壓縮的比較狠,這里放一張截圖:


(資料圖片)

1 免費注冊即構開發(fā)者

前往即構控制臺網站:注冊開發(fā)者賬戶。注冊成功后,創(chuàng)建項目:

控制臺中可以得到AppID和AppSign兩個數(shù)據,這兩個數(shù)據是重要憑證,后面會用到。

由于我們用到了即構的??Avatar??功能,但目前官方沒有提供線上自動開啟方式,需要主動找客服申請(當然,這是免費的),只需提供自己項目的包名,即可開通Avatar權限。

注意,如果不向客服申請??Avatar??權限,調用??AvatarSDK??會失?。?/strong>

2 準備開發(fā)環(huán)境

前往即構官方元宇宙開發(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)請求權限代碼可看附件源碼。

3 導入資源

上一小節(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");

4 創(chuàng)建虛擬形象

創(chuàng)建虛擬形象本質上來說就是調用即構的??Avatar SDK??,其大致流程如下:

接下來我們逐步實現(xiàn)上面流程。

需要注意的是,上面示意圖中采用的是AvatarView,可以非常方便的直接展示Avatar形象,但是不方便后期將畫面通過RTC實時傳遞, 因此,我們后面的具體實現(xiàn)視通過TextureView替代AvatarView。

4.1 申請權鑒

這里再次強調一下,一定要打開??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);        }    });

4.2 初始化AvatarService

初始化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");}

4.3 創(chuàng)建虛擬形象

前面在??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)建,關鍵代碼全部展示。

標簽: 虛擬形象 絕對路徑 服務器端

上一篇:焦點!關于Kubernetes集群中常見問題的排查方法的一些筆記
下一篇:GO 學習