面试问答汇总

图形学相关

渲染管线

顶点数据 => 顶点着色器 => 曲面细分 => 几何着色器 => 图元装配 => 裁剪剔除 => 光栅化 => 片元着色器 => 测试混合

Unity轻量级渲染管线

又称通用渲染管线,区别于Unity内置渲染管线,仅保留很小的C++内核,暴露出更多的接口来支持C#脚本自定义渲染管线。特点为注重性能,单通道前向渲染,Shader Graph支持。而Unity内置渲染管线则更加通用而功能强大。

渲染为什么以三角形为基础
  • 任意三点必然共面。
  • 三角形是最简单的多边形。
  • 三角形经多种转换之后,仍然是三角形,对于仿射转换和透视转换成立。
  • 几乎所有商用图形加速硬件都是为三角形光栅化而设计的。
变换矩阵的存储方式
  • Direct3D:行优先存储(row-major),变换时行向量右乘矩阵
  • OpenGL:列优先存储(column-major),变换时列向量左乘矩阵
  • HLSL:列优先存储(column-major)
  • GLSL:列优先存储(column-major)
人物阴影渲染
两个立方体之间的阴影渲染
Shadow Map的基本原理

先从光源位置观察渲染一遍深度信息保存,第二遍渲染时将物体的坐标转换至光源观察坐标,和之前记录的深度比较,判断物体是否在阴影内。

PBR主要参数

以Unity标准材质为例:

  • Albedo 反射率(漫反射)
  • Metallic 金属度
  • specular 高光度(镜面反射)
  • Smoothness 光滑度
  • Occlusion 环境光遮蔽贴图
  • Normal 法线贴图
  • Emission 自发光贴图

扩展链接:https://zhuanlan.zhihu.com/p/53086060

光栅化的几种方法

线段扫描转换:

  • 数字微分画线算法DDA
  • Bresenham光栅化算法

多边形填充:

  • 扫描线填充算法
BRDF的物理含义

双向反射分布函数:

双向:入射方向和观察方向(分别为BRDF的两个输入)

BRDF:描述了入射光线经过某个表面反射后如何在各个出射方向上分布

想象你有一个不透明的桌面,一个激光发射器。你先让激光向下垂直地射在那个桌面上,这样你就可以在桌面上看到一个亮点,接着你从各个不同的方向来观察那个亮点,你会发现亮点的亮度随着观察方向的不同而发生了改变。然后你站着不动,改变激光发射方向和桌面的夹角,你又会发现亮点的亮度发生了改变。这就是说,一个表面对不同的光线入射角和反射角的组合,拥有不同的反射率。BRDF就是用来对这种反射性质进行定义的。

https://www.zhihu.com/question/20286038

mipmap的用途

Mipmapping是贴图渲染中常用技术,为了加快渲染速度减少图像锯齿,贴图被处理成由一系列被预先计算和优化过的图片组成的文件,被称为mipmap。

Mipmap中每一个层级的小图都是主图的一个特定比例的缩小细节的复制品,当贴图被缩小或者只需要从远距离观看时,mipmap就会转换到适当的层级。

引擎相关

游戏引擎主要模块

渲染、物理、动画、AI、输入、内存管理、资源管理、编辑器、网络、UI

可靠UDP的实现思路

最简单的方式是在应用层模仿传输层TCP的可靠性传输。

  • 1、添加TCP中的seq/ack机制,确保数据发送到对端。
  • 2、添加发送和接收缓冲区,主要是用户超时重传。
  • 3、添加超时重传机制。

详细说明:送端发送数据时,生成一个随机seq=x,然后每一片按照数据大小分配seq。数据到达接收端后接收端放入缓存,并发送一个ack=x的包,表示对方已经收到了数据。发送端收到了ack包后,删除缓冲区对应的数据。使用定时任务定时检查是否需要重传数据。

目前有如下开源程序利用UDP实现了可靠的数据传输:分别为RUDP、RTP、UDT

C#相关

C#拆装箱

装箱:值类型变量转变为引用类型Object存储在堆内存中

拆箱:引用类型Object转变为值类型存储在线程栈中

ArrayList(每个元素为Object类型)中存取Vector3(值类型)时分别用到装拆箱操作。

C#结构体与类的区别
  1. 结构体是值类型,类是引用类型
  2. 结构体不能被继承,但是结构体能够继承接口
  3. 结构体不能包含显式默认构造函数,没有析构函数;类有默认构造函数,有析构函数,可以使用abstract和sealed,有protected修饰符,必须使用new初始化。
委托与事件

(等我用到的时候再来写)

C++相关

指针常量和常量指针

*前面的是对被指向对象的修饰,*后面的是对指针本身的修饰。

const int p;      // p 为整型常量
const int* p; // p 为指向整型常量的指针
int const* p; // p 为指向整型常量的指针
int* const p; // p 为指向整型变量的指针常量
智能指针

(暂留)

new的两个阶段
  1. 调用operator new (sizeof(A))
  2. 调用A:A()
  3. 返回指针

事实上,分配内存这一操作就是由operator new(size_t)来完成的,如果类A重载了operator new,那么将调用A::operator new(size_t ),否则调用全局::operator new(size_t ),后者由C++默认提供。

动态链接库和静态链接库区别

二者的区别在于编译过程中的链接阶段。

静态链接是指把调用的函数链接到可执行文件中,成为可执行文件的一部分。换句话说,函数代码就在程序的exe文件中,该文件包含了运行时所需的全部代码。当多个程序都调用相同函数时,内存中就会存在这个函数的多个拷贝,这样就浪费了宝贵的内存资源。

动态链接所调用的函数代码并没有被拷贝到应用程序的可执行文件中去,而是仅仅在其中加入了所调用函数的描述信息。仅当应用程序被装入内存开始运行时,才在应用程序与相应的DLL之间建立链接关系。当要执行所调用DLL中的函数时,根据链接产生的重定位信息,才转去执行DLL中相应的函数。一般情况下,如果一个应用程序使用了动态链接库,系统保证内存中只有DLL的一份复制品。

动态链接库和静态链接库的相同点是它们都实现了代码的共享。

静态链接库lib文件将导出声明和实现都放在lib中。编译后所有代码都嵌入到宿主程序。该lib中不能再包含其他动态链接或者静态链接的库了。

动态链接库lib文件相当于一个h文件,是对实现部分(dll文件)的导出声明。一个dll文件中可以包含其他动态链接库或者静态链接库。

静态库特点:

  1. 静态库对函数库的链接是放在编译时期完成的。
  2. 程序在运行时与函数库再无瓜葛,移植方便。
  3. 浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。

动态库特点:

  1. 动态库把对一些库函数的链接载入推迟到程序运行的时期。
  2. 可以实现进程之间的资源共享。(因此也称为共享库)
  3. 将一些程序升级变得简单。
  4. 甚至可以真正做到链接载入完全由程序员在程序代码中控制(显式调用)。

算法相关

(暂时不写)

项目相关

毛笔写字效果
夜视效果