大気の流れというものを、太陽からの輻射熱、地表の温度上昇、熱の伝播、暖気は冷気より軽い、重い空気は落下し…大気は流れる、というロジックで動かしてみたいと思います。
・地表ブロックの熱量増加
先ほど追加した SurfaceWork 関数を実装して、輻射熱係数をたよりに heat を上昇させたり、下降させたりしてみます。
テストとして、最高温度に達したら空気ブロックにマテリアル変換を起こすロジックを作ってみました。すると…
こんな感じで昼の間に表面の土ブロックだけが消失している絵ができました。
動画はこちら
VIDEO www.youtube.com
今のままだと太陽光を受け、熱が上昇し続け、あっというまに最高温度に到達するわけですが、これは物理現象としてあり得ません。(地上で生きていけない…)
本来物質は固体の状態で熱が上がると、自身もその熱に応じて輻射熱を周囲へ放射します。
熱が上がれば輻射熱量もどんどん高くなるため、ある程度熱くなってくると、なかなか熱くならなくなるわけです。
しかし、残念ながら現在の計算スピードを考えると、周囲ブロックへ放射熱が伝播するところまで計算していられないので、ここは乱数に頼ってみようと思います。
低温状態の地表ブロックに太陽からの輻射熱を与えると高い確率で地表ブロックの熱が上昇しますが、熱量が高まるにつれて上昇の確率が下がっていくという仕組みを導入します。
熱量が上昇すると、徐々に熱の上昇確率が減っていき、例えば以下のようなグラフの曲線はどうでしょうか?
こちらは 0.98 を熱量の値で累乗した結果です。
最初は 0 乗なので 100%, 255の値付近では 0.5% です。
チャンクのブロック処理の頻度はそこまで高くないため、だいたい100の値を越えたあたりで、ほぼ熱量の上昇はしないという形になります。
次に太陽光を受け取らなくなった時に、徐々に放熱しなければなりませんので、これについては次のように、高い温度だった場合に周囲に高い熱量を放射する…
いや、これだけで高い温度に到達しずらい仕組みが作れそうですね。
ある角度で太陽光が当たったなら、その角度に応じた熱がブロックに加算され、比熱の値がブロックのマテリアルごとに決まっていて、その値で割った値が上昇する温度
しかし、温度に応じで周囲へ放射する量が決まっていて、太陽光の放射量がそれを越えない限りそこからの温度上昇は起きないという形です。
これなら太陽の力が強い時はしっかりとそれに応じた地表温度というものに収束します。(漸近)
テストシーンにこの仕組みを適用すれば、ブロックは地上から干上がることはなくなりました。
最終的に落ち着いたアイディアとして、太陽の日照密度というものを 0~1 の値で表現するようにして、単純に日の出の瞬間に0から始まり sin 波として太陽が直上に来た時に 1.0、日が沈むころに 0 となり、夜間はすべて 0 となる値を地表ブロックは受け取れるようにしました。
そこに比熱となる要素をかけることにします。
この比熱の値は light の値の範囲に気を付けないと、永遠に地表は温まらないことになるので、後程調整が必要です。(byte で温度表現のつらいところですね)
public virtual BlockDataSt SurfaceWork(BlockDataSt blockData, ref float light, IHexWorldUtility worldUtility)
{
if (1.0f > lightTransmittance)
{
float addHeat = ((1.0f - lightTransmittance) * light) / specificHeat;
float nextHeat = blockData.heat + addHeat;
float radiantHeat = (float )Math.Pow(radiantHeatBase, nextHeat);
blockData.heat = (byte )Math.Min(Math.Max(nextHeat - radiantHeat, 0 ), byte .MaxValue);
light = lightTransmittance * light;
}
return blockData;
}
今後、シミュレーション結果を通じて、各ブロックの放射熱量、比熱の全体スケールというものを調整していきたいと思います。
・地表ブロックに触れている空気へ熱が移動
まず、私は気象学について無知でしたので次の入門記述を読みました。
気象学超入門
要はここに書いてある、上昇気流が低温化して雲を発生させるメカニズムというものを SimpleHexWorld (マイクロワールド)で実現しようとしています。
これまでの構想を経て、現在空気ブロックには熱と圧力の値が byte で詰められています。
この二つの値のうち、今は熱を動かそうとしていますが、まずは空気ブロックに標高に応じた圧力の値を詰めるべきと考えました。
単純に最も上空の空気は熱 0 / 255, 圧力 0 / 255 という宇宙(space)ブロックで固定し、宇宙に熱(分子の運動量)を逃がし続ける存在とします。
初期値としては地上に向かって降りるにしたがって圧力を 1 ずつ増やしていき、チャンクの最深部に到達する頃には圧力が 255 / 255 になる形で初期値を打ちます。
圧力の伝播も熱の伝播と同様にベースとなるロジックを書く必要があると思いました。
では、次に熱の伝導の部分を詰めていきましょう。
熱の伝導において、与える熱量と温まりやすさという度合いが重要になります。(熱伝導量と比熱ですね)
空気ブロックに、乾燥断熱減率の概念を導入しようと思います。
圧力が低くなるにつれて、heat が一定の割合で低くなっていくという現象です。(開放系の気体の振る舞いですが、圧力が異なる気体の中の分子の動きを脳内でイメージするの、難しいんですよね)
上空は低密度ですので、単純に単位ブロックあたりの質量は小さくなります。
大きい質量と小さい質量の気体を重力場 に置けば、流動結果として大きい質量の方が重力方向の先に移動します。
地球に住む人間はこれを上昇ととらえているみたいですね。
上昇下降については単位ブロックあたりの質量を見て、大きい方を常に重力方向に配置するように動かせばよいのです。
太陽の光によって、だいたい地表の温度は 0~255 の値を行き来します。
自身を含めた周囲の温度の平均を自身の温度にするようにしてみます。
まずはこれで地表付近の空気ブロックから温まり始め、徐々に熱は上空に移動し始めます。
空気は放射する計算を行わせていないので、夜になると地表より空気の方が暖かくなり、地面によって空気は冷やされることになります。
夜は上空の大気の方が暖かくなる可能性があります。
もちろん地上も空気の熱伝導によって温まりますが、空気と同じように温度を混ぜ合わせることはできません。
例えば、土ブロックの周囲に空気ブロックがある場合、周囲の空気ブロックが 100 ℃だったとして、土ブロックが0℃、このとき周囲8ブロック+自身1ブロックの平均88℃まで土ブロックは上昇しません。
温まりやすさというものがあったとして、…つまり比熱ですね。
このブロックの heat の値を絶対にするか相対にするかで悩みましたが、絶対にすると熱伝導の高低の判別計算が楽ですので、絶対にします。(世界の熱分解能は低いですね…)
分解能の低さは丸め誤差 による情報の欠損です。あまりにもエネルギーが保存されない世界はそれはそれで作る意味がありません。
そこで!
熱の移動について、ここに確率を取り入れてみるのはどうでしょうか?
比熱比で確率計算する感じで、具体的には、ブロックの周囲のheat値を確認し、もし自分が高い場合は、相手に熱を +1 する、しかしそこに確率が生じていて
土から空気の場合は比熱比が1以上になるので確実に +1 、つまり 100% 伝導し、移動した熱量だけ自分も -1 するべきところ、しかし、そこに確率が生じていて、比熱比が 0.01 となっているので1%で自身の heat を-1します。
次に、もし自分が低い場合は、空気ブロックの heat を -1 して、土のブロックの heat を +1 しますが、これも先ほどの確率計算で 100% と 1% の値を使って、周囲の空気は冷えやすく、土ブロックは温まりづらいという現象が実現でき、さらに丸め誤差 による情報欠損が多少なり改善されます。(この低分解能な世界に必要な表現です、なかなか良いアイディアを思いついたと思っています。)
先ほどの空気と土が互いに88℃になる現象は、100%と1% の確率を使えば、期待値として7.4℃になるわけです。(土ブロックは空気ブロック100個分の熱エネルギーを蓄えられると考える)
こういう話、密度と比重と体積について、みなさん勉強してきたと思います。
ですが、このマイクロワールドにおいて、体積は固定されたブロックの単位でのみ存在しますので、考えるべきは密度と比重をかけた値、つまりブロックの質量のみを考えるだけでうまくいきます。
また、熱伝導率という概念を導入して、先ほど +1 ずつ値を変化させましたが、高い熱伝導率を持つような金属の場合はもっと高速に heat のやり取りを行うべきです。(温度差が 1 なのに 10も移動してはおかしいので、そういう場合は比熱比を確認して 確率を上昇させてから +1 にするなどですね、これもいいアイディアを思いついたと思いました。)
整理するとマテリアルのロジックは次の定数を持つべきということです。
public float specificHeat { get; protected set; }
public float lightTransmittance { get; protected set; }
public float radiantHeatBase { get; protected set; }
public float thermalConductivity { get; protected set; }
また実装で詰まってきた…チャンクを越えられない…
チャンク内はスレッド内のローカル変数を書き換えるだけなので問題ありませんが、チャンクをまたいだ編集を行おうとすると他のスレッドからアクセスされないようにロックする必要が…
これではまたパフォーマンスが落ちてしまうので、悩みます。
一つアイディアが沸いたのですが、スレッド内のローカル変数にチャンクに接している周辺8つのチャンクのためのローカル変数を用意しておき
仕事中はスレッド内の変数を更新するのみとして、処理後にロックが必要ならロックして書き換えるというのはどうでしょうか?
これもパフォーマンスは悪くなりそうですが、仕方ないですよね。
作ってみます。(処理が重くなるのこえぇ)
デバッグ 風景
先ほどのアイディアを具体的にすると次の通り(このポッと具体化するのが大変なんですよね…頭使う、まぁ問題を解決するために手段を具体化して簡単な作業の組み合わせに変えるのが仕事なんですけどね)
lock (LockObject)
{
if (null != BlockData)
{
for (int copyWidthIndex = 0 ; copyWidthIndex < Width; copyWidthIndex++)
{
Array.Copy(BlockData, Depth * Height * copyWidthIndex, localBlockData, (Depth + 2 ) * Height * (copyWidthIndex + 1 ) + Height, Depth * Height);
}
}
}
{
Chunk chunk_mm = World.Instance.ChunkData[World.Instance.RemapLongitude(PositionX, -1 )][World.Instance.RemapLatitude(PositionZ, -1 )];
lock (chunk_mm.LockObject)
{
if (null != chunk_mm.BlockData)
{
Array.Copy(chunk_mm.BlockData, Width * Depth * Height - Height, localBlockData, 0 , Height);
}
}
}
{
Chunk chunk_pm = World.Instance.ChunkData[World.Instance.RemapLongitude(PositionX, +1 )][World.Instance.RemapLatitude(PositionZ, -1 )];
lock (chunk_pm.LockObject)
{
if (null != chunk_pm.BlockData)
{
Array.Copy(chunk_pm.BlockData, (Depth - 1 ) * Height, localBlockData, (Width + 1 ) * (Depth + 2 ) * Height, Height);
}
}
}
{
Chunk chunk_pp = World.Instance.ChunkData[World.Instance.RemapLongitude(PositionX, +1 )][World.Instance.RemapLatitude(PositionZ, +1 )];
lock (chunk_pp.LockObject)
{
if (null != chunk_pp.BlockData)
{
Array.Copy(chunk_pp.BlockData, 0 , localBlockData, (Width + 2 ) * (Depth + 2 ) * Height - Height, Height);
}
}
}
{
Chunk chunk_mp = World.Instance.ChunkData[World.Instance.RemapLongitude(PositionX, -1 )][World.Instance.RemapLatitude(PositionZ, +1 )];
lock (chunk_mp.LockObject)
{
if (null != chunk_mp.BlockData)
{
Array.Copy(chunk_mp.BlockData, (Width - 1 ) * Depth * Height, localBlockData, (Depth + 1 ) * Height, Height);
}
}
}
{
Chunk chunk_zm = World.Instance.ChunkData[World.Instance.RemapLongitude(PositionX, 0 )][World.Instance.RemapLatitude(PositionZ, -1 )];
lock (chunk_zm.LockObject)
{
if (null != chunk_zm.BlockData)
{
for (int copyWidthIndex = 1 ; copyWidthIndex <= Width; copyWidthIndex++)
{
Array.Copy(chunk_zm.BlockData, Depth * Height * copyWidthIndex - Height, localBlockData, (Depth + 2 ) * Height + ((Depth + 2 ) * Height * (copyWidthIndex - 1 )), Height);
}
}
}
}
{
Chunk chunk_zp = World.Instance.ChunkData[World.Instance.RemapLongitude(PositionX, 0 )][World.Instance.RemapLatitude(PositionZ, 1 )];
lock (chunk_zp.LockObject)
{
if (null != chunk_zp.BlockData)
{
for (int copyWidthIndex = 1 ; copyWidthIndex <= Width; copyWidthIndex++)
{
Array.Copy(chunk_zp.BlockData, (Depth * Height) * (copyWidthIndex - 1 ), localBlockData, (Depth + 2 ) * Height * (copyWidthIndex + 1 ) - Height, Height);
}
}
}
}
{
Chunk chunk_mz = World.Instance.ChunkData[World.Instance.RemapLongitude(PositionX, -1 )][World.Instance.RemapLatitude(PositionZ, 0 )];
lock (chunk_mz.LockObject)
{
if (null != chunk_mz.BlockData)
{
Array.Copy(chunk_mz.BlockData, (Depth * Height) * (Width - 1 ), localBlockData, Height, Depth * Height);
}
}
}
{
Chunk chunk_pz = World.Instance.ChunkData[World.Instance.RemapLongitude(PositionX, +1 )][World.Instance.RemapLatitude(PositionZ, 0 )];
lock (chunk_pz.LockObject)
{
if (null != chunk_pz.BlockData)
{
Array.Copy(chunk_pz.BlockData, 0 , localBlockData, (Depth + 2 ) * Height * (Width + 1 ) + Height, Depth * Height);
}
}
}
lock (LockObject)
{
if (null != BlockData)
{
for (int copyWidthIndex = 0 ; copyWidthIndex < Width; copyWidthIndex++)
{
Array.Copy(localBlockData, (Depth + 2 ) * Height * (copyWidthIndex + 1 ) + Height, BlockData, Depth * Height * copyWidthIndex, Depth * Height);
}
}
}
では、テストとしてあるブロックを用意します。
見た目で分かるようにオレンジ色のブロックにしました。
ロジックは次の通り、周囲ブロックを自分と置き換える増殖ロジックとしました。
System.Random rand = HexWorldUtility.RandomProvider.GetThreadRandom();
if (1 == rand.Next(0 , 30 ))
{
BlockDataSt localBlock = BlockDataSt.AirBlock;
for (int sideIndex = 0 ; sideIndex < 6 ; sideIndex++)
{
int sideWidthIndex = widthIndex;
int sideDepthIndex = depthIndex;
worldUtility.GetSideBlockIndex(widthIndex, depthIndex, sideIndex, out sideWidthIndex, out sideDepthIndex);
for (int nextHeight = 0 ; nextHeight < Chunk.Height; nextHeight++)
{
localBlock.data = localBlockData[sideWidthIndex * (Chunk.Depth + 2 ) * Chunk.Height + sideDepthIndex * Chunk.Height + nextHeight];
if (MaterialID.Material_Opacity > localBlock.materialId)
{
localBlockData[sideWidthIndex * (Chunk.Depth + 2 ) * Chunk.Height + sideDepthIndex * Chunk.Height + nextHeight - 1 ] = blockData.data;
break ;
}
}
}
blockData.materialId = MaterialID.Earth;
}
return blockData;
結果がこちら
うわ、増えていってます。
確認したかったのはチャンクの境で情報が遮断されずに、しっかりと情報が届き、処理されていくかという点です。
最後にテストとして、ブロックがある方角へ進み続けるというロジックを組んでみます。
走り出しました。(消えることがあったので、やはりチャンクの境で他の処理と重なってブロックが消されることはありそうですね)
さて、メッシュの更新フラグの立て方について調整が必要なことがわかりました。
自身のマテリアルIDが変わったときを、これまでトリガーとしていましたが、周囲のマテリアルIDが変わったときにもそれを通知する必要があるためです。
これについてはロジック内の現象なので、外部からいちいちサポートできませんが、ユーティリティ関数を用意して、ローカルブロック情報のどこを変更したかを記録することとします。
しかし、これもスレッドごとにまとまってしょりしなければならない値なので、周囲8ブロック + 自身 1ブロック分のレイヤー更新フラグを所持します。
デバッグ を続けて、ひとまず形になりました。
VIDEO www.youtube.com
話は戻って熱伝導ですが、ブロックのロジックから他のブロックのロジックを手に入れることができないので、できる仕組みを考えて作ります。
素案としては、こんな感じ
protected void HeatConduction(int widthIndex, int depthIndex, int heightIndex, ulong [] chunkBlockData, BlockDataSt blockData, IHexWorldUtility worldUtility)
{
float removeHeat = 0 ;
BlockDataSt localData = BlockDataSt.AirBlock;
for (int heightOffset = 1 ; heightOffset >= -1 ; heightOffset--)
{
if (0 == heightOffset)
continue ;
int topBottomBlockIndex = GetDataArrayIndex(widthIndex, depthIndex, heightIndex + heightOffset);
localData.data = chunkBlockData[topBottomBlockIndex];
_MoveHeat(ref blockData, ref localData, ref removeHeat, ref worldUtility);
chunkBlockData[topBottomBlockIndex] = localData.data;
}
for (int sideIndex = 0 ; sideIndex < 6 ; sideIndex++)
{
int sideWidthIndex = widthIndex;
int sideDepthIndex = depthIndex;
GetSideBlockIndex(widthIndex, depthIndex, sideIndex, out sideWidthIndex, out sideDepthIndex);
int sideBlockIndex = GetDataArrayIndex(sideWidthIndex, sideDepthIndex, heightIndex);
localData.data = chunkBlockData[sideBlockIndex];
_MoveHeat(ref blockData, ref localData, ref removeHeat, ref worldUtility);
chunkBlockData[sideBlockIndex] = localData.data;
}
float subHeat = Math.Max(removeHeat, 1 );
Random random = worldUtility.GetThreadRandom();
if (removeHeat > random.NextDouble())
blockData.heat = (byte )Math.Max(blockData.heat - subHeat, 0 );
}
private void _MoveHeat(ref BlockDataSt blockData, ref BlockDataSt targetBlockData, ref float removeHeat, ref IHexWorldUtility worldUtility)
{
if (blockData.heat > targetBlockData.heat)
{
BaseBlockLogic targetLogic = worldUtility.GetBaseLogic(targetBlockData.materialId);
if (null != targetLogic)
{
float addHeat = Math.Min(targetLogic.thermalConductivity, blockData.heat - targetBlockData.heat);
targetBlockData.heat = (byte )Math.Min(targetBlockData.heat + addHeat, byte .MaxValue);
removeHeat += addHeat * (targetLogic.heatUpEnergy / heatUpEnergy);
}
}
}
private void _SetLayerUpdatedFlag(bool [][] layerUpdatedFlags, int sideIndex, int heightIndex)
{
for (int heightOffset = -1 ; heightOffset < 2 ; heightOffset++)
{
layerUpdatedFlags[sideIndex][Math.Min(Math.Max((heightIndex + heightOffset) / Chunk.LayerHeight, 0 ), Chunk.LayerCount - 1 )] = true ;
}
}
それから色々と調整して、次の形にしました。
protected BlockDataSt HeatConduction(int widthIndex, int depthIndex, int heightIndex, ulong [] chunkBlockData, BlockDataSt blockData, IHexWorldUtility worldUtility)
{
Random random = worldUtility.GetThreadRandom();
float removeHeat = 0 ;
BlockDataSt localData = BlockDataSt.AirBlock;
for (int heightOffset = -1 ; heightOffset <= 1 ; heightOffset++)
{
if (0 == heightOffset)
continue ;
int topBottomBlockIndex = GetDataArrayIndex(widthIndex, depthIndex, heightIndex + heightOffset);
localData.data = chunkBlockData[topBottomBlockIndex];
_MoveHeat(ref blockData, ref localData, ref removeHeat, ref worldUtility);
chunkBlockData[topBottomBlockIndex] = localData.data;
}
for (int sideIndex = 0 ; sideIndex < 6 ; sideIndex++)
{
if (1 == random.Next(0 , 2 ))
continue ;
int sideWidthIndex = widthIndex;
int sideDepthIndex = depthIndex;
GetSideBlockIndex(widthIndex, depthIndex, sideIndex, out sideWidthIndex, out sideDepthIndex);
int sideBlockIndex = GetDataArrayIndex(sideWidthIndex, sideDepthIndex, heightIndex);
localData.data = chunkBlockData[sideBlockIndex];
_MoveHeat(ref blockData, ref localData, ref removeHeat, ref worldUtility);
chunkBlockData[sideBlockIndex] = localData.data;
}
if (1.0f > removeHeat)
{
if (removeHeat > random.NextDouble())
blockData.heat = (byte )Math.Max(blockData.heat - 1 , 0 );
}
else
{
blockData.heat = (byte )Math.Max(blockData.heat - removeHeat, 0 );
}
return blockData;
}
private void _MoveHeat(ref BlockDataSt blockData, ref BlockDataSt targetBlockData, ref float removeHeat, ref IHexWorldUtility worldUtility)
{
if (blockData.heat > targetBlockData.heat)
{
BaseBlockLogic targetLogic = worldUtility.GetBaseLogic(targetBlockData.materialId);
if (null != targetLogic)
{
float addHeat = Math.Max(targetLogic.thermalConductivity, 1 );
Random random = worldUtility.GetThreadRandom();
if (targetLogic.thermalConductivity > random.NextDouble())
{
targetBlockData.heat = (byte )Math.Min(targetBlockData.heat + addHeat, byte .MaxValue);
float ratio = specificHeat / targetLogic.specificHeat;
removeHeat += addHeat / ratio;
}
}
}
}
リアルタイムの熱伝導の様子をビジュアライズする機能を追加してデバッグ すると次の通り
VIDEO www.youtube.com
デバッグ 目的で大気の温度を最大にしてチェックしていますが、地上と宇宙に接している部分から熱が逃げていき、徐々に大気が冷えている様子がうかがえます。