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

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

ただいまの
回答率

87.49%

unity 3D ボリュームレンダリング rawデータからテクスチャ3Dへの変換

解決済

回答 2

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 1,167

score 11

Unity 3Dで彫刻ゲームを引き続き作っている者です。

凹みさんのページを参照に、削られた断面にCTデータをdicomとして出力したものをraw変換し、これをテクスチャ3Dしたシェーダを貼りたいと思っています。

http://tips.hecomi.com/entry/2018/01/05/192332

凹みさんにコメントでご質問したのですが、お忙しいのかお返事がないので、こちらでご質問させていただきました。

上記ページのまんなかあたり、pvmをrawデータにするところあたりから躓いています。

pvm2rawがc++のためかうまく起動できず、このページの道筋を正確に終えていないのですが、推測するにpvmデータはCT画像がrawフォーマットで200-500枚程度出力されたフォルダとなるかと思います。

この推測のもとに、次のステップ、
「ちょっとコードが長くてアレですが...、以下のようなコードを書くと、RAW データをそのまま 3D テクスチャとして使えるようになります。」

「RAW データをドラッグ&ドロップすると次の図のように public 変数がインスペクタに表示されます。enum がプルダウンにならないのがアレですが...、鯉のデータは 256 * 256 * 512、16 bit なので、そうなるよう入力し、Apply ボタンを押します。」

とありますが、rawデータをドラッグ&ドロップとは、このフォルダごとプロジェクトウィンドウにドラッグすればよいということでしょうか。(ちなみにdicomファイルをdngに変換はできたのですが、rawに変換するのも苦慮しています。。)

いろいろと疑問ばかりで申し訳ございませんが、少しでも先達の方々のお知恵を拝借できれば幸甚です。
よろしくお願いいたします。

12/10追記
無事Texture3Dは生成できたのですが、オブジェクトへの適用でつまずいております。。

引き続き上記、凹みさんのページを参照し、Texture3Dを参照するシェーダのスクリプトを書いたのですが、オブジェクトにドラッグ&ドロップできません。。

Shader "VolumeRendering/3D Texture"
{

    Properties
    {
        _Volume("Volume", 3D) = "" {}
    }

        CGINCLUDE

#include "UnityCG.cginc"

        struct appdata
    {
        float4 vertex : POSITION;
    };

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

    sampler3D _Volume;

    v2f vert(appdata v)
    {
        v2f o;
        o.vertex = UnityObjectToClipPos(v.vertex);
        float4 wpos = mul(unity_ObjectToWorld, v.vertex);
        o.uv = wpos.xyz * 0.5 + 0.5;
        return o;
    }

    fixed4 frag(v2f i) : SV_Target
    {
        return tex3D(_Volume, i.uv);
    }

        ENDCG

        SubShader
    {

        Tags
        {
            "RenderType" = "Opaque"
        }

            Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            ENDCG
        }

    }

}
コード

毎度毎度初歩的な質問ばかりで申し訳ございません。。

適用しているオブジェクトが、sphereを楕円球にしたものを複数合成させたものになりますので、形状が複雑だから適用できないのかと思い単純なCubeにもD&Dを試みたのですが、やはり無理でした。。

texture3Dの適用には何か独特な手順が必要なのでしょうか。
ご教示頂けますと幸甚です。

***12/13追記
テクスチャ3Dを反映できました!ここまで本当にありがとうございます。

あと一歩、いや1.5歩のように思います!

例のsphereの合成体にこのテクスチャ3Dを反映したところ、以下の結果が得られました。
現状

理想的には、下の図の緑の部分のように、例えばinspecterで表示される地球儀マップ状のテクスチャを、そのままsphereの合成体に貼り付けたいと思っています。
緑の部分を切り抜いて
↓理想とする結果
![理想の結果](a81890058cee08fbc56ca66966815923.jpeg)

現状ではsphere上にテクスチャ3Dが繰り返し表示され、また目的の場所が切り取られないというのが問題に思いますが、スクリプトを改変することで理想は達成できそうでしょうか。
自動的に必要な部分のuv座標を計算させ、リアルタイムに反映させるのは難しいように思いますので、少なくともtiling等をいじって画像が何列もダブって表示されるのだけでも解消しようと試みたのですが、tilingを0.1や10等に変えてもテクスチャ自体には変化が起こりませんでした。

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

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

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

    クリップを取り消します

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

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

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

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

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

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

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

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

    質問の評価を下げる

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

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

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

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

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

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

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

    詳細な説明はこちら

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

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

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

回答 2

checkベストアンサー

0

タイリング、オフセットを調整できるように改造

設定項目を追加してみました。それぞれの項目で使われるのはX、Y、Zの3成分ですが、インスペクター上では不要なWまで表示されてしまい若干見苦しくはあります。インスペクター用のスクリプトを用意すればもっとマシな見た目になるだろうと思いますが、手抜きしました...

Shader "VolumeRendering/3D Texture"
{
    Properties
    {
        // デフォルト状態ではVolume欄にTilingとOffsetが表示されてしまうが、これは3次元では役に立たない
        // これらが表示されていると紛らわしいため、NoScaleOffset属性を付けて隠してしまう
        [NoScaleOffset] _Volume("Volume", 3D) = "" {}

        // 代わりに、独自にTilingと...
        _Tiling("Tiling", Vector) = (1, 1, 1, 0)

        // Offsetを定義する
        _Offset("Offset", Vector) = (0, 0, 0, 0)

        // さらに利便性の観点から、シェーダーにテクスチャサイズを伝えられるようにする
        // 2Dテクスチャであれば(テクスチャ名)_TexelSizeの形のユニフォーム変数を宣言すると
        // 自動的にテクスチャサイズを取得できるのだが、3DテクスチャではZ軸もあるため
        // 利用できないので、面倒だが入力欄に手動でサイズを入力しなければならない
        // 補助的なスクリプトを用意することでいくらかは自動化することも可能なはずだが、
        // 説明がかえってややこしくなりそうで省略した
        _Size("Texture Size", Vector) = (256, 256, 256, 0)
    }

    CGINCLUDE

    #include "UnityCG.cginc"

    struct appdata
    {
        float4 vertex : POSITION;
    };

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

    sampler3D _Volume;

    // シェーダー内でプロパティと対応するユニフォーム変数を宣言すると、レンダリング時に自動的に
    // プロパティの値がセットされるので、シェーダーコード上でその値を利用できる
    float3 _Tiling;
    float3 _Offset;
    float3 _Size;

    v2f vert(appdata v)
    {
        v2f o;
        o.vertex = UnityObjectToClipPos(v.vertex);
        float4 wpos = mul(unity_ObjectToWorld, v.vertex);

        // テクスチャの縦・横・奥行きが異なっているケースも多いと思われる
        // たとえば実験で使ったHeadは512x512x245だが、これをサンプリングするときの座標は0~1に
        // 正規化されているため、ちゃんと対策しないと長さの短いZ方向だけ伸びて見えてしまうかもしれない
        // そこで3軸のアスペクト比を求めておく
        float3 texelSize = 1.0 / _Size;
        float3 aspect = max(max(_Size.x, _Size.y), _Size.z) * texelSize;

        // テクスチャ座標を_Tiling、aspectに応じて拡大・縮小し、_Offsetに応じてずらすようにする
        // _OffsetはtexelSizeをかけることで値をテクスチャのピクセル単位で解釈することにした
        o.uv = wpos.xyz * _Tiling * aspect + _Offset * texelSize;

        return o;
    }

    fixed4 frag(v2f i) : SV_Target
    {
        return tex3D(_Volume, i.uv);
    }

    ENDCG

    SubShader
    {
        Tags
        {
            "RenderType" = "Opaque"
        }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            ENDCG
        }
    }
}

図

投稿

編集

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2020/12/16 16:40

    思っていたことにかなり近いことが実装できました!
    本当にすごいです。
    長きにわたって本当にありがとうございました!

    キャンセル

0

「rawデータをドラッグ&ドロップ」とあるのは、pvm2rawで処理してできあがった単一のファイルをプロジェクトビューに放り込むことを指していると思われます。するとPvmRawImporterによって自動的にTexture3Dになるということでしょう。普通の画像ファイルをプロジェクトビューに入れれば自動的にTexture2Dとして扱えるのと同じ使い勝手のようですね。

ですがRAWデータを作るところで難儀していらっしゃるのなら、そこから何とかしなければならないでしょう。
何かお役に立ちそうなものがないか検索したところ、fo-dicomなんてものがありました。説明文にあるように、

var image = new DicomImage("imagename.dcm");
var texture = image.RenderImage().AsTexture2D();

だけでTexture2Dができあがるという親切お手軽設計でした。そこで凹みTipsさんとはちょっと違ったアプローチですが、まずfo-dicomをインポートした上で、プロジェクト内にEditorフォルダを作って下記のようなスクリプトを置き...

using System;
using System.IO;
using System.Linq;
using Dicom.Imaging;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;

public static class DicomFilesToTexture3D
{
    [MenuItem("Utility/DICOM/Create Texture3D from DICOM files")]
    private static void CreateTexture3DFromDicomFiles()
    {
        // まずフォルダ選択ダイアログを出し、DICOMファイル群が収められたフォルダを選んでもらう
        var sourceFolderPath = EditorUtility.OpenFolderPanel(
            "Choose a folder that contains DICOM (*.dcm, *.dic) images",
            string.Empty,
            string.Empty);
        if (string.IsNullOrEmpty(sourceFolderPath))
        {
            return;
        }

        if (!Directory.Exists(sourceFolderPath))
        {
            EditorUtility.DisplayDialog("Invalid source", "Choose a valid source folder.", "OK");
            return;
        }

        // フォルダ内でファイル名が「.dcm」または「.dic」で終わるものを抜き出してファイルパスを得る
        var sourceFilePaths = Directory.EnumerateFiles(sourceFolderPath).Where(
                path => !string.IsNullOrEmpty(path) &&
                        (path.EndsWith(".dcm", StringComparison.OrdinalIgnoreCase) ||
                         path.EndsWith(".dic", StringComparison.OrdinalIgnoreCase)))
            .OrderBy(path => path).ToArray();
        var sourceFileCount = sourceFilePaths.Length;
        if (sourceFileCount <= 0)
        {
            EditorUtility.DisplayDialog("Invalid source", "No DICOM images were found.", "OK");
            return;
        }

        // ひとまず最初のファイルからDicomImageを作り、できあがるであろうTexture3Dの
        // サイズ見積もりを提示し、続行するかどうか確認する
        var firstFilePath = sourceFilePaths.First();
        DicomImage firstImage;
        try
        {
            firstImage = new DicomImage(firstFilePath);
        }
        catch (Exception e)
        {
            EditorUtility.DisplayDialog("Failed", $"{e.GetType()}\n{e.Message}", "OK");
            throw;
        }

        var dimension = new Vector3Int(
            firstImage.Width,
            firstImage.Height,
            firstImage.NumberOfFrames > 1 ? firstImage.NumberOfFrames : sourceFileCount);
        var resultSize = dimension.x * dimension.y * dimension.z * 4;
        var resultSizeInKib = resultSize / 1024.0;
        var resultSizeInMib = resultSizeInKib / 1024.0;
        var resultSizeInGib = resultSizeInMib / 1024.0;
        if (!EditorUtility.DisplayDialog(
            "Confirm Texture3D data size",
            $"The resulting Texture3D data size will be {resultSize} bytes ({(resultSizeInGib >= 1.0 ? $"{resultSizeInGib:0.##} GB" : resultSizeInMib >= 1.0 ? $"{resultSizeInMib:0.##} MB" : $"{resultSizeInKib:0.##} KB")}). Are you sure want to continue?",
            "Continue",
            "Cancel"))
        {
            return;
        }

        Texture3D resultTexture;
        if (firstImage.NumberOfFrames > 1)
        {
            // DICOMは1つのファイルに複数枚のフレームを持つこともできるらしい?
            // それらフレームが個々の断面を表しているケースもあるのかもしれない...と思い
            // 先ほど作ったDicomImageに複数のフレームが格納されている場合は、それぞれの
            // フレームを断面と見なすことにし、もしフォルダ内に他のDICOMファイルが入っていたとしても
            // それらは無視することにした
            if ((sourceFileCount > 1) && !EditorUtility.DisplayDialog(
                "Texture3D will be created from single file",
                $"{sourceFileCount} DICOM images exist in the folder {Path.GetFileName(sourceFolderPath)}, but file {Path.GetFileName(firstFilePath)} has multiple frames and the Texture3D slices will be taken from this file. All other files will be ignored.",
                "Continue",
                "Cancel"))
            {
                return;
            }

            // まずTexture3Dを生成して...
            resultTexture = new Texture3D(dimension.x, dimension.y, dimension.z, TextureFormat.RGBA32, false);
            for (var i = 0; i < firstImage.NumberOfFrames; i++)
            {
                // 各フレームからUnityImageを生成、さらにそれからTexture2Dを生成して
                // そのデータをTexture3Dにコピー、最後に用済みのTexture2Dを削除する
                try
                {
                    using (var tempImage = firstImage.RenderImage(i))
                    {
                        var tempTexture = tempImage.AsTexture2D();
                        Graphics.CopyTexture(tempTexture, 0, 0, resultTexture, i, 0);
                        Object.DestroyImmediate(tempTexture);
                    }
                }
                catch (Exception e)
                {
                    EditorUtility.DisplayDialog("Failed", $"{e.GetType()}\n{e.Message}", "OK");
                    Object.DestroyImmediate(resultTexture);
                    throw;
                }
            }
        }
        else
        {
            // 最初に調べた1枚目のDicomImageがシングルフレームだったなら、
            // 断面がそれぞれ1ファイルずつに分けられているものと想定する
            // まずTexture3Dを生成して...
            resultTexture = new Texture3D(dimension.x, dimension.y, dimension.z, TextureFormat.RGBA32, false);
            for (var i = 0; i < sourceFilePaths.Length; i++)
            {
                // フォルダ内のファイルを1つずつ列挙し、DicomImageを作る
                var sourceFilePath = sourceFilePaths[i];
                var sourceFileName = Path.GetFileName(sourceFilePath);
                DicomImage image;
                try
                {
                    image = new DicomImage(sourceFilePath);
                }
                catch (Exception e)
                {
                    EditorUtility.DisplayDialog("Failed", $"{e.GetType()}\n{e.Message}", "OK");
                    Object.DestroyImmediate(resultTexture);
                    throw;
                }

                // こちらのケースでは1つのファイルが1つの断面を表していることを前提としており、
                // もしそこに複数フレームを持つファイルがまぎれ込んでいたら異常事態と見なして中断する
                if (image.NumberOfFrames > 1)
                {
                    EditorUtility.DisplayDialog(
                        "Invalid source",
                        $"A multi-frame image {sourceFileName} was found.",
                        "OK");
                    Object.DestroyImmediate(resultTexture);
                    return;
                }

                // また、すべての断面は幅・高さが同じでないとまずいため、
                // もしそうでないファイルがまぎれ込んでいた場合も中断する
                if ((image.Width != dimension.x) || (image.Height != dimension.y))
                {
                    EditorUtility.DisplayDialog(
                        "Invalid source",
                        $"All images should be the same size. The expected size is ({dimension.x}, {dimension.y}), but the size of image {sourceFileName} is ({image.Width}, {image.Height})",
                        "OK");
                    Object.DestroyImmediate(resultTexture);
                    return;
                }

                // DicomImageからUnityImageを生成、さらにそれからTexture2Dを生成して
                // そのデータをTexture3Dにコピー、最後に用済みのTexture2Dを削除する
                try
                {
                    using (var tempImage = image.RenderImage())
                    {
                        var tempTexture = tempImage.AsTexture2D();
                        Graphics.CopyTexture(tempTexture, 0, 0, resultTexture, i, 0);
                        Object.DestroyImmediate(tempTexture);
                    }
                }
                catch (Exception e)
                {
                    EditorUtility.DisplayDialog("Failed", $"{e.GetType()}\n{e.Message}", "OK");
                    Object.DestroyImmediate(resultTexture);
                    throw;
                }
            }
        }

        // セーブダイアログを出してアセットの名前、保存場所を決めてもらい...
        var destinationPath = EditorUtility.SaveFilePanelInProject(
            "Save Texture3D",
            "DICOM Texture",
            "asset",
            "Enter an asset name for the Texture3D.");
        if (string.IsNullOrEmpty(destinationPath))
        {
            Object.DestroyImmediate(resultTexture);
            return;
        }

        // できあがったTexture3Dをアセットファイル化して保存する
        AssetDatabase.CreateAsset(resultTexture, destinationPath);
        AssetDatabase.SaveAssets();
    }

    // fo-dicomの作るTexture2DがRGBA32フォーマットだったので生成されるTexture3Dのフォーマットも
    // RGBA32にしたものの、今回のようなグレースケールに4チャンネルも使うのはもったいなく思い
    // 生成されたRGBA32のTexture3DからRだけを抜き出してR8フォーマット版を作るメソッドも用意した
    [MenuItem("Utility/DICOM/Convert Texture3D format from RGBA32 to/R8")]
    private static void ConvertToR8()
    {
        ConvertTo(DestinationFormat.R8);
    }

    // こちらは抜き出したRをAとして扱わせる
    [MenuItem("Utility/DICOM/Convert Texture3D format from RGBA32 to/Alpha8")]
    private static void ConvertToAlpha8()
    {
        ConvertTo(DestinationFormat.Alpha8);
    }

    private static void ConvertTo(DestinationFormat format)
    {
        var targetTexture = Selection.activeObject as Texture3D;
        if ((targetTexture == null) || (targetTexture.format != TextureFormat.RGBA32))
        {
            return;
        }

        var newData = targetTexture.GetPixels32(0).Select(c => c.r).ToArray();
        var newTexture = new Texture3D(
            targetTexture.width,
            targetTexture.height,
            targetTexture.depth,
            (TextureFormat)format,
            false);
        newTexture.SetPixelData(newData, 0);
        var destinationPath = EditorUtility.SaveFilePanelInProject(
            "Save Texture3D",
            $"{targetTexture.name} {format}",
            "asset",
            "Enter an asset name for the Texture3D.");
        if (string.IsNullOrEmpty(destinationPath))
        {
            Object.DestroyImmediate(newTexture);
            return;
        }

        AssetDatabase.CreateAsset(newTexture, destinationPath);
        AssetDatabase.SaveAssets();
    }

    private enum DestinationFormat
    {
        R8 = TextureFormat.R8,
        Alpha8 = TextureFormat.Alpha8
    }
}

実験材料としてMR Research Facility :: Visible Human Project Datasetsから入手した「Visible Male CT Datasets」の「Head」を読ませてみたところ、下図のようなTexture3Dを作成できました。

図1

RGBA32だとさすがにサイズがかさむので、1チャンネル化してしまってもいいでしょう。
R8だと...

図2

Alpha8だと...

図3

となり、いずれもサイズが1/4になっています。

せっかくなのでオブジェクトに適用してみました。なおテクスチャ自体はグレーですが、下図では階調に応じて着色し見やすくしてあります。Z方向にも次元を持つテクスチャですので、任意の向きで断面を描かせることができました。

図4

Unity 2019.4.13f1で見たHeadの様子

図5

投稿

編集

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2020/12/11 04:46

    ご提示のシェーダーコードを試してみましたが、ちゃんと動作するようでした。
    「オブジェクトにドラッグ&ドロップできない」とおっしゃるのは、もしかして作成したシェーダーファイル(虹色の紙に「S」の字が書かれたアイコン)そのものをオブジェクトにドラッグ&ドロップしようとしているということでしょうか?
    もしそうでしたら、そうではなくてまずマテリアルを作成してやる必要があるはずです。

    新しくマテリアルを作成し、マテリアルのインスペクターを表示させると、最上部のマテリアル名の直下に「Shader」とラベルがついたポップアップメニューがあるはずですが、ここで目的のシェーダーを探して選択してください(ご提示のシェーダーの場合「VolumeRendering」→「3D Texture」にあるはずです)。
    するとインスペクターの表示内容が選択したシェーダーに合わせた設定項目に変化するはずですので、項目を設定し(とはいえ「Volume」にTexture3Dをセットするぐらいしかやることはないでしょうが...)、そしてマテリアルをオブジェクトにドラッグ&ドロップすればそのオブジェクトにマテリアルが適用されるはずです。
    Texture3Dを使っているからといって特別な手順を踏まなければならない...といったことはなく、一般的なTexture2Dを使ったシェーダーや、あるいはテクスチャを必要としないシェーダーであっても、手順的には変わらないと思います。

    ちなみに、プロジェクトビュー内のシェーダーファイルを選択した状態で新規マテリアルを作成すると、最初からShaderメニューにそのシェーダーが選択された状態でマテリアルが作成されますので、ちょっと操作の手間が軽減されますね。

    キャンセル

  • 2020/12/13 14:35

    Bongoさん、いつも本当にありがとうございます。初歩的な問題であったようで大変失礼いたしました。ご指導いただきました通りに作業しますと、テクスチャ3Dを無事反映できるようになりました!
    画像の取得位置、サイズについて調整が必要なようで、質問に追記させていただきました。

    キャンセル

  • 2020/12/13 22:04

    TilingやOffsetにご注目なさったのは鋭いと思いますが、あの設定項目はTexture2D向けの設定であり、しかもシェーダー内で設定値に応じてテクスチャサンプリング位置を操作するようなコードになっていないと効果を発揮しないはずです。
    現状のコードでは、サンプリング位置を...

    float4 wpos = mul(unity_ObjectToWorld, v.vertex);
    o.uv = wpos.xyz * 0.5 + 0.5;

    という風にワールド座標だけしか利用せずに決定していますので、テクスチャの貼られ方を調整する余地がない状態と言えるでしょう。
    ここは貼られ方を調整できるようマテリアルにプロパティを追加してやる必要があるでしょうね。改造案を検討してみましたので追記したかったのですが、字数の限界に到達してしまったため、すみませんが別回答に移ります。

    キャンセル

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

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

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