这是UnityChan技术探究系列的第二弹,第一篇介绍了UnityChan中的舞台灯光跟随音乐节奏的实现方法以及音乐可视化的概念,这一篇主要会拆解UnityChan这个项目中,中央舞台的一些效果的实现方式。
舞台效果
UnityChan中,有一个非常酷炫的中央舞台,unity酱就是在上面表演的,这个舞台有很多效果:类似于玻璃材质的反射、漩涡形状的发光特效、六边形网格状向外扩散的脉冲,等等。
起初我以为中间的发光特效是一团粒子特效,但是看了他的代码之后发现并不是,中间的网格脉冲和漩涡都是通过Shader来实现的,这就引起了我的好奇。
镜面反射效果的实现
仔细观察这张图片,可以看到人物脚下的舞台像是一面镜子,反射出场景与人物。
看到这个效果,第一反应这是shader做的,于是去看舞台上的材质,发现它是用了一张程序生成的纹理。
这张纹理由一个专门渲染镜面反射的相机实时渲染。
动态创建这个相机的代码如下:
1
2
3
4
5
6
7
8
GameObject go = new GameObject("Mirror Refl Camera id" + GetInstanceID() + " for " + currentCamera.GetInstanceID(), typeof(Camera), typeof(Skybox));
reflectionCamera = go.GetComponent<Camera>();
reflectionCamera.enabled = false;
reflectionCamera.transform.position = transform.position;
reflectionCamera.transform.rotation = transform.rotation;
reflectionCamera.gameObject.AddComponent<FlareLayer>();
go.hideFlags = HideFlags.HideAndDontSave; //隐藏相机
m_ReflectionCameras[currentCamera] = reflectionCamera;
然后为这个相机设置targetTexture,将它“看”到的东西渲染到纹理之中:
1
2
3
4
5
6
7
8
// Setup oblique projection matrix so that near plane is our reflection
// plane. This way we clip everything below/above it for free.
Vector4 clipPlane = CameraSpacePlane(reflectionCamera, pos, normal, 1.0f);
Matrix4x4 projection = cam.projectionMatrix;
CalculateObliqueMatrix(ref projection, clipPlane);
reflectionCamera.projectionMatrix = projection;
reflectionCamera.cullingMask = ~(1 << 4) & m_ReflectLayers.value; // never render water layer
reflectionCamera.targetTexture = m_ReflectionTexture;
除此之外,还需要渲染一张深度图。深度图的生成是在CopyDepth.shader中。
有了这两张纹理,接下来要做的就是在舞台材质的shader中对纹理进行采样,但是为了使效果更佳理想,需要对采样得到的颜色进行一定的处理,比如,通过参数来控制镜面反射的强度,以及通过深度图来实现反射效果随深度渐隐或模糊的效果:
1
o.Emission += refcolor * _ReflectionStrength * fade_by_depth * (1.0-grid*0.9);
漩涡特效的实现
其实这个效果的实现原理并不难,但是想要呈现出很好的效果,就需要强大的设计能力和数学能力,因为需要把数学公式变成图形输出。
这里的旋涡主要是根据时间(Time),以及采样点到中心点的距离,实时计算采样点的颜色,具体实现代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
float Rings(float3 pos)
{
float pi = 3.14159;
float2 wpos = pos.xz;
float stride = _RingSrtide;
float strine_half = stride * 0.5;
float thickness = 1.0 - (_RingThicknessMin + length(_Spectra)*(_RingThicknessMax-_RingThicknessMin));
float distance = abs(length(wpos) - _Time.y*0.1);
float fra = _gl_mod(distance, stride);
float cycle = floor((distance)/stride);
float c = strine_half - abs(fra-strine_half) - strine_half*thickness;
c = max(c * (1.0/(strine_half*thickness)), 0.0);
float rs = iq_rand(cycle*cycle);
float r = iq_rand(cycle) + _Time.y*(_RingSpeedMin+(_RingSpeedMax-_RingSpeedMin)*rs);
float angle = atan2(wpos.y, wpos.x) / pi *0.5 + 0.5; // 0.0-1.0
float a = 1.0-_gl_mod(angle + r, 1.0);
a = max(a-0.7, 0.0) * c;
return a;
}
六边形网格脉冲的实现
相对于旋涡特效,这个效果的实现要稍稍复杂一点。
首先,需要画出每个六边形网格:
1
2
3
4
5
6
7
8
9
10
11
12
13
float HexGrid(float3 p)
{
float scale = 1.2;
float2 grid = float2(0.692, 0.4) * scale;
float radius = 0.22 * scale;
float2 p1 = _gl_mod(p.xz, grid) - grid*0.5;
float c1 = Hex(p1, radius);
float2 p2 = _gl_mod(p.xz+grid*0.5, grid) - grid*0.5;
float c2 = Hex(p2, radius);
return min(c1, c2);
}
然后根据时间Time来计算脉冲的内圈半径和外圈半径:
1
2
3
4
5
6
7
8
float Circle(float3 pos)
{
float o_radius = 5.0;
float i_radius = 4.0;
float d = length(pos.xz);
float c = max(o_radius-(o_radius-_gl_mod(d-_Time.y*1.5, o_radius))-i_radius, 0.0);
return c;
}
并高亮这个区间内的所有纹素:
1
2
o.Albedo += _GridColor * grid * 0.1;
o.Emission += _GridColor * (grid * circle) * _GridEmission;