¿Convertir números de coma flotante a dígitos decimales en GLSL?

Como otros han discutido, GLSL carece de cualquier tipo de depuración printf. Pero a veces realmente quiero examinar los valores numéricos mientras depuro mis sombreadores.

He estado tratando de crear una herramienta de depuración visual. Descubrí que es posible representar una serie arbitraria de dígitos con bastante facilidad en un sombreador, si trabaja con unsampler2D en el que los dígitos0123456789 han sido renderizados en monoespacio. Básicamente, solo haces malabares con tu coordenada x.

Ahora, para usar esto para examinar un número de punto flotante, necesito un algoritmo para convertir unfloat a una secuencia de dígitos decimales, como puede encontrar en cualquierprintf implementación. Desafortunadamente,hasta donde yo entiendo el tema estos algoritmos parecen necesitar representar nuevamente el número de coma flotante en un formato de mayor precisión, y no veo cómo esto será posible en GLSL, donde parece que solo tengo 32 bitsfloats disponible. Por esta razón, creo que esta pregunta no es un duplicado de ninguna pregunta general sobre "cómo funciona printf", sino más bien específicamente sobre cómo se puede hacer que estos algoritmos funcionen bajo las restricciones de GLSL. He vistoesta pregunta y respuesta, pero no tengo idea de lo que está pasando allí.

Los algoritmos que he probado no son muy buenos. Mi primer intento, marcado como Versión A (comentado), parecía bastante malo: para tomar tres ejemplos aleatorios,RenderDecimal(1.0) prestado como1.099999702, RenderDecimal(2.5) me dio2.599999246 yRenderDecimal(2.6) salió como2.699999280. Mi segundo intento, marcado como Versión B, parecía un poco mejor:1.0 y2.6 ambos salen bien, peroRenderDecimal(2.5) todavía no coincide con un redondeo aparente de la5 con el hecho de que el residual es0.099.... El resultado aparece como2.599000022.

Mi ejemplo mínimo / completo / verificable, a continuación, comienza con un breve código GLSL 1.20, y luego he elegido Python 2.x para el resto, solo para compilar los sombreadores y cargar y renderizar las texturas. Requiere los paquetes de terceros pygame, numpy, PyOpenGL y PIL. Tenga en cuenta que Python es en realidad una plantilla repetitiva y podría reescribirse trivialmente (aunque tediosamente) en C o cualquier otra cosa. Solo el código GLSL en la parte superior es crítico para esta pregunta, y por esta razón no creo que elpython opython 2.x Las etiquetas serían útiles.

Requiere que la siguiente imagen se guarde comodigits.png:

vertexShaderSource = """\

varying vec2 vFragCoordinate;
void main(void)
{
    vFragCoordinate = gl_Vertex.xy;
    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}

"""
fragmentShaderSource = """\

varying vec2      vFragCoordinate;

uniform vec2      uTextureSize;
uniform sampler2D uTextureSlotNumber;

float OrderOfMagnitude( float x )
{
    return x == 0.0 ? 0.0 : floor( log( abs( x ) ) / log( 10.0 ) );
}
void RenderDecimal( float value )
{
    // Assume that the texture to which uTextureSlotNumber refers contains
    // a rendering of the digits '0123456789' packed together, such that
    const vec2 startOfDigitsInTexture = vec2( 0, 0 ); // the lower-left corner of the first digit starts here and
    const vec2 sizeOfDigit = vec2( 100, 125 ); // each digit spans this many pixels
    const float nSpaces = 10.0; // assume we have this many digits' worth of space to render in

    value = abs( value );
    vec2 pos = vFragCoordinate - startOfDigitsInTexture;
    float dpstart = max( 0.0, OrderOfMagnitude( value ) );
    float decimal_position = dpstart - floor( pos.x / sizeOfDigit.x );
    float remainder = mod( pos.x, sizeOfDigit.x );

    if( pos.x >= 0 && pos.x < sizeOfDigit.x * nSpaces && pos.y >= 0 && pos.y < sizeOfDigit.y  )
    {
        float digit_value;

        // Version B
        float dp, running_value = value;
        for( dp = dpstart; dp >= decimal_position; dp -= 1.0 )
        {
            float base = pow( 10.0, dp );
            digit_value = mod( floor( running_value / base ), 10.0 );
            running_value -= digit_value * base;
        }

        // Version A
        //digit_value = mod( floor( value * pow( 10.0, -decimal_position ) ), 10.0 );



        vec2 textureSourcePosition = vec2( startOfDigitsInTexture.x + remainder + digit_value * sizeOfDigit.x, startOfDigitsInTexture.y + pos.y );
        gl_FragColor = texture2D( uTextureSlotNumber, textureSourcePosition / uTextureSize );
    }

    // Render the decimal point
    if( ( decimal_position == -1.0 && remainder / sizeOfDigit.x < 0.1 && abs( pos.y ) / sizeOfDigit.y < 0.1 ) ||
        ( decimal_position ==  0.0 && remainder / sizeOfDigit.x > 0.9 && abs( pos.y ) / sizeOfDigit.y < 0.1 ) )
    {
        gl_FragColor = texture2D( uTextureSlotNumber, ( startOfDigitsInTexture + sizeOfDigit * vec2( 1.5, 0.5 ) ) / uTextureSize );
    }
}

void main(void)
{
    gl_FragColor = texture2D( uTextureSlotNumber, vFragCoordinate / uTextureSize );
    RenderDecimal( 2.5 ); // for current demonstration purposes, just a constant
}

"""

# Python (PyOpenGL) code to demonstrate the above
# (Note: the same OpenGL calls could be made from any language)

import os, sys, time

import OpenGL
from OpenGL.GL import *
from OpenGL.GLU import *

import pygame, pygame.locals # just for getting a canvas to draw on

try: from PIL import Image  # PIL.Image module for loading image from disk
except ImportError: import Image  # old PIL didn't package its submodules on the path

import numpy # for manipulating pixel values on the Python side

def CompileShader( type, source ):
    shader = glCreateShader( type )
    glShaderSource( shader, source )
    glCompileShader( shader )
    result = glGetShaderiv( shader, GL_COMPILE_STATUS )
    if result != 1:
        raise Exception( "Shader compilation failed:\n" + glGetShaderInfoLog( shader ) )
    return shader

class World:
    def __init__( self, width, height ):

        self.window = pygame.display.set_mode( ( width, height ), pygame.OPENGL | pygame.DOUBLEBUF )

        # compile shaders
        vertexShader = CompileShader( GL_VERTEX_SHADER, vertexShaderSource )
        fragmentShader = CompileShader( GL_FRAGMENT_SHADER, fragmentShaderSource )
        # build shader program
        self.program = glCreateProgram()
        glAttachShader( self.program, vertexShader )
        glAttachShader( self.program, fragmentShader )
        glLinkProgram( self.program )
        # try to activate/enable shader program, handling errors wisely
        try:
            glUseProgram( self.program )
        except OpenGL.error.GLError:
            print( glGetProgramInfoLog( self.program ) )
            raise

        # enable alpha blending
        glTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE )
        glEnable( GL_DEPTH_TEST )
        glEnable( GL_BLEND )
        glBlendEquation( GL_FUNC_ADD )
        glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA )

        # set projection and background color
        gluOrtho2D( 0, width, 0, height )
        glClearColor( 0.0, 0.0, 0.0, 1.0 )

        self.uTextureSlotNumber_addr = glGetUniformLocation( self.program, 'uTextureSlotNumber' )
        self.uTextureSize_addr = glGetUniformLocation( self.program, 'uTextureSize' )

    def RenderFrame( self, *textures ):
        glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT )
        for t in textures: t.Draw( world=self )
        pygame.display.flip()

    def Close( self ):
        pygame.display.quit()

    def Capture( self ):
        w, h = self.window.get_size()
        rawRGB = glReadPixels( 0, 0, w, h, GL_RGB, GL_UNSIGNED_BYTE )
        return Image.frombuffer( 'RGB', ( w, h ), rawRGB, 'raw', 'RGB', 0, 1 ).transpose( Image.FLIP_TOP_BOTTOM )

class Texture:
    def __init__( self, source, slot=0, position=(0,0,0) ):

        # wrangle array
        source = numpy.array( source )
        if source.dtype.type not in [ numpy.float32, numpy.float64 ]: source = source.astype( float ) / 255.0
        while source.ndim < 3: source = numpy.expand_dims( source, -1 )
        if source.shape[ 2 ] == 1: source = source[ :, :, [ 0, 0, 0 ] ]    # LUMINANCE -> RGB
        if source.shape[ 2 ] == 2: source = source[ :, :, [ 0, 0, 0, 1 ] ] # LUMINANCE_ALPHA -> RGBA
        if source.shape[ 2 ] == 3: source = source[ :, :, [ 0, 1, 2, 2 ] ]; source[ :, :, 3 ] = 1.0  # RGB -> RGBA
        # now it can be transferred as GL_RGBA and GL_FLOAT

        # housekeeping
        self.textureSize = [ source.shape[ 1 ], source.shape[ 0 ] ]
        self.textureSlotNumber = slot
        self.textureSlotCode = getattr( OpenGL.GL, 'GL_TEXTURE%d' % slot )
        self.listNumber = slot + 1
        self.position = list( position )

        # transfer texture content
        glActiveTexture( self.textureSlotCode )
        self.textureID = glGenTextures( 1 )
        glBindTexture( GL_TEXTURE_2D, self.textureID )
        glEnable( GL_TEXTURE_2D )
        glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA32F, self.textureSize[ 0 ], self.textureSize[ 1 ], 0, GL_RGBA, GL_FLOAT, source[ ::-1 ] )
        glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST )
        glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST )

        # define surface
        w, h = self.textureSize
        glNewList( self.listNumber, GL_COMPILE )
        glBegin( GL_QUADS )
        glColor3f( 1, 1, 1 )
        glNormal3f( 0, 0, 1 )
        glVertex3f( 0, h, 0 )
        glVertex3f( w, h, 0 )
        glVertex3f( w, 0, 0 )
        glVertex3f( 0, 0, 0 )
        glEnd()
        glEndList()

    def Draw( self, world ):
        glPushMatrix()
        glTranslate( *self.position )
        glUniform1i( world.uTextureSlotNumber_addr, self.textureSlotNumber )
        glUniform2f( world.uTextureSize_addr, *self.textureSize )
        glCallList( self.listNumber )
        glPopMatrix()


world = World( 1000, 800 )
digits = Texture( Image.open( 'digits.png' ) )
done = False
while not done:
    world.RenderFrame( digits )
    for event in pygame.event.get():
        # Press 'q' to quit or 's' to save a timestamped snapshot
        if event.type  == pygame.locals.QUIT: done = True
        elif event.type == pygame.locals.KEYUP and event.key in [ ord( 'q' ), 27 ]: done = True
        elif event.type == pygame.locals.KEYUP and event.key in [ ord( 's' ) ]:
            world.Capture().save( time.strftime( 'snapshot-%Y%m%d-%H%M%S.png' ) )
world.Close()

Respuestas a la pregunta(3)

Su respuesta a la pregunta