62. 애니메이션_애니메이션#2_CreateAnimationTransform, CreateTexture
ModelAnimator::Update에서 //TODO를 채우면 된다.
이게 쉽지가 않다.
1. ModelAnimator.h에 2차원 배열 transforms를 담은 struct AnimTransform 정의하기
텍스쳐를 만들어서 모든 애니메이션의 모든 본의 모든 트랜스폼에 대한 정보를 담아서 전달해 줄 것이다.
애니메이션마다 2차 배열이 있으니까 3차 배열이라고 볼 수 있다.
ModelAnimator.h에
struct AnimTransform
{
// [ ][ ][ ][ ][ ][ ][ ] ... 250개
using TransformArrayType = array<Matrix, MAX_MODEL_TRANSFORMS>;
};
뼈가 최대 250개라는 얘기가 된다. 이걸 이용해서 배열을 만들게 되면 250개의 관절 정보를 담아줄 수 있게 된다.
RenderManager.h에
#define MAX_MODEL_KEYFRAMES 500
을 추가한다.
2차배열을 만들어야 하니까 array를 하나 더 만든다.
struct AnimTransform
{
// [ ][ ][ ][ ][ ][ ][ ] ... 250개
using TransformArrayType = array<Matrix, MAX_MODEL_TRANSFORMS>;
// [ ][ ][ ][ ][ ][ ][ ] ... 500 개
array<TransformArrayType, MAX_MODEL_KEYFRAMES> transforms;
};
transforms 에는 TransformArrayType의 배열이 있는 것이다. TransformArrayType은 Matrix의 배열이다. 즉 2차 배열이 된 것이다.
500개의 프레임에 프레임 당 250개의 본이 들어갈 수 있는 2차원 배열을 만들었는데 이런 게 하나의 애니메이션인 거고, 애니메이션이 2,3,4개 늘어날 수 있으니까 이런 그림 같은 게 몇 칸씩 늘어나게 되는 3차원 공간이라 볼 수 있다.
2차 배열인 transforms가 완성이 되었다.
2. ModelAnimator.h에 vector<AnimTransform> _animTransforms;를 선언하고 이를 채워주고 Texture와 View로 만들 준비 하기
그거를 ModelAnimator.h에 만들어준다.
private:
vector<AnimTransform> _animTransforms;
이걸 꽉꽉 채워준 다음에 거대한 텍스쳐로 만들어서 그 텍스쳐를 쉐이더한테 건내주는 그런 방식으로 작업을 하게 될 것이다.
private:
void CreateTexture();
void CreateAnimationTransform(uint32 index);
각기 두개의 함수를 이렇게 만들어서 작업을 할 것인데
CreateTexture에서 _animTransforms를 먼저 채워준 다음에 그걸 이용해서 texture로 만들어줄 거라서
ComPtr<ID3D11Texture2D> _texture;
이걸 선언하고
Texture는 리소스고 GPU에 넘겨줄 때는 그걸 View라는 걸로 만들었다.
ComPtr<ID3D11ShaderResourceView> _srv;
이렇게 _srv를 선언한다.
3. T포즈일 때 Global 좌표를 Relative 좌표로 변환한 뒤 다시 Global 좌표로 변환하는 ModelAnimator::CreateAnimationTransform함수 정의하기
ModelAnimator::Update의 //TODO에서
// TODO
if (_texture == nullptr)
CreateTexture();
texture가 없으면 texture를 만들어서 진행을 할 것이다.
한 번만 만들어 주면 되는데 Load 할 때 초기화 함수에서 해도 되긴 하지만 Update에서 해준다.
ModelAnimator::CreateTexture에서
void ModelAnimator::CreateTexture()
{
if (_model->GetAnimationCount() == 0)
return;
_animTransforms.resize(_model->GetAnimationCount());
for (uint32 i = 0; i < _model->GetAnimationCount(); i++)
CreateAnimationTransform(i);
}
CreateAnimationTransform 이 부분이 굉장히 중요하다.
ModelAnimator.cpp에
#include "ModelAnimation.h"
를 추가하고
CreateAnimationTransform을 정의한다.
void ModelAnimator::CreateAnimationTransform(uint32 index)
{
vector<Matrix> tempAnimBoneTransforms(MAX_MODEL_TRANSFORMS, Matrix::Identity); // 캐싱 용도로 사용한다.
shared_ptr<ModelAnimation> animation = _model->GetAnimationByIndex(index); //작업해야 할 애니메이션
for (uint32 f = 0; f < animation->frameCount; f++) // 프레임 순회
{
for (uint32 b = 0; b < _model->GetBoneCount(); b++) // 본 순회
{
shared_ptr<ModelBone> bone = _model->GetBoneByIndex(b);
Matrix matAnimation;
shared_ptr<ModelKeyframe> frame = animation->GetKeyframe(bone->name);
if (frame != nullptr)
{
ModelKeyframeData& data = frame->transforms[f]; // bone이름으로 찾은 행렬중에서 해당 프레임 정보에 접근
Matrix S, R, T;
// 수학라이브러리에서 제공하는 S,R,T 구하는 함수들
S = Matrix::CreateScale(data.scale.x, data.scale.y, data.scale.z);
R = Matrix::CreateFromQuaternion(data.rotation);
T = Matrix::CreateTranslation(data.translation.x, data.translation.y, data.translation.z);
matAnimation = S * R * T;
// 특정 본의 특정 프레임에 관련된 SRT를 구해왔다.
// SRT를 곱한게 어떤 의미를 들고 있는 걸까. global로 넘어간 상태가 아니라 animation의 relative한 상태의 변환행렬이라고 볼 수 있다.
// 상위 부모로 넘어가고 싶은 그 상태라고 볼 수 있다.
}
else
{
matAnimation = Matrix::Identity;
}
// ! 여기서 해주고 싶은 건 parnet를 곱해주고 싶다
// 지금까지 구해왔던 애들 중에서 부모를 찾아서 거기다 기입하는 작업을 해봤다.
// Converter::ReadModelData에서 좌표를 구해줄 때 Relative로 구해줬지만,
// 그걸 다시 global로 넘겨주기 위해 transform*matParent를 해서 저장을 해준 부분이랑 코드가 유사하게 흐른다.
// 지금 까지 구한 애들을 vector<Matrix> tempAnimBoneTransforms(MAX_MODEL_TRANSFORMS, Matrix::Identity); 여기다가 일단 저 장해 줄 것이다.
// !!!!! 오늘의 핵심
Matrix toRootMatrix = bone->transform; // bone->transform이 무언지 생각하면 사실상 global 변환 행렬이다. 유사한 코드인 Converter::ReadModelData 흐름 참조.
Matrix invGlobal = toRootMatrix.Invert(); // 거꾸로 relative한 영역으로 데리고 왜고 싶다면
int32 parentIndex = bone->parentIndex;
// 애니메이션과 관련된 부분 연산
Matrix matParent = Matrix::Identity;
if (parentIndex >= 0) // 즉 부모가 있다고 한다면
matParent = tempAnimBoneTransforms[parentIndex]; // 구하는 거 마다 업데이트를 해주는 아이
tempAnimBoneTransforms[b] = matAnimation * matParent; // matAnimatino은 SRT라 상위 부모로 가는 행렬, matParent는 나의 부모님에서 global까지 쭉 뚫고 올라가는 그 부분을 저장한 것
// b를 0 부터 시작하는 이유
// tempAnimBoneTransforms에 들어가는 정보들이 어떠한 뼈대를 정한다 해도 그 뼈대에서 global로 가는 그 행렬을 여기서 구해주게 된다.
// 관절을 기준으로 한 거에서 애니메이션이 틀어진 상태의 글로벌로 넘어가는 그 상태가 되는 것이기 때문에
// 최종적으로 결론을 내리자면
_animTransforms[index].transforms[f][b] = invGlobal * tempAnimBoneTransforms[b];
// 중요한 건 왜 이렇게 되는지를 이해하는 게 중요하다.
}
}
}
Converter::ReadModelData에서 비슷하게 흘러갔던 부분
// Relative Transform
Matrix transform(node->mTransformation[0]);
bone->transform = transform.Transpose();
// 2) Root (Local)
Matrix matParent = Matrix::Identity;
if (parent >= 0)
matParent = _bones[parent]->transform;
// Local (Root) Transform
bone->transform = bone->transform * matParent;
지금 알고 있는 정점 정보들은 티포즈를 기준으로 했을 때의 좌표들을 갖고 있는 것이다. 이걸 포즈를 변환한 좌표로 바꿔야 하는데 이걸 바로 넘어갈 수 있는 수단이 없다. 그래서 한번 내려와서 특정한 Bone( 특정한 관절 )을 기준으로 하는 좌표계로 변환을 하는데 이게 Global에서 Relative로 변환하는 invGlobal이라고 하는 이 것을 적용해서 Relative로 간 다음에 SRT를 누적시켜서 구한 tempAnimBoneTransforms[b]를 이용해 Relative에서 Global로 넘어가고 있는 좌표로 다시 변환을 해줘야 된다.
이 부분이 오늘의 핵심이다.
T포즈 기준으로 했을 때 첫번째 Global 영역에 좌표가 있고, 변화해 가지고 관절을 기준으로 하는 상대 좌표로 바꾼 다음에, 그거를 다시 돌아가서 Global로 한다.
자신을 기준으로 하는 거에서 부모로 가는, 그리고 부모로 쭉쭉 타고 가다 보면은 빠져나와서 루트를 기준으로 하는 그게 사실상 글로벌이라고 부르는 그거까지 가는 거를 익숙해져야 이걸 이해할 수 있다고 했는데 수식은 간단하다. 이해하는 게 쉽지 않다.
결국
_animTransforms[index].transforms[f][b] = invGlobal * tempAnimBoneTransforms[b];
이것의 수치가 구해진다고 하면 이거를 만약에 우리가 어떠한 정점이 있는데, 정점에다가 해당하는 이걸 곱할 수만 있다고 하면 그러면 해당 위치로 뼈대가 이동하는 변환 행렬이 된다라고 볼 수 있다.
여기서 하나 디테일을 더 나가자면
특정 점점은 하나의 뼈대에 묶여 있으면 하나의 매트릭스만 적용하면 되겠지만
여기선 4개의 뼈에 묶여 있다고 했다.
가중치를 적용하는 작업을 쉐이더에서 4번을 해줘야겠지만 근본적으로 봤을 때 여기가 가장 핵심이라고 볼 수 있다.
Converter::ReadModelData와 비슷하지만 다른 점은 Mesh와 관련된게 아니라 애니메이션의 포지션을 구해주는 그 부분이 구해졌다고 보면 된다.
심지어 프레임도 여러개가 있다. 본이 여러 개 있으니까 엄청 많다고 볼 수 있다.
뭔가를 열심히 만들었다. 어떤 정점에 이 아이를 곱하면 최종적으로 해당 애니메이션의 특정 프레임에 해당하는 위치를 구할 수 있다.
이거를 이해하는게 절반이고 끝이 아니다.
이 구해준 데이터를 들고 있는 거에서 끝나는 게 아니라 texture로 만들어서 넘겨줘야 한다고 했다.
텍스쳐를 만드는게 간단하지 않다.
4. texture리소스를 만들고, 텍스쳐를 묘사하는 ShaderResourceView를 만드는 ModelAnimator::CreateTexture를 정의하기
void ModelAnimator::CreateTexture()
{
if (_model->GetAnimationCount() == 0)
return;
_animTransforms.resize(_model->GetAnimationCount());
for (uint32 i = 0; i < _model->GetAnimationCount(); i++)
CreateAnimationTransform(i);
}
CreateTexture에서 그다음에 해야 하는 게 texture 리소스도 만들어줘야 하고, 그 텍스쳐 리소스를 묘사하는 SRV를 만들어서 그 SRV를 GPU에 던져주는 방식으로 작업을 해야 한다.
1) D3D11_TEXTURE2D_DESC desc 세팅하기
// CreateTexture
{
D3D11_TEXTURE2D_DESC desc;
ZeroMemory(&desc, sizeof(D3D11_TEXTURE2D_DESC));
desc.Width = MAX_MODEL_TRANSFORMS * 4;
desc.Height = MAX_MODEL_KEYFRAMES;
desc.ArraySize = _model->GetAnimationCount();
desc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT; // 16바이트
desc.Usage = D3D11_USAGE_IMMUTABLE; // 한번 설정하면 안고치겠다
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
desc.MipLevels = 1;
desc.SampleDesc.Count = 1;
여기서 설명할게 많다.
만들어줄 때 쉐이더 쪽에서 Texutre2DArray라는 거로 texture 배열 같은 느낌으로 만들어 줄 수가 있다. ArraySize가 그래서 들어간다.
desc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
이렇게 했는데 즉 128비트가 된다는 얘기인데 한마디로 16바이트로 한다는 얘기다.
이거보다 더 큰 포멧이 딱히 존재하지 않는다.
여기서 문제가 되는건 그림에서 보면 FinalMat하나를 각 칸에 저장해야 하는데, Mat 하나는 크기가 얼마일까. 4x4로 16개가 있다 보니까 64바이트가 될 것이다. 문제는 최대로 텍스쳐 하나를 만들 때 넣어줄 수 있는 포맷 자체가 16바이트가 최대이다 보니까 어쩔 수 없이 이걸 더 늘려서 FinalMat을 4개로 쪼개가지고 4칸을 사용하는 식으로 작업을 하게 될 것이다.
4칸짜리인데 한칸처럼 분류를 해가지고 알아서 가져가세요 형식으로 만들 것이기 때문에
desc.Width = MAX_MODEL_TRANSFORMS * 4;
이렇게 4가 곱해져있다.
width는 최대 250개의 뼈대에다가 4를 곱한 건데, 4칸씩 하나로 묶어서 매트릭스 하나를 넣어줄 것이기 때문에 4를 곱했다.
2) 버퍼로 사용할 메모리 할당하기
텍스쳐를 일단 만들어 준 다음에 그다음에 텍스쳐를 버퍼에 데이터를 기입해서 만들어 줘야 된다. 먼저 데이터를 넣어줄 때는 항상 버퍼를 넣어서 카피를 한 다음에 뚜껑을 닫아주는 식으로 작업을 했었다. 비슷하게 여기서도 아주 큰 버퍼를 만들어준 다음에 거기에 버퍼를 채워줄 것이다. 그 해당 메모리를 처음에 텍스쳐를 만들 때 건네주면 된다.
const uint32 dataSize = MAX_MODEL_TRANSFORMS * sizeof(Matrix);
const uint32 pageSize = dataSize * MAX_MODEL_KEYFRAMES;
void* mallocPtr = ::malloc(pageSize * _model->GetAnimationCount());
이렇게 최종 사이즈만큼 메모리를 할당해서 들고 있다.
new, delete는 C++에서 사용하는 거지만 C에서는 malloc free가 세트를 이룬다.
둘의 차이는 뭘까?
생성자 호출만 차이가 있다. malloc은 생성자 호출이 안된다. 메모리를 할당해서 필요 없어지면 free를 해주면 된다.
3) 파편화된 데이터를 조립하기
우리가 원하는 만큼 큰 영역을 받아가지고, 거기다가 파편화된 데이터를 하나씩 조립을 해주기 시작할 것이다.
// 파편화된 데이터를 조립한다.
for (uint32 c = 0; c < _model->GetAnimationCount(); c++)
{
uint32 startOffset = c * pageSize;
BYTE* pageStartPtr = reinterpret_cast<BYTE*>(mallocPtr) + startOffset;
for (uint32 f = 0; f < MAX_MODEL_KEYFRAMES; f++)
{
void* ptr = pageStartPtr + dataSize * f;
::memcpy(ptr, _animTransforms[c].transforms[f].data(), dataSize);
}
}
포인터에 익숙하지 않으면 어려울 수 있다.
포인터가 작업하는 코드를 보면 캐스팅을 해서 BYTE나 char형 포인터로 바꿔치기하는 경우가 종종 있다 왜 하는 것일까?
C++ 작업하면 빠르게 볼 수 있어야 한다.
포인터의 덧셈 뺄셈 연산을 할 때는 항상 조심해야 하는 게 mallocPtr+4를 한다고 진짜 4가 더해지는 게 아니다.
mallocPtr 포인터 타입이 어떤 포인터 타입인지에 따라 다르다. int 형 포인터에 4를 더하면 16이라는 숫자가 더해진다. 원하는 크기만큼 포인터를 이동시키고 싶다면 1바이트짜리 데이터를 가리키는 포인터로 바꿔줘야 덧셈, 뺄셈 연산을 편하게 할 수 있다.
BYTE*로 캐스팅을 안 해주면 +startOffset에 엉뚱한 값이 곱해져서 더해지게 될 수 있다.
그 부분 때문에 캐스팅을 해주는 것이다.
특히나 언젠가 메모리 할당기 같은 거 만들고, 메모리 풀링을 하고 이럴 때 이런 거 많이 사용하게 된다. 원하는 공간에다가 딱 접근해가지고 거기다 뭔가 만들어주고 이럴 부분이 많이 생기는데 그 때 항상 이런거 조심해야 한다.
결국 vector<AnimTransform> _animTransforms가 들고 있는 게 비록 개념적으로는 2차 배열이긴 하지만 array의 array로 만들었고, array도 클래스이다 보니까 실제로 2차 배열처럼 되는 게 아니라 정보들이 파편화되어 있을 수 있다. 그걸 하나씩 주워서 데이터를 밀어 넣는 작업을 해준 것이라고 볼 수 있다.
AnimTransform에 구해준 부분을 malloc은 연속한 공간이니 거기다가 채워주는 작업을 해주고 있다.
4) ComPtr<ID3D11Texture2D> _texture 리소스 만들기
여기까지 데이터 조립이 끝났고, 리소스를 만들어 주면 된다.
CreateTexture2D를 이용해면 되긴 하지만, 일반 Texture 하나를 만들어주는 게 아니라 여러 개의 texture를 만들어 주겠다고
desc.ArraySize = _model->GetAnimationCount();
ArraySize를 1이 아닌 딴 값으로 설정했었다.
subResourceData라는 거를 여러개 준비해 가지고, 그 아이를 넘겨줘야 되는데
// 리소스 만들기
vector<D3D11_SUBRESOURCE_DATA> subResources(_model->GetAnimationCount());
for (uint32 c = 0; c < _model->GetAnimationCount(); c++)
{
void* ptr = (BYTE*)mallocPtr + c * pageSize;
subResources[c].pSysMem = ptr;
subResources[c].SysMemPitch = dataSize;
subResources[c].SysMemSlicePitch = pageSize;
}
HRESULT hr = DEVICE->CreateTexture2D(&desc, subResources.data(), _texture.GetAddressOf());
CHECK(hr);
::free(mallocPtr);
}
subResources를 AnimationCount개 만큼으로 설정해 놓은 다음에
원하는 주소값에다가 dataSize, pageSize를 기입을 해서
CreateTexure2D를 호출하면 된다.
5) ComPtr<ID3D11ShaderResourceView> _srv 만들기
여기까지 호출이 됐으면 _texture라는 ComPtr리소스에는 우리가 원하는 크기의 텍스쳐 배열이 마련이 될 것이고, 그걸 이용해 건네주면 되긴 하지만
이 Textuer를 넘겨주는 게 아니라 항상 ShaderResourceView라는 걸 이용해서 렌더링 파이프라인을 연결해 줬기 때문에 마지막 단계가 srv를 만드는 단계가 하나 더 있다.
// Create SRV
{
D3D11_SHADER_RESOURCE_VIEW_DESC desc;
ZeroMemory(&desc, sizeof(desc));
desc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
desc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2DARRAY;
desc.Texture2DArray.MipLevels = 1;
desc.Texture2DArray.ArraySize = _model->GetAnimationCount();
HRESULT hr = DEVICE->CreateShaderResourceView(_texture.Get(), &desc, _srv.GetAddressOf());
CHECK(hr);
}
이렇게 만들어 주면 된다.
최종적으로 만들어준
struct AnimTransform
{
// [ ][ ][ ][ ][ ][ ][ ] ... 250개
using TransformArrayType = array<Matrix, MAX_MODEL_TRANSFORMS>; // 뼈가 최대 250개
// [ ][ ][ ][ ][ ][ ][ ] ... 500 개
array<TransformArrayType, MAX_MODEL_KEYFRAMES> transforms; // 500프레임에 프레임 당 250개의 뼈대가 있는 거
};
vector<AnimTransform> _animTransforms; // 애니메이션 갯수만큼 2차 배열이 늘어난다.
CPU 메모리 상에 들고 있는 이 vector<AnimTransform> _animTransforms; 의 정보를
열심히 긁어가지고
ComPtr<ID3D11Texture2D> _texture;
하나의 texture 파일로 만들어가지고,
그거를
ComPtr<ID3D11ShaderResourceView> _srv;
srv로 저장을 하고 있다가 결론이 된다.
void ModelAnimator::CreateTexture()
{
if (_model->GetAnimationCount() == 0)
return;
_animTransforms.resize(_model->GetAnimationCount());
for (uint32 i = 0; i < _model->GetAnimationCount(); i++)
CreateAnimationTransform(i);
// CreateTexture
{
D3D11_TEXTURE2D_DESC desc;
ZeroMemory(&desc, sizeof(D3D11_TEXTURE2D_DESC));
desc.Width = MAX_MODEL_TRANSFORMS * 4;
desc.Height = MAX_MODEL_KEYFRAMES;
desc.ArraySize = _model->GetAnimationCount();
desc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT; // 16바이트
desc.Usage = D3D11_USAGE_IMMUTABLE; // 한번 설정하면 안고치겠다
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
desc.MipLevels = 1;
desc.SampleDesc.Count = 1;
const uint32 dataSize = MAX_MODEL_TRANSFORMS * sizeof(Matrix);
const uint32 pageSize = dataSize * MAX_MODEL_KEYFRAMES;
void* mallocPtr = ::malloc(pageSize * _model->GetAnimationCount());
// 파편화된 데이터를 조립한다.
for (uint32 c = 0; c < _model->GetAnimationCount(); c++)
{
uint32 startOffset = c * pageSize;
BYTE* pageStartPtr = reinterpret_cast<BYTE*>(mallocPtr) + startOffset;
for (uint32 f = 0; f < MAX_MODEL_KEYFRAMES; f++)
{
void* ptr = pageStartPtr + dataSize * f;
::memcpy(ptr, _animTransforms[c].transforms[f].data(), dataSize);
}
}
// 리소스 만들기
vector<D3D11_SUBRESOURCE_DATA> subResources(_model->GetAnimationCount());
for (uint32 c = 0; c < _model->GetAnimationCount(); c++)
{
void* ptr = (BYTE*)mallocPtr + c * pageSize;
subResources[c].pSysMem = ptr;
subResources[c].SysMemPitch = dataSize;
subResources[c].SysMemSlicePitch = pageSize;
}
HRESULT hr = DEVICE->CreateTexture2D(&desc, subResources.data(), _texture.GetAddressOf());
CHECK(hr);
::free(mallocPtr);
}
// Create SRV
{
D3D11_SHADER_RESOURCE_VIEW_DESC desc;
ZeroMemory(&desc, sizeof(desc));
desc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
desc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2DARRAY;
desc.Texture2DArray.MipLevels = 1;
desc.Texture2DArray.ArraySize = _model->GetAnimationCount();
HRESULT hr = DEVICE->CreateShaderResourceView(_texture.Get(), &desc, _srv.GetAddressOf());
CHECK(hr);
}
}
중요한 건 ShaderResourceView라는 애가
결국에는 엄청난 잡동사니들을 들고 있는 상태가 되는 거고
얘를 이용해 가지고 GPU에 떠넘겨줘서
알아서 해당하는 부분을 찾아가세요를 할 수가 있게 되었다.
6) 테스트하기
CreateTexure가 되는지 궁금하니까 ModelAnimator::CreateTexture의
_animTransforms.resize(_model->GetAnimationCount());
에 break point를 잡아보자. 과정을 보고
HRESULT hr = DEVICE->CreateShaderResourceView(_texture.Get(), &desc, _srv.GetAddressOf());
여기까지 왔을 때 보면
잘 들어가 있는 것을 볼 수 있다.
CHECK(hr);
를 통과하면 성공한 거고 _srv를 이용해 떠넘겨줄 준비를 해주면 된다.
shader에 넘겨서 그걸 이용해 가지고 네가 알아서 섞어서 뭔가를 해주세요가 되면 된다.