很久很久以前,做过一个离线Mesh切割方式的Decay效果Unity3D中的贴花效果 适合场景景观布置,批次合并等,但运行时性能较差,这次我们来玩玩运行时投影器。


先上成平图


测试效果图, 图中的裤子上投影了一个眼睛
那么投影的原理是什么呢。。。 那么请看下面这张

这张图左下角就是投影器看到的景象,投影贴图“眼睛” 充满了整个投影器的视野,那么原理就呼之而出了。
在正常渲染裤子的顶点时,顺便变换到投影器的屏幕空间,然后再渲染裤子的片段处理函数中将位于投影器屏幕空间的像素都换成眼睛即可。

渲染裤子的Shader

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
Shader "Unlit/ProjectorShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
           
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 projectorUV : TEXCOORD1;
                float4 vertex : SV_POSITION;
            };

            float4x4 _ProjectorP;
            float4x4 _ProjectorV;
            float4x4 _ProjectorVP;
            sampler2D _ProjectorTex;
            sampler2D _ProjectorFallOut;
            sampler2D _MainTex;
            float4 _MainTex_ST;
           
            v2f vert (appdata v)
            {
                v2f o;

                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);

                float4x4 propMVP = mul(_ProjectorVP, unity_ObjectToWorld);
                float4 projProjPos = mul(propMVP, v.vertex);

                projProjPos = ComputeScreenPos(projProjPos);
                o.projectorUV = projProjPos;

                return o;
            }
           
            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
               
                fixed4 projectorCol = tex2Dproj(_ProjectorTex, i.projectorUV);

                // tex2Dproj = xyz/ w
                fixed4 projectorFallOutCol = tex2Dproj(_ProjectorFallOut, i.projectorUV);

                projectorCol *= projectorFallOutCol;

                col.rgb += projectorCol.rgb;


                return col;
            }
            ENDCG
        }
    }
}

自定义投影器的CS脚本

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[ExecuteInEditMode]
public class CustomProjector : MonoBehaviour {
    public Camera ProjectorCam;
    public Texture2D Tex;
    public Texture2D FallOut;
    public Material Mat;

    private void Start()
    {
       
    }

    private void Update()
    {
        Matrix4x4 P = GL.GetGPUProjectionMatrix(ProjectorCam.projectionMatrix, false);
        Matrix4x4 V = ProjectorCam.worldToCameraMatrix;
        Mat.SetMatrix("_ProjectorV", V);
        Mat.SetMatrix("_ProjectorP", P);
        Mat.SetMatrix("_ProjectorVP", P * V);
        Mat.SetTexture("_ProjectorTex", Tex);
        Mat.SetTexture("_ProjectorFallOut", FallOut);
    }
}

范例中的代码借用了Unity的相机,实际上并不需要相机,仅仅是借用了相机的投影矩阵和世界空间矩阵而已。

很多项目组在制作移动端游戏时,都使用Projector来制作主角的投影,虽然比起ShadowMap是优化了许多,但是实际上只要和Projector碰撞到物件其DC 都会翻倍, 对于我来说,这还是不可接受的。
而使用上面范例的代码,可以让DC不翻倍,但是并不通用, 因为受Projector影响的物体都需要定制Shader.

Unity自带Projector会翻倍的原因主要也是通用性,跨平台,使用方便, 因此它的原理是
1.找到所有和Projector有碰撞的MeshRenderer
2.使用Projector的材质球,将MeshRenderer的顶点再渲染一遍,并贴图
也因此被投影的物体无法触发动态合批

那么问题来,有没有一种方案,既可以保证通用性,不需要定制被投影目标的Shader,又可以使DC不翻倍呢? 答案是:有的, 但是有代价
代价1:需要使用深度图
代价2:DC不会翻倍,但是总共的DC为,被投影物体数量 + 1. 既物体自带的DC + 1 * (Projector数量) 其实代价2根本不算个事

说搞就搞
1.开启相机的深度渲染
Camera.main.depthTextureMode |= DepthTextureMode.Depth;
2.创建一个表示投影器范围的网格,我搞了个Cube Mesh
3.创建Cube Mesh对应的相关矩阵,因为是Cube 因此创建的投影为正交投影, 当然,如果也可以使用透视投影。

1
2
3
4
5
6
7
8
9
10
11
12
13
BoxCollider collider = this.GetComponent<BoxCollider>();
        this.m_size = collider.size.x / 2;
        this.m_nearClip = -collider.size.x / 2;
        this.m_farClip = collider.size.x / 2;
        this.m_aspect = 1;

        Matrix4x4 projector = default(Matrix4x4);
        projector = Matrix4x4.Ortho(-m_aspect * m_size, m_aspect * m_size, -m_size, m_size, m_nearClip, m_farClip);

        m_worldToProjector = projector * this.transform.worldToLocalMatrix;

        MeshRenderer mr = this.GetComponent<MeshRenderer>();
        mr.sharedMaterial.SetMatrix("_WorldToProjector", m_worldToProjector);

好了,准备工作做完了。开始渲染吧。
首先是将投影器覆盖的区域,采样出当前屏幕空间的深度,类似这样的效果

要实现这样的效果,就是讲顶点变换到投影平面,并将坐标变换到UV值域下
大概这样

1
2
3
4
5
6
7
vert part
o.screenPos = ComputeScreenPos(o.vertex);

fragment part
fixed4 screenPos = i.screenPos;
screenPos.xy = screenPos.xy / screenPos.w;
float depth = tex2D(_CameraDepthTexture, screenPos).r;

好了,现在我们有了深度,下一部就是讲当前像素的深度还原回该深度对应的世界坐标了
只需要两部矩阵变换
1.从屏幕空间变换到相机空间 unity_CameraInvProjection
2.从相机空间变换到世界空间 unity_MatrixInvV

有了世界坐标后,就可以将该坐标变换到Projector的控件,就是准备工作中的 _WorldToProjector
变换到Projector空间后,还记得范例上的投影器的全部视野就是需要投射的贴图范围吗?因此这里要做UV值域的变换

1
2
3
4
5
6
7
8
//变换到自定义投影器投影空间
fixed4 projectorPos = mul(_WorldToProjector, worldSpacePos);
projectorPos /= projectorPos.w;

fixed2 projUV = projectorPos.xy * 0.5 + 0.5;  //变换到uv坐标系
fixed4 col = tex2D(_ProjectorTex, projUV);  //采样投影贴图
fixed4 mask = tex2D(_ProjectorTexMask, projUV); //采样遮罩贴图
col.rgb =  lerp(fixed3(1, 1, 1), col.rgb, (1 - mask.r)); //融合

大功告成! 你可能会好奇,为什么多了一个遮罩贴图? 虽然你讲投影器视野内的像素部分都贴了投影贴图,但是是野外的像素怎么办?这个时候就需要遮罩图抹掉,因此遮罩图的纹理设置要设置为Clamp,保证边缘像素为拉伸且外侧的Alpha为0

项目完整源码:
https://github.com/dreamfairy/Unity-CubeProjector

一个使用ECS框架,制定Agent移动, 最后使用倒播功能还原路径的功能演示。

20180502 Unity 2018.1 正式版出来了, 带来了内置的ECS和Job System。 在UnityECS中, Entity对应的是GameObject, Component对应的是MonoBehavior, 而System对应的是ComponentSystem。
不过在写这篇文章的测试demo时使用的还是GitHub上的版本, 两者之间仅有API的名称上的区别。
GitHub地址:https://github.com/sschmid/Entitas-CSharp

安装:
版本 1.52
https://github.com/sschmid/Entitas-CSharp/releases

1.使用已编译的版本 https://github.com/sschmid/Entitas-CSharp/releases/download/1.5.2/Entitas.zip
直接将文件解压到Assets目录下即可

2.使用源码安装 git clone https://github.com/sschmid/Entitas-CSharp.git
1.将Libraries/Dependencies 里的文件全部拷贝到 Plugins 目录下
2.将Entitas 和 Addones 目录拷贝到Assets 目录下
3.随便打开一个C#文件,让Unity生成项目文件后,就可以在工具栏启动 Tools/Entitas/Perfererces
4.如果在第三步出错了,那是因为源码文件中 EntitasResources.cs 取资源目录里的文件Version.txt 失败了, 这是由于Unity生成的dll文件不会包含资源,因此要么修改这个函数直接返回版本号(不取Version),要么直接使用官方已经编译好的Entitas.dll

ECS架构概述

ECS架构看起来就是这样子的。先有个World,它是系统(译注,这里的系统指的是ECS中的S,不是一般意义上的系统,为了方便阅读,下文统称System)和实体(Entity)的集合。而实体就是一个ID,这个ID对应了组件(Component)的集合。组件用来存储游戏状态并且没有任何的行为(Behavior)。System有行为但是没有状态。

这听起来可能挺让人惊讶的,因为组件没有函数而System没有任何字段。

from:http://gad.qq.com/article/detail/28682

ECS框架其实在许多年前就已经诞生了,这几年名声大噪源于GDC2017守望先锋的一次技术分享,ECS的理念是组合模式优于面向对象模式,ECS解决的问题是之前OOP框架开发时状态和行为混合,在对象功能非常庞大时维护的成本很高。
ECS的全称是 Entity , Component, System. 其中Entity是对象实体, Component维护了对象的所有状态, 而System则是利用对象身上的Compoent实现各种行为, 在实际编程发现这种编程方式非常类似行为树的构架,开发过行为树逻辑的同学非常熟悉开发模式就是讲各种逻辑进行拆分,分散一个个处理细节的函数,使这些函数可以被行为树任意组合复用。

C#版的ECS利用了许多C#的特性,比如分散类 partical. ECS中Component 实际上是把OOP中的成员属性变成一个个单独的partical class进行拆分,但是本质上组合后还是属于他的成员。
举个例子, 在OOP中一个玩家包含了生命和移动速度的属性 Like this

1
2
3
4
public class Player {
    public uint HP;
    public uint MoveSpeed;
}

而在ECS构架中这个类会变成这样

1
2
3
4
5
6
7
8
9
10
HPComponent.cs

public partial class Player{
    public HPComponent hpComponent;
}

MoveSpeedComponent.cs
public partial class Player{
    public MoveSpeedComponent moveSpeedComponent
}

这里我为什么要把类名标识出来, 这是因为对于程序员开发来说,当你要修改Player的某个属性时,只要关注属性类即可, 不需要去Player.cs中大海捞针找属性在哪,这是一种开发上的快捷。 而对于编辑器来说,这些类都都是partial 聚合类, 本质上都是Player.cs 的一部分与OOP无差。

继续阅读

首先。。今天是个好日子,因为可以

好了,进入正题
现在很多项目都使用xlua来开发整个项目,但是实际上使用的并不是xlua标榜的“热修复”,毕竟国内游戏还是要要求可以热更新新功能的,因此如果采用热修复的方案,则需要小版本使用lua写功能,大版本又要把lua版本转换为C#代码重写一次,不太现实,因此现实中的许多公司都是使用lua来写大部分的逻辑。

但是u3d是个C#语言为编程语言的引擎(当然还有JS…),一般lua项目中,我们会把逻辑运算量大,复杂度高,对性能有要求的代码写在C#代码,或者C++ DLL库中,比如加载,更新,框架,战斗等, 这就需要程序要经常同时使用C#和lua写代码,容易人格分裂。

在Ilruntime 1.3版本之后,有稳定的调试插件(通过tcp 连接,因此可以真机调试),值绑定等功能后 也成为一个不错选择,程序员不需要更换语言来编写项目。至于它的局限性,对比lua来说都是半斤八两,比如主工程的泛型类无法导出,常用值类型需要生成wrap等(否则会有严重性能问题)。

目前很多人对Ilruntime 的看法有2点。 1,使用的项目比较少,未预见的坑比较多。 2,性能比较差,毕竟lua 有Jit, 在支持Jit的设备上是接近c的性能,大部分的性能损耗在接口交互上,而Ilruntime 是自己实现了一套解释器,是C#编写的,原生性能较差。 因此我打算做一个性能测试,看看真实的情况是什么。

使用的ILruntime库地址
https://github.com/Ourpalm/ILRuntime
使用的Xlua库地址
https://github.com/Tencent/xLua

注:Ilruntime 已经设置全局宏 DISABLE_ILRUNTIME_DEBUG, 并且hotfix项目为Release, 生成了Vecto3_Binding
Xlua 生成了 Vector3_Wrap
.Net 3.5版本下

测试3种情况下的性能情况

Test1 测试U3d内部值计算

ILRuntime:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void Test1()
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();
            for(int i = 0; i < 1000000; i++)
            {
                Vector3 a = new Vector3(1, 2, 3);
                Vector3 b = new Vector3(4, 5, 6);
                Vector3 c = a + b;
            }
            sw.Stop();
            UnityEngine.Debug.Log("il test1:" + sw.ElapsedMilliseconds);
        }

Xlua:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 void LuaTest1()
    {
        System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
        sw.Start();

        env.DoString(@"

        for i = 0, 1000000, 1 do

        local a = CS.UnityEngine.Vector3(1,2,3)

        local b = CS.UnityEngine.Vector3(4,5,6)

        local c = a + b

        end

        "
);
        sw.Stop();

        Debug.Log("lua test1:" + sw.ElapsedMilliseconds);
    }

继续阅读

一接触到新的东西,总想看看背后的原理是怎样的,xlua也不例外。于是试着写了一下,算是了解底层的实现原理,以后不用xlua也能有借鉴的地方。

xlua的热修复原理实际上是在 C# 编译成中间语言的时候,进行代码的插入这部分用到了 Mono.Ceil 库来操作,当然还有其他很多的库也可以实现。 因为是在IL的部分插入,因此直接支持IL2CPP

直接进入主题

已知有一个类

1
2
3
4
5
6
7
8
9
10
public class InputTest{

    void Start(){
        Hello();
    }
    private void Hello(){
        Debug.Log("hello");
        Debug.Log("666"):
    }
}

这个类在被Unity调用的时候会输出 “Hello”
那么如果我们想修改Hello函数该怎么做呢

1
2
3
4
string injectPath = @"./Library\ScriptAssemblies\Assembly-CSharp.dll";
AssemblyDefinition assemblyDefinition = null;
var readerParameters = new ReaderParameters { ReadSymbols = true };
assemblyDefinition = AssemblyDefinition.ReadAssembly(injectPath, readerParameters);

第一步 是要将当前代码的 Assembly 读出来, U3d有3个Assembly。 一个是项目代码叫 Assembly-CSharp.dll 一个是编辑器代码 Assembly-Editor-CSharp.dll.
还有一个是插件 Assembly-Plugin-CSharp.dll. 因为 InputTest是项目代码部分,所以读取 Assembly-CSharp.dll即可

读取成功后,所有的数据都在 assemblyDefinition 中,只需要遍历一下找到要修改的类即可

1
2
3
4
5
6
7
8
9
10
 foreach (Mono.Cecil.TypeDefinition item in assemblyDefinition.MainModule.Types) {

                if(item.FullName == "InputTest") {
                    foreach (MethodDefinition method in item.Methods) {

                        if (method.Name.Equals("Hello")) {
                        }
                    }
                }
}

第二步 通过遍历类型定义找到我们的类 “InputTest” 然后在 类定义中遍历所有的函数定义,找到我们要修改的 “Hello”函数
找到函数后,就可以正式做函数修改了。

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
var ins = method.Body.Instructions.First();
var worker = method.Body.GetILProcessor();
var logRef = assemblyDefinition.MainModule.Import(typeof(Debug).GetMethod("Log", new Type[] { typeof(string) }));

worker.InsertBefore(ins, worker.Create(OpCodes.Ldstr, "Fuck Off"));
worker.InsertBefore(ins, worker.Create(OpCodes.Call, logRef));

worker.InsertBefore(ins, worker.Create(OpCodes.Ldstr, "Fuck On"));
worker.InsertBefore(ins, worker.Create(OpCodes.Call, logRef));

Type type = typeof(InjectTest);

 if (null != type) {
     MethodInfo subMethod = type.GetMethod("SayFuck");

     if (null != subMethod) {
          Debug.Log("Find Method: " + subMethod);

          var sayRef = assemblyDefinition.MainModule.Import(subMethod);

          worker.InsertBefore(ins, worker.Create(OpCodes.Call, sayRef));
      }
}

var writerParameters = new WriterParameters { WriteSymbols = true };
assemblyDefinition.Write(injectPath, new WriterParameters());

第三步 做了3件事情, 绑定了2个UnityEngine的Log函数,打印了 “Fuck Off”, “Fuck On” 之后再绑定一个类 “InjectTest”中的静态函数 SayFuck()
这样原本的 Hello()函数就会在 打印”Hello”之前先打印 “Fuck Off”, “Fuck On” 调用 InjectTest.SayFuck().

最后就是将执行的修改进行保存 assemblyDefinition.Write

最后的最后用C#反编译软件打开 Assembly-CSharp.dll 看看修改后的Hello()函数

可以看到已经成功的修改啦。

之前一直在纠结项目的某些运营界面使用全Lua来写,这样可以热更新上线不同的活动,而不需要上架不同的版本,虽然可以通过资源更新加载xml的方式来实现,但是要通过xml来组合一些新的活动逻辑会很困难.

然后听了很多朋友最近都在推荐xlua, 不但可以做纯lua的逻辑更新,还可以做 C# 代码的bug hotfix.
就是可以在保持项目使用C#逻辑开发的前提下,出现bug后使用lua来修复.听起来很棒棒.

github地址 : xlua

试用一下之后,发现xlua的hotfix原理也很简单, 就是通过反射取出打上了 [hotfix] 标记的类,
然后对需要fix的函数执行下列伪代码

1
2
3
4
5
6
7
void Start()
{
  if(_hotfix)
   _lua_add(a,b);
  else
   _csharp_add(a,b);
}

那么思路就来了.
设定一个全局 hotfix管理类, 在游戏初始化的时候拉取服务器的 hotfix 版本号, 看看服务器上是否有新的补丁需要下载, 如果有,将补丁存到本地. 之后加载 补丁, 打补丁. 最后进入游戏.

正好以前自己写过好几个lua项目比如 : PrompaLua, lua还没忘记..正好用上 哈哈

首先是一个范例:
一个有bug的类

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using XLua;

[Hotfix]
public class FuckYouSelf : MonoBehaviour {

    private int A;
    private int B;

    void Init() {

    }
    // Use this for initialization
    void Start () {
        Init();
        UnityEngine.Debug.Log(Add(A, B));
    }

    int Add(int a, int b) {
        return a - b;
    }
   
    // Update is called once per frame
    void Update () {
       
    }
}

错误有 A,B 没有初始化
Add函数被写成了减法

好在类的开头被打上了 [Hotfix] 标记,让我们有机会热修复他.

然后是HotFix类

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
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using XLua;


public class HotFix : MonoBehaviour {

    public GameObject StartUp;

    [Serializable]
    struct VersionData {
        public string Version;
        public string FixUrl;
    }

    LuaEnv luaevn = new LuaEnv();
    // Use this for initialization
    void Awake () {

        StartCoroutine(LoadVersion("http://www.dreamfairy.cn/xlua/test/version.txt"));
    }

    IEnumerator LoadVersion(string versionUrl) {
        WWW versionData = new WWW(versionUrl);

        yield return versionData;

        if(null != versionData.error) {
            Debug.LogError(versionData.error);
        } else {
            VersionData data = JsonUtility.FromJson<VersionData>(versionData.text);
            StartCoroutine(LoadFix(data.FixUrl, data.Version));
        }
    }
   
    IEnumerator LoadFix(string fixUrl, string version) {

        //todo: check storage hotfix version

        WWW fixData = new WWW(fixUrl);

        yield return fixData;

        if (null != fixData.error) {
            Debug.LogError(fixData.error);
        } else {
            ApplyHotFix(fixData.text);
            SaveToStorage(fixData.text, version);
        }
    }

    void ApplyHotFix(string luastr) {
        luaevn.DoString(luastr);

        if(null != StartUp) {
            StartUp.SetActive(true);
        }
    }

    void SaveToStorage(string luastr, string version) {
        //todo
    }

    private void OnDestroy() {
       
    }
}

他会在游戏启动的时候去下载 http://www.dreamfairy.cn/xlua/test/version.txt 版本号数据
之后去补丁地址下载补丁文件 http://www.dreamfairy.cn/xlua/test/hotfix.lua

下载完毕后,缓存到本地, 然后调用游戏启动 StartUp

启动游戏后, 原本控制台输出 0 的结构, 热更新后 控制台输出 600

最后是lua部分的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
xlua.private_accessible(CS.FuckYouSelf)

xlua.hotfix(CS.FuckYouSelf, 'Init',
function(self)
    self.A = 300
    self.B = 300
end
)

xlua.hotfix(CS.FuckYouSelf, 'Add',
function(self, a, b)
    return a + b
end
)

lua部分的代码,就3段 第一段 xlua.private_accessible(CS.FuckYouSelf)
让lua可以访问私有字段, 目的是为了改成员变量 A,B 他们是私有的

第二段是修改 类中的 Init函数, 来初始化A,B

第三段是修改 类中的 Add函数,让他正确执行加法

虽然u3d标准资源包自带这货,但是我就喜欢重复造轮子,你奈我何

unity3d_hdr_bloom

简单来说 hdr_bloom 就3个Shader
第一个是shader, 筛选出像素亮度
第二个shader, 对像素进行模糊
第三个shader, 合并原始纹理 + 模糊亮度的纹理

第一个Shader
筛选亮度的算法

float brightness = col.r * 0.299 + col.g * 0.587 + col.b * 0.114;

之后我们给 Shader 一个 uniform 参数,作为亮度的调节参数即可

之后,为了让亮度周围出现光晕,我们将亮度纹理绘制在一张特别小的纹理上,然后再将这张小纹理进行拉伸,使其出现像素马赛克效果,这样处理后,迈赛克的亮度部分就会比原始纹理的明亮部分范围更大了,初步的光晕范围就出来了。
但是这部分光晕因为是马赛克,于是要将其进行模糊处理

绘制到缩小纹理再拉伸的效果是这样的

unity3d_hdr_bloom_2


请不要无脑复制转载本文, 由 dreamfairy 原创, 转载请注明出处 本文地址 http://www.dreamfairy.cn/blog/2016/05/28/unity3d-%e4%b8%ad%e7%9a%84-hdr_bloom/
那么实际操作的时候,我们将亮度提取的纹理先绘制到 1/16 大小的纹理上, 模糊后,再绘制到 1/8 纹理上, 然后 1/4, 1/2
最后将 1/2的模糊亮度纹理 和 原始纹理进行像素合并即可

这是模糊 1/2 后的纹理效果
unity3d_hdr_bloom_3

模糊的算法,原本采用 3×3 的二维高斯模糊算法,但是实际上和 2×2 的平滑模糊效果差不多, 于是就使用 2×2 的平滑模糊算法来做, 实际上使用 2个pass, 1个pass使用纵向3次 一维高斯算法, 1个pass 使用横向3次 一维高斯算法的效果最好, 不过我目前在做手机上的shader,差这么一点变现力还是可以接受的。

后面是3个完整Shader

取亮度

Shader "Aoi/OutputLuminosityShader"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
		_Brightness("BrightnessBias", float) = 0.1
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		LOD 100

		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			// make fog work
			#pragma multi_compile_fog
			
			#include "UnityCG.cginc"

			struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
			};

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

			sampler2D _MainTex;
			uniform float _Brightness;
			float4 _MainTex_ST;
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				UNITY_TRANSFER_FOG(o,o.vertex);
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				// sample the texture
				fixed4 col = tex2D(_MainTex, i.uv);
				float brightness = col.r * 0.299 + col.g * 0.587 + col.b * 0.114;

				if (brightness < _Brightness) {
					col.rgb = 0;
				}

				// apply fog
				UNITY_APPLY_FOG(i.fogCoord, col);
				return col;
			}
			ENDCG
		}
	}
}

2×2模糊

Shader "Aoi/2x2SimpleBlur"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		LOD 100

		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"

			struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
			};

			struct v2f
			{
				float2 uv : TEXCOORD0;
				float4 leftUpUV : TEXCOORD1;
				float4 rightDownUV : TEXCOORD2;
				float4 vertex : SV_POSITION;
			};

			sampler2D _MainTex;
			float4 _MainTex_ST;
			float4 _MainTex_TexelSize;
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);

				float2 uv = TRANSFORM_TEX(v.uv, _MainTex);
				o.uv = uv;
				o.leftUpUV = float4(uv.x - _MainTex_TexelSize.x, uv.y, uv.x, uv.y + _MainTex_TexelSize.y);
				o.rightDownUV = float4(uv.x + _MainTex_TexelSize.x, uv.y, uv.x, uv.y - _MainTex_TexelSize.y);
				
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				// sample the texture
				fixed4 col = tex2D(_MainTex, i.uv);
				
				col += tex2D(_MainTex, i.leftUpUV.xy);
				col += tex2D(_MainTex, i.leftUpUV.zw);
				col += tex2D(_MainTex, i.rightDownUV.xy);
				col += tex2D(_MainTex, i.rightDownUV.zw);

				col /= 5;
				
				return col;
			}
			ENDCG
		}
	}
}

合并纹理 的shader 就不上了
实际C# Code

using UnityEngine;
using System.Collections;

public class HDRBloomPostEffect : MonoBehaviour {
    public float BrightnessBias = 1.0f;

    private Material m_hdrBloomMat;
    private Material m_filterMat;
    private Material m_combineMat;

    // Use this for initialization
    void Start () {
        m_hdrBloomMat = new Material(Shader.Find("Aoi/OutputLuminosityShader"));
        m_filterMat = new Material(Shader.Find("Aoi/2x2SimpleBlur"));
        m_combineMat = new Material(Shader.Find("Aoi/CombineTex"));
    }

    float ComputeGaussianWeight(int posX, int posY, float sigma)
    {
        float v1 = 1.0f / (2.0f * Mathf.PI * sigma * sigma);
        float v2 = -(posX * posX + posY * posY) / (2 * sigma * sigma);
        v2 = Mathf.Exp(v2);

        return v1 * v2;
    }

    void OnRenderImage(RenderTexture src, RenderTexture dst) {
        RenderTexture rt16 = RenderTexture.GetTemporary(src.width / 16, src.height / 16);
        RenderTexture rt8 = RenderTexture.GetTemporary(src.width / 8, src.height / 8);
        RenderTexture rt4 = RenderTexture.GetTemporary(src.width / 4, src.height / 4);
        RenderTexture rt2 = RenderTexture.GetTemporary(src.width / 2, src.height / 2);

        m_hdrBloomMat.SetFloat("_Brightness", BrightnessBias);
        Graphics.Blit(src, rt16, m_hdrBloomMat);

        Graphics.Blit(rt16, rt8, m_filterMat);
        Graphics.Blit(rt8, dst, m_filterMat);
        Graphics.Blit(rt4, rt2, m_filterMat);

        m_combineMat.SetTexture("_AnotherTex", src);
        Graphics.Blit(rt2, dst, m_combineMat);

        RenderTexture.ReleaseTemporary(rt16);
        RenderTexture.ReleaseTemporary(rt8);
        RenderTexture.ReleaseTemporary(rt4);
        RenderTexture.ReleaseTemporary(rt2);
    }
}

gdc vertex color

之前做Terrain Brush Grass 的时候一直被它的性能折磨,稍微密集一些的植被,帧数直接变为个位数。 而GDC上使用顶点动画的想法瞬间给了我许多启发。

说干,就干。
在 Unity 中拖出一个 Cube 拉伸出 草的模样,然后给顶点上色。 像这样
红色表示可以被晃动的顶点
绿色表示晃动的幅度

两个颜色叠加在一起的时候,就变成屎黄了。。。
不过根部没有颜色呈现黑色可以看出过度

vertex color1

最后在Shader如下

v2f vert (appdata v)
{
v2f o;
float scale = cos(_Time.y * _Speed) * v.color.r * (v.color.g);float szControl = randValue > 0.5;
v.vertex.z += scale * szControl;
v.vertex.x += scale * (1.0 – szControl);

float4 worldPos = mul(_Object2World, v.vertex);

o.normal = normalize(mul(_Object2World, v.normal));
o.worldPos = worldPos;
o.pos = mul(UNITY_MATRIX_VP, worldPos);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);

return o;
}

fixed4 frag (v2f i) : SV_Target
{
float3 lightDir = normalize(WorldSpaceLightDir(i.worldPos));
float3 viewDir = normalize(_WorldSpaceCameraPos – i.worldPos);
float3 halfDir = normalize(lightDir + viewDir);
float3 diffuse = tex2D(_MainTex, i.uv) * max(0, dot(i.normal, lightDir));
float3 specular = _LightColor0.rgb * pow(max(0, dot(i.normal, halfDir)), _Shiness);
float4 finalCol = float4(diffuse + specular + UNITY_LIGHTMODEL_AMBIENT, 1.0);

return finalCol;
}

最后我在场景中放了 2.0w 根草, 随机摆动 draw call 1, 25帧
使用Terrain 的 Grass Brush 2000个草,就已经在 2.5帧了
grass