K-Shoot Mania v2 曲線レーザー仕様についての質問

カテゴリ: 質問
(スレッド番号: No. 113)
1 : ME.ARi
2026-03-02 16:14:03  
K-Shoot Mania Editor alpha 3を利用させていただいています。

曲線レーザーの仕様について確認したい点があり、投稿しました。

私の理解では、現在の曲線レーザーは2次ベジェ曲線(quadratic Bezier curve)を使用しており、a と b の値が中間の weight point(制御点)の座標として機能している構造だと認識しています。まずこの理解が正しいかどうか確認したいです。

また、a と b の値が 0〜100 の範囲に制限(clamp)されているように見受けられます。2次ベジェ曲線の性質上、制御点は理論的には範囲外にも設定可能だと思いますが、現状では少なくともどちらか一方は 100 を超えられない仕様のように見えます。この制限は設計上意図されたものでしょうか。

(0,0) から (100,100) へ接続する sin 曲線を近似できないかと考え、別途計算したところ、最適な制御点はおよそ (50, 108.27) 付近になりました。しかし現在の仕様ではその値をそのまま入力することができないため、実装は事実上不可能だと判断しています。

もしこの制限を解除した場合、内部処理や描画面で問題が発生する可能性はあるのでしょうか。
設計意図や技術的な制約があれば教えていただきたいです。

よろしくお願いいたします。
2 : Nitro
2026-03-02 20:29:10  
Sorry if I'm wrong, but isn't a sin curve weaker than a quad? It seems like (50, 108.27) would result in a stronger curve.

I assume you need a sin curve because you are trying to achieve the result of 50% position at 1/3rd of the duration of the Laser. I think you could approximate a sin curve with that specific value with (59.76, 100) or (50, 87.50).
https://imgur.com/a/QZUFJ4V
3 : masaka(管理人)
2026-03-02 21:23:24 (編集: 2026-03-02 21:27:48)  
>>1 ME.ARiさん
ご連絡ありがとうございます!

はい、曲線の数式は2次ベジェ曲線で、aが制御点のx座標、bが制御点のy座標です。
具体的には以下の数式で計算されます。

f(x) = 2(1-t)tb + t^2
t = (a - sqrt(a^2 + x - 2ax)) / (-1 + 2a)

ここで、a,bは曲線パラメータ、xは進行率、0≦a,b,x≦1です。

レーザーにおいては途中で折り返さない単調な関数である必要があるため、これを保証するために0≦a,b≦1に制限しています。
※これは十分条件のため、この値域を外れた場合でも単調な関数になることはあります

0≦a,b≦1を外れた値は譜面仕様として受け付けないため、特定の式をより正確に近似したい場合はレーザーを2分割するなどして挿入する必要がある場合があります。

曲線レーザーの(a, b)については私が発案した数式ではないため設計意図は正確か分からないですが、私の理解としては上記となります。
具体的な数式は下記のDrewolさんの投稿をご参照ください。
https://github.com/kshootmania/ksm-chart-format/issues/1#issuecomment-1118140549
https://github.com/kshootmania/ksm-chart-format/issues/1#issuecomment-1118140716

曲線の数式を評価する処理のソースコード:
https://github.com/kshootmania/libkson/blob/09ff1b588f6c6822153ebfa0d5ceff9b302da493/src/Util/GraphCurve.cpp#L8
4 : masaka(管理人)
2026-03-02 21:34:08  
Nitroさんが記載されているように、(59.76, 100)でsinを近似できそうですね。

計算過程は忘れてしまいましたが、私が過去にばねエフェクト(swing)を(a,b)パラメータで近似しようとした際に近似計算した際も同じように(100, 59), (0, 59)になっていたので、おそらくそちらの値は正しそうです。
https://github.com/kshootmania/ksm-chart-format/commit/c447c93ffd3646c9a1ab00a3aea2f30238cb27d1#diff-55bfe0459ad698695578f97366eb4ed2003f1304399ddb706f7856564964d492L566
5 : ME.ARi
2026-03-05 10:56:17  
ご回答ありがとうございます!
実は私は現状でも十分満足しているのですが、友人から代わりに質問してほしいと頼まれていた内容だったので、いただいた回答はきちんと伝えておきます。

ちなみに座標に関する部分ですが、正確なコードを教えていただいたおかげで、Pythonで簡単な学習モデルを作り、近似値を探してみました。すぐに実行できるコードと結果の画像を下に添付しています。

私自身はそれほど気にしている部分ではないのですが、もし何かの参考になればと思い共有させていただきました。
いつもありがとうございます!

ちなみに、私は専門的にコーディングをしているわけではないので、このやり方が正しいのかはあまり自信がありません。:)
-----------------------------------------------------------

import numpy as np
import matplotlib.pyplot as plt


AMPLITUDE = 30.0
NUM_SAMPLES = 20000
LEARNING_RATE = 0.003
EPOCHS = 50000
TRAIN_SAMPLES = NUM_SAMPLES
FD_EPS = 1e-5
START_POINTS = [(0.5, 1.0), (0.5, 0.5), (0.3, 1.0), (0.7, 1.0)]


def target_curve(t, amplitude=AMPLITUDE):
    x = 100.0 * t
    y = x + amplitude * np.sin(np.pi * t)
    return np.column_stack((x, y))


def evaluate_curve_cpp_style(a, b, x):
    discriminant = a * a + x - 2.0 * a * x
    d_sqrt = np.sqrt(np.maximum(discriminant, 0.0))

    if a < 0.25:
        denom = -1.0 + 2.0 * a
        t = (a - d_sqrt) / denom
    else:
        t = x / (a + d_sqrt)

    return 2.0 * (1.0 - t) * t * b + t * t


def fit_control_point_cpp_style(
    x_train, y_train, init_a, init_b, lr=LEARNING_RATE, epochs=EPOCHS, eps=FD_EPS
):
    a = float(init_a)
    b = float(init_b)
    history = []

    def loss_at(a_val, b_val):
        pred = evaluate_curve_cpp_style(a_val, b_val, x_train)
        return np.mean((pred - y_train) ** 2)

    for epoch in range(epochs):
        loss = loss_at(a, b)

        grad_a = (loss_at(a + eps, b) - loss_at(a - eps, b)) / (2.0 * eps)
        grad_b = (loss_at(a, b + eps) - loss_at(a, b - eps)) / (2.0 * eps)
        a -= lr * grad_a
        b -= lr * grad_b

        if epoch % 100 == 0 or epoch == epochs - 1:
            history.append(loss)

    return a, b, history, loss_at(a, b)


def main():
    t = np.linspace(0.0, 1.0, NUM_SAMPLES)
    target_points = target_curve(t)
    x_norm = target_points[:, 0] / 100.0
    y_norm = target_points[:, 1] / 100.0
    train_idx = np.linspace(0, NUM_SAMPLES - 1, TRAIN_SAMPLES, dtype=int)
    x_train = x_norm[train_idx]
    y_train = y_norm[train_idx]

    best = None
    for init_a, init_b in START_POINTS:
        a_try, b_try, loss_history_try, final_loss_try = fit_control_point_cpp_style(
            x_train, y_train, init_a=init_a, init_b=init_b
        )
        if best is None or final_loss_try < best[0]:
            best = (final_loss_try, a_try, b_try, loss_history_try, init_a, init_b)

    _, a_norm, b_norm, loss_history, best_init_a, best_init_b = best
    cpp_b = 100.0 * evaluate_curve_cpp_style(a_norm, b_norm, x_norm)
    cpp_mse = np.mean((cpp_b - target_points[:, 1]) ** 2)

    print(f"Best init = (a={100.0*best_init_a:.2f}, b={100.0*best_init_b:.2f})")
    print(f"Learned weight point = (a={100.0*a_norm:.6f}, b={100.0*b_norm:.6f})")
    print(f"C++-style EvaluateCurve MSE = {cpp_mse:.6f}")

    fig, axes = plt.subplots(1, 2, figsize=(12, 5))

    # Display with horizontal axis b and vertical axis a.
    target_a, target_b = target_points[:, 0], target_points[:, 1]
    axes[0].plot(target_b, target_a, label="Target sin curve", linewidth=2)
    axes[0].plot(cpp_b, target_a, "--", linewidth=2, label="Fitted C++ EvaluateCurve")
    axes[0].scatter([0.0, 100.0 * b_norm, 100.0], [0.0, 100.0 * a_norm, 100.0], c=["black", "red", "black"], zorder=3)
    axes[0].text(100.0 * b_norm + 2, 100.0 * a_norm, f"w=(a={100.0*a_norm:.2f}, b={100.0*b_norm:.2f})")
    axes[0].set_title("Curve Fit (C++ EvaluateCurve)")
    axes[0].set_xlabel("b")
    axes[0].set_ylabel("a")
    axes[0].legend()
    axes[0].grid(alpha=0.25)
    axes[0].axis("equal")

    axes[1].plot(np.arange(len(loss_history)) * 100, loss_history)
    axes[1].set_title("Training Loss")
    axes[1].set_xlabel("Epoch")
    axes[1].set_ylabel("MSE")
    axes[1].grid(alpha=0.25)

    plt.tight_layout()
    plt.show()


if __name__ == "__main__":
    main()

-----------------------------------------------------------

https://imgur.com/a/QlhRLMD

このスレッドに返信する