Tangent Space Normal Mapping
为什么要法线贴图法线贴图(Normal Map)正如其名所述,是一张包含了法线相关信息的贴图(我在说什么废话)。要介绍为什么使用法线贴图的话,我们首先得了解这样一个事实,在光照和着色算法不变的情况下,我们的图形质量高低基本上取决于我们模型的信息密度,更进一步说的话就是模型的三角形数量。
而这一概念也被应用到一种叫做LOD(level of detail)的技术之中,可以看到最右面的模型只有750个三角形,导致它头部的光圈基本上坍缩成了一个矩形,渲染质量显然没有最左面的好。
在有了上面的认知之后,大概率我们会有一个错误的认知,好,无脑堆素材质量就行了,看我一个模型整他个上亿个三角形,就不信出不了好画面。这种力大砖飞的情况虽然确实能显著提高渲染质量,但是游戏的包体大小也会成倍增加,并且这么高模的模型导入游戏,游戏渲染的压力会急速上升,如果没有UE5的Nanite这种技术来做简化,我们基本上没有希望达到实时渲染的速度要求。
不过聪明的人们想到了一种办法来进行近似,其原理大概是这样,虽然我们无法直接导入高模的模型,但是要是我们利用其表面的法线信息以及一个相对低模的模型,也能 ...
硬盘的基础知识总结
总线,协议,接口最近发现自己退化的厉害,故为了防止病情的进一步恶化,我尝试简单的总结一下我最近学的相关知识,主要是与硬盘相关,毕竟这东西经常买,多知道一点还是有不少的好处的。
而在这篇文章中,我们不讨论机械硬盘或者固态硬盘中硬件原理,而是专门的总结一下与其相关的总线,接口,以及协议的相关知识。
总线:总线的任务主要是进行数据的传输,总线的性能主要是用带宽(bandwidth)来进行描述,其定义为总线在单位时间内所能传输的数据量,现在常见的总线有SATA, PCIE, SAS。
接口: 硬盘中用来和主板接入的实际接触部分,没有这个东西你就没法把硬盘塞到主板上。
协议: 用来定义和实现通信的一组规则,简而言之的话,就是硬件之间沟通的具体方法,现在常用的协议有ACHI,NVME,SCSI。
通常来讲这三者要互相匹配,并且整体的速度上限往往是由总线来决定的,这里借用一张硬件茶谈中的图来进行描述。
如果以我们常买的固态硬盘为例的话,一般会为M.2接口,PCIe总线,NVMe协议这个组合。
这里顺便记录下常见的总线带宽上限,让自己稍微有个速度上定量的概念。
SATA3.0
...
Vulkan的各种presentation mode
Presentation mode是什么在创建vulkan的swap chain的时候,我们有一项必须要指定的便是VkPresentModeKHR,这个是用来控制swap chain中的对象是如何显示到我们的屏幕之上。总的来看,vulkan支持以下4种方案:
VK_PRESENT_MODE_IMMEDIATE_KHR
VK_PRESENT_MODE_FIFO_KHR
VK_PRESENT_MODE_FIFO_RELAXED_KHR
VK_PRESENT_MODE_MAILBOX_KHR
并且在这4种方案中,只有VK_PRESENT_MODE_FIFO_KHR是vulkan规定必须要支持的显示模式,其他的不做要求。
VK_PRESENT_MODE_IMMEDIATE_KHR这个模式是最简单的一种情况,你渲染出来的图片直接就会被扔到显示器上进行显示,不会做任何同步。这种模式一般情况下是用来最大化你的fps的,通常强调输入延迟的fps游戏会搞这种显示模式。
VK_PRESENT_MODE_FIFO_KHR & VK_PRESENT_MODE_MAILBOX_KHR这两个可以放在一 ...
C++中的类型推导
类型推导—Effective modern C++ 学习笔记auto和template虽然用起来很爽,但是作为程序员我们应该了解C++编译器做了哪些事情,从而确实的保证整套机制能够顺利的运作。12345//模板声明部分template<typename T>void f(ParamType param);//调用部分f(expr)模板类型推导就是关于如何根据expr的类型来推断出T的类型以及ParamType的类型。通常来讲,T和ParamType不会一样,因为ParamType往往会给T加上const或者引用之类的修饰。比如T可能是String类型,但ParamType是const String&类型,这在平常的函数模板使用中是非常常见的一种情况。
但是即便刨除T和ParamType之间的类型不同,我们也需要知道这样一个事情,虽然T的类型推导应该完全依赖于expr的类型,但是实际上并非如此。T的类型推导不仅仅依赖于expr的类型,也取决于ParamType的类型。
具体来看的话,ParamType造成的影响可以分为三类来进行讨论。
当ParamType是一个指针或 ...
派生类虚函数调用父类虚函数的正确实践
简单背景描述这个问题是我在阅读Effective C++条例27中所涉及到一个小知识点,其主要是在关注当我们需要在派生类的虚函数中调用对应的父类虚函数时,我们的一些不恰当转型(cast)操作可能会让我们的当前派生类对象进入一种”伤残状态”,即派生类的基类数据部分没有如我们所想的发生改变,而是只改变了其派生类的成员数据。当然这么说有点过于抽象,我还是在下面贴出实际代码来解释一下吧。
12345678910111213141516171819202122232425262728293031323334353637383940#include <iostream>#include <format>using namespace std;// 基类定义class Base{private: int m_base;public: Base(int base) :m_base(base) {} virtual void Resize() { m_base = -1; } int G ...
利用xmake以及vscode配置vulkan的开发环境
为什么xmake是神做过C++开发的人员一定或多或少都被C++的编译流程折磨过,首先编译慢这个老生常态的问题就不提了,希望C++20的module能够快快普及开来,减少开发人员的时间负担(我好想import std啊,呜呜)。更为离谱的是,长久以来c++的社区都缺乏一个真正高效的统一构建工具,能够负责从第三方库管理一直到项目文件生成,然而这点隔壁的Rust做的就很好,Cargo永远的神。
所以本来我以为这辈子的C++生活可能就这样了,windows上用msbuild,然后跨平台用camke写写脚本,再配合上ninja之类的构建工具来生成项目文件。但是直到有一天我水知乎的时候发现有很多人都在推荐xmake这款构建工具,说其既可以管理第三方库,又可以进行项目文件生成。我遂尝试了一番,结果发现,嘿,真香!
故在这里贴上xmake的地址,希望大家有机会可以去试一下,真的很不错!而且还是国产的。
至于我的具体配置其实是从麦老师这里学习而来,是clang-cl + xmake + vscode。
vulkan环境的配置在有了xmake之后,我们可以写出一个非常简单的配置文件,我这里给出我的作为参考 ...
实现std::move和std::forward
你所需要的基础知识通用引用和右值引用在实现std::move()以及std::forward()前,我们要对通用引用以及右值引用之间做一个基本的了解。一般来讲,当我们见到&&的时候,我们都会认为这是个右值引用,是用来绑定到右值上一个引用类型。但是如果这种情况出现模板参数的推导时,事情就会发生变化。
1234template <typename T>void Func(T&& param) { std::cout << param << std::endl;}
上面这种情况如果在不知道通用引用的情况下,我们会认为这个函数的参数只能绑定到右值上,因为很明显,&&代表的是右值引用。但是实际上下面的两个调用都可以通过编译并且给出运行结果。
123std::string value = "Hello world!";Func(value);Func(std::move(value));
也就是说神奇的事情发生了,Func即接受左值,又接收右值,通常来讲我们只会在con ...
场景加速结构
场景加速结构在游戏引擎中,如何高效的管理游戏场景物体是一个非常重要的话题,在执行碰撞检测或者光线追踪求交时,如果我们每帧都要进行暴力检测的话,毫无疑问或造成非常多的计算资源浪费。于是为了解决这些问题,我们在构建世界的时候,往往会利用一些场景加速结构(Acceleration Structure)来管理。
常规的场景加速机构的主要思路是,我们利用一些空间信息或者物体位置来对物体进行树形划分,每个子节点管理一批物体,这样的话在查询的时候,我们就不用遍历整个场景,而是可以做到亚线性(Sublinear)的速度。
总的来看,场景划分的方法可以分为两大类,一种是基于空间的算法(Spatial Partition),一种是基于物体的划分(Object Partition)。
Spatial Partition
Oct-Tree: 通常成为八叉树,和二叉树其实本质上并不区别,每次将空间划分为8个区域,并且在有物体的地方继续进行递归。
KD-Tree: KDTree和八叉树的区别是在于KDTree交替选择空间的三个轴进行切分,然后分成两块,而不是像八叉树沿着三个轴划分为8份。
BSP- ...
多线程下的缓存优化
项目背景在写Raytracer或者Rasterizer的时候,由于每个像素之间的计算是可以做到相互独立的,所以一个常见的优化便是开启多线程来进行加速,这样的话便不会说出现一核有难,多核围观的情况。但是,多线程启动后,我们需要额外关注对于全局状态的访问,并且尽可能的提高程序的cache友好性。
不过这次主要是对Finding and Fixing Slow Code // Ray Tracing series的一个记录,下面的代码是记录当光线击中物体时进行反射的一个情况,在测试的时候,发现Random::vec3()这个函数开销非常大,大概总时间的50%都是在算Random,这个说实话确实不太合理。
12ray.Origin = payload.WorldPosition + payload.WorldNormal * 0.0001f;ray.Direction = reflect(ray.Direction, payload.WorldNormal + material.Roughness * Random::Vec3(-0.5f, 0.5f));
在找到重点问题后,我 ...
返回值优化(RVO, NRVO)
返回值优化返回值优化在C++里面主要是通过RVO(Return Value Optimization)以及NVRO(Named Return Value Optimization)来完成。其中NVRO是在C++11中被引入,并且在C++17中作为强制要求。本质上是一种Copy Elision, 用来减少按值返回临时对象时所发生的拷贝。
具体来讲,我们分析下面这个例子
12345std::vector<int> func() { std::vector<int> data = {1, 2, 3, 4, 5}; return data;}std::vector<int> t = func();
第一眼看上去,99%的人都会觉得这东西性能一定有大问题,毕竟直接返回了一个数组,那不得每次都得拷贝一大堆数据。但是实际上如果我们打开类似于cpp insight之类的分析工具,我们会看到以下代码。
12345std::vector<int, std::allocator<int> > func() ...