2019-10-07 14:46:26 sddxccj 阅读数 225
  • 从这里开始虚幻4-Editor介绍 v4.18

    本课程系列取名英译系列,是录制人员参考国外英文原版经典教程,结合中国人的习惯录制而成。希望能够给大家以帮助。从这里开始虚幻4系列教程,是Unreal的官方发布的入门教学,非常经典,是学习Unreal的佳入口。

    2396 人正在学习 去看看 杨石兴

《楚留香》《逆水寒》《天涯明月刀》等一批武侠游戏都将捏脸系统作为了标配,并且开放了大量的参数给玩家,从而能够自由的发挥自己的想象力,捏出一堆鬼脸~在知乎(《Honey Select》)以及其他文章里对捏脸的原理进行了详细的分析,本文呢,主要记录基于骨骼的捏脸在Unreal4中的实现。

原理

基于调整骨骼进行捏脸的核心就是修改脸部骨骼的Scale、Rotation,Position,从而改变骨骼对应的蒙皮的顶点的位置,以达到捏脸的效果:

上图是在动画蓝图里添加一个内置的改变骨骼的节点(下图)来修改鼻子的x坐标的scale 的效果:
看起来捏脸也就这么回事了!但是呢,要想达到游戏中千人千面的效果,基于骨骼的捏脸有以下几点要求:
  1. 需要设计一套有足够表达能力的骨骼以及细致的脸部蒙皮
  2. 大量的骨骼对应的大量参数带来的自由度过高,不易调节,应便于用户调节
  3. 性能消耗相对较少
  4. 跟现有的动画系统以及基于blendshape的表情兼容
  5. 如果有AI能力,根据用户提供的照片自动生成对应的模型是最好不过的了
  6. 有一套对应的妆容方案

其中 1 主要由3D建模师操作,另外对于脸部的对称部分,设计其对应的骨骼为对称骨骼,从而方便调节;对于第二条,大部分的游戏会设计一套叫做controller的第二层骨骼,每个controller同时操纵多根骨骼的多个参数的不同组合来调节局部区域,controller1控制眼部的整体的大小,需要添加眼部骨骼到controller控制的骨骼的列表中,controller的示意图如下:
在这里插入图片描述
这样用户通过操纵controller的滑竿便可以一次性调节一个局部区域,实际上,通过二层骨骼我们降低了局部骨骼参数的自由度,从而方便用户精细的调整角色脸部的细节表情。举例:controller1通过控制三根骨骼的缩放参数来达到整体调节眼部大小的目的:
在这里插入图片描述
3暂且按下不表;接下来4的话会涉及到如何在unreal里实现捏脸,因此会展开详细记录。

Unreal实现

分为捏脸部分和与动画系统融合部分

捏脸部分

首先,开篇所述的直接用ModifyBone蓝图节点来修改每根骨骼的话,对于程序非常的不友好,为了捏脸的效果和充分的表达能力,SkeletalMesh中通常设置较多的骨骼,因此直接使用ModifyBone节点是不太方便的。
我们整体的逻辑应该是这样:

  1. 根据json文件解析出的controller生成所有的调节滑杆,并加载其默认值;
  2. 如果滑杆值发生变化,则对应线性插值或者样条插值该controller对应的所有的骨骼的对应的参数;
  3. 然后将变化的相对Transform更新到骨架的transform上;
  4. Rendering。

第一步和第二步实现比较简单,略去。对于第三步在Unreal中针对骨架有多套数据结构,从捏脸的方便性上来说,这里我们选择PoseableMesh来操作,查看PoseableMeshComponent.h的源码,可以看到以下函数:

class ENGINE_API UPoseableMeshComponent : public USkinnedMeshComponent
	{
	GENERATED_UCLASS_BODY()

	/** Temporary array of local-space (ie relative to parent bone) rotation/translation/scale for each bone. */
	TArray<FTransform> BoneSpaceTransforms;

	UFUNCTION(BlueprintCallable, Category="Components|PoseableMesh")
	void SetBoneTransformByName(FName BoneName, const FTransform& InTransform, EBoneSpaces::Type BoneSpace);

	UFUNCTION(BlueprintCallable, Category="Components|PoseableMesh") 
	FTransform GetBoneTransformByName(FName BoneName, EBoneSpaces::Type BoneSpace);

	UFUNCTION(BlueprintCallable, Category="Components|PoseableMesh")
	void ResetBoneTransformByName(FName BoneName);

	UFUNCTION(BlueprintCallable, Category="Components|PoseableMesh")
	void CopyPoseFromSkeletalComponent(const USkeletalMeshComponent* InComponentToCopy);
	};

可以看到利用PoseableMesh我们可以方便的操纵Transform,从而达到捏脸的目的。下面放两张Demo的截图,左侧为直接调节单根骨骼,右侧为调节controller:

在这里插入图片描述在这里插入图片描述

与动画系统的融合

PoseableMesh虽好,可不要贪杯哦(划掉),但是不支持动画,不支持Blendshape,换句话说,PoseableMesh就像专门的一套方便处理骨架transform的数据结构,其他的功能还是交由SkeletalMesh来做,那么问题就来了,如何将那捏脸的数据传到SkeletalMesh中,从而与动画以及BlendShape融合呢?这里我选择在AnimationBlueprint里实现一个自定义的AnimNode ModifyTransform来将PoseableMesh处理好的捏脸数据喂到SkeletalMesh的Render_Thread中,整个流程如下图所示:
在这里插入图片描述

  1. 因为我们有两套mesh来处理不同的数据,因此在蓝图中我们选择挂载俩mesh,将其中的PoseableMesh设为不可见:
    在这里插入图片描述
  2. 新建一个UAvatarAnimInstance继承自UAnimInstance,并添加以下数据:
UCLASS()
	class AVATAR_UE4_API UAvatarAnimInstance : public UAnimInstance
	{
		GENERATED_BODY()
		public:
		UAvatarAnimInstance(const FObjectInitializer& ObjectInitializer);

		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = BoneTransform)
		TArray<FVector> BonesTranslation;

		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = BoneTransform)
		TArray<FRotator> BonesRotation;

		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = BoneTransform)
		TArray<FVector> BonesScale;

		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = BoneTransform)
		TArray<FName> BonesName;
	};
  1. 在计算完捏脸滑杆逻辑即将捏脸的transform更新到PoseableMesh后,添加并调用以下函数将数据传入到AnimInstance中:
void UAutoPinch::TransformBoneData2AnimInstance()
	{
		if(Animation)
		{ 
			for (int i = 0; i < Animation->BonesName.Num(); i++)
			{
				Animation->BonesTranslation[i] = PoseableMesh->GetBoneLocationByName(Animation->BonesName[i], EBoneSpaces::ComponentSpace);
				Animation->BonesRotation[i] = PoseableMesh->GetBoneRotationByName(Animation->BonesName[i], EBoneSpaces::ComponentSpace);
				Animation->BonesScale[i] = PoseableMesh->GetBoneScaleByName(Animation->BonesName[i], EBoneSpaces::ComponentSpace);
			}
		}
	}
  1. 创建一个蓝图类继承自UAvatarAnimInstance,并将其指定为第一步中的skeletalMesh的AnimClass中的动画蓝图的父类。
  2. 接下来创建自定义动画蓝图节点,主要分为编辑器部分和runtime部分,编辑器部分的创建可参考其他文档,这里我们只记录如何创建自定义动画蓝图节点的runtime部分。
  3. 创建FAnimNode_ModifyTransform类继承自FAnimNode_SkeletalControlBase
USTRUCT()
	struct AVATAR_UE4_API FAnimNode_ModifyTransform :public FAnimNode_SkeletalControlBase
	{
	GENERATED_USTRUCT_BODY()
	public:
	
	/*New Transform to use*/
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Translation, meta = (PinShownByDefault))
		FBonesTransfroms BonesTransfroms;

	/** Whether and how to modify the translation of this bone. */
	UPROPERTY(EditAnywhere, Category = Translation)
		TEnumAsByte<EBoneModificationMode> TranslationMode;

	/** Whether and how to modify the translation of this bone. */
	UPROPERTY(EditAnywhere, Category = Rotation)
		TEnumAsByte<EBoneModificationMode> RotationMode;

	/** Whether and how to modify the translation of this bone. */
	UPROPERTY(EditAnywhere, Category = Scale)
		TEnumAsByte<EBoneModificationMode> ScaleMode;

	/** Reference frame to apply Translation in. */
	UPROPERTY(EditAnywhere, Category = Translation)
		TEnumAsByte<enum EBoneControlSpace> TranslationSpace;

	/** Reference frame to apply Rotation in. */
	UPROPERTY(EditAnywhere, Category = Rotation)
		TEnumAsByte<enum EBoneControlSpace> RotationSpace;

	/** Reference frame to apply Scale in. */
	UPROPERTY(EditAnywhere, Category = Scale)
		TEnumAsByte<enum EBoneControlSpace> ScaleSpace;
	FAnimNode_ModifyTransform();
	//  // FAnimNode_Base interface  
	virtual void GatherDebugData(FNodeDebugData& DebugData) override;
	//  // End of FAnimNode_Base interface  

		// FAnimNode_SkeletalControlBase interface  
	virtual void EvaluateSkeletalControl_AnyThread(FComponentSpacePoseContext& Output, TArray<FBoneTransform>& OutBoneTransforms) override;
	
	bool IsValidToEvaluate(const USkeleton* Skeleton, const FBoneContainer& RequiredBones) override;
	// End of FAnimNode_SkeletalControlBase interface  

	private:
	// FAnimNode_SkeletalControlBase interface  

	virtual void InitializeBoneReferences(const FBoneContainer& RequiredBones) override;
	// End of FAnimNode_SkeletalControlBase interface  

	};
  1. 因为自定义蓝图节点不支持TArray做为输入,这里我们创建一个struct用来接收AnimInstance中传过来的数据:
USTRUCT(BlueprintType)
	struct FBonesTransfroms
	{
	GENERATED_BODY()
		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BonesTransfroms")
		TArray<FName> BonesName;
		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BonesTransfroms")
		TArray<FVector> BonesTranslation;
		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BonesTransfroms")
		TArray<FVector> BonesScale;
		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BonesTransfroms")
		TArray<FRotator> BonesRotation;
	};
  1. 然后实现FAnimNode_ModifyTransform中的虚函数,其中最重要的就是EvaluateSkeletalControl_AnyThread:
void FAnimNode_ModifyTransform::EvaluateSkeletalControl_AnyThread(FComponentSpacePoseContext & Output, TArray<FBoneTransform>& OutBoneTransforms)
	{
	check(OutBoneTransforms.Num() == 0);
	// the way we apply transform is same as FMatrix or FTransform
	// we apply scale first, and rotation, and translation
	// if you'd like to translate first, you'll need two nodes that first node does translate and second nodes to rotate.
	const FBoneContainer& RequiredBones = Output.AnimInstanceProxy->GetRequiredBones();
	const FBoneContainer& BoneContainer = Output.Pose.GetPose().GetBoneContainer();
	for (int i=0;i<BonesTransfroms.BonesName.Num();i++)
	{
		auto name = BonesTransfroms.BonesName[i];
		FBoneReference MyBoneToModify(name);
		auto ret = MyBoneToModify.Initialize(RequiredBones);
	
		FCompactPoseBoneIndex CompactPoseBoneToModify = MyBoneToModify.GetCompactPoseIndex(BoneContainer);
		FTransform NewBoneTM = Output.Pose.GetComponentSpaceTransform(CompactPoseBoneToModify);
		FTransform ComponentTransform = Output.AnimInstanceProxy->GetComponentTransform();
	
		FVector Scale = BonesTransfroms.BonesScale[i];
		FVector Translation = BonesTransfroms.BonesTranslation[i];
		FQuat Rotation(BonesTransfroms.BonesRotation[i]);
		
		if (ScaleMode != BMM_Ignore)
		{
			// Convert to Bone Space.
			FAnimationRuntime::ConvertCSTransformToBoneSpace(ComponentTransform, Output.Pose, NewBoneTM, CompactPoseBoneToModify, ScaleSpace);

			if (ScaleMode == BMM_Additive)
			{
				NewBoneTM.SetScale3D(NewBoneTM.GetScale3D() * Scale);
			}
			else
			{
				NewBoneTM.SetScale3D(Scale);
			}

			// Convert back to Component Space.
			FAnimationRuntime::ConvertBoneSpaceTransformToCS(ComponentTransform, Output.Pose, NewBoneTM, CompactPoseBoneToModify, ScaleSpace);
		}
		if (RotationMode != BMM_Ignore)
		{
			// Convert to Bone Space.
			FAnimationRuntime::ConvertCSTransformToBoneSpace(ComponentTransform, Output.Pose, NewBoneTM, CompactPoseBoneToModify, RotationSpace);

			const FQuat BoneQuat(Rotation);
			if (RotationMode == BMM_Additive)
			{
				NewBoneTM.SetRotation(BoneQuat * NewBoneTM.GetRotation());
			}
			else
			{
				NewBoneTM.SetRotation(BoneQuat);
			}

			// Convert back to Component Space.
			FAnimationRuntime::ConvertBoneSpaceTransformToCS(ComponentTransform, Output.Pose, NewBoneTM, CompactPoseBoneToModify, RotationSpace);
		}
		if (TranslationMode != BMM_Ignore)
		{
			// Convert to Bone Space.
			FAnimationRuntime::ConvertCSTransformToBoneSpace(ComponentTransform, Output.Pose, NewBoneTM, CompactPoseBoneToModify, TranslationSpace);

			if (TranslationMode == BMM_Additive)
			{
				NewBoneTM.AddToTranslation(Translation);
			}
			else
			{
				NewBoneTM.SetTranslation(Translation);
			}

			// Convert back to Component Space.
			FAnimationRuntime::ConvertBoneSpaceTransformToCS(ComponentTransform, Output.Pose, NewBoneTM, CompactPoseBoneToModify, TranslationSpace);
		}

		OutBoneTransforms.Add(FBoneTransform(MyBoneToModify.GetCompactPoseIndex(BoneContainer), NewBoneTM));
	}
	}
  1. 最后在skeletalMesh的动画蓝图中添加以下节点:在这里插入图片描述
  2. 这样整个数据流就跑通了,从捏脸的数据到最后的rendering。
    链接: https://buaaccj.github.io/.
    谢谢。
2018-11-15 17:21:01 xiaoxiaolooi 阅读数 256
  • 从这里开始虚幻4-Editor介绍 v4.18

    本课程系列取名英译系列,是录制人员参考国外英文原版经典教程,结合中国人的习惯录制而成。希望能够给大家以帮助。从这里开始虚幻4系列教程,是Unreal的官方发布的入门教学,非常经典,是学习Unreal的佳入口。

    2396 人正在学习 去看看 杨石兴

unreal GIS 模块

unreal4 源代码模块

自从unreal4 开源以来,osg ogre 渐渐不能用来吃饭,于是本着听人劝吃饱饭的思想,转头unreal门下,继续为混口饭事业,投身革命。可是unreal的c++编程基本没有什么例子可看,只好自己从头做起。

  1. 开发者驱动
  2. 编辑器
  3. 工程目录
  4. 基础框架代码
  5. 第三方库

在这里插入图片描述
这个是文档目录,代码量难以阅读,决定从以下三方向入手,第一次写文字,真心不容易。
1.案例编写使用unreal4 的普通界面搭建自定义三维场景 介绍 editor层的基础代码
2.添加GIS地图属性信息 介绍 驱动层的基础代码
3.程序化植物合成 介绍 与unreal基础平台结合
以后会陆续跟新到这里
推荐大家使用 阿里云 https://promotion.aliyun.com/ntms/yunparter/invite.html?userCode=ic4sfgeu

2016-08-14 01:21:07 hyn89 阅读数 1319
  • 从这里开始虚幻4-Editor介绍 v4.18

    本课程系列取名英译系列,是录制人员参考国外英文原版经典教程,结合中国人的习惯录制而成。希望能够给大家以帮助。从这里开始虚幻4系列教程,是Unreal的官方发布的入门教学,非常经典,是学习Unreal的佳入口。

    2396 人正在学习 去看看 杨石兴

Unreal中的Cross Product使用的是左手定则而非右手定则。

因为从向量积方向的定义上来看,右手定则适用于当X表示前方向时Y表示左方向,而Unreal中Y表示的是右方向,所以按照右手定则来判断Cross Product的方向一定是反的。在Unreal中想要正确判断向量积的方向应该使用左手定则。

2019-11-27 11:50:55 t163361 阅读数 27
  • 从这里开始虚幻4-Editor介绍 v4.18

    本课程系列取名英译系列,是录制人员参考国外英文原版经典教程,结合中国人的习惯录制而成。希望能够给大家以帮助。从这里开始虚幻4系列教程,是Unreal的官方发布的入门教学,非常经典,是学习Unreal的佳入口。

    2396 人正在学习 去看看 杨石兴

原文地址:https://blog.csdn.net/t163361/article/details/103273128

官方视频教程
中文文档地址
Unity开发者学习通道
启动器下载
如何下载编译引擎源码
官方源码地址
里面包含Unreal的源码等开源库
想要下载Unreal源码,需要申请成为Epic Games成员

2014-06-03 21:16:07 u010832643 阅读数 1404
  • 从这里开始虚幻4-Editor介绍 v4.18

    本课程系列取名英译系列,是录制人员参考国外英文原版经典教程,结合中国人的习惯录制而成。希望能够给大家以帮助。从这里开始虚幻4系列教程,是Unreal的官方发布的入门教学,非常经典,是学习Unreal的佳入口。

    2396 人正在学习 去看看 杨石兴

近来新到一个使用Unreal Engine 3的项目,本着熟悉代码的目的,看了一些代码,简单记录一下。

本文主要分析Unreal Engine 3中对于字符串封装后的结构FName,内容主要包含以下3点:

1.FName的作用;

2.FName的具体实现;

3.FName的一些特殊处理


1.FName的作用

FName利用hash table来存贮字符串。
FName对象进行比较时,实际上只需对两个整数进行比较。
查找操作类似于在std::map中根据主键(整数)查找对应的值。效率上有了很明显的改善。

2.FName的具体实现
FName中的关键数据结构
struct  FNameEntry
{
		INT 	Index;
	       FNameEntry*	HashNext;
}
FNameEntry是一个全局的结构,后面会说到的存储结构中都是以FNameEntry为元素存储的。
class  FName
{
		INT 	Index;
		INT	Number;

		static 	TArrayNoInit<FNameEntry*>	Names;
		static	FNameEntry*		NameHash[65536];
}

FName就是今天要分析的重点,这里是几个关键的变量及存储字符串的容器。

Index是Names数组中的索引,用于快速的查找字符串的字符部分,Number是字符串中的数字部分。 


接着,来看FName的Init()函数,Init()函数会在FName的构造函数中调用,一般情况下,使用FName只需传入第一个参数,其他参数在FName的构造函数中填入默认值。FName的Init()函数传入了4个参数,InName字符串中的字符部分,InNumber字符串中的数字部分,FindType表示操作类型,bSplitName分割标示符。传入的字符串在InName中,分割后会将字符保留在InName内,而将数字部分保存在InNumber中。

void FName::Init(const TCHAR* InName, INT InNumber, EFindName FindType, UBOOL bSplitName)
{
  	StaticInit();
  	if (InNumber == NAME_NO_NUMBER_INTERNAL && bSplitName == TRUE)
	{
   		if (SplitNameWithCheck(InName,...))
		{}
	}
	INT iHash;
	iHash = appStrhash( InName ) & ( ARRAY_COUNT(NameHash)-1 );
  	for (FNameEntry* Hash = NameHash[iHash]; Hash; Hash=Hash->HashNext)
	{
  		if( Hash->IsEqual(InName))
		{
			Index = Hash->GetIndex();
			...
		}
	}
	Index = Names.Add();
	Names(Index) = NameHash[iHash] = AllocateNameEntry( NewName, Index, NameHash[iHash], bIsPureAnsi );
}

FName::Init()函数主要做了以下的工作:

1)—做一些必要的初始化工作,初始化hashtable,注册关键字
2)分割传入字符串中的字符和数字
3)根据传入的字符串(InName)计算得到hash值,在hash table中查找hash值,如果不存在对应的值,会将该hash值加入hash table。(重点)
其中NameHash的结构可以参照下图

根据FindType的不同,Fname::Init()函数有两个作用,第一个作用是作为FName的初始化函数,第二个作用是作为FName的查找函数。

Init()中具体的查找步骤为通过计算字符串的hash值,在hash table中定位到到对应链表的头结点,接着遍历链表依次比较当前元素与传入的InName。

Enum  EFindName
{
	 FNAME_Find,
	 FNAME_Add,
         FNAME_Replace
}

当Init中的FindName变量值为FNAME_Find时,当没有找到对应的字符串会将Index = NAME_None,找到对应的字符串会将Index置为hash table中的hash值。

当FindName为FName_Add时,没有找到对应的字符串就会将该字符串经过hash加入到hash table对应的位置。

当FindName为FName_Replace时,会将查找到的字符串替换为传入的字符串。


FName重载==运算符用于比较操作
FORCEINLINE  UBOOL  operator==(const FNAME& other) const
{
		return  Index == Other.Index  &&  Number == Other.Number;
}

对FName对象的比较有两种方式,第一种是通过重载关系运算符==,首先比较索引(这个索引是保存在Names数组中的索引值),第二种实现了一个compare函数,不同之处在于两个FName对象不等时compare函数的返回值将根据字母表的升序来返回小于0或者大于0。

INT  FName::Compare(const  FName& other) const
{
		if (GetIndex() == Other.GetIndex())
		{
		    return GetNumber() – Other.GetNumber();
		}
		else
		…
}

可以看到比较两个FName对象时,先比较二者的Index,如果不等则退化为调用传统的字符比较函数(正常情况下经由FName的构造函数调用FName::Init()都会生成对应的Index)。


3.FName的一些特殊处理
static 	TArrayNoInit<FNameEntry*>	Names;

static	FNameEntry*		NameHash[65536];

在前面FName的定义中,看到除了定义NameHash数组之外,还定义了一个Names数组,这是为什么?两个数组在功能上有什么不同之处?

其原因在于hash table可以快速查找,却不能随机存取一个元素,当需要根据Index来获取一个FNameEntry对象时,在Names数组中通过下标直接存取效率会更高,这样做弥补了hash table无法随机存取的缺点。


在FName::Init()中,有提到最后一个参数bSplitName是分割标示符,而在实际的使用中,假设我们将其置为TRUE,传入“test1”,发现调用分割函数FName::SplitNameWithCheck()并没有成功。查看FName::SplitNameWithCheck()的代码后,发现分割函数只是针对类似“test_1”这样格式的字符串进行分割,经过与同事的交流得知,Unreal编辑生成的资源文件多是以这样格式命名(或者在内部处理时将对象的命名统一格式)以加快处理速度。




Unreal里的动画

阅读数 1623

unreal

阅读数 239

没有更多推荐了,返回首页