Triangle
阅读信息
约 163 个字 10 分钟 本页总访问量:加载中... 次
画三角形
尝试
因为 ssloy 说 Usually, after this introduction I leave my students for about an hour(
用unordered_map<int,vector<int>>
记录每个 x 对应的边界上的 y,最后用竖直线填充三角形。
由于统一用 x 作为 key,不能根据斜率翻转。但是普通 Bresenhan 方法每次 y 只能上升或下降一格,斜率大的线会变成 45 度直线+竖直直线。这里用了int step = error / dx;
来进行大斜率的移动。
C++ #include <bits/stdc++.h>
#include "model.h"
#include "tgaimage.h"
using namespace std ;
const TGAColor white = TGAColor ( 255 , 255 , 255 , 255 );
const TGAColor blue = TGAColor ( 0 , 0 , 255 , 255 );
void addEdge ( vec3 v0 , vec3 v1 , unordered_map < int , vector < int >>& line ) {
int x0 = v0 . x ;
int y0 = v0 . y ;
int x1 = v1 . x ;
int y1 = v1 . y ;
if ( x0 > x1 ) {
swap ( x0 , x1 );
swap ( y0 , y1 );
}
int dx = x1 - x0 ;
int dy = y1 - y0 ;
int derror = abs ( dy * 2 );
int error = 0 ;
int y = y0 ;
for ( int x = x0 ; x <= x1 ; x ++ ) {
line [ x ]. push_back ( y );
error += derror ;
if ( error >= dx ) {
int step = error / dx ;
y += ( y1 > y0 ) ? step : - step ;
error -= step * dx * 2 ;
}
}
}
void triangle1 ( vec3 v0 , vec3 v1 , vec3 v2 , TGAImage & image , TGAColor color ) {
unordered_map < int , vector < int >> line ;
addEdge ( v0 , v1 , line );
addEdge ( v1 , v2 , line );
addEdge ( v2 , v0 , line );
int xmin = min ({ v0 . x , v1 . x , v2 . x });
int xmax = max ({ v0 . x , v1 . x , v2 . x });
for ( int x = xmin ; x <= xmax ; x ++ ) {
vector < int > vy = line [ x ];
if ( vy . empty ()) {
cout << "vy is empty!" << '\n' ;
}
int ymin = * min_element ( vy . begin (), vy . end ());
int ymax = * max_element ( vy . begin (), vy . end ());
for ( int y = ymin ; y <= ymax ; y ++ ) {
image . set ( x , y , color );
}
}
}
int main () {
TGAImage image ( 600 , 600 , TGAImage :: RGB );
vec3 v0 = { 100. , 200. , 100. };
vec3 v1 = { 300. , 400. , 100. };
vec3 v2 = { 400. , 100. , 100. };
triangle1 ( v1 , v2 , v0 , image , blue );
image . write_tga_file ( "triangle.tga" );
return 0 ;
}
结果:
(下面是官方的改进流程)
改进过程
1. 画边框
C++ void triangle2 ( vec3 v0 , vec3 v1 , vec3 v2 , TGAImage & image , TGAColor color ) {
line ( v0 . x , v0 . y , v1 . x , v1 . y , image , color );
line ( v1 . x , v1 . y , v2 . x , v2 . y , image , color );
line ( v2 . x , v2 . y , v0 . x , v0 . y , image , color );
}
2. 区分边界
将 v0, v1, v2 按 y 的升序排列,由 v0, v1 和 v1, v2 画的边为一组,中间有转折;由 v0, v2 画的边为一组,无转折。
C++ void triangle2 ( vec3 v0 , vec3 v1 , vec3 v2 , TGAImage & image , TGAColor color ) {
if ( v0 . y > v1 . y )
swap ( v0 , v1 );
if ( v0 . y > v2 . y )
swap ( v0 , v2 );
if ( v1 . y > v2 . y )
swap ( v1 , v2 );
line ( v0 . x , v0 . y , v1 . x , v1 . y , image , color );
line ( v1 . x , v1 . y , v2 . x , v2 . y , image , color );
line ( v2 . x , v2 . y , v0 . x , v0 . y , image , color );
}
3. 区分上下部分
交换后 v1 为转折点,根据 v1 分割成上下两部分,分别计算左右边界的 x 值。
C++ void triangle2 ( vec3 v0 , vec3 v1 , vec3 v2 , TGAImage & image , TGAColor color ) {
if ( v0 . y > v1 . y )
swap ( v0 , v1 );
if ( v0 . y > v2 . y )
swap ( v0 , v2 );
if ( v1 . y > v2 . y )
swap ( v1 , v2 );
int total_height = v2 . y - v0 . y ;
int down_height = v1 . y - v0 . y + 1 ; // +1避免除以零
int up_height = v2 . y - v1 . y + 1 ;
// 下半个三角形
for ( int y = v0 . y ; y <= v1 . y ; y ++ ) {
float alpha = ( float )( y - v0 . y ) / total_height ; // alpha表示长边的比例
float beta = ( float )( y - v0 . y ) / down_height ; // beta表示转折边的比例
int x_a = v0 . x + alpha * ( v2 . x - v0 . x );
int x_b = v0 . x + beta * ( v1 . x - v0 . x );
image . set ( x_a , y , red );
image . set ( x_b , y , green );
}
// 上半个三角形
for ( int y = v1 . y ; y <= v2 . y ; y ++ ) {
float alpha = ( float )( y - v0 . y ) / total_height ; // alpha表示长边的比例
float beta = ( float )( y - v1 . y ) / up_height ; // beta表示转折边的比例
int x_a = v0 . x + alpha * ( v2 . x - v0 . x );
int x_b = v1 . x + beta * ( v2 . x - v1 . x );
image . set ( x_a , y , red );
image . set ( x_b , y , green );
}
}
4. 水平线填充
C++ // 下半个三角形
for ( int y = v0 . y ; y <= v1 . y ; y ++ ) {
float alpha = ( float )( y - v0 . y ) / total_height ; // alpha表示长边的比例
float beta = ( float )( y - v0 . y ) / down_height ; // beta表示转折边的比例
int x_a = v0 . x + alpha * ( v2 . x - v0 . x );
int x_b = v0 . x + beta * ( v1 . x - v0 . x );
// image.set(x_a, y, red);
// image.set(x_b, y, green);
if ( x_a > x_b )
swap ( x_a , x_b ); // 确定左右关系
for ( int x = x_a ; x <= x_b ; x ++ ) {
image . set ( x , y , color );
}
}
// 上半个三角形
for ( int y = v1 . y ; y <= v2 . y ; y ++ ) {
float alpha = ( float )( y - v0 . y ) / total_height ; // alpha表示长边的比例
float beta = ( float )( y - v1 . y ) / up_height ; // beta表示转折边的比例
int x_a = v0 . x + alpha * ( v2 . x - v0 . x );
int x_b = v1 . x + beta * ( v2 . x - v1 . x );
// image.set(x_a, y, red);
// image.set(x_b, y, green);
if ( x_a > x_b )
swap ( x_a , x_b ); // 确定左右关系
for ( int x = x_a ; x <= x_b ; x ++ ) {
image . set ( x , y , color );
}
}
5. 合并代码
C++ void triangle3 ( vec3 v0 , vec3 v1 , vec3 v2 , TGAImage & image , TGAColor color ) {
if ( v0 . y == v1 . y && v0 . y == v2 . y )
return ;
// 按y的升序排列
if ( v0 . y > v1 . y )
swap ( v0 , v1 );
if ( v0 . y > v2 . y )
swap ( v0 , v2 );
if ( v1 . y > v2 . y )
swap ( v1 , v2 );
int total_height = v2 . y - v0 . y + 1 ;
for ( int y = v0 . y ; y <= v2 . y ; y ++ ) {
bool is_up = y > v1 . y || v0 . y == v1 . y ;
int segment_height = is_up ? v2 . y - v1 . y + 1 : v1 . y - v0 . y + 1 ;
float alpha = ( float )( y - v0 . y ) / total_height ; // 长边的比例
float beta = ( float )( y - ( is_up ? v1 . y : v0 . y )) /
segment_height ; // 转折边的比例
int x_a = v0 . x + alpha * ( v2 . x - v0 . x );
int x_b =
is_up ? v1 . x + beta * ( v2 . x - v1 . x ) : v0 . x + beta * ( v1 . x - v0 . x );
if ( x_a > x_b )
swap ( x_a , x_b );
for ( int x = x_a ; x <= x_b ; x ++ ) {
image . set ( x , y , color );
}
}
}
结果:
重心坐标
已知三角形 ABC 和点 P,P 的重心坐标为三个顶点的加权值:
\[ P=u\cdot A+v\cdot B+w\cdot C\]
其中\(u+v+w=1\) .
若\(u,v,w\) 都在[0, 1]范围内,则 P 在三角形内;若至少一个权值在范围外,则至少一个值小于零,P 在三角形外。
令三个顶点的坐标分别为\(x_i, y_i, z_i\) ,P 点坐标为\(x_p, y_p, z_p\) ,则:
\[
\begin{cases}
x_p-x_2=u\cdot (x_0-x_2)+v\cdot (x_1-x_2) \\
y_p-y_2=u\cdot (y_0-y_2)+v\cdot (y_1-y_2) \\
\end{cases}
\]
增广矩阵:
\[
\begin{pmatrix}
x_0-x_2 & x_1-x_2 \\
y_0-y_2 & y_1-y_2 \\
\end{pmatrix}
\begin{pmatrix}
u \\
v \\
\end{pmatrix}=
\begin{pmatrix}
x_p-x_2 \\
y_p-y_2 \\
\end{pmatrix}
\]
Cramer's Rule:
\[
u=\frac{D_u}{D}
=\frac{
\begin{vmatrix}
x_p - x_2 & x_1 - x_2 \\
y_p - y_2 & y_1 - y_2 \\
\end{vmatrix}
} {
\begin{vmatrix}
x_0 - x_2 & x_1 - x_2 \\
y_0 - y_2 & y_1 - y_2 \\
\end{vmatrix}
},
v=\frac{D_v}{D}=\frac{
\begin{vmatrix}
x_0 - x_2 & x_p - x_2 \\
y_0 - y_2 & y_p - y_2 \\
\end{vmatrix}
} {
\begin{vmatrix}
x_0 - x_2 & x_1 - x_2 \\
y_0 - y_2 & y_1 - y_2 \\
\end{vmatrix}
}
\]
定义 U:
\[
\begin{aligned}
U &=
\begin{pmatrix}
x_0 - x_2 & x_1 - x_2 & x_2 - x_p
\end{pmatrix}
\times
\begin{pmatrix}
y_0 - y_2 & y_1 - y_2 & y_2 - y_p
\end{pmatrix} \\
&=
\begin{vmatrix}
i & j & k\\
x_0 - x_2 & x_1 - x_2 & x_2 - x_p \\
y_0 - y_2 & y_1 - y_2 & y_2 - y_p
\end{vmatrix} \\
&=
\begin{pmatrix}
D_u & D_v & D
\end{pmatrix}
\end{aligned}
\]
则:
\[
u=\frac{U.x}{U.z}, v=\frac{U.y}{U.z}
\]
代码:
C++ vec3 barycentric ( vec3 * pts , vec3 p ) {
vec3 u =
cross ( vec3 { pts [ 0 ]. x - pts [ 2 ]. x , pts [ 1 ]. x - pts [ 2 ]. x , pts [ 2 ]. x - p . x },
vec3 { pts [ 0 ]. y - pts [ 2 ]. y , pts [ 1 ]. y - pts [ 2 ]. y , pts [ 2 ]. y - p . y });
if ( abs ( u . z ) < 1 )
return vec3 { -1 , 1 , 1 };
return vec3 { u . x / u . z , u . y / u . z , 1 - u . x / u . z - u . y / u . z };
}
void triangle4 ( vec3 * pts , TGAImage & image , TGAColor color ) {
// 确定包围盒大小
vec2 bboxmin = {
image . width () - 1. ,
image . height () - 1. }; // 记录左上角,初始化为最大值(右下角)
vec2 bboxmax = { 0. , 0. }; // 记录右下角,初始化为最小值(左上角)
vec2 clamp = { image . width () - 1. , image . height () - 1. }; // 限制大小
for ( int i = 0 ; i < 3 ; i ++ ) {
bboxmin . x = max ( 0. , min ( bboxmin . x , pts [ i ]. x ));
bboxmin . y = max ( 0. , min ( bboxmin . y , pts [ i ]. y ));
bboxmax . x = min ( clamp . x , max ( bboxmax . x , pts [ i ]. x ));
bboxmax . y = min ( clamp . y , max ( bboxmax . y , pts [ i ]. y ));
}
// 判断是否在三角形内
for ( int x = bboxmin . x ; x <= bboxmax . x ; x ++ ) {
for ( int y = bboxmin . y ; y <= bboxmax . y ; y ++ ) {
vec3 bf = barycentric ( pts , {( double ) x , ( double ) y , 0. });
if ( bf . x < 0 || bf . y < 0 || bf . z < 0 )
continue ;
image . set ( x , y , color );
}
}
}
结果:
平面着色
随机着色
这里自己加了vec2i
结构体,用来表示vector<2,int>
。
代码:
C++ #include "model.h"
#include "tgaimage.h"
using namespace std ;
vec3 barycentric ( vec2i * pts , vec2i p ) {
vec3 u = cross ( vec3 { static_cast < double > ( pts [ 0 ]. x - pts [ 2 ]. x ),
static_cast < double > ( pts [ 1 ]. x - pts [ 2 ]. x ),
static_cast < double > ( pts [ 2 ]. x - p . x )},
vec3 { static_cast < double > ( pts [ 0 ]. y - pts [ 2 ]. y ),
static_cast < double > ( pts [ 1 ]. y - pts [ 2 ]. y ),
static_cast < double > ( pts [ 2 ]. y - p . y )});
if ( abs ( u . z ) < 1 )
return vec3 { -1 , 1 , 1 };
return vec3 { u . x / u . z , u . y / u . z , 1 - u . x / u . z - u . y / u . z };
}
void triangle4 ( vec2i * pts , TGAImage & image , TGAColor color ) {
// 确定包围盒大小
vec2i bboxmin = {
image . width () - 1 ,
image . height () - 1 }; // 记录左上角,初始化为最大值(右下角)
vec2i bboxmax = { 0 , 0 }; // 记录右下角,初始化为最小值(左上角)
vec2i clamp = { image . width () - 1 , image . height () - 1 }; // 限制大小
for ( int i = 0 ; i < 3 ; i ++ ) {
bboxmin . x = max ( 0 , min ( bboxmin . x , pts [ i ]. x ));
bboxmin . y = max ( 0 , min ( bboxmin . y , pts [ i ]. y ));
bboxmax . x = min ( clamp . x , max ( bboxmax . x , pts [ i ]. x ));
bboxmax . y = min ( clamp . y , max ( bboxmax . y , pts [ i ]. y ));
}
for ( int x = bboxmin . x ; x <= bboxmax . x ; x ++ ) {
for ( int y = bboxmin . y ; y <= bboxmax . y ; y ++ ) {
vec3 bf = barycentric ( pts , { x , y });
if ( bf . x < 0 || bf . y < 0 || bf . z < 0 )
continue ;
image . set ( x , y , color );
}
}
}
int main ( int argc , char ** argv ) {
Model * model = nullptr ;
if ( 2 == argc ) {
model = new Model ( argv [ 1 ]);
} else {
model = new Model ( "obj/african_head.obj" );
}
int width = 1000 ;
int height = 1000 ;
TGAImage image ( width , height , TGAImage :: RGB );
for ( int i = 0 ; i < model -> nfaces (); i ++ ) {
vec2i screen_coords [ 3 ];
for ( int j = 0 ; j < 3 ; j ++ ) {
vec3 v0 = model -> vert ( i , j );
int x0 = ( v0 . x + 1. ) * width / 2. ;
int y0 = ( v0 . y + 1. ) * height / 2. ;
screen_coords [ j ] = vec2i { x0 , y0 };
}
// 随机生成颜色
unsigned char r = rand () % 255 ;
unsigned char g = rand () % 255 ;
unsigned char b = rand () % 255 ;
TGAColor { r , g , b , 255 };
triangle4 ( screen_coords , image , TGAColor { r , g , b , 255 });
}
image . write_tga_file ( "tri_head_rand.tga" );
return 0 ;
}
结果:
光照
认为光照强度等于光向量与三角形法线向量的点积。
代码:
C++ int main ( int argc , char ** argv ) {
Model * model = nullptr ;
if ( 2 == argc ) {
model = new Model ( argv [ 1 ]);
} else {
model = new Model ( "obj/african_head.obj" );
}
int width = 1000 ;
int height = 1000 ;
vec3 light_dir = { 0 , 0 , -1 };
TGAImage image ( width , height , TGAImage :: RGB );
for ( int i = 0 ; i < model -> nfaces (); i ++ ) {
vec2i screen_coords [ 3 ];
vec3 world_coords [ 3 ];
for ( int j = 0 ; j < 3 ; j ++ ) {
vec3 v0 = model -> vert ( i , j );
int x0 = ( v0 . x + 1. ) * width / 2. ;
int y0 = ( v0 . y + 1. ) * height / 2. ;
screen_coords [ j ] = vec2i { x0 , y0 };
world_coords [ j ] = v0 ;
}
// 用叉乘计算法向量并单位化
vec3 n = cross ( world_coords [ 2 ] - world_coords [ 0 ],
world_coords [ 1 ] - world_coords [ 0 ]);
double len = norm ( n );
vec3 unit_n = normalized ( n );
// 计算光照强度
double intensity = unit_n * light_dir ;
cout << intensity << '\n' ;
if ( intensity > 0 ) {
triangle4 ( screen_coords , image ,
TGAColor ( intensity * 255 , intensity * 255 ,
intensity * 255 , 255 ));
}
}
image . write_tga_file ( "tri_head_rand.tga" );
return 0 ;
}
结果:
因为没有用 z-buffer,嘴巴内表面渲染后覆盖了外表面的结果(?)
深度缓冲
按图像的大小构造 z-buffer 数组,初始化为负无穷,遍历三角形时对其中每个像素的值取最大(规定 z 值越大表示离摄像机越近)。
z-buffer 用于记录每个像素的深度,保证绘制前面的图形、遮挡后面的图形。
代码:
C++ // 计算zbuffer
double z = 0. ;
for ( int k = 0 ; k < 3 ; k ++ ) {
z += pts [ k ]. z * bc [ k ]; // 用重心坐标得到深度的插值
}
if ( z > zbuffer [ static_cast < int > ( x + y * width )]) {
zbuffer [ static_cast < int > ( x + y * width )] = z ;
image . set ( x , y , color ); // 当距离更近时绘制
}
结果:
Zbuffer结果:
2025-09-05
2025-07-26
GitHub