遠い地震で高層ビルだけが大きく揺れるのは、周期が合うから

2011年3月11日、震源からおよそ770 km離れた大阪で、超高層の庁舎の最上階が片側137 cm揺れ、揺れは約10分続いた。地面の揺れの周期と、建物の揺れやすい周期がほぼ一致したとみられている。

記事

遠い地震で高層ビルだけが大きく揺れるのは、周期が合うから

2011年3月11日、震源からおよそ770 km離れた大阪で、超高層の庁舎の最上階が片側137 cm揺れ、揺れは約10分続いた。地面の揺れの周期と、建物の揺れやすい周期がほぼ一致したとみられている。

作成日 2026年11月4日・更新日 2026年11月4日・執筆・監修 佐々木 航

現象

770 km離れた大阪で、最上階は片側137 cm動いた

2011年3月11日の14時49分ごろ、東北地方太平洋沖地震の揺れが大阪湾岸の大阪府咲洲庁舎に届いた。震源からはおよそ770 km。高さ256 mの建物は約10分揺れ続け、最上階の52階は短辺方向に片側137 cm動いた1。18階は30 cmで、上へ行くほど揺れが大きい。

内装や防火戸の損傷は360か所にのぼり、エレベーターは32基すべてが止まった。それでも抜き取り検査で、柱や梁に損傷は見つかっていない。大阪府は、超高層建物そのものが倒壊するおそれはほとんどないと説明している。

なぜ、遠くの地震で高いビルがこれほど揺れるのか。建物には揺れやすい周期があり、地面の揺れの周期がそれに合うと、揺れが少しずつ大きくなるからだ。ブランコを同じ間合いで押し続けると、弱い力でも振れ幅が大きくなるのと変わらない。しかも周期の長い地面の揺れは弱まりにくく、遠くまで届く1。

画面の3棟は同じ地面に建っている。地面は周期4.2秒でゆっくり往復していて、これは40階の揺れやすい周期と同じだ。5階と15階はほとんど動かないのに、40階だけが1分ほどかけて大きく揺れていく。建物をドラッグして離すと、その建物の周期で揺れ続ける。

仕組み

建物は、重りとばねの列になる

建物の重さの大部分は床にあり、柱と壁は床どうしをつないで元の形へ戻そうとする。そこで床を重り、1階分の柱と壁をばねとみなすと、建物は重りとばねを交互につないだ列になる。

揺れている建物 階の高さ ずれの差 層間変形角 = ずれの差 ÷ 階の高さ 置き換える 重りとばねの列 m m m m k c 床の重さ 層のばねと減衰 地面の加速度 ag
揺れている建物を、床の重さと層のばね、減衰の列に置き換える。層間変形角は、上下の階のずれの差を階の高さで割った値

重りとばねが1組だけなら、1往復にかかる時間(秒)は重さ(kg)とばねの固さ(N/m)で決まる。

重いほど、柔らかいほど、ゆっくり揺れる。外から力を加えずに揺らしたときのこの周期を、固有周期と呼ぶ。高い建物は多くの重りを多くのばねで支える形なので、階が増えるほど周期は延びる。設計に使う周期の略算は、建築基準法の告示が高さ(m)の式で定めている2。

は、鉄骨造や木造の階が高さに占める割合である。鉄筋コンクリート造なら0、鉄骨造なら1。高さ140 mの鉄骨造は4.2秒、高さ17.5 mなら0.5秒あまり。

地面が急に動くと、上の階は置いていかれる。つまり地面から見れば、各階を反対向きに押す力が働いたのと同じだ。各階のずれをとすると、運動の式はこう書ける。

は各階の重さ、は揺れを弱める減衰、は層の固さ、は地面の加速度を表す。右辺が「床の重さ × 地面の加速度」の力で、どの階にも同じ向きに働く。

階が個あれば、固有周期も個ある。いちばん長い1次の周期では建物全体が同じ向きにしなり、2次では途中に動かない階(節)が1つ、3次では2つできる。下の図の右の建物は、地面を指で動かせる。1次の周期で往復させると、弱い動きでも揺れが大きくなる。

仕組み

周期が合うと、揺れは押すたびに増す

同じ周期で揺らし続けると、建物はどこまで揺れるのか。重りとばねが1組なら、地面から見た揺れ幅と地面の揺れ幅の比は次の式になる。は固有周期を地面の周期で割った値、は減衰定数(揺れの弱まりやすさを表す数)だ。

地面の周期が建物よりずっと長ければ、比は0に近い。建物は地面と一緒に動くだけで、ほとんど変形しない。長周期の揺れを受ける低い建物がこれにあたる。反対に地面の周期がずっと短ければ、比は1に近づく。地面だけが動き、上の階はその場に残る。

問題は周期が合ったときだ。では比がになり、が3%なら約17倍に達する。シミュレーションの多層の建物では、最上階の比が約22倍まで上がる。地面が±3.3 cm動くと、最上階は地面から±73 cmずれる。

ただし、一瞬では大きくならない。揺れ幅が最終の値の6割あまりに届くまでの時間は、およそである。が3%なら、周期4.2秒で約22秒、周期0.5秒で3秒ほど。地面が止まった後も、同じ時間をかけてようやく4割ほどに減る。高いビルの揺れが長く続くのはこのためだ。

建物の傷みの目安には、上下の階のずれの差を階の高さで割った層間変形角を使う。建築基準法施行令は構造計算で原則1/200以内の確認を求め、超高層の設計では1/100が通常の目安とされてきた3。

高いビルほど大きく傾くように思えるが、地面の最大速度をそろえて計算すると話が変わる。地面の揺れ幅はなので、共振したときの最上階の揺れは周期に比例する。周期は高さに比例するから、高さで割った変形の割合は建物の高さによらない。共振すれば、低い建物も同じ割合で変形するのだ。遠い地震で高いビルだけが大きく揺れるのは、遠くまで届く揺れの周期に、高いビルの周期が重なるからである。

仕組み

周期をずらすか、重りで引き戻すか

揺れを減らす方法は2つに分かれる。免震は建物の下に柔らかい層を入れ、建物全体の周期を延ばす。一般的な免震建物の設計周期は3〜5秒で4、周期の短い揺れを受けても上の建物はその場に残る。

もう1つの制振には、柱のあいだのダンパーで揺れの勢いを吸う方法と、屋上の重りで建物を引き戻す方法がある。後者を同調質量ダンパーと呼ぶ。固有周期に近い周期の重りをばねでつなぐと、重りは建物より遅れて大きく振れ、建物を逆向きに引く。重りの重さと、1次の揺れに効く建物の重さの比をとすると、周期はの倍にするとよい5。

そのまま 地面の揺れが層に伝わり 上の階ほど大きくずれる 免震 ゴムの層が大きく変形し 上の建物はほとんど変形しない 制振(重り) 重りが遅れて揺れ 建物を逆向きに引き戻す
そのままの建物、免震の建物、屋上の重りで制振する建物の揺れ方の違い

反対に、変形を集めてしまう形もある。ばねの列の1か所だけが柔らかいと、変形はそこに集まる。1階が駐車場などで壁の少ないピロティ形式がこれにあたる。1階の柱が力の上限(降伏)に達すると、変形はさらに1階へ集中する。

試す

3棟を同じ揺れにかけて比べる

共振曲線の縦軸は、最上階のずれが地面の揺れ幅の何倍かを表す。

  1. 周期4.2秒・最大速度5 cm/sの揺れで、40階の最上階は30秒で±47 cm、2分ほどで±73 cmに達する。15階は±7 mm、5階は±0.7 mmのままだ。揺れの周期を1.6秒にすると15階が±24 cmに、0.55秒にすると5階が±4.9 cmになり、大きく揺れる建物が入れ替わる。層間変形角は40階が1/150、15階が1/171、5階が1/270で、高さほどの差は出ない。どれも同じ周期を何十秒も与え続けた値である。揺れを「なし」にして40階を引いて離すと、測った周期は約4.2秒になる。
  2. 15階の建物で、地面の周期を90秒かけて0.3秒から5秒まで上げていく。20秒すぎに2次の周期を通ると建物がS字にくねり、50秒すぎに1次の周期を通ると、少し遅れて最上階が±16 cmまで揺れる。共振曲線の上の点は、同じ周期で揺らし続けたときの半分ほどにしか届かない。周期が先へ進むので、最大に達する前に通り過ぎるからだ。
  3. 8階の2棟を、最大速度40 cm/sの短周期の多い揺れにかける。最上階の加速度はそのままの建物で11.4 m/s²、免震で1.0 m/s²。層間変形角は1/121と1/1264で、免震層が22 cm変形して引き受けている。ところが揺れを周期4秒の正弦波に変えると、立場が逆転する。そのままの建物の最上階は±1.5 cmなのに、免震層は±65 cmまで揺れる。免震層の周期と合ったからだ。同じ周期が続くのは現実の地震ではまず起きない条件で、弱点を際立たせた形である。
  4. 40階の2棟に周期4.2秒の揺れを与える。右の屋上の重りは建物の重さの2%で、±77 cm振れ、最上階の揺れは±73 cmから±20 cmに減る。共振曲線の1次の山は22倍から7倍ほどに下がり、周期3.9秒と4.6秒の2つに割れる。そのため揺れの周期を5.2秒にすると、そのままの建物が±10 cm、重りのある建物が±14 cmと逆転する。地面の揺れを「長周期が多い」に、最大速度を20 cm/sに変えると、最大は32 cmほどから23 cmほどに減るだけ。周期が定まらないうえ、重りが振れ出すまでに時間がかかるからだ。
  5. 6階の2棟のうち、右は1階の固さが4分の1で、降伏する力も6割しかない。最大速度30 cm/sの揺れで、左は層間変形角が最大1/229にとどまる。右は1階だけが1/45で、2階から上は左より小さい。最大速度を50 cm/sにすると左も降伏して1/93になるが、右の1階は1/22まで進む。
  6. 周期5.5秒を中心にした合成波の最大速度は20 cm/s。50階の最上階は±59 cm揺れ、15階は±1.8 cm、5階は±1.7 mmにとどまる。地面の揺れが65秒ほどで最大の1割を下回っても、50階は80秒で±15 cm動いている。減衰定数を1%にすると最大は±82 cmに増え、115秒でも±31 cm残る。
現実では

大阪、メキシコ市、八代で起きたこと

咲洲庁舎の固有周期は、地震計の記録をもとに解析し直すと6.5〜7.0秒になり、大阪湾地域の揺れの卓越周期(揺れの中で強く現れる周期)6〜7秒とほぼ重なる。だから大阪府は共振で大きく揺れたとみて、ダンパーを増やす計画を立てた。解析では、同じ地震の短辺方向の最大振幅が137 cmから69 cmに減る1。

気象庁の長周期地震動階級で最大の4は、高層ビルで「立っていることができず、はわないと動くことができない」揺れを指す。東北地方太平洋沖地震では東京23区などが階級4、大阪府は階級2だった6。階級は地上の記録からの見積もりで、建物の頂部はそれより大きく揺れることがある。

周期が合うのは高いビルだけとは限らない。1985年のメキシコ地震では、市内の旧湖底で周期2秒の成分の強い揺れが長く続いた。ベックとホールの報告によると、被害が集中したのは6〜15階の建物である。階数から予想される15〜30階より低いのは、損傷で柔らかくなると周期が延びて2秒へ近づくからだと、2人はみている7。

免震層が大きく動いた例は、2026年の熊本地震でも報告された。国土技術政策総合研究所と建築研究所の速報によると、八代市の観測点の記録は、多くの免震建物の設計周期に近い3秒付近に大きな山を持っていた。市内で調べた3棟には、周囲のすき間を超えるか、すき間に達するほど動いた形跡がある。すき間60 cmの7階建ての事務所では免震層の部材の破断も見つかったが、免震の働きは基本的に保たれ、室内の被害は軽微だったと管理者は報告している4。

1階が弱い形の被害は、2016年の熊本地震でも確認された。10階建ての鉄筋コンクリート造の共同住宅は、1階の柱1本がせん断破壊するなど被害が1〜2階に集中し、大破と判定された。解析でも、1階の層間変形角が上の階を大きく上回っている8。

重りの代表は、高さ508 mの台北101だろう。87階と92階のあいだに660トンの球が吊られ、大きな台風では最大150 cmほど振れる5。

省いていること

シミュレーションが省いていること

  • この計算は建物の安全を判定するものではない。画面の層間変形角を1/200や1/100と比べても、建物の良し悪しは決まらない。超高層の設計では、部材を入れた解析モデルと敷地ごとの地震動で確認する
  • 床は変形せず、揺れは水平の1方向だけで、ねじれや上下の揺れ、地盤の変形は入っていない。重さは床1 m²あたり1トン、階の高さは3.5 mという想定の値で、減衰定数の3%も仮の値である。固さは下から上へ半分まで減らし、「層の固さ1倍」で1次の周期がになるようにした
  • 降伏するのは「弱い1階」の場面の2棟だけで、力の上限は想定の値である。柱が壊れて力が落ちること、重さで傾きが増すこと、倒れることは計算しない。ほかの場面はどこまでも元の固さで揺れるので、大きな揺れでは本物の建物と数字が離れる
  • 免震層は周期4秒、減衰定数20%のばねとダンパー1組で、すき間に当たることはない。重りの重さ(建物の2%)は仮の値で、周期と減衰はデン・ハルトッグの式の値に固定した
  • 正弦波のように同じ周期が何分も続く揺れは、現実にはまず起きない。合成波は乱数を濾過して作ったもので実在の地震の記録ではなく、長さも本物の長周期地震動より短い
参考文献

参考文献

  1. 大阪府総務部「咲洲庁舎の安全性等についての検証結果」2011年5月、同 概要、同 2011年8月9日一部修正版、「第1回専門家会議の主な論点」. https://www.pref.osaka.lg.jp/documents/27416/230624file3-1.pdf https://www.pref.osaka.lg.jp/documents/27416/230624file3-2.pdf https://www.pref.osaka.lg.jp/documents/27416/shiryou-120sakishima.pdf https://www.pref.osaka.lg.jp/documents/27416/senmonkakaigi3-1.pdf 大林組「プロジェクト最前線 長周期地震動から超高層ビルを守る」2013年8月21日. https://www.obayashi.co.jp/thinking/detail/project22.html
  2. 建設省「建築基準法施行令第八十八条第一項、第二項及び第四項の規定に基づくZの数値、Rt及びAiを算出する方法並びに地盤が著しく軟弱な区域として特定行政庁が指定する基準」昭和55年建設省告示第1793号. https://www.mlit.go.jp/notice/noticedata/pdf/201703/00006623.pdf
  3. 気象庁「長周期地震動とは?」「長周期地震動階級および長周期地震動階級関連解説表について」「過去の地震における長周期地震動階級の事例」「東日本大震災 ~東北地方太平洋沖地震~ 関連ポータルサイト」、地震・火山月報(防災編)令和6年12月 付録10. https://www.jma.go.jp/jma/kishou/know/jishin/choshuki/choshuki_eq1.html https://www.jma.go.jp/jma/kishou/know/jishin/ltpgm_explain/about_level.html https://www.jma.go.jp/jma/kishou/know/jishin/ltpgm_explain/ltpgm_level_example.pdf https://www.jma.go.jp/jma/menu/jishin-portal.html https://www.data.jma.go.jp/eqev/data/gaikyo/monthly/202412/202412furoku_10.pdf
  4. 荒川区「主な構造関係法令リスト」. https://www.city.arakawa.tokyo.jp/documents/3192/kankeihorelist_r3.pdf 鹿島建設ほか「S29 長周期地震動に対する超高層鉄骨造の安全性検証法に関する検討」平成30年度建築基準整備促進事業 報告会資料. https://www.mlit.go.jp/common/001288918.pdf 国土交通省住宅局建築指導課「超高層建築物等における南海トラフ沿いの巨大地震による長周期地震動への対策について」2016年6月24日. https://www.mlit.go.jp/common/001135788.pdf
  5. 国土交通省国土技術政策総合研究所・建築研究所「令和8年(2026年)熊本地震による免震建築物の被害調査報告(速報)」2026年8月31日(9月3日修正). https://www.kenken.go.jp/japanese/contents/topics/2026/0831m.pdf
  6. Ormondroyd, J., Den Hartog, J. P.「The Theory of the Dynamic Vibration Absorber」『Transactions of the American Society of Mechanical Engineers』49-50(2), 1928. Brock, J. E.「A Note on the Damped Vibration Absorber」『Journal of Applied Mechanics』13(4), A284, 1946. Den Hartog, J. P.『Mechanical Vibrations』第4版, McGraw-Hill, 1956. The Skyscraper Museum「Taipei 101」. https://old.skyscraper.org/EXHIBITIONS/TEN_TOPS/taipei101.php Guinness World Records「Largest tuned mass damper」. https://www.guinnessworldrecords.com/world-records/774616-largest-tuned-mass-damper
  7. Beck, J. L., Hall, J. F.「Engineering Features of the Recent Mexican Earthquake」『Engineering and Science』49(3), 2–9, California Institute of Technology, 1986. https://calteches.library.caltech.edu/3478/
  8. 向井智久ほか「熊本地震で被災した鉄筋コンクリート造建築物を対象とした地震後継続使用確保に資する検討」建築研究資料 第204号 第4章, 建築研究所, 2021. https://www.kenken.go.jp/japanese/contents/publications/data/204/5.pdf

注

  1. 参考文献1。到達の時刻、最大振幅(長辺方向は最上階86 cm、18階32 cm)、損傷とエレベーターの数、長周期地震動の性質、倒壊のおそれの説明は検証結果による。距離は気象庁が発表した震央(北緯38度06.2分、東経142度51.6分)と庁舎の位置から求めた値で、大林組の記事も約770 kmと書いている。専門家会議では委員が700 kmと発言し、8月の検証結果は震源距離を約600 kmとしている。高さは大林組の記事により、同じ記事は地上55階建て、大阪府の資料は52階を最上階としている。固有周期は当初の解析で短辺5.7秒、長辺6.0秒、杭まで入れた見直し後で6.5秒、7.0秒。ダンパーは短辺方向にオイルダンパー144台、長辺方向にオイルダンパー40台と鋼材系ダンパー112台の計画で、揺れ始めて約5分後の短辺方向の振幅は約75 cmから約18 cmに減ると計算された。 ↩ ↩2 ↩3

  2. 参考文献2の第二。告示は高さをと書き、を「柱及びはりの大部分が木造又は鉄骨造である階の高さの合計」のに対する比としている。 ↩

  3. 参考文献4。1/200は施行令第82条の2の原則で、階ごとの固さの釣り合いを示す剛性率0.6以上の規定もこのリストに載っている。1/100は「通常のクライテリア(層間変形角1/100,塑性率2)」という記述による。長周期地震動への備えとして、国土交通省は2016年に、高さ60 mを超える建物と地上4階以上の免震建物に検討を求める対策をまとめた。対象は関東、静岡、中京、大阪の地域で、新築の大臣認定に2017年4月1日以降の申請から適用された。 ↩

  4. 参考文献5。設計周期の範囲と観測点K-NET八代の記録の分析は4.1節、3棟の形跡と管理者の報告は5章。すき間60 cmの事務所は地下1階、地上7階の鉄骨造で、周囲の擁壁の上端から約95 cm離れた植栽に擦った跡が残り、積層ゴムの破断が確認された。速報のため、内容が修正される可能性がある。 ↩ ↩2

  5. 参考文献6。シミュレーションはこの周期の比と、重り自身の周期で測った減衰定数を使った。同じ値を建物の周期で測るとになる。台北101の振れ幅は、The Skyscraper Museumの59インチをセンチに直した値である。同館は、660トンの球が8本のケーブルで92階から吊られていると書いている。 ↩ ↩2

  6. 参考文献3。階級は減衰定数5%の絶対速度応答スペクトル(周期1.6〜7.8秒の最大値)で決まり、階級1は5 cm/s以上、階級2は15 cm/s以上、階級3は50 cm/s以上、階級4は100 cm/s以上。 ↩

  7. 参考文献7。周期の目安は階数に対して秒という見積もりで書かれている。 ↩

  8. 参考文献8の第4章。せん断破壊した柱では、帯筋の破断や主筋の座屈も確認された。 ↩

プログラム
プログラム

このシミュレーションのプログラム

このシミュレーションは、階数と固さから建物の式を組み立て、固有周期と共振曲線を求めておく。その後は1/240秒ごとに地面の加速度を作り、全部の階の動きを解く。立体と光と影を描くのはthree.js(WebGL2の3Dの部品集)で、記事の計算からは柱と床を置く行列だけを渡している。

長さはm、時間は秒

1 = 1 mで作っているので、高さも揺れの幅も本物の大きさで計算できる。ここにある重さと階の高さ、平面の大きさは想定の値で、変えても周期の式の形は変わらない。T_PER_Hが「固さ1倍」の周期で、でとしたときの0.03である。

定数と単位(src/sims/quake-building.ts の 20〜33 行)
const G0 = 9.80665; // 重力加速度(m/s²)const STORY = 3.5; // 1 階分の高さ(m)const PLAN = 24; // 平面の 1 辺(m)const FLOOR_M = PLAN * PLAN * 1000; // 1 階分の重さ(kg)。床 1 m² あたり 1 トンとするconst T_PER_H = 0.03; // 「固さ 1 倍」の 1 次の固有周期 ÷ 高さ(秒/m)。T = 高さ × (0.02 + 0.01α) で α = 1 のときconst TAPER = 0.5; // 最上階の層の固さ ÷ 1 階の層の固さconst H = 1 / 240; // 計算の刻み(秒)const ISO_T = 4, ISO_H = 0.2, ISO_M = 1.5; // 免震層の周期(秒)・減衰定数・免震層の上の床の重さ(1 階分の何倍か)const TMD_RATIO = 0.02; // 制振の重りの重さ ÷ 建物の重さconst CY = 0.55, HARDEN = 0.05; // 層が降伏する力の大きさ(ベースシア係数)と、降伏した後の固さの割合const SOFT_K = 0.25, SOFT_Q = 0.6; // 弱い 1 階の固さと降伏する力の割合(ふつうの階と比べて)const T_MIN = 0.1, T_MAX = 10, NP = 240; // 共振曲線の周期の範囲(秒)と点の数const BIN = 0.25, HN = 240; // 揺れの記録の間隔(秒)と個数(1 分分)const FOV = 30; // 視野角(度)

建物の式を組み立て、周期を探す

建物は「節点」を下から順に並べた列として持つ。階の数と同じ節点に、免震なら免震層の上の床を一番下に、制振なら重りを一番上に足す。どの節点も上下の隣としかつながらないので、式の行列は対角とその両隣だけが値を持つ三重対角になる。

建物の式(src/sims/quake-building.ts の 51〜149 行)
/** * ホルツァー法: ある λ(= 角振動数の 2 乗)で揺れていると仮定し、1 階から順に「層の力 → 次の階のずれ」を求める。 * 最上階の上に余る力が 0 になる λ が固有値。φ と余る力の符号が変わる回数が、λ より小さい固有値の数になる */function holzer(n: number, m: Float64Array, k: Float64Array, lam: number, phi: Float64Array) {  let u = 1, V = k[0], count = 0;  for (let i = 0; i < n; i++) {    phi[i] = u; V -= lam * m[i] * u;    if (i + 1 < n) { const w = u + V / k[i + 1]; if (u * w <= 0) count++; u = w; } else if (u * V < 0) count++;  }  return count;}/** 小さい方から cnt 個の固有周期と振動の形(いちばん大きいずれを 1 にそろえる)を、区間を半分にしていく方法で求める */function modes(n: number, m: Float64Array, k: Float64Array, cnt: number) {  let up0 = 0; for (let i = 0; i < n; i++) up0 = Math.max(up0, (2 * (k[i] + (i + 1 < n ? k[i + 1] : 0))) / m[i]);  const T: number[] = [], phi: Float64Array[] = [], tmp = FA(n);  for (let j = 0; j < cnt; j++) {    let lo = 0, hi = up0;    for (let it = 0; it < 70; it++) { const mid = (lo + hi) / 2; if (holzer(n, m, k, mid, tmp) > j) hi = mid; else lo = mid; }    const lam = (lo + hi) / 2, p = FA(n); holzer(n, m, k, lam, p);    let big = 1e-30; for (let i = 0; i < n; i++) big = Math.max(big, Math.abs(p[i]));    for (let i = 0; i < n; i++) p[i] /= big;    T.push((2 * Math.PI) / Math.sqrt(lam)); phi.push(p);  }  return { T, phi };}/** 重さ・固さ・減衰を決める。揺れている途中のずれと速さはそのまま残す */function tune(b: Bld, stiff: number, hd: number) {  const N = b.N, f = b.f0, m = FA(N).fill(FLOOR_M), k = FA(N);  for (let i = 0; i < N; i++) k[i] = 1 - ((1 - TAPER) * i) / Math.max(1, N - 1);  // 固さの形を決めてから、1 次の固有周期が T = 0.03 × 高さ になるように全体を拡大する。周期は固さの平方根に反比例するので、固さ 4 倍で周期は半分  const w0 = (2 * Math.PI) / modes(N, m, k, 1).T[0], want = (2 * Math.PI) / (T_PER_H * N * STORY), ks = stiff * (want / w0) ** 2;  for (let i = 0; i < N; i++) k[i] *= ks;  if (b.kind === "soft") k[0] *= SOFT_K;  const bare = modes(N, m, k, Math.min(3, N)); b.T = bare.T; b.phi = bare.phi;  const w1 = (2 * Math.PI) / b.T[0], w2 = (2 * Math.PI) / b.T[Math.min(1, b.T.length - 1)];  // レイリー減衰 C = aM + bK。どちらも隣の階としかつながらないので、計算の形(三重対角)が保たれる  const al = b.kind === "iso" ? 0 : (2 * hd * w1 * w2) / (w1 + w2), be = b.kind === "iso" ? (2 * hd) / w1 : (2 * hd) / (w1 + w2);  // 層が降伏する力: 上に載る重さ × 係数 × Ai の分布(上の階ほど大きな力を受けるとする、建築基準法の分布)  let W = 0; const W1 = N * FLOOR_M * G0, Tq = T_PER_H * N * STORY;  for (let i = N - 1; i >= 0; i--) {    W += FLOOR_M * G0;    const a = W / W1, Ai = 1 + ((1 / Math.sqrt(a) - a) * 2 * Tq) / (1 + 3 * Tq);    b.qy[f + i] = b.yields ? CY * Ai * W : Infinity;    b.m[f + i] = m[i]; b.k[f + i] = k[i]; b.c[f + i] = be * k[i]; b.cm[f + i] = al * m[i];  }  if (b.kind === "soft") b.qy[0] *= SOFT_Q;  const M = N * FLOOR_M;  if (b.kind === "iso") {    // 免震層: 上の全部の重さが周期 ISO_T で揺れる固さ    const Mt = M + ISO_M * FLOOR_M, wb = (2 * Math.PI) / ISO_T;    b.m[0] = ISO_M * FLOOR_M; b.k[0] = Mt * wb * wb; b.c[0] = 2 * ISO_H * Mt * wb; b.cm[0] = 0; b.qy[0] = Infinity;    b.Tf = modes(b.n, b.m, b.k, 1).T[0];  } else if (b.kind === "tmd") {    // 制振の重り: デン・ハルトッグの式。1 次の振動の重さ(最上階を基準)に対する比 μ から決める。周期は建物の (1 + μ) 倍、重り自身の周期で測った減衰定数は √(3μ / (8(1 + μ)))    const p = b.phi[0], top = p[N - 1]; let Ms = 0; for (let i = 0; i < N; i++) Ms += (m[i] * p[i] * p[i]) / (top * top);    const md = TMD_RATIO * M, mu = md / Ms, wd = w1 / (1 + mu), z = Math.sqrt((3 * mu) / (8 * (1 + mu))), j = b.n - 1;    b.m[j] = md; b.k[j] = md * wd * wd; b.c[j] = 2 * z * md * wd; b.cm[j] = 0; b.qy[j] = Infinity; b.Td = (2 * Math.PI) / wd;    b.Tf = b.T[0];  } else b.Tf = b.T[0];  factor(b);  for (let i = 0; i < b.n; i++) { const dl = b.x[i] - (i > 0 ? b.x[i - 1] : 0); b.fs0[i] = clamp(b.k[i] * dl, -b.qy[i], b.qy[i]); b.dl0[i] = dl; }  curveOf(b);}function makeBld(N: number, kind: Kind, name: string, x0: number, stiff: number, hd: number): Bld {  const f0 = kind === "iso" ? 1 : 0, n = N + f0 + (kind === "tmd" ? 1 : 0);  const b: Bld = {    kind, name, N, x0, n, f0, top: f0 + N - 1,    m: FA(n), k: FA(n), c: FA(n), cm: FA(n), qy: FA(n), off: FA(n), dd: FA(n), ll: FA(n), x: FA(n), v: FA(n), a: FA(n), xp: FA(n), vp: FA(n), r: FA(n), sh: FA(n), vv: FA(n), fs: FA(n), fs0: FA(n), dl0: FA(n),    T: [], phi: [], Tf: 0, Td: 0, yields: kind === "soft" || kind === "frame", pull: null,    curve: new Float32Array(NP), hA: new Float32Array(HN), hB: new Float32Array(HN), lo: Infinity, hi: -Infinity, loT: 0, hiT: 0,    amp: 0, peakTop: 0, peakAbs: 0, peakDev: 0, ampDev: 0, w0: 0, w1: 0, d0: 0, d1: 0, wt: 0, peak: FA(N), env: FA(N), maxD: 0, maxAt: 1, last: 0, tUp: -1, Tm: 0, noted: false, yielded: false,  };  tune(b, stiff, hd);  return b;}const periodAt = (j: number) => T_MIN * Math.pow(T_MAX / T_MIN, j / (NP - 1));/** 共振曲線: 周期ごとに正弦波の地面の揺れを入れたときの、最上階の揺れ ÷ 地面の揺れ。複素数の三重対角の式を周期の数だけ解く(降伏は考えない) */function curveOf(b: Bld) {  const n = b.n, { m, k, c, cm } = b, cr = FA(n), ci = FA(n), dr = FA(n), di = FA(n);  for (let j = 0; j < NP; j++) {    const w = (2 * Math.PI) / periodAt(j);    let pr = 0, pi = 0, qr = 0, qi = 0; // 1 つ下の行の消去の結果 c'・d'    for (let i = 0; i < n; i++) {      const k1 = i + 1 < n ? k[i + 1] : 0, c1 = i + 1 < n ? c[i + 1] : 0;      const zr = k[i] + k1 - w * w * m[i], zi = w * (c[i] + c1 + cm[i]);      // 下の階とのつながり o = −(k + iωc)      const or = -k[i], oi = -w * c[i], ur = -k1, ui = -w * c1;      const nr = i ? zr - (or * pr - oi * pi) : zr, ni = i ? zi - (or * pi + oi * pr) : zi, den = nr * nr + ni * ni;      const br = w * w * m[i] - (i ? or * qr - oi * qi : 0), bi = -(i ? or * qi + oi * qr : 0);      pr = (ur * nr + ui * ni) / den; pi = (ui * nr - ur * ni) / den;      qr = (br * nr + bi * ni) / den; qi = (bi * nr - br * ni) / den;      cr[i] = pr; ci[i] = pi; dr[i] = qr; di[i] = qi;    }    let xr = dr[n - 1], xi = di[n - 1];    for (let i = n - 2; i >= b.top; i--) { const r = dr[i] - (cr[i] * xr - ci[i] * xi), q = di[i] - (cr[i] * xi + ci[i] * xr); xr = r; xi = q; }    b.curve[j] = Math.hypot(xr, xi);  }}

固有周期はホルツァー法で探す。ある角振動数の2乗で揺れていると仮定すると、1階のずれから1階の層の力が決まり、そこからを引いた力で2階のずれが決まる。これを最上階まで続け、屋上の上に余る力が0になるが固有値だ。ずれと余る力の符号が変わる回数はより小さい固有値の数に等しいので、modesは区間を半分にしていくだけで目当ての次数の周期を見つけられる。

tuneは、まず固さの形だけで周期を求め、になるように全体を拡大する。周期は固さの平方根に反比例するから、1回の拡大で済む。減衰はのレイリー減衰で、1次と2次の周期で減衰定数がになるようにとを決めた。免震の建物は上部をだけにしている。地面を基準にしたの項は、免震層の揺れまで余分に弱めてしまうからだ。

制振の重りは、本文の倍の周期と、重り自身の周期で測った減衰定数で決める。は1次の振動に効く重さとの比である。

curveOfが共振曲線を作る。地面の揺れをとすると、運動の式は複素数の連立方程式になる。三重対角なので、前から消して後ろから代入する計算(トーマス法)を周期の数だけくり返せばよい。つまみを動かしたときだけ計算し直す。

乱数を濾過して地面の揺れを作る

正弦波と周期を上げていく揺れは、位相を足していって作る。周期のつまみを動かしても波が途切れない。合成波は白色雑音を2段のばねとダッシュポットに通したものである。1段目は地盤の揺れのモデルとして広く使われてきた形(金井清の式をもとにしたもの)で、周期Tcのあたりを残す。2段目はとてもゆっくりした成分を除く。

地面の揺れ(src/sims/quake-building.ts の 151〜216 行)
interface Wave { dt: number; n: number; a: Float32Array; v: Float32Array; d: Float32Array; dur: number; Tc: number; pa: number }/** 合成波の決まり: 濾過の中心の周期 Tc と減衰 zc、ゆっくりした揺れを除く周期 Th、強さの時間変化(t1 まで増え、t2 まで一定、その後は率 dk で減る)、長さ dur、乱数の種 */const WAVES: Record<string, { Tc: number; zc: number; Th: number; t1: number; t2: number; dk: number; dur: number; seed: number }> = {  short: { Tc: 0.5, zc: 0.6, Th: 3, t1: 1.5, t2: 9, dk: 0.18, dur: 40, seed: 7 },  long: { Tc: 5.5, zc: 0.25, Th: 40, t1: 12, t2: 50, dk: 0.12, dur: 90, seed: 11 },};const waveCache: Record<string, Wave> = {};/** * 乱数(白色雑音)を 2 段のばねとダッシュポットに通して、地面の揺れを作る。1 段目は周期 Tc のあたりを残し、2 段目はとてもゆっくりした成分を除く。 * 2 段目の出力を地面のずれとして使うので、速度と加速度はそのずれの微分になり、最後は元の位置に戻る。最大速度を 1 m/s にそろえておき、つまみの値を掛けて使う */function makeWave(api: Api, key: string): Wave {  if (waveCache[key]) return waveCache[key];  const c = WAVES[key], dt = 0.01, n = Math.ceil(c.dur / dt), rnd = api.rng(c.seed);  const a = new Float32Array(n), v = new Float32Array(n), d = new Float32Array(n);  const wf = (2 * Math.PI) / c.Tc, wh = (2 * Math.PI) / c.Th, zh = 0.7;  let xf = 0, vf = 0, y = 0, vy = 0, big = 1e-9;  for (let i = 0; i < n; i++) {    const t = i * dt, e = t < c.t1 ? (t / c.t1) ** 2 : t < c.t2 ? 1 : Math.exp(-c.dk * (t - c.t2));    const w = (e * Math.sqrt(-2 * Math.log(1 - rnd())) * Math.cos(2 * Math.PI * rnd())) / Math.sqrt(dt);    vf += dt * (-2 * c.zc * wf * vf - wf * wf * xf - w); xf += dt * vf;    const g = -(2 * c.zc * wf * vf + wf * wf * xf);    const ay = g - 2 * zh * wh * vy - wh * wh * y;    vy += dt * ay; y += dt * vy;    a[i] = ay; v[i] = vy; d[i] = y; big = Math.max(big, Math.abs(vy));  }  let pa = 0;  for (let i = 0; i < n; i++) { a[i] /= big; v[i] /= big; d[i] /= big; pa = Math.max(pa, Math.abs(a[i])); }  return (waveCache[key] = { dt, n, a, v, d, dur: c.dur, Tc: c.Tc, pa });}interface Ground { t: number; ph: number; ag: number; vg: number; dg: number; T: number; wait: number; ended: boolean; hmin: number; hmax: number; hA: Float32Array; hB: Float32Array }const SWEEP = { T0: 0.3, T1: 5, dur: 90 };const sweepT = (t: number) => SWEEP.T0 * Math.pow(SWEEP.T1 / SWEEP.T0, clamp(t / SWEEP.dur, 0, 1));/** 地面の加速度・速度・ずれを刻み 1 つ分進める。正弦波と周期を上げていく揺れは位相を足して作る(周期のつまみを動かしても波がつながる) */function stepGround(api: Api, s: S, h: number) {  const g = s.g, key = api.str("wave"), V = api.num("amp") / 100;  g.t += h;  if (key === "sine" || key === "sweep") {    const T = key === "sine" ? api.num("period") : sweepT(g.t), w = (2 * Math.PI) / T;    const env = key === "sine" ? smooth(g.t / (2.5 * T)) : smooth(g.t / 3) * smooth((SWEEP.dur - g.t) / 3);    g.T = T; g.ph += w * h;    g.vg = V * env * Math.sin(g.ph); g.ag = V * env * w * Math.cos(g.ph);    g.dg += h * (g.vg - g.dg / 90);    if (key === "sweep" && g.t > SWEEP.dur) endWave(api, s, h);  } else if (key === "short" || key === "long") {    const W = makeWave(api, key), q = g.t / W.dt, i = Math.floor(q), f = q - i;    if (i + 1 < W.n) {      g.ag = V * (W.a[i] + f * (W.a[i + 1] - W.a[i])); g.vg = V * (W.v[i] + f * (W.v[i + 1] - W.v[i])); g.dg = V * (W.d[i] + f * (W.d[i + 1] - W.d[i]));    } else { g.ag = 0; g.vg = 0; g.dg *= Math.exp(-h / 2); endWave(api, s, h); }    g.T = W.Tc;  } else { g.ag = 0; g.vg = 0; g.dg *= Math.exp(-h / 2); g.T = 0; }}/** 地面の揺れが終わったら知らせ、しばらく最上階の揺れを見せてから、同じ揺れをもう一度与える */function endWave(api: Api, s: S, h: number) {  const g = s.g;  if (!g.ended) { g.ended = true; g.wait = 30; api.note("地面の揺れが止まった。建物は固有周期で揺れ続ける", 3200); }  g.wait -= h;  if (g.wait <= 0) { restartGround(s); api.note("同じ揺れをもう一度与える", 2200); }}function restartGround(s: S) {  const g = s.g; g.t = 0; g.ph = 0; g.ended = false; g.wait = 0;  resetPeaks(s); s.sw.fill(0);}function resetPeaks(s: S) {  for (const b of s.bs) { b.peakTop = 0; b.peakAbs = 0; b.peakDev = 0; b.peak.fill(0); b.maxD = 0; b.maxAt = 1; b.noted = false; b.yielded = false; }}

2段目の出力をそのまま地面のずれに使うと、速度は、加速度はになり、積分しなくても3つがそろう。揺れの終わりには地面が元の位置へ戻る。乱数の種を決めてあるので、誰が開いても同じ揺れになる。最大速度を1 m/sにそろえて保存し、つまみの値を掛けて使う。

ニューマーク法で1刻み進める

本文の運動の式を、ネイサン・ニューマークが1959年に発表したニューマーク法(、)で解く。新しい時刻の加速度と速度を新しいずれで表すと、未知数はだけになる。

とは今の状態から求めた予測の値だ。これを運動の式に入れると、の連立方程式になる。は毎刻み同じなので、factorで一度だけ前から消しておく。60階の周期の短い振動があっても発散しないのが、この方法を使う理由である。

時間を進める(src/sims/quake-building.ts の 218〜314 行)
/** 内側の式を解くための下準備(三重対角の行列を前から消しておく)。k̂ = K + (2/h)C + (4/h²)M で、毎刻み同じものを使える */function factor(b: Bld) {  const n = b.n, a0 = 4 / (H * H), a1 = 2 / H;  for (let i = 0; i < n; i++) {    const k1 = i + 1 < n ? b.k[i + 1] : 0, c1 = i + 1 < n ? b.c[i + 1] : 0;    const d = b.k[i] + k1 + a1 * (b.c[i] + c1 + b.cm[i]) + a0 * b.m[i] + (b.pull && b.pull.i === i ? b.pull.kp : 0);    b.off[i] = -(k1 + a1 * c1);    b.ll[i] = i ? b.off[i - 1] / b.dd[i - 1] : 0;    b.dd[i] = i ? d - b.ll[i] * b.off[i - 1] : d;  }}/** * ニューマーク法(β = 1/4、γ = 1/2)で 1 刻み進める。周期の短い振動があっても発散しない。 * 新しい時刻の加速度と速度をずれ x で表し、M a + C v + (層の力) = −M a_g を x について解く。 * 層の力は「傾き k の直線を上限と下限で切った形」(降伏)。降伏するときは残りの力が 0 になるまで解き直す */function stepBld(b: Bld, ag: number) {  const n = b.n, { m, k, c, cm, qy, x, v, a, xp, vp, r, sh, vv, fs, fs0, dl0, dd, ll, off } = b, h = H, a0 = 4 / (h * h), a1 = 2 / h;  for (let i = 0; i < n; i++) { xp[i] = x[i] + h * v[i] + 0.25 * h * h * a[i]; vp[i] = v[i] + 0.5 * h * a[i]; x[i] = xp[i]; }  const iters = b.yields ? 10 : 1, P = b.pull;  for (let it = 0; it < iters; it++) {    let xl = 0, vl = 0;    for (let i = 0; i < n; i++) {      const vi = vp[i] + a1 * (x[i] - xp[i]), dl = x[i] - xl;      let f = fs0[i] + k[i] * (dl - dl0[i]);      if (qy[i] < Infinity) { const hard = HARDEN * k[i] * dl, top = qy[i] * (1 - HARDEN); f = clamp(f, hard - top, hard + top); }      fs[i] = f; vv[i] = vi; sh[i] = f + c[i] * (vi - vl);      xl = x[i]; vl = vi;    }    for (let i = 0; i < n; i++) r[i] = -m[i] * ag - m[i] * a0 * (x[i] - xp[i]) - cm[i] * vv[i] - sh[i] + (i + 1 < n ? sh[i + 1] : 0);    if (P) r[P.i] += P.kp * (P.target - x[P.i]);    for (let i = 1; i < n; i++) r[i] -= ll[i] * r[i - 1];    r[n - 1] /= dd[n - 1];    for (let i = n - 2; i >= 0; i--) r[i] = (r[i] - off[i] * r[i + 1]) / dd[i];    let big = 0;    for (let i = 0; i < n; i++) { x[i] += r[i]; big = Math.max(big, Math.abs(r[i])); }    if (big < 1e-6) break;  }  let xl = 0;  for (let i = 0; i < n; i++) {    const d = x[i] - xp[i]; a[i] = a0 * d; v[i] = vp[i] + a1 * d;    const dl = x[i] - xl;    let f = fs0[i] + k[i] * (dl - dl0[i]);    if (qy[i] < Infinity) { const hard = HARDEN * k[i] * dl, top = qy[i] * (1 - HARDEN); f = clamp(f, hard - top, hard + top); }    fs0[i] = f; dl0[i] = dl; xl = x[i];  }}/** 測る: 層間変形角の最大・最上階の揺れの幅と加速度・上向きに 0 を横切る間隔(測った周期) */function track(api: Api, s: S, b: Bld) {  const j = b.top, xt = b.x[j], T = Math.max(b.Tf, s.g.T || 0), fd = Math.exp(-H / 1.5);  // 揺れの幅: 今の周期のあいだの最大(半周期ごとに 2 つの枠を入れ替えるので、揺れの途中でも値が波打たない)  const dev = b.kind === "iso" ? Math.abs(b.x[0]) : b.kind === "tmd" ? Math.abs(b.x[b.n - 1] - b.x[j]) : 0;  b.wt += H;  if (b.wt >= 0.55 * T) { b.wt = 0; b.w0 = b.w1; b.w1 = 0; b.d0 = b.d1; b.d1 = 0; }  b.w1 = Math.max(b.w1, Math.abs(xt)); b.d1 = Math.max(b.d1, dev);  b.amp = Math.max(b.w0, b.w1); b.ampDev = Math.max(b.d0, b.d1);  b.peakTop = Math.max(b.peakTop, Math.abs(xt));  b.peakAbs = Math.max(b.peakAbs, Math.abs(b.a[j] + s.g.ag));  if (b.last < 0 && xt >= 0 && b.amp > 1e-3) { if (b.tUp >= 0) b.Tm = s.clock - b.tUp; b.tUp = s.clock; }  b.last = xt;  for (let i = 0; i < b.N; i++) {    const q = b.f0 + i, d = Math.abs(b.x[q] - (q > 0 ? b.x[q - 1] : 0)) / STORY;    b.env[i] = Math.max(d, b.env[i] * fd);    if (d > b.peak[i]) b.peak[i] = d;    if (d > b.maxD) { b.maxD = d; b.maxAt = i + 1; }  }  b.peakDev = Math.max(b.peakDev, dev);  if (!b.noted && b.maxD > 0.01 && !b.pull) { b.noted = true; api.note(`${b.name}: 層間変形角が 1/100 を超えた(${b.maxAt} 階)`, 2600); }  if (b.kind === "soft" && !b.yielded && Math.abs(b.fs0[0]) >= b.qy[0] * 0.98) { b.yielded = true; api.note("弱い 1 階が降伏した。ここから変形が 1 階に集まる", 3000); }}function tick(api: Api, s: S, h: number) {  s.clock += h;  stepGround(api, s, h);  const g = s.g;  // 指で引く位置はなめらかに追いかける(急に引くと、波が建物を伝わって一部の階だけが大きくずれる)  if (s.grab) { const P = s.grab.b.pull!; P.target += (s.grab.want - P.target) * (1 - Math.exp(-h / 0.3)); }  for (const b of s.bs) {    stepBld(b, g.ag); track(api, s, b);    const xt = b.x[b.top];    if (xt < b.lo) { b.lo = xt; b.loT = g.t; }    if (xt > b.hi) { b.hi = xt; b.hiT = g.t; }  }  g.hmin = Math.min(g.hmin, g.vg); g.hmax = Math.max(g.hmax, g.vg);  // 周期を上げていく揺れでは、周期ごとの最上階の揺れの比を記録する(共振曲線の上に点で描く)  if (api.str("wave") === "sweep" && !g.ended) {    const j = Math.round(((NP - 1) * Math.log(g.T / T_MIN)) / Math.log(T_MAX / T_MIN)), D = (api.num("amp") / 100 * g.T) / (2 * Math.PI), b = s.bs[s.bs.length - 1];    if (j >= 0 && j < NP) s.sw[j] = Math.max(s.sw[j], Math.abs(b.x[b.top]) / Math.max(1e-6, D));  }  s.bin += h;  if (s.bin >= BIN) {    s.bin -= BIN;    // 区間の中の最小と最大を、起きた順に記録する(ゆっくりした揺れはなめらかな線に、速い揺れは帯になる)    for (const b of s.bs) { const first = b.loT <= b.hiT; b.hA[s.hi] = first ? b.lo : b.hi; b.hB[s.hi] = first ? b.hi : b.lo; b.lo = Infinity; b.hi = -Infinity; }    g.hA[s.hi] = g.hmin; g.hB[s.hi] = g.hmax; g.hmin = Infinity; g.hmax = -Infinity;    s.hi = (s.hi + 1) % HN; s.hn = Math.min(HN, s.hn + 1);  }}

降伏する層の力は、傾きで増やした値を上限と下限で切って求める(2本の直線で表す履歴)。切られた分だけ式が釣り合わなくなるので、残りの力が小さくなるまで同じで解き直す。降伏しない建物は1回で厳密に解ける。指で引いている間は、引く点に固いばねを足してを作り直す。

trackは1刻みごとに、層間変形角の最大と、最上階の揺れの幅を記録する。最上階のずれが上向きに0を横切る間隔が「測った周期」だ。

three.jsが受け持つところ

ここから先は描画である。柱はInstancedMeshで同じ箱を並べ、毎コマ行列の配列を書き換える。行列の縦の列に横の成分を入れると箱が平行四辺形に傾くので、上下の床にちょうどつながる。1本を3段に分け、の形にずらすと、上下の床に直角につながる柱の曲がりになる。

立体を描く(src/sims/quake-building.ts の 316〜408 行)
const MAX_COL = 4700, MAX_SLAB = 170, MAX_BEAR = 24;/** 平面の上の柱の位置: 外周の 12 本(画質が低いときは四隅の 4 本) */const COL_P: [number, number][] = [], COL_C: [number, number][] = [];{  const e = PLAN / 2 - 1, q = e / 3;  for (const x of [-e, -q, q, e]) for (const z of [-e, -q, q, e]) { if (Math.abs(x) === e || Math.abs(z) === e) COL_P.push([x, z]); if (Math.abs(x) === e && Math.abs(z) === e) COL_C.push([x, z]); }}interface Meshes { col: THREE.InstancedMesh; tone: Float32Array; toneA: THREE.InstancedBufferAttribute; slab: THREE.InstancedMesh; bear: THREE.InstancedMesh; ball: THREE.Mesh; cable: THREE.LineSegments; cableP: Float32Array; ground: THREE.Group }function makeMeshes(api: Api, v: View3): Meshes {  const box = new THREE.BoxGeometry(1, 1, 1);  const col = new THREE.InstancedMesh(box, v.mat(1), MAX_COL);  const tone = new Float32Array(MAX_COL * 3), toneA = new THREE.InstancedBufferAttribute(tone, 3);  toneA.setUsage(THREE.DynamicDrawUsage); col.instanceColor = toneA; col.instanceMatrix.setUsage(THREE.DynamicDrawUsage);  const slab = new THREE.InstancedMesh(new THREE.BoxGeometry(1, 1, 1), v.mat(0.82), MAX_SLAB);  slab.instanceMatrix.setUsage(THREE.DynamicDrawUsage);  const bear = new THREE.InstancedMesh(new THREE.CylinderGeometry(0.5, 0.5, 1, 12), v.mat(1), MAX_BEAR);  bear.instanceMatrix.setUsage(THREE.DynamicDrawUsage);  for (const m of [col, slab, bear]) { m.castShadow = true; m.receiveShadow = true; m.frustumCulled = false; m.count = 0; }  const ball = new THREE.Mesh(new THREE.SphereGeometry(1, 24, 16), v.mat(0.95, { shine: 0.4 }));  ball.castShadow = true; ball.visible = false;  const cableP = new Float32Array(12), cg = new THREE.BufferGeometry();  cg.setAttribute("position", new THREE.BufferAttribute(cableP, 3));  const cable = new THREE.LineSegments(cg, v.lineMat(1)); cable.frustumCulled = false; cable.visible = false;  // 地面と町並み。建物の土台と同じだけ動く(カメラは揺れない)  const ground = new THREE.Group();  // 地面は奥の面より十分に広くしておく(端が見えると、そこに輪郭線が出る)  const plane = new THREE.Mesh(new THREE.PlaneGeometry(60000, 60000).rotateX(-Math.PI / 2), v.mat(0.14));  plane.receiveShadow = true; ground.add(plane);  const rnd = api.rng(5), CN = 60, city = new THREE.InstancedMesh(new THREE.BoxGeometry(1, 1, 1), v.mat(0.3, { flat: true }), CN), mt = new THREE.Matrix4();  for (let i = 0; i < CN; i++) {    const w = 14 + rnd() * 18, d = 14 + rnd() * 18, h = 5 + rnd() * rnd() * 34, x = -560 + rnd() * 1120, z = -130 - rnd() * 300;    mt.makeScale(w, h, d); mt.setPosition(x, h / 2, z); city.setMatrixAt(i, mt);  }  city.castShadow = true; city.receiveShadow = true; city.frustumCulled = false; ground.add(city);  v.scene.add(col, slab, bear, ball, cable, ground);  return { col, tone, toneA, slab, bear, ball, cable, cableP, ground };}/** 箱 1 個の並べ方の行列を書き込む。縦の軸を shx だけ横へ傾ける(平行四辺形の柱)ので、上下の階にちょうどつながる */function putBox(a: ArrayLike<number> & Float32Array, i: number, sx: number, sy: number, sz: number, shx: number, ox: number, oy: number, oz: number) {  const o = i * 16;  a[o] = sx; a[o + 1] = 0; a[o + 2] = 0; a[o + 3] = 0;  a[o + 4] = shx; a[o + 5] = sy; a[o + 6] = 0; a[o + 7] = 0;  a[o + 8] = 0; a[o + 9] = 0; a[o + 10] = sz; a[o + 11] = 0;  a[o + 12] = ox; a[o + 13] = oy; a[o + 14] = oz; a[o + 15] = 1;}const S_CURVE = (q: number) => q * q * (3 - 2 * q); // 上下の床に直角につながる柱の曲がり方/** 計算の結果を、柱と床の位置に写す。three.js が受け持つのはここから先の描画だけで、毎コマ行列の配列を書き換えるだけにする */function place(api: Api, s: S) {  const K = api.num("scale"), gx = K * s.g.dg, m = s.m, ca = m.col.instanceMatrix.array as Float32Array, sa = m.slab.instanceMatrix.array as Float32Array, ba = m.bear.instanceMatrix.array as Float32Array, ton = m.tone;  const q = api.quality, seg = q >= 2 ? 3 : q === 1 ? 2 : 1, pts = q === 0 ? COL_C : COL_P, CW = 1.1, ST = 0.6;  let ic = 0, is = 0, ib = 0;  m.ground.position.x = gx; m.ball.visible = false; m.cable.visible = false;  for (const b of s.bs) {    const X = b.x0 + gx;    putBox(sa, is++, PLAN + 10, 1, PLAN + 10, 0, X, 0, 0); // 土台    let y = 0.5, xl = 0;    if (b.kind === "iso") {      // 免震層: 積層ゴムの円柱を、土台と床のあいだで傾ける      const xm = K * b.x[0], hb = 2.4;      for (const [cx, cz] of pts) putBox(ba, ib++, 1.9, hb, 1.9, xm, X + cx + xm / 2, y + hb / 2, cz);      y += hb; putBox(sa, is++, PLAN + 2, 0.9, PLAN + 2, 0, X + xm, y + 0.45, 0); y += 0.9; xl = xm;    }    for (let i = 0; i < b.N; i++) {      const xi = K * b.x[b.f0 + i], dl = xi - xl, t = 0.42 + 0.58 * Math.min(1, b.env[i] * 150);      for (const [cx, cz] of pts) {        for (let k = 0; k < seg; k++) {          const a0 = k / seg, a1 = (k + 1) / seg, s0 = S_CURVE(a0), s1 = S_CURVE(a1);          putBox(ca, ic, CW, STORY / seg, CW, dl * (s1 - s0), X + cx + xl + (dl * (s0 + s1)) / 2, y + (STORY * (a0 + a1)) / 2, cz);          ton[ic * 3] = t; ton[ic * 3 + 1] = t; ton[ic * 3 + 2] = t; ic++;        }      }      y += STORY;      putBox(sa, is++, PLAN + 0.8, ST, PLAN + 0.8, 0, X + xi, y - ST / 2, 0);      xl = xi;    }    if (b.kind === "tmd") {      // 屋上の枠から吊った重り。重りのずれは最上階から測り、同じ倍率で描く      const hf = 18, xr = xl, xb = X + xr + K * (b.x[b.n - 1] - b.x[b.top]), yb = y + 8, R = 5.5, W = 9;      for (const cx of [-W, W]) for (const cz of [-W, W]) { putBox(ca, ic, 0.9, hf, 0.9, 0, X + xr + cx, y + hf / 2, cz); ton[ic * 3] = ton[ic * 3 + 1] = ton[ic * 3 + 2] = 0.8; ic++; }      putBox(sa, is++, 2 * W + 2, 0.8, 2 * W + 2, 0, X + xr, y + hf, 0);      m.ball.visible = true; m.ball.scale.setScalar(R); m.ball.position.set(xb, yb, 0);      const c = m.cableP, top = y + hf - 0.4;      c[0] = X + xr; c[1] = top; c[2] = 5; c[3] = xb; c[4] = yb + R * 0.6; c[5] = 2;      c[6] = X + xr; c[7] = top; c[8] = -5; c[9] = xb; c[10] = yb + R * 0.6; c[11] = -2;      m.cable.visible = true; (m.cable.geometry.attributes.position as THREE.BufferAttribute).needsUpdate = true;    }  }  m.col.count = ic; m.slab.count = is; m.bear.count = ib;  m.col.instanceMatrix.needsUpdate = true; m.slab.instanceMatrix.needsUpdate = true; m.bear.instanceMatrix.needsUpdate = true; m.toneA.needsUpdate = true;  // 輪郭線は奥行きの精度に頼るので、見ている距離に合わせて手前と奥の面を決める  const d = s.v.orbit.dist; s.v.camera.near = Math.max(1, d * 0.12); s.v.camera.far = d * 3.2;  if (Math.abs(d - s.fogD) > 0.05 * s.fogD) { s.fogD = d; s.v.set({ fog: [d * 1.2, d * 2.5] }); }}

変形とは別に、地面も同じ倍率で動かす。カメラは揺らさない。柱の濃さは最近の層間変形角で変え、変形の集まった階が目立つようにした。影と視点の操作、白黒の網点と輪郭線への変換は、全記事に共通の部品が受け持つ。画質が低いときは柱を四隅の4本に、段を1段に減らす。

指で引く

指の下の建物は、画面の上で土台と屋上を結ぶ線からの距離で探す。立体に光線を当てるより軽く、建物が傾いていても当たる。指の動きは、その階を右へ1 m動かすと画面で何px動くかの比で実際のずれに直し、倍率で割る。

指で引く(src/sims/quake-building.ts の 410〜442 行)
interface Grab { b: Bld; i: number; px: number; py: number; ex: number; ey: number; x0: number; lim: number; want: number }/** 指の下の建物と階を探す(画面の上で、土台と屋上を結ぶ線からの距離で決める) */function pick(api: Api, s: S) {  const v = s.v, P = api.pointer, K = api.num("scale"), gx = K * s.g.dg, R = api.u(P.kind === "touch" ? 14 : 6);  let best: { b: Bld; f: number } | null = null, bd = Infinity;  for (const b of s.bs) {    const yTop = (b.kind === "iso" ? 3.8 : 0.5) + b.N * STORY, xr = b.x0 + gx + K * b.x[b.top];    const a = v.project([b.x0 + gx, 0, 0]), c = v.project([xr, yTop, 0]), e = v.project([xr + PLAN / 2, yTop, 0]);    const ux = c.x - a.x, uy = c.y - a.y, L2 = Math.max(1, ux * ux + uy * uy), t = clamp(((P.x - a.x) * ux + (P.y - a.y) * uy) / L2, 0, 1);    const dist = Math.hypot(P.x - (a.x + t * ux), P.y - (a.y + t * uy)), half = Math.abs(e.x - c.x) + R;    if (dist < half && dist < bd) { bd = dist; best = { b, f: clamp(Math.round(t * b.N), 1, b.N) }; }  }  return best;}function startPull(api: Api, s: S, hit: { b: Bld; f: number }) {  const b = hit.b, i = b.f0 + hit.f - 1, K = api.num("scale"), gx = K * s.g.dg, X = b.x0 + gx + K * b.x[i], Y = hit.f * STORY;  const a = s.v.project([X, Y, 0]), c = s.v.project([X + 1, Y, 0]);  // 引くばね: 層の固さより十分に固くして、指で引いた階が指についてくるようにする  b.pull = { i, target: b.x[i], kp: 6 * b.k[i] }; factor(b); b.noted = true;  s.grab = { b, i, px: api.pointer.x, py: api.pointer.y, ex: c.x - a.x, ey: c.y - a.y, x0: b.x[i], lim: (hit.f * STORY) / 20, want: b.x[i] };}function movePull(api: Api, s: S) {  const g = s.grab!, p = api.pointer, K = api.num("scale");  const off = ((p.x - g.px) * g.ex + (p.y - g.py) * g.ey) / Math.max(0.04, g.ex * g.ex + g.ey * g.ey);  g.want = clamp(g.x0 + off / K, -g.lim, g.lim);}function endPull(api: Api, s: S) {  const g = s.grab!, b = g.b, moved = Math.abs(b.x[b.top]) > 1e-3;  b.pull = null; factor(b); s.grab = null;  // 引いている間の変形は測った値に入れない。離した後の揺れだけを測り直す  b.peak.fill(0); b.maxD = 0; b.maxAt = 1; b.peakTop = 0; b.peakAbs = 0;  if (moved) { b.tUp = -1; b.Tm = 0; api.note(`離した: ${b.name} は周期 ${fmt(b.Tf, 2)} 秒で揺れ続ける`, 2800); }}

引く位置を急に変えると、波が建物を伝わって一部の階だけがずれるので、0.3秒の遅れで追いかけるようにした。離すと引くばねが消え、建物は固有周期で揺れ出す。

2Dのグラフ

3Dの絵を描いたあとに、札と3つのグラフを2Dの面へ重ねる。揺れの記録は0.25秒ごとの最小と最大を起きた順に残す。ゆっくりした揺れはなめらかな線に、速い揺れは帯に見える。周期を上げていく場面では、共振曲線の上に測った比を点で打っている。

文字とグラフ(src/sims/quake-building.ts の 483〜643 行)
function panels(api: Api) {  const u = api.u, pad = u(10);  if (api.w < 760) {    const H1 = clamp(api.h * 0.1, u(62), u(82)), H2 = clamp(api.h * 0.15, u(84), u(120)), w2 = (api.w - 3 * pad) / 2, y2 = api.h - pad - H2, y1 = y2 - pad * 0.7 - H1;    return { hist: { x: pad, y: y1, w: api.w - 2 * pad, h: H1 }, res: { x: pad, y: y2, w: w2, h: H2 }, dr: { x: 2 * pad + w2, y: y2, w: w2, h: H2 }, frac: (api.h - y1 + pad) / api.h, narrow: true };  }  const ph = clamp(api.h * 0.24, u(100), u(176)), pw = Math.min((api.w - 4 * pad) / 3, u(350)), y = api.h - pad - ph;  return { hist: { x: pad, y, w: pw, h: ph }, res: { x: 2 * pad + pw, y, w: pw, h: ph }, dr: { x: 3 * pad + 2 * pw, y, w: pw, h: ph }, frac: (ph + 2 * pad) / api.h, narrow: false };}/** 上へ切り上げた、きりのよい数 */function nice(x: number) {  const e = Math.pow(10, Math.floor(Math.log10(Math.max(1e-9, x)))), q = x / e;  return (q <= 1 ? 1 : q <= 1.5 ? 1.5 : q <= 2 ? 2 : q <= 3 ? 3 : q <= 5 ? 5 : q <= 7 ? 7 : 10) * e;}const lineStyle = (api: Api, i: number, n: number): { w: number; dash?: number[] } => (i === n - 1 ? { w: api.u(2.2) } : i === n - 2 ? { w: api.u(1) } : { w: api.u(1), dash: [api.u(3), api.u(3)] });const lenText = (m: number) => (m >= 1 ? `${fmt(m, 2)} m` : m >= 0.1 ? `${fmt(m * 100, 0)} cm` : m >= 0.01 ? `${fmt(m * 100, 1)} cm` : `${fmt(m * 1000, 1)} mm`);const roundText = (m: number) => (m >= 1 ? `${m} m` : m >= 0.01 ? `${Math.round(m * 100)} cm` : `${Math.round(m * 1000)} mm`);const angleText = (d: number) => (d < 1e-5 ? "0" : `1/${Math.round(1 / d)}`);function legend(api: Api, s: S, R: Rect, fs: number) {  let lx = R.x + R.w - api.u(6);  for (let i = 0; i < s.bs.length; i++) {    const b = s.bs[i];    api.text(b.name, lx, R.y + fs * 1.05, { size: fs * 0.8, align: "right" });    lx -= api.ctx.measureText(b.name).width + api.u(3);    api.line(lx - api.u(12), R.y + fs * 0.72, lx, R.y + fs * 0.72, lineStyle(api, i, s.bs.length));    lx -= api.u(18);  }}function drawHist(api: Api, s: S, R: Rect, fs: number, narrow: boolean) {  const u = api.u; api.rect(R.x, R.y, R.w, R.h, { fill: "paper", w: u(1) });  api.text(R.w < u(345) && s.bs.length > 2 ? "最上階の揺れ" : "最上階の揺れ(過去 1 分)", R.x + u(6), R.y + fs * 1.05, { size: fs * 0.88 });  legend(api, s, R, fs);  const x0 = R.x + Math.max(u(38), fs * 3.9), w = R.x + R.w - u(6) - x0, y0 = R.y + fs * 1.5, h = R.h - fs * 1.5 - u(5), gh = h * 0.24, ih = h - gh - u(3), n = s.hn, pts = s.pts;  // 地面の速さ(上の帯)  const g = s.g; let vm = 1e-6; for (let k = 0; k < n; k++) vm = Math.max(vm, Math.abs(g.hA[k]), Math.abs(g.hB[k]));  const gy = y0 + gh / 2;  api.line(x0, gy, x0 + w, gy, { w: u(0.6), dash: [u(2), u(4)] });  api.text("地面", x0 - u(5), gy + fs * 0.3, { size: fs * 0.78, align: "right" });  const put = (A: Float32Array, B: Float32Array, y: (q: number) => number) => {    let c = 0;    const dx = w / (HN - 1) / 4;    for (let k = 0; k < n; k++) { const i = (s.hi - n + k + HN) % HN, px = x0 + w - ((n - 1 - k) / (HN - 1)) * w; pts[c++] = px - dx; pts[c++] = y(A[i]); pts[c++] = px + dx; pts[c++] = y(B[i]); }    return c;  };  if (n > 1) api.poly(pts.subarray(0, put(g.hA, g.hB, (q) => gy - (q / vm) * gh * 0.46)), { w: u(0.8) });  // 最上階  let m = 0.001; for (const b of s.bs) for (let k = 0; k < n; k++) m = Math.max(m, Math.abs(b.hA[k]), Math.abs(b.hB[k]));  const top = nice(m), cy = y0 + gh + u(3) + ih / 2, Y = (q: number) => cy - (clamp(q, -top, top) / top) * (ih / 2);  api.line(x0, cy, x0 + w, cy, { w: u(0.7) });  api.text(`±${roundText(top)}`, x0 - u(4), Y(top) + fs * 0.75, { size: fs * 0.72, align: "right" });  if (n > 1) s.bs.forEach((b, i) => { api.poly(pts.subarray(0, put(b.hA, b.hB, Y)), lineStyle(api, i, s.bs.length)); });}function drawRes(api: Api, s: S, R: Rect, fs: number, narrow: boolean) {  const u = api.u; api.rect(R.x, R.y, R.w, R.h, { fill: "paper", w: u(1) });  api.text(narrow ? "共振曲線" : "共振曲線(最上階のずれ ÷ 地面の揺れ幅)", R.x + u(6), R.y + fs * 1.05, { size: fs * 0.88 });  const x0 = R.x + u(narrow ? 22 : 28), w = R.w - u(narrow ? 28 : 36), y0 = R.y + fs * 1.5, h = R.h - fs * 1.5 - fs * 1.35;  const X = (T: number) => x0 + (w * Math.log(clamp(T, T_MIN, T_MAX) / T_MIN)) / Math.log(T_MAX / T_MIN);  let m = 3; for (const b of s.bs) for (let j = 0; j < NP; j++) m = Math.max(m, b.curve[j]);  const top = nice(m), Y = (q: number) => y0 + h - (Math.min(q, top) / top) * h;  api.line(x0, y0 + h, x0 + w, y0 + h, { w: u(0.8) }); api.line(x0, y0, x0, y0 + h, { w: u(0.8) });  for (const T of narrow ? [0.2, 1] : [0.2, 0.5, 1, 2, 5]) { api.line(X(T), y0 + h, X(T), y0 + h + u(3), { w: u(0.8) }); api.text(`${T}`, X(T), y0 + h + fs * 1.1, { size: fs * 0.76, align: "center", font: "mono" }); }  api.text("10 秒", x0 + w, y0 + h + fs * 1.1, { size: fs * 0.76, align: "right", font: "mono" });  api.text(`${top}`, x0 - u(3), y0 + fs * 0.5, { size: fs * 0.76, align: "right", font: "mono" });  api.text("1", x0 - u(3), Y(1) + fs * 0.3, { size: fs * 0.76, align: "right", font: "mono" });  // 入力の周期の印  const key = api.str("wave"), g = s.g;  if (key === "short" || key === "long") {    const Tc = WAVES[key].Tc, a = X(Tc / 1.6), b = X(Tc * 1.6);    api.hatch([[a, y0], [b, y0], [b, y0 + h], [a, y0 + h]], { gap: u(5), w: u(0.7) });  } else if (key === "sine" || key === "sweep") {    const T = g.T || api.num("period");    api.line(X(T), y0, X(T), y0 + h, { w: u(1.1), dash: [u(3), u(3)] });    const lab = `${fmt(T, 2)} 秒`, xL = clamp(X(T), x0 + u(26), x0 + w - u(26));    api.label(lab, xL, y0 + fs * 0.75, { size: fs * 0.78, align: "center", font: "mono", pad: 1 });  }  const pts = s.pts;  s.bs.forEach((b, i) => {    for (let j = 0; j < NP; j++) { pts[2 * j] = X(periodAt(j)); pts[2 * j + 1] = Y(b.curve[j]); }    api.poly(pts.subarray(0, 2 * NP), lineStyle(api, i, s.bs.length));  });  // 周期を上げていく揺れで、実際に測った比を点で重ねる  if (key === "sweep") for (let j = 0; j < NP; j++) if (s.sw[j] > 0) api.dot(X(periodAt(j)), Y(s.sw[j]), u(1.7));}function drawDrift(api: Api, s: S, R: Rect, fs: number, narrow: boolean) {  const u = api.u; api.rect(R.x, R.y, R.w, R.h, { fill: "paper", w: u(1) });  api.text(narrow ? "層間変形角の最大" : "層間変形角の最大(階の高さごと)", R.x + u(6), R.y + fs * 1.05, { size: fs * 0.88 });  const x0 = R.x + u(narrow ? 15 : 22), w = R.w - u(narrow ? 24 : 34), y0 = R.y + fs * 1.5, h = R.h - fs * 1.5 - fs * 1.35;  let m = 0; for (const b of s.bs) m = Math.max(m, b.maxD);  const top = [1 / 400, 1 / 200, 1 / 100, 1 / 50, 1 / 25, 1 / 10, 1 / 5, 1 / 2, 1].find((q) => q >= m * 1.02) ?? 1;  const X = (q: number) => x0 + (Math.min(q, top) / top) * w;  api.line(x0, y0 + h, x0 + w, y0 + h, { w: u(0.8) }); api.line(x0, y0, x0, y0 + h, { w: u(0.8) });  api.text("上", x0 - u(3), y0 + fs * 0.6, { size: fs * 0.76, align: "right" }); api.text("下", x0 - u(3), y0 + h, { size: fs * 0.76, align: "right" });  for (const q of [top, top / 2]) { api.line(X(q), y0, X(q), y0 + h, { w: u(0.6), dash: [u(2), u(4)] }); api.text(`1/${Math.round(1 / q)}`, q === top ? x0 + w : X(q), y0 + h + fs * 1.1, { size: fs * 0.76, align: q === top ? "right" : "center", font: "mono" }); }  const pts = s.pts;  s.bs.forEach((b, i) => {    let c = 0;    for (let f = 0; f < b.N; f++) { const px = X(b.peak[f]); pts[c++] = px; pts[c++] = y0 + h - (f / b.N) * h; pts[c++] = px; pts[c++] = y0 + h - ((f + 1) / b.N) * h; }    api.poly(pts.subarray(0, c), lineStyle(api, i, s.bs.length));  });}function overlay(api: Api, s: S) {  const u = api.u, v = s.v, K = api.num("scale"), gx = K * s.g.dg, fs = clamp(u(12.5), 11, 15), P = panels(api), pad = u(10);  for (const b of s.bs) {    const xt = b.x0 + gx + K * b.x[b.top], yt = (b.kind === "iso" ? 3.8 : 0.5) + b.N * STORY, a = v.project([xt, yt, 0]);    if (!a.on) continue;    const r = u(5), g = s.grab && s.grab.b === b && s.grab.i === b.top;    api.rect(a.x - r, a.y - r, 2 * r, 2 * r, { fill: g ? "ink" : "paper", w: u(1.5) });    const ly = a.y - (b.kind === "tmd" ? u(62) : u(14)), lx = clamp(a.x, u(58), api.w - u(58));    api.label(`${b.name}  T₁ ${fmt(b.Tf, 2)} 秒`, lx, ly - fs * 1.25, { size: fs, align: "center" });    api.label(`±${lenText(b.amp)}`, lx, ly, { size: fs, align: "center", font: "mono" });  }  if (s.grab) {    const g = s.grab, b = g.b, F = b.pull!.kp * Math.abs(b.pull!.target - b.x[g.i]), floor = g.i - b.f0 + 1, y = (b.kind === "iso" ? 3.8 : 0.5) + floor * STORY, a = v.project([b.x0 + gx + K * b.x[g.i], y, 0]);    if (a.on) { api.circle(a.x, a.y, u(7), { w: u(1.6), fill: "paper" }); api.label(`${floor} 階を引く  ${lenText(Math.abs(b.x[g.i]))}  ${fmt(F / 1e6, 2)} MN`, clamp(a.x + u(14), pad, api.w - u(170)), a.y - u(12), { size: fs }); }  }  // 倍率の見本: 1 本の線の長さが、実際のどれだけのずれに見えるか  const main = s.bs[s.bs.length - 1], yt = main.N * STORY, c0 = v.project([main.x0, yt, 0]), c1 = v.project([main.x0 + K, yt, 0]), per = Math.abs(c1.x - c0.x);  let len = 1; for (const q of [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5]) if (per * q >= u(20)) { len = q; break; }  api.label(`変形は ${K} 倍で表示${P.narrow ? "" : "(地面のずれも同じ)"}`, pad, pad + fs, { size: fs, weight: 600 });  if (per > 1) {    const bx = pad, by = pad + fs * 2, lp = Math.min(per * len, u(170));    api.line(bx, by, bx + lp, by, { w: u(1.8) }); api.line(bx, by - u(3), bx, by + u(3), { w: u(1.2) }); api.line(bx + lp, by - u(3), bx + lp, by + u(3), { w: u(1.2) });    api.label(`= 本当は ${roundText(len)}`, bx + lp + u(5), by + fs * 0.34, { size: fs * 0.88 });  }  drawHist(api, s, P.hist, fs, P.narrow); drawRes(api, s, P.res, fs, P.narrow); drawDrift(api, s, P.dr, fs, P.narrow);}function readout(api: Api, s: S) {  const g = s.g, key = api.str("wave"), bs = s.bs, b = bs[bs.length - 1], a = bs[0], V = api.num("amp");  const tt = (T: number) => `${fmt(T, 2)} 秒`;  let wave = "地面の揺れ  なし";  if (key === "sine" || key === "sweep") {    const T = key === "sine" ? api.num("period") : sweepT(g.t);    wave = `地面の揺れ  ${key === "sine" ? "正弦波" : "周期を上げていく"}  周期 ${tt(T)}    ±${lenText((V / 100 * T) / (2 * Math.PI))}    最大加速度 ${fmt((V / 100 * 2 * Math.PI) / T, 2)} m/s²`;    if (key === "sweep" && g.ended) wave = `地面の揺れ  止まった  あと ${fmt(g.wait, 0)} 秒でもう一度`;  } else if (key === "short" || key === "long") {    const W = makeWave(api, key), name = key === "short" ? "短周期の多い合成波" : "長周期の多い合成波";    wave = g.ended ? `地面の揺れ  止まった  あと ${fmt(g.wait, 0)} 秒でもう一度` : `地面の揺れ  ${name}  残り ${fmt(W.dur - g.t, 0)} 秒    最大加速度 ${fmt((W.pa * V) / 100, 2)} m/s²`;  }  const lines: string[] = [];  if (s.layout === "iso") {    lines.push(`固有周期  そのまま ${tt(a.T[0])}    免震 ${tt(b.Tf)}`, wave,      `最上階の加速度の最大  そのまま ${fmt(a.peakAbs, 2)} m/s²    免震 ${fmt(b.peakAbs, 2)} m/s²`,      `層間変形角の最大  そのまま ${angleText(a.maxD)}    免震 ${angleText(b.maxD)}`,      `免震層の変形の最大  ${lenText(b.peakDev)}`);  } else if (s.layout === "tmd") {    lines.push(`固有周期 T₁  ${tt(b.T[0])}    重りの周期  ${tt(b.Td)}`, wave,      `最上階の揺れ  そのまま ±${lenText(a.amp)}(最大 ${lenText(a.peakTop)})    制振 ±${lenText(b.amp)}(最大 ${lenText(b.peakTop)})`,      `重りの振れ(最上階から)  ±${lenText(b.ampDev)}`);  } else if (s.layout === "soft") {    const res = (q: Bld) => lenText(Math.abs(q.dl0[0]));    lines.push(`固有周期  そのまま ${tt(a.T[0])}    弱い 1 階 ${tt(b.T[0])}`, wave,      `層間変形角の最大  そのまま ${angleText(a.maxD)}(${a.maxAt} 階)    弱い 1 階 ${angleText(b.maxD)}(${b.maxAt} 階)`,      `今の 1 階のずれ  そのまま ${res(a)}    弱い 1 階 ${res(b)}`);  } else {    const Ts = b.T.slice(0, 3).map((T, i) => `T${"₁₂₃"[i]}  ${tt(T)}`).join("    ");    lines.push(`固有周期(${b.N} 階)  ${Ts}`, wave,      `最上階の揺れ  ±${lenText(b.amp)}(最大 ${lenText(b.peakTop)})    測った周期  ${b.Tm > 0 && b.amp > 1e-3 ? tt(b.Tm) : "-"}`,      `層間変形角の最大  ${bs.length > 1 ? bs.map((q) => `${q.name} ${angleText(q.maxD)}`).join("    ") : `${angleText(b.maxD)}(${b.maxAt} 階)`}`);  }  api.readout(lines);}

小さな図の左の3棟は、固有振動の形にを掛けて描くだけなので、時間を積分しない。右の1棟だけ、指を追いかける地面の加速度を使って計算している。

地面を揺らす(src/sims/quake-building-modes.ts の 34〜52 行)
/** 指の位置を追いかける地面(なめらかにつなぐ 2 次の追従)。その加速度を建物に与え、速度ベルレ法で 1 刻み進める */function advance(api: Api, s: S, h: number) {  const W = 26, M = s.M, n = M.N, hd = api.num("damp") / 100, w1 = (2 * Math.PI) / M.T[0], cc = (2 * hd) / w1;  const ag = W * W * (s.want - s.g) - 2 * W * s.gv;  s.gv += ag * h; s.g += s.gv * h; s.t += h;  for (let i = 0; i < n; i++) { s.v[i] += 0.5 * h * s.a[i]; s.x[i] += h * s.v[i]; }  for (let i = 0; i < n; i++) {    const dl = s.x[i] - (i ? s.x[i - 1] : 0), du = i + 1 < n ? s.x[i + 1] - s.x[i] : 0, vl = s.v[i] - (i ? s.v[i - 1] : 0), vu = i + 1 < n ? s.v[i + 1] - s.v[i] : 0;    const ku = i + 1 < n ? M.k[i + 1] : 0;    // 層の力と、固さに比例する減衰。地面の加速度は全部の階に同じ力として働く    s.a[i] = (-M.k[i] * (dl + cc * vl) + ku * (du + cc * vu)) / M.m - ag;  }  for (let i = 0; i < n; i++) s.v[i] += 0.5 * h * s.a[i];  const xt = s.x[n - 1], f = Math.exp(-h / (1.2 * M.T[0]));  s.amp = Math.max(Math.abs(xt), s.amp * f); s.gAmp = Math.max(Math.abs(s.g), s.gAmp * f);  // 指の周期: 地面の速さが上向きに 0 を横切る間隔  if (s.last < 0 && s.gv >= 0 && s.gAmp > 0.01) { if (s.tUp >= 0) s.Tf = s.t - s.tUp; s.tUp = s.t; }  s.last = s.gv;}

画面の部品(描画・場面の切り替え・画質の自動調整・GPU の網点)は全記事で共通です。シミュレーションの共通の仕組みで解説しています。

プログラム全体を見る(src/sims/quake-building.ts・709 行)

読み込んでいます

プログラム全体を見る(src/sims/quake-building-modes.ts・126 行)

読み込んでいます

姉妹サイト

株式会社Game が運営するメディアです。