3Dの描画知識0から始めたShader奮闘記

先に軽く自己紹介だけさせて頂きます。

  • 名前:#ポテト (ハッシュポテト)

  • 学年:専門学校2年生

  • プログラミング歴:C言語 半年、C++ 半年、C# 2カ月

  • 3D描画経験:ほぼ0

見出しにもある通り3D全然分かりません!!(未だに)

周りの先生もUnityのShader分かりません!!

それでもShaderが必要!!

じゃあ一人で頑張ろうという成り行きからShader苦悩生活が始まります。
未だに3Dの知識が怪しいので、暖かい目で見ていただけると幸いです。

とりあえず調べてみよう

まず私がシェーダーを作成するにあたって一つ目標がありました。
それはプレイヤーが乗った時にボフッって凹む雲です。(伝われ…)
そんな雲を目指して走り出しましたが初手から大コケしました。

そもそもシェーダーってなに???

そう、あれだけシェーダーシェーダー騒いでいたのにシェーダーを何か理解していませんでした。そこでまずシェーダーとは何か調べました。

シェーダとは、3次元コンピュータグラフィックス(3DCG)において、陰影付けや表面の質感や凹凸の設定、各画素の表示色の決定などを行うプログラムのこと

https://e-words.jp/w/%E3%82%B7%E3%82%A7%E3%83%BC%E3%83%80.html

プログラムで3Dオブジェクトの見た目を変える方法なのか。
大体は理解できたが調べている時に気になる文字を発見。

頂点 / フラグメント(ピクセル)シェーダー


聞いたことない単語が出てきました。とりあえず頂点 / フラグメント(ピクセル)シェーダーを調べてそれとなく理解し、図に起こしてみました。

頂点シェーダーで三角形の頂点の場所を決めてフラグメントシェーダーで三角形内の色塗り

参考サイト

これでとりあえず原理が分かったのでシェーダーの作成に取り掛かります。

初めてのシェーダーの作成!

いざ制作!!…と行きたかったですがシェーダーの書き方が分からず初手から自作は出来なかったのでネットに上がっているものをコピペして知識を増やしていきました。

参考サイト(とても分かりやすくてオススメです!)

やっと本題

さて最初の目標の雲を作るにあたってやりたかったことが

  • 雲を凹ます(頂点の移動)

  • ライティングを当てる

以上の2つだったので頂点シェーダとサーフェスシェーダー(フラグメントシェーダーがより使いやすくなり、ライティングなどを勝手にやってくれる)のふたつを組み合わせ作りました。

参考サイト

そして2週間かけて完成したver1.0がこちら

処理としてはスクリプト側からプレイヤーの中心点を受け取り頂点をワールド座標に変更し、頂点がプレイヤーの中心点から一定の範囲内なら頂点の位置を変更するプログラムを書きました。

これでそれっぽく凹ますことは出来たけれど雲から離れた時に雲が揺れないし挙動もおかしい…

ということでver2.0はガラッと変えて頂点を移動する際に参照するものをプレイヤーの中心点からブロックに変更となりました。

更に改善
そもそも現在プレイヤーが乗ってる物は雲本体ではなくその中に隠れた繋がったブロックに乗っています。

この繋がったブロックは簡単に言えばトランポリンみたいな挙動をしていてver1.0のシェーダーでは、そのブロックに連動して見た目を変えていた訳ではなくどちらかというとブロックの動きにシェーダーを合わせていました。

ブロックの動きにシェーダーを合わせてダメならシェーダーをブロックの動きに合わせよう!
それがver2.0のコンセプトとなっています。

最終的なアルゴリズム

まずブロックが全体で何個あるか取得し1つのブロックが担当する雲の範囲を設定します。

その次に担当する箇所が初期座標から動いたら、担当する箇所をブロックが動いた分だけ頂点を動かします。

そして完成した物がこちら

//ボックスを管理するスクリプト
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BoxManager : MonoBehaviour
{
    private GameObject[] boxs;
    private Vector4[] oldBoxPositions;
    private Vector4[] boxPositions;
    private Vector4[] boxScales;

    public GameObject tests;
    public GameObject cloud;
    private Material material;

    void Start()
    {
        //初期化
        boxs = new GameObject[this.gameObject.transform.childCount];
        boxPositions = new Vector4[boxs.Length];
        oldBoxPositions = new Vector4[boxPositions.Length];
        boxScales = new Vector4[boxPositions.Length];

        material = cloud.GetComponent<Renderer>().material;

        for (int i = 0; i < this.gameObject.transform.childCount; i++)
        {
            boxs[i] = this.gameObject.transform.GetChild(i).gameObject;
            
            //初期座標を記録
            oldBoxPositions[i] = boxs[i].transform.position;
            boxPositions[i] = boxs[i].transform.position;

            boxScales[i] = boxs[i].transform.localScale;
        }

    }

    void Update()
    {
        //初期座標と現在の座標の差
        Vector4[] remainder = new Vector4[oldBoxPositions.Length];

        for (int i = 0; i < boxScales.Length; i++)
        {
            boxPositions[i] = boxs[i].transform.position;

            //初期座標からどれだけ動いているか判定
            remainder[i] = (oldBoxPositions[i] - boxPositions[i]);
        }

        //boxの左端の座標をもらう
        float cloudMinPos = boxPositions[0].x ;        
        
        //ボックス1つあたりが担当する割合
        float cloudRatio = boxScales[0].x;

        ////マテリアル側に情報を送る
        material.SetFloat("_CloudRatio", cloudRatio);
        material.SetFloat("_CloudMinPos", cloudMinPos);
        material.SetInt("_BoxElement", boxPositions.Length);
        material.SetVectorArray("_BoxPositions", remainder);
        material.SetVectorArray("_BoxScales", boxScales);
    }
}
Shader "Custom/cloud"
{

Properties
{
    _Color ("Color", Color) = (1,1,1,1)
    _MainTex ("Albedo (RGB)", 2D) = "white" {}
    _Glossiness ("Smoothness", Range(0,1)) = 0.5
    _Metallic ("Metallic", Range(0,1)) = 0.0
    _BoxElement("BoxElement", Int) = 0
    _CloudRatio("CloudRatio", Float) = 0
    _CloudMinPos("CloudMinPos",Float) = 0
    _Spring("Spring",Range(0.00005, 0.0005)) = 0.0001
    _BoundCoefficient("Bound"Int) = 350
}
SubShader
{
    Tags { "RenderType"="Opaque" }
    
    CGPROGRAM

    #pragma surface surf Standard 
    #pragma vertex vert
    #include "UnityCG.cginc"

    sampler2D _MainTex;

    struct Input
    {
        float2 uv_MainTex;
        float3 worldPos;
    };

    half _Glossiness;
    half _Metallic;
    fixed4 _Color;
    
    //スクリプト側から受けとる変数

    //ボックスの数
    int _BoxElement;

    //ボックスの座標
    float4 _BoxPositions[400];

    //ボックスのサイズ
    float4 _BoxScales[400];

    //雲の一番左端の座標
    float _CloudMinPos;
    
    //ブロックひとつあたりが担当する割合
    float _CloudRatio;

    //頂点の移動量
    float _Spring;

    void vert(inout appdata_full v)
    {
        //頂点座標をワールド座標に変更
        float3 worldpos = mul(unity_ObjectToWorld ,v.vertex).xyz;

        for (int i = 0; i < _CloudRatio; i++)
        {
           
            if ((worldpos.x > ((i * _CloudRatio) + _CloudMinPos)) &&
                (worldpos.x < (((i + 1) * _CloudRatio) + _CloudMinPos)))
            {
                _BoxPositions[i].y = (_BoxPositions[i].y > _Spring) ? _Spring : _BoxPositions[i].y;

                v.vertex.z -= _BoxPositions[i].y / _BoundCoefficient;
                break;
            }
        }
    }

    void surf (Input IN, inout SurfaceOutputStandard o)
    {
        fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
        o.Albedo = c.rgb;
        o.Alpha = c.a;
    }

    ENDCG
}
    
}

少し跳ねすぎだけどとりあえず完成✨✨

更に調整

やるなら徹底的にやりたいので不具合の洗い出し

  • よく見ると上には跳ねてるけど下に動いてない
    (プレイヤーが乗った時に凹まない)。

  • それっぽい動きには近づいてきたけどやはりどこか違和感…

そしてソースコードを見直し、ある文に注目。

_BoxPositions[i].y = (_BoxPositions[i].y > _Spring) ? _Spring : _BoxPositions[i].y;

この文はclamp処理のつもりで跳ねすぎる値が送られてきたら抑える文ですが、この文の条件式にある_Springが0.01と0より上の値のせいで下に下がるマイナスの値が送られてきた際に問答無用でプラスの値にしていました…

そしてブロック側のスクリプトも調整し完成したものがこちら

Shader "Custom/Cloud"
{
    Properties
    {
        _Color("Color", Color) = (1,1,1,1)
        _MainTex("Albedo (RGB)", 2D) = "white" {}
        _Glossiness("Smoothness", Range(0,1)) = 0.5
        _Metallic("Metallic", Range(0,1)) = 0.0
        _BoxElement("BoxElement", Int) = 0
        _CloudRatio("CloudRatio", Float) = 0
        _CloudMinPos("CloudMinPos",Float) = 0
    _BoundCoefficient("Bound"Int) = 350
    }
SubShader
{
    Tags { "RenderType" = "Opaque" }

    CGPROGRAM

    #pragma surface surf Standard 
    #pragma vertex vert
    #include "UnityCG.cginc"

    sampler2D _MainTex;

    struct Input
    {
        float2 uv_MainTex;
        float3 worldPos;
    };

    half _Glossiness;
    half _Metallic;
    fixed4 _Color;

    //スクリプト側から受けとる変数
    int _BoxElement;
    float4 _BoxPositions[400];
    float4 _BoxScales[400];

    //雲の一番左端の座標
    float _CloudMinPos;

    //ブロックひとつあたりが担当する割合
    float _CloudRatio;

    //反発係数
    int _BoundCoefficient;

    void vert(inout appdata_full v)
    {
        //頂点座標をワールド座標に変更
        float3 worldpos = mul(unity_ObjectToWorld ,v.vertex).xyz;

        for (int i = 0; i < _BoxElement; i++)
        {
            if ((worldpos.x < (((i + 1) * _CloudRatio) + _CloudMinPos)))
            {
                v.vertex.z -= _BoxPositions[i].y / _BoundCoefficient;
                break;
            }
        }
    }

    void surf(Input IN, inout SurfaceOutputStandard o)
    {
        fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
        o.Albedo = c.rgb;
        o.Alpha = c.a;

    }

    ENDCG
}

}

※スクリプト側は変更していないため記載していません。

かなりそれっぽくなりました。
clamp処理はマイナスの時の処理を書いたり試行錯誤してみましたが、うまく実装できなかったため一旦見送りました。
無駄な処理や見送ったclamp処理がありますが

  • 見た目に影響ないこと

  • 組み込めていないものによる影響が少ない

  • 一つの大きな区切りになり次シェーダーを作る際に気をつける指標になる

以上のことからこのシェーダーは一区切りつけたいと思います。

まとめ


3ヶ月ほど1から独学で勉強してこれだけ出来ました。
苦しい事も色々ありましたけど完成するとやっぱり達成感が多くてまたシェーダーをやりたいと思えました。

この記事が気に入ったらサポートをしてみませんか?