Exemplo n.º 1
0
        public static SharedCharacterFaceStylesProvider GetForGender(bool isMale)
        {
            var subpath = "Human/" + (isMale ? "Male" : "Female") + "/";

            if (Providers.TryGetValue(subpath, out var provider))
            {
                return(provider);
            }

            Providers[subpath] = provider = new SharedCharacterFaceStylesProvider(subpath);
            return(provider);
        }
Exemplo n.º 2
0
        public static async Task<ITextureResource> GenerateHeadSprite(
            CharacterHeadSpriteData data,
            ProceduralTextureRequest request,
            bool isMale,
            HeadSpriteType headSpriteType,
            bool isPreview,
            Vector2Ushort? customTextureSize = null,
            sbyte spriteQualityOffset = 0)
        {
            var isFrontFace = headSpriteType == HeadSpriteType.Front;
            var renderingTag = request.TextureName;
            var side = isFrontFace ? "Front" : "Back";

            var style = data.FaceStyle;

            var faceStylesProvider = SharedCharacterFaceStylesProvider.GetForGender(isMale);

            var facePath = $"{faceStylesProvider.FacesFolderPath}{style.FaceId}/{side}";
            var faceShapePath = facePath + ".png";

            if (!IsFileExists(faceShapePath))
            {
                Api.Logger.Error("Face sprite not found: " + faceShapePath);
                // try fallback
                facePath = faceStylesProvider.FacesFolderPath + "Face01/" + side;
                faceShapePath = facePath;
                if (!IsFileExists(faceShapePath))
                {
                    // no fallback
                    return TextureResource.NoTexture;
                }
            }

            var faceTopPath = $"{facePath}Top{style.TopId}.png";
            var faceBottomPath = $"{facePath}Bottom{style.BottomId}.png";

            if (isFrontFace)
            {
                if (!IsFileExists(faceTopPath))
                {
                    Api.Logger.Error("Face top sprite not found: " + faceTopPath);
                    // try fallback
                    faceTopPath = $"{facePath}Top01.png";
                    if (!IsFileExists(faceTopPath))
                    {
                        // no fallback
                        return TextureResource.NoTexture;
                    }
                }

                if (!IsFileExists(faceBottomPath))
                {
                    Api.Logger.Error("Face bottom sprite not found: " + faceBottomPath);

                    // try fallback
                    faceBottomPath = $"{facePath}Bottom01.png";
                    if (!IsFileExists(faceBottomPath))
                    {
                        // no fallback
                        return TextureResource.NoTexture;
                    }
                }
            }

            var protoItemHeadEquipment = data.HeadEquipmentItemProto;
            var isHairVisible = protoItemHeadEquipment?.IsHairVisible ?? true;
            isHairVisible &= style.HairId is not null;

            string hair = null, hairBehind = null;
            if (isHairVisible
                && !string.IsNullOrEmpty(style.HairId))
            {
                var hairBase = faceStylesProvider.HairFolderPath + $"{style.HairId}/{side}";
                hair = hairBase + ".png";
                hairBehind = hairBase + "Behind.png";
            }

            string skinTone = null;
            if (!string.IsNullOrEmpty(style.SkinToneId))
            {
                skinTone = SharedCharacterFaceStylesProvider.GetSkinToneFilePath(style.SkinToneId);
            }

            string hairColor = null;
            if (!string.IsNullOrEmpty(style.HairColorId))
            {
                hairColor = SharedCharacterFaceStylesProvider.HairColorRootFolderPath + $"{style.HairColorId}" + ".png";
            }

            string helmetFront = null, helmetBehind = null;
            TextureResource helmetFrontMaskTextureResource = null;

            if (protoItemHeadEquipment is not null)
            {
                protoItemHeadEquipment.ClientGetHeadSlotSprites(data.HeadEquipmentItem,
                                                                isMale,
                                                                data.SkeletonResource,
                                                                isFrontFace,
                                                                isPreview,
                                                                out helmetFront,
                                                                out helmetBehind);

                if (helmetFront is null)
                {
                    throw new Exception("Helmet attachment is not available for " + protoItemHeadEquipment);
                }

                if (isFrontFace)
                {
                    helmetFrontMaskTextureResource = new TextureResource(
                        helmetFront.Substring(0, helmetFront.Length - ".png".Length) + "Mask.png",
                        qualityOffset: spriteQualityOffset);

                    if (!Api.Shared.IsFileExists(helmetFrontMaskTextureResource.FullPath))
                    {
                        helmetFrontMaskTextureResource = null;
                    }
                }
            }

            // let's combine all the layers (if some elements are null - they will not be rendered)
            List<ComposeLayer> layers;
            if (protoItemHeadEquipment is null
                || protoItemHeadEquipment.IsHeadVisible)
            {
                var faceLayer = await CreateFaceTexture(
                                    request,
                                    renderingTag,
                                    customTextureSize,
                                    new List<ComposeLayer>()
                                    {
                                        new(faceShapePath, spriteQualityOffset),
                                        new(faceTopPath, spriteQualityOffset),
                                        new(faceBottomPath, spriteQualityOffset)
                                    },
        public static void SetupSkeletonEquipmentForCharacter(
            ICharacter character,
            IItemsContainer containerEquipment,
            IComponentSkeleton skeletonRenderer,
            ProtoCharacterSkeleton skeleton,
            List <IClientComponent> skeletonComponents,
            bool isPreview = false)
        {
            if (!(skeleton is SkeletonHumanMale) &&
                !(skeleton is SkeletonHumanFemale))
            {
                // not a human
                // setup only implants
                using var equipmentImplants = Api.Shared.WrapInTempList(
                          containerEquipment.GetItemsOfProto <IProtoItemEquipmentImplant>());
                foreach (var item in equipmentImplants.AsList())
                {
                    var proto = (IProtoItemEquipmentImplant)item.ProtoGameObject;
                    proto.ClientSetupSkeleton(item,
                                              character,
                                              skeletonRenderer,
                                              skeletonComponents,
                                              isPreview);
                }

                return;
            }

            bool isMale, isHeadEquipmentHiddenForSelfAndPartyMembers;
            CharacterHumanFaceStyle faceStyle;

            if (character.ProtoCharacter is PlayerCharacter)
            {
                var publicState = PlayerCharacter.GetPublicState(character);
                faceStyle = publicState.FaceStyle;
                isMale    = publicState.IsMale;
                isHeadEquipmentHiddenForSelfAndPartyMembers = publicState.IsHeadEquipmentHiddenForSelfAndPartyMembers;

                if (isMale && !(skeleton is SkeletonHumanMale) ||
                    !isMale && !(skeleton is SkeletonHumanFemale))
                {
                    throw new Exception(
                              $"Skeleton don\'t match the gender of the player\'s character: isMale={isMale}, {skeleton}");
                }
            }
            else
            {
                // for NPC it will generate a random face
                isMale    = true;
                faceStyle = SharedCharacterFaceStylesProvider.GetForGender(isMale).GenerateRandomFace();
                isHeadEquipmentHiddenForSelfAndPartyMembers = false;
            }

            skeletonRenderer.ResetAttachments();
            skeleton.ClientResetItemInHand(skeletonRenderer);

            var skinToneId = faceStyle.SkinToneId;

            if (string.IsNullOrEmpty(skinToneId))
            {
                skeletonRenderer.DefaultTextureRemapper = null;
            }
            else
            {
                // use colorizer for the original sprites (to apply the skin tone)
                skeletonRenderer.DefaultTextureRemapper
                    = textureResource =>
                    {
                    var filePath = textureResource.LocalPath;
                    if (filePath.IndexOf("/Weapon", StringComparison.Ordinal) >= 0 ||
                        filePath.IndexOf("/Head", StringComparison.Ordinal) >= 0)
                    {
                        // no need to remap the original head and weapon sprites
                        // (they're never used as is)
                        return(textureResource);
                    }

                    return(ClientCharacterSkinTexturesCache.Get(textureResource,
                                                                skinToneId));
                    };
            }

            // setup equipment items
            using var equipmentItems = Api.Shared.WrapInTempList(
                      containerEquipment.GetItemsOfProto <IProtoItemEquipment>());
            if (!IsAllowNakedHumans)
            {
                if (!equipmentItems.AsList().Any(i => i.ProtoGameObject is IProtoItemEquipmentArmor))
                {
                    // no armor equipped - apply generic one
                    var pants = GenericPantsAttachments.Value;
                    ClientSkeletonAttachmentsLoader.SetAttachments(
                        skeletonRenderer,
                        isMale
                            ? pants.SlotAttachmentsMale
                            : pants.SlotAttachmentsFemale);

                    // select a random generic T-shirt based on character ID
                    var allShirts          = GenericShirtAttachments.Value;
                    var selectedShirtIndex = character.Id % allShirts.Length;
                    var shirt = allShirts[(int)selectedShirtIndex];
                    ClientSkeletonAttachmentsLoader.SetAttachments(
                        skeletonRenderer,
                        isMale
                            ? shirt.SlotAttachmentsMale
                            : shirt.SlotAttachmentsFemale);
                }
            }

            IItem headEquipmentForFaceSprite = null;

            foreach (var item in equipmentItems.AsList())
            {
                var proto = (IProtoItemEquipment)item.ProtoGameObject;
                proto.ClientSetupSkeleton(item,
                                          character,
                                          skeletonRenderer,
                                          skeletonComponents,
                                          isPreview);

                if (item.ProtoItem is IProtoItemEquipmentHead &&
                    headEquipmentForFaceSprite is null)
                {
                    headEquipmentForFaceSprite = item;
                }
            }

            if (isHeadEquipmentHiddenForSelfAndPartyMembers &&
                (character.IsCurrentClientCharacter ||
                 PartySystem.ClientIsPartyMember(character.Name) ||
                 PveSystem.ClientIsPve(false)))
            {
                headEquipmentForFaceSprite = null;
            }

            // generate head sprites for human players
            const string slotName       = "Head",
                         attachmentName = "Head";

            headGenerationId++;

            var spriteQualityOffset = skeletonRenderer.SpriteQualityOffset;

            skeletonRenderer.SetAttachmentSprite(
                skeleton.SkeletonResourceFront,
                slotName,
                attachmentName,
                new ProceduralTexture(
                    $"Head Front CharacterID={character.Id} gen={headGenerationId}",
                    proceduralTextureRequest =>
                    ClientCharacterHeadSpriteComposer.GenerateHeadSprite(
                        new CharacterHeadSpriteData(faceStyle,
                                                    headEquipmentForFaceSprite,
                                                    skeleton.SkeletonResourceFront),
                        proceduralTextureRequest,
                        isMale,
                        headSpriteType: ClientCharacterHeadSpriteComposer.HeadSpriteType.Front,
                        spriteQualityOffset: spriteQualityOffset),
                    isTransparent: true,
                    isUseCache: false));

            skeletonRenderer.SetAttachmentSprite(
                skeleton.SkeletonResourceBack,
                slotName,
                attachmentName,
                new ProceduralTexture(
                    $"Head Back CharacterID={character.Id} gen={headGenerationId}",
                    proceduralTextureRequest =>
                    ClientCharacterHeadSpriteComposer.GenerateHeadSprite(
                        new CharacterHeadSpriteData(faceStyle,
                                                    headEquipmentForFaceSprite,
                                                    skeleton.SkeletonResourceBack),
                        proceduralTextureRequest,
                        isMale,
                        headSpriteType: ClientCharacterHeadSpriteComposer.HeadSpriteType.Back,
                        spriteQualityOffset: spriteQualityOffset),
                    isTransparent: true,
                    isUseCache: false));

            skeletonRenderer.SetAttachmentSprite(
                skeleton.SkeletonResourceBack,
                slotName + "Back",
                attachmentName,
                new ProceduralTexture(
                    $"Head Back2 CharacterID={character.Id} gen={headGenerationId}",
                    proceduralTextureRequest =>
                    ClientCharacterHeadSpriteComposer.GenerateHeadSprite(
                        new CharacterHeadSpriteData(faceStyle,
                                                    headEquipmentForFaceSprite,
                                                    skeleton.SkeletonResourceBack),
                        proceduralTextureRequest,
                        isMale,
                        headSpriteType: ClientCharacterHeadSpriteComposer.HeadSpriteType.BackOverlay,
                        spriteQualityOffset: spriteQualityOffset),
                    isTransparent: true,
                    isUseCache: false));
        }
        public static async Task <ITextureResource> GenerateHeadSprite(
            CharacterHeadSpriteData data,
            ProceduralTextureRequest request,
            bool isMale,
            bool isFrontFace,
            Vector2Ushort?customTextureSize = null,
            sbyte spriteQualityOffset       = 0)
        {
            var renderingTag = request.TextureName;
            var side         = isFrontFace ? "Front" : "Back";

            var style = data.FaceStyle;

            var faceStylesProvider = SharedCharacterFaceStylesProvider.GetForGender(isMale);

            var facePath      = $"{faceStylesProvider.FacesFolderPath}{style.FaceId}/{side}";
            var faceShapePath = facePath + ".png";

            if (!IsFileExists(faceShapePath))
            {
                Api.Logger.Error("Face sprite not found: " + faceShapePath);
                // try fallback
                facePath      = faceStylesProvider.FacesFolderPath + "Face01/" + side;
                faceShapePath = facePath;
                if (!IsFileExists(faceShapePath))
                {
                    // no fallback
                    return(TextureResource.NoTexture);
                }
            }

            var faceTopPath    = $"{facePath}Top{style.TopId}.png";
            var faceBottomPath = $"{facePath}Bottom{style.BottomId}.png";

            if (isFrontFace)
            {
                if (!IsFileExists(faceTopPath))
                {
                    Api.Logger.Error("Face top sprite not found: " + faceTopPath);
                    // try fallback
                    faceTopPath = $"{facePath}Top01.png";
                    if (!IsFileExists(faceTopPath))
                    {
                        // no fallback
                        return(TextureResource.NoTexture);
                    }
                }

                if (!IsFileExists(faceBottomPath))
                {
                    Api.Logger.Error("Face bottom sprite not found: " + faceBottomPath);

                    // try fallback
                    faceBottomPath = $"{facePath}Bottom01.png";
                    if (!IsFileExists(faceBottomPath))
                    {
                        // no fallback
                        return(TextureResource.NoTexture);
                    }
                }
            }

            var itemHeadEquipment      = data.HeadEquipment;
            var protoItemHeadEquipment = (IProtoItemEquipmentHead)itemHeadEquipment?.ProtoItem;
            var isHairVisible          = protoItemHeadEquipment?.IsHairVisible ?? true;

            isHairVisible &= style.HairId != null;

            string hair = null, hairBehind = null;

            if (isHairVisible)
            {
                var hairBase = faceStylesProvider.HairFolderPath + $"{style.HairId}/{side}";
                hair       = hairBase + ".png";
                hairBehind = hairBase + "Behind.png";
            }

            string helmetFront = null, helmetBehind = null;

            if (protoItemHeadEquipment != null)
            {
                protoItemHeadEquipment.ClientGetHeadSlotSprites(
                    itemHeadEquipment,
                    isMale,
                    data.SkeletonResource,
                    isFrontFace,
                    out helmetFront,
                    out helmetBehind);

                if (helmetFront == null)
                {
                    throw new Exception("Helmet attachment is not available for " + protoItemHeadEquipment);
                }
            }

            var isHeadVisible = protoItemHeadEquipment?.IsHeadVisible ?? true;

            // let's combine all the layers (if some elements are null - they will not be rendered)
            List <ComposeLayer> layers;

            if (isHeadVisible)
            {
                layers = new List <ComposeLayer>()
                {
                    new ComposeLayer(helmetBehind, spriteQualityOffset),
                    new ComposeLayer(hairBehind, spriteQualityOffset),
                    new ComposeLayer(faceShapePath, spriteQualityOffset),
                    new ComposeLayer(faceTopPath, spriteQualityOffset),
                    new ComposeLayer(faceBottomPath, spriteQualityOffset),
                    new ComposeLayer(hair, spriteQualityOffset),
                    new ComposeLayer(helmetFront, spriteQualityOffset)
                };
            }
            else // if head is not visible (defined by head equipment item)
            {
                layers = new List <ComposeLayer>()
                {
                    new ComposeLayer(helmetBehind, spriteQualityOffset),
                    new ComposeLayer(helmetFront, spriteQualityOffset)
                };
            }

            // load only those layers which had the according file
            layers.RemoveAll(
                t => t.TextureResource == null ||
                !IsFileExists(t.TextureResource.FullPath));

            if (layers.Count == 0)
            {
                Api.Logger.Error("No sprites for face rendering: " + request.TextureName);
                return(TextureResource.NoTexture);
            }

            // load all the layers data
            var resultTextureSize = await PrepareLayers(request, layers);

            if (customTextureSize.HasValue)
            {
                resultTextureSize = customTextureSize.Value;
            }

            var referencePivotPos = new Vector2Ushort(
                (ushort)(resultTextureSize.X / 2),
                (ushort)(resultTextureSize.Y / 2));

            // create camera and render texture
            var renderTexture = Rendering.CreateRenderTexture(renderingTag, resultTextureSize.X, resultTextureSize.Y);
            var cameraObject  = Api.Client.Scene.CreateSceneObject(renderingTag);
            var camera        = Rendering.CreateCamera(cameraObject,
                                                       renderingTag,
                                                       drawOrder: -100);

            camera.RenderTarget = renderTexture;
            camera.SetOrthographicProjection(resultTextureSize.X, resultTextureSize.Y);

            // create and prepare renderer for each layer
            foreach (var layer in layers)
            {
                var pivotPos = layer.PivotPos;
                var offsetX  = referencePivotPos.X - pivotPos.X;
                var offsetY  = pivotPos.Y - referencePivotPos.Y;
                var offset   = (offsetX, offsetY);

                Rendering.CreateSpriteRenderer(
                    cameraObject,
                    layer.TextureResource,
                    positionOffset: offset,
                    // draw down
                    spritePivotPoint: (0, 1),
                    renderingTag: renderingTag);
            }

            // ReSharper disable once CoVariantArrayConversion
            request.ChangeDependencies(layers.Select(l => l.TextureResource).ToArray());

            await camera.DrawAsync();

            cameraObject.Destroy();

            request.ThrowIfCancelled();

            var generatedTexture = await renderTexture.SaveToTexture(
                isTransparent : true,
                qualityScaleCoef : Rendering.CalculateCurrentQualityScaleCoefWithOffset(
                    spriteQualityOffset));

            renderTexture.Dispose();
            request.ThrowIfCancelled();
            return(generatedTexture);
        }
        public static void SetupSkeletonEquipmentForCharacter(
            ICharacter character,
            IItemsContainer containerEquipment,
            IComponentSkeleton skeletonRenderer,
            ProtoCharacterSkeleton skeleton,
            List <IClientComponent> skeletonComponents)
        {
            if (!(skeleton is SkeletonHumanMale) &&
                !(skeleton is SkeletonHumanFemale))
            {
                // not a human
                return;
            }

            skeletonRenderer.ResetAttachments();

            bool isMale;
            CharacterHumanFaceStyle faceStyle;

            if (character.ProtoCharacter is PlayerCharacter)
            {
                var pubicState = PlayerCharacter.GetPublicState(character);
                faceStyle = pubicState.FaceStyle;
                isMale    = pubicState.IsMale;

                if (isMale && !(skeleton is SkeletonHumanMale) ||
                    !isMale && !(skeleton is SkeletonHumanFemale))
                {
                    throw new Exception(
                              $"Skeleton don\'t match the gender of the player\'s character: isMale={isMale}, {skeleton}");
                }
            }
            else
            {
                // for NPC it will generate a random face
                isMale    = true;
                faceStyle = SharedCharacterFaceStylesProvider.GetForGender(isMale).GenerateRandomFace();
            }

            // setup equipment items
            var equipmentItems = containerEquipment.GetItemsOfProto <IProtoItemEquipment>().ToList();

            if (!IsAllowNakedHumans)
            {
                if (!equipmentItems.Any(i => i.ProtoGameObject is IProtoItemEquipmentLegs))
                {
                    // no lower cloth - apply generic one
                    ClientSkeletonAttachmentsLoader.SetAttachments(
                        skeletonRenderer,
                        isMale
                            ? GenericPantsAttachments.Value.SlotAttachmentsMale
                            : GenericPantsAttachments.Value.SlotAttachmentsFemale);
                }

                if (!equipmentItems.Any(i => i.ProtoGameObject is IProtoItemEquipmentChest))
                {
                    // no upper cloth - apply generic one (based on character Id)
                    var allShirts          = GenericShirtAttachments.Value;
                    var selectedShirtIndex = character.Id % allShirts.Length;
                    var shirt = allShirts[(int)selectedShirtIndex];
                    ClientSkeletonAttachmentsLoader.SetAttachments(
                        skeletonRenderer,
                        isMale
                            ? shirt.SlotAttachmentsMale
                            : shirt.SlotAttachmentsFemale);
                }
            }

            IItem headEquipment = null;

            foreach (var item in equipmentItems)
            {
                var proto = (IProtoItemEquipment)item.ProtoGameObject;
                proto.ClientSetupSkeleton(item, character, skeletonRenderer, skeletonComponents);

                if (item.ProtoItem is IProtoItemEquipmentHead &&
                    headEquipment == null)
                {
                    headEquipment = item;
                }
            }

            // generate head sprites for human players
            const string slotName = "Head", attachmentName = "Head";

            skeletonRenderer.SetAttachmentSprite(
                skeleton.SkeletonResourceFront,
                slotName,
                attachmentName,
                new ProceduralTexture(
                    "Head Front CharacterID=" + character.Id,
                    proceduralTextureRequest =>
                    ClientCharacterHeadSpriteComposer.GenerateHeadSprite(
                        new CharacterHeadSpriteData(faceStyle, headEquipment, skeleton.SkeletonResourceFront),
                        proceduralTextureRequest,
                        isMale,
                        isFrontFace: true,
                        spriteQualityOffset: skeletonRenderer.SpriteQualityOffset),
                    isTransparent: true,
                    isUseCache: false));

            skeletonRenderer.SetAttachmentSprite(
                skeleton.SkeletonResourceBack,
                slotName,
                attachmentName,
                new ProceduralTexture(
                    "Head Back CharacterID=" + character.Id,
                    proceduralTextureRequest =>
                    ClientCharacterHeadSpriteComposer.GenerateHeadSprite(
                        new CharacterHeadSpriteData(faceStyle, headEquipment, skeleton.SkeletonResourceBack),
                        proceduralTextureRequest,
                        isMale,
                        isFrontFace: false,
                        spriteQualityOffset: skeletonRenderer.SpriteQualityOffset),
                    isTransparent: true,
                    isUseCache: false));

            ClientResetWeaponAttachments(skeletonRenderer);
        }
        public static async Task <ITextureResource> GenerateHeadSprite(
            CharacterHeadSpriteData data,
            ProceduralTextureRequest request,
            bool isMale,
            HeadSpriteType headSpriteType,
            Vector2Ushort?customTextureSize = null,
            sbyte spriteQualityOffset       = 0)
        {
            var isFrontFace  = headSpriteType == HeadSpriteType.Front;
            var renderingTag = request.TextureName;
            var side         = isFrontFace ? "Front" : "Back";

            var style = data.FaceStyle;

            var faceStylesProvider = SharedCharacterFaceStylesProvider.GetForGender(isMale);

            var facePath      = $"{faceStylesProvider.FacesFolderPath}{style.FaceId}/{side}";
            var faceShapePath = facePath + ".png";

            if (!IsFileExists(faceShapePath))
            {
                Api.Logger.Error("Face sprite not found: " + faceShapePath);
                // try fallback
                facePath      = faceStylesProvider.FacesFolderPath + "Face01/" + side;
                faceShapePath = facePath;
                if (!IsFileExists(faceShapePath))
                {
                    // no fallback
                    return(TextureResource.NoTexture);
                }
            }

            var faceTopPath    = $"{facePath}Top{style.TopId}.png";
            var faceBottomPath = $"{facePath}Bottom{style.BottomId}.png";

            if (isFrontFace)
            {
                if (!IsFileExists(faceTopPath))
                {
                    Api.Logger.Error("Face top sprite not found: " + faceTopPath);
                    // try fallback
                    faceTopPath = $"{facePath}Top01.png";
                    if (!IsFileExists(faceTopPath))
                    {
                        // no fallback
                        return(TextureResource.NoTexture);
                    }
                }

                if (!IsFileExists(faceBottomPath))
                {
                    Api.Logger.Error("Face bottom sprite not found: " + faceBottomPath);

                    // try fallback
                    faceBottomPath = $"{facePath}Bottom01.png";
                    if (!IsFileExists(faceBottomPath))
                    {
                        // no fallback
                        return(TextureResource.NoTexture);
                    }
                }
            }

            var itemHeadEquipment      = data.HeadEquipment;
            var protoItemHeadEquipment = (IProtoItemEquipmentHead)itemHeadEquipment?.ProtoItem;
            var isHairVisible          = protoItemHeadEquipment?.IsHairVisible ?? true;

            isHairVisible &= style.HairId != null;

            string hair = null, hairBehind = null;

            if (isHairVisible &&
                !string.IsNullOrEmpty(style.HairId))
            {
                var hairBase = faceStylesProvider.HairFolderPath + $"{style.HairId}/{side}";
                hair       = hairBase + ".png";
                hairBehind = hairBase + "Behind.png";
            }

            string skinTone = null;

            if (!string.IsNullOrEmpty(style.SkinToneId))
            {
                skinTone = SharedCharacterFaceStylesProvider.GetSkinToneFilePath(style.SkinToneId);
            }

            string hairColor = null;

            if (!string.IsNullOrEmpty(style.HairColorId))
            {
                hairColor = SharedCharacterFaceStylesProvider.HairColorRootFolderPath + $"{style.HairColorId}" + ".png";
            }

            string helmetFront = null, helmetBehind = null;

            if (protoItemHeadEquipment != null)
            {
                protoItemHeadEquipment.ClientGetHeadSlotSprites(
                    itemHeadEquipment,
                    isMale,
                    data.SkeletonResource,
                    isFrontFace,
                    out helmetFront,
                    out helmetBehind);

                if (helmetFront is null)
                {
                    throw new Exception("Helmet attachment is not available for " + protoItemHeadEquipment);
                }
            }

            // let's combine all the layers (if some elements are null - they will not be rendered)
            List <ComposeLayer> layers;

            if (protoItemHeadEquipment is null ||
                protoItemHeadEquipment.IsHeadVisible)
            {
                var faceLayer = await CreateFaceTexture(
                    request,
                    renderingTag,
                    customTextureSize,
                    new List <ComposeLayer>()
                {
                    new ComposeLayer(faceShapePath, spriteQualityOffset),
                    new ComposeLayer(faceTopPath, spriteQualityOffset),
                    new ComposeLayer(faceBottomPath, spriteQualityOffset)
                },
                    skinTone);

                layers = new List <ComposeLayer>();

                if (isHairVisible)
                {
                    var(layerHair, layerHairBehind) = await GetHairLayers(request,
                                                                          hair,
                                                                          hairBehind,
                                                                          hairColor,
                                                                          spriteQualityOffset);

                    if (headSpriteType != HeadSpriteType.BackOverlay)
                    {
                        layers.Add(new ComposeLayer(helmetBehind, spriteQualityOffset));
                        layers.Add(layerHairBehind);
                        layers.Add(faceLayer);
                    }

                    if (headSpriteType != HeadSpriteType.Back)
                    {
                        if (layerHair.TextureResource is null &&
                            helmetFront is null &&
                            layers.Count == 0)
                        {
                            return(TransparentTexturePlaceholder);
                        }

                        layers.Add(layerHair);
                        layers.Add(new ComposeLayer(helmetFront, spriteQualityOffset));
                    }
                }
                else // hair is not visible
                {
                    if (headSpriteType == HeadSpriteType.BackOverlay)
                    {
                        return(TransparentTexturePlaceholder);
                    }

                    layers.Add(new ComposeLayer(helmetBehind, spriteQualityOffset));
                    layers.Add(faceLayer);
                    layers.Add(new ComposeLayer(helmetFront, spriteQualityOffset));
                }
            }