其他分享
首页 > 其他分享> > 【Heskey带你玩渲染】软光栅渲染器与UE光照系统原理

【Heskey带你玩渲染】软光栅渲染器与UE光照系统原理

作者:互联网

本文未经允许禁止转载
B站:Heskey0

性能

控制台命令:

stat fps		//显示帧率
t.maxfps = 600	//修改上限帧率

stat unit			//显示部分渲染参数
stat rhi			//显示各种渲染信息(rendering head interface)
stat scenerendering	//显示所有渲染信息

渲染管线

CPU端
GPU端
软光栅渲染器
1. Load Scene

//Light

struct Light
{
	Vector4f position;
	Vector3f intensity;
	Vector3f ambientIntensity;
};

//Camera

struct Camera
{
	Vector3f position;	//相机坐标
	Vector3f g;			//观看方向,单位
	Vector3f t;			//向上向量,单位
	float fov;			//视野(高)
	float nNear;		//近平面
	float nFar;			//远平面
	float aspectRatio;	//屏幕宽高比
};

//Mesh + Triangle + Vertex

struct Vertex
{
	// Position Vector
	Vector3 Position;

	// Normal Vector
	Vector3 Normal;

	// Texture Coordinate Vector
	Vector2 TextureCoordinate;
};
struct Triangle
{
	Vector4f v[3]; //三个顶点的齐次坐标
	Vector3f color[3]; //三个顶点的颜色
	Vector4f normal[3]; //三个法线坐标,方便后面计算,所以用4维向量
	Vector2f texCoord[3]; //三个纹理坐标
}
struct Mesh
{
	std::vector<Triangle> triangles;	//三角形
	Vector3f position;					//位置
	Vector3f rotation;					//旋转
	Vector3f scale;						//缩放
};

(CPU) Load a Mesh

//Load a Mesh
//CPU.cpp (main.cpp)
Mesh* mesh = new Mesh();
for (int i = 0; i < loadedMesh.Vertices.size(); i += 3)
{
	Triangle* tmpTriangle = new Triangle();
    //遍历Vertex
    //每3个Vertex组成一个Triangle
	for (int j = 0; j < 3; j++)
	{
		tmpTriangle->SetVertex(j, Vector4f(loadedMesh.Vertices[i + j].Position.X, loadedMesh.Vertices[i + j].Position.Y, loadedMesh.Vertices[i + j].Position.Z, 1.0f));
		tmpTriangle->SetColor(j, Vector3f(255, 255, 255));
		tmpTriangle->SetNormal(j, Vector4f(loadedMesh.Vertices[i + j].Normal.X, loadedMesh.Vertices[i + j].Normal.Y, loadedMesh.Vertices[i + j].Normal.Z, 0.0f));
		tmpTriangle->SetTexCoord(j, Vector2f(loadedMesh.Vertices[i + j].TextureCoordinate.X, loadedMesh.Vertices[i + j].TextureCoordinate.Y));
	}
	mesh->triangles.push_back(*tmpTriangle);
}
mesh->position = Vector3f(0, 0, 0);
mesh->rotation = Vector3f(0, 135, 0);
mesh->scale = Vector3f(1, 1, 1);
2.提交资源到GPU

//GPU的数据成员

class GPU
{
private:
	int width, height; //视口宽高

	//顶点着色器相关矩阵
	Matrix4f model, view, projection;
	Matrix4f mvp;
	Matrix4f viewport;

	//Buffer
	std::vector<Vector3f> frameBuffer;
	std::vector<float> depthBuffer;

    //CPU Data : Texture
	std::optional<Texture> texture;
	std::optional<Texture> bumpMap;
	std::optional<Texture> normalMap;
    //CPU Data : Scene
    std::vector<Mesh> meshList;
    std::vector<Light> lightList;
    Camera camera;
    
	Shader shader;
};

(CPU) 数据提交到GPU

//CPU.cpp (main.cpp)
SetMesh(gpu);
SetLight(gpu);
SetCamera(gpu);
SetTexture(gpu);
3. Vertex Shader

(GPU) MVP矩阵

//Model Matrix
void GPU::SetModelMatrix(const Mesh& o)
{
	Matrix4f rX, rY, rZ; //XYZ轴的旋转矩阵
	float radX, radY, radZ; //xyz轴的旋转弧度
	Matrix4f scale; //缩放矩阵
	Matrix4f move; //位移矩阵

	radX = ToRadian(o.rotation.x());
	radY = ToRadian(o.rotation.y());
	radZ = ToRadian(o.rotation.z());
	rX << 1, 0, 0, 0,
		0, cos(radX), -sin(radX), 0,
		0, sin(radX), cos(radX), 0,
		0, 0, 0, 1;
	rY << cos(radY), 0, sin(radY), 0,
		0, 1, 0, 0,
		-sin(radY), 0, cos(radY), 0,
		0, 0, 0, 1;
	rZ << cos(radZ), -sin(radZ), 0, 0,
		sin(radZ), cos(radZ), 0, 0,
		0, 0, 1, 0,
		0, 0, 0, 1;
	scale << o.scale.x(), 0, 0, 0,
		0, o.scale.y(), 0, 0,
		0, 0, o.scale.z(), 0,
		0, 0, 0, 1;
	move << 1, 0, 0, o.position.x(),
		0, 1, 0, o.position.y(),
		0, 0, 1, o.position.z(),
		0, 0, 0, 1;
	//矩阵左乘计算出模型矩阵
	model = move * scale * rZ * rY * rX;
}


//View Matrix
void GPU::SetViewMatrix(const Camera& c)
{
	//将摄像机移动到原点,然后使用旋转矩阵的正交性让摄像机摆正
	Matrix4f t; //移动矩阵
	Vector3f cX; //摄像机的x轴
	Matrix4f r; //旋转矩阵的旋转矩阵

	t << 1, 0, 0, -c.position.x(),
		0, 1, 0, -c.position.y(),
		0, 0, 1, -c.position.z(),
		0, 0, 0, 1;
	cX = c.g.cross(c.t);
	r << cX.x(), cX.y(), cX.z(), 0,
		c.t.x(), c.t.y(), c.t.z(), 0,
		-c.g.x(), -c.g.y(), -c.g.z(), 0,
		0, 0, 0, 1;
	//矩阵左乘计算出视图矩阵
	view = r * t;
}


//Projection Matrix
void GPU::SetProjectionMatrix(const Camera& c)
{
	//透视投影矩阵
	Matrix4f p2o; //将梯台状透视视锥挤成长方体正交投影
	Matrix4f orthoTrans, orthoScale, ortho; //正交投影矩阵的平移和缩放分解
	float t, r; //近平面的上边界和右边界
	float radFov; //视野的弧度制

	radFov = ToRadian(c.fov);
	t = tan(radFov / 2) * c.nNear;	//Near面y长度
	r = c.aspectRatio * t;	//Near面x长度

    //透视投影:视椎体拍扁,与Near面同长宽
	p2o << c.Near, 0, 0, 0,
		0, c.Near, 0, 0,
		0, 0, c.Far + c.Near, c.Near* c.Far,
		0, 0, -1, 0;
    //正交投影:把中心移到坐标原点,再缩放到[-1, 1]^3裁剪空间内
	orthoTrans << 1, 0, 0, 0,
		0, 1, 0, 0,
		0, 0, 1, (c.Near + c.Far) / 2,
		0, 0, 0, 1;
	orthoScale << 1 / r, 0, 0, 0,
		0, 1 / t, 0, 0,
		0, 0, 2 / (c.Far - c.Near), 0,
		0, 0, 0, 1;
	ortho = orthoScale * orthoTrans;
	//矩阵左乘计算出透视投影矩阵
	projection = ortho * p2o;
}


//Viewport Matrix
viewport << width / 2, 0, 0, width / 2,
			0, height / 2, 0, height / 2,
			0, 0, 1, 0,
			0, 0, 0, 1;

//Vertex Shader

//所有三角形顶点 变换到裁剪空间,然后将齐次坐标归一化
//法线和光源不做projection变换,但要做齐次归一化:因为光照在相机空间进行计算
//每一个Mesh
SetModelMatrix(object);
SetViewMatrix(c);
SetProjectionMatrix(c);
mvp = projection * view * model;
for (Triangle& t : Mesh.triangles)
{
	//对于三角形的每个顶点
	for (auto& vec : t.v)
	{
		//变换到裁剪空间
		vec = mvp * vec;
        //变换到屏幕空间
		vec = viewport * vec;
		//齐次坐标归一化
		vec.x() /= vec.w();
		vec.y() /= vec.w();
		vec.z() /= vec.w();
		vec.w() /= vec.w();
	}
	//对于三角形的每个法线
	for (auto& nor : t.normal)
	{
		nor = view * model * nor;
		nor = nor.normalized();
	}
}
//每一个光源
for (auto& l : lightList)
{
	//变换
	l.position = view * l.position;

	//齐次坐标归一化
	l.position.x() /= l.position.w();
	l.position.y() /= l.position.w();
	l.position.z() /= l.position.w();
	l.position.w() /= l.position.w();
}
4. Fragment Shader

//设置bounding box

//为每个三角形设置1个bounding box
float minXf, maxXf, minYf, maxYf;
minXf = width;
maxXf = 0;
minYf = height;
maxYf = 0;
for (const auto& ver : t.v)
{
	if (ver.x() <= minXf) minXf = ver.x();
	if (ver.x() >= maxXf) maxXf = ver.x();
	if (ver.y() <= minYf) minYf = ver.y();
	if (ver.y() >= maxYf) maxYf = ver.y();
}
if (minXf >= width || maxXf <= 0 || minYf >= height || maxYf <= 0) continue;
if (minXf < 0) minXf = 0;
if (maxXf > width) maxXf = width;
if (minYf < 0) minYf = 0;
if (maxYf > height) maxYf = height;
//将BoundingBox取整
int minX, maxX, minY, maxY;
minX = floor(minXf);
maxX = ceil(maxXf);
minY = floor(minYf);
maxY = ceil(maxYf);

//重心坐标

// 计算2D重心坐标
std::tuple<float, float, float> Rasterizer::Barycentric2D(float x, float y, const Vector4f* v)
{
	float c1 = (x * (v[1].y() - v[2].y()) + (v[2].x() - v[1].x()) * y + v[1].x() * v[2].y() - v[2].x() * v[1].y()) / (v[0].x() * (v[1].y() - v[2].y()) + (v[2].x() - v[1].x()) * v[0].y() + v[1].x() * v[2].y() - v[2].x() * v[1].y());
	float c2 = (x * (v[2].y() - v[0].y()) + (v[0].x() - v[2].x()) * y + v[2].x() * v[0].y() - v[0].x() * v[2].y()) / (v[1].x() * (v[2].y() - v[0].y()) + (v[0].x() - v[2].x()) * v[1].y() + v[2].x() * v[0].y() - v[0].x() * v[2].y());
	float c3 = (x * (v[0].y() - v[1].y()) + (v[1].x() - v[0].x()) * y + v[0].x() * v[1].y() - v[1].x() * v[0].y()) / (v[2].x() * (v[0].y() - v[1].y()) + (v[1].x() - v[0].x()) * v[2].y() + v[0].x() * v[1].y() - v[1].x() * v[0].y());
	return { c1,c2,c3 };
}

// 计算3D重心坐标
std::tuple<float, float, float> Rasterizer::Barycentric3D(const Vector4f& point, const Vector4f* v)
{
	//计算法线方向
	Vector3f a, b, c, p;
	a = Vector3f(v[0].x(), v[0].y(), v[0].z());
	b = Vector3f(v[1].x(), v[1].y(), v[1].z());
	c = Vector3f(v[2].x(), v[2].y(), v[2].z());
	p = Vector3f(point.x(), point.y(), point.z());

	Vector3f n = (b - a).cross(c - a);
	Vector3f na = (c - b).cross(p - b);
	Vector3f nb = (a - c).cross(p - c);
	Vector3f nc = (b - a).cross(p - a);
	float c1 = n.dot(na) / (n.norm() * n.norm());
	float c2 = n.dot(nb) / (n.norm() * n.norm());
	float c3 = n.dot(nc) / (n.norm() * n.norm());
	return { c1,c2,c3 };
}

//着色

//对于每一个bounding box中的每一个像素
for (int y = minY; y < maxY; y++)
{
	for (int x = minX; x < maxX; x++)
	{
		//判断像素中心是否在三角形内
		if (InsideTriangle((float)x + 0.5f, (float)y + 0.5f, t))
		{
			//在的话计算2D重心坐标
			float alpha2D, beta2D, gamma2D;
			std::tie(alpha2D, beta2D, gamma2D) = Barycentric2D((float)x + 0.5f, (float)y + 0.5f, t.v);
			float w2D = alpha2D + beta2D + gamma2D;
			float interpolateZ2D = Interpolate(alpha2D, beta2D, gamma2D, t.v[0].z(), t.v[1].z(), t.v[2].z());
			interpolateZ2D /= w2D;
			//像素中点的坐标
			Vector4f p{ (float)x + 0.5f,(float)y + 0.5f,interpolateZ2D,1.f };

			//模型空间Position
			p = mvp.inverse() * viewport.inverse() * p;
			Vector4f v[3] =
			{
				mvp.inverse() * viewport.inverse() * t.v[0],
				mvp.inverse() * viewport.inverse() * t.v[1],
				mvp.inverse() * viewport.inverse() * t.v[2]
			};

			//计算3D重心坐标
			float alpha3D, beta3D, gamma3D;
			std::tie(alpha3D, beta3D, gamma3D) = Barycentric3D(p, v);
			float interpolateZ3D = Interpolate(alpha3D, beta3D, gamma3D, t.v[0].z(), t.v[1].z(), t.v[2].z());
			interpolateZ3D *= -1;

			//相机空间Position
			Vector4f position[3] =
			{
				projection.inverse() * viewport.inverse() * t.v[0],
				projection.inverse() * viewport.inverse() * t.v[1],
				projection.inverse() * viewport.inverse() * t.v[2],
			};

			//深度测试
			if (depthBuffer[GetPixelIndex(x, y)] > interpolateZ3D)
			{
				//深度更近的话插值出各种值,然后更新深度信息
				auto interpolateColor = Interpolate(alpha3D, beta3D, gamma3D, t.color[0], t.color[1], t.color[2]);
				auto interpolateNormal = Interpolate(alpha3D, beta3D, gamma3D, t.normal[0], t.normal[1], t.normal[2]).normalized();
				if (interpolateNormal.head<3>().dot(Vector3f(0, 0, 1)) <= 0) continue;
				auto interpolatePosition = Interpolate(alpha3D, beta3D, gamma3D, position[0], position[1], position[2]);
				auto interpolateTexCoord = Interpolate(alpha3D, beta3D, gamma3D, t.texCoord[0], t.texCoord[1], t.texCoord[2]);
				//传入Shader需要的信息
				shader.SetColor(interpolateColor);
				shader.SetNormal(interpolateNormal);
				shader.SetLight(lightList);
				shader.SetPosition(interpolatePosition);
				shader.SetTexCoord(interpolateTexCoord);

				Vector3f pixelColor = shader.Shading();
				SetPixelColor(Vector2i(x, y), pixelColor);
				depthBuffer[GetPixelIndex(x, y)] = interpolateZ3D;
			}
		}
	}
}

Tips:

  1. 遍历Mesh的Vertex,每3个Vertex组成一个Triangle
  2. View Matrix将摄像机移动到原点(连同场景所有顶点),然后使用旋转矩阵的正交性让摄像机摆正
  3. Projection Matrix
    1. 透视投影:视椎体拍扁,与Near面同长宽;
    2. 正交投影:把中心移到坐标原点,再缩放到[-1, 1]^3空间内
  4. Vertex Shader
    1. 所有三角形顶点 变换到裁剪空间,然后将齐次坐标归一化
    2. 法线和光源不做projection变换,但要做齐次归一化:因为光照在相机空间进行计算
  5. 通过Projection Matrix之后,重心坐标会改变。在三维空间中的属性(光照,法线不做projection),建议在三维空间中做插值,再变换到二维。

一.Intro

延迟渲染:生成G-buffer中的图片,这些图片最后组合到屏幕上

  1. Shading happens in deferred passes
  2. Works compositing based using the G-Buffer
  3. Good at rendering dynamic light
  4. Good at stable predictable(稳定, 可预测) high end performance
  5. More flexible when it comes to disabling features, less flexible when it comes to surface attributes
  6. No MSAA possible, relies on TAA

前向渲染

  1. Computes shading in the same pass as geometry/materials
  2. More flexible in how lighting/materials are computed but less flexible when many features are mixed
  3. Good at translucent surface rendering
  4. Faster for simpler uses
  5. Dynamic lighting has a big performance impact
  6. MSAA possible

二.渲染前的遮挡

按照流水线方式工作:

CPU(calculate) 1 => CPU(calculate) 2 => CPU(calculate) 3

​ CPU(Draw)1 => CPU(Draw) 2 => CPU(Draw) 3

​ GPU 1 => GPU 2 => GPU 3

  1. CPU(calculate): CPU计算所有逻辑和一切对象的位置 => transform

  2. CPU(Draw): culling => list of all visible models

    Culling:

    ↓ 依次执行,开销依次增大

    1. Distance Culling

      不是默认设置,要手动开启

      Actor实例的:

      (1)Desired Max Draw Distance属性

      • 超过某个最大距离时将物体剔除

      • 为0时禁用Distance Culling

      (2)Current Max Draw Distance属性

      • 由Cull Distance Volume的Cull Distance控制(会修改Volume Box中Actor的属性)
    2. Frustum Culling

    3. Precomputed Visibility

      World Settings的:

      precompute visibility属性

      • 先放置precomputed visibility volume
      • Show=> Visualize=> Precomputed Visibility Cells
      • 每个Cell都存储了这个位置可以看见的对象
    4. Occlusion Culling (遮挡剔除)

      控制台命令:

      freezerendering		//冻结当前已经绘制的物体
      stat initviews		//提供有关遮挡的概览
      

      会消耗更多性能,更加精确

Tips:

  1.  设置distance culling,默认不开启

  2. 粒子也能被遮挡(通过bounding box进行遮挡)

  3. 将模型拼合在一起会降低CPU效率

    原因:只要能看到模型的一小部分,整个合并的网格体都会渲染

三.几何渲染

  1. GPU
1. Draw Calls

不宜超过5000

  • 每一种新材质都意味着一次新的Draw Call

  • 每次渲染器完成Draw Call时,都需要接收来自渲染线程的命令(Draw Call之间有延迟),导致损耗增加

  • Components逐个遮挡,逐个渲染。Components == draw calls

为了降低draw call而拼合模型,会导致:

  1. worse for occlusion
  2. worse for lightmapping
  3. worse for collision calculation
  4. worse for memory

Modular Meshes:

  1. 使用更少的内存,容易放置,容易调整
  2. 每一张光照贴图分辨率会变高
  3. 但是draw call会更高
2. 实例化静态网格体渲染 (Instance static mesh component)

相同模型添加到实例化静态网格体组件中,按组渲染,有效降低Draw call

  • 模型会在内存中实例化,并不会在渲染时实例化
  • 在内存中实例化意味着:如果导入模型,它只在内存中存在一次
  • 如果在关卡中放置两次,内存任然不变,但是会被渲染两次

Tips:

  1. 只合并相同材质的网格体
  2. 远距离物体适合被合并
3. HLOD (Hierachical LOD)
  • LOD只是降低面数,不减少Draw Call

  • HLOD将远处的物体分组,把一组对象换成一个对象,降低Draw Call

Window => Hierachical LOD Outliner

4. Early-Z

Project Settings的:

Early Z-pass

四.光栅化和G-Buffer

1. Overshading

Tips:

  1. 多边形越密集,渲染消耗更大(从远处看)。可以使用LOD来解决。
  2. 初始Pixel Shader的pass越复杂,Overshading损耗越大,所以Overshading对Forward Rendering产生的影响比Diferred Rendering大
2. G-Buffer

RT0 : Scene Color Deferred

    • 有静态光照,本质上只是纹理

RT1 : G Buffer A

    • 世界法线贴图

RT2 : G Buffer B

    • Metallic + Specular + Roughness (材质编辑器中的属性)
    • 反映了场景的光泽度

RT3 : G Buffer C

    • 不带任何光照效果的图像

RT4 : 屏幕中特殊区域 (例如毛发,次表面散射)

RT5 : 同RT4

DS : Scene Depth Z 深度贴图

Custom Depth:

  • Actor实例的细节面板开启Render CustomDepth Pass。

    • 显示:Scene面板 => Viewport Options => High Resolution Screenshot => 勾选Use Custom Depth as mask
  • 使用单独的RT或者说G Buffer来包含模型

五.反射

六.静态照明

1. Lightmass

是一个独立的应用,用于处理光源渲染和光照贴图烘焙。

2. Indirect Lighting Cache

ILC用来解决动态模型上的预计算光照,由Lightmass importance Volume生成

光照贴图

七.动态照明

1. Shadows
(1) Regular Dynamic Shadows
(2) Per Object Shadows - Stationary Light Shadows
(3) Cascaded Shadow Maps (CSM) - Directional light shadowing
(4) Distance Field Shadow - Use DF info instead of tracing geometry

Tips:

  • 比PCSS软,而且和CSM相比,因为没有那么大的几何和填充负担,所以反而要便宜很多,和静态的Shadow map相比,没有Draw Call,又可以支持物体级别的移动(虽然不支持顶点动画),所以是一个相当不错的阴影解决方案,堡垒之夜就用了这项技术,堡垒之夜的近处是级联阴影,远处是SDF
  • 因为涉及到大量求交,所以我们这时候就需要用到空间划分了,UE用的是KDop树
  • 一般距离场的范围是比物体的AABB大一圈
  • UE4采用的是暴力建场
  • 烘焙慢,一万面左右的模型,一个32分辨率的场大概要烘十来秒到一分钟不等,128分辨率的经常五分钟起步,对于比较大的项目而言,这会是一个比较大的开支,有的时候可以考虑只烘焙比较大的建筑物等。
  • 显存占用也是一个问题,比如在UE4里开启距离场会吃掉300M的显存
(5) Capsule Shadows - very cheap
2. Lighting
(1) Screen Space Global Illumination
(2) Light Channels

Tips:

  1. 损耗并不是来自光源本身,而是来自阴影
  2. 多边形面数不重要,但会对动态阴影产生影响,进而影响性能
  3. 距离场阴影最适合与棱角分明的静态模型一起使用。
  4. 调节光源的Max Draw Distance属性,或尽可能关闭阴影投射,能显著提升性能
  5. SSGI is recommended as a means to improve indirect lighting illumination in your scene but not as a sole indirect lighting method.

八.雾

Distance Fog
Atmospheric and Exponential Fog

Asmospheric fog 性能更好一点

Exponential fog 效果更强一点

九.后处理

Anti-aliazing

Tips:

MSAA:开销大,常规物体边缘效果最好,但是ps阶段的锯齿没有改善

FXAA:太糊了

TAA:运动物体能看到残影

SMAA:效果较好,没有FXAA和TAA那样有很糊的情况。性能较差,但比MSAA更好

标签:Draw,渲染器,渲染,Vector3f,float,Heskey,UE,CPU,光照
来源: https://www.cnblogs.com/Heskey0/p/16500496.html