Unity-Chan技术探究——中央舞台效果实现

Shader的妙用

Posted by LioMiss on November 30, 2019 阅读()

这是UnityChan技术探究系列的第二弹,第一篇介绍了UnityChan中的舞台灯光跟随音乐节奏的实现方法以及音乐可视化的概念,这一篇主要会拆解UnityChan这个项目中,中央舞台的一些效果的实现方式。

舞台效果

UnityChan中,有一个非常酷炫的中央舞台,unity酱就是在上面表演的,这个舞台有很多效果:类似于玻璃材质的反射、漩涡形状的发光特效、六边形网格状向外扩散的脉冲,等等。
起初我以为中间的发光特效是一团粒子特效,但是看了他的代码之后发现并不是,中间的网格脉冲和漩涡都是通过Shader来实现的,这就引起了我的好奇。
QZFyDJ.md.gif

镜面反射效果的实现

QZkGRK.png
仔细观察这张图片,可以看到人物脚下的舞台像是一面镜子,反射出场景与人物。
看到这个效果,第一反应这是shader做的,于是去看舞台上的材质,发现它是用了一张程序生成的纹理。
QZLXrV.png
这张纹理由一个专门渲染镜面反射的相机实时渲染。
QZXfAS.png
动态创建这个相机的代码如下:

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);

漩涡特效的实现

其实这个效果的实现原理并不难,但是想要呈现出很好的效果,就需要强大的设计能力和数学能力,因为需要把数学公式变成图形输出。
QmGzVA.md.png
这里的旋涡主要是根据时间(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;
}

六边形网格脉冲的实现

QmJSUI.md.png
相对于旋涡特效,这个效果的实现要稍稍复杂一点。
首先,需要画出每个六边形网格:

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;