元宇宙是人类利用数字技术构建的虚拟世界,被现实世界映射或超越,并能与现实世界互动,拥有一个新的社会体系的数字生活空间。
可见,超宇宙的第一步是创建专属虚拟形象,但创建3D虚拟形象需要3D基础知识。对于大部分安卓开发者(包括我自己)来说,没有这方面的积累。我们很难进入超宇宙的世界吗?不,今天有了即时平台提供的头像SDK,只要有安卓基础,就能进入最火热的超宇宙世界!先看效果:
上面的gif压缩的相当苛刻,所以这里是截图:
1免费注册:即时开发者去即时控制台网站注册开发者账号。注册成功后,创建一个项目:
你可以在控制台中得到AppID和AppSign两个数据,这是很重要的凭证,后面会用到。
因为我们使用的是内置的头像功能,但是目前官方并没有提供在线自动打开的方法,所以需要我们主动向客服申请(当然这个是免费的),提供自己项目的包名就可以打开头像权限。打开右下角的“联系我们”,点击即可申请免费获取客服。
注意,如果没有向客服申请头像权限,调用AvatarSDK会失败!
2准备开发环境,从元宇宙开发SDK官方网站下载SDK,得到如下文件列表:
下一个过程如下:
打开SDK目录,将其中的ZegoAvatar.aar复制到app/libs目录。添加SDK引用。打开app/build.gradle文件,导入所有jar和AAR:dependencies { implementation file tree(dir:' libs 'include: ['*。jar '' *。AAR '])//通配符导入//其他缩写}来设置权限。根据实际应用需求,设置应用所需的权限。进入app/src/main/Android manifest . 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 '/
用途-功能
Android:gles version='0x 00020000 '
android:required='true' /
uses-feature Android:name=' Android . hardware . camera '/
uses-feature Android:name=' Android . hardware . camera . auto focus '/由于Android 6.0要求在一些重要权限上必须申请动态权限,所以不能只通过AndroidMainfest.xml文件申请静态权限。具体的动态请求权限代码,请参考附后的源代码。
3导入资源在上一节下载的zip文件中,有一个资产目录。包含头像形象相关资源,比如衣服,眉毛,鞋子等等。这是政府免费提供的资源,可以满足一般需求。当然,如果你想自定义自己的资源,也可以。资产文件的内容如下:
资源名称
解释
AIModel.bundle
阿凡达的AI模型资源。使用表情跟随、语音跟随、AI捏脸等能力时。必须先将该资源的绝对路径设置为Avatar SDK。
基础包
美术资源,包括基础3D人物模型资源、资源映射表、人物模型默认外观等。
包装
美容化妆品、吊坠、装饰品等资源。每个资源从200 kb到1 MB不等,和资源的复杂程度有关。
注:由于资源文件较大,以上下载的美术资源仅包含少量必要的资源。如果需要所有资源,可以去官网问客服。
以上文件需要保存在Android本地的SDCard上。这里有两个方案供参考:
方案一:将资源先放入到应用程序/服务资源/资产目录内,然后在应用启动时,自动将资产的内容拷贝到SDcard中。方案二:将资源放入到服务器端,运行时自动从服务器端下载。为了简单起见,我们这里采用方案一。参考代码如下,详细代码可以看附件:
资产文件转移。copyassetsdir 2手机(这个。获取应用程序(),
AIModel.bundle '' assets ');
资产文件转移。copyassetsdir 2手机(这个。获取应用程序(),
base.bundle '' assets ');
资产文件转移。copyassetsdir 2手机(这个。获取应用程序(),
包''资产');四创建虚拟形象创建虚拟形象本质上来说就是调用即构的阿凡达SDK,其大致流程如下:
接下来我们逐步实现上面流程。
需要注意的是,上面示意图中采用的是全景图,可以非常方便的直接展示电影《阿凡达》形象,但是不方便后期将画面通过雷达跟踪中心(雷达跟踪中心的缩写)实时传递,因此,我们后面的具体实现视通过TextureView替代全景图。
4.1 申请权鉴这里再次强调一下,一定要打开点击右下角有\”联系我们\”,向客服申请免费开通电影《阿凡达》权限。否则无法使用阿凡达SDK。
申请权鉴代码如下:
公共类密钥中心{
//控制台地址:https://console . zego . im/仪表板
公共静态长APP_ID=这里值可以在控制台查询,参考第一节;//这里填写APPID
公共静态字符串APP_SIGN=这里值可以在控制台查询,参考第一节;
//鉴权服务器的地址
公开决赛静态字符串后端_ API _ URL=' https://ai效果-API。泽戈。im?action=DescribeAvatarLicense '
公共静态字符串avatarLicense=null
公共静态字符串getURL(字符串authInfo) {
尤里builder builder=uri。parse(后端API URL).构建于();
建筑商。appendqueryparameter(' AppId '字符串。(APP _ ID)的值;
建筑商。appendqueryparameter(' AuthInfo 'AuthInfo);
返回builder.build().toString();
}
公共接口IGetLicenseCallback {
void onGetLicense(int代码,字符串消息,ZegoLicense许可证);
}
/**
* 在线拉取许可证
* @param上下文
* @param回调
*/
公共静态void get许可证(上下文Context,final IGetLicenseCallback回调){
请求许可(zegoavatarservice。getauthinfo(APP _ SIGN,context),回调);
}
/**
* 获取许可证
* */
公共静态void请求许可证(字符串authInfo,最终IGetLicenseCallback回调){
string URL=getURL(authInfo);
HttpRequest.asyncGet(url,ZegoLicense.class,(代码,消息,responseJsonBean) – {
如果(回调!=null) {
callback.onGetLicense(代码、消息、响应JSON bean);
}
});
}
公共类ZegoLicense
@SerializedName('许可证)
私有字符串许可证;
公共字符串getLicense() {
归还许可证;
}
公共作废setLicense(字符串许可证){
this.license=许可证;
}
}
}在获取林肯斯时,只需调用获取许可证函数,例如在活动类中只需如下调用:
KeyCenter.getLicense(this,(代码,消息,响应)- {
if (code==0) {
钥匙中心。头像授权=响应。获取许可证();
showLoading('正在初始化.');
avatarMngr=avatarMngr。getinstance(get application());
avatarmngr。设置许可证(密钥中心。头像执照,这个);
}否则{
土司('执照获取失败,代码:'代码);
}
});4.2 初始化AvatarService初始化AvatarService过程比较漫长(可能要几秒),通过开启工人线程后台加载以避免主线程阻塞。因此我们定义一个回调函数,待完成初始化后回调通知:
公共接口OnAvatarServiceInitSucced {
void onInitSucced();
}
公共void setLicense(字符串许可证,OnAvatarServiceInitSucced侦听器){
this.listener=监听器
zegoavatarservice。addserviceobserver(this);
string ai path=file utils。getphonepath(mApp,' AIModel.bundle '' assets ');//AI模型的绝对路径
ZegoServiceConfig config=new ZegoServiceConfig(许可证,ai路径);
ZegoAvatarService.init(mApp,config);
}
@覆盖
公共void onStateChange(ZegoAvatarServiceState状态){
if(state==ZegoAvatarServiceState .InitSucceed) {
Log.i('ZegoAvatar '' Init success ');
//要记得及时移除通知
zegoavatarservice。removeserviceobserver(this);
如果(听者!=null)侦听器。oninitsucced();
}
}这里设置许可证函数内完成初始化AvatarService,初始化完成后会回调onStateChange函数。但是要注意,在初始化之前必须把资源文件拷贝到本地南达科他州卡,即完成资源导入:
私有void initRes(应用程序){
//先把资源拷贝到南达科他州卡,注意:线上使用时,需要做一下判断,避免多次拷贝。资源也可以做成从网络下载。
如果(!FileUtils.checkFile(app,' AIModel.bundle '' assets '))
fileutils。copyassetsdir 2手机(app,' AIModel.bundle '' assets ');
如果(!FileUtils.checkFile(app,' base.bundle '' assets '))
文件实用程序。copyassetsdir 2手机(app,' base.bundle '' assets ');
如果(!FileUtils.checkFile(app,' human.bundle '' assets '))
fileutils。copyassetsdir 2手机(app,' human.bundle '' assets ');
如果(!FileUtils.checkFile(应用程序、\”包\”、\”资产\”))
fileutils。copyassetsdir 2手机(应用程序,包''资产');
}4.3 创建虚拟形象前面在下载软件开发工具包(软件开发工具包)包含了助手目录,这个目录里面有非常重要的两个文件:
其中ZegoCharacterHelper文件是个接口定义类,即虽然是个类,但具体的实现全部在ZegoCharacterHelperImpl中。我们先一睹为快,看看ZegoCharacterHelper包含了哪些可处理的属性:
公共类ZegoCharacterHelper {
公共静态最终字符串MODEL _ ID _ MALE='男性
public static final String MODEL _ ID _ FEMALE=' FEMALE '
//****************************** 捏脸维度的键值******************************/
公共静态最终字符串脸型_眉毛_大小_ Y='脸型_眉毛_大小_ Y '//眉毛厚度,取值范围0.0-1.0,默认值0.5
公共静态最终字符串脸型_眉毛_大小_ X='脸型_眉毛_大小_ X '//眉毛长度,取值范围0.0-1.0,默认值0.5
public static final String face shape _ BROW _ ALL _ Y=' face shape _ BROW _ ALL _ Y '//眉毛高度,取值范围0.0-1.0,默认值0.5
public static final String face shape _ BROW _ ALL _ ROLL _ Z=' face shape _ BROW _ ALL _ ROLL _ Z '//眉毛旋转,取值范围0.0-1.0,默认值0.5
公共静态最终字符串脸型_眼睛_大小='脸型_眼睛_大小'//眼睛大小,取值范围0.0-1.0,默认值0.5
公共静态最终字符串脸型_眼睛_大小_ Y='脸型_眼睛_大小_ Y '
公共静态最终字符串脸型_眼_卷_ Y='脸型_眼_卷_ Y '//眼睛高度,取值范围0.0-1.0,默认值0.5
公共静态最终字符串脸型_眼_卷_ Z='脸型_眼_卷_ Z '//眼睛旋转,取值范围0.0-1.0,默认值0.5
公共静态最终字符串脸型_眼睛_ X='脸型_眼睛_ X '//双眼眼距,取值范围0.0-1.0,默认值0.5
公共静态最终字符串脸型_鼻子_所有_ X='脸型_鼻子_所有_ X '//鼻子宽度,取值范围0.0-1.0,默认值0.5
公共静态最终字符串脸型_鼻子_所有_ Y='脸型_鼻子_所有_ Y '//鼻子高度,取值范围0.0-1.0,默认值0.5
公共静态最终字符串脸型_鼻子_大小_ Z='脸型_鼻子_大小_ Z '
public static final String face shape _ NOSE _ ALL _ ROLL _ Y=' face shape _ NOSE _ ALL _ ROLL _ Y '//鼻头旋转,取值范围0.0-1.0,默认值0.5
public static final String face shape _ narrow _ ROLL _ Y=' face shape _ narrow _ ROLL _ Y '//鼻翼旋转,取值范围0.0-1.0,默认值0.5
公共静态最终字符串脸型_鼻孔_ X=' faceshape _鼻孔_ X '
public static final String face shape _ MOUTH _ ALL _ Y=' face shape _ MOUTH _ ALL _ Y '//嘴巴上下,取值范围0.0-1.0,默认值0.5
公共静态最终字符串face shape _ LIP _ ALL _ SIZE _ Y=' face shape _ LIP _ ALL _ SIZE _ Y '//嘴唇厚度,取值范围0.0-1.0,默认值0.5
公共静态最终字符串脸型_唇角_ Y='脸型_唇角_ Y '//嘴角旋转,取值范围0.0-1.0,默认值0.5
公共静态最终字符串face shape _ LIP _ UPPER _ SIZE _ X=' face shape _ LIP _ UPPER _ SIZE _ X '//上唇宽度,取值范围0.0-1.0,默认值0.5
公共静态最终字符串face shape _ LIP _ LOWER _ SIZE _ X=' face shape _ LIP _ LOWER _ SIZE _ X '//下唇宽度,取值范围0.0-1.0,默认值0.5
公共静态最终字符串脸型_下巴_所有_尺寸_ X='脸型_下巴_所有_尺寸_ X '//下巴宽度,取值范围0.0-1.0,默认值0.5
公共静态最终字符串脸型_下巴_ Y='脸型_下巴_ Y '//下巴高度,取值范围0.0-1.0,默认值0.5
公共静态最终字符串脸型_脸颊_全部_大小_ X='脸型_脸颊_全部_大小_ X '//脸颊宽度,取值范围0.0-1.0,默认值0.5
//其他函数略
}可以看到,上面数据基本包含所有人脸属性了,基本具备了捏脸能力,篇幅原因,我们这里不具体去实现。有这方面需求的读者,可以通过在界面上调整上面相关属性来实现。
接下来我们开始创建虚拟形象,首先创建一个用户实体类:
公共类用户{
公共字符串用户名;//用户名
公共字符串userId//用户身份证明
公共布尔值isMan//性别
公共(同Internationalorganizations)国际组织宽度;//预览宽度
公共(同Internationalorganizations)国际组织高度;//预览高度
public int bgColor//背景颜色
public int shirtIdx=0;//T恤资源身份证明(识别)
public int浏览器idx=0;//眉毛资源身份证明(识别)
公共用户(字符串用户名,字符串用户Id,整数宽度,整数高度){
this .用户名=用户名;
this.userId=用户标识
this.width=宽度;
this.height=高度;
this.isMan=true
bgColor=Color.argb(255,33,66,99);
}
}示例作用,为了简单起见,我们这里只针对眉毛和衣服资源做选取。接下来创建一个活动:
公共类可用性活动扩展基础活动{
private int vWidth=720
私有int vHeight=1080
私有用户用户=新用户(' C_0001 '' C_0001 'vWidth,vHeight);
私有纹理查看mtextreview//用于显示电影《阿凡达》形象
private AvatarMngr mAvatarMngr//用于维护管理电影《阿凡达》
private ColorPickerDialog ColorPickerDialog;//用于背景色选取
@覆盖
受保护的void onCreate(Bundle saved instancestate){
超级棒。oncreate(savedInstanceState);
setContentView(r . layout。活动_头像);
//.
//其他初始化界面相关代码略.
//.
user.isMan=true
mtextreview=findViewById(r . id。头像_查看);
mZegoMngr=zegomngr。getinstance(get application());
//开启虚拟形象预览
mavatarmngr。开始(mtextreview,用户);
}最后一行代码中开启了虚拟形象预览,那么这里开启虚拟形象预览具体做了哪些工作呢?给我看看代码:
公共类AvatarMngr实现ZegoAvatarServiceDelegate .捕获侦听器{
私有静态最终字符串标记=' AvatarMngr
私有静态AvatarMngr实例;
私有布尔型失误=错误
私有用户mUser=null
私有纹理bgrender mbg render=null
私有OnAvatarServiceInitSucced侦听器;
private ZegoCharacterHelper mCharacterHelper;
私有应用mApp
公共接口OnAvatarServiceInitSucced {
void onInitSucced();
}
公共void setLicense(字符串许可证,OnAvatarServiceInitSucced侦听器){
this.listener=监听器
zegoavatarservice。addserviceobserver(this);
string ai path=file utils。getphonepath(mApp,' AIModel.bundle '' assets ');//AI模型的绝对路径
ZegoServiceConfig config=new ZegoServiceConfig(许可证,ai路径);
ZegoAvatarService.init(mApp,config);
}
公共无效站点(){
mIsStop=true
mUser=null
停止表达式();
}
公共void updateUser(用户用户){
mUser=用户;
if (user.shirtIdx==0) {
mcharacterhelper。设置包(' m-shirt 01 ');
}否则{
mcharacterhelper。套装(' m-shirt 02 ');
}
if (user.browIdx==0) {
mcharacterhelper。设置包(' brows _ 1 ');
}否则{
mcharacterhelper。设置包(' brows _ 2 ');
}
}
/**
* 启动阿凡达,调用此函数之前,请确保已经调用过设置许可证
*
* @param avatarView
*/
公共void start(纹理视图头像视图,用户用户){
mUser=用户;
失误=错误
initAvatar(avatarView,user);
开始表达式();
}
私有void init avatar(纹理视图avatar视图,用户user) {
String sex=ZegoCharacterHelper .型号_ ID _男性
如果(!用户。isman)sex=ZegoCharacterHelper .型号_ ID _女性;
//创建助手简化调用
//base.bundle是头模,human.bundle是全身人模
mCharacterHelper=new ZegoCharacterHelper(file utils。getphonepath(mApp,' human.bundle '' assets ');
mcharacterhelper。setextendpackagepath(文件实用程序。getphonepath(mApp,' Packages '' assets ');
//设置形象配置
mcharacterhelper。setdefaultavatar(性);
更新用户(用户);
//获取当前妆容数据,可以保存到用户资料中
string JSON=mcharacterhelper。getavatarjson();
}
//启动表情检测
私有void startExpression() {
//启动表情检测前要申请摄像头权限,这里是在主要活动已经申请过了
zegoavatarservice。getinteractengine().startDetectExpression(ZegoExpressionDetectMode .相机,表情- {
//表情直接塞给阿凡达驱动
mCharacterHelper.setExpression(表达式);
});
}
//停止表情检测
私有void stopExpression() {
//不用的时候记得停止
zegoavatarservice。getinteractengine().stopdetexpression();
}
//获取到阿凡达纹理后的处理
capturevatar上的公共void(int texture id,int width,int height) {
if (mIsStop || mUser==null) { //rtc的onStop是异步的,可能活动已经运行到onStop了,rtc还没
返回;
}
布尔函数useFBO=true
if (mBgRender==null) {
mbg render=new texture bgrender(texture id,useFBO,width,height,Texture2dProgram .程序类型。纹理_ 2D _ BG);
}
mbg渲染。设置输入纹理(纹理id);
浮动r=颜色。红色(muser。bgcolor)/255 f;
浮动g=颜色。绿色(muser。bgcolor)/255 f;
浮动b=颜色。蓝色(muser。bgcolor)/255 f;
浮动a=颜色。阿尔法(muser。bgcolor)/255 f;
mBgRender.setBgColor(r,g,b,a);
mbgrender。draw(use fbo);//画到fbo上需要反向的
ZegoExpressEngine.getEngine().sendCustomVideoCaptureTextureData(mbg render。getoutputtextureid()、宽度、高度、系统。当前时间毫秒());
}
@覆盖
public void onStartCapture() {
if (mUser==null)返回;
////收到回调后,开发者需要执行启动视频采集相关的业务逻辑,例如开启摄像头等
AvatarCaptureConfig=新的AvatarCaptureConfig(muser。宽度,缪瑟。身高);
////开始捕获纹理
mcharacterhelper。startcaptureavatar(config,this:onCaptureAvatar);
}
@覆盖
public void onStopCapture() {
mcharacterhelper。stopcaptureavatar();
停止表达式();
}
私有void initRes(应用程序){
//先把资源拷贝到南达科他州卡,注意:线上使用时,需要做一下判断,避免多次拷贝。资源也可以做成从网络下载。
如果(!FileUtils.checkFile(app,' AIModel.bundle '' assets '))
fileutils。copyassetsdir 2手机(app,' AIModel.bundle '' assets ');
如果(!FileUtils.checkFile(app,' base.bundle '' assets '))
文件实用程序。copyassetsdir 2手机(app,' base.bundle '' assets ');
如果(!FileUtils.checkFile(app,' human.bundle '' assets '))
fileutils。copyassetsdir 2手机(app,' human.bundle '' assets ');
如果(!FileUtils.checkFile(应用程序、\”包\”、\”资产\”))
fileutils。copyassetsdir 2手机(应用程序,包''资产');
}
@覆盖
公共void on error(ZegoAvatarErrorCode,字符串desc) {
Log.e(标签,' errorcode : ' code.getErrorCode()'desc:' desc ');
}
@覆盖
公共void onStateChange(ZegoAvatarServiceState状态){
if(state==ZegoAvatarServiceState .InitSucceed) {
Log.i('ZegoAvatar '' Init success ');
//要记得及时移除通知
zegoavatarservice。removeserviceobserver(this);
如果(听者!=null)侦听器。oninitsucced();
}
}
私有AvatarMngr(应用程序){
mApp=app
initRes(app);
}
公共静态AvatarMngr getInstance(应用程序app) {
if(null==最小实例){
同步(AvatarMngr.class) {
if(null==最小实例){
min instance=new AvatarMngr(app);
}
}
}
回报;
}
}
以上代码完成了整个虚拟形象的创建,关键代码全部展示。