Tutorial 21 - Alpha Blending
Fecha Viernes, 04 julio a las 19:24:01
Tema DirectX/Opengl


Vamos con un nuevo tutorial en el que veremos cómo utilizar la técnica de "alpha blending" para producir un efecto de destello mediante una textura que usaremos como disparo. También renderizaremos elementos en primer plano usando esta técnica (en el punto de mira) y la técnica de "color key" que ya vimos en tutoriales anteriores para renderizar la cabina.



Bien, la técnica de "alpha blending" consiste en algo así como mezclar colores. En el fondo, lo que tendremos será una función para "mezclar colores" y si concretamente mezclamos el canal alpha con los colores que ya han sido renderizados previamente, pues lo que tendrémos será una mezcla con el canal alpha.

En el Tutorial 08 ya definimos nuestra función de mezclado:


    ///////////////////////////////////////////////////////////////////////////////
    ///     SetBlending: Fija los valores de mezclado de los canales RGBA.
    ///
    ///     @param  e_BLEND_FACTOR sourceBlendFactor: valor con el que se mezcla.
    ///     @param  e_BLEND_FACTOR destinationBlendFactor: valor de mezclado del color
    ///                 que ya se encuentra en el buffer.
    ///
    ///     @return  nada
    ///////////////////////////////////////////////////////////////////////////////
    void SetBlending( e_BLEND_FACTOR sourceBlendFactor, e_BLEND_FACTOR destinationBlendFactor= eBlendDisable );

en el que se expresa el color origen a mezclar y el valor de mezclado que ya se ha enviado a renderizar (en el framebuffer).

En el fichero 3dstructures.h podréis ver los valores que pueden especificarse:


    ///////////////////////////////////////////////////////////////////////////////
    /// enum   e_BLEND_FACTOR Valores de mezclado.
    ///////////////////////////////////////////////////////////////////////////////
    enum e_BLEND_FACTOR
    {
        eBlendZero,             ///< Factor blend: (0, 0, 0, 0)
        eBlendOne,              ///< Factor blend: (1, 1, 1, 1)

        // DESTINATION
        eBlendDestColor,        ///< Factor blend: (Rd, Gd, Bd, Ad)
        eBlendOneMinusDestColor,///< Factor blend: (1-R, 1-G, 1-B, 1-A)
        eBlendInvDestColor,     ///< Factor blend: (1-Rd, 1-Gd, 1-Bd, 1-Ad)

        eBlendDestAlpha,        ///< Factor blend: (Ad, Ad, Ad, Ad)
        eBlendOneMinusDestAlpha,///< Factor blend: (1-Ad, 1-Ad, 1-Ad, 1-Ad).

        // SOURCE
        eBlendSrcColor,         ///< Factor blend: (Rs, Gs, Bs, As)
        eBlendOneMinusSrcColor, ///< Factor blend: (1-Rs, 1-Gs, 1-Bs, 1-As)

        eBlendSrcAlpha,         ///< Factor blend: (As, As, As, As)
        eBlendOneMinusSrcAlpha, ///< Factor blend: (1-As, 1-As, 1-As, 1-As)
        eBlendSrcAlphaSat,      ///< Factor blend: (f, f, f, 1); f = min(As, 1-Ad)

        eBlendDisable           ///< Desactiva el blending.
    };

Así como una nueva definición de vértice que vamos a utilizar para renderizar los destellos y los elementos de la cabina:


    ///////////////////////////////////////////////////////////////////////////////
    /// struct   VertexBlend: Vértice para mezclas.
    ///
    /// rief
    /// author  Antonio Lucas Moreno version 1.00 date 06/05/2008 13:48:10
    ///////////////////////////////////////////////////////////////////////////////
    struct VertexBlend
    {
        float           x, y, z;            ///< Coordenadas del vértice.
        ulColorRGBA     color;              ///< Color del vértice.
        float           su, tv;             ///< Coordenadas de textura.
    };

He creado una nueva clase "Particle" que gestiona el movimiento, el ciclo de vida de la partícula y su renderizado.

La función de renderizado fija la matriz de transformación para actualizar su posición. Posteriormente seleccionamos el canal alpha de la textura y "mezclamos" con lo que ya se envió a renderizar en el "framebuffer". La partícula se renderiza como un cuadrado (y par de triángulos) con la textura mapeada sobre ella:


    ///////////////////////////////////////////////////////////////////////////////
    ///     Render: Renderiza la partícula.
    ///
    ///     @param   nada
    ///
    ///     @return  nada
    ///////////////////////////////////////////////////////////////////////////////
    void Particle::Render()
    {
        Matrix transform, worldTransform;

        IVideoDriver *pVideoDriver= IVideoDriver::GetVideoDriver();

        if ( pVideoDriver )
        {
            // Transformamos del espacio local al espacio global.
            transform.Translate( m_vPosition );
            worldTransform= GetLocalMatrix() * transform;

            pVideoDriver->SetTransform( worldTransform );

            pVideoDriver->SetBlending ( eBlendDestAlpha, eBlendOne );
            pVideoDriver->SetTexture  ( m_Texture );

            pVideoDriver->RenderVertexArray( eTriangleStrip, (BYTE*)m_Vertex,
                                             sizeof(VertexBlend), 0, 4,
                                             eFVF_XYZ|eFVF_DIFFUSE|eFVF_TEX1 );
        }
    }

Cada vez que pulsemos el botón izquierdo del ratón, crearemos una nueva partícula desde la posición en la que se encuentra nuestro punto de vista (el de la cámara) y fijamos su velocidad y el tiempo de vida hasta desintegrarse:


    ///////////////////////////////////////////////////////////////////////////////
    ///     OnLButtonDown: Función invocada cuando se pulsa el botón izquierdo ratón.
    ///
    ///     @param  int mouseX: Coordenada X de pantalla.
    ///     @param  int mouseY: Coordenada Y de pantalla.
    ///
    ///     @return  nada
    ///////////////////////////////////////////////////////////////////////////////
    void GraphicApplication::OnLButtonDown( int mouseX, int mouseY )
    {
    // ********************************* NEW **************************************

        char name[32];
        Particle *pParticle= NULL;

        Vector3 vPosition= m_Player.GetPosition() + m_Camera.GetPosition();
        Vector3 vUp      = m_Camera.GetLocalMatrix().GetUpVector();
        Vector3 vLook    = vPosition - m_Camera.GetLocalMatrix().GetForwardVector();

        sprintf( name, "PARTICLE-%d", m_IdParticle++ );

        pParticle= new Particle(name);

        if ( pParticle )
        {
            pParticle->SetPosition      ( vPosition  );
            pParticle->SetOrientation   ( vLook, vUp );
            pParticle->SetTexture       ( m_ParticleTexture );
            pParticle->SetSpeed         ( 0.02f );
            pParticle->SetTimeLife      ( 2000 );

            m_ObjectList[ string(name) ]= pParticle;
        }

        m_msShootEffect= 80;

    // ****************************************************************************
    }

Posteriormente, en el método RenderSceneObjects(), desactivamos la iluminación, activamos el uso de texturas y las mezclas con el "framebuffer".


    // ********************************* NEW **************************************
    else if ( strstr(pObject->GetName(),"PARTICLE") )
    {
        // Renderizamos las partículas con Alpha Blending
        m_pVideoDriver->EnableLighting( false );
        m_pVideoDriver->EnableTexture ( true  );
        m_pVideoDriver->EnableBlending( true  );

        pParticle= dynamic_cast( pObject );
        if ( pParticle )
        {
            if ( pParticle->GetTimeLife() < 0 )
            {
                // Está muerta, la eliminamos
                delete pParticle;
                m_ObjectList.erase( itObj++ );
            }
            else
            {
                pParticle->Render();
                ++itObj;
            }
        }
        m_pVideoDriver->EnableBlending( false );
    }
    // ****************************************************************************

Fijaros bien en que al eliminar la partícula de la lista de objetos: m_ObjectList.erase( itObj++ ), se realiza un incremento del iterador para que apunte al siguiente elemento. Cuando se recorre la lista m_ObjectList en el "for", no se incrementa el iterador, ya que puede que se elimine un elemento como ocurre en el caso anterior. Se incrementa sólo cuando corresponda.

Finalmente, tras renderizar los elementos "3D", renderizamos los del primer plano. Fijémonos en el método OnRender():


    ///////////////////////////////////////////////////////////////////////////////
    ///     OnRender: Esta función es invocada cuando el sistema está listo para renderizar.
    ///
    ///     @param  nada
    ///
    ///     @return  nada
    ///////////////////////////////////////////////////////////////////////////////
    void GraphicApplication::OnRender()
    {
        Matrix mIdentity;
        fColorRGBA clearColor= { 0.0f, 0.0f, 0.8f, 1.0f };  // Color de fondo.

        if ( m_pVideoDriver )
        {
            // Limpiamos la pantalla.
            m_pVideoDriver->ClearColor( clearColor );
            m_pVideoDriver->ClearZBuffer();
            m_pVideoDriver->EnableDepthBuffer( true );

            // Proyección en perspectiva
            m_pVideoDriver->SetPerspective( FOV, m_WindowProps.width, m_WindowProps.height );

            Vector3 vPosition= m_Player.GetPosition() + m_Camera.GetPosition();


            Vector3 vUp      = m_Camera.GetLocalMatrix().GetUpVector();
            Vector3 vLook    = vPosition - m_Camera.GetLocalMatrix().GetForwardVector();

            m_pVideoDriver->LookAt( vPosition, vLook, vUp );

            m_pVideoDriver->BeginRender();

            m_pVideoDriver->SetTransform( mIdentity );          // Reseteamos las transformaciones previas.

            m_pVideoDriver->EnableLighting( false );            // Desactivamos la iluminación.
            m_pVideoDriver->EnableTexture ( false );            // Desactivamos el uso de texturas.
            RenderFloor ( 20, 20 );                             // Renderizamos el suelo.

            if ( m_bWireframe )
            {
                m_pVideoDriver->SetRenderMode ( eWireFrame );   // Renderizado wireframe
                m_pVideoDriver->SetCullFaces  ( eCullNone  );   // Desactivamos la selección de caras a renderizar.
            }
            else
            {
                m_pVideoDriver->SetRenderMode ( eSolid     );   // Renderizado sólido.
                m_pVideoDriver->SetCullFaces  ( eCullBack  );   // No se renderizarán las caras traseras.
            }

            m_pVideoDriver->EnableLighting( true );             // Activamos la iluminación.
            RenderSceneObjects();                               // Renderizamos las geometrías.
            m_pVideoDriver->EnableLighting( false );            // Desactivamos la iluminación.

    // ********************************* NEW **************************************

            // Reseteamos las transformaciones previas (si no, en DirectX no va bien)
            m_pVideoDriver->SetTransform( mIdentity );

            // Reseteamos la cámara (si no, en DirectX no va bien)
            m_pVideoDriver->LookAt( Vector3( 0.0f, 0.0f, 0.1f ) ,   // Posición de la cámara.
                                    Vector3( 0.0f, 0.0f, 0.0f ) ,   // A dónde mira.
                                    Vector3( 0.0f, 1.0f, 0.0f ) );  // ¿Qué es arriba? (el eje Y).

            // Fijamos la perspectiva ortogonal
            m_pVideoDriver->SetOrthoProjection( -m_WindowProps.width /2, m_WindowProps.width /2,
                                                -m_WindowProps.height/2, m_WindowProps.height/2,
                                                -1.0f, 1.0f );
            // Renderizamos el punto de mira
            RenderCrosshair();

            // Renderizamos la cabina
            RenderCockpit();

    // ****************************************************************************

            m_pVideoDriver->EndRender();
        }
    }

Vemos cómo el truco consiste en cambiar de la proyección 3D a una proyección ortogonal en 2D. Tras lo cual renderizamos el punto de mira y la cabina.

A continuación vemos que las renderizamos como simples elementos 2D, un cuadrado/rectánculo (un par de triángulos) con una textura mapeada en ellos. En el caso del punto de mira lo renderizamos mediante "alpha blending" y la cabina con "color key" haciendo que todo lo que sea color fuxia no se mezcle con lo que ya se encuentra en el "framebuffer".


    ///////////////////////////////////////////////////////////////////////////////
    ///     RenderCrosshair: Renderiza el punto de mira.
    ///
    ///     @param  nada
    ///
    ///     @return  nada
    ///////////////////////////////////////////////////////////////////////////////
    void GraphicApplication::RenderCrosshair()
    {
        // Definimos un rectángulo (formado por dos triángulos consecutivos)
        //
        // v0   v2
        //  *---*
        //  |  /|
        //  |/  |
        //  *---*
        // v1   v3
        //
        static VertexBlend crosshairVertex[4] =
        {
        //      x  ,   y  ,  z  ,    color  , su,tv
            {-50.0f, 50.0f, 0.5f, 0xffffffff,  0,1 },   /* v0 */
            {-50.0f,-50.0f, 0.5f, 0xffffffff,  0,0 },   /* v1 */
            { 50.0f, 50.0f, 0.5f, 0xffffffff,  1,1 },   /* v2 */
            { 50.0f,-50.0f, 0.5f, 0xffffffff,  1,0 }    /* v3 */
        };

        if ( m_pVideoDriver )
        {
            m_pVideoDriver->EnableTexture ( true );
            m_pVideoDriver->EnableBlending( true );

            // Alpha blending
            m_pVideoDriver->SetBlending ( /*fuente*/eBlendDestAlpha, /*destino*/eBlendOne );
            m_pVideoDriver->SetTexture  ( m_CrosshairTexture );

            m_pVideoDriver->RenderVertexArray( eTriangleStrip, (BYTE*)crosshairVertex,
                                               sizeof(VertexBlend), 0, 4,
                                               eFVF_XYZ|eFVF_DIFFUSE|eFVF_TEX1 );

            m_pVideoDriver->EnableBlending( false );
        }
    }

Fijáos en el truco de usar dos texturas en la cabina para dar la sensación de que ésta se ilumna cuando disparamos. Para ello he definido la variable m_msShootEffect que indica durante cuánto tiempo debe mostrarse la cabina iluminada.


    ///////////////////////////////////////////////////////////////////////////////
    ///     RenderCockpit: Renderiza la cabina.
    ///
    ///     @param  nada
    ///
    ///     @return  nada
    ///////////////////////////////////////////////////////////////////////////////
    void GraphicApplication::RenderCockpit()
    {
        // Definimos un rectángulo (formado por dos triángulos consecutivos)
        //
        // v0   v2
        //  *---*
        //  |  /|
        //  |/  |
        //  *---*
        // v1   v3
        //
        static VertexBlend cockpitVertex[4] =
        {
        //      x   ,   y   ,  z  ,    color  , su,tv
            {-320.0f, 240.0f, 0.5f, 0xffffffff,  0,1 }, /* v0 */
            {-320.0f,-240.0f, 0.5f, 0xffffffff,  0,0 }, /* v1 */
            { 320.0f, 240.0f, 0.5f, 0xffffffff,  1,1 }, /* v2 */
            { 320.0f,-240.0f, 0.5f, 0xffffffff,  1,0 }  /* v3 */
        };

        if ( m_pVideoDriver )
        {
            m_pVideoDriver->EnableTexture ( true );
            m_pVideoDriver->EnableBlending( true );

            // Blend color-key
            m_pVideoDriver->SetBlending( /*fuente*/eBlendSrcAlpha, /*destino*/eBlendOneMinusSrcAlpha );

            if ( m_msShootEffect > 0 )
                m_pVideoDriver->SetTexture( m_CockpitTexture02 );
            else
                m_pVideoDriver->SetTexture( m_CockpitTexture01 );

            m_pVideoDriver->RenderVertexArray( eTriangleStrip, (BYTE*)cockpitVertex,
                                               sizeof(VertexBlend), 0, 4,
                                               eFVF_XYZ|eFVF_DIFFUSE|eFVF_TEX1 );

            m_pVideoDriver->EnableBlending( false );
        }
    }

Podéis descargaros el ejemplo: tutorial-21.zip



Compartir en Facebook



Este artículo proviene de Programacion Grafica: Desarrollo 2D/3D con C++ y DirectX/OpenGL/GLUT/SDL - Windows/Linux
http://www.programaciongrafica.com

La dirección de esta noticia es:
http://www.programaciongrafica.com/modules.php?name=News&file=article&sid=24