8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Unity #3Advent Calendar 2019

Day 14

【Unity】ワールド座標によって三つのテクスチャを滑らかに切り替えるシェーダー

Posted at

はじめに

本記事はUnity #3 Advent Calendar 201912月14日の記事です。

みなさんはステージ上で、チームによって動的にテクスチャを変えたいと思ったことはありませんか?
私はありました。

ということで、本記事では、チームによってステージのテクスチャを変えるために私が使用した方法を紹介しようと思います。(強引な導入)

方法は簡単で、通常のレンダリングに利用されるテクスチャやパラメータを3セット用意し、選択マップで3セット分のテクスチャやパラメータをブレンドするという力業なものです。

なお、本記事で説明するシェーダーは、昔からあるUnity built-in render pipelineを対象としたもので、Scriptable Render Pipeline(SRP)は対象としていません。
(SRPで使えるShader Graphを使えば本記事の内容はコードを書かずに簡単に実現できるかもしれないので、機会があったらShader Graphバージョンの記事を書くかもしれない)

確認環境

  • Unity2019.2.12f1
  • Windows10

概要

以下の画像の上段のような3つのテクスチャと、それぞれをRGBチャンネルに対応付けた下段のような選択マップを用意して……
triple_standard_shader1.png

これらのテクスチャからこのように表示されるようにしたい。
triple_standard_shader2.jpg

選択マップの赤部分に左のテクスチャが、緑部分に真ん中のテクスチャが、青部分に右のテクスチャが表示されているのが分かります。

選択マップはワールド座標におけるxz平面に対応しており、位置によってテクスチャを切り替えています。この性質からこんなことができます。
triple_standard_shader3.gif

動かしているボックスのテクスチャが位置によって変わっているのが分かると思います。

この記事ではこの動作を実現するシェーダーの実装方法について説明します。

方針

Unityでは主に以下の種類のシェーダーがあります。
より詳細な説明はNEAREAL「Unity の Shader の種類:超速Unityシェーダ入門(1)」が参考になります。

  • Surface Shader: 簡単なコードを書くだけで内部で複雑なシェーダーを生成してくれるシェーダー
  • Vertex, Fragment Shader: 独自にカスタマイズしたシェーダーを作れる。Surface Shaderでは実現できないような処理が書きたいときなどに使う
  • Compute Shader: GPUで計算を行うためのシェーダー

本記事ではそのお手軽さから、Surface Shaderを使用します。

実装

基本的なSurface Shader

まず、基本的な機能を備えた物理ベースシェーダーの実装を確認します。
StandardシェーダーでRendering ModeOpaqueに設定されている場合に相当するものです。

Surface Shaderを使用すると簡単に書くことができます。

StandardOpaque.shader
Shader "Custom/StandardOpaque" {
    Properties {
        _Color("Color", Color) = (1,1,1,1)
        _MainTex("Albedo (RGB)", 2D) = "white" {}
        _Glossiness("Smoothness", Range(0,1)) = 0.5
        [Gamma] _Metallic("Metallic Scale", Range(0,1)) = 0.0
        [NoScaleOffset]_MetallicGlossMap("Metallic", 2D) = "white" {}
        _BumpScale("Normal Scale", Float) = 1.0
        [NoScaleOffset][Normal] _BumpMap("Normal Map", 2D) = "bump" {}
        [HDR] _EmissionColor("Emittion Color", Color) = (0, 0, 0, 0)
        [NoScaleOffset] _EmissionTex("Emission", 2D) = "white" {}
        _OcclusionStrength("Occlusion Strength", Range(0.0, 1.0)) = 1.0
        [NoScaleOffset] _OcclusionMap("Occlusion", 2D) = "white" {}
    }

    SubShader {
        Tags { "Queue"="Geometry" "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM

        #pragma surface surf Standard fullforwardshadows
        #pragma target 3.0

        fixed4 _Color;
        sampler2D _MainTex;
        half _BumpScale;
        sampler2D _BumpMap;
        fixed4 _EmissionColor;
        sampler2D _EmissionTex;
        half _OcclusionStrength;
        sampler2D _OcclusionMap;
        half _Glossiness;
        half _Metallic;
        sampler2D _MetallicGlossMap;

        struct Input {
            float2 uv_MainTex;
        };

        void surf (Input IN, inout SurfaceOutputStandard o) {
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            // Bump Map
            fixed4 n = tex2D(_BumpMap, IN.uv_MainTex);
            o.Normal = UnpackScaleNormal(n, _BumpScale);
            // Emission
            fixed4 e = tex2D(_EmissionTex, IN.uv_MainTex);
            o.Emission = _EmissionColor * e;
            // Occlusion (合ってるかわからない)
            fixed4 oc = tex2D(_OcclusionMap, IN.uv_MainTex);
            o.Occlusion = oc * _OcclusionStrength;
            // Metallic and smoothness come from slider variables
            fixed4 m = tex2D(_MetallicGlossMap, IN.uv_MainTex);
            o.Metallic = m * _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Standard"
}

本記事の説明で必要な部分を中心に説明していきます。
本記事で説明していないSurface Shaderのより詳しい説明は、LIGHT11「【Unity】Surface Shaderの基本を総まとめ!難しい計算はUnity任せでサクッとシェーダ作成」@IT「Unityで始めるシェーダー入門」が参考になります。

まず、Propertiesブロックで、Unityのインスペクタで設定できるプロパティを定義しています。

続いて、SubShaderブロックではシェーダー本体のコードや設定が書かれています。

以下のコードは、どのようなSurface Shaderを生成するか設定しています。今回は物理ベースですべてのライトシャドウをサポートするよう設定しています。加えて、surf()を実際の処理時に呼びされるシェーダー関数として設定しています。

SurfaceShader設定
#pragma surface surf Standard fullforwardshadows

その下で、シェーダープログラム内で使用する変数を定義しています。
Propertyブロックで定義した変数はここで定義することで、プログラム内で使用可能になります。

次にInput構造体ですが、これはシェーダー関数(今回はsurf())の入力として使用されるものです。
処理対象のピクセルなどの情報が入っています。あらかじめ決められたものを定義することで、適切な値が格納されます。ここで定義しているuv_MainTexには、メインテクスチャ上のUV座標が格納されます。

そして、このシェーダーの本体であるsurf()です。上で説明した#pragma surface surf ...の部分で指定されているものです。
説明したInput型の変数を入力として受け取り、SurfaceOutputStandard 型のとして受け取った変数へ出力する計算結果を格納します。
surf()内では出力変数の各値について、計算して格納しています。それぞれの詳細は本記事では触れないので、参考サイトなどを参照してください。

三種類のテクスチャを扱う

本題に入ります。
まずは、単純に前の章で使用したPropertyブロックの項目をそれぞれ3つ分用意します。

ここで、全ての項目を3つづつ用意すると、テクスチャプロパティ数がDirectX11における上限である16個を超えてしまいます。
そのため、法線マップ、金属光沢マップ、オクルージョンマップは共通のものを使用するようにしています。

また、三種類のテクスチャをどのように反映するかを選択するための、選択テクスチャ_selectionMapと選択に用いるワールド空間の領域_SelectionAreaを加え、プロパティは以下のようになります。_selectionMap_SelectionAreaについては下で説明します。

Propertiesブロック
Properties{
    [NoScaleOffset] _SelectionMap("Selection Map", 2D) = "red" {}
    _SelectionArea("Selection Area (MinX, MinZ, MaxX, MaxZ)", Vector) = (0, 0, 100, 100)

    _Cutoff("Alpha Cutoff (Valid In Cutout Mode)", Range(0,1)) = 0.5

    _Color("[1] Color", Color) = (1,1,1,1)
    _MainTex("[1] Albedo (RGB)", 2D) = "white" {}
    _Glossiness("[1] Smoothness", Range(0,1)) = 0.5
    [Gamma] _Metallic("[1] Metallic Scale", Range(0,1)) = 0.0
    [NoScaleOffset]_MetallicGlossMap("[1] Metallic", 2D) = "white" {}
    _BumpScale("[1] Normal Scale", Float) = 1.0
    [NoScaleOffset][Normal] _BumpMap("[1] Normal Map", 2D) = "bump" {}
    [HDR] _EmissionColor("[1] Emittion Color", Color) = (0, 0, 0, 0)
    [NoScaleOffset] _EmissionTex("[1] Emission", 2D) = "white" {}
    _OcclusionStrength("[1] Occlusion Strength", Range(0.0, 1.0)) = 1.0
    [NoScaleOffset] _OcclusionMap("[1] Occlusion", 2D) = "white" {}

    _Color2("[2] Color", Color) = (1,1,1,1)
    [NoScaleOffset] _MainTex2("[2] Albedo (RGB)", 2D) = "white" {}
    _Glossiness2("[2] Smoothness", Range(0,1)) = 0.5
    [Gamma] _Metallic2("[2] Metallic Scale", Range(0,1)) = 0.0
    [HDR] _EmissionColor2("[2] Emittion Color", Color) = (0, 0, 0, 0)
    [NoScaleOffset] _EmissionTex2("[2] Emission", 2D) = "white" {}
    _OcclusionStrength2("[2] Occlusion Strength", Range(0.0, 1.0)) = 1.0

    _Color3("[3] Color", Color) = (1,1,1,1)
    [NoScaleOffset] _MainTex3("[3] Albedo (RGB)", 2D) = "white" {}
    _Glossiness3("[3] Smoothness", Range(0,1)) = 0.5
    [Gamma] _Metallic3("[3] Metallic Scale", Range(0,1)) = 0.0
    [HDR] _EmissionColor3("[3] Emittion Color", Color) = (0, 0, 0, 0)
    [NoScaleOffset] _EmissionTex3("[3] Emission", 2D) = "white" {}
    _OcclusionStrength3("[3] Occlusion Strength", Range(0.0, 1.0)) = 1.0
}

インスペクタに表示するデータを構造体でまとめることはできないので、単純にコピペしたのですが、もっといい方法ないですかね。

ワールド座標の選択テクスチャUV座標への変換

選択テクスチャのどの部分を参照するのかを決めるために、表示しようとしている点のワールド座標を選択テクスチャ上の座標に変換します。

まず、選択テクスチャがワールド座標においてカバーしている範囲の情報が必要になります。その情報の指定を_SelectionAreaで行うことができます。

_SelectionAreaは、XZ平面で選択テクスチャがカバーする矩形をx座標とz座標の最大値最小値として保持します。

SelectionSreaプロパティ
_SelectionArea("Selection Area (MinX, MinZ, MaxX, MaxZ)", Vector) = (0, 0, 100, 100)

次に、計算対象となる点のワールド座標が必要です。
これは入力構造体の定義に以下の行を追加することで使用可能です。

ワールド座標定義
struct Input {
    float2 uv_MainTex;
    float3 worldPos; // これ
};

以上の二つを使用して、選択マップにおける座標は以下のコードで計算できます。

座標変換
float2 calculate_selection_map_uv(float3 world_pos, float4 selection_area){
    float2 uv;
    uv.x = (world_pos.x - selection_area.x) / (selection_area.z - selection_area.x);
    uv.y = (world_pos.z - selection_area.y) / (selection_area.w - selection_area.y);
    return uv;
}

戻り値は、選択マップ上の位置を表す、それぞれの要素が0から1であるfloat2型です。

選択マップの反映

上で求めた参照位置における、選択マップの色情報を以下のコードで取得します。αチャンネルは使用しないので、fixed3型で受け取ることで無視します。

SelectionMapの色情報取得
float2 selection_map_uv = calculate_selection_map_uv(IN.worldPos, _SelectionArea);
fixed3 sm = tex2D(_SelectionMap, selection_map_uv);

smは各チャンネルがそれぞれ赤、緑、青を表す、値域が0から1の色情報を格納した三次元ベクトルです。
3つのテクスチャをsmの各チャンネルの値で重み付けして加算し、得られた値を最終出力とします。

この動作を実現するために、surf_core()を以下のように定義します。
surf_core()は各チャンネルでの出力値を計算し反映する関数です。

surf_core関数
void surf_core(Input IN, MaterialSetting ms, fixed ratio, inout SurfaceOutputStandard o) {
    // Albedo comes from a texture tinted by color
    fixed4 c = tex2D(ms.MainTex, IN.uv_MainTex) * ms.Color;
    o.Albedo += c.rgb * ratio;
    // Bump Map
    fixed4 n = tex2D(ms.BumpMap, IN.uv_MainTex);
    o.Normal += UnpackScaleNormal(n, ms.BumpScale) * ratio;
    // Emission
    fixed4 e = tex2D(ms.EmissionTex, IN.uv_MainTex);
    o.Emission += ms.EmissionColor * e * ratio;
    // Occlusion (合ってるかわからない)
    fixed4 oc = tex2D(ms.OcclusionMap, IN.uv_MainTex);
    o.Occlusion += oc * ms.OcclusionStrength * ratio;
    // Metallic and smoothness come from slider variables
    fixed4 m = tex2D(ms.MetallicGlossMap, IN.uv_MainTex);
    o.Metallic += m * ms.Metallic * ratio;
    o.Smoothness += ms.Glossiness * ratio;
    o.Alpha += c.a * ratio;
}

基本的には[基本的なSurface Shader](#基本的なSurface Shader)に載せたコードのsurf()関数と同じです。
違いは、比率ratioを受け取り、各要素にratioを掛けた値を加算するようになっている点です。

引数で使用されているMaterialSetting構造体は、入力変数をまとめたものです。

MaterialSetting
struct MaterialSetting {
    fixed4 Color;
    sampler2D MainTex;
    half BumpScale;
    sampler2D BumpMap;
    fixed4 EmissionColor;
    sampler2D EmissionTex;
    half OcclusionStrength;
    sampler2D OcclusionMap;
    half Glossiness;
    half Metallic;
    sampler2D MetallicGlossMap;
};

値を設定するのではなく加算していくので、一番初めに出力構造体の要素を全て初期化する必要があります。

このsurf_core()surf()ないで、_SelectionMapの3チャンネル分呼び出します。
これで、最終的には_SelectionMapの各チャンネルの値に応じて、3つの設定によるしゅつりょkぐあブレンドされた出力が得られます。

全体の実装

ソースコードのファイルは以下の二つになっています。

  • TripleBlendStandardOpaque.shader: StandardShanderの設定とTripleBlendStandardCore.cgincのインクルード
  • TripleBlendStandardCore.cginc: 選択マップによるテクスチャの加算

cgincファイルはほかのシェーダープログラムからC言語のように#includeすることができるファイルで、同じ処理や変数のコード共通化などに活用できます。

TripleBlendStandardOpaque.shader
Shader "Triple/Standard-Opaque"{
    Properties{
        [NoScaleOffset] _SelectionMap("Selection Map", 2D) = "red" {}
        _SelectionArea("Selection Area (MinX, MinZ, MaxX, MaxZ)", Vector) = (0, 0, 100, 100)

        _Cutoff("Alpha Cutoff (Valid In Cutout Mode)", Range(0,1)) = 0.5

        _Color("[1] Color", Color) = (1,1,1,1)
        _MainTex("[1] Albedo (RGB)", 2D) = "white" {}
        _Glossiness("[1] Smoothness", Range(0,1)) = 0.5
        [Gamma] _Metallic("[1] Metallic Scale", Range(0,1)) = 0.0
        [NoScaleOffset]_MetallicGlossMap("[1] Metallic", 2D) = "white" {}
        _BumpScale("[1] Normal Scale", Float) = 1.0
        [NoScaleOffset][Normal] _BumpMap("[1] Normal Map", 2D) = "bump" {}
        [HDR] _EmissionColor("[1] Emittion Color", Color) = (0, 0, 0, 0)
        [NoScaleOffset] _EmissionTex("[1] Emission", 2D) = "white" {}
        _OcclusionStrength("[1] Occlusion Strength", Range(0.0, 1.0)) = 1.0
        [NoScaleOffset] _OcclusionMap("[1] Occlusion", 2D) = "white" {}

        _Color2("[2] Color", Color) = (1,1,1,1)
        [NoScaleOffset] _MainTex2("[2] Albedo (RGB)", 2D) = "white" {}
        _Glossiness2("[2] Smoothness", Range(0,1)) = 0.5
        [Gamma] _Metallic2("[2] Metallic Scale", Range(0,1)) = 0.0
        [HDR] _EmissionColor2("[2] Emittion Color", Color) = (0, 0, 0, 0)
        [NoScaleOffset] _EmissionTex2("[2] Emission", 2D) = "white" {}
        _OcclusionStrength2("[2] Occlusion Strength", Range(0.0, 1.0)) = 1.0

        _Color3("[3] Color", Color) = (1,1,1,1)
        [NoScaleOffset] _MainTex3("[3] Albedo (RGB)", 2D) = "white" {}
        _Glossiness3("[3] Smoothness", Range(0,1)) = 0.5
        [Gamma] _Metallic3("[3] Metallic Scale", Range(0,1)) = 0.0
        [HDR] _EmissionColor3("[3] Emittion Color", Color) = (0, 0, 0, 0)
        [NoScaleOffset] _EmissionTex3("[3] Emission", 2D) = "white" {}
        _OcclusionStrength3("[3] Occlusion Strength", Range(0.0, 1.0)) = 1.0
    }

    CGINCLUDE
    #include "TripleBlendStandardCore.cginc"
    ENDCG

    SubShader{
        Tags { "Queue" = "Geometry" "RenderType" = "Opaque" }
        LOD 200
        CGPROGRAM

        #pragma surface surf Standard fullforwardshadows
        #pragma target 3.0

        ENDCG
    }

    Fallback "Standard"
}
TripleBlendStandardCore.cginc
#ifndef TRIPLE_STANDARD_CORE_CGINC_INCLUDED
#define TRIPLE_STANDARD_CORE_CGINC_INCLUDED

sampler2D _SelectionMap;
float4 _SelectionArea;

fixed4 _Color;
sampler2D _MainTex;
half _BumpScale;
sampler2D _BumpMap;
fixed4 _EmissionColor;
sampler2D _EmissionTex;
half _OcclusionStrength;
sampler2D _OcclusionMap;
half _Glossiness;
half _Metallic;
sampler2D _MetallicGlossMap;

fixed4 _Color2;
sampler2D _MainTex2;
fixed4 _EmissionColor2;
sampler2D _EmissionTex2;
half _OcclusionStrength2;
half _Glossiness2;
half _Metallic2;

fixed4 _Color3;
sampler2D _MainTex3;
fixed4 _EmissionColor3;
sampler2D _EmissionTex3;
half _OcclusionStrength3;
half _Glossiness3;
half _Metallic3;

struct Input {
    float2 uv_MainTex;
    float3 worldPos;
};

struct MaterialSetting {
    fixed4 Color;
    sampler2D MainTex;
    half BumpScale;
    sampler2D BumpMap;
    fixed4 EmissionColor;
    sampler2D EmissionTex;
    half OcclusionStrength;
    sampler2D OcclusionMap;
    half Glossiness;
    half Metallic;
    sampler2D MetallicGlossMap;
};

float2 calculate_selection_map_uv(float3 world_pos, float4 selection_area){
    float2 uv;
    uv.x = (world_pos.x - selection_area.x) / (selection_area.z - selection_area.x);
    uv.y = (world_pos.z - selection_area.y) / (selection_area.w - selection_area.y);
    return uv;
}

void surf_core(Input IN, MaterialSetting ms, fixed ratio, inout SurfaceOutputStandard o) {
    // Albedo comes from a texture tinted by color
    fixed4 c = tex2D(ms.MainTex, IN.uv_MainTex) * ms.Color;
    o.Albedo += c.rgb * ratio;
    // Bump Map
    fixed4 n = tex2D(ms.BumpMap, IN.uv_MainTex);
    o.Normal += UnpackScaleNormal(n, ms.BumpScale) * ratio;
    // Emission
    fixed4 e = tex2D(ms.EmissionTex, IN.uv_MainTex);
    o.Emission += ms.EmissionColor * e * ratio;
    // Occlusion (合ってるかわからない)
    fixed4 oc = tex2D(ms.OcclusionMap, IN.uv_MainTex);
    o.Occlusion += oc * ms.OcclusionStrength * ratio;
    // Metallic and smoothness come from slider variables
    fixed4 m = tex2D(ms.MetallicGlossMap, IN.uv_MainTex);
    o.Metallic += m * ms.Metallic * ratio;
    o.Smoothness += ms.Glossiness * ratio;
    o.Alpha += c.a * ratio;
}

void surf(Input IN, inout SurfaceOutputStandard o) {
    float2 selection_map_uv = calculate_selection_map_uv(IN.worldPos, _SelectionArea);
    fixed3 sm = tex2D(_SelectionMap, selection_map_uv);

    o.Albedo = 0;
    o.Normal = 0;
    o.Emission = 0;
    o.Occlusion = 0;
    o.Metallic = 0;
    o.Smoothness = 0;
    o.Alpha = 0;

    MaterialSetting ms;
    ms.Color = _Color;
    ms.MainTex = _MainTex;
    ms.BumpScale = _BumpScale;
    ms.BumpMap = _BumpMap;
    ms.EmissionColor = _EmissionColor;
    ms.EmissionTex = _EmissionTex;
    ms.OcclusionStrength = _OcclusionStrength;
    ms.OcclusionMap = _OcclusionMap;
    ms.Glossiness = _Glossiness;
    ms.Metallic = _Metallic;
    ms.MetallicGlossMap = _MetallicGlossMap;
    surf_core(IN, ms, sm.r, o);

    ms.Color = _Color2;
    ms.MainTex = _MainTex2;
    ms.BumpScale = _BumpScale;
    ms.BumpMap = _BumpMap;
    ms.EmissionColor = _EmissionColor2;
    ms.EmissionTex = _EmissionTex2;
    ms.OcclusionStrength = _OcclusionStrength2;
    ms.OcclusionMap = _OcclusionMap;
    ms.Glossiness = _Glossiness2;
    ms.Metallic = _Metallic2;
    ms.MetallicGlossMap = _MetallicGlossMap;
    surf_core(IN, ms, sm.g, o);

    ms.Color = _Color3;
    ms.MainTex = _MainTex3;
    ms.BumpScale = _BumpScale;
    ms.BumpMap = _BumpMap;
    ms.EmissionColor = _EmissionColor3;
    ms.EmissionTex = _EmissionTex3;
    ms.OcclusionStrength = _OcclusionStrength3;
    ms.OcclusionMap = _OcclusionMap;
    ms.Glossiness = _Glossiness3;
    ms.Metallic = _Metallic3;
    ms.MetallicGlossMap = _MetallicGlossMap;
    surf_core(IN, ms, sm.b, o);
}

#endif // TRIPLE_STANDARD_CORE_CGINC_INCLUDED

注意しないといけないのは、使用するテクスチャ枚数が2倍以上になっているため使用するビデオメモリの量も2倍以上になる点です。

使用方法

マテリアルのインスペクタで以下のように設定します。
inspector_setting.jpg

すると、初めに見せた画像のような状態になります。

triple_standard_shader3.gif

応用例

現在制作しているゲームで使用している、本記事で書いた方法の使用例を載せておきます。

現在作成しているゲームでは、ステージ内にタワーが配置されており、タワーは「影」「光」「中立」チームのいずれかが保持しています。
タワーの周辺はそのチームの領域になり、それによってステージの見た目が変化する仕組みになっています。

まずは、【Unity】コンピュートシェーダーを使ってボロノイ図のビットマップを生成するの記事で説明した方法で、チーム領域ごとの選択マップを生成します。緑が光、青が影、赤が中立チームです。それを使用して、本記事で説明した方法でステージ全体をチームごとに色分けします。
application1.jpg

結果的にこんな感じになります。
application2.jpg

タワーのチームが変わると、それに応じてリアルタイムに周辺の地形の色が変化します。

その他

ここで説明したソースコードは以下のリポジトリに置いてあります。

GitHub「unity-triple-blend-shader」

今回説明したシェーダーは、StandardシェーダーにおけるRendering ModeOpaqueに設定されている場合に当てはまるものですが、ほかの3つのモードに対応するシェーダーもリポジトリに置いてあります。
また、選択マップを実行時に設定するためのスクリプトも用意してあります。

参考文献

8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?