游戏引擎 浅入浅出

Introduction

前言












12. 拆分引擎和项目















89. Doxygen生成API文档





代码资源下载


目录

24.2 FBO RenderTexture GameTurbo DLSS

  1. 「游戏引擎 浅入浅出」是一本开源电子书,PDF/随书代码/资源下载: https://github.com/ThisisGame/cpp-game-engine-book
  1. CLion项目文件位于 samples\engine_editor\draw_rtt

原神游戏画面为何变糊,手机厂商宣称的GameTurbo又是如何提升帧率?

这到底是米哈游的技术优化,还是手机厂商的丧心病狂?

DLSS/FSR又是何方神圣,与GameTurbo有何区别?

1. 什么是FBO?

  1. 参考:https://www.khronos.org/opengl/wiki/Rendering_Pipeline_Overview

在OpenGL里,在顶点着色器计算好顶点在屏幕坐标,在片段着色器计算好像素颜色之后,还需要经过下列测试:

  1. 裁剪测试:判断像素是否窗口内
  2. 模板测试
  3. 深度测试

14.3 UIMask 介绍了模板测试,这里就不再重复。

1.1 深度测试与深度缓冲区

和模板测试一样,GPU中有一个二维数组缓冲区,尺寸等于屏幕尺寸,每个像素对应数组的一个值,这个值就是当前像素记录的深度,这个二维数组缓冲区就是深度缓冲区。

垂直于屏幕的一条直线上,可能有多个片段。

OpenGL每处理一个片段,就把它的深度记录到深度缓冲区,当处理下一个片段的深度比这个大,那就抛弃这个片段,否则就更新深度值。

就是通过这个处理,实现了近处物体遮挡远处物体。

1.2 颜色缓冲区

经过深度测试后,还需要进行颜色混合运算、逻辑运算、ColorMask运算,然后可以确定了要显示的颜色。

每个像素点都有一个颜色值,那么也需要一个二维数组,记录整个屏幕的颜色值,这个二维数组就是颜色缓冲区。

1.3 帧缓冲区

这一系列的测试所需要的缓冲区,每一帧它们都会刷新重置,它们都归属于帧缓冲区。

默认情况下,是由操作系统提供的帧缓冲区。

操作系统提供的默认帧缓冲区,是直接输出到显示器上的,应用程序没法对其进行修改。

所以OpenGL提供了帧缓冲区对象(OpenGL FrameBuffer Object),简称FBO,将模板、深度、颜色先存储到FBO中,然后再输出到系统提供的默认帧缓冲区中。

有了FBO这个中间对象后,我们就可以对FBO进行修改,例如修改色调、加后期等。

1.4 附着点

OpenGL的FBO自身并不存储数据,它记录着颜色缓冲区、深度缓冲区的指针。

也就是说,我们需要创建颜色缓冲区、深度缓冲区,然后指定到FBO里。

这个指针,叫做附着点。

FBO包括一个模板附着点、一个深度附着点、多个颜色附着点。

  1. 模板数据,其实是int 二维数组。
  2. 深度数据,也是 int 二维数组。
  3. 颜色数据,其实是vec3二维数组。

int二维数组,其实就是单通道纹理。
vec3二维数组,就是3通道纹理。

所以,3种附着点,都可以设置为Texture,就是模板贴图、深度贴图、颜色贴图,也就是RenderTexture。

而我们日常所说的RenderTexture,一般是指颜色贴图。

当启用了模板测试,则必须设置模板附着点,不然模板数据没有地方存储,就不会进行模板测试。

当启用了深度测试,则必须设置深度附着点,不然深度数据没有地方存储,就不会进行深度测试。

2. 创建FBO与RenderTexture

在OpenGL中使用FBO特别简单。

当创建了FBO对象,并且指定了使用它,那么OpenGL就会渲染到FBO里。

创建FBO之前,先来创建深度附着点、颜色附着点所需的Texture。

这里新建RenderTexture类,来创建Texture与FBO。

  1. //file:source/renderer/render_texture.h
  2. class Texture2D;
  3. class RenderTexture {
  4. public:
  5. RenderTexture();
  6. ~RenderTexture();
  7. /// 初始化RenderTexture,在GPU生成帧缓冲区对象(FrameBufferObject)
  8. /// \param width
  9. /// \param height
  10. void Init(unsigned short width,unsigned short height);
  11. unsigned short width(){
  12. return width_;
  13. }
  14. void set_width(unsigned short width){
  15. width_=width;
  16. }
  17. unsigned short height(){
  18. return height_;
  19. }
  20. void set_height(unsigned short height){
  21. height_=height;
  22. }
  23. unsigned int frame_buffer_object_handle(){
  24. return frame_buffer_object_handle_;
  25. }
  26. /// 是否正在被使用
  27. bool in_use(){
  28. return in_use_;
  29. }
  30. void set_in_use(bool in_use){
  31. in_use_=in_use;
  32. }
  33. Texture2D* color_texture_2d(){
  34. return color_texture_2d_;
  35. }
  36. Texture2D* depth_texture_2d(){
  37. return depth_texture_2d_;
  38. }
  39. private:
  40. unsigned short width_;
  41. unsigned short height_;
  42. unsigned int frame_buffer_object_handle_;//关联的FBO Handle
  43. Texture2D* color_texture_2d_;//FBO颜色附着点关联的颜色纹理
  44. Texture2D* depth_texture_2d_;//FBO深度附着点关联的深度纹理
  45. bool in_use_;//正在被使用
  46. };
  1. //file:source/renderer/render_texture.cpp
  2. RenderTexture::RenderTexture(): width_(128), height_(128), frame_buffer_object_handle_(0),in_use_(false),
  3. color_texture_2d_(nullptr),depth_texture_2d_(nullptr) {
  4. }
  5. RenderTexture::~RenderTexture() {
  6. if(frame_buffer_object_handle_>0){
  7. RenderTaskProducer::ProduceRenderTaskDeleteFBO(frame_buffer_object_handle_);
  8. }
  9. //删除Texture2D
  10. if(color_texture_2d_!= nullptr){
  11. delete color_texture_2d_;
  12. }
  13. if(depth_texture_2d_!= nullptr){
  14. delete depth_texture_2d_;
  15. }
  16. }
  17. void RenderTexture::Init(unsigned short width, unsigned short height) {
  18. width_=width;
  19. height_=height;
  20. color_texture_2d_=Texture2D::Create(width_,height_,GL_RGB,GL_RGB,GL_UNSIGNED_SHORT_5_6_5, nullptr,0);
  21. depth_texture_2d_=Texture2D::Create(width_,height_,GL_DEPTH_COMPONENT,GL_DEPTH_COMPONENT,GL_UNSIGNED_SHORT, nullptr,0);
  22. //创建FBO任务
  23. frame_buffer_object_handle_ = GPUResourceMapper::GenerateFBOHandle();
  24. RenderTaskProducer::ProduceRenderTaskCreateFBO(frame_buffer_object_handle_,width_,height_,color_texture_2d_->texture_handle(),depth_texture_2d_->texture_handle());
  25. }

RenderTexture::Init里,发出了创建Texture与FBO的任务。

在渲染线程中,执行创建FBO的逻辑。

  1. //file:source/render_device/render_task_consumer.cpp line:423
  2. /// 创建FBO任务
  3. void RenderTaskConsumer::CreateFBO(RenderTaskBase* task_base){
  4. RenderTaskCreateFBO* task=dynamic_cast<RenderTaskCreateFBO*>(task_base);
  5. //查询当前GL实现所支持的最大的RenderBufferSize,就是尺寸
  6. GLint support_size=0;
  7. glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE, &support_size);
  8. //如果我们设定的尺寸超过了所支持的尺寸,就抛出错误
  9. if (support_size <= task->width_ || support_size <= task->height_) {
  10. DEBUG_LOG_ERROR("CreateFBO FBO Size Too Large!Not Support!");
  11. return;
  12. }
  13. //创建FBO
  14. GLuint frame_buffer_object_id=0;
  15. glGenFramebuffers(1, &frame_buffer_object_id);__CHECK_GL_ERROR__
  16. if(frame_buffer_object_id==0){
  17. DEBUG_LOG_ERROR("CreateFBO FBO Error!");
  18. return;
  19. }
  20. GPUResourceMapper::MapFBO(task->fbo_handle_, frame_buffer_object_id);
  21. glBindFramebuffer(GL_FRAMEBUFFER, frame_buffer_object_id);__CHECK_GL_ERROR__
  22. //将颜色纹理绑定到FBO颜色附着点
  23. GLuint color_texture=GPUResourceMapper::GetTexture(task->color_texture_handle_);
  24. glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, color_texture, 0);__CHECK_GL_ERROR__
  25. //将深度纹理绑定到FBO深度附着点
  26. GLuint depth_texture=GPUResourceMapper::GetTexture(task->depth_texture_handle_);
  27. glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depth_texture, 0);__CHECK_GL_ERROR__
  28. glBindFramebuffer(GL_FRAMEBUFFER, 0);__CHECK_GL_ERROR__
  29. }

创建了FBO之后,将创建的颜色纹理和深度纹理,分别绑定到FBO的颜色附着点、深度附着点上。

3. 渲染到RenderTexture

创建RenderTexture实例,并调用RenderTexture::Init初始化之后,FBO也就创建好了。

接下来在每一帧开始渲染时,设置将场景渲染到RenderTexture。

实例框架的渲染,是遍历所有Camera,然后在Camera下遍历所有MeshRenderer。

那么只要在Camera遍历MeshRenderer之前,设置好RenderTexture,就可以使用FBO了。

Camera添加RenderTexture成员变量。

  1. //file:source/renderer/camera.h
  2. class Camera: public Component {
  3. public:
  4. ......
  5. /// 检查target_render_texture_是否设置,是则使用FBO,渲染到RenderTexture。
  6. void CheckRenderToTexture();
  7. /// 检查是否要取消使用RenderTexture.
  8. void CheckCancelRenderToTexture();
  9. /// 设置渲染目标RenderTexture
  10. /// \param render_texture
  11. void set_target_render_texture(RenderTexture* render_texture);
  12. /// 清空渲染目标RenderTexture
  13. void clear_target_render_texture();
  14. protected:
  15. ......
  16. RenderTexture* target_render_texture_;//渲染目标RenderTexture
  17. ......
  18. };

在每个Camera的每一帧渲染之前,都检查是否指定了RenderTexture,如果指定了,就使用FBO,将游戏画面渲染到FBO。

  1. //file:source/renderer/camera.cpp line:71
  2. /// 检查target_render_texture_是否设置,是则使用FBO,渲染到RenderTexture。
  3. void Camera::CheckRenderToTexture(){
  4. if(target_render_texture_== nullptr){//没有设置目标RenderTexture
  5. return;
  6. }
  7. if(target_render_texture_->in_use()){
  8. return;
  9. }
  10. if(target_render_texture_->frame_buffer_object_handle() == 0){//还没有初始化,没有生成FBO。
  11. return;
  12. }
  13. RenderTaskProducer::ProduceRenderTaskSetViewportSize(target_render_texture_->width(),target_render_texture_->height());
  14. RenderTaskProducer::ProduceRenderTaskBindFBO(target_render_texture_->frame_buffer_object_handle());
  15. target_render_texture_->set_in_use(true);
  16. }
  17. /// 检查是否要取消使用RenderTexture.
  18. void Camera::CheckCancelRenderToTexture(){
  19. if(target_render_texture_== nullptr){//没有设置目标RenderTexture
  20. return;
  21. }
  22. if(target_render_texture_->in_use()==false){
  23. return;
  24. }
  25. if(target_render_texture_->frame_buffer_object_handle() == 0){//还没有初始化,没有生成FBO。
  26. return;
  27. }
  28. //更新ViewPort的尺寸
  29. RenderTaskProducer::ProduceRenderTaskSetViewportSize(Screen::width(),Screen::height());
  30. RenderTaskProducer::ProduceRenderTaskUnBindFBO(target_render_texture_->frame_buffer_object_handle());
  31. target_render_texture_->set_in_use(false);
  32. }
  33. /// 设置渲染目标RenderTexture
  34. /// \param render_texture
  35. void Camera::set_target_render_texture(RenderTexture* render_texture){
  36. if(render_texture== nullptr){
  37. clear_target_render_texture();
  38. }
  39. target_render_texture_=render_texture;
  40. }
  41. /// 清空渲染目标RenderTexture
  42. void Camera::clear_target_render_texture() {
  43. if(target_render_texture_== nullptr){//没有设置目标RenderTexture
  44. return;
  45. }
  46. if(target_render_texture_->in_use()== false){
  47. return;
  48. }
  49. RenderTaskProducer::ProduceRenderTaskUnBindFBO(target_render_texture_->frame_buffer_object_handle());
  50. target_render_texture_->set_in_use(false);
  51. }
  52. ......
  53. void Camera::Foreach(std::function<void()> func) {
  54. for (auto iter=all_camera_.begin();iter!=all_camera_.end();iter++){
  55. current_camera_=*iter;
  56. current_camera_->CheckRenderToTexture();
  57. current_camera_->Clear();
  58. func();
  59. current_camera_->CheckCancelRenderToTexture();
  60. }
  61. }

要注意的是,当设置了FBO之后,所有的物体就会被渲染到FBO上,FBO顶替了系统默认帧缓冲区。

我们可以简单的认为,FBO就是一个看不见的屏幕,所以使用FBO也叫做离屏渲染

由于FBO附着点上的Texture尺寸,并不一定等于窗口尺寸,所以设置FBO时,需要更新ViewPort的尺寸:

  1. //file:source/renderer/camera.cpp line:96
  2. /// 检查是否要取消使用RenderTexture.
  3. void Camera::CheckCancelRenderToTexture(){
  4. ......
  5. //更新ViewPort的尺寸
  6. RenderTaskProducer::ProduceRenderTaskSetViewportSize(Screen::width(),Screen::height());
  7. RenderTaskProducer::ProduceRenderTaskUnBindFBO(target_render_texture_->frame_buffer_object_handle());
  8. ......
  9. }

4. 测试

在之前的Lua代码中,渲染了一个飞机。

现在我们创建好RenderTexture,然后设置到3D Camera上,将飞机渲染到RenderTexture上。

然后再将RenderTexture以UI的形式显示出来。

  1. --file:example/login_scene.lua
  2. ......
  3. --- 创建主相机
  4. function LoginScene:CreateMainCamera()
  5. --创建相机1 GameObject
  6. self.go_camera_= GameObject.new("main_camera")
  7. --挂上 Transform 组件
  8. self.go_camera_:AddComponent(Transform):set_position(glm.vec3(0, 0, 10))
  9. self.go_camera_:GetComponent(Transform):set_rotation(glm.vec3(0, 0, 0))
  10. --挂上 Camera 组件
  11. self.camera_=self.go_camera_:AddComponent(Camera)
  12. --设置为黑色背景
  13. self.camera_:set_clear_color(0,0,0,1)
  14. self.camera_:set_depth(0)
  15. self.camera_:set_culling_mask(1)
  16. self.camera_:SetView(glm.vec3(0.0,0.0,0.0), glm.vec3(0.0,1.0,0.0))
  17. self.camera_:SetPerspective(60, Screen:aspect_ratio(), 1, 1000)
  18. --设置RenderTexture
  19. self.render_texture_ = RenderTexture.new()
  20. self.render_texture_:Init(960,640)
  21. self.camera_:set_target_render_texture(self.render_texture_)
  22. end
  23. ......
  24. function LoginScene:CreateUI()
  25. -- 创建UI相机 GameObject
  26. self.go_camera_ui_=GameObject.new("ui_camera")
  27. -- 挂上 Transform 组件
  28. local transform_camera_ui=self.go_camera_ui_:AddComponent(Transform)
  29. transform_camera_ui:set_position(glm.vec3(0, 0, 10))
  30. -- 挂上 Camera 组件
  31. local camera_ui=self.go_camera_ui_:AddComponent(UICamera)
  32. camera_ui:set_culling_mask(2)
  33. -- 设置正交相机
  34. camera_ui:SetView(glm.vec3(0, 0, 0), glm.vec3(0, 1, 0))
  35. camera_ui:SetOrthographic(-Screen.width()/2,Screen.width()/2,-Screen.height()/2,Screen.height()/2,-100,100)
  36. -- 创建 UIImage
  37. self.go_ui_image_rtt_=GameObject.new("draw render texture")
  38. self.go_ui_image_rtt_:set_layer(2)
  39. -- 设置尺寸
  40. local rect_transform=self.go_ui_image_rtt_:AddComponent(RectTransform)
  41. rect_transform:set_rect(glm.vec2(960,640))
  42. -- 挂上 UIImage 组件,将RenderTexture绘制出来
  43. local ui_image_mod_bag=self.go_ui_image_rtt_:AddComponent(UIImage)
  44. ui_image_mod_bag:set_texture(self.render_texture_:color_texture_2d())
  45. end
  46. ......

运行后效果:

和之前不使用RenderTexture的效果一致。

效果一致的原因是,本质上FBO可以看做是一块临时的软屏幕,将原本要输出到系统屏幕缓冲区的颜色数据,输出到了FBO。

另外一个原因就是,我们上面的代码,创建的RenderTexture尺寸,和实际系统屏幕缓冲区尺寸,也就是窗口尺寸是一致的。

5. GameTurbo

这里有个有意思的是,RenderTexture的宽高。

在Unity创建RenderTexture,也是需要指定宽高的。

这或许是句废话,创建Texture,当然需要指定宽高。

那这个宽高会影响到什么?

默认情况下,OpenGL是使用系统提供的默认帧缓冲区,而这个默认帧缓冲区,就等于屏幕像素值,当你的手机是960x640,那就是960x640。

这就意味着,每一帧,片段处理器要执行960x640次。

那现在使用了OpenGL的FBO,创建了480x320大小的RenderTexture,那么片段处理器就只需要处理480x320次了。

这个性能提升怎么样呢?

下图是960x640的原生分辨率RenderTexture下,GPU在5.4&-6.2%之间浮动。

下图是480x320的低分辨率RenderTexture下,GPU在4.6&-5.4%之间浮动。

提升巨大!

当然了,FBO的颜色数据最终还是要送到系统默认的帧缓冲区去的,要呈现在显示器中。

这个时候480x320的数据,要显示到960x640的屏幕上,就会模糊了。

而红色的关机按钮,是作为UI渲染,并没有使用FBO。

很多大型手游为了保证流畅度,都会选中这种,降低场景FBO分辨率、保证UI原生分辨率的方式。

下面是我在原神里,截取的极高、极低画面效果对比,设备为一加Ace Pro。

可以看到场景人物分辨率差距很大,而UI并没有变化。

现在各品牌手机也有类似功能,智能分辨率,就是检测到游戏负载压力大时,直接就把游戏的默认帧缓冲区分辨率给降低。

好处都不用游戏自己来写逻辑,带来的问题就是,不仅场景人物模糊了,UI都给模糊了……

所以玩家们还是要关闭这种系统优化,在游戏自带的设置里调整比较好。

6. DLSS/FSR

上面我们将游戏场景渲染到480x320的RenderTexture上,然后放到到960x640,性能得到极大提升,但是画面变得特别模糊。

RenderDoc中拿到的480x320RenderTexture,渲染到了960x640的窗口。

我之前用过一些人工智能放大图片的软件,可以将低分辨率图片,放到至2x、4x大小,而保持一定的清晰度,这里以waifu2x举例。

  1. https://github.com/nagadomi/waifu2x/

先在RenderDoc中,将480x320的RenderTexture保存为图片。

从RenderDoc中将480x320的RenderTexture保存为图片

480x320的RenderTexture

然后用waifu2x进行放大。

waifu2x设置界面

实际游戏画面和waifu2x使用AI放大的对比。

效果不是很好,毕竟只是固定的AI模型,没有针对我们的项目进行训练。

那如果有一套AI,针对我们的项目进行大量训练,然后在游戏以低分辨率运行时,用AI实时将低分辨率画面进行2x的提升,这样既能降低GPU消耗的同时,又能保证一定的画质,岂不是很美好!

这就是NVIDIA DLSS的功能了。

从网上摘抄一段DLSS简介:

  1. NVIDIA DLSS是唯一由AI驱动的超级分辨率技术,这一优势可转化为最高2倍的游戏性能提升。
  2. 同时它也是唯一可用的缩放技术,借助深度学习神经网络,确保图像质量媲美原生分辨率。
  3. 图像缩放技术若没有AI支持的时间性缩放,生成的图像会产生难看的伪影,如运动伪影、闪烁图案和暗淡、模糊的纹理。
  4. 简单来说DLSS就是利用AI深度学习神经网络在不影响图像质量的同时提升性能,带给玩家最好的画质和游戏体验。

DLSS是一种超分辨率技术,就是将低分辨率图片,借助AI技术,将其放大至2X,4X倍数,而不会变模糊。

就是说DLSS可以将480x320的RenderTexture,借助AI技术,实时将其放大到960x640,甚至1920x1080。

FSR的作用也差不多,只不过DLSS是NVIDIA的,而FSR是AMD家开源的。

后续有希望在手机上使用到FSR,届时手游性能会有一个新的提升,这不是GameTurbo这种伪技术可以比拟的。

Introduction

前言












12. 拆分引擎和项目















89. Doxygen生成API文档