forked from LinkClinton/IGP-DirectX12-Chinese
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathChapter 6 Drawing in Direct3D.html
1012 lines (766 loc) · 67.1 KB
/
Chapter 6 Drawing in Direct3D.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<head>
<link rel="stylesheet" href="github-markdown.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.11.0/styles/default.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.8.3/katex.min.css">
<script src='https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/MathJax.js?config=TeX-MML-AM_CHTML'></script>
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
tex2jax: {
inlineMath: [ ['$','$'], ['\\(','\\)'] ]
}
});
</script>
</head>
<body class="markdown-body"></body>
<style>
.markdown-body {
box-sizing: border-box;
min-width: 200px;
max-width: 980px;
margin: 0 auto;
padding: 45px;
}
@media (max-width: 767px) {
.markdown-body {
padding: 15px;
}
}
</style>
<h1>Chapter 6 DRAWING IN DIRECT3D</h1>
<p>在之前的章节中,我们主要是讨论了渲染管道中的一些概念和一些数学知识。
在本章中,我们就转为主要讨论<code>Direct3D API</code>,从而来配置渲染管道,编写顶点着色器和像素着色器,以及提交图形到渲染管道并且绘制它。
在本章结束的时候,我们就能够成功绘制一个立方体了。</p>
<blockquote>
<p>目标:
- 了解一些用于定义,存储绘制图形的<code>Direct3D API</code>。
- 了解学习如何编写基础的顶点着色器和像素着色器代码。
- 了解如何使用管道状态来配置渲染管道。
- 了解如何创建一个常缓冲并且将其绑定到渲染管道中去,并且熟悉<code>Root Signature</code>(暂且叫做来源标记,主要是用于定义在着色器使用哪些资源)。</p>
</blockquote>
<h2>6.1 VERTICES AND INPUT LAYOUTS</h2>
<p>回顾5.5.1,在<code>Direct3D</code>中,顶点除了他的位置外它还可以附加一些其他数据。
为了创建一个自定义的顶点格式,我们需要创建一个结构体来描述我们要给顶点附加的数据。
下面就举出两个不同的顶点格式的例子,其中一个附加了位置和颜色数据,另外一个附加了位置,法向量,以及两个纹理坐标数据。</p>
<pre><code>
struct Vertex1
{
Float3 Pos;
Float4 Color;
};
struct Vertex2
{
Float3 Pos;
Float3 Normal;
Float2 Tex0;
Float2 Tex1;
};
</code></pre>
<p>我们虽然定义了顶点格式,但是我们同样需要告诉<code>Direct3D</code>我们的顶点格式,不然的话<code>Direct3D</code>是没法知道我们给顶点附加了哪些额外的数据。
我们使用<code>D3D12_INPUT_LAYOUT_DESC</code>来做这件事。</p>
<pre><code>
struct D3D12_INPUT_LAYOUT_DESC
{
const D3D12_INPUT_ELEMENT_DESC *pInputElementDescs;
UINT NumElements;
};
</code></pre>
<ul>
<li><code>pInputElementDescs</code>: 一个数组,告诉<code>Direct3D</code>我们的顶点附加的数据信息。</li>
<li><code>NumElements</code>: 数组的大小。</li>
</ul>
<p><code>pInputElementDescs</code>里面的每一个元素都相当于顶点格式的一个附加数据信息。
因此如果我们的一个顶点格式他有两个附加数据信息的话,那么这个数组的大小就需要是两个。</p>
<pre><code>struct D3D12_INPUT_ELEMENT_DESC
{
LPCSTR SemanticName;
UINT SemanticIndex;
DXGI_FORMAT Format;
UINT InputSlot;
UINT AlignedByteOffset;
D3D12_INPUT_CLASSIFICATION InputSlotClass;
UINT InstanceDataStepRate;
};
</code></pre>
<p><img src="Images/6.1.png" alt="Image6.1" /></p>
<ul>
<li><code>SemanticName</code>: 一个用于联系元素的字符串。他的值必须在合法的范围内。我们使用这个来将顶点中的元素映射到着色器的输入数据中去。参见图片6.1。</li>
<li><code>SemanticIndex</code>: 索引值,具体可以参见图片6.1。例如一个顶点可能会有不止一个纹理坐标,我们必须区分这些纹理坐标,因此我们就加入了索引值来区分一个顶点里面的多个纹理坐标。如果一个<code>semanticName</code>没有加上索引的话就默认为索引值为0。例如<code>POSITION</code>就是<code>POSITION0</code>。</li>
<li><code>Format</code>: 附加的数据信息的格式,类型是<code>DXGI_FORMAT</code>。</li>
<li><code>InputSlot</code>: 指定这个元素从哪个输入口进入,<code>Direct3D</code>支持16个输入口(0-15)输入顶点数据。对于现在来说,我们只使用0输入口。</li>
<li><code>AlignedByteOffset</code>: 内存偏移量,单位是字节。从顶点结构的开端到这个元素的开端的字节大小。下面是一个例子。
<code>C++
struct Vertex
{
Float3 Pos; // 0-byte offset
Float3 Normal; // 12-byte offset
Float2 Tex0; // 24-byte offset
Float2 Tex1; // 32-byte offset
};
</code></li>
<li><code>InputSlotClass</code>: 现在默认使用<code>D3D12_INPUT_PER_VERTEX_DATA</code>。另外的一个类型是用于<code>Instancing</code>技术的。</li>
</ul>
<p>这里我们给一个例子:</p>
<pre><code>struct Vertex
{
Float3 Pos; // 0-byte offset
Float3 Normal; // 12-byte offset
Float2 Tex0; // 24-byte offset
Float2 Tex1; // 32-byte offset
};
D3D12_INPUT_ELEMENT_DESC desc [] =
{
{“POSITION”, 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_PER_VERTEX_DATA, 0},
{“NORMAL”, 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D12_INPUT_PER_VERTEX_DATA, 0},
{“TEXCOORD”, 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D12_INPUT_PER_VERTEX_DATA, 0},
{“TEXCOORD”, 1, DXGI_FORMAT_R32G32_FLOAT, 0, 32, D3D12_INPUT_PER_VERTEX_DATA, 0}
};
</code></pre>
<h2>6.2 VERTEX BUFFERS</h2>
<p>为了能够让<code>GPU</code>访问一组顶点的数据信息,我们需要将顶点信息放入到<code>GPU</code>资源中去(<strong>ID3D12Resource</strong>),我们称之为缓冲。
我们将存储顶点数据的缓冲称之为顶点缓冲(<code>Vertex Buffer</code>)。
缓冲比纹理简单一些,他只有一维,并且他没有纹理明细(<code>MipMaps</code>),过滤器(<code>filters</code>),多重采样(<code>multisampling</code>)这些东西。
无论在什么时候,如果我们要给<code>GPU</code>提供一组数据信息例如顶点数据信息,我们都会使用缓冲来实现。</p>
<p>我们在之前讲过通过填充<code>D3D12_RESOURCE_DESC</code>类型来创建一个<code>ID3D12Resource</code>。
因此我们也会通过这样的方式来创建一个缓冲,即填充<code>D3D12_RESOURCE_DESC</code>结构来描述我们要创建的缓冲的属性,然后使用<code>ID3D12Device::CreateCommittedResource</code>创建缓冲。</p>
<p>在<code>d3dx12.h</code>中提供了一个简便的类型<code>CD3D12_RESOURCE_DESC</code>来创建资源,具体可以去参见<code>d3dx12.h</code>。</p>
<p><strong>注意我们在<code>Direct3D 12</code>中并没有定义一个具体的类型来表示这个资源是一个缓冲或者是一个纹理(<code>Direct3D 11</code>中是这样做的)。
因此我们在创建资源的时候需要通过<code>D3D12_RESOURCE_DESC::D3D12_RESOURCE_DIMENSION</code>来指定资源的类型。</strong></p>
<p>对于静态的图形(即每一帧中通常不会改变),我们会将他的顶点缓冲放在默认堆中(<strong>Default Heap</strong>)以保持最优的性能,通常大部分游戏中的图形都会放在默认堆中,例如树,建筑,地形,人物等。
因为我们在创建好它的顶点缓冲后,<code>GPU</code>只会读取里面的顶点信息来绘制它,而不会做其他的事情,因此将其放在默认堆里面是最好的。
然而由于<code>GPU</code>并不能将数据写入到处于默认堆里面的资源,我们该如何将初始的顶点数据放到缓冲中去?</p>
<p>为了实现这个,我们需要创建一个上传缓冲资源(<code>Upload Buffer</code>),它会被放在上传堆中(<strong>Upload Heap</strong>)。
回顾4.3.8章节,当我们要将数据从内存拷贝到显存中去的时候,我们提交了一个资源到上传堆中。
在我们创建完一个上传缓冲后,我们将顶点数据拷贝到上传缓冲中,然后我们从上传缓冲中拷贝顶点数据到我们的顶点缓冲中去。</p>
<p>之后的内容是一个例子,这里可以自己去看代码。我只将几个主要使用的结构体类型介绍下。</p>
<p><code>C++
struct D3D12_SUBRESOURCE_DATA
{
const void *pData;
LONG_PTR RowPitch;
LONG_PTR SlicePitch;
};
</code></p>
<ul>
<li><code>pData</code>: 数组的首元素指针。</li>
<li><code>RowPitch</code>: 对于缓冲来说他就是我们拷贝的数据的大小,单位字节。</li>
<li><code>SlicePitch</code>: 对于缓冲来说他就是我们拷贝的数据的大小,单位字节。</li>
</ul>
<p>注意的是,对于顶点缓冲来说,我们创建他的描述符是不需要将其放入描述符堆中的。</p>
<pre><code>struct D3D12_VERTEX_BUFFER_VIEW
{
D3D12_GPU_VIRTUAL_ADDRESS BufferLocation;
UINT SizeInBytes;
UINT StrideInBytes;
};
</code></pre>
<ul>
<li><code>BufferLocation</code>: 我们想要使用的顶点缓冲的虚拟地址。我们可以使用<code>ID3D12Resource::GetGPUVirtualAddress</code>来获取地址。</li>
<li><code>SizeInBytes</code>: 描述符可以只声明他只使用缓冲中的一部分数据,这个参数就是用来告诉我们要使用的缓冲大小,单位字节,从<code>BufferLocation</code>位置开始往后偏移。</li>
<li><code>StrideInBytes</code>: 每个顶点元素的大小,单位字节。</li>
</ul>
<p>在我们创建完缓冲和缓冲的描述符后,我们可以将他们绑定到渲染管道的输入口,然后在输入装配阶段将顶点缓冲输入进去。</p>
<pre><code> void ID3D12GraphicsCommandList::IASetVertexBuffers(
UINT StartSlot,
UINT NumBuffers,
const D3D12_VERTEX_BUFFER_VIEW *pViews);
</code></pre>
<ul>
<li><code>StartSlot</code>: 绑定的顶点缓冲的输入口的起始位置,总共有16个,范围在0-15之间。</li>
<li><code>NumBuffers</code>: 我们要绑定的顶点缓冲的个数,我们假设我们绑定n个顶点缓冲,输入口的起始位置是k,那么第i个顶点缓冲的输入口就是<code>k + i - 1</code>。</li>
<li><code>pViews</code>: 我们要绑定的顶点缓冲的描述符数组的首元素的地址。</li>
</ul>
<blockquote>
<p>官网中说的是DX12最大支持的输入口个数是32。</p>
</blockquote>
<p>由于要支持多个顶点缓冲从任意一个输入口输入数据,这个函数设计的就有点复杂了。
但是在这里我们只使用一个输入口。在章节最后的练习中我们会使用到两个输入口。</p>
<p>只有当我们改变这个输入口绑定的顶点缓冲的时候,原本的顶点缓冲才会取消绑定。
因此我们虽然只使用一个输入口但是我们仍然可以使用多个顶点缓冲。例如这样。</p>
<pre><code> D3D12_VERTEX_BUFFER_VIEW_DESC BufferView1;
D3D12_VERTEX_BUFFER_VIEW_DESC BufferView2;
/*Create Vertex Buffer Views*/
commandList->IASetVertexBuffers(0, 1, &BufferView1);
/*Draw by using VertexBuffer1*/
commandList->IASetVertexBuffers(0, 1, &BufferView2);
/*Draw by using VertexBuffer2*/
</code></pre>
<p>绑定一个顶点缓冲到输入口并不代表我们绘制了这个缓冲,我们只是准备好让顶点输入到渲染管道中而已。
因此最后我们还需要使用绘制函数来绘制这些顶点。</p>
<pre><code> void ID3D12CommandList::DrawInstanced(
UINT VertexCountPerInstance,
UINT InstanceCount,
UINT StartVertexLocation,
UINT StartInstanceLocation);
</code></pre>
<ul>
<li><code>VertexCountPerInstance</code>: 我们需要绘制的顶点个数(对于每个实例来说)。</li>
<li><code>InstanceCount</code>: 我们要绘制的实例个数,这里我们设置为1。</li>
<li><code>StartVertexLocation</code>: 指定从顶点缓冲中的第几个顶点开始绘制。</li>
<li><code>StartInstanceLocation</code>: 这里我们设置为0。</li>
</ul>
<p><code>VertexCountPerInstance</code>和<code>StartVertexLocation</code>一起决定了我们要绘制顶点缓冲中的哪个范围。
图片<a href="#Image6.2">6.2</a>给出了例子。</p>
<p><img src="Images/6.2.png" alt="Image6.2" /></p>
<p><code>StartVertexLocation</code>指定了我们要绘制第一个顶点在顶点缓冲中的位置,<code>VertexCountPerInstance</code>指定了我们要绘制的顶点个数。</p>
<p><code>DrawInstanced</code>并没有让我们指定我们绘制的时候使用的图元的类型。
因此我们需要使用<code>ID3D12GraphicsCommandList::IASetPrimitiveTopology</code>去设置。</p>
<h2>6.3 INDICES AND INDEX BUFFERS</h2>
<p>和顶点类似,为了能够让<code>GPU</code>能够访问到索引数据,我们同样需要将索引数据放到缓冲中去。
我们称之为索引缓冲。索引缓冲的创建方式和顶点缓冲是一样的,因此这里就不再讨论了。</p>
<p>我们同样需要将索引缓冲绑定到渲染管道中去,因此我们也要为索引缓冲创建描述符,并且和顶点缓冲一样,我们不需要使用到描述符堆。</p>
<pre><code>struct D3D12_INDEX_BUFFER_VIEW
{
D3D12_GPU_VIRTUAL_ADDRESS BufferLocation;
UINT SizeInBytes;
DXGI_FORMAT Format;
};
</code></pre>
<ul>
<li><code>BufferLocation</code>: 和顶点缓冲的一样。</li>
<li><code>SizeInBytes</code>: 和顶点缓冲的一样。</li>
<li><code>Format</code>: 一个索引占据的字节大小,必须设置为<code>DXGI_FORMAT_R16_UINT</code>或者<code>DXGI_FORMAT_R32_UINT</code>。</li>
</ul>
<p>和顶点缓冲一样,以及其他的<code>Direct3D</code>资源我们如果想要使用他们的话,一般都需要将其绑定到渲染管道中去。
这里我们同样也需要将索引缓冲绑定到和顶点缓冲一样的阶段(使用<code>ID3D12CommandList::SetIndexBuffer</code>),即输入装配阶段。</p>
<p>下面的代码是一个例子,你可以去自己看看。</p>
<p>如果你需要使用索引缓冲的话,我们就不能够使用<code>DrawInstanced</code>来绘制图形了,而必须使用<code>DrawIndexedInstanced</code>来绘制。</p>
<pre><code> ID3D12GraphicsCommandList::DrawIndexedInstanced(
UINT IndexCountPerInstance,
UINT InstanceCount,
UINT StartIndexLocation,
INT BaseVertexLocation,
UINT StartInstanceLocation);
</code></pre>
<ul>
<li><code>IndexCountPerInstance</code>: 我们绘制的时候使用的索引个数。</li>
<li><code>InstanceCount</code>: 实例个数,这里我们设置为1。</li>
<li><code>StartIndexLocation</code>: 指定从顶点缓冲中的哪个索引位置开始绘制。</li>
<li><code>BaseVertexLocation</code>: 指定我们绘制的时候使用的第一个顶点在顶点缓冲中的位置,即在顶点缓冲中这个顶点之前的顶点我们并不使用,我们从这个顶点开始重新编号。</li>
<li><code>StartInstanceLocation</code>: 我们这里设置为0。</li>
</ul>
<p>假设我们有一个球体,一个长方体,一个圆柱体。
首先每个物体都有他自己的顶点缓冲和索引缓冲。
然后我们将每个物体的索引缓冲和顶点缓冲连接起来,即拼接起来变成一个索引缓冲和一个顶点缓冲,暂且叫做全局缓冲吧,可以参见图片<a href="#Image6.3">6.3</a>(将顶点缓冲和索引缓冲拼起来也有一些不好的地方,当我们需要改变其中一个物体的顶点缓冲或者索引缓冲的时候,性能开销会比分开来说要多,但是在大多数情况下来说并起来后的优化会比分开来说大)。
在将它们拼接起来后,那么就会出现一个问题,我们索引缓冲中存储的索引值是相对于原本的那个顶点缓冲来说的,它里面记录的顶点编号也是相对于原本的顶点缓冲来说的,但是我们这里将顶点缓冲拼接起来后,肯定有一些顶点的编号是改变的了。
因此我们需要重新计算索引缓冲。</p>
<p><img src="Images/6.3.png" alt="Image6.3" /></p>
<p>值得庆幸的是我们的顶点缓冲和索引缓冲并不是分散的,一个物体的顶点缓冲或者索引缓冲都是全局缓冲中的一段缓冲。
因此我们能够很容易的就知道一个物体它的顶点缓冲的范围和索引缓冲的范围。
但是由于索引缓冲他的值是相对于原本的顶点缓冲来说的,如果我们要重新计算索引缓冲的话显然并不划算,但是如果我们假设这个索引缓冲对应的顶点缓冲的第一个顶点的编号在全局缓冲中的编号为0的话,那么就刚好可以对上。
因此我们就有了<code>BaseVertexLocation</code>参数来将全局缓冲中的一个顶点的编号暂时看作为0。</p>
<pre><code> commandList->DrawIndexedInstanced(numSphereIndices, 1, 0, 0, 0);
commandList->DrawIndexedInstanced(numBoxIndices, 1,
firstBoxIndex, firstBoxVertexPos, 0);
commandList->DrawIndexedInstanced(numCylIndices, 1,
firstCylIndex, firstCylVertexPos, 0);
</code></pre>
<h2>EXAMPLE VERTEX SHADER</h2>
<pre><code>
cbuffer PerObject : register(b0)
{
float4x4 gWorldViewProj;
};
void VsMain(float3 pos : POSITION,
float4 inputColor : COLOR,
out float4 posH : SV_POSITION,
out float4 outColor : COLOR)
{
posH = mul(float4(pos, 1.0f), gWorldViewProj);
outColor = inputColor;
}
</code></pre>
<p>我们使用<strong>HLSL</strong>(<strong>High Level Shading Language</strong>)语言来编写着色器,他和<strong>C++</strong> 非常类似,因此非常容易就能够学会。
我们会在阅读过程中会逐步学习关于HLSL着色器的一些概念,从而我们能够自己来实现一些简单的演示程序。
我们通常会使用<code>.hlsl</code>格式来表示这个文件是一个着色器代码。</p>
<p>在例子中,顶点着色器就是叫做<code>VSMain</code>的函数,我们在顶点着色器阶段就会运行这个函数。
你可以给你的顶点着色器取任何合法的函数名,我们只需要在编译的时候指定入口点函数就好了。
我们可以看到例子中的顶点着色器有4个参数,前面两个是输入的参数,后面两个是输出的参数(使用<strong>out</strong>标志)。
在HLSL中是没有任何引用和指针的,所以要想一个函数返回多个参数的话你要么使用结构体要么就使用<strong>out</strong>关键词来声明一个参数是输出参数。
并且要注意,在<strong>HLSL</strong>中所有的函数都是<code>inline</code>的。</p>
<p>我们可以知道我们前面两个输入的参数和我们绘制的时候使用的顶点格式是向对应的,在参数后面的<code>POSITION</code>,<code>COLOR</code>就是用来将顶点中的数据映射到顶点着色器的参数中去。</p>
<p><img src="Images/6.5.png" alt="Image6.4" /></p>
<p>后面的两个输出参数后面的<code>SV_POSITION</code>,<code>COLOR</code>则是用来将这个阶段输出的数据映射到下一个阶段(例如几何着色器和像素着色器)输入的数据中去。
需要注意的是<code>SV_POSITION</code>是稍微有一点特殊的(<strong>SV</strong>表示的意思是<strong>System Value</strong>)。
它表示我们的顶点在齐次裁剪空间的位置,我们必须使用这个标志输出的顶点位置而不是<code>POSITION</code>。
因为<code>GPU</code>是需要知道这些顶点的位置的,从而才能够进行一些需要顶点位置信息的操作,例如裁剪,深度测试以及光栅化。
除了系统值外我们需要使用指定的标志,其余的标志名我们是可以任意取的,只需要你到时候能够对应上就好了。</p>
<p>第一行代码中,我们做的就是使用一个矩阵将顶点坐标从模型空间转换到齐次裁剪空间。</p>
<pre><code> posH = mul(float4(pos, 1.0f), gWorldViewProj);
</code></pre>
<p><code>float4</code>既是数据类型也是构造函数,<code>mul</code>则有多个重载,支持各种类型的乘法。</p>
<p>然后一行代码我们只是将输入的颜色作为输出的颜色而已。</p>
<pre><code> outColor = inputColor;
</code></pre>
<p>我们同样也可以在着色器中加入结构体来充当输入和输出的参数,只要能够互相对应就可以了。</p>
<pre><code>cbuffer PerObject : register(b0)
{
float4x4 gWorldViewProj;
};
struct InputVertex
{
float3 pos : POSITION;
float4 inputColor : COLOR;
};
struct OutputVertex
{
float4 posH : SV_POSITION;
float4 outColor : COLOR;
};
OutputVertex VsMain(InputVertex input)
{
OutputVertex output;
output.posH = mul(float4(input.pos, 1.0f), gWorldViewProj);
output.outColor = input.inputColor;
return output;
}
</code></pre>
<p>如果我们没有使用几何着色器的话,我们就必须在顶点着色器中输出顶点在齐次裁剪空间中的位置,即必须有一个分量是<code>SV_POSITION</code>标志。
如果我们有使用几何着色器的话,那么我们就可以在几何着色器中输出。</p>
<p>透视除法并不需要我们去做,通常会由硬件来做。</p>
<h2>6.5 EXAMPLE PIXEL SHADER</h2>
<p>我们之前在<strong>5.10.3</strong>中讲了在光栅化的时候每个三角形的像素的一些属性都会由对应的顶点(<strong>由顶点着色器或者几何着色器输出的顶点</strong>)属性插值而来。
然后这些插值计算出来的属性会作为像素着色器的输入数据。
这里我们默认我们没有使用几何着色器。图片<a href="#Image6.5">6.5</a>将会介绍他们的关联。</p>
<p><img src="Images/6.5.png" alt="Image6.5" /></p>
<p>像素着色器类似顶点着色器,他也同样是一个函数。
并且我们会对每个像素运行一次来计算出这个像素的颜色。
但是你需要注意这里我们处理的像素并不是最后放到后台缓冲中的像素,这些像素可能会被<code>Clip</code>函数裁剪掉,或者被另外一个深度值更小的像素覆盖。</p>
<p>一些已经确定不可能出现在后台缓冲中的像素会被硬件直接优化掉,即直接不对这个像素运行像素着色器。
例如没有通过深度测试的像素就没有必要去运行像素着色器了,因为无论如何这个像素都不会被写入到后台缓冲中去。</p>
<pre><code>cbuffer PerObject : register(b0)
{
float4x4 gWorldViewProj;
};
void VsMain(float3 pos : POSITION,
float4 inputColor : COLOR,
out float4 posH : SV_POSITION,
out float4 outColor : COLOR)
{
posH = mul(float4(pos, 1.0f), gWorldViewProj);
outColor = inputColor;
}
float4 PsMain(float4 posH : SV_POSITION,
float4 color : COLOR) : SV_TARGET
{
return color;
//lol, 原文这里返回的是pin.Color,我简直是醉了。
}
</code></pre>
<p>在上面的例子中,我们就直接了返回插值后的颜色。
并且你需要注意的是像素着色器的输入参数要和顶点着色器的输出参数一致。
通常来说像素着色器的返回值是一个4维向量,<code>SV_TARGET</code>标志意味着我们的返回值的类型要能够和<code>Render Target</code>的格式一致。</p>
<p>我们同样也可以使用结构体来代替上面的顶点和像素着色器的输入输出数据。</p>
<pre><code>cbuffer cbPerObject : register(b0)
{
float4x4 gWorldViewProj;
};
struct VertexIn
{
float3 Pos : POSITION;
float4 Color : COLOR;
};
struct VertexOut
{
float4 PosH : SV_POSITION;
float4 Color : COLOR;
};
VertexOut Vs(VertexIn inPut)
{
Vertex outPut;
outPut.posH = mul(float4(inPut.Pos, 1.0f)), gWorldViewProj);
outPut.Color = inPut.Color;
return outPut;
}
float4 Ps(VertexOut inPut) : SV_Target
{
return inPut.Color;
}
</code></pre>
<h2>6.6 CONSTANT BUFFERS</h2>
<h3>6.6.1 Creating Constant Buffers</h3>
<p>常缓冲就是一种能够被着色器程序引用的<code>GPU</code>资源。
在我们之后的学习中,我们会知道纹理以及其他类型的缓冲资源都能够被着色器程序引用。</p>
<pre><code>cbuffer cbPerObject : register(b0)
{
float4x4 gWorldViewProj;
}
</code></pre>
<p>这里我们定义了一个叫做<code>cbPerObject</code>的<code>cbuffer</code>(<strong>Constant Buffer</strong>)。
在这里,我们的常缓冲中存储了一个4x4的矩阵,将世界变换矩阵,视角矩阵以及投影矩阵组合起来了,用于将一个顶点从模型空间中转换到齐次裁剪空间中去。</p>
<p>不像顶点和索引缓冲,常缓冲通常需要在每一帧的时候都通过<code>CPU</code>去更新数据。
例如,如果我们的摄像机在每一帧的时候都在移动,那么我们就需要在每一帧中使用新的视角矩阵更新我们的缓冲。
因此我们创建常缓冲的时候可以将他放到上传堆(<strong>Upload Heap</strong>)而不是默认堆中,这样的话我们就可以直接使用<code>CPU</code>去更新常缓冲了。</p>
<p>注意的是,常缓冲的空间大小必须是硬件能够分配的最小的空间大小的倍数,即<strong>256bytes</strong>的倍数。</p>
<p>我们通常可能需要使用多个同样类型的缓冲,例如我们之前代码里的那个缓冲,我们每个不同的物体都需要使用到。</p>
<p>在<code>Direct3D 12</code>中我们可以使用<code>Shader Model 5.1</code>,因此我们可以使用下面的方法定义一个常缓冲。</p>
<pre><code>struct ObjectConstants
{
float4x4 gWorldViewProj;
uint matIndex;
};
ConstantBuffer<ObjectConstants> gObjConstants : register(b0);
</code></pre>
<h3>6.6.2 Updating Constant Buffers</h3>
<p>由于我们的常缓冲是创建在上传堆中的,因此我们可以直接用<code>CPU</code>中更新缓冲。
具体来说我们需要先获取资源数据的指针,我们可以使用<code>Map</code>函数来获取。</p>
<pre><code> ComPtr<ID3D12Resource> uploadBuffer;
BYTE* data = nullptr;
uploadBuffer.Map(0, nullptr, reinterpret_cast<void**>(&data));
</code></pre>
<p><code>Map</code>函数的第一个参数就是表示我们要获取的是资源中第几个子资源的指针。
对于缓冲来说,他只有一个子资源就是他自己,因此我们就设置为0。
第二个参数的类型是<code>D3D12_RANGE</code>来表示我们要映射的内存范围,设置为<code>null</code>的话就代表我们要映射的是整个资源。
第三个参数的话就是返回我们要的资源的指针。</p>
<p>我们可以使用下面的方法将数据复制到常缓冲中去:</p>
<pre><code> memcpy(data, &SourceData, DataSize);
</code></pre>
<p>当我们完成更新不需要再更新的时候,我们应该使用<code>Unmap</code>函数去释放内存。</p>
<pre><code> if (uploadBuffer != nullptr)
uploadBuffer->Unmap(0, nullptr);
data = nullptr;
</code></pre>
<p>第一个参数是我们要对哪个子资源进行释放,对于缓冲来说我们只需要设置为<strong>0</strong>就好了。
第二个参数的类型是<code>D3D12_RANGE</code>表示这个子资源中我们要释放的内存范围,设置为<code>nullptr</code>表示整个资源。</p>
<h3>6.6.3 Upload Buffer Helper</h3>
<p>Nullptr!!!</p>
<h3>6.6.4 Constant Buffer Descriptors</h3>
<p>回顾4.1.6,我们是通过描述符将资源绑定到渲染管道中去的。
到现在为止我们绑定渲染目标(<strong>Render Target</strong>),深度模板缓冲(<strong>Depth/Stencil Buffer</strong>)以及顶点和索引缓冲(<strong>Vertex/Index Buffer</strong>)都是使用的描述符。
因此我们要绑定常缓冲到管道上去的话还是需要创建描述符。
常缓冲的描述符在的描述符堆的类型是<code>D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV</code>,也就是说这个堆能够存储常缓冲(<strong>Constant Buffer</strong>),着色器资源(<strong>Shader Resource</strong>)和无序资源(<strong>unordered access</strong>)的描述符。
为了存储这些描述符,我们需要创建一个这样的类型的堆来存储。</p>
<pre><code> D3D12_DESCRIPTOR_HEAP_DESC desc;
desc.NumDescriptors = 1;
desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
desc.NodeMask = 0;
ComPtr<ID3D12DescriptorHeap> cbvHeap;
device->CreateDescriptorHeap(&desc, IID_PPV_ARGS(&cbvHeap));
</code></pre>
<p>虽然创建这个类型的堆的代码和我们之前创建其他类型的堆的代码是差不多的,但是我们需要注意设定<code>D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE</code>这个标志(<code>Flags</code>)来声明这个堆里面的描述符能够被着色器程序访问。</p>
<p>我们需要填充<code>D3D12_CONSTANT_BUFFER_VIEW_DESC</code>结构才可以创建常缓冲描述符,然后使用<code>ID3D12Device::CreateConstantBufferView</code>函数创建它。</p>
<pre><code> D3D12_CONSTANT_BUFFER_VIEW_DESC desc;
desc.BufferLocation = bufferVisualAddress;
desc.SizeInBytes = bufferSize;
device->CreateConstantBufferView(&desc,
heap->GetCPUDescriptorHandleForHeapStart());
</code></pre>
<p>我们通常使用<code>D3D12_CONSTANT_BUFFER_VIEW_DESC</code>来将缓冲中的一个子资源或者一个缓冲绑定到着色器中对应的常缓冲结构体中。
之前我们也提到过一个常量缓冲可以存储多个物体的数据信息,具体使用某一个资源的时候,我们可以使用<code>BufferLocation</code>以及<code>SizeInBytes</code>来做到。
并且你需要注意<code>D3D12_CONSTANT_BUFFER_VIEW_DESC::SizeInBytes</code>和<code>D3D12_CONSTANT_BUFFER_VIEW_DESC::OffsetInBytes</code>的大小必须是<code>256bytes</code>的倍数。</p>
<h3>6.6.5 Root Signature and Descriptor Tables</h3>
<p>通常来说,不同的着色器程序在绘制指令执行前需要绑定到管道的资源都会有所不同。
并且资源一般来说都会被绑定到管道中的一个具体的输入点(<strong>register slots</strong>),这样我们就可以通过着色器访问被绑定的资源了。</p>
<p>我们之前使用的着色器都只使用了一个常缓冲而已,但在之后我们会使用到更多的资源,例如纹理,采样器等,这些都是需要绑定到管道上的。</p>
<pre><code>SamplerState gsamPointWrap : register(s0);
SamplerState gsamPointClamp : register(s1);
SamplerState gsamLinearWrap : register(s2);
SamplerState gsamLinearClamp : register(s3);
SamplerState gsamAnisotropicWrap : register(s4);
SamplerState gsamAnisotropicClamp : register(s5);
cbuffer PerObject : register(b0)
{
float4x4 gWorld;
float4x4 gTexTransform;
};
cbuffer Pass : register(b1)
{
float4x4 gView;
float4x4 gProj;
//Other
};
cbuffer Material : register(b2)
{
float4 gDiffuseAlbedo;
float3 gFresnelR0;
float gRoughness;
float4x4 gMatTransform;
}
</code></pre>
<p>来源标记(<strong>Root Signature</strong>)存储我们在绘制指令执行前要绑定到渲染管道的资源有哪些,以及这些资源将会被映射到着色器的哪个输入寄存器中。
来源标记存储的内容必须和我们使用的着色器内容相吻合(即在绘制指令执行之前,我们必须在来源标记中声明那些被着色器声明以及使用的资源)。
我们将会在创建渲染管道的时候验证两者的内容是否吻合。注意不同的绘制指令可能需要不同的着色器程序以及不同的来源标记。</p>
<p>我们可以假设着色器程序是一个函数,在着色器中使用的资源是参数,那么我们可以认为来源标记就是用来声明参数的。</p>
<p>在<code>Direct3D</code>中我们使用<code>ID3D12RootSignature</code>来表示一个来源标记。
它将存储一组来源参数来描述我们要在着色器中使用哪些资源。
一个来源参数可以是常量,描述符或者描述符表。
我们在之后会讲到常量和描述符,但是在本章中我们会使用到描述符表。
描述符表是在描述符堆的基础上,指定堆的一段范围作为我们要使用的一组描述符。</p>
<pre><code>//这里给出大致代码
D3D12_DESCRIPTOR_RANGE cbvRange;
D3D12_ROOT_PARAMETER rootParameter[1];
cbvRange.RangeType =
cbvRange.NumDescriptors = 1;
cbvRange.BaseShaderRegister = 0;
rootParameter.ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;
rootParameter.ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL;
rootParameter.DescriptorTable.NumDescriptorRanges = 1;
rootParameter.DescriptorTable.pDescriptorRanges = &cbvRange;
//代码先坑着,网速不好没法上MSDN看参数。
</code></pre>
<p>之前说过来源标记只是定义了我们要绑定到渲染管道的资源。
并不意味我们需要在创建它的时候就将资源绑定到渲染管道上。
我们会使用指令列表来设置我们要使用的资源,准确来说是函数<code>ID3D12GraphicsCommandList::SetGraphicsRootDescriptorTable</code>。</p>
<pre><code> void ID3D12GraphicsCommandList::SetGraphicsRootDescriptorTable(
UINT RootParameterIndex,
D3D12_GPU_DESCRIPTOR_HANDLE BaseDescriptor);
</code></pre>
<ul>
<li><code>RootParameterIndex</code>: 我们要设置到哪个来源参数中去。</li>
<li><code>BaseDescriptor</code>: 描述符在描述符堆中的位置,表示我们要设置的描述符表的第一个元素。例如,我们指定了一个有5个描述符的描述符表,那么<code>BaseDescriptor</code>和在他后面4个的描述符就会作为一个描述符表被设置。</li>
</ul>
<p>下面的代码我们会设置一个<code>CBV</code>堆和一个描述符表到管道。</p>
<pre><code> commandList->SetGraphicsRootSignature(...);
commandList->SetDescriptorHeaps(descriptorHeapsCount, descriptorHeaps);
D3DX12_GPU_DESCRIPTOR_HANDLE cbvPosition = descriptorHeaps->GetGPUDescriptorHandleForHeapStart() + offset;
commandList->SetGraphicsRootDescriptorTable(0, cbvPosition);
</code></pre>
<blockquote>
<p>为了性能考虑,建议不要创建太大的来源标记,以及不要过于频繁更换来源标记。
当你更换一个来源标记的时候,你原本绑定的资源全部都会被取消绑定,你需要重新绑定。</p>
</blockquote>
<h2>6.7 COMPILING SHADERS</h2>
<p>在<code>Direct3D</code>中着色器程序首先需要编译成较为简便的字节码。
然后图形驱动会将编译后的字节码重新编译成<code>GPU</code>的指令。
我们可以在程序运行的时候使用下面的函数去编译我们的着色器程序。</p>
<pre><code> HRESULT D3DCompileFromFile(
LPCWSTR pFileName,
const D3D_SHADER_MACRO *ppDefines,
ID3DInclude *pInclude,
LPCSTR pEntrypoint,
LPCSTR pTarget,
UINT Flags1,
UINT Flags2,
ID3DBlob **ppCode,
ID3DBlob **ppErrorMsgs);
</code></pre>
<ul>
<li><code>pFileName</code>: 我们要编译的着色器代码所在的文件名。</li>
<li><code>pDefines</code>: 我们目前不需要使用,如果你需要使用的话,参见SDK文档。</li>
<li><code>pInclude</code>: 同上。</li>
<li><code>pEntrypoint</code>: 我们要使用的着色器程序的名字,因为本身来说一个着色器程序就是一个函数,因此就是对应的函数名,我们可以在一个着色器代码文件里面写多个着色器。</li>
<li><code>pTarget</code>: 指定我们的着色器的类型和版本,具体参数参见文档。</li>
<li><code>Flags1</code>: 指定我们要如何编译着色器,参数很多。我们这里主要使用<code>D3DCOMPILE_DEBUG</code>和<code>D3DCOMPILE_SKIP_OPTIMIZATION</code>用来方便我们进行调试。</li>
<li><code>Flags2</code>: 我们目前不需要使用。</li>
<li><code>ppCode</code>: 返回编译后的字节码,类型<code>ID3DBlob</code>。</li>
<li><code>ppErrorMsgs</code>: 如果编译错误,返回编译的错误信息,类型<code>ID3DBlob</code>。</li>
</ul>
<p><code>ID3DBlob</code>本质上就是一块内存,我们可能需要使用到下面的两个成员函数。</p>
<ul>
<li><code>LPVOID GetBufferPointer</code>: 返回这块内存的头指针。</li>
<li><code>SIZE_T GetBufferSize</code>: 返回这块内存的大小,单位<code>byte</code>。</li>
</ul>
<h3>6.7.1 Offline Compilation</h3>
<p>除了在运行的时候编译着色器程序,我们也可以预先编译好它。</p>
<p>下面是一些为什么要预先编译的理由:</p>
<ul>
<li>编译着色器程序需要花费一段时间,预先编译的话就可以减少这段时间。</li>
<li>预先编译的话能够更好的去调试我们的着色器程序,以及更早的知道我们的着色器程序的错误。</li>
<li>Windows 8 应用商店程序只能使用预先编译好的着色器程序。</li>
</ul>
<p>编译后的文件格式会变成<code>.cso</code>。</p>
<p>我们通常<code>DirectX</code>附带的<code>FXC</code>工具来预先编译我们的着色器程序。<code>FXC</code>是一个控制台工具,因此我们需要键入一些指令来编译我们的着色器。</p>
<p>下面的例子是我们使用<code>Debug</code>模式编译我们的着色器程序。</p>
<p>```command line
fxc ".hlsl" /Od /Zi /T vs_5_0 /E "entryPoint" /Fo ".cso" /Fc ".asm"</p>
<pre><code>fxc ".hlsl" /Od /Zi /T ps_5_0 /E "entryPoint" /Fo ".cso" /Fc ".asm"
</code></pre>
<pre><code>
下面的例子是编译成`Release`模式的。
```command line
fxc ".hlsl" /Od /Zi /T vs_5_0 /E "entryPoint" /Fo ".cso" /Fc ".asm"
fxc ".hlsl" /Od /Zi /T ps_5_0 /E "entryPoint" /Fo ".cso" /Fc ".asm"
</code></pre>
<table>
<thead>
<tr>
<th>参数</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td>/Od</td>
<td>禁止优化,通常用于调试。</td>
</tr>
<tr>
<td>/Zi</td>
<td>允许调试信息。</td>
</tr>
<tr>
<td>/T "string"</td>
<td>着色器的类型和版本。</td>
</tr>
<tr>
<td>/E "string"</td>
<td>着色器的入口点函数名。</td>
</tr>
<tr>
<td>/Fo "string"</td>
<td>编译完成后的文件名。</td>
</tr>
<tr>
<td>/Fc "string"</td>
<td>输出一个用于调试,检测指令数,以及了解代码是如何生成的文件。</td>
</tr>
</tbody>
</table>
<p>如果你编译的着色器程序有语法错误的话,FXC将会输出错误或者警告信息。例如我们编译的代码中有着一段:</p>
<pre><code> //worldViewProj is not exist!!
output.posH = mul(float4(input.pos, 1.0f)worldViewProj);
</code></pre>
<p>然后他就会输出这样的错误:</p>
<p>```command line
xxx.hlsl(29,42-54): error X3004: undeclared identifier "worldViewProj"</p>
<pre><code>xxx.hlsl(29,14-55): error X3013: 'mul': no matching 2 parameter intrinsic function
...
</code></pre>
<pre><code>
虽然我们能够预先编译好了着色器代码,但是我们还是需要读入代码到我们的程序中去,我们可以使用输入输出流在完成这个工作。
```C++
std::ifstream file(fileName, std::ios::binary);
file.seekg(0, std::ios_base::end);
std::ifstream::pos_type size = (int)file.tellg();
file.seekg(0, std::ios_base::beg);
ComPtr<ID3DBlob> blob;
ThrowIfFailed(D3DCreateBlob(size, blob.GetAddressOf()));
file.read((char*)blob->GetBufferPointer(), size);
file.close();
</code></pre>
<h3>6.7.2 Generated Assembly</h3>
<p><code>/Fc</code> 参数将会告诉<code>FXC</code>生成一段汇编代码。
在调试或者其他时候,时不时看下着色器的汇编码能够很好的帮助我们检查着色器的指令数,以及了解代码如何生成的,并且生成代码是有可能和你预想的不一样的。例如,如果你的代码里面有一个条件语句,你可能会觉得在编译后的代码中会是一个分支指令。
但是在早期的可编程式<code>GPU</code>上在着色器上使用分支结构的开销是非常大的。因此有时候编译器就会将一个分支语句变成一种插值形式从而减少开销的同时达到一样的结果。</p>
<blockquote>
<p>这里是原始代码。</p>
</blockquote>
<pre><code> float x = 0;
if (s == 1)
x = sqrt(y);
else
x = 2 * y;
</code></pre>
<blockquote>
<p>这里是可能编译的时候生成的代码。</p>
</blockquote>
<pre><code>float a = 2 * y;
float b = sqrt(y);
float x = a + s * (b - a);
//这样的话,当s是1的时候,x = b,否则x = a。
//效果是一样的。
</code></pre>
<p>我们从上面的代码可以知道编译器帮我们做了这些事情,但是如果我们不去看汇编码的话,我们是不会知道他做了这些事情的。
因此有时候去详细看看汇编码才可以知道我们的代码到底会变成什么样子。</p>
<h3>6.7.3 Using Visual Studio to Compile Shaders Offline</h3>
<p>Visual Studio 2013以上可以直接编译我们的着色器。你只需要将你的<code>.hlsl</code>格式的文件加入到你的工程里面(<strong>应该只支持C++工程</strong>)去,然后Visual Studio识别他们并且提供编译的设置(可以参见图片6.6)。这些设置其实就是将<code>FXC</code>工具的参数UI化了而已。
当你将的你的着色器代码加入到工程后,Visual Studio将会在编译过程中也会使用<code>FXC</code>去编译我们的着色器代码。</p>
<p><img src="Images/6.6.png" alt="Image6.6" /></p>
<p>但是使用Visual studio去编译着色器的话就有一个缺点,就是一个文件只能够支持一个着色器程序,即我们不能将多个着色器程序放到一个文件里面去了。</p>
<h2>6.8 RASTERIZER STATE</h2>
<p>虽然渲染管道有很多部分都是可编程的,但是还是有一些部分只允许我们设置参数,而不是可编程。例如关于光栅化部分,我们就不能编写代码,而是必须使用<code>D3D12_RASTERIZER_DESC</code>结构来设置我们的渲染管道的光栅化阶段。</p>
<pre><code> struct D3D12_RASTERIZER_DESC {
D3D12_FILL_MODE FillMode; //Default: D3D12_FILL_SOLID
D3D12_CULL_MODE CullMode; //Default: D3D12_CULL_BACK
BOOL FrontCounterClockwise; //Default: false
INT DepthBias; //Default: 0
FLOAT DepthBiasClamp; //Default: 0.0f
FLOAT SlopeScaledDepthBias; //Default: 0.0f
BOOL DepthClipEnable; //Default: true
BOOL ScissorEnable; //Default: false
BOOL MultisampleEnable; //Default: false
BOOL AntialiasedLineEnable; //Default: false
UINT ForcedSampleCount; //Default: 0
D3D12_CONSERVATIVE_RASTERIZATION_MODE ConservativeRaster; //Default: D3D12_CONSERVATIVE_RASTERIZATION_MODE_OFF
}
</code></pre>
<p>大部分参数我们都不会经常使用。如果你想要了解所有的参数的话,建议去翻看SDK文档。我们这里只介绍4个常用的。</p>
<ul>
<li><code>FillMode</code>: 指定绘制的模式。<code>D3D12_FILL_WIREFRAME</code>表示线框模式,<code>D3D12_FILL_SOLID</code>表示实体模式。</li>
<li><code>CullMode</code>: 指定剔除模式。<code>D3D12_CULL_NONE</code>表示不进行剔除,<code>D3D12_CULL_BACK</code>表示进行背面剔除,<code>D3D12_CULL_FRONT</code>表示进行正面剔除。</li>
<li><code>FrontCounterClockwise</code>: <code>false</code>表示顺时针方向的三角形会认为是正面,逆时针方向的三角形会变成反面。<code>true</code>这相反。需要注意的是所谓的顺时针方向和逆时针方向都是相对摄像机来说的。</li>
<li><code>ScissorEnable</code>: <code>true</code>表示开启在4.3.10提到过的剪裁测试,<code>false</code>表示关闭。</li>
</ul>
<h2>6.9 PIPELINE STATE OBJECT</h2>
<p>到现在为止,我们已经介绍了如何描述输入的顶点格式,如何创建一个顶点着色器和一个像素着色器,如何设置光栅化阶段状态。然而我们并没有介绍如何绑定这些东西到我们的渲染管道中去。这里我们就定义了一个叫做管道状态(<code>PSO, Pipeline State Object</code>)的东西来将上面的东西集合在一起,然后统一设定到渲染管道上去。在<code>Direct3D</code>中对应<code>ID3D12PipelineState</code>接口。</p>
<p>如果要创建一个管道状态的话,我们必须填充<code>D3D12_GRAPHICS_PIPELINE_STATE_DESC</code>结构来创建。</p>
<pre><code>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;
};
struct D3D12_INPUT_LAYOUT_DESC
{
const D3D12_INPUT_ELEMENT_DESC *pInputElementDescs;
UINT NumElements;
};
enum D3D12_PRIMITIVE_TOPOLOGY_TYPE
{
D3D12_PRIMITIVE_TOPOLOGY_TYPE_UNDEFINED = 0,
D3D12_PRIMITIVE_TOPOLOGY_TYPE_POINT = 1,
D3D12_PRIMITIVE_TOPOLOGY_TYPE_LINE = 2,
D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE = 3,
D3D12_PRIMITIVE_TOPOLOGY_TYPE_PATCH = 4
};
</code></pre>
<ul>
<li><code>pRootSignature</code>:要绑定到这个管道状态的来源标记的指针。来源标记的内容必须要和绑定到这个管道状态的着色器内容相吻合。</li>
<li><code>VS</code>: 要绑定的顶点着色器(<code>Vertex Shader</code>)。</li>
<li><code>PS</code>: 要绑定的像素着色器(<code>Pixel Shader</code>)。</li>
<li><code>DS</code>: 要绑定的域着色器(<code>Domain Shader</code>)。</li>
<li><code>HS</code>: 要绑定的外壳着色器(<code>Hull Shader</code>)。</li>
<li><code>GS</code>: 要绑定的几何着色器(<code>Geometry Shader</code>)。</li>
<li><code>StreamOutput</code>: 用于流式输出,我们目前不需要关心。</li>
<li><code>BlendState</code>: 指定用于设置混合属性的混合状态。我们之后会讨论。这里我们使用默认值。</li>
<li><code>SampleMask</code>: 多重采样最多支持32个采样点,因此我们可以通过这一个$32bit$大小的整型来设置我们采样的时候要忽略的采样点,例如你第5位是$0$,就表示我们在进行多重采样的时候会忽略第5个采样点。通常我们设置为默认值<code>0xffffffff</code>表示不忽略任何采样点。</li>
<li><code>RasterizerState</code>: 指定我们要绑定的光栅化阶段状态。</li>
<li><code>DepthStencilState</code>: 指定我们要绑定的用于深度和目标测试的深度和模板状态。我们将会在之后介绍,这里我们使用默认值。</li>
<li><code>InputLayout</code>: 指定我们要绑定的输入顶点格式。</li>
<li><code>PrimitiveTopologyType</code>: 指定我们要使用的图元拓扑类型。</li>
<li><code>NumRenderTargets</code>: 指定我们同时使用的渲染目标的个数。</li>
<li><code>RTVFormats</code>: 渲染目标的数据格式。由于支持同时渲染到多个渲染目标中去,因此每个格式要和他对应的渲染目标相吻合。</li>
<li><code>DSVFormat</code>: 指定我们要使用的深度模板缓冲的格式,必须和我们之后设置的深度模板缓冲的格式相吻合。</li>
<li><code>SampleDesc</code>: 指定我们要使用的多重采样的采样数和采样等级,必须要和我们使用的渲染目标的设置相同。</li>
</ul>
<p>我们填充完<code>D3D12_GRAPHICS_PIPELINE_STATE_DESC</code>结构后就可以通过它来创建我们的渲染管道状态了。</p>
<pre><code> D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc;
//Fill psoDesc
...
///
ComPtr<ID3D12PipelineState> pso;
device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&pso));
</code></pre>
<p>虽然我们需要在创建渲染管道状态的时候指定很多状态,但是这样做的话能够提高性能。我们可以通过提前将所有的状态指定好,那么<code>Direct3D</code>就会验证我们的这个管道状态是否合法,然后驱动也会提前生成对应控制硬件状态的代码。在<code>Direct3D 11</code>中,我们是可以分别设置渲染管道的状态。但是有些状态是分散的相关联的,例如你改变了一个状态,那么可能另外一个和这个状态相关的状态也需要改变。这样就可能在我们改变几个状态的时候导致会有很多的状态会被改变,以及产生多余的改变,例如一个状态改变多次。为了避免这样的情况,驱动并不会立马改变状态,而是会到下一次绘制指令时,我们能够确定具体的渲染管道状态时才会开始改变,但是如果这样做的话,就意味着驱动要在运行的时候记录这些内容,它需要记录哪些状态需要被改变,以及最后会改变成什么样子,然后在运行的时候生成对应的代码将硬件的状态改变成对应的状态。在<code>Direct3D 12</code>中我们可以通过提前创建好渲染管道状态,然后直接更换我们的渲染管道状态一次性的将状态改变,就能够节省性能的浪费。</p>
<p>由于验证和创建一个渲染管道状态的时间开销是比较大的,因此我们通常都会在初始化的时候创建完毕,当然也可以在第一次引用的时候创建然后通过哈希表或者数据结构来存储下来下次直接引用第一次创建的就好了。</p>
<p>并且并不是所有的状态都被放到渲染管道状态中去了,也有一些和渲染管道无关的状态没有放到渲染管道状态中去,例如视角和裁剪矩形。</p>
<p>大致上来说<code>Direct3D</code>可以算一个状态机,因此他的所有的状态都会保存到直到这个状态被改变为止。下面的代码将会介绍使用不同的渲染管道状态。当然最好尽可能的少改变我们的渲染管道状态。</p>
<pre><code> commandList->Reset(commandListAlloc, pso);
//Draw Call
commandList->SetPipelineState(pso2);
//Draw Call
commandList->SetPipelineState(pso3);
//Draw Call