ウェブコンポーネントによるグラフ


私は当初、4つの異なるタイプの図面に同じAPIを適用するオリジナルシリーズを行う予定でした:SVG、キャンバス、WebGL、およびCSS.しかし、私がWebgpuでより多くの探査をしている今、私は同様にそれも比較するかもしれないと思いました.この文書のWebGpuは安定していないが、クロムブラウザのフラグの後ろに見つけることができるので、我々は実装を構築することができますうまくいけばそれはあまり変更されません.あなたがそのポストを読んだと仮定して、私はすべてとピックアップに触れません.

ボイラプレート
再び、同じ基本的なボイラープレートから始めます.
function hyphenCaseToCamelCase(text) {
    return text.replace(/-([a-z])/g, g => g[1].toUpperCase());
}

class WcGraphWgpu extends HTMLElement {
    #points = [];
    #colors = [];
    #width = 320;
    #height = 240;
    #xmax = 100;
    #xmin = -100;
    #ymax = 100;
    #ymin = -100;
    #func;
    #step = 1;
    #thickness = 1;
    #continuous = false;

    #defaultSize = 4;
    #defaultColor = [1, 0, 0, 1];

    static observedAttributes = ["points", "func", "step", "width", "height", "xmin", "xmax", "ymin", "ymax", "default-size", "default-color", "continuous", "thickness"];
    constructor() {
        super();
        this.bind(this);
    }
    bind(element) {
        element.attachEvents.bind(element);
    }
    connectedCallback() {
        this.attachShadow({ mode: "open" });
        this.canvas = document.createElement("canvas");
        this.shadowRoot.appendChild(this.canvas);
        this.canvas.height = this.#height;
        this.canvas.width = this.#width;
        this.context = this.canvas.getContext("webgpu");

        this.render();
        this.attachEvents();
    }
    render() {

    }
    attachEvents() {

    }
    attributeChangedCallback(name, oldValue, newValue) {
        this[hyphenCaseToCamelCase(name)] = newValue;
    }
    set points(value) {
        if (typeof (value) === "string") {
            value = JSON.parse(value);
        }

        this.#points = value.map(p => [
            p[0],
            p[1],
            p[2] ?? this.#defaultColor[0],
            p[3] ?? this.#defaultColor[1],
            p[4] ?? this.#defaultColor[2],
            p[5] ?? this.#defaultColor[3]
        ]).flat();

        this.render();
    }
    get points() {
        return this.#vertices;
    }
    set width(value) {
        this.#width = parseFloat(value);
    }
    get width() {
        return this.#width;
    }
    set height(value) {
        this.#height = parseFloat(value);
    }
    get height() {
        return this.#height;
    }
    set xmax(value) {
        this.#xmax = parseFloat(value);
    }
    get xmax() {
        return this.#xmax;
    }
    set xmin(value) {
        this.#xmin = parseFloat(value);
    }
    get xmin() {
        return this.#xmin;
    }
    set ymax(value) {
        this.#ymax = parseFloat(value);
    }
    get ymax() {
        return this.#ymax;
    }
    set ymin(value) {
        this.#ymin = parseFloat(value);
    }
    get ymin() {
        return this.#ymin;
    }
    set func(value) {
        this.#func = new Function(["x"], value);
        this.render();
    }
    set step(value) {
        this.#step = parseFloat(value);
    }
    set defaultSize(value) {
        this.#defaultSize = parseFloat(value);
    }
    set defaultColor(value) {
        if (typeof (value) === "string") {
            this.#defaultColor = JSON.parse(value);
        } else {
            this.#defaultColor = value;
        }
    }
    set continuous(value) {
        this.#continuous = value !== undefined;
    }
    set thickness(value) {
        this.#thickness = parseFloat(value);
    }
}

customElements.define("wc-graph-wgpu", WcGraphWgpu);
文脈タイプがそうであるのを除いて、それはWebGLと同じスケルトンですwebgpu . The points 少し違うです.それが我々がそれらを通過する方法であるので、平らな配列で働くのはずっと簡単です、それで、我々は属性セッターでその仕事のすべてをします.

レンダリングパイプラインの設定

初期設定
まず最初に、レンダリングごとに変更されない初期のものを設定します.
#dom;
#context;
#device;
#vertexBufferDescriptor;

async connectedCallback() {
    this.cacheDom()
    await this.setupGpu();
    this.render();
    this.attachEvents();
}
cacheDom(){
    this.attachShadow({ mode: "open" });
    this.#dom = {};
    this.#dom.canvas = document.createElement("canvas");
    this.shadowRoot.appendChild(this.#dom.canvas);
    this.#dom.canvas.height = this.#height;
    this.#dom.canvas.width = this.#width;
}
async setupGpu() {
    const adapter = await navigator.gpu.requestAdapter();
    this.#device = await adapter.requestDevice();
    this.#context = this.#dom.canvas.getContext("webgpu");
    this.#context.configure({
        device: this.#device,
        format: "bgra8unorm"
    });
    this.#vertexBufferDescriptor = [{
        attributes: [
            {
                shaderLocation: 0,
                offset: 0,
                format: "float32x2"
            },
            {
                shaderLocation: 1,
                offset: 8,
                format: "float32x4"
            }
        ],
        arrayStride: 24,
        stepMode: "vertex"
    }];
}
ここではあまりにも奇妙な何も.私たちはいくつかの基本的なDOMセットアップをしており、WebGPUデバイスをキャンバスと関連づけます.The vertextBufferDescriptor 頂点バッファ形式を設定します.最初の2 float 32 sは場所です、次の4は色です.

パイプライン
#shaderModule;
#renderPipeline;

//call this after `setupGpu` in connectedCallback
async loadShaderPipeline() {
    this.#shaderModule = this.#device.createShaderModule({
        code: `
            <placeholder>
        `
    });
    const pipelineDescriptor = {
        vertex: {
            module: this.#shaderModule,
            entryPoint: "vertex_main",
            buffers: this.#vertexBufferDescriptor
        },
        fragment: {
            module: this.#shaderModule,
            entryPoint: "fragment_main",
            targets: [
                {
                    format: "bgra8unorm"
                }
            ]
        },
        primitive: {
            topology: "point-list"
        }
    };
    this.#renderPipeline = this.#device.createRenderPipeline(pipelineDescriptor);
}
パイプラインは、頂点が画像にどのように作られるかについて説明します.ここで最も興味深いことは、遮光物モジュールを供給する遮光物モジュールです(私たちは常に同じ遮光物を使用しますので、一度だけ行う必要があります).我々は、それのためにそれをスキップするように、それ自身のセクションの遮光物について議論します.次はパイプラインディスクリプタです.最後のステップから遮光物モジュールとvertexbufferdescriptorを使用して、それを構築します.The entrypoint sはまだ存在していない遮光物コード関数名を参照しています.遮光物は、キャンバス構成に合っている「brga 8 unorm」またはRGBカラーを出力する.最後に私たちが使っている原始はpoint-list これは似ている点のリストですが、私たちがgl.POINTS WebGLバージョンで使用します.

頂点マッピング
render(){
  const vertexBuffer = this.#device.createBuffer({
    size: this.#points.length * 4,
    usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
    mappedAtCreation: true
  });
  new Float32Array(vertexBuffer.getMappedRange()).set(this.#points);
  vertexBuffer.unmap();

  //more to come...
}
バッファへのポイントの作成と書き込みを開始します.各要素がfloat 32であるため、バッファの合計サイズは長さ* 4であるため、配列は平坦化されます.
render(){
        //...write to vertex buffer (above)
    const clearColor = { r: 0.0, g: 0.5, b: 1.0, a: 1.0 };
    const renderPassDescriptor = {
        colorAttachments: [
            {
                loadValue: clearColor,
                storeOp: "store",
                view: this.#context.getCurrentTexture().createView()
            }
        ]
    };
        const commandEncoder = this.#device.createCommandEncoder();
    const passEncoder = commandEncoder.beginRenderPass(renderPassDescripto
    passEncoder.setPipeline(this.#renderPipeline);
    passEncoder.setVertexBuffer(0, vertexBuffer);
    passEncoder.draw(this.#points.length / 6);
    passEncoder.endPass();
    this.#device.queue.submit([commandEncoder.finish()]);
}
クリアカラーは、背景色(コーンフロー青)です.パス記述子は、ベースカラーを使用して、キャンバスコンテクストに出力します.次に、描画命令をエンコードするコマンドエンコーダを作成します.我々の頂点バッファを使用して、パスを作成するとき、最後のステップからパイプラインを提出してください.我々は描画しているthis.#points.length / 6 各要素は6つの値を持ち、配列を平坦化したので、要素.最後に、エンコードされた命令をデバイスキューに送信し、それを描画します.

遮光物
この時点で少なくともエラーは発生しませんが、遮光物が無効な原因は何も描画されません.それを直しましょう.
struct VertexOut {
    [[builtin(position)]] position : vec4<f32>;
    [[location(0)]] color : vec4<f32>;
};
[[stage(vertex)]]
fn vertex_main([[location(0)]] position: vec2<f32>, [[location(1)]] color: vec4<f32>) -> VertexOut
{
    var output : VertexOut;
    output.position = vec4<f32>(position, 0.0, 1.0);
    output.color = color;
    return output;
}
[[stage(fragment)]]
fn fragment_main(fragData: VertexOut) -> [[location(0)]] vec4<f32>
{
    return fragData.color;
}
これは基本的に、私たちの頂点バッファ記述子によって定義された値をとるOP - op遮光物です.これは、2 Dポイントを取る4 D(組み込みの位置として4値の要素を取る)にして、フラグメント遮光物によって出力される色を通過します.

パイプラインの試験
今、我々は完全なパイプラインを持っている.テストしましょう.私たちはまだビューポートのスケーリングを扱っていないことを心に留めておいてください.そうすれば私たちは- 1から1の間のポイントを保つでしょう.選んだ[[-0.5,-0.5], [0.5,-0.5], [0.5,0.5], [-0.5,0.5]]
うーんそれはうまくいかない.それともそれとも?ズーム右上:

さて、それはpoint-list 作品が、それは私たちに1ピクセルのポイントを与える.Unlike gl.POINTS 我々は、これがかなり役に立たないように、サイズを変えることさえできません.ああまあ.
連続モードも可能です.
const pipelineDescriptor = {
    vertex: {
        module: this.#shaderModule,
        entryPoint: "vertex_main",
        buffers: this.#vertexBufferDescriptor
    },
    fragment: {
        module: this.#shaderModule,
        entryPoint: "fragment_main",
        targets: [
            {
                format: "bgra8unorm"
            }
        ]
    }
};
if(this.#continuous){
    pipelineDescriptor.primitive = {
        topology: "line-strip",
        stripIndexFormat: "uint16"
    }
} else {
    pipelineDescriptor.primitive = {
        topology: "point-list"
    }
}
それが連続であるならば、我々はAを使うことができますline-strip 代わりにトポロジー.しかし、インデックス形式を提供する必要もあります.私は本当にこれがなぜ必要かわからない.ちょうどあなたがポインタのサイズにカップルのバイトを保存することができますか?しかし、あなたはそれを必要とします、そして、あなたは16または32ビット符号なし整数を選ぶことができます.連続モードでは以下のようになります.

私たちはラインを見ることができます、しかし、再び、我々は彼らの幅を変えることができません.

ビューポートへのスケーリング
再び、我々はWebGLバージョンから逆のLerpを使用できます.WGLSLでこのように表現されています:
fn inverse_lerp(a: f32, b: f32, v: f32) -> f32{
    return (v-a)/(b-a);
}
また、キャンバスの境界を取得する必要があります.そこで、遮光物にデータを保持する構造体を作成する必要があります.
[[block]]
struct Bounds {
    left: f32;
    right: f32;
    top: f32;
    bottom: f32;
};
The [[block]] 結合のために必要です、そして、structの4つの特性はF 32 sです.次に、境界を保持してバインドする実際の変数を作成する必要があります.
[[group(0), binding(0)]] var<uniform> bounds: Bounds;
タイプの変数ですBounds しかし、bindグループ0からの結合を受け入れるように注釈を付けます.The <uniform> 遮光物がそれがユニフォーム(基本的に遮光物プログラムのための束縛定数)であるということを知っている必要があるので、私は推測も必要です.
インrender 構造体をバッファとしてエンコードする必要がある頂点バッファを同じ方法で設定できます.
const boundsBuffer = this.#device.createBuffer({
    size: 16,
    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
        mappedAtCreation: true
});
new Float32Array(boundsBuffer.getMappedRange()).set([this.#xmin, this.#xmax, this.#ymin, this.#ymax]);
boundsBuffer.unmap();
違いは、使用方法のタイプを使用しているUNIFORM . bindgroupsも作成します.
const boundsGroup = this.#device.createBindGroup({
    layout: this.#renderPipeline.getBindGroupLayout(0),
    entries: [
        {
            binding: 0,
            resource: {
                buffer: boundsBuffer,
                offset: 0,
                size: 16
            }
        }
    ]
});
レイアウトインデックスはgroup(0) 注釈、バインドインデックスはbinding(0) 注釈.4つのF 32値をバッファに入れて、structとして取り出す.必要なオフセットはありません、サイズはバッファの正確なサイズです.
パイプラインを使う必要があります.
passEncoder.setBindGroup(0, boundsGroup);
私はこの後直接setVertexBuffer . The 0 レイアウトインデックスです.
最後に遮光体では、スケーリング操作を使用する位置を変更できます.
output.position = vec4<f32>(
    mix(-1.0, 1.0, inverse_lerp(bounds.left, bounds.right,  position[0])),
    mix(-1.0, 1.0, inverse_lerp(bounds.top, bounds.bottom, position[1])),
    0.0, 
    1.0
);
WGSLにはmix glslのように機能します(あまりにも悪いので、彼らは名前を変更する機会を取らなかった)lerp ...).

ガイドの描画
グラフを完了するには、クロスヘアガイドを描画します.私がポイントのセットを作成して、同じ遮光物を使っているWebGLバージョンと同じトリックを使用することができた間、私は私が若干のより多くのWebGPU実行を得るために今回それを綴ると思いました.
これは別の描画パスは、我々はそれ自身の遮光物を与えるよ.私は、このパスのためにすべてを一度にセットしました:
setupGuidePipeline(){
    const shaderModule = this.#device.createShaderModule({
        code: `
            struct VertexOut {
                [[builtin(position)]] position : vec4<f32>;
            };
            [[stage(vertex)]]
            fn vertex_main([[location(0)]] position: vec2<f32>) -> VertexOut
            {
                var output : VertexOut;
                output.position = vec4<f32>(
                    position,
                    0.0, 
                    1.0
                );
                return output;
            }
            [[stage(fragment)]]
            fn fragment_main(fragData: VertexOut) -> [[location(0)]] vec4<f32>
            {
                return vec4<f32>(0.0, 0.0, 0.0, 1.0);
            }
        `
    });
    const vertexBufferDescriptor = [{
        attributes: [
            {
                shaderLocation: 0,
                offset: 0,
                format: "float32x2"
            }
        ],
        arrayStride: 8,
        stepMode: "vertex"
    }];
    const pipelineDescriptor = {
        vertex: {
            module: shaderModule,
            entryPoint: "vertex_main",
            buffers: vertexBufferDescriptor
        },
        fragment: {
            module: shaderModule,
            entryPoint: "fragment_main",
            targets: [
                {
                    format: "bgra8unorm"
                }
            ]
        },
        primitive: {
            topology: "line-list"
        }
    };
    this.#guidePipeline = this.#device.createRenderPipeline(pipelineDescriptor);
    this.#guideVertexBuffer = this.#device.createBuffer({
        size: 32,
        usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
        mappedAtCreation: true
    });
    new Float32Array(this.#guideVertexBuffer.getMappedRange()).set([0.0, -1, 0.0, 1, -1, 0.0, 1, 0.0]);
    this.#guideVertexBuffer.unmap();
}
遮光物コードは驚くべきではありません、それは2 D位置をとって、黒い線を描きます.このような頂点バッファ記述子も単純で、頂点あたり1つの2 D ( 2 XF 32 )ポイントだけです.パイプライン記述子もよく知らなければなりません、それは我々が使用していたものと同じですline-list . ラインリストのような音です.2頂点の各セットは1行です.我々は2行を描画する必要があるので、4つのポイントを持つ頂点バッファを必要とします.これは、画面スペース(1 - 1)の水平線と垂直線の先頭と末尾の座標です.我々はこのパイプライン全体を#guidePipeline あとで表示される#guideVertexBuffer . 我々は、ちょうど我々がすべてのレンダリングのためにこれらの両方を再利用することができるので、これをする必要があります.
レンダリングでは、いくつかのことを追加する必要があります.別のオブジェクトを描画するには、別の呼び出しを行う必要がありますdraw Passencoderについて
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);

//draw guides
passEncoder.setPipeline(this.#guidePipeline);
passEncoder.setVertexBuffer(0, this.#guideVertexBuffer);
passEncoder.draw(4);

//draw points
passEncoder.setPipeline(this.#renderPipeline);
passEncoder.setVertexBuffer(0, vertexBuffer);
passEncoder.setBindGroup(0, boundsGroup);
passEncoder.draw(points.length / 6);

passEncoder.endPass();
それで、我々はただ一つのパス・エンコーダを得て、パイプラインと頂点をセットして、それから、ガイドのためにDrawを呼びます、そして、ポイント(ポイントはまた、境界のためにバインドグループをセットしました)のためにもう一度呼びます.それから、我々は電話しますendPass . 残りは前回と全く同じです.

結論
それで、我々は面白い点で我々を残します.WebGLのように我々はポイントやラインを変更するようなことを行うことができます特定の機能が不足している.実際、Webgpuはポイントサイズをコントロールすることさえできないので、より悪いです.それはさらに冗長であり、ブラウザの書き込み時には安定したチャネルでWebgpuをサポートしています.すべてのすべてのそれはおそらくこれのための大きな選択ではないが、それは比較対照的に興味深いです.しかし、ここで終わろうとするのに少しがっかりしているので、私たちが将来的に線厚と点の形のようなものを得ることができる方法を見たいです.