上一篇我们用Direct 2D绘制了一个平面图形。接下来我们用Direct 3D绘制一个3D图形。
首先我们还是通过复制的方法重用我们的代码。拷贝helloengine_d2d.cpp到helloengine_d3d.cpp。然后作如下修改(本文使用的是D3D 11接口,因为直接上D3D 12的话学习难度坡度太大):
@@ -2,13 +2,39 @@
#include <windows.h>
#include <windowsx.h>
#include <tchar.h>
+#include <stdint.h>
-#include <d2d1.h>
+#include <d3d11.h>
+#include <d3d11_1.h>
+#include <d3dcompiler.h>
+#include <DirectXMath.h>
+#include <DirectXPackedVector.h>
+#include <DirectXColors.h>
-ID2D1Factory *pFactory = nullptr;
-ID2D1HwndRenderTarget *pRenderTarget = nullptr;
-ID2D1SolidColorBrush *pLightSlateGrayBrush = nullptr;
-ID2D1SolidColorBrush *pCornflowerBlueBrush = nullptr;
+using namespace DirectX;
+using namespace DirectX::PackedVector;
+
+const uint32_t SCREEN_WIDTH = 960;
+const uint32_t SCREEN_HEIGHT = 480;
+
+// global declarations
+IDXGISwapChain *g_pSwapchain = nullptr; // the pointer to the swap chain interface
+ID3D11Device *g_pDev = nullptr; // the pointer to our Direct3D device interface
+ID3D11DeviceContext *g_pDevcon = nullptr; // the pointer to our Direct3D device context
+
+ID3D11RenderTargetView *g_pRTView = nullptr;
+
+ID3D11InputLayout *g_pLayout = nullptr; // the pointer to the input layout
+ID3D11VertexShader *g_pVS = nullptr; // the pointer to the vertex shader
+ID3D11PixelShader *g_pPS = nullptr; // the pointer to the pixel shader
+
+ID3D11Buffer *g_pVBuffer = nullptr; // Vertex Buffer
+
+// vertex buffer structure
+struct VERTEX {
+ XMFLOAT3 Position;
+ XMFLOAT4 Color;
+};
template<class T>
inline void SafeRelease(T **ppInterfaceToRelease)
@@ -21,32 +47,164 @@ inline void SafeRelease(T **ppInterfaceToRelease)
}
}
+void CreateRenderTarget() {
+ HRESULT hr;
+ ID3D11Texture2D *pBackBuffer;
+
+ // Get a pointer to the back buffer
+ g_pSwapchain->GetBuffer( 0, __uuidof( ID3D11Texture2D ),
+ ( LPVOID* )&pBackBuffer );
+
+ // Create a render-target view
+ g_pDev->CreateRenderTargetView( pBackBuffer, NULL,
+ &g_pRTView );
+ pBackBuffer->Release();
+
+ // Bind the view
+ g_pDevcon->OMSetRenderTargets( 1, &g_pRTView, NULL );
+}
+
+void SetViewPort() {
+ D3D11_VIEWPORT viewport;
+ ZeroMemory(&viewport, sizeof(D3D11_VIEWPORT));
+
+ viewport.TopLeftX = 0;
+ viewport.TopLeftY = 0;
+ viewport.Width = SCREEN_WIDTH;
+ viewport.Height = SCREEN_HEIGHT;
+
+ g_pDevcon->RSSetViewports(1, &viewport);
+}
+
+// this is the function that loads and prepares the shaders
+void InitPipeline() {
+ // load and compile the two shaders
+ ID3DBlob *VS, *PS;
+
+ D3DReadFileToBlob(L"copy.vso", &VS);
+ D3DReadFileToBlob(L"copy.pso", &PS);
+
+ // encapsulate both shaders into shader objects
+ g_pDev->CreateVertexShader(VS->GetBufferPointer(), VS->GetBufferSize(), NULL, &g_pVS);
+ g_pDev->CreatePixelShader(PS->GetBufferPointer(), PS->GetBufferSize(), NULL, &g_pPS);
+
+ // set the shader objects
+ g_pDevcon->VSSetShader(g_pVS, 0, 0);
+ g_pDevcon->PSSetShader(g_pPS, 0, 0);
+
+ // create the input layout object
+ D3D11_INPUT_ELEMENT_DESC ied[] =
{
+ {"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0},
+ {"COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0},
+ };
+ g_pDev->CreateInputLayout(ied, 2, VS->GetBufferPointer(), VS->GetBufferSize(), &g_pLayout);
+ g_pDevcon->IASetInputLayout(g_pLayout);
+ VS->Release();
+ PS->Release();
+}
+// this is the function that creates the shape to render
+void InitGraphics() {
+ // create a triangle using the VERTEX struct
+ VERTEX OurVertices[] =
+ {
+ {XMFLOAT3(0.0f, 0.5f, 0.0f), XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f)},
+ {XMFLOAT3(0.45f, -0.5, 0.0f), XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f)},
+ {XMFLOAT3(-0.45f, -0.5f, 0.0f), XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f)}
+ };
+ // create the vertex buffer
+ D3D11_BUFFER_DESC bd;
+ ZeroMemory(&bd, sizeof(bd));
+ bd.Usage = D3D11_USAGE_DYNAMIC; // write access access by CPU and GPU
+ bd.ByteWidth = sizeof(VERTEX) * 3; // size is the VERTEX struct * 3
+ bd.BindFlags = D3D11_BIND_VERTEX_BUFFER; // use as a vertex buffer
+ bd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; // allow CPU to write in buffer
+
+ g_pDev->CreateBuffer(&bd, NULL, &g_pVBuffer); // create the buffer
+
+ // copy the vertices into the buffer
+ D3D11_MAPPED_SUBRESOURCE ms;
+ g_pDevcon->Map(g_pVBuffer, NULL, D3D11_MAP_WRITE_DISCARD, NULL, &ms); // map the buffer
+ memcpy(ms.pData, OurVertices, sizeof(VERTEX) * 3); // copy the data
+ g_pDevcon->Unmap(g_pVBuffer, NULL); // unmap the buffer
+}
+
+// this function prepare graphic resources for use
HRESULT CreateGraphicsResources(HWND hWnd)
{
HRESULT hr = S_OK;
- if (pRenderTarget == nullptr)
- {
- RECT rc;
- GetClientRect(hWnd, &rc);
- D2D1_SIZE_U size = D2D1::SizeU(rc.right - rc.left,
- rc.bottom - rc.top);
- hr = pFactory->CreateHwndRenderTarget(
- D2D1::RenderTargetProperties(),
- D2D1::HwndRenderTargetProperties(hWnd, size),
- &pRenderTarget);
- if (SUCCEEDED(hr))
- {
- hr = pRenderTarget->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::LightSlateGray), &pLightSlateGrayBrush);
- }
- if (SUCCEEDED(hr))
- {
- hr = pRenderTarget->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::CornflowerBlue), &pCornflowerBlueBrush);
+ if (g_pSwapchain == nullptr)
+ {
+ // create a struct to hold information about the swap chain
+ DXGI_SWAP_CHAIN_DESC scd;
+
+ // clear out the struct for use
+ ZeroMemory(&scd, sizeof(DXGI_SWAP_CHAIN_DESC));
+
+ // fill the swap chain description struct
+ scd.BufferCount = 1; // one back buffer
+ scd.BufferDesc.Width = SCREEN_WIDTH;
+ scd.BufferDesc.Height = SCREEN_HEIGHT;
+ scd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; // use 32-bit color
+ scd.BufferDesc.RefreshRate.Numerator = 60;
+ scd.BufferDesc.RefreshRate.Denominator = 1;
+ scd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; // how swap chain is to be used
+ scd.OutputWindow = hWnd; // the window to be used
+ scd.SampleDesc.Count = 4; // how many multisamples
+ scd.Windowed = TRUE; // windowed/full-screen mode
+ scd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH; // allow full-screen switching
+
+ const D3D_FEATURE_LEVEL FeatureLevels[] = { D3D_FEATURE_LEVEL_11_1,
+ D3D_FEATURE_LEVEL_11_0,
+ D3D_FEATURE_LEVEL_10_1,
+ D3D_FEATURE_LEVEL_10_0,
+ D3D_FEATURE_LEVEL_9_3,
+ D3D_FEATURE_LEVEL_9_2,
+ D3D_FEATURE_LEVEL_9_1};
+ D3D_FEATURE_LEVEL FeatureLevelSupported;
+
+ HRESULT hr = S_OK;
+
+ // create a device, device context and swap chain using the information in the scd struct
+ hr = D3D11CreateDeviceAndSwapChain(NULL,
+ D3D_DRIVER_TYPE_HARDWARE,
+ NULL,
+ 0,
+ FeatureLevels,
+ _countof(FeatureLevels),
+ D3D11_SDK_VERSION,
+ &scd,
+ &g_pSwapchain,
+ &g_pDev,
+ &FeatureLevelSupported,
+ &g_pDevcon);
+
+ if (hr == E_INVALIDARG) {
+ hr = D3D11CreateDeviceAndSwapChain(NULL,
+ D3D_DRIVER_TYPE_HARDWARE,
+ NULL,
+ 0,
+ &FeatureLevelSupported,
+ 1,
+ D3D11_SDK_VERSION,
+ &scd,
+ &g_pSwapchain,
+ &g_pDev,
+ NULL,
+ &g_pDevcon);
+ }
+
+ if (hr == S_OK) {
+ CreateRenderTarget();
+ SetViewPort();
+ InitPipeline();
+ InitGraphics();
}
}
return hr;
@@ -54,11 +212,40 @@ HRESULT CreateGraphicsResources(HWND hWnd)
void DiscardGraphicsResources()
{
- SafeRelease(&pRenderTarget);
- SafeRelease(&pLightSlateGrayBrush);
- SafeRelease(&pCornflowerBlueBrush);
+ SafeRelease(&g_pLayout);
+ SafeRelease(&g_pVS);
+ SafeRelease(&g_pPS);
+ SafeRelease(&g_pVBuffer);
+ SafeRelease(&g_pSwapchain);
+ SafeRelease(&g_pRTView);
+ SafeRelease(&g_pDev);
+ SafeRelease(&g_pDevcon);
}
+// this is the function used to render a single frame
+void RenderFrame()
+{
+ // clear the back buffer to a deep blue
+ const FLOAT clearColor[] = {0.0f, 0.2f, 0.4f, 1.0f};
+ g_pDevcon->ClearRenderTargetView(g_pRTView, clearColor);
+
+ // do 3D rendering on the back buffer here
+ {
+ // select which vertex buffer to display
+ UINT stride = sizeof(VERTEX);
+ UINT offset = 0;
+ g_pDevcon->IASetVertexBuffers(0, 1, &g_pVBuffer, &stride, &offset);
+
+ // select which primtive type we are using
+ g_pDevcon->IASetPrimitiveTopology(D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
+
+ // draw the vertex buffer to the back buffer
+ g_pDevcon->Draw(3, 0);
+ }
+
+ // swap the back buffer and the front buffer
+ g_pSwapchain->Present(0, 0);
+}
// the WindowProc function prototype
LRESULT CALLBACK WindowProc(HWND hWnd,
@@ -77,9 +264,6 @@ int WINAPI WinMain(HINSTANCE hInstance,
// this struct holds information for the window class
WNDCLASSEX wc;
- // initialize COM
- if (FAILED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE))) return -1;
-
// clear out the window class for use
ZeroMemory(&wc, sizeof(WNDCLASSEX));
@@ -97,17 +281,17 @@ int WINAPI WinMain(HINSTANCE hInstance,
// create the window and use the result as the handle
hWnd = CreateWindowEx(0,
- _T("WindowClass1"), // name of the window class
- _T("Hello, Engine![Direct 2D]"), // title of the window
- WS_OVERLAPPEDWINDOW, // window style
- 100, // x-position of the window
- 100, // y-position of the window
- 960, // width of the window
- 540, // height of the window
- NULL, // we have no parent window, NULL
- NULL, // we aren't using menus, NULL
- hInstance, // application handle
- NULL); // used with multiple windows, NULL
+ _T("WindowClass1"), // name of the window class
+ _T("Hello, Engine![Direct 3D]"), // title of the window
+ WS_OVERLAPPEDWINDOW, // window style
+ 100, // x-position of the window
+ 100, // y-position of the window
+ SCREEN_WIDTH, // width of the window
+ SCREEN_HEIGHT, // height of the window
+ NULL, // we have no parent window, NULL
+ NULL, // we aren't using menus, NULL
+ hInstance, // application handle
+ NULL); // used with multiple windows, NULL
// display the window on the screen
ShowWindow(hWnd, nCmdShow);
@@ -127,9 +311,6 @@ int WINAPI WinMain(HINSTANCE hInstance,
DispatchMessage(&msg);
}
- // uninitialize COM
- CoUninitialize();
-
// return this part of the WM_QUIT message to Windows
return msg.wParam;
}
@@ -144,108 +325,27 @@ LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lPara
switch(message)
{
case WM_CREATE:
- if (FAILED(D2D1CreateFactory(
- D2D1_FACTORY_TYPE_SINGLE_THREADED, &pFactory)))
- {
- result = -1; // Fail CreateWindowEx.
- }
wasHandled = true;
- result = 1;
break;
case WM_PAINT:
- {
- HRESULT hr = CreateGraphicsResources(hWnd);
- if (SUCCEEDED(hr))
- {
- PAINTSTRUCT ps;
- BeginPaint(hWnd, &ps);
-
- // start build GPU draw command
- pRenderTarget->BeginDraw();
-
- // clear the background with white color
- pRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::White));
-
- // retrieve the size of drawing area
- D2D1_SIZE_F rtSize = pRenderTarget->GetSize();
-
- // draw a grid background.
- int width = static_cast<int>(rtSize.width);
- int height = static_cast<int>(rtSize.height);
-
- for (int x = 0; x < width; x += 10)
- {
- pRenderTarget->DrawLine(
- D2D1::Point2F(static_cast<FLOAT>(x), 0.0f),
- D2D1::Point2F(static_cast<FLOAT>(x), rtSize.height),
- pLightSlateGrayBrush,
- 0.5f
- );
- }
-
- for (int y = 0; y < height; y += 10)
- {
- pRenderTarget->DrawLine(
- D2D1::Point2F(0.0f, static_cast<FLOAT>(y)),
- D2D1::Point2F(rtSize.width, static_cast<FLOAT>(y)),
- pLightSlateGrayBrush,
- 0.5f
- );
- }
-
- // draw two rectangles
- D2D1_RECT_F rectangle1 = D2D1::RectF(
- rtSize.width/2 - 50.0f,
- rtSize.height/2 - 50.0f,
- rtSize.width/2 + 50.0f,
- rtSize.height/2 + 50.0f
- );
-
- D2D1_RECT_F rectangle2 = D2D1::RectF(
- rtSize.width/2 - 100.0f,
- rtSize.height/2 - 100.0f,
- rtSize.width/2 + 100.0f,
- rtSize.height/2 + 100.0f
- );
-
- // draw a filled rectangle
- pRenderTarget->FillRectangle(&rectangle1, pLightSlateGrayBrush);
-
- // draw a outline only rectangle
- pRenderTarget->DrawRectangle(&rectangle2, pCornflowerBlueBrush);
-
- // end GPU draw command building
- hr = pRenderTarget->EndDraw();
- if (FAILED(hr) || hr == D2DERR_RECREATE_TARGET)
- {
- DiscardGraphicsResources();
- }
-
- EndPaint(hWnd, &ps);
- }
- }
+ result = CreateGraphicsResources(hWnd);
+ RenderFrame();
wasHandled = true;
break;
case WM_SIZE:
- if (pRenderTarget != nullptr)
+ if (g_pSwapchain != nullptr)
{
- RECT rc;
- GetClientRect(hWnd, &rc);
-
- D2D1_SIZE_U size = D2D1::SizeU(rc.right - rc.left, rc.bottom - rc.top);
-
- pRenderTarget->Resize(size);
+ DiscardGraphicsResources();
}
wasHandled = true;
break;
case WM_DESTROY:
DiscardGraphicsResources();
- if (pFactory) {pFactory->Release(); pFactory=nullptr; }
PostQuitMessage(0);
- result = 1;
wasHandled = true;
break;
简单解释一下:
(关于D3D编程的系统教程在这里:Getting Started with Direct3D)
基本窗体创建没有任何变化。因为不是COM,不需要创建Factory。(需要考证。从结果上来说是这样,但是这里不需要COM相关的代码的原因应该是D3D11的库当中已经进行了相关的处理。因为D3D12明显是COM)所以WM_CREATE里面基本不做任何事情。
新加了如下几个函数(子过程):
CreateRenderTarget();
SetViewPort();
InitPipeline();
InitGraphics();
第一个依然是我们熟悉的,创建RenderTarget,也就是画布。
第二个是设置视口。也就是设置渲染结果在画布当中的映射。我们目前是将整个画布都分配给了一个视口。在实际的游戏开发当中,会有多人分屏游玩的模式。这个时候就需要把一张画布分割成好几个视口。另外一个典型的运用就是VR。VR需要绘制左眼和右眼两幅图像,因此也需要将画布分割为两个视口。
第三个是初始化渲染管道。渲染管道就是GPU的工作流水线。使用GPU进行3D渲染的时候,最一般的会有顶点变换,像素化和像素填色这3个阶段。在这个过程当中,顶点变换和填色是可以编程的(而像素化是硬件固定功能,不可编程)。在这个初始化函数里面,我们可以看到我们从磁盘读取了两个GPU用的程序,一个叫“copy.vso”,一个叫”copy.pso”。它们分别对应着GPU的顶点变换阶段(Vertex Shading)和像素填色阶段(Pixel Shading)。这两个程序是我们编写的,使用的语言是HLSL(这是一种类似C语言的,微软推出的GPU编程语言)。具体内容在下面说明。
第四个则是传入实际要绘制的模型的顶点信息了。我们这里绘制的是一个三角形,因此有3个顶点。注意在D3D当中,使用的坐标系为左手坐标系,就是x轴向右,y轴向上,z轴指向屏幕里面。(这点很特别,今后写别的图形API的时候就有比较)
Coordinate Systems (Direct3D 9)
我们的顶点结构是这样的:
// vertex buffer structure
struct VERTEX {
XMFLOAT3 Position;
XMFLOAT4 Color;
};
因此,我们是这样初始化顶点的:
// create a triangle using the VERTEX struct
VERTEX OurVertices[] =
{
{XMFLOAT3(0.0f, 0.5f, 0.0f), XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f)},
{XMFLOAT3(0.45f, -0.5, 0.0f), XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f)},
{XMFLOAT3(-0.45f, -0.5f, 0.0f), XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f)}
};
坐标系的原点在视口的中心,因为我们只有一个视口,所以就是屏幕中心。视口在各个坐标轴的缺省的范围是[-1, 1],因此0.5差不多正好是在画布长宽各1/4的地方。第二个部分是颜色。分别对应R(红色通道)G(绿色通道)B(蓝色通道)和A(透明通道)。这个顺序是在代码的layout部分指定的。范围是[0, 1]。所以我们可以看到
- 第一个顶点是上方中央,红色
- 第二个顶点是下方右侧,绿色
- 第三个顶点是下方左侧,蓝色
然后我们来看我们所写的GPU程序(Shader,称为着色器)
copy.vs
#include "cbuffer.h"
#include "vsoutput.hs"
v2p main(a2v input) {
v2p output;
output.position = float4(input.position, 1.0);
output.color = input.color;
return output;
}
看到这个程序基本就是将输入原样输出。其中输入是来自我们的应用程序(就是我们上面定义的VERTEX),而输出是输出给流水线的下一个步骤。在我们这个例子里面,就是像素着色器:
#include "vsoutput.hs"
float4 main(v2p input) : SV_TARGET
{
return input.color;
}
而像素着色器也只是原样输出输入的颜色。
注意到我们用到了两个头文件。一个是定义应用程序传给Vertex Shader的数据结构,一个是定义Vertex Shader输出给Pixel Shader的数据结构。内容如下:
cbuffer.h
struct a2v {
float3 position : POSITION;
float4 color : COLOR;
};
vsoutput.hs
struct v2p {
float4 position : SV_POSITION;
float4 color : COLOR;
};
我们可以看到,这里面有一些奇怪的,标准C/C++不支持的东西。就是冒号后面的那些东西。这些东西是用来将变量和GPU的寄存器进行绑定的。因为GPU并不是全部可编程的,整个处理流水线当中混杂着可编程的环节和不可编程的环节。因此,当我们的输出要提供给不可编程的环节使用的时候(比如Vertex Shader的输出当中的position会被像素化模块用来插值计算三角形内部的点的坐标;比如Pixel Shader输出的color会被GPU的显示输出模块用来输出画面),就需要将这些变量绑定到一些事先定义好的寄存器当中去。
具体细节,请参考HLSL教程:
以及GPU渲染管道的说明:
https://en.m.wikipedia.org/wiki/Graphics_pipeline
在Windows当中,编译Shader(D3D规格)的方法如下:
fxc /T vs_5_0 /Zi /Fo copy.vso copy.vs
fxc /T ps_5_0 /Zi /Fo copy.pso copy.ps
如果找不到fxc.exe,那么应该是没有安装DirectX相关的开发包。重新运行Visual Studio选择安装就可以了。
代码编译的方法如下(调试版)(编译出现大量DirectXMath相关的错误的话,继续看下面):
D:wenliSourceReposGameEngineFromScratchPlatformWindows>clang-cl -I./DirectXMath/Inc -c -Z7 -o helloengine_d3d.obj helloengine_d3d.cpp
D:wenliSourceReposGameEngineFromScratchPlatformWindows>link -debug user32.lib d3d11.lib d3dcompiler.lib helloengine_d3d.obj
Release版:
D:wenliSourceReposGameEngineFromScratchPlatformWindows>clang -I./DirectXMath/Inc -l user32.lib -l d3d11.lib -l d3dcompiler.lib -o helloengine_d3d.exe helloengine_d3d.cpp
Direct 3D工作的方式与Direct 2D不同,不是采用COM的方式,(需要考证。从结果上来说确实不需要链接ole32.lib。但是有可能是D3D11的库里面包括了这一部分)而是直接调用显卡的驱动。所以我们去掉COM相关的初始化代码,链接的时候也去掉ole32.lib。
注意我们这里加入了一个新的头文件目录:./DirectXMath。这是因为目前随Visual Studio安装的(正确来说应该是随Windows SDK安装的)DirectXMath库似乎版本还是比较老的,不支持clang的编译(就是说用了clang所不支持的特性)。所以我们需要从github上面下载一个最新的版本:
D:wenliSourceReposGameEngineFromScratchPlatformWindows>git submodule add https://github.com/Microsoft/DirectXMath.git DirectXMath
D:wenliSourceReposGameEngineFromScratchPlatformWindows>git submodule init DirectXMath
D:wenliSourceReposGameEngineFromScratchPlatformWindows>git submodule update DirectXMath
然后重新编译应该就好了。
运行的效果如下:
三角形内部出现了漂亮的过渡色。这是因为我们程序给GPU的只有3个顶点,对于其它的点,GPU是根据它到3个顶点的距离(准确说是重心坐标,就是从被计算的点向3个顶点作辅助线,从而整个3角形被划分为3个小三角形(如果点在三角形边缘的时候会出现面积为0的塌缩三角形),每个小三角性的面积除以原本的大三角形的面积,得到3个[0,1]之间的值,根据这个值加权平均3个顶点的颜色)来进行插值运算的。这种插值运算不仅仅发生在position这个参数当中,也发生在color这个参数当中。所以最终就形成了这么一种结果。
好了。我们已经跨入了3D的殿堂了。保存我们的代码,准备下一个部分。
参考引用: