목차
- [[#6.6 상수 버퍼|6.6 상수 버퍼]]
- [[#6.6 상수 버퍼#6.6.4 상수 버퍼 서술자|6.6.4 상수 버퍼 서술자]]
- [[#6.6 상수 버퍼#6.6.5 루트 서명과 서술자 테이블|6.6.5 루트 서명과 서술자 테이블]]
- [[#6.7 셰이더의 컴파일|6.7 셰이더의 컴파일]]
- [[#6.7 셰이더의 컴파일#6.7.1 오프라인 컴파일|6.7.1 오프라인 컴파일]]
- [[#6.8 래스터화기 상태|6.8 래스터화기 상태]]
- [[#6.9 파이프라인 상태 객체|6.9 파이프라인 상태 객체]]
- [[#번외|번외]]
- [[#번외#✅ 구성만 가능한 고정 기능(Fixed-function) 단계들(shader가 아닌 단계들)|✅ 구성만 가능한 고정 기능(Fixed-function) 단계들(shader가 아닌 단계들)]]
6.6 상수 버퍼
상수 버퍼(contant buffer) : 셰이더 프로그램에서 참조하는 자료를 담는 GPU 자원(ID3DResource)의 예
변하지 않는 작은 데이터를 저장하는 용도로 사용
HLSL에서의 상수 버퍼는
cbuffer
타입 <- "ShaderModel 4.0"; "5.1 이상에서는ConstantBuffer<{type}>
을 선호"최대 4096개의 벡터 보유가능 ( 최대 4개의 32비트 값 포함)
- 256개의 float4x4 ->
64 KB
- 256개의 float4x4 ->
파이프라인 단계당 최대 14개의 상수 버퍼 바인딩 가능(2개의 추가 슬롯은 내부용으로 예약되어 있음) { b0 ~ b13 }
cbuffer cbPerObject : register(b0) <- b0 상수 버퍼 사용 { ... }
CPU가 프레임당 한 번 갱신하는 것이 일반적 (업로드 힙 사용)
- GPU가 빠르게 참조
- CPU-GPU 동기화를 피하기 위해서 , 상수 버퍼에도 더블, 트리플 버퍼링 사용
- 정적 데이터 사용 시에는 디폴트 힙 사용
최소 하드웨어 할당 크기(256 Byte)의 배수여야 함
GPU는 256바이트 단위로 상수버퍼 로딩
효율적인 캐시/버스 활용을 위해
HLSL에서 사용자의 정의 Byte가 256일 필요x (내부적으로 256 byte 배수로 처리)
Dx(c++)에서는 지정해야 함;
(256 Byte 단위로 읽기 때문에 패딩을 맞춰주어야 하기 때문)cbuffer PerFrame : register(b0) { float4x4 view; // 64 bytes float4x4 projection; // 64 bytes float time; // 4 bytes float3 padding; // 12 bytes (패딩 맞춤) } // 총 크기 = 64 + 64 + 4 + 12 = 144 bytes → 내부적으로는 256 bytes로 할당됨
Dx 상수버퍼 생성 예제
struct ObjectConstants { DirectX::XMFLOAT4X4 WorldViewProj = MathHelper::Identity4x4(); }; UINT mElementByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants)); // 업로드 힙에 상수 버퍼 생성 ComPtr<ID3D12Resource> mUploadCBuffer; device->CreateCommitedResource( &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD), D3D12_HEAP_FLAG_NONE, &CD3DX12_RESOURCE_DESC::Buffer(mElementByteSize * NumElements), D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&mUploadCBuffer)); // CBV(상수 버퍼 뷰) 생성 D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc; cbvDesc.BufferLocation = mUploadCBuffer->GetGPUVirtualAddress(); cbvDesc.SizeInBytes = mElementByteSize; device->CreateConstantBufferView(&cbvDesc, cbvHeap->GetCPUDescriptorHandleForHeapStart()); // CBV 루트 시그니처에 바인딩 commandList->SetGraphicsRootDescriptorTable(0, cbvHeap->GetGPUDescriptorHandleForHeapStart()); // 상수 버퍼 갱신 ObjectConstants* mMappedData = nullptr; // (대응 부분자원 색인, 메모리 범위 서술(nullptr 전체대응). 대응된 자료 가리키는 포인터) mUploadCBuffer->Map(0, nullptr, reinterpret_cast<void**>(&mMappedData)); // 쓰기만 할거기에 메모리 범위를 나타내는 CD3DX12_RANGE readRange(0, 0);가 범위 // 그래서 nullptr을 쓸 수 있다 ObjectConstants objConstants; DirectX::XMMATRIX world = DirectX::XMMatrixIdentity(); DirectX::XMMATRIX view = ...; // 뷰 행렬 DirectX::XMMATRIX proj = ...; // 프로젝션 행렬 DirectX::XMMATRIX wvp = world * view * proj; XMStoreFloat4x4(&objConstants.WorldViewProj, XMMatrixTranspose(wvp)); // HLSL과 행 우선/열 우선 일치 // 프레임마다 갱신 시 mMappedData[i] = objContants; // 리소스 해제는 선택 사항 : dx에서 알아서 해제해줌, 그래도 디버깅 및 예외 발생을 대비해 하는것이 좋음 (! 지속적으로 사용되는 리소스는 해제x ) // 업로드 힙 더이상 사용안하면 리소스 해제 if (mUploadBuffer != nullptr) mUploadBuffer->Unmap(0, nullptr); mMappedData = nullptr;
->
d3dUtil::CalcConstantBufferByteSize()
: 256 byte 배수 단위 버퍼 크기로 계산 반환
-> 상수 버퍼에서 해당 물체를 위한 상수들이 있는 부분 영역을 서술하는 상수 버퍼 뷰를 파이프라인에 묶음256 Byte배수 단위 변환 구현
#include <iostream> using namespace std; constexpr size_t ALIGNMENT = 256; size_t CeilAlign(size_t size) { return (size + (ALIGNMENT - 1)) & ~(ALIGNMENT - 1); } int main() { size_t size1 = 180; // 예제: 180바이트 데이터 size_t size2 = 300; // 예제: 300바이트 데이터 // 256 cout << "Ceil Align (180): " << CeilAlign(size1) << endl; // 512 cout << "Ceil Align (300): " << CeilAlign(size2) << endl; return 0; }
6.6.4 상수 버퍼 서술자
D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV
형식의 서술자 힙에 담김
D3D12_DESCRIPTOR_HEAP_DESC cbvHeapDesc;
cbvHeapDesc.NumDescriptors = 1;
cbvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
cbvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
cbvHeapDesc.NodeMask = 0;
ComPtr<ID3D12DescriptorHeap> mCbvHeap = nullptr;
md3dDevice->CreateDescriptorHeap(&cbvHeapDesc, IID_PPV_ARGS(&mCbvHeap);
-> 셰이더 프로그램에서 서술자에게 접근할 것임을 뜻하는 flag 지정
6.6.5 루트 서명과 서술자 테이블
루트 서명(root signature) : 그리기 호출 전에 응용 프로그램이 필수로 바인딩 해야하는 자원 및 그 자원들에 대한 셰이더 입력 레지스터들에 어떻게 대응되는지를 정의
- 반드시, 그리기 호출에 쓰이는 셰이더들과 호환되어야 함
- 루트 서명 유효성은 파이프라인 상태 객체를 생성할 떄 검증
- 정의를 할 뿐, 바인딩 하지는 않음
대표 인터페이스 :
ID3D12RootSignature
- 주어진 그리기 호출에서 셰이더들이 기대하는 자원들을 서술하는 루트 매개변수들의 배열로 정의
- 루트 매개변수 : 하나의 루트 상수 or 루트 서술자 or 서술자 테이블 일 수 있음
루트 서명 생성 예시
CD3DX12_ROOT_PARAMETER slotRootParameter[1]; // CVB 하나를 담는 서술자 테이블 생성 CD3DX12_DESCRIPTOR_RANGE cbvTable; cbvTable.Init( D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, // 테이블의 서술자 개수 0 // 이루트 매개변수에 묶일 셰이더 인수들의 기준 레지스터 번호 ); slotRootParameter[0].InitAsDescriptorTable( 1, // range 개수 &cbvTable // 구간들의 배열을 가리키는 포인터 ); // 루트 서명은 루트 매개변수들의 배열 CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(1, slotRootParameter, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT); // 상수 버퍼 하나로 구성된 서술자 구간을 가리키는 // 슬롯 하나로 이루어진 루트 서명 생성 ComPtr<ID3DBlob> serializedRootSig = nullptr; ComPtr<ID3DBlob> errorBlob = nullptr; HRESULT hr = D3D12SerializeRootSignature(&rootSigDesc, D3D_ROOT_SIGANTURE_VERSION_1, serializedRootSig->GetAddressOf(), errorBlob.GetAddressOf()); ThrowIfFailed(md3dDevice->CreateRootSignature( 0, serializedRootSig->GetBufferPointer(), serializedRootSig->GetBufferSize(), IID_PPV_ARGS(&mRootSignature)));
자원 바인딩 :
ID3D12GraphicsCommandList::SetGraphicsRootDescriptorTable
을 호출해서 서술자 테이블을 파이프라인에 묶음void ID3D12GraphicsCommandList::SetGraphicsRootDescriptorTable( UINT RootParameterIndex, D3D12_GPU_DESCRIPTOR_HANDLE BaseDescriptor);
RootParameterIndex
: 설정하고자 하는 루트 서명의 색인BaseDescriptor
: 설정하고자 하는 서술자 테이블으 첫 서술자에 해당하는 서술자의 핸들
루트 서명과 CBV 힙을 명력 목록에 설정하고, 파이프라인에 묶을 자원들을 지정하는 서술자 테이블 생성
mCommandList->SetGraphicsRootSignature(mRootSignature.Get()); ID3D12DescriptorHeap* descriptorHeaps[] = {mCbvHeap.Get()}; mCommandList->SetDescriptorHeaps(_countof(descriptorHeaps), descriptorHeaps); // 이번 그리기 호출에서 사용할 CBV의 오프셋 CD3DX12_GPU_DESCRIPTOR_HANDLE cbv(mCbvHeap->GetGPUDescriptorHandleForHeapStart()); cbv.Offset(cbvIndex, mCbvSrvUavDescritoprSize); mCommandList->SetGraphicsRootDescriptorTable(0, cbv);
성능을 위해서는 루트 서명을 최대한 작게 만들고, 렌더링 과정에서 루트 서명 변경 최소화
응용 프로그램이 묶은 루트 서명의 구성물이 그리기/분배(dispatch) 호출들 사이에서 변할 때마다, D3D12 드라이버가 해당 내용물에 자동으로 버전 번호 부여.
- 각각의 그리기/분배 호출은 고유한 루트 서명 상태들의 집합을 받게 됨
루트 서명 변경시, 기존의 모든 바인딩이 삭제됨
6.7 셰이더의 컴파일
D3DCompileFromFile
HRESULT D3DCompileFromFile ( LPCWSTR pFileName, const D3D_SHADER_MACRO *pDefines, ID3DInclude *pInclude, LPCSTR pEntrypoint, LPCSTR pTarget, UINT Flags1, UINT Flags2, ID3DBlob **ppCode, ID3DBlob **ppErrorMsgs);
pFileName
: 컴파일할 HLSL 소스 코드를 담은 .hlsl 파일 이름pEntrypoint
: 파일 내부에 여러개의 셰이더 있을 시, 진입점 명시pTarget
: 셰이더 프로그램의 종류와 대상 버전을 나타내는 문자열 (vs, hs, ds, ...)Flags1
: 셰이더 코드의 세부적인 컴파일 방식 제어 플래그D3DCOMPILE_DEBUG
: 셰이더를 디버그 모드에서 컴파일D3DCOMPILE_SKIP_OPTIMIZATION
: 최적화 생략 (디버깅에 용이)
Flags2
: 효과(effect)의 컴파일 관한 고급옵션ppCode
: 컴파일된 셰이더 목적 바이트코드를 담은 II3DBlob 구조체의 포인터를 매개변수를 통해 돌려줌ppErrorMsgs
: 컴파일 오류 발생한 경우, 오류 메시지 문자열을 담은 ID3DBlob 구조체의 포인터를 이 매개변수를 통해 돌려줌
ID3DBlob
을 이용한 디버깅LPVOID GetBufferPointer
: 버퍼를 가리키는 void* 포인터를 돌려줌. (실제 사용하려면 적절한 형식으로 캐스팅)SIZE_T GetBufferSize
: 버퍼의 크기(바이트 개수)를 반환사용 예시
UINT compileFlags = 0; #if defined(DEBUG) || defined(_DEBUF) compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION; #endif HRESULT hr = S_OK; ComPtr<ID3DBlob> byteCode = nullptr; ComPtr<ID3DBlob> errors; hr = D3DCompileFromFile(filename.c_str(), defines, D3D_COMPILE_STANDARD_FILE_INCLUDE, entrypoint.c_str(), target.c_str(), compileFlags, 0, &byteCode, &errors); // 오류 메시지 디버그 창에 출력 if (errors != nullptr) OutputDebugStringA((char*)errors->GetBufferPointer())); ThrowIfFailed(hr); return byteCode;
셰이더의 컴파일은 파이프라인에 바인딩 되는 것을 의미하지 않는다
VS에서는
hlsl
확장자 파일이HLSL컴파일러
타입 빌드로 되어있는데, 이를 빌드하지 않음으로 설정한다; (D3D에서 별도 컴파일 API 사용 or fxc.exe 도구로 사전 컴파일 선호)
6.7.1 오프라인 컴파일
오프라인 컴파일 하는 이유
- 복잡한 셰이더는 컴파일 시간이 오래 걸림; 오프라인 컴파일 시, 게임의 적재(loading)시간이 빨라짐
- 셰이더 컴파일 오류들은 실행 시점이 아니라 빌드 과정에서 일찍 점검하는 것이 편함
- Windows8 스토어 앱은 반드시 오프라인 컴파일 사용
컴파일된 셰이더를 담는 파일의 확자자로는
.cso(compiled shader object)
를 사용하는 것이 관례셰이더 오프라인 컴파일할 떄에는 DirectX에 포함된 FXC 도구 사용 (CL 도구)
- 디버그 모드 컴파일
fxc "color.hlsl" /Od /Zi /T vs_5_0 /E "VS" /Fo "color_vs.cso" /Fc "color_vs.asm"
- 명령줄 옵션
/Od
: 최적화 비활성화/Zi
: 디버그 정보 활성화/T <문자열>
: 셰이더의 종류와 대상 버전/E <문자열>
: 셰이더 진입점/Fo <문자열>
: 컴파일된 셰이더 바이트코드 목적 파일(object file)/Fc <문자열>
: 어셈블리 코드 목록 출력 (디버깅, 명령 개수 점검, 생성된 코드 종류 확인 등에 유용)
- 디버그 모드 컴파일
컴파일된 셰이더 파일 사용 예시
bool LoadShader(const std::string& path, ComPtr<ID3DBlob>& outBlob) { std::ifstream file(path, std::ios::binary | std::ios::ate); if (!file) return false; std::streamsize size = file.tellg(); if (size <= 0) return false; file.seekg(0, std::ios::beg); if (FAILED(D3DCreateBlob(size, &outBlob))) return false; if (!file.read((char*)outBlob->GetBufferPointer(), size)) return false; return true; }
6.8 래스터화기 상태
렌더링 파이프라인 단계 중 구성(설정)만 가능한 단계
래스터화기 상태(resterizer state) 를 통해서 구성
대표 구조체 :
D3D12_RASTERIZER_DESC
구조체typedef struct D3D12_RASTERIZER_DESC { D3D12_FILL_MODE FillMode; // 기본값 : D3D12_FILL_SOLID D3D12_CULL_MODE CullMode; // 기본값 : D3D12_CULL_BACK BOOL FrontCounterClockwise; // 기본값 : false INT DepthBias; // 기본값 : 0 FLOAT DepthBiasClamp; // 기본값 : 0.0f FLOAT SlopeScaledDepthBias; // 기본값 : 0.0f BOOL DepthClipEnable; // 기본값 : true BOOL ScissorEnable; // 기본값 : false BOOL MultisampleEnable; // 기본값 : false BOOL AntialiasedLineEnable; // 기본값 : false UINT ForcedSampleCount; // 기본값 : 0 }
FillMode
: 와이어프레임 렌더링을 위해서는D3D12_FILL_WIREFRAME
을, 면의 속을 채운 (solid) 렌더링을 위해서는D3D12_FILL_SOLID
지정CullMode
: 선별을 끄려면D3D12_CULL_NONE
, 후면 삼각형들을 선별하려면D3D12_CULL_BACK
, 전면 선별은D3D12_CULL_FRONT
FrontCounterClockwise
: 정점들이 시계방향(카메라 기준)으로 감긴 삼각형을 전면 삼각형으로 취급하려면true
, 반시계방향(카메라 기준)으로 감긴 삼각형을 후면 삼각형으로 취급하려면false
ScissorEnable
: 가위 판정 활성화는true
6.9 파이프라인 상태 객체
파이프라인 상태 객체(pipeline state object, PSO) :
- 렌더링 파이프라인의 상태를 제어하는 집합체
- 그래픽스 파이프라인의 다양한 상태(예: 셰이더, 블렌드 상태, 래스터라이저 상태, 깊이 스텐실 상태 등)를 하나의 객체로 묶음
- PSO는 생성 후 변경할 수 없는 불변 객체;
따라서 렌더링 중에 빠르게 상태를 전환할 수 있음 - 기존 D11에서 개별로 관리하던 파이프라인 상태를 D12에서 PSO로 통합관리
- PSO는 검증과 생성에 많은 시간이 걸릴 수 있으므로 초기화 시점에서 생성
- 다른 PSO로 교체할 떄를 대비해, 미리 PSO를 만들어 두고 사용
대표 구조체 :
ID3D12PipelineState
typedef struct D3D12_GRAPHICS_PIPELINE_STATE_DESC { ID3D12RootSignature* pRootSignature; D3D12_SHADER_BYTECODE VS; D3D12_SHADER_BYTECODE PS; D3D12_SHADER_BYTECODE DS; D3D12_SHADER_BYTECODE HS; D3D12_SHADER_BYTECODE GS; D3D12_STREAM_OUTPUT_DESC StreamOutput; D3D12_BLEND_DESC BlendState; UINT SampleMask; D3D12_RASTERIZER_DESC RasterizerState; D3D12_DEPTH_STENCIL_DESC DepthStencilState; D3D12_INPUT_LAYOUT_DESC InputLayout; D3D12_PRIMITIVE_TOPOLOGY_TYPE PrimitiveTopologyType; UINT NumRenderTargets; DXGI_FORMAT RTVFormats[8]; DXGI_FORMAT DSVFormat; DXGI_SAMPLE_DESC SampleDesc; } D3D12_GRAPHICS_PIPELINE_STATE_DESC;
pRootSignature
:PSO(Pipeline State Object)와 함꼐 묶을 루트 서명을 가리키는 포인터, 루트 서명은 반드시 PSO로 묶는 셰이더들과 호환되어야 함
D3D12_SHADER_BYTE_CODE
: 셰이더 컴파일 바이트 코드; (VS, PS, DS, HS, GS)typedef struct D3D12_SHADER_BYTE_CODE { const BYTE *pShaderBytecode; SIZE_T BytecodeLength; } D3D12_SHADER_BYTECODE;
StreamOutput
:- 출력 정의, 주로 데이터 캡처 or 변환 작업에 사용
State & Layout
:- 각각의 상태 정의
SampleMask
:- 다중표본화 샘플 개수(최대 32개까지 가능);
- SampleDesc의 Count보다 큰 비트는 무시됨
- 기본 값
0xffffffff(모든 표본 활성)
NumRenderTargets
:- 동시에 사용할 렌더 대상 개수
RTVFormats
:- 렌더 타겟 포맷
- PSO와 함께 사용할 렌더 타겟의 설정들과 부합해야 함
DSVFormats
:- 깊이 스텐실 버퍼 포맷
- PSO와 함께 사용할 깊이 스텐실 버퍼의 설정들과 부합해야 함
SampleDesc
:- 멀티 샘플링 샘플 개수 및 품질 수준 서술
- PSO와 함꼐 사용할 렌더 타겟의 설정들과 부합해야 함
생성 예제
D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc; ZeroMemory(&psoDesc, sizeof(D3D12_GRAPHICS_PIPELINE_STATE_DESC)); psoDesc.InputLayout = { mInputLayout.data(), (UINT)mInputLayout.size() }; psoDesc.pRootSignature = mRootSignature.Get(); psoDesc.VS = { reinterpret_cast<BYTE*>(mvsByteCode->GetBufferPointer()), mvsByteCode->GetBufferSize() }; psoDesc.PS = { reinterpret_cast<BYTE*>(mpsByteCode->GetBufferPointer()), mpsByteCode->GetBufferSize() }; psoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT); psoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT); psoDesc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT); psoDesc.SampleMask = UINT_MAX; psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; psoDesc.NumRenderTargets = 1; psoDesc.RTVFormats[0] = mBackBufferFormat; psoDesc.SampleDesc.Count = m4xMsaaState ? 4 : 1; psoDesc.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0; psoDesc.DSVFormat = mDepthStencilFormat; ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&mPSO)));
PSO 교체
- 성능을 위해 상태 변경을 최소화
- 같은 PSO 사용 가능 물체는 함께 그리기
PSO 교체 예제
// Reset을 호출해서 초기 PSO 지정 mCommandList->Reset(mDirectCmdListAlloc.Get(), mPSO1.Get()); /* PSO1 물체 */ // PSO 변경 mCommandList->SetPipelineState(mPSO2.Get());
번외
✅ 구성만 가능한 고정 기능(Fixed-function) 단계들(shader가 아닌 단계들)
단계 | 구성만 가능? | 설명 |
---|---|---|
Input Assembler (IA) | ✅ 예 | 정점 버퍼, 인덱스 버퍼, Input Layout 등 구성 |
Rasterizer Stage | ✅ 예 | 뷰포트, 스카이솟, 카울링, 와이어프레임 설정 등 |
Output Merger (OM) | ✅ 예 | 렌더 타겟, 깊이-스텐실 버퍼 설정, 블렌딩 옵션 등 |
Stream Output (SO) | ✅ 예 | 정점 쉐이더 결과를 GPU 메모리로 출력할 수 있도록 설정만 함 |
Multisample Stage (MSAA) | ✅ 예 | 샘플링 품질 및 해상도 등 구성만 가능 |
상수 버퍼의 업로드 힙, 디폴트 힙 사용 비교
힙 타입 | CPU 접근 가능 여부 | GPU 성능 | 사용 예시 |
---|---|---|---|
업로드 힙 (D3D12_HEAP_TYPE_UPLOAD ) |
✅ 가능 | 🚀 중간 | 프레임마다 갱신되는 상수 버퍼 (예: 카메라 변환 행렬, 조명 데이터) |
디폴트 힙 (D3D12_HEAP_TYPE_DEFAULT ) |
❌ 불가능 | ⚡ 빠름 | 변경되지 않는 정적 데이터 (예: 모델 정점 데이터, 고정된 조명 설정) |
'프레임워크 > DirectX' 카테고리의 다른 글
그리기_연산02 (2) | 2025.04.14 |
---|---|
[이슈] 정점구조체의 가상소멸자로 인한 size 변경 (0) | 2025.04.08 |
06.그리기_연산01_1 (0) | 2025.04.05 |
06.그리기_연산01 (0) | 2025.03.25 |
05. 렌더링파이프 개념문제 개인제작 (1) | 2025.03.15 |