神様は有給消化中です。

技術ネタを書こうかな。時事ネタとか私生活ネタはうけないし。

【Unity】カメラからどれだけ離れても、最低1ピクセルは表示されるように保障する

通常3Dオブジェクトがカメラから離れすぎると、レンダリングサイズが1ピクセルを下回った時点で画面に描画されなくなり、カメラとの位置関係によって描画が切れたり出たりしてチカチカと表示されることがあります。今回はこの問題の解決方法(レンダリングの最低ピクセル保障)を共有します。


考え方

実現するためには、「同次クリップ空間上でビルボードを縦横に指定ピクセルずつ大きくして描画」すれば良いことになります。

座標変換については以下のサイトが詳しく解説されていますので、興味のある方はどうぞ。
3D座標変換 - ゲームプログラミングWiki

実現方法

頂点情報として位置と法線を与えて、頂点シェーダーで法線方向に指定ピクセル分引き延ばすことで実現します。
常に指定ピクセル分大きく表示されてしまいますが、あまり大きな値を入れない限りは大丈夫かと。
逆に、ビルボードのサイズを極端に小さくして、常に同じピクセル数で表示させるようなことも可能です。

ビルボード
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class MyBillBorad : MonoBehaviour
{
    [SerializeField] private Vector2 size = new Vector2(1f, 1f);

    private void Update()
    {
        Camera mainCamera = Camera.main;
        if (mainCamera != null) {
            this.transform.LookAt(mainCamera.transform.position);  
        }
    }

    private Mesh CreateMesh()
    {
        List<Vector3>    vertices    = new List<Vector3> ();
        List<int>        triangles    = new List<int> ();
        List<Vector2>    uvs            = new List<Vector2> ();
        List<Vector3>    normals        = new List<Vector3> ();

        float width        = size.x * 0.5f;
        float height    = size.y * 0.5f;

        // 頂点
        vertices.Add(new Vector3(-width, -height, 0f));
        vertices.Add(new Vector3( width, -height, 0f));
        vertices.Add(new Vector3(-width,  height, 0f));
        vertices.Add(new Vector3( width,  height, 0f));

        // インデックス
        triangles.AddRange(new int[]{1, 2, 0});
        triangles.AddRange(new int[]{3, 2, 1});

        // uv
        uvs.Add(new Vector2(1f, 0f));
        uvs.Add(new Vector2(0f, 0f));
        uvs.Add(new Vector2(1f, 1f));
        uvs.Add(new Vector2(0f, 1f));

        // 法線
        normals.AddRange(vertices.ToArray());

        Mesh mesh = new Mesh ();
        mesh.vertices    = vertices.ToArray ();
        mesh.triangles    = triangles.ToArray ();
        mesh.uv            = uvs.ToArray ();
        mesh.normals    = normals.ToArray ();

        mesh.RecalculateBounds ();

        return mesh;
    }

    private void Awake()
    {
        MeshFilter meshFilter = GetComponent<MeshFilter> ();
        meshFilter.sharedMesh = CreateMesh ();
    }


}
シェーダー
Shader "Unlit/MyBillBorad"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _MinimunPixel ("Minimum Pixel", Float) = 1
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

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


            struct v2f
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            sampler2D _MainTex;
            float _MinimunPixel;
            
            v2f vert (appdata_base v)
            {
                v2f o;

                float4 pos  = mul(UNITY_MATRIX_MVP, v.vertex);
                // (注1)v.normalをfloat4にキャストしないと、iOSでシェーダービルドに失敗する
                float4 norm = mul(UNITY_MATRIX_MVP, float4(v.normal, 0));

                // 法線情報から、縦横の引き伸ばし情報以外を捨てる
                norm = float4(sign(norm.x), sign(norm.y), 1, 1);

                // スクリーン上のピクセルサイズに合わせて引き延ばす
                float px = (_ScreenParams.z - 1) * _MinimunPixel;
                float py = (_ScreenParams.w - 1) * _MinimunPixel;
                // (注2)MVP変換後のpos.wにスケール値が格納されている
                float scale = pos.w;
                pos = pos + (norm * float4(px * scale, py * scale, 0, 0));

                // ピクセルシェーダーへ情報を渡す
                o.vertex = pos;
                o.uv     = v.texcoord;

                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                return tex2D(_MainTex, i.uv);
            }
            ENDCG
        }
    }
}  


(注1)
v.normalをfloat3のまま行列変換しようとすると、iOSでのみ「non-square matrices not supported (4x1)」というエラーが出てシェーダーコンパイルができない現象が出ました。UnityEditor、Androidは通ります。

(注2)
ピクセルサイズをそのまま足しこむと、上手くサイズ保障ができません(カメラから離れれば離れるほどpx,pyの値が相対的に小さくなるため)。pos.wに入っているスケール値をかけることで正しく動作します。この件については、以下のサイトが詳しいです。
3Dのタッチ操作と四次元の話 - ケイプロ奮闘記