完成步骤2:读取 NanoVDB 密度并渲染体积云 修改内容: 1. NanoVDBLoader: 添加 voxel_size 输出用于调试 2. volume.hlsl: 实现完整的 ray marching 体积渲染 - 初始化 NanoVDB 访问器 - 密度采样函数 - 光线步进和累积 3. wireframe.hlsl: 新增边界框线框着色器 4. main.cpp: - 移动相机到云的外部 - 添加独立的常量缓冲区避免冲突 - 同时渲染体积云和边界框 已知问题: - 边界框位置计算需要改进 - bunny.nvdb 渲染效果不佳 - 需要进一步优化密度和步进参数
356 lines
15 KiB
C++
356 lines
15 KiB
C++
#include <windows.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include "BattleFireDirect.h"
|
|
#include "StaticMeshComponent.h"
|
|
#include "stbi/stb_image.h"
|
|
#include "Utils.h"
|
|
#include "NanoVDBLoader.h"
|
|
|
|
#pragma comment(lib,"d3d12.lib")
|
|
#pragma comment(lib,"dxgi.lib")
|
|
#pragma comment(lib,"d3dcompiler.lib")
|
|
#pragma comment(lib,"winmm.lib")
|
|
|
|
LPCWSTR gWindowClassName = L"BattleFire";
|
|
|
|
LRESULT CALLBACK WindowProc(HWND inHWND, UINT inMSG, WPARAM inWParam, LPARAM inLParam) {
|
|
switch (inMSG) {
|
|
case WM_CLOSE:
|
|
PostQuitMessage(0);
|
|
break;
|
|
}
|
|
return DefWindowProc(inHWND, inMSG, inWParam, inLParam);
|
|
}
|
|
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int inShowCmd) {
|
|
AttachConsole(ATTACH_PARENT_PROCESS);
|
|
freopen("CONOUT$", "w", stdout);
|
|
freopen("CONOUT$", "w", stderr);
|
|
printf("XCVolumeRenderer started\n");
|
|
printf("Initializing D3D12...\n");
|
|
//register
|
|
WNDCLASSEX wndClassEx;
|
|
wndClassEx.cbSize = sizeof(WNDCLASSEX);
|
|
wndClassEx.style = CS_HREDRAW | CS_VREDRAW;
|
|
wndClassEx.cbClsExtra = NULL;//class
|
|
wndClassEx.cbWndExtra = NULL;//instance
|
|
wndClassEx.hInstance = hInstance;
|
|
wndClassEx.hIcon = LoadIcon(NULL, IDI_APPLICATION);
|
|
wndClassEx.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
|
|
wndClassEx.hCursor = LoadCursor(NULL, IDC_ARROW);
|
|
wndClassEx.hbrBackground = NULL;
|
|
wndClassEx.lpszMenuName = NULL;
|
|
wndClassEx.lpszClassName = gWindowClassName;
|
|
wndClassEx.lpfnWndProc = WindowProc;
|
|
if (!RegisterClassEx(&wndClassEx)) {
|
|
MessageBox(NULL, L"Register Class Failed!", L"Error", MB_OK | MB_ICONERROR);
|
|
return -1;
|
|
}
|
|
//create
|
|
int viewportWidth = 1280;
|
|
int viewportHeight = 720;
|
|
RECT rect;
|
|
rect.left = 0;
|
|
rect.top = 0;
|
|
rect.right = viewportWidth;
|
|
rect.bottom = viewportHeight;
|
|
AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, FALSE);
|
|
int windowWidth = rect.right - rect.left;
|
|
int windowHeight = rect.bottom - rect.top;
|
|
HWND hwnd = CreateWindowEx(NULL,
|
|
gWindowClassName,
|
|
L"My Render Window",
|
|
WS_OVERLAPPEDWINDOW,
|
|
CW_USEDEFAULT, CW_USEDEFAULT,
|
|
windowWidth, windowHeight,
|
|
NULL,
|
|
NULL,
|
|
hInstance,
|
|
NULL);
|
|
if (!hwnd) {
|
|
MessageBox(NULL, L"Create Window Failed!", L"Error", MB_OK | MB_ICONERROR);
|
|
return -1;
|
|
}
|
|
//show
|
|
InitD3D12(hwnd, 1280, 720);
|
|
printf("D3D12 initialized\n");
|
|
ID3D12GraphicsCommandList* commandList = GetCommandList();
|
|
ID3D12CommandAllocator* commandAllocator = GetCommandAllocator();
|
|
|
|
NanoVDBData nanoVDBData = {};
|
|
LoadNanoVDB("Res/NanoVDB/cloud.nvdb", nanoVDBData, commandList, commandAllocator);
|
|
printf("NanoVDB loaded: %llu bytes\n", (unsigned long long)nanoVDBData.byteSize);
|
|
printf("NanoVDB BBox: [%.2f, %.2f, %.2f] - [%.2f, %.2f, %.2f]\n",
|
|
nanoVDBData.worldBBox[0], nanoVDBData.worldBBox[1], nanoVDBData.worldBBox[2],
|
|
nanoVDBData.worldBBox[3], nanoVDBData.worldBBox[4], nanoVDBData.worldBBox[5]);
|
|
|
|
float bboxMin[3] = { (float)nanoVDBData.worldBBox[0], (float)nanoVDBData.worldBBox[1], (float)nanoVDBData.worldBBox[2] };
|
|
float bboxMax[3] = { (float)nanoVDBData.worldBBox[3], (float)nanoVDBData.worldBBox[4], (float)nanoVDBData.worldBBox[5] };
|
|
float bboxCenter[3] = { (bboxMin[0] + bboxMax[0]) / 2, (bboxMin[1] + bboxMax[1]) / 2, (bboxMin[2] + bboxMax[2]) / 2 };
|
|
float bboxSize[3] = { bboxMax[0] - bboxMin[0], bboxMax[1] - bboxMin[1], bboxMax[2] - bboxMin[2] };
|
|
printf("BBox center: [%.2f, %.2f, %.2f], size: [%.2f, %.2f, %.2f]\n",
|
|
bboxCenter[0], bboxCenter[1], bboxCenter[2],
|
|
bboxSize[0], bboxSize[1], bboxSize[2]);
|
|
|
|
float cubeVertices[] = {
|
|
-0.5f, -0.5f, -0.5f,
|
|
0.5f, -0.5f, -0.5f,
|
|
0.5f, 0.5f, -0.5f,
|
|
-0.5f, 0.5f, -0.5f,
|
|
-0.5f, -0.5f, 0.5f,
|
|
0.5f, -0.5f, 0.5f,
|
|
0.5f, 0.5f, 0.5f,
|
|
-0.5f, 0.5f, 0.5f,
|
|
};
|
|
uint32_t cubeIndices[] = {
|
|
0, 1, 1, 2, 2, 3, 3, 0,
|
|
4, 5, 5, 6, 6, 7, 7, 4,
|
|
0, 4, 1, 5, 2, 6, 3, 7
|
|
};
|
|
ID3D12Resource* cubeVBO = CreateBufferObject(commandList, cubeVertices, sizeof(cubeVertices), D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER);
|
|
ID3D12Resource* cubeIBO = CreateBufferObject(commandList, cubeIndices, sizeof(cubeIndices), D3D12_RESOURCE_STATE_INDEX_BUFFER);
|
|
printf("Cube mesh created\n");
|
|
|
|
float quadVertices[] = {
|
|
-1.0f, -1.0f, 0.0f, 0.0f,
|
|
1.0f, -1.0f, 1.0f, 0.0f,
|
|
1.0f, 1.0f, 1.0f, 1.0f,
|
|
-1.0f, 1.0f, 0.0f, 1.0f,
|
|
};
|
|
uint32_t quadIndices[] = { 0, 1, 2, 0, 2, 3 };
|
|
ID3D12Resource* quadVBO = CreateBufferObject(commandList, quadVertices, sizeof(quadVertices), D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER);
|
|
ID3D12Resource* quadIBO = CreateBufferObject(commandList, quadIndices, sizeof(quadIndices), D3D12_RESOURCE_STATE_INDEX_BUFFER);
|
|
printf("Quad mesh created\n");
|
|
|
|
StaticMeshComponent staticMeshComponent;
|
|
staticMeshComponent.InitFromFile(commandList, "Res/Model/Sphere.lhsm");
|
|
printf("Mesh loaded\n");
|
|
|
|
ID3D12RootSignature* rootSignature = InitRootSignature();
|
|
printf("Root signature created\n");
|
|
D3D12_SHADER_BYTECODE vs,gs,ps;
|
|
CreateShaderFromFile(L"Res/Shader/gs.hlsl", "MainVS", "vs_5_1", &vs);
|
|
CreateShaderFromFile(L"Res/Shader/gs.hlsl", "MainGS", "gs_5_1", &gs);
|
|
CreateShaderFromFile(L"Res/Shader/gs.hlsl", "MainPS", "ps_5_1", &ps);
|
|
ID3D12PipelineState*pso=CreatePSO(rootSignature, vs, ps, gs);
|
|
printf("PSO created\n");
|
|
|
|
ID3D12RootSignature* volumeRootSignature = InitVolumeRootSignature();
|
|
printf("Volume root signature created\n");
|
|
|
|
D3D12_SHADER_BYTECODE volumeVS, volumePS;
|
|
memset(&volumeVS, 0, sizeof(volumeVS));
|
|
memset(&volumePS, 0, sizeof(volumePS));
|
|
CreateShaderFromFile(L"Res/Shader/volume.hlsl", "MainVS", "vs_5_1", &volumeVS);
|
|
CreateShaderFromFile(L"Res/Shader/volume.hlsl", "MainPS", "ps_5_1", &volumePS);
|
|
printf("Volume VS: ptr=%p, size=%zu\n", volumeVS.pShaderBytecode, volumeVS.BytecodeLength);
|
|
printf("Volume PS: ptr=%p, size=%zu\n", volumePS.pShaderBytecode, volumePS.BytecodeLength);
|
|
|
|
ID3D12PipelineState* quadPSO = CreateQuadPSO(volumeRootSignature, volumeVS, volumePS);
|
|
if (!quadPSO) {
|
|
printf("Quad PSO creation failed!\n");
|
|
} else {
|
|
printf("Quad PSO created: %p\n", quadPSO);
|
|
}
|
|
|
|
D3D12_SHADER_BYTECODE wireframeVS, wireframePS;
|
|
memset(&wireframeVS, 0, sizeof(wireframeVS));
|
|
memset(&wireframePS, 0, sizeof(wireframePS));
|
|
CreateShaderFromFile(L"Res/Shader/wireframe.hlsl", "MainVS", "vs_5_1", &wireframeVS);
|
|
CreateShaderFromFile(L"Res/Shader/wireframe.hlsl", "MainPS", "ps_5_1", &wireframePS);
|
|
printf("Wireframe VS: ptr=%p, size=%zu\n", wireframeVS.pShaderBytecode, wireframeVS.BytecodeLength);
|
|
printf("Wireframe PS: ptr=%p, size=%zu\n", wireframePS.pShaderBytecode, wireframePS.BytecodeLength);
|
|
|
|
ID3D12PipelineState* volumePSO = CreateVolumePSO(volumeRootSignature, wireframeVS, wireframePS);
|
|
if (!volumePSO) {
|
|
printf("Volume PSO creation failed!\n");
|
|
} else {
|
|
printf("Volume PSO created: %p\n", volumePSO);
|
|
}
|
|
|
|
ID3D12Resource* volumeCB = CreateConstantBufferObject(65536);
|
|
ID3D12Resource* wireframeCB = CreateConstantBufferObject(65536);
|
|
|
|
ID3D12Resource* cb = CreateConstantBufferObject(65536);//1024x64(4x4)
|
|
DirectX::XMMATRIX projectionMatrix=DirectX::XMMatrixPerspectiveFovLH(
|
|
(45.0f*3.141592f)/180.0f,1280.0f/720.0f,0.1f,1000.0f);
|
|
DirectX::XMMATRIX viewMatrix = DirectX::XMMatrixLookAtLH(
|
|
DirectX::XMVectorSet(-10.0f, 73.0f, -600.0f, 1.0f),
|
|
DirectX::XMVectorSet(-10.0f, 73.0f, 0.0f, 1.0f),
|
|
DirectX::XMVectorSet(0.0f, 1.0f, 0.0f, 1.0f));
|
|
DirectX::XMMATRIX modelMatrix = DirectX::XMMatrixTranslation(0.0f,0.0f,5.0f);
|
|
//modelMatrix *= DirectX::XMMatrixRotationZ(90.0f*3.141592f/180.0f);
|
|
DirectX::XMFLOAT4X4 tempMatrix;
|
|
float matrices[64];
|
|
|
|
DirectX::XMStoreFloat4x4(&tempMatrix, projectionMatrix);
|
|
memcpy(matrices, &tempMatrix, sizeof(float) * 16);
|
|
DirectX::XMStoreFloat4x4(&tempMatrix, viewMatrix);
|
|
memcpy(matrices+16, &tempMatrix, sizeof(float) * 16);
|
|
DirectX::XMStoreFloat4x4(&tempMatrix, modelMatrix);
|
|
memcpy(matrices + 32, &tempMatrix, sizeof(float) * 16);;
|
|
DirectX::XMVECTOR determinant;
|
|
DirectX::XMMATRIX inverseModelMatrix = DirectX::XMMatrixInverse(&determinant, modelMatrix);
|
|
if (DirectX::XMVectorGetX(determinant) != 0.0f) {
|
|
DirectX::XMMATRIX normalMatrix = DirectX::XMMatrixTranspose(inverseModelMatrix);
|
|
DirectX::XMStoreFloat4x4(&tempMatrix, modelMatrix);
|
|
memcpy(matrices + 48, &tempMatrix, sizeof(float) * 16);;
|
|
}
|
|
UpdateConstantBuffer(cb, matrices, sizeof(float) * 64);
|
|
|
|
|
|
ID3D12Resource* sb = CreateConstantBufferObject(65536);//1024x64(4x4)
|
|
struct MaterialData {
|
|
float r;
|
|
};
|
|
MaterialData* materialDatas = new MaterialData[3000];
|
|
for (int i=0;i<3000;i++){
|
|
materialDatas[i].r = srandom() * 0.1f + 0.1f;//0.0~1.0
|
|
}
|
|
UpdateConstantBuffer(sb, materialDatas, sizeof(MaterialData) * 3000);
|
|
|
|
int imageWidth, imageHeight,imageChannel;
|
|
stbi_uc* pixels = stbi_load("Res/Image/earth_d.jpg", &imageWidth, &imageHeight, &imageChannel, 4);
|
|
printf("Texture loaded: %dx%d\n", imageWidth, imageHeight);
|
|
ID3D12Resource* texture = CreateTexture2D(commandList, pixels,
|
|
imageWidth * imageHeight * imageChannel, imageWidth, imageHeight,DXGI_FORMAT_R8G8B8A8_UNORM);
|
|
delete[]pixels;
|
|
ID3D12Device* d3dDevice = GetD3DDevice();
|
|
|
|
ID3D12DescriptorHeap* srvHeap = nullptr;
|
|
D3D12_DESCRIPTOR_HEAP_DESC d3dDescriptorHeapDescSRV = {};
|
|
d3dDescriptorHeapDescSRV.NumDescriptors = 3;
|
|
d3dDescriptorHeapDescSRV.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
|
|
d3dDescriptorHeapDescSRV.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
|
|
d3dDevice->CreateDescriptorHeap(&d3dDescriptorHeapDescSRV, IID_PPV_ARGS(&srvHeap));
|
|
|
|
ID3D12DescriptorHeap* descriptorHeaps[] = {srvHeap};
|
|
|
|
D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
|
|
srvDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
|
|
srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
|
|
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
|
|
srvDesc.Texture2D.MipLevels = 1;
|
|
|
|
D3D12_CPU_DESCRIPTOR_HANDLE srvHeapPtr = srvHeap->GetCPUDescriptorHandleForHeapStart();
|
|
d3dDevice->CreateShaderResourceView(texture, &srvDesc, srvHeapPtr);
|
|
srvHeapPtr.ptr += d3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
|
|
|
|
EndCommandList();
|
|
WaitForCompletionOfCommandList();
|
|
printf("All resources uploaded to GPU\n");
|
|
|
|
ShowWindow(hwnd, inShowCmd);
|
|
UpdateWindow(hwnd);
|
|
printf("Window shown, entering render loop\n");
|
|
float color[] = {0.5f,0.5f,0.5f,1.0f};
|
|
MSG msg;
|
|
DWORD last_time = timeGetTime();
|
|
DWORD appStartTime = last_time;
|
|
int frameCount = 0;
|
|
while (true){
|
|
ZeroMemory(&msg, sizeof(MSG));
|
|
if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) {
|
|
if (msg.message == WM_QUIT) {
|
|
break;
|
|
}
|
|
TranslateMessage(&msg);
|
|
DispatchMessage(&msg);
|
|
} else {
|
|
//rendering
|
|
WaitForCompletionOfCommandList();
|
|
DWORD current_time = timeGetTime();//ms
|
|
DWORD frameTime = current_time - last_time;
|
|
DWORD timeSinceAppStartInMS = current_time - appStartTime;
|
|
last_time = current_time;
|
|
float frameTimeInSecond = float(frameTime) / 1000.0f;//second
|
|
float timeSinceAppStartInSecond = float(timeSinceAppStartInMS) / 1000.0f;
|
|
color[0] = timeSinceAppStartInSecond;
|
|
commandAllocator->Reset();
|
|
commandList->Reset(commandAllocator, nullptr);
|
|
BeginRenderToSwapChain(commandList);
|
|
//draw
|
|
commandList->SetPipelineState(pso);
|
|
commandList->SetGraphicsRootSignature(rootSignature);
|
|
commandList->SetDescriptorHeaps(_countof(descriptorHeaps),descriptorHeaps);
|
|
commandList->SetGraphicsRootConstantBufferView(0, cb->GetGPUVirtualAddress());
|
|
commandList->SetGraphicsRoot32BitConstants(1, 4, color, 0);
|
|
commandList->SetGraphicsRootDescriptorTable(2, srvHeap->GetGPUDescriptorHandleForHeapStart());
|
|
commandList->SetGraphicsRootShaderResourceView(3, sb->GetGPUVirtualAddress());
|
|
commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
|
|
if (frameCount == 1) {
|
|
printf("Drawing mesh with %d vertices, VBO=%p, subMeshes=%zu\n",
|
|
staticMeshComponent.mVertexCount, staticMeshComponent.mVBO, staticMeshComponent.mSubMeshes.size());
|
|
}
|
|
staticMeshComponent.Render(commandList);
|
|
|
|
DirectX::XMMATRIX viewProj = viewMatrix * projectionMatrix;
|
|
DirectX::XMMATRIX invViewProj = DirectX::XMMatrixInverse(nullptr, viewProj);
|
|
DirectX::XMFLOAT4X4 invViewProjMat;
|
|
DirectX::XMStoreFloat4x4(&invViewProjMat, invViewProj);
|
|
|
|
float volumeCBData[32];
|
|
memcpy(volumeCBData, &invViewProjMat, sizeof(float) * 16);
|
|
volumeCBData[16] = -10.0f;
|
|
volumeCBData[17] = 73.0f;
|
|
volumeCBData[18] = -600.0f;
|
|
volumeCBData[19] = 0.01f; // DensityScale
|
|
volumeCBData[20] = (float)nanoVDBData.worldBBox[0];
|
|
volumeCBData[21] = (float)nanoVDBData.worldBBox[1];
|
|
volumeCBData[22] = (float)nanoVDBData.worldBBox[2];
|
|
volumeCBData[23] = 0.5f; // StepSize
|
|
volumeCBData[24] = (float)nanoVDBData.worldBBox[3];
|
|
volumeCBData[25] = (float)nanoVDBData.worldBBox[4];
|
|
volumeCBData[26] = (float)nanoVDBData.worldBBox[5];
|
|
volumeCBData[27] = 128;
|
|
UpdateConstantBuffer(volumeCB, volumeCBData, sizeof(float) * 28);
|
|
if (frameCount == 1) {
|
|
printf("Volume BBox: [%.2f, %.2f, %.2f] - [%.2f, %.2f, %.2f]\n",
|
|
volumeCBData[20], volumeCBData[21], volumeCBData[22],
|
|
volumeCBData[24], volumeCBData[25], volumeCBData[26]);
|
|
}
|
|
|
|
if (quadPSO) {
|
|
commandList->SetPipelineState(quadPSO);
|
|
commandList->SetGraphicsRootSignature(volumeRootSignature);
|
|
commandList->SetGraphicsRootConstantBufferView(0, volumeCB->GetGPUVirtualAddress());
|
|
commandList->SetGraphicsRootShaderResourceView(1, nanoVDBData.gpuBuffer->GetGPUVirtualAddress());
|
|
commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
|
|
commandList->IASetVertexBuffers(0, 1, &D3D12_VERTEX_BUFFER_VIEW{ quadVBO->GetGPUVirtualAddress(), sizeof(quadVertices), sizeof(float) * 4 });
|
|
commandList->IASetIndexBuffer(&D3D12_INDEX_BUFFER_VIEW{ quadIBO->GetGPUVirtualAddress(), sizeof(quadIndices), DXGI_FORMAT_R32_UINT });
|
|
commandList->DrawIndexedInstanced(6, 1, 0, 0, 0);
|
|
}
|
|
|
|
DirectX::XMMATRIX bboxScale = DirectX::XMMatrixScaling(bboxSize[0], bboxSize[1], bboxSize[2]);
|
|
DirectX::XMMATRIX bboxTranslate = DirectX::XMMatrixTranslation(bboxCenter[0], bboxCenter[1], bboxCenter[2]);
|
|
DirectX::XMMATRIX bboxModel = bboxScale * bboxTranslate;
|
|
DirectX::XMMATRIX bboxViewProj = bboxModel * viewMatrix * projectionMatrix;
|
|
DirectX::XMFLOAT4X4 bboxViewProjMat;
|
|
DirectX::XMStoreFloat4x4(&bboxViewProjMat, bboxViewProj);
|
|
|
|
float wireframeCBData[16];
|
|
memcpy(wireframeCBData, &bboxViewProjMat, sizeof(float) * 16);
|
|
UpdateConstantBuffer(wireframeCB, wireframeCBData, sizeof(float) * 16);
|
|
|
|
if (volumePSO) {
|
|
commandList->SetPipelineState(volumePSO);
|
|
commandList->SetGraphicsRootSignature(volumeRootSignature);
|
|
commandList->SetGraphicsRootConstantBufferView(0, wireframeCB->GetGPUVirtualAddress());
|
|
commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_LINELIST);
|
|
commandList->IASetVertexBuffers(0, 1, &D3D12_VERTEX_BUFFER_VIEW{ cubeVBO->GetGPUVirtualAddress(), 8 * 3 * sizeof(float), sizeof(float) * 3 });
|
|
commandList->IASetIndexBuffer(&D3D12_INDEX_BUFFER_VIEW{ cubeIBO->GetGPUVirtualAddress(), 24 * sizeof(uint32_t), DXGI_FORMAT_R32_UINT });
|
|
commandList->DrawIndexedInstanced(24, 1, 0, 0, 0);
|
|
}
|
|
|
|
EndRenderToSwapChain(commandList);
|
|
EndCommandList();
|
|
SwapD3D12Buffers();
|
|
frameCount++;
|
|
if (frameCount <= 3) {
|
|
printf("Frame %d rendered\n", frameCount);
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
} |