GoW2 의 큰 특징중 하나는 '잔혹한 표현' 이다.
GoW 아이콘중 하나인 체인소우 (일명 랜서) 에는 전기톱이 달려있고, 이것을 이용하여 적의 사지를 절단하는 액션은 이 게임의 백미라 할 수 있다.
위는 고어적 연출을 하기 위해 특별히 제작된 SkeletalMesh 들이다.
좌 는 깔끔하게 쪼개진 조각들이 뼈대에 스키닝 되어있는 것이고, 우 는 soft weighted 로 스키닝 되어있는 것이다.
고어적 연출을 하기 위해서는 두가지 형태로 모두 제작되어야 한다.
soft weighted 로 스키닝 되어있는 것을 좀 더 자세히 살펴보면
완전히 쪼개진 조각들이미지1을 가지고 soft weighted 처리한 것을 알 수 있다. 즉, 서로 다른 조각의 끄트머리를 꿰매서 구현한 것이다.
꿰맬때는 그냥 하면 안되고 다음의 규칙을 따라야 한다. <box>절단부위의 vertex 들은 절단부위가 걸쳐져 있는 위와 아래 bone, 양쪽 모두로부터 weight 영향을 받고 있어야 한다.1)</box>
정리하면 다음과 같은 순서와 조건으로 리소스가 제작되어야 한다.
이제 위에서 제작된 GoreSkeletalMesh 들을 에디터에서 불러들이고 사용할 수 있게끔 가공해야 한다. 여기서 가공한다는 뜻은 In-Game 에서 특정 이벤트 (썰기,터뜨리기 등 고어적 표현이 필요한 이벤트) 발생 시, GoreMesh_SoftWeighted → GoreMesh 로 weight 값을 변경할 vertex 들을 추려내어2) metadata 로 저장해 놓는 것을 말한다.
크게 다음의 세가지 스탭으로 이루어 진다.
// GearPawn.uc var bool bIsGore; // Gore 연출 할 SkeletalMesh 와 PhysicsAsset 으로 전환이 이루어졌는지 여부 var bool bGoreSetupForDeath; // Gore Setup 을 완수하였는지 여부. Gore Setup 은 쪼개진 Gib 들을 위해 약간의 최적화 작업을 수행한다. var bool bHasBrokenConstraints; // 하나라도 신체부위가 쪼개졌다면 TRUE var bool bUsingNewSoftWeightedGoreWhichHasStretchiesFixed; // Gore 연출을 위해 SoftWeighted 된 Mesh 를 사용하고 있는지 여부 var SkeletalMesh GoreSkeletalMesh; // Gore 연출 시 사용할 SkeletalMesh var PhysicsAsset GorePhysicsAsset; // Gore 연출 시 사용할 PhysicsAsset var Array<MorphTargetSet> GoreMorphSets; // Gore 연출 시 사용할 MorphSets var Array<Name> GoreBreakableJoints; // Gore 연출 시 쪼갤 뼈대 이름을 담은 배열 var Array<Name> HostageHealthBuckets; // Hostage 상태일 때, 각 파츠가 떨어지는 Gore 효과를 위해 Health 를 가지는 뼈대 이름을 담은 배열 struct native DependantBreakInfo { var Name ParentBone; // 부모 Bone var Array<Name> DependantBones; // 부모 Bone 이 절단될 때, 같이 절단되어야 하는 Bone 리스트 }; var Array<DependantBreakInfo> JointsWithDependantBreaks; // 덩달아 쪼개질 Bone 정보 리스트
// GearPawn.uc struct native PhysicsImpactRBRemap { var() Name RB_FromName; // Impulse 가 들어온 Bone var() Name RB_ToName; // Impulse 를 적용시킬 Bone }; var(HitReactions) editinline Array<PhysicsImpactRBRemap> PhysicsImpactRBRemapTable; // 특정 Bone 은 Impulse 에 좋게 반응하지 못한다. 때문에 더 나은 비쥬얼 결과를 얻기 위해 다른 Bone 으로 Impulse 를 전가시킬 필요가 있다. var(HitReactions) float PhysicsImpactMassEffectScale; // 질량에 따른 Impact 를 적용할 때 scale 되는 값 // DamageType.uc var(RigidBody) float KDamageImpulse; // 이 데미지가 KActor 에 전가하는 impulse 세기 var(RigidBody) float KDeathUpKick; // 죽었을 경우 위로 솟아오르는 힘 // GearDamageType.uc var config const float DistFromHitLocToGib; // 절단 부위를 계산하기 위한 Damage Cylinder 의 반지름. 이 Damage Cylinder 영역 안에 있는 절단부위는 모두 쪼개뜨리게 된다. 이 값의 default 는 -1 로 모든 절단부위를 쪼갠다. var const config bool bAllowHeadShotGib; // 이 무기로 머리를 맞출 때, 뽀개지는 (Gore) 연출을 할까?
// SkeletalMeshComponent.uc BreakConstraint // GearPawn.uc PlayDying PlayDeath ReplicatedPlayTakeHitEffects PlayDeath GoreExplosion GoreExplosion ChainsawGore // 데미지 타입이 GDT_Chainsaw 일 경우 호출 BreakConstraint 호출 BreakDependantBones BreakConstraint 호출 UpdateHostageHealth BreakConstraint 호출 BreakConstraint SkeletalMeshComponent.BreakConstraint BreakDependantBones 호출 // GearPawn_COGGear.uc, GearPawn_COGDom.uc, GearPawn_COGMarcus.uc, GearPawn_Locust_Base.uc ChainSawGore BreakConstraint // GearPC.uc GoreTest GearPawn.BreakConstraint GoreTestJointList GearPawn.BreakConstraint
로커스트 그런트를 예로 설명
// AnimSetViewerTools.cpp void WxAnimSetViewer::OnSkeletonTreeMenuHandleCommand( wxCommandEvent &In ) { ... else if ( Command == IDM_ANIMSET_SKELETONTREE_CALCULATEBONEBREAKS ) { ... if ( BoneIndexPairs.Num() > 0 ) { // BoneIndexPairs ( Parent & Child ) 에 모두 영향을 받는 Vertices 를 색출해 내어 BonePairs to VertList 로 매핑해 둔다. CalculateBoneWeightInfluences( PreviewSkelComp->SkeletalMesh, BoneIndexPairs ); ... } ... } ... }
// AnimSetViewerTools.cpp void CalculateBoneWeightInfluences( USkeletalMesh* SkeletalMesh, TArray<FBoneIndexPair>& BonePairs ) { // SkeletalMesh 의 모든 LOD 를 순회하면서 BonePairs 에 있는 Parent & Child 에 모두 스키닝 되어 있는 Vertex 를 리스트에 담아 // VertexInfluences.VertexInfluenceMapping 에 BonePairs to VertList 로 담아둔다. }
// GearPawn_LocustDroneBase.uc defaultproperties { ... bUsingNewSoftWeightedGoreWhichHasStretchiesFixed=TRUE GoreSkeletalMesh=SkeletalMesh'Locust_Grunt.Mesh.Locust_Grunt_Gore_SoftWeights' GorePhysicsAsset=PhysicsAsset'Locust_Grunt.PhysicsAsset.Locust_Grunt_CamSkel_Physics' GoreBreakableJointsTest=("b_MF_Face","b_MF_Head","b_MF_Spine_03","b_MF_Armor_Crotch","b_MF_Armor_Sho_R","b_MF_UpperArm_R","b_MF_Hand_R","b_MF_Calf_R","b_MF_Calf_L") GoreBreakableJoints=("b_MF_Face","b_MF_Head","b_MF_Spine_03","b_MF_Armor_Crotch","b_MF_Armor_Sho_R","b_MF_UpperArm_R","b_MF_Hand_R","b_MF_Calf_R","b_MF_Calf_L") ... }
// 죽으면 호출되는 이벤트 함수 simulated function PlayDying( class<DamageType> DamageType, vector HitLoc ) { ... if ( WorldInfo.NetMode != NM_DedicatedServer ) { PlayDeath( GearDamageType, HitLoc ); // 죽음 처리를 시작 } }
// GearPawn.uc simulated function PlayDeath( class<GearDamageType> GearDamageType, vector HitLoc ) { ... if ( bShowGore ) { // 헤드샷 처리 ... // 사지절단 시작 else if ( GearDamageType.static.ShouldGib( Self, KilledByPawn ) && ShouldGib( GearDamageType, KilledByPawn ) ) { if ( !bIsGore && GoreSkeletalMesh != None && GorePhysicsAsset != None ) { CreateGoreSkeleton( GoreSkeletalMesh, GorePhysicsAsset ); // 고어적 연출을 하기 위해 특수 제작된 SkeletalMesh 와 PhysicsAsset 으로 교체한다. } if ( bIsGore && GoreBreakableJoints.Length > 0 ) { GoreExplosion( TearOffMomentum, HitLoc, GearDamageType ); // 특정 부위를 제거하는 작업을 시작한다. } } } }
// GearPawn.uc simulated final function CreateGoreSkeleton( SkeletalMesh TheSkeletalMesh, PhysicsAsset ThePhysicsAsset ) { // 교체할 고어메시가 없거나 고어표현을 하면 안되는 게임환경 (청소년 등급) 이라면 리턴 ... // 죽음 이벤트가 발동 되었다면 에니메이션을 모두 정지한다. ... // 이전 메시의 Attachment 를 백업 // 고어 Material 을 위해 기존 Material 클리어 // SkeletalMesh 교체 // MorphTargets 교체 // PhysicsAsset 교체 // CastShadow 속성 제거. 퍼포먼스 위해 // soft weighted 메시를 사용하지 않는다면 모든 부위를 hard weight 로 처리 // 이전 메시의 Attachment 를 적용 // 바운딩 박스가 바뀌었으므로 Encroach 체크를 활성화 한다. // PhysicsAsset 을 unfixed 상태로 한다. }
// GearPawn.uc simulated function GoreExplosion( Vector Momentum, Vector HitLocation, class<GearDamageType> GearDamageType, optional bool bRandomizeGibImpluse ) { ... if ( GearDamageType == class'GDT_Chainsaw' ) { ChainsawGore(); // 톱질 Gore 발동 bHasGoreExploded = TRUE; // 고어폭발되었음 return; } ... }
// GearPawn_COGMarcus.uc simulated function ChainSawGore() { BreakConstraint( vect( 100, 0, 0 ), vect( 0, 10, 0 ), 'b_MF_Spine_03' ); BreakConstraint( vect( 0, 100, 0 ), vect( 0, 0, 10 ), 'b_MF_UpperArm_R' ); }
// GearPawn.uc simulated final function BreakConstraint( Vector Impulse, Vector HitLocation, Name InBoneName, optional bool bVelChange ) { ... // Mesh 의 Constraint 해제 함수 호출 Mesh.BreakConstraint( Impulse, HitLocation, InBoneName, bVelChange ); // 덩달아 쪼갤 Bone 들의 Constraint 를 해제 BreakDependantBones( Impulse, HitLocation, InBoneName ); ... }
// GearPawn.uc struct native DependantBreakInfo { var Name ParentBone; // 아래 배열에 포함된 Bone들이 존속된 부모Bone 이름 var Array<Name> DependantBones; // 부모Bone 이 쪼개질 때, 같이 쪼개져야 하는 Bone 리스트 }; var Array<DependantBreakInfo> JointsWithDependantBreaks; // 내가 쪼개질 때, 같이 쪼개질 Bone들의 정보를 담은 배열 simulated final function BreakDependantBones( Vector Impulse, Vector HiLocation, Name InBoneName, optional bool bVelChange ) { for ( i = 0; i < JointsWithDependantBreaks.length; i++ ) { if ( JointsWithDependantBreaks[i].ParentBone == InBoneName ) { for ( j = 0; j < JointsWithDependantBreaks[i].DependantBones.length; J++ ) { // InBoneName 에 의존적인 모든 Bone 의 Constraint 역시 해제 BreakConstraint( Impulse, HitLocation, JointsWithDependantBreaks[i].DependantBones[j], bVelChange ); } } } }
// SkeletalMeshComponent.uc simulated final function BreakConstraint( Vector Impulse, Vector HitLocation, Name InBoneName, optional bool bVelChange ) { ToggleInstanceVertexWeights( TRUE ); // Instance Vertex Weights 를 사용하기로 한다. GoreSkeletalMesh 에서 부드럽게 이어진 피부조직을 떼기 위해 AddInstanceVertexWeightBoneParented( InBoneName ); // 절단될 자식 Bone 을 추가한다. 이 Bone 은 그 Parent 로부터 상호 스키닝 정보가 완전히 분리될 것이다. Body.SetFixed( FALSE ); // Constraint 절단을 가능토록 한다. Constraint.TermConstraint(); // 해당부위 Constraint 를 제거 UpdateMeshForBrokenConstraints(); // 쪼개진 부위에 대하여 Mesh 의 PhysicsAsset 업데이트 AddImpulse( ... ); // 쪼개진 부위에 Impulse 적용 }
// UnSkeletalComponent.cpp void USkeletalMeshComponent::ToggleInstanceVertexWeights( UBOOL bEnabled ) { ... bNeedsInstanceWeightUpdate = TRUE; // 추후 USkeletalMeshComponent::Tick 에서 UpdateInstanceVertexWeights() 를 호출하게끔 한다. InstanceVertexWeightBones.Empty(); // 이후에 이곳에 절단될 Bone Pair 이름이 추가될 것이다. ... }
// UnSkeletalComponent.cpp void USkeletalMeshComponent::AddInstanceVertexWeightBoneParented( FName BoneName, UBOOL bPairWithParent ) { ... InstanceVertexWeightBones.AddItem( BonePair ); // Bone 과 Parent 로 이루어진 Pair 정보를 추가한다. bNeedsInstanceWeightUpdate = TRUE: // 추후 USkeletalMeshComponent::Tick 에서 UpdateInstanceVertexWeights() 를 호출하게끔 한다. }
// UnSkeletalComponent.cpp void USkeletalMeshComponent::Tick( FLOAT DeltaTime ) { ... if ( bNeedsInstanceWeightUpdate ) { UpdateInstanceVertexWeights(); // GoreSkel_SoftWeighted 의 절단된 부위 vertex weight 값을 완전히 절단된 GoreSkel 의 것으로 교체작업을 시작한다. } ... } void USkeletalMeshComponent::UpdateInstanceVertexWeights() { if ( MeshObject ) { if ( InstanceVertexWeightBones.Num() > 0 ) { ... const UBOOL bResetWeights = TRUE; // Bone 과 Parent 둘다 영향을 받는 (절단 부위) Vertex 들의 weight 값 교체 MeshObject->UpdateVertexInfluences( BoneIndexPairs, 0, bResetWeights ); } else { ... } bNeedsInstanceWeightUpdate = FALSE; // 처리했으니 플래그 off } }
// UnSkeletalRenderGPUSkin.cpp void FSkeletalMeshObjectGPUSkin::UpdateVertexInfluences( const TArray<FBoneIndexPair>& BonePairs, INT InfluenceIdx, UBOOL bResetInfluences ) { ... // 교체 작업을 RenderingThread 에 command 로 전달 }
피를 적절한 곳에 Decal 로써 뿌리는 처리는 고어게임에서 가장 기본적인 양념이다. 위 이미지는 다음과 같은 피 처리를 하고 있다.
기본적으로는 엔진이 지원하는 Emitter 와 Decal 을 사용하지만 GoW2 에서는 좀 더 최적화된 모습으로 이들을 처리하고 있다.
GearPawnFX 는 고어적 표현에 사용되는 피blood를 편하고 효과적이고 최적화된 방법으로 남기기 위해 고안된 클래스 이다.
게임 내에서 사용되는 모든 핏자국은 이것을 통하여 남는다고 봐도 된다.
// GearPawnFX.uc simulated final funciton LeaveADecal( delegate<DecalTrace> DecalTraceFunc, // Trace 할 시작과 끝 위치를 구하는 함수의 delegator delegate<DecalChoice> DecalChoiceFunc, // 어떠한 Decal 을 사용할 것인가를 결정하는 함수의 delegator delegate<DecalTimeVaryingParams> DecalTimeVaryingParamsFunc, // 시간의 흐름에 따라 수정해야 할 파라메터, 예를 들어 fade in/out, 시간에 민감한 효과 등 optional Vector ForceStartLocation ) { // DecalChoiceFunc 함수로 사용할 Decal 을 선택한다. ... // DecalTraceFunc 함수로 Trace 의 시작,끝 위치를 구한다. ... // 실제 Trace 함수 수행 ... // Trace 결과 무언가 hit 했다면 그곳에 Decal 을 남긴다. if ( TraceActor != None ) { ... GD = GearGRI( WorldInfo.GRI ).GOP.GetDecal_Blood( out_HitLocation ); // GearObjectPool 에서 GearDecal 하나를 꺼낸다. if ( GD != None ) { // 절절하게 Parameter 를 세팅하고 TraceActor 에 Attach 한다. ... } } }
// GearPawnFX.uc // Tracing 의 시작/끝 위치를 구하는 함수 원형 simulated delegate DecalTrace( out vector out_TraceStart, out vector out_TraceDest, const float RandomOffsetRadius, optional vector ForceStartLocation ) { `log( "DecalTrace Delegate was not set" ); ScriptTrace(); } // 실제 구현이 담긴 함수들 simulated final function BloodDecalTrace_TraceGibImpact( ... ); // 현재 위치에서 땅을 향하는 시작/끝 위치를 획득 simulated final function BloodDecalTrace_FromNeck( ... ); // 목으로부터 대각선 아래를 향하는 시작/끝 위치를 획득 simulated final function BloodDecalTrace_FromNeckForFacePunch( ... ); // 죽빵을 날렸을 때 땅에 남길 피의 위치를 구하는 용도? 암튼 위와 거의 비슷 simulated final function BloodDecalTrace_GroundBelowThePawn( ... ); // Pawn 으로부터 그 아래 바닥으로 향하는 시작/끝 위치를 획득 simulated final function BloodDecalTrace_GroundBelowThePawn_Rand( ... ); // 바로 위의 Random 버전 simulated final function BloodDecalTrace_360AroundPawn_Forward( ... ); // Pawn 으로부터 앞을 향하는 시작/끝 위치 획득 simulated final function BloodDecalTrace_360AroundPawn_Left( ... ); // Pawn 으로부터 왼쪽을 향하는 시작/끝 위치 획득 simulated final function BloodDecalTrace_360AroundPawn_Right( ... ); // Pawn 으로부터 오른쪽을 향하는 시작/끝 위치 획득 simulated final function BloodDecalTrace_360AroundPawn_Backward( ... ); // Pawn 으로부터 뒤를 향하는 시작/끝 위치 획득 simulated final function BloodDecalTrace_360AroundPawn_Up( ... ); // Pawn 으로부터 위를 향하는 시작/끝 위치 획득 simulated final function BloodDecalTrace_360AroundPawn_Down( ... ); // Pawn 으로부터 아래를 향하는 시작/끝 위치 획득 simulated final function BloodDecalTrace_GroundBelowTheWeaponEnd_Rand( ... ); // 총구로부터 지면을 향하는 시작/끝 위치를 획득 simulated final function BloodDecalTrace_CoverBehindPawnMiddleOfBody( ... ); // Pelvis 부터 Cover 를 향하는 시작/끝 위치를 획득 simulated final function BloodDecalTrace_TraceFromEndOfChainsaw( ... ); // 총구로부터 총구방향을 향하는 시작/끝 위치를 획득
// GearPawnFX.uc // 사용할 Decal 을 선택하는 함수 원형 simulated delegate DecalChoice( const out TraceHitInfo HitInfo, out float out_DecalRotation, out DecalData out_DecalData ) { `log( "DecalChoice Delegate was not set" ); ScriptTrace(); } // 실제 구현이 담긴 함수들 simulated final function BloodDecalChoice_HeadShot( ... ); // 헤드샷 시 뿜어져 나오는 Decal 선택 simulated final function BloodDecalChoice_Splat( ... ); simulated final function BloodDecalChoice_GibImpact( ... ); simulated final function BloodDecalChoice_ChainsawSpray_Wall( ... ); // Chainsaw 액션 시, 벽에 남는 Decal 선택 simulated final function BloodDecalChoice_ChainsawSpray_Ground( ... ); // Chainsaw 액션 시, 땅에 남는 Decal 선택 simulated final function BloodDecalChoice_GibExplode_Ground( ... ); // 신체절단이 일어날 때 땅에 남는 Decal 선택 simulated final function BloodDecalChoice_Wall( ... ); simulated final function BloodDecalChoice_DBNO( ... ); // DBNO 시, 땅에 남는 Decal 선택 simulated final function BloodDecalChoice_MeatBag( ... ); simulated final function BloodDecalChoice_MeatBagHeelScuff( ... ); simulated final function BloodDecalChoice_BloodPool( ... ); simulated final function BloodDecalChoice_GibExplode_Ground_SmallSplat( ... ); simulated final function BloodDecalChoice_PunchFace( ... ); simulated final function BloodDecalChoice_PawnIsReallyHurt( ... ); // 열라 아플 때? simulated final function BloodDecalChoice_HitByBullet( ... ); // 총알 맞았을 때? simulated final function BloodDecalChoice_LimbBreak( ... );
// GearPawnFX.uc // Decal 에 대한 시간을 조절하는 함수 원형 simulated delegate DecalTimeVaryingParams( out MaterialInstance MI_Decal ) { `log( "DecalTimeVaryingParams Delegate was not set" ); ScriptTrace(); } // 실제 구현이 담긴 함수들 simulated final function BloodDecalTimeVaryingParams_Default( ... ); // 일반적인 타임 세팅 simulated final function BloodDecalTimeVaryingParams_DBNO( ... ); // DBNO 시, 타임 세팅 simulated final function BloodDecalTimeVaryingParams_Wall( ... ); // 벽에 뭍는 Decal 에 대한 타임 세팅
// GearPawnFX.uc // DBNO 시, blood trail 효과를 남긴다. simulated final function SpawnABloodTrail_DBNO() { if ( WorldInfo.GRI.ShouldShowGore() ) { // DBNO 상태에서 움직이면 if ( VSize( Velocity ) > 10.f ) { LeaveADecal( BloodDecalTrace_GroundBelowThePawn, BloodDecalChoice_DBNO, BloodDecalTimeVaryingParams_DBNO ); // 바닥에 Decal 을 남긴다. } } } // 위와 같은 식으로 LeaveADecal 을 사용하는 함수들 simulated final function SpawnABloodTrail_MeatBag(); simulated final function SpawnABloodTrail_Wall(); // Cover 에 있을 시 blood trail 효과를 남긴다. simulated final function SpawnABloodTrail_GibExplode_Ground(); // 시체 폭파 시 지면에 blood trail 효과를 남긴다. simulated final function SpawnABloodTrail_GibExplode_360(); // 시체 폭파 시 사방에 blood trail 효과를 남긴다. simulated final function SpawnABloodTrail_ChainsawSpray_Ground(); // Chainsaw 액션 시 땅에 blood trail 효과를 남긴다. simulated final function SpawnABloodTrail_ChainsawSpray_Wall(); // Chainsaw 액션 시 벽에 blood trail 효과를 남긴다. simulated final function SpawnABloodTrail_GibImpact( ... ); simulated final function SpawnABloodTrail_HeadShot(); // 헤드샷 blood trail 효과를 남긴다. simulated final function SpawnABloodTrail_BloodPool(); simulated final function SpawnABloodTrail_PawnIsReallyHurt(); // 열라 아플 시 blood trail 효과를 남긴다. simulated final function SpawnABloodTrail_HitByABullet(); // 총알에 맞을 시 blood trail 효과를 남긴다. simulated final function SpawnABloodTrail_LimbBreak( ... );
GearPawnFX 를 사용하는 주체에 특별한 제약은 없지만 보통 GearPawn 에 의해 함수가 호출이 된다. GearPawnFX 는 within GearPawn 키워드로 감싸져 있어 GearPawn 내부에서 사용되는 것이 의도되어 있다.
코드 플로우를 정확하게 이해하려면 GearPawnFX.uc 이외에도 GearObjectPool 을 이해해야 한다.
전체적인 플로우는 다음과 같다. (하나의 예시, 이것 말고도 다양한 코드 흐름이 나올 수 있다.)
한눈에 흐름을 잡기 위한 이미지
GoW2 소스