Converter números de ponto flutuante em dígitos decimais em GLSL?

Como outros discutiram, O GLSL não possui nenhum tipo de depuração printf. Mas às vezes eu realmente quero examinar valores numéricos enquanto depuro meus shaders.

Eu tenho tentado criar uma ferramenta de depuração visual. Descobri que é possível renderizar uma série arbitrária de dígitos com bastante facilidade em um sombreador, se você trabalhar com umsampler2D em que os dígitos0123456789 foram renderizados em espaço único. Basicamente, você apenas manipula sua coordenada x.

Agora, para usar isso para examinar um número de ponto flutuante, preciso de um algoritmo para converter umfloat para uma sequência de dígitos decimais, como você pode encontrar em qualquerprintf implementação. Infelizmente,tanto quanto eu entendo o tópico esses algoritmos parecem precisar representar novamente o número de ponto flutuante em um formato de maior precisão, e não vejo como isso será possível no GLSL, onde pareço ter apenas 32 bitsfloats disponíveis. Por esse motivo, acho que essa pergunta não é uma duplicata de nenhuma pergunta geral "como funciona a impressão", mas especificamente sobre como esses algoritmos podem ser feitos para funcionar sob as restrições do GLSL. eu tenho vistoesta pergunta e resposta, mas não faço ideia do que está acontecendo lá.

Os algoritmos que tentei não são muito bons. Minha primeira tentativa, marcada como Versão A (comentada), pareceu muito ruim: para pegar três exemplos aleatórios,RenderDecimal(1.0) processado como1.099999702, RenderDecimal(2.5) deu-me2.599999246 eRenderDecimal(2.6) saiu como2.699999280. Minha segunda tentativa, marcada Versão B, parecia um pouco melhor:1.0 e2.6 ambos saem bem, masRenderDecimal(2.5) ainda não corresponde a um aparente arredondamento da5 com o fato de que o residual é0.099.... O resultado aparece como2.599000022.

Meu exemplo mínimo / completo / verificável, abaixo, começa com um código GLSL 1.20 abreviado e, por acaso, escolhi o Python 2.x para o resto, apenas para compilar os shaders e as texturas carregadas e renderizadas. Requer os pacotes de terceiros pygame, numpy, PyOpenGL e PIL. Observe que o Python é realmente apenas clichê e pode ser trivialmente (embora tediosamente) reescrito em C ou qualquer outra coisa. Somente o código GLSL na parte superior é crítico para esta pergunta e, por esse motivo, não acho que opython oupython 2.x tags seria útil.

Requer que a imagem a seguir seja salva 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()

questionAnswers(3)

yourAnswerToTheQuestion