質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

90.53%

  • C#

    8820questions

    C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

  • Unity

    5317questions

    Unityは、ユニティテクノロジーが開発したゲームエンジンです。 主にモバイルやブラウザ向けのゲーム製作に利用されていましたが、3Dの重力付きゲームが簡単に作成できることから需要が増え、現在はマルチプラットフォームに対応しています。 言語はC言語/C++で書かれていますが、C#、JavaScript、Booで書かれたコードにも対応しています。

  • Unity2D

    1227questions

  • ゲーム開発

    205questions

Unity2D テクスチャブレンド

解決済

回答 1

投稿

  • 評価
  • クリップ 0
  • VIEW 92

t2t21212

score 31

前提・実現したいこと

メッシュに適応しているテクスチャをブレンドするシェーダーを作り、自然な見た目の地形表現をしたい。

該当のソースコード

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

public class World : MonoBehaviour
{

    public int width;
    public int height;

    public Tile[,] tiles;

    public Material material;

    Mesh mesh;

    public string seed;
    public bool randomSeed;

    public float frequency;
    public float amplitude;

    public float lacunarity;
    public float persistance;

    public int octaves;

    Noise noise;

    private void Awake()
    {
        if(randomSeed == true)
        {
            int value = Random.Range(-1000, 10000);
            seed = value.ToString();
        }

        noise = new Noise(seed.GetHashCode(), frequency, amplitude, lacunarity, persistance, octaves);
    }

    // Use this for initialization
    void Start()
    {
        CreateTiles();
        GenerateMesh();
    }

    // Update is called once per frame
    void Update()
    {
        Graphics.DrawMesh(mesh, Vector3.zero, Quaternion.identity, material, 0);
    }

    void CreateTiles()
    {
        tiles = new Tile[width, height];

        float[,] noiseValues = noise.GetNoiseValues(width, height);

        for (int i = 0; i < width; i++)
        {
            for (int j = 0; j < height; j++)
            {
                if(noiseValues[i,j] > 0.5f)
                {
                    tiles[i, j] = new Tile(Tile.Type.Grass);
                }
                else
                {
                    tiles[i, j] = new Tile(Tile.Type.Dirt);
                }
            }
        }
    }

    void GenerateMesh()
    {
        MeshData data = new MeshData(tiles);
        mesh = new Mesh();

        // 頂点インデックスのフォーマットを32ビット整数に変更する
        mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;

        mesh.vertices = data.vertices.ToArray();
        mesh.triangles = data.triangles.ToArray();
        mesh.uv = data.UVs.ToArray();
        mesh.RecalculateNormals();
        mesh.RecalculateBounds();
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MeshData{

    public List<Vector3> vertices;
    public List<int> triangles;
    public List<Vector2> UVs;

    public MeshData(Tile[,] data)
    {
        vertices = new List<Vector3>();
        triangles = new List<int>();
        UVs = new List<Vector2>();

        for(int i = 0; i < data.GetLength(0); i++)
        {
            for(int j = 0;j < data.GetLength(1); j++)
            {
                CreateSquare(data[i, j], i, j);
            }
        }
    }

    void CreateSquare(Tile tile,int x,int y)
    {
        vertices.Add(new Vector3(x + 0, y + 0));
        vertices.Add(new Vector3(x + 1, y + 0));
        vertices.Add(new Vector3(x + 0, y + 1));
        vertices.Add(new Vector3(x + 1, y + 1));

        triangles.Add(vertices.Count - 1);
        triangles.Add(vertices.Count - 3);
        triangles.Add(vertices.Count - 4);

        triangles.Add(vertices.Count - 2);
        triangles.Add(vertices.Count - 1);
        triangles.Add(vertices.Count - 4);

        UVs.AddRange(SpriteLoader.instance.GetTileUVs(tile));
    }

}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Tile{

    public enum Type { Dirt,Grass }
    public Type type;

    public Tile(Type type)
    {
        this.type = type;
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Noise {

    int seed;

    float frequency;
    float amplitude;

    float lacunarity;
    float persistance;

    int octaves;

    public Noise(int seed,float frequency,float amplitude,float lacunarity,float persistance,int octaves)
    {
        this.seed = seed;
        this.frequency = frequency;
        this.amplitude = amplitude;
        this.lacunarity = lacunarity;
        this.persistance = persistance;
        this.octaves = octaves;
    }

    public float [,] GetNoiseValues(int width,int height)
    {
        float[,] noiseValues = new float[width, height];

        float max = 0f;
        float min = float.MaxValue;

        for(int i = 0; i < width; i++)
        {
            for(int j =0; j < height; j++)
            {
                noiseValues[i, j] = 0f;

                float tempA = amplitude;
                float tempF = frequency;

                for(int k = 0;k < octaves; k++)
                {
                    noiseValues[i, j] += Mathf.PerlinNoise((i + seed) / (float)width * frequency, j / (float)height * frequency) * amplitude;
                    frequency *= lacunarity;
                    amplitude *= persistance;
                }

                amplitude = tempA;
                frequency = tempF;

                if(noiseValues[i,j] > max)
                {
                    max = noiseValues[i, j];
                }

                if(noiseValues[i,j] < min)
                {
                    min = noiseValues[i, j];
                }
            }
        }

        for (int i = 0; i < width; i++)
        {
            for (int j = 0; j < height; j++)
            {

                noiseValues[i, j] = Mathf.InverseLerp(max, min, noiseValues[i, j]);
            }
        }

        return noiseValues;
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;

public class SpriteLoader : MonoBehaviour
{

    public static SpriteLoader instance;

    Dictionary<string, Vector2[]> tileUVMap;

    // Use this for initialization
    void Awake()
    {

        instance = this;

        tileUVMap = new Dictionary<string, Vector2[]>();

        Sprite[] sprites = Resources.LoadAll<Sprite>("World Tiles");

        float imageWidth = 0f;
        float imageHeight = 0f;

        foreach (Sprite s in sprites)
        {

            if (s.rect.x + s.rect.width > imageWidth)
                imageWidth = s.rect.x + s.rect.width;

            if (s.rect.y + s.rect.height > imageHeight)
                imageHeight = s.rect.y + s.rect.height;
        }

        foreach (Sprite s in sprites)
        {

            Vector2[] uvs = new Vector2[4];

            uvs[0] = new Vector2(s.rect.x / imageWidth, s.rect.y / imageHeight);
            uvs[1] = new Vector2((s.rect.x + s.rect.width) / imageWidth, s.rect.y / imageHeight);
            uvs[2] = new Vector2(s.rect.x / imageWidth, (s.rect.y + s.rect.height) / imageHeight);
            uvs[3] = new Vector2((s.rect.x + s.rect.width) / imageWidth, (s.rect.y + s.rect.height) / imageHeight);

            tileUVMap.Add(s.name, uvs);

            //Debug.Log(s.name + " :" + uvs[0] + "," + uvs[1] + "," + uvs[2] + "," + uvs[3]);
        }

    }

    // Update is called once per frame
    void Update()
    {

    }

    public Vector2[] GetTileUVs(Tile tile)
    {

        string key = tile.type.ToString();

        if (tileUVMap.ContainsKey(key) == true)
        {

            return tileUVMap[key];
        }
        else
        {

            Debug.LogError("There is no UV map for tile type: " + key);
            return tileUVMap["Void"];
        }
    }
}

試したこと

テクスチャのあるメッシュを生成しましたが、どうやれば目的のシェーダーを作れるのかが分かりませんでした。

![イメージ説明](f3724da58c717697379d711cc49660bf.png)
イメージ説明

理想↓
イメージ説明

回答お願いします。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

+1

たとえば、タイルおよびメッシュに前景・背景情報とそれらのブレンド率を追加して、描画時にはそれに基づいて前景と背景のグラフィックを混ぜ合わせて表示してやる方法が考えられるでしょう。

まず、Tileは下記のようにして...

public class Tile{

    public enum Type { Dirt, Grass }
    public Type type; // ゲームロジックのための地形タイプ
    public Type foregroundType; // 描画のための前景の地形タイプ
    public Type backgroundType; // 描画のための背景の地形タイプ
    public Vector4 blendWeights; // 描画のための前景・背景混合率(x、y、z、wが左下、右下、左上、右上の角に対応)

    public Tile(Type type, Type foreground, Type background, Vector4 weights)
    {
        this.type = type;
        this.foregroundType = foreground;
        this.backgroundType = background;
        this.blendWeights = weights;
    }
}

WorldCreateTilesは下記のように変え...

void CreateTiles()
{
    this.tiles = new Tile[this.width, this.height];

    // ノイズのサイズを縦横1だけ広げて、それをタイル中心ではなくタイル四隅における値として利用する
    float[,] noiseValues = this.noise.GetNoiseValues(this.width + 1, this.height + 1);

    for (int i = 0; i < this.width; i++)
    {
        for (int j = 0; j < this.height; j++)
        {
            // 4点のノイズ値を取得して、それを四隅における前景・背景混合率とし...
            Vector4 weights = new Vector4(noiseValues[i, j], noiseValues[i + 1, j], noiseValues[i, j + 1], noiseValues[i + 1, j + 1]);

            // 4点の平均値を中心の値として、それをもとに地形タイプを決定し...
            float averageWeight = Vector4.Dot(weights, Vector4.one) * 0.25f;
            Tile.Type type = (averageWeight > 0.5f) ? Tile.Type.Grass : Tile.Type.Dirt;

            // 前景を草地、背景を裸地としてタイルを生成する
            this.tiles[i, j] = new Tile(type, Tile.Type.Grass, Tile.Type.Dirt, weights);
        }
    }
}

SpriteLoaderGetTileUVsは、引数としてタイルではなく地形タイプを受け取るようにし...

public Vector2[] GetTileUVs(Tile.Type type)
{

    string key = type.ToString();

    if (this.tileUVMap.ContainsKey(key) == true)
    {

        return this.tileUVMap[key];
    }
    else
    {

        Debug.LogError("There is no UV map for tile type: " + key);
        return this.tileUVMap["Void"];
    }
}

MeshDataにも追加情報を持たせ...

public class MeshData
{

    public List<Vector3> vertices;
    public List<int> triangles;
    public List<Vector4> UVs; // Vector4に変更し、xyに前景、zwに背景のUVを格納することにする
    public List<Color> colors; // 混合率は色として表現する(UV2以降を使ってもいいと思います)

    public MeshData(Tile[,] data)
    {
        vertices = new List<Vector3>();
        triangles = new List<int>();
        UVs = new List<Vector4>();
        colors = new List<Color>();

        for (int i = 0; i < data.GetLength(0); i++)
        {
            for (int j = 0;j < data.GetLength(1); j++)
            {
                CreateSquare(data[i, j], i, j);
            }
        }
    }

    void CreateSquare(Tile tile, int x, int y)
    {
        vertices.Add(new Vector3(x + 0, y + 0));
        vertices.Add(new Vector3(x + 1, y + 0));
        vertices.Add(new Vector3(x + 0, y + 1));
        vertices.Add(new Vector3(x + 1, y + 1));

        triangles.Add(vertices.Count - 1);
        triangles.Add(vertices.Count - 3);
        triangles.Add(vertices.Count - 4);

        triangles.Add(vertices.Count - 2);
        triangles.Add(vertices.Count - 1);
        triangles.Add(vertices.Count - 4);

        // 前景・背景についてそれぞれUVを取得し、合成してUVsに追加する
        // (Zipで合成しましたが、この場合「using System.Linq;」が必要になります)
        Vector2[] foregroundUVs = SpriteLoader.instance.GetTileUVs(tile.foregroundType);
        Vector2[] backgroundUVs = SpriteLoader.instance.GetTileUVs(tile.backgroundType);
        UVs.AddRange(foregroundUVs.Zip(backgroundUVs, (f, b) => new Vector4(f.x, f.y, b.x, b.y)));

        // 混合率はA成分に埋め込む
        // RGBに他の情報を埋め込む余地があります
        // 例えばランダムな色を入れてやり、描画時にそれを利用することで地形に色ムラをつけるとか...
        for (int i = 0; i < 4; i++)
        {
            colors.Add(new Color(0.0f, 0.0f, 0.0f, tile.blendWeights[i]));
        }
    }
}

そしてWorldGenerateMeshも変更します。

void GenerateMesh()
{
    MeshData data = new MeshData(this.tiles);
    this.mesh = new Mesh();

    // 頂点インデックスのフォーマットを32ビット整数に変更する
    this.mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;

    this.mesh.vertices = data.vertices.ToArray();
    this.mesh.triangles = data.triangles.ToArray();

    // uvへの代入だとVector2しか受け付けてくれないので、代わりにSetUVsを使う
    this.mesh.SetUVs(0, data.UVs);

    // 混合率もセットする
    this.mesh.colors = data.colors.ToArray();

    this.mesh.RecalculateNormals();
    this.mesh.RecalculateBounds();
}

Worldにセットするマテリアルについては、単純には下記のような合成でいいかと思います。

Shader "Unlit/World"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _EdgeSoftness ("Edge Softness", Range(0.0, 1.0)) = 0.05
    }

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

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float4 uv : TEXCOORD0;
                float4 color : COLOR;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float4 uv : TEXCOORD0;
                float weight : TEXCOORD1;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _EdgeSoftness;

            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = float4(TRANSFORM_TEX(v.uv.xy, _MainTex), TRANSFORM_TEX(v.uv.zw, _MainTex));
                o.weight = v.color.a;
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                fixed4 foreColor = tex2D(_MainTex, i.uv.xy);
                fixed4 backColor = tex2D(_MainTex, i.uv.zw);
                float smoothingWidth = _EdgeSoftness * 0.5;
                float ratio = smoothstep(0.5 - smoothingWidth, 0.5 + smoothingWidth, i.weight);
                return lerp(backColor, foreColor, ratio);
            }
            ENDCG
        }
    }
}

結果1

ですがご提示の理想図を見ますと、境界部分にフラクタルノイズっぽい乱れがあるようにも見えます。一例としては、下記のようにするとそれに近づけられるのではないでしょうか?

Shader "Unlit/World"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _EdgeSoftness ("Edge Softness", Range(0.0, 1.0)) = 0.025
        _EdgeDist ("Edge Disturbance", Range(0.0, 1.0)) = 0.05
        _EdgeDistFreq ("Edge Disturbance Frequency", Range(0.0, 16.0)) = 4.0
    }

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

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float4 uv : TEXCOORD0;
                float4 color : COLOR;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float4 uv : TEXCOORD0;
                float weight : TEXCOORD1;
                float2 modelPos : TEXCOORD2;
            };

            // https://thebookofshaders.com/11/ の方法によるバリューノイズの生成
            inline float random(float2 st) {
                return frac(sin(dot(st, float2(12.9898, 78.233))) * 43758.5453123);
            }

            float noise(float2 st)
            {
                float2 i = floor(st);
                float2 f = frac(st);
                float a = random(i);
                float b = random(i + float2(1.0, 0.0));
                float c = random(i + float2(0.0, 1.0));
                float d = random(i + float2(1.0, 1.0));
                float2 u = f * f * (3.0 - 2.0 * f);

                return dot(float2(lerp(a, b, u.x), lerp(c - a, d - b, u.x)), float2(1.0, u.y));
            }

            // バリューノイズを適当に重ね合わせてフラクタルっぽくする
            float fractal(float2 st)
            { 
                float4 amp = float4(1.0, 0.5, 0.25, 0.125);
                float4 v;

                v.x = noise(st);
                st = st * 2.0 + float2(14.1421356237, 17.3205080757);
                v.y = noise(st);
                st = st * 2.0 + float2(22.360679775, 26.4575131106);
                v.z = noise(st);
                st = st * 2.0 + float2(31.4159265359, 27.1828182846);
                v.w = noise(st);

                return dot(v, amp) / dot(1.0, amp);
            }

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _MainTex_TexelSize;
            float _EdgeSoftness;
            float _EdgeDist;
            float _EdgeDistFreq;

            v2f vert(appdata v)
            {
                v2f o;
                o.uv = float4(TRANSFORM_TEX(v.uv.xy, _MainTex), TRANSFORM_TEX(v.uv.zw, _MainTex));
                o.weight = v.color.a;
                o.modelPos = v.vertex.xy;
                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                fixed4 foreColor = tex2D(_MainTex, i.uv.xy);
                fixed4 backColor = tex2D(_MainTex, i.uv.zw);
                float smoothingWidth = _EdgeSoftness * 0.5;
                float f = dot(float2(_EdgeDist, 1.0 - _EdgeDist), float2(fractal(i.modelPos * _EdgeDistFreq), 0.5));
                float upper = 1.0 - 2.0 * (1.0 - i.weight) * (1.0 - f);
                float lower = 2.0 * i.weight * f;
                float weight = lerp(lower, upper, step(0.5, i.weight));
                float ratio = smoothstep(0.5 - smoothingWidth, 0.5 + smoothingWidth, weight);
                return lerp(backColor, foreColor, ratio);
            }
            ENDCG
        }
    }
}

結果2

※実験中に気付いたのですが、シードの値によってはノイズパターンの水平方向周波数が極端に小さくなってしまうことがあるようでしたので、さらに下記のような変更を加えています。

public float [,] GetNoiseValues(int width,int height)
{
    float[,] noiseValues = new float[width, height];

    float max = 0f;
    float min = float.MaxValue;

    // seedに極端に大きな値が入ると、(i + seed) / (float)width * frequencyの部分で
    // 精度不足となるようだったため、適当に小さくしました
    seed %= 1024;

    // 省略

    return noiseValues;
}

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/04/13 21:16

    丁寧な回答ありがとうございます。本当に勉強になります。
    コードの内容はまだ完全に理解出来ていませんが、理解出来るように勉強しようと思います。

    キャンセル

  • 2019/04/15 21:20

    ベストアンサーを選んでから質問して申し訳ありません。
    TileのTypeを二種類以上に増やす場合は、どうコードを書けばよいのでしょうか?
    二種類以上の時、foregroundTypeとbackgroundTypeに、なにを指定すればいいか分かりません。
    もしよければ教えていただきたいです。

    キャンセル

  • 2019/04/16 06:39

    なるほど、ちょっとやっかいそうな問題ですね...
    今回の2地形選択だとノイズ値に対する地形を [0.0←土→0.5←草→1.0] という具合に割り当てましたが、もしこれを単純に多段階化して、たとえば [0.0←水→0.25←土→0.5←草→0.75←岩→1.0] という風に増やすのであれば、隣り合った地形...水・土、土・草、草・岩をそれぞれ背景・前景とすることである程度は対応できそうに思います(多段階対応のために少々コードを修正することになるでしょうが、おそらく大きくは変わらないでしょう)。
    ですがもっと複雑な地形割り当てを行いたい場合は、どの地形とどの地形が隣り合うかわからないので描画戦略を見直すことになりそうです。
    それら複数種類の地形をどのように割り当てるのかご説明いただければ(可能であれば、当初ご提示いただいたようなタイルがきっちり分かれた状態でかまいませんので、スクリプトをご提示いただけるとありがたいです)、もしいい案が思いつきましたら追記いたします。

    キャンセル

  • 2019/04/16 06:44

    おっとすみません、新しい関連質問(https://teratail.com/questions/184671 )をご投稿いただいているのを見逃していました。そちらも見てみようと思います。

    キャンセル

同じタグがついた質問を見る

  • C#

    8820questions

    C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

  • Unity

    5317questions

    Unityは、ユニティテクノロジーが開発したゲームエンジンです。 主にモバイルやブラウザ向けのゲーム製作に利用されていましたが、3Dの重力付きゲームが簡単に作成できることから需要が増え、現在はマルチプラットフォームに対応しています。 言語はC言語/C++で書かれていますが、C#、JavaScript、Booで書かれたコードにも対応しています。

  • Unity2D

    1227questions

  • ゲーム開発

    205questions