読者です 読者をやめる 読者になる 読者になる

しゅみぷろ

プログラミングとか

オブジェクトの一部のみ鏡面反射を行うようにする

Graphics Shader Unity UnityTexturePaint C#

はじめに

オブジェクトにインクっぽいものを塗って、そのインク部分だけが反射するような表現がしたかったので作りました。

多用は出来ませんが、数カ所こういった特徴的な表現ができるとゲームがかなり尖ります(と思います。

UnityTexturePaintを利用しているので、法線マップペイント、ハイトマップペイントにも対応しています。 これらを併用することでちょっと歪んだ鏡面の表現などが容易に実現可能です。

f:id:es_program:20161102021156p:plain:w600

f:id:es_program:20161102021159g:plain:w600

ペイント処理については

esprog.hatenablog.com

を参考にして下さい。

サンプルは

GitHub - EsProgram/UnityTexturePaint: Unityで利用可能なテクスチャーペイント

に同梱されています。

実装方法

大枠はかなり単純です。

反射を伴うペイントを行った際、ペイントした場所にカメラを生成し、カメラのレンダリングターゲットにスクリプト上で生成したRenderTextureを指定してやります。

f:id:es_program:20161102022350p:plain:w600

こうすると、上記画像の右下のように鏡に映り込む対象がレンダリングされます。

これをペイントした場所に映してやれば反射表現の完成です。

ただ、普通にレンダリングしたテクスチャをそのまま映してしまうと、ペイント範囲外でも鏡面反射の影響が出てしまいます。

これを解決するために、UnityTexturePaintにこっそり追加してあるGrabArea機能を使います。

GrabAreaは、TexturePaintの逆を行う機能です。普段は指定箇所を"塗る"わけですが、GrabAreaの場合は指定箇所を"切り取る"事ができます(言い回し的には切り取るよりも"コピーする"といった方が正しいです)。

f:id:es_program:20161102021913g:plain:w600

上記の画像がGrabAreaの基本機能です。ゆいちゃんをインク(ブラシ)の形で切り抜いて、ゆずちゃんにぶっかけてます。 例としてわかりやすいようにコピーの際、ちょっと赤いインクでペイントしていますが、GrabArea機能のみを使う場合はペイント処理は発生しません。

この機能を使えば反射が起こる場所を制限して、いい感じに反射する場所を動的に作り出すことができるようになります。

言葉だけだとアレなので具体的にコードを追っていきます。

以下はサンプルに含まれる、クリックした場所を鏡面反射させるためのスクリプトです。

using Es.TexturePaint;
using System.Collections;
using UnityEngine;

public class ReflectPainter : MonoBehaviour
{
    [SerializeField]
    private PaintBrush brush;

    [SerializeField]
    private GameObject camPref;

    private RenderTexture rt;
    private Camera cam;
    private Vector2 uv;
    private DynamicCanvas paintObject;

    public void Awake()
    {
        rt = new RenderTexture(Screen.width, Screen.height, 16, RenderTextureFormat.ARGB32);
        brush.ColorBlending = PaintBrush.ColorBlendType.UseBrush;
    }

    public void OnGUI()
    {
        if(GUILayout.Button("Reset"))
        {
            if(paintObject != null)
                paintObject.ResetPaint();
            Destroy(cam);
            cam = null;
        }
    }

    private void Update()
    {
        if(cam == null && Input.GetMouseButtonDown(0))
        {
            var ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hitInfo;
            if(Physics.Raycast(ray, out hitInfo))
            {
                paintObject = hitInfo.transform.GetComponent<DynamicCanvas>();
                if(paintObject != null)
                {
                    uv = hitInfo.textureCoord;
                    var camObj = Instantiate(camPref, hitInfo.point, Quaternion.LookRotation(hitInfo.normal), hitInfo.transform) as GameObject;
                    cam = camObj.GetComponent<Camera>();
                    cam.targetTexture = rt;
                    camObj.SetActive(true);
                }
            }
        }
        else if(cam != null)
        {
            var buf = RenderTexture.GetTemporary(brush.BrushTexture.width, brush.BrushTexture.height);
            Es.Effective.GrabArea.Clip(brush.BrushTexture, brush.Scale, rt, Vector3.one * 0.5f, Es.Effective.GrabArea.GrabTextureWrapMode.Clip, buf);
            Es.Effective.ReverseUV.Horizontal(buf, buf);
            var brushBuf = brush.BrushTexture;
            brush.BrushTexture = buf;
            if(paintObject != null)
                paintObject.PaintUVDirect(brush, uv);
            RenderTexture.ReleaseTemporary(buf);
            brush.BrushTexture = brushBuf;
        }
    }
}

とりあえずはコレだけで鏡面表現が出来ます。

Awakeで、反射の映り込みに使う画像を格納するRenderTextureを生成しています。

            if(Physics.Raycast(ray, out hitInfo))
            {
                paintObject = hitInfo.transform.GetComponent<DynamicCanvas>();
                if(paintObject != null)
                {
                    uv = hitInfo.textureCoord;
                    var camObj = Instantiate(camPref, hitInfo.point, Quaternion.LookRotation(hitInfo.normal), hitInfo.transform) as GameObject;
                    cam = camObj.GetComponent<Camera>();
                    cam.targetTexture = rt;
                    camObj.SetActive(true);
                }
            }

Updateのこの部分では、クリック位置にカメラを生成し、そのカメラのレンダリングターゲットをAwakeで生成したRenderTextureに差し替える処理を行っています。

次に、反射処理で呼ばれる

            var buf = RenderTexture.GetTemporary(brush.BrushTexture.width, brush.BrushTexture.height);
            Es.Effective.GrabArea.Clip(brush.BrushTexture, brush.Scale, rt, Vector3.one * 0.5f, Es.Effective.GrabArea.GrabTextureWrapMode.Clip, buf);
            Es.Effective.ReverseUV.Horizontal(buf, buf);
            var brushBuf = brush.BrushTexture;
            brush.BrushTexture = buf;
            if(paintObject != null)
                paintObject.PaintUVDirect(brush, uv);
            RenderTexture.ReleaseTemporary(buf);
            brush.BrushTexture = brushBuf;

この部分です。

Awakeで生成したRenderTextureを、ブラシの形状に切り取るために、GrabArea.Clipメソッドを利用しています。 Clip後、映り込み部分を左右反転させ、映り込みレンダリング用カメラを生成した位置(最初のクリック位置)に、加工した映り込みのテクスチャをペイントしています。

映り込みのテクスチャをそのままペイントしてしまうと対称的になってしまうため、ReverseUVのHorizontalメソッドで横方向のUVを反転する処理を行っています。

反転を行わなかった場合の挙動です。