tiny dots

裏方の日々

深度 + アルファブレンディングの闇

今やってる案件で、画像を円形のマスクで抜きつつ3D空間の中で動かす+パフォーマンス的にはFBOに突っ込みたいというお題があって↓のようなコードを書いていたがうまくいかず。

void ofApp::setup()
{
    ofEnableAlphaBlending();
    
    shader.load("mask");
    image.loadImage("image.jpg");
    
    fbo.allocate(ofGetWidth(), ofGetHeight(), GL_RGBA, 8);
    fbo.begin();
    {
        ofClear(0, 0);
    }
    fbo.end();

    mask.allocate(image.width, image.height, GL_RGBA, 8);
    mask.begin();
    {
        ofClear(0, 0);
        ofFill();
        ofSetColor(255, 255);
        ofCircle(mask.getWidth() * 0.5, mask.getHeight() * 0.5, 40);
    }
    mask.end();
}

void ofApp::update()
{
    fbo.begin();
    {
        ofEnableDepthTest();
        ofEnableAlphaBlending();
        
        ofClear(0, 0);
        
        maskShader.begin();
        {
            maskShader.setUniformTexture("mask", maskFbo.getTextureReference(), 1);
            
            ofSetColor(255, 255);
            image.draw(0, 0);
        }
        maskShader.end();
    }
    fbo.end();
}

void ofApp::draw()
{
    ofBackground(0, 0);
    fbo.draw(0, 0);    
}

シェーダーの中身はこんな感じ。

// mask.vert
#version 120

varying vec2 texCoordVarying;

void main()
{
    texCoordVarying = gl_MultiTexCoord0.xy;
    gl_Position = ftransform();
}
// mask.frag
#version 120

uniform sampler2DRect tex0;
uniform sampler2DRect mask;

varying vec2 texCoordVarying;

void main()
{
    vec4 texel0 = texture2DRect(tex0, texCoordVarying);
    vec4 texel1 = texture2DRect(mask, texCoordVarying);
    
    gl_FragColor = vec4(texel0.rgb, texel0.a * texel1.a);
}

これを実行すると↓な画ができあがる。んー。 f:id:hideyukisaito:20150712012022p:plain

フラグメントシェーダーの中でアルファテストをかけてみる。

if (texel1.a < 0.6) {
    discard;
}

すると、、、 f:id:hideyukisaito:20150712012256p:plain

ん〜おしい。。。ちなみに切り捨てるアルファ値を上げると見るに耐えないくらいエッジがジャギってしまい全然ダメ。 心が折れる寸前までいったのでバカにされる覚悟でof-slackに質問を投げてみたところ、けっこうこのトピックに悩まされる人は多いらしくあの手この手の解決方法を提案していただけました。 そのうちのひとつが深度テストをせずに自前でZ-Sortするというもので、わりと定石とのこと。コード的にはこんなん。

ofEasyCam cam;
vector<ofVec3f> positions;

bool sortByDistance(const pair<int, float> &left, const pair<int, float> &right)
{
    return left.second == right.second ? false : left.second > right.second;
}

deque< pair<int, float> > distances;
for (unsigned int i = 0; i < positions.size(); ++i) {
    // 単純な z ではなくカメラからの距離で計算する
    distances_.push_back(make_pair<int, float>(i, cam.getPosition().distance(positions[i])));
}

sort(distances.begin(), distances.end(), sortByDistance);

for (deque< pair<int, float> >::iterator it = distances.begin(); it != distances.end(); ++it) {
    ofCircle(positions[it->first], 50);
}

おーいい感じ! f:id:hideyukisaito:20150712013350p:plain

エッジの綺麗さはまだ課題だけど、まあ誤魔化しはきくレベル。
早速プロジェクトの方に移植してみるもなんかうまくいかない。。あーそいういえばカメラの方を回してたんだった。ということで

float angle;
ofVec3f axis;
cam.getRotation(angle, axis);

deque< pair<int, float> > distances;
for (unsigned int i = 0; i < positions.size(); ++i) {
    ofVec3f v = positions[i];
    v.rotate(angle, axis);
    distances_.push_back(make_pair<int, float>(i, cam.getPosition().distance(v)));
}

という具合にカメラの回転を考慮して計算してやると見事いい感じになった。今回は対象オブジェクトの数も少ないのでこの方法でいくことに決定。

他にもポイントスプライトを使って点の中心から一定距離のピクセルをdiscardするアプローチを教えてもらったりして目からウロコでした。

marina.sys.wakayama-u.ac.jp

あとソフトパーティクルとか

wgld.org

デプスピーリングとか

marina.sys.wakayama-u.ac.jp

奥が深いが闇も深い。なんにしろ積極的に質問投げるべきですね。。。
早くこれ完成形お披露目したいなー。