Skip to content
This repository has been archived by the owner on Aug 21, 2024. It is now read-only.

Commit

Permalink
Add Avatarurn.me Avatar Selector API (#9307)
Browse files Browse the repository at this point in the history
* integrate API , UI and demo link for avaturn

* simplify menu functions

* update env.default file

* fix asset type being missing for avaturn urls

* improve and fix it

* retargeter a/t pose conditional instead of avatar provider guess

---------

Co-authored-by: sybiote <ghoshr698@gmail.com>
Co-authored-by: AidanCaruso <jamesajcme@gmail.com>
Co-authored-by: lonedevr <102248647+AidanCaruso@users.noreply.github.com>
  • Loading branch information
4 people authored Nov 22, 2023
1 parent 14e1c63 commit ee5e6e2
Show file tree
Hide file tree
Showing 10 changed files with 144 additions and 28 deletions.
2 changes: 2 additions & 0 deletions .env.local.default
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ VITE_EMAILJS_TEMPLATE_ID=
VITE_EMAILJS_USER_ID=
VITE_ROOT_REDIRECT=false
VITE_READY_PLAYER_ME_URL=https://xre.readyplayer.me
VITE_AVATURN_URL="https://demo.avaturn.dev" #using public one
VITE_AVATURN_API=https://api.avaturn.me/
VITE_PWA_ENABLED=false
# CHAPI Mediator URI
VITE_MEDIATOR_SERVER=https://authn.io
Expand Down
4 changes: 3 additions & 1 deletion packages/client-core/i18n/en/user.json
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@
"phoneError": "Invalid phone #",
"emailError": "Invalid email address",
"useReadyPlayerMe": "Use ReadyPlayer.Me",
"useAvaturn": "Use Avaturn",
"loginWithXRWallet": "Login with XR Wallet",
"issueVC": "Issue a VC",
"requestVC": "Request a VC",
Expand Down Expand Up @@ -349,7 +350,8 @@
"upload-success-msg": "Avatar Uploaded Successfully.",
"remove-success-msg": "Avatar Uploaded Successfully.",
"warning-msg": "Avatar resource is empty, have you synced avatars to your static file storage?",
"loadingRPM": "Loading ReadyPlayer.Me",
"loadingReadyPlayerMe": "Loading ReadyPlayer.Me",
"loadingAvaturn": "Loading Avaturn",
"downloading": "Downloading Avatar",
"loadingPreview": "Loading Avatar Preview",
"uploading": "Uploading Avatar",
Expand Down
7 changes: 5 additions & 2 deletions packages/client-core/src/user/UserUISystem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ import { PresentationSystemGroup } from '@etherealengine/engine/src/ecs/function
import { useTranslation } from 'react-i18next'
import { InviteService } from '../social/services/InviteService'
import { PopupMenuState } from './components/UserMenu/PopupMenuService'
import AvatarCreatorMenu, { SupportedSdks } from './components/UserMenu/menus/AvatarCreatorMenu'
import AvatarModifyMenu from './components/UserMenu/menus/AvatarModifyMenu'
import AvatarSelectMenu from './components/UserMenu/menus/AvatarSelectMenu'
import EmoteMenu from './components/UserMenu/menus/EmoteMenu'
import ProfileMenu from './components/UserMenu/menus/ProfileMenu'
import ReadyPlayerMenu from './components/UserMenu/menus/ReadyPlayerMenu'
import SettingMenu from './components/UserMenu/menus/SettingMenu'
import ShareMenu from './components/UserMenu/menus/ShareMenu'

Expand All @@ -59,6 +59,7 @@ export const UserMenus = {
Profile: 'user.Profile',
Settings: 'user.Settings',
ReadyPlayer: 'user.ReadyPlayer',
Avaturn: 'user.Avaturn',
AvatarSelect: 'user.AvatarSelect',
AvatarModify: 'user.AvatarModify',
Share: 'user.Share',
Expand All @@ -77,7 +78,8 @@ const reactor = () => {
[UserMenus.Settings]: SettingMenu,
[UserMenus.AvatarSelect]: AvatarSelectMenu,
[UserMenus.AvatarModify]: AvatarModifyMenu,
[UserMenus.ReadyPlayer]: ReadyPlayerMenu,
[UserMenus.ReadyPlayer]: AvatarCreatorMenu(SupportedSdks.ReadyPlayerMe),
[UserMenus.Avaturn]: AvatarCreatorMenu(SupportedSdks.Avaturn),
[UserMenus.Share]: ShareMenu,
[UserMenus.Emote]: EmoteMenu
})
Expand All @@ -95,6 +97,7 @@ const reactor = () => {
[UserMenus.AvatarSelect]: none,
[UserMenus.AvatarModify]: none,
[UserMenus.ReadyPlayer]: none,
[UserMenus.Avaturn]: none,
[UserMenus.Share]: none,
[UserMenus.Emote]: none
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,25 +39,33 @@ import Icon from '@etherealengine/ui/src/primitives/mui/Icon'
import IconButton from '@etherealengine/ui/src/primitives/mui/IconButton'

import { AVATAR_ID_REGEX, generateAvatarId } from '../../../../util/avatarIdFunctions'
import { AvatarService } from '../../../services/AvatarService'
import { UserMenus } from '../../../UserUISystem'
import styles from '../index.module.scss'

import { AssetType } from '@etherealengine/engine/src/assets/enum/AssetType'
import { isAvaturn } from '@etherealengine/engine/src/avatar/functions/avatarFunctions'
import { AvatarService } from '../../../services/AvatarService'
import { PopupMenuServices } from '../PopupMenuService'
import styles from '../index.module.scss'

enum LoadingState {
None,
LoadingRPM,
LoadingCreator,
Downloading,
LoadingPreview,
Uploading
}

const ReadyPlayerMenu = () => {
export const SupportedSdks = {
Avaturn: 'Avaturn',
ReadyPlayerMe: 'ReadyPlayerMe'
}

const AvatarCreatorMenu = (selectedSdk: string) => () => {
const { t } = useTranslation()
const [selectedBlob, setSelectedBlob] = useState<Blob>()
const [avatarName, setAvatarName] = useState('')
const [avatarUrl, setAvatarUrl] = useState('')
const [loading, setLoading] = useState(LoadingState.LoadingRPM)
const [loading, setLoading] = useState(LoadingState.LoadingCreator)
const [error, setError] = useState('')

useEffect(() => {
Expand All @@ -67,9 +75,18 @@ const ReadyPlayerMenu = () => {
}
}, [avatarUrl])

const handleMessageEvent = async (event) => {
const url = event.data
const getSdkUrl = () => {
switch (selectedSdk) {
case SupportedSdks.Avaturn:
return config.client.avaturnUrl
case SupportedSdks.ReadyPlayerMe:
default:
return config.client.readyPlayerMeUrl
}
}

const handleReadyPlayerMeMessageEvent = async (event) => {
const url = event.data
const avatarIdRegexExec = AVATAR_ID_REGEX.exec(url)

if (url && url.toString().toLowerCase().startsWith('http')) {
Expand All @@ -78,7 +95,7 @@ const ReadyPlayerMenu = () => {
setAvatarName(avatarIdRegexExec ? avatarIdRegexExec[1] : generateAvatarId())

try {
const assetType = AssetLoader.getAssetType(url)
const assetType = AssetLoader.getAssetType(url) ?? isAvaturn(url) ? AssetType.glB : null
if (assetType) {
const res = await fetch(url)
const data = await res.blob()
Expand All @@ -95,6 +112,53 @@ const ReadyPlayerMenu = () => {
}
}

const handleAvaturnMessageEvent = async (event) => {
const response = event.data
let json
try {
json = JSON.parse(response)
} catch (error) {
console.log('Error parsing the event data.')
return
}

if (json.source !== 'avaturn') return // always check the source its always 'avaturn'

// Get avatar GLB URL
if (json.eventName === 'v2.avatar.exported') {
const url = json.data.url
const avatarIdRegexExec = AVATAR_ID_REGEX.exec(url)
if (url && url.toString().toLowerCase().startsWith('http')) {
setLoading(LoadingState.Downloading)
setError('')
setAvatarName(avatarIdRegexExec ? avatarIdRegexExec[1] : generateAvatarId())

try {
const res = await fetch(url)
const data = await res.blob()
setLoading(LoadingState.LoadingPreview)
setAvatarUrl(url)
setSelectedBlob(data)
} catch (error) {
console.error(error)
setError(t('user:usermenu.avatar.selectValidFile'))
setLoading(LoadingState.None)
}
}
}
}

const handleMessageEvent = async (event) => {
switch (selectedSdk) {
case SupportedSdks.Avaturn:
handleAvaturnMessageEvent(event)
break
case SupportedSdks.ReadyPlayerMe:
default:
handleReadyPlayerMeMessageEvent(event)
}
}

const handleNameChange = (e) => {
const { value } = e.target

Expand All @@ -118,7 +182,9 @@ const ReadyPlayerMenu = () => {
newContext?.drawImage(avatarCanvas, 0, 0)

const thumbnailName = avatarUrl.substring(0, avatarUrl.lastIndexOf('.')) + '.png'
const modelName = avatarUrl.substring(0, avatarUrl.lastIndexOf('.')) + '.glb'
const modelName = !isAvaturn(avatarUrl)
? avatarUrl.substring(0, avatarUrl.lastIndexOf('.')) + '.glb'
: avatarUrl.split('/').pop() + '.glb'

const blob = await getCanvasBlob(canvas)

Expand All @@ -138,15 +204,15 @@ const ReadyPlayerMenu = () => {
return (
<Menu
open
maxWidth={loading === LoadingState.LoadingRPM ? 'sm' : 'xs'}
maxWidth={loading === LoadingState.LoadingCreator ? 'sm' : 'xs'}
showBackButton={avatarPreviewLoaded ? true : false}
title={avatarPreviewLoaded ? t('user:avatar.titleSelectThumbnail') : undefined}
onBack={() => PopupMenuServices.showPopupMenu(UserMenus.Profile)}
onClose={() => PopupMenuServices.showPopupMenu()}
>
<Box
className={styles.menuContent}
sx={{ minHeight: loading === LoadingState.LoadingRPM ? '450px !important' : '370px !important' }}
sx={{ minHeight: loading === LoadingState.LoadingCreator ? '450px !important' : '370px !important' }}
>
{loading !== LoadingState.None && (
<LoadingView
Expand All @@ -159,12 +225,12 @@ const ReadyPlayerMenu = () => {
? t('user:avatar.loadingPreview')
: loading === LoadingState.Uploading
? t('user:avatar.uploading')
: t('user:avatar.loadingRPM')
: t(`user:avatar.loading${selectedSdk}`)
}
/>
)}

{loading === LoadingState.LoadingRPM && (
{loading === LoadingState.LoadingCreator && (
<iframe
style={{
position: 'absolute',
Expand All @@ -175,7 +241,7 @@ const ReadyPlayerMenu = () => {
maxWidth: '100%',
border: 0
}}
src={config.client.readyPlayerMeUrl}
src={getSdkUrl()}
/>
)}

Expand All @@ -190,7 +256,7 @@ const ReadyPlayerMenu = () => {
/>
)}

{loading !== LoadingState.LoadingRPM && (
{loading !== LoadingState.LoadingCreator && (
<Box padding="10px 0">
<AvatarPreview
avatarUrl={avatarUrl}
Expand All @@ -211,4 +277,4 @@ const ReadyPlayerMenu = () => {
)
}

export default ReadyPlayerMenu
export default AvatarCreatorMenu
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,15 @@ const AvatarModifyMenu = ({ selectedAvatar }: Props) => {
{t('user:usermenu.profile.useReadyPlayerMe')}
</Button>

<Button
fullWidth
type="gradientRounded"
sx={{ mt: 1 }}
onClick={() => PopupMenuServices.showPopupMenu(UserMenus.Avaturn)}
>
{t('user:usermenu.profile.useAvaturn')}
</Button>

<InputText
name="name"
label={t('user:avatar.name')}
Expand Down
2 changes: 2 additions & 0 deletions packages/common/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ const client = {
rootRedirect: globalThis.process.env['VITE_ROOT_REDIRECT'],
tosAddress: globalThis.process.env['VITE_TERMS_OF_SERVICE_ADDRESS'],
readyPlayerMeUrl: globalThis.process.env['VITE_READY_PLAYER_ME_URL'],
avaturnUrl: globalThis.process.env['VITE_AVATURN_URL'],
avaturnAPI: globalThis.process.env['VITE_AVATURN_API'],
key8thWall: globalThis.process.env['VITE_8TH_WALL']!,
featherStoreKey: globalThis.process.env['VITE_FEATHERS_STORE_KEY'],
gaMeasurementId: globalThis.process.env['VITE_GA_MEASUREMENT_ID']
Expand Down
14 changes: 10 additions & 4 deletions packages/engine/src/assets/classes/AssetLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,15 +369,16 @@ const load = (
args: LoadingArgs,
onLoad = (response: any) => {},
onProgress = (request: ProgressEvent) => {},
onError = (event: ErrorEvent | Error) => {}
onError = (event: ErrorEvent | Error) => {},
assetTypeOverride: AssetType = null!
) => {
if (!_url) {
onError(new Error('URL is empty'))
return
}
const url = getAbsolutePath(_url)

const assetType = AssetLoader.getAssetType(url)
const assetType = assetTypeOverride ? assetTypeOverride : AssetLoader.getAssetType(url)
const loader = getLoader(assetType)

const callback = assetLoadCallback(url, args, assetType, onLoad)
Expand All @@ -389,9 +390,14 @@ const load = (
}
}

const loadAsync = async (url: string, args: LoadingArgs = {}, onProgress = (request: ProgressEvent) => {}) => {
const loadAsync = async (
url: string,
args: LoadingArgs = {},
onProgress = (request: ProgressEvent) => {},
assetTypeOverride: AssetType = null!
) => {
return new Promise<any>((resolve, reject) => {
load(url, args, resolve, onProgress, reject)
load(url, args, resolve, onProgress, reject, assetTypeOverride)
})
}

Expand Down
14 changes: 13 additions & 1 deletion packages/engine/src/avatar/AvatarBoneMatching.ts
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,16 @@ export const recursiveHipsLookup = (model: Object3D) => {
}
}

const _dir = new Vector3()
export function getAPose(rightHand: Vector3, _rightUpperArmPos: Vector3): boolean {
//get direction of right arm
_dir.subVectors(rightHand, _rightUpperArmPos).normalize()
const angle = _dir.angleTo(new Vector3(0, 1, 0))
return angle > 2
}

const _rightHandPos = new Vector3(),
_rightUpperArmPos = new Vector3()
export default function avatarBoneMatching(model: Object3D): VRM {
const bones = {} as VRMHumanBones
//use hips name as a standard to determine what to do with the mixamo prefix
Expand Down Expand Up @@ -705,8 +715,10 @@ export default function avatarBoneMatching(model: Object3D): VRM {
meta: { name: model.children[0].name } as VRM1Meta
} as VRMParameters)

humanoid.humanBones.rightHand.node.getWorldPosition(_rightHandPos)
humanoid.humanBones.rightUpperArm.node.getWorldPosition(_rightUpperArmPos)
//quick dirty tag to disable flipping on mixamo rigs
;(vrm as any).userData = { flipped: false, needsMixamoPrefix: needsMixamoPrefix } as any
;(vrm as any).userData = { flipped: false, useAPose: getAPose(_rightHandPos, _rightUpperArmPos) } as any
return vrm
}

Expand Down
Loading

0 comments on commit ee5e6e2

Please sign in to comment.