一点心得


  • 首页

  • 归档

Unity游戏内存回收的优化(译)

发表于 2017-11-18

原文Optimizing garbage collection in Unity games。这里对这篇文章的粗略翻译,当作自己的笔记。

Garbage Collector在这里被翻译成名词GC。对于Garbage Collection这里翻译成GC内存回收。

内存垃圾: 代码中销毁了(disposed)但是GC还没有清理的内存。

Unity的托管内存的简单介绍

为了理解GC(本文GC指代Garbage Collector)在内存分配和回收时是怎样工作的,我们必须首先了解Unity的引擎代码和我们自己编写的脚本是怎么利用内存来工作的。

Unity引擎代码运行时管理内存的方式叫做手动管理内存(manual memory management)。也就是说引擎代码必须显示的声明内存是怎么使用的。手动管理内存没有用到GC,这部分内容本文不做介绍。

当运行我们自己编写的脚本时Unity管理这部分内存的方式叫做自动内存管理(automatic memory management).这意味这我们自己编写的代码不需要很详细的去告诉Unity怎样去管理这部分内存。Unity都帮我们做好了。

简而言之,Unity的自动管理内存工作方式如下:

  • Unity有两个可以访问的内存池:栈(stack)和堆 (heap)(也被叫做托管堆。栈内存被用在短期存储一些小片段的数据,堆则用来存储一些长期并且比较大的片段数据)。
  • 当一个变量创建好了,Unity会在栈或者堆中申请一个内存块。
  • 只要这个创建的变量在作用范围之内(我们的代码一直可以访问到它),为它分配的相应的那块内存就一直是保持可用状态。我们把这块内存成为已经被分配的(allocated)。我们把在栈中分配内存的变量叫做栈上的对象,把内存分配在堆上的变量成为堆上的对象。
  • 当这个变量已经脱离了它的作用区域,相应的这个变量所分配的内存也就不再需要了,这块内存随之就会把返回给分配时的那个内存池。我们称这块内存叫做已回收(deallocated)。栈上的内存一旦离开了作用区域就会立即被回收。如果是堆上的内存即便是它的引用已经超出了作用区域也不会被立即回收,它还会保持它的分配状态。
  • GC会标记并且回收堆上没有被使用的内存。它会周期性的对堆上的内存进行清理。

现在我们了解了内存使用的流程,让我们来更深入的来了解栈和堆上的内存的分配和释放。

阅读全文 »

Quaternion的插值分析及总结

发表于 2017-08-30

下面的内容是阅读3D数学基础后结合自己的理解的总结

Unity中应用

我们接触到的地方就是Unity的Transform组件。Transform组件维护了四元数实现方位角位的变换。但是我们也看到Transform组件提供了eulerAgnles属性,按理说里面也实现了欧拉角的,但事实并不是这样。接下来详细分析下

首先看看类似与这种操作,我们对一个GameObject实现旋转的时候,可以直接对其transform组件的eulerAngles属性赋值需要旋转的角度即可。

1
transform.eulerAngles = Vector3.zero;

这个是transform的eulerAngles的实现,可以看到其实里面直接把欧拉角转换成了四元数来处理,所以欧拉角其实只是作为一个方法的表达形式,并没有作为Transform的成员变量之类的属性来维护 (实际上也不需要,因为有了四元数之后,欧拉角和四元数就可以实现相互的转换)

1
2
3
4
5
6
7
8
void Transform::SetLocalEulerAngles (const Vector3f& eulerAngles)
{
    ABORT_INVALID_VECTOR3 (eulerAngles, localEulerAngles, transform)
     
    SetLocalRotationSafe (EulerToQuaternion (eulerAngles * Deg2Rad (1)));
  
    ...
}

我们想对一个GameObject实现旋转还可以对其的transform组件的rotation属性以四元素的方式赋值,来实现旋转。

1
transform.rotation = Quaternion.identity;

对于rotation属性,其具体实现是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void Transform::SetRotation (const Quaternionf& q)
{
    ...
    
    if (father != NULL)
        SetLocalRotation (Inverse (father->GetRotation ()) * q);
    else
        SetLocalRotation (q);
}

void Transform::SetLocalRotation (const Quaternionf& q)
{
    ABORT_INVALID_QUATERNION (q, localRotation, transform);
    m_LocalRotation = q;
    
   ...
}

可以看到标红色的那行,传入的四元数被赋值参数被赋值给了m_LocalRotation成员变量。

上面介绍了在Unity里面封装的Quaternion的方位与角位的情况。那么下面Quaternion里面常用到的一个叫做Lerp的方法。相应的这个方法有一个 对应的Slerp方法。那么这两个方法到底有什么区别呢?下面来看看。

线性插值和球面线性插值的原理

Lerp是用来求两个目标之前的差值,其表达式为Lerp(a, b, t)。a和b分别为起始和终点两个点,t为差值参数变量,范围在0到1之间。Lerp表示为标准的线性差值公式的话,其公式为:

1
Lerp(a, b, t) = a + (b - a) * t

代码实现着这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private Quaternion LerpX (Quaternion a, Quaternion b, float t)
{
    Quaternion result;
 
    //线性的
    float k0 = 1.0f - t;
    float k1 = t;
 
    //插值
    result.w = a.w * k0 + b.w * k1;
    result.x = a.x * k0 + b.x * k1;
    result.y = a.y * k0 + b.y * k1;
    result.z = a.z * k0 + b.z * k1;
 
    return result;
}

Slerp的计算方式(里面数学公式编辑比较麻烦,干脆直接写下来上图片了):

得出了推导公式,那么代码实现就比较简单了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
 private Quaternion SlerpX(Quaternion a, Quaternion b, float t)
 {
        Quaternion result;
 
        //利用点乘计算两个四元素夹角的cos值
        //
        //   q1 * q2 = [w1 v1] * [w2 v2]
        //
        //           = [w1 (x1 y1 z1)] * [w2 (x2 y2 z2)]
        //
        //           = w1*w2 + x1*x2 + y1*y2 + z1*z2
 
        // 这个可以参考向量点积: 两个向量的点积等于两个向量的模乘以两个向量的夹角
        // a * b = ||a|| ||b|| cosθ
        // 如果 a和b是单位向量
        // 那么 a * b = cosθ
 
        //这里a,b是单位四元数
        //   q   = [cos(θ/2) sin(θ/2)n]
        // ||q|| = sqrt(cos(θ/2)^2 + (sin(θ/2)n)^2)
        // 如果n为单位向量,则:
        //       = sqrt(cos(θ/2)^2 + (sin(θ/2)^2)
        //       = 1
        float cosOmega = a.w * b.w + a.x * b.x + a.y * b.y + a.z * b.z;
 
        if (cosOmega < 0.0f)
        {
            result.w = -a.w;
            result.x = -a.x;
            result.y = -a.y;
            result.z = -a.z;
 
            cosOmega = -cosOmega;
        }
 
        float k0, k1;
 
        if (cosOmega > 0.9999f) //线性计算
        { //Unity中为0.95f
            k0 = 1.0f - t;
            k1 = t;
        }
        else //球面平滑的
        {
            // 用三角公式 sin^2(omega) + cos^2(omega) = 1求得
            float sinOmega = Mathf.Sqrt(1.0f - cosOmega * cosOmega);
 
            // tan(omega) = sin(omega) / cos(omega)
            // omega = atan(sin(omega) / cos(omega)
            float omega = Mathf.Atan2(sinOmega, cosOmega);
 
            // 计算 1 / sin(omega)
            float oneOverSinOmega = 1.0f / sinOmega;
 
            k0 = Mathf.Sin((1.0f - t) * omega) * oneOverSinOmega;
 
            k1 = Mathf.Sin(t * omega) * oneOverSinOmega;
        }
 
        //插值
        result.w = a.w * k0 + b.w * k1;
        result.x = a.x * k0 + b.x * k1;
        result.y = a.y * k0 + b.y * k1;
        result.z = a.z * k0 + b.z * k1;
 
        return result;
 }

两种方式比较: Lerp更少的计算量,Slerp更加平滑。实际测试下使用Slerp和Lerp的运动效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
private const int mMaxDotCount = 20;
private int mCurDotCount = 0;
private float mStep = 0.05f;
  
void Start ()
{
    StartCoroutine (Loop ());
}
  
IEnumerator Loop ()
{
    Quaternion slerpStartRoation = SlerpCapsule.rotation;
    Quaternion lerpStartRotation = LerpCapsule.rotation;
  
    while (mCurDotCount < mMaxDotCount) { //只计算mMaxDotCount次
  
        //平滑表现测试
        Quaternion newSlerpRotation = SlerpX (slerpStartRoation, Targeter.rotation, mStep);
        Quaternion newLerpRotation = LerpX (lerpStartRotation, Targeter.rotation, mStep);
  
        mStep += 0.05f;
  
        GameObject lerpDot = Instantiate (LerpDotTemplate);
  
        lerpDot.transform.SetParent (null);
        lerpDot.transform.localPosition = newLerpRotation.eulerAngles;
  
        GameObject slerpDot = Instantiate (SlerpDotTemplate);
  
        slerpDot.transform.SetParent (null);
        slerpDot.transform.localPosition = newSlerpRotation.eulerAngles;
  
        SlerpCapsule.rotation = newSlerpRotation;
        LerpCapsule.rotation = newLerpRotation;

    mCurDotCount++;
              
        yield return new WaitForSeconds (0.02f);
    }
}

通过一个个的小球,我把每次Slerp和Lerp的插值画出来,形成了一个轨迹(其中红色是Slerp的插值轨迹,白色的是Lerp的插值轨迹)

通过这个轨迹图片可以看得到(由于各个小球的Z周有一点差异,导致画面有一点透视效果),代码Slerp的红色小球的轨迹相邻的之间距离比较均匀,但是代表Lerp的白色的小球两球之前的距离由最开始逐渐变小然后到达中间之后又开始变大。分析之后可以归结到下面两张图片中(左图描述Lerp,右图描述Slerp)。Lerp求得的是四元数在圆上的弦上的等分,而Slerp求得的是四元数载圆上的圆弧的等分(论据的图是参考的这里)。

项目中的使用问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private Quaternion _sphereOriginRotation;
private readonly float _rotationFactor = 0.2f;
 
void Start()
{
    _sphereOriginRotation = Sphere.rotation;
}
 
void Update()
{
    //这样用其实没有用上Slerp的球面线性差值的特性
    Sphere.rotation = Quaternion.Slerp(Sphere.rotation, Target.rotation, _rotationFactor);
 
    //上面这么写的话,其实和用Lerp得到的效果基本一样。但是Lerp的计算效率会高很多
    Sphere.rotation = Quaternion.Lerp(Sphere.rotation, Target.rotation, _rotationFactor);
         
    //要用到Slerp的球面差值的特性可以这样写,改变目标旋转的物体之前,先保存这个物体的Rotation
    Sphere.rotation = Quaternion.Slerp(_sphereOriginRotation, Target.rotation, _rotationFactor);
}
阅读全文 »

Unity中shader代码的条件分支的性能问题

发表于 2017-08-20

优化方案

GPU对条件分支语句的执行性能比较敏感似乎是一个比较普遍的认知了,那么到底影响有多大呢?这里做了一次测试,测试针对Shader中的条件分支语句的性能情况。在有if语句和没有if语句时候的程序不同写法的:

针对if语句的优化方法,参考的这里, 目前没有找到更好的替换if语句的办法,希望有更好办法的同学评论里面提出来,非常感谢!

1
2
3
4
5
6
7
8
9
10
11
12
13
fixed4 col = tex2D(_MainTex, i.uv);
fixed4 col2 = tex2D(_SubTex, i.uv);
 
//1. if语句写法
if (col.r < 0.1)
{
	col.r = col2.r*0.1;
}
 
//2. 对if优化之后的写法
fixed r_factor = max(0, sign(col.r - 0.1));
col.r = r_factor * col.r + (1 - r_factor) * col2.r*0.1; 
 

为了放大测试效果,我在测试的shader里面添加了30个if语句,对应的在替换if语句计算的公式也添加了30个。在空场景上渲染500个14.4k面的模型,并且对每个模型的material更改了颜色,保证500个模型渲染不batching,没有关照及其他因素影响。

测试环境: macOS 10.12.6,  unity 5.6.3f1,  xcode 8.3.3, iOS 10.3.3,  iPhone7 测试的结果主要针对iphone7的设备,其他设备测试结果可能不同,和GPU的型号直接关系。

结果验证

测试场景

测试结果

  1. 没有if的测试情况
程序执行1min 程序执行5min
FPS: 11 FPS: 7
CPU: 94ms ~105ms CPU: 140ms  ~ 144ms
GPU: 65ms  ~  71ms GPU: 90ms  ~ 98ms

5min时刻的性能截图:

  1. 存在if语句的测试情况
程序执行1min 程序执行5min
FPS: 11 FPS: 7
CPU: 90ms ~ 98ms CPU: 138ms ~160ms
GPU: 66ms ~ 71ms GPU: 90ms  ~  101ms

5min时刻的性能截图:

总结: 通过测试发现两种写法的shader性能基本相近, 数值一直在波动,但是波动范围和数值频率差别比较微小, 没有哪种情况能够从数值上面看出绝对的优势。也就是相比使用if语句的代码,优化之后的代码并没有执行效率的明显提升,所以这里所谓的优化就不存在了。

阅读全文 »

Unity3D Sprite常用存储格式性能对比

发表于 2017-07-14

UI图片资源对于项目里面内存的占用还是包大小的比例都是很大一部分,这里对带Alpha的图片的几种常用格式做了下对比,便于找出最好的解决方案应用到项目中去

测试环境: Unity5.5.4f1, Window7, 魅蓝Note1

在网上找了8张512x512的PNG图片,分别测试了RGBA32、RGBA16和ETC1+Alpha(这种格式主要是考虑Android平台)三种格式在魅蓝Note1上的显示效果、AB的大小和加载时间(测试了5次求的平均值)。

格式 AB大小 加载时间
RGBA32 17.5MB 545ms
RGBA16 3.01MB 326ms
ETC1+Alpha 3.19MB 385ms

对于显示效果图,可以到这里下载

通过上面对比,可以看到ETC1+Alpha这种格式的显示效果和RGBA32的相差不是很大,但是AB的大小和加载时间确实和RGBA16的比较接近。至于RGBA16虽然AB的大小和加载速度很有优势,但是无奈显示效果和RGBA32相差太大。可以考虑使用这种格式应用到项目中来降低包大小和提高加载速度。

阅读全文 »

Unity模型渲染层级顺序问题

发表于 2017-07-08

问题描述

项目中遇到一个问题,在Untiy中如果关闭ZTest的话,模型的子部件之间的层级可能会出现错乱的现象,模型是3ds max导出的模型。

原因思考

既然是层级错误,根据Unity渲染管线的规则决定层级的有两个地方。一个是传入Unity的顶点数据的顺序(如果用了顶点数据索引,那么就是索引的顺序),另外一个是Unity里面的DepthTest功能决定。那么两种情况的规则是怎样的呢?

Unity中的shader有一个RenderQueue设置,使用设置了同一个RendererQueue的值的shader模型在同一批被渲染,比如RenderQueue设置了一般不透明物体设置为Geometry(对应的数值为2000),透明物体物体设置为Transparent(对应的数值为3000)。Geometry的对象会先渲染,然后才会渲染Transparent的物体。本文我遇到的问题的模型都是用的同一个shader,所以RendererQueue不是问题的原因。

首先看DepthTest的作用,在Unity中利用了深度缓冲来保证了渲染的层级问题,深度缓冲是根据RenderQueue和深度值(顶点的Z值)来确定的。渲染管线在光栅化之后会对顶点数据做一次DepthTest 。

对于顶点数据的顺序的影响也是经过这个问题之后我才发现的。在DepthTest打开的情况下(Shader里面ZWrite&ZTest开启),这个顺序也没有什么作用的,但是当我们关闭DepthTest的时候(比如渲染半透明的物体需要进行混合操作),Unity的渲染就直接根据这个数据的顺序去渲染了。

基于上面两种情况的分析,通过测试发现我们的模型在打开DepthTest的时候是没有层级的问题的,关闭DepthTest之后才出现层级混乱的问题。这样我们就把问题定位到原因了。那么接下来验证一下原因

问题解决

首先我直接在Unity中通过代码生成一个Mesh来渲染,目的是为了可以方便的更改顶点数据的顺序。(创建Mesh的代码参考了ProceduralMesh,根据自己测试需求做了一些改动)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

public class CreateMesh : MonoBehaviour {

    public bool UseRightTrianglesOrder;
    private MeshFilter m_MeshFilter;

    private void Start()
    {
        gameObject.AddComponent<MeshRenderer>();
        m_MeshFilter = gameObject.AddComponent<MeshFilter>();
        m_MeshFilter.mesh = new Mesh();

        BuildMesh();
    }

    public void OnGUI()
    {
        if(GUI.Button(new Rect(20, 20, 200, 80), "ReBuild"))
        {
            BuildMesh();
        }
    }

    public void BuildMesh()
    {
        Vector3 p0 = new Vector3(0, 0, 0);
        Vector3 p1 = new Vector3(1, 0, 0);
        Vector3 p2 = new Vector3(0.5f, 0, Mathf.Sqrt(0.75f));
        Vector3 p3 = new Vector3(0.5f, Mathf.Sqrt(0.75f), Mathf.Sqrt(0.75f) / 3);

        Mesh mesh = m_MeshFilter.sharedMesh;
        mesh.Clear();

        mesh.vertices = new Vector3[]{
            p0,p1,p2,
            p0,p2,p3,
            p2,p1,p3,
            p0,p3,p1
        };

        if (UseRightTrianglesOrder)
        {
            //这是正确的顺序
            mesh.triangles = new int[]{
                9,10,11, //蓝色的那个三角形
                0,1,2,
                3,4,5,
                6,7,8
            };
        }
        else
        {
            //这是错误的顺序
            mesh.triangles = new int[]{
                0,1,2,
                3,4,5,
                6,7,8,
                9,10,11 //蓝色的那个三角形,如果ZTest关闭,那么这个三角形在最后被渲染,会覆盖前面的三个三角形部分区域
            };
        }

        Vector2 uv3a = new Vector2(0, 0);
        Vector2 uv1 = new Vector2(0.5f, 0);
        Vector2 uv0 = new Vector2(0.25f, Mathf.Sqrt(0.75f) / 2);
        Vector2 uv2 = new Vector2(0.75f, Mathf.Sqrt(0.75f) / 2);
        Vector2 uv3b = new Vector2(0.5f, Mathf.Sqrt(0.75f));
        Vector2 uv3c = new Vector2(1, 0);

        mesh.uv = new Vector2[]{
            uv0,uv1,uv2,
            uv0,uv2,uv3b,
            uv0,uv1,uv3a,
            uv1,uv2,uv3c
        };

        mesh.RecalculateNormals();
        mesh.RecalculateBounds();
    }
}

在使用正确的triangles的数据并且关闭ZTest的时候,显示的效果如下:

这种效果是正确的。在使用乱序的triangles的索引并且关闭ZTest的时候,显示效果如下:

这看起来很怪异,再看看3D空间视图里面的顶部视图:

通过这个图我们可以看到,其实乱序的triangles数据渲染出来的效果是因为蓝色的那个三角形被渲染在前面了,然而他本身的位置是在红色和绿色的后面的。说明他比红色和绿色的面片要后渲染出来。

上面的两个测试证明了在关闭ZTest的时候,Unity的渲染顺序其实就是根据triangles数据的顺序来渲染的。如果我们开启ZTest,那么上面两个triangles数据渲染出来的模型我们会发现都是没有问题的。现在证明了是triangles顺序的问题但是我们模型的triangles的数据来自于3dsMax,说明这应该是在3dsMax导出模型的一些设置问题,或者说是模型本身有问题。

3DS Max导出模型的问题

现在问题定位到模型导出的问题,由于没用过3dsMax。为了测试需要我直接摸索创建了一个包含3个Cube的模型,然后合并三个模型的Mesh。导出FBX之后再在Unity观察三个Cube的层级关系,结果重现了问题模型的问题。那么到底是哪一步出错了呢?经过无数次的测试,最终发现了triangles顺序是由合并Mesh的顺序决定的。如果想得到triangles数据的顺序正好是对应顶点的层级由低到高的顺序,那么在3dsMax中在合并Mesh(Attach子模型)的时候需要从层级最低的模型逐个Attach到层级最高的模型。

最后测试的Shader(其实就是官方给的Unlit-Alpha改了一点点)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// Unlit alpha-blended shader.
// - no lighting
// - no lightmap support
// - no per-material color

Shader "Custom/TestRenderOrder" {
Properties {
	_MainTex ("Base (RGB) Trans (A)", 2D) = "white" {}
}

SubShader {
	Tags {"RenderType"="Opaque"}
	LOD 100

	//ZWrite Off
	ZTest Off
	Cull Off

	Pass {  
		CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma target 2.0
			#pragma multi_compile_fog

			#include "UnityCG.cginc"

			struct appdata_t {
				float4 vertex : POSITION;
				float2 texcoord : TEXCOORD0;
				UNITY_VERTEX_INPUT_INSTANCE_ID
			};

			struct v2f {
				float4 vertex : SV_POSITION;
				float2 texcoord : TEXCOORD0;
				UNITY_FOG_COORDS(1)
				UNITY_VERTEX_OUTPUT_STEREO
			};

			sampler2D _MainTex;
			float4 _MainTex_ST;

			v2f vert (appdata_t v)
			{
				v2f o;
				UNITY_SETUP_INSTANCE_ID(v);
				UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
				UNITY_TRANSFER_FOG(o,o.vertex);
				return o;
			}

			fixed4 frag (v2f i) : SV_Target
			{
				fixed4 col = tex2D(_MainTex, i.texcoord);
				UNITY_APPLY_FOG(i.fogCoord, col);
				return col;
			}
		ENDCG
	}
}
}
阅读全文 »
1 … 5 6 7 … 9
© 2025 yiliangduan@qq.com