-
Notifications
You must be signed in to change notification settings - Fork 16
/
index.html
1415 lines (1048 loc) · 53.1 KB
/
index.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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>index</title>
<style type="text/css">
body {
font-family: Helvetica, arial, sans-serif;
font-size: 14px;
line-height: 1.6;
padding-top: 10px;
padding-bottom: 10px;
background-color: white;
padding: 30px; }
body > *:first-child {
margin-top: 0 !important; }
body > *:last-child {
margin-bottom: 0 !important; }
a {
color: #4183C4; }
a.absent {
color: #cc0000; }
a.anchor {
display: block;
padding-left: 30px;
margin-left: -30px;
cursor: pointer;
position: absolute;
top: 0;
left: 0;
bottom: 0; }
h1, h2, h3, h4, h5, h6 {
margin: 20px 0 10px;
padding: 0;
font-weight: bold;
-webkit-font-smoothing: antialiased;
cursor: text;
position: relative; }
h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor, h5:hover a.anchor, h6:hover a.anchor {
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA09pVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoMTMuMCAyMDEyMDMwNS5tLjQxNSAyMDEyLzAzLzA1OjIxOjAwOjAwKSAgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OUM2NjlDQjI4ODBGMTFFMTg1ODlEODNERDJBRjUwQTQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OUM2NjlDQjM4ODBGMTFFMTg1ODlEODNERDJBRjUwQTQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo5QzY2OUNCMDg4MEYxMUUxODU4OUQ4M0REMkFGNTBBNCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo5QzY2OUNCMTg4MEYxMUUxODU4OUQ4M0REMkFGNTBBNCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PsQhXeAAAABfSURBVHjaYvz//z8DJYCRUgMYQAbAMBQIAvEqkBQWXI6sHqwHiwG70TTBxGaiWwjCTGgOUgJiF1J8wMRAIUA34B4Q76HUBelAfJYSA0CuMIEaRP8wGIkGMA54bgQIMACAmkXJi0hKJQAAAABJRU5ErkJggg==) no-repeat 10px center;
text-decoration: none; }
h1 tt, h1 code {
font-size: inherit; }
h2 tt, h2 code {
font-size: inherit; }
h3 tt, h3 code {
font-size: inherit; }
h4 tt, h4 code {
font-size: inherit; }
h5 tt, h5 code {
font-size: inherit; }
h6 tt, h6 code {
font-size: inherit; }
h1 {
font-size: 28px;
color: black; }
h2 {
font-size: 24px;
border-bottom: 1px solid #cccccc;
color: black; }
h3 {
font-size: 18px; }
h4 {
font-size: 16px; }
h5 {
font-size: 14px; }
h6 {
color: #777777;
font-size: 14px; }
p, blockquote, ul, ol, dl, li, table, pre {
margin: 15px 0; }
hr {
background: transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAECAYAAACtBE5DAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OENDRjNBN0E2NTZBMTFFMEI3QjRBODM4NzJDMjlGNDgiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OENDRjNBN0I2NTZBMTFFMEI3QjRBODM4NzJDMjlGNDgiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo4Q0NGM0E3ODY1NkExMUUwQjdCNEE4Mzg3MkMyOUY0OCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo4Q0NGM0E3OTY1NkExMUUwQjdCNEE4Mzg3MkMyOUY0OCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PqqezsUAAAAfSURBVHjaYmRABcYwBiM2QSA4y4hNEKYDQxAEAAIMAHNGAzhkPOlYAAAAAElFTkSuQmCC) repeat-x 0 0;
border: 0 none;
color: #cccccc;
height: 4px;
padding: 0;
}
body > h2:first-child {
margin-top: 0;
padding-top: 0; }
body > h1:first-child {
margin-top: 0;
padding-top: 0; }
body > h1:first-child + h2 {
margin-top: 0;
padding-top: 0; }
body > h3:first-child, body > h4:first-child, body > h5:first-child, body > h6:first-child {
margin-top: 0;
padding-top: 0; }
a:first-child h1, a:first-child h2, a:first-child h3, a:first-child h4, a:first-child h5, a:first-child h6 {
margin-top: 0;
padding-top: 0; }
h1 p, h2 p, h3 p, h4 p, h5 p, h6 p {
margin-top: 0; }
li p.first {
display: inline-block; }
li {
margin: 0; }
ul, ol {
padding-left: 30px; }
ul :first-child, ol :first-child {
margin-top: 0; }
dl {
padding: 0; }
dl dt {
font-size: 14px;
font-weight: bold;
font-style: italic;
padding: 0;
margin: 15px 0 5px; }
dl dt:first-child {
padding: 0; }
dl dt > :first-child {
margin-top: 0; }
dl dt > :last-child {
margin-bottom: 0; }
dl dd {
margin: 0 0 15px;
padding: 0 15px; }
dl dd > :first-child {
margin-top: 0; }
dl dd > :last-child {
margin-bottom: 0; }
blockquote {
border-left: 4px solid #dddddd;
padding: 0 15px;
color: #777777; }
blockquote > :first-child {
margin-top: 0; }
blockquote > :last-child {
margin-bottom: 0; }
table {
padding: 0;border-collapse: collapse; }
table tr {
border-top: 1px solid #cccccc;
background-color: white;
margin: 0;
padding: 0; }
table tr:nth-child(2n) {
background-color: #f8f8f8; }
table tr th {
font-weight: bold;
border: 1px solid #cccccc;
margin: 0;
padding: 6px 13px; }
table tr td {
border: 1px solid #cccccc;
margin: 0;
padding: 6px 13px; }
table tr th :first-child, table tr td :first-child {
margin-top: 0; }
table tr th :last-child, table tr td :last-child {
margin-bottom: 0; }
img {
max-width: 100%; }
span.frame {
display: block;
overflow: hidden; }
span.frame > span {
border: 1px solid #dddddd;
display: block;
float: left;
overflow: hidden;
margin: 13px 0 0;
padding: 7px;
width: auto; }
span.frame span img {
display: block;
float: left; }
span.frame span span {
clear: both;
color: #333333;
display: block;
padding: 5px 0 0; }
span.align-center {
display: block;
overflow: hidden;
clear: both; }
span.align-center > span {
display: block;
overflow: hidden;
margin: 13px auto 0;
text-align: center; }
span.align-center span img {
margin: 0 auto;
text-align: center; }
span.align-right {
display: block;
overflow: hidden;
clear: both; }
span.align-right > span {
display: block;
overflow: hidden;
margin: 13px 0 0;
text-align: right; }
span.align-right span img {
margin: 0;
text-align: right; }
span.float-left {
display: block;
margin-right: 13px;
overflow: hidden;
float: left; }
span.float-left span {
margin: 13px 0 0; }
span.float-right {
display: block;
margin-left: 13px;
overflow: hidden;
float: right; }
span.float-right > span {
display: block;
overflow: hidden;
margin: 13px auto 0;
text-align: right; }
code, tt {
margin: 0 2px;
padding: 0 5px;
white-space: nowrap;
border: 1px solid #eaeaea;
background-color: #f8f8f8;
border-radius: 3px; }
pre code {
margin: 0;
padding: 0;
white-space: pre;
border: none;
background: transparent; }
.highlight pre {
background-color: #f8f8f8;
border: 1px solid #cccccc;
font-size: 13px;
line-height: 19px;
overflow: auto;
padding: 6px 10px;
border-radius: 3px; }
pre {
background-color: #f8f8f8;
border: 1px solid #cccccc;
font-size: 13px;
line-height: 19px;
overflow: auto;
padding: 6px 10px;
border-radius: 3px; }
pre code, pre tt {
background-color: transparent;
border: none; }
sup {
font-size: 0.83em;
vertical-align: super;
line-height: 0;
}
kbd {
display: inline-block;
padding: 3px 5px;
font-size: 11px;
line-height: 10px;
color: #555;
vertical-align: middle;
background-color: #fcfcfc;
border: solid 1px #ccc;
border-bottom-color: #bbb;
border-radius: 3px;
box-shadow: inset 0 -1px 0 #bbb
}
* {
-webkit-print-color-adjust: exact;
}
@media screen and (min-width: 914px) {
body {
width: 854px;
margin:0 auto;
}
}
@media print {
table, pre {
page-break-inside: avoid;
}
pre {
word-wrap: break-word;
}
}
</style>
</head>
<body>
<h1 id="toc_0">从画点开始</h1>
<h2 id="toc_1">TGAImage</h2>
<p>生成图像我们使用TGAImage,这个使用起来很简单:</p>
<div><pre><code class="language-C++">#include "tgaimage.h"
const TGAColor white = TGAColor(255, 255, 255, 255);
const TGAColor red = TGAColor(255, 0, 0, 255);
int main(int argc, char** argv){
TGAImage image(100, 100, TGAImage::RGB);
image.set(52, 41, red);
image.flip_vertically(); // i want to have the origin at the left bottom corner of the image
image.write_tga_file("output.tga");
return 0;
}</code></pre></div>
<p>生成的图像(注意中间哪一个小小的红色点):</p>
<p><img src="images/prepare.png" alt=""></p>
<p><a href="https://github.com/KrisYu/tinyrender/tree/master/code/00_prepare">代码</a></p>
<p>compile:</p>
<div><pre><code class="language-none">g++ main.cpp tgaimage.cpp -o main</code></pre></div>
<h2 id="toc_2">wavefront obj</h2>
<p>然后我们来学习一种3d格式文件,wavefront obj file:</p>
<div><pre><code class="language-none"># List of geometric vertices, with (x, y, z [,w]) coordinates, w is optional and defaults to 1.0.
v 0.123 0.234 0.345 1.0
v ...
...
# List of texture coordinates, in (u, [v ,w]) coordinates, these will vary between 0 and 1, v and w are optional and default to 0.
vt 0.500 1 [0]
vt ...
...
# List of vertex normals in (x,y,z) form; normals might not be unit vectors.
vn 0.707 0.000 0.707
vn ...
...
# Parameter space vertices in ( u [,v] [,w] ) form; free form geometry statement ( see below )
vp 0.310000 3.210000 2.100000
vp ...
...
# Polygonal face element (see below)
f 1 2 3
f 3/1 4/2 5/3
f 6/4/1 3/5/3 7/6/5
f 7//1 8//2 9//3
f ...
...
# Line element (see below)
l 5 8 1 2 4 9</code></pre></div>
<p>我们现在只需要知道了解顶点是v,现在我们想把一个文件中的3d模型的顶点 v (x, y, z) 给画出来,(因为我们已经知道怎么在图上相应的位置放像素)这个文件所有的 x, y, z ∈ [-1, 1],所以我们</p>
<ul>
<li>需要把它们映射到合适范围。</li>
<li>然后注意我们画的点 <code>image.set(52, 41, red);</code>, 这里的 52 和 41 是 int,映射之后需要转成int,因为我们总是画在一个一个像素点上。</li>
</ul>
<p>写一个简单的parser读入文件建立模型,画之。</p>
<p>核心部分长这样:</p>
<div><pre><code class="language-C++">for (int i = 0; i != model->nverts(); i++) {
Vec3f v = model->vert(i);
Vec2i p = world2screen(v);
image.set(p.x, p.y, white);
}</code></pre></div>
<p><img src="images/points.png" alt=""></p>
<p><a href="https://github.com/KrisYu/tinyrender/tree/master/code/01_points">代码</a></p>
<p>compile:</p>
<div><pre><code class="language-none">g++ main.cpp tgaimage.cpp model.cpp -o main</code></pre></div>
<h1 id="toc_3">画线</h1>
<p>画完了点,我们来开始画线。画线的同时我们依旧要记得,我们是画在一个一个整数的pixel上。</p>
<h2 id="toc_4">尝试一: 按照参数绘制直线</h2>
<div><pre><code class="language-C++">void line(int x0, int y0, int x1, int y1, TGAImage &image, TGAColor color) {
for (float t=0.; t<1.; t+=.01) {
int x = x0 + (x1-x0)*t;
int y = y0 + (y1-y0)*t;
image.set(x, y, color);
}
}</code></pre></div>
<p>这里的问题有两个:</p>
<ul>
<li>效率低</li>
<li>t如何控制</li>
</ul>
<p>t取大了画出来的并不是线,而是一堆点。t取小了会浪费,有很多重复的x和y。</p>
<h2 id="toc_5">尝试二: 按x的增加画线</h2>
<div><pre><code class="language-C++">void line(int x0, int y0, int x1, int y1, TGAImage &image, TGAColor color) {
for (int x=x0; x<=x1; x++) {
float t = (x-x0)/(float)(x1-x0);
int y = y0 + (y1 - y0)*t;
image.set(x, y, color);
}
}</code></pre></div>
<p>我们想着要节约,就每次 x 增加1,然后来画y。</p>
<p>这样画线是对的因为我们假设 \(y = mx + b \), 直线斜率m, 截距b</p>
<p>\[
\frac{y_1 - y_0}{x_1 - x_0} = m
\]</p>
<p>\[
y_0 = mx_0 + b
\]</p>
<p>\[
y = y_0 + \frac{y_1 - y_0}{x_1 - x_0}(x - x_0)
\]</p>
<p>所以</p>
<p>\[
y = y_0 + mx - mx_0 = mx + (y_0 - mx_0) = mx + b
\]</p>
<p>同时它的问题是我们也已经指出:</p>
<ul>
<li>如果直线斜率太大,比如 m = 3, 那么x每增加1个像素,y增加3个像素,这样画出来就是分离的点。</li>
<li>只能适用于 x0 < x1的状况</li>
</ul>
<h2 id="toc_6">尝试三</h2>
<p>所以想法是:</p>
<ul>
<li>如有必要交换 x0 和 x1,这样使得 x0 一定小于 x1</li>
<li>如果斜率比较大,则交换 x 和 y</li>
</ul>
<p>看代码:</p>
<div><pre><code class="language-C++">void line(int x0, int y0, int x1, int y1, TGAImage &image, TGAColor color) {
bool steep = false;
if (std::abs(x0-x1)<std::abs(y0-y1)) { // if the line is steep, we transpose the image
std::swap(x0, y0);
std::swap(x1, y1);
steep = true;
}
if (x0>x1) { // make it left−to−right
std::swap(x0, x1);
std::swap(y0, y1);
}
for (int x=x0; x<=x1; x++) {
float t = (x-x0)/(float)(x1-x0);
int y = y0 + (y1 - y0)*t;
if (steep) {
image.set(y, x, color); // if transposed, de−transpose
} else {
image.set(x, y, color);
}
}
}</code></pre></div>
<p>这样就可以完善上述出现的问题来画线了。</p>
<p>这里其实还是有一些可以进步的空间,比如出现了浮点数t,同时也依旧我们之前说的我们只需要画在整数上。可以参见: </p>
<p><a href="https://zhuanlan.zhihu.com/p/64989645">再谈绘制直线</a>中的优化部分。</p>
<p>不过我们画线就暂时停在这里。我们就用这个函数来画了,因为compiler的优化已经足够好了。</p>
<h2 id="toc_7">wavefront obj</h2>
<p>之前我们已经用过这个文件,上次我们认识了v 代表顶点(vertex),这次我们来多认识一个f 代表面(face),实际上是三角形面,在这个文件中我们的一行f有:</p>
<div><pre><code class="language-none">f 1193/1240/1193 1180/1227/1180 1179/1226/1179</code></pre></div>
<p>我们现在只需要知道每组的第一个数字: 1193,1180,1179 是代表vertex list中的三个顶点的索引(index),这三个顶点构成一个三角形。</p>
<p>所以进一步修改model parser,我们来用这个画出线框,核心代码长这样:</p>
<div><pre><code class="language-C++">for (int i = 0; i < model->nfaces(); i++) {
std::vector<int> face = model->face(i);
// face: i0,i1,i2 of triangle
for (int j = 0; j < 3; j++) {
Vec3f v0 = model->vert(face[j]);
// this % used for when j = 2 to get i2, i0
Vec3f v1 = model->vert(face[(j+1)%3]);
int x0 = (v0.x+1.)*width/2.;
int y0 = (v0.y+1.)*height/2.;
int x1 = (v1.x+1.)*width/2.;
int y1 = (v1.y+1.)*height/2.;
line(x0, y0, x1, y1, image, white);
}
}</code></pre></div>
<p><img src="images/wireframe.png" alt=""></p>
<p>looks good.</p>
<p><a href="https://github.com/KrisYu/tinyrender/tree/master/code/02_wireframe">代码</a></p>
<p>compile</p>
<div><pre><code class="language-none">g++ -std=c++11 main.cpp tgaimage.cpp model.cpp -o main</code></pre></div>
<h1 id="toc_8">填充三角形</h1>
<h2 id="toc_9">线性插值</h2>
<p>来复习一下画线,我们知道画线的时候我们做的实际上是这样的事情:对于要画的线AB,在满足我们设定的条件(斜率 ≤ 1, A < B)之后,因为我们要画的是整数的像素点,对于x每增加1,我们算出对应的y,然后来画点(x,y),这样重复直到x增加到B点:</p>
<p><img src="images/Bresenham.png" alt=""></p>
<p>对于 AB 上的任意一点 P 满足:</p>
<p>\[
P = A + t(B - A), 0 \le t \le 1
\]</p>
<p>也可以写成:</p>
<p>\[
P = (1 - t)A + tB , 0 \le t \le 1
\]</p>
<p>这个公式是著名的线性插值,实际上也是我们画线的基础。因为在画线部分核心代码长这样:</p>
<div><pre><code class="language-C++">for (int x=x0; x<=x1; x++) {
float t = (x-x0)/(float)(x1-x0);
int y = y0 + (y1 - y0)*t;
...
}</code></pre></div>
<p>对于P点,我们根据增加后的x算出t值,然后算出y,得到应该画的点。</p>
<p>其实在画框架的时候我们已经画过三角形了,就画三条线就OK。现在我们要做的是来填充三角形。</p>
<h2 id="toc_10">扫描法</h2>
<p>若要填充一个三角形,最简单的能想到的办法是对于三角形的每一个y,我们找到对应的左侧和右侧,x_left和x_right,我们画上x_left到x_right的线,那么从三角形最上面的点按y增加扫到最下面的点既可。</p>
<p>为了简单起见,我们先把三角形拆成上下两部分:</p>
<p><img src="images/upper_down_triangle.png" alt=""></p>
<p>那么对于一个特定的y,我们想要找到它的左边和右边 A B 两点,思路是这样:</p>
<ul>
<li>首先排序,保证 t0 ≤ t1 ≤ t2</li>
<li>整个三角形的高度必为 t2.y - t0.y</li>
<li>那么对于上半部分,y每增加1(注意有可能t0 == t1),我们用插值法算出对应的两点A和B</li>
</ul>
<p>这样就能算出对应的 A 和 B</p>
<div><pre><code class="language-C++">void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage &image, TGAColor color) {
// sort the vertices, t0, t1, t2 lower−to−upper (bubblesort yay!)
if (t0.y>t1.y) std::swap(t0, t1);
if (t0.y>t2.y) std::swap(t0, t2);
if (t1.y>t2.y) std::swap(t1, t2);
int total_height = t2.y-t0.y;
for (int y=t0.y; y<=t1.y; y++) {
int segment_height = t1.y-t0.y+1;
float alpha = (float)(y-t0.y)/total_height;
float beta = (float)(y-t0.y)/segment_height; // be careful with divisions by zero
Vec2i A = t0 + (t2-t0)*alpha;
Vec2i B = t0 + (t1-t0)*beta;
image.set(A.x, y, red);
image.set(B.x, y, green);
}
}</code></pre></div>
<p><img src="images/triangle_left_right.png" alt=""></p>
<p>那么有了 A 和 B 之后,我们在AB之间调用我们的画线函数,再用同样的方法给下半部分填满,问题既解决。</p>
<div><pre><code class="language-C++">void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage &image, TGAColor color) {
// sort the vertices, t0, t1, t2 lower−to−upper (bubblesort yay!)
if (t0.y>t1.y) std::swap(t0, t1);
if (t0.y>t2.y) std::swap(t0, t2);
if (t1.y>t2.y) std::swap(t1, t2);
int total_height = t2.y-t0.y;
for (int y=t0.y; y<=t1.y; y++) {
int segment_height = t1.y-t0.y+1;
float alpha = (float)(y-t0.y)/total_height;
float beta = (float)(y-t0.y)/segment_height; // be careful with divisions by zero
Vec2i A = t0 + (t2-t0)*alpha;
Vec2i B = t0 + (t1-t0)*beta;
if (A.x>B.x) std::swap(A, B);
for (int j=A.x; j<=B.x; j++) {
image.set(j, y, color); // attention, due to int casts t0.y+i != A.y
}
}
for (int y=t1.y; y<=t2.y; y++) {
int segment_height = t2.y-t1.y+1;
float alpha = (float)(y-t0.y)/total_height;
float beta = (float)(y-t1.y)/segment_height; // be careful with divisions by zero
Vec2i A = t0 + (t2-t0)*alpha;
Vec2i B = t1 + (t2-t1)*beta;
if (A.x>B.x) std::swap(A, B);
for (int j=A.x; j<=B.x; j++) {
image.set(j, y, color); // attention, due to int casts t0.y+i != A.y
}
}
}</code></pre></div>
<p>这样三角形填充就解决。代码里有很多重复的部分,然后这里决定让代码短一点,代价是读起来没那么清楚了:</p>
<div><pre><code class="language-C++">void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage &image, TGAColor color) {
if (t0.y==t1.y && t0.y==t2.y) return; // I dont care about degenerate triangles
// sort the vertices, t0, t1, t2 lower−to−upper (bubblesort yay!)
if (t0.y>t1.y) std::swap(t0, t1);
if (t0.y>t2.y) std::swap(t0, t2);
if (t1.y>t2.y) std::swap(t1, t2);
int total_height = t2.y-t0.y;
for (int i=0; i<total_height; i++) {
bool second_half = i>t1.y-t0.y || t1.y==t0.y;
int segment_height = second_half ? t2.y-t1.y : t1.y-t0.y;
float alpha = (float)i/total_height;
float beta = (float)(i-(second_half ? t1.y-t0.y : 0))/segment_height; // be careful: with above conditions no division by zero here
Vec2i A = t0 + (t2-t0)*alpha;
Vec2i B = second_half ? t1 + (t2-t1)*beta : t0 + (t1-t0)*beta;
if (A.x>B.x) std::swap(A, B);
for (int j=A.x; j<=B.x; j++) {
image.set(j, t0.y+i, color); // attention, due to int casts t0.y+i != A.y
}
}
}</code></pre></div>
<h2 id="toc_11">wavefront obj</h2>
<p>上一章我们画了框架,这下我们来填上三角形:</p>
<p><img src="images/filledframe.png" alt=""></p>
<p>好吧,并不是很动人=。=之所以不动人是因为光影光影,我们只有颜色,没有考虑光,</p>
<p><a href="https://github.com/KrisYu/tinyrender/tree/master/code/03_filledtriangle">代码</a></p>
<p>compile:</p>
<div><pre><code class="language-none">g++ -std=c++11 main.cpp tgaimage.cpp model.cpp -o main</code></pre></div>
<h2 id="toc_12">重心坐标法</h2>
<p>除了上面提到的扫描法之外,另外一个可以想到的办法是,因为我们终究是画到二维平面上的像素,一个一个的点,那么对于我们要画的区域内的每一个点,我们是否可以检测看它是否在三角形之内,如果是的话,画它,否则不理之。这样的思路是可行的,对于三角形内及其边上的任意一点,我们都可以用重心坐标系来表示:</p>
<p>\[
P = (1 - u - v)A + uB + vC, 0 \le u,v \le 1
\]</p>
<p>这个长得也很像线性插值。</p>
<p>运算:</p>
<p>\[
P = A + u\overrightarrow{AB} + v\overrightarrow{AC}
\]</p>
<p>继续:</p>
<p>\[
u\overrightarrow{AB} + v\overrightarrow{AC} + \overrightarrow{PA} = 0
\]</p>
<p>PA是AB和AC的线性组合。</p>
<p>拆一拆:</p>
<p>\[
u\overrightarrow{AB}_x + v\overrightarrow{AC}_x + \overrightarrow{PA}_x = 0
\]</p>
<p>\[
u\overrightarrow{AB}_y + v\overrightarrow{AC}_y + \overrightarrow{PA}_y = 0
\]</p>
<p>实际上我们都可以看做是我们在寻找向量 \((u, v, 1)\) 同时垂直于向量 \((\overrightarrow{AB}_x, \overrightarrow{AC}_x,\overrightarrow{PA}_x)\) 和向量 \((\overrightarrow{AB}_y, \overrightarrow{AC}_y,\overrightarrow{PA}_y)\)。 这就是叉乘。</p>
<div><pre><code class="language-none">xvector = (B_x - A_x, C_x - A_x, A_x - P_x)
yvector = (B_y - A_y, C_y - A_y, A_y - P_y)
u = xvector x yvector
# 如果 u 的 z 分量不等于1则说明P点不在三角形内</code></pre></div>
<p>因为我们的计算有浮点数,可能u的z分量不会一定等于1,令 u 的三个分量是 (a, b, c),我们代入原式子:</p>
<p>\[
a\overrightarrow{AB} + b\overrightarrow{AC} + c\overrightarrow{PA} = 0
\]</p>
<p>\[
P = (1 - a/c - b/c)A + a/cB + b/cC, c \ne 0
\]</p>
<p>代码我们这样写:</p>
<div><pre><code class="language-C++">Vec3f barycentric(Vec2f A, Vec2f B, Vec2f C, Vec2f P) {
Vec3f s[2];
for (int i=2; i--; ) {
s[i][0] = C[i]-A[i];
s[i][1] = B[i]-A[i];
s[i][2] = A[i]-P[i];
}
Vec3f u = cross(s[0], s[1]);
if (std::abs(u[2])>1e-2) // dont forget that u[2] is integer. If it is zero then triangle ABC is degenerate
return Vec3f(1.f-(u.x+u.y)/u.z, u.y/u.z, u.x/u.z);
return Vec3f(-1,1,1); // in this case generate negative coordinates, it will be thrown away by the rasterizator
}</code></pre></div>
<p>我们当然也不用把平面区域的每个点代入P去做检查,我们只需要找到三角形的 bounding_box,然后看其中的每一个整数点,如果在其中,那就画之。</p>
<p>用同样的方法来给模型填色,效果一样。</p>
<p><a href="https://github.com/KrisYu/tinyrender/tree/master/code/04_barycentricfilled">代码</a></p>
<p>效果跟之前依旧一致,我们给每个三角形随机填上色:</p>
<p><img src="images/filledrandom.png" alt=""></p>
<p>随机填色这个我们看起来倒是有点cool.</p>
<h1 id="toc_13">z-buffer</h1>
<h2 id="toc_14">简单光</h2>
<p>我们现在先复习一下,我们经过了画点、画线,填三角形之后已经能画出来一些东西了,现在我们有好几条路可以走,那就是 </p>
<ul>
<li>光(上帝说“要有光”)</li>
<li>纹理(不然就填白色和随机颜色么?)</li>
<li>数学(之前做的所有事情就是简单的把x,y对应的画到图像上来)</li>
</ul>
<p>这里我们做的事就是简单的给我们的模型一点‘方向光’,注意我这里说了一专有名词‘方向光’,所以还会有别光(暂且不表)。方向光就是类似太阳光一样的,我们只考虑它的方向:</p>
<p><img src="images/directional_light.png" alt=""></p>
<p>对于一束光,我们到达物体表面的能量实际上是:</p>
<p><img src="images/cos_alpha.png" alt=""></p>
<p>它的强度 Icosα, α是物体光与物体的法向量的夹角。</p>
<p>如果我们用\(\overrightarrow{L}\)表示光的方向,\(\overrightarrow{N}\)指向物体光照处'向内的'法向量,那么</p>
<p>\[
cos\alpha = \frac{\overrightarrow{L} \cdot \overrightarrow{N}}{|\overrightarrow{L}| \cdot |\overrightarrow{N}|}
\]</p>
<p>这里我们就必须要考虑一些数学问题了,物体我们放在这,然后有光的方向:</p>
<p><img src="images/light_scene01.png" alt=""></p>
<p>那么'朝内的'法向量可以这样得到\(\overrightarrow{AC} \times \overrightarrow{AB}\),然后正交化: </p>
<p><img src="images/light_scene02.png" alt=""></p>
<p>这里我们先做很多简化操作:</p>
<ul>
<li>光的方向是 Vec3f light(0, 0, -1), 强度就是1</li>
<li>假设每个三角形收到光照的强度相同,都是 Icosα</li>
<li>三角形法向量\(\overrightarrow{AC} \times \overrightarrow{AB}\)</li>
<li>当然我们还要知道 cosα 大于0才有意义,我们不可能减去光o(╯□╰)o</li>
</ul>
<p>核心代码:</p>
<div><pre><code class="language-C++">Vec3f norm = cross(world_coords[2] - world_coords[0], world_coords[1] - world_coords[0]);
norm.normalize();
float intensity = light*norm;
if (intensity > 0) {
triangle(screen_coords, image, TGAColor(intensity*255,intensity*255,intensity*255,255));
}</code></pre></div>
<p>看效果:</p>
<p><img src="images/simple_light.png" alt=""></p>
<p>妈妈他是凸嘴。我们换一个光的方向。</p>
<p><img src="images/simple_light2.png" alt=""></p>
<p>更吓人了。。。。他嘴巴怎么长后面了。。</p>
<p><a href="https://github.com/KrisYu/tinyrender/tree/master/code/05_simplelight">代码</a></p>
<p>compile & run:</p>
<div><pre><code class="language-none">$ g++ -std=c++11 main.cpp tgaimage.cpp model.cpp -o main
$ ./main</code></pre></div>
<h2 id="toc_15">z-buffer</h2>
<p>造成这个问题的原因很简单,我们就是一股脑的把三角形画出来了,没有考虑三角形的先后顺序,正如画画一样,我们应该先画远处的东西,如果近处有什么东西把它给覆盖了,我们就不会看到远处的东西,这里我们就是画三角形的时候没有考虑先后顺序。那么这个问题要怎么解决呢?</p>
<p>这里我们先继续回顾一下三角形重心坐标:</p>
<p>\[
P = (1 - a/c - b/c)A + a/cB + b/cC, c \ne 0
\]</p>
<p>这里其实有一个很cool的点,就是我们把P表示成三角形三个顶点的线性组合,再回忆一下线性插值,其实对于P点的任何性质,我们都可以利用类似线性插值,把它变成三个顶点的组合:</p>
<p>\[
P_z = (1 - a/c - b/c)A_z + a/cB_z + b/cC_z, c \ne 0
\]</p>
<p>所以这里就给了我们提示,对于任意一点P,我们算出它的z值,如果z值更靠近我们,那么我们就用它来替换已经画上的点,否则我们则不更新P点。</p>
<p>同样我们也只用考虑画布上的所有的点的P值,可以用一个二维的数组来表示,不过我们这里偷懒,就用一维的数组,因为画布上的(x,y)点可以写成(x + y *width),可以这样来转换:</p>
<div><pre><code class="language-C++">int idx = x + y*width;</code></pre></div>
<div><pre><code class="language-C++">int x = idx % width;
int y = idx / width;</code></pre></div>
<p>同时注意我们在把物体坐标系做映射时,需要保留z值,所以一些计算我们最好就用float.同时我们也需要注意在转换坐标系的时候我们需要注意还是需要把 x 和 y 变成int,否则有些地方会因为浮点数的原因for loop不会覆盖所有的像素,会有黑色部分产生:</p>
<div><pre><code class="language-C++">Vec3f world2screen(Vec3f v) {
// 注意这里我们还是保留了int这个操作,因为我们的画像素的for loop要用到这个x和y
// 如果都是浮点数,那么for loop有些可能无法顺利进行
// 我们再加上0.5来四舍五入
return Vec3f(int((v.x+1.)*width/2.+.5), int((v.y+1.)*height/2.+.5), v.z);
}</code></pre></div>
<p>第二个需要注意的点是我们物体的位置和朝向,这里我们把z-buffer初始化为负无穷大,然后如果P.z更大意味更靠近我们。</p>
<div><pre><code class="language-none">void triangle(Vec3f *pts, float *zbuffer, TGAImage &image, TGAColor color) {
Vec2f bboxmin(std::numeric_limits<float>::max(),std::numeric_limits<float>::max());
Vec2f bboxmax(std::numeric_limits<float>::min(),std::numeric_limits<float>::min());
Vec2f clamp(image.get_width()-1, image.get_height()-1);
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 2; j++) {
bboxmin[j] = std::max(0.f, std::min(bboxmin[j], pts[i][j]));
bboxmax[j] = std::min(clamp[j], std::max(bboxmax[j], pts[i][j]));
}
}
Vec3f P;
for (P.x = bboxmin.x; P.x <= bboxmax.x; P.x++) {
for (P.y = bboxmin.y; P.y <= bboxmax.y; P.y++) {
Vec3f bc_screen = barycentric(pts[0], pts[1], pts[2], P);
if (bc_screen.x < 0 || bc_screen.y < 0 || bc_screen.z < 0 ) continue;
P.z = 0;
for (int i=0; i<3; i++) P.z += pts[i][2]*bc_screen[i];
if (zbuffer[int(P.x+P.y*width)] < P.z) {
image.set(P.x, P.y, color);
zbuffer[int(P.x+P.y*width)] = P.z;
}
}
}
}</code></pre></div>
<p>看结果:</p>
<p><img src="images/z_buffer01.png" alt=""></p>
<p><a href="https://github.com/KrisYu/tinyrender/tree/master/code/06_simplelightzbuffer">代码</a></p>
<h1 id="toc_16">纹理</h1>
<p>之前我们说有三个方向可以走 - 光、纹理、数学,我们先轻松一下,看一下纹理。</p>
<p>纹理其实就是贴图,比如看这个正方体,如果我们想要它有木质效果。对于它的每一面,我们给它贴上图就ok.</p>
<p><img src="images/r17-texture-mapping.png" alt=""></p>
<p>注意只用图的一部分也是完全ok的。</p>
<p>我们定义 u 和 v</p>
<p>\[
0 \le u \le 1
\]</p>
<p>\[
0 \le v \le 1
\]</p>
<p>(u, v) 会对应到图片(宽w, 高h)上的 (u(w - 1), v(h - 1)),这样定义的的好处当然很多啦, 比如我们可以随意换纹理图,还可以随意换纹理图大小...等等等。</p>
<p><img src="images/uvwh.png" alt=""></p>
<p>针对每个三角形的顶点我们有 (u, v), 同样用重心坐标系算出对于三角形的每一点的 (u, v),然后根据这个 (u, v) 来画图。</p>
<p><img src="images/texture_uv.png" alt=""></p>
<h2 id="toc_17">wavefront obj</h2>
<p>对于我们的文件,其中有:</p>
<div><pre><code class="language-none">vt 0.532 0.923 0.000</code></pre></div>
<p>这个数据就是我们三角形对应的(u, v)。</p>
<p>而我们之前读f的时候,丢弃了一些数据,实际上:</p>
<div><pre><code class="language-none">f 1193/1240/1193 1180/1227/1180 1179/1226/1179</code></pre></div>
<p>实际上三个数据分别是: 顶点索引/顶点法向量索引/顶点纹理索引。</p>
<p>我们来修改model, 给出纹理文件:'african_head_diffuse.tga',读入纹理,然后贴上图看看,注意这里我们并没有给他加上光:</p>
<p><img src="images/texture.png" alt=""></p>
<p>looks good!</p>
<p><a href="https://github.com/KrisYu/tinyrender/tree/master/code/07_texture">代码</a></p>
<p>compile & run:</p>
<div><pre><code class="language-none">$ g++ -std=c++11 main.cpp tgaimage.cpp model.cpp -o main
$ ./main</code></pre></div>
<h1 id="toc_18">数学知识</h1>
<p>之前我们说有三个方向可以走 - 光、纹理、数学。然后我们简单看了一个光的例子来学习z-buffer和纹理,现在我们来看一下相关的数学知识。这些也很重要。</p>
<p>需要了解的概念包括:</p>
<ul>
<li>矩阵是如何变换向量的: V' = Matrix * V</li>
<li>窗口变换、坐标变换: 同样可以用矩阵表示</li>
<li>正交投影、透视投影:矩阵表示</li>
<li>左手坐标系vs右手坐标系</li>
<li>...</li>
</ul>
<p>这部分可以参考我的一些文章</p>
<ul>
<li><a href="https://zhuanlan.zhihu.com/p/63610995">[从零开始计算机图形学]之十四数学知识</a> 中的齐次坐标、变换矩阵部分</li>
<li><a href="https://zhuanlan.zhihu.com/p/66240124">矩阵的逆、坐标变换和窗口变换</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/66384929">LookAt、Viewport、Perspective矩阵</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/64707259">左手坐标系vs右手坐标系</a></li>
</ul>
<p>经过这些补充之后,我们需要知道的是为了变换物体位置,并且符合近大远小的原则我们最终画在屏幕上的点需要经过这些变换:</p>
<div><pre><code class="language-none">顶点 → 世界坐标系 → 摄像头坐标系 → 投影 → 屏幕坐标系
viewport * projection * view * model * vertex.</code></pre></div>
<h2 id="toc_19">加上数学</h2>
<p>这里我们简单用一点点数学,我们不动物体,但是把眼睛/摄像头放到 z = 3 的位置,头像缩小一点点,再加上透视投影。</p>
<p><img src="images/math_loc.png" alt=""></p>
<p>结果:</p>
<p><img src="images/math01.png" alt=""></p>