OutputDebugString

  • Windows 환경에서 제공되는 디버그 출력 함수

  • <Windows.h> 헤더에 포함

    #include <windows.h>
    #include <string>
    
    int main() {
        OutputDebugString(TEXT("Hello, Debug World!"));
        OutputDebugString(L"Hello");
        std::wstring ws = L"WString" + std::to_string({변수});
        OutputDebugString(ws);
        return 0;
    }
  • print처럼 포맷 적용하는 법 : https://blog.codingcat.kr/76

FAILED

  • 결과를 HRESULT 타입으로 반환
  • 코드 실행이 정상적으로 되었는지 비교할 떄 사용
    • 쉐이더 파일 내부 오류는 제대로 인지 불가
      if (FAILED(walkImage->Init(
        TEXT("Image/iori_walk.bmp"), 612, 104)))
      {
            MessageBox(g_hWnd, TEXT("파일 로드에 실패"), TEXT("경고"), MB_OK);
      }
  • 주요 HRESULT 값들

    • 성공(Success):
      • S_OK (값: 0): 작업이 성공적으로 완료됨.
      • S_FALSE (값: 1): 작업은 성공했으나 추가 정보가 제공됨.
    • 실패(Failure):
      • E_FAIL (값: 0x80004005): 일반적인 실패.
      • E_INVALIDARG (값: 0x80070057): 잘못된 인수.
      • E_OUTOFMEMORY (값: 0x8007000E): 메모리 부족.

'프레임워크 > WinAPI' 카테고리의 다른 글

기본 개념 및 함수  (0) 2025.03.14
애니메이션  (0) 2025.03.14

기본 이미지 클래스 구조 만들기

class Image
{
public:
    enum class IMAGE_LOAD_TYPE
    {
            Resource,  // 프로젝트 자체 포함 이미지
            File,      // 외부에서 업로드할 이미지
            Empty,     // 자체 생산할 이미지
            End
    };

    typedef struct tagImageInfo
    {
        DWORD resID;        // 리소스 고유 ID
        HDC hMemDC;            // 그리기 주관 객체 핸들
        HBITMAP hBitmap;    // 이미지 정보
        HBITMAP hOldBIt;    // 기존 이미지 정보
        int width;
        int height;
        BYTE loadType;        // 로드 타입

        tagImageInfo()
        {
            resID = 0;
            hMemDC = nullptr;
            hBitmap = nullptr;
            hOldBIt = nullptr;
            width = 0;
            height = 0;
            loadType = (BYTE)IMAGE_LOAD_TYPE::Empty;
        }
    } IMAGE_INFO, * LPIMAGE_INFO;

private:
    LPIMAGE_INFO imageInfo;

public:
    // 빈 비트맵 이미지를 만드는 함수
    HRESULT Init(int width, int height);
    // 파일로부터 이미지를 로드하는 함수
    HRESULT Init(const wchar_t* filePath, int width, int height);
    // 화면 출력 함수
    void Render(HDC hdc, int destX = 0, int destY = 0);
    void Render(HDC hdc, int destX, int destY, int frameIndex);
    // 메모리 해제
    void Release();
};

이미지 생성

HRESULT Image::Init(const wchar_t* filePath, int width, int height)
{
    HDC hdc = GetDC(g_hWnd);

    imageInfo = new IMAGE_INFO();
    imageInfo->resID = 0;
    imageInfo->hMemDC = CreateCompatibleDC(hdc);
    imageInfo->hBitmap = (HBITMAP)LoadImage(g_hInstance, filePath, IMAGE_BITMAP, width, height, LR_LOADFROMFILE);
    imageInfo->hOldBIt = (HBITMAP)SelectObject(imageInfo->hMemDC, imageInfo->hBitmap);
    imageInfo->width = width;
    imageInfo->height = height;
    imageInfo->loadType = (BYTE)IMAGE_LOAD_TYPE::File;

    ReleaseDC(g_hWnd, hdc);

    if (imageInfo->hBitmap == nullptr)
    {
        Release();
        return E_FAIL;
    }

    return S_OK;
}
  • GetDC(HWND) :
    • 특정 윈도우의 디바이스 컨텍스트(Device Context, DC)를 가져오는 역할
  • CreateCompatibleDC(HDC) :
    • 주어진 디바이스 컨텍스트(DC)와 호환되는 메모리 디바이스 컨텍스트(Memory DC)를 생성
    • 주로 오프스크린(off-screen) 렌더링을 수행하거나 임시 그래픽 작업을 처리하는 데 유용

이미지 출력

void Image::Render(HDC hdc, int destX, int destY, 
    int frameIndex, int row, int col)
{
    BitBlt(
        hdc,                                    
        // 복사 목적지 DC
        destX, destY,                            
        // 복사 목적지 위치
        imageInfo->width/col, imageInfo->height/row,    
        // 원본에서 복사될 크기
        imageInfo->hMemDC,                        
        // 원본 DC
        imageInfo->width/col * (frameIndex % col), 
        imageInfo->height/row * (frameIndex / col),                    
        // 원본 복사 시작 위치
        SRCCOPY                                    
        // 복사 옵션
    );
}
  • BitBlt
    • 디바이스 컨텍스트(DC) 간에 비트맵 데이터(픽셀 배열) 를 복사하거나 이동하는 데 사용
    • 주로 화면(디스플레이 DC) 또는 메모리 DC 간에 데이터를 전송하거나 그래픽 작업을 수행하기 위해 활용

'프레임워크 > WinAPI' 카테고리의 다른 글

기본 개념 및 함수  (0) 2025.03.14
디버그 출력  (0) 2025.03.14

string 리터럴

  • 일반적으로 "abcd" 와 같이 작성하는 것을 말함
  • 읽기 메모리에 저장되며, 같은 리터럴은 재사용됨
    • "abcd"를 n 번 출력해도, 메모리에 저장된 것은 "abcd" 1개
  • char* pt {"hello"}에서 요소를 변경하면, 컴파일러에 따라 결과값이 다를 수 있음
    • const char* pt {"hello"}로 정의하여, 요소 변경을 금지 시켜야 함
    • 이는 읽기 메모리에 값을 써야하기 때문
    • char pt[100] {"hello"} 와 같이 쓰면, 컴파일러가 읽기 메모리에 값을 저장x

로 스트링 리터럴(raw string literal)

  • const char* str {R"Hi, "amy""} 형태로 사용
    • 내부에서 ", \n 등의 문자를 별도 처리 없이 사용 가능
      • 주소나 긴 글 작성에 용이
  • {R"abc(a()()b)abc"} 와 같이 쓰면, 괄호를 문제없이 출력 가능
    • " ,( 사이의 16자를 구분자로 사용 ( abc(, )abc )

와이드 스트링 리터럴(wide string literal)

  • 유니코드를 포함한 모든 그래픽 리터럴을 사용가능
  • L"abcd"와 같이 사용
  • VS에서 Output 출력을 통해, 디버그를 할 때 주로 사용
  • 로케일 지원에 용이

    std::string

  • c++20 부터는 constexpr 클래스기에, 컴파일 시간에 연산을 수행하는데 사용 가능

'공부 > C++' 카테고리의 다른 글

decltype  (0) 2025.03.26
추상 클래스  (0) 2025.03.15
참조자(&)  (0) 2025.03.14
템플릿(Template)  (0) 2025.03.08
상속  (0) 2025.03.07

참고 : 소년코딩 - C++ 08.03 - 참조로 전달 (Pass by reference), 전문가를 위한 C++ 개정5판

참조자(&)

  • 사용 이유
    • 큰 구조체 or 클래스를 전달할 떄, 값의 복사를 사용하면 큰 비용
    • 포인터와 달리 항상 null 값이 될 수 없어 안정성이 좋음
    • 기본적으로 const 속성
    • 값 사용이 편함 (접근할 떄 *, & 같은거 안붙이고 써도 댐)
    • 연산자 오버로딩에 주로 사용 (lvalue에 값을 직접 대입할 떄)
  • 주의점
    • 생성하자마자 초기화가 필요
    • 레퍼런스 대상 변경 불가
        int x {3}, y{4};
        int& xRef{x};
        xRef = y;  // xRef가 y참조로 바뀌는게 아닌 x 값이 4로 변경
    • 리터럴 값 할당 불가
      • const int& a {5}; 와 같이 const 레퍼런스로만 가능
    • 출력 매개 변수보다 값으로 리턴이 바람직
      • 컴파일러가 불필요한 복제 작업 제거를 하기 때문에, 오히려 최적화가 더 잘됨
        • 리턴값 최적화(RV), 이름있는 리턴값 최적화(NRVO) 같은 복제 생략이 적용

포인터 참조

  • 포인터 추가 복사 방지
    • 포인터가 복사되지 않음
  • 포인터 주소 수정 가능
    • 기존에는 포인터가 참조하는 값을 변경하는 거지, 포인터가 가르치는 주소 변경은 불가
  • 안정성 증가
    • null 포인터, 잘못된 포인터 실수 방지 가능
      void foo(int*& ptr)

배열 참조

  • 배열의 크기를 명시해야함
      void foo(int (&ptr)[4])
  • 가변 배열의 경우에는 포인터로 전달
      void foo(int* ptr)

'공부 > C++' 카테고리의 다른 글

추상 클래스  (0) 2025.03.15
문자열  (1) 2025.03.14
템플릿(Template)  (0) 2025.03.08
상속  (0) 2025.03.07
foreach 방식 (범위 기반 for 문)  (0) 2025.03.05

5.1 3차원의 환상

  • 렌더링 파이프라인 : 현재 가상 카메라에 비친 3차원 장면의 모습에 근거해서 2차원 이미지를 생성하는데 필요한 일련의 단계
  • 물체겹침 : 불투명한 물체가 그 뒤에 있는 물체 일부 or 전체 가림

5.2 모형의 표현

  • 일반적으로 "D3D" 에서는 고형(solid)의 3차원 물체를 삼각형 메시로 근사("approximation")해서 표현
    • 삼각형 메시의 증가 = 처리량 증가
    • 선이나 점도 물체를 근사하는데 유용

5.3 컴퓨터 색상의 기본 ㅐ념

  • 기본적으로 빛의 세기 "RGB" 값을 사용
    • 일반적으로 0 ~ 1 사이 값
    • 0 은 빛이 전혀 없음, 1은 빛의 세기가 최대

5.3.1 색상 연산

  • 벡터의 덧셈과 스칼라 곱셈은 색상 변화에 영향을 끼친다
  • 내적, 외적 연산은 색상 벡터에 별로 의미가 없다
    • 내적은 스칼라 값이 결과이기에 색상 값에 영향x
    • 외적은 색상 공간에서 의미없는 방향이기에 영향x
  • 변조(modulation) : 성분별 곱셈(componentwise multiplication)
    • $(c_r, c_g, c_b)\odot (k_r, k_g, k_b) = (c_r k_r, c_g k_g, c_b k_b)$
    • 적색광 50%, 녹생광 75%, 청색광 25% 반사
      • $(r, g, b)\odot (0.5, 0.75, 0.25) = (0.5r, 0.75g, 0.25b)$
  • 한정(clamping) : 특정 구간을 벗어나지 않게 보정하는 것
    • 덧셈 연산으로 값이 벗어나면 [0, 1] 구간 내로 보정

5.3.2 128비트 색상

  • 하나의 색상을 표현하는데 128비트 (알파 값 포함)(4차원 벡터)
    • 각 성분을 32비트 부동 소수점 값 하나로 표현
    • XMVECTOR 형식 사용
    • XMVECTOR XM_CALLCONV XMColorModulate( //Returns c1 ⊗ c2 FXMVECTOR C1, FXMVECTOR C2);
    • 정밀도가 높은 색상 연산이 필요한 곳 (픽셀 셰이더 등)에 사용
      • 유효 자릿수가 많기 떄문

5.3.3 32비트 색상

  • 하나의 색상을 표현하는데 32비트 (알파 값 포함)(4차원 벡터)
    • 각 성분을 8비트 정수
    • XMCOLOR 형식 사용 : <DirectXPackedVector.h> 필요
      • [0, 255] :
        • 실수값 구간[0, 1]로 사상함으로써 32비트 색상 128비트 변환 가능
        • 32비트 -> 128비트 색상 변환 메서드 (ARGB 형식 사용)
        • XMVECTOR XM_CALLCONV PackedVector::XMLoadColor( const XMCOLOR* pSource);
        • XMVector -> XMColor 변환 메서드
        • void XM_CALLCONV PackedVector::XMStoreColor( XMCOLOR* pDestination, FXMVECTOR V)
    • 일반적으로 후면 버퍼에 최종 픽셀 색상은 32비트
      • 32bit가 메모리 및 파워 적게 사용
      • 32bit 색상 맞춤의 GPU 및 SW가 대중적
      • 사람 눈에 32bit 색상으로 충분

5.4 렌더링 파이프라인 개요

    • OM(출력 병합기) 단계 : 후면 버퍼, 깊이-스텐실 버퍼 같은 텍스처에 자료 기록
      • 양방향인 이유 -> GPU자원을 읽기도 하고 쓰기도 함을 뜻함
    • 필수 단계 목록
      • IA, VS, RS, PS, OM

5.5 입력 조립기 단계(IA)

  • 메모리에서 기하 자료(정점들과 색인들)을 읽어서 기하학적 기본도형(primitive; 삼각형, 선분, 점 등, 더 복잡한 형태 만드는 데 사용 가능한 기본도형)을 조립하는 단계

5.5.1 정점 (Vertex)

  • 3D 모델을 구성하는 기본 단위
  • 공간적 위치 이외의 정보도 담을 수 있음
    • 조명 구현을 위한 법선 벡터 추가
    • 텍스처 적용을 위한 텍스처 좌표 추가
    • "D3D"에서는 정점 형식을 정의할 수 있는 유연성 제공

5.5.2 기본도형 위상구조

  • 정점 버퍼(Vertex Buffer) : 정점을 담은 버퍼로 정점들은 여기에 담겨 렌더링 파이프라인에 묶임
  • 기본도형 위상구조(primitive toplogy)를 설정해야 함
    • 정점 버퍼에 어떻게 도형을 형성하는 지에 대한 정보가 없기 때문
    • 위상구조 설정 메서드와 관련된 열거형
        ID3D12GraphicsCommandList::IASetPrimitiveTopology( 
            D3D_PRIMITIVE_TOPOLOGY Topology);
        typedef enum D3D_PRIMITIVE_TOPOLOGY
        {
            D3D_PRIMITIVE_TOPOLOGY_UNDEFINED = 0,
            D3D_PRIMITIVE_TOPOLOGY_POINTLIST = 1,
            D3D_PRIMITIVE_TOPOLOGY_LINELIST = 2,
            D3D_PRIMITIVE_TOPOLOGY_LINESTRIP = 3,
            D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST = 4,
            D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP = 5,
            D3D_PRIMITIVE_TOPOLOGY_LINELIST_ADJ = 10,
            D3D_PRIMITIVE_TOPOLOGY_LINESTRIP_ADJ = 11,
            D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST_ADJ = 12,
            D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP_ADJ = 13, 
            D3D_PRIMITIVE_TOPOLOGY_1_CONTROL_POINT_PATCHLIST 
            = 33,
            D3D_PRIMITIVE_TOPOLOGY_2_CONTROL_POINT_PATCHLIST 
            = 34,
            .
            .
            .
            D3D_PRIMITIVE_TOPOLOGY_32_CONTROL_POINT_PATCHLIST 
            = 64,
        } D3D_PRIMITIVE_TOPOLOGY;
      • POINTLIST : 모든 정점은 개별적인 점을 그려짐
      • LINESTRIP : 차례로 연결된 선분 형성 (N + 1개 정점 = N개 선분)
      • LINELIST : 2개씩 연결된 선분 형성 (2N개 정점 = N개 선분)
      • TRIANGLESTRIP : 삼각형 띠 설정; 짝수 번쨰, 홀수 번째 삼각형 정점들이 감기는 순서가 다름 (N개 정점 = N-2개 삼각형)
        • 후면 선별 시 문제가 됨 <- GPU가 내부적으로 짝수 번쨰 삼각형의 처음 두 정점 순서를 바꿔 홀수 번쨰와 같은 순서가 되게 해서 해결
      • TRIANGLELIST : 정점 3개가 1개의 떨어진 삼각형 형성 (3N 정점 = N 삼각형)
      • 인접성(Adjacency) 정보를 가진 기본도형
        • 각 삼각형에 그에 접한 이웃 삼각형 3 개에 관한 정보 포함
        • 인접 삼각형에 접근해야 하는 특정 기하 셰이딩 알고리즘에 사용
          • 기하 셰이더의 입력으로 만 사용, 실제 그려지는 것은 X
        • "ADJ"가 뒤에 붙은 타입
      • PATCHLIST : 위상구조는 정점 자료를 N개의 제어점들로 이루어진 패치 목록으로 해석해야 함; 테셀레이션 단계들에 사용

5.5.3 색인(Index)

  • 삼각형 목록에서 정점을 제거하는 방법을 고안한 것
  • 색인을 이용하는 정점 목록과 함꼐 색인 목록을 만듦
    • 정점 목록 : 고유한 정점들로만 구성
    • 색인 목록 : 어떤 정점들을 어떤 순서로 사용해서 삼각형 형성해야 하는지를 나타내는 색인
    • Vertex v[4] {v0,v1,v2,v3}; UINT indexList[6] {0,1,2,0,2,3};
  • 색인 목록은 단순 정수이므로 메모리 소모 적음
  • 정점들이 적절한 순서로 캐시에 저장되면, GPU는 중복 정점을 자주 처리할 필요X

5.6 정점 셰이더 단계(Vertex Shader)

  • 각 정점의 위치와 속성을 처리하여 3D 공간에서 화면 공간으로 변환하는 단계
  • 화면에 그려질 모든 정점은 정점 셰이더를 거쳐 감
  • 구체적인 내용은 프로그래머가 구현해서 GPU에 제출
    • 각 정점에 대해 GPU에서 실행되기 떄문에 빠름
  • GPU 메모리에 담긴 다른 자료에 접근 가능
  • 객체의 정점을 처리하여 동차 절단 공간(Homogeneous Clipping Space)으로 변환
  • 일반적으로 클립좌표(동차 절단 좌표; 투영 행렬 거쳐 변환된 좌표)를 내보냄.
  • 월드 좌표 및 뷰 좌표가 필요하면, 사용자 정의로 추가로 저장

5.6.1 국소 공간과 세계 공간

  • 세계 공간(world space) : 물체의 기하구조를 장면 전역의 좌표게를 기준으로 직접 구축
  • 국소 공간(local space) : 물체 자신의 국소 좌표계를 기준으로 구축
  • 세계 변환(world transform) : 국소 좌표계에 상대적인 좌표를 전역 장면 좌표계에 상대적인 좌표로 바꾸는 것
  • 세계 행렬(world matrix) : 세계 변환 행렬
  • 모형을 local space에 정의하는 것에 대한 장점
    • 일반적으로 local 좌표계에서는 물체 중심이 원점과 일치; 주축 중 하나에 대칭;
    • 여러 장면에 재사용 될 때, 물체 좌표를 특정 장면을 기준으로 고정시키는 것은 비합리적;
      local 좌표계를 기준으로 좌표 저장 후, world space로 변환하는 변경 행렬을 정희해서 적용하는 것이 나음
    • 인스턴스마다 물체의 정점 및 색인 자료를 중복해서 지정하면 낭비 심함;
      local 좌표계 기준으로 한 기하구조의 복사본을 두고 개별 인스턴스마다 world matrix를 다르설정해서 그리는 것이 효율적; (인스턴싱)
  • $$
    W = \begin{vmatrix} u_x & u_y & u_z & 0 \ v_x & v_y & v_z & 0 \ w_x & w_y & w_z & 0 \ Q_x & Q_y & Q_z & 1 \end{vmatrix}
    $$
    • W 에 S(scale)R(rotation)T(translate)를 곱하여 localspace -> world space로 변경
      • W = SRT

5.6.2 시야 공간

  • 가상 카메라에 local 좌표계를 부여 할 떄, 좌표계는 시점 공간(eye space), 카메라 공간(camera space)라고도 하는 시야 공간(view space)을 정의한다
  • 카메라는 시야 공간의 원점에 놓여서 양의 z축을 바라봄
    • x축은 카메라 우측, y 축은 카메라 위
  • 렌더링 파이프라인의 후반부 단계들에서는 장면의 정점들을 world space가 아닌 view space를 기준으로 서술하는 것이 편한 경우가 있음
  • 시야 변환(view transform) : world space -> view space 좌표 변환
    • 시야 행렬(view matrix) : 시야 변환 행렬
  • 일반적으로 world 좌표계와 view 좌표계는 RT만 다르기에
    • $V = W^{-1} = (RT)^{-1} = T^{-1}R^{-1}=T^{-1}R^{T}$
    • $$
      V = \begin{vmatrix} u_x & v_x & w_x & 0 \ u_y & v_y & w_y & 0 \ u_z & v_z & w_z & 0 \ -Q \cdot u & -Q \cdot v & -Q \cdot w & 1 \end{vmatrix}
      $$
  • 직관적으로 구하는 방법
    • Q : 카메라 위치; T : 카메라가 바라보는 지점; j : world space y축(상향벡터)
    • $w = \frac{T - Q}{||T-Q||}$
    • $u = \frac{j \times w}{||j \times w||}$
    • $v = w \times u$
  • view matrix 계산 함수
    • XMMatrixLookAtLH
      XMMATRIX XM_CALLCONV XMMatrixLookAtLH( // 시야 행렬 v를 출력
          FXMVECTOR EyePosition,             // 입력 카메라 위치 Q
          FXMVECTOR FoucusPosition,             // 입력 대상 점 T
          FXMVECTOR UpDirection,             // 입력 세계 상향 벡터 j
          )

5.6.3 투영과 동차 절단 공간

  • 카메라에 보이는 공간은 절두체(frustum; 각뿔대) 로 정의 됨
  • 3차원 장면을 2차원 이미지로 표현하려면, 절두체 안의 3차원 기하구조를 2차원 투영 창으로 투영(projection)
  • 투영 방식은 반드시 하나의 소실점으로 수렴하는 방식, 깊이가 증가함에 따라 투영크기가 감소하는 방식
  • 정점의 투영선(line of projection) :3차원 기하 구조의 한 정점에서 시점으로의 직선

5.6.3.1 절두체의 정의

  • 절두체(Frustum) :
    • 원근 투영에서 사용되는 두 개의 평행한 평면으로 잘린 피라미드 모양의 공간
  • 투영 중심에서 양의 z 축을 바라보는 시야 절두체 4가지 수량
    • n : 원점과 가까운 평면 사이 거리 (z축 상 거리)
    • f : 먼 평면 사이의 거리 (z축 상 거리)
    • a : 수직 시야각
    • b : 수평 시야각
    • r : 종횡비 (w/h)(후면 버퍼 종횡비)
  • 투영 창은 후면 버퍼에 사상되므로, 종횡비를 후면 버퍼의 종횡비와 일치시키는 것이 좋음
    • 일치하지 않으면, "비균등 비례"를 적용 (이미지 왜곡됨)
  • $$
    h=2 일 떄, r = \frac{w}{h} = \frac{w}{2} => w = 2r
    $$
  • $$
    \tan{(\frac{\alpha}{2})} = \frac{1}{d} => d = \cot{(\frac{\alpha}{2})}
    $$
  • $$
    \tan{(\frac{\beta}{2})} = \frac{r}{d} = \frac{r}{\cot{(\frac{\alpha}{2})}} = r \cdot \tan{(\frac{\alpha}{2})}
    $$
  • $$
    \beta = 2 \tan^{-1}{(r \cdot \tan{(\frac{\alpha}{2})})}
    $$

5.6.3.2 정점의 투영

  • 점 (x, y, z)를 z = d 평면에 투영한 점 (x', y', d)를 구할려면,
    • $$
      \frac{x'}{d} = \frac{x}{z} => x' = \frac{xd}{z} = \frac{x \cot{\alpha / 2}}{z} = \frac{x}{z \tan{(\alpha/2)}}
      $$
    • $$
      \frac{y'}{d} = \frac{y}{z} => y' = \frac{yd}{z} = \frac{y \cot{\alpha / 2}}{z} = \frac{y}{z \tan{(\alpha/2)}}
      $$
    • 절두체 안에 있을 필요 충분 조건, $-r<=x'<=r;; -1<=y'<=1;; n<=z<=f;$

5.6.3.3 정규화된 장치 좌표(NDC) <- VS단계X 원근 나누기 단계

  • 종횡비에 대한 의존성을 없애기
    • -r <= x' <= r => -1 <= x'/r <= 1
    • [-r, r] 구간에서 [-1, 1]로 비례하는 것
  • 정규화된 좌표(normalized device coodrdinates, NDC) : 종횡비에 대한 의존성을 없앤 좌표
  • NCD 공간으로의 이러한 변화를 일종의 단위 변환(unit conversion)으로 볼 수도 있음
  • 원근 나누기(Perspective Division) :
    • 아래와 같이 원근 투영한 좌표를 NDC 좌표로 변환하는 것
  • x 축에서 NDC의 한 단위는 시야 공간의 r 단위와 같음 (1 ndc = r vs)
    • $x,vs \cdot \frac{1,ndc}{r,vs} = \frac{x}{r} ndc$
  • $$x' = \frac{xd}{z} = \frac{x}{rz \tan{(\alpha/2)}}$$
  • $$y' = \frac{yd}{z} = \frac{y}{z \tan{(\alpha/2)}}$$
  • 하드웨어는 종횡비에 대해 몰라도 됨

5.6.3.4 투영 변환을 NDC행렬로 표현

  • 일관성을 위해 투영 변환을 행렬로 표현하는 것이 바람직
  • 식을 선형인 부분과 비선형인 부분으로 나눠서 비선형 식 해결
    • z로 나누기가 비선형 부분
    • 투영 변환 전에 z 성분을 저장해야함 (정규화 시, z 성분도 정규화 되므로)
      • 동차 좌표 w 성분에 입력 (행렬 관점 [2][3] 성분을 1, [3][3] 성분을 0)
  • $$
    P = \begin{vmatrix}
    \frac{1}{r \tan{(\alpha/2)}} & 0 & 0 & 0 \
    0 & \frac{1}{r \tan{(\alpha/2)}} & 0 & 0 \
    0 & 0 & A & 1 \
    0 & 0 & B & 0
    \end{vmatrix} = \begin{vmatrix} \frac{x}{r \tan{(\alpha/2)}}, & \frac{y}{r \tan{(\alpha/2)}}, & Az + B, & z \end{vmatrix}
    $$
  • w = z로 나눠서 NDC 투영변환 완료
    • $$
      \begin{vmatrix} \frac{x}{rz \tan{(\alpha/2)}}, & \frac{y}{rz \tan{(\alpha/2)}}, & A + \frac{B}{z}, & 1 \end{vmatrix}
      $$

5.6.3.5 정규화된 깊이 값

  • 깊이 버퍼링 알고리즘을 위해 3차원 깊이 정보 필요
  • 깊이 성분의 정규화 구간은 [0, 1]
  • [n, f]를 구간 [0, 1]로 사상하는 함수 g(z)는 순서를 보존하는 함수여야 함
  • $g(z) = A + \frac{B}{z}$
    • 조건 1 : g(z) = A + B /n = 0 (가까운 평면이 0으로 사상)
    • 조건 2 : g(z) = A + B /f = 1 (가까운 평면이 1로 사상)
  • 조건에 맞춘 함수 g(z)는 다음과 같음
    • $g(z) = \frac{f}{f-n}-\frac{nf}{(f-n)z}$
    • 이 함수는 순증가(striclty increasing) 함수 이자 비선형 함수
      • 순서 보존 함수
      • 가까운 평면에 근접한 값들이 대부분을 차지
      • 가까운 평면과 먼 평면을 최대한 가깝게 해서 깊이 정밀도 문제 최소화
  • A와 B에 대한 원근투영 행렬
    • $$
      P = \begin{vmatrix}
      \frac{1}{r \tan{(\alpha/2)}} & 0 & 0 & 0 \
      0 & \frac{1}{r \tan{(\alpha/2)}} & 0 & 0 \
      0 & 0 & \frac{f}{f-n} & 1 \
      0 & 0 & \frac{-nf}{f-n} & 0
      \end{vmatrix}
      $$

5.6.3.6 XMMatrixPerspectiveFovLH 함수

  • FOV : 카메라의 시야각을 정의하는 값, 얼마나 넓은 범위를 볼 수 있는 지
    • 수직(FOVy) 또는 수평(FOVx)각도로 정의
      • d3d 에서는 수직(FOVy) 각도를 기준으로 사용 (수평각은 종횡비 계산으로 사용)
    • FOV는 절두체의 모양을 결정하는 데 영향
      • Y 각도가 커지면 절두체의 Y크기가 커지는데, 종횡비에 맞춰서 X 각도도 커지기에 종횡비는 변하지 않는다. → 절두체의 크기가 커졌기에, 물체는 작아져 보인다.
  • 원근투영 행렬 구축 함수
    // 투영 행렬 반환
    XMMATRIX XM_CALLCONV XMMatrixPerspctiveFovLH(
        float FovAngleY   // 수직 시야각(라디안)
        float Aspect,     // 종회비 = 너비 / 높이
        float NearZ,      // 가까운 평면 거리
        float FarZ        // 먼 평면 거리
    )
    ```    ```cpp
    // 투영 행렬 반환
    XMMATRIX XM_CALLCONV XMMatrixPerspctiveFovLH(
        float FovAngleY   // 수직 시야각(라디안)
        float Aspect,     // 종회비 = 너비 / 높이
        float NearZ,      // 가까운 평면 거리
        float FarZ        // 먼 평면 거리
    )

5.7 테셀레이션 단계들

  • 선택적 단계
  • 테셀레이션(Tessellation) :
    • 주어진 메시의 삼각형들을 더 잘게 쪼개서 새로운 삼각형들을 만드는 과정
    • 새 삼각형을 새로운 위치로 이동 함으로써, 원래 메시에는 없는 세부적인 특징을 만들어 냄
  • 장점
    • 카메라에 가까운 삼각형들에는 테셀레이션을 적용해서 세부도 향상, 먼 삼각형에는 적용하지 않는 방식의 세부수준(level-of-detail, LOD) 메커니즘 구현 가능
    • 메모리에는 저다각형(low-poly) 메시를 담고 즉석에서 메시를 추가함으로써 메모리 절약
    • 애니메이션이나 물리 처리 같은 연산들은 low-poly에서 수행, 테셀레이션된 고다각형 메시는 렌더링에만 사용 (계산량 줄이기)
  • D3D11 부터 추가 된, 기하구조를 GPU에서 테셀레이션하는 수단 제공

5.8 기하 셰이더 단계

  • 선택적 단계
  • 기하 셰이더 단계(Geometry Shader) :
    • 하나의 온전한 기본 도형을 입력받아서 임의로 변형
    • 정점 셰이더의 처리를 거친 정점을 입력 받음
    • 입력 받은 기본 도형을 바탕으로 새로운 도형 생성, 파괴
      • 점이나 선분을 사각형으로 확장
    • 기하 셰이더에서 출력된 정점 위치들은 반드시 동차 절단 공간으로 변환된 것이어야 함
  • 장점
    • 기하구조를 GPU에서 생성하거나 파괴 가능
    • 스트림 출력 단계를 통해 메모리의 버퍼에 저장해 두고 나중에 활용 가능

5.9 절단

  • 절단 (clipping) :
    • 완전히 시야 절두체 바깥에 있는 기하구조는 폐기, 절두체의 경계면과 교차하는 기하구조는 절두체 내부의 것만 남도록 잘라내는 것
    • 볼록 도형 절단 시, 볼록 도형이 남음
    • 삼각형을 절단할 시, 사각형이 되므로 삼각화 연산 필요
    • 하드웨어가 수행해 줌
      • 서덜런드-호지먼 절단 알고리즘 참고
  • 4차원 점 (x,y,z,w)가 시야 절두체 안에 있을려면 다음 조건 만족
    • -w <= x <= w; -w <= y <= w; 0 <= z <= w
      • 왼쪽 평면 : w = -x; 오른쪽 평면 : w = x; 아래 평면 w = -y;
      • 위 평면 : w = y; 가까운 평면 : z = 0; 먼 평면 : z = w;

5.10 래스터화 단계(resterization sate)

  • 투영된 3차원 삼각형으로부터 픽셀 색상들을 계산

5.10.1 뷰포트 변환

  • 절단 후, 하드웨어는 원근 나누기를 수행해서 동차 절단 공간 좌표를 장치 좌표(NDC)로 변환할 수 있음
  • 정점들이 NDC 공간으로 들어오면, 2차원 이미지 형성 점들의 2차원 x,y 좌표성분들이 후면 버퍼의 한 직사각형 영역으로 변환 (뷰포트)
  • 뷰포트 변환 시, z 성분 변하지 않음
    • 선형 변환이기에 왜곡되지 않았다는 것이지 [0,1]구간으로 값 자체는 선형 매핑으로 변함
    • D3D12_VIEWPORT 구조체의 MinDepth, MaxDepth 변경으로 깊이 값에 영향 미칠 수 있음 <- 반드시 [0, 1] 구간

5.10.2 후면 선별

  • 삼각형 정점들이 $v_0, v_1, v_2$ 순서로 감길 때, 삼각형의 법선 n은 다음과 같이 정의
    • $e_0 = v_1 - v_0$, $e_1 = v_2-v_0$, $n=\frac{e_0 \times e_1}{e_0 \times e_1}$
    • 이 법선이 가리키는 방향이 삼각형의 앞쪽(front side), 반대는 **뒤쪽(back side)
  • 관찰자에게 보이는 면이 앞쪽이면 전면(front-facing), 뒤쪽이면 후면(back-facing)
    • D3D에서는 기본적으로 "전면 = 시계 방향", "후면 = 반시계 방향" 간주
  • 후면 선별(backface culling) : 카메라에 보이지 않는 후면 삼각형을 골라서 폐기하는 것

5.10.3 정점 특성의 보간

  • 뷰포트 변환 후에 정점의 특성(색상, 법선 벡터, 텍스처 좌표)들을 삼각형을 덮는 각 픽셀에 대해 보간해야 함
  • 정점의 깊이 값도 보간하여, 각 픽셀에 깊이 버퍼링 알고리즘을 위한 깊이 값 부여 필요
  • 정점 특성들은 3차원 공간에서 삼각형의 면을 따라 선형으로 보간
    • 그대로 화면 공간에 사상하면 간격이 고르지 않음
  • 원근 보정 보간은 하드웨어가 수행하는 것으로 세부 사항 숙지 필요 x
    • [Everly01]에 수학적 유도 과정있음
  • 3차원 공간에서 균등한 분할은 화면 공간(2차원) 투영 시 유지x
    • 3차원 공간에서 선형 보간을 위해서는 화면 공간에서 비선형 보간 수행

5.11 픽셀 셰이더 단계

  • 선택적 단계
  • 색상 출력 단계
  • 픽셀 셰이더(pixel shader) :
    • 프로그래머가 작성하고 GPU가 실행하는 프로그램
    • 각각의 픽셀 단편(pixel fragment)에 대해 실행
    • 보간된 정점 특성들을 입력받아서 하나의 색상 출력
    • 고정된 상수 색 반환, 픽셀 당 조명, 반사 ,그림자 효과 등 수행

5.12 출력 병합기 단계

  • PS 가 생성한 픽셀 단편들을 입력으로 받음
    • 일부 픽셀 단편들이 기각될 수 있음 (깊이 판정, 스텐실 판정에 의해)
    • 기각되지 않은 픽셀 단편드을 후면 버퍼에 기록
  • 혼합(blending)이 일어나는 단계
    • 새 픽셀이 후면 버퍼의 기존 픽셀을 완전히 덮어쓰는 것이 아니라 일정 공식에 따라 섞은 결과를 기혹하는 것
    • 반투명 특수 효과에 사용

참조 : 씹어먹는 C++ - <9 - 4. 템플릿 메타 프로그래밍 2>

기본

  • 템프릿은 컴파일 시에 아무런 코드로 변환X
    • 문법 검사만 실행
  • 인스턴스화 했을 떄, 변환
  • typename T = int 와 같이 디폴트 값 설정 가능

클래스 템플릿

  •   template <typename T>  // <- <class T> 보다 typename 권장
      class Vector {
          T* data;
      }
      // 부분 특수화
      template <typename B>
      class test<int, B, double> {};
    
      // 완전 특수화
      template <>
      class test<int, int, double> {};

명시적 템플릿 인스턴스화

  • 일반적으로 클래스 템플릿은 객체 생성과 타입 지정이 명시적으로 이루어져야 하는 경우가 많음.

    • 중복 코드 방지 (같은 타입의 클래스가 여러 파일에서 중복 인스턴스화 문제)
    • ODR(One Definition Rule) 위반 링크 에러 가능성 높음
  • 함수 템플릿은 암시적 인스턴스화가 유연하게 잘 동작

    • 링크 단계에서 대부분 하나로 병합됨.
    • 즉, 컴파일러에서 최적화를 잘 해줌
  • 명시적 템플릿 인스턴스화 예시

      // 헤더 파일
      template<typename T>
      class MyTemplate {
      public:
          void func();
      };
    
      // 명시적 선언
      extern template class MyTemplate<int>;
    
      // 소스 파일
      template class MyTemplate<int>;

함수 템플릿

  • 기본 사용

      template <typename T>
      T max(T& a, T& b) {
        return a > b ? a : b;
      }
  • Functor : 함수는 아니지만 함수 인 척을 하는 객체

      template <typename Cont, typename Comp>
      void sort(Cont& cont, Comp& comp)
      {
          if(!comp(cont[1], cont[0]))
              cont.swap();
      }
    
      struct Comp1 {
    bool operator()(int a, int b) { return a > b; }
      };
    
    Comp1 comp1;
    sort(int_vec, comp1);
  • 디폴트 템플릿 인자 : 입력값 없을 떄, 기본 타입 지정

      template <typename Cont = int>
  • 템플릿 비타입 매개변수 : 컴파일 시간에 결정되는 정적 값

      template <typename Cont, int num = 5>
  • 팩토리얼 구현 예시

      #include <iostream>
    
      // 기본 템플릿 선언
      template <int N>
      struct Factorial {
          static constexpr int value = N * Factorial<N - 1>::value;
      };
    
      // 특수화: N == 0일 때 재귀 종료
      template <>
      struct Factorial<0> {
          static constexpr int value = 1;
      };
    
      int main() {
          // Factorial<3>의 결과 출력
          std::cout << "Factorial<3> = " << Factorial<3>::value << std::endl; // 출력: 6
          return 0;
      }
    

다양한 타입의 복제, 대입 생성자

  • 만약, double 타입과 int 타입의 템플릿으로 생성된 인스턴스가 있을 때는 다음과 같이 복제, 대입 연산자를 선언해야 한다.
      template <typename T>
      class Grid
      {
      public:
          template <typename E>
          Grid<const Grid<E>& src);
          template <typenmae E>
          Grid<T>& operator=(const Grid<E>& rhs);    
      }
      /* 소스파일 */
      template <typename T>
      template <typename E>    // template <typename T, typename E> 와 같이 작성 불가
      Grid<T>::Grid(const Grid<E>& src) : Grid { src.getWidth(), src.getHeight()}
      {
          ...
      }
      template <typename T>
      template <typename E>
      Grid<T>& Grid<T>::operator=(const Grid<E>& rhs)
      {
          ...
      }
    • template <typename T, typename E> 와 같이 작성 불가

가변길이 템플릿

  • 템플릿 파라미터 팩(parameter pack)
    • template <typename T, typename... Types>
    • void print(T arg, Types... args)
  • 이전 print(T arg)가 있어야 됨.
     template <typename T>
     void print(T arg) {
       std::cout << arg << std::endl;
    }
     template <typename T, typename... Types>
     void print(T arg, Types... args) {
       std::cout << arg << ", ";
       print(args...);
     }
  • 기저 템플릿 없이 사용할려면
      template <typename T, typename... Types>
      void print(T arg, Types... args) {
        std::cout << arg << ", ";
        if constexpr (sizeof(args) > 0)
            print(args...);
      }
  • Fold 방식 (C++17)
    • 4가지 지원
    • 괄호 필수
      return (...+ nums);  // Left Fold
      return (nums + ...); // Right Fold
      return (initial + ... + args); //Left Fold with Initial
      return (args + ... + initial); // Right Fold with Initial

TMP(Template Meta Programing)

  • 예제

      template <int N, int D = 1>
      struct Ratio {
        typedef Ratio<N, D> type;
        static const int num = N;  // 분자
        static const int den = D;  // 분모
      };
      template <class R1, class R2>
      struct _Ratio_add {
        typedef Ratio<R1::num * R2::den + R2::num * R1::den, R1::den * R2::den> type;
      };
    
      template <class R1, class R2>
      struct Ratio_add : _Ratio_add<R1, R2>::type {};
    
      int main() {
        typedef Ratio<2, 3> rat;
        typedef Ratio<3, 2> rat2;
        typedef Ratio_add<rat, rat2> rat3;
    
        std::cout << rat3::num << " / " << rat3::den << std::endl;
    
        return 0;
      }
  • C++11 부터는 using 키워드로 대체가능

      typedef Ratio_add<rat, rat2> rat3;
      using rat3 = Ratio_add<rat, rat2>;
  • 의존 타입은 typename으로 타입임을 명시

where 제약 조건 사용법

  • C++20 이전 :

    • SFINAE (Substitution Failure Is Not An Error) 이용 <- 치환 실패 시, 오버로딩 후보에서 제외

    • enalbe_if 이용

      #include <iostream>
      #include <type_traits>
      
        // 첫 번째 조건: T1이 정수, T2가 실수
        // 템플릿 파라미터에 조건 걸기
        template <typename T1, typename T2,
                  typename std::enable_if<std::is_integral<T1>::value, void>::type* = nullptr,
                  typename std::enable_if<std::is_floating_point<T2>::value, void>::type* = nullptr>
        void func(T1 a, T2 b) {
            std::cout << "정수: " << a << ", 실수: " << b << std::endl;
        }
      
        // 템플릿 파라미터에 조건 걸기, 한 줄로 쓰기
        template <typename T1, typename T2,
              typename std::enable_if<std::is_integral<T1>::value && std::is_floating_point<T2>::value, void>::type* = nullptr>
        void func(T1 a, T2 b) {
            std::cout << "정수: " << a << ", 실수: " << b << std::endl;
        }
      
        // 반환 타입으로 사용
        template <typename T1, typename T2>
        typename std::enable_if<std::is_integral<T1>::value && std::is_floating_point<T2>::value, void>::type
        void func(T1 a, T2 b) {
            std::cout << "정수: " << a << ", 실수: " << b << std::endl;
        }
      
      
    int main() {
    func(42, 3.14);  // 올바른 호출
    // func(3.14, 42);  // 오류: 첫 번째 인자가 실수이므로 조건 불충족
    return 0;
}

    ```
  • C++20 이후 : Concepts

      #include <iostream>
      #include <concepts>
    
      // 정수 타입을 요구하는 Concept
      template <typename T>
      concept Integer = std::is_integral_v<T>;
    
      // Integer Concept을 만족하는 타입만 허용
      template <Integer T>
      T add(T a, T b) {
          return a + b;
      }
    
      int main() {
          std::cout << add(3, 4) << std::endl; // 올바른 호출, 정수 타입
          // std::cout << add(3.5, 4.5) << std::endl; // 컴파일 오류, 실수 타입
          return 0;
      }
    

'공부 > C++' 카테고리의 다른 글

문자열  (1) 2025.03.14
참조자(&)  (0) 2025.03.14
상속  (0) 2025.03.07
foreach 방식 (범위 기반 for 문)  (0) 2025.03.05
(C++17) optional & variant  (0) 2025.03.05

참조 : 전문가를 위한 C++ 개정 5판

사용 주의 점

  • 다중 상속 사용 시, 중복 상속 주의
    • virtual 상속으로 방지하기
  • 부모 클래스는 virtual 소멸자 지정하기
    • 소멸자에 virtaul 키워드가 없으면 자식의 소멸자 실행 안될 수 있음
  • 부모 클래스의 생성자, 소멸자 자동 호출
  • void 매개변수 함수는 오버로딩 시, 보이드 함수가 가려짐
    • using {부모 함수} 를 사용하여, 유지 가능
      • 부모 함수의 모든 생성자를 상속 받게 됨
  • 부모의 오버로드된 메서드 중 1개만 오버라이드해도, 나머지 모두 오버라이드로 간주
    • 실제 부모의 다른 오버로드 메서드를 호출 시, 파생 클래스의 오버로드 메서드를 실행하기에 선언 안되었다고 에러 발생;
      • 명시적으로 전부 override하거나 using으로 해당 메서드 전부 가져오기
  • 업 캐스팅은 문제 x, 다운 캐스팅은 주의
    • 다운 캐스팅은 부모 클래스가 자식 클래스보다 작기에 데이터 부족
    • dynamic_cast 사용
  • static 메서드는 상속되지 않음
  • private 메서드는 호출이 안될 뿐, override는 가능
  • 파생 클래스에서 부모 클래스의 메서드 접근권한 변경해서 사용 가능
    • 부모에서 protected -> public, public -> protected 가능
    • 부모에서는 여전히 protected 지만, 파생클래스로 접근 시, 접근가능
  • 파생 클래스에서 복제, 대입 연산자 새롭게 정의 시, 부모 복제, 대입 연산자 정의
    • 명시적으로 파생클래스에서 부모의 복제, 대입 연산자를 사용
  • 디폴트 인수는 상속되지 않음

사용 방법

  • 일반

    • 상속 접근 제한자 : 부모의 모든 멤버의 접근제한자를 해당 접근제한이 최대치로 해서 가져옴.

    • 메서드를 상속받을려면, 옆에 부모 클래스의 메서드 표기 (초기화 리스트보다는 항상 앞에)

      class Derived : public Base {
      std::string s;
      
      public:
      Derived() : Base(), s("파생") {
         std::cout << "파생 클래스" <<  std::endl;
      
        // Base 에서 what() 을 물려 받았으므로
        // Derived 에서 당연히 호출 가능하다
        what();
      }
      };
  • 순수 가상 함수

    • 반드시 override 해야함.
    • 이를 포함한 클래스를 추상클래스라고 함
    • virtaul {function} = 0
      class Animal {
      public:
      Animal() {}
      virtual ~Animal() {}
      virtual void speak() = 0;
      };
  • 다중 상속

    • 상속 여러 개 받기
    • 중복 될 가능성 있는 클래스에 virtaul 키워드 붙여서 받기
    • using으로 부모의 생성자를 여러개 상속 받을 떄, 겹치는 부분은 명시적으로 선언
      class C : public virtual public A, public B
      {
        using A::A;
        using B::B;
        C(int k); // A와 B의 (int k) 생성자 겹치는 부분 명시적으로 선언
      }

virtual 과 override

  • virtual 키워드 사용 시, 런타임에 컴파일러가 인식하고 동적 바인딩을 실행

    • virual 함수는 가상 함수 테이블을 생성, 호출 시에 해당 테이블 탐색 과정이 추가
    • 파생 (자식)클래스는 virtaul 키워드가 없어도 virtaul상태를 유지한다
    • 가상 함수 테이블의 성능 오버헤드는 미미한 편. (크게 신경써서 가독성, 유지보수성 꺠지 않기)
      • 그래픽 용 클래스 (point, edge 등에는 신경쓰기)는 무수히 많이 사용하기에 주의
  • override :

    • 11부터 사용
    • 실수로 오버라이드 하지 않는 경우를 방지하기 위해 사용
      • 코드 안정성과 명확성 떄문에 사용
    • 사용하지 않는다고 override 안되는 거 아님

virtual 소멸자

  • 자식은 본인이 상속 받는 다는 것을 알고 있기에 부모의 소멸자를 호출
  • 부모는 자식이 상속 받고 있다는 것을 모르기에 본인의 소멸자 만 호출
    • -> 이를 방지하기 위해 부모의 소멸자를 virtaul로 지정

공변 리턴 타입(covariant return type)

  • 리턴 타입이 부모인 메서드를 오버라이드 하여, 본인 리턴타입으로 변환하는 것
    • 전혀 다른 타입으로 변환은 불가
    • 리스코프 치환 원칙(LIP)를 통해 확인
      • 원본 메서드의 리턴 타입을 변경해도 기존 코드가 제대로 작동하는지 확인
      • 부모가 사용되는 위치에 자식을 넣어도 문제가 없는지
        class BingCherry
        {
        // virtual Cherry* BingCherry::Pick();
        BingCherry* BingCherry::Pick() override
        }
  • unique_ptr<Cherry>unique_ptr<BingCherry>는 연관이 없기에 변환 불가능

복제, 대입 연산자

  • 파생 클래스에서 기본적으로 부모의 복제, 대입연산자를 파생 클래스에 맞게 변환
  • 파생 클래스에서 새롭게 정의 시, 부모 복제, 대입 연산자 정의
      Derived(const Derived& src) : Base { src }{}
      Derived& Derived::operator=(const Derived& rhs)
      {
          if(rhs == this)
          {
              return *this;
          }
          Base::operator=(rhs); // 부모 대입 연산자 호출
          // 파생 클래스 대입 연산 수행
          return *this;
      }

'공부 > C++' 카테고리의 다른 글

참조자(&)  (0) 2025.03.14
템플릿(Template)  (0) 2025.03.08
foreach 방식 (범위 기반 for 문)  (0) 2025.03.05
(C++17) optional & variant  (0) 2025.03.05
(C++17,20)구조적 바인딩  (0) 2025.03.05

+ Recent posts

let textNodes = document.querySelectorAll("div.tt_article_useless_p_margin.contents_style > *:not(figure):not(pre)"); textNodes.forEach(function(a) { a.innerHTML = a.innerHTML.replace(/`(.*?)`/g, '$1'); });