script
Unity
Shader

Shaderの_Timeをスクリプトで制御する際に気を付けること

Scriptを使ってShaderのPropertyを変更することはよくあるが、_Timeの速さを変更する際にガクガクになる問題があったので、それの解決記録として残しておく。


ガクガクする原因

今回例に挙げるのは、Shaderで縦にUVScrollしているが、Spaceキーを押している間だけ10倍速でUVScrollをするという仕組みを作ろうとしている。

そのために、今回用意したShaderとScriptは以下の通りである。


Scroll.shader

Shader "HC/Scroll"

{
Properties
{
_MainTex ("MainTexture", 2D) = "white" {}
_Speed("VerticalSpeed", Range(-20, 20)) = 1
}
SubShader
{
Tags
{ "Queue" = "Geometry" "RenderType" = "Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"

uniform sampler2D _MainTex;
uniform float4 _MainTex_ST;
uniform float _Speed;

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

struct v2f
{
float4 pos : SV_POSITION;
float2 uvTex : TEXCOORD0;
};

v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uvTex = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}

fixed4 frag (v2f i) : SV_Target
{

float2 uv = i.uvTex;
uv.y += _Time.y*_Speed;
fixed4 color = tex2D(_MainTex, uv);
return color;
}
ENDCG
}
}
}



uvscroll.cs

using System.Collections;

using System.Collections.Generic;
using UnityEngine;

public class uvscroll : MonoBehaviour {

private Renderer renderer;

private float speed
{
get{ return GetComponent<Renderer>().material.GetFloat("_Speed"); }
set{ GetComponent<Renderer>().material.SetFloat("_Speed", value); }
}

void Start () {
renderer = GetComponent<Renderer>();
}

void Update () {
if(Input.GetKeyDown(KeyCode.Space))
{
speed = 10.0f;
}
if(Input.GetKeyUp(KeyCode.Space))
{
speed = 1.0f;
}
}
}


これを実装すると次のような動き方をしてしまう。

Before

どうして画像がガクガクしてしまうのかというと、普通の_Timeの時間軸と10倍の_Timeの時間軸をSpaceキーを押すことで、瞬間移動のように行ったり来たりしてしまうためにこの現象が起こる。

特に、Spaceキーを離したとき、時間が巻き戻るという異質な現象が起きるために余計にガクつきが発生する。


解決策(素案)

この問題を解決するために、スクリプトのTime関数を使ってその値をShaderに流し込むという方法をとる。

ただ、これだけだとシェーダーの_Timeに倍率をかけている前の発想と同様な仕組みとなるため、ガクつきを抑えるためにLerp関数を用いてみるとする。

そして、Lerpの第三変数の遷移をAnimatorを使ってアナログにアニメーションすれば綺麗に行きそうではと試してみる。

ということで検証してみたが、やっぱりLerpの第三変数が0から1に行く過程で10倍の時間軸に移動するため、結局uvが早送りになってしまった。


解決策(結論)

なので、どうしようと沼にはまる。

そこでUnityの公式のスクリプトリファレンスのTime関数とにらめっこしてみた。

すると、timeScaleという便利な関数があるではないか!ということでこれに倍率を代入してみることとする。

ということで、完成版のコードは以下の通りとなった。


scroll1.shader

Shader "HC/Scroll1"

{
Properties
{
_MainTex("MainTexture", 2D) = "white" {}
_Speed("VerticalSpeed", Range(-20, 20)) = 1
}
SubShader
{
Tags
{ "Queue" = "Geometry" "RenderType" = "Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"

uniform sampler2D _MainTex;
uniform float4 _MainTex_ST;
uniform float _Speed;

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

struct v2f
{
float4 pos : SV_POSITION;
float2 uvTex : TEXCOORD0;
};

v2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uvTex = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}

fixed4 frag(v2f i) : SV_Target
{

float2 uv = i.uvTex;
uv.y = _Speed;
fixed4 color = tex2D(_MainTex, uv);
return color;
}
ENDCG
}
}
}



uvscroll1.cs

using System.Collections;

using System.Collections.Generic;
using UnityEngine;

public class uvscroll1 : MonoBehaviour
{
private Renderer renderer;

private float speed
{
get { return GetComponent<Renderer>().material.GetFloat("_Speed"); }
set { GetComponent<Renderer>().material.SetFloat("_Speed", value); }
}

void Start()
{
renderer = GetComponent<Renderer>();
}

void Update()
{
float time = Time.time;
if (Input.GetKeyDown(KeyCode.Space))
{
Time.timeScale = 10.0f;
}
if (Input.GetKeyUp(KeyCode.Space))
{
Time.timeScale = 1.0f;
}
speed = time;
}
}


これを実行すると、この動画のようにうまく連続的に倍率をかけられるようになった。

(追記)

ただ、timeScaleをいじるので、すべての時間軸が変化することに注意が必要となる。

必要ならunscaledTime等を他で適応させる必要があるだろう。

After


おまけ(宣伝)

今回のシェーダーのベースとなっているシンプルなUVScrollに透過機能を備えたシェーダーを売っているので、よければこちらからどうぞ!