以下の内容はhttps://labs.spookies.co.jp/より取得しました。


レイマーチングをやってみよう

みなさん、こんにちは!多分ctfの時のブログ振り、こまさんです!

今日はなんか面白い事したいな~と思って、多少得意分野の3D系からちょっとお話してみようかな、と思います!

実際にWebGL2でぐりぐりうごかせるデモもあるので、ぜひ触ってみて下さいね!

そもそもレイマーチングとは何ぞや

一言でいうと、3Dのレンダリング(シーンを描画すること)の方法の一つです。

とまぁこれだけじゃ「はぁ。そうですか。。」としかならないと思うので、ちょっとしっかり説明していきますね!「そんなの知っとるわ!」という方はガンガン飛ばしてもらって大丈夫です!

まぁそんなわけで、「レイマーチング」について語る前に、ここ5年強ぐらいで(特にゲームを触る方なら)聞くことが多くなったであろう「レイトレーシング」というものから話そうかな、と思います。

こちらは、「我々が見ている視野は、目に入ってきた光の色や強さで決まるよね。じゃあ逆に目の方から光線を追跡してその線が光源の方に近づけば明るい。みたいな感じで計算できるんじゃね?」といった感じの方法です。

読んで字の如く、レイ(=Ray、光線)をトレース(=Trace、追跡)するんですね。

ただこれってかなり処理の量が多いんです。というのも、光を追跡する、と言っても反射の計算が必要になります。壁に当たったら反射させて、また当たったら反射させて、、とやって光源にある程度まで近づけば光に衝突した、という感じで、色を計算したりするのですが、、、

もちろん計算する反射の回数を制限すればいいよね、となるのはその通りなのですが、あまり少なくしすぎると今度画質が苦しいものになってしまいます。。

でも昨今のGPUの進化によってある程度リアルタイムでもできるようになってきていて、実際ゲームでもレイトレーシングを使って画質の向上をできるものが結構あります。

まぁちょっと脇道に逸れてしまいましたが、今回話そうと思っていた「レイマーチング」はこのレイトレーシングの一種です。

ですが、大きな違いがあります。まず2つ、

  • 基本的に反射は計算しません!(これは計算することもできます)
  • そして次に、光線を追跡する際にレイトレーシングは、オブジェクトに当たるところまでを1ステップで進行させます。それに対して、レイマーチングは少しずつ進めていく(レイ(=Ray、光線)がマーチ(=March、行進)するということですね!)という感じ。そして一度衝突したらそこで終わりです。

ここから分かるように、よりきれいでリアルに近いような描写をするときにはレイトレーシングの方が向いています。

というか基本的にはレイトレーシングの方が優秀です。

ですが、今回レイマーチングに的を絞った理由は、3つ目の違いがとても重要で、限られた場面ではレイマーチングがとても優秀なんです。 その違いは、

  • オブジェクトの形状の定義が「関数」によって行われる

というところです。

正確に言うと、

  • 「ある点が与えられたときにその点から描画したいオブジェクトの表面までの最短距離」を求める関数

を定めると、そのオブジェクトが描ける、という感じです。

正直多分ほとんどの人は「なんでそれで描けるんだ???」と思ったと思いますが、今回は細かい事は省略して、実際にどんな感じのものが描けるか、というデモを通しながらレイマーチングの魅力を見せていきたいかな、と思います。

詳しいことが知りたい人は色々調べてみてくださいね!とっても奥が深いです


まずは手始めに、立方体!

ここから先のデモはWebGL2を使用して描画しています!比較的古い端末だと、重かったりそもそも見れなかったりするので、そこはご留意ください!

まずはとてもシンプルに立方体を1つです。

回転やズームもできます! (PCならドラッグで回転、ホイールでズームイン/アウト。モバイルならタッチで回転、ピンチ操作でズームイン/アウトできます)

フルスクリーン表示に対応している環境では、キャンバスの右上にボタンがあると思うので、スペックに自信のある方はぜひ大画面で楽しんでください。(描画するピクセル数が上がるので結構重いです!ご注意を!)

ブーリアン演算

次にとても強力なレイマーチングのメリットを一つ紹介してみます。

見出しで「ブーリアン演算」といったのでCADやモデリングなどを触ったことのある人はピンときたかもしれませんが、要は2つのオブジェクトの「和・差・積」です。

  • 和(Union): 二つのオブジェクトの合体ですね。
  • 差(Subtract): 片方のオブジェクトからもう一方のオブジェクトの重なっている部分をくり抜く感じです。
  • 積(Intersect): 二つのオブジェクトの重なっている部分だけを残す感じです。

これを形状に対して行うのって難しそう、と思ったかもしれませんが、レイマーチングでシーンを描画する場合驚くほど簡単になります。

ここで、先程言った「オブジェクトの形状を決める関数」を距離関数(以下ではSDFと呼称)と呼びます。

ここで立方体と球があるとして、さっきの3つの操作をしてみます。

立方体のSDF(最初のデモで使った「立方体を表す関数」)をf、球のSDF(同じように「球を表す関数」)をgとします。 そして先程の3操作を行った形状もまたSDFで表現されます。

  • 和(Union): U(x) = min(f(x), g(x))
  • 差(Subtract): S(x) = max(f(x), -g(x))
  • 積(Intersect): I(x) = max(f(x), g(x))

これだけです。二つのオブジェクトを表す関数で距離を計算して、それの小さいほうや大きい方をとる(差は符号を反転させる必要がありますが)。 これだけで「それぞれ3つの操作を行った形状を表す関数」になっちゃうんです。

そしたらその関数を使って描画すると、合体してたり、くり抜かれたり、重なってる部分だけになったり、と面白いです。

そんなわけで次のデモでは、立方体と球体を同じ配置でそれぞれ3つの演算をしてみています。

左から順に「和」「差」「積」です。

フラクタル図形を描画できる。

これがもう一つの強力なレイマーチングのメリットというか強みです。

フラクタル図形、というのは「自己相似図形」、つまり図形の一部分が全体と同じような構造になっている。というようなものです。

今回はその中から「メンガーのスポンジ」と「マンデルバルブ」というものを描画してみました。

もしかしたら集合体恐怖症の方は苦手かもしれないので注意してくださいね。

メンガーのスポンジはメッシュを細かくしていったら描画できそうだと思うかもしれません。まぁもちろんできなくはないのですが、再帰的に穴をあけていく関係上、指数的にメッシュの数が増加してしまうのですぐに限界が来てしまいます。

レイマーチングはこういうものを効率的に描画することができます。

(今回のデモでは8階層まで穴をあけています。メッシュデータに起こそうとしたらとんでもない頂点数になるはずです)


終わりに

いかがでしたか? 多分初めて知った人なら、なかなか新しい感覚を得られたんじゃないかな、と思います!

使う場面が来るかは分かりませんが、もしこれを見た人の創作などに新しいインスピレーションを提供できればうれしく思います。

あと最後に、ソースコードはscriptタグで埋め込んであるので、興味のある方はDevtoolsとかで取り出してもらって自由に研究したりこねこねしたりしてみてくださいね~(Gistとかに上げてる感じじゃなくてすみません😅)

それでは!

仮想マシン上で動作するJavaScriptコードに出会った話

キッカケ

日頃から、Webサイトを閲覧する際に「このサイトはどのようなコードで動作しているのか」という観点で、DevToolsを開いて実装を確認することが多い。

ある日、そうした調査の中で、通常のJavaScriptとは明らかに異なる構造を持つコードに遭遇した。確認してみると、それは単なる難読化ではなく、仮想マシンそのものをJavaScript上に実装し、その上で独自の命令列を実行するという手法が用いられていた。

Javaのように、元のコードを一度独自バイトコードへコンパイルし、それを仮想マシン上で解釈実行する構成になっており、ブラウザ上で動作するJavaScriptとしては非常に異質なものである。

構造を理解しようとコードを追い始めたものの、静的に読むだけでは処理内容を把握するのは困難だった。しかし同時に、「これは解析対象として非常に興味深い」と感じ、本格的に挙動を調査してみることにした。これが、今回の解析を始めたきっかけである。

仮想マシンベースの難読化とは

通常の難読化といえば、変数名を意味不明な文字列に置き換えたり、文字列リテラルを暗号化したりする手法が一般的です。しかし、仮想マシンベースの難読化は全く異なるアプローチを取ります。

  1. 元のJavaScriptコードを独自の命令セットにコンパイル
  2. その命令を実行する仮想マシンをJavaScriptで実装
  3. 実行時に仮想マシンが命令を解釈・実行する

実際のコード例

実際に見つけたコードは、概ね次のような構造をしていました

// デコード処理
var B = S("ながーいバイトコード", "C8p6e3HND=F...");

...

// 命令関数一覧
var P = new Proxy("ながーいバイトコード".split("|"), {
    get: function(T, n) {
        return new Function(u(T[r[Number(n)]], "7pjXi...").map(...).join(""))();
    }
});

...

var d = {
    d: [0],
    v: {},
};

function w(e, r) {
    e.d[g(e)] = r;
}

function A(v, u, f, a) {
    var r = v[u[0]++];
    if (r !== f[0]) {
        if (r === f[5]) return !1;
        if (r === f[1]) return !0;
        if (r === f[4]) return null;
        ...
        return u[r >> 5];
    }
};

function main(e) {
    while (true) {
        var _ = P[B[e.d[0]++]];
        var o = _(e, ...);
        if (o === null) {
            break;
        }
    }
}

ざっくり言うと、Eが命令列、Nが命令ハンドラー、dが仮想マシンの状態、lがメインの実行ループになっています。通常のJavaScriptとは全く異なる構造で、一見しただけでは何をしているのか全く分かりません。

解析・デコードをしていく

さて、このコードを解析していくわけですが、今回の目標はシンプルに設定しました。

  • 命令処理の一覧を出力する
  • 制御フローを可視化する

「よし、デコーダーを1から書くぞ!」と意気込んでみたものの、すぐに現実を思い知らされます。仮想マシンの命令セットを完全に解析して、デコーダーを実装するのは途方もない作業です。 そこで方針転換。デコード処理を1から書くのではなく、元のJavaScriptコードに解析用のコードを挿入して、動的に解析することにしました。つまり、仮想マシンを実際に動かしながら、その挙動をトレースしていくわけです。

  1. メインループに命令の実行ログを挿入
var _ = P[B[e.d[0]++]];
var evaluatePosition = e.d[0] - 1;
var o = _(e, ...);
console.debug(JSON.stringify({
    "evaluatePosition": evaluatePosition,
    "instructionText": _.toString()
}));
  1. オペランド読み取り処理にログを挿入
function b(e) {
    var witnessedValue = zx(E, e.d, I);
    console.debug(JSON.stringify({
        "evaluatePosition": e.d[0],
        "accessDirection": "read",
        "witnessedValue": witnessedValue
    }));
    return witnessedValue;
}
  1. 値書き込み処理にログを挿入
function w(e, r) {
    var evaluatePosition = g(e);
    console.debug(JSON.stringify({
        "evaluatePosition": evaluatePosition,
        "accessDirection": "write",
        "witnessedValue": r
    }));
    e.d[evaluatePosition] = r;
}

この状態でJSDOMを用いて実行すると以下のようなログが得られます。

...
{"evaluatePosition":147208,"accessDirection":"read","witnessedValue":"t"}
{"evaluatePosition":147211,"accessDirection":"read","witnessedValue":"t"}
{"evaluatePosition":4,"accessDirection":"write","witnessedValue":true}
{"evaluatePosition":147204,"instructionText":"function(n,e,a){a(n,e(n)===e(n))}"}
...
{"evaluatePosition":375,"accessDirection":"read","witnessedValue":73}
{"evaluatePosition":376,"accessDirection":"read","witnessedValue":165}
{"evaluatePosition":7,"accessDirection":"write","witnessedValue":73}
{"evaluatePosition":374,"instructionText":"function(n,e,a,v,i,r){var o=r[5];a(n,o(n)%o(n))}"}
...
{"evaluatePosition":115656,"accessDirection":"read","witnessedValue":"Object"}
{"evaluatePosition":115659,"accessDirection":"read","witnessedValue":"stringify"}
{"evaluatePosition":115660,"accessDirection":"read","witnessedValue":"function(){var s=t();s.d[3]=arguments;for(var f=0;f<arguments.length;f++)s.d[f+4]=arguments[f];return s.d[1]={Z:this,d:[0],v:[],m:u,o:d,n:u==null?void 0:u.n},s.d[0]=l,b(s),s.d[2]}"}
{"evaluatePosition":115659,"instructionText":"function(n,e){e(n)[e(n)]=e(n)}"}

ログから見えてきたこと

出力されたログを見れば、仮想マシンの動きがある程度わかる。

例えば115659の命令は

  • 115656で"Object"を読み取り
  • 115659で"stringify"を読み取り
  • 115660で関数を読み取り

つまりこれは、Object["stringify"] = function() {...} のようなプロパティの設定処理を行っていることがわかります。 命令自体は抽象的ですが、実際に読み取られている値と組み合わせることで、具体的な動作が見えてくるわけです。

難読化の本質

VMを用いた難読化は強力です。コードを見ただけでは、何をしているのか全く理解できません。 しかし、今回の解析を通して得られた知見は、「完全に理解しなくても、動かして観察すれば大体わかる」 ということでした。 静的解析は困難でも、動的解析なら十分に追跡可能です。ログを仕込んで実行すれば、仮想マシンが「実際に何をやっているか」は見えてきます。 難読化されたコードと向き合うときは、「完璧に理解しよう」とするよりも、「必要な情報だけ抽出しよう」という姿勢の方が、結果的に近道になることが多いのかもしれません。

社内ツールは「ちゃんと雑」でいい

背景

社内でツールを運用していると、次のようなやり取りが定期的に発生します。

  • 「そのリンク、どこから取ってきました?」

  • 「esa に貼ってあった URL、遷移できなくないですか?」

いずれも致命的な問題ではなく、業務が止まるわけでもありません。
そのため長い間、「まあ困るけど仕方ない」という扱いのまま放置されてきた類の問題です。

ツール自体は正しく動作している一方で、
URL を共有することが不安定、という状態でした。

画面や機能が増えるにつれて、

  • 条件付きパラメータを含む URL が増える

といったことが日常的に起きていました。

大きな障害ではありませんが、
違和感だけが残り続ける状態です。


「ちゃんと作るほどでもない」問題への向き合い方

この手の問題を正面から解決しようとすると、だいたい次の案に行き着きます。

  • 専用のリダイレクト API

  • 管理画面

  • ルール化された運用

実際に要件を書き出し、構成図を描き、検討もしました。

しかし整理すればするほど、コストと問題の重さが釣り合わないことが明確になります。

  • URL のパターンは多くても十数種類

  • 利用者は社内メンバーのみ

  • 権限や認可は遷移先ですでに担保されている

  • 重要なのは柔軟性より「迷わず使えること」

つまり、
解決策は軽くあるべきという性質でした。

ここで方針を切り替えました。

  • 増えたら足せる

  • 壊れにくい

  • 説明しなくていい

「きれいに作る」のではなく、
雑でいられる構成を目指すことにしました。


URL を「人が読める形」に戻す

やりたかったこと自体は非常に単純です。

  • esa や Slack に貼ったときに

  • 見ただけで用途が分かり

  • 間違えにくいこと

そのために用意したのは、次のような形式の URL です。

/共通パス/[用途]?意味のあるパラメータ

設計方針は以下の通りです。

  • [用途] でリンクの種類を明示する

  • パラメータ名は省略せず、人が読める名前にする

  • 内部で既存の複雑な URL に変換する

プロジェクト指定、メンバー指定、複数条件の集約なども、
そのまま貼っても安心できる URLとして扱えるようになりました。


AI の使い方

この仕組みを作る過程で AI も利用しましたが、
コード生成と設計の壁打ち役として使っています。

具体的には次のような点を確認しました。

  • サーバーは本当に必要か

  • 静的な構成で完結しないか

  • 設定ファイルに逃がせる部分はどこか

  • 同じ歪みを将来また生まないか

長く放置された問題は、
「前提」だけが固定されたままになっていることが多いです。

それを一つずつ疑う相手として AI を使いました。

結果として、

  • 実装は最小限

  • 設定追加で拡張可能

  • 既存運用に自然に混ざる

という形に落ち着きました。


「また増やして」と言われても破綻しないために

こうした改善で避けたいのは、
一度整えた結果、触りづらくなることです。

そのため、次の点だけは意識しました。

  • 触る場所は 1 箇所に集約する

  • 書き方に迷わない構造にする

  • 実装者以外でも触れる

完成度よりも、
雑に増やせる余白を優先しています。

社内ツールは、その方が長く生きます。


まとめ

今回の仕組み自体は目立つものではありません。

ただ、

  • esa に貼るときに迷わなくなった

  • Slack で説明文を書く頻度が減った

それだけで十分な効果がありました。

長く存在してきた不便さは、
派手に改善するよりも
考えなくていい状態に戻すことで解消されることがあります。

社内改善とは、新しいものを足すことではなく、
見て見ぬふりをしてきた違和感を片づけることなのかもしれません。

速すぎる技術の進化に疲れた私が、一周回って「基礎」に救われた話

はじめに

こんにちは、エンジニアのhiraokaです。

気がつけばもう12月。街はクリスマス一色ですが、我々エンジニア界隈はアドベントカレンダー一色ですね。

さて、いきなりですが懺悔させてください。 今年の技術トレンド、正直ぜんぜん追いきれませんでした。

毎朝起きるたびに新しいAIモデルが発表され、フロントエンド界隈では「これからは〇〇だ!いや××だ!」という議論が繰り返され…。ブラウザのブックマークには「あとで読む」つもりの技術記事が山のように積まれていますが、これらを「今年中に読む」可能性は、限りなくゼロに近いでしょう。

「置いていかれる…」という焦燥感で胃がキリキリしていた時期もありました。 でも、年の瀬に改めて振り返ってみると、ある一つの考えに達しました。

「技術が進化すればするほど、結局最後に頼れるのは『基礎』だ」 という考えです。

AIにコードを書かせて気づいた「違和感」

きっかけは、業務で生成AIを活用し始めたことでした。

確かにAIは凄いです。やりたい処理を投げれば、70、80点くらいのコードは一瞬で返してくれます。ボイラープレートを書く時間は劇的に減りました。 でも、そのコードをプロダクトに組み込んだ瞬間、予期せぬエラーが出たとします。

その時、AIは急に沈黙します。(あるいは、適当な嘘をついてループし始めます)

結局、デバッガを起動し、ログを追いかけ、原因を突き止めるのは自分自身です。そして、その時に役立った知識を思い出してみると、決して「最新フレームワークの特別な作法」ではありませんでした。

  • 「あ、これ非同期処理の順序がおかしいな」(言語仕様の基礎)

  • 「HTTPヘッダに認証トークンが乗ってないじゃん」(HTTPの基礎)

  • 「このSQL、インデックス効いてないから遅いんだ」(DBの基礎)

  • 「そもそも、問題解決のアプローチとして、このやり方は正しいのか?」(問題解決の基礎)

そう、トラブルの現場で私を救ってくれたのは、結局「何十年も変わっていない枯れた技術」やエンジニア以前の「社会人としての基礎力」だったんです。

流行り廃りの激流で「変わらないもの」を武器にする

最新のフレームワークも、便利なSaaSも、皮を剥いでみれば結局は「HTTPプロトコル」や「アルゴリズム」といった基礎の上に成り立っています。

表面のツール(How)は凄まじいスピードで変化し、陳腐化していきます。今日覚えたツールの使い方は、3年後には役に立たないかもしれません。 でも、その下にある原理原則(Why)は、10年後も、おそらく20年後も変わりません。

AI時代になって、「コードを書く」ハードルは下がりました。 その代わり、「コードの良し悪しを判断する」ハードルは上がっています。

AIが出してきたコードに対して、「動くからヨシ!」ではなく、「なぜ動くのか?」「セキュリティリスクはないか?」「パフォーマンスは最適か?」を見極める力。 その力の源泉こそが、地味で退屈に見える「基礎力」なのだと再認識しました。

来年の抱負:焦らず「深く」潜る

というわけで、来年の私のテーマは「温故知新」です。

新しい技術に飛びつくのをやめるわけではありません。でも、情報の波に溺れそうになったら、一度「地面」に足をつけようと思います。

流行りのライブラリの使い方を覚える前に、公式ドキュメントを読んでみる。

エラーが出たら、Stack Overflowの答えをコピペする前に、エラーログの英語をちゃんと読んでみる。

そうやって「基礎」という根っこを太くしていくことが、結果として、どんな激しい変化の風が吹いても倒れないエンジニアへの近道なのかもしれません。

今年も一年、お疲れ様でした。 年末年始は、難しい技術書はいったん閉じて、ゆっくり頭を休めましょう。 それでは、よいお年を!

Slackがちょっと便利になるツールをAIだけ頼って開発した話。

どうも。 AIありきの仕事に慣れすぎて、もう一年前には戻れないと感じている鈴木とまっちゃんです。

さて、この度社内ソリューションとして、ちょっと使いやすいSlackのグループ編集ツール「SlackGroupManager」をほぼAIの力だけで開発したので、その道のりをブログにします。

きっかけ

2025年3月。 AIコーディングエージェントやバイブコーディングが実用の域に達し始めた頃。 スプーキーズでも、当然AIの業務導入は話題になりました。

「見せてもらおうか、(当時)最新のAIの性能とやらを。」

ちょうど「Slackのグループ割当のUIが使いづらい」という悩みが社内の業務改善チケットの中に転がっていたので、一旦ベンチマークを兼ねてGitHub Copilotに社内ソリューションを作らせようと試みたことから話は始まります。

完成までの道のり

最初に選んだのはNext.js。 SlackAPIを叩く都合上プロキシが必要だったためです。 また、FEとBEを分ける構成にするとコード量が増えてコンテキストを消費し、仮にモノレポであっても完成できないんじゃないかと思ったことも理由の一つでした。

人間ならSlackのAPI仕様を調べて、セットアップして、コード書いて、とどうやっても一日はかかるだろうと言う仕事を、当時のGitHub Copilotくんは一時間そこらで動くものを作り出してしまったのです。 このときの感動は今でもはっきり覚えています。

が、しかし当時のAIコーディングは考慮不足も多く、社内ツールとしてイントラに載せるにしても改修が必要で、AIの力だけではデプロイにまでは辿り着けませんでした。 またお陰様で他の案件が忙しくなったのもあり、優先順位が低かったこのプロジェクトは一時的に止まってしまいます。

そして半年以上経った先月、流石にデプロイさせて一旦プロジェクトに終止符を打とうと再度動き出します。 ツールもGithubCopilotからCodexに、モデルもClaude Sonnet 3.7からGPT-5-Codexに進化。 また、最近社内でCloudflareが各所で導入されており、このソリューションもCloudflareスタックで固めようという方向性にシフト。 これにより、構成から根本的に見直され、Codexによる大改修が始まりました。

技術構成

最終的には、下のような技術構成になりました。

  • React
  • Hono
  • Slack API
  • Cloudflare Workers
  • Cloudflare Zero Trust

半年前GitHub Copilotに作ってもらった Next.js ベースのアプリを、フロントエンドとバックエンドに分離し、Workers上で動かすための構成にしました。 この作業は、Codexを使って行いました。

プロンプトはシンプルで、

このNext.jsのプロジェクトを、ReactとHonoを使用して、Cloudflare Workersでデプロイできるようにしてください

のように指示を投げて完成させました。 ただ、パッケージが古かったので、ncuを使ってパッケージの更新をして完了!

詳細な構成として、バックエンドは Hono で API ルーティングを管理し、静的アセットは Workers の Assets 機能で配信する、というかなりオーソドックスな構成です。 シンプルイズベストですね

今後どういうふうにしていきたい?

Slack APIのレートリミットが厳しく、本番の運用には耐えられないかもしれません... ということで、KVを活用したAPIのキャッシュなどに力を入れていきたいですね 💪

また、誤操作などで権限を振ってしまうと大変(!)なので、確認ダイアログなども作成して誤操作防止などにも力を入れていきたいです!




以上の内容はhttps://labs.spookies.co.jp/より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14