첫번째 애프터 이펙트

TL;DR

관심 있는 분야이기도 하니 설치해서 내장되어 있는 튜토리얼을 따라해봤다!

alt text

CC Rainfall 효과와 Lumetri 색상을 설정해주었다.

blah

우선 프로그램 내에서 튜토리얼을 할 수 있다는 것이 아주 마음에 든다.

alt text

  • 레이어 개념이 있다는 것이 신기하다.
  • 각 레이어 자식에는 컴포넌트들(게임 엔진 개념으로)이 존재한다.
  • 변형은 Transform으로 이해해도 좋을 듯.

alt text

  • 컴포넌트 안에는 값을 조절할 수 있는 변수들이 존재하며, 값을 바꾸면 이미지처리를 해주는 것으로 추정된다.

프로그래밍을 활용하여 할 수 있는 방법을 찾는 것이 목표!

그래픽스 관련 내용 통합 정리

TL:DR

왜 중요한 순간에 기억이 나지 않는 것일까?
그래픽스 관련 내용을 한 페이지에서 해결 하기 위해 이글을 작성한다.

렌더링 파이프라인

3차원으로 만들어진 모델을 2차원 화면에 투영하는 렌더링 과정.

  1. Input Assembler(입력 조립기): 정점 데이터와 인덱스 데이터를 수집하고 이를 조립하여 렌더링 파이프라인에 전달.
  2. Vertex Shader(정점 셰이더): 정점 배열에 저장된 모든 정점을 변환하고 다양한 연산을 수행. 주로 공간 변환(모델, 뷰, 투영 변환)과 라이팅 계산을 수행.
  3. Tessellation(테셀레이션, 선택적): 정점을 세밀하게 분할하여 더 많은 폴리곤을 생성, 부드러운 곡면을 표현할 때 사용.
    • Tessellation Control Shader(테셀레이션 제어 셰이더): 테셀레이션 레벨 결정.
    • Tessellation Evaluation Shader(테셀레이션 평가 셰이더): 생성된 새로운 정점의 위치 계산.
  4. Geometry Shader(지오메트리 셰이더, 선택적): 기본 도형(주로 삼각형)을 다른 도형으로 변환하거나 새로운 정점을 생성.
  5. Rasterizer(래스터라이저): 변환된 정점들로 삼각형을 조립하고 프래그먼트를 생성.
  6. Clipping(클리핑): 클립 공간의 정점들을 화면 공간으로 투영하기 전에 시야 범위 밖의 정점을 제거.
  7. Fragment Shader(프래그먼트 셰이더): 프래그먼트의 색상을 결정(라이팅, 텍스처링 등 진행).
  8. Output Merger(출력 병합기): 깊이, 스텐실, 블렌딩 테스트를 통해 최종 픽셀 색상을 결정하고 컬러 버퍼를 갱신.
  9. Viewport Transform(뷰포트 변환): 클립 공간에서 정점들을 화면 좌표로 변환.
  10. Framebuffer(프레임버퍼): 최종적으로 렌더링된 이미지가 저장되는 버퍼.

정점 셰이더와 프래그먼트 셰이더는 두 가지 프로그램(셰이더 코드)을 작성해야 하며, 래스터라이저와 출력 병합기는 하드웨어로 고정된 단계로 정해진 연산을 수행한다.

공간 변환 순서

오브젝트 공간 -(월드 변환)-> 월드 공간 -(뷰 변환)-> 카메라 공간 -(투영 변환)-> 클립 공간.

정점 셰이더가 수행하는 연산이 위에 적힌 공간 변환들입니다.

월드 변환

각 오브젝트 공간에서 만들어진 물체를 단일한 월드 공간으로 변환.

뷰 변환

월드 공간의 특정 영역을 스크린에 렌더링하기 위해 가상 카메라의 위치와 방향을 설정.

투영 변환

카메라 기준의 정점 위치를 화면에 보이기 위한 정점 위치로 변환.

동차 좌표계

기하학적 변환을 보다 편리하게 수행하기 위해 사용되는 확장된 좌표계. 2D나 3D좌표를 고차원으로 확장하여 행렬 연산을 통해 다양한 기하학적 변환(이동, 회전, 스케일링, 원근 투영 등)을 일관되게 수행할 수 있음.

3차원 동차 좌표계.

일반적인 3D 좌표 (x, y, z)를 동차 좌표계에서는 (x, y, z, w)로 표현합니다. 이때도 w는 스케일링 팩터로, 일반적으로 1로 설정. 일반 좌표 (x, y, z)는 (x/w, y/w, z/w)로 변환할 수 있다.

4x4 행렬 사용 이유

  1. 일관된 기하학적 변환:
    • 3D 공간에서의 모든 주요 기하학적 변환(이동, 회전, 스케일링, 원근 투영 등)을 하나의 행렬 연산으로 통합 할 수 있다.
    • 4x4 행렬을 사용하면 모든 변환을 하나의 행렬로 표현할 수 있어, 복잡한 변환을 단순한 행렬 곱셈으로 처리할 수 있다.
  2. 변환의 조합:
  • 여러 변환을 연속적으로 수행할 때, 각각의 변환을 개별적으로 수행하는 대신, 변환 행렬들을 곱해서 하나의 행렬로 결합할 수 있다.
  • 변환 행렬들의 곱은 순차적인 변환을 하나의 단일 행렬 연산으로 표현할 수 있게 한다.
  1. 동차 좌표계의 표현:
    • 동차 좌표계에서의 점 (x, y, z, w)을 4x4 행렬과 곱하여 새로운 점을 얻는다.
    • 이동 변환은 동차 좌표계에서 다음과 같이 표현:
      alt text
  2. 원근 투영 변환:
    • 원근 투영 변환을 효과적으로 수행할 수 있다. 원근 투영 변환은 z-축을 기준으로 거리 비례 축소를 적용한다. 이는 동차 좌표계에서 다음과 같은 4x4 행렬로 표현된다.
      alt text

      여기서 d는 원근 투영의 거리이다.

동차 좌표계는 3D 기하학적 변환을 단순화하고, 여러 변환을 하나의 연산으로 통합할 수 있게 한다. 4x4 행렬을 사용함으로써, 다양한 변환을 효율적이고 일관되게 수행할 수 있으며, 특히 복잡한 변환을 쉽게 조합하고 적용할 수 있다.

벡터 활용

두 벡터 사이의 각도 구하기

내적으로 두 벡터의 사이각 구하기 (0~180도)

두 벡터의 내적(dot product)을 두 벡터의 크기(magnitude)의 곱으로 나눈 후, 역코사인 함수를 사용하면 두 벡터 사이의 각도를 구할 수 있다.

내/외적으로 두 벡터의 사이각 구하기 (0 ~ 360도)

두 벡터의 내적(dot product)을 두 벡터의 크기(magnitude)의 곱으로 나눈 값을 코사인 값으로 각도를 구하고, 외적(cross product)의 방향을 사용해 각도의 방향을 결정하여 0도에서 360도 사이의 각도를 구할 수 있다.

로컬 회전 구하는 법.

[추가 예정]

Emscripten에서의 Test Suite

TL;DR

요즘 Emscripten으로 WASM 작업하고 있는데 동작하나 확인을 하기 위해서는 c++ 코드 작성하고, 자바스크립트 작성 하고, 브라우저 켜서 콘솔창 확인하기 너무 번거로운 것이다. 이왕하는 것 c++ 코드로만 테스트를 진행하고 싶다. 그래서 이것저것 찾아봐서 Google Test를 적용시켜 보았다.

전제 조건

  • Emscripten SDK가 설치되어 있고 환경 변수가 구성되어 있어야 합니다.

함수 작성

테스트를 진행할 함수를 작성했다.

1
2
3
4
5
double add(double a, double b) { return a + b; }

EMSCRIPTEN_BINDINGS(my_module) {
function("add", &add);
};

EMSCRIPTEN_BINDINGS을 사용하여, add 함수가 자바스크립트에서 사용할 수 있게 바인딩 했다. 해당 부분은 Embind를 확인하면 된다.

Test Suite 작성

1
2
3
4
5
6
7
8
9
10
#include <emscripten.h>
#include <emscripten/val.h>

using namespace emscripten;

TEST(WebTest, Add) {
val Module = val::global("Module");
double add_result = Module.call<double>("add", 1, 2);
EXPECT_DOUBLE_EQ(1 + 2, add_result);
}

Add 함수를 테스트하여 덧셈이 올바른지 확인한다. emscripten::val을 사용하여 C++에서 JavaScript 객체를 호출하고 사용할 수 있다.
Embind로 바인딩한 add 함수를 호출해서 사용한다.

CMake 설정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.14.0
)

FetchContent_MakeAvailable(googletest)
include(GoogleTest)

set(LOPTS "${LOPTS} -lembind -O3 -std=c++11 -s ASYNCIFY")
set_property(TARGET ${TARGET} PROPERTY SUFFIX ".html")
link_libraries("-lembind")
target_link_options(${TARGET} PRIVATE "--emrun")
set_target_properties(${TARGET}
PROPERTIES
LINK_FLAGS ${LOPTS}
)

target_link_libraries(${TARGET} PRIVATE gtest_main)

핵심적인 부분은 FetchContent를 통해 googletest를 설치하고, 사용할 수 있게 하고, emscripten를 위한 링크 플래그를 추가했다.

emrun은 Emscripten으로 생성된 HTML 페이지를 로컬에서 실행하고 디버깅할 수 있게 해주는 도구이다. link_libraries("-lembind")를 추가하여 emrun 활성화를 시킨다.
emrun에 대한 자세한 내용은 여기를 참고하면 된다.

실행

1
emrun --browser chrome --browser_args="--headless --disable-gpu --remote-debugging-port=9222" --kill_exit [HTML]

해당 명령어를 사용하면 된다. 옵션의 대한 간단한 설명은 다음과 같다.

  • --browser chrome : 구글 크롬을 사용하여 실행
  • --browser_args : 크롬 실행 시 추가적으로 전달할 옵션을 지정.
  • --kill_exit : HTML 실행 후 emrun이 종료될 때까지 대기하지 않고 즉시 종료.

실행을 하면 테스트함수가 진행되는 것을 볼 수 있다!

Finish

ぼくがかんがえたさいきょうのWasm(Emscripten/C++)テスト環境 해당 블로그에서 작업한 것을 봤길래 나도 냉큼 따라 해봤다. 생각보다 어렵지 않아서 좋은걸! 특히 자바스크립트와 html을 따로 작성안해서 좋다. 나는 웹 개발이 주가 아니라서 어렵단 말이지..

더 다양한 것을 하려면 복잡하겠지만, 간단한 작업은 이와 같이 하면 된다. 전체 내용은 gist 올렸으니 확인하면 된다. add 함수 외에 다양한 예시도 있다.

레퍼런스

더 자세한 정보를 원하시면 다음 문서를 참고하면 된다.

Emscripten에서 파일 로딩하기

TL;DR

폴더 안의 파일을 불러와서 사용하고 싶다.

Packaging using emcc

공식 페이지에서는 컴파일 시 emcc 명령어를 사용하면 된다고 한다.

--preload-file 의 명령어를 사용하여 어떤 파일을 사용할지 설정해주면 된다.

CMakeList.txt에서 사용하기.

1
set_target_properties(app LINK_FLAGS "--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/ShaderFiles/EsShader@ShaderFiles")

@를 통해 가상 파일 위치를 매핑할 수 있다. ${CMAKE_CURRENT_SOURCE_DIR}/ShaderFiles/EsShader의 위치를 ShaderFiles로 매핑했다.

파일 로드.

1
2
3
4
5
6
├── ShaderFiles
│ ├── EsShader
│ │ ├── basic_lighting.fs
│ │ └── basic_lighting.vs
│ ├── basic_lighting.fs
│ └── basic_lighting.vs

파일 트리는 다음과 같다. ${CMAKE_CURRENT_SOURCE_DIR}/ShaderFiles/EsShader의 위치를 ShaderFiles 로 매핑했기 때문에 플랫폼에 따라 경로를 변경하지 않아도 된다.

GitHub Issue를 기반으로 새 브랜치 생성하기 - Alias 활용 방법

TL:DR

GitHub 프로젝트에서 작업할 때, issue를 사용하여 작업 항목을 추적하고 관리하기로 마음 먹었다. issue와 연관된 작업을 수행하기 위해 새로운 브랜치를 만들어야 하는데 이 과정을 더욱 편하게 하기 위해 alias를 사용하여 GitHub issue를 기반으로 새로운 브랜치를 만들도록 했다.

alt text

필수 설치 사항

  • GitHub CLI : GitHub의 기능을 커맨드 라인에서 사용할 수 있도록 도와주는 도구.
  • fzf : 터미널에서 상호작용적인 검색을 제공하는 유용한 도구.
  • Git

git alias 설정

먼저, 다음과 같이 .gitconfig 파일에 alias를 정의한다.

1
2
[alias]
icb = "!f() { gh issue list | fzf --ansi | awk -F'\\t' '{gsub(\" \", \"_\", $3); print $1\"-\"$3}' | xargs -I {} git checkout -b {}; }; f"

git icb 명령을 실행하면, GitHub issue 목록을 검색하고 선택한 issue를 기반으로 브랜치를 만들 수 있다.

코드 설명

이 코드는 다음과 같은 작업을 수행한다.

  1. gh issue list 명령을 사용하여 현재 GitHub 프로젝트의 issue 목록을 가져옵니다.

  2. fzf를 사용하여 사용자에게 issue 목록을 선택하도록 합니다.

  3. 사용자가 선택한 issue의 번호와 제목을 awk를 사용하여 적절한 형식으로 가공합니다.

    이 단계에서는 awk 명령을 사용하여 사용자가 선택한 issue의 번호와 제목을 가공한다. -F 옵션을 사용하여 입력 필드 구분자를 설정하고, $1$3를 사용하여 선택한 issue의 번호와 제목에 접근했다. 그런 다음, gsub() 함수를 사용하여 제목에서 공백을 밑줄(_)로 대체하여 브랜치 이름으로 사용할 수 있도록 했다. 최종적으로, print 함수를 사용하여 적절한 형식으로 가공된 문자열을 출력하도록 했다.

  4. 최종적으로 git checkout -b 명령을 사용하여 선택한 issue를 기반으로 새로운 브랜치를 만든다.

여담

사실 커맨드 라인에 다음과 같은 명령어를 쳐도 된다.

1
gh issue list | fzf --ansi | awk -F'\t' '{gsub(" ", "_", $3); print $1"-"$3}' | xargs -I {} git checkout -b {}

하지만 이럴 경우 너무 길고, 불편하기 때문에 git alias에 등록하고 했다. git alias를 사용하면 내가 원하는 축약된 이름으로 위의 명령어를 사용할 수 있다고 생각했기 때문이다.

하지만 git alias에서 이 명령어가 통하지 않았다. ㅠㅠ 여기저기 찾아보고 ai의 도움도 받았다.

여기서(링크) 함수를 선언 후 사용할 수 있다고 했길래, f()라는 함수로 감싸서 실행시켰다.

그 다음 문제점은 바로 터미널에서 실행했을 때는 상관없었지만, alias 파일에 등록할 때는 " "로 감싸야했다. 하지만 나는 " "를 사용했기 때문에 꼬이기 시작한 것이다. 이부분에 대해서는 \를 사용하면 해결된다는 것을 알기 때문에 쉽게 해결할 수 있었다. c/c++ 코딩하면서 얻은 경험치가 여기서 사용이 되다니! 아무튼 \""가 문자열의 일부라는 것을 알려주는 것이기에 되는 것이다.

그 외에 다른 명령어들은 chat gpt한테 물어본 후 함수를 알고 그 함수를 이것저것 테스트하면서 내가 원하는 형식으로 만들 수 있게 되었다. 아마 수십번 테스트 한 것이다. 언제 한번 본격적으로 배워야하는데 미루고 있다.

위에는 살짝 가식은 섞은 것이고, 진짜 이유는 맥북에 모니터를 연결하지 못할 경우, 크롬 켰다, vscode 켰다 하는게 귀찮아서 CLI로 해결하자는 마음이 거대해졌기 때문이다.

그리고 fzf를 사용해보고, 왠지 모르게 나의 도전욕구를 건드리고 있다. 많은 아이디어를 생각나게 하는 것은 오랜만인걸. 결과도 바로바로 볼 수 있어서 좋다.

이만 여담은 마치고, 또 재미있거나 유용한 것 만들면 다시 나타날 것이다.

언리얼 엔진에서의 공격 애니메이션 작업기

Warning

이 내용은 2019년에 작성된 내용입니다.

TL:DR

마우스 클릭 시 공격 애니메이션 재생.

공격 애니메이션을 어떻게 추가할까 고민하다가 애니메이션 스테이트 머신에 적용할려고 했다.
하지만 강좌를 찾는 도중에 이 강좌를 찾았다!

https://youtu.be/cWWuDf2ZUj0?list=PLCeaAi_Ah78SEV2Q-iVuFbe6xOtQYH6sH

몽타주 추가

1
2
3
//melee attack montage
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = Animation, meta = (AllowPrivateAccess = "true"))
class UAnimMontage* MeleeAttackMontage;
1
2
3
4
5
6
//몽타주 읽기.
static ConstructorHelpers::FObjectFinder<UAnimMontage> MeleeAttackMontageObject(TEXT("AnimMontage'/Game/Animation/PrimaryAttack_C_Slow_Montage1.PrimaryAttack_C_Slow_Montage1'"));

if (MeleeAttackMontageObject.Succeeded()) {
MeleeAttackMontage = MeleeAttackMontageObject.Object;
}

몽타주는 기존에 있는 것을 팔라곤 에셋인 것을 사용하기로 하고 몽타주는 따로 제작하지 않았다.
몽타주를 읽기 위해서 프로젝트 폴더내에 있는 몽타주를 가져와서 읽는데 경로는 해당하는 레퍼런스 복사를 누르면 된다.

20.06 나중에 리소스 삭제할려고 할 때 레퍼런스 참조로 삭제가 힘들게 뜬다. 삭제한다고 하면, 나중에 컴파일 때 엔진이 크래시나는 문제가 있다. 블루프린터로 따로 빼서 리소스를 등록하게 하는 것이 편하다.

alt text

몽타주 실행 함수

1
2
3
4
void AMyPlayer::AttackStart()
{
PlayAnimMontage(MeleeAttackMontage, -1.f, FName("Default"));
}

설정해놓은 몽타주 애니메이션을 재생시킨다.

키 맵핑

1
2
PlayerInputComponent->BindAction("Attack", IE_Pressed, this, &AMyPlayer::AttackStart);
PlayerInputComponent->BindAction("Attack", IE_Released, this, &AMyPlayer::AttackEnd);

마우스 키를 눌렸을 때 만들어준 AttackStart 함수를 호출하고 마우스 키를 뗄 떼는 AttackEnd함수를 호출한다.

1
2
3
4
void AMyPlayer::AttackEnd()
{
GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Blue, __FUNCTION__);
}

AttackEnd는 별 것 없다. 그냥 쉽게 로그를 확인하기 위해서이다.
alt text
뭔가 끝나는 동작이 이상하다. 애니메이션이 검을 내리고 빠르게 원상 복귀가 되는 것이다.

2019/11/12

1
2
- PlayAnimMontage(MeleeAttackMontage, -1.f, FName("Default"));
+ PlayAnimMontage(MeleeAttackMontage, +1.f, FName("Default"));

-1.f가 아니라 1로하면 자연스럽게 애니메이션이 재생된다. 난 바보 같이 -1로 한거나….ㅠㅠ

검에 콜라이더 추가.

애니메이션이 재생되었다고해도 콜라이더를 붙여서 다른 오브젝트와 충돌 상호작용을 해야한다.

그래서 박스 콜라이더를 붙여보았다.

1
2
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Attack)
class UBoxComponent* MeleeCollisionBox;
1
2
3
4
MeleeCollisionBox = CreateDefaultSubobject<UBoxComponent>(TEXT("MeleeCollisionBox"));
MeleeCollisionBox->SetupAttachment(RootComponent);
MeleeCollisionBox->SetCollisionProfileName("NoCollision");
MeleeCollisionBox->SetHiddenInGame(false);

하지만 원하는 것은 검의 움직임에 따라 같이 콜라이더가 움직이는 것인데 그렇지 않다. ㅠㅠ

https://youtu.be/IWnSlOmVhTM?list=PLCeaAi_Ah78SEV2Q-iVuFbe6xOtQYH6sH

하지만 그 다음 강좌에 콜라이더를 붙이는 강좌가 있다.

1
2
3
4
5
6
7
8
void AMyPlayer::BeginPlay()
{
Super::BeginPlay();
//attach collision components to sockets based on transformation definnate
const FAttachmentTransformRules AttackmentRules(EAttachmentRule::SnapToTarget, EAttachmentRule::SnapToTarget, EAttachmentRule::KeepWorld, false);
MeleeCollisionBox->AttachToComponent(GetMesh(), AttackmentRules, "weapon_r_collison");
GetCapsuleComponent()->OnComponentBeginOverlap.AddDynamic(this, &AMyPlayer::OnOverlapBegin);
}

부착 규칙을 설정해서 콜라이더를 붙여준다.
alt text
해당 메시의 스켈레톤의 “weapon_r_collision”의 중심점으로 만든 콜라이더가 붙는다.
alt text
콜라이더가 검의 움직임에 맞춰서 동작하는 것을 볼 수 있다!!!!

상호 작용하는 모습은 다음과 같다.
alt text

콤보 공격 만들기

alt text
기획서상에 콤보 공격이 존재한다!!! 앞에서 한 것은 콤보공격을 위한 준비과정일뿐.

http://joyeeeeeee.blogspot.com/2017/11/unreal491-combo.html

콤보 공격의 아이디어를 여기서부터 출발했다. 애니메이션 몽타주를 배열로 만든 다음 콤보 수를 배열의 인덱스로 사용하여 재생시키기로 했다!!!!

몽타주 배열 정의

1
2
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = Animation, meta = (AllowPrivateAccess = "true"))
TArray<UAnimMontage * >MeleeAttackMontages;

공격 몽타주를 정의해준다. 언리얼에서는 배열을 TArray로 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static ConstructorHelpers::FObjectFinder<UAnimMontage> MeleeAttackMontageObject1(TEXT("AnimMontage'/Game/Animation/PrimaryAttack_A_Slow_Montage1.PrimaryAttack_A_Slow_Montage1'"));
if (MeleeAttackMontageObject1.Succeeded()) {
MeleeAttackMontages.Add(MeleeAttackMontageObject1.Object);
}

static ConstructorHelpers::FObjectFinder<UAnimMontage> MeleeAttackMontageObject2(TEXT("AnimMontage'/Game/Animation/PrimaryAttack_B_Slow_Montage1.PrimaryAttack_B_Slow_Montage1'"));
if (MeleeAttackMontageObject2.Succeeded()) {
MeleeAttackMontages.Add(MeleeAttackMontageObject2.Object);
}

static ConstructorHelpers::FObjectFinder<UAnimMontage> MeleeAttackMontageObject3(TEXT("AnimMontage'/Game/Animation/PrimaryAttack_C_Slow_Montage1.PrimaryAttack_C_Slow_Montage1'"));
if (MeleeAttackMontageObject3.Succeeded()) {
MeleeAttackMontages.Add(MeleeAttackMontageObject3.Object);
}

몽타주들을 배열에 넣어준다. 그러면 인덱스 값에 따라 몽타주들을 관리할 수 있다!

그 다음 콤보에 대한 설정을 해준다.

1
2
3
4
5
//콤보상태인가?
bool m_bCombo;

//콤보 공격 인덱스
int m_iCombo;

두개의 변수를 만든다. 콤보 상태인가 아닌가를 구분해 줄 bool형 변수와 현재 콤보가 몇 인지 알려주는 변수 2개를 추가했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
void AMyPlayer::AttackStart()
{
//지금 콤보상태인지 아닌지 확인해본다.
if (m_bCombo) {
m_iCombo = (m_iCombo + 1) % 3;
}
else {
//콤보상태가 아닐경우 combo상태로 변환.
m_bCombo = true;
m_iCombo = 0;
}
PlayAnimMontage(MeleeAttackMontages[m_iCombo], 1.f, FName("Default"));
}

공격을 했을 때 콤보 상태인지 아닌지를 확인해서 콤보 상태일 경우 콤보를 1증가시키고 아닐 경우에는 콤보 상태로 바꾸고, 콤보를 0으로 초기화시킨다. 그 이후 콤보 애니메이션을 재생시킨다.

1
2
3
4
5
void AMyPlayer::AttackEnd()
{
m_bCombo = false; //콤보를 취소한다.
m_iCombo = 0;
}

AttackEnd함수에서 콤보를 취소시킨다.
alt text
빠르게 첫 번째 애니메이션만 재생이 된다. ㅠㅠ 그 이유를 알아보니 AttackEnd함수는 마우스를 뗄 떼 콤보랑 콤보 상태가 초기화되기 때문이다. 애니메이션이 종료될 때 AttackEnd함수를 호출해야 한다.

그렇다면 애니메이션이 끝날 때를 판단할 수 있을까?
https://darkcatgame.tistory.com/4

이 블로그를 참고하여 인터페이스 함수를 제작하고 그 함수를 호출시키는 Anim Notify 제작 후 연동시켰다.

Interface Class 제작

1
2
3
4
5
// Itf_AnimationNotify.h

//콤보가 취소될 때 호출
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Itf_AnimState")
void Itf_ResetCombo();

캐릭터 클래스에서 함수 정의를 해준다. 참고로 캐릭터 클래스는 Itf_AnimationNotify를 상속받아야 한다.

1
2
3
4
5
6
// MyPlayer.h

//콤보 리셋 메시지 받기.
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Itf_AnimState")
void Itf_ResetCombo();
virtual void Itf_ResetCombo_Implementation() override;
1
2
3
4
5
6
7
// MyPlayer.cpp

void AMyPlayer::Itf_ResetCombo_Implementation()
{
m_bCombo = false; //콤보를 취소한다.
m_iCombo = 0;
}

인터페이스로 만든 함수가 호출 시 이 함수가 호출 될 것이다. 호출이 되면 애니메이션이 끝나게 되는 상황이기 때문에 콤보를 초기화 해준다.
그 다음 애니메이션에서 애니메이션이 끝나는 부분에 Interface함수를 호출해야 한다.

그렇게 하기 위해서는 AnimNotify를 만들어 준 후 Anim Notify Blueprint 클래스를 제작해야한다.

alt text

AnimNotify Blueprint 클래스를 제작한다.
alt text

Interface함수를 호출하는 노드를 제작한다.
alt text

원하는 애니메이션 시점에 만들어준 Notify를 추가하면 된다. 이렇게 해서 끝나는 시점까지 누르지 않으면 콤보가 초기화 되는 조작을 하게 되었다.

그런데 말입니다….
alt text
누르는 속도에 따라 엄청나게 빠르게 칼질하는 플레이어의 모습을 볼 수 있다.

이렇게 되는 이유는 마우스 클릭할 때마다 AttackStart()의 애니메이션이 재생되기 때문이다. 자연스러운 동작을 보고 싶으면 애니메이션이 끝날 때 콤보가 있을 경우 다음 애니메이션으로 보여주게 하는 것이다.

그러면 우선 AttackStart부분을 수정해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// MyPlayer.cpp

void AMyPlayer::AttackStart()
{
//attack이 true 일 때 클릭하면
if (m_bAttack) {
//콤보상태를 true한다.
m_bCombo = true;
}
else {//그렇지 않을 경우
//m_bAttack 상태를 true 바꾸고, 콤보도 0으로 바꾸고
m_bAttack = true; m_iCombo = 0;

//애니메이션을 재생시킨다.
GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Blue, __FUNCTION__);
PlayAnimMontage(MeleeAttackMontages[m_iCombo], 1.f, FName("Default"));
}
}

m_bAttack 변수를 주어 그것이 true일 때 클릭하는 경우 콤보상태로 바꾸고 그렇지 않을 경우에는 m_bAttack 상태를 true로 바꾼다음 콤보를 0으로 바꿔준다.

그 이유는 m_bAttack 상태가 false인 경우 현재 공격이 아닌 다른 애니메이션이 재생되었다가 공격을 하는 거라서 첫번째 애니메이션이 재생이 되어야 한다. 그래서 m_iCombo0으로 초기화한 것이다.

이렇게 m_bAttack 상태를 true로 바꿔서 한번 더 누를 경우 m_bAttacktrue이기 때문에 콤보상태가 true로 바꿔진다.

그 다음 Itf_ResetCombo_Implementation에서 콤보와 애니메이션을 설정 작업을 해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// MyPlayer.cpp

//콤보가 리셋될 때 호출 됨.
void AMyPlayer::Itf_ResetCombo_Implementation()
{
GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, __FUNCTION__);

if (m_bCombo) { //콤보 상태일 경우
//콤보를 1 증가 시킨다.
m_iCombo = (m_iCombo + 1) % 3;
//다음 애니메이션을 재생시킨다.
PlayAnimMontage(MeleeAttackMontages[m_iCombo], 1.f, FName("Default"));
}
else { //콤보 상태가 아닐 경우
m_bAttack = false; //공격 상태를 false로 한다.
m_bCombo = false; //콤보를 취소한다.
m_iCombo = 0; //0으로 초기화한다.
}

m_bCombo = false; //콤보 상태를 해제 시킨다.
}

그 후 애니메이션이 설정한 시간을 도달하게 되면 함수가 호출된다. 우선 콤보상태일 경우 콤보를 하나 더 증가시킨다.(애니메이션이 종료되기 전에 공격 키를 눌려 콤보를 유지시켰기 때문에) 그 후 증가시킨 인덱스의 애니메이션을 재생시킨다.

콤보상태가 아닐 경우 설정해둔 변수들을 초기화 시켜준다.(공격이 계속 진행되는 상황이라도 그 공격 애니메이션이 콤보 종료 상태가 들어가기 전에는 한 번 더 조작키를 눌려줘야지 콤보로 인식하기 때문이다.)

그 후 m_bCombo는 무조건 초기화를 해줘야한다.
alt text

이렇게 아무리 빨리 눌려도 애니메이션이 끝나야지 다음 애니메이션으로 넘어가게 만들었다. 자연스러운 애니메이션 동작이 확인가능하게 되었다. 짝짝짝-

여담

2023.02.29

오랜만에 옛날 자료를 보다가 잘 정리된 자료를 찾았습니다. 졸업과제 하면서 시행착오도 거치고, 많이 공부했다는 것이 보이네요. 옛날 자료이기는 하지만 누군가가 보고 도움이 되기를 바라면 백업을 해두겠습니다.

메모리 문제 해결하기

TL;DR

Web에서 웹캠 프레임 데이터를 받아 처리를 하는 프로그램을 만들고 있다. 근데 시간이 지나면 메모리 문제가 발생한다. 이를 해결하기 위해 여러 방법을 시도하게 되는데!

현재 상황과 문제점

프로세스는 웹캠 프레임 데이터를 처리하기 위해 큐를 활용하고 있다. 그러나 매번 새로운 데이터가 추가될 때마다 메모리를 할당하고 큐에 저장한 후, 데이터를 처리한 후에 해당 데이터의 메모리를 해제해야 하는 과정에서 문제가 발생하고 있다. 이 과정에서 메모리 할당이 해제보다 빠르게 진행되어 메모리 사용량이 지나치게 늘어나는 현상이 나타나고 있다. 이로 인해 큐에 저장되는 데이터의 양이 해제되는 데이터의 양보다 많아지는 문제가 발생하고 있다.

alt text

현재 설정된 최대 메모리 한계는 약 2GB이지만, 실제 데이터가 저장되는 양은 이를 초과한다. 예를 들어, 각 데이터가 800(width) * 600(height) * 4(rgba) 바이트 크기로 저장되고 이러한 데이터가 1729개 저장된다면 전체 메모리 사용량은 3.18GB에 이를 것이다.

해결 방안

이 문제를 해결하기 위해 매번 새로운 데이터가 추가될 때마다 메모리를 할당하는 것보다는 미리 몇 개의 메모리를 할당하고 재사용하는 방법을 추천 받았다. 이를 위해 원형 큐를 변형하여 사용하기로 결정했다.

원형 큐를 선택한 이유는 미리 몇 개의 메모리를 할당하여 재사용할 수 있으며, 큐가 가득 찬 경우 가장 오래된 데이터를 제거하고 새로운 데이터를 추가할 수 있기 때문이다. 이러한 방식은 웹캠 프레임 데이터의 손실이 허용되었기 때문에 가능했다.

해야할 조치는 다음과 같다.

  1. 매번 데이터를 저장할 때마다 새로운 메모리를 할당하는 것이 아니라, 몇 개의 메모리를 미리 할당하여 재사용해야한다.
  2. 큐에 데이터가 가득 찬 경우, 가장 오래된 데이터를 제거하고 새로운 데이터를 추가해야 한다. 이를 통해 큐의 크기를 일정하게 유지할 수 있다.

원형 큐 활용하기

위의 문제를 해결하기 위해 원형 큐를 활용하여 구현했다.

원형 큐의 생성

1
2
3
4
5
CircularQueue(unsigned int buffer_size) : head_(0), tail_(0), size_(0), buffer_size_(buffer_size) {
for (int i = 0; i < MAX_BUFFER_SIZE; i++) {
data_.push_back(new unsigned char[buffer_size]);
}
}

원하는 버퍼 사이즈만큼 추가하여 원형 큐를 생성한다. 이때, head는 나중에 데이터를 가져올 때의 인덱스를, tail은 데이터를 추가할 위치를 나타낸다.

데이터 추가

1
2
3
4
5
6
7
8
9
10
void Enqueue(unsigned char* value) {
if (size_ < MAX_BUFFER_SIZE) {
size_++;
} else {
head_ = (head_ + 1) % MAX_BUFFER_SIZE;
}

std::memcpy(data_[tail_], value, buffer_size_);
tail_ = (tail_ + 1) % MAX_BUFFER_SIZE;
}

데이터를 추가할 때, 큐의 크기를 넘어서는 경우 가장 오래된 데이터를 제거한 후 새로운 데이터를 추가한다.

원형 큐의 특성상 가장 오래된 데이터의 위치는 head에 저장되어 있다. 따라서 head를 한칸씩 증가시킴으로써 가장 오래된 데이터를 제거하고 새로운 데이터를 추가할 공간을 만들어 준다.

새로운 데이터를 큐의 tail 위치에 복사합니다. 이때 std::memcpy 함수를 사용하여 새로운 데이터를 큐에 복사합니다. 그리고 tail_을 한 칸 증가시켜 다음 데이터가 저장될 위치를 나타낸다.

이렇게 함으로써 큐의 크기를 넘어가는 경우 가장 오래된 데이터를 제거하고 새로운 데이터를 추가할 수 있게된다.

데이터 가져오기

1
2
3
4
5
6
7
8
9
unsigned char* Dequeue() {
if (size_ == 0) {
return nullptr;
}
unsigned char* value = data_[head_];
head_ = (head_ + 1) % MAX_BUFFER_SIZE;
size_--;
return value;
}

가장 오래된 데이터를 큐에서 가져온다.

큐가 비어있지 않는 경우 큐에서 가장 오래된 데이터를 가져와야한다. 이를 위해 head가 가리키는 위치에 있는 데이터를 가져와 value에 저장한다. 그리고 head를 한 칸 증가시켜 다음으로 가져올 데이터의 위치를 나타낸다.

마지막으로, 큐의 크기를 하나 줄여주고 가져온 데이터를 반환한다.

이를 통해 데이터가 큐의 크기를 넘어가게 되면 가장 오래된 데이터를 지우고 순차적으로 데이터를 배치할 수 있다.

Finish

이러한 데이터를 처리하는 작업은 하지 않아서 메모리 문제가 발생했을 때 당황했었다. 다행히 선배들이 방법을 알려줘서 쉽게 해결할 수 있었다. 무지 무지 감사합니다~

덕분에 메모리가 터치지 않고, 웹에서 웹캠에 새그멘테이션을 적용하고 있다. 좀 더 최적화 해야하고 API도 다듬도 할게 많다!

RenderDoc 사용 방법

TL;DR

OpenGL을 사용하여 작업하는데 렌더링이 제대로 되지 않는 경우가 빈번해서 꽤나 골치아팠다. 이럴 때 디버깅을 할 수 있는 도구가 있을까 찾아보다가 렌더독(RenderDoc)을 알게 되었다! 렌더독을 사용하면서 많은 문제들을 해결할 수 있게 되었다. 지금은 렌더독을 사용하는 방법에 대해 정리해보고자 한다.

설치

https://renderdoc.org/builds 에서 파일을 다운 받아 설치

Capturing a frame

File -> Launch Application
Alt text

Program Setting

  • Excutable Path : 캡쳐할 어플리케이션의 실행 파일 경로를 선택. 캡쳐하려는 프로그램이 어디에 설치되어 있는지 정확히 지정해야한다.
  • Working Directory : 어플리케이션을 실행할 때 필요한 작업 디렉토리를 설정. 어플리케이션이 관련된 파일을 찾을 수 있도록 정확한 디렉토리를 지정해야 한다.
  • Command-line Arguments : 어플리케이션을 실행할 때 필요한 커맨드 라인 인자를 설정.

Launch 버튼으로 실행.

프레임 캡처

RenderDoc이 성공적으로 로드외어 프레임을 캡쳐할 준비가 되었음을 나타내기 위한 최소한의 인앱 오버레이가 존재.
준비가 되면 F12 또는 PrintScreen를 누르면 다음 프레임이 캡쳐되며 이는 오버레이에 표시되어 프레임이 성공적으로 저장되었음을 알려줌.
Alt text
어플리케이션이 종료될 때 프레임을 캡처한 경우 자동으로 RenderDoc UI에서 열리기 시작함.

그래픽 파이프라인 검사

파이프라인 뷰어는 그래픽 파이프라인의 모든 상태 설정을 보여주며, 바인딩 된 리소스, 래스터라이저 설정 등을 포함한다.
Window -> Pipeline State
Alt text

파이프라인 흐름도

파이프라인 뷰어 상단에 파이프라인 흐름도가 존재.
그래픽 파이프라인의 고수준 블록 레벨 다이어그램을 보여줌.
Alt text

  • 빨간색 : 선택한 블록.
  • 연한 회색 : 현재 활성화되어 이 작업에 참여하는 부분.
  • 어두운 회색 : 비활성화되어 패스스루 / 아무 동작도 수행하지 않음.

버퍼 검사

파이프라인 단계(VTX(Vertex Input), VS, FS) 에서 버퍼를 확인할 수 있다.
Alt text

  • glDrawElements 이벤트의 VTX (Vertex Input)단계의 버퍼 확인.

이동 아이콘을 누르면 더 자세한 보기가 가능하다.
Alt text

  • 자세한 보기를 누른 상태.
  • vertex.aPos의 값을 확인할 수 있다.

텍스처 검사

텍스처 뷰어는 어플리케이션에서 텍스처를 검사하고 대상을 렌더링 할 수 있다.
다양한 시각화 컨트롤이 있어 여러 채널, 밉 레벨 등을 볼 수 있다.
Alt text

사용 예시

VertexShader Input 값과 Output 값을 확인(Opengl)

  1. Event Browser에서 glDrawElements 이벤트 선택.
  2. Pipeline State에서 VTX(Vertex Input) 단계 Vertex Attribute Format을 아무거나 Go(초록색 화살표 클릭).
    Alt text
  3. Mesh Viewer 창이 뜨며 VS Input에서 입력 값과 VS Output에서 버택스 셰이더의 아웃풋 값을 확인할 수 있다.
    Alt text

Preview에서 메시를 확인할 수 있다.

  • VS In의 메시.
    Alt text

  • VS Out의 메시.
    Alt text

Windows에서 Visual Studio Code로 C++ 개발 환경 설정하기

TL;DR

주로 macOS에서 개발하는데 하필이면 렌더독이라고 그래픽관련 디버깅 도구가 윈도우에서 작동이 된다고 한다. 그래서 디버깅을 하기 위해 윈도우 개발 환경을 설정하기로 하는데!!!

전제 조건

  1. 비주얼 스튜디오 코드 설치.
  2. VS Code용 C/C++ 확장 설치.
  3. Microsoft Visual C++(MSVC) 컴파일러 도구 세트를 설치.
  4. CMake 설치.
    • C++를 이용한 데스크톱 개발를 설치할 때 같이 설정이 되는데 만약 설정 해제하고 설치했다면 따로 설치하도록 한다.

프로젝트 생성

샘플코드를 하나 만들어 준다.

main.cpp

1
2
3
4
5
6
#include <iostream>

int main() {
std::cout << "Hello, World!" << std::endl;
return 0;
}

CMakeLists.txt

1
2
3
4
5
cmake_minimum_required(VERSION 3.0)

project(SampleCPlusPlusProject)

add_executable(SampleCPlusPlusProject main.cpp)

빌드

1
2
cmake -B ./build -G "Visual Studio 16 2019" .
cmake --build ./build --config Debug

해당 명령어를 사용하면 빌드를 할 수 있지만, Visual Studio Code를 사용하면 쉽게 할 수 있다.

vscode 폴더

.vscode 폴더는 Visual Studio Code 프로젝트나 작업 공간에서 설정과 관련된 파일을 보관하는 디렉토리이다. 이 폴더는 특정 프로젝트 또는 작업에 대한 설정 및 환경 구성을 담당한다.

  • tasks.json (빌드 명령어 설정 파일): 이 파일은 프로젝트를 빌드하기 위한 명령어와 설정을 정의.
  • launch.json (디버깅 설정 파일) : 디버깅을 위한 설정을 담고 있으며, 디버거 설정과 실행 환경을 구성.

생성된 파일들은 Visual Studio Code에서 C++ 개발 환경을 구성하고 프로젝트를 관리하는 데 도움을 준다.

tasks.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"version": "2.0.0",
"tasks": [
{
"label": "cmake build and run", // 작업 이름
"type": "shell", // 명령어 실행을 위한 shell 타입
"command": "cmake -B ./build -G 'Visual Studio 16 2019' .; cmake --build ./build --config Debug; cd ./build/Debug; ./SampleCPlusPlusProject.exe", // 빌드 및 실행 커맨드 입력
"options": {
"cwd": "${workspaceFolder}" // 현재 작업 디렉토리 설정
},
"group": {
"kind": "build", // 작업 그룹을 빌드로 설정
"isDefault": true // 기본 작업으로 설정
}
}
]
}

ctrl + shift + b 를 누르면 프로그램이 실행되는 것을 확인할 수 있다.

launch.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"version": "0.2.0",
"configurations": [
{
"name": "cmake build and debug", // 디버그 구성 이름
"type": "cppvsdbg", // C++ 디버그 타입
"request": "launch", // 디버그 실행 요청
"program": "${workspaceFolder}/build/Debug/SampleCPlusPlusProject.exe", // 실행할 바이너리 파일 경로
"args": [], // 실행 시 전달할 명령행 인수
"stopAtEntry": true, // 브레이크 포인트 사용 여부
"cwd": "", // 현재 작업 디렉토리 (의존성 및 기타 파일을 찾기 위함)
"environment": [], // 환경 변수 설정
"externalConsole": true, // 외부 콘솔 사용 여부
"preLaunchTask": "cmake build and run" // 디버그 실행 전에 실행할 작업(Task) 이름
}
]
}

F5를 누르면 디버거가 실행되며 디버깅을 할 수 있게 된다.

Finish

이제 명령어를 안치고 간단한 단축키로 실행파일을 실행하고, 디버깅도 할 수 있다!!! 쉘 스크립트나 CMake를 이용해서 하나의 명령어로 OS별로 빌드, 실행, 디버깅되면 편하겠다. 이 부분에 대해서 좀 더 연구해봐야 할 듯!!!

참고 링크

GLM 행렬(mat) 또는 벡터(vec) 출력하기

TL;DR

glm의 계산 결과를 확인하기 위해서 출력해야하는 경우가 있다. 이럴 때는 glm/gtx/string_cast.hpp을 인클루드 하고 glm::to_string 함수를 호출하면 된다.

Code

1
2
3
4
#include <glm/gtx/string_cast.hpp>

glm::mat4 mat;
std::cout << glm::to_string(mat) << std::endl;

Example

벡터 출력

1
2
3
4
5
6
glm::vec3 vector1(1.0f, 2.0f, 3.0f);
glm::vec3 vector2(4.0f, 5.0f, 6.0f);

glm::vec3 result_vec = vector1 + vector2;

std::cout << glm::to_string(result_vec) << std::endl;

결과

1
vec3(5.000000, 7.000000, 9.000000)

행렬 출력

1
2
glm::mat4 result_mat = glm::translate(glm::mat4(1.0f), glm::vec3(1.0f, 2.0f, 3.0f));
std::cout << glm::to_string(result_mat) << std::endl;

결과

1
mat4x4((1.000000, 0.000000, 0.000000, 0.000000), (0.000000, 1.000000, 0.000000, 0.000000), (0.000000, 0.000000, 1.000000, 0.000000), (1.000000, 2.000000, 3.000000, 1.000000))

정리하면 다음과 같다.

1
2
3
4
(1.000000, 0.000000, 0.000000, 0.000000)
(0.000000, 1.000000, 0.000000, 0.000000)
(0.000000, 0.000000, 1.000000, 0.000000)
(1.000000, 2.000000, 3.000000, 1.000000)

glm은 열중심으로 출력되어 있는 것을 확인할 수 있다.

여담