LoginSignup
3
3

More than 3 years have passed since last update.

GitHub Actions で WebGL2 を動かすまで

Posted at

capture_.png

概要

WebGL2 を含むプログラムを GitHub Actions の Node.js 環境で実行する現実的な選択肢は二つ

  • Headless Chrome を使って localhost 越しに呼びたい式を渡し、評価された結果を受け取る
  • node-gles を使って WebGL の関数呼び出しを OpenGL ES に内部で変換して呼び出す

どちらも面倒な点はありますが、それぞれ動かすところまで試したのでやり方を書いておきます。


対象読者

  • Node.js を使ったことがある
  • WebGL を使ったことがある

環境

  • Node.js
  • TypeScript
  • Jest

リポジトリ

方法1. Headless Chrome を使う

Chrome には画面を立ち上げずにバックグラウンドで動作するヘッドレスモードがあり、プログラムからこれを操作することでテストやキャプチャの撮影などが自動で行えます。

Node.js から Chrome を操作するには次のいずれかのライブラリを使用するのが一般的なようです。

今回はページを開いて式を実行するだけなのでどちらでも大して変わりませんが、より簡単な Puppeteer を使うことにします。

npm install -D puppeteer

実験1.1. WebGL の情報を取得して表示する(Chrome)

まずは WebGL のバージョンや使えるリソースのサイズを取得するプログラムで実験をしてみます。

ソースコード

次の webglSimple() は引数に WebGL2RenderingContext を取るのでこれをどうやって与えるかというのが今回の問題になります。

src/index.ts

export function webglSimple(gl: WebGL2RenderingContext): string
{
    return [
        `------------------------------------------------------------`,
        `gl.RENDERER  | ${gl.getParameter(gl.RENDERER)}`,
        `gl.VERSION   | ${gl.getParameter(gl.VERSION)}`,
        `------------------------------------------------------------`,
        `gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS  | ${gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS)}`,
        `gl.MAX_CUBE_MAP_TEXTURE_SIZE         | ${gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE)}`,
        `gl.MAX_FRAGMENT_UNIFORM_VECTORS      | ${gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS)}`,
        `gl.MAX_TEXTURE_IMAGE_UNITS           | ${gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS)}`,
        `gl.MAX_TEXTURE_SIZE                  | ${gl.getParameter(gl.MAX_TEXTURE_SIZE)}`,
        `gl.MAX_VARYING_VECTORS               | ${gl.getParameter(gl.MAX_VARYING_VECTORS)}`,
        `gl.MAX_VERTEX_ATTRIBS                | ${gl.getParameter(gl.MAX_VERTEX_ATTRIBS)}`,
        `gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS    | ${gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS)}`,
        `gl.MAX_VERTEX_UNIFORM_VECTORS        | ${gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS)}`,
        `------------------------------------------------------------`,
    ].join('\n');
}

この関数を GitHub Actions から呼び出して結果を表示するのがとりあえずの目標です。

ブラウザから使うので HTML も仮で用意しておきます。

public/index.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>webgl2-test</title>
    </head>
    <body>
        <canvas id="gl"></canvas>
        <script src="./index.js"></script>
    </body>
</html>

テストコード

テストでは Puppeteer を使ってブラウザ上で JavaScript を実行します。

次の remoteEval() 関数は puppeteer.launch() で Chrome を立ち上げた後 page.goto() で localhost に接続します。そしてページが読み込まれたら page.evaluate() を呼ぶことでページ上で式を実行してその結果を返します。

test/chrome_helper.ts
const puppeteer = require('puppeteer');

export async function remoteEval(expr: () => any, port: number, headless: boolean, chromeFlags: string[] = [])
{
    return new Promise(async (resolve: (x:any) => void) =>
    {
        const browser = await puppeteer.launch({ headless: headless, args: chromeFlags });
        const page = await browser.newPage();
        await page.goto(`http://localhost:${port}`, { waitUntil: 'domcontentloaded' });

        const result = await page.evaluate(expr);

        await browser.close();
        resolve(result);
    });
}

これを使って src/index.ts で定義した webglSimple() を呼び出すだけのテストを書きます。

次の call_webglSimple() は先ほどの HTML を開いたページ上で実行するため document.querySelector() で canvas を取得することができます。これを通常モードとヘッドレスモードでそれぞれ実行するテストを書きます。

test/chrome_simple.test.ts
import {remoteEval} from "./chrome_helper";

const call_webglSimple = () => eval(`(() =>
{
    const canvas = document.querySelector("canvas");
    const gl = canvas.getContext("webgl2");
    return webglSimple(gl);
})()`);

test("simple (chrome headless)", (async function()
{
    return remoteEval(call_webglSimple, 8080, true).then(
        (result:any) =>
        {
            console.log(result);
            expect(`${result}`).not.toBe("");
        });
}), 60000);

test("simple (chrome browser)", (async function()
{
    return remoteEval(call_webglSimple, 8080, false).then(
        (result:any) =>
        {
            console.log(result);
            expect(`${result}`).not.toBe("");
        });
}), 60000);

GitHub Actions

最後にこのプログラムを実際に実行するワークフローを書きます。run 以外はテンプレをそのまま使いました。ここでは tsc で TypeScript をコンパイルした後 http-server を立ち上げた状態で test を実行しています。

.github/workflows/node.js.yml
#... 略 ...
jobs:
  chrome_simple:
    runs-on: macos-latest
    strategy:
      matrix:
        node-version: [12.x]
    steps:
    - uses: actions/checkout@v2
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}
    - run: |
        npm ci
        npm install -D puppeteer
        npm run tsc
        npm run server ./public -p 8080 &
        npm test -- chrome_simple

実行結果

https://github.com/agehama/webgl2-test/runs/1268400898

simple (chrome headless)
    ------------------------------------------------------------
    gl.RENDERER  | WebKit WebGL
    gl.VERSION   | WebGL 2.0 (OpenGL ES 3.0 Chromium)
    ------------------------------------------------------------
    gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS  | 32
    gl.MAX_CUBE_MAP_TEXTURE_SIZE         | 8192
    gl.MAX_FRAGMENT_UNIFORM_VECTORS      | 261
    gl.MAX_TEXTURE_IMAGE_UNITS           | 16
    gl.MAX_TEXTURE_SIZE                  | 8192
    gl.MAX_VARYING_VECTORS               | 32
    gl.MAX_VERTEX_ATTRIBS                | 32
    gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS    | 16
    gl.MAX_VERTEX_UNIFORM_VECTORS        | 256
    ------------------------------------------------------------

simple (chrome browser)
    ------------------------------------------------------------
    gl.RENDERER  | WebKit WebGL
    gl.VERSION   | WebGL 2.0 (OpenGL ES 3.0 Chromium)
    ------------------------------------------------------------
    gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS  | 80
    gl.MAX_CUBE_MAP_TEXTURE_SIZE         | 16384
    gl.MAX_FRAGMENT_UNIFORM_VECTORS      | 1024
    gl.MAX_TEXTURE_IMAGE_UNITS           | 16
    gl.MAX_TEXTURE_SIZE                  | 16384
    gl.MAX_VARYING_VECTORS               | 32
    gl.MAX_VERTEX_ATTRIBS                | 16
    gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS    | 16
    gl.MAX_VERTEX_UNIFORM_VECTORS        | 1024
    ------------------------------------------------------------

GitHub Actions で動かした出力結果がこちらです。これで WebGL2 が動作することを無事確認できました!

ただし、ここで気になるのはヘッドレスモードのリソースサイズが通常モードより大きく制限されているという点です。このように同じ実行環境でもヘッドレスモードだとグラフィックスの挙動が変わるということは意識しておく必要があります。

実験1.2. Texture3D に MRT を使って描き込む(Chrome)

動作が確認できたので早速 WebGL2 でないと動かないプログラムを試してみましょう。

ソースコード

次の webglTexture3d() は Texture3D に Multiple Render Targets を使って値を描き込み、その結果を gl.readPixels() で読んで配列に詰めて返す関数です。描き込む値はフラグメントシェーダの中で定義しています。

src/index.ts
export function webglTexture3d(gl: WebGL2RenderingContext): Uint8Array[]
{
    const vs = gl.createShader(gl.VERTEX_SHADER) as WebGLShader;
    {
        gl.shaderSource(vs,
`#version 300 es
void main()
{
    vec3[6] vertices = vec3[](
        vec3(-1.0, -1.0, 0),
        vec3(+1.0, -1.0, 0),
        vec3(-1.0, +1.0, 0),
        vec3(-1.0, +1.0, 0),
        vec3(+1.0, -1.0, 0),
        vec3(+1.0, +1.0, 0)
    );
    gl_Position = vec4(vertices[gl_VertexID], 1);
}`
        );
        gl.compileShader(vs);
    }

    const fs = gl.createShader(gl.FRAGMENT_SHADER) as WebGLShader;
    {
        gl.shaderSource(fs, 
`#version 300 es
precision mediump float;
out uvec4 outColor[4];
void main()
{
    uvec2 xy = uvec2(gl_FragCoord.xy);
    outColor[0] = uvec4(xy + uvec2( 0,  0), 0u, 255u);
    outColor[1] = uvec4(xy + uvec2(10, 10), 0u, 255u);
    outColor[2] = uvec4(xy + uvec2(20, 20), 0u, 255u);
    outColor[3] = uvec4(xy + uvec2(30, 30), 0u, 255u);
}`
        );
        gl.compileShader(fs);
    }

    const program = gl.createProgram() as WebGLProgram;
    gl.attachShader(program, vs);
    gl.attachShader(program, fs);
    gl.linkProgram(program);
    gl.useProgram(program);

    const width = 2;
    const height = 2;
    const depth = 4;
    const texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_3D, texture);
    gl.texImage3D(gl.TEXTURE_3D, 0, gl.RGBA8UI, width, height, depth, 0, gl.RGBA_INTEGER, gl.UNSIGNED_BYTE, null);

    const fb = gl.createFramebuffer();
    gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
    for(let z = 0; z < 4; z++)
    {
        gl.framebufferTextureLayer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + z, texture, 0, z);
    }
    gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1, gl.COLOR_ATTACHMENT2, gl.COLOR_ATTACHMENT3]);

    gl.viewport(0, 0, width, height);
    gl.drawArrays(gl.TRIANGLES, 0, 6);

    let results = [];
    const pixels = new Uint8Array(4*width*height);
    for(let z = 0; z < 4; z++)
    {
        gl.readBuffer(gl.COLOR_ATTACHMENT0 + z);
        gl.readPixels(0, 0, width, height, gl.RGBA_INTEGER, gl.UNSIGNED_BYTE, pixels);
        results.push(pixels.slice());
    }

    return results;
}

テストコード

こちらも通常モードとヘッドレスモードでそれぞれ webglTexture3d() を呼び出すテストを書きます。

test/chrome_texture3d.test.ts
import {remoteEval} from "./chrome_helper";

const call_webglTexture3d = () => eval(`(() =>
{
    const canvas = document.querySelector("canvas");
    const gl = canvas.getContext("webgl2");
    return webglTexture3d(gl);
})()`);

test("texture3d (chrome headless)", (async function()
{
    return remoteEval(call_webglTexture3d, 8080, true).then(
        (result:any) =>
        {
            console.log(result);
            expect(`${result}`).not.toBe([]);
        });
}), 60000);

test("texture3d (chrome browser)", (async function()
{
    return remoteEval(call_webglTexture3d, 8080, false).then(
        (result:any) =>
        {
            console.log(result);
            expect(`${result}`).not.toBe([]);
        });
}), 60000);

GitHub Actions

ワークフローも 実験1.1 と同じで、呼び出す test だけ変えます。

.github/workflows/node.js.yml
jobs:
  #... 略 ...
  chrome_texture3d:
    runs-on: macos-latest
    strategy:
      matrix:
        node-version: [12.x]
    steps:
    - uses: actions/checkout@v2
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}
    - run: |
        npm ci
        npm install -D puppeteer
        npm run tsc
        npm run server ./public -p 8080 &
        npm test -- chrome_texture3d

実行結果

https://github.com/agehama/webgl2-test/runs/1268400887

// texture3d (chrome headless)
[
    { '0':   1, '1':   0,  '2':   0,  '3':   0,  '4':   0,  '5':   0,  '6':   0,  '7':   0,
      '8':   0, '9':   0, '10':   0, '11':   0, '12': 112, '13': 114, '14': 101, '15': 115 },
    { '0':  35, '1': 118,  '2': 101,  '3': 114,  '4': 115,  '5': 105,  '6': 111,  '7': 110,
      '8':  32, '9':  51, '10':  48, '11':  48, '12':  32, '13': 101, '14': 115, '15':  10 },
    { '0': 112, '1': 114,  '2': 101,  '3':  99,  '4': 105,  '5': 115,  '6': 105,  '7': 111,
      '8': 110, '9':  32, '10': 109, '11': 101, '12': 100, '13': 105, '14': 117, '15': 109 },
    { '0': 112, '1':  32,  '2': 102,  '3': 108,  '4': 111,  '5':  97,  '6': 116,  '7':  59,
      '8':  10, '9': 111, '10': 117, '11': 116, '12':  32, '13': 117, '14': 118, '15': 101 }
]

// texture3d (chrome browser)
[
    { '0':  0, '1':  0,  '2': 0,  '3': 255,  '4':  1,  '5':  0,  '6': 0,  '7': 255,
      '8':  0, '9':  1, '10': 0, '11': 255, '12':  1, '13':  1, '14': 0, '15': 255 },
    { '0': 10, '1': 10,  '2': 0,  '3': 255,  '4': 11,  '5': 10,  '6': 0,  '7': 255,
      '8': 10, '9': 11, '10': 0, '11': 255, '12': 11, '13': 11, '14': 0, '15': 255 },
    { '0': 20, '1': 20,  '2': 0,  '3': 255,  '4': 21,  '5': 20,  '6': 0,  '7': 255,
      '8': 20, '9': 21, '10': 0, '11': 255, '12': 21, '13': 21, '14': 0, '15': 255 },
    { '0': 30, '1': 30,  '2': 0,  '3': 255,  '4': 31,  '5': 30,  '6': 0,  '7': 255,
      '8': 30, '9': 31, '10': 0, '11': 255, '12': 31, '13': 31, '14': 0, '15': 255 }
]

実行するとなんと二つの出力が全然異なる結果になってしまいました。これはヘッドレスモードの方が間違っており通常モードでは正しい結果を出力できています。ヘッドレスモードの実行結果はランダムっぽい値が入っているときと全て0埋めされているときがあるので、もはや何も描画できていないのかもしれません。

そしてもう一つ気になるのが、関数からは Uint8Array[] 型で結果を返しているのに受け取った結果は JSON に変換されているという点です。この理由はシンプルで、テストを行う Node.js と式を実行する Chrome はそれぞれ別々に JavaScript 実行環境を動かしており、両者の間でデータを受け渡す手段が文字列しかないためです。この変換は Puppeteer が内部で自動的に行っているようで、chrome-remote-interface だと容赦なく undefined が返って来るため事前に JSON.stringify() などに通して文字列にした状態で返す必要があります。

自分の想定では複雑なシェーダについて CPU で同じ計算を行う関数を書いておき、シェーダの計算結果と照らし合わせるテストをしたいと思っていたので途中でこういった変換を挟まざるを得ないというのはちょっと微妙だなあという感想でした。まあテストまで全部 Chrome 上で行ってしまえばこれは問題になりませんが…

いずれにせよ WebGL の単体テストを行うという用途に対しては、この方法だと少し大がかり過ぎてあまり向いていない気がしました。しかしその分 Chrome での動作が保証できるという強みはあるので、厳密なデータ比較まで行わなくてもとりあえず動かすだけ動かしておくというのが良いのかもしれません。

方法2. node-gles を使う

node-gles は WebGL から OpenGL ES へのバインディングを提供するライブラリで、これ自身は内部で ANGLE を呼び出します。同じコンセプトで広く使われている headless-gl から WebGL2 に対応する気配がないため TensorFlow.js の中の人によって新たに開発されています。

しかしながら、2020年10月現在ではまだ WebGL2 の関数バインディングはほとんど入っていません。バインディングが少ない理由については一応説明があり、将来的に Khronos の IDL ファイルから自動生成する予定でとりあえず使うものしか入れていない(https://github.com/google/node-gles/issues/1#issuecomment-435165176) とのことです。ということで今はかなり作り途中のものを無理やり使っていくことになります。

ここで大事なのはバインディングさえ追加すれば WebGL 2.0 ( ES 3.0 ) の関数を呼び出せることで、追加するだけならそれほど難しくはないので現時点でも実用できなくはないということです。また、GitHub Actions の windows-latest と ubuntu-latest では残念ながら動きませんでしたが、macos-latest では動くことが確認できたのでひとまずこれで使ってみることにします。

npm install -D node-gles

実験2.1. WebGL の情報を取得して表示する(node-gles)

使うソースコードは 実験1.1 と同じ webglSimple() 関数なので省略します。ちなみに webglSimple() は現在の node-gles が対応している範囲に収まっている(収めた)ためそのまま実行することができます。

テストコード

nodeGles.createWebGLRenderingContext() で WebGL と同じ API にアクセスできるオブジェクトが返ってくるので、これをそのまま webglSimple() に渡して結果を取得します。

test/node_gles_simple.test.ts
const nodeGles = require('node-gles');
import {webglSimple} from "../src/index";

test("simple (node-gles)", () =>
{
    const gl = nodeGles.createWebGLRenderingContext();
    const result = webglSimple(gl);
    console.log(result);
    expect(result).not.toBe([]);
});

GitHub Actions

ワークフローは 実験1.1 とほぼ同じです。

.github/workflows/node.js.yml
jobs:
  #... 略 ...
  node_gles_simple:
    runs-on: macos-latest
    strategy:
      matrix:
        node-version: [12.x]
    steps:
    - uses: actions/checkout@v2
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}
    - run: |
        npm ci
        npm install -D node-gles
        npm test -- node_gles_simple

実行結果

https://github.com/agehama/webgl2-test/runs/1268400898

simple (node-gles)
    ------------------------------------------------------------
    gl.RENDERER  | ANGLE (Apple Inc., Apple Software Renderer, OpenGL 4.1 core)
    gl.VERSION   | OpenGL ES 3.0 (ANGLE 2.1.0.9512a0ef062a)
    ------------------------------------------------------------
    gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS  | 32
    gl.MAX_CUBE_MAP_TEXTURE_SIZE         | 16384
    gl.MAX_FRAGMENT_UNIFORM_VECTORS      | 1024
    gl.MAX_TEXTURE_IMAGE_UNITS           | 16
    gl.MAX_TEXTURE_SIZE                  | 16384
    gl.MAX_VARYING_VECTORS               | 32
    gl.MAX_VERTEX_ATTRIBS                | 16
    gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS    | 16
    gl.MAX_VERTEX_UNIFORM_VECTORS        | 1024
    ------------------------------------------------------------

node-gles でも無事に動作が確認できました。

実験1.1gl.RENDERER の情報を取得したときは WebKit としか返してくれませんでしたが、今回は ANGLE が中で実際に呼んでいるグラフィックス API の情報を取得できていますね。Software Renderer と書いてあるのが若干気になりますが、まあこれは仮想環境なのでしょうがないのかもしれません。よくわかりませんが。

とりあえず動くことが確認できたので先に進むことにします。

実験2.2. Texture3D に MRT を使って描き込む(node-gles)

次に 実験1.2 で書いた webglTexture3d() をこちらでも動かしてみます。

ここで問題になるのが webglTexture3d() の中でバインディングが足りない関数がいくつかあることです。具体的には texImage3D(), framebufferTextureLayer(), drawBuffers(), readBuffer() とあと他にいくつかの定数が足りていないのでこれらを以下に追加していきます。

バインディングの追加

必要になるのは基本的に次の三つです

  1. egl_context_wrapper.h(.cc) に呼び出したい GLES の関数ポインタを取得してメンバに持っておく
  2. webgl_rendering_context.h(.cc) に WebGL から受け取った引数を取り出して取得した関数ポインタに渡して呼び出す関数を定義する
  3. webgl_rendering_context.cc に 2. で定義した関数に NAPI_DEFINE_METHOD() で名前を付けて Node.js に公開する

あとはnpm install を呼ぶと自動でコンパイルが走るようなので、これで build/Release/nodejs_gl_binding.node が生成されて勝手に使えるようになります。

今回追加した差分はこちらから確認できます。
https://github.com/agehama/node-gles/compare/b4cb488...47d2d00

テストコード

実験2.1 と大体同じですがこちらは nodeGles のパスだけ fork 版を呼ぶために変更しています。

node_gles_texture3d.test.ts
const nodeGles = require('../temp/node-gles/src/index');
import {webglTexture3d} from "../src/index";

test("texture3d (node-gles)", () =>
{
    const gl = nodeGles.createWebGLRenderingContext();
    const result = webglTexture3d(gl);
    console.log(result);
    expect(result).not.toBe([]);
});

GitHub Actions

ワークフローにも fork 版の node-gles を使用するために、npm からではなく git から clone してインストールするという手順を加えています。

.github/workflows/node.js.yml
jobs:
  #... 略 ...
  node_gles_texture3d:
    runs-on: macos-latest
    strategy:
      matrix:
        node-version: [12.x]
    steps:
    - uses: actions/checkout@v2
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}
    - run: |
        cd temp
        git clone https://github.com/agehama/node-gles.git
        cd node-gles
        npm install
        cd ../../
        npm ci
        npm test -- node_gles_texture3d

実行結果

https://github.com/agehama/webgl2-test/runs/1268400898

texture3d (node-gles)
    [
      Uint8Array(16) [
        0, 0, 0, 255, 1, 0, 0, 255,
        0, 1, 0, 255, 1, 1, 0, 255
      ],
      Uint8Array(16) [
        10, 10, 0, 255, 11, 10, 0, 255,
        10, 11, 0, 255, 11, 11, 0, 255
      ],
      Uint8Array(16) [
        20, 20, 0, 255, 21, 20, 0, 255,
        20, 21, 0, 255, 21, 21, 0, 255
      ],
      Uint8Array(16) [
        30, 30, 0, 255, 31, 30, 0, 255,
        30, 31, 0, 255, 31, 31, 0, 255
      ]
    ]

こちらが実行結果です。今度は完全に期待通りの出力が得られました!!嬉しい!

ということでバインディングを追加するという手間さえ無ければ文句なくお勧めできるのですが、現状だと実験的なプロジェクトとかでない限りまだ気軽には使いにくそうだなと感じました。

また、ブラウザの WebGL との挙動の違いについてですが、node-gles だと例えばシェーダが使っていない uniform 変数へ値をセットしようとするとセグフォで落ちてしまいます。ブラウザだと仮に不正な入力があってもそのまま投げて落ちないよう入念にチェックしていると思うのでそこは大きく異なる点だと思います。グラフィックス部分に限れば ANGLE に投げているだけなのでブラウザと描画結果が変わるみたいなことはあまり起きにくいんじゃないかなあと思います。

結論

しばらくは node-gles の成長を待ちつつ使いたい関数を自分で追加していくのが良いかなと思いました。それはそれとして Puppeteer でページを表示するだけなら簡単だったので headless: false でとりあえず動かしてキャプチャでも撮っておくのが良さそうでした。

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3