20200518のC#に関する記事は8件です。

【Unity】非同期処理を理解する〜コルーチン編〜

コルーチンとは

  • 非同期処理できる。
  • 関数を任意のタイミングで中断・再開できる機能。
  • 複数の処理を疑似並列できる。(完全な並列ではない)
  • Unity のメインスレッドで処理する。
  • 結果は返せない。
  • GameObject と処理が紐づく。
  • IEnumerator 型を戻り値とした関数を定義する。
  • コルーチンを実行したいときは StartCoroutine(string コルーチン名) または StartCoroutine(IEnumerator型の変数)
  • コルーチンを停止(中断)したいときは StopCoroutine(string コルーチン名) または StopCoroutine(IEnumerator型の変数)

コルーチンの主要な機能一覧

機能 詳細
yield return null 1フレーム分処理を中断、次のフレームで続きの行を処理
yield break コルーチンを途中で終了、再開はできない
yield return new WaitForSeconds(float seconds) 指定した seconds 秒、コルーチンを中断する
yield return new WaitUntil(Func predicate) predicate の関数の返り値が true になったら処理を再開する
yield return new WaitWhile(Func predicate) predicate の関数の返り値が false になったら処理を再開する
yield return StartCoroutine() 指定したコルーチンを実行、完了するまで後続処理は行わない
StopAllCoroutines() コルーチンを全て止める

yield return null

yield return null は、1フレーム分処理を中断して、次のフレームで続きの行を処理します。

using System.Collections;
using UnityEngine;

public class Test : MonoBehaviour
{
    void Start()
    {
        IEnumerator sample1 = Sample1();
        IEnumerator sample2 = Sample2();
        StartCoroutine(sample1);
        StartCoroutine(sample2);
    }

    IEnumerator Sample1()
    {
        Debug.Log("Sample1 Start.");
        yield return null;            // Sample1()の処理は1フレーム待機
        Debug.Log("Sample1 End.");
    }

    IEnumerator Sample2()
    {
        Debug.Log("Sample2 Start.");
        yield return null;            // Sample2()の処理は1フレーム待機
        Debug.Log("Sample2 End.");
    }
}

だから、結果は下の通りで、
1. Sample1 Start.
2. Sample2 Start.
3. Sample1 End.
4. Sample2 End.
の順で実行される。
image.png

yield break

yield break は、コルーチンを途中で終了します。
再開はできないです。

using System.Collections;
using UnityEngine;

public class Test : MonoBehaviour
{
    void Start()
    {
        IEnumerator sample1 = Sample1();
        IEnumerator sample2 = Sample2();
        StartCoroutine(sample1);
        StartCoroutine(sample2);
    }

    IEnumerator Sample1()
    {
        Debug.Log("Sample1 Start.");
        yield break;                  // Sample1() の処理はここで止める
        Debug.Log("Sample1 End.");
    }

    IEnumerator Sample2()
    {
        Debug.Log("Sample2 Start.");
        yield break;                  // Sample2() の処理をここで止める
        Debug.Log("Sample2 End.");
    }
}

コルーチンを途中で終了したので、Sample1 End.Sample2 End. は出力されません。
image.png

yield return new WaitForSeconds(float seconds)

yield return new WaitForSeconds(seconds) は、指定した秒数の間、コルーチンの実行を待ちます。

using System.Collections;
using UnityEngine;

public class Test : MonoBehaviour
{
    void Start()
    {
        IEnumerator sample1 = Sample1();
        IEnumerator sample2 = Sample2();
        StartCoroutine(sample1);
        StartCoroutine(sample2);
    }

    IEnumerator Sample1()
    {
        Debug.Log("Sample1 Start.");
        yield return null;            // Sample1()の処理は1フレーム待機
        Debug.Log("Sample1 End.");
    }

    IEnumerator Sample2()
    {
        Debug.Log("Sample2 Start.");
        yield return new WaitForSeconds (1.0f); // Sample2()の処理は1秒待機
        Debug.Log("Sample2 End.");
    }
}

Sample2 End. が、他より1秒経過してから出力されるようになります。
image.png

yield return new WaitUntil(Func predicate)

yield return new WaitUntil(Func<bool> predicate) は、predicate で指定した関数が true を返したときに再開します。

using System.Collections;
using UnityEngine;

public class Test : MonoBehaviour
{
    bool flg = false;
    void Start()
    {
        IEnumerator sample1 = Sample1();
        IEnumerator sample2 = Sample2();
        StartCoroutine(sample1);
        StartCoroutine(sample2);
    }

    IEnumerator Sample1()
    {
        Debug.Log("Sample1 Start.");
        yield return new WaitUntil(() => flg == true); // flg が true になるまで処理が止まる
        Debug.Log("Sample1 End.");
    }

    IEnumerator Sample2()
    {
        Debug.Log("Sample2 Start.");
        yield return null;
        Debug.Log("Sample2 End.");
        flg = true; // Sample2()の最後に flg を true にする
    }
}

flg が true になるまで、 Sample1 End. が出力されないため、下記のような結果になります。
image.png

yield return new WaitWhile(Func predicate)

yield return new WaitWhile(Func<bool> predicate) は、predicate で指定した関数が false を返したときに再開します。
WaitUntil(Func<bool> predicate) の逆バージョン。
使い方は上と一緒なので省略。

yield return StartCoroutine()

yield return StartCoroutine() は、指定したコルーチンを実行し、完了するまで処理を中断します。

using System.Collections;
using UnityEngine;

public class Test : MonoBehaviour
{
    bool flg = false;
    void Start()
    {
        IEnumerator sample1 = Sample1();
        StartCoroutine(sample1);
    }

    IEnumerator Sample1()
    {
        Debug.Log("Sample1 Start.");
        IEnumerator sample2 = Sample2();
        yield return StartCoroutine(sample2); // Sample2() を実行が終わるまで待機
        Debug.Log("Sample1 End.");
    }

    IEnumerator Sample2()
    {
        Debug.Log("Sample2 Start.");
        yield return null;           // Sample2()の処理は1秒待機
        Debug.Log("Sample2 End.");
    }
}

結果は下のような感じです。
image.png

StopAllCoroutines()

名前の通り、コルーチンの全てを止めます。
Behavior 上で実行されている全てを止めます。

using System.Collections;
using UnityEngine;

public class Test : MonoBehaviour
{
    void Start()
    {
        IEnumerator sample1 = Sample1();
        IEnumerator sample2 = Sample2();
        StartCoroutine(sample1);
        StartCoroutine(sample2);
        StopAllCoroutines(); // 全てのコルーチンを止める
    }

    IEnumerator Sample1()
    {
        Debug.Log("Sample1 Start.");
        yield return null;
        Debug.Log("Sample1 End.");
    }

    IEnumerator Sample2()
    {
        Debug.Log("Sample2 Start.");
        yield return null;
        Debug.Log("Sample2 End.");
    }
}

コルーチンを途中で終了したので、Sample1 End.Sample2 End. は出力されません。
image.png

終わりに

Unity 初心者なので、間違いがあったら教えてくれるとありがたいです。
今まで雰囲気で非同期処理を書いていたので、しっかり勉強するために記事を書くことにしました。
いまのところ、async/await編、UniRx編を書く予定です。
挫折したらごめんなさい。

参考文献

コルーチン - Unity マニュアル
【C#/Unity】コルーチン(Coroutine)とは何なのか
Unityのコルーチン機能を使う

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CSOM(C#)でSharePointグループを作成する

概要

CSOM(C#)でSharePointグループを作成します。
SharePointサイト作成時に、合わせてグループを作成する際や、まとまった数のグループを作成する際などに便利です。

開発環境

.NET Framework 4.7.2
NugetでMicrosoft.SharePointOnline.CSOMを追加
image.png

手順

  1. SharePointOnlineで、グループを作成したいサイトにログインする
     * usingでサイトのURLを指定 - http://xxx.xxxx.com/sites/sitecollection/sitename の形式

  2. 作成したいグループの存在確認を行う
     * サイトのSPOグループから、ここでは作成したいグループのタイトルと同じタイトルのグループを検索して取得
     * 下記ソースコードでは、既に存在している場合はそこで処理を終了

  3. GroupCreationInformationクラスを使用し、グループを追加する
     * GroupCreationInformaionクラスにグループのタイトルと説明を設定し、サイトグループにAddする

  4. 属性の編集が必要な場合は、編集して更新する
     * 作成したグループを取得し、属性の更新を行う

コード

CreateGroup.cs
// ログインユーザー
var loginUser = ConfigurationManager.AppSettings["SPO_ID"];
// サイトコレクション(サイト)のURL
var siteCol = ConfigurationManager.AppSettings["SITE_COLLECTION_URL"];
// サイトのURL名
var newSiteUrl = ConfigurationManager.AppSettings["NEW_SITE_URL"];

using (var clientContext = new ClientContext(Utility.URLCombine(siteCol, newSiteUrl)))
using (var secureString = new SecureString())
{
    foreach (var c in ConfigurationManager.AppSettings["SPO_PASS"]) secureString.AppendChar(c);
    secureString.MakeReadOnly();
    clientContext.Credentials = new SharePointOnlineCredentials(loginUser, secureString);

    var message = string.Empty;

    var newGroupName = $"{newSiteUrl.TrimEnd('/')}Admin";       // サイトURL名+Admin 管理者用グループ
    var newGroupDisc = "管理者用グループ";

    // 指定したグループが既存かどうかを確認する
    var groups = clientContext.Web.SiteGroups;
    clientContext.Load(groups, gp => gp.Include(g => g.Title).Where(g => g.Title == newGroupName));
    clientContext.ExecuteQuery();
    // 存在する場合は処理終了
    if (groups.FirstOrDefault() != null) return;

    // グループ作成
    var groupCreateInfo = new GroupCreationInformation
    {
        Title = newGroupName,
        Description = newGroupDisc,
    };

    var newGroupAdd = clientContext.Web.SiteGroups.Add(groupCreateInfo);
    clientContext.Load(newGroupAdd);
    clientContext.ExecuteQuery();

    // グループ設定の編集
    // 編集のために再取得
    groups = clientContext.Web.SiteGroups;
    clientContext.Load(groups, gp => gp.Include(g => g.Title).Where(g => g.Title == newGroupName));
    clientContext.ExecuteQuery();

    var group = groups.FirstOrDefault();
    group.OnlyAllowMembersViewMembership = false;           // グループのメンバーシップを表示できるユーザー:すべてのユーザー
    group.AllowMembersEditMembership = true;                // グループのメンバーシップを編集できるユーザー:グループメンバー
    group.Update();
    clientContext.ExecuteQuery();
}

GroupCreationInformationクラス

新しく作成するSharepointグループの属性を指定します。下記以外の属性は、作成時初期値となり、必要に応じて別途設定が必要となるようです。

  • Title … グループ名
  • Description … グループの説明

実行結果

下記のようにSharePointグループが作成されます。
image.png

参照

GroupCreationInformationクラス

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityで『Unexpected character』というエラーが出ているが、おかしい場所(部分)が見当たらない。

 実現したいこと

  • スクリプトのエラーの解消
  • Unityで作成したプログラムを動かしたい

 困っていること

Unityで『Object2Terrain』のスクリプトを書いていたのですが、下記のようなメッセージが出た上、メニューアイテムにこのプログラムが追加されないので困っています…。プログラムはVisual studio 2019で書きました。

 発生しているエラーメッセージ

No MonoBehaviour scripts in the file, or their names do not match fale name.

 試したこと

- クラス名の修正
- 大文字、小文字のミスがないか確認
- using System.Collection;の追加

using UnityEngine;
using UnityEditor;
using System.Collection;

 コード比較(修正前、修正後)

 修正前のコード

using UnityEngine;
using UnityEditor;

public class Object2Terrain : EditorWindow
{

    [MenuItem("Terrain/Object to Terrain", false, 2000)]
    static void OpenWindow()
    {

        EditorWindow.GetWindow<Object2Terrain>(true);
    }

    private int resolution = 512;
    private Vector3 addTerrain;
    int bottomTopRadioSelected = 0;
    static string[] bottomTopRadio = new string[] { "Bottom Up", "Top Down" };
    private float shiftHeight = 0f;

    void OnGUI()
    {

        resolution = EditorGUILayout.IntField("Resolution", resolution);
        addTerrain = EditorGUILayout.Vector3Field("Add terrain", addTerrain);
        shiftHeight = EditorGUILayout.Slider("Shift height", shiftHeight, -1f, 1f);
        bottomTopRadioSelected = GUILayout.SelectionGrid(bottomTopRadioSelected, bottomTopRadio, bottomTopRadio.Length, EditorStyles.radioButton);

        if (GUILayout.Button("Create Terrain"))
        {

            if (Selection.activeGameObject == null)
            {

                EditorUtility.DisplayDialog("No object selected", "Please select an object.", "Ok");
                return;
            }

            else
            {

                CreateTerrain();
            }
        }
    }

    delegate void CleanUp();

    void CreateTerrain()
    {

        //fire up the progress bar
        ShowProgressBar(1, 100);

        TerrainData terrain = new TerrainData();
        terrain.heightmapResolution = resolution;
        terrain.SetDetailResolution(resolution, 100);
        GameObject terrainObject = Terrain.CreateTerrainGameObject(terrain);

        Undo.RegisterCreatedObjectUndo(terrainObject, "Object to Terrain");

        MeshCollider collider = Selection.activeGameObject.GetComponent<MeshCollider>();
        CleanUp cleanUp = null;

        //Add a collider to our source object if it does not exist.
        //Otherwise raycasting doesn't work.
        if (!collider)
        {

            collider = Selection.activeGameObject.AddComponent<MeshCollider>();
            cleanUp = () => DestroyImmediate(collider);
        }

        Bounds bounds = collider.bounds;
        float sizeFactor = collider.bounds.size.y / (collider.bounds.size.y + addTerrain.y);
        terrain.size = collider.bounds.size + addTerrain;
        bounds.size = new Vector3(terrain.size.x, collider.bounds.size.y, terrain.size.z);

        // Do raycasting samples over the object to see what terrain heights should be
        float[,] heights = new float[terrain.heightmapWidth, terrain.heightmapHeight];
        Ray ray = new Ray(new Vector3(bounds.min.x, bounds.max.y + bounds.size.y, bounds.min.z), -Vector3.up);
        RaycastHit hit = new RaycastHit();
        float meshHeightInverse = 1 / bounds.size.y;
        Vector3 rayOrigin = ray.origin;

        int maxHeight = heights.GetLength(0);
        int maxLength = heights.GetLength(1);

        Vector2 stepXZ = new Vector2(bounds.size.x / maxLength, bounds.size.z / maxHeight);

        for (int zCount = 0; zCount < maxHeight; zCount++)
        {

            ShowProgressBar(zCount, maxHeight);

            for (int xCount = 0; xCount < maxLength; xCount++)
            {

                float height = 0.0f;

                if (collider.Raycast(ray, out hit, bounds.size.y * 3))
                {

                    height = (hit.point.y - bounds.min.y) * meshHeightInverse;
                    height += shiftHeight;

                    //bottom up
                    if (bottomTopRadioSelected == 0)
                    {

                        height *= sizeFactor;
                    }

                    //clamp
                    if (height < 0)
                    {

                        height = 0;
                    }
                }

                heights[zCount, xCount] = height;
                rayOrigin.x += stepXZ[0];
                ray.origin = rayOrigin;
            }

            rayOrigin.z += stepXZ[1];
            rayOrigin.x = bounds.min.x;
            ray.origin = rayOrigin;
        }

        terrain.SetHeights(0, 0, heights);

        EditorUtility.ClearProgressBar();

        if (cleanUp != null)
        {

            cleanUp();
        }
    }

    void ShowProgressBar(float progress, float maxProgress)
    {

        float p = progress / maxProgress;
        EditorUtility.DisplayProgressBar("Creating Terrain...", Mathf.RoundToInt(p * 100f) + " %", p);
    }
}

 修正後のコード

using UnityEngine;
using UnityEditor;
using System.Collections;

public class Object2Terrain : EditorWindow {

    [MenuItem("Terrain/Object to Terrain", false, 2000)]
    static void OpenWindow() => EditorWindow.GetWindow(typeof(Object2Terrain))(true);

    private int resolution = 512;
    private Vector3 addTerrain;
    int bottomTopRadioSelected = 0;
    static string[] bottomTopRadio = new string[] { "Bottom Up", "Top Down"};
    private float shiftHeight = 0f;

    void OnGUI () {

        resolution = EditorGUILayout.IntField("Resolution", resolution);
        addTerrain = EditorGUILayout.Vector3Field("Add terrain", addTerrain);
        shiftHeight = EditorGUILayout.Slider("Shift height", shiftHeight, -1f, 1f);
        bottomTopRadioSelected = GUILayout.SelectionGrid(bottomTopRadioSelected, bottomTopRadio, bottomTopRadio.Length, EditorStyles.radioButton);

        if(GUILayout.Button("Create Terrain")){

            if(Selection.activeGameObject == null){

                EditorUtility.DisplayDialog("No object selected", "Please select an object.", "Ok");
                return;
            }

            else{

                CreateTerrain();
            }
        }
    }

    delegate void CleanUp();

    void CreateTerrain(){    

        //fire up the progress bar
        ShowProgressBar(1, 100);

        TerrainData terrain = new TerrainData();
        terrain.heightmapResolution = resolution;
        GameObject terrainObject = Terrain.CreateTerrainGameObject(terrain);

        Undo.RegisterCreatedObjectUndo(terrainObject, "Object to Terrain");

        MeshCollider collider = Selection.activeGameObject.GetComponent<MeshCollider>();
        CleanUp cleanUp = null;

        //Add a collider to our source object if it does not exist.
        //Otherwise raycasting doesn't work.
        if(!collider){

            collider = Selection.activeGameObject.AddComponent<MeshCollider>();
            cleanUp = () => DestroyImmediate(collider);
        }

        Bounds bounds = collider.bounds;    
        float sizeFactor = collider.bounds.size.y / (collider.bounds.size.y + addTerrain.y);
        terrain.size = collider.bounds.size + addTerrain;
        bounds.size = new Vector3(terrain.size.x, collider.bounds.size.y, terrain.size.z);

        // Do raycasting samples over the object to see what terrain heights should be
        float[,] heights = new float[terrain.heightmapWidth, terrain.heightmapHeight];    
        Ray ray = new Ray(new Vector3(bounds.min.x, bounds.max.y + bounds.size.y, bounds.min.z), -Vector3.up);
        RaycastHit hit = new RaycastHit();
        float meshHeightInverse = 1 / bounds.size.y;
        Vector3 rayOrigin = ray.origin;

        int maxHeight = heights.GetLength(0);
        int maxLength = heights.GetLength(1);

        Vector2 stepXZ = new Vector2(bounds.size.x / maxLength, bounds.size.z / maxHeight);

        for(int zCount = 0; zCount < maxHeight; zCount++){

            ShowProgressBar(zCount, maxHeight);

            for(int xCount = 0; xCount < maxLength; xCount++){

                float height = 0.0f;

                if(collider.Raycast(ray, out hit, bounds.size.y * 3)){

                    height = (hit.point.y - bounds.min.y) * meshHeightInverse;
                    height += shiftHeight;

                    //bottom up
                    if(bottomTopRadioSelected == 0){

                        height *= sizeFactor;
                    }

                    //clamp
                    if(height < 0){

                        height = 0;
                    }
                }

                heights[zCount, xCount] = height;
                   rayOrigin.x += stepXZ[0];
                   ray.origin = rayOrigin;
            }

            rayOrigin.z += stepXZ[1];
              rayOrigin.x = bounds.min.x;
              ray.origin = rayOrigin;
        }

        terrain.SetHeights(0, 0, heights);

        EditorUtility.ClearProgressBar();

        if(cleanUp != null){

            cleanUp();    
        }
    }

    void ShowProgressBar(float progress, float maxProgress){

        float p = progress / maxProgress;
        EditorUtility.DisplayProgressBar("Creating Terrain...", Mathf.RoundToInt(p * 100f)+ " %", p);
    }
}

 修正したのは良いが

修正しましたが、そしたらまたエラーメッセージが…。

Assets\Editor\object2terrain.cs(1,1):error CS1056: Unexpected character"

そんなメッセージが出たので再度修正作業を行いました。

 2回目の修正後のコード

using UnityEngine;
using UnityEditor;
using System.Collections;

public class Object2Terrain : EditorWindow {

    [MenuItem("Terrain/Object2Terrain", false, 2000)]
    static void OpenWindow() => EditorWindow.GetWindow(typeof(Object2Terrain))(true);

        private int resolution = 512;
    private Vector3 addTerrain;
    int bottomTopRadioSelected = 0;
    static string[] bottomTopRadio = new string[] ("Bottom Up", "Top Down");
    private float shiftHeight = 0f;

    void OnGUI () {

        resolution = EditorGUILayout.IntField("Resolution", resolution);
        addTerrain = EditorGUILayout.Vector3Field("Add terrain", addTerrain);
        shiftHeight = EditorGUILayout.Slider("Shift height", shiftHeight, -1f, 1f);
        bottomTopRadioSelected = GUILayout.SelectionGrid(bottomTopRadioSelected, bottomTopRadio, bottomTopRadio.Length, EditorStyles.radioButton);

        if(GUILayout.Button("Create Terrain")){

            if(Selection.activeGameObject == null){

                EditorUtility.DisplayDialog("No object selected", "Please select an object.", "Ok");

                                return;
            }


                    else{

                CreateTerrain();
            }
        }
    }


        delegate void CleanUp();

    void CreateTerrain(){   

        //Fire up the progress bar
        ShowProgressBar(1, 100);

        TerrainData terrain = new TerrainData();
        terrain.heightmapResolution = resolution;
        GameObject terrainObject = Terrain.CreateTerrainGameObject(terrain);

        Undo.RegisterCreatedObjectUndo(terrainObject, "Object2Terrain");

        MeshCollider collider = Selection.activeGameObject.GetComponent<MeshCollider>();
        CleanUp cleanUp = null;

        //Add a collider to our source object if it does not exist.
        //Otherwise raycasting doesn't work.
        if(!collider){

            collider = Selection.activeGameObject.AddComponent<MeshCollider>();
            cleanUp = () => DestroyImmediate(collider);
                    }


        Bounds bounds = collider.bounds;    
        float sizeFactor = collider.bounds.size.y / (collider.bounds.size.y + addTerrain.y);
        terrain.size = collider.bounds.size + addTerrain;
        bounds.size = new Vector3(terrain.size.x, collider.bounds.size.y, terrain.size.z);

        // Do raycasting samples over the object to see what terrain heights should be
        float[,] heights = new float[terrain.heightmapWidth, terrain.heightmapHeight];  
        Ray ray = new Ray(new Vector3(bounds.min.x, bounds.max.y + bounds.size.y, bounds.min.z), -Vector3.up);
        RaycastHit hit = new RaycastHit();
        float meshHeightInverse = 1 / bounds.size.y;
        Vector3 rayOrigin = ray.origin;

        int maxHeight = heights.GetLength(0);
        int maxLength = heights.GetLength(1);

        Vector2 stepXZ = new Vector2(bounds.size.x / maxLength, bounds.size.z / maxHeight);

        for(int zCount = 0; zCount < maxHeight; zCount++){

            ShowProgressBar(zCount, maxHeight);

            for(int xCount = 0; xCount < maxLength; xCount++){

                float height = 0.0f;

                if(collider.Raycast(ray, out hit, bounds.size.y * 3)){

                    height = (hit.point.y - bounds.min.y) * meshHeightInverse;
                    height += shiftHeight;

                    //bottom up
                    if(bottomTopRadioSelected == 0){

                        height *= sizeFactor;
                    }

                    //clamp
                    if(height < 0){

                        height = 0;
                                  }
                }

                heights[zCount, xCount] = height;
                rayOrigin.x += stepXZ[0];
                ray.origin = rayOrigin;
            }

            rayOrigin.z += stepXZ[1];
            rayOrigin.x = bounds.min.x;
            ray.origin = rayOrigin;
        }

        terrain.SetHeights(0, 0, heights);

        EditorUtility.ClearProgressBar();

        if(cleanUp != null){

            cleanUp();    
        }
    }

    void ShowProgressBar(float progress, float maxProgress){

        float p = progress / maxProgress;
        EditorUtility.DisplayProgressBar("Creating Terrain...", Mathf.RoundToInt(p * 100f)+ " %", p);
    }

 しかし!

Assets\Editor\object2terrain.cs(1,1):error CS1056: Unexpected character"

このメッセージが消えず…。なにか対処法はありますか!?

自分にはミスがわからないので救世主が欲しいです…。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TwainDotNetが内部で32ビットBitmapを作成する部分を8ビット以下にも対応

1. はじめに

TwainDotNetでグレースケールをスキャンすると、内部で32ビットBitmapが生成され、保存した時にファイルサイズが大きくなります。このため、グレースケールの場合には8ビットで保存できるようにTwainDotNetを改造してみました。スキャンアプリケーション ITScan - Qiitaに含めています。

2. 変更前ソース

TwainDotNetバージョン1.0.0を元にしています。
BitmapコンストラクタにPixelFormatの指定がなく、必ず32ビットのBitmapが作成されるようになっています。

BitmapRenderer.cs抜粋
        public Bitmap RenderToBitmap()
        {
            Bitmap bitmap = new Bitmap(_rectangle.Width, _rectangle.Height);

            using (Graphics graphics = Graphics.FromImage(bitmap))
            {
                IntPtr hdc = graphics.GetHdc();

                try
                {
                    Gdi32Native.SetDIBitsToDevice(hdc, 0, 0, _rectangle.Width, _rectangle.Height,
                        0, 0, 0, _rectangle.Height, _pixelInfoPointer, _bitmapPointer, 0);
                }
                finally
                {
                    graphics.ReleaseHdc(hdc);
                }
            }

            bitmap.SetResolution(PpmToDpi(_bitmapInfo.XPelsPerMeter), PpmToDpi(_bitmapInfo.YPelsPerMeter));

            return bitmap;
        }

3. 変更後ソース

元データが8ビット以下の場合に8ビットのビットマップを構築しています。詳細は後述します。8ビットより大きい場合は元の処理を行っています。

BitmapRenderer.cs抜粋
        public Bitmap RenderToBitmap()
        {
            if (_bitmapInfo.BitCount <= 8)
            {
                int sizeBitmapFileHeader = Marshal.SizeOf(typeof(BitmapFileHeader));

                BitmapFileHeader bitmapFile = new BitmapFileHeader();
                bitmapFile.Type = 'M' * 256 + 'B';
                bitmapFile.Size = (_pixelInfoPointer.ToInt32() - _bitmapPointer.ToInt32()) + sizeBitmapFileHeader + _bitmapInfo.SizeImage;
                bitmapFile.Reserved1 = 0;
                bitmapFile.Reserved2 = 0;
                bitmapFile.OffBits = (_pixelInfoPointer.ToInt32() - _bitmapPointer.ToInt32()) + sizeBitmapFileHeader;

                IntPtr _bitmapFilePointer = Marshal.AllocHGlobal(sizeBitmapFileHeader);
                Marshal.StructureToPtr(bitmapFile, _bitmapFilePointer, true);

                byte[] buffer = new byte[bitmapFile.Size];
                Marshal.Copy(_bitmapFilePointer, buffer, 0, sizeBitmapFileHeader);
                Marshal.Copy(_bitmapPointer, buffer, sizeBitmapFileHeader, bitmapFile.Size - sizeBitmapFileHeader);

                using (MemoryStream ms = new MemoryStream(buffer))
                {
                    Bitmap bmp = new Bitmap(ms);

                    BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, bmp.PixelFormat);
                    Bitmap bitmap = new Bitmap(bmp.Width, bmp.Height, bmpData.Stride, bmp.PixelFormat, bmpData.Scan0);
                    bmp.UnlockBits(bmpData);

                    bitmap.Palette = bmp.Palette;

                    bitmap.SetResolution(PpmToDpi(_bitmapInfo.XPelsPerMeter), PpmToDpi(_bitmapInfo.YPelsPerMeter));

                    return bitmap;
                }
            }
            else
            {
                Bitmap bitmap = new Bitmap(_rectangle.Width, _rectangle.Height);

                using (Graphics graphics = Graphics.FromImage(bitmap))
                {
                    IntPtr hdc = graphics.GetHdc();

                    try
                    {
                        Gdi32Native.SetDIBitsToDevice(hdc, 0, 0, _rectangle.Width, _rectangle.Height,
                            0, 0, 0, _rectangle.Height, _pixelInfoPointer, _bitmapPointer, 0);
                    }
                    finally
                    {
                        graphics.ReleaseHdc(hdc);
                    }
                }

                bitmap.SetResolution(PpmToDpi(_bitmapInfo.XPelsPerMeter), PpmToDpi(_bitmapInfo.YPelsPerMeter));

                return bitmap;
            }
        }

既存のBitmapInfoHeader.csを元に、下記のファイルを同じように作成しました。

BitmapFileHeader.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.Drawing;
using System.Diagnostics;

namespace TwainDotNet.Win32
{
    [StructLayout(LayoutKind.Sequential, Pack = 2)]
    public class BitmapFileHeader
    {
        public short Type;
        public int Size;
        public short Reserved1;
        public short Reserved2;
        public int OffBits;

        public override string ToString()
        {
            return string.Format(
                "t:{0} s:{1} r1:{2} r2:{3} o:{4}",
                Type,
                Size,
                Reserved1,
                Reserved2,
                OffBits);
        }
    }
}

4. 解説

変更前は下記のようにBitmapコンストラクタにPixelFormatの指定がなく、必ず32ビットのBitmapが作成されるようになっています。

            Bitmap bitmap = new Bitmap(_rectangle.Width, _rectangle.Height);

ここの前に、ビット数が8(グレースケール)以下の場合に分岐を加えます。

            if (_bitmapInfo.BitCount <= 8)
            {

但し、BitmapコンストラクタにPixelFormat.Format8bppIndexedを指定してGraphics.FromImage()を行うと例外が発生してGraphicsオブジェクトを作成できません。
このため、GraphicsオブジェクトからgetHdc()を行ってhDCを取得し、SetDIBitsToDevice()を行うという手が使用できません。

失敗例
            if (_bitmapInfo.BitCount <= 8)
            {
                Bitmap bitmap = new Bitmap(_rectangle.Width, _rectangle.Height, PixelFormat.Format8bppIndexed);

                using (Graphics graphics = Graphics.FromImage(bitmap))
                {

これを回避するため、MemoryStreamからBitmapを作成しています。但し、元データにBitmapFileHeaderがないため、自前で作成しています。
BitmapFileHeader作成、byte配列の先頭にコピーします。

                int sizeBitmapFileHeader = Marshal.SizeOf(typeof(BitmapFileHeader));

                BitmapFileHeader bitmapFile = new BitmapFileHeader();
                bitmapFile.Type = 'M' * 256 + 'B';
                bitmapFile.Size = (_pixelInfoPointer.ToInt32() - _bitmapPointer.ToInt32()) + sizeBitmapFileHeader + _bitmapInfo.SizeImage;
                bitmapFile.Reserved1 = 0;
                bitmapFile.Reserved2 = 0;
                bitmapFile.OffBits = (_pixelInfoPointer.ToInt32() - _bitmapPointer.ToInt32()) + sizeBitmapFileHeader;

                IntPtr _bitmapFilePointer = Marshal.AllocHGlobal(sizeBitmapFileHeader);
                Marshal.StructureToPtr(bitmapFile, _bitmapFilePointer, true);

BitmapInfoHeaderとカラーテーブルとビットイメージをBitmapFileHeaderの次にコピーします。

                byte[] buffer = new byte[bitmapFile.Size];
                Marshal.Copy(_bitmapFilePointer, buffer, 0, sizeBitmapFileHeader);
                Marshal.Copy(_bitmapPointer, buffer, sizeBitmapFileHeader, bitmapFile.Size - sizeBitmapFileHeader);

MemoryStreamを作成し、そこからBitmapを作成します。それを呼び出し元で保存すると、グレースケールの場合には8ビットのBMPやPNG等で保存できます。
但し、これだけだとMemoryStreamを解放していないのでメモリリークします。

失敗例1:MemoryStreamを解放していない
                MemoryStream ms = new MemoryStream(buffer);
                Bitmap bitmap = new Bitmap(ms);

                bitmap.SetResolution(PpmToDpi(_bitmapInfo.XPelsPerMeter), PpmToDpi(_bitmapInfo.YPelsPerMeter));

                return bitmap;

MemoryStreamをusing等で解放すると、画像の保存時(Image.Saveメソッド実行時)に「GDI+で汎用エラーが発生しました」が発生します。

失敗例2:呼び出し元でSave時に「GDI+で汎用エラー」
                using (MemoryStream ms = new MemoryStream(buffer))
                {
                    Bitmap bitmap = new Bitmap(ms);

                    bitmap.SetResolution(PpmToDpi(_bitmapInfo.XPelsPerMeter), PpmToDpi(_bitmapInfo.YPelsPerMeter));

                    return bitmap;
                }

これを回避するため、Bitmapを元にBitmapオブジェクトを作り直してそれを返却しています。但し、単純に Bitmap bitmap = new Bitmap(bmp) とすると、32ビットBitmapになってしまいます。

失敗例3:32ビットBitmapになる
                using (MemoryStream ms = new MemoryStream(buffer))
                {
                    Bitmap bmp = new Bitmap(ms);
                    Bitmap bitmap = new Bitmap(bmp);

                    bitmap.SetResolution(PpmToDpi(_bitmapInfo.XPelsPerMeter), PpmToDpi(_bitmapInfo.YPelsPerMeter));

                    return bitmap;
                }

この回避のために、LockBits()をして、PixelFomatかつビットマップイメージの引数があるコンストラクタを呼んでいます。それだけだとカラーテーブルがコピーされないため、それもコピーしています。

正しい方法:8ビット以下のBitmapになる
                using (MemoryStream ms = new MemoryStream(buffer))
                {
                    Bitmap bmp = new Bitmap(ms);

                    BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, bmp.PixelFormat);
                    Bitmap bitmap = new Bitmap(bmp.Width, bmp.Height, bmpData.Stride, bmp.PixelFormat, bmpData.Scan0);
                    bmp.UnlockBits(bmpData);

                    bitmap.Palette = bmp.Palette;

                    bitmap.SetResolution(PpmToDpi(_bitmapInfo.XPelsPerMeter), PpmToDpi(_bitmapInfo.YPelsPerMeter));

                    return bitmap;
                }

5. 補足

これでグレースケールでスキャンしたものが8ビットBitmapになります(Canon DR-G1130で確認)。それをPNGで保存すると8ビットPNGになります。
1ビット(2値)にも対応しました。同じ処理でいけました。
ざっと確認したばかりで、詳細なテストは未実施です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TwainDotNetが内部で32ビットBitmapを作成する部分を8ビットにも対応

1. はじめに

TwainDotNetでグレースケールをスキャンすると、内部で32ビットBitmapが生成され、保存した時にファイルサイズが大きくなります。このため、グレースケールの場合には8ビットで保存するようにTwainDotNetを変更してみました。

2. 変更前ソース

TwainDotNetバージョン1.0.0を元にしています。
BitmapコンストラクタにPixelFormatの指定がなく、必ず32ビットのBitmapが作成されるようになっています。

BitmapRenderer.cs抜粋
        public Bitmap RenderToBitmap()
        {
            Bitmap bitmap = new Bitmap(_rectangle.Width, _rectangle.Height);

            using (Graphics graphics = Graphics.FromImage(bitmap))
            {
                IntPtr hdc = graphics.GetHdc();

                try
                {
                    Gdi32Native.SetDIBitsToDevice(hdc, 0, 0, _rectangle.Width, _rectangle.Height,
                        0, 0, 0, _rectangle.Height, _pixelInfoPointer, _bitmapPointer, 0);
                }
                finally
                {
                    graphics.ReleaseHdc(hdc);
                }
            }

            bitmap.SetResolution(PpmToDpi(_bitmapInfo.XPelsPerMeter), PpmToDpi(_bitmapInfo.YPelsPerMeter));

            return bitmap;
        }

3. 変更後ソース

元データが8ビットの場合に8ビットのビットマップを構築しています。詳細は後述します。8ビット以外は元の処理を行っています。

BitmapRenderer.cs抜粋
        public Bitmap RenderToBitmap()
        {
            if (_bitmapInfo.BitCount == 8)
            {
                int sizeBitmapFileHeader = Marshal.SizeOf(typeof(BitmapFileHeader));

                BitmapFileHeader bitmapFile = new BitmapFileHeader();
                bitmapFile.Type = 'M' * 256 + 'B';
                bitmapFile.Size = (_pixelInfoPointer.ToInt32() - _bitmapPointer.ToInt32()) + sizeBitmapFileHeader + _bitmapInfo.SizeImage;
                bitmapFile.Reserved1 = 0;
                bitmapFile.Reserved2 = 0;
                bitmapFile.OffBits = (_pixelInfoPointer.ToInt32() - _bitmapPointer.ToInt32()) + sizeBitmapFileHeader;

                IntPtr _bitmapFilePointer = Marshal.AllocHGlobal(sizeBitmapFileHeader);
                Marshal.StructureToPtr(bitmapFile, _bitmapFilePointer, true);

                byte[] buffer = new byte[bitmapFile.Size];
                Marshal.Copy(_bitmapFilePointer, buffer, 0, sizeBitmapFileHeader);
                Marshal.Copy(_bitmapPointer, buffer, sizeBitmapFileHeader, bitmapFile.Size - sizeBitmapFileHeader);

                using (MemoryStream ms = new MemoryStream(buffer))
                {
                    Bitmap bmp = new Bitmap(ms);

                    BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, bmp.PixelFormat);
                    Bitmap bitmap = new Bitmap(bmp.Width, bmp.Height, bmpData.Stride, bmp.PixelFormat, bmpData.Scan0);
                    bmp.UnlockBits(bmpData);

                    bitmap.Palette = bmp.Palette;

                    bitmap.SetResolution(PpmToDpi(_bitmapInfo.XPelsPerMeter), PpmToDpi(_bitmapInfo.YPelsPerMeter));

                    return bitmap;
                }
            }
            else
            {
                Bitmap bitmap = new Bitmap(_rectangle.Width, _rectangle.Height);

                using (Graphics graphics = Graphics.FromImage(bitmap))
                {
                    IntPtr hdc = graphics.GetHdc();

                    try
                    {
                        Gdi32Native.SetDIBitsToDevice(hdc, 0, 0, _rectangle.Width, _rectangle.Height,
                            0, 0, 0, _rectangle.Height, _pixelInfoPointer, _bitmapPointer, 0);
                    }
                    finally
                    {
                        graphics.ReleaseHdc(hdc);
                    }
                }

                bitmap.SetResolution(PpmToDpi(_bitmapInfo.XPelsPerMeter), PpmToDpi(_bitmapInfo.YPelsPerMeter));

                return bitmap;
            }
        }

既存のBitmapInfoHeader.csを元に、下記のファイルを同じように作成しました。

BitmapFileHeader.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.Drawing;
using System.Diagnostics;

namespace TwainDotNet.Win32
{
    [StructLayout(LayoutKind.Sequential, Pack = 2)]
    public class BitmapFileHeader
    {
        public short Type;
        public int Size;
        public short Reserved1;
        public short Reserved2;
        public int OffBits;

        public override string ToString()
        {
            return string.Format(
                "t:{0} s:{1} r1:{2} r2:{3} o:{4}",
                Type,
                Size,
                Reserved1,
                Reserved2,
                OffBits);
        }
    }
}

4. 解説

変更前は下記のようにBitmapコンストラクタにPixelFormatの指定がなく、必ず32ビットのBitmapが作成されるようになっています。

            Bitmap bitmap = new Bitmap(_rectangle.Width, _rectangle.Height);

ここの前に、ビット数が8(グレースケール)の場合に分岐を加えます。

            if (_bitmapInfo.BitCount == 8)
            {

但し、BitmapコンストラクタにPixelFormat.Format8bppIndexedを指定してGraphics.FromImage()を行うと例外が発生してGraphicsオブジェクトを作成できません。
このため、GraphicsオブジェクトからgetHdc()を行ってhDCを取得し、SetDIBitsToDevice()を行うという手が使用できません。

失敗例
            if (_bitmapInfo.BitCount == 8)
            {
                Bitmap bitmap = new Bitmap(_rectangle.Width, _rectangle.Height, PixelFormat.Format8bppIndexed);

                using (Graphics graphics = Graphics.FromImage(bitmap))
                {

これを回避するため、MemoryStreamからBitmapを作成しています。但し、元データにBitmapFileHeaderがないため、自前で作成しています。
BitmapFileHeader作成、byte配列の先頭にコピーします。

                int sizeBitmapFileHeader = Marshal.SizeOf(typeof(BitmapFileHeader));

                BitmapFileHeader bitmapFile = new BitmapFileHeader();
                bitmapFile.Type = 'M' * 256 + 'B';
                bitmapFile.Size = (_pixelInfoPointer.ToInt32() - _bitmapPointer.ToInt32()) + sizeBitmapFileHeader + _bitmapInfo.SizeImage;
                bitmapFile.Reserved1 = 0;
                bitmapFile.Reserved2 = 0;
                bitmapFile.OffBits = (_pixelInfoPointer.ToInt32() - _bitmapPointer.ToInt32()) + sizeBitmapFileHeader;

                IntPtr _bitmapFilePointer = Marshal.AllocHGlobal(sizeBitmapFileHeader);
                Marshal.StructureToPtr(bitmapFile, _bitmapFilePointer, true);

BitmapInfoHeaderとカラーテーブルとビットイメージをBitmapFileHeaderの次にコピーします。

                byte[] buffer = new byte[bitmapFile.Size];
                Marshal.Copy(_bitmapFilePointer, buffer, 0, sizeBitmapFileHeader);
                Marshal.Copy(_bitmapPointer, buffer, sizeBitmapFileHeader, bitmapFile.Size - sizeBitmapFileHeader);

MemoryStreamを作成し、そこからBitmapを作成します。
MemoryStreamを元にしたBitmapを返却し、MemoryStreamをusing等で解放すると、画像の保存時(Image.Saveメソッド実行時)に「GDI+で汎用エラーが発生しました」が発生します。これを回避するため、Bitmapを元にBitmapオブジェクトを作り直してそれを返却しています。
但し、単純に Bitmap bitmap = new Bitmap(bmp) とすると、32ビットBitmapになってしまいます。この回避のために、LockBits()をして、PixelFomatかつビットマップイメージの引数があるコンストラクタを呼んでいます。それだけだとカラーテーブルがコピーされないため、それもコピーしています。

NG例
                using (MemoryStream ms = new MemoryStream(buffer))
                {
                    Bitmap bmp = new Bitmap(ms);
                    Bitmap bitmap = new Bitmap(bmp);

                    bitmap.SetResolution(PpmToDpi(_bitmapInfo.XPelsPerMeter), PpmToDpi(_bitmapInfo.YPelsPerMeter));

                    return bitmap;
                }
正しい方法
                using (MemoryStream ms = new MemoryStream(buffer))
                {
                    Bitmap bmp = new Bitmap(ms);

                    BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, bmp.PixelFormat);
                    Bitmap bitmap = new Bitmap(bmp.Width, bmp.Height, bmpData.Stride, bmp.PixelFormat, bmpData.Scan0);
                    bmp.UnlockBits(bmpData);

                    bitmap.Palette = bmp.Palette;

                    bitmap.SetResolution(PpmToDpi(_bitmapInfo.XPelsPerMeter), PpmToDpi(_bitmapInfo.YPelsPerMeter));

                    return bitmap;
                }

5. 補足

これでグレースケールでスキャンしたものが8ビットBitmapになります。(Canon DR-G1130で確認) それをPNGで保存すると8ビットPNGになります。
ざっと確認したばかりで、詳細なテストは未実施です。
1ビット(2値)には対応していません。(同じコードでいける?)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TwainDotNetが内部で32ビットBitmapを作成する部分を24ビット以下にも対応

1. はじめに

TwainDotNetでグレースケールをスキャンすると、内部で32ビットBitmapが生成され、保存した時にファイルサイズが大きくなります。このため、グレースケールの場合には8ビットで保存できるようにTwainDotNetを改造してみました。1ビット、24ビットにも対応しています。スキャンアプリケーション ITScan - Qiitaに含めています。

2. 変更前ソース

TwainDotNetバージョン1.0.0を元にしています。
BitmapコンストラクタにPixelFormatの指定がなく、必ず32ビットのBitmapが作成されるようになっています。

BitmapRenderer.cs抜粋
        public Bitmap RenderToBitmap()
        {
            Bitmap bitmap = new Bitmap(_rectangle.Width, _rectangle.Height);

            using (Graphics graphics = Graphics.FromImage(bitmap))
            {
                IntPtr hdc = graphics.GetHdc();

                try
                {
                    Gdi32Native.SetDIBitsToDevice(hdc, 0, 0, _rectangle.Width, _rectangle.Height,
                        0, 0, 0, _rectangle.Height, _pixelInfoPointer, _bitmapPointer, 0);
                }
                finally
                {
                    graphics.ReleaseHdc(hdc);
                }
            }

            bitmap.SetResolution(PpmToDpi(_bitmapInfo.XPelsPerMeter), PpmToDpi(_bitmapInfo.YPelsPerMeter));

            return bitmap;
        }

3. 変更後ソース

元データが24ビット以下の場合にビット数に合わせたビットマップを構築しています。詳細は後述します。24ビットより大きい場合(32ビット)は元の処理を行っています。

BitmapRenderer.cs抜粋
        public Bitmap RenderToBitmap()
        {
            if (_bitmapInfo.BitCount <= 24)
            {
                int sizeBitmapFileHeader = Marshal.SizeOf(typeof(BitmapFileHeader));

                BitmapFileHeader bitmapFile = new BitmapFileHeader();
                bitmapFile.Type = 'M' * 256 + 'B';
                bitmapFile.Size = (_pixelInfoPointer.ToInt32() - _bitmapPointer.ToInt32()) + sizeBitmapFileHeader + _bitmapInfo.SizeImage;
                bitmapFile.Reserved1 = 0;
                bitmapFile.Reserved2 = 0;
                bitmapFile.OffBits = (_pixelInfoPointer.ToInt32() - _bitmapPointer.ToInt32()) + sizeBitmapFileHeader;

                IntPtr _bitmapFilePointer = Marshal.AllocHGlobal(sizeBitmapFileHeader);
                Marshal.StructureToPtr(bitmapFile, _bitmapFilePointer, true);

                byte[] buffer = new byte[bitmapFile.Size];
                Marshal.Copy(_bitmapFilePointer, buffer, 0, sizeBitmapFileHeader);
                Marshal.Copy(_bitmapPointer, buffer, sizeBitmapFileHeader, bitmapFile.Size - sizeBitmapFileHeader);

                Bitmap bitmap = null;
                using (MemoryStream ms = new MemoryStream(buffer))
                {
                    Bitmap bmp = new Bitmap(ms);

                    BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, bmp.PixelFormat);
                    try
                    {
                        bitmap = new Bitmap(bmp.Width, bmp.Height, bmpData.Stride, bmp.PixelFormat, bmpData.Scan0);
                    }
                    finally
                    {
                        bmp.UnlockBits(bmpData);
                    }

                    if (_bitmapInfo.BitCount <= 8)
                    {
                        bitmap.Palette = bmp.Palette;
                    }
                }

                bitmap.SetResolution(PpmToDpi(_bitmapInfo.XPelsPerMeter), PpmToDpi(_bitmapInfo.YPelsPerMeter));

                return bitmap;
            }
            else
            {
                Bitmap bitmap = new Bitmap(_rectangle.Width, _rectangle.Height);

                using (Graphics graphics = Graphics.FromImage(bitmap))
                {
                    IntPtr hdc = graphics.GetHdc();

                    try
                    {
                        Gdi32Native.SetDIBitsToDevice(hdc, 0, 0, _rectangle.Width, _rectangle.Height,
                            0, 0, 0, _rectangle.Height, _pixelInfoPointer, _bitmapPointer, 0);
                    }
                    finally
                    {
                        graphics.ReleaseHdc(hdc);
                    }
                }

                bitmap.SetResolution(PpmToDpi(_bitmapInfo.XPelsPerMeter), PpmToDpi(_bitmapInfo.YPelsPerMeter));

                return bitmap;
            }
        }

既存のBitmapInfoHeader.csを元に、下記のファイルを同じように作成しました。

BitmapFileHeader.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.Drawing;
using System.Diagnostics;

namespace TwainDotNet.Win32
{
    [StructLayout(LayoutKind.Sequential, Pack = 2)]
    public class BitmapFileHeader
    {
        public short Type;
        public int Size;
        public short Reserved1;
        public short Reserved2;
        public int OffBits;

        public override string ToString()
        {
            return string.Format(
                "t:{0} s:{1} r1:{2} r2:{3} o:{4}",
                Type,
                Size,
                Reserved1,
                Reserved2,
                OffBits);
        }
    }
}

4. 解説

変更前は下記のようにBitmapコンストラクタにPixelFormatの指定がなく、必ず32ビットのBitmapが作成されるようになっています。

            Bitmap bitmap = new Bitmap(_rectangle.Width, _rectangle.Height);

ここの前に、ビット数が24以下の場合に分岐を加えます。

            if (_bitmapInfo.BitCount <= 24)
            {

但し、BitmapコンストラクタにPixelFormat.Format8bppIndexedを指定してGraphics.FromImage()を行うと例外が発生してGraphicsオブジェクトを作成できません。
このため、GraphicsオブジェクトからgetHdc()を行ってhDCを取得し、SetDIBitsToDevice()を行うという手が使用できません。

失敗例
            if (_bitmapInfo.BitCount <= 24)
            {
                Bitmap bitmap = new Bitmap(_rectangle.Width, _rectangle.Height, PixelFormat.Format8bppIndexed);

                using (Graphics graphics = Graphics.FromImage(bitmap))
                {

これを回避するため、MemoryStreamからBitmapを作成しています。但し、元データにBitmapFileHeaderがないため、自前で作成しています。
BitmapFileHeader作成、byte配列の先頭にコピーします。

                int sizeBitmapFileHeader = Marshal.SizeOf(typeof(BitmapFileHeader));

                BitmapFileHeader bitmapFile = new BitmapFileHeader();
                bitmapFile.Type = 'M' * 256 + 'B';
                bitmapFile.Size = (_pixelInfoPointer.ToInt32() - _bitmapPointer.ToInt32()) + sizeBitmapFileHeader + _bitmapInfo.SizeImage;
                bitmapFile.Reserved1 = 0;
                bitmapFile.Reserved2 = 0;
                bitmapFile.OffBits = (_pixelInfoPointer.ToInt32() - _bitmapPointer.ToInt32()) + sizeBitmapFileHeader;

                IntPtr _bitmapFilePointer = Marshal.AllocHGlobal(sizeBitmapFileHeader);
                Marshal.StructureToPtr(bitmapFile, _bitmapFilePointer, true);

BitmapInfoHeaderとカラーテーブルとビットイメージをBitmapFileHeaderの次にコピーします。

                byte[] buffer = new byte[bitmapFile.Size];
                Marshal.Copy(_bitmapFilePointer, buffer, 0, sizeBitmapFileHeader);
                Marshal.Copy(_bitmapPointer, buffer, sizeBitmapFileHeader, bitmapFile.Size - sizeBitmapFileHeader);

MemoryStreamを作成し、そこからBitmapを作成します。それを呼び出し元で保存すると、グレースケールの場合には8ビットのBMPやPNG等で保存できます。
但し、これだけだとMemoryStreamを解放していないのでメモリリークします。

失敗例1:MemoryStreamを解放していない
                MemoryStream ms = new MemoryStream(buffer);
                Bitmap bitmap = new Bitmap(ms);

                bitmap.SetResolution(PpmToDpi(_bitmapInfo.XPelsPerMeter), PpmToDpi(_bitmapInfo.YPelsPerMeter));

                return bitmap;

MemoryStreamをusing等で解放すると、画像の保存時(Image.Saveメソッド実行時)に「GDI+で汎用エラーが発生しました」が発生します。

失敗例2:呼び出し元でSave時に「GDI+で汎用エラー」
                using (MemoryStream ms = new MemoryStream(buffer))
                {
                    Bitmap bitmap = new Bitmap(ms);

                    bitmap.SetResolution(PpmToDpi(_bitmapInfo.XPelsPerMeter), PpmToDpi(_bitmapInfo.YPelsPerMeter));

                    return bitmap;
                }

これを回避するため、Bitmapを元にBitmapオブジェクトを作り直してそれを返却しています。但し、単純に Bitmap bitmap = new Bitmap(bmp) とすると、32ビットBitmapになってしまいます。

失敗例3:32ビットBitmapになる
                using (MemoryStream ms = new MemoryStream(buffer))
                {
                    Bitmap bmp = new Bitmap(ms);
                    Bitmap bitmap = new Bitmap(bmp);

                    bitmap.SetResolution(PpmToDpi(_bitmapInfo.XPelsPerMeter), PpmToDpi(_bitmapInfo.YPelsPerMeter));

                    return bitmap;
                }

この回避のために、LockBits()をして、PixelFomatかつビットマップイメージの引数があるコンストラクタを呼んでいます。それだけだとカラーテーブルがコピーされないため、8ビット以下の場合(24ビット以外)の場合にはカラーテーブルもコピーしています。24ビットの場合にコピーすると例外が発生します。

正しい方法:グレースケールが8ビットのBitmapになる
                Bitmap bitmap = null;
                using (MemoryStream ms = new MemoryStream(buffer))
                {
                    Bitmap bmp = new Bitmap(ms);

                    BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, bmp.PixelFormat);
                    try
                    {
                        bitmap = new Bitmap(bmp.Width, bmp.Height, bmpData.Stride, bmp.PixelFormat, bmpData.Scan0);
                    }
                    finally
                    {
                        bmp.UnlockBits(bmpData);
                    }

                    if (_bitmapInfo.BitCount <= 8)
                    {
                        bitmap.Palette = bmp.Palette;
                    }
                }

                bitmap.SetResolution(PpmToDpi(_bitmapInfo.XPelsPerMeter), PpmToDpi(_bitmapInfo.YPelsPerMeter));

                return bitmap;

5. 補足

これでグレースケールでスキャンしたものが8ビットBitmapになります(Canon DR-G1130で確認)。それをPNGで保存すると8ビットPNGになります。1ビット、24ビットもそれぞれのビット数になります。
ざっと確認したばかりで、詳細なテストは未実施です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C#で確実にファイル名を変更する

C#でファイル名を変更するにはFile.Move(元ファイルパス,後ファイルパス)を使うのが一番ラクですが、実はこのメソッドには2つ落とし穴があるので、それを回避する方法を説明します。

フォルダ(ディレクトリ)では使えない

Fileなので当たり前といえばそうですが、フォルダかどうかは気にせずファイル名を変更したいですね。
フォルダの場合は代わりにDirectory.Move()を使用します。
そして、指定されたパスがフォルダかどうかはFileInfo.Attributesから調べます。

public static void Rename(string sourceFilePath, string outputFilePath)
{
    var fileInfo = new FileInfo(sourceFilePath);

    if (fileInfo.Attributes.HasFlag(FileAttributes.Directory))
    {     
        Directory.Move(fileInfo.FullName, outputFilePath);
    }
    else
    {
        fileInfo.MoveTo(outputFilePath);
    }
}

フォルダ(ディレクトリ)は大文字小文字だけの変更はできない

たとえば、C:\abcC:\ABCといった変更は直接できません。ですので一度別のファイル名に変更してから再変更します。

public static void RenameDirectory(string sourceFilePath, string outputFilePath)
{
    if ((String.Compare(sourceFilePath, outputFilePath, true) == 0))
    {
        var tempPath = GetSafeTempName(outputFilePath);
        Directory.Move(sourceFilePath, tempPath);
        Directory.Move(tempPath, outputFilePath);
    }
    else
    {
        Directory.Move(sourceFilePath, outputFilePath);
    }
}

private static string GetSafeTempName(string outputFilePath)
{
    outputFilePath += "_";
    while (File.Exists(outputFilePath))
    {
        outputFilePath += "_";
    }
    return outputFilePath;
}

なお、この問題はFile.Move()では発生しません。謎い。

まとめ

以上2つの解決策をまとめると、以下のようになります。
他にもこんな落とし穴がある、とかあったら教えてください。

/// <summary>
/// 確実にファイル/ディレクトリの名前を変更する
/// </summary>
/// <param name="sourceFilePath">変更元ファイルパス</param>
/// <param name="outputFilePath">変更後ファイルパス</param>
public static void Rename(string sourceFilePath, string outputFilePath)
{
    var fileInfo = new FileInfo(sourceFilePath);

    if (fileInfo.Attributes.HasFlag(FileAttributes.Directory))
    {
        RenameDirectory(sourceFilePath, outputFilePath);
    }
    else
    {
        fileInfo.MoveTo(outputFilePath);
    }
}

/// <summary>
/// 確実にディレクトリの名前を変更する
/// </summary>
/// <param name="sourceFilePath">変更元ファイルパス</param>
/// <param name="outputFilePath">変更後ファイルパス</param>
public static void RenameDirectory(string sourceFilePath, string outputFilePath)
{
    //Directory.Moveはなぜか、大文字小文字だけの変更だとエラーする
    //なので、大文字小文字だけの変更の場合は一度別のファイル名に変更する
    if ((String.Compare(sourceFilePath, outputFilePath, true) == 0))
    {
        var tempPath = GetSafeTempName(outputFilePath);

        Directory.Move(sourceFilePath, tempPath);
        Directory.Move(tempPath, outputFilePath);
    }
    else
    {
        Directory.Move(sourceFilePath, outputFilePath);
    }
}

/// <summary>
/// 指定したファイルパスが他のファイルパスとかぶらなくなるまで"_"を足して返す
/// </summary>
private static string GetSafeTempName(string outputFilePath)
{
    outputFilePath += "_";
    while (File.Exists(outputFilePath))
    {
        outputFilePath += "_";
    }
    return outputFilePath;
}

参考

https://dobon.net/vb/dotnet/file/filecopy.html

環境

VisualStudio2019
.NET Core 3.1
C#8

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity(C#)】unity1weekで共同開発に初挑戦した振り返り

unity1week

不定期で行われているunity1weekというゲームジャムに参加しました。

Unityを使って1週間でゲームを作るイベントです。
UnityがインストールされたPCとインターネット環境があればどなたでも参加可能です。
月曜0時にお題が発表されます。日曜20時までにゲームを作って投稿しましょう。
お題については多少こじつけでも大丈夫。ゲーム作りを楽しむことを心がけてください。

【引用リンク】:unityroom

共同開発

私自身参加は6回目なのですが、今回の参加においては共同開発に挑戦しました。
こんな感じのノリで始まりました。
GWunity.PNG

この友人のお仕事は、プログラムを書いてお金をもらうお仕事でもないですし、
理系でコンピューターサイエンスを専攻してたとかでもない、"ただただ興味がある"
という状態だったのでまずは一緒に何か動かしてみることにしました。

Day0

unity1week開催前日にDiscordで通話して画面共有を行いながら
同じ手順を踏んでもらう形でボールをキー入力で動かすというゲーム?を作りました。

Mitsu0.gif

using UnityEngine;

/// <summary>
/// Inspectorで動かしたいSphereにAdd Component
/// </summary>
public class BallMove : MonoBehaviour
{
    //ボールを動かす力を調整
    //[SerializeField]でInspectorに表示
    //[Range(最小値,最大値)]でInspectorにスライダーを表示
    [SerializeField, Range(0, 100)] private float power = 10;

    //Rigidbody:物理挙動に関する設定を簡単に扱える便利Component
    //Rigidbodyの入れ物を用意する
    private Rigidbody rb;

    //最初にStart関数内に書いた処理が実行される
    private void Start()
    {
        //先程用意した入れ物にRigidbodyを入れる

        //この.ゲームオブジェクトから.コンポーネントを取得<Rigidbodyを指定>
        //this.gameObject.GetComponent<Rigidbody>();
        rb = this.gameObject.GetComponent<Rigidbody>();
    }

    //Start関数内に書いた処理が実行されたあと、Update関数内に書いた処理が実行される
    //Updateは毎フレーム呼ばれる
    //1秒間に60回フレームが更新されるなら→60fps
    private void Update()
    {
        //Rigidbodyクラスに定義されているAddForceという関数を使う

        //もし、右矢印キーを押していたら、、、
        if (Input.GetKey(KeyCode.UpArrow))
        {
            //ワールド空間の指定した方向に任意の力を加える
            rb.AddForce(Vector3.forward * power);
        }

        //もし、下矢印キーを押していたら、、、
        if (Input.GetKey(KeyCode.DownArrow))
        {
            //ワールド空間の指定した方向に任意の力を加える
            rb.AddForce(Vector3.back * power);
        }

        //もし、右矢印キーを押していたら、、、
        if (Input.GetKey(KeyCode.RightArrow))
        {
            //ワールド空間の指定した方向に任意の力を加える
            rb.AddForce(Vector3.right * power);
        }

        //もし、左矢印キーを押していたら、、、
        if (Input.GetKey(KeyCode.LeftArrow))
        {
            //ワールド空間の指定した方向に任意の力を加える
            rb.AddForce(Vector3.left * power);
        }

}

友人に教えている最中に
あまりにも書いてあることがわからなさすぎて技術書を噛みちぎろうとした日を思い出しました。
(まあまあの値段がしたので踏みとどまりました)

何もわからない状態からなら、丁寧すぎて逆に理解しづらいかな?という懸念は一旦捨てて良い
と仮説を立てて"くどい"くらいにコメントを書きました。

Day1

unity1weekは、開始と同時にお題が発表され、
そのお題に関連性のあるゲームを開発するというルールがあります。

今回のお題は「密」でした。

Day1はどんなゲームを作るかアイデア出しを行いました。

アイデア出しの進め方としては、「密 熟語」で検索して
アイデアのベースになりそうなワードを列挙しました。

そして、そのワードからイメージを派生し、ゲーム性、実現可能性について検討しました。


★実際のメモ

Day1メモ.PNG

アイデア出しに関してはunity1week online共有会 #1(24:10~)
でお話してるので参考にしてみてください。

実際にできたアイデアのデザインモックがこちらです。
Getting Over It With Bennett Foddy × モンスト
をイメージしたゲーム性です。

キャプチャ.PNG

Day2

下記リンクを参考にUnity Collaborateの環境構築を完了させました。

【参考リンク】:【Unity】Unity Collaborateでリアルタイム通信コンテンツの制作フローを効率化

いきなりのGitはハードルがさすがに高いと思ったので試みましたが、
大正解でした。本当に簡単にプロジェクトの共同編集が可能なので、超おススメです。

Day3

この日からはひたすら実装です。
先ほどのデザインモックの段階でゲーム性の関してはある程度固まっていましたが、
"何を"飛ばすかがまだ決まっていませんでした。

キャプチャ.PNG

ただ、引っ付いた際、発射する際のアニメーションを自分で作るのが面倒だったので
手足の無い無機質な生命体の方が楽できそうだなと
実装可能性の観点からこの時点でアイデアは絞れていました。

手足の無い無機質な生命体で思いついたのはスライムです。
壁に密着するというアイデアにシナジーも生まれます。

スライムでアセットストアで検索した際に欲しかった形の
無機質な生命体を発見したので採用しました。
SlimePBR.PNG

ただ、スライムの引っ付いたり弾んだりする質感を実装で再現するのが面倒だったので
ここでもう一段階、実装を楽にするための設定を考えました。

ズバリ、磁力です。

この設定の付与で実装のイメージが一気に固まりました。

この日はキャラクターを"飛ばす"、それに伴う"補助線"の実装で終了しました。

Mitsu1.gif

Mitsu2.gif

Day4

磁力の実装に着手しました。

Mitsu3.gif

磁力の実装には万有引力の公式を利用しています。


F[N]:質量m₁[kg]と質量m₂[kg]の2つの物体の間に働く万有引力の大きさ
r[m]:物体間の距離
G:万有引力定数(G=6.673×10-11N・m2/kg2) ※今回はここを係数にして力を調整可能に

F = G\frac{m₁m₂}{r^2}

ただ、このまま利用するとm₁とm₂の物体間距離が遠くなればなるほど
力が弱くなる
という性質になり、なかなか気持ち良い挙動にはなりませんでした。

なので、m₁とm₂の物体距離が遠くなればなるほど力が強くなる式に変更して利用してみました。

F = Gm₁m₂r^2

望み通りの挙動になったのですが、
質量は今回関与する必要はないので下記が最終的な計算式です。

F = Gr^2

私がほしかったのはただの指数関数だったわけですね。

コード

磁力として扱うオブジェクトにアタッチ
/// <summary>
/// 磁力の表現 万有引力の計算式を利用
/// S,Nでそれぞれベクトルが逆になる
/// 磁力として扱うオブジェクトにアタッチ
/// </summary>
public class MagnetFunction : MonoBehaviour
{
    private enum MagnetState
    {
        S,
        N
    }

    [SerializeField] private float _accelerationScale;
    [SerializeField] private MagnetState _magnetState;

    private Rigidbody _collisionObjRigidbody;
    private Vector3 _direction;

    private float _distance;
    private float _magnetPower;

    private void OnTriggerEnter(Collider other)
    {
        //Start関数内で名指しで取得する実装でも良いかも
        _collisionObjRigidbody = other.gameObject.transform.root.GetComponent<Rigidbody>();
    }

    private void OnTriggerStay(Collider other)
    {
        //衝突したキャラがSかNの判定を入れた方がよさそう
            switch (_magnetState)
            {
                case MagnetState.S:
                    // 星に向かう向きの取得
                    _direction = this.gameObject.transform.position - other.transform.position;
                    // 星までの距離の2乗を取得
                    _distance = _direction.magnitude;
                    _distance *= _distance;
                    // 万有引力計算
                    _magnetPower = _accelerationScale * _distance;
                    // 力を与える
                    _collisionObjRigidbody.AddForce(_magnetPower * _direction.normalized, ForceMode.Force);
                    break;

                case MagnetState.N:
                    // 星に向かう向きの取得
                    _direction =  other.transform.position - this.gameObject.transform.position;
                    // 星までの距離の2乗を取得
                    _distance = _direction.magnitude;
                    _distance *= _distance;
                    // 万有引力計算
                    _magnetPower = _accelerationScale * _distance;
                    // 力を与える
                    _collisionObjRigidbody.AddForce(_magnetPower * _direction.normalized, ForceMode.Force);
                    break;
            }
    }
}

InspectorでS極N極それぞれの役割を変更できるようにしました。
S,Nそれぞれの機能でクラスを用意しても良いかと思います。

磁力としてBox Colliderのみを保持するオブジェクトを用意し、
その子に磁石として扱うオブジェクトを配置します。

磁力のコライダーの範囲内に留まっている間は
磁力が働く仕組みです。

NCube.PNG

Day5

友人から稼働時間を確保できると連絡がきたので
タイトルシーンを作ってもらうことにしました。

簡単なデザインモックを渡した後、
適当に調べてもらって分からないことがあれば
逐一、Slackで聞いてもらう形を取りました。

TitleMock.PNG

すぐ作ってくれたので助かりました。
友人もUnityもすごい。
MitsuMasuo1.gif

私は 磁力の調整、および発射パワー調整を行いました。

Day6

下記を頑張りました。

・ステージの実装(チュートリアルっぽいやつ)
・音探して編集して入れたり
・パワーの入力タイミングに制限を加えた
・パワー入力タイミングに関して画面に出す画像を用意してプレイヤーに伝えられるようにした
・フェードインアウト実装

Mitsu5.gif

フェードインアウト

DoTween(無料版)を使って実装しました。

実際に使ったコードはもっと乱雑でしたが、
次回からはこのようなUtilityを用意しておいて最小限の労力で臨もうかと思ってます。

using DG.Tweening;
using UnityEngine;
using UnityEngine.EventSystems;

/// <summary>
/// 任意のUIを表示、非表示に関するユーティリティークラス
/// アニメーション中はUIいじれない
/// </summary>
public static class UIAppearTweenAnimeUtility
{
    /// <summary>
    /// UI表示
    /// </summary>
    /// <returns>シーケンス</returns>
    public static Sequence AppearUI(RectTransform uiRectTransform)
    {
        EventSystem eventSystem = EventSystem.current;

        float appearDuration = 0.5f;

        return DOTween.Sequence()
            .OnStart(()=>eventSystem.enabled = false)
            .Append(uiRectTransform.DOScale(Vector3.one, appearDuration))
            .OnComplete(()=>eventSystem.enabled = true);
    }

    /// <summary>
    /// UI非表示
    /// </summary>
    /// <returns>シーケンス</returns>
    public static Sequence DisappearUI(RectTransform uiRectTransform)
    {
        EventSystem eventSystem = EventSystem.current;

        float disappearDuration = 0.5f;

        return DOTween.Sequence()
            .OnStart(()=>eventSystem.enabled = false)
            .Append(uiRectTransform.DOScale(Vector3.zero, disappearDuration))
            .OnComplete(()=>eventSystem.enabled = true);
    }
}

ポイントとしては
・メソッドがSequence(シーケンス)を返す
・EventSystemのenableを操作するヤンキーコング1

の二点です。

メソッドがSequenceを返す

DoTweenのSequenceはいろいろな処理を任意の順番、タイミングで実行することができます。
また、SequenceとSequenceを繋いで実行することもできます。

呼び出し側でUIアニメーションの処理に追加して
新しい処理を行わせることが可能になります。

UIのアニメーション処理の終了後に音を再生
AppearUI(uiRectTransform)
    .AppendCallback(()=>_fadeOpenAudioSource.Play())
    .Play();

アセットのインポート直後の初期設定ではSequenceが自動で実行される設定になっているかと思うので、
ツールバーから設定を変更しておくとScriptから実行できます。
DoTweenPanel.png

TweenAutoPlay.PNG

EventSystemのenableを操作するヤンキーコング

UIアニメーションの処理を行っている間は入力を受け付けないようにしたかったので
EventSystemごとenableをオフにしています。
そのため、一時的にすべてのEventSystem関連の入力が受け取れない状態になります。
(Unityゲーム開発者ギルドで教えてくださった方ありがとうございます)

EventSystem eventSystem = EventSystem.current;
eventSystem.enabled = true

Day7

結果的に私は毎度のことのように〆切に間に合わなかったのですが、
この日はリトライ処理の実装やステージ解放の実装を行っていました。

【参考リンク】:【Unity(C#)】覚えゲーのリトライポイント実装
【参考リンク】:【Unity(C#)】PlayerPrefsを使用したステージ解放の実装

Mitsu6.gif

友人にはステージを作ってもらっていました。
ゲーム性を与えるセンス抜群&ツールを使いこなす速度がバケモノで助かりました。
MMGameSS.PNG

振り返り

評価はなかなかシビアな感じでした。
MMReview.PNG

操作性

特に操作性の部分ではGetting Over It With Bennett Foddyを意識した難易度設計から、
難しい→操作しづらい という
印象の変換が無意識下で行われてしまったのではないか、という分析をしています。

Getting Over It With Bennett Foddyは確実に名作ですが、
操作をしやすいか質問して、"はい"という回答は得られにくい
かと思います。

Getting Over It With Bennett Foddyは
操作しづらいことで大きなゲーム性が生まれています。

つまり何が言いたいかというと、
評価項目の何かしらが欠損していることによってゲーム性が拡張されるゲーム
unity1weekで上位を狙いにくいということです。

文字起こししてみると当たり前ですが、
意外と見落としがちな要素かと思います。

操作性を振り返っての結論としては、
unity1weekで上位に食い込めなかったからといって
おもしろくないゲームであるという解釈にはならない
ということです。(暴論)

最後に

自分への慰めも含めて声を大にして言いますが、
みなさん胸を張って自分の作ったゲームに誇りを持ちましょう。

あと、運営、開発、実況等、本当にみなさんお疲れさまでした。
合わせて、お礼の言葉で締めさせていただきます。ありがとうございました。

(技術的な誤り、誤植等ありましたらお気軽にコメントください)

参考リンク

[Unity] 惑星に向かって物体が落ちるようにする
Unity で EventSystem.current が null になってる問題
[Unity] DOTweenのSequenceを使ってアニメーションを結合する

【作ったゲーム】:MAGNET MONSTER


  1. ヤンキーコングはヤンキーコーディングを略した造語です。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む