Hatena::ブログ(Diary)

連科学ぷろぽおざる

2012-02-20

[]よく役に立つ滑らかな関数

メモ書き.

template<typename T>
inline T sigmoid( T x )
{
	return 1 / (1 + std::exp(-x));
}

template<typename T>
inline T softmax( T a, T b )
{
	using namespace std;
	return log( exp( a ) + exp( b ) );
}

template<typename T>
inline T softmin( T a, T b )
{
	using namespace std;
	return -log( exp( -a ) + exp( -b ) );
}

template<typename T>
inline T softclamp( T val, T min, T max )
{
	using namespace std;
	return log( exp( max ) + exp( -log( exp( -min ) + exp( -val ) ) ));
}

// ------------------------------------ eases
template<typename T>
inline T cos_curve( T val )
{
	return 0.5 * std::cos( val * PI ) * 0.5;
}

template<typename T>
inline T poly_ease( T val )
{
	const T val_pow2 = val*val;
	const T val_pow3 = val*val_pow2;
	return 3*val_pow2 - 2*val_pow3;
}

template<typename T>
inline T exp_ease( T val )
{
	return 1.0 - std::exp( -3.0 * val );
}

template<typename T>
inline T invpow_ease( T val )
{
	T inv = 1-val;
	return 1 - inv*inv*inv;
}

とってもなめらか.

2012-02-07

[][][]フラグメントシェーダでレーザ(ビーム?)

粒子感のあるプロシージャルなレーザをフラグメントシェーダで作ってみた.

http://shiracha.net/local/laser.zip

http://shiracha.net/local/laser2.zip

OpenGL3.3要求.見れない方は

http://shiracha.net/graphic/laser.png

http://shiracha.net/graphic/laser2.png

http://shiracha.net/graphic/laser3.png

パーティクルも飛ばしたいなー.

2012-01-22

[][]friendとstd::make_sharedの相性の悪さ

普通のC++クラス

普通,C++のクラスというのは次のサイトにあるように作るようです.

http://dev.activebasic.com/egtra/2011/12/02/451/

今回取り上げたい話では,データ抽象以外は一般にnoncopyableだということが効いてきます.

Factoryクラス

インスタンスを作成するときに一般の手順をまとめて,コード上にそのコンセプトを表現しておきたいことがあります.

このような手順をまとめるデザインパターンFactoryといいます.(GoFデザインパターンではFactoryの具体的なものにいくつか種類がありますが,ここではそれらを総称してFactoryと呼びます.)

たとえばPC向けゲーム作りでよく利用されるDirect3Dライブラリでは,IDirect3DDevice9のインスタンスを生成するコードが必要になりますが,

このIDirect3DDevice9はグラフィックデバイスの能力に合わせてさまざまなバリエーションがあり,もっともアプリケーションに適切なものを生成する必要があります.

そのため,能力の調査と必要な機能の列挙,アプリケーションの要求の定義と機能別でのフィルタデバイスの生成という手順が必要になり,しかもそれらの挙動はアプリケーションごとにカスタマイズが必要になります.

しかしこれだけの操作をコンストラクタの中で行うには巨大すぎると考えられます.

またコンストラクタというたった一つの関数でカスタマイズの効くコードを提供するのは難しいことです.

このような場合はFactoryを利用することですっきりとした設計を行うことが出来ます.

例1.GraphicDeviceFactory.h

class GraphicDeviceFactory : public boost::noncopyable
{
public:
	GraphicDevice CreateDevice();

private:
	static bool CreateD3DDevice(
		IDirect3D9* d3d,
		HWND windowHandle,
		UINT adapterNo,
		DWORD deviceCreateFlag,
		D3DPRESENT_PARAMETERS presentParam,
		IDirect3DDevice9** device
	);

	virtual HWND GetWindowHandle() const = 0;
	virtual AdapterCapacity GetTargetAdapter(const std::vector<const AdapterCapacity>& adapterCapacities) const = 0;
	virtual DeviceCreateFlag::Type GetDeviceCreateFlag() const = 0;
	virtual D3DDEVTYPE GetDeviceType() const = 0;
	virtual D3DPRESENT_PARAMETERS GetPresentParameter(const AdapterCapacity& adapter) const = 0;
};

例2.GraphicDeviceFactory.cpp

bool GraphicDeviceFactory::CreateD3DDevice // 省略

// この戻り値はとりあえず仮にXXXXXにしてあるが,戻り値は数パターン考えられる.
XXXXX GraphicDeviceFactory::CreateDevice()
{
	IDirect3D9* d3d = Direct3DCreate9(D3D_SDK_VERSION);

	// ※この行は本当なら適切な例外を設定しておきたい.またその例外を飛ばす可能性をドキュメンテーションしたい.しかしこれは別の話題である
	if( d3d == nullptr ) throw std::exception("GraphicDeviceFactory: Direct3Dを利用出来ないようです。");

	// WindowHandleを取得
	const HWND windowHandle = GetWindowHandle();

	// 利用するDeviceCreateFlagを検索
	const DeviceCreateFlag::Type deviceCreateFlag = GetDeviceCreateFlag();
	const D3DDEVTYPE deviceType = GetDeviceType();

	// どのような能力の機材があるのか全て集める(AdapterCapacityはそういう能力の記述の入ったデータ抽象クラス)
	std::vector<const AdapterCapacity> adapterCapacities;
	AdapterCapacity::GetAllAdapterCapacities(d3d, deviceType, adapterCapacities);

	// 適合するAdapterを検索
	const AdapterCapacity capacity = GetTargetAdapter(adapterCapacities);

	// 利用するPresentationParameterを作成
	const D3DPRESENT_PARAMETERS presentParams = GetPresentParameter(capacity);

	// IDirect3DDevice9を作成
	IDirect3DDevice9* d3ddev = nullptr;
	if (!CreateD3DDevice(d3d, windowHandle, capacity.GetAdapterNumber(), deviceCreateFlag, presentParams, &d3ddev))
	{
		if (d3d != nullptr) d3d->Release(); // もちろんintrusive_ptrやscoped_guardなんかを使うほうがよいだろう.
		if (d3ddev != nullptr) d3ddev->Release();

		// ※ここも適切な例外を設定したいが,別の話題である.
		throw std::exception("GraphicDeviceFactory: デバイスの生成に失敗した");
	}

	// scoped_guard使うべきだが.
	d3d->Release();

	// !!完成したDeviceを返却!!

	// ここが問題だ!GraphicDeviceは「noncopyableなのだ!」

	// Pattern A :: Noncopyableなので不能.moveとは,あるいはRVO最適化とは,なんだったのか.
	/*
	GraphicDevice graphicDevice(d3ddev, capacity, presentParams);
	return std::move( graphicDevice );
	// return graphicDevice	
	*/

	// PatternB :: std::make_sharedはprivateコンストラクタへのアクセスを持たない.
	// 仮に持つようにするとfriendをstd::make_sharedに与えるか,コンストラクタをpublicにする必要がある.
	// いずれにしてもFactory以外からもmake_shared経由でコンストラクタへアクセス可能になってしまう.
	/*
	return std::make_shared<GraphicDevice>(d3ddev, capacity, presentParams);
	*/

	// PatternC :: std::make_sharedをあきらめる.
	return std::shared_ptr<GraphicDevice>( new GraphicDevice( d3ddev, capacity, presentParams ) );
}

末尾の部分にいろいろ問題を指摘する形になっていますが大体こんなものです.

大まかには以下の様なクラスによって継承Factoryを動かすことになります(Template Factory Methodパターン)

例3.DefaultGraphicDeviceFactory.h

class DefaultGraphicDeviceFactory : public GraphicDeviceFactory
{
private:
	HWND m_windowHandle;
	SizeU m_size;
	bool m_isFullScreen;

public:
	DefaultGraphicDeviceFactory();
	DefaultGraphicDeviceFactory(HWND windowHandle, SizeU size, bool isFullScreen);

	void SetWindowHandle(HWND hWnd);
	void SetWindowSize(SizeU size);
	void SetScreenMode(bool isFullScreen);

private:
	virtual HWND GetWindowHandle() const;
	virtual AdapterCapacity GetTargetAdapter(const std::vector<const AdapterCapacity>& adapterCapacities) const;
	virtual DeviceCreateFlag::Type GetDeviceCreateFlag() const;
	virtual D3DDEVTYPE GetDeviceType() const;
	virtual D3DPRESENT_PARAMETERS GetPresentParameter(const AdapterCapacity& adapter) const;
};

例4.DefaultGraphicDeviceFactory.cpp

DefaultGraphicDeviceFactory::DefaultGraphicDeviceFactory(HWND windowHandle, SizeU size, bool isFullScreen)
	:	m_windowHandle(windowHandle)
	,	m_size(size)
	,	m_isFullScreen(isFullScreen)
{
}
void SetWindowHandle(HWND hWnd){ m_windowHandle = hWnd ; }
void SetWindowSize(SizeU size){ m_size = size; }
void SetScreenMode(bool isFullScreen){ m_isFullScreen = isFullScreen; }

HWND DefaultGraphicDeviceFactory::GetWindowHandle() const { return m_windowHandle; }

AdapterCapacity DefaultGraphicDeviceFactory::GetTargetAdapter(const std::vector<const AdapterCapacity>& adapterCapacities) const
{
	for(
		std::vector<const AdapterCapacity>::const_iterator it = adapterCapacities.begin();
		it != adapterCapacities.end();
		++it
	)
	{
		// デフォルトデバイスを返す
		if( it->GetAdapterNumber() == D3DADAPTER_DEFAULT ) return *it;
	}
	throw std::exception();
}

// ここでは深く考えず何時でも理想的なものを生成するファクトリを考えたいとして,返すものを切る
DeviceCreateFlag::Type DefaultGraphicDeviceFactory::GetDeviceCreateFlag() const
{
	return DeviceCreateFlag::PureDevice;
}

D3DDEVTYPE DefaultGraphicDeviceFactory::GetDeviceType() const
{
	return D3DDEVTYPE_HAL;
}

D3DPRESENT_PARAMETERS DefaultGraphicDeviceFactory::GetPresentParameter(const AdapterCapacity& adapter) const
{
	// 省略.能力指定に合わせたD3DPRESENT_PARAMETERSを生成して返す
}

こういう具合にアプリケーションに合わせたDeviceFactoryを作れば,あとはCreateDeviceを呼び出すだけで綺麗に生成管理が出来ます.普通の設計ですね.割と誰でもやってると思います.

さて相性が悪い.

さて,例2に書いたCreateDeviceメソッドの末尾なのですが,ここでGraphicDeviceクラスは普通のクラスであるため,先の記事に合わせてnoncopyableであって欲しいです.(GraphicDeviceが普通のクラスであって欲しいことは,OpenGL互換レイヤーなどを作ることを考えてみてください.)

さらに,Factoryを利用するように誘導したいため,コンストラクタを公開するのが危険なため,コンストラクタはprivateにしてしまいたいです.(※この仮定は妥当性にかけるかもしれません.後述.)

もちろんprivateにしたコンストラクタFactoryに対しては公開されていて欲しいため,Factoryはfriendに指定します.

ここでいくつかパターンが発生します.

PatternA :: 直接戻り値にする

まず1つ目のパターンは,moveコンストラクタを利用したり,RVO最適化を期待して,単に戻り値としてGraphicDeviceを返そうとします.

もちろんGraphicDeviceはnoncopyableです.無理です.

PatternB :: std::shared_ptrにし,std::make_sharedを試みる.

std::make_sharedはfriend指定されていないためprivateコンストラクタ不可能です.

じゃあstd::make_sharedもfriendに追加しましょう.

……だめです.Factory以外の全てのクラスからstd::make_sharedはつかえるため,std::make_shared経由でそのコンストラクタが利用できてしまいます.つまりアクセス保護が無効になってしまいます.

(わき道ですが,このことはshared_ptrの利用を強制したい場合,コンストラクタをprivateにし,std::make_sharedをfriendにするという方法があることを示唆しています)

PatternC :: std::shared_ptrを直接生成する

std::make_sharedがダメなのであれば,もうstd::shared_ptrを直接newによって生成しましょう.

あれ,でもこれっていやな使い方なんじゃありませんでした??

これらのパターンでは...

つまりFactoryクラスを作ると,それぞれ何らかの問題のあるインタフェイスを提供することになるか,あるいはstd::make_sharedを使わない形でstd::shared_ptrを利用することになります.

コンストラクタをprivateにするべきか,否か.それが問題だ.

ただし,ここまでの話で妥当性に疑問があるのがコンストラクタをprivateにするべきかどうかという点です.

Factoryを利用してコンストラクションを行うことを強制することの価値はどれほどでしょうか?

今回のサンプルの場合は,本当にカスタマイズがしたくなるという可能性が残っており,案外publicのほうがよいのかもしれないとも思います.

他方コンストラクタをどうしてもprivateにしたい場合がないとも言い切れず.

さて,今回もやはり結論は歯切れの悪いものになるというわけです.

special thanks

twitterタイムライン上で議論してくださった@egtra さん @hitoyozake さんありがとうございました.

2011-12-21

[][]メンバ由来の例外とドキュメンテーション

ゲームプログラマのためのC++

ゲームプログラマのためのC++(http://www.amazon.co.jp/dp/4797366761)」を買いました!そして5章まで読みました.

キチンと理由まで含めて文法の使い方を話していくスタイルの本で,改めて納得できる形でC++を勉強できるよい本です.

今読み終わったばかりの5章が,ちょうど例外に関するお話で,内容に問題を感じているわけではないのですが,そこに語られていない例外処理のお話で疑問に感じる所をふと思い出したので,書いておいて見ます.


例外とドキュメンテーション

例外は,その関数メソッドにとっては重要なインタフェイスであるため,その観点に従ったドキュメンテーションが必要になります.

--

ところでここで,ドキュメンテーションの定義が必要になるので,先に語っておきます.

この記事におけるドキュメントとは,実装の詳細に立ち入ることなく,そのモジュールを利用する側の人から見て,公開されている情報の全体のことです.

また,ドキュメンテーションとは,新しいドキュメントをモジュールのユーザに公開することです.

したがって,ここで書くドキュメントとしては例外仕様を用いてもよいでしょうし,ヘッダファイルにコメントを書いてもよいでしょうし,あるいは本当に”ドキュメント”として文書をそろえてもよいでしょう.つまるところ仕様であればなんでもよいのです.

--

……ドキュメンテーションは必要です.

このときもしすべての例外に対して,例外が起きた場合に,それを受け取った場所ごとに一種類の処理で対処するならば問題は簡単です.

例1.

{
  try{ 
    : // anything
    : // process
  } catch( ... ) {  // any exception catch
    // the process
  }
}

この場合は,ある関数メソッドがどのような例外を発生させるのかについての知識は必要になりません.

このときは例外に対して,どの程度の保証を持っているのかを考慮してコードを書けばC++の使い方の範囲で問題は解決します.

たとえばRAIIを利用するなどの方法がC++にはすでに存在しており,リソースリークの問題も対処できます.

ですから,ある関数に対する例外の種類などは問題にならず,どのような例外安全の保証がある関数なのかを記述しておけばよいということになります.

しかし,実際には

  • ユーザに対して例外の原因を通達したい
  • ちょっと通信をウェイトしておいて5回までは再試行したい

といった,例外を受けたクライアントコードで行いたい処理があり,そもそもこれが例外を使うモチベーションになっています.

このような場合,実際には全ての例外について同じ対処を取りたいわけではありません.

そのため実際に送出されうる例外は何があるのかについて知る必要があります.

しかしたとえばバイナリファイルにビルドされてしまったコード上で,ソースが手に入らない場合そのような情報はどこにも存在しないことになります.

したがって例外を送出する関数メソッドには,発生しうる例外のドキュメントが欲しいのです.

has-aしているオブジェクト由来の例外と問題

しかしあるメソッドが送出しうる例外は,ポリモフィズムが存在するため,そんなに単純に決定することができません.

例2.

class X {
private:
  const SuperClass& m_s;
public:
  X( const SuperClass& s ) : m_s(s) {  }
 
  void method(){
    m_s.use();  // throw!!
    if( isError() ) throw MyException();
  }
};

また,ここでは本質的に等価ですが,テンプレートでも同種の問題が発生します.

例3.

template<typename SuperClass>
class X {
private:
  const SuperClass& m_s;
public:
  X( const SuperClass& s ) : m_s(s) { }
  void method(){
    m_s.use();  // throw!!
    if( isError() ) throw MyException();
  }
};

例2,3のようなクラスXが存在した場合,いったいこのmethodはどんな例外を送出すると記載したらよいのでしょうか.

実際にはこのようなコードは結局,どんな例外でも送出する可能性があるように見えます.

なぜならm_sを利用する際に,このSuperClassクラスの子クラスがuseの中でどのような例外をも送出する可能性があるからです.

しかし「いかなる例外も送出する可能性がある」という記述は役に立ちません.

なぜなら,このような記述をされるとクライアントコードで取れる戦略が

  • 送出された例外をもみ消す
  • 例外を受け取り,何かよくわからない例外がおきたという例外を送出する
  • システムをシャットダウンする
  • 現在のシステムにありえる例外を全て1つ1つチェックして,可能な限りキャッチしそれに対処する

くらいしか存在しないからです.

三つ目までは論外です.

したがって四つ目を考えてみるわけですが,これはシステムに定義された例外が増えたり,新しくどこかのライブラリを導入したりすると,あっという間に破綻してしまいます.

つまりこの方法では例外に対して有効な対策をとることができないということになります.

新しい例外のみをドキュメントするという方法

ここで元の目的を思い出すと,methodは自分自身で構築した例外のみをドキュメントにし,またほかのいずれの例外も発生させる可能性がある旨をドキュメントしておけばよいと思われるかも知れません.

Xクラスでは,「MyExceptionをxxの場合に送出するよ.」という記述を入れておくわけです.

そしてそれ以外の例外を何らかのクラスが発行した場合には,単に再送出を行うようにします.

しかしこれも結局は問題の先送りで,先に述べたのと同じ問題をクライアントコード側で抱えることになってしまいます.

キャッチして,定義済みにする

ところで,普通の状況に目を移して考えてみます.

ネットワークプロトコルを扱う2つのライブラリを統合して扱うことができるクラスを作っていたとします.

それぞれのライブラリでは別の類似した例外が存在しており,これをそのまま送出した場合にはクライアントコードで,その2つの例外をキャッチしなければなりません.

また,今後対応した別種のプロトコルをまた別のライブラリを元に実装したならば,そこでも新しい例外が登場することは容易に想像できます.

このような通常の状況では,キャッチして定義済みの例外を再発行するという方法が有効です.

作成するクラスでは,自前の例外を定義して,それだけを発行するようにすることにします.

この場合は発行する例外は自前の例外のみであることをドキュメンテーションできますから,有効なドキュメントが得られます.

ではこの方法をクラスXに適用するとどうでしょうか.


うまくいかないのではないかと思います.

せいぜい予想外の例外に関して,予想外の状況である旨を伝える定義済みの例外を発行することができるだけです.

このような例外を受け取ったクライアントコードは,やはりここまでの方法と同じ問題を抱えてしまいます.

「不明な例外」ではドキュメンテーションされていても仕方が無いのです.


ではうまく行く状況と,こちらの状況の違いはなんでしょうか.

私は暗黙のうちにネットワークプロトコルライブラリが送出しうる例外がドキュメンテーションされているということにしていました.

実はそこがSuperClassクラスとの違いで,SuperClassは,いかなる例外,不明な例外が発行されうるために,受け取る側のXクラスで例外の送出に関して意味のあるドキュメントが書けないのです.

例外はインタフェイスの一部ではないだろうか?

そして,私の疑問は,ここからです.

ここまでの結論から,あるインタフェイスやスーパクラスを定めるとき,その時点で発行しうる例外を全て定めるべきなのかもしれません.(なぜ歯切れが悪いのかは後述します.)

もしSuperClassクラスが発行する例外が,陽に定まっており,子クラスがそれにしたがっていることがわかっていたならば,Xクラスはその例外が発行される旨をドキュメンテーションすることができます.

たとえばSuperClassクラスはAException, BExceptionを発行することがあるとします.

このときXクラスのmethodメソッドでは,AException, BException, MyExcetionと,C++仕様で制定されたランタイムの例外が発行されうることがわかります.

これならドキュメントに記述できます.

ですからXクラスのクライアントコードで,例外に対する処理をうまくコーディングすることができます.

またよく考えるとmethodメソッドが例外を送出するというのは,methodメソッドのもつひとつの性質であると考えることができます.

したがって,ここにはリスコフの置換原則が働いてきます.

このことからもやはりインタフェイスやスーパクラス側では,methodメソッドに期待される性質のひとつとして,どのような例外が発行されうるのかを定める必要があります.

この対処はすばらしくうまくいくように見えます.

しかし問題はSuperClassにおいてどの様な形で例外の送出ルールを定めるのかです.

私は3つの方法を思いつきました.

1.送出する例外をメソッド単位で記述する

1つ目は,送出する例外をメソッド単位で記述するという方法です.

この方法は,ある例外を発行しないことになっているメソッドの修正を困難にします.

例外を発行しないことになっていたメソッドに新しい例外を追加した場合を考えます.

すると,この変更の影響はそのメソッドの呼び出し元全て,また,その呼び出し元の呼び出し元全て,さらに……という連鎖をたどるため,容易に修正困難になることが予測できます.

2.送出する例外をメソッド単位で記述し,また記述では本当の必要よりも例外の危険性を大きめに取って記述する

2つ目は,送出する例外をメソッド単位で記述し,また記述では本当の必要よりも例外の危険性を大きめに取って記述するという方法です.

この方法では,ある程度の修正までは耐えることができるのですが,予想の範囲を超えるような修正が必要になる可能性が常にあるため,非常に不安です.

さらに「大きめに取る」といっても,その基準が曖昧なままなためプラクティカルにはどの様に定めるべきなのかが見えません.

たとえば最大まで取ってしまうという戦略をとると,それはつまりなんでも送出する可能性があるということなので,本文で紹介した問題が立ち戻ってきます.

3.例外の送出をクラス単位で記述する

3つ目は,例外の送出をクラス単位で記述するという方法です.

クラス単位で,全てのメンバを見て,そこの中だけで,メソッドが利用するものが変化したときに,ありえる全ての例外送出をドキュメントするという方針です.

C++では本質的に強い例外安全性を保証するのに必要なswapメソッドなどに,不送出保証の要求があるため,単純にこれを実行することはできません.

そこで本質的に不送出を保証するメソッドとそれ以外を分けて,それ以外の部分についてのみ例外を送出する可能性があるという形にします.

この方法ならば,2つ目で問題になった基準が明確になります.

しかしながら,この基準は「それっぽい」というだけであって実際にうまくいく保証はどこにもありません.

まとめと結論

これらの挙動をまとめると,次のような方法で例外に対処すると,一応はよいことがわかります.

  • スーパクラスを定めた時点で,それぞれのメソッドにおいて発行する例外を確定させ,ドキュメンテーションし,テストする.
    • スーパクラスと違う例外を発行するようなサブクラスはリスコフの置換原則に違反するという観点でも必要です.
  • あるメソッド関数が発行するすべての例外はドキュメンテーションする
    • このため,利用している物から発行される例外の捕捉を行うか,行わないかは明示的に意識する必要がある.その後の対処は3つになる
      • 捕捉し,その場で例外に対処する.ドキュメントには,対処がどのように行われるかについて記述する.
      • 捕捉し,定義しておいた例外に統合する.ドキュメントには定義しておいた例外を発行する旨を記述する.
      • 補足せず,そのまま再発行する.ドキュメントは,再発行する例外について記述する.

しかし次のような問題が起きてしまいます.

  • 送出する例外をメソッド単位で記述すると,ある例外を発行しないことになっているメソッドの修正を困難にします
  • 送出する例外をメソッド単位にし,本当の必要よりも例外の危険性を大きめに取って記述すると,予想の範囲を超える可能性が常にあります.
    • また,そのまま全ての例外がありえることにしてしまうと,本文で紹介した問題が立ち戻ってきます.
  • 送出する例外をクラス単位で記述すると,本質的に例外安全性を保証するのに必要なメソッドが存在するため不可能です
    • 例外を送出しないメソッドと,このクラスで送出されうる例外なら全て発行しうるメソッドに分割する方法はありえます.
      • ただし,この方法はメソッド単位で大きめに記述する方法に,「それっぽい基準」を与えたに過ぎません(本当に修正が容易になっているという根拠がありません)

このように,どの方針にも問題があることがわかります.そのため歯切れの悪い結論になってしまっています.

本質的には例外が使われた位置からどこまでもスタックをさかのぼるという性質のため,抽象化レイヤーを容易に破壊してしまうという性質が原因にあるように思います.

残念ながら,私はこの問題に対して結論を持ちません.

……どなたかご存知でしたら,お教えください.

2011-12-20

[]表明と評価と感想と.

鑑賞が終わった.その後に……

表明

ある料理,たとえば何かラーメンでも食べたあと「ああ,いい料理だった」と一言述べる.

あるいは,クラブにでも行ってDJのプレイを聞いた後「こいつは駄目な音楽だった」などと一言述べる.

よくあるワンシーンです.

ここで前者も後者も,その善し悪しは違っていても,その言葉の意味するところは,好き嫌いです.

この方法を私は表明と呼んでいます.

表明のメリットは,単にすぐわいてくる快・不快の感情をスッと述べるだけで意見することができる点です.お手軽さというもので,作品鑑賞に気軽に参加できるすばらしいメリットです.

しかしここで,どうしても注意しなければならないのは,二分している,ということです.

つまり作品に対する感情を,快と不快のたった二つに分類してしまっているのです.

人間の感情・感覚・感性が,そんなに単純なはずがありません.

ではなぜ二分された形で表明が登場するかといえば,表明を述べる人は最終的に受けた感覚を自分の中で理解せず,快か不快か,という2つのおおざっぱな物として解しているからに他ならないと思っています.

つまり残念なことに,表明を述べるときにその人は受けた感覚を自分で理解しているわけではないのです.

このように表明には快・不快の二分法で自分自身の心を閉じ込めている側面があります.

感性をよりしっかりと働かせるには,表明しか述べられない自分から一度脱却する必要があるのではないかと考えています.

評価

別のシーンを考えてみましょう.

初めと同じ料理を食べた後「塩が多い」と一言述べる.

あるいは,同じプレイを聞いた後「低温が強い」と一言述べる.

これもよくあるワンシーンです.

やはりここでの前者も後者も,その取り上げている項目は違っています.

しかしどちらも意味するところは,作品のある項目について事実を述べるという形をしています.

あくまで作品がどのような物なのかを噛み砕くにすぎません.そこには主観がないのです.

この方法を私は評価と呼んでいます.

このような方法で作品を鑑賞するときには,人は心の中で,自分が作品に対してどのように当たろうかという基準のチェックリストを持っていると考えられます.デッサンができてない絵は”駄目”.色彩の色が飛んでいるのは”駄目”.不協和音程の頻繁なぶつかりは無い,”よし”.

この鑑賞の利点は,非常に鋭く自分の感性を超えたレベルで作品を評価することができるであろうという点です.代わりにその結果は実は自身の趣向を見つめたものになるとは限らないというデメリットもあります.

またチェックリストというのは,その内容や精細さに違いはあれど,最後に得られるのは合格・不合格の二分割です.

作品は数多く存在するのですが,評価の形で鑑賞している限り世界には2つの作品しか存在しません.

もちろんチェックリストを多種持っておき,それぞれの合格不合格でより細分化した世界を見ていくという手もあり得ることでしょう.

しかし,鑑賞を常に評価によって行っている限りは,チェックリストの数は増えることはありません.なぜならば,その方法ではチェックリストの中にある基準以外を考慮することが出来ないからです.新しい基準は見つかりません.

感想

また別のシーンを考えてみましょう.

やはり初めと同じ料理を食べた後「アットホームな雰囲気を受けた」と一言述べる.

あるいはやはり,同じプレイを聞いた後「鋭い緊張感を受けた」と一言述べる.

これもやはりよくあるワンシーンです.

この言葉も,取り上げている感情は違っています.

ですが,どちらも鑑賞した人物の心内で起きた感情,受け取った物事について述べた意見です.

これまでの二つと違う,明確なメリットが二分法ではない,という点です.

作品の多種多様さをそのままに受け入れることができ,また自分自身の感情の多種多様さをありのままに示すことができます.

しかし難しいのは,ほんの少しだけ普段よりも鋭敏になって感情を言葉にしてみる必要があることです.本当に「ああ,これはアットホームだな」という程度の,ほんの少しだけなのですが,難しい人には難しいことでしょう.

作者としては

以上の三つは,私が様々な人から頂いた私の作品に対する言葉を,よく私が理解する上で見つけた分類です.

作者によって,どの種類の鑑賞を行って欲しいのかというのは違ってくるだろうなと思っています.

私は,感想が欲しい人であるため,感想のもつ難しさという側面を乗り越えようとしています.

いまは,作品が強いものであれば,その感情を口に出すことを容易にすることができるのではないかと私は考えており,従ってその方向で作品を作っています.

しかし実際には表明と評価が混ざったような形になっていることが多く(曰く「この曲は低音がぶつかっていてよくない」)私としては,実力不足を感じさせられます.

それでも鑑賞のあと感想を述べてくれる人というのは一定におり,私はそういう方の存在に深く感謝しております.改めて,ありがとうございます.

ちなみに別の方では,極限の快であるような作品を目指しておられる方も居るようでして,その場合は感想は逆に困った意見になるのかなと思っています.

鑑賞者として

私は自分の心境と向き合うことを是とする人間なので,割と感想を基軸にして鑑賞をするようにしています.

手軽にたくさんの作品を見ていく,自分自身がどの様な物を好む人なのか人と共有するというのも良いと思っていますし,これが最高の作品であるというものを見つけることを是とする向きもあると思いますので,表明も評価もまた欲しいことがあるものかなとも思っています.

総じていえば鑑賞する側であれば,この辺りは好き勝手にやるのがいいだろうと.

しかし,自分がどのような意味で鑑賞をしているのかは一度見ておくと,あるいは,別の鑑賞方式を試してみると,少し世界が広がるかもしれません.

草々.