質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

90.12%

シェーダーファイルをShader.Findで読み込むとビルド後動作しない

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 104

masa_000

score 7

private Material equirectangularMaterial;
this.equirectangularMaterial = new Material(Shader.Find(@"Hidden\CubeToEquirectangular"));


Shader.Findという関数を使ってシェーダーファイルを読み込もうとしています。ビルド前は上記のように書いてうまくいっているのですがビルド後に実行するとNullReferenceExceptionになり実行できません。
調べてみるとShader.Findはビルド後はこういう動作をするので、Inspecterでhttps://qiita.com/shirai/items/3d2e3ff9e0d9a55a2e23のようにすると解決するらしいのですが、なんとかスクリプト上でビルド後もシェーダーファイルを読み込むことはできないのでしょうか?ビルドされたものに処理を差し込みたいのでスクリプト上で解決したいのです。

Unity5.6.2f1です。よろしくお願いします。

追記
処理が差し込めないのはStereoEquirectangularTextureUpdaterだけなので、StereoCubemapUpdaterで作ったCubemapLeft,CubemapRightをファイルかなにかに出力して、後でそのファイルをシェーダーファイルを読み込める環境で処理すればできないでしょうか。
それで質問なのですが、StereoCubemapUpdaterでつくったCubemapLeft,CubemapRightをStereoEquirectangularTextureUpdaterで再度利用するためにはどんな形式で出力して保存すればいいのでしょうか?

    void savePng(String filename, int count, RenderTexture saveRT)
    {

        Texture2D tex = new Texture2D(saveRT.width, saveRT.height, TextureFormat.RGB24, false);
        RenderTexture.active = saveRT;
        tex.ReadPixels(new Rect(0, 0, saveRT.width, saveRT.height), 0, 0);
        tex.Apply();

        // Encode texture into PNG
        byte[] bytes = tex.EncodeToPNG();
        UnityEngine.Object.Destroy(tex);

        var name = @"C:\Users\Username\Desktop\unity\Captures\" + filename + count.ToString("0") + ".png";
        //Write to a file in the project folder
        File.WriteAllBytes(name, bytes);

    }


とりあえず上記の関数でCubemapLeftをpngにしてみたのですが、下のような画像になってしまってキューブマップではないと思います。
イメージ説明

本当に度々申し訳ありません。わかりにくい文章で恐縮ですが時間があるときでよいので回答よろしくお願いします。

追記

void Update()
{
    Texture2D tex;
    private Color[] CubeMapColors;
    string path = @"C:\Users\Username\Desktop\unity\Captures\";
    CubemapFace[] cubemapFaces =
    {
        CubemapFace.PositiveX, CubemapFace.NegativeX,
        CubemapFace.PositiveY, CubemapFace.NegativeY,
        CubemapFace.PositiveZ, CubemapFace.NegativeZ
    };
    foreach (CubemapFace face in cubemapFaces)
    {
        name = path + @"Right\" + "frame" + framecount.ToString("0") + face.ToString() + ".png";
        tex = ReadTexture(name, 1024, 1024);
        CubeMapColors = tex.GetPixels();
        cubemapRight.SetPixels(CubeMapColors, face);
        cubemapRight.Apply();
    }
}
byte[] ReadPngFile(string path)
{
     FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read);
     BinaryReader bin = new BinaryReader(fileStream);
     byte[] values = bin.ReadBytes((int)bin.BaseStream.Length);

     bin.Close();

     return values;
}

Texture2D ReadTexture(string path, int width, int height)
{
     byte[] readBinary = ReadPngFile(path);

     Texture2D texture = new Texture2D(width, height,TextureFormat.RGB24, false);
     texture.LoadImage(readBinary);

     return texture;
}


すみませんが最後にもう一つだけ質問してよろしいでしょうか。
Cubemap.SetPixelsを使って6枚の画像をRenderTextureにセットするスクリプトを書いてみたのですが、RenderTextureにうまくセットできていないようです。ファイルから画像を読み込むことはできているようです。Cubemap.SetPixelsの使い方かヒントを教えていだだけないでしょうか。
質問ばかりで申し訳ありませんが時間のあるときでいいのでよろしくお願いします。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

+1

では、インスペクター上で参照をセットしておくことは可能でしょうか?
スクリプトに[SerializeField] private Shader equirectangularShader;のようにフィールドを追加して、インスペクター上の欄にプロジェクトビューからシェーダーファイルをドラッグ&ドロップし、マテリアル作成部分ではthis.equirectangularMaterial = new Material(this.equirectangularShader);という風にするのではどうでしょう。

追記
すみませんが、独自のシェーダーを持ち込む方法はわかりませんでした...
アプローチを変えて、「Unity5.6.2f1でメインカメラの両目の映像をレンダリングしたい」で申し上げた正距円筒図法展開をビルトインシェーダーのみで行えないか検討してみました。
デフォルトの状態ですとAlways Included ShadersにHidden/CubeCopyが入っています。もしそのゲームでそこがいじられていなければ、それを使うことができるんじゃないかともくろみました。

CubeCopyは単純に入力されたUV座標からキューブマップをサンプリングして出力するだけのシェーダーですので、円筒図法展開に利用するには入力するメッシュをちょっと工夫してやることになるでしょう。
そこで頂点座標は四角い展開図の形、UV座標は3次元の球体の形のメッシュを作り、それをDrawMeshNowで描画することにしました。
前回のStereoEquirectangularTextureUpdaterをベースにしており相変わらずSerializeFieldやらを使っていますので、ご質問者さんの用途には直接利用できないかもしれません。使えそうな部分だけご参考にしていただきたく思います。

using System;
using System.Collections.Generic;
using UnityEngine;

public class StereoEquirectangularTextureUpdaterBuiltinShader : MonoBehaviour
{
    public enum Arrangement
    {
        TopAndBottom,
        SideBySide
    }

    public enum FieldOfView
    {
        Full,
        Half
    }

    [SerializeField] private StereoCubemapUpdater stereoCubemapUpdater;
    [SerializeField] private RenderTexture equirectangularTexture;
    [SerializeField] private bool useLocalDirection;
    [SerializeField] private Arrangement arrangement;
    [SerializeField] private FieldOfView fieldOfView;
    [SerializeField] private int sphereNetDivision = 16;

    private RenderTexture cubemapLeft;
    private RenderTexture cubemapRight;
    private Material equirectangularMaterial;
    private Mesh sphereNet;
    private List<Vector3> sphereNetUvs;
    private List<Vector3> rotatedSphereNetUvs;
    private Mesh hemisphereNet;
    private List<Vector3> hemisphereNetUvs;
    private List<Vector3> rotatedHemisphereNetUvs;

    // 球面の展開図を表すメッシュを生成するメソッド
    // divisionを大きくするほど正確に展開できるはずだが、その分頂点数が増えるので
    // ローカル展開の場合のUV更新にかかるコストが大きくなると思われる
    private Mesh CreateSphereNet(int division, bool asHemisphere, out List<Vector3> uvs)
    {
        var deltaAngle = Mathf.PI / division;
        var divisionX = (asHemisphere ? 1 : 2) * division;
        var divisionY = division;
        var divisionXPlus1 = divisionX + 1;
        var divisionYPlus1 = divisionY + 1;
        var deltaX = 2.0f / divisionX;
        var deltaY = 2.0f / divisionY;
        var x = new float[divisionXPlus1];
        var phi0 = -(asHemisphere ? 0.5f : 1.0f) * Mathf.PI;
        var sinPhi = new float[x.Length];
        var cosPhi = new float[x.Length];
        var y = new float[divisionYPlus1];
        var theta0 = -0.5f * Mathf.PI;
        var sinTheta = new float[y.Length];
        var cosTheta = new float[y.Length];
        for (var i = 0; i <= divisionX; i++)
        {
            var angle = phi0 + (deltaAngle * i);
            sinPhi[i] = Mathf.Sin(angle);
            cosPhi[i] = Mathf.Cos(angle);
            x[i] = (deltaX * i) - 1.0f;
        }

        for (var i = 0; i <= divisionY; i++)
        {
            var angle = theta0 + (deltaAngle * i);
            sinTheta[i] = Mathf.Sin(angle);
            cosTheta[i] = Mathf.Cos(angle);
            y[i] = (deltaY * i) - 1.0f;
        }

        var vertices = new Vector3[divisionXPlus1 * divisionYPlus1];
        uvs = new List<Vector3>(vertices.Length);
        for (var j = 0; j <= divisionY; j++)
        {
            var o = j * divisionXPlus1;
            for (var i = 0; i <= divisionX; i++)
            {
                vertices[o + i] = new Vector3(x[i], y[j], 0.0f);
                uvs.Add(new Vector3(sinPhi[i] * cosTheta[j], sinTheta[j], cosPhi[i] * cosTheta[j]));
            }
        }

        var triangles = new int[divisionX * divisionY * 6];
        for (var j = 0; j < divisionY; j++)
        {
            var o = j * divisionX;
            for (var i = 0; i < divisionX; i++)
            {
                var k = (o + i) * 6;
                triangles[k] = (divisionXPlus1 * j) + i;
                triangles[k + 1] = (divisionXPlus1 * (j + 1)) + i;
                triangles[k + 2] = triangles[k] + 1;
                triangles[k + 3] = triangles[k + 1] + 1;
                triangles[k + 4] = triangles[k + 2];
                triangles[k + 5] = triangles[k + 1];
            }
        }

        var net = new Mesh();
        net.vertices = vertices;
        net.SetUVs(0, uvs);
        net.triangles = triangles;
        net.MarkDynamic();
        return net;
    }

    private void Start()
    {
        this.cubemapLeft = this.stereoCubemapUpdater.CubemapLeft;
        this.cubemapRight = this.stereoCubemapUpdater.CubemapRight;
        this.equirectangularMaterial = new Material(Shader.Find("Hidden/CubeCopy"));
        this.sphereNet = this.CreateSphereNet(this.sphereNetDivision, false, out this.sphereNetUvs);
        this.hemisphereNet = this.CreateSphereNet(this.sphereNetDivision, true, out this.hemisphereNetUvs);
        this.rotatedSphereNetUvs = new List<Vector3>(this.sphereNetUvs);
        this.rotatedHemisphereNetUvs = new List<Vector3>(this.hemisphereNetUvs);
    }

    private void LateUpdate()
    {
        // 描画先座標の移動・伸縮はMatrix4x4で表現する
        Matrix4x4 leftVertexTransform;
        Matrix4x4 rightVertexTransform;
        switch (this.arrangement)
        {
            case Arrangement.TopAndBottom:
                leftVertexTransform = Matrix4x4.TRS(
                    new Vector3(0.0f, 0.5f, 0.0f),
                    Quaternion.identity,
                    new Vector3(1.0f, 0.5f, 1.0f));
                rightVertexTransform = Matrix4x4.TRS(
                    new Vector3(0.0f, -0.5f, 0.0f),
                    Quaternion.identity,
                    new Vector3(1.0f, 0.5f, 1.0f));
                break;
            case Arrangement.SideBySide:
                leftVertexTransform = Matrix4x4.TRS(
                    new Vector3(-0.5f, 0.0f, 0.0f),
                    Quaternion.identity,
                    new Vector3(0.5f, 1.0f, 1.0f));
                rightVertexTransform = Matrix4x4.TRS(
                    new Vector3(0.5f, 0.0f, 0.0f),
                    Quaternion.identity,
                    new Vector3(0.5f, 1.0f, 1.0f));
                break;
            default:
                throw new ArgumentOutOfRangeException();
        }

        // ローカル展開の場合はUV座標を回転しメッシュに再設定する
        var rotation = this.stereoCubemapUpdater.transform.rotation;
        switch (this.fieldOfView)
        {
            case FieldOfView.Full:
                if (this.useLocalDirection)
                {
                    for (var i = 0; i < this.sphereNetUvs.Count; i++)
                    {
                        this.rotatedSphereNetUvs[i] = rotation * this.sphereNetUvs[i];
                    }

                    this.sphereNet.SetUVs(0, this.rotatedSphereNetUvs);
                }
                else
                {
                    this.sphereNet.SetUVs(0, this.sphereNetUvs);
                }

                this.sphereNet.RecalculateBounds();
                break;
            case FieldOfView.Half:
                if (this.useLocalDirection)
                {
                    for (var i = 0; i < this.hemisphereNetUvs.Count; i++)
                    {
                        this.rotatedHemisphereNetUvs[i] = rotation * this.hemisphereNetUvs[i];
                    }

                    this.hemisphereNet.SetUVs(0, this.rotatedHemisphereNetUvs);
                }
                else
                {
                    this.hemisphereNet.SetUVs(0, this.hemisphereNetUvs);
                }

                this.hemisphereNet.RecalculateBounds();
                break;
            default:
                throw new ArgumentOutOfRangeException();
        }

        // メッシュをレンダーテクスチャ上にレンダリング
        if (this.equirectangularMaterial.SetPass(0))
        {
            var activeTexture = RenderTexture.active;
            RenderTexture.active = this.equirectangularTexture;
            var net = this.fieldOfView == FieldOfView.Full ? this.sphereNet : this.hemisphereNet;
            GL.PushMatrix();
            GL.LoadProjectionMatrix(Matrix4x4.identity);
            this.equirectangularMaterial.mainTexture = this.cubemapLeft;
            Graphics.DrawMeshNow(net, leftVertexTransform);
            this.equirectangularMaterial.mainTexture = this.cubemapRight;
            Graphics.DrawMeshNow(net, rightVertexTransform);
            GL.PopMatrix();
            RenderTexture.active = activeTexture;
        }
    }
}

注意点として、前回の独自シェーダーによる方法ではピクセル単位の正確さで展開されるのに対し、今回の方法では展開に使うメッシュをある程度細かくしないとゆがみが目立ってしまう弱点があります。
たとえばsphereNetDivisionを4に下げると、こんな風になってしまいます...

図

キューブマップをファイルに保存する件について追記
以前申し上げましたように、キューブマップには6つの面があります。保存メソッドを下記のようにしてみるとどうでしょうか?

void savePng(string filename, int count, RenderTexture saveRT)
{
    CubemapFace[] cubemapFaces =
    {
        CubemapFace.PositiveX, CubemapFace.NegativeX,
        CubemapFace.PositiveY, CubemapFace.NegativeY,
        CubemapFace.PositiveZ, CubemapFace.NegativeZ
    };
    Texture2D tex = new Texture2D(saveRT.width, saveRT.height, TextureFormat.RGB24, false);

    foreach (CubemapFace face in cubemapFaces)
    {
        Graphics.SetRenderTarget(saveRT, 0, face);
        tex.ReadPixels(new Rect(0, 0, saveRT.width, saveRT.height), 0, 0);
        tex.Apply();

        // Encode texture into PNG
        byte[] bytes = tex.EncodeToPNG();

        var name = @"C:\Users\Username\Desktop\unity\Captures\" + filename + count.ToString("0") + face.ToString() + ".png";
        //Write to a file in the project folder
        File.WriteAllBytes(name, bytes);
    }

    UnityEngine.Object.Destroy(tex);
}


保存された画像からキューブマップを作る場合、書き出し時と逆にCubemap.SetPixelsfaceをそれぞれのCubemapFaceに切り替えながら計6回セットしてやることになるかと思います。

画像ファイルからキューブマップを作る件について追記

提示いたしましたコードのcubemapLeftcubemapRightCubemap型ではなく「dimensionCubeにしたRenderTexture型」です。これはTexture2Dと普通のRenderTextureとの関係と同様で、前者は画像ファイルなどCPU側のデータをGPU側でのテクスチャとして使えるように送り込む用途、後者はGPU側でテクスチャ上にレンダリングを行う用途に適していると言えるでしょう。
今回のケースでは画像ロード先のキューブマップがRenderTextureである必要はないので、Cubemapに変更してはいかがでしょうか。

private Cubemap cubemapLeft;
private Cubemap cubemapRight;


そして初期化時もCubemapを作るようにしてやれば、cubemapLeftcubemapRightに対してSetPixelsが使えるようになるはずです。

this.cubemapLeft = new Cubemap(1024, TextureFormat.RGB24, false);
this.cubemapRight = new Cubemap(1024, TextureFormat.RGB24, false);


それ以降の円筒投影するGraphics.Blit(this.cubemapRight, this.equirectangularTexture, this.equirectangularMaterial);の部分ではcubemapLeftcubemapRightRenderTextureであってもCubemapであっても区別せず扱えるはずですので、コードはそのままでいいかと思います。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/10/10 00:59

    独自シェーダーを読み込ませるのは困難そうでしたので代替案を考えてみましたが、いかがでしょうか。
    もしこれでもダメだとなると、あとは完全にシェーダーに頼らず展開するとかでしょうかね?
    おそらく絶望的に重くなりそうな気配がしますが、仕方ないかもしれません...

    キャンセル

  • 2019/10/11 11:28

    画像ファイルの内容をキューブマップとして使えるようにする件については、cubemapLeftとcubemapRightはCubemapではなくRenderTextureですので、対策としては画像ファイルから読み込んだ色データをRenderTextureであるcubemapRight上にレンダリングしてやるか、cubemapRightの型をCubemapに変えてやるかのいずれかになるでしょう。今回は状況的に後者の方が適切だろうと思います。

    キャンセル

  • 2019/10/12 08:03

    無事完成しました。正直自分のできる限界を超えていたのでBongoさんの助けがなければ完成することはできませんでした。何度も何度も教えていただき本当にありがとうございました。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 90.12%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる