HSPポータル
サイトマップ お問い合わせ


HSPTV!掲示板


未解決 解決 停止 削除要請

2021
0919
アキアキノヒロロ[getangr]での角度取得について11解決


アキアキノヒロロ

リンク

2021/9/19(Sun) 09:09:06|NO.93915

以前、「物理設定を利用した動きの制御方法」の NO.89074 で
[getangr]での角度取得について書きました。
http://hsp.tv/play/pforum.php?mode=pastwch&num=88831
また、去年のコンテストにも、
「=Hgimg4=Test_Sample」内の「角度取得時の注意=getangr.hsp」として、応募しました。

しかし、これらについて反応がないままです。
この方法が有効なものなのか、検証していただけないでしょうか。

再録になりますが、一応、「getangr.hsp」は、以下のものです。
(「sample/hgimg4」に入れて実行)

#include "hgimg4.as" gpreset setcls CLSMODE_SOLID, $404040 gpload id,"res/tamane_sd/tamane_2" ; モデル読み込み setscale id, 0.05, 0.05, 0.05 setpos id, 0, 0, 0 gpfloor id_floor, 30, 30, $00ffff ; 床ノードを追加 setpos GPOBJ_CAMERA, 0, 12, 25 ; カメラ位置を設定 repeat stick key if key&16 { ; 16 スペースキー addangr_OK++ if addangr_OK=2 : addangr_OK=0 } if addangr_OK=0 : addangr id, 0, 1, 0 ;〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓 getangr id, angr_x, angr_y, angr_z if angr_x=0 : y=angr_y+64 if angr_x=128 | angr_x=-128 : y=192-angr_y angr255_y = y - 64 : if angr255_y < 0 : angr255_y = 192 + y ;〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓 gplookat GPOBJ_CAMERA, 0, 9, 0 ; カメラから指定した座標を見る redraw 0 ; 描画開始 gpdraw ; シーンの描画 font "",20 pos 20,20 color 255,255,255 mes "getangr id, (angr_x), (angr_y), (angr_z)" mes " " color 255,255,0 mes "[ angr ] = "+ angr_x +" / "+ angr_y +" / "+ angr_z ; [id_modelの角度] mes " " mes " " color 255,255,255 mes "if (angr_x) = 0 : y = (angr_y) + 64" mes "if (angr_x) = 128 | (angr_x) = -128 : y = 192 - (angr_y)" mes "(angr255_y) = y - 64 : if (angr255_y) < 0 : (angr255_y) = 192 + y" mes " " color 255,255,0 mes "[ angr255_y ] = "+angr255_y redraw 1 ; 描画終了 await 1000/60 *10 ; 待ち時間 loop
おにたまさん からは
「クォータニオンで管理されているため、取得した角度と完全に一致させることがなかなか難しい」
と言われていますが。



この記事に返信する


Rosh

リンク

2021/9/19(Sun) 15:11:39|NO.93920

オイラー角で物体の角度を設定
(HGIMG4内でクォータニオンに変換される)
物体の角度をオイラー角で取得
という処理の結果
設定した角度と取得した角度が違う
という事を問題だと思ってるんですよね?

クォータニオンとオイラー角の相互変換はそういうものです
調べてみれば良いと思います

オイラー角は、向きを手動で設定したり
数値表示する際には直感的で良いけど
計算で色んな方向に回転させるにはかなり不便な表現なので
固執しない方が良いです



アキアキノヒロロ

リンク

2021/9/19(Sun) 23:23:58|NO.93924

>設定した角度と取得した角度が違う
>という事を問題だと思ってるんですよね?

言葉が足りなかったようです。
角度が違うというのではなく、その同じ角度を表す方法が、[getangr]での取得の時と、[setangr]での設定の時では違うらしく、ある一つの回転軸の取得数値そのままを設定数値に代入することが出来ず、ひと工夫が必要になってしまう、ということです。
そのひと工夫というのが、ここに載せたスクリプトなのです。
例えば、ここではY軸についてならば、[angr_y] を [angr255_y] に変換(?)すれば、X軸Z軸[0]のまま、Y軸設定数値に[angr255_y]を代入出来るようになるのです。
何故、[angr]にこだわるのかというと、あるモデルのY軸角度の変化量を別のモデルのY軸角度の変化量にしたいためです。[quat]のほうでは、パラメーターが(x,y,z,w)の実数型の変数で、この変化量を出すことがより困難に思えたからです。

ただ、これを組み込んだプログラムが、よくここの箇所でエラーになるようで困っています。プログラムが相当に込み入ってから生じたエラーで、うまく原因が掴めないので、この変換は使い物にならないのか、と考え込んでしまったのです。

しかし、次のように[angr255_y]変換代入で[setangr]したモデル(右側のもの)も、[getquat]したそのままを[setquat]したモデル(左側のもの)も、元のモデルとまるっきり同じ回転になるので、[angr255_y]変換は正常に働いていることがわかります。
以前のものに手を加えた次のスクリプトを実行し、Ctrlキーで両側のモデルを中央のモデルに近付けたりしてみて下さい。

#include "hgimg4.as" gpreset setcls CLSMODE_SOLID, $404040 gpload id_a,"res/tamane_sd/tamane_2" ; 中央=元モデル読み込み setscale id_a, 0.05, 0.05, 0.05 setpos id_a, 0, 0, 10 xx=0 gpload id_b,"res/tamane_sd/tamane_2" ; 左=[setquat]用モデル読み込み setscale id_b, 0.05, 0.05, 0.05 setpos id_b, -5+xx, 0, 10 gpload id_c,"res/tamane_sd/tamane_2" ; 右=[angr255_y]変換代入の[setangr]用モデル読み込み setscale id_c, 0.05, 0.05, 0.05 setpos id_c, 5-xx, 0, 10 gpfloor id_floor, 30, 30, $00ffff ; 床ノードを追加 setpos GPOBJ_CAMERA, 0, 12+15, 25 ; カメラ位置を設定 repeat ; タッチでカメラ位置を動かす if dragmd { ; ドラッグ中 getkey a,1 if a=1 { dx=0.05*(mousex-dragx)+cx dy=0.05*(mousey-dragy)+cy setpos GPOBJ_CAMERA, dx,dy,cz } else { dragmd=0 } } else { ; ドラッグなし getkey a,1 if a { dragx=mousex:dragy=mousey getpos GPOBJ_CAMERA, cx,cy,cz dragmd=1 } } stick key ; 両側のモデルを中央の元モデルに近付ける if key&64 { ; 64 Ctrlキー xx++ : if xx=6 : xx=0 } setpos id_b, -5+xx, 0, 10 setpos id_c, 5-xx, 0, 10 ; 元モデル回転を一時停止/再開 if key&16 { ; 16 スペースキー addangr_OK++ if addangr_OK=2 : addangr_OK=0 } if addangr_OK=0 : addangr id_a, 0, 1, 0 ;〓〓[angr255_y]変換〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓 getangr id_a, angr_x, angr_y, angr_z if angr_x=0 : y=angr_y+64 if angr_x=128 | angr_x=-128 : y=192-angr_y angr255_y = y - 64 : if angr255_y < 0 : angr255_y = 192 + y ;〓〓[angr255_y]変換〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓〓 setangr id_c, 0, angr255_y, 0 ; 右モデル=[angr255_y]変換代入の[setangr] getquat id_a,x,y,z,w setquat id_b,x,y,z,w ; 左モデル=[setquat] gplookat GPOBJ_CAMERA, 0, 9-9, 0 ; カメラから指定した座標を見る redraw 0 ; 描画開始 gpdraw ; シーンの描画 font "",20 pos 20,20 color 255,255,255 mes "getangr id_a, (angr_x), (angr_y), (angr_z)" mes " " color 255,255,0 mes "[ angr ] = "+ angr_x +" / "+ angr_y +" / "+ angr_z ; [id_modelの角度] mes "[ quat ] = "+ x +" / "+ y +" / "+ z +" / "+ w mes " " color 255,255,255 mes "if (angr_x) = 0 : y = (angr_y) + 64" mes "if (angr_x) = 128 | (angr_x) = -128 : y = 192 - (angr_y)" mes "(angr255_y) = y - 64 : if (angr255_y) < 0 : (angr255_y) = 192 + y" mes " " color 255,255,0 mes "[ angr255_y ] = "+angr255_y redraw 1 ; 描画終了 await 1000/60 *10 ; 待ち時間 loop



アキアキノヒロロ

リンク

2021/9/20(Mon) 03:17:52|NO.93926

読込みモデルのスクリプト箇所、間違えました。中央/左/右3つとも

>gpload id_a,"res/tamane_sd/tamane_2"
は、
>gpload id_a,"res/tamane2"

でした。これで、「sample/hgimg4」に入れて実行です。



usagi

リンク

2021/9/20(Mon) 11:42:17|NO.93927

こんにちわ。
角度の表し方はsetもgetも設定は一緒だと思います。
y軸±64でx,z軸が反転しますから、y軸だけ回転させる仕様であれば
そのまま69行目に設定すれば一致すると思うのですが、

setangr id_c, angr_x, angr_y, angr_z
何か不都合がありますでしょうか。


アキアキノヒロロさんの提示されたスクリプトは、この条件下では
仰る通り”正常”に動いているものと思われますが、これを組み込んだプログラムでエラーが
よく出ているとのことで、どの様なエラーかは想像ですが、あるモデルの変化量にしたいとのことなので、
オイラー脳で考えると、二つの方向の回転の補完は難しく、ジンバルロック、フリップの問題など、
3Dをオイラー角で考えた時の問題に直面しているのだと予測しています。

オイラー角を基準とした回転はRoshさんが指摘している通りかなぁとは思います。
※限定した使い方ではオイラー角の方が簡単なのかと思いますが、
 例えばロケットを飛ばして回転させてたりすると、困ってくるかと思います。

一例ですが、クォータニオンで補完した場合です、任意の傾きに最短で合わせる事が出来ます。
aのモデルが補完で動きますので、b,cの角度を変えたり回してみてください。
最近の3Dゲームなどでよく使われている手法です。


#module // ベクトルと角度からクオータニオンを求める #deffunc qtset array a_qt, double a_x, double a_y, double a_z, double a_w _r = a_w / 2 _sin = sin(_r) a_qt.0 = a_x * _sin a_qt.1 = a_y * _sin a_qt.2 = a_z * _sin a_qt.3 = cos(_r) return #defcfunc qtang double a_w return 2.0 * atan(sqrt(1.0-a_w*a_w), a_w) // acos // 内積 #defcfunc qtdot array a_q1, array a_q2 return a_q1.0 * a_q2.0 + a_q1.1 * a_q2.1 + a_q1.2 * a_q2.2 + a_q1.3 * a_q2.3 // 球面線形補間 #deffunc qtslerp array a_q, array a_q1, array a_q2, double t repeat 4 q1.cnt = a_q1.cnt q2.cnt = a_q2.cnt loop dot = qtdot(q1, q2) if dot < 0.0 { q2 = -q2.0, -q2.1, -q2.2, -q2.3 dot = -dot } if dot > 0.9995 { _t = 1.0 - t repeat 4 a_q.cnt = _t * q1.cnt + t * q2.cnt loop return } theta = atan(sqrt(1.0-dot*dot), dot) sin_theta = sin(theta) s1 = sin((1.0 - t)*theta) / sin_theta s2 = sin(t * theta) / sin_theta repeat 4 a_q.cnt = s1 * q1.cnt + s2 * q2.cnt loop return #global #include "hgimg4.as" gpreset : setcls CLSMODE_SOLID, $404040 gpload id_a,"res/tamane2" ; 中 setscale id_a, 0.05, 0.05, 0.05 setpos id_a gpload id_b,"res/tamane2" ; 左 setscale id_b, 0.05, 0.05, 0.05 setpos id_b, -5, 0, 0 gpload id_c,"res/tamane2" ; 右 setscale id_c, 0.05, 0.05, 0.05 setpos id_c, 5, 0, 0 gpfloor id_floor, 30, 30, $00ffff ; 床ノードを追加 setpos GPOBJ_CAMERA, 0, 10, 20 ; カメラ位置を設定 t=0.0 setease 0,1, ease_quartic_inout|ease_loop repeat getkey esc, 27 : if esc : end t += 0.01 setangr id_b, 0,-32,32 ; 右 getquat id_b,x,y,z,w qt_b = x, y, z, w setangr id_c, 0,32,-32 ; 左 getquat id_c,x,y,z,w qt_c = x, y, z, w qtslerp qt_a, qt_b, qt_c, geteasef(t) setquat id_a, qt_a.0, qt_a.1, qt_a.2, qt_a.3 ; 中は右と左を補完する gplookat GPOBJ_CAMERA, ,5, redraw 0 gpdraw font "",20 pos 20,20 color 255,255,255 mes "右は過去のクオータニオンとする" color 255,255,0 mes strf("quat: %+.4f, %+.4f, %+.4f, %+.4f", qt_b.0, qt_b.1, qt_b.2, qt_b.3) color 255,255,255 mes "左は未来のクオータニオンとする" color 255,255,0 mes strf("quat: %+.4f, %+.4f, %+.4f, %+.4f", qt_c.0, qt_c.1, qt_c.2, qt_c.3) color 255,255,255 mes "中は右と左を補完するよ" color 255,255,0 mes strf("quat: %+.4f, %+.4f, %+.4f, %+.4f", qt_a.0, qt_a.1, qt_a.2, qt_a.3) redraw 1 await 1000/60 loop



usagi

リンク

2021/9/21(Tue) 01:18:25|NO.93930

連続投稿失礼します。
もう少し直感的な例を用意してみました。(クォータニオンの回転入れてなかったので)
十字キーで角度を操作できます。
角度を操作した時、おそらくクォータニオンの結果がイメージした感覚に近しいと思います。
オイラーでの結果は状態によってイメージと違う動きをしてしまうと思います。
※1※2に疑問へのコメント書いてます。

hgimg4で表現できることが増えてますので、クォータニオンのサポート関数が用意されると、
もっと便利に使う人が増えるかと考えてます。(昔Easy3Dが流行ってた時みたいに)


#module #deffunc qtset array a_qt, double a_x, double a_y, double a_z, double a_w _r = a_w / 2 : _sin = sin(_r) a_qt.0 = a_x * _sin : a_qt.1 = a_y * _sin : a_qt.2 = a_z * _sin a_qt.3 = cos(_r) return #deffunc qtmul array a_qt, array a_q1, array a_q2 a_qt.0 = a_q1.3*a_q2.0 + a_q2.3*a_q1.0 + a_q1.1*a_q2.2 - a_q1.2*a_q2.1 a_qt.1 = a_q1.3*a_q2.1 + a_q2.3*a_q1.1 - a_q1.0*a_q2.2 + a_q1.2*a_q2.0 a_qt.2 = a_q1.3*a_q2.2 + a_q2.3*a_q1.2 + a_q1.0*a_q2.1 - a_q1.1*a_q2.0 a_qt.3 = a_q1.3*a_q2.3 - a_q1.0*a_q2.0 - a_q1.1*a_q2.1 - a_q1.2*a_q2.2 return #global #include "hgimg4.as" chdir dir_exe+"\\sample\\hgimg4" gpreset setcls CLSMODE_SOLID, $404040 gpload id_a,"res/tamane2" ; 上左:動かせる setscale id_a, 0.01,0.01,0.01 setpos id_a, -1.5, 1, 0 gpload id_b,"res/tamane2" ; 上右:クォータニオンでY軸回転させる setscale id_b, 0.01,0.01,0.01 setpos id_b, 1.5, 1, 0 gpload id_c,"res/tamane2" ; 下左:オイラーでの回転; X->Y->Z setscale id_c, 0.01,0.01,0.01 setpos id_c, -3,-1, 0 gpload id_d,"res/tamane2" ; 下中:オイラーでの回転;Y->X->Z setscale id_d, 0.01,0.01,0.01 setpos id_d, 0,-1, 0 gpload id_e,"res/tamane2" ; 下右:したオイラーでの回転;Y->X->Z setscale id_e, 0.01,0.01,0.01 setpos id_e, 3,-1, 0 setpos GPOBJ_CAMERA, 0, 0, 10 ; カメラ位置を設定 ax = 0.0 : az = 0.0 : ay = 0.0 ; 角度 repeat ay += 0.1 ; Y軸は自動で回す stick pad, 15, 0 ; XZ軸はキー操作 if pad&1 : az+=0.1 if pad&2 : ax-=0.1 if pad&4 : az-=0.1 if pad&8 : ax+=0.1 ; 上左を好きに回転できる setang id_a, ax,0,az ; y軸周りにay回転させるクォータニオンを作成 ; ※1 「X軸Z軸[0]で変化させたい」という要望の答え ;   そういった操作はクォータニオンが便利 qtset ziku, 0,1,0, ay ; 回転させる(つまりクォータニオンを掛け合わす) ; ※2 「この変換は使い物に…」という疑問の変換の答え ;   状況によっては使えるが、場合によっては使い物にならない getquat id_a, x,y,z,w : qt_a = x,y,z,w ; 上左のクォータニオン取得 qtmul qt_r, qt_a, ziku ; 上左と軸を掛ける setquat id_b, qt_r.0, qt_r.1, qt_r.2, qt_r.3 ; セットする ; オイラー脳で考えると都合に合わせて回転順番を考慮しなくてくてはいけない setang id_c, ax,ay,az ; X->Y->Z setangy id_d, ax,ay,az ; Y->X->Z setangz id_e, ax,ay,az ; Z->Y->X gplookat GPOBJ_CAMERA, ,, redraw 0 : gpdraw font "",20 pos 20,20 : color 255,255,0 mes strf("角度: %+4.4f, %+4.4f, %+4.4f", ax, ay, az), 4 color 255,255,255 pos 270,200 : mes "A: 自由に動かせる    B: クォータニオン",4 pos 200,360 : mes "C: X->Y->Z  D: Y->X->Z  E: Z->Y->X",4 redraw 1 : await 1000/60 loop



アキアキノヒロロ

リンク

2021/9/21(Tue) 13:21:38|NO.93932

対象のモデルは、X軸Z軸は、回転させていないものです。偶発的にもY軸以外回転はしないはずなので、多分別の原因でエラーになっていると思います。
この条件なら、「※限定した使い方ではオイラー角の方が簡単」なはず、というかオイラー角でしか考えられない、初心者同然の私でも、どうにかなる。
ということで、どうにかしたのが、[angr255_y]です。

どうも自分がやりたかったことをうまく説明できていませんでした。
対象のモデルをある角度に徐々にY軸回転させたかったのです。
>そのまま69行目に設定すれば一致する
確かに、その角度に変えるだけならば、
>setangr id_c, angr_x, angr_y, angr_z
で、そのまま代入すればいいだけの話です。
しかし、取得したY軸のオイラー角が「-63〜64」の数値だと、その数値を徐々に変化させているその途中の時点(y軸±64)で、X軸Z軸のオイラー角も反転させないといけないことになります。
Y軸の徐々の変化だけで、徐々に回転させるには、Y軸のオイラー角を「0〜255」に直して使う必要があったということです。

ここまで自分で書いてきて、はたと気付きました。
それなら、そのある角度まで、
>addangr id, 0, 1, 0
というように、Y軸を[1]ずつ[addangr]していけばいいだけだ。そのある角度になった時点で止めればいい。なんでこんなことに気付かなかったのか、お恥ずかしい。
この方法で、エラーにならないかやってみようと思います。

usagiさん、色々、教えて頂いて、有難うございます。まだまだ、分らないことばかりで、理解しきれていません。勉強していきたいです。

>hgimg4で表現できることが増えてますので、クォータニオンのサポート関数が用意されると、
>もっと便利に使う人が増えるかと考えてます。(昔Easy3Dが流行ってた時みたいに)

クォータニオンについて、一から勉強しないといけないと思いますが、私も usagiさんに同感です。
まずは、ここで教えて頂いた方法も、自分で思い通りに使えるようになりたいです。

追伸.) 失礼ですが、「usagi」さん とは、『珠音ちゃんとカボチャのオバケ』等の作者さんの「ウサギ」さん(「takeshima」 さん)ではありませんか? 
間違いでしたら、お詫び致します。
もし、同一の方でいらっしゃったら、今までも、色々お世話になったので、重ねて御礼申し上げます。たとえ、そうでなくともですが。



usagi

リンク

2021/9/21(Tue) 20:29:08|NO.93937

いいえ、私は別のusagiでございます。
紛らわしくてすみません。


>この条件なら、「※限定した使い方ではオイラー角の方が簡単」なはず
そうですね。仰る通りかと思います。
その条件でYを0〜255に丸めたいというお話しであれば、
私であれば場合分けとモジュロで書くかもです。


if angr_x = 0 { angr255_y = ( angr_y+256) \ 256 } else { angr255_y = (-angr_y+384) \ 256 }


>クォータニオンについて
数学的な理解からだと難しいと思いますが、
計算の道具としての理解やゲーム等での使い方から入ると
使いやすくなると思います。

3Dを表現するには、計算の道具が足りなく、四則演算だけでは難しいので、
複素数 > 四元数(クォータニオン)
とだんだん次元を増やしてみると考えが理解しやすいかもです。



アキアキノヒロロ

リンク

2021/9/22(Wed) 08:25:52|NO.93943

おにたまさんが、
>よりスマートな角度の取得方法などがあれば、今後対応していきたいところです
とおっしゃっていますが、どうも対応の優先順位は低いようです。

[Easy3D]を使わせて頂いていた私としては、あの使い勝手のよさが[hgimg4]でも叶えられたらと、初心者同然の者の思いです。

usagiさんがここで作って下さったこのような用途に応じたモジュール、そういったライブラリを夢見ます。
usagiさんのような方が、作って頂けると嬉しいのですが。

色々と、お心遣い有難うございました。

以上、一応の解決と致します。



アキアキノヒロロ

リンク

2021/9/25(Sat) 22:01:32|NO.93969

解決としましたが、試しに、自分の[angr255_y]変換の代わりに、usagiさんの

if angr_x = 0 { angr255_y = ( angr_y+256) \ 256 } else { angr255_y = (-angr_y+384) \ 256 }
を使わせて頂くと、今までのエラーがうそのように、全く起こらなくなりました。
何故だか、色々考えてみても分らないのですが、よかったです。
ご報告まで。



usagi

リンク

2021/9/28(Tue) 04:19:28|NO.94001

解決したのですね。良かったです。

エラーがどういった物なのか分らないので憶測ですが、アキアキノヒロロさんのスクリプトだと可能性としては。。。
angr_y が -64〜0〜64 の値をとると限定すると、0〜255に収まりそうですが、
angr_y が -255〜0〜255 左右回り255の値をとりうる可能性を考えると
破綻する所がでてきます。
私の式はその辺りをサポートしてみている違いがあります。

直接的な原因か分らないので、ロギングするのが良いかと思いますので、
エラーがでるスクリプトに下記を追加して、エラーが出たタイミングで停止してログをみて、
本当に値が想定通りなのか見てみるなど如何でしょうか。

logmes strf("%+3d, %+3d, %+3d = %+3d ", angr_x, angr_y, angr_z, angr255_y)
エラー対象の値がはっきりしているなら assert を使えば、
勝手にエラーでブレイクしてくれますので、ログが見やすいかとは思います。



アキアキノヒロロ

リンク

2021/9/28(Tue) 07:20:00|NO.94002

>angr_y が -255〜0〜255 左右回り255の値をとりうる可能性を考えると

まさしく、その通りでした。
ここに載せた検証のためのスクリプトでは、

>addangr id_a, 0, 1, 0
>getangr id_a, angr_x, angr_y, angr_z

での、[angr255_y]の値の変化を見ているため、ここでの[angr_y]は必ず[-64〜0〜64]の値に収まっていました。
しかし、エラーの出ていた元のプログラムでは、

>angr_x=0 : angr_y=[-255〜0〜255] : angr_z=0
>setangr id_a, angr_x, angr_y, angr_z

といった命令の箇所から取った[angr_y]を[angr255_y]変換させている場合が生じていました。
この時も、「getangr id_a, angr_x, angr_y, angr_z」してから、[angr255_y]変換させればいいものを、
そのままの[angr_y]を使っていたため、「-255〜0〜255 左右回り255の値」をとっていたのです。

重ね々々、有難うございます。
[logmes strf]等、さらに学ぶべきことが増えました。



ONION software Copyright 1997-2023(c) All rights reserved.