对一个shader进行调试非常困难,这也是造成shader难写的原因之一,如果发现效果不对,我们可能要花非常多的时间来找到问题所在,shader中可以选择调试的方法非常有限,甚至简单输出都不行。
下面给出unity对unity shader的调试方法,主要包含两种。
假彩色图像指的是用假彩色技术生成的一种图像,与假彩色图像对应的是照片这种真彩色图像。一张假彩色图像可以用于可视化一些数据,如何用它对shader进行调试?
主要思想是我们可以把需要调试的变量映射到[0,1]之间,把他们作为颜色输出到屏幕上,然后通过屏幕上显示的像素颜色来判断这个值是否正确。这种方法得到的调试信息很模糊,能够得到的信息有限,但在很长的一段时间内,这种方法的确是唯一的可选方法。
需要注意的是,由于颜色的分量范围在[0,1],因此我们需要小心处理需要调试的变量的范围,如果我们已知它的值域范围,可以先把它映射到[0,1]之间在进行输出。如果不知道一个变量的范围,只能不停地实验。一个提示是,颜色分量中任何大于1的数值将被设置为1,而任何小于0的数值会被设置为0。因此我们可以尝试使用不同的映射,直到发现颜色发生了变化。(这意味着得到了0到1的值)。
如果要调试一个一维数据,可以选择一个单独的颜色分量(比如R分量)进行输出,其他颜色分量设置为0。如果是多维数据,可以选择对他的每一个分量单独调试,或者选择多个颜色分量进行输出。
下面使用一个实例,使用假彩色图像的方式可视化一些模型数据,如法线、切线、纹理、坐标、顶点颜色以及他们之间的运算结构等。代码如下:
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f {
float4 pos : SV_POSITION;
fixed4 color : COLOR0;
};
v2f vert(appdata_full v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
// Visualize normal
o.color = fixed4(v.normal * 0.5 + fixed3(0.5, 0.5, 0.5), 1.0);
// Visualize tangent
o.color = fixed4(v.tangent.xyz * 0.5 + fixed3(0.5, 0.5, 0.5), 1.0);
// Visualize binormal
fixed3 binormal = cross(v.normal, v.tangent.xyz) * v.tangent.w;
o.color = fixed4(binormal * 0.5 + fixed3(0.5, 0.5, 0.5), 1.0);
// Visualize the first set texcoord
o.color = fixed4(v.texcoord.xy, 0.0, 1.0);
// Visualize the second set texcoord
o.color = fixed4(v.texcoord1.xy, 0.0, 1.0);
// Visualize fractional part of the first set texcoord
o.color = frac(v.texcoord);
if (any(saturate(v.texcoord) - v.texcoord)) {
o.color.b = 0.5;
}
o.color.a = 1.0;
// Visualize fractional part of the second set texcoord
o.color = frac(v.texcoord1);
if (any(saturate(v.texcoord1) - v.texcoord1)) {
o.color.b = 0.5;
}
o.color.a = 1.0;
// Visualize vertex color
// o.color = v.color;
return o;
}
fixed4 frag(v2f i) : SV_Target {
return i.color;
}
ENDCG
}
}
上面的代码使用了一个unity内置的结构体——appdata_full,可以在UnityCG.cginc里查看该结构体的构成。它几乎包含所有的模型数据。
我们把计算得到的假彩色存储到了顶点着色器的输出结构体——v2f中的color变量,并在片元着色器输出了这个颜色。我们可以对其中的代码添加或取消注释,观察不同运算和数据得到的效果。
为了得到某点的颜色值,我们可以使用类似颜色拾取器的脚步得到屏幕上某点的RGBA值,从而推断出该点的调试信息。下面附上代码,把该脚步拖拽到一个摄像机上,单击运行用鼠标点击屏幕,以得到该点的颜色值。
using UnityEngine;
using System.Collections;
public class ColorPicker : MonoBehaviour {
public BoxCollider pickerCollider;
private bool m_grab;
private Camera m_camera;
private Texture2D m_screenRenderTexture;
private static Texture2D m_staticRectTexture;
private static GUIStyle m_staticRectStyle;
private static Vector3 m_pixelPosition = Vector3.zero;
private Color m_pickedColor = Color.white;
void Awake() {
// Get the Camera component
m_camera = GetComponent();
if (m_camera == null) {
Debug.LogError("You need to dray this script to a camera!");
return;
}
// Attach a BoxCollider to this camera
// In order to receive mouse events
if (pickerCollider == null) {
pickerCollider = gameObject.AddComponent();
// Make sure the collider is in the camera's frustum
pickerCollider.center = Vector3.zero;
pickerCollider.center += m_camera.transform.worldToLocalMatrix.MultiplyVector(m_camera.transform.forward) * (m_camera.nearClipPlane + 0.2f);
pickerCollider.size = new Vector3(Screen.width, Screen.height, 0.1f);
}
}
// Draw the color we picked
public static void GUIDrawRect( Rect position, Color color )
{
if( m_staticRectTexture == null )
{
m_staticRectTexture = new Texture2D(1, 1);
}
if( m_staticRectStyle == null )
{
m_staticRectStyle = new GUIStyle();
}
m_staticRectTexture.SetPixel(0, 0, color);
m_staticRectTexture.Apply();
m_staticRectStyle.normal.background = m_staticRectTexture;
GUI.Box(position, GUIContent.none, m_staticRectStyle);
}
// OnPostRender is called after a camera has finished rendering the scene.
// This message is sent to all scripts attached to the camera.
// Use it to grab the screen
// Note: grabing is a expensive operation
void OnPostRender() {
if (m_grab) {
m_screenRenderTexture = new Texture2D(Screen.width, Screen.height);
m_screenRenderTexture.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0);
m_screenRenderTexture.Apply();
m_pickedColor = m_screenRenderTexture.GetPixel(Mathf.FloorToInt(m_pixelPosition.x), Mathf.FloorToInt(m_pixelPosition.y));
m_grab = false;
}
}
void OnMouseDown() {
m_grab = true;
// Record the mouse position to pick pixel
m_pixelPosition = Input.mousePosition;
}
void OnGUI() {
GUI.Box(new Rect(0, 0, 120, 200), "Color Picker");
GUIDrawRect(new Rect(20, 30, 80, 80), m_pickedColor);
GUI.Label(new Rect(10, 120, 100, 20), "R: " + System.Math.Round((double)m_pickedColor.r, 4) + "\t(" + Mathf.FloorToInt(m_pickedColor.r * 255)+ ")");
GUI.Label(new Rect(10, 140, 100, 20), "G: " + System.Math.Round((double)m_pickedColor.g, 4) + "\t(" + Mathf.FloorToInt(m_pickedColor.g * 255)+ ")");
GUI.Label(new Rect(10, 160, 100, 20), "B: " + System.Math.Round((double)m_pickedColor.b, 4) + "\t(" + Mathf.FloorToInt(m_pickedColor.b * 255)+ ")");
GUI.Label(new Rect(10, 180, 100, 20), "A: " + System.Math.Round((double)m_pickedColor.a, 4) + "\t(" + Mathf.FloorToInt(m_pickedColor.a * 255)+ ")");
}
}
效果图:
Visual Studio作为Windows系统下的开发利器,在Visual Studio 2012版本中也提供了对unity shader的调试功能——Graphics Debugger。
通过Graphics Debugger,我们不仅可以查看每个像素的最终颜色、位置信息,还可以对顶点着色器和片元着色器进行单步调试。具体安装和使用方式可以看unity官网文档中使用Visual Studio对DirectX 11的shader进行调试一文。
当然本方法也有一些限制。需要保证unity运行在DirectX 11平台上,而且Graphics Debugger本身存在一些bug。但这已经是Windows用户的福音了。
我们可以使用它来看到游戏图像的某一帧是如何一步步渲染出来的。
帧调试器可以用于查看渲染该帧时进行的各种渲染事件,这些事件包含了Draw Call系列,也包括了类似清空帧缓存等操作。单机某个事件在右侧窗口中就会显示出该事件的细节,例如几何图形的细节以及使用了哪个shader等。同时在Game视图也可以看到它的效果。
unity5提供的帧调试器实际上并没有实现一个真正的帧拾取功能,而是仅仅使用停止渲染的方法来查看渲染事件的结果。例如我们想看第四个draw call的结果,那么帧调试器就会在第4个draw call调用完毕后停止渲染,这种方法虽然简单,但得到的信息很有限。如果想获取更多信息,还需要使用外部功能,例如Visual Studio插件,或者Intel GPA、NVIDIA NSight、AMD GPU PerfStudio、RenderDoc等工具。