文章目录
The Camera相机
在世界空间定义相机
- 相机位置点 e ⃗ \vec{e} e
- 拍摄方向 g ^ \hat{g} g^
- 向上的方向 t ^ \hat{t} t^(垂直于拍摄方向)
- 相机空间的正交基 ( g ^ × t ^ , t ^ , g ^ ) (\hat{g}\times\hat{t}, \hat{t}, \hat{g}) (g^×t^,t^,g^)
把世界空间变换到相机空间,标准基(X,Y,-Z)
- 平移相机位置到原点
- 旋转相机,使得拍摄方向到-Z,向上方向到Y
观察变换 M v i e w ⋅ v w o r l d = R v i e w ⋅ T v i e w ⋅ v w o r l d M_{view}\cdot v_{world}=R_{view}\cdot T_{view}\cdot v_{world} Mview⋅vworld=Rview⋅Tview⋅vworld
这个方程表示了将世界空间中的一个向量 v w o r l d v_{world} vworld转换到相机或观察空间的过程, M v i e w M_{view} Mview通常是视图变换矩阵, R v i e w R_{view} Rview是旋转变换矩阵, T v i e w T_{view} Tview是平移变换矩阵。
- 先将相机从位置 e ⃗ \vec{e} e移到原点
- 再把拍摄方向 g ^ \hat{g} g^旋转到-Z上
- 再把向上方向
t
^
\hat{t}
t^旋转到Y
OpenGL Transformation
在同一个矩阵设置Model Transformation,Viewing Transformation
Model Transformation,模型变换,指的是将模型从模型坐标系(局部坐标系)变换到世界坐标系中的过程。在OpenGL中,模型变换通常通过设置模型矩阵(Model Matrix)来实现。模型矩阵负责将模型的顶点坐标从局部坐标系变换到世界坐标系,包括平移(Translation)、旋转(Rotation)、缩放(Scaling)等操作。
Viewing Transformation,视图变换,指的是将场景或物体从世界坐标系变换到相机或观察者的视角空间中的过程。在OpenGL中,视图变换通常通过设置视图矩阵(View Matrix)来实现。视图矩阵负责将世界坐标系中的物体位置和方向变换到相机或观察者的视角空间中,用于定义观察者的视角和观察方向。
void glMatrixMode(GL_MODELVIEW):用于设置当前矩阵堆栈模式的函数调用。这个函数告诉OpenGL后续对矩阵堆栈的操作是针对模型视图矩阵(Model-View Matrix)的。默认设置是ModelView,除非用GL_PROJECTION作为参数。
使用glLoadIndentity()初始化矩阵
使用glMultMatrix()复合变换矩阵
Viewing Transformation
OpenGL提供gluLookAt()计算,用户提供:相机位置
e
⃗
=
e
y
e
\vec{e}=eye
e=eye,拍摄方向
g
^
=
c
e
n
t
e
r
−
e
y
e
\hat{g}=center-eye
g^=center−eye,向上方向
t
^
=
u
p
\hat{t}=up
t^=up
void gluLookAt(
GLdouble
e
y
e
x
eye_x
eyex, GLdouble
e
y
e
y
eye_y
eyey, GLdouble
e
y
e
z
eye_z
eyez,
GLdouble
c
e
n
t
e
r
x
center_x
centerx, GLdouble
c
e
n
t
e
r
y
center_y
centery, GLdouble
c
e
n
t
e
r
z
center_z
centerz,
GLdouble
u
p
x
up_x
upx, GLdouble
u
p
y
up_y
upy, GLdouble
u
p
z
up_z
upz);
默认设置: gluLookat(0.0, 0.0, 0.0, 0.0, 0.0, -100.0, 0.0, 1.0, 0.0);
Model Transformation
通过复合下列变换,把物体放入视锥
- void glTranslate{fd}(TYPE x, TYPE y, TYPE z);将当前指定的矩阵(通常是模型视图矩阵或投影矩阵)进行平移变换。
- void glRotate{fd}(TYPE angle, TYPE x, TYPE y, TYPE z);将当前指定的矩阵进行旋转变换。
- void glScale{fd}(TYPE x, TYPE y, TYPE z);将当前指定的矩阵进行缩放变换。
- void glLoadMatrix{fd}(const TYPE *m);将指定的 4x4 矩阵 m 加载到当前指定的矩阵堆栈顶部
- void glMultMatrix{fd}(const TYPE *m);将指定的 4x4 矩阵 m 与当前指定的矩阵相乘,并将结果替换为当前指定的矩阵。
OpenGL Model Transformation三维流程
- 首先
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();用于将当前处理的矩阵重置为单位矩阵 - 其次
gluLookat( e x , e y , e z , c e n t e r x , c e n t e r y , c e n t e r z , u p x , u p y , u p z e_x, e_y, e_z, center_x, center_y, center_z, up_x, up_y, up_z ex,ey,ez,centerx,centery,centerz,upx,upy,upz); - 最后,比如绕着物体中心点(
c
e
n
t
e
r
x
,
c
e
n
t
e
r
y
,
c
e
n
t
e
r
z
center_x, center_y, center_z
centerx,centery,centerz),按Z轴转动90°
glTranslatef( c e n t e r x , c e n t e r y , c e n t e r z center_x, center_y, center_z centerx,centery,centerz),
glRotate(90, 0, 0, 1),
glTranslatef( − c e n t e r x , − c e n t e r y , − c e n t e r z -center_x, -center_y, -center_z −centerx,−centery,−centerz)
Draw_Obj() - 可选项:保存变换栈glPushMatrix()和glPopMatrix()
法向量
法向量来源:
- 来自OBJ文件,读取“VN 1.0 2.0 3.0”
VN:是OBJ文件中用于表示法向量的关键字。
1.0 2.0 3.0:表示一个三维向量,其中:
1.0 是法向量在 X 轴方向上的分量(即法向量的 X 分量)。
2.0 是法向量在 Y 轴方向上的分量(即法向量的 Y 分量)。
3.0 是法向量在 Z 轴方向上的分量(即法向量的 Z 分量)。 - 自己根据网格顶点坐标计算
1)面法向量
2)顶点法向量
法向量变换:
- Model-View变换矩阵。法向量变换矩阵是
(
M
T
)
−
1
(M^T)^{-1}
(MT)−1。当
M
M
M是正交矩阵时,
M
=
(
M
T
)
−
1
M=(M^T)^{-1}
M=(MT)−1
推导:法向量 n n n,曲面切线 v v v,变换矩阵 M M M
切向量 v v v经过线性变换 M M M后, v ′ = M ⋅ v v'=M\cdot v v′=M⋅v
因为对任意切向量 v v v,都有 < v , n > = v T ⋅ n = 0 <v,n>=v^T\cdot n=0 <v,n>=vT⋅n=0
v T ⋅ M T ⋅ ( M T ) − 1 ⋅ n = 0 ( M ⋅ v ) T ⋅ ( M T ) − 1 ⋅ n = 0 ( v ′ ) T ⋅ ( M T ) − 1 ⋅ n = 0 < v ′ , ( M T ) − 1 ⋅ n > = 0 所以 n ′ = ( M T ) − 1 ⋅ n v^T\cdot M^T\cdot (M^T)^{-1}\cdot n=0\\ (M\cdot v)^T\cdot (M^T)^{-1}\cdot n=0\\ (v')^T\cdot (M^T)^{-1}\cdot n=0\\ <v', (M^T)^{-1}\cdot n>=0\\ 所以n'=(M^T)^{-1}\cdot n vT⋅MT⋅(MT)−1⋅n=0(M⋅v)T⋅(MT)−1⋅n=0(v′)T⋅(MT)−1⋅n=0<v′,(MT)−1⋅n>=0所以n′=(MT)−1⋅n
OpenGL法向量设置
在绘制顶点前先指定顶点法向量
glRotate(90,0,0,1),
…
glBegin(GL_TRIANGLES);
for(int k=0;k<3;k++){
glNormal3f(n.x, n.y, n.z);//设置顶点法向量
glVertex3f(v.x, v.y, v.z);
}
glEnd();
投影变换
正交投影
相机坐标下,相机在原点,看向-Z,向上为Y
移除掉物体的Z坐标
将结果缩放后映射到
[
−
1
,
1
]
2
[-1,1]^2
[−1,1]2
透视投影
平行线相交于一点,满足近大远小
将视角锥内点(x,y,z,1)投影至近平面n
再进行正交投影
如何求透视变换:
- 先将视角锥内点(x,y,z,1)投影至近平面n
M p e r s p − > o r t h o 4 × 4 = [ n 0 0 0 0 n 0 0 0 0 n + f − n f 0 0 1 0 ] M^{4\times 4}_{persp->ortho}=\begin{bmatrix}n&0&0&0\\0&n&0&0\\0&0&n+f&-nf\\0&0&1&0\end{bmatrix} Mpersp−>ortho4×4= n0000n0000n+f100−nf0
M p e r s p − > o r t h o ⋅ [ x y z 1 ] = [ n x n y ( n + f ) z − n f z ] M_{persp->ortho}\cdot\begin{bmatrix}x \\y\\z\\1\end{bmatrix}=\begin{bmatrix}nx\\ny\\(n+f)z-nf\\z\end{bmatrix} Mpersp−>ortho⋅ xyz1 = nxny(n+f)z−nfz
- 再进行正交投影,即合成为 M p e r s p = M o r t h o ⋅ M p e r s p → o r t h o M_{persp}=M_{ortho}\cdot M_{persp\to ortho} Mpersp=Mortho⋅Mpersp→ortho
Frustum 视锥
视锥定义:
- 宽高比(aspect ratio),如4:3,16:9
- 垂直可视高度(field-of-view Y, fovY)
- 近平面n
- 远平面f
计算视锥变换的长方体
t
a
n
f
o
v
Y
2
=
t
∣
n
∣
和
a
s
p
e
c
t
=
r
t
tan\frac{fovY}{2}=\frac{t}{|n|}和aspect=\frac{r}{t}
tan2fovY=∣n∣t和aspect=tr
投影转换Projection Transformations
透视投影
void gluPerspective(double
f
o
v
y
fovy
fovy,double
a
s
p
e
c
t
aspect
aspect, double
z
N
e
a
r
zNear
zNear, double
z
F
a
r
zFar
zFar);
用于设置透视投影矩阵。这个函数可以帮助定义透视投影的视角、屏幕宽高比、近裁剪面和远裁剪面的参数,以便正确设置视景体和投影矩阵,从而实现透视投影效果。
void glFrustum(GLdouble
l
e
f
t
left
left, GLdouble
r
i
g
h
t
right
right, GLdouble
b
o
t
t
o
m
bottom
bottom, GLdouble
t
o
p
top
top, GLdouble
n
e
a
r
near
near, GLdouble
f
a
r
far
far);
glFrustum 是 OpenGL 中用于设置透视投影矩阵的函数之一,它定义了一个视景体的截锥体(Frustum),用于将三维空间中的物体坐标转换为二维屏幕坐标,实现透视投影效果。
left, right:指定视景体左侧和右侧面的位置,即截锥体左侧和右侧面在 X 轴上的位置坐标。通常 left 小于 right。
bottom, top:指定视景体底部和顶部面的位置,即截锥体底部和顶部面在 Y 轴上的位置坐标。通常 bottom 小于 top。
near, far:指定视景体的近裁剪面和远裁剪面的位置,即截锥体的近端和远端距离视点的距离。near 表示近裁剪面的距离,必须为正值;far 表示远裁剪面的距离,必须大于 near。
正交投影
void glOrtho(double
l
e
f
t
left
left, double
r
i
g
h
t
right
right, double
b
o
t
t
o
m
bottom
bottom, double
t
o
p
top
top, double
z
N
e
a
r
zNear
zNear, double
z
F
a
r
zFar
zFar);
glOrtho 是 OpenGL 中用于设置正交投影矩阵的函数之一,它定义了一个正交投影的视景体(Orthographic Frustum),用于将三维空间中的物体坐标转换为二维屏幕坐标,实现正交投影效果。
投影转换流程
-
glMatrixMode(GL_PROJECTION);
glLoadIdentity(); -
void gluPerspective();
void glFrustum();
void glOrtho();
转换顺序
- 首先Modeling:把物体放入世界空间
M m o d e l = M r o t a t e ⋅ M t r a n s l a t e ⋅ M m o d e l M_{model}=M_{rotate}\cdot M_{translate}\cdot M_{model} Mmodel=Mrotate⋅Mtranslate⋅Mmodel - 再Viewing:摆好相机,切换到相机空间
M v i e w = R v i e w ⋅ T v i e w M_{view}=R_{view}\cdot T_{view} Mview=Rview⋅Tview - 然后拍摄,场景投影到近平面
M p e r s p = M o r t h o ⋅ M p e r s p → o r t h o M_{persp}=M_{ortho}\cdot M_{persp\to ortho} Mpersp=Mortho⋅Mpersp→ortho - 最后视屏变换: M v i e w p o r t M_{viewport} Mviewport
M
t
r
a
n
s
f
o
r
m
=
M
v
i
e
w
p
o
r
t
⋅
M
p
e
r
s
p
⋅
M
v
i
e
w
⋅
M
m
o
d
e
l
M_{transform}=M_{viewport}\cdot M_{persp}\cdot M_{view}\cdot M_{model}
Mtransform=Mviewport⋅Mpersp⋅Mview⋅Mmodel
y
=
M
v
p
o
r
t
⋅
(
M
p
r
o
j
⋅
(
M
m
v
⋅
x
)
)
=
(
M
v
p
o
r
t
⋅
M
p
r
o
j
⋅
M
m
v
)
⋅
x
y=M_{vport}\cdot(M_{proj}\cdot (M_{mv}\cdot x))=(M_{vport}\cdot M_{proj}\cdot M_{mv})\cdot x
y=Mvport⋅(Mproj⋅(Mmv⋅x))=(Mvport⋅Mproj⋅Mmv)⋅x
分层模型
- 一个复杂的对象可以由相对简单的对象构成
- 主部件的变换作用到副部件
- 可以用树结构描述这种变换关系
- 每个节点有自己的局部坐标
分层模型1:
glMatrixMode(GL_MODEVIEW);
glLoadIdentity();
glTranslatef(bx, by, bz);
create_base(); //创建基座(base)或基础部分的物体。
glTranslatef(0, j1y, 0);
glRotatef(joint1_orientation);
create_joint1(); //创建关节1
glTranslatef(0, uay, 0);
create_upperarm(); //创建上臂
glTranslatef(0, j2y);
glRotatef(joint2_orientation);
create_joint2(); //创建关节2
glTranslatef(0, lay, 0);
create_lowerarm(); //创建下臂
glTranslatef(0, py, 0);
glRotatef(point_orientation);
create_pointer(); //创建指针
分层模型2:
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glTranslatef(bx, by, bz);
create_base();
glTranslatef(0, jy, 0);
glRotatef(joint1_orientation);
create_joint1();
glTranslatef(0, ay, 0);
create_upperarm();
glTranslatef(0, wy);
glRotatef(wrist_orientation);
create_wrist();
glPushMatrix();//save frame
glTranslatef(-xf, fy0, 0);
glRotatef(lowerfinger1_orientation);
glTranslatef(0, fy1, 0);
create_lowerfinger1();
glTranslatef(0, fy2, 0);
glRotatef(upperfinger1_orientation);
create_fingerjoint1();
glTranslatef(0, fy3, 0);
create_upperfinger1();
glPopMatrix(); //restore frame
glPushMatrix();
// do finger 2…
glPopMatrix();
glPushMatrix();
// do finger 3…
glPopMatrix();
剪切 Clipping
投影变换后,齐次剪切空间(Homogeneous Clip Space)
裁剪操作
- 删除视野外的图元
- 保留视野内的图元
- 裁剪部分在视野内的图元
- 关键在于快速实现
线段裁剪
- Cohen-Sutherland algorithm
多边形裁剪 - Sutherland-Hodgman algorithm
线段裁剪
平面分成九个区域:
- Top: 0x1000
- Bottom: 0x0100
- Right: 0x0010
- Left: 0x0001
接受:两个点的位置码(OR)为0
淘汰:两个点的位置码(AND)不为0
其余:按TBRL循环裁剪直到接受或者淘汰
OR和AND指按位或和按位与
多边形裁剪
多边形
P
=
{
p
1
,
p
2
,
p
3
,
p
4
}
P=\{p_1,p_2,p_3,p_4\}
P={p1,p2,p3,p4}
多边形裁剪窗口(长方形)
按裁剪窗口的边l依次对多边形P裁剪
输入是多边形,输出裁剪后的多边形
foreach clipping edge l:
SH_CLIPPING(l, in_P, out_P)
in_P = out_P
def SH_CLIPPING(l, in_P, out_P):
foreach edge (
p
i
,
p
j
p_i,p_j
pi,pj) of in_P:
case 1: Front & Front
case 2: Front & Back
case 3: Back & Front
case 4: Back & Back
def SH_CLIPPING(
l
,
i
n
p
,
o
u
t
p
l,in_p,out_p
l,inp,outp):
1. 边
(
p
1
,
p
2
)
:
o
u
t
p
=
{
p
2
}
(p_1,p_2):out_p=\{p_2\}
(p1,p2):outp={p2}
2. 边
(
p
2
,
p
3
)
:
o
u
t
p
=
{
p
2
,
p
3
}
(p_2,p_3):out_p=\{p_2,p_3\}
(p2,p3):outp={p2,p3}
3. 边
(
p
3
,
p
4
)
:
o
u
t
p
=
{
p
2
,
p
3
,
p
3
′
}
(p_3,p_4):out_p=\{p_2,p_3,p'_3\}
(p3,p4):outp={p2,p3,p3′}
4. 边
(
p
4
,
p
1
)
:
o
u
t
p
=
{
p
2
,
p
3
,
p
3
′
,
p
4
′
,
p
1
}
(p_4,p_1):out_p=\{p_2,p_3,p'_3,p'_4,p_1\}
(p4,p1):outp={p2,p3,p3′,p4′,p1}
归一化处理
透视纠正
用在纹理贴图纠正等。线性插值时对于远的区域插值纠正。齐次坐标保留W(=Z)值的用途之一
从齐次坐标转换成欧式空间坐标(除以W=Z)
生成NDC(Normalized Device Coordinates)
NDC(Normalized Device Coordinates)指的是归一化的设备坐标。它是将物理设备坐标系转换为标准化设备坐标系的过程,这个转换过程是为了方便计算机进行图形渲染和显示。
屏幕映射(Screen Mapping)
从NDC坐标屏幕映射:把标准二维图像缩放到屏幕坐标
输出每个图元顶点的:
- 屏幕坐标 ( x , y ) (x,y) (x,y)和深度 z z z坐标
- 颜色 ( r , g , b ) (r,g,b) (r,g,b)和透明度alpha
- 纹理坐标
(
s
,
t
)
(s,t)
(s,t)
从NDC到屏幕坐标:
- 屏幕左下角是(0,0)
- 把(-1,-1)移动到(0,0)
- x轴放大 width/2,y轴放大height/2
M v i e w p o r t = [ w i d t h 2 0 0 w i d t h 2 0 h e i g h t 2 0 h e i g h t 2 0 0 1 0 0 0 0 1 ] M_{viewport}=\begin{bmatrix}\frac{width}{2}&0&0&\frac{width}{2}\\0&\frac{height}{2}&0&\frac{height}{2}\\0&0&1&0\\0&0&0&1\end{bmatrix} Mviewport= 2width00002height0000102width2height01
调用glViewport来设置
void glViewport(GLint x, GLint y,GLsizei width, GLsiezi height)
(x, y):viewport左下角相对于窗口左下角位置
width/height:viewport的宽高
默认是(0,0, window_width, window_height),从NDC映射到(x,y,width,height)
一般来说,viewport的宽高比和视锥的宽高比设置为一样
不然最后屏幕图像会变形
void reshape(int w, int h)
{
glViewport(0,0,(GLsizei)w,(GLsizei)h);
glMatrixMode(GL_PROJECTION);
glLoadIdentiy();
//透视变换1,保持视锥比例,拉伸到整个窗口
gluPerspective(60,1,1,10);
//透视变换2,调整视锥宽高比,和窗口一样
gluPerspective(60,(float)w/(float)h,1,10);
gluPostRedisplay();
}
Tessellation Shader细分着色器
曲面细分着色器:在已有图元的基础上加入更多Vertex,这些Vertex还是在原始的图元内,以形成更精细的模型
案例
谢尔宾斯基三角形Sieroinski Triangle
void draw_triangle(vec2 a, vec2 b, vec2 c)
{
glBegin(GL_TRIANGLES);
glVertex2f(a.x, a.y);
glVertex2f(b.x, b.y);
glVertex2f(c.x, c.y);
glEnd();
}
void divide_triangle(vec2 a, vec2 b, vec2 c, int depth)
{
if(depth <= 0)
{
return draw_triangle(a, b, c);
}
vec2 v1 = (a + b) * 0.5f;
vec2 v2 = (a + b) * 0.5f;
vec2 v3 = (b + c) * 0.5f;
divide_triangle(a, v1, v2, depth - 1);
divide_triangle(c, v2, v3, depth - 1);
divide_triangle(b, v3, v1, depth - 1);
}
科赫雪花Koch Snowflake
vec3 compute_koch_point(vec3 a, vec3 b)//旋转线段
{
vec3 seg = b - a;
vec3 ret;
ret.x = seg.x * cos(radians(60.0f)) - seg.y * sin(radians(60.0f));//旋转矩阵
ret.y = seg.x * sin(radians(60.0f)) + seg.y * cos(radians(60.0f));
ret.z = 0.0f;
return ret + a;
}
void divide_line(vec3 a, vec3 b, int m)
{
if(m <= 0)
{
return draw_line(a, b);
}
vec3 v1 = a * (2.f / 3.f) + b * (1.f / 3.f);
vec3 v3 = a * (1.f / 3.f) + b * (2.f / 3.f);
vec3 v2 = compute_koch_point(v1, v3);
divide_line(a, v1, m - 1);
divide_line(v1, v2, m - 1);
divide_line(v2, v3, m - 1);
divide_line(v3, b, m - 1);
}
几何着色器
逐图元着色操作,生成新的图元
与镶嵌相反,在原始图元外增加新的Vertex