零基础开启元宇宙|如何快速创建虚拟形象

元宇宙(Metaverse),是人类运用数字技术构建的,由现实世界映射或超越现实世界,可与现实世界交互的虚拟世界,具备新型社会体系的数字生活空间。

可见元宇宙第一步是创建专属虚拟形象,但创建3D虚拟形象需要3D基础知识。对于大部分android开发者(包括我本人)来说没有这方面的积累。难道因此我们就难以进入元宇宙的世界吗?不,今天我们借助即构平台提供的Avatar SDK,只要有Android基础即可进入最火的元宇宙世界!先看效果:

上面gif被压缩的比较狠,这里放一张截图:

1 免费注册即构开发者


前往即构控制台网站:console.zego.im/注册开发者账户。注册成功后,创建项目:

控制台中可以得到AppID和AppSign两个数据,这两个数据是重要凭证,后面会用到。

由于我们用到了即构的Avatar功能,但目前官方没有提供线上自动开启方式,需要主动找客服申请(当然,这是免费的),只需提供自己项目的包名,即可开通Avatar权限。打开doc-zh.zego.im/article/152…右下角有“联系我们”,点击即可跟客服申请免费开通权限。

注意,如果不向客服申请Avatar权限,调用AvatarSDK会失败!

2 准备开发环境


前往即构官方元宇宙开发SDK网站doc-zh.zego.im/article/153…下载SDK,得到如下文件列表:

接下来过程如下:

  1. 打开SDK目录,将里面的ZegoAvatar.aar拷贝至app/libs目录下。

  1. 添加SDK引用。打开app/build.gradle文件,在dependencies节点引入 libs下所有的jar和aar:

dependencies {
 
    implementation fileTree(dir: 'libs', include: ['*.jar', "*.aar"]) //通配引入
    
    //其他略
}
  1. 设置权限。根据实际应用需要,设置应用所需权限。进入app/src/main/AndroidManifest.xml 文件,添加权限。

<uses-permissionandroid:name="android.permission.RECORD_AUDIO"/>
<uses-permissionandroid:name="android.permission.INTERNET"/>
<uses-permissionandroid:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permissionandroid:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permissionandroid:name="android.permission.CAMERA"/>
<uses-permissionandroid:name="android.permission.BLUETOOTH"/>
<uses-permissionandroid:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permissionandroid:name="android.permission.READ_EXTERNAL_STORAGE"/>
​
<uses-feature
    android:glEsVersion="0x00020000"
    android:required="true"/>
<uses-featureandroid:name="android.hardware.camera"/>
<uses-featureandroid: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 不等,跟资源复杂度相关。

注意:由于资源文件很大,上面下载的美术资源只包含少量必须资源。如果需要全部资源,可以去官网找客服索要: doc-zh.zego.im/article/148…

上面的文件需要存放到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。

【文章福利】小编整理了一些音视频学习资料包、大厂面试题、技术视频和学习路线图,包括(C/C++,Linux,FFmpegwebRTCrtmp hlsrtsp ffplay srs 等等资料)有需要的可以点击994289133加群免费领取哦~

4.1 申请权鉴


这里再次强调一下,一定要打开doc-zh.zego.im/article/152…点击右下角有“联系我们”,向客服申请免费开通Avatar权限。否则无法使用Avatar SDK。

申请权鉴代码如下:

publicclassKeyCenter { 
    // 控制台地址: https://console.zego.im/dashboard 
    publicstaticlongAPP_ID=这里值可以在控制台查询,参考第一节;  //这里填写APPID
    publicstaticStringAPP_SIGN=  这里值可以在控制台查询,参考第一节; 
    // 鉴权服务器的地址
    publicfinalstaticStringBACKEND_API_URL="https://aieffects-api.zego.im?Action=DescribeAvatarLicense";
​
    publicstaticStringavatarLicense=null;
    publicstaticStringgetURL(StringauthInfo) {
        Uri.Builderbuilder=Uri.parse(BACKEND_API_URL).buildUpon();
        builder.appendQueryParameter("AppId", String.valueOf(APP_ID));
        builder.appendQueryParameter("AuthInfo", authInfo);
​
        returnbuilder.build().toString();
    }
​
    publicinterfaceIGetLicenseCallback {
        voidonGetLicense(intcode, Stringmessage, ZegoLicenselicense);
    }
​
    /**
     * 在线拉取 license
     * @param context
     * @param callback
     */
    publicstaticvoidgetLicense(Contextcontext, finalIGetLicenseCallbackcallback) {
        requestLicense(ZegoAvatarService.getAuthInfo(APP_SIGN, context), callback);
    }
​
    /**
     * 获取license
     * */
    publicstaticvoidrequestLicense(StringauthInfo, finalIGetLicenseCallbackcallback) {
​
        Stringurl=getURL(authInfo);
​
        HttpRequest.asyncGet(url, ZegoLicense.class, (code, message, responseJsonBean) -> {
            if (callback!=null) {
                callback.onGetLicense(code, message, responseJsonBean);
            }
        });
    }
​
​
    publicclassZegoLicense {
        @SerializedName("License")
        privateStringlicense;
        publicStringgetLicense() {
            returnlicense;
        }
        publicvoidsetLicense(Stringlicense) {
            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线程后台加载以避免主线程阻塞。因此我们定义一个回调函数,待完成初始化后回调通知:

publicinterfaceOnAvatarServiceInitSucced {
    voidonInitSucced();
}
​
publicvoidsetLicense(Stringlicense, OnAvatarServiceInitSuccedlistener) {
    this.listener=listener;
    ZegoAvatarService.addServiceObserver(this);
    StringaiPath=FileUtils.getPhonePath(mApp, "AIModel.bundle", "assets"); //   AI 模型的绝对路径
    ZegoServiceConfigconfig=newZegoServiceConfig(license, aiPath);
    ZegoAvatarService.init(mApp, config);
}
​
@Override
publicvoidonStateChange(ZegoAvatarServiceStatestate) {
    if (state==ZegoAvatarServiceState.InitSucceed) {
        Log.i("ZegoAvatar", "Init success");
        // 要记得及时移除通知
        ZegoAvatarService.removeServiceObserver(this);
        if (listener!=null) listener.onInitSucced();
    }
}

这里setLicense函数内完成初始化AvatarService,初始化完成后会回调onStateChange函数。但是要注意,在初始化之前必须把资源文件拷贝到本地SDCard,即完成资源导入:

privatevoidinitRes(Applicationapp) {

// 先把资源拷贝到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 创建虚拟形象


前面在doc-zh.zego.im/article/153…下载SDK包含了helper目录,这个目录里面有非常重要的两个文件:

其中ZegoCharacterHelper文件是个接口定义类,即虽然是个类,但具体的实现全部在ZegoCharacterHelperImpl中。我们先一睹为快,看看ZegoCharacterHelper包含了哪些可处理的属性:

publicclassZegoCharacterHelper {
    publicstaticfinalStringMODEL_ID_MALE="male";
    publicstaticfinalStringMODEL_ID_FEMALE="female";
    //****************************** 捏脸维度的 key 值 ******************************/
    publicstaticfinalStringFACESHAPE_BROW_SIZE_Y="faceshape_brow_size_y";// 眉毛厚度, 取值范围0.0-1.0,默认值0.5
    publicstaticfinalStringFACESHAPE_BROW_SIZE_X="faceshape_brow_size_x";// 眉毛长度, 取值范围0.0-1.0,默认值0.5
    publicstaticfinalStringFACESHAPE_BROW_ALL_Y="faceshape_brow_all_y";// 眉毛高度, 取值范围0.0-1.0,默认值0.5
    publicstaticfinalStringFACESHAPE_BROW_ALL_ROLL_Z="faceshape_brow_all_roll_z";// 眉毛旋转, 取值范围0.0-1.0,默认值0.5
    publicstaticfinalStringFACESHAPE_EYE_SIZE="faceshape_eye_size"; // 眼睛大小, 取值范围0.0-1.0,默认值0.5
    publicstaticfinalStringFACESHAPE_EYE_SIZE_Y="faceshape_eye_size_y";
    publicstaticfinalStringFACESHAPE_EYE_ROLL_Y="faceshape_eye_roll_y";// 眼睛高度, 取值范围0.0-1.0,默认值0.5
    publicstaticfinalStringFACESHAPE_EYE_ROLL_Z="faceshape_eye_roll_z";// 眼睛旋转, 取值范围0.0-1.0,默认值0.5
    publicstaticfinalStringFACESHAPE_EYE_X="faceshape_eye_x";// 双眼眼距, 取值范围0.0-1.0,默认值0.5
    publicstaticfinalStringFACESHAPE_NOSE_ALL_X="faceshape_nose_all_x";// 鼻子宽度, 取值范围0.0-1.0,默认值0.5
    publicstaticfinalStringFACESHAPE_NOSE_ALL_Y="faceshape_nose_all_y";// 鼻子高度, 取值范围0.0-1.0,默认值0.5
    publicstaticfinalStringFACESHAPE_NOSE_SIZE_Z="faceshape_nose_size_z";
    publicstaticfinalStringFACESHAPE_NOSE_ALL_ROLL_Y="faceshape_nose_all_roll_y";// 鼻头旋转, 取值范围0.0-1.0,默认值0.5
    publicstaticfinalStringFACESHAPE_NOSTRIL_ROLL_Y="faceshape_nostril_roll_y";// 鼻翼旋转, 取值范围0.0-1.0,默认值0.5
    publicstaticfinalStringFACESHAPE_NOSTRIL_X="faceshape_nostril_x";
    publicstaticfinalStringFACESHAPE_MOUTH_ALL_Y="faceshape_mouth_all_y";// 嘴巴上下, 取值范围0.0-1.0,默认值0.5
    publicstaticfinalStringFACESHAPE_LIP_ALL_SIZE_Y="faceshape_lip_all_size_y";// 嘴唇厚度, 取值范围0.0-1.0,默认值0.5
    publicstaticfinalStringFACESHAPE_LIPCORNER_Y="faceshape_lipcorner_y";// 嘴角旋转, 取值范围0.0-1.0,默认值0.5
    publicstaticfinalStringFACESHAPE_LIP_UPPER_SIZE_X="faceshape_lip_upper_size_x"; // 上唇宽度, 取值范围0.0-1.0,默认值0.5
    publicstaticfinalStringFACESHAPE_LIP_LOWER_SIZE_X="faceshape_lip_lower_size_x"; // 下唇宽度, 取值范围0.0-1.0,默认值0.5
    publicstaticfinalStringFACESHAPE_JAW_ALL_SIZE_X="faceshape_jaw_all_size_x";// 下巴宽度, 取值范围0.0-1.0,默认值0.5
    publicstaticfinalStringFACESHAPE_JAW_Y="faceshape_jaw_y";// 下巴高度, 取值范围0.0-1.0,默认值0.5
    publicstaticfinalStringFACESHAPE_CHEEK_ALL_SIZE_X="faceshape_cheek_all_size_x";// 脸颊宽度, 取值范围0.0-1.0,默认值0.5
​
    //其他函数略
}

可以看到,上面数据基本包含所有人脸属性了,基本具备了捏脸能力,篇幅原因,我们这里不具体去实现。有这方面需求的读者,可以通过在界面上调整上面相关属性来实现。

接下来我们开始创建虚拟形象,首先创建一个User实体类:

publicclassUser {
    publicStringuserName; //用户名
    publicStringuserId; //用户ID
    publicbooleanisMan; //性别
    publicintwidth; //预览宽度
    publicintheight; //预览高度
    publicintbgColor; //背景颜色
    publicintshirtIdx=0; // T-shirt资源id
    publicintbrowIdx=0; //眉毛资源id
​
    publicUser(StringuserName, StringuserId, intwidth, intheight) {
        this.userName=userName;
        this.userId=userId;
        this.width=width;
        this.height=height;
        this.isMan=true;
        bgColor=Color.argb(255, 33, 66, 99);
    }  
}

示例作用,为了简单起见,我们这里只针对眉毛和衣服资源做选取。接下来创建一个Activity:

publicclassAvatarActivityextendsBaseActivity  {
    privateintvWidth=720;
    privateintvHeight=1080; 
    privateUseruser=newUser("C_0001", "C_0001", vWidth, vHeight);
    privateTextureViewmTextureView;  //用于显示Avatar形象
    privateAvatarMngrmAvatarMngr; // 用于维护管理Avatar
    privateColorPickerDialogcolorPickerDialog; //用于背景色选取
​
    @Override
    protectedvoidonCreate(BundlesavedInstanceState) {
        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:

publicclassAvatarMngrimplementsZegoAvatarServiceDelegate, RTCMngr.CaptureListener {
    privatestaticfinalStringTAG="AvatarMngr";
    privatestaticAvatarMngrmInstance;
    privatebooleanmIsStop=false;
​
    privateUsermUser=null;
    privateTextureBgRendermBgRender=null;
    privateOnAvatarServiceInitSuccedlistener;
    privateZegoCharacterHelpermCharacterHelper;
    privateApplicationmApp;
​
    publicinterfaceOnAvatarServiceInitSucced {
        voidonInitSucced();
    }
​
    publicvoidsetLicense(Stringlicense, OnAvatarServiceInitSuccedlistener) {
        this.listener=listener;
        ZegoAvatarService.addServiceObserver(this);
        StringaiPath=FileUtils.getPhonePath(mApp, "AIModel.bundle", "assets"); //   AI 模型的绝对路径
        ZegoServiceConfigconfig=newZegoServiceConfig(license, aiPath);
        ZegoAvatarService.init(mApp, config);
    }
​
    publicvoidstop() {
        mIsStop=true;
        mUser=null;
        stopExpression();
    }
​
    publicvoidupdateUser(Useruser) {
        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
     */
    publicvoidstart(TextureViewavatarView, Useruser) {
        mUser=user;
        mIsStop=false;
        initAvatar(avatarView, user);
        startExpression();
    }
​
    privatevoidinitAvatar(TextureViewavatarView, Useruser) {
        Stringsex=ZegoCharacterHelper.MODEL_ID_MALE;
        if (!user.isMan) sex=ZegoCharacterHelper.MODEL_ID_FEMALE;
​
        // 创建 helper 简化调用
        // base.bundle 是头模, human.bundle 是全身人模
        mCharacterHelper=newZegoCharacterHelper(FileUtils.getPhonePath(mApp, "human.bundle", "assets"));
        mCharacterHelper.setExtendPackagePath(FileUtils.getPhonePath(mApp, "Packages", "assets"));
        // 设置形象配置
        mCharacterHelper.setDefaultAvatar(sex);
        updateUser(user);
        // 获取当前妆容数据, 可以保存到用户资料中
        Stringjson=mCharacterHelper.getAvatarJson();
​
    }
​
    // 启动表情检测
    privatevoidstartExpression() {
        // 启动表情检测前要申请摄像头权限, 这里是在 MainActivity 已经申请过了
        ZegoAvatarService.getInteractEngine().startDetectExpression(ZegoExpressionDetectMode.Camera, expression-> {
            // 表情直接塞给 avatar 驱动
            mCharacterHelper.setExpression(expression);
        });
    }
​
    // 停止表情检测
    privatevoidstopExpression() {
        // 不用的时候记得停止
        ZegoAvatarService.getInteractEngine().stopDetectExpression();
    }
​
    // 获取到 avatar 纹理后的处理
    publicvoidonCaptureAvatar(inttextureId, intwidth, intheight) {
        if (mIsStop||mUser==null) { // rtc 的 onStop 是异步的, 可能activity已经运行到onStop了, rtc还没
            return;
        }
        booleanuseFBO=true;
        if (mBgRender==null) {
            mBgRender=newTextureBgRender(textureId, useFBO, width, height, Texture2dProgram.ProgramType.TEXTURE_2D_BG);
        }
        mBgRender.setInputTexture(textureId);
        floatr=Color.red(mUser.bgColor) /255f;
        floatg=Color.green(mUser.bgColor) /255f;
        floatb=Color.blue(mUser.bgColor) /255f;
        floata=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
    publicvoidonStartCapture() {
        if (mUser==null) return;
//        // 收到回调后,开发者需要执行启动视频采集相关的业务逻辑,例如开启摄像头等
        AvatarCaptureConfigconfig=newAvatarCaptureConfig(mUser.width, mUser.height);
//        // 开始捕获纹理
        mCharacterHelper.startCaptureAvatar(config, this::onCaptureAvatar);
    }
​
    @Override
    publicvoidonStopCapture() {
        mCharacterHelper.stopCaptureAvatar();
        stopExpression();
    }
​
​
    privatevoidinitRes(Applicationapp) {
        // 先把资源拷贝到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
    publicvoidonError(ZegoAvatarErrorCodecode, Stringdesc) {
        Log.e(TAG, "errorcode : "+code.getErrorCode() +",desc : "+desc);
    }
​
    @Override
    publicvoidonStateChange(ZegoAvatarServiceStatestate) {
        if (state==ZegoAvatarServiceState.InitSucceed) {
            Log.i("ZegoAvatar", "Init success");
            // 要记得及时移除通知
            ZegoAvatarService.removeServiceObserver(this);
            if (listener!=null) listener.onInitSucced();
        }
    }
​
    privateAvatarMngr(Applicationapp) {
        mApp=app;
        initRes(app);
    }
​
    publicstaticAvatarMngrgetInstance(Applicationapp) {
        if (null==mInstance) {
            synchronized (AvatarMngr.class) {
                if (null==mInstance) {
                    mInstance=newAvatarMngr(app);
                }
            }
        }
        returnmInstance;
    }
}

以上代码完成了整个虚拟形象的创建,关键代码全部展示,如果还需要具体全部代码,直接从附件中下载即可。

5 附件


作者:RTC 程序猿Wang链接: https://juejin.cn/post/7176134976293830713
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值