🌇

GLSL / WGSLに変換できるシェーダ言語を作った

に公開

Claude Code に作ってもらいました 😭

https://glre.dev/docs

https://github.com/tseijp/glre

開発背景

WebGL より強力な WebGPU API の登場

WebGL ライブラリの Three.js が WebGPU 対応を進めており、Apple のサポートも進歩して、WebGPU の実用性が高まってきました。WebGL は GLSL(OpenGL Shading Language)で記述しますが、WebGPU は WGSL(WebGPU Shading Language)で書きます。Three.js で WebGPU を扱う場合は独自の TSL(Three.js Shading Language)で記述するようです。

AI などレンダリング以外の GPU 活用を可能にする compute shader

WebGL のシェーダーは画像や 3DCG レンダリング用でしたが、ブラウザでも WebGPU API を通じて compute shader を活用できるようになりました。Three.js でパーティクル実装を試したのですが、まだ compute shader がうまく動作しませんでした、、、

なぜ作ろうと思ったか

WebGPU をずっとやりたかったので、勉強をはじめました。WebGL を勉強したときに WebGL ライブラリを開発してたこともあり、自作ライブラリの WebGPU サポートから開始しました。WebGPU を実装するための WGSL は Rust 言語風の構文で、WebGPU API は CPU から GPU へのデータ送信設定などを事前に行う必要があります。これらをシェーダーコードから自動的に設定したかったため、Claude Code に WebGPU 対応と中間言語を 1 ヶ月くらいで実装してもらいました。

欲しかった機能

Three.js シェーダー言語互換で GLSL と WGSL の両方に出力

WebGPU には compute shader と、計算結果を変数として保存できるストレージ機能があります。compute shader は新しい WebGPU 技術のため、WebGL 側にも同様の機能を実装する必要がありました。WebGL で同じ動作を実現するため、シェーダーの計算結果を画像に書き込むなど CPU-GPU 間の処理を自動化しました。開発した中間言語の compute shader は WebGL と WebGPU 両方で動作します。

Claude Code による 自動エラー検出

vec3 と vec2 など異なる長さのベクトル演算をしたときに、TypeScript が事前にエラー表示できれば、Claude Code が IDE MCP を通じて自動検出できます。Claude Code が自動でエラー検出すれば、ブラウザの dev tool からエラーメッセージを確認することなく自動修正できます。

Claude Code によるシェーダー自動実装

TypeScript がエラー出力できれば Claude Code が自動実装できるため、欲しい絵を言えばそのまま出力されるはずです。中間言語開発も Claude Code の得意分野ですので、Three.js shading language のドキュメントを要件と仕様として渡すだけで実装してもらいました。

できるまで

スマホから PC に SSH して tmux 経由で Claude Code 操作

Tailscale で PC とスマホを同じ VPN に設定しています。PC の開発環境は VSCode の devcontainer を使ってコンテナで動かしています。スマホから Termux を使って以下のコマンドを実行します。後で PC で確認するために、tmux を使って出力を同期させます。

ssh ${username}@${address}
docker exec -u node -it ${docker_id} bash
tmux -u new -A -t dev
claude

要件定義として実装計画を出力 → 技術仕様をコメント

次のようなプロンプトを使用しました。まず Claude Code に用意したマークダウンと修正が必要なファイルを読ませ、要件定義として実装計画を出力します。実装計画に技術仕様を加筆しながら実装してもらいました。

- **MainTask [1]**
     - **SubTask [1.1]**
          - Read(`.articles/Q&A_boytchev_tsl-textures_Wiki.md`)
          - Read(`.articles/Three.js_Shading_Language.md`)
          - Read(`.articles/WebGPU_from_WebGL.md`)
     - **SubTask [1.2]**
          - Read(`packages/core/src/node/build.ts`)
          - Read(`packages/core/src/node/create.ts`)
          - Read(`packages/core/src/node/scope.ts`)
          - Read(`packages/core/src/node/types.ts`)
     - Write(`.articles/2025${M}${D}_issue_${this_issue_title}.md`)
     - Interrupt
- **MainTask [2]**
     - Fix after confirming Feedback

実装自動化

ある程度開発ができてからは、デモコードを作成してもらい、コードを実行して出てくるエラーメッセージを原因予測と一緒に Claude に渡して修正していきました。エラーが発生するたびに Claude に投げて実装計画を出力させ、技術仕様を確認して改良していきました。

言語仕様と機能

TypeScript から GLSL と WGSL に変換して実行

基本的な GLSL / WGSL 構文をサポートし、TypeScript なので型やコードをモジュールとして外部化できます。floor 関数や uv 変数など GLSL/WGSL 共通のビルトイン関数・変数を用意しています。

const fragment = () => {
        const cell = uv.mul(8).floor()
        const checker = mod(cell.x.add(cell.y), 2)
        return vec4(vec3(checker), 1)
}

Screenshot of simple checker demo

Colors and Coordinates | glre

また、中間言語から compute/fragment/vertex シェーダーの uniform/varying/attribute を設定できます。Buffer layout など CPU から GPU へのデータ送信設定は自動構築されます。以下のように、中間言語出力のマクロ代わりとしても TypeScript を使用できます。

const xyz2rgb = (x, y, z) => XYZ2RGB.mul(vec3(x, y, z)).clamp(0, 1)

Screenshot of XYZ2RGB demo

Colors and Coordinates | glre

チュートリアル作成

私は GLSL をちょっとわかってきた程度で、WGSL と TSL も初めてだったので、自分の勉強用としてもチュートリアルを作成してもらいました。エッジ検出など自分では思い付かないようなデモもあります。

Screenshot of edge detection demo

Images and Textures | glre

2024 年版レイマーチの論文移植

レイマーチは陰関数から 3D レンダリングする技術です。レイマーチは計算が重くなりがちですが、最新論文だと数学の力で最適化させることができるそうです。4 次元までで調和関数であれば最適化できるという、次元が違う手法っぽいのです。実装はシンプルなので Claude さんにお願いして実装してもらいました。メタリックやラフネスなどちょっと間違っている気がするのですが、軽い処理でレンダリングできてそうです。

Recording of harnack demo

https://www.youtube.com/watch?v=9h13FPuBvM8

自分で使ってみて

WebGL と WebGPU 両方で動作する compute shader 実装

次のような compute shader は GLSL と WGSL 両方に変換され、WebGL と WebGPU 両方で動作可能です。

const compute = Fn(([id]: [Vec3]) => {
        const pos = positions.element(id.x).toVar('pos')
        const vel = velocities.element(id.x).toVar('vel')
        pos.assign(pos.add(vel.mul(0.01)))
        If(pos.x.lessThan(0.0).or(pos.x.greaterThan(1.0)), () => {
                vel.x.assign(vel.x.mul(-1.0))
                pos.x.assign(pos.x.clamp(0.0, 1.0))
        })
        If(pos.y.lessThan(0.0).or(pos.y.greaterThan(1.0)), () => {
                vel.y.assign(vel.y.mul(-1.0))
                pos.y.assign(pos.y.clamp(0.0, 1.0))
        })
        positions.element(id.x).assign(pos)
        velocities.element(id.x).assign(vel)
})

Recording of particle GPGPU demo site

同じコードで compute shader が動作する実装

WebGL には compute shader がないため、テクスチャバッファーとしてデータを保存・取得させて同じような機能を実装していますが、 fragColor として計算結果を出力してテクスチャに書き込むため、通常は 1 つのストレージしか使用できません。そこで WebGL2 の Multiple Render Targets を使うことで、複数ストレージを再現しました。

今後

現在は作成したサンプルコードから型安全性を強化したり、バグがあれば修正しています。以前 GLSL をアップロードできるサイトを開発していました。TSL 対応が完了したため、TypeScript でシェーダーを書けるサイトを作ろうと思っています。
https://play.glre.dev

技術実装の詳細

抽象構文木の設計

以前、Ruby でシェーダー風コードを書く glrb というライブラリを作成していました。しかし、glrb は複数行スコープや関数定義ができていませんでした。その反省から、すべてがワンライナーにならないよう複数行スコープと関数定義を実装しました。抽象構文木の資料を参考にして、二分木ではありませんが木構造みたいなものを構築しました。木構造の各ノードは create 関数で構築しています。React.createElement を参考に、同じ引数を使用し、children で木構造を保存しています。

const create = (type, props, ...args) => {

React と同じように木構造を構築しているので、JSX でもシェーダーを記述できます、、、

<script type="module" src="https://esm.sh/tsx"></script>
<script type="text/babel">
        /** @jsx create */
        import { create, uv } from 'https://esm.sh/glre@latest'
        document.body.innerText = (
                <scope>
                        <declare>
                                {uv.x}
                                <variable id="x" />
                        </declare>
                        <return>
                                <conversion>
                                        vec4
                                        <variable id="x" />
                                        {0.4}
                                        {0.6}
                                        {0.8}
                                </conversion>
                        </return>
                </scope>
        )
</script>

上記コードは次のように出力されます。

var x: f32 = (out.position.xy / iResolution).x;
return vec4f(x, 0.4, 0.6, 0.8);

型推論システム

const fragment = Fn(([uv]: [Vec2]) => {
        const x = uv.x.toVar('x')
        return vec4(x, 0.4, 0.8, 1)
})

JavaScript と TypeScript 両方で型推論を実装する必要がありました。上記の fragment 関数を fn fragment(uv: vec2f) -> vec4f のように変換するため、JavaScript で 引数と戻り値の型を推論する必要がありました。木構造を確認していって、引数 uv が vec2、戻り値が vec4 と判定することで型推論しています。WGSL コード出力用の型推論とは別に、ジェネリクスを使った TypeScript 側の型推論も別で実装しました。

GitHubで編集を提案

Discussion