Convertire i numeri in virgola mobile in cifre decimali in GLSL?

Come altri hanno discusso , GLSL non ha alcun tipo di debug di printf. Ma a volte voglio davvero esaminare i valori numerici mentre eseguo il debug dei miei ombreggiatori.

Ho cercato di creare uno strumento di debug visivo. Ho scoperto che è ansible rendere una serie arbitraria di cifre abbastanza facilmente in uno shader, se si lavora con un sampler2D in cui le cifre 0123456789 sono state renderizzate in monospace. Fondamentalmente, ti limiti a coordinare le coordinate x.

Ora, per usare questo per esaminare un numero in virgola mobile, ho bisogno di un algoritmo per convertire un float in una sequenza di cifre decimali, come si potrebbe trovare in qualsiasi implementazione di printf . Sfortunatamente, per quanto comprendo l’argomento questi algoritmi sembrano aver bisogno di ri-rappresentare il numero in virgola mobile in un formato di maggiore precisione, e non vedo come questo sarà ansible in GLSL dove mi sembra di avere sono disponibili solo float 32 bit. Per questa ragione, penso che questa domanda non sia un duplicato di una domanda generale “come funziona printf”, ma piuttosto specificamente su come questi algoritmi possano essere fatti funzionare sotto i vincoli di GLSL. Ho visto questa domanda e risposta , ma non ho idea di cosa sta succedendo lì.

Gli algoritmi che ho provato non sono molto buoni. Il mio primo tentativo, contrassegnato con la versione A (commentato) sembrava piuttosto brutto: prendere tre esempi casuali, RenderDecimal(1.0) reso come 1.099999702 , RenderDecimal(2.5) mi ha dato 2.599999246 e RenderDecimal(2.6) è uscito come 2.699999280 . Il mio secondo tentativo, contrassegnato con Versione B, mi è sembrato leggermente migliore: 1.0 e 2.6 entrambi risultati RenderDecimal(2.5) , ma RenderDecimal(2.5) non corrisponde ancora ad un evidente arrotondamento del 5 con il fatto che il residuo è 0.099... Il risultato appare come 2.599000022 .

Il mio esempio minimale / completo / verificabile, di seguito, inizia con un breve codice GLSL 1.20, e quindi ho scelto Python 2.x per il resto, solo per ottenere gli shader compilati e le trame caricate e renderizzate. Richiede i pacchetti pygame, numpy, PyOpenGL e PIL di terze parti. Si noti che Python è in realtà solo un boilerplate e potrebbe essere banalmente (anche se noiosamente) riscritto in C o qualsiasi altra cosa. Solo il codice GLSL in alto è fondamentale per questa domanda, e per questo motivo non penso che i tag python o python 2.x possano essere utili.

Richiede la seguente immagine per essere salvata come 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 per un problema interessante. Era curioso quindi ho provato a codificarlo. Ho bisogno dell’uso di array, quindi ho scelto #version 420 core . La mia app esegue il rendering di un singolo schermo di copertura quad con le coordinate <-1,+1> . Sto usando un intero font ASCII 8×8 pixel di 32×8 caratteri che ho creato alcuni anni fa:

font

Il vertice è semplice:

 //--------------------------------------------------------------------------- // 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; } //--------------------------------------------------------------------------- 

Il frammento è un po ‘più complicato:

 //--------------------------------------------------------------------------- // 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; } //--------------------------------------------------------------------------- 

Ecco le mie divise laterali 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)); 

dove xs,ys è la mia risoluzione dello schermo. Il carattere è 8×8 nell’unità 0

Qui l’output per il codice del frammento di test:

immagine dello schermo

Se la precisione in virgola mobile diminuisce a causa dell’implementazione HW, dovresti prendere in considerazione la stampa in esadecimale dove non è presente alcuna perdita di precisione (utilizzando l’accesso binario). Questo potrebbe essere convertito in base decadica su interi successivamente …

vedere:

  • conversione stringa hex2dec su matematica intera

[Modifica2] shader GLSL vecchio stile

Ho provato a eseguire il porting su GLSL vecchio stile e all’improvviso funziona (prima non sarebbe compilato con gli array presenti ma quando ci penso provavo char[] che era la vera ragione).

 //--------------------------------------------------------------------------- // 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; } //--------------------------------------------------------------------------- 

Prima di tutto voglio menzionare che la straordinaria soluzione di Spektre è quasi perfetta e ancor più una soluzione generale per l’output di testo. Ho dato la sua risposta un upvote . In alternativa, presento una soluzione minimamente invasiva e migliora il codice della domanda.

Non voglio hide il fatto che ho studiato la soluzione di Spektre e integrato nella mia soluzione.

 // 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 funzione ValueToDigits interpreta un numero in virgola mobile e riempie una matrice con le cifre. Ogni numero nella matrice è in ( 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 ); } } 

Chiama ValueToDigits nella tua funzione originale e trova le coordinate numeriche e textur per il frammento corrente.

 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 ); } } 

Ecco il mio shader di frammenti aggiornato, che può essere inserito nell’elenco nella mia domanda originale. Implementa l’algoritmo di individuazione delle cifre decimali proposto da Spektre, in un modo che è persino compatibile con il dialetto legacy GLSL 1.20 che sto usando. Senza questa limitazione, la soluzione di Spektre è, ovviamente, molto più elegante e potente.

 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 }