Unity - gamma space下还原linear space效果

2024-01-25 06:04

本文主要是介绍Unity - gamma space下还原linear space效果,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 环境
  • 目的
  • 环境
  • 问题
  • 实践结果
  • 要处理的点
    • 处理细节
      • 【OnPostProcessTexture 实现 sRGB 2 Linear 编码】 - 预处理
      • 【封装个简单的 *.cginc】 - shader runtime
      • 【shader需要gamma space下还原记得 #define _RECOVERY_LINEAR_IN_GAMMA】
      • 【颜色参数应用前 和 颜色贴图采样后】
      • 【灯光颜色】
      • 【F0应用(绝缘体正对视角下的反射率)】
      • 【BRDF BRDF1_Unity_PBS 不适用gamma调整】
      • 【自发光颜色处理】
      • 【雾效颜色】
      • 【FBO的color处理Linear 2 sRGB的后处理】
    • Cubemap texture to linear
  • Project
  • References


环境

unity : 2023.3.37f1
pipeline : BRP


之前写过一篇: Gamma Correction/Gamma校正/灰度校正/亮度校正 - 部分 DCC 中的线性工作流配置,此文,自己修修改改不下于 50次,因为以前很多概念模糊

如果发现还有错误,请指出来,谢谢


目的

处理H5使用 WEB GL 1.0 的问题
因为项目要移植到 H5 WEB GL 1.0 的 graphics api
而因为我们之前的项目配置,使用的是 color space : linear
然后在H5平台中,如果使用 linear 的话,unity 会提示,只支持 WEB GL 2.0
而WEB GL 2.0 无论是 unity,微信,还是 浏览器,都是处于 BETA 阶段
甚至iOS或是 mac 下,直接不支持 (因为苹果要推他的 web metal,但是以前的 web gl 1.0 他是支持的)

因此为了设备兼容率,我们只能将 linear 转为 gamma

但是颜色空间不同的话,其实最大差异就是 sRGB 贴图颜色 和 最后后处理的 gamma校正 的处理
还有比较隐蔽的一些颜色相关的常量值 (比如PBR中的 绝缘体的 F0 常量值,等等)
还有灯光颜色,材质颜色,等


环境

unity : 2020.3.37f1
pipeline : BRP


问题

正常 Linear space 和 gamma space 下渲染差异如下:
在这里插入图片描述
在这里插入图片描述


实践结果

如下图,我目前对比了 linear 和 gamma 下的渲染区别
并且在 gamma space 下,尽可能的还原了 linear space 的效果
其中人物的衣服渲染算是还原了
这里头还有很多需要还原的:

  • skybox (cubemap ,这类 cube 还不能通过 SetPixels 设置值,会有报错)
  • 皮肤
  • 后处理的所有颜色

请添加图片描述

下面是又是后续处理了皮肤
还有头发之后的 (头发没有处理完整,因为使用 ASE 连连看练出来的,使用 surface shader,虽然可以生成一下 vert, frag 的方式在修改,但是我懒得去修改了,这样就是 PBR 的 BRDF 里面的部分曲线是不一样的,所以可以看到头发有一些差异)

(剩下一些: cubemap 的贴图部分没有没有还原,这部分后续再想想方案)
请添加图片描述


要处理的点

  1. 所有颜色贴图 (注意不是数据贴图)的 RGB 通道需要处理 预处理的 sRGB 2 Linear - 性能高一些,毕竟是预处理
  2. 或者是不在预处理阶段,而是改在: shader sample 后的 pow(tex_color, 2.2) - 会比较浪费性能,但是如果为了快速出效果,或是验证,这是不二之选
  3. 所有shading时,材质 (shahder program)传入的颜色相关参数都需要在 shading 前做 pow(color, 2.2)
  4. 也可以在预处理阶段处理所有材质里面的所有 color 遍历处理(工具化)
  5. 所有shading 结束后,增加一个 postprocess 后处理,将屏幕的所有颜色处理 Linear 2 sRGB

处理细节

【OnPostProcessTexture 实现 sRGB 2 Linear 编码】 - 预处理

在 AssetPostProcessor 中的 OnPostProcessTexture 回调用处理 Texture2D 的资源
其中 Texture2D 只包含, Texture2D, Sprite 的回调处理

注意:如果是 Cubemap 的纹理,unity是不会回调进这个函数的
而且 cubemap 的问题,我一直没想好怎么处理

还要注意,如果实现了预处理贴图,就不要在 shader runtime 对 sample 后的颜色贴图做 sRGB 2 Linear 了

    private static void GammaSpace_Non_HDR_TexPP_Handler(Texture2D texture){for (int mipmapIDX = 0; mipmapIDX < texture.mipmapCount; mipmapIDX++){Color[] c = texture.GetPixels(mipmapIDX);for (int i = 0; i < c.Length; i++){c[i] = c[i].linear;}texture.SetPixels(c, mipmapIDX);}}private static bool NeedToRemoveGammaCorrect(string assetPath){TextureImporter ti = AssetImporter.GetAtPath(assetPath) as TextureImporter;return NeedToRemoveGammaCorrect(ti);}// jave.lin : 是否需要删除 gamma correctprivate static bool NeedToRemoveGammaCorrect(TextureImporter ti){if (ti == null) return false;// jave.lin : 没开启if (PlayerPrefs.GetInt("Enabled_GammaSpaceTexPP", 0) == 0) return false;// jave.lin : linear color space 下不处理,gamma color space 下才处理if (QualitySettings.activeColorSpace == ColorSpace.Linear) return false;// jave.lin : 原来 linear 下,不是 sRGB 不用处理if (ti.sRGBTexture == false) return false;return true;}private void OnPostprocessTexture(Texture2D texture){Debug.Log($"OnPostprocessTexture.assetPath:{assetPath}");if (NeedToRemoveGammaCorrect(assetPath)){GammaSpace_Non_HDR_TexPP_Handler(texture);}}

代码太多,我只罗列出关键要修改的 PBR 着色的地方要修改的地方


【封装个简单的 *.cginc】 - shader runtime

注意:如果使用了 OnPostProcessTexture 实现 sRGB 2 Linear 编码 的预处理,就不要处理 shader runtime 里面的 sample 后的 COLOR_TRANS 或是 CHANGED_COLOR 处理

#ifndef __CUSTOM_COLOR_SPACE_VARS_H__
#define __CUSTOM_COLOR_SPACE_VARS_H__// jave.lin 2024/01/17
// custom the color space const & vars#define unity_ColorSpaceGrey1 fixed4(0.214041144, 0.214041144, 0.214041144, 0.5)
#define unity_ColorSpaceDouble1 fixed4(4.59479380, 4.59479380, 4.59479380, 2.0)
#define unity_ColorSpaceDielectricSpec1 half4(0.04, 0.04, 0.04, 1.0 - 0.04) // standard dielectric reflectivity coef at incident angle (= 4%)
#define unity_ColorSpaceLuminance1 half4(0.0396819152, 0.458021790, 0.00609653955, 1.0) // Legacy: alpha is set to 1.0 to specify linear mode#if defined(UNITY_COLORSPACE_GAMMA) && defined(_RECOVERY_LINEAR_IN_GAMMA)
// jave.lin : force using linear effect
#define __FORCE_LINEAR_EFFECT__
#endif#ifdef __FORCE_LINEAR_EFFECT__// sRGB to Linear    #define COLOR_TRANS(col) pow(col, 2.2)#define CHANGED_COLOR(col) (col = pow(col, 2.2));// const defines#define GREY_COLOR (unity_ColorSpaceGrey1)#define DOUBLE_COLOR (unity_ColorSpaceDouble1)#define DIELECTRIC_SPEC_COLOR (unity_ColorSpaceDielectricSpec1)#define LUMINANCE_COLOR (unity_ColorSpaceLuminance1)
#else// sRGB to Linear  #define COLOR_TRANS(col) (col)#define CHANGED_COLOR(col) // const defines - gamma space#define GREY_COLOR (unity_ColorSpaceGrey)#define DOUBLE_COLOR (unity_ColorSpaceDouble)#define DIELECTRIC_SPEC_COLOR (unity_ColorSpaceDielectricSpec)#define LUMINANCE_COLOR (unity_ColorSpaceLuminance)
#endif#endif

【shader需要gamma space下还原记得 #define _RECOVERY_LINEAR_IN_GAMMA】

`#define _RECOVERY_LINEAR_IN_GAMMA`

【颜色参数应用前 和 颜色贴图采样后】

half4 Albedo1(float4 texcoords)
{//return _Color * tex2D(_MainTex, texcoords.xy);//return _Color * tex2Dbias(_MainTex, float4(texcoords.xy, 0.0, UNITY_ACCESS_INSTANCED_PROP(Props, _MainTex_mipmapBias)));half4 __color = _Color; // jave.lin : if this color is HDR color, unnesscessory to do sRGB to Linearhalf4 __tex_color = tex2D(_MainTex, texcoords.xy);//CHANGED_COLOR(__color.rgb)CHANGED_COLOR(__tex_color.rgb)return __color * __tex_color;
}

【灯光颜色】

    UnityLight mainLight = MainLight();CHANGED_COLOR(mainLight.color.rgb) // jave.lin : gamma correct light color

【F0应用(绝缘体正对视角下的反射率)】

使用我们自己定义的 DIELECTRIC_SPEC_COLOR

inline half OneMinusReflectivityFromMetallic1(half metallic)
{// We'll need oneMinusReflectivity, so//   1-reflectivity = 1-lerp(dielectricSpec, 1, metallic) = lerp(1-dielectricSpec, 0, metallic)// store (1-dielectricSpec) in DIELECTRIC_SPEC_COLOR.a, then//   1-reflectivity = lerp(alpha, 0, metallic) = alpha + metallic*(0 - alpha) =//                  = alpha - metallic * alphahalf oneMinusDielectricSpec = DIELECTRIC_SPEC_COLOR.a;return oneMinusDielectricSpec - metallic * oneMinusDielectricSpec;
}inline half3 DiffuseAndSpecularFromMetallic1(half3 albedo, half metallic, out half3 specColor, out half oneMinusReflectivity)
{specColor = lerp(DIELECTRIC_SPEC_COLOR.rgb, albedo, metallic);oneMinusReflectivity = OneMinusReflectivityFromMetallic1(metallic);return albedo * oneMinusReflectivity;
}FragmentCommonData1 MetallicSetup1(half3 albedo, fixed2 metallicGloss)
{half metallic = metallicGloss.x;half smoothness = metallicGloss.y; // this is 1 minus the square root of real roughness m.half oneMinusReflectivity;half3 specColor;// half3 diffColor = DiffuseAndSpecularFromMetallic(Albedo(i_tex), metallic, /*out*/ specColor, /*out*/ oneMinusReflectivity);half3 diffColor = DiffuseAndSpecularFromMetallic1(albedo, metallic, /*out*/specColor, /*out*/oneMinusReflectivity);FragmentCommonData1 o = (FragmentCommonData1) 0;o.diffColor = diffColor;o.specColor = specColor;o.oneMinusReflectivity = oneMinusReflectivity;o.smoothness = smoothness;return o;
}

【BRDF BRDF1_Unity_PBS 不适用gamma调整】

注释掉下面代码

//#ifdef UNITY_COLORSPACE_GAMMA
//        specularTerm = sqrt(max(1e-4h, specularTerm)); // jave.lin : if you want to recovery linear result in gamma space, don't do this one
//#endif

【自发光颜色处理】

    // jave.lin : emissionhalf3 emission_col = Emission(i.tex.xy);CHANGED_COLOR(emission_col.rgb)c.rgb += emission_col.rgb;

【雾效颜色】

    CHANGED_COLOR(unity_FogColor.rgb)UNITY_EXTRACT_FOG_FROM_EYE_VEC(i);UNITY_APPLY_FOG(_unity_fogCoord, c.rgb);return OutputForward(c, s.alpha);

【FBO的color处理Linear 2 sRGB的后处理】

csharp monobehaviour 如下

// jave.lin : 2024/01/08
// testing linear to gamma (linear to srgb)using UnityEngine;[ExecuteInEditMode]
public class LinearToGammaPP : MonoBehaviour
{public Color backgroundColor;public Shader shader;private Material material;private Camera cam;private bool InLinearColorSpace(){return QualitySettings.activeColorSpace == ColorSpace.Linear;}private void OnRenderImage(RenderTexture source, RenderTexture destination){if(cam == null) cam = GetComponent<Camera>();cam.backgroundColor = InLinearColorSpace() ? backgroundColor : backgroundColor.linear;if (InLinearColorSpace()){Graphics.Blit(source, destination);return;}if (material == null){material = new Material(shader);}Graphics.Blit(source, destination, material);}private void OnDestroy(){if(material != null){if (Application.isPlaying)Object.Destroy(material);elseObject.DestroyImmediate(material);}}
}

shader 如下

// jave.lin 2024/01/08 postprocess for linear 2 sRGBShader "Hidden/LinearToGamma"
{Properties{_MainTex ("Texture", 2D) = "white" {}}SubShader{// No culling or depthCull Off ZWrite Off ZTest AlwaysPass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct appdata{float4 vertex : POSITION;float2 uv : TEXCOORD0;};struct v2f{float2 uv : TEXCOORD0;float4 vertex : SV_POSITION;};v2f vert (appdata v){v2f o;o.vertex = UnityObjectToClipPos(v.vertex);o.uv = v.uv;return o;}sampler2D _MainTex;fixed4 frag (v2f i) : SV_Target{fixed4 col = tex2D(_MainTex, i.uv);#if defined(UNITY_COLORSPACE_GAMMA)col.rgb = pow(col.rgb, 1.0/2.2);//col.rgb = pow(col.rgb, 2.2);#endifreturn col;}ENDCG}}
}

Cubemap texture to linear

其实就是要对一些 HDR 贴图做 sRGB to Linear 的处理
HDR color 我们知道是: HDR_COLOR = color_normalized * pow(2, intensity)

因此我们只要算出 NextPowerOfTwo 就可以还原出 color_normalizedpow(2, intensity) ,就可以重新编码颜色

因此 HDR texture 的话,我们也尝试这种编码处理方式,但是会有 Unsupported GraphicsFormat(130) for SetPixel operations. 的错误,如下图:
在这里插入图片描述

CSHARP 代码中,我们看到代码没什么问题,但是 unity Cubemap 中不提供正确的 API 调用

    // jave.lin : 处理 HDR 的纹理// Cubemap.SetPixels 有异常: Unsupported GraphicsFormat(130) for SetPixel operations.// 通过 baidu, google 搜索得知,可以通过 un-compressed 格式 (比如:RGB(A)16,24,32,64)来避免这个问题// 但是会导致贴图内存增加很多(谨慎使用),因此只能代码中处理这部分的srgb to linearprivate static void GammaSpace_HDR_TexPP_Handler(Cubemap cubemap){var max_val = -1f;for (int faceIDX = 0; faceIDX < CubemapFaceIterateArray.Length; faceIDX++){var face = CubemapFaceIterateArray[faceIDX];// jave.lin : 获取第 0 层 mipmap 的 max valueColor[] colos_mipmap0 = cubemap.GetPixels(face, 0);for (int i = 0; i < colos_mipmap0.Length; i++){var c = colos_mipmap0[i];var temp_max_val = Mathf.Max(c.r, c.g, c.b);if (temp_max_val > max_val){max_val = temp_max_val;}}}Debug.Log($"max_val : {max_val}");if (max_val <= 1.0f){Debug.Log($"max_val <= 1.0f, non-HDR srgb to lienar, max_val : {max_val}");// jave.lin : 将 gamma space 下的 srgb to linearfor (int faceIDX = 0; faceIDX < CubemapFaceIterateArray.Length; faceIDX++){var face = CubemapFaceIterateArray[faceIDX];for (int mipmapIDX = 0; mipmapIDX < cubemap.mipmapCount; mipmapIDX++){Color[] colors_mipmap = cubemap.GetPixels(face, mipmapIDX);for (int i = 0; i < colors_mipmap.Length; i++){colors_mipmap[i] = colors_mipmap[i].linear;}// jave.lin : Unsupported GraphicsFormat(130) for SetPixel operations.cubemap.SetPixels(colors_mipmap, face, mipmapIDX);}}}else{//var assetPath = AssetDatabase.GetAssetPath(cubemap);//Debug.LogWarning($"不是HDR贴图不用处理, assetPath : {assetPath}");// jave.lin : 计算 next power of two (npot)var npot = (float)Mathf.Max(Mathf.NextPowerOfTwo((int)max_val), 1.0f);Debug.Log($"max_val > 1.0f, HDR srgb to lienar, max_val : {max_val}, npot : {npot}");// jave.lin : 将 gamma space 下的 srgb to linearfor (int faceIDX = 0; faceIDX < CubemapFaceIterateArray.Length; faceIDX++){var face = CubemapFaceIterateArray[faceIDX];for (int mipmapIDX = 0; mipmapIDX < cubemap.mipmapCount; mipmapIDX++){Color[] colors_mipmap = cubemap.GetPixels(face, mipmapIDX);for (int i = 0; i < colors_mipmap.Length; i++){var c = colors_mipmap[i];c = new Color(c.r / npot, c.g / npot, c.b / npot, c.a).linear;c *= new Color(npot, npot, npot, 1.0f);colors_mipmap[i] = c;}// jave.lin : Unsupported GraphicsFormat(130) for SetPixel operations.cubemap.SetPixels(colors_mipmap, face, mipmapIDX);}}}}private static void OnPostprocessCubemapEXT(string assetPath, Cubemap cubemap){Debug.Log($"OnPostprocessCubemapEXT.assetPath:{assetPath}");TextureImporter ti = AssetImporter.GetAtPath(assetPath) as TextureImporter;// jave.lin : 修改 readable (这一步风险有点大),会导致 主存、显存 都有一份 内存if (ti.isReadable == false){Debug.Log($"assetPath:{assetPath}, changing readable true");ti.isReadable = true;ti.SaveAndReimport();return;}GammaSpace_HDR_TexPP_Handler(cubemap);}private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths){foreach (var path in importedAssets){if (NeedToRemoveGammaCorrect(path)){Debug.Log($"OnPostprocessAllAssets.assetPath:{path}");//var tex2D = AssetDatabase.LoadAssetAtPath<Texture2D>(path);//var tex = AssetDatabase.LoadAssetAtPath<Texture>(path);var cubemap = AssetDatabase.LoadAssetAtPath<Cubemap>(path);// jave.lin : 下面输出:/*imported asset path: Assets/Scene/UiEffectScene/ReflectionProbe-0.exr, tex2D : , tex :ReflectionProbe-0 (UnityEngine.Cubemap), cubemap: ReflectionProbe-0 (UnityEngine.Cubemap)UnityEngine.Debug:Log(object)*///Debug.Log($"imported asset path: {path}, tex2D : {tex2D}, tex :{tex}, cubemap: {cubemap}");if (cubemap == null) continue;OnPostprocessCubemapEXT(path, cubemap);}}}

其实上面的代码判断 是否有分量 > 1.0f 的方式来判断是否 HDR 是不太合理的,因为不同的贴图格式的编码方式不同

有一些编码比如,RGBM,使用 A 通道来保存 255 被缩放的数值,作为: color_normalized * pow(2, A_channel_normalized * 255) 来解码

百度,谷歌上也没有搜索到对应的回答,唯一搜索到类似的:unity报错篇-Unsupported texture format - needs to be ARGB32。。。。

如果 使用了 带压缩格式的,然后再使用 Cubemap.SetPixels 都会报这个错误
在这里插入图片描述

注意压缩后大小非常小,才 288B 字节 (我这个是测试用的纹理)
在这里插入图片描述

然后我们将其格式修改成 未压缩 格式,就没有这个报错了
在这里插入图片描述

但是大小会比原来的大4倍
在这里插入图片描述

本身H5里面的内存就是很珍贵的设备资源,因此这种方式不可取
那么只能牺牲一些性能,在 shader 代码中采样处理了
比如: skybox对cubemap的处理,或是 reflection probe 等 IBL 反射效果 的 颜色的 pow(val, 2.2) 的处理


Project

  • Testing_Recovery_Linear_shading_in_UnityGammaSpace_2020.3.37f1_BRP.rar - 里面带有一些 逆向学习用的资源,不能公开
  • Testing_Recovery_Linear_shading_in_UnityGammaSpace_2020.3.37f1_BRP_V2.rar - 同上

References

  • gamma下还原linear效果

这篇关于Unity - gamma space下还原linear space效果的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/642292

相关文章

Kotlin Compose Button 实现长按监听并实现动画效果(完整代码)

《KotlinComposeButton实现长按监听并实现动画效果(完整代码)》想要实现长按按钮开始录音,松开发送的功能,因此为了实现这些功能就需要自己写一个Button来解决问题,下面小编给大... 目录Button 实现原理1. Surface 的作用(关键)2. InteractionSource3.

使用WPF实现窗口抖动动画效果

《使用WPF实现窗口抖动动画效果》在用户界面设计中,适当的动画反馈可以提升用户体验,尤其是在错误提示、操作失败等场景下,窗口抖动作为一种常见且直观的视觉反馈方式,常用于提醒用户注意当前状态,本文将详细... 目录前言实现思路概述核心代码实现1、 获取目标窗口2、初始化基础位置值3、创建抖动动画4、动画完成后

uniapp小程序中实现无缝衔接滚动效果代码示例

《uniapp小程序中实现无缝衔接滚动效果代码示例》:本文主要介绍uniapp小程序中实现无缝衔接滚动效果的相关资料,该方法可以实现滚动内容中字的不同的颜色更改,并且可以根据需要进行艺术化更改和自... 组件滚动通知只能实现简单的滚动效果,不能实现滚动内容中的字进行不同颜色的更改,下面实现一个无缝衔接的滚动

Java实现图片淡入淡出效果

《Java实现图片淡入淡出效果》在现代图形用户界面和游戏开发中,**图片淡入淡出(FadeIn/Out)**是一种常见且实用的视觉过渡效果,它可以用于启动画面、场景切换、轮播图、提示框弹出等场景,通过... 目录1. 项目背景详细介绍2. 项目需求详细介绍2.1 功能需求2.2 非功能需求3. 相关技术详细

使用animation.css库快速实现CSS3旋转动画效果

《使用animation.css库快速实现CSS3旋转动画效果》随着Web技术的不断发展,动画效果已经成为了网页设计中不可或缺的一部分,本文将深入探讨animation.css的工作原理,如何使用以及... 目录1. css3动画技术简介2. animation.css库介绍2.1 animation.cs

Flutter实现文字镂空效果的详细步骤

《Flutter实现文字镂空效果的详细步骤》:本文主要介绍如何使用Flutter实现文字镂空效果,包括创建基础应用结构、实现自定义绘制器、构建UI界面以及实现颜色选择按钮等步骤,并详细解析了混合模... 目录引言实现原理开始实现步骤1:创建基础应用结构步骤2:创建主屏幕步骤3:实现自定义绘制器步骤4:构建U

idea maven编译报错Java heap space的解决方法

《ideamaven编译报错Javaheapspace的解决方法》这篇文章主要为大家详细介绍了ideamaven编译报错Javaheapspace的相关解决方法,文中的示例代码讲解详细,感兴趣的... 目录1.增加 Maven 编译的堆内存2. 增加 IntelliJ IDEA 的堆内存3. 优化 Mave

Vue项目的甘特图组件之dhtmlx-gantt使用教程和实现效果展示(推荐)

《Vue项目的甘特图组件之dhtmlx-gantt使用教程和实现效果展示(推荐)》文章介绍了如何使用dhtmlx-gantt组件来实现公司的甘特图需求,并提供了一个简单的Vue组件示例,文章还分享了一... 目录一、首先 npm 安装插件二、创建一个vue组件三、业务页面内 引用自定义组件:四、dhtmlx

前端原生js实现拖拽排课效果实例

《前端原生js实现拖拽排课效果实例》:本文主要介绍如何实现一个简单的课程表拖拽功能,通过HTML、CSS和JavaScript的配合,我们实现了课程项的拖拽、放置和显示功能,文中通过实例代码介绍的... 目录1. 效果展示2. 效果分析2.1 关键点2.2 实现方法3. 代码实现3.1 html部分3.2

基于Python实现PDF动画翻页效果的阅读器

《基于Python实现PDF动画翻页效果的阅读器》在这篇博客中,我们将深入分析一个基于wxPython实现的PDF阅读器程序,该程序支持加载PDF文件并显示页面内容,同时支持页面切换动画效果,文中有详... 目录全部代码代码结构初始化 UI 界面加载 PDF 文件显示 PDF 页面页面切换动画运行效果总结主