Archive for 5月, 2013

[] より . を使おう

Posted 2013.05.30 in 吉里吉里

吉里吉里(TJS2)ではオブジェクトのメンバ(または辞書の要素)へのアクセス方法には
直接メンバ選択演算子 . による直接参照と
間接メンバ選択演算子 [] による間接参照があるが
今回は

[] より . の方が速い!

というお話。

実験

次のようなテストスクリプトを書いて
それぞれの書き込み時と読み込み時の速度を計測してみた。


function test_pureLoop(testNum)
{
  var begin = System.getTickCount();
  for(var i=0; i < testNum; i++) ;
  var end = System.getTickCount();
  return end-begin;
}

function test_set1(testNum)
{
  var dic = %[];
  var begin = System.getTickCount();
  for(var i=0; i < testNum; i++) dic["X"] = 999;
  var end = System.getTickCount();
  return end-begin;
}

function test_set2(testNum)
{
  var dic = %[];
  var begin = System.getTickCount();
  for(var i=0; i < testNum; i++) dic["XXXXXXXXXX"]  = 999;
  var end = System.getTickCount();
  return end-begin;
}

function test_set3(testNum)
{
  var dic = %[];
  var begin = System.getTickCount();
  for(var i=0; i < testNum; i++) dic["XXXXXXXXXXXXXXXXXXXX"]  = 999;
  var end = System.getTickCount();
  return end-begin;
}

function test_set4(testNum)
{
  var dic = %[];
  var begin = System.getTickCount();
  for(var i=0; i < testNum; i++) dic.X = 999;
  var end = System.getTickCount();
  return end-begin;
}

function test_set5(testNum)
{
  var dic = %[];
  var begin = System.getTickCount();
  for(var i=0; i < testNum; i++) dic.XXXXXXXXXX  = 999;
  var end = System.getTickCount();
  return end-begin;
}
  
function test_set6(testNum)
{
  var dic = %[];
  var begin = System.getTickCount();
  for(var i=0; i < testNum; i++) dic.XXXXXXXXXXXXXXXXXXXX = 999;
  var end = System.getTickCount();
  return end-begin;
}

function test_get1(testNum)
{
  var dic = %[ "X" => 999 ];
  var begin = System.getTickCount();
  for(var i=0; i < testNum; i++) dic["X"];
  var end = System.getTickCount();
  return end-begin;
}

function test_get2(testNum)
{
  var dic = %[ "XXXXXXXXXX" => 999 ];
  var begin = System.getTickCount();
  for(var i=0; i < testNum; i++) dic["XXXXXXXXXX"];
  var end = System.getTickCount();
  return end-begin;
}

function test_get3(testNum)
{
  var dic = %[ "XXXXXXXXXXXXXXXXXXXX" => 999 ];
  var begin = System.getTickCount();
  for(var i=0; i < testNum; i++) dic["XXXXXXXXXXXXXXXXXXXX"];
  var end = System.getTickCount();
  return end-begin;
}

function test_get4(testNum)
{
  var dic = %[ "X" => 999 ];
  var begin = System.getTickCount();
  for(var i=0; i < testNum; i++) dic.X;
  var end = System.getTickCount();
  return end-begin;
}

function test_get5(testNum)
{
  var dic = %[ "XXXXXXXXXX" => 999 ];
  var begin = System.getTickCount();
  for(var i=0; i < testNum; i++) dic.XXXXXXXXXX;
  var end = System.getTickCount();
  return end-begin;
}

function test_get6(testNum)
{
  var dic = %[ "XXXXXXXXXXXXXXXXXXXX" => 999 ];
  var begin = System.getTickCount();
  for(var i=0; i < testNum; i++) dic.XXXXXXXXXXXXXXXXXXXX;
  var end = System.getTickCount();
  return end-begin;
}

// 割り込みなどによる計測誤差をなくすために複数回計測して最小の値を採用する
function repeatTest(func)
{
  var TEST_NUM = 1000000;
  var PEPEAT_NUM = 100;
  var minTime;
  for(var i=0; i < PEPEAT_NUM; i++)
  {
    var time = func(TEST_NUM);
    if(minTime === void)
      minTime = time;
    else
      minTime = Math.min(minTime, time);
  }
  return minTime;
}

function test()
{
  var base = repeatTest(test_pureLoop);
  Debug.message("pure loop                         : %d ms"
    .sprintf(base));

  Debug.message(">set");
  Debug.message("dic[\"X\"] = 999                    : %d ms"
    .sprintf(repeatTest(test_set1)-base));
  Debug.message("dic[\"XXXXXXXXXX\"] = 999           : %d ms"
    .sprintf(repeatTest(test_set2)-base));
  Debug.message("dic[\"XXXXXXXXXXXXXXXXXXXX\"] = 999 : %d ms"
    .sprintf(repeatTest(test_set3)-base));
  Debug.message("dic.X = 999                       : %d ms"
    .sprintf(repeatTest(test_set4)-base));
  Debug.message("dic.XXXXXXXXXX = 999              : %d ms"
    .sprintf(repeatTest(test_set5)-base));
  Debug.message("dic.XXXXXXXXXXXXXXXXXXXX = 999    : %d ms"
    .sprintf(repeatTest(test_set6)-base));

  Debug.message(">get");
  Debug.message("dic[\"X\"]                          : %d ms"
    .sprintf(repeatTest(test_get1)-base));
  Debug.message("dic[\"XXXXXXXXXX\"]                 : %d ms"
    .sprintf(repeatTest(test_get2)-base));
  Debug.message("dic[\"XXXXXXXXXXXXXXXXXXXX\"]       : %d ms"
    .sprintf(repeatTest(test_get3)-base));
  Debug.message("dic.X                             : %d ms"
    .sprintf(repeatTest(test_get4)-base));
  Debug.message("dic.XXXXXXXXXX                    : %d ms"
    .sprintf(repeatTest(test_get5)-base));
  Debug.message("dic.XXXXXXXXXXXXXXXXXXXX          : %d ms"
    .sprintf(repeatTest(test_get6)-base));
}

var win = new Window();
win.visible = true;
Debug.console.visible = true;
test();

実行結果。


pure loop                         : 17 ms
>set
dic["X"] = 999                    : 68 ms
dic["XXXXXXXXXX"] = 999           : 67 ms
dic["XXXXXXXXXXXXXXXXXXXX"] = 999 : 67 ms
dic.X = 999                       : 41 ms
dic.XXXXXXXXXX = 999              : 41 ms
dic.XXXXXXXXXXXXXXXXXXXX = 999    : 41 ms
>get
dic["X"]                          : 53 ms
dic["XXXXXXXXXX"]                 : 72 ms
dic["XXXXXXXXXXXXXXXXXXXX"]       : 96 ms
dic.X                             : 34 ms
dic.XXXXXXXXXX                    : 34 ms
dic.XXXXXXXXXXXXXXXXXXXX          : 34 ms

ちなみに比較しやすいように各値からは pure loop 分を差し引いている。
パッと見で明らかに

間接参照による取得が遅い!

キー文字列の長さに比例して遅くなっているように思える。
10文字で直接参照に対して倍近い速度差。
これは場合によってはちょっと無視できない差だ。

何が起きているのか

この現象を理解するにはまず、文字列が保持する『ヒント』情報について知る必要がある。

文字列が辞書に対するキー文字列として使われる時、キー文字列からハッシュ値が計算される。
ハッシュ値は文字列の内容全体から計算されるので、文字列が長ければ長いほど計算に時間がかかる。
計算で得られたハッシュ値はハッシュテーブル内の位置を示す値として使われる一方で、
文字列に『ヒント』情報として保持される。

保持された『ヒント』は次回辞書要素参照時にハッシュ値のかわりに使用され
ハッシュ値の計算を省いた簡易的な検索が行われる。
こうなるとキー文字列の長さは関係なくなる。
キー文字列の長さに関わらず速度がほぼ一定の計測結果が得られたのはそのためである。

ではなぜ間接参照による取得時だけ速度が一定ではないのか。
それは

間接参照による取得時だけヒントが使われない

からだ。
すなわち、毎回ハッシュ値が再計算されているのである。

検証

間接参照による取得の実装は tTJSInterCodeContext::GetPropertyIndirect にある。
tjsInterCodeExec.cpp

本当にヒントを使わないことが原因なのか確認するために
間接参照による取得時にもヒントを使うように改造してみた。


pure loop                         : 17 ms
>set
dic["X"] = 999                    : 68 ms
dic["XXXXXXXXXX"] = 999           : 69 ms
dic["XXXXXXXXXXXXXXXXXXXX"] = 999 : 68 ms
dic.X = 999                       : 41 ms
dic.XXXXXXXXXX = 999              : 41 ms
dic.XXXXXXXXXXXXXXXXXXXX = 999    : 40 ms
>get
dic["X"]                          : 46 ms
dic["XXXXXXXXXX"]                 : 46 ms
dic["XXXXXXXXXXXXXXXXXXXX"]       : 46 ms
dic.X                             : 33 ms
dic.XXXXXXXXXX                    : 33 ms
dic.XXXXXXXXXXXXXXXXXXXX          : 33 ms

期待通り一定速度になった!

ヒントを使わない理由

間接参照による設定時にはヒントを使っているので
取得時だけヒントを使わない理由はあまりないように思える。
ソースには
// TODO: verify here needs hint holding
と記述があるので、ヒントを使うべきか検討中…
といったかんじだろうか。

『その文字列の内容から計算されるハッシュ値』と『ヒント』が常にイコールなら
取得時にヒントを使ってもおそらく何も問題は起きないだろう。
しかしどうやら、常にイコールなわけではないようだ。
ヒント作成後に文字列が内部的に改変された場合、ヒントはクリアも更新もされないからである。

文字列が後から改変された状態で辞書を参照すると、ヒントを使った要素の簡易検索に失敗し
ハッシュ値が再計算されヒントが再設定され、もう一度検索が行われる。
すなわちヒントによる検索が逆に無駄な処理になってしまう。

そういった問題を回避するために、間接参照ではヒントを使わないようにしているものと思われる。
直接参照でヒントが使われるは、直接参照のキー文字列は必ず文字列定数だからだろう。
定数なので文字列が後から改変される心配がない、ということだ。

ただTJS上での文字列操作は、文字列の内容をいじるのではなく、
新しく文字列を生成するようになっているため
文字列が後から改変されるといった状況は根本的に起こらないような気もする。
何か見落としているだけかもしれない。

ちなみに辞書への要素の追加時とdelete演算子による削除時にも
文字列が保持しているヒントは実質的に使われず必ずハッシュ値が再計算されるようだ。

まとめ

ヒントが使われない理由はともあれ、このようなことから間接参照による取得は
直接参照に比べて大きくパフォーマンスが落ちる場合がある。
全般的に間接参照よりも直接参照の方が少しだけ速いことからも
直接参照を使える場面では、直接参照を使ったほうが良いと言えるだろう。


affineCopyを-0.5する意味

Posted 2013.05.19 in 吉里吉里

affineCopy のピクセルの特殊な扱いは、入力領域のみならず出力領域においても関係がある。

仕様の再確認

ひとまず affineCopy におけるピクセルの扱いを再確認しておこう。

整数座標上で (0, 0) のピクセルは、
実数座標上では (0.0, 0.0) – (1.0, 1.0) の範囲を覆っていると見なすのが普通である。
それに対して、affineCopyでは (0, 0) の位置にあるピクセルは
(-0.5, -0.5) – (0.5, 0.5) の範囲にあると見なされる。

KiriKiri_AffineCopy3_Image1

出力領域の扱い

このピクセルの扱いは、出力領域においても適用される。
ソレがいったいどんな作用をもたらすのか、ちょっとわかりにくいかもしれない。
このことは

”ラスタライズ処理における、サンプリング位置”

に関係がある。

ラスタライズ

「ラスタライズ」とは、抽象的な描画情報を具体的なピクセルに落とし込む処理のことである。

ここまで述べてきたように整数空間において点であるピクセルは、
実数空間では特定の範囲を持つ矩形として扱われる。

画像のラスタライズ処理において最も気を遣わなければならないのが、
この「実数空間上の入力元ピクセルを、いかに整数空間上の出力先ピクセルに落とし込むか」
言い換えれば

「範囲を、いかに点に落とし込むか」

という部分だ。

サンプリング

ラスタライズ処理において、出力先ピクセルに出力する情報を
入力元から抽出するプロセスを「サンプリング」と呼ぶ。

ある出力先ピクセルに出力する情報は
出力先画像の上に入力元画像を重ね合わせた時、
出力先ピクセルの範囲と、範囲が被っている入力元ピクセルの中から決定する。

この時、出力先ピクセルの範囲内には
複数の入力元ピクセルの範囲が含まれる可能性がある。
そこで問題になってくるのが、範囲が被っている入力ピクセルのうち
どのピクセルの情報を採用するのかという問題だ。

サンプリング点による抽出

この決定は「サンプリング点」によって行う。

サンプリング点は実数空間上のピクセルの範囲の中のいずれかの点だ。
サンプリング点の位置を基準にサンプリングを行う。
最近傍法の場合、サンプリング点の真上にある入力元ピクセルが採用される。

サンプリング点にはピクセル範囲の中心位置を使うのが一般的だ。
一般的なピンセル範囲ではこれは (0.5, 0.5) の位置である。

affineCopy の場合、 ピクセルの範囲は (-0.5, -0.5) – (0.5, 0.5) なので
中心位置は (0.0, 0.0) ということになる。
実際 affineCopy では (0.0, 0.0) の位置がサンプリング点となっている。

KiriKiri_AffineCopy3_Image2

(0.0, 0.0) の位置は一般的なピクセル範囲においては左上の位置を表す。
このことから、一般的なピクセル範囲の考え方において affineCopy は

サンプリング点がピクセルの左上になっている

と見なすことができる。

ズレの真の原因

サンプリング点がピクセルの左上だと……
最近傍法の場合、座標をわずかに動かしただけで
サンプリング対象が変化するという現象をひきおこす。
線形補間の場合、サンプリング位置が左上方向にズレることにより、描画が右下にズレる。
これまでに見てきた現象だ!

そう、すなわちズレの原因はサンプリング点が実質的に
ピクセルの左上になっていることにあるのである。
ただしこのズレは不具合ではなく、

ピクセル範囲の考え方の齟齬によって起こる現象

だと言える。
そしてそれこそがズレの真の原因である。

-0.5の意味

ここまで理解すると-0.5の意味はもうわかるだろう。
出力座標を-0.5すると、ピクセル範囲の考え方のズレを補正することができるのである。

またこのことは別の見方もできる。
出力座標を-0.5することは、出力座標上の入力座標を出力座標+0.5分だけずらすことと同義である。
これはサンプリング点の位置をピクセルの左上 (0.0, 0.0) から
ピクセルの中央 (0.5, 0.5) に移動させることと同じことだ。
よってズレが補正されるのである。

まとめ

回りくどく長々と説明してきたが…
結局のところ、描画のズレはピクセル範囲の考え方の齟齬に起因するものだということだ。
それさえ理解していれば affineCopy はなんら問題なく使うことができる。
またこの問題は operateAffine などでも同様である。

ちなみに stretchCopy は内部的に affineCopy を利用する形で実装されているが、
そこでは出力座標がしっかり -0.5 されている!

affineCopy を使う場合は -0.5 を忘れずに!

[追記]
Direct3D9 や OpenGL はピクセルの範囲が (-0.5, -0.5) – (0.5, 0.5) だったようだ。
affineCopy のピクセルの扱いはそれらに準拠したものかもしれない。
なお Direct3D10 からは ピクセルの範囲は (0.0, 0.0) – (1.0, 1.0) になっている。
参考:座標系 (Direct3D 10)

[追記]
上記のOpenGLは勘違いでした。


affineCopyのピクセルの扱い

Posted 2013.05.19 in 吉里吉里

なぜ affineCopy がズレて描画され、なぜ出力座標を -0.5 するとズレなくなるのか。
ヒントは affineCopy の仕様にある。

仕様の確認

吉里吉里のマニュアルには次のように書かれている。

affineCopy
アフィン変換においては、ピクセルは 1.0 × 1.0 のサイズを持っていると見なされます。
つまり、(0, 0) の位置にあるピクセルは (-0.5, -0.5) – (0.5, 0.5) の範囲にあると見なされます。

一般的には、整数座標上で (0, 0) のピクセルは、
実数座標上では (0.0, 0.0) – (1.0, 1.0) の範囲を覆っていると見なすのが普通である。
それに対して、-0.5 ずれた範囲を覆っているものとして扱われると書かれている。

なるほど -0.5 というあたりからして、ズレの原因はこれっぽいが
(-0.5, -0.5) – (0.5, 0.5) の範囲にあると見なされるとは、
いったいどういう意味だろうか。

入力領域の扱い

まずこの仕様が関係してくるのが

”affine=trueの時に出力領域の計算をする際の入力領域の扱い”

である。
affine=true の時、入力領域全体を -0.5 移動させてから、
アフィン変換行列による変形計算を行い、出力領域が決定される。
これがどういうことかは、次のようにしてみるとわかる。


     var x = 50;
     var y = 50;
     primaryLayer.affineCopy(image, 0, 0, w, h
      , true, 100, 0, 0, 100, x, y, stNearest);

KiriKiri_AffineCopy2_Test1

画像を(50, 50)の位置に100倍に拡大して描画しているが、
左上に大きくズレこんで描画されてしまっている。
これは (-0.5, -0.5) – (0.5, 0.5) の範囲にあるとみなされる入力ピクセルが100倍になり
(-50.0, -50.0) – (50.0, 50.0) の範囲に出力されているからである。

この仕様は正直、あまり直感的ではなく扱いづらく思える。
ちょっと不思議な仕様だ。

affine=true 時のズレ

affineCopy を affine=true で使い、画像を無変形で描画する時
出力座標を -0.5 しなくても描画はズレないように見える。
しかし実は affine=true の場合でも、ズレ自体は起きている。
ではなぜズレがないように見えるのか。

先の仕様を思い出して欲しい。
入力領域全体を -0.5 移動させて、それを出力領域にするということは…
出力領域全体を -0.5 移動させることと同義である。
そう、先の仕様によってズレが補正されているのだ。

ただしこれが同義なのは無変形の場合だけであることに注意が必要だ。
画像を変形させると隠れ潜んでいたズレが姿を現す。

ここでのまとめ

affine=true 時のズレを補正するのは不可能ではないが、ちょっと難しい。
入力座標を +0.5 し、出力座標を -0.5 すれば
仕様を打ち消し、ズレもなくせそうな気がするが…
affineCopy は入力座標に整数しか指定できないため、それはできない。
行列計算で補正する必要がある。

厳密にズレを補正しようと思うなら、affine=false にして
出力座標計算は自力で行った方が簡単だろう。

つづく!


affineCopyは-0.5せよ

Posted 2013.05.16 in 吉里吉里

今回は

『affineCopy を affine=false で使う場合は、
 出力座標を-0.5しなければならない!』

というお話。

-0.5しないと起きる問題

-0.5しないとどうなるのか。
一言で言えば

-0.5しないと画像がずれて描画される!

のである。

サンプルスクリプト

とりあえず、affineCopy を使って普通に画像を描画する tjs スクリプトを書いてみよう。

Box.png
Box


class TestWindow extends Window
{
  function TestWindow()
  {
    super.Window();
    
    // プライマリレイヤー作成
    add(new Layer(this, null));
    primaryLayer.setImageSize(200,200);
    primaryLayer.setSizeToImageSize();
    setInnerSize(primaryLayer.width, primaryLayer.height);
    
    // 画像をロード
    var image = new Layer(this, primaryLayer);
    image.loadImages("Box");
    
    // 画像を描画
    var w = image.imageWidth;
    var h = image.imageHeight;
    var x = 50;
    var y = 50;
    primaryLayer.affineCopy(image, 0, 0, w, h
      , false, x, y, x+w, y, x, y+h, stNearest);

    // 後始末
    invalidate image;
  }
}

var win = new TestWindow();
win.visible = true;

KiriKiri_AffineCopy_Test1

何も問題ないように見える。

線形補間の場合に起こる問題

サンプリング方法を線形補間など(stFastLinear, stLinearなど)にすると現象を明らかに確認できる。


    var x = 50;
    var y = 50;
    primaryLayer.affineCopy(image, 0, 0, w, h
      , false, x, y, x+w, y, x, y+h, stFastLinear);

KiriKiri_AffineCopy_Test2

なんだか変にぼやけている。
というか

画像が右下に
0.5 ピクセル分ずれて描画されている!

では出力座標を-0.5してみよう。


    var x = 50-0.5;
    var y = 50-0.5;
    primaryLayer.affineCopy(image, 0, 0, w, h
      , false, x, y, x+w, y, x, y+h, stFastLinear);

KiriKiri_AffineCopy_Test3

ずれがなくなった!

stNearest の場合に起こる問題

描画がずれる現象は補間を行う場合だけの問題なのだろうか。
そうではない。
stNearest の場合にも、一見わかりにくいが描画がずれている。
症状は画像をわずかに拡大させると確認できる。


    var x = 50;
    var y = 50;
    primaryLayer.affineCopy(image, 0, 0, w, h
      , false, x, y, x+w, y, x, y+h+0.1, stNearest);

KiriKiri_AffineCopy_Test4

サイズが1ピクセル大きくなり、上辺のラインが太くなっている。
+0.2にしても、+0.3にしても上辺のラインが常に太い状態になる。
それの何が問題なのかと思うかもしれないが、
これは不自然な画像の拡大の仕方であると言える。

出力座標を-0.5してみよう。


    var x = 50-0.5;
    var y = 50-0.5;
    primaryLayer.affineCopy(image, 0, 0, w, h
      , false, x, y, x+w, y, x, y+h+0.1, stNearest);

KiriKiri_AffineCopy_Test5

サイズは変化せず、上辺のラインが太くならなくなった。

じりじり拡大させるテスト

stNearest の場合の問題のポイントは、描画サイズのわずかな変化で
実際に描画される画像サイズに大きな変化が起こる点だ。
画像をゆっくりとじりじり拡大させるような演出を行う際、この問題は顕著に現れる。


class TestWindow extends Window
{
  var image;
  var timer;
  
  function TestWindow()
  {
    super.Window();
    
    // プライマリレイヤー作成
    add(new Layer(this, null));
    primaryLayer.setImageSize(200,200);
    primaryLayer.setSizeToImageSize();
    setInnerSize(primaryLayer.width, primaryLayer.height);
    
    // 画像をロード
    image = new Layer(this, primaryLayer);
    image.loadImages("Box");
    
    // タイマー作成
    timer = new Timer(onTimer, "");
    timer.interval = 1000;
    timer.enabled = true;
    onTimer();
  }
  
  function finalize()
  {
    invalidate timer;
    invalidate image;

    super.finalize();
  }
  
  var step = 0;
  function onTimer()
  {
     caption = "step:"+step;
     primaryLayer.fillRect(0, 0
       , primaryLayer.width, primaryLayer.height, 0xFFFFFFFF);
     
     // 画像を描画
     var w = image.imageWidth;
     var h = image.imageHeight;
     var x = 50;
     var y = 50;
     primaryLayer.affineCopy(image, 0, 0, w, h
       , false, x, y, x+w, y, x, y+h+step/10, stNearest);

     step++;
  }
}

var win = new TestWindow();
win.visible = true;

これを実行すると動き出しの部分で画像全体が一気にビクっとずれるのがわかる。
描画サイズのわずかな変化で、大きな変化が起きてしまっているからだ。

これの出力座標を-0.5すると、動き出しで画像が大きく動かなくなる。
そしてサイズの変化に応じてじりじり画像が動くようになる。
わずかな変化で大きく動く前の状態より、こちらの方がより自然な動きだと思わないだろうか。

まとめ

なぜこの問題が起き、なぜ-0.5すると問題が改善するのか。
詳しくは次回~


オーバーライドされたプロパティ

Posted 2013.05.13 in 吉里吉里

派生クラスによってオーバーライドされて隠蔽されてしまい
普通にアクセスできなくなった基底クラスの関数にアクセスするには
incontextof 演算子を使って


(global.Layer.update incontextof layer)()

と書けばよいが、オーバーライドされたプロパティにアクセスしようと同じノリで


(global.Layer.left incontextof layer)

と書いてもこれはエラーになる。
incontextof 演算子の意味を考えればあたり前なのだけれど…
ちょっとハマりがちなところだと思う。

オーバーライドされたプロパティにアクセスするには、次の3つの方法がある。

1. superを使う


var value = super.left;

superキーワードを介して親クラスのプロパティにアクセスできる。

この方法は最も単純明快だが、この方法を使えるのはそのクラス内だけである。
super を使って外からアクセスできるようにするには別名のプロパティを作って
そこから super を介してアクセスするといったような回りくどい方法をとる必要がある。

2. プロパティオブジェクトを使う


var value = *(&global.Layer.left incontextof layer);

親クラスのオブジェクトから&演算子でプロパティオブジェクトを取得し、
incontextof 演算子でコンテキストを置き換えて*演算子で参照する方法。
なんだか難しそうな表記だが、&演算子と*演算子の意味を理解すれば単純だ。

&演算子で取得したプロパティオブジェクトは、変数に格納して使うこともできる。


var propLeft = (&global.Layer.left incontextof layer);
var value = *propLeft;

3. クラスオブジェクトを使う


var value = (global.Layer incontextof layer).left;

親クラスのオブジェクトのコンテキストを置き換えて使う方法。
この方法はたぶんあまり知られていない気がする。
プロパティオブジェクトを使うよりこっちの方が直感的だろう。

変数に格納して使うこともできるので、
たくさんのオーバーライドされたプロパティにアクセスする場合には特に便利だ。


var globalLayer = (global.Layer incontextof layer);
var value = globalLayer.left;

速度の比較

3つのアクセス方法の速度を計測してみよう。


class MyLayer extends Layer
{
  function MyLayer(window, parent)
  {
    super.Layer(...);
    super.left = 222;
  }

  var m_left = 111;
  property left
  {
    setter(v){ m_left = v; }
    getter(){ return m_left; }
  }

  property superLeft
  {
    getter(){ return super.left; }
  }
}

class TestWindow extends Window
{
  function TestWindow()
  {
    super.Window();
    add(new Layer(this, null));
  }

  function test()
  {
    var TEST_NUM = 10000000;
    var layer = new MyLayer(this, primaryLayer);
    var value;
    {
      var begin = System.getTickCount();
      for(var i=0; i < TEST_NUM; i++)
        value = layer.superLeft;
      var end = System.getTickCount();
      Debug.message("value = layer.superLeft : "+(end-begin)+" ms");
    }
    {
      var begin = System.getTickCount();
      for(var i=0; i < TEST_NUM; i++)
        value = *(&global.Layer.left incontextof layer);
      var end = System.getTickCount();
      Debug.message("value = *(&global.Layer.left incontextof layer) : "
        +(end-begin)+" ms");
    }
    {
      var begin = System.getTickCount();
      for(var i=0; i < TEST_NUM; i++)
        value = (global.Layer incontextof layer).left;
      var end = System.getTickCount();
      Debug.message("value = (global.Layer incontextof layer).left : "
        +(end-begin)+" ms");
    }
    Debug.message("------");
    {
      var begin = System.getTickCount();
      var propLeft;
      for(var i=0; i < TEST_NUM; i++)
        propLeft = (&global.Layer.left incontextof layer);
      var end = System.getTickCount();
      Debug.message("propLeft = (&global.Layer.left incontextof layer) : "
        +(end-begin)+" ms");
    }
    {
      var begin = System.getTickCount();
      var propLeft = (&global.Layer.left incontextof layer);
      for(var i=0; i < TEST_NUM; i++)
        value = *propLeft;
      var end = System.getTickCount();
      Debug.message("value = *propLeft : "+(end-begin)+" ms");
    }
    {
      var begin = System.getTickCount();
      var globalLayer;
      for(var i=0; i < TEST_NUM; i++)
        globalLayer = (global.Layer incontextof layer);
      var end = System.getTickCount();
      Debug.message("globalLayer = (global.Layer incontextof layer) : "
        +(end-begin)+" ms");
    }
    {
      var begin = System.getTickCount();
      var globalLayer = (global.Layer incontextof layer);
      for(var i=0; i < TEST_NUM; i++)
        value = globalLayer.left;
      var end = System.getTickCount();
      Debug.message("value = globalLayer.left : "+(end-begin)+" ms");
    }
    invalidate layer;
  }
} 

var win = new TestWindow();
win.visible = true;
Debug.console.visible = true;
win.test();

実行結果。


value = layer.superLeft : 4892 ms
value = *(&global.Layer.left incontextof layer) : 2440 ms
value = (global.Layer incontextof layer).left : 2728 ms
------
propLeft = (&global.Layer.left incontextof layer) : 2414 ms
value = *propLeft : 414 ms
globalLayer = (global.Layer incontextof layer) : 1895 ms
value = globalLayer.left : 862 ms

下の4つはコンテキストを置き換える部分の速度と、
コンテキストを置き換えたオブジェクトを変数に格納してアクセスした場合の速度だ。

計測結果から次のことがわかる。

・incontextof 演算子は単にオブジェクトのコンテキストを置き換えるだけにすぎないので非常に高速
・別名のプロパティを介してsuperキーワードを使うよりは、incontextof 演算子を使う方がずっと速い
・一度コンテキストを置き換えてしまえば、さらに高速にアクセスできる
・プロパティオブジェクトを使う方法が最速
・複数のプロパティにアクセスする場合は、クラスオブジェクトを使う方法が効率的

とはいえこの速度差は大抵の場合問題にならないだろう。

まとめ

オーバーライドされたプロパティにアクセスする方法には、どの方法にも一長一短がある。
状況に合わせてうまく使い分けていきたい。


遮蔽処理の問題点

Posted 2013.05.08 in 吉里吉里

遮蔽の詳しい条件を確認していて、いくつか妙な点があることに気がついた。

だんだん小さくなる遮蔽矩形

遮蔽面が複数ある場合、それらの面によって作られる遮蔽領域は
より上のレイヤーからより下のレイヤーに向かうにつれて、だんだんと広がっていくはずである。
しかし遮蔽面によって作られる遮蔽矩形が重なった場合、
その交差をとって新たな遮蔽矩形とするので

遮蔽領域は逆にだんだん小さくなっていく。

さらに遮蔽面が重なっていない場合、その交差は空の矩形となるので遮蔽領域がいったん消滅し
次の遮蔽面から新たに遮蔽矩形が作成されるといった奇妙な動作になる。

この交差判定は、遮蔽領域の合成処理であると同時に
子レイヤーの遮蔽領域を、親レイヤーの領域内に制限するためのものだろう。
しかしそれらは本来別個の処理である。
また遮蔽領域を単一の矩形で管理していることがこの問題をややこしくしている。

不要な条件

「遮蔽される条件」として

・レイヤーとその親レイヤー全てが遮蔽有効条件
(レイヤーが可視であり、不透明であり、不透明時に透けない表示タイプである)を満たしている

があるが

この条件はおそらく不要である。

この条件は「遮蔽する条件」の一部でもあるので、
遮蔽レイヤーの親レイヤーは常にこの条件を満たし、遮蔽される。
しかしその兄弟レイヤーなどはこの条件を満たすとは限らない。
この条件により、本来遮蔽されうるはずのレイヤーが遮蔽されなくなってしまっている。

ちなみにレイヤーが遮蔽されないようにするトリックはこの条件を逆手に取ったものであり、
この条件がなくなると使えなくなる。

限定的な遮断

実際に UpdateExcludeRect を使った遮断が行われるのは、
子レイヤーと重なる領域に限定されている。
けれどもよくよく考えると、そのような限定はまったく必要がないように思える。
なぜわざわざそのように限定しているのか。

察するに、吉里吉里の遮蔽処理はおそらく本来

「親レイヤーを遮蔽する子レイヤーがある場合に
 親レイヤーの描画を省く」

ことのみを目的として作られた機能なのであろう。
だとすれば合点が行く。

しかし実際には、全てのレイヤーを考慮した遮蔽処理が行われている。
そのせいでいろいろ不整合が生じているものと思われる。

遮断の手抜き

tTJSNI_BaseLayer::CopySelf を見ると、UpdateExcludeRect が描画矩形を
上下にまたいでいない場合、遮断が行われないことがわかる。
言い換えると

遮断は横方向にしか行われない

ということである。

これは一見ひどい手抜きに思えるが、
実際には吉里吉里の画面描画の仕組みを考慮した設計だろう。

というのも、吉里吉里では画面を再描画する時、
画面領域を縦方向に細かく分割して描画が行われるからだ。
この領域を遮断によって縦方向にさらに分割すると、
かえってコストが高くついてしまう可能性が考えられる。
そこで敢えて”手抜き”をしているものと思われる。

ただし画面領域を分割するかどうかは設定で切り替えられるので、
分割しないように設定した場合は、
この”手抜き”によって遮蔽処理の効果が大幅に低下する可能性がある。

遮断のミス

tTJSNI_BaseLayer::CopySelf において、
UpdateExcludeRect が描画矩形の左辺をまたいだ状態を判定する次の条件───


else if(r.left >= uer.left && r.left < uer.left)

この条件は絶対に成立しない!

このため UpdateExcludeRect が描画矩形の左辺をまたいだ状態の時、遮断が行われない。
これは単純なミスだろう。
正しくは


else if(r.left >= uer.left && r.left < uer.right)

である。

まとめ

ここまで見てきたように、遮蔽処理にはかなりの癖がある。
レイヤーの type を ltOpaque にすると、遮蔽処理が行われ
遮蔽処理がもたらす難解な問題に遭遇する可能性があることに注意が必要だ。
最背面のレイヤーを ltOpaque にすることは得に問題ないだろうが、
手前に ltOpaque なレイヤーを作る場合は気をつけなくてはならない。

深みにはまる前に、根本的に ltOpaque の使用を避けるのも一つの方法だろう。
しかし遮蔽による描画量の削減はそれでもなお魅力的である。
敢えて ltOpaque を使い、遮蔽処理に由来する問題に遭遇した時、
遮蔽処理に関する知識がきっと問題解決の役に立つはずだ。


遮蔽の条件

Posted 2013.05.07 in 吉里吉里

以前に、遮蔽処理が行われるのは
「type==ltOpaque && opacity==255」
を満たす時であると述べたが、これはあくまで遮蔽処理の大前提となる条件であり
実際に遮蔽処理が行われるのはさらに限定的な状況だけである。

この条件については
「遮蔽する条件」
「遮蔽される条件」
「遮断を試行する条件」
の3つに分けて考えると考えやすい。

遮蔽する条件

以下の条件を全て満たす場合にレイヤーは遮蔽矩形を生成する。

・レイヤーとその親レイヤー全てが遮蔽有効条件
(レイヤーが可視であり、不透明であり、不透明時に透けない表示タイプである)を満たしている
・レイヤーが「type==ltOpaque && opacity==255」の条件を満たしている

より手前のレイヤーによって生成された遮蔽矩形が既にある場合は、
既存の遮蔽矩形と交差する矩形を新たな遮蔽矩形とする。

遮蔽される条件

以下の条件を全て満たす場合に
UpdateExcludeRect に遮蔽矩形が設定され、レイヤーは遮蔽される。

・より手前のレイヤーによって生成された遮蔽矩形がある
・レイヤーとその親レイヤー全てが遮蔽有効条件
(レイヤーが可視であり、不透明であり、不透明時に透けない表示タイプである)を満たしている

前回紹介したレイヤーが遮蔽されないようにするトリックは
この2つのめの条件を利用したものである。

遮断を試行する条件

上記2つの条件によって作成された UpdateExcludeRect の情報を元に描画を省くわけだが
その際にもさらに条件がある。

chached がtrueの時
・領域全体について遮断が試行される

chached がfalseの時
・可視の子レイヤーが1個以上あれば、
 子レイヤーと重なる領域について遮断が試行される
・可視の子レイヤーがなければ遮断は行われない
・子レイヤーと重ならない領域については遮断は行われない

また遮断の試行において、実際に遮断が行われるのは
UpdateExcludeRect が描画矩形に縦方向にまたがっている場合だけである。

遮蔽処理の実装

遮蔽処理の動作は、主に次の3つの関数に実装されている。

LayerIntf.cpp
tTJSNI_BaseLayer::QueryUpdateExcludeRect
tTJSNI_BaseLayer::Draw
tTJSNI_BaseLayer::CopySelf

これらの条件がどのように実装されているかはそちらを参照されたし。

まとめ

それにしても

ややこしい!

こうして条件を確認してみると、いくつか妙な部分があることに気がつく。
そのあたりの考察についてはさらに次回へ続く!


piledCopyと遮蔽

Posted 2013.05.06 in 吉里吉里

遮蔽処理にはさらに複雑な問題がある。

piledCopy によって起こる現象

遮蔽処理は Layer クラスの piledCopy 関数を実行した時にも行われる。
次の tjs スクリプトを実行してみて欲しい。


class TestWindow extends Window
{
  var layerGray;
  var layerBlack;
  var layerPiled;
  var timer;

  function TestWindow()
  {
    super.Window();
    setInnerSize(800, 600);

    // プライマリレイヤー
    add(new Layer(this, null));
    primaryLayer.setImageSize(innerWidth, innerHeight);
    primaryLayer.setSizeToImageSize();
    primaryLayer.fillRect(0, 0, innerWidth, innerHeight, 0xFFFFFFFF); // 白

    // 黒レイヤーを作る
    layerBlack = new Layer(this, primaryLayer);
    initLayer(layerBlack, 100, 100, 400, 400, 0xFF000000, 0, ltAlpha); // 黒

    // 黒レイヤーに紫の子レイヤーを作る
    var sub = new Layer(this, layerBlack);
    add(sub);
    initLayer(sub, 100, 100, 200, 200, 0xFFFF00FF, 0, ltAlpha); // 紫

    // タイマーを起動
    timer = new Timer(onTimer, "");
    timer.interval = 1000;
    timer.enabled = true;
  }

  function finalize()
  {
    invalidate layerGray;
    invalidate layerBlack;
    invalidate layerPiled;
    invalidate timer;

    super.finalize();
  }

  function initLayer(layer, x, y, w, h, color, absolute, type)
  {
    layer.left = x;
    layer.top = y;
    layer.setImageSize(w, h);
    layer.setSizeToImageSize();
    layer.fillRect(0, 0, w, h, color);
    layer.absolute = absolute;
    layer.type = type;
    layer.visible = true;
  }

  var step = 0;
  function onTimer()
  {
    switch(step)
    {
    case 0:
      // 灰色レイヤーを黒レイヤーの上に作る(遮蔽する)
      layerGray = new Layer(this, primaryLayer);
      initLayer(layerGray, 100, 100, 400, 400, 0xFF777777, 5, ltOpaque); // 灰
      break;

    case 1:
      // 黒レイヤーをpiledCopyしたレイヤーを一番上に作る
      layerPiled = new Layer(this, primaryLayer);
      initLayer(layerPiled, 100, 100, 400, 400, 0xFFFF0000, 100, ltAlpha);
      layerPiled.piledCopy(0, 0, layerBlack, 0, 0, 400, 400);
      break;
    }
    step++;
  }
}

var win = new TestWindow();
win.visible = true;

これを実行すると次のようになる。

KiriKiri_Exclude3_1

黒レイヤーを描画できていない!

本来次のようになるはずである。

KiriKiri_Exclude3_2

回避方法

この現象は遮蔽処理にまつわるものなのだが、
absolute の変更によって起きた問題とはまた原因が異なる。
UpdateExcludeRect の更新ができていないわけではない。
試しに次のように記述しても動作は何も変わらない。


    case 1:
      // 黒レイヤーをpiledCopyしたレイヤーを一番上に作る
      layerPiled = new Layer(this, primaryLayer);
      initLayer(layerPiled, 100, 100, 400, 400, 0xFFFF0000, 100, ltAlpha);

      layerBlack.opacity = 0;
      layerBlack.opacity = 255;
      layerPiled.piledCopy(0, 0, layerBlack, 0, 0, 400, 400);
      break;

この問題を回避するには次のようにする。


    case 1:
      // 黒レイヤーをpiledCopyしたレイヤーを一番上に作る
      layerPiled = new Layer(this, primaryLayer);
      initLayer(layerPiled, 100, 100, 400, 400, 0xFFFF0000, 100, ltAlpha);

      layerBlack.visible = false;
      layerPiled.piledCopy(0, 0, layerBlack, 0, 0, 400, 400);
      layerBlack.visible = true;
      break;

piledCopy 対象のレイヤーを不可視にしてから piledCopy するようにしている。
なぜこれで問題を回避できるのか…

原因

この問題の原因は、

遮蔽処理には常に全てのレイヤーが考慮される

という点にある。

黒レイヤーを piledCopy しているので、
遮蔽処理は黒レイヤー以下のレイヤーについてのみ考慮されるものと期待するだろうが
実際には黒レイヤーの兄弟レイヤーである灰色レイヤーも考慮される。

つまり灰色レイヤーによる遮蔽によって、
黒レイヤーの描画が省かれてしまったというわけだ。

回避方法の意味

では対象レイヤーを不可視にすると、なぜこの現象を回避できるのか。
この回避方法は

「レイヤーが遮蔽されるのは、
 レイヤーが可視の時だけである」

という条件を逆手に取ったものだ。

レイヤーを不可視にすることで、
レイヤーが他のレイヤーによって遮蔽されないようにできるのである。
対象レイヤーが不可視でも、piledCopy は実行できるので問題ない。

単純に、遮蔽している灰色レイヤーの方を不可視にしてもこの問題は回避できるが、
どのレイヤーが遮蔽しているのかわかりやすいケースばかりとは限らないので
対象を不可視にするというトリックは便利だろう。

まとめ

今回は遮蔽処理の条件を逆手にとって問題を回避したが
遮蔽処理が実施される条件は実際のところかなり複雑だ。

黒レイヤーの描画が省かれる一方で、その子レイヤーである紫レイヤーが
ちゃんと描画されている点について不思議に思ったかもしれない。
これもまた遮蔽処理の複雑な条件に理由がある。

詳しくは次回にでも~


遮蔽処理の仕組み

Posted 2013.05.06 in 吉里吉里

前回の記事では遮蔽処理にまつわる不可思議な現象を再現してみたが、
この現象を回避する方法は、現象の複雑さに対して実に簡単だ。

不具合の回避

onTimer関数に次の記述を追加してみよう。


  var step = 0;
  function onTimer()
  {
    switch(step)
    {
    case 0:
      // 黒レイヤーを上に移動(不具合発生)
      layerBlack.absolute = 10;
      break;
    case 1:
      // 無意味にopacityを変更する(不具合回避)
      layerBlack.opacity = 0;
      layerBlack.opacity = 255;
      break;
    }
    step++;
  }

一見無意味に思える処理だが、実際この処理を行うと正しい動作結果になる。
この回避方法の意味を知るには、吉里吉里の遮蔽処理の仕組みを理解する必要がある。

遮蔽処理の仕組み

全てのレイヤーはそれぞれ個別に UpdateExcludeRect(更新遮断矩形) という情報を持つ。
UpdateExcludeRect は、そのレイヤーが他のレイヤーによって遮蔽されている領域を表す。

UpdateExcludeRect の情報は、レイヤーの再描画時に利用される。
レイヤーの再描画範囲が UpdateExcludeRect に被る時、
再描画範囲は UpdateExcludeRect にって分割され
UpdateExcludeRect に含まれる領域の再描画が省かれる。
まさしく「更新を遮断する矩形」なのである。
これによって遮蔽されていてどうせ見えない領域の描画の無駄が削減される。

重要なのは UpdateExcludeRect の情報が更新されるタイミングである。
UpdateExcludeRect は再描画のたびに毎回更新されるわけではない。
UpdateExcludeRect の更新は、
再描画時に VisualStateChanged フラグが立っている時だけ行われる。
VisualStateChanged フラグは、遮蔽状態に変化がある操作が行われた時に立てられる。
よって遮蔽状態に変化がなければ UpdateExcludeRect は更新されない。

回避方法の意味

先の不具合は、この UpdateExcludeRect の情報が正しく更新されないことによって起こる。
absolute の操作によって遮蔽状態が変化するにもかかわらず、
VisualStateChanged フラグが立てられていないからである。

この不具合を回避するには、立てられていない VisualStateChanged フラグを立ててやればよい。
opacity を操作すれば VisualStateChanged フラグが立てられるので、
opacity を無意味に変更すれば VisualStateChanged フラグを間接的に立てることができる。
これにより不具合を回避することができるのである。

よりスマートな回避方法

問題のあるプロパティをオーバーライドしておくと
プロパティを操作する度にいちいち気を遣う必要がなく便利だろう。
把握している限りでは order, type プロパティにも同様の問題があるので、そちらも対処しておく。


class OcclusionLayer extends Layer
{
  function OcclusionLayer(window, parent)
  {
    super.Layer(...);

    // 遮蔽するように設定
    type = ltOpaque;
  }

  // VisualStateChanged フラグを間接的に立てる
  function notifyVisualStateChanged()
  {
    var temp = opacity;
    opacity = !opacity;
    opacity = temp;
  }

  property absolute
  {
    setter(v){ super.absolute = v; notifyVisualStateChanged(); }
    getter(){ return super.absolute; }
  }

  property order
  {
    setter(v){ super.order = v; notifyVisualStateChanged(); }
    getter(){ return super.order; }
  }

  property type
  {
    setter(v){ super.type = v; notifyVisualStateChanged(); }
    getter(){ return super.type; }
  }
}

まとめ

実はこの absolute の問題は、わたなべごうさんに修正していただき
最新の開発版の吉里吉里では修正されている。
今回紹介した回避方法は、古いバージョンの吉里吉里を使っている場合に
この問題に遭遇した時の応急策として使えるだろう。


遮蔽処理の不具合

Posted 2013.05.06 in 吉里吉里

吉里吉里では、かなり限定された状況で遮蔽処理が行われるが、
その遮蔽処理には、いくつか問題が潜んでいる。
遮蔽処理にまつわる問題は非常に複雑怪奇な現象を引き起こし
とにかく原因を特定しづらい。

不可思議な動作

まずは次のtjsスクリプトを実行して見て欲しい。


class TestWindow extends Window
{
  var layerGray;
  var layerBlack;
  var timer;

  function TestWindow()
  {
    super.Window();
    setInnerSize(800, 600);

    // プライマリレイヤー
    add(new Layer(this, null));
    primaryLayer.setImageSize(innerWidth, innerHeight);
    primaryLayer.setSizeToImageSize();
    primaryLayer.fillRect(0, 0, innerWidth, innerHeight, 0xFFFFFFFF); // 白

    // 灰色レイヤーを上に作る
    layerGray = new Layer(this, primaryLayer);
    initLayer(layerGray, 50, 50, 400, 400, 0xFF777777, 5, ltOpaque); // 灰
    
    // 黒レイヤーを下に作る
    layerBlack = new Layer(this, primaryLayer);
    initLayer(layerBlack, 100, 100, 400, 400, 0xFF000000, 0, ltOpaque); // 黒
    
    // 黒レイヤーに子レイヤーを作る
    var sub = new Layer(this, layerBlack);
    add(sub);
    initLayer(sub, 100, 100, 200, 200, 0xFFFF00FF, 0, ltAlpha); // 紫
    sub.opacity = 128; // 半透明
    
    // このタイミングで黒レイヤーを上に移動させると正しい動作結果になる
    // layerBlack.absolute = 10;
    
    // タイマーを起動
    timer = new Timer(onTimer, "");
    timer.interval = 1000;
    timer.enabled = true;
  }

  function finalize()
  {
    invalidate layerGray;
    invalidate layerBlack;
    invalidate timer;

    super.finalize();
  }

  function initLayer(layer, x, y, w, h, color, absolute, type)
  {
    layer.left = x;
    layer.top = y;
    layer.setImageSize(w, h);
    layer.setSizeToImageSize();
    layer.fillRect(0, 0, w, h, color);
    layer.absolute = absolute;
    layer.type = type;
    layer.visible = true;
  }

  var step = 0;
  function onTimer()
  {
    switch(step)
    {
    case 0:
      // 黒レイヤーを上に移動(不具合発生)
      layerBlack.absolute = 10;
      break;
    }
    step++;
  }
}

var win = new TestWindow();
win.visible = true;

これを実行すると次のような動作結果になる。(version2.32 rev2 で確認)

KiriKiri_Exclude1_1

おかしい動作結果なのだが、何がおかしいかわかるだろうか?

紫色が明るすぎる!

のである。
次の部分のコメントアウトを外すと、正しい動作結果になるので確認してみよう。


    // このタイミングで黒レイヤーを上に移動させると正しい動作結果になる
    // layerBlack.absolute = 10;

KiriKiri_Exclude1_2

紫色が暗くなった!
opcity=128なのだから、この程度の色合いが正しい。

さらに不可思議な動作

子レイヤーを作る部分を次のように書き換えると、さらに現象がわかりやすくなる。


    // 黒レイヤーに子レイヤーを作る
    for(var x=0; x<10; x++)
    {
      for(var y=0; y<10; y++)
      {
        var sub = new Layer(this, layerBlack);
        add(sub);
        initLayer(sub, 50+x*30, 50+y*30, 8, 8, 0xFFFF00FF, 0, ltAlpha); // 紫
        sub.opacity = 128; // 半透明
      }
    }

子レイヤーとして紫の四角をたくさん作成するようにしただけだ。
これを実行すると次のようになる。

KiriKiri_Exclude2_1

背後の灰色が透けてしまっている!

コメントアウトをはずすとやはり正しい動作になるので確認しておこう。

KiriKiri_Exclude2_2

何がおきているのか

この不可思議な現象は、遮蔽処理にまつわる問題によって引き起こされている。
遮蔽処理にミスが生じ、実際には遮蔽していないにもかかわらず、
遮蔽による最適化が行われてしまっているのである。

この現象の回避は、遮蔽処理の存在を知らないと非常に困難だろう。
詳しい原因の追求と回避方法についてはまた次回~