忍者ブログ

Memeplexes

プログラミング、3DCGとその他いろいろについて

[PR]

×

[PR]上記の広告は3ヶ月以上新規記事投稿のないブログに表示されています。新しい記事を書く事で広告が消えます。


かんたんXNA その18 BasicEffectで平行光

このページは古いです
最新版はこちら

BasicEffect.EnableDefaultLightingメソッドを使うと
簡単なライティングを行うことが出来ますが、
BasicEffect.DirectionalLight0プロパティを使うと、
もう少し詳しいライティングの設定が出来ます。
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;

class MyGame : Game
{
    GraphicsDeviceManager graphics;
    BasicEffect basicEffect;

    VertexPositionNormalTexture[] vertices;

    ContentManager content;
    Texture2D texture;

    public MyGame()
    {
        graphics = new GraphicsDeviceManager(this);
        content = new ContentManager(Services);

        vertices  = new VertexPositionNormalTexture[3];
        vertices[0].Position = new Vector3(0, 1, 0);
        vertices[0].Normal = new Vector3(0, 0, 1);
        vertices[0].TextureCoordinate = new Vector2(0.5f, 0);

        vertices[1].Position = new Vector3(1, 0, 0);
        vertices[1].Normal = new Vector3(0, 0, 1);
        vertices[1].TextureCoordinate = new Vector2(1, 1);

        vertices[2].Position = new Vector3(-1, 0, 0);
        vertices[2].Normal = new Vector3(0, 0, 1);
        vertices[2].TextureCoordinate = new Vector2(0, 1);
        
    }

    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        if (loadAllContent)
        {
            basicEffect = new BasicEffect(graphics.GraphicsDevice, null);
            basicEffect.View = Matrix.CreateLookAt(
                new Vector3(0, 0, 3),
                new Vector3(0, 0, 0),
                new Vector3(0, 1, 0)
                );
            basicEffect.Projection = Matrix.CreatePerspectiveFieldOfView(
                MathHelper.ToRadians(45), 
                Window.ClientBounds.Width/(float)Window.ClientBounds.Height,
                1, 100
                );


            texture = content.Load<Texture2D>("FlyingSpaghettiMonster");
        }
    }

    protected override void UnloadGraphicsContent(bool unloadAllContent)
    {
        if (unloadAllContent) { content.Unload(); }
    }

    protected override void Draw(GameTime gameTime)
    {
        graphics.GraphicsDevice.Clear(Color.CornflowerBlue);

        graphics.GraphicsDevice.VertexDeclaration = new VertexDeclaration(
            graphics.GraphicsDevice,
            VertexPositionNormalTexture.VertexElements
            );

        basicEffect.TextureEnabled = true;
        basicEffect.Texture = texture;

        basicEffect.LightingEnabled = true;
        basicEffect.DirectionalLight0.Enabled = true;
        basicEffect.DirectionalLight0.Direction = new Vector3(1, 0, -1);
        basicEffect.DirectionalLight0.DiffuseColor = new Vector3(0.5f, 0.5f, 0.5f);
        basicEffect.DirectionalLight0.SpecularColor = Color.White.ToVector3();

        basicEffect.Begin();

        foreach (EffectPass pass in basicEffect.CurrentTechnique.Passes)
        {
            pass.Begin();

            graphics.GraphicsDevice.DrawUserPrimitives<VertexPositionNormalTexture>(
                PrimitiveType.TriangleList,
                vertices,
                0,
                vertices.Length / 3
                );

            pass.End();
        }

        basicEffect.End();
    }
}

directionalLight1.JPG
このサンプルプログラムでは、斜め45度の角度から三角形に光を照射しています。
左が明るく、右が暗くなっています。
ここでは、太陽光のように光源が十分に遠くにある、光をシミュレートしています。
(そのため、指定するのは光の方向だけで、光源の位置は指定しません
「十分に遠くにある」という情報だけで十分なのです。
十分に遠くにあれば光の方向は大体どこでも同じだからです。
もし光源が近くにあるのなら、
光の方向はちょっと動いただけでも変わりますが、
遠くにあるのならほとんど変わりません。
ちょうど車で移動しているときに月がどこまでも
追いかけてくるように見えるのと同じ理由です。)


まず、BasicEffectで(手作業で)ライティングを行いたいときには、
BasicEffect.LightingEnabledプロパティをtrueにセットします。
(EnableDefaultLightingメソッドを使うときにはこの作業がいらなかったのは、
多分メソッドの内部でこのプロパティをtrueにセットしてくれているからでしょう。)


public bool LightingEnabled { get; set; }

これをtrueにセットしないとライティングが考慮されなくなって
普通に描画されてしまいます。(影がつきません)
xnaSimplextTriangleTextured.JPG

そして、無限遠から来る光、平行光を使うには
BasicEffect.DirectionalLight0プロパティを使います。

public BasicDirectionalLight DirectionalLight0 { get; }

一番最後に"0"がついているのは、平行光を複数使いたいときのために、
まだまだ他にも用意されているからです。
(ここで使っているのは最初の0だけですが)

public BasicDirectionalLight DirectionalLight1 { get; }
public BasicDirectionalLight DirectionalLight2 { get; }


こういうのは本当は配列にすべきなのですが、そうなっていないのは
BasicEffectが実はきたならしいHLSLのラッパー
であることと関係があるのでしょう。

BasicDirectionalLightを使うにはまずEnabledプロパティをtrueにセットします。

public bool Enabled { get; set; }

これをtrueにしないと、そもそも光が当たりません。
真っ黒になります。
(BasicEffect.LightingEnabledとは違います。)

directionalLightWithoutEnabled.JPG

その後、BasicDirectionalLight.Directionプロパティで光の方向を決めます。

public Vector3 Direction { get; set; }

光の色を決めるのがBasicDirectionalLight.DiffuseColorプロパティと
BasicDirectionalLight.SpecularColorプロパティです。
(この2つは色を表すプロパティですが、
型がColorではなくVector3になっています。
これはBasicEffectが実はきたならしいHLSLのラッパーで
あることと関係があるのでしょう。)


public Vector3 DiffuseColor { get; set; }
public Vector3 SpecularColor { get; set; }


どうして光の色を決めるプロパティが2つもあるのかというと、
この2つは性質が違うからです。
DiffuseColorは乱反射による色ですが、
SpecularColorは鏡面反射による色です。
この2つを合成した色が実際には表示されます。

この違いをはっきりさせるには、片方だけセットしてみるといいでしょう。

乱反射(Diffuse)

まず、DiffuseColor(乱反射)だけセットするとこうなります。
directionalLightDiffuse.JPG

全体的に暗めでのっぺりしています。
どこも同じような明るさです。
この明るさは面に当たる光の角度に依存しています。

diffuse90.JPG90度(三角形に平行:横からの光)diffuse75.JPG75度
diffuse60.JPG60度diffuse30.JPG30度
diffuse15.JPG15度diffuse0.JPG0度(三角形に垂直:まっすぐな光)

面に垂直に光が当たるようになるほど明るくなるのがわかると思います。
これは赤道直下が暑くて北極や南極が寒くなる理由と同じです。(赤道は緯度0度で、北極点は北緯90度です。)

鏡面反射(Specular)

次に、SpecularColor(鏡面反射)だけセットするとこうなります。

directionalLightSpecular.JPG

左側がぼんやりと明るく、右側は暗くなっています。
この明るくなっている部分は鏡に映った太陽とでも思えばいいでしょう。

実際、三角形を動かしていくとこうなります。
directionalLightSpecularMoved3.JPGdirectionalLightSpecularMoved2.JPGdirectionalLightSpecularMoved1.JPG


directionalLightSpecularMoved0.5f.JPGdirectionalLightSpecular.JPG

光の方向と面とカメラの位置によって明るさが変わります。
カメラの位置によって明るさが変わるというのは乱反射ではありえないことです。
こういうのは鏡面反射でのみおこります。
ちょうどメガネがキラリと光るようなものです。

最終的には、この2つの効果が組み合わさります。
そうすることによって、そこそこ自然な描画が出来るのです。
環境光(Ambient Color)

このサンプルでは使いませんでしたが、環境光というものもあります。
これは、光がどのような角度で当たっても、
カメラがどのような方向から見ても、
同じような明るさになります。

どういういうことかというと、環境光とは周囲を反射しまくった微妙な光のことなのですが、
それをわざわざ計算してシミュレートするのはとても莫大な計算が必要になるため、
とりあえず「どんなときにも当たる光」ということにしているのです。

これを表すプロパティは、BasicEffect.AmbientColorです。

public Vector3 AmbientLightColor { get; set; }

拍手[0回]

PR

"Orcas"とC# 3.0 その3 暗黙的に型付けされたローカル変数

C# 3.0ではローカル変数の宣言時に型を指定しなくて良いのだそうです。
class Program
{
    static void Main()
    {
        var id = 1;  //Int32
        System.Console.WriteLine(id);  //1
    }
}

これは、コンパイラが初期化の仕方から自動的に型を推定してくれるからで、
推定しようがない場合はコンパイルエラーになります。
(別に動的型付け言語になったわけではありません)

Compile Error!
class Program
{
    static void Main()
    {
        var id;
        id = 1;
        System.Console.WriteLine(id);
    }
}

これによって何が良くなったかと言うと、1つには多分変更のしやすさでしょう。
コードコンプリートにあったように、IDを数から文字列に変更したくなった場合、
これだと変更するのは一ヶ所で済みます。
(この例よりも複雑な場合でも、少なくとも変更すべき場所が減るはずです)

class Program
{
    static void Main()
    {
        var id = "1";  //String
        System.Console.WriteLine(id);  //1
    }
}

もし明示的に型を指定してしまえば、その部分もintから
stringへ変更しなければなりません。
暗黙的に型付けしてしまえばその必要はないということです。
より変更しやすくなった、アジャイル向けな機能といったところでしょうか。

もう少し重要な理由としてC# 3.0の匿名型が関係しているのでしょう。
using System;

class Program
{
    static void Main()
    {
        var book = new { 
           Name = "利己的な遺伝子",
           Price = 2940 };
        Console.WriteLine("{0} ¥{1}", book.Name, book.Price);
    }
}

驚くべきことに型を即興で作ることが出来ます。
そしてその型には名前がありません。
(もちろん共通中間言語レベルではあるようですが)

このようなことをすると、varが必要不可欠になります。
型の名前がわからないので指定しようが無いからです。

この匿名型はLinqで結果を手っ取り早くまとめるのに使われるようです。

拍手[0回]


"Orcas"とC# 3.0 その2 拡張メソッド

C# 3.0 には拡張メソッドなるものがあるそうで、
あるクラスのメソッドを後から増やせるそうです。
using System;

class Person { public int Age { get; set; } }

static class PersonExtension
{
    public static bool IsAdult(this Person person) {
        return person.Age >= 20;
    }
}

class Program
{
    static void Main(string[] args)
    {
        Person child = new Person { Age = 10 };
        Person father = new Person { Age = 35 };
        Console.WriteLine(child.IsAdult()); //False
        Console.WriteLine(father.IsAdult()); //True 
    }
}

一見したところこれはクラスの凝集性を破壊するだけ
のように思えます。
別々に書く意味は全くありません。
PersonクラスにIsAdultメソッドも書くべきです。
実際、公式でもあまり軽々しく使うなと言われているようです。

ではなぜ拡張メソッドが存在するかと言うと、
1つにはインターフェースが実装しやすくなる
と言うのがあるようです。

using System;
using System.IO;

interface IPerson { int Age { get; set; } }
class Person : IPerson { public int Age { get; set; } }
class PersonFile : IPerson
{
    public int Age
    {
        get { return Int32.Parse(File.ReadAllText("person.txt")); }
        set { File.WriteAllText("person.txt", value.ToString()); }
    }
}

static class PersonExtension
{
    public static bool IsAdult(this IPerson person)
    {
        return person.Age >= 20;
    }
}

class Program
{
    static void Main(string[] args)
    {
        Person child = new Person { Age = 10 };
        PersonFile father = new PersonFile { Age = 35 };
        Console.WriteLine(child.IsAdult());
        Console.WriteLine(father.IsAdult());
    }
}
とりあえずPersonクラスに似た、PersonFileクラスを作りました。
メモリではなくファイルに年齢を格納します。
(ここは本当はファイルではなくネットワークに接続する
Proxyパターンにするべきなのでしょうけど長くなると困りますからね。)

もしPersonのインターフェースを作った場合、
それを使ってIsAdult()を呼べるべきです。

しかしそうすると、もし拡張メソッドを使わなかったら、
全ての実装したクラスでも
Age >= 20;
とやらなければなりません。

新たにクラスを1つ作るたびに重複が多くなってしまいます。

拡張メソッドを使えばその問題が解決されます。
Age >= 20;
は一ヶ所、拡張メソッドの中に集中します。
すばらしい。

実際の.netライブラリではオブジェクトのコレクション(?)
を表すIEnumerableインターフェースが拡張されています。

IEnumerableはオブジェクトを列挙する能力しかありませんが、
System.Linq.Enumerableによってさまざまな便利な能力を
付与されています。

拍手[1回]


"Orcas"とC# 3.0 その1 自動実装プロパティ

遅ればせながら次期Visual Studio、
"Orcas" Express Editionをインストールしてあそんでみました。

インストールしてさっそく起動してみると、
・・・特に2005と変わらないように見えます。
orcasStartPage.JPG
しいて違うところをあげるならタブの色がよりLunaっぽく、
かっこよくなっているということくらいでしょうか。

とりあえずプロジェクトを作って何かやってみることにしました。
orcasNewProject.JPG
当たり前ですが作れるプロジェクトの種類が2005より少ないようですね。
ともかく、一番単純な、コンソールプログラムで遊ぶことにしました。

今回の目玉はLinqらしいので
おいしいものは最後に取っておくと言う意味で
まずは地味な機能を使うことにします。
なんでもC# 3.0ではプロパティの宣言が
簡単になったとの事ですので。(自動プロパティ)

using System; struct Vector2 { public float X { get; set; } public float Y { get; set; } } class Program { static void Main(string[] args) { Vector2 vector = new Vector2(); vector.X = 0; vector.Y = 1; Console.WriteLine("x = {0}, y = {1}", vector.X, vector.Y); } }

結果はこうです:

x = 0, y = 1

Rubyのように、プロパティを宣言するだけで、
変数の宣言の必要は無いらしいです。
それにしてもこのプロパティの書き方はインターフェースで
プロパティを宣言するやり方に似ています。(伏線)

次にコンストラクタを書いてみました。
わざわざ2行使ってプロパティに値を代入するのは
面倒に思えたからです。
しかし、失敗しました

Compile Error!

using System; struct Vector2 { public float X { get; set; } public float Y { get; set; } public Vector2(float x, float y) { this.X = x; this.Y = y; } } class Program { static void Main(string[] args) { Vector2 vector = new Vector2(0, 1); Console.WriteLine("x = {0}, y = {1}", vector.X, vector.Y); } }
"The 'this' object cannot be used before all of its fields are assigned to"
とでました。他にもそれに関連したエラーが2つ。
どうやらコンストラクタ中でプロパティに代入してはいけないようです。

「だったら一体どうやって初期化するんだ」という気になります。
調べてみると、オブジェクトを初期化する特別な方法も出たそうで、
それを使えばよさそうです。
using System; struct Vector2 { public float X { get; set; } public float Y { get; set; } } class Program { static void Main(string[] args) { Vector2 vector = new Vector2 { X = 0, Y = 1 }; Console.WriteLine("x = {0}, y = {1}", vector.X, vector.Y); } }
すばらしい・・・。

ものすごく簡潔です。
引数をとるコンストラクタを書く必要がありません。

もしかするとWindows Presentation Foundation(WPF)で
引数をコンストラクタにもつクラスが少なかったのは
これへの布石だったのかもしれません。
単に怠慢でコンストラクタを用意しなかったのではなく、
必要なかったから用意しなかっただけなのかもしれませんね。
引数(?)の意味もより明確になっています。

なんとなくCωの初期化の仕方を思い出します。

次に気になるのは「読み取り専用のプロパティを作れるのかどうか」
ということです。

そこで確かめるために次のようなコードを書いてみましたが、
失敗です。

Compile Error!
using System; struct Vector2 { public float X { get; } public float Y { get; } public static Vector2 Create(float x, float y) { return new Vector2 { X = x, Y = y }; } } class Program { static void Main(string[] args) { Vector2 vector = Vector2.Create(0, 1); Console.WriteLine("x = {0}, y = {1}", vector.X, vector.Y); } }
"'Vector2.X.get' must declare a body because it is not marked abstract or extern.
 Automatically implemented properties must define both get and set accessors."

・・・だそうです。どうもabstractやexternのプロパティと勘違いされたようです。
実際書き方は同じですし。
と言うか書き方が同じと言うより意味的に同じなのかもしれません。
エラーメッセージによると、自動プロパティは
自動的に実装される」と言う意味らしいからです。

ネーミングの由来は納得しましたが
だからと言って問題が解決したわけではありません。
やはり調べてみると、盲点でした、setの前にprivateをつけるらしいです。
using System; struct Vector2 { public float X { get; private set; } public float Y { get; private set; } public static Vector2 Create(float x, float y) { return new Vector2 { X = x, Y = y }; } } class Program { static void Main(string[] args) { Vector2 vector = Vector2.Create(0, 1); Console.WriteLine("x = {0}, y = {1}", vector.X, vector.Y); } }
この方法はプロパティを手動で実装するときと同じですが、
その場合はたいていprotected set...となります。
まさかprivate setになるとは・・・完全に盲点でした。

しかしおかしな話です。
上のブログでも言われていますが、private setくらい
自動的に補完して欲しいものです。
setできないのならプロパティに意味なんて無いわけですから。
(そしてこの疑問に対する回答もやはり要領を得ません。)

将来は改善されるといいのですが・・・

拍手[0回]


3D酔いの原因

3D酔いとは?

3Dゲームをしていると乗り物酔いのように気分が悪くなり吐き気を催すことがあります。
これを3D酔いといいます。

例えばマイクロソフトのシューティングゲーム、Halo(体験版はこちら)は
面白いゲームですが3D酔いでその面白さが半分台無しになっています。

ワートホグ(車のこと)に乗れば5分も経たずに気分が悪くなってきます。
視点が揺さぶられるわカメラが障害物をすいすい(この「すいすい」がいけません!)避けるわで
すぐにゲームを続けたい気持ちとさっさとトイレに
駆け込みたくなる気持ちの間でアンビバレンツに陥ります。
(まぁしばらくすると落ち着くのですが)
3D酔いの原因

この3D酔いの詳しい原因はよくわかっていませんが、
大体の原因は目から入る情報と三半規管から入る情報のミスマッチであることは間違いないようです。
(こんなことがわかっても3D酔い対策はほとんど出来ないのであまり意味の無い話です。)

つまり、目からは「自分は動いている」と言う情報が来るのに
三半規管からは「自分は止まっている」と言う情報が来るので
脳が混乱して気分が悪くなったり、吐き気がするというわけです。

これはちょうど乗り物酔いの逆で、乗り物酔いの場合は
目からは「自分は止まっている」という情報が来るのに
三半規管からは「自分は動いている」と言う情報が来るわけです。
(ですから乗り物酔いになったときは外の風景を眺めてこの情報の食い違いを解消するのです。)
しかしこれは「情報のミスマッチ」が酔いを引き起こすと言う点で同じです。

また似たような症状として「宇宙酔い」があります。
宇宙酔いというのは宇宙の無重力空間でやはり気分が悪くなり吐き気を催してしまう症状のことで、
現在スペースシャトルの乗組員のだいたい60%が最初のフライトで宇宙酔いに悩まされます。
この原因はおそらく重力が打ち消されているからで、
実際宇宙飛行士が特に吐き気に襲われるのは地上の、
重力のある環境ではありえないものを見たときです。
(例えば同僚の宇宙飛行士が頭をにしていたりとか)
これもやはり目と三半規管のミスマッチが引き起こすものです。

このことから考えて、乗り物酔いにしろ3D酔いにしろ宇宙酔いにしろ全て
目と三半規管から脳に入ってくる情報のミスマッチ」が原因であると考えてよさそうです。



ダーウィン主義的理由

情報のミスマッチがなぜ吐き気をもたらすのでしょう?

一般には単に「それは脳が混乱するからだ」と言われますが、
オックスフォード大学の心理学者のマイケル・トリーズマンは
驚くべき、しかし説得力を持った別の説を提唱しています。

その説によると、「脳はわざと気分が悪くなり吐こうとしている」のです。
一見ありえなさそうに思えますがそうでもありません。
風邪を引いたときに高い熱が出るのは体内の病原体を殺すためです。
同様に、乗り物酔いになり、吐き気がするのにもきちんとした理由があるという理屈です。

この説は次のように進みます。
動物が何かを吐くのは、毒があるものを食べてしまったときに体からそれを排除するためです。
吐き気と言うのは、毒を食べてしまったときにその個体を操って毒を排除するためのコマンドだと言うわけです。
・・・そして毒と言うのは、たいてい神経に働き、神経系を狂わせます。
ということは逆に言って、そのコマンドを発動すべきタイミングは
神経系が何かおかしくなったときに違いありません。

しかし、「何かおかしくなったとき」とは具体的にどう定義すればいいのでしょう?
自然淘汰は基本的にXPでいう仮実装しかしないやっつけプログラマーなのでこう「考え」ます:
「脳のいろんな部分の重力に関する意見を聞けばいい。
重力が無くなったり変わったりすることなんてありえないからいい指標になる。
それでどいつかの意見が食い違っていたらこれはきっと神経系がやられてる。」
よし!これでとりあえず動くでしょう。

かくして、脳の内部に存在している重力(というか加速度)をあつかう
モジュールのいずれかの間で情報の食い違いが発生すると
神経系が毒で狂っていると認識し、「吐き気コマンド」を発行することになりました。

さて、そのような生存機械がもし車の後部座席に乗って本を読んだり、
Haloをプレイしたり、スペースシャトルに乗って無重量状態を体験したらどうなるでしょう?

これがこの説のエッセンスです。
酔いというのは、限られた環境では上手く動く防衛機能が暴走するすることによって起こる、
花粉症のようなものだったのです。

教訓

このことから何が言えるでしょうか。
具体的には、3D酔いを防ぐために何が出来るでしょう?

理論上、神経系がおかしくなったと認識されなければ3D酔いにはならないはずです。
しかしそうするには目から入る情報と三半規管から入る情報を一致させなければなりません。
(いや、目と三半規管に限定するのは危険かもしれません。
脳の中でミスマッチを生むものならなんだっていいはずです)


かといって完全に一致させることなど不可能です。
ゲームをプレイする人は普通いすの上に座って静止しています。
画面が動けば即アウトです。
そんなゲームはちっとも楽しくありません。

もっとも実際にはそんなに厳しくなくて、ある程度動きは許容されるはずです。
というのも自然状態では脳のモジュール間のある程度の誤差はつきものだからです。
少し違ったくらいで毎回吐いていたら体力が持ちません。逆効果です。
おそらく誤差がある閾値をこえて初めて「吐き気コマンド」が発動するのでしょう。

ということはゲームの画面をある程度「止まっている」ように見せればいいはずです。
ある程度、画面を非現実的にして、脳がその情報をあまり信じないようにすればいいかもしれません!
ゲーム画面の視野の角度を小さくして臨場感をあえて抑えると言うのが手です。
いやでも角度が小さすぎてもそれが何らかのミスマッチを生み出し逆効果になるかもしれませんね・・・。
(そして、理論を弄って出る結論としてよくあることですが、こんなことは最初からわかっています!)

カメラをごちゃごちゃ動かさないというのもいいかもしれません。
あんまり動かしすぎると脳内のミスマッチが大きくなる可能性があります。

あるいは逆にカメラワークを上手くすることによって
脳が画面からの情報を重視しないように出来るかもしれません。
もちろんその条件はわかりませんし、第一こんなことが出来るかどうかもわかりませんが。

ソフトウェアからできるのはこのくらいが限界でしょう。
しかしハードウェアを使ってもいいのなら、動く椅子なり
三半規管に電極をつないで動いているような信号を出力させるなり
方法はいくらでもあるはずです。

つまり結論は、「人類はさっさと電脳を実用化させて3D酔いを克服すべきだ」と言うことですね!


 


拍手[0回]