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

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

ただいまの
回答率

88.03%

unity Texture2Dをリサイズしたいのですが,ピントが少しボケてしまいます。

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 1
  • VIEW 3,082

score 26

前提・実現したいこと

Texture2Dをリサイズしたいのですが解像度? ピントが少しボケています。
良い方法があれば教えて頂けませんか。
scaner
イメージ説明
resize (pcにて)
イメージ説明
resize (スマホにて)
イメージ説明
元の画像 (640,1136)
![イメージ説明](d597ffd0c016f6987c8ed6561f1f2f00.jpeg)
シェーダーのリサイズ
イメージ説明
canvas/image
イメージ説明
イメージ説明
canvas
イメージ説明

該当のソースコード

   private static Texture2D GetResized(Texture2D texture, int width, int height)
    {
        // リサイズ後のサイズを持つRenderTextureを作成して書き込む
        var rt = RenderTexture.GetTemporary(width, height);
        Graphics.Blit(texture, rt);

        // リサイズ後のサイズを持つTexture2Dを作成してRenderTextureから書き込む
        var preRT = RenderTexture.active;
        RenderTexture.active = rt;
        var ret = new Texture2D(width, height);
        ret.ReadPixels(new Rect(0, 0, width, height), 0, 0);
        ret.Apply();
        RenderTexture.active = preRT;

        RenderTexture.ReleaseTemporary(rt);
        return ret;
    }
  • 気になる質問をクリップする

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

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

    クリップを取り消します

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

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

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

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

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

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

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

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

    質問の評価を下げる

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

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

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

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

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

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

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

    詳細な説明はこちら

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

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

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

質問への追記・修正、ベストアンサー選択の依頼

  • 29507-01

    2020/05/04 15:34

    画像を添付しましたがこの程度だと我慢する?

    キャンセル

  • sakura_hana

    2020/05/04 16:00

    個人的にはscanerの方が荒く見えますしresizeのボケ方もこれぐらいならいいのでは?と思いますが、許容値は個人差があります。
    「この程度ならよしとする」か「別の方法を調べる」か「200,200以外の値(例えば640,640とか)に出来ないか調べ直す」かはご自分で決めてください。
    (別の方法案として「640,640の画像を200,200の領域に表示」とかも考えられますがシステム上それでいいのかと、結構縮小率が高いので綺麗に見えるか定かでないです)

    キャンセル

  • 29507-01

    2020/05/04 16:07

    有難うございます。
    参考にします。

    キャンセル

回答 1

checkベストアンサー

0

確かにちょっとぼやけているように感じますね。
元の画像が

図1

のとき(サイズ640×640)、ご質問者さんの方法で200×200に縮小すると

図2

となるのに対して、Photoshopの「バイリニア法」だと

図3

「バイキュービック法 - シャープ (縮小)」だと

図4

といった結果になり、ぼやけの程度が小さく抑えられています。
毎フレーム行う処理でないのなら多少処理コストがかかってもかまわないでしょうから、自前でバイキュービック法のような縮小方法を実装するとか、あるいはアンシャープマスク処理を加えてやるのがいいかもしれません。

あるいは単純に、縮小前のテクスチャ中において縮小後の1ピクセルが占める領域を何カ所もサンプリングするというのはどうでしょうか。
たとえば下記のようなシェーダーを用意しておいて...

// 字数節約のため削除しました...削除前のコードは編集履歴をご参照ください

ご質問者さんのスクリプトを下記のように改変して実行したところ...

using System.IO;
using UnityEditor;
using UnityEngine;

public static class TextureResizer
{
    private static Material overSamplingMaterial;

    [MenuItem("Utility/Resize Selected Texture to 200 x 200")]
    public static void ResizeTextureTo200x200()
    {
        const int newWidth = 200;
        const int newHeight = 200;
        const int loopCount = 16;
        var targetTexture = Selection.activeObject as Texture2D;
        if (targetTexture == null)
        {
            return;
        }

        var targetPath = AssetDatabase.GetAssetPath(targetTexture);
        if (string.IsNullOrEmpty(targetPath))
        {
            return;
        }

        var targetDirectory = Path.GetDirectoryName(targetPath);
        var targetName = Path.GetFileNameWithoutExtension(targetPath);
        var targetExtension = Path.GetExtension(targetPath);
        var newName = $"{targetName}({newWidth}x{newHeight})";
        var newPath = $"{targetDirectory}{Path.DirectorySeparatorChar}{newName}{targetExtension}";
        var newTexture = GetResized(targetTexture, newWidth, newHeight, loopCount);
        File.WriteAllBytes(newPath, newTexture.EncodeToPNG());
        AssetDatabase.ImportAsset(newPath);
    }

    private static Texture2D GetResized(Texture2D texture, int width, int height, int loopCount)
    {
        // オーバーサンプリング用マテリアルを作成する
        if (overSamplingMaterial == null)
        {
            overSamplingMaterial = new Material(Shader.Find("Hidden/OverSampling"));
        }

        // まず元と同じサイズのRenderTextureに書き込む
        var sourceRT = RenderTexture.GetTemporary(texture.width, texture.height);
        Graphics.Blit(texture, sourceRT);

        // filterModeをPointにしておき、サンプリング時に注目点以外の色が混じらないようにしておく
        sourceRT.filterMode = FilterMode.Point;

        // リサイズ後のサイズを持つRenderTextureを作成して、オーバーサンプリングマテリアルを使って書き込む
        var rt = RenderTexture.GetTemporary(width, height);
        overSamplingMaterial.SetInt("_LoopCount", Mathf.Max(loopCount, 1));
        Graphics.Blit(sourceRT, rt, overSamplingMaterial);

        // リサイズ後のサイズを持つTexture2Dを作成してRenderTextureから書き込む
        var preRT = RenderTexture.active;
        RenderTexture.active = rt;
        var ret = new Texture2D(width, height);
        ret.ReadPixels(new Rect(0, 0, width, height), 0, 0);
        ret.Apply();
        RenderTexture.active = preRT;
        RenderTexture.ReleaseTemporary(rt);
        RenderTexture.ReleaseTemporary(sourceRT);
        return ret;
    }
}

loopCountが1だと(これは実質的にニアレストネイバー法と同等のはずです)...

図5

loopCountが4だと(1ピクセルにつき16点サンプリング)...

図6

loopCountが16だと(1ピクセルにつき256点サンプリング)...

図7

となりました。だいぶ精密さが向上したんじゃないかと思いますが、いかがでしょうか?

正方形トリミングについて

まずシェーダーコードは下記のように変更して、UV座標を操作することで画像切り出し範囲を調整できるようにしておきます。

Shader "Hidden/OverSampling"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Cull Off ZWrite Off ZTest Always

        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 _TilingAndOffset;
            int _LoopCount;

            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = (v.uv * _TilingAndOffset.xy) + _TilingAndOffset.zw;
                return o;
            }

            float4 frag (v2f i) : SV_Target
            {
                float2 texelSize = fwidth(i.uv);
                float2 deltaUV = texelSize / _LoopCount;
                float2 uv0 = i.uv + ((deltaUV - texelSize) * 0.5);
                float4 col = 0.0;
                for (int j = 0; j < _LoopCount; j++)
                {
                    for (int i = 0; i < _LoopCount; i++)
                    {
                        col += tex2D(_MainTex, uv0 + (float2(i, j) * deltaUV));
                    }
                }
                return col / (_LoopCount * _LoopCount);
            }
            ENDCG
        }
    }
}

そして制御用のスクリプトは下記のようにしました。
ご追記のスクリーンショットを拝見しますに、どうやらImageを使って縮小後のテクスチャを表示している様子ですね。表示用のスプライトはどのように用意していますでしょうか?私の場合はさしあたり縮小後のテクスチャを使ったスプライトを新規作成し、それをImageコンポーネントにセットしてやることにしました。
スクリプトはサイズ200×200のImageオブジェクトにアタッチして使うことを想定しており、Start内で元テクスチャをロード・縮小テクスチャを生成・スプライトを作成してImageにセット...の処理を行っています。

using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(Image))]
public class ResizedImageLoader : MonoBehaviour
{
    private static Material overSamplingMaterial;

    [SerializeField] private bool useActualSize;

    private void Start()
    {
        var texture2D = Resources.Load<Texture2D>("motogp");

        // 目標サイズは200x200決め打ちではなく、Imageのサイズを見て決めることにする
        var image = this.GetComponent<Image>();
        var rectTransform = this.transform as RectTransform;
        var sizeDelta = rectTransform.sizeDelta;
        var toWidth = Mathf.RoundToInt(sizeDelta.x);
        var toHeight = Mathf.RoundToInt(sizeDelta.y);

        // インスペクターの「Use Actual Size」がオンの場合
        // 目標サイズを最終的な表示サイズに合わせて調整する
        if (this.useActualSize)
        {
            var canvas = image.canvas;
            var canvasCamera = canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : canvas.worldCamera;
            var corners = new Vector3[4];
            rectTransform.GetWorldCorners(corners);
            var bottomLeft = RectTransformUtility.WorldToScreenPoint(canvasCamera, corners[0]);
            var topRight = RectTransformUtility.WorldToScreenPoint(canvasCamera, corners[2]);
            var actualSize = topRight - bottomLeft;
            toWidth = Mathf.RoundToInt(Mathf.Abs(actualSize.x));
            toHeight = Mathf.RoundToInt(Mathf.Abs(actualSize.y));
        }

        // 元のテクスチャから切り出すべき領域を求める
        // テクスチャの中心からなるべく大きな矩形を切り出したいので
        // まずテクスチャの高さに縦横比を掛けて希望切り出し幅を求める
        // テクスチャの幅が希望切り出し幅よりも小さい場合、切り出し幅は
        // テクスチャ幅とし、代わりに切り出し高さを切り詰める
        // 起点のズレは余白の大きさの半分ということになる
        var toAspect = (float)toWidth / toHeight;
        var width = texture2D.width;
        var height = texture2D.height;
        var expectedWidth = Mathf.RoundToInt(height * toAspect);
        var fromWidth = Mathf.Min(width, expectedWidth);
        var fromHeight = expectedWidth > width ? Mathf.RoundToInt(width / toAspect) : height;
        var offsetX = (width - fromWidth) / 2;
        var offsetY = (height - fromHeight) / 2;

        // 確認のため、リサイズ前とリサイズ後のサイズをコンソールに表示してみる
        Debug.LogFormat("({0}, {1}) -> [({2}, {3}), ({4}, {5})] -> ({6}, {7})",
            width, height,
            offsetX, offsetY, fromWidth, fromHeight,
            toWidth, toHeight);

        // 切り出された領域をリサイズして最終的なテクスチャとする
        // ちなみに、ご質問者さんが作成された方のGetResizedも残してありますので、
        // 引数を元テクスチャと目標サイズだけに省略するとそちらが使われます
        // そちらに切り替えてみると、トリミング処理の有無による違いやぼけ具合の
        // 比較ができるかと思います
        var resizedTexture = GetResized(texture2D, offsetX, offsetY, fromWidth, fromHeight, toWidth, toHeight);
        //var resizedTexture = GetResized(texture2D, 200, 200);

        // 最終的に表示される位置・サイズによっては、小数点以下のズレのため
        // ぼやけやImage外周部に反対側の辺の色が乗る現象が起こる可能性がある
        // そこで、それらを回避するようにテクスチャの設定を変更する
        resizedTexture.filterMode = FilterMode.Point;
        resizedTexture.wrapMode = TextureWrapMode.Clamp;

        // リサイズ後のテクスチャをもとにスプライトを作成し、Imageにセット
        var sprite = Sprite.Create(
            resizedTexture,
            new Rect(0, 0, resizedTexture.width, resizedTexture.height),
            new Vector2(0.5f, 0.5f));
        image.sprite = sprite;
    }

    // 元のテクスチャから(fromX, fromY)を起点とする幅fromWidth、高さfromHeightの領域を
    // 切り出し、それを幅toWidth、高さtoHeightにリサイズしたテクスチャを生成する
    private static Texture2D GetResized(
        Texture2D texture,
        int fromX, int fromY, int fromWidth, int fromHeight,
        int toWidth, int toHeight,
        int loopCount = 16,
        bool generateMipmaps = false)
    {
        if (overSamplingMaterial == null)
        {
            overSamplingMaterial = new Material(Shader.Find("Hidden/OverSampling"));
        }

        // まずは以前同様に元テクスチャをRenderTextureに写し取り、FilterModeをPointにしておく
        var preRT = RenderTexture.active;
        var sourceWidth = texture.width;
        var sourceHeight = texture.height;
        var sourceRT = RenderTexture.GetTemporary(sourceWidth, sourceHeight);
        Graphics.Blit(texture, sourceRT);
        sourceRT.filterMode = FilterMode.Point;

        // マテリアルの設定を行う
        // 元のテクスチャから起点(fromX, fromY)、サイズ(fromWidth, fromHeight)の部分を
        // 切り出すということは、UV座標のタイリング・オフセットは下記のようになる
        overSamplingMaterial.SetVector(
            "_TilingAndOffset",
            new Vector4(
                (float)fromWidth / sourceWidth,
                (float)fromHeight / sourceHeight,
                (float)fromX / sourceWidth,
                (float)fromY / sourceHeight));
        overSamplingMaterial.SetInt("_LoopCount", Mathf.Max(loopCount, 1));

        // あとは以前と同様にリサイズし、結果を返す
        var rt = RenderTexture.GetTemporary(toWidth, toHeight);
        Graphics.Blit(sourceRT, rt, overSamplingMaterial);
        RenderTexture.active = rt;
        var ret = new Texture2D(toWidth, toHeight, TextureFormat.ARGB32, generateMipmaps);
        ret.ReadPixels(new Rect(0, 0, toWidth, toHeight), 0, 0);
        ret.Apply();
        RenderTexture.active = preRT;
        RenderTexture.ReleaseTemporary(rt);
        RenderTexture.ReleaseTemporary(sourceRT);
        return ret;
    }

    // ご質問者さんの方法によるGetResized
    private static Texture2D GetResized(Texture2D texture, int width, int height)
    {
        var rt = RenderTexture.GetTemporary(width, height);
        Graphics.Blit(texture, rt);
        var preRT = RenderTexture.active;
        RenderTexture.active = rt;
        var ret = new Texture2D(width, height);
        ret.ReadPixels(new Rect(0, 0, width, height), 0, 0);
        ret.Apply();
        RenderTexture.active = preRT;
        RenderTexture.ReleaseTemporary(rt);
        return ret;
    }
}

当初の方法だと、縦長の画像が正方形に縮小されることで縦に潰れてしまっていたのが...

図8

正方形に切り出す処理が加わることで、被写体が潰れないようになりました。

図9

投稿

編集

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2020/05/12 18:37 編集

    有難うございます。被写体が潰れないようになりました。
    コーディングについては週末の時間のある時に見ようと思います。
    この後はcanvasに全体像を表示し枠内を切り抜きしようと思います。

    Canvasのスクショを登録しました。

    Canvas Scalerの設定次第では、キャンバス全体に拡大縮小がかかって最終的な表示サイズが200×200にならないかもしれません(UI Scale ModeがScale With Screen Sizeの場合だとか)
    テクスチャがもっとも美しく表示されるのは最終的にテクスチャが画面上に表示される時のサイズとテクスチャのサイズが一致する時

    よろしければ,この辺りの事を教えて頂けませんか?

    キャンセル

  • 2020/05/13 17:31

    Canvas Scalerの件に関しては、「Unity Canvas Scaler」あたりのキーワードで検索(https://www.google.com/search?q=Unity+Canvas+Scaler )していただけるといろいろな解説が出てくるかと思います。「実行時のモバイル端末の解像度がさまざまでも、UIのレイアウトを同じように見せるにはどうするべきか」というテーマで説明しているサイトが多いように思われますね。

    UI Scale ModeをScale With Screen Sizeとして、ゲームビュー上部の解像度プルダウンメニューを「Free Aspect」とした状態でゲームビューの外周をドラッグしてサイズを変更してみると、Canvasのインスペクター上のRect TransformのScaleの値がゲームビューのサイズに応じて変化する様子を観察できるかと思います。
    Canvas ScalerのReference Resolutionで設定した解像度よりもゲームビューのサイズが小さければCanvasは縮小され、大きければ拡大されるはずです。Canvasの子であるImageも拡大縮小の影響を受け、たとえImage自身のサイズを200×200に設定していたとしても、ゲームビューのスクリーンショットを撮影して実際のサイズを画像処理ソフトで調べてみると200ピクセル×200ピクセルになっているとは限らないんじゃないでしょうか。

    前回追記しましたスクリプトを改変して、200×200決め打ちではなく実際の表示サイズを考慮してリサイズするオプションを追加してみました(インスペクター上で「Use Actual Size」をオンにしてみてください)。シェーダーコードはいじっていません。
    また、その結果投稿字数制限を超えてしまったため、すみませんが最初の回答で例示しました古い方の「Hidden/OverSampling」のコードを回答文から削除しました。
    私の回答欄本文の末尾に編集を行った日時が表示されているはずですが、そのリンクを開くとどの部分が変更されたか見ることができるかと思います。

    キャンセル

  • 2020/05/13 18:06

    丁寧に有難うございました。
    勉強になる事ばかりです。

    キャンセル

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

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

関連した質問

同じタグがついた質問を見る