Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2aa2879
feat: implement Builder mode
dmarotta Mar 10, 2026
09160a1
fix: made zoom buttons work in builder mode
dmarotta Mar 10, 2026
2a5d5f2
base64 wearables removing entities loaded from URNs
daniele-dcl Mar 11, 2026
3eb0c05
play looping emotes
daniele-dcl Mar 11, 2026
464b124
cleaned up debug logs
daniele-dcl Mar 12, 2026
e09ef4e
support for base64 emotes
daniele-dcl Mar 12, 2026
38e8924
Merge branch 'main' into feat/builder-mode-support
daniele-dcl Mar 12, 2026
a34e271
support disableFace query parameter in builder mode
daniele-dcl Mar 12, 2026
4854f0a
Add emote control functions and messaging to JSBridge and PreviewCont…
RocioCM Apr 2, 2026
e2221bb
Merge pull request #52 from decentraland/fix/builder-mode-emotes
RocioCM Apr 6, 2026
3c16f80
Merge branch 'main' of github.com:decentraland/aang-renderer into fea…
RocioCM Apr 6, 2026
84d2aa7
fix: improve wearable entity deduplication logic in Reload method
RocioCM Apr 8, 2026
0b8bb5e
Increase drag sensibility for builder
RocioCM Apr 8, 2026
70632f3
Render wearables with spring bones
RocioCM Apr 9, 2026
431cb3c
Fix spring bones wearables position during emotes
RocioCM Apr 9, 2026
a67e1e6
refactor: remove unused glTF extension registration and improve extra…
RocioCM Apr 9, 2026
85a0d18
Fix PR comments
RocioCM Apr 9, 2026
5fa5717
Merge branch 'feat/builder-mode-support' of github.com:decentraland/a…
RocioCM Apr 9, 2026
d9e17a6
Merge branch 'main' of github.com:decentraland/aang-renderer into fea…
RocioCM Apr 10, 2026
7319838
Update variables names
RocioCM Apr 10, 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
3 changes: 2 additions & 1 deletion Assets/Scripts/Bootstrap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,5 @@ private void SetName()
var bridge = FindAnyObjectByType<JSBridge>();
bridge.SetUsername("Miha");
}
}

}
36 changes: 1 addition & 35 deletions Assets/Scripts/Loading/GLTFLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -236,40 +236,6 @@ private static void Sanitize(Transform root)
Object.Destroy(sceneChild.gameObject);
}

// var skinnedRenderer = root.GetComponentInChildren<SkinnedMeshRenderer>();
// var armatureRoot = skinnedRenderer.rootBone.parent;
// armatureRoot.name = "Armature"; // Force Armature name since legacy animation needs it
//
// // Fix for wearables with incorrect hierarchy. Why do we even have conventions?
// // Offender: urn:decentraland:matic:collections-v2:0xa6a59f7a7b1401670ea09dc5554b55757163e20d:0
// if (armatureRoot.parent != root)
// {
// var armatureParent = armatureRoot.parent;
//
// while (armatureParent.childCount > 0)
// {
// var child = armatureParent.GetChild(0);
// child.SetParent(root, true);
// }
//
// Object.Destroy(armatureParent.gameObject);
// }
//
// // Some emotes like urn:decentraland:matic:collections-v2:0x705652b66a12dcf782b0b3d5673fbf0c1797eba2:3
// // move the armature??? And not all emotes reset it in animation. Dance does, idle does not.
// armatureRoot.localPosition = Vector3.zero;
//
// foreach (Transform t in root)
// {
// // Some wearables have weird scales so we normalize them
// t.localScale = new Vector3(0.01f, 0.01f, 0.01f);
//
// // If there are objects named Armature that aren't the actual armature, rename them
// if (t.name == "Armature" && t != armatureRoot)
// {
// t.name = "Armature_ThatShouldNotBeHere";
// }
// }
}
}
}
}
107 changes: 97 additions & 10 deletions Assets/Scripts/Utils/AvatarUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,29 +166,47 @@ public static void HideBodyShapeFacialFeatures(GameObject bodyShape, bool hideEy
public static void SetupColors(GameObject go, AvatarColors colors,
List<Renderer> outlineRenderers, Transform avatarRootBone = null, Transform[] avatarBones = null)
{
Dictionary<string, Transform> avatarBoneMap = null;
var renderers = go.GetComponentsInChildren<SkinnedMeshRenderer>();
foreach (var r in renderers)
foreach (var renderer in renderers)
{
if (r.material.name.Contains("skin", StringComparison.OrdinalIgnoreCase))
if (renderer.material.name.Contains("skin", StringComparison.OrdinalIgnoreCase))
{
r.material.SetColor(WearablesConstants.Shaders.BASE_COLOR_ID, colors.Skin);
renderer.material.SetColor(WearablesConstants.Shaders.BASE_COLOR_ID, colors.Skin);
}
else if (r.material.name.Contains("hair", StringComparison.OrdinalIgnoreCase))
else if (renderer.material.name.Contains("hair", StringComparison.OrdinalIgnoreCase))
{
r.material.SetColor(WearablesConstants.Shaders.BASE_COLOR_ID, colors.Hair);
renderer.material.SetColor(WearablesConstants.Shaders.BASE_COLOR_ID, colors.Hair);
}

if (avatarRootBone != null && avatarBones != null)
{
r.rootBone = avatarRootBone;
r.bones = avatarBones;
if (renderer.bones.Length <= avatarBones.Length)
{
renderer.rootBone = avatarRootBone;
renderer.bones = avatarBones;
}
else
{
// Wearable has extra bones (e.g. spring bones).
// Remap standard avatar bones by name, preserve extra ones.
avatarBoneMap ??= BuildBoneMap(avatarBones);
RemapBonesPreservingExtras(renderer, avatarRootBone, avatarBoneMap);
}
}

if (r.material.shader.name == "DCL/DCL_Toon" && r.sharedMaterial.renderQueue is >= 2000 and < 3000)
if (renderer.material.shader.name == "DCL/DCL_Toon" && renderer.sharedMaterial.renderQueue is >= 2000 and < 3000)
{
outlineRenderers.Add(r);
outlineRenderers.Add(renderer);
}
}

// Re-parent extra bones (spring bone chains) under their nearest avatar skeleton ancestor
// so they follow the avatar during emotes instead of staying at a fixed world position.
if (avatarBoneMap != null)
{
ReparentExtraBonesUnderAvatarSkeleton(go, avatarBoneMap);
}
}

public static void SetupFacialFeatures(GameObject go, AvatarColors colors,
Expand Down Expand Up @@ -252,5 +270,74 @@ private static Color GetFacialFeatureColor(string category, AvatarColors colors)
_ => throw new ArgumentOutOfRangeException(nameof(category), category, null)
};
}

private static Dictionary<string, Transform> BuildBoneMap(Transform[] bones)
{
var map = new Dictionary<string, Transform>(bones.Length);
foreach (var bone in bones)
{
if (bone != null)
map[bone.name] = bone;
}
return map;
}

/// <summary>
/// Remaps a skinned mesh's bones to the avatar skeleton by name,
/// preserving any extra bones (e.g. spring bone chains) that don't
/// exist in the avatar skeleton.
/// </summary>
private static void RemapBonesPreservingExtras(SkinnedMeshRenderer renderer,
Transform avatarRootBone, Dictionary<string, Transform> avatarBoneMap)
{
var meshBones = renderer.bones;
var remapped = new Transform[meshBones.Length];

for (var i = 0; i < meshBones.Length; i++)
{
remapped[i] = meshBones[i] != null && avatarBoneMap.TryGetValue(meshBones[i].name, out var avatarBone)
? avatarBone
: meshBones[i];
}

renderer.rootBone = avatarRootBone;
renderer.bones = remapped;
}

/// <summary>
/// Re-parents the roots of extra-bone chains (e.g. spring bone chains) under their
/// nearest avatar skeleton ancestor so they follow the avatar during emotes.
/// Only chain roots are re-parented; descendants stay under their chain parent,
/// preserving the chain hierarchy.
/// </summary>
private static void ReparentExtraBonesUnderAvatarSkeleton(GameObject wearableRoot,
Dictionary<string, Transform> avatarBoneMap)
{
// Collect all transforms in the wearable's hierarchy
var allTransforms = wearableRoot.GetComponentsInChildren<Transform>(true);

// Build a set of all live avatar transforms for fast identity checks
var liveAvatarTransforms = new HashSet<Transform>(avatarBoneMap.Values);

foreach (var transform in allTransforms)
{
// Skip the root itself
if (transform == wearableRoot.transform) continue;

// Skip if this transform is itself a live avatar bone
if (liveAvatarTransforms.Contains(transform)) continue;

// Only re-parent chain roots: transforms whose direct parent is a live avatar bone.
// Descendants within the chain keep their existing parent, preserving chain structure.
if (transform.parent == null || !liveAvatarTransforms.Contains(transform.parent)) continue;

// The direct parent is a live avatar bone by identity, but it may be the wearable's
// copy of that bone rather than the actual live instance. Re-parent under the live one.
if (avatarBoneMap.TryGetValue(transform.parent.name, out var liveParent) && transform.parent != liveParent)
{
transform.SetParent(liveParent, true);
}
}
}
}
}
}
Loading