Мягкие частицы (Soft Particles).

Одной из хорошо распиаренных возможностей DX10 являются так называемые мягкие частицы (soft particles). Однако на самом деле для реализации этого эффекта вполне достаточно поддержки SM3 и и они великолепно делаются на OpenGL (не требуя при этом мелкомягкой и убогой ви$ты).

Рассмотрим что же именно подразумевается под "мягкими" частицами и в чем заключается их отличие от традиционных частиц.

Для представления обычных частиц используются так называемые billboard'ы - грани, всегда повернутые к камере. На billboard обычно натягивается текстура. OpenGL предоставляет хорошую поддержку таких частиц через расширения ARB_point_sprite и ARB_point_parameters.

Пока мы работаем с большим количеством маленьких частиц особых проблем не заметно. Но при использовании небольшого числа больших частиц становится заметно, что частицы представлены billboard'ами - при пересечении billboard и грани происходит ее частичное отсечение (см. рис 1).

particle screenshot

Рис 1. Артефакты, возникающие при использовании традиционных частиц.

Еще хуже дело обстоит, когда частица вдруг пропадает, за счет того, что порождающая ее точка оказываются невидима (не проходит тест глубины) - в этом случае резкие скачки в изображении легко заметны (это обычно происходит при использовании расширения ARB_point_sprite, проверяющего глубину всего для одной точки billboard-а).

Это связано с тем, что частицы представлена одним полигоном, в то время как частицы обычно соответствуют не плоским фигурам, а объемными.

Мягкие частицы как раз и опираются на представление частиц как некоторых объемов с туманом - в этом случае учитывается какая именно часть этого объема видна и подобных резких скачков не происходит.

soft particle screenshot

Рис 2. Сцена с рис. 1, но с использованием мягких частиц.

Рис 3. Отличие мягкой частицы от обычной.

Основная задача при рендеринге мягких частиц - это определение видимой части частицы, точнее луча, лежащего внутри объема частицы.

В идеальном случае при выводе такой частицы нам необходимо определить длину части луча (соответствующего текущему фрагменту), целиком лежащей внутри видимой части объема частицы.

При этом возможны два случая.

1. Луч "протыкает" объемную частицу насквозь и просто берем прозрачность из текстуры (рис 4.a).

2. Внутри шара находятся непрозрачные объекты, выведенные ранее. Тогда нам нужен отрезок от точки входа в частицу до ближайшего пересечения луча с объектами сцен, лежащими внутри объемной частицы. В этом случае отношение длины этого отрезка к заданной толщине частицы задает увеличение прозрачности частицы (рис 4.b).

Рис 4.

Таким образом, в тем местах, где billboad уходит за объекты сцены происходит плавное "пропадание" частицы (вместо резкого скачка).

Для того, чтобы можно было понять протыкает ли луч насквозь такую объемную частицу или нет, нам достаточно прочесть значение из z-буфера, соответствующее данному фрагменту, и сравнить его с текущей глубиной частицы с учетом заранее заданной толщины частицы.

Поскольку полупрозрачные объекты, в том числе и частицы, всегда выводят после вывода непрозрачных, то можно после вывода всех непрозрачных объектов скопировать буфер глубины в текстуру и при выводе мягких частиц читать из нее значения глубины.

Это удобно еще и потому, что во многих случаях для повышения быстродействия сначала осуществляется рендеринг всей сцены только в буфер глубины (подобный прием был использован Дж. Кармаком еще в Doom III).

Ниже приводится соответствующий фрагментный шейдер и скриншот, полученный при его использовании.

//
// Soft particles fragment shader

varying	vec3 pos;

uniform sampler2DRect depthMap;
uniform sampler2D     particleMap;

void main (void)
{
    const float tau = 0.7;
    const float n   = 0.1;        // zNear
    const float f   = 100.0;      // zFar
    float scale     = 0.3;

    float d = distance ( pos, gl_TexCoord [1].xyz );
    float r = gl_TexCoord [1].w;

    if ( d >= r )
        discard;

    float w   = r*r - d*d;
    float zs  = texture2DRect ( depthMap, gl_FragCoord.xy ).r;

    float d1  = n*f/(f - zs*(f-n));
    float d2  = n*f/(f - gl_FragCoord.z*(f-n));
    float dz  = min ( 0.5, scale * ( d1 - d2 ) / r );
    vec4  clr = texture2D ( particleMap, gl_TexCoord [0].xy );

    gl_FragColor = vec4 ( clr.rgb, dz*clr.r );			// use Red as Alpha for RGB greyscale textures
}

soft particle screenshot

Рис. 5. Скриншоты.

Ниже приводится полный исходный код на С++, использованный для рендеринга сцены с рис. 2 и 5.

//
// Example of soft particles in OpenGL
//
// Author: Alex V. Boreskoff <steps3d@narod.ru>
//

#include    "libExt.h"

#ifdef  MACOSX
    #include    <GLUT/glut.h>
#else
    #include    <glut.h>
#endif

#include    <stdio.h>
#include    <stdlib.h>

#include    "libTexture.h"
#include    "TypeDefs.h"
#include    "Vector3D.h"
#include    "Vector2D.h"
#include    "boxes.h"
#include    "GlslProgram.h"
#include    "utils.h"
#include    "Camera.h"

Vector3D    eye   ( -1.5, -1.5, 1.5 );  // camera position
unsigned    decalMap;                   // decal (diffuse) texture
unsigned    stoneMap;
unsigned    teapotMap;
unsigned    depthMap;
unsigned    particleMap;

float   angle = 0;
float   yaw   = 0;
float   pitch = 0;
float   roll  = 0;

Camera      camera ( eye, 0, 0, 0 );    // camera to be used

GlslProgram program;

void displayBoxes ();
void reshape      ( int w, int h );

void displayBoxes ()
{
    glMatrixMode ( GL_MODELVIEW );
    glPushMatrix ();

    drawBox  ( Vector3D ( -5, -5, 0 ), Vector3D ( 10, 10, 3 ),  stoneMap, false );
    drawBox  ( Vector3D ( 3,  2, 0 ), Vector3D ( 1,  2,  1.5 ), decalMap, true  );
    drawBox  ( Vector3D ( 1,  3, 0 ), Vector3D ( 1,  1,  1.5 ), decalMap, true  );
    drawBox  ( Vector3D ( -2, 1, 0 ), Vector3D ( 1,  1,  1.5 ), decalMap, true  );

    glBindTexture   ( GL_TEXTURE_2D, teapotMap );
    glTranslatef    ( 0.2, 1, 0.7 );
    glRotatef       ( angle * 45.3, 1, 0, 0 );
    glRotatef       ( angle * 57.2, 0, 1, 0 );
    glutSolidTeapot ( 0.5 );

    glPopMatrix     ();
}

void    displayParticle ( const Vector3D& pos, float r )
{
    glMultiTexCoord4f ( GL_TEXTURE1_ARB, pos.x, pos.y, pos.z, r );
    
                                                    // now setup particle
    glBegin ( GL_QUADS );
    
    glMultiTexCoord2f ( GL_TEXTURE0_ARB, 0, 0 );
    glVertex3fv  ( pos );
    
    glMultiTexCoord2f ( GL_TEXTURE0_ARB, 0, 1 );
    glVertex3fv  ( pos );
    
    glMultiTexCoord2f ( GL_TEXTURE0_ARB, 1, 1 );
    glVertex3fv  ( pos );
    
    glMultiTexCoord2f ( GL_TEXTURE0_ARB, 1, 0 );
    glVertex3fv  ( pos );
    
    glEnd ();
}

void display ()
{
    glClear ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

    camera.apply ();
    displayBoxes ();

    glActiveTextureARB ( GL_TEXTURE1_ARB );
    glBindTexture      ( GL_TEXTURE_2D, particleMap );

    glActiveTextureARB ( GL_TEXTURE0_ARB );
    glBindTexture      ( GL_TEXTURE_RECTANGLE_ARB, depthMap );
    glCopyTexImage2D   ( GL_TEXTURE_RECTANGLE_ARB, 0, GL_DEPTH_COMPONENT, 0, 0, 640, 480, 0 );

    
    glDepthMask ( GL_FALSE );
    glEnable    ( GL_BLEND );
    glBlendFunc ( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
    
    program.bind ();
    
    displayParticle ( Vector3D ( 0, 0, 0.5 ), 1 );
    displayParticle ( Vector3D ( 1, 1, 0.5 ), 1 );
    displayParticle ( Vector3D ( 1, 2, 0.5 ), 1 );
    
    program.unbind ();
    
    glDepthMask ( GL_TRUE );
    glDisable   ( GL_BLEND );
    
    glutSwapBuffers ();
}

void reshape ( int w, int h )
{
    camera.setViewSize ( w, h, 60 );
    camera.apply       ();
}

void key ( unsigned char key, int x, int y )
{
    if ( key == 27 || key == 'q' || key == 'Q' )        // quit requested
        exit ( 0 );
    else
    if ( key == 'w' || key == 'W' )
        camera.moveBy ( camera.getViewDir () * 0.2 );
    else
    if ( key == 'x' || key == 'X' )
        camera.moveBy ( -camera.getViewDir () * 0.2 );
    else
    if ( key == 'a' || key == 'A' )
        camera.moveBy ( -camera.getSideDir () * 0.2 );
    else
    if ( key == 'd' || key == 'D' )
        camera.moveBy ( camera.getSideDir () * 0.2 );

    glutPostRedisplay ();
}

void    specialKey ( int key, int x, int y )
{
    if ( key == GLUT_KEY_UP )
        yaw += M_PI / 90;
    else
    if ( key == GLUT_KEY_DOWN )
        yaw -= M_PI / 90;
    else
    if ( key == GLUT_KEY_RIGHT )
        roll += M_PI / 90;
    else
    if ( key == GLUT_KEY_LEFT )
        roll -= M_PI / 90;

    camera.setEulerAngles ( yaw, pitch, roll );

    glutPostRedisplay ();
}

void    mouseFunc ( int x, int y )
{
    static  int lastX = -1;
    static  int lastY = -1;

    if ( lastX == -1 )              // not initialized
    {
        lastX = x;
        lastY = y;
    }

    yaw  -= (y - lastY) * 0.02;
    roll += (x - lastX) * 0.02;

    lastX = x;
    lastY = y;

    camera.setEulerAngles ( yaw, pitch, roll );

    glutPostRedisplay ();
}

void    animate ()
{
    static  float   lastTime = 0.0;
    float           time     = 0.001f * glutGet ( GLUT_ELAPSED_TIME );

    angle += 2 * (time - lastTime);

    lastTime = time;

    glutPostRedisplay ();
}

int main ( int argc, char * argv [] )
{
                                // initialize glut
    glutInit            ( &argc, argv );
    glutInitDisplayMode ( GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH );
    glutInitWindowSize  ( 640, 480 );


                                // create window
    glutCreateWindow ( "OpenGL Soft Particles example" );

                                // register handlers
    glutDisplayFunc       ( display    );
    glutReshapeFunc       ( reshape    );
    glutKeyboardFunc      ( key        );
    glutSpecialFunc       ( specialKey );
    glutPassiveMotionFunc ( mouseFunc );
    glutIdleFunc          ( animate    );

    init           ();
    initExtensions ();

    assertExtensionsSupported ( "EXT_framebuffer_object" );

    decalMap    = createTexture2D ( true, "../../Textures/oak.bmp" );
    stoneMap    = createTexture2D ( true, "../../Textures/block.bmp" );
    teapotMap   = createTexture2D ( true, "../../Textures/Oxidated.jpg" );
    particleMap = createTexture2D ( true, "maskSmoke.bmp" );
    
    if ( !program.loadShaders ( "sf.vsh", "sf.fsh" ) )
    {
        printf ( "Error loading shaders:\n%s\n", program.getLog ().c_str () );

        return 3;
    }
    
                                // create depth texture
    glGenTextures   ( 1, &depthMap );
    glBindTexture   ( GL_TEXTURE_RECTANGLE_ARB, depthMap );
    glTexParameteri ( GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
    glTexParameteri ( GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
    glTexParameteri ( GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
    glTexParameteri ( GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_MIN_FILTER, GL_NEAREST );

    glTexImage2D     ( GL_TEXTURE_RECTANGLE_ARB, 0, GL_DEPTH_COMPONENT, 640, 480, 0,
                       GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE, NULL );
    
    program.bind ();
    program.setTexture  ( "depthMap",    0 );
    program.setTexture  ( "particleMap", 1 );
    program.unbind      ();

    camera.setRightHanded ( false );
    
    glutMainLoop ();

    return 0;
}

По этой ссылке можно скачать весь исходный код к этой статье. Также доступны для скачивания откомпилированные версии для M$ Windows и Linux.