写在前面
最近开始学习Unity性能优化,是结合了《Unity游戏优化》这本书和教程《Unity性能优化》第叁节——静态资源优化(3)——纹理的基础概念一起学习。在学习纹理优化部分时候遇到了问题,固定管线下Unity的Scene窗口有一个可视化Mipmap的渲染模式:
而这批Miscellaneous模式的选项在URP下相同位置没了:
又比较需要这个便捷的查看方法!于是搜一下想看看有没有出一些插件之类的,突然搜到了大概20年也有小伙伴提出这个问题:
Missing Scene view Draw Modes (Mipmaps, Overdraw, etc.) - Unity Forum
底下Unity技术人员回复了,大概意思就是会加入Rendering Debugger来实现之前固定管线下的D
rawMode的一些功能:
于是就发现了Rnedering Debugger功能,What's new in URP 12 (Unity 2021.2) | Universal RP | 12.0.0
之前URP下做东西的时候完全没有注意和使用过!一起来简单看看:
Rendering Debugger
非常方便了!比如Material的一些查看,可以单独看albedo的效果:
比如OverDraw:
曾经固定管线下的级联阴影、Alpha通道等的一键预览都有了!但是还是没有Mipmap。
其他版本的URP该怎么办?
小插曲,在Rendering Debugger没出来前,这位大佬Scene View Debug Modes in the Unity URP — John Austin甚至自己做了个插件:
虽然后续URP已经提供了rendering debugger,但是这个把shader添加进debug里的整个框架我们是可以参考过来的。
讨论Build-in下Scene视图的Mipmaps
其实Mipmap可视化已经有人总结过了:Re0-TA成长笔记 01关于Mipmap问题
而且asset store有免费的类似插件:Colored Mipmap Texture - Visualized Checker
还有其他的方法,要么方法我看不太懂,,要么颜色太多我感觉也不需要那么多颜色,只是想把Mipmap视图作为优化纹理的小工具。我希望在URP下也能实现固定管线那种简单的蓝/红色的Mipmap显示效果,实现方法尽量的简单一点,固定管线下就这样简单:
场景很简陋(之前学习入门精要的build-in项目~),也算体现了大概吧,当纹理大小刚刚好的时候展示的就是纹理的样子,纹理太小了精度不够就会是蓝色,纹理太大精度过大就会是红色!
因为比较好奇,build-in下的Scene视图的Mipmap到底是怎么运行的?物体shader里的贴图一定不只有一张主纹理,他的法线纹理、金属度等纹理都会参与Mipmap,这个大小是综合所有贴图判断的吗?还是只关注主要的albedo贴图?
我们测试一下,拿之前学习法线映射的shader为例,shader有两张纹理,albedo和normal,大小均为2048,显然都过大了,所以Mipmap视图下整体是红色:
现在把albedo贴图改为32,变成蓝色了!:
如果我们只改normal的大小为32,albedo那张不变呢?没有变化!还是红色:
这就初步说明,固定管线下这个Mipmap level判断依据,是根据shader中的MainTex(主纹理)来判断的,而且物体shader的SubShader的RenderType一定要有,才能参与这个Scene视图下Mipmap的红蓝判断:
还需要确定一点,就是这个Mipmap渲染的shader到底怎么获取纹理的?我们再测试一下,如果shader里如果主纹理命名不是默认的_MainTex,而是_BaseTex的话:
场景中之前的那个物体又变蓝了!
好了,到这里我们可以初步推断:
Build-in管线下,Scene View里的Mipmaps可视化,只针对shader主纹理命名为_MainTex的物体有效,且只关注_MainTex一张贴图的大小是否合适,其余的法线纹理等其他贴图不受关注。
探讨URP下Mipmap可视化方案
初步猜测:如果我只想要简单的红蓝检测效果,且实践方法尽量简单,我必须要在URP下能够访问所有项目的主纹理,然后单独做一个ViewDebug_Mipmaps的shader,挂在View视图窗口。
紧接着,找资料突然就看到了11年的时候就有人提出了一种可视化Mipmap方案:
A way to visualize mip levels · Aras' website (aras-p.info)
有意思的是在查看Build-in的Debug.hlsl文件时,发现Unity的可视化方案也是参考了之前的那个文章(Unity的Debug.hlsl文件):
上面那个人的主要shader如下:
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float2 mipuv : TEXCOORD1;
};
float2 mainTextureSize;
v2f vert (float4 vertex : POSITION, float2 uv : TEXCOORD0)
{
v2f o;
o.pos = mul (matrix_mvp, vertex);
o.uv = uv;
o.mipuv = uv * mainTextureSize / 8.0;
return o;
}
half4 frag (v2f i) : COLOR0
{
half4 col = tex2D (mainTexture, i.uv);
half4 mip = tex2D (mipColorsTexture, i.mipuv);
half4 res;
res.rgb = lerp (col.rgb, mip.rgb, mip.a);
res.a = col.a;
return res;
}
OHHHH,他的实现思路差不多就是我的意思!shader里的mainTextureSize,实际上就是URP下shader里我们能默认获取得到的BaseMap_TextureSize,其余的操作只是为了能根据距离动态判断纹理大小是否合适,根据判断结果给上红蓝色。
那我们就开始吧!首先是确定,怎么获取纹理?想起来了,可以从URP下自带的Rendering Debugger入手:
看看Rendering Debugger的逻辑
简单一点,我们打开URP的文件夹,搜索Debug
成功定位到Debugging3d.hlsl:
Debugging3D.hlsl
具体内容就不一句一句来了,单独挑一个,Material的:
你会发现!原来Debugger的逻辑并不是重新自己有一套shader,而是直接拿SurfaceData的东西用,输出想要的项就行了。
SurfaceData
之前扒URP的PBR Shader的时候就已经见过他了,SurfaceData是在SurfaceData.hlsl定义的结构体:
然后在LitInput.hlsl中定义了一个InitializeStandardLitSurfaceData()函数,去初始化SurfaceData:
尝试输出Surface.albedo
刚好我作为联系我有想要优化的项目,那就直接在项目中写个shader看看能不能成功提取SurfaceData.albedo吧!写个shader:
Shader "Debug/Debug Albedo"
{
SubShader
{
Tags{"RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline"}
Pass
{
HLSLPROGRAM
#pragma vertex LitPassVertex
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceData.hlsl"
#include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl"
#include "Packages/com.unity.render-pipelines.universal/Shaders/LitForwardPass.hlsl"
float4 frag(Varyings input) : SV_TARGET {
UNITY_SETUP_INSTANCE_ID(input);
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
#if defined(_PARALLAXMAP)
#if defined(REQUIRES_TANGENT_SPACE_VIEW_DIR_INTERPOLATOR)
half3 viewDirTS = input.viewDirTS;
#else
half3 viewDirTS = GetViewDirectionTangentSpace(input.tangentWS, input.normalWS, input.viewDirWS);
#endif
ApplyPerPixelDisplacement(viewDirTS, input.uv);
#endif
SurfaceData surfaceData;
InitializeStandardLitSurfaceData(input.uv, surfaceData);
return float4(surfaceData.albedo, 1);
}
ENDHLSL
}
}
}
参考上面自己Debug的框架,给他做成开关放在Scene下的DrawMode里:
结果,,,Scene是全黑的,并没有单独展示Albedo:
哪里出错了?想起来,不会是因为BaseMap命名和采样的问题吧。为了测试我的想法是否正确,场景中拖个正方体,给他一个简单的shader,shader严格按照BaseMap来:
同样的DrawMode选择Albedo,居然出现了!
果然,验证了我的猜想。检查一下项目里的shader,发现shader都是拿ASE实现的,,,转化的命名什么的都有点混乱,,,
再拿个之前搭的模型为例吧:
同样的Shader:
成功了!调出了albedo,啊,可视化的途径我们算是找到了,接下来就是实现了!
URP下实现Mipmap可视化
给texture每个mip层赋值
这其实是最关键的一步——我们需要有图可采样成颜色。
Tutorial: Creating and modifying custom mipmaps - Texture Tools - NVIDIA Developer Forums
我尝试过Unity创建纹理,也尝试过PS、GIMP生成dds图,但是这俩好像只能打开dds却不能编辑每一个Mip层的颜色!!!
于是另寻他路:dds的目的是给每个mipLevel给一个颜色值,那我们直接获取mipmap的level,根据这个level值输出对应的颜色不就好了,也就是shader里实现这行采样代码:
half4 mip = tex2D (mipColorsTexture, i.mipuv);
这里注意一下,tex2D的本质是什么?这是Unity提供的一个采样器,输入需要采样的纹理及对应的uv值,会进行采样,并根据纹理的设置生成mipmap。这里要关注另一个采样函数:tex2Dlod,这个是可以根据传入uv的.w分量信息指定mipmap层的。随便搜一下tex2Dlod的原理,就能明白了,比如这篇文章:MipMap的LOD实现原理 - 知乎里写的:
float mipmapLevel(float2 uv, float2 textureSize)
{
float dx = ddx(uv * textureSize.x);
float dy = ddy(uv * textureSize.y);
float d = max(dot(dx, dx), dot(dy, dy));
return 0.5 * log2(d);//0.5是技巧,本来是d的平方。
}
我直接在fragment shader里计算:
// 直接自己计算lod,参考tex2DLod
float2 mipUV = o.uv * o.mainTextureSize/ 8; // 参考文章的方案
float dx = ddx(mipUV); //
float dy = ddy(mipUV);
float px = 32 * dx;
float py = 32 * dy;
float lod = 0.5 * log2(max(dot(px, px), dot(py, py)));
再写个函数,根据传入的lod值lerp我的颜色,颜色取值来自上面的那篇文章:
// 根据当前纹理的Mip层值返回给定颜色
float4 GetCurMipColorByManualColor(float mipLevel)
{
if(mipLevel==0) // 纹理压根没有开启mipmap
{
return real4(0.0,0.0,1.0,1); // 给.a为0,即baseMap
}
else
{
if(mipLevel < 1 ) // 代表着纹理太小了,给个蓝色
{
return lerp(real4(0.0,0.0,1.0,1),real4(0.0,0.0,1.0,0.8),mipLevel);
}
else if (mipLevel <2)
{
return lerp(real4(0.0,0.0,1.0,0.8),real4(1,1,1,0), mipLevel-1);
}
else if(mipLevel <3) // mip的正正好,于是.a值给0,意味着纹理此时恰到好处
{
return lerp(real4(1,1,1,0),real4(1.0,0.7,0.0,0.2),mipLevel-2);
}
else if(mipLevel <4)
{
return lerp(real4(1.0,0.7,0.0,0.2),real4(1.0,0.3,0.0,0.6),mipLevel-3);
}
else if(mipLevel <5) // mip太多了吧,意味着纹理太大了!因此给个更红的颜色
{
return lerp(real4(1.0,0.3,0.0,0.6),real4(1.0,0.0,0.0,0.8),mipLevel-4);
}
else
{
return real4(1.0,0.0,0.0,0.8); // mip了超大,直接给正红色,.a直接点满,完全为debug的颜色
}
}
}
接着fragment shader里:
float3 baseColor = surfaceData.albedo;
float4 debugColor = GetCurMipColorByManualColor(lod);
float4 res;
res.rgb =lerp(baseColor.rgb, debugColor.rgb, debugColor.a); //由debugColor.a控制插值
res.a = 1;
return res;
就是说,到这里就已经实现了!具体怎么把他搬进Scene视图下,前面列举了一个URP下别人实现Albedo可视化的框架,我是直接拿过来用了,他给了源码,所以脚本这里就不过多的介绍。
效果展示
放上一个跟固定管线里的相同模型、贴图的场景的mipmap可视化对比,左边是固定管线下,右边是我实现的:
可以看出在显示上是有一定偏差的,因为Unity内部到底是怎么做mipmap可视化,颜色如何规定的?颜色和mip层的关系是怎样的?没在源码中找到太多的依据。
但基本上算是实现了!虽然shader用了很多个if,但是只是一个debug环节,不用太考虑效率问题。总的来说这个方案学习过程中自己使用起来还是足够的!
文章写的很潦草,涉及到的内容太扩散了,其实也是我实现过程中的心路历程,希望看到这里的你能看懂!!!!