FlutterのPlatformView (Android)

2024年は8月くらいまではずっとFlutter書いてました。その中で、AndroidのViewをFlutterのUIに組み込むPlatform Viewを使ったのですが、これどうやって画面に描画されてるのだろう?という疑問がありました。

docs.flutter.dev

Platform Viewについて調べると、自分の前提知識が足りないせいで「なんか分かった気になってるだけな気がするんだよなぁ」という気持ちになってました。

というわけで、今回はFlutterのPlatformViewを使うと何が起きてるのかを調べてみた日記です。(Andoirdだけです、iOSには触れません)

前回の日記(AndroidでOpenGL ES - bati11 の 日記)の内容は、今回につながっています。

Platform Viewを使ってない場合

まずは、Platform View関係なくFlutterアプリがどういう仕組みになっているか。

この日記で特に書くことはなく、Flutterの公式ページを読む。

docs.flutter.dev

以下のような図がある。

レイアウトして描画するのはEngineの仕事。上記の公式ページの「Rendering and layout」に書いてある。詳しくは追加で以下のスライドを読む、講演を聴く。とても勉強になりました。

プラットフォーム固有の描画先を用意するのはEmbbederの仕事。上記の公式ページの「Platform embedding」に書いてある。

Androidの場合について以下のように書いてある。FlutterViewが重要。

On Android, Flutter is, by default, loaded into the embedder as an Activity. The view is controlled by a FlutterView, which renders Flutter content either as a view or a texture, depending on the composition and z-ordering requirements of the Flutter content.

Flutterのバージョン3.24.5時点でのFlutterViewの実装では、FlutterViewFrameLayoutを継承していて、FlutterSurfaceViewというSurfaceViewを継承したviewをFlutterView自身にaddView()している。

FlutterのWidgetツリー、というよりRenderObjectツリーは、Engineによってレイアウトされ、FlutterSurfaceViewSurfaceに対して描画されるのだろう。

Platform Viewを使ってる場合

WidgetツリーにAndroidViewがある場合、FlutterのWidgetとは別世界の、AndroidのViewツリーについてもアプリのUIとして描画されるわけだけど、これはどういう仕組みになっているのかな。

flutter/docs/platforms/android/Android-Platform-Views.md at 3.24.5 · flutter/flutter · GitHub

AndroidにおいてPlatform Viewには3つのモードがある。

  • VD (Virtual Display)
  • HC (Hybrid Composition)
  • TLHC (Texture Layer Hybrid Composition)

実際のソースコードを見ていくことにする。Flutterのバージョンは3.24.5です。

Flutterアプリに組み込みたいAndroidのViewは、io.flutter.plugin.platform.PlatformViewというインタフェースを実装する必要がある。FlutterのWidgetツリーにはAndroidViewを書く(TLHCの場合)。

ここら辺の記述の仕方は公式ページの通り実装すればいい。

AndroidViewの実装を読むとMethodChannelJavaのコードが実行されることが分かる。ここで利用するMethodChannelSystemChannelsというクラスがあって SystemChannels.platform_views で用意されてる。Platform ViewだけでなくFlutterフレームワークが他の用途でJavaとやりとりするためのMethodChannelがいくつか用意されてるのが分かる。今回は 'flutter/platform_views' に注目。

  static const MethodChannel platform_views = MethodChannel(
    'flutter/platform_views',
  );

https://github.com/flutter/flutter/blob/3.24.5/packages/flutter/lib/src/services/system_channels.dart#L381-L383

_AndroidViewControllerInternals#sendCreateMessage()MethodChannelcreateをコールしている。

return SystemChannels.platform_views.invokeMethod<dynamic>('create', args);

https://github.com/flutter/flutter/blob/3.24.5/packages/flutter/lib/src/services/platform_views.dart#L1240

ここから先はJavaで書かれたコードの話。

MethodChannelのhandlerを登録しているのはPlatformViewsChannel.javaのここ↓

public PlatformViewsChannel(@NonNull DartExecutor dartExecutor) {
  channel =
      new MethodChannel(dartExecutor, "flutter/platform_views", StandardMethodCodec.INSTANCE);
  channel.setMethodCallHandler(parsingHandler);
}

https://github.com/flutter/engine/blob/3.24.5/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java#L267-L271

そして、上記の parsingHandlercreate コールバックが実行され、 PlatformViewsHandler#createForTextureLayer() が呼ばれる。このメソッドが重要そう。

PlatformViewsHandler#createForTextureLayer() に分岐がある。ドキュメント等を読んでいる感じ、AndroidのPlatform Viewは VD→TC→TLHC と改善されてきている模様。ソースコードでもTLHCが使えるなら使う、使えない場合はVDもしくはHCにフォールバックするようになってる。

// Fall back to Hybrid Composition or Virtual Display when necessary, depending on which
// fallback mode is requested.
if (!supportsTextureLayerMode) {
  if (request.displayMode
      == PlatformViewsChannel.PlatformViewCreationRequest.RequestedDisplayMode
          .TEXTURE_WITH_HYBRID_FALLBACK) {
    configureForHybridComposition(platformView, request);
    return PlatformViewsChannel.PlatformViewsHandler.NON_TEXTURE_FALLBACK;
  } else if (!usesSoftwareRendering) { // Virtual Display doesn't support software mode.
    return configureForVirtualDisplay(platformView, request);
  }
  // TODO(stuartmorgan): Consider throwing a specific exception here as a breaking change.
  // For now, preserve the 3.0 behavior of falling through to Texture Layer mode even
  // though it won't work correctly.
}
return configureForTextureLayerComposition(platformView, request);

https://github.com/flutter/engine/blob/3.24.5/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java#L217-L232

3つのモードがあると言いつつ、公式のPlatformViewのページを見ると、HCとTLHCの説明しか書いてない。VDの弱点はTLHCで補うことができるからVDはもうほとんど場合で不要、ということかな。

推奨されるTLHCを見ていく。

TLHC (Texture Layer Hybrid Composition)

flutter/docs/platforms/android/Texture-Layer-Hybrid-Composition.md at 3.24.5 · flutter/flutter · GitHub

さっきのフォールバックの分岐処理の部分で、フォールバックせずに、TLHCが選ばれたら configureForTextureLayerComposition()を呼ぶ。そこから先はざっと以下のような流れ。

  1. まず、PlatformViewRenderTargetオブジェクトをmakePlatformViewRenderTarget()で生成する
  2. 次に、PlatformViewWrapperを生成する。このとき、コンストラクタの引数に上で生成したPlatformViewRenderTargetを指定する
    • PlatformViewWrapperAndroidFrameLayoutのサブクラスである
  3. PlatformViewWrapperのサイズや配置場所を決める
  4. Platform Viewとして埋め込みたい自分で作ったAndroidのView(PlatformViewを継承して実装する)をPlatformViewWrapperの子ツリーにする
    • final View embeddedView = platformView.getView();
      ...
      viewWrapper.addView(embeddedView);
      
  5. FlutterViewPlatformViewWapperを追加する

ここまででAndroidアプリのViewツリーは以下のように、FlutterViewの子にPlatformViewWrapperが、PlatformViewWrapperの子に自分で作ったAndroidのView(Flutterアプリに組み込みたいAndroidのViewツリー)がぶら下がる。

さて、FlutterとしてのWidgetツリーの方ではAndroidViewの部分に Texture が差し込まれる。

前回の記事を思い返すと、AndroidSurfaceにはプロデューサーとコンシューマーがいると書いた。Java側で生成したPlatformViewRenderTargetが保持するSurfaceがプロデューサーとなりバッファに描画をして、Flutter側のTextureがコンシューマーとなりバッファを読み取る、という形で連携する。

AndroidアプリのViewツリーの描画先はアプリのSurfaceViewになるはずだが、どうやって描画先をPlatformViewRenderTargetが保持するSurfaceにしているのか。

PlatformViewWrapperの実装を見ると分かる。Androidアプリでは、Viewツリーのルートから順に各Viewのdraw(canvas)を辿り、各Viewが引数のcanvas|に対して自分自身を描画することで、結果的にアプリ全体を描画する。PlatformViewWrapperdraw(canvas)をoverrideして引数のcanvasではなく、自分自身が保持するSurfaceからcanvas`を取得して、そこに描画するように差し替える。

final Canvas targetCanvas = targetSurface.lockHardwareCanvas();
...
try {
  // Fill the render target with transparent pixels. This is needed for platform views that
  // expect a transparent background.
  targetCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
  // Override the canvas that this subtree of views will use to draw.
  super.draw(targetCanvas);
} finally {
  renderTarget.scheduleFrame();
  targetSurface.unlockCanvasAndPost(targetCanvas);
}

https://github.com/flutter/engine/blob/3.24.5/shell/platform/android/io/flutter/plugin/platform/PlatformViewWrapper.java#L176-L185

このようにPlatformViewWrapperAndroidアプリとしてのcanvasではなく、自分自身が保持するcanvas、つまりSurfaceに描画する。このSurfaceのコンシューマーがFlutterのTexutureということ。

Androidアプリとしては、PlatformViewWrapper部分のViewツリーについて、アプリのSurfaceに描画はしないがViewツリーとしては保持する、という状態になる。AndroidアプリはViewツリーとしては保持しているため、VDにおけるテキスト入力やアクセシビリティの問題を回避できる。なるほどなー。

VD (Virtual Display)

flutter/docs/platforms/android/Virtual-Display.md at 3.24.5 · flutter/flutter · GitHub

TLHCが使えない場合のフォールバックとしてVDを使う場合もPlatformViewRenderTargetオブジェクトをmakePlatformViewRenderTarget()で生成するのは同じ。

PlatformViewRenderTargetが保持するSurfaceを、AndroidフレームワークDisplayManager#createVirtualDisplay() の引数に指定して android.hardware.display.VirtualDisplay を手にいれる。PlatformViewVirtualDisplayに対して描画すると、SurfaceのコンシューマーであるFlutter WidgetツリーのTextureで利用される。

描画はTLHCと同じような流れだけど、AndroidアプリとしてのViewツリーにPlatformViewが含まれないことが大きな違い。これがVDの問題の原因となっている。

HC (Hybrid Composition)

flutter/docs/platforms/Hybrid-Composition.md at 3.24.5 · flutter/flutter · GitHub

HCはスタミナ切れでコード読んでない。ドキュメント読んでる感じだとTLHCやVDと全然違うみたい。公式PlatformViewのページの文章にはこう書いてある。

Platform Views are rendered as they are normally. Flutter content is rendered into a texture. SurfaceFlinger composes the Flutter content and the platform views.

Android platform-views | Flutter

SurfaceFlingerによって合成されると書いてある。Androidアプリとしては複数のSurfaceを持っている状態でそれぞれ描画されて、最終的にAndroidシステムサービスのSurfaceFlingerによって合成される。

HC個別のmarkdownより、Platform全般のmarkdownの方が説明がわかりやすい。

flutter/docs/platforms/android/Android-Platform-Views.md at 3.24.5 · flutter/flutter · GitHub

The Flutter widget tree is divided into two different Android Views, one below the platform view and one above it.

FlutterのWidgetが描画されるSurfacePlatformViewの下と上に分割されると。これらがSurfaceFlingerで合成されるのだろう。PlatformViewなしの場合とだいぶ違うし、パフォーマンスへの影響が大きい可能性があると。

以下のスライド見るとイメージしやすいです。

Androidで不安定なPlatform Viewsとの闘い - Speaker Deck

おしまい

最初よりはだいぶ分かった気がするぞー。

AndroidでOpenGL ES

前回WebGLアルファブレンディングをやってみました。WebGLcanvasタグを書いておけばそこにシェーダーで描画できました。

WebGLはとても簡単に動かすことができました。以前にAndroidOpenGL ESの入門者向けの本を読んでとても勉強になったのですが、本が古いこともあって動かすのが大変でした。。その時のことのメモです。

いつも以上に自分向けの日記になってます・・・!

書いたソースコードはここ。MyOpenGlEsActivityでSurfaceViewを用意して、Java/Kotlin側でUIスレッドとは別のRenderThreadからJNIでC/C++のコードを呼び、C/C++側でOpenGL ESを使ってシェーダーを使うという流れです。

github.com

例えばこんな感じ。SurfaceView上で画像がグルグル動く。

EGL

vertexシェーダーとfragmentシェーダーといったシェーディング言語(GLSL ES)を含むOpenGL ESでは、「描画対象が何か」については決められていない。

前回触ったWebGLではcanvasタグが描画対象でありcanvasタグの外側のことは何も知らない。画面全体のどこにcanvasが置かれるのかも知らない。GLSLでは座標の (0, 0)canvasタグの領域の左下になり外側のことは知らない。

WebGLではcanvasタグが描画先だが、Androidアプリの場合は描画先はどうなるのか?

Open GL ES と描画先となる何かを繋ぐ役割を担うのが EGL 。EGLの役目は、AndroidLinuxなどプラットフォーム依存となる描画先との繋ぎ込み抽象化する。おかげでOpenGL ES自体はプラットフォーム固有の事情を知らなくていい。

WebGLでは以下のように書くだけでcanvasにシェーダーで描画するための gl オブジェクトを簡単に取得できた。

// WebGL

const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl');

この部分でEGLを利用することでAndroidプラットフォーム特有の描画先と繋ぎこむことになる。

EGLContextとEGLSurface

AndroidのEGLについて以下のドキュメントを読む。

EGLSurfaces and OpenGL ES  |  Android Open Source Project

Before you draw with GLES, you need to create a GL context. In EGL, this means creating an EGLContext and an EGLSurface.

GL contextがまず必要。WebGLでは const gl = canvas.getContext('webgl'); で取得できたが、EGLでは EGLContextとEGLSurfaceが必要みたい。これらのオブジェクトの生成については「マルチプラットフォ-ムのためのOpenGL ES入門: Android/iOS対応グラフィックスプログラミング (応用編)」にも書いてある。

EGLContextは EGL10#eglCreateContext() で生成できる。( ※ 新しいバージョンのAPIがあるが書籍に従っておく。以降のEGL関連のAPIも同様。新しいAPIも少し見たけどこの日記の範囲ではあまり変わらない)

EGLSurfaceは EGL10#eglCreateWindowSurface() で生成できる。このメソッドの第3引数 native_window が重要。AOSPの実装を見ると以下のようになっていて、第3引数の型が SurfaceView, SurfaceHolder, Surface の場合とSurfaceTexture の場合とで処理が異なってる。

public EGLSurface eglCreateWindowSurface(EGLDisplay display, EGLConfig config, Object native_window, int[] attrib_list) {
    Surface sur = null;
    if (native_window instanceof SurfaceView) {
        SurfaceView surfaceView = (SurfaceView)native_window;
        sur = surfaceView.getHolder().getSurface();
    } else if (native_window instanceof SurfaceHolder) {
        SurfaceHolder holder = (SurfaceHolder)native_window;
        sur = holder.getSurface();
    } else if (native_window instanceof Surface) {
        sur = (Surface) native_window;
    }
    
    long eglSurfaceId;
    if (sur != null) {
        eglSurfaceId = _eglCreateWindowSurface(display, config, sur, attrib_list);
    } else if (native_window instanceof SurfaceTexture) {
        eglSurfaceId = _eglCreateWindowSurfaceTexture(display, config,
                native_window, attrib_list);
    } else {
        throw new java.lang.UnsupportedOperationException(
            "eglCreateWindowSurface() can only be called with an instance of " +
            "Surface, SurfaceView, SurfaceHolder or SurfaceTexture at the moment.");
    }

SurfaceView と SurfaceTexture の違いは一旦気にしないでおく。

SurfaceViewの場合、以下の流れで EGLSurfaceが手に入る。

  • Activityなどで SurfaceView を生成する
  • surfaceView.getHolder().addCallback() でコールバックを登録できるので、コールバックのインタフェース SurfaceHolder.Callback を実装してaddCallback()に渡す
  • surfaceChange(SurfaceHolder holder, int format, int width, int height) がコールバックされる。引数の holder を使って EGL10#eglCreateWindowSurface() を呼ぶ

これでEGLContextとEGLSurfaceが手に入った!SurfaceViewがWebGLで言うところのcanvasに相当するのだろう。

スレッド

しかし、まだダメ。EGLContextとEGLSurfaceをスレッドと関連づける必要がある。

前回WebGLgl#clearColor()gl#drawArrays() などを呼び出していた。

// WebGL

const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl');
...
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);

Androidでも同じようなAPIが用意されていて利用するわけだけど、OpenGL ES ではこれらのメソッドを実行するスレッドが重要。スレッドと関連づくEGLContextとEGLSurfaceに対して処理を実行するようになってる。1つのスレッドにEGLContextとEGLSurfaceが関連づけて、他のスレッドからは操作できないようになってる。スレッドとの関連付けは EGL10#eglMakeCurrent() にEGLSurfaceとEGLContextを渡す。

これで準備OK!

おさらいすると、

  • EGLContextとEGLSurfaceを用意する
    • EGLSurfaceのインスタンスを取得するときには、SurfaceViewから手に入るSurfaceHolderが必要
  • EGL10#eglMakeCurrent() を呼ぶことで現在のスレッドとEGLContext,EGLSurfaceを関連づける
  • 以降、同じスレッドでOpenGL ESのAPIを呼ぶと、結果はSurfaceHolderが保持するSurfaceに描画される

マルチプラットフォ-ムのためのOpenGL ES入門: Android/iOS対応グラフィックスプログラミング (応用編)」では、JNIをC/C++の関数を呼び出して、C/C++OpenGL ESのAPIを利用する構成になってた。JNI経由でC/C++の処理を実行すると同じスレッドになるのでOpenGL ESの呼び出しがうまくいく、ということだと思う。

Surface

SurfaceViewがWebGLで言うところのcanvasに相当するのだろう、ってさっき書いた。もうちょい踏み込んでみる。

下のページを読む。

Surface and SurfaceHolder  |  Android Open Source Project

A surface represents a block of memory that gets composited to the screen.

Surfaceはメモリブロック。

A surface view is a View object that wraps a Surface object for drawing,

SurfaceViewは、Surfaceを保持するVIew。

The surface represents the producer side of a buffer queue that is often consumed by SurfaceFlinger.

Surfaceはバッファキューのプロデューサー、SurfaceFlingerがバッファキューのコンシューマー。SurfaceFlingerというのはAndroidのOSが起動しているシステムサービス。SurfaceFlingerが複数のバッファキューの内容を画面に出力する。

https://source.android.com/static/docs/core/graphics/images/graphics-pipeline.png

https://source.android.com/docs/core/graphics#data-flow

SurfaceFlingerが管理しているバッファキューに対して、AndroidアプリからはSurface経由で描画できるようになっている。というわけで、EGLSurface(が保持するSurface)に対してOpenGL ES で描画すると、SurfaceFlingerが画面に出力する処理をしてくれる。

App developers draw images to the screen in three ways: with Canvas, OpenGL ES, or Vulkan.

という記述がある通り、OpenGL ESを明示的に使わずにwidgetでUIを構築する場合も同じ(widgetの場合はSkiaというOpen GLをラップしたライブラリ使ってCanvasに描く)。

ということだと思う。

ここら辺はさらにドキュメントを読み漁ったり

以下の記事もとても勉強になりました。

Androidを支える技術 Ⅰ」を読むと詳しく書いてあり勉強になります。

実際にAndroid端末でSurfaceを調べてみる

$ adb shell dumpsys SurfaceFlinger とコマンド実行すると、出力の最初の方に * Layer ... といくつかある。手元にあるPixel3で自分のアプリを起動した状態で、grepしつつ出力するとこうなった。

$ adb -s 8BUX1FAZ3 shell dumpsys SurfaceFlinger | grep "* Layer "
* Layer 0x72a5f7fd40 (info.bati11.android.otameshi/info.bati11.android.otameshi.MainActivity#0)
* Layer 0x72a5f57f60 (StatusBar#0)
* Layer 0x72a5f551d0 (NavigationBar0#0)
* Layer 0x72a5f412e0 (ScreenDecorOverlay#0)
* Layer 0x72a5f3e550 (ScreenDecorOverlayBottom#0)

複数のレイヤーというものがあることが分かる。 geomBufferSizegrepの対象に追加する。

$ adb shell dumpsys SurfaceFlinger | grep -e "* Layer " -e "geomBufferSize"
* Layer 0x72a603bc60 (info.bati11.android.otameshi/info.bati11.android.otameshi.MainActivity#0)
      geomBufferSize=[0 0 1080 2160] geomContentCrop=[0 0 1080 2160] geomCrop=[0 0 -1 -1] geomBufferTransform=0 
* Layer 0x72a5f57f60 (StatusBar#0)
      geomBufferSize=[0 0 1080 77] geomContentCrop=[0 0 1080 77] geomCrop=[0 0 -1 -1] geomBufferTransform=0 
* Layer 0x72a5f551d0 (NavigationBar0#0)
      geomBufferSize=[0 0 1080 132] geomContentCrop=[0 0 1080 132] geomCrop=[0 0 -1 -1] geomBufferTransform=0 
* Layer 0x72a5f412e0 (ScreenDecorOverlay#0)
      geomBufferSize=[0 0 1080 78] geomContentCrop=[0 0 1080 78] geomCrop=[0 0 -1 -1] geomBufferTransform=0 
* Layer 0x72a5f3e550 (ScreenDecorOverlayBottom#0)
      geomBufferSize=[0 0 1080 78] geomContentCrop=[0 0 1080 78] geomCrop=[0 0 -1 -1] geomBufferTransform=0

geomBufferSizeがレイヤーのピクセル数かな?ちなみに、端末のピクセル数は以下の通り。

$ adb shell wm size                                                        
Physical size: 1080x2160

レイヤーについてドキュメントのSurfaceFlingerのページで書かれている。

SurfaceFlinger and WindowManager  |  Android Open Source Project

When an app comes to the foreground, it requests buffers from WindowManager. WindowManager then requests a layer from SurfaceFlinger. A layer is a combination of a surface, which contains the BufferQueue, and a SurfaceControl, which contains the layer metadata like the display frame. SurfaceFlinger creates the layer and sends it to WindowManager.

アプリがforegroundになるとWindowManagerへバッファキューの作成を依頼、WindowManagerはレイヤーをSurfaceFlingerへ依頼する。レイヤーはSurfaceとSurfaceControlの組み合わせで、WindowManagerへ送られる。

レイヤーのページもある。

Layers and displays  |  Android Open Source Project

どこに配置されるかやZ-orderを持っている。

つまり、Surface自身は描画自体のメモリ(バッファキュー)への参照で、レイヤーはSurface自体に加えてSurfaceへの描画が画面上のどこに配置されるかの情報を持ってる。

もうちょっと $ adb shell dumpsys SurfaceFlinger の出力を見てみる。少し下の方に行くと以下のように Displays (1 entries) 出力がある。

Displays (1 entries)
+ DisplayDevice{0, internal, primary, "Internal display"}

...

   5 Layers
  - Output Layer 0x7c2cb83680(info.bati11.android.otameshi/info.bati11.android.otameshi.MainActivity#0)
        Region visibleRegion (this=0x7c2cb83698, count=1)
    [  0,   0, 1080, 2160]
        Region visibleNonTransparentRegion (this=0x7c2cb83700, count=1)
    [  0,   0, 1080, 2160]
        Region coveredRegion (this=0x7c2cb83768, count=2)
    [  0,   0, 1080,  78]
    [  0, 2028, 1080, 2160]
        Region output visibleRegion (this=0x7c2cb837d0, count=1)
    [  0,   0, 1080, 2160]
        Region shadowRegion (this=0x7c2cb83838, count=1)
    [  0,   0,   0,   0]
      forceClientComposition=false clearClientTarget=false displayFrame=[0 0 1080 2160] sourceCrop=[0.000000 0.000000 1080.000000 2160.000000] bufferTransform=0 (0) dataspace=UNKNOWN (0) override buffer=0x0 override acquire fence=0x0 override display frame=[0 0 -1 -1] override dataspace=UNKNOWN (0) override display space=ProjectionSpace(bounds = Rect(0, 0, -1, -1), content = Rect(0, 0, -1, -1), orientation = ROTATION_0) override damage region=  Region  (this=0x7c2cb83920, count=1)
    [  0,   0,  -1,  -1]
 override visible region=  Region  (this=0x7c2cb83988, count=1)
    [  0,   0,   0,   0]
 override peekThroughLayer=0x0 override disableBackgroundBlur=false 
      hwc: layer=0x0815 composition=DEVICE (2) 
  - Output Layer 0x7c2cb815c0(StatusBar#0)
        Region visibleRegion (this=0x7c2cb815d8, count=1)
    [  0,   0, 1080,  77]
        Region visibleNonTransparentRegion (this=0x7c2cb81640, count=1)
    [  0,   0, 1080,  77]
        Region coveredRegion (this=0x7c2cb816a8, count=1)
    [  0,   0, 1080,  77]
        Region output visibleRegion (this=0x7c2cb81710, count=1)
    [  0,   0, 1080,  77]
        Region shadowRegion (this=0x7c2cb81778, count=1)
    [  0,   0,   0,   0]
      forceClientComposition=false clearClientTarget=true displayFrame=[0 0 1080 77] sourceCrop=[0.000000 0.000000 1080.000000 77.000000] bufferTransform=0 (0) dataspace=V0_SRGB (142671872) override buffer=0x0 override acquire fence=0x0 override display frame=[0 0 -1 -1] override dataspace=UNKNOWN (0) override display space=ProjectionSpace(bounds = Rect(0, 0, -1, -1), content = Rect(0, 0, -1, -1), orientation = ROTATION_0) override damage region=  Region  (this=0x7c2cb81860, count=1)
    [  0,   0,  -1,  -1]
 override visible region=  Region  (this=0x7c2cb818c8, count=1)
    [  0,   0,   0,   0]
 override peekThroughLayer=0x0 override disableBackgroundBlur=false 
      hwc: layer=0x086 composition=DEVICE (2) 
  - Output Layer 0x7c2cb8fb00(NavigationBar0#0)
        Region visibleRegion (this=0x7c2cb8fb18, count=1)
    [  0, 2028, 1080, 2160]
        Region visibleNonTransparentRegion (this=0x7c2cb8fb80, count=1)
    [  0, 2028, 1080, 2160]
        Region coveredRegion (this=0x7c2cb8fbe8, count=1)
    [  0, 2082, 1080, 2160]
        Region output visibleRegion (this=0x7c2cb8fc50, count=1)
    [  0, 2028, 1080, 2160]
        Region shadowRegion (this=0x7c2cb8fcb8, count=1)
    [  0,   0,   0,   0]
      forceClientComposition=false clearClientTarget=true displayFrame=[0 2028 1080 2160] sourceCrop=[0.000000 0.000000 1080.000000 132.000000] bufferTransform=0 (0) dataspace=V0_SRGB (142671872) override buffer=0x0 override acquire fence=0x0 override display frame=[0 0 -1 -1] override dataspace=UNKNOWN (0) override display space=ProjectionSpace(bounds = Rect(0, 0, -1, -1), content = Rect(0, 0, -1, -1), orientation = ROTATION_0) override damage region=  Region  (this=0x7c2cb8fda0, count=1)
    [  0,   0,  -1,  -1]
 override visible region=  Region  (this=0x7c2cb8fe08, count=1)
    [  0,   0,   0,   0]
 override peekThroughLayer=0x0 override disableBackgroundBlur=false 
      hwc: layer=0x085 composition=DEVICE (2) 
  - Output Layer 0x7c2cb88030(ScreenDecorOverlay#0)
        Region visibleRegion (this=0x7c2cb88048, count=1)
    [  0,   0, 1080,  78]
        Region visibleNonTransparentRegion (this=0x7c2cb880b0, count=1)
    [  0,   0, 1080,  78]
        Region coveredRegion (this=0x7c2cb88118, count=1)
    [  0,   0,   0,   0]
        Region output visibleRegion (this=0x7c2cb88180, count=1)
    [  0,   0, 1080,  78]
        Region shadowRegion (this=0x7c2cb881e8, count=1)
    [  0,   0,   0,   0]
      forceClientComposition=false clearClientTarget=true displayFrame=[0 0 1080 78] sourceCrop=[0.000000 0.000000 1080.000000 78.000000] bufferTransform=0 (0) dataspace=V0_SRGB (142671872) override buffer=0x0 override acquire fence=0x0 override display frame=[0 0 -1 -1] override dataspace=UNKNOWN (0) override display space=ProjectionSpace(bounds = Rect(0, 0, -1, -1), content = Rect(0, 0, -1, -1), orientation = ROTATION_0) override damage region=  Region  (this=0x7c2cb882d0, count=1)
    [  0,   0,  -1,  -1]
 override visible region=  Region  (this=0x7c2cb88338, count=1)
    [  0,   0,   0,   0]
 override peekThroughLayer=0x0 override disableBackgroundBlur=false 
      hwc: layer=0x084 composition=DEVICE (2) 
  - Output Layer 0x7c2cb8b150(ScreenDecorOverlayBottom#0)
        Region visibleRegion (this=0x7c2cb8b168, count=1)
    [  0, 2082, 1080, 2160]
        Region visibleNonTransparentRegion (this=0x7c2cb8b1d0, count=1)
    [  0, 2082, 1080, 2160]
        Region coveredRegion (this=0x7c2cb8b238, count=1)
    [  0,   0,   0,   0]
        Region output visibleRegion (this=0x7c2cb8b2a0, count=1)
    [  0, 2082, 1080, 2160]
        Region shadowRegion (this=0x7c2cb8b308, count=1)
    [  0,   0,   0,   0]
      forceClientComposition=false clearClientTarget=true displayFrame=[0 2082 1080 2160] sourceCrop=[0.000000 0.000000 1080.000000 78.000000] bufferTransform=0 (0) dataspace=V0_SRGB (142671872) override buffer=0x0 override acquire fence=0x0 override display frame=[0 0 -1 -1] override dataspace=UNKNOWN (0) override display space=ProjectionSpace(bounds = Rect(0, 0, -1, -1), content = Rect(0, 0, -1, -1), orientation = ROTATION_0) override damage region=  Region  (this=0x7c2cb8b3f0, count=1)
    [  0,   0,  -1,  -1]
 override visible region=  Region  (this=0x7c2cb8b458, count=1)
    [  0,   0,   0,   0]
 override peekThroughLayer=0x0 override disableBackgroundBlur=false 
      hwc: layer=0x083 composition=DEVICE (2) 

色々書いてあるけど、雰囲気で読むと「1つ目のOutput Layer はMainActivity で、visibleRegion(表示領域)が (0,0)~(1080,2160)、coveredRegion(他のレイヤーと重なってる領域)が (0,0)~(1080,78)(0,2028)~(1080,2160) 」ということだろうか?(ソースコード読んで確認したのではなく、ただの予想)

2つ目と3つ目のOutput LayerはそれぞれStatusBarとNavigationBarと名前がついてるところから、画面上部と下部のエリアで、これがアプリ(MainActivity)のレイヤーと重なっている。

スクショに赤い線で分かるようにするとこんな感じ。

ScreenDecorOverlayとScreenDecorOverlayBottomはなんだろう?通知のUI出すためのスワイプのイベントやジェスチャーナビゲーションのイベント検知するための領域かなぁ?

自前アプリ内でSurfaceViewを使ってOpen GL ESで描画してみるとどうなるやってみる。アプリ内でOpen GL ES を使う場合は、SurfaceViewを用意してSurfaceを保持するSurfaceHolderを取得、SurfaceHolderを使ってEGLSurfaceを生成しOpenGL ESで描画する、というのが流れ。書いたコードはこちら↓。

otameshi-android/feature/myopengles/src/main/cpp/gl/sample5.c at main · kariyayo/otameshi-android · GitHub

動かすとこんな感じ。SurfaceView上で画像がグルグル動く。

$ adb shell dumpsys SurfaceFlinger してみる。おー! 5 Layers だったのが 6 Layers になってる。一番上に Output Layer 0x7c2cb8e270(SurfaceView というのが増えてるー。 自分で追加したSurfaceViewだ!

Displays (1 entries)
+ DisplayDevice{0, internal, primary, "Internal display"}

...

   6 Layers
  - Output Layer 0x7c2cb8e270(SurfaceView[info.bati11.android.otameshi/info.bati11.opengles.myopengles.glapp.MyOpenGlEsActivity](BLAST)#0)
        Region visibleRegion (this=0x7c2cb8e288, count=1)
    [  0,  77, 1080, 2028]
        Region visibleNonTransparentRegion (this=0x7c2cb8e2f0, count=1)
    [  0,  77, 1080, 2028]
        Region coveredRegion (this=0x7c2cb8e358, count=1)
    [  0,  77, 1080, 2028]
        Region output visibleRegion (this=0x7c2cb8e3c0, count=1)
    [  0,  77, 1080, 2028]
        Region shadowRegion (this=0x7c2cb8e428, count=1)
    [  0,   0,   0,   0]
      forceClientComposition=false clearClientTarget=false displayFrame=[0 77 1080 2028] sourceCrop=[0.000000 0.000000 1080.000000 1951.000000] bufferTransform=0 (0) dataspace=UNKNOWN (0) override buffer=0x0 override acquire fence=0x0 override display frame=[0 0 -1 -1] override dataspace=UNKNOWN (0) override display space=ProjectionSpace(bounds = Rect(0, 0, -1, -1), content = Rect(0, 0, -1, -1), orientation = ROTATION_0) override damage region=  Region  (this=0x7c2cb8e510, count=1)
    [  0,   0,  -1,  -1]
 override visible region=  Region  (this=0x7c2cb8e578, count=1)
    [  0,   0,   0,   0]
 override peekThroughLayer=0x0 override disableBackgroundBlur=false 
      hwc: layer=0x0817 composition=DEVICE (2) 
  - Output Layer 0x7c2cb90330(info.bati11.android.otameshi/info.bati11.opengles.myopengles.glapp.MyOpenGlEsActivity#0)
        Region visibleRegion (this=0x7c2cb90348, count=1)
    [  0,   0, 1080, 2160]
        Region visibleNonTransparentRegion (this=0x7c2cb903b0, count=2)
    [  0,   0, 1080,  77]
    [  0, 2028, 1080, 2160]
        Region coveredRegion (this=0x7c2cb90418, count=2)
    [  0,   0, 1080,  78]
    [  0, 2028, 1080, 2160]
        Region output visibleRegion (this=0x7c2cb90480, count=1)
    [  0,   0, 1080, 2160]
        Region shadowRegion (this=0x7c2cb904e8, count=1)
    [  0,   0,   0,   0]
      forceClientComposition=false clearClientTarget=false displayFrame=[0 0 1080 2160] sourceCrop=[0.000000 0.000000 1080.000000 2160.000000] bufferTransform=0 (0) dataspace=UNKNOWN (0) override buffer=0x0 override acquire fence=0x0 override display frame=[0 0 -1 -1] override dataspace=UNKNOWN (0) override display space=ProjectionSpace(bounds = Rect(0, 0, -1, -1), content = Rect(0, 0, -1, -1), orientation = ROTATION_0) override damage region=  Region  (this=0x7c2cb905d0, count=1)
    [  0,   0,  -1,  -1]
 override visible region=  Region  (this=0x7c2cb90638, count=1)
    [  0,   0,   0,   0]
 override peekThroughLayer=0x0 override disableBackgroundBlur=false 
      hwc: layer=0x0816 composition=DEVICE (2) 
...

SurfaceとWindowManager

ところで、adbの出力を見るとActivityのレイヤーがあることが分かる。これがアプリのUI。このレイヤーはいつできるのか?

Activityがforegroundになるとき、具体的には ActivityThread#handleResumeActivity() でViewツリーのRootとなるDecorViewが作られ、 WindowManager#addView() の引数にViewRootImplを渡してる。ここから WindowManagerImpl#addView()WindowManagerGlobal#addView() で ViewRootImplが作られ(ViewRootImpl#mSurfaceSurfaceを保持してる)、 ViewRootImpl#setView() が呼ばれる。ViewRootImplが保持してるSurfaceがレイヤーが持つSurfaceと同じということかなぁ?

Androidを支える技術〈I〉──60fpsを達成するモダンなGUIシステム (WEB+DB PRESS plus) の「6.5 ViewRootImpl」が参考になりました。

おしまい

WebGLcanvasを用意してglオブジェクトを生成する部分に相当する処理は、Androidでは

  • 描画対象としてEGLという仕組みというか仕様に従っている
  • EGLContextとEGLSurfaceを用意してスレッドに関連づける
  • Androidにおいて、EGLSurfaceが描画対象として持つのはSurface
  • 結果、OpenGL ES でSurfaceに対して描画する

Surface、というか実態であるバッファーキューは複数あって、SurfaceFlingerが合成して実際の画面の描画となる。

他にも色々脱線したけど楽しい!!

WebGLのアルファブレンディングやってみる

前々回はドットで線を描画、前回はドットで矩形の描画とアルファブレンディングをやってみました。

どちらもループ処理で1ドットずつ打っていきました。例えばフルHDだとピクセル数は1920×1080=2073600になりますが、これだけの数を順番に1ピクセルずつ色をつけているのでしょうか?たぶん実際はGPUを使って並列にピクセルに色をつけるのではないかなと思います。今回は、WebGLでシェーダーを使ってGPUでドットを打ちます。

JavaScriptでCPUを使った処理だと下図の左のように1ピクセルずつ順番にループ処理で色付け、WebGLGPUを使った処理だと下図の右のようにvertexシェーダーで緑点を指定して色付けする範囲を指定し、範囲内の青く囲ったピクセルを並列でfragmentシェーダーに従って色付けする、というようなイメージでしょうか。

gyazo.com

わたくし、シェーダーは完全に初心者です。The Book of Shaders が良いとどこかで知って、いつか読んでみようと思っていたので読みながらWebGLで動かしてみました、という日記です。

まずはシェーダーをWebGLで動かす

Hello World!

The Book of Shaders の Hello World! のページを動かす。

完全に初心者だけどAndroidOpenGL ESの入門者向けの本は少し読んだことがある。そこで学んだことを思い出しつつやってみる。

Getting started with WebGL - Web APIs | MDN を見てみると、HTMLにcanvasタグをおいて const gl = canvas.getContext("webgl"); とやれば gl オブジェクトが手に入るみたい。

シェーダーを書くための文法はGLSL。WebGLではHTMLのscriptタグ内にvertexシェーダーとfragmentシェーダー、それぞれのソースコードを記述するっぽい。

The Book of Shaders にはfragmentシェーダーしか記述されてないけど、vertexシェーダーはcanvas全体を使うような矩形にすれば良いでしょう。fragmentシェーダーは写経する。

<canvas style="width:800px;height:400px;"></canvas>

<script id="vertex-shader" type="x-shader/x-vertex">
attribute vec2 pos;
void main() {
  gl_Position = vec4(pos, 0, 1);
}
</script>

<script id="fragment-shader" type="x-shader/x-fragment">
void main() {
    gl_FragColor = vec4(1.0,0.0,1.0,1.0);
}
</script>

前回までのようにループ処理で1ドット打つようなソースコードではない。vertexシェーダーで色をつける場所を指定する、fragmentシェーダーで1ピクセルごとの色付けを指定する、GPUはシェーダーに従ってたくさんのピクセルを並列で同時に色付けする、というようなイメージかな。GPUなので超並列で実行できるし、いくつかの関数はハードウェアを使って高速に実行されるらしい。

WebGLを使うJavaScriptのコードを書く。 initialize() という関数を用意して初期化処理をする、その後 gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); で描画する。

const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl');

function initialize() {
  gl.clearColor(0, 0, 0, 1);
  gl.clear(gl.COLOR_BUFFER_BIT);
  
  // vertexシェーダーのソースコードをHTMLのscriptタグから読み込んでコンパイルする
  const vertexShaderSource = document.querySelector('#vertex-shader').text;
  const vertShader = gl.createShader(gl.VERTEX_SHADER);
  gl.shaderSource(vertShader, vertexShaderSource);
  gl.compileShader(vertShader);
  var isCompiled = gl.getShaderParameter( vertShader, gl.COMPILE_STATUS );
  if ( !isCompiled ) {
    throw new Error( 'Shader compile error: ' + gl.getShaderInfoLog( vertShader ) );
  }
  
  // fragmentシェーダーのソースコードをHTMLのscriptタグから読み込んでコンパイルする
  const fragmentShaderSource = document.querySelector('#fragment-shader').text;
  const fragShader = gl.createShader(gl.FRAGMENT_SHADER);
  gl.shaderSource(fragShader, fragmentShaderSource);
  gl.compileShader(fragShader);
  var isCompiled = gl.getShaderParameter( fragShader, gl.COMPILE_STATUS );
  if ( !isCompiled ) {
    throw new Error( 'Shader compile error: ' + gl.getShaderInfoLog( fragShader ) );
  }
  
  // リンクして実行できるプログラムにする
  var program = gl.createProgram();
  gl.attachShader(program, vertShader);
  gl.attachShader(program, fragShader);
  gl.linkProgram(program);
  gl.useProgram(program);
    
  // vertexシェーダーの `pos` 変数を使って頂点を4箇所セットする
  const vertex_buffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer);
  const position = new Float32Array([
    -1, -1,
    1, -1,
    -1, 1,
    1, 1,
  ]);
  gl.bufferData(gl.ARRAY_BUFFER, position, gl.STATIC_DRAW);
  const positionLocation = gl.getAttribLocation(program, 'pos');
  gl.enableVertexAttribArray(positionLocation);
  gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
}

// 初期化処理を実行する
initialize();

// 4つの頂点を三角形を使って描画する。シェーダーでは点・線・三角形で描画する
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

結果は紫色の矩形が表示されるだけ。fragmentシェーダーで gl_FragColor = vec4(1.0,0.0,1.0,1.0); と記述があって、RGBAで色を指定している。redとblueが1.0になってるので紫色になる。

Uniforms

The Book of Shaders の Uniformsのページを読んでいく。

uniform変数というのがある。CPU側の処理、つまり今回だとJavaScript側からパラメータを、GPU側の処理(つまり、fragmentシェーダー)に渡せる。全てのピクセルに対する処理でこのパラメータを読めるということ。

まず u_time というuniform変数が導入されてる。これは実行を開始してからの経過時間をJavaScript側からfragmentシェーダーへ伝えてるのに使う。経過時間に応じて色を変えるということをする。

<script id="fragment-shader" type="x-shader/x-fragment">
uniform float u_time;

void main() {
  gl_FragColor = vec4(abs(sin(u_time)),0.0,0.0,1.0);
}
</script>

次に、処理開始時間と u_time に値を渡すための値を app というオブジェクトに持たせることにする。

const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl');

const app = {
  startTime: new Date().getTime(),
  uTime: null
};

function initialize() {
  ...
  
  gl.useProgram(program);
    
  // gl.useProgram()以降で可能
  app.uTime = gl.getUniformLocation(program, "u_time");

  ...

描画する処理を rendering() という関数にする。1度描画したらそれで終わりというわけではないので、 requestAnimationFrame(rendering); で繰り返し実行されるようにする。

function rendering() {
  // uniforms変数を更新
  const now = new Date().getTime();
  const currentTime = (now - app.startTime) / 1000;
  gl.uniform1f(app.uTime, currentTime); // gl.uniform1f() という関数で値をセット
  
  // 描画
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

  requestAnimationFrame(rendering);
}

initialize();
rendering();

結果、こうなる。いいね、ちゃんと色に変化がある!各ピクセルを色付けするfragmentシェーダーは、CPU側のJavaScriptから渡された utime によって色が変わるため。

Image from Gyazo

次は u_resolution というuniform変数を導入する。これは描画領域全体のサイズをJavaScript側で計算してfragmentシェーダーへ伝える。

さらに gl_FragCoord というシェーダーで最初から用意されている変数を使う。ピクセルの座標を表す。これはfragmentシェーダーが実行される毎に異なる値になっていて、varying変数と呼ぶ。

<script id="fragment-shader" type="x-shader/x-fragment">
uniform float u_resolution;
uniform float u_time;

void main() {
  // 経過時間で色を変える
  //gl_FragColor = vec4(abs(sin(u_time)), 0.0, 0.0, 1.0); コメントアウト
  
  // ピクセルの位置で色を変える
  vec2 st = gl_FragCoord.xy/u_resolution;
  gl_FragColor = vec4(st.x,st.y,0.0,1.0);
}
</script>

app に追加。

const app = {
  startTime: new Date().getTime(),
  uTime: null,
  uResolution: null
};

function initialize() {
  ...
  
  gl.useProgram(program);
  
  // uniforms
  app.uTime = gl.getUniformLocation(program, "u_time");
  app.uResolution = gl.getUniformLocation(program, "u_resolution");
  
  ...

canvasのサイズが動的になっても対応できるようにしておく。

function resize() {
  app.width = canvas.width;
  app.height = canvas.height;
  gl.uniform2f(app.uResolution, app.width, app.height);
  gl.viewport(0, 0, app.width, app.height);
}
window.addEventListener('resize', resize);

initialize();
resize();
rendering();

結果、こうなる。良いですな〜。

最後に u_mouse というuniform変数を導入する。これはマウスの位置をJavaScript側からfragmentシェーダーに渡す。マウスの位置で色付けを変える。ここまでとやってることはあまり変わらないので結果だけ。

See the Pen WebGL HelloWorld by kariyayo (@kariyayo) on CodePen.


できた!!

好きな大きさの長方形を描く

ここまでの例はcanvas全体に色付けしていた。canvas内の好きな場所に好きな大きさの長方形を描くにはどうすればいいか?

The Book of Shaders の「 Shapes」で、正規化された座標(canvas全体の座標を0.0 ~ 1.0で表した座標)で、(0.1, 0.1) から (0.9, 0.9) の範囲を白、それ以外を黒にする例がある。これを応用すればできるはず。

(0.1, 0.1) から (0.9, 0.9) の範囲を白にするのは以下のコードでできた。

<script id="fragment-shader" type="x-shader/x-fragment">
uniform float u_resolution;

void main() {
  vec2 st = gl_FragCoord.xy/u_resolution.xy;
  vec3 color = vec3(0.0);

  // x, y 両方とも0.1以上なら白(vec2(1.0))
  vec2 bl = step(vec2(0.1), st);
  // x. y 両方とも0.9以上なら白(vec2(1.0))
  vec2 tr = step(vec2(0.1), 1.0-st);
  
  // 全て条件を満たせば vec3(1.0) つまり白、どれか1つでも満たしていなければ vec3(0.0) つまり黒
  color = vec3(bl.x * bl.y * tr.x * tr.y);

  gl_FragColor = vec4(color,1.0);
}
</script>

これを始点と大きさで長方形を表して、その範囲内であれば色付けするように書き換える。あと好きな色も指定できるようにする。diffは以下。

@@ -5,11 +5,18 @@
   vec2 st = gl_FragCoord.xy/u_resolution.xy;
   vec3 color = vec3(0.0);
 
-  // x, y 両方とも0.1以上なら白(vec2(1.0))
-  vec2 bl = step(vec2(0.1), st);
-  // x. y 両方とも0.9以上なら白(vec2(1.0))
-  vec2 tr = step(vec2(0.1), 1.0-st);
-  
+  // 始点
+  vec2 start = vec2(0.1);
+  // 大きさ(0.1~0.9の範囲なので0.8)
+  vec2 wh = vec2(0.8);
+
+  vec2 end = start + wh;
+
+  // x, y 両方ともstart以上なら白
+  vec2 bl = step(start, st);
+  // x. y 両方ともend以下なら白
+  vec2 tr = step(vec2(1.0-end.x, 1.0-end.y), 1.0-st);
+
   // 全て条件を満たせば vec3(1.0) つまり白、どれか1つでも満たしていなければ vec3(0.0) つまり黒
   color = vec3(bl.x * bl.y * tr.x * tr.y);

ここまでOK。これを関数化する。ついでに白以外の好きな色を指定できるようにしておく。

vec3 rect(vec2 st, vec2 start, vec2 wh, vec3 color) {
  vec2 end = start + wh;
  // x, y 両方ともstart以上なら色付け
  vec2 bottom_left = step(start, st);
  // x. y 両方ともend以下なら色付け
  vec2 top_right = step(vec2(1.0-end.x, 1.0-end.y), 1.0-st);
  // 全て条件を満たせばcolorで色づけ、どれか1つでも満たしていなければ黒
  return color * vec3(bottom_left.x * bottom_left.y * top_right.x * top_right.y);
}

void main() {
  ...
  
  vec2 start = vec2(0.5, 0.0);
  vec2 wh = vec2(0.5, 0.5);
  color = rect(st, start, wh, vec3(1.0, 0.0, 0.0));

ここまでくれば、 rect() を2回利用することで赤い長方形と青い長方形を描くことができる。

  // 赤い長方形
  vec2 start = vec2(0.05, 0.5);
  vec2 wh = vec2(0.2, 0.4);
  vec3 c = rect(st, start, wh, vec3(1.0, 0.0, 0.0));
  if (c != vec3(0.0)) {
    color = c;
  }
  
  // 青い長方形
  start = vec2(0.1, 0.25);
  wh = vec2(0.2, 0.4);
  c = rect(st, start, wh, vec3(0.0, 0.0, 1.0));
  if (c != vec3(0.0)) {
    color = c;
  }

結果はこうなる。

gyazo.com

アルファブレンディング

自分で計算する

長方形が重なった部分の色をアルファブレンディングしたい。前回同様に不透明度を使って計算すればできる。けどWebGLにはブレンディングの機能があってそっちを使うべきな気がする。

一旦ここまでのソースコードを活かすと、アルファブレンディングする関数を用意して色を決める時に呼べば良いだけ。

vec4 alphaBlend(vec4 fg, vec4 bg) {
  // 背景の色成分 * (1.0 - 不透明度) + 前景の色成分 * 不透明度
  vec3 cc = bg.rgb * (vec3(1.0) - vec3(fg.a)) + fg.rgb * vec3(fg.a);
  return vec4(cc, fg.a);
}

...

  // 青い長方形
  start = vec2(0.1, 0.25);
  wh = vec2(0.2, 0.4);
  c = rect(st, start, wh, vec4(0.0, 0.0, 1.0, 0.8));
  if (c != vec4(0.0)) {
    color = alphaBlend(c, color);
  }

結果はこうなる。

See the Pen WebGL Shapes by kariyayo (@kariyayo) on CodePen.


WebGLの機能を使う

これまではvertexシェーダーで範囲を指定して、fragmentシェーダーで各ピクセルの色づけしていた。この「範囲を指定してその範囲内のピクセルの色づけをする」というステップを長方形の数だけJavaScript側から実行する。この別のやり方でアルファブレンディングしてみる。

vertexシェーダーで指定した範囲のピクセルを全て同じ色にすればいいのでfragmentシェーダーはすごいシンプルになる。

uniform vec4 u_color;

void main() {
  gl_FragColor = u_color;
}

JavaScriptrendering からvertexシェーダーの position にデータを流すのと、fragmentシェーダーの u_color にデータを流すのを、赤い長方形、青い長方形と2回実行する。

function rendering() {

  ...
  
  // 赤い長方形
  const redRect = new Float32Array([
    // 左下
    -0.8, 0,
    // 右下
    0, 0,
    // 左上
    -0.8, 1,
    // 右上
    0, 1,
  ]);
  gl.bufferData(gl.ARRAY_BUFFER, redRect, gl.STATIC_DRAW);
  gl.uniform4f(app.uColor, 1.0, 0.0, 0.0, 1.0);
  // render
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  
  // 青い長方形
  const blueRect = new Float32Array([
    // 左下
    -0.5, -0.5,
    // 右下
    0.2, -0.5,
    // 左上
    -0.5, 0.4,
    // 右上
    0.2, 0.4,
  ]);
  gl.bufferData(gl.ARRAY_BUFFER, blueRect, gl.STATIC_DRAW);
  gl.uniform4f(app.uColor, 0.0, 0.0, 1.0, 0.8);
  // render
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

  requestAnimationFrame(rendering);
}

そして、WebGLのブレンディング機能を使う。 gl.blendFunc() の第一引数はこれから色付けする色に掛ける係数。不透明度を掛けるので gl.SRC_ALPHA を指定する。第二引数は既についている色(背景色)に掛ける係数で、 1.0-不透明度 を掛けるので gl.ONE_MINUS_SRC_ALPHA を指定する。

function rendering() {
  ...

  gl.enable(gl.BLEND);
  gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

  ...

結果こうなる。

See the Pen WebGL Shapes with blend functor by kariyayo (@kariyayo) on CodePen.


できた!

おしまい

シェーダー楽しい!!!