UVで移動・回転・拡縮をする

環境

Unity2021.2.18f1

概要

UVで移動・回転・拡縮をしてみました。

AnimationCurveからKeyframe配列のtimeとvalueを抜き出し、シェーダでバイナリサーチをかけ、リニア補間した値を使用しています。

using Unity.Collections;
using UnityEditor;
using UnityEngine;

public unsafe class UvTransform : MonoBehaviour
{
    [SerializeField] Material _material = null;
    //リニア補間のみ
    [SerializeField] AnimationCurve _uvOffsetXs = null;
    [SerializeField] AnimationCurve _uvOffsetYs = null;
    [SerializeField] AnimationCurve _uvRotateZs = null;
    [SerializeField] AnimationCurve _uvScaleXs = null;
    [SerializeField] AnimationCurve _uvScaleYs = null;

    private static int _spUvOffsetXs = Shader.PropertyToID("_UvOffsetXs");
    private static int _spUvOffsetYs = Shader.PropertyToID("_UvOffsetYs");
    private static int _spUvRotateZs = Shader.PropertyToID("_UvRotateZs");
    private static int _spUvScaleXs  = Shader.PropertyToID("_UvScaleXs");
    private static int _spUvScaleYs  = Shader.PropertyToID("_UvScaleYs");
    private static int _spMaxTime  = Shader.PropertyToID("_MaxTime");

    private GraphicsBuffer[] _gbUvTransforms = null;
    private float _maxTime = 0.0f;
#if UNITY_EDITOR
    private void Reset()
    {
        _uvOffsetXs.AddKey(new Keyframe(0.0f, 0.0f));
        _uvOffsetXs.AddKey(new Keyframe(1.0f, 0.0f));
        _uvOffsetYs.AddKey(new Keyframe(0.0f, 0.0f));
        _uvOffsetYs.AddKey(new Keyframe(1.0f, 0.0f));
        _uvRotateZs.AddKey(new Keyframe(0.0f, 0.0f));
        _uvRotateZs.AddKey(new Keyframe(1.0f, 0.0f));
        _uvScaleXs.AddKey(new Keyframe(0.0f, 1.0f));
        _uvScaleXs.AddKey(new Keyframe(1.0f, 1.0f));
        _uvScaleYs.AddKey(new Keyframe(0.0f, 1.0f));
        _uvScaleYs.AddKey(new Keyframe(1.0f, 1.0f));

        var animCurves = new AnimationCurve[]
        {
            _uvOffsetXs, _uvOffsetYs,
            _uvRotateZs,
            _uvScaleXs, _uvScaleYs
        };
        foreach(var animCurve in animCurves)
        {
            for (int i = 0; i < animCurve.length; i++)
            {
                AnimationUtility.SetKeyLeftTangentMode(animCurve, i, AnimationUtility.TangentMode.Linear);
                AnimationUtility.SetKeyRightTangentMode(animCurve, i, AnimationUtility.TangentMode.Linear);
            }
        }
    }

    private void OnValidate()
    {
        Setup();
    }
#endif
    private void Setup()
    {
        if(Application.isPlaying == false)
            return;
        var animCurves = new AnimationCurve[]
        {
            _uvOffsetXs, _uvOffsetYs,
            _uvRotateZs,
            _uvScaleXs, _uvScaleYs
        };
        var shaderIds = new int[]
        {
            _spUvOffsetXs, _spUvOffsetYs,
            _spUvRotateZs,
            _spUvScaleXs, _spUvScaleYs
        };
        if(_gbUvTransforms == null)
        {
            _gbUvTransforms = new GraphicsBuffer[animCurves.Length];
            for(int i = 0; i < _gbUvTransforms.Length; i++)
            {
                var animCurve = animCurves[i];
                _gbUvTransforms[i] = new GraphicsBuffer(GraphicsBuffer.Target.Structured, animCurve.length, sizeof(Vector2));
            }
        }
        _maxTime = 0.0f;
        for(int i = 0; i < _gbUvTransforms.Length; i++)
        {
            var animCurve = animCurves[i];
            var keyValues = new NativeArray<Vector2>(animCurve.length, Allocator.Temp);
            for(int j = 0; j < animCurve.length; j++)
            {
                var keyframe = animCurve.keys[j];
                keyValues[j] = new Vector2(keyframe.time, keyframe.value);
            }
            if(_maxTime < keyValues[animCurve.length - 1].x)
                _maxTime = keyValues[animCurve.length - 1].x;
            _gbUvTransforms[i].SetData(keyValues);
            _material.SetBuffer(shaderIds[i], _gbUvTransforms[i]);
        }
        _material.SetFloat(_spMaxTime, _maxTime);
    }

    private void Start()
    {
        Setup();
    }

    private void OnDestroy()
    {
        if(_gbUvTransforms != null)
        {
            foreach(var gb in _gbUvTransforms)
            {
                gb.Dispose();
            }
            _gbUvTransforms = null;
        }
    }
}
Shader "Custom/UvTransform"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }

    CGINCLUDE
    #pragma target 4.5
    #include "UnityCG.cginc"

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

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

    sampler2D _MainTex;
    float4 _MainTex_ST;
    StructuredBuffer<float2> _UvOffsetXs;
    StructuredBuffer<float2> _UvOffsetYs;
    StructuredBuffer<float2> _UvRotateZs;
    StructuredBuffer<float2> _UvScaleXs;
    StructuredBuffer<float2> _UvScaleYs;
    float _MaxTime;

    float SearchValue(float time, StructuredBuffer<float2> keyValues)
    {
        uint len;
        uint stride;
        keyValues.GetDimensions(len, stride);

        uint min = 0;
        uint max = len - 1;

        if (time <= keyValues[min].x)
            return keyValues[min].y;
        if (time >= keyValues[max].x)
            return keyValues[max].y;

        while ((max - min) > 1)
        {
            uint index = (min + max) / 2;
            if (keyValues[index].x < time)
            {
                min = index;
            }
            else //if (time < anims[index].x)
            {
                max = index;
            }
        }
        float r = (time - keyValues[min].x) / (keyValues[max].x - keyValues[min].x);
        return lerp(keyValues[min].y, keyValues[max].y, r);
    }

    float2 UvTransform(float2 tiling, float2 offset, float2 texcoord, float2 pos, float2 scale, float rotateZ)
    {
        float2 pivot = float2(0.5, 0.5);
        float cosAngle, sinAngle;

        sincos(rotateZ, sinAngle, cosAngle);
        float2 s = ((tiling - 1.0) + scale);
        float2x2 rotMatrix = float2x2(float2(cosAngle, -sinAngle), float2(sinAngle, cosAngle));
        float2 uv = texcoord - pivot;
        uv = mul(rotMatrix, uv) * s + pivot + -pos;
        uv += pivot * tiling + offset - pivot;
        return uv;
    }

    v2f vert(appdata v)
    {
        v2f o;
        o.vertex = UnityObjectToClipPos(v.vertex);

        float time = fmod(_Time.y, _MaxTime);
        float ox = SearchValue(time, _UvOffsetXs);
        float oy = SearchValue(time, _UvOffsetYs);
        float rz = SearchValue(time, _UvRotateZs);
        float sx = SearchValue(time, _UvScaleXs);
        float sy = SearchValue(time, _UvScaleYs);

        o.uv = UvTransform(_MainTex_ST.xy, _MainTex_ST.zw, v.uv, float2(ox, oy), float2(sx, sy), rz);
        return o;
    }

    fixed4 frag(v2f i) : SV_Target
    {
        fixed4 col = tex2D(_MainTex, i.uv);
        return col;
    }
    ENDCG

    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            ENDCG
        }
    }
}