最近使用 ParticleSystem 时发现了一个问题,追查下去,才发现,常常使用的 BillboardSet 其实也并不牢靠。

ParticleSystem 中的 accurate_facing 这个属性估计没多少人会打开的,默认值是Off。
这个属性适用于 BillboardParticleRenderer, BillboardParticleRenderer 是 Ogre 下 ParticleSystem 默认的 Render。ParticleSystem 产生的 Particle 通过这个 Render 最后生成顶点数据之流,用于描画。
BillboardParticleRenderer 的实现是基于 BillboardSet 的。
如果打开 accurate_facing 这个属性,计算 BillBoard 时,会使用 Particle 中心到 Camera 这个向量来作为 BillBoard 的方向。如果不打开的话,只是使用 Camera 的方向来计算,可以节约少许运算量。

但是很不幸的,BillboardParticleRenderer 在此处被 BillboardSet 的 Bug黑了,如下

//-----------------------------------------------------------------------
void BillboardParticleRenderer::_updateRenderQueue(RenderQueue* queue, 
    std::list<Particle*>& currentParticles, bool cullIndividually)
{
    mBillboardSet->setCullIndividually(cullIndividually);

    // Update billboard set geometry
    mBillboardSet->beginBillboards(currentParticles.size());
    Billboard bb;
    for (std::list<Particle*>::iterator i = currentParticles.begin();
        i != currentParticles.end(); ++i)
    {
        Particle* p = *i;
        bb.mPosition = p->position;
        if (mBillboardSet->getBillboardType() == BBT_ORIENTED_SELF ||
            mBillboardSet->getBillboardType() == BBT_PERPENDICULAR_SELF)
        {
            // Normalise direction vector
            bb.mDirection = p->direction;
            bb.mDirection.normalise();
        }
        bb.mColour = p->colour;
        bb.mRotation = p->rotation;
        // Assign and compare at the same time
        if (bb.mOwnDimensions = p->mOwnDimensions)
        {
            bb.mWidth = p->mWidth;
            bb.mHeight = p->mHeight;
        }
        mBillboardSet->injectBillboard(bb);

    }
    
    mBillboardSet->endBillboards();

    // Update the queue
    mBillboardSet->_updateRenderQueue(queue);
}

可以看到, 如果 p->mOwnDimensions 为 false 的话,Billboard bb的 mWidth 和 mHeight 是不确定的,因为 Billboard  的构造函数中没有初始化过。
嗯,这个问题很容易看出来,所以难免有人奇怪,为什么 Ogre 这个问题一直没人修改呢?
我们进到 BillboardSet::injectBillboard 去看:

//-----------------------------------------------------------------------
void BillboardSet::injectBillboard(const Billboard& bb)
{
    // Don't accept injections beyond pool size
    if (mNumVisibleBillboards == mPoolSize) return;

    // Skip if not visible (NB always true if not bounds checking individual billboards)
    if (!billboardVisible(mCurrentCamera, bb)) return;

    if (!mPointRendering &&
        (mBillboardType == BBT_ORIENTED_SELF ||
        mBillboardType == BBT_PERPENDICULAR_SELF ||
        (mAccurateFacing && mBillboardType != BBT_PERPENDICULAR_COMMON)))
    {
        // Have to generate axes & offsets per billboard
        genBillboardAxes(&mCamX, &mCamY, &bb);
    }

    // If they're all the same size or we're point rendering
    if( mAllDefaultSize || mPointRendering)
    {
        /* No per-billboard checking, just blast through.
        Saves us an if clause every billboard which may
        make a difference.
        */

        if (!mPointRendering &&
            (mBillboardType == BBT_ORIENTED_SELF ||
               mBillboardType == BBT_PERPENDICULAR_SELF ||
               (mAccurateFacing && mBillboardType != BBT_PERPENDICULAR_COMMON)))
        {
            genVertOffsets(mLeftOff, mRightOff, mTopOff, mBottomOff,
                mDefaultWidth, mDefaultHeight, mCamX, mCamY, mVOffset);
        }
        genVertices(mVOffset, bb);
    }
    else // not all default size and not point rendering
    {
        Vector3 vOwnOffset[4];
        // If it has own dimensions, or self-oriented, gen offsets
        if (mBillboardType == BBT_ORIENTED_SELF ||
            mBillboardType == BBT_PERPENDICULAR_SELF ||
            bb.mOwnDimensions ||
            (mAccurateFacing && mBillboardType != BBT_PERPENDICULAR_COMMON))
        {
            // Generate using own dimensions
            genVertOffsets(mLeftOff, mRightOff, mTopOff, mBottomOff,
                bb.mWidth, bb.mHeight, mCamX, mCamY, vOwnOffset);
            // Create vertex data
            genVertices(vOwnOffset, bb);
        }
        else // Use default dimension, already computed before the loop, for faster creation
        {
            genVertices(mVOffset, bb);
        }
    }
    // Increment visibles
    mNumVisibleBillboards++;
}

如果 mAccurateFacing 这个属性不是 true 的话,显然 mWidth,mHeight 没初始化的这个问题,是不会暴露出来的,因为它会走 genVertices(mVOffset, bb) 这个流程,这个流程里面,未初始化过的 mWidth,mHeight 被丢弃了,根本不用。
看到这里,说不定你也很开心的打开了这个属性,想验证一下,是不是会有问题。结果不太乐观啊,因为这个问题居然没出现!
莫不是楼主眼光不够,其实 Ogre 在别处巧妙地规避了这个问题了呢?
那么你再尝试在这个 ParticleSystem 中加入一个 Scale Affactor。
如果是使用 Script 文件的方式的话,那么就是这样

affector Scaler
{
        rate 1
}

 

于是问题很奇妙的出现了~
这个问题的巧妙之处在于,如果你不加入一个会改变大小的 Affactor 的话,所有的 Billboard 片都是一样大小 , mAllDefaultSize 变量为 true ,产生数据流时,bb 的 mWidth,mHeight 仍然是被无视的。
而你一旦加入了一个这样的 Affactor,mAllDefaultSize 变量就不再为 true 了,mWidth,mHeight 终于可以翻身做主人啦@!

但仅仅这样,仍然不会出问题的,因为你查看 Scale Affactor, 会发现 mWidth,mHeight 这两个值已经被 Affactor 改为新值了, p->mOwnDimensions 也变成 true 了,根本不会出现 p->mOwnDimensions 为 false 的情况!

嗯哼,同学,你忘记了 ParticleSystem的更新流程!
它是按照这个顺序来更新的

// Update existing particles
_expire(timeElapsed);
_triggerAffectors(timeElapsed);
_applyMotion(timeElapsed);
// Emit new particles
_triggerEmitters(timeElapsed);

也就是说,Affacter 在 _triggerAffectors 中先动作,然后才会调用 _triggerEmitters来产生新 Particle ,这些新产生的 Particle,很不幸的没有的到 Affacter 的关照,于是它的 mOwnDimensions  仍然是默认的 false。
于是,悲剧发生了。

这其实都源于 BillboardSet 在处理加入的 Billboard 时,产生的一个逻辑错误。

比较简单的修正方法是在 BillboardSet::injectBillboard 函数中,Vector3 vOwnOffset[4]; 这一行之后,修改成如下代码。
 

// If it has own dimensions, or self-oriented, gen offsets
if( !bb.mOwnDimensions )
{
    genVertices(mVOffset, bb);
}
else if (mBillboardType == BBT_ORIENTED_SELF ||
    mBillboardType == BBT_PERPENDICULAR_SELF ||
    bb.mOwnDimensions ||
    (mAccurateFacing && mBillboardType != BBT_PERPENDICULAR_COMMON))
{

 

嗯,逻辑上完美无暇,至于代码有点丑陋,呵呵,反正问题解决了不是,咱们就省点脑子吧。

BTW: Ogre 帮助文档上说 在 Particle Script中使用

    accurate_facing on

的格式来打开 accurate_facing 的使用。但是其实需要用

    accurate_facing true

才能打开,囧~