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

Como otros han discutido , GLSL no tiene ningún tipo de depuración de printf. Pero a veces realmente quiero examinar 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 un sampler2D en el que los dígitos 0123456789 se han renderizado en monoespacio. Básicamente, solo haces malabares con tu coordenada x.

Ahora, para usar esto para examinar un número de coma flotante, necesito un algoritmo para convertir un float en una secuencia de dígitos decimales, como puede encontrar en cualquier implementación de printf . Desafortunadamente, por lo que entiendo del tema, estos algoritmos parecen necesitar volver a representar el número de coma flotante en un formato de mayor precisión, y no veo cómo va a ser posible en GLSL, donde parece que tengo solo float 32 bits disponibles. Por esta razón, creo que esta pregunta no es un duplicado de ninguna pregunta general sobre “cómo funciona printf”, sino específicamente sobre cómo se puede hacer que esos algoritmos funcionen bajo las restricciones de GLSL. He visto esta pregunta y respuesta , pero no tengo idea de lo que está pasando allí.

Los algoritmos que he probado no son muy buenos. Mi primer bash, marcada Versión A (comentada) parecía bastante mala: tomar tres ejemplos aleatorios, RenderDecimal(1.0) representado como 1.099999702 , RenderDecimal(2.5) me dio 2.599999246 y RenderDecimal(2.6) salió como 2.699999280 . Mi segundo bash, marcado Versión B, parecía algo mejor: 1.0 y 2.6 ambos salen bien, pero RenderDecimal(2.5) aún no coincide con un aparente redondeo del 5 con el hecho de que el residual es 0.099... El resultado aparece como 2.599000022 .

Mi ejemplo mínimo / completo / verificable, a continuación, comienza con un código GLSL 1.20 corto, y luego he elegido Python 2.x para el rest, solo para obtener los sombreadores comstackdos y las texturas cargadas y renderizadas. Requiere los paquetes de pygame, numpy, PyOpenGL y PIL de terceros. Tenga en cuenta que el Python es en realidad una repetición y podría ser trivial (aunque tediosamente) reescrito en C o cualquier otra cosa. Solo el código GLSL en la parte superior es fundamental para esta pregunta, y por esta razón, no creo que las tags python o python 2.x sean útiles.

Requiere la siguiente imagen para guardarse como digits.png :

0123456789

 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 = 0 && pos.y = 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.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  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() 

+1 por problema interesante. Era curioso así que traté de codificar esto. Necesito el uso de matrices, así que elegí #version 420 core . Mi aplicación está renderizando una sola pantalla que cubre cuatro cuadrantes con coordenadas <-1,+1> . Estoy usando una textura de fuente de caracteres enteros ASCII 8×8 píxeles 32×8 que creé hace algunos años:

fuente

El vértice es simple:

 //--------------------------------------------------------------------------- // Vertex //--------------------------------------------------------------------------- #version 420 core //--------------------------------------------------------------------------- layout(location=0) in vec4 vertex; out vec2 pos; // screen position <-1,+1> void main() { pos=vertex.xy; gl_Position=vertex; } //--------------------------------------------------------------------------- 

Fragmento es un poco más complicado:

 //--------------------------------------------------------------------------- // Fragment //--------------------------------------------------------------------------- #version 420 core //--------------------------------------------------------------------------- in vec2 pos; // screen position <-1,+1> out vec4 gl_FragColor; // fragment output color uniform sampler2D txr_font; // ASCII 32x8 characters font texture unit uniform float fxs,fys; // font/screen resolution ratio //--------------------------------------------------------------------------- const int _txtsiz=32; // text buffer size int txt[_txtsiz],txtsiz; // text buffer and its actual size vec4 col; // color interface for txt_print() //--------------------------------------------------------------------------- void txt_decimal(float x) // print float x into txt { int i,j,c; // l is size of string float y,a; const float base=10; // handle sign if (x<0.0) { txt[txtsiz]='-'; txtsiz++; x=-x; } else { txt[txtsiz]='+'; txtsiz++; } // divide to int(x).fract(y) parts of number y=x; x=floor(x); y-=x; // handle integer part i=txtsiz; // start of integer part for (;txtsiz<_txtsiz;) { a=x; x=floor(x/base); a-=base*x; txt[txtsiz]=int(a)+'0'; txtsiz++; if (x<=0.0) break; } j=txtsiz-1; // end of integer part for (;ifloat(txtsiz))||(y<0.0)||(y>1.0)) return; // get font texture position for target ASCII i=int(x); // char index in txt x-=float(i); i=txt[i]; x+=float(int(i&31)); y+=float(int(i>>5)); x/=32.0; y/=8.0; // offset in char texture col=texture2D(txr_font,vec2(x,y)); } //--------------------------------------------------------------------------- void main() { col=vec4(0.0,1.0,0.0,1.0); // background color txtsiz=0; txt[txtsiz]='F'; txtsiz++; txt[txtsiz]='l'; txtsiz++; txt[txtsiz]='o'; txtsiz++; txt[txtsiz]='a'; txtsiz++; txt[txtsiz]='t'; txtsiz++; txt[txtsiz]=':'; txtsiz++; txt[txtsiz]=' '; txtsiz++; txt_decimal(12.345); txt_print(1.0,1.0); gl_FragColor=col; } //--------------------------------------------------------------------------- 

Aquí mis uniformes laterales de CPU:

  glUniform1i(glGetUniformLocation(prog_id,"txr_font"),0); glUniform1f(glGetUniformLocation(prog_id,"fxs"),(8.0)/float(xs)); glUniform1f(glGetUniformLocation(prog_id,"fys"),(8.0)/float(ys)); 

donde xs,ys es mi resolución de pantalla. La fuente es 8×8 en la unidad 0

Aquí la salida para el código de fragmento de prueba:

captura de pantalla

Si la precisión de su punto flotante se reduce debido a la implementación de HW, debería considerar imprimir en hexadecimal, donde no hay pérdida de precisión (mediante acceso binario). Eso podría convertirse en base decadica en enteros más tarde …

ver:

  • conversión de cadena hex2dec en matemáticas enteras

[Edit2] shaders GLSL de estilo antiguo

Traté de portar al viejo estilo GLSL y de repente funciona (antes no comstackría con matrices presentes pero cuando lo pienso intenté char[] que fue la verdadera razón).

 //--------------------------------------------------------------------------- // Vertex //--------------------------------------------------------------------------- varying vec2 pos; // screen position <-1,+1> void main() { pos=gl_Vertex.xy; gl_Position=gl_Vertex; } //--------------------------------------------------------------------------- 
 //--------------------------------------------------------------------------- // Fragment //--------------------------------------------------------------------------- varying vec2 pos; // screen position <-1,+1> uniform sampler2D txr_font; // ASCII 32x8 characters font texture unit uniform float fxs,fys; // font/screen resolution ratio //--------------------------------------------------------------------------- const int _txtsiz=32; // text buffer size int txt[_txtsiz],txtsiz; // text buffer and its actual size vec4 col; // color interface for txt_print() //--------------------------------------------------------------------------- void txt_decimal(float x) // print float x into txt { int i,j,c; // l is size of string float y,a; const float base=10.0; // handle sign if (x<0.0) { txt[txtsiz]='-'; txtsiz++; x=-x; } else { txt[txtsiz]='+'; txtsiz++; } // divide to int(x).fract(y) parts of number y=x; x=floor(x); y-=x; // handle integer part i=txtsiz; // start of integer part for (;txtsiz<_txtsiz;) { a=x; x=floor(x/base); a-=base*x; txt[txtsiz]=int(a)+'0'; txtsiz++; if (x<=0.0) break; } j=txtsiz-1; // end of integer part for (;ifloat(txtsiz))||(y<0.0)||(y>1.0)) return; // get font texture position for target ASCII i=int(x); // char index in txt x-=float(i); i=txt[i]; x+=float(int(i-((i/32)*32))); y+=float(int(i/32)); x/=32.0; y/=8.0; // offset in char texture col=texture2D(txr_font,vec2(x,y)); } //--------------------------------------------------------------------------- void main() { col=vec4(0.0,1.0,0.0,1.0); // background color txtsiz=0; txt[txtsiz]='F'; txtsiz++; txt[txtsiz]='l'; txtsiz++; txt[txtsiz]='o'; txtsiz++; txt[txtsiz]='a'; txtsiz++; txt[txtsiz]='t'; txtsiz++; txt[txtsiz]=':'; txtsiz++; txt[txtsiz]=' '; txtsiz++; txt_decimal(12.345); txt_print(1.0,1.0); gl_FragColor=col; } //--------------------------------------------------------------------------- 

En primer lugar, quiero mencionar que la increíble solución de Spektre es casi perfecta y más aún una solución general para la salida de texto. Le di una respuesta a su respuesta. Como alternativa, presento una solución mínimamente invasiva y mejoro el código de la pregunta.

No quiero ocultar el hecho de que he estudiado la solución de Spektre e integrado en mi solución.

 // Assume that the texture to which uTextureSlotNumber refers contains // a rendering of the digits '0123456789' packed together, such that const vec2 startOfDigitsInTexture = vec2( 100, 125 ); // the lower-left corner of the first digit starts here and const vec2 sizeOfDigit = vec2( 0.1, 0.2 ); // each digit spans this many pixels const float nSpaces = 10.0; // assume we have this many digits' worth of space to render in void RenderDigit( int strPos, int digit, vec2 pos ) { float testStrPos = pos.x / sizeOfDigit.x; if ( testStrPos >= float(strPos) && testStrPos < float(strPos+1) ) { float start = sizeOfDigit.x * float(digit); vec2 textureSourcePosition = vec2( startOfDigitsInTexture.x + start + mod( pos.x, sizeOfDigit.x ), startOfDigitsInTexture.y + pos.y ); gl_FragColor = texture2D( uTextureSlotNumber, textureSourcePosition / uTextureSize ); } } 

La función ValueToDigits interpreta un número de coma flotante y llena una matriz con los dígitos. Cada número en la matriz está en ( 0 , 9 ).

 const int MAX_DIGITS = 32; int digits[MAX_DIGITS]; int noOfDigits = 0; int posOfComma = 0; void Reverse( int start, int end ) { for ( ; start < end; ++ start, -- end ) { int digit = digits[start]; digits[start] = digits[end]; digits[end] = digit; } } void ValueToDigits( float value ) { const float base = 10.0; int start = noOfDigits; value = abs( value ); float frac = value; value = floor(value); frac -= value; // integral digits for ( ; value > 0.0 && noOfDigits < MAX_DIGITS; ++ noOfDigits ) { float newValue = floor( value / base ); digits[noOfDigits] = int( value - base * newValue ); value = newValue; } Reverse( start, noOfDigits-1 ); posOfComma = noOfDigits; // fractional digits for ( ; frac > 0.0 && noOfDigits < MAX_DIGITS; ++ noOfDigits ) { frac *= base; float digit = floor( frac ); frac -= digit; digits[noOfDigits] = int( digit ); } } 

Llame a ValueToDigits en su función original y encuentre las coordenadas de dígitos y texturas para el fragmento actual.

 void RenderDecimal( float value ) { // fill the array of digits with the floating point value ValueToDigits( value ); // Render the digits vec2 pos = vFragCoordinate.xy - startOfDigitsInTexture; if( pos.x >= 0 && pos.x < sizeOfDigit.x * nSpaces && pos.y >= 0 && pos.y < sizeOfDigit.y ) { // render the digits for ( int strPos = 0; strPos < noOfDigits; ++ strPos ) RenderDigit( strPos, digits[strPos], pos ); } // Render the decimal point float testStrPos = pos.x / sizeOfDigit.x; float remainder = mod( pos.x, sizeOfDigit.x ); if( ( testStrPos >= float(posOfComma) && testStrPos < float(posOfComma+1) && remainder / sizeOfDigit.x < 0.1 && abs( pos.y ) / sizeOfDigit.y < 0.1 ) || ( testStrPos >= float(posOfComma-1) && testStrPos < float(posOfComma) && remainder / sizeOfDigit.x > 0.9 && abs( pos.y ) / sizeOfDigit.y < 0.1 ) ) { gl_FragColor = texture2D( uTextureSlotNumber, ( startOfDigitsInTexture + sizeOfDigit * vec2( 1.5, 0.5 ) ) / uTextureSize ); } } 

Aquí está mi sombreador de fragmentos actualizado, que se puede soltar en la lista en mi pregunta original. Implementa el algoritmo de búsqueda de dígitos decimales propuesto por Spektre, de una manera que es incluso compatible con el dialecto GLSL 1.20 heredado que estoy usando. Sin esa restricción, la solución de Spektre es, por supuesto, mucho más elegante y poderosa.

 varying vec2 vFragCoordinate; uniform vec2 uTextureSize; uniform sampler2D uTextureSlotNumber; float Digit( float x, int position, float base ) { int i; float digit; if( position < 0 ) { x = fract( x ); for( i = -1; i >= position; i-- ) { if( x <= 0.0 ) { digit = 0.0; break; } x *= base; digit = floor( x ); x -= digit; } } else { x = floor( x ); float prevx; for( i = 0; i <= position; i++ ) { if( x <= 0.0 ) { digit = 0.0; break; } prevx = x; x = floor( x / base ); digit = prevx - base * x; } } return digit; } 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 ) ); int decimal_position = int( dpstart - floor( pos.x / sizeOfDigit.x ) ); float remainder = mod( pos.x, sizeOfDigit.x ); if( pos.x >= 0.0 && pos.x < sizeOfDigit.x * nSpaces && pos.y >= 0.0 && pos.y < sizeOfDigit.y ) { float digit_value = Digit( value, 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 && remainder / sizeOfDigit.x < 0.1 && abs( pos.y ) / sizeOfDigit.y < 0.1 ) || ( decimal_position == 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 }