2026年8月31日月曜日

師匠の難題4

いよいよ地獄の窯が開きます。これまでの中間総集編となる内容です。

1.画像回転を処理するにあたり、まず「何処に」転送するかから考えていきます
前回の線の描画ルーチンを元に、縦方向の転送先座標を順に計算していきます。
次いで、90度(128度法で32度)角度を進めて横方向の座標を求める計算を入れ子して求めていきます。

2.次に「どこから」転送するかを考えていきます
転送先が斜めになった際、縦幅にしろ横幅にしろ、元の幅より狭くなります。
つまり、そのまま転送するのではなく数ドット間引きして縮めて送る必要が出てきます。
第一回で行ったブレゼンハムアルゴリズムは「長辺を元に短辺の位置」を算出していましたが、
今回は逆に短い幅を元に長い幅(元画像の幅)を求めなくてはなりません。
このため、第一回で作ったアルゴリズムを主体が短辺の時も対応できるように改造します。

ソースここから

import pyxel

my_table = (
※第三回参照

def my_sin(angle,r):
※第三回参照
    
def my_cos(angle,r):
※第三回参照

class VBresenham:
※第二回参照

class Bresenham:
    
    def __init__(self):
        self.l_side = 0
        self.s_side = 0
        self.counter = 0
        self.adder = 0
        
        # Src side is longer
        self.b_long_ss = True

    def setup(self,Dest_side,Src_side):
        self.counter = 0
        self.adder = 0
        
        if Dest_side > Src_side:
            self.b_long_ss = False
            self.l_side = Dest_side+1
            self.s_side = Src_side+1
            return self.l_side
            
        self.b_long_ss = True
        self.l_side = Src_side+1
        self.s_side = Dest_side+1
        return self.s_side
        
    def step(self):
        ret = self.adder
        
        if self.b_long_ss:
            while self.counter <= self.l_side:
                self.adder += 1
                self.counter += self.s_side
                
            self.counter -= self.l_side
                
        else:
            self.counter += self.s_side
            i = self.counter - self.l_side
            if i >= 0:
                self.counter = i
                self.adder += 1
        return ret

class App:

    def __init__(self):
        # 画面サイズ 160x120 で初期化
        pyxel.init(160, 120, title="RollBlt")
        self.add_x = 0
        pyxel.load("yajirushi.pyxres")
        
        self.x = 0
        self.y = 0
        self.angle = 0

        self.vbre_v = VBresenham()
        self.vbre_h = VBresenham()

        self.bre_v = Bresenham()
        self.bre_h = Bresenham()

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

    def update(self):
        self.angle += 1
        self.angle &= 127
        
        self.x = my_sin(self.angle,15)+80
        self.y = my_cos(self.angle,15)+60

    def draw(self):
        # 画面を黒(色番号0)でクリア
        pyxel.cls(0)

        # 画像縦方向ループ
        reg_v = self.vbre_v.setup(80,60,self.x,self.y)

        self.bre_v.setup(reg_v-1,15)

        for i in range (reg_v):
            x_v,y_v = self.vbre_v.step()

            src_y = self.bre_v.step()
            
            # 画像横方向ループ
            x_h = my_sin(self.angle+32,31)+x_v
            y_h = my_cos(self.angle+32,31)+y_v
            reg_h = self.vbre_h.setup(x_v,y_v,x_h,y_h)
            
            self.bre_h.setup(reg_h-1,31)
            
            for j in range (reg_h):
                x,y = self.vbre_h.step()
                
                src_x = self.bre_h.step()
            
                pyxel.blt(x, y, 0, src_x, src_y, 1, 1)

App()


ソースここまで

※紫色にしている部分が短辺主体に追加した部分

そして出力
…はい、理屈の上でのアルゴリズムは間違っていません。ただ、ブレゼンハムアルゴリズムは本来アナログな直線を整数に区切るアルゴリズムです。
当然、ある程度のズレが生じます。45度付近になると顕著にズレるため、このような表示になります。
いよいよ地獄が見えてきました。

正直に言えばこうなる事は最初から解っていましたが、段階を踏むとはこう言う事です。
次は更に踏み込んでいくのですが…今回のプログラムだけでも半日かかっていまして…次からはほぼ未知の領域なので予定がたたないのですよね…。

2026年8月30日日曜日

師匠の難題3

今回は計算済み三角関数テーブルによる回転計算(?)です。

ソースここから

import pyxel

my_table = (0,6,12,18,24,31,37,43,48,54,60,65,71,76,81,85,
90,94,98,102,106,109,112,115,118,120,122,124,125,126,127,127,128)

def my_sin(angle,r):
    angle &= 127
    work = angle & 63
    if work > 32:
        work = 64 - work
    ret = (r * my_table[work]>>7)
    if angle > 64:
        ret *= -1
    return ret

def my_cos(angle,r):
    return my_sin(angle+32,r)

class VBresenham:
※第2回参照

class App:

    def __init__(self):
        # 画面サイズ 160x120 で初期化
        pyxel.init(160, 120, title="Line")
        
        self.x = 0
        self.y = 0
        self.angle = 0

        self.vbre = VBresenham()

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

    def update(self):
        self.angle += 1
        self.angle &= 127
        
        self.x = my_sin(self.angle,40)+80
        self.y = my_cos(self.angle,40)+60

    def draw(self):
        # 画面を黒(色番号0)でクリア
        pyxel.cls(0)

        reg = self.vbre.setup(80,60,self.x,self.y)

        for i in range (reg):
            x,y = self.vbre.step()
            pyxel.pset(x,y,6)

App()

ソースここまで

はい、ラジアン角で言う所の0度~90度までを今回は128度法にするので0~32度までの33テーブルとし、単位長を128とするので128をかけた値をテーブル(今回はタプル)で用意(my_table)します。
尚、このテーブルを算出したプログラムは以下の通り。

ソースここから

import math

f : float = 0.0
for i in range (33):
    f = i/32.0
    print(str(int(128 * math.sin(f*math.pi/2.0)))+",",end="")

ソースここまで

サイン、コサインについてですが、ラジアン角で90度分のデータがあれば360度分をカバーできます。
※説明は全てラジアン角

当然、メモリが許すならば全て計算済みのテーブルを用意する方が処理が短くて済むので高速になります。

ソースここから

my_table = (
    0,   6,  12,  18,  24,  31,  37,  43,  48,  54,  60,  65,  71,  76,  81,  85,
   90,  94,  98, 102, 106, 109, 112, 115, 118, 120, 122, 124, 125, 126, 127, 127,
  128, 127, 127, 126, 125, 124, 122, 120, 118, 115, 112, 109, 106, 102,  98,  94,
   90,  85,  81,  76,  71,  65,  60,  54,  48,  43,  37,  31,  24,  18,  12,   6,
    0,  -6, -12, -18, -24, -31, -37, -43, -48, -54, -60, -65, -71, -76, -81, -85,
  -90, -94, -98,-102,-106,-109,-112,-115,-118,-120,-122,-124,-125,-126,-127,-127,
 -128,-127,-127,-126,-125,-124,-122,-120,-118,-115,-112,-109,-106,-102, -98, -94,
  -90, -85, -81, -76, -71, -65, -60, -54, -48, -43, -37, -31, -24, -18, -12,  -6,
    0,   6,  12,  18,  24,  31,  37,  43,  48,  54,  60,  65,  71,  76,  81,  85,
   90,  94,  98, 102, 106, 109, 112, 115, 118, 120, 122, 124, 125, 126, 127, 127
   )

def my_sin(angle,r):
    angle &= 127
    return r * my_table[angle]>>7
    
def my_cos(angle,r):
    angle &= 127
    angle += 32
    return r * my_table[angle]>>7

ソースここまで

※出力結果は当然同じになります

…これをやって初めて気づいたのですが、Python の右ビットシフト処理って符号ありビットシフトだったんですね…

ただし、最初の短いテーブル版は0~128の正数であるため1バイトとなり、これを33テーブル用意しているので33バイトで済むのに対して、長いテーブル版は-128~128の符号付2バイトの上で128+32テーブルの320バイトを使います。
…正直この程度のバイト数で悩むのは8ビット機くらいだと思いますが、300バイトくらい在ればまともな処理が組める事と、33バイト程度ならキャッシュに置いておくという手もあるので悩む人も居た…かも。

なぜ単位長が128なのか?
単位長が127なら符号付でも1バイトですがなぜ128なのか?解る方は多いと思いますが一応解説。たとえば、10進数でnを100で割って余りを除いた答え(商)を求めよと言われた場合、まともに計算せずに末端2桁を削除しますよね。

6789÷100=?

6789→67

同様に128は2進数では

10000000b

となるので、ビットシフト演算を使って末端7桁を削除すると割り算の代わりになる為です。
…まあ、端数切り捨てになるので本来は上記のテーブルもあらかじめ四捨五入済みのテーブルを作る等の工夫が必要なのですが、今回はザックリ作っています。


反時計回りに回転していますが、コレは単にY軸が下がプラスという座標系な為です。

…さて、ここまでで準備が整いました。
次回から、コレのタイトルが課題でも宿題でも無く難題としている理由が牙をむく…予定です。
というか、ここからは本当に難しいので終るか判らないのですよ、なんせ20年近くほったらかしていて、これ以上放置していると墓場まで持っていきそうなので始めたくらいなので。

2026年8月28日金曜日

師匠の難題2

今回はブレゼンハムアルゴリズムで線を引いていきます

ソースここから

import pyxel

class VBresenham:
    
    def __init__(self):
        self.l_side = 0
        self.s_side = 0
        
        # horizonatial side is longer
        self.b_long_hs = True

        # Direction (1 or -1)
        self.dct_x = 1
        self.dct_y = 1

        self.counter = 0
        self.adder_x = 0
        self.adder_y = 0
        
        self.start_x = 0
        self.start_y = 0

    # start x,y terminus x,y
    def setup(self,s_x,s_y,t_x,t_y):
        
        self.b_long_hs = True
        self.dct_x = 1
        self.dct_y = 1
        self.counter = 0
        self.adder_x = 0
        self.adder_y = 0

        self.start_x = s_x
        self.start_y = s_y
        
        if (s_x > t_x):
            self.l_side = s_x - t_x
            self.dct_x = -1
        else:
            self.l_side = t_x - s_x

        if (s_y > t_y):
            self.s_side = s_y - t_y
            self.dct_y = -1
        else:
            self.s_side = t_y - s_y
            
        if (self.s_side > self.l_side):
            # swap
            self.s_side,self.l_side = self.l_side,self.s_side
            self.b_long_hs = False

        self.l_side += 1
        self.s_side += 1
        return self.l_side

    def step(self):
        self.counter += self.s_side
        i = self.counter - self.l_side
        
        ret_x = self.start_x+self.adder_x
        ret_y = self.start_y+self.adder_y

        if self.b_long_hs:
            self.adder_x += self.dct_x
            if i >= 0:
                self.counter = i
                self.adder_y += self.dct_y
        else:
            self.adder_y += self.dct_y
            if i >= 0:
                self.counter = i
                self.adder_x += self.dct_x
                
        return ret_x,ret_y

class App:

    def __init__(self):
        # 画面サイズ 160x120 で初期化
        pyxel.init(160, 120, title="Line")

        self.vbre = VBresenham()

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

    def update(self):
        pass

    def draw(self):
        # 画面を黒(色番号0)でクリア
        pyxel.cls(0)

        reg = self.vbre.setup(80,60,pyxel.mouse_x,pyxel.mouse_y)

        for i in range (reg):
            x,y = self.vbre.step()
            pyxel.pset(x,y,6)

App()

ソースここまで

※8/29 ブレゼンハムのクラスを流用した際に暴走するので修正しました

マウスの動きに合わせて線を引くプログラムです。
…今回初めて複数の戻り値を持つ関数や、変数のスワップを使ったのですが、Python って便利な言語なんですね…言語の進化を感じます。

解説
  • setup の最後でなぜ長辺と短辺に1を加算しているのか?

self.l_side += 1
self.s_side += 1

の部分ですね。コレ、前回もやっていたのですが、解説してませんでした…

前回の図を使うとこんな感じ…
ざっくり言うと、計算は「格子」の座標系、描画は「格子の中」としているから。
実は加算しなくても動作するのですが、どうしてそんな事をしているのか?
例えば、始点と終点が同一点だったら?短辺の値が同一(=短辺が0だったら)?
これが3次元ポリゴンであったなら理屈上ポリゴンは厚みが無いので「描画しない」が正解になりますが、2次元の描画の場合は描画を行います。
つまり、最低限の描画を保障する為に加算しています。…あと、この方が描画が綺麗だったりするんですよね…。

  • アセンブラで書くなら
前回に引き続き、setup 関数でループ回数を取得し、 step 関数をループ回数分実行する処理になっています。

…と言う事は当然中に書かれている

if self.b_long_hs:

の分岐処理はループ回数実行されますが…毎回結果は同じです。
これを毎回分岐処理するといろいろ勿体ないので、アセンブラで書く場合はジャンプ先を上書きしたりします。

C言語で書くなら

goto vs_longer

// X軸の方が長かった場合の処理
goto end

// Y軸の方が長かった場合の処理
vs_longer:
// 以後最終処理
end:

のような記述をして、状況に応じて初期化時に goto vs_longer を書いたり消したりします。
アセンブラで変数として使用するレジスターは有限な上に数が少ないので使わないで済むならその方が良いですし、分岐処理が減ると数ステップ(クロック)処理が早くなります。線を引く際のドット数分全てにこの高速化がかかって来るのでバカにできません。
…まあ、現代のCPUパワーでこんな事を気にしないといけない状況は皆無だと思いますが。

同様に

self.dct_x
self.dct_y

この二つの変数、ベクトルのXとYがどちらに進むのかを1、もしくは-1で保存して、step 関数内で加算しているのですが…
ほぼ全てのCPUには1加算する Inc 命令と 1減算する Dec 命令が存在し、大抵高速に動作します。
なので

self.adder_y += self.dct_y


self.adder_x += self.dct_x

にあたる処理部分を初期化時に Inc や Dec に書き換えればレジスターも節約できるし、処理も高速化できます。


ただし…
これらのプログラム自身が自分のプログラムを上書きする…と言う動作は昔のプログラムでは有効でしたが、現代ではセキュリティの関係からメモリを書き換え禁止で保護して不可能になっているかもしれません。
そもそも、プログラム実行中にプログラムが書き換わるのでデバッグのやり辛さが尋常ではありません。
それでも、これらの処理で高速化を積み重ねる事が有効だった時代があった(今でも有効ですが)のですよ…遥か昔のお話なんですけどね…。


今回直線のプログラムを書きましたし、次回は回転を構成するもう一つのパーツ、三角関数テーブルを作って線の回転を作ってみます。

2026年8月24日月曜日

ひょっとしなくても

ニコ動でアニメを観てる身として、今期観ている「対あり」。
核となる格ゲーシーンが作画労力0という画期的なアニメ(と言っても、50年前の「まんがはじめて物語」等からある手法ですが)で、その分の労力をちゃんとアニメパートに回しているので作画クオリティも高い良作なんですが…

やたら丁寧なパロディネタ
ミリ残りから逆転できる白百合
カフェの使用キャラが春麗

…原作はミリも知らないのですが

レッツゴージャスティーン!

…この流れでやらない方がびっくりですよ。調べてみたら22年前のEVOでのウメハラの試合なんですが…
スト3とスト6の違いもありますが、撮影協力ウメハラが乗るんでしょうか?
実は対戦した二人が10年後の14年にエキシビジョンでこの時のネタを再現(しかも左右逆)したらしいのですが…今もビーストはこの操作をできるのか?
場外戦にも期待ですね。

師匠の難題1

 ではいきなりソースコード。

ソースここから

import pyxel

class Bresenham:
    
    def __init__(self):
        self.l_side = 0
        self.s_side = 0
        self.counter = 0
        self.adder = 0

    # 長辺、短辺を入力
    def setup(self,l_side,s_side):
        self.l_side = l_side+1
        self.s_side = s_side+1
        return self.l_side
        
    # 現在の短辺の位置を返し、次の短辺の位置を計算
    def step(self):
        ret = self.adder
        self.counter += self.s_side
        i = self.counter - self.l_side
        if i >= 0:
            self.counter = i
            self.adder += 1
        return ret

class App:

    def __init__(self):
        # 画面サイズ 160x120 で初期化
        pyxel.init(160, 120, title="RollBlt")
        self.add_x = 0
        pyxel.load("yajirushi.pyxres")
        
        pyxel.run(self.update, self.draw)

    def update(self):
        # 処理の都合上制限をかけている
        if pyxel.btn(pyxel.KEY_LEFT):
            self.add_x -= 1
        if pyxel.btn(pyxel.KEY_RIGHT):
            self.add_x += 1
        if self.add_x < 0:
            self.add_x = 0
        if self.add_x > 15:
            self.add_x = 15

    def draw(self):
        # 画面を黒(色番号0)でクリア
        pyxel.cls(0)

        bre = Bresenham()
        rag = bre.setup(l_side=15,s_side=self.add_x)

        for y in range (rag):
            add_x = bre.step()
            for x in range (32):
                pyxel.blt(80+add_x + x, 60 + y, 0, x, y, 1, 1)

App()

ソースここまで

そして実行結果
なんだか、斜めにずれていきます。
今回の主役はこの斜めを作るアルゴリズム…ブレゼンハムアルゴリズムです。
まあ、検索すればいくらでも解説が出てくる古典の描画アルゴリズムですが…
基本、こんな感じ。
カウンター0から、長辺方向(この場合は横)に向けて短辺の数(この場合は3)を足していく。で、カウンターが長辺より大きくなったら、カウンターの値を長辺の数(この場合は5)引いて次の描画の際に1段ずらす…以後繰り返し。

はえー、昔の人は賢いな…と、思うのですが原理は簡単です。
小学生が掛け算を習う際に、掛け算とは一方の数をもう一方の数の分だけ足したものと習います。
短編の数を長辺の数分足しているのですから、長辺で引く部分を除いたカウンターの総和は短辺と長辺をかけたもの(この場合は15)となり、当然、その数は長辺で割り切れて、割った答えは短辺。
カウンターの最終値になるまでに長辺で割り切れる回数は当然短辺と同じ(この場合3回)。
では、その割り切れるタイミングは?
…と考えれば理解が早いと思います。

ただし、このプログラムは穴だらけ
Pyxel 座標系はこんな感じ(右方向がXの正の方向、下方向がYの正の方向)ですが。今回のアルゴリズムは長辺>短辺かつ、どちらもプラス方向に値が加算される前提で組まれています。

※中心から各方向へ延ばしたベクトルのX、Y要素の関係

具体的には45度しかカバーできていません。
次回は、ちゃんとした2次元ベクトル上のブレゼンハムクラスを作って、直線を引くプログラムを書いていきます。

2026年8月21日金曜日

掘り、終了

E2-3乙1周目(A勝利)

E2-3乙7周目(初のS勝利)

油と弾が26日の友軍まで待つと自然回復量を回復しきってしまう為、周回をしてみました…案の定A勝利しかできず…(というか、途中撤退1C敗北1だったりします)。
初めてのS勝利で出てくれました、流石に大和、アイオワ連撃をセットしているのでこれ以上掘りたくは無かったですね。

これにて我が艦隊に不在の艦はアイツボルとデイスのみになりました
今後掘る物があるとすれば…

E5
ジャンバール(2隻目)
グローリアス(2隻目)
プリンツオイゲン(3隻目)
アークロイヤル(5隻目)
まるゆ

E4
モガドール(2隻目)

E3
百一号(3隻目)

E1
海防艦

…まあ、正直強く欲しいのはジャンバールとモガドールくらいですかね。
空母のレベリングがあるのでE3とE4は回りますが…それほど本気で回る事は無くなる予定です。とりあえず、全部終わってほっとしてます。

※8/24追記
E3を回っていて、2隻目のラングレーが出て、まあ御駄賃的によかったね…とか思っていたのですが…
2隻目。
君、出るんかい。しかも丁で。なんか申し訳ない感じですわ。レキシントンとサラトガの育成は終わりましたが、しばらくメリーランド、ワスプ、ラングレーの育成でも良いのかも。
ただ、流石にベアルンが75まで育ったら欧州の周回をしようかと思うのですが…
正直、ハロウィン用の天霧及び三隈が溢れていて新艦を拾う枠も無いのですよね。仕方なく三隈改を合成に回していますが、若干勿体ない気もしています。

2026年8月18日火曜日

師匠の難題0

ゲーム業界を引退し、プログラマーを辞めて20年近くが経ちます。
技術不足ゆえ早期退職を促され、受理して辞めた訳で、未練は無いのですですが、一つ心残りは有りました。

プログラマーになって最初の年に師匠(先輩プログラマ)から出された課題を解いておらず、そのまま有耶無耶になり、引退してしまったのです。

で、その課題が画像の回転コピーを作ってみよでした。
私がプログラマーになった年は、ゲーム業界に入って2年目の97年です。
当時のVC++でも BitBlt 関数で回転コピーはできたと思いますが、当然ライブラリや出来合いの関数を使えと言っているわけでは無いことは明白です。ちなみに師匠はアセンブラの名手で、自作のライブラリを作り、ゲーム機のCPUに合わせてカスタマイズし続けていました。

97年はペンティアム2が出たてくらいで、インテルの486や下手すると386すら現役の時代でした。386は後に386SXと改名され、コプロセッサが搭載された拡張版が386DXとなります。で、このコプロセッサが無いとどうなるか?私もはっきり覚えていないのですが、実数の計算ができませんでした。

つまり…アセンブラ化を前提とした実数を使用しない回転コピーを作ってみよ、という意図だったのではないかと推測されます。

なのでひとまず、縛りと仕様を決めます

仕様
転送元画像の一辺の長さは最大128ドット
転送先の画面の一辺の長さは最大512ドット

縛り
実数は使わない
割り算は使わない
三角関数は計算済みテーブルのみを使用(単位長128で、128度法)

まあ、日和って掛け算は使うんですけどね。
以前セットアップした Pyxel ライブラリを使います。…というのも、以前 Kivy の描画関数で描画プログラムを書いた際は遅すぎてとても実用に耐えなかったからです。
ちなみに Pyxel はパレットカラーマッピングですが、本質的に画像の転送方法が課題で色はどうでも良いので気にしません。


で、ひとまず作ってみたのはこんなプログラム。

ソースここから

import pyxel


class App:

    def __init__(self):
        # 画面サイズ 160x120 で初期化
        pyxel.init(160, 120, title="RollBlt")
        
        # 矢印画像をバンクへ読み込み
        pyxel.load("yajirushi.pyxres")
        
        pyxel.run(self.update, self.draw)

    def update(self):
        pass

    def draw(self):
        # 画面を黒(色番号0)でクリア
        pyxel.cls(0)
        
        # 1ドットずつ転送
        for y in range (16):
            for x in range (32):
                pyxel.blt(80 + x, 60 + y, 0, x, y, 1, 1)

App()

ソースここまで

出力結果

バンクからわざわざ1ドット分の画像を縦横の長さ分転送を繰り返して送っています。
普通こんな無駄な転送を行いませんが今後必要になるコードです。次回からはこれを改造していきます。