【Unity2018】 ShaderGraphで遊ぼう!その1

Unity2018で、ついにShaderGraphが追加されました。 ↓のような感じで、ノードを繋げることでデザイナーでもシェーダーを簡単に開発することができる機能です。 f:id:notargs:20180508064607g:plain f:id:notargs:20180508065031g:plain

元々Unreal Engine 4などのHD向けのゲームエンジンではあった機能で、これと同じような機能を持ったUnityAssetなども販売されていましたが、ついに公式でのサポートが入りました。

同じくUnity2018から正式版になったScriptableRenderPipelineという機能がありますが、プロジェクトで採用したRenderPipelineごとに大きくシェーダーコードが変わるため、アーティストのレイヤーではそこの差異を吸収したい!という目的もあり、優先的に実装されたっぽい雰囲気を感じています。

今回は、このShaderGraphを使い、適当なサイバーっぽいシェーダーを作ってみようと思います。

まずは導入

新しいUnityProjectを作ったら、おもむろにWindow -> Package Managerを立ち上げ、ShaderGraphをインポートします。

f:id:notargs:20180508065541p:plain

今回は、Unlit Graphを使ってみます。 こちらはPBR Graphと違ってライトの影響を受けませんが、逆に軽量、かつ自分で好きなようにライティング出来るのがメリットになります。 Sci-fi的なオブジェクトだったり、ToonShaderなど、現実感のないオブジェクトを作る時に活用できます。

f:id:notargs:20180508065720p:plain

リムライトを作ってみる

まずは、オブジェクトの端が光るような、リムライトの表現を作ってみます。 f:id:notargs:20180508070247g:plain

Unlit MasterのColor(3)からドラッグ&ドロップで線を伸ばし、Normal Vectorノードへ接続します。 これでオブジェクトの法線をそのまま色として出力するシェーダーが出来ました。 f:id:notargs:20180508070529p:plain f:id:notargs:20180508070653p:plain f:id:notargs:20180508070914p:plain

今回欲しいのはカメラから見た法線なので、SpaceをViewに変更します。

f:id:notargs:20180508070719p:plain

同じように、Dot ProductノードとAddノードを間に繋げるとリムライトが完成します。 f:id:notargs:20180508071116p:plain

Dot Productノードで手前方向と法線の向きの差を求め、Addで少し光の領域を広げてやる、といったイメージの処理になります。

スキャンラインを足してみる

次は、サイバー感のあるスキャンラインのエフェクトを足してみます。 さっきの処理は一旦脇においておいて、下の画像のようにノードを組みます。

f:id:notargs:20180508071545p:plain f:id:notargs:20180508071800g:plain

ワールド空間での位置からSplitノードでY軸の値だけを取り出し、それをTimeに加算した上で、Fractノードで0~1の範囲の繰り返しに変換、Colorへ流し込むノードを組みました。 これだけでもまあまあそれっぽい感じになると思います。

Addノードでさっきの処理と繋げてやることで、リムライトとスキャンラインを共存させることが出来ました。

f:id:notargs:20180508071930p:plain

今は1mごとに1個のスキャンラインが走っていますが、もう少し密度を上げてみようと思います。

Positionに対してDivノードを繋げ、0.1で割ってやることで、スキャンラインの密度を10cmごとに変更できました。

f:id:notargs:20180508072138p:plain f:id:notargs:20180508072314g:plain

せっかくなので、Sliderで値を変更できるようにしてみます。 定数だった0.1から線を伸ばし、Sliderを接続します。

f:id:notargs:20180508072712g:plain

更に、右クリックしてConvert To Propertyを選ぶことでマテリアルへとスライダーを公開することが出来ます。

f:id:notargs:20180508072842p:plain f:id:notargs:20180508072921p:plain

色がないと少し寂しいので、スキャンラインに色を足してみます。

f:id:notargs:20180508073137p:plain f:id:notargs:20180508073211g:plain

こちらも同様に、右クリック→Convert To Propertyから外へプロパティを公開できます。

f:id:notargs:20180508073402p:plain

まとめ

完成したシェーダーはこんな感じです。

f:id:notargs:20180508073355g:plain

ShaderGraphを利用することで、非エンジニアでもお手軽にこういった表現ができるようになりました。 普通のゲームを作る上では困らないくらい自由度があり、触っていて楽しい機能なので、ぜひ一度触ってみましょう!

【Unity】UGUIのRectTransformを100倍快適に扱うためのユーティリティを作った

概要

この記事はCluster,Inc. Advent Calendar 2017の14日目の記事です。

f:id:notargs:20171214105828g:plain

こんな感じのWindowのようなものを作っていたのですが、 RectTransformの具体的なサイズが知りたい! 親Transformの左端、右端から、相対的に位置を指定したい! など、RectTransformに思うところが多かったので、RectTransformを便利に扱うためのユーティリティを作ってみました。

コード

/**
MIT Lisence

Copyright (c) 2017 Cluster,Inc.

This software is released under the MIT License.
http://opensource.org/licenses/mit-license.php
*/

public enum Corner
{
    LeftTop,
    RightTop,
    LeftBottom,
    RightBottom,
}
/**
MIT Lisence

Copyright (c) 2017 Cluster,Inc.

This software is released under the MIT License.
http://opensource.org/licenses/mit-license.php
*/

using System;
using System.Linq;
using UnityEngine;

public static class RectTransformUtil
{
    public static Bounds GetWorldBounds(this RectTransform rectTransform)
    {
        var corners = new Vector3[4];
        rectTransform.GetWorldCorners(corners);

        var center = corners.Aggregate(Vector3.zero, (current, corner) => current + corner) / corners.Length;
        var size = new Vector3(
            corners.Max(corner => corner.x) - corners.Min(corner => corner.x),
            corners.Max(corner => corner.y) - corners.Min(corner => corner.y),
            1);
        return new Bounds(center, size);
    }

    public static Vector2 GetRelativePosition(this RectTransform rectTransform, Corner corner)
    {
        var parentBounds = rectTransform.parent.GetComponent<RectTransform>().GetWorldBounds();
        var bounds = rectTransform.GetWorldBounds();

        var pos = Vector2.zero;
            
        switch (corner)
        {
            case Corner.LeftBottom:
            case Corner.RightBottom:
                pos.y = bounds.min.y - parentBounds.min.y;
                break;
            case Corner.LeftTop:
            case Corner.RightTop:
                pos.y = parentBounds.max.y - bounds.max.y;
                break;
            default:
                throw new ArgumentOutOfRangeException("corner", corner, null);
        }

        switch (corner)
        {
            case Corner.LeftTop:
            case Corner.LeftBottom:
                pos.x = bounds.min.x - parentBounds.min.x;
                break;
            case Corner.RightTop:
            case Corner.RightBottom:
                pos.x = parentBounds.max.x - bounds.max.x;
                break;
            default:
                throw new ArgumentOutOfRangeException("corner", corner, null);
        }

        return pos;
    }

    public static void SetRelativePosition(this RectTransform rectTransform, Vector2 pos, Corner corner)
    {
        var parentBounds = rectTransform.parent.GetComponent<RectTransform>().GetWorldBounds();
        var bounds = rectTransform.GetWorldBounds();
        var anchoredPosition = rectTransform.position;

        switch (corner)
        {
            case Corner.LeftBottom:
            case Corner.RightBottom:
                anchoredPosition.y += parentBounds.min.y - bounds.min.y + pos.y;
                break;
            case Corner.LeftTop:
            case Corner.RightTop:
                anchoredPosition.y += parentBounds.max.y - bounds.max.y - pos.y;
                break;
            default:
                throw new ArgumentOutOfRangeException("corner", corner, null);
        }
            
        switch (corner)
        {
            case Corner.LeftTop:
            case Corner.LeftBottom:
                anchoredPosition.x += parentBounds.min.x - bounds.min.x + pos.x;
                break;
            case Corner.RightTop:
            case Corner.RightBottom:
                anchoredPosition.x += parentBounds.max.x - bounds.max.x - pos.x;
                break;
            default:
                throw new ArgumentOutOfRangeException("corner", corner, null);
        }
            
        rectTransform.position = anchoredPosition;
    }
}

つかいかた

rectTransform.GetWorldBounds() のように、拡張メソッドとして使えるようになっています。

GetWorldBounds

Boundsとして、ワールド空間でのRectTransformの位置やサイズ、各点の位置などが取得できます。

GetRelativePosition / SetRelativePosition

右側/左側/上側/下側が、親の端からどのくらい離れているかを相対位置で取得/設定できます。 親のオブジェクトをはみ出さないように位置を制限する、といった挙動の実装に便利です。

微妙なところ

パフォーマンス

メソッドが呼ばれるたびにGetComponent<RectTransform>()を2回も呼んでいるので、それなりにパフォーマンスが悪いです。 適宜、必要に応じてキャッシュする仕組みなどを作ると良いと思います。

親がRectTransformじゃない場合の挙動は?

わからん 😱

親にスケールがかかっていた場合の挙動は?

わからん😇😇😇 なんかもっといいアプローチがあったら教えてください

SurfaceShaderで主線を描画する

概要

主線を表示し、エッジを強調するシェーダーをSurface Shaderで作りました。

f:id:notargs:20171012191320g:plain

主線に影は乗っていませんが、ライトの色を変更すると主線の色も変わるのがキモです。

f:id:notargs:20171012191354g:plain

仕組み

1パス目

普通にモデルを描画しています

f:id:notargs:20171012191935p:plain

2パス目

頂点シェーダーで少しだけ法線方向に拡大したものを描画します。 自前のライティング関数を指定することで、影を描画しないように変更しています。

f:id:notargs:20171012191932p:plain

コード

/*
StandardEdge

Copyright(c) 2017 Yutaka Sato

This software is released under the MIT License.
http://opensource.org/licenses/mit-license.php
*/

Shader "Custom/StandardEdge" 
{
    Properties 
    {
        _Color("Color", Color) = (1,1,1,1)
        _EdgeColor("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
        _EdgeWidth("EdgeWidth", Range(0, 0.1)) = 0.02
    }
    SubShader 
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGINCLUDE
       #include "UnityPBSLighting.cginc"
        
        half _Glossiness;
        half _Metallic;
        float4 _Color;
        float3 _EdgeColor;
        float _EdgeWidth;

        sampler2D _MainTex;

        struct Input 
        {
            float2 uv_MainTex;
        };

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

        void edgeVert(inout appdata_full v)
        {
            v.vertex.xyz += v.normal * _EdgeWidth;
        }

        void edgeSurf(Input IN, inout SurfaceOutputStandard o)
        {
            o.Albedo = _EdgeColor;
        }

        half4 LightingEdge(SurfaceOutputStandard s, half3 lightDir, half atten)
        {
            half4 c;
            c.rgb = s.Albedo * _LightColor0.rgb;
            c.a = s.Alpha;
            return c;
        }
        ENDCG

        Cull Back
        CGPROGRAM
       #pragma surface surf Standard fullforwardshadows
       #pragma target 3.0
        ENDCG

        Cull Front
        CGPROGRAM
       #pragma surface edgeSurf Edge fullforwardshadows vertex:edgeVert
       #pragma target 3.0

        ENDCG
    }
    FallBack "Diffuse"
}

【Unity】"Error building Player because scripts had compiler errors"とだけ出た時のエラー箇所の探し方

Editorビルドのみが失敗しているときなど、極稀にError building Player because scripts had compiler errorsとだけログに出力されることがあります。

それしかエラーが出ないため該当箇所の特定が難しかったのですが、対処方法がわかったためメモっておきます。

続きを読む

【Unity】#unity1week に参加して大人気WebGLゲー「qvsm」を作ったレポ

先週一週間、前々から気になっていた、1週間でUnityを使ってゲームを作るイベント#unity1weekに参加していました!

f:id:notargs:20170530011332p:plain

Unity 1週間ゲームジャム | ゲーム投稿サイト unityroom - Unityのゲームをアップロードして公開しよう

雑な茶色い料理でひたすら飯テロをしているだけに見えますが気のせいです。たぶん。

作ったゲーム

qvsmというゲームを作りました。

キュビズムと読みます。Cube/Diceあたりのイントネーションを含んだ、シンプルかつ商標取れそうな文字列がこれくらいしかありませんでした。 パブロ・ピカソなどの現代美術と一切関係はありません。

qvsm | ゲーム投稿サイト unityroom - Unityのゲームをアップロードして公開しよう

幸い、予想を上回る人数の方に遊んでいただき、週間ランキング、人気TOP3に食い込むなど、なかなか面白いゲームが作れたんじゃないかと思っています。

続きを読む

【C#】WindowsでもSlackに豆腐が打ちたい!

概要

Slackクライアント最新版(2.6.0-bata1)にて、Macでメッセージを打ち込んだ時に余計な文字が入ってしまい、Windows上で見た時に豆腐が表示されてしまうというバグが発生しています。

Macの人ばかり楽しそうに豆腐を打ち込んでいて羨ましかったので、Windowsでも豆腐を打ち込むことができるツールをC#で作りました。

f:id:notargs:20170521123654p:plain

仕組み

受け取った変数に、特殊な文字\bを付け足してクリップボードにコピーします。

使い方

適当に文字を打ち込んでEnterを押すとコピーされます。 そのままSlackへ貼り付けることで、Windowsでは読めず、Macでは読める文字列が出来上がります。 f:id:notargs:20170521174241g:plain

ソースコード

/*
TofuWriter

Copyright(c) 2017 Yutaka Sato

This software is released under the MIT License.
http://opensource.org/licenses/mit-license.php
*/
using System;
using System.Windows.Forms;

namespace TofuWriter
{
    class Program
    {
        [STAThread]
        static void Main(string[] args)
        {
            while (true)
            {
                var text = Console.ReadLine();

                var temp = text.ToCharArray();
                var temp2 = new char[temp.Length + 2];
                Array.Copy(temp, 0, temp2, 1, temp.Length);
                temp2[0] = (char)8;
                
                Clipboard.SetText(new string(temp2));

                Console.WriteLine("Copied");
            }
        }
    }
}

【C#】Slackクライアントの豆腐をWindowsでも解読できるツールを作った

概要

Slackクライアント最新版(2.6.0-bata1)にて、Macでメッセージを打ち込んだ時に余計な文字が入ってしまい、Windows上で見た時に豆腐が表示されてしまうというバグが発生しています。

MacやAndroidなどでメッセージを確認すれば読むことができるのですが、それも結構面倒だったので豆腐翻訳機を作ってみました。

動作

起動したまま、豆腐を選択してコピー(Ctrl+Cなど)すると、豆腐を翻訳することが出来ます。

f:id:notargs:20170517140227g:plain

ちなみに、写真のSlackチームは雑談Slack(http://samezi-but.com/zdnj.html)です。 和気藹々とした、仕事をサボるには最適なチームなのでぜひご活用ください!

gif画像の転載を快く許可していただいた@gauraさん、@erukitiさんありがとうございます!

仕組み

クリップボードを読み取り、文字コード8の文字を削除してから表示するだけのシンプルなプログラムです。

コード

/*
TofuReader

Copyright (c) 2017 Yutaka Sato

This software is released under the MIT License.
http://opensource.org/licenses/mit-license.php
*/
using System;
using System.Threading;
using System.Windows.Forms;

namespace TofuReader
{
    class Program
    {
        [STAThread]
        static void Main(string[] args)
        {
            var text = string.Empty;
            var prevText = string.Empty;
            while (true)
            {
                prevText = text;
                text = Clipboard.GetText();
                if (prevText != text)
                {
                    Console.WriteLine(text.Replace(((char)8).ToString(), ""));
                }
                Thread.Sleep(10);
            }
        }
    }
}