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); }
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)); } }