tinyrenderer笔记(Shader)

  • tinyrenderer
  • 个人代码仓库:tinyrenderer个人练习代码

前言

现在我们将所有的渲染代码都放在了 main.cpp 中,然而在 OpenGL 渲染管线中,渲染的核心逻辑是位于 shader 中的,下面是 OpenGL 的渲染管线:

image.png

蓝色是我们可以自定义的 shader,现在让我们来模仿顶点着色器与片段着色器的工作流程(忽略几何着色器)。

UniformVar

在 glsl 中,我们可以定义一些 uniform 变量,在着色器之外设置值,并在着色器之内访问。这些变量在所有着色器都可以访问。为了模拟这种变量访问模式,我定义了一个 UniformVar 结构体,内部通过 unordered_map std::string 索引实际的变量值:

template <typename T>
struct UniformVar
{std::unordered_map<std::string, T> var;void add_var(const std::string& name){var[name] = T();}T get_var(const std::string& name){if (var.find(name) == var.end()){std::cout << "[glProgram] UniformVar get_var " << name << " not found" << std::endl;return T();}return var[name];}void set_var(const std::string& name, const T& value){if (var.find(name) == var.end()){std::cout << "[glProgram] UniformVar set_var " << name << " not found" << std::endl;return;}var[name] = value;}
};

这是一个模板结构体,以满足不同类型的变量定义。

VaryingVar

在 glsl 中,顶点着色器定义的输出变量经过插值之后会输出到片段着色器中。插值是对一个图元考虑的,在我的代码中只考虑三角形图元。我们模拟 varying 变量时,需要一种辅助结构体来帮助我们插值,它会存储三角形三个顶点的属性。

template <typename T>
struct VaryingVar
{std::unordered_map<std::string, std::array<T, 3>> var;void add_var(const std::string& name){var[name] = std::array<T, 3>();}T get_var(const std::string& name, size_t index){if (var.find(name) == var.end()){std::cout << "[glProgram] VaryingVar get_var " << name << " not found" << std::endl;return T();}if (index > 3){std::cout << "[glProgram] VaryingVar get_var " << name << " index out of range" << std::endl;return T();}return var[name][index];}std::array<T, 3> get_var(const std::string& name){if (var.find(name) == var.end()){std::cout << "[glProgram] VaryingVar get_var " << name << " not found" << std::endl;return T();}return var[name];}void set_var(const std::string& name,const T& value, size_t index){if (var.find(name) == var.end()){std::cout << "[glProgram] VaryingVar set_var " << name << " not found" << std::endl;return;}if (index > 3){std::cout << "[glProgram] VaryingVar set_var " << name << " index out of range" << std::endl;return;}var[name][index] = value;}
};

glProgram

我们需要一个能够管理顶点着色器与片段着色器的类,它会使用自定义着色器的功能,并在内部做一些光栅化操作,类比于 OpenGL 的 Program,我们自定义一个 glProgram 类。

glProgram 会管理所有 uniform 与 varying 变量,并提供外部接口来访问不同类型的变量:

UniformVar<glm::mat4>   uniform_mat4;
UniformVar<float>       uniform_float;
·········
VaryingVar<Vec4f>       varying_vec4f;
VaryingVar<Vec3f>       varying_vec3f;
·········

访问通过模板函数来确定访问变量的类型:

template<typename T>
T GetUniformVar(const std::string& name)
{if constexpr (std::is_same_v<T, glm::mat4>){return uniform_mat4.get_var(name);}·········
}template<typename T>
void SetUniformVar(const std::string& name, const T& value)
{if constexpr (std::is_same_v<T, glm::mat4>){uniform_mat4.set_var(name, value);}·········
}template<typename T>
void AddUniformVar(const std::string& name)
{if constexpr (std::is_same_v<T, glm::mat4>){uniform_mat4.add_var(name);}·········
}template<typename T>
T GetVaryingVar(const std::string& name, size_t index)
{if constexpr (std::is_same_v<T, Vec4f>){return varying_vec4f.get_var(name, index);}·········
}template<typename T>
void SetVaryingVar(const std::string& name, const T& value, size_t index)
{if constexpr (std::is_same_v<T, Vec4f>){varying_vec4f.set_var(name, value, index);}·········
}template<typename T>
void AddVaryingVar(const std::string& name)
{if constexpr (std::is_same_v<T, Vec4f>){varying_vec4f.add_var(name);}·········
}

glProgram 会管理顶点着色器与片段着色器,内部会存储它们的指针:

VertexShader*   m_vertexShader;
FragmentShader* m_fragmentShader;
// 注册顶点着色器
void glProgram::RegisterVertexShader(VertexShader* vertexShader)
{m_vertexShader = vertexShader;vertexShader->SetProgram(this);vertexShader->ConfirmVaryingVar();
}
// 注册片段着色器
void glProgram::RegisterFragmentShader(FragmentShader* fragmentShader)
{m_fragmentShader = fragmentShader;fragmentShader->SetProgram(this);
}

会提供一个函数来确定视口:

glm::mat4 m_viewport;
void glProgram::SetViewPort(int width, int height)
{m_viewport = glm::viewport(width, height);
}

最重要的是提供一个函数来绘制一个三角形(功能后续讲解):

void Draw(TGAImage& target, float* zbuffer, const std::array<VertexInfoBase*, 3>& vertexInfos);// 绘制一个三角面

IShader

这是最基本的着色器类,它会派生一个顶点着色器与片段着色器。所有着色器可以获得全局变量的值,这个值在着色器之外设置。所以 IShader 会为子类提供一个访问 uniform 变量的方法:

class IShader
{friend class glProgram;
public:void SetProgram(glProgram* program){m_program = program;}
protected:template<typename T>T GetUniformVar(const std::string& name){return m_program->GetUniformVar<T>(name);}glProgram* m_program;
};

SetProgram 会在注册着色器的时候调用,保存 glProgram 的指针用于访问 uniform 或者 varying 变量。

VertexShader

顶点着色器基类,下面是顶点着色器的功能:

The main goal of the vertex shader is to transform the coordinates of the vertices. The secondary goal is to prepare data for the fragment shader.

顶点着色器的主要目标是转换顶点的坐标。次要目标是准备片段着色器所需的数据。

最核心的就是这个函数:

virtual Vec4f vertex(VertexInfoBase* vertexInfo) = 0;

会接受一个 VertexInfoBase 类型的变量,然后返回经过 MVP 变换后的顶点坐标。VertexInfoBase 是什么?在 OpenGL 中,我们可以定义一些缓冲区来为顶点着色器输入一些变量(坐标、法线、纹理坐标),每个顶点的数据不一样。VertexInfoBase 就是我用来模仿为顶点着色器输入变量的方法:

// 顶点所需的信息
struct VertexInfoBase
{size_t index;
};

保存了当前顶点在三角形的 index,对于各种不同的自定义着色器,我们所需的顶点信息不同,可以通过继承 VertexInfoBase 来添加一些属性:

struct PhongVertexInfo : VertexInfoBase
{Vec3f location;Vec2f textureCoord;Vec3f normal;
};

顶点着色器会定义一些 varying 变量,插值后给片段着色器使用:

virtual void ConfirmVaryingVar() = 0;// 定义varying变量template<typename T>
void AddVaryingVar(const std::string& name)
{m_program->AddVaryingVar<T>(name);
}
template<typename T>
void SetVaryingVar(const std::string& name, size_t index, const T& value)
{m_program->SetVaryingVar<T>(name, value, index);
}

FragmentShader

对于片段着色器,它的功能如下:

The main goal of the fragment shader - is to determine the color of the current pixel. Secondary goal - we can discard current pixel by returning true.

片段着色器的主要目标是确定当前像素的颜色。次要目标——我们可以通过返回 true 来丢弃当前像素。

最核心的函数是这个:

virtual bool fragment(Vec3f barycentric_coords, TGAColor& color) = 0;

会接受当前像素坐标对应的重心坐标,用于插值得到当前像素的属性。为什么要这么做?其实是我还没想到简易的架构,可以不输入重心坐标而自动获得插值后的属性,所以需要用一个函数显式获得:

template<typename T>
T GetInterVaryingVar(const std::string& name, const Vec3f& barycentric_coords)
{std::array<T, 3> varyings;T res{};for (int i = 0; i < 3; i++){varyings[i] = m_program->GetVaryingVar<T>(name, i);res = res + varyings[i] * barycentric_coords[i];}return res;
}

Draw

最后来介绍 glProgramDraw 函数,其实这里面的代码就是将之前 triangle 函数与 drawModel 的功能合并。

void Draw(TGAImage& target, float* zbuffer, const std::array<VertexInfoBase*, 3>& vertexInfos);// 绘制一个三角面

首先会调用顶点着色器获得顶点对应的屏幕坐标:

Vec3f viewport_coords[3];
for (int i = 0;i < 3;i++)
{// 顶点着色器返回投影坐标vertexInfos[i]->index = i;Vec4f coord = m_vertexShader->vertex(vertexInfos[i]);// 透视除法coord = coord / coord.w;// 视口变换viewport_coords[i] = m_viewport * coord;
}

然后就是计算三个顶点的包围盒:

// 包围盒范围
Vec2f bboxmin(width - 1, height - 1);
Vec2f bboxmax(0, 0);
bboxmin.x = std::min({ bboxmin.x, viewport_coords[0].x, viewport_coords[1].x, viewport_coords[2].x});
bboxmin.y = std::min({ bboxmin.y, viewport_coords[0].y, viewport_coords[1].y, viewport_coords[2].y });
bboxmax.x = std::max({ bboxmax.x, viewport_coords[0].x, viewport_coords[1].x, viewport_coords[2].x });
bboxmax.y = std::max({ bboxmax.y, viewport_coords[0].y, viewport_coords[1].y, viewport_coords[2].y });

遍历包围盒的每个像素:

for (int x = bboxmin.x; x <= bboxmax.x; x++)
{for (int y = bboxmin.y; y <= bboxmax.y; y++){······}
}

循环内先获得当前像素的重心坐标,然后插值获得深度值:

Vec3f P = Vec3f(x, y, 0);
Vec3f bc_screen = glm::barycentric(viewport_coords[0], viewport_coords[1], viewport_coords[2], P);
if (bc_screen.x < 0 || bc_screen.y < 0 || bc_screen.z < 0) continue;
if (x < 0 || x >= width || y < 0 || y >= height)
{continue;
}
// 重心坐标插值
P.z = bc_screen.x * viewport_coords[0].z + bc_screen.y * viewport_coords[1].z + bc_screen.z * viewport_coords[2].z;
if (P.z < 0 || P.z > 1 || zbuffer[x + y * width] < P.z)
{continue;
}

深度测试与裁剪判断通过之后,就会调用片段着色器来获得当前像素的颜色值:

TGAColor color;
bool bDiscard = m_fragmentShader->fragment(bc_screen, color);
if (!bDiscard)
{zbuffer[x + y * width] = P.z;target.set(x, y, color);
}

main

在框架的基础上,来看看我们实现之前的 Phong Shading 需要干些什么。

首先定义每个顶点所需的属性:

struct PhongVertexInfo : VertexInfoBase
{Vec3f location;// 位置Vec2f textureCoord;// 纹理坐标Vec3f normal;// 法线
};

来看看顶点着色器:

class PhongVertexShader : public VertexShader
{
public:// 确定有哪些 Varying 变量virtual void ConfirmVaryingVar() override{AddVaryingVar<Vec2f>("textureCoord");AddVaryingVar<Vec3f>("normal");}virtual Vec4f vertex(VertexInfoBase* vertexInfos) override{// 解码顶点属性实际类型auto* info = static_cast<PhongVertexInfo*>(vertexInfos);// 获得矩阵glm::mat4 modelMatrix = GetUniformVar<glm::mat4>("Model");glm::mat4 viewMatrix = GetUniformVar<glm::mat4>("View");glm::mat4 proMatrix = GetUniformVar<glm::mat4>("Proj");// 设置 Varying 变量SetVaryingVar<Vec2f>("textureCoord", info->index, info->textureCoord);glm::mat3 normalMatrix = modelMatrix.inverse().transpose().to_mat3();Vec3f normal = normalMatrix * info->normal;SetVaryingVar<Vec3f>("normal", info->index, normal);return proMatrix * viewMatrix * modelMatrix * Vec4f(info->location, 1.f);}
};

再看看片段着色器:

class PhongFragmentShader : public FragmentShader
{
public:virtual bool fragment(Vec3f barycentric_coords, TGAColor& color) override{Vec3f light_dir = GetUniformVar<Vec3f>("lightDir");// 光线方向TGAImage* tex = GetUniformVar<TGAImage*>("texture");// 纹理贴图glm::mat4 modelMatrix = GetUniformVar<glm::mat4>("Model");light_dir = modelMatrix * Vec4f(light_dir, 0.f);light_dir.normalize();Vec3f normal = GetInterVaryingVar<Vec3f>("normal", barycentric_coords);// 获得插值后的法线normal.normalize();// 插值之后记得归一化Vec2f textureCoord = GetInterVaryingVar<Vec2f>("textureCoord", barycentric_coords);// 获得插值后的纹理坐标TGAColor diffuse = tex->texture(textureCoord.x, textureCoord.y);// 采样得到纹理颜色float intensity = std::max(0.f, -normal.dot(light_dir));// 计算光照强度color = TGAColor(diffuse.r * intensity, diffuse.g * intensity, diffuse.b * intensity, 255);// 设置颜色return false;// do not discard the pixel}
};

在着色器之外呢?

先注册片段着色器与顶点着色器、设置视口:

phongProgram->RegisterVertexShader(vertexShader);
phongProgram->RegisterFragmentShader(fragmentShader);
phongProgram->SetViewPort(width, height);

确定有哪些 uniform 变量:

phongProgram->AddUniformVar<glm::mat4>("Model");
phongProgram->AddUniformVar<glm::mat4>("View");
phongProgram->AddUniformVar<glm::mat4>("Proj");
phongProgram->AddUniformVar<Vec3f>("lightDir");
phongProgram->AddUniformVar<TGAImage*>("texture");

设置 uniform 变量:

glm::mat4 viewMatrix = glm::lookAt(Vec3f(0, 0, 2), Vec3f(0, 0, 0), Vec3f(0, 1, 0));
glm::mat4 projMatrix = glm::perspective(45, float(width) / height, 0.1f, 100.f);
glm::mat4 modelMatrix;
modelMatrix = glm::translate(modelMatrix, Vec3f(-0.5, 0, 0));
modelMatrix = glm::rotate(modelMatrix, 45, Vec3f(0, 1, 0));
modelMatrix = glm::scale(modelMatrix, Vec3f(0.5, 0.5, 0.5));
phongProgram->SetUniformVar<glm::mat4>("Model", modelMatrix);
phongProgram->SetUniformVar<glm::mat4>("View", viewMatrix);
phongProgram->SetUniformVar<glm::mat4>("Proj", projMatrix);
phongProgram->SetUniformVar<Vec3f>("lightDir", light_dir);
phongProgram->SetUniformVar<TGAImage*>("texture", texture);

遍历模型的每个三角面并绘制:

std::array<VertexInfoBase*, 3> vertexInfos;
for (int i = 0; i < model->nfaces(); i++)
{std::vector<int> face_v = model->face_v(i);std::vector<int> face_vt = model->face_vt(i);std::vector<int> face_vn = model->face_vn(i);for (int j = 0; j < 3; j++){auto* info = new PhongVertexInfo();info->location = Vec4f(model->vert(face_v[j]), 1.f);info->textureCoord = model->vertTexture(face_vt[j]);info->normal = model->vertNormal(face_vn[j]);vertexInfos[j] = info;}phongProgram->Draw(result, zbuffer, vertexInfos);
}

over!来看看效果:

image.png

效果还不错,整个框架是我花了大概一天时间写出来的,还有很多不成熟的地方,大家可以在这上面自己改进改进,代码仓库在文章开头。

本次代码提交记录:

image.png

  1. 提交的代码,片段着色器处 light_dir 忘记乘上模型变换矩阵了
  2. 这个版本的 LookAt 函数存在错误!2025-4-29 16.23 提交修复

参考

  • tinyrenderer

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/79653.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++高性能内存池

目录 1. 项目介绍 1. 这个项目做的是什么? 2. 该项目要求的知识储备 2. 什么是内存池 1. 池化技术 2. 内存池 3. 内存池主要解决的问题 4.malloc 3. 先设计一个定长的内存池 4.高并发内存池 -- 整体框架设计 5. 高并发内存池 -- thread cache 6. 高并发内存池 -- …

LintCode407-加一,LintCode第479题-数组第二大数

第407题: 描述 给定一个非负数&#xff0c;表示一个数字数组&#xff0c;在该数的基础上1&#xff0c;返回一个新的数组。 该数字按照数位高低进行排列&#xff0c;最高位的数在列表的最前面. 样例 1&#xff1a; 输入&#xff1a;[1,2,3] 输出&#xff1a;[1,2,4] 样例 …

SMT贴片钢网精密设计与制造要点解析

内容概要 SMT贴片钢网作为电子组装工艺的核心载体&#xff0c;其设计与制造质量直接影响焊膏印刷精度及产品良率。本文系统梳理了钢网全生命周期中的15项关键技术指标&#xff0c;从材料选择、结构设计到工艺控制构建完整技术框架。核心要点涵盖激光切割精度的微米级调控、开口…

OpenCV进阶操作:角点检测

文章目录 一、角点检测1、定义2、检测流程1&#xff09;输入图像2&#xff09;图像预处理3&#xff09;特征提取4&#xff09;角点检测5&#xff09;角点定位和标记6&#xff09;角点筛选或后处理&#xff08;可选&#xff09;7&#xff09;输出结果 二、Harris 角点检测&#…

江苏正力新能Verify认知能力测评笔试已通知 | SHL测评题库预测题 | 华东同舟求职讲求职

江苏正力新能入职笔试通知&#xff0c;Verify&#xff08;认知能力测评&#xff09;&#xff0c;用时约46分钟&#xff0c;其中正式测试部分计时36分钟&#xff1b;时间到了试卷会自动提交&#xff0c;请合理安排答题时间&#xff01;前面有10分钟练习时间&#xff0c;可以略过…

在若依里创建新菜单

首先打开左侧菜单栏的系统管理&#xff0c;然后点击菜单管理 可以点击左上角的新增&#xff0c;也可以点击右侧对应目录的新增 这里我选择了右侧的新增&#xff0c;即在系统管理目录下新增菜单 其中的组件路径就是写好的页面的路径 &#xff08;从views的下一级开始写即可&…

【AI知识库云研发部署】RAGFlow + DeepSeek

可以分成两台机器部署,一台gpu,一台cpu,cpu的机器运行ragflow的主程序,使用模型时才访问gpu。当然全部在一台机器上部署是完全ok的。全文没有复杂的环境问题 gpu 安装screen:yum install screen 配置ollama: 下载官方安装脚本并执行: curl -fsSL https://ollama.co…

Java后端开发day40--异常File

&#xff08;以下内容全部来自上述课程&#xff09; 异常 异常&#xff1a;异常就是代表程序出现的问题 1. 异常的分类 1.1 Error 代表的是系统级别的错误&#xff08;属于严重问题&#xff09; 系统一旦出现问题&#xff0c;sun公司会把这些错误封装成Error对象。 Error…

算法思想之深度优先搜索(DFS)、递归以及案例(最多能得到多少克黄金、精准核酸检测、最富裕的小家庭)

深度优先搜索&#xff08;DFS&#xff09;、递归 深度优先搜索&#xff08;Depth First Search&#xff0c;DFS&#xff09;是一种用于遍历或搜索树或图的算法。在 DFS 算法中&#xff0c;从起始节点开始&#xff0c;沿着一条路径尽可能深地访问节点&#xff0c;直到到达叶子节…

Spark,HDFS客户端操作

hadoop客户端环境准备 找到资料包路径下的Windows依赖文件夹&#xff0c;拷贝hadoop-3.1.0到非中文路径&#xff08;比如d:\hadoop-3.1.0&#xff09; ① 打开环境变量 ② 在下方系统变量中新建HADOOP_HOME环境变量,值就是保存hadoop的目录。 效果如下&#xff1a; ③ 配置Pa…

共享会议室|物联网解决方案:打造高效、智能的会议空间!

在数字化转型的浪潮下&#xff0c;企业、园区、公共机构的会议室面临诸多痛点&#xff0c;如何通过物联网技术实现会议室资源的智能调度、环境设备的自动化控制以及用户体验的全面升级&#xff1f;本文将结合行业实践与技术方案&#xff0c;探讨基于物联网的共享会议室解决方案…

ts bug 找不到模块或相应类型的声明,@符有红色波浪线

解决方法&#xff1a;在env.d.ts文件中添加以下代码&#xff0c;这段代码是一个 TypeScript 的声明文件&#xff0c;用于让 TypeScript 知道如何处理 Vue 单文件组件&#xff08;.vue 文件&#xff09;的导入。 /// <reference types"vite/client" /> // 声明…

端口隔离基本配置

1.top图 2.交换机配置 # sysname sw1 # vlan batch 10 # interface GigabitEthernet0/0/1port link-type trunkport trunk allow-pass vlan 10 # interface GigabitEthernet0/0/2port link-type trunkport trunk allow-pass vlan 10sys sw2 # vlan batch 10 # interface Giga…

Android View#post()源码分析

文章目录 Android View#post()源码分析概述onCreate和onResume不能获取View的宽高post可以获取View的宽高总结 Android View#post()源码分析 概述 在 Activity 中&#xff0c;在 onCreate() 和 onResume() 中是无法获取 View 的宽高&#xff0c;可以通过 View#post() 获取 Vi…

SecureCrt设置显示区域横列数

1. Logical rows //逻辑行调显示区域高度的 一般超过50就全屏了 2. Logical columns //逻辑列调显示区域宽度的 3. Scrollback buffer //缓冲区大小

最短路径-Dijkstra算法板子(java)

自己把Dijkstra的板子整理了一下&#xff0c;也方便自己后续做题&#xff0c;在此做个记录。 Dijkstra基本上都会需要这些变量&#xff1a; dist[]&#xff1a;记录各个节点到起始节点的最短权值 path[]&#xff1a;记录各个节点的上一个节点(用来联系该节点到起始节点的路径)…

PostgreSQL数据库的array类型

PostgreSQL数据库相比其它数据库&#xff0c;有很多独有的字段类型。 比如array类型&#xff0c;以下表的pay_by_quarter与schedule两个字段便是array类型&#xff0c;即数组类型。 CREATE TABLE sal_emp (name text,pay_by_quarter integer[],schedule t…

centos的根目录占了大量空间怎么办

问题 当根目录磁盘不够时&#xff0c;就必须删除无用的文件了 上面的&#xff0c;如果删除/usr 或/var是可以释放出系统盘的 定位占空间大的文件 经过命令&#xff0c;一层层查哪些是占磁盘的。 du -sh /* | sort -rh | head -n 10 最终排查&#xff0c;是有个系统日志占了20…

PostgreSQL存储过程“多态“实现:同一方法名支持不同参数

引言 在传统编程语言中&#xff0c;方法重载&#xff08;同一方法名不同参数&#xff09;是实现多态的重要手段。但当我们将目光转向PostgreSQL数据库时&#xff0c;是否也能在存储过程&#xff08;函数&#xff09;中实现类似的功能&#xff1f;本文将深入探讨PostgreSQL中如…

快速学会Linux的WEB服务

一.用户常用关于WEB的信息 什么是WWW www是world wide web的缩写&#xff0c;及万维网&#xff0c;也就是全球信息广播的意思 通常说的上网就是使用www来查询用户所需要的信息。 www可以结合文字、图形、影像以及声音等多媒体&#xff0c;超链接的方式将信息以Internet传递到世…