2026年9月8日火曜日

師匠の難題5

前回の回転でモワレが出てしまう原因は何か?それは転送の方法です。

前回はこのように「横に読み込んで斜めに」転送したのですが…

正しくはこのように「斜めに読み込んで横に」転送する方法ならば隙間ができません。
以前「線」を描画するプログラムは書きましたので、画面の縦幅分のテーブル(広義のラインバッファ)を設けて、「転送先:左端、右端 転送元:どこから、どこ」のデータをため込んで、出力すれば可能なハズです。
ただ、この方法をいっきに作るのは大変なので、処理を分けて今回は「転送先:左端、右端」のバッファのみを作って「回転する長方形」を描画する処理を書いていきます。

さて、既に線描のプログラムは作っているので線描で回転する長方形を作るのは簡単に作れました。この点描した座標をテーブルに都度書き込んで、バッファの情報を元に左端から右端までの間を埋めていけば良い…

アレ?
これは先ほどの水色の線描処理の上からバッファに入った「左端/右端」を元に赤紫で塗りつぶしたのですが…なんかちらちら水色が見えてますね。
線描を拡大すると判るのですが、同一Y座標上でも複数のドットがあり、どちらの端のドットをバッファに積み込むか?を正しく選択しないとこうなります。
正しく積み込めばこの通りなのですが…

この正しい積み込み方法は360度法で90度おきに変わります、しかも縦軸と横軸で処理が違います…
と言う訳でソースコードを今回は全文載せてしまうとこんな感じ…

ソースここから

import pyxel

SCREEN_SIZE_W = 160
SCREEN_SIZE_H = 120
CENTER_X = 80
CENTER_Y = 60
EMPTY = -32767

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

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 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:
            self.counter += self.s_side
            i = self.counter - self.l_side
            if i >= 0:
                self.counter = i
                self.adder += 1
        else:
            while self.counter <= self.l_side:
                self.adder += 1
                self.counter += self.s_side
                
            self.counter -= self.l_side

        return ret

class LineBuffer:
    def __init__(self):
        self.l = 0 # left
        self.r = 0 # right

class RollBlt:
    
    def __init__(self):
        self.buff = []
        for i in range(SCREEN_SIZE_H):
            self.buff.append(LineBuffer())

        self.top = 0
        self.bottom =0

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

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

    def setup(self):
        pass
        
    def blt(self,dest_x, dest_y, bank, src_x, src_y, src_w, src_h, angle):

        # 角度のまるめ
        angle &= 127

        # 原点から見た縦方向の端の座標
        tv_x = my_sin(angle,src_h-1)+dest_x
        tv_y = my_cos(angle,src_h-1)+dest_y

        # 原点から見た横方向の端の座標
        th_x = my_sin(angle+32,src_w-1)+dest_x
        th_y = my_cos(angle+32,src_w-1)+dest_y
        
        # 4頂点と比較し上下の座標を求める
        self.top = dest_y
        if tv_y < self.top:
            self.top = tv_y
        if th_y < self.top:
            self.top = th_y
        if (tv_y + th_y - dest_y) < self.top:
            self.top = tv_y + th_y - dest_y
        if self.top < 0:
            self.top = 0

        self.bottom = dest_y
        if tv_y > self.bottom:
            self.bottom = tv_y
        if th_y > self.bottom:
            self.bottom = th_y
        if (tv_y + th_y - dest_y) > self.bottom:
            self.bottom = tv_y + th_y - dest_y
        if self.bottom > (SCREEN_SIZE_H-1):
            self.bottom = SCREEN_SIZE_H-1

        # テーブル初期化
        for y in range (self.top, self.bottom+1):
            self.buff[y].l = EMPTY
            self.buff[y].r = EMPTY

        # 画像縦方向処理
        reg = self.vbre.setup(dest_x,dest_y,tv_x,tv_y)
        
        match angle:
            # 垂直にコピーする時は縦方向の処理を行わない
            case a if a == 32 or a == 96:
                pass
                
            case a if a < 32:
                
                for i in range (reg):
                    x,y = self.vbre.step()

                    if self.buff[y].l == EMPTY:
                        self.buff[y].l = x
                    pyxel.pset(x,y,6)
                    
                    x = x - dest_x + th_x
                    y = y - dest_y + th_y
                    
                    self.buff[y].r = x
                    pyxel.pset(x,y,6)

            case a if a < 64:

                for i in range (reg):
                    x,y = self.vbre.step()
                    
                    self.buff[y].r = x
                    pyxel.pset(x,y,6)
                    
                    x = x - dest_x + th_x
                    y = y - dest_y + th_y
                    
                    if self.buff[y].l == EMPTY:
                        self.buff[y].l = x
                    pyxel.pset(x,y,6)
            
            case a if a < 96:

                for i in range (reg):
                    x,y = self.vbre.step()
                    
                    if self.buff[y].r == EMPTY:
                        self.buff[y].r = x
                    pyxel.pset(x,y,6)
                    
                    x = x - dest_x + th_x
                    y = y - dest_y + th_y
                    
                    self.buff[y].l = x
                    pyxel.pset(x,y,6)

            case _:

                for i in range (reg):
                    x,y = self.vbre.step()
                
                    self.buff[y].l = x
                    pyxel.pset(x,y,6)
                    
                    x = x - dest_x + th_x
                    y = y - dest_y + th_y
                    
                    if self.buff[y].r == EMPTY:
                        self.buff[y].r = x
                    pyxel.pset(x,y,6)

        # 画像横方向処理
        reg = self.vbre.setup(dest_x,dest_y,th_x,th_y)

        match angle:
            # 水平にコピーする時は横方向の処理を行わない
            case a if angle == 0 or angle == 64:
                pass
                
            case a if a < 32:
                
                for i in range (reg):
                    x,y = self.vbre.step()
                    
                    if self.buff[y].l == EMPTY:
                        self.buff[y].l = x
                    pyxel.pset(x,y,6)
                    
                    x = x - dest_x + tv_x
                    y = y - dest_y + tv_y

                    self.buff[y].r = x
                    pyxel.pset(x,y,6)
                
            case a if a < 64:

                for i in range (reg):
                    x,y = self.vbre.step()
                
                    self.buff[y].l = x
                    pyxel.pset(x,y,6)
                    
                    x = x - dest_x + tv_x
                    y = y - dest_y + tv_y

                    if self.buff[y].r == EMPTY:
                        self.buff[y].r = x
                    pyxel.pset(x,y,6)

            case a if a < 96:

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

                    if self.buff[y].r == EMPTY:
                        self.buff[y].r = x
                    pyxel.pset(x,y,6)
                    
                    x = x - dest_x + tv_x
                    y = y - dest_y + tv_y

                    self.buff[y].l = x
                    pyxel.pset(x,y,6)

            case _:
                
                for i in range (reg):
                    x,y = self.vbre.step()
                    
                    self.buff[y].r = x
                    pyxel.pset(x,y,6)
                    
                    x = x - dest_x + tv_x
                    y = y - dest_y + tv_y

                    if self.buff[y].l == EMPTY:
                        self.buff[y].l = x
                    pyxel.pset(x,y,6)

        # テーブルを基に線描で塗りつぶし
        for y in range (self.top, self.bottom+1):
            pyxel.line(self.buff[y].l,y,self.buff[y].r,y,2)


class App:

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

        self.rb = RollBlt()
        
        self.angle = 0

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

    def update(self):
        self.angle += 1
        self.angle &= 127

    def draw(self):
        # 画面を黒(色番号0)でクリア
        pyxel.cls(0)
        
        self.rb.blt(CENTER_X, CENTER_Y, 0, 0, 0, 32, 16, self.angle)

App()

ソースここまで

冗長。
解説すると、ラインバッファテーブルを作りたかったのですが、C言語のような構造体テーブルは作れない(NumPyライブラリを使うのは主題とずれるので…)ため、LineBuffer クラスを作り、これをノードとして使用します。
そして、今回から回転転送を扱う RollBlt クラスを新設してこちらで一括して処理する事になりました。
この為、メインの App クラスは角度の加算と RollBlt の blt 関数呼び出しのみを行う事になり、シンプルになったのですが…

全体の解説を言語化すると頭がパンクしそうなので端折りますが
この blt 関数の中に2か所ある match angle: ~ からが処理のメインとなります。
縦軸にしろ横軸にしろ、中心点から放射状に座標を求めています。この為、バッファに積み込むのは最初に求めたX座標か最後に求めたX座標となります。
最後に求めたX座標に関しては上書きを繰り返せば良いだけですが、最初に求めたX座標のみを積み込むには仕掛けが要ります。
これが

self.buff[y].l = EMPTY
self.buff[y].r = EMPTY

で行っている初期化で、バッファに EMPTY が入っていれば未入力と言う事で入力。以後入力しない…という処理を組んで最初に求めたX座標のみを積み込んでいます。
また、各軸で90度おき(128度法で32度おき)に処理を変えているのですが…図示しながら解説すると、長くなるので解説は省略します。

各 case の中に同じ for i in range (reg): ループが書かれていますが…
確かに match angle: 分岐の外に for 文を書けばソースコードは短くなります…が、実測はしていませんが match case 文が重い分岐処理である事は想像に難くないです。
その上で、毎回分岐結果が同じと判っている物をループ内で処理すると遅くなるので分岐の中にループを書いています。

毎度書かれている pyxel.pset(x,y,6)
これが水色の点を描画する処理で、 blt 関数末尾の塗りつぶし処理をコメントアウトすると水色の線描の長方形になります。
これはチェック用に書いた物であり、最初に示した通り処理が失敗している場合は端に水色が表示されるようになります。
最終的には全て削除する予定です。


…次で一応回転コピーが見た目上できるようになる…予定です
あくまで見た目上。このままでは実用に耐えませんし。そして、恐らく師匠が用意したであろう真の課題…も想像がつくのですが、そこまではやらないかもしれません。
正直、ここまででも相当ドはまりしてますので…ホントに大変な課題を出されたんだなあと実感します。

0 件のコメント:

コメントを投稿