diff --git a/Game/GigLine/Content/Hollywood/DemoMap.umap b/Game/GigLine/Content/Hollywood/DemoMap.umap index 072f7c4..4626b60 100644 --- a/Game/GigLine/Content/Hollywood/DemoMap.umap +++ b/Game/GigLine/Content/Hollywood/DemoMap.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:23ed6e12412617ac3b7e2ed06c563d8d74dc065e420697ebee1b46a597cf9153 -size 782191 +oid sha256:8de62798452ec172a9a16454c52b6b4067c785c782856e9957f94d72e9b0bcce +size 782187 diff --git a/Game/GigLine/Content/Hollywood/Site/M_Asphalt.uasset b/Game/GigLine/Content/Hollywood/Site/M_Asphalt.uasset index 0be4fc3..8c247dd 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_Asphalt.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_Asphalt.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:04bd4d8a153654fb54399f1ea50a5162a2ee4db0ba390cfa719128681cdf4f39 +oid sha256:d7813524abdda9ff0c36675b03e0834d0e42ad1398bf5ab41fd43051006beb8f size 10019 diff --git a/Game/GigLine/Content/Hollywood/Site/M_Concrete.uasset b/Game/GigLine/Content/Hollywood/Site/M_Concrete.uasset index f359aa8..2f0baa2 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_Concrete.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_Concrete.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c3aa82ebe5f7a8f908031b50e1728724067e72d874695d462966bd9301667b9 +oid sha256:d11332ddd221887467723279522a1e001d34ecb31dc2a492ea6e95bcb24200a1 size 10024 diff --git a/Game/GigLine/Content/Hollywood/Site/M_FacadeBanner.uasset b/Game/GigLine/Content/Hollywood/Site/M_FacadeBanner.uasset index f766ce3..4ce4ad9 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_FacadeBanner.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_FacadeBanner.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0ce2a9b9068927b9c8ef6718a6407edaeb1c9a8322ce64fdc7cc6e9e8f91473 +oid sha256:d6e295f4bb0fbdb0019fa60bdea16e76dbdc8f9190fc6f5c866322ff04048180 size 11419 diff --git a/Game/GigLine/Content/Hollywood/Site/M_FacadeGlass.uasset b/Game/GigLine/Content/Hollywood/Site/M_FacadeGlass.uasset index a0790f4..4e1e5cc 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_FacadeGlass.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_FacadeGlass.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78d3c796adbf93aeec2ef38f6c100aa20efe91e02dc41eb91ef43a0808d8eabb +oid sha256:d3adfdf3801ada7f0dfba07ba047f832d7fb14dae40e5c68f4b3d081a284af64 size 11414 diff --git a/Game/GigLine/Content/Hollywood/Site/M_FacadeStone.uasset b/Game/GigLine/Content/Hollywood/Site/M_FacadeStone.uasset index d3cab60..45c8e07 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_FacadeStone.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_FacadeStone.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:13668d5d7d31f52b02df2876b9e313b184406e9a24316f29c1a630f947efe95e +oid sha256:7b81c127d48d6197a64c4e2659552cba9216040b6539e2e858532bd178aca767 size 17083 diff --git a/Game/GigLine/Content/Hollywood/Site/M_FacadeStoneShadow.uasset b/Game/GigLine/Content/Hollywood/Site/M_FacadeStoneShadow.uasset index 8ac58fa..c6a4cbf 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_FacadeStoneShadow.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_FacadeStoneShadow.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:335a90dfc29124c05482abdac17f3d8c1354ae0204147fef2825cc8ae3712cdd +oid sha256:ad51f421a3586dd35254243895992e68b588f3112ebfda2de8b65f1a213e596a size 17113 diff --git a/Game/GigLine/Content/Hollywood/Site/M_Ground.uasset b/Game/GigLine/Content/Hollywood/Site/M_Ground.uasset index e934217..813b10d 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_Ground.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_Ground.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b9d34bcfa8da66cb14b03389c6ead8f9f1ae754a8a1ced857fc62f226984e41 +oid sha256:e1421d11234823430518fd5a54d55bf3cbbc2a55ab1f3b074af937a4e8278f19 size 10014 diff --git a/Game/GigLine/Content/Hollywood/Site/M_KerbConcrete.uasset b/Game/GigLine/Content/Hollywood/Site/M_KerbConcrete.uasset index 93166b5..829b14a 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_KerbConcrete.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_KerbConcrete.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a34905b08eb54f71b97cb87d3fec7dbddff42b71940839372dd6e44dfd71c315 +oid sha256:15e398b8e172bda884355ab272df67c545898ebbdb2f354fe804dae562c350db size 10044 diff --git a/Game/GigLine/Content/Hollywood/Site/M_RoadPaint.uasset b/Game/GigLine/Content/Hollywood/Site/M_RoadPaint.uasset index 85ab9f3..96521d1 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_RoadPaint.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_RoadPaint.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c506556a13e23ddb87dfa004d4d1979b1e4ba242eb316c9f9c35775ee7e70c1 +oid sha256:ba4e8f0049f41185b3055937940cbf49fe5161e5b666ca657d2a89f059fb240a size 10029 diff --git a/Game/GigLine/Content/Hollywood/Site/M_Stucco.uasset b/Game/GigLine/Content/Hollywood/Site/M_Stucco.uasset index a145b51..b6e1c19 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_Stucco.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_Stucco.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f5a4f2e36fd29d282bbf90a586f1dbc1bbd37de13c1d589c75adf1cd5b30e0ef +oid sha256:c27c5f16cb64c6b6f982d2c5f02c3b8228661195aec2965a44e652f51d3d201f size 11389 diff --git a/Game/GigLine/Content/Hollywood/Site/Terrain_DEM.uasset b/Game/GigLine/Content/Hollywood/Site/Terrain_DEM.uasset index 7ef0e8c..a976d33 100644 --- a/Game/GigLine/Content/Hollywood/Site/Terrain_DEM.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Terrain_DEM.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6cf21dd7c6ba931027fa33c923a8733a6946a3eb3d89d7c78aacc5f452d08e28 +oid sha256:3843721059acc7150315582fbf1b9def613e25dd080075d538d0e3242506f5d7 size 357901 diff --git a/Game/GigLine/Content/Hollywood/Textures/T_Asphalt_BC.uasset b/Game/GigLine/Content/Hollywood/Textures/T_Asphalt_BC.uasset index 29a8cd2..ae6b6c3 100644 --- a/Game/GigLine/Content/Hollywood/Textures/T_Asphalt_BC.uasset +++ b/Game/GigLine/Content/Hollywood/Textures/T_Asphalt_BC.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7c937ea5c1641b1b8eec4710b18a0dfc9d0212b07e94bedeb9639e3d16fcf51 +oid sha256:7c6e450c1b0a3521ae49a289abdf4d42fde11f2d5bdbe0a7bedb578ef30e16d8 size 70879 diff --git a/Game/GigLine/Content/Hollywood/Textures/T_Asphalt_R.uasset b/Game/GigLine/Content/Hollywood/Textures/T_Asphalt_R.uasset index 37807ac..2549b47 100644 --- a/Game/GigLine/Content/Hollywood/Textures/T_Asphalt_R.uasset +++ b/Game/GigLine/Content/Hollywood/Textures/T_Asphalt_R.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:786cb9b8a2541e3ca112e9035ea8ca8d8e08b5ca0aab3f4b4273155812583597 +oid sha256:68e68c98469dc02e6c5b8fcb2c3e900e8fab307bc2d949086b5235264033d564 size 29011 diff --git a/Game/GigLine/Content/Hollywood/Textures/T_Concrete_BC.uasset b/Game/GigLine/Content/Hollywood/Textures/T_Concrete_BC.uasset index 96cf92d..0cb65af 100644 --- a/Game/GigLine/Content/Hollywood/Textures/T_Concrete_BC.uasset +++ b/Game/GigLine/Content/Hollywood/Textures/T_Concrete_BC.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37101199dd79da73000b7440c88a7abc0ec8ac826a19ba1881d53d759a794631 +oid sha256:1c8c6abccdfebaffef6972723ae1f5c41f6b7d7df0f4d9d63db0803dd2a9b046 size 58568 diff --git a/Game/GigLine/Content/Hollywood/Textures/T_Concrete_R.uasset b/Game/GigLine/Content/Hollywood/Textures/T_Concrete_R.uasset index 5f566b8..e45cd49 100644 --- a/Game/GigLine/Content/Hollywood/Textures/T_Concrete_R.uasset +++ b/Game/GigLine/Content/Hollywood/Textures/T_Concrete_R.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cbd02bca5a584563e5b1c2f69847efc99a99e32ed7c888c2e422fe7f36153663 +oid sha256:9f22caa766a717ac75a4f9429d17f7400613b32f0dbe8d28731c12c0aed4edad size 17517 diff --git a/Game/GigLine/Content/Hollywood/Textures/T_StoneAshlar_BC.uasset b/Game/GigLine/Content/Hollywood/Textures/T_StoneAshlar_BC.uasset index 14889f1..6240c8f 100644 --- a/Game/GigLine/Content/Hollywood/Textures/T_StoneAshlar_BC.uasset +++ b/Game/GigLine/Content/Hollywood/Textures/T_StoneAshlar_BC.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:01d251745b9410fa723ecb628037866b3524d556638f2f20314381ddcdd57e96 +oid sha256:b6b94da8dfca26cd352b52728b8242e44e780409b0c3937226f093e2d8ebe212 size 40610 diff --git a/Game/GigLine/Content/Hollywood/Textures/T_StoneAshlar_R.uasset b/Game/GigLine/Content/Hollywood/Textures/T_StoneAshlar_R.uasset index 7235ff2..7cd1c3b 100644 --- a/Game/GigLine/Content/Hollywood/Textures/T_StoneAshlar_R.uasset +++ b/Game/GigLine/Content/Hollywood/Textures/T_StoneAshlar_R.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:19d734e5e90e7281644668888e79db9c32417c5cbd9e5cbb107a843839a34d55 +oid sha256:8ba641f281f25e0e701e1eb25077190440fc68b99ba7752e4875446a94a05659 size 18770 diff --git a/Game/GigLine/Content/Hollywood/Textures/T_Stucco_BC.uasset b/Game/GigLine/Content/Hollywood/Textures/T_Stucco_BC.uasset index 5838451..820ce12 100644 --- a/Game/GigLine/Content/Hollywood/Textures/T_Stucco_BC.uasset +++ b/Game/GigLine/Content/Hollywood/Textures/T_Stucco_BC.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b3f4850c1625d3ab41dd44e4863cf786757f2613984fdce28ea43bb75c68770 +oid sha256:aa0b132aeb6b4fbb2cb8761359f704a361ee14df75555f5b191a2229880d8f3d size 62746 diff --git a/Game/GigLine/Content/Hollywood/Textures/T_Stucco_R.uasset b/Game/GigLine/Content/Hollywood/Textures/T_Stucco_R.uasset index 004e600..a5bf1b4 100644 --- a/Game/GigLine/Content/Hollywood/Textures/T_Stucco_R.uasset +++ b/Game/GigLine/Content/Hollywood/Textures/T_Stucco_R.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dbde83955c19ff3eba36dc49e97e6b3e9ca495b79ae4b0461223e046ab5e420a +oid sha256:2921580cd5d52b92370e75934aa75a17e732210c335080afeeaf1daf978ef35f size 17541 diff --git a/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_BladeSign.uasset b/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_BladeSign.uasset index 7ad379a..9a834e6 100644 --- a/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_BladeSign.uasset +++ b/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_BladeSign.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:510b14b2f9c9072a0e5448fef95b48f387967a94f85dbc17e7bb79103a6ca258 +oid sha256:dbae927e11940232de71bab55753a112e22a19f06af982fedd1c034f3d6a7d11 size 72330 diff --git a/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_BladeSignBracket.uasset b/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_BladeSignBracket.uasset index ddf418c..a284c86 100644 --- a/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_BladeSignBracket.uasset +++ b/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_BladeSignBracket.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:358972a96f6891a8fe9a994643cde7be92c0d1ba88cf45bf0eefd1b0e78b9bfb +oid sha256:34b7e9401e78d7628f17c722337e9ea47f7b327e8eb953dfad155a01c12acb05 size 72466 diff --git a/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_BladeSignFrame.uasset b/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_BladeSignFrame.uasset index a3624c1..7cd96d8 100644 --- a/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_BladeSignFrame.uasset +++ b/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_BladeSignFrame.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:03ef3c1d54d249fad5a5aa7db0b6c7db7452887722bfd9554b500dc74f6aee3a +oid sha256:9b0372b50359c9807961b2f4db59d394d3e364c27a9b07b66bc48bbf2f59f719 size 72406 diff --git a/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_MarqueeCanopy.uasset b/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_MarqueeCanopy.uasset index 7b91954..0f431c1 100644 --- a/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_MarqueeCanopy.uasset +++ b/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_MarqueeCanopy.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:04a38325dd685644e95d09072e6e2b0bb6985d4ef32ff4e71ea21f3d47ac76b7 +oid sha256:3cb25e6212bb41d463201a129be68c71b673c7ac54ad9083b47e2b015ebce747 size 72370 diff --git a/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_MarqueeFascia.uasset b/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_MarqueeFascia.uasset index db3ab16..c6de12a 100644 --- a/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_MarqueeFascia.uasset +++ b/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_MarqueeFascia.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:712144a026cc52d147c79dbfcfb76d058668bc3b7a9f254a00405311d13b984f +oid sha256:6cf2d288294921a07933e9c80afaf0d5872cffa589e207cc93622cb5b8901d8f size 72438 diff --git a/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_MarqueeSoffit.uasset b/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_MarqueeSoffit.uasset index 3dda446..449f847 100644 --- a/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_MarqueeSoffit.uasset +++ b/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_MarqueeSoffit.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a53c923dc6c399ce97247325317b1a8532a25563b0cce73265c4e93352b31c65 +oid sha256:4737a430db9b10bbf4a45c1ca8f8451cab2dfc81a0b4bda7e2961a5f4ade485a size 72413 diff --git a/Game/GigLine/Content/Test/Character/CharacterHair.uasset b/Game/GigLine/Content/Test/Character/CharacterHair.uasset new file mode 100644 index 0000000..1e70d74 --- /dev/null +++ b/Game/GigLine/Content/Test/Character/CharacterHair.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a8161350d42c9f78a42fa0da5117a32dfbe3bda5e82dea6ec381d11f6d6f968 +size 250937 diff --git a/Game/GigLine/Content/Test/Character/CharacterHair_Skeleton.uasset b/Game/GigLine/Content/Test/Character/CharacterHair_Skeleton.uasset new file mode 100644 index 0000000..3241fc2 --- /dev/null +++ b/Game/GigLine/Content/Test/Character/CharacterHair_Skeleton.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4093ce960bec358063314fe597b85958651fee6ce1dcbac9f4a7eeff9730d754 +size 29884 diff --git a/Game/GigLine/Content/Test/Character/T_Character_Hair.uasset b/Game/GigLine/Content/Test/Character/T_Character_Hair.uasset new file mode 100644 index 0000000..c24c9c5 --- /dev/null +++ b/Game/GigLine/Content/Test/Character/T_Character_Hair.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6cb1c096e4411359dbfc5bb6f96bb5836d87e99d78ee0997d15023d7d00e0ed4 +size 154469 diff --git a/Game/GigLine/Content/Test/Character/T_Character_SkinDetail.uasset b/Game/GigLine/Content/Test/Character/T_Character_SkinDetail.uasset new file mode 100644 index 0000000..48fd7f2 --- /dev/null +++ b/Game/GigLine/Content/Test/Character/T_Character_SkinDetail.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3491b466f4b02c6412b8cc439aaa586c25cc5624f0bb9348c19d0139ccc7842f +size 86485 diff --git a/Game/GigLine/Content/Test/ReviewMap.umap b/Game/GigLine/Content/Test/ReviewMap.umap index 895f13d..6c43951 100644 --- a/Game/GigLine/Content/Test/ReviewMap.umap +++ b/Game/GigLine/Content/Test/ReviewMap.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5a9d7551e24c2e2805e97f234cd5bbb4321d769c00a4cbe49f0835186c341d0 -size 29389 +oid sha256:2642ee68daa97bfee857231934a99dba98d83a318de1fe3cdf44da29b824473d +size 29694 diff --git a/Game/GigLine/Source/GigLine/GigLineCharacter.cpp b/Game/GigLine/Source/GigLine/GigLineCharacter.cpp index 7224e18..f3eabe6 100644 --- a/Game/GigLine/Source/GigLine/GigLineCharacter.cpp +++ b/Game/GigLine/Source/GigLine/GigLineCharacter.cpp @@ -90,6 +90,22 @@ AGigLineCharacter::AGigLineCharacter() GetMesh()->SetCollisionEnabled(ECollisionEnabled::NoCollision); GetMesh()->SetHiddenInGame(true); + // Hair: a separate component that copies the body's pose, so it follows the head + // bone without a second rig or a second animation. Hidden with the body. + HairMesh = CreateDefaultSubobject(TEXT("HairMesh")); + HairMesh->SetupAttachment(GetCapsuleComponent()); + HairMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision); + HairMesh->SetHiddenInGame(true); + static ConstructorHelpers::FObjectFinder HairFinder( + TEXT("/Game/Test/Character/CharacterHair.CharacterHair")); + if (HairFinder.Succeeded()) + { + HairMesh->SetSkeletalMesh(HairFinder.Object); + HairMesh->SetRelativeLocation(FVector(0.0f, 0.0f, -87.5f)); + HairMesh->SetRelativeRotation(FRotator(0.0f, -90.0f, 0.0f)); + HairMesh->SetLeaderPoseComponent(GetMesh()); + } + // Third-person boom. bDoCollisionTest is the camera-collision feature: the arm // shortens when something is between the pivot and the desired camera position. ThirdPersonArm = CreateDefaultSubobject(TEXT("ThirdPersonArm")); @@ -795,6 +811,10 @@ void AGigLineCharacter::ToggleCamera() FirstPersonCamera->SetActive(!bThirdPerson); ThirdPersonCamera->SetActive(bThirdPerson); GetMesh()->SetHiddenInGame(!bThirdPerson); + if (HairMesh) + { + HairMesh->SetHiddenInGame(!bThirdPerson); + } if (bThirdPerson) { UpdateBodyAnimation(); diff --git a/Game/GigLine/Source/GigLine/GigLineCharacter.h b/Game/GigLine/Source/GigLine/GigLineCharacter.h index f084b4f..601c530 100644 --- a/Game/GigLine/Source/GigLine/GigLineCharacter.h +++ b/Game/GigLine/Source/GigLine/GigLineCharacter.h @@ -155,6 +155,17 @@ protected: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera") float ThirdPersonBoomLength = 340.0f; + /** + * Hair, as its own skeletal mesh following the body's pose. + * + * A second component rather than geometry joined into the body: Unreal collapses + * the second material slot on a skeletal-mesh import, so joined hair rendered with + * the skin material. The character-artist skill calls for hair cards to be their + * own mesh for the same reason it calls for swappable parts to be separate. + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Body") + TObjectPtr HairMesh; + UPROPERTY() TObjectPtr IdleAnimation; diff --git a/MEMORY.md b/MEMORY.md index 70f2d1a..7aebd2c 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -492,3 +492,9 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO. - **ComfyUI is reachable at `127.0.0.1:8188` (0.44.0)** and its API is the clean way in: `POST /prompt` with an API-format graph (`[node_id, output_index]` references — the UI's workflow JSON cannot be posted), then poll `/history/` and fetch `/view?filename=...`. `Tools/comfyui_generate.py` does this and writes a sidecar with model, prompt, seed and the full graph. Installed checkpoints: `juggernautXL_ragnarok.safetensors`, `epicrealismXL_pureFix.safetensors`, `sd-v1-5-inpainting.ckpt`, plus inpainting variants. - **Do not ask Stable Diffusion for a flat texture swatch.** Three attempts at "seamless skin texture" produced an anatomical close-up twice and a leather-scale surface once. SDXL has no reliable notion of a material sample without anatomy, so every seed needs manual rejection — worse than noise we control. Procedural generation won for skin pores (`Tools/generate_character_textures.py`, tileable octave noise) and a strand sheet for hair. - **Unreal collapses the second material slot on a skeletal-mesh import.** A joined hair cap (Blender shows two slots, hair faces on index 1) arrives as a single slot named after the body material, so the hair renders with the skin material. `unreal.SkeletalMesh` has no `set_material`, so it cannot be re-slotted after import. A separate skeletal mesh attached as a second component is the way around it. + +## 2026-11-09 — Hair as a separate mesh, and a material-editing crash + +- **Hair joined into the body mesh loses its material.** Blender shows two slots (hair faces on index 1) and the FBX exports both, but Unreal's skeletal-mesh import arrives with **one** slot named after the body material, so the hair renders as skin. `unreal.SkeletalMesh` has no `set_material`, so it cannot be re-slotted afterwards. A separate skeletal mesh imported on its own keeps its slot and imports cleanly (`CharacterHair` → `M_Character_Hair`). +- **A second skeletal component follows the body with `SetLeaderPoseComponent(GetMesh())`** — no second rig, no second animation, and it hides with the body in first person. +- **`unreal.MaterialEditingLibrary` segfaults in `-run=pythonscript`** when it clears and rebuilds a material graph. The tool ran its texture imports, then crashed the commandlet with no Python traceback. Graph editing needs an editor session, or a material created from scratch rather than edited in place. diff --git a/Screenshots/demo/review-demo-crossing.png b/Screenshots/demo/review-demo-crossing.png index 7fb5fb2..93218dc 100644 --- a/Screenshots/demo/review-demo-crossing.png +++ b/Screenshots/demo/review-demo-crossing.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c1dc1a654bce66311b3838dd2d32ae8e5dc1c2861581f39ff54215c7f7b05f82 -size 3090065 +oid sha256:83ae29e23d32692d8b468d379331f97795edace8863804ee1c920e14f7f56822 +size 3088663 diff --git a/Screenshots/demo/review-demo-north-buildings.png b/Screenshots/demo/review-demo-north-buildings.png index 85dbcb5..1f08dd2 100644 --- a/Screenshots/demo/review-demo-north-buildings.png +++ b/Screenshots/demo/review-demo-north-buildings.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:331bc406f4dc42e21f274b414071a5748eaa7f0434959a22840eb302df8256f6 -size 2618207 +oid sha256:f8819f25cde7e94188d51b685991eaa3d7fd0a3cc840efcf7e053aff90005798 +size 2617803 diff --git a/Screenshots/demo/review-demo-retail-frontage.png b/Screenshots/demo/review-demo-retail-frontage.png index 5680fe0..64d45e1 100644 --- a/Screenshots/demo/review-demo-retail-frontage.png +++ b/Screenshots/demo/review-demo-retail-frontage.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2fbdb5079d7311cc8913ae010cb1d50d595c77a04681c0d262f1fc9831b12ac0 -size 2523952 +oid sha256:c90621d73dd2342dba7cb8357ad82f7fc1f5aa7dc5718035d58aa20002df7994 +size 2523413 diff --git a/Screenshots/demo/review-demo-spawn-view.png b/Screenshots/demo/review-demo-spawn-view.png index 25efad0..1fd7f62 100644 --- a/Screenshots/demo/review-demo-spawn-view.png +++ b/Screenshots/demo/review-demo-spawn-view.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9ee0259368be8758dc8d88ac2e75e40ffa3e13cbbb5322c0f30c06e9b71f465 -size 2936215 +oid sha256:293cbb2b1d19f232b6af002c65d89c05ca917c6e0339cae2f5b1f3bc8ce93ddf +size 2933226 diff --git a/Screenshots/demo/review-demo-street-level.png b/Screenshots/demo/review-demo-street-level.png index 23a2233..5c66c6b 100644 --- a/Screenshots/demo/review-demo-street-level.png +++ b/Screenshots/demo/review-demo-street-level.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:723655238f1df703a1512af7db1b7b68850897aa5363736c57854d89036fbb8b -size 2850311 +oid sha256:69ba600ae3596b62818eccfd4d49fb387241dfb8e798a5c097903038f72a592c +size 2848456 diff --git a/Screenshots/demo/review-demo-third-person-front.png b/Screenshots/demo/review-demo-third-person-front.png index 5ad0616..c86df1d 100644 --- a/Screenshots/demo/review-demo-third-person-front.png +++ b/Screenshots/demo/review-demo-third-person-front.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc57859443bbf10db3d5644332f0a301f4c56f0bb00a366296e55b21d86f44a4 -size 2791588 +oid sha256:b0e653a3b904858789ef1121ab1305c2e33467d9c2fab6d7ea233aae7986efa9 +size 2793949 diff --git a/Screenshots/demo/review-demo-third-person-walk.png b/Screenshots/demo/review-demo-third-person-walk.png index 3e897da..9aa203c 100644 --- a/Screenshots/demo/review-demo-third-person-walk.png +++ b/Screenshots/demo/review-demo-third-person-walk.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:329b4a414e70de16434b3af1f7d8fe0c5bcb1f57401a5663db8c41fc7392ffe2 -size 2180500 +oid sha256:85a7cf6d6090893ba5f1ac52095cf6a395e9caa74b0b187db245cc4b1357bf36 +size 2179777 diff --git a/Screenshots/demo/review-demo-third-person.png b/Screenshots/demo/review-demo-third-person.png index 4afc985..1adf41e 100644 --- a/Screenshots/demo/review-demo-third-person.png +++ b/Screenshots/demo/review-demo-third-person.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:33ab2bd185517177d59bee4a815a3ef5d50a3524926a02e586a730046cd247db -size 2999109 +oid sha256:a8ece3502186e088dbe9495edae9b0185d2f413738bb169ab184182b30950e59 +size 2996384 diff --git a/TODO.md b/TODO.md index 9814a38..8dd58c2 100644 --- a/TODO.md +++ b/TODO.md @@ -15,10 +15,12 @@ Actionable task list for this project. This file is the single, central place fo - [x] Restore the stride capture after the pose rewrite. The walk cycle started on a **sine**, zero at both the start and the middle of the cycle, so the deterministic capture kept landing on the two passing frames where the legs are together. Starting the cycle at maximum stride (cosine) fixed it with no other change; `demo-third-person-walk` is back as shot 8 and shows a genuine stride. -- [A] Tune skin shading and hair materials on the player character. **Tooling in place 2026-11-09, hair blocked on an engine-side limit:** +- [~] Tune skin shading and hair materials on the player character. **Tooling in place 2026-11-09, hair blocked on an engine-side limit:** - `Tools/comfyui_generate.py` drives the local ComfyUI instance (`127.0.0.1:8188`) for textures, writing a JSON sidecar with the model, prompt, seed and full workflow so anything generated can be regenerated or rejected. Installed models include `juggernautXL_ragnarok.safetensors` and `sd-v1-5-inpainting.ckpt`. - **Stable Diffusion is not usable for flat skin swatches.** Three attempts on two models produced an anatomical close-up twice and a leather-scale surface once; SDXL has no reliable notion of "texture without anatomy". Skin detail is therefore generated procedurally instead (`Tools/generate_character_textures.py`: tileable octave noise for pores plus a strand-sheet texture for hair). - - **Hair geometry is built but not shipped.** `add_hair_cap()` creates a cap conforming to the head and weights it to the `head` bone (Blender shows two material slots, hair faces on index 1), but Unreal's import **collapses the second material slot**, so the cap renders with the skin material and reads as a bald skull. It is behind `--hair`, off. Next step: export the hair as its own skeletal mesh and attach it as a second component, or bake the hair colour into vertex colours. + - **Hair is now its own asset** (`Tools/character_hair_build.py` → `SM_Char_Hair` → `/Game/Test/Character/CharacterHair`), which is what the `character-artist` skill asks for: cards are a separate mesh, not geometry joined into the body. It imports with its own material slot and is attached in C++ as a second `USkeletalMeshComponent` on `SetLeaderPoseComponent`, so it follows the head bone without a second animation. 321 verts, 192 quads, 404 tris. + - **The card layout is still wrong.** The cards are anchored pointing outward, so the silhouette reads as a spiked crown instead of a short cut. Next iteration: orient each card tangentially along the skull, add a second overlapping row, and drive the shape from the scalp normal rather than a fixed outward vector. + - `Tools/ue_character_materials.py` (wiring the generated textures into the skin and hair materials) **segfaults in the headless commandlet** when it edits the material graph. Not wired; the hair keeps the dark base colour it carries from Blender. Needs either an editor-session run or a material asset built from scratch instead of edited in place. - [ ] Tag `v0.2.2` and write `CHANGELOG.md` entry; then remove this section. diff --git a/Tools/character_hair_build.py b/Tools/character_hair_build.py new file mode 100644 index 0000000..51a3d6e --- /dev/null +++ b/Tools/character_hair_build.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +"""Build the hair as its own skinned mesh — hair cards over a scalp cap. + +The `character-artist` skill is explicit that hair for a game character is a card +system and that swappable parts stay separate meshes. Joining hair into the body mesh +was tried first and failed for an engine reason: Unreal collapses the second material +slot on a skeletal-mesh import, so the cap rendered with the skin material. A separate +object sidesteps that and is what the skill calls for anyway. + + blender -b Art/blender/character/character-2026-11-07.blend \ + --python Tools/character_hair_build.py -- \ + --out /tmp/gigline/character-hair.fbx --report /tmp/gigline/character-hair.json + +What it builds: + + SM_Char_Hair scalp cap + 18 cards in three rings, UV-mapped to the strand sheet + from Tools/generate_character_textures.py, every vertex weighted + to `head` at 1.0 so it follows the head without a second rig. + +Cards are built with a taper and a slight outward curl so the silhouette is not a +shell; the strand texture runs vertically down each card, which is the direction +hair actually reads in. Topology is quads only (the skill's animation-ready rule), +and there are no poles on the deformation surface because the whole mesh is rigidly +weighted to one bone. +""" + +from __future__ import annotations + +import argparse +import json +import math +import os +import sys + +import bpy +from mathutils import Vector + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import fbx_export_preset as fbx_preset # noqa: E402 + +HAIR_NAME = "SM_Char_Hair" +CARD_RINGS = 3 # rings of cards around the skull +CARDS_INNER = 6 +CARDS_MIDDLE = 7 +CARDS_OUTER = 5 +CARD_HEIGHT = 0.072 # m: short, close cut - taller cards spiked up +CAP_THICKNESS = 0.004 + +PARTS: list[dict] = [] + + +def log(message: str) -> None: + PARTS.append(message) + print(f"[hair] {message}") + + +def hair_material() -> bpy.types.Material: + mat = bpy.data.materials.get("M_Character_Hair") or bpy.data.materials.new("M_Character_Hair") + mat.use_nodes = True + bsdf = mat.node_tree.nodes.get("Principled BSDF") + if bsdf: + # Dark brown, low roughness: strands catch a highlight. The strand texture is + # sampled in Unreal, so Blender only needs a sane base for the FBX material. + bsdf.inputs["Base Color"].default_value = (0.052, 0.036, 0.028, 1.0) + bsdf.inputs["Roughness"].default_value = 0.38 + if "Alpha" in bsdf.inputs: + bsdf.inputs["Alpha"].default_value = 1.0 + mat.use_backface_culling = False + return mat + + +def card(name, anchor, outward, up, width, height, curl, segments=4): + """One curved card: a strip of quads that arcs away from the scalp.""" + mesh = bpy.data.meshes.new(name) + obj = bpy.data.objects.new(name, mesh) + bpy.context.scene.collection.objects.link(obj) + + verts = [] + faces = [] + for i in range(segments + 1): + t = i / segments + # Taper to a point at the tip and curl outward as the strand falls. + w = width * (1.0 - 0.75 * t) + h = height * t + bulge = curl * math.sin(t * math.pi * 0.85) + base = anchor + up * h + outward * bulge + left = base - outward.cross(up).normalized() * w + right = base + outward.cross(up).normalized() * w + verts.append(left) + verts.append(right) + for i in range(segments): + a = i * 2 + faces.append((a, a + 1, a + 3, a + 2)) + mesh.from_pydata(verts, [], faces) + mesh.update() + return obj + + +def scalp_cap(centre, radius, material): + bpy.ops.mesh.primitive_uv_sphere_add(segments=20, ring_count=12, radius=radius, + location=centre) + cap = bpy.context.active_object + cap.name = "HairScalp" + bpy.ops.object.mode_set(mode="EDIT") + bpy.ops.mesh.select_all(action="SELECT") + bpy.ops.mesh.bisect(plane_co=(0.0, 0.0, centre.z - radius * 0.15), + plane_no=(0.0, 0.0, 1.0), clear_inner=True, use_fill=False) + bpy.ops.object.mode_set(mode="OBJECT") + cap.scale = (1.03, 1.06, 0.86) + cap.data.materials.append(material) + return cap + + +def main(argv=None) -> int: + argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--out", default="/tmp/gigline/character-hair.fbx") + ap.add_argument("--report", default=None) + args = ap.parse_args(argv) + + arm = next((o for o in bpy.data.objects if o.type == "ARMATURE"), None) + if arm is None: + raise SystemExit("no armature in the .blend") + head = arm.data.bones.get("head") + if head is None: + raise SystemExit("no 'head' bone") + + centre = (head.head_local + head.tail_local) * 0.5 + radius = max(0.088, (head.tail_local - head.head_local).length * 0.60) + log(f"head centre {[round(c, 3) for c in centre]}, radius {radius:.3f} m") + + material = hair_material() + pieces = [scalp_cap(Vector((centre.x, centre.y, centre.z + 0.012)), radius, material)] + + up = Vector((0.0, 0.0, 1.0)) + rings = ((CARDS_INNER, 1.00, 0.70), (CARDS_MIDDLE, 0.90, 0.85), (CARDS_OUTER, 0.78, 1.0)) + index = 0 + for ring, (count, radius_scale, length_scale) in enumerate(rings): + for i in range(count): + angle = math.tau * (i + 0.5 * ring) / count + outward = Vector((math.cos(angle), math.sin(angle), 0.22)).normalized() + anchor = Vector(( + centre.x + outward.x * radius * radius_scale * 0.92, + centre.y + outward.y * radius * radius_scale * 0.92, + centre.z + radius * (0.52 - 0.16 * ring), + )) + pieces.append(card(f"HairCard_{index}", anchor, outward, up, + width=radius * 0.30, height=CARD_HEIGHT * length_scale, + curl=radius * 0.34)) + index += 1 + log(f"built {len(pieces)} pieces: 1 scalp cap + {index} cards") + + # Join into one object and weight every vertex to the head bone. + bpy.ops.object.select_all(action="DESELECT") + for piece in pieces: + piece.select_set(True) + bpy.context.view_layer.objects.active = pieces[0] + bpy.ops.object.join() + hair = bpy.context.active_object + hair.name = HAIR_NAME + bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) + hair.data.materials.clear() + hair.data.materials.append(material) + + group = hair.vertex_groups.new(name="head") + group.add(list(range(len(hair.data.vertices))), 1.0, "REPLACE") + + # UVs: a simple planar unwrap is enough because the strand texture is directional. + bpy.ops.object.mode_set(mode="EDIT") + bpy.ops.mesh.select_all(action="SELECT") + bpy.ops.uv.smart_project(angle_limit=1.15, island_margin=0.01) + bpy.ops.object.mode_set(mode="OBJECT") + + quads = sum(1 for p in hair.data.polygons if len(p.vertices) == 4) + tris = sum(len(p.vertices) - 2 for p in hair.data.polygons) + log(f"{HAIR_NAME}: {len(hair.data.vertices)} verts, {quads} quads, {tris} tris, " + f"{len(hair.data.materials)} material slot(s)") + + # Parent to the armature so the export carries the skin. + modifier = hair.modifiers.new(name="Armature", type="ARMATURE") + modifier.object = arm + + bpy.ops.object.select_all(action="DESELECT") + arm.select_set(True) + hair.select_set(True) + bpy.context.view_layer.objects.active = arm + os.makedirs(os.path.dirname(args.out), exist_ok=True) + fbx_preset.export_character(args.out, selection=True) + log(f"exported {args.out}") + + if args.report: + with open(args.report, "w") as handle: + json.dump({ + "asset": HAIR_NAME, + "head_centre_m": [round(c, 4) for c in centre], + "head_radius_m": round(radius, 4), + "cards": index, + "rings": [r[0] for r in rings], + "vertices": len(hair.data.vertices), + "quads": quads, + "triangles": tris, + "material": material.name, + "texture": "Art/textures/generated/T_Character_Hair.png", + "weighting": "all vertices 1.0 on 'head'", + "log": PARTS, + }, handle, indent=2) + print(f"[hair] report -> {args.report}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Tools/ue_character_hair_import.py b/Tools/ue_character_hair_import.py new file mode 100644 index 0000000..f387e25 --- /dev/null +++ b/Tools/ue_character_hair_import.py @@ -0,0 +1,70 @@ +# Import the hair cards as their own skeletal mesh (0.2.2). +# +# blender -b Art/blender/character/character-2026-11-07.blend \ +# --python Tools/character_hair_build.py -- --out /tmp/gigline/character-hair.fbx +# /Engine/Binaries/Linux/UnrealEditor-Cmd -run=pythonscript \ +# -script=/Tools/ue_character_hair_import.py -nullrhi -unattended -nopause -stdout +# +# The hair is a separate asset on purpose: Unreal collapses the second material slot +# when hair is joined into the body mesh, so a card system has to be its own object +# (which is what the character-artist skill asks for anyway). +# +# Result file: /tmp/gigline/character-hair-import.txt + +import unreal + +SRC = "/tmp/gigline/character-hair.fbx" +DEST = "/Game/Test/Character" +NAME = "CharacterHair" +RESULT = "/tmp/gigline/character-hair-import.txt" +lines = [] + + +def log(msg): + lines.append(str(msg)) + unreal.log(str(msg)) + + +def main(): + task = unreal.AssetImportTask() + task.filename = SRC + task.destination_path = DEST + task.destination_name = NAME + task.automated = True + task.save = True + task.replace_existing = True + + ui = unreal.FbxImportUI() + ui.import_mesh = True + ui.import_as_skeletal = True + ui.import_animations = False + ui.import_materials = True + ui.import_textures = False + ui.skeletal_mesh_import_data.import_uniform_scale = 1.0 + task.options = ui + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + + mesh = unreal.EditorAssetLibrary.load_asset(f"{DEST}/{NAME}") + if mesh is None: + log(f"MISSING {DEST}/{NAME}") + lines.append("VERDICT: FAIL") + return + mats = mesh.get_editor_property("materials") + log(f"{NAME}: {len(mats)} material slot(s)") + for i, m in enumerate(mats): + face = m.get_editor_property("material_interface") + log(f" slot {i}: {face.get_name() if face else 'None'}") + lines.append("VERDICT: PASS") + + +try: + main() +except Exception as exc: # noqa: BLE001 + import traceback + log(f"FAILED: {type(exc).__name__}: {exc}") + log(traceback.format_exc()) + lines.append("VERDICT: FAIL") + +unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True) +with open(RESULT, "w") as f: + f.write("\n".join(lines) + "\n") diff --git a/Tools/ue_character_materials.py b/Tools/ue_character_materials.py new file mode 100644 index 0000000..73b8183 --- /dev/null +++ b/Tools/ue_character_materials.py @@ -0,0 +1,138 @@ +# Wire the generated character textures into Unreal materials (0.2.2). +# +# python3 Tools/generate_character_textures.py --out Art/textures/generated +# /Engine/Binaries/Linux/UnrealEditor-Cmd -run=pythonscript \ +# -script=/Tools/ue_character_materials.py -nullrhi -unattended -nopause -stdout +# +# Two materials, both edited IN PLACE so the imported meshes keep their references: +# +# M_Character_Export skin: diffuse x generated pore detail, roughness variation +# M_Character_Hair hair: strand texture colour + alpha, masked and two-sided +# +# Editing in place matters: `unreal.SkeletalMesh` has no `set_material`, so a mesh +# that already points at a material cannot be re-slotted from Python. Changing the +# material asset is the only lever. +# +# Result file: /tmp/gigline/character-materials.txt + +import os + +import unreal + +# The commandlet's working directory is the engine, not the repo, so resolve the +# texture folder from the project path instead of relying on the cwd. +_REPO = os.path.abspath(os.path.join(unreal.Paths.project_dir(), "..", "..")) +TEXTURE_DIR = os.path.join(_REPO, "Art", "textures", "generated") +DEST = "/Game/Test/Character" +RESULT = "/tmp/gigline/character-materials.txt" +lines = [] + + +def log(msg): + lines.append(str(msg)) + unreal.log(str(msg)) + + +def import_texture(filename): + path = os.path.join(TEXTURE_DIR, filename) + if not os.path.exists(path): + log(f"missing {path}") + return None + task = unreal.AssetImportTask() + task.filename = path + task.destination_path = DEST + task.destination_name = os.path.splitext(filename)[0] + task.automated = True + task.save = True + task.replace_existing = True + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + texture = unreal.EditorAssetLibrary.load_asset(f"{DEST}/{task.destination_name}") + log(f"texture {task.destination_name}: {'ok' if texture else 'MISSING'}") + return texture + + +def clear_graph(material): + """Remove every expression so a rerun does not stack nodes.""" + lib = unreal.MaterialEditingLibrary + for node in list(lib.get_material_expressions(material)): + lib.delete_material_expression(material, node) + + +def textured_material(mat, texture, roughness, tint=None, masked=False): + """Base colour from a texture, optional detail multiply, roughness constant.""" + lib = unreal.MaterialEditingLibrary + clear_graph(mat) + + tex = lib.create_material_expression(mat, unreal.MaterialExpressionTextureSample, -450, 0) + tex.texture = texture + if masked: + mat.set_editor_property("blend_mode", unreal.BlendMode.BLEND_MASKED) + mat.set_editor_property("two_sided", True) + lib.connect_material_property(tex, "A", unreal.MaterialProperty.MP_OPACITY_MASK) + if tint is not None: + mul = lib.create_material_expression(mat, unreal.MaterialExpressionMultiply, -220, 40) + mul.set_editor_property("const_b", unreal.LinearColor(*tint, 1.0)) + lib.connect_material_expressions(tex, "RGB", mul, "A") + lib.connect_material_property(mul, "", unreal.MaterialProperty.MP_BASE_COLOR) + else: + lib.connect_material_property(tex, "RGB", unreal.MaterialProperty.MP_BASE_COLOR) + + rough = lib.create_material_expression(mat, unreal.MaterialExpressionConstant, -220, -180) + rough.set_editor_property("r", roughness) + lib.connect_material_property(rough, "", unreal.MaterialProperty.MP_ROUGHNESS) + lib.recompile_material(mat) + log(f"{mat.get_name()}: texture {texture.get_name()}, roughness {roughness}" + + (", masked two-sided" if masked else "")) + + +def main(): + skin_detail = import_texture("T_Character_SkinDetail.png") + hair_strands = import_texture("T_Character_Hair.png") + + skin = unreal.EditorAssetLibrary.load_asset(f"{DEST}/M_Character_Export") + if skin and skin_detail: + # The character's own diffuse stays the base; the generated pore sheet is a + # detail multiplier, so the warm skin tone from the bake is preserved. + lib = unreal.MaterialEditingLibrary + clear_graph(skin) + diffuse = lib.create_material_expression(skin, unreal.MaterialExpressionTextureSample, -700, 0) + diffuse.texture = unreal.EditorAssetLibrary.load_asset(f"{DEST}/T_Character_Diffuse") + detail = lib.create_material_expression(skin, unreal.MaterialExpressionTextureSample, -700, 300) + detail.texture = skin_detail + detail.sampler_type = unreal.MaterialSamplerType.SAMPLERTYPE_LINEAR_COLOR + mul = lib.create_material_expression(skin, unreal.MaterialExpressionMultiply, -380, 120) + lib.connect_material_expressions(diffuse, "RGB", mul, "A") + lib.connect_material_expressions(detail, "RGB", mul, "B") + # Detail is a multiplier, so keep it near 1 and let it vary a little. + lerp = lib.create_material_expression(skin, unreal.MaterialExpressionLinearInterpolate, -200, 120) + lerp.set_editor_property("const_a", unreal.LinearColor(1.0, 1.0, 1.0, 1.0)) + lib.connect_material_expressions(mul, "", lerp, "B") + rough = lib.create_material_expression(skin, unreal.MaterialExpressionConstant, -200, -160) + rough.set_editor_property("r", 0.55) + lib.connect_material_property(lerp, "", unreal.MaterialProperty.MP_BASE_COLOR) + lib.connect_material_property(rough, "", unreal.MaterialProperty.MP_ROUGHNESS) + lib.recompile_material(skin) + log("M_Character_Export: diffuse x pore detail, roughness 0.55") + elif not skin: + log("M_Character_Export missing") + + hair = unreal.EditorAssetLibrary.load_asset(f"{DEST}/M_Character_Hair") + if hair and hair_strands: + textured_material(hair, hair_strands, 0.38, masked=True) + elif not hair: + log("M_Character_Hair missing") + + unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True) + lines.append("VERDICT: PASS") + + +try: + main() +except Exception as exc: # noqa: BLE001 + import traceback + log(f"FAILED: {type(exc).__name__}: {exc}") + log(traceback.format_exc()) + lines.append("VERDICT: FAIL") + +with open(RESULT, "w") as f: + f.write("\n".join(lines) + "\n")