TinyRenderer
tinyrenderer
跟着ssloy/tinyrenderer: A brief computer graphics / rendering course (github.com)项目实现一个基础的渲染器。
ZhangFirst1/tinyrenderer: 跟随tinyrenderer项目实现 (github.com)
Lesson 1: 画直线
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)) { //直线斜率大于1
//对调端点(x,y)坐标,使得斜率小于1,不会产生y的间隔
std::swap(x0, y0);
std::swap(x1, y1);
steep = true;
}
if (x0 > x1) { //保证x从小到大
std::swap(x0, x1);
std::swap(y0, y1);
}
int dx = x1 - x0;
int dy = y1 - y0;
//float derror = std::abs(dy / float(dx)); //使用累计误差的方法优化
int derror2 = std::abs(dy) * 2; //两边*2 等式变形 无需使用除法和浮点数
int error2 = 0;
int y = y0;
for (float x = x0; x <= x1; x++) {
//float t = (x - x0) / (float)(x1 - x0);
//int y = y0 * (1.0 - t) + y1 * t;
if (steep == true) {
image.set(y, x, color);
}
else {
image.set(x, y, color);
}
error2 += derror2;
if (error2 > dx) {
y += (y1 > y0 ? 1 : -1);
error2 -= dx * 2;
}
}
}
Lesson 2: 画线框
使用.obj格式文件
1.v顶点(Geometric vertices)
v 1.000000 -1.000000 -1.000000
-
格式: vt u v w
-
意义:表示顶点,每个顶点的坐标
2. vt 顶点纹理坐标 (Texture vertices)
vt 0.748573 0.750412
vt 0.749279 0.501284
vt 0.999110 0.501077
- 格式:vt u v w
- 意义:绘制模型的三角面片时,每个顶点取像素点时对应的纹理图片上的坐标。纹理图片的坐标指的是,纹理图片如果被放在屏幕上显示时,以屏幕左下角为原点的坐标。
- 注意:w 一般用于形容三维纹理,大部分是用不到的,基本都为 0
3. vn 顶点法向量 (Vertex normals)
vn 0.000000 0.000000 -1.000000
vn -1.000000 -0.000000 -0.000000
vn -0.000000 -0.000000 1.000000
- 格式:vn x y z
- 意义:绘制模型三角面片时,需要确定三角面片的朝向,整个面的朝向,是由构成每个面的顶点对应的顶点法向量的做矢量和决定的(xyz 的坐标分别相加再除以 3 得到的)。
4. f 面 (Face)
f 5/1/1 1/2/1 4/3/1
f 5/1/1 4/3/1 8/4/1
f 3/5/2 7/6/2 8/7/2
- 格式 :f v/vt/vn v/vt/vn v/vt/vn(f 顶点索引 / 纹理坐标索引 / 顶点法向量索引)
测试代码:
if (2 == argc) {
model = new Model(argv[1]); //命令行方式构造
}
else {
model = new Model("obj/african_head.obj"); //代码构造
}
TGAImage image(width, height, TGAImage::RGB);
for (int i = 0; i < model->nfaces(); i++) {
std::vector<int> face = model->face(i); //保存一个面的三个顶点坐标
for (int j = 0; j < 3; j++) {
//根据两个顶点划线
Vec3f v0 = model->vert(face[j]);
Vec3f v1 = model->vert(face[(j + 1) % 3]);
//模型坐标 -> 屏幕坐标
//[-1,1] -> [0,2] -> [0,800]
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);
}
}
image.flip_vertically(); // i want to have the origin at the left bottom corner of the image
image.write_tga_file("output.tga");
delete model;
Lesson 3: 三角形光栅化和Z-buffer
逐行画法(需要重载上方的line适配Vec2i)
/*使用逐行画法*/
void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage& image, TGAColor color) {
if (t0.y == t1.y && t1.y == t2.y) return; //三角形面积为零
//保证t0->t2是从底到上
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);
//以y轴为差填充三角形
int tall = t2.y - t0.y;
for (int i = 0; i < tall; i++) {
//根据t1将三角形分成上下两部分
bool second_half = i > t1.y - t0.y || t1.y == t0.y; //上部为1 下部为0
int segment_height = second_half ? t2.y - t1.y : t1.y - t0.y; //上下部高度
float alpha = (float)i / tall;
float beta = (float)(i - (second_half ? t1.y - t0.y : 0)) / segment_height;
//计算左右两点坐标
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);
//根据A和B对当前高度着色
for (int j = A.x; j <= B.x; j++) {
image.set(j, t0.y + i, color);
}
}
}
bounding box
/*
使用重心坐标判断三角形bounding box内像素是否在三角形内绘制三角形
*/
Vec3f barycentric(Vec3f A, Vec3f B, Vec3f C, Vec3f 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]);
//z分量为零 不在三角形内
if (std::abs(u[2]) > 1e-2) return Vec3f(1.f - (u.x + u.y) / u.z, u.y / u.z, u.x / u.z);
//返回P点重心坐标
return Vec3f(-1, 1, 1);
}
//重载triangle
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>::max(), -std::numeric_limits<float>::max());
Vec2f clamp(image.get_width() - 1, image.get_height() - 1);
//确定bounding box 大小
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 screen = barycentric(pts[0], pts[1], pts[2], P);
//三个分量之一小于零说明像素在三角形外
if (screen.x < 0 || screen.y < 0 || screen.z < 0) continue;
P.z = 0;
//计算zbuffer 并 每个顶点的z值乘上对应的质心坐标分量
for (int i = 0; i < 3; i++) P.z += pts[i][2] * screen[i];
if (zbuffer[int(P.x + P.y * width)] < P.z) {
zbuffer[int(P.x + P.y * width)] = P.z;
image.set(P.x, P.y, color);
}
}
}
if (2 == argc) {
model = new Model(argv[1]); //命令行方式构造
}
else {
model = new Model("obj/african_head.obj"); //代码构造
}
TGAImage image(width, height, TGAImage::RGB);
//创建zbuffer并初始化为一个很小的值
float* zbuffer = new float[width * height];
for (int i = width * height; i--; zbuffer[i] = -std::numeric_limits<float>::max());
Vec3f light_dir(0, 0, -1); // define light_dir
for (int i = 0; i < model->nfaces(); i++) {
std::vector<int> face = model->face(i);
//屏幕坐标与世界坐标
Vec3f world_coords[3];
for (int j = 0; j < 3; j++) {
world_coords[j] = model->vert(face[j]);
}
//法线方向 并 正交化
Vec3f n = cross((world_coords[2] - world_coords[0]), (world_coords[1] - world_coords[0]));
n.normalize();
//光照强度=法向量*光线方向(点乘)
float intensity = n * light_dir;
if (intensity > 0) {
Vec3f pts[3];
for (int j = 0; j < 3; j++) pts[j] = world2screen(model->vert(face[j]));
triangle(pts, zbuffer, image, TGAColor(intensity * 255, intensity * 255, intensity * 255, 255));
//triangle(pts, zbuffer, image, TGAColor(rand() % 255, rand() % 255, rand() % 255, 255));
}
}
image.flip_vertically(); // i want to have the origin at the left bottom corner of the image
image.write_tga_file("output3.tga");
delete model;
Lesson 4 & 5: 变换
3D物体渲染到2D屏幕的矩阵变换过程:模型变换(Modeling Trans)、视图变换(View Trans)和投影变换(Projection Trans) - 知乎 (zhihu.com)
Viewport * Projection * View * Model * v.
Vec3f v = model->vert(face[j]);
screen_coords[j] = Vec3f(ViewPort*Projection*ModelView*Matrix(v));
1.首先将模型三角形平面的三个顶点向量转为矩阵
Matrix v2m(Vec3f v) {
Matrix m(4, 1);
m[0][0] = v.x;
m[1][0] = v.y;
m[2][0] = v.z;
m[3][0] = 1.f;
return m;
}
2.ModelView(视图变换)变换,改变相机位置与角度
Matrix lookat(Vec3f eye, Vec3f center, Vec3f up) {
Vec3f z = (eye - center).normalize();
Vec3f x = (up ^ z).normalize();
Vec3f y = (z ^ x).normalize();
Matrix rotation = Matrix::identity(4);
Matrix translation = Matrix::identity(4);
for (int i = 0; i < 3; i++) {
translation[i][3] = -center[i];
}
for (int i = 0; i < 3; i++) {
rotation[0][i] = x[i];
rotation[1][i] = y[i];
rotation[2][i] = z[i];
}
return rotation * translation;
}
Matrix ModelView = lookat(eye, center, Vec3f(0,1,0));
3.Projection(投影变换)变换,产生透视投影效果
Matrix Projection = Matrix::identity(4);
Projection[3][2] = -1.f / (eye - center).norm();
4.ViewPort(视口变换)变换,将[-1,1][1]的世界坐标转换为屏幕坐标[w/2, h/2, depth/2],保留深度信息用以做z-buffer
Matrix viewport(int x, int y, int w, int h)
{
Matrix m = Matrix::identity(4);
m[0][3] = x + w / 2.f;
m[1][3] = y + h / 2.f;
m[2][3] = depth / 2.f;
m[0][0] = w / 2.f;
m[1][1] = h / 2.f;
m[2][2] = depth / 2.f;
return m;
}
5.将矩阵转为向量方便进行光栅化
Vec3f m2v(Matrix m) {
return Vec3f(m[0][0] / m[3][0], m[1][0] / m[3][0], m[2][0] / m[3][0]);
}
补充:应用纹理坐标后需要进行插值
Vec3i P = Vec3f(A) + Vec3f(B - A) * phi; //点插值
Vec2i uvP = uvA + (uvB - uvA) * phi; //纹理坐标插值
float ityP = ityA + (ityB - ityA) * phi; //intensity插值
与games101中管线的异同之处
3D物体渲染到2D屏幕的矩阵变换过程:模型变换(Modeling Trans)、视图变换(View Trans)和投影变换(Projection Trans) - 知乎 (zhihu.com)
- 并没有实现MVP变换中的Model变换(调整模型位置)。
- 视图变换与之相同。
- games101中,实现Projection是乘上p矩阵,即把视锥远裁剪面缩小到与近裁剪面相同大小,再按照正交投影将包裹所有可见模型物体的虚拟长方体转换到取值[-1,1] [-1,1]的2 * 2 * 2的立方体中(长方体转为立方体是为了方便后续的裁剪以及其他操作的计算)。实际投影矩阵是GPU自己做了透视除法,而项目中只自己实现了透视除法。
- games101中,近平面是自定义的,且投影到近平面上,而项目中默认是投影到xoy面(计算时不区分近远平面)。
- 视口变换与games101中相同。
Lesson 6 & 7: 渲染 & 阴影
按照之前实现的代码十分混乱,让我们来重构代码。
分为:geometry.h + .cpp
model.h + .cpp
our_gl.h + .cpp
main.cpp
geometry: 使用模板元编程实现Vec,mat及相关操作。
model: 定义模型,以及模型相关操作。
class Model {
private:
std::vector<Vec3f> verts_; //顶点
std::vector<std::vector<Vec3i> > faces_; // vertex/uv/normal index
std::vector<Vec3f> norms_; //法线
std::vector<Vec2f> uv_; //纹理uv坐标
//Bling-Phong
TGAImage diffusemap_; //纹理图(也就是diffuse)
TGAImage normalmap_; //法线贴图
TGAImage specularmap_; //高光图(用于补高光)
//加载材质
void load_texture(std::string filename, const char* suffixl, TGAImage& img);
public:
Model(const char* filename);
~Model();
int nverts(); //面和点的总数量
int nfaces();
Vec3f normal(int iface, int nthvert); //返回方向向量
Vec3f normal(Vec2f uv);
Vec3f vert(int i); //返回指定点坐标
Vec3f vert(int iface, int nthvert);
Vec2f uv(int iface, int nthvert); //返回uv坐标
TGAColor diffuse(Vec2f uv); //纹理
float specular(Vec2f uv); //高光
std::vector<int> face(int idx); //返回三角形的三个顶点坐标
};
our_gl: 定义IShader类和triangle方法,IShader虚类中有纯虚函数顶点着色器和片元着色器,其中顶点着色器用于计算颜色和计算变换矩阵,片元着色器用于计算法线、光照,应用纹理。
extern Matrix ModelView;
extern Matrix ViewPort;
extern Matrix Projection;
const float depth = 2000.f;
void viewport(int x, int y, int w, int h);
void projection(float coeff = 0.f);
void lookat(Vec3f eye, Vec3f center, Vec3f up);
//抽象类
struct IShader {
virtual ~IShader();
//纯虚函数 顶点着色器 和 片元着色器必须实现
virtual Vec4f vertex(int iface, int nthvert) = 0;
virtual bool fragment(Vec3f bar, TGAColor& color) = 0;
};
void triangle(Vec4f* pts, IShader& shader, TGAImage& image, float* zbuffer);
Matrix ModelView;
Matrix ViewPort;
Matrix Projection;
IShader::~IShader() {}
//视口变换
void viewport(int x, int y, int w, int h) {
ViewPort = Matrix::identity();
ViewPort[0][3] = x + w / 2.f;
ViewPort[1][3] = y + h / 2.f;
ViewPort[2][3] = 255.f / 2.f;
ViewPort[0][0] = w / 2.f;
ViewPort[1][1] = h / 2.f;
ViewPort[2][2] = 255.f / 2.f;
}
//投影变换
void projection(float coeff) {
Projection = Matrix::identity();
Projection[3][2] = coeff;
}
//视图变换
void lookat(Vec3f eye, Vec3f center, Vec3f up) {
Vec3f z = (eye - center).normalize();
Vec3f x = cross(up, z).normalize();
Vec3f y = cross(z, x).normalize();
ModelView = Matrix::identity();
for (int i = 0; i < 3; i++) {
ModelView[0][i] = x[i];
ModelView[1][i] = y[i];
ModelView[2][i] = z[i];
ModelView[i][3] = -center[i];
}
}
//重心坐标计算
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
}
//填充三角形
//输入 齐次坐标 着色器 图像 z-buffer
void triangle(Vec4f* pts, IShader& shader, TGAImage& image, float* zbuffer) {
//bounding box
Vec2f bboxmin(std::numeric_limits<float>::max(), std::numeric_limits<float>::max());
Vec2f bboxmax(-std::numeric_limits<float>::max(), -std::numeric_limits<float>::max());
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 2; j++) {
bboxmin[j] = std::min(bboxmin[j], pts[i][j] / pts[i][3]);
bboxmax[j] = std::max(bboxmax[j], pts[i][j] / pts[i][3]);
}
}
Vec2i P;
TGAColor color;
for (P.x = bboxmin.x; P.x <= bboxmax.x; P.x++) {
for (P.y = bboxmin.y; P.y <= bboxmax.y; P.y++) {
//重心坐标
Vec3f c = barycentric(proj<2>(pts[0] / pts[0][3]), proj<2>(pts[1] / pts[1][3]), proj<2>(pts[2] / pts[2][3]), proj<2>(P));
//深度插值
float z = pts[0][2] * c.x + pts[1][2] * c.y + pts[2][2] * c.z;
float w = pts[0][3] * c.x + pts[1][3] * c.y + pts[2][3] * c.z;
int frag_depth = z / w;
//不在三角形内或深度测试失败
if (c.x < 0 || c.y < 0 || c.z<0 || zbuffer[P.x + P.y * image.get_width()]>frag_depth) continue;
//片元着色器判断是否要丢弃该像素点(默认不丢弃)
bool discard = shader.fragment(c, color);
if (!discard) {
//更新深度数组并填充像素
zbuffer[P.x + P.y * image.get_width()] = frag_depth;
image.set(P.x, P.y, color);
}
}
}
}
**main: **实现继承自IShader类的Shader类,并实现顶点和片元着色器。
实现继承自IShader类的DepthShader类,用于深度检测,渲染两次,第一次深度测试,第二次着色。
#include <vector>
#include <iostream>
#include "tgaimage.h"
#include "model.h"
#include "geometry.h"
#include "our_gl.h"
Model* model = NULL;
float* shadowbuffer = NULL;
const int width = 800;
const int height = 800;
Vec3f light_dir(1, 1, 1);
Vec3f eye(1, 1, 3);
Vec3f center(0, 0, 0);
Vec3f up(0, 1, 0);
struct Shader : public IShader {
mat<4, 4, float> uniform_M; // Projection*ModelView
mat<4, 4, float> uniform_MIT; // (Projection*ModelView).invert_transpose()
mat<4, 4, float> uniform_Mshadow; // 当前片段的屏幕坐标转换为阴影缓冲区内的屏幕坐标
mat<2, 3, float> varying_uv; // uv坐标齐次坐标, VS中写,FS中读
mat<3, 3, float> varying_tri; // 视口变换前的三角形齐次坐标,VS中写,FS中读
Shader(Matrix M, Matrix MIT, Matrix MS) : uniform_M(M), uniform_MIT(MIT), uniform_Mshadow(MS), varying_uv(), varying_tri() {}
virtual Vec4f vertex(int iface, int nthvert) {
Vec4f gl_Vertex = ViewPort * Projection * ModelView * embed<4>(model->vert(iface, nthvert)); //变换矩阵
varying_uv.set_col(nthvert, model->uv(iface, nthvert)); //设置uv坐标
varying_tri.set_col(nthvert, proj<3>(gl_Vertex / gl_Vertex[3])); //视口变换前的三角形齐次坐标
return gl_Vertex;
}
virtual bool fragment(Vec3f bar, TGAColor& color) {
Vec4f sb_p = uniform_Mshadow * embed<4>(varying_tri * bar); // corresponding point in the shadow buffer
sb_p = sb_p / sb_p[3];
int idx = int(sb_p[0]) + int(sb_p[1]) * width; // 深度检测数组下标
float shadow = .3 + .7 * (shadowbuffer[idx] < sb_p[2] + 43.34); // 解决z-fighting问题
Vec2f uv = varying_uv * bar; // 对当前像素插值uv坐标
Vec3f n = proj<3>(uniform_MIT * embed<4>(model->normal(uv))).normalize(); // 法线
Vec3f l = proj<3>(uniform_M * embed<4>(light_dir)).normalize(); // 入射光线
Vec3f r = (n * (n * l * 2.f) - l).normalize(); // 反射光线
float spec = pow(std::max(r.z, 0.0f), model->specular(uv)); // 高光
float diff = std::max(0.f, n * l); // 漫反射
TGAColor c = model->diffuse(uv); // 纹理
//bling-phong模型
//ambient + diffuse + specular
//当深度测试通过时shadow为1对整体没有影响 未通过时乘偏移量0.3制造阴影
for (int i = 0; i < 3; i++) color[i] = std::min<float>(20 + c[i] * shadow * (1.2 * diff + .6 * spec), 255);
return false;
}
};
//深度测试
struct DepthShader : public IShader {
mat<3, 3, float> varying_tri;
DepthShader() : varying_tri() {}
virtual Vec4f vertex(int iface, int nthvert) {
Vec4f gl_Vertex = embed<4>(model->vert(iface, nthvert)); // read the vertex from .obj file
gl_Vertex = ViewPort * Projection * ModelView * gl_Vertex; // transform it to screen coordinates
varying_tri.set_col(nthvert, proj<3>(gl_Vertex / gl_Vertex[3])); // setting triangle coordinates
return gl_Vertex;
}
virtual bool fragment(Vec3f bar, TGAColor& color) {
Vec3f p = varying_tri * bar; //插值
color = TGAColor(255, 255, 255) * (p.z / depth);
return false;
}
};
int main(int argc, char** argv) {
if (2 == argc) {
model = new Model(argv[1]);
}
else {
model = new Model("obj/african_head.obj");
}
//初始化深度数组
float* zbuffer = new float[width * height];
shadowbuffer = new float[width * height];
for (int i = width * height; --i; ) {
zbuffer[i] = shadowbuffer[i] = -std::numeric_limits<float>::max();
}
light_dir.normalize();
{
//深度测试渲染
TGAImage depth(width, height, TGAImage::RGB);
lookat(light_dir, center, up);
viewport(width / 8, height / 8, width * 3 / 4, height * 3 / 4);
projection(0);
DepthShader depthshader;
Vec4f screen_coords[3];
for (int i = 0; i < model->nfaces(); i++) {
for (int j = 0; j < 3; j++) {
screen_coords[j] = depthshader.vertex(i, j);
}
triangle(screen_coords, depthshader, depth, shadowbuffer);
}
depth.flip_vertically(); // to place the origin in the bottom left corner of the image
depth.write_tga_file("depth.tga");
}
Matrix M = ViewPort * Projection * ModelView;
{
// rendering
TGAImage frame(width, height, TGAImage::RGB);
lookat(eye, center, up);
viewport(width / 8, height / 8, width * 3 / 4, height * 3 / 4);
projection(-1.f / (eye - center).norm());
//uniform_M uniform_MIT uniform_Mshadow
Shader shader(ModelView, (Projection * ModelView).invert_transpose(), M * (ViewPort * Projection * ModelView).invert());
Vec4f screen_coords[3];
for (int i = 0; i < model->nfaces(); i++) {
for (int j = 0; j < 3; j++) {
screen_coords[j] = shader.vertex(i, j);
}
triangle(screen_coords, shader, frame, zbuffer);
}
frame.flip_vertically(); // to place the origin in the bottom left corner of the image
frame.write_tga_file("framebuffer.tga");
}
delete model;
delete[] zbuffer;
delete[] shadowbuffer;
return 0;
}
OpenGL渲染管线
其中蓝色为可编程部分
- 顶点数据从模型中读入的一些三维点的集合。
- 顶点着色器对顶点属性做基本处理,求出变换矩阵,进行坐标转换到世界空间。
- 光栅化将3D连续的物体转换为离散屏幕像素点,生成供片元着色器使用的片段,即代码中triangle部分。
- 片元着色器计算一个像素最终颜色,还包含3D场景的数据(光照、阴影等),在此会进行光照计算和阴影处理等。仅处理单个片元,不能在片元着色器中获取临近片元的信息,以此可保证独立性,实现高并发。
一些问题:
1.法线贴图:
是一种伪造凹凸光照的技术,用于不使用更多多边形来增添细节,存储形式为RGB图像。RGB分量范围[0,1],而法线分量范围[-1, 1],因此需要对每个分量作映射。
在对象空间中(模型顶点的坐标系),法线信息是相对于对象空间朝向,各个方向都有,因此色彩比较丰富。
而在切线空间中(位于三角形表面上的空间)以原法线方向作为z轴正方向,定义一个切线空间,并在切线空间中用三个坐标值就可以表示出偏移量。存储的是每个点在各自切线空间中的法线偏移,方向接近[0, 0, 1]因此看上去接近蓝色。
通常使用切线空间法线纹理,优势如下:
- 切线空间存储的是相对法线信息,因此换个网格(或者网格变换deforming)应用该纹理,也能得到合理的结果。
- 可以进行uv动画,通过移动该纹理的uv坐标实现凹凸移动的效果
- 可以重用法线纹理,比如,一个砖块,我们仅使用一张法线纹理就可以用到所有的6个面。
- 可以压缩。因为切线空间的法线z方向总是正方向,因此可以仅存储xy方向,从而推到z方向(normal是单位向量,用勾股定理由xy得出z,取z为正的一个即可)。而模型空间的法线纹理方向各异,无法压缩。
TBN矩阵:由切线、副切线、法线(世界空间的向量)组成的3x3的矩阵,用以沟通切向(纹理)空间和世界空间,上文已说,多使用切线空间法线纹理,而实际需要的是世界空间的法线信息。
世界空间法线 = 切向空间法线 * TBN矩阵
$$
\begin{bmatrix}
T_x && T_y && T_z \
B_x && B_y && B_z \
N_x && N_Y && N_z
\end{bmatrix}
$$
2.各个空间及变换
Vertex Shader输出是在裁剪空间(Clip Space)即做完MVP变换后的空间,之后由GPU自己做透视除法将顶点转到标准化设备坐标(NDC)
而fragment Shader的输入是NDC做视口变换后的屏幕空间(Screen Space)
过程为:
(Vertex Shader) => Clip Space => (透视除法) => NDC => (视口变换) => Screen Space => (Fragment Shader)
项目中有些许不同:GPU自己做透视除法的部分由我们自己实现,即VS后直接得到Screen Space
总过程为:
**模型空间(Model Space) => M矩阵 => 世界空间(World Space) => V矩阵 => 视觉空间(以摄像机为中心)(View Space) => P矩阵 => 裁剪空间 (Clip Space) => Vertex Shader结束 => 透视除法 => 标准化设备空间(NDC) => 视口变换 => 屏幕空间(Screen Space) => 进入Fragmen Shader **
3.z-fighting
Z-Fighting 是指当多个不透明的面在世界空间中处于共面时,它们的片元可能在深度缓冲区的值相同(精度误差),在渲染像素时就会随机绘制,最终导致在每帧之间出现闪烁的现象。会产生如下效果。
解决方案:
- 禁用深度测试。
- 提升精度,但是会牺牲性能。
- 拉远近平面,但在近处物体可能被裁剪掉。
- 坐标偏移,在多个面共面时对面进行偏移分开,但当相机仰角过大时上面的面可能会遮住下面。
-1,1 ↩︎
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了