2010年09月26日
lsl 戦車砲をシミュレートする
表題の相談を受けまして作成しましたが、内容の説明はチャットだけではつらい為、中身の説明が主な目的となります。
とりあえず、動画から詳しい動きを解析しました。
1.砲塔そのものは車体の現在の傾きに対して何かを中心に左右にだけ回転すること。
2.砲身は砲塔の現在の傾きに対して付け根を中心にして上下にだけ回転すること。
3.限界の回転速度が各々あること。
4.1、2、3を合成した結果が照準に追随すること。
という動き方をしているようです。
車体が回転していない状況では、対空砲と基本的に同じです。ただし、水平回転と垂直回転を各々砲塔、砲身に分担させればよいということになります。
車体の回転や砲塔の回転を考慮する今回の場合は、対空砲ではリージョン座標(SIMの座標)系で水平垂直の回転の合成として向くべき方向をそのまま計算していたものを、対象の方向を自分の土台(ルートまたは砲塔)のローカル座標系に変換してから水平垂直の回転の合成に置き換えることになります。
この種の直交座標系の変換は、自分の回転状態の逆回転で絶対座標系の値(ベクトル)を回転させ、自分は回転していないものと見做してもろもろの計算を行います。位置関係が必要であれば自分の絶対座標系での座標を目標の座標から引きます。
次に、砲塔も砲身も回転中心がプリムの中心(スクリプトで回転させる際の実際の回転中心)とはずれていますので、vectorの回転の方法を使って回転中心をずらして表現します。(下図)
砲身は自身の偏心だけでなく、砲塔の旋回による位置変更も伴います。これらも上図と同様に位置変更を計算して合成する必要があります。
4.は照準を指すまでの回転と移動を特定の距離・回転角度に抑え込んで補間移動するという事になりますので、位置移動と回転について姿勢の補間を使って、さらに最初~最後までの変更速度を一定にするために、何らかの基準になる変更速度との比を用いて表現します。
この種の中間姿勢の補間方法は一般式にすると
その時刻の姿勢=始点の姿勢×Fstart(T)+終わりの姿勢×Fend(T)
(但T=0.0(始点)~1.0(終点)、Fstart(T)+Fend(T)=1.0で一定の関数)
です。
与えるTの値が0~1に変化していくときに、Tの値によって始点と終点の合成された姿勢が戻ってきます。また、Tの刻み幅が一定で変化するときに戻る姿勢の刻み幅は関数Fstart(X)、Fend(X)の戻りに依存します。普通は姿勢の刻みが一定に見えるように関数を作りますので、たとえば180度を回転する場合のTの刻み(⊿T0)をあらかじめ決めておいて、
今回のTの刻み=⊿T0*PI/今回の始点から終点までの回転角度
として用いれば良いでしょう。
移動には線形補間を、回転には球面線形補間を使うのが普通だと思います。
今回の運動では回転ですべてが表現されていますので、rotationの球面線形補間を使うことになります。なお、クオータニオンの補間内部で使う4次元ベクトルの内積のアークコサインは、180度での値はPI_BY_TWOになります。ですので、Tの刻みは、
今回のTの刻み=⊿T0*PI_BY_TWO/今回の始点と終点のrotationの内積の値のアークコサイン
とします。
次に、スクリプトの作りを少し考察します。
リンクされたプリムを回転させる場合、なめらかに動作するllSetLinkPrimitiveParamsFast()を利用することを前提とします。
照準の取得はタイマーなりlink_messageなりになるかと思われますが、rotationの座標系を回転処理本体に合わせることを要求すると、クオータニオンの知識が利用者にも必要となる可能性があるため、利用の便利さを考えますと、照準による戦車砲の回転の始点と終点は、標準の関数で簡単に割り出せ、扱いも簡単なベクトルで設定する事にしておいて、処理側でローテーションに勝手に変換する形で指定させる方が汎用性は高くなると思われます。
ルートが直接回転操作の対象となる様な(つまり砲塔や砲身がルートプリムであるような)乗り物はまずないと思いますので、回転操作の対象は子プリムに限定します。
メンテナンス性は子プリムにスクリを入れるよりルートにすべてを入れた方がよいのではないかと思いますので、プリム番号の取得のような物を使ってあらかじめ対象の番号を設定することを前提にして、ルートから操作することにしました。
全てスクリプトの冒頭に集めておき、ここだけ修正すればよいようにしています。
TARGET_PRIM_NUMBER
スクリプトが回転操作を行う対象のリンク番号を設定します。1以下の数字(ルートを表す)の場合は実行されません。
SYNC_PRIM_NUMBER
FALSEまたは砲塔のリンク番号を設定します。リンク番号が指定の場合はそのプリムの回転によって相対位置を保ちながら補正します。FALSEの場合はルートとの相対位置を維持しようとします。
ENABLED_RL
TRUEにすると左右に旋回します。FALSEにすると左右旋回しません。
ENABLED_UD
TRUEにすると上下に旋回します。FALSEにすると上下旋回しません。
DECENTERING
回転中心の、プリムの中心からのずれをvectorで指定します。
VEC_END
この変数に照準の方向(目標を指差すベクトル)を指定します。初期状態で特定方向を向ける場合などには、ここに方向を示すリージョン座標を設定をしておけばそちらを向きますが、内部で上書きされる変数のため、別途デフォルトの向きを指定する変数を用意しない限り、普通は意味がないと思われます。
DT_PI
180度旋回する場合の補間の刻み(T値の刻み幅)を指定します。
小さいほどゆっくり旋回します。1以上の値にするとすぐさま目標の方向を向きます。ゼロ以下の場合は回転しません。
関数 init_params()
スクリプトの保存時とリセットの時に呼び出されます。対象プリムの初期状態の回転と、ルートまたは連動対象との相対位置関係を取得しています。
対象プリム設定がルートを指している場合FALSE、その他はTRUEを返します。
関数 init()
初期化関数ですが何もせずサンプル(センサーイベント)を起動しています。
実行開始時とrezの時に呼び出されます。
sensorイベント
照準設定のサンプルが入っています。
最も近い距離にいるアバターの方向を、自分から見た方向として変数VEC_ENDに設定しています。
カメラ方向などを指定する場合は回転初期値startVecの設定と同じように、llRot2Fwd(カメラ方向)などとすれば良いかと思われます。
また、回転スクリプトがカメラ操作権限を取ることはないと思いますので、実際に乗り物に利用時はセンサーではなくlink_messageなどの中で設定を行うことになり、timerイベント(回転処理本体)の開始もlink_messageの中になるだろうと思われます。
attachイベント
使っていませんが、改造用に変数isAttachにアタッチされているかどうかを設定しています。
timerイベント
実際の回転処理が入っています。
アタッチメント、REZの両者に対応するために、llGetLinkPrimitiveParams()を多用しています。
これは、従来関数のllGet系はアタッチとREZで動作が異なって少々面倒な場合がある為、いつでもリージョン座標系の値しか返さないllGetLinkPrimitiveParams()を使っているという意味です。ルートに置くことを前提にしているためでもありますが、子プリムに入れる場合は従来関数を使用している部分を数か所、適正に修正するだけでよいはずです。(※取消:ルートのローカル回転は依然、ルートのllGetLocalRot()でなければ取れませんでした。)(※再度取消:llGetLinkPrimitiveParams()でPRIM_ROT_LOCALを指定してルートの回転を取れば子プリムに入っていてもアタッチ・REZを問わず動作させられます。同様に回転の実行時にもPRIM_ROT_LOCALを使う方が簡単になります。)
実行の順序的には、
1.与えられた始点と終点のベクトルを現在の自分のローカル座標系から見たrotationに変換
2.微小回転を球面線形補間(関数 interpolationRot())を用いて計算
3.砲身の場合(SYNC_PRIM_NUMBERに値がある)には回転土台をそれとして位置補正を計算
4.回転を実行
となっています。
以下詳細。。
timer()
{
最初に、回転対象がルートは、パラメーターミスのため実行せず戻ります。
if (TARGET_PRIM_NUMBER<=1)
{
llSetTimerEvent(0.0);
return;
}
ルート回転をllGetLocalRot()で取得します。リージョン回転またはアタッチポイントとの相対回転が取得されます。
avRotは正確にはアタッチメント、REZを問わずrootプリム(アバター※)のリージョン回転になりますが、アバターのアニメは含まれない為、アタッチで使用する場合はアニメーションの方ではアタッチ位置の回転が行われない様にしなければなりません。(※修正:気になって調べてみたところllGetRootRotation()と同じ戻りの模様でした。)
rotation rootRot=llGetLocalRot();
rotation avRot=llList2Rot(llGetLinkPrimitiveParams(LINK_ROOT,[PRIM_ROTATION]),0);
座標系の変換処理
照準のベクトルを自分のルートプリムの現在の回転で割ってローカル回転にしています。
vector norm=llVecNorm(startVec/avRot);
vector norm2=llVecNorm(VEC_END/avRot);
旋回土台の回転を取得
rotation parentRot=avRot;
if (SYNC_PRIM_NUMBER)
{
連動対象がある場合はそちらの回転を採用
parentRot=llList2Rot(llGetLinkPrimitiveParams(SYNC_PRIM_NUMBER,[PRIM_ROTATION]),0);
照準のベクトルも連動対象のローカル座標系の単位ベクトルに再設定します。
自分の土台から見た自分自身の回転だけを計算させる方が(恐らく)改造が楽なため、このようにしてあります。(たとえば砲身の仰角・俯角に限界を設ける場合は、このようにしておけば自分のY軸旋回だけを見て限界判定できますので。。)
norm=llVecNorm(startVec/parentRot);
norm2=llVecNorm(VEC_END/parentRot);
}
ベクトルを水平面垂直面回転に変換しています。
正面軸がX軸でない場合はllEuler2Rot()やllRotBetween()の中の値がある座標軸を変更します。
rotation startRot=llEuler2Rot(<0,(llAcos(norm.z)-PI/2.0)*(float)ENABLED_UD,0>)*llRotBetween(<1,0,0>,<norm.x*(float)ENABLED_RL,norm.y*(float)ENABLED_RL,0>);
rotation endRot=llEuler2Rot(<0,(llAcos(norm2.z)-PI/2.0)*(float)ENABLED_UD,0>)*llRotBetween(<1,0,0>,<norm2.x*(float)ENABLED_RL,norm2.y*(float)ENABLED_RL,0>);
T値の刻みを補正しています。
angleBetweenRot()は球面線形補間内で使われるアークコサインの値を返します。
180度旋回時の値PI_BY_TWOをそれで割ってT刻みの初期値に掛けることで見かけの回転速度を一定にします。
dT=1.0;
float a=angleBetweenRot(startRot,endRot);
if (a!=0.0)
{
dT=PI_BY_TWO/a*DT_PI;
}
else
{
無回転のため、処理を終えて戻ります。
llSetTimerEvent(0.0);
return;
}
今回の回転を球面線形補間で計算しています。
Tの値が1以上(回転終了)の場合にはタイマーを停止します。
T+=dT;
if (T>=1.0) llSetTimerEvent(0.0);
rotation rot=interpolationRot(startRot,endRot,T);
位置補正(連動があれば)を計算します。
初期状態では位置は偏心成分と、ルートから見た相対位置として設定します。
vector pos=DECENTERING;
rotation rot_sync=ZERO_ROTATION;
vector pos_sync=RELATIVE_POS;
if (SYNC_PRIM_NUMBER)
{
連動プリムが設定されている場合は値を上書きします。
対象のローカル回転を取得します。連動プリムのリージョン回転をルートのリージョン回転で割って、ローカル回転成分だけを取得します。
rot_sync=parentRot/avRot;
座標移動をルートからのローカル位置関係として計算します。
リージョン座標系での相対移動距離を計算後に、ルートのリージョン回転と逆回転してローカル座標系に移しています。
pos_sync=(RELATIVE_POS*parentRot+llList2Vector(llGetLinkPrimitiveParams(SYNC_PRIM_NUMBER,[PRIM_POSITION]),0)-llList2Vector(llGetLinkPrimitiveParams(LINK_ROOT,[PRIM_POSITION]),0))/avRot;
}
回転を合成します。
自分のデフォルトの回転、今回の回転、土台の回転を合成します。
rot=defaultMyRot*rot*rot_sync;
移動量を合成します。
pos_syncはルートからの移動量となっています。
自分の偏心成分(pos内)は今回の回転全体をかけて方向を修正します。
pos=pos*rot+pos_sync;
さらに回転全体をルートプリムの回転で割ります。(LSLのバグ?のため必要な決まり事です。)
rot=rot/rootRot;
姿勢変更を実行します。
llSetLinkPrimitiveParamsFast(TARGET_PRIM_NUMBER,[PRIM_POSITION,pos,PRIM_ROTATION,rot]);
}
概要
とりあえず、動画から詳しい動きを解析しました。
1.砲塔そのものは車体の現在の傾きに対して何かを中心に左右にだけ回転すること。
2.砲身は砲塔の現在の傾きに対して付け根を中心にして上下にだけ回転すること。
3.限界の回転速度が各々あること。
4.1、2、3を合成した結果が照準に追随すること。
という動き方をしているようです。
構造
車体が回転していない状況では、対空砲と基本的に同じです。ただし、水平回転と垂直回転を各々砲塔、砲身に分担させればよいということになります。
車体の回転や砲塔の回転を考慮する今回の場合は、対空砲ではリージョン座標(SIMの座標)系で水平垂直の回転の合成として向くべき方向をそのまま計算していたものを、対象の方向を自分の土台(ルートまたは砲塔)のローカル座標系に変換してから水平垂直の回転の合成に置き換えることになります。
この種の直交座標系の変換は、自分の回転状態の逆回転で絶対座標系の値(ベクトル)を回転させ、自分は回転していないものと見做してもろもろの計算を行います。位置関係が必要であれば自分の絶対座標系での座標を目標の座標から引きます。
次に、砲塔も砲身も回転中心がプリムの中心(スクリプトで回転させる際の実際の回転中心)とはずれていますので、vectorの回転の方法を使って回転中心をずらして表現します。(下図)
砲身は自身の偏心だけでなく、砲塔の旋回による位置変更も伴います。これらも上図と同様に位置変更を計算して合成する必要があります。
4.は照準を指すまでの回転と移動を特定の距離・回転角度に抑え込んで補間移動するという事になりますので、位置移動と回転について姿勢の補間を使って、さらに最初~最後までの変更速度を一定にするために、何らかの基準になる変更速度との比を用いて表現します。
この種の中間姿勢の補間方法は一般式にすると
その時刻の姿勢=始点の姿勢×Fstart(T)+終わりの姿勢×Fend(T)
(但T=0.0(始点)~1.0(終点)、Fstart(T)+Fend(T)=1.0で一定の関数)
です。
与えるTの値が0~1に変化していくときに、Tの値によって始点と終点の合成された姿勢が戻ってきます。また、Tの刻み幅が一定で変化するときに戻る姿勢の刻み幅は関数Fstart(X)、Fend(X)の戻りに依存します。普通は姿勢の刻みが一定に見えるように関数を作りますので、たとえば180度を回転する場合のTの刻み(⊿T0)をあらかじめ決めておいて、
今回のTの刻み=⊿T0*PI/今回の始点から終点までの回転角度
として用いれば良いでしょう。
移動には線形補間を、回転には球面線形補間を使うのが普通だと思います。
今回の運動では回転ですべてが表現されていますので、rotationの球面線形補間を使うことになります。なお、クオータニオンの補間内部で使う4次元ベクトルの内積のアークコサインは、180度での値はPI_BY_TWOになります。ですので、Tの刻みは、
今回のTの刻み=⊿T0*PI_BY_TWO/今回の始点と終点のrotationの内積の値のアークコサイン
とします。
次に、スクリプトの作りを少し考察します。
リンクされたプリムを回転させる場合、なめらかに動作するllSetLinkPrimitiveParamsFast()を利用することを前提とします。
照準の取得はタイマーなりlink_messageなりになるかと思われますが、rotationの座標系を回転処理本体に合わせることを要求すると、クオータニオンの知識が利用者にも必要となる可能性があるため、利用の便利さを考えますと、照準による戦車砲の回転の始点と終点は、標準の関数で簡単に割り出せ、扱いも簡単なベクトルで設定する事にしておいて、処理側でローテーションに勝手に変換する形で指定させる方が汎用性は高くなると思われます。
ルートが直接回転操作の対象となる様な(つまり砲塔や砲身がルートプリムであるような)乗り物はまずないと思いますので、回転操作の対象は子プリムに限定します。
メンテナンス性は子プリムにスクリを入れるよりルートにすべてを入れた方がよいのではないかと思いますので、プリム番号の取得のような物を使ってあらかじめ対象の番号を設定することを前提にして、ルートから操作することにしました。
設定項目
全てスクリプトの冒頭に集めておき、ここだけ修正すればよいようにしています。
TARGET_PRIM_NUMBER
スクリプトが回転操作を行う対象のリンク番号を設定します。1以下の数字(ルートを表す)の場合は実行されません。
SYNC_PRIM_NUMBER
FALSEまたは砲塔のリンク番号を設定します。リンク番号が指定の場合はそのプリムの回転によって相対位置を保ちながら補正します。FALSEの場合はルートとの相対位置を維持しようとします。
ENABLED_RL
TRUEにすると左右に旋回します。FALSEにすると左右旋回しません。
ENABLED_UD
TRUEにすると上下に旋回します。FALSEにすると上下旋回しません。
DECENTERING
回転中心の、プリムの中心からのずれをvectorで指定します。
VEC_END
この変数に照準の方向(目標を指差すベクトル)を指定します。初期状態で特定方向を向ける場合などには、ここに方向を示すリージョン座標を設定をしておけばそちらを向きますが、内部で上書きされる変数のため、別途デフォルトの向きを指定する変数を用意しない限り、普通は意味がないと思われます。
DT_PI
180度旋回する場合の補間の刻み(T値の刻み幅)を指定します。
小さいほどゆっくり旋回します。1以上の値にするとすぐさま目標の方向を向きます。ゼロ以下の場合は回転しません。
内容(改造のための要旨)
関数 init_params()
スクリプトの保存時とリセットの時に呼び出されます。対象プリムの初期状態の回転と、ルートまたは連動対象との相対位置関係を取得しています。
対象プリム設定がルートを指している場合FALSE、その他はTRUEを返します。
関数 init()
初期化関数ですが何もせずサンプル(センサーイベント)を起動しています。
実行開始時とrezの時に呼び出されます。
sensorイベント
照準設定のサンプルが入っています。
最も近い距離にいるアバターの方向を、自分から見た方向として変数VEC_ENDに設定しています。
カメラ方向などを指定する場合は回転初期値startVecの設定と同じように、llRot2Fwd(カメラ方向)などとすれば良いかと思われます。
また、回転スクリプトがカメラ操作権限を取ることはないと思いますので、実際に乗り物に利用時はセンサーではなくlink_messageなどの中で設定を行うことになり、timerイベント(回転処理本体)の開始もlink_messageの中になるだろうと思われます。
attachイベント
使っていませんが、改造用に変数isAttachにアタッチされているかどうかを設定しています。
timerイベント
実際の回転処理が入っています。
アタッチメント、REZの両者に対応するために、llGetLinkPrimitiveParams()を多用しています。
これは、従来関数のllGet系はアタッチとREZで動作が異なって少々面倒な場合がある為、いつでもリージョン座標系の値しか返さないllGetLinkPrimitiveParams()を使っているという意味です。ルートに置くことを前提にしているためでもあります
実行の順序的には、
1.与えられた始点と終点のベクトルを現在の自分のローカル座標系から見たrotationに変換
2.微小回転を球面線形補間(関数 interpolationRot())を用いて計算
3.砲身の場合(SYNC_PRIM_NUMBERに値がある)には回転土台をそれとして位置補正を計算
4.回転を実行
となっています。
以下詳細。。
timer()
{
最初に、回転対象がルートは、パラメーターミスのため実行せず戻ります。
if (TARGET_PRIM_NUMBER<=1)
{
llSetTimerEvent(0.0);
return;
}
ルート回転をllGetLocalRot()で取得します。リージョン回転またはアタッチポイントとの相対回転が取得されます。
avRotは
rotation rootRot=llGetLocalRot();
rotation avRot=llList2Rot(llGetLinkPrimitiveParams(LINK_ROOT,[PRIM_ROTATION]),0);
座標系の変換処理
照準のベクトルを自分のルートプリムの現在の回転で割ってローカル回転にしています。
vector norm=llVecNorm(startVec/avRot);
vector norm2=llVecNorm(VEC_END/avRot);
旋回土台の回転を取得
rotation parentRot=avRot;
if (SYNC_PRIM_NUMBER)
{
連動対象がある場合はそちらの回転を採用
parentRot=llList2Rot(llGetLinkPrimitiveParams(SYNC_PRIM_NUMBER,[PRIM_ROTATION]),0);
照準のベクトルも連動対象のローカル座標系の単位ベクトルに再設定します。
自分の土台から見た自分自身の回転だけを計算させる方が(恐らく)改造が楽なため、このようにしてあります。(たとえば砲身の仰角・俯角に限界を設ける場合は、このようにしておけば自分のY軸旋回だけを見て限界判定できますので。。)
norm=llVecNorm(startVec/parentRot);
norm2=llVecNorm(VEC_END/parentRot);
}
ベクトルを水平面垂直面回転に変換しています。
正面軸がX軸でない場合はllEuler2Rot()やllRotBetween()の中の値がある座標軸を変更します。
rotation startRot=llEuler2Rot(<0,(llAcos(norm.z)-PI/2.0)*(float)ENABLED_UD,0>)*llRotBetween(<1,0,0>,<norm.x*(float)ENABLED_RL,norm.y*(float)ENABLED_RL,0>);
rotation endRot=llEuler2Rot(<0,(llAcos(norm2.z)-PI/2.0)*(float)ENABLED_UD,0>)*llRotBetween(<1,0,0>,<norm2.x*(float)ENABLED_RL,norm2.y*(float)ENABLED_RL,0>);
T値の刻みを補正しています。
angleBetweenRot()は球面線形補間内で使われるアークコサインの値を返します。
180度旋回時の値PI_BY_TWOをそれで割ってT刻みの初期値に掛けることで見かけの回転速度を一定にします。
dT=1.0;
float a=angleBetweenRot(startRot,endRot);
if (a!=0.0)
{
dT=PI_BY_TWO/a*DT_PI;
}
else
{
無回転のため、処理を終えて戻ります。
llSetTimerEvent(0.0);
return;
}
今回の回転を球面線形補間で計算しています。
Tの値が1以上(回転終了)の場合にはタイマーを停止します。
T+=dT;
if (T>=1.0) llSetTimerEvent(0.0);
rotation rot=interpolationRot(startRot,endRot,T);
位置補正(連動があれば)を計算します。
初期状態では位置は偏心成分と、ルートから見た相対位置として設定します。
vector pos=DECENTERING;
rotation rot_sync=ZERO_ROTATION;
vector pos_sync=RELATIVE_POS;
if (SYNC_PRIM_NUMBER)
{
連動プリムが設定されている場合は値を上書きします。
対象のローカル回転を取得します。連動プリムのリージョン回転をルートのリージョン回転で割って、ローカル回転成分だけを取得します。
rot_sync=parentRot/avRot;
座標移動をルートからのローカル位置関係として計算します。
リージョン座標系での相対移動距離を計算後に、ルートのリージョン回転と逆回転してローカル座標系に移しています。
pos_sync=(RELATIVE_POS*parentRot+llList2Vector(llGetLinkPrimitiveParams(SYNC_PRIM_NUMBER,[PRIM_POSITION]),0)-llList2Vector(llGetLinkPrimitiveParams(LINK_ROOT,[PRIM_POSITION]),0))/avRot;
}
回転を合成します。
自分のデフォルトの回転、今回の回転、土台の回転を合成します。
rot=defaultMyRot*rot*rot_sync;
移動量を合成します。
pos_syncはルートからの移動量となっています。
自分の偏心成分(pos内)は今回の回転全体をかけて方向を修正します。
pos=pos*rot+pos_sync;
さらに回転全体をルートプリムの回転で割ります。(LSLのバグ?のため必要な決まり事です。)
rot=rot/rootRot;
姿勢変更を実行します。
llSetLinkPrimitiveParamsFast(TARGET_PRIM_NUMBER,[PRIM_POSITION,pos,PRIM_ROTATION,rot]);
}
斜面に沿って回転させる
秋祭り開催中(lsl+α:任意形状のあたり判定をする)
lsl 連立一次方程式を解く
lsl 多倍長演算
lsl 特定の面のサイズと4隅のリージョン座標を取得する
lsl 座標系の変換(vector)
秋祭り開催中(lsl+α:任意形状のあたり判定をする)
lsl 連立一次方程式を解く
lsl 多倍長演算
lsl 特定の面のサイズと4隅のリージョン座標を取得する
lsl 座標系の変換(vector)
Posted by RBK Drachnyd(しお) at 14:09│Comments(0)
│算数