頭と尻尾はくれてやる!

パソコンおやじのiPhoneアプリ・サイト作成・運営日記


Metalのステンシルバッファでマスクしてみる

Metalでステンシルバッファを使ってみようとしたのよ。Metalではもちろん、OpenGLでも使ったことなかったのでなかなか苦しんでるところだけどとりあえず最低限の動作をさせてみようとしたんだ。
わかりやすそうなところで、マスクをやってみたのよ。



↑これはいつも通りの(?)ロボット、もう一つは平面を用意してロボットの周りを回る。これはまだステンシルバッファは使っていなくて単に両方描いてるだけね。
この平面の部分のみロボットを描く、みたいなことをやってみる。

毎回の描画ループではこんな感じ :
1.ステンシルバッファの値は0にクリア
2.平面の描画時はステンシルバッファに128を書き込む
3.ロボットの描画時はステンシルバッファの値をみて128なら表示する

その前に下準備をせねば、、、

下準備(1) MTLRenderPassDescriptor オブジェクトの設定

ステンシルバッファ用のメモリ確保などをしておくよ。コードはこんな感じで。

-(void)setupRenderPassDescriptor
 {
    self.renderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor];
    id  drawable = [_view.metalLayer nextDrawable];

    MTLTextureDescriptor *textureDescriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatStencil8
                                                                                        width: drawable.texture.width
                                                                                       height: drawable.texture.height
                                                                                    mipmapped: NO];
    textureDescriptor.textureType =  MTLTextureType2D;
    id  stencilTexture = [device newTextureWithDescriptor: textureDescriptor];

    MTLRenderPassStencilAttachmentDescriptor *stencilAttachment = _renderPassDescriptor.stencilAttachment;
    stencilAttachment.texture = stencilTexture;
    stencilAttachment.loadAction = MTLLoadActionClear;
    stencilAttachment.storeAction = MTLStoreActionDontCare;
    stencilAttachment.clearStencil = 0;
}
_viewってのは表示するUIViewオブジェクトで
@property(nonatomic , weak) CAMetalLayer *metalLayer;
ってプロパティを持ってる(AppleのMetal関連のサンプルコードのままだけど)。

なるほど、ステンシルバッファもデプスバッファみたいにサイズ指定して ’テクスチャ’ として作るのかあ!と地味に感動したわ。上のコードだとstencilTextureはローカル変数だけど、インスタンス変数にしといたらこれを他のシェーダに突っ込んで、、、とかできるんだろうな。
ところで、デプスバッファの方だけど、今回は平面とロボットの前後関係は問わないとしてるのでデプスバッファのpixel formatは MTLPixelFormatInvalid ってことで。こうしておくと自分でメモリ確保なんかしなくてもよきに計らってくれるし、ロボットはちゃんと表示される。

下準備(2) 平面オブジェクトの設定

下のコードは平面のオブジェクトまわりの設定ね。
{
    MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init] ;
    pipelineStateDescriptor.label = @“pipeline for plane";
    pipelineStateDescriptor.sampleCount = 1;
    pipelineStateDescriptor.vertexFunction = vertexProgram;
    pipelineStateDescriptor.fragmentFunction = nil;
    pipelineStateDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm;
    //pipelineStateDescriptor.colorAttachments[0].writeMask = MTLColorWriteMaskNone;
    pipelineStateDescriptor.depthAttachmentPixelFormat = _depthPixelFormat;
    pipelineStateDescriptor.stencilAttachmentPixelFormat = _stencilPixelFormat;

    NSError *pipelineError;
    self.renderPipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor   error: &pipelineError];
}
長いので半分に分けとくわ。
最初はMTLRenderPipelineStateオブジェクトの設定。面白いのはフラグメントシェーダがnilでいいんだわ。平面は画面に表示させる必要がないので MTLColorWriteMaskNone としてるけど、これなくても表示されないな。マスク用オブジェクトを表示させないない方法は他にもあるみたい(デプステストで必ず失敗させるとか)。

depth,stencilのpixel formatが引数になってるけど、下準備(1)でセットしたMTLRenderPassDescriptor オブジェクトの内容と合わせないと実行時にエラーが出ると思う。

下はこの続きね。
{
    MTLStencilDescriptor *stencilDescriptor = [[MTLStencilDescriptor alloc] init];
    stencilDescriptor.stencilCompareFunction = MTLCompareFunctionAlways;
    stencilDescriptor.depthStencilPassOperation = MTLStencilOperationReplace;

    MTLDepthStencilDescriptor *desc = [[MTLDepthStencilDescriptor alloc] init];//—(A)
    desc.frontFaceStencil = stencilDescriptor;
    desc.backFaceStencil = stencilDescriptor;
    self.depthStencilState = [_device newDepthStencilStateWithDescriptor:desc];
}
上のコードの(A)の部分なんだけど MTLDepthStencilDescriptor オブジェクトの設定で depthCompareFunction プロパティを設定しないとデフォルトで MTLCompareFunctionAlways なので、depthテストは常にパスすることになるよね。
MTLStencilDescriptor オブジェクトの設定で stencilCompareFunction プロパティを MTLCompareFunctionAlways にしとけば平面部分についてはstencilテストも常にパスすることになる。

depthStencilPassOperation プロパティ(これはdepthテストもstencilテストの両方がパスした時の挙動を設定する)これを MTLStencilOperationReplace にする。この〜Replaceってのはステンシルバッファの値を参照値に置き換えるのよ。平面がある部分のdepthテストもstencilテストも常にパスする設定なので、結局平面のある部分のステンシルバッファの値が参照値に書き換えられることになる(平面のないところは0のママ)。

下準備(3) ロボットオブジェクトの設定

次に、ロボットの方ね。ロボットの方も平面に似たような設定するんだけど、ロボットはステンシルバッファの値を見て参照値と同じなら表示するような設定にしてる。
{
    MTLStencilDescriptor *stencilDescriptor = [[MTLStencilDescriptor alloc] init];
    stencilDescriptor.stencilCompareFunction = MTLCompareFunctionEqual;
    stencilDescriptor.stencilFailureOperation = MTLStencilOperationKeep;
    stencilDescriptor.depthFailureOperation = MTLStencilOperationKeep;
    stencilDescriptor.depthStencilPassOperation = MTLStencilOperationKeep;

    MTLDepthStencilDescriptor *desc = [[MTLDepthStencilDescriptor alloc] init];
    desc.depthCompareFunction = MTLCompareFunctionLess;
    desc.depthWriteEnabled = YES;
    desc.frontFaceStencil = stencilDescriptor;
    desc.backFaceStencil = stencilDescriptor;

    self.depthStencilState = [_device newDepthStencilStateWithDescriptor:desc];
}
stencilCompareFunction プロパティを MTLCompareFunctionEqual としてるのでロボットを描こうとした部分のステンシルバッファの値と参照値が同じならパスする、という設定。このstencilテストで結果がどうあれステンシルバッファの値を変更する必要はないから〜Operationはどれも〜Keepになってる。
depthテストはいつも通り。


下準備は以上のような感じ。毎フレーム、コールされる部分で今までと違うのは参照値を教えてあげることくらいかな。
{
    [renderEncoder setStencilReferenceValue: 128];
}
こんな感じでセットすることができる。参照値としてるけど、基準値とか比較値とか見るところによっていろいろだったりする。


以上の結果がこんな感じ。



わかりにくいけど、まあ意図通り動いてる。ふー。


参考になったページ
wgld.org | WebGL: ステンシルバッファ |
Tsurumura Seisakusho: OpenGl ステンシルバッファ
ステンシル バッファー テクニック (Direct3D 9)

<< Metalで鏡を表現してみる  TopPage  煙の粒子の運動方程式 >>

コメント


管理者にだけ表示を許可する
 

トラックバック

トラックバックURL
http://ringsbell.blog117.fc2.com/tb.php/976-0bac725d




Copyright ©頭と尻尾はくれてやる!. Powered by FC2 Blog. Template by eriraha.

FC2Ad