用数组来实现热度图 着色器难以掌握的一个典型的原因就是缺少合适的文档。许多开发者在学习着色器的时候被代码搞得一团糟,原因就是他们没有很深的知识来解释眼前到底发生了什么。事实上 Cg/HLSL 中有着大量的臆断,这些臆断又没有被正确的证明过,这就让问题变得更加的突出。Unity3d允许C#脚本使用诸如 SetFloat ,SetInt ,SetVector 等之类的方法来跟着色器通信。遗憾的是,Unity3D没有类似 SetArray 的方法,也正因如此导致很多开发者们以为 Cg/HLSL 不支持 数组(arrays)。但事实并非如此,这篇文章将会给你展示向着色器传递数组的可能性。需要注意的是GPU为并行计算进行了高度的优化,所以如果在着色器中使用 循环结构(loops) 将会极大的降低它的性能。
在这个知识点中,我们将会实现一个热度图,看起来大概跟下图一样:
始前准备
这个知识点中介绍的效果是从一些设置好的点中创建一个热度图。这个热度图将会覆盖在另外一张图片的上面,就像一张前置图片。下面是必要的步骤: 1.在Unity中使用一张纹理创建一个 quad,这张纹理就是用来创建热度图的那张纹理。在我们的例子中,使用了一张伦敦地图的纹理。 2.创建另外一个 quad,并且把它放在前面创建的那个quad的上面。我们的热度图将会在这个quad上产生。 3.创建一个对应的材质,并且把材质和着色器都应用到第二个quad上。 操作步骤
这个着色器跟我们之前创建的着色器还是有很大不同,当然它也相对较短。因此,下面的步骤中直接提供了着色器完整代码: 1.将下面的的着色器代码整个复制到新创建的着色器中: // Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld' // Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)' shader "Custom/Heatmap" { Properties { _HeatTex ("Texture", 2D) = "white" {} } Subshader { Tags {"Queue"="Transparent"} Blend SrcAlpha OneMinusSrcAlpha // Alpha blend Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag struct vertInput { float4 pos : POSITION; }; struct vertOutput { float4 pos : POSITION; fixed3 worldPos : TEXCOORD1; }; vertOutput vert(vertInput input) { vertOutput o; o.pos = UnityObjectToClipPos(input.pos); o.worldPos = mul(unity_ObjectToWorld, input.pos).xyz; return o; } uniform int _Points_Length = 0; uniform float3 _Points [20]; // (x, y, z) = position uniform float2 _Properties [20]; // x = radius, y = intensity sampler2D _HeatTex; half4 frag(vertOutput output) : COLOR { // Loops over all the points half h = 0; for (int i = 0; i < _Points_Length; i ++) { // Calculates the contribution of each point half di = distance(output.worldPos, _Points[i].xyz); half ri = _Properties[i].x; half hi = 1 - saturate(di / ri); h += hi * _Properties[i].y; } // Converts (0-1) according to the heat texture h = saturate(h); half4 color = tex2D(_HeatTex, fixed2(h, 0.5)); return color; } ENDCG } } Fallback "Diffuse" } 2.当你把这个着色器挂载到对应的材质球上的时候,你还需要为热度图提供一张渐变纹理。重要的是记得把这张纹理的 Wrap Mode 设置为 Clamp。下图中的纹理就是我们这个例子中使用的渐变纹理:
注意
如果你的热度图要作为一个覆盖层,那么还要确保这张渐变纹理有一个alpha通道并且导入图片设置那里记得勾选 Alpha is Transparency 。 3.创建一个名为 Heatmaps 的C#脚本,并输入下面的代码: using System.Collections; using UnityEngine; public class Heatmaps : MonoBehaviour { public Vector3[] positions; public float[] radiuses; public float[] intensities; public Material material; void Start() { material.SetInt("_Points_Length", positions.Length); for (int i = 0; i < positions.Length; i++) { material.SetVector("_Points"+ i, positions[i]); Vector2 properties = new Vector2(radiuses[i], intensities[i]); material.SetVector("_Properties" + i, properties); } } void Update() { } } 4.将这个脚本挂载到场景中的一个游戏对象上,最好是你之前创建的quad。然后将为这个效果创建的材质球拖拽到脚本中的 材质变量(material slot) 上。做完这些后,C#脚本就能访问材质球了并且会对它进行初始化。 5.最后,在C#脚本上,展开 positions,radiuses 和 intensities 这些数组变量,给它们填入热度图需要的参数值。positions 指的是热度图的位置点信息(世界坐标),radiuses 指的是热度图的半径大小,intensities 指的是热度图周围的强烈程度:
原理介绍
这个着色器的实现依赖了我们本书之前从来没有介绍的一些东西;首先第一个就是数组。在Cg语言中可以通过下面的语法创建数组: uniform float3 _Points [20]; Cg不支持未知大小的数组:你必须要预先知道你要分配的空间大小。前面那行代码创建了一个20个元素的数组。 Unity并没有直接暴露出任何能初始化那些数组的方法。然而,里面的单个元素却可以用数组的名称 (_Points) 加元素位置的方式去访问,比如 _Points0 或者 _Points10 这样去访问。但是这种方式只对确切类型的数组有效,比如 float3 类型和 float2 类型。我们把这个C#脚本挂载在了一个quad上,然后对着色器中的数组元素逐个进行了初始化。 在着色器的片元函数中,每个循环结构中相似的事情是,对材质上的每个像素,求所有的点对热度图的影响程度: // Loops over all the points half h = 0; for (int i = 0; i < _Points_Length; i ++) { // Calculates the contribution of each point half di = distance(output.worldPos, _Points[i].xyz); half ri = _Properties[i].x; half hi = 1 - saturate(di / ri); h += hi * _Properties[i].y; } 变量 h 保存了来自所有点的热度,它是根据半径和强度计算出来的。之后它就被用于查看到底该用渐变纹理上的哪些颜色。 着色器和数组其实是一个很好的组合,但是却很少有游戏能在使用时发挥出它们的全部潜力。然而在这里,它们却引来了一个很大的瓶颈,因为对于每一个像素来说,着色器都必须要去遍历完所有的点。