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

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

ただいまの
回答率

89.64%

unity マウスクリックした部分の色を抽出する

解決済

回答 1

投稿

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

pigma689

score 3

unityで、マウスクリックした部分の画素の色を取得したい。
http://tsubakit1.hateblo.jp/entry/20131203/1386000440
など、いろいろ調べて参考にした結果、以下のように記述しました。
しかし、実行すると

ReadPixels was called to read pixels from system frame buffer, while not inside drawing frame.
UnityEngine.Texture2D:ReadPixels(Rect, Int32, Int32)

[d3d11] attempting to ReadPixels outside of RenderTexture bounds! Reading (77, 1877, 78, 1878) from (951, 563)
のエラーが出てしまいました。
また
ReadPixcels(Rect,distX,distY,: int, recalculateMipMaps: bool = true): void;
のdistX,distYの部分の意味が分かっていませんし、エラーを出しつつ得られたcolorの値はすべて同じでした。

エラーの意味とReadPixcelsの解説、正しいcolorの値が得られない理由を教えていただけないでしょうか。

using UnityEngine;

public class GetColor : MonoBehaviour
{
    public Color32 color;
    private Texture2D tex = null;

    void Start()
    {
        tex = new Texture2D(Screen.width, Screen.height, TextureFormat.RGBA32, false);
    }

    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            Vector2 pos = Input.mousePosition;
            Vector2 objPos = Camera.main.ScreenToWorldPoint(pos);
            tex.ReadPixels(new Rect(objPos.x, objPos.y, 1, 1), 0, 0);
            color = tex.GetPixel(0, 0);
            Debug.Log(color.ToString());
        }
    }
}
  • 気になる質問をクリップする

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

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

    クリップを取り消します

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

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

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

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

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

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

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

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

    質問の評価を下げる

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

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

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

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

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

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

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

    詳細な説明はこちら

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

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

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

回答 1

checkベストアンサー

+2

ReadPixelsの引数のsourceは読み取り元(今回の場合は画面)のピクセル単位での読み取り範囲、destXdestYは書き込み先(今回の場合はtex)のピクセル単位の位置を表します。おそらくすでにリファレンスをご覧になったかと思いますが、パラメーター説明の「読み込み元のビューの長方形領域」はまだマシな気がしますが「テクスチャ内の読みこむピクセルの水平位置/垂直位置」という表現は何だか意味不明ですね...
英語版だと「Horizontal pixel position in the texture to place the pixels that are read」...つまり「読み込まれたピクセルを置く(書き込む)、テクスチャ内の水平ピクセル位置」と説明されており、こっちのほうがしっくりくるように思います。

エラー自体は取得範囲が不適切であることを言っているようですが、ご質問者さんのコードでテクスチャのサイズやposの座標系をテラシュールブログさんのものと変えているのは、それを修正しようと試みてのことでしょうか?
今回検討するべきなのはReadPixelsを行うタイミングのように思います。Updateだと描画が終わっておらず早すぎるでしょうから、テラシュールブログさんと同様にOnPostRenderを使ってはいかがでしょう。
ただし、試してみたかぎりではOnPostRender内だとGetMouseButtonDownを正しく取れないっぽいので、Updateも併用する形がいいんじゃないかと思います。

using UnityEngine;

public class GetColor : MonoBehaviour
{
    public Color32 color;
    private GetColorHelper helper;

    void Start()
    {
        // GetColorスクリプト自体をカメラにアタッチしてもいいなら、GetColorにUpdateと
        // OnPostRenderを両方実装して連携させることができるかと思いますが、GetColorは
        // 別のオブジェクトにアタッチしたい...といった事情がある場合は、GetColor本体と
        // カメラ用コンポーネントの2つに担当を分けて連携させることになるかと思います
        // 今回の例では補助コンポーネントとしてGetColorHelperを用意し、これをメインカメラに
        // アタッチしてOnPostRender部分を担当させることにしました
        helper = Camera.main.GetComponent<GetColorHelper>();
        if (helper == null)
        {
            helper = Camera.main.gameObject.AddComponent<GetColorHelper>();
        }
    }

    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            color = helper.Tex.GetPixel(0, 0);
            Debug.Log(color.ToString());
        }
    }

    [RequireComponent(typeof(Camera))]
    private class GetColorHelper : MonoBehaviour
    {
        public Texture2D Tex { get; private set; }

        void Awake()
        {
            Tex = new Texture2D(1, 1, TextureFormat.RGBA32, false);
        }

        void OnPostRender()
        {
            Vector2 pos = Input.mousePosition; 

            // 座標系がOpenGL方式かDirectX方式かによって上下が変わるのを考慮する
            if (SystemInfo.graphicsUVStartsAtTop)
            {
                pos.y = Screen.height - 1 - pos.y;
            }

            // マウスポインタが画面外にあるとReadPixelsに失敗するため、座標を範囲内におさめる
            float x = Mathf.Clamp(pos.x, 0.0f, Screen.width - 1);
            float y = Mathf.Clamp(pos.y, 0.0f, Screen.height - 1);

            Tex.ReadPixels(new Rect(x, y, 1, 1), 0, 0);
        }
    }
}

追記
ご指摘ありがとうございます。当初のコードはWindows版でしか試していなかったのですが、確かにMac版でMetalを使用している場合は座標の上下が逆転してしまいますね...
その場しのぎ的ですみませんが、下記のようにMetalの場合に座標の上下反転を行わせないようにしてはどうでしょうか。
もし他にうまくいかない条件が見つかりましたらコメントください(とはいえMacとWindows以外にすぐ試せる環境を持っておらず、お力になれないかもしれませんが...)。

using UnityEngine;
using UnityEngine.Rendering;

namespace GetPixelColor
{
    public class GetColor : MonoBehaviour
    {
        public Color32 color;
        private GetColorHelper helper;

        void Start()
        {
            helper = Camera.main.GetComponent<GetColorHelper>();
            if (helper == null)
            {
                helper = Camera.main.gameObject.AddComponent<GetColorHelper>();
            }
        }

        void Update()
        {
            if (Input.GetMouseButtonDown(0))
            {
                color = helper.Tex.GetPixel(0, 0);
                Debug.Log(color.ToString());
            }
        }

        [RequireComponent(typeof(Camera))]
        private class GetColorHelper : MonoBehaviour
        {
            public Texture2D Tex { get; private set; }

            void Awake()
            {
                Tex = new Texture2D(1, 1, TextureFormat.RGBA32, false);
            }

            void OnPostRender()
            {
                Vector2 pos = Input.mousePosition; 

                // 座標系がOpenGL方式かDirectX方式かによって上下が変わるのを考慮する
                // Metalの場合、例外的に反転は行わないようにした
                if (SystemInfo.graphicsUVStartsAtTop && SystemInfo.graphicsDeviceType != GraphicsDeviceType.Metal)
                {
                    pos.y = Screen.height - 1 - pos.y;
                }

                float x = Mathf.Clamp(pos.x, 0.0f, Screen.width - 1);
                float y = Mathf.Clamp(pos.y, 0.0f, Screen.height - 1);

                Tex.ReadPixels(new Rect(x, y, 1, 1), 0, 0);
            }
        }
    }
}

また、Planeに貼り付けた色が取れないという現象は私の試した限りでは再現させられなかったのですが(Planeであっても他の3Dオブジェクトと違いはないはずなので、Cubeで成功したのならPlaneでもいける気がするのですが...)、ImageなどUIオブジェクトの色を取れないという現象は確認できました。
おそらくCanvasの「Render Mode」が「Screen Space - Overlay」になっているんじゃないでしょうか?どうやらこのモードの場合、UIの描画はOnPostRenderよりも後のタイミングになるため失敗したものと思われます。もしRender Modeを変えてもかまわないのなら、これを「Screen Space - Camera」や「World Space」 にすればOnPostRenderより先にUI描画が完了し、色を取ってくることができるでしょう。
あるいは、下記のようにWaitForEndOfFrameのタイミングならScreen Space - Overlayでもいけそうな感じでした。どうしてもScreen Space - Overlayでないと困るようでしたらこちらも試してみてもいいかと思います。

using System.Collections;
using UnityEngine;

namespace GetPixelColor
{
    public class GetColor : MonoBehaviour
    {
        public Color32 color;
        private GetColorHelper helper;

        void Start()
        {
            helper = Camera.main.GetComponent<GetColorHelper>();
            if (helper == null)
            {
                helper = Camera.main.gameObject.AddComponent<GetColorHelper>();
            }
        }

        void Update()
        {
            if (Input.GetMouseButtonDown(0))
            {
                color = helper.Tex.GetPixel(0, 0);
                Debug.Log(color.ToString());
            }
        }

        [RequireComponent(typeof(Camera))]
        private class GetColorHelper : MonoBehaviour
        {
            public Texture2D Tex { get; private set; }

            void Awake()
            {
                Tex = new Texture2D(1, 1, TextureFormat.RGBA32, false);
            }

            IEnumerator Start()
            {
                var waitForEndOfFrame = new WaitForEndOfFrame();

                while (true)
                {
                    yield return waitForEndOfFrame;

                    Vector2 pos = Input.mousePosition;

                    float x = Mathf.Clamp(pos.x, 0.0f, Screen.width - 1);
                    float y = Mathf.Clamp(pos.y, 0.0f, Screen.height - 1);

                    Tex.ReadPixels(new Rect(x, y, 1, 1), 0, 0);
                }
            }
        }
    }
}

追記
まず、下記のようなメインテクスチャ・メインカラーで塗るだけのシンプルなUnlitシェーダーを用意しました。

Shader "Unlit/UnlitAlbedo"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Color ("Color", Color) = (1, 1, 1, 1)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

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

            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _Color;

            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

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

通常のレンダリングの場合下図のように見えるシーンを...

図1

この代替シェーダーでシーンをレンダリングした場合、下図のように環境の映り込みや陰影がない単純な色塗り状態の映像が得られます。実際にはこの映像を画面上に表示するわけではなく、RenderTexture上に描画して色取り元とすることになりますね。

図2

また、この追加レンダリングはわざわざ画面全体について行う必要はなく、色を取りたい地点の1ピクセル分だけの大きさがあれば十分なはずです。そこで、このレンダリングの時だけ一時的に投影行列に手を加えて注目点だけを拡大し、それを1×1サイズのRenderTextureに描画することにしました。描画面積が大幅に節約でき、レンダリングコストを低減できると思われます。

色取りスクリプトは下記のようにしてみました。補助スクリプトは不要になったため廃止しています。

using System.Collections;
using UnityEngine;

public class AlbedoColorPicker : MonoBehaviour
{
    public Color32 color;
    public Renderer targetRenderer;

    private Camera mainCamera;
    private Shader alternativeShader;
    private Texture2D pixelTexture;

    private void Start()
    {
        this.mainCamera = Camera.main;
        this.alternativeShader = Shader.Find("Unlit/UnlitAlbedo");
        this.pixelTexture = new Texture2D(1, 1, TextureFormat.RGBA32, false);
    }

    private void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            // マウスポインタの位置を狙う変換行列を作り...
            var screenScale = new Vector2(Screen.width, Screen.height);
            var pixelPosition = ((Vector2)Input.mousePosition * 2.0f) - screenScale;
            var pixelMatrix = Matrix4x4.TRS(
                -pixelPosition,
                Quaternion.identity,
                new Vector3(screenScale.x, screenScale.y, 1.0f));

            // projectionMatrixの後段に追加の変換を加え、alternativeShaderを使ってレンダリングする
            var targetTexture = this.mainCamera.targetTexture;
            var projectionMatrix = this.mainCamera.projectionMatrix;
            var renderTexture = RenderTexture.GetTemporary(1, 1);
            this.mainCamera.targetTexture = renderTexture;
            this.mainCamera.projectionMatrix = pixelMatrix * projectionMatrix;
            this.mainCamera.RenderWithShader(this.alternativeShader, null);
            this.mainCamera.targetTexture = targetTexture;
            this.mainCamera.ResetProjectionMatrix();

            // renderTextureの内容をpixelTextureに読み取り、さらにpixelTextureから色を取り出す
            var activeTexture = RenderTexture.active;
            RenderTexture.active = renderTexture;
            this.pixelTexture.ReadPixels(new Rect(0, 0, 1, 1), 0, 0);
            RenderTexture.active = activeTexture;
            RenderTexture.ReleaseTemporary(renderTexture);
            this.color = this.pixelTexture.GetPixel(0, 0);

            // 色設定対象がセットされていれば、それに取得された色をセットする
            if (this.targetRenderer != null)
            {
                this.targetRenderer.material.color = this.color;
            }
        }
    }
}

動かしてみると下図のようになりました。取った色に陰影付けの影響が及んでいないことをアピールするため、色の適用先はPlaneではなく画面右側にあるSphereにしています。

図3

投稿

編集

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2019/09/06 07:46

    「照明効果自体もう必要ないと考えていて」とのお返事を見落としておりました。確かにUnityが用意しているUnlitシェーダーには「単色塗り」と「テクスチャ塗り」はあっても、二つを併せ持つシェーダーはないようですね。
    でしたら、追記の中で申し上げたUnlitAlbedoをそのままマテリアルのシェーダーとして使用してみてはいかがでしょうか?これは両者を複合させたような形になっていますので、テクスチャを貼った上でさらにcolorプロパティから色をセットできるはずです。最終的にレンダリングされる色は、提示しましたコードの通りテクスチャ色と単色を乗算合成した色になります。

    キャンセル

  • 2019/09/09 15:26

    イメージした通りにできました。
    丁寧に教えてくださり、本当にありがとうございました!

    キャンセル

  • 2019/10/17 22:39

    すみません、別の方のご質問( https://teratail.com/questions/217389 )にてコードを再利用するために見直していたところ、今さらながらピクセル拡大行列の拡大量が足りておらず、2×2ピクセル相当までしか拡大できていなかったことに気付きました。
    まさしく1ピクセル単位での色取りが求められないのであればさほど問題にはならないとは思うのですが、念のため修正しました。

    キャンセル

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

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