diff --git a/DX11Starter.sln b/DX11Starter.sln new file mode 100644 index 0000000..d89bc90 --- /dev/null +++ b/DX11Starter.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29209.62 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DX11Starter", "DX11Starter.vcxproj", "{7B07137C-8E03-4F0C-BEDA-4C9915CD667C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7B07137C-8E03-4F0C-BEDA-4C9915CD667C}.Debug|x64.ActiveCfg = Debug|x64 + {7B07137C-8E03-4F0C-BEDA-4C9915CD667C}.Debug|x64.Build.0 = Debug|x64 + {7B07137C-8E03-4F0C-BEDA-4C9915CD667C}.Debug|x86.ActiveCfg = Debug|Win32 + {7B07137C-8E03-4F0C-BEDA-4C9915CD667C}.Debug|x86.Build.0 = Debug|Win32 + {7B07137C-8E03-4F0C-BEDA-4C9915CD667C}.Release|x64.ActiveCfg = Release|x64 + {7B07137C-8E03-4F0C-BEDA-4C9915CD667C}.Release|x64.Build.0 = Release|x64 + {7B07137C-8E03-4F0C-BEDA-4C9915CD667C}.Release|x86.ActiveCfg = Release|Win32 + {7B07137C-8E03-4F0C-BEDA-4C9915CD667C}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0783F28E-8634-436C-B05A-98F92CB21105} + EndGlobalSection +EndGlobal diff --git a/DX11Starter.vcxproj b/DX11Starter.vcxproj new file mode 100644 index 0000000..0e1ebf5 --- /dev/null +++ b/DX11Starter.vcxproj @@ -0,0 +1,160 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 16.0 + {7B07137C-8E03-4F0C-BEDA-4C9915CD667C} + DX11Starter + 10.0 + + + + Application + true + v142 + MultiByte + + + Application + false + v142 + true + MultiByte + + + Application + true + v142 + MultiByte + + + Application + false + v142 + true + MultiByte + + + + + + + + + + + + + + + + + + + + + + + Level3 + Disabled + true + true + + + Windows + + + + + Level3 + Disabled + true + true + + + Windows + + + + + Level3 + MaxSpeed + true + true + true + true + + + Windows + true + true + + + + + Level3 + MaxSpeed + true + true + true + true + + + Windows + true + true + + + + + + + + + + + + + + + Pixel + 5.0 + Pixel + 5.0 + Pixel + 5.0 + Pixel + 5.0 + + + Vertex + 5.0 + Vertex + 5.0 + Vertex + 5.0 + Vertex + 5.0 + + + + + + \ No newline at end of file diff --git a/DX11Starter.vcxproj.filters b/DX11Starter.vcxproj.filters new file mode 100644 index 0000000..4f0fa0b --- /dev/null +++ b/DX11Starter.vcxproj.filters @@ -0,0 +1,50 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + {e76f01f5-08db-40d4-8720-b5b21f7ec0a3} + + + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + + + Shaders + + + Shaders + + + \ No newline at end of file diff --git a/DXCore.cpp b/DXCore.cpp new file mode 100644 index 0000000..e7553a9 --- /dev/null +++ b/DXCore.cpp @@ -0,0 +1,673 @@ +#include "DXCore.h" + +#include +#include + +// Define the static instance variable so our OS-level +// message handling function below can talk to our object +DXCore* DXCore::DXCoreInstance = 0; + +// -------------------------------------------------------- +// The global callback function for handling windows OS-level messages. +// +// This needs to be a global function (not part of a class), but we want +// to forward the parameters to our class to properly handle them. +// -------------------------------------------------------- +LRESULT DXCore::WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + return DXCoreInstance->ProcessMessage(hWnd, uMsg, wParam, lParam); +} + +// -------------------------------------------------------- +// Constructor - Set up fields and timer +// +// hInstance - The application's OS-level handle (unique ID) +// titleBarText - Text for the window's title bar +// windowWidth - Width of the window's client (internal) area +// windowHeight - Height of the window's client (internal) area +// debugTitleBarStats - Show debug stats in the title bar, like FPS? +// -------------------------------------------------------- +DXCore::DXCore( + HINSTANCE hInstance, // The application's handle + const char* titleBarText, // Text for the window's title bar + unsigned int windowWidth, // Width of the window's client area + unsigned int windowHeight, // Height of the window's client area + bool debugTitleBarStats) // Show extra stats (fps) in title bar? +{ + // Save a static reference to this object. + // - Since the OS-level message function must be a non-member (global) function, + // it won't be able to directly interact with our DXCore object otherwise. + // - (Yes, a singleton might be a safer choice here). + DXCoreInstance = this; + + // Save params + this->hInstance = hInstance; + this->titleBarText = titleBarText; + this->width = windowWidth; + this->height = windowHeight; + this->titleBarStats = debugTitleBarStats; + + // Initialize fields + this->hasFocus = true; + + this->fpsFrameCount = 0; + this->fpsTimeElapsed = 0.0f; + this->currentTime = 0; + this->deltaTime = 0; + this->startTime = 0; + this->totalTime = 0; + + // Query performance counter for accurate timing information + __int64 perfFreq; + QueryPerformanceFrequency((LARGE_INTEGER*)&perfFreq); + perfCounterSeconds = 1.0 / (double)perfFreq; +} + +// -------------------------------------------------------- +// Destructor - Clean up (release) all DirectX references +// -------------------------------------------------------- +DXCore::~DXCore() +{ + // Note: Since we're using smart pointers (ComPtr), + // we don't need to explicitly clean up those DirectX objects + // - If we weren't using smart pointers, we'd need + // to call Release() on each DirectX object created in DXCore +} + +// -------------------------------------------------------- +// Created the actual window for our application +// -------------------------------------------------------- +HRESULT DXCore::InitWindow() +{ + // Start window creation by filling out the + // appropriate window class struct + WNDCLASS wndClass = {}; // Zero out the memory + wndClass.style = CS_HREDRAW | CS_VREDRAW; // Redraw on horizontal or vertical movement/adjustment + wndClass.lpfnWndProc = DXCore::WindowProc; + wndClass.cbClsExtra = 0; + wndClass.cbWndExtra = 0; + wndClass.hInstance = hInstance; // Our app's handle + wndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION); // Default icon + wndClass.hCursor = LoadCursor(NULL, IDC_ARROW); // Default arrow cursor + wndClass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); + wndClass.lpszMenuName = NULL; + wndClass.lpszClassName = "Direct3DWindowClass"; + + // Attempt to register the window class we've defined + if (!RegisterClass(&wndClass)) + { + // Get the most recent error + DWORD error = GetLastError(); + + // If the class exists, that's actually fine. Otherwise, + // we can't proceed with the next step. + if (error != ERROR_CLASS_ALREADY_EXISTS) + return HRESULT_FROM_WIN32(error); + } + + // Adjust the width and height so the "client size" matches + // the width and height given (the inner-area of the window) + RECT clientRect; + SetRect(&clientRect, 0, 0, width, height); + AdjustWindowRect( + &clientRect, + WS_OVERLAPPEDWINDOW, // Has a title bar, border, min and max buttons, etc. + false); // No menu bar + + // Center the window to the screen + RECT desktopRect; + GetClientRect(GetDesktopWindow(), &desktopRect); + int centeredX = (desktopRect.right / 2) - (clientRect.right / 2); + int centeredY = (desktopRect.bottom / 2) - (clientRect.bottom / 2); + + // Actually ask Windows to create the window itself + // using our settings so far. This will return the + // handle of the window, which we'll keep around for later + hWnd = CreateWindow( + wndClass.lpszClassName, + titleBarText.c_str(), + WS_OVERLAPPEDWINDOW, + centeredX, + centeredY, + clientRect.right - clientRect.left, // Calculated width + clientRect.bottom - clientRect.top, // Calculated height + 0, // No parent window + 0, // No menu + hInstance, // The app's handle + 0); // No other windows in our application + + // Ensure the window was created properly + if (hWnd == NULL) + { + DWORD error = GetLastError(); + return HRESULT_FROM_WIN32(error); + } + + // The window exists but is not visible yet + // We need to tell Windows to show it, and how to show it + ShowWindow(hWnd, SW_SHOW); + + // Return an "everything is ok" HRESULT value + return S_OK; +} + + +// -------------------------------------------------------- +// Initializes DirectX, which requires a window. This method +// also creates several DirectX objects we'll need to start +// drawing things to the screen. +// -------------------------------------------------------- +HRESULT DXCore::InitDirectX() +{ + // This will hold options for DirectX initialization + unsigned int deviceFlags = 0; + +#if defined(DEBUG) || defined(_DEBUG) + // If we're in debug mode in visual studio, we also + // want to make a "Debug DirectX Device" to see some + // errors and warnings in Visual Studio's output window + // when things go wrong! + deviceFlags |= D3D11_CREATE_DEVICE_DEBUG; +#endif + + // Create a description of how our swap + // chain should work + DXGI_SWAP_CHAIN_DESC swapDesc = {}; + swapDesc.BufferCount = 2; + swapDesc.BufferDesc.Width = width; + swapDesc.BufferDesc.Height = height; + swapDesc.BufferDesc.RefreshRate.Numerator = 60; + swapDesc.BufferDesc.RefreshRate.Denominator = 1; + swapDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + swapDesc.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED; + swapDesc.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED; + swapDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; + swapDesc.Flags = 0; + swapDesc.OutputWindow = hWnd; + swapDesc.SampleDesc.Count = 1; + swapDesc.SampleDesc.Quality = 0; + swapDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; + swapDesc.Windowed = true; + + // Result variable for below function calls + HRESULT hr = S_OK; + + // Attempt to initialize DirectX + hr = D3D11CreateDeviceAndSwapChain( + 0, // Video adapter (physical GPU) to use, or null for default + D3D_DRIVER_TYPE_HARDWARE, // We want to use the hardware (GPU) + 0, // Used when doing software rendering + deviceFlags, // Any special options + 0, // Optional array of possible verisons we want as fallbacks + 0, // The number of fallbacks in the above param + D3D11_SDK_VERSION, // Current version of the SDK + &swapDesc, // Address of swap chain options + swapChain.GetAddressOf(), // Pointer to our Swap Chain pointer + device.GetAddressOf(), // Pointer to our Device pointer + &dxFeatureLevel, // This will hold the actual feature level the app will use + context.GetAddressOf()); // Pointer to our Device Context pointer + if (FAILED(hr)) return hr; + + // The above function created the back buffer render target + // for us, but we need a reference to it + ID3D11Texture2D* backBufferTexture = 0; + swapChain->GetBuffer( + 0, + __uuidof(ID3D11Texture2D), + (void**)&backBufferTexture); + + // Now that we have the texture, create a render target view + // for the back buffer so we can render into it. Then release + // our local reference to the texture, since we have the view. + if (backBufferTexture != 0) + { + device->CreateRenderTargetView( + backBufferTexture, + 0, + backBufferRTV.GetAddressOf()); + backBufferTexture->Release(); + } + + // Set up the description of the texture to use for the depth buffer + D3D11_TEXTURE2D_DESC depthStencilDesc = {}; + depthStencilDesc.Width = width; + depthStencilDesc.Height = height; + depthStencilDesc.MipLevels = 1; + depthStencilDesc.ArraySize = 1; + depthStencilDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT; + depthStencilDesc.Usage = D3D11_USAGE_DEFAULT; + depthStencilDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL; + depthStencilDesc.CPUAccessFlags = 0; + depthStencilDesc.MiscFlags = 0; + depthStencilDesc.SampleDesc.Count = 1; + depthStencilDesc.SampleDesc.Quality = 0; + + // Create the depth buffer and its view, then + // release our reference to the texture + ID3D11Texture2D* depthBufferTexture = 0; + device->CreateTexture2D(&depthStencilDesc, 0, &depthBufferTexture); + if (depthBufferTexture != 0) + { + device->CreateDepthStencilView( + depthBufferTexture, + 0, + depthStencilView.GetAddressOf()); + depthBufferTexture->Release(); + } + + // Bind the views to the pipeline, so rendering properly + // uses their underlying textures + context->OMSetRenderTargets( + 1, + backBufferRTV.GetAddressOf(), + depthStencilView.Get()); + + // Lastly, set up a viewport so we render into + // to correct portion of the window + D3D11_VIEWPORT viewport = {}; + viewport.TopLeftX = 0; + viewport.TopLeftY = 0; + viewport.Width = (float)width; + viewport.Height = (float)height; + viewport.MinDepth = 0.0f; + viewport.MaxDepth = 1.0f; + context->RSSetViewports(1, &viewport); + + // Return the "everything is ok" HRESULT value + return S_OK; +} + +// -------------------------------------------------------- +// When the window is resized, the underlying +// buffers (textures) must also be resized to match. +// +// If we don't do this, the window size and our rendering +// resolution won't match up. This can result in odd +// stretching/skewing. +// -------------------------------------------------------- +void DXCore::OnResize() +{ + // Release the buffers before resizing the swap chain + backBufferRTV.Reset(); + depthStencilView.Reset(); + + // Resize the underlying swap chain buffers + swapChain->ResizeBuffers( + 2, + width, + height, + DXGI_FORMAT_R8G8B8A8_UNORM, + 0); + + // Recreate the render target view for the back buffer + // texture, then release our local texture reference + ID3D11Texture2D* backBufferTexture = 0; + swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast(&backBufferTexture)); + if (backBufferTexture != 0) + { + device->CreateRenderTargetView( + backBufferTexture, + 0, + backBufferRTV.ReleaseAndGetAddressOf()); // ReleaseAndGetAddressOf() cleans up the old object before giving us the pointer + backBufferTexture->Release(); + } + + // Set up the description of the texture to use for the depth buffer + D3D11_TEXTURE2D_DESC depthStencilDesc; + depthStencilDesc.Width = width; + depthStencilDesc.Height = height; + depthStencilDesc.MipLevels = 1; + depthStencilDesc.ArraySize = 1; + depthStencilDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT; + depthStencilDesc.Usage = D3D11_USAGE_DEFAULT; + depthStencilDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL; + depthStencilDesc.CPUAccessFlags = 0; + depthStencilDesc.MiscFlags = 0; + depthStencilDesc.SampleDesc.Count = 1; + depthStencilDesc.SampleDesc.Quality = 0; + + // Create the depth buffer and its view, then + // release our reference to the texture + ID3D11Texture2D* depthBufferTexture = 0; + device->CreateTexture2D(&depthStencilDesc, 0, &depthBufferTexture); + if (depthBufferTexture != 0) + { + device->CreateDepthStencilView( + depthBufferTexture, + 0, + depthStencilView.ReleaseAndGetAddressOf()); // ReleaseAndGetAddressOf() cleans up the old object before giving us the pointer + depthBufferTexture->Release(); + } + + // Bind the views to the pipeline, so rendering properly + // uses their underlying textures + context->OMSetRenderTargets( + 1, + backBufferRTV.GetAddressOf(), // This requires a pointer to a pointer (an array of pointers), so we get the address of the pointer + depthStencilView.Get()); + + // Lastly, set up a viewport so we render into + // to correct portion of the window + D3D11_VIEWPORT viewport = {}; + viewport.TopLeftX = 0; + viewport.TopLeftY = 0; + viewport.Width = (float)width; + viewport.Height = (float)height; + viewport.MinDepth = 0.0f; + viewport.MaxDepth = 1.0f; + context->RSSetViewports(1, &viewport); +} + + +// -------------------------------------------------------- +// This is the main game loop, handling the following: +// - OS-level messages coming in from Windows itself +// - Calling update & draw back and forth, forever +// -------------------------------------------------------- +HRESULT DXCore::Run() +{ + // Grab the start time now that + // the game loop is running + __int64 now; + QueryPerformanceCounter((LARGE_INTEGER*)&now); + startTime = now; + currentTime = now; + previousTime = now; + + // Give subclass a chance to initialize + Init(); + + // Our overall game and message loop + MSG msg = {}; + while (msg.message != WM_QUIT) + { + // Determine if there is a message waiting + if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) + { + // Translate and dispatch the message + // to our custom WindowProc function + TranslateMessage(&msg); + DispatchMessage(&msg); + } + else + { + // Update timer and title bar (if necessary) + UpdateTimer(); + if(titleBarStats) + UpdateTitleBarStats(); + + // The game loop + Update(deltaTime, totalTime); + Draw(deltaTime, totalTime); + } + } + + // We'll end up here once we get a WM_QUIT message, + // which usually comes from the user closing the window + return (HRESULT)msg.wParam; +} + + +// -------------------------------------------------------- +// Sends an OS-level window close message to our process, which +// will be handled by our message processing function +// -------------------------------------------------------- +void DXCore::Quit() +{ + PostMessage(this->hWnd, WM_CLOSE, NULL, NULL); +} + + +// -------------------------------------------------------- +// Uses high resolution time stamps to get very accurate +// timing information, and calculates useful time stats +// -------------------------------------------------------- +void DXCore::UpdateTimer() +{ + // Grab the current time + __int64 now; + QueryPerformanceCounter((LARGE_INTEGER*)&now); + currentTime = now; + + // Calculate delta time and clamp to zero + // - Could go negative if CPU goes into power save mode + // or the process itself gets moved to another core + deltaTime = max((float)((currentTime - previousTime) * perfCounterSeconds), 0.0f); + + // Calculate the total time from start to now + totalTime = (float)((currentTime - startTime) * perfCounterSeconds); + + // Save current time for next frame + previousTime = currentTime; +} + + +// -------------------------------------------------------- +// Updates the window's title bar with several stats once +// per second, including: +// - The window's width & height +// - The current FPS and ms/frame +// - The version of DirectX actually being used (usually 11) +// -------------------------------------------------------- +void DXCore::UpdateTitleBarStats() +{ + fpsFrameCount++; + + // Only calc FPS and update title bar once per second + float timeDiff = totalTime - fpsTimeElapsed; + if (timeDiff < 1.0f) + return; + + // How long did each frame take? (Approx) + float mspf = 1000.0f / (float)fpsFrameCount; + + // Quick and dirty title bar text (mostly for debugging) + std::ostringstream output; + output.precision(6); + output << titleBarText << + " Width: " << width << + " Height: " << height << + " FPS: " << fpsFrameCount << + " Frame Time: " << mspf << "ms"; + + // Append the version of DirectX the app is using + switch (dxFeatureLevel) + { + case D3D_FEATURE_LEVEL_11_1: output << " DX 11.1"; break; + case D3D_FEATURE_LEVEL_11_0: output << " DX 11.0"; break; + case D3D_FEATURE_LEVEL_10_1: output << " DX 10.1"; break; + case D3D_FEATURE_LEVEL_10_0: output << " DX 10.0"; break; + case D3D_FEATURE_LEVEL_9_3: output << " DX 9.3"; break; + case D3D_FEATURE_LEVEL_9_2: output << " DX 9.2"; break; + case D3D_FEATURE_LEVEL_9_1: output << " DX 9.1"; break; + default: output << " DX ???"; break; + } + + // Actually update the title bar and reset fps data + SetWindowText(hWnd, output.str().c_str()); + fpsFrameCount = 0; + fpsTimeElapsed += 1.0f; +} + +// -------------------------------------------------------- +// Allocates a console window we can print to for debugging +// +// bufferLines - Number of lines in the overall console buffer +// bufferColumns - Numbers of columns in the overall console buffer +// windowLines - Number of lines visible at once in the window +// windowColumns - Number of columns visible at once in the window +// -------------------------------------------------------- +void DXCore::CreateConsoleWindow(int bufferLines, int bufferColumns, int windowLines, int windowColumns) +{ + // Our temp console info struct + CONSOLE_SCREEN_BUFFER_INFO coninfo; + + // Get the console info and set the number of lines + AllocConsole(); + GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &coninfo); + coninfo.dwSize.Y = bufferLines; + coninfo.dwSize.X = bufferColumns; + SetConsoleScreenBufferSize(GetStdHandle(STD_OUTPUT_HANDLE), coninfo.dwSize); + + SMALL_RECT rect; + rect.Left = 0; + rect.Top = 0; + rect.Right = windowColumns; + rect.Bottom = windowLines; + SetConsoleWindowInfo(GetStdHandle(STD_OUTPUT_HANDLE), TRUE, &rect); + + FILE *stream; + freopen_s(&stream, "CONIN$", "r", stdin); + freopen_s(&stream, "CONOUT$", "w", stdout); + freopen_s(&stream, "CONOUT$", "w", stderr); + + // Prevent accidental console window close + HWND consoleHandle = GetConsoleWindow(); + HMENU hmenu = GetSystemMenu(consoleHandle, FALSE); + EnableMenuItem(hmenu, SC_CLOSE, MF_GRAYED); +} + +// -------------------------------------------------------------------------- +// Gets the actual path to this executable +// +// - As it turns out, the relative path for a program is different when +// running through VS and when running the .exe directly, which makes +// it a pain to properly load files external files (like textures) +// - Running through VS: Current Dir is the *project folder* +// - Running from .exe: Current Dir is the .exe's folder +// - This has nothing to do with DEBUG and RELEASE modes - it's purely a +// Visual Studio "thing", and isn't obvious unless you know to look +// for it. In fact, it could be fixed by changing a setting in VS, but +// the option is stored in a user file (.suo), which is ignored by most +// version control packages by default. Meaning: the option must be +// changed every on every PC. Ugh. So instead, here's a helper. +// -------------------------------------------------------------------------- +std::string DXCore::GetExePath() +{ + // Assume the path is just the "current directory" for now + std::string path = ".\\"; + + // Get the real, full path to this executable + char currentDir[1024] = {}; + GetModuleFileName(0, currentDir, 1024); + + // Find the location of the last slash charaacter + char* lastSlash = strrchr(currentDir, '\\'); + if (lastSlash) + { + // End the string at the last slash character, essentially + // chopping off the exe's file name. Remember, c-strings + // are null-terminated, so putting a "zero" character in + // there simply denotes the end of the string. + *lastSlash = 0; + + // Set the remainder as the path + path = currentDir; + } + + // Toss back whatever we've found + return path; +} + + +// --------------------------------------------------- +// Same as GetExePath(), except it returns a wide character +// string, which most of the Windows API requires. +// --------------------------------------------------- +std::wstring DXCore::GetExePath_Wide() +{ + // Grab the path as a standard string + std::string path = GetExePath(); + + // Convert to a wide string + wchar_t widePath[1024] = {}; + mbstowcs_s(0, widePath, path.c_str(), 1024); + + // Create a wstring for it and return + return std::wstring(widePath); +} + + +// ---------------------------------------------------- +// Gets the full path to a given file. NOTE: This does +// NOT "find" the file, it simply concatenates the given +// relative file path onto the executable's path +// ---------------------------------------------------- +std::string DXCore::GetFullPathTo(std::string relativeFilePath) +{ + return GetExePath() + "\\" + relativeFilePath; +} + + + +// ---------------------------------------------------- +// Same as GetFullPathTo, but with wide char strings. +// +// Gets the full path to a given file. NOTE: This does +// NOT "find" the file, it simply concatenates the given +// relative file path onto the executable's path +// ---------------------------------------------------- +std::wstring DXCore::GetFullPathTo_Wide(std::wstring relativeFilePath) +{ + return GetExePath_Wide() + L"\\" + relativeFilePath; +} + + + + + +// -------------------------------------------------------- +// Handles messages that are sent to our window by the +// operating system. Ignoring these messages would cause +// our program to hang and Windows would think it was +// unresponsive. +// -------------------------------------------------------- +LRESULT DXCore::ProcessMessage(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + // Check the incoming message and handle any we care about + switch (uMsg) + { + // This is the message that signifies the window closing + case WM_DESTROY: + PostQuitMessage(0); // Send a quit message to our own program + return 0; + + // Prevent beeping when we "alt-enter" into fullscreen + case WM_MENUCHAR: + return MAKELRESULT(0, MNC_CLOSE); + + // Prevent the overall window from becoming too small + case WM_GETMINMAXINFO: + ((MINMAXINFO*)lParam)->ptMinTrackSize.x = 200; + ((MINMAXINFO*)lParam)->ptMinTrackSize.y = 200; + return 0; + + // Sent when the window size changes + case WM_SIZE: + // Don't adjust anything when minimizing, + // since we end up with a width/height of zero + // and that doesn't play well with the GPU + if (wParam == SIZE_MINIMIZED) + return 0; + + // Save the new client area dimensions. + width = LOWORD(lParam); + height = HIWORD(lParam); + + // If DX is initialized, resize + // our required buffers + if (device) + OnResize(); + + return 0; + + // Is our focus state changing? + case WM_SETFOCUS: hasFocus = true; return 0; + case WM_KILLFOCUS: hasFocus = false; return 0; + case WM_ACTIVATE: hasFocus = (LOWORD(wParam) != WA_INACTIVE); return 0; + } + + // Let Windows handle any messages we're not touching + return DefWindowProc(hWnd, uMsg, wParam, lParam); +} + diff --git a/DXCore.h b/DXCore.h new file mode 100644 index 0000000..534cc1c --- /dev/null +++ b/DXCore.h @@ -0,0 +1,97 @@ +#pragma once + +#include +#include +#include +#include // Used for ComPtr - a smart pointer for COM objects + +// We can include the correct library files here +// instead of in Visual Studio settings if we want +#pragma comment(lib, "d3d11.lib") + +class DXCore +{ +public: + DXCore( + HINSTANCE hInstance, // The application's handle + const char* titleBarText, // Text for the window's title bar + unsigned int windowWidth, // Width of the window's client area + unsigned int windowHeight, // Height of the window's client area + bool debugTitleBarStats); // Show extra stats (fps) in title bar? + ~DXCore(); + + // Static requirements for OS-level message processing + static DXCore* DXCoreInstance; + static LRESULT CALLBACK WindowProc( + HWND hWnd, // Window handle + UINT uMsg, // Message + WPARAM wParam, // Message's first parameter + LPARAM lParam // Message's second parameter + ); + + // Internal method for message handling + LRESULT ProcessMessage(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); + + // Initialization and game-loop related methods + HRESULT InitWindow(); + HRESULT InitDirectX(); + HRESULT Run(); + void Quit(); + virtual void OnResize(); + + // Pure virtual methods for setup and game functionality + virtual void Init() = 0; + virtual void Update(float deltaTime, float totalTime) = 0; + virtual void Draw(float deltaTime, float totalTime) = 0; + +protected: + HINSTANCE hInstance; // The handle to the application + HWND hWnd; // The handle to the window itself + std::string titleBarText; // Custom text in window's title bar + bool titleBarStats; // Show extra stats in title bar? + + // Size of the window's client area + unsigned int width; + unsigned int height; + + // Does our window currently have focus? + // Helpful if we want to pause while not the active window + bool hasFocus; + + // DirectX related objects and variables + D3D_FEATURE_LEVEL dxFeatureLevel; + Microsoft::WRL::ComPtr swapChain; + Microsoft::WRL::ComPtr device; + Microsoft::WRL::ComPtr context; + + Microsoft::WRL::ComPtr backBufferRTV; + Microsoft::WRL::ComPtr depthStencilView; + + // Helper function for allocating a console window + void CreateConsoleWindow(int bufferLines, int bufferColumns, int windowLines, int windowColumns); + + // Helpers for determining the actual path to the executable + std::string GetExePath(); + std::wstring GetExePath_Wide(); + + std::string GetFullPathTo(std::string relativeFilePath); + std::wstring GetFullPathTo_Wide(std::wstring relativeFilePath); + + +private: + // Timing related data + double perfCounterSeconds; + float totalTime; + float deltaTime; + __int64 startTime; + __int64 currentTime; + __int64 previousTime; + + // FPS calculation + int fpsFrameCount; + float fpsTimeElapsed; + + void UpdateTimer(); // Updates the timer for this frame + void UpdateTitleBarStats(); // Puts debug info in the title bar +}; + diff --git a/Game.cpp b/Game.cpp new file mode 100644 index 0000000..9cbc56a --- /dev/null +++ b/Game.cpp @@ -0,0 +1,311 @@ +#include "Game.h" +#include "Vertex.h" + +// Needed for a helper function to read compiled shader files from the hard drive +#pragma comment(lib, "d3dcompiler.lib") +#include + +// For the DirectX Math library +using namespace DirectX; + +// -------------------------------------------------------- +// Constructor +// +// DXCore (base class) constructor will set up underlying fields. +// DirectX itself, and our window, are not ready yet! +// +// hInstance - the application's OS-level handle (unique ID) +// -------------------------------------------------------- +Game::Game(HINSTANCE hInstance) + : DXCore( + hInstance, // The application's handle + "DirectX Game", // Text for the window's title bar + 1280, // Width of the window's client area + 720, // Height of the window's client area + true) // Show extra stats (fps) in title bar? +{ + +#if defined(DEBUG) || defined(_DEBUG) + // Do we want a console window? Probably only in debug mode + CreateConsoleWindow(500, 120, 32, 120); + printf("Console window created successfully. Feel free to printf() here.\n"); +#endif + +} + +// -------------------------------------------------------- +// Destructor - Clean up anything our game has created: +// - Release all DirectX objects created here +// - Delete any objects to prevent memory leaks +// -------------------------------------------------------- +Game::~Game() +{ + // Note: Since we're using smart pointers (ComPtr), + // we don't need to explicitly clean up those DirectX objects + // - If we weren't using smart pointers, we'd need + // to call Release() on each DirectX object created in Game + +} + +// -------------------------------------------------------- +// Called once per program, after DirectX and the window +// are initialized but before the game loop. +// -------------------------------------------------------- +void Game::Init() +{ + // Helper methods for loading shaders, creating some basic + // geometry to draw and some simple camera matrices. + // - You'll be expanding and/or replacing these later + LoadShaders(); + CreateBasicGeometry(); + + // Tell the input assembler stage of the pipeline what kind of + // geometric primitives (points, lines or triangles) we want to draw. + // Essentially: "What kind of shape should the GPU draw with our data?" + context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); +} + +// -------------------------------------------------------- +// Loads shaders from compiled shader object (.cso) files +// and also created the Input Layout that describes our +// vertex data to the rendering pipeline. +// - Input Layout creation is done here because it must +// be verified against vertex shader byte code +// - We'll have that byte code already loaded below +// -------------------------------------------------------- +void Game::LoadShaders() +{ + // Blob for reading raw data + // - This is a simplified way of handling raw data + ID3DBlob* shaderBlob; + + // Read our compiled vertex shader code into a blob + // - Essentially just "open the file and plop its contents here" + D3DReadFileToBlob( + GetFullPathTo_Wide(L"VertexShader.cso").c_str(), // Using a custom helper for file paths + &shaderBlob); + + // Create a vertex shader from the information we + // have read into the blob above + // - A blob can give a pointer to its contents, and knows its own size + device->CreateVertexShader( + shaderBlob->GetBufferPointer(), // Get a pointer to the blob's contents + shaderBlob->GetBufferSize(), // How big is that data? + 0, // No classes in this shader + vertexShader.GetAddressOf()); // The address of the ID3D11VertexShader* + + + // Create an input layout that describes the vertex format + // used by the vertex shader we're using + // - This is used by the pipeline to know how to interpret the raw data + // sitting inside a vertex buffer + // - Doing this NOW because it requires a vertex shader's byte code to verify against! + // - Luckily, we already have that loaded (the blob above) + D3D11_INPUT_ELEMENT_DESC inputElements[2] = {}; + + // Set up the first element - a position, which is 3 float values + inputElements[0].Format = DXGI_FORMAT_R32G32B32_FLOAT; // Most formats are described as color channels; really it just means "Three 32-bit floats" + inputElements[0].SemanticName = "POSITION"; // This is "POSITION" - needs to match the semantics in our vertex shader input! + inputElements[0].AlignedByteOffset = D3D11_APPEND_ALIGNED_ELEMENT; // How far into the vertex is this? Assume it's after the previous element + + // Set up the second element - a color, which is 4 more float values + inputElements[1].Format = DXGI_FORMAT_R32G32B32A32_FLOAT; // 4x 32-bit floats + inputElements[1].SemanticName = "COLOR"; // Match our vertex shader input! + inputElements[1].AlignedByteOffset = D3D11_APPEND_ALIGNED_ELEMENT; // After the previous element + + // Create the input layout, verifying our description against actual shader code + device->CreateInputLayout( + inputElements, // An array of descriptions + 2, // How many elements in that array + shaderBlob->GetBufferPointer(), // Pointer to the code of a shader that uses this layout + shaderBlob->GetBufferSize(), // Size of the shader code that uses this layout + inputLayout.GetAddressOf()); // Address of the resulting ID3D11InputLayout* + + + + // Read and create the pixel shader + // - Reusing the same blob here, since we're done with the vert shader code + D3DReadFileToBlob( + GetFullPathTo_Wide(L"PixelShader.cso").c_str(), // Using a custom helper for file paths + &shaderBlob); + + device->CreatePixelShader( + shaderBlob->GetBufferPointer(), + shaderBlob->GetBufferSize(), + 0, + pixelShader.GetAddressOf()); +} + + + +// -------------------------------------------------------- +// Creates the geometry we're going to draw - a single triangle for now +// -------------------------------------------------------- +void Game::CreateBasicGeometry() +{ + // Create some temporary variables to represent colors + // - Not necessary, just makes things more readable + XMFLOAT4 red = XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f); + XMFLOAT4 green = XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f); + XMFLOAT4 blue = XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f); + + // Set up the vertices of the triangle we would like to draw + // - We're going to copy this array, exactly as it exists in memory + // over to a DirectX-controlled data structure (the vertex buffer) + // - Note: Since we don't have a camera or really any concept of + // a "3d world" yet, we're simply describing positions within the + // bounds of how the rasterizer sees our screen: [-1 to +1] on X and Y + // - This means (0,0) is at the very center of the screen. + // - These are known as "Normalized Device Coordinates" or "Homogeneous + // Screen Coords", which are ways to describe a position without + // knowing the exact size (in pixels) of the image/window/etc. + // - Long story short: Resizing the window also resizes the triangle, + // since we're describing the triangle in terms of the window itself + Vertex vertices[] = + { + { XMFLOAT3(+0.0f, +0.5f, +0.0f), red }, + { XMFLOAT3(+0.5f, -0.5f, +0.0f), blue }, + { XMFLOAT3(-0.5f, -0.5f, +0.0f), green }, + }; + + // Set up the indices, which tell us which vertices to use and in which order + // - This is somewhat redundant for just 3 vertices (it's a simple example) + // - Indices are technically not required if the vertices are in the buffer + // in the correct order and each one will be used exactly once + // - But just to see how it's done... + unsigned int indices[] = { 0, 1, 2 }; + + + // Create the VERTEX BUFFER description ----------------------------------- + // - The description is created on the stack because we only need + // it to create the buffer. The description is then useless. + D3D11_BUFFER_DESC vbd; + vbd.Usage = D3D11_USAGE_IMMUTABLE; + vbd.ByteWidth = sizeof(Vertex) * 3; // 3 = number of vertices in the buffer + vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER; // Tells DirectX this is a vertex buffer + vbd.CPUAccessFlags = 0; + vbd.MiscFlags = 0; + vbd.StructureByteStride = 0; + + // Create the proper struct to hold the initial vertex data + // - This is how we put the initial data into the buffer + D3D11_SUBRESOURCE_DATA initialVertexData; + initialVertexData.pSysMem = vertices; + + // Actually create the buffer with the initial data + // - Once we do this, we'll NEVER CHANGE THE BUFFER AGAIN + device->CreateBuffer(&vbd, &initialVertexData, vertexBuffer.GetAddressOf()); + + + + // Create the INDEX BUFFER description ------------------------------------ + // - The description is created on the stack because we only need + // it to create the buffer. The description is then useless. + D3D11_BUFFER_DESC ibd; + ibd.Usage = D3D11_USAGE_IMMUTABLE; + ibd.ByteWidth = sizeof(unsigned int) * 3; // 3 = number of indices in the buffer + ibd.BindFlags = D3D11_BIND_INDEX_BUFFER; // Tells DirectX this is an index buffer + ibd.CPUAccessFlags = 0; + ibd.MiscFlags = 0; + ibd.StructureByteStride = 0; + + // Create the proper struct to hold the initial index data + // - This is how we put the initial data into the buffer + D3D11_SUBRESOURCE_DATA initialIndexData; + initialIndexData.pSysMem = indices; + + // Actually create the buffer with the initial data + // - Once we do this, we'll NEVER CHANGE THE BUFFER AGAIN + device->CreateBuffer(&ibd, &initialIndexData, indexBuffer.GetAddressOf()); + +} + + +// -------------------------------------------------------- +// Handle resizing DirectX "stuff" to match the new window size. +// For instance, updating our projection matrix's aspect ratio. +// -------------------------------------------------------- +void Game::OnResize() +{ + // Handle base-level DX resize stuff + DXCore::OnResize(); +} + +// -------------------------------------------------------- +// Update your game here - user input, move objects, AI, etc. +// -------------------------------------------------------- +void Game::Update(float deltaTime, float totalTime) +{ + // Quit if the escape key is pressed + if (GetAsyncKeyState(VK_ESCAPE)) + Quit(); +} + +// -------------------------------------------------------- +// Clear the screen, redraw everything, present to the user +// -------------------------------------------------------- +void Game::Draw(float deltaTime, float totalTime) +{ + // Background color (Cornflower Blue in this case) for clearing + const float color[4] = { 0.4f, 0.6f, 0.75f, 0.0f }; + + // Clear the render target and depth buffer (erases what's on the screen) + // - Do this ONCE PER FRAME + // - At the beginning of Draw (before drawing *anything*) + context->ClearRenderTargetView(backBufferRTV.Get(), color); + context->ClearDepthStencilView( + depthStencilView.Get(), + D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, + 1.0f, + 0); + + + // Set the vertex and pixel shaders to use for the next Draw() command + // - These don't technically need to be set every frame + // - Once you start applying different shaders to different objects, + // you'll need to swap the current shaders before each draw + context->VSSetShader(vertexShader.Get(), 0, 0); + context->PSSetShader(pixelShader.Get(), 0, 0); + + + // Ensure the pipeline knows how to interpret the data (numbers) + // from the vertex buffer. + // - If all of your 3D models use the exact same vertex layout, + // this could simply be done once in Init() + // - However, this isn't always the case (but might be for this course) + context->IASetInputLayout(inputLayout.Get()); + + + // Set buffers in the input assembler + // - Do this ONCE PER OBJECT you're drawing, since each object might + // have different geometry. + // - for this demo, this step *could* simply be done once during Init(), + // but I'm doing it here because it's often done multiple times per frame + // in a larger application/game + UINT stride = sizeof(Vertex); + UINT offset = 0; + context->IASetVertexBuffers(0, 1, vertexBuffer.GetAddressOf(), &stride, &offset); + context->IASetIndexBuffer(indexBuffer.Get(), DXGI_FORMAT_R32_UINT, 0); + + + // Finally do the actual drawing + // - Do this ONCE PER OBJECT you intend to draw + // - This will use all of the currently set DirectX "stuff" (shaders, buffers, etc) + // - DrawIndexed() uses the currently set INDEX BUFFER to look up corresponding + // vertices in the currently set VERTEX BUFFER + context->DrawIndexed( + 3, // The number of indices to use (we could draw a subset if we wanted) + 0, // Offset to the first index we want to use + 0); // Offset to add to each index when looking up vertices + + + + // Present the back buffer to the user + // - Puts the final frame we're drawing into the window so the user can see it + // - Do this exactly ONCE PER FRAME (always at the very end of the frame) + swapChain->Present(0, 0); + + // Due to the usage of a more sophisticated swap chain, + // the render target must be re-bound after every call to Present() + context->OMSetRenderTargets(1, backBufferRTV.GetAddressOf(), depthStencilView.Get()); +} \ No newline at end of file diff --git a/Game.h b/Game.h new file mode 100644 index 0000000..db945fa --- /dev/null +++ b/Game.h @@ -0,0 +1,44 @@ +#pragma once + +#include "DXCore.h" +#include +#include // Used for ComPtr - a smart pointer for COM objects + +class Game + : public DXCore +{ + +public: + Game(HINSTANCE hInstance); + ~Game(); + + // Overridden setup and game loop methods, which + // will be called automatically + void Init(); + void OnResize(); + void Update(float deltaTime, float totalTime); + void Draw(float deltaTime, float totalTime); + +private: + + // Initialization helper methods - feel free to customize, combine, etc. + void LoadShaders(); + void CreateBasicGeometry(); + + + // Note the usage of ComPtr below + // - This is a smart pointer for objects that abide by the + // Component Object Model, which DirectX objects do + // - More info here: https://github.com/Microsoft/DirectXTK/wiki/ComPtr + + // Buffers to hold actual geometry data + Microsoft::WRL::ComPtr vertexBuffer; + Microsoft::WRL::ComPtr indexBuffer; + + // Shaders and shader-related constructs + Microsoft::WRL::ComPtr pixelShader; + Microsoft::WRL::ComPtr vertexShader; + Microsoft::WRL::ComPtr inputLayout; + +}; + diff --git a/Main.cpp b/Main.cpp new file mode 100644 index 0000000..efc8c9e --- /dev/null +++ b/Main.cpp @@ -0,0 +1,41 @@ + +#include +#include "Game.h" + +// -------------------------------------------------------- +// Entry point for a graphical (non-console) Windows application +// -------------------------------------------------------- +int WINAPI WinMain( + _In_ HINSTANCE hInstance, // The handle to this app's instance + _In_opt_ HINSTANCE hPrevInstance, // A handle to the previous instance of the app (always NULL) + _In_ LPSTR lpCmdLine, // Command line params + _In_ int nCmdShow) // How the window should be shown (we ignore this) +{ +#if defined(DEBUG) | defined(_DEBUG) + // Enable memory leak detection as a quick and dirty + // way of determining if we forgot to clean something up + // - You may want to use something more advanced, like Visual Leak Detector + _CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF ); +#endif + + // Create the Game object using + // the app handle we got from WinMain + Game dxGame(hInstance); + + // Result variable for function calls below + HRESULT hr = S_OK; + + // Attempt to create the window for our program, and + // exit early if something failed + hr = dxGame.InitWindow(); + if(FAILED(hr)) return hr; + + // Attempt to initialize DirectX, and exit + // early if something failed + hr = dxGame.InitDirectX(); + if(FAILED(hr)) return hr; + + // Begin the message and game loop, and then return + // whatever we get back once the game loop is over + return dxGame.Run(); +} diff --git a/PixelShader.hlsl b/PixelShader.hlsl new file mode 100644 index 0000000..88e9657 --- /dev/null +++ b/PixelShader.hlsl @@ -0,0 +1,34 @@ + +// Struct representing the data we expect to receive from earlier pipeline stages +// - Should match the output of our corresponding vertex shader +// - The name of the struct itself is unimportant +// - The variable names don't have to match other shaders (just the semantics) +// - Each variable must have a semantic, which defines its usage +struct VertexToPixel +{ + // Data type + // | + // | Name Semantic + // | | | + // v v v + float4 position : SV_POSITION; + float4 color : COLOR; +}; + +// -------------------------------------------------------- +// The entry point (main method) for our pixel shader +// +// - Input is the data coming down the pipeline (defined by the struct) +// - Output is a single color (float4) +// - Has a special semantic (SV_TARGET), which means +// "put the output of this into the current render target" +// - Named "main" because that's the default the shader compiler looks for +// -------------------------------------------------------- +float4 main(VertexToPixel input) : SV_TARGET +{ + // Just return the input color + // - This color (like most values passing through the rasterizer) is + // interpolated for each pixel between the corresponding vertices + // of the triangle we're rendering + return input.color; +} \ No newline at end of file diff --git a/Vertex.h b/Vertex.h new file mode 100644 index 0000000..d48e03b --- /dev/null +++ b/Vertex.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +// -------------------------------------------------------- +// A custom vertex definition +// +// You will eventually ADD TO this, and/or make more of these! +// -------------------------------------------------------- +struct Vertex +{ + DirectX::XMFLOAT3 Position; // The position of the vertex + DirectX::XMFLOAT4 Color; // The color of the vertex +}; \ No newline at end of file diff --git a/VertexShader.hlsl b/VertexShader.hlsl new file mode 100644 index 0000000..e0bf00b --- /dev/null +++ b/VertexShader.hlsl @@ -0,0 +1,64 @@ + +// Struct representing a single vertex worth of data +// - This should match the vertex definition in our C++ code +// - By "match", I mean the size, order and number of members +// - The name of the struct itself is unimportant, but should be descriptive +// - Each variable must have a semantic, which defines its usage +struct VertexShaderInput +{ + // Data type + // | + // | Name Semantic + // | | | + // v v v + float3 position : POSITION; // XYZ position + float4 color : COLOR; // RGBA color +}; + +// Struct representing the data we're sending down the pipeline +// - Should match our pixel shader's input (hence the name: Vertex to Pixel) +// - At a minimum, we need a piece of data defined tagged as SV_POSITION +// - The name of the struct itself is unimportant, but should be descriptive +// - Each variable must have a semantic, which defines its usage +struct VertexToPixel +{ + // Data type + // | + // | Name Semantic + // | | | + // v v v + float4 position : SV_POSITION; // XYZW position (System Value Position) + float4 color : COLOR; // RGBA color +}; + +// -------------------------------------------------------- +// The entry point (main method) for our vertex shader +// +// - Input is exactly one vertex worth of data (defined by a struct) +// - Output is a single struct of data to pass down the pipeline +// - Named "main" because that's the default the shader compiler looks for +// -------------------------------------------------------- +VertexToPixel main( VertexShaderInput input ) +{ + // Set up output struct + VertexToPixel output; + + // Here we're essentially passing the input position directly through to the next + // stage (rasterizer), though it needs to be a 4-component vector now. + // - To be considered within the bounds of the screen, the X and Y components + // must be between -1 and 1. + // - The Z component must be between 0 and 1. + // - Each of these components is then automatically divided by the W component, + // which we're leaving at 1.0 for now (this is more useful when dealing with + // a perspective projection matrix, which we'll get to in the future). + output.position = float4(input.position, 1.0f); + + // Pass the color through + // - The values will be interpolated per-pixel by the rasterizer + // - We don't need to alter it here, but we do need to send it to the pixel shader + output.color = input.color; + + // Whatever we return will make its way through the pipeline to the + // next programmable stage we're using (the pixel shader for now) + return output; +} \ No newline at end of file