이것은 문서의 이전 버전입니다!


개요

GoW2 의 큰 특징중 하나는 '잔혹한 표현' 이다.

GoW 아이콘중 하나인 체인소우 (일명 랜서) 에는 전기톱이 달려있고, 이것을 이용하여 적의 사지를 절단하는 액션은 이 게임의 백미라 할 수 있다.

사지 절단

5226340582_d1f49f878d.jpg

GoW2 의 메인 액션인 chainsaw 장면이다.

이를 구현하기 위해서는 다음과 같은 것들이 필요하다.

리소스

5225797424_3610fd357a.jpg 5302620621_89c79a9f1c.jpg
이미지1 이미지2

위는 고어적 연출을 하기 위해 특별히 제작된 SkeletalMesh 들이다.

는 깔끔하게 쪼개진 조각들이 뼈대에 스키닝 되어있는 것이고, 는 soft weighted 로 스키닝 되어있는 것이다.

고어적 연출을 하기 위해서는 두가지 형태로 모두 제작되어야 한다.

soft weighted 로 스키닝 되어있는 것을 좀 더 자세히 살펴보면

5303213914_d9f95c6e0c.jpg 5302620687_3933098217.jpg
이미지3 이미지4

완전히 쪼개진 조각들이미지1을 가지고 soft weighted 처리한 것을 알 수 있다. 즉, 서로 다른 조각의 끄트머리를 꿰매서 구현한 것이다.

꿰맬때는 그냥 하면 안되고 다음의 규칙을 따라야 한다. <box>절단부위의 vertex 들은 절단부위가 걸쳐져 있는 위와 아래 bone, 양쪽 모두로부터 weight 영향을 받고 있어야 한다.1)</box>

정리하면 다음과 같은 순서와 조건으로 리소스가 제작되어야 한다.

  1. 이미지1과 같이, 완전히 구분된 조각들로 스키닝 된 SkeletalMesh 제작 (이하 GoreMesh)
  2. 이미지2와 같이, 바로 전에 사용된 조각들을 서로 꿰매서 soft weighted 된 SkeletalMesh 제작. (이하 GoreMesh_SoftWeighted) 단, 절단부위 vertex 들은 위, 아래 bone 모두에게 weight 영향을 받도록 할 것.

엔진 임포트 및 절단부위를 위한 후처리 작업

이제 위에서 제작된 GoreSkeletalMesh 들을 에디터에서 불러들이고 사용할 수 있게끔 가공해야 한다. 여기서 가공한다는 뜻은 In-Game 에서 특정 이벤트 (썰기,터뜨리기 등 고어적 표현이 필요한 이벤트) 발생 시, GoreMesh_SoftWeighted → GoreMesh 로 weight 값을 변경할 vertex 들을 추려내어2) metadata 로 저장해 놓는 것을 말한다.

크게 다음의 세가지 스탭으로 이루어 진다.

  • GoreSkeletalMesh import
    1. GoreMesh_SoftWeighted 를 엔진으로 import 한다.
  • 변경될 (절단된) GoreMesh 의 weight influence 값 import
    1. import 한 SkeletalMesh 를 열어 AnimSet editor 를 활성화 시킨다.
    2. 상단 메뉴 Alt. Bone Weighting → Import Mesh Weights… 실행한다.
    3. LOD 선택을 한다.
    4. Base Mesh 를 선택하라는 dialog 창에서 GoreMesh_SoftWeighted 를 선택한다.
    5. Weights Mesh 를 선택하라는 dialog 창에서 GoreMesh 를 선택한다.
  • 절단 부위별로 weight 값을 교체할 vertex 추리기
    1. 현 시점에서 상단 메뉴 View → Alt. Bone Weighting Edit Mode 가 활성화되어야 한다. 하지만 엔진 버그인지 활성화되어 있지 않을 것이다. 이때는 AnimSet editor 를 껐다가 다시 켜면 활성화되어 있을 것이다.
    2. View → Alt. Bone Weighting Edit Mode 를 선택하여 에디트모드를 활성화 한다.
    3. Skeleton Tree 창에서 절단면이 걸쳐져 있는 2개의 Bone (Parent, Child) 중 Child Bone 을 선택.3)
    4. 마우스 우클릭하여 Calculate Bone Break Vert Weightings 선택한다.
    5. 위에 언급했던 조언을 따랐다면((절단부위의 vertex 들을 절단면이 걸쳐져 있는 위, 아래 Bone 모두에게 weight 영향을 받도록 했다면) 깨끗하게 잘려진 모습을 보게 될 것이고, 그렇지 않으면 나머지 vertex 들을 일일이 클릭하여 보정해 줘야 한다. (모든 절단부위에 걸쳐 이를 수행한다.)
    6. 모두 끝마쳤으면 View → Alt. Bone Weighting Edit Mode 를 선택하여 에디트모드를 종료 한다.
    7. 저장

변수

  • 직접적인 변수
    // 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

코드 플로우

로커스트 그런트를 예로 설명

사전작업

  1. AnimSetViewer → SkeletonTree → 절단할 노드에서 마우스 우클릭 후 Calculate Bone Break Vert Weightings
    // 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 );
                ...
            }
            ...
        }
        ...
    }
  2. Parent & Child 모두 영향을 받는 Vertices 를 색출
    // 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")
    ...
}

톱질

  1. 죽음 발동
    // 죽으면 호출되는 이벤트 함수
    simulated function PlayDying( class<DamageType> DamageType, vector HitLoc )
    {
        ...
        if ( WorldInfo.NetMode != NM_DedicatedServer )
        {
            PlayDeath( GearDamageType, HitLoc );  // 죽음 처리를 시작
        }
    }
  2. 죽음 처리
    // 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;
          }
          ...
      }
  3. 특정 부위 절단 호출
    // 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' );
    }
  4. Constraint 해제
    // 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 );
        ...
    }
    • 관련 Bone의 Constraint 모두 해제
      // 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 );
                  }
              }
          }
      }
  5. 실제 Mesh 로부터 Constraint 해제
    // 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 적용
    }
    • Instance Vertex Weights 토글
      // UnSkeletalComponent.cpp
      void USkeletalMeshComponent::ToggleInstanceVertexWeights( UBOOL bEnabled )
      {
          ...
          bNeedsInstanceWeightUpdate = TRUE;   // 추후 USkeletalMeshComponent::Tick 에서 UpdateInstanceVertexWeights() 를 호출하게끔 한다.
          InstanceVertexWeightBones.Empty();   // 이후에 이곳에 절단될 Bone Pair 이름이 추가될 것이다.
          ...
      }
    • 쪼개질 Bone 을 추가
      // UnSkeletalComponent.cpp
      void USkeletalMeshComponent::AddInstanceVertexWeightBoneParented( FName BoneName, UBOOL bPairWithParent )
      {
          ...
          InstanceVertexWeightBones.AddItem( BonePair );     // Bone 과 Parent 로 이루어진 Pair 정보를 추가한다.
          bNeedsInstanceWeightUpdate = TRUE:   // 추후 USkeletalMeshComponent::Tick 에서 UpdateInstanceVertexWeights() 를 호출하게끔 한다.
      }
  6. USkeletalMeshComponent::Tick
    // 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
        }
    }
  7. 절단 부위 Vertex 들의 weight 값 교체
    // UnSkeletalRenderGPUSkin.cpp
    void FSkeletalMeshObjectGPUSkin::UpdateVertexInfluences(
        const TArray<FBoneIndexPair>& BonePairs,
        INT InfluenceIdx,
        UBOOL bResetInfluences )
    {
        ...
        // 교체 작업을 RenderingThread 에 command 로 전달
    }

3215812168_36d1b754c5_o.jpg

피를 적절한 곳에 Decal 로써 뿌리는 처리는 고어게임에서 가장 기본적인 양념이다. 위 이미지는 다음과 같은 피 처리를 하고 있다.

  1. 개틀링건을 들고 있는 Dizzy 란 캐릭터가 지금 총에 맞고 있으며, 맞은 부위에는 피가 튀기고 있다.
  2. Dizzy 와 Carmine (모자쓴 캐릭) 의 가운데 벽을 보면 누군가 피를 흘린채 cover 이동을 하며 생긴 핏자국을 볼 수 있다.
  3. 땅에 떨어진 핏자국을 볼 수 있다.
  4. 이 이미지에는 볼 수 없지만, 치명상을 입게 되면 자신의 캐릭 신체부위를 타고 흘러내리는 피를 볼 수 있다.

기본적으로는 엔진이 지원하는 Emitter 와 Decal 을 사용하지만 GoW2 에서는 좀 더 최적화된 모습으로 이들을 처리하고 있다.

변수

함수

// GearPawnFX.uc
이 클래스는 캐릭터가 피를 흘리는 다양한 상황에 대한 처리를 담당하고 있다.
DBNO 시 지면에 피가 남는 Decal 처리
MeatBag? 시 지면에 피가 남는 Decal 처리
치명상인 상태로 Cover 이동 시 벽에 피가 남는 Decal 처리

코드 플로우

참조

1) 반드시 따라야 하는 것은 아니지만 이렇게 하면 여러모로 편해진다. 이 방식을 따르면 추후 weight 값을 교체할 vertex 를 추려내는 작업을 엔진이 깔끔하게 수행할 수 있게 된다. 그렇지 않으면 엔진이 깔끔하게 수행하지 못하게 되고 일부 vertex 들을 사용자가 직접 추려내야 한다. 눈치 챘겠지만 엔진이 weight 값을 교체할 vertex 를 추려내는 조건은 위, 아래 bone 모두에게 영향을 받고 있는 vertex 이다.
2) 모든 vertex 의 weight 값을 교체하면 모든 절단부위가 동시에 떨어져 나가는 효과일 것이다. 하지만 보통은 이것을 기대하지 않을 것이다. 원하는 절단부위의 vertex weight 만을 교체하여 팔,다리만 떨어져 나가는 효과를 원할 것이다. 이러기 위해서는 절단부위의 vertex 만을 추려내어 부위별로 분류해둘 필요가 있다.
3) 이어지는 절단면 vertex 추리기 로직이 Child 기준으로 작성되었기 때문이다.