7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

高校生のためのPyxelとPymunk 変型する! -- 三角関数と行列

7
Last updated at Posted at 2025-12-10

 数学の三角関数や、線形代数の行列を使っていろいろな動きを表現します。インプット、アウトプットが何なのか、コードを実行し、いろいろいじってみましょう。
 昔買った参考書籍:DXライブラリ本「14歳からはじめる リアルに動く!ゲーム物理プログラミング教室 C言語編 Windows 98/2000/Me/XP/Vista対応(大槻有一郎著)」の目次を参考にして、三角関数と行列を使ってpyxelでのサンプルプログラムを作りました。今回はPymunkの出番はありません。

4.1 三角関数の基礎(sin, cosの意味と役割)

半径180の円周上を緑のボール(現在の角度の点半径5)が回ります。
sinの値を縦方向の長さとして表示(赤の線)
cosの値を横方向の長さとして表示(青の線)
画面640x480の中央に単位円(半径200ピクセル)を描画。
角度を0〜359度で毎フレーム変化させ、対応するcos, sinの値を計算。
cosは横方向の成分、sinは縦方向の成分として青線・赤線で表示。
単位円上の点が角度に応じて動き、三角関数の意味を視覚的に理解できる。

import pyxel
import math

class App:
    def __init__(self):
        pyxel.init(640, 480, title="4.1 三角関数の基礎 (sin, cos)")

        self.angle_deg = 0  # 角度(度)
        self.center = (320, 240)
        self.radius = 180

        pyxel.run(self.update, self.draw)

    def update(self):
        # 角度を毎フレーム少しずつ増やす(0〜359度をループ)
        self.angle_deg = (self.angle_deg + 1) % 360

    def draw(self):
        pyxel.cls(0)
        cx, cy = self.center

        # 角度をラジアンに変換
        angle_rad = math.radians(self.angle_deg)

        # cos, sinの値を計算
        cos_val = math.cos(angle_rad)
        sin_val = math.sin(angle_rad)

        # 単位円上の点の座標を計算
        x = cx + self.radius * cos_val
        y = cy - self.radius * sin_val  # y軸は画面では下方向が正なので反転

        # 中心点を描画
        pyxel.circ(cx, cy, 3, 7)

        # 単位円の円周を描画
        pyxel.circb(cx, cy, self.radius, 8)

        # 現在の角度の点を描画
        pyxel.circ(int(x), int(y), 5, 11)

        # cosの値を横方向の長さとして表示(青の線)
        pyxel.line(cx, cy, int(cx + self.radius * cos_val), cy, 12)

        # sinの値を縦方向の長さとして表示(赤の線)
        pyxel.line(cx, cy, cx, int(cy - self.radius * sin_val), 8)

        # テキスト表示
        pyxel.text(5, 5, f"Angle: {self.angle_deg} deg", pyxel.COLOR_WHITE)
        pyxel.text(5, 15, f"cos: {cos_val:.3f}", 12)  # 青色
        pyxel.text(5, 25, f"sin: {sin_val:.3f}", 8)   # 赤色

App()

4.1.2 三角関数の基礎(sin, cosの意味と役割)

画面640x480の中央に単位円(半径200ピクセル)を描画。
角度を0〜359度で毎フレーム変化させ、対応するcos, sinの値を計算。
cosは横方向の成分、sinは縦方向の成分として青線・赤線で表示。
単位円上の点が角度に応じて動き、三角関数の意味を視覚的に理解できる。

import pyxel
import math

# 色定数の定義
COLOR_RED = 8
COLOR_BLUE = 5
COLOR_WHITE = 7

class UnitCircleVisualization:
    def __init__(self):
        # 画面サイズの設定
        self.width = 640
        self.height = 480
        pyxel.init(self.width, self.height, title="Unit Circle Visualization")
        
        # 円の半径と中心座標
        self.radius = 200
        self.center_x = self.width // 2
        self.center_y = self.height // 2
        
        # 回転角度(度数法)の初期値
        self.angle = 0
        
        # Pyxelメインループの開始
        pyxel.run(self.update, self.draw)
    
    def update(self):
        # 角度を1度ずつ増加させ、360度で0に戻る
        self.angle = (self.angle + 1) % 360
    
    def draw(self):
        # 背景をピーチ(15)でクリア
        pyxel.cls(15)
        
        # 単位円の描画(ライトブルー色6)
        pyxel.circ(self.center_x, self.center_y, self.radius, 6)
        
        # 角度をラジアンに変換
        rad = math.radians(self.angle)
        
        # cosとsinの値を計算
        cos_val = math.cos(rad)
        sin_val = math.sin(rad)
        
        # 円周上の点の座標を計算
        # px = 中心x + cos * 半径(横方向の位置)
        # py = 中心y - sin * 半径(縦方向の位置、画面座標系は上が小さいため減算)
        px = self.center_x + cos_val * self.radius
        py = self.center_y - sin_val * self.radius
        
        # cos成分を表す水平線(青)
        pyxel.line(self.center_x, self.center_y, int(px), self.center_y, COLOR_BLUE)
        
        # sin成分を表す垂直線(赤)
        pyxel.line(int(px), self.center_y, int(px), int(py), COLOR_RED)
        
        # 中心から円周上の点への半径線(黒)
        pyxel.line(self.center_x, self.center_y, int(px), int(py), 0)
        
        # 円周上の点にマーカーを描画(赤色8の円)
        pyxel.circ(int(px), int(py), 7, 8)
        
        # テキスト表示位置の設定
        text_x = 5
        text_y = 5
        
        # 現在の角度と三角関数の値を表示
        pyxel.text(text_x, text_y, f"Angle: {self.angle} deg", 0)
        pyxel.text(text_x, text_y+10, f"cos: {cos_val:.3f}", COLOR_BLUE)
        pyxel.text(text_x, text_y+20, f"sin: {sin_val:.3f}", COLOR_RED)

# プログラムの実行
UnitCircleVisualization()
  • 単位円の可視化: 数学の三角関数(cos, sin)を視覚的に理解するための教材
  • アニメーション: 角度が0度から359度まで自動的に回転

視覚要素:

  • グレーの円: 単位円
  • 青い横線: cos値(x軸方向の成分)
  • 赤い縦線: sin値(y軸方向の成分)
  • 黒い斜線: 半径(原点から円周上の点への線)
  • 赤い点: 現在の角度における円周上の位置
    pymunk未使用: このコードは物理シミュレーションではなく、純粋な数学的可視化のためpymunkは使用していません
    pyxel-20251129-114625.gif

4.2 ベクトルの回転:三角関数を使った角度変更

画面640x480
2Dベクトル (x, y) の定義
回転角度 θ(ラジアン)を決める
回転後のベクトルの計算式:
実際にベクトルを回転させる関数を作成
pyxelで元のベクトルと回転後のベクトルを視覚的に表示
左右矢印キー入力で回転角度を変更し動的に回転させる

import pyxel
import math

# 色定数の定義
COLOR_WHITE = 7
COLOR_RED = 8
COLOR_GREEN = 11

class VectorRotationApp:
   def __init__(self):
       # 画面サイズの設定
       self.width = 640
       self.height = 480
       pyxel.init(self.width, self.height, title="Vector Rotation")
       
       # 原点の座標(画面中央)
       self.origin = (self.width // 2, self.height // 2)
       
       # 元のベクトル(右向き、長さ240)
       self.vector = (240, 0)
       
       # 現在の回転角度(ラジアン)
       self.angle = 0.0
       
       # 1回の操作での回転角度(5度をラジアンに変換)
       self.angle_step = math.radians(5)
       
       # Pyxelメインループの開始
       pyxel.run(self.update, self.draw)
   
   def rotate_vector(self, v, theta):
       """
       2次元ベクトルを回転行列で回転させる関数
       
       回転行列:
       | cos(θ)  -sin(θ) |
       | sin(θ)   cos(θ) |
       
       新しいx = x*cos(θ) - y*sin(θ)
       新しいy = x*sin(θ) + y*cos(θ)
       """
       x, y = v
       cos_t = math.cos(theta)
       sin_t = math.sin(theta)
       return (x * cos_t - y * sin_t, x * sin_t + y * cos_t)
   
   def update(self):
       # 右矢印キー: 反時計回りに回転(角度を増加)
       if pyxel.btn(pyxel.KEY_RIGHT):
           self.angle += self.angle_step
       
       # 左矢印キー: 時計回りに回転(角度を減少)
       if pyxel.btn(pyxel.KEY_LEFT):
           self.angle -= self.angle_step
   
   def draw(self):
       # 画面を黒でクリア
       pyxel.cls(0)
       
       # 原点座標を取得
       ox, oy = self.origin
       
       # 元のベクトルの成分を取得
       vx, vy = self.vector
       
       # 回転後のベクトルを計算
       rx, ry = self.rotate_vector(self.vector, self.angle)
       
       # 元のベクトル(赤色)を描画
       # 画面座標系は上が小さいため、y成分は減算
       pyxel.line(ox, oy, ox + int(vx), oy - int(vy), COLOR_RED)
       
       # 回転後のベクトル(緑色)を描画
       pyxel.line(ox, oy, ox + int(rx), oy - int(ry), COLOR_GREEN)
       
       # 元のベクトルの終点にマーカー(赤)
       pyxel.circ(ox + int(vx), oy - int(vy), 6, COLOR_RED)
       
       # 回転後のベクトルの終点にマーカー(緑)
       pyxel.circ(ox + int(rx), oy - int(ry), 6, COLOR_GREEN)
       
       # 現在の回転角度を度数法で表示
       pyxel.text(5, 5, f"Angle: {math.degrees(self.angle):.1f} deg", COLOR_WHITE)
       
       # 操作説明の表示
       pyxel.text(5, 15, "Left/Right keys to rotate", COLOR_WHITE)

# プログラムの実行
if __name__ == "__main__":
   VectorRotationApp()

1.ベクトル回転の可視化: 2次元ベクトルの回転を視覚的に理解するための教材
2.インタラクティブ操作:

  • 右矢印キー: 反時計回りに5度ずつ回転
  • 左矢印キー: 時計回りに5度ずつ回転

3.視覚要素:

  • 赤いベクトル: 元のベクトル(常に右向き)
  • 緑のベクトル: 回転後のベクトル
  • 赤/緑の点: 各ベクトルの終点

4.数学的概念:

  • 回転行列: 2次元平面上でベクトルを回転させる標準的な方法
  • x成分 = x×cos(θ) - y×sin(θ)
  • y成分 = x×sin(θ) + y×cos(θ)

5.pymunk未使用: 物理シミュレーションではなく、数学的な幾何変換の可視化プログラム

4.3 2D回転行列の導入と実装

画面640x480
rotate_vector 関数で2D回転行列によるベクトル回転を実装。
update で角度を1度ずつ増やし、ベクトルを毎フレーム回転させる。
元のベクトル(緑)と回転後のベクトル(赤)を画面に表示。

import pyxel
import math

# 色定数の定義
COLOR_GREEN = 3
COLOR_RED = 8

class RotateVectorSimulation:
    def __init__(self):
        # 画面サイズの設定
        self.width = 640
        self.height = 480
        pyxel.init(self.width, self.height, title="2D Vector Rotation")
        
        # 回転角度の初期値(度数法)
        self.angle = 0
        
        # 基準ベクトル(右向き、長さ230)
        self.base_vector = (230, 0)
        
        # Pyxelメインループの開始
        pyxel.run(self.update, self.draw)
    
    def rotate_vector(self, vector, angle_deg):
        """
        2次元ベクトルを指定角度(度数法)で回転させる関数
        
        回転行列を使用:
        新しいx = x*cos(θ) - y*sin(θ)
        新しいy = x*sin(θ) + y*cos(θ)
        """
        # 度数法からラジアンに変換
        rad = math.radians(angle_deg)
        
        # 三角関数の値を事前計算
        cos_a = math.cos(rad)
        sin_a = math.sin(rad)
        
        # ベクトルの成分を取得
        x, y = vector
        
        # 回転行列を適用して新しいベクトルを返す
        return (x * cos_a - y * sin_a, x * sin_a + y * cos_a)
    
    def update(self):
        # 角度を1度ずつ増加させ、360度で0に戻る(自動アニメーション)
        self.angle = (self.angle + 1) % 360
    
    def draw(self):
        # 画面を黒でクリア
        pyxel.cls(0)
        
        # 原点(画面中央)の座標
        origin_x = self.width // 2
        origin_y = self.height // 2
        
        # 基準ベクトルの成分を取得
        vx, vy = self.base_vector
        
        # 回転後のベクトルを計算
        rx, ry = self.rotate_vector(self.base_vector, self.angle)
        
        # 基準ベクトル(緑色)を描画
        # 画面座標系は上が小さいため、y成分は減算
        pyxel.line(origin_x, origin_y, origin_x + int(vx), origin_y - int(vy), COLOR_GREEN)
        
        # 回転後のベクトル(赤色)を描画
        pyxel.line(origin_x, origin_y, origin_x + int(rx), origin_y - int(ry), COLOR_RED)
        
        # 軌跡を示す円(半径230、白の輪郭のみ)
        pyxel.circb(origin_x, origin_y, 230, 1)
        
        # 現在の回転角度を表示
        pyxel.text(5, 5, f"Angle: {self.angle} deg", 7)

# プログラムの実行
if __name__ == "__main__":
    RotateVectorSimulation()

1.自動回転アニメーション: 前のプログラムとの違いは、キー操作ではなく自動的に回転する点

2.視覚要素:

  • 緑のベクトル: 基準ベクトル(常に右向き、固定)
  • 赤のベクトル: 回転後のベクトル(1フレームごとに1度ずつ回転)
  • 白い円: ベクトルの終点が描く軌跡(半径230の円周)

3.アニメーションの仕組み:

  • update()で角度が毎フレーム1度ずつ増加
  • 360度に達すると0度に戻り、無限ループ

4.数学的概念:

  • ベクトルの回転は回転行列で実現
  • 回転角度が変化することで、ベクトルが円周上を移動

5.用途: ベクトル回転の基礎を視覚的に理解するための教材(三角関数と回転行列の関係を示す)

4.4 行列を使った座標変換(平行移動、回転、拡大縮小)

画面640x480
平行移動はベクトルの足し算で実装することを説明
拡大縮小行列の定義:
回転行列、拡大縮小行列を組み合わせる方法を説明
同次座標を使った3x3行列で平行移動も含めた変換を紹介(応用)
実装としては、複数の行列を掛け合わせて合成変換行列を作成
同次座標(x, y, 1)と3x3行列を使い、平行移動、回転、拡大縮小を一つの合成行列として扱う。
Mat3 クラスで行列の生成、掛け算を実装。
拡大縮小→回転→平行移動の順に変換を合成。
変換前の点を変換して画面に描画。
変換パラメータは時間経過で変化し、動的に変形がわかる。
pyxelで図形の座標に対して変換を適用し描画

import pyxel
import math

class Mat3:
    """3×3の行列クラス(2D変換用の同次座標系)"""
    
    def __init__(self, m=None):
        """
        行列の初期化
        m が None の場合は単位行列を生成
        """
        if m is None:
            # 単位行列(何も変換しない行列)
            self.m = [
                [1.0, 0.0, 0.0],
                [0.0, 1.0, 0.0],
                [0.0, 0.0, 1.0]
            ]
        else:
            # 指定された行列をコピー
            self.m = m
    
    def __mul__(self, other):
        """
        行列の乗算演算子のオーバーロード
        self * other を計算
        
        行列の乗算:
        C[i][j] = Σ(k=0 to 2) A[i][k] * B[k][j]
        """
        # 結果を格納する3×3行列を初期化
        result = [[0.0]*3 for _ in range(3)]
        
        # 行列の乗算を実行
        for i in range(3):
            for j in range(3):
                s = 0.0
                for k in range(3):
                    s += self.m[i][k] * other.m[k][j]
                result[i][j] = s
        
        return Mat3(result)
    
    def transform_point(self, x, y):
        """
        点(x, y)を行列で変換
        
        同次座標系での変換:
        [x']   [m00 m01 m02]   [x]
        [y'] = [m10 m11 m12] * [y]
        [1 ]   [m20 m21 m22]   [1]
        """
        tx = self.m[0][0]*x + self.m[0][1]*y + self.m[0][2]
        ty = self.m[1][0]*x + self.m[1][1]*y + self.m[1][2]
        return tx, ty

class TransformDemo:
    def __init__(self):
        # 画面サイズの設定
        self.width = 640
        self.height = 480
        pyxel.init(self.width, self.height, title="Matrix Transform Demo")
        
        # アニメーション用の時間変数
        self.t = 0.0
        
        # 変換対象の四角形の頂点(原点基準)
        # 100×100の正方形
        self.base_points = [(0, 0), (100, 0), (100, 100), (0, 100)]
        
        # Pyxelメインループの開始
        pyxel.run(self.update, self.draw)
    
    def update(self):
        # 時間を進める(アニメーション速度の調整)
        self.t += 0.02
        
        # Qキーで終了
        if pyxel.btnp(pyxel.KEY_Q):
            pyxel.quit()
    
    def draw(self):
        # 画面を黒でクリア
        pyxel.cls(0)
        
        # スケール値(1.0を中心に±0.5で振動)
        s = 1.0 + 0.5 * math.sin(self.t * 2.0)
        
        # 回転角度(時間に応じて一定速度で回転)
        angle = self.t * 2.0 * math.pi
        
        # 移動先の座標(円軌道上を移動)
        tx = 320 + 100 * math.cos(self.t)
        ty = 240 + 100 * math.sin(self.t)
        
        # スケール変換行列(拡大縮小)
        # [s  0  0]
        # [0  s  0]
        # [0  0  1]
        scale_mat = Mat3([
            [s, 0, 0],
            [0, s, 0],
            [0, 0, 1]
        ])
        
        # 三角関数の値を事前計算
        cos_a = math.cos(angle)
        sin_a = math.sin(angle)
        
        # 回転変換行列
        # [cos(θ)  -sin(θ)  0]
        # [sin(θ)   cos(θ)  0]
        # [0        0       1]
        rot_mat = Mat3([
            [cos_a, -sin_a, 0],
            [sin_a, cos_a, 0],
            [0, 0, 1]
        ])
        
        # 平行移動変換行列
        # [1  0  tx]
        # [0  1  ty]
        # [0  0  1 ]
        trans_mat = Mat3([
            [1, 0, tx],
            [0, 1, ty],
            [0, 0, 1]
        ])
        
        # 変換行列の合成(右から順に適用される)
        # 1. スケール変換
        # 2. 回転変換
        # 3. 平行移動変換
        transform = trans_mat * rot_mat * scale_mat
        
        # 各頂点を変換
        points = []
        for x, y in self.base_points:
            # 変換行列を適用
            px, py = transform.transform_point(x, y)
            
            # 整数座標に変換
            px_int = int(px)
            # 画面座標系に変換(上下反転)
            py_int = int(self.height - py)
            
            points.append((px_int, py_int))
        
        # 変換後の四角形を描画(各辺を線で結ぶ)
        for i in range(len(points)):
            x0, y0 = points[i]
            # 次の点(最後の点は最初の点に戻る)
            x1, y1 = points[(i+1) % len(points)]
            pyxel.line(x0, y0, x1, y1, 7)

# プログラムの実行
if __name__ == "__main__":
    TransformDemo()

1.3×3行列クラス (Mat3):

  • 2D変換を表現するための同次座標系の行列
  • 行列の乗算演算子をオーバーロード
  • 点の変換メソッドを提供

2.複合変換のアニメーション:

  • スケール: sin波で拡大縮小(0.5〜1.5倍)
  • 回転: 一定速度で回転
  • 平行移動: 円軌道上を移動

3.変換の順序:

  • transform = trans_mat * rot_mat * scale_mat
  • 右から順に適用: スケール → 回転 → 平行移動
  • この順序が重要(順序が変わると結果が変わる)

4.数学的概念:

  • 同次座標系: 2D変換を3×3行列で統一的に表現
  • 行列の合成: 複数の変換を1つの行列にまとめられる

5.用途: 線形代数と幾何変換の理解を深める教材

pyxel.tri()を使った四角形に適応してください。塗りは赤。

import pyxel
import math

class Mat3:
    def __init__(self, m=None):
        if m is None:
            self.m = [
                [1.0, 0.0, 0.0],
                [0.0, 1.0, 0.0],
                [0.0, 0.0, 1.0]
            ]
        else:
            self.m = m

    def __mul__(self, other):
        result = [[0.0]*3 for _ in range(3)]
        for i in range(3):
            for j in range(3):
                s = 0.0
                for k in range(3):
                    s += self.m[i][k] * other.m[k][j]
                result[i][j] = s
        return Mat3(result)

    def transform_point(self, x, y):
        tx = self.m[0][0]*x + self.m[0][1]*y + self.m[0][2]
        ty = self.m[1][0]*x + self.m[1][1]*y + self.m[1][2]
        return tx, ty

class TransformDemo:
    def __init__(self):
        self.width = 640
        self.height = 480
        pyxel.init(self.width, self.height, title="Matrix Transform Demo")
        self.t = 0.0
        self.base_points = [(0, 0), (100, 0), (100, 100), (0, 100)]
        self.COLOR_RED = 8
        pyxel.run(self.update, self.draw)

    def update(self):
        self.t += 0.02
        if pyxel.btnp(pyxel.KEY_Q):
            pyxel.quit()

    def draw(self):
        pyxel.cls(0)
        s = 1.0 + 0.5 * math.sin(self.t * 2.0)
        angle = self.t * 2.0 * math.pi
        tx = 320 + 100 * math.cos(self.t)
        ty = 240 + 100 * math.sin(self.t)
        scale_mat = Mat3([
            [s, 0, 0],
            [0, s, 0],
            [0, 0, 1]
        ])
        cos_a = math.cos(angle)
        sin_a = math.sin(angle)
        rot_mat = Mat3([
            [cos_a, -sin_a, 0],
            [sin_a, cos_a, 0],
            [0, 0, 1]
        ])
        trans_mat = Mat3([
            [1, 0, tx],
            [0, 1, ty],
            [0, 0, 1]
        ])
        transform = trans_mat * rot_mat * scale_mat
        verts = []
        for x, y in self.base_points:
            px, py = transform.transform_point(x, y)
            px_int = int(px)
            py_int = int(self.height - py)
            verts.append((px_int, py_int))
        v0, v1, v2, v3 = verts
        pyxel.tri(v0[0], v0[1], v1[0], v1[1], v2[0], v2[1], self.COLOR_RED)
        pyxel.tri(v2[0], v2[1], v3[0], v3[1], v0[0], v0[1], self.COLOR_RED)

if __name__ == "__main__":
    TransformDemo()

pyxel-20251129-132056.gif

4.5 行列とベクトルの掛け算で形を変える

画面640x480
複数の頂点を持つ図形(多角形)の定義
各頂点に行列変換を適用して座標を更新
拡大縮小や回転を組み合わせて形状の変形を実装
pyxelで変形前と変形後の図形を描画し比較
実時間で変換パラメータを変えてアニメーション化
正方形の4頂点を同次座標で定義。
拡大縮小、回転、平行移動を合成した変換行列を作成。
各頂点に変換行列を掛けて形状を変形。
pyxelで変形前の形ではなく変形後の多角形を描画。
時間経過で変換パラメータが変わり、形が動的に変化する。

import pyxel
import math

class Mat3:
    """3x3行列クラス(同次座標変換用)"""
    def __init__(self, m=None):
        if m:
            self.m = m
        else:
            self.m = [
                [1,0,0],
                [0,1,0],
                [0,0,1]
            ]

    def __mul__(self, other):
        """行列×行列 または 行列×ベクトル(同次座標)の掛け算"""
        if isinstance(other, Mat3):
            result = [[0]*3 for _ in range(3)]
            for i in range(3):
                for j in range(3):
                    for k in range(3):
                        result[i][j] += self.m[i][k] * other.m[k][j]
            return Mat3(result)
        elif isinstance(other, (list, tuple)) and len(other) == 3:
            res = [0,0,0]
            for i in range(3):
                for j in range(3):
                    res[i] += self.m[i][j] * other[j]
            return res
        else:
            raise TypeError("Unsupported multiplication")

    @staticmethod
    def translation(tx, ty):
        return Mat3([
            [1,0,tx],
            [0,1,ty],
            [0,0,1]
        ])

    @staticmethod
    def rotation(angle_rad):
        c = math.cos(angle_rad)
        s = math.sin(angle_rad)
        return Mat3([
            [c,-s,0],
            [s, c,0],
            [0, 0,1]
        ])

    @staticmethod
    def scaling(sx, sy):
        return Mat3([
            [sx,0, 0],
            [0, sy,0],
            [0, 0, 1]
        ])

class App:
    def __init__(self):
        pyxel.init(256, 256, title="4.5 行列×ベクトルで形を変える")

        self.center = (128, 128)

        # 正方形の4頂点(同次座標)
        self.shape = [
            [-30, -30, 1],
            [30, -30, 1],
            [30, 30, 1],
            [-30, 30, 1]
        ]

        self.angle_deg = 0
        self.scale_x = 1.0
        self.scale_y = 1.0
        self.tx = 0
        self.ty = 0

        pyxel.run(self.update, self.draw)

    def update(self):
        self.angle_deg = (self.angle_deg + 1) % 360
        self.scale_x = 1.0 + 0.5 * math.sin(math.radians(self.angle_deg))
        self.scale_y = 1.0 + 0.5 * math.cos(math.radians(self.angle_deg))
        self.tx = 30 * math.cos(math.radians(self.angle_deg))
        self.ty = 30 * math.sin(math.radians(self.angle_deg))

    def draw(self):
        pyxel.cls(0)
        cx, cy = self.center

        # 合成変換行列
        S = Mat3.scaling(self.scale_x, self.scale_y)
        R = Mat3.rotation(math.radians(self.angle_deg))
        T = Mat3.translation(self.tx, self.ty)
        M = T * R * S

        # 変換後の頂点を計算
        transformed = [M * v for v in self.shape]

        # 頂点を画面座標に変換(中心基準)
        points = [(int(cx + v[0]), int(cy - v[1])) for v in transformed]

        # 多角形を描画
        for i in range(len(points)):
            x1, y1 = points[i]
            x2, y2 = points[(i+1) % len(points)]
            pyxel.line(x1, y1, x2, y2, 11)

        # 中心点描画
        pyxel.circ(cx, cy, 3, 7)

        # テキスト表示
        pyxel.text(5, 5, f"Angle: {self.angle_deg} deg", pyxel.COLOR_WHITE)
        pyxel.text(5, 15, f"Scale X: {self.scale_x:.2f}", pyxel.COLOR_WHITE)
        pyxel.text(5, 25, f"Scale Y: {self.scale_y:.2f}", pyxel.COLOR_WHITE)
        pyxel.text(5, 35, f"Translate: ({self.tx:.1f}, {self.ty:.1f})", pyxel.COLOR_WHITE)

App()

pyxel-20251129-133241.gif

import pyxel
import math

class MatrixVectorTransform:
    def __init__(self):
        # 画面サイズの設定
        self.width = 640
        self.height = 480
        pyxel.init(self.width, self.height, title="Matrix Vector Transform")
        
        # 正方形のサイズ
        self.size = 100
        
        # 画面中央の座標
        self.center = (self.width // 2, self.height // 2)
        
        # 正方形の4つの頂点(同次座標系: [x, y, 1])
        # 原点を中心とした100×100の正方形
        self.vertices = [
            [ -self.size / 2,  self.size / 2, 1],  # 左上
            [  self.size / 2,  self.size / 2, 1],  # 右上
            [  self.size / 2, -self.size / 2, 1],  # 右下
            [ -self.size / 2, -self.size / 2, 1]   # 左下
        ]
        
        # アニメーション用の時間変数
        self.time = 0.0
        
        # Pyxelメインループの開始
        pyxel.run(self.update, self.draw)
    
    def mat_mult(self, m, v):
        """
        3×3行列と3次元ベクトル(同次座標)の乗算
        
        [x']   [m00 m01 m02]   [x]
        [y'] = [m10 m11 m12] * [y]
        [w']   [m20 m21 m22]   [w]
        
        結果を同次座標として正規化(w で除算)
        """
        # 行列とベクトルの積を計算
        x = m[0][0]*v[0] + m[0][1]*v[1] + m[0][2]*v[2]
        y = m[1][0]*v[0] + m[1][1]*v[1] + m[1][2]*v[2]
        w = m[2][0]*v[0] + m[2][1]*v[1] + m[2][2]*v[2]
        
        # w が 0 でない場合は同次座標を正規化
        if w != 0:
            return [x / w, y / w, 1]
        else:
            return [x, y, 1]
    
    def update(self):
        # 時間を進める(60FPS想定で1/60秒ずつ)
        self.time += 1 / 60.0
    
    def draw(self):
        # 画面を黒でクリア
        pyxel.cls(0)
        
        # 回転角度(時間に応じて増加)
        angle = self.time
        
        # X軸方向のスケール(1.0を中心に±0.5で振動)
        scale_x = 1 + 0.5 * math.sin(self.time * 2)
        
        # Y軸方向のスケール(X軸とは異なる周期で振動)
        scale_y = 1 + 0.5 * math.cos(self.time * 2)
        
        # X方向の平行移動量(円軌道のX成分)
        tx = 100 * math.cos(self.time)
        
        # Y方向の平行移動量(楕円軌道のY成分)
        ty = 50 * math.sin(self.time * 1.5)
        
        # 三角関数の値を事前計算
        cos_a = math.cos(angle)
        sin_a = math.sin(angle)
        
        # スケール変換行列(X軸とY軸で異なるスケール)
        # [sx  0   0]
        # [0   sy  0]
        # [0   0   1]
        scale_matrix = [
            [scale_x, 0, 0],
            [0, scale_y, 0],
            [0, 0, 1]
        ]
        
        # 回転変換行列
        # [cos(θ)  -sin(θ)  0]
        # [sin(θ)   cos(θ)  0]
        # [0        0       1]
        rotation_matrix = [
            [cos_a, -sin_a, 0],
            [sin_a, cos_a, 0],
            [0, 0, 1]
        ]
        
        # 平行移動変換行列
        # [1  0  tx]
        # [0  1  ty]
        # [0  0  1 ]
        translation_matrix = [
            [1, 0, tx],
            [0, 1, ty],
            [0, 0, 1]
        ]
        
        def mat_mult3(a, b):
            """
            3×3行列同士の乗算
            C[i][j] = Σ(k=0 to 2) A[i][k] * B[k][j]
            """
            # 結果を格納する3×3行列を初期化
            r = [[0,0,0],[0,0,0],[0,0,0]]
            
            # 行列の乗算を実行
            for i in range(3):
                for j in range(3):
                    s = 0
                    for k in range(3):
                        s += a[i][k]*b[k][j]
                    r[i][j] = s
            return r
        
        # 変換行列の合成(右から順に適用される)
        # 1. スケール変換
        # 2. 回転変換
        # 3. 平行移動変換
        transform = mat_mult3(translation_matrix, mat_mult3(rotation_matrix, scale_matrix))
        
        # 各頂点に変換行列を適用
        transformed = [self.mat_mult(transform, v) for v in self.vertices]
        
        # 変換後の座標を画面座標系に変換
        # - 画面中央を原点とする
        # - Y軸を反転(数学座標系→画面座標系)
        verts = [(int(self.center[0] + v[0]), int(self.center[1] - v[1])) for v in transformed]
        
        # 画面中央に原点のマーカーを描画(白い点)
        pyxel.circ(self.center[0], self.center[1], 3, 7)
        
        # 変換後の正方形を2つの三角形で描画(赤色)
        # 三角形1: 頂点0-1-2
        pyxel.tri(verts[0][0], verts[0][1], verts[1][0], verts[1][1], verts[2][0], verts[2][1], 8)
        # 三角形2: 頂点2-3-0
        pyxel.tri(verts[2][0], verts[2][1], verts[3][0], verts[3][1], verts[0][0], verts[0][1], 8)

# プログラムの実行
if __name__ == "__main__":
    MatrixVectorTransform()

1.同次座標系による変換:

  • 頂点を [x, y, 1] の3次元ベクトルとして表現
  • すべての変換(スケール、回転、平行移動)を3×3行列で統一的に扱う

2.複合アニメーション:

  • 非均等スケール: X軸とY軸で異なる速度で拡大縮小(正方形が長方形に変形)
  • 回転: 一定速度で回転
  • 楕円軌道移動: 円ではなく楕円軌道上を移動

3.行列演算の実装:

  • mat_mult: 行列×ベクトルの乗算(同次座標の正規化を含む)
  • mat_mult3: 行列×行列の乗算(ローカル関数として定義)

4.描画の工夫:

  • 正方形を2つの三角形に分割して描画
  • 画面中央に原点マーカーを表示

5.前のプログラムとの違い:

  • Mat3クラスを使わず、リストで行列を表現
  • 非均等スケールと楕円軌道により、より複雑な動きを実現
    pyxel-20251129-133927.gif

4.6 実例:図形の回転アニメーション

画面640x480
正六角形の頂点を同次座標で定義。
毎フレーム回転角度を増加させ、回転行列で頂点を回転変換。
回転後の頂点を画面座標に変換し、多角形を描画。
中心点と回転角度も表示し、回転の様子を視覚的に確認できる。

import pyxel
import math

# 色定数の定義
COLOR_WHITE = 7
COLOR_RED = 8

# 画面サイズの定数
WIDTH = 640
HEIGHT = 480

class RotateHexagon:
    def __init__(self):
        # Pyxelの初期化
        pyxel.init(WIDTH, HEIGHT, title="Rotate Hexagon")
        
        # 六角形の中心座標(画面中央)
        self.center_x = WIDTH // 2
        self.center_y = HEIGHT // 2
        
        # 回転角度の初期値(ラジアン)
        self.angle = 0.0
        
        # 六角形の半径(中心から頂点までの距離)
        self.radius = 100
        
        # 六角形の頂点リスト(同次座標系: [x, y, 1])
        self.vertices = []
        
        # 正六角形の6つの頂点を生成
        for i in range(6):
            # 各頂点の角度(60度 = π/3 ラジアンずつ)
            theta = math.pi / 3 * i
            
            # 極座標から直交座標への変換
            x = self.radius * math.cos(theta)
            y = self.radius * math.sin(theta)
            
            # 同次座標として頂点を追加
            self.vertices.append((x, y, 1))
        
        # Pyxelメインループの開始
        pyxel.run(self.update, self.draw)
    
    def update(self):
        # 回転角度を0.05ラジアンずつ増加
        self.angle += 0.05
        
        # 角度が2πを超えたら0に戻す(角度の正規化)
        if self.angle > 2 * math.pi:
            self.angle -= 2 * math.pi
    
    def draw(self):
        # 画面を黒でクリア
        pyxel.cls(0)
        
        # 現在の回転角度に対応する三角関数の値を計算
        cos_a = math.cos(self.angle)
        sin_a = math.sin(self.angle)
        
        # 回転後の頂点座標を格納するリスト
        rotated_points = []
        
        # 各頂点を回転変換
        for x, y, w in self.vertices:
            # 回転行列を適用
            # rx = cos(θ) * x - sin(θ) * y
            # ry = sin(θ) * x + cos(θ) * y
            rx = cos_a * x - sin_a * y
            ry = sin_a * x + cos_a * y
            
            # 回転後の座標を画面座標系に変換
            # 画面中央を原点とするため、center_x と center_y を加算
            screen_x = int(self.center_x + rx)
            screen_y = int(self.center_y + ry)
            
            # 画面座標を保存
            rotated_points.append((screen_x, screen_y))
        
        # 六角形の各辺を描画
        for i in range(6):
            # 現在の頂点
            x0, y0 = rotated_points[i]
            
            # 次の頂点(最後の頂点の次は最初の頂点)
            x1, y1 = rotated_points[(i + 1) % 6]
            
            # 頂点間を赤い線で結ぶ
            pyxel.line(x0, y0, x1, y1, COLOR_RED)
        
        # 六角形の中心座標を表示
        pyxel.text(10, 10, f"Center: ({self.center_x}, {self.center_y})", COLOR_WHITE)
        
        # 現在の回転角度を表示
        pyxel.text(10, 20, f"Angle: {self.angle:.2f} rad", COLOR_WHITE)

# プログラムの実行
RotateHexagon()

1.正六角形の生成:

  • 正六角形の頂点は、中心から等距離(半径100)に配置
  • 各頂点の角度は60度(π/3ラジアン)ずつ増加
  • 極座標 (r, θ) から直交座標 (x, y) への変換を使用

2.回転アニメーション:

  • 毎フレーム0.05ラジアンずつ回転
  • 2π(360度)を超えたら0に戻す(オーバーフロー防止)

3.回転変換の実装:

  • 回転行列を各頂点に適用:
  • rx = cos(θ) * x - sin(θ) * y
  • ry = sin(θ) * x + cos(θ) * y

4.回転後の座標を画面中央を基準に配置

  • 描画方法:
  • 6つの頂点を順番に線で結ぶことで六角形を描画
  • 最後の頂点と最初の頂点も結ぶ((i + 1) % 6で実現)

5.特徴:

  • 同次座標 (x, y, 1) を使用しているが、実際には2D回転のみ
  • Y軸の反転がないため、数学的な座標系と画面座標系が一致
  • 数学的背景: 正n角形の頂点は θ = 2π/n * i(i = 0, 1, ..., n-1)で配置される

pyxel-20251129-140254.gif

以下、pyxel.tri()を使って塗りを青で表示するコードです:

import pyxel
import math

# 色定数の定義
COLOR_WHITE = 7
COLOR_RED = 8
COLOR_BLUE = 5  # 青色を追加

# 画面サイズの定数
WIDTH = 640
HEIGHT = 480

class RotateHexagon:
    def __init__(self):
        # Pyxelの初期化
        pyxel.init(WIDTH, HEIGHT, title="Rotate Hexagon")
        
        # 六角形の中心座標(画面中央)
        self.center_x = WIDTH // 2
        self.center_y = HEIGHT // 2
        
        # 回転角度の初期値(ラジアン)
        self.angle = 0.0
        
        # 六角形の半径(中心から頂点までの距離)
        self.radius = 100
        
        # 六角形の頂点リスト(同次座標系: [x, y, 1])
        self.vertices = []
        
        # 正六角形の6つの頂点を生成
        for i in range(6):
            # 各頂点の角度(60度 = π/3 ラジアンずつ)
            theta = math.pi / 3 * i
            
            # 極座標から直交座標への変換
            x = self.radius * math.cos(theta)
            y = self.radius * math.sin(theta)
            
            # 同次座標として頂点を追加
            self.vertices.append((x, y, 1))
        
        # Pyxelメインループの開始
        pyxel.run(self.update, self.draw)
    
    def update(self):
        # 回転角度を0.05ラジアンずつ増加
        self.angle += 0.05
        
        # 角度が2πを超えたら0に戻す(角度の正規化)
        if self.angle > 2 * math.pi:
            self.angle -= 2 * math.pi
    
    def draw(self):
        # 画面を黒でクリア
        pyxel.cls(0)
        
        # 現在の回転角度に対応する三角関数の値を計算
        cos_a = math.cos(self.angle)
        sin_a = math.sin(self.angle)
        
        # 回転後の頂点座標を格納するリスト
        rotated_points = []
        
        # 各頂点を回転変換
        for x, y, w in self.vertices:
            # 回転行列を適用
            # rx = cos(θ) * x - sin(θ) * y
            # ry = sin(θ) * x + cos(θ) * y
            rx = cos_a * x - sin_a * y
            ry = sin_a * x + cos_a * y
            
            # 回転後の座標を画面座標系に変換
            # 画面中央を原点とするため、center_x と center_y を加算
            screen_x = int(self.center_x + rx)
            screen_y = int(self.center_y + ry)
            
            # 画面座標を保存
            rotated_points.append((screen_x, screen_y))
        
        # 六角形を中心点から各辺への三角形で塗りつぶす
        # 中心点の座標
        center = (self.center_x, self.center_y)
        
        # 6つの三角形を描画して六角形を塗りつぶす
        for i in range(6):
            # 現在の頂点
            x0, y0 = rotated_points[i]
            
            # 次の頂点(最後の頂点の次は最初の頂点)
            x1, y1 = rotated_points[(i + 1) % 6]
            
            # 中心点と2つの隣接する頂点で三角形を描画(青色で塗りつぶし)
            pyxel.tri(center[0], center[1], x0, y0, x1, y1, COLOR_BLUE)
        
        # 六角形の輪郭を赤い線で描画(塗りつぶしの上に重ねる)
        for i in range(6):
            # 現在の頂点
            x0, y0 = rotated_points[i]
            
            # 次の頂点(最後の頂点の次は最初の頂点)
            x1, y1 = rotated_points[(i + 1) % 6]
            
            # 頂点間を赤い線で結ぶ
            pyxel.line(x0, y0, x1, y1, COLOR_RED)
        
        # 六角形の中心座標を表示
        pyxel.text(10, 10, f"Center: ({self.center_x}, {self.center_y})", COLOR_WHITE)
        
        # 現在の回転角度を表示
        pyxel.text(10, 20, f"Angle: {self.angle:.2f} rad", COLOR_WHITE)

# プログラムの実行
RotateHexagon()

pyxel-20251129-135457.gif

1.色定数の追加: COLOR_BLUE = 5 を追加
2.塗りつぶしの実装:

  • 六角形を6つの三角形に分割
  • 各三角形は「中心点」と「隣接する2つの頂点」で構成
  • pyxel.tri()で青色(COLOR_BLUE)で塗りつぶし

3.描画順序:

  • 先に青色の塗りつぶし(三角形)を描画
  • その後、赤色の輪郭線を描画(見やすくするため)

4.六角形の三角形分割:

  • 三角形0: 中心 - 頂点0 - 頂点1
  • 三角形1: 中心 - 頂点1 - 頂点2
  • 三角形2: 中心 - 頂点2 - 頂点3
  • ...
  • 三角形5: 中心 - 頂点5 - 頂点0

以下の六角形は塗りに空きがあります。中心点を基準に、扇形の三角形6個で六角形を構成するように、青い塗りを入れてください。

import pyxel
import math

COLOR_ORANGE = 9
COLOR_WHITE = 7
COLOR_BLUE = 5
WIDTH = 640
HEIGHT = 480

class RotateHexagon:
    def __init__(self):
        pyxel.init(WIDTH, HEIGHT, title="Rotate Hexagon")
        self.center_x = WIDTH // 2
        self.center_y = HEIGHT // 2
        self.angle = 0.0
        self.radius = 100
        self.vertices = []
        for i in range(6):
            theta = math.pi / 3 * i
            x = self.radius * math.cos(theta)
            y = self.radius * math.sin(theta)
            self.vertices.append((x, y, 1))
        pyxel.run(self.update, self.draw)

    def update(self):
        self.angle += 0.05
        if self.angle > 2 * math.pi:
            self.angle -= 2 * math.pi

    def draw(self):
        pyxel.cls(0)
        cos_a = math.cos(self.angle)
        sin_a = math.sin(self.angle)
        rotated_points = []
        for x, y, w in self.vertices:
            rx = cos_a * x - sin_a * y
            ry = sin_a * x + cos_a * y
            screen_x = int(self.center_x + rx)
            screen_y = int(self.center_y + ry)
            rotated_points.append((screen_x, screen_y))
        cx = self.center_x
        cy = self.center_y
        # ステップ1: 青色の六角形を描画(中心から放射状の6つの三角形)
        for i in range(6):
            x0, y0 = rotated_points[i]
            x1, y1 = rotated_points[(i + 1) % 6]
            pyxel.tri(cx, cy, x0, y0, x1, y1, COLOR_BLUE)
        # ステップ2: オレンジ色の三角形を上に重ねて描画(連続3頂点)
        for i in range(4):
            x0, y0 = rotated_points[i]
            x1, y1 = rotated_points[i + 1]
            x2, y2 = rotated_points[i + 2]
            pyxel.tri(x0, y0, x1, y1, x2, y2, COLOR_ORANGE)
        # ステップ3: 最後のオレンジ三角形
        pyxel.tri(rotated_points[4][0], rotated_points[4][1], rotated_points[5][0], rotated_points[5][1], rotated_points[0][0], rotated_points[0][1], COLOR_ORANGE)
        pyxel.text(10, 10, f"Center: ({self.center_x}, {self.center_y})", COLOR_WHITE)
        pyxel.text(10, 20, f"Angle: {self.angle:.2f} rad", COLOR_WHITE)

RotateHexagon()

重ね描きをしてしまった。

描画される三角形の詳細:
青色(6個の三角形):
中心-頂点0-頂点1
中心-頂点1-頂点2
中心-頂点2-頂点3
中心-頂点3-頂点4
中心-頂点4-頂点5
中心-頂点5-頂点0

オレンジ色(5個の三角形、青の上に重ねる):
頂点0-頂点1-頂点2
頂点1-頂点2-頂点3
頂点2-頂点3-頂点4
頂点3-頂点4-頂点5
頂点4-頂点5-頂点0

結果
完全に重ね描きしています
先に青色で六角形全体を塗る
その後、オレンジ色の三角形が部分的に青を覆い隠す
最終的に見えるのは「青とオレンジの組み合わせ」
Pyxelは後から描いたものが上に表示される(ペインターアルゴリズム)
このコードは無駄な描画をしているため、青色のみまたはオレンジ色のみにするか、意図的に2色のパターンを作りたい場合以外は、どちらか一方を削除すべきです。

pyxel-20251129-145758.gif

「コードを書くという面倒な部分はAIに手伝ってもらうが、そのコードがなぜそう動くのかを理解するのは、人の役割である」
一歩一歩です。ありがとうございます。

7
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?