애니메이션 애드온의 제작 #1

11.8

날짜를 강조해서 쓰기 위해 마크업 문법 “###”(제목표시줄3)를 첫머리에 쓰면 저런 도움말이 뜬다. 그 다음 날짜를 쓰면 저 문구가 사라지며 내용이 써지는 식이다. 내가 늘상 헤딩하고 있는 건 맞는데, 블로그가 그렇게 말하니 기분나쁘네!? 야이..

리깅은 제약조건과의 싸움이다. 리깅의 기본이 되는 IK/FK도 제약조건(Constraint)이 만들어낸 마술이다. 하지만 이러한 제약조건들은 단일 모션에는 적합할지 몰라도 범용적으로 사용하긴 어렵다. 제약조건이 복잡해 질수록 의도치 않은 문제가 생긴다. 컨트롤 본이 늘어나는 것은 덤이다. 가뜩이나 복잡한 릭이 더 복잡해진다. 때문에 게임 실무에서는 이걸 잘 안쓴다. 본이 몸을 뚫든 말든 스프링본을 돌리고 베이킹해서 끝낸다. 하지만 타임라인에 지저분한 키가 남는다. 이게 마음에 안드는 이들은 그냥 손으로 잡는다. 하지만 품이 너무 많이 든다.

세컨더리 애니메이션을 아름답게 처리할 수 있는 방법이 없을까? 리깅 전에 이걸 먼저 해결해야 할 것 같다.

먼저 테스트 모델을 제작한다.

애니메이션 스크립트는 가장 건들기 싫은 부류의 스크립트다. 블렌더의 애니메이션 시스템도 복잡하거니와 챗GPT도 엉뚱한 답만 내놓기 때문에 스스로 생각해야 하기 때문이다.

블렌더의 애니메이션 데이터는 Action이 들고 있다. 본이 아니다. 때문에 액션에서 커브를 찾아 매칭시켜야 한다.

  • armature
    • animation_data
      • action
        • fcurve
          • keyframe_point

우리가 애니메이션을 만들면 action에 fcurve를 추가하게 된다. 블렌더는 이 때 해당 본의 이름으로 data_path를 생성하고 인덱스를 부여해서 커브를 구성하게 된다.

그런데 문제가 있다. fcurve가 복사가 안된다!

키프레임도 안된다! 되는 게 뭐야!

그렇다며언…으음 모두 수동으로 구성해주는 방법뿐이다. 어쩔 수 없지

러프한 구현. 이렇게 움직인 다음, 스크립트를 돌리면

오프셋된 키가 만들어진다.

오일러 테스트. 생각보다 잘 작동한다. 오오

아직은 비루한 메뉴ㅋㅋ

스프링 베이킹과는 달리, 키를 망치지 않는다. 그냥 전이할 뿐이다.

가능성이 보이니 계속해 보자. 현재 코드는 차일드가 1개뿐일 때만을 계산한다. 더 아래에 있는 차일드에게도 전이하도록 해야 한다.

…는 그냥 되고 있었는데 코드를 잘못 봐서 조기리턴되고 있었다.

바람이 치마를 훑고 가기 위해선 키를 밀어야 한다. 하는 김에 트랜스폼 3형제를 따로 밀 수 있도록 구현했다. 그렇다면 자식본에 회전값이 아니라, 선택한 값을 모두 물려주는 메뉴로 바꿀 수도 있겠다. 이것도 구현했다. 본격적인 사용은 좀 더 나중이 될 것이다.

다음은 순환 본의 처리. 치마 중 하나를 움직인 후 나머지 본에 모두 같은 값을 붙이는 기능이 필요하다. 월드와 로컬로 나뉘어야 하는데 이게 매우 어려워 보인다. 일단 자리부터 깔자

옆본 보간은 본을 하나만 제어했을 경우 나머지 본을 알아서 제어해 줄 수 있게 만들 예정이다. 뜻대로 잘 되면 좋겠다.먼저 키 따라하기부터 만들어보자.

11.10

오늘의 깨달음 :

  • 본 정보의 matrix는 월드 기준 행렬, matrix_basis는 부모 기준 행렬이다.
  • .to_quaternion()메서드는 해당 본의 회전정보를 쿼터니온으로 뽑아준다.
  • 여기에 .to_matrix()를 붙이면 행렬화 해주고
  • 여기에 .to_4x4()를 붙이면 4×4행렬로 만들어준다.
  • 이제 이걸 붙이고 싶은 본의 역행렬과 곱해주면 본의 회전정보를 같게 만든다.
  • 이걸 다시 .to_quaternion()으로 뽑아서 회전값에 넣어주면 된다.
a = bpy.context.selected_pose_bones[0]
b = bpy.context.selected_pose_bones[1]
        
m = a.matrix.to_quaternion().to_matrix().to_4x4()        
b.rotation_quaternion = (b.matrix.inverted() @ m).to_quaternion()

이렇게 되면 2개의 본은 완전히 같은 회전값을 가진다. 하지만 내가 하고 싶은 건 상대적 월드 회전값이다. 예를 들어 A본을 45도 돌리면 B본도 45도 돌아야 한다. 로컬기준 회전은 그냥 값을 복사하면 되지만, 월드기준으로 돌아야 하는 것은 꽤 까다롭다. 이걸 알아내는데 꼬박 하루를 보냈는데, 풀지 못했다. 그런데 저녁먹고 밤되서 다시 해보니 10분만에 됐다.(!?)

저녁의 깨달음:

  • 본 정보의 matrix_chennel은 원본 대비 회전값을 월드좌표 기준으로 저장한 행렬이다. 이를 이용해서 다른 본도 같은 회전값을 갖게 할 수 있다.
  • 포즈본이 아닌 데이터본 정보의 matrix_local은 월드좌표 기준 행렬이다. 즉, 초기값이다. alt+rgs누르면 돌아가는 그 값
  • 회전값만 뽑은 행렬을 원본 행렬과 곱해주면 위치가 어긋난다. 하지만 여기서 다시 회전값만 뽑으면 잘 된다. 내가 헛갈린 부분이 이 부분인데, 행렬곱은 만능으로 쓸 수 있는 공간 전환 마법이 아니었다.

하지만 그럼에도 불구하고 문제를 완전히 해결하진 못했다. 결국 이것 또한 삽질이었다.

11.12

컴퓨터 그래픽스에서 오브젝트의 회전은 공간이 회전하는 것이다. 공간은 행렬로 이루어져 있는데, 그동안 공부를 기피해왔었다. 어려웠기 때문이다.

하지만 원하는 걸 하기 위해선 행렬을 알아야 했고, 3일동안 공부를 해서 쓸만큼은 이해했는데, 뭔가 여전히 잘 작동하지 않는다. 그래서 챗GPT에게 물어봤더니… 와. 금방 알려줬다(…) 처음엔 왜 안알려줬어??!! 어쩌면 난 질문을 하기 위해 공부한 것일 지도 모르겠다.

import bpy
from math import radians
from mathutils import Matrix

def rotate_bone(pose_bone, angles_deg):
    # angles_deg: [x_angle, y_angle, z_angle]

    # 각도를 라디안으로 변환
    angles_rad = [radians(angle) for angle in angles_deg]

    # X, Y, Z 축을 기준으로 회전하는 행렬 생성
    rotation_matrices = [
        Matrix.Rotation(angles_rad[0], 4, 'X'),
        Matrix.Rotation(angles_rad[1], 4, 'Y'),
        Matrix.Rotation(angles_rad[2], 4, 'Z')
    ]

    # 회전 행렬들을 결합
    combined_matrix = rotation_matrices[2] @ rotation_matrices[1] @ rotation_matrices[0]

    # 현재 본의 전역 행렬을 가져옴
    world_matrix = pose_bone.bone.matrix

    # 결합된 회전 행렬을 전역 행렬에 적용
    pose_bone.bone.matrix = combined_matrix @ world_matrix

    # 화면 업데이트
    bpy.context.view_layer.update()

# 테스트를 위해 활성 포즈 객체와 본을 얻음
pose = bpy.context.active_pose_bone

# X, Y, Z 축을 기준으로 각각 45도씩 회전
rotate_bone(pose, [45.0, 45.0, 45.0])

코드에 문제가 아주 없는 건 아닌데, 그건 사소한 문제다. 핵심은 다 들어있다. 3일 밤낮으로 고민하던 게 이렇게 쉽고 허무하게…흑흑

하지만 적어도 이번 삽질 덕분에 행렬이 어떻게 생겼는지 조금은 이해하게 됐다. 이동값은 오른쪽끝에, 회전값은 3×3공간에 사인과 코사인값으로 나누어져 들어가 있고, 스케일은 대각선을 곱한다. 블렌더에서 제공되는 본의 행렬은 4가지가 있다. world, basis, channel, local

어쨌거나 이제야 다음 단계로 넘어갈 수 있게 됐다. 어휴.. 3년은 늙은 것 같다…

11.13

아휴… 난잡해… 하지만 그림이라도 없으면 카테고리 분류가 너무 안된다. 아이콘 리스트는 여기

기능은 모두 완료되지는 않았다. 하지만, 가장 어려웠던 건 해결했다. 이제 그냥 차근차근 코드 리팩토링과 함께 진행만 하면된다.

11.14

드디어 도메인이 연결됐다! 이제 https://ix9.net으로 접속할 수 있다.

블렌더의 포즈본 시스템은 어찌된 건지, 본을 선택하면 순서대로 리스트에 들어가지 않는다. 굳이 뒤죽박죽 섞는다! 왜! 그것 때문에 본의 선택 순서를 장담할 수가 없다. 그래서 한 번에 본을 잡고 뭔가를 처리하는 것에 제약이 많이 걸린다.

이 애드온은 세컨더리 애니메이션에 사용할 요령으로 제작했다. 머리카락이나 치마같이 흩날리는 애니메이션을 제작할 경우에 손으로 잡는 키를 최소화하기 위해 쓰일 것이다. 그런데 생각보다 이해가 쉽지 않고, 벌써 손이 안가는 메뉴들도 생겼다.(…) 며칠간 좀 더 손을 봐야 할 것으로 보인다.

부모본의 값들을 자식에게 물려줄 때 배율이 좀 더 강했으면 좋겠다. 싶어서 배율항목을 추가하고 값에 이를 곱해주면 되겠지. 싶었는데… 쿼터니온은 값이 오른다고 회전이 2배가 되는 것이 아니다.

그래서 키를 물려줄 때 오일러로 변환 후 다시 쿼터니온으로 변환하는 번잡시러운 과정을 거쳐야 한다. 그럼 쿼터니온 공식을 직접 조작하는 게 좋지 않을까 싶어서 챗GPT에게 물어보니

뭐라는 거야? ^^

내장함수가 왜 존재하는 지 알 것 같았다.

여차저차 구현 후 적용. 이것은 기존의 1배율 물려주기

이것이 배율을 높여서(1.4배) 물려주기이다. 확실히 더 자연스럽다. 그런데 이게 버그다… 의도했던 움직임이 아니다.

이것이 의도했던 순차적 오프셋. 그런데 버그느낌이 더 좋다! 의도적으로 키간격을 축소시키는 기능을 추가해보자.

정작 해보니 어느 상황에나 잘 맞아떨어지진 않는다. 상황봐가며 적당히 사용하도록 하자.

번호순으로 키 밀기기능도 완료. 회전 애니메이션 시에 용이하게 쓰일 것으로 기대한다.

댓글 남기기