Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c3f27a0
feat(avatar): port facial expression data layer
alejandro-jimenez-dcl Apr 28, 2026
a04ddb6
feat(avatar): port AvatarFacialExpressionSystem (lean orchestrator)
alejandro-jimenez-dcl Apr 28, 2026
7dea39d
feat(protocol): regen Comms with FacialExpression message
alejandro-jimenez-dcl May 5, 2026
9cf919b
feat(facial-expression): add bus + send/receive systems
alejandro-jimenez-dcl May 5, 2026
c3df097
feat(input): add FaceExpressions action map (Y + 1-9)
alejandro-jimenez-dcl May 5, 2026
5bbe089
feat(avatar): add UpdateFaceExpressionInputSystem
alejandro-jimenez-dcl May 5, 2026
8809b3e
feat(avatar): wire facial expression plugins + DI
alejandro-jimenez-dcl May 5, 2026
66920c7
Updates textures
RominaMarchetti May 11, 2026
21766f3
Update EmoteSlot.prefab
RominaMarchetti May 11, 2026
5f71133
Update EmotesWheelHUD.prefab
RominaMarchetti May 11, 2026
dec4745
feat(avatar): facial expressions wheel UI
alejandro-jimenez-dcl May 11, 2026
4417374
feat(avatar): extract face shortcut handler + gate input map
alejandro-jimenez-dcl May 11, 2026
061ecf0
refactor(avatar): tighten facial expression seams
alejandro-jimenez-dcl May 11, 2026
ab727fc
feat(avatar): wheel HUD prefab + plugin wiring
alejandro-jimenez-dcl May 11, 2026
f4abee5
Update textures
RominaMarchetti May 11, 2026
26f324d
feat(avatar): emote wheel face tab swap
alejandro-jimenez-dcl May 12, 2026
67e0ce5
feat(avatar): gate facial expressions on _expressions atlas
alejandro-jimenez-dcl May 12, 2026
b2e732f
feat(avatar): drive facial expression slicing via _ExpressionIndex
alejandro-jimenez-dcl May 12, 2026
3907633
fix(avatar): drive _ExpressionIndex via material write, plug remainin…
alejandro-jimenez-dcl May 12, 2026
afeec13
fix: use new facial expression shader
alejandro-jimenez-dcl May 12, 2026
d9eef4e
Merge remote-tracking branch 'origin/dev' into feat/avatar-facial-exp…
alejandro-jimenez-dcl May 12, 2026
8427f15
fix(avatar): keep facial atlas in sync across wearable swaps
alejandro-jimenez-dcl May 12, 2026
95cf172
refactor(avatar): track face material refs to restore IsDirty gating
alejandro-jimenez-dcl May 12, 2026
088bf8f
@
alejandro-jimenez-dcl May 14, 2026
9af1533
Update facial expressions prefabs
RominaMarchetti May 14, 2026
0055be7
Fix slots animation
RominaMarchetti May 14, 2026
310ad29
Set to 95 black backgrounds with transparency
RominaMarchetti May 14, 2026
187780c
Quit close button scale animation on both wheels
RominaMarchetti May 14, 2026
4d752e8
Move textures folder to the feature folder one
RominaMarchetti May 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ MonoBehaviour:
m_DefaultGroup: 47f803e1e9c5079449bd106df98a0b7d
m_currentHash:
serializedVersion: 2
Hash: 8173ea4fb741d0d3cbbc7d6ca2d76db7
Hash: 69405828b03b7e3bfc18552c607113d2
m_OptimizeCatalogSize: 0
m_BuildRemoteCatalog: 0
m_CatalogRequestsTimeout: 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ MonoBehaviour:
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 69832a4fb177e7645af56a1f27d0dce2
m_Address: Assets/DCL/PluginSystem/Global/AvatarFaceAnimationSettings.asset
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 6a252383173104e17acac54eb364afc4
m_Address: Assets/DCL/AvatarRendering/AvatarShape/Assets/PointAtMarker.prefab
m_ReadOnly: 0
Expand Down Expand Up @@ -355,6 +360,11 @@ MonoBehaviour:
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: f13fd0befee6d48479806b957b3ca94f
m_Address: Assets/DCL/PluginSystem/Global/AvatarFaceExpressionConfig.asset
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: f3a58c428a71b443e883f8fac00cfbbd
m_Address: Assets/DCL/AvatarRendering/AvatarShape/Assets/EmoteMaskCatalog.asset
m_ReadOnly: 0
Expand Down
5 changes: 5 additions & 0 deletions Explorer/Assets/AddressableAssetsData/AssetGroups/UI.asset
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,11 @@ MonoBehaviour:
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 5c07800f394bd8945936cf8e55cdd020
m_Address: FacialExpressionsWheelHUD.prefab
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 5deacee45f1ebfe4394a97755a51fcca
m_Address: Assets/Textures/ExplorePanel/BackpackHoverUnhoverSpriteAtlas.spriteatlasv2
m_ReadOnly: 0
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using UnityEngine;

namespace DCL.AvatarRendering.AvatarShape.Assets
{
[CreateAssetMenu(fileName = "AvatarFaceAnimationSettings", menuName = "DCL/Avatar/Face Animation Settings")]
public class AvatarFaceAnimationSettings : ScriptableObject
{
[field: Header("Blink")]
[field: SerializeField] public float MinBlinkInterval { get; private set; } = 2.0f;
[field: SerializeField] public float MaxBlinkInterval { get; private set; } = 8.0f;
[field: SerializeField] public float BlinkFrameDuration { get; private set; } = 0.05f;

[field: Header("Mouth Pose")]
[field: SerializeField] public float MouthPoseDuration { get; private set; } = 0.08f;
[field: SerializeField] public float VowelMouthPoseDuration { get; private set; } = 0.12f;
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;
using UnityEngine;

namespace DCL.AvatarRendering.AvatarShape
{
/// <summary>
/// ScriptableObject holding all named face expressions available to avatars.
/// Configure via Assets &gt; Create &gt; DCL &gt; Avatar &gt; Face Expression Config.
/// Expressions are the base layer of the face, underneath blink and mouth-pose animations
/// which temporarily override eyes and mouth respectively.
/// </summary>
[CreateAssetMenu(fileName = "AvatarFaceExpressionConfig", menuName = "DCL/Avatar/Face Expression Config")]
public class AvatarFaceExpressionConfig : ScriptableObject
{
public AvatarFaceExpressionDefinition[] Expressions = Array.Empty<AvatarFaceExpressionDefinition>();
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using UnityEngine;

namespace DCL.AvatarRendering.AvatarShape
{
/// <summary>
/// A named face pose combining specific atlas slice indices for eyebrows, eyes, and mouth.
/// Eyebrows atlas: 0 Idle, 1 Up, 2 Down, 3 Angry, 4 Sad, 5 Surprised, 6-15 Unused.
/// Eyes atlas: 0 Idle, 1 HalfClosed, 2 Closed, 3 WideOpen, 4-7 Look directions, 8-15 Unused.
/// Mouth atlas: 0-11 Mouth poses, 12 Sad, 13 Happy, 14 Smile, 15 Worried.
/// </summary>
[Serializable]
public struct AvatarFaceExpressionDefinition
{
public string Name;

public Sprite Icon;

[Range(0, 15)]
public int EyebrowsIndex;

[Range(0, 15)]
public int EyesIndex;

[Range(0, 15)]
public int MouthIndex;
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using DCL.AvatarRendering.AvatarShape.Components;
using DCL.AvatarRendering.AvatarShape.Rendering.TextureArray;
using DCL.AvatarRendering.Loading.Assets;
using UnityEngine;

namespace DCL.AvatarRendering.AvatarShape
{
/// <summary>
/// Material/MaterialPropertyBlock side-effects for face animation. The avatar facial
/// expression system delegates all expression-index binding here so it stays focused
/// on state transitions.
/// </summary>
public static class AvatarFaceMaterialUtils
{
public static void ApplyEyebrowsFrame(ref AvatarFaceComponent face, int eyebrowsIndex)
{
if (face.EyebrowsRenderer == null) return;
if (face.CurrentEyebrowsIndex == eyebrowsIndex) return;

face.CurrentEyebrowsIndex = eyebrowsIndex;
SetExpressionIndex(face.EyebrowsRenderer, eyebrowsIndex);
}

public static void ApplyEyeFrame(ref AvatarFaceComponent face, int eyeIndex)
{
if (face.EyeRenderer == null) return;
if (face.CurrentEyeIndex == eyeIndex) return;

face.CurrentEyeIndex = eyeIndex;
SetExpressionIndex(face.EyeRenderer, eyeIndex);
}

public static void ApplyMouthPose(ref AvatarFaceComponent face, int mouthPoseIndex)
{
if (face.MouthRenderer == null) return;
if (face.CurrentMouthPoseIndex == mouthPoseIndex) return;

face.CurrentMouthPoseIndex = mouthPoseIndex;
SetExpressionIndex(face.MouthRenderer, mouthPoseIndex);
}

public static void StartBlink(ref AvatarFaceComponent face)
{
face.IsBlinking = true;
face.BlinkFrameIndex = 0;
face.BlinkFrameTimer = 0f;
ApplyEyeFrame(ref face, AvatarFacialExpressionConstants.BLINK_SEQUENCE[0]);
}

public static void EndBlink(ref AvatarFaceComponent face, float minBlinkInterval, float maxBlinkInterval)
{
face.IsBlinking = false;
face.TimeSinceLastBlink = 0f;
face.NextBlinkTime = Random.Range(minBlinkInterval, maxBlinkInterval);
ApplyEyeFrame(ref face, face.EyesExpressionIndex);
}

public static void StopMouthAnimation(ref AvatarFaceComponent face)
{
face.AnimatingText = null;
ApplyMouthPose(ref face, face.MouthExpressionIndex);
}

/// <summary>
/// Searches the avatar's instantiated wearables for the first renderer whose name ends
/// with <paramref name="suffix"/>. Returns null when none of the wearables expose one
/// (e.g. wearable lacks a Mask_* mesh).
/// </summary>
public static Renderer? FindRendererWithSuffix(in AvatarShapeComponent avatarShape, string suffix)
{
for (var i = 0; i < avatarShape.InstantiatedWearables.Count; i++)
{
CachedAttachment wearable = avatarShape.InstantiatedWearables[i];

for (var j = 0; j < wearable.Renderers.Count; j++)
{
Renderer renderer = wearable.Renderers[j];

if (renderer.name.EndsWith(suffix))
return renderer;
}
}

return null;
}

// Sentinel index (<0) disables atlas slicing in the shader so non-atlas wearables sample
// their full base map; >=0 picks one cell of the 4x4 atlas grid. _ExpressionIndex lives
// in UnityPerMaterial CBuffer for SRP batcher, so MPB can't override it; each face
// renderer already has its own pooled material instance, so a direct write is safe.
private static void SetExpressionIndex(Renderer renderer, int index)
{
Material material = renderer.sharedMaterial;

if (material != null)
material.SetInteger(TextureArrayConstants.EXPRESSION_INDEX_SHADER, index);
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
namespace DCL.AvatarRendering.AvatarShape
{
/// <summary>
/// Atlas slice indices, mouth-pose mapping, and pause-character classification used by
/// the avatar facial expression pipeline.
/// </summary>
/// <remarks>
/// Atlases are 1024×1024, 4×4 grid of 256px cells (top-to-bottom, left-to-right).
///
/// Eyebrows: 0 Idle, 1 Up, 2 Down, 3 Angry, 4 Sad, 5 Surprised, 6-15 Unused.
/// Eyes: 0 Idle, 1 HalfClosed, 2 Closed, 3 WideOpen, 4-7 Look*, 8-15 Unused.
/// Mouth: 0 Idle, 1 a/e/i, 2 b/m/p, 3 f/v, 4 d/th, 5 u, 6 c/g/h/k/n/s/t/x/y/z,
/// 7 o, 8 l, 9 r, 10 ch/j/sh, 11 w/q, 12 Sad, 13 Happy, 14 Smile, 15 Worried.
/// </remarks>
public static class AvatarFacialExpressionConstants
{
public const int NO_EYEBROWS_OVERRIDE = -1;
public const int NO_EYE_OVERRIDE = -1;
public const int NO_MOUTH_POSE = -1;

public const int EYE_HALF_CLOSED = 1;
public const int EYE_CLOSED = 2;

public const float UPPERCASE_DURATION_MULTIPLIER = 2f;

/// <summary>Mouth-pose-rich text looped while an avatar is actively speaking in voice chat.</summary>
public const string VOICE_CHAT_LOOP_TEXT = "el murcielago hindu comia feliz cardillo y kiwi";

/// <summary>One blink: HalfClosed → Closed → HalfClosed → restore expression.</summary>
public static readonly int[] BLINK_SEQUENCE = { EYE_HALF_CLOSED, EYE_CLOSED, EYE_HALF_CLOSED };

public static bool IsVowel(char c)
{
c = char.ToLowerInvariant(c);
return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u';
}

/// <summary>
/// Pause characters hold the expression mouth (not idle) instead of an articulated pose.
/// </summary>
public static bool IsPauseChar(char c) =>
c == ',' || c == '.' || c == ';' || c == ':' || c == '!' || c == '?' || c == ' ' || c == '\n';

/// <summary>
/// Maps a character at <paramref name="index"/> to a mouth pose slice. Digraphs
/// (th, ch, sh) detected by peeking. Pause chars and any unmapped char return
/// <see cref="NO_MOUTH_POSE"/> so the caller falls back to the expression mouth.
/// </summary>
public static int MapCharToMouthPose(string text, int index)
{
char c = char.ToLowerInvariant(text[index]);

if (IsPauseChar(c))
return NO_MOUTH_POSE;

char next = index + 1 < text.Length ? char.ToLowerInvariant(text[index + 1]) : '\0';

switch (c)
{
case 'a': case 'e': case 'i': return 1;
case 'b': case 'm': case 'p': return 2;
case 'f': case 'v': return 3;
case 't': return next == 'h' ? 4 : 6;
case 'u': return 5;
case 'd': return 4;
case 'g': case 'h':
case 'k': case 'n': case 'x':
case 'y': case 'z': return 6;
case 'c': return next == 'h' ? 10 : 6;
case 's': return next == 'h' ? 10 : 6;
case 'o': return 7;
case 'l': return 8;
case 'r': return 9;
case 'j': return 10;
case 'w': case 'q': return 11;
default: return NO_MOUTH_POSE;
}
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using UnityEngine;

namespace DCL.AvatarRendering.AvatarShape.Components
{
/// <summary>
/// Per-avatar facial animation state. One component holds all three layers because they are
/// always added/removed together and the pipeline reads them together each frame:
/// base expression → blink overrides eyes → chat/voice overrides mouth → pause restores
/// mouth to <see cref="MouthExpressionIndex"/> (not idle).
/// </summary>
public struct AvatarFaceComponent
{
// Renderers — null while setup is pending or after avatar re-instantiation.
public Renderer EyebrowsRenderer;
public Renderer EyeRenderer;
public Renderer MouthRenderer;

// Cached <see cref="Renderer.sharedMaterial"/> at the time we last wrote _ExpressionIndex.
// The skinning material pool can swap the material under us during a wearable swap while
// returning the SAME renderer instance from the wearable cache — the new material starts at
// the shader-default sentinel, so a renderer-ref-only diff would happily skip the write and
// the channel would render the full atlas. AvatarFacialExpressionSystem diffs these against
// <c>renderer.sharedMaterial</c> each frame to detect that case.
public Material EyebrowsMaterial;
public Material EyeMaterial;
public Material MouthMaterial;

// Per-channel atlas capability of the currently worn wearable. Stable across animation;
// only changes when a wearable swap rebinds the face renderers.
public bool EyebrowsHasExpressionAtlas;
public bool EyesHasExpressionAtlas;
public bool MouthHasExpressionAtlas;

// Resting atlas cell per channel (0..N when capability bool is true, -1 otherwise).
// Eyes/mouth are restored to these when blink / mouth animation ends.
public int EyebrowsExpressionIndex;
public int EyesExpressionIndex;
public int MouthExpressionIndex;

// Currently applied MaterialPropertyBlock slice indices. -1 means no override (material default).
public int CurrentEyebrowsIndex;
public int CurrentEyeIndex;
public int CurrentMouthPoseIndex;

// Blink state.
public bool IsBlinking;
public float TimeSinceLastBlink;
public float NextBlinkTime;
public int BlinkFrameIndex;
public float BlinkFrameTimer;

// Mouth-pose animation state. AnimatingText is null when idle.
public string? AnimatingText;
public int CharacterIndex;
public float CharacterTimer;

/// <summary>True when expression indices changed and need to be pushed to renderers this frame.</summary>
public bool IsDirty;
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading