SVX日記

2004|04|05|06|07|08|09|10|11|12|
2005|01|02|03|04|05|06|07|08|09|10|11|12|
2006|01|02|03|04|05|06|07|08|09|10|11|12|
2007|01|02|03|04|05|06|07|08|09|10|11|12|
2008|01|02|03|04|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|
2021|01|02|03|04|05|06|07|08|09|10|11|12|
2022|01|02|03|04|05|06|07|08|09|10|11|12|
2023|01|02|03|04|05|06|07|08|09|10|11|12|
2024|01|02|03|

2024-03-11(Mon) そんなメルカトル、補正してやるッ!!

  ついに走り出したところで、次はなにを実装すべぇかなぁ、と考えなしに考えていたら、我ながら意外なところに向かってしまった。緯度補正だ。Google Map(ほかオンライン)の地図データはメルカトル図法なので、北に行くほど「より大きく」表示されるのだ。

  当初、そんなもん大した違いではないから無視しよう、と思っていたのだが、実は無視していいレベルの違いではなかった。だいぶ違う。一応は、実際に遊んだときに実際のF1のラップに近いタイムが出るようにしたいと思っているのだが、そうしたければとても無視できないレベルだ。

  とりあえず「鈴鹿」「シルバーストン」をテストの対象に開発を進めていたのだが、こうなれば赤道に至近なサーキットもテストの対象に加えるべきだ。最近はF1観てないんだよなぁ……なので、ググる。すると「マリーナベイ・ストリート・サーキット」がそれらしい。シンガポールGPが開催されている市街地サーキットで、北緯1度17分にある。ほぼ、赤道直下といっていい。

  画像の説明 画像の説明 画像の説明

  で、なにげなく座標データだけ入力してやると……「ちっさ」。左から「シルバーストン」「鈴鹿」「マリーナベイ」である。「シルバーストン」では気づかなかったが、こりゃ補正不可避である。幸い、開発したばかりの「BG版の回転拡大縮小機能」は、特段の負荷なしに自在に拡大縮小が可能だ。で、ここからは数学の時間である。まずは、一番直感的に書けるRubyで補正値を求めるプログラムを書いてみた。

include Math
 
# equ_px = (2 **  0) * 256                      # 赤道のピクセル数(ズームレベル0)
equ_px = (2 ** 20) * 256                        # 赤道のピクセル数(ズームレベル20)
equ_m = 40075 * 1000.0                          # 赤道の周長(m)
 
p equ_1px = equ_m / equ_px                      # 赤道下の1ピクセルの長さ(m)
#=> 0.1492910087108612
 
car_px = 24                                     # 車幅のピクセル数
car_m = 2.0                                     # 車幅(m)
 
p car_1px = car_m / car_px                      # 車の1ピクセルの長さ(m)
#=> 0.08333333333333333
 
p equ_times = car_1px / equ_1px                 # 赤道下の補正値
 
p 256 * equ_times                               # 回転拡大縮小機能への補正値
#=> 142.8976434518611

  まずは、赤道直下を対象にした補正値だ。使用する定数は「赤道の周長(40075km)」と「F1の車幅(2.0m)」だ。どちらもWikipediaで調べた。ゲームとしての基本的な仕様はスーパーフォーミュラをパク……オマージュるつもりなので、車のサイズは24x43ピクセルだ。2.0mを24ピクセルで表現したい、ということになる。回転拡大縮小機能に与える補正値は、サンプリングベクトルで256が標準値であり、それより小さい値を与えると拡大される。計算の結果、赤道直下の場合は143を与えればいいと出た。

  次は、緯度補正だ。緯度が高くなるにつれ地球の周長は減少していくが、それはコサイン一発で求められる。

p   [lat = 50, 'イギリス' ]
# p [lat = 35, '日本' ]
# p [lat =  0, '赤道' ]
 
p cos = Math.cos(lat * PI / 180)
p lat_times = car_1px / equ_1px / cos           # 任意の緯度下の補正値
 
p 256 * lat_times                               # 回転拡大縮小機能への補正値
#=> 222.30926872026404                          # [lat = 50, 'イギリス' ]
#=> 174.44581191992688                          # [lat = 35, '日本' ]
#=> 142.8976434518611                           # [lat =  0, '赤道' ]

  日本の場合は174、イギリスの場合は222を与えればいいと出た。概ね45度が7割なんだから合っていそうだ。

  次は、CoffeeScriptへの組み込み。ほぼ、上記のRubyのコードがそのまま動いたが、ひとつ考慮することがある。車を南北に移動した場合、補正値も変化させるべきか? ということだ。さすがに1フレームの移動毎に計算するのは過剰で、タイルをまたがったタイミング毎で十分だろうから処理は軽い。ゲーム内で使用する座標情報はWposというクラスで管理しているから、それに関数を追加するか……と実装しかけて気づいた。Wposクラスは経緯度で座標を与えられる仕様ではあるが、直後にメルカトル座標に変換して保持し、緯度情報は破棄してしまうのであった。ゲーム中の車の座標管理ならメルカトル座標のが扱いやすく、緯度を継続的に保持する必要性はないからだ。

  つうわけで、今回の目的は主にサーキットを走るのがメインであって、大陸縦断をするわけではないので、サーキットを選択した時点で補正値を計算し、それ以後の補正値の更新はなしとした。結果、コードは以下のようになった。

equ_times = car_1px / equ_1px                   # 赤道下の補正値
@lat_index = equ_times * 65536                  # 緯度の補正指数
@t = Math.round(@lat_index / Vec.v2vxy(128 - Math.round((@car.lat0 * 64) / 90.0), 1)[0])

  コサイン値は既存のVecクラスのテーブルの参照で済ませる。テーブルは90度を64分割、1.0を256として保持しているので上記のようになる。そして、表示させたスターティンググリッドが以下だ。

  画像の説明 画像の説明 画像の説明 画像の説明

  うぉーッ!! スターティンググリッドの位置が見事に揃いましたゼ、ダンナッ!! プログラミングって、こういう感じにわかりやすく美しい結果を得られた時が、たまらなく楽しい瞬間なんだよなぁ。これも今回、調べて初めて知ったことなのだが、スターティンググリッドの間隔は8mと決められているらしい。

  それではと、画面上のスターティンググリッドの間隔を測ってみると、ポールポジションと3番手のグリッドの間隔は192ピクセル。今回は2.0mを24ピクセルで表現しているのだから……16mッ!! パーフェクトだウォルター。

  ……と、ふと気になって、パク……オマージュり元のスーパーフォーミュラはどうなのかとおもったら……あれ? 狭い……つうか、ちょうど半分の間隔になってる!? なんだこれッ!? 偶然ッ!?


2024-03-10(Sun) 気づいたら走り出してたのさ

  というほどに簡単ではなかったが、まずは任意の場所(シルバーストン!)から走り出せるようになった。

 

  現状、ほぼ60FPSを確保できているが、もう少し高速化(というよりは負荷分散)する予定(動きがぎこちないのは録画した都合)。また、地図の切り替わりが遅いのはややワザとではあるが、さすがにレーシングスピードだとキャッシュは不可避かなぁ。


2024-03-06(Wed) SinatraのPOSTでrequest.body.rewindが動かない

  先日、Sinatraに認証機能を付けたのだが、その際にbundle addをやり直してGem関係を最新にしたらPOSTの内容がreadできなくなってしまった。

NoMethodError at / undefined method `rewind' for #<Rack::Lint::Wrapper::InputWrapper:0xXXXX @input=#<StringIO:0xXXXX>>

  なんて出る。何かやらかしたかと思ったが、特段イジっていない。該当のコードは「request.body.rewind」らしいのだが、だいぶ前なので、なんでそんなコードを書いたのか覚えていない。が、どうもSinatraのオフィシャルサイトのサンプルコードから持ってきていたようだ。

post "/api" do
  request.body.rewind  # in case someone already read it
  data = JSON.parse request.body.read
  "Hello #{data['name']}!"
end

  それなのに動かないってどういうことよ。と思って追跡していくと、どうもRackのバージョンアップの影響らしい。Gemfileの記述を戻すと動く。「request.body」のメソッドを表示させてみると、実際に変わってしまっていて「rewind」メソッドがなくなってる。そら動かん。bundleの仕組みは、今回のようにバージョンアップの影響で動かなくなることを防ぐ意図もあるのだが、実際にこんなことあるんやな。

# 動くバージョン
request.body: #<Rack::Lint::InputWrapper:0xXXXX @input=#<StringIO:0xXXXX>>
request.body.methods: [:!, :!=, :!~, :<=>, :==, :===, :__id__,
:__send__, :assert, :class, :clone, :close, :define_singleton_method,
:display, :dup, :each, :enum_for, :eql?, :equal?, :extend, :freeze,
:frozen?, :gem, :gets, :hash, :inspect, :instance_eval,
:instance_exec, :instance_of?, :instance_variable_defined?,
:instance_variable_get, :instance_variable_set, :instance_variables,
:is_a?, :itself, :kind_of?, :method, :methods, :nil?, :object_id,
:private_methods, :protected_methods, :public_method, :public_methods,
:public_send, :read, :remove_instance_variable, :respond_to?, :rewind,
:send, :singleton_class, :singleton_method, :singleton_methods, :tap,
:then, :to_enum, :to_json, :to_s, :yield_self]
request.body.read: "key=value1&key=value2&commit=do+POST+test"
# 動かないバージョン
request.body: #<Rack::Lint::Wrapper::InputWrapper:0xXXXX @input=#<StringIO:0xXXXX>>
request.body.methods: [:!, :!=, :!~, :<=>, :==, :===, :__id__,
:__send__, ※, :class, :clone, :close, :define_singleton_method, 
:display, :dup, :each, :enum_for, :eql?, :equal?, :extend, :freeze,
:frozen?, :gem, :gets, :hash, :inspect, :instance_eval,
:instance_exec, :instance_of?, :instance_variable_defined?,
:instance_variable_get, :instance_variable_set, :instance_variables,
:is_a?, :itself, :kind_of?, :method, :methods, :nil?, :object_id,
:private_methods, :protected_methods, :public_method, :public_methods,
:public_send, :read, :remove_instance_variable, :respond_to?, ※,
:send, :singleton_class, :singleton_method, :singleton_methods, :tap,
:then, :to_enum, :to_json, :to_s, :yield_self]
request.body.read: ""

  そんなら、バージョンを指定する仕組みもあるんかいな、と思ったらシッカリある。

$ gem search '^rack$' --all
rack (3.0.9.1, 3.0.9, 3.0.8, 3.0.7, 3.0.6.1, 3.0.6, 3.0.5, 3.0.4.2,
3.0.4.1, 3.0.4, 3.0.3, 3.0.2, 3.0.1, 3.0.0, 2.2.8.1, 2.2.8, 2.2.7,
2.2.6.4, 2.2.6.3, 2.2.6.2, 2.2.6.1, 2.2.6, 2.2.5, 2.2.4, 2.2.3.1,
2.2.3, 2.2.2, 2.2.1, 2.2.0, 2.1.4.4, 2.1.4.3, 2.1.4.2, 2.1.4.1, 2.1.4,
2.1.3, 2.1.2, 2.1.1, 2.1.0, 2.0.9.4, 2.0.9.3, 2.0.9.2, 2.0.9.1, 2.0.9,
2.0.8, 2.0.7, 2.0.6, 2.0.5, 2.0.4, 2.0.3, 2.0.2, 2.0.1, 1.6.13,
1.6.12, 1.6.11, 1.6.10, 1.6.9, 1.6.8, 1.6.7, 1.6.6, 1.6.5, 1.6.4,
1.6.3, 1.6.2, 1.6.1, 1.6.0, 1.5.5, 1.5.4, 1.5.3, 1.5.2, 1.5.1, 1.5.0,
1.4.7, 1.4.6, 1.4.5, 1.4.4, 1.4.3, 1.4.2, 1.4.1, 1.4.0, 1.3.10, 1.3.9,
1.3.8, 1.3.7, 1.3.6, 1.3.5, 1.3.4, 1.3.3, 1.3.2, 1.3.1, 1.3.0, 1.2.8,
1.2.7, 1.2.6, 1.2.5, 1.2.4, 1.2.3, 1.2.2, 1.2.1, 1.2.0, 1.1.6, 1.1.5,
1.1.4, 1.1.3, 1.1.2, 1.1.1, 1.1.0, 1.0.1, 1.0.0, 0.9.1, 0.9.0, 0.4.0,
0.3.0, 0.2.0, 0.1.0)
$ bundle add rack --version=2.2.8.1 --skip-install
$ bundle add puma sinatra net-ldap --skip-install

  バージョンを検索する仕組みもあるし、指定する仕組みもある。これはよくできているな。

  問題は「3.0.0」から起こるようだ。根も深そうなので、今回はバージョンを戻す形で対処することにする。

  エラく時間を浪費させられたので憤慨する気持ちもないわけではないが、まぁ、近年のフレームワークのラクチンさは、恐ろしく多数の物件を積み上げた成果なわけで、たまにはこういうのもしゃーない。というわけで、他の人の時間の浪費が防がれることを祈って、ここに記録しておく。

  というような作業の合間に引き続きドルアーガ。ついに1人を残して59階に到達。いくのか? いってしまうのか? あ゛ぁー……連続のミスで打倒ならず。まぁ、だいぶ精度は上がってきているけどなぁ。もう少しだなぁ。

  画像の説明


2024-03-04(Mon) pack/unpackをよりどうにかする

  たわむれは、おわり、のはずだったが、美作にいけてしまった(?)ので、もうちっとだけ続くんじゃ。以下は前回のコードだが。

:c_ << 'ABC'
=> [65, 66, 67]

  unpackの場合、結果は配列になるので、こう書いたほうが、より直感的な気がしてきた。

[:c_] << 'ABC' # 動くけど……
=> [65, 66, 67]

  しかし、これは文法として有効なので、正しい結果は「[:c, 'ABC']」だ。ほんじゃ、演算子「<<」じゃなく「<」を使うか。

class Array
    def <(packed)
        packed.unpack(self[0].to_s.sub(/_$/, '*'))
    end
end

  「<」の元来の意味は「より小さい」だが、「<<」の元来の意味だって「左シフト」なのに、Rubyオフィシャルに「左に追加」の意味で使っているのだから、「<」を「左に渡す」の意味で使ったって構わんだろう。

  以上をまとめると、以下のようになる。

class String
    alias :perc :%
    def %(arg)
        self =~ /^:(.+)/ ? arg.pack($1.sub(/_$/, '*')) : perc(arg)
    end
end
 
class Array
    def <(packed)
        packed.unpack(self[0].to_s.sub(/_$/, '*'))
    end
end
 
':c_' % [65, 66, 67]
=> "ABC"
 
[:c_] < 'ABC'
=> [65, 66, 67]
 
':m_' % ['ABCDE']
=> "QUJDREU=\n"
 
[:m_] < 'QUJDREU='
=> ["ABCDE"]

  この記述方法だと、オレ的には澱みなく気分よくコードを記述できた感がある。まぁ、pack/unpackなんてそう頻繁に使うわけじゃないので実用性は薄いが、想像以上にイイ感じになったのでもったいないなぁ。


2024-03-03(Sun) pack/unpackをどうにかする

  昨日、pack/unpackって記述方法としてはどうなのよ、と書いてから、なんだか考え始めてしまった。要するに、以下の書き方が全然ピンとこないのでちっとも覚えられない、って話である。

[65, 66, 67].pack('c*')
=> ABC
'ABC'.unpack('c*')
=> [65, 66, 67]

  じゃ、ピンとくる書き方ってなんだって考えたら、以下が思い浮かんだ。これは実際に動く。いわゆるprintfだよね。

'%c%c%c' % [65, 66, 67]
=> ABC

  「%」演算子を使っているのがミソだ。「文字列化する」「引数は配列」というイメージが自然に湧く。じゃ、逆に「配列化する」演算子はなんだ? 苦し紛れだが、こんなのはどうだ。こんな文法はないので動かないが。

'%c%c%c' << 'ABC' # 動きません
=> [65, 66, 67]

  これに近い記述方法で、実際に動かすことはできないか? って考えたら、思い浮かんでしまい、できてしまった。

:c_ << 'ABC'
=> [65, 66, 67]

  RubyのSymbolを悪用(?)して定義した。「<<」演算子を再定義している。「c*」とは書けないので「c_」で代用してみた。

class Symbol
    def <<(packed)
        packed.unpack(self.to_s.sub(/_$/, '*'))
    end
end

  これを使うと、BASE64のデコード処理を以下のように書ける。

:m_ << 'QUJDREU='
=> ["ABCDE"]

  そうなると、逆にエンコードする時はこう書きたい。そんな指示子はないので動かないが。

'%m' % ['ABCDE'] # 動きません
=> "QUJDREU=\n"

  んが、今度は逆に、Stringの「%」演算子を再定義してしまえばいい。指示子の指定が「%」だと既存の機能と衝突するので「:」を割り振ってみた。

class String
    alias :perc :%
    def %(arg)
        self =~ /^:(.+)/ ? arg.pack($1.sub(/_$/, '*')) : perc(arg)
    end
end

  これを使うと、BASE64のエンコード処理を以下のように書ける。

':m_' % ['ABCDE']
=> "QUJDREU=\n"

  そもそもBASE64の変換は「文字↔文字」だからピンときにくいだけのことかもしれない。無理にピンとこさせずとも、以下をメモっておけば十分か。

['ABCDE'].pack('m*')
=> "QUJDREU=\n"
'QUJDREU='.unpack('m*')[0]
=> "ABCDE"

  まぁ、単なるたわむれプログラミングだ。わはははははははは、たわむれは、おわりじゃ。


2024-03-02(Sat) Sinatraでhtpasswd認証したりldap認証したり

  回転のプログラミングの途中だが、ひょんなことから、Sinatraでウェブサービスを提供する各種のコンテナに認証機能を付ける必要が生じた。ハテ?基本的にはHAProxyを被せて運用しているのだが、認証ってどうすんだっけ?

  調べると、HAProxyには単純な認証機能はあるものの、どうもLDAP認証の機能はないらしい。まぁ、Sinatra側でやるべきだよな、と思ったら、Sinatraにも単純な認証機能しかないようだ。

use Rack::Auth::Basic do |username, password|
    username == 'admin' && password == 'secret'
end

  これを追加すると全体に認証がかかる。恐ろしいほどに見たまんまなコードだw。逆に言えば、ここに自分で仕組みを組み込んでやれば、好みの認証機能を実現できるということだ。ハッシュの知識はあるし、既にLDAPにアクセスするコードも持っている。ほんじゃ、ということで書いてみた。

require 'sinatra'
 
eval(File.read('pv/sinatra.config')) rescue true
@configs ||= {}
 
unless(@configs[:no_auth])
    use Rack::Auth::Basic, 'Authorization Required' do |username, password|
 
        authorized = false
 
        if(@configs[:auth_htpasswd])
            ht_password = false
            open(@configs[:htpasswd_file]) {|fh|
                fh.each {|l|
                    l =~ /^#{username}:(.+)/ and ht_password = $1 and break
                }
            }
            if((it = ht_password) and it =~ /^{SHA}(.+)/i)
                require 'digest/sha1'
                hash_base64 = $1
                hash = hash_base64.unpack('m*')[0]
                challenge = Digest::SHA1.digest(password)
                authorized |= (hash == challenge)
            end
        end
 
        if(@configs[:auth_ldap])
            require 'net/ldap'
            ldap_password = false
            ldap = Net::LDAP.new(
                :host   => @configs[:ldap_host],
                :port   => @configs[:ldap_port],
                :auth   => @configs[:ldap_auth],
            )
            results = ldap.search(
                :base       => @configs[:ldap_search_base],
                :filter     => '(cn=%s)' % username,
                :attributes => ['userpassword'],
            )
            (it = results) and (it = it[0]) and (it = it[:userpassword]) and (it = it[0]) and ldap_password = it
            if((it = ldap_password) and it =~ /^{SSHA}(.+)/i)
                require 'digest/sha1'
                hash_salt_base64 = $1
                hash_salt = hash_salt_base64.unpack('m*')[0]
                hash = hash_salt[0, 20]
                salt = hash_salt[20, 4]
                challenge = Digest::SHA1.digest(password + salt)
                authorized |= (hash == challenge)
            end
        end
 
        authorized
    end
end

  上記のコードをapp.rbの冒頭に追加すればいい。ApacheのhtpasswdとLDAPの両方に対応するが、今のところ前者は「{SHA}」後者は「{SSHA}」形式のみ対応。当初「$apr1$」形式に対応しようと思ったのだが、それは単なるMD5ではなく、どうもApacheの独自実装らしい。ソース読めばRubyで実装できなくはないが、そんなのに付き合っても今後とも得はなさそうなので「{SHA}」への対応にした。なので、htpasswdでハッシュを生成する際には-sを指定する必要がある。

  どうでもいいが、Rubyのpack/unpackって、ホントに覚えられないなぁ。いつもpack/unpackのどっちが配列化/バイナリ化だっけ? ってなる。で、考えるもの面倒になって、適当にコードを書いて済ましちゃう。pack/unpackって、機能の格納方法としては天才的な発想だと思うけど、記述方法としてはどうなのよ……Perlが元祖なのかな。うーん……うううーん。

  ま、それはそれとして、GitLabに置いてあるSinatraのスケルトンコンテナに上記のコードを組み込んで、とりあえずは作業完了である。

  画像の説明

  話は替わるが、だいぶ前から我が家では卓上カレンダを自作しているのだが、時期がきたので印刷したらグレートシングだった。え、もう2周もしたの? と思ったらそのとおりだった。うげぇ。エンジニアすぐ死ぬ。


2024-02-26(Mon) トラディショナルなバックグラウンドの回転拡大縮小技術を再現完了

  特定の位置を中心に回転拡大縮小ができるようになり、非常に使い勝手がよくなった。十分な速度であり、描画の結果も非常に正確だ。我ながら傑作コードである。

  画像の説明 画像の説明 画像の説明

  自分で言うのも何だが、今回のコードには無駄な部分が一切存在しない。極めてシンプル。書きながら、煮詰めていくうちに、自然にそうなった。そこで、ハッと気づかされたのだ、当時の回転拡大縮小機能は「こう実装されていた」のかと。

  今回のコードは、ループの終了をゼロフラグで判断する(値比較で判断するより命令が減らせる)都合で、右下から左上にラスタースキャンするように回転パターンを生成しているが「ラスタースキャンするように生成している」のがミソだ。原理は同じなのだ。当時の回転拡大縮小機能も、ブラウン管モニタに「ラスタースキャンしながら回転パターンを生成していた」に違いないのである。PCG、スプライトなどと同様、CRTCに搭載されていた機能だったのだ。左上から順に、VRAMアドレス空間を「斜めに」参照しながら塗りつぶしていくという原理だったのだ。

  思えば、なんとなーく手を染め始めたWebAssemblyであったが、往年の回転拡大縮小機能の実装を再発明してしまうとは、気づけばエラいトコロに着地したもんだ。さて、レーシングゲームの実装を進めよう。


2024-02-23(Fri) アチラもコチラも微妙に進捗

  先日の思いつきを得て、BGの回転処理を作り始めたのだが、wasmのロード処理が非同期なのがどうにも扱いづらい。以前には悩んだ末にどうにかできたと思っていたのだが、ワザとヒドく遅延するプロキシを作って試しに通してみたら、以前に回っていたものも実はダメだった。JavaScriptの非同期処理はややこしく、検索すると多数ヒットするので悩んでいる人は多いようだ。

  結局、それだけで4日くらい地獄を這い回ってしまった。いや、Promiseとかasyncとかawaitとか、必要なときには便利なんだろうけど、必要じゃないときには邪魔すぎるんですが。UNIXのブロッキングみたいな扱いにできないもんすかね。ホント、本質じゃないところに時間かかり過ぎて殺意が湧きましたわ。まぁ、そのへんのコードは一段落したところで公開予定。

  その問題がクリアできたら、以前に自分が書いた回転処理を参考に、BGの回転処理を書くのだが、何しろアセンブラは可読性が悪く、自分の書いたコードとはいえ、再度、脳に染み込むまでには時間がかかる。んが、結局は大きくない修正でBGが回転するようになった。回転のサンプル画像には、知っている人なら知っている、回転するにふさわしい画像を使ってみたw。

  画像の説明

  BGの回転処理ではパターンのキャッシュは行わないので、都度の生成だが、手元の環境だと、240x320、60FPSでのCPU使用率は、Firefoxで20%、Chromeで6%程度。ちなみに240x320というサイズは、スーパーフォーミュラへのオマージュであるが、その前提であれば処理速度にはまだ十分に余裕があるといえよう。SIMD命令は、まぁ気が向いたら、で。次は、地球上の任意の場所を走り回れるようにするのがマイルストーンかな。

  そして、引き続きドルアーガ。上達するにつれて、難しい印象のフロアが変わってくるんだな。最近は、フロア10のレッドスライム、フロア45のナイトの重なり、ダークグリーンスライム、ブルーウィルオーウィスプが出てくるフロアがツラい。要するに「運が悪かった」で済まそうとしないと、テキが変わってくるというワケなのだ。

  画像の説明

  高次面で終わってそのままだと、その先が上達しないので、コンティニューしてクリアする。フロア59もだいぶ運が絡むよなぁ。もう少し練習したら、天野に出陣しようかしらん。


2024-02-19(Mon) オマエラが観たかったのはコ、レ!

  公開されてからしばらくして、突然に観たくなってきて「SEED FREEDOM」を観てきた。結論からいうと面白かった。大満足。以下、失礼な言い方を多数しているが、ほぼポジティブな意味で書いている。まさに「こういうのでいいんだよ」という映画だった。ホント、面白いって、なんだろう?

  振り返ればSEEDはDESTINYの途中くらいから、サカノボる形でハマったのだが、十分に楽しんだ反面、DESTINYでは、終盤で主人公を「戻す」という展開があり、どうもそれがネットの評判に影響を受けた結果のように感じて、なんだか安っぽいなぁ、というのが最後に残った印象であった。なので、しばらくして劇場化を計画中という話を聞いた時は、あんなグダグダの続きをどうしたいってんだ、と醒めていた。

  しかし、だ。時間のもたらす効果なのだろうか。無性に観たくなってしまった。どうせ、いつものペラい内容なのだろうし、ご都合主義なのだろうが、たぶん絶対に面白い予感しかない。

  画像の説明

  で、観た。せっかくなので4DXで観た。これまで4DXで観て損したと感じたことはないし、今回のモーションもバッチリであった。いや、肝心の映画の内容は安っぽい。20年前から、まったく進歩していない。登場人物は、パイロットであり政治家でありガキンチョだ。主にスキだのキライだので行動する。不殺というテーマらしいが、そんなのまったく感じさせないし、どうでもいい。でもそれでいい。もう、調味料をコレでもかコレでもかとブッかけすぎたトリプルチーズバーガーである。足せば足すほどウマいに違いないと思ってる。全部入りが正義だと思ってる。「面白ければなんでもいーんだよー」と思ってる。

  変に作家性を出さずに、同窓会に徹したのは、なかなかの選択ではないかと思う。目新しいテーマを立てたかっただろうし、新型モビルスーツを活躍させたかっただろうし、新曲を流したかっただろう、と思う。でもそこは、特段に妙なテーマを押し付けるわけでもなく、新型、新曲は序盤に片付けwて、クライマックスでは「いつもの」ですよ。「ストライクフリーダム」と「Meteor-ミーティア」ですよ。「オマエラが観たかったのはコレ」だろ、と。そうなんだよ「オレタチが観たかったのはソレ」なんだ、と。

  そういう意味では「トップガン マーヴェリック」にも似ているが、アッチはそれなりに時間の経過を感じさせているよね。コッチは、相変わらず「僕たちは何も守れてない」とか言って主人公がスネちゃうからね。成長していないにもホドがある。まぁ、劇中の時間では2年だからそれで妥当なのかもしれんが。

  こういう同窓会的な作り方をすると、次作が作りにくい気もするが、また10年後くらいにヒョコっと作る分にはいいかもしれない。DESTINYで「グフ」「ドム」まで、FREEDOMで「ギャン」「ズゴック」「ゲルググ」まで使ったから、まだ「ビグザム」「ジオング」あたりは残っているしなw。こういう過去ネタ頼りも、DESTINYの頃にはアザトさを感じたが、もう芸風として許せちゃう。ナイスバカ。あー、もう一回みたい。小ネタを全部回収したい。

  観終わって、深夜、雨の止んだ季節外れの暖かさの中を、オープンにしたロードスターでチンタラと帰りつつ振り返る。ロードスターも二番煎じな企画から始まって、代を重ねても「楽しければなんでもいーんだよー」だけで、ある意味、進歩させずに来たクルマなんだよな、と。なんだかシアワセを感じるな。

  自分は、以前エヴァのラストは新鮮味がないのがイマイチな原因だと書いたが、逆だったのかもしれないな。観たかったのが「いつもの」じゃなかったのが原因だったのかも。きっと「こういうのでいいんだよ」っていう感じのが観たかったんだ。

  マクロスもやりません? こういうの。なぜか、ミンメイが輝に迫ってきて、グラっときて、新型バルキリーで逃避行して、行き先で壊されて、なぜだかそこにあったVF-1で脱出するけど、ピンチになったところに、マクロスが来てダイダロスアタックして、美沙とモトサヤって感じでいいのでw。

  ホント、面白いって、なんなんだろう……?w


2024-02-16(Fri) トラディショナルなバックグラウンドの回転技術に思い至る

  例によって、常に何かに取り組んでいないと気がすまない人なので、ドルアーガで遊びながらも、プログラミングを進めている。まぁ、それ以外の細かいネタもいくつか並行しているんだけど。

  というのも、結構なペースでレトロゲーの類似品を作っている人をツイッターで見て、ちょっと触発されてしまったのだ。自分は、ゲームっぽいものを作るのは好きなのだが、ゲームシステムを作った辺りで満足して終わってしまうのだよな。レベルデザインに興味がないと言うか。

  しかし、ちょっと思いついたのだ。せっかく「WebAssemblyでトラディショナルな回転技術を再現」したのだから、それを生かしたゲームができないだろうか。しかも、ほとんどレベルデザインをする必要なしに、だ……あるんだ……あるんだよ。

  画像の説明

  こういう感じのヤツだ。コースを作ることなく、世界中のサーキットでレースをすることができる。通信対戦できるようにしたいなぁ。

  と思いつつ、あまりにユルユルとやっているので、任意の場所を映して、それを回転させた辺りで、着手から1ヶ月も経ってしまい、しかも、重大な問題に気づいてしまった。というのは、オブジェクト(キャラクタ/スプライト)の回転機能とバックグラウンドの回転機能は違うということだ。

  キャラクタの回転機能は、特定の「正方形のパターン」を元に、回転後の「正方形のパターン」を得るものだ。当初、バックグラウンドを表示するなら、それを敷き詰めればいいと思い、アルゴリズムを考えたのだが、「パターンの特定位置を中心に、回転後のパターンの表示位置」を求めようと思うと、どうにもパターン中心までのベクトルを求める必要が生じて、アークタンジェントを計算することが避けられそうにない。

  画像の説明

  これまで、すべて整数演算で済ませていたのに、そこに逆三角関数が出てくるのは美しくなさすぎる。数表で済まそうとしても、済ませられない規模になるし、誤差も出るだろうから、敷き詰めたパターンにスキ間が出ることも予測される。しかし、当時のアーケード(A-JAX, アサルト, スーパーフォーミュラなど)に一切のスキ間は見当たらない。つまり、当時からバックグラウンドの回転機能は、回転パターンを敷き詰めて実装しているワケではないのだ。

  で、気づいた。気づいたら簡単なことだ。たぶん、当時もそうだったのだろうと思える。バックグラウンドの回転機能とは、大きな正方形の領域が用意されていて、そこの回転前のパターンを描画し、それに対する「開始位置、サンプリングベクトル(角度)」を指定することで、画面全面に回転後のパターンを表示するという機能なのだ。それならば整数演算で済む。回転という概念が加わっても、オブジェクトとバックグラウンドは異なる実装になるんだなぁ。

  そういうことになると、以前に作った「WebAssemblyによるトラディショナルな回転技術」はそのまま使えないことになる。まー、最初から作り直しではないものの、作り替えという感じにはなるなぁ。それはそれで、ガッカリ半分、ウキウキ半分だけれども。