[DirectX11] 048 Play Animation
다이렉트X 11에서, "트윈"(tween)은 시간이 지남에 따라 두 값 사이를 보간하는 방법이다. 애니메이션 및 기타 그래픽 효과에서 서로 다른 상태 간의 원활한 전환을 위해 일반적으로 사용된다. DirectX 11의 tween 방법은 게임이나 응용 프로그램에 생기를 불어넣을 수 있는 부드럽고 실제와 같은 애니메이션을 만드는 강력한 도구이다.
Play Animation
Shader | |
25_Mesh.fx 38_Model.fx 45_Animator.fx 48_AnimationTweening.fx |
|
Framework | |
Model | |
Model.h .cpp ModelAnimator.h .cpp ModelClip.h .cpp ModelMesh.h .cpp ModelRender.h .cpp |
|
Renders | |
Material.h. cpp | |
ModelEditor | |
Demo | |
AnimationDemo.h .cpp ModelDemo.h .cpp |
|
Writer | |
Converter.h .cpp | |
ExportFile.h .cpp Type.h |
Tween
다이렉트X 11에서, "트윈"(tween)은 시간이 지남에 따라 두 값 사이를 보간하는 방법이다. 애니메이션 및 기타 그래픽 효과에서 서로 다른 상태 간의 원활한 전환을 위해 일반적으로 사용된다.
트위닝 뒤에 있는 기본적인 아이디어는 화면에 있는 물체의 위치와 같은 두 개의 값으로 시작한 다음 지정된 시간 동안 물체를 시작 위치에서 끝 위치로 이동시키는 일련의 중간 값을 계산하는 것이다. 결과 애니메이션은 개체가 실제로 개별 단계로 이동하고 있음에도 불구하고 부드럽고 연속적으로 나타낸다.
다이렉트X 11은 개발자들이 이러한 애니메이션을 만드는 데 사용할 수 있는 몇 가지 내장된 트위닝 기능을 제공한다. 예를 들어, ID3DX11 애니메이션 인터페이스는 애니메이션의 두 키 프레임 사이를 원활하게 보간하는 데 사용할 수 있는 인터폴레이트와 같은 메서드를 포함합니다.
내장된 트위닝 기능 외에도 다이렉트X11은 개발자가 구현할 수 있는 커스텀 트위닝 알고리즘도 지원한다. 이러한 알고리즘은 더 복잡한 애니메이션을 만들거나 내장된 기능으로는 불가능한 특정 시각적 효과를 얻기 위해 사용될 수 있다.
전반적으로, DirectX 11의 twen 방법은 게임이나 응용 프로그램에 생기를 불어넣을 수 있는 부드럽고 실제와 같은 애니메이션을 만드는 강력한 도구이다.
Lerp
DirectX 11에서 Lerp("선형 보간"의 줄임말)와 함께 twen 메소드를 사용하려면 다음과 같은 일반적인 단계를 수행한다.
- 시작 및 종료 값을 정의: 이 경우 개체의 시작 위치와 끝 위치와 같이 보간할 두 값이 필요하다.
- 기간 지정: 이 둘 사이의 지속 시간을 밀리초 또는 초 단위로 결정한다.
- 경과 시간을 계산: 이 작업은 시스템 타이머 또는 기타 시간 기록 메커니즘을 사용하여 수행할 수 있다.
- 보간 값을 계산: 시작 값과 끝 값 사이를 보간하는 데 사용할 값입니다. 이 경우 Lerp를 사용하여 이 값을 계산할 수 있다. Lerp는 시작 값, 끝 값 및 보간 값의 세 가지 인수를 사용합니다. 이 값은 0과 1 사이의 값이어야 하며 이 값은 두 값 사이의 진행률을 나타낸다.
- 보간된 값을 적용: 보간된 값을 사용하여 개체의 위치(또는 애니메이션 중인 다른 속성)를 업데이트한다.
float startValue = 0.0f; float endValue = 10.0f; float duration = 1000.0f; // 1 second // Get the elapsed time since the start of the tween float elapsedTime = GetElapsedTime(); // Calculate the interpolation value using Lerp float t = elapsedTime / duration; // t should be between 0 and 1 float interpolatedValue = Lerp(startValue, endValue, t); // Apply the interpolated value to your object's position (or any other property) object.position = interpolatedValue;
이 예제에서 Lerp는 시작 값, 끝 값 및 보간 값을 인수로 사용하고 보간 값을 반환하는 사용자 정의 함수이다. 이 기능을 직접 정의하거나 DirectX 11에서 제공하는 기본 제공 Lerp 기능 중 하나 또는 원하는 프로그래밍 언어를 사용할 수 있다.
fx
Animator.fx
#include "00_Global.fx" float3 Direction = float3(-1, -1, +1); struct VertexModel { float4 Position : Position; float2 Uv : Uv; float3 Normal : Normal; float3 Tangent : Tangent; float4 BlendIndices : BlendIndices; float4 BlendWeights : BlendWeights; }; #define MAX_MODEL_TRANSFORMS 250 #define MAX_MODEL_KEYFRAMES 500 cbuffer CB_Bone { matrix BoneTransforms[MAX_MODEL_TRANSFORMS]; uint BoneIndex; }; //애니메이션 프레임 구조체 struct AnimationFrame { int Clip; uint CurrFrame; uint NextFrame; float Time; float Running; float3 Padding; }; cbuffer CB_TweenFrame { AnimationFrame Keyframes; }; Texture2DArray TransformsMap; struct VertexOutput { float4 Position : SV_Position; float3 Normal : Normal; float2 Uv : Uv; }; void SetAnimationWorld(inout matrix world, VertexModel input) { float indices[4] = { input.BlendIndices.x, input.BlendIndices.y, input.BlendIndices.z, input.BlendIndices.w }; float weights[4] = { input.BlendWeights.x, input.BlendWeights.y, input.BlendWeights.z, input.BlendWeights.w }; int clip; int currFrame; int nextFrame; float time; clip = Keyframes.Clip; currFrame = Keyframes.CurrFrame; nextFrame = Keyframes.NextFrame; time = Keyframes.Time; float4 c0, c1, c2, c3; float4 n0, n1, n2, n3; matrix curr = 0; matrix transform = 0; //최종행렬 초기값 [unroll(4)] for (int i = 0; i < 4; i++) { //x=Bone번호, y=Keyframe, z=Bone이었던것, z=MipMap c0 = TransformsMap.Load(int4(indices[i] * 4 + 0, currFrame, clip, 0)); c1 = TransformsMap.Load(int4(indices[i] * 4 + 1, currFrame, clip, 0)); c2 = TransformsMap.Load(int4(indices[i] * 4 + 2, currFrame, clip, 0)); c3 = TransformsMap.Load(int4(indices[i] * 4 + 3, currFrame, clip, 0)); curr = matrix(c0, c1, c2, c3); //최종행렬에 'curr에 weight(가중치)만큼 곱해줌' 누적시켜서 더해준다. transform += mul(weights[i], curr); } //최종행렬 World로 변환 //transform은 애니메이션이 이동할 행렬. world는 모델이 출력될 world world = mul(transform, world); } VertexOutput VS(VertexModel input) { VertexOutput output; //World = mul(BoneTransforms[BoneIndex], World); SetAnimationWorld(World, input); output.Position = WorldPosition(input.Position); output.Position = ViewProjection(output.Position); output.Normal = WorldNormal(input.Normal); output.Uv = input.Uv; return output; } float4 PS(VertexOutput input) : SV_Target { float NdotL = dot(normalize(input.Normal), -Direction); return DiffuseMap.Sample(LinearSampler, input.Uv) * NdotL; } technique11 T0 { P_VP(P0, VS, PS) P_RS_VP(P1, FillMode_WireFrame, VS, PS) }
AnimatorTweening.fx
#include "00_Global.fx" float3 Direction = float3(-1, -1, +1); struct VertexModel { float4 Position : Position; float2 Uv : Uv; float3 Normal : Normal; float3 Tangent : Tangent; float4 BlendIndices : BlendIndices; float4 BlendWeights : BlendWeights; }; #define MAX_MODEL_TRANSFORMS 250 #define MAX_MODEL_KEYFRAMES 500 cbuffer CB_Bone { matrix BoneTransforms[MAX_MODEL_TRANSFORMS]; uint BoneIndex; }; struct AnimationFrame { int Clip; uint CurrFrame; uint NextFrame; float Time; float Running; float3 Padding; }; struct TweenFrame { float TakeTime; float TweenTime; float RunningTime; float Padding; AnimationFrame Curr; AnimationFrame Next; }; cbuffer CB_TweenFrame { TweenFrame TweenFrames; }; Texture2DArray TransformsMap; struct VertexOutput { float4 Position : SV_Position; float3 Normal : Normal; float2 Uv : Uv; }; void SetAnimationWorld(inout matrix world, VertexModel input) { float indices[4] = { input.BlendIndices.x, input.BlendIndices.y, input.BlendIndices.z, input.BlendIndices.w }; float weights[4] = { input.BlendWeights.x, input.BlendWeights.y, input.BlendWeights.z, input.BlendWeights.w }; int clip[2]; int currFrame[2]; int nextFrame[2]; float time[2]; clip[0] = TweenFrames.Curr.Clip; currFrame[0] = TweenFrames.Curr.CurrFrame; nextFrame[0] = TweenFrames.Curr.NextFrame; time[0] = TweenFrames.Curr.Time; clip[1] = TweenFrames.Next.Clip; currFrame[1] = TweenFrames.Next.CurrFrame; nextFrame[1] = TweenFrames.Next.NextFrame; time[1] = TweenFrames.Next.Time; float4 c0, c1, c2, c3; float4 n0, n1, n2, n3; matrix curr = 0, next = 0; matrix currAnim = 0; matrix nextAnim = 0; matrix transform = 0; [unroll(4)] for (int i = 0; i < 4; i++) { c0 = TransformsMap.Load(int4(indices[i] * 4 + 0, currFrame[0], clip[0], 0)); c1 = TransformsMap.Load(int4(indices[i] * 4 + 1, currFrame[0], clip[0], 0)); c2 = TransformsMap.Load(int4(indices[i] * 4 + 2, currFrame[0], clip[0], 0)); c3 = TransformsMap.Load(int4(indices[i] * 4 + 3, currFrame[0], clip[0], 0)); curr = matrix(c0, c1, c2, c3); n0 = TransformsMap.Load(int4(indices[i] * 4 + 0, nextFrame[0], clip[0], 0)); n1 = TransformsMap.Load(int4(indices[i] * 4 + 1, nextFrame[0], clip[0], 0)); n2 = TransformsMap.Load(int4(indices[i] * 4 + 2, nextFrame[0], clip[0], 0)); n3 = TransformsMap.Load(int4(indices[i] * 4 + 3, nextFrame[0], clip[0], 0)); next = matrix(n0, n1, n2, n3); currAnim = lerp(curr, next, time[0]); [flatten] if (clip[1] > -1) { c0 = TransformsMap.Load(int4(indices[i] * 4 + 0, currFrame[1], clip[1], 0)); c1 = TransformsMap.Load(int4(indices[i] * 4 + 1, currFrame[1], clip[1], 0)); c2 = TransformsMap.Load(int4(indices[i] * 4 + 2, currFrame[1], clip[1], 0)); c3 = TransformsMap.Load(int4(indices[i] * 4 + 3, currFrame[1], clip[1], 0)); curr = matrix(c0, c1, c2, c3); n0 = TransformsMap.Load(int4(indices[i] * 4 + 0, nextFrame[1], clip[1], 0)); n1 = TransformsMap.Load(int4(indices[i] * 4 + 1, nextFrame[1], clip[1], 0)); n2 = TransformsMap.Load(int4(indices[i] * 4 + 2, nextFrame[1], clip[1], 0)); n3 = TransformsMap.Load(int4(indices[i] * 4 + 3, nextFrame[1], clip[1], 0)); next = matrix(n0, n1, n2, n3); nextAnim = lerp(curr, next, time[1]); currAnim = lerp(currAnim, nextAnim, TweenFrames.TweenTime); } transform += mul(weights[i], currAnim); } world = mul(transform, world); } VertexOutput VS(VertexModel input) { VertexOutput output; //World = mul(BoneTransforms[BoneIndex], World); SetAnimationWorld(World, input); output.Position = WorldPosition(input.Position); output.Position = ViewProjection(output.Position); output.Normal = WorldNormal(input.Normal); output.Uv = input.Uv; return output; } float4 PS(VertexOutput input) : SV_Target { float NdotL = dot(normalize(input.Normal), -Direction); return DiffuseMap.Sample(LinearSampler, input.Uv) * NdotL; } technique11 T0 { P_VP(P0, VS, PS) P_RS_VP(P1, FillMode_WireFrame, VS, PS) }
Model Animator
ModelAnimator.h
#pragma once class ModelAnimator { public: ModelAnimator(Shader* shader); ~ModelAnimator(); void Update(); void Render(); public: void ReadMesh(wstring file); void ReadMaterial(wstring file); void ReadClip(wstring file); Transform* GetTransform() { return transform; } Model* GetModel() { return model; } void Pass(UINT pass); void PlayTweenMode(UINT clip, float speed = 1.0f, float takeTime = 1.0f); private: void CreateTexture(); void CreateClipTransform(UINT index); private: struct ClipTransform { Matrix** Transform; ClipTransform() { Transform = new Matrix*[MAX_MODEL_KEYFRAMES]; for (UINT i = 0; i < MAX_MODEL_KEYFRAMES; i++) Transform[i] = new Matrix[MAX_MODEL_TRANSFORMS]; } ~ClipTransform() { for (UINT i = 0; i < MAX_MODEL_KEYFRAMES; i++) SafeDeleteArray(Transform[i]); SafeDeleteArray(Transform); } }; ClipTransform* clipTransforms = NULL; ID3D11Texture2D* texture = NULL; ID3D11ShaderResourceView* srv = NULL; private: struct KeyframeDesc { int Clip = 0; UINT CurrFrame = 0; UINT NextFrame = 0; float Time = 0.0f; float RunningTime = 0.0f; float Speed = 1.0f; Vector2 Padding; }; // keyframeDesc; ConstantBuffer* frameBuffer; ID3DX11EffectConstantBuffer* sFrameBuffer; struct TweenDesc { float TakeTime = 1.0f; float TweenTime = 0.0f; float ChangeTime = 0.0f; float Padding; KeyframeDesc Curr; KeyframeDesc Next; TweenDesc() { Curr.Clip = 0; Next.Clip = -1; } } tweenDesc; private: Shader* shader; Model* model; Transform* transform; };
ModelAnimator.cpp
#include "Framework.h" #include "ModelAnimator.h" ModelAnimator::ModelAnimator(Shader * shader) : shader(shader) { model = new Model(); transform = new Transform(shader); frameBuffer = new ConstantBuffer(&tweenDesc, sizeof(TweenDesc)); sFrameBuffer = shader->AsConstantBuffer("CB_TweenFrame"); } ModelAnimator::~ModelAnimator() { SafeDelete(model); SafeDelete(transform); SafeDeleteArray(clipTransforms); SafeRelease(texture); SafeRelease(srv); SafeDelete(frameBuffer); } void ModelAnimator::Update() { TweenDesc& desc = tweenDesc; //현재 애니메이션 { ModelClip* clip = model->ClipByIndex(desc.Curr.Clip); desc.Curr.RunningTime += Time::Delta(); //누적 float time = 1.0f / clip->FrameRate() / desc.Curr.Speed; //비율 if (desc.Curr.Time >= 1.0f) //1.0f이 넘는 순간 다음 프레임으로 넘긴다. { desc.Curr.RunningTime = 0; desc.Curr.CurrFrame = (desc.Curr.CurrFrame + 1) % clip->FrameCount(); desc.Curr.NextFrame = (desc.Curr.CurrFrame + 1) % clip->FrameCount(); } desc.Curr.Time = desc.Curr.RunningTime / time; } if (desc.Next.Clip > -1) { desc.ChangeTime += Time::Delta(); desc.TweenTime = desc.ChangeTime / desc.TakeTime; if (desc.TweenTime >= 1.0f) { desc.Curr = desc.Next; desc.Next.Clip = -1; desc.Next.CurrFrame = 0; desc.Next.NextFrame = 0; desc.Next.Time = 0; desc.Next.RunningTime = 0.0f; desc.ChangeTime = 0.0f; desc.TweenTime = 0.0f; } else { ModelClip* clip = model->ClipByIndex(desc.Next.Clip); desc.Next.RunningTime += Time::Delta(); float time = 1.0f / clip->FrameRate() / desc.Next.Speed; if (desc.Next.Time >= 1.0f) { desc.Next.RunningTime = 0; desc.Next.CurrFrame = (desc.Next.CurrFrame + 1) % clip->FrameCount(); desc.Next.NextFrame = (desc.Next.CurrFrame + 1) % clip->FrameCount(); } desc.Next.Time = desc.Next.RunningTime / time; } } if (texture == NULL) { for (ModelMesh* mesh : model->Meshes()) mesh->SetShader(shader); CreateTexture(); } for (ModelMesh* mesh : model->Meshes()) mesh->Update(); } void ModelAnimator::Render() { frameBuffer->Render(); sFrameBuffer->SetConstantBuffer(frameBuffer->Buffer()); for (ModelMesh* mesh : model->Meshes()) { mesh->SetTransform(transform); mesh->Render(); } } void ModelAnimator::ReadMesh(wstring file) { model->ReadMesh(file); } void ModelAnimator::ReadMaterial(wstring file) { model->ReadMaterial(file); } void ModelAnimator::ReadClip(wstring file) { model->ReadClip(file); } void ModelAnimator::Pass(UINT pass) { for (ModelMesh* mesh : model->Meshes()) mesh->Pass(pass); } void ModelAnimator::PlayTweenMode(UINT clip, float speed, float takeTime) { tweenDesc.TakeTime = takeTime; tweenDesc.Next.Clip = clip; tweenDesc.Next.Speed = speed; } void ModelAnimator::CreateTexture() { //Matrix matrix[MAX_MODEL_KEYFRAMES][MAX_MODEL_TRANSFORMS]; clipTransforms = new ClipTransform[model->ClipCount()]; for (UINT i = 0; i < model->ClipCount(); i++) CreateClipTransform(i); //Create Texture { D3D11_TEXTURE2D_DESC desc; ZeroMemory(&desc, sizeof(D3D11_TEXTURE2D_DESC)); desc.Width = MAX_MODEL_TRANSFORMS * 4; desc.Height = MAX_MODEL_KEYFRAMES; desc.ArraySize = model->ClipCount(); desc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT; //16Byte * 4 = 64Byte desc.Usage = D3D11_USAGE_IMMUTABLE; desc.BindFlags = D3D11_BIND_SHADER_RESOURCE; desc.MipLevels = 1; desc.SampleDesc.Count = 1; UINT pageSize = MAX_MODEL_TRANSFORMS * 4 * 16 * MAX_MODEL_KEYFRAMES; //void* p = malloc(pageSize * model->ClipCount()); void* p = VirtualAlloc(NULL, pageSize * model->ClipCount(), MEM_RESERVE, PAGE_READWRITE); //MEMORY_BASIC_INFORMATION, VirtualQuery for (UINT c = 0; c < model->ClipCount(); c++) { UINT start = c * pageSize; for (UINT k = 0; k < MAX_MODEL_KEYFRAMES; k++) { void* temp = (BYTE *)p + MAX_MODEL_TRANSFORMS * k * sizeof(Matrix) + start; VirtualAlloc(temp, MAX_MODEL_TRANSFORMS * sizeof(Matrix), MEM_COMMIT, PAGE_READWRITE); memcpy(temp, clipTransforms[c].Transform[k], MAX_MODEL_TRANSFORMS * sizeof(Matrix)); } }//for(c) D3D11_SUBRESOURCE_DATA* subResources = new D3D11_SUBRESOURCE_DATA[model->ClipCount()]; for (UINT c = 0; c < model->ClipCount(); c++) { void* temp = (BYTE *)p + c * pageSize; subResources[c].pSysMem = temp; subResources[c].SysMemPitch = MAX_MODEL_TRANSFORMS * sizeof(Matrix); subResources[c].SysMemSlicePitch = pageSize; } Check(D3D::GetDevice()->CreateTexture2D(&desc, subResources, &texture)); SafeDeleteArray(subResources); VirtualFree(p, 0, MEM_RELEASE); } //Create SRV { D3D11_SHADER_RESOURCE_VIEW_DESC desc; ZeroMemory(&desc, sizeof(D3D11_SHADER_RESOURCE_VIEW_DESC)); desc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT; desc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2DARRAY; desc.Texture2DArray.MipLevels = 1; desc.Texture2DArray.ArraySize = model->ClipCount(); Check(D3D::GetDevice()->CreateShaderResourceView(texture, &desc, &srv)); } for (ModelMesh* mesh : model->Meshes()) mesh->TransformsSRV(srv); } void ModelAnimator::CreateClipTransform(UINT index) { Matrix* bones = new Matrix[MAX_MODEL_TRANSFORMS]; ModelClip* clip = model->ClipByIndex(index); for (UINT f = 0; f < clip->FrameCount(); f++) { for (UINT b = 0; b < model->BoneCount(); b++) { ModelBone* bone = model->BoneByIndex(b); Matrix parent; Matrix invGlobal = bone->Transform(); D3DXMatrixInverse(&invGlobal, NULL, &invGlobal); int parentIndex = bone->ParentIndex(); if (parentIndex < 0) D3DXMatrixIdentity(&parent); else parent = bones[parentIndex]; Matrix animation; ModelKeyframe* frame = clip->Keyframe(bone->Name()); if (frame != NULL) { ModelKeyframeData& data = frame->Transforms[f]; Matrix S, R, T; D3DXMatrixScaling(&S, data.Scale.x, data.Scale.y, data.Scale.z); D3DXMatrixRotationQuaternion(&R, &data.Rotation); D3DXMatrixTranslation(&T, data.Translation.x, data.Translation.y, data.Translation.z); animation = S * R * T; } else { D3DXMatrixIdentity(&animation); } bones[b] = animation * parent; clipTransforms[index].Transform[f][b] = invGlobal * bones[b]; }//for(b) }//for(f) }
다음동작의 clip = -1이면 없는걸로 간주. clip의 값이 -1~1 사이의 어느값으로 변화하면 다음동작 수행.
Animation Demo
AnimationDemo.cpp
#include "stdafx.h" #include "AnimationDemo.h" #include "Converter.h" void AnimationDemo::Initialize() { Context::Get()->GetCamera()->RotationDegree(20, 0, 0); Context::Get()->GetCamera()->Position(1, 36, -85); shader = new Shader(L"45_Animation.fx"); Kachujin(); } void AnimationDemo::Update() { if (kachujin != NULL) kachujin->Update(); } void AnimationDemo::Render() { static int speed = 1; ImGui::SliderFloat("Speed", &speed, 0.1f, 5.0f); static int clip = 0; if (Keyboard::Get()->Down(VK_SPACE)) { clip++; clip %= 4; kachujin->PlayTweenMode(clip, speed); } ImGui::SliderFloat3("Direction2", direction, -1, +1); shader->AsVector("Direction")->SetFloatVector(direction); static int pass = 0; ImGui::InputInt("Pass2", &pass); pass %= 2; if (kachujin != NULL) { kachujin->Pass(pass); kachujin->Render(); } } void AnimationDemo::Kachujin() { kachujin = new ModelAnimator(shader); kachujin->ReadMesh(L"Kachujin/Mesh"); kachujin->ReadMaterial(L"Kachujin/Mesh"); kachujin->ReadClip(L"Kachujin/Sword And Shield Idle"); kachujin->ReadClip(L"Kachujin/Sword And Shield Run"); kachujin->ReadClip(L"Kachujin/Sword And Shield Slash"); kachujin->ReadClip(L"Kachujin/Salsa Dancing"); kachujin->GetTransform()->Position(0, 0, -30); kachujin->GetTransform()->Scale(0.025f, 0.025f, 0.025f); }
실행화면

'⭐ DirectX > DirectX11 3D' 카테고리의 다른 글
[DirectX11] 051~52 Instancing Mesh & Model (0) | 2023.02.18 |
---|---|
[DirectX11] 050 Instancing (0) | 2023.02.18 |
[DirectX11] 045 Model Animator (0) | 2023.02.13 |
[DirectX11] 042~44 Animaton Read Clip (0) | 2023.02.10 |
PDB 파일을 찾거나 열 수 없습니다. 오류 해결 (0) | 2023.02.08 |
댓글
이 글 공유하기
다른 글
-
[DirectX11] 051~52 Instancing Mesh & Model
[DirectX11] 051~52 Instancing Mesh & Model
2023.02.18 -
[DirectX11] 050 Instancing
[DirectX11] 050 Instancing
2023.02.18 -
[DirectX11] 045 Model Animator
[DirectX11] 045 Model Animator
2023.02.13 -
[DirectX11] 042~44 Animaton Read Clip
[DirectX11] 042~44 Animaton Read Clip
2023.02.10
댓글을 사용할 수 없습니다.