일단 컴포넌트, 월드 공간으로 어떻게 회전하는건지 이해했다.
이제 손 제어하기 전에
각 포인트들의 RPY값들을 좀 보려고함.
MapBoneLeft, MapBoneRight에는
원래 키로 2~16 사용하는 손관절들 뿐이었는데
나머지 관절 이름을 저장한다고
MapBoneLeft.Add(16, FString("b_l_wrist"));
MapBoneLeft.Add(17, FString("b_l_thumb0"));
MapBoneLeft.Add(18, FString("b_l_thumb1"));
MapBoneLeft.Add(19, FString("b_l_pinky0"));
로 추가한상태
하지만 MapRoll/Pitch/Yaw에는 2~16 관절 rpy값만 들어있다.
그런데 이전에 작성한 rpy 코드가 맞는질 몰라서 값좀 출력해보도록 수정
관절보다가 이거보니 이게왜이렇게됫는지 생각이안난다.
내가 구현한 피치요대신 찾아넨 calc rotation 썻는대
/*
void ADesktopGameModeBase::get_pitch_yaw(cv::Point3f pt_start, cv::Point3f pt_end, float& pitch, float& yaw) {
float dx = pt_end.x - pt_start.x;
float dy = pt_end.y - pt_start.y;
dy *= -1;
yaw = std::atan2(dy, dx) * 180 / CV_PI;
yaw = yaw - 90;
float dz = pt_end.z - pt_start.z;
float xy_norm = std::sqrt(dx * dx + dy * dy);
pitch = std::atan2(dz, xy_norm) * 180 / CV_PI;
pitch *= -1;
}
*/
void ADesktopGameModeBase::calculateRotation(const cv::Point3f& pt1, const cv::Point3f& pt2, float& roll, float& pitch, float& yaw) {
cv::Point3f vec = pt2 - pt1; // 두 벡터를 연결하는 벡터 계산
roll = atan2(vec.y, vec.x) * 180 / CV_PI; // 롤(Roll) 회전 계산
pitch = asin(vec.z / cv::norm(vec)) * 180 / CV_PI; // 피치(Pitch) 회전 계산
cv::Point2f vecXY(vec.x, vec.y); // xy 평면으로 투영
yaw = (atan2(vec.y, vec.x) - atan2(vec.z, cv::norm(vecXY))) * 180 / CV_PI; // 요(Yaw) 회전 계산
}
어제 하던 파이썬 코드 돌아와서 다시보자
손 편상태 랜드마크를 npy로 저장한걸 다시 로드하고 반정규화 처리한걸 3d 플롯시켰었는데
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
%matplotlib widget
# 3차원 산점도 그리기
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
x = denormalize_landmark[:, 0]
y = denormalize_landmark[:, 1]
z = denormalize_landmark[:, 2]
# 색상 지정
colors = ['black'] + ['red'] * 4 + ['orange'] * 4 + ['yellow'] * 4 + ['green'] * 4 + ['blue'] * 4
ax.scatter(x, y, z, c=colors)
#손가락
colors = ['red', 'orange', 'yellow', 'green', 'blue']
groups = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16], [17, 18, 19, 20]]
for i, group in enumerate(groups):
for j in range(len(group)-1):
plt.plot([x[group[j]], x[group[j+1]]], [y[group[j]], y[group[j+1]]], [z[group[j]], z[group[j+1]]], color=colors[i])
#손등
lines = [[0, 1], [0, 5], [0, 17], [5, 9], [9, 13], [13, 17]]
for line in lines:
ax.plot([x[line[0]], x[line[1]]], [y[line[0]], y[line[1]]], [z[line[0]], z[line[1]]], color='gray')
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
plt.gca().invert_xaxis()
plt.show()
calcRotation을 python으로 만들어
검지 계산을 위해 점 5, 6을 넣어서 rpy를 구해보면
import numpy as np
import math
def calculateRotation(pt1, pt2):
vec = pt2 - pt1
roll = math.atan2(vec[1], vec[0]) * 180 / math.pi
pitch = math.asin(vec[2] / np.linalg.norm(vec)) * 180 / math.pi
vecXY = np.array([vec[0], vec[1]])
yaw = (math.atan2(vec[1], vec[0]) - math.atan2(vec[2], np.linalg.norm(vecXY))) * 180 / math.pi
return roll, pitch, yaw
roll, pitch, yaw = calculateRotation(denormalize_landmark[15], denormalize_landmark[16])
print(roll, pitch, yaw)
-102, -7, -95가 나왔다.
yz 평면을 보면 -7이 맞을것같고
영 이해가 안되서 다시 그림그려봄
15, 16 pt 포인트가 저러므로
z축 회전각 yaw는 그렇게 크진 않아야한다.
근데 위에서 나온 yaw는 -95씩이나 된다.
좀 이상해서 보니 1을 더붙여서 엉뚱한 손가락보고있었다.
검지 시작하는 5,6 을보면
xy 평면으로 보면 위 그림과 x축 기준으로 반대가 되어있는데
위 그림은 아랫방향에서 내려가며 커지기 때문
그리고 위의 calc Rot가 잘못된게
x,y 좌표값으로 얻은건 z축 회전 yaw이므로 좀 수정해서봐야할듯
필요한건 저각도이므로
atan2(dy/dx) -> deg
y 방향이 반대이므로 -1, 그리고 보라색 축 기준 각도가 필요하므로 90 - 계산결과
vec = pt2 - pt1
yaw = 90 - (math.atan2(vec[1], vec[0]) * 180 / math.pi * -1)
피치는 zy평면의 저 각도이므로
atan2( dz/ norm(d xy)) -> deg
pitch = math.atan2(vec[2], np.linalg.norm(vec[0:2])) * 180 / math.pi
롤은 xz 평면에서 봐야하는데
저 각일듯 하니
atan2(dz, dx) ->deg 쯤 되지않을까.
x가 오른쪽으로 갈수록 줄어드는데 주의하자
xz 평면에 놓고 생각하면 이런식으로 될듯
계산된 RPY는 -16, -5, 18이 되었다.
스켈레톤 에디터에서 해봤는데
롤은 전역(컴포넌트) 공간 90-16 = 74
피치는 -5이므로 0 - 5 = -5
요는 0 + 18 = 18하려했는데 방향이 이상해서 -18로 하니비슷하게됨
다음 손가락도 해보면
74 - 31 = 43
-5 -12 = -17
-18 -20 = -38
뭔가 비슷한 느낌이나긴한데 방향은 이상하다
43 - 33 = 10
-17 - 11 = -28
-38 - 17 = 55
방향은 이상하지만 뭔가 좀 비슷하게 된거같다.
![]() |
![]() |
손이 처음부터 90도 회전되있어서 전역계 설정이 너무 어려운데
roll은 제외하고 피치, 요만 다루는게 맞긴할듯
아까 계산한 피치는
-5, -12, - 11이었는데
이것도 -방향해줘야햇음
5,
12,
11
요가
18, 20, 17 이었는데
- 방향으로 회전해줘야 맞게감
-18,
-20,
-17로 설정하면될듯
새끼손까락을보면
피치 -10, -11, -9 -> 10, 11, 9
요 -41, -43, -36 -> 41, 43, 36
엄지0은 90, 17, -47일때 정상적인 손모양
썸1은 131, 0, -44
이때 모든 좌표계 방향이 비슷함
근대 썸이 약간 기울어진게아쉽긴한데 이대로하고
어제에서 진행된건 없지만 조금 이해했으니
그냥 어제 만든 코드로 돌아가고 손 이동 좌표 구상부터
1920 / 640 = 3 이므로
9번점(중지 시작) x 좌표 * 3한걸 왼손 액터의 Y축 위치 값 설정
1020 / 480 = 2.25 이므로
int(9번점 y좌표 * 2.25)로 왼속액터 z축 위치값 설정
이 아니라
-700 ~ 700 = 1400
0 ~ 600이 적당해보임
x는 2.2 곱
y는 1.25
bP 왼손에 좌표가져오기위한 변수추가
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "Blaze.h"
#include "Windows/AllowWindowsPlatformTypes.h"
#include <Windows.h>
#include "Windows/HideWindowsPlatformTypes.h"
#include "PreOpenCVHeaders.h"
#include <opencv2/opencv.hpp>
#include "PostOpenCVHeaders.h"
#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "DesktopGameModeBase.generated.h"
/**
*
*/
UCLASS()
class HANDDESKTOP_API ADesktopGameModeBase : public AGameModeBase
{
GENERATED_BODY()
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
UFUNCTION(BlueprintCallable)
void ReadFrame();
int monitorWidth = 1920;
int monitorHeight = 1080;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UTexture2D* imageTextureScreen1;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UTexture2D* imageTextureScreen2;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UTexture2D* imageTextureScreen3;
cv::Mat imageScreen1;
cv::Mat imageScreen2;
cv::Mat imageScreen3;
void ScreensToCVMats();
void CVMatsToTextures();
int webcamWidth = 640;
int webcamHeight = 480;
cv::VideoCapture capture;
cv::Mat webcamImage;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UTexture2D* webcamTexture;
void MatToTexture2D(const cv::Mat InMat);
//var and functions with blaze
Blaze blaze;
cv::Mat img256;
cv::Mat img128;
float scale;
cv::Scalar pad;
// vars and funcs for rotator
int hand_conns_indexes[14] = {2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
void get_pitch_yaw(cv::Point3f pt_start, cv::Point3f pt_end, float& pitch, float& yaw);
//void calculateRotation(const cv::Point3f& pt1, const cv::Point3f& pt2, float& roll, float& pitch, float& yaw);
void make_map_for_rotators(std::vector<cv::Mat> denorm_imgs_landmarks);
void make_map_bone();
UPROPERTY(BlueprintReadWrite, Category = "RotatorMap")
TMap<int32, float> MapRoll;
UPROPERTY(BlueprintReadWrite, Category="RotatorMap")
TMap<int32, float> MapPitch;
UPROPERTY(BlueprintReadWrite, Category = "RotatorMap")
TMap<int32, float> MapYaw;
UPROPERTY(BlueprintReadWrite, Category = "RotatorMap")
TMap<int32, FString> MapBoneLeft;
UPROPERTY(BlueprintReadWrite, Category = "RotatorMap")
TMap<int32, FString> MapBoneRight;
UPROPERTY(BlueprintReadWrite, Category = "HandCoord")
float HandLeftX;
UPROPERTY(BlueprintReadWrite, Category = "HandCoord")
float HandLeftY;
};
// Fill out your copyright notice in the Description page of Project Settings.
#include "DesktopGameModeBase.h"
void ADesktopGameModeBase::BeginPlay()
{
Super::BeginPlay();
blaze = Blaze();
capture = cv::VideoCapture(0);
if (!capture.isOpened())
{
UE_LOG(LogTemp, Log, TEXT("Open Webcam failed"));
return;
}
else
{
UE_LOG(LogTemp, Log, TEXT("Open Webcam Success"));
}
capture.set(cv::CAP_PROP_FRAME_WIDTH, webcamWidth);
capture.set(cv::CAP_PROP_FRAME_HEIGHT, webcamHeight);
webcamTexture = UTexture2D::CreateTransient(monitorWidth, monitorHeight, PF_B8G8R8A8);
imageScreen1 = cv::Mat(monitorHeight, monitorWidth, CV_8UC4);
imageScreen2 = cv::Mat(monitorHeight, monitorWidth, CV_8UC4);
imageScreen3 = cv::Mat(monitorHeight, monitorWidth, CV_8UC4);
imageTextureScreen1 = UTexture2D::CreateTransient(monitorWidth, monitorHeight, PF_B8G8R8A8);
imageTextureScreen2 = UTexture2D::CreateTransient(monitorWidth, monitorHeight, PF_B8G8R8A8);
imageTextureScreen3 = UTexture2D::CreateTransient(monitorWidth, monitorHeight, PF_B8G8R8A8);
make_map_bone();
}
void ADesktopGameModeBase::ReadFrame()
{
if (!capture.isOpened())
{
return;
}
capture.read(webcamImage);
/*
get filtered detections
*/
blaze.ResizeAndPad(webcamImage, img256, img128, scale, pad);
//UE_LOG(LogTemp, Log, TEXT("scale value: %f, pad value: (%f, %f)"), scale, pad[0], pad[1]);
std::vector<Blaze::PalmDetection> normDets = blaze.PredictPalmDetections(img128);
std::vector<Blaze::PalmDetection> denormDets = blaze.DenormalizePalmDetections(normDets, webcamWidth, webcamHeight, pad);
std::vector<Blaze::PalmDetection> filteredDets = blaze.FilteringDets(denormDets, webcamWidth, webcamHeight);
std::vector<cv::Rect> handRects = blaze.convertHandRects(filteredDets);
std::vector<cv::Mat> handImgs;
blaze.GetHandImages(webcamImage, handRects, handImgs);
std::vector<cv::Mat> imgs_landmarks = blaze.PredictHandDetections(handImgs);
std::vector<cv::Mat> denorm_imgs_landmarks = blaze.DenormalizeHandLandmarks(imgs_landmarks, handRects);
make_map_for_rotators(denorm_imgs_landmarks);
//draw hand rects/ plam detection/ dets info/ hand detection
blaze.DrawRects(webcamImage, handRects);
blaze.DrawPalmDetections(webcamImage, filteredDets);
blaze.DrawDetsInfo(webcamImage, filteredDets, normDets, denormDets);
blaze.DrawHandDetections(webcamImage, denorm_imgs_landmarks);
//cv::mat to utexture2d
MatToTexture2D(webcamImage);
/*
모니터 시각화
*/
ScreensToCVMats();
CVMatsToTextures();
}
void ADesktopGameModeBase::MatToTexture2D(const cv::Mat InMat)
{
if (InMat.type() == CV_8UC3)//example for pre-conversion of Mat
{
cv::Mat resizedImage;
cv::resize(InMat, resizedImage, cv::Size(monitorWidth, monitorHeight));
cv::Mat bgraImage;
//if the Mat is in BGR space, convert it to BGRA. There is no three channel texture in UE (at least with eight bit)
cv::cvtColor(resizedImage, bgraImage, cv::COLOR_BGR2BGRA);
//actually copy the data to the new texture
FTexture2DMipMap& Mip = webcamTexture->GetPlatformData()->Mips[0];
void* Data = Mip.BulkData.Lock(LOCK_READ_WRITE);//lock the texture data
FMemory::Memcpy(Data, bgraImage.data, bgraImage.total() * bgraImage.elemSize());//copy the data
Mip.BulkData.Unlock();
webcamTexture->PostEditChange();
webcamTexture->UpdateResource();
}
else if (InMat.type() == CV_8UC4)
{
//actually copy the data to the new texture
FTexture2DMipMap& Mip = webcamTexture->GetPlatformData()->Mips[0];
void* Data = Mip.BulkData.Lock(LOCK_READ_WRITE);//lock the texture data
FMemory::Memcpy(Data, InMat.data, InMat.total() * InMat.elemSize());//copy the data
Mip.BulkData.Unlock();
webcamTexture->PostEditChange();
webcamTexture->UpdateResource();
}
//if the texture hasnt the right pixel format, abort.
webcamTexture->PostEditChange();
webcamTexture->UpdateResource();
}
void ADesktopGameModeBase::ScreensToCVMats()
{
HDC hScreenDC = GetDC(NULL);
HDC hMemoryDC = CreateCompatibleDC(hScreenDC);
int screenWidth = GetDeviceCaps(hScreenDC, HORZRES);
int screenHeight = GetDeviceCaps(hScreenDC, VERTRES);
HBITMAP hBitmap = CreateCompatibleBitmap(hScreenDC, screenWidth, screenHeight);
HBITMAP hOldBitmap = (HBITMAP)SelectObject(hMemoryDC, hBitmap);
//screen 1
BitBlt(hMemoryDC, 0, 0, screenWidth, screenHeight, hScreenDC, 0, 0, SRCCOPY);
GetBitmapBits(hBitmap, imageScreen1.total() * imageScreen1.elemSize(), imageScreen1.data);
//screen 2
BitBlt(hMemoryDC, 0, 0, screenWidth, screenHeight, hScreenDC, 1920, 0, SRCCOPY);
GetBitmapBits(hBitmap, imageScreen2.total() * imageScreen2.elemSize(), imageScreen2.data);
//screen 3
BitBlt(hMemoryDC, 0, 0, screenWidth, screenHeight, hScreenDC, 3840, 0, SRCCOPY);
GetBitmapBits(hBitmap, imageScreen3.total() * imageScreen3.elemSize(), imageScreen3.data);
SelectObject(hMemoryDC, hOldBitmap);
DeleteDC(hScreenDC);
DeleteDC(hMemoryDC);
DeleteObject(hBitmap);
DeleteObject(hOldBitmap);
}
void ADesktopGameModeBase::CVMatsToTextures()
{
for (int i = 0; i < 3; i++)
{
if (i == 0)
{
FTexture2DMipMap& Mip = imageTextureScreen1->GetPlatformData()->Mips[0];
void* Data = Mip.BulkData.Lock(LOCK_READ_WRITE);//lock the texture data
FMemory::Memcpy(Data, imageScreen1.data, imageScreen1.total() * imageScreen1.elemSize());//copy the data
Mip.BulkData.Unlock();
imageTextureScreen1->PostEditChange();
imageTextureScreen1->UpdateResource();
}
else if (i == 1)
{
FTexture2DMipMap& Mip = imageTextureScreen2->GetPlatformData()->Mips[0];
void* Data = Mip.BulkData.Lock(LOCK_READ_WRITE);//lock the texture data
FMemory::Memcpy(Data, imageScreen2.data, imageScreen2.total() * imageScreen2.elemSize());//copy the data
Mip.BulkData.Unlock();
imageTextureScreen2->PostEditChange();
imageTextureScreen2->UpdateResource();
}
else if (i == 2)
{
FTexture2DMipMap& Mip = imageTextureScreen3->GetPlatformData()->Mips[0];
void* Data = Mip.BulkData.Lock(LOCK_READ_WRITE);//lock the texture data
FMemory::Memcpy(Data, imageScreen3.data, imageScreen3.total() * imageScreen3.elemSize());//copy the data
Mip.BulkData.Unlock();
imageTextureScreen3->PostEditChange();
imageTextureScreen3->UpdateResource();
}
}
}
void ADesktopGameModeBase::get_pitch_yaw(cv::Point3f pt_start, cv::Point3f pt_end, float& pitch, float& yaw) {
float dx = pt_end.x - pt_start.x;
float dy = pt_end.y - pt_start.y;
dy *= -1;
yaw = std::atan2(dy, dx) * 180 / CV_PI;
yaw = yaw - 90;
float dz = pt_end.z - pt_start.z;
float xy_norm = std::sqrt(dx * dx + dy * dy);
pitch = std::atan2(dz, xy_norm) * 180 / CV_PI;
pitch *= -1;
}
/*
void ADesktopGameModeBase::calculateRotation(const cv::Point3f& pt1, const cv::Point3f& pt2, float& roll, float& pitch, float& yaw) {
cv::Point3f vec = pt2 - pt1; // 두 벡터를 연결하는 벡터 계산
roll = atan2(vec.y, vec.x) * 180 / CV_PI; // 롤(Roll) 회전 계산
pitch = asin(vec.z / cv::norm(vec)) * 180 / CV_PI; // 피치(Pitch) 회전 계산
cv::Point2f vecXY(vec.x, vec.y); // xy 평면으로 투영
yaw = (atan2(vec.y, vec.x) - atan2(vec.z, cv::norm(vecXY))) * 180 / CV_PI; // 요(Yaw) 회전 계산
}
*/
void ADesktopGameModeBase::make_map_for_rotators(std::vector<cv::Mat> denorm_imgs_landmarks)
{
for (auto& denorm_landmarks : denorm_imgs_landmarks)
{
std::vector<std::array<int, 2>> HAND_CONNECTIONS = blaze.HAND_CONNECTIONS;
for (auto& hand_conns_index : hand_conns_indexes)
{
std::array<int, 2> hand_conns = HAND_CONNECTIONS.at(hand_conns_index);
float roll, pitch, yaw;
cv::Point3f pt_start, pt_end;
pt_start.x = denorm_landmarks.at<float>(hand_conns.at(0), 0);
pt_start.y = denorm_landmarks.at<float>(hand_conns.at(0), 1);
pt_start.z = denorm_landmarks.at<float>(hand_conns.at(0), 2);
pt_end.x = denorm_landmarks.at<float>(hand_conns.at(1), 0);
pt_end.y = denorm_landmarks.at<float>(hand_conns.at(1), 1);
pt_end.z = denorm_landmarks.at<float>(hand_conns.at(1), 2);
//calculateRotation(pt_start, pt_end, roll, pitch, yaw);
get_pitch_yaw(pt_start, pt_end, pitch, yaw);
MapRoll.Add(hand_conns_index, roll);
MapPitch.Add(hand_conns_index, pitch);
MapYaw.Add(hand_conns_index, yaw);
}
HandLeftX = denorm_landmarks.at<float>(9, 0);
HandLeftY = denorm_landmarks.at<float>(9, 1);
}
}
/*
std::vector<std::array<int, 2>> HAND_CONNECTIONS = {
{0, 1}, {1, 2}, {2, 3}, {3, 4},
{5, 6}, {6, 7}, {7, 8},
{9, 10}, {10, 11}, {11, 12},
{13, 14}, {14, 15}, {15, 16},
{17, 18}, {18, 19}, {19, 20},
{0, 5}, {5, 9}, {9, 13}, {13, 17}, {0, 17}
};
*/
void ADesktopGameModeBase::make_map_bone()
{
MapBoneLeft.Add(2, FString("b_l_thumb2"));
MapBoneLeft.Add(3, FString("b_l_thumb3"));
MapBoneLeft.Add(4, FString("b_l_index1"));
MapBoneLeft.Add(5, FString("b_l_index2"));
MapBoneLeft.Add(6, FString("b_l_index3"));
MapBoneLeft.Add(7, FString("b_l_middle1"));
MapBoneLeft.Add(8, FString("b_l_middle2"));
MapBoneLeft.Add(9, FString("b_l_middle3"));
MapBoneLeft.Add(10, FString("b_l_ring1"));
MapBoneLeft.Add(11, FString("b_l_ring2"));
MapBoneLeft.Add(12, FString("b_l_ring3"));
MapBoneLeft.Add(13, FString("b_l_pinky1"));
MapBoneLeft.Add(14, FString("b_l_pinky2"));
MapBoneLeft.Add(15, FString("b_l_pinky3"));
MapBoneRight.Add(2, FString("b_r_thumb2"));
MapBoneRight.Add(3, FString("b_r_thumb3"));
MapBoneRight.Add(4, FString("b_r_index1"));
MapBoneRight.Add(5, FString("b_r_index2"));
MapBoneRight.Add(6, FString("b_r_index3"));
MapBoneRight.Add(7, FString("b_r_middle1"));
MapBoneRight.Add(8, FString("b_r_middle2"));
MapBoneRight.Add(9, FString("b_r_middle3"));
MapBoneRight.Add(10, FString("b_r_ring1"));
MapBoneRight.Add(11, FString("b_r_ring2"));
MapBoneRight.Add(12, FString("b_r_ring3"));
MapBoneRight.Add(13, FString("b_r_pinky1"));
MapBoneRight.Add(14, FString("b_r_pinky2"));
MapBoneRight.Add(15, FString("b_r_pinky3"));
MapBoneLeft.Add(16, FString("b_l_wrist"));
MapBoneLeft.Add(17, FString("b_l_thumb0"));
MapBoneLeft.Add(18, FString("b_l_thumb1"));
MapBoneLeft.Add(19, FString("b_l_pinky0"));
MapBoneRight.Add(16, FString("b_r_wrist"));
MapBoneRight.Add(17, FString("b_r_thumb0"));
MapBoneRight.Add(18, FString("b_r_thumb1"));
MapBoneRight.Add(19, FString("b_r_pinky0"));
}
대충 손 움직이기 구현은 했다.
왜 관절이 이렇게 되나 해매느라 구현 상 진전은 크진않음.
'컴퓨터과학 > 언리얼' 카테고리의 다른 글
HandDesktop17 - 손 관절 가지고 놀기5 좌표로 다루기 구현 (0) | 2024.02.02 |
---|---|
HandDesktop16 - 손 관절 가지고 놀기4 rpy 대신 좌표 다루기 구상 (0) | 2024.02.02 |
HandDesktop14 - 손 관절 가지고 놀기2 컴포넌트공간 관절회전 (0) | 2024.02.01 |
HandDesktop13 - 손 관절 가지고 놀기 (0) | 2024.02.01 |
HandDesktop12 - 손 관절 회전 방향 구하기(fail?) (0) | 2024.02.01 |