(渲染)(引擎)软光栅渲染器与UE光照系统原理

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

性能

控制台命令:

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

stat unit			//显示部分渲染参数
stat rhi			//显示各种渲染信息(rendering head interface)
stat scenerendering	//显示所有渲染信息
  • 开销主要来自Draw Call,而不是三角形面数。三角形面数不等于Draw Call次数

  • 动态阴影开销很大(随着三角形数量而增加),而不是动态光照

  • 半透明由于像素着色器导致高开销

渲染管线

CPU端
  • 视锥剔除

  • 渲染排序

  • SetPass Call:(设置渲染状态)

    告诉GPU要使用哪个Shader,哪种混合模式,设置背面剔除等

  • Draw Call:

    告诉GPU要使用哪些模型数据进行渲染

GPU端
  • Vertex Shader

    模型空间 => 裁剪空间

    • 模型空间 M> 世界空间 V> 相机空间 P> 裁剪空间
  • (Early-Z)

  • 硬件阶段 (图元装配,光栅化)

    裁剪空间 视口变换> 屏幕空间 图元装配> 图元 光栅化> 片元

    • 裁剪空间 剔除裁剪操作透视除法> NDC坐标 背面剔除视口转换==> 屏幕坐标 图元装配> 图元 光栅化> 片元
  • Fragment Shader

    片元 着色> 像素

  • 输出合并

    Alpha测试 => Stencil Test(模板测试) => Depth Test => Blending => Frame Buffer

软光栅渲染器
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
  • Draw Call就是渲染时采用的单一处理过程,几何体会根据Draw Call逐个渲染

不宜超过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
  • 由于硬件是设定,以2*2像素格为单位询问这4个像素是否在三角形上,以获取纹理等信息。如果多边形很小,会对4个像素着色,这称为Overshading。

  • Overshading会导致一个像素组被多次计算

  • Scene面板 => Lit => Optimization Viewmodes => Quad Overdraw

    显示OverShading

Tips:

  1. 多边形越密集,渲染消耗更大(从远处看)。可以使用LOD来解决。
  2. 初始Pixel Shader的pass越复杂,Overshading损耗越大,所以Overshading对Forward Rendering产生的影响比Diferred Rendering大
2. G-Buffer
  • 不再依靠几何体计算结果,而是通过图片 (Render Target)。

  • G-Buffer用于合成各种内容(材质,光照,雾等)。

  • 显示G-Buffer: Scene面板 => Buffer Visualization => Overview

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来包含模型

五.反射

  • 反射几乎是除了光照以外,最难实现的效果之一

  • UE中有3种反射系统

    ↓ 优先程度依次升高

    1. Sky Light Reflection

    2. Reflection Captures (反射探针)

      在一个特定的位置捕获一张静态 Cube map,细节面板的Cubemap

      可在Project Settings中修改分辨率

      • Precalculated

        在打开关卡时更新捕获结果,课通过Lighting => Update Reflection Captures手动更新

      • Very fast

        只是一张混合在材质上的图片

      • Inaccurate

      • Local effect near the capture location

        相机靠近探针时,反射会变得精确。只在范围内起作用

    3. Planar Reflections

      类似反射探针,但是使用平面捕获内容,而且是动态的

      • 动态
      • 消耗大
      • 不适合墙面反射,适合平滑的平面
    4. Screen Space Reflection

      在Post Process Volume中设置

      • 能反射所有对象
      • 实时,精确
      • 有噪点,损耗很大
      • 只会反射屏幕中的内容

六.静态照明

1. Lightmass

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

  • 它是编辑器之外的渲染器

  • 支持网格分布式渲染

    Lightmass Importance Volume

    体积内的物体会有更高质量的光照

    The Lightmass Importance Volume controls the area that Lightmass emits photons in, allowing you to concentrate it only on the area that needs detailed indirect lighting. Areas outside the importance volume get only one bounce of indirect lighting at a lower quality.

    Multiple Lightmass Importance Volume

    most of the lighting work will be done with a bounding box that contains all of them. However, Volume Lighting Samples are only placed inside the smaller volumes.

    Lightmass Portals

    When Lightmass is building the light, the Lightmass Portals tell Lightmass that more light rays should come from this area yielding higher quality light and shadows

    Tips:

    1. Movable objects outside the Lightmass Importance Volume will have black indirect lighting.
    2. Ambient occlusion is enabled by default, and can be disabled by unchecking the checkbox Use Ambient Occlusion in the Lightmass Settings of Lightmass under the World Settings .
2. Indirect Lighting Cache

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

  • ILC会根据动态物体的位置,将各点上的间接光信息(Lightmass的结果) 进行插值

  • 设置

    Scene面板 => Show => Visualize => 勾选Volume Lighting Samples

    Actor实例 => Indirect Lighting Cache => Quality

    World Settings => Volume Light Sample Placement:生成点的密度

    其中的每一个点都存储了间接环境光照,物体会找到最近的点,查询它的亮度,将其与自身混合

光照贴图
  • UV

    • 导入模型时,勾选Generate Lightmap UVs,则会生成一套UV用于光照贴图

      Scene面板 => View Mode => Optimization Viewmodes => Lightmap Density:

      • 按网状形式展示光照贴图UV坐标

      Actor实例 => Overridden Light Map Resolution:

      • 调节光照贴图分辨率
    • 没有UV,静态光照无法工作

  • 光照贴图分辨率会影响内存和文件大小,而不是帧率,和纹理类似。

七.动态照明

1. Shadows
(1) Regular Dynamic Shadows
    • 设置:

      Light Actor实例 => Movable && 勾选Cast Shadows

    • 生成硬阴影,会与光照贴图混合

    • 不贡献全局光照

(2) Per Object Shadows - Stationary Light Shadows
    • 设置:

      Light Actor实例 => Stationary && 勾选Cast Shadows

    • 固定阴影会混合使用静态光照和动态光照

    • 生成软阴影

(3) Cascaded Shadow Maps (CSM) - Directional light shadowing
    • 设置

      Light Actor实例 => Cascaded Shadow Maps

    • 只能被用于 Directional Light

    • 超过一定距离的阴影不会被计算

(4) Distance Field Shadow - Use DF info instead of tracing geometry
    • DFAO

      距离场环境光遮蔽(DFAO)一般用来算特别大范围的遮蔽,和SSAO的高频率的小范围遮蔽不同, DFAO出来的效果不会特别精细,而是特别软、特别大范围的效果,相比烘GI,DFAO的好处是动态可以有物体级别的移动,只是不支持顶点动画,很适合堡垒之夜这种需要经常拆房子的游戏

      • 先分tile做剔除,这点和阴影差不多,只是这个是在世界空间,而不是光源空间剔除
      • 然后合并global sdf,用4层clipmap,每层128*128*128,且卷屏绘制来降低开销
      • trace的过程是在cone的前端追踪mesh的sdf,末端追踪global sdf
    • 设置

      Project Settings => Generate Mesh Distance Fields

      Light Actor Instance => Distance Field Shadow

    Snene面板 => Show => Visualize => Mesh Distance Fields

    • 全屏1spp的ray march,在pc平台上是完全可以接受的 。 因为没有随机,1spp画面也是稳定的

    • march的方向是固定朝着光源方向进行

      执行步骤:

      先光源空间分tile和每个mesh的bound求交,存tile list,然后每个像素在trace的时候变换到光源空间,从tile list里取可能相交的mesh sdf贴图,然后对这些贴图进行march。虚幻的sdf阴影是没有合并mesh sdf到global sdf的,只是将所有的mesh sdf给atlas到一张512*512*512的体贴图上(300m显存)

      1. 剔除 (分Tile剔除)

        对距离场进行march之前,我们要确定当前这个像素点会交到哪些距离场,因为march的方向是固定朝着光源方向进行,所以我们从光源的方向进行分块剔除是一个比较合适的思路。

      2. Ray March (cone trace)

        • 沿光源方向march,步进过程中求最小的 surfaceDistance / rayDistance,作为阴影的值,如果碰到负的,就说明是本影,如果没碰到负的,那么取最小的距离和半径的比值,作为半影的遮蔽值,cone的角度大小决定软硬程度,角度越小的cone,trace出来的阴影越硬
    float march(float3 rayOrigin, float3 rayDirection)
    {
    	int maxDistance = 64;
    	float rayDistance = 0.01;
    	for(rayDistance ; rayDistance < maxDistance;)
    	{
    		float3 p = rayOrigin + rayDirection * rayDistance;
    		float surfaceDistance = map(p);	//获取距离场
    		if(surfaceDistance < 0.001)
    		{
    			return 0.0;
    		}
    		rayDistance += surfaceDistance;
    	}
    	return 1.0;
    }
    
    • 软阴影:
    ret = min(ret, 10 * surfaceDistance / rayDistance);
    
    • 距离场本质上是一张2D Texture(体积纹理),但是以3D显示

      • 这张Texture被切割成不同部分,每个部分按层堆叠,形成立体区域

      • 暴力建场的方法,就是直接迭代32*32*32个点,然后在每个点上向周围发射一大堆光线,然后保存交到的最小距离,作为距离场在这个点上的值

    • 使用距离场表示的几何会投射阴影

Tips:

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

      Capsule Shadows can be used to enable support for soft shadow casting for Skeletal Meshes by using a capsule representation of your character made from a Physics Asset.

2. Lighting
(1) Screen Space Global Illumination
    • 设置:

      Project Settings => Rendering => Screen Space Global Illumination

    • 控制台:

      r.SSGI.Quality = 1	//1(high) ~ 4(low)
      r.SSGI.HalfRes		//render SSGI at half resolution
      
    • works best as a supplimental indirect lighting illumination method to precomputed lighting from Lightmass.

(2) Light Channels
    • 设置:

      Light Actor Instance => Lighting Channels

      Mesh Actor Instance => Lighting Channels

    • Lighting Channels is a rendering feature that enables you to choose what renderable surfaces are lit by specific lights.

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
  • 基于Pixel Shader,根据深度贴图
  • the fog fades in the distance
Atmospheric and Exponential Fog

Asmospheric fog 性能更好一点

Exponential fog 效果更强一点

九.后处理

  • Light Bloom
  • Depth of Field/Blurring
  • Some types of Lensflares
  • Light Shafts
  • Vignette
  • Tonemapping/Color correction
  • Exposure
  • Motion Blur

Anti-aliazing

Tips:

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

FXAA:太糊了

TAA:运动物体能看到残影

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

posted @ 2022-07-21 01:28  Heskey0  阅读(500)  评论(0编辑  收藏  举报

载入天数...载入时分秒...