シェーダでノイズ2(パーリンノイズ)

シェーダでノイズ2(パーリンノイズ)

 パーリンノイズは単純なランダムノイズと異なり、滑らかなノイズが得られます。そのため、様々なテクスチャの生成や炎、雲及び地形などの自然物を表現する際によく使用されます。

一次元パーリンノイズ

 始めに、xの少数部分を取り出します。これにより、整数の間で\(0\)から\(1\)未満の値を繰り返すようになります。したがって、下図で色分けされているように整数の間を一つの区間として区切ることができます。ここで、区切られた区間を格子、区間の境目を格子線とします。また、丸で囲まれている座標を格子点とします。

ある格子内において、左側の格子点、つまり、\(floor(x)\)におけるランダムな値\(a_{L}\)と右側の格子点 \(floor(x+1)\)におけるランダムな値\(a_{R}\) を求めます。次に、それぞれの格子点を原点とし、ランダムな値を傾きとする直線を以下の式より求めます。

$$ \begin{align} &w_{L}(x)=a_{L}\cdot frac(x)\\ &w_{R}(x)=a_{R}\cdot (frac(x)-1) \end{align} $$

上式により求められる直線は下図のようになります。赤色の直線が\(w_{L}(x)\)、青色の直線が\(w_{R}(x)\)です。ある格子線の左側の青い直線と右側の赤い直線が一直線となっているのは、傾きとして求められるランダムな値が同じ格子点であれば、必ず同じ値となるためです。

この直線により求められる二つの値をエルミート補間することでノイズが求まります。使用するエルミート補間の式は以下の通りです。

$$ u=3f^2-2f^3 $$

また、エルミート補間を図にすると以下のようになります。

よって、ノイズを求める式は以下の式となります。

$$ \begin{align} &f=frac(x),u=f^{2}(3-2f)\\ &n=lerp(w_{L},w_{R},u) \end{align} $$

この式より、以下の図に示すノイズを求めることができます。

上図より、滑らかでかつランダムな値が得られることがわかります。また、パーリンノイズは格子線上において必ず\(0\)となることもわかります。

Script

 Unityで一次元パーリンノイズを求めるスクリプトを作成しました。スクリプトは以下の通りです。このスクリプトは適当なゲームオブジェクトにアタッチすれば動作します。このスクリプトでは、上記のように直線をエルミート補間する方法だけではなく、 こちらに掲載されているウェーブレット関数を求め、それらを線形補間する方法でもノイズを計算しています。


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class PerlinNoise : MonoBehaviour {

    public Sprite sprite_UISprite, sprite_checkmark, sprite_knob, sprite_background;
    public Material mat_default_line;
    private GameObject obj_line, obj_graph, obj_canvas, obj_grad, obj_wave;
    private RectTransform[] rect_scale = new RectTransform[5];
    private RectTransform[] rect_image = new RectTransform[2];
    private Toggle[] toggle = new Toggle[4];
    private Slider slider_seed, slider_width, slider_height, slider_scale;
    private LineRenderer line_easeNoize, line_waveNoize, line_frame, line_center;
    private LineRenderer[] line_x = new LineRenderer[9];
    private LineRenderer[] line_y = new LineRenderer[8];
    private LineRenderer[] line_grad = new LineRenderer[20];
    private LineRenderer[] line_wave = new LineRenderer[2];
    private int step = 20, range = 10, linerenderer_size;
    private float scale = 1f;
    private Vector2 graph_size = new Vector2(10.5f, 2.8f);
    private Vector2[] text_offset = { new Vector2(-5f, 0f), new Vector2(0f, -5f) };
    private Vector2 screenSize;
    private Text[] text_scale = new Text[2];
    private Color color_bg = new Color(0.745f, 0.745f, 0.745f, 1);

    // Use this for initialization
    void Start() {
        screenSize = new Vector2(Screen.width, Screen.height);
        CameraPosition();

        //カメラ
        Camera.main.orthographic = true;
        Camera.main.orthographicSize = 4f;
        Camera.main.clearFlags = CameraClearFlags.SolidColor;
        Camera.main.backgroundColor = color_bg;

        //UIの作成
        obj_canvas = GameObject.Find("Canvas");
        if (obj_canvas == null)
        {
            obj_canvas = AddCanvas();
        }

        GameObject obj_ui = new GameObject("UI");
        obj_ui.transform.SetParent(obj_canvas.transform);
        obj_ui.layer = LayerMask.NameToLayer("UI");
        RectTransform rect_ui = obj_ui.AddComponent();
        rect_ui.anchoredPosition = Vector2.zero;
        rect_ui.anchorMin = new Vector2(0f, 1f);
        rect_ui.anchorMax = new Vector2(0f, 1f);

        //toggle
        toggle[0] = AddToggle(obj_ui, new Vector2(90f, -20f), "toggle", "hermite");
        toggle[1] = AddToggle(obj_ui, new Vector2(170f, -20), "toggle", "L(x)");
        toggle[2] = AddToggle(obj_ui, new Vector2(90f, -45f), "toggle", "wavelet");
        toggle[3] = AddToggle(obj_ui, new Vector2(170f, -45f), "toggle", "W(x)");

        toggle[0].isOn = true;
        toggle[3].interactable = false;

        //slider
        slider_seed = AddSlider(obj_ui, new Vector2(245f, -20f), "slider");
        slider_seed.wholeNumbers = true;
        slider_seed.maxValue = 300;
        Text text_slider = AddText(obj_ui, new Vector2(235f, -20f), 16, "Seed", "text_seed").GetComponent();
        text_slider.alignment = TextAnchor.MiddleLeft;

        slider_width = AddSlider(obj_ui, new Vector2(410f, -20f), "slider_width");
        slider_width.minValue = 1f;
        slider_width.maxValue = 20f;
        slider_width.value = graph_size.x;
        Text text_width = AddText(obj_ui, new Vector2(390f, -20f), 16, "width", "text_width").GetComponent();
        text_width.alignment = TextAnchor.MiddleLeft;

        slider_height = AddSlider(obj_ui, new Vector2(410f, -40f), "slider_height");
        slider_height.minValue = 0.5f;
        slider_height.maxValue = 5f;
        slider_height.value = graph_size.y;
        Text text_height = AddText(obj_ui, new Vector2(390f, -40f), 16, "height", "text_height").GetComponent();
        text_height.alignment = TextAnchor.MiddleLeft;

        slider_scale = AddSlider(obj_ui, new Vector2(245f, -40f), "slider_scale");
        slider_scale.minValue = 0;
        slider_scale.maxValue = 2;
        slider_scale.wholeNumbers = true;
        slider_scale.value = 1;
        Text text_yscale = AddText(obj_ui, new Vector2(235f, -40f), 16, "y", "text_scale").GetComponent();
        text_yscale.alignment = TextAnchor.MiddleLeft;

        //目盛り
        string[] str = { "-1", "0", "1", "0", "10" };
        for (int i = 0; i < 5; i++)
        {
            GameObject obj_scale_text = AddText(obj_canvas, Vector2.zero, 18, str[i], str[i]);
            Text text_n = obj_scale_text.GetComponent();
            if (i == 0) text_scale[0] = text_n;
            if (i == 2) text_scale[1] = text_n;

            rect_scale[i] = obj_scale_text.GetComponent();
            rect_scale[i].anchorMin = Vector2.zero;
            rect_scale[i].anchorMax = Vector2.zero;

            if (i < 3)
            {
                rect_scale[i].pivot = new Vector2(1f, 0.5f);
                text_n.alignment = TextAnchor.MiddleRight;
            }
            else
            {
                rect_scale[i].pivot = new Vector2(0.5f, 1f);
                text_n.alignment = TextAnchor.UpperCenter;
            }
        }
        ScaleText();

        //枠線
        obj_graph = new GameObject("graph");
        obj_line = new GameObject("frame");
        obj_line.transform.position = Vector3.zero;
        line_frame = obj_line.AddComponent();
#if UNITY_EDITOR
        line_frame.material = UnityEditor.AssetDatabase.GetBuiltinExtraResource("Default-Line.mat");
#else
            line_frame.material = mat_default_line;
#endif

        line_frame.startColor = Color.black;
        line_frame.endColor = Color.black;
        line_frame.startWidth = 0.05f;
        line_frame.endWidth = 0.05f;
        line_frame.positionCount = 5;

        linerenderer_size = range * step;

        //中心線
        line_center = LineCreator(obj_line, obj_graph, "center", Vector3.zero, Color.black, 0.05f, 2);

        //縦線
        for (int n = 0; n < line_x.Length; n++)
        {
            line_x[n] = LineCreator(obj_line, obj_graph, "v_line", Vector3.zero, new Color(0.67f, 0.67f, 0.67f, 1f), 0.03f, 2);
        }

        //横線
        for (int n = 0; n < line_y.Length * 0.5f; n++)
        {
            line_y[n] = LineCreator(obj_line, obj_graph, "h_line", Vector3.zero, new Color(0.67f, 0.67f, 0.67f, 1f), 0.03f, 2);
            line_y[line_y.Length - n - 1] = LineCreator(obj_line, obj_graph, "h_line", Vector3.zero, new Color(0.67f, 0.67f, 0.67f, 1f), 0.02f, 2);
        }

        //ノイズ用ラインレンダラー
        line_easeNoize = LineCreator(obj_line, null, "Noise-Ease", Vector3.zero, new Color(1f, 0, 1f, 1f), 0.05f, linerenderer_size + 1);
        line_waveNoize = LineCreator(obj_line, null, "Noise-Wavelet", Vector3.zero, new Color(1f, 0, 1f, 1f), 0.05f, linerenderer_size + 1);

        //L(x)
        obj_grad = new GameObject("L(t)");
        for (int x = 0; x < range; x++)
        {
            line_grad[x] = LineCreator(obj_line, obj_grad, "grad_left_" + x.ToString(), Vector3.zero, Color.red, 0.05f, 2);
            line_grad[x + range] = LineCreator(obj_line, obj_grad, "grad_right_" + x.ToString(), Vector3.zero, Color.blue, 0.05f, 2);
        }

        //wavelet用ラインレンダラー
        obj_wave = new GameObject();
        line_wave[0] = LineCreator(obj_line, obj_wave, "wavelet_left", Vector3.zero, Color.red, 0.05f, linerenderer_size + 1);
        line_wave[1] = LineCreator(obj_line, obj_wave, "wavelet_right", Vector3.zero, Color.blue, 0.05f, linerenderer_size + 1);


        line_waveNoize.gameObject.SetActive(false);
        obj_grad.SetActive(false);
        obj_wave.SetActive(false);

        SetUI();
        GraphLine();
        DrawNoize(0);
    }

    //グラフの枠線とグリッド線
    void GraphLine()
    {
        //枠線
        line_frame.SetPosition(0, new Vector3(0, graph_size.y, -1f));
        line_frame.SetPosition(1, new Vector3(0, -graph_size.y, -1f));
        line_frame.SetPosition(2, new Vector3(graph_size.x, -graph_size.y, -1f));
        line_frame.SetPosition(3, new Vector3(graph_size.x, graph_size.y, -1f));
        line_frame.SetPosition(4, new Vector3(0, graph_size.y, -1f));

        //中心線
        line_center.SetPosition(0, new Vector3(0, 0, 1f));
        line_center.SetPosition(1, new Vector3(graph_size.x, 0, 1f));

        //縦線
        float x = graph_size.x / range;
        float y = -graph_size.y;
        for (int n = 0; n < line_x.Length; n++)
        {
            for (int m = 0; m < 2; m++)
            {
                y *= -1;
                line_x[n].SetPosition(m, new Vector3((n + 1f) * x, y, 2f));
            }
        }

        //横線
        y = graph_size.y / 5f;
        for (int n = 0; n < line_y.Length * 0.5f; n++)
        {
            for (int m = 0; m < 2; m++)
            {
                line_y[n].SetPosition(m, new Vector3(m * graph_size.x, y * (n + 1), 2f));
                line_y[line_y.Length - n - 1].SetPosition(m, new Vector3(m * graph_size.x, -y * (n + 1), 2f));
            }
        }


    }

    //UI要素の動作
    void SetUI()
    {
        //toggle
        toggle[0].onValueChanged.AddListener((flg) => {
            if (toggle[0].isOn)
            {
                line_easeNoize.gameObject.SetActive(true);
                line_waveNoize.gameObject.SetActive(false);
                toggle[1].interactable = true;
                toggle[2].isOn = false;
                toggle[3].isOn = false;
                toggle[3].interactable = false;
            }
            else
            {
                line_easeNoize.gameObject.SetActive(false);
            }
        });

        toggle[2].onValueChanged.AddListener((flg) => {
            if (toggle[2].isOn)
            {
                line_waveNoize.gameObject.SetActive(true);
                line_easeNoize.gameObject.SetActive(false);
                toggle[0].isOn = false;
                toggle[1].isOn = false;
                toggle[1].interactable = false;
                toggle[3].interactable = true;
            }
            else
            {
                line_waveNoize.gameObject.SetActive(false);
            }
        });

        toggle[1].onValueChanged.AddListener((flg) => {
            if (toggle[1].isOn)
            {
                obj_grad.SetActive(true);
            }
            else
            {
                obj_grad.SetActive(false);
            }
        });

        toggle[3].onValueChanged.AddListener((flg) => {
            if (toggle[3].isOn)
            {
                obj_wave.SetActive(true);
            }
            else
            {
                obj_wave.SetActive(false);
            }
        });

        //slider
        slider_seed.onValueChanged.AddListener((flg) =>
        {
            DrawNoize((int)slider_seed.value);
        });

        slider_width.onValueChanged.AddListener((flg) =>
        {
            graph_size.x = slider_width.value;
            GraphLine();
            DrawNoize((int)slider_seed.value);
            CameraPosition();
            ScaleText();
        });

        slider_height.onValueChanged.AddListener((flg) =>
        {
            graph_size.y = slider_height.value;
            GraphLine();
            DrawNoize((int)slider_seed.value);
            ScaleText();
        });

        slider_scale.onValueChanged.AddListener((flg) =>
        {
            int val = (int)slider_scale.value;
            switch (val)
            {
                case 0:
                    text_scale[0].text = "-0.5";
                    text_scale[1].text = "0.5";
                    scale = 2f;
                    break;

                case 1:
                    text_scale[0].text = "-1";
                    text_scale[1].text = "1";
                    scale = 1f;
                    break;

                case 2:
                    text_scale[0].text = "-2";
                    text_scale[1].text = "2";
                    scale = 0.5f;
                    break;

                default:
                    break;
            }
            DrawNoize((int)slider_seed.value);
        });
    }

    //目盛りの位置
    void ScaleText()
    {
        Vector2[] text_wpos = new Vector2[5];
        text_wpos[0] = RectTransformUtility.WorldToScreenPoint(Camera.main, new Vector3(0f, -graph_size.y, 0f));
        text_wpos[1] = RectTransformUtility.WorldToScreenPoint(Camera.main, new Vector3(0f, 0f, 0f));
        text_wpos[2] = RectTransformUtility.WorldToScreenPoint(Camera.main, new Vector3(0f, graph_size.y, 0f));
        text_wpos[3] = RectTransformUtility.WorldToScreenPoint(Camera.main, new Vector3(0f, -graph_size.y, 0f));
        text_wpos[4] = RectTransformUtility.WorldToScreenPoint(Camera.main, new Vector3(graph_size.x, -graph_size.y, 0f));

        for (int i = 0; i < 5; i++)
        {
            if (i < 3)
            {
                rect_scale[i].position = text_wpos[i] + text_offset[0];
            }
            else
            {
                rect_scale[i].position = text_wpos[i] + text_offset[1];
            }
        }
    }

    //LineRendererの作成
    LineRenderer LineCreator(GameObject obj, GameObject obj_parent, string line_name, Vector3 pos, Color color, float width, int posSize)
    {
        GameObject obj_tmp = Instantiate(obj, pos, Quaternion.identity);
        if(obj_parent != null) obj_tmp.transform.SetParent(obj_parent.transform);
        obj_tmp.name = line_name;
        LineRenderer line = obj_tmp.GetComponent();

        line.startColor = color;
        line.endColor = color;
        line.startWidth = width;
        line.endWidth = width;

        line.positionCount = posSize;

        return line;
    }

    //Canvas
    GameObject AddCanvas()
    {
        GameObject obj = new GameObject("Canvas");
        obj.layer = LayerMask.NameToLayer("UI");
        Canvas canvas = obj.AddComponent();
        canvas.renderMode = RenderMode.ScreenSpaceOverlay;
        canvas.sortingOrder = 0;
        canvas.targetDisplay = 0;
        canvas.additionalShaderChannels = AdditionalCanvasShaderChannels.None;
        canvas.pixelPerfect = false;

        CanvasScaler canvas_scalar = obj.AddComponent();
        canvas_scalar.uiScaleMode = CanvasScaler.ScaleMode.ConstantPixelSize;
        canvas_scalar.scaleFactor = 1;
        canvas_scalar.referencePixelsPerUnit = 100;

        obj.AddComponent();

        RectTransform rect = obj.GetComponent();
        rect.anchorMin = Vector2.zero;
        rect.anchorMax = Vector2.one;
        rect.anchoredPosition = Vector2.zero;
        rect.sizeDelta = Vector2.zero;

        return obj;
    }

    //Text
    GameObject AddText(GameObject obj_parent, Vector2 pos, int font_size, string cont, string text_name)
    {
        GameObject obj_text = new GameObject(text_name);
        obj_text.transform.SetParent(obj_parent.transform);
        obj_text.layer = LayerMask.NameToLayer("UI");

        RectTransform rect_text = obj_text.AddComponent();
        rect_text.anchorMin = new Vector2(0.5f, 0.5f);
        rect_text.anchorMax = new Vector2(0.5f, 0.5f);
        rect_text.anchoredPosition = pos;
        rect_text.sizeDelta = new Vector2(160f, 30f);

        Text text = obj_text.AddComponent();
        text.raycastTarget = false;
        text.font = Resources.GetBuiltinResource("Arial.ttf");
        text.fontSize = font_size;
        text.text = cont;
        float c = 50f / 255f;
        text.color = new Color(c, c, c, 1f);

        return obj_text;
    }

    //Toggle
    Toggle AddToggle(GameObject obj_parent, Vector2 pos, string toggle_name, string label)
    {
        GameObject obj_toggle = new GameObject(toggle_name);
        obj_toggle.transform.SetParent(obj_parent.transform);
        obj_toggle.layer = LayerMask.NameToLayer("UI");

        RectTransform rect_toggle = obj_toggle.AddComponent();
        rect_toggle.anchoredPosition = pos;
        rect_toggle.sizeDelta = new Vector3(160f, 20f);
        Toggle toggle = obj_toggle.AddComponent();

        //background
        GameObject obj_bg = new GameObject("Background");
        obj_bg.transform.SetParent(obj_toggle.transform);
        obj_bg.layer = LayerMask.NameToLayer("UI");

        RectTransform rect_bg = obj_bg.AddComponent();
        rect_bg.anchorMin = new Vector2(0f, 1f);
        rect_bg.anchorMax = new Vector2(0f, 1f);
        rect_bg.anchoredPosition = new Vector2(10f, -10f);
        rect_bg.sizeDelta = new Vector2(20f, 20f);
        Image image_bg = obj_bg.AddComponent();
        image_bg.type = Image.Type.Sliced;
#if UNITY_EDITOR
        image_bg.sprite = UnityEditor.AssetDatabase.GetBuiltinExtraResource("UI/Skin/UISprite.psd");
#else
        image_bg.sprite = sprite_UISprite;
#endif

        //checkmark
        GameObject obj_mark = new GameObject("Checkmark");
        obj_mark.transform.SetParent(obj_bg.transform);
        obj_mark.layer = LayerMask.NameToLayer("UI");

        RectTransform rect_mark = obj_mark.AddComponent();
        rect_mark.anchorMin = new Vector2(0.5f, 0.5f);
        rect_mark.anchorMax = new Vector2(0.5f, 0.5f);
        rect_mark.anchoredPosition = Vector2.zero;
        rect_mark.sizeDelta = new Vector2(20f, 20f);

        Image image_mark = obj_mark.AddComponent();
#if UNITY_EDITOR
        image_mark.sprite = UnityEditor.AssetDatabase.GetBuiltinExtraResource("UI/Skin/Checkmark.psd");
#else
        image_mark.sprite = sprite_checkmark;
#endif

        //label
        GameObject obj_label = new GameObject("Label");
        obj_label.transform.SetParent(obj_toggle.transform);

        RectTransform rect_label = obj_label.AddComponent();
        rect_label.anchorMin = Vector2.zero;
        rect_label.anchorMax = Vector2.one;
        rect_label.offsetMin = new Vector2(23f, 1f);
        rect_label.offsetMax = new Vector2(-5f, -2f);

        Text text_label = obj_label.AddComponent();
        text_label.text = label;
        text_label.color = Color.black;
        text_label.font = Resources.GetBuiltinResource("Arial.ttf");

        toggle.targetGraphic = image_bg;
        toggle.graphic = image_mark;

        return toggle;
    }

    //Slider
    Slider AddSlider(GameObject obj_parent, Vector2 pos, string slider_name)
    {
        GameObject obj_slider = new GameObject(slider_name);
        obj_slider.transform.SetParent(obj_parent.transform);
        obj_slider.layer = LayerMask.NameToLayer("UI");

        RectTransform rect_slider = obj_slider.AddComponent();
        rect_slider.anchorMin = new Vector2(0.5f, 0.5f);
        rect_slider.anchorMax = new Vector2(0.5f, 0.5f);
        rect_slider.anchoredPosition = pos;
        rect_slider.sizeDelta = new Vector2(100f, 20f);

        Slider slider = obj_slider.AddComponent();

        //background
        GameObject obj_bg = new GameObject("Background");
        obj_bg.transform.SetParent(obj_slider.transform);
        obj_bg.layer = LayerMask.NameToLayer("UI");

        RectTransform rect_bg = obj_bg.AddComponent();
        rect_bg.anchorMin = new Vector2(0f, 0.25f);
        rect_bg.anchorMax = new Vector2(1f, 0.75f);
        rect_bg.anchoredPosition = Vector2.zero;
        rect_bg.sizeDelta = Vector2.zero;

        Image image_bg = obj_bg.AddComponent();
#if UNITY_EDITOR
        image_bg.sprite = UnityEditor.AssetDatabase.GetBuiltinExtraResource("UI/Skin/Background.psd");
#else
        image_bg.sprite = sprite_background;
#endif
        image_bg.type = Image.Type.Sliced;

        //fill area
        GameObject obj_fillarea = new GameObject("Fill Area");
        obj_fillarea.transform.SetParent(obj_slider.transform);
        obj_fillarea.layer = LayerMask.NameToLayer("UI");

        RectTransform rect_fillarea = obj_fillarea.AddComponent();
        rect_fillarea.anchorMin = new Vector2(0f, 0.25f);
        rect_fillarea.anchorMax = new Vector2(1f, 0.75f);
        rect_fillarea.anchoredPosition = new Vector3(-5f, 0f, 0f);
        rect_fillarea.sizeDelta = new Vector2(-20f, 0f);

        //fill
        GameObject obj_fill = new GameObject("Fill");
        obj_fill.transform.SetParent(obj_fillarea.transform);
        obj_fill.layer = LayerMask.NameToLayer("UI");

        RectTransform rect_fill = obj_fill.AddComponent();
        rect_fill.anchorMin = new Vector2(0f, 0f);
        rect_fill.anchorMax = new Vector2(0f, 1f);
        rect_fill.anchoredPosition = Vector2.zero;
        rect_fill.sizeDelta = new Vector2(10f, 0f);

        Image image_fill = obj_fill.AddComponent();
#if UNITY_EDITOR
        image_fill.sprite = UnityEditor.AssetDatabase.GetBuiltinExtraResource("UI/Skin/UISprite.psd");
#else
        image_fill.sprite = sprite_UISprite;
#endif
        image_fill.type = Image.Type.Sliced;

        //handle side area
        GameObject obj_hsarea = new GameObject("Handle Slide Area");
        obj_hsarea.transform.SetParent(obj_slider.transform);
        obj_hsarea.layer = LayerMask.NameToLayer("UI");

        RectTransform rect_hsarea = obj_hsarea.AddComponent();
        rect_hsarea.anchorMin = Vector2.zero;
        rect_hsarea.anchorMax = new Vector2(1f, 1f);
        rect_hsarea.anchoredPosition = Vector2.zero;
        rect_hsarea.sizeDelta = new Vector2(-20f, 0f);

        //handle
        GameObject obj_handle = new GameObject("Handle");
        obj_handle.transform.SetParent(obj_hsarea.transform);
        obj_handle.layer = LayerMask.NameToLayer("UI");

        RectTransform rect_handle = obj_handle.AddComponent();
        rect_handle.anchorMin = Vector2.zero;
        rect_handle.anchorMax = new Vector2(0f, 1f);
        rect_handle.anchoredPosition = Vector2.zero;
        rect_handle.sizeDelta = new Vector2(20f, 0f);

        Image image_handle = obj_handle.AddComponent();
#if UNITY_EDITOR
        image_handle.sprite = UnityEditor.AssetDatabase.GetBuiltinExtraResource("UI/Skin/Knob.psd");
#else
        image_handle.sprite = sprite_knob;
#endif

        slider.targetGraphic = image_handle;
        slider.fillRect = rect_fill;
        slider.handleRect = rect_handle;

        return slider;
    }

    //メインカメラの位置
    void CameraPosition()
    {
        float y = -0.25f * (Screen.height - 320f) / 280f + 0.55f;
        Camera.main.transform.position = new Vector3(graph_size.x * 0.5f - 0.16f, y, -5f);
    }

    float Rand(Vector2 st, int seed)
    {
        //return frac(sin(dot(st.xy, float2(12.9898, 78.233)) + seed) * 43758.5453123);
        float dt = Vector2.Dot(st, new Vector2(12.9898f, 78.233f));
        return (Mathf.Sin(dt + seed) * 43758.5453123f) % 1;
    }

    //エルミート曲線で補間
    float Noise(float x, int seed)
    {
        float ip = Mathf.Floor(x);
        float fe = x - ip;

        float w0 = Rand(new Vector2(ip, 0.2f), seed) * fe;
        float w1 = Rand(new Vector2(ip, 0.2f) + new Vector2(1f, 0), seed) * (fe - 1f);

        float u = fe * fe * (3 - 2 * fe);

        return Mathf.Lerp(w0, w1, u);
    }

    Vector2 Grad(float x, int seed)
    {
        float ip = Mathf.Floor(x);

        float w0 = Rand(new Vector2(ip, 0.2f), seed);
        float w1 = Rand(new Vector2(ip, 0.2f) + new Vector2(1f, 0), seed);

        return new Vector2(w0, w1);
    }

    //ウェーブレット関数を線形補間
    float Noise2(float x, int seed)
    {
        float ip = Mathf.Floor(x);
        float fe = x - ip;

        float w0 = Rand(new Vector2(ip, 0.2f), seed) * fe * Func(fe);
        float w1 = Rand(new Vector2(ip, 0.2f) + new Vector2(1f, 0), seed) * (fe - 1f) * Func(fe - 1f);

        return Mathf.Lerp(w0, w1, fe);
    }

    float Func(float fe)
    {
        return 1f - 3f * fe * fe + 2f * Mathf.Abs(fe * fe * fe);
    }

    //ウェーブレット関数
    Vector2 Wavelet(float x, int seed)
    {
        float ip = Mathf.Floor(x);
        float fe = x - ip;

        float w0 = Rand(new Vector2(ip, 0.2f), seed) * fe * Func(fe);
        float w1 = Rand(new Vector2(ip, 0.2f) + new Vector2(1f, 0), seed) * (fe - 1f) * Func(fe - 1f);

        return new Vector2(w0, w1);
    }

    //ノイズを描画
    void DrawNoize(int seed)
    {
        float w = (float)1f / step;
        float size = graph_size.x / range;
        for (int i = 0; i < line_easeNoize.positionCount; i++)
        {
            float x = i * w;
            line_easeNoize.SetPosition(i, new Vector3(x * size, Noise(x, seed) * graph_size.y * scale, 0f));
            line_waveNoize.SetPosition(i, new Vector3(x * size, Noise2(x, seed) * graph_size.y * scale, 0f));
        }

        //L(x)
        for (int j = 0; j < range; j++)
        {
            Vector2 grad = Grad(j, seed);
            float x_l = 1f;
            float g_l = grad.x * graph_size.y * scale * 1f;
            if (Mathf.Abs(g_l) > graph_size.y)
            {
                x_l = 1f / (Mathf.Abs(grad.x) * scale);
                g_l = grad.x * graph_size.y * scale * x_l;
            }
            line_grad[j].SetPosition(0, new Vector3(j * size, 0));
            line_grad[j].SetPosition(1, new Vector3((j + x_l) * size, g_l));

            float x_r = -1f;
            float g_r = grad.y * graph_size.y * scale * -1f;
            if (Mathf.Abs(g_r) > graph_size.y)
            {
                x_r = -1f / (Mathf.Abs(grad.y) * scale);
                g_r = grad.y * graph_size.y * scale * x_r;
            }
            line_grad[j + range].SetPosition(0, new Vector3((j + 1f + x_r) * size, g_r));
            line_grad[j + range].SetPosition(1, new Vector3((j + 1f) * size, 0f));
        }

        for (int k = 0; k < line_wave[0].positionCount; k++)
        {
            float x = k * w;
            Vector2 grad = Wavelet(x, seed);

            line_wave[0].SetPosition(k, new Vector3(x * size, grad.x * graph_size.y * scale, 0f));
            line_wave[1].SetPosition(k, new Vector3(x * size, grad.y * graph_size.y * scale, 0f));
        }

    }

    // Update is called once per frame
    void Update () {
        if (screenSize.x != Screen.width || screenSize.y != Screen.height)
        {
            GraphLine();
            DrawNoize((int)slider_seed.value);
            screenSize = new Vector2(Screen.width, Screen.height);
            CameraPosition();
            ScaleText();
        }
    }
}

実行結果

 トグルを操作することで表示するグラフを変更できます。hermiteが直線をエルミート補間する方法で求められるノイズを、\(L(x)\)はノイズを求めるときに使用した直線を表示します。また、waveletはウェーブレット関数を線形補間することで求められるノイズを、\(W(x)\)はウェーブレット関数を表示します。

二次元パーリンノイズ

 二次元パーリンノイズは一次元の時と同様にして求めることができます。初めに、\(x\)及び\(y\)の少数部分を取り出すことで、下図に示すように格子状に区切ります。

一次元の場合は直線を補間しノイズを求めましたが、二次元の場合は平面を補間することでノイズを求めます。平面の式は以下の通りです。

$$ \begin{align} w(x,y)&=a_{x}\cdot x+a_{y}\cdot y\\ &=(a_{x},a_{y})\cdot (x,y) \end{align} $$

\(a_{x}\)及び\(a_{y}\)はそれぞれランダムな値を示しています。平面の方程式は上式のように、ランダムな値のベクトルと格子点から格子内にある任意の点に向かうベクトルの内積で表すことができます。よって、ランダムな値で構成されるグラディエントと距離ベクトルの内積を格子点ごとに求め、それらを補間することでノイズが求まることがわかります。この式を用いて、ある点\(P\)における値を求めます。下図は点\(P\)のある格子を示しています。

先ほどの式より格子点ごとに\(w\)を求めます。 それぞれの格子点から点\(P\)へ向かう距離ベクトルは以下の式で表されます。

$$ \begin{align} &\vec{d_{00}}=(frac(x),frac(y))\\ &\vec{d_{10}}=(frac(x)-1,frac(y))\\ &\vec{d_{01}}=(frac(x),frac(y)-1)\\ &\vec{d_{11}}=(frac(x)-1,frac(y)-1)\\ \end{align} $$

これより、格子点ごとの\(w\) は以下のようになります。

$$ \begin{align} &w_{00}(x,y)=(a_{00x},a_{00y})\cdot (frac(x),frac(y))\\ &w_{10}(x,y)=(a_{10x},a_{10y})\cdot (frac(x)-1,frac(y))\\ &w_{01}(x,y)=(a_{01x},a_{01y})\cdot (frac(x),frac(y)-1)\\ &w_{11}(x,y)=(a_{11x},a_{11y})\cdot (frac(x)-1,frac(y)-1)\\ \end{align} $$

この式より求められる値をエルミート補間することでノイズが求まります。よって、ノイズは以下の式より求めることができます。

$$ \begin{align} &f_{x}=frac(x),f_{y}=frac(y)\\ &u_{x}=3f_{x}^{2}-2f_{x}^{3},u_{y}=3f_{y}^{2}-2f_{y}^3\\ &n=lerp\{lerp(w_{00},w_{10},u_{x}),lerp(w_{01},w_{11},u_{x}),u_{y}\} \end{align} $$

shader

 二次元パーリンノイズのシェーダを以下に示します。uv座標に定数を掛けることにより格子数を決定しています。

Shader "Noise/PerlinNoise"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
		_Seed ("SeedX", Int) = 0
		_SizeX ("SizeX", Int) = 1
		_SizeY ("SizeY", Int) = 1
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }

		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			
			#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;
			
			int _Seed;
			int _SizeX;
			int _SizeY;
			
			float2 rand(float2 st, int seed)
			{
				float2 s = float2(dot(st, float2(127.1, 311.7)) + seed, dot(st, float2(269.5, 183.3)) + seed);
				return -1 + 2 * frac(sin(s) * 43758.5453123);
			}

			float noise(float2 st, int seed)
			{
				float2 p = floor(st);
				float2 f = frac(st);

				float w00 = dot(rand(p, seed), f);
				float w10 = dot(rand(p + float2(1, 0), seed), f - float2(1, 0));
				float w01 = dot(rand(p + float2(0, 1), seed), f - float2(0, 1));
				float w11 = dot(rand(p + float2(1, 1), seed), f - float2(1, 1));
				
				float2 u = f * f * (3 - 2 * f);

				return lerp(lerp(w00, w10, u.x), lerp(w01, w11, u.x), u.y);
			}

			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				fixed4 col;
				col.rgb = noise(float2(i.uv.x * _SizeX, i.uv.y * _SizeY), _Seed) * 0.5 + 0.5;
				col.a = 1;
				return col;
			}
			ENDCG
		}
	}
}

実行結果

 上記シェーダの実行結果は以下の通りとなります。

参考サイト

○×(まるぺけ)つくろーどっとコム:その3 パーリンノイズとフラクタル ~ フラクタブルな地形を作る基盤 ~

The Book of Shaders:ノイズ

POSTD:パーリンノイズを理解する